Compare commits
No commits in common. "master" and "feature/v3-fzy" have entirely different histories.
master
...
feature/v3
|
|
@ -4,6 +4,4 @@
|
|||
docs
|
||||
cmd/server/wire_gen.go
|
||||
__debug*
|
||||
.bin/
|
||||
.idea/
|
||||
cache/
|
||||
.bin/
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
## 使用官方Go镜像作为构建环境
|
||||
FROM golang:1.24.7-alpine AS builder
|
||||
FROM golang:1.24.1-alpine AS builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
[https://p6-img.searchpstatp.com/tos-cn-i-vvloioitz3/6e5e76d274df2efabde9194a06f97e89~tplv-vvloioitz3-6:190:124.jpeg]
|
||||
|
||||
|
||||

|
||||
|
|
@ -10,12 +10,9 @@ import (
|
|||
)
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "./config/config.yaml", "Path to configuration file")
|
||||
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
||||
onBot := flag.String("bot", "", "bot start")
|
||||
cron := flag.String("cron", "", "close")
|
||||
runJob := flag.String("runJob", "", "run single job and exit")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
bc, err := config.LoadConfig(*configPath)
|
||||
if err != nil {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
|
|
@ -28,17 +25,7 @@ func main() {
|
|||
defer func() {
|
||||
cleanup()
|
||||
}()
|
||||
//钉钉机器人
|
||||
app.DingBotServer.Run(ctx, *onBot)
|
||||
//定时任务 - 测试环境不启用
|
||||
if *cron == "start" {
|
||||
app.Cron.Run(ctx)
|
||||
}
|
||||
// 运行指定任务并退出
|
||||
if *runJob != "" {
|
||||
app.Cron.RunOnce(ctx, *runJob)
|
||||
return
|
||||
}
|
||||
app.DingBotServer.Run(context.Background(), *onBot)
|
||||
|
||||
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,18 +6,14 @@ package main
|
|||
import (
|
||||
"ai_scheduler/internal/biz"
|
||||
"ai_scheduler/internal/biz/handle/dingtalk"
|
||||
"ai_scheduler/internal/biz/handle/qywx"
|
||||
"ai_scheduler/internal/biz/tools_regis"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"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"
|
||||
|
||||
// "ai_scheduler/internal/tool_callback"
|
||||
"ai_scheduler/internal/tool_callback"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/utils"
|
||||
|
||||
|
|
@ -37,11 +33,8 @@ func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), erro
|
|||
impl.ProviderImpl,
|
||||
utils.ProviderUtils,
|
||||
dingtalk.ProviderSetDingTalk,
|
||||
qywx.ProviderSetQywx,
|
||||
tools_regis.ProviderToolsRegis,
|
||||
// tool_callback.ProviderSetCallBackTools,
|
||||
component.ProviderSet,
|
||||
repo.ProviderSet,
|
||||
tool_callback.ProviderSetCallBackTools,
|
||||
))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,59 +4,26 @@ server:
|
|||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://192.168.6.115:11434"
|
||||
model: "qwen3:8b"
|
||||
generate_model: "qwen3:8b"
|
||||
mapping_model: "qwen3:8b"
|
||||
# model: "qwen3-coder:480b-cloud"
|
||||
# generate_model: "qwen3-coder:480b-cloud"
|
||||
# mapping_model: "deepseek-v3.2:cloud"
|
||||
base_url: "http://127.0.0.1:11434"
|
||||
model: "qwen3-coder:480b-cloud"
|
||||
generate_model: "qwen3-coder:480b-cloud"
|
||||
vl_model: "qwen2.5vl:3b"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
coze:
|
||||
base_url: "https://api.coze.cn"
|
||||
|
||||
lsxd:
|
||||
# 统一登录
|
||||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
code: "456789"
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
|
||||
|
||||
sys:
|
||||
session_len: 6
|
||||
channel_pool_len: 100
|
||||
channel_pool_size: 32
|
||||
llm_pool_len: 5
|
||||
heartbeat_interval: 300
|
||||
key: report-api
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
tls: 30
|
||||
db:
|
||||
redis:
|
||||
host: 47.97.27.195:6379
|
||||
type: node
|
||||
pass: lansexiongdi@666
|
||||
key: ai_scheduler_prov
|
||||
key: report-api
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
|
|
@ -65,12 +32,6 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
bucket: "attachment-public"
|
||||
domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com"
|
||||
endpoint: "https://oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
tools:
|
||||
zltxOrderDetail:
|
||||
|
|
@ -103,81 +64,7 @@ tools:
|
|||
zltxOrderAfterSaleResellerBatch:
|
||||
enabled: true
|
||||
base_url: "https://revcl.1688sup.com/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:
|
||||
# 货易通商品上传
|
||||
hytProductUpload:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/supplier/batch/add/complete"
|
||||
add_url: "https://hyt.86698.cn/#/goods/goodsManage"
|
||||
# 货易通供应商查询
|
||||
hytSupplierSearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/supplier/list"
|
||||
# 货易通仓库查询
|
||||
hytWarehouseSearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/warehouse/list"
|
||||
# 货易通商品添加
|
||||
hytGoodsAdd:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/add"
|
||||
add_url: "https://hyt.86698.cn/#/goods/goodsManage"
|
||||
# 货易通商品图片添加
|
||||
hytGoodsMediaAdd:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/media/add/batch"
|
||||
# 货易通商品分类添加
|
||||
hytGoodsCategoryAdd:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/good/category/relation/add"
|
||||
# 货易通商品分类查询
|
||||
hytGoodsCategorySearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/category/list"
|
||||
# 货易通商品品牌查询
|
||||
hytGoodsBrandSearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/brand/list"
|
||||
# == 电商充值系统 ==
|
||||
# 我们的商品统计
|
||||
rechargeStatisticsOursProduct:
|
||||
base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct"
|
||||
# == 通用工具 ==
|
||||
# 表格转图片
|
||||
excel2pic:
|
||||
base_url: "http://192.168.6.115:8010/api/v1/convert"
|
||||
|
||||
dingtalk:
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
api_secret: "ObqxwyR20r9rVNhju0sCPQyQA98_FZSc32W4vgxnGFH_b02HZr1BPCJsOAF816nu"
|
||||
table_demand:
|
||||
url: "https://alidocs.dingtalk.com/i/nodes/2Amq4vjg89RnYx9DTp66m2orW3kdP0wQ"
|
||||
base_id: "2Amq4vjg89RnYx9DTp66m2orW3kdP0wQ"
|
||||
sheet_id_or_name: "数据表"
|
||||
# 机器人群组
|
||||
bot_group_id:
|
||||
bbxt: 29
|
||||
|
||||
qywx:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk"
|
||||
token: "zJdukry6"
|
||||
aes_key: "4VLH47qRGUogc2d3QLWuUhvJlk8Y0YuRjXzeBquBq8B"
|
||||
init_account: "les.,FuZhongYun"
|
||||
chat_id_len: 16
|
||||
default_config_id: 1
|
||||
bot_group_id:
|
||||
bbxt: 37
|
||||
|
||||
default_prompt:
|
||||
img_recognize:
|
||||
|
|
|
|||
|
|
@ -7,35 +7,17 @@ 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"
|
||||
vl_model: "qwen2.5vl:7b"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
base_url: "http://117.175.169.61:16001/v1"
|
||||
vl_model: "models/Qwen2.5-VL-3B-Instruct-AWQ"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
coze:
|
||||
base_url: "https://api.coze.cn"
|
||||
api_secret: "sat_AqvFcdNgesP8megy1ItTscWFXRcsHRzmM4NJ1KNavfcdT0EPwYuCPkDqGhItpx13"
|
||||
|
||||
lsxd:
|
||||
# 统一登录
|
||||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
code: "456789"
|
||||
|
||||
sys:
|
||||
session_len: 6
|
||||
|
|
@ -56,12 +38,6 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
bucket: "attachment-public"
|
||||
domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com"
|
||||
endpoint: "https://oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
tools:
|
||||
zltxOrderDetail:
|
||||
|
|
@ -94,78 +70,7 @@ tools:
|
|||
zltxOrderAfterSaleResellerBatch:
|
||||
enabled: true
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
|
||||
zltxResellerAuthProductToManagerAndDefaultLossReason:
|
||||
base_url: "https://revcl.1688sup.com/api/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason"
|
||||
|
||||
# eino tool 配置
|
||||
eino_tools:
|
||||
# == 货易通 hyt ==
|
||||
# 货易通商品上传
|
||||
hytProductUpload:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/supplier/batch/add/complete"
|
||||
add_url: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
|
||||
# 货易通供应商查询
|
||||
hytSupplierSearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/supplier/list"
|
||||
# 货易通仓库查询
|
||||
hytWarehouseSearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/warehouse/list"
|
||||
# 货易通商品添加
|
||||
hytGoodsAdd:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/add"
|
||||
add_url: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
|
||||
# 货易通商品图片添加
|
||||
hytGoodsMediaAdd:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/media/add/batch"
|
||||
# 货易通商品分类添加
|
||||
hytGoodsCategoryAdd:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/good/category/relation/add"
|
||||
# 货易通商品分类查询
|
||||
hytGoodsCategorySearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/category/list"
|
||||
# 货易通商品品牌查询
|
||||
hytGoodsBrandSearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list"
|
||||
# == 报表分析 data analytics ==
|
||||
# 负利润分析列表
|
||||
daOursProductLoss:
|
||||
base_url: "https://reportapi.1688sup.com/api/dataanalytics/statisOursProductLossSum"
|
||||
# 利润同比排行榜
|
||||
daProfitRanking:
|
||||
base_url: "https://reportapi.1688sup.com/api/dataanalytics/profitRankingSum"
|
||||
# 销售同比分析列表
|
||||
daOfficialProduct:
|
||||
base_url: "https://reportapi.1688sup.com/api/dataanalytics/statisOfficialProduct"
|
||||
# == 电商充值系统 ==
|
||||
# 我们的商品统计
|
||||
rechargeStatisticsOursProduct:
|
||||
base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct"
|
||||
# == 通用工具 ==
|
||||
# 表格转图片
|
||||
excel2pic:
|
||||
base_url: "http://192.168.6.109:8010/api/v1/convert"
|
||||
|
||||
dingtalk:
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
api_secret: "ObqxwyR20r9rVNhju0sCPQyQA98_FZSc32W4vgxnGFH_b02HZr1BPCJsOAF816nu"
|
||||
table_demand:
|
||||
url: "https://alidocs.dingtalk.com/i/nodes/YQBnd5ExVE6qAbnOiANQg2KKJyeZqMmz"
|
||||
base_id: "YQBnd5ExVE6qAbnOiANQg2KKJyeZqMmz"
|
||||
sheet_id_or_name: "数据表"
|
||||
# 机器人群组
|
||||
bot_group_id:
|
||||
bbxt: 23
|
||||
|
||||
qywx:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk"
|
||||
token: "zJdukry6"
|
||||
aes_key: "4VLH47qRGUogc2d3QLWuUhvJlk8Y0YuRjXzeBquBq8B"
|
||||
init_account: "les.,FuZhongYun"
|
||||
chat_id_len: 16
|
||||
default_config_id: 1
|
||||
bot_group_id:
|
||||
bbxt: 36
|
||||
|
||||
|
||||
default_prompt:
|
||||
|
|
|
|||
|
|
@ -3,40 +3,22 @@ server:
|
|||
port: 8090
|
||||
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://127.0.0.1:11434"
|
||||
model: "qwen3-coder:480b-cloud"
|
||||
generate_model: "qwen3-coder:480b-cloud"
|
||||
vl_model: "gemini-3-pro-preview"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
base_url: "http://host.docker.internal:8001/v1"
|
||||
vl_model: "models/Qwen2.5-VL-3B-Instruct-AWQ"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
coze:
|
||||
base_url: "https://api.coze.cn"
|
||||
api_secret: "sat_AqvFcdNgesP8megy1ItTscWFXRcsHRzmM4NJ1KNavfcdT0EPwYuCPkDqGhItpx13"
|
||||
|
||||
|
||||
lsxd:
|
||||
# 统一登录
|
||||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
code: "456789"
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
|
||||
sys:
|
||||
session_len: 6
|
||||
|
|
@ -48,7 +30,7 @@ redis:
|
|||
host: 47.97.27.195:6379
|
||||
type: node
|
||||
pass: lansexiongdi@666
|
||||
key: ai_scheduler_test
|
||||
key: report-api-test
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
|
|
@ -57,12 +39,6 @@ redis:
|
|||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai_test?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
bucket: "attachment-public"
|
||||
domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com"
|
||||
endpoint: "https://oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
tools:
|
||||
zltxOrderDetail:
|
||||
|
|
@ -109,67 +85,8 @@ tools:
|
|||
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:
|
||||
# 货易通商品上传
|
||||
hytProductUpload:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/supplier/batch/add/complete"
|
||||
add_url: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
|
||||
# 货易通供应商查询
|
||||
hytSupplierSearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/supplier/list"
|
||||
# 货易通仓库查询
|
||||
hytWarehouseSearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/warehouse/list"
|
||||
# 货易通商品添加
|
||||
hytGoodsAdd:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/add"
|
||||
add_url: "https://gateway.dev.cdlsxd.cn/sw//#/goods/goodsManage"
|
||||
# 货易通商品图片添加
|
||||
hytGoodsMediaAdd:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/media/add/batch"
|
||||
# 货易通商品分类添加
|
||||
hytGoodsCategoryAdd:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/good/category/relation/add"
|
||||
# 货易通商品分类查询
|
||||
hytGoodsCategorySearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/category/list"
|
||||
# 货易通商品品牌查询
|
||||
hytGoodsBrandSearch:
|
||||
base_url: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list"
|
||||
# == 电商充值系统 ==
|
||||
# 我们的商品统计
|
||||
rechargeStatisticsOursProduct:
|
||||
base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct"
|
||||
# == 通用工具 ==
|
||||
# 表格转图片
|
||||
excel2pic:
|
||||
base_url: "http://192.168.6.115:8010/api/v1/convert"
|
||||
|
||||
dingtalk:
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
api_secret: "ObqxwyR20r9rVNhju0sCPQyQA98_FZSc32W4vgxnGFH_b02HZr1BPCJsOAF816nu"
|
||||
table_demand:
|
||||
url: "https://alidocs.dingtalk.com/i/nodes/YQBnd5ExVE6qAbnOiANQg2KKJyeZqMmz"
|
||||
base_id: "YQBnd5ExVE6qAbnOiANQg2KKJyeZqMmz"
|
||||
sheet_id_or_name: "数据表"
|
||||
# 机器人群组
|
||||
bot_group_id:
|
||||
bbxt: 23
|
||||
|
||||
qywx:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk"
|
||||
token: "zJdukry6"
|
||||
aes_key: "4VLH47qRGUogc2d3QLWuUhvJlk8Y0YuRjXzeBquBq8B"
|
||||
init_account: "les.,FuZhongYun"
|
||||
chat_id_len: 16
|
||||
default_config_id: 1
|
||||
bot_group_id:
|
||||
bbxt: 36
|
||||
|
||||
default_prompt:
|
||||
img_recognize:
|
||||
|
|
@ -188,7 +105,7 @@ permissionConfig:
|
|||
llm:
|
||||
providers:
|
||||
ollama:
|
||||
endpoint: http://host.docker.internal:11434
|
||||
endpoint: http://127.0.0.1:11434
|
||||
timeout: 60s
|
||||
models:
|
||||
- id: qwen3-coder:480b-cloud
|
||||
|
|
@ -232,3 +149,9 @@ llm:
|
|||
temperature: 0.7
|
||||
max_tokens: 4096
|
||||
stream: true
|
||||
|
||||
|
||||
#ding_talk_bots:
|
||||
# public:
|
||||
# client_id: "dingchg59zwwvmuuvldx",
|
||||
# client_secret: "ZwetAnRiTQobNFVlNrshRagSMAJIFpBAepWkWI7on7Tt_o617KHtTjBLp8fQfplz",
|
||||
|
|
|
|||
20
deploy.sh
20
deploy.sh
|
|
@ -3,7 +3,6 @@
|
|||
#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'
|
||||
|
||||
|
||||
|
|
@ -15,13 +14,11 @@ fi
|
|||
|
||||
CONFIG_FILE="config/config.yaml"
|
||||
BRANCH="master"
|
||||
BOT="All"
|
||||
CRON="start"
|
||||
BOT="ALL"
|
||||
if [ "$MODE" = "dev" ]; then
|
||||
CONFIG_FILE="config/config_test.yaml"
|
||||
BOT="zltx"
|
||||
BRANCH="test"
|
||||
CRON="close"
|
||||
fi
|
||||
|
||||
git fetch origin
|
||||
|
|
@ -32,25 +29,12 @@ git pull origin "$BRANCH"
|
|||
docker build -t ${CONTAINER_NAME} .
|
||||
docker stop ${CONTAINER_NAME}
|
||||
docker rm -f ${CONTAINER_NAME}
|
||||
|
||||
# 依赖服务绑定同一网络,以便相互通信
|
||||
docker network create "${NETWORK_NAME}" 2>/dev/null || true
|
||||
docker network connect "${NETWORK_NAME}" excel2pic 2>/dev/null || true
|
||||
docker network connect "${NETWORK_NAME}" WeKnora-app 2>/dev/null || true
|
||||
|
||||
docker run -itd \
|
||||
--name "${CONTAINER_NAME}" \
|
||||
--restart=always \
|
||||
--add-host=host.docker.internal:host-gateway \
|
||||
--network="${NETWORK_NAME}" \
|
||||
-e "OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://host.docker.internal:11434}" \
|
||||
-e "MODE=${MODE}" \
|
||||
-p 8090:8090 \
|
||||
-v ./cache:/app/cache \
|
||||
-v ./tmpl:/app/tmpl \
|
||||
-v ./go.mod:/app/go.mod \
|
||||
"${CONTAINER_NAME}" ./server \
|
||||
--config "./${CONFIG_FILE}" --bot "${BOT}" --cron "${CRON}"
|
||||
|
||||
"${CONTAINER_NAME}" ./server --config "./${CONFIG_FILE}" --bot "./${BOT}"
|
||||
|
||||
docker logs -f ${CONTAINER_NAME}
|
||||
29
go.mod
29
go.mod
|
|
@ -3,22 +3,19 @@ module ai_scheduler
|
|||
go 1.24.7
|
||||
|
||||
require (
|
||||
gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go v0.9.3
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.12
|
||||
github.com/alibabacloud-go/dingtalk v1.6.96
|
||||
github.com/alibabacloud-go/tea v1.2.2
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/cloudwego/eino v0.7.7
|
||||
github.com/cloudwego/eino-ext/components/model/ollama v0.1.6
|
||||
github.com/cloudwego/eino-ext/components/model/openai v0.1.5
|
||||
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20
|
||||
github.com/emirpasic/gods v1.18.1
|
||||
github.com/faabiosr/cachego v0.26.0
|
||||
github.com/fastwego/dingding v1.0.0-beta.4
|
||||
github.com/gabriel-vasile/mimetype v1.4.11
|
||||
github.com/go-kratos/kratos/v2 v2.9.2
|
||||
github.com/go-kratos/kratos/v2 v2.9.1
|
||||
github.com/go-playground/locales v0.14.1
|
||||
github.com/go-playground/universal-translator v0.18.1
|
||||
github.com/go-playground/validator/v10 v10.20.0
|
||||
|
|
@ -27,14 +24,10 @@ require (
|
|||
github.com/google/uuid v1.6.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/ollama/ollama v0.12.7
|
||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
|
||||
github.com/redis/go-redis/v9 v9.16.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shopspring/decimal v1.4.0
|
||||
github.com/spf13/viper v1.17.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tmc/langchaingo v0.1.13
|
||||
github.com/xuri/excelize/v2 v2.10.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/grpc v1.64.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
|
|
@ -59,7 +52,7 @@ require (
|
|||
github.com/clbanning/mxj/v2 v2.5.5 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
|
@ -92,9 +85,6 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pkoukk/tiktoken-go v0.1.6 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/richardlehane/mscfb v1.0.4 // indirect
|
||||
github.com/richardlehane/msoleps v1.0.4 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.3.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
|
|
@ -105,26 +95,23 @@ require (
|
|||
github.com/spf13/afero v1.10.0 // indirect
|
||||
github.com/spf13/cast v1.5.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.7.1 // indirect
|
||||
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/wk8/go-ordered-map/v2 v2.1.8 // 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
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
|
||||
golang.org/x/net v0.46.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
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
|
||||
|
|
|
|||
57
go.sum
57
go.sum
|
|
@ -38,8 +38,6 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go v0.9.3 h1:qaSPxVz5kHCs2AWvShnOG8mUgrUP9Gc3uUB4ZX1BF5A=
|
||||
gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go v0.9.3/go.mod h1:5mCPTjBxOk69LRJPHWJRNTkfxcffqlQSOBMD4M5JVnE=
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8 h1:FaKRql9mCVcSoaGqPeBOAruZ52slzRngQ6VRTYKNSsA=
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8/go.mod h1:Qf4hVXm2Eu5vOvwXk8D7U0q/aekMCkZ4Fg9wnRKlasQ=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
|
|
@ -92,8 +90,6 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.6 h1:ZkmUlhlQbaDC+Eba/GARMPy6hKdCLi
|
|||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3 h1:7LYnm+JbOq2B+T/B0fHC4Ies4/FofC4zHzYtqw7dgt0=
|
||||
github.com/alibabacloud-go/tea-xml v1.1.3/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g=
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
|
|
@ -193,8 +189,8 @@ github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclK
|
|||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kratos/kratos/v2 v2.9.2 h1:px8GJQBeLpquDKQWQ9zohEWiLA8n4D/pv7aH3asvUvo=
|
||||
github.com/go-kratos/kratos/v2 v2.9.2/go.mod h1:Jc7jaeYd4RAPjetun2C+oFAOO7HNMHTT/Z4LxpuEDJM=
|
||||
github.com/go-kratos/kratos/v2 v2.9.1 h1:EGif6/S/aK/RCR5clIbyhioTNyoSrii3FC118jG40Z0=
|
||||
github.com/go-kratos/kratos/v2 v2.9.1/go.mod h1:a1MQLjMhIh7R0kcJS9SzJYR43BRI7EPzzN0J1Ksu2bA=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
|
|
@ -279,6 +275,8 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
|
|||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
|
||||
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
|
|
@ -356,6 +354,8 @@ github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1ls
|
|||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8=
|
||||
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
|
|
@ -370,15 +370,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
|||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
|
||||
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
|
||||
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
|
||||
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
|
|
@ -390,8 +383,6 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR
|
|||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJh/zsFQ12yEE89xfCrGKK63Rr7ctU/uCo4g=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
|
@ -434,8 +425,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
|
||||
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
|
|
@ -453,12 +442,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/
|
|||
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/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=
|
||||
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
|
||||
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||
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/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
|
|
@ -498,8 +481,8 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf
|
|||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
@ -514,8 +497,6 @@ golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrC
|
|||
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
|
|
@ -582,8 +563,8 @@ golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
|||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
@ -605,8 +586,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -661,8 +642,8 @@ golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
|
|
@ -671,8 +652,8 @@ golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
|||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
@ -685,13 +666,11 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"ai_scheduler/internal/pkg/util"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
|
|
@ -31,8 +30,7 @@ func (s *ChatHistoryBiz) List(ctx context.Context, query *entitys.ChatHistQuery)
|
|||
con := []impl.CondFunc{
|
||||
s.chatHiRepo.WithSessionId(query.SessionID),
|
||||
s.chatHiRepo.PaginateScope(query.Page, query.PageSize),
|
||||
// s.chatHiRepo.OrderByDesc("his_id"),
|
||||
s.chatHiRepo.OrderByAsc("his_id"),
|
||||
s.chatHiRepo.OrderByDesc("his_id"),
|
||||
}
|
||||
if query.HisID > 0 {
|
||||
con = append(con, s.chatHiRepo.WithHisId(query.HisID))
|
||||
|
|
@ -129,5 +127,5 @@ func (c *ChatHistoryBiz) UpdateContent(ctx context.Context, chat *entitys.Update
|
|||
func (s *ChatHistoryBiz) Update(ctx context.Context, chat *entitys.UseFulRequest) error {
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"his_id": chat.HisId})
|
||||
return s.chatHiRepo.UpdateByCond(&cond, &model.AiChatHi{Useful: chat.Useful})
|
||||
return s.chatHiRepo.UpdateByCond(&cond, &model.AiChatHi{HisID: chat.HisId, Useful: chat.Useful})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,49 +3,37 @@ package biz
|
|||
import (
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/biz/handle/dingtalk"
|
||||
"ai_scheduler/internal/biz/handle/qywx"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/biz/tools_regis"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/tools"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l-dingtalk-stream-sdk-go/chatbot"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// AiRouterBiz 智能路由服务
|
||||
type DingTalkBotBiz struct {
|
||||
do *do.Do
|
||||
handle *do.Handle
|
||||
botConfigImpl *impl.BotConfigImpl
|
||||
replier *chatbot.ChatbotReplier
|
||||
log log.Logger
|
||||
dingTalkUser *dingtalk.User
|
||||
botGroupImpl *impl.BotGroupImpl
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
botGroupQywxImpl *impl.BotGroupQywxImpl
|
||||
toolManager *tools.Manager
|
||||
chatHis *impl.BotChatHisImpl
|
||||
conf *config.Config
|
||||
cardSend *dingtalk.SendCardClient
|
||||
qywxGroupHandle *qywx.Group
|
||||
groupConfigBiz *GroupConfigBiz
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
macro *do.Macro
|
||||
do *do.Do
|
||||
handle *do.Handle
|
||||
botConfigImpl *impl.BotConfigImpl
|
||||
replier *chatbot.ChatbotReplier
|
||||
log log.Logger
|
||||
dingTalkUser *dingtalk.User
|
||||
botTools []model.AiBotTool
|
||||
botGroupImpl *impl.BotGroupImpl
|
||||
toolManager *tools.Manager
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
|
|
@ -55,28 +43,18 @@ func NewDingTalkBotBiz(
|
|||
botConfigImpl *impl.BotConfigImpl,
|
||||
botGroupImpl *impl.BotGroupImpl,
|
||||
dingTalkUser *dingtalk.User,
|
||||
chatHis *impl.BotChatHisImpl,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
tools *tools_regis.ToolRegis,
|
||||
toolManager *tools.Manager,
|
||||
conf *config.Config,
|
||||
cardSend *dingtalk.SendCardClient,
|
||||
groupConfigBiz *GroupConfigBiz,
|
||||
macro *do.Macro,
|
||||
) *DingTalkBotBiz {
|
||||
return &DingTalkBotBiz{
|
||||
do: do,
|
||||
handle: handle,
|
||||
botConfigImpl: botConfigImpl,
|
||||
replier: chatbot.NewChatbotReplier(),
|
||||
dingTalkUser: dingTalkUser,
|
||||
groupConfigBiz: groupConfigBiz,
|
||||
botGroupImpl: botGroupImpl,
|
||||
toolManager: toolManager,
|
||||
chatHis: chatHis,
|
||||
conf: conf,
|
||||
cardSend: cardSend,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
macro: macro,
|
||||
do: do,
|
||||
handle: handle,
|
||||
botConfigImpl: botConfigImpl,
|
||||
replier: chatbot.NewChatbotReplier(),
|
||||
dingTalkUser: dingTalkUser,
|
||||
botTools: tools.BootTools,
|
||||
botGroupImpl: botGroupImpl,
|
||||
toolManager: toolManager,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +71,7 @@ func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalk
|
|||
if err != nil {
|
||||
d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error())
|
||||
}
|
||||
config.BotIndex = v.RobotCode
|
||||
config.BotIndex = v.BotIndex
|
||||
dingBotList = append(dingBotList, config)
|
||||
}
|
||||
return
|
||||
|
|
@ -110,8 +88,8 @@ func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallb
|
|||
}
|
||||
|
||||
func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||||
//entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等")
|
||||
//defer close(requireData.Ch)
|
||||
entitys.ResText(requireData.Ch, "", "收到消息,正在处理中,请稍等")
|
||||
defer close(requireData.Ch)
|
||||
switch constants.ConversationType(requireData.Req.ConversationType) {
|
||||
case constants.ConversationTypeSingle:
|
||||
err = d.handleSingleChat(ctx, requireData)
|
||||
|
|
@ -120,9 +98,6 @@ func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDat
|
|||
default:
|
||||
err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType)
|
||||
}
|
||||
if err != nil {
|
||||
entitys.ResText(requireData.Ch, "", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +108,6 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti
|
|||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
//requireData.ID=requireData.UserInfo.UserID
|
||||
////如果不是管理或者不是老板,则进行权限判断
|
||||
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
|
||||
//
|
||||
|
|
@ -142,28 +116,11 @@ func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *enti
|
|||
}
|
||||
|
||||
func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
|
||||
group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle, requireData.Req.RobotCode)
|
||||
group, err := d.initGroup(ctx, requireData.Req.ConversationId, requireData.Req.ConversationTitle)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
groupConfig, err := d.groupConfigBiz.GetGroupConfig(ctx, group.ConfigID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//宏
|
||||
sucMsg, err, isFinal := d.macro.Router(ctx, requireData.Req.Text.Content, groupConfig)
|
||||
if err != nil {
|
||||
entitys.ResText(requireData.Ch, "", err.Error())
|
||||
return
|
||||
}
|
||||
if len(sucMsg) > 0 {
|
||||
entitys.ResText(requireData.Ch, "", sucMsg)
|
||||
}
|
||||
if isFinal {
|
||||
return
|
||||
}
|
||||
requireData.ID = group.GroupID
|
||||
groupTools, err := d.groupConfigBiz.getGroupTools(ctx, groupConfig)
|
||||
groupTools, err := d.getGroupTools(ctx, group)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -172,11 +129,11 @@ func (d *DingTalkBotBiz) handleGroupChat(ctx context.Context, requireData *entit
|
|||
return
|
||||
}
|
||||
|
||||
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig)
|
||||
return d.handleMatch(ctx, rec)
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string, robotCode string) (group *model.AiBotGroup, err error) {
|
||||
group, err = d.botGroupImpl.GetByConversationIdAndRobotCode(conversationId, robotCode)
|
||||
func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, conversationTitle string) (group *model.AiBotGroup, err error) {
|
||||
group, err = d.botGroupImpl.GetByConversationId(conversationId)
|
||||
if err != nil {
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
|
||||
|
|
@ -188,31 +145,50 @@ func (d *DingTalkBotBiz) initGroup(ctx context.Context, conversationId string, c
|
|||
group = &model.AiBotGroup{
|
||||
ConversationID: conversationId,
|
||||
Title: conversationTitle,
|
||||
RobotCode: robotCode,
|
||||
ToolList: "",
|
||||
}
|
||||
//如果不存在则创建
|
||||
_, err = d.botGroupImpl.Add(group)
|
||||
d.botGroupImpl.Add(group)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) getGroupTools(ctx context.Context, group *model.AiBotGroup) (tools []model.AiBotTool, err error) {
|
||||
if len(d.botTools) == 0 {
|
||||
return
|
||||
}
|
||||
var (
|
||||
groupRegisTools map[string]struct{}
|
||||
)
|
||||
if group.ToolList != "" {
|
||||
groupList := strings.Split(group.ToolList, ",")
|
||||
for _, tool := range groupList {
|
||||
groupRegisTools[tool] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range d.botTools {
|
||||
if v.PermissionType == constants.PermissionTypeNone {
|
||||
tools = append(tools, v)
|
||||
continue
|
||||
}
|
||||
if _, ex := groupRegisTools[v.Index]; ex {
|
||||
tools = append(tools, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, tools []model.AiBotTool) (rec *entitys.Recognize, err error) {
|
||||
|
||||
userContent, err := d.getUserContent(requireData.Req.Msgtype, requireData.Req.Text.Content)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, err
|
||||
}
|
||||
rec = &entitys.Recognize{
|
||||
Ch: requireData.Ch,
|
||||
SystemPrompt: d.defaultPrompt(),
|
||||
UserContent: userContent,
|
||||
}
|
||||
//历史记录
|
||||
rec.ChatHis, err = d.getHis(ctx, constants.ConversationType(requireData.Req.ConversationType), requireData.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//工具注册
|
||||
if len(tools) > 0 {
|
||||
rec.Tasks = make([]entitys.RegistrationTask, 0, len(tools))
|
||||
for _, task := range tools {
|
||||
|
|
@ -224,50 +200,15 @@ func (d *DingTalkBotBiz) recognize(ctx context.Context, requireData *entitys.Req
|
|||
|
||||
rec.Tasks = append(rec.Tasks, entitys.RegistrationTask{
|
||||
Name: task.Index,
|
||||
Desc: task.TempPrompt,
|
||||
Desc: task.Desc,
|
||||
TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
rec.Ext = pkg.JsonByteIgonErr(&entitys.TaskExt{
|
||||
UserName: requireData.Req.SenderNick,
|
||||
})
|
||||
|
||||
err = d.handle.Recognize(ctx, rec, &do.WithDingTalkBot{})
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) getHis(ctx context.Context, conversationType constants.ConversationType, Id int32) (content entitys.ChatHis, err error) {
|
||||
|
||||
var (
|
||||
his []model.AiBotChatHi
|
||||
)
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"his_type": conversationType})
|
||||
cond = cond.And(builder.Eq{"id": Id})
|
||||
_, err = d.chatHis.GetListToStruct(&cond, &dataTemp.ReqPageBo{Limit: d.conf.Sys.SessionLen}, &his, "his_id desc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
messages := make([]entitys.HisMessage, 0, len(his))
|
||||
for _, v := range his {
|
||||
messages = append(messages, entitys.HisMessage{
|
||||
Role: constants.Caller(v.Role), // 用户角色
|
||||
Content: v.Content, // 用户输入内容
|
||||
Timestamp: v.CreateAt.Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
return entitys.ChatHis{
|
||||
SessionId: fmt.Sprintf("%s_%d", conversationType, Id),
|
||||
Messages: messages,
|
||||
Context: entitys.HisContext{
|
||||
UserLanguage: constants.LangZhCN, // 默认中文
|
||||
SystemMode: constants.SystemModeTechnicalSupport, // 默认技术支持模式
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{}) (content *entitys.RecognizeUserContent, err error) {
|
||||
switch constants.BotMsgType(msgType) {
|
||||
case constants.BotMsgTypeText:
|
||||
|
|
@ -280,81 +221,91 @@ func (d *DingTalkBotBiz) getUserContent(msgType string, msgContent interface{})
|
|||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) HandleStreamRes(ctx context.Context, data *chatbot.BotCallbackDataModel, content chan string) (err error) {
|
||||
err = d.cardSend.NewCard(ctx, &dingtalk.CardSend{
|
||||
RobotCode: data.RobotCode,
|
||||
ConversationType: constants.ConversationType(data.ConversationType),
|
||||
Template: constants.CardTempDefault,
|
||||
ContentChannel: content, // 指定内容通道
|
||||
ConversationId: data.ConversationId,
|
||||
SenderStaffId: data.SenderStaffId,
|
||||
Title: data.Text.Content,
|
||||
})
|
||||
return
|
||||
func (d *DingTalkBotBiz) defaultPrompt() string {
|
||||
|
||||
return `{"system":"智能路由系统,精准解析用户意图并路由至任务模块,遵循以下规则:","rule":{"返回格式":"{\\"index\\":\\"工具索引\\",\\"confidence\\":\\"0.0-1.0\\",\\"reasoning\\":\\"判断理由\\",\\"parameters\\":\\"转义JSON参数\\",\\"is_match\\":true|false,\\"chat\\":\\"追问内容\\"}","工具匹配":["用工具parameters匹配,区分必选(required)和可选(optional)参数","无法匹配时,is_match=false,chat提醒用户适用工具(例:'请问您要查询订单还是商品?')"],"参数提取":["从用户输入提取parameters中明确提及的参数","必须参数仅用用户直接提及的,缺失时is_match=false,chat提醒补充(例:'需补充XX信息')"],"格式要求":["所有字段值为字符串(含confidence)","parameters为转义JSON字符串(如\\"{\\\\"key\\\\":\\\\"value\\\\"}\\")"]}}`
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroup, report *bbxt.ReportRes) (err error) {
|
||||
if report == nil {
|
||||
return errors.New("report is nil")
|
||||
}
|
||||
reportChan := make(chan string, 10)
|
||||
defer close(reportChan)
|
||||
reportChan <- report.Title
|
||||
reportChan <- fmt.Sprintf("", report.Url)
|
||||
err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||||
RobotCode: groupInfo.RobotCode,
|
||||
ConversationType: constants.ConversationTypeGroup,
|
||||
ConversationId: groupInfo.ConversationID,
|
||||
Text: chatbot.BotCallbackDataTextModel{
|
||||
Content: report.ReportName,
|
||||
},
|
||||
}, reportChan)
|
||||
return
|
||||
}
|
||||
func (d *DingTalkBotBiz) handleMatch(ctx context.Context, rec *entitys.Recognize) (err error) {
|
||||
|
||||
// SendReports 发送多个报告
|
||||
func (d *DingTalkBotBiz) SendReports(ctx context.Context, groupInfo *model.AiBotGroup, reports []*bbxt.ReportRes) (err error) {
|
||||
if len(reports) == 0 {
|
||||
return errors.New("report is empty")
|
||||
}
|
||||
|
||||
title := fmt.Sprintf("截止%s日报", time.Now().Format("1月2日15点"))
|
||||
reportChan := make(chan string, len(reports)*2)
|
||||
writeCount := 0
|
||||
for _, v := range reports {
|
||||
if v == nil {
|
||||
continue
|
||||
if !rec.Match.IsMatch {
|
||||
if len(rec.Match.Chat) != 0 {
|
||||
entitys.ResText(rec.Ch, "", rec.Match.Chat)
|
||||
} else {
|
||||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
}
|
||||
reportChan <- fmt.Sprintf("**%s**", v.Title)
|
||||
reportChan <- fmt.Sprintf("", v.Url)
|
||||
writeCount += 2
|
||||
return
|
||||
}
|
||||
close(reportChan)
|
||||
if writeCount == 0 {
|
||||
return errors.New("report is empty")
|
||||
var pointTask *model.AiBotTool
|
||||
for _, task := range d.botTools {
|
||||
if task.Index == rec.Match.Index {
|
||||
pointTask = &task
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if pointTask == nil || pointTask.Index == "other" {
|
||||
return d.otherTask(ctx, rec)
|
||||
}
|
||||
switch constants.TaskType(pointTask.Type) {
|
||||
//case constants.TaskTypeApi:
|
||||
//return d.handleApiTask(ctx, requireData, pointTask)
|
||||
case constants.TaskTypeFunc:
|
||||
return d.handleTask(ctx, rec, pointTask)
|
||||
default:
|
||||
return d.otherTask(ctx, rec)
|
||||
}
|
||||
err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
|
||||
RobotCode: groupInfo.RobotCode,
|
||||
ConversationType: constants.ConversationTypeGroup,
|
||||
ConversationId: groupInfo.ConversationID,
|
||||
Text: chatbot.BotCallbackDataTextModel{
|
||||
Content: title,
|
||||
},
|
||||
}, reportChan)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroup, err error) {
|
||||
func (d *DingTalkBotBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) {
|
||||
var configData entitys.ConfigDataTool
|
||||
err = json.Unmarshal([]byte(task.Config), &configData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"group_id": groupId})
|
||||
cond = cond.And(builder.Eq{"status": constants.Enable})
|
||||
err = d.botGroupImpl.GetOneBySearchToStrut(&cond, &group)
|
||||
err = d.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) ReplyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||||
func (d *DingTalkBotBiz) otherTask(ctx context.Context, rec *entitys.Recognize) (err error) {
|
||||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
return
|
||||
}
|
||||
func (d *DingTalkBotBiz) HandleRes(ctx context.Context, data *chatbot.BotCallbackDataModel, resp entitys.Response) error {
|
||||
switch resp.Type {
|
||||
case entitys.ResponseText:
|
||||
return d.replyText(ctx, data.SessionWebhook, resp.Content)
|
||||
case entitys.ResponseStream:
|
||||
return d.replySteam(ctx, data.SessionWebhook, resp.Content)
|
||||
case entitys.ResponseImg:
|
||||
return d.replyImg(ctx, data.SessionWebhook, resp.Content)
|
||||
case entitys.ResponseFile:
|
||||
return d.replyFile(ctx, data.SessionWebhook, resp.Content)
|
||||
case entitys.ResponseMarkdown:
|
||||
return d.replyMarkdown(ctx, data.SessionWebhook, resp.Content)
|
||||
case entitys.ResponseActionCard:
|
||||
return d.replyActionCard(ctx, data.SessionWebhook, resp.Content)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) replySteam(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||||
msg := content
|
||||
if len(arg) > 0 {
|
||||
msg = fmt.Sprintf(content, arg)
|
||||
}
|
||||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) replyText(ctx context.Context, SessionWebhook string, content string, arg ...string) error {
|
||||
msg := content
|
||||
if len(arg) > 0 {
|
||||
msg = fmt.Sprintf(content, arg)
|
||||
|
|
@ -393,53 +344,3 @@ func (d *DingTalkBotBiz) replyActionCard(ctx context.Context, SessionWebhook str
|
|||
}
|
||||
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) SaveHis(ctx context.Context, requireData *entitys.RequireDataDingTalkBot, chat []string) (err error) {
|
||||
if len(chat) == 0 {
|
||||
return
|
||||
}
|
||||
his := []*model.AiBotChatHi{
|
||||
{
|
||||
HisType: requireData.Req.ConversationType,
|
||||
ID: requireData.ID,
|
||||
Role: "user",
|
||||
Content: requireData.Req.Text.Content,
|
||||
},
|
||||
{
|
||||
HisType: requireData.Req.ConversationType,
|
||||
ID: requireData.ID,
|
||||
Role: "system",
|
||||
Content: strings.Join(chat, "\n"),
|
||||
},
|
||||
}
|
||||
_, err = d.chatHis.Add(his)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DingTalkBotBiz) defaultPrompt() string {
|
||||
now := time.Now().Format(time.DateTime)
|
||||
return `[system] 你是一个智能路由系统,核心职责是 **精准解析用户意图并路由至对应任务模块**。请严格遵循以下规则:
|
||||
[rule]
|
||||
1. **返回格式**:
|
||||
仅输出以下 **严格格式化的 JSON 字符串**(禁用 Markdown):
|
||||
{ "index": "工具索引index", "confidence": 0.0-1.0,"reasoning": "判断理由","parameters":"jsonstring |提取参数","is_match":true||false,"chat": "追问内容"}
|
||||
关键规则(按优先级排序):
|
||||
|
||||
2. **工具匹配**:
|
||||
|
||||
- 若匹配到工具,使用工具的 parameters 作为模板做参数匹配
|
||||
- 注意区分 parameters 中的 必须参数(required) 和 可选参数(optional),按下述参数提取规则处理。
|
||||
- 若**完全无法匹配**,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户需要适用何种工具(例:"请问您是要查询订单还是商品呢")。
|
||||
|
||||
3. **参数提取**:
|
||||
|
||||
- 根据 parameters 字段列出的参数名,从用户输入中提取对应值。
|
||||
- **仅提取**明确提及的参数,忽略未列出的内容。
|
||||
- 必须参数仅使用用户直接提及的参数,不允许从上下文推断。
|
||||
- 若必须参数缺失,立即设置 is_match: false,并在 chat 中已第一人称的角度提醒用户提供缺少的参数追问(例:"需要您补充XX信息")。
|
||||
|
||||
4. 格式强制要求:
|
||||
-所有字段值必须是**字符串**(包括 confidence)。
|
||||
-parameters 必须是 **转义后的 JSON 字符串**(如 "{\"product_name\": \"京东月卡\"}")。
|
||||
当前时间:` + now + `,所有的时间识别精确到秒`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package do
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
|
|
@ -33,18 +32,16 @@ type Do struct {
|
|||
}
|
||||
|
||||
func NewDo(
|
||||
sessionImpl *impl.SessionImpl,
|
||||
sysImpl *impl.SysImpl,
|
||||
taskImpl *impl.TaskImpl,
|
||||
hisImpl *impl.ChatHisImpl,
|
||||
conf *config.Config,
|
||||
) *Do {
|
||||
return &Do{
|
||||
conf: conf,
|
||||
sessionImpl: sessionImpl,
|
||||
sysImpl: sysImpl,
|
||||
hisImpl: hisImpl,
|
||||
taskImpl: taskImpl,
|
||||
conf: conf,
|
||||
sysImpl: sysImpl,
|
||||
hisImpl: hisImpl,
|
||||
taskImpl: taskImpl,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +253,7 @@ func (d *Do) startMessageHandler(
|
|||
requireData *entitys.RequireData,
|
||||
) <-chan struct{} {
|
||||
done := make(chan struct{})
|
||||
var chat []entitys.Response
|
||||
var chat []string
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
|
|
@ -266,29 +263,15 @@ func (d *Do) startMessageHandler(
|
|||
hisLog = &entitys.ChatHisLog{}
|
||||
)
|
||||
if len(chat) > 0 {
|
||||
// 合并所有回答-转json字符串
|
||||
ans, _ := json.Marshal(chat)
|
||||
// 通过 chat 获取 task_id
|
||||
taskId := d.getTaskIdByChat(chat)
|
||||
|
||||
AiRes := &model.AiChatHi{
|
||||
SessionID: requireData.Session,
|
||||
Ques: requireData.Req.Text,
|
||||
Ans: string(ans),
|
||||
Ans: strings.Join(chat, ""),
|
||||
Files: requireData.Req.Img,
|
||||
TaskID: taskId,
|
||||
TaskID: requireData.Task.TaskID,
|
||||
}
|
||||
d.hisImpl.AddWithData(AiRes)
|
||||
hisLog.HisId = AiRes.HisID
|
||||
|
||||
// 查询当前session
|
||||
cond := builder.NewCond().And(builder.Eq{"session_id": requireData.Session})
|
||||
sessionMap, _ := d.sessionImpl.GetOneBySearch(&cond)
|
||||
requireData.SessionInfo.Title = sessionMap["title"].(string)
|
||||
// 当前 session title为空 ,更新为用户输入
|
||||
if requireData.SessionInfo.Title == "" {
|
||||
d.sessionImpl.UpdateByCond(&cond, &model.AiSession{Title: requireData.Req.Text})
|
||||
}
|
||||
}
|
||||
|
||||
_ = entitys.MsgSend(client, entitys.Response{
|
||||
|
|
@ -298,42 +281,14 @@ func (d *Do) startMessageHandler(
|
|||
|
||||
}()
|
||||
|
||||
streamText := ""
|
||||
streamIndex := ""
|
||||
for v := range requireData.Ch { // 自动检测通道关闭
|
||||
if err := sendWithTimeout(client, v, 10*time.Second); err != nil {
|
||||
log.Errorf("Send error: %v", err)
|
||||
return
|
||||
}
|
||||
// 文本+卡片
|
||||
if v.Type == entitys.ResponseText || v.Type == entitys.ResponseJson {
|
||||
chat = append(chat, v)
|
||||
if v.Type == entitys.ResponseText || v.Type == entitys.ResponseStream || v.Type == entitys.ResponseJson {
|
||||
chat = append(chat, v.Content)
|
||||
}
|
||||
// 流式-追加
|
||||
if v.Type == entitys.ResponseStream {
|
||||
streamText += v.Content
|
||||
streamIndex = v.Index
|
||||
}
|
||||
// 流式-阶段结束|对话结束
|
||||
if streamText != "" && v.Type != entitys.ResponseStream {
|
||||
chat = append(chat, entitys.Response{
|
||||
Content: streamText,
|
||||
Type: entitys.ResponseText,
|
||||
Index: streamIndex,
|
||||
})
|
||||
streamText = ""
|
||||
streamIndex = ""
|
||||
}
|
||||
}
|
||||
// 流式结束
|
||||
if streamText != "" {
|
||||
chat = append(chat, entitys.Response{
|
||||
Content: streamText,
|
||||
Type: entitys.ResponseText,
|
||||
Index: streamIndex,
|
||||
})
|
||||
streamText = ""
|
||||
streamIndex = ""
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -394,7 +349,7 @@ func (d *Do) LoadUserPermission(client *gateway.Client, requireData *entitys.Req
|
|||
|
||||
// 检查响应状态码
|
||||
if res.StatusCode != http.StatusOK {
|
||||
err = errors.SysErr(fmt.Sprintf("获取用户权限失败,状态码:%d %s %s %s", res.StatusCode, res.Text, request.Url, request.Headers["Authorization"]))
|
||||
err = errors.SysErr("获取用户权限失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -413,28 +368,3 @@ func (d *Do) LoadUserPermission(client *gateway.Client, requireData *entitys.Req
|
|||
|
||||
return respBody.Codes, nil
|
||||
}
|
||||
|
||||
// getTaskIdByChat 从 chat 中获取 task_id
|
||||
func (d *Do) getTaskIdByChat(chat []entitys.Response) (taskId int32) {
|
||||
if len(chat) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 taskIndex
|
||||
taskIndex := chat[0].Index
|
||||
|
||||
if _, ok := constants.IrregularTaskToolIndexMap[taskIndex]; ok {
|
||||
taskIndex = constants.IrregularTaskToolIndexMap[taskIndex]
|
||||
}
|
||||
|
||||
// 通过 taskIndex 获取 taskId
|
||||
cond := builder.NewCond().And(builder.Eq{"`index`": taskIndex})
|
||||
taskMap, _ := d.taskImpl.GetOneBySearch(&cond)
|
||||
if taskMap == nil || taskMap["task_id"] == nil {
|
||||
return
|
||||
}
|
||||
|
||||
taskId = taskMap["task_id"].(int32)
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"ai_scheduler/internal/biz/llm_service"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
|
|
@ -12,61 +11,45 @@ import (
|
|||
"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"
|
||||
errorsSpecial "errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/coze-dev/coze-go"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"gorm.io/gorm/utils"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
Ollama *llm_service.OllamaService
|
||||
Vllm *llm_service.VllmService
|
||||
toolManager *tools.Manager
|
||||
conf *config.Config
|
||||
sessionImpl *impl.SessionImpl
|
||||
workflowManager *runtime.Registry
|
||||
dingtalkOldClient *dingtalk.OldClient
|
||||
dingtalkContactClient *dingtalk.ContactClient
|
||||
dingtalkNotableClient *dingtalk.NotableClient
|
||||
Ollama *llm_service.OllamaService
|
||||
toolManager *tools.Manager
|
||||
|
||||
conf *config.Config
|
||||
sessionImpl *impl.SessionImpl
|
||||
workflowManager *runtime.Registry
|
||||
}
|
||||
|
||||
func NewHandle(
|
||||
Ollama *llm_service.OllamaService,
|
||||
Vllm *llm_service.VllmService,
|
||||
toolManager *tools.Manager,
|
||||
conf *config.Config,
|
||||
sessionImpl *impl.SessionImpl,
|
||||
|
||||
workflowManager *runtime.Registry,
|
||||
dingtalkOldClient *dingtalk.OldClient,
|
||||
dingtalkContactClient *dingtalk.ContactClient,
|
||||
dingtalkNotableClient *dingtalk.NotableClient,
|
||||
) *Handle {
|
||||
return &Handle{
|
||||
Ollama: Ollama,
|
||||
Vllm: Vllm,
|
||||
toolManager: toolManager,
|
||||
conf: conf,
|
||||
sessionImpl: sessionImpl,
|
||||
workflowManager: workflowManager,
|
||||
dingtalkOldClient: dingtalkOldClient,
|
||||
dingtalkContactClient: dingtalkContactClient,
|
||||
dingtalkNotableClient: dingtalkNotableClient,
|
||||
Ollama: Ollama,
|
||||
toolManager: toolManager,
|
||||
conf: conf,
|
||||
sessionImpl: sessionImpl,
|
||||
|
||||
workflowManager: workflowManager,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,8 +58,7 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
|
|||
|
||||
prompt, err := promptProcessor.CreatePrompt(ctx, rec)
|
||||
//意图识别
|
||||
// recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{
|
||||
recognizeMsg, err := r.Vllm.IntentRecognize(ctx, &entitys.ToolSelect{
|
||||
recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{
|
||||
Prompt: prompt,
|
||||
Tools: rec.Tasks,
|
||||
})
|
||||
|
|
@ -87,7 +69,7 @@ func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptPr
|
|||
entitys.ResLog(rec.Ch, "recognize_end", "意图识别结束")
|
||||
var match entitys.Match
|
||||
if err = json.Unmarshal([]byte(recognizeMsg), &match); err != nil {
|
||||
err = errors.SysErrf("数据结构错误:%v", err.Error())
|
||||
err = errors.SysErr("数据结构错误:%v", err.Error())
|
||||
return
|
||||
}
|
||||
rec.Match = &match
|
||||
|
|
@ -124,26 +106,21 @@ func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, rec *e
|
|||
}
|
||||
|
||||
// 校验用户权限
|
||||
if err = r.PermissionAuth(client, pointTask); err != nil {
|
||||
log.Errorf("权限验证失败: %s", err.Error())
|
||||
return
|
||||
}
|
||||
// if err = r.PermissionAuth(client, pointTask); err != nil {
|
||||
// log.Errorf("权限验证失败: %s", err.Error())
|
||||
// return
|
||||
// }
|
||||
|
||||
switch constants.TaskType(pointTask.Type) {
|
||||
case constants.TaskTypeApi:
|
||||
return r.handleApiTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeKnowle:
|
||||
return r.handleKnowle(ctx, rec, pointTask)
|
||||
case constants.TaskTypeFunc:
|
||||
return r.handleTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeBot:
|
||||
return r.HandleBot(ctx, rec, &entitys.Task{
|
||||
Index: pointTask.Index,
|
||||
})
|
||||
case constants.TaskTypeKnowle:
|
||||
return r.handleKnowle(ctx, rec, pointTask)
|
||||
|
||||
case constants.TaskTypeEinoWorkflow:
|
||||
return r.handleEinoWorkflow(ctx, rec, pointTask)
|
||||
case constants.TaskTypeCozeWorkflow:
|
||||
return r.handleCozeWorkflow(ctx, rec, pointTask)
|
||||
default:
|
||||
return r.handleOtherTask(ctx, requireData)
|
||||
}
|
||||
|
|
@ -252,113 +229,6 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
|
|||
return
|
||||
}
|
||||
|
||||
// bot 临时实现,后续转到 eino 工作流
|
||||
func (r *Handle) HandleBot(ctx context.Context, rec *entitys.Recognize, task *entitys.Task) (err error) {
|
||||
if task.Index == "bug_optimization_submit" {
|
||||
var unionId string
|
||||
entitys.ResLoading(rec.Ch, task.Index, "需求记录中...\n")
|
||||
// 获取dingtalk accessToken
|
||||
accessToken, _ := r.dingtalkOldClient.GetAccessToken()
|
||||
// Ext 中获取 sessionId
|
||||
taskExt := rec.GetTaskExt()
|
||||
if taskExt == nil {
|
||||
return errorcode.ParamErr("taskExt参数错误")
|
||||
}
|
||||
if len(taskExt.Session) > 0 {
|
||||
// 获取创建者 dingtalk unionId
|
||||
unionId = r.getUserDingtalkUnionId(ctx, accessToken, taskExt.Session)
|
||||
} else if len(taskExt.UserName) > 0 {
|
||||
unionId = r.getUserDingtalkUnionIdWithUserName(ctx, accessToken, taskExt.UserName)
|
||||
} else {
|
||||
return errorcode.ParamErr("taskExt参数错误,重要参数缺失")
|
||||
}
|
||||
|
||||
// 附件url
|
||||
var attachmentUrl string
|
||||
for _, file := range rec.UserContent.File {
|
||||
attachmentUrl = file.FileUrl
|
||||
break
|
||||
}
|
||||
|
||||
req := &dingtalk.InsertRecordReq{
|
||||
BaseId: r.conf.Dingtalk.TableDemand.BaseId,
|
||||
SheetIdOrName: r.conf.Dingtalk.TableDemand.SheetIdOrName,
|
||||
// OperatorId: tool_callback.BotBugOptimizationSubmitAdminUnionId,
|
||||
OperatorId: unionId,
|
||||
CreatorUnionId: unionId,
|
||||
Content: rec.UserContent.Text,
|
||||
AttachmentUrl: attachmentUrl,
|
||||
}
|
||||
|
||||
recordId, err := r.dingtalkNotableClient.InsertRecord(accessToken, req)
|
||||
if err != nil {
|
||||
errCode := r.dingtalkNotableClient.GetHTTPStatus(err)
|
||||
// 权限不足
|
||||
if errCode == 403 {
|
||||
return errorcode.ForbiddenErr("您当前没有AI需求表编辑权限,请联系管理员添加权限")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if recordId == "" {
|
||||
return errors.NewBusinessErr(422, "创建记录失败,请联系管理员")
|
||||
}
|
||||
var detailPage string
|
||||
entitys.ResLog(rec.Ch, task.Index, "需求记录完成")
|
||||
switch rec.OutPutScene {
|
||||
case entitys.OutPutSceneDingTalk:
|
||||
// 构建跳转链接
|
||||
detailPage = "[去查看](" + r.conf.Dingtalk.TableDemand.Url + ")"
|
||||
default:
|
||||
// 构建跳转链接
|
||||
detailPage = util.BuildJumpLink(r.conf.Dingtalk.TableDemand.Url, "去查看")
|
||||
}
|
||||
entitys.ResText(rec.Ch, task.Index, fmt.Sprintf("需求已记录,正在分配相关人员处理,请您耐心等待处理结果。点击查看工单进度:%s", detailPage))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.NewBusinessErr(422, "bot 任务未实现")
|
||||
}
|
||||
|
||||
// getUserDingtalkUnionId 获取用户的 dingtalk unionId
|
||||
func (r *Handle) getUserDingtalkUnionId(ctx context.Context, accessToken, sessionID string) (unionId string) {
|
||||
if len(sessionID) == 0 {
|
||||
// 查询用户名
|
||||
return ""
|
||||
}
|
||||
session, has, err := r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(sessionID))
|
||||
if err != nil || !has {
|
||||
log.Warnf("session not found: %s", sessionID)
|
||||
return
|
||||
}
|
||||
return r.getUserDingtalkUnionIdWithUserName(ctx, accessToken, session.UserName)
|
||||
}
|
||||
|
||||
func (r *Handle) getUserDingtalkUnionIdWithUserName(ctx context.Context, accessToken, userName string) (unionId string) {
|
||||
// 获取创建者uid 用户名 -> dingtalk uid
|
||||
creatorId, err := r.dingtalkContactClient.SearchUserOne(accessToken, userName)
|
||||
if err != nil {
|
||||
log.Warnf("search dingtalk user one failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户详情 dingtalk uid -> dingtalk unionId
|
||||
userDetails, err := r.dingtalkOldClient.QueryUserDetails(ctx, creatorId)
|
||||
if err != nil {
|
||||
log.Warnf("query user dingtalk details failed: %v", err)
|
||||
return
|
||||
}
|
||||
if userDetails == nil {
|
||||
log.Warnf("user details not found: %s", creatorId)
|
||||
return
|
||||
}
|
||||
|
||||
unionId = userDetails.UnionID
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Handle) handleApiTask(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||
var (
|
||||
request l_request.Request
|
||||
|
|
@ -424,7 +294,7 @@ func (r *Handle) handleEinoWorkflow(ctx context.Context, rec *entitys.Recognize,
|
|||
|
||||
// 工作流内部输出
|
||||
workflowId := task.Index
|
||||
_, err = r.workflowManager.Invoke(ctx, workflowId, &runtime.WorkflowArgs{Recognize: rec})
|
||||
_, err = r.workflowManager.Invoke(ctx, workflowId, rec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -432,106 +302,6 @@ func (r *Handle) handleEinoWorkflow(ctx context.Context, rec *entitys.Recognize,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (r *Handle) handleCozeWorkflow(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
|
||||
entitys.ResLoading(rec.Ch, task.Index, "正在执行工作流(coze)")
|
||||
|
||||
customClient := &http.Client{
|
||||
Timeout: time.Minute * 30,
|
||||
}
|
||||
|
||||
authCli := coze.NewTokenAuth(r.conf.Coze.ApiSecret)
|
||||
cozeCli := coze.NewCozeAPI(
|
||||
authCli,
|
||||
coze.WithBaseURL(r.conf.Coze.BaseURL),
|
||||
coze.WithHttpClient(customClient),
|
||||
)
|
||||
|
||||
// 从参数中获取workflowID
|
||||
type requestParams struct {
|
||||
Request l_request.Request `json:"request"`
|
||||
}
|
||||
var config requestParams
|
||||
err = json.Unmarshal([]byte(task.Config), &config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workflowId, ok := config.Request.Json["workflow_id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("workflow_id不能为空")
|
||||
}
|
||||
// 提取参数
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal([]byte(rec.Match.Parameters), &data)
|
||||
|
||||
req := &coze.RunWorkflowsReq{
|
||||
WorkflowID: workflowId,
|
||||
Parameters: data,
|
||||
// IsAsync: true,
|
||||
}
|
||||
|
||||
stream := config.Request.Json["stream"].(bool)
|
||||
|
||||
entitys.ResLog(rec.Ch, task.Index, "工作流执行中...")
|
||||
|
||||
if stream {
|
||||
streamResp, err := cozeCli.Workflows.Runs.Stream(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handleCozeWorkflowEvents(ctx, streamResp, cozeCli, workflowId, rec.Ch, task.Index)
|
||||
} else {
|
||||
resp, err := cozeCli.Workflows.Runs.Create(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entitys.ResJson(rec.Ch, task.Index, resp.Data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handleCozeWorkflowEvents 处理 coze 工作流事件
|
||||
func handleCozeWorkflowEvents(ctx context.Context, resp coze.Stream[coze.WorkflowEvent], cozeCli coze.CozeAPI, workflowID string, ch chan entitys.Response, index string) {
|
||||
defer resp.Close()
|
||||
for {
|
||||
event, err := resp.Recv()
|
||||
if errorsSpecial.Is(err, io.EOF) {
|
||||
fmt.Println("Stream finished")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("Error receiving event:", err)
|
||||
break
|
||||
}
|
||||
|
||||
switch event.Event {
|
||||
case coze.WorkflowEventTypeMessage:
|
||||
entitys.ResStream(ch, index, event.Message.Content)
|
||||
case coze.WorkflowEventTypeError:
|
||||
entitys.ResError(ch, index, fmt.Sprintf("工作流执行错误: %v", event.Error))
|
||||
case coze.WorkflowEventTypeDone:
|
||||
entitys.ResEnd(ch, index, "工作流执行完成")
|
||||
case coze.WorkflowEventTypeInterrupt:
|
||||
resumeReq := &coze.ResumeRunWorkflowsReq{
|
||||
WorkflowID: workflowID,
|
||||
EventID: event.Interrupt.InterruptData.EventID,
|
||||
ResumeData: "your data",
|
||||
InterruptType: event.Interrupt.InterruptData.Type,
|
||||
}
|
||||
newResp, err := cozeCli.Workflows.Runs.Resume(ctx, resumeReq)
|
||||
if err != nil {
|
||||
entitys.ResError(ch, index, fmt.Sprintf("工作流恢复执行错误: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
entitys.ResLog(ch, index, "工作流恢复执行中...")
|
||||
handleCozeWorkflowEvents(ctx, newResp, cozeCli, workflowID, ch, index)
|
||||
}
|
||||
}
|
||||
fmt.Printf("done, log:%s\n", resp.Response().LogID())
|
||||
}
|
||||
|
||||
// 权限验证
|
||||
func (r *Handle) PermissionAuth(client *gateway.Client, pointTask *model.AiTask) (err error) {
|
||||
// 授权检查权限
|
||||
|
|
|
|||
|
|
@ -1,463 +0,0 @@
|
|||
package do
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"ai_scheduler/utils"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type Macro struct {
|
||||
botGroupImpl *impl.BotGroupImpl
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
config *config.Config
|
||||
rdb *utils.Rdb
|
||||
}
|
||||
|
||||
func NewMacro(
|
||||
botGroupImpl *impl.BotGroupImpl,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
config *config.Config,
|
||||
rdb *utils.Rdb,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
) *Macro {
|
||||
return &Macro{
|
||||
botGroupImpl: botGroupImpl,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
config: config,
|
||||
rdb: rdb,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
}
|
||||
}
|
||||
|
||||
type MacroFunc func(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig, config *config.Config) (successMsg string, err error, isFinish bool)
|
||||
|
||||
func (m *Macro) Router(ctx context.Context, reqContent string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) {
|
||||
reqContent = strings.TrimSpace(reqContent)
|
||||
switch {
|
||||
case strings.HasPrefix(reqContent, "[利润同比报表]商品修改"):
|
||||
return m.ProductModify(ctx, reqContent, groupConfig)
|
||||
case strings.HasPrefix(reqContent, "[利润同比报表]商品列表"):
|
||||
return m.ProductList(ctx, groupConfig)
|
||||
case strings.HasPrefix(reqContent, "[负利润分析]导出"):
|
||||
return m.NegativeProfitGet(ctx)
|
||||
case strings.HasPrefix(reqContent, "[负利润分析]更新"):
|
||||
return m.NegativeProfitUpdate(ctx, reqContent)
|
||||
case strings.HasPrefix(reqContent, "[负利润分析]清理"):
|
||||
return m.NegativeProfitClear()
|
||||
default:
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Macro) NegativeProfitClear() (successMsg string, err error, isFinish bool) {
|
||||
dayDate := time.Now().Format(time.DateOnly)
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail})
|
||||
cond = cond.And(builder.Eq{"cache_key": dayDate})
|
||||
cond = cond.And(builder.Eq{"status": 1})
|
||||
err = m.reportDailyCacheImpl.UpdateByCond(&cond, &model.AiReportDailyCache{
|
||||
Status: 2,
|
||||
})
|
||||
if err != nil {
|
||||
err = fmt.Errorf("解析失败:%v", err)
|
||||
return
|
||||
}
|
||||
isFinish = true
|
||||
successMsg = "清理成功"
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Macro) NegativeProfitUpdate(ctx context.Context, content string) (successMsg string, err error, isFinish bool) {
|
||||
//newContent := strings.ReplaceAll(strings.TrimSpace(content), "[负利润分析]更新:", "")
|
||||
jsonData, err := ParseLossData(content)
|
||||
b, err := bbxt.NewBbxtTools(m.config, lsxd.NewLogin(m.config, m.rdb))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
dayData := now.Format(time.DateOnly)
|
||||
value, err := b.GetMapResellerLossSumProductRelation(ctx, now, m.GetReportCache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var setData = make(map[int32]*bbxt.ResellerLossSumProductRelation)
|
||||
for k, v := range jsonData {
|
||||
if _, ok := value[k]; !ok {
|
||||
continue
|
||||
}
|
||||
for productId, product := range v.Products {
|
||||
if _, ok := value[k].Products[productId]; !ok {
|
||||
continue
|
||||
}
|
||||
if value[k].Products[productId].LossReason == product.LossReason {
|
||||
continue
|
||||
}
|
||||
if _, ex := setData[k]; !ex {
|
||||
setData[k] = &bbxt.ResellerLossSumProductRelation{
|
||||
ResellerName: value[k].ResellerName,
|
||||
AfterSaleName: value[k].AfterSaleName,
|
||||
Products: make(map[int32]*bbxt.LossReason),
|
||||
}
|
||||
}
|
||||
setData[k].Products[productId] = &bbxt.LossReason{
|
||||
ProductName: product.ProductName,
|
||||
LossReason: product.LossReason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail})
|
||||
cond = cond.And(builder.Eq{"cache_key": dayData})
|
||||
cond = cond.And(builder.Eq{"status": 1})
|
||||
var cache model.AiReportDailyCache
|
||||
err = m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if cache.ID == 0 {
|
||||
|
||||
cache = model.AiReportDailyCache{
|
||||
CacheKey: dayData,
|
||||
CacheIndex: bbxt.IndexLossSumDetail,
|
||||
Value: pkg.JsonStringIgonErr(setData),
|
||||
}
|
||||
_, err = m.reportDailyCacheImpl.Add(&cache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = m.reportDailyCacheImpl.UpdateByCond(&cond, &model.AiReportDailyCache{
|
||||
Value: pkg.JsonStringIgonErr(setData),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
isFinish = true
|
||||
successMsg = "更新成功"
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Macro) NegativeProfitGet(ctx context.Context) (successMsg string, err error, isFinish bool) {
|
||||
b, err := bbxt.NewBbxtTools(m.config, lsxd.NewLogin(m.config, m.rdb))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
value, err := b.GetMapResellerLossSumProductRelation(ctx, now, m.GetReportCache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//将value转为string
|
||||
//**[供应商id]供应商名称->商务名称**\n
|
||||
//└──商品名称:亏损原因\n
|
||||
var valueString strings.Builder
|
||||
valueString.WriteString("[负利润分析]更新:\n")
|
||||
for k, v := range value {
|
||||
if len(v.AfterSaleName) == 0 {
|
||||
v.AfterSaleName = "未查找到对应商务"
|
||||
}
|
||||
valueString.WriteString(fmt.Sprintf("**[%d]%s->%s**\n", k, v.ResellerName, v.AfterSaleName))
|
||||
for kk, vv := range v.Products {
|
||||
valueString.WriteString(fmt.Sprintf("└── [%d]%s:%s\n", kk, vv.ProductName, vv.LossReason))
|
||||
}
|
||||
}
|
||||
successMsg = valueString.String()
|
||||
isFinish = true
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Macro) ProductModify(ctx context.Context, content string, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) {
|
||||
content = processString(content)
|
||||
if parts := strings.SplitN(content, ":", 2); len(parts) == 2 {
|
||||
itemInfo := strings.TrimSpace(parts[1])
|
||||
log.Infof("商品修改信息: %s", itemInfo)
|
||||
groupConfig.ProductName = itemInfo
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"config_id": groupConfig.ConfigID})
|
||||
err = m.botGroupConfigImpl.UpdateByCond(&cond, groupConfig)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("修改失败:%v", err)
|
||||
return
|
||||
}
|
||||
successMsg = "修改成功"
|
||||
isFinish = true
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Macro) ProductList(ctx context.Context, groupConfig *model.AiBotGroupConfig) (successMsg string, err error, isFinish bool) {
|
||||
if len(groupConfig.ProductName) == 0 {
|
||||
successMsg = "暂未设置"
|
||||
} else {
|
||||
successMsg = groupConfig.ProductName
|
||||
isFinish = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func processString(s string) string {
|
||||
// 替换中文逗号为英文逗号
|
||||
s = strings.ReplaceAll(s, ",", ",")
|
||||
|
||||
return string(clearSpacialFormat(s))
|
||||
}
|
||||
|
||||
func clearSpacialFormat(s string) (result []rune) {
|
||||
//过滤控制字符(如 \n, \t, \r 等)
|
||||
for _, char := range s {
|
||||
// 判断是否是控制字符(ASCII < 32 或 = 127)
|
||||
if !unicode.IsControl(char) {
|
||||
// 如果需要完全移除 \n 和 \t,可以改成:
|
||||
// if !unicode.IsControl(char)
|
||||
result = append(result, char)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ParseLossData(input string) (map[int32]*bbxt.ResellerLossSumProductRelation, error) {
|
||||
result := make(map[int32]*bbxt.ResellerLossSumProductRelation)
|
||||
|
||||
// 按行分割
|
||||
lines := strings.Split(input, "\n")
|
||||
|
||||
// 跳过第一行的标题
|
||||
startIdx := 0
|
||||
for i, line := range lines {
|
||||
if strings.HasPrefix(line, "[") && strings.Contains(line, "]") && strings.Contains(line, "->") {
|
||||
startIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var currentResellerID int32
|
||||
var currentReseller *bbxt.ResellerLossSumProductRelation
|
||||
|
||||
for i := startIdx; i < len(lines); i++ {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查是否是供应商行(以 [ 开头)
|
||||
if strings.HasPrefix(line, "[") && strings.Contains(line, "]") {
|
||||
// 解析供应商行:[25131]兴业银行-营销系统->未查找到对应商务
|
||||
|
||||
// 找到第一个 ] 的位置
|
||||
bracketEnd := strings.Index(line, "]")
|
||||
if bracketEnd == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 提取供应商ID
|
||||
idStr := line[1:bracketEnd]
|
||||
id, err := strconv.ParseInt(idStr, 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
currentResellerID = int32(id)
|
||||
|
||||
// 提取供应商名称和商务名称
|
||||
remaining := strings.TrimSpace(line[bracketEnd+1:])
|
||||
|
||||
// 分割供应商名称和商务名称
|
||||
var resellerName, afterSaleName string
|
||||
arrowIdx := strings.Index(remaining, "->")
|
||||
if arrowIdx != -1 {
|
||||
resellerName = strings.TrimSpace(remaining[:arrowIdx])
|
||||
afterSaleName = strings.TrimSpace(remaining[arrowIdx+2:])
|
||||
} else {
|
||||
resellerName = remaining
|
||||
afterSaleName = ""
|
||||
}
|
||||
|
||||
// 创建新的供应商对象
|
||||
currentReseller = &bbxt.ResellerLossSumProductRelation{
|
||||
AfterSaleName: afterSaleName,
|
||||
ResellerName: resellerName,
|
||||
Products: make(map[int32]*bbxt.LossReason),
|
||||
}
|
||||
|
||||
result[currentResellerID] = currentReseller
|
||||
|
||||
} else if strings.Contains(line, "└──") {
|
||||
// 解析商品行:└── [460]全国话费30元:未填写
|
||||
if currentReseller == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 移除└──前缀
|
||||
productLine := strings.TrimPrefix(line, "└──")
|
||||
productLine = strings.TrimSpace(productLine)
|
||||
|
||||
if !strings.HasPrefix(productLine, "[") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 找到商品ID的结束位置
|
||||
prodBracketEnd := strings.Index(productLine, "]")
|
||||
if prodBracketEnd == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 提取商品ID
|
||||
prodIDStr := productLine[1:prodBracketEnd]
|
||||
prodID, err := strconv.ParseInt(prodIDStr, 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 提取商品名称和亏损原因
|
||||
prodRemaining := strings.TrimSpace(productLine[prodBracketEnd+1:])
|
||||
|
||||
// 找到冒号分隔符
|
||||
colonIdx := strings.Index(prodRemaining, ":")
|
||||
var productName, lossReason string
|
||||
|
||||
if colonIdx != -1 {
|
||||
productName = strings.TrimSpace(prodRemaining[:colonIdx])
|
||||
lossReason = strings.TrimSpace(prodRemaining[colonIdx+1:])
|
||||
} else {
|
||||
productName = prodRemaining
|
||||
lossReason = ""
|
||||
}
|
||||
|
||||
// 添加到当前供应商的商品列表中
|
||||
currentReseller.Products[int32(prodID)] = &bbxt.LossReason{
|
||||
ProductName: productName,
|
||||
LossReason: lossReason,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (m *Macro) GetReportCache(ctx context.Context, day time.Time, totalDetail []*bbxt.ResellerLoss, bbxtObj *bbxt.BbxtTools) error {
|
||||
dayDate := day.Format(time.DateOnly)
|
||||
|
||||
// 1. 从 API 获取数据并填充
|
||||
apiRelations, err := bbxtObj.GetResellerLossMannagerAndLossReasonFromApi(ctx, totalDetail)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get API data failed: %w", err)
|
||||
}
|
||||
|
||||
// 使用 API 数据填充损失原因
|
||||
fillLossReasonFromData(totalDetail, apiRelations, "未填写")
|
||||
|
||||
// 2. 从缓存获取数据并覆盖
|
||||
cachedRelations, err := m.getCachedRelations(dayDate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get cache data failed: %w", err)
|
||||
}
|
||||
|
||||
// 使用缓存数据覆盖损失原因
|
||||
if cachedRelations != nil {
|
||||
fillLossReasonFromData(totalDetail, cachedRelations, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 从缓存获取关系数据
|
||||
func (m *Macro) getCachedRelations(dayDate string) (map[int32]*bbxt.ResellerLossSumProductRelation, error) {
|
||||
cond := builder.NewCond().
|
||||
And(builder.Eq{"cache_index": bbxt.IndexLossSumDetail}).
|
||||
And(builder.Eq{"cache_key": dayDate}).
|
||||
And(builder.Eq{"status": 1})
|
||||
|
||||
var cache model.AiReportDailyCache
|
||||
err := m.reportDailyCacheImpl.GetOneBySearchToStrut(&cond, &cache)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// 缓存不存在是正常情况
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("query cache failed: %w", err)
|
||||
}
|
||||
|
||||
if cache.ID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var relations map[int32]*bbxt.ResellerLossSumProductRelation
|
||||
if err := json.Unmarshal([]byte(cache.Value), &relations); err != nil {
|
||||
return nil, fmt.Errorf("unmarshal cache failed: %w", err)
|
||||
}
|
||||
|
||||
return relations, nil
|
||||
}
|
||||
|
||||
// 使用指定数据源填充损失原因
|
||||
func fillLossReasonFromData(
|
||||
totalDetail []*bbxt.ResellerLoss,
|
||||
relations map[int32]*bbxt.ResellerLossSumProductRelation,
|
||||
defaultReason string, // 当数据不存在时使用的默认值
|
||||
) {
|
||||
for _, detail := range totalDetail {
|
||||
resellerRelation, exists := relations[detail.ResellerId]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// 设置售后经理(只有在有值时才设置)
|
||||
if resellerRelation.AfterSaleName != "" {
|
||||
detail.Manager = resellerRelation.AfterSaleName
|
||||
}
|
||||
|
||||
// 为每个产品设置损失原因
|
||||
for _, product := range detail.ProductLoss {
|
||||
setProductLossReason(product, resellerRelation, defaultReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置单个产品的损失原因
|
||||
func setProductLossReason(
|
||||
product *bbxt.ProductLoss,
|
||||
resellerRelation *bbxt.ResellerLossSumProductRelation,
|
||||
defaultReason string,
|
||||
) {
|
||||
// 如果该经销商没有产品数据,跳过
|
||||
if resellerRelation.Products == nil {
|
||||
return
|
||||
}
|
||||
|
||||
productRelation, exists := resellerRelation.Products[product.ProductId]
|
||||
if !exists {
|
||||
if defaultReason != "" {
|
||||
product.LossReason = defaultReason
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果有损失原因则设置,否则使用默认值
|
||||
if productRelation.LossReason != "" {
|
||||
product.LossReason = productRelation.LossReason
|
||||
} else if defaultReason != "" {
|
||||
product.LossReason = defaultReason
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package do
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_report(t *testing.T) {
|
||||
con := "[利润同比报表]商品修改:官方–优酷周卡,官方–优酷月卡,官方–优酷季卡,官方–优酷年卡,,官方–爱奇艺-月卡,官方–爱奇艺-季卡,官方–爱奇艺-年卡,官方–芒果-PC周卡,官方–芒果-PC月卡,官方–芒果-PC季卡,官方–QQ音乐-绿钻月卡,官方–饿了么超级会员月卡,官方–网易云黑胶vip月卡,官方–喜马拉雅巅峰会员月卡,剪映会员7天卡,剪映会员月卡,剪映会员年卡,剪映SVIP会员7天卡,剪映SVIP会员月卡,剪映SVIP会员年卡"
|
||||
run()
|
||||
|
||||
chatId, err, i := ma.ProductModify(context.Background(), con, &model.AiBotGroupConfig{ConfigID: 1, ToolList: "8,9,10,11,12,13,16"})
|
||||
t.Log(chatId, err, i)
|
||||
}
|
||||
|
||||
var ma *Macro
|
||||
|
||||
func run() {
|
||||
configConfig, _ := config.LoadConfigWithTest()
|
||||
db, _ := utils.NewGormDb(configConfig)
|
||||
|
||||
rdb := utils.NewRdb(configConfig)
|
||||
reportDailyCacheImpl := impl.NewReportDailyCacheImpl(db)
|
||||
botGroupImpl := impl.NewBotGroupImpl(db)
|
||||
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
|
||||
ma = NewMacro(botGroupImpl, reportDailyCacheImpl, configConfig, rdb, botGroupConfigImpl)
|
||||
}
|
||||
|
|
@ -2,13 +2,9 @@ package do
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/handle"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
|
|
@ -19,7 +15,6 @@ type PromptOption interface {
|
|||
}
|
||||
|
||||
type WithSys struct {
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
func (f *WithSys) CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes []api.Message, err error) {
|
||||
|
|
@ -31,28 +26,24 @@ func (f *WithSys) CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建提示消息列表,包含系统提示、助手回复和用户内容
|
||||
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 *WithSys) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) {
|
||||
var hasFile bool
|
||||
if len(rec.UserContent.File) > 0 {
|
||||
if rec.UserContent.File != nil && len(rec.UserContent.File) > 0 {
|
||||
hasFile = true
|
||||
}
|
||||
content.WriteString(rec.UserContent.Text)
|
||||
|
|
@ -66,62 +57,23 @@ func (f *WithSys) getUserContent(ctx context.Context, rec *entitys.Recognize) (c
|
|||
content.WriteString(rec.UserContent.Tag)
|
||||
}
|
||||
|
||||
// if len(rec.ChatHis.Messages) > 0 {
|
||||
// content.WriteString("### 引用历史聊天记录:\n")
|
||||
// content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
// }
|
||||
if len(rec.ChatHis.Messages) > 0 {
|
||||
content.WriteString("### 引用历史聊天记录:\n")
|
||||
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
}
|
||||
|
||||
if hasFile {
|
||||
content.WriteString("\n")
|
||||
content.WriteString("### 文件内容:\n")
|
||||
for _, file := range rec.UserContent.File {
|
||||
handle.HandleRecognizeFile(file)
|
||||
// 文件识别
|
||||
switch file.FileType {
|
||||
case constants.FileTypeImage:
|
||||
entitys.ResLog(rec.Ch, "recognize_img_start", "图片识别中...")
|
||||
var imageContent string
|
||||
imageContent, err = f.recognizeWithImgVllm(ctx, file)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
entitys.ResLog(rec.Ch, "recognize_img_end", "图片识别完成,识别内容:"+imageContent)
|
||||
|
||||
// 解析结果回写到file
|
||||
file.FileRec = imageContent
|
||||
content.WriteString(file.FileRec)
|
||||
default:
|
||||
content.WriteString(file.FileRec)
|
||||
}
|
||||
}
|
||||
|
||||
//...do something with file
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *WithSys) recognizeWithImgVllm(ctx context.Context, file *entitys.RecognizeFile) (content string, err error) {
|
||||
if file.FileData == nil || file.FileType != constants.FileTypeImage {
|
||||
return
|
||||
}
|
||||
|
||||
client, cleanup, err := utils_vllm.NewClient(f.Config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
outMsg, err := client.RecognizeWithImgBytes(ctx,
|
||||
f.Config.DefaultPrompt.ImgRecognize.SystemPrompt,
|
||||
f.Config.DefaultPrompt.ImgRecognize.UserPrompt,
|
||||
file.FileData,
|
||||
file.FileRealMime,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return outMsg.Content, nil
|
||||
}
|
||||
|
||||
type WithDingTalkBot struct {
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +118,6 @@ func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recog
|
|||
}
|
||||
|
||||
if len(rec.ChatHis.Messages) > 0 {
|
||||
content.WriteString("\n")
|
||||
content.WriteString("### 引用历史聊天记录:\n")
|
||||
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,427 +0,0 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/biz/tools_regis"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/domain/workflow/recharge"
|
||||
"ai_scheduler/internal/domain/workflow/runtime"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"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"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coze-dev/coze-go"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// AiRouterBiz 智能路由服务
|
||||
type GroupConfigBiz struct {
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl
|
||||
ossClient *utils_oss.Client
|
||||
workflowManager *runtime.Registry
|
||||
botTools []model.AiBotTool
|
||||
toolManager *tools.Manager
|
||||
conf *config.Config
|
||||
rdb *utils.Rdb
|
||||
macro *do.Macro
|
||||
handle *do.Handle
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
func NewGroupConfigBiz(
|
||||
tools *tools_regis.ToolRegis,
|
||||
ossClient *utils_oss.Client,
|
||||
botGroupConfigImpl *impl.BotGroupConfigImpl,
|
||||
workflowManager *runtime.Registry,
|
||||
conf *config.Config,
|
||||
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
|
||||
rdb *utils.Rdb,
|
||||
macro *do.Macro,
|
||||
toolManager *tools.Manager,
|
||||
handle *do.Handle,
|
||||
) *GroupConfigBiz {
|
||||
return &GroupConfigBiz{
|
||||
botTools: tools.BootTools,
|
||||
ossClient: ossClient,
|
||||
botGroupConfigImpl: botGroupConfigImpl,
|
||||
workflowManager: workflowManager,
|
||||
conf: conf,
|
||||
reportDailyCacheImpl: reportDailyCacheImpl,
|
||||
rdb: rdb,
|
||||
macro: macro,
|
||||
toolManager: toolManager,
|
||||
handle: handle,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) GetGroupConfig(ctx context.Context, configId int32) (*model.AiBotGroupConfig, error) {
|
||||
var groupConfig model.AiBotGroupConfig
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"config_id": configId})
|
||||
err := g.botGroupConfigImpl.GetOneBySearchToStrut(&cond, &groupConfig)
|
||||
return &groupConfig, err
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model.AiBotGroupConfig) (reports []*bbxt.ReportRes, err error) {
|
||||
if groupConfig == nil {
|
||||
return
|
||||
}
|
||||
var product []string
|
||||
if groupConfig.ProductName != "" {
|
||||
product = strings.Split(groupConfig.ProductName, ",")
|
||||
}
|
||||
|
||||
reportList, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
reports, err = reportList.DailyReport(ctx, time.Now(), bbxt.DownWardValue, product, bbxt.SumFilter, g.ossClient, g.macro.GetReportCache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
//追加电商充值系统统计 - 返回统一使用[]*bbxt.ReportRes
|
||||
rechargeReports, err := g.rechargeDailyReport(ctx, time.Now(), nil, g.ossClient)
|
||||
if err != nil || len(rechargeReports) == 0 {
|
||||
return
|
||||
}
|
||||
reports = append(rechargeReports, reports...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// rechargeDailyReport 获取电商充值系统统计报告
|
||||
func (g *GroupConfigBiz) rechargeDailyReport(ctx context.Context, now time.Time, productNames []string, ossClient *utils_oss.Client) (reports []*bbxt.ReportRes, err error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
workflowId := recharge.WorkflowIDStatisticsOursProduct
|
||||
args := &runtime.WorkflowArgs{
|
||||
Args: map[string]any{
|
||||
"product_names": productNames,
|
||||
"now": now,
|
||||
},
|
||||
}
|
||||
res, err := g.workflowManager.Invoke(ctx, workflowId, args)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("imgUrl: %s", res["url"].(string))
|
||||
|
||||
reports = []*bbxt.ReportRes{
|
||||
{
|
||||
ReportName: "我们的商品统计(电商充值系统)",
|
||||
Title: res["title"].(string),
|
||||
Path: res["path"].(string),
|
||||
Url: res["url"].(string),
|
||||
Data: res["data"].([][]string),
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) handleReport(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool, groupConfig *model.AiBotGroupConfig) error {
|
||||
var configData entitys.ConfigDataReport
|
||||
err := json.Unmarshal([]byte(rec.Match.Parameters), &configData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := time.Parse(time.DateTime, configData.Time)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02 15:04", configData.Time)
|
||||
if err != nil {
|
||||
t, err = time.Parse("2006-01-02", configData.Time)
|
||||
if err != nil {
|
||||
log.Infof("时间识别失败:%s", configData.Time)
|
||||
entitys.ResText(rec.Ch, "", "时间识别失败了!可以给我一份比较具体的时间吗,例如“2025-12-31 12:00,抱歉抱歉😀")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rep, err := bbxt.NewBbxtTools(g.conf, lsxd.NewLogin(g.conf, g.rdb))
|
||||
uploader := bbxt.NewUploader(g.ossClient, g.conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var reports []*bbxt.ReportRes
|
||||
switch rec.Match.Index {
|
||||
case "report_loss_analysis":
|
||||
repo, _err := rep.StatisOursProductLossSum(ctx, t, g.macro.GetReportCache)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, repo)
|
||||
case "report_sales_analysis":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := rep.GetStatisOfficialProductSum(t, product)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, repo)
|
||||
|
||||
case "report_ranking_of_distributors":
|
||||
repo, _err := rep.GetProfitRankingSum(t)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, repo)
|
||||
case "report_daily":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := rep.DailyReport(ctx, t, bbxt.DownWardValue, product, bbxt.SumFilter, nil, g.macro.GetReportCache)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
rechargeReport, _err := g.rechargeDailyReport(ctx, t, product, nil)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, rechargeReport...)
|
||||
reports = append(reports, repo...)
|
||||
|
||||
case "report_daily_recharge":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := g.rechargeDailyReport(ctx, t, product, nil)
|
||||
if _err != nil || len(repo) == 0 {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, repo...)
|
||||
case "report_sale_down_analysis":
|
||||
product := strings.Split(groupConfig.ProductName, ",")
|
||||
repo, _err := rep.GetStatisOfficialProductSumDecline(t, bbxt.DownWardValue, product, bbxt.SumFilter)
|
||||
if _err != nil {
|
||||
return _err
|
||||
}
|
||||
reports = append(reports, repo)
|
||||
default:
|
||||
return fmt.Errorf("未找到的报表:%s", rec.Match.Index)
|
||||
}
|
||||
|
||||
for _, report := range reports {
|
||||
if report == nil {
|
||||
continue
|
||||
}
|
||||
err = uploader.Run(report)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
|
||||
entitys.ResText(rec.Ch, "", fmt.Sprintf("%s", report.Title, report.Url))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) handleMatch(ctx context.Context, rec *entitys.Recognize, groupConfig *model.AiBotGroupConfig) (err error) {
|
||||
|
||||
if !rec.Match.IsMatch {
|
||||
if len(rec.Match.Chat) != 0 {
|
||||
entitys.ResText(rec.Ch, "", rec.Match.Chat)
|
||||
} else {
|
||||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
}
|
||||
return
|
||||
}
|
||||
var pointTask *model.AiBotTool
|
||||
for _, task := range g.botTools {
|
||||
if task.Index == rec.Match.Index {
|
||||
pointTask = &task
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
rec.OutPutScene = entitys.OutPutSceneDingTalk
|
||||
if pointTask == nil || pointTask.Index == "other" {
|
||||
return g.otherTask(ctx, rec)
|
||||
}
|
||||
|
||||
switch constants.TaskType(pointTask.Type) {
|
||||
case constants.TaskTypeFunc:
|
||||
return g.handleTask(ctx, rec, pointTask)
|
||||
case constants.TaskTypeBot:
|
||||
return g.handle.HandleBot(ctx, rec, &entitys.Task{
|
||||
Index: pointTask.Index,
|
||||
})
|
||||
case constants.TaskTypeReport:
|
||||
return g.handleReport(ctx, rec, pointTask, groupConfig)
|
||||
case constants.TaskTypeCozeWorkflow:
|
||||
return g.handleCozeWorkflow(ctx, rec, pointTask)
|
||||
default:
|
||||
return g.otherTask(ctx, rec)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) getGroupTools(ctx context.Context, groupConfig *model.AiBotGroupConfig) (tools []model.AiBotTool, err error) {
|
||||
if len(g.botTools) == 0 {
|
||||
return
|
||||
}
|
||||
var (
|
||||
groupRegisTools = make(map[int]struct{})
|
||||
)
|
||||
if groupConfig.ToolList != "" {
|
||||
groupToolList := strings.Split(groupConfig.ToolList, ",")
|
||||
for _, tool := range groupToolList {
|
||||
if tool == "" {
|
||||
continue
|
||||
}
|
||||
num, _err := strconv.Atoi(tool)
|
||||
if _err != nil {
|
||||
continue
|
||||
}
|
||||
groupRegisTools[num] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range g.botTools {
|
||||
if v.PermissionType == constants.PermissionTypeNone {
|
||||
tools = append(tools, v)
|
||||
continue
|
||||
}
|
||||
if _, ex := groupRegisTools[int(v.ToolID)]; ex {
|
||||
tools = append(tools, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (q *GroupConfigBiz) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) {
|
||||
var configData entitys.ConfigDataTool
|
||||
err = json.Unmarshal([]byte(task.Config), &configData)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = q.toolManager.ExecuteTool(ctx, configData.Tool, rec)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) handleCozeWorkflow(ctx context.Context, rec *entitys.Recognize, task *model.AiBotTool) (err error) {
|
||||
entitys.ResLoading(rec.Ch, task.Index, "正在执行工作流(coze)\n")
|
||||
|
||||
customClient := &http.Client{
|
||||
Timeout: time.Minute * 30,
|
||||
}
|
||||
|
||||
authCli := coze.NewTokenAuth(g.conf.Coze.ApiSecret)
|
||||
cozeCli := coze.NewCozeAPI(
|
||||
authCli,
|
||||
coze.WithBaseURL(g.conf.Coze.BaseURL),
|
||||
coze.WithHttpClient(customClient),
|
||||
)
|
||||
|
||||
// 从参数中获取workflowID
|
||||
type requestParams struct {
|
||||
Request l_request.Request `json:"request"`
|
||||
}
|
||||
var config requestParams
|
||||
err = json.Unmarshal([]byte(task.Config), &config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
workflowId, ok := config.Request.Json["workflow_id"].(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("workflow_id不能为空")
|
||||
}
|
||||
// 提取参数
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal([]byte(rec.Match.Parameters), &data)
|
||||
|
||||
req := &coze.RunWorkflowsReq{
|
||||
WorkflowID: workflowId,
|
||||
Parameters: data,
|
||||
// IsAsync: true,
|
||||
}
|
||||
|
||||
stream := config.Request.Json["stream"].(bool)
|
||||
|
||||
entitys.ResLog(rec.Ch, task.Index, "工作流执行中...")
|
||||
|
||||
if stream {
|
||||
streamResp, err := cozeCli.Workflows.Runs.Stream(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.handleCozeWorkflowEvents(ctx, streamResp, cozeCli, workflowId, rec.Ch, task.Index)
|
||||
} else {
|
||||
resp, err := cozeCli.Workflows.Runs.Create(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entitys.ResJson(rec.Ch, task.Index, resp.Data)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handleCozeWorkflowEvents 处理 coze 工作流事件
|
||||
func (g *GroupConfigBiz) handleCozeWorkflowEvents(ctx context.Context, resp coze.Stream[coze.WorkflowEvent], cozeCli coze.CozeAPI, workflowID string, ch chan entitys.Response, index string) {
|
||||
defer resp.Close()
|
||||
for {
|
||||
event, err := resp.Recv()
|
||||
if errors.Is(err, io.EOF) {
|
||||
fmt.Println("Stream finished")
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("Error receiving event:", err)
|
||||
break
|
||||
}
|
||||
|
||||
switch event.Event {
|
||||
case coze.WorkflowEventTypeMessage:
|
||||
entitys.ResStream(ch, index, event.Message.Content)
|
||||
case coze.WorkflowEventTypeError:
|
||||
entitys.ResError(ch, index, fmt.Sprintf("工作流执行错误: %v", event.Error))
|
||||
case coze.WorkflowEventTypeDone:
|
||||
entitys.ResEnd(ch, index, "工作流执行完成")
|
||||
case coze.WorkflowEventTypeInterrupt:
|
||||
resumeReq := &coze.ResumeRunWorkflowsReq{
|
||||
WorkflowID: workflowID,
|
||||
EventID: event.Interrupt.InterruptData.EventID,
|
||||
ResumeData: "your data",
|
||||
InterruptType: event.Interrupt.InterruptData.Type,
|
||||
}
|
||||
newResp, err := cozeCli.Workflows.Runs.Resume(ctx, resumeReq)
|
||||
if err != nil {
|
||||
entitys.ResError(ch, index, fmt.Sprintf("工作流恢复执行错误: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
entitys.ResLog(ch, index, "工作流恢复执行中...")
|
||||
g.handleCozeWorkflowEvents(ctx, newResp, cozeCli, workflowID, ch, index)
|
||||
}
|
||||
}
|
||||
fmt.Printf("done, log:%s\n", resp.Response().LogID())
|
||||
}
|
||||
|
||||
func (g *GroupConfigBiz) otherTask(ctx context.Context, rec *entitys.Recognize) (err error) {
|
||||
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
|
||||
return
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_report(t *testing.T) {
|
||||
run()
|
||||
chatId, err := groupConfigBiz.GetReportLists(context.Background(), nil)
|
||||
t.Log(chatId, err)
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
|
|
@ -39,26 +38,21 @@ func (a *Auth) GetAccessToken(ctx context.Context, clientId string, clientSecret
|
|||
return nil, errors.New("clientId is empty")
|
||||
}
|
||||
accessToken := a.redis.Get(ctx, a.getKey(clientId)).Val()
|
||||
var expire time.Duration
|
||||
if accessToken == "" {
|
||||
dingTalkAuthRes, _err := a.getNewAccessToken(ctx, clientId, clientSecret)
|
||||
if _err != nil {
|
||||
return nil, _err
|
||||
}
|
||||
expire = time.Duration(dingTalkAuthRes.ExpireIn-3600) * time.Second
|
||||
err = a.redis.SetEx(ctx, a.getKey(clientId), dingTalkAuthRes.AccessToken, expire).Err()
|
||||
err = a.redis.SetEx(ctx, a.getKey(clientId), dingTalkAuthRes.AccessToken, time.Duration(dingTalkAuthRes.ExpireIn-3600)*time.Second).Err()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = dingTalkAuthRes.AccessToken
|
||||
} else {
|
||||
expire, _ = a.redis.TTL(ctx, a.getKey(clientId)).Result()
|
||||
}
|
||||
return &AuthInfo{
|
||||
ClientId: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
AccessToken: accessToken,
|
||||
Expire: expire,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +60,6 @@ func (a *Auth) getKey(clientId string) string {
|
|||
return a.cfg.Redis.Key + ":" + constants.DingTalkAuthBaseKeyPrefix + ":" + clientId
|
||||
}
|
||||
|
||||
func (a *Auth) getKeyBot(botCode string) string {
|
||||
return a.cfg.Redis.Key + ":" + constants.DingTalkAuthBaseKeyBotPrefix + ":" + botCode
|
||||
}
|
||||
|
||||
func (a *Auth) getNewAccessToken(ctx context.Context, clientId string, clientSecret string) (auth DingTalkAuthIRes, err error) {
|
||||
if clientId == "" || clientSecret == "" {
|
||||
err = errors.New("clientId or clientSecret is empty")
|
||||
|
|
@ -99,61 +89,30 @@ func (a *Auth) GetTokenFromBotOption(ctx context.Context, botOption ...BotOption
|
|||
option(botInfo)
|
||||
}
|
||||
|
||||
if botInfo.Id == 0 && botInfo.BotConfig == nil && botInfo.BotCode == "" {
|
||||
if botInfo.id == 0 && botInfo.botConfig == nil {
|
||||
err = errors.New("botInfo is nil")
|
||||
return
|
||||
}
|
||||
|
||||
if botInfo.BotConfig == nil {
|
||||
err = a.GetBotConfigFromModel(botInfo)
|
||||
if botInfo.botConfig == nil {
|
||||
var botConfigDo model.AiBotConfig
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"bot_id": botInfo.id})
|
||||
err = a.botConfigImpl.GetOneBySearchToStrut(&cond, &botConfigDo)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
authInfo := a.redis.Get(ctx, a.getKeyBot(botInfo.BotConfig.RobotCode)).Val()
|
||||
if authInfo == "" {
|
||||
var botConfig entitys.DingTalkBot
|
||||
err = json.Unmarshal([]byte(botInfo.BotConfig.BotConfig), &botConfig)
|
||||
if err != nil {
|
||||
log.Infof("初始化“%s”失败:%s", botInfo.BotConfig.BotName, err.Error())
|
||||
if botConfigDo.BotID == 0 {
|
||||
err = errors.New("未找到机器人服务配置")
|
||||
return
|
||||
}
|
||||
token, err = a.GetAccessToken(ctx, botConfig.ClientId, botConfig.ClientSecret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = a.redis.SetEx(ctx, a.getKeyBot(botInfo.BotConfig.RobotCode), pkg.JsonStringIgonErr(token), token.Expire).Err()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
var tokenData AuthInfo
|
||||
err = json.Unmarshal([]byte(authInfo), &tokenData)
|
||||
token = &tokenData
|
||||
botInfo.botConfig = &botConfigDo
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (a *Auth) GetBotConfigFromModel(botInfo *Bot) (err error) {
|
||||
var (
|
||||
botConfigDo model.AiBotConfig
|
||||
)
|
||||
cond := builder.NewCond()
|
||||
if botInfo.Id > 0 {
|
||||
cond = cond.And(builder.Eq{"bot_id": botInfo.Id})
|
||||
}
|
||||
if botInfo.BotCode != "" {
|
||||
cond = cond.And(builder.Eq{"robot_code": botInfo.BotCode})
|
||||
}
|
||||
err = a.botConfigImpl.GetOneBySearchToStrut(&cond, &botConfigDo)
|
||||
var botConfig entitys.DingTalkBot
|
||||
err = json.Unmarshal([]byte(botInfo.botConfig.BotConfig), &botConfig)
|
||||
if err != nil {
|
||||
log.Infof("初始化“%s”失败:%s", botInfo.botConfig.BotName, err.Error())
|
||||
return
|
||||
}
|
||||
if botConfigDo.BotID == 0 {
|
||||
err = errors.New("未找到机器人服务配置")
|
||||
return
|
||||
}
|
||||
botInfo.BotConfig = &botConfigDo
|
||||
return nil
|
||||
return a.GetAccessToken(ctx, botConfig.ClientId, botConfig.ClientSecret)
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,34 +3,19 @@ package dingtalk
|
|||
import "ai_scheduler/internal/data/model"
|
||||
|
||||
type Bot struct {
|
||||
Id int
|
||||
BotCode string
|
||||
BotConfig *model.AiBotConfig
|
||||
id int
|
||||
botConfig *model.AiBotConfig
|
||||
}
|
||||
type BotOption func(*Bot)
|
||||
|
||||
func WithId(id int) BotOption {
|
||||
return func(b *Bot) {
|
||||
b.Id = id
|
||||
b.id = id
|
||||
}
|
||||
}
|
||||
|
||||
func WithBotConfig(BotConfig *model.AiBotConfig) BotOption {
|
||||
func WithBootConfig(BotConfig *model.AiBotConfig) BotOption {
|
||||
return func(bot *Bot) {
|
||||
bot.BotConfig = BotConfig
|
||||
}
|
||||
}
|
||||
|
||||
func WithBotCode(BotCode string) BotOption {
|
||||
return func(bot *Bot) {
|
||||
bot.BotCode = BotCode
|
||||
}
|
||||
}
|
||||
|
||||
func WithBot(botSelf *Bot) BotOption {
|
||||
return func(bot *Bot) {
|
||||
bot.BotCode = botSelf.BotCode
|
||||
bot.Id = botSelf.Id
|
||||
bot.BotConfig = botSelf.BotConfig
|
||||
bot.botConfig = BotConfig
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,5 +8,4 @@ var ProviderSetDingTalk = wire.NewSet(
|
|||
NewUser,
|
||||
NewAuth,
|
||||
NewDept,
|
||||
NewSendCardClient,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,292 +0,0 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
|
||||
util "github.com/alibabacloud-go/tea-utils/v2/service"
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const DefaultInterval = 100 * time.Millisecond
|
||||
const HeardBeatX = 100
|
||||
|
||||
type SendCardClient struct {
|
||||
Auth *Auth
|
||||
CardClient *sync.Map
|
||||
mu sync.RWMutex // 保护 CardClient 的并发访问
|
||||
logger log.AllLogger // 日志记录
|
||||
botOption *Bot
|
||||
}
|
||||
|
||||
func NewSendCardClient(auth *Auth, logger log.AllLogger) *SendCardClient {
|
||||
return &SendCardClient{
|
||||
Auth: auth,
|
||||
CardClient: &sync.Map{},
|
||||
logger: logger,
|
||||
botOption: &Bot{},
|
||||
}
|
||||
}
|
||||
|
||||
// initClient 初始化或复用 DingTalk 客户端
|
||||
func (s *SendCardClient) initClient(robotCode string) (*dingtalkim_1_0.Client, error) {
|
||||
if client, ok := s.CardClient.Load(robotCode); ok {
|
||||
return client.(*dingtalkim_1_0.Client), nil
|
||||
}
|
||||
s.botOption.BotCode = robotCode
|
||||
config := &openapi.Config{
|
||||
Protocol: tea.String("https"),
|
||||
RegionId: tea.String("central"),
|
||||
}
|
||||
client, err := dingtalkim_1_0.NewClient(config)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to init DingTalk client")
|
||||
return nil, fmt.Errorf("init client failed: %w", err)
|
||||
}
|
||||
|
||||
s.CardClient.Store(robotCode, client)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (s *SendCardClient) NewCard(ctx context.Context, cardSend *CardSend) error {
|
||||
// 参数校验
|
||||
if (len(cardSend.ContentSlice) == 0 || cardSend.ContentSlice == nil) && cardSend.ContentChannel == nil {
|
||||
return errors.New("卡片内容不能为空")
|
||||
}
|
||||
if cardSend.UpdateInterval == 0 {
|
||||
cardSend.UpdateInterval = DefaultInterval // 默认更新间隔
|
||||
}
|
||||
if cardSend.Title == "" {
|
||||
cardSend.Title = "钉钉卡片"
|
||||
}
|
||||
//替换标题
|
||||
replace, err := pkg.SafeReplace(string(cardSend.Template), "${title}", cardSend.Title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cardSend.Template = constants.CardTemp(replace)
|
||||
// 初始化客户端
|
||||
client, err := s.initClient(cardSend.RobotCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化client失败: %w", err)
|
||||
}
|
||||
|
||||
// 生成卡片实例ID
|
||||
cardInstanceId, err := uuid.NewUUID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建uuid失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建初始请求
|
||||
request, err := s.buildBaseRequest(cardSend, cardInstanceId.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送初始卡片
|
||||
if _, err := s.SendInteractiveCard(ctx, request, cardSend.RobotCode, client); err != nil {
|
||||
return fmt.Errorf("发送初始卡片失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理切片内容(同步)
|
||||
if len(cardSend.ContentSlice) > 0 {
|
||||
if err := s.processContentSlice(ctx, cardSend, cardInstanceId.String(), client); err != nil {
|
||||
return fmt.Errorf("内容同步失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理通道内容(异步)
|
||||
if cardSend.ContentChannel != nil {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.processContentChannel(ctx, cardSend, cardInstanceId.String(), client)
|
||||
}()
|
||||
wg.Wait()
|
||||
log.Info("处理通道结束")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildBaseRequest 构建基础请求
|
||||
func (s *SendCardClient) buildBaseRequest(cardSend *CardSend, cardInstanceId string) (*dingtalkim_1_0.SendRobotInteractiveCardRequest, error) {
|
||||
cardData := fmt.Sprintf(string(cardSend.Template), "") // 初始空内容
|
||||
request := &dingtalkim_1_0.SendRobotInteractiveCardRequest{
|
||||
CardTemplateId: tea.String("StandardCard"),
|
||||
CardBizId: tea.String(cardInstanceId),
|
||||
CardData: tea.String(cardData),
|
||||
RobotCode: tea.String(cardSend.RobotCode),
|
||||
SendOptions: &dingtalkim_1_0.SendRobotInteractiveCardRequestSendOptions{},
|
||||
PullStrategy: tea.Bool(false),
|
||||
}
|
||||
|
||||
switch cardSend.ConversationType {
|
||||
case constants.ConversationTypeGroup:
|
||||
request.SetOpenConversationId(cardSend.ConversationId)
|
||||
case constants.ConversationTypeSingle:
|
||||
receiver, err := json.Marshal(map[string]string{"userId": cardSend.SenderStaffId})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("数据整理失败: %w", err)
|
||||
}
|
||||
request.SetSingleChatReceiver(string(receiver))
|
||||
default:
|
||||
return nil, errors.New("未知的聊天场景")
|
||||
}
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// processContentChannel 处理通道内容(异步更新)
|
||||
func (s *SendCardClient) processContentChannel(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
s.logger.Error("panic in processContentChannel")
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(cardSend.UpdateInterval)
|
||||
defer ticker.Stop()
|
||||
heartbeatTicker := time.NewTicker(time.Duration(HeardBeatX) * DefaultInterval)
|
||||
defer heartbeatTicker.Stop()
|
||||
|
||||
var (
|
||||
contentBuilder strings.Builder
|
||||
lastUpdate = time.Now()
|
||||
)
|
||||
for {
|
||||
|
||||
select {
|
||||
case content, ok := <-cardSend.ContentChannel:
|
||||
if !ok {
|
||||
// 通道关闭,发送最终内容
|
||||
if contentBuilder.Len() > 0 {
|
||||
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
|
||||
log.Info("contentBuilder.Len()修改失败1")
|
||||
s.logger.Errorf("更新卡片失败1:%s", err.Error())
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
contentBuilder.WriteString(content)
|
||||
if contentBuilder.Len() > 0 {
|
||||
if err := s.updateCardContent(ctx, cardSend, cardInstanceId, contentBuilder.String(), client); err != nil {
|
||||
log.Info("contentBuilder.Len()修改失败2")
|
||||
s.logger.Errorf("更新卡片失败2:%s", err.Error())
|
||||
}
|
||||
}
|
||||
lastUpdate = time.Now()
|
||||
|
||||
case <-heartbeatTicker.C:
|
||||
if time.Now().Unix()-lastUpdate.Unix() >= HeardBeatX {
|
||||
log.Infof("心跳超时,当前时间:%d,最后时间:%d", time.Now().Unix(), lastUpdate.Unix())
|
||||
return
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
log.Info("send_card上下文失效")
|
||||
s.logger.Info("context canceled, stop channel processing")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// processContentSlice 处理切片内容(同步更新)
|
||||
func (s *SendCardClient) processContentSlice(ctx context.Context, cardSend *CardSend, cardInstanceId string, client *dingtalkim_1_0.Client) error {
|
||||
var contentBuilder strings.Builder
|
||||
for _, content := range cardSend.ContentSlice {
|
||||
|
||||
contentBuilder.WriteString(content)
|
||||
err := s.updateCardRequest(ctx, &UpdateCardRequest{
|
||||
Template: string(cardSend.Template),
|
||||
Content: contentBuilder.String(),
|
||||
Client: client,
|
||||
RobotCode: cardSend.RobotCode,
|
||||
CardInstanceId: cardInstanceId,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("更新卡片失败: %w", err)
|
||||
}
|
||||
time.Sleep(cardSend.UpdateInterval) // 控制更新频率
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateCardContent 封装卡片更新逻辑
|
||||
func (s *SendCardClient) updateCardContent(ctx context.Context, cardSend *CardSend, cardInstanceId, content string, client *dingtalkim_1_0.Client) error {
|
||||
err := s.updateCardRequest(ctx, &UpdateCardRequest{
|
||||
Template: string(cardSend.Template),
|
||||
Content: content,
|
||||
Client: client,
|
||||
RobotCode: cardSend.RobotCode,
|
||||
CardInstanceId: cardInstanceId,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SendCardClient) updateCardRequest(ctx context.Context, updateCardRequest *UpdateCardRequest) error {
|
||||
content, err := pkg.SafeReplace(updateCardRequest.Template, "%s", updateCardRequest.Content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateRequest := &dingtalkim_1_0.UpdateRobotInteractiveCardRequest{
|
||||
CardBizId: tea.String(updateCardRequest.CardInstanceId),
|
||||
CardData: tea.String(content),
|
||||
}
|
||||
_, err = s.UpdateInteractiveCard(ctx, updateRequest, updateCardRequest.RobotCode, updateCardRequest.Client)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateInteractiveCard 更新交互卡片(封装错误处理)
|
||||
func (s *SendCardClient) UpdateInteractiveCard(ctx context.Context, request *dingtalkim_1_0.UpdateRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (*dingtalkim_1_0.UpdateRobotInteractiveCardResponse, error) {
|
||||
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get token failed: %w", err)
|
||||
}
|
||||
|
||||
headers := &dingtalkim_1_0.UpdateRobotInteractiveCardHeaders{
|
||||
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
|
||||
}
|
||||
|
||||
response, err := client.UpdateRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API call failed: %w,request:%v", err, request.String())
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// SendInteractiveCard 发送交互卡片(封装错误处理)
|
||||
func (s *SendCardClient) SendInteractiveCard(ctx context.Context, request *dingtalkim_1_0.SendRobotInteractiveCardRequest, robotCode string, client *dingtalkim_1_0.Client) (res *dingtalkim_1_0.SendRobotInteractiveCardResponse, err error) {
|
||||
err = s.Auth.GetBotConfigFromModel(s.botOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化bot失败: %w", err)
|
||||
}
|
||||
authInfo, err := s.Auth.GetTokenFromBotOption(ctx, WithBot(s.botOption))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get token failed: %w", err)
|
||||
}
|
||||
|
||||
headers := &dingtalkim_1_0.SendRobotInteractiveCardHeaders{
|
||||
XAcsDingtalkAccessToken: tea.String(authInfo.AccessToken),
|
||||
}
|
||||
|
||||
response, err := client.SendRobotInteractiveCardWithOptions(request, headers, &util.RuntimeOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("API call failed: %w", err)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
package dingtalk
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"time"
|
||||
|
||||
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
|
||||
)
|
||||
import "time"
|
||||
|
||||
type DingTalkAuthIRes struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
|
|
@ -83,28 +78,7 @@ type DeptResResult struct {
|
|||
}
|
||||
|
||||
type AuthInfo struct {
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
Expire time.Duration `json:"expireIn"`
|
||||
}
|
||||
|
||||
type CardSend struct {
|
||||
RobotCode string
|
||||
ConversationType constants.ConversationType
|
||||
ConversationId string
|
||||
Template constants.CardTemp
|
||||
SenderStaffId string
|
||||
Title string
|
||||
ContentSlice []string
|
||||
ContentChannel chan string
|
||||
UpdateInterval time.Duration // 控制通道更新的频率
|
||||
}
|
||||
|
||||
type UpdateCardRequest struct {
|
||||
Template string
|
||||
Content string
|
||||
Client *dingtalkim_1_0.Client
|
||||
RobotCode string
|
||||
CardInstanceId string
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,14 +65,17 @@ func HandleRecognizeFile(files *entitys.RecognizeFile) {
|
|||
// 分支3:仅有数据、无类型→内容检测并填充
|
||||
if len(files.FileData) > 0 && len(strings.TrimSpace(files.FileType.String())) == 0 {
|
||||
if len(files.FileData) > maxSize {
|
||||
files.FileType = constants.FileTypeUnknown
|
||||
files.FileType = constants.Caller(constants.FileTypeUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
reader := bytes.NewReader(files.FileData)
|
||||
detected, fileRealMime := detectFileType(reader, "")
|
||||
files.FileType = detected
|
||||
files.FileRealMime = fileRealMime
|
||||
detected := detectFileType(reader, "")
|
||||
if detected == constants.FileTypeUnknown {
|
||||
files.FileType = constants.Caller(constants.FileTypeUnknown)
|
||||
return
|
||||
}
|
||||
files.FileType = constants.Caller(detected)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -80,19 +83,18 @@ func HandleRecognizeFile(files *entitys.RecognizeFile) {
|
|||
if len(files.FileUrl) > 0 {
|
||||
fileBytes, contentType, err := downloadFile(files.FileUrl)
|
||||
if err != nil || len(fileBytes) == 0 {
|
||||
files.FileType = constants.FileTypeUnknown
|
||||
files.FileType = constants.Caller(constants.FileTypeUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
if len(fileBytes) > maxSize {
|
||||
// 超限:不写入数据,类型置 unknown
|
||||
files.FileType = constants.FileTypeUnknown
|
||||
files.FileType = constants.Caller(constants.FileTypeUnknown)
|
||||
return
|
||||
}
|
||||
|
||||
// 优先使用响应头的 Content-Type 映射
|
||||
detected := mapToFileType(contentType)
|
||||
fileRealMime := contentType
|
||||
|
||||
if detected == constants.FileTypeUnknown {
|
||||
// 回退:内容检测 + URL 文件名扩展名辅助
|
||||
|
|
@ -101,13 +103,17 @@ func HandleRecognizeFile(files *entitys.RecognizeFile) {
|
|||
fname = filepath.Base(u.Path)
|
||||
}
|
||||
reader := bytes.NewReader(fileBytes)
|
||||
detected, fileRealMime = detectFileType(reader, fname)
|
||||
detected = detectFileType(reader, fname)
|
||||
}
|
||||
|
||||
// 写入数据
|
||||
files.FileData = fileBytes
|
||||
files.FileType = detected
|
||||
files.FileRealMime = fileRealMime
|
||||
|
||||
if detected == constants.FileTypeUnknown {
|
||||
files.FileType = constants.Caller(constants.FileTypeUnknown)
|
||||
return
|
||||
}
|
||||
files.FileType = constants.Caller(detected)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +150,7 @@ func downloadFile(fileUrl string) (fileBytes []byte, contentType string, err err
|
|||
}
|
||||
|
||||
// detectFileType 判断文件类型
|
||||
func detectFileType(file io.ReadSeeker, filename string) (constants.FileType, string) {
|
||||
func detectFileType(file io.ReadSeeker, filename string) constants.FileType {
|
||||
// 1. 读取文件头检测 MIME
|
||||
buffer := make([]byte, 512)
|
||||
n, _ := file.Read(buffer)
|
||||
|
|
@ -154,7 +160,7 @@ func detectFileType(file io.ReadSeeker, filename string) (constants.FileType, st
|
|||
for fileType, items := range constants.FileTypeMappings {
|
||||
for _, item := range items {
|
||||
if !strings.HasPrefix(item, ".") && item == detectedMIME {
|
||||
return fileType, detectedMIME
|
||||
return fileType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,10 +170,10 @@ func detectFileType(file io.ReadSeeker, filename string) (constants.FileType, st
|
|||
for fileType, items := range constants.FileTypeMappings {
|
||||
for _, item := range items {
|
||||
if strings.HasPrefix(item, ".") && item == ext {
|
||||
return fileType, ext
|
||||
return fileType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return constants.FileTypeUnknown, ""
|
||||
return constants.FileTypeUnknown
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
package qywx
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Auth struct {
|
||||
redis *redis.Client
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewAuth(cfg *config.Config, redis *utils.Rdb) *Auth {
|
||||
return &Auth{
|
||||
redis: redis.Rdb,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Auth) GetAccessToken(ctx context.Context, corpid string, corpsecret string) (authInfo *AuthInfo, err error) {
|
||||
if corpid == "" {
|
||||
return nil, errors.New("corpid is empty")
|
||||
}
|
||||
accessToken := a.redis.Get(ctx, a.getKey(corpsecret)).Val()
|
||||
var expire time.Duration
|
||||
if accessToken == "" {
|
||||
authRes, _err := a.getNewAccessToken(ctx, corpid, corpsecret)
|
||||
if _err != nil {
|
||||
return nil, _err
|
||||
}
|
||||
expire = time.Duration(authRes.ExpiresIn-60) * time.Second
|
||||
err = a.redis.SetEx(ctx, a.getKey(corpsecret), authRes.AccessToken, expire).Err()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accessToken = authRes.AccessToken
|
||||
} else {
|
||||
expire, _ = a.redis.TTL(ctx, a.getKey(corpsecret)).Result()
|
||||
}
|
||||
return &AuthInfo{
|
||||
Corpid: corpid,
|
||||
Corpsecret: corpsecret,
|
||||
AccessToken: accessToken,
|
||||
Expire: expire,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *Auth) getKey(corpsecret string) string {
|
||||
return a.cfg.Redis.Key + ":" + constants.QywxAuthBaseKeyPrefix + ":" + corpsecret
|
||||
}
|
||||
|
||||
func (a *Auth) getNewAccessToken(ctx context.Context, corpid string, corpsecret string) (auth AuthRes, err error) {
|
||||
if corpid == "" || corpsecret == "" {
|
||||
err = errors.New("corpid or corpsecret is empty")
|
||||
return
|
||||
}
|
||||
|
||||
req := l_request.Request{
|
||||
Method: http.MethodGet,
|
||||
Url: "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + corpid + "&corpsecret=" + corpsecret,
|
||||
}
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(res.Content, &auth)
|
||||
if auth.Errcode != 0 {
|
||||
err = fmt.Errorf("请求失败:%s", auth.Errmsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type UploadMediaRes struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Type string `json:"type"`
|
||||
MediaId string `json:"media_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
package qywx
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
groupImpl *impl.BotGroupQywxImpl
|
||||
auth *Auth
|
||||
}
|
||||
|
||||
func NewGroup(groupImpl *impl.BotGroupQywxImpl, auth *Auth) *Group {
|
||||
return &Group{
|
||||
groupImpl: groupImpl,
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
// Create 方法用于创建群聊
|
||||
// 参数:
|
||||
// - ctx: context.Context,上下文,用于控制请求的超时和取消
|
||||
// - req: GroupCreateReq,创建群聊的请求参数结构体
|
||||
// - corpid: string,企业的CorpID
|
||||
// - corpsecret: string,应用的Secret
|
||||
//
|
||||
// 返回值:
|
||||
// - GroupCreateResp: 创建群聊的响应结果
|
||||
// - error: 错误信息,如果请求失败则返回错误
|
||||
func (g *Group) Create(ctx context.Context, req GroupCreateReq, corpid string, corpsecret string) (GroupCreateResp, error) {
|
||||
|
||||
var res GroupCreateResp
|
||||
|
||||
param, _ := util.StructToMap(req)
|
||||
|
||||
_, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/create", &res, corpid, corpsecret)
|
||||
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// SendMarkDown 方法用于发送Markdown格式的消息到群聊
|
||||
// 参数:
|
||||
// - ctx: 上下文信息,用于控制请求的超时和取消
|
||||
// - req: 群聊发送Markdown消息的请求参数结构体
|
||||
// - corpid: 企业微信corp ID
|
||||
// - corpsecret: 企业微信应用的secret
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 操作过程中发生的错误,如果成功则为nil
|
||||
func (g *Group) SendMarkDown(ctx context.Context, req GroupSendMarkDownReq, corpid string, corpsecret string) error {
|
||||
|
||||
req.Msgtype = "markdown_v2"
|
||||
|
||||
param, _ := util.StructToMap(req)
|
||||
|
||||
_, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) SendNews(ctx context.Context, req GroupSendNewsReq, corpid string, corpsecret string) error {
|
||||
|
||||
req.Msgtype = "news"
|
||||
|
||||
param, _ := util.StructToMap(req)
|
||||
|
||||
_, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) SendImg(ctx context.Context, req GroupSendImgReq, corpid string, corpsecret string) error {
|
||||
|
||||
req.Msgtype = "image"
|
||||
|
||||
param, _ := util.StructToMap(req)
|
||||
|
||||
_, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) SendMpNews(ctx context.Context, req GroupSendMpNewsReq, corpid string, corpsecret string) error {
|
||||
|
||||
req.Msgtype = "mpnews"
|
||||
|
||||
param, _ := util.StructToMap(req)
|
||||
|
||||
_, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) SendText(ctx context.Context, req GroupSendTextReq, corpid string, corpsecret string) error {
|
||||
|
||||
req.Msgtype = "text"
|
||||
|
||||
param, _ := util.StructToMap(req)
|
||||
|
||||
_, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Group) request(ctx context.Context, param map[string]interface{}, url string, resData interface{}, corpid string, corpsecret string) ([]byte, error) {
|
||||
auth, err := g.auth.GetAccessToken(ctx, corpid, corpsecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req := l_request.Request{
|
||||
Method: http.MethodPost,
|
||||
Url: url + "?access_token=" + auth.AccessToken,
|
||||
Json: param,
|
||||
}
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("request failed, status code: %d,reason: %s", res.StatusCode, res.Reason)
|
||||
}
|
||||
var code commonResp
|
||||
if err = json.Unmarshal(res.Content, &code); err != nil {
|
||||
return nil, fmt.Errorf("返回结构异常:%s", string(res.Content))
|
||||
}
|
||||
if code.Errcode != 0 {
|
||||
return nil, fmt.Errorf("返回状态异常:%s", string(code.Errmsg))
|
||||
}
|
||||
if resData != nil {
|
||||
if err = json.Unmarshal(res.Content, resData); err != nil {
|
||||
return nil, fmt.Errorf("返回数据异常:%s", string(res.Content))
|
||||
}
|
||||
}
|
||||
|
||||
return res.Content, nil
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# weworkapi_cplusplus
|
||||
official lib of wework api https://work.weixin.qq.com/api/doc
|
||||
|
||||
# 注意事项
|
||||
|
||||
* 1.回调sdk json版本
|
||||
|
||||
* 2.wxbizjsonmsgcrypt.go文件中声明并实现了WXBizJsonMsgCrypt类,提供用户接入企业微信的三个接口。sample.go文件提供了如何使用这三个接口的示例。
|
||||
|
||||
* 3.WXBizJsonMsgCrypt类封装了VerifyURL, DecryptMsg, EncryptMsg三个接口,分别用于开发者验证回调url,收到用户回复消息的解密以及开发者回复消息的加密过程。使用方法可以参考sample.go文件。
|
||||
|
||||
* 4.加解密协议请参考企业微信官方文档。
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
package json_callback
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const token = "gY1AGR3mjBhzy"
|
||||
const receiverId = "wwabfd0cec7171e769"
|
||||
const encodingAeskey = "g8VGfQEqluUhoKOlyjmmll8Q9C5tVFUTX5T2qkmI9Sv"
|
||||
|
||||
func getString(str, endstr string, start int, msg *string) int {
|
||||
end := strings.Index(str, endstr)
|
||||
*msg = str[start:end]
|
||||
return end + len(endstr)
|
||||
}
|
||||
|
||||
func VerifyURL(w http.ResponseWriter, r *http.Request) {
|
||||
//httpstr := `&{GET /?msg_signature=825075c093249d5a60967fe4a613cae93146636b×tamp=1597998748&nonce=1597483820&echostr=neLB8CftccHiz19tluVb%2BUBnUVMT3xpUMZU8qvDdD17eH8XfEsbPYC%2FkJyPsZOOc6GdsCeu8jSIa2noSJ%2Fez2w%3D%3D HTTP/1.1 1 1 map[Cache-Control:[no-cache] Accept:[*/*] Pragma:[no-cache] User-Agent:[Mozilla/4.0]] 0x86c180 0 [] false 100.108.211.112:8893 map[] map[] <nil> map[] 100.108.79.233:59663 /?msg_signature=825075c093249d5a60967fe4a613cae93146636b×tamp=1597998748&nonce=1597483820&echostr=neLB8CftccHiz19tluVb%2BUBnUVMT3xpUMZU8qvDdD17eH8XfEsbPYC%2FkJyPsZOOc6GdsCeu8jSIa2noSJ%2Fez2w%3D%3D <nil>}`
|
||||
fmt.Println(r, r.Body)
|
||||
httpstr := r.URL.RawQuery
|
||||
start := strings.Index(httpstr, "msg_signature=")
|
||||
start += len("msg_signature=")
|
||||
|
||||
var msg_signature string
|
||||
next := getString(httpstr, "×tamp=", start, &msg_signature)
|
||||
|
||||
var timestamp string
|
||||
next = getString(httpstr, "&nonce=", next, ×tamp)
|
||||
|
||||
var nonce string
|
||||
next = getString(httpstr, "&echostr=", next, &nonce)
|
||||
|
||||
echostr := httpstr[next:len(httpstr)]
|
||||
|
||||
echostr, _ = url.QueryUnescape(echostr)
|
||||
fmt.Println(msg_signature, timestamp, nonce, echostr, next)
|
||||
|
||||
wxcpt := wxbizjsonmsgcrypt.NewWXBizMsgCrypt(token, encodingAeskey, receiverId, wxbizjsonmsgcrypt.JsonType)
|
||||
echoStr, cryptErr := wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
|
||||
if nil != cryptErr {
|
||||
fmt.Println("verifyUrl fail", cryptErr)
|
||||
}
|
||||
fmt.Println("verifyUrl success echoStr", string(echoStr))
|
||||
fmt.Fprintf(w, string(echoStr))
|
||||
}
|
||||
|
||||
type MsgContent struct {
|
||||
ToUsername string `json:"ToUserName"`
|
||||
FromUsername string `json:"FromUserName"`
|
||||
CreateTime uint32 `json:"CreateTime"`
|
||||
MsgType string `json:"MsgType"`
|
||||
Content string `json:"Content"`
|
||||
Msgid uint64 `json:"MsgId"`
|
||||
Agentid uint32 `json:"AgentId"`
|
||||
}
|
||||
|
||||
func MsgHandler(w http.ResponseWriter, r *http.Request) {
|
||||
httpstr := r.URL.RawQuery
|
||||
start := strings.Index(httpstr, "msg_signature=")
|
||||
start += len("msg_signature=")
|
||||
|
||||
var msg_signature string
|
||||
next := getString(httpstr, "×tamp=", start, &msg_signature)
|
||||
|
||||
var timestamp string
|
||||
next = getString(httpstr, "&nonce=", next, ×tamp)
|
||||
|
||||
nonce := httpstr[next:len(httpstr)]
|
||||
fmt.Println(msg_signature, timestamp, nonce)
|
||||
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
fmt.Println(string(body), err)
|
||||
wxcpt := wxbizjsonmsgcrypt.NewWXBizMsgCrypt(token, encodingAeskey, receiverId, wxbizjsonmsgcrypt.JsonType)
|
||||
|
||||
msg, err_ := wxcpt.DecryptMsg(msg_signature, timestamp, nonce, body)
|
||||
fmt.Println(string(msg), err_)
|
||||
var msgContent MsgContent
|
||||
err = json.Unmarshal(msg, &msgContent)
|
||||
if nil != err {
|
||||
fmt.Println("Unmarshal fail", err)
|
||||
} else {
|
||||
fmt.Println("struct", msgContent)
|
||||
}
|
||||
|
||||
fmt.Println(msgContent, err)
|
||||
ToUsername := msgContent.ToUsername
|
||||
msgContent.ToUsername = msgContent.FromUsername
|
||||
msgContent.FromUsername = ToUsername
|
||||
fmt.Println("replaymsg", msgContent)
|
||||
replayJson, err := json.Marshal(&msgContent)
|
||||
|
||||
encryptMsg, cryptErr := wxcpt.EncryptMsg(string(replayJson), "1409659589", "1409659589")
|
||||
if nil != cryptErr {
|
||||
fmt.Println("DecryptMsg fail", cryptErr)
|
||||
}
|
||||
|
||||
sEncryptMsg := string(encryptMsg)
|
||||
|
||||
fmt.Println("after encrypt sEncryptMsg: ", sEncryptMsg)
|
||||
fmt.Fprintf(w, sEncryptMsg)
|
||||
}
|
||||
|
||||
func CallbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
httpstr := r.URL.RawQuery
|
||||
echo := strings.Index(httpstr, "echostr")
|
||||
if echo != -1 {
|
||||
VerifyURL(w, r)
|
||||
} else {
|
||||
MsgHandler(w, r)
|
||||
}
|
||||
|
||||
fmt.Println("finished CallbackHandler", httpstr)
|
||||
}
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", CallbackHandler) // 设置访问路由
|
||||
log.Fatal(http.ListenAndServe(":8893", nil))
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
package json_callback
|
||||
|
||||
//
|
||||
//import (
|
||||
// "ai_scheduler/internal/biz/handle/qywx/json_callback/wxbizjsonmsgcrypt"
|
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
//)
|
||||
//
|
||||
//type MsgContent struct {
|
||||
// ToUsername string `json:"ToUserName"`
|
||||
// FromUsername string `json:"FromUserName"`
|
||||
// CreateTime uint32 `json:"CreateTime"`
|
||||
// MsgType string `json:"MsgType"`
|
||||
// Content string `json:"Content"`
|
||||
// Msgid uint64 `json:"MsgId"`
|
||||
// Agentid uint32 `json:"AgentId"`
|
||||
//}
|
||||
//
|
||||
//func main() {
|
||||
// token := "QDG6eK"
|
||||
// receiverId := "wx5823bf96d3bd56c7"
|
||||
// encodingAeskey := "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"
|
||||
// wxcpt := wxbizjsonmsgcrypt.NewWXBizMsgCrypt(token, encodingAeskey, receiverId, wxbizjsonmsgcrypt.JsonType)
|
||||
// /*
|
||||
// ------------使用示例一:验证回调URL---------------
|
||||
// *企业开启回调模式时,企业微信会向验证url发送一个get请求
|
||||
// 假设点击验证时,企业收到类似请求:
|
||||
// * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
|
||||
// * HTTP/1.1 Host: qy.weixin.qq.com
|
||||
//
|
||||
// 接收到该请求时,企业应
|
||||
// 1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr),
|
||||
// 这一步注意作URL解码。
|
||||
// 2.验证消息体签名的正确性
|
||||
// 3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信
|
||||
// 第2,3步可以用企业微信提供的库函数VerifyURL来实现。
|
||||
//
|
||||
// */
|
||||
// // 解析出url上的参数值如下:
|
||||
// // verifyMsgSign := HttpUtils.ParseUrl("msg_signature")
|
||||
// verifyMsgSign := "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3"
|
||||
// // verifyTimestamp := HttpUtils.ParseUrl("timestamp")
|
||||
// verifyTimestamp := "1409659589"
|
||||
// // verifyNonce := HttpUtils.ParseUrl("nonce")
|
||||
// verifyNonce := "263014780"
|
||||
// // verifyEchoStr := HttpUtils.ParseUrl("echoStr")
|
||||
// verifyEchoStr := "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ=="
|
||||
// echoStr, cryptErr := wxcpt.VerifyURL(verifyMsgSign, verifyTimestamp, verifyNonce, verifyEchoStr)
|
||||
// if nil != cryptErr {
|
||||
// fmt.Println("verifyUrl fail", cryptErr)
|
||||
// }
|
||||
// fmt.Println("verifyUrl success echoStr", string(echoStr))
|
||||
// // 验证URL成功,将sEchoStr返回
|
||||
// // HttpUtils.SetResponse(sEchoStr)
|
||||
//
|
||||
// /*
|
||||
// ------------使用示例二:对用户回复的消息解密---------------
|
||||
// 用户回复消息或者点击事件响应时,企业会收到回调消息,此消息是经过企业微信加密之后的密文以post形式发送给企业,密文格式请参考官方文档
|
||||
// 假设企业收到企业微信的回调消息如下:
|
||||
// POST /cgi-bin/wxpush? msg_signature=477715d11cdb4164915debcba66cb864d751f3e6×tamp=1409659813&nonce=1372623149 HTTP/1.1
|
||||
// Host: qy.weixin.qq.com
|
||||
// Content-Length: 613
|
||||
// {
|
||||
// "tousername":"wx5823bf96d3bd56c7",
|
||||
// "encrypt":"CZWs4CWRpI4VolQlvn4dlPBlXke6+HgmuI7p0LueFp1fKH40TNL+YHWJZwqIiYV+3kTrhdNU7fZwc+PmtgBvxSczkFeRz+oaVSsomrrtP2Z91LE313djjbWujqInRT+7ChGbCeo7ZzszByf8xnDSunPBxRX1MfX3kAxpKq7dqduW1kpMAx8O8xUzZ9oC0TLuZchbpxaml4epzGfF21O+zyXDwTxbCEiO0E87mChtzuh/VPlznXYbfqVrnyLNZ5pr",
|
||||
// "agentid":"218"
|
||||
// }
|
||||
//
|
||||
// 企业收到post请求之后应该:
|
||||
// 1.解析出url上的参数,包括消息体签名(msg_signature),时间戳(timestamp)以及随机数字串(nonce)
|
||||
// 2.验证消息体签名的正确性。
|
||||
// 3.将post请求的数据进行json解析,并将"Encrypt"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
|
||||
// 第2,3步可以用企业微信提供的库函数DecryptMsg来实现。
|
||||
// */
|
||||
//
|
||||
// // reqMsgSign := HttpUtils.ParseUrl("msg_signature")
|
||||
// reqMsgSign := "0623cbc5a8cbee5bcc137c70de99575366fc2af3"
|
||||
// // reqTimestamp := HttpUtils.ParseUrl("timestamp")
|
||||
// reqTimestamp := "1409659813"
|
||||
// // reqNonce := HttpUtils.ParseUrl("nonce")
|
||||
// reqNonce := "1372623149"
|
||||
// // post请求的密文数据
|
||||
// // reqData = HttpUtils.PostData()
|
||||
//
|
||||
// reqData := []byte(`{"tousername":"wx5823bf96d3bd56c7","encrypt":"CZWs4CWRpI4VolQlvn4dlEC1alN2MUEY2VklGehgBVLBrlVF7SyT+SV+Toj43l4ayJ9UMGKphktKKmP7B2j/P1ey67XB8PBgS7Wr5/8+w/yWriZv3Vmoo/MH3/1HsIWZrPQ3N2mJrelStIfI2Y8kLKXA7EhfZgZX4o+ffdkZDM76SEl79Ib9mw7TGjZ9Aw/x/A2VjNbV1E8BtEbRxYYcQippYNw7hr8sFfa3nW1xLdxokt8QkRX83vK3DFP2F6TQFPL2Tu98UwhcUpPvdJBuu1/yiOQIScppV3eOuLWEsko=","agentid":"218"}`)
|
||||
//
|
||||
// msg, cryptErr := wxcpt.DecryptMsg(reqMsgSign, reqTimestamp, reqNonce, reqData)
|
||||
// if nil != cryptErr {
|
||||
// fmt.Println("DecryptMsg fail", cryptErr)
|
||||
// }
|
||||
// fmt.Println("after decrypt msg: ", string(msg))
|
||||
// // TODO: 解析出明文json标签的内容进行处理
|
||||
// // For example:
|
||||
//
|
||||
// var msgContent MsgContent
|
||||
// err := json.Unmarshal(msg, &msgContent)
|
||||
// if nil != err {
|
||||
// fmt.Println("Unmarshal fail", err)
|
||||
// } else {
|
||||
// fmt.Println("struct", msgContent)
|
||||
// }
|
||||
//
|
||||
// /*
|
||||
// ------------使用示例三:企业回复用户消息的加密---------------
|
||||
// 企业被动回复用户的消息也需要进行加密,并且拼接成密文格式的json串。
|
||||
// 假设企业需要回复用户的明文如下:
|
||||
//
|
||||
// {
|
||||
// "ToUserName": "mycreate",
|
||||
// "FromUserName":"wx5823bf96d3bd56c7",
|
||||
// "CreateTime": 1348831860,
|
||||
// "MsgType": "text",
|
||||
// "Content": "this is a test",
|
||||
// "MsgId": 1234567890123456,
|
||||
// "AgentID": 128
|
||||
// }
|
||||
//
|
||||
// 为了将此段明文回复给用户,企业应:
|
||||
// 1.自己生成时间时间戳(timestamp),随机数字串(nonce)以便生成消息体签名,也可以直接用从企业微信的post url上解析出的对应值。
|
||||
// 2.将明文加密得到密文。
|
||||
// 3.用密文,步骤1生成的timestamp,nonce和企业在企业微信设定的token生成消息体签名。
|
||||
// 4.将密文,消息体签名,时间戳,随机数字串拼接成json格式的字符串,发送给企业。
|
||||
// 以上2,3,4步可以用企业微信提供的库函数EncryptMsg来实现。
|
||||
// */
|
||||
// respData := "{\"ToUserName\":\"wx5823bf96d3bd56c7\",\"FromUserName\":\"mycreate\",\"CreateTime\": 1409659813,\"MsgType\":\"text\",\"Content\":\"hello\",\"MsgId\":4561255354251345929,\"AgentID\": 218}"
|
||||
// //respData := `{"ToUserName":"wx5823bf96d3bd56c7","FromUserName":"mycreate","CreateTime": 1409659813,"MsgType":"text","Content":"hello","MsgId":4561255354251345929,"AgentID": 218}`
|
||||
// //respData := `{"FromUserName":"mycreate","CreateTime": 1409659813,"MsgType":"text","Content":"hello","MsgId":4561255354251345929,"AgentID": 218}`
|
||||
// encryptMsg, cryptErr := wxcpt.EncryptMsg(respData, reqTimestamp, reqNonce)
|
||||
// if nil != cryptErr {
|
||||
// fmt.Println("DecryptMsg fail", cryptErr)
|
||||
// }
|
||||
//
|
||||
// sEncryptMsg := string(encryptMsg)
|
||||
//
|
||||
// fmt.Println("after encrypt sEncryptMsg: ", sEncryptMsg)
|
||||
// // 加密成功
|
||||
// // TODO:
|
||||
// // HttpUtils.SetResponse(sEncryptMsg)
|
||||
//}
|
||||
|
|
@ -1,310 +0,0 @@
|
|||
package wxbizjsonmsgcrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha1"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
const (
|
||||
ValidateSignatureError int = -40001
|
||||
ParseJsonError int = -40002
|
||||
ComputeSignatureError int = -40003
|
||||
IllegalAesKey int = -40004
|
||||
ValidateCorpidError int = -40005
|
||||
EncryptAESError int = -40006
|
||||
DecryptAESError int = -40007
|
||||
IllegalBuffer int = -40008
|
||||
EncodeBase64Error int = -40009
|
||||
DecodeBase64Error int = -40010
|
||||
GenJsonError int = -40011
|
||||
IllegalProtocolType int = -40012
|
||||
)
|
||||
|
||||
type ProtocolType int
|
||||
|
||||
const (
|
||||
JsonType ProtocolType = 1
|
||||
)
|
||||
|
||||
type CryptError struct {
|
||||
ErrCode int
|
||||
ErrMsg string
|
||||
}
|
||||
|
||||
func NewCryptError(err_code int, err_msg string) *CryptError {
|
||||
return &CryptError{ErrCode: err_code, ErrMsg: err_msg}
|
||||
}
|
||||
|
||||
type WXBizJsonMsg4Recv struct {
|
||||
Tousername string `json:"tousername"`
|
||||
Encrypt string `json:"encrypt"`
|
||||
Agentid string `json:"agentid"`
|
||||
}
|
||||
|
||||
type WXBizJsonMsg4Send struct {
|
||||
Encrypt string `json:"encrypt"`
|
||||
Signature string `json:"msgsignature"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Nonce string `json:"nonce"`
|
||||
}
|
||||
|
||||
func NewWXBizJsonMsg4Send(encrypt, signature, timestamp, nonce string) *WXBizJsonMsg4Send {
|
||||
return &WXBizJsonMsg4Send{Encrypt: encrypt, Signature: signature, Timestamp: timestamp, Nonce: nonce}
|
||||
}
|
||||
|
||||
type ProtocolProcessor interface {
|
||||
parse(src_data []byte) (*WXBizJsonMsg4Recv, *CryptError)
|
||||
serialize(msg_send *WXBizJsonMsg4Send) ([]byte, *CryptError)
|
||||
}
|
||||
|
||||
type WXBizMsgCrypt struct {
|
||||
token string
|
||||
encoding_aeskey string
|
||||
receiver_id string
|
||||
protocol_processor ProtocolProcessor
|
||||
}
|
||||
|
||||
type JsonProcessor struct {
|
||||
}
|
||||
|
||||
func (self *JsonProcessor) parse(src_data []byte) (*WXBizJsonMsg4Recv, *CryptError) {
|
||||
var msg4_recv WXBizJsonMsg4Recv
|
||||
err := json.Unmarshal(src_data, &msg4_recv)
|
||||
if nil != err {
|
||||
fmt.Println("Unmarshal fail", err)
|
||||
return nil, NewCryptError(ParseJsonError, "json to msg fail")
|
||||
}
|
||||
return &msg4_recv, nil
|
||||
}
|
||||
|
||||
func (self *JsonProcessor) serialize(msg4_send *WXBizJsonMsg4Send) ([]byte, *CryptError) {
|
||||
json_msg, err := json.Marshal(msg4_send)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(GenJsonError, err.Error())
|
||||
}
|
||||
|
||||
return json_msg, nil
|
||||
}
|
||||
|
||||
func NewWXBizMsgCrypt(token, encoding_aeskey, receiver_id string, protocol_type ProtocolType) *WXBizMsgCrypt {
|
||||
var protocol_processor ProtocolProcessor
|
||||
if protocol_type != JsonType {
|
||||
panic("unsupport protocal")
|
||||
} else {
|
||||
protocol_processor = new(JsonProcessor)
|
||||
}
|
||||
|
||||
return &WXBizMsgCrypt{token: token, encoding_aeskey: (encoding_aeskey + "="), receiver_id: receiver_id, protocol_processor: protocol_processor}
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) randString(n int) string {
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) pKCS7Padding(plaintext string, block_size int) []byte {
|
||||
padding := block_size - (len(plaintext) % block_size)
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(plaintext)
|
||||
buffer.Write(padtext)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) pKCS7Unpadding(plaintext []byte, block_size int) ([]byte, *CryptError) {
|
||||
plaintext_len := len(plaintext)
|
||||
if nil == plaintext || plaintext_len == 0 {
|
||||
return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding error nil or zero")
|
||||
}
|
||||
if plaintext_len%block_size != 0 {
|
||||
return nil, NewCryptError(DecryptAESError, "pKCS7Unpadding text not a multiple of the block size")
|
||||
}
|
||||
padding_len := int(plaintext[plaintext_len-1])
|
||||
return plaintext[:plaintext_len-padding_len], nil
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) cbcEncrypter(plaintext string) ([]byte, *CryptError) {
|
||||
aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(DecodeBase64Error, err.Error())
|
||||
}
|
||||
const block_size = 32
|
||||
pad_msg := self.pKCS7Padding(plaintext, block_size)
|
||||
|
||||
block, err := aes.NewCipher(aeskey)
|
||||
if err != nil {
|
||||
return nil, NewCryptError(EncryptAESError, err.Error())
|
||||
}
|
||||
|
||||
ciphertext := make([]byte, len(pad_msg))
|
||||
iv := aeskey[:aes.BlockSize]
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
|
||||
mode.CryptBlocks(ciphertext, pad_msg)
|
||||
base64_msg := make([]byte, base64.StdEncoding.EncodedLen(len(ciphertext)))
|
||||
base64.StdEncoding.Encode(base64_msg, ciphertext)
|
||||
|
||||
return base64_msg, nil
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) cbcDecrypter(base64_encrypt_msg string) ([]byte, *CryptError) {
|
||||
aeskey, err := base64.StdEncoding.DecodeString(self.encoding_aeskey)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(DecodeBase64Error, err.Error())
|
||||
}
|
||||
|
||||
encrypt_msg, err := base64.StdEncoding.DecodeString(base64_encrypt_msg)
|
||||
if nil != err {
|
||||
return nil, NewCryptError(DecodeBase64Error, err.Error())
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(aeskey)
|
||||
if err != nil {
|
||||
return nil, NewCryptError(DecryptAESError, err.Error())
|
||||
}
|
||||
|
||||
if len(encrypt_msg) < aes.BlockSize {
|
||||
return nil, NewCryptError(DecryptAESError, "encrypt_msg size is not valid")
|
||||
}
|
||||
|
||||
iv := aeskey[:aes.BlockSize]
|
||||
|
||||
if len(encrypt_msg)%aes.BlockSize != 0 {
|
||||
return nil, NewCryptError(DecryptAESError, "encrypt_msg not a multiple of the block size")
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
|
||||
mode.CryptBlocks(encrypt_msg, encrypt_msg)
|
||||
|
||||
return encrypt_msg, nil
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) calSignature(timestamp, nonce, data string) string {
|
||||
sort_arr := []string{self.token, timestamp, nonce, data}
|
||||
sort.Strings(sort_arr)
|
||||
var buffer bytes.Buffer
|
||||
for _, value := range sort_arr {
|
||||
buffer.WriteString(value)
|
||||
}
|
||||
|
||||
sha := sha1.New()
|
||||
sha.Write(buffer.Bytes())
|
||||
signature := fmt.Sprintf("%x", sha.Sum(nil))
|
||||
return string(signature)
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) ParsePlainText(plaintext []byte) ([]byte, uint32, []byte, []byte, *CryptError) {
|
||||
const block_size = 32
|
||||
plaintext, err := self.pKCS7Unpadding(plaintext, block_size)
|
||||
if nil != err {
|
||||
return nil, 0, nil, nil, err
|
||||
}
|
||||
|
||||
text_len := uint32(len(plaintext))
|
||||
if text_len < 20 {
|
||||
return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 1")
|
||||
}
|
||||
random := plaintext[:16]
|
||||
msg_len := binary.BigEndian.Uint32(plaintext[16:20])
|
||||
if text_len < (20 + msg_len) {
|
||||
return nil, 0, nil, nil, NewCryptError(IllegalBuffer, "plain is to small 2")
|
||||
}
|
||||
|
||||
msg := plaintext[20 : 20+msg_len]
|
||||
receiver_id := plaintext[20+msg_len:]
|
||||
|
||||
return random, msg_len, msg, receiver_id, nil
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) VerifyURL(msg_signature, timestamp, nonce, echostr string) ([]byte, *CryptError) {
|
||||
signature := self.calSignature(timestamp, nonce, echostr)
|
||||
|
||||
if strings.Compare(signature, msg_signature) != 0 {
|
||||
return nil, NewCryptError(ValidateSignatureError, "signature not equal")
|
||||
}
|
||||
|
||||
plaintext, err := self.cbcDecrypter(echostr)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, _, msg, receiver_id, err := self.ParsePlainText(plaintext)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
|
||||
fmt.Println(string(receiver_id), self.receiver_id, len(receiver_id), len(self.receiver_id))
|
||||
return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) EncryptMsg(reply_msg, timestamp, nonce string) ([]byte, *CryptError) {
|
||||
rand_str := self.randString(16)
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(rand_str)
|
||||
|
||||
msg_len_buf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(msg_len_buf, uint32(len(reply_msg)))
|
||||
buffer.Write(msg_len_buf)
|
||||
buffer.WriteString(reply_msg)
|
||||
buffer.WriteString(self.receiver_id)
|
||||
|
||||
tmp_ciphertext, err := self.cbcEncrypter(buffer.String())
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := string(tmp_ciphertext)
|
||||
|
||||
signature := self.calSignature(timestamp, nonce, ciphertext)
|
||||
|
||||
msg4_send := NewWXBizJsonMsg4Send(ciphertext, signature, timestamp, nonce)
|
||||
return self.protocol_processor.serialize(msg4_send)
|
||||
}
|
||||
|
||||
func (self *WXBizMsgCrypt) DecryptMsg(msg_signature, timestamp, nonce string, post_data []byte) ([]byte, *CryptError) {
|
||||
msg4_recv, crypt_err := self.protocol_processor.parse(post_data)
|
||||
if nil != crypt_err {
|
||||
return nil, crypt_err
|
||||
}
|
||||
|
||||
signature := self.calSignature(timestamp, nonce, msg4_recv.Encrypt)
|
||||
|
||||
if strings.Compare(signature, msg_signature) != 0 {
|
||||
return nil, NewCryptError(ValidateSignatureError, "signature not equal")
|
||||
}
|
||||
|
||||
plaintext, crypt_err := self.cbcDecrypter(msg4_recv.Encrypt)
|
||||
if nil != crypt_err {
|
||||
return nil, crypt_err
|
||||
}
|
||||
|
||||
_, _, msg, receiver_id, crypt_err := self.ParsePlainText(plaintext)
|
||||
if nil != crypt_err {
|
||||
return nil, crypt_err
|
||||
}
|
||||
|
||||
if len(self.receiver_id) > 0 && strings.Compare(string(receiver_id), self.receiver_id) != 0 {
|
||||
return nil, NewCryptError(ValidateCorpidError, "receiver_id is not equil")
|
||||
}
|
||||
|
||||
return msg, nil
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package qywx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Other struct {
|
||||
auth *Auth
|
||||
}
|
||||
|
||||
func NewOther(auth *Auth) *Other {
|
||||
return &Other{
|
||||
|
||||
auth: auth,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Other) UploadMediaWithUrl(ctx context.Context, fileUrl, mediaType, corpid, corpsecret string) (uploadRes *UploadMediaRes, err error) {
|
||||
// 1. 获取AccessToken
|
||||
auth, err := g.auth.GetAccessToken(ctx, corpid, corpsecret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取AccessToken失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 下载文件
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Get(fileUrl)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("下载文件失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("下载文件状态码错误: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 3. 准备上传请求(修正点:使用正确的上传API)
|
||||
uploadUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s",
|
||||
auth.AccessToken, mediaType)
|
||||
|
||||
// 4. 创建multipart表单
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// 从URL提取文件名(或使用默认名)
|
||||
filename := path.Base(fileUrl)
|
||||
if filename == "/" || filename == "." {
|
||||
filename = fmt.Sprintf("file_%d.dat", time.Now().Unix())
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile("media", filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建表单文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 5. 流式传输文件内容
|
||||
if _, err = io.Copy(part, resp.Body); err != nil {
|
||||
return nil, fmt.Errorf("写入文件内容失败: %v", err)
|
||||
}
|
||||
|
||||
// 6. 关闭writer
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, fmt.Errorf("关闭writer失败: %v", err)
|
||||
}
|
||||
|
||||
// 7. 发送上传请求
|
||||
req, err := http.NewRequest("POST", uploadUrl, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建请求失败: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
uploadResp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("上传请求失败: %v", err)
|
||||
}
|
||||
defer uploadResp.Body.Close()
|
||||
|
||||
// 8. 解析响应
|
||||
respBody, err := io.ReadAll(uploadResp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(respBody, &uploadRes); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
if uploadRes.Errcode != 0 {
|
||||
return nil, fmt.Errorf("上传失败 [code=%d]: %s", uploadRes.Errcode, uploadRes.Errmsg)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package qywx
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetQywx = wire.NewSet(
|
||||
NewAuth,
|
||||
NewGroup,
|
||||
NewOther,
|
||||
)
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package qywx
|
||||
|
||||
import "time"
|
||||
|
||||
type AuthRes struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
|
||||
type AuthInfo struct {
|
||||
Corpid string `json:"corpid"`
|
||||
Corpsecret string `json:"corpsecret"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
Expire time.Duration `json:"expireIn"`
|
||||
}
|
||||
|
||||
type GroupCreateReq struct {
|
||||
Name string `json:"name"`
|
||||
Owner string `json:"owner"`
|
||||
Userlist []string `json:"userlist"`
|
||||
Chatid string `json:"chatid"`
|
||||
}
|
||||
|
||||
type GroupCreateResp struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
Chatid string `json:"chatid"`
|
||||
}
|
||||
|
||||
type commonResp struct {
|
||||
Errcode int `json:"errcode"`
|
||||
Errmsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
type GroupSendMarkDownReq struct {
|
||||
Chatid string `json:"chatid"`
|
||||
Msgtype string `json:"msgtype"`
|
||||
Markdown MarkDown `json:"markdown_v2"`
|
||||
}
|
||||
|
||||
type MarkDown struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GroupSendNewsReq struct {
|
||||
Chatid string `json:"chatid"`
|
||||
Msgtype string `json:"msgtype"`
|
||||
News News `json:"news"`
|
||||
Safe int `json:"safe"`
|
||||
}
|
||||
|
||||
type News struct {
|
||||
Articles []Articles `json:"articles"`
|
||||
}
|
||||
type Articles struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Url string `json:"url"`
|
||||
Picurl string `json:"picurl"`
|
||||
}
|
||||
|
||||
type GroupSendImgReq struct {
|
||||
Chatid string `json:"chatid"`
|
||||
Msgtype string `json:"msgtype"`
|
||||
Image Image `json:"image"`
|
||||
Safe int `json:"safe"`
|
||||
}
|
||||
type Image struct {
|
||||
MediaId string `json:"media_id"`
|
||||
}
|
||||
|
||||
type GroupSendMpNewsReq struct {
|
||||
Chatid string `json:"chatid"`
|
||||
Msgtype string `json:"msgtype"`
|
||||
Mpnews Mpnews `json:"mpnews"`
|
||||
Safe int `json:"safe"`
|
||||
}
|
||||
|
||||
type Mpnews struct {
|
||||
Articles []ArticlesMpnews `json:"articles"`
|
||||
}
|
||||
type ArticlesMpnews struct {
|
||||
Title string `json:"title"`
|
||||
ThumbMediaId string `json:"thumb_media_id"`
|
||||
Author string `json:"author"`
|
||||
ContentSourceUrl string `json:"content_source_url"`
|
||||
Content string `json:"content"`
|
||||
Digest string `json:"digest"`
|
||||
}
|
||||
|
||||
type GroupSendTextReq struct {
|
||||
Chatid string `json:"chatid"`
|
||||
Msgtype string `json:"msgtype"`
|
||||
Text Text `json:"text"`
|
||||
Safe int `json:"safe"`
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
package llm_service
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
type VllmService struct {
|
||||
client *utils_vllm.Client
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewVllmService(
|
||||
client *utils_vllm.Client,
|
||||
config *config.Config,
|
||||
) *VllmService {
|
||||
return &VllmService{
|
||||
client: client,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *VllmService) IntentRecognize(ctx context.Context, req *entitys.ToolSelect) (msg string, err error) {
|
||||
msgs := s.convertMessages(req.Prompt)
|
||||
tools := s.convertTools(req.Tools)
|
||||
|
||||
resp, err := s.client.ToolSelect(ctx, msgs, tools)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Content == "" {
|
||||
if len(resp.ToolCalls) > 0 {
|
||||
call := resp.ToolCalls[0]
|
||||
var matchFromTools = &entitys.Match{
|
||||
Confidence: 1,
|
||||
Index: call.Function.Name,
|
||||
Parameters: call.Function.Arguments,
|
||||
IsMatch: true,
|
||||
}
|
||||
msg = pkg.JsonStringIgonErr(matchFromTools)
|
||||
} else {
|
||||
err = errors.New("不太明白你想表达的意思呢,可以在仔细描述一下您所需要的内容吗,感谢感谢")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
msg = resp.Content
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *VllmService) convertMessages(prompts []api.Message) []*schema.Message {
|
||||
msgs := make([]*schema.Message, 0, len(prompts))
|
||||
for _, p := range prompts {
|
||||
msg := &schema.Message{
|
||||
Role: schema.RoleType(p.Role),
|
||||
Content: p.Content,
|
||||
}
|
||||
|
||||
// 这里实际应该不会走进来
|
||||
if len(p.Images) > 0 {
|
||||
parts := []schema.MessageInputPart{
|
||||
{Type: schema.ChatMessagePartTypeText, Text: p.Content},
|
||||
}
|
||||
for _, imgData := range p.Images {
|
||||
b64 := base64.StdEncoding.EncodeToString(imgData)
|
||||
mimeType := "image/jpeg"
|
||||
parts = append(parts, schema.MessageInputPart{
|
||||
Type: schema.ChatMessagePartTypeImageURL,
|
||||
Image: &schema.MessageInputImage{
|
||||
MessagePartCommon: schema.MessagePartCommon{
|
||||
MIMEType: mimeType,
|
||||
Base64Data: &b64,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
msg.UserInputMultiContent = parts
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
func (s *VllmService) convertTools(tasks []entitys.RegistrationTask) []*schema.ToolInfo {
|
||||
tools := make([]*schema.ToolInfo, 0, len(tasks))
|
||||
for _, task := range tasks {
|
||||
params := make(map[string]*schema.ParameterInfo)
|
||||
for k, v := range task.TaskConfigDetail.Param.Properties {
|
||||
dt := schema.String
|
||||
|
||||
// Handle v.Type dynamically to support both string and []string (compiler suggests []string)
|
||||
// Using fmt.Sprint handles both cases safely without knowing exact type structure
|
||||
typeStr := fmt.Sprintf("%v", v.Type)
|
||||
typeStr = strings.Trim(typeStr, "[]") // normalize "[string]" -> "string"
|
||||
|
||||
switch typeStr {
|
||||
case "string":
|
||||
dt = schema.String
|
||||
case "integer", "int":
|
||||
dt = schema.Integer
|
||||
case "number", "float":
|
||||
dt = schema.Number
|
||||
case "boolean", "bool":
|
||||
dt = schema.Boolean
|
||||
case "object":
|
||||
dt = schema.Object
|
||||
case "array":
|
||||
dt = schema.Array
|
||||
}
|
||||
|
||||
required := false
|
||||
for _, r := range task.TaskConfigDetail.Param.Required {
|
||||
if r == k {
|
||||
required = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
desc := v.Description
|
||||
if len(v.Enum) > 0 {
|
||||
var enumStrs []string
|
||||
for _, e := range v.Enum {
|
||||
enumStrs = append(enumStrs, fmt.Sprintf("%v", e))
|
||||
}
|
||||
desc += " Enum: " + strings.Join(enumStrs, ", ")
|
||||
}
|
||||
|
||||
params[k] = &schema.ParameterInfo{
|
||||
Type: dt,
|
||||
Desc: desc,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
tools = append(tools, &schema.ToolInfo{
|
||||
Name: task.Name,
|
||||
Desc: task.Desc,
|
||||
ParamsOneOf: schema.NewParamsOneOfByParams(params),
|
||||
})
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/biz/llm_service"
|
||||
"ai_scheduler/internal/biz/support"
|
||||
|
||||
"github.com/google/wire"
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/biz/llm_service"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetBiz = wire.NewSet(
|
||||
|
|
@ -14,14 +13,9 @@ var ProviderSetBiz = wire.NewSet(
|
|||
NewChatHistoryBiz,
|
||||
//llm_service.NewLangChainGenerate,
|
||||
llm_service.NewOllamaGenerate,
|
||||
llm_service.NewVllmService,
|
||||
//handle.NewHandle,
|
||||
do.NewDo,
|
||||
do.NewHandle,
|
||||
do.NewHandle,
|
||||
NewTaskBiz,
|
||||
NewDingTalkBotBiz,
|
||||
NewQywxAppBiz,
|
||||
NewGroupConfigBiz,
|
||||
do.NewMacro,
|
||||
support.NewHytAddressIngester,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,171 +0,0 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/handle/qywx"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/tools/bbxt"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ai_scheduler/internal/config"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// AiRouterBiz 智能路由服务
|
||||
type QywxAppBiz struct {
|
||||
conf *config.Config
|
||||
botGroupQywxImpl *impl.BotGroupQywxImpl
|
||||
qywxGroupHandle *qywx.Group
|
||||
qywxOtherHandle *qywx.Other
|
||||
}
|
||||
|
||||
// NewDingTalkBotBiz
|
||||
func NewQywxAppBiz(
|
||||
conf *config.Config,
|
||||
botGroupQywxImpl *impl.BotGroupQywxImpl,
|
||||
qywxGroupHandle *qywx.Group,
|
||||
qywxOtherHandle *qywx.Other,
|
||||
) *QywxAppBiz {
|
||||
return &QywxAppBiz{
|
||||
conf: conf,
|
||||
botGroupQywxImpl: botGroupQywxImpl,
|
||||
qywxGroupHandle: qywxGroupHandle,
|
||||
qywxOtherHandle: qywxOtherHandle,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *QywxAppBiz) InitGroup(ctx context.Context) (string, error) {
|
||||
chatId := pkg.RandomString(q.conf.Qywx.ChatIdLen)
|
||||
GroupInfo := &model.AiBotGroupQywx{
|
||||
Title: "bot_group_" + time.Now().Format(time.DateOnly),
|
||||
ChatID: chatId,
|
||||
ConfigID: q.conf.Qywx.DefaultConfigId,
|
||||
AppSecret: q.conf.Qywx.AppSecret,
|
||||
}
|
||||
_, err := q.botGroupQywxImpl.Add(GroupInfo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := q.qywxGroupHandle.Create(
|
||||
ctx,
|
||||
qywx.GroupCreateReq{
|
||||
Name: GroupInfo.Title,
|
||||
Chatid: GroupInfo.ChatID,
|
||||
Userlist: strings.Split(q.conf.Qywx.InitAccount, ","),
|
||||
},
|
||||
q.conf.Qywx.CorpId,
|
||||
GroupInfo.AppSecret,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.Chatid, nil
|
||||
}
|
||||
|
||||
func (q *QywxAppBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroupQywx, err error) {
|
||||
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"group_id": groupId})
|
||||
cond = cond.And(builder.Eq{"status": constants.Enable})
|
||||
err = q.botGroupQywxImpl.GetOneBySearchToStrut(&cond, &group)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (q *QywxAppBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroupQywx, report *bbxt.ReportRes) (err error) {
|
||||
//confitent := fmt.Sprintf("%s\n%s", report.Title, fmt.Sprintf("", report.Url))
|
||||
|
||||
upload, err := q.qywxOtherHandle.UploadMediaWithUrl(ctx, report.Url, "image", q.conf.Qywx.CorpId, groupInfo.AppSecret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = q.qywxGroupHandle.SendText(ctx, qywx.GroupSendTextReq{
|
||||
Chatid: groupInfo.ChatID,
|
||||
Text: qywx.Text{
|
||||
Content: report.ReportName + "\n" + report.Title,
|
||||
},
|
||||
}, q.conf.Qywx.CorpId, groupInfo.AppSecret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = q.qywxGroupHandle.SendImg(ctx, qywx.GroupSendImgReq{
|
||||
Chatid: groupInfo.ChatID,
|
||||
Image: qywx.Image{
|
||||
MediaId: upload.MediaId,
|
||||
},
|
||||
}, q.conf.Qywx.CorpId, groupInfo.AppSecret)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SendReportV2 发送到货易通指定的群聊
|
||||
func (q *QywxAppBiz) SendReportHYT(ctx context.Context, groupInfo *model.AiBotGroupQywx, report *bbxt.ReportRes) (err error) {
|
||||
|
||||
if report == nil {
|
||||
return fmt.Errorf("report is nil")
|
||||
}
|
||||
// 文本消息
|
||||
err = q.sendReportHYT(groupInfo, &bbxt.ReportRes{
|
||||
Title: report.Title,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second) // 等待1秒,避免被频控
|
||||
|
||||
// 图片消息
|
||||
err = q.sendReportHYT(groupInfo, &bbxt.ReportRes{
|
||||
ReportName: report.ReportName,
|
||||
Url: report.Url,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second) // 等待1秒,避免被频控
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QywxAppBiz) sendReportHYT(groupInfo *model.AiBotGroupQywx, report *bbxt.ReportRes) (err error) {
|
||||
req := l_request.Request{
|
||||
Method: "POST",
|
||||
Url: "https://hyt.86698.cn/admin_upload/api/v1/sendWxCommon",
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Json: map[string]interface{}{
|
||||
"room_id": groupInfo.ChatID, // 群ID
|
||||
"oss_url": report.Url, // 图片URL
|
||||
"file_type": 2, // 文件类型,2:图片
|
||||
"file_name": report.ReportName, // 文件名称
|
||||
"content": report.Title, // 输入文本,此处有值就走文本发送,没有就走文件发送
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := req.Send()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.SysErrf("发送到货易通群聊失败,状态码:%d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 记录相应结果日志
|
||||
log.Printf("发送到货易通群聊成功,状态码:%d, 响应体:%s", resp.StatusCode, resp.Text)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/handle/qywx"
|
||||
"ai_scheduler/internal/biz/tools_regis"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/domain/component"
|
||||
"ai_scheduler/internal/domain/component/callback"
|
||||
"ai_scheduler/internal/domain/repo"
|
||||
"ai_scheduler/internal/domain/workflow"
|
||||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
"ai_scheduler/internal/pkg/utils_ollama"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/utils"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_InitGroup(t *testing.T) {
|
||||
run()
|
||||
chatId, err := qywxAppBiz.InitGroup(context.Background())
|
||||
t.Log(chatId, err)
|
||||
}
|
||||
|
||||
var (
|
||||
configConfig *config.Config
|
||||
qywxAppBiz *QywxAppBiz
|
||||
groupConfigBiz *GroupConfigBiz
|
||||
)
|
||||
|
||||
func run() {
|
||||
configConfig, _ = config.LoadConfigWithTest()
|
||||
// 初始化数据库连接
|
||||
db, _ := utils.NewGormDb(configConfig)
|
||||
rdb := utils.NewRdb(configConfig)
|
||||
botGroupQywxImpl := impl.NewBotGroupQywxImpl(db)
|
||||
qywxAuth := qywx.NewAuth(configConfig, rdb)
|
||||
group := qywx.NewGroup(botGroupQywxImpl, qywxAuth)
|
||||
sessionImpl := impl.NewSessionImpl(db)
|
||||
other := qywx.NewOther(qywxAuth)
|
||||
repos := repo.NewRepos(sessionImpl, configConfig, rdb)
|
||||
pkgRdb := pkg.NewRdb(configConfig)
|
||||
redisManager := callback.NewRedisManager(pkgRdb)
|
||||
login := lsxd.NewLogin(configConfig, rdb)
|
||||
components := component.NewComponents(redisManager, login)
|
||||
repos = repo.NewRepos(sessionImpl, configConfig, rdb)
|
||||
botToolsImpl := impl.NewBotToolsImpl(db)
|
||||
toolRegis := tools_regis.NewToolsRegis(botToolsImpl)
|
||||
utils_ossClient, _ := utils_oss.NewClient(configConfig)
|
||||
client, _, _ := utils_ollama.NewClient(configConfig)
|
||||
|
||||
registry := workflow.NewRegistry(configConfig, client, repos, components)
|
||||
botGroupConfigImpl := impl.NewBotGroupConfigImpl(db)
|
||||
qywxAppBiz = NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other)
|
||||
groupConfigBiz = NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig)
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package biz
|
|||
|
||||
import (
|
||||
"ai_scheduler/internal/biz/do"
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
errors "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/gateway"
|
||||
|
|
@ -22,19 +21,16 @@ import (
|
|||
type AiRouterBiz struct {
|
||||
do *do.Do
|
||||
handle *do.Handle
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
// NewAiRouterBiz 创建路由服务
|
||||
func NewAiRouterBiz(
|
||||
do *do.Do,
|
||||
handle *do.Handle,
|
||||
config *config.Config,
|
||||
) *AiRouterBiz {
|
||||
return &AiRouterBiz{
|
||||
do: do,
|
||||
handle: handle,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +94,7 @@ func (r *AiRouterBiz) SetRec(ctx context.Context, requireData *entitys.RequireDa
|
|||
// 对应不同的appKey, 配置不同的系统提示词
|
||||
switch requireData.Sys.AppKey {
|
||||
default:
|
||||
sys = &do.WithSys{Config: r.config}
|
||||
sys = &do.WithSys{}
|
||||
}
|
||||
|
||||
// 1. 系统提示词
|
||||
|
|
@ -174,17 +170,16 @@ func (r *AiRouterBiz) buildChatHistory(requireData *entitys.RequireData) entitys
|
|||
// 用户消息
|
||||
messages = append(messages, entitys.HisMessage{
|
||||
Role: constants.RoleUser, // 用户角色
|
||||
Content: h.Ques, // 用户输入内容
|
||||
Content: h.Ans, // 用户输入内容
|
||||
Timestamp: h.CreateAt.Format(time.DateTime),
|
||||
})
|
||||
|
||||
// 助手消息 - 助手回复噪音太大且无需,pass
|
||||
// ansStr := r.ansNoiseReduction(h.Ans) // 助手回复降噪
|
||||
// messages = append(messages, entitys.HisMessage{
|
||||
// Role: constants.RoleAssistant, // 助手角色
|
||||
// Content: ansStr, // 助手回复内容
|
||||
// Timestamp: h.CreateAt.Format(time.DateTime),
|
||||
// })
|
||||
// 助手消息
|
||||
messages = append(messages, entitys.HisMessage{
|
||||
Role: constants.RoleAssistant, // 助手角色
|
||||
Content: h.Ques, // 助手回复内容
|
||||
Timestamp: h.CreateAt.Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
|
||||
// 构建聊天历史上下文
|
||||
|
|
@ -197,20 +192,3 @@ func (r *AiRouterBiz) buildChatHistory(requireData *entitys.RequireData) entitys
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ansNoiseReduction 助手回复降噪
|
||||
// func (r *AiRouterBiz) ansNoiseReduction(ansJson string) string {
|
||||
// // 使用anw统一类型解析
|
||||
// ansStruct := make([]*entitys.Response, 0)
|
||||
// err := json.Unmarshal([]byte(ansJson), &ansStruct)
|
||||
// if err != nil {
|
||||
// log.Errorf("解析助手回复失败: %s", err.Error())
|
||||
// return ansJson
|
||||
// }
|
||||
// var ansStr string
|
||||
// for _, item := range ansStruct {
|
||||
// ansStr += item.Content
|
||||
// }
|
||||
|
||||
// return ansStr
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
package biz
|
||||
|
||||
// import (
|
||||
// "ai_scheduler/internal/config"
|
||||
// "ai_scheduler/internal/data/impl"
|
||||
// "ai_scheduler/internal/data/model"
|
||||
// "ai_scheduler/internal/entitys"
|
||||
// "ai_scheduler/internal/pkg"
|
||||
// "ai_scheduler/internal/pkg/utils_ollama"
|
||||
// "ai_scheduler/internal/tools"
|
||||
// "ai_scheduler/utils"
|
||||
// "encoding/json"
|
||||
// "flag"
|
||||
// "fmt"
|
||||
// "os"
|
||||
// "path/filepath"
|
||||
// "testing"
|
||||
|
||||
// "github.com/gofiber/fiber/v2/log"
|
||||
// )
|
||||
|
||||
// func Test_task(t *testing.T) {
|
||||
// var c entitys.TaskConfig
|
||||
// config := `{"param": {"type": "object", "required": ["number"], "properties": {"number": {"type": "string", "description": "订单编号/流水号"}}}, "request": {"url": "http://www.baidu.com/${number}", "headers": {"Authorization": "${authorization}"}, "method": "GET"}}`
|
||||
// err := json.Unmarshal([]byte(config), &c)
|
||||
// t.Log(err)
|
||||
// }
|
||||
|
||||
// type configData struct {
|
||||
// Param map[string]interface{} `json:"param"`
|
||||
// Do map[string]interface{} `json:"do"`
|
||||
// }
|
||||
|
||||
// func Test_Order(t *testing.T) {
|
||||
// routerBiz := in()
|
||||
// ch := make(chan entitys.Response, 5)
|
||||
// defer close(ch)
|
||||
// err := routerBiz.handleTask(ch, nil, &entitys.Match{Index: "order_diagnosis", Parameters: `{"order_number":"822895927188791297"}`}, &model.AiTask{Config: `{"tool": "zltxOrderDetail", "param": {"type": "object", "optional": [], "required": ["order_number"], "properties": {"order_number": {"type": "string", "description": "订单编号/流水号"}}}}`})
|
||||
// select {
|
||||
// case v := <-ch: // 尝试接收
|
||||
// fmt.Println("接收到值:", v)
|
||||
// default:
|
||||
// fmt.Println("无数据可接收")
|
||||
// }
|
||||
// t.Log(err)
|
||||
// }
|
||||
|
||||
// func Test_OrderLog(t *testing.T) {
|
||||
// routerBiz := in()
|
||||
// ch := make(chan entitys.Response, 5)
|
||||
// defer close(ch)
|
||||
// err := routerBiz.handleTask(ch, nil, &entitys.Match{Index: "order_diagnosis", Parameters: `{"order_number":"822979421673758721","serial_number":"822979421979938817"}`}, &model.AiTask{Config: `{"tool": "zltxOrderDirectLog", "param": {"type": "object", "optional": [], "required": ["order_number"], "properties": {"order_number": {"type": "string", "description": "订单编号/流水号"}}}}`})
|
||||
// t.Log(err)
|
||||
// }
|
||||
|
||||
// func Test_ProductLog(t *testing.T) {
|
||||
// routerBiz := in()
|
||||
// ch := make(chan entitys.Response, 5)
|
||||
// defer close(ch)
|
||||
// err := routerBiz.handleTask(ch, nil, &entitys.Match{Index: "order_diagnosis", Parameters: `{"name":"利楚测试"}`}, &model.AiTask{Config: `{"tool": "zltxProduct", "param": {"type": "object", "optional": [], "required": ["order_number"], "properties": {"order_number": {"type": "string", "description": "订单编号/流水号"}}}}`})
|
||||
// t.Log(err)
|
||||
// }
|
||||
|
||||
// func Test_ZltxStatistics(t *testing.T) {
|
||||
// routerBiz := in()
|
||||
// ch := make(chan entitys.Response, 5)
|
||||
// defer close(ch)
|
||||
// err := routerBiz.handleTask(ch, nil, &entitys.Match{Index: "order_diagnosis", Parameters: `{"number":"13737882067"}`}, &model.AiTask{Config: `{"tool": "zltxOrderStatistics", "param": {"type": "object", "optional": [], "required": ["number"], "properties": {"number": {"type": "string", "description": "充值账号/分销商ID"}}}}`})
|
||||
// t.Log(err)
|
||||
// }
|
||||
|
||||
// func in() *AiRouterBiz {
|
||||
|
||||
// modDir, err := getModuleDir()
|
||||
// if err != nil {
|
||||
// panic("1")
|
||||
// }
|
||||
// configPath := flag.String("config", fmt.Sprintf("%s/config/config.yaml", modDir), "Path to configuration file")
|
||||
// flag.Parse()
|
||||
|
||||
// configConfig, err := config.LoadConfig(*configPath)
|
||||
// if err != nil {
|
||||
// panic("加载配置失败")
|
||||
// }
|
||||
// client, _, err := utils_ollama.NewClient(configConfig)
|
||||
// allLogger := log.DefaultLogger()
|
||||
// utilOllama := utils_ollama.NewUtilOllama(configConfig, allLogger)
|
||||
// manager := tools.NewManager(configConfig, client)
|
||||
|
||||
// db, _ := utils.NewGormDb(configConfig)
|
||||
// sessionImpl := impl.NewSessionImpl(db)
|
||||
// sysImpl := impl.NewSysImpl(db)
|
||||
// taskImpl := impl.NewTaskImpl(db)
|
||||
// chatImpl := impl.NewChatImpl(db)
|
||||
// safeChannelPool, _ := pkg.NewSafeChannelPool(configConfig)
|
||||
// routerBiz := NewAiRouterBiz(manager, sessionImpl, sysImpl, taskImpl, chatImpl, configConfig, utilOllama, safeChannelPool, client)
|
||||
|
||||
// return routerBiz
|
||||
// }
|
||||
|
||||
// func getModuleDir() (string, error) {
|
||||
// dir, err := os.Getwd()
|
||||
// if err != nil {
|
||||
// return "", err
|
||||
// }
|
||||
|
||||
// for {
|
||||
// modPath := filepath.Join(dir, "go.mod")
|
||||
// if _, err := os.Stat(modPath); err == nil {
|
||||
// return dir, nil // 找到 go.mod
|
||||
// }
|
||||
|
||||
// // 向上查找父目录
|
||||
// parent := filepath.Dir(dir)
|
||||
// if parent == dir {
|
||||
// break // 到达根目录,未找到
|
||||
// }
|
||||
// dir = parent
|
||||
// }
|
||||
|
||||
// return "", fmt.Errorf("go.mod not found in current directory or parents")
|
||||
// }
|
||||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/internal/entitys"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
|
|
@ -36,31 +35,41 @@ func NewSessionBiz(conf *config.Config, sessionImpl *impl.SessionImpl, sysImpl *
|
|||
func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRequest) (result *entitys.SessionInitResponse, err error) {
|
||||
|
||||
// 获取系统配置
|
||||
sysConfig, err := s.GetSysConfig(req.SysId)
|
||||
sysConfig, has, err := s.sysRepo.FindOne(s.sysRepo.WithSysId(req.SysId))
|
||||
if err != nil {
|
||||
return
|
||||
} else if !has {
|
||||
err = errorcode.SysNotFound
|
||||
return
|
||||
}
|
||||
|
||||
result = &entitys.SessionInitResponse{
|
||||
Chat: make([]entitys.ChatHistory, 0),
|
||||
}
|
||||
|
||||
// 获取 当天的最新session
|
||||
// 获取 当天的session
|
||||
t := time.Now().Truncate(24 * time.Hour)
|
||||
session, has, err := s.sessionRepo.Take(
|
||||
session, has, err := s.sessionRepo.FindOne(
|
||||
s.sessionRepo.WithUserId(req.UserId), // 条件:用户ID
|
||||
s.sessionRepo.WithStartTime(t), // 条件:会话开始时间
|
||||
s.sysRepo.WithSysId(sysConfig.SysID),
|
||||
s.sessionRepo.OrderByDesc("create_at"), // 条件:系统ID
|
||||
s.sysRepo.WithSysId(sysConfig.SysID), // 条件:系统ID
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !has {
|
||||
} else if !has {
|
||||
// 不存在,创建一个
|
||||
session, err = s.CreateNew(sysConfig.SysID, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
session = model.AiSession{
|
||||
SysID: sysConfig.SysID,
|
||||
SessionID: utils.UUID(),
|
||||
UserID: req.UserId,
|
||||
UserName: req.UserName,
|
||||
DingUserId: req.DingUserId,
|
||||
}
|
||||
err = s.sessionRepo.Create(&session)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
chat := entitys.ChatHistory{
|
||||
SessionID: session.SessionID,
|
||||
Role: constants.RoleSystem,
|
||||
|
|
@ -113,32 +122,6 @@ func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRe
|
|||
return
|
||||
}
|
||||
|
||||
func (s *SessionBiz) GetSysConfig(sysId string) (model.AiSy, error) {
|
||||
sysConfig, has, err := s.sysRepo.FindOne(s.sysRepo.WithSysId(sysId))
|
||||
if err != nil {
|
||||
return sysConfig, err
|
||||
} else if !has {
|
||||
err = errorcode.SysNotFound
|
||||
return sysConfig, err
|
||||
}
|
||||
return sysConfig, nil
|
||||
}
|
||||
|
||||
func (s *SessionBiz) CreateNew(sysID int32, req *entitys.SessionInitRequest) (model.AiSession, error) {
|
||||
session := model.AiSession{
|
||||
SysID: sysID,
|
||||
SessionID: utils.UUID(),
|
||||
UserID: req.UserId,
|
||||
UserName: req.UserName,
|
||||
DingUserId: req.DingUserId,
|
||||
}
|
||||
err := s.sessionRepo.Create(&session)
|
||||
if err != nil {
|
||||
return session, err
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// SessionList 会话列表
|
||||
func (s *SessionBiz) SessionList(ctx context.Context, req *entitys.SessionListRequest) (list []model.AiSession, err error) {
|
||||
|
||||
|
|
@ -152,38 +135,9 @@ func (s *SessionBiz) SessionList(ctx context.Context, req *entitys.SessionListRe
|
|||
list, err = s.sessionRepo.FindAll(
|
||||
s.sessionRepo.WithUserId(req.UserId), // 条件:用户ID
|
||||
s.sessionRepo.WithSysId(req.SysId), // 条件:系统ID
|
||||
s.sessionRepo.WithDeleteAt(), // 条件:未删除
|
||||
s.sessionRepo.PaginateScope(req.Page, req.PageSize), // 分页
|
||||
s.sessionRepo.OrderByDesc("create_at"), // 排序:按创建时间降序
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// DeleteSession 删除会话
|
||||
func (s *SessionBiz) DeleteSession(ctx context.Context, req *entitys.SessionDeleteRequest) error {
|
||||
err := s.sessionRepo.Update(
|
||||
&model.AiSession{
|
||||
DeleteAt: util.AnyToPoint(time.Now()), // 设置删除时间
|
||||
},
|
||||
s.sessionRepo.WithSessionId(req.SessionId), // 条件:会话ID
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenameSession 会话重命名
|
||||
func (s *SessionBiz) RenameSession(ctx context.Context, req *entitys.SessionRenameRequest) error {
|
||||
err := s.sessionRepo.Update(
|
||||
&model.AiSession{
|
||||
Title: req.NewName, // 设置会话名称
|
||||
},
|
||||
s.sessionRepo.WithSessionId(req.SessionId), // 条件:会话ID
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
package support
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/constants"
|
||||
errorcode "ai_scheduler/internal/data/error"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"ai_scheduler/internal/pkg/utils_vllm"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudwego/eino/schema"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// HytAddressIngester 货易通地址提取实现
|
||||
type HytAddressIngester struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
func NewHytAddressIngester(cfg *config.Config) *HytAddressIngester {
|
||||
return &HytAddressIngester{cfg: cfg}
|
||||
}
|
||||
|
||||
// Auth 鉴权逻辑
|
||||
func (s *HytAddressIngester) Auth(c *fiber.Ctx) error {
|
||||
// 读取头
|
||||
token := strings.TrimSpace(c.Get("X-Source-Key"))
|
||||
ts := strings.TrimSpace(c.Get("X-Timestamp"))
|
||||
|
||||
// 时间窗口校验
|
||||
if ts != "" && !util.IsInTimeWindow(ts, 5*time.Minute) {
|
||||
return errorcode.AuthNotFound
|
||||
}
|
||||
// token校验
|
||||
if token == "" || token != constants.TokenAddressIngestHyt {
|
||||
return errorcode.KeyNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ingest 执行提取逻辑
|
||||
func (s *HytAddressIngester) Ingest(ctx context.Context, text string) (*AddressIngestResp, error) {
|
||||
// 模型调用
|
||||
client, cleanup, err := utils_vllm.NewClient(s.cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
res, err := client.Chat(ctx, []*schema.Message{
|
||||
{
|
||||
Role: "system",
|
||||
Content: constants.SystemPromptAddressIngestHyt,
|
||||
},
|
||||
{
|
||||
Role: "user",
|
||||
Content: text,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析模型返回结果
|
||||
var addr AddressIngestResp
|
||||
// 尝试直接解析
|
||||
if err := json.Unmarshal([]byte(res.Content), &addr); err != nil {
|
||||
// 修复json字符串
|
||||
repairedContent, err := util.JSONRepair(res.Content)
|
||||
if err != nil {
|
||||
return nil, errorcode.ParamErrf("invalid response body: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(repairedContent), &addr); err != nil {
|
||||
return nil, errorcode.ParamErrf("invalid response body: %v", err)
|
||||
}
|
||||
}
|
||||
return &addr, nil
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package support
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// AddressIngestResp 通用地址提取响应
|
||||
type AddressIngestResp struct {
|
||||
Recipient string `json:"recipient"` // 收货人
|
||||
Phone string `json:"phone"` // 联系电话
|
||||
Address string `json:"address"` // 收货地址
|
||||
}
|
||||
|
||||
// AddressIngester 地址提取接口
|
||||
type AddressIngester interface {
|
||||
// Auth 鉴权逻辑
|
||||
Auth(c *fiber.Ctx) error
|
||||
// Ingest 执行提取逻辑
|
||||
Ingest(ctx context.Context, text string) (*AddressIngestResp, error)
|
||||
}
|
||||
|
|
@ -70,13 +70,13 @@ func (t *TaskBiz) GetUserPermission(req *entitys.TaskRequest, auth string) (code
|
|||
// 发送请求
|
||||
res, err := request.Send()
|
||||
if err != nil {
|
||||
err = errors.SysErrf("请求用户权限失败")
|
||||
err = errors.SysErr("请求用户权限失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查响应状态码
|
||||
if res.StatusCode != http.StatusOK {
|
||||
err = errors.SysErrf("获取用户权限失败")
|
||||
err = errors.SysErr("获取用户权限失败")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"ai_scheduler/pkg"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
|
@ -13,24 +12,15 @@ type Config struct {
|
|||
Server ServerConfig `mapstructure:"server"`
|
||||
Ollama OllamaConfig `mapstructure:"ollama"`
|
||||
Vllm VllmConfig `mapstructure:"vllm"`
|
||||
Coze CozeConfig `mapstructure:"coze"`
|
||||
LSXD LSXDConfig `mapstructure:"lsxd"`
|
||||
Sys SysConfig `mapstructure:"sys"`
|
||||
Tools ToolsConfig `mapstructure:"tools"`
|
||||
EinoTools EinoToolsConfig `mapstructure:"eino_tools"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Redis Redis `mapstructure:"redis"`
|
||||
DB DB `mapstructure:"db"`
|
||||
Oss Oss `mapstructure:"oss"`
|
||||
DefaultPrompt SysPrompt `mapstructure:"default_prompt"`
|
||||
PermissionConfig PermissionConfig `mapstructure:"permissionConfig"`
|
||||
LLM LLM `mapstructure:"llm"`
|
||||
Dingtalk DingtalkConfig `mapstructure:"dingtalk"`
|
||||
Qywx QywxConfig `mapstructure:"qywx"`
|
||||
}
|
||||
|
||||
type ZLTX struct {
|
||||
ReqUrl string `mapstructure:"req_url"`
|
||||
// DingTalkBots map[string]*DingTalkBot `mapstructure:"ding_talk_bots"`
|
||||
}
|
||||
|
||||
type SysPrompt struct {
|
||||
|
|
@ -69,33 +59,6 @@ type LLMCapabilityConfig struct {
|
|||
Parameters LLMParameters `mapstructure:"parameters"`
|
||||
}
|
||||
|
||||
// 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"` // 机器人群组
|
||||
}
|
||||
|
||||
// QywxConfig 企业微信配置
|
||||
type QywxConfig struct {
|
||||
CorpId string `mapstructure:"corp_id"`
|
||||
DefaultConfigId int32 `mapstructure:"default_config_id"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
InitAccount string `mapstructure:"init_account"`
|
||||
Token string `mapstructure:"token"`
|
||||
AES_KEY string `mapstructure:"aes_key"`
|
||||
ChatIdLen int `mapstructure:"chat_id_len"`
|
||||
BotGroupID map[string]int `mapstructure:"bot_group_id"` // 应用群
|
||||
}
|
||||
|
||||
// TableDemandConfig 需求表配置
|
||||
type AITableConfig struct {
|
||||
Url string `mapstructure:"url"`
|
||||
BaseId string `mapstructure:"base_id"`
|
||||
SheetIdOrName string `mapstructure:"sheet_id_or_name"`
|
||||
}
|
||||
|
||||
// SysConfig 系统配置
|
||||
type SysConfig struct {
|
||||
SessionLen int `mapstructure:"session_len"`
|
||||
|
|
@ -116,39 +79,17 @@ type OllamaConfig struct {
|
|||
BaseURL string `mapstructure:"base_url"`
|
||||
Model string `mapstructure:"model"`
|
||||
GenerateModel string `mapstructure:"generate_model"`
|
||||
MappingModel string `mapstructure:"mapping_model"`
|
||||
VlModel string `mapstructure:"vl_model"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
}
|
||||
|
||||
type VllmConfig struct {
|
||||
VLModel VllmModel `mapstructure:"vl_model"`
|
||||
TextModel VllmModel `mapstructure:"text_model"`
|
||||
}
|
||||
|
||||
type VllmModel struct {
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
Model string `mapstructure:"model"`
|
||||
VlModel string `mapstructure:"vl_model"`
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
Level string `mapstructure:"level"`
|
||||
}
|
||||
|
||||
// CozeConfig Coze配置
|
||||
type CozeConfig struct {
|
||||
BaseURL string `mapstructure:"base_url"`
|
||||
ApiKey string `mapstructure:"api_key"`
|
||||
ApiSecret string `mapstructure:"api_secret"`
|
||||
}
|
||||
|
||||
// LSXDConfig 统一登录配置
|
||||
type LSXDConfig struct {
|
||||
LoginURL string `mapstructure:"login_url"`
|
||||
Phone string `mapstructure:"phone"`
|
||||
Password string `mapstructure:"password"`
|
||||
Code string `mapstructure:"code"`
|
||||
CheckTokenURL string `mapstructure:"check_token_url"`
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Type string `mapstructure:"type"`
|
||||
|
|
@ -170,15 +111,6 @@ type DB struct {
|
|||
IsDebug bool `mapstructure:"isDebug"`
|
||||
}
|
||||
|
||||
// Oss 阿里云OSS配置
|
||||
type Oss struct {
|
||||
AccessKey string `mapstructure:"access_key"`
|
||||
SecretKey string `mapstructure:"secret_key"`
|
||||
Bucket string `mapstructure:"bucket"`
|
||||
Domain string `mapstructure:"domain"`
|
||||
Endpoint string `mapstructure:"endpoint"`
|
||||
}
|
||||
|
||||
// ToolsConfig 工具配置
|
||||
type ToolsConfig struct {
|
||||
Weather ToolConfig `mapstructure:"weather"`
|
||||
|
|
@ -200,8 +132,7 @@ type ToolsConfig struct {
|
|||
// Coze 快递查询工具
|
||||
CozeExpress ToolConfig `mapstructure:"cozeExpress"`
|
||||
// Coze 公司查询工具
|
||||
CozeCompany ToolConfig `mapstructure:"cozeCompany"`
|
||||
ZltxResellerAuthProductToManagerAndDefaultLossReason ToolConfig `mapstructure:"zltxResellerAuthProductToManagerAndDefaultLossReason"`
|
||||
CozeCompany ToolConfig `mapstructure:"cozeCompany"`
|
||||
}
|
||||
|
||||
// ToolConfig 单个工具配置
|
||||
|
|
@ -214,38 +145,6 @@ type ToolConfig struct {
|
|||
AddURL string `mapstructure:"add_url"`
|
||||
}
|
||||
|
||||
// EinoToolsConfig eino tool 配置
|
||||
type EinoToolsConfig struct {
|
||||
// 货易通商品上传
|
||||
HytProductUpload ToolConfig `mapstructure:"hytProductUpload"`
|
||||
// 货易通供应商查询
|
||||
HytSupplierSearch ToolConfig `mapstructure:"hytSupplierSearch"`
|
||||
// 货易通仓库查询
|
||||
HytWarehouseSearch ToolConfig `mapstructure:"hytWarehouseSearch"`
|
||||
// 货易通商品添加
|
||||
HytGoodsAdd ToolConfig `mapstructure:"hytGoodsAdd"`
|
||||
// 货易通商品图片添加
|
||||
HytGoodsMediaAdd ToolConfig `mapstructure:"hytGoodsMediaAdd"`
|
||||
// 货易通商品分类添加
|
||||
HytGoodsCategoryAdd ToolConfig `mapstructure:"hytGoodsCategoryAdd"`
|
||||
// 货易通商品分类查询
|
||||
HytGoodsCategorySearch ToolConfig `mapstructure:"hytGoodsCategorySearch"`
|
||||
// 货易通商品品牌查询
|
||||
HytGoodsBrandSearch ToolConfig `mapstructure:"hytGoodsBrandSearch"`
|
||||
// 负利润分析列表、 详情
|
||||
DaOursProductLoss ToolConfig `mapstructure:"daOursProductLoss"`
|
||||
// 利润同比排行榜
|
||||
DaProfitRanking ToolConfig `mapstructure:"daProfitRanking"`
|
||||
// 销售同比分析列表
|
||||
DaOfficialProduct ToolConfig `mapstructure:"daOfficialProduct"`
|
||||
// 销售同比下滑详情
|
||||
DaOfficialProductDecline ToolConfig `mapstructure:"daOfficialProductDecline"`
|
||||
// 我们的商品统计
|
||||
RechargeStatisticsOursProduct ToolConfig `mapstructure:"rechargeStatisticsOursProduct"`
|
||||
// Excel 转图片
|
||||
Excel2Pic ToolConfig `mapstructure:"excel2Pic"`
|
||||
}
|
||||
|
||||
// LoggingConfig 日志配置
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"`
|
||||
|
|
@ -278,54 +177,10 @@ func LoadConfig(configPath string) (*Config, error) {
|
|||
}
|
||||
|
||||
// 解析配置
|
||||
var bc Config
|
||||
if err := viper.Unmarshal(&bc); err != nil {
|
||||
var config Config
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &bc, nil
|
||||
}
|
||||
|
||||
func LoadConfigWithTest() (*Config, error) {
|
||||
var bc Config
|
||||
modularDir, err := pkg.GetModuleDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viper.SetConfigFile(modularDir + "/config/config_test.yaml")
|
||||
viper.SetConfigType("yaml")
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
// 解析配置
|
||||
|
||||
if err := viper.Unmarshal(&bc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &bc, nil
|
||||
|
||||
}
|
||||
|
||||
func LoadConfigWithEnv() (*Config, error) {
|
||||
var bc Config
|
||||
modularDir, err := pkg.GetModuleDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
viper.SetConfigFile(modularDir + "/config/config.yaml")
|
||||
viper.SetConfigType("yaml")
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
// 解析配置
|
||||
|
||||
if err := viper.Unmarshal(&bc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &bc, nil
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,10 +34,6 @@ const (
|
|||
|
||||
const DingTalkAuthBaseKeyPrefix = "dingTalk_auth"
|
||||
|
||||
const DingTalkAuthBaseKeyBotPrefix = "dingTalk_auth_bot"
|
||||
|
||||
const QywxAuthBaseKeyPrefix = "qywx_auth"
|
||||
|
||||
// PermissionType 工具使用权限
|
||||
type PermissionType int32
|
||||
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
package constants
|
||||
|
||||
// Token
|
||||
const (
|
||||
CapabilityProductIngestToken = "A7f9KQ3mP2X8LZC4R5e"
|
||||
)
|
||||
|
||||
// Prompt
|
||||
const (
|
||||
SystemPrompt = `
|
||||
你是一个专业的商品属性提取助手,你的唯一任务是提取属性并以指定格式输出。请严格遵守:
|
||||
<<< 格式规则 >>>
|
||||
1. 输出必须是且仅是一个紧凑的、无任何多余空白字符(包括换行、缩进)的纯JSON字符串。
|
||||
2. 整个JSON必须在一行内,例如:{"商品标题":"示例","价格":100}。
|
||||
3. 严格禁止输出任何Markdown代码块标识、额外解释、思考过程或提示词本身。
|
||||
4. 任何对上述规则的偏离都会导致系统解析失败。
|
||||
<<< 规则结束 >>>
|
||||
|
||||
接下来,请处理用户输入并直接输出符合上述规则的结果。`
|
||||
)
|
||||
|
||||
// 商品属性模板-中文
|
||||
const (
|
||||
// 货易通供应商商品属性模板-中文
|
||||
HYTSupplierProductPropertyTemplateZH = `{
|
||||
"货品编号": "string", // 商品编号
|
||||
"条码": "string", // 货品编号
|
||||
"分类名称": "string", // 商品分类
|
||||
"货品名称": "string", // 商品名称
|
||||
"商品货号": "string", // 货品编号
|
||||
"品牌": "string", // 商品品牌
|
||||
"单位": "string", // 商品单位,若无则使用'个'
|
||||
"规格参数": "string", // 商品规格参数
|
||||
"货品说明": "string", // 商品说明
|
||||
"保质期": "string", // 商品保质期,无则空
|
||||
"保质期单位": "string", // 商品保质期单位,无则空
|
||||
"链接": "string", // 空
|
||||
"货品图片": ["string"], // 商品多图,取前2个即可
|
||||
"电商销售价格": "string", // 商品电商销售价格 decimal(10,2)
|
||||
"销售价": "string", // 商品销售价格 decimal(10,2)
|
||||
"备注": "string", // 无则空
|
||||
"长": "string", // 商品长度,decimal(10,2)+单位
|
||||
"宽": "string", // 商品宽度,decimal(10,2)+单位
|
||||
"高": "string", // 商品高度,decimal(10,2)+单位
|
||||
"重量": "string", // 商品重量,decimal(10,2)+单位(kg)
|
||||
"SPU名称": "string", // 商品SPU名称
|
||||
"SPU编码": "string" // 货品编号
|
||||
"供应商报价": "string", // 空
|
||||
"税率": "string", // 商品税率 x%,无则空
|
||||
"利润": "string", // 空
|
||||
"默认供应商": "string", // 空
|
||||
"默认存放仓库": "string", // 空
|
||||
}`
|
||||
// 货易通商品属性模板-中文 Ps:手机端主图、详情图文、平台资质图 (暂时无需)
|
||||
HYTGoodsAddPropertyTemplateZH = `{
|
||||
"商品标题": "string", // 商品名称
|
||||
"商品编码": "string", // 商品编号+rand(1000-999)
|
||||
"SPU名称": "string", // 商品SPU名称
|
||||
"SPU编码": "string", // 'ai_'+商品编号
|
||||
"商品货号": "string", // 商品编号
|
||||
"商品条形码": "string", // 商品编号
|
||||
"市场价": "string", // 优惠前价格 decimal(10,2)
|
||||
"建议销售价": "string", // 市场价
|
||||
"电商销售价格": "string", // 优惠后价格 decimal(10,2)
|
||||
"单位": "string", // 价格单位,默认'元'
|
||||
"折扣": "string", // 商品折扣(%),默认'0%'
|
||||
"税率": "string", // 商品税率(%),默认'13%'
|
||||
"运费模版": "string", // 商品运费模版,默认空
|
||||
"保质期": "string", // 商品保质期,无则空
|
||||
"保质期单位": "string", // 商品保质期单位,无则空
|
||||
"品牌": "string", // 商品品牌,若无则空
|
||||
"是否热销主推": "string", // 默认'否'
|
||||
"外部平台链接": "string", // 空即可
|
||||
"商品卖点": "string", // 商品卖点
|
||||
"商品规格参数": "string", // 商品规格参数
|
||||
"商品说明": "string", // 商品说明
|
||||
"备注": "string", // 无则空
|
||||
"分类名称": "string", // 商品分类
|
||||
"电脑端主图": ["string"], // 商品电脑端主图,取第一张
|
||||
}`
|
||||
)
|
||||
|
||||
// 缓存key
|
||||
const (
|
||||
CapabilityProductIngestCacheKey = "ai_scheduler:capability:product_ingest:%s"
|
||||
)
|
||||
|
|
@ -16,8 +16,6 @@ const (
|
|||
TaskTypeFunc TaskType = 3
|
||||
TaskTypeBot TaskType = 4
|
||||
TaskTypeEinoWorkflow TaskType = 5 // eino 工作流
|
||||
TaskTypeCozeWorkflow TaskType = 6 // coze 工作流
|
||||
TaskTypeReport TaskType = 7 //报表
|
||||
)
|
||||
|
||||
type UseFul int32
|
||||
|
|
|
|||
|
|
@ -49,32 +49,3 @@ type BotMsgType string
|
|||
const (
|
||||
BotMsgTypeText BotMsgType = "text"
|
||||
)
|
||||
|
||||
type CardTemp string
|
||||
|
||||
const (
|
||||
CardTempDefault CardTemp = `{
|
||||
"config": {
|
||||
"autoLayout": true,
|
||||
"enableForward": true
|
||||
},
|
||||
"header": {
|
||||
"title": {
|
||||
"type": "text",
|
||||
"text": "${title}",
|
||||
},
|
||||
"logo": "@lALPDfJ6V_FPDmvNAfTNAfQ"
|
||||
},
|
||||
"contents": [
|
||||
{
|
||||
"type": "divider",
|
||||
"id": "divider_1765952728523"
|
||||
},
|
||||
{
|
||||
"type": "markdown",
|
||||
"text": "%s",
|
||||
"id": "markdown_1765970168635"
|
||||
}
|
||||
]
|
||||
}`
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,7 +46,3 @@ var FileTypeMappings = map[FileType][]string{
|
|||
".csv",
|
||||
},
|
||||
}
|
||||
|
||||
func (ft FileType) String() string {
|
||||
return string(ft)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
package constants
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
CACHE_KEY_LSXD_TOKEN = "ai_scheduler:lsxd_token"
|
||||
EXPIRE_LSXD_TOKEN = time.Hour * 2 // 2小时
|
||||
)
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package constants
|
||||
|
||||
// Token
|
||||
const (
|
||||
TokenAddressIngestHyt = "E632C7D3E60771B03264F2337CCFA014" // md5("address_ingest_hyt")
|
||||
)
|
||||
|
||||
// 系统提示词
|
||||
const (
|
||||
SystemPromptAddressIngestHyt = `# 你是一个地址信息结构化解析器。
|
||||
你的任务是从用户提供的非结构化文本中,准确抽取并区分以下字段:
|
||||
|
||||
1. 收货人 recipient (真实姓名或带掩码姓名,如“张三”)
|
||||
2. 联系电话 phone (中国大陆手机号,11位数字)
|
||||
3. 收货地址 address
|
||||
|
||||
解析规则:
|
||||
- 电话号码只提取最可能的一个
|
||||
- 不要编造不存在的信息
|
||||
|
||||
输出示例:
|
||||
{\"recipient\": \"张三\",\"phone\": \"13458968095\",\"address\": \"四川省成都市武侯区天府三街88号\"}
|
||||
|
||||
输出格式必须为严格 JSON,不要输出任何解释性文字。`
|
||||
)
|
||||
|
|
@ -9,15 +9,3 @@ const (
|
|||
Enable = 1
|
||||
Disable = 2
|
||||
)
|
||||
|
||||
// IrregularTaskToolIndexMap 不规则任务工具索引映射
|
||||
var IrregularTaskToolIndexMap = map[string]string{
|
||||
"zltxProduct": "product_diagnosis",
|
||||
"zltxOrderDetail": "order_diagnosis",
|
||||
"knowledgeBase": "knowledge_qa",
|
||||
"zltxOrderStatistics": "account_statistics",
|
||||
"normalChat": "chat",
|
||||
"zltxOrderAfterSaleSupplier": "after_sales_supplier",
|
||||
"zltxOrderAfterSaleReseller": "after_sales_reseller",
|
||||
"zltxOrderAfterSaleResellerBatch": "after_sales_reseller_batch",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ package errorcode
|
|||
import "fmt"
|
||||
|
||||
var (
|
||||
Success = &BusinessErr{code: 200, message: "成功"}
|
||||
ParamError = &BusinessErr{code: 401, message: "参数错误"}
|
||||
ForbiddenError = &BusinessErr{code: 403, message: "权限不足"}
|
||||
NotFoundError = &BusinessErr{code: 404, message: "请求地址未找到"}
|
||||
SystemError = &BusinessErr{code: 405, message: "系统错误"}
|
||||
Success = &BusinessErr{code: 200, message: "成功"}
|
||||
ParamError = &BusinessErr{code: 401, message: "参数错误"}
|
||||
NotFoundError = &BusinessErr{code: 404, message: "请求地址未找到"}
|
||||
SystemError = &BusinessErr{code: 405, message: "系统错误"}
|
||||
|
||||
ClientNotFound = &BusinessErr{code: 406, message: "未找到client_id"}
|
||||
SessionNotFound = &BusinessErr{code: 407, message: "未找到会话信息"}
|
||||
|
|
@ -16,7 +15,6 @@ var (
|
|||
SysNotFound = &BusinessErr{code: 410, message: "未找到系统信息"}
|
||||
SysCodeNotFound = &BusinessErr{code: 411, message: "未找到系统编码"}
|
||||
InvalidParam = &BusinessErr{code: InvalidParamCode, message: "无效参数"}
|
||||
WorkflowError = &BusinessErr{code: 501, message: "工作流过程错误"}
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -45,30 +43,14 @@ func NewBusinessErr(code int, message string) *BusinessErr {
|
|||
return &BusinessErr{code: code, message: message}
|
||||
}
|
||||
|
||||
func SysErrf(message string, arg ...any) *BusinessErr {
|
||||
func SysErr(message string, arg ...any) *BusinessErr {
|
||||
return &BusinessErr{code: SystemError.code, message: fmt.Sprintf(message, arg)}
|
||||
}
|
||||
|
||||
func SysErr(message string) *BusinessErr {
|
||||
return &BusinessErr{code: SystemError.code, message: message}
|
||||
}
|
||||
|
||||
func ParamErrf(message string, arg ...any) *BusinessErr {
|
||||
func ParamErr(message string, arg ...any) *BusinessErr {
|
||||
return &BusinessErr{code: ParamError.code, message: fmt.Sprintf(message, arg)}
|
||||
}
|
||||
|
||||
func ParamErr(message string) *BusinessErr {
|
||||
return &BusinessErr{code: ParamError.code, message: message}
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Wrap(err error) *BusinessErr {
|
||||
return NewBusinessErr(e.code, err.Error())
|
||||
}
|
||||
|
||||
func WorkflowErr(message string) *BusinessErr {
|
||||
return NewBusinessErr(WorkflowError.code, message)
|
||||
}
|
||||
|
||||
func ForbiddenErr(message string) *BusinessErr {
|
||||
return NewBusinessErr(ForbiddenError.code, message)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,21 +43,19 @@ type BaseRepository[P PO] interface {
|
|||
FindAll(conditions ...CondFunc) ([]P, error) // 查询所有
|
||||
Paginate(page, pageSize int, conditions ...CondFunc) (*PaginationResult[P], error) // 分页查询
|
||||
FindOne(conditions ...CondFunc) (P, bool, error) // 查询单条记录,若未找到则返回 has=false, err=nil
|
||||
Take(conditions ...CondFunc) (P, bool, error)
|
||||
Create(m *P) error // 创建
|
||||
BatchCreate(m *[]P) (err error) // 批量创建
|
||||
Update(m *P, conditions ...CondFunc) (err error) // 更新
|
||||
Delete(conditions ...CondFunc) (err error) // 删除
|
||||
Count(conditions ...CondFunc) (count int64, err error) // 查询条数
|
||||
PaginateScope(page, pageSize int) CondFunc // 分页
|
||||
OrderByDesc(field string) CondFunc // 倒序排序
|
||||
OrderByAsc(field string) CondFunc // 正序排序
|
||||
WithId(id interface{}) CondFunc // 查询id
|
||||
WithStatus(status int) CondFunc // 查询status
|
||||
GetDb() *gorm.DB // 获取数据库连接
|
||||
WithLimit(limit int) CondFunc // 限制返回条数
|
||||
In(field string, values interface{}) CondFunc // 查询字段是否在列表中
|
||||
Select(fields ...string) CondFunc // 选择字段
|
||||
Create(m *P) error // 创建
|
||||
BatchCreate(m *[]P) (err error) // 批量创建
|
||||
Update(m *P, conditions ...CondFunc) (err error) // 更新
|
||||
Delete(conditions ...CondFunc) (err error) // 删除
|
||||
Count(conditions ...CondFunc) (count int64, err error) // 查询条数
|
||||
PaginateScope(page, pageSize int) CondFunc // 分页
|
||||
OrderByDesc(field string) CondFunc // 倒序排序
|
||||
WithId(id interface{}) CondFunc // 查询id
|
||||
WithStatus(status int) CondFunc // 查询status
|
||||
GetDb() *gorm.DB // 获取数据库连接
|
||||
WithLimit(limit int) CondFunc // 限制返回条数
|
||||
In(field string, values interface{}) CondFunc // 查询字段是否在列表中
|
||||
Select(fields ...string) CondFunc // 选择字段
|
||||
}
|
||||
|
||||
// PaginationResult 分页查询结果
|
||||
|
|
@ -129,26 +127,6 @@ func (this *BaseModel[P]) FindOne(conditions ...CondFunc) (P, bool, error) {
|
|||
return result, true, err
|
||||
}
|
||||
|
||||
func (this *BaseModel[P]) Take(conditions ...CondFunc) (P, bool, error) {
|
||||
var (
|
||||
result P
|
||||
)
|
||||
|
||||
err := this.Db.Model(new(P)).
|
||||
Scopes(conditions...).
|
||||
Take(&result).
|
||||
Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return result, false, nil // 未找到记录
|
||||
}
|
||||
if err != nil {
|
||||
return result, false, fmt.Errorf("查询单条记录失败: %w", err)
|
||||
}
|
||||
|
||||
return result, true, err
|
||||
}
|
||||
|
||||
// 创建
|
||||
func (this *BaseModel[P]) Create(m *P) error {
|
||||
err := this.Db.Create(m).Error
|
||||
|
|
@ -215,13 +193,6 @@ func (this *BaseModel[P]) OrderByDesc(field string) CondFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// 正序排序条件生成器
|
||||
func (this *BaseModel[P]) OrderByAsc(field string) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order(fmt.Sprintf("%s ASC", field))
|
||||
}
|
||||
}
|
||||
|
||||
// ID查询条件生成器
|
||||
func (this *BaseModel[P]) WithId(id interface{}) CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
|
|||
}
|
||||
}
|
||||
|
||||
func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
|
||||
func (k BotGroupImpl) GetByConversationId(staffId 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 = ?", staffId).Find(&data).Error
|
||||
if data.GroupID == 0 {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type BotGroupConfigImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewBotGroupConfigImpl(db *utils.Db) *BotGroupConfigImpl {
|
||||
return &BotGroupConfigImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotGroupConfig)),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type BotGroupQywxImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewBotGroupQywxImpl(db *utils.Db) *BotGroupQywxImpl {
|
||||
return &BotGroupQywxImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotGroupQywx)),
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,4 @@ var ProviderImpl = wire.NewSet(
|
|||
NewBotChatHisImpl,
|
||||
NewBotToolsImpl,
|
||||
NewBotGroupImpl,
|
||||
NewBotGroupConfigImpl,
|
||||
NewBotGroupQywxImpl,
|
||||
NewReportDailyCacheImpl,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
type ReportDailyCacheImpl struct {
|
||||
dataTemp.DataTemp
|
||||
}
|
||||
|
||||
func NewReportDailyCacheImpl(db *utils.Db) *ReportDailyCacheImpl {
|
||||
return &ReportDailyCacheImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiReportDailyCache)),
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,8 @@ import (
|
|||
"ai_scheduler/internal/data/model"
|
||||
"ai_scheduler/tmpl/dataTemp"
|
||||
"ai_scheduler/utils"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SessionImpl struct {
|
||||
|
|
@ -47,10 +46,3 @@ func (impl *SessionImpl) WithSessionId(sessionId interface{}) CondFunc {
|
|||
return db.Where("session_id = ?", sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDeletedAt 条件:是否删除
|
||||
func (impl *SessionImpl) WithDeleteAt() CondFunc {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("delete_at IS NULL")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ const TableNameAiBotChatHi = "ai_bot_chat_his"
|
|||
// AiBotChatHi mapped from table <ai_bot_chat_his>
|
||||
type AiBotChatHi struct {
|
||||
HisID int64 `gorm:"column:his_id;primaryKey;autoIncrement:true" json:"his_id"`
|
||||
HisType string `gorm:"column:his_type;not null;default:1;comment:1为个人,2为群聊" json:"his_type"` // 1为个人,2为群聊
|
||||
ID int32 `gorm:"column:id;not null;comment:对应的id" json:"id"` // 对应的id
|
||||
SessionID string `gorm:"column:session_id;not null" json:"session_id"`
|
||||
Role string `gorm:"column:role;not null;comment:system系统输出,assistant助手输出,user用户输入" json:"role"` // system系统输出,assistant助手输出,user用户输入
|
||||
Content string `gorm:"column:content;not null" json:"content"`
|
||||
Files string `gorm:"column:files;not null" json:"files"`
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ const TableNameAiBotConfig = "ai_bot_config"
|
|||
// AiBotConfig mapped from table <ai_bot_config>
|
||||
type AiBotConfig struct {
|
||||
BotID int32 `gorm:"column:bot_id;primaryKey;autoIncrement:true" json:"bot_id"`
|
||||
SysID int32 `gorm:"column:sys_id;not null" json:"sys_id"`
|
||||
BotType int32 `gorm:"column:bot_type;not null;default:1;comment:类型,1为钉钉机器人" json:"bot_type"` // 类型,1为钉钉机器人
|
||||
SysPrompt string `gorm:"column:sys_prompt" json:"sys_prompt"`
|
||||
BotName string `gorm:"column:bot_name;not null;comment:名字" json:"bot_name"` // 名字
|
||||
BotConfig string `gorm:"column:bot_config;not null;comment:配置" json:"bot_config"` // 配置
|
||||
RobotCode string `gorm:"column:robot_code;not null;comment:索引" json:"robot_code"` // 索引
|
||||
BotName string `gorm:"column:bot_name;not null;comment:名字" json:"bot_name"` // 名字
|
||||
BotConfig string `gorm:"column:bot_config;not null;comment:配置" json:"bot_config"` // 配置
|
||||
BotIndex string `gorm:"column:bot_index;not null;comment:索引" json:"bot_index"` // 索引
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
Status int32 `gorm:"column:status;not null" json:"status"`
|
||||
|
|
|
|||
|
|
@ -13,10 +13,9 @@ const TableNameAiBotGroup = "ai_bot_group"
|
|||
// AiBotGroup mapped from table <ai_bot_group>
|
||||
type AiBotGroup struct {
|
||||
GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"`
|
||||
ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID
|
||||
RobotCode string `gorm:"column:robot_code;not null;comment:绑定机器人code" json:"robot_code"` // 绑定机器人code
|
||||
ConfigID int32 `gorm:"column:config_id;not null;comment:关联ai_bot_group_config" json:"config_id"` // 关联ai_bot_group_config
|
||||
Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称
|
||||
ConversationID string `gorm:"column:conversation_id;not null;comment:会话ID" json:"conversation_id"` // 会话ID
|
||||
Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称
|
||||
ToolList string `gorm:"column:tool_list;not null;comment:开通工具列表" json:"tool_list"` // 开通工具列表
|
||||
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
|
||||
DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"`
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
// 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
|
||||
|
||||
const TableNameAiBotGroupConfig = "ai_bot_group_config"
|
||||
|
||||
// AiBotGroupConfig mapped from table <ai_bot_group_config>
|
||||
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"`
|
||||
}
|
||||
|
||||
// TableName AiBotGroupConfig's table name
|
||||
func (*AiBotGroupConfig) TableName() string {
|
||||
return TableNameAiBotGroupConfig
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
// 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 TableNameAiBotGroupQywx = "ai_bot_group_qywx"
|
||||
|
||||
// AiBotGroupQywx mapped from table <ai_bot_group_qywx>
|
||||
type AiBotGroupQywx struct {
|
||||
GroupID int32 `gorm:"column:group_id;primaryKey;autoIncrement:true" json:"group_id"`
|
||||
ChatID string `gorm:"column:chat_id;not null;comment:会话ID" json:"chat_id"` // 会话ID
|
||||
ConfigID int32 `gorm:"column:config_id;not null" json:"config_id"`
|
||||
AppSecret string `gorm:"column:app_secret;not null;comment:绑定机器人code" json:"app_secret"` // 绑定机器人code
|
||||
Title string `gorm:"column:title;not null;comment:群名称" json:"title"` // 群名称
|
||||
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
|
||||
DeleteAt *time.Time `gorm:"column:delete_at" json:"delete_at"`
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
}
|
||||
|
||||
// TableName AiBotGroupQywx's table name
|
||||
func (*AiBotGroupQywx) TableName() string {
|
||||
return TableNameAiBotGroupQywx
|
||||
}
|
||||
|
|
@ -13,16 +13,16 @@ const TableNameAiBotTool = "ai_bot_tools"
|
|||
// AiBotTool mapped from table <ai_bot_tools>
|
||||
type AiBotTool struct {
|
||||
ToolID int32 `gorm:"column:tool_id;primaryKey;autoIncrement:true" json:"tool_id"`
|
||||
PermissionType int32 `gorm:"column:permission_type;not null;default:1;comment:类型,1为公共工具,不需要进行权限管理,反之则为2" json:"permission_type"` // 类型,1为公共工具,不需要进行权限管理,反之则为2
|
||||
Config string `gorm:"column:config;comment:类型下所需路由以及参数" json:"config"` // 类型下所需路由以及参数
|
||||
PermissionType int32 `gorm:"column:permission_type;not null;comment:类型,1为公共工具,不需要进行权限管理,反之则为2" json:"permission_type"` // 类型,1为公共工具,不需要进行权限管理,反之则为2
|
||||
Config string `gorm:"column:config;not null;comment:类型下所需路由以及参数" json:"config"` // 类型下所需路由以及参数
|
||||
Type int32 `gorm:"column:type;not null;default:3" json:"type"`
|
||||
Name string `gorm:"column:name;not null;comment:工具名称" json:"name"` // 工具名称
|
||||
Name string `gorm:"column:name;not null;default:1;comment:工具名称" json:"name"` // 工具名称
|
||||
Index string `gorm:"column:index;not null;comment:索引" json:"index"` // 索引
|
||||
Desc string `gorm:"column:desc;not null;comment:工具描述" json:"desc"` // 工具描述
|
||||
TempPrompt string `gorm:"column:temp_prompt;not null;comment:提示词模板" json:"temp_prompt"` // 提示词模板
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
|
||||
Status int32 `gorm:"column:status;not null" json:"status"`
|
||||
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ type AiChatHi struct {
|
|||
Useful int32 `gorm:"column:useful;not null;comment:0不评价,1有用,其他为无用" json:"useful"` // 0不评价,1有用,其他为无用
|
||||
CreateAt time.Time `gorm:"column:create_at;default:CURRENT_TIMESTAMP" json:"create_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;default:CURRENT_TIMESTAMP" json:"updated_at"`
|
||||
TaskID int32 `gorm:"column:task_id;comment:任务id" json:"task_id"` // 任务id
|
||||
Content string `gorm:"column:content;comment:前端回传数据" json:"content"` // 前端回传数据
|
||||
TaskID int32 `gorm:"column:task_id;not null" json:"task_id"` // 任务ID
|
||||
Content string `gorm:"column:content" json:"content"` // 前端回传数据
|
||||
}
|
||||
|
||||
// TableName AiChatHi's table name
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
// 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
|
||||
|
||||
const TableNameAiReportDailyCache = "ai_report_daily_cache"
|
||||
|
||||
// AiReportDailyCache mapped from table <ai_report_daily_cache>
|
||||
type AiReportDailyCache struct {
|
||||
ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"`
|
||||
CacheKey string `gorm:"column:cache_key;not null;default:1;comment:索引方式,可以是任意类型" json:"cache_key"` // 索引方式,可以是任意类型
|
||||
Value string `gorm:"column:value;comment:类型下所需路由以及参数" json:"value"` // 类型下所需路由以及参数
|
||||
CacheIndex string `gorm:"column:cache_index;not null;comment:类型" json:"cache_index"` // 类型
|
||||
Status int32 `gorm:"column:status;not null;default:1" json:"status"`
|
||||
}
|
||||
|
||||
// TableName AiReportDailyCache's table name
|
||||
func (*AiReportDailyCache) TableName() string {
|
||||
return TableNameAiReportDailyCache
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
package callback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"ai_scheduler/internal/pkg"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Manager interface {
|
||||
Register(ctx context.Context, taskID string, sessionID string) error
|
||||
Wait(ctx context.Context, taskID string, timeout time.Duration) (string, error)
|
||||
Notify(ctx context.Context, taskID string, result string) error
|
||||
GetSession(ctx context.Context, taskID string) (string, error)
|
||||
}
|
||||
|
||||
type RedisManager struct {
|
||||
rdb *redis.Client
|
||||
}
|
||||
|
||||
func NewRedisManager(rdb *pkg.Rdb) *RedisManager {
|
||||
return &RedisManager{
|
||||
rdb: rdb.Rdb,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
keyPrefixSession = "callback:session:"
|
||||
keyPrefixSignal = "callback:signal:"
|
||||
defaultTTL = 24 * time.Hour
|
||||
)
|
||||
|
||||
func (m *RedisManager) Register(ctx context.Context, taskID string, sessionID string) error {
|
||||
key := keyPrefixSession + taskID
|
||||
return m.rdb.Set(ctx, key, sessionID, defaultTTL).Err()
|
||||
}
|
||||
|
||||
func (m *RedisManager) Wait(ctx context.Context, taskID string, timeout time.Duration) (string, error) {
|
||||
key := keyPrefixSignal + taskID
|
||||
// BLPop 阻塞等待
|
||||
result, err := m.rdb.BLPop(ctx, timeout, key).Result()
|
||||
if err != nil {
|
||||
if err == redis.Nil {
|
||||
return "", fmt.Errorf("timeout waiting for callback")
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
// result[0] is key, result[1] is value
|
||||
if len(result) < 2 {
|
||||
return "", fmt.Errorf("invalid redis result")
|
||||
}
|
||||
return result[1], nil
|
||||
}
|
||||
|
||||
func (m *RedisManager) Notify(ctx context.Context, taskID string, result string) error {
|
||||
key := keyPrefixSignal + taskID
|
||||
// Push 信号,同时设置过期时间防止堆积
|
||||
pipe := m.rdb.Pipeline()
|
||||
pipe.RPush(ctx, key, result)
|
||||
pipe.Expire(ctx, key, 1*time.Hour) // 信号列表也需要过期
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *RedisManager) GetSession(ctx context.Context, taskID string) (string, error) {
|
||||
key := keyPrefixSession + taskID
|
||||
return m.rdb.Get(ctx, key).Result()
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package callback
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewRedisManager, wire.Bind(new(Manager), new(*RedisManager)))
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package component
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/domain/component/callback"
|
||||
"ai_scheduler/internal/pkg/lsxd"
|
||||
)
|
||||
|
||||
type Components struct {
|
||||
Callback callback.Manager
|
||||
LSXDLogin *lsxd.Login
|
||||
}
|
||||
|
||||
func NewComponents(callbackManager callback.Manager, lsxdLogin *lsxd.Login) *Components {
|
||||
return &Components{
|
||||
Callback: callbackManager,
|
||||
LSXDLogin: lsxdLogin,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package component
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/domain/component/callback"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetComponent = wire.NewSet(NewComponents)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
callback.NewRedisManager, wire.Bind(new(callback.Manager), new(*callback.RedisManager)),
|
||||
NewComponents,
|
||||
)
|
||||
|
|
@ -3,15 +3,14 @@ package llm
|
|||
import "time"
|
||||
|
||||
type Options struct {
|
||||
Temperature float32
|
||||
MaxTokens int
|
||||
Stream bool
|
||||
Timeout time.Duration
|
||||
Modalities []string
|
||||
SystemPrompt string
|
||||
Model string
|
||||
TopP float32
|
||||
Stop []string
|
||||
Endpoint string
|
||||
Thinking bool
|
||||
Temperature float32
|
||||
MaxTokens int
|
||||
Stream bool
|
||||
Timeout time.Duration
|
||||
Modalities []string
|
||||
SystemPrompt string
|
||||
Model string
|
||||
TopP float32
|
||||
Stop []string
|
||||
Endpoint string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@ func New() *Adapter { return &Adapter{} }
|
|||
|
||||
func (a *Adapter) Generate(ctx context.Context, input []*schema.Message, opts llm.Options) (*schema.Message, error) {
|
||||
cm, err := eino_ollama.NewChatModel(ctx, &eino_ollama.ChatModelConfig{
|
||||
BaseURL: opts.Endpoint,
|
||||
Timeout: opts.Timeout,
|
||||
Model: opts.Model,
|
||||
Options: &eino_ollama.Options{Temperature: opts.Temperature, NumPredict: opts.MaxTokens},
|
||||
Thinking: &eino_ollama.ThinkValue{Value: opts.Thinking},
|
||||
BaseURL: opts.Endpoint,
|
||||
Timeout: opts.Timeout,
|
||||
Model: opts.Model,
|
||||
Options: &eino_ollama.Options{Temperature: opts.Temperature, NumPredict: opts.MaxTokens},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// SessionAdapter 适配 impl.SessionImpl 到 SessionRepo 接口
|
||||
type SessionAdapter struct {
|
||||
impl *impl.SessionImpl
|
||||
}
|
||||
|
||||
func NewSessionAdapter(impl *impl.SessionImpl) *SessionAdapter {
|
||||
return &SessionAdapter{impl: impl}
|
||||
}
|
||||
|
||||
func (s *SessionAdapter) GetUserName(ctx context.Context, sessionID string) (string, error) {
|
||||
// 复用 SessionImpl 的查询能力
|
||||
// 这里假设 sessionID 是唯一的,直接用 FindOne
|
||||
session, has, err := s.impl.FindOne(s.impl.WithSessionId(sessionID))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !has {
|
||||
return "", errors.New("session not found")
|
||||
}
|
||||
return session.UserName, nil
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package repo
|
||||
|
||||
import "github.com/google/wire"
|
||||
|
||||
var ProviderSet = wire.NewSet(NewRepos)
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/data/impl"
|
||||
"ai_scheduler/internal/pkg/utils_oss"
|
||||
"ai_scheduler/utils"
|
||||
)
|
||||
|
||||
// Repos 聚合所有 Repository
|
||||
type Repos struct {
|
||||
Session SessionRepo
|
||||
OssClient *utils_oss.Client
|
||||
Rdb *utils.Rdb
|
||||
}
|
||||
|
||||
func NewRepos(sessionImpl *impl.SessionImpl, cfg *config.Config, rdb *utils.Rdb) *Repos {
|
||||
ossClient, _ := utils_oss.NewClient(cfg)
|
||||
return &Repos{
|
||||
Session: NewSessionAdapter(sessionImpl),
|
||||
OssClient: ossClient,
|
||||
Rdb: rdb,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// SessionRepo 定义会话相关的查询接口
|
||||
// 这里只暴露 workflow 真正需要的方法,避免直接依赖 impl 层
|
||||
type SessionRepo interface {
|
||||
GetUserName(ctx context.Context, sessionID string) (string, error)
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package excel_generator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
// Client Excel 生成器
|
||||
type Client struct{}
|
||||
|
||||
func New() *Client {
|
||||
return &Client{}
|
||||
}
|
||||
|
||||
// Call 根据模板和数据生成 Excel 字节流
|
||||
func (g *Client) Call(req *ExcelGeneratorRequest) ([]byte, error) {
|
||||
if req.StartRow <= 0 {
|
||||
req.StartRow = 2
|
||||
}
|
||||
if req.StyleRow <= 0 {
|
||||
req.StyleRow = 2
|
||||
}
|
||||
|
||||
f, err := excelize.OpenFile(req.TemplatePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
sheet := f.GetSheetName(0)
|
||||
|
||||
// 若提供标题,替换第一行表格标题
|
||||
if len(req.Title) > 0 {
|
||||
f.SetCellValue(sheet, "A1", req.Title)
|
||||
}
|
||||
|
||||
// 获取样式和行高
|
||||
styleID, err := f.GetCellStyle(sheet, fmt.Sprintf("A%d", req.StyleRow))
|
||||
if err != nil {
|
||||
log.Errorf("获取样式失败: %v", err)
|
||||
styleID = 0
|
||||
}
|
||||
rowHeight, err := f.GetRowHeight(sheet, req.StyleRow)
|
||||
if err != nil {
|
||||
log.Errorf("获取行高失败: %v", err)
|
||||
rowHeight = 31 // 默认高度
|
||||
}
|
||||
|
||||
row := req.StartRow
|
||||
for i, item := range req.ExcelData {
|
||||
currentRow := row + i
|
||||
|
||||
// 设置行高
|
||||
f.SetRowHeight(sheet, currentRow, rowHeight)
|
||||
|
||||
// 填充数据
|
||||
for col, value := range item {
|
||||
cell := fmt.Sprintf("%c%d", 'A'+col, currentRow)
|
||||
f.SetCellValue(sheet, cell, value)
|
||||
}
|
||||
|
||||
// 设置样式
|
||||
if styleID != 0 {
|
||||
endCol := 'A' + len(item) - 1
|
||||
f.SetCellStyle(sheet, fmt.Sprintf("A%d", currentRow), fmt.Sprintf("%c%d", endCol, currentRow), styleID)
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := f.WriteToBuffer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package excel_generator
|
||||
|
||||
type ExcelGeneratorRequest struct {
|
||||
TemplatePath string // 模板文件路径
|
||||
ExcelData [][]string // 二维字符串数组
|
||||
StartRow int // 数据填充起始行 (默认 2)
|
||||
StyleRow int // 样式参考行 (默认 2)
|
||||
Title string // 表格标题(仅替换)
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package image_converter
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Client 图片转换器
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Call 将 Excel 文件转换为图片
|
||||
func (c *Client) Call(filename string, fileBytes []byte, scale int) ([]byte, error) {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
part, err := writer.CreateFormFile("file", filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = io.Copy(part, bytes.NewReader(fileBytes)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 添加 scale 参数
|
||||
if scale <= 0 {
|
||||
scale = 2
|
||||
}
|
||||
if err = writer.WriteField("scale", fmt.Sprintf("%d", scale)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = writer.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", c.cfg.BaseURL, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("excel2pic service returned status: %s", resp.Status)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
package official_product
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Call 调用销售同比分析接口
|
||||
func (c *Client) Call(ctx context.Context, req OfficialProductRequest) (*OfficialProductData, error) {
|
||||
// 构建 URL 参数
|
||||
var queryParams []string
|
||||
|
||||
if req.Page > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page))
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit))
|
||||
}
|
||||
|
||||
for _, pid := range req.OfficialProductIds {
|
||||
queryParams = append(queryParams, fmt.Sprintf("official_product_id[]=%s", pid))
|
||||
}
|
||||
|
||||
for _, t := range req.Ct {
|
||||
queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+")))
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryParams, "&")
|
||||
fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString)
|
||||
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey),
|
||||
}
|
||||
|
||||
reqObj := l_request.Request{
|
||||
Method: "GET",
|
||||
Url: fullURL,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
res, err := reqObj.Send()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
var resData OfficialProductResponse
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||
}
|
||||
|
||||
return &resData.Data, nil
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package official_product
|
||||
|
||||
// OfficialProductRequest 销售同比分析请求参数
|
||||
type OfficialProductRequest struct {
|
||||
Page int `json:"page"` // 页码
|
||||
Limit int `json:"limit"` // 每页条数
|
||||
OfficialProductIds []string `json:"official_product_ids"` // 官方产品ID列表
|
||||
Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间]
|
||||
}
|
||||
|
||||
// OfficialProductResponse 销售同比分析响应结构
|
||||
type OfficialProductResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data OfficialProductData `json:"data"`
|
||||
}
|
||||
|
||||
type OfficialProductData struct {
|
||||
OfficialProductSum []OfficialProductItem `json:"officialProductSum"`
|
||||
DataCount int `json:"dataCount"`
|
||||
}
|
||||
|
||||
type OfficialProductItem struct {
|
||||
OfficialProductId int `json:"officialProductId"`
|
||||
OfficialProductName string `json:"officialProductName"`
|
||||
CurrentNum int `json:"currentNum"`
|
||||
HistoryOneNum int `json:"historyOneNum"`
|
||||
HistoryTwoNum int `json:"historyTwoNum"`
|
||||
HistoryOneDiff int `json:"historyOneDiff"`
|
||||
HistoryTwoDiff int `json:"historyTwoDiff"`
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
package official_product_decline
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Call 调用销售同比下滑详情接口
|
||||
func (c *Client) Call(ctx context.Context, req OfficialProductDeclineRequest) (*OfficialProductDeclineData, error) {
|
||||
// 构建 URL 参数
|
||||
var queryParams []string
|
||||
|
||||
if req.Page > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page))
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit))
|
||||
}
|
||||
if req.DownwardValue > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("downwardValue=%d", req.DownwardValue))
|
||||
}
|
||||
// showTime 可能是 0,所以这里不做 > 0 判断,如果业务默认是 0 可以忽略,或者根据实际需求
|
||||
// 假设始终传递该参数如果已设置
|
||||
queryParams = append(queryParams, fmt.Sprintf("showTime=%d", req.ShowTime))
|
||||
|
||||
for _, pid := range req.OfficialProductIds {
|
||||
queryParams = append(queryParams, fmt.Sprintf("official_product_id[]=%s", pid))
|
||||
}
|
||||
|
||||
for _, t := range req.Ct {
|
||||
queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+")))
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryParams, "&")
|
||||
fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString)
|
||||
|
||||
headers := map[string]string{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey),
|
||||
"Accept-Language": "zh-CN,zh;q=0.9,en-GB;q=0.8,en;q=0.7",
|
||||
}
|
||||
|
||||
reqObj := l_request.Request{
|
||||
Method: "GET",
|
||||
Url: fullURL,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
res, err := reqObj.Send()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
var resData OfficialProductDeclineResponse
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||
}
|
||||
|
||||
return &resData.Data, nil
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package official_product_decline
|
||||
|
||||
// OfficialProductDeclineRequest 销售同比下滑详情请求参数
|
||||
type OfficialProductDeclineRequest struct {
|
||||
Page int `json:"page"` // 页码
|
||||
Limit int `json:"limit"` // 每页条数
|
||||
Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间]
|
||||
OfficialProductIds []string `json:"official_product_ids"` // 官方产品ID列表
|
||||
DownwardValue int `json:"downward_value"` // 下滑值
|
||||
ShowTime int `json:"show_time"` // 是否显示时间 (0:不显示, 1:显示)
|
||||
}
|
||||
|
||||
// OfficialProductDeclineResponse 销售同比下滑详情响应结构
|
||||
type OfficialProductDeclineResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data OfficialProductDeclineData `json:"data"`
|
||||
}
|
||||
|
||||
type OfficialProductDeclineData struct {
|
||||
OfficialProductSumDecline []OfficialProductDeclineItem `json:"officialProductSumDecline"`
|
||||
DataCount int `json:"dataCount"`
|
||||
}
|
||||
|
||||
type OfficialProductDeclineItem struct {
|
||||
ResellerId int `json:"resellerId"`
|
||||
OfficialProductId int `json:"officialProductId"`
|
||||
OfficialProductName string `json:"officialProductName"`
|
||||
ResellerName string `json:"resellerName"`
|
||||
CurrentNum int `json:"currentNum"`
|
||||
HistoryOneNum int `json:"historyOneNum"`
|
||||
HistoryTwoNum int `json:"historyTwoNum"`
|
||||
HistoryOneDiff int `json:"historyOneDiff"`
|
||||
HistoryTwoDiff int `json:"historyTwoDiff"`
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package ours_product_loss
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Call 调用负利润分析接口
|
||||
// 支持列表查询和详情查询
|
||||
// 列表查询:提供 page, limit, ct[]
|
||||
// 详情查询:提供 ct[], resellerId
|
||||
func (c *Client) Call(ctx context.Context, req OursProductLossRequest) (*OursProductLossData, error) {
|
||||
// 处理数组参数 ct[]
|
||||
// util.StructToMap 通常不支持数组到 url query array 的转换,这里手动处理查询字符串
|
||||
// 或者如果 l_request 支持 map 中的 slice 自动转换最好,假设不支持需手动拼接
|
||||
|
||||
// 构建 URL 参数
|
||||
var queryParams []string
|
||||
|
||||
if req.Page > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("page=%d", req.Page))
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
queryParams = append(queryParams, fmt.Sprintf("limit=%d", req.Limit))
|
||||
}
|
||||
if req.ResellerId != "" {
|
||||
queryParams = append(queryParams, fmt.Sprintf("resellerId=%s", req.ResellerId))
|
||||
}
|
||||
|
||||
for _, t := range req.Ct {
|
||||
// URL 编码处理,这里简单处理,实际应使用 url.QueryEscape
|
||||
// 假设输入已经是合法的格式
|
||||
queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+")))
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryParams, "&")
|
||||
fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString)
|
||||
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey),
|
||||
}
|
||||
|
||||
reqObj := l_request.Request{
|
||||
Method: "GET",
|
||||
Url: fullURL,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
res, err := reqObj.Send()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
var resData OursProductLossResponse
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||
}
|
||||
|
||||
return &resData.Data, nil
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package ours_product_loss
|
||||
|
||||
// OursProductLossRequest 负利润分析请求参数
|
||||
type OursProductLossRequest struct {
|
||||
Page int `json:"page"` // 页码
|
||||
Limit int `json:"limit"` // 每页条数
|
||||
Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间]
|
||||
ResellerId string `json:"reseller_id"` // 经销商ID (详情查询时使用)
|
||||
}
|
||||
|
||||
// OursProductLossResponse 负利润分析响应结构
|
||||
type OursProductLossResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data OursProductLossData `json:"data"`
|
||||
}
|
||||
|
||||
type OursProductLossData struct {
|
||||
List []OursProductLossItem `json:"list"`
|
||||
DataCount int `json:"dataCount"`
|
||||
}
|
||||
|
||||
type OursProductLossItem struct {
|
||||
OursProductId int `json:"oursProductId"`
|
||||
OursProductName string `json:"oursProductName"`
|
||||
ResellerName string `json:"resellerName"`
|
||||
ResellerId int `json:"resellerId"`
|
||||
Loss float64 `json:"loss"`
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package profit_ranking
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Call 调用利润同比排行榜接口
|
||||
func (c *Client) Call(ctx context.Context, req ProfitRankingRequest) (*ProfitRankingData, error) {
|
||||
// 构建 URL 参数
|
||||
var queryParams []string
|
||||
|
||||
for _, t := range req.Ct {
|
||||
queryParams = append(queryParams, fmt.Sprintf("ct[]=%s", strings.ReplaceAll(t, " ", "+")))
|
||||
}
|
||||
|
||||
for _, rid := range req.ResellerIds {
|
||||
queryParams = append(queryParams, fmt.Sprintf("resellerIds[]=%s", rid))
|
||||
}
|
||||
|
||||
queryString := strings.Join(queryParams, "&")
|
||||
fullURL := fmt.Sprintf("%s?%s", c.cfg.BaseURL, queryString)
|
||||
|
||||
headers := map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", c.cfg.APIKey),
|
||||
}
|
||||
|
||||
reqObj := l_request.Request{
|
||||
Method: "GET",
|
||||
Url: fullURL,
|
||||
Headers: headers,
|
||||
}
|
||||
|
||||
res, err := reqObj.Send()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
var resData ProfitRankingResponse
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败,err: %v, resp: %s", err, res.Text)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return nil, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||
}
|
||||
|
||||
return &resData.Data, nil
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package profit_ranking
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestClient_Call(t *testing.T) {
|
||||
cfg := config.ToolConfig{
|
||||
BaseURL: "http://test.analysis.com/api/dataanalytics/profitRankingSum",
|
||||
APIKey: "test_jwt_token",
|
||||
}
|
||||
|
||||
client := New(cfg)
|
||||
assert.NotNil(t, client)
|
||||
|
||||
req := ProfitRankingRequest{
|
||||
Ct: []string{"2025-01-01 00:00:00", "2025-01-01 23:59:59"},
|
||||
ResellerIds: []string{"1001", "1002"},
|
||||
}
|
||||
|
||||
t.Logf("Testing Call with req: %+v", req)
|
||||
// _, err := client.Call(context.Background(), req)
|
||||
// assert.Error(t, err)
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package profit_ranking
|
||||
|
||||
// ProfitRankingRequest 利润同比排行请求参数
|
||||
type ProfitRankingRequest struct {
|
||||
Ct []string `json:"ct"` // 时间范围 [开始时间, 结束时间]
|
||||
ResellerIds []string `json:"reseller_ids"` // 经销商ID列表
|
||||
}
|
||||
|
||||
// ProfitRankingResponse 利润同比排行响应结构
|
||||
type ProfitRankingResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data ProfitRankingData `json:"data"`
|
||||
}
|
||||
|
||||
type ProfitRankingData struct {
|
||||
List []ProfitRankingItem `json:"list"`
|
||||
DataCount int `json:"dataCount"`
|
||||
}
|
||||
|
||||
type ProfitRankingItem struct {
|
||||
ResellerId string `json:"resellerId"`
|
||||
ResellerName string `json:"resellerName"`
|
||||
CurrentProfit float64 `json:"currentProfit"`
|
||||
HistoryOneProfit float64 `json:"historyOneProfit"`
|
||||
HistoryTwoProfit float64 `json:"historyTwoProfit"`
|
||||
HistoryOneDiff float64 `json:"historyOneDiff"`
|
||||
HistoryTwoDiff float64 `json:"historyTwoDiff"`
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package goods_add
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Call(ctx context.Context, req *GoodsAddRequest) (*GoodsAddResponse, error) {
|
||||
apiReq, _ := util.StructToMap(req)
|
||||
|
||||
r := l_request.Request{
|
||||
Method: "Post",
|
||||
Url: c.cfg.BaseURL,
|
||||
Json: apiReq,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
res, err := r.Send()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
type resType struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"message"`
|
||||
Data struct {
|
||||
Id int `json:"id"` // 商品 ID
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
var resData resType
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败,err: %v", err)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return nil, fmt.Errorf("业务错误,%s", resData.Msg)
|
||||
}
|
||||
|
||||
toolResp := &GoodsAddResponse{
|
||||
PreviewUrl: c.cfg.AddURL,
|
||||
SpuCode: req.SpuCode,
|
||||
Id: resData.Data.Id,
|
||||
}
|
||||
|
||||
return toolResp, nil
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package goods_add
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test_Call
|
||||
func Test_Call(t *testing.T) {
|
||||
req := &GoodsAddRequest{
|
||||
Unit: "元",
|
||||
IsComposeGoods: 2,
|
||||
GoodsAttributes: "<p><span style=\"color: rgb(96, 98, 102); background-color: rgb(255, 255, 255); font-size: 14px;\">商品规格参数</span></p>",
|
||||
Introduction: "<p><span style=\"color: rgb(96, 98, 102); background-color: rgb(255, 255, 255); font-size: 14px;\">商品卖点</span></p>",
|
||||
GoodsIllustration: "<p><span style=\"color: rgb(96, 98, 102); background-color: rgb(255, 255, 255); font-size: 14px;\">商品说明</span></p>",
|
||||
IsHot: 2,
|
||||
Title: "fu测试001",
|
||||
GoodsNum: "futest001sku",
|
||||
SpuCode: "futest001spu",
|
||||
SpuName: "fu测试001",
|
||||
Price: 100,
|
||||
SalesPrice: 80,
|
||||
Discount: 15,
|
||||
TaxRate: 13,
|
||||
FreightId: 3,
|
||||
Remark: "备注说明",
|
||||
SellByDate: 180,
|
||||
ExternalPrice: 120,
|
||||
GoodsBarCode: "futest001code2",
|
||||
GoodsCode: "futest001code1",
|
||||
SellByDateUnit: "天",
|
||||
BrandId: 3,
|
||||
ExternalUrl: "https://www.baidu.com",
|
||||
}
|
||||
|
||||
cfg := config.ToolConfig{
|
||||
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/add",
|
||||
}
|
||||
|
||||
client := New(cfg)
|
||||
toolResp, err := client.Call(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Call() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("toolResp: %v\n", toolResp)
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package goods_add
|
||||
|
||||
type GoodsAddRequest struct {
|
||||
Title string `json:"title"` // 商品标题
|
||||
GoodsCode string `json:"goods_code"` // 商品编码
|
||||
SpuName string `json:"spu_name"` // SPU 名称
|
||||
SpuCode string `json:"spu_code"` // SPU 编码
|
||||
GoodsNum string `json:"goods_num"` // 商品货号
|
||||
GoodsBarCode string `json:"goods_bar_code"` // 商品条形码
|
||||
Price float64 `json:"price"` // 市场价
|
||||
SalesPrice float64 `json:"sales_price"` // 建议销售价
|
||||
ExternalPrice float64 `json:"external_price"` // 电商销售价格
|
||||
Unit string `json:"unit"` // 价格单位
|
||||
Discount int `json:"discount"` // 折扣
|
||||
TaxRate int `json:"tax_rate"` // 税率
|
||||
FreightId int `json:"freight_id"` // 运费模板 ID
|
||||
SellByDate int `json:"sell_by_date"` // 保质期
|
||||
SellByDateUnit string `json:"sell_by_date_unit"` // 保质期单位
|
||||
BrandId int `json:"brand_id"` // 品牌 ID
|
||||
IsHot int `json:"is_hot"` // 是否热销主推 1.是 2.否(默认)
|
||||
ExternalUrl string `json:"external_url"` // 外部平台链接
|
||||
Introduction string `json:"introduction"` // 商品卖点
|
||||
GoodsAttributes string `json:"goods_attributes"` // 商品规格参数
|
||||
GoodsIllustration string `json:"goods_illustration"` // 商品说明
|
||||
Remark string `json:"remark"` // 备注说明
|
||||
IsComposeGoods int `json:"is_compose_goods"` // 是否组合商品 1.是 2.否(默认)
|
||||
}
|
||||
|
||||
type GoodsAddResponse struct {
|
||||
PreviewUrl string `json:"preview_url"` // 预览URL
|
||||
SpuCode string `json:"spu_code"` // SPU编码
|
||||
Id int `json:"id"` // 商品ID
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package goods_brand_search
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Call(ctx context.Context, name string) (int, error) {
|
||||
if name == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
reqBody := GoodsBrandSearchRequest{
|
||||
Page: 1,
|
||||
Limit: 1,
|
||||
Search: SearchCondition{
|
||||
Name: name,
|
||||
},
|
||||
}
|
||||
|
||||
apiReq, _ := util.StructToMap(reqBody)
|
||||
|
||||
req := l_request.Request{
|
||||
Method: "Post",
|
||||
Url: c.cfg.BaseURL,
|
||||
Json: apiReq,
|
||||
Headers: map[string]string{
|
||||
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
res, err := req.Send()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
var resData GoodsBrandSearchResponse
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return 0, fmt.Errorf("解析响应失败,err: %v", err)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return 0, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||
}
|
||||
|
||||
if len(resData.Data.List) == 0 {
|
||||
return 0, fmt.Errorf("品牌不存在")
|
||||
}
|
||||
|
||||
// 返回第一个匹配的品牌ID
|
||||
return resData.Data.List[0].ID, nil
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package goods_brand_search
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test_Call
|
||||
func Test_Call(t *testing.T) {
|
||||
// 使用示例中的查询条件
|
||||
name := "vivo"
|
||||
|
||||
cfg := config.ToolConfig{
|
||||
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/goods/brand/list",
|
||||
}
|
||||
|
||||
client := New(cfg)
|
||||
toolResp, err := client.Call(context.Background(), name)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Call() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("toolResp (BrandID): %v\n", toolResp)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package goods_brand_search
|
||||
|
||||
type GoodsBrandSearchRequest struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Search SearchCondition `json:"search"`
|
||||
}
|
||||
|
||||
type SearchCondition struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type GoodsBrandSearchResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"message"`
|
||||
Data struct {
|
||||
List []BrandInfo `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type BrandInfo struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Logo string `json:"logo"`
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
package goods_category_add
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"ai_scheduler/internal/pkg/l_request"
|
||||
"ai_scheduler/internal/pkg/util"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
cfg config.ToolConfig
|
||||
}
|
||||
|
||||
func New(cfg config.ToolConfig) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Call(ctx context.Context, req *GoodsCategoryAddRequest) (bool, error) {
|
||||
apiReq, _ := util.StructToMap(req)
|
||||
|
||||
r := l_request.Request{
|
||||
Method: "Post",
|
||||
Url: c.cfg.BaseURL,
|
||||
Json: apiReq,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
|
||||
res, err := r.Send()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("请求失败,err: %v", err)
|
||||
}
|
||||
|
||||
var resData GoodsCategoryAddResponse
|
||||
if err := json.Unmarshal([]byte(res.Text), &resData); err != nil {
|
||||
return false, fmt.Errorf("解析响应失败,err: %v", err)
|
||||
}
|
||||
|
||||
if resData.Code != 200 {
|
||||
return false, fmt.Errorf("业务错误,code: %d, msg: %s", resData.Code, resData.Msg)
|
||||
}
|
||||
|
||||
return resData.Data.IsSuccess, nil
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package goods_category_add
|
||||
|
||||
import (
|
||||
"ai_scheduler/internal/config"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test_Call
|
||||
func Test_Call(t *testing.T) {
|
||||
req := &GoodsCategoryAddRequest{
|
||||
GoodsId: 8496,
|
||||
CategoryIds: []int{1667},
|
||||
IsCover: false,
|
||||
}
|
||||
|
||||
cfg := config.ToolConfig{
|
||||
BaseURL: "https://gateway.dev.cdlsxd.cn/goods-admin/api/v1/good/category/relation/add",
|
||||
}
|
||||
|
||||
client := New(cfg)
|
||||
toolResp, err := client.Call(context.Background(), req)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Call() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("toolResp: %v\n", toolResp)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue