Compare commits

...

70 Commits

Author SHA1 Message Date
renzhiyuan 9590721d30 refactor: 重构advice模块并优化对话功能 2026-02-08 16:34:41 +08:00
renzhiyuan 46049475c1 Merge branch 'feature/rzy/talk_advicer' into test
# Conflicts:
#	internal/biz/provider_set.go
#	internal/data/impl/provider_set.go
#	internal/server/router/router.go
#	internal/services/dtalk_bot_test.go
2026-02-05 13:42:59 +08:00
fuzhongyun 24f5b43ea9 Merge branch 'v4-fzy' into test 2026-02-05 11:12:11 +08:00
fuzhongyun a53f3af1c3 fix: 修复提交后的卡片展示 2026-02-05 11:12:03 +08:00
fuzhongyun 48f248fecd Merge branch 'v4-fzy' into test 2026-02-05 10:58:04 +08:00
fuzhongyun f0e8613d57 fix: 修复参数错误 2026-02-05 10:57:44 +08:00
fuzhongyun 849eb44870 Merge branch 'v4-fzy' into test 2026-02-05 10:45:23 +08:00
fuzhongyun 3b5ac98c08 fix: debug 2026-02-05 10:45:13 +08:00
fuzhongyun 02e88ed610 Merge branch 'v4-fzy' into test 2026-02-05 10:11:29 +08:00
fuzhongyun b104572e1b fix: 1.修改HTTP机器人回调 2.修改HTTP卡片回调 3.追加知识库命中判断 2026-02-05 10:10:08 +08:00
fuzhongyun 99865c2bc4 fix: 优化提示词 2026-02-04 17:27:35 +08:00
fuzhongyun cfeaa6e201 fix: 拆分系统判断与问题分类判断 2026-02-04 16:53:35 +08:00
fuzhongyun c9c9bca9ce fix: 拆分系统判断与问题分类判断 2026-02-04 16:52:23 +08:00
fuzhongyun 2f5b0af3a4 fix: 临时调整一个常量 2026-02-04 15:54:03 +08:00
fuzhongyun ec41a3d787 fix: 增加群名称 2026-02-04 15:35:11 +08:00
fuzhongyun 719fd805e6 fix: 1.调整历史记录用法 2.增加相关常量 3.调整改下知识库Query用法 4.增加友好输出 5.调整系统提示词 2026-02-04 11:20:11 +08:00
fuzhongyun c174ab683a fix:1.调整钉钉单聊问题路由负责人整体逻辑 2.增加机器人中间回复 3. 单元测试,提示词调整 2026-02-03 18:50:51 +08:00
fuzhongyun c1971e71c1 chore: 告一段落 2026-02-02 18:31:04 +08:00
fuzhongyun ece04df2cb Merge remote-tracking branch 'origin/master' into v4-fzy 2026-02-02 17:32:49 +08:00
fuzhongyun 7e71ad52a4 fix: 1.增加env配置 2.增加任务后门 2026-02-02 17:27:24 +08:00
fuzhongyun 847eb8b5db fix: 单聊机器人初步开发完成 2026-02-02 16:32:19 +08:00
fuzhongyun 3b6471a196 fix: 1.开放机器人单聊处理 2. 增加问题->负责人路由相关方法 3.多轮对话获取用户实际问题 4.知识库调用 5.建群(无效) 2026-02-02 15:07:51 +08:00
fuzhongyun b3b09f184b feat: 增加三张问题处理相关表 2026-02-02 10:29:08 +08:00
fuzhongyun 1eb63498d7 fix: 移除测试环境bot 2026-01-31 18:31:54 +08:00
fuzhongyun 21585e731f fix: llm model local -> cloud 2026-01-31 18:14:03 +08:00
fuzhongyun 71ed8146f5 fix: HeardBeatX 10s -> 100s 2026-01-31 18:10:05 +08:00
fuzhongyun 22b7a3d096 fix: llm model cloud -> local 2026-01-31 18:02:38 +08:00
fuzhongyun 9418d95a65 fix: heartbeat_interval 30 -> 300 2026-01-31 17:59:08 +08:00
fuzhongyun 091a3a50b0 fix: 意图输出到日志 2026-01-30 09:24:20 +08:00
fuzhongyun 7120eef4e8 fix: 调整测试环境 botname 2026-01-29 17:35:00 +08:00
fuzhongyun 3fc8c5dd93 Merge branch 'v4-fzy' into test 2026-01-29 17:21:41 +08:00
fuzhongyun 2cdeb4a9ae fix:知识库切换 WeKnora -> lightRAG 2026-01-29 17:21:26 +08:00
fuzhongyun a7ac1610bb Merge branch 'v4-fzy' into test 2026-01-29 17:14:37 +08:00
fuzhongyun 5a4dc13324 Merge remote-tracking branch 'origin/master' into test 2026-01-29 17:09:29 +08:00
fuzhongyun 22a9de2841 fix:配置调整 2026-01-29 17:09:10 +08:00
fuzhongyun f33096a506 fix: 还原 excel2pic 2026-01-28 19:05:42 +08:00
fuzhongyun b1c5bfa6f5 fix:切换测试 excel2pic 2026-01-28 18:44:04 +08:00
fuzhongyun ce74a27b53 fix:切换测试 excel2pic 2026-01-28 18:37:26 +08:00
fuzhongyun 6f33665e16 dev 2026-01-28 18:36:34 +08:00
fuzhongyun e19ccfa0f3 fix:调整测试环境配置 2026-01-28 17:15:39 +08:00
fuzhongyun 71a5118180 fix:知识库切换 WeKnora -> lightRAG 2026-01-28 17:14:23 +08:00
fuzhongyun 5b11cb728f fix: 1.调整模型 2.解决空指针问题 3.建群卡片接收人增加@人 2026-01-27 18:03:18 +08:00
fuzhongyun 88ed4ff714 fix: 线上使用ollama云模型 2026-01-27 17:13:19 +08:00
fuzhongyun 2fd3d2ae60 fix:增加 provider 2026-01-27 17:11:10 +08:00
fuzhongyun 39d2fc1e62 fix: 调整代码层级 2026-01-27 14:14:16 +08:00
fuzhongyun 36db8e7a86 fix:移除注释 2026-01-27 11:56:45 +08:00
fuzhongyun fa08cad74a fix: 代码优化 1.需要环境区分的配置,从常量移动到yaml 2. 新增获取机器人应用配置的公共方法 3.以上修改相应的业务调整 4.修复一些单元测试文件的报错 2026-01-27 11:51:26 +08:00
fuzhongyun 32cd8691b7 fix:代码优化 2026-01-26 17:54:23 +08:00
fuzhongyun 5560e879d0 fix: 调整钉钉机器人场景的提示词 2026-01-26 16:37:09 +08:00
fuzhongyun c74fe839d8 fix: 处理问题群卡片推送所有人 2026-01-26 16:04:25 +08:00
fuzhongyun 634bca5c60 fix: 群聊使用知识库流程基本串通 2026-01-26 15:08:33 +08:00
fuzhongyun d7ae15797b fix: 跳过响应包装 2026-01-26 09:27:45 +08:00
fuzhongyun 855156374e fix: 跳过响应包装 2026-01-26 09:23:53 +08:00
fuzhongyun 498d165915 fix: 1.增加一个自定义json解析的type 2.处理取消按钮回调 2026-01-24 11:58:29 +08:00
fuzhongyun a3935cf9ec fix: 增加代理转发 2026-01-24 09:21:39 +08:00
fuzhongyun 451f68056c fix: 增加卡片回调路由 2026-01-24 09:18:17 +08:00
fuzhongyun 5d58cbc0f6 fix: 1.增加知识库配置,增加知识库工具方法,增加知识库输出格式化方法 2.优化机器人对话时知识库调用链路,增加配置、常量 3.增加新&旧SDK创建场景群方法 4.增加问题处理群机器人对话唤起QA数据收集组件 2026-01-23 18:21:51 +08:00
fuzhongyun 0430595a73 fix: 修改代理地址 2026-01-22 14:11:42 +08:00
fuzhongyun 7f5947c443 fix:调整路由 2026-01-22 14:10:55 +08:00
fuzhongyun 6173bd00b1 fix:钉钉机器人回调代理到本地 2026-01-22 14:07:26 +08:00
fuzhongyun 5b1a138ca1 fix:调整钉钉机器人回调临时转发 2026-01-22 13:33:53 +08:00
fuzhongyun 51f012d315 fix:调整钉钉机器人回调临时转发 2026-01-22 10:38:57 +08:00
fuzhongyun 9468037d66 fix: 暂存 2026-01-22 10:36:30 +08:00
fuzhongyun 6173a92735 fix:修复钉钉机器人回调临时转发 2026-01-22 10:21:57 +08:00
fuzhongyun b91b7bb328 fix:add钉钉机器人回调临时转发 2026-01-22 10:07:27 +08:00
fuzhongyun 534da15898 1. 增加钉钉IM客户端,增加创建并投放卡片方法 2.规范化多处配置获取代码 3.增加获取入群二维码链接方法 4.调整加群流程为 确认加群-创建群聊-将机器人添加到新群-获取新群分享链接-点击跳转至新群 2026-01-21 18:43:01 +08:00
fuzhongyun a0b76f1581 fix: 1.增加回调 OutTrackId 模板处理 2.群组查询增加状态判断 3.群组配置表增加群组问题处理人字段 4.增加卡片更新方法,调整回调执行逻辑 2026-01-21 14:42:14 +08:00
fuzhongyun 17d7b01fdf fix:1. 增加钉钉card客户端,增加card创建并投放卡片方法 2.增加交互卡片回调方法 3.增加钉钉建群方法,建群demo 2026-01-20 18:39:24 +08:00
fuzhongyun 44864cc7f0 feat: 新增钉钉 oauth-client、robot-client,新增消息推送demo方法 2026-01-19 18:25:45 +08:00
fuzhongyun e8061799b8 feat: 新增知识库调用demo、段落输出demo 2026-01-15 18:12:34 +08:00
74 changed files with 3937 additions and 266 deletions

File diff suppressed because one or more lines are too long

View File

@ -13,6 +13,7 @@ func main() {
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file") configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
onBot := flag.String("bot", "", "bot start") onBot := flag.String("bot", "", "bot start")
cron := flag.String("cron", "", "close") cron := flag.String("cron", "", "close")
runJob := flag.String("runJob", "", "run single job and exit")
flag.Parse() flag.Parse()
ctx := context.Background() ctx := context.Background()
bc, err := config.LoadConfig(*configPath) bc, err := config.LoadConfig(*configPath)
@ -32,6 +33,11 @@ func main() {
if *cron == "start" { if *cron == "start" {
app.Cron.Run(ctx) app.Cron.Run(ctx)
} }
// 运行指定任务并退出
if *runJob != "" {
app.Cron.RunOnce(ctx, *runJob)
return
}
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port))) log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
} }

View File

@ -5,9 +5,12 @@ server:
ollama: ollama:
base_url: "http://172.17.0.1:11434" base_url: "http://172.17.0.1:11434"
model: "qwen3:8b" # model: "qwen3:8b"
generate_model: "qwen3:8b" # generate_model: "qwen3:8b"
mapping_model: "qwen3:8b" # mapping_model: "qwen3:8b"
model: "qwen3-coder:480b-cloud"
generate_model: "qwen3-coder:480b-cloud"
mapping_model: "deepseek-v3.2:cloud"
vl_model: "qwen2.5vl:3b" vl_model: "qwen2.5vl:3b"
timeout: "120s" timeout: "120s"
level: "info" level: "info"
@ -36,7 +39,7 @@ sys:
channel_pool_len: 100 channel_pool_len: 100
channel_pool_size: 32 channel_pool_size: 32
llm_pool_len: 5 llm_pool_len: 5
heartbeat_interval: 30 heartbeat_interval: 300
key: report-api key: report-api
pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池 pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池
minIdleConns: 2 #最小空闲连接数 minIdleConns: 2 #最小空闲连接数

View File

@ -4,10 +4,13 @@ server:
host: "0.0.0.0" host: "0.0.0.0"
ollama: ollama:
base_url: "http://192.168.6.109:11434" base_url: "http://192.168.6.115:11434"
model: "qwen3-coder:480b-cloud" model: "qwen3:8b"
generate_model: "qwen3-coder:480b-cloud" generate_model: "qwen3:8b"
mapping_model: "deepseek-v3.2:cloud" mapping_model: "qwen3:8b"
# model: "qwen3-coder:480b-cloud"
# generate_model: "qwen3-coder:480b-cloud"
# mapping_model: "deepseek-v3.2:cloud"
vl_model: "qwen2.5vl:7b" vl_model: "qwen2.5vl:7b"
timeout: "120s" timeout: "120s"
level: "info" level: "info"
@ -96,7 +99,22 @@ tools:
zltxOrderAfterSaleResellerBatch: zltxOrderAfterSaleResellerBatch:
enabled: true enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai" base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
weather:
enabled: true
base_url: "https://restapi.amap.com/v3/weather/weatherInfo"
api_key: "12afbde5ab78cb7e575ff76bd0bdef2b"
cozeExpress:
enabled: true
base_url: "https://api.coze.cn"
api_key: "7582477438102552616"
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
cozeCompany:
enabled: true
base_url: "https://api.coze.cn"
api_key: "7583905168607100978"
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
zltxResellerAuthProductToManagerAndDefaultLossReason:
base_url: "https://revcl.1688sup.com/api/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason"
# eino tool 配置 # eino tool 配置
eino_tools: eino_tools:
# == 货易通 hyt == # == 货易通 hyt ==
@ -155,6 +173,26 @@ dingtalk:
# 机器人群组 # 机器人群组
bot_group_id: bot_group_id:
bbxt: 23 bbxt: 23
# 互动卡片
card:
# 卡片回调路由key - https://gateway.dev.cdlsxd.cn/zltx_api/aitest/api/v1//callback/dingtalk-card
callback_route_key: "gateway.dev.cdlsxd.cn-dingtalk-card"
# 卡片调试工具 [show展示 hide隐藏]
debug_tool_entry_show: "hide"
# 卡片模板
template:
# 基础消息卡片(title + content)
base_msg: "291468f8-a048-4132-a37e-a14365e855e9.schema"
# 内容收集卡片title + textarea + button
content_collect: "3a447814-6a3e-4a02-b48a-92c57b349d77.schema"
# 创建群聊申请title + content + button
create_group_approve: "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"
# 场景群
scene_group:
# 问题处理群模板ID
group_template_id_issue_handling: "aa3aa4fe-e709-4491-b24b-c3d5b27e86d0"
# 问题处理群模板机器人ID
group_template_robot_id_issue_handling: "VqgJYpB91j3RnB217690607273471011"
qywx: qywx:
corp_id: "ww48151f694fb8ec67" corp_id: "ww48151f694fb8ec67"
@ -177,6 +215,16 @@ default_prompt:
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。 若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
' '
user_prompt: '识别图片内容' user_prompt: '识别图片内容'
# 权限配置 # 权限配置
permissionConfig: permissionConfig:
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId=" permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
# 知识库配置
knowledge_config:
base_url: "http://192.168.6.115:9600"
tenant_id: "default"
mode: "naive"
stream: true
think: false
only_rag: true

View File

@ -4,10 +4,10 @@ server:
host: "0.0.0.0" host: "0.0.0.0"
ollama: ollama:
base_url: "http://192.168.6.115:11434" base_url: "http://host.docker.internal:11434"
model: "qwen3:8b" model: "qwen3-coder:480b-cloud"
generate_model: "qwen3:8b" generate_model: "qwen3-coder:480b-cloud"
mapping_model: "qwen3:8b" mapping_model: "deepseek-v3.2:cloud"
vl_model: "gemini-3-pro-preview" vl_model: "gemini-3-pro-preview"
timeout: "120s" timeout: "120s"
level: "info" level: "info"
@ -163,6 +163,26 @@ dingtalk:
# 机器人群组 # 机器人群组
bot_group_id: bot_group_id:
bbxt: 23 bbxt: 23
# 互动卡片
card:
# 卡片回调路由key
callback_route_key: "gateway.dev.cdlsxd.cn-dingtalk-card"
# 卡片调试工具 [show展示 hide隐藏]
debug_tool_entry_show: "show"
# 卡片模板
template:
# 基础消息卡片(title + content)
base_msg: "291468f8-a048-4132-a37e-a14365e855e9.schema"
# 内容收集卡片title + textarea + button
content_collect: "3a447814-6a3e-4a02-b48a-92c57b349d77.schema"
# 创建群聊申请title + content + button
create_group_approve: "faad6d5d-726d-467f-a6ba-28c1930aa5f3.schema"
# 场景群
scene_group:
# 问题处理群模板ID
group_template_id_issue_handling: "aa3aa4fe-e709-4491-b24b-c3d5b27e86d0"
# 问题处理群模板机器人ID
group_template_robot_id_issue_handling: "VqgJYpB91j3RnB217690607273471011"
qywx: qywx:
corp_id: "ww48151f694fb8ec67" corp_id: "ww48151f694fb8ec67"
@ -188,6 +208,15 @@ default_prompt:
permissionConfig: permissionConfig:
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId=" permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
# 知识库配置
knowledge_config:
base_url: "http://192.168.6.115:9600"
tenant_id: "default"
mode: "naive"
stream: true
think: false
only_rag: true
# llm 服务配置 # llm 服务配置
llm: llm:
providers: providers:

1
go.mod
View File

@ -65,6 +65,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/duke-git/lancet/v2 v2.3.8 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eino-contrib/jsonschema v1.0.3 // indirect github.com/eino-contrib/jsonschema v1.0.3 // indirect
github.com/eino-contrib/ollama v0.1.0 // indirect github.com/eino-contrib/ollama v0.1.0 // indirect

2
go.sum
View File

@ -156,6 +156,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/duke-git/lancet/v2 v2.3.8 h1:dlkqn6Nj2LRWFuObNxttkMHxrFeaV6T26JR8jbEVbPg=
github.com/duke-git/lancet/v2 v2.3.8/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=

View File

@ -35,10 +35,10 @@ func NewAdviceAdvicerBiz(
} }
} }
func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInitReq) error { func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInitReq) (int32, error) {
birth, err := time.Parse("2006-01-02", data.Birth) birth, err := time.Parse("2006-01-02", data.Birth)
if err != nil { if err != nil {
return err return 0, err
} }
param := &model.AiAdviceAdvicer{ param := &model.AiAdviceAdvicer{
AdvicerID: data.AdvicerID, AdvicerID: data.AdvicerID,
@ -49,13 +49,13 @@ func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInit
WorkingYears: data.WorkingYears, WorkingYears: data.WorkingYears,
} }
if param.AdvicerID == 0 { if param.AdvicerID == 0 {
_, err = a.advicerImpl.Add(param) err = a.advicerImpl.AddWithData(param)
} else { } else {
cond := builder.NewCond() cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID}) cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
err = a.advicerImpl.UpdateByCond(&cond, param) err = a.advicerImpl.UpdateByCond(&cond, param)
} }
return err return param.AdvicerID, err
} }
func (a *AdviceAdvicerBiz) List(ctx context.Context, data *entitys.AdvicerListReq) ([]map[string]interface{}, error) { func (a *AdviceAdvicerBiz) List(ctx context.Context, data *entitys.AdvicerListReq) ([]map[string]interface{}, error) {
@ -66,14 +66,14 @@ func (a *AdviceAdvicerBiz) List(ctx context.Context, data *entitys.AdvicerListRe
return list, err return list, err
} }
func (a *AdviceAdvicerBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerVersionAddReq) (err error) { func (a *AdviceAdvicerBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerVersionAddReq) (id interface{}, err error) {
cond := builder.NewCond() cond := builder.NewCond()
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID}) cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
_, err = a.advicerImpl.GetOneBySearch(&cond) _, err = a.advicerImpl.GetOneBySearch(&cond)
if err != nil { if err != nil {
return errors.New("顾问不存在") return 0, errors.New("顾问不存在")
} }
_, err = a.mongo.Co(a.advicerVersionMongo).InsertOne(ctx, &mongo_model.AdvicerVersionMongo{ res, err := a.mongo.Co(a.advicerVersionMongo).InsertOne(ctx, &mongo_model.AdvicerVersionMongo{
AdvicerId: param.AdvicerID, AdvicerId: param.AdvicerID,
VersionDesc: param.VersionDesc, VersionDesc: param.VersionDesc,
DialectFeatures: param.DialectFeatures, DialectFeatures: param.DialectFeatures,
@ -83,8 +83,10 @@ func (a *AdviceAdvicerBiz) VersionAdd(ctx context.Context, param *entitys.Advice
SignatureDialogues: param.SignatureDialogues, SignatureDialogues: param.SignatureDialogues,
LastUpdateTime: time.Now(), LastUpdateTime: time.Now(),
}) })
if err != nil {
return err return nil, err
}
return res.InsertedID, err
} }
func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionUpdateReq) (err error) { func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionUpdateReq) (err error) {
@ -118,7 +120,7 @@ func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, param *entitys.Advic
filter := bson.M{} filter := bson.M{}
// 1. advicer_id 条件 // 1. advicer_id 条件
if param.AdvicerId != 0 { if param.AdvicerId != 0 {
filter["AdvicerId"] = param.AdvicerId filter["advicerId"] = param.AdvicerId
} }
// 2. _id 条件 // 2. _id 条件
@ -133,7 +135,7 @@ func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, param *entitys.Advic
// 3. version_desc 模糊查询 // 3. version_desc 模糊查询
if len(param.VersionDesc) != 0 { if len(param.VersionDesc) != 0 {
// 正确的方式:指定字段名 // 正确的方式:指定字段名
filter["VersionDesc"] = bson.M{ filter["versionDesc"] = bson.M{
"$regex": primitive.Regex{ "$regex": primitive.Regex{
Pattern: param.VersionDesc, Pattern: param.VersionDesc,
Options: "i", Options: "i",

View File

@ -2,83 +2,278 @@ package biz
import ( import (
"ai_scheduler/internal/biz/llm_service/third_party" "ai_scheduler/internal/biz/llm_service/third_party"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/mongo_model"
"ai_scheduler/internal/data/impl"
dbmodel "ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
"ai_scheduler/utils" "ai_scheduler/utils"
"context" "context"
"encoding/json" "encoding/json"
"strings" "errors"
"time" "fmt"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model/responses"
"github.com/volcengine/volcengine-go-sdk/volcengine" "github.com/volcengine/volcengine-go-sdk/volcengine"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo/options"
"xorm.io/builder"
"strings"
"time"
) )
type AdviceChatBiz struct { type AdviceChatBiz struct {
hsyq *third_party.Hsyq hsyq *third_party.Hsyq
rdb *utils.Rdb rdb *utils.Rdb
aiAdviceSessionImpl *impl.AiAdviceSessionImpl
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl
advicerChatHisMongo *mongo_model.AdvicerChatHisMongo
mongo *pkg.Mongo
} }
func NewAdviceChatBiz( func NewAdviceChatBiz(
hsyq *third_party.Hsyq, hsyq *third_party.Hsyq,
rdb *utils.Rdb, rdb *utils.Rdb,
aiAdviceSessionImpl *impl.AiAdviceSessionImpl,
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl,
advicerChatHisMongo *mongo_model.AdvicerChatHisMongo,
mongo *pkg.Mongo,
) *AdviceChatBiz { ) *AdviceChatBiz {
return &AdviceChatBiz{ return &AdviceChatBiz{
hsyq: hsyq, hsyq: hsyq,
rdb: rdb, rdb: rdb,
aiAdviceSessionImpl: aiAdviceSessionImpl,
aiAdviceModelSupImpl: aiAdviceModelSupImpl,
advicerChatHisMongo: advicerChatHisMongo,
mongo: mongo,
} }
} }
func (a *AdviceChatBiz) Regis(ctx context.Context, chatData *entitys.ChatData) (string, error) { func (a *AdviceChatBiz) contextCache(ctx context.Context, chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq, projectInfo *entitys.AdvicerProjectInfoRes) (promptJson string, contextCache string, err error) {
sessionId := uuid.New().String() switch constants.Mode(projectInfo.ModelInfo.Mode) {
prompt, err := a.buildBasePrompt(ctx, chatData) case constants.ModeResponse:
prompt, err := a.buildBasePromptResponse(ctx, chatData, req)
if err != nil {
return "", "", err
}
cache, err := a.hsyq.CreateResponse(ctx, projectInfo.ModelInfo.Key, projectInfo.ModelInfo.ChatModel, prompt, "", true)
if err != nil {
return "", "", err
}
contextCache = cache.Id
promptJson = pkg.JsonStringIgonErr(prompt)
case constants.ModeContext:
prompt, err := a.buildBasePromptContext(ctx, chatData, req)
if err != nil {
return "", "", err
}
contextCache, err = a.hsyq.CreateContextCache(ctx, projectInfo.ModelInfo.Key, projectInfo.ModelInfo.ChatModel, prompt)
if err != nil {
return "", "", err
}
promptJson = pkg.JsonStringIgonErr(prompt)
default:
return "", "", fmt.Errorf("未知的mode类型%d", projectInfo.ModelInfo.Mode)
}
return
}
func (a *AdviceChatBiz) Regis(ctx context.Context, chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq, projectInfo *entitys.AdvicerProjectInfoRes) (string, error) {
promptJson, contextCache, err := a.contextCache(ctx, chatData, req, projectInfo)
if err != nil { if err != nil {
return "", err return "", err
} }
err = a.rdb.Rdb.SetEx(ctx, sessionId, pkg.JsonStringIgonErr(prompt), 3600*time.Second).Err() sessionId := uuid.New().String()
//创建会话
_, err = a.aiAdviceSessionImpl.Add(&dbmodel.AiAdviceSession{
SessionID: sessionId,
ProjectID: projectInfo.Base.ProjectID,
SupID: projectInfo.Base.ModelSupID,
AdvicerVersionID: req.AdvicerVersionId,
ClientID: req.ClientId,
TalkSkillID: req.TalkSkillId,
Mission: req.Mission,
ContextCache: contextCache,
CreateAt: time.Now(),
})
if err != nil {
return "", err
}
err = a.rdb.Rdb.SetEx(ctx, sessionId, promptJson, 3600*time.Second).Err()
return sessionId, err return sessionId, err
} }
func (a *AdviceChatBiz) Chat(ctx context.Context, chat *entitys.AdvicerChatReq) ([]string, error) { func (a *AdviceChatBiz) Chat(ctx context.Context, chat *entitys.AdvicerChatReq) (assistant mongo_model.Assistant, err error) {
var session dbmodel.AiAdviceSession
cond := builder.NewCond()
cond = cond.And(builder.Eq{"session_id": chat.SessionId})
err = a.aiAdviceSessionImpl.GetOneBySearchToStrut(&cond, &session)
if err != nil {
return assistant, err
}
if session.SessionID == "" {
return assistant, errors.New("未找到会话信息")
}
if len(chat.Content) == 0 { if len(chat.Content) == 0 {
return nil, nil return assistant, nil
} }
basePromptJson, err := a.getChatDataFromStringSessionId(ctx, chat.SessionId) var modelInfo dbmodel.AiAdviceModelSup
cond = builder.NewCond()
cond = cond.And(builder.Eq{"sup_id": session.SupID})
err = a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&cond, &modelInfo)
if err != nil { if err != nil {
return nil, err return assistant, err
} }
prompt, err := a.setContent(ctx, basePromptJson, chat.Content) if modelInfo.SupID == 0 {
return assistant, errors.New("未找到模型信息")
}
//basePromptJson, err := a.getChatDataFromStringSessionId(ctx, chat.SessionId)
//if err != nil {
// return nil, err
//}
chatHis, err := a.getChatHis(ctx, session.SessionID, 6)
prompt, err := a.buildChatPromptResponse(ctx, chat, &session, chatHis)
if err != nil { if err != nil {
return nil, err return assistant, err
} }
resContent, err := a.callLlm(ctx, prompt, fileModel) resContent, err := a.callLlmResponse(ctx, prompt, modelInfo.Key, modelInfo.ChatModel, session.ContextCache)
if err != nil { if err != nil {
return nil, err return assistant, err
} }
resSlice := strings.Split(resContent, "\n")
return resSlice, nil result := resContent.Output[0].GetOutputMessage().Content[0].GetText().GetText()
if err = json.Unmarshal([]byte(result), &assistant); err != nil {
return assistant, err
}
chatCtx, cancel := context.WithCancel(context.Background())
go func(session dbmodel.AiAdviceSession) {
defer cancel()
_, _ = a.mongo.Co(a.advicerChatHisMongo).InsertOne(chatCtx, &mongo_model.AdvicerChatHisMongo{
SessionId: chat.SessionId,
User: chat.Content,
Assistant: assistant,
InToken: resContent.Usage.InputTokens,
OutToken: resContent.Usage.OutputTokens,
CreatAt: time.Now(),
})
if assistant.MissionStatus == "fail" || assistant.MissionStatus == "complete" {
cond = builder.NewCond()
cond = cond.And(builder.Eq{"session_id": chat.SessionId})
session.MissionStatus = assistant.MissionStatus
session.MissionCompleteDesc = assistant.MissionCompleteDesc
_ = a.aiAdviceSessionImpl.UpdateByCond(&cond, session)
}
}(session)
return
} }
func (a *AdviceChatBiz) buildBasePrompt(ctx context.Context, chatData *entitys.ChatData) ([]*model.ChatCompletionMessage, error) { func (a *AdviceChatBiz) buildChatPromptResponse(ctx context.Context, chat *entitys.AdvicerChatReq, session *dbmodel.AiAdviceSession, chatList []mongo_model.AdvicerChatHisMongoEntity) ([]*responses.InputItem, error) {
var message = make([]*model.ChatCompletionMessage, 3)
var message = make([]*responses.InputItem, 3)
message[0] = &responses.InputItem{
Union: &responses.InputItem_EasyMessage{
EasyMessage: &responses.ItemEasyMessage{
Role: responses.MessageRole_system,
Content: &responses.MessageContent{Union: &responses.MessageContent_StringValue{StringValue: a.taskPrompt(session)}},
},
},
}
message[1] = &responses.InputItem{
Union: &responses.InputItem_EasyMessage{
EasyMessage: &responses.ItemEasyMessage{
Role: responses.MessageRole_system,
Content: &responses.MessageContent{Union: &responses.MessageContent_StringValue{StringValue: "历史聊天记录:\n" + pkg.JsonStringIgonErr(chatList)}},
},
},
}
message[2] = &responses.InputItem{
Union: &responses.InputItem_EasyMessage{
EasyMessage: &responses.ItemEasyMessage{
Role: responses.MessageRole_user,
Content: &responses.MessageContent{Union: &responses.MessageContent_StringValue{StringValue: chat.Content}},
},
},
}
return message, nil
}
func (a *AdviceChatBiz) getChatHis(ctx context.Context, sessionId string, limit int64) (chatList []mongo_model.AdvicerChatHisMongoEntity, err error) {
chatList = make([]mongo_model.AdvicerChatHisMongoEntity, 0)
filter := bson.M{}
filter["sessionId"] = sessionId
cursor, err := a.mongo.Co(a.advicerChatHisMongo).Find(ctx, filter, options.Find().SetLimit(limit))
if err != nil {
return chatList, err
}
for cursor.Next(ctx) {
var chatHIS mongo_model.AdvicerChatHisMongo
if err := cursor.Decode(&chatHIS); err != nil {
return nil, err
}
chatList = append(chatList, chatHIS.Entity())
}
if err := cursor.Err(); err != nil {
return nil, err
}
return
}
func (a *AdviceChatBiz) buildChatPrompt(ctx context.Context, chat *entitys.AdvicerChatReq, session *dbmodel.AiAdviceSession, modelInfo *dbmodel.AiAdviceModelSup) (model.ContextChatCompletionRequest, error) {
var message = make([]*model.ChatCompletionMessage, 2)
message[0] = &model.ChatCompletionMessage{
Role: model.ChatMessageRoleUser,
Content: &model.ChatCompletionMessageContent{
StringValue: volcengine.String(chat.Content),
},
}
message[1] = &model.ChatCompletionMessage{
Role: model.ChatMessageRoleAssistant,
Content: &model.ChatCompletionMessageContent{
StringValue: volcengine.String(a.taskPrompt(session)),
},
}
req := model.ContextChatCompletionRequest{
ContextID: session.ContextCache,
Model: modelInfo.ChatModel,
Messages: message,
Stream: false,
}
return req, nil
}
func (a *AdviceChatBiz) taskPrompt(session *dbmodel.AiAdviceSession) string {
//type mission struct {
// missionName string
// status string
// missionCompleteDesc string
//}
//var m = &mission{
// missionName: session.Mission,
// status: pkg.Ter(session.MissionStatus == 1, "进行中", "已完成"),
// missionCompleteDesc: session.MissionCompleteDesc,
//}
//missionJon, _ := json.Marshal(m)
return "[当前时间]" + time.Now().Format("2006-01-02 15:04:05")
}
func (a *AdviceChatBiz) buildBasePromptContext(ctx context.Context, chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq) ([]*model.ChatCompletionMessage, error) {
var message = make([]*model.ChatCompletionMessage, 2)
message[0] = &model.ChatCompletionMessage{ message[0] = &model.ChatCompletionMessage{
Role: model.ChatMessageRoleSystem, Role: model.ChatMessageRoleSystem,
Content: &model.ChatCompletionMessageContent{ Content: &model.ChatCompletionMessageContent{
StringValue: volcengine.String(a.sysPrompt(chatData)), StringValue: volcengine.String(a.sysPrompt(chatData, req)),
}, },
} }
message[1] = &model.ChatCompletionMessage{ message[1] = &model.ChatCompletionMessage{
Role: model.ChatMessageRoleUser, Role: model.ChatMessageRoleSystem,
Content: &model.ChatCompletionMessageContent{
StringValue: volcengine.String("{{chat_content}}"),
},
}
message[2] = &model.ChatCompletionMessage{
Role: model.ChatMessageRoleAssistant,
Content: &model.ChatCompletionMessageContent{ Content: &model.ChatCompletionMessageContent{
StringValue: volcengine.String(a.assistantPrompt(chatData)), StringValue: volcengine.String(a.assistantPrompt(chatData)),
}, },
@ -86,7 +281,28 @@ func (a *AdviceChatBiz) buildBasePrompt(ctx context.Context, chatData *entitys.C
return message, nil return message, nil
} }
func (a *AdviceChatBiz) setContent(ctx context.Context, basePromptJson string, content string) ([]*model.ChatCompletionMessage, error) { func (a *AdviceChatBiz) buildBasePromptResponse(ctx context.Context, chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq) ([]*responses.InputItem, error) {
var message = make([]*responses.InputItem, 2)
message[0] = &responses.InputItem{
Union: &responses.InputItem_EasyMessage{
EasyMessage: &responses.ItemEasyMessage{
Role: responses.MessageRole_system,
Content: &responses.MessageContent{Union: &responses.MessageContent_StringValue{StringValue: a.sysPrompt(chatData, req)}},
},
},
}
message[1] = &responses.InputItem{
Union: &responses.InputItem_EasyMessage{
EasyMessage: &responses.ItemEasyMessage{
Role: responses.MessageRole_system,
Content: &responses.MessageContent{Union: &responses.MessageContent_StringValue{StringValue: a.assistantPrompt(chatData)}},
},
},
}
return message, nil
}
func (a *AdviceChatBiz) setContent(ctx context.Context, basePromptJson string, content string, session *dbmodel.AiAdviceSession) ([]*model.ChatCompletionMessage, error) {
promptJson := strings.ReplaceAll(basePromptJson, "{{chat_content}}", content) promptJson := strings.ReplaceAll(basePromptJson, "{{chat_content}}", content)
var basePrompt []*model.ChatCompletionMessage var basePrompt []*model.ChatCompletionMessage
err := json.Unmarshal([]byte(promptJson), &basePrompt) err := json.Unmarshal([]byte(promptJson), &basePrompt)
@ -97,8 +313,14 @@ func (a *AdviceChatBiz) setContent(ctx context.Context, basePromptJson string, c
return basePrompt, nil return basePrompt, nil
} }
func (a *AdviceChatBiz) sysPrompt(chatData *entitys.ChatData) string { func (a *AdviceChatBiz) sysPrompt(chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq) string {
return "【场景】现在你是专业的顾问我们在聊天软件里面聊天当前时间是2026/01/30 16:13:30\n【要求】提供的json内容包含顾问项目对话技巧客户信息请根据json内容进行模仿真人进行对话** 绝对不能让对方看出来你是ai!**\n【规则】\n1.必须模仿顾问的聊天方式不能有markdown格式聊天内容必须贴近聊天软件的正常聊天非客户要求不要自顾自的介绍项目开场白不要太长\n2.不要有任何特殊格式,有比较长的对话内容注意提行\n3.尽量邀约对方来项目上来\n4.不能有任何画外音的文字格式出现!比如(等待对方回复)" var prompt strings.Builder
prompt.WriteString(constants.BasePrompt)
prompt.WriteString(req.Mission)
prompt.WriteString(constants.BasePrompt2)
return prompt.String()
} }
func (a *AdviceChatBiz) assistantPrompt(chatData *entitys.ChatData) string { func (a *AdviceChatBiz) assistantPrompt(chatData *entitys.ChatData) string {
@ -115,10 +337,18 @@ func (a *AdviceChatBiz) getChatDataFromStringSessionId(ctx context.Context, sess
return cache.Val(), cache.Err() return cache.Val(), cache.Err()
} }
func (a *AdviceChatBiz) callLlm(ctx context.Context, prompt []*model.ChatCompletionMessage, modelName string) (string, error) { func (a *AdviceChatBiz) callLlm(ctx context.Context, request model.ContextChatCompletionRequest, key string) (string, error) {
res, err := a.hsyq.RequestHsyq(ctx, key, modelName, prompt) res, err := a.hsyq.ChatWithRequest(ctx, key, request)
if err != nil { if err != nil {
return "", err return "", err
} }
return *res.Choices[0].Message.Content.StringValue, nil return *res.Choices[0].Message.Content.StringValue, nil
} }
func (a *AdviceChatBiz) callLlmResponse(ctx context.Context, request []*responses.InputItem, key string, modelName string, id string) (*responses.ResponseObject, error) {
res, err := a.hsyq.CreateResponse(ctx, key, modelName, request, id, false)
if err != nil {
return nil, err
}
return res, nil
}

View File

@ -29,9 +29,9 @@ func NewAdviceClientBiz(
} }
} }
func (a *AdviceClientBiz) Add(ctx context.Context, param *entitys.AdvicerClientAddReq) (err error) { func (a *AdviceClientBiz) Add(ctx context.Context, param *entitys.AdvicerClientAddReq) (id interface{}, err error) {
_, err = a.mongo.Co(a.AdvicerClientMongo).InsertOne(ctx, &mongo_model.AdvicerClientMongo{ res, err := a.mongo.Co(a.AdvicerClientMongo).InsertOne(ctx, &mongo_model.AdvicerClientMongo{
ProjectId: param.ProjectId, ProjectId: param.ProjectId,
AdvicerId: param.AdvicerId, AdvicerId: param.AdvicerId,
PersonalInfo: param.PersonalInfo, PersonalInfo: param.PersonalInfo,
@ -41,8 +41,10 @@ func (a *AdviceClientBiz) Add(ctx context.Context, param *entitys.AdvicerClientA
DecisionProfile: param.DecisionProfile, DecisionProfile: param.DecisionProfile,
LastUpdateTime: time.Now(), LastUpdateTime: time.Now(),
}) })
if err != nil {
return err return nil, err
}
return res.InsertedID, err
} }
func (a *AdviceClientBiz) Update(ctx context.Context, param *entitys.AdvicerrClientUpdateReq) (err error) { func (a *AdviceClientBiz) Update(ctx context.Context, param *entitys.AdvicerrClientUpdateReq) (err error) {

View File

@ -149,7 +149,7 @@ func (a *AdviceFileBiz) callLlm(ctx context.Context, prompt string, modelName st
StringValue: volcengine.String(prompt), StringValue: volcengine.String(prompt),
}, },
} }
res, err := a.hsyq.RequestHsyq(ctx, key, modelName, message) res, err := a.hsyq.Chat(ctx, key, modelName, message)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -1,10 +1,11 @@
package biz package biz
import ( import (
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/data/mongo_model" "ai_scheduler/internal/data/mongo_model"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
"errors"
"fmt" "fmt"
"time" "time"
@ -12,26 +13,60 @@ import (
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/bson/primitive"
"xorm.io/builder"
) )
type AdviceProjectBiz struct { type AdviceProjectBiz struct {
AdvicerProjectMongo *mongo_model.AdvicerProjectMongo AdvicerProjectMongo *mongo_model.AdvicerProjectMongo
mongo *pkg.Mongo adviceProjectImpl *impl.AdviceProjectImpl
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl
mongo *pkg.Mongo
} }
func NewAdviceProjectBiz( func NewAdviceProjectBiz(
advicerProjectMongo *mongo_model.AdvicerProjectMongo, advicerProjectMongo *mongo_model.AdvicerProjectMongo,
adviceProjectImpl *impl.AdviceProjectImpl,
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl,
mongo *pkg.Mongo, mongo *pkg.Mongo,
) *AdviceProjectBiz { ) *AdviceProjectBiz {
return &AdviceProjectBiz{ return &AdviceProjectBiz{
AdvicerProjectMongo: advicerProjectMongo, AdvicerProjectMongo: advicerProjectMongo,
mongo: mongo, mongo: mongo,
adviceProjectImpl: adviceProjectImpl,
aiAdviceModelSupImpl: aiAdviceModelSupImpl,
} }
} }
func (a *AdviceProjectBiz) Add(ctx context.Context, param *entitys.AdvicerProjectAddReq) (err error) { func (a *AdviceProjectBiz) BaseAdd(ctx context.Context, param *entitys.AdvicerProjectBaseAddReq) (res *entitys.AdvicerProjectBaseAddRes, err error) {
add := &model.AiAdviceProject{
Name: param.Name,
ModelSupID: param.ModelSupId,
}
err = a.adviceProjectImpl.AddWithData(add)
if err != nil {
return nil, err
}
return &entitys.AdvicerProjectBaseAddRes{
ProjectId: add.ProjectID,
}, err
}
_, err = a.mongo.Co(a.AdvicerProjectMongo).InsertOne(ctx, &mongo_model.AdvicerProjectMongo{ func (a *AdviceProjectBiz) BaseUpdate(ctx context.Context, param *entitys.AdvicerProjectBaseUpdateReq) (err error) {
if param.ProjectId == 0 {
return
}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"project_id": param.ProjectId})
err = a.adviceProjectImpl.UpdateByCond(&cond, &model.AiAdviceProject{
Name: param.Name,
ModelSupID: param.ModelSupId,
})
return err
}
func (a *AdviceProjectBiz) Add(ctx context.Context, param *entitys.AdvicerProjectAddReq) (id interface{}, err error) {
res, err := a.mongo.Co(a.AdvicerProjectMongo).InsertOne(ctx, &mongo_model.AdvicerProjectMongo{
ProjectId: param.ProjectId, ProjectId: param.ProjectId,
ProjectInfo: param.ProjectInfo, ProjectInfo: param.ProjectInfo,
RegionValue: param.RegionValue, RegionValue: param.RegionValue,
@ -42,22 +77,27 @@ func (a *AdviceProjectBiz) Add(ctx context.Context, param *entitys.AdvicerProjec
LastUpdateTime: time.Now(), LastUpdateTime: time.Now(),
}) })
return err return res.InsertedID, err
} }
func (a *AdviceProjectBiz) Update(ctx context.Context, param *entitys.AdvicerrProjectUpdateReq) (err error) { func (a *AdviceProjectBiz) Update(ctx context.Context, param *entitys.AdvicerrProjectUpdateReq) (err error) {
filter := bson.M{} filter := bson.M{}
if len(param.Id) == 0 { if len(param.Id) != 0 {
return errors.New("ID不能为空") objectID, err := primitive.ObjectIDFromHex(param.Id)
if err != nil {
return fmt.Errorf("ID转换失败: %w", err)
}
filter["_id"] = objectID
} }
objectID, err := primitive.ObjectIDFromHex(param.Id) if param.ProjectId != 0 {
if err != nil {
return fmt.Errorf("ID转换失败: %w", err) filter["projectId"] = param.ProjectId
} }
filter["_id"] = objectID
update := bson.M{ update := bson.M{
"$set": &mongo_model.AdvicerProjectMongo{ "$set": &mongo_model.AdvicerProjectMongo{
ProjectId: param.ProjectId, ProjectId: param.ProjectId,
ProjectInfo: param.ProjectInfo,
RegionValue: param.RegionValue, RegionValue: param.RegionValue,
CompetitionComparison: param.CompetitionComparison, CompetitionComparison: param.CompetitionComparison,
CoreSellingPoints: param.CoreSellingPoints, CoreSellingPoints: param.CoreSellingPoints,
@ -70,7 +110,53 @@ func (a *AdviceProjectBiz) Update(ctx context.Context, param *entitys.AdvicerrPr
return res.Err() return res.Err()
} }
func (a *AdviceProjectBiz) Info(ctx context.Context, param *entitys.AdvicerProjectInfoReq) (info mongo_model.AdvicerProjectMongo, err error) { func (a *AdviceProjectBiz) Info(ctx context.Context, param *entitys.AdvicerProjectInfoReq) (info *entitys.AdvicerProjectInfoRes, err error) {
configInfo, err := a.ConfigInfo(ctx, param)
if err != nil {
return nil, err
}
baseInfo, err := a.BaseInfo(configInfo.ProjectId)
if err != nil {
return nil, err
}
supInfo, err := a.ModelInfo(baseInfo.ModelSupID)
if err != nil {
return nil, err
}
return &entitys.AdvicerProjectInfoRes{
ConfigInfo: configInfo,
Base: baseInfo,
ModelInfo: supInfo,
}, nil
}
func (a *AdviceProjectBiz) BaseInfo(projectId int32) (baseInfo model.AiAdviceProject, err error) {
if projectId == 0 {
return
}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"project_id": projectId})
err = a.adviceProjectImpl.GetOneBySearchToStrut(&cond, &baseInfo)
if err != nil {
return baseInfo, err
}
return baseInfo, nil
}
func (a *AdviceProjectBiz) ModelInfo(supId int32) (supInfo model.AiAdviceModelSup, err error) {
if supId == 0 {
return
}
cond := builder.NewCond()
cond = cond.And(builder.Eq{"sup_id": supId})
err = a.aiAdviceModelSupImpl.GetOneBySearchToStrut(&cond, &supInfo)
if err != nil {
return supInfo, err
}
return supInfo, nil
}
func (a *AdviceProjectBiz) ConfigInfo(ctx context.Context, param *entitys.AdvicerProjectInfoReq) (info mongo_model.AdvicerProjectMongo, err error) {
filter := bson.M{} filter := bson.M{}
if param.ProjectId != 0 { if param.ProjectId != 0 {

View File

@ -29,9 +29,9 @@ func NewAdviceSkillBiz(
} }
} }
func (a *AdviceSkillBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerTalkSkillAddReq) (err error) { func (a *AdviceSkillBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerTalkSkillAddReq) (id interface{}, err error) {
_, err = a.mongo.Co(a.AdvicerTalkSkillMongo).InsertOne(ctx, &mongo_model.AdvicerTalkSkillMongo{ res, err := a.mongo.Co(a.AdvicerTalkSkillMongo).InsertOne(ctx, &mongo_model.AdvicerTalkSkillMongo{
ProjectId: param.ProjectId, ProjectId: param.ProjectId,
AdvicerId: param.AdvicerId, AdvicerId: param.AdvicerId,
Desc: param.Desc, Desc: param.Desc,
@ -42,8 +42,10 @@ func (a *AdviceSkillBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerT
CommunicationRhythm: param.CommunicationRhythm, CommunicationRhythm: param.CommunicationRhythm,
LastUpdateTime: time.Now(), LastUpdateTime: time.Now(),
}) })
if err != nil {
return err return nil, err
}
return res.InsertedID, err
} }
func (a *AdviceSkillBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerTalkSkillUpdateReq) (err error) { func (a *AdviceSkillBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerTalkSkillUpdateReq) (err error) {

294
internal/biz/callback.go Normal file
View File

@ -0,0 +1,294 @@
package biz
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/pkg/utils_ollama"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/gofiber/fiber/v2/log"
"github.com/ollama/ollama/api"
)
type CallbackBiz struct {
cfg *config.Config
ollamaClient *utils_ollama.Client
dingtalkCardClient *dingtalk.CardClient
botConfigImpl *impl.BotConfigImpl
}
func NewCallbackBiz(
cfg *config.Config,
ollamaClient *utils_ollama.Client,
dingtalkCardClient *dingtalk.CardClient,
botConfigImpl *impl.BotConfigImpl,
) *CallbackBiz {
return &CallbackBiz{
cfg: cfg,
ollamaClient: ollamaClient,
dingtalkCardClient: dingtalkCardClient,
botConfigImpl: botConfigImpl,
}
}
// IssueHandlingGroup 问题处理群机器人回调
// 能力1 通过[内容提取] 宏分析用户QA问题调出QA表单卡片
// 能力2 通过[QA收集] 宏,收集用户反馈,写入知识库
// 能力3 通过[知识库查询] 宏,查询知识库,返回答案
func (c *CallbackBiz) IssueHandlingGroup(data chatbot.BotCallbackDataModel) error {
// 能力1、2分析用户QA问题写入知识库
if strings.Contains(data.Text.Content, "[内容提取]") || strings.Contains(data.Text.Content, "[QA收集]") {
c.issueHandlingExtractContent(data)
}
// 能力3查询知识库返回答案
if strings.Contains(data.Text.Content, "[知识库查询]") {
c.issueHandlingQueryKnowledgeBase(data)
}
return nil
}
// 问题处理群机器人内容提取
func (c *CallbackBiz) issueHandlingExtractContent(data chatbot.BotCallbackDataModel) {
// 1.提取用户输入
prompt := fmt.Sprintf(constants.IssueHandlingExtractContentPrompt, data.Text.Content)
log.Infof("问题提取提示词: %s", prompt)
// LLM 提取
generateResp, err := c.ollamaClient.Generation(context.Background(), &api.GenerateRequest{
Model: c.cfg.Ollama.GenerateModel,
Prompt: prompt,
Stream: util.AnyToPoint(false),
})
if err != nil {
log.Errorf("问题提取失败: %v", err)
return
}
// 解析 JSON 响应
var resp struct {
Question string `json:"question"`
Answer string `json:"answer"`
Confidence string `json:"confidence"`
}
if err := json.Unmarshal([]byte(generateResp.Response), &resp); err != nil {
log.Errorf("解析 JSON 响应失败: %v", err)
return
}
// 2.获取应用AppKey
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
if err != nil {
log.Errorf("获取应用配置失败: %v", err)
return
}
// 3.创建并投放卡片
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode) // 构建卡片 OutTrackId
_, err = c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.ContentCollect),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("HTTP"),
CallbackRouteKey: tea.String(c.cfg.Dingtalk.Card.CallbackRouteKey),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"_CARD_DEBUG_TOOL_ENTRY": tea.String(c.cfg.Dingtalk.Card.DebugToolEntryShow), // 调试字段
"title": tea.String("QA知识收集"),
"button_display": tea.String("true"),
"textarea_display": tea.String("normal"),
"action_id": tea.String("collect_qa"),
"tenant_id": tea.String(constants.KnowledgeTenantIdDefault),
"question": tea.String(resp.Question),
"answer": tea.String(resp.Answer),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(c.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
},
},
)
}
// 问题处理群机器人查询知识库
func (c *CallbackBiz) issueHandlingQueryKnowledgeBase(data chatbot.BotCallbackDataModel) {
// 获取应用配置
appKey, err := c.botConfigImpl.GetRobotAppKey(data.RobotCode)
if err != nil {
log.Errorf("应用机器人配置不存在: %s, err: %v", data.RobotCode, err)
return
}
// 创建卡片
outTrackId := constants.BuildCardOutTrackId(data.ConversationId, data.RobotCode)
_, err = c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String(data.Text.Content),
"markdown": tea.String("知识库检索中..."),
},
},
OutTrackId: tea.String(outTrackId),
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + data.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(data.RobotCode),
},
},
)
// 查询知识库
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: constants.KnowledgeTenantIdDefault,
Query: data.Text.Content,
Mode: constants.KnowledgeModeMix,
Stream: false,
Think: false,
OnlyRAG: true,
})
if err != nil {
log.Errorf("查询知识库失败: %v", err)
return
}
knowledgeRespBytes, err := io.ReadAll(knowledgeResp)
if err != nil {
log.Errorf("读取知识库响应失败: %v", err)
return
}
// 卡片更新
message, isRetrieved, err := knowledge_base.ParseOpenAIHTTPData(string(knowledgeRespBytes))
if err != nil {
log.Errorf("读取知识库 SSE 数据失败: %v", err)
return
}
content := message.Content
if !isRetrieved {
content = "知识库未检测到匹配信息,请核查知识库数据是否正确。"
}
// 卡片更新
_, err = c.dingtalkCardClient.UpdateCard(
appKey,
&card_1_0.UpdateCardRequest{
OutTrackId: tea.String(outTrackId),
CardData: &card_1_0.UpdateCardRequestCardData{
CardParamMap: map[string]*string{
"markdown": tea.String(content),
},
},
CardUpdateOptions: &card_1_0.UpdateCardRequestCardUpdateOptions{
UpdateCardDataByKey: tea.Bool(true),
},
},
)
if err != nil {
log.Errorf("更新卡片失败: %v", err)
return
}
}
// IssueHandlingCollectQA 问题处理群机器人 QA 收集回调
func (c *CallbackBiz) IssueHandlingCollectQA(data card.CardRequest) *card.CardResponse {
// 确认提交,文本写入知识库
var question, answer string
if data.CardActionData.CardPrivateData.Params["submit"] == "submit" {
question = data.CardActionData.CardPrivateData.Params["question"].(string)
answer = data.CardActionData.CardPrivateData.Params["answer"].(string)
tenantID := data.CardActionData.CardPrivateData.Params["tenant_id"].(string)
// 协程执行耗时操作,防止阻塞
util.SafeGo("inject_knowledge_base", func() {
knowledgeBase := knowledge_base.New(c.cfg.KnowledgeConfig)
err := knowledgeBase.IngestBatchQA(&knowledge_base.IngestBacthQARequest{
TenantID: tenantID,
QAList: []*knowledge_base.QA{
{
Question: question,
Answer: answer,
},
},
})
if err != nil {
log.Errorf("注入知识库失败: %v", err)
} else {
log.Infof("注入知识库成功: tenantID=%s", tenantID)
}
// 解析当前卡片的 ConversationId 和 robotCode
conversationId, robotCode := constants.ParseCardOutTrackId(data.OutTrackId)
// 获取应用配置
appKey, err := c.botConfigImpl.GetRobotAppKey(robotCode)
if err != nil {
log.Errorf("获取应用机器人配置失败: %v", err)
return
}
// 发送卡片通知用户注入成功
outTrackId := constants.BuildCardOutTrackId(conversationId, robotCode)
c.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(c.cfg.Dingtalk.Card.Template.BaseMsg),
OutTrackId: tea.String(outTrackId),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("QA知识收集结果"),
"markdown": tea.String("[Get] **成功**"),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + conversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(robotCode),
},
},
)
})
}
// 取消提交,禁用输入框
resp := &card.CardResponse{
CardUpdateOptions: &card.CardUpdateOptions{
UpdateCardDataByKey: true,
},
CardData: &card.CardDataDto{
CardParamMap: map[string]string{
"textarea_display": "disabled",
},
},
}
if question != "" {
resp.CardData.CardParamMap["question"] = question
}
if answer != "" {
resp.CardData.CardParamMap["answer"] = answer
}
return resp
}

View File

@ -8,11 +8,13 @@ import (
"ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/bbxt" "ai_scheduler/internal/tools/bbxt"
"ai_scheduler/tmpl/dataTemp" "ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
"context" "context"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
@ -21,9 +23,17 @@ import (
"strings" "strings"
"time" "time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
dingtalkPkg "ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/util"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/duke-git/lancet/v2/slice"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
"github.com/redis/go-redis/v9"
"xorm.io/builder" "xorm.io/builder"
) )
@ -40,12 +50,19 @@ type DingTalkBotBiz struct {
botGroupQywxImpl *impl.BotGroupQywxImpl botGroupQywxImpl *impl.BotGroupQywxImpl
toolManager *tools.Manager toolManager *tools.Manager
chatHis *impl.BotChatHisImpl chatHis *impl.BotChatHisImpl
botUserImpl *impl.BotUserImpl
conf *config.Config conf *config.Config
cardSend *dingtalk.SendCardClient cardSend *dingtalk.SendCardClient
qywxGroupHandle *qywx.Group qywxGroupHandle *qywx.Group
groupConfigBiz *GroupConfigBiz groupConfigBiz *GroupConfigBiz
reportDailyCacheImpl *impl.ReportDailyCacheImpl reportDailyCacheImpl *impl.ReportDailyCacheImpl
macro *do.Macro macro *do.Macro
dingtalkOauth2Client *dingtalkPkg.Oauth2Client
dingTalkOld *dingtalkPkg.OldClient
dingtalkCardClient *dingtalkPkg.CardClient
redisCli *redis.Client
issueImpl *impl.IssueImpl
sysImpl *impl.SysImpl
} }
// NewDingTalkBotBiz // NewDingTalkBotBiz
@ -54,14 +71,22 @@ func NewDingTalkBotBiz(
handle *do.Handle, handle *do.Handle,
botConfigImpl *impl.BotConfigImpl, botConfigImpl *impl.BotConfigImpl,
botGroupImpl *impl.BotGroupImpl, botGroupImpl *impl.BotGroupImpl,
botGroupConfigImpl *impl.BotGroupConfigImpl,
dingTalkUser *dingtalk.User, dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl, chatHis *impl.BotChatHisImpl,
botUserImpl *impl.BotUserImpl,
reportDailyCacheImpl *impl.ReportDailyCacheImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl,
toolManager *tools.Manager, toolManager *tools.Manager,
conf *config.Config, conf *config.Config,
cardSend *dingtalk.SendCardClient, cardSend *dingtalk.SendCardClient,
groupConfigBiz *GroupConfigBiz, groupConfigBiz *GroupConfigBiz,
macro *do.Macro, macro *do.Macro,
dingtalkOauth2Client *dingtalkPkg.Oauth2Client,
dingTalkOld *dingtalkPkg.OldClient,
dingtalkCardClient *dingtalkPkg.CardClient,
rdb *utils.Rdb,
issueImpl *impl.IssueImpl,
sysImpl *impl.SysImpl,
) *DingTalkBotBiz { ) *DingTalkBotBiz {
return &DingTalkBotBiz{ return &DingTalkBotBiz{
do: do, do: do,
@ -71,12 +96,20 @@ func NewDingTalkBotBiz(
dingTalkUser: dingTalkUser, dingTalkUser: dingTalkUser,
groupConfigBiz: groupConfigBiz, groupConfigBiz: groupConfigBiz,
botGroupImpl: botGroupImpl, botGroupImpl: botGroupImpl,
botGroupConfigImpl: botGroupConfigImpl,
toolManager: toolManager, toolManager: toolManager,
chatHis: chatHis, chatHis: chatHis,
botUserImpl: botUserImpl,
conf: conf, conf: conf,
cardSend: cardSend, cardSend: cardSend,
reportDailyCacheImpl: reportDailyCacheImpl, reportDailyCacheImpl: reportDailyCacheImpl,
macro: macro, macro: macro,
dingtalkOauth2Client: dingtalkOauth2Client,
dingTalkOld: dingTalkOld,
dingtalkCardClient: dingtalkCardClient,
redisCli: rdb.Rdb,
issueImpl: issueImpl,
sysImpl: sysImpl,
} }
} }
@ -126,19 +159,294 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat
return return
} }
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { // handleSingleChat 单聊处理
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新") // 先不接意图识别-仅提供问题处理
return func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error {
//requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1)) // 1. 获取用户信息
//if err != nil { user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId)
// return if err != nil {
//} return err
//requireData.ID=requireData.UserInfo.UserID }
////如果不是管理或者不是老板,则进行权限判断 requireData.ID = int32(user.UserID)
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse { requireData.UserInfo = &entitys.DingTalkUserInfo{
// UserId: int(user.UserID),
//} StaffId: user.StaffID,
//return Name: user.Name,
}
// 2. 获取历史记录 (最近6轮用户输入)
userHist, err := d.getRecentUserHistory(ctx, constants.ConversationTypeSingle, requireData.ID, d.conf.Sys.SessionLen)
if err != nil {
return err
}
// 3. 系统&问题分类(意图识别阶段)
resolveResult, err := d.resolveSystemAndIssueType(ctx, requireData, userHist)
if err != nil {
return err
}
log.Debugf("系统&分类结果: %s - %s原因%s", resolveResult.Sys.SysName, resolveResult.IssueType.Name, resolveResult.Classification.Reason)
// 4. 分类处理(后续考虑接各自的工作流/agent
switch resolveResult.IssueType.Code {
case constants.IssueTypeKnowledgeQA:
// 知识库问答
return d.handleKnowledgeQA(ctx, requireData, userHist, resolveResult)
default: // 其他问题类型
// 系统为空,再次询问
if resolveResult.Sys.SysID == 0 {
entitys.ResText(requireData.Ch, "", "\n抱歉我无法确定您咨询的是哪个系统。请告诉我具体系统名称直连天下系统、货易通系统以便我为您准确解答或安排对应的技术支持。")
return nil
}
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
}
}
// 知识库问答
func (d *DingTalkBotBiz) handleKnowledgeQA(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, userHist []model.AiBotChatHi, resolveResult *resolveSystemAndIssueTypeResult) error {
// 获取租户ID
tenantId := constants.KnowledgeTenantIdDefault
if resolveResult.Sys.KnowlegeTenantKey != "" {
tenantId = resolveResult.Sys.KnowlegeTenantKey
}
// 改写 Query (Query Rewriting)
rewrittenQuery, err := d.handle.RewriteQuery(ctx, userHist, requireData.Req.Text.Content)
var queryText = requireData.Req.Text.Content
if err == nil && rewrittenQuery != "" {
queryText = rewrittenQuery
}
log.Debugf("改写前后的Query: %s -> %s", requireData.Req.Text.Content, queryText)
// 获取知识库结果
isRetrieved, responseContent, err := d.getKnowledgeAnswer(ctx, requireData.Ch, tenantId, queryText)
if err != nil {
return err
}
if isRetrieved {
// 过一遍 LLM 判断是否真的命中知识库
isRetrieved, err = d.handle.IsAnswerRelevant(ctx, queryText, responseContent)
if err != nil {
return err
}
}
// 未匹配&全局 -> 明确具体系统
if !isRetrieved && resolveResult.Sys.SysID == 0 {
entitys.ResText(requireData.Ch, "", "\n抱歉知识库未命中无法回答您的问题。\n若您的问题是某一具体系统的请告诉我具体系统名称直连天下系统、货易通系统以便我为您准确解答。")
return nil
}
// 未匹配&指定系统 -> 拉群卡片
if !isRetrieved && resolveResult.Sys.SysID != 0 {
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉%s知识库未命中无法回答您的问题。即将为您创建群聊解答。", resolveResult.Sys.SysName))
return d.fallbackToGroupCreation(ctx, requireData, resolveResult)
}
return nil
}
// 获取知识库问答结果
func (d *DingTalkBotBiz) getKnowledgeAnswer(ctx context.Context, ch chan entitys.Response, tenantId string, queryText string) (bool, string, error) {
// 请求知识库工具
knowledgeBase := knowledge_base.New(d.conf.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: tenantId, // 后续动态接参
Query: queryText,
Mode: constants.KnowledgeModeMix,
Stream: true,
Think: false,
OnlyRAG: true,
})
if err != nil {
return false, "", fmt.Errorf("请求知识库工具失败err: %v", err)
}
// 读取知识库SSE数据
return d.groupConfigBiz.readKnowledgeSSE(knowledgeResp, ch, true)
}
type resolveSystemAndIssueTypeResult struct {
Sys model.AiSy
IssueType model.AiIssueType
Classification *do.IssueClassification
}
// 解析系统和问题类型
func (d *DingTalkBotBiz) resolveSystemAndIssueType(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, userHist []model.AiBotChatHi) (*resolveSystemAndIssueTypeResult, error) {
// 1. 获取所有系统和问题类型用于分类
allSys, err := d.sysImpl.FindAll()
if err != nil {
return nil, err
}
sysNames := slice.Map(allSys, func(_ int, sys model.AiSy) string {
return sys.SysName
})
allIssueTypes, err := d.issueImpl.IssueType.FindAll()
if err != nil {
return nil, err
}
issueTypeNames := slice.Map(allIssueTypes, func(_ int, it model.AiIssueType) string {
return it.Name
})
// 2. LLM 分类
// 系统名称
classificationSys, err := d.handle.ClassifyIssueSystem(ctx, sysNames, requireData.Req.Text.Content, userHist)
if err != nil {
return nil, err
}
// 问题类型
classificationIssueType, err := d.handle.ClassifyIssueType(ctx, issueTypeNames, sysNames, requireData.Req.Text.Content, userHist)
if err != nil {
return nil, err
}
// 合并
classification := &do.IssueClassification{
SysName: classificationSys.SysName,
IssueTypeName: classificationIssueType.IssueTypeName,
Summary: classificationIssueType.Summary,
Reason: fmt.Sprintf("系统名称推断理由:%s\n问题类型推断理由%s", classificationSys.Reason, classificationIssueType.Reason),
}
// 3. 匹配系统
var sys model.AiSy
for _, s := range allSys {
if s.SysName == classification.SysName {
sys = s
break
}
}
// 4. 匹配问题类型
var issueType model.AiIssueType
for _, it := range allIssueTypes {
if it.Name == classification.IssueTypeName {
issueType = it
break
}
}
return &resolveSystemAndIssueTypeResult{
Sys: sys,
IssueType: issueType,
Classification: classification,
}, nil
}
// handleWithSpecificSys 处理用户明确指定的系统
// func (d *DingTalkBotBiz) handleWithSpecificSys(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, sysName string) error {
// // 1. 查找系统
// var sys model.AiSy
// cond := builder.NewCond().And(builder.Eq{"sys_name": sysName})
// err := d.sysImpl.GetOneBySearchToStrut(&cond, &sys)
// if err != nil {
// if errors.Is(err, sql.ErrNoRows) {
// entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。")
// return nil
// }
// return err
// }
// // 2. 既然已经明确了系统直接尝试拉群这里假设问题类型为“其他”或由LLM再次分析
// // 为简化,这里再次调用分类逻辑,但带上已确定的系统
// return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
// }
// getRecentUserHistory 获取最近的用户输入历史
func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) {
var his []model.AiBotChatHi
cond := builder.NewCond().
And(builder.Eq{"his_type": conversationType}).
And(builder.Eq{"id": id}).
And(builder.Eq{"role": "user"})
_, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc")
if err != nil {
return nil, err
}
return his, nil
}
// 在已知系统&问题类型的情况下进行分类并拉群
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error {
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n检测到您想咨询 %s-%s 问题。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
// 查找分配规则
rule, found, _ := d.issueImpl.IssueAssignRule.FindOne(
d.issueImpl.WithSysID(resolveResult.Sys.SysID),
d.issueImpl.WithIssueTypeID(resolveResult.IssueType.ID),
d.issueImpl.WithStatus(1),
)
if !found {
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉当前系统未配置路由规则 %s-%s请联系管理员配置。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
return nil
}
var groupMember, groupMemberName []string
if rule.ID != 0 {
// 获取配置的用户
assignUsers, err := d.issueImpl.IssueAssignUser.FindAll(d.issueImpl.WithRuleID(rule.ID))
if len(assignUsers) == 0 {
log.Errorf("assign user not found for rule %d; err: %v", rule.ID, err)
return fmt.Errorf("分配用户 %d 不存在", rule.ID)
}
userIds := slice.Map(assignUsers, func(_ int, au model.AiIssueAssignUser) int32 {
return au.UserID
})
// 获取有效用户
botUsers, err := d.botUserImpl.GetByUserIds(userIds)
if err != nil {
return err
}
// 仅获取有效用户的 staff_id
for _, au := range assignUsers {
botUser, found := slice.Find(botUsers, func(_ int, bu model.AiBotUser) bool {
return bu.UserID == au.UserID
})
if found && botUser.StaffID != "" {
groupMember = append(groupMember, botUser.StaffID)
groupMemberName = append(groupMemberName, "@"+botUser.Name)
}
}
}
// 兜底处理人
if len(groupMember) == 0 {
groupMember = []string{"17415698414368678"}
}
// 合并提问者
groupMember = append([]string{requireData.Req.SenderStaffId}, groupMember...)
groupMember = slice.Unique(groupMember)
// 先回复用户
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n已检索到处理人\n%s\n是否创建群聊", strings.Join(groupMemberName, "、")))
// 发送确认卡片
groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary)
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
RobotCode: requireData.Req.RobotCode,
ConversationId: requireData.Req.ConversationId,
SenderStaffId: requireData.Req.SenderStaffId,
UserIds: groupMember,
GroupName: groupName,
Summary: resolveResult.Classification.Summary,
})
}
// createDefaultGroup 兜底发送确认卡片
func (d *DingTalkBotBiz) createDefaultGroup(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, reason string) error {
userIds := []string{requireData.Req.SenderStaffId, "17415698414368678"}
groupName := fmt.Sprintf("[未知]-%s", reason)
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
RobotCode: requireData.Req.RobotCode,
ConversationId: requireData.Req.ConversationId,
SenderStaffId: requireData.Req.SenderStaffId,
UserIds: userIds,
GroupName: groupName,
Summary: reason,
})
} }
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) { func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
@ -172,7 +480,7 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
return return
} }
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig) return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig, requireData.Req)
} }
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) { func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) {
@ -252,6 +560,9 @@ func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.
} }
messages := make([]entitys.HisMessage, 0, len(his)) messages := make([]entitys.HisMessage, 0, len(his))
for _, v := range his { for _, v := range his {
if v.Role != "user" {
continue
}
messages = append(messages, entitys.HisMessage{ messages = append(messages, entitys.HisMessage{
Role: constants.Caller(v.Role), // 用户角色 Role: constants.Caller(v.Role), // 用户角色
Content: v.Content, // 用户输入内容 Content: v.Content, // 用户输入内容
@ -411,3 +722,315 @@ func (d *DingTalkBotBiz) defaultPrompt() string {
-parameters 必须是 **转义后的 JSON 字符串** "{\"product_name\": \"京东月卡\"}" -parameters 必须是 **转义后的 JSON 字符串** "{\"product_name\": \"京东月卡\"}"
当前时间` + now + `所有的时间识别精确到秒` 当前时间` + now + `所有的时间识别精确到秒`
} }
// CreateIssueHandlingGroupAndInit 创建问题处理群聊并初始化
func (d *DingTalkBotBiz) CreateIssueHandlingGroupAndInit(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) {
// 解析 OutTrackId 以获取 SpaceId 和 BotId
spaceId, botId := constants.ParseCardOutTrackId(data.OutTrackId)
// 获取操作状态
status := data.CardActionData.CardPrivateData.Params["status"]
if status == "confirm" {
// 获取新群聊人员 (从卡片参数中统一解析)
targetUserIdsStr := data.CardActionData.CardPrivateData.Params["target_user_ids"].(string)
var userIds []string
if targetUserIdsStr != "" {
userIds = strings.Split(targetUserIdsStr, ",")
}
if len(userIds) == 0 {
return nil, errors.New("target_user_ids 参数不能为空")
}
// 创建群聊及群初始化(异步响应)
util.SafeGo("CreateIssueHandlingGroupAndInit", func() {
err := d.createIssueHandlingGroupAndInit(ctx, data.CardActionData.CardPrivateData.Params, spaceId, botId, userIds)
if err != nil {
log.Errorf("创建群聊及群初始化失败: %v", err)
}
})
}
// 构建关闭创建群组卡片按钮的响应
return d.buildCreateGroupCardResp(), nil
}
type SendGroupCreationConfirmCardParams struct {
RobotCode string
ConversationId string
SenderStaffId string
UserIds []string
GroupName string
Summary string
IsGroupChat bool
}
// SendGroupCreationConfirmCard 发送创建群聊确认卡片
func (d *DingTalkBotBiz) SendGroupCreationConfirmCard(ctx context.Context, params *SendGroupCreationConfirmCardParams) error {
// 获取人员姓名用于展示
var userNames []string
for _, uid := range params.UserIds {
if uid == params.SenderStaffId {
continue
}
user, err := d.botUserImpl.GetByStaffId(uid)
if err == nil && user != nil {
userNames = append(userNames, "@"+user.Name)
} else {
userNames = append(userNames, "@"+uid)
}
}
issueOwnerStr := strings.Join(userNames, "、")
// 获取应用配置
appKey, err := d.botConfigImpl.GetRobotAppKey(params.RobotCode)
if err != nil {
return err
}
// 构建卡片 OutTrackId
outTrackId := constants.BuildCardOutTrackId(params.SenderStaffId, params.RobotCode)
// 准备可见人员列表
var recipients []*string
if params.IsGroupChat {
// 群聊:提问者 + 负责人可见
for _, uid := range params.UserIds {
recipients = append(recipients, tea.String(uid))
}
// 确保提问者也在可见列表中
foundSender := false
for _, uid := range params.UserIds {
if uid == params.SenderStaffId {
foundSender = true
break
}
}
if !foundSender {
recipients = append(recipients, tea.String(params.SenderStaffId))
}
} else {
// 单聊:仅提问者可见
recipients = append(recipients, tea.String(params.SenderStaffId))
}
// 发送钉钉卡片
_, err = d.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.CreateGroupApprove),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("STREAM"),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("创建群聊提醒"),
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊\n\n%s", issueOwnerStr)),
"remark": tea.String("注:如若无需,忽略即可"),
"button_left": tea.String("创建群聊"),
"button_right": tea.String("忽略"),
"action_id": tea.String("create_group"),
"button_display": tea.String("true"),
"group_scope": tea.String(params.Summary),
"target_user_ids": tea.String(strings.Join(params.UserIds, ",")),
"group_name": tea.String(params.GroupName),
},
},
OpenSpaceId: tea.String("dtv1.card//IM_ROBOT." + params.SenderStaffId),
ImRobotOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImRobotOpenDeliverModel{
SpaceType: tea.String("IM_ROBOT"),
RobotCode: tea.String(params.RobotCode),
},
ImRobotOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImRobotOpenSpaceModel{
SupportForward: tea.Bool(false),
},
})
return err
}
// buildNewGroupUserIds 构建新群聊人员列表
func (d *DingTalkBotBiz) buildNewGroupUserIds(spaceId, botId, groupOwner string) ([]string, error) {
// 群id+机器人id确认一个群配置
botGroup, err := d.botGroupImpl.GetByConversationIdAndRobotCode(spaceId, botId)
if err != nil {
return nil, err
}
// 获取群配置
var groupConfig model.AiBotGroupConfig
cond := builder.NewCond().And(builder.Eq{"config_id": botGroup.ConfigID})
err = d.botGroupConfigImpl.GetOneBySearchToStrut(&cond, &groupConfig)
if err != nil {
return nil, err
}
// 获取处理人列表
issueOwnerJson := groupConfig.IssueOwner
type issueOwnerType struct {
UserId string `json:"userid"`
Name string `json:"name"`
}
var issueOwner []issueOwnerType
if err = json.Unmarshal([]byte(issueOwnerJson), &issueOwner); err != nil {
return nil, err
}
// 合并所有userid
userIds := []string{groupOwner} // 当前用户为群主
for _, owner := range issueOwner {
userIds = append(userIds, owner.UserId)
}
return userIds, nil
}
// createIssueHandlingGroupAndInit 创建问题处理群聊及群初始化
func (d *DingTalkBotBiz) createIssueHandlingGroupAndInit(ctx context.Context, callbackParams map[string]any, spaceId, botId string, userIds []string) error {
// 获取应用配置
appKey, err := d.botConfigImpl.GetRobotAppKey(botId)
if err != nil {
return err
}
// 获取 access_token
accessToken, err := d.dingtalkOauth2Client.GetAccessToken(appKey)
if err != nil {
return err
}
appKey.AccessToken = accessToken
// 创建群聊
var groupName string
if s, ok := callbackParams["group_name"].(string); ok {
groupName = s
}
_, openConversationId, err := d.createIssueHandlingGroup(ctx, accessToken, groupName, userIds)
if err != nil {
return err
}
// 添加当前机器人到新群 - SDK 有问题,后续再考虑使用
// _, err = d.dingtalkImClient.AddRobotToConversation(
// appKey,
// &im_1_0.AddRobotToConversationRequest{
// OpenConversationId: tea.String(openConversationId),
// RobotCode: tea.String(botId),
// })
// if err != nil {
// fmt.Printf("添加机器人到会话失败: %v", err)
// }
// 返回新群分享链接,直接进群 - SDK 有问题,后续再考虑使用
// newGroupShareLink, err = d.dingTalkOld.GetJoinGroupQrcode(ctx, chatId, data.UserId)
// if err != nil {
// fmt.Printf("获取入群二维码失败: %v", err)
// }
// 初始化群聊
groupScope := callbackParams["group_scope"].(string) // 群主题
d.initIssueHandlingGroup(appKey, openConversationId, groupScope)
return nil
}
// createIssueHandlingGroup 创建问题处理群聊会话
func (d *DingTalkBotBiz) createIssueHandlingGroup(ctx context.Context, accessToken string, groupName string, userIds []string) (chatId, openConversationId string, err error) {
// 是否使用模板群开关
var useTemplateGroup bool = true
if groupName == "" {
groupName = "问题处理群"
}
// 创建内部群会话
if !useTemplateGroup {
return d.dingTalkOld.CreateInternalGroupConversation(ctx, accessToken, groupName, userIds)
}
// 根据群模板ID创建群
if useTemplateGroup {
return d.dingTalkOld.CreateSceneGroupConversation(ctx, accessToken, groupName, userIds, d.conf.Dingtalk.SceneGroup.GroupTemplateIDIssueHandling)
}
return
}
// initIssueHandlingGroup 初始化问题处理群聊
func (d *DingTalkBotBiz) initIssueHandlingGroup(appKey dingtalkPkg.AppKey, openConversationId, groupScope string) error {
// 1.开场白
outTrackId := constants.BuildCardOutTrackId(openConversationId, d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling)
_, err := d.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.BaseMsg),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("HTTP"),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("当前会话主题"),
"markdown": tea.String("问题:" + groupScope),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
AtUserIds: map[string]*string{
"@ALL": tea.String("@ALL"),
},
},
},
)
if err != nil {
return err
}
// 2. 机器人能力
// 构建卡片 OutTrackId
outTrackId = constants.BuildCardOutTrackId(openConversationId, d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling)
_, err = d.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(d.conf.Dingtalk.Card.Template.BaseMsg),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("HTTP"),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("当前机器人能力"),
"markdown": tea.String("- 聊天内容提取(@机器人 [内容提取]{聊天记录/问答描述} \n - QA知识收集卡片信息收集 \n - QA知识问答@机器人 [知识库查询]{问题描述}"),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + openConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(d.conf.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling),
AtUserIds: map[string]*string{
"@ALL": tea.String("@ALL"),
},
},
},
)
if err != nil {
return err
}
return nil
}
// buildCreateGroupCardResp 构建关闭创建群组卡片按钮
func (d *DingTalkBotBiz) buildCreateGroupCardResp() *card.CardResponse {
return &card.CardResponse{
CardData: &card.CardDataDto{
CardParamMap: map[string]string{
"button_display": "false",
},
},
CardUpdateOptions: &card.CardUpdateOptions{
UpdateCardDataByKey: true,
},
}
}

View File

@ -8,17 +8,17 @@ import (
errors "ai_scheduler/internal/data/error" errors "ai_scheduler/internal/data/error"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/gateway" "ai_scheduler/internal/gateway"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/dingtalk" "ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/mapstructure" "ai_scheduler/internal/pkg/mapstructure"
"ai_scheduler/internal/pkg/rec_extra" "ai_scheduler/internal/pkg/rec_extra"
"ai_scheduler/internal/pkg/util" "ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/public" "bufio"
errorsSpecial "errors" errorsSpecial "errors"
"io" "io"
"net/http" "net/http"
@ -31,6 +31,7 @@ import (
"github.com/coze-dev/coze-go" "github.com/coze-dev/coze-go"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
"github.com/ollama/ollama/api"
"gorm.io/gorm/utils" "gorm.io/gorm/utils"
) )
@ -88,9 +89,226 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
} }
rec.Match = &match rec.Match = &match
// 意图输入到日志
log.Infof("recognize: %s", recognizeMsg)
return return
} }
// RewriteQuery 改写查询词,支持多轮对话
func (r *Handle) RewriteQuery(ctx context.Context, history []model.AiBotChatHi, currentQuery string) (string, error) {
if len(history) == 0 {
return currentQuery, nil
}
histStr := strings.Builder{}
for _, h := range history {
if h.Role == "user" {
histStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content))
}
}
systemPrompt := `你是一个搜索查询改写专家请结合用户的历史对话上下文将用户当前的输入改写为一个独立的语义完整的适合知识库检索的中文查询词
要求
1. 当前输入最能反映用户的意图权重按照时间逆序依次减弱改写后的查询词应与当前输入的语义相关
2. 保持原意补全指代刚才那个问题
3. 只返回改写后的查询词不要有任何解释
4. 如果当前输入已经很完整直接返回原句`
userPrompt := fmt.Sprintf("### 历史对话:\n%s\n### 当前输入:\n%s\n### 改写后的查询词:", histStr.String(), currentQuery)
messages := []api.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userPrompt},
}
return r.Ollama.Chat(ctx, messages)
}
type IssueClassification struct {
SysName string `json:"sys_name"`
IssueTypeName string `json:"issue_type_name"`
Summary string `json:"summary"`
Reason string `json:"reason"`
}
// ClassifyIssueSys 问题系统分析
func (r *Handle) ClassifyIssueSystem(ctx context.Context, systems []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) {
systemPrompt := fmt.Sprintf(`## 角色
你是一个系统类型判定专家你的唯一任务是基于多轮对话识别用户当前讨论的系统sys_name不需要输出问题类型输出必须严格遵守 JSON 格式
## 推理规则
1. 系统判定逻辑
- 当前输入明确提到系统 直接覆盖历史系统
- 当前输入未提系统但历史对话有 继承最近历史系统
- 当前输入和历史均未出现 "全局"
- 询问公司企业制度层面的问题 "全局"
2. 特殊规则
- 如果当前输入仅包含系统名称CRM视为系统上下文补充仅更新 sys_name不做其他推断
## 背景数据
可用系统列表[%s]
## 输出格式
{
"sys_name": "系统名称",
"reason": "说明系统来源:当前输入 / 历史继承 / 默认"
}
`, strings.Join(systems, ", "))
historyStr := strings.Builder{}
historyStr.WriteString("### 历史对话:\n")
for _, h := range userHist {
if h.Role == "user" {
historyStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content))
}
}
messages := []api.Message{
{Role: "system", Content: systemPrompt},
{Role: "assistant", Content: historyStr.String()},
{Role: "user", Content: userInput},
}
resp, err := r.Ollama.Chat(ctx, messages)
if err != nil {
return nil, err
}
// 尝试清理 JSON 内容(有时模型会返回 markdown 块)
resp = strings.TrimPrefix(resp, "```json")
resp = strings.TrimSuffix(resp, "```")
resp = strings.TrimSpace(resp)
var result IssueClassification
if err := json.Unmarshal([]byte(resp), &result); err != nil {
return nil, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp)
}
return &result, nil
}
// ClassifyIssueType 问题分类分析
func (r *Handle) ClassifyIssueType(ctx context.Context, issueTypes []string, systems []string, userInput string, userHist []model.AiBotChatHi) (*IssueClassification, error) {
systemPrompt := fmt.Sprintf(`## 角色
你是一个业务问题类型分析专家你的任务是基于多轮对话识别用户讨论的**问题类型issue_type_name**问题类型必须严格来自背景数据-可用问题类型列表
你不负责系统名称判断输出必须严格遵守 JSON 格式
## 推理规则
1. 构建完整问题意图
- 将当前输入与历史对话合并理解为完整问题演进
- 当前输入可能是补充条件追问修正或只给模块名/报错片段
- 不要只看当前一句
- 忽略历史中的系统名称相关
2. 问题类型判定逻辑
- 当前输入明确匹配列表中某个类型 使用该类型
- 当前输入未明确但历史已有 继承历史类型
- 当前输入未匹配历史也没有 选择最接近的列表类型尽量匹配意图
- 除非是闲聊你好在吗禁止返回空值
- 除非明确是需求否则禁止返回开发需求类型疑问句式一定不能返回开发需求类型
3. 特殊规则
- 当前输入只包含系统名/模块名/参数名 视为问题补充继承历史 issue_type_name
- 输出必须严格匹配列表中的类型不允许生成列表外的自造类型
## 背景数据
可用问题类型列表[%s]
系统名称列表参考[%s]
## 输出格式
{
"issue_type_name": "问题类型名称",
"summary": "15字内问题标题",
"reason": "说明问题类型是基于哪句话判断,或说明继承自历史,继承自哪条历史"
}`, strings.Join(issueTypes, ", "), strings.Join(systems, ", "))
historyStr := strings.Builder{}
historyStr.WriteString("### 历史对话:\n")
for _, h := range userHist {
if h.Role == "user" {
historyStr.WriteString(fmt.Sprintf("%s:%s\n", h.CreateAt, h.Content))
}
}
messages := []api.Message{
{Role: "system", Content: systemPrompt},
{Role: "assistant", Content: historyStr.String()},
{Role: "user", Content: userInput},
}
resp, err := r.Ollama.Chat(ctx, messages)
if err != nil {
return nil, err
}
// 尝试清理 JSON 内容(有时模型会返回 markdown 块)
resp = strings.TrimPrefix(resp, "```json")
resp = strings.TrimSuffix(resp, "```")
resp = strings.TrimSpace(resp)
var result IssueClassification
if err := json.Unmarshal([]byte(resp), &result); err != nil {
return nil, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp)
}
return &result, nil
}
type IsAnswerRelevant struct {
Relevance string `json:"relevance"`
Reason string `json:"reason"`
}
// 判断答案是否回答了问题
func (r *Handle) IsAnswerRelevant(ctx context.Context, question string, answer string) (bool, error) {
prompt := `## 角色
你是一个答案评估专家你的任务是判断给定的答案是否真正回答了用户的问题你必须严格分析语义意图和信息覆盖情况避免只看关键词
## 输入
- question: %s
- answer: %s
## 判断逻辑
1. **直接回答**答案明确提供了解决方案步骤结论或可执行信息 输出 True
2. **未回答**答案仅泛泛提示缺少关键步骤或信息或者只是提供背景登录信息等无关内容 输出 False
3. **部分回答**答案提供了一部分可用信息但未完全解决问题 输出 Partial
## 输出要求
输出严格 JSON 格式只包含以下字段
{
"relevance": "True / False / Partial",
"reason": "简要说明为什么答案被认为回答或未回答问题"
}`
resp, err := r.Ollama.Generation(ctx, fmt.Sprintf(prompt, question, answer))
if err != nil {
return false, err
}
// 尝试清理 JSON 内容(有时模型会返回 markdown 块)
resp = strings.TrimPrefix(resp, "```json")
resp = strings.TrimSuffix(resp, "```")
resp = strings.TrimSpace(resp)
var result IsAnswerRelevant
if err := json.Unmarshal([]byte(resp), &result); err != nil {
return false, fmt.Errorf("解析分类结果失败: %w, 原文: %s", err, resp)
}
log.Debug("分析结果:%s原因%s", result.Relevance, result.Reason)
if result.Relevance == "True" {
return true, nil
}
return false, nil
}
func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) { func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) {
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning) entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
return return
@ -129,7 +347,7 @@ func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, rec *e
case constants.TaskTypeApi: case constants.TaskTypeApi:
return r.handleApiTask(ctx, rec, pointTask) return r.handleApiTask(ctx, rec, pointTask)
case constants.TaskTypeKnowle: case constants.TaskTypeKnowle:
return r.handleKnowle(ctx, rec, pointTask) return r.handleKnowleV2(ctx, rec, pointTask)
case constants.TaskTypeFunc: case constants.TaskTypeFunc:
return r.handleTask(ctx, rec, pointTask) return r.handleTask(ctx, rec, pointTask)
case constants.TaskTypeBot: case constants.TaskTypeBot:
@ -166,81 +384,115 @@ func (r *Handle) handleTask(ctx context.Context, rec *entitys.Recognize, task *m
} }
// 知识库 // 知识库
func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) { // func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
// var (
// configData entitys.ConfigDataTool
// sessionIdKnowledge string
// query string
// host string
// )
// err = json.Unmarshal([]byte(task.Config), &configData)
// if err != nil {
// return
// }
// ext, err := rec_extra.GetTaskRecExt(rec)
// if err != nil {
// return
// }
// // 通过session 找到知识库session
// var has bool
// if len(ext.Session) == 0 {
// return errors.SessionNotFound
// }
// ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
// if err != nil {
// return
// } else if !has {
// return errors.SessionNotFound
// }
// // 找到知识库的host
// {
// tool, exists := r.toolManager.GetTool(configData.Tool)
// if !exists {
// return fmt.Errorf("tool not found: %s", configData.Tool)
// }
// if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
// return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
// } else {
// host = knowledgeTool.GetConfig().BaseURL
// }
// }
// // 知识库的session为空请求知识库获取, 并绑定
// if ext.SessionInfo.KnowlegeSessionID == "" {
// // 请求知识库
// if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
// return
// }
// // 绑定知识库session下次可以使用
// ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
// if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
// return
// }
// }
// // 用户输入解析
// var ok bool
// input := make(map[string]string)
// if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
// return
// }
// if query, ok = input["query"]; !ok {
// return fmt.Errorf("query不能为空")
// }
// ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
// Session: ext.SessionInfo.KnowlegeSessionID,
// ApiKey: ext.Sys.KnowlegeTenantKey,
// Query: query,
// }
// rec.Ext = pkg.JsonByteIgonErr(ext)
// // 执行工具
// err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
// if err != nil {
// return
// }
// return
// }
// 知识库V2 - lightRAG自建
func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
// 获取用户session信息
var (
configData entitys.ConfigDataTool
sessionIdKnowledge string
query string
host string
)
err = json.Unmarshal([]byte(task.Config), &configData)
if err != nil {
return
}
ext, err := rec_extra.GetTaskRecExt(rec) ext, err := rec_extra.GetTaskRecExt(rec)
if err != nil { if err != nil {
return return
} }
// 通过session 找到知识库session // 获取租户ID 形式为 {biz-user} 比如 "zltx-platform"
var has bool tenantID := ext.Sys.KnowlegeTenantKey
if len(ext.Session) == 0 {
return errors.SessionNotFound // 请求知识库工具
} knowledgeBase := knowledge_base.New(r.conf.KnowledgeConfig)
ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session)) knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: tenantID, // 后续动态接参
Query: rec.UserContent.Text,
Mode: constants.KnowledgeModeMix,
Stream: true,
Think: false,
OnlyRAG: true,
})
if err != nil { if err != nil {
return return fmt.Errorf("请求知识库工具失败err: %v", err)
} else if !has {
return errors.SessionNotFound
} }
// 找到知识库的host // 读取知识库SSE数据
{ err = r.readKnowledgeSSE(knowledgeResp, rec.Ch, false)
tool, exists := r.toolManager.GetTool(configData.Tool)
if !exists {
return fmt.Errorf("tool not found: %s", configData.Tool)
}
if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
} else {
host = knowledgeTool.GetConfig().BaseURL
}
}
// 知识库的session为空请求知识库获取, 并绑定
if ext.SessionInfo.KnowlegeSessionID == "" {
// 请求知识库
if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
return
}
// 绑定知识库session下次可以使用
ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
return
}
}
// 用户输入解析
var ok bool
input := make(map[string]string)
if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
return
}
if query, ok = input["query"]; !ok {
return fmt.Errorf("query不能为空")
}
ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
Session: ext.SessionInfo.KnowlegeSessionID,
ApiKey: ext.Sys.KnowlegeTenantKey,
Query: query,
}
rec.Ext = pkg.JsonByteIgonErr(ext)
// 执行工具
err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
if err != nil { if err != nil {
return return
} }
@ -248,6 +500,67 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
return return
} }
// 读取知识库 SSE 数据
func (r *Handle) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (err error) {
scanner := bufio.NewScanner(resp)
var buffer strings.Builder
var taskIndex string = "knowledgeBase"
for scanner.Scan() {
line := scanner.Text()
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
if err != nil {
return fmt.Errorf("解析SSE数据失败: %w", err)
}
if done {
break
}
if delta == nil {
continue
}
// 推理内容
if delta.ReasoningContent != "" {
entitys.ResStream(channel, taskIndex, delta.ReasoningContent)
continue
}
// 输出内容 - 段落
if delta.Content != "" && useParagraphMode {
// 存入缓冲区
buffer.WriteString(delta.Content)
content := buffer.String()
// 检查是否有换行符,按段落输出
if idx := strings.LastIndex(content, "\n"); idx != -1 {
// 发送直到最后一个换行符的内容
toSend := content[:idx+1]
entitys.ResStream(channel, taskIndex, toSend)
// 重置缓冲区,保留剩余部分
remaining := content[idx+1:]
buffer.Reset()
buffer.WriteString(remaining)
}
}
// 输出内容 - 逐字
if delta.Content != "" && !useParagraphMode {
entitys.ResStream(channel, taskIndex, delta.Content)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("读取SSE流中断: %w", err)
}
// 发送缓冲区剩余内容(仅在段落模式下需要)
if useParagraphMode && buffer.Len() > 0 {
entitys.ResStream(channel, taskIndex, buffer.String())
}
return nil
}
// bot 临时实现,后续转到 eino 工作流 // bot 临时实现,后续转到 eino 工作流
func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) { func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) {
if task.Index == "bug_optimization_submit" { if task.Index == "bug_optimization_submit" {
@ -333,7 +646,7 @@ func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessio
func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) { func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) {
// 获取创建者uid 用户名 -> dingtalk uid // 获取创建者uid 用户名 -> dingtalk uid
creatorId, err := r.dingtalkContactClient.SearchUserOne(accessToken, userName) creatorId, err := r.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, userName)
if err != nil { if err != nil {
log.Warnf("search dingtalk user one failed: %v", err) log.Warnf("search dingtalk user one failed: %v", err)
return return

View File

@ -138,20 +138,24 @@ func (f *WithDingTalkBot) CreatePrompt(ctx context.Context, rec *entitys.Recogni
mes = append(prompt, api.Message{ mes = append(prompt, api.Message{
Role: "system", // 系统角色 Role: "system", // 系统角色
Content: rec.SystemPrompt, // 系统提示内容 Content: rec.SystemPrompt, // 系统提示内容
// }, api.Message{ // 助手回复无需
// Role: "assistant", // 助手角色
// Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
}, api.Message{ }, api.Message{
Role: "assistant", // 助手角色 Role: "assistant", // 助手角色
Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容 Content: "用户历史输入:" + pkg.JsonStringIgonErr(rec.ChatHis), // 用户历史输入
}, api.Message{ }, api.Message{
Role: "user", // 用户角色 Role: "user", // 用户角色
Content: content.String(), // 用户输入内容 Content: content.String(), // 用户输入内容
}) })
fmt.Printf("[意图识别]最终prompt:%v", mes)
return return
} }
func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) { func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) {
var hasFile bool var hasFile bool
if rec.UserContent.File != nil && len(rec.UserContent.File) > 0 { if len(rec.UserContent.File) > 0 {
hasFile = true hasFile = true
} }
content.WriteString(rec.UserContent.Text) content.WriteString(rec.UserContent.Text)
@ -165,11 +169,10 @@ func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recog
content.WriteString(rec.UserContent.Tag) content.WriteString(rec.UserContent.Tag)
} }
if len(rec.ChatHis.Messages) > 0 { // if len(rec.ChatHis.Messages) > 0 {
content.WriteString("\n") // content.WriteString("### 引用历史聊天记录:\n")
content.WriteString("### 引用历史聊天记录:\n") // content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis)) // }
}
return return
} }

View File

@ -7,15 +7,19 @@ import (
"ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl" "ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/domain/workflow/recharge" "ai_scheduler/internal/domain/workflow/recharge"
"ai_scheduler/internal/domain/workflow/runtime" "ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/l_request" "ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/lsxd" "ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools" "ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/bbxt" "ai_scheduler/internal/tools/bbxt"
"ai_scheduler/utils" "ai_scheduler/utils"
"bufio"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -26,7 +30,11 @@ import (
"strings" "strings"
"time" "time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/alibabacloud-go/dingtalk/card_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/coze-dev/coze-go" "github.com/coze-dev/coze-go"
"github.com/duke-git/lancet/v2/slice"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
"xorm.io/builder" "xorm.io/builder"
) )
@ -35,12 +43,14 @@ import (
type GroupConfigBiz struct { type GroupConfigBiz struct {
botGroupConfigImpl *impl.BotGroupConfigImpl botGroupConfigImpl *impl.BotGroupConfigImpl
reportDailyCacheImpl *impl.ReportDailyCacheImpl reportDailyCacheImpl *impl.ReportDailyCacheImpl
botConfigImpl *impl.BotConfigImpl
ossClient *utils_oss.Client ossClient *utils_oss.Client
workflowManager *runtime.Registry workflowManager *runtime.Registry
botTools []model.AiBotTool botTools []model.AiBotTool
toolManager *tools.Manager toolManager *tools.Manager
conf *config.Config conf *config.Config
rdb *utils.Rdb rdb *utils.Rdb
dingtalkCardClient *dingtalk.CardClient
macro *do.Macro macro *do.Macro
handle *do.Handle handle *do.Handle
} }
@ -50,24 +60,28 @@ func NewGroupConfigBiz(
tools *tools_regis.ToolRegis, tools *tools_regis.ToolRegis,
ossClient *utils_oss.Client, ossClient *utils_oss.Client,
botGroupConfigImpl *impl.BotGroupConfigImpl, botGroupConfigImpl *impl.BotGroupConfigImpl,
botConfigImpl *impl.BotConfigImpl,
workflowManager *runtime.Registry, workflowManager *runtime.Registry,
conf *config.Config, conf *config.Config,
reportDailyCacheImpl *impl.ReportDailyCacheImpl, reportDailyCacheImpl *impl.ReportDailyCacheImpl,
rdb *utils.Rdb, rdb *utils.Rdb,
macro *do.Macro,
toolManager *tools.Manager, toolManager *tools.Manager,
dingtalkCardClient *dingtalk.CardClient,
macro *do.Macro,
handle *do.Handle, handle *do.Handle,
) *GroupConfigBiz { ) *GroupConfigBiz {
return &GroupConfigBiz{ return &GroupConfigBiz{
botTools: tools.BootTools, botTools: tools.BootTools,
ossClient: ossClient, ossClient: ossClient,
botGroupConfigImpl: botGroupConfigImpl, botGroupConfigImpl: botGroupConfigImpl,
botConfigImpl: botConfigImpl,
workflowManager: workflowManager, workflowManager: workflowManager,
conf: conf, conf: conf,
reportDailyCacheImpl: reportDailyCacheImpl, reportDailyCacheImpl: reportDailyCacheImpl,
rdb: rdb, rdb: rdb,
macro: macro,
toolManager: toolManager, toolManager: toolManager,
dingtalkCardClient: dingtalkCardClient,
macro: macro,
handle: handle, handle: handle,
} }
} }
@ -235,7 +249,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
return nil return nil
} }
func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig) (err error) { func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (err error) {
if !rec.Match.IsMatch { if !rec.Match.IsMatch {
if len(rec.Match.Chat) != 0 { if len(rec.Match.Chat) != 0 {
@ -269,6 +283,9 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
return g.handleReport(ctx, rec, pointTask, groupConfig) return g.handleReport(ctx, rec, pointTask, groupConfig)
case constants.TaskTypeCozeWorkflow: case constants.TaskTypeCozeWorkflow:
return g.handleCozeWorkflow(ctx, rec, pointTask) return g.handleCozeWorkflow(ctx, rec, pointTask)
case constants.TaskTypeKnowle: // 知识库lightRAG版本
_, err = g.handleKnowledge(ctx, rec, groupConfig, callback)
return err
default: default:
return g.otherTask(ctx, rec) return g.otherTask(ctx, rec)
} }
@ -426,3 +443,215 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
entitys.ResText(rec.Ch, "", rec.Match.Reasoning) entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
return return
} }
func (g *GroupConfigBiz) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
var ResellerProductRelation map[int32]*bbxt.ResellerLossSumProductRelation
dayDate := day.Format(time.DateOnly)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail})
cond = cond.And(builder.Eq{"cache_key": dayDate})
var cache model.AiReportDailyCache
err := g.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
if err != nil {
return err
}
if cache.ID == 0 {
ResellerProductRelation, err = bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
if err != nil {
return err
}
cache = model.AiReportDailyCache{
CacheKey: dayDate,
CacheIndex: bbxt.IndexLossSumDetail,
Value: pkg.JsonStringIgonErr(ResellerProductRelation),
}
_, err = g.reportDailyCacheImpl.Add(&cache)
} else {
err = json.Unmarshal([]byte(cache.Value), &ResellerProductRelation)
}
if err != nil {
return err
}
for _, v := range totalDetail {
if _, ex := ResellerProductRelation[v.ResellerId]; !ex {
continue
}
v.Manager = ResellerProductRelation[v.ResellerId].AfterSaleName
for _, vv := range v.ProductLoss {
if _, ex := ResellerProductRelation[v.ResellerId].Products[vv.ProductId]; !ex {
continue
}
vv.LossReason = ResellerProductRelation[v.ResellerId].Products[vv.ProductId].LossReason
}
}
return nil
}
// handleKnowledge 处理知识库V2版本
func (g *GroupConfigBiz) handleKnowledge(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) (isRetrieved bool, err error) {
// 请求知识库工具
knowledgeBase := knowledge_base.New(g.conf.KnowledgeConfig)
knowledgeResp, err := knowledgeBase.Query(&knowledge_base.QueryRequest{
TenantID: constants.KnowledgeTenantIdDefault, // 后续动态接参
Query: rec.UserContent.Text,
Mode: constants.KnowledgeModeMix,
Stream: true,
Think: false,
OnlyRAG: true,
})
if err != nil {
return false, fmt.Errorf("请求知识库工具失败err: %v", err)
}
// 读取知识库SSE数据
isRetrieved, _, err = g.readKnowledgeSSE(knowledgeResp, rec.Ch, true)
if err != nil {
return
}
// 未检索到匹配信息,群聊时询问是否拉群
if !isRetrieved && callback.ConversationType == constants.ConversationTypeGroup {
g.shouldCreateIssueHandlingGroup(ctx, rec, groupConfig, callback)
return false, nil
}
return
}
// 读取知识库 SSE 数据
func (g *GroupConfigBiz) readKnowledgeSSE(resp io.ReadCloser, channel chan entitys.Response, useParagraphMode bool) (isRetrieved bool, allContent string, err error) {
scanner := bufio.NewScanner(resp)
var buffer strings.Builder
var allContentBuilder strings.Builder
for scanner.Scan() {
line := scanner.Text()
delta, done, err := knowledge_base.ParseOpenAIStreamData(line)
if err != nil {
return false, "", fmt.Errorf("解析SSE数据失败: %w", err)
}
if done {
break
}
if delta == nil {
continue
}
// 知识库未命中 输出提示后中断
if delta.XRagStatus == constants.KnowledgeRagStatusMiss {
var missContent string = "知识库未检测到匹配信息。"
entitys.ResStream(channel, "", missContent)
return false, missContent, nil
}
// 推理内容
if delta.ReasoningContent != "" {
entitys.ResStream(channel, "", delta.ReasoningContent)
continue
}
// 输出内容 - 段落
if delta.Content != "" && useParagraphMode {
// 存入缓冲区
buffer.WriteString(delta.Content)
allContentBuilder.WriteString(delta.Content)
content := buffer.String()
// 检查是否有换行符,按段落输出
if idx := strings.LastIndex(content, "\n"); idx != -1 {
// 发送直到最后一个换行符的内容
toSend := content[:idx+1]
entitys.ResStream(channel, "", toSend)
// 重置缓冲区,保留剩余部分
remaining := content[idx+1:]
buffer.Reset()
buffer.WriteString(remaining)
}
}
// 输出内容 - 逐字
if delta.Content != "" && !useParagraphMode {
entitys.ResStream(channel, "", delta.Content)
allContentBuilder.WriteString(delta.Content)
}
}
if err := scanner.Err(); err != nil {
return true, "", fmt.Errorf("读取SSE流中断: %w", err)
}
// 发送缓冲区剩余内容(仅在段落模式下需要)
if useParagraphMode && buffer.Len() > 0 {
entitys.ResStream(channel, "", buffer.String())
}
return true, allContentBuilder.String(), nil
}
// 询问是否创建群聊处理问题
func (g *GroupConfigBiz) shouldCreateIssueHandlingGroup(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig, callback *chatbot.BotCallbackDataModel) error {
// 获取群问题处理人
type issueOwnerType struct {
UserId string `json:"userid"`
Name string `json:"name"`
}
var issueOwner []issueOwnerType
if err := json.Unmarshal([]byte(groupConfig.IssueOwner), &issueOwner); err != nil {
return fmt.Errorf("解析群问题处理人失败err: %v", err)
}
// 合并所有name、Id
userNames := make([]string, 0, len(issueOwner))
userIds := make([]string, 0, len(issueOwner))
for _, owner := range issueOwner {
userNames = append(userNames, "@"+owner.Name)
userIds = append(userIds, owner.UserId)
}
issueOwnerStr := strings.Join(userNames, "、")
targetUserIds := append(userIds, callback.SenderStaffId)
recipientsUsers := slice.Map(targetUserIds, func(_ int, item string) *string {
return tea.String(item)
})
// 获取应用配置
appKey, err := g.botConfigImpl.GetRobotAppKey(callback.RobotCode)
if err != nil {
return fmt.Errorf("获取机器人配置失败err: %v", err)
}
// 构建卡片 OutTrackId
outTrackId := constants.BuildCardOutTrackId(callback.ConversationId, callback.RobotCode)
// 发送钉钉卡片
_, err = g.dingtalkCardClient.CreateAndDeliver(
appKey,
&card_1_0.CreateAndDeliverRequest{
CardTemplateId: tea.String(g.conf.Dingtalk.Card.Template.CreateGroupApprove),
OutTrackId: tea.String(outTrackId),
CallbackType: tea.String("STREAM"),
CardData: &card_1_0.CreateAndDeliverRequestCardData{
CardParamMap: map[string]*string{
"title": tea.String("创建群聊提醒"),
"content": tea.String(fmt.Sprintf("**确认创建群聊?**\n\n将邀请以下成员加入群聊\n\n%s", issueOwnerStr)),
"remark": tea.String("注:如若无需,忽略即可"),
"button_left": tea.String("创建群聊"),
"button_right": tea.String("忽略"),
"action_id": tea.String("create_group"),
"button_display": tea.String("true"),
"group_scope": tea.String(strings.TrimSpace(rec.UserContent.Text)),
"target_user_ids": tea.String(strings.Join(targetUserIds, ",")),
},
},
ImGroupOpenSpaceModel: &card_1_0.CreateAndDeliverRequestImGroupOpenSpaceModel{
SupportForward: tea.Bool(false),
},
OpenSpaceId: tea.String("dtv1.card//im_group." + callback.ConversationId),
ImGroupOpenDeliverModel: &card_1_0.CreateAndDeliverRequestImGroupOpenDeliverModel{
RobotCode: tea.String(callback.RobotCode),
Recipients: recipientsUsers,
},
})
if err != nil {
return fmt.Errorf("发送钉钉卡片失败err: %v", err)
}
return nil
}

View File

@ -20,7 +20,7 @@ import (
) )
const DefaultInterval = 100 * time.Millisecond const DefaultInterval = 100 * time.Millisecond
const HeardBeatX = 100 const HeardBeatX = 1000
type SendCardClient struct { type SendCardClient struct {
Auth *Auth Auth *Auth

View File

@ -62,6 +62,27 @@ func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSe
return return
} }
func (r *OllamaService) Chat(ctx context.Context, messages []api.Message) (string, error) {
res, err := r.client.Chat(ctx, r.config.Ollama.Model, messages)
if err != nil {
return "", err
}
return res.Message.Content, nil
}
func (r *OllamaService) Generation(ctx context.Context, prompt string) (string, error) {
res, err := r.client.Generation(ctx, &api.GenerateRequest{
Model: r.config.Ollama.GenerateModel,
Stream: new(bool),
Prompt: prompt,
Think: &api.ThinkValue{Value: false},
})
if err != nil {
return "", err
}
return res.Response, nil
}
//func (r *OllamaService) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) { //func (r *OllamaService) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) {
// if imgByte == nil { // if imgByte == nil {
// return // return

View File

@ -9,6 +9,7 @@ import (
"github.com/volcengine/volcengine-go-sdk/service/arkruntime" "github.com/volcengine/volcengine-go-sdk/service/arkruntime"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model/responses" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model/responses"
"github.com/volcengine/volcengine-go-sdk/volcengine"
) )
type Hsyq struct { type Hsyq struct {
@ -38,7 +39,7 @@ func (h *Hsyq) getClient(key string) *arkruntime.Client {
} }
// 火山引擎 // 火山引擎
func (h *Hsyq) RequestHsyq(ctx context.Context, key string, modelName string, prompt []*model.ChatCompletionMessage) (model.ChatCompletionResponse, error) { func (h *Hsyq) Chat(ctx context.Context, key string, modelName string, prompt []*model.ChatCompletionMessage) (model.ChatCompletionResponse, error) {
req := model.CreateChatCompletionRequest{ req := model.CreateChatCompletionRequest{
Model: modelName, Model: modelName,
Messages: prompt, Messages: prompt,
@ -54,6 +55,68 @@ func (h *Hsyq) RequestHsyq(ctx context.Context, key string, modelName string, pr
return resp, err return resp, err
} }
// 火山引擎
func (h *Hsyq) ChatWithRequest(ctx context.Context, key string, request model.ContextChatCompletionRequest) (model.ChatCompletionResponse, error) {
resp, err := h.getClient(key).CreateContextChatCompletion(ctx, request)
if err != nil {
return model.ChatCompletionResponse{ID: ""}, err
}
log.Info("token用量", resp.Usage.TotalTokens, "输入:", resp.Usage.PromptTokens, "输出:", resp.Usage.CompletionTokens)
return resp, err
}
func (h *Hsyq) CreateContextCache(ctx context.Context, key string, modelName string, prompt []*model.ChatCompletionMessage) (string, error) {
req := model.CreateContextRequest{
Model: modelName,
Messages: prompt,
TTL: volcengine.Int(3600),
Mode: model.ContextModeSession,
TruncationStrategy: &model.TruncationStrategy{Type: model.TruncationStrategyTypeRollingTokens},
}
resp, err := h.getClient(key).CreateContext(ctx, req)
if err != nil {
return "", err
}
log.Info("token用量", resp.Usage.TotalTokens, "输入:", resp.Usage.PromptTokens, "输出:", resp.Usage.CompletionTokens)
return resp.ID, err
}
func (h *Hsyq) CreateResponse(ctx context.Context, key string, modelName string, prompt []*responses.InputItem, id string, isRegis bool) (*responses.ResponseObject, error) {
req := &responses.ResponsesRequest{
Model: modelName,
Input: &responses.ResponsesInput{
Union: &responses.ResponsesInput_ListValue{
ListValue: &responses.InputItemList{ListValue: prompt},
},
},
Stream: new(bool),
Thinking: &responses.ResponsesThinking{Type: responses.ThinkingType_disabled.Enum()},
}
if isRegis {
prefix := true
req.Caching = &responses.ResponsesCaching{Type: responses.CacheType_enabled.Enum(), Prefix: &prefix}
req.ExpireAt = volcengine.Int64(time.Now().Unix() + 3600)
}
if len(id) != 0 {
req.PreviousResponseId = &id
//req.Text = &responses.ResponsesText{
// Format: &responses.TextFormat{
// Type: responses.TextType_json_object,
// Schema:
// }
//}
}
resp, err := h.getClient(key).CreateResponses(ctx, req)
if err != nil {
return nil, err
}
log.Info("token用量", resp.Usage.TotalTokens, "输入:", resp.Usage.InputTokens, "输出:", resp.Usage.OutputTokens)
return resp, err
}
func (h *Hsyq) RequestHsyqJson(ctx context.Context, key string, modelName string, prompt []*responses.InputItem) (*responses.ResponseObject, error) { func (h *Hsyq) RequestHsyqJson(ctx context.Context, key string, modelName string, prompt []*responses.InputItem) (*responses.ResponseObject, error) {
req := responses.ResponsesRequest{ req := responses.ResponsesRequest{
Model: modelName, Model: modelName,

View File

@ -22,6 +22,7 @@ var ProviderSetBiz = wire.NewSet(
NewQywxAppBiz, NewQywxAppBiz,
NewGroupConfigBiz, NewGroupConfigBiz,
do.NewMacro, do.NewMacro,
NewCallbackBiz,
NewAdviceFileBiz, NewAdviceFileBiz,
third_party.NewHsyq, third_party.NewHsyq,
NewAdviceAdvicerBiz, NewAdviceAdvicerBiz,

View File

@ -10,9 +10,11 @@ import (
"ai_scheduler/internal/domain/repo" "ai_scheduler/internal/domain/repo"
"ai_scheduler/internal/domain/workflow" "ai_scheduler/internal/domain/workflow"
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/lsxd" "ai_scheduler/internal/pkg/lsxd"
"ai_scheduler/internal/pkg/utils_ollama" "ai_scheduler/internal/pkg/utils_ollama"
"ai_scheduler/internal/pkg/utils_oss" "ai_scheduler/internal/pkg/utils_oss"
"ai_scheduler/internal/tools"
"ai_scheduler/utils" "ai_scheduler/utils"
"context" "context"
"testing" "testing"
@ -53,6 +55,11 @@ func run() {
registry := workflow.NewRegistry(configConfig, client, repos, components) registry := workflow.NewRegistry(configConfig, client, repos, components)
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db) botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
botConfigImpl := impl.NewBotConfigImpl(db)
qywxAppBiz = NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other) qywxAppBiz = NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig) reportDailyCacheImpl := impl.NewReportDailyCacheImpl(db)
toolManager := tools.NewManager(configConfig, client)
oauth2Client, _ := dingtalk.NewOauth2Client(rdb)
dingtalkCardClient, _ := dingtalk.NewCardClient(oauth2Client)
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, botConfigImpl, registry, configConfig, reportDailyCacheImpl, rdb, toolManager, dingtalkCardClient)
} }

View File

@ -25,6 +25,7 @@ type Config struct {
Oss Oss `mapstructure:"oss"` Oss Oss `mapstructure:"oss"`
DefaultPrompt SysPrompt `mapstructure:"default_prompt"` DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"` PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
KnowledgeConfig KnowledgeConfig `mapstructure:"knowledge_config"`
LLM LLM `mapstructure:"llm"` LLM LLM `mapstructure:"llm"`
Dingtalk DingtalkConfig `mapstructure:"dingtalk"` Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
Qywx QywxConfig `mapstructure:"qywx"` Qywx QywxConfig `mapstructure:"qywx"`
@ -72,10 +73,12 @@ type LLMCapabilityConfig struct {
// DingtalkConfig 钉钉配置 // DingtalkConfig 钉钉配置
type DingtalkConfig struct { type DingtalkConfig struct {
ApiKey string `mapstructure:"api_key"` ApiKey string `mapstructure:"api_key"`
ApiSecret string `mapstructure:"api_secret"` ApiSecret string `mapstructure:"api_secret"`
TableDemand AITableConfig `mapstructure:"table_demand"` TableDemand AITableConfig `mapstructure:"table_demand"`
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组 BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组
Card CardConfig `mapstructure:"card"` // 互动卡片
SceneGroup SceneGroupConfig `mapstructure:"scene_group"` // 场景群
} }
// QywxConfig 企业微信配置 // QywxConfig 企业微信配置
@ -97,6 +100,34 @@ type AITableConfig struct {
SheetIdOrName string `mapstructure:"sheet_id_or_name"` SheetIdOrName string `mapstructure:"sheet_id_or_name"`
} }
// CardConfig 互动卡片配置
type CardConfig struct {
// 卡片回调路由key
CallbackRouteKey string `mapstructure:"callback_route_key"`
// 卡片调试工具 [show展示 hide隐藏]
DebugToolEntryShow string `mapstructure:"debug_tool_entry_show"`
// 卡片模板
Template CardTemplateConfig `mapstructure:"template"`
}
// CardTemplateConfig 卡片模板配置
type CardTemplateConfig struct {
// 基础消息卡片(title + content)
BaseMsg string `mapstructure:"base_msg"`
// 内容收集卡片title + textarea + button
ContentCollect string `mapstructure:"content_collect"`
// 创建群聊申请title + content + button
CreateGroupApprove string `mapstructure:"create_group_approve"`
}
// SceneGroupConfig 场景群配置
type SceneGroupConfig struct {
// 问题处理群模板ID
GroupTemplateIDIssueHandling string `mapstructure:"group_template_id_issue_handling"`
// 问题处理群模板机器人ID
GroupTemplateRobotIDIssueHandling string `mapstructure:"group_template_robot_id_issue_handling"`
}
// SysConfig 系统配置 // SysConfig 系统配置
type SysConfig struct { type SysConfig struct {
SessionLen int `mapstructure:"session_len"` SessionLen int `mapstructure:"session_len"`
@ -265,6 +296,20 @@ type PermissionConfig struct {
PermissionURL string `mapstructure:"permission_url"` PermissionURL string `mapstructure:"permission_url"`
} }
// KnowledgeConfig 知识库配置
type KnowledgeConfig struct {
// 知识库地址
BaseURL string `mapstructure:"base_url"`
// 默认租户ID
TenantID string `mapstructure:"tenant_id"`
// 模式
Mode string `mapstructure:"mode"`
// 是否思考
Think bool `mapstructure:"think"`
// 是否仅RAG
OnlyRAG bool `mapstructure:"only_rag"`
}
// LoadConfig 加载配置 // LoadConfig 加载配置
func LoadConfig(configPath string) (*Config, error) { func LoadConfig(configPath string) (*Config, error) {
viper.SetConfigFile(configPath) viper.SetConfigFile(configPath)

View File

@ -45,3 +45,11 @@ const (
PermissionTypeNone = 1 PermissionTypeNone = 1
PermissionTypeDept = 2 PermissionTypeDept = 2
) )
// IssueType 问题类型
const (
IssueTypeKnowledgeQA = "knowledge_qa" // 知识问答
IssueTypeUI = "ui" // UI需求
IssueTypeBug = "bug" // Bug
IssueTypeDemand = "demand" // 开发需求
)

View File

@ -1,6 +1,11 @@
package constants package constants
import "net/url" import (
"net/url"
"strings"
"github.com/google/uuid"
)
const DingTalkBseUrl = "https://oapi.dingtalk.com" const DingTalkBseUrl = "https://oapi.dingtalk.com"
@ -78,3 +83,77 @@ const (
] ]
}` }`
) )
// 交互卡片回调
const (
// 回调类型
CardActionCallbackTypeAction string = "actionCallback" // 交互卡片回调事件类型
// 回调事件类型
CardActionTypeCreateGroup string = "create_group" // 创建群聊
)
// dingtalk 卡片 OutTrackId 模板
const CardOutTrackIdTemplate string = "{space_id}:{bot_id}:{uuid}"
func BuildCardOutTrackId(spaceId string, botId string) (outTrackId string) {
uuid := uuid.New().String()
outTrackId = strings.ReplaceAll(CardOutTrackIdTemplate, "{space_id}", spaceId)
outTrackId = strings.ReplaceAll(outTrackId, "{bot_id}", botId)
outTrackId = strings.ReplaceAll(outTrackId, "{uuid}", uuid)
return
}
func ParseCardOutTrackId(outTrackId string) (spaceId string, botId string) {
parts := strings.Split(outTrackId, ":")
if len(parts) != 3 {
return
}
spaceId, botId, _ = parts[0], parts[1], parts[2]
return
}
// 问题处理群机器人 - LLM 提示词
const IssueHandlingExtractContentPrompt string = `你是一个问题与答案生成助手
你的职责是
- 分析用户输入的内容
- 识别其中隐含或明确的问题
- 基于输入内容本身生成对应的问题与答案
当用户输入为多条群聊聊天记录
- 结合问题主题判断聊天记录中正在讨论或试图解决的问题
- 一个群聊中可能包含多个相互独立的问题但它们都围绕着一个主题一般为用户提出的第一个问题尽可能总结为一个问题一个答案
生成答案时的原则
- 答案必须来源于聊天内容中已经给出的信息或共识
- 不要引入外部知识不要使用聊天记录中真实人名或敏感信息适当总结
- 若聊天中未形成明确答案应明确标记为暂无明确结论
- 若存在多种不同观点应分别列出不要擅自合并或裁决
JSON 输出原则
- 你的最终输出必须是**合法的 JSON**
- 不得输出任何额外解释性文字
- JSON 结构必须严格符合以下约定
JSON 结构约定
{
"question": "清晰、独立、可复用的问题描述",
"answer": "基于聊天内容整理出的答案;如无结论则为“暂无明确结论”",
"confidence": "high | medium | low"
}
字段说明
- question抽象后的标准问题表述不包含具体聊天语句
- answer整理后的答案不得引入聊天之外的信息
- confidence根据聊天中信息的一致性和明确程度给出判断
如果无法从输入中识别出任何有效问题返回
{ "confidence": "low" }
用户输入
%s
`

View File

@ -0,0 +1,25 @@
package constants
type SupWay int32
const (
SupWayHsyq SupWay = iota + 1
)
type Mode int32
const (
ModeResponse Mode = 1
ModeContext Mode = 2
)
const BasePrompt = `**[场景]**现在你是专业的顾问我们在聊天软件里面聊天
**[要求]**提供的json内容包含顾问项目对话技巧客户信息请根据json内容进行模仿真人进行对话** 绝对不能让对方看出来你是ai!**
**[规则]**
1.必须模仿顾问的聊天方式不能有markdown格式聊天内容必须贴近聊天软件的正常聊天非客户要求不要自顾自的介绍项目开场白不要太长
2.不要有任何特殊格式有比较长的对话内容注意提行
3.结合[任务内容]信息若任务状态为"in_progress",则在对话中尽量完成任务;如果任务状态为"completed""fail",则主动结束对话,且该轮对话中不再更新任务状态mission_complete_desc记录最终结果且不再更改不要每一句对话都包含任务内容不要目的性太强
4.不能有任何画外音的文字格式出现比如等待对方回复
**[任务内容]**`
const BasePrompt2 = "**[输出格式]**\n- **格式类型**严格输出json格式字符串不需要其他任何格式和内容\n- **数据结构**\n {\n \"result\": \"{{chat_content}}\",\n \"mission\": \"{{mission}}\"\n \"mission_status\": \"{{mission_status}}\",\n \"mission_complete_desc\": \"{{mission_complete_desc}}\"\n }\n\n**[字段说明]**\n1. **result** (字符串)\n - 对应变量:`{{chat_content}}`\n - 内容:顾问的实际对话内容\n - 要求:自然语言回复,面向用户\n\n2. **mission** (字符串)\n - 对应变量:`{{mission}}`\n - 取值:任务内容\n - 说明:需要完成的任务内容\n\n3. **mission_status** (字符串)\n - 对应变量:`{{mission_status}}`\n - 取值:`\"completed\"` 或 `\"in_progress\"` 或 `\"fail\"`\n - 说明:标识当前任务完成状态\n - `\"completed\"`:任务已全部完成\n - `\"in_progress\"`:任务仍在进行中\n - `\"fail\"`:任务失败,客户已经明确拒绝或者对任务内容表达反对\n\n4. **mission_complete_desc** (字符串)\n - 对应变量:`{{mission_complete_desc}}`\n - 内容根据mission_status提供相应描述\n - 当`mission_status: \"completed\"`时:简要总结任务完成情况\n - 当`mission_status: \"in_progress\"`时:说明下一步需要做什么\n\n**[示例]**\n{\n\"result\": \"需要我给您安排时间吗\",\n\"mission\": \"邀请客户到售楼部\",\n\"mission_status\": \"in_progress\",\n\"mission_complete_desc\": \"需要用户确认什么时候到售楼部\"\n}\n\n{\n\"result\": \"好的,那就周日下午两点,我到时候在售楼部等您,来了记得给我打电话\",\n\"mission_status\": \"completed\",\n\"mission_complete_desc\": \"客户确认周日下午两点到售楼部\"\n}\n\n**[强制要求]**\n1. 必须输出完整、有效的JSON对象\n2. 所有字段均为必需字段,不可省略\n3. JSON格式必须严格正确无语法错误\n4. `mission_status`只能使用指定的两个值\n5. `result`字段内容需符合对话语境"

View File

@ -20,3 +20,25 @@ func GetKnowledgeId(caller Caller) KnowledgeId {
} }
return CallerKnowledgeIdMap[caller] return CallerKnowledgeIdMap[caller]
} }
// 知识库
const (
// KnowledgeTenantIdDefault = "default"
KnowledgeTenantIdDefault = "sk-EfnUANKMj3DUOiEPJZ5xS8SGMsbO6be_qYAg9uZ8T3zyoFM-" // 演示,临时设置为直连天下
)
// 知识库模式
const (
KnowledgeModeBypass = "bypass" // 绕过知识库,直接返回用户输入
KnowledgeModeNaive = "naive" // 简单模式,直接返回知识库答案
KnowledgeModeLocal = "local" // 本地模式,仅使用本地知识库
KnowledgeModeGlobal = "global" // 全局模式,使用全局知识库
KnowledgeModeHybrid = "hybrid" // 混合模式,结合本地和全局知识库
KnowledgeModeMix = "mix" // 混合模式,结合本地、全局和知识库
)
// 知识库命中状态
const (
KnowledgeRagStatusHit = "hit" // 知识库命中
KnowledgeRagStatusMiss = "miss" // 知识库未命中
)

View File

@ -0,0 +1,17 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
)
type AiAdviceModelSupImpl struct {
dataTemp.DataTemp
}
func NewAiAdviceModelSupImpl(db *utils.Db) *AiAdviceModelSupImpl {
return &AiAdviceModelSupImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceModelSup)),
}
}

View File

@ -0,0 +1,17 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
)
type AiAdviceSessionImpl struct {
dataTemp.DataTemp
}
func NewAiAdviceSessionImpl(db *utils.Db) *AiAdviceSessionImpl {
return &AiAdviceSessionImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceSession)),
}
}

View File

@ -0,0 +1,64 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/utils"
"gorm.io/gorm"
)
type IssueImpl struct {
IssueType BaseRepository[model.AiIssueType]
IssueAssignRule BaseRepository[model.AiIssueAssignRule]
IssueAssignUser BaseRepository[model.AiIssueAssignUser]
}
func NewIssueImpl(db *utils.Db) *IssueImpl {
return &IssueImpl{
IssueType: NewBaseModel[model.AiIssueType](db.Client),
IssueAssignRule: NewBaseModel[model.AiIssueAssignRule](db.Client),
IssueAssignUser: NewBaseModel[model.AiIssueAssignUser](db.Client),
}
}
// WithName 名称查询
func (a *IssueImpl) WithName(name string) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("name = ?", name)
}
}
// WithCode 编码查询
func (a *IssueImpl) WithCode(code string) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("code = ?", code)
}
}
// WithSysID 系统ID查询
func (a *IssueImpl) WithSysID(sysID any) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("sys_id = ?", sysID)
}
}
// WithIssueTypeID 问题类型ID查询
func (a *IssueImpl) WithIssueTypeID(issueTypeID any) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("issue_type_id = ?", issueTypeID)
}
}
// WithRuleID 规则ID查询
func (a *IssueImpl) WithRuleID(ruleID any) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("rule_id = ?", ruleID)
}
}
// WithStatus 状态查询
func (a *IssueImpl) WithStatus(status any) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", status)
}
}

View File

@ -22,7 +22,8 @@ BaseModel 是一个泛型结构体用于封装GORM数据库通用操作。
// 定义受支持的PO类型集合可根据需要扩展, 只有包含表结构才能使用BaseModel避免使用出现问题 // 定义受支持的PO类型集合可根据需要扩展, 只有包含表结构才能使用BaseModel避免使用出现问题
type PO interface { type PO interface {
model.AiChatHi | model.AiChatHi |
model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig |
model.AiIssueType | model.AiIssueAssignRule | model.AiIssueAssignUser
} }
type BaseModel[P PO] struct { type BaseModel[P PO] struct {

View File

@ -2,8 +2,13 @@ package impl
import ( import (
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/tmpl/dataTemp" "ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils" "ai_scheduler/utils"
"encoding/json"
"xorm.io/builder"
) )
type BotConfigImpl struct { type BotConfigImpl struct {
@ -15,3 +20,33 @@ func NewBotConfigImpl(db *utils.Db) *BotConfigImpl {
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotConfig)), DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotConfig)),
} }
} }
// GetRobotConfig 获取机器人配置
func (b *BotConfigImpl) GetRobotConfig(robotCode string) (*entitys.DingTalkBot, error) {
// 获取机器人配置
var botConfig model.AiBotConfig
cond := builder.NewCond().And(builder.Eq{"robot_code": robotCode}).And(builder.Eq{"status": 1})
err := b.GetOneBySearchToStrut(&cond, &botConfig)
if err != nil {
return nil, err
}
// 解出 config
var config entitys.DingTalkBot
err = json.Unmarshal([]byte(botConfig.BotConfig), &config)
if err != nil {
return nil, err
}
return &config, nil
}
// GetRobotAppKey 获取机器人应用ID
func (b *BotConfigImpl) GetRobotAppKey(robotCode string) (dingtalk.AppKey, error) {
// 获取机器人配置
dingTalkBotConfig, err := b.GetRobotConfig(robotCode)
if err != nil {
return dingtalk.AppKey{}, err
}
return dingTalkBotConfig.GetAppKey(), nil
}

View File

@ -19,7 +19,7 @@ func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) { func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
var data model.AiBotGroup var data model.AiBotGroup
err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ?", staffId, robotCode).Find(&data).Error err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ? and status = 1", staffId, robotCode).Find(&data).Error
if data.GroupID == 0 { if data.GroupID == 0 {
err = sql.ErrNoRows err = sql.ErrNoRows
} }

View File

@ -5,6 +5,8 @@ import (
"ai_scheduler/tmpl/dataTemp" "ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils" "ai_scheduler/utils"
"database/sql" "database/sql"
"xorm.io/builder"
) )
type BotUserImpl struct { type BotUserImpl struct {
@ -25,3 +27,14 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) {
} }
return &data, err return &data, err
} }
func (k BotUserImpl) GetByUserIds(userIds []int32) ([]model.AiBotUser, error) {
var data []model.AiBotUser
cond := builder.NewCond()
for _, userId := range userIds {
cond = cond.Or(builder.Eq{"user_id": userId})
}
_, err := k.GetListToStruct(&cond, nil, &data, "user_id")
return data, err
}

View File

@ -18,9 +18,12 @@ var ProviderImpl = wire.NewSet(
NewBotGroupConfigImpl, NewBotGroupConfigImpl,
NewBotGroupQywxImpl, NewBotGroupQywxImpl,
NewReportDailyCacheImpl, NewReportDailyCacheImpl,
NewIssueImpl,
NewAdviceAdvicerImpl, NewAdviceAdvicerImpl,
NewAdviceProjectImpl, NewAdviceProjectImpl,
NewAdviceTalkImpl, NewAdviceTalkImpl,
NewAdviceAdvicerVersionImpl, NewAdviceAdvicerVersionImpl,
NewAdviceClientImpl, NewAdviceClientImpl,
NewAiAdviceSessionImpl,
NewAiAdviceModelSupImpl,
) )

View File

@ -0,0 +1,29 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiAdviceModelSup = "ai_advice_model_sup"
// AiAdviceModelSup mapped from table <ai_advice_model_sup>
type AiAdviceModelSup struct {
SupID int32 `gorm:"column:sup_id;primaryKey;autoIncrement:true" json:"sup_id"`
SupName string `gorm:"column:sup_name;not null;comment:备注" json:"sup_name"` // 备注
SupWay int32 `gorm:"column:sup_way;not null;comment:供应方1火山引擎" json:"sup_way"` // 供应方1火山引擎
Key string `gorm:"column:key;not null" json:"key"`
FileModel string `gorm:"column:file_model;not null;comment:文件读取model" json:"file_model"` // 文件读取model
JSONModel string `gorm:"column:json_model;not null;comment:json格式处理model" json:"json_model"` // json格式处理model
ChatModel string `gorm:"column:chat_model;not null;comment:对话模型" json:"chat_model"` // 对话模型
Mode int32 `gorm:"column:mode;not null;comment:模式" json:"mode"` // 模式
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
}
// TableName AiAdviceModelSup's table name
func (*AiAdviceModelSup) TableName() string {
return TableNameAiAdviceModelSup
}

View File

@ -12,14 +12,10 @@ const TableNameAiAdviceProject = "ai_advice_project"
// AiAdviceProject mapped from table <ai_advice_project> // AiAdviceProject mapped from table <ai_advice_project>
type AiAdviceProject struct { type AiAdviceProject struct {
ProjectID int32 `gorm:"column:project_id;primaryKey;autoIncrement:true" json:"project_id"` ProjectID int32 `gorm:"column:project_id;primaryKey;autoIncrement:true" json:"project_id"`
Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名 Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名
RegionValue string `gorm:"column:region_value;comment:区域价值话术库" json:"region_value"` // 区域价值话术库 ModelSupID int32 `gorm:"column:model_sup_id;not null;comment:模型提供方配置关联advicer_model_sup" json:"model_sup_id"` // 模型提供方配置关联advicer_model_sup
CompetitionComparison string `gorm:"column:competition_comparison;comment:竞品对比话术" json:"competition_comparison"` // 竞品对比话术 CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
CoreSellingPoints string `gorm:"column:core_selling_points;comment:项目核心卖点" json:"core_selling_points"` // 项目核心卖点
SupportingFacilities string `gorm:"column:supporting_facilities;comment:配套体系" json:"supporting_facilities"` // 配套体系
DeveloperBacking string `gorm:"column:developer_backing;comment:开发商背书" json:"developer_backing"` // 开发商背书
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
} }
// TableName AiAdviceProject's table name // TableName AiAdviceProject's table name

View File

@ -0,0 +1,32 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiAdviceSession = "ai_advice_session"
// AiAdviceSession mapped from table <ai_advice_session>
type AiAdviceSession struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
SessionID string `gorm:"column:session_id;not null" json:"session_id"`
ProjectID int32 `gorm:"column:project_id;not null" json:"project_id"`
SupID int32 `gorm:"column:sup_id;not null" json:"sup_id"`
AdvicerVersionID string `gorm:"column:advicer_version_id;not null;comment:顾问版本" json:"advicer_version_id"` // 顾问版本
ClientID string `gorm:"column:client_id;not null;comment:客户信息id" json:"client_id"` // 客户信息id
TalkSkillID string `gorm:"column:talk_skill_id;not null;comment:聊天话术id" json:"talk_skill_id"` // 聊天话术id
ContextCache string `gorm:"column:context_cache;not null;comment:上下文缓存信息(火山引擎)" json:"context_cache"` // 上下文缓存信息(火山引擎)
Mission string `gorm:"column:mission;not null;comment:任务" json:"mission"` // 任务
MissionStatus string `gorm:"column:mission_status;not null;default:1;comment:任务状态" json:"mission_status"` // 任务状态
MissionCompleteDesc string `gorm:"column:mission_complete_desc;not null;comment:任务完成描述" json:"mission_complete_desc"` // 任务完成描述
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
}
// TableName AiAdviceSession's table name
func (*AiAdviceSession) TableName() string {
return TableNameAiAdviceSession
}

View File

@ -11,6 +11,7 @@ type AiBotGroupConfig struct {
ConfigID int32 `gorm:"column:config_id;primaryKey;autoIncrement:true" json:"config_id"` ConfigID int32 `gorm:"column:config_id;primaryKey;autoIncrement:true" json:"config_id"`
ToolList string `gorm:"column:tool_list;not null" json:"tool_list"` ToolList string `gorm:"column:tool_list;not null" json:"tool_list"`
ProductName string `gorm:"column:product_name;not null" json:"product_name"` ProductName string `gorm:"column:product_name;not null" json:"product_name"`
IssueOwner string `gorm:"column:issue_owner;comment:群组问题处理人" json:"issue_owner"` // 群组问题处理人
} }
// TableName AiBotGroupConfig's table name // TableName AiBotGroupConfig's table name

View File

@ -0,0 +1,27 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiIssueAssignRule = "ai_issue_assign_rule"
// AiIssueAssignRule AI问题分配规则表指定系统+问题类型对应分配规则
type AiIssueAssignRule struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
SysID int32 `gorm:"column:sys_id;not null;comment:系统ID关联 ai_sys.id" json:"sys_id"` // 系统ID关联 ai_sys.id
IssueTypeID int32 `gorm:"column:issue_type_id;not null;comment:问题类型ID关联 ai_issue_type.id" json:"issue_type_id"` // 问题类型ID关联 ai_issue_type.id
Status int32 `gorm:"column:status;not null;default:1;comment:规则状态1=启用0=停用" json:"status"` // 规则状态1=启用0=停用
Description string `gorm:"column:description;comment:规则描述,用于说明规则用途" json:"description"` // 规则描述,用于说明规则用途
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
}
// TableName AiIssueAssignRule's table name
func (*AiIssueAssignRule) TableName() string {
return TableNameAiIssueAssignRule
}

View File

@ -0,0 +1,24 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiIssueAssignUser = "ai_issue_assign_user"
// AiIssueAssignUser 规则对应的用户表,命中规则时需要通知的钉钉用户
type AiIssueAssignUser struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
RuleID int32 `gorm:"column:rule_id;not null;comment:规则ID关联 ai_issue_assign_rule.id" json:"rule_id"` // 规则ID关联 ai_issue_assign_rule.id
UserID int32 `gorm:"column:user_id;not null;comment:钉钉用户ID关联 ai_bot_user.id" json:"user_id"` // 钉钉用户ID关联 ai_bot_user.id
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
}
// TableName AiIssueAssignUser's table name
func (*AiIssueAssignUser) TableName() string {
return TableNameAiIssueAssignUser
}

View File

@ -0,0 +1,25 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiIssueType = "ai_issue_type"
// AiIssueType AI问题类型表
type AiIssueType struct {
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true;comment:主键ID" json:"id"` // 主键ID
Code string `gorm:"column:code;not null;comment:问题类型编码,例如: ui, bug, demand" json:"code"` // 问题类型编码,例如: ui, bug, demand
Name string `gorm:"column:name;not null;comment:问题类型名称,例如: UI问题, Bug, 需求" json:"name"` // 问题类型名称,例如: UI问题, Bug, 需求
CreatedAt time.Time `gorm:"column:created_at;not null;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
UpdatedAt time.Time `gorm:"column:updated_at;not null;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"` // 更新时间
}
// TableName AiIssueType's table name
func (*AiIssueType) TableName() string {
return TableNameAiIssueType
}

View File

@ -0,0 +1,46 @@
package mongo_model
import (
"time"
)
type AdvicerChatHisMongo struct {
SessionId string `json:"sessionId" bson:"sessionId"`
User string `json:"User" bson:"User"`
Assistant Assistant `json:"assistant" bson:"assistant"`
InToken int64 `json:"inToken" bson:"inToken"`
OutToken int64 `json:"outToken" bson:"outToken"`
CreatAt time.Time `json:"creatAt" bson:"creatAt"`
}
func NewAdvicerChatHisMongo() *AdvicerChatHisMongo {
return &AdvicerChatHisMongo{}
}
func (a *AdvicerChatHisMongo) MongoTableName() string {
return "advicer_chat_his"
}
type AdvicerChatHisMongoEntity struct {
User string `json:"user"`
Assistant string `json:"assistant"`
MissionStatus string `json:"missionStatus"`
MissionNext string `json:"missionNext"`
CreateAt string `json:"createAt"`
}
func (a *AdvicerChatHisMongo) Entity() AdvicerChatHisMongoEntity {
return AdvicerChatHisMongoEntity{
User: a.User,
Assistant: a.Assistant.Result,
MissionStatus: a.Assistant.MissionStatus,
MissionNext: a.Assistant.MissionCompleteDesc,
CreateAt: a.CreatAt.Format(time.DateTime),
}
}
type Assistant struct {
Result string `json:"result"`
MissionStatus string `json:"mission_status"`
MissionCompleteDesc string `json:"mission_complete_desc"`
}

View File

@ -7,4 +7,5 @@ var ProviderSetMongo = wire.NewSet(
NewAdvicerTalkSkillMongo, NewAdvicerTalkSkillMongo,
NewAdvicerProjectMongo, NewAdvicerProjectMongo,
NewAdvicerClientMongo, NewAdvicerClientMongo,
NewAdvicerChatHisMongo,
) )

View File

@ -0,0 +1,163 @@
package knowledge_base
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type Client struct {
cfg config.KnowledgeConfig
}
func New(cfg config.KnowledgeConfig) *Client {
return &Client{cfg: cfg}
}
// 查询知识库
func (c *Client) Query(req *QueryRequest) (io.ReadCloser, error) {
if req == nil {
return nil, fmt.Errorf("req is nil")
}
if req.TenantID == "" {
return nil, fmt.Errorf("tenantID is empty")
}
if req.Query == "" {
return nil, fmt.Errorf("query is empty")
}
if req.Mode == "" {
req.Mode = c.cfg.Mode
}
if !req.Think {
req.Think = c.cfg.Think
}
if !req.OnlyRAG {
req.OnlyRAG = c.cfg.OnlyRAG
}
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
rsp, err := (&l_request.Request{
Method: "POST",
Url: baseURL + "/query",
Headers: map[string]string{
"Content-Type": "application/json",
"X-Tenant-ID": req.TenantID,
"Accept": "text/event-stream",
},
Json: map[string]interface{}{
"query": req.Query,
"mode": req.Mode,
"stream": req.Stream,
"think": req.Think,
"only_rag": req.OnlyRAG,
},
}).SendNoParseResponse()
if err != nil {
return nil, err
}
if rsp == nil || rsp.Body == nil {
return nil, fmt.Errorf("empty response")
}
if rsp.StatusCode != http.StatusOK {
defer rsp.Body.Close()
bodyPreview, _ := io.ReadAll(io.LimitReader(rsp.Body, 4096))
if len(bodyPreview) > 0 {
return nil, fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, string(bodyPreview))
}
return nil, fmt.Errorf("knowledge base returned status %d", rsp.StatusCode)
}
return rsp.Body, nil
}
// IngestText 向知识库中注入文本
func (c *Client) IngestText(req *IngestTextRequest) error {
if req == nil {
return fmt.Errorf("req is nil")
}
if req.TenantID == "" {
return fmt.Errorf("tenantID is empty")
}
if req.Text == "" {
return fmt.Errorf("text is empty")
}
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
rsp, err := (&l_request.Request{
Method: "POST",
Url: baseURL + "/ingest/text",
Headers: map[string]string{
"Content-Type": "application/json",
"X-Tenant-ID": req.TenantID,
},
Json: map[string]interface{}{
"text": req.Text,
},
}).Send()
if err != nil {
return err
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, rsp.Text)
}
return nil
}
// IngestBatchQA 向知识库中注入问答对
func (c *Client) IngestBatchQA(req *IngestBacthQARequest) error {
if req == nil {
return fmt.Errorf("req is nil")
}
if req.TenantID == "" {
return fmt.Errorf("tenantID is empty")
}
for _, item := range req.QAList {
if item.Question == "" {
return fmt.Errorf("question is empty")
}
if item.Answer == "" {
return fmt.Errorf("answer is empty")
}
}
data := []map[string]string{}
for _, item := range req.QAList {
data = append(data, map[string]string{
"question": item.Question,
"answer": item.Answer,
})
}
jsonByte, err := json.Marshal(data)
if err != nil {
return err
}
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
rsp, err := (&l_request.Request{
Method: "POST",
Url: baseURL + "/ingest/batch_qa",
Headers: map[string]string{
"Content-Type": "application/json",
"X-Tenant-ID": req.TenantID,
},
JsonByte: jsonByte,
}).Send()
if err != nil {
return err
}
if rsp.StatusCode != http.StatusOK {
return fmt.Errorf("knowledge base returned status %d: %s", rsp.StatusCode, rsp.Text)
}
return nil
}

View File

@ -0,0 +1,63 @@
package knowledge_base
import (
"ai_scheduler/internal/config"
"bufio"
"strings"
"testing"
)
func TestCall(t *testing.T) {
req := &QueryRequest{
TenantID: "admin_test_qa",
Query: "lightRAG 的优势?",
Mode: "naive",
Stream: true,
Think: false,
OnlyRAG: true,
}
client := New(config.KnowledgeConfig{BaseURL: "http://127.0.0.1:9600"})
resp, err := client.Query(req)
if err != nil {
t.Errorf("Call failed: %v", err)
}
if resp == nil {
t.Error("Response is nil")
}
defer resp.Close()
scanner := bufio.NewScanner(resp)
var outThinking strings.Builder
var outContent strings.Builder
for scanner.Scan() {
line := scanner.Text()
delta, done, err := ParseOpenAIStreamData(line)
if err != nil {
t.Fatalf("parse openai stream failed: %v", err)
}
if delta == nil {
continue
}
if done {
break
}
if delta.XRagStatus != "" {
t.Logf("XRagStatus: %s", delta.XRagStatus)
}
if delta.Content != "" {
outContent.WriteString(delta.Content)
}
if delta.ReasoningContent != "" {
outThinking.WriteString(delta.ReasoningContent)
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("scan failed: %v", err)
}
t.Logf("Thinking: %s", outThinking.String())
t.Logf("Content: %s", outContent.String())
}

View File

@ -0,0 +1,75 @@
package knowledge_base
import (
"encoding/json"
"fmt"
"strings"
)
type openAIChunk struct {
Choices []struct {
Delta *Delta `json:"delta"`
Message *Message `json:"message"`
FinishReason *string `json:"finish_reason"`
} `json:"choices"`
}
type Delta struct {
ReasoningContent string `json:"reasoning_content"` // 推理内容
Content string `json:"content"` // 内容
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
}
type Message struct {
Role string `json:"role"` // 角色
Content string `json:"content"` // 内容
XRagStatus string `json:"x_rag_status"` // rag命中状态 hit|miss
}
func ParseOpenAIStreamData(dataLine string) (delta *Delta, done bool, err error) {
data := strings.TrimSpace(strings.TrimPrefix(dataLine, "data:"))
if data == "" {
return nil, false, nil
}
data = strings.TrimSpace(data)
if data == "" {
return nil, false, nil
}
if data == "[DONE]" {
return nil, true, nil
}
var chunk openAIChunk
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
return nil, false, fmt.Errorf("unmarshal openai stream chunk failed: %w", err)
}
for _, c := range chunk.Choices {
if c.Delta != nil {
return c.Delta, false, nil // 只输出第一个delta
}
}
return nil, false, nil
}
func ParseOpenAIHTTPData(body string) (message *Message, done bool, err error) {
data := strings.TrimSpace(body)
if data == "" {
return nil, false, nil
}
var resp openAIChunk
if err := json.Unmarshal([]byte(data), &resp); err != nil {
return nil, false, fmt.Errorf("unmarshal openai stream chunk failed: %w", err)
}
for _, c := range resp.Choices {
if c.Message != nil {
return c.Message, true, nil // 只输出第一个message
}
}
return nil, false, nil
}

View File

@ -0,0 +1,25 @@
package knowledge_base
type QueryRequest struct {
TenantID string // 租户 ID
Query string // 查询内容
Mode string // 模式,默认 naive 可选:[bypass|naive|local|global|hybrid|mix]
Stream bool // 仅支持流式输出
Think bool // 是否开启思考模式
OnlyRAG bool // 是否仅开启 RAG 模式
}
type IngestTextRequest struct {
TenantID string // 租户 ID
Text string // 要注入的文本内容
}
type IngestBacthQARequest struct {
TenantID string // 租户 ID
QAList []*QA // 问答对列表
}
type QA struct {
Question string // 问题
Answer string // 答案
}

View File

@ -4,6 +4,7 @@ import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/domain/tools/common/excel_generator" "ai_scheduler/internal/domain/tools/common/excel_generator"
"ai_scheduler/internal/domain/tools/common/image_converter" "ai_scheduler/internal/domain/tools/common/image_converter"
"ai_scheduler/internal/domain/tools/common/knowledge_base"
"ai_scheduler/internal/domain/tools/hyt/goods_add" "ai_scheduler/internal/domain/tools/hyt/goods_add"
"ai_scheduler/internal/domain/tools/hyt/goods_brand_search" "ai_scheduler/internal/domain/tools/hyt/goods_brand_search"
"ai_scheduler/internal/domain/tools/hyt/goods_category_add" "ai_scheduler/internal/domain/tools/hyt/goods_category_add"
@ -25,6 +26,7 @@ type Manager struct {
type CommonTools struct { type CommonTools struct {
ExcelGenerator *excel_generator.Client ExcelGenerator *excel_generator.Client
ImageConverter *image_converter.Client ImageConverter *image_converter.Client
KnowledgeBase *knowledge_base.Client
} }
type HytTools struct { type HytTools struct {
@ -60,6 +62,7 @@ func NewManager(cfg *config.Config) *Manager {
Common: &CommonTools{ Common: &CommonTools{
ExcelGenerator: excel_generator.New(), ExcelGenerator: excel_generator.New(),
ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic), ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic),
KnowledgeBase: knowledge_base.New(cfg.KnowledgeConfig),
}, },
} }
} }

View File

@ -1,14 +1,17 @@
package entitys package entitys
import "ai_scheduler/internal/data/mongo_model" import (
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/data/mongo_model"
)
type AdvicerInitReq struct { type AdvicerInitReq struct {
AdvicerID int32 `json:"AdvicerId"` AdvicerID int32 `json:"advicerId"`
ProjectID int32 `json:"ProjectId"` ProjectID int32 `json:"projectId"`
Name string `json:"name"` // 姓名 Name string `json:"name"` // 姓名
Birth string `json:"birth"` // 用户名称 Birth string `json:"birth"` // 用户名称
Gender int32 `json:"gender"` // 1:男2 Gender int32 `json:"gender"` // 1:男2
WorkingYears int32 `json:"WorkingYears"` // 工作年限 WorkingYears int32 `json:"workingYears"` // 工作年限
} }
type AdvicerInfoReq struct { type AdvicerInfoReq struct {
@ -16,7 +19,7 @@ type AdvicerInfoReq struct {
} }
type AdvicerListReq struct { type AdvicerListReq struct {
ProjectId int32 `json:"ProjectId"` ProjectId int32 `json:"projectId"`
} }
type AdvicerVersionAddReq struct { type AdvicerVersionAddReq struct {
@ -92,6 +95,21 @@ type AdvicerTalkSkillInfoReq struct {
Id string `json:"id"` Id string `json:"id"`
} }
type AdvicerProjectBaseAddReq struct {
Name string `json:"name"`
ModelSupId int32 `json:"modelSupId"`
}
type AdvicerProjectBaseAddRes struct {
ProjectId int32 `json:"projectId"`
}
type AdvicerProjectBaseUpdateReq struct {
ProjectId int32 `json:"projectId"`
Name string `json:"name"`
ModelSupId int32 `json:"modelSupId"`
}
type AdvicerProjectAddReq struct { type AdvicerProjectAddReq struct {
ProjectId int32 `json:"projectId" bson:"projectId"` ProjectId int32 `json:"projectId" bson:"projectId"`
ProjectInfo mongo_model.ProjectInfo `json:"projectInfo" bson:"projectInfo"` ProjectInfo mongo_model.ProjectInfo `json:"projectInfo" bson:"projectInfo"`
@ -118,6 +136,12 @@ type AdvicerProjectInfoReq struct {
ProjectId int32 `json:"projectId" bson:"projectId"` ProjectId int32 `json:"projectId" bson:"projectId"`
} }
type AdvicerProjectInfoRes struct {
Base model.AiAdviceProject
ConfigInfo mongo_model.AdvicerProjectMongo
ModelInfo model.AiAdviceModelSup
}
type AdvicerClientAddReq struct { type AdvicerClientAddReq struct {
ProjectId int32 `json:"projectId" bson:"projectId"` ProjectId int32 `json:"projectId" bson:"projectId"`
AdvicerId int32 `json:"advicerId" bson:"advicerId"` AdvicerId int32 `json:"advicerId" bson:"advicerId"`
@ -157,6 +181,7 @@ type AdvicerChatRegistReq struct {
AdvicerVersionId string `json:"advicerVersionId"` AdvicerVersionId string `json:"advicerVersionId"`
ClientId string `json:"clientId"` ClientId string `json:"clientId"`
TalkSkillId string `json:"talkSkillId"` TalkSkillId string `json:"talkSkillId"`
Mission string `json:"mission"`
} }
type AdvicerChatRegistRes struct { type AdvicerChatRegistRes struct {

View File

@ -2,6 +2,7 @@ package entitys
import ( import (
"ai_scheduler/internal/data/model" "ai_scheduler/internal/data/model"
"ai_scheduler/internal/pkg/dingtalk"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
) )
@ -26,6 +27,13 @@ type DingTalkBot struct {
ClientSecret string `json:"client_secret"` ClientSecret string `json:"client_secret"`
} }
func (d *DingTalkBot) GetAppKey() dingtalk.AppKey {
return dingtalk.AppKey{
AppKey: d.ClientId,
AppSecret: d.ClientSecret,
}
}
type Task struct { type Task struct {
Index string `json:"bot_index"` Index string `json:"bot_index"`
} }

View File

@ -0,0 +1,81 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
card "github.com/alibabacloud-go/dingtalk/card_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type CardClient struct {
cli *card.Client
oauth2Client *Oauth2Client
}
func NewCardClient(oauth2Client *Oauth2Client) (*CardClient, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := card.NewClient(cfg)
if err != nil {
return nil, err
}
return &CardClient{cli: c, oauth2Client: oauth2Client}, nil
}
// 创建并投放卡片
func (c *CardClient) CreateAndDeliver(appKey AppKey, cardData *card.CreateAndDeliverRequest) (bool, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return false, err
}
// 调用API
resp, err := c.cli.CreateAndDeliverWithOptions(
cardData,
&card.CreateAndDeliverHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return false, err
}
if resp.Body == nil {
return false, errorcode.ParamErrf("empty response body")
}
if !*resp.Body.Success {
return false, errorcode.ParamErrf("create and deliver failed")
}
return true, nil
}
// 更新卡片
func (c *CardClient) UpdateCard(appKey AppKey, cardData *card.UpdateCardRequest) (bool, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return false, err
}
// 调用API
resp, err := c.cli.UpdateCardWithOptions(
cardData,
&card.UpdateCardHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return false, err
}
if resp.Body == nil {
return false, errorcode.ParamErrf("empty response body")
}
return *resp.Body.Success, nil
}

View File

@ -1,7 +1,6 @@
package dingtalk package dingtalk
import ( import (
"ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error" errorcode "ai_scheduler/internal/data/error"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
@ -11,22 +10,20 @@ import (
) )
type ContactClient struct { type ContactClient struct {
config *config.Config cli *contact.Client
cli *contact.Client oauth2Client *Oauth2Client
} }
func NewContactClient(config *config.Config) (*ContactClient, error) { func NewContactClient(oauth2Client *Oauth2Client) (*ContactClient, error) {
cfg := &openapi.Config{ cfg := &openapi.Config{
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey), Protocol: tea.String("https"),
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret), RegionId: tea.String("central"),
Protocol: tea.String("https"),
RegionId: tea.String("central"),
} }
c, err := contact.NewClient(cfg) c, err := contact.NewClient(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ContactClient{config: config, cli: c}, nil return &ContactClient{cli: c, oauth2Client: oauth2Client}, nil
} }
type SearchUserReq struct { type SearchUserReq struct {
@ -40,15 +37,23 @@ type SearchUserResp struct {
Body interface{} Body interface{}
} }
func (c *ContactClient) SearchUserOne(accessToken string, name string) (string, error) { func (c *ContactClient) SearchUserOne(appKey AppKey, name string) (string, error) {
headers := &contact.SearchUserHeaders{} // 获取token
headers.XAcsDingtalkAccessToken = tea.String(accessToken) accessToken, err := c.oauth2Client.GetAccessToken(appKey)
resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{ if err != nil {
FullMatchField: tea.Int32(1), return "", err
QueryWord: tea.String(name), }
Offset: tea.Int32(0),
Size: tea.Int32(1), resp, err := c.cli.SearchUserWithOptions(
}, headers, &util.RuntimeOptions{}) &contact.SearchUserRequest{
FullMatchField: tea.Int32(1),
QueryWord: tea.String(name),
Offset: tea.Int32(0),
Size: tea.Int32(1),
},
&contact.SearchUserHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -0,0 +1,78 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
im "github.com/alibabacloud-go/dingtalk/im_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type ImClient struct {
cli *im.Client
oauth2Client *Oauth2Client
}
func NewImClient(oauth2Client *Oauth2Client) (*ImClient, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := im.NewClient(cfg)
if err != nil {
return nil, err
}
return &ImClient{cli: c, oauth2Client: oauth2Client}, nil
}
// 创建并投放卡片
func (c *ImClient) AddRobotToConversation(appKey AppKey, imData *im.AddRobotToConversationRequest) (string, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
// 调用API
resp, err := c.cli.AddRobotToConversationWithOptions(
imData,
&im.AddRobotToConversationHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
return *resp.Body.ChatBotUserId, nil
}
// 创建场景群 不返回chatid如果没有获取群聊分享链接的诉求可以使用该接口
func (c *ImClient) CreateSceneGroup(appKey AppKey, req *im.CreateSceneGroupConversationRequest) (openConversationId string, err error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
// 调用API
resp, err := c.cli.CreateSceneGroupConversationWithOptions(
req,
&im.CreateSceneGroupConversationHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
return *resp.Body.OpenConversationId, nil
}

View File

@ -1,7 +1,6 @@
package dingtalk package dingtalk
import ( import (
"ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error" errorcode "ai_scheduler/internal/data/error"
"encoding/json" "encoding/json"
"time" "time"
@ -13,22 +12,20 @@ import (
) )
type NotableClient struct { type NotableClient struct {
config *config.Config cli *notable.Client
cli *notable.Client oauth2Client *Oauth2Client
} }
func NewNotableClient(config *config.Config) (*NotableClient, error) { func NewNotableClient(oauth2Client *Oauth2Client) (*NotableClient, error) {
cfg := &openapi.Config{ cfg := &openapi.Config{
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey), Protocol: tea.String("https"),
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret), RegionId: tea.String("central"),
Protocol: tea.String("https"),
RegionId: tea.String("central"),
} }
c, err := notable.NewClient(cfg) c, err := notable.NewClient(cfg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &NotableClient{config: config, cli: c}, nil return &NotableClient{cli: c, oauth2Client: oauth2Client}, nil
} }
type UpdateRecordReq struct { type UpdateRecordReq struct {
@ -43,9 +40,13 @@ type UpdateRecordsserResp struct {
Body interface{} Body interface{}
} }
func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (bool, error) { func (c *NotableClient) UpdateRecord(appKey AppKey, req *UpdateRecordReq) (bool, error) {
headers := &notable.UpdateRecordsHeaders{} // 获取token
headers.XAcsDingtalkAccessToken = tea.String(accessToken) accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return false, err
}
resp, err := c.cli.UpdateRecordsWithOptions( resp, err := c.cli.UpdateRecordsWithOptions(
tea.String(req.BaseId), tea.String(req.BaseId),
tea.String(req.SheetId), tea.String(req.SheetId),
@ -63,7 +64,10 @@ func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (
Id: tea.String(req.RecordId), Id: tea.String(req.RecordId),
}, },
}, },
}, headers, &util.RuntimeOptions{}) },
&notable.UpdateRecordsHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil { if err != nil {
return false, err return false, err
} }

View File

@ -0,0 +1,74 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
"ai_scheduler/utils"
"context"
"fmt"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
oauth2 "github.com/alibabacloud-go/dingtalk/oauth2_1_0"
"github.com/alibabacloud-go/tea/tea"
"github.com/redis/go-redis/v9"
)
type Oauth2Client struct {
cli *oauth2.Client
redisCli *redis.Client
}
func NewOauth2Client(rds *utils.Rdb) (*Oauth2Client, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := oauth2.NewClient(cfg)
if err != nil {
return nil, err
}
return &Oauth2Client{cli: c, redisCli: rds.Rdb}, nil
}
type AppKey struct {
AppKey string `json:"appKey"`
AppSecret string `json:"appSecret"`
AccessToken string `json:"accessToken"`
}
// GetAccessToken 获取access token
func (c *Oauth2Client) GetAccessToken(req AppKey) (string, error) {
// 兼容直接传入 access token 场景
if req.AccessToken != "" {
return req.AccessToken, nil
}
// 取cache
ctx := context.Background()
accessToken, err := c.redisCli.Get(ctx, fmt.Sprintf("dingtalk:oauth2:%s:access_token", req.AppKey)).Result()
if err == nil {
fmt.Println("get access token from cache:", accessToken)
return accessToken, nil
}
if err != redis.Nil {
return "", err
}
// 调用API
resp, err := c.cli.GetAccessToken(&oauth2.GetAccessTokenRequest{
AppKey: tea.String(req.AppKey),
AppSecret: tea.String(req.AppSecret),
})
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
// 缓存token
c.redisCli.Set(ctx, fmt.Sprintf("dingtalk:oauth2:%s:access_token", req.AppKey), *resp.Body.AccessToken, time.Duration(*resp.Body.ExpireIn)*time.Second)
return *resp.Body.AccessToken, nil
}

View File

@ -4,6 +4,7 @@ package dingtalk
import ( import (
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/pkg/l_request"
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
@ -12,6 +13,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strings"
"github.com/faabiosr/cachego/file" "github.com/faabiosr/cachego/file"
"github.com/fastwego/dingding" "github.com/fastwego/dingding"
@ -111,3 +113,141 @@ func (c *OldClient) QueryUserDetailsByMobile(ctx context.Context, mobile string)
func (c *OldClient) GetAccessToken() (string, error) { func (c *OldClient) GetAccessToken() (string, error) {
return c.atm.GetAccessToken() return c.atm.GetAccessToken()
} }
// CreateInternalGroupConversation 创建企业内部群聊
func (c *OldClient) CreateInternalGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string) (chatId, openConversationId string, err error) {
body := struct {
Name string `json:"name"`
Owner string `json:"owner"`
UserIds []string `json:"useridlist"`
ShowHistoryType int `json:"showHistoryType"`
Searchable int `json:"searchable"`
ValidationType int `json:"validationType"`
MentionAllAuthority int `json:"mentionAllAuthority"`
ManagementType int `json:"managementType"`
ChatBannedType int `json:"chatBannedType"`
}{
Name: groupName,
Owner: userIds[0],
UserIds: userIds,
}
b, _ := json.Marshal(body)
req := l_request.Request{
Method: "POST",
JsonByte: b,
Url: "https://oapi.dingtalk.com/chat/create?access_token=" + accessToken,
Headers: map[string]string{
"Content-Type": "application/json",
},
}
res, err := req.Send()
if err != nil {
return
}
var resp struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
ChatId string `json:"chatid"`
OpenConversationId string `json:"openConversationId"`
ConversationTag int `json:"conversationTag"`
}
if err = json.Unmarshal(res.Content, &resp); err != nil {
return
}
if resp.Code != 0 {
return "", "", errors.New(resp.Msg)
}
return resp.ChatId, resp.OpenConversationId, nil
}
// CreateSceneGroupConversation 创建场景群-基于群模板
func (c *OldClient) CreateSceneGroupConversation(ctx context.Context, accessToken, groupName string, userIds []string, templateId string) (chatId, openConversationId string, err error) {
body := struct {
Title string `json:"title"` // 群名称
TemplateId string `json:"template_id"` // 群模板ID
OwnerUserID string `json:"owner_user_id"` // 群主的userid。
UserIds string `json:"user_ids"` // 群成员userid列表。
SubAdminIds string `json:"subadmin_ids"` // 群管理员userid列表。
UUID string `json:"uuid"` // 建群去重的业务ID由接口调用方指定。
Icon string `json:"icon"` // 群头像格式为mediaId。需要调用上传媒体文件接口上传群头像获取mediaId。
MentionAllAuthority int `json:"mention_all_authority"` // @all 权限0默认所有人都可以@all 1仅群主可@all
ShowHistoryType int `json:"show_history_type"` // 新成员是否可查看聊天历史消息0默认不可以查看历史记录 1可以查看历史记录
ValidationType int `json:"validation_type"` // 入群是否需要验证0默认不验证入群 1入群验证
Searchable int `json:"searchable"` // 群是否可搜索0默认不可搜索 1可搜索
ChatBannedType int `json:"chat_banned_type"` // 是否开启群禁言0默认不禁言 1全员禁言
ManagementType int `json:"management_type"` // 管理类型0默认所有人可管理 1仅群主可管理
OnlyAdminCanDing int `json:"only_admin_can_ding"` // 群内发DING权限0默认所有人可发DING 1仅群主和管理员可发DING
AllMembersCanCreateMcsConf int `json:"all_members_can_create_mcs_conf"` // 群会议权限0仅群主和管理员可发起视频和语音会议 1默认所有人可发起视频和语音会议
AllMembersCanCreateCalendar int `json:"all_members_can_create_calendar"` // 群日历权限0仅群主和管理员可创建群日历 1默认所有人可创建群日历
GroupEmailDisabled int `json:"group_email_disabled"` // 群邮件权限0默认群内成员可以对本群发送群邮件 1群内成员不可对本群发送群邮件
OnlyAdminCanSetMsgTop int `json:"only_admin_can_set_msg_top"` // 置顶群消息权限0默认所有人可置顶群消息 1仅群主和管理员可置顶群消息
AddFriendForbidden int `json:"add_friend_forbidden"` // 群成员私聊权限0默认所有人可私聊 1普通群成员之间不能够加好友、单聊且部分功能使用受限管理员与非管理员之间不受影响
GroupLiveSwitch int `json:"group_live_switch"` // 群直播权限0仅群主与管理员可发起直播 1默认群内任意成员可发起群直播
MembersToAdminChat int `json:"members_to_admin_chat"` // 是否禁止非管理员向管理员发起单聊0默认非管理员可以向管理员发起单聊 1禁止非管理员向管理员发起单聊
}{
Title: groupName,
TemplateId: templateId,
OwnerUserID: userIds[0],
UserIds: strings.Join(userIds, ","),
SubAdminIds: strings.Join(userIds, ","),
}
b, _ := json.Marshal(body)
req := l_request.Request{
Method: "POST",
JsonByte: b,
Url: "https://oapi.dingtalk.com/topapi/im/chat/scenegroup/create?access_token=" + accessToken,
Headers: map[string]string{
"Content-Type": "application/json",
},
}
res, err := req.Send()
if err != nil {
return
}
var resp struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
Result struct {
ChatId string `json:"chat_id"`
OpenConversationId string `json:"open_conversation_id"`
} `json:"result"`
}
if err = json.Unmarshal(res.Content, &resp); err != nil {
return
}
if resp.Code != 0 {
return "", "", errors.New(resp.Msg)
}
return resp.Result.ChatId, resp.Result.OpenConversationId, nil
}
// 获取入群二维码链接
func (c *OldClient) GetJoinGroupQrcode(ctx context.Context, chatId, userId string) (string, error) {
body := struct {
ChatId string `json:"chatid"`
UserId string `json:"userid"`
}{ChatId: chatId, UserId: userId}
b, _ := json.Marshal(body)
res, err := c.do(ctx, http.MethodPost, "/topapi/chat/qrcode/get", b)
if err != nil {
return "", err
}
var resp struct {
Code int `json:"errcode"`
Msg string `json:"errmsg"`
Result string `json:"result"`
}
if err := json.Unmarshal(res, &resp); err != nil {
return "", err
}
if resp.Code != 0 {
return "", errors.New(resp.Msg)
}
return resp.Result, nil
}

View File

@ -0,0 +1,65 @@
package dingtalk
import (
errorcode "ai_scheduler/internal/data/error"
"encoding/json"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
robot "github.com/alibabacloud-go/dingtalk/robot_1_0"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
)
type RobotClient struct {
cli *robot.Client
oauth2Client *Oauth2Client
}
func NewRobotClient(oauth2Client *Oauth2Client) (*RobotClient, error) {
cfg := &openapi.Config{
Protocol: tea.String("https"),
RegionId: tea.String("central"),
}
c, err := robot.NewClient(cfg)
if err != nil {
return nil, err
}
return &RobotClient{cli: c, oauth2Client: oauth2Client}, nil
}
type SendGroupMessagesReq struct {
MsgKey string
MsgParam map[string]any
OpenConversationId string
RobotCode string
}
func (c *RobotClient) SendGroupMessages(appKey AppKey, req *SendGroupMessagesReq) (string, error) {
// 获取token
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
if err != nil {
return "", err
}
msgParamBytes, _ := json.Marshal(req.MsgParam)
msgParamJson := string(msgParamBytes)
resp, err := c.cli.OrgGroupSendWithOptions(
&robot.OrgGroupSendRequest{
MsgKey: tea.String(req.MsgKey),
MsgParam: tea.String(msgParamJson),
OpenConversationId: tea.String(req.OpenConversationId),
RobotCode: tea.String(req.RobotCode),
},
&robot.OrgGroupSendHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
&util.RuntimeOptions{},
)
if err != nil {
return "", err
}
if resp.Body == nil {
return "", errorcode.ParamErrf("empty response body")
}
return *resp.Body.ProcessQueryKey, nil
}

View File

@ -21,6 +21,10 @@ var ProviderSetClient = wire.NewSet(
dingtalk.NewOldClient, dingtalk.NewOldClient,
dingtalk.NewContactClient, dingtalk.NewContactClient,
dingtalk.NewNotableClient, dingtalk.NewNotableClient,
dingtalk.NewRobotClient,
dingtalk.NewOauth2Client,
dingtalk.NewCardClient,
dingtalk.NewImClient,
utils_oss.NewClient, utils_oss.NewClient,
lsxd.NewLogin, lsxd.NewLogin,

View File

@ -15,7 +15,7 @@ func HandleResponse(c *fiber.Ctx, data interface{}, e error) (err error) {
case error: case error:
err = data.(error) err = data.(error)
case int, int32, int64, float32, float64, string, bool: case int, int32, int64, float32, float64, string, bool:
c.Response().SetBody([]byte(fmt.Sprintf("%s", data))) c.Response().SetBody([]byte(fmt.Sprintf("%v", data)))
case []byte: case []byte:
c.Response().SetBody(data.([]byte)) c.Response().SetBody(data.([]byte))
default: default:

37
internal/pkg/util/json.go Normal file
View File

@ -0,0 +1,37 @@
package util
import (
"encoding/json"
"fmt"
"strconv"
)
type FlexibleType string
func (ft *FlexibleType) UnmarshalJSON(data []byte) error {
var v interface{}
if err := json.Unmarshal(data, &v); err != nil {
return err
}
switch val := v.(type) {
case string:
*ft = FlexibleType(val)
case float64:
*ft = FlexibleType(strconv.FormatFloat(val, 'f', -1, 64))
case bool:
*ft = FlexibleType(strconv.FormatBool(val))
default:
*ft = FlexibleType(fmt.Sprintf("%v", val))
}
return nil
}
func (ft FlexibleType) Int() int {
if ft == "" {
return 0
}
i, _ := strconv.Atoi(string(ft))
return i
}

View File

@ -0,0 +1,62 @@
package util
import (
"os"
"runtime/debug"
"sync"
"time"
"github.com/bytedance/gopkg/util/gopool"
"github.com/go-kratos/kratos/v2/log"
)
var (
logger *log.Helper
once sync.Once
)
// getLogger 懒加载获取日志器
func getLogger() *log.Helper {
once.Do(func() {
// 如果没有手动初始化,使用默认的标准输出日志器
if logger == nil {
stdLogger := log.With(log.NewStdLogger(os.Stdout),
"ts", log.DefaultTimestamp,
"caller", log.DefaultCaller,
"component", "safe_pool",
)
logger = log.NewHelper(stdLogger)
}
})
return logger
}
// InitSafePool 初始化安全协程池(可选,如果不调用会使用默认日志器)
func InitSafePool(l log.Logger) {
logger = log.NewHelper(l)
}
// SafeGo 安全执行协程
// taskName: 协程任务名称,用于日志记录
// fn: 要执行的函数
func SafeGo(taskName string, fn func()) {
gopool.Go(func() {
defer func() {
if r := recover(); r != nil {
stack := debug.Stack()
getLogger().Errorf("协程 [%s] 发生panic: %v\n堆栈信息:\n%s", taskName, r, string(stack))
}
}()
// 记录协程开始执行
getLogger().Infof("协程 [%s] 开始执行", taskName)
start := time.Now()
// 执行用户函数
fn()
// 记录协程执行完成
duration := time.Since(start)
getLogger().Infof("协程 [%s] 执行完成,耗时: %v", taskName, duration)
})
}

View File

@ -3,6 +3,7 @@ package server
import ( import (
"ai_scheduler/internal/services" "ai_scheduler/internal/services"
"context" "context"
"fmt"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
@ -20,6 +21,7 @@ type cronJob struct {
EntryId int32 EntryId int32
Func func(context.Context) error Func func(context.Context) error
Name string Name string
Key string
Schedule string Schedule string
} }
@ -42,11 +44,13 @@ func (c *CronServer) InitJobs(ctx context.Context) {
{ {
Func: c.cronService.CronReportSendDingTalk, Func: c.cronService.CronReportSendDingTalk,
Name: "直连天下报表推送(钉钉)", Name: "直连天下报表推送(钉钉)",
Key: "ding_report_dingtalk",
Schedule: "20 12,18,23 * * *", Schedule: "20 12,18,23 * * *",
}, },
{ {
Func: c.cronService.CronReportSendQywx, Func: c.cronService.CronReportSendQywx,
Name: "直连天下报表推送(微信)", Name: "直连天下报表推送(微信)",
Key: "ding_report_qywx",
Schedule: "20 12,18,23 * * *", Schedule: "20 12,18,23 * * *",
}, },
} }
@ -96,3 +100,39 @@ func (c *CronServer) Stop() {
c.log.Info("Cron调度器已停止") c.log.Info("Cron调度器已停止")
} }
} }
func (c *CronServer) RunOnce(ctx context.Context, key string) error {
if c.jobs == nil {
c.InitJobs(ctx)
}
// 获取key对应的任务
var job *cronJob
for _, j := range c.jobs {
if j.Key == key {
job = j
break
}
}
if job == nil {
return fmt.Errorf("unknown job key: %s\n", key)
}
defer func() {
if r := recover(); r != nil {
fmt.Printf("任务[once]:%s执行时发生panic: %v\n", job.Name, r)
}
fmt.Printf("任务[once]:%s执行结束\n", job.Name)
}()
fmt.Printf("任务[once]:%s开始执行\n", job.Name)
err := job.Func(ctx)
if err != nil {
return fmt.Errorf("任务[once]:%s执行失败: %s\n", job.Name, err.Error())
}
fmt.Printf("任务[once]:%s执行成功\n", job.Name)
return nil
}

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"sync" "sync"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/client"
"github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/log"
@ -15,6 +16,7 @@ import (
type DingBotServiceInterface interface { type DingBotServiceInterface interface {
GetServiceCfg() ([]entitys.DingTalkBot, error) GetServiceCfg() ([]entitys.DingTalkBot, error)
OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error) OnChatBotMessageReceived(ctx context.Context, data *chatbot.BotCallbackDataModel) (content []byte, err error)
OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error)
} }
type DingTalkBotServer struct { type DingTalkBotServer struct {
@ -38,7 +40,7 @@ func NewDingTalkBotServer(
} }
cli := DingBotServerInit(serviceConf.ClientId, serviceConf.ClientSecret, service) cli := DingBotServerInit(serviceConf.ClientId, serviceConf.ClientSecret, service)
if cli == nil { if cli == nil {
log.Info("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error()) log.Infof("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
continue continue
} }
clients[serviceConf.BotIndex] = cli clients[serviceConf.BotIndex] = cli
@ -52,7 +54,9 @@ func NewDingTalkBotServer(
func ProvideAllDingBotServices( func ProvideAllDingBotServices(
dingBotSvc *services.DingBotService, dingBotSvc *services.DingBotService,
) []DingBotServiceInterface { ) []DingBotServiceInterface {
return []DingBotServiceInterface{dingBotSvc} return []DingBotServiceInterface{
dingBotSvc,
}
} }
func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) { func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
@ -103,5 +107,6 @@ func (d *DingTalkBotServer) Run(ctx context.Context, botIndex string) {
func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) { func DingBotServerInit(clientId string, clientSecret string, service DingBotServiceInterface) (cli *client.StreamClient) {
cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret))) cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived) cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived)
cli.RegisterCardCallbackRouter(service.OnCardMessageReceived)
return return
} }

View File

@ -62,7 +62,11 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
r.Post("/chat/useful", ChatService.Useful) r.Post("/chat/useful", ChatService.Useful)
// 回调 // 回调
r.Post("/callback", callbackService.Callback) r.Post("/callback", callbackService.Callback)
// 回调 // 钉钉机器人回调
r.Post("/callback/dingtalk-robot", callbackService.CallbackDingtalkRobot)
// 钉钉卡片回调
r.Post("/callback/dingtalk-card", callbackService.CallbackDingtalkCard)
// 企业微信回调
r.Get("/qywx/callback", callbackService.QywxCallback) r.Get("/qywx/callback", callbackService.QywxCallback)
//广播 //广播
r.Get("/broadcast", func(ctx *fiber.Ctx) error { r.Get("/broadcast", func(ctx *fiber.Ctx) error {
@ -107,11 +111,13 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
advicer.Post("skill/list", adviceTalkSkill.TalkSkillList) advicer.Post("skill/list", adviceTalkSkill.TalkSkillList)
advicer.Post("skill/add", adviceTalkSkill.TalkSkillAdd) advicer.Post("skill/add", adviceTalkSkill.TalkSkillAdd)
advicer.Post("skill/update", adviceTalkSkill.TalkSkillUpdate) advicer.Post("skill/update", adviceTalkSkill.TalkSkillUpdate)
advicer.Post("skill/del", adviceTalkSkill.TalkSkillUpdate) advicer.Post("skill/del", adviceTalkSkill.TalkSkillDel)
//项目 //项目
advicer.Post("project/add", adviceProject.Add) advicer.Post("project/base/init", adviceProject.BaseInit)
advicer.Post("project/update", adviceProject.Update) advicer.Post("project/base/update", adviceProject.BaseUpdate)
advicer.Post("project/info/add", adviceProject.Add)
advicer.Post("project/info/update", adviceProject.Update)
advicer.Post("project/info", adviceProject.Info) advicer.Post("project/info", adviceProject.Info)
//客户 //客户
@ -120,7 +126,7 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
advicer.Post("client/list", adviceClient.List) advicer.Post("client/list", adviceClient.List)
advicer.Post("client/del", adviceClient.Del) advicer.Post("client/del", adviceClient.Del)
//客户 //会话
advicer.Post("chat/regis", adviceChat.Regis) advicer.Post("chat/regis", adviceChat.Regis)
advicer.Post("chat/chat", adviceChat.Chat) advicer.Post("chat/chat", adviceChat.Chat)
@ -181,6 +187,9 @@ func registerCommon(c *fiber.Ctx, err error) error {
} }
body := c.Response().Body() body := c.Response().Body()
if c.Locals("skip_response_wrap") == true {
return c.JSON(string(body))
}
var rawData json.RawMessage var rawData json.RawMessage
if len(body) > 0 { if len(body) > 0 {
if err := json.Unmarshal(body, &rawData); err != nil { if err := json.Unmarshal(body, &rawData); err != nil {

View File

@ -31,7 +31,8 @@ func (d *AdvicerService) AdvicerUpdate(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
return d.adviceBiz.Update(c.UserContext(), req) id, err := d.adviceBiz.Update(c.UserContext(), req)
return pkg.HandleResponse(c, int(id), err)
} }
func (d *AdvicerService) AdvicerList(c *fiber.Ctx) error { func (d *AdvicerService) AdvicerList(c *fiber.Ctx) error {
@ -48,7 +49,8 @@ func (d *AdvicerService) AdvicerVersionAdd(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
return d.adviceBiz.VersionAdd(c.UserContext(), req) id, err := d.adviceBiz.VersionAdd(c.UserContext(), req)
return pkg.HandleResponse(c, id, err)
} }
func (d *AdvicerService) AdvicerVersionUpdate(c *fiber.Ctx) error { func (d *AdvicerService) AdvicerVersionUpdate(c *fiber.Ctx) error {

View File

@ -53,6 +53,10 @@ func (a *ChatService) Regis(c *fiber.Ctx) error {
if len(req.TalkSkillId) == 0 { if len(req.TalkSkillId) == 0 {
return errorcode.ParamErr("talkSkillId is empty") return errorcode.ParamErr("talkSkillId is empty")
} }
if len(req.Mission) == 0 {
return errorcode.ParamErr("misiion is empty")
}
//顾问版本信息 //顾问版本信息
versionInfo, err := a.adviceAdvicerBiz.VersionInfo(c.UserContext(), &entitys.AdvicerVersionInfoReq{ versionInfo, err := a.adviceAdvicerBiz.VersionInfo(c.UserContext(), &entitys.AdvicerVersionInfoReq{
Id: req.AdvicerVersionId, Id: req.AdvicerVersionId,
@ -68,6 +72,7 @@ func (a *ChatService) Regis(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
//项目信息 //项目信息
projectInfo, err := a.adviceProjectBiz.Info(c.UserContext(), &entitys.AdvicerProjectInfoReq{ projectInfo, err := a.adviceProjectBiz.Info(c.UserContext(), &entitys.AdvicerProjectInfoReq{
ProjectId: advicerInfo.ProjectID, ProjectId: advicerInfo.ProjectID,
@ -75,6 +80,15 @@ func (a *ChatService) Regis(c *fiber.Ctx) error {
if err != nil { if err != nil {
return err return err
} }
if len(projectInfo.ModelInfo.Key) == 0 {
return errorcode.ParamErr("项目未设置模型key")
}
if len(projectInfo.ModelInfo.ChatModel) == 0 {
return errorcode.ParamErr("项目未设置模型对话模型")
}
//销售技巧 //销售技巧
talkSkill, err := a.adviceSkillBiz.Info(c.UserContext(), &entitys.AdvicerTalkSkillInfoReq{ talkSkill, err := a.adviceSkillBiz.Info(c.UserContext(), &entitys.AdvicerTalkSkillInfoReq{
Id: req.TalkSkillId, Id: req.TalkSkillId,
@ -96,11 +110,12 @@ func (a *ChatService) Regis(c *fiber.Ctx) error {
chat := entitys.ChatData{ chat := entitys.ChatData{
ClientInfo: clientInfo.Entity(), ClientInfo: clientInfo.Entity(),
TalkSkill: talkSkill.Entity(), TalkSkill: talkSkill.Entity(),
ProjectInfo: projectInfo.Entity(), ProjectInfo: projectInfo.ConfigInfo.Entity(),
AdvicerInfo: advicerInfo.Entity(), AdvicerInfo: advicerInfo.Entity(),
AdvicerVersion: versionInfo.Entity(), AdvicerVersion: versionInfo.Entity(),
} }
sessionId, err := a.adviceChatBiz.Regis(c.UserContext(), &chat) sessionId, err := a.adviceChatBiz.Regis(c.UserContext(), &chat, req, projectInfo)
log.Info(sessionId) log.Info(sessionId)
return pkg.HandleResponse(c, sessionId, err) return pkg.HandleResponse(c, sessionId, err)
} }
@ -110,7 +125,28 @@ func (a *ChatService) Chat(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
if len(req.SessionId) == 0 {
return errorcode.ParamErr("SessionId is empty")
}
if len(req.Content) == 0 {
return errorcode.ParamErr("Content is empty")
}
res, err := a.adviceChatBiz.Chat(c.UserContext(), req)
log.Info(res)
return pkg.HandleResponse(c, res, err)
}
func (a *ChatService) ChatContext(c *fiber.Ctx) error {
req := &entitys.AdvicerChatReq{}
if err := c.BodyParser(req); err != nil {
return err
}
if len(req.SessionId) == 0 {
return errorcode.ParamErr("SessionId is empty")
}
if len(req.Content) == 0 {
return errorcode.ParamErr("Content is empty")
}
res, err := a.adviceChatBiz.Chat(c.UserContext(), req) res, err := a.adviceChatBiz.Chat(c.UserContext(), req)
log.Info(res) log.Info(res)
return pkg.HandleResponse(c, res, err) return pkg.HandleResponse(c, res, err)

View File

@ -31,7 +31,8 @@ func (d *ClientService) Add(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
return d.AdviceClientBiz.Add(c.UserContext(), req) id, err := d.AdviceClientBiz.Add(c.UserContext(), req)
return pkg.HandleResponse(c, id, err)
} }
func (d *ClientService) Update(c *fiber.Ctx) error { func (d *ClientService) Update(c *fiber.Ctx) error {

View File

@ -3,6 +3,7 @@ package advice
import ( import (
"ai_scheduler/internal/biz" "ai_scheduler/internal/biz"
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
errorcode "ai_scheduler/internal/data/error"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg" "ai_scheduler/internal/pkg"
@ -26,12 +27,33 @@ func NewProjectService(
} }
} }
func (d *ProjectService) BaseInit(c *fiber.Ctx) error {
req := &entitys.AdvicerProjectBaseAddReq{}
if err := c.BodyParser(req); err != nil {
return err
}
add, err := d.adviceProjectBiz.BaseAdd(c.UserContext(), req)
return pkg.HandleResponse(c, add, err)
}
func (d *ProjectService) BaseUpdate(c *fiber.Ctx) error {
req := &entitys.AdvicerProjectBaseUpdateReq{}
if err := c.BodyParser(req); err != nil {
return err
}
if req.ProjectId == 0 {
return errorcode.ParamErr("projectId is empty")
}
return d.adviceProjectBiz.BaseUpdate(c.UserContext(), req)
}
func (d *ProjectService) Add(c *fiber.Ctx) error { func (d *ProjectService) Add(c *fiber.Ctx) error {
req := &entitys.AdvicerProjectAddReq{} req := &entitys.AdvicerProjectAddReq{}
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
return d.adviceProjectBiz.Add(c.UserContext(), req) id, err := d.adviceProjectBiz.Add(c.UserContext(), req)
return pkg.HandleResponse(c, id, err)
} }
func (d *ProjectService) Update(c *fiber.Ctx) error { func (d *ProjectService) Update(c *fiber.Ctx) error {

View File

@ -31,7 +31,8 @@ func (d *TalkSkillService) TalkSkillAdd(c *fiber.Ctx) error {
if err := c.BodyParser(req); err != nil { if err := c.BodyParser(req); err != nil {
return err return err
} }
return d.adviceSkillBiz.VersionAdd(c.UserContext(), req) id, err := d.adviceSkillBiz.VersionAdd(c.UserContext(), req)
return pkg.HandleResponse(c, id, err)
} }
func (d *TalkSkillService) TalkSkillUpdate(c *fiber.Ctx) error { func (d *TalkSkillService) TalkSkillUpdate(c *fiber.Ctx) error {

View File

@ -1,6 +1,7 @@
package services package services
import ( import (
"ai_scheduler/internal/biz"
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt" "ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants" "ai_scheduler/internal/data/constants"
@ -19,6 +20,8 @@ import (
"strings" "strings"
"time" "time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log" "github.com/gofiber/fiber/v2/log"
) )
@ -30,17 +33,39 @@ type CallbackService struct {
dingtalkOldClient *dingtalk.OldClient dingtalkOldClient *dingtalk.OldClient
dingtalkContactClient *dingtalk.ContactClient dingtalkContactClient *dingtalk.ContactClient
dingtalkNotableClient *dingtalk.NotableClient dingtalkNotableClient *dingtalk.NotableClient
dingtalkCardClient *dingtalk.CardClient
callbackManager callback.Manager callbackManager callback.Manager
dingTalkBotBiz *biz.DingTalkBotBiz
callbackBiz *biz.CallbackBiz
// ollamaClient *utils_ollama.Client
// botConfigImpl *impl.BotConfigImpl
} }
func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkOldClient *dingtalk.OldClient, dingtalkContactClient *dingtalk.ContactClient, dingtalkNotableClient *dingtalk.NotableClient, callbackManager callback.Manager) *CallbackService { func NewCallbackService(
cfg *config.Config,
gateway *gateway.Gateway,
dingtalkOldClient *dingtalk.OldClient,
dingtalkContactClient *dingtalk.ContactClient,
dingtalkNotableClient *dingtalk.NotableClient,
dingtalkCardClient *dingtalk.CardClient,
callbackManager callback.Manager,
dingTalkBotBiz *biz.DingTalkBotBiz,
callbackBiz *biz.CallbackBiz,
// ollamaClient *utils_ollama.Client,
// botConfigImpl *impl.BotConfigImpl,
) *CallbackService {
return &CallbackService{ return &CallbackService{
cfg: cfg, cfg: cfg,
gateway: gateway, gateway: gateway,
dingtalkOldClient: dingtalkOldClient, dingtalkOldClient: dingtalkOldClient,
dingtalkContactClient: dingtalkContactClient, dingtalkContactClient: dingtalkContactClient,
dingtalkNotableClient: dingtalkNotableClient, dingtalkNotableClient: dingtalkNotableClient,
dingtalkCardClient: dingtalkCardClient,
callbackManager: callbackManager, callbackManager: callbackManager,
dingTalkBotBiz: dingTalkBotBiz,
callbackBiz: callbackBiz,
// ollamaClient: ollamaClient,
// botConfigImpl: botConfigImpl,
} }
} }
@ -270,7 +295,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
// 获取创建者uid // 获取创建者uid
accessToken, _ := s.dingtalkOldClient.GetAccessToken() accessToken, _ := s.dingtalkOldClient.GetAccessToken()
creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator) creatorId, err := s.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, data.Creator)
if err != nil { if err != nil {
return "", errorcode.ParamErrf("invalid data type: %v", err) return "", errorcode.ParamErrf("invalid data type: %v", err)
} }
@ -286,7 +311,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
unionId := userDetails.UnionID unionId := userDetails.UnionID
// 更新记录 // 更新记录
ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk.UpdateRecordReq{ ok, err := s.dingtalkNotableClient.UpdateRecord(dingtalk.AppKey{AccessToken: accessToken}, &dingtalk.UpdateRecordReq{
BaseId: data.BaseId, BaseId: data.BaseId,
SheetId: data.SheetId, SheetId: data.SheetId,
RecordId: data.RecordId, RecordId: data.RecordId,
@ -364,3 +389,76 @@ func getString(str, endstr string, start int, msg *string) int {
*msg = str[start:end] *msg = str[start:end]
return end + len(endstr) return end + len(endstr)
} }
// CallbackDingtalkRobot 钉钉机器人回调
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-robot
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
// 获取body中的参数
body := c.Request().Body()
var data chatbot.BotCallbackDataModel
if err := json.Unmarshal(body, &data); err != nil {
return fmt.Errorf("invalid body: %v", err)
}
// token 校验 ? token 好像没带?
// 通过机器人ID路由到不同能力
switch data.RobotCode {
case s.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling:
// 问题处理群机器人
// err := s.issueHandling(data)
err := s.callbackBiz.IssueHandlingGroup(data)
if err != nil {
return fmt.Errorf("IssueHandlingGroup failed: %v", err)
}
default:
// 其他机器人
return nil
}
return nil
}
// CallbackDingtalkCard 处理钉钉卡片回调
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error {
// 获取body中的参数
body := c.Request().Body()
// HTTP 回调结构与SDK结构体不符包装结构体
tmp := struct {
card.CardRequest // 嵌入原结构体
UserIdType util.FlexibleType `json:"userIdType"` // 重写type字段
}{}
if err := json.Unmarshal(body, &tmp); err != nil {
return fmt.Errorf("invalid body: %v", err)
}
// 异常字段覆盖
data := tmp.CardRequest
data.UserIdType = tmp.UserIdType.Int()
if err := json.Unmarshal([]byte(data.Content), &data.CardActionData); err != nil {
return fmt.Errorf("invalid content: %v", err)
}
log.Debug("body%s", string(body))
// 非回调类型不处理
if data.Type != constants.CardActionCallbackTypeAction {
return nil
}
// 处理卡片回调
var resp *card.CardResponse
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
switch actionId {
case "collect_qa":
// 问题处理群机器人 QA 收集
resp = s.callbackBiz.IssueHandlingCollectQA(data)
}
}
// 跳过响应包装
c.Locals("skip_response_wrap", true)
return c.JSON(resp)
}

View File

@ -3,12 +3,14 @@ package services
import ( import (
"ai_scheduler/internal/biz" "ai_scheduler/internal/biz"
"ai_scheduler/internal/config" "ai_scheduler/internal/config"
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/entitys" "ai_scheduler/internal/entitys"
"context" "context"
"log" "log"
"sync" "sync"
"time" "time"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot" "gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -18,7 +20,10 @@ type DingBotService struct {
dingTalkBotBiz *biz.DingTalkBotBiz dingTalkBotBiz *biz.DingTalkBotBiz
} }
func NewDingBotService(config *config.Config, dingTalkBotBiz *biz.DingTalkBotBiz) *DingBotService { func NewDingBotService(
config *config.Config,
dingTalkBotBiz *biz.DingTalkBotBiz,
) *DingBotService {
return &DingBotService{ return &DingBotService{
config: config, config: config,
dingTalkBotBiz: dingTalkBotBiz, dingTalkBotBiz: dingTalkBotBiz,
@ -140,3 +145,26 @@ func (d *DingBotService) runBackgroundTasks(ctx context.Context, data *chatbot.B
return nil return nil
} }
// OnCardMessageReceived 处理卡片回调
func (d *DingBotService) OnCardMessageReceived(ctx context.Context, data *card.CardRequest) (resp *card.CardResponse, err error) {
// 非回调类型暂不接受
if data.Type != constants.CardActionCallbackTypeAction {
return nil, nil
}
// action 处理 - 这里先只处理第一个匹配的actionId
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
switch actionId {
case constants.CardActionTypeCreateGroup:
resp, err = d.dingTalkBotBiz.CreateIssueHandlingGroupAndInit(ctx, data)
if err != nil {
return nil, err
}
return
}
}
return &card.CardResponse{}, nil
}

View File

@ -1,7 +1,6 @@
package dataTemp package dataTemp
import ( import (
"ai_scheduler/internal/pkg/mapstructure"
"ai_scheduler/utils" "ai_scheduler/utils"
"context" "context"
"database/sql" "database/sql"
@ -10,7 +9,6 @@ import (
"github.com/go-kratos/kratos/v2/log" "github.com/go-kratos/kratos/v2/log"
"gorm.io/gorm" "gorm.io/gorm"
"xorm.io/builder" "xorm.io/builder"
) )
@ -62,18 +60,17 @@ func (k DataTemp) GetById(id int32) (data map[string]interface{}, err error) {
} }
func (k DataTemp) Add(data interface{}) (id int, err error) { func (k DataTemp) Add(data interface{}) (id int, err error) {
var primary *PrimaryKey
add := k.Db.Model(k.Model).Create(data) add := k.Db.Model(k.Model).Create(data)
_ = mapstructure.Decode(data, &primary) return 0, add.Error
return primary.Id, add.Error
} }
func (k DataTemp) AddWithData(data interface{}) (interface{}, error) { func (k DataTemp) AddWithData(data interface{}) error {
result := k.Db.Model(k.Model).Create(data) result := k.Db.Model(k.Model).Create(data)
if result.Error != nil { if result.Error != nil {
return data, result.Error return result.Error
} }
return data, nil return nil
} }
func (k DataTemp) GetList(cond *builder.Cond, pageBoIn *ReqPageBo) (list []map[string]interface{}, pageBoOut *RespPageBo, err error) { func (k DataTemp) GetList(cond *builder.Cond, pageBoIn *ReqPageBo) (list []map[string]interface{}, pageBoOut *RespPageBo, err error) {