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`字段内容需符合对话语境
|
||||
|
|
@ -13,14 +13,14 @@ func main() {
|
|||
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")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
bc, err := config.LoadConfig(*configPath)
|
||||
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)
|
||||
}
|
||||
|
|
@ -33,6 +33,11 @@ func main() {
|
|||
if *cron == "start" {
|
||||
app.Cron.Run(ctx)
|
||||
}
|
||||
// 运行指定任务并退出
|
||||
if *runJob != "" {
|
||||
app.Cron.RunOnce(ctx, *runJob)
|
||||
return
|
||||
}
|
||||
|
||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,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"
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ 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"
|
||||
|
|
@ -50,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"
|
||||
|
|
@ -88,7 +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 ==
|
||||
|
|
@ -147,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"
|
||||
|
|
@ -169,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,10 +4,10 @@ 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"
|
||||
|
|
@ -52,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"
|
||||
|
|
@ -154,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"
|
||||
|
|
@ -179,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))
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//requireData.ID=requireData.UserInfo.UserID
|
||||
////如果不是管理或者不是老板,则进行权限判断
|
||||
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
|
||||
//
|
||||
//}
|
||||
//return
|
||||
// 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 {
|
||||
// if errors.Is(err, sql.ErrNoRows) {
|
||||
// entitys.ResText(requireData.Ch, "", "抱歉,我还是没有找到名为“"+sysName+"”的系统。请联系管理员确认系统名称。")
|
||||
// return nil
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // 2. 既然已经明确了系统,直接尝试拉群(这里假设问题类型为“其他”或由LLM再次分析)
|
||||
// // 为简化,这里再次调用分类逻辑,但带上已确定的系统
|
||||
// return d.fallbackToGroupCreationWithSys(ctx, requireData, &sys)
|
||||
// }
|
||||
|
||||
// getRecentUserHistory 获取最近的用户输入历史
|
||||
func (d *DingTalkBotBiz) getRecentUserHistory(ctx context.Context, conversationType constants.ConversationType, id int32, limit int) ([]model.AiBotChatHi, error) {
|
||||
var his []model.AiBotChatHi
|
||||
cond := builder.NewCond().
|
||||
And(builder.Eq{"his_type": conversationType}).
|
||||
And(builder.Eq{"id": id}).
|
||||
And(builder.Eq{"role": "user"})
|
||||
|
||||
_, err := d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: limit}, &his, "his_id desc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return his, nil
|
||||
}
|
||||
|
||||
// 在已知系统&问题类型的情况下进行分类并拉群
|
||||
func (d *DingTalkBotBiz) fallbackToGroupCreation(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, resolveResult *resolveSystemAndIssueTypeResult) error {
|
||||
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n检测到您想咨询 %s-%s 问题。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
|
||||
|
||||
// 查找分配规则
|
||||
rule, found, _ := d.issueImpl.IssueAssignRule.FindOne(
|
||||
d.issueImpl.WithSysID(resolveResult.Sys.SysID),
|
||||
d.issueImpl.WithIssueTypeID(resolveResult.IssueType.ID),
|
||||
d.issueImpl.WithStatus(1),
|
||||
)
|
||||
if !found {
|
||||
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n抱歉,当前系统未配置路由规则 %s-%s,请联系管理员配置。", resolveResult.Sys.SysName, resolveResult.IssueType.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
var groupMember, groupMemberName []string
|
||||
if rule.ID != 0 {
|
||||
// 获取配置的用户
|
||||
assignUsers, err := d.issueImpl.IssueAssignUser.FindAll(d.issueImpl.WithRuleID(rule.ID))
|
||||
if len(assignUsers) == 0 {
|
||||
log.Errorf("assign user not found for rule %d; err: %v", rule.ID, err)
|
||||
return fmt.Errorf("分配用户 %d 不存在", rule.ID)
|
||||
}
|
||||
userIds := slice.Map(assignUsers, func(_ int, au model.AiIssueAssignUser) int32 {
|
||||
return au.UserID
|
||||
})
|
||||
// 获取有效用户
|
||||
botUsers, err := d.botUserImpl.GetByUserIds(userIds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 仅获取有效用户的 staff_id
|
||||
for _, au := range assignUsers {
|
||||
botUser, found := slice.Find(botUsers, func(_ int, bu model.AiBotUser) bool {
|
||||
return bu.UserID == au.UserID
|
||||
})
|
||||
if found && botUser.StaffID != "" {
|
||||
groupMember = append(groupMember, botUser.StaffID)
|
||||
groupMemberName = append(groupMemberName, "@"+botUser.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底处理人
|
||||
if len(groupMember) == 0 {
|
||||
groupMember = []string{"17415698414368678"}
|
||||
}
|
||||
|
||||
// 合并提问者
|
||||
groupMember = append([]string{requireData.Req.SenderStaffId}, groupMember...)
|
||||
groupMember = slice.Unique(groupMember)
|
||||
|
||||
// 先回复用户
|
||||
entitys.ResText(requireData.Ch, "", fmt.Sprintf("\n已检索到处理人\n%s\n是否创建群聊?", strings.Join(groupMemberName, "、")))
|
||||
|
||||
// 发送确认卡片
|
||||
groupName := fmt.Sprintf("[%s]-%s", resolveResult.IssueType.Name, resolveResult.Classification.Summary)
|
||||
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
|
||||
RobotCode: requireData.Req.RobotCode,
|
||||
ConversationId: requireData.Req.ConversationId,
|
||||
SenderStaffId: requireData.Req.SenderStaffId,
|
||||
UserIds: groupMember,
|
||||
GroupName: groupName,
|
||||
Summary: resolveResult.Classification.Summary,
|
||||
})
|
||||
}
|
||||
|
||||
// createDefaultGroup 兜底发送确认卡片
|
||||
func (d *DingTalkBotBiz) createDefaultGroup(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, reason string) error {
|
||||
userIds := []string{requireData.Req.SenderStaffId, "17415698414368678"}
|
||||
groupName := fmt.Sprintf("[未知]-%s", reason)
|
||||
return d.SendGroupCreationConfirmCard(ctx, &SendGroupCreationConfirmCardParams{
|
||||
RobotCode: requireData.Req.RobotCode,
|
||||
ConversationId: requireData.Req.ConversationId,
|
||||
SenderStaffId: requireData.Req.SenderStaffId,
|
||||
UserIds: userIds,
|
||||
GroupName: groupName,
|
||||
Summary: reason,
|
||||
})
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||||
|
|
@ -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, // 用户输入内容
|
||||
|
|
@ -411,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,6 +31,7 @@ import (
|
|||
|
||||
"github.com/coze-dev/coze-go"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/ollama/ollama/api"
|
||||
"gorm.io/gorm/utils"
|
||||
)
|
||||
|
||||
|
|
@ -88,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
|
||||
|
|
@ -129,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:
|
||||
|
|
@ -166,81 +384,115 @@ func (r *Handle) handleTask(ctx context.Context, rec *entitys.Recognize, task *m
|
|||
}
|
||||
|
||||
// 知识库
|
||||
func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||
// func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||
|
||||
// var (
|
||||
// configData entitys.ConfigDataTool
|
||||
// sessionIdKnowledge string
|
||||
// query string
|
||||
// host string
|
||||
// )
|
||||
// err = json.Unmarshal([]byte(task.Config), &configData)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// ext, err := rec_extra.GetTaskRecExt(rec)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// // 通过session 找到知识库session
|
||||
// var has bool
|
||||
// if len(ext.Session) == 0 {
|
||||
// return errors.SessionNotFound
|
||||
// }
|
||||
// ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
|
||||
// if err != nil {
|
||||
// return
|
||||
// } else if !has {
|
||||
// return errors.SessionNotFound
|
||||
// }
|
||||
|
||||
// // 找到知识库的host
|
||||
// {
|
||||
// tool, exists := r.toolManager.GetTool(configData.Tool)
|
||||
// if !exists {
|
||||
// return fmt.Errorf("tool not found: %s", configData.Tool)
|
||||
// }
|
||||
|
||||
// if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
|
||||
// return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
|
||||
// } else {
|
||||
// host = knowledgeTool.GetConfig().BaseURL
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// // 知识库的session为空,请求知识库获取, 并绑定
|
||||
// if ext.SessionInfo.KnowlegeSessionID == "" {
|
||||
// // 请求知识库
|
||||
// if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 绑定知识库session,下次可以使用
|
||||
// ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
|
||||
// if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 用户输入解析
|
||||
// var ok bool
|
||||
// input := make(map[string]string)
|
||||
// if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
|
||||
// return
|
||||
// }
|
||||
// if query, ok = input["query"]; !ok {
|
||||
// return fmt.Errorf("query不能为空")
|
||||
// }
|
||||
|
||||
// ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
|
||||
// Session: ext.SessionInfo.KnowlegeSessionID,
|
||||
// ApiKey: ext.Sys.KnowlegeTenantKey,
|
||||
// Query: query,
|
||||
// }
|
||||
// rec.Ext = pkg.JsonByteIgonErr(ext)
|
||||
// // 执行工具
|
||||
// err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
|
||||
// return
|
||||
// }
|
||||
|
||||
// 知识库V2 - lightRAG自建
|
||||
func (r *Handle) handleKnowleV2(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||
// 获取用户session信息
|
||||
|
||||
var (
|
||||
configData entitys.ConfigDataTool
|
||||
sessionIdKnowledge string
|
||||
query string
|
||||
host string
|
||||
)
|
||||
err = json.Unmarshal([]byte(task.Config), &configData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ext, err := rec_extra.GetTaskRecExt(rec)
|
||||
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
|
||||
}
|
||||
|
|
@ -248,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" {
|
||||
|
|
@ -333,7 +646,7 @@ func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessio
|
|||
|
||||
func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) {
|
||||
// 获取创建者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
|
||||
|
|
|
|||
|
|
@ -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), // 助手回复内容
|
||||
Role: "assistant", // 助手角色
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
@ -235,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 {
|
||||
|
|
@ -269,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)
|
||||
}
|
||||
|
|
@ -426,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package biz
|
|||
import (
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/biz/llm_service"
|
||||
"ai_scheduler/internal/biz/llm_service/third_party"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
|
@ -21,4 +22,12 @@ var ProviderSetBiz = wire.NewSet(
|
|||
NewQywxAppBiz,
|
||||
NewGroupConfigBiz,
|
||||
do.NewMacro,
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
@ -71,10 +73,12 @@ type LLMCapabilityConfig struct {
|
|||
|
||||
// DingtalkConfig 钉钉配置
|
||||
type DingtalkConfig struct {
|
||||
ApiKey string `mapstructure:"api_key"`
|
||||
ApiSecret string `mapstructure:"api_secret"`
|
||||
TableDemand AITableConfig `mapstructure:"table_demand"`
|
||||
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 机器人群组
|
||||
ApiKey string `mapstructure:"api_key"`
|
||||
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"`
|
||||
|
|
@ -157,7 +189,8 @@ type Redis struct {
|
|||
}
|
||||
|
||||
type DB struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
Driver string `mapstructure:"driver"`
|
||||
|
||||
Source string `mapstructure:"source"`
|
||||
MaxIdle int32 `mapstructure:"maxIdle"`
|
||||
MaxOpen int32 `mapstructure:"maxOpen"`
|
||||
|
|
@ -165,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"`
|
||||
|
|
@ -253,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)
|
||||
|
|
@ -309,7 +366,7 @@ func LoadConfigWithEnv() (*Config, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viper.SetConfigFile(modularDir + "/config/config_env.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" // 知识库未命中
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,22 +10,20 @@ import (
|
|||
)
|
||||
|
||||
type ContactClient struct {
|
||||
config *config.Config
|
||||
cli *contact.Client
|
||||
cli *contact.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewContactClient(config *config.Config) (*ContactClient, error) {
|
||||
func NewContactClient(oauth2Client *Oauth2Client) (*ContactClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
c, err := contact.NewClient(cfg)
|
||||
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{
|
||||
FullMatchField: tea.Int32(1),
|
||||
QueryWord: tea.String(name),
|
||||
Offset: tea.Int32(0),
|
||||
Size: tea.Int32(1),
|
||||
}, headers, &util.RuntimeOptions{})
|
||||
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),
|
||||
},
|
||||
&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,22 +12,20 @@ import (
|
|||
)
|
||||
|
||||
type NotableClient struct {
|
||||
config *config.Config
|
||||
cli *notable.Client
|
||||
cli *notable.Client
|
||||
oauth2Client *Oauth2Client
|
||||
}
|
||||
|
||||
func NewNotableClient(config *config.Config) (*NotableClient, error) {
|
||||
func NewNotableClient(oauth2Client *Oauth2Client) (*NotableClient, error) {
|
||||
cfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(config.Tools.DingTalkBot.APIKey),
|
||||
AccessKeySecret: tea.String(config.Tools.DingTalkBot.APISecret),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
c, err := notable.NewClient(cfg)
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package server
|
|||
import (
|
||||
"ai_scheduler/internal/services"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
|
@ -20,6 +21,7 @@ type cronJob struct {
|
|||
EntryId int32
|
||||
Func func(context.Context) error
|
||||
Name string
|
||||
Key string
|
||||
Schedule string
|
||||
}
|
||||
|
||||
|
|
@ -42,11 +44,13 @@ func (c *CronServer) InitJobs(ctx context.Context) {
|
|||
{
|
||||
Func: c.cronService.CronReportSendDingTalk,
|
||||
Name: "直连天下报表推送(钉钉)",
|
||||
Key: "ding_report_dingtalk",
|
||||
Schedule: "20 12,18,23 * * *",
|
||||
},
|
||||
{
|
||||
Func: c.cronService.CronReportSendQywx,
|
||||
Name: "直连天下报表推送(微信)",
|
||||
Key: "ding_report_qywx",
|
||||
Schedule: "20 12,18,23 * * *",
|
||||
},
|
||||
}
|
||||
|
|
@ -96,3 +100,39 @@ func (c *CronServer) Stop() {
|
|||
c.log.Info("Cron调度器已停止")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CronServer) RunOnce(ctx context.Context, key string) error {
|
||||
|
||||
if c.jobs == nil {
|
||||
c.InitJobs(ctx)
|
||||
}
|
||||
|
||||
// 获取key对应的任务
|
||||
var job *cronJob
|
||||
for _, j := range c.jobs {
|
||||
if j.Key == key {
|
||||
job = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if job == nil {
|
||||
return fmt.Errorf("unknown job key: %s\n", key)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("任务[once]:%s执行时发生panic: %v\n", job.Name, r)
|
||||
}
|
||||
fmt.Printf("任务[once]:%s执行结束\n", job.Name)
|
||||
}()
|
||||
|
||||
fmt.Printf("任务[once]:%s开始执行\n", job.Name)
|
||||
|
||||
err := job.Func(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("任务[once]:%s执行失败: %s\n", job.Name, err.Error())
|
||||
}
|
||||
|
||||
fmt.Printf("任务[once]:%s执行成功\n", job.Name)
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,22 +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
|
||||
}
|
||||
|
||||
func NewHTTPServer(
|
||||
service *services.ChatService,
|
||||
session *services.SessionService,
|
||||
|
|
@ -28,10 +19,16 @@ func NewHTTPServer(
|
|||
callback *services.CallbackService,
|
||||
chatHis *services.HistoryService,
|
||||
capabilityService *services.CapabilityService,
|
||||
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)
|
||||
router.SetupRoutes(app, service, session, task, gateway, callback, chatHis, capabilityService, adviceFile, adviceData, adviceChat, adviceProject, adviceTalkSkill, adviceClient)
|
||||
return app
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
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"
|
||||
"strings"
|
||||
|
|
@ -14,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,
|
||||
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 头
|
||||
|
|
@ -68,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 {
|
||||
|
|
@ -98,6 +96,40 @@ 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) // 商品数据提取确认
|
||||
|
||||
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)
|
||||
|
||||
//项目
|
||||
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)
|
||||
|
||||
//客户
|
||||
advicer.Post("client/add", adviceClient.Add)
|
||||
advicer.Post("client/update", adviceClient.Update)
|
||||
advicer.Post("client/list", adviceClient.List)
|
||||
advicer.Post("client/del", adviceClient.Del)
|
||||
|
||||
//会话
|
||||
advicer.Post("chat/regis", adviceChat.Regis)
|
||||
advicer.Post("chat/chat", adviceChat.Chat)
|
||||
|
||||
}
|
||||
|
||||
func routerSocket(app *fiber.App, chatService *services.ChatService) {
|
||||
|
|
@ -134,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(),
|
||||
|
|
@ -152,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
|
|
@ -0,0 +1,62 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ClientService 数据处理
|
||||
type ClientService struct {
|
||||
AdviceClientBiz *biz.AdviceClientBiz
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewDataService
|
||||
func NewClientService(
|
||||
AdviceClientBiz *biz.AdviceClientBiz,
|
||||
cfg *config.Config,
|
||||
) *ClientService {
|
||||
return &ClientService{
|
||||
AdviceClientBiz: AdviceClientBiz,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ClientService) Add(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerClientAddReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := d.AdviceClientBiz.Add(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, id, err)
|
||||
}
|
||||
|
||||
func (d *ClientService) Update(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerrClientUpdateReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.AdviceClientBiz.Update(c.UserContext(), req)
|
||||
}
|
||||
|
||||
func (d *ClientService) List(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerClientListReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := d.AdviceClientBiz.List(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, list, err)
|
||||
}
|
||||
|
||||
func (d *ClientService) Del(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerClientDelReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.AdviceClientBiz.Del(c.UserContext(), req)
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ClientAdd(t *testing.T) {
|
||||
reqBody := `{"projectId":1,"AdvicerId":1,"personalInfo":{"name":"杜先生","gender":"男","location":"","isFirstHome":false,"familyOrganize":"夫妻+2孩"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"450-500万","preferredLayout":"140㎡四房三卫","coreAppeal":"户型实用、景观好、社区品质高"},"concerns":["小区小是否保值","价格是否有优惠","开发商交付能力"],"decisionProfile":["注重户型实用性和景观","关注社区品质和后期保值","对价格敏感,希望拿到优惠"]}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := client.Add(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_ClientUpdate(t *testing.T) {
|
||||
reqBody := `{"id":"698199fa0c5f4ae098e009ab","projectId":1,"AdvicerId":1,"personalInfo":{"name":"唐先生1","gender":"男","location":"成都北门","isFirstHome":false,"familyOrganize":"夫妻+1孩+父母同住"},"purchasePurpose":{"primaryPurpose":"改善居住条件","secondaryPurpose":"资产保值,方便子女上学","decisionMakers":"夫妻双方"},"coreDemands":{"totalBudget":"350-400万","preferredLayout":"118㎡四房三卫双套房","coreAppeal":"在预算内满足家庭居住功能,确保房产保值,临近学校"},"concerns":["总价超预算风险","板块保值能力","小区小是否影响居住体验","开发商资金实力"],"decisionProfile":["预算导向,严格控制总价","重点关注户型功能性和实用性","需要对比板块发展潜力","对开发商交付能力有顾虑"]}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := client.Update(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_ClientList(t *testing.T) {
|
||||
reqBody := `{"projectId":1}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := client.List(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_ClientDel(t *testing.T) {
|
||||
reqBody := `{"id":"698056073059550befc4f0da"}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := advicerService.AdvicerVersionDel(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
var (
|
||||
client *ClientService
|
||||
)
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/file_download"
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"net/url"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// FileService 文件处理
|
||||
type FileService struct {
|
||||
adviceBiz *biz.AdviceFileBiz
|
||||
cfg *config.Config
|
||||
adviceProjectBiz *biz.AdviceProjectBiz
|
||||
}
|
||||
|
||||
// NewFileService
|
||||
func NewFileService(
|
||||
adviceBiz *biz.AdviceFileBiz,
|
||||
cfg *config.Config,
|
||||
adviceProjectBiz *biz.AdviceProjectBiz,
|
||||
) *FileService {
|
||||
return &FileService{
|
||||
adviceBiz: adviceBiz,
|
||||
cfg: cfg,
|
||||
adviceProjectBiz: adviceProjectBiz,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *FileService) WordAna(c *fiber.Ctx) error {
|
||||
req := &entitys.WordAnaReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ProjectId == 0 {
|
||||
return errorcode.ParamErr("ProjectId is empty")
|
||||
}
|
||||
//项目信息
|
||||
projectInfo, err := a.adviceProjectBiz.Info(c.UserContext(), &entitys.AdvicerProjectInfoReq{
|
||||
ProjectId: req.ProjectId,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// URL 解码
|
||||
fileURL, err := url.PathUnescape(req.WordFileUrl)
|
||||
if err != nil {
|
||||
return errors.New("URL 解码失败")
|
||||
}
|
||||
result, _, err := file_download.GetWordTextFromURL(fileURL, file_download.IsWordFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ana, err := a.adviceBiz.WordAna(context.Background(), result, projectInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = c.JSON(ana)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *FileService) WordAnat(path string) ([]byte, error) {
|
||||
|
||||
// URL 解码
|
||||
projectInfo, err := a.adviceProjectBiz.Info(context.Background(), &entitys.AdvicerProjectInfoReq{
|
||||
ProjectId: 127,
|
||||
})
|
||||
fileURL, err := url.PathUnescape(path)
|
||||
if err != nil {
|
||||
return nil, errors.New("URL 解码失败")
|
||||
}
|
||||
result, _, err := file_download.GetWordTextFromURL(fileURL, file_download.IsWordFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ana, err := a.adviceBiz.WordAna(context.Background(), result, projectInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pkg.JsonByteIgonErr(ana), err
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// ProjectService 数据处理
|
||||
type ProjectService struct {
|
||||
adviceProjectBiz *biz.AdviceProjectBiz
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewProjectService
|
||||
func NewProjectService(
|
||||
adviceProjectBiz *biz.AdviceProjectBiz,
|
||||
cfg *config.Config,
|
||||
) *ProjectService {
|
||||
return &ProjectService{
|
||||
adviceProjectBiz: adviceProjectBiz,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ProjectService) BaseInit(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerProjectBaseAddReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
add, err := d.adviceProjectBiz.BaseAdd(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, add, err)
|
||||
}
|
||||
|
||||
func (d *ProjectService) BaseUpdate(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerProjectBaseUpdateReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.ProjectId == 0 {
|
||||
return errorcode.ParamErr("projectId is empty")
|
||||
}
|
||||
return d.adviceProjectBiz.BaseUpdate(c.UserContext(), req)
|
||||
}
|
||||
|
||||
func (d *ProjectService) Add(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerProjectAddReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := d.adviceProjectBiz.Add(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, id, err)
|
||||
}
|
||||
|
||||
func (d *ProjectService) Update(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerrProjectUpdateReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.adviceProjectBiz.Update(c.UserContext(), req)
|
||||
}
|
||||
|
||||
func (d *ProjectService) Info(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerProjectInfoReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := d.adviceProjectBiz.Info(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, list, err)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_ProjectAdd(t *testing.T) {
|
||||
reqBody := `{"projectId":1,"projectInfo":{"projectName":"中信资产项目 *","projectAddress":"成华区槐树店板块,2.5环内侧 *","area":"成华区","houseTypes":[{"name":"118平户型","buildArea":"118㎡","innerArea":"132㎡ *","unitPrice":"约33000元/㎡ * (套内单价)","totalPrice":"约389万元 *"},{"name":"核心主力户型 *","buildArea":"120-140㎡ *","innerArea":"高得房率,约132-156㎡ *","unitPrice":"约32000-35000元/㎡ *","totalPrice":"约384-490万元 *"},{"name":"大平层户型 *","buildArea":"140㎡+ *","innerArea":"实得约156㎡+ * (得房率110%+)","unitPrice":"约35000元/㎡ *","totalPrice":"约490万元+ *"}]},"competitionComparison":{"华润置地云上":{"价格对比":"他们单价35000左右,还靠近安置小区","优势突出":"我们小区纯粹,没有安置小区,居住环境更安静","优点承认":"华润品牌影响力大"},"招商璟宸序":{"价格对比":"他们单价32000左右,但地段在28板块,地价比我们便宜5000","优势突出":"我们地段在槐树店,是成华区核心板块,未来增值空间更大","优点承认":"招商品牌大,物业也是自己的"},"龙湖滨江云河颂":{"价格对比":"他们单价32000-35000,但得房率只有95%,套内算下来36000+","优势突出":"我们118平实得132平,套内单价才33000,还做四房三卫,他们143平才双卫","优点承认":"龙湖位置确实好,能看沙河公园"}},"coreSellingPoints":{"产品配置高端":"3.2米层高、全玻璃幕墙+三层中空玻璃、无机磨石车库、方太Y9烟机、高仪卫浴、国千木作柜体","地段稀缺性":"成华区2.5环内侧槐树店板块,成华区房价天花板区域,被万象城、339、火车东站包围","得房率高":"118平实得132平,得房率超110%,四房三卫双套房设计,市面上同面积段没有竞品","物业优质":"招商局铂金物业,有夜间送外卖、免费宠物喂养、全屋保洁等增值服务"},"developerBacking":{"公司实力":"中信资产,多元化民营企业,涉及矿产、有色金属、生态农业、地产开发","合作方":"招商局物业,百年央企,首次与外部企业合作提供铂金服务","开发经验":"2011年开始做地产,在宜宾、贵州开发超过500万平米,成都是首个项目","资金安全":"在河南渑池有两座优质铝矿,每年稳定收入10亿,现金流充足"},"regionValue":{"区位层级":["成华区2.5环内侧,槐树店板块是成华区number one板块","北接339商圈,西靠万象城,东临火车东站","属于槐树店崔怀板块,成华区目前最好的开发板块"],"发展规划":["槐树店是成华区未来的富人区,板块还有大量待开发土地","未来这个区域会形成连片高端居住区,城市界面会越来越好"],"地价论证":["我们地价19500,比28板块贵5000多","华润华城府地价20400,我们和它同属一个板块,地价差距小","面粉都这么贵,面包不可能便宜"],"板块热度":["从2021年新希望锦麟一品开始,这边全是高端盘","龙湖最高端的滨江云河颂在这里,卖得特别火","各大品牌开发商都在这边拿地,未来全是改善盘"]},"supportingFacilities":{"交通配套":{"地铁":"7号线双店路站350米,4号线槐树店站550米,未来还有12号线","通达性":"到万象城2个站,到华西锦江院区30分钟车程","道路":"中环路、成洛大道,到春熙路5个站,到火车东站2个站"},"医疗配套":{"三甲医院":"市六医院、市二医院3公里内","顶尖医疗":"华西医院锦江院区、华西本部30分钟车程"},"商业配套":{"便利性":"到万象城2个站,到339商圈3个站","核心商圈":"万象城商圈、339商圈","社区商业":"和悦广场、东方希望上东里"},"教育配套":{"小学":"成华小学,1-3年级在项目附近,4-6年级在二环内","生源优势":"周边新盘都是300万+,生源纯粹"}}}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := project.Add(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_ProjectUpdate(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 := project.Update(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_ProjectInfo(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 := project.Info(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
var project *ProjectService
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// TalkSkillService 数据处理
|
||||
type TalkSkillService struct {
|
||||
adviceSkillBiz *biz.AdviceSkillBiz
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewTalkSkillService
|
||||
func NewTalkSkillService(
|
||||
adviceSkillBiz *biz.AdviceSkillBiz,
|
||||
cfg *config.Config,
|
||||
) *TalkSkillService {
|
||||
return &TalkSkillService{
|
||||
adviceSkillBiz: adviceSkillBiz,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *TalkSkillService) TalkSkillAdd(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerTalkSkillAddReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := d.adviceSkillBiz.VersionAdd(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, id, err)
|
||||
}
|
||||
|
||||
func (d *TalkSkillService) TalkSkillUpdate(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerTalkSkillUpdateReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.adviceSkillBiz.VersionUpdate(c.UserContext(), req)
|
||||
}
|
||||
|
||||
func (d *TalkSkillService) TalkSkillList(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerTalkSkillListReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
list, err := d.adviceSkillBiz.VersionList(c.UserContext(), req)
|
||||
return pkg.HandleResponse(c, list, err)
|
||||
}
|
||||
|
||||
func (d *TalkSkillService) TalkSkillDel(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerTalkSkillDelReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.adviceSkillBiz.VersionDel(c.UserContext(), req)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package advice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_TalkSkillAdd(t *testing.T) {
|
||||
reqBody := `{"advicerId":124,"projectId":1,"desc":"第一版本","closingTechniques":{"优惠策略":{"价格优惠":["今天定的话,我可以跟领导申请额外1个点的折扣","买车位的话,总价再给你优惠2万块","一次性付款的话,还能再降1个点"],"附加价值":["送一年物业费","送品牌家电礼包","优先选车位"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠我第一时间通知你","今天不定的话,我帮你留意着这个好楼层"]},"紧迫感营造":{"房源稀缺":["118的户型只剩20多套了,好楼层只有这几套了","这栋楼一共就60户,卖一套少一套,现在不订可能就没了"],"时间紧迫":["今天是周末活动最后一天,这个价格只有今天能申请","月底冲业绩,领导给的权限最大,过了今天就没这个优惠了"]}},"communicationRhythm":{"开场阶段":{"关键动作":"亲切称呼,简单寒暄,确认客户关注点","时间占比":"5%","目标":"建立关系,了解需求"},"样板间带看":{"关键动作":"讲解户型功能→展示装修标准→强调细节品质","时间占比":"40%","目标":"体验产品优势"},"沙盘讲解":{"关键动作":"板块价值→周边配套→项目亮点→开发商介绍","时间占比":"30%","目标":"建立价值认知"},"洽谈阶段":{"关键动作":"算价格→对比竞品→解决顾虑→逼定成交","时间占比":"25%","目标":"促单成交"}},"needsMining":{"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?"],"教育需求":["小孩在哪里上学?对学校距离有要求吗?","看重学校的哪些方面?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"]},"painPointResponse":{"小区太小":{"对比竞品":"仁和春天29号院才29亩,照样是千万级豪宅","承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩,339的邦泰才11亩","转化优势":"但小区人少安静,楼间距开阔,200多米的楼间距比很多大楼盘还宽"},"担心保值":{"举例论证":"你看九龙仓擎天半岛,就两栋楼,现在二手房还是卖3万多;望江名门一栋楼,照样是千万级豪宅","承认顾虑":"我理解你担心小小区不保值","核心逻辑":"保值看的是地段,我们槐树店是成华区核心板块,地价19500,未来只会涨不会跌"},"物业费高":{"价值分析":"但6块里有2块是增值服务,招商物业是铂金服务,这些服务外面花钱都买不来","价格补贴":"前三年开发商补贴1块,你只需要交5块,和其他改善盘差不多","理解感受":"我懂你觉得6块有点贵"}},"valueBuilding":{"产品价值塑造":["我们是用改善的价格,买豪宅的配置","3.2米层高、全落地窗、无机磨石车库,这些都是千万级豪宅的标配","118平实得132平,得房率超过110%,市面上找不到第二家"],"地段价值塑造":["买房最重要的是地段、地段、还是地段","2.5环内的核心地段卖一块少一块,不可再生","槐树店是成华区房价天花板,买这里的房子保值有保障"]}}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := skill.TalkSkillAdd(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_TalkSkillUpdate(t *testing.T) {
|
||||
reqBody := `{"id":"698063ff5215bdb9c6344e88","advicerId":124,"projectId":3,"desc":"第0版本","closingTechniques":{"优惠策略":{"价格优惠":["双十一特价,118㎡优惠后360-400万,140㎡优惠后450-500万","渠道客户可额外申请优惠,相当于多一个点左右的优惠"],"附加价值":["车位双十一特惠,5.3米长车位9.8万,5.1米长车位8.8万"]},"决策推动":{"小步推进":["要不先交个小定保留房源?","可以先排个号,有优惠优先通知你","今天不定的话,我帮你留意好楼层"]},"紧迫感营造":{"房源稀缺":["118㎡只剩部分楼层,140㎡只有二十多套公园景观房","好楼层卖一套少一套,性价比高的楼层不多了"],"时间紧迫":["现在是双十一/年底冲刺,有特价优惠","优惠是阶段性的,错过就没有了"]}},"communicationRhythm":{"开场阶段":{"关键动作":"亲切称呼,简单寒暄,确认看房重点","时间占比":"5%","目标":"建立关系,了解需求"},"样板间带看":{"关键动作":"细节讲解→户型优势→空间体验→竞品对比","时间占比":"40%","目标":"强化产品感知"},"沙盘讲解":{"关键动作":"板块价值→周边配套→项目亮点→开发商介绍","时间占比":"30%","目标":"建立价值认知"},"洽谈阶段":{"关键动作":"需求匹配→痛点应对→优惠释放→决策推动","时间占比":"25%","目标":"解决顾虑,促进成交"}},"needsMining":{"居住需求":["几个人住?有老人小孩吗?","主要是自住还是考虑投资?","现在住哪里?想改善哪些方面?","对房间数量、卫生间数量有要求吗?"],"通勤需求":["在哪个位置上班?","主要开车还是坐地铁?","对地铁距离有要求吗?"],"预算需求":["你们总价想控制在多少以内?","是考虑按揭还是一次性?","月供能接受多少范围?"]},"painPointResponse":{"地块太小":{"对比竞品":"339的邦泰才11亩,人家是千万级豪宅","承认事实":"14亩确实不大","普遍现象":"2.5环内都是小地块,万景13亩,中铁建8.8亩","转化优势":"但人少安静,圈层更纯粹,楼间距反而更开阔"},"客户质疑开发商实力":{"合作背书":"招商物业首次外部合作,品牌物业认可开发商实力","实力展示":"公司有6000万吨铝矿,年稳定收入10亿,现金流雄厚","开发经验":"做房地产14年,在宜宾、贵州开发超500万平米项目"},"担心南侧住宅用地遮挡阳光":{"澄清方向":"我们主采光面朝南,南侧住宅用地规划会错开楼间距,不会遮挡","竞品类比":"南侧用地会做高端大户型,开发商会考虑业主采光,不会影响我们的日照"},"担心新小区不保值":{"产品稀缺":"新规产品得房率高,未来政策限制赠送,产品竞争力强","地段支撑":"槐树店是成华区地价最高的板块,周边都是高端项目,地价和高端项目带动房价保值","需求保障":"未来大量业主会置换新规产品,该板块是首选,供需决定价值"},"物业费高":{"价值分析":"但6块里1块是增值服务(保洁、送外卖、宠物服务)","价格补贴":"前三年补贴到5块,跟其他盘差不多","未来可协商":"后期业主委员会可以协商调整物业费,仁恒滨河湾就从7.9谈到5块","理解感受":"我懂你,我们也觉得有点贵"}},"valueBuilding":{"产品价值塑造":["我们是用改善的价格,买豪宅的标准","很多细节都是千万级豪宅才有的配置","外立面成本比竞品高,单价却相当","3.2米层高、无机磨石车库这些都是高端配置"],"地段价值塑造":["买房最重要的是地段、地段、还是地段","核心地段的核心资产才保值增值","2.5环内的地卖一块少一块,不可再生","槐树店是成华区地价最高的板块,地价高对应房价支撑强"]}}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := skill.TalkSkillUpdate(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_TalkSkillList(t *testing.T) {
|
||||
reqBody := `{"projectId":1}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := skill.TalkSkillList(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
func Test_TalkSkillDel(t *testing.T) {
|
||||
reqBody := `{"id":"698056073059550befc4f0da"}`
|
||||
Run(context.Background(), []byte(reqBody))
|
||||
err := skill.TalkSkillDel(fiberCtx)
|
||||
t.Log(err)
|
||||
}
|
||||
|
||||
var (
|
||||
skill *TalkSkillService
|
||||
)
|
||||
|
||||
// run 函数是程序的入口函数,负责初始化和配置各个组件
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
|
|
@ -19,6 +20,8 @@ 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"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
|
@ -30,17 +33,39 @@ type CallbackService struct {
|
|||
dingtalkOldClient *dingtalk.OldClient
|
||||
dingtalkContactClient *dingtalk.ContactClient
|
||||
dingtalkNotableClient *dingtalk.NotableClient
|
||||
dingtalkCardClient *dingtalk.CardClient
|
||||
callbackManager callback.Manager
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz
|
||||
callbackBiz *biz.CallbackBiz
|
||||
// ollamaClient *utils_ollama.Client
|
||||
// botConfigImpl *impl.BotConfigImpl
|
||||
}
|
||||
|
||||
func NewCallbackService(cfg *config.Config, gateway *gateway.Gateway, dingtalkOldClient *dingtalk.OldClient, dingtalkContactClient *dingtalk.ContactClient, dingtalkNotableClient *dingtalk.NotableClient, callbackManager callback.Manager) *CallbackService {
|
||||
func NewCallbackService(
|
||||
cfg *config.Config,
|
||||
gateway *gateway.Gateway,
|
||||
dingtalkOldClient *dingtalk.OldClient,
|
||||
dingtalkContactClient *dingtalk.ContactClient,
|
||||
dingtalkNotableClient *dingtalk.NotableClient,
|
||||
dingtalkCardClient *dingtalk.CardClient,
|
||||
callbackManager callback.Manager,
|
||||
dingTalkBotBiz *biz.DingTalkBotBiz,
|
||||
callbackBiz *biz.CallbackBiz,
|
||||
// ollamaClient *utils_ollama.Client,
|
||||
// botConfigImpl *impl.BotConfigImpl,
|
||||
) *CallbackService {
|
||||
return &CallbackService{
|
||||
cfg: cfg,
|
||||
gateway: gateway,
|
||||
dingtalkOldClient: dingtalkOldClient,
|
||||
dingtalkContactClient: dingtalkContactClient,
|
||||
dingtalkNotableClient: dingtalkNotableClient,
|
||||
dingtalkCardClient: dingtalkCardClient,
|
||||
callbackManager: callbackManager,
|
||||
dingTalkBotBiz: dingTalkBotBiz,
|
||||
callbackBiz: callbackBiz,
|
||||
// ollamaClient: ollamaClient,
|
||||
// botConfigImpl: botConfigImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +295,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
|
|||
|
||||
// 获取创建者uid
|
||||
accessToken, _ := s.dingtalkOldClient.GetAccessToken()
|
||||
creatorId, err := s.dingtalkContactClient.SearchUserOne(accessToken, data.Creator)
|
||||
creatorId, err := s.dingtalkContactClient.SearchUserOne(dingtalk.AppKey{AccessToken: accessToken}, data.Creator)
|
||||
if err != nil {
|
||||
return "", errorcode.ParamErrf("invalid data type: %v", err)
|
||||
}
|
||||
|
|
@ -286,7 +311,7 @@ func (s *CallbackService) handleBugOptimizationSubmitUpdate(ctx context.Context,
|
|||
unionId := userDetails.UnionID
|
||||
|
||||
// 更新记录
|
||||
ok, err := s.dingtalkNotableClient.UpdateRecord(accessToken, &dingtalk.UpdateRecordReq{
|
||||
ok, err := s.dingtalkNotableClient.UpdateRecord(dingtalk.AppKey{AccessToken: accessToken}, &dingtalk.UpdateRecordReq{
|
||||
BaseId: data.BaseId,
|
||||
SheetId: data.SheetId,
|
||||
RecordId: data.RecordId,
|
||||
|
|
@ -364,3 +389,76 @@ func getString(str, endstr string, start int, msg *string) int {
|
|||
*msg = str[start:end]
|
||||
return end + len(endstr)
|
||||
}
|
||||
|
||||
// CallbackDingtalkRobot 钉钉机器人回调
|
||||
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-robot
|
||||
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
|
||||
func (s *CallbackService) CallbackDingtalkRobot(c *fiber.Ctx) (err error) {
|
||||
// 获取body中的参数
|
||||
body := c.Request().Body()
|
||||
var data chatbot.BotCallbackDataModel
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return fmt.Errorf("invalid body: %v", err)
|
||||
}
|
||||
|
||||
// token 校验 ? token 好像没带?
|
||||
|
||||
// 通过机器人ID路由到不同能力
|
||||
switch data.RobotCode {
|
||||
case s.cfg.Dingtalk.SceneGroup.GroupTemplateRobotIDIssueHandling:
|
||||
// 问题处理群机器人
|
||||
// err := s.issueHandling(data)
|
||||
err := s.callbackBiz.IssueHandlingGroup(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("IssueHandlingGroup failed: %v", err)
|
||||
}
|
||||
default:
|
||||
// 其他机器人
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallbackDingtalkCard 处理钉钉卡片回调
|
||||
// 钉钉 callbackRouteKey: gateway.dev.cdlsxd.cn-dingtalk-card
|
||||
// 钉钉 apiSecret: aB3dE7fG9hI2jK4L5M6N7O8P9Q0R1S2T
|
||||
func (s *CallbackService) CallbackDingtalkCard(c *fiber.Ctx) error {
|
||||
// 获取body中的参数
|
||||
body := c.Request().Body()
|
||||
|
||||
// HTTP 回调结构与SDK结构体不符,包装结构体
|
||||
tmp := struct {
|
||||
card.CardRequest // 嵌入原结构体
|
||||
UserIdType util.FlexibleType `json:"userIdType"` // 重写type字段
|
||||
}{}
|
||||
if err := json.Unmarshal(body, &tmp); err != nil {
|
||||
return fmt.Errorf("invalid body: %v", err)
|
||||
}
|
||||
// 异常字段覆盖
|
||||
data := tmp.CardRequest
|
||||
data.UserIdType = tmp.UserIdType.Int()
|
||||
if err := json.Unmarshal([]byte(data.Content), &data.CardActionData); err != nil {
|
||||
return fmt.Errorf("invalid content: %v", err)
|
||||
}
|
||||
log.Debug("body:%s", string(body))
|
||||
|
||||
// 非回调类型不处理
|
||||
if data.Type != constants.CardActionCallbackTypeAction {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理卡片回调
|
||||
var resp *card.CardResponse
|
||||
for _, actionId := range data.CardActionData.CardPrivateData.ActionIdList {
|
||||
switch actionId {
|
||||
case "collect_qa":
|
||||
// 问题处理群机器人 QA 收集
|
||||
resp = s.callbackBiz.IssueHandlingCollectQA(data)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳过响应包装
|
||||
c.Locals("skip_response_wrap", true)
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue