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))
+}