增加卡片交互流式输出的能力 (#315)

* 将ai交互切换为go-openai

* add stream

*  feat(stream): 优化流式响应机制,实现实时卡片更新

- 将固定1.5秒更新改为基于300ms最小间隔的实时更新策略
- 新增内容缓冲区机制,避免过于频繁的API调用
- 改进流式中断处理,保护已接收的内容不丢失

🔧 chore(llm): 优化HTTP客户端配置

- 增加连接池设置(MaxIdleConns: 100, MaxIdleConnsPerHost: 10)
- 设置空闲连接超时时间为90秒
- 添加HTTP/2禁用选项注释,用于解决流式错误问题

📝 docs(stream): 更新流式更新策略文档

- 详细说明实时流式更新机制和缓冲策略
- 新增HTTP/2流式错误的故障排除指南
- 更新配置参数说明和建议范围

🐛 fix(stream): 修复流式中断时的内容丢失问题

- 在流式接收中断时,确保已接收的内容不会丢失
- 改进错误处理逻辑,区分有内容和无内容的情况

* modify ai
This commit is contained in:
二丫讲梵
2025-12-11 18:22:35 +08:00
committed by GitHub
parent d098fe7ad1
commit f7326b6797
29 changed files with 1683 additions and 908 deletions

View File

@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(tree:*)",
"Bash(rm:*)",
"Bash(mkdir:*)",
"Bash(go mod tidy:*)",
"Bash(go build:*)",
"Bash(go get:*)"
]
}
}

337
STREAM_MODE.md Normal file
View File

@@ -0,0 +1,337 @@
# 流式输出功能使用指南
## 功能概述
项目已支持钉钉机器人的流式输出功能,可以让 AI 回答像打字一样逐字显示,提供更好的用户体验。
## 两种流式模式
### 1. 简化流式模式(推荐快速开始)
**特点**:
- 无需配置钉钉卡片模板
- 直接使用累积内容一次性回复
- 配置简单,开箱即用
**配置方式**:
`config.yml` 中添加:
```yaml
# 启用流式输出
stream_mode: true
```
### 2. 高级流式卡片模式
**特点**:
- 使用钉钉互动卡片实现真正的流式更新
- 内容逐步显示,类似 ChatGPT 网页版效果
- 需要在钉钉开放平台创建卡片模板
**配置方式**:
`config.yml` 中添加:
```yaml
# 启用流式输出
stream_mode: true
# 钉钉卡片模板ID (可选,用于高级流式卡片模式)
card_template_id: "your-card-template-id"
```
## 配置钉钉卡片模板(高级模式)
### 步骤 1: 创建卡片模板
1. 登录 [钉钉开放平台](https://open.dingtalk.com/)
2. 进入你的应用 -> 互动卡片 -> 卡片模板管理
3. 创建新模板,使用以下 JSON Schema:
```json
{
"type": "object",
"properties": {
"content": {
"type": "string",
"title": "内容"
}
}
}
```
### 步骤 2: 设计卡片样式
在卡片编辑器中,添加一个 Markdown 组件来显示 `content` 字段:
```json
{
"type": "markdown",
"text": "{{content}}"
}
```
### 步骤 3: 发布并获取模板ID
1. 保存并发布卡片模板
2. 复制模板ID类似: `4d18414c-aabc-4ec8-9e67-4ceefeada72a.schema`
3. 将模板ID填入 `config.yml``card_template_id` 字段
## 完整配置示例
```yaml
# 日志级别
log_level: "info"
# OpenAI 配置
api_key: "sk-..."
model: "gpt-4o"
base_url: "" # 可选,用于 API 中转
# 流式输出配置
stream_mode: true # 启用流式输出
card_template_id: "" # 可选钉钉卡片模板ID
# 其他配置...
session_timeout: 600s
max_question_len: 2048
max_answer_len: 2048
max_text: 4096
default_mode: "单聊"
```
## 实现原理
### 简化模式流程
```
用户提问 → OpenAI 流式响应 → 累积完整内容 → 一次性回复
```
### 高级卡片模式流程
```
用户提问
创建钉钉卡片(空内容)
发送初始状态 "稍等,让我想一想..."
OpenAI 流式响应
接收到内容 → 立即累积到缓冲区
距离上次更新超过300ms? → 是 → 更新卡片
↓ 否 ↓
继续接收 ←-----------┘
流式结束,发送最终内容(标记为完成)
```
## 技术架构
### 新增文件
1. **[pkg/llm/stream.go](pkg/llm/stream.go)**
- 实现 OpenAI 流式响应
- 提供 `SingleQaStream()``ContextQaStream()` API
- 支持 ChatCompletion 流式调用
2. **[pkg/dingbot/stream.go](pkg/dingbot/stream.go)**
- 实现钉钉流式卡片更新
- 封装钉钉 Streaming Update API
- 提供 `UpdateAIStreamCard()` 方法
3. **[pkg/process/stream.go](pkg/process/stream.go)**
- 实现流式处理逻辑
- `DoStream()` - 简化流式模式
- `DoStreamWithCard()` - 高级卡片模式
- 包含定时更新和错误处理
### 改动文件
1. **[config/config.go](config/config.go)**
- 添加 `StreamMode` 配置项
- 添加 `CardTemplateID` 配置项
2. **[pkg/process/process_request.go](pkg/process/process_request.go)**
- 根据配置自动选择流式或普通模式
- 支持流式卡片和流式普通两种方式
## API 使用示例
### 在代码中使用流式 API
```go
import "github.com/eryajf/chatgpt-dingtalk/pkg/llm"
// 单聊流式
contentCh, cleanup, err := llm.SingleQaStream("你好", "user123")
if err != nil {
log.Fatal(err)
}
defer cleanup()
for content := range contentCh {
fmt.Print(content) // 逐块输出
}
// 串聊流式
client, contentCh, err := llm.ContextQaStream("继续", "user123")
if err != nil {
log.Fatal(err)
}
defer client.Close()
fullAnswer := ""
for content := range contentCh {
fullAnswer += content
fmt.Print(content)
}
// 保存对话上下文
client.ChatContext.SaveConversation("user123")
```
## 性能优化
### 流式更新策略
高级卡片模式采用**实时流式更新**策略:
- 从大模型接收到内容后立即更新卡片
- 使用缓冲机制避免更新过于频繁(默认最小间隔 **300ms**)
- 这样可以实现真正的实时流式体验,类似 ChatGPT 网页版
可以在 [pkg/process/stream.go](pkg/process/stream.go) 中修改最小更新间隔:
```go
minUpdateInterval := 300 * time.Millisecond // 修改这里
```
建议范围200ms - 500ms
- 更小的间隔:更实时,但 API 调用更频繁
- 更大的间隔:API 调用较少,但流式感觉不明显
### 流式模式选择建议
| 场景 | 推荐模式 | 原因 |
|------|---------|------|
| 快速部署 | 简化模式 | 无需额外配置 |
| 追求体验 | 高级卡片模式 | 真正的流式显示 |
| 高频使用 | 简化模式 | 减少 API 调用 |
| 演示展示 | 高级卡片模式 | 视觉效果更好 |
## 兼容性
- ✅ 保持原有非流式模式完全兼容
- ✅ 支持单聊和串聊两种对话模式
- ✅ 支持所有 OpenAI 兼容的模型
- ✅ 支持 Azure OpenAI
- ✅ 保留敏感词过滤、请求限制等功能
## 故障排查
### 问题 1: 流式模式不生效
**检查项**:
1. 确认 `config.yml``stream_mode: true`
2. 重启应用以加载新配置
3. 查看日志是否有错误信息
### 问题 2: 卡片模式无法显示 / 日志显示 "robot code is empty"
**原因**: 这是正常的降级行为
**说明**:
- 高级卡片模式需要通过 `credentials` 配置才能工作
- 如果没有配置 `credentials`,系统会自动降级为简化流式模式
- 简化流式模式不需要卡片,依然可以正常工作
**解决方案**:
1. 如果想使用高级卡片模式,需要在 `config.yml` 中配置 `credentials`:
```yaml
credentials:
- client_id: "your-app-key"
client_secret: "your-app-secret"
```
2. 如果不需要卡片模式,可以忽略这个警告,或者将 `card_template_id` 留空
### 问题 3: 卡片模板配置正确但不显示
**检查项**:
1. 确认卡片模板ID正确
2. 确认卡片模板已发布
3. 确认钉钉应用有卡片权限
4. 确认 `credentials` 配置正确
5. 查看日志是否有降级提示
### 问题 4: 流式响应中断
**可能原因**:
1. OpenAI API 超时 - 检查网络连接
2. 钉钉 Access Token 过期 - 会自动刷新
3. 上下文超过限制 - 减少 `max_text` 配置
### 问题 5: HTTP/2 流式错误 "stream error: INTERNAL_ERROR"
**错误信息**:
```
Post "https://api.xxx.com/v1/completions": stream error: stream ID 5; INTERNAL_ERROR; received from peer
```
**可能原因**:
1. 上游 API 服务器内部错误或资源不足
2. 网络不稳定,长连接中断
3. HTTP/2 连接管理问题
**解决方案**:
1. **已优化**: 代码已优化 HTTP 客户端配置,增加连接池和超时设置
2. **部分内容保护**: 如果已接收到部分内容,不会因错误而丢失
3. **禁用 HTTP/2** (如果问题频繁): 在 [pkg/llm/client.go](pkg/llm/client.go) 中取消注释:
```go
Transport: &http.Transport{
// ...
ForceAttemptHTTP2: false, // 取消注释这行
}
```
注意:禁用 HTTP/2 会降低性能,但可能更稳定
4. **检查 API 服务器**: 如果使用中转服务,检查中转服务器的健康状况和资源
## 智能降级机制
系统实现了智能降级机制,确保即使高级功能无法使用,基础功能依然可用:
```
尝试高级卡片模式
检查 RobotCode 是否存在
↓ (否)
降级为简化流式模式
检查 credentials 配置
↓ (无配置)
降级为简化流式模式
尝试创建卡片
↓ (失败)
降级为简化流式模式
正常流式输出
```
这意味着:
- ✅ 即使配置不完整,流式功能依然可用
- ✅ 不会因为卡片失败而导致整个功能不可用
- ✅ 日志会清楚地告诉你当前使用的是哪种模式
## 参考资料
- [钉钉流式消息更新 API](https://open.dingtalk.com/document/development/api-streamingupdate)
- [OpenAI Streaming API](https://platform.openai.com/docs/api-reference/streaming)
- [PandaWiki 项目参考实现](tmp/PandaWiki)
## 贡献
欢迎提交 Issue 和 Pull Request 来改进流式功能!

View File

@@ -71,3 +71,14 @@ azure_openai_token: "xxxxxxx"
credentials:
- client_id: "put-your-client-id-here"
client_secret: "put-your-client-secret-here"
# ==================== 流式输出配置 (新功能) ====================
# 启用后AI 回答将像打字一样逐字显示
stream_mode: false # true=启用流式输出, false=使用传统一次性回复
# 钉钉卡片模板ID (可选,用于高级流式卡片模式)
# 如果不配置,将使用简化流式模式(直接累积后回复)
# 配置后,将使用钉钉互动卡片实现真正的流式更新
# 获取方式: 在钉钉开放平台创建卡片模板后获得
# 详细配置教程: 请查看 STREAM_MODE.md
card_template_id: "" # 例如: "4d18414c-aabc-4ec8-9e67-4ceefeada72a.schema"

View File

@@ -79,6 +79,10 @@ type Configuration struct {
AzureOpenAIToken string `yaml:"azure_openai_token"`
// 钉钉应用鉴权凭据
Credentials []Credential `yaml:"credentials"`
// 是否启用流式输出
StreamMode bool `yaml:"stream_mode"`
// 钉钉卡片模板ID(用于流式输出)
CardTemplateID string `yaml:"card_template_id"`
}
var (

25
go.mod
View File

@@ -3,25 +3,37 @@ module github.com/eryajf/chatgpt-dingtalk
go 1.22
require (
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.12
github.com/alibabacloud-go/dingtalk v1.6.96
github.com/alibabacloud-go/tea v1.3.14
github.com/alibabacloud-go/tea-utils/v2 v2.0.7
github.com/charmbracelet/log v0.4.0
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
github.com/go-resty/resty/v2 v2.13.1
github.com/google/uuid v1.6.0
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0
github.com/pandodao/tokenizer-go v0.2.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sashabaranov/go-openai v1.27.1
github.com/solywsh/chatgpt v0.0.14
github.com/sashabaranov/go-openai v1.41.2
golang.org/x/image v0.18.0
gopkg.in/yaml.v3 v3.0.1
gorm.io/gorm v1.25.11
)
require (
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
github.com/alibabacloud-go/gateway-dingtalk v1.0.2 // indirect
github.com/alibabacloud-go/openapi-util v0.1.1 // indirect
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.4.6 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bytedance/sonic v1.12.0 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.12.1 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dlclark/regexp2 v1.11.2 // indirect
@@ -38,7 +50,6 @@ require (
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -52,24 +63,22 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pandodao/tokenizer-go v0.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
modernc.org/libc v1.55.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.31.1 // indirect
)
replace github.com/solywsh/chatgpt => ./pkg/chatgpt

197
go.sum
View File

@@ -1,5 +1,57 @@
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.12 h1:Dqhik/9iK3/ltjMuVy2kkuuWK3KPRes2vSzxnrehT74=
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.12/go.mod h1:cgtLEj8i4ddXMcQgq4PnpVQvlzS+y5B+QtdSfmcLM3A=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/dingtalk v1.6.96 h1:oV4qOgvIYjVph8ksk8DNKtoZMIdxKgE4SuMMBGB9j50=
github.com/alibabacloud-go/dingtalk v1.6.96/go.mod h1:mUcgNRgMGQzABtiZtTK8a3b6LwQBQ8t9WsDKzklqVpg=
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
github.com/alibabacloud-go/gateway-dingtalk v1.0.2 h1:+etjmc64QTmYvHlc6eFkH9y2DOc3UPcyD2nF3IXsVqw=
github.com/alibabacloud-go/gateway-dingtalk v1.0.2/go.mod h1:JUvHpkJtlPFpgJcfXqc9Y4mk2JnoRn5XpKbRz38jJho=
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
github.com/alibabacloud-go/openapi-util v0.1.1 h1:ujGErJjG8ncRW6XtBBMphzHTvCxn4DjrVw4m04HsS28=
github.com/alibabacloud-go/openapi-util v0.1.1/go.mod h1:/UehBSE2cf1gYT43GV4E+RxTdLRzURImCYY0aRmlXpw=
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea v1.3.14 h1:/Uzj5ZCFPpbPR+Bs7jfzsyXkYIVsi5TOIuQNOWwc/9c=
github.com/alibabacloud-go/tea v1.3.14/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
github.com/alibabacloud-go/tea-utils/v2 v2.0.1/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
github.com/aliyun/credentials-go v1.4.6 h1:CG8rc/nxCNKfXbZWpWDzI9GjF4Tuu3Es14qT8Y0ClOk=
github.com/aliyun/credentials-go v1.4.6/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bytedance/sonic v1.12.0 h1:YGPgxF9xzaCNvd/ZKdQ28yRovhfMFZQjuk6fKBzZ3ls=
@@ -7,16 +59,22 @@ github.com/bytedance/sonic v1.12.0/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKz
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM=
github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -28,6 +86,9 @@ github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc h1:MKYt39yZJi0Z
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -54,6 +115,21 @@ github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TC
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -61,18 +137,25 @@ github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQu
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -84,12 +167,16 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0 h1:DL64ORGMk6AUB8q5LbRp8KRFn4oHhdrSepBmbMrtmNo=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.0/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
github.com/pandodao/tokenizer-go v0.2.0 h1:NhfI8fGvQkDld2cZCag6NEU3pJ/ugU9zoY1R/zi9YCs=
@@ -100,18 +187,24 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sashabaranov/go-openai v1.27.1 h1:7Nx6db5NXbcoutNmAUQulEQZEpHG/SkzfexP2X5RWMk=
github.com/sashabaranov/go-openai v1.27.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM=
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -119,42 +212,91 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -162,21 +304,34 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
@@ -184,16 +339,46 @@ golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -201,6 +386,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.20.5 h1:s04akhT2dysD0DFOlv9fkQ6oUTLPYgMnnDk9oaqjszM=

View File

@@ -86,7 +86,7 @@ func (r *ChatReceiver) OnChatBotMessageReceived(ctx context.Context, data *chatb
IsInAtList: data.IsInAtList,
SessionWebhook: data.SessionWebhook,
Text: dingbot.Text(data.Text),
RobotCode: "",
RobotCode: r.clientId, // 使用 clientId 作为 RobotCode
Msgtype: dingbot.MsgType(data.Msgtype),
}
clientId := r.clientId

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2022 Shihao
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1 +0,0 @@
> 因为三方包写死了很多参数,这里转到本地,便于二次改造。 感谢https://github.com/solywsh/chatgpt

View File

@@ -1,31 +0,0 @@
package chatgpt
import (
"fmt"
"testing"
)
func TestChatGPT_ChatWithContext(t *testing.T) {
chat := New("")
defer chat.Close()
//go func() {
// select {
// case <-chat.GetDoneChan():
// fmt.Println("time out")
// }
//}()
question := "现在你是一只猫,接下来你只能用\"喵喵喵\"回答."
fmt.Printf("Q: %s\n", question)
answer, err := chat.ChatWithContext(question)
if err != nil {
fmt.Println(err)
}
fmt.Printf("A: %s\n", answer)
question = "你是一只猫吗?"
fmt.Printf("Q: %s\n", question)
answer, err = chat.ChatWithContext(question)
if err != nil {
fmt.Println(err)
}
fmt.Printf("A: %s\n", answer)
}

View File

@@ -1,343 +0,0 @@
package chatgpt
import (
"bytes"
"context"
"encoding/base64"
"encoding/gob"
"errors"
"fmt"
"golang.org/x/image/webp"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"os"
"strings"
"time"
"github.com/pandodao/tokenizer-go"
openai "github.com/sashabaranov/go-openai"
"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
"github.com/eryajf/chatgpt-dingtalk/public"
)
var (
DefaultAiRole = "AI"
DefaultHumanRole = "Human"
DefaultCharacter = []string{"helpful", "creative", "clever", "friendly", "lovely", "talkative"}
DefaultBackground = "The following is a conversation with AI assistant. The assistant is %s"
DefaultPreset = "\n%s: 你好,让我们开始愉快的谈话!\n%s: 我是 AI assistant ,请问你有什么问题?"
)
type (
ChatContext struct {
background string // 对话背景
preset string // 预设对话
maxSeqTimes int // 最大对话次数
aiRole *role // AI角色
humanRole *role // 人类角色
old []conversation // 旧对话
restartSeq string // 重新开始对话的标识
startSeq string // 开始对话的标识
seqTimes int // 对话次数
maintainSeqTimes bool // 是否维护对话次数 (自动移除旧对话)
}
ChatContextOption func(*ChatContext)
conversation struct {
Role *role
Prompt string
}
role struct {
Name string
}
)
func NewContext(options ...ChatContextOption) *ChatContext {
ctx := &ChatContext{
aiRole: &role{Name: DefaultAiRole},
humanRole: &role{Name: DefaultHumanRole},
background: "",
maxSeqTimes: 1000,
preset: "",
old: []conversation{},
seqTimes: 0,
restartSeq: "\n" + DefaultHumanRole + ": ",
startSeq: "\n" + DefaultAiRole + ": ",
maintainSeqTimes: false,
}
for _, option := range options {
option(ctx)
}
return ctx
}
// PollConversation 移除最旧的一则对话
func (c *ChatContext) PollConversation() {
c.old = c.old[1:]
c.seqTimes--
}
// ResetConversation 重置对话
func (c *ChatContext) ResetConversation(userid string) {
public.UserService.ClearUserSessionContext(userid)
}
// SaveConversation 保存对话
func (c *ChatContext) SaveConversation(userid string) error {
var buffer bytes.Buffer
enc := gob.NewEncoder(&buffer)
err := enc.Encode(c.old)
if err != nil {
return err
}
public.UserService.SetUserSessionContext(userid, buffer.String())
return nil
}
// LoadConversation 加载对话
func (c *ChatContext) LoadConversation(userid string) error {
dec := gob.NewDecoder(strings.NewReader(public.UserService.GetUserSessionContext(userid)))
err := dec.Decode(&c.old)
if err != nil {
return err
}
c.seqTimes = len(c.old)
return nil
}
func (c *ChatContext) SetHumanRole(role string) {
c.humanRole.Name = role
c.restartSeq = "\n" + c.humanRole.Name + ": "
}
func (c *ChatContext) SetAiRole(role string) {
c.aiRole.Name = role
c.startSeq = "\n" + c.aiRole.Name + ": "
}
func (c *ChatContext) SetMaxSeqTimes(times int) {
c.maxSeqTimes = times
}
func (c *ChatContext) GetMaxSeqTimes() int {
return c.maxSeqTimes
}
func (c *ChatContext) SetBackground(background string) {
c.background = background
}
func (c *ChatContext) SetPreset(preset string) {
c.preset = preset
}
// 通过 base64 编码字符串开头字符判断图像类型
func getImageTypeFromBase64(base64Str string) string {
switch {
case strings.HasPrefix(base64Str, "/9j/"):
return "JPEG"
case strings.HasPrefix(base64Str, "iVBOR"):
return "PNG"
case strings.HasPrefix(base64Str, "R0lG"):
return "GIF"
case strings.HasPrefix(base64Str, "UklG"):
return "WebP"
default:
return "Unknown"
}
}
func (c *ChatGPT) ChatWithContext(question string) (answer string, err error) {
question = question + "."
if tokenizer.MustCalToken(question) > c.maxQuestionLen {
return "", OverMaxQuestionLength
}
if c.ChatContext.seqTimes >= c.ChatContext.maxSeqTimes {
if c.ChatContext.maintainSeqTimes {
c.ChatContext.PollConversation()
} else {
return "", OverMaxSequenceTimes
}
}
var promptTable []string
promptTable = append(promptTable, c.ChatContext.background)
promptTable = append(promptTable, c.ChatContext.preset)
for _, v := range c.ChatContext.old {
if v.Role == c.ChatContext.humanRole {
promptTable = append(promptTable, "\n"+v.Role.Name+": "+v.Prompt)
} else {
promptTable = append(promptTable, v.Role.Name+": "+v.Prompt)
}
}
promptTable = append(promptTable, "\n"+c.ChatContext.restartSeq+question)
prompt := strings.Join(promptTable, "\n")
prompt += c.ChatContext.startSeq
// 删除对话直到prompt的长度满足条件
for tokenizer.MustCalToken(prompt) > c.maxText {
if len(c.ChatContext.old) > 1 { // 至少保留一条记录
c.ChatContext.PollConversation() // 删除最旧的一条对话
// 重新构建 prompt计算长度
promptTable = promptTable[1:] // 删除promptTable中对应的对话
prompt = strings.Join(promptTable, "\n") + c.ChatContext.startSeq
} else {
break // 如果已经只剩一条记录,那么跳出循环
}
}
// if tokenizer.MustCalToken(prompt) > c.maxText-c.maxAnswerLen {
// return "", OverMaxTextLength
// }
model := public.Config.Model
userId := c.userId
if public.Config.AzureOn {
userId = ""
}
if isModelSupportedChatCompletions(model) {
req := openai.ChatCompletionRequest{
Model: model,
Messages: []openai.ChatCompletionMessage{
{
Role: "user",
Content: prompt,
},
},
MaxTokens: c.maxAnswerLen,
Temperature: 0.6,
User: userId,
}
resp, err := c.client.CreateChatCompletion(c.ctx, req)
if err != nil {
return "", err
}
resp.Choices[0].Message.Content = formatAnswer(resp.Choices[0].Message.Content)
c.ChatContext.old = append(c.ChatContext.old, conversation{
Role: c.ChatContext.humanRole,
Prompt: question,
})
c.ChatContext.old = append(c.ChatContext.old, conversation{
Role: c.ChatContext.aiRole,
Prompt: resp.Choices[0].Message.Content,
})
c.ChatContext.seqTimes++
return resp.Choices[0].Message.Content, nil
} else {
req := openai.CompletionRequest{
Model: model,
MaxTokens: c.maxAnswerLen,
Prompt: prompt,
Temperature: 0.6,
User: c.userId,
Stop: []string{c.ChatContext.aiRole.Name + ":", c.ChatContext.humanRole.Name + ":"},
}
resp, err := c.client.CreateCompletion(c.ctx, req)
if err != nil {
return "", err
}
resp.Choices[0].Text = formatAnswer(resp.Choices[0].Text)
c.ChatContext.old = append(c.ChatContext.old, conversation{
Role: c.ChatContext.humanRole,
Prompt: question,
})
c.ChatContext.old = append(c.ChatContext.old, conversation{
Role: c.ChatContext.aiRole,
Prompt: resp.Choices[0].Text,
})
c.ChatContext.seqTimes++
return resp.Choices[0].Text, nil
}
}
func (c *ChatGPT) GenerateImage(ctx context.Context, prompt string) (string, error) {
model := public.Config.Model
imageModel := public.Config.ImageModel
if isModelSupportedChatCompletions(model) {
req := openai.ImageRequest{
Prompt: prompt,
Model: imageModel,
Size: openai.CreateImageSize1024x1024,
ResponseFormat: openai.CreateImageResponseFormatB64JSON,
N: 1,
User: c.userId,
}
respBase64, err := c.client.CreateImage(c.ctx, req)
if err != nil {
return "", err
}
imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON)
if err != nil {
return "", err
}
r := bytes.NewReader(imgBytes)
// dall-e-3 返回的是 WebP 格式的图片,需要判断处理
imgType := getImageTypeFromBase64(respBase64.Data[0].B64JSON)
var imgData image.Image
var imgErr error
if imgType == "WebP" {
imgData, imgErr = webp.Decode(r)
} else {
imgData, _, imgErr = image.Decode(r)
}
if imgErr != nil {
return "", imgErr
}
imageName := time.Now().Format("20060102-150405") + ".png"
clientId, _ := ctx.Value(public.DingTalkClientIdKeyName).(string)
client := public.DingTalkClientManager.GetClientByOAuthClientID(clientId)
mediaResult, uploadErr := &dingbot.MediaUploadResult{}, errors.New(fmt.Sprintf("unknown clientId: %s", clientId))
if client != nil {
mediaResult, uploadErr = client.UploadMedia(imgBytes, imageName, dingbot.MediaTypeImage, dingbot.MimeTypeImagePng)
}
err = os.MkdirAll("data/images", 0755)
if err != nil {
return "", err
}
file, err := os.Create("data/images/" + imageName)
if err != nil {
return "", err
}
defer file.Close()
if err := png.Encode(file, imgData); err != nil {
return "", err
}
if uploadErr == nil {
return mediaResult.MediaID, nil
} else {
return public.Config.ServiceURL + "/images/" + imageName, nil
}
}
return "", nil
}
func WithMaxSeqTimes(times int) ChatContextOption {
return func(c *ChatContext) {
c.SetMaxSeqTimes(times)
}
}
// WithOldConversation 从文件中加载对话
func WithOldConversation(userid string) ChatContextOption {
return func(c *ChatContext) {
_ = c.LoadConversation(userid)
}
}
func WithMaintainSeqTimes(maintain bool) ChatContextOption {
return func(c *ChatContext) {
c.maintainSeqTimes = maintain
}
}

View File

@@ -1,81 +0,0 @@
package chatgpt
import (
"os"
"testing"
)
func TestOfflineContext(t *testing.T) {
key := os.Getenv("CHATGPT_API_KEY")
if key == "" {
t.Skip("CHATGPT_API_KEY is not set")
}
cli := New("")
reply, err := cli.ChatWithContext("我叫老三,你是?")
if err != nil {
t.Fatal(err)
}
t.Logf("我叫老三,你是? => %s", reply)
err = cli.ChatContext.SaveConversation("test.conversation")
if err != nil {
t.Fatalf("储存对话记录失败: %v", err)
}
cli.ChatContext.ResetConversation("")
reply, err = cli.ChatWithContext("你知道我是谁吗?")
if err != nil {
t.Fatal(err)
}
t.Logf("你知道我是谁吗? => %s", reply)
// assert.NotContains(t, reply, "老三")
err = cli.ChatContext.LoadConversation("test.conversation")
if err != nil {
t.Fatalf("读取对话记录失败: %v", err)
}
reply, err = cli.ChatWithContext("你知道我是谁吗?")
if err != nil {
t.Fatal(err)
}
t.Logf("你知道我是谁吗? => %s", reply)
// AI 理应知道他叫老三
// assert.Contains(t, reply, "老三")
}
func TestMaintainContext(t *testing.T) {
key := os.Getenv("CHATGPT_API_KEY")
if key == "" {
t.Skip("CHATGPT_API_KEY is not set")
}
cli := New("")
cli.ChatContext = NewContext(
WithMaxSeqTimes(1),
WithMaintainSeqTimes(true),
)
reply, err := cli.ChatWithContext("我叫老三,你是?")
if err != nil {
t.Fatal(err)
}
t.Logf("我叫老三,你是? => %s", reply)
reply, err = cli.ChatWithContext("你知道我是谁吗?")
if err != nil {
t.Fatal(err)
}
t.Logf("你知道我是谁吗? => %s", reply)
// 对话次数已经超过 1 次因此最先前的对话已被移除AI 理应不知道他叫老三
// assert.NotContains(t, reply, "老三")
}
func init() {
// 本地加载适用于本地测试如果要在github进行测试可以透过传入 secrets 到环境参数
// _ = godotenv.Load(".env.local")
}

View File

@@ -1,12 +0,0 @@
package chatgpt
import "errors"
// OverMaxSequenceTimes 超过最大对话时间
var OverMaxSequenceTimes = errors.New("maximum conversation times exceeded")
// OverMaxTextLength 超过最大文本长度
var OverMaxTextLength = errors.New("maximum text length exceeded")
// OverMaxQuestionLength 超过最大问题长度
var OverMaxQuestionLength = errors.New("maximum question length exceeded")

View File

@@ -1,83 +0,0 @@
package chatgpt
import (
"context"
"time"
"github.com/avast/retry-go"
"github.com/eryajf/chatgpt-dingtalk/pkg/logger"
"github.com/eryajf/chatgpt-dingtalk/public"
)
// SingleQa 单聊
func SingleQa(question, userId string) (answer string, err error) {
chat := New(userId)
defer chat.Close()
// 定义一个重试策略
retryStrategy := []retry.Option{
retry.Delay(100 * time.Millisecond),
retry.Attempts(3),
retry.LastErrorOnly(true),
}
// 使用重试策略进行重试
err = retry.Do(
func() error {
answer, err = chat.ChatWithContext(question)
if err != nil {
return err
}
return nil
},
retryStrategy...)
return
}
// ContextQa 串聊
func ContextQa(question, userId string) (chat *ChatGPT, answer string, err error) {
chat = New(userId)
if public.UserService.GetUserSessionContext(userId) != "" {
err := chat.ChatContext.LoadConversation(userId)
if err != nil {
logger.Warning("load station failed: %v\n", err)
}
}
retryStrategy := []retry.Option{
retry.Delay(100 * time.Millisecond),
retry.Attempts(3),
retry.LastErrorOnly(true)}
// 使用重试策略进行重试
err = retry.Do(
func() error {
answer, err = chat.ChatWithContext(question)
if err != nil {
return err
}
return nil
},
retryStrategy...)
return
}
// ImageQa 生成图片
func ImageQa(ctx context.Context, question, userId string) (answer string, err error) {
chat := New(userId)
defer chat.Close()
// 定义一个重试策略
retryStrategy := []retry.Option{
retry.Delay(100 * time.Millisecond),
retry.Attempts(3),
retry.LastErrorOnly(true),
}
// 使用重试策略进行重试
err = retry.Do(
func() error {
answer, err = chat.GenerateImage(ctx, question)
if err != nil {
return err
}
return nil
},
retryStrategy...)
return
}

View File

@@ -1,21 +0,0 @@
package chatgpt
import (
"regexp"
"strings"
)
// 适配 deepseek r1
func formatAnswer(answer string) string {
answer = strings.TrimSpace(answer)
re := regexp.MustCompile(`(?s)<think>.*?</think>`)
answer = re.ReplaceAllString(answer, "")
answer = strings.ReplaceAll(answer, "<think>", "")
answer = strings.ReplaceAll(answer, "</think>", "")
answer = strings.TrimSpace(answer)
return answer
}

View File

@@ -1,51 +0,0 @@
module chatgpt
go 1.22
toolchain go1.22.5
require (
github.com/avast/retry-go v3.0.0+incompatible
github.com/chai2010/webp v1.1.1
github.com/eryajf/chatgpt-dingtalk v1.0.11
github.com/pandodao/tokenizer-go v0.2.0
github.com/sashabaranov/go-openai v1.27.1
)
replace github.com/eryajf/chatgpt-dingtalk v1.0.11 => ../..
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/lipgloss v0.12.1 // indirect
github.com/charmbracelet/log v0.4.0 // indirect
github.com/dlclark/regexp2 v1.11.2 // indirect
github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5 // indirect
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-resty/resty/v2 v2.13.1 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gorm.io/gorm v1.25.11 // indirect
modernc.org/libc v1.55.6 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.31.1 // indirect
)

View File

@@ -1,186 +0,0 @@
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E=
github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c=
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
github.com/charmbracelet/log v0.2.1 h1:1z7jpkk4yKyjwlmKmKMM5qnEDSpV32E7XtWhuv0mTZE=
github.com/charmbracelet/log v0.2.1/go.mod h1:GwFfjewhcVDWLrpAbY5A0Hin9YOlEn40eWT4PNaxFT4=
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.9.0 h1:pTK/l/3qYIKaRXuHnEnIf7Y5NxfRPfpb7dis6/gdlVI=
github.com/dlclark/regexp2 v1.9.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.11.2/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20221118162653-d4bf6fde1b86/go.mod h1:yRkwfj0CBpOGre+TwBsqPV0IH0Pk73e4PXJOeNDboGs=
github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079 h1:xkbJGxVnk5sM8/LXeTKaBOfAZrI+iqvIPyH8oK1c6CQ=
github.com/dop251/goja v0.0.0-20230402114112-623f9dda9079/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20240707163329-b1681fb2a2f5/go.mod h1:o31y53rb/qiIAONF7w3FHJZRqqP3fzHUr1HqanthByw=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124 h1:QDuDMgEkC/lnmvk0d/fZfcUUml18uUbS9TY5QtbdFhs=
github.com/dop251/goja_nodejs v0.0.0-20230322100729-2550c7b6c124/go.mod h1:0tlktQL7yHfYEtjcRGi/eiOkbDR5XF7gyFFvbC5//E0=
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc/go.mod h1:VULptt4Q/fNzQUJlqY/GP3qHyU7ZH46mFkBZe0ZTokU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.7.0 h1:A7Xj/KN2Lvie4Z4rrgQHY8MsbebX3NyWsL3n2i82MVI=
github.com/glebarez/sqlite v1.7.0/go.mod h1:PkeevrRlF/1BhQBCnzcMWzgrIk7IOop+qS2jUYLfHhk=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20230406165453-00490a63f317 h1:hFhpt7CTmR3DX+b4R19ydQFtofxT0Sv3QsKNMVQYTMQ=
github.com/google/pprof v0.0.0-20230406165453-00490a63f317/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pandodao/tokenizer-go v0.2.0 h1:NhfI8fGvQkDld2cZCag6NEU3pJ/ugU9zoY1R/zi9YCs=
github.com/pandodao/tokenizer-go v0.2.0/go.mod h1:t6qFbaleKxbv0KNio2XUN/mfGM5WKv4haPXDQWVDG00=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/sashabaranov/go-openai v1.17.6 h1:hYXRPM1xO6QLOJhWEOMlSg/l3jERiKDKd1qIoK22lvs=
github.com/sashabaranov/go-openai v1.17.6/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/sashabaranov/go-openai v1.27.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/libc v1.55.6/go.mod h1:JXguUpMkbw1gknxspNE9XaG+kk9hDAAnBxpA6KGLiyA=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=

View File

@@ -1,30 +0,0 @@
package chatgpt
import openai "github.com/sashabaranov/go-openai"
var ModelsSupportChatCompletions = []string{
openai.GPT432K0613,
openai.GPT432K0314,
openai.GPT432K,
openai.GPT40613,
openai.GPT40314,
openai.GPT4TurboPreview,
openai.GPT4VisionPreview,
openai.GPT4,
openai.GPT4oMini,
openai.GPT3Dot5Turbo1106,
openai.GPT3Dot5Turbo0613,
openai.GPT3Dot5Turbo0301,
openai.GPT3Dot5Turbo16K,
openai.GPT3Dot5Turbo16K0613,
openai.GPT3Dot5Turbo,
}
func isModelSupportedChatCompletions(model string) bool {
for _, m := range ModelsSupportChatCompletions {
if m == model {
return true
}
}
return false
}

154
pkg/dingbot/stream.go Normal file
View File

@@ -0,0 +1,154 @@
package dingbot
import (
"fmt"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dingtalkcard "github.com/alibabacloud-go/dingtalk/card_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"github.com/google/uuid"
)
// StreamCardClient 流式卡片客户端
type StreamCardClient struct {
client *dingtalkcard.Client
}
// NewStreamCardClient 创建流式卡片客户端
func NewStreamCardClient() (*StreamCardClient, error) {
config := &openapi.Config{}
config.Protocol = tea.String("https")
config.RegionId = tea.String("central")
client, err := dingtalkcard.NewClient(config)
if err != nil {
return nil, err
}
return &StreamCardClient{
client: client,
}, nil
}
// CreateAndDeliverCardRequest 创建并投放卡片请求
type CreateAndDeliverCardRequest struct {
CardTemplateID string
OutTrackID string
ConversationID string
SenderStaffID string
RobotCode string
OpenSpaceID string
ConversationType string // "1" for private chat, "2" for group chat
CardData map[string]string
}
// CreateAndDeliverCard 创建并投放流式卡片
func (s *StreamCardClient) CreateAndDeliverCard(accessToken string, req *CreateAndDeliverCardRequest) error {
headers := &dingtalkcard.CreateAndDeliverHeaders{
XAcsDingtalkAccessToken: tea.String(accessToken),
}
cardData := &dingtalkcard.CreateAndDeliverRequestCardData{
CardParamMap: make(map[string]*string),
}
for k, v := range req.CardData {
cardData.CardParamMap[k] = tea.String(v)
}
createReq := &dingtalkcard.CreateAndDeliverRequest{
CardTemplateId: tea.String(req.CardTemplateID),
OutTrackId: tea.String(req.OutTrackID),
CardData: cardData,
CallbackType: tea.String("STREAM"),
UserIdType: tea.Int32(1),
ImGroupOpenSpaceModel: &dingtalkcard.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(true),
},
ImRobotOpenSpaceModel: &dingtalkcard.CreateAndDeliverRequestImRobotOpenSpaceModel{
SupportForward: tea.Bool(true),
},
}
if req.OpenSpaceID != "" {
createReq.SetOpenSpaceId(req.OpenSpaceID)
}
// Handle different conversation types with appropriate delivery models
switch req.ConversationType {
case "2": // Group chat
if req.RobotCode != "" {
createReq.SetImGroupOpenDeliverModel(
&dingtalkcard.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(req.RobotCode),
})
}
case "1": // Private chat with robot
// For private chat, use ImRobotOpenDeliverModel with SpaceType
createReq.SetImRobotOpenDeliverModel(
&dingtalkcard.CreateAndDeliverRequestImRobotOpenDeliverModel{
SpaceType: tea.String("IM_GROUP"),
})
default:
// Fallback to group model if conversation type is unknown
if req.RobotCode != "" {
createReq.SetImGroupOpenDeliverModel(
&dingtalkcard.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(req.RobotCode),
})
}
}
_, err := s.client.CreateAndDeliverWithOptions(createReq, headers, &util.RuntimeOptions{})
return err
}
// StreamingUpdateRequest 流式更新请求
type StreamingUpdateRequest struct {
OutTrackID string
Key string
Content string
IsFull bool
IsFinalize bool
}
// StreamingUpdate 流式更新卡片内容
func (s *StreamCardClient) StreamingUpdate(accessToken string, req *StreamingUpdateRequest) error {
headers := &dingtalkcard.StreamingUpdateHeaders{
XAcsDingtalkAccessToken: tea.String(accessToken),
}
updateReq := &dingtalkcard.StreamingUpdateRequest{
OutTrackId: tea.String(req.OutTrackID),
Guid: tea.String(uuid.New().String()),
Key: tea.String(req.Key),
Content: tea.String(req.Content),
IsFull: tea.Bool(req.IsFull),
IsFinalize: tea.Bool(req.IsFinalize),
IsError: tea.Bool(false),
}
_, err := s.client.StreamingUpdateWithOptions(updateReq, headers, &util.RuntimeOptions{})
return err
}
// UpdateAIStreamCard 更新AI流式卡片 (简化版本,不依赖卡片模板)
func (c *DingTalkClient) UpdateAIStreamCard(trackID, content string, isFinalize bool) error {
cardClient, err := NewStreamCardClient()
if err != nil {
return fmt.Errorf("failed to create stream card client: %w", err)
}
accessToken, err := c.GetAccessToken()
if err != nil {
return fmt.Errorf("failed to get access token: %w", err)
}
req := &StreamingUpdateRequest{
OutTrackID: trackID,
Key: "content",
Content: content,
IsFull: true,
IsFinalize: isFinalize,
}
return cardClient.StreamingUpdate(accessToken, req)
}

34
pkg/llm/api.go Normal file
View File

@@ -0,0 +1,34 @@
package llm
import (
"context"
"github.com/eryajf/chatgpt-dingtalk/public"
)
// SingleQa 单聊
func SingleQa(question, userId string) (string, error) {
client := NewClient(userId)
defer client.Close()
return client.ChatWithContext(question)
}
// ContextQa 串聊
func ContextQa(question, userId string) (*Client, string, error) {
client := NewClient(userId)
if public.UserService.GetUserSessionContext(userId) != "" {
_ = client.ChatContext.LoadConversation(userId)
}
answer, err := client.ChatWithContext(question)
return client, answer, err
}
// ImageQa 生成图片
func ImageQa(ctx context.Context, question, userId string) (string, error) {
client := NewClient(userId)
defer client.Close()
return client.GenerateImage(ctx, question)
}

48
pkg/llm/chat.go Normal file
View File

@@ -0,0 +1,48 @@
package llm
import (
"github.com/pandodao/tokenizer-go"
openai "github.com/sashabaranov/go-openai"
"github.com/eryajf/chatgpt-dingtalk/public"
)
// ChatWithContext 对话接口
func (c *Client) ChatWithContext(question string) (string, error) {
if tokenizer.MustCalToken(question) > c.maxQuestionLen {
return "", ErrOverMaxQuestionLength
}
// 构建消息列表
messages := c.buildMessages(question)
model := public.Config.Model
userId := c.userId
if public.Config.AzureOn {
userId = ""
}
req := openai.ChatCompletionRequest{
Model: model,
Messages: messages,
MaxTokens: c.maxAnswerLen,
Temperature: 0.6,
User: userId,
}
resp, err := c.client.CreateChatCompletion(c.ctx, req)
if err != nil {
return "", err
}
answer := resp.Choices[0].Message.Content
// 保存对话上下文
c.ChatContext.old = append(c.ChatContext.old,
conversation{Role: c.ChatContext.humanRole, Prompt: question},
conversation{Role: c.ChatContext.aiRole, Prompt: answer},
)
c.ChatContext.seqTimes++
return answer, nil
}

View File

@@ -1,4 +1,4 @@
package chatgpt
package llm
import (
"context"
@@ -11,32 +11,31 @@ import (
"github.com/eryajf/chatgpt-dingtalk/public"
)
type ChatGPT struct {
type Client struct {
client *openai.Client
ctx context.Context
userId string
maxQuestionLen int
maxText int
maxAnswerLen int
timeOut time.Duration // 超时时间, 0表示不超时
timeOut time.Duration
doneChan chan struct{}
cancel func()
ChatContext *ChatContext
ChatContext *Context
}
func New(userId string) *ChatGPT {
var ctx context.Context
var cancel func()
ctx, cancel = context.WithTimeout(context.Background(), 600*time.Second)
func NewClient(userId string) *Client {
ctx, cancel := context.WithTimeout(context.Background(), 600*time.Second)
timeOutChan := make(chan struct{}, 1)
go func() {
<-ctx.Done()
timeOutChan <- struct{}{} // 发送超时信号或是提示结束用于聊天机器人场景配合GetTimeOutChan() 使用
timeOutChan <- struct{}{}
}()
config := openai.DefaultConfig(public.Config.ApiKey)
// Azure配置
if public.Config.AzureOn {
config = openai.DefaultAzureConfig(
public.Config.AzureOpenAIToken,
@@ -47,42 +46,48 @@ func New(userId string) *ChatGPT {
return public.Config.AzureDeploymentName
}
} else {
if public.Config.HttpProxy != "" {
config.HTTPClient.Transport = &http.Transport{
// 设置代理
Proxy: func(req *http.Request) (*url.URL, error) {
return url.Parse(public.Config.HttpProxy)
}}
// HTTP客户端配置
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
}
if public.Config.HttpProxy != "" {
proxyURL, _ := url.Parse(public.Config.HttpProxy)
transport.Proxy = http.ProxyURL(proxyURL)
}
config.HTTPClient = &http.Client{Transport: transport}
if public.Config.BaseURL != "" {
config.BaseURL = public.Config.BaseURL + "/v1"
}
}
return &ChatGPT{
return &Client{
client: openai.NewClientWithConfig(config),
ctx: ctx,
userId: userId,
maxQuestionLen: public.Config.MaxQuestionLen, // 最大问题长度
maxAnswerLen: public.Config.MaxAnswerLen, // 最大答案长度
maxText: public.Config.MaxText, // 最大文本 = 问题 + 回答, 接口限制
maxQuestionLen: public.Config.MaxQuestionLen,
maxAnswerLen: public.Config.MaxAnswerLen,
maxText: public.Config.MaxText,
timeOut: public.Config.SessionTimeout,
doneChan: timeOutChan,
cancel: func() {
cancel()
},
ChatContext: NewContext(),
cancel: cancel,
ChatContext: NewContext(),
}
}
func (c *ChatGPT) Close() {
func (c *Client) Close() {
c.cancel()
}
func (c *ChatGPT) GetDoneChan() chan struct{} {
func (c *Client) GetDoneChan() chan struct{} {
return c.doneChan
}
func (c *ChatGPT) SetMaxQuestionLen(maxQuestionLen int) int {
func (c *Client) SetMaxQuestionLen(maxQuestionLen int) int {
if maxQuestionLen > c.maxText-c.maxAnswerLen {
maxQuestionLen = c.maxText - c.maxAnswerLen
}

139
pkg/llm/context.go Normal file
View File

@@ -0,0 +1,139 @@
package llm
import (
"bytes"
"encoding/gob"
"strings"
"github.com/eryajf/chatgpt-dingtalk/public"
)
var (
DefaultAiRole = "AI"
DefaultHumanRole = "Human"
DefaultCharacter = []string{"helpful", "creative", "clever", "friendly", "lovely", "talkative"}
DefaultBackground = "The following is a conversation with AI assistant. The assistant is %s"
DefaultPreset = "\n%s: 你好,让我们开始愉快的谈话!\n%s: 我是 AI assistant ,请问你有什么问题?"
)
type Context struct {
background string
preset string
maxSeqTimes int
aiRole *role
humanRole *role
old []conversation
restartSeq string
startSeq string
seqTimes int
maintainSeqTimes bool
}
type ContextOption func(*Context)
type conversation struct {
Role *role
Prompt string
}
type role struct {
Name string
}
func NewContext(options ...ContextOption) *Context {
ctx := &Context{
aiRole: &role{Name: DefaultAiRole},
humanRole: &role{Name: DefaultHumanRole},
background: "",
maxSeqTimes: 1000,
preset: "",
old: []conversation{},
seqTimes: 0,
restartSeq: "\n" + DefaultHumanRole + ": ",
startSeq: "\n" + DefaultAiRole + ": ",
maintainSeqTimes: false,
}
for _, option := range options {
option(ctx)
}
return ctx
}
func (c *Context) PollConversation() {
c.old = c.old[1:]
c.seqTimes--
}
func (c *Context) ResetConversation(userid string) {
public.UserService.ClearUserSessionContext(userid)
}
func (c *Context) SaveConversation(userid string) error {
var buffer bytes.Buffer
enc := gob.NewEncoder(&buffer)
err := enc.Encode(c.old)
if err != nil {
return err
}
public.UserService.SetUserSessionContext(userid, buffer.String())
return nil
}
func (c *Context) LoadConversation(userid string) error {
dec := gob.NewDecoder(strings.NewReader(public.UserService.GetUserSessionContext(userid)))
err := dec.Decode(&c.old)
if err != nil {
return err
}
c.seqTimes = len(c.old)
return nil
}
func (c *Context) SetHumanRole(role string) {
c.humanRole.Name = role
c.restartSeq = "\n" + c.humanRole.Name + ": "
}
func (c *Context) SetAiRole(role string) {
c.aiRole.Name = role
c.startSeq = "\n" + c.aiRole.Name + ": "
}
func (c *Context) SetMaxSeqTimes(times int) {
c.maxSeqTimes = times
}
func (c *Context) GetMaxSeqTimes() int {
return c.maxSeqTimes
}
func (c *Context) SetBackground(background string) {
c.background = background
}
func (c *Context) SetPreset(preset string) {
c.preset = preset
}
func WithMaxSeqTimes(times int) ContextOption {
return func(c *Context) {
c.SetMaxSeqTimes(times)
}
}
func WithOldConversation(userid string) ContextOption {
return func(c *Context) {
_ = c.LoadConversation(userid)
}
}
func WithMaintainSeqTimes(maintain bool) ContextOption {
return func(c *Context) {
c.maintainSeqTimes = maintain
}
}

10
pkg/llm/errors.go Normal file
View File

@@ -0,0 +1,10 @@
package llm
import "errors"
var (
ErrOverMaxQuestionLength = errors.New("maximum question length exceeded")
ErrOverMaxAnswerLength = errors.New("maximum answer length exceeded")
ErrOverMaxTextLength = errors.New("maximum text length exceeded")
ErrOverMaxSequenceTimes = errors.New("maximum number of sequence exceeded")
)

102
pkg/llm/image.go Normal file
View File

@@ -0,0 +1,102 @@
package llm
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
"image/png"
"os"
"strings"
"time"
"golang.org/x/image/webp"
openai "github.com/sashabaranov/go-openai"
"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
"github.com/eryajf/chatgpt-dingtalk/public"
)
func getImageTypeFromBase64(base64Str string) string {
switch {
case strings.HasPrefix(base64Str, "/9j/"):
return "JPEG"
case strings.HasPrefix(base64Str, "iVBOR"):
return "PNG"
case strings.HasPrefix(base64Str, "R0lG"):
return "GIF"
case strings.HasPrefix(base64Str, "UklG"):
return "WebP"
default:
return "Unknown"
}
}
func (c *Client) GenerateImage(ctx context.Context, prompt string) (string, error) {
imageModel := public.Config.ImageModel
req := openai.ImageRequest{
Prompt: prompt,
Model: imageModel,
Size: openai.CreateImageSize1024x1024,
ResponseFormat: openai.CreateImageResponseFormatB64JSON,
N: 1,
User: c.userId,
}
respBase64, err := c.client.CreateImage(c.ctx, req)
if err != nil {
return "", err
}
imgBytes, err := base64.StdEncoding.DecodeString(respBase64.Data[0].B64JSON)
if err != nil {
return "", err
}
r := bytes.NewReader(imgBytes)
imgType := getImageTypeFromBase64(respBase64.Data[0].B64JSON)
var imgData image.Image
if imgType == "WebP" {
imgData, err = webp.Decode(r)
} else {
imgData, _, err = image.Decode(r)
}
if err != nil {
return "", err
}
imageName := time.Now().Format("20060102-150405") + ".png"
clientId, _ := ctx.Value(public.DingTalkClientIdKeyName).(string)
client := public.DingTalkClientManager.GetClientByOAuthClientID(clientId)
mediaResult, uploadErr := &dingbot.MediaUploadResult{}, errors.New(fmt.Sprintf("unknown clientId: %s", clientId))
if client != nil {
mediaResult, uploadErr = client.UploadMedia(imgBytes, imageName, dingbot.MediaTypeImage, dingbot.MimeTypeImagePng)
}
err = os.MkdirAll("data/images", 0755)
if err != nil {
return "", err
}
file, err := os.Create("data/images/" + imageName)
if err != nil {
return "", err
}
defer file.Close()
if err := png.Encode(file, imgData); err != nil {
return "", err
}
if uploadErr == nil {
return mediaResult.MediaID, nil
}
return public.Config.ServiceURL + "/images/" + imageName, nil
}

153
pkg/llm/stream.go Normal file
View File

@@ -0,0 +1,153 @@
package llm
import (
"errors"
"io"
"github.com/pandodao/tokenizer-go"
openai "github.com/sashabaranov/go-openai"
"github.com/eryajf/chatgpt-dingtalk/public"
)
// ChatWithContextStream 流式对话,返回一个channel用于接收流式内容
func (c *Client) ChatWithContextStream(question string) (<-chan string, error) {
if tokenizer.MustCalToken(question) > c.maxQuestionLen {
return nil, ErrOverMaxQuestionLength
}
// 构建消息列表
messages := c.buildMessages(question)
model := public.Config.Model
userId := c.userId
if public.Config.AzureOn {
userId = ""
}
req := openai.ChatCompletionRequest{
Model: model,
Messages: messages,
MaxTokens: c.maxAnswerLen,
Temperature: 0.6,
User: userId,
Stream: true,
}
contentCh := make(chan string, 10)
go func() {
defer close(contentCh)
stream, err := c.client.CreateChatCompletionStream(c.ctx, req)
if err != nil {
contentCh <- err.Error()
return
}
defer stream.Close()
fullAnswer := ""
for {
response, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
if fullAnswer == "" {
contentCh <- err.Error()
}
return
}
if len(response.Choices) > 0 {
delta := response.Choices[0].Delta.Content
if delta != "" {
fullAnswer += delta
contentCh <- delta
}
}
}
// 保存对话上下文
c.ChatContext.old = append(c.ChatContext.old,
conversation{Role: c.ChatContext.humanRole, Prompt: question},
conversation{Role: c.ChatContext.aiRole, Prompt: fullAnswer},
)
c.ChatContext.seqTimes++
}()
return contentCh, nil
}
// buildMessages 构建消息列表
func (c *Client) buildMessages(question string) []openai.ChatCompletionMessage {
var messages []openai.ChatCompletionMessage
// 添加历史对话
for _, v := range c.ChatContext.old {
role := "assistant"
if v.Role == c.ChatContext.humanRole {
role = "user"
}
messages = append(messages, openai.ChatCompletionMessage{
Role: role,
Content: v.Prompt,
})
}
// 添加当前问题
messages = append(messages, openai.ChatCompletionMessage{
Role: "user",
Content: question,
})
return messages
}
// SingleQaStream 单聊流式版本
func SingleQaStream(question, userId string) (<-chan string, func(), error) {
client := NewClient(userId)
contentCh := make(chan string, 10)
done := make(chan struct{})
go func() {
defer close(contentCh)
defer close(done)
stream, err := client.ChatWithContextStream(question)
if err != nil {
contentCh <- err.Error()
client.Close()
return
}
for content := range stream {
contentCh <- content
}
client.Close()
}()
cleanup := func() {
<-done
}
return contentCh, cleanup, nil
}
// ContextQaStream 串聊流式版本
func ContextQaStream(question, userId string) (*Client, <-chan string, error) {
client := NewClient(userId)
if public.UserService.GetUserSessionContext(userId) != "" {
_ = client.ChatContext.LoadConversation(userId)
}
stream, err := client.ChatWithContextStream(question)
if err != nil {
client.Close()
return nil, nil, err
}
return client, stream, nil
}

View File

@@ -5,10 +5,9 @@ import (
"fmt"
"strings"
"github.com/solywsh/chatgpt"
"github.com/eryajf/chatgpt-dingtalk/pkg/db"
"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
"github.com/eryajf/chatgpt-dingtalk/pkg/llm"
"github.com/eryajf/chatgpt-dingtalk/pkg/logger"
"github.com/eryajf/chatgpt-dingtalk/public"
)
@@ -34,7 +33,7 @@ func ImageGenerate(ctx context.Context, rmsg *dingbot.ReceiveMsg) error {
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
reply, err := chatgpt.ImageQa(ctx, rmsg.Text.Content, rmsg.GetSenderIdentifier())
reply, err := llm.ImageQa(ctx, rmsg.Text.Content, rmsg.GetSenderIdentifier())
if err != nil {
logger.Info(fmt.Errorf("gpt request error: %v", err))
_, err = rmsg.ReplyToDingtalk(string(dingbot.TEXT), fmt.Sprintf("请求openai失败了错误信息%v", err))

View File

@@ -6,10 +6,9 @@ import (
"strings"
"time"
"github.com/solywsh/chatgpt"
"github.com/eryajf/chatgpt-dingtalk/pkg/db"
"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
"github.com/eryajf/chatgpt-dingtalk/pkg/llm"
"github.com/eryajf/chatgpt-dingtalk/pkg/logger"
"github.com/eryajf/chatgpt-dingtalk/public"
)
@@ -94,8 +93,36 @@ func ProcessRequest(rmsg *dingbot.ReceiveMsg) error {
}
default:
if public.FirstCheck(rmsg) {
// 检查是否启用流式模式
if public.Config.StreamMode {
logger.Info("📡 使用串聊流式模式")
if public.Config.CardTemplateID != "" {
logger.Info("🎴 使用流式卡片输出")
// 使用流式卡片输出
return DoStreamWithCard("串聊", rmsg, public.Config.CardTemplateID)
} else {
logger.Info("💬 使用简化流式输出")
// 使用流式普通输出
return DoStream("串聊", rmsg)
}
}
logger.Info("💭 使用传统串聊模式")
return Do("串聊", rmsg)
} else {
// 检查是否启用流式模式
if public.Config.StreamMode {
logger.Info("📡 使用单聊流式模式")
if public.Config.CardTemplateID != "" {
logger.Info("🎴 使用流式卡片输出")
// 使用流式卡片输出
return DoStreamWithCard("单聊", rmsg, public.Config.CardTemplateID)
} else {
logger.Info("💬 使用简化流式输出")
// 使用流式普通输出
return DoStream("单聊", rmsg)
}
}
logger.Info("💭 使用传统单聊模式")
return Do("单聊", rmsg)
}
}
@@ -120,7 +147,7 @@ func Do(mode string, rmsg *dingbot.ReceiveMsg) error {
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
reply, err := chatgpt.SingleQa(rmsg.Text.Content, rmsg.GetSenderIdentifier())
reply, err := llm.SingleQa(rmsg.Text.Content, rmsg.GetSenderIdentifier())
if err != nil {
logger.Info(fmt.Errorf("gpt request error: %v", err))
if strings.Contains(fmt.Sprintf("%v", err), "maximum question length exceeded") {
@@ -179,7 +206,7 @@ func Do(mode string, rmsg *dingbot.ReceiveMsg) error {
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
cli, reply, err := chatgpt.ContextQa(rmsg.Text.Content, rmsg.GetSenderIdentifier())
cli, reply, err := llm.ContextQa(rmsg.Text.Content, rmsg.GetSenderIdentifier())
if err != nil {
logger.Info(fmt.Sprintf("gpt request error: %v", err))
if strings.Contains(fmt.Sprintf("%v", err), "maximum text length exceeded") {

404
pkg/process/stream.go Normal file
View File

@@ -0,0 +1,404 @@
package process
import (
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/eryajf/chatgpt-dingtalk/pkg/db"
"github.com/eryajf/chatgpt-dingtalk/pkg/dingbot"
"github.com/eryajf/chatgpt-dingtalk/pkg/llm"
"github.com/eryajf/chatgpt-dingtalk/pkg/logger"
"github.com/eryajf/chatgpt-dingtalk/public"
)
// DoStream 使用流式输出执行处理请求
func DoStream(mode string, rmsg *dingbot.ReceiveMsg) error {
// 先把模式注入
public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), mode)
switch mode {
case "单聊":
return doSingleChatStream(rmsg)
case "串聊":
return doContextChatStream(rmsg)
default:
return nil
}
}
// doSingleChatStream 单聊流式处理
func doSingleChatStream(rmsg *dingbot.ReceiveMsg) error {
// 保存问题到数据库
qObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.Q,
ParentContent: 0,
Content: rmsg.Text.Content,
}
qid, err := qObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
// 获取流式内容
contentCh, cleanup, err := llm.SingleQaStream(rmsg.Text.Content, rmsg.GetSenderIdentifier())
if err != nil {
logger.Info(fmt.Errorf("gpt request error: %v", err))
if strings.Contains(fmt.Sprintf("%v", err), "maximum question length exceeded") {
public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier())
_, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v\n\n> 已超过最大文本限制,请缩短提问文字的字数。", err))
if err != nil {
logger.Warning(fmt.Errorf("send message error: %v", err))
}
} else {
_, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v", err))
if err != nil {
logger.Warning(fmt.Errorf("send message error: %v", err))
}
}
return err
}
defer cleanup()
// 使用简化版本:直接累积内容后一次性回复
fullContent := ""
for content := range contentCh {
fullContent += content
}
if fullContent == "" {
logger.Warning("get gpt result failed: empty response")
return nil
}
// 格式化和处理答案
fullContent = strings.TrimSpace(fullContent)
fullContent = strings.Trim(fullContent, "\n")
// 保存答案到数据库
aObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.A,
ParentContent: qid,
Content: fullContent,
}
_, err = aObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, fullContent))
// 敏感词过滤
if public.JudgeSensitiveWord(fullContent) {
fullContent = public.SolveSensitiveWord(fullContent)
}
// 回复用户
_, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), FormatMarkdown(fullContent))
if err != nil {
logger.Warning(fmt.Errorf("send message error: %v", err))
return err
}
return nil
}
// doContextChatStream 串聊流式处理
func doContextChatStream(rmsg *dingbot.ReceiveMsg) error {
// 保存问题到数据库
lastAid := public.UserService.GetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle())
qObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.Q,
ParentContent: lastAid,
Content: rmsg.Text.Content,
}
qid, err := qObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
// 获取流式内容
cli, contentCh, err := llm.ContextQaStream(rmsg.Text.Content, rmsg.GetSenderIdentifier())
if err != nil {
logger.Info(fmt.Sprintf("gpt request error: %v", err))
if strings.Contains(fmt.Sprintf("%v", err), "maximum text length exceeded") {
public.UserService.ClearUserSessionContext(rmsg.GetSenderIdentifier())
_, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v\n\n> 串聊已超过最大文本限制,对话已重置,请重新发起。", err))
if err != nil {
logger.Warning(fmt.Errorf("send message error: %v", err))
}
} else {
_, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), fmt.Sprintf("[Wrong] 请求 OpenAI 失败了\n\n> 错误信息:%v", err))
if err != nil {
logger.Warning(fmt.Errorf("send message error: %v", err))
}
}
return err
}
defer cli.Close()
// 使用简化版本:直接累积内容后一次性回复
fullContent := ""
for content := range contentCh {
fullContent += content
}
if fullContent == "" {
logger.Warning("get gpt result failed: empty response")
return nil
}
// 格式化和处理答案
fullContent = strings.TrimSpace(fullContent)
fullContent = strings.Trim(fullContent, "\n")
// 保存答案到数据库
aObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.A,
ParentContent: qid,
Content: fullContent,
}
aid, err := aObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
// 将当前回答的ID放入缓存
public.UserService.SetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle(), aid)
logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, fullContent))
// 敏感词过滤
if public.JudgeSensitiveWord(fullContent) {
fullContent = public.SolveSensitiveWord(fullContent)
}
// 回复用户
_, err = rmsg.ReplyToDingtalk(string(dingbot.MARKDOWN), FormatMarkdown(fullContent))
if err != nil {
logger.Warning(fmt.Errorf("send message error: %v", err))
return err
}
// 保存对话上下文
_ = cli.ChatContext.SaveConversation(rmsg.GetSenderIdentifier())
return nil
}
// DoStreamWithCard 使用流式卡片输出执行处理请求 (需要配置卡片模板)
func DoStreamWithCard(mode string, rmsg *dingbot.ReceiveMsg, cardTemplateID string) error {
// 先把模式注入
public.UserService.SetUserMode(rmsg.GetSenderIdentifier(), mode)
// 检查是否有 RobotCode如果没有则降级为简化流式模式
clientId := rmsg.RobotCode
if clientId == "" {
logger.Warning("RobotCode is empty, fallback to simple stream mode")
return DoStream(mode, rmsg)
}
// 获取钉钉客户端
dingClient := public.DingTalkClientManager.GetClientByOAuthClientID(clientId)
if dingClient == nil {
logger.Warning(fmt.Errorf("dingtalk client not found for robot code: %s, fallback to simple stream mode", clientId))
return DoStream(mode, rmsg)
}
client, ok := dingClient.(*dingbot.DingTalkClient)
if !ok {
logger.Warning("invalid dingtalk client type, fallback to simple stream mode")
return DoStream(mode, rmsg)
}
// 生成唯一追踪ID
trackID := uuid.New().String()
// 创建并投放卡片
accessToken, err := client.GetAccessToken()
if err != nil {
return fmt.Errorf("failed to get access token: %w", err)
}
cardClient, err := dingbot.NewStreamCardClient()
if err != nil {
return fmt.Errorf("failed to create stream card client: %w", err)
}
// 构建OpenSpaceID
var openSpaceID string
if rmsg.ConversationType == "2" { // 群聊
openSpaceID = fmt.Sprintf("dtv1.card//IM_GROUP.%s", rmsg.ConversationID)
logger.Info(fmt.Sprintf("🎴 群聊模式 - OpenSpaceID: %s, RobotCode: %s", openSpaceID, rmsg.RobotCode))
} else { // 单聊
openSpaceID = fmt.Sprintf("dtv1.card//IM_ROBOT.%s", rmsg.SenderStaffId)
logger.Info(fmt.Sprintf("🎴 私聊模式 - OpenSpaceID: %s, ConversationType: %s", openSpaceID, rmsg.ConversationType))
}
createReq := &dingbot.CreateAndDeliverCardRequest{
CardTemplateID: cardTemplateID,
OutTrackID: trackID,
ConversationID: rmsg.ConversationID,
SenderStaffID: rmsg.SenderStaffId,
RobotCode: rmsg.RobotCode,
OpenSpaceID: openSpaceID,
ConversationType: rmsg.ConversationType,
CardData: map[string]string{
"content": "",
},
}
if err := cardClient.CreateAndDeliverCard(accessToken, createReq); err != nil {
logger.Warning(fmt.Errorf("failed to create card: %v", err))
// 卡片创建失败,降级为普通消息
return DoStream(mode, rmsg)
}
// 发送初始状态
initialContent := fmt.Sprintf("**%s**\n\n%s", rmsg.Text.Content, "稍等,让我想一想……")
if err := client.UpdateAIStreamCard(trackID, initialContent, false); err != nil {
logger.Warning(fmt.Errorf("failed to update initial card: %v", err))
}
// 获取流式内容
var contentCh <-chan string
var cli *llm.Client
if mode == "单聊" {
var cleanup func()
contentCh, cleanup, err = llm.SingleQaStream(rmsg.Text.Content, rmsg.GetSenderIdentifier())
defer cleanup()
} else {
cli, contentCh, err = llm.ContextQaStream(rmsg.Text.Content, rmsg.GetSenderIdentifier())
defer cli.Close()
}
if err != nil {
errorMsg := fmt.Sprintf("**%s**\n\n出错了: %v", rmsg.Text.Content, err)
if err := client.UpdateAIStreamCard(trackID, errorMsg, true); err != nil {
logger.Warning(fmt.Errorf("failed to update error card: %v", err))
}
return err
}
// 实时流式更新卡片内容
questionHeader := fmt.Sprintf("**%s**\n\n", rmsg.Text.Content)
fullContent := questionHeader
// 使用缓冲机制避免更新过于频繁
updateBuffer := ""
lastUpdateTime := time.Now()
minUpdateInterval := 300 * time.Millisecond // 最小更新间隔300ms
for {
content, ok := <-contentCh
if !ok {
// 流结束,发送最后的更新(如果有未发送的缓冲内容)
if updateBuffer != "" {
fullContent += updateBuffer
if err := client.UpdateAIStreamCard(trackID, fullContent, true); err != nil {
logger.Error(fmt.Errorf("failed to finalize card: %v", err))
}
} else {
// 标记为完成
if err := client.UpdateAIStreamCard(trackID, fullContent, true); err != nil {
logger.Error(fmt.Errorf("failed to finalize card: %v", err))
}
}
// 保存到数据库并处理后续逻辑
saveStreamResult(mode, rmsg, fullContent[len(questionHeader):], cli)
return nil
}
// 累积接收到的内容到缓冲区
updateBuffer += content
// 检查是否应该更新(距离上次更新超过最小间隔)
if time.Since(lastUpdateTime) >= minUpdateInterval {
fullContent += updateBuffer
updateBuffer = ""
// 立即更新卡片
if err := client.UpdateAIStreamCard(trackID, fullContent, false); err != nil {
logger.Warning(fmt.Errorf("failed to update card: %v", err))
}
lastUpdateTime = time.Now()
}
}
}
// saveStreamResult 保存流式结果到数据库
func saveStreamResult(mode string, rmsg *dingbot.ReceiveMsg, answer string, cli *llm.Client) {
answer = strings.TrimSpace(answer)
answer = strings.Trim(answer, "\n")
if mode == "单聊" {
qObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.Q,
ParentContent: 0,
Content: rmsg.Text.Content,
}
qid, err := qObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
aObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.A,
ParentContent: qid,
Content: answer,
}
_, err = aObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
} else { // 串聊
lastAid := public.UserService.GetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle())
qObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.Q,
ParentContent: lastAid,
Content: rmsg.Text.Content,
}
qid, err := qObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
aObj := db.Chat{
Username: rmsg.SenderNick,
Source: rmsg.GetChatTitle(),
ChatType: db.A,
ParentContent: qid,
Content: answer,
}
aid, err := aObj.Add()
if err != nil {
logger.Error("往MySQL新增数据失败,错误信息:", err)
}
public.UserService.SetAnswerID(rmsg.SenderNick, rmsg.GetChatTitle(), aid)
if cli != nil {
_ = cli.ChatContext.SaveConversation(rmsg.GetSenderIdentifier())
}
}
logger.Info(fmt.Sprintf("🤖 %s得到的答案: %#v", rmsg.SenderNick, answer))
}