Compare commits
72 Commits
master
...
feature/rz
| Author | SHA1 | Date |
|---|---|---|
|
|
041e9fad0b | |
|
|
ec53207225 | |
|
|
9590721d30 | |
|
|
46049475c1 | |
|
|
6394715bfe | |
|
|
ec5ff4a0a9 | |
|
|
24f5b43ea9 | |
|
|
a53f3af1c3 | |
|
|
48f248fecd | |
|
|
f0e8613d57 | |
|
|
849eb44870 | |
|
|
3b5ac98c08 | |
|
|
02e88ed610 | |
|
|
b104572e1b | |
|
|
99865c2bc4 | |
|
|
cfeaa6e201 | |
|
|
c9c9bca9ce | |
|
|
2f5b0af3a4 | |
|
|
ec41a3d787 | |
|
|
719fd805e6 | |
|
|
c174ab683a | |
|
|
c1971e71c1 | |
|
|
ece04df2cb | |
|
|
7e71ad52a4 | |
|
|
847eb8b5db | |
|
|
3b6471a196 | |
|
|
b3b09f184b | |
|
|
1eb63498d7 | |
|
|
6fedb76631 | |
|
|
091a3a50b0 | |
|
|
7120eef4e8 | |
|
|
3fc8c5dd93 | |
|
|
2cdeb4a9ae | |
|
|
a7ac1610bb | |
|
|
5a4dc13324 | |
|
|
22a9de2841 | |
|
|
a950a7b025 | |
|
|
f79481d2bf | |
|
|
27b7191865 | |
|
|
e5bbddd58d | |
|
|
6c7ee0a666 | |
|
|
6f33665e16 | |
|
|
e19ccfa0f3 | |
|
|
71a5118180 | |
|
|
5b11cb728f | |
|
|
2fd3d2ae60 | |
|
|
39d2fc1e62 | |
|
|
36db8e7a86 | |
|
|
fa08cad74a | |
|
|
32cd8691b7 | |
|
|
5560e879d0 | |
|
|
c74fe839d8 | |
|
|
634bca5c60 | |
|
|
d7ae15797b | |
|
|
855156374e | |
|
|
498d165915 | |
|
|
a3935cf9ec | |
|
|
451f68056c | |
|
|
5d58cbc0f6 | |
|
|
0430595a73 | |
|
|
7f5947c443 | |
|
|
6173bd00b1 | |
|
|
5b1a138ca1 | |
|
|
51f012d315 | |
|
|
9468037d66 | |
|
|
6173a92735 | |
|
|
b91b7bb328 | |
|
|
534da15898 | |
|
|
a0b76f1581 | |
|
|
17d7b01fdf | |
|
|
44864cc7f0 | |
|
|
e8061799b8 |
|
|
@ -1,4 +1,48 @@
|
|||
[https://p6-img.searchpstatp.com/tos-cn-i-vvloioitz3/6e5e76d274df2efabde9194a06f97e89~tplv-vvloioitz3-6:190:124.jpeg]
|
||||
**[输出格式]**
|
||||
- **格式类型**:严格输出json格式字符串,不需要其他任何格式和内容
|
||||
- **数据结构**:
|
||||
{
|
||||
"result": "{{chat_content}}",
|
||||
"mission_status": "{{mission_status}}",
|
||||
"mission_complete_desc": "{{mission_complete_desc}}"
|
||||
}
|
||||
|
||||
**[字段说明]**
|
||||
1. **result** (字符串)
|
||||
- 对应变量:`{{chat_content}}`
|
||||
- 内容:顾问的实际对话内容
|
||||
- 要求:自然语言回复,面向用户
|
||||
|
||||

|
||||
2. **mission_status** (字符串)
|
||||
- 对应变量:`{{mission_status}}`
|
||||
- 取值:`"completed"` 或 `"in_progress"` 或 `"fail"`
|
||||
- 说明:标识当前任务完成状态
|
||||
- `"completed"`:任务已全部完成
|
||||
- `"in_progress"`:任务仍在进行中
|
||||
- `"fail"`:任务失败,客户已经明确拒绝或者对任务内容表达反对
|
||||
|
||||
3. **mission_complete_desc** (字符串)
|
||||
- 对应变量:`{{mission_complete_desc}}`
|
||||
- 内容:根据mission_status提供相应描述
|
||||
- 当`mission_status: "completed"`时:简要总结任务完成情况
|
||||
- 当`mission_status: "in_progress"`时:说明下一步需要做什么
|
||||
|
||||
**[示例]**
|
||||
{
|
||||
"result": "需要我给您安排时间吗",
|
||||
"mission_status": "in_progress",
|
||||
"mission_complete_desc": "需要用户确认什么时候到售楼部"
|
||||
}
|
||||
|
||||
{
|
||||
"result": "好的,那就周日下午两点,我到时候在售楼部等您,来了记得给我打电话",
|
||||
"mission_status": "completed",
|
||||
"mission_complete_desc": "客户确认周日下午两点到售楼部"
|
||||
}
|
||||
|
||||
**[强制要求]**
|
||||
1. 必须输出完整、有效的JSON对象
|
||||
2. 所有字段均为必需字段,不可省略
|
||||
3. JSON格式必须严格正确,无语法错误
|
||||
4. `mission_status`只能使用指定的两个值
|
||||
5. `result`字段内容需符合对话语境
|
||||
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "./config/config.yaml", "Path to configuration file")
|
||||
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
||||
onBot := flag.String("bot", "", "bot start")
|
||||
cron := flag.String("cron", "", "close")
|
||||
runJob := flag.String("runJob", "", "run single job and exit")
|
||||
|
|
@ -20,8 +20,7 @@ func main() {
|
|||
if err != nil {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
app, cleanup, err := InitializeApp(bc, log.DefaultLogger())
|
||||
app, cleanup, err := InitializeApp(ctx, bc, log.DefaultLogger())
|
||||
if err != nil {
|
||||
log.Fatalf("项目初始化失败: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,14 @@ import (
|
|||
"ai_scheduler/internal/biz/tools_regis"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/domain/component"
|
||||
"ai_scheduler/internal/domain/repo"
|
||||
"ai_scheduler/internal/domain/workflow"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/server"
|
||||
"ai_scheduler/internal/services"
|
||||
"context"
|
||||
|
||||
// "ai_scheduler/internal/tool_callback"
|
||||
"ai_scheduler/internal/tools"
|
||||
|
|
@ -26,7 +28,7 @@ import (
|
|||
)
|
||||
|
||||
// InitializeApp 初始化应用程序
|
||||
func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), error) {
|
||||
func InitializeApp(context.Context, *config.Config, log.AllLogger) (*server.Servers, func(), error) {
|
||||
panic(wire.Build(
|
||||
server.ProviderSetServer,
|
||||
workflow.ProviderSetWorkflow,
|
||||
|
|
@ -42,6 +44,7 @@ func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), erro
|
|||
// tool_callback.ProviderSetCallBackTools,
|
||||
component.ProviderSet,
|
||||
repo.ProviderSet,
|
||||
mongo_model.ProviderSetMongo,
|
||||
))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,27 +4,21 @@ server:
|
|||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://192.168.6.115:11434"
|
||||
model: "qwen3:8b"
|
||||
generate_model: "qwen3:8b"
|
||||
mapping_model: "qwen3:8b"
|
||||
# model: "qwen3-coder:480b-cloud"
|
||||
# generate_model: "qwen3-coder:480b-cloud"
|
||||
# mapping_model: "deepseek-v3.2:cloud"
|
||||
base_url: "http://172.17.0.1:11434"
|
||||
# model: "qwen3:8b"
|
||||
# generate_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"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
base_url: "http://172.17.0.1:8001/v1"
|
||||
vl_model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
|
|
@ -65,6 +59,14 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
mongo:
|
||||
source: mongodb://root:lsxd2026123@192.168.6.115:27017
|
||||
dataBase: ai_scheduler
|
||||
maxPoolSize: 100
|
||||
minPoolSize: 10
|
||||
maxConnIdleTime: 30
|
||||
connectTimeout: 10
|
||||
socketTimeout: 30
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
|
|
@ -155,7 +157,7 @@ eino_tools:
|
|||
# == 通用工具 ==
|
||||
# 表格转图片
|
||||
excel2pic:
|
||||
base_url: "http://192.168.6.115:8010/api/v1/convert"
|
||||
base_url: "http://excel2pic:8000/api/v1/convert"
|
||||
|
||||
dingtalk:
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
|
|
|
|||
|
|
@ -4,24 +4,21 @@ server:
|
|||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://192.168.6.109:11434"
|
||||
model: "qwen3-coder:480b-cloud"
|
||||
generate_model: "qwen3-coder:480b-cloud"
|
||||
mapping_model: "deepseek-v3.2:cloud"
|
||||
base_url: "http://192.168.6.115:11434"
|
||||
model: "qwen3:8b"
|
||||
generate_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:7b"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
base_url: "http://117.175.169.61:16001/v1"
|
||||
vl_model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
|
|
@ -56,6 +53,14 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
mongo:
|
||||
source: mongodb://root:lsxd2026123@192.168.6.115:27017
|
||||
dataBase: ai_scheduler_test
|
||||
maxPoolSize: 100
|
||||
minPoolSize: 10
|
||||
maxConnIdleTime: 30
|
||||
connectTimeout: 10
|
||||
socketTimeout: 30
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
|
|
@ -94,9 +99,22 @@ tools:
|
|||
zltxOrderAfterSaleResellerBatch:
|
||||
enabled: true
|
||||
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_tools:
|
||||
# == 货易通 hyt ==
|
||||
|
|
@ -155,6 +173,26 @@ dingtalk:
|
|||
# 机器人群组
|
||||
bot_group_id:
|
||||
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:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
|
|
@ -177,6 +215,16 @@ default_prompt:
|
|||
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
|
||||
'
|
||||
user_prompt: '识别图片内容'
|
||||
|
||||
# 权限配置
|
||||
permissionConfig:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,24 +4,18 @@ server:
|
|||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://192.168.6.115:11434"
|
||||
model: "qwen3:8b"
|
||||
generate_model: "qwen3:8b"
|
||||
mapping_model: "qwen3:8b"
|
||||
base_url: "http://host.docker.internal:11434"
|
||||
model: "qwen3-coder:480b-cloud"
|
||||
generate_model: "qwen3-coder:480b-cloud"
|
||||
mapping_model: "deepseek-v3.2:cloud"
|
||||
vl_model: "gemini-3-pro-preview"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
base_url: "http://host.docker.internal:8001/v1"
|
||||
vl_model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
|
|
@ -32,11 +26,12 @@ coze:
|
|||
|
||||
lsxd:
|
||||
# 统一登录
|
||||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
code: "456789"
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
login_url: "http://api.test.user.1688sup.com/v1/login/phone"
|
||||
phone: "OFJ8UpqOlI7+w3Qklf36ZA=="
|
||||
password: "tEbFegH/DRRh6LutFb7o3g=="
|
||||
code: "123456"
|
||||
check_token_url: "http://api.test.user.1688sup.com/v1/user/welcome"
|
||||
|
||||
|
||||
sys:
|
||||
session_len: 6
|
||||
|
|
@ -57,6 +52,15 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
mongo:
|
||||
source: mongodb://root:lsxd2026123@192.168.6.115:27017
|
||||
dataBase: ai_scheduler_test
|
||||
maxPoolSize: 100
|
||||
minPoolSize: 10
|
||||
maxConnIdleTime: 30
|
||||
connectTimeout: 10
|
||||
socketTimeout: 30
|
||||
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
|
|
@ -147,7 +151,7 @@ eino_tools:
|
|||
# == 通用工具 ==
|
||||
# 表格转图片
|
||||
excel2pic:
|
||||
base_url: "http://192.168.6.115:8010/api/v1/convert"
|
||||
base_url: "http://192.168.6.109:8010/api/v1/convert"
|
||||
|
||||
dingtalk:
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
|
|
@ -159,6 +163,26 @@ dingtalk:
|
|||
# 机器人群组
|
||||
bot_group_id:
|
||||
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:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
|
|
@ -184,6 +208,15 @@ default_prompt:
|
|||
permissionConfig:
|
||||
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:
|
||||
providers:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
#export GO111MODULE=on
|
||||
#export GOPROXY=https://goproxy.cn,direct
|
||||
#export GOPATH=/root/go
|
||||
#export GOCACHE=/root/.cache/go-build
|
||||
export CONTAINER_NAME=ai_scheduler
|
||||
export NETWORK_NAME=ai_scheduler_network
|
||||
#export CGO_ENABLED='0'
|
||||
|
||||
|
||||
|
||||
MODE="$1"
|
||||
|
|
@ -27,8 +23,7 @@ fi
|
|||
git fetch origin
|
||||
git checkout "$BRANCH"
|
||||
git pull origin "$BRANCH"
|
||||
#go mod tidy
|
||||
#make build
|
||||
|
||||
docker build -t ${CONTAINER_NAME} .
|
||||
docker stop ${CONTAINER_NAME}
|
||||
docker rm -f ${CONTAINER_NAME}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
# MySQL 8.0 服务
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: mysql_db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-lsxd2026}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-myapp}
|
||||
MYSQL_USER: ${MYSQL_USER:-myuser}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-mypassword}
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
|
||||
networks:
|
||||
- ai_scheduler_network
|
||||
command:
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
--character-set-server=utf8mb4
|
||||
--collation-server=utf8mb4_unicode_ci
|
||||
--max_connections=1000
|
||||
|
||||
# MongoDB 服务
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
container_name: mongodb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USERNAME:-root}
|
||||
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD:-lsxd2026123}
|
||||
ports:
|
||||
- "27017:27017"
|
||||
volumes:
|
||||
- mongodb_data:/data/db
|
||||
command:
|
||||
--auth
|
||||
--bind_ip_all # 允许所有IP连接
|
||||
networks:
|
||||
- ai_scheduler_network
|
||||
|
||||
# Redis 服务up
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD:-redispassword123} --bind 0.0.0.0
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./redis/redis.conf:/usr/local/etc/redis/redis.conf
|
||||
networks:
|
||||
- ai_scheduler_network
|
||||
|
||||
networks:
|
||||
ai_scheduler_network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
driver: local
|
||||
mongodb_data:
|
||||
driver: local
|
||||
redis_data:
|
||||
driver: local
|
||||
14
go.mod
14
go.mod
|
|
@ -33,7 +33,10 @@ require (
|
|||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
github.com/volcengine/volcengine-go-sdk v1.2.9
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
go.mongodb.org/mongo-driver v1.14.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
|
|
@ -62,6 +65,7 @@ require (
|
|||
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/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/eino-contrib/jsonschema v1.0.3 // indirect
|
||||
github.com/eino-contrib/ollama v0.1.0 // indirect
|
||||
|
|
@ -70,11 +74,13 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/goph/emperror v0.17.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
|
|
@ -88,6 +94,7 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||
github.com/nikolalohinski/gonja v1.5.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
|
|
@ -110,12 +117,16 @@ require (
|
|||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23 // indirect
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||
github.com/yargevad/filepathx v1.0.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
|
|
@ -128,5 +139,6 @@ require (
|
|||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
|||
33
go.sum
33
go.sum
|
|
@ -101,6 +101,7 @@ github.com/aliyun/credentials-go v1.4.6 h1:CG8rc/nxCNKfXbZWpWDzI9GjF4Tuu3Es14qT8
|
|||
github.com/aliyun/credentials-go v1.4.6/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
|
||||
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
|
||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||
|
|
@ -155,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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||
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/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
|
||||
|
|
@ -237,6 +240,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
|
|||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
|
|
@ -248,6 +254,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
|
@ -266,6 +273,7 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
|
|||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
|
|
@ -293,6 +301,10 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
|
|||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
|
|
@ -310,6 +322,7 @@ github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
|
@ -343,6 +356,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
|
|||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
|
||||
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
|
||||
|
|
@ -449,10 +464,20 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S
|
|||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
|
||||
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
|
||||
github.com/volcengine/volcengine-go-sdk v1.2.9 h1:du2gnImtyWXKkQFnJW/GXCs+UBibGGOXIbP1Ams2pB8=
|
||||
github.com/volcengine/volcengine-go-sdk v1.2.9/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
|
||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
|
||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
|
||||
|
|
@ -461,6 +486,8 @@ github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBL
|
|||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
|
||||
github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
@ -468,6 +495,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
|
||||
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
|
|
@ -681,6 +710,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
|
|
@ -839,11 +869,14 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
|||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/bsm/ratelimit.v1 v1.0.0-20160220154919-db14e161995a/go.mod h1:KF9sEfUPAXdG8Oev9e99iLGnl2uJMjc5B+4y3O7x610=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type AdviceAdvicerBiz struct {
|
||||
advicerImpl *impl.AdviceAdvicerImpl
|
||||
advicerVersionMongo *mongo_model.AdvicerVersionMongo
|
||||
mongo *pkg.Mongo
|
||||
}
|
||||
|
||||
func NewAdviceAdvicerBiz(
|
||||
advicerImpl *impl.AdviceAdvicerImpl,
|
||||
advicerVersionMongo *mongo_model.AdvicerVersionMongo,
|
||||
mongo *pkg.Mongo,
|
||||
) *AdviceAdvicerBiz {
|
||||
return &AdviceAdvicerBiz{
|
||||
advicerImpl: advicerImpl,
|
||||
advicerVersionMongo: advicerVersionMongo,
|
||||
mongo: mongo,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) Update(ctx context.Context, data *entitys.AdvicerInitReq) (int32, error) {
|
||||
birth, err := time.Parse("2006-01-02", data.Birth)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
param := &model.AiAdviceAdvicer{
|
||||
AdvicerID: data.AdvicerID,
|
||||
ProjectID: data.ProjectID,
|
||||
Name: data.Name,
|
||||
Birth: birth,
|
||||
Gender: data.Gender,
|
||||
WorkingYears: data.WorkingYears,
|
||||
}
|
||||
if param.AdvicerID == 0 {
|
||||
err = a.advicerImpl.AddWithData(param)
|
||||
} else {
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
|
||||
err = a.advicerImpl.UpdateByCond(&cond, param)
|
||||
}
|
||||
return param.AdvicerID, err
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) List(ctx context.Context, data *entitys.AdvicerListReq) ([]map[string]interface{}, error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"project_id": data.ProjectId})
|
||||
list, err := a.advicerImpl.GetRange(&cond)
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerVersionAddReq) (id interface{}, err error) {
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
|
||||
_, err = a.advicerImpl.GetOneBySearch(&cond)
|
||||
if err != nil {
|
||||
return 0, errors.New("顾问不存在")
|
||||
}
|
||||
res, err := a.mongo.Co(a.advicerVersionMongo).InsertOne(ctx, &mongo_model.AdvicerVersionMongo{
|
||||
AdvicerId: param.AdvicerID,
|
||||
VersionDesc: param.VersionDesc,
|
||||
DialectFeatures: param.DialectFeatures,
|
||||
SentencePatterns: param.SentencePatterns,
|
||||
ToneTags: param.ToneTags,
|
||||
PersonalityTags: param.PersonalityTags,
|
||||
SignatureDialogues: param.SignatureDialogues,
|
||||
LastUpdateTime: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.InsertedID, err
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerVersionUpdateReq) (err error) {
|
||||
filter := bson.M{}
|
||||
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
|
||||
update := bson.M{
|
||||
"$set": &mongo_model.AdvicerVersionMongo{
|
||||
AdvicerId: param.AdvicerID,
|
||||
VersionDesc: param.VersionDesc,
|
||||
DialectFeatures: param.DialectFeatures,
|
||||
SentencePatterns: param.SentencePatterns,
|
||||
ToneTags: param.ToneTags,
|
||||
PersonalityTags: param.PersonalityTags,
|
||||
SignatureDialogues: param.SignatureDialogues,
|
||||
LastUpdateTime: time.Now(),
|
||||
},
|
||||
}
|
||||
res := a.mongo.Co(a.advicerVersionMongo).FindOneAndUpdate(ctx, filter, update)
|
||||
|
||||
return res.Err()
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) VersionList(ctx context.Context, param *entitys.AdvicerVersionListReq) (list []mongo_model.AdvicerVersionMongo, err error) {
|
||||
filter := bson.M{}
|
||||
// 1. advicer_id 条件
|
||||
if param.AdvicerId != 0 {
|
||||
filter["advicerId"] = param.AdvicerId
|
||||
}
|
||||
|
||||
// 2. _id 条件
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
// 3. version_desc 模糊查询
|
||||
if len(param.VersionDesc) != 0 {
|
||||
// 正确的方式:指定字段名
|
||||
filter["versionDesc"] = bson.M{
|
||||
"$regex": primitive.Regex{
|
||||
Pattern: param.VersionDesc,
|
||||
Options: "i",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
cursor, err := a.mongo.Co(a.advicerVersionMongo).Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 遍历结果
|
||||
for cursor.Next(ctx) {
|
||||
var advicerVersion mongo_model.AdvicerVersionMongo
|
||||
if err := cursor.Decode(&advicerVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, advicerVersion)
|
||||
}
|
||||
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) VersionDel(ctx context.Context, param *entitys.AdvicerVersionDelReq) (err error) {
|
||||
filter := bson.M{}
|
||||
// 1. advicer_id 条件
|
||||
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
_, err = a.mongo.Co(a.advicerVersionMongo).DeleteOne(ctx, filter)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) VersionInfo(ctx context.Context, param *entitys.AdvicerVersionInfoReq) (info mongo_model.AdvicerVersionMongo, err error) {
|
||||
filter := bson.M{}
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("ID转换失败: %w", err)
|
||||
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
res := a.mongo.Co(a.advicerVersionMongo).FindOne(ctx, filter)
|
||||
if res.Err() != nil {
|
||||
return info, res.Err()
|
||||
}
|
||||
|
||||
if err := res.Decode(&info); err != nil {
|
||||
return info, err
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (a *AdviceAdvicerBiz) AdvicerInfo(ctx context.Context, param *entitys.AdvicerInfoReq) (info model.AiAdviceAdvicer, err error) {
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"advicer_id": param.AdvicerID})
|
||||
|
||||
err = a.advicerImpl.GetOneBySearchToStrut(&cond, &info)
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"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/pkg"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"xorm.io/builder"
|
||||
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdviceChatBiz struct {
|
||||
hsyq *third_party.Hsyq
|
||||
rdb *utils.Rdb
|
||||
aiAdviceSessionImpl *impl.AiAdviceSessionImpl
|
||||
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl
|
||||
advicerChatHisMongo *mongo_model.AdvicerChatHisMongo
|
||||
mongo *pkg.Mongo
|
||||
}
|
||||
|
||||
func NewAdviceChatBiz(
|
||||
hsyq *third_party.Hsyq,
|
||||
rdb *utils.Rdb,
|
||||
aiAdviceSessionImpl *impl.AiAdviceSessionImpl,
|
||||
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl,
|
||||
advicerChatHisMongo *mongo_model.AdvicerChatHisMongo,
|
||||
mongo *pkg.Mongo,
|
||||
) *AdviceChatBiz {
|
||||
return &AdviceChatBiz{
|
||||
hsyq: hsyq,
|
||||
rdb: rdb,
|
||||
aiAdviceSessionImpl: aiAdviceSessionImpl,
|
||||
aiAdviceModelSupImpl: aiAdviceModelSupImpl,
|
||||
advicerChatHisMongo: advicerChatHisMongo,
|
||||
mongo: mongo,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdviceChatBiz) contextCache(ctx context.Context, chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq, projectInfo *entitys.AdvicerProjectInfoRes) (promptJson string, contextCache string, err error) {
|
||||
switch constants.Mode(projectInfo.ModelInfo.Mode) {
|
||||
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 {
|
||||
return "", 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
|
||||
}
|
||||
|
||||
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 {
|
||||
return assistant, nil
|
||||
}
|
||||
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 {
|
||||
return assistant, err
|
||||
}
|
||||
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 {
|
||||
return assistant, err
|
||||
}
|
||||
resContent, err := a.callLlmResponse(ctx, prompt, modelInfo.Key, modelInfo.ChatModel, session.ContextCache)
|
||||
if err != nil {
|
||||
return assistant, err
|
||||
}
|
||||
|
||||
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) buildChatPromptResponse(ctx context.Context, chat *entitys.AdvicerChatReq, session *dbmodel.AiAdviceSession, chatList []mongo_model.AdvicerChatHisMongoEntity) ([]*responses.InputItem, error) {
|
||||
|
||||
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{
|
||||
Role: model.ChatMessageRoleSystem,
|
||||
Content: &model.ChatCompletionMessageContent{
|
||||
StringValue: volcengine.String(a.sysPrompt(chatData, req)),
|
||||
},
|
||||
}
|
||||
message[1] = &model.ChatCompletionMessage{
|
||||
Role: model.ChatMessageRoleSystem,
|
||||
Content: &model.ChatCompletionMessageContent{
|
||||
StringValue: volcengine.String(a.assistantPrompt(chatData)),
|
||||
},
|
||||
}
|
||||
return message, nil
|
||||
}
|
||||
|
||||
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)
|
||||
var basePrompt []*model.ChatCompletionMessage
|
||||
err := json.Unmarshal([]byte(promptJson), &basePrompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return basePrompt, nil
|
||||
}
|
||||
|
||||
func (a *AdviceChatBiz) sysPrompt(chatData *entitys.ChatData, req *entitys.AdvicerChatRegistReq) string {
|
||||
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 {
|
||||
return pkg.JsonStringIgonErr(chatData)
|
||||
}
|
||||
|
||||
func (a *AdviceChatBiz) getChatDataFromStringSessionId(ctx context.Context, sessionId string) (basePromptJson string, err error) {
|
||||
cache := a.rdb.Rdb.Get(ctx, sessionId)
|
||||
if cache.Err() != nil {
|
||||
err = cache.Err()
|
||||
return
|
||||
}
|
||||
|
||||
return cache.Val(), cache.Err()
|
||||
}
|
||||
|
||||
func (a *AdviceChatBiz) callLlm(ctx context.Context, request model.ContextChatCompletionRequest, key string) (string, error) {
|
||||
res, err := a.hsyq.ChatWithRequest(ctx, key, request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AdviceClientBiz struct {
|
||||
AdvicerClientMongo *mongo_model.AdvicerClientMongo
|
||||
mongo *pkg.Mongo
|
||||
}
|
||||
|
||||
func NewAdviceClientBiz(
|
||||
advicerClientMongo *mongo_model.AdvicerClientMongo,
|
||||
mongo *pkg.Mongo,
|
||||
) *AdviceClientBiz {
|
||||
return &AdviceClientBiz{
|
||||
AdvicerClientMongo: advicerClientMongo,
|
||||
mongo: mongo,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdviceClientBiz) Add(ctx context.Context, param *entitys.AdvicerClientAddReq) (id interface{}, err error) {
|
||||
|
||||
res, err := a.mongo.Co(a.AdvicerClientMongo).InsertOne(ctx, &mongo_model.AdvicerClientMongo{
|
||||
ProjectId: param.ProjectId,
|
||||
AdvicerId: param.AdvicerId,
|
||||
PersonalInfo: param.PersonalInfo,
|
||||
PurchasePurpose: param.PurchasePurpose,
|
||||
CoreDemands: param.CoreDemands,
|
||||
Concerns: param.Concerns,
|
||||
DecisionProfile: param.DecisionProfile,
|
||||
LastUpdateTime: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.InsertedID, err
|
||||
}
|
||||
|
||||
func (a *AdviceClientBiz) Update(ctx context.Context, param *entitys.AdvicerrClientUpdateReq) (err error) {
|
||||
filter := bson.M{}
|
||||
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
|
||||
update := bson.M{
|
||||
"$set": &mongo_model.AdvicerClientMongo{
|
||||
ProjectId: param.ProjectId,
|
||||
AdvicerId: param.AdvicerId,
|
||||
PersonalInfo: param.PersonalInfo,
|
||||
PurchasePurpose: param.PurchasePurpose,
|
||||
CoreDemands: param.CoreDemands,
|
||||
Concerns: param.Concerns,
|
||||
DecisionProfile: param.DecisionProfile,
|
||||
LastUpdateTime: time.Now(),
|
||||
},
|
||||
}
|
||||
res := a.mongo.Co(a.AdvicerClientMongo).FindOneAndUpdate(ctx, filter, update)
|
||||
return res.Err()
|
||||
}
|
||||
|
||||
func (a *AdviceClientBiz) List(ctx context.Context, param *entitys.AdvicerClientListReq) (list []mongo_model.AdvicerClientMongo, err error) {
|
||||
filter := bson.M{}
|
||||
// 1. advicer_id 条件
|
||||
if param.AdvicerId != 0 {
|
||||
filter["AdvicerId"] = param.AdvicerId
|
||||
}
|
||||
|
||||
if param.ProjectId != 0 {
|
||||
filter["projectId"] = param.ProjectId
|
||||
}
|
||||
|
||||
// 2. _id 条件
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
cursor, err := a.mongo.Co(a.AdvicerClientMongo).Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 遍历结果
|
||||
for cursor.Next(ctx) {
|
||||
var advicerVersion mongo_model.AdvicerClientMongo
|
||||
if err := cursor.Decode(&advicerVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, advicerVersion)
|
||||
}
|
||||
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (a *AdviceClientBiz) Del(ctx context.Context, param *entitys.AdvicerClientDelReq) (err error) {
|
||||
filter := bson.M{}
|
||||
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
_, err = a.mongo.Co(a.AdvicerClientMongo).DeleteOne(ctx, filter)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *AdviceClientBiz) Info(ctx context.Context, param *entitys.AdvicerClientInfoReq) (info mongo_model.AdvicerClientMongo, err error) {
|
||||
filter := bson.M{}
|
||||
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("ID转换失败: %w", err)
|
||||
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
res := a.mongo.Co(a.AdvicerClientMongo).FindOne(ctx, filter)
|
||||
if res.Err() != nil {
|
||||
return info, res.Err()
|
||||
}
|
||||
|
||||
if err = res.Decode(&info); err != nil {
|
||||
return info, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/llm_service/third_party"
|
||||
dbmodel "ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type AdviceFileBiz struct {
|
||||
hsyq *third_party.Hsyq
|
||||
}
|
||||
|
||||
func NewAdviceFileBiz(hsyq *third_party.Hsyq) *AdviceFileBiz {
|
||||
return &AdviceFileBiz{
|
||||
hsyq: hsyq,
|
||||
}
|
||||
}
|
||||
|
||||
var DataMap = map[string]mongo_model.AdviceData{
|
||||
"dialectFeatures": &mongo_model.DialectFeatures{},
|
||||
"sentencePatterns": &mongo_model.SentencePatterns{},
|
||||
"personalityTags": &mongo_model.PersonalityTags{},
|
||||
"toneTags": &mongo_model.ToneTags{},
|
||||
"signatureDialogues": &mongo_model.SignatureDialogues{},
|
||||
"regionValue": &mongo_model.RegionValue{},
|
||||
"competitionComparison": &mongo_model.CompetitionComparison{},
|
||||
"coreSellingPoints": &mongo_model.CoreSellingPoints{},
|
||||
"supportingFacilities": &mongo_model.SupportingFacilities{},
|
||||
"developerBacking": &mongo_model.DeveloperBacking{},
|
||||
"needsMining": &mongo_model.NeedsMining{},
|
||||
"painPointResponse": &mongo_model.PainPointResponse{},
|
||||
"valueBuilding": &mongo_model.ValueBuilding{},
|
||||
"closingTechniques": &mongo_model.ClosingTechniques{},
|
||||
"communicationRhythm": &mongo_model.CommunicationRhythm{},
|
||||
"customer": &mongo_model.Customer{},
|
||||
}
|
||||
|
||||
func (a *AdviceFileBiz) WordAna(ctx context.Context, wordContent string, projectInfo *entitys.AdvicerProjectInfoRes) (map[mongo_model.AdviceRole]map[string]mongo_model.AdviceData, error) {
|
||||
if len(projectInfo.ModelInfo.FileModel) == 0 {
|
||||
return nil, fmt.Errorf("项目文件模型信息缺失")
|
||||
}
|
||||
timeSte := time.Now().Format("200601021504")
|
||||
dir := "./cache/" + timeSte
|
||||
os.Mkdir(dir, 0755)
|
||||
//获取示例
|
||||
examples := a.getAllExamples()
|
||||
|
||||
//构建提示词
|
||||
prompt := a.buildSimplePrompt(wordContent, examples)
|
||||
os.WriteFile(dir+"/requset.json", []byte(prompt), 0644)
|
||||
|
||||
//llm提取信息
|
||||
anaContent, err := a.callLlm2(ctx, prompt, &projectInfo.ModelInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
os.WriteFile(dir+"/res.json", []byte(anaContent), 0644)
|
||||
|
||||
//格式整理
|
||||
data, err := a.parseResponse(ctx, []byte(anaContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//组装数据
|
||||
resData := a.cateData(data)
|
||||
os.WriteFile("./cache/"+timeSte+"/extracted.json", pkg.JsonByteIgonErr(resData), 0644)
|
||||
return resData, err
|
||||
}
|
||||
|
||||
func (a *AdviceFileBiz) cateData(data map[string]mongo_model.AdviceData) map[mongo_model.AdviceRole]map[string]mongo_model.AdviceData {
|
||||
var res = make(map[mongo_model.AdviceRole]map[string]mongo_model.AdviceData)
|
||||
for k, v := range data {
|
||||
if _, ok := res[v.Role()]; !ok {
|
||||
res[v.Role()] = make(map[string]mongo_model.AdviceData)
|
||||
}
|
||||
res[v.Role()][k] = v
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (a *AdviceFileBiz) parseResponse(ctx context.Context, responseByte []byte) (resultOutPut map[string]mongo_model.AdviceData, err error) {
|
||||
//只尝试修复一次
|
||||
//if isValid := json.Valid(responseByte); !isValid {
|
||||
//
|
||||
// if err != nil {
|
||||
// return nil, fmt.Errorf("json格式错误,修复失败:%s", err.Error())
|
||||
// }
|
||||
//}
|
||||
if isValid := json.Valid(responseByte); !isValid {
|
||||
return nil, fmt.Errorf("json格式错误")
|
||||
}
|
||||
|
||||
var (
|
||||
result map[string]interface{}
|
||||
)
|
||||
|
||||
resultOutPut = make(map[string]mongo_model.AdviceData)
|
||||
if err = json.Unmarshal(responseByte, &result); err != nil {
|
||||
|
||||
return
|
||||
}
|
||||
for k, v := range result {
|
||||
if _, ok := DataMap[k]; !ok {
|
||||
return
|
||||
}
|
||||
var vbyte []byte
|
||||
if vbyte, err = json.Marshal(v); err != nil {
|
||||
return
|
||||
}
|
||||
newData := DataMap[k].Copy()
|
||||
|
||||
if err = json.Unmarshal(vbyte, newData); err != nil {
|
||||
return
|
||||
}
|
||||
resultOutPut[k] = newData
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//func (a *AdviceFileBiz) fixJson(ctx context.Context, json []byte) ([]byte, error) {
|
||||
// prompt := "你是一个专业的JSON修复专家。请帮我修复以下错误的JSON格式。\n\n要求:\n1. 保持原有数据的结构和内容不变\n2. 修复JSON语法错误\n3. 输出格式化的正确JSON\n4. 简要说明修复了哪些问题\n\n错误的JSON:\n" + string(json) + "\n\n请直接输出修复后的JSON。"
|
||||
// call, err := a.callLlm(ctx, prompt, jsonModel)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// return []byte(call), nil
|
||||
//}
|
||||
|
||||
func (a *AdviceFileBiz) callLlm(ctx context.Context, prompt string, modelInfo *dbmodel.AiAdviceModelSup) (string, error) {
|
||||
var message = make([]*model.ChatCompletionMessage, 1)
|
||||
message[0] = &model.ChatCompletionMessage{
|
||||
Role: model.ChatMessageRoleUser,
|
||||
Content: &model.ChatCompletionMessageContent{
|
||||
StringValue: volcengine.String(prompt),
|
||||
},
|
||||
}
|
||||
res, err := a.hsyq.Chat(ctx, modelInfo.Key, modelInfo.FileModel, message)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return *res.Choices[0].Message.Content.StringValue, nil
|
||||
}
|
||||
|
||||
func (a *AdviceFileBiz) callLlm2(ctx context.Context, prompt string, modelInfo *dbmodel.AiAdviceModelSup) (string, error) {
|
||||
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: prompt}},
|
||||
},
|
||||
},
|
||||
}
|
||||
res, err := a.hsyq.CreateResponse(ctx, modelInfo.Key, modelInfo.FileModel, message, "", false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return res.Output[0].GetOutputMessage().Content[0].GetText().GetText(), nil
|
||||
}
|
||||
|
||||
func (a *AdviceFileBiz) getAllExamples() map[string]mongo_model.AdviceData {
|
||||
return DataMap
|
||||
}
|
||||
|
||||
func (a *AdviceFileBiz) buildSimplePrompt(wordContent string, examples map[string]mongo_model.AdviceData) string {
|
||||
// 最简单的提示词模板
|
||||
template := `分析以下房地产销售对话,按指定格式提取信息:
|
||||
|
||||
对话内容:
|
||||
%s
|
||||
|
||||
请按照以下` + fmt.Sprintf("%d", len(examples)) + `个格式生成JSON数据,key为格式名称,value为对应值:
|
||||
|
||||
%s
|
||||
|
||||
输出要求:
|
||||
1. 所有内容必须严格基于提供的对话原文,不得编造(重要!)
|
||||
2. 每个结构体一个JSON对象
|
||||
3. 严格按照示例格式
|
||||
4. 将上述生成的` + fmt.Sprintf("%d", len(examples)) + `个JSON对象,json不需要有可读性,不要有特殊符号,比如"\n",用map[string]json来包裹所有json对象:{"SupportingFacilities":{...},"SignatureDialogues":[{...},{...}]}`
|
||||
// 构建格式部分
|
||||
var formats strings.Builder
|
||||
for name, example := range examples {
|
||||
formats.WriteString(fmt.Sprintf("=== %s (%s:%s)===\n示例:%s\n\n", name, mongo_model.RoleDesc[example.Role()], example.Desc(), example.Example()))
|
||||
}
|
||||
|
||||
return fmt.Sprintf(template, wordContent, formats.String())
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type AdviceProjectBiz struct {
|
||||
AdvicerProjectMongo *mongo_model.AdvicerProjectMongo
|
||||
adviceProjectImpl *impl.AdviceProjectImpl
|
||||
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl
|
||||
mongo *pkg.Mongo
|
||||
}
|
||||
|
||||
func NewAdviceProjectBiz(
|
||||
advicerProjectMongo *mongo_model.AdvicerProjectMongo,
|
||||
adviceProjectImpl *impl.AdviceProjectImpl,
|
||||
aiAdviceModelSupImpl *impl.AiAdviceModelSupImpl,
|
||||
mongo *pkg.Mongo,
|
||||
) *AdviceProjectBiz {
|
||||
return &AdviceProjectBiz{
|
||||
AdvicerProjectMongo: advicerProjectMongo,
|
||||
mongo: mongo,
|
||||
adviceProjectImpl: adviceProjectImpl,
|
||||
aiAdviceModelSupImpl: aiAdviceModelSupImpl,
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,
|
||||
ProjectInfo: param.ProjectInfo,
|
||||
RegionValue: param.RegionValue,
|
||||
CompetitionComparison: param.CompetitionComparison,
|
||||
CoreSellingPoints: param.CoreSellingPoints,
|
||||
SupportingFacilities: param.SupportingFacilities,
|
||||
DeveloperBacking: param.DeveloperBacking,
|
||||
LastUpdateTime: time.Now(),
|
||||
})
|
||||
|
||||
return res.InsertedID, err
|
||||
}
|
||||
|
||||
func (a *AdviceProjectBiz) Update(ctx context.Context, param *entitys.AdvicerrProjectUpdateReq) (err error) {
|
||||
filter := bson.M{}
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
if param.ProjectId != 0 {
|
||||
|
||||
filter["projectId"] = param.ProjectId
|
||||
}
|
||||
|
||||
update := bson.M{
|
||||
"$set": &mongo_model.AdvicerProjectMongo{
|
||||
ProjectId: param.ProjectId,
|
||||
ProjectInfo: param.ProjectInfo,
|
||||
RegionValue: param.RegionValue,
|
||||
CompetitionComparison: param.CompetitionComparison,
|
||||
CoreSellingPoints: param.CoreSellingPoints,
|
||||
SupportingFacilities: param.SupportingFacilities,
|
||||
DeveloperBacking: param.DeveloperBacking,
|
||||
LastUpdateTime: time.Now(),
|
||||
},
|
||||
}
|
||||
res := a.mongo.Co(a.AdvicerProjectMongo).FindOneAndUpdate(ctx, filter, update)
|
||||
return res.Err()
|
||||
}
|
||||
|
||||
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{}
|
||||
|
||||
if param.ProjectId != 0 {
|
||||
filter["projectId"] = param.ProjectId
|
||||
}
|
||||
|
||||
// 2. _id 条件
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
res := a.mongo.Co(a.AdvicerProjectMongo).FindOne(ctx, filter)
|
||||
if res.Err() != nil {
|
||||
return info, res.Err()
|
||||
}
|
||||
// 遍历结果
|
||||
|
||||
if err := res.Decode(&info); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
type AdviceSkillBiz struct {
|
||||
AdvicerTalkSkillMongo *mongo_model.AdvicerTalkSkillMongo
|
||||
mongo *pkg.Mongo
|
||||
}
|
||||
|
||||
func NewAdviceSkillBiz(
|
||||
advicerTalkSkillMongo *mongo_model.AdvicerTalkSkillMongo,
|
||||
mongo *pkg.Mongo,
|
||||
) *AdviceSkillBiz {
|
||||
return &AdviceSkillBiz{
|
||||
AdvicerTalkSkillMongo: advicerTalkSkillMongo,
|
||||
mongo: mongo,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdviceSkillBiz) VersionAdd(ctx context.Context, param *entitys.AdvicerTalkSkillAddReq) (id interface{}, err error) {
|
||||
|
||||
res, err := a.mongo.Co(a.AdvicerTalkSkillMongo).InsertOne(ctx, &mongo_model.AdvicerTalkSkillMongo{
|
||||
ProjectId: param.ProjectId,
|
||||
AdvicerId: param.AdvicerId,
|
||||
Desc: param.Desc,
|
||||
NeedsMining: param.NeedsMining,
|
||||
PainPointResponse: param.PainPointResponse,
|
||||
ValueBuilding: param.ValueBuilding,
|
||||
ClosingTechniques: param.ClosingTechniques,
|
||||
CommunicationRhythm: param.CommunicationRhythm,
|
||||
LastUpdateTime: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.InsertedID, err
|
||||
}
|
||||
|
||||
func (a *AdviceSkillBiz) VersionUpdate(ctx context.Context, param *entitys.AdvicerTalkSkillUpdateReq) (err error) {
|
||||
filter := bson.M{}
|
||||
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
|
||||
update := bson.M{
|
||||
"$set": &mongo_model.AdvicerTalkSkillMongo{
|
||||
AdvicerId: param.AdvicerId,
|
||||
ProjectId: param.ProjectId,
|
||||
Desc: param.Desc,
|
||||
NeedsMining: param.NeedsMining,
|
||||
PainPointResponse: param.PainPointResponse,
|
||||
ValueBuilding: param.ValueBuilding,
|
||||
ClosingTechniques: param.ClosingTechniques,
|
||||
CommunicationRhythm: param.CommunicationRhythm,
|
||||
LastUpdateTime: time.Now(),
|
||||
},
|
||||
}
|
||||
res := a.mongo.Co(a.AdvicerTalkSkillMongo).FindOneAndUpdate(ctx, filter, update)
|
||||
return res.Err()
|
||||
}
|
||||
|
||||
func (a *AdviceSkillBiz) VersionList(ctx context.Context, param *entitys.AdvicerTalkSkillListReq) (list []mongo_model.AdvicerTalkSkillMongo, err error) {
|
||||
filter := bson.M{}
|
||||
// 1. advicer_id 条件
|
||||
if param.AdvicerId != 0 {
|
||||
filter["AdvicerId"] = param.AdvicerId
|
||||
}
|
||||
|
||||
if param.ProjectId != 0 {
|
||||
filter["projectId"] = param.ProjectId
|
||||
}
|
||||
|
||||
// 2. _id 条件
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
// 3. version_desc 模糊查询
|
||||
if len(param.Desc) != 0 {
|
||||
// 正确的方式:指定字段名
|
||||
filter["desc"] = bson.M{
|
||||
"$regex": primitive.Regex{
|
||||
Pattern: param.Desc,
|
||||
Options: "i",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
cursor, err := a.mongo.Co(a.AdvicerTalkSkillMongo).Find(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 遍历结果
|
||||
for cursor.Next(ctx) {
|
||||
var advicerVersion mongo_model.AdvicerTalkSkillMongo
|
||||
if err := cursor.Decode(&advicerVersion); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, advicerVersion)
|
||||
}
|
||||
|
||||
if err := cursor.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (a *AdviceSkillBiz) VersionDel(ctx context.Context, param *entitys.AdvicerTalkSkillDelReq) (err error) {
|
||||
filter := bson.M{}
|
||||
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
|
||||
_, err = a.mongo.Co(a.AdvicerTalkSkillMongo).DeleteOne(ctx, filter)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *AdviceSkillBiz) Info(ctx context.Context, param *entitys.AdvicerTalkSkillInfoReq) (info mongo_model.AdvicerTalkSkillMongo, err error) {
|
||||
filter := bson.M{}
|
||||
|
||||
if len(param.Id) != 0 {
|
||||
objectID, err := primitive.ObjectIDFromHex(param.Id)
|
||||
if err != nil {
|
||||
return info, fmt.Errorf("ID转换失败: %w", err)
|
||||
}
|
||||
filter["_id"] = objectID
|
||||
}
|
||||
res := a.mongo.Co(a.AdvicerTalkSkillMongo).FindOne(ctx, filter)
|
||||
if res.Err() != nil {
|
||||
return info, err
|
||||
}
|
||||
// 遍历结果
|
||||
|
||||
if err = res.Decode(&info); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
return info, nil
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -8,11 +8,13 @@ import (
|
|||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
|
@ -21,9 +23,17 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/card"
|
||||
"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/redis/go-redis/v9"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
|
|
@ -40,12 +50,19 @@ type DingTalkBotBiz struct {
|
|||
botGroupQywxImpl *impl.BotGroupQywxImpl
|
||||
toolManager *tools.Manager
|
||||
chatHis *impl.BotChatHisImpl
|
||||
botUserImpl *impl.BotUserImpl
|
||||
conf *config.Config
|
||||
cardSend *dingtalk.SendCardClient
|
||||
qywxGroupHandle *qywx.Group
|
||||
groupConfigBiz *GroupConfigBiz
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
macro *do.Macro
|
||||
dingtalkOauth2Client *dingtalkPkg.Oauth2Client
|
||||
dingTalkOld *dingtalkPkg.OldClient
|
||||
dingtalkCardClient *dingtalkPkg.CardClient
|
||||
redisCli *redis.Client
|
||||
issueImpl *impl.IssueImpl
|
||||
sysImpl *impl.SysImpl
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
|
|
@ -54,14 +71,22 @@ func NewDingTalkBotBiz(
|
|||
handle *do.Handle,
|
||||
botConfigImpl *impl.BotConfigImpl,
|
||||
botGroupImpl *impl.BotGroupImpl,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
dingTalkUser *dingtalk.User,
|
||||
chatHis *impl.BotChatHisImpl,
|
||||
botUserImpl *impl.BotUserImpl,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
toolManager *tools.Manager,
|
||||
conf *config.Config,
|
||||
cardSend *dingtalk.SendCardClient,
|
||||
groupConfigBiz *GroupConfigBiz,
|
||||
macro *do.Macro,
|
||||
dingtalkOauth2Client *dingtalkPkg.Oauth2Client,
|
||||
dingTalkOld *dingtalkPkg.OldClient,
|
||||
dingtalkCardClient *dingtalkPkg.CardClient,
|
||||
rdb *utils.Rdb,
|
||||
issueImpl *impl.IssueImpl,
|
||||
sysImpl *impl.SysImpl,
|
||||
) *DingTalkBotBiz {
|
||||
return &DingTalkBotBiz{
|
||||
do: do,
|
||||
|
|
@ -71,12 +96,20 @@ func NewDingTalkBotBiz(
|
|||
dingTalkUser: dingTalkUser,
|
||||
groupConfigBiz: groupConfigBiz,
|
||||
botGroupImpl: botGroupImpl,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
toolManager: toolManager,
|
||||
chatHis: chatHis,
|
||||
botUserImpl: botUserImpl,
|
||||
conf: conf,
|
||||
cardSend: cardSend,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
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
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||||
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新")
|
||||
return
|
||||
//requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1))
|
||||
// handleSingleChat 单聊处理
|
||||
// 先不接意图识别-仅提供问题处理
|
||||
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) error {
|
||||
// 1. 获取用户信息
|
||||
user, err := d.botUserImpl.GetByStaffId(requireData.Req.SenderStaffId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requireData.ID = int32(user.UserID)
|
||||
requireData.UserInfo = &entitys.DingTalkUserInfo{
|
||||
UserId: int(user.UserID),
|
||||
StaffId: user.StaffID,
|
||||
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 {
|
||||
// return
|
||||
// if errors.Is(err, sql.ErrNoRows) {
|
||||
// entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。")
|
||||
// return nil
|
||||
// }
|
||||
//requireData.ID=requireData.UserInfo.UserID
|
||||
////如果不是管理或者不是老板,则进行权限判断
|
||||
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
|
||||
//
|
||||
// return err
|
||||
// }
|
||||
//return
|
||||
|
||||
// // 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) {
|
||||
|
|
@ -172,7 +480,7 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
|
|||
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) {
|
||||
|
|
@ -252,6 +560,9 @@ func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.
|
|||
}
|
||||
messages := make([]entitys.HisMessage, 0, len(his))
|
||||
for _, v := range his {
|
||||
if v.Role != "user" {
|
||||
continue
|
||||
}
|
||||
messages = append(messages, entitys.HisMessage{
|
||||
Role: constants.Caller(v.Role), // 用户角色
|
||||
Content: v.Content, // 用户输入内容
|
||||
|
|
@ -312,38 +623,6 @@ func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotG
|
|||
return
|
||||
}
|
||||
|
||||
// SendReports 发送多个报告
|
||||
func (d *DingTalkBotBiz) SendReports(ctx context.Context, groupInfo *model.AiBotGroup, reports []*bbxt.ReportRes) (err error) {
|
||||
if len(reports) == 0 {
|
||||
return errors.New("report is empty")
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("截止%s日报", time.Now().Format("1月2日15点"))
|
||||
reportChan := make(chan string, len(reports)*2)
|
||||
writeCount := 0
|
||||
for _, v := range reports {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
reportChan <- fmt.Sprintf("**%s**", v.Title)
|
||||
reportChan <- fmt.Sprintf("", v.Url)
|
||||
writeCount += 2
|
||||
}
|
||||
close(reportChan)
|
||||
if writeCount == 0 {
|
||||
return errors.New("report is empty")
|
||||
}
|
||||
err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||||
RobotCode: groupInfo.RobotCode,
|
||||
ConversationType: constants.ConversationTypeGroup,
|
||||
ConversationId: groupInfo.ConversationID,
|
||||
Text: chatbot.BotCallbackDataTextModel{
|
||||
Content: title,
|
||||
},
|
||||
}, reportChan)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroup, err error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
|
|
@ -443,3 +722,315 @@ func (d *DingTalkBotBiz) defaultPrompt() string {
|
|||
-parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。
|
||||
当前时间:` + 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,17 +8,17 @@ import (
|
|||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/domain/workflow/runtime"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/gateway"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/mapstructure"
|
||||
"ai_scheduler/internal/pkg/rec_extra"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/public"
|
||||
"bufio"
|
||||
errorsSpecial "errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
|
@ -31,12 +31,12 @@ import (
|
|||
|
||||
"github.com/coze-dev/coze-go"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/ollama/ollama/api"
|
||||
"gorm.io/gorm/utils"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
Ollama *llm_service.OllamaService
|
||||
Vllm *llm_service.VllmService
|
||||
toolManager *tools.Manager
|
||||
conf *config.Config
|
||||
sessionImpl *impl.SessionImpl
|
||||
|
|
@ -48,7 +48,6 @@ type Handle struct {
|
|||
|
||||
func NewHandle(
|
||||
Ollama *llm_service.OllamaService,
|
||||
Vllm *llm_service.VllmService,
|
||||
toolManager *tools.Manager,
|
||||
conf *config.Config,
|
||||
sessionImpl *impl.SessionImpl,
|
||||
|
|
@ -59,7 +58,6 @@ func NewHandle(
|
|||
) *Handle {
|
||||
return &Handle{
|
||||
Ollama: Ollama,
|
||||
Vllm: Vllm,
|
||||
toolManager: toolManager,
|
||||
conf: conf,
|
||||
sessionImpl: sessionImpl,
|
||||
|
|
@ -75,8 +73,7 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
|
|||
|
||||
prompt, err := promptProcessor.CreatePrompt(ctx, rec)
|
||||
//意图识别
|
||||
// recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{
|
||||
recognizeMsg, err := r.Vllm.IntentRecognize(ctx, &entitys.ToolSelect{
|
||||
recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{
|
||||
Prompt: prompt,
|
||||
Tools: rec.Tasks,
|
||||
})
|
||||
|
|
@ -92,9 +89,226 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
|
|||
}
|
||||
rec.Match = &match
|
||||
|
||||
// 意图输入到日志
|
||||
log.Infof("recognize: %s", recognizeMsg)
|
||||
|
||||
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) {
|
||||
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
|
||||
return
|
||||
|
|
@ -133,7 +347,7 @@ func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, rec *e
|
|||
case constants.TaskTypeApi:
|
||||
return r.handleApiTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeKnowle:
|
||||
return r.handleKnowle(ctx, rec, pointTask)
|
||||
return r.handleKnowleV2(ctx, rec, pointTask)
|
||||
case constants.TaskTypeFunc:
|
||||
return r.handleTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeBot:
|
||||
|
|
@ -170,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)
|
||||
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))
|
||||
// 获取租户ID 形式为 {biz-user} 比如 "zltx-platform"
|
||||
tenantID := ext.Sys.KnowlegeTenantKey
|
||||
|
||||
// 请求知识库工具
|
||||
knowledgeBase := knowledge_base.New(r.conf.KnowledgeConfig)
|
||||
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 {
|
||||
return
|
||||
} else if !has {
|
||||
return errors.SessionNotFound
|
||||
return fmt.Errorf("请求知识库工具失败,err: %v", err)
|
||||
}
|
||||
|
||||
// 找到知识库的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)
|
||||
// 读取知识库SSE数据
|
||||
err = r.readKnowledgeSSE(knowledgeResp, rec.Ch, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -252,6 +500,67 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
|
|||
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 工作流
|
||||
func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) {
|
||||
if task.Index == "bug_optimization_submit" {
|
||||
|
|
@ -337,7 +646,7 @@ func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessio
|
|||
|
||||
func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) {
|
||||
// 获取创建者uid 用户名 -> dingtalk uid
|
||||
creatorId, err := r.dingtalkContactClient.SearchUserOne(accessToken, userName)
|
||||
creatorId, err := r.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, userName)
|
||||
if err != nil {
|
||||
log.Warnf("search dingtalk user one failed: %v", err)
|
||||
return
|
||||
|
|
@ -510,7 +819,7 @@ func handleCozeWorkflowEvents(ctx context.Context, resp coze.Stream[coze.Workflo
|
|||
case coze.WorkflowEventTypeMessage:
|
||||
entitys.ResStream(ch, index, event.Message.Content)
|
||||
case coze.WorkflowEventTypeError:
|
||||
entitys.ResError(ch, index, fmt.Sprintf("工作流执行错误: %v", event.Error))
|
||||
entitys.ResError(ch, index, fmt.Sprintf("工作流执行错误: %s", event.Error))
|
||||
case coze.WorkflowEventTypeDone:
|
||||
entitys.ResEnd(ch, index, "工作流执行完成")
|
||||
case coze.WorkflowEventTypeInterrupt:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ import (
|
|||
|
||||
type Macro struct {
|
||||
botGroupImpl *impl.BotGroupImpl
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
config *config.Config
|
||||
rdb *utils.Rdb
|
||||
|
|
@ -37,14 +36,12 @@ func NewMacro(
|
|||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
config *config.Config,
|
||||
rdb *utils.Rdb,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
) *Macro {
|
||||
return &Macro{
|
||||
botGroupImpl: botGroupImpl,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
config: config,
|
||||
rdb: rdb,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,7 +193,7 @@ func (m *Macro) ProductModify(ctx context.Context, content string, groupConfig *
|
|||
groupConfig.ProductName = itemInfo
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"config_id": groupConfig.ConfigID})
|
||||
err = m.botGroupConfigImpl.UpdateByCond(&cond, groupConfig)
|
||||
err = m.botGroupImpl.UpdateByCond(&cond, groupConfig)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("修改失败:%v", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package do
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_report(t *testing.T) {
|
||||
con := "[利润同比报表]商品修改:官方–优酷周卡,官方–优酷月卡,官方–优酷季卡,官方–优酷年卡,,官方–爱奇艺-月卡,官方–爱奇艺-季卡,官方–爱奇艺-年卡,官方–芒果-PC周卡,官方–芒果-PC月卡,官方–芒果-PC季卡,官方–QQ音乐-绿钻月卡,官方–饿了么超级会员月卡,官方–网易云黑胶vip月卡,官方–喜马拉雅巅峰会员月卡,剪映会员7天卡,剪映会员月卡,剪映会员年卡,剪映SVIP会员7天卡,剪映SVIP会员月卡,剪映SVIP会员年卡"
|
||||
run()
|
||||
|
||||
chatId, err, i := ma.ProductModify(context.Background(), con, &model.AiBotGroupConfig{ConfigID: 1, ToolList: "8,9,10,11,12,13,16"})
|
||||
t.Log(chatId, err, i)
|
||||
}
|
||||
|
||||
var ma *Macro
|
||||
|
||||
func run() {
|
||||
configConfig, _ := config.LoadConfigWithTest()
|
||||
db, _ := utils.NewGormDb(configConfig)
|
||||
|
||||
rdb := utils.NewRdb(configConfig)
|
||||
reportDailyCacheImpl := impl.NewReportDailyCacheImpl(db)
|
||||
botGroupImpl := impl.NewBotGroupImpl(db)
|
||||
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
|
||||
ma = NewMacro(botGroupImpl, reportDailyCacheImpl, configConfig, rdb, botGroupConfigImpl)
|
||||
}
|
||||
|
|
@ -138,20 +138,24 @@ func (f *WithDingTalkBot) CreatePrompt(ctx context.Context, rec *entitys.Recogni
|
|||
mes = append(prompt, api.Message{
|
||||
Role: "system", // 系统角色
|
||||
Content: rec.SystemPrompt, // 系统提示内容
|
||||
// }, api.Message{ // 助手回复无需
|
||||
// Role: "assistant", // 助手角色
|
||||
// Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
|
||||
}, api.Message{
|
||||
Role: "assistant", // 助手角色
|
||||
Content: "### 聊天记录:" + pkg.JsonStringIgonErr(rec.ChatHis), // 助手回复内容
|
||||
Content: "用户历史输入:" + pkg.JsonStringIgonErr(rec.ChatHis), // 用户历史输入
|
||||
}, api.Message{
|
||||
Role: "user", // 用户角色
|
||||
Content: content.String(), // 用户输入内容
|
||||
})
|
||||
fmt.Printf("[意图识别]最终prompt:%v", mes)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) {
|
||||
var hasFile bool
|
||||
if rec.UserContent.File != nil && len(rec.UserContent.File) > 0 {
|
||||
if len(rec.UserContent.File) > 0 {
|
||||
hasFile = true
|
||||
}
|
||||
content.WriteString(rec.UserContent.Text)
|
||||
|
|
@ -165,11 +169,10 @@ func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recog
|
|||
content.WriteString(rec.UserContent.Tag)
|
||||
}
|
||||
|
||||
if len(rec.ChatHis.Messages) > 0 {
|
||||
content.WriteString("\n")
|
||||
content.WriteString("### 引用历史聊天记录:\n")
|
||||
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
}
|
||||
// if len(rec.ChatHis.Messages) > 0 {
|
||||
// content.WriteString("### 引用历史聊天记录:\n")
|
||||
// content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
// }
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@ import (
|
|||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/domain/tools/common/knowledge_base"
|
||||
"ai_scheduler/internal/domain/workflow/recharge"
|
||||
"ai_scheduler/internal/domain/workflow/runtime"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/utils"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -26,7 +30,11 @@ import (
|
|||
"strings"
|
||||
"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/duke-git/lancet/v2/slice"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
|
@ -35,12 +43,14 @@ import (
|
|||
type GroupConfigBiz struct {
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
botConfigImpl *impl.BotConfigImpl
|
||||
ossClient *utils_oss.Client
|
||||
workflowManager *runtime.Registry
|
||||
botTools []model.AiBotTool
|
||||
toolManager *tools.Manager
|
||||
conf *config.Config
|
||||
rdb *utils.Rdb
|
||||
dingtalkCardClient *dingtalk.CardClient
|
||||
macro *do.Macro
|
||||
handle *do.Handle
|
||||
}
|
||||
|
|
@ -50,24 +60,28 @@ func NewGroupConfigBiz(
|
|||
tools *tools_regis.ToolRegis,
|
||||
ossClient *utils_oss.Client,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
botConfigImpl *impl.BotConfigImpl,
|
||||
workflowManager *runtime.Registry,
|
||||
conf *config.Config,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
rdb *utils.Rdb,
|
||||
macro *do.Macro,
|
||||
toolManager *tools.Manager,
|
||||
dingtalkCardClient *dingtalk.CardClient,
|
||||
macro *do.Macro,
|
||||
handle *do.Handle,
|
||||
) *GroupConfigBiz {
|
||||
return &GroupConfigBiz{
|
||||
botTools: tools.BootTools,
|
||||
ossClient: ossClient,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
botConfigImpl: botConfigImpl,
|
||||
workflowManager: workflowManager,
|
||||
conf: conf,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
rdb: rdb,
|
||||
macro: macro,
|
||||
toolManager: toolManager,
|
||||
dingtalkCardClient: dingtalkCardClient,
|
||||
macro: macro,
|
||||
handle: handle,
|
||||
}
|
||||
}
|
||||
|
|
@ -98,12 +112,14 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model.
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
//追加电商充值系统统计 - 返回统一使用[]*bbxt.ReportRes
|
||||
rechargeReports, err := g.rechargeDailyReport(ctx, time.Now(), nil, g.ossClient)
|
||||
if err != nil || len(rechargeReports) == 0 {
|
||||
return
|
||||
}
|
||||
reports = append(rechargeReports, reports...)
|
||||
|
||||
reports = append(reports, rechargeReports...)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -173,7 +189,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, repo)
|
||||
reports = append(reports, repo...)
|
||||
case "report_sales_analysis":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := rep.GetStatisOfficialProductSum(t, product)
|
||||
|
|
@ -198,9 +214,8 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, rechargeReport...)
|
||||
reports = append(reports, repo...)
|
||||
|
||||
reports = append(reports, rechargeReport...)
|
||||
case "report_daily_recharge":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := g.rechargeDailyReport(ctx, t, product, nil)
|
||||
|
|
@ -234,7 +249,7 @@ func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recogniz
|
|||
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 len(rec.Match.Chat) != 0 {
|
||||
|
|
@ -268,6 +283,9 @@ func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize
|
|||
return g.handleReport(ctx, rec, pointTask, groupConfig)
|
||||
case constants.TaskTypeCozeWorkflow:
|
||||
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
||||
case constants.TaskTypeKnowle: // 知识库lightRAG版本
|
||||
_, err = g.handleKnowledge(ctx, rec, groupConfig, callback)
|
||||
return err
|
||||
default:
|
||||
return g.otherTask(ctx, rec)
|
||||
}
|
||||
|
|
@ -425,3 +443,215 @@ func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize)
|
|||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import (
|
|||
)
|
||||
|
||||
const DefaultInterval = 100 * time.Millisecond
|
||||
const HeardBeatX = 100
|
||||
const HeardBeatX = 1000
|
||||
|
||||
type SendCardClient struct {
|
||||
Auth *Auth
|
||||
|
|
@ -115,7 +115,6 @@ func (s *SendCardClient) NewCard(ctx context.Context, cardSend *CardSend) error
|
|||
s.processContentChannel(ctx, cardSend, cardInstanceId.String(), client)
|
||||
}()
|
||||
wg.Wait()
|
||||
log.Info("处理通道结束")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -164,7 +163,7 @@ func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *Ca
|
|||
|
||||
var (
|
||||
contentBuilder strings.Builder
|
||||
lastUpdate = time.Now()
|
||||
lastUpdate time.Time
|
||||
)
|
||||
for {
|
||||
|
||||
|
|
@ -174,7 +173,6 @@ func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *Ca
|
|||
// 通道关闭,发送最终内容
|
||||
if contentBuilder.Len() > 0 {
|
||||
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
|
||||
log.Info("contentBuilder.Len()修改失败1")
|
||||
s.logger.Errorf("更新卡片失败1:%s", err.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -183,7 +181,6 @@ func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *Ca
|
|||
contentBuilder.WriteString(content)
|
||||
if contentBuilder.Len() > 0 {
|
||||
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
|
||||
log.Info("contentBuilder.Len()修改失败2")
|
||||
s.logger.Errorf("更新卡片失败2:%s", err.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -191,12 +188,10 @@ func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *Ca
|
|||
|
||||
case <-heartbeatTicker.C:
|
||||
if time.Now().Unix()-lastUpdate.Unix() >= HeardBeatX {
|
||||
log.Infof("心跳超时,当前时间:%d,最后时间:%d", time.Now().Unix(), lastUpdate.Unix())
|
||||
return
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
log.Info("send_card上下文失效")
|
||||
s.logger.Info("context canceled, stop channel processing")
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,27 @@ func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSe
|
|||
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) {
|
||||
// if imgByte == nil {
|
||||
// return
|
||||
|
|
|
|||
|
|
@ -0,0 +1,142 @@
|
|||
package third_party
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"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/responses"
|
||||
"github.com/volcengine/volcengine-go-sdk/volcengine"
|
||||
)
|
||||
|
||||
type Hsyq struct {
|
||||
mapClient map[string]*arkruntime.Client
|
||||
}
|
||||
|
||||
func NewHsyq() *Hsyq {
|
||||
return &Hsyq{
|
||||
mapClient: make(map[string]*arkruntime.Client),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hsyq) getClient(key string) *arkruntime.Client {
|
||||
var client *arkruntime.Client
|
||||
if _, ok := h.mapClient[key]; ok {
|
||||
client = h.mapClient[key]
|
||||
} else {
|
||||
client = arkruntime.NewClientWithApiKey(
|
||||
key,
|
||||
arkruntime.WithRegion("cn-beijing"),
|
||||
arkruntime.WithTimeout(2*time.Minute),
|
||||
arkruntime.WithRetryTimes(2),
|
||||
)
|
||||
h.mapClient[key] = client
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// 火山引擎
|
||||
func (h *Hsyq) Chat(ctx context.Context, key string, modelName string, prompt []*model.ChatCompletionMessage) (model.ChatCompletionResponse, error) {
|
||||
req := model.CreateChatCompletionRequest{
|
||||
Model: modelName,
|
||||
Messages: prompt,
|
||||
Stream: new(bool),
|
||||
Thinking: &model.Thinking{Type: model.ThinkingTypeDisabled},
|
||||
}
|
||||
|
||||
resp, err := h.getClient(key).CreateChatCompletion(ctx, req)
|
||||
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) 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),
|
||||
Reasoning: &responses.ResponsesReasoning{Effort: responses.ReasoningEffort_minimal},
|
||||
Thinking: &responses.ResponsesThinking{Type: responses.ThinkingType_disabled.Enum()},
|
||||
Text: &responses.ResponsesText{Format: &responses.TextFormat{Type: responses.TextType_json_object}},
|
||||
}
|
||||
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) {
|
||||
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()},
|
||||
Text: &responses.ResponsesText{Format: &responses.TextFormat{Type: responses.TextType_json_object}},
|
||||
}
|
||||
|
||||
resp, err := h.getClient(key).CreateResponses(ctx, &req)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
log.Info("token用量:", resp.Usage.TotalTokens)
|
||||
return resp, err
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
package llm_service
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
type VllmService struct {
|
||||
client *utils_vllm.Client
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewVllmService(
|
||||
client *utils_vllm.Client,
|
||||
config *config.Config,
|
||||
) *VllmService {
|
||||
return &VllmService{
|
||||
client: client,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *VllmService) IntentRecognize(ctx context.Context, req *entitys.ToolSelect) (msg string, err error) {
|
||||
msgs := s.convertMessages(req.Prompt)
|
||||
tools := s.convertTools(req.Tools)
|
||||
|
||||
resp, err := s.client.ToolSelect(ctx, msgs, tools)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Content == "" {
|
||||
if len(resp.ToolCalls) > 0 {
|
||||
call := resp.ToolCalls[0]
|
||||
var matchFromTools = &entitys.Match{
|
||||
Confidence: 1,
|
||||
Index: call.Function.Name,
|
||||
Parameters: call.Function.Arguments,
|
||||
IsMatch: true,
|
||||
}
|
||||
msg = pkg.JsonStringIgonErr(matchFromTools)
|
||||
} else {
|
||||
err = errors.New("不太明白你想表达的意思呢,可以在仔细描述一下您所需要的内容吗,感谢感谢")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
msg = resp.Content
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *VllmService) convertMessages(prompts []api.Message) []*schema.Message {
|
||||
msgs := make([]*schema.Message, 0, len(prompts))
|
||||
for _, p := range prompts {
|
||||
msg := &schema.Message{
|
||||
Role: schema.RoleType(p.Role),
|
||||
Content: p.Content,
|
||||
}
|
||||
|
||||
// 这里实际应该不会走进来
|
||||
if len(p.Images) > 0 {
|
||||
parts := []schema.MessageInputPart{
|
||||
{Type: schema.ChatMessagePartTypeText, Text: p.Content},
|
||||
}
|
||||
for _, imgData := range p.Images {
|
||||
b64 := base64.StdEncoding.EncodeToString(imgData)
|
||||
mimeType := "image/jpeg"
|
||||
parts = append(parts, schema.MessageInputPart{
|
||||
Type: schema.ChatMessagePartTypeImageURL,
|
||||
Image: &schema.MessageInputImage{
|
||||
MessagePartCommon: schema.MessagePartCommon{
|
||||
MIMEType: mimeType,
|
||||
Base64Data: &b64,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
msg.UserInputMultiContent = parts
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func (s *VllmService) convertTools(tasks []entitys.RegistrationTask) []*schema.ToolInfo {
|
||||
tools := make([]*schema.ToolInfo, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
params := make(map[string]*schema.ParameterInfo)
|
||||
for k, v := range task.TaskConfigDetail.Param.Properties {
|
||||
dt := schema.String
|
||||
|
||||
// Handle v.Type dynamically to support both string and []string (compiler suggests []string)
|
||||
// Using fmt.Sprint handles both cases safely without knowing exact type structure
|
||||
typeStr := fmt.Sprintf("%v", v.Type)
|
||||
typeStr = strings.Trim(typeStr, "[]") // normalize "[string]" -> "string"
|
||||
|
||||
switch typeStr {
|
||||
case "string":
|
||||
dt = schema.String
|
||||
case "integer", "int":
|
||||
dt = schema.Integer
|
||||
case "number", "float":
|
||||
dt = schema.Number
|
||||
case "boolean", "bool":
|
||||
dt = schema.Boolean
|
||||
case "object":
|
||||
dt = schema.Object
|
||||
case "array":
|
||||
dt = schema.Array
|
||||
}
|
||||
|
||||
required := false
|
||||
for _, r := range task.TaskConfigDetail.Param.Required {
|
||||
if r == k {
|
||||
required = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
desc := v.Description
|
||||
if len(v.Enum) > 0 {
|
||||
var enumStrs []string
|
||||
for _, e := range v.Enum {
|
||||
enumStrs = append(enumStrs, fmt.Sprintf("%v", e))
|
||||
}
|
||||
desc += " Enum: " + strings.Join(enumStrs, ", ")
|
||||
}
|
||||
|
||||
params[k] = &schema.ParameterInfo{
|
||||
Type: dt,
|
||||
Desc: desc,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
tools = append(tools, &schema.ToolInfo{
|
||||
Name: task.Name,
|
||||
Desc: task.Desc,
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(params),
|
||||
})
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package biz
|
|||
import (
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/biz/llm_service"
|
||||
"ai_scheduler/internal/biz/support"
|
||||
"ai_scheduler/internal/biz/llm_service/third_party"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
|
@ -14,7 +14,6 @@ var ProviderSetBiz = wire.NewSet(
|
|||
NewChatHistoryBiz,
|
||||
//llm_service.NewLangChainGenerate,
|
||||
llm_service.NewOllamaGenerate,
|
||||
llm_service.NewVllmService,
|
||||
//handle.NewHandle,
|
||||
do.NewDo,
|
||||
do.NewHandle,
|
||||
|
|
@ -23,5 +22,12 @@ var ProviderSetBiz = wire.NewSet(
|
|||
NewQywxAppBiz,
|
||||
NewGroupConfigBiz,
|
||||
do.NewMacro,
|
||||
support.NewHytAddressIngester,
|
||||
NewCallbackBiz,
|
||||
NewAdviceFileBiz,
|
||||
third_party.NewHsyq,
|
||||
NewAdviceAdvicerBiz,
|
||||
NewAdviceSkillBiz,
|
||||
NewAdviceProjectBiz,
|
||||
NewAdviceClientBiz,
|
||||
NewAdviceChatBiz,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ import (
|
|||
"ai_scheduler/internal/domain/repo"
|
||||
"ai_scheduler/internal/domain/workflow"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"testing"
|
||||
|
|
@ -53,6 +55,11 @@ func run() {
|
|||
|
||||
registry := workflow.NewRegistry(configConfig, client, repos, components)
|
||||
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
|
||||
botConfigImpl := impl.NewBotConfigImpl(db)
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
package support
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// HytAddressIngester 货易通地址提取实现
|
||||
type HytAddressIngester struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewHytAddressIngester(cfg *config.Config) *HytAddressIngester {
|
||||
return &HytAddressIngester{cfg: cfg}
|
||||
}
|
||||
|
||||
// Auth 鉴权逻辑
|
||||
func (s *HytAddressIngester) Auth(c *fiber.Ctx) error {
|
||||
// 读取头
|
||||
token := strings.TrimSpace(c.Get("X-Source-Key"))
|
||||
ts := strings.TrimSpace(c.Get("X-Timestamp"))
|
||||
|
||||
// 时间窗口校验
|
||||
if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) {
|
||||
return errorcode.AuthNotFound
|
||||
}
|
||||
// token校验
|
||||
if token == "" || token != constants.TokenAddressIngestHyt {
|
||||
return errorcode.KeyNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ingest 执行提取逻辑
|
||||
func (s *HytAddressIngester) Ingest(ctx context.Context, text string) (*AddressIngestResp, error) {
|
||||
// 模型调用
|
||||
client, cleanup, err := utils_vllm.NewClient(s.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
res, err := client.Chat(ctx, []*schema.Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: constants.SystemPromptAddressIngestHyt,
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: text,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析模型返回结果
|
||||
var addr AddressIngestResp
|
||||
// 尝试直接解析
|
||||
if err := json.Unmarshal([]byte(res.Content), &addr); err != nil {
|
||||
// 修复json字符串
|
||||
repairedContent, err := util.JSONRepair(res.Content)
|
||||
if err != nil {
|
||||
return nil, errorcode.ParamErrf("invalid response body: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(repairedContent), &addr); err != nil {
|
||||
return nil, errorcode.ParamErrf("invalid response body: %v", err)
|
||||
}
|
||||
}
|
||||
return &addr, nil
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package support
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// AddressIngestResp 通用地址提取响应
|
||||
type AddressIngestResp struct {
|
||||
Recipient string `json:"recipient"` // 收货人
|
||||
Phone string `json:"phone"` // 联系电话
|
||||
Address string `json:"address"` // 收货地址
|
||||
}
|
||||
|
||||
// AddressIngester 地址提取接口
|
||||
type AddressIngester interface {
|
||||
// Auth 鉴权逻辑
|
||||
Auth(c *fiber.Ctx) error
|
||||
// Ingest 执行提取逻辑
|
||||
Ingest(ctx context.Context, text string) (*AddressIngestResp, error)
|
||||
}
|
||||
|
|
@ -21,9 +21,11 @@ type Config struct {
|
|||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Redis Redis `mapstructure:"redis"`
|
||||
DB DB `mapstructure:"db"`
|
||||
Mongo Mongo `mapstructure:"mongo"`
|
||||
Oss Oss `mapstructure:"oss"`
|
||||
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
||||
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
||||
KnowledgeConfig KnowledgeConfig `mapstructure:"knowledge_config"`
|
||||
LLM LLM `mapstructure:"llm"`
|
||||
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
||||
Qywx QywxConfig `mapstructure:"qywx"`
|
||||
|
|
@ -75,6 +77,8 @@ type DingtalkConfig struct {
|
|||
ApiSecret string `mapstructure:"api_secret"`
|
||||
TableDemand AITableConfig `mapstructure:"table_demand"`
|
||||
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组
|
||||
Card CardConfig `mapstructure:"card"` // 互动卡片
|
||||
SceneGroup SceneGroupConfig `mapstructure:"scene_group"` // 场景群
|
||||
}
|
||||
|
||||
// QywxConfig 企业微信配置
|
||||
|
|
@ -96,6 +100,34 @@ type AITableConfig struct {
|
|||
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 系统配置
|
||||
type SysConfig struct {
|
||||
SessionLen int `mapstructure:"session_len"`
|
||||
|
|
@ -122,13 +154,8 @@ type OllamaConfig struct {
|
|||
}
|
||||
|
||||
type VllmConfig struct {
|
||||
VLModel VllmModel `mapstructure:"vl_model"`
|
||||
TextModel VllmModel `mapstructure:"text_model"`
|
||||
}
|
||||
|
||||
type VllmModel struct {
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
Model string `mapstructure:"model"`
|
||||
VlModel string `mapstructure:"vl_model"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
Level string `mapstructure:"level"`
|
||||
}
|
||||
|
|
@ -163,6 +190,7 @@ type Redis struct {
|
|||
|
||||
type DB struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
|
||||
Source string `mapstructure:"source"`
|
||||
MaxIdle int32 `mapstructure:"maxIdle"`
|
||||
MaxOpen int32 `mapstructure:"maxOpen"`
|
||||
|
|
@ -170,6 +198,16 @@ type DB struct {
|
|||
IsDebug bool `mapstructure:"isDebug"`
|
||||
}
|
||||
|
||||
type Mongo struct {
|
||||
Source string `mapstructure:"source"`
|
||||
DataBase string `mapstructure:"dataBase"`
|
||||
MaxPoolSize uint64 `mapstructure:"maxPoolSize"`
|
||||
MinPoolSize uint64 `mapstructure:"minPoolSize"`
|
||||
MaxConnIdleTime int32 `mapstructure:"maxConnIdleTime"`
|
||||
ConnectTimeout int32 `mapstructure:"connectTimeout"`
|
||||
SocketTimeout int32 `mapstructure:"socketTimeout"`
|
||||
}
|
||||
|
||||
// Oss 阿里云OSS配置
|
||||
type Oss struct {
|
||||
AccessKey string `mapstructure:"access_key"`
|
||||
|
|
@ -258,6 +296,20 @@ type PermissionConfig struct {
|
|||
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 加载配置
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
|
|
@ -314,7 +366,7 @@ func LoadConfigWithEnv() (*Config, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viper.SetConfigFile(modularDir + "/config/config.yaml")
|
||||
viper.SetConfigFile(modularDir + "/config/config_test.yaml")
|
||||
viper.SetConfigType("yaml")
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
|
|
|
|||
|
|
@ -45,3 +45,11 @@ const (
|
|||
PermissionTypeNone = 1
|
||||
PermissionTypeDept = 2
|
||||
)
|
||||
|
||||
// IssueType 问题类型
|
||||
const (
|
||||
IssueTypeKnowledgeQA = "knowledge_qa" // 知识问答
|
||||
IssueTypeUI = "ui" // UI需求
|
||||
IssueTypeBug = "bug" // Bug
|
||||
IssueTypeDemand = "demand" // 开发需求
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
package constants
|
||||
|
||||
import "net/url"
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
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
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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`字段内容需符合对话语境"
|
||||
|
|
@ -20,3 +20,25 @@ func GetKnowledgeId(caller Caller) KnowledgeId {
|
|||
}
|
||||
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" // 知识库未命中
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
package constants
|
||||
|
||||
// Token
|
||||
const (
|
||||
TokenAddressIngestHyt = "E632C7D3E60771B03264F2337CCFA014" // md5("address_ingest_hyt")
|
||||
)
|
||||
|
||||
// 系统提示词
|
||||
const (
|
||||
SystemPromptAddressIngestHyt = `# 你是一个地址信息结构化解析器。
|
||||
你的任务是从用户提供的非结构化文本中,准确抽取并区分以下字段:
|
||||
|
||||
1. 收货人 recipient (真实姓名或带掩码姓名,如“张三”)
|
||||
2. 联系电话 phone (中国大陆手机号,11位数字)
|
||||
3. 收货地址 address
|
||||
|
||||
解析规则:
|
||||
- 电话号码只提取最可能的一个
|
||||
- 不要编造不存在的信息
|
||||
|
||||
输出示例:
|
||||
{\"recipient\": \"张三\",\"phone\": \"13458968095\",\"address\": \"四川省成都市武侯区天府三街88号\"}
|
||||
|
||||
输出格式必须为严格 JSON,不要输出任何解释性文字。`
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type AdviceAdvicerImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewAdviceAdvicerImpl(db *utils.Db) *AdviceAdvicerImpl {
|
||||
return &AdviceAdvicerImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceAdvicer)),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type AdviceAdvicerVersionImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewAdviceAdvicerVersionImpl(db *utils.Db) *AdviceAdvicerVersionImpl {
|
||||
return &AdviceAdvicerVersionImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceAdvicerVersion)),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type AdviceClientImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewAdviceClientImpl(db *utils.Db) *AdviceClientImpl {
|
||||
return &AdviceClientImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceClient)),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type AdviceProjectImpl struct {
|
||||
dataTemp.DataTemp
|
||||
BaseRepository[model.AiTask]
|
||||
}
|
||||
|
||||
func NewAdviceProjectImpl(db *utils.Db) *AdviceProjectImpl {
|
||||
return &AdviceProjectImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceProject)),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type AdviceTalkImpl struct {
|
||||
dataTemp.DataTemp
|
||||
BaseRepository[model.AiTask]
|
||||
}
|
||||
|
||||
func NewAdviceTalkImpl(db *utils.Db) *AdviceTalkImpl {
|
||||
return &AdviceTalkImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiAdviceTalk)),
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@ BaseModel 是一个泛型结构体,用于封装GORM数据库通用操作。
|
|||
// 定义受支持的PO类型集合(可根据需要扩展), 只有包含表结构才能使用BaseModel,避免使用出现问题
|
||||
type PO interface {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@ package impl
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"encoding/json"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type BotConfigImpl struct {
|
||||
|
|
@ -15,3 +20,33 @@ func NewBotConfigImpl(db *utils.Db) *BotConfigImpl {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
|
|||
|
||||
func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
|
||||
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 {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"database/sql"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type BotUserImpl struct {
|
||||
|
|
@ -25,3 +27,14 @@ func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,4 +18,12 @@ var ProviderImpl = wire.NewSet(
|
|||
NewBotGroupConfigImpl,
|
||||
NewBotGroupQywxImpl,
|
||||
NewReportDailyCacheImpl,
|
||||
NewIssueImpl,
|
||||
NewAdviceAdvicerImpl,
|
||||
NewAdviceProjectImpl,
|
||||
NewAdviceTalkImpl,
|
||||
NewAdviceAdvicerVersionImpl,
|
||||
NewAdviceClientImpl,
|
||||
NewAiAdviceSessionImpl,
|
||||
NewAiAdviceModelSupImpl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 TableNameAiAdviceAdvicer = "ai_advice_advicer"
|
||||
|
||||
// AiAdviceAdvicer mapped from table <ai_advice_advicer>
|
||||
type AiAdviceAdvicer struct {
|
||||
AdvicerID int32 `gorm:"column:advicer_id;primaryKey;autoIncrement:true" json:"advicer_id"`
|
||||
ProjectID int32 `gorm:"column:project_id;not null" json:"project_id"`
|
||||
Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名
|
||||
Birth time.Time `gorm:"column:birth;not null;comment:用户名称" json:"birth"` // 用户名称
|
||||
Gender int32 `gorm:"column:gender;not null;comment:1:男,2:女" json:"gender"` // 1:男,2:女
|
||||
WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiAdviceAdvicer's table name
|
||||
func (*AiAdviceAdvicer) TableName() string {
|
||||
return TableNameAiAdviceAdvicer
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
// 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 (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AiAdviceAdvicerEntity struct {
|
||||
Name string `json:"name"` // 姓名
|
||||
Birth string `json:"birth"` // 用户名称
|
||||
Gender string `json:"gender"` // 1:男,2:女
|
||||
WorkingYears string `json:"working_years"` // 工作年限
|
||||
}
|
||||
|
||||
func (a *AiAdviceAdvicer) Entity() *AiAdviceAdvicerEntity {
|
||||
var (
|
||||
gender string
|
||||
)
|
||||
switch a.Gender {
|
||||
case 1:
|
||||
gender = "男"
|
||||
case 2:
|
||||
gender = "女"
|
||||
default:
|
||||
gender = "未知"
|
||||
}
|
||||
return &AiAdviceAdvicerEntity{
|
||||
Name: a.Name,
|
||||
Birth: a.Birth.Format(time.DateOnly),
|
||||
Gender: gender,
|
||||
WorkingYears: fmt.Sprintf("%d年", a.WorkingYears),
|
||||
}
|
||||
}
|
||||
|
|
@ -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 TableNameAiAdviceAdvicerVersion = "ai_advice_advicer_version"
|
||||
|
||||
// AiAdviceAdvicerVersion mapped from table <ai_advice_advicer_version>
|
||||
type AiAdviceAdvicerVersion struct {
|
||||
VersionID int32 `gorm:"column:version_id;primaryKey;autoIncrement:true" json:"version_id"`
|
||||
AdvicerID int32 `gorm:"column:advicer_id;not null" json:"advicer_id"`
|
||||
VersionDesc string `gorm:"column:version_desc;not null;comment:版本名称" json:"version_desc"` // 版本名称
|
||||
DialectFeatures string `gorm:"column:dialect_features;not null;comment:语言风格" json:"dialect_features"` // 语言风格
|
||||
SentencePatterns string `gorm:"column:sentence_patterns;comment:句子模式" json:"sentence_patterns"` // 句子模式
|
||||
ToneTags string `gorm:"column:tone_tags;comment:语气标签" json:"tone_tags"` // 语气标签
|
||||
PersonalityTags string `gorm:"column:personality_tags;not null;comment:个性标签" json:"personality_tags"` // 个性标签
|
||||
SignatureDialogues string `gorm:"column:signature_dialogues;comment:代表性对话示例" json:"signature_dialogues"` // 代表性对话示例
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiAdviceAdvicerVersion's table name
|
||||
func (*AiAdviceAdvicerVersion) TableName() string {
|
||||
return TableNameAiAdviceAdvicerVersion
|
||||
}
|
||||
|
|
@ -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 TableNameAiAdviceClient = "ai_advice_client"
|
||||
|
||||
// AiAdviceClient mapped from table <ai_advice_client>
|
||||
type AiAdviceClient struct {
|
||||
ClientID int32 `gorm:"column:client_id;primaryKey;autoIncrement:true" json:"client_id"`
|
||||
PersonalInfo string `gorm:"column:personal_info;comment:区域价值话术库" json:"personal_info"` // 区域价值话术库
|
||||
PurchasePurpose string `gorm:"column:purchase_purpose;comment:竞品对比话术" json:"purchase_purpose"` // 竞品对比话术
|
||||
CoreDemands string `gorm:"column:core_demands;comment:项目核心卖点" json:"core_demands"` // 项目核心卖点
|
||||
Concerns string `gorm:"column:concerns;comment:配套体系" json:"concerns"` // 配套体系
|
||||
DecisionProfile string `gorm:"column:decision_profile;comment:开发商背书" json:"decision_profile"` // 开发商背书
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiAdviceClient's table name
|
||||
func (*AiAdviceClient) TableName() string {
|
||||
return TableNameAiAdviceClient
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 TableNameAiAdviceProject = "ai_advice_project"
|
||||
|
||||
// AiAdviceProject mapped from table <ai_advice_project>
|
||||
type AiAdviceProject struct {
|
||||
ProjectID int32 `gorm:"column:project_id;primaryKey;autoIncrement:true" json:"project_id"`
|
||||
Name string `gorm:"column:name;not null;comment:姓名" json:"name"` // 姓名
|
||||
ModelSupID int32 `gorm:"column:model_sup_id;not null;comment:模型提供方配置,关联advicer_model_sup" json:"model_sup_id"` // 模型提供方配置,关联advicer_model_sup
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiAdviceProject's table name
|
||||
func (*AiAdviceProject) TableName() string {
|
||||
return TableNameAiAdviceProject
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 TableNameAiAdviceTalk = "ai_advice_talk"
|
||||
|
||||
// AiAdviceTalk mapped from table <ai_advice_talk>
|
||||
type AiAdviceTalk struct {
|
||||
TalkID int32 `gorm:"column:talk_id;primaryKey;autoIncrement:true" json:"talk_id"`
|
||||
NeedsMining string `gorm:"column:needs_mining;comment:需求挖掘话术" json:"needs_mining"` // 需求挖掘话术
|
||||
PainPointResponse string `gorm:"column:pain_point_response;comment:痛点应对策略" json:"pain_point_response"` // 痛点应对策略
|
||||
ValueBuilding string `gorm:"column:value_building;comment:价值塑造技巧" json:"value_building"` // 价值塑造技巧
|
||||
ClosingTechniques string `gorm:"column:closing_techniques;comment:促单话术" json:"closing_techniques"` // 促单话术
|
||||
CommunicationRhythm string `gorm:"column:communication_rhythm;comment:沟通节奏控制" json:"communication_rhythm"` // 沟通节奏控制
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiAdviceTalk's table name
|
||||
func (*AiAdviceTalk) TableName() string {
|
||||
return TableNameAiAdviceTalk
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ type AiBotGroupConfig struct {
|
|||
ConfigID int32 `gorm:"column:config_id;primaryKey;autoIncrement:true" json:"config_id"`
|
||||
ToolList string `gorm:"column:tool_list;not null" json:"tool_list"`
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"`
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package mongo_model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdvicerClientMongo struct {
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
PersonalInfo PersonalInfo `json:"personalInfo" bson:"personalInfo"`
|
||||
PurchasePurpose PurchasePurpose `json:"purchasePurpose" bson:"purchasePurpose"`
|
||||
CoreDemands CoreDemands `json:"coreDemands" bson:"coreDemands"`
|
||||
Concerns []string `json:"concerns" bson:"concerns"`
|
||||
DecisionProfile []string `json:"decisionProfile" bson:"decisionProfile"`
|
||||
LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"`
|
||||
}
|
||||
|
||||
func NewAdvicerClientMongo() *AdvicerClientMongo {
|
||||
return &AdvicerClientMongo{}
|
||||
}
|
||||
|
||||
func (a *AdvicerClientMongo) MongoTableName() string {
|
||||
return "advicer_client"
|
||||
}
|
||||
|
||||
type AdvicerClientMongoEntity struct {
|
||||
PersonalInfo PersonalInfo `json:"personalInfo"`
|
||||
PurchasePurpose PurchasePurpose `json:"purchasePurpose"`
|
||||
CoreDemands CoreDemands `json:"coreDemands"`
|
||||
Concerns []string `json:"concerns"`
|
||||
DecisionProfile []string `json:"decisionProfile"`
|
||||
}
|
||||
|
||||
func (a *AdvicerClientMongo) Entity() *AdvicerClientMongoEntity {
|
||||
return &AdvicerClientMongoEntity{
|
||||
PersonalInfo: a.PersonalInfo,
|
||||
PurchasePurpose: a.PurchasePurpose,
|
||||
CoreDemands: a.CoreDemands,
|
||||
Concerns: a.Concerns,
|
||||
DecisionProfile: a.DecisionProfile,
|
||||
}
|
||||
}
|
||||
|
||||
// Customer 客户信息
|
||||
type Customer []ClientInfo
|
||||
type ClientInfo struct {
|
||||
// 个人信息
|
||||
PersonalInfo PersonalInfo `json:"personalInfo"`
|
||||
|
||||
// 购房目的
|
||||
PurchasePurpose PurchasePurpose `json:"purchasePurpose"`
|
||||
|
||||
// 核心需求
|
||||
CoreDemands CoreDemands `json:"coreDemands"`
|
||||
|
||||
// 关注点与顾虑
|
||||
Concerns []string `json:"concerns"`
|
||||
|
||||
// 决策建议
|
||||
DecisionProfile []string `json:"decisionProfile"`
|
||||
}
|
||||
|
||||
type PersonalInfo struct {
|
||||
Name string `json:"name"` // 姓氏
|
||||
Gender string `json:"gender"` // 性别
|
||||
Location string `json:"location"` // 来源地/当前居住地
|
||||
IsFirstHome bool `json:"isFirstHome"` // 是否首套房
|
||||
FamilyOrganize string `json:"familyOrganize"` // 家庭人数
|
||||
}
|
||||
|
||||
type PurchasePurpose struct {
|
||||
PrimaryPurpose string `json:"primaryPurpose"` // 主要目的
|
||||
SecondaryPurpose string `json:"secondaryPurpose"` // 次要目的
|
||||
DecisionMakers string `json:"decisionMakers"` // 决策人
|
||||
}
|
||||
|
||||
type CoreDemands struct {
|
||||
TotalBudget string `json:"totalBudget"` // 预算范围
|
||||
PreferredLayout string `json:"preferredLayout"` // 偏好户型
|
||||
CoreAppeal string `json:"coreAppeal"` // 核心述求
|
||||
}
|
||||
|
||||
func (e *Customer) Example() string {
|
||||
return `[{"personalInfo":{"name":"唐","gender":"男","location":"成都北门","isFirstHome":true,"familyOrganize":"夫妻"},"purchasePurpose":{"primaryPurpose":"首次置业,解决自住","secondaryPurpose":"资产保值,未来可出租","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400"","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在有限预算内满足家庭居住功能,确保房产保值"},"concerns":["总价超预算风险","板块保值能力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性"]},{"personalInfo":{"name":"冯女士","gender":"女","location":"","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"子女教育质量提升","decisionMakers":"夫妻双方需家庭共同商议]},"coreDemands":{"totalBudget":"400-500","preferredLayout":"118㎡四房三卫(非全景户型)","coreAppeal":"安静舒适、学区有保障的改善型住房"},"concerns":["临路噪音影响老人休息","学区质量和稳定性","社区小,绿化空间有限","得房率是否足够","二八板块学区对比"],"decisionProfile":["对噪音敏感,需要安静环境","重视教育资源配置","关注社区品质和舒适度","需要详细对比不同板块学区优势"]}]`
|
||||
}
|
||||
|
||||
func (e *Customer) Copy() AdviceData {
|
||||
return new(Customer)
|
||||
}
|
||||
|
||||
func (e *Customer) Role() AdviceRole {
|
||||
return RoleClient
|
||||
}
|
||||
|
||||
func (e *Customer) Desc() string {
|
||||
return "客户信息"
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package mongo_model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdvicerProjectMongo struct {
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
ProjectInfo ProjectInfo `json:"projectInfo" bson:"projectInfo"`
|
||||
RegionValue RegionValue `json:"regionValue" bson:"regionValue"`
|
||||
CompetitionComparison CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"`
|
||||
CoreSellingPoints CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"`
|
||||
SupportingFacilities SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"`
|
||||
DeveloperBacking DeveloperBacking `json:"developerBacking" bson:"developerBacking"`
|
||||
LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"`
|
||||
}
|
||||
|
||||
func NewAdvicerProjectMongo() *AdvicerProjectMongo {
|
||||
return &AdvicerProjectMongo{}
|
||||
}
|
||||
|
||||
func (a *AdvicerProjectMongo) MongoTableName() string {
|
||||
return "advicer_project"
|
||||
}
|
||||
|
||||
type AdvicerProjectMongoEntity struct {
|
||||
RegionValue RegionValue `json:"regionValue" bson:"regionValue"`
|
||||
CompetitionComparison CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"`
|
||||
CoreSellingPoints CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"`
|
||||
SupportingFacilities SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"`
|
||||
DeveloperBacking DeveloperBacking `json:"developerBacking" bson:"developerBacking"`
|
||||
}
|
||||
|
||||
func (a *AdvicerProjectMongo) Entity() *AdvicerProjectMongoEntity {
|
||||
return &AdvicerProjectMongoEntity{
|
||||
RegionValue: a.RegionValue,
|
||||
CompetitionComparison: a.CompetitionComparison,
|
||||
CoreSellingPoints: a.CoreSellingPoints,
|
||||
SupportingFacilities: a.SupportingFacilities,
|
||||
DeveloperBacking: a.DeveloperBacking,
|
||||
}
|
||||
}
|
||||
|
||||
type ProjectInfo struct {
|
||||
Name string `json:"projectName" bson:"projectName"`
|
||||
Address string `json:"projectAddress" bson:"projectAddress"`
|
||||
Area string `json:"area" bson:"area"`
|
||||
HouseTypes []HouseType `json:"houseTypes" bson:"houseTypes"`
|
||||
}
|
||||
|
||||
type HouseType struct {
|
||||
Name string `json:"name" bson:"name"`
|
||||
BuildArea string `json:"buildArea" bson:"buildArea"`
|
||||
InnerArea string `json:"innerArea" bson:"innerArea"`
|
||||
UnitPrice string `json:"unitPrice" bson:"unitPrice"`
|
||||
TotalPrice string `json:"totalPrice" bson:"totalPrice"`
|
||||
}
|
||||
|
||||
// RegionValue 区域价值话术库
|
||||
type RegionValue map[string][]string
|
||||
|
||||
func (e *RegionValue) Example() string {
|
||||
return `{"区位层级":["成华区2.5环内侧,这个位置真的稀缺","槐树店板块现在是成华区的number one板块","北接三板桥商圈,西靠万象城,东临火车东站","属于淮舜板块,万象城东的核心位置"],"地价论证":["我们地价19500,华晨府20400,棕榈也是2万+","2.5环内现在地价没有低于19000的","面粉贵了,面包不可能便宜"],"板块热度":["从21年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江系列在这里,新希望的锦麟系列也在这里","各大品牌开发商争相恐后都在这边拿地"],"发展规划":["槐树店板块是棋盘成钢之后第二个富人区","整个板块都是300万到900万的总价段","未来全是改善型住宅,没有刚需盘"]}`
|
||||
}
|
||||
func (e *RegionValue) Copy() AdviceData {
|
||||
return new(RegionValue)
|
||||
}
|
||||
|
||||
func (e *RegionValue) Role() AdviceRole {
|
||||
return RoleProject
|
||||
}
|
||||
|
||||
func (e *RegionValue) Desc() string {
|
||||
return "区域价值话术"
|
||||
}
|
||||
|
||||
// CompetitionComparison 竞品对比话术
|
||||
type CompetitionComparison map[string]map[string]string
|
||||
|
||||
func (e *CompetitionComparison) Example() string {
|
||||
return `{"龙湖滨江云河颂":{"优点承认":"龙湖位置确实好,看沙河公园","价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们得房率118平实得132平,套内单价才33000"},"邦泰云锦":{"定位相似":"邦泰也是首个项目,要打造口碑","价格参考":"他们当时12800拿地,现在卖34000","品质对比":"我们外立面全玻璃幕墙,比他们成本高30%"}}`
|
||||
}
|
||||
|
||||
func (e *CompetitionComparison) Copy() AdviceData {
|
||||
return new(CompetitionComparison)
|
||||
}
|
||||
|
||||
func (e *CompetitionComparison) Role() AdviceRole {
|
||||
return RoleProject
|
||||
}
|
||||
|
||||
func (e *CompetitionComparison) Desc() string {
|
||||
return "竞品对比话术"
|
||||
}
|
||||
|
||||
// CoreSellingPoints 核心卖点
|
||||
type CoreSellingPoints map[string]string
|
||||
|
||||
func (e *CoreSellingPoints) Example() string {
|
||||
return `{"产品配置高端":"全玻璃幕墙+铝单板外立面,三层中空氩气玻璃,3.2米层高,方太Y9烟机灶具,高仪卫浴","地段稀缺性":"成华区2.5环内侧核心地段,槐树店板块是成华区number one板块,被三板桥、万象城、火车东站包围","得房率高":"118平实得132平,套内单价33000,比龙湖滨江云河颂套内单价低3000"}`
|
||||
}
|
||||
func (e *CoreSellingPoints) Copy() AdviceData {
|
||||
return new(CoreSellingPoints)
|
||||
}
|
||||
|
||||
func (e *CoreSellingPoints) Role() AdviceRole {
|
||||
return RoleProject
|
||||
}
|
||||
|
||||
func (e *CoreSellingPoints) Desc() string {
|
||||
return "核心卖点"
|
||||
}
|
||||
|
||||
// SupportingFacilities 配套体系
|
||||
type SupportingFacilities map[string]map[string]string
|
||||
|
||||
func (e *SupportingFacilities) Example() string {
|
||||
return `{"交通配套":{"地铁":"双店路站350米(7号线),槐树店站550米(4号线),未来12号线","道路":"中环路、成洛大道,到春熙路5个站","通达性":"到火车东站2个站,到华西30分钟内"},"教育配套":{"幼儿园":"楼下公立幼儿园","小学":"城市附小锦汇东城(成华区生源最好的学校)","生源优势":"周边新盘都是300万+,生源纯粹"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区30分钟车程","便利性":"到华西本部也是30分钟内"}}`
|
||||
}
|
||||
|
||||
func (e *SupportingFacilities) Copy() AdviceData {
|
||||
return new(SupportingFacilities)
|
||||
}
|
||||
|
||||
func (e *SupportingFacilities) Role() AdviceRole {
|
||||
return RoleProject
|
||||
}
|
||||
|
||||
func (e *SupportingFacilities) Desc() string {
|
||||
return "配套体系"
|
||||
}
|
||||
|
||||
// DeveloperBacking 开发商背书
|
||||
type DeveloperBacking map[string]string
|
||||
|
||||
func (e *DeveloperBacking) Example() string {
|
||||
return `{"公司实力":"中信资产,多元化民营企业","资金安全":"在河南渑池有铝土矿,每年稳定收入10亿","开发经验":"宜宾有5个项目,贵州2个,成都是首个项目","合作方":"招商铂金物业,首次与外部企业合作"}`
|
||||
}
|
||||
|
||||
func (e *DeveloperBacking) Copy() AdviceData {
|
||||
return new(DeveloperBacking)
|
||||
}
|
||||
|
||||
func (e *DeveloperBacking) Role() AdviceRole {
|
||||
return RoleProject
|
||||
}
|
||||
|
||||
func (e *DeveloperBacking) Desc() string {
|
||||
return "开发商背书"
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package mongo_model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdvicerTalkSkillMongo struct {
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
Desc string `json:"desc" bson:"desc"`
|
||||
NeedsMining NeedsMining `json:"needsMining" bson:"needsMining"`
|
||||
PainPointResponse PainPointResponse `json:"painPointResponse" bson:"painPointResponse"`
|
||||
ValueBuilding ValueBuilding `json:"valueBuilding" bson:"valueBuilding"`
|
||||
ClosingTechniques ClosingTechniques `json:"closingTechniques" bson:"closingTechniques"`
|
||||
CommunicationRhythm CommunicationRhythm `json:"communicationRhythm" bson:"communicationRhythm"`
|
||||
LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"`
|
||||
}
|
||||
|
||||
func NewAdvicerTalkSkillMongo() *AdvicerTalkSkillMongo {
|
||||
return &AdvicerTalkSkillMongo{}
|
||||
}
|
||||
|
||||
func (a *AdvicerTalkSkillMongo) MongoTableName() string {
|
||||
return "advicer_talk_skill"
|
||||
}
|
||||
|
||||
type AdvicerTalkSkillMongoEntity struct {
|
||||
NeedsMining NeedsMining `json:"needsMining"`
|
||||
PainPointResponse PainPointResponse `json:"painPointResponse"`
|
||||
ValueBuilding ValueBuilding `json:"valueBuilding"`
|
||||
ClosingTechniques ClosingTechniques `json:"closingTechniques"`
|
||||
CommunicationRhythm CommunicationRhythm `json:"communicationRhythm"`
|
||||
}
|
||||
|
||||
func (a *AdvicerTalkSkillMongo) Entity() *AdvicerTalkSkillMongoEntity {
|
||||
return &AdvicerTalkSkillMongoEntity{
|
||||
NeedsMining: a.NeedsMining,
|
||||
PainPointResponse: a.PainPointResponse,
|
||||
ValueBuilding: a.ValueBuilding,
|
||||
ClosingTechniques: a.ClosingTechniques,
|
||||
CommunicationRhythm: a.CommunicationRhythm,
|
||||
}
|
||||
}
|
||||
|
||||
// NeedsMining 需求挖掘话术
|
||||
type NeedsMining map[string][]string
|
||||
|
||||
func (e *NeedsMining) Example() string {
|
||||
return `{"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"],"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"]}`
|
||||
}
|
||||
|
||||
func (e *NeedsMining) Copy() AdviceData {
|
||||
return new(NeedsMining)
|
||||
}
|
||||
|
||||
func (e *NeedsMining) Role() AdviceRole {
|
||||
return RoleSkill
|
||||
}
|
||||
|
||||
func (e *NeedsMining) Desc() string {
|
||||
return "需求挖掘话术"
|
||||
}
|
||||
|
||||
// PainPointResponse 痛点应对策略
|
||||
type PainPointResponse map[string]map[string]string
|
||||
|
||||
func (e *PainPointResponse) Example() string {
|
||||
return `{"地块太小":{"承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,楼间距反而更开阔","对比竞品":"339的邦泰才11亩,人家上千万豪宅"},"物业费高":{"理解感受":"我懂你,我们也觉得有点贵","价值分析":"但6块里3块是增值服务(保洁、送外卖)","价格补贴":"前三年补贴到5块,跟其他盘差不多"}}`
|
||||
}
|
||||
func (e *PainPointResponse) Copy() AdviceData {
|
||||
return new(PainPointResponse)
|
||||
}
|
||||
|
||||
func (e *PainPointResponse) Role() AdviceRole {
|
||||
return RoleSkill
|
||||
}
|
||||
|
||||
func (e *PainPointResponse) Desc() string {
|
||||
return "痛点应对策略"
|
||||
}
|
||||
|
||||
// ValueBuilding 价值塑造技巧
|
||||
type ValueBuilding map[string][]string
|
||||
|
||||
func (e *ValueBuilding) Example() string {
|
||||
return `{"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生"],"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是3000万豪宅才有的配置","外立面成本比竞品高30%,但单价差不多"]}`
|
||||
}
|
||||
|
||||
func (e *ValueBuilding) Copy() AdviceData {
|
||||
return new(ValueBuilding)
|
||||
}
|
||||
|
||||
func (e *ValueBuilding) Role() AdviceRole {
|
||||
return RoleSkill
|
||||
}
|
||||
|
||||
func (e *ValueBuilding) Desc() string {
|
||||
return "价值塑造技巧"
|
||||
}
|
||||
|
||||
// ClosingTechniques 促单话术
|
||||
type ClosingTechniques map[string]map[string][]string
|
||||
|
||||
func (e *ClosingTechniques) Example() string {
|
||||
return `{"紧迫感营造":{"时间紧迫":["今天是月底最后一天,领导有压力价格可谈","我们刚刚开盘,还有额外优惠","月底冲业绩,价格最有弹性"],"房源稀缺":["118只剩20多套了,好楼层不多","这栋楼就60户,卖一套少一套","特价房只有这几套,今天不定可能就没了"]},"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外折扣","买车位的话,总价多给两个点优惠","一次性付款再优惠一个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]}}`
|
||||
}
|
||||
|
||||
func (e *ClosingTechniques) Copy() AdviceData {
|
||||
return new(ClosingTechniques)
|
||||
}
|
||||
|
||||
func (e *ClosingTechniques) Role() AdviceRole {
|
||||
return RoleSkill
|
||||
}
|
||||
|
||||
func (e *ClosingTechniques) Desc() string {
|
||||
return "促单话术"
|
||||
}
|
||||
|
||||
// CommunicationRhythm 沟通节奏控制
|
||||
type CommunicationRhythm map[string]map[string]string
|
||||
|
||||
func (e *CommunicationRhythm) Example() string {
|
||||
return `{"开场阶段":{"时间占比":"5%","目标":"建立关系,了解需求","关键动作":"亲切称呼,简单寒暄,确认看房重点"},"沙盘讲解":{"时间占比":"30%","目标":"建立价值认知","关键动作":"板块价值→周边配套→项目亮点→开发商介绍"}}`
|
||||
}
|
||||
|
||||
func (e *CommunicationRhythm) Copy() AdviceData {
|
||||
return new(CommunicationRhythm)
|
||||
}
|
||||
|
||||
func (e *CommunicationRhythm) Role() AdviceRole {
|
||||
return RoleSkill
|
||||
}
|
||||
|
||||
func (e *CommunicationRhythm) Desc() string {
|
||||
return "沟通节奏控制"
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package mongo_model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type AdvicerVersionMongo struct {
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
VersionDesc string `json:"versionDesc" bson:"versionDesc"`
|
||||
DialectFeatures DialectFeatures `json:"dialectFeatures" bson:"DialectFeatures"`
|
||||
SentencePatterns SentencePatterns `json:"sentencePatterns" bson:"sentencePatterns"`
|
||||
ToneTags ToneTags `json:"toneTags" bson:"toneTags"`
|
||||
PersonalityTags PersonalityTags `json:"personalityTags" bson:"personalityTags"`
|
||||
SignatureDialogues SignatureDialogues `json:"signatureDialogues" bson:"signatureDialogues"`
|
||||
LastUpdateTime time.Time `json:"lastUpdateTime" bson:"lastUpdateTime"`
|
||||
}
|
||||
|
||||
func NewAdvicerVersionMongo() *AdvicerVersionMongo {
|
||||
return &AdvicerVersionMongo{}
|
||||
}
|
||||
|
||||
func (a *AdvicerVersionMongo) MongoTableName() string {
|
||||
return "advicer_version"
|
||||
}
|
||||
|
||||
type AdvicerVersionMongoEntity struct {
|
||||
DialectFeatures DialectFeatures `json:"dialectFeatures"`
|
||||
SentencePatterns SentencePatterns `json:"sentencePatterns"`
|
||||
ToneTags ToneTags `json:"toneTags"`
|
||||
PersonalityTags PersonalityTags `json:"personalityTags"`
|
||||
SignatureDialogues SignatureDialogues `json:"signatureDialogues"`
|
||||
}
|
||||
|
||||
func (a *AdvicerVersionMongo) Entity() *AdvicerVersionMongoEntity {
|
||||
return &AdvicerVersionMongoEntity{
|
||||
DialectFeatures: a.DialectFeatures,
|
||||
SentencePatterns: a.SentencePatterns,
|
||||
ToneTags: a.ToneTags,
|
||||
PersonalityTags: a.PersonalityTags,
|
||||
SignatureDialogues: a.SignatureDialogues,
|
||||
}
|
||||
}
|
||||
|
||||
// DialectFeatures 方言特征
|
||||
type DialectFeatures struct {
|
||||
Region string `json:"region"` //方言使用程度
|
||||
Intensity float64 `json:"intensity"` // 方言使用强度(0-1)
|
||||
KeyWords []string `json:"keyWords"`
|
||||
}
|
||||
|
||||
func (e *DialectFeatures) Example() string {
|
||||
return `{"region":"四川成都话","intensity":0.4,"keyWords":["噻","要得","没得","不晓得","是不是"]}`
|
||||
}
|
||||
|
||||
func (e *DialectFeatures) Copy() AdviceData {
|
||||
return new(DialectFeatures)
|
||||
}
|
||||
|
||||
func (e *DialectFeatures) Role() AdviceRole {
|
||||
return RoleAdvicer
|
||||
}
|
||||
|
||||
func (e *DialectFeatures) Desc() string {
|
||||
return "方言特征"
|
||||
}
|
||||
|
||||
// SentencePatterns 句子模式
|
||||
type SentencePatterns struct {
|
||||
OpeningMode []string `json:"openingMode"` //开场模式
|
||||
ExplanationMode []string `json:"explanationMode"` //解释模式
|
||||
ConfirmationMode []string `json:"confirmationMode"` //确认模式
|
||||
SummaryMode []string `json:"summaryMode"` //总结模式
|
||||
TransitionMode []string `json:"transitionMode"` //过渡模式
|
||||
}
|
||||
|
||||
func (e *SentencePatterns) Example() string {
|
||||
return `{"openingMode":["我给你介绍一下","我们先来看一下"],"explanationMode":["是这样的","我跟你讲","你发现没得"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?"],"summaryMode":["所以说","简单说就是"],"transitionMode":["然后的话","再其次","还有一点"]}`
|
||||
}
|
||||
|
||||
func (e *SentencePatterns) Copy() AdviceData {
|
||||
return new(SentencePatterns)
|
||||
}
|
||||
|
||||
func (e *SentencePatterns) Role() AdviceRole {
|
||||
return RoleAdvicer
|
||||
}
|
||||
|
||||
func (e *SentencePatterns) Desc() string {
|
||||
return "句子模式"
|
||||
}
|
||||
|
||||
// PersonalityTags 个性标签
|
||||
type PersonalityTags []string
|
||||
|
||||
func (e *PersonalityTags) Example() string {
|
||||
return `["耐心细致","细节控"]`
|
||||
}
|
||||
func (e *PersonalityTags) Copy() AdviceData {
|
||||
return new(PersonalityTags)
|
||||
}
|
||||
|
||||
func (e *PersonalityTags) Role() AdviceRole {
|
||||
return RoleAdvicer
|
||||
}
|
||||
|
||||
func (e *PersonalityTags) Desc() string {
|
||||
return "个性标签"
|
||||
}
|
||||
|
||||
// ToneTags 语气标签
|
||||
type ToneTags struct {
|
||||
Enthusiasm float64 `json:"enthusiasm"`
|
||||
Patience float64 `json:"patience"`
|
||||
Confidence float64 `json:"confidence"`
|
||||
Friendliness float64 `json:"friendliness"`
|
||||
Persuasion float64 `json:"persuasion"`
|
||||
}
|
||||
|
||||
func (e *ToneTags) Example() string {
|
||||
return `{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.75,"persuasion":0.7}`
|
||||
}
|
||||
|
||||
func (e *ToneTags) Copy() AdviceData {
|
||||
return new(ToneTags)
|
||||
}
|
||||
|
||||
func (e *ToneTags) Role() AdviceRole {
|
||||
return RoleAdvicer
|
||||
}
|
||||
|
||||
func (e *ToneTags) Desc() string {
|
||||
return "语气标签"
|
||||
}
|
||||
|
||||
// SignatureDialogues 代表性对话示例
|
||||
type SignatureDialogues []struct {
|
||||
Context string `json:"context"`
|
||||
Dialogue string `json:"dialogue"` //解释
|
||||
}
|
||||
|
||||
func (e *SignatureDialogues) Example() string {
|
||||
return `[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都是2.5环内城买房,这种是个普遍存在的一个现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有管家送外卖、免费宠物喂养这些增值服务。你算一下,就算贵一块钱,十年也就多14000,但好物业让房子增值不止这点!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比28板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"}]`
|
||||
}
|
||||
|
||||
func (e *SignatureDialogues) Copy() AdviceData {
|
||||
return new(SignatureDialogues)
|
||||
}
|
||||
|
||||
func (e *SignatureDialogues) Role() AdviceRole {
|
||||
return RoleAdvicer
|
||||
}
|
||||
|
||||
func (e *SignatureDialogues) Desc() string {
|
||||
return "代表性对话示例"
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package mongo_model
|
||||
|
||||
type AdviceRole string
|
||||
|
||||
const (
|
||||
RoleAdvicer AdviceRole = "advicer"
|
||||
RoleProject AdviceRole = "project"
|
||||
RoleSkill AdviceRole = "skill"
|
||||
RoleClient AdviceRole = "client"
|
||||
)
|
||||
|
||||
var RoleDesc = map[AdviceRole]string{
|
||||
RoleAdvicer: "顾问",
|
||||
RoleProject: "项目",
|
||||
RoleSkill: "沟通技巧",
|
||||
RoleClient: "客户",
|
||||
}
|
||||
|
||||
type AdviceData interface {
|
||||
Example() string
|
||||
Copy() AdviceData
|
||||
Role() AdviceRole
|
||||
Desc() string
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package mongo_model
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSetMongo = wire.NewSet(
|
||||
NewAdvicerVersionMongo,
|
||||
NewAdvicerTalkSkillMongo,
|
||||
NewAdvicerProjectMongo,
|
||||
NewAdvicerClientMongo,
|
||||
NewAdvicerChatHisMongo,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 // 答案
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/domain/tools/common/excel_generator"
|
||||
"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_brand_search"
|
||||
"ai_scheduler/internal/domain/tools/hyt/goods_category_add"
|
||||
|
|
@ -25,6 +26,7 @@ type Manager struct {
|
|||
type CommonTools struct {
|
||||
ExcelGenerator *excel_generator.Client
|
||||
ImageConverter *image_converter.Client
|
||||
KnowledgeBase *knowledge_base.Client
|
||||
}
|
||||
|
||||
type HytTools struct {
|
||||
|
|
@ -60,6 +62,7 @@ func NewManager(cfg *config.Config) *Manager {
|
|||
Common: &CommonTools{
|
||||
ExcelGenerator: excel_generator.New(),
|
||||
ImageConverter: image_converter.New(cfg.EinoTools.Excel2Pic),
|
||||
KnowledgeBase: knowledge_base.New(cfg.KnowledgeConfig),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package entitys
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
)
|
||||
|
||||
type ChatData struct {
|
||||
ClientInfo *mongo_model.AdvicerClientMongoEntity `json:"clientInfo"`
|
||||
TalkSkill *mongo_model.AdvicerTalkSkillMongoEntity `json:"talkSkill"`
|
||||
ProjectInfo *mongo_model.AdvicerProjectMongoEntity `json:"projectInfo"`
|
||||
AdvicerInfo *model.AiAdviceAdvicerEntity `json:"advicerInfo"`
|
||||
AdvicerVersion *mongo_model.AdvicerVersionMongoEntity `json:"advicerVersion"`
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package entitys
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
)
|
||||
|
||||
type WordAnaReq struct {
|
||||
WordFileUrl string `json:"wordFileUrl"`
|
||||
ProjectId int32 `json:"projectId"`
|
||||
}
|
||||
|
||||
type AdvicerInitReq struct {
|
||||
AdvicerID int32 `json:"advicerId"`
|
||||
ProjectID int32 `json:"projectId"`
|
||||
Name string `json:"name"` // 姓名
|
||||
Birth string `json:"birth"` // 用户名称
|
||||
Gender int32 `json:"gender"` // 1:男,2:女
|
||||
WorkingYears int32 `json:"workingYears"` // 工作年限
|
||||
}
|
||||
|
||||
type AdvicerInfoReq struct {
|
||||
AdvicerID int32 `json:"AdvicerId"`
|
||||
}
|
||||
|
||||
type AdvicerListReq struct {
|
||||
ProjectId int32 `json:"projectId"`
|
||||
}
|
||||
|
||||
type AdvicerVersionAddReq struct {
|
||||
AdvicerID int32 `json:"advicerId"`
|
||||
VersionDesc string `json:"versionDesc"`
|
||||
DialectFeatures mongo_model.DialectFeatures `json:"dialectFeatures"`
|
||||
PersonalityTags mongo_model.PersonalityTags `json:"personalityTags"`
|
||||
SentencePatterns mongo_model.SentencePatterns `json:"sentencePatterns"`
|
||||
SignatureDialogues mongo_model.SignatureDialogues `json:"signatureDialogues"`
|
||||
ToneTags mongo_model.ToneTags `json:"toneTags"`
|
||||
}
|
||||
|
||||
type AdvicerVersionUpdateReq struct {
|
||||
Id string `json:"id"`
|
||||
AdvicerID int32 `json:"advicerId"`
|
||||
VersionDesc string `json:"versionDesc"`
|
||||
DialectFeatures mongo_model.DialectFeatures `json:"dialectFeatures"`
|
||||
PersonalityTags mongo_model.PersonalityTags `json:"personalityTags"`
|
||||
SentencePatterns mongo_model.SentencePatterns `json:"sentencePatterns"`
|
||||
SignatureDialogues mongo_model.SignatureDialogues `json:"signatureDialogues"`
|
||||
ToneTags mongo_model.ToneTags `json:"toneTags"`
|
||||
}
|
||||
|
||||
type AdvicerVersionListReq struct {
|
||||
Id string `json:"id"`
|
||||
AdvicerId int32 `json:"advicerId"`
|
||||
VersionDesc string `json:"versionDesc"`
|
||||
}
|
||||
|
||||
type AdvicerVersionDelReq struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type AdvicerVersionInfoReq struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type AdvicerTalkSkillAddReq struct {
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
Desc string `json:"desc" bson:"desc"`
|
||||
NeedsMining mongo_model.NeedsMining `json:"needsMining" bson:"needsMining"`
|
||||
PainPointResponse mongo_model.PainPointResponse `json:"painPointResponse" bson:"painPointResponse"`
|
||||
ValueBuilding mongo_model.ValueBuilding `json:"valueBuilding" bson:"valueBuilding"`
|
||||
ClosingTechniques mongo_model.ClosingTechniques `json:"closingTechniques" bson:"closingTechniques"`
|
||||
CommunicationRhythm mongo_model.CommunicationRhythm `json:"communicationRhythm" bson:"communicationRhythm"`
|
||||
}
|
||||
|
||||
type AdvicerTalkSkillUpdateReq struct {
|
||||
Id string `json:"id"`
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId" :"advicer-id"`
|
||||
Desc string `json:"desc" bson:"desc" :"desc"`
|
||||
NeedsMining mongo_model.NeedsMining `json:"needsMining" bson:"needsMining" :"needs-mining"`
|
||||
PainPointResponse mongo_model.PainPointResponse `json:"painPointResponse" bson:"painPointResponse" :"pain-point-response"`
|
||||
ValueBuilding mongo_model.ValueBuilding `json:"valueBuilding" bson:"valueBuilding" :"value-building"`
|
||||
ClosingTechniques mongo_model.ClosingTechniques `json:"closingTechniques" bson:"closingTechniques" :"closing-techniques"`
|
||||
CommunicationRhythm mongo_model.CommunicationRhythm `json:"communicationRhythm" bson:"communicationRhythm" :"communication-rhythm"`
|
||||
}
|
||||
|
||||
type AdvicerTalkSkillListReq struct {
|
||||
Id string `json:"id"`
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
Desc string `json:"desc" bson:"desc"`
|
||||
}
|
||||
|
||||
type AdvicerTalkSkillDelReq struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type AdvicerTalkSkillInfoReq struct {
|
||||
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 {
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
ProjectInfo mongo_model.ProjectInfo `json:"projectInfo" bson:"projectInfo"`
|
||||
RegionValue mongo_model.RegionValue `json:"regionValue" bson:"regionValue"`
|
||||
CompetitionComparison mongo_model.CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"`
|
||||
CoreSellingPoints mongo_model.CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"`
|
||||
SupportingFacilities mongo_model.SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"`
|
||||
DeveloperBacking mongo_model.DeveloperBacking `json:"developerBacking" bson:"developerBacking"`
|
||||
}
|
||||
|
||||
type AdvicerrProjectUpdateReq struct {
|
||||
Id string `json:"id"`
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
ProjectInfo mongo_model.ProjectInfo `json:"projectInfo" bson:"projectInfo"`
|
||||
RegionValue mongo_model.RegionValue `json:"regionValue" bson:"regionValue"`
|
||||
CompetitionComparison mongo_model.CompetitionComparison `json:"competitionComparison" bson:"competitionComparison"`
|
||||
CoreSellingPoints mongo_model.CoreSellingPoints `json:"coreSellingPoints" bson:"coreSellingPoints"`
|
||||
SupportingFacilities mongo_model.SupportingFacilities `json:"supportingFacilities" bson:"supportingFacilities"`
|
||||
DeveloperBacking mongo_model.DeveloperBacking `json:"developerBacking" bson:"developerBacking"`
|
||||
}
|
||||
|
||||
type AdvicerProjectInfoReq struct {
|
||||
Id string `json:"id"`
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
}
|
||||
|
||||
type AdvicerProjectInfoRes struct {
|
||||
Base model.AiAdviceProject
|
||||
ConfigInfo mongo_model.AdvicerProjectMongo
|
||||
ModelInfo model.AiAdviceModelSup
|
||||
}
|
||||
|
||||
type AdvicerClientAddReq struct {
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
PersonalInfo mongo_model.PersonalInfo `json:"personalInfo" bson:"personalInfo"`
|
||||
PurchasePurpose mongo_model.PurchasePurpose `json:"purchasePurpose" bson:"purchasePurpose"`
|
||||
CoreDemands mongo_model.CoreDemands `json:"coreDemands" bson:"coreDemands"`
|
||||
Concerns []string `json:"concerns" bson:"concerns"`
|
||||
DecisionProfile []string `json:"decisionProfile" bson:"decisionProfile"`
|
||||
}
|
||||
|
||||
type AdvicerrClientUpdateReq struct {
|
||||
Id string `json:"id"`
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
PersonalInfo mongo_model.PersonalInfo `json:"personalInfo" bson:"personalInfo"`
|
||||
PurchasePurpose mongo_model.PurchasePurpose `json:"purchasePurpose" bson:"purchasePurpose"`
|
||||
CoreDemands mongo_model.CoreDemands `json:"coreDemands" bson:"coreDemands"`
|
||||
Concerns []string `json:"concerns" bson:"concerns"`
|
||||
DecisionProfile []string `json:"decisionProfile" bson:"decisionProfile"`
|
||||
}
|
||||
|
||||
type AdvicerClientListReq struct {
|
||||
Id string `json:"id"`
|
||||
ProjectId int32 `json:"projectId" bson:"projectId"`
|
||||
AdvicerId int32 `json:"advicerId" bson:"advicerId"`
|
||||
}
|
||||
|
||||
type AdvicerClientDelReq struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type AdvicerClientInfoReq struct {
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type AdvicerChatRegistReq struct {
|
||||
AdvicerVersionId string `json:"advicerVersionId"`
|
||||
ClientId string `json:"clientId"`
|
||||
TalkSkillId string `json:"talkSkillId"`
|
||||
Mission string `json:"mission"`
|
||||
}
|
||||
|
||||
type AdvicerChatRegistRes struct {
|
||||
SessionId string `json:"sessionId"`
|
||||
}
|
||||
|
||||
type AdvicerChatReq struct {
|
||||
SessionId string `json:"sessionId"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type AdvicerChatRes struct {
|
||||
Content int32 `json:"content"`
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package entitys
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/pkg/dingtalk"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
)
|
||||
|
|
@ -22,6 +23,13 @@ type DingTalkBot struct {
|
|||
ClientSecret string `json:"client_secret"`
|
||||
}
|
||||
|
||||
func (d *DingTalkBot) GetAppKey() dingtalk.AppKey {
|
||||
return dingtalk.AppKey{
|
||||
AppKey: d.ClientId,
|
||||
AppSecret: d.ClientSecret,
|
||||
}
|
||||
}
|
||||
|
||||
type Task struct {
|
||||
Index string `json:"bot_index"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
|
|
@ -11,14 +10,12 @@ import (
|
|||
)
|
||||
|
||||
type ContactClient struct {
|
||||
config *config.Config
|
||||
cli *contact.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewContactClient(config *config.Config) (*ContactClient, error) {
|
||||
func NewContactClient(oauth2Client *Oauth2Client) (*ContactClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
|
|
@ -26,7 +23,7 @@ func NewContactClient(config *config.Config) (*ContactClient, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ContactClient{config: config, cli: c}, nil
|
||||
return &ContactClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
type SearchUserReq struct {
|
||||
|
|
@ -40,15 +37,23 @@ type SearchUserResp struct {
|
|||
Body interface{}
|
||||
}
|
||||
|
||||
func (c *ContactClient) SearchUserOne(accessToken string, name string) (string, error) {
|
||||
headers := &contact.SearchUserHeaders{}
|
||||
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
||||
resp, err := c.cli.SearchUserWithOptions(&contact.SearchUserRequest{
|
||||
func (c *ContactClient) SearchUserOne(appKey AppKey, name string) (string, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := c.cli.SearchUserWithOptions(
|
||||
&contact.SearchUserRequest{
|
||||
FullMatchField: tea.Int32(1),
|
||||
QueryWord: tea.String(name),
|
||||
Offset: tea.Int32(0),
|
||||
Size: tea.Int32(1),
|
||||
}, headers, &util.RuntimeOptions{})
|
||||
},
|
||||
&contact.SearchUserHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
|
@ -13,14 +12,12 @@ import (
|
|||
)
|
||||
|
||||
type NotableClient struct {
|
||||
config *config.Config
|
||||
cli *notable.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewNotableClient(config *config.Config) (*NotableClient, error) {
|
||||
func NewNotableClient(oauth2Client *Oauth2Client) (*NotableClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
|
|
@ -28,7 +25,7 @@ func NewNotableClient(config *config.Config) (*NotableClient, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &NotableClient{config: config, cli: c}, nil
|
||||
return &NotableClient{cli: c, oauth2Client: oauth2Client}, nil
|
||||
}
|
||||
|
||||
type UpdateRecordReq struct {
|
||||
|
|
@ -43,9 +40,13 @@ type UpdateRecordsserResp struct {
|
|||
Body interface{}
|
||||
}
|
||||
|
||||
func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (bool, error) {
|
||||
headers := ¬able.UpdateRecordsHeaders{}
|
||||
headers.XAcsDingtalkAccessToken = tea.String(accessToken)
|
||||
func (c *NotableClient) UpdateRecord(appKey AppKey, req *UpdateRecordReq) (bool, error) {
|
||||
// 获取token
|
||||
accessToken, err := c.oauth2Client.GetAccessToken(appKey)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
resp, err := c.cli.UpdateRecordsWithOptions(
|
||||
tea.String(req.BaseId),
|
||||
tea.String(req.SheetId),
|
||||
|
|
@ -63,7 +64,10 @@ func (c *NotableClient) UpdateRecord(accessToken string, req *UpdateRecordReq) (
|
|||
Id: tea.String(req.RecordId),
|
||||
},
|
||||
},
|
||||
}, headers, &util.RuntimeOptions{})
|
||||
},
|
||||
¬able.UpdateRecordsHeaders{XAcsDingtalkAccessToken: tea.String(accessToken)},
|
||||
&util.RuntimeOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ package dingtalk
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
|
@ -12,6 +13,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/faabiosr/cachego/file"
|
||||
"github.com/fastwego/dingding"
|
||||
|
|
@ -111,3 +113,141 @@ func (c *OldClient) QueryUserDetailsByMobile(ctx context.Context, mobile string)
|
|||
func (c *OldClient) GetAccessToken() (string, error) {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package file_download
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 下载文件
|
||||
func DownloadFile(url string, validFunc func(resp *http.Response) error) ([]byte, string, error) {
|
||||
// 设置超时
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
if validFunc != nil {
|
||||
err = validFunc(resp)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
}
|
||||
|
||||
// 读取文件数据
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
filename := getFilenameFromURL(url, resp)
|
||||
|
||||
return data, filename, nil
|
||||
}
|
||||
|
||||
// 从 URL 或响应头获取文件名
|
||||
func getFilenameFromURL(urlStr string, resp *http.Response) string {
|
||||
// 1. 尝试从 Content-Disposition 头获取
|
||||
contentDisposition := resp.Header.Get("Content-Disposition")
|
||||
if contentDisposition != "" {
|
||||
if strings.Contains(contentDisposition, "filename=") {
|
||||
parts := strings.Split(contentDisposition, "filename=")
|
||||
if len(parts) > 1 {
|
||||
filename := strings.Trim(parts[1], `"' `)
|
||||
return sanitizeFilename(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 从 URL 路径获取
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err == nil {
|
||||
path := parsedURL.Path
|
||||
if path != "" {
|
||||
filename := filepath.Base(path)
|
||||
if filename != "" && filename != "." && filename != "/" {
|
||||
return sanitizeFilename(filename)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 生成默认文件名
|
||||
return fmt.Sprintf("word_%d.docx", time.Now().Unix())
|
||||
}
|
||||
|
||||
// 清理文件名
|
||||
func sanitizeFilename(filename string) string {
|
||||
// 移除非法字符
|
||||
illegalChars := []string{"/", "\\", ":", "*", "?", "\"", "<", ">", "|"}
|
||||
for _, char := range illegalChars {
|
||||
filename = strings.ReplaceAll(filename, char, "_")
|
||||
}
|
||||
|
||||
// 确保有扩展名
|
||||
if !strings.Contains(filename, ".") {
|
||||
filename += ".docx"
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
|
||||
// 从URL获取Word文件的纯文本内容
|
||||
func GetWordTextFromURL(url string, validFunc func(resp *http.Response) error) (string, string, error) {
|
||||
// 1. 下载文件
|
||||
data, fileName, err := DownloadFile(url, validFunc)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("下载失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 解析Word文件
|
||||
text, err := parseWordContent(data)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("解析失败: %w", err)
|
||||
}
|
||||
|
||||
return text, fileName, nil
|
||||
}
|
||||
|
||||
// 解析Word内容 - 简单版本,只提取文字
|
||||
func parseWordContent(data []byte) (string, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
zipReader, err := zip.NewReader(reader, int64(len(data)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解压docx失败: %v", err)
|
||||
}
|
||||
|
||||
var textBuilder strings.Builder
|
||||
|
||||
// 遍历 ZIP 文件中的文件
|
||||
for _, file := range zipReader.File {
|
||||
// 只处理文档主体文件
|
||||
if file.Name == "word/document.xml" {
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("打开文档文件失败: %v", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// 读取 XML 内容
|
||||
xmlData, err := io.ReadAll(rc)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取XML失败: %v", err)
|
||||
}
|
||||
|
||||
// 提取文本
|
||||
text, err := parseWordXML(xmlData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析XML失败: %v", err)
|
||||
}
|
||||
|
||||
textBuilder.WriteString(text)
|
||||
break // 找到主文档后退出循环
|
||||
}
|
||||
}
|
||||
|
||||
return textBuilder.String(), nil
|
||||
}
|
||||
|
||||
// 解析 Word XML 文档
|
||||
func parseWordXML(xmlData []byte) (string, error) {
|
||||
type WordDocument struct {
|
||||
XMLName xml.Name `xml:"document"`
|
||||
Body struct {
|
||||
Paragraphs []struct {
|
||||
Runs []struct {
|
||||
Text string `xml:"t"`
|
||||
} `xml:"r"`
|
||||
} `xml:"p"`
|
||||
} `xml:"body"`
|
||||
}
|
||||
|
||||
var doc WordDocument
|
||||
if err := xml.Unmarshal(xmlData, &doc); err != nil {
|
||||
// 尝试简化解析
|
||||
return extractTextSimple(xmlData), nil
|
||||
}
|
||||
|
||||
var textBuilder strings.Builder
|
||||
for _, para := range doc.Body.Paragraphs {
|
||||
for _, run := range para.Runs {
|
||||
textBuilder.WriteString(run.Text)
|
||||
}
|
||||
textBuilder.WriteString("\n")
|
||||
}
|
||||
|
||||
return textBuilder.String(), nil
|
||||
}
|
||||
|
||||
// 简化文本提取(处理更复杂的文档结构)
|
||||
func extractTextSimple(xmlData []byte) string {
|
||||
var textBuilder strings.Builder
|
||||
|
||||
// 简单提取 <w:t> 标签内容
|
||||
decoder := xml.NewDecoder(bytes.NewReader(xmlData))
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if startElem, ok := token.(xml.StartElement); ok {
|
||||
if startElem.Name.Local == "t" {
|
||||
// 读取文本内容
|
||||
if nextToken, err := decoder.Token(); err == nil {
|
||||
if charData, ok := nextToken.(xml.CharData); ok {
|
||||
textBuilder.WriteString(string(charData))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return textBuilder.String()
|
||||
}
|
||||
|
||||
// 判断是否为 Word 文件
|
||||
func IsWordFile(resp *http.Response) error {
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
wordContentTypes := []string{
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-word",
|
||||
"application/octet-stream", // 有些服务器可能返回这个
|
||||
}
|
||||
|
||||
contentType = strings.ToLower(contentType)
|
||||
for _, ct := range wordContentTypes {
|
||||
if strings.Contains(contentType, ct) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("错误的文件类型")
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/utils_mongo"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
)
|
||||
|
||||
type Mongo struct {
|
||||
Client *mongo.Client
|
||||
c *config.Config
|
||||
}
|
||||
|
||||
func NewMongoDb(ctx context.Context, c *config.Config) (*Mongo, func()) {
|
||||
transDBClient, err := utils_mongo.NewMongoClient(ctx, &utils_mongo.ClientStruct{
|
||||
Uri: c.Mongo.Source,
|
||||
MaxPoolSize: c.Mongo.MaxPoolSize,
|
||||
MinPoolSize: c.Mongo.MinPoolSize,
|
||||
MaxConnIdleTime: time.Duration(c.Mongo.MaxConnIdleTime) * time.Minute,
|
||||
ConnectTimeout: time.Duration(c.Mongo.ConnectTimeout) * time.Second,
|
||||
SocketTimeout: time.Duration(c.Mongo.SocketTimeout) * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("mongo数据库错误: %v", err))
|
||||
}
|
||||
|
||||
if err = transDBClient.Ping(ctx, nil); err != nil {
|
||||
panic(fmt.Sprintf("mongo链接失败: %v", err))
|
||||
}
|
||||
return &Mongo{
|
||||
Client: transDBClient,
|
||||
c: c,
|
||||
}, func() {
|
||||
transDBClient.Disconnect(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
type MongoModel interface {
|
||||
MongoTableName() string
|
||||
}
|
||||
|
||||
func (m *Mongo) Co(mongoModel MongoModel) *mongo.Collection {
|
||||
return m.Client.Database(m.c.Mongo.DataBase).Collection(mongoModel.MongoTableName())
|
||||
}
|
||||
|
|
@ -21,7 +21,12 @@ var ProviderSetClient = wire.NewSet(
|
|||
dingtalk.NewOldClient,
|
||||
dingtalk.NewContactClient,
|
||||
dingtalk.NewNotableClient,
|
||||
dingtalk.NewRobotClient,
|
||||
dingtalk.NewOauth2Client,
|
||||
dingtalk.NewCardClient,
|
||||
dingtalk.NewImClient,
|
||||
|
||||
utils_oss.NewClient,
|
||||
lsxd.NewLogin,
|
||||
NewMongoDb,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func HandleResponse(c *fiber.Ctx, data interface{}, e error) (err error) {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
switch data.(type) {
|
||||
case error:
|
||||
err = data.(error)
|
||||
case int, int32, int64, float32, float64, string, bool:
|
||||
c.Response().SetBody([]byte(fmt.Sprintf("%v", data)))
|
||||
case []byte:
|
||||
c.Response().SetBody(data.([]byte))
|
||||
default:
|
||||
dataByte, _ := json.Marshal(data)
|
||||
c.Response().SetBody(dataByte)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -2,8 +2,6 @@ package util
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -44,74 +42,3 @@ func Contains[T comparable](strings []T, str T) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// json LLM专用字符串修复
|
||||
func JSONRepair(input string) (string, error) {
|
||||
s := strings.TrimSpace(input)
|
||||
|
||||
s = trimToJSONObject(s)
|
||||
s = normalizeQuotes(s)
|
||||
s = removeTrailingCommas(s)
|
||||
s = quoteObjectKeys(s)
|
||||
s = balanceBrackets(s)
|
||||
|
||||
// 最终校验
|
||||
var js any
|
||||
if err := json.Unmarshal([]byte(s), &js); err != nil {
|
||||
return "", fmt.Errorf("json repair failed: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// 裁剪前后垃圾文本
|
||||
func trimToJSONObject(s string) string {
|
||||
start := strings.IndexAny(s, "{[")
|
||||
end := strings.LastIndexAny(s, "}]")
|
||||
if start == -1 || end == -1 || start >= end {
|
||||
return s
|
||||
}
|
||||
return s[start : end+1]
|
||||
}
|
||||
|
||||
// 引号统一
|
||||
func normalizeQuotes(s string) string {
|
||||
// 只替换“看起来像字符串的单引号”
|
||||
re := regexp.MustCompile(`'([^']*)'`)
|
||||
return re.ReplaceAllString(s, `"$1"`)
|
||||
}
|
||||
|
||||
// 删除尾随逗号
|
||||
func removeTrailingCommas(s string) string {
|
||||
re := regexp.MustCompile(`,(\s*[}\]])`)
|
||||
return re.ReplaceAllString(s, `$1`)
|
||||
}
|
||||
|
||||
// 给 object key 自动补双引号
|
||||
func quoteObjectKeys(s string) string {
|
||||
re := regexp.MustCompile(`([{,]\s*)([a-zA-Z0-9_]+)\s*:`)
|
||||
return re.ReplaceAllString(s, `$1"$2":`)
|
||||
}
|
||||
|
||||
// 括号补齐
|
||||
func balanceBrackets(s string) string {
|
||||
var stack []rune
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '{', '[':
|
||||
stack = append(stack, r)
|
||||
case '}', ']':
|
||||
if len(stack) > 0 {
|
||||
stack = stack[:len(stack)-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := len(stack) - 1; i >= 0; i-- {
|
||||
switch stack[i] {
|
||||
case '{':
|
||||
s += "}"
|
||||
case '[':
|
||||
s += "]"
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
package utils_mongo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type ClientStruct struct {
|
||||
Uri string
|
||||
MaxPoolSize uint64
|
||||
MinPoolSize uint64
|
||||
MaxConnIdleTime time.Duration
|
||||
ConnectTimeout time.Duration
|
||||
SocketTimeout time.Duration
|
||||
}
|
||||
|
||||
func NewMongoClient(ctx context.Context, config *ClientStruct) (*mongo.Client, error) {
|
||||
clientOptions := options.Client().ApplyURI(config.Uri).
|
||||
SetMaxPoolSize(config.MaxPoolSize). // 最大连接数
|
||||
SetMinPoolSize(config.MinPoolSize). // 最小连接数
|
||||
SetMaxConnIdleTime(config.MaxConnIdleTime). // 连接最大空闲时间
|
||||
SetConnectTimeout(config.ConnectTimeout). // 连接超时
|
||||
SetSocketTimeout(config.ConnectTimeout) // 操作超时
|
||||
|
||||
return mongo.Connect(ctx, clientOptions)
|
||||
}
|
||||
|
|
@ -7,63 +7,33 @@ import (
|
|||
"encoding/base64"
|
||||
|
||||
"github.com/cloudwego/eino-ext/components/model/openai"
|
||||
"github.com/cloudwego/eino/components/model"
|
||||
"github.com/cloudwego/eino/schema"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
vlModel *openai.ChatModel
|
||||
generateModel *openai.ChatModel
|
||||
model *openai.ChatModel
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewClient(config *config.Config) (*Client, func(), error) {
|
||||
// 初始化视觉模型
|
||||
vl, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{
|
||||
BaseURL: config.Vllm.VLModel.BaseURL,
|
||||
Model: config.Vllm.VLModel.Model,
|
||||
Timeout: config.Vllm.VLModel.Timeout,
|
||||
m, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{
|
||||
BaseURL: config.Vllm.BaseURL,
|
||||
Model: config.Vllm.VlModel,
|
||||
Timeout: config.Vllm.Timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 初始化生成模型
|
||||
gen, err := openai.NewChatModel(context.Background(), &openai.ChatModelConfig{
|
||||
BaseURL: config.Vllm.TextModel.BaseURL,
|
||||
Model: config.Vllm.TextModel.Model,
|
||||
Timeout: config.Vllm.TextModel.Timeout,
|
||||
ExtraFields: map[string]any{
|
||||
"chat_template_kwargs": map[string]any{
|
||||
"enable_thinking": false,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
vlModel: vl,
|
||||
generateModel: gen,
|
||||
config: config,
|
||||
}
|
||||
c := &Client{model: m, config: config}
|
||||
cleanup := func() {}
|
||||
return c, cleanup, nil
|
||||
}
|
||||
|
||||
func (c *Client) Chat(ctx context.Context, msgs []*schema.Message) (*schema.Message, error) {
|
||||
// 默认聊天使用生成模型
|
||||
return c.generateModel.Generate(ctx, msgs)
|
||||
}
|
||||
|
||||
func (c *Client) ToolSelect(ctx context.Context, msgs []*schema.Message, tools []*schema.ToolInfo) (*schema.Message, error) {
|
||||
// 工具选择使用生成模型
|
||||
return c.generateModel.Generate(ctx, msgs, model.WithTools(tools))
|
||||
return c.model.Generate(ctx, msgs)
|
||||
}
|
||||
|
||||
func (c *Client) RecognizeWithImg(ctx context.Context, systemPrompt, userPrompt string, imgURLs []string) (*schema.Message, error) {
|
||||
// 图片识别使用视觉模型
|
||||
in := []*schema.Message{
|
||||
{
|
||||
Role: schema.System,
|
||||
|
|
@ -88,12 +58,11 @@ func (c *Client) RecognizeWithImg(ctx context.Context, systemPrompt, userPrompt
|
|||
}
|
||||
|
||||
in[1].UserInputMultiContent = parts
|
||||
return c.vlModel.Generate(ctx, in)
|
||||
return c.model.Generate(ctx, in)
|
||||
}
|
||||
|
||||
// 识别图片by二进制文件
|
||||
func (c *Client) RecognizeWithImgBytes(ctx context.Context, systemPrompt, userPrompt string, imgBytes []byte, imgType string) (*schema.Message, error) {
|
||||
// 图片识别使用视觉模型
|
||||
in := []*schema.Message{
|
||||
{
|
||||
Role: schema.System,
|
||||
|
|
@ -113,10 +82,9 @@ func (c *Client) RecognizeWithImgBytes(ctx context.Context, systemPrompt, userPr
|
|||
MIMEType: imgType,
|
||||
Base64Data: util.AnyToPoint(base64.StdEncoding.EncodeToString(imgBytes)),
|
||||
},
|
||||
Detail: schema.ImageURLDetailHigh,
|
||||
},
|
||||
})
|
||||
|
||||
in[1].UserInputMultiContent = parts
|
||||
return c.vlModel.Generate(ctx, in)
|
||||
return c.model.Generate(ctx, in)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"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/client"
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
|
|
@ -15,6 +16,7 @@ import (
|
|||
type DingBotServiceInterface interface {
|
||||
GetServiceCfg() ([]entitys.DingTalkBot, 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 {
|
||||
|
|
@ -38,7 +40,7 @@ func NewDingTalkBotServer(
|
|||
}
|
||||
cli := DingBotServerInit(serviceConf.ClientId, serviceConf.ClientSecret, service)
|
||||
if cli == nil {
|
||||
log.Info("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
|
||||
log.Infof("%s客户端初始失败:%s", serviceConf.BotIndex, err.Error())
|
||||
continue
|
||||
}
|
||||
clients[serviceConf.BotIndex] = cli
|
||||
|
|
@ -52,7 +54,9 @@ func NewDingTalkBotServer(
|
|||
func ProvideAllDingBotServices(
|
||||
dingBotSvc *services.DingBotService,
|
||||
) []DingBotServiceInterface {
|
||||
return []DingBotServiceInterface{dingBotSvc}
|
||||
return []DingBotServiceInterface{
|
||||
dingBotSvc,
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
cli = client.NewStreamClient(client.WithAppCredential(client.NewAppCredentialConfig(clientId, clientSecret)))
|
||||
cli.RegisterChatBotCallbackRouter(service.OnChatBotMessageReceived)
|
||||
cli.RegisterCardCallbackRouter(service.OnCardMessageReceived)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,23 +4,13 @@ import (
|
|||
"ai_scheduler/internal/gateway"
|
||||
"ai_scheduler/internal/server/router"
|
||||
"ai_scheduler/internal/services"
|
||||
"ai_scheduler/internal/services/advice"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/fiber/v2/middleware/recover"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
app *fiber.App
|
||||
service *services.ChatService
|
||||
session *services.SessionService
|
||||
gateway *gateway.Gateway
|
||||
callback *services.CallbackService
|
||||
chatHis *services.HistoryService
|
||||
capabilityService *services.CapabilityService
|
||||
supportService *services.SupportService
|
||||
}
|
||||
|
||||
func NewHTTPServer(
|
||||
service *services.ChatService,
|
||||
session *services.SessionService,
|
||||
|
|
@ -29,11 +19,16 @@ func NewHTTPServer(
|
|||
callback *services.CallbackService,
|
||||
chatHis *services.HistoryService,
|
||||
capabilityService *services.CapabilityService,
|
||||
supportService *services.SupportService,
|
||||
adviceFile *advice.FileService,
|
||||
adviceData *advice.AdvicerService,
|
||||
adviceChat *advice.ChatService,
|
||||
adviceProject *advice.ProjectService,
|
||||
adviceTalkSkill *advice.TalkSkillService,
|
||||
adviceClient *advice.ClientService,
|
||||
) *fiber.App {
|
||||
//构建 server
|
||||
app := initRoute()
|
||||
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, supportService)
|
||||
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, adviceFile, adviceData, adviceChat, adviceProject, adviceTalkSkill, adviceClient)
|
||||
return app
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/gateway"
|
||||
"ai_scheduler/internal/services"
|
||||
"ai_scheduler/internal/services/advice"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -16,19 +16,11 @@ import (
|
|||
"github.com/gofiber/websocket/v2"
|
||||
)
|
||||
|
||||
type RouterServer struct {
|
||||
app *fiber.App
|
||||
service *services.ChatService
|
||||
session *services.SessionService
|
||||
gateway *gateway.Gateway
|
||||
chatHist *services.HistoryService
|
||||
capabilityService *services.CapabilityService
|
||||
}
|
||||
|
||||
// SetupRoutes 设置路由
|
||||
func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionService *services.SessionService, task *services.TaskService,
|
||||
gateway *gateway.Gateway, callbackService *services.CallbackService, chatHist *services.HistoryService,
|
||||
capabilityService *services.CapabilityService, supportService *services.SupportService,
|
||||
capabilityService *services.CapabilityService, adviceFile *advice.FileService, adviceData *advice.AdvicerService,
|
||||
adviceChat *advice.ChatService, adviceProject *advice.ProjectService, adviceTalkSkill *advice.TalkSkillService, adviceClient *advice.ClientService,
|
||||
) {
|
||||
app.Use(func(c *fiber.Ctx) error {
|
||||
// 设置 CORS 头
|
||||
|
|
@ -70,7 +62,11 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
|
|||
r.Post("/chat/useful", ChatService.Useful)
|
||||
// 回调
|
||||
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("/broadcast", func(ctx *fiber.Ctx) error {
|
||||
|
|
@ -101,53 +97,39 @@ func SetupRoutes(app *fiber.App, ChatService *services.ChatService, sessionServi
|
|||
r.Post("/capability/product/ingest", capabilityService.ProductIngest) // 商品数据提取
|
||||
r.Post("/capability/product/ingest/:thread_id/confirm", capabilityService.ProductIngestConfirm) // 商品数据提取确认
|
||||
|
||||
// 外部系统支持
|
||||
r.Post("/support/address/ingest/:platform", supportService.AddressIngest) // 通用收获地址提取
|
||||
advicer := r.Group("advice/")
|
||||
advicer.Post("file/word/ana", adviceFile.WordAna)
|
||||
//顾问
|
||||
advicer.Post("advicer/add", adviceData.AdvicerUpdate)
|
||||
advicer.Post("advicer/update", adviceData.AdvicerUpdate)
|
||||
advicer.Post("advicer/list", adviceData.AdvicerList)
|
||||
advicer.Post("advicer/version/add", adviceData.AdvicerVersionAdd)
|
||||
advicer.Post("advicer/version/update", adviceData.AdvicerVersionUpdate)
|
||||
advicer.Post("advicer/version/del", adviceData.AdvicerVersionDel)
|
||||
advicer.Post("advicer/version/list", adviceData.AdvicerVersionList)
|
||||
//聊天技巧
|
||||
advicer.Post("skill/list", adviceTalkSkill.TalkSkillList)
|
||||
advicer.Post("skill/add", adviceTalkSkill.TalkSkillAdd)
|
||||
advicer.Post("skill/update", adviceTalkSkill.TalkSkillUpdate)
|
||||
advicer.Post("skill/del", adviceTalkSkill.TalkSkillDel)
|
||||
|
||||
// 转发Python服务
|
||||
r.All("/proxy/fingerprint", forwardToPythonService)
|
||||
}
|
||||
//项目
|
||||
advicer.Post("project/base/init", adviceProject.BaseInit)
|
||||
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)
|
||||
|
||||
func forwardToPythonService(c *fiber.Ctx) error {
|
||||
targetURL := "http://192.168.6.115:10086/fingerprint"
|
||||
//客户
|
||||
advicer.Post("client/add", adviceClient.Add)
|
||||
advicer.Post("client/update", adviceClient.Update)
|
||||
advicer.Post("client/list", adviceClient.List)
|
||||
advicer.Post("client/del", adviceClient.Del)
|
||||
|
||||
req, err := http.NewRequest(c.Method(), targetURL, c.Context().Request.BodyStream())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||
}
|
||||
//会话
|
||||
advicer.Post("chat/regis", adviceChat.Regis)
|
||||
advicer.Post("chat/chat", adviceChat.Chat)
|
||||
|
||||
c.Request().Header.VisitAll(func(key, value []byte) {
|
||||
req.Header.Add(string(key), string(value))
|
||||
})
|
||||
|
||||
if c.Context().QueryArgs().Len() > 0 {
|
||||
q := req.URL.Query()
|
||||
c.Context().QueryArgs().VisitAll(func(key, value []byte) {
|
||||
q.Add(string(key), string(value))
|
||||
})
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadGateway).SendString(err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
for key, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
c.Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
c.Status(resp.StatusCode)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
|
||||
}
|
||||
|
||||
return c.Send(body)
|
||||
}
|
||||
|
||||
func routerSocket(app *fiber.App, chatService *services.ChatService) {
|
||||
|
|
@ -184,12 +166,13 @@ func registerCommon(c *fiber.Ctx, err error) error {
|
|||
if c.Path() == "/api/v1/qywx/callback" {
|
||||
return nil
|
||||
}
|
||||
bsErr, ok := err.(*errors.BusinessErr)
|
||||
if !ok {
|
||||
bsErr = errors.SystemError
|
||||
}
|
||||
|
||||
// 如果有错误发生
|
||||
if err != nil {
|
||||
bsErr, ok := err.(*errors.BusinessErr)
|
||||
if !ok {
|
||||
bsErr = errorcode.SysErr(err.Error())
|
||||
}
|
||||
// 返回自定义错误响应
|
||||
return c.JSON(fiber.Map{
|
||||
"message": bsErr.Error(),
|
||||
|
|
@ -202,10 +185,20 @@ func registerCommon(c *fiber.Ctx, err error) error {
|
|||
// 是 SSE 请求
|
||||
return c.SendString("这是 SSE 请求")
|
||||
}
|
||||
var data interface{}
|
||||
json.Unmarshal(c.Response().Body(), &data)
|
||||
|
||||
body := c.Response().Body()
|
||||
if c.Locals("skip_response_wrap") == true {
|
||||
return c.JSON(string(body))
|
||||
}
|
||||
var rawData json.RawMessage
|
||||
if len(body) > 0 {
|
||||
if err := json.Unmarshal(body, &rawData); err != nil {
|
||||
// 解析失败,作为字符串包装成JSON
|
||||
rawData = json.RawMessage(`"` + strings.ReplaceAll(string(body), `"`, `\"`) + `"`)
|
||||
}
|
||||
}
|
||||
return c.JSON(fiber.Map{
|
||||
"data": data,
|
||||
"data": rawData,
|
||||
"message": errors.Success.Error(),
|
||||
"code": errors.Success.Code(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// AdvicerService 数据处理
|
||||
type AdvicerService struct {
|
||||
adviceBiz *biz.AdviceAdvicerBiz
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewDataService
|
||||
func NewAdvicerService(
|
||||
adviceBiz *biz.AdviceAdvicerBiz,
|
||||
cfg *config.Config,
|
||||
) *AdvicerService {
|
||||
return &AdvicerService{
|
||||
adviceBiz: adviceBiz,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *AdvicerService) AdvicerUpdate(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerInitReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := d.adviceBiz.Update(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, int(id), err)
|
||||
}
|
||||
|
||||
func (d *AdvicerService) AdvicerList(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerListReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := d.adviceBiz.List(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, list, err)
|
||||
}
|
||||
|
||||
func (d *AdvicerService) AdvicerVersionAdd(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerVersionAddReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := d.adviceBiz.VersionAdd(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, id, err)
|
||||
}
|
||||
|
||||
func (d *AdvicerService) AdvicerVersionUpdate(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerVersionUpdateReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.adviceBiz.VersionUpdate(c.UserContext(), req)
|
||||
}
|
||||
|
||||
func (d *AdvicerService) AdvicerVersionList(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerVersionListReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := d.adviceBiz.VersionList(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, list, err)
|
||||
}
|
||||
|
||||
func (d *AdvicerService) AdvicerVersionDel(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerVersionDelReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.adviceBiz.VersionDel(c.UserContext(), req)
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/biz/llm_service/third_party"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
|
||||
"encoding/json"
|
||||
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
func Test_WordAna(t *testing.T) {
|
||||
Run(context.Background(), nil)
|
||||
ana, err := fileService.WordAnat("https://attachment-public.oss-cn-hangzhou.aliyuncs.com/ai-scheduler/data-analytics/word/content2.docx")
|
||||
t.Log(ana, err)
|
||||
}
|
||||
|
||||
func Test_AdvicerInit(t *testing.T) {
|
||||
reqBody := `{"advicer_id": 124, "name": "张三111", "birth": "1990-01-01", "gender": 1, "working_years": 10}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
|
||||
err := advicerService.AdvicerUpdate(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_AdvicerVersionAdd(t *testing.T) {
|
||||
reqBody := `{"advicerId":124,"versionDesc":"第三个版本","dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := advicerService.AdvicerVersionAdd(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_AdvicerVersionUpdate(t *testing.T) {
|
||||
reqBody := `{"id":"69804b5a6532131383aeda3a","advicerId":124,"versionDesc":"第三个版本","dialectFeatures":{"region":"四川成都话","intensity":0.6,"KeyWords":null},"personalityTags":["耐心细致","专业务实","经验丰富","善于引导"],"sentencePatterns":{"openingMode":["我给你介绍一下","我们先来看一下","这边请"],"explanationMode":["是这样的","我跟你讲","你发现没得","说白了"],"confirmationMode":["对吧?","是不是嘛?","你晓得不?","明白了噻?","对不对?"],"summaryMode":["所以说","简单说就是","其实"],"transitionMode":["然后的话","再其次","还有一点","另外"]},"signatureDialogues":[{"context":"客户质疑地块大小","dialogue":"哥,14亩确实不大,但你要在成都2.5环内城买房,这种是普遍现象。你看万景和绿城都是13亩,中铁建只有8.8亩,339那个帮泰只有11亩。我们虽然地小,但楼间距开阔啊,看过去都是200多米!而且小小区人少安静,圈层更纯粹!"},{"context":"客户担心物业费高","dialogue":"姐,我懂你意思,我们也觉得物业费是有点贵。但招商物业是铂金服务,有夜间送外卖、免费宠物喂养、年度保洁这些增值服务。而且前三年开发商补贴一块钱,只需要交5块,跟其他盘差不多!好物业能让房子后期保值增值更多!"},{"context":"客户犹豫价格","dialogue":"说实话,这个地段的地价都比二八板块贵5000多,但我们单价只贵3000。你看龙湖滨江云河颂套内单价都36000了,我们才33000,真的性价比高!现在不买,以后这个板块可能就买不起了。"},{"context":"客户担心小区小不保值","dialogue":"哥,你不用担心小地块不保值,东大街的九龙仓擎天半岛只有两栋楼,现在二手房还能卖3万左右,是当年的豪宅项目。还有望江名门、仁和春天29号院,都是小地块但照样是高端保值盘。核心还是地段,我们在槐树店这个成华区最贵的板块,保值根本没问题!"}],"toneTags":{"enthusiasm":0.8,"patience":0.9,"confidence":0.85,"friendliness":0.8,"persuasion":0.75}}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := advicerService.AdvicerVersionUpdate(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_VersionList(t *testing.T) {
|
||||
reqBody := `{"id":"69804060c17976e5e21858a8"}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := advicerService.AdvicerVersionList(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_AdvicerVersionDel(t *testing.T) {
|
||||
reqBody := `{"id":"698056073059550befc4f0da"}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := advicerService.AdvicerVersionDel(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_Json(t *testing.T) {
|
||||
responseByte, err := os.ReadFile("./res.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
var (
|
||||
result map[string]interface{}
|
||||
res = make(map[string]mongo_model.AdviceData)
|
||||
)
|
||||
|
||||
if err = json.Unmarshal(responseByte, &result); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for k, v := range result {
|
||||
if _, ok := dataMap[k]; !ok {
|
||||
continue
|
||||
}
|
||||
var vbyte []byte
|
||||
if vbyte, err = json.Marshal(v); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newData := dataMap[k].Copy()
|
||||
|
||||
if err = json.Unmarshal(vbyte, newData); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
res[k] = newData
|
||||
}
|
||||
t.Log(result)
|
||||
}
|
||||
|
||||
var (
|
||||
fileService *FileService
|
||||
advicerService *AdvicerService
|
||||
adviceChatService *ChatService
|
||||
configConfig *config.Config
|
||||
fiberCtx *fiber.Ctx
|
||||
)
|
||||
|
||||
// run 函数是程序的入口函数,负责初始化和配置各个组件
|
||||
func Run(ctx context.Context, reqBody []byte) {
|
||||
if reqBody != nil {
|
||||
app := fiber.New()
|
||||
fctx := &fasthttp.RequestCtx{}
|
||||
fctx.Request.Header.SetMethod("POST")
|
||||
fctx.Request.SetBody(reqBody)
|
||||
fctx.Request.Header.SetContentType("application/json")
|
||||
fiberCtx = app.AcquireCtx(fctx)
|
||||
}
|
||||
configConfig, _ = config.LoadConfigWithEnv()
|
||||
// 初始化数据库连接
|
||||
db, _ := utils.NewGormDb(configConfig)
|
||||
rdb := utils.NewRdb(configConfig)
|
||||
advicerImpl := impl.NewAdviceAdvicerImpl(db)
|
||||
advicerVersionMongo := mongo_model.NewAdvicerVersionMongo()
|
||||
advicerChatHisMongo := mongo_model.NewAdvicerChatHisMongo()
|
||||
aiAdviceModelSupImpl := impl.NewAiAdviceModelSupImpl(db)
|
||||
aiAdviceSessionImpl := impl.NewAiAdviceSessionImpl(db)
|
||||
|
||||
advicerTalkSkillMongo := mongo_model.NewAdvicerTalkSkillMongo()
|
||||
advicerClientMongo := mongo_model.NewAdvicerClientMongo()
|
||||
advicerProjectMongo := mongo_model.NewAdvicerProjectMongo()
|
||||
hsyq := third_party.NewHsyq()
|
||||
|
||||
adviceProjectImpl := impl.NewAdviceProjectImpl(db)
|
||||
mongo, _ := pkg.NewMongoDb(ctx, configConfig)
|
||||
adviceAdvicerBiz := biz.NewAdviceAdvicerBiz(advicerImpl, advicerVersionMongo, mongo)
|
||||
adviceChatBiz := biz.NewAdviceChatBiz(hsyq, rdb, aiAdviceSessionImpl, aiAdviceModelSupImpl, advicerChatHisMongo, mongo)
|
||||
skillBiz := biz.NewAdviceSkillBiz(advicerTalkSkillMongo, mongo)
|
||||
clientBiz := biz.NewAdviceClientBiz(advicerClientMongo, mongo)
|
||||
adviceFileBiz := biz.NewAdviceFileBiz(hsyq)
|
||||
adviceProjectBiz := biz.NewAdviceProjectBiz(advicerProjectMongo, adviceProjectImpl, aiAdviceModelSupImpl, mongo)
|
||||
adviceClientBiz := biz.NewAdviceClientBiz(advicerClientMongo, mongo)
|
||||
adviceSkillBiz := biz.NewAdviceSkillBiz(advicerTalkSkillMongo, mongo)
|
||||
fileService = NewFileService(adviceFileBiz, configConfig, adviceProjectBiz)
|
||||
advicerService = NewAdvicerService(adviceAdvicerBiz, configConfig)
|
||||
skill = NewTalkSkillService(skillBiz, configConfig)
|
||||
client = NewClientService(clientBiz, configConfig)
|
||||
project = NewProjectService(adviceProjectBiz, configConfig)
|
||||
adviceChatService = NewChatService(adviceChatBiz, adviceClientBiz, adviceAdvicerBiz, adviceProjectBiz, adviceSkillBiz, configConfig)
|
||||
}
|
||||
|
||||
var dataMap = map[string]mongo_model.AdviceData{
|
||||
"DialectFeatures": &mongo_model.DialectFeatures{},
|
||||
"SentencePatterns": &mongo_model.SentencePatterns{},
|
||||
"PersonalityTags": &mongo_model.PersonalityTags{},
|
||||
"ToneTags": &mongo_model.ToneTags{},
|
||||
"SignatureDialogues": &mongo_model.SignatureDialogues{},
|
||||
"RegionValue": &mongo_model.RegionValue{},
|
||||
"CompetitionComparison": &mongo_model.CompetitionComparison{},
|
||||
"CoreSellingPoints": &mongo_model.CoreSellingPoints{},
|
||||
"SupportingFacilities": &mongo_model.SupportingFacilities{},
|
||||
"DeveloperBacking": &mongo_model.DeveloperBacking{},
|
||||
"NeedsMining": &mongo_model.NeedsMining{},
|
||||
"PainPointResponse": &mongo_model.PainPointResponse{},
|
||||
"ValueBuilding": &mongo_model.ValueBuilding{},
|
||||
"ClosingTechniques": &mongo_model.ClosingTechniques{},
|
||||
"CommunicationRhythm": &mongo_model.CommunicationRhythm{},
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/pkg"
|
||||
|
||||
"ai_scheduler/internal/data/mongo_model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
// FileService 文件处理
|
||||
type ChatService struct {
|
||||
adviceChatBiz *biz.AdviceChatBiz
|
||||
adviceClientBiz *biz.AdviceClientBiz
|
||||
adviceAdvicerBiz *biz.AdviceAdvicerBiz
|
||||
adviceProjectBiz *biz.AdviceProjectBiz
|
||||
adviceSkillBiz *biz.AdviceSkillBiz
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewFileService
|
||||
func NewChatService(
|
||||
adviceChatBiz *biz.AdviceChatBiz,
|
||||
adviceClientBiz *biz.AdviceClientBiz,
|
||||
adviceAdvicerBiz *biz.AdviceAdvicerBiz,
|
||||
adviceProjectBiz *biz.AdviceProjectBiz,
|
||||
adviceSkillBiz *biz.AdviceSkillBiz,
|
||||
cfg *config.Config,
|
||||
) *ChatService {
|
||||
return &ChatService{
|
||||
adviceChatBiz: adviceChatBiz,
|
||||
cfg: cfg,
|
||||
adviceClientBiz: adviceClientBiz,
|
||||
adviceAdvicerBiz: adviceAdvicerBiz,
|
||||
adviceProjectBiz: adviceProjectBiz,
|
||||
adviceSkillBiz: adviceSkillBiz,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ChatService) Regis(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerChatRegistReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(req.AdvicerVersionId) == 0 {
|
||||
return errorcode.ParamErr("AdvicerVersionId is empty")
|
||||
}
|
||||
if len(req.TalkSkillId) == 0 {
|
||||
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{
|
||||
Id: req.AdvicerVersionId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//顾问信息
|
||||
advicerInfo, err := a.adviceAdvicerBiz.AdvicerInfo(c.UserContext(), &entitys.AdvicerInfoReq{
|
||||
AdvicerID: versionInfo.AdvicerId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//项目信息
|
||||
projectInfo, err := a.adviceProjectBiz.Info(c.UserContext(), &entitys.AdvicerProjectInfoReq{
|
||||
ProjectId: advicerInfo.ProjectID,
|
||||
})
|
||||
if err != nil {
|
||||
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{
|
||||
Id: req.TalkSkillId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//客户信息
|
||||
var clientInfo mongo_model.AdvicerClientMongo
|
||||
if len(req.ClientId) != 0 {
|
||||
|
||||
clientInfo, err = a.adviceClientBiz.Info(c.UserContext(), &entitys.AdvicerClientInfoReq{
|
||||
Id: req.ClientId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
chat := entitys.ChatData{
|
||||
ClientInfo: clientInfo.Entity(),
|
||||
TalkSkill: talkSkill.Entity(),
|
||||
ProjectInfo: projectInfo.ConfigInfo.Entity(),
|
||||
AdvicerInfo: advicerInfo.Entity(),
|
||||
AdvicerVersion: versionInfo.Entity(),
|
||||
}
|
||||
sessionId, err := a.adviceChatBiz.Regis(c.UserContext(), &chat, req, projectInfo)
|
||||
|
||||
log.Info(sessionId)
|
||||
return pkg.HandleResponse(c, sessionId, err)
|
||||
}
|
||||
|
||||
func (a *ChatService) Chat(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)
|
||||
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)
|
||||
log.Info(res)
|
||||
return pkg.HandleResponse(c, res, err)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue