diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..7a74400 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "Bash(tree:*)", + "Bash(rm:*)", + "Bash(mkdir:*)", + "Bash(go mod tidy:*)", + "Bash(go build:*)", + "Bash(go get:*)" + ] + } +} diff --git a/STREAM_MODE.md b/STREAM_MODE.md new file mode 100644 index 0000000..e3540cb --- /dev/null +++ b/STREAM_MODE.md @@ -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 来改进流式功能! diff --git a/config.example.yml b/config.example.yml index 3ef0a10..9ab6756 100644 --- a/config.example.yml +++ b/config.example.yml @@ -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" diff --git a/config/config.go b/config/config.go index 2b43e12..c7c9402 100644 --- a/config/config.go +++ b/config/config.go @@ -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 ( diff --git a/go.mod b/go.mod index c0f5726..302e21f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c417f5c..c31e08e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 5799dc5..3713cda 100644 --- a/main.go +++ b/main.go @@ -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 diff --git a/pkg/chatgpt/LICENSE b/pkg/chatgpt/LICENSE deleted file mode 100644 index f47f42e..0000000 --- a/pkg/chatgpt/LICENSE +++ /dev/null @@ -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. \ No newline at end of file diff --git a/pkg/chatgpt/README.md b/pkg/chatgpt/README.md deleted file mode 100644 index a16226f..0000000 --- a/pkg/chatgpt/README.md +++ /dev/null @@ -1 +0,0 @@ -> 因为三方包写死了很多参数,这里转到本地,便于二次改造。 感谢:https://github.com/solywsh/chatgpt diff --git a/pkg/chatgpt/chatgpt_test.go b/pkg/chatgpt/chatgpt_test.go deleted file mode 100644 index 73c6760..0000000 --- a/pkg/chatgpt/chatgpt_test.go +++ /dev/null @@ -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) -} diff --git a/pkg/chatgpt/context.go b/pkg/chatgpt/context.go deleted file mode 100644 index 9bba002..0000000 --- a/pkg/chatgpt/context.go +++ /dev/null @@ -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 - } -} diff --git a/pkg/chatgpt/context_test.go b/pkg/chatgpt/context_test.go deleted file mode 100644 index 36e6d1a..0000000 --- a/pkg/chatgpt/context_test.go +++ /dev/null @@ -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") -} diff --git a/pkg/chatgpt/errors.go b/pkg/chatgpt/errors.go deleted file mode 100644 index 8c5713f..0000000 --- a/pkg/chatgpt/errors.go +++ /dev/null @@ -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") diff --git a/pkg/chatgpt/export.go b/pkg/chatgpt/export.go deleted file mode 100644 index 3088580..0000000 --- a/pkg/chatgpt/export.go +++ /dev/null @@ -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 -} diff --git a/pkg/chatgpt/format.go b/pkg/chatgpt/format.go deleted file mode 100644 index 9dbc294..0000000 --- a/pkg/chatgpt/format.go +++ /dev/null @@ -1,21 +0,0 @@ -package chatgpt - -import ( - "regexp" - "strings" -) - -// 适配 deepseek r1 -func formatAnswer(answer string) string { - answer = strings.TrimSpace(answer) - - re := regexp.MustCompile(`(?s).*?`) - answer = re.ReplaceAllString(answer, "") - - answer = strings.ReplaceAll(answer, "", "") - answer = strings.ReplaceAll(answer, "", "") - - answer = strings.TrimSpace(answer) - - return answer -} diff --git a/pkg/chatgpt/go.mod b/pkg/chatgpt/go.mod deleted file mode 100644 index 480ba69..0000000 --- a/pkg/chatgpt/go.mod +++ /dev/null @@ -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 -) diff --git a/pkg/chatgpt/go.sum b/pkg/chatgpt/go.sum deleted file mode 100644 index d83c4a6..0000000 --- a/pkg/chatgpt/go.sum +++ /dev/null @@ -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= diff --git a/pkg/chatgpt/models.go b/pkg/chatgpt/models.go deleted file mode 100644 index c5799c3..0000000 --- a/pkg/chatgpt/models.go +++ /dev/null @@ -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 -} diff --git a/pkg/dingbot/stream.go b/pkg/dingbot/stream.go new file mode 100644 index 0000000..03a4c7d --- /dev/null +++ b/pkg/dingbot/stream.go @@ -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) +} diff --git a/pkg/llm/api.go b/pkg/llm/api.go new file mode 100644 index 0000000..287df0b --- /dev/null +++ b/pkg/llm/api.go @@ -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) +} diff --git a/pkg/llm/chat.go b/pkg/llm/chat.go new file mode 100644 index 0000000..0acfcaa --- /dev/null +++ b/pkg/llm/chat.go @@ -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 +} diff --git a/pkg/chatgpt/chatgpt.go b/pkg/llm/client.go similarity index 55% rename from pkg/chatgpt/chatgpt.go rename to pkg/llm/client.go index 23fdd07..d76006f 100644 --- a/pkg/chatgpt/chatgpt.go +++ b/pkg/llm/client.go @@ -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 } diff --git a/pkg/llm/context.go b/pkg/llm/context.go new file mode 100644 index 0000000..526bbde --- /dev/null +++ b/pkg/llm/context.go @@ -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 + } +} diff --git a/pkg/llm/errors.go b/pkg/llm/errors.go new file mode 100644 index 0000000..704dd65 --- /dev/null +++ b/pkg/llm/errors.go @@ -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") +) diff --git a/pkg/llm/image.go b/pkg/llm/image.go new file mode 100644 index 0000000..ceef00d --- /dev/null +++ b/pkg/llm/image.go @@ -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 +} diff --git a/pkg/llm/stream.go b/pkg/llm/stream.go new file mode 100644 index 0000000..3c64d56 --- /dev/null +++ b/pkg/llm/stream.go @@ -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 +} diff --git a/pkg/process/image.go b/pkg/process/image.go index ce533b5..f635640 100644 --- a/pkg/process/image.go +++ b/pkg/process/image.go @@ -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)) diff --git a/pkg/process/process_request.go b/pkg/process/process_request.go index dae2f73..db7c677 100644 --- a/pkg/process/process_request.go +++ b/pkg/process/process_request.go @@ -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") { diff --git a/pkg/process/stream.go b/pkg/process/stream.go new file mode 100644 index 0000000..9aa35e2 --- /dev/null +++ b/pkg/process/stream.go @@ -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)) +}