Compare commits

..

2 Commits

Author SHA1 Message Date
fuzhongyun 23f92d4f8b Merge branch 'master' into feature/fzy/optimize 2025-12-26 17:18:29 +08:00
fuzhongyun a39b7f86df feat: 增加图片识别结果传输 2025-12-24 18:00:16 +08:00
252 changed files with 1657 additions and 17618 deletions

4
.gitignore vendored
View File

@ -4,6 +4,4 @@
docs
cmd/server/wire_gen.go
__debug*
.bin/
.idea/
cache/
.bin/

View File

@ -1,28 +1,3 @@
## 使用官方Go镜像作为构建环境
FROM golang:1.24.7-alpine AS builder
# 设置工作目录
WORKDIR /app
# 使用国内镜像源加速APK包下载
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
# 使用国内镜像源加速依赖下载
ENV GOPROXY=https://goproxy.cn,direct
# 复制项目源码
COPY . .
# 复制go模块依赖文件
COPY go.mod go.sum ./
RUN go mod tidy
RUN go install github.com/google/wire/cmd/wire@latest
RUN wire ./cmd/server
# 编译Go应用程序生成静态链接的二进制文件
RUN go build -ldflags="-s -w" -o server ./cmd/server
# 创建最终镜像用于运行编译后的Go程序
FROM alpine
@ -37,13 +12,10 @@ RUN echo 'http://mirrors.ustc.edu.cn/alpine/v3.5/main' > /etc/apk/repositories \
WORKDIR /app
# 将编译好的二进制文件从构建阶段复制到运行阶段
COPY --from=builder /app/server ./server
# 复制配置文件夹
COPY --from=builder /app/config ./config
COPY ./ /app
ENV TZ=Asia/Shanghai
# 设置容器启动时运行的命令
CMD ["./server"]
CMD ["./bin/server"]

View File

@ -1,4 +0,0 @@
[https://p6-img.searchpstatp.com/tos-cn-i-vvloioitz3/6e5e76d274df2efabde9194a06f97e89~tplv-vvloioitz3-6:190:124.jpeg]
![图片](https://p6-img.searchpstatp.com/tos-cn-i-vvloioitz3/ab5ae998d8162b431f44fb2a0ed9ae33~tplv-vvloioitz3-6:190:124.jpeg)

View File

@ -2,7 +2,7 @@ package main
import (
"ai_scheduler/internal/config"
"context"
"flag"
"fmt"
@ -10,12 +10,8 @@ import (
)
func main() {
configPath := flag.String("config", "./config/config.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")
configPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
flag.Parse()
ctx := context.Background()
bc, err := config.LoadConfig(*configPath)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
@ -27,18 +23,8 @@ 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
}
log.Fatal(app.HttpServer.Listen(fmt.Sprintf(":%d", bc.Server.Port)))
}

View File

@ -5,20 +5,13 @@ 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/tools"
"ai_scheduler/internal/tools_bot"
"ai_scheduler/utils"
"github.com/gofiber/fiber/v2/log"
@ -29,19 +22,13 @@ import (
func InitializeApp(*config.Config, log.AllLogger) (*server.Servers, func(), error) {
panic(wire.Build(
server.ProviderSetServer,
workflow.ProviderSetWorkflow,
tools.ProviderSetTools,
pkg.ProviderSetClient,
services.ProviderSetServices,
biz.ProviderSetBiz,
impl.ProviderImpl,
utils.ProviderUtils,
dingtalk.ProviderSetDingTalk,
qywx.ProviderSetQywx,
tools_regis.ProviderToolsRegis,
// tool_callback.ProviderSetCallBackTools,
component.ProviderSet,
repo.ProviderSet,
tools_bot.ProviderSetBotTools,
))
}

View File

@ -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-test
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:

View File

@ -1,182 +0,0 @@
# 服务器配置
server:
port: 8090
host: "0.0.0.0"
ollama:
base_url: "http://192.168.6.109:11434"
model: "qwen3-coder:480b-cloud"
generate_model: "qwen3-coder:480b-cloud"
mapping_model: "deepseek-v3.2:cloud"
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"
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
channel_pool_len: 100
channel_pool_size: 32
llm_pool_len: 5
heartbeat_interval: 30
redis:
host: 47.97.27.195:6379
type: node
pass: lansexiongdi@666
key: report-api-test
pollSize: 5 #连接池大小不配置或配置为0表示不启用连接池
minIdleConns: 2 #最小空闲连接数
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
tls: 30
db:
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:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/direct/ai/%s"
add_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/direct/log/%s/%s"
api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzU4MDkxOTU4LCJuYmYiOjE3NTgwOTAxNTgsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Bjsx9f8yfcrV9EWxb0n6POwnXVOq9XPRD78JFZnnf1_VAVMN78W4W570SZL27PWuDnkD7E4oUg6RzeZwZgl7BZrNpNr-a-QpNC5qCptqrqXeNfVStmX7pxWA8GqnzI8ybkZgbhQ58Gje7DzdJtBq_8zte_LDaYhTYXdIc5EAG0AbCzAk22nPTl47nkMeHtmisXQVLEsdibl1hW3ViFJlXwfXvUrOENItmL1_mRYkggUB0MaTu2nHJOYM6PaOVGLHx-74eepnmK2rm6konFEb6ed-Ukc6gVR-nM9yWZaYLYNGNKJLwZoCX3tRuerq74n4kzQgWmUEJeaVI1yIGSw1zw"
zltxProduct:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/oursProduct"
add_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/platformProduct/getProductsByOfficialProductId"
api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzU2MTgyNTM1LCJuYmYiOjE3NTYxODA3MzUsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.N1xv1PYbcO8_jR5adaczc16YzGsr4z101gwEZdulkRaREBJNYTOnFrvRxTFx3RJTooXsqTqroE1MR84v_1WPX6BS6kKonA-kC1Jgot6yrt5rFWhGNGb2Cpr9rKIFCCQYmiGd3AUgDazEeaQ0_sodv3E-EXg9VfE1SX8nMcck9Yjnc8NCy7RTWaBIaSeOdZcEl-JfCD0S6GSx3oErp_hk-U9FKGwf60wAuDGTY1R0BP4BYpcEqS-C2LSnsSGyURi54Cuk5xH8r1WuF0Dm5bwAj5d7Hvs77-N_sUF-C5ONqyZJRAEhYLgcmN9RX_WQZfizdQJxizlTczdpzYfy-v-1eQ"
zltxOrderStatistics:
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/direct/ai/search/"
enabled: true
api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzU2MTgyNTM1LCJuYmYiOjE3NTYxODA3MzUsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.N1xv1PYbcO8_jR5adaczc16YzGsr4z101gwEZdulkRaREBJNYTOnFrvRxTFx3RJTooXsqTqroE1MR84v_1WPX6BS6kKonA-kC1Jgot6yrt5rFWhGNGb2Cpr9rKIFCCQYmiGd3AUgDazEeaQ0_sodv3E-EXg9VfE1SX8nMcck9Yjnc8NCy7RTWaBIaSeOdZcEl-JfCD0S6GSx3oErp_hk-U9FKGwf60wAuDGTY1R0BP4BYpcEqS-C2LSnsSGyURi54Cuk5xH8r1WuF0Dm5bwAj5d7Hvs77-N_sUF-C5ONqyZJRAEhYLgcmN9RX_WQZfizdQJxizlTczdpzYfy-v-1eQ"
knowledge:
base_url: "http://117.175.169.61:10000"
enabled: true
DingTalkBot:
enabled: true
api_key: "dingsbbntrkeiyazcfdg"
api_secret: "ObqxwyR20r9rVNhju0sCPQyQA98_FZSc32W4vgxnGFH_b02HZr1BPCJsOAF816nu"
zltxOrderAfterSaleSupplier:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/directs"
zltxOrderAfterSaleReseller:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
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:
img_recognize:
system_prompt:
'你是一个具备图像理解与用户意图分析能力的智能助手。当用户提供一张图片时,请完成以下任务:
1. 关键信息提取:
提取出图片中对用户可能有用的关键信息(例如金额、日期、标题、编号、联系信息、商品名称等)。
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
'
user_prompt: '识别图片内容'
# 权限配置
permissionConfig:
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="

View File

@ -4,51 +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"
vl_model: "gemini-3-pro-preview"
base_url: "http://127.0.0.1:11434"
model: "qwen3-coder:480b-cloud"
generate_model: "qwen3-coder:480b-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"
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
channel_pool_len: 100
channel_pool_size: 32
llm_pool_len: 5
heartbeat_interval: 30
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 +32,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:
@ -95,81 +64,8 @@ tools:
zltxOrderAfterSaleResellerBatch:
enabled: true
base_url: "https://gateway.dev.cdlsxd.cn/zltx_api/admin/afterSales/reseller_pre_ai"
weather:
enabled: true
base_url: "https://restapi.amap.com/v3/weather/weatherInfo"
api_key: "12afbde5ab78cb7e575ff76bd0bdef2b"
cozeExpress:
enabled: true
base_url: "https://api.coze.cn"
api_key: "7582477438102552616"
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
cozeCompany:
enabled: true
base_url: "https://api.coze.cn"
api_key: "7583905168607100978"
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
zltxResellerAuthProductToManagerAndDefaultLossReason:
base_url: "https://revcl.1688sup.com/api/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason"
# eino tool 配置
eino_tools:
# 货易通商品上传
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:
@ -183,52 +79,3 @@ default_prompt:
# 权限配置
permissionConfig:
permission_url: "http://api.test.user.1688sup.cn:8001/v1/menu/myCodes?systemId="
# llm 服务配置
llm:
providers:
ollama:
endpoint: http://host.docker.internal:11434
timeout: 60s
models:
- id: qwen3-coder:480b-cloud
name: qwen3-coder:480b-cloud
streaming: true
modalities: [text]
max_tokens: 4096
vllm:
endpoint: http://117.175.169.61:16001
timeout: 60s
models:
- id: models/Qwen2.5-VL-3B-Instruct-AWQ
name: qwen2.5-vl-3b
streaming: true
modalities: [text, image]
max_tokens: 4096
# 每个能力只绑定一个 provider+model不做自动回退
capabilities:
intent:
provider: vllm
model: qwen2.5-vl-3b
parameters:
temperature: 0.2
max_tokens: 4096
stream: false
vision:
provider: vllm
model: qwen2.5-vl-3b
parameters:
temperature: 0.5
max_tokens: 4096
stream: true
chat:
provider: ollama
model: qwen3-coder:480b-cloud
parameters:
temperature: 0.7
max_tokens: 4096
stream: true

View File

@ -1,10 +1,9 @@
#export GO111MODULE=on
#export GOPROXY=https://goproxy.cn,direct
#export GOPATH=/root/go
#export GOCACHE=/root/.cache/go-build
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
export GOPATH=/root/go
export GOCACHE=/root/.cache/go-build
export CONTAINER_NAME=ai_scheduler
export NETWORK_NAME=ai_scheduler_network
#export CGO_ENABLED='0'
export CGO_ENABLED='0'
MODE="$1"
@ -15,42 +14,25 @@ fi
CONFIG_FILE="config/config.yaml"
BRANCH="master"
BOT="All"
CRON="start"
if [ "$MODE" = "dev" ]; then
CONFIG_FILE="config/config_test.yaml"
BOT="zltx"
BRANCH="test"
CRON="close"
fi
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
#go mod tidy
#make build
go mod tidy
make build
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 "OLLAMA_BASE_URL=${OLLAMA_BASE_URL:-http://172.17.0.1: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}" ./bin/server --config "./${CONFIG_FILE}"
docker logs -f ${CONTAINER_NAME}

68
go.mod
View File

@ -1,41 +1,30 @@
module ai_scheduler
go 1.24.7
go 1.24.0
toolchain go1.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-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.20.0
github.com/go-kratos/kratos/v2 v2.9.1
github.com/gofiber/fiber/v2 v2.52.9
github.com/gofiber/websocket/v2 v2.2.1
github.com/google/uuid v1.6.0
github.com/google/wire v0.7.0
github.com/ollama/ollama v0.12.7
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
google.golang.org/protobuf v1.34.1
gopkg.in/yaml.v3 v3.0.1
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.0
xorm.io/builder v0.3.13
@ -50,83 +39,48 @@ require (
github.com/alibabacloud-go/tea-xml v1.1.3 // indirect
github.com/aliyun/credentials-go v1.4.6 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
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/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
github.com/eino-contrib/jsonschema v1.0.3 // indirect
github.com/eino-contrib/ollama v0.1.0 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/fasthttp/websocket v1.5.3 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/goph/emperror v0.17.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/meguminnnnnnnnn/go-openai v0.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nikolalohinski/gonja v1.5.3 // indirect
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
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
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.36.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.31.0 // indirect
golang.org/x/text v0.23.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
gopkg.in/yaml.v3 v3.0.1 // indirect
)

163
go.sum
View File

@ -38,15 +38,12 @@ 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=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
@ -92,8 +89,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=
@ -101,29 +96,12 @@ github.com/aliyun/credentials-go v1.4.6 h1:CG8rc/nxCNKfXbZWpWDzI9GjF4Tuu3Es14qT8
github.com/aliyun/credentials-go v1.4.6/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/mockey v1.2.14 h1:KZaFgPdiUwW+jOWFieo3Lr7INM1P+6adO3hxZhDswY8=
github.com/bytedance/mockey v1.2.14/go.mod h1:1BPHF9sol5R1ud/+0VEHGQq/+i2lN+GTsr3O2Q9IENY=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
@ -132,21 +110,9 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E=
github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/eino v0.7.7 h1:WhP0SMWWPgLdOH03HrKUxtP9/Q96NhziMZNEQl9lxpU=
github.com/cloudwego/eino v0.7.7/go.mod h1:nA8Vacmuqv3pqKBQbTWENBLQ8MmGmPt/WqiyLeB8ohQ=
github.com/cloudwego/eino-ext/components/model/ollama v0.1.6 h1:ZbrhV91uE0hGIOYXhb2i3G6tQJ/rK2SLYtoYrmocZXM=
github.com/cloudwego/eino-ext/components/model/ollama v0.1.6/go.mod h1:GDXrvorGdRNV6g2mK5jdla2D8Xc/hh7XDrTeGDteLLo=
github.com/cloudwego/eino-ext/components/model/openai v0.1.5 h1:+yvGbTPw93li9GSmdm6Rix88Yy8AXg5NNBcRbWx3CQU=
github.com/cloudwego/eino-ext/components/model/openai v0.1.5/go.mod h1:IPVYMFoZcuHeVEsDTGN6SZjvue0xr1iZFhdpq1SBWdQ=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2 h1:r9Id2wzJ05PoHl+Km7jQgNMgciaZI93TVnUYso89esM=
github.com/cloudwego/eino-ext/libs/acl/openai v0.1.2/go.mod h1:S4OkvglPY9hsm9tXeShODrf/WN1Cgu4bqu4nn/CnIic=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20 h1:m6P88V9lLrxZsE7uj9otq7l7nqDuCSAJ86KhzRlWf0M=
github.com/coze-dev/coze-go v0.0.0-20251029161603-312b7fd62d20/go.mod h1:wdT5CFt/sFsWz9hna2Z7DWzUra9spx0SoX1PUZyoSB0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@ -155,12 +121,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0=
github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4=
github.com/eino-contrib/ollama v0.1.0 h1:z1NaMdKW6X1ftP8g5xGGR5zDRPUtuTKFq35vBQgxsN4=
github.com/eino-contrib/ollama v0.1.0/go.mod h1:mYsQ7b3DeqY8bHPuD3MZJYTqkgyL6LoemxoP/B7ZNhA=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@ -169,8 +129,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/faabiosr/cachego v0.15.0/go.mod h1:L2EomlU3/rUWjzFavY9Fwm8B4zZmX2X6u8kTMkETrwI=
github.com/faabiosr/cachego v0.26.0 h1:EDDv2y9T0XJ4Cx3tUhbKSUayGWxCGkkZUivNLceHRWY=
github.com/faabiosr/cachego v0.26.0/go.mod h1:p54WXVzeB1CctH1ix/rjqv1EotNzD0Xoxk2IsR1PQX8=
@ -184,34 +142,18 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI=
github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98=
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-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=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w=
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -273,14 +215,8 @@ github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18=
github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/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.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=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -288,26 +224,19 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -316,12 +245,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -330,10 +255,6 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/meguminnnnnnnnn/go-openai v0.1.0 h1:BGzB1PlS2Epq0mBB2TGLwzMihbR7BANrlMH3w4ZnY88=
github.com/meguminnnnnnnnn/go-openai v0.1.0/go.mod h1:qs96ysDmxhE4BZoU45I43zcyfnaYxU3X+aRzLko/htY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -344,22 +265,16 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c=
github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/ollama/ollama v0.12.7 h1:dxokli1UyO/a0Aun5sE4+0Gg+A9oMUAPiFQhxrXOIXA=
github.com/ollama/ollama v0.12.7/go.mod h1:9+1//yWPsDE2u+l1a5mpaKrYw4VdnSsRU3ioq5BvMms=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
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/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=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pkoukk/tiktoken-go v0.1.6 h1:JF0TlJzhTbrI30wCvFuiw6FzP2+/bR+FIxUdgEAcUsw=
@ -370,19 +285,11 @@ 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=
github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ=
github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ=
github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
@ -390,20 +297,9 @@ 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=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI=
github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
@ -415,52 +311,33 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI=
github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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=
github.com/tmc/langchaingo v0.1.13 h1:rcpMWBIi2y3B90XxfE4Ao8dhCQPVDMaNPnN5cGB1CaA=
github.com/tmc/langchaingo v0.1.13/go.mod h1:vpQ5NOIhpzxDfTZK9B6tf2GM/MoaHewPWM5KXXGh7hg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/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=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -476,13 +353,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -498,8 +370,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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
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 +386,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 +452,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,10 +475,9 @@ 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.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=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -650,7 +519,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -661,8 +529,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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.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 +539,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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
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 +553,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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
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=
@ -855,7 +721,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/redis.v4 v4.2.4/go.mod h1:8KREHdypkCEojGKQcjMqAODMICIVwZAONWq8RowTITA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,117 +1,41 @@
package biz
import (
errors "ai_scheduler/internal/data/error"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/util"
"context"
"encoding/json"
"xorm.io/builder"
)
type ChatHistoryBiz struct {
chatHiRepo *impl.ChatHisImpl
taskRepo *impl.TaskImpl
chatRepo *impl.ChatImpl
}
func NewChatHistoryBiz(chatHiRepo *impl.ChatHisImpl, taskRepo *impl.TaskImpl) *ChatHistoryBiz {
func NewChatHistoryBiz(chatRepo *impl.ChatImpl) *ChatHistoryBiz {
s := &ChatHistoryBiz{
chatHiRepo: chatHiRepo,
taskRepo: taskRepo,
chatRepo: chatRepo,
}
//go s.AsyncProcess(context.Background())
return s
}
// 查询会话历史
func (s *ChatHistoryBiz) List(ctx context.Context, query *entitys.ChatHistQuery) ([]entitys.ChatHisQueryResponse, error) {
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"),
}
if query.HisID > 0 {
con = append(con, s.chatHiRepo.WithHisId(query.HisID))
}
chats, err := s.chatHiRepo.FindAll(
con...,
)
if err != nil {
return nil, err
}
taskIds := make([]int32, 0, len(chats))
for _, chat := range chats {
// 去重任务ID
if !util.Contains(taskIds, chat.TaskID) {
taskIds = append(taskIds, chat.TaskID)
}
}
// 查询任务名称
tasks, err := s.taskRepo.FindAll(s.taskRepo.In("task_id", taskIds))
if err != nil {
return nil, err
}
taskMap := make(map[int32]model.AiTask)
for _, task := range tasks {
taskMap[task.TaskID] = task
}
// 构建结果
result := make([]entitys.ChatHisQueryResponse, 0, len(chats))
for _, chat := range chats {
item := entitys.ChatHisQueryResponse{}
item.FromModel(chat, taskMap[chat.TaskID])
result = append(result, item)
}
return result, nil
}
//func (s *ChatHistoryBiz) create(ctx context.Context, sessionID, role, content string) error {
// chat := model.AiChatHi{
// SessionID: sessionID,
// Role: role,
// Content: content,
// }
//
// return s.chatRepo.Create(&chat)
//}
//
//// 添加会话历史
//func (s *ChatHistoryBiz) Create(ctx context.Context, chat entitys.ChatHistory) error {
// return s.chatHiRepo.Create(&model.AiChatHi{
// SessionID: chat.SessionID,
// Ques: chat.Role.String(),
// Ans: chat.Content,
// })
// return s.create(ctx, chat.SessionID, chat.Role.String(), chat.Content)
//}
// 更新会话历史内容, 追加内容, 不覆盖原有内容, content 使用json格式存储
func (c *ChatHistoryBiz) UpdateContent(ctx context.Context, chat *entitys.UpdateContentRequest) error {
var contents []string
chatHi, has, err := c.chatHiRepo.FindOne(c.chatHiRepo.WithHisId(chat.HisID))
if err != nil {
return err
} else if !has {
return errors.NewBusinessErr(errors.InvalidParamCode, "chat history not found")
}
if "" != chatHi.Content {
// 解析历史内容
err = json.Unmarshal([]byte(chatHi.Content), &contents)
if err != nil {
return err
}
}
contents = append(contents, chat.Content)
b, err := json.Marshal(contents)
if err != nil {
return err
}
chatHi.Content = string(b)
return c.chatHiRepo.Update(&chatHi,
c.chatHiRepo.Select("content"),
c.chatHiRepo.WithHisId(chatHi.HisID))
}
// 异步添加会话历史
//func (s *ChatHistoryBiz) AsyncCreate(ctx context.Context, chat entitys.ChatHistory) {
// s.chatRepo.AsyncCreate(ctx, model.AiChatHi{
@ -129,5 +53,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.chatRepo.UpdateByCond(&cond, &model.AiChatHi{HisID: chat.HisId, Useful: chat.Useful})
}

View File

@ -1,445 +0,0 @@
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/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"
"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
}
// NewDingTalkBotBiz
func NewDingTalkBotBiz(
do *do.Do,
handle *do.Handle,
botConfigImpl *impl.BotConfigImpl,
botGroupImpl *impl.BotGroupImpl,
dingTalkUser *dingtalk.User,
chatHis *impl.BotChatHisImpl,
reportDailyCacheImpl *impl.ReportDailyCacheImpl,
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,
}
}
func (d *DingTalkBotBiz) GetDingTalkBotCfgList() (dingBotList []entitys.DingTalkBot, err error) {
botConfig := make([]model.AiBotConfig, 0)
cond := builder.NewCond()
cond = cond.And(builder.Eq{"status": constants.Enable})
cond = cond.And(builder.Eq{"bot_type": constants.BotTypeDingTalk})
err = d.botConfigImpl.GetRangeToMapStruct(&cond, &botConfig)
for _, v := range botConfig {
var config entitys.DingTalkBot
err = json.Unmarshal([]byte(v.BotConfig), &config)
if err != nil {
d.log.Info("初始化“%s”失败:%s", v.BotName, err.Error())
}
config.BotIndex = v.RobotCode
dingBotList = append(dingBotList, config)
}
return
}
func (d *DingTalkBotBiz) InitRequire(ctx context.Context, data *chatbot.BotCallbackDataModel) (requireData *entitys.RequireDataDingTalkBot, err error) {
requireData = &entitys.RequireDataDingTalkBot{
Req: data,
Ch: make(chan entitys.Response, 2),
}
return
}
func (d *DingTalkBotBiz) Do(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
//entitys.ResLoading(requireData.Ch, "", "收到消息,正在处理中,请稍等")
//defer close(requireData.Ch)
switch constants.ConversationType(requireData.Req.ConversationType) {
case constants.ConversationTypeSingle:
err = d.handleSingleChat(ctx, requireData)
case constants.ConversationTypeGroup:
err = d.handleGroupChat(ctx, requireData)
default:
err = errors.New("未知的聊天类型:" + requireData.Req.ConversationType)
}
if err != nil {
entitys.ResText(requireData.Ch, "", err.Error())
}
return
}
func (d *DingTalkBotBiz) handleSingleChat(ctx context.Context, requireData *entitys.RequireDataDingTalkBot) (err error) {
entitys.ResLog(requireData.Ch, "", "个人聊天暂未开启,请期待后续更新")
return
//requireData.UserInfo, err = d.dingTalkUser.GetUserInfoFromBot(ctx, requireData.Req.SenderStaffId, dingtalk.WithId(1))
//if err != nil {
// return
//}
//requireData.ID=requireData.UserInfo.UserID
////如果不是管理或者不是老板,则进行权限判断
//if requireData.UserInfo.IsSenior == constants.IsSeniorFalse && requireData.UserInfo.IsBoss == constants.IsBossFalse {
//
//}
//return
}
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)
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)
if err != nil {
return
}
rec, err := d.recognize(ctx, requireData, groupTools)
if err != nil {
return
}
return d.groupConfigBiz.handleMatch(ctx, rec, groupConfig)
}
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)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return
}
}
if group.GroupID == 0 {
group = &model.AiBotGroup{
ConversationID: conversationId,
Title: conversationTitle,
RobotCode: robotCode,
}
//如果不存在则创建
_, err = d.botGroupImpl.Add(group)
}
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
}
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 {
taskConfig := entitys.TaskConfigDetail{}
if err = json.Unmarshal([]byte(task.Config), &taskConfig); err != nil {
log.Errorf("解析任务配置失败: %s, 任务ID: %s", err.Error(), task.Index)
continue // 解析失败时跳过该任务,而不是直接返回错误
}
rec.Tasks = append(rec.Tasks, entitys.RegistrationTask{
Name: task.Index,
Desc: task.TempPrompt,
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:
content = &entitys.RecognizeUserContent{
Text: msgContent.(string),
}
default:
return nil, errors.New("未知的消息类型:" + msgType)
}
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) 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("![图片](%s)", 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
}
// SendReports 发送多个报告
func (d *DingTalkBotBiz) SendReports(ctx context.Context, groupInfo *model.AiBotGroup, reports []*bbxt.ReportRes) (err error) {
if len(reports) == 0 {
return errors.New("report is empty")
}
title := fmt.Sprintf("截止%s日报", time.Now().Format("1月2日15点"))
reportChan := make(chan string, len(reports)*2)
writeCount := 0
for _, v := range reports {
if v == nil {
continue
}
reportChan <- fmt.Sprintf("**%s**", v.Title)
reportChan <- fmt.Sprintf("![图片](%s)", v.Url)
writeCount += 2
}
close(reportChan)
if writeCount == 0 {
return errors.New("report is empty")
}
err = d.HandleStreamRes(ctx, &chatbot.BotCallbackDataModel{
RobotCode: groupInfo.RobotCode,
ConversationType: constants.ConversationTypeGroup,
ConversationId: groupInfo.ConversationID,
Text: chatbot.BotCallbackDataTextModel{
Content: title,
},
}, reportChan)
return
}
func (d *DingTalkBotBiz) GetGroupInfo(ctx context.Context, groupId int) (group model.AiBotGroup, err error) {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"group_id": groupId})
cond = cond.And(builder.Eq{"status": constants.Enable})
err = d.botGroupImpl.GetOneBySearchToStrut(&cond, &group)
return
}
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)
}
return d.replier.SimpleReplyText(ctx, SessionWebhook, []byte(msg))
}
func (d *DingTalkBotBiz) replyImg(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) replyFile(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) replyMarkdown(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) replyActionCard(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) 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 + `所有的时间识别精确到秒`
}

View File

@ -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"
@ -20,6 +19,8 @@ import (
"gitea.cdlsxd.cn/self-tools/l_request"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/websocket/v2"
"xorm.io/builder"
)
@ -28,23 +29,21 @@ type Do struct {
sessionImpl *impl.SessionImpl
sysImpl *impl.SysImpl
taskImpl *impl.TaskImpl
hisImpl *impl.ChatHisImpl
hisImpl *impl.ChatImpl
conf *config.Config
}
func NewDo(
sessionImpl *impl.SessionImpl,
sysImpl *impl.SysImpl,
taskImpl *impl.TaskImpl,
hisImpl *impl.ChatHisImpl,
hisImpl *impl.ChatImpl,
conf *config.Config,
) *Do {
return &Do{
conf: conf,
sessionImpl: sessionImpl,
sysImpl: sysImpl,
hisImpl: hisImpl,
taskImpl: taskImpl,
conf: conf,
sysImpl: sysImpl,
hisImpl: hisImpl,
taskImpl: taskImpl,
}
}
@ -82,20 +81,6 @@ func (d *Do) DataAuth(ctx context.Context, client *gateway.Client, requireData *
return nil
}
func (d *Do) DataAuthForBot(ctx context.Context, client *gateway.Client, requireData *entitys.RequireData) (err error) {
// 2. 加载系统信息
if err = d.loadSystemInfo(ctx, client, requireData); err != nil {
return fmt.Errorf("获取系统信息失败: %w", err)
}
// 3. 加载任务列表
if err = d.loadTaskList(ctx, client, requireData); err != nil {
return fmt.Errorf("获取任务列表失败: %w", err)
}
return nil
}
// 提取数据验证为单独函数
func (d *Do) validateClientData(client *gateway.Client, requireData *entitys.RequireData) error {
requireData.Session = client.GetSession()
@ -119,7 +104,7 @@ func (d *Do) validateClientData(client *gateway.Client, requireData *entitys.Req
// 获取系统信息的辅助函数
func (d *Do) loadSystemInfo(ctx context.Context, client *gateway.Client, requireData *entitys.RequireData) error {
if sysInfo := client.GetSysInfo(); sysInfo == nil {
sys, err := d.GetSysInfo(requireData)
sys, err := d.getSysInfo(requireData)
if err != nil {
return err
}
@ -134,8 +119,7 @@ func (d *Do) loadSystemInfo(ctx context.Context, client *gateway.Client, require
// 获取任务列表的辅助函数
func (d *Do) loadTaskList(ctx context.Context, client *gateway.Client, requireData *entitys.RequireData) error {
if taskInfo := client.GetTasks(); len(taskInfo) == 0 {
// 从数据库获取任务列表, 0 表示获取公共的任务
tasks, err := d.GetTasks(requireData.Sys.SysID, 0)
tasks, err := d.getTasks(requireData.Sys.SysID)
if err != nil {
return err
}
@ -144,7 +128,6 @@ func (d *Do) loadTaskList(ctx context.Context, client *gateway.Client, requireDa
} else {
requireData.Tasks = taskInfo
}
return nil
}
@ -158,10 +141,10 @@ func (d *Do) loadChatHistory(ctx context.Context, requireData *entitys.RequireDa
return nil
}
func (d *Do) MakeCh(client *gateway.Client, requireData *entitys.RequireData) (ctx context.Context, deferFunc func()) {
func (d *Do) MakeCh(c *websocket.Conn, requireData *entitys.RequireData) (ctx context.Context, deferFunc func()) {
requireData.Ch = make(chan entitys.Response)
ctx, cancel := context.WithCancel(context.Background())
done := d.startMessageHandler(ctx, client, requireData)
done := d.startMessageHandler(ctx, c, requireData)
return ctx, func() {
close(requireData.Ch) //关闭主通道
<-done // 等待消息处理完成
@ -219,7 +202,7 @@ func (d *Do) getRequireData() (err error) {
return
}
func (d *Do) GetSysInfo(requireData *entitys.RequireData) (sysInfo model.AiSy, err error) {
func (d *Do) getSysInfo(requireData *entitys.RequireData) (sysInfo model.AiSy, err error) {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"app_key": requireData.Key})
cond = cond.And(builder.IsNull{"delete_at"})
@ -238,12 +221,12 @@ func (d *Do) getSessionChatHis(requireData *entitys.RequireData) (his []model.Ai
return
}
func (d *Do) GetTasks(sysId ...int32) (tasks []model.AiTask, err error) {
func (d *Do) getTasks(sysId int32) (tasks []model.AiTask, err error) {
cond := builder.NewCond()
//cond = cond.And(builder.Eq{"sys_id": sysId})
cond = cond.And(builder.Eq{"sys_id": sysId})
cond = cond.And(builder.IsNull{"delete_at"})
cond = cond.And(builder.Eq{"status": 1}.And(builder.In("sys_id", sysId)))
cond = cond.And(builder.Eq{"status": 1})
_, err = d.taskImpl.GetListToStruct(&cond, nil, &tasks, "")
return
@ -252,11 +235,11 @@ func (d *Do) GetTasks(sysId ...int32) (tasks []model.AiTask, err error) {
// startMessageHandler 启动独立的消息处理协程
func (d *Do) startMessageHandler(
ctx context.Context,
client *gateway.Client,
c *websocket.Conn,
requireData *entitys.RequireData,
) <-chan struct{} {
done := make(chan struct{})
var chat []entitys.Response
var chat []string
go func() {
defer func() {
@ -266,74 +249,31 @@ 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,
}
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{
_ = entitys.MsgSend(c, entitys.Response{
Content: pkg.JsonStringIgonErr(hisLog),
Type: entitys.ResponseEnd,
})
}()
streamText := ""
streamIndex := ""
for v := range requireData.Ch { // 自动检测通道关闭
if err := sendWithTimeout(client, v, 10*time.Second); err != nil {
if err := sendWithTimeout(c, v, 2*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 = ""
}
}()
@ -341,7 +281,7 @@ func (d *Do) startMessageHandler(
}
// 辅助函数:带超时的 WebSocket 发送
func sendWithTimeout(client *gateway.Client, data entitys.Response, timeout time.Duration) error {
func sendWithTimeout(c *websocket.Conn, data entitys.Response, timeout time.Duration) error {
sendCtx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
@ -354,7 +294,7 @@ func sendWithTimeout(client *gateway.Client, data entitys.Response, timeout time
close(done)
}()
// 如果 MsgSend 阻塞,这里会卡住
err := entitys.MsgSend(client, data)
err := entitys.MsgSend(c, data)
done <- err
}()
@ -394,7 +334,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 +353,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
}

View File

@ -4,94 +4,64 @@ 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"
"ai_scheduler/internal/domain/workflow/runtime"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/gateway"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/dingtalk"
"ai_scheduler/internal/pkg/l_request"
"ai_scheduler/internal/pkg/mapstructure"
"ai_scheduler/internal/pkg/rec_extra"
"ai_scheduler/internal/pkg/util"
"ai_scheduler/internal/tools"
"ai_scheduler/internal/tools/public"
errorsSpecial "errors"
"io"
"net/http"
"time"
"ai_scheduler/internal/tools_bot"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/coze-dev/coze-go"
"github.com/gofiber/fiber/v2/log"
"gorm.io/gorm/utils"
"strings"
)
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
Bot *tools_bot.BotTool
conf *config.Config
sessionImpl *impl.SessionImpl
}
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,
dTalkBot *tools_bot.BotTool,
) *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,
Bot: dTalkBot,
}
}
func (r *Handle) Recognize(ctx context.Context, rec *entitys.Recognize, promptProcessor PromptOption) (err error) {
entitys.ResLog(rec.Ch, "recognize_start", "准备意图识别")
func (r *Handle) Recognize(ctx context.Context, requireData *entitys.RequireData) (err error) {
entitys.ResLog(requireData.Ch, "recognize_start", "准备意图识别")
prompt, err := promptProcessor.CreatePrompt(ctx, rec)
//意图识别
// recognizeMsg, err := r.Ollama.IntentRecognize(ctx, &entitys.ToolSelect{
recognizeMsg, err := r.Vllm.IntentRecognize(ctx, &entitys.ToolSelect{
Prompt: prompt,
Tools: rec.Tasks,
})
recognizeMsg, err := r.Ollama.IntentRecognize(ctx, requireData)
if err != nil {
return
}
entitys.ResLog(rec.Ch, "recognize", recognizeMsg)
entitys.ResLog(rec.Ch, "recognize_end", "意图识别结束")
entitys.ResLog(requireData.Ch, "recognize", recognizeMsg)
entitys.ResLog(requireData.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
requireData.Match = &match
return
}
@ -100,68 +70,76 @@ func (r *Handle) handleOtherTask(ctx context.Context, requireData *entitys.Requi
return
}
func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, rec *entitys.Recognize, requireData *entitys.RequireData) (err error) {
func (r *Handle) HandleMatch(ctx context.Context, client *gateway.Client, requireData *entitys.RequireData) (err error) {
if !rec.Match.IsMatch {
if len(rec.Match.Chat) != 0 {
entitys.ResText(rec.Ch, "", rec.Match.Chat)
if !requireData.Match.IsMatch {
if len(requireData.Match.Chat) != 0 {
entitys.ResText(requireData.Ch, "", requireData.Match.Chat)
} else {
entitys.ResText(rec.Ch, "", rec.Match.Reasoning)
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
}
return
}
var pointTask *model.AiTask
for _, task := range requireData.Tasks {
if task.Index == rec.Match.Index {
if task.Index == requireData.Match.Index {
pointTask = &task
break
}
}
if pointTask == nil || pointTask.Index == "other" {
return r.OtherTask(ctx, rec)
return r.OtherTask(ctx, requireData)
}
// 校验用户权限
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)
return r.handleApiTask(ctx, requireData, pointTask)
case constants.TaskTypeFunc:
return r.handleTask(ctx, rec, pointTask)
return r.handleTask(ctx, requireData, pointTask)
case constants.TaskTypeKnowle:
return r.handleKnowle(ctx, requireData, pointTask)
case constants.TaskTypeBot:
return r.HandleBot(ctx, rec, &entitys.Task{
Index: pointTask.Index,
})
case constants.TaskTypeEinoWorkflow:
return r.handleEinoWorkflow(ctx, rec, pointTask)
case constants.TaskTypeCozeWorkflow:
return r.handleCozeWorkflow(ctx, rec, pointTask)
return r.handleBot(ctx, requireData, pointTask)
default:
return r.handleOtherTask(ctx, requireData)
}
}
func (r *Handle) OtherTask(ctx context.Context, requireData *entitys.Recognize) (err error) {
func (r *Handle) OtherTask(ctx context.Context, requireData *entitys.RequireData) (err error) {
entitys.ResText(requireData.Ch, "", requireData.Match.Reasoning)
return
}
func (r *Handle) handleTask(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
func (r *Handle) handleBot(ctx context.Context, requireData *entitys.RequireData, task *model.AiTask) (err error) {
var configData entitys.ConfigDataTool
err = json.Unmarshal([]byte(task.Config), &configData)
if err != nil {
return
}
err = r.Bot.Execute(ctx, configData.Tool, requireData)
if err != nil {
return
}
return
}
func (r *Handle) handleTask(ctx context.Context, requireData *entitys.RequireData, task *model.AiTask) (err error) {
var configData entitys.ConfigDataTool
err = json.Unmarshal([]byte(task.Config), &configData)
if err != nil {
return
}
err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
err = r.toolManager.ExecuteTool(ctx, configData.Tool, requireData)
if err != nil {
return
}
@ -170,7 +148,7 @@ func (r *Handle) handleTask(ctx context.Context, rec *entitys.Recognize, task *m
}
// 知识库
func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
func (r *Handle) handleKnowle(ctx context.Context, requireData *entitys.RequireData, task *model.AiTask) (err error) {
var (
configData entitys.ConfigDataTool
@ -182,16 +160,13 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
if err != nil {
return
}
ext, err := rec_extra.GetTaskRecExt(rec)
if err != nil {
return
}
// 通过session 找到知识库session
var has bool
if len(ext.Session) == 0 {
if len(requireData.Session) == 0 {
return errors.SessionNotFound
}
ext.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(ext.Session))
requireData.SessionInfo, has, err = r.sessionImpl.FindOne(r.sessionImpl.WithSessionId(requireData.Session))
if err != nil {
return
} else if !has {
@ -205,7 +180,7 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
return fmt.Errorf("tool not found: %s", configData.Tool)
}
if knowledgeTool, ok := tool.(*public.KnowledgeBaseTool); !ok {
if knowledgeTool, ok := tool.(*tools.KnowledgeBaseTool); !ok {
return fmt.Errorf("未找到知识库Tool: %s", configData.Tool)
} else {
host = knowledgeTool.GetConfig().BaseURL
@ -214,15 +189,15 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
}
// 知识库的session为空请求知识库获取, 并绑定
if ext.SessionInfo.KnowlegeSessionID == "" {
if requireData.SessionInfo.KnowlegeSessionID == "" {
// 请求知识库
if sessionIdKnowledge, err = public.GetKnowledgeBaseSession(host, ext.Sys.KnowlegeBaseID, ext.Sys.KnowlegeTenantKey); err != nil {
if sessionIdKnowledge, err = tools.GetKnowledgeBaseSession(host, requireData.Sys.KnowlegeBaseID, requireData.Sys.KnowlegeTenantKey); err != nil {
return
}
// 绑定知识库session下次可以使用
ext.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
if err = r.sessionImpl.Update(&ext.SessionInfo, r.sessionImpl.WithSessionId(ext.SessionInfo.SessionID)); err != nil {
requireData.SessionInfo.KnowlegeSessionID = sessionIdKnowledge
if err = r.sessionImpl.Update(&requireData.SessionInfo, r.sessionImpl.WithSessionId(requireData.SessionInfo.SessionID)); err != nil {
return
}
}
@ -230,21 +205,21 @@ func (r *Handle) handleKnowle(ctx context.Context, rec *entitys.Recognize, task
// 用户输入解析
var ok bool
input := make(map[string]string)
if err = json.Unmarshal([]byte(rec.Match.Parameters), &input); err != nil {
if err = json.Unmarshal([]byte(requireData.Match.Parameters), &input); err != nil {
return
}
if query, ok = input["query"]; !ok {
return fmt.Errorf("query不能为空")
}
ext.KnowledgeConf = entitys.KnowledgeBaseRequest{
Session: ext.SessionInfo.KnowlegeSessionID,
ApiKey: ext.Sys.KnowlegeTenantKey,
requireData.KnowledgeConf = entitys.KnowledgeBaseRequest{
Session: requireData.SessionInfo.KnowlegeSessionID,
ApiKey: requireData.Sys.KnowlegeTenantKey,
Query: query,
}
rec.Ext = pkg.JsonByteIgonErr(ext)
// 执行工具
err = r.toolManager.ExecuteTool(ctx, configData.Tool, rec)
err = r.toolManager.ExecuteTool(ctx, configData.Tool, requireData)
if err != nil {
return
}
@ -252,139 +227,18 @@ 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) {
func (r *Handle) handleApiTask(ctx context.Context, requireData *entitys.RequireData, task *model.AiTask) (err error) {
var (
request l_request.Request
requestParam map[string]interface{}
)
ext, err := rec_extra.GetTaskRecExt(rec)
err = json.Unmarshal([]byte(requireData.Match.Parameters), &requestParam)
if err != nil {
return
}
err = json.Unmarshal([]byte(rec.Match.Parameters), &requestParam)
if err != nil {
return
}
// request.Url = strings.ReplaceAll(task.Config, "${authorization}", requireData.Auth)
task.Config = strings.ReplaceAll(task.Config, "${authorization}", ext.Auth)
request.Url = strings.ReplaceAll(task.Config, "${authorization}", requireData.Auth)
for k, v := range requestParam {
if vStr, ok := v.(string); ok {
task.Config = strings.ReplaceAll(task.Config, "${"+k+"}", vStr)
} else {
var jsonStr []byte
jsonStr, err = json.Marshal(v)
if err != nil {
return errors.NewBusinessErr(422, "请求参数解析失败")
}
task.Config = strings.ReplaceAll(task.Config, "\"${"+k+"}\"", string(jsonStr))
}
task.Config = strings.ReplaceAll(task.Config, "${"+k+"}", fmt.Sprintf("%v", v))
}
var configData entitys.ConfigDataHttp
err = json.Unmarshal([]byte(task.Config), &configData)
@ -399,139 +253,15 @@ func (r *Handle) handleApiTask(ctx context.Context, rec *entitys.Recognize, task
err = errors.NewBusinessErr(422, "api地址获取失败")
return
}
entitys.ResLoading(rec.Ch, task.Index, "正在请求数据")
res, err := request.Send()
if err != nil {
return
}
entitys.ResJson(rec.Ch, task.Index, res.Text)
entitys.ResJson(requireData.Ch, "", pkg.JsonStringIgonErr(res.Text))
return
}
// eino 工作流
func (r *Handle) handleEinoWorkflow(ctx context.Context, rec *entitys.Recognize, task *model.AiTask) (err error) {
// token 写入ctx
ext, err := rec_extra.GetTaskRecExt(rec)
if err != nil {
return
}
ctx = util.SetTokenToContext(ctx, ext.Auth)
entitys.ResLoading(rec.Ch, task.Index, "正在执行工作流")
// 工作流内部输出
workflowId := task.Index
_, err = r.workflowManager.Invoke(ctx, workflowId, &runtime.WorkflowArgs{Recognize: rec})
if err != nil {
return err
}
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) {
// 授权检查权限

View File

@ -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
}
}

View File

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

View File

@ -1,175 +0,0 @@
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"
)
type PromptOption interface {
CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes []api.Message, err error)
}
type WithSys struct {
Config *config.Config
}
func (f *WithSys) CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes []api.Message, err error) {
var (
prompt = make([]api.Message, 0) // 初始化一个空的api.Message切片
)
// 获取用户内容,如果出错则直接返回错误
content, err := f.getUserContent(ctx, rec)
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), // 用户历史输入
}, 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 {
hasFile = true
}
content.WriteString(rec.UserContent.Text)
if hasFile {
content.WriteString("\n")
}
if len(rec.UserContent.Tag) > 0 {
content.WriteString("\n")
content.WriteString("### 工具必须使用:")
content.WriteString(rec.UserContent.Tag)
}
// 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)
}
}
}
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 {
}
func (f *WithDingTalkBot) CreatePrompt(ctx context.Context, rec *entitys.Recognize) (mes []api.Message, err error) {
var (
prompt = make([]api.Message, 0) // 初始化一个空的api.Message切片
)
// 获取用户内容,如果出错则直接返回错误
content, err := f.getUserContent(ctx, rec)
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: "user", // 用户角色
Content: content.String(), // 用户输入内容
})
return
}
func (f *WithDingTalkBot) getUserContent(ctx context.Context, rec *entitys.Recognize) (content strings.Builder, err error) {
var hasFile bool
if rec.UserContent.File != nil && len(rec.UserContent.File) > 0 {
hasFile = true
}
content.WriteString(rec.UserContent.Text)
if hasFile {
content.WriteString("\n")
}
if len(rec.UserContent.Tag) > 0 {
content.WriteString("\n")
content.WriteString("### 工具必须使用:")
content.WriteString(rec.UserContent.Tag)
}
if len(rec.ChatHis.Messages) > 0 {
content.WriteString("\n")
content.WriteString("### 引用历史聊天记录:\n")
content.WriteString(pkg.JsonStringIgonErr(rec.ChatHis))
}
return
}

View File

@ -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![图片](%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
}

View File

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

View File

@ -1,159 +0,0 @@
package dingtalk
import (
"ai_scheduler/internal/config"
"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/pkg/l_request"
"ai_scheduler/utils"
"context"
"encoding/json"
"errors"
"net/http"
"time"
"github.com/gofiber/fiber/v2/log"
"github.com/redis/go-redis/v9"
"xorm.io/builder"
)
type Auth struct {
redis *redis.Client
cfg *config.Config
botConfigImpl *impl.BotConfigImpl
}
func NewAuth(cfg *config.Config, redis *utils.Rdb, botConfigImpl *impl.BotConfigImpl) *Auth {
return &Auth{
redis: redis.Rdb,
cfg: cfg,
botConfigImpl: botConfigImpl,
}
}
func (a *Auth) GetAccessToken(ctx context.Context, clientId string, clientSecret string) (authInfo *AuthInfo, err error) {
if clientId == "" {
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()
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
}
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")
return
}
req := l_request.Request{
Method: http.MethodPost,
Url: "https://api.dingtalk.com/v1.0/oauth2/accessToken",
Json: map[string]interface{}{
"appKey": clientId,
"appSecret": clientSecret,
},
}
res, err := req.Send()
if err != nil {
return
}
err = json.Unmarshal(res.Content, &auth)
return
}
func (a *Auth) GetTokenFromBotOption(ctx context.Context, botOption ...BotOption) (token *AuthInfo, err error) {
botInfo := &Bot{}
for _, option := range botOption {
option(botInfo)
}
if botInfo.Id == 0 && botInfo.BotConfig == nil && botInfo.BotCode == "" {
err = errors.New("botInfo is nil")
return
}
if botInfo.BotConfig == nil {
err = a.GetBotConfigFromModel(botInfo)
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())
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
}
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)
if err != nil {
return
}
if botConfigDo.BotID == 0 {
err = errors.New("未找到机器人服务配置")
return
}
botInfo.BotConfig = &botConfigDo
return nil
}

View File

@ -1,108 +0,0 @@
package dingtalk
import (
"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/pkg/l_request"
"context"
"encoding/json"
"fmt"
"net/http"
"xorm.io/builder"
)
type Dept struct {
dingDeptImpl *impl.BotDeptImpl
auth *Auth
}
func NewDept(dingDeptImpl *impl.BotDeptImpl, auth *Auth) *Dept {
return &Dept{
dingDeptImpl: dingDeptImpl,
auth: auth,
}
}
func (d *Dept) GetDeptInfoByDeptIds(ctx context.Context, deptIds []int, authInfo *AuthInfo) (depts []*entitys.Dept, err error) {
if len(deptIds) == 0 || authInfo == nil {
return
}
var deptsInfo []model.AiBotDept
cond := builder.NewCond()
cond = cond.And(builder.Eq{"dingtalk_dept_id": deptIds})
err = d.dingDeptImpl.GetRangeToMapStruct(&cond, &deptsInfo)
if err != nil {
return
}
var existDept = make([]int, len(deptsInfo), 0)
for _, dept := range deptsInfo {
depts = append(depts, &entitys.Dept{
DeptId: int(dept.DeptID),
Name: dept.Name,
ToolList: dept.ToolList,
})
existDept = append(existDept, int(dept.DeptID))
}
diff := pkg.Difference(deptIds, existDept)
if len(diff) > 0 {
deptDo := make([]model.AiBotDept, 0)
for _, deptId := range diff {
deptInfo, _err := d.GetDeptInfoFromDingTalk(ctx, deptId, authInfo.AccessToken)
if _err != nil {
return nil, _err
}
depts = append(depts, &entitys.Dept{
DeptId: deptInfo.DeptId,
Name: deptInfo.Name,
})
deptDo = append(deptDo, model.AiBotDept{
DingtalkDeptID: int32(deptInfo.DeptId),
Name: deptInfo.Name,
})
}
if len(deptDo) > 0 {
_, err = d.dingDeptImpl.Add(deptDo)
if err != nil {
return nil, err
}
}
}
return
}
func (d *Dept) GetDeptInfoFromDingTalk(ctx context.Context, deptId int, token string) (depts DeptResResult, err error) {
if deptId == 0 || len(token) == 0 {
return
}
req := l_request.Request{
Method: http.MethodPost,
Url: constants.GetDingTalkRequestUrl(constants.RequestUrlGetDeptGet, map[string]string{
"access_token": token,
}),
Json: map[string]interface{}{
"dept_id": deptId,
},
}
res, _err := req.Send()
if _err != nil {
err = _err
return
}
var deptInfo DeptRes
err = json.Unmarshal(res.Content, &deptInfo)
if err != nil {
return
}
if deptInfo.Errcode != 0 {
fmt.Errorf("钉钉请求报错:%s", deptInfo.Errmsg)
}
return deptInfo.DeptResResult, err
}

View File

@ -1,36 +0,0 @@
package dingtalk
import "ai_scheduler/internal/data/model"
type Bot struct {
Id int
BotCode string
BotConfig *model.AiBotConfig
}
type BotOption func(*Bot)
func WithId(id int) BotOption {
return func(b *Bot) {
b.Id = id
}
}
func WithBotConfig(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
}
}

View File

@ -1,12 +0,0 @@
package dingtalk
import (
"github.com/google/wire"
)
var ProviderSetDingTalk = wire.NewSet(
NewUser,
NewAuth,
NewDept,
NewSendCardClient,
)

View File

@ -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
}

View File

@ -1,110 +0,0 @@
package dingtalk
import (
"ai_scheduler/internal/data/constants"
"time"
dingtalkim_1_0 "github.com/alibabacloud-go/dingtalk/im_1_0"
)
type DingTalkAuthIRes struct {
AccessToken string `json:"accessToken"`
ExpireIn int64 `json:"expireIn"`
}
type UserInfoRes struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
Result UserInfoResResult `json:"result"`
RequestId string `json:"request_id"`
}
type UserInfoResResult struct {
Active bool `json:"active"`
Admin bool `json:"admin"`
Avatar string `json:"avatar"`
Boss bool `json:"boss"`
CreateTime time.Time `json:"create_time"`
DeptIdList []int `json:"dept_id_list"`
DeptOrderList []struct {
DeptId int `json:"dept_id"`
Order int64 `json:"order"`
} `json:"dept_order_list"`
ExclusiveAccount bool `json:"exclusive_account"`
HideMobile bool `json:"hide_mobile"`
HiredDate int64 `json:"hired_date"`
JobNumber string `json:"job_number"`
LeaderInDept []struct {
DeptId int `json:"dept_id"`
Leader bool `json:"leader"`
} `json:"leader_in_dept"`
ManagerUserid string `json:"manager_userid"`
Name string `json:"name"`
RealAuthed bool `json:"real_authed"`
RoleList []struct {
GroupName string `json:"group_name"`
Id int `json:"id"`
Name string `json:"name"`
} `json:"role_list"`
Senior bool `json:"senior"`
Title string `json:"title"`
Unionid string `json:"unionid"`
Userid string `json:"userid"`
}
type DeptRes struct {
Errcode int `json:"errcode"`
Errmsg string `json:"errmsg"`
DeptResResult DeptResResult `json:"result"`
RequestId string `json:"request_id"`
}
type DeptResResult struct {
DeptPermits []int `json:"dept_permits"`
OuterPermitUsers []string `json:"outer_permit_users"`
DeptManagerUseridList []string `json:"dept_manager_userid_list"`
OrgDeptOwner string `json:"org_dept_owner"`
OuterDept bool `json:"outer_dept"`
DeptGroupChatId string `json:"dept_group_chat_id"`
GroupContainSubDept bool `json:"group_contain_sub_dept"`
AutoAddUser bool `json:"auto_add_user"`
HideDept bool `json:"hide_dept"`
Name string `json:"name"`
OuterPermitDepts []int `json:"outer_permit_depts"`
UserPermits []interface{} `json:"user_permits"`
DeptId int `json:"dept_id"`
CreateDeptGroup bool `json:"create_dept_group"`
Order int `json:"order"`
Code string `json:"code"`
UnionDeptExt struct {
CorpId string `json:"corp_id"`
DeptId int `json:"dept_id"`
} `json:"union_dept_ext"`
}
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
}

View File

@ -1,128 +0,0 @@
package dingtalk
import (
"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/pkg/l_request"
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
)
type User struct {
dingUserImpl *impl.BotUserImpl
botConfigImpl *impl.BotConfigImpl
auth *Auth
dept *Dept
}
func NewUser(
dingUserImpl *impl.BotUserImpl,
auth *Auth,
dept *Dept,
) *User {
return &User{
dingUserImpl: dingUserImpl,
auth: auth,
dept: dept,
}
}
func (u *User) GetUserInfoFromBot(ctx context.Context, staffId string, botOption ...BotOption) (userInfo *entitys.DingTalkUserInfo, err error) {
if len(staffId) == 0 {
return
}
user, err := u.dingUserImpl.GetByStaffId(staffId)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return
}
}
//待优化
authInfo, err := u.auth.GetTokenFromBotOption(ctx, botOption...)
if err != nil || authInfo == nil {
return
}
//如果没有找到,则新增
if user == nil {
DingUserInfo, _err := u.getUserInfoFromDingTalk(ctx, authInfo.AccessToken, staffId)
if _err != nil {
return nil, _err
}
user = &model.AiBotUser{
StaffID: DingUserInfo.Userid,
Name: DingUserInfo.Name,
Title: DingUserInfo.Title,
//Extension: DingUserInfo.Extension,
DeptIDList: strings.Join(pkg.SliceIntToString(DingUserInfo.DeptIdList), ","),
IsBoss: int32(pkg.Ter(DingUserInfo.Boss, constants.IsBossTrue, constants.IsBossFalse)),
IsSenior: int32(pkg.Ter(DingUserInfo.Senior, constants.IsSeniorTrue, constants.IsSeniorFalse)),
HiredDate: time.UnixMilli(DingUserInfo.HiredDate),
}
_, err = u.dingUserImpl.Add(user)
if err != nil {
return
}
}
userInfo = &entitys.DingTalkUserInfo{
UserId: int(user.UserID),
StaffId: user.StaffID,
Name: user.Name,
IsBoss: constants.IsBoss(user.IsBoss),
IsSenior: constants.IsSenior(user.IsSenior),
HiredDate: user.HiredDate,
Extension: user.Extension,
}
if len(user.DeptIDList) > 0 {
deptIdList := pkg.SliceStringToInt(strings.Split(user.DeptIDList, ","))
depts, _err := u.dept.GetDeptInfoByDeptIds(ctx, deptIdList, authInfo)
if _err != nil {
return nil, err
}
for _, dept := range depts {
userInfo.Dept = append(userInfo.Dept, dept)
}
}
return userInfo, nil
}
func (u *User) getUserInfoFromDingTalk(ctx context.Context, token string, staffId string) (user UserInfoResResult, err error) {
if token == "" && staffId == "" {
err = errors.New("获取钉钉用户信息的必要参数不足")
return
}
req := l_request.Request{
Method: http.MethodPost,
Url: constants.GetDingTalkRequestUrl(constants.RequestUrlGetUserGet, map[string]string{
"access_token": token,
}),
Data: map[string]string{
"userid": staffId,
},
}
res, err := req.Send()
if err != nil {
return
}
var userInfoRes UserInfoRes
err = json.Unmarshal(res.Content, &userInfoRes)
if err != nil {
return
}
if userInfoRes.Errcode != 0 {
fmt.Errorf("钉钉请求报错:%s", userInfoRes.Errmsg)
}
return userInfoRes.Result, err
}

View File

@ -1,173 +0,0 @@
package handle
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg/l_request"
"bytes"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/gabriel-vasile/mimetype"
)
// HandleRecognizeFile 这里的目的是无论将什么类型的file都转为二进制格式
// 最终输出1.将 files.FileData 填充为文件的二进制数据 2.将 files.FileType 填充为文件的类型(当前为 constants.Caller兼容写入其字符串值
// 判断文件大小统一限制为10MB判断文件类型判断文件是否合法类型在白名单映射中无法识别/非法/超限→填充unknown并兼容返回
// 若 FileData 不存在 且 FileUrl 不存在, 则直接退出
// 若 FileData 存在 FileType 存在, 则直接退出
// 若 FileData 存在 FileType 不存在, 则根据 FileData 推断文件类型并填充 FileType
// 若 FileUrl 存在, 则下载文件并填充 FileData 和 FileType
func HandleRecognizeFile(files *entitys.RecognizeFile) {
if files == nil {
return
}
const maxSize = 10 * 1024 * 1024 // 10MB 上限
// 工具:根据 MIME 或扩展名映射到 FileType
mapToFileType := func(s string) constants.FileType {
if len(s) == 0 {
return constants.FileTypeUnknown
}
s = strings.ToLower(strings.TrimSpace(s))
for ft, items := range constants.FileTypeMappings {
for _, item := range items {
if !strings.HasPrefix(item, ".") { // MIME
if s == item {
return ft
}
} else { // 扩展名
if s == item {
return ft
}
}
}
}
return constants.FileTypeUnknown
}
// 分支1无数据、无URL→直接返回
if len(files.FileData) == 0 && len(files.FileUrl) == 0 {
return
}
// 分支2已有数据且已有类型→直接返回
if len(files.FileData) > 0 && len(strings.TrimSpace(files.FileType.String())) > 0 {
return
}
// 分支3仅有数据、无类型→内容检测并填充
if len(files.FileData) > 0 && len(strings.TrimSpace(files.FileType.String())) == 0 {
if len(files.FileData) > maxSize {
files.FileType = constants.FileTypeUnknown
return
}
reader := bytes.NewReader(files.FileData)
detected, fileRealMime := detectFileType(reader, "")
files.FileType = detected
files.FileRealMime = fileRealMime
return
}
// 分支4存在URL→下载并填充数据与类型
if len(files.FileUrl) > 0 {
fileBytes, contentType, err := downloadFile(files.FileUrl)
if err != nil || len(fileBytes) == 0 {
files.FileType = constants.FileTypeUnknown
return
}
if len(fileBytes) > maxSize {
// 超限:不写入数据,类型置 unknown
files.FileType = constants.FileTypeUnknown
return
}
// 优先使用响应头的 Content-Type 映射
detected := mapToFileType(contentType)
fileRealMime := contentType
if detected == constants.FileTypeUnknown {
// 回退:内容检测 + URL 文件名扩展名辅助
var fname string
if u, perr := url.Parse(files.FileUrl); perr == nil {
fname = filepath.Base(u.Path)
}
reader := bytes.NewReader(fileBytes)
detected, fileRealMime = detectFileType(reader, fname)
}
// 写入数据
files.FileData = fileBytes
files.FileType = detected
files.FileRealMime = fileRealMime
return
}
}
// 下载文件并返回二进制数据、MIME 类型
func downloadFile(fileUrl string) (fileBytes []byte, contentType string, err error) {
if len(fileUrl) == 0 {
return
}
req := l_request.Request{
Method: "GET",
Url: fileUrl,
Headers: map[string]string{
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
},
}
res, err := req.Send()
if err != nil {
return
}
var ex bool
if contentType, ex = res.Headers["Content-Type"]; !ex {
err = errors.New("Content-Type不存在")
return
}
if res.StatusCode != http.StatusOK {
err = fmt.Errorf("server returned non-200 status: %d", res.StatusCode)
}
fileBytes = res.Content
return fileBytes, contentType, nil
}
// detectFileType 判断文件类型
func detectFileType(file io.ReadSeeker, filename string) (constants.FileType, string) {
// 1. 读取文件头检测 MIME
buffer := make([]byte, 512)
n, _ := file.Read(buffer)
file.Seek(0, io.SeekStart) // 重置读取位置
detectedMIME := mimetype.Detect(buffer[:n]).String()
for fileType, items := range constants.FileTypeMappings {
for _, item := range items {
if !strings.HasPrefix(item, ".") && item == detectedMIME {
return fileType, detectedMIME
}
}
}
// 2. 备用:通过扩展名检测
ext := strings.ToLower(filepath.Ext(filename))
for fileType, items := range constants.FileTypeMappings {
for _, item := range items {
if strings.HasPrefix(item, ".") && item == ext {
return fileType, ext
}
}
}
return constants.FileTypeUnknown, ""
}

View File

@ -0,0 +1,22 @@
package handle
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/tools"
)
type Handle struct {
toolManager *tools.Manager
conf *config.Config
}
func NewHandle(
toolManager *tools.Manager,
conf *config.Config,
) *Handle {
return &Handle{
toolManager: toolManager,
conf: conf,
}
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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.加解密协议请参考企业微信官方文档。

View File

@ -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&timestamp=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&timestamp=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, "&timestamp=", start, &msg_signature)
var timestamp string
next = getString(httpstr, "&nonce=", next, &timestamp)
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, "&timestamp=", start, &msg_signature)
var timestamp string
next = getString(httpstr, "&nonce=", next, &timestamp)
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))
}

View File

@ -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&timestamp=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返回给企业微信
// 第23步可以用企业微信提供的库函数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&timestamp=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"标签的内容进行解密,解密出来的明文即是用户回复消息的明文,明文格式请参考官方文档
// 第23步可以用企业微信提供的库函数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格式的字符串发送给企业。
// 以上234步可以用企业微信提供的库函数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)
//}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,11 +0,0 @@
package qywx
import (
"github.com/google/wire"
)
var ProviderSetQywx = wire.NewSet(
NewAuth,
NewGroup,
NewOther,
)

View File

@ -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"`
}

View File

@ -1,7 +1,9 @@
package llm_service
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"context"
"time"
)
@ -18,3 +20,48 @@ func buildSystemPrompt(prompt string) string {
return prompt
}
func buildAssistant(his []model.AiChatHi) (chatHis entitys.ChatHis) {
for _, item := range his {
if len(chatHis.SessionId) == 0 {
chatHis.SessionId = item.SessionID
}
chatHis.Messages = append(chatHis.Messages, []entitys.HisMessage{
{
Role: constants.RoleUser,
Content: item.Ques,
Timestamp: item.CreateAt.Format(time.DateTime),
},
{
Role: constants.RoleAssistant,
Content: item.Ans,
Timestamp: item.CreateAt.Format(time.DateTime),
},
}...)
}
chatHis.Context = entitys.HisContext{
UserLanguage: "zh-CN",
SystemMode: "technical_support",
}
return
}
func BuildChatHisMessage(his []model.AiChatHi) (chatHis []entitys.HisMessage) {
for _, item := range his {
chatHis = append(chatHis, []entitys.HisMessage{
{
Role: constants.RoleUser,
Content: item.Ques,
Timestamp: item.CreateAt.Format(time.DateTime),
},
{
Role: constants.RoleAssistant,
Content: item.Ans,
Timestamp: item.CreateAt.Format(time.DateTime),
},
}...)
}
return
}

View File

@ -1,76 +1,87 @@
package llm_service
//type LangChainService struct {
// client *utils_langchain.UtilLangChain
//}
//
//func NewLangChainGenerate(
// client *utils_langchain.UtilLangChain,
//) *LangChainService {
//
// return &LangChainService{
// client: client,
// }
//}
//
//func (r *LangChainService) IntentRecognize(ctx context.Context, sysInfo model.AiSy, history []model.AiChatHi, userInput string, tasks []model.AiTask) (msg string, err error) {
// prompt := r.getPrompt(sysInfo, history, userInput, tasks)
// AgentClient := r.client.Get()
// defer r.client.Put(AgentClient)
// match, err := AgentClient.Llm.GenerateContent(
// ctx, // 使用可取消的上下文
// prompt,
// llms.WithJSONMode(),
// )
// msg = match.Choices[0].Content
// return
//}
//
//func (r *LangChainService) getPrompt(sysInfo model.AiSy, history []model.AiChatHi, reqInput string, tasks []model.AiTask) []llms.MessageContent {
// var (
// prompt = make([]llms.MessageContent, 0)
// )
// prompt = append(prompt, llms.MessageContent{
// Role: llms.ChatMessageTypeSystem,
// Parts: []llms.ContentPart{
// llms.TextPart(buildSystemPrompt(sysInfo.SysPrompt)),
// },
// }, llms.MessageContent{
// Role: llms.ChatMessageTypeTool,
// Parts: []llms.ContentPart{
// llms.TextPart(pkg.JsonStringIgonErr(buildAssistant(history))),
// },
// }, llms.MessageContent{
// Role: llms.ChatMessageTypeTool,
// Parts: []llms.ContentPart{
// llms.TextPart(pkg.JsonStringIgonErr(r.registerTools(tasks))),
// },
// }, llms.MessageContent{
// Role: llms.ChatMessageTypeHuman,
// Parts: []llms.ContentPart{
// llms.TextPart(reqInput),
// },
// })
// return prompt
//}
//
//func (r *LangChainService) registerTools(tasks []model.AiTask) []llms.Tool {
// taskPrompt := make([]llms.Tool, 0)
// for _, task := range tasks {
// var taskConfig entitys.TaskConfig
// err := json.Unmarshal([]byte(task.Config), &taskConfig)
// if err != nil {
// continue
// }
// taskPrompt = append(taskPrompt, llms.Tool{
// Type: "function",
// Function: &llms.FunctionDefinition{
// Name: task.Index,
// Description: task.Desc,
// Parameters: taskConfig.Param,
// },
// })
//
// }
// return taskPrompt
//}
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/internal/entitys"
"ai_scheduler/internal/pkg"
"ai_scheduler/internal/pkg/utils_langchain"
"context"
"encoding/json"
"github.com/tmc/langchaingo/llms"
)
type LangChainService struct {
client *utils_langchain.UtilLangChain
}
func NewLangChainGenerate(
client *utils_langchain.UtilLangChain,
) *LangChainService {
return &LangChainService{
client: client,
}
}
func (r *LangChainService) IntentRecognize(ctx context.Context, sysInfo model.AiSy, history []model.AiChatHi, userInput string, tasks []model.AiTask) (msg string, err error) {
prompt := r.getPrompt(sysInfo, history, userInput, tasks)
AgentClient := r.client.Get()
defer r.client.Put(AgentClient)
match, err := AgentClient.Llm.GenerateContent(
ctx, // 使用可取消的上下文
prompt,
llms.WithJSONMode(),
)
msg = match.Choices[0].Content
return
}
func (r *LangChainService) getPrompt(sysInfo model.AiSy, history []model.AiChatHi, reqInput string, tasks []model.AiTask) []llms.MessageContent {
var (
prompt = make([]llms.MessageContent, 0)
)
prompt = append(prompt, llms.MessageContent{
Role: llms.ChatMessageTypeSystem,
Parts: []llms.ContentPart{
llms.TextPart(buildSystemPrompt(sysInfo.SysPrompt)),
},
}, llms.MessageContent{
Role: llms.ChatMessageTypeTool,
Parts: []llms.ContentPart{
llms.TextPart(pkg.JsonStringIgonErr(buildAssistant(history))),
},
}, llms.MessageContent{
Role: llms.ChatMessageTypeTool,
Parts: []llms.ContentPart{
llms.TextPart(pkg.JsonStringIgonErr(r.registerTools(tasks))),
},
}, llms.MessageContent{
Role: llms.ChatMessageTypeHuman,
Parts: []llms.ContentPart{
llms.TextPart(reqInput),
},
})
return prompt
}
func (r *LangChainService) registerTools(tasks []model.AiTask) []llms.Tool {
taskPrompt := make([]llms.Tool, 0)
for _, task := range tasks {
var taskConfig entitys.TaskConfig
err := json.Unmarshal([]byte(task.Config), &taskConfig)
if err != nil {
continue
}
taskPrompt = append(taskPrompt, llms.Tool{
Type: "function",
Function: &llms.FunctionDefinition{
Name: task.Index,
Description: task.Desc,
Parameters: taskConfig.Param,
},
})
}
return taskPrompt
}

View File

@ -3,42 +3,46 @@ package llm_service
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/pkg/utils_vllm"
"context"
"encoding/json"
"errors"
"strings"
"time"
"github.com/ollama/ollama/api"
"xorm.io/builder"
)
type OllamaService struct {
client *utils_ollama.Client
vllmClient *utils_vllm.Client
config *config.Config
chatHis *impl.ChatHisImpl
client *utils_ollama.Client
config *config.Config
chatHis *impl.ChatImpl
}
func NewOllamaGenerate(
client *utils_ollama.Client,
vllmClient *utils_vllm.Client,
config *config.Config,
chatHis *impl.ChatHisImpl,
chatHis *impl.ChatImpl,
) *OllamaService {
return &OllamaService{
client: client,
vllmClient: vllmClient,
config: config,
chatHis: chatHis,
client: client,
config: config,
chatHis: chatHis,
}
}
func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSelect) (msg string, err error) {
func (r *OllamaService) IntentRecognize(ctx context.Context, requireData *entitys.RequireData) (msg string, err error) {
prompt, err := r.getPrompt(ctx, requireData)
if err != nil {
return
}
toolDefinitions := r.registerToolsOllama(requireData.Tasks)
toolDefinitions := r.registerToolsOllama(req.Tools)
match, err := r.client.ToolSelect(ctx, req.Prompt, toolDefinitions)
match, err := r.client.ToolSelect(ctx, prompt, toolDefinitions)
if err != nil {
return
}
@ -62,63 +66,109 @@ func (r *OllamaService) IntentRecognize(ctx context.Context, req *entitys.ToolSe
return
}
//func (r *OllamaService) RecognizeWithImg(ctx context.Context, imgByte []api.ImageData, ch chan entitys.Response) (desc api.GenerateResponse, err error) {
// if imgByte == nil {
// return
// }
// entitys.ResLog(requireData.Ch, "recognize_img_start", "图片识别中...")
//
// desc, err = r.client.Generation(ctx, &api.GenerateRequest{
// Model: r.config.Ollama.VlModel,
// Stream: new(bool),
// System: r.config.DefaultPrompt.ImgRecognize.SystemPrompt,
// Prompt: r.config.DefaultPrompt.ImgRecognize.UserPrompt,
// Images: requireData.ImgByte,
// KeepAlive: &api.Duration{Duration: 3600 * time.Second},
// //Think: &api.ThinkValue{Value: false},
// })
// if err != nil {
// return
// }
// entitys.ResLog(requireData.Ch, "recognize_img_end", "图片识别完成,识别内容:"+desc.Response)
// return
//}
func (r *OllamaService) getPrompt(ctx context.Context, requireData *entitys.RequireData) ([]api.Message, error) {
//func (r *OllamaService) RecognizeWithImgVllm(ctx context.Context, requireData *entitys.RequireData) (desc api.GenerateResponse, err error) {
// if requireData.ImgByte == nil {
// return
// }
// entitys.ResLog(requireData.Ch, "recognize_img_start", "图片识别中...")
//
// outMsg, err := r.vllmClient.RecognizeWithImg(ctx,
// r.config.DefaultPrompt.ImgRecognize.SystemPrompt,
// r.config.DefaultPrompt.ImgRecognize.UserPrompt,
// requireData.ImgUrls,
// )
// if err != nil {
// return api.GenerateResponse{}, err
// }
//
// desc = api.GenerateResponse{
// Response: outMsg.Content,
// }
//
// entitys.ResLog(requireData.Ch, "recognize_img_end", "图片识别完成,识别内容:"+desc.Response)
// return
//}
var (
prompt = make([]api.Message, 0)
)
content, err := r.getUserContent(ctx, requireData)
if err != nil {
return nil, err
}
prompt = append(prompt, api.Message{
Role: "system",
Content: buildSystemPrompt(requireData.Sys.SysPrompt),
}, api.Message{
Role: "assistant",
Content: "### 聊天记录:" + pkg.JsonStringIgonErr(buildAssistant(requireData.Histories)),
}, api.Message{
Role: "user",
Content: content,
})
func (r *OllamaService) registerToolsOllama(tasks []entitys.RegistrationTask) []api.Tool {
return prompt, nil
}
func (r *OllamaService) getUserContent(ctx context.Context, requireData *entitys.RequireData) (string, error) {
var content strings.Builder
content.WriteString(requireData.Req.Text)
if len(requireData.ImgByte) > 0 {
content.WriteString("\n")
}
if len(requireData.Req.Tags) > 0 {
content.WriteString("\n")
content.WriteString("### 工具必须使用:")
content.WriteString(requireData.Req.Tags)
}
if len(requireData.ImgByte) > 0 {
desc, err := r.RecognizeWithImg(ctx, requireData)
if err != nil {
return "", err
}
content.WriteString("### 上传图片解析内容:\n")
content.WriteString(requireData.Req.Tags)
content.WriteString(desc.Response)
}
if requireData.Req.MarkHis > 0 {
var his model.AiChatHi
cond := builder.NewCond()
cond = cond.And(builder.Eq{"his_id": requireData.Req.MarkHis})
err := r.chatHis.GetOneBySearchToStrut(&cond, &his)
if err != nil {
return "", err
}
content.WriteString("### 引用历史聊天记录:\n")
content.WriteString(pkg.JsonStringIgonErr(BuildChatHisMessage([]model.AiChatHi{his})))
}
return content.String(), nil
}
func (r *OllamaService) RecognizeWithImg(ctx context.Context, requireData *entitys.RequireData) (desc api.GenerateResponse, err error) {
if requireData.ImgByte == nil {
return
}
entitys.ResLog(requireData.Ch, "recognize_img_start", "图片识别中...")
desc, err = r.client.Generation(ctx, &api.GenerateRequest{
Model: r.config.Ollama.VlModel,
Stream: new(bool),
System: r.config.DefaultPrompt.ImgRecognize.SystemPrompt,
Prompt: r.config.DefaultPrompt.ImgRecognize.UserPrompt,
Images: requireData.ImgByte,
KeepAlive: &api.Duration{Duration: 3600 * time.Second},
})
if err != nil {
return
}
// 图片识别结果 减少后期重复识别耗时
requireData.ImgContent = desc.Response
entitys.ResLog(requireData.Ch, "recognize_img_end", "图片识别完成,识别内容:"+desc.Response)
return
}
func (r *OllamaService) registerToolsOllama(tasks []model.AiTask) []api.Tool {
taskPrompt := make([]api.Tool, 0)
for _, task := range tasks {
var taskConfig entitys.TaskConfigDetail
err := json.Unmarshal([]byte(task.Config), &taskConfig)
if err != nil {
continue
}
taskPrompt = append(taskPrompt, api.Tool{
Type: "function",
Function: api.ToolFunction{
Name: task.Name,
Name: task.Index,
Description: task.Desc,
Parameters: api.ToolFunctionParameters{
Type: task.TaskConfigDetail.Param.Type,
Required: task.TaskConfigDetail.Param.Required,
Properties: task.TaskConfigDetail.Param.Properties,
Type: taskConfig.Param.Type,
Required: taskConfig.Param.Required,
Properties: taskConfig.Param.Properties,
},
},
})

View File

@ -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
}

View File

@ -2,8 +2,8 @@ package biz
import (
"ai_scheduler/internal/biz/do"
"ai_scheduler/internal/biz/handle"
"ai_scheduler/internal/biz/llm_service"
"ai_scheduler/internal/biz/support"
"github.com/google/wire"
)
@ -12,16 +12,10 @@ var ProviderSetBiz = wire.NewSet(
NewAiRouterBiz,
NewSessionBiz,
NewChatHistoryBiz,
//llm_service.NewLangChainGenerate,
llm_service.NewLangChainGenerate,
llm_service.NewOllamaGenerate,
llm_service.NewVllmService,
//handle.NewHandle,
handle.NewHandle,
do.NewDo,
do.NewHandle,
NewTaskBiz,
NewDingTalkBotBiz,
NewQywxAppBiz,
NewGroupConfigBiz,
do.NewMacro,
support.NewHytAddressIngester,
)

View File

@ -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("![图片](%s)", 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
}

View File

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

View File

@ -2,17 +2,8 @@ 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"
"ai_scheduler/internal/pkg/rec_extra"
"context"
"encoding/json"
"strings"
"time"
"ai_scheduler/internal/entitys"
"github.com/gofiber/fiber/v2/log"
@ -22,19 +13,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,
}
}
@ -51,9 +39,11 @@ func (r *AiRouterBiz) RouteWithSocket(client *gateway.Client, req *entitys.ChatS
requireData := &entitys.RequireData{
Req: req,
}
// 获取WebSocket连接
conn := client.GetConn()
//初始化通道/上下文
ctx, clearFunc := r.do.MakeCh(client, requireData)
ctx, clearFunc := r.do.MakeCh(conn, requireData)
defer func() {
if err != nil {
entitys.ResError(requireData.Ch, "", err.Error())
@ -66,151 +56,17 @@ func (r *AiRouterBiz) RouteWithSocket(client *gateway.Client, req *entitys.ChatS
log.Errorf("数据验证和收集失败: %s", err.Error())
return
}
//组装意图识别
rec, sys, err := r.SetRec(ctx, requireData)
if err != nil {
log.Errorf("组装意图识别失败: %s", err.Error())
return
}
//意图识别
err = r.handle.Recognize(ctx, &rec, sys)
if err != nil {
//意图识别
if err = r.handle.Recognize(ctx, requireData); err != nil {
log.Errorf("意图识别失败: %s", err.Error())
return
}
//任务处理
rec_extra.SetTaskRecExt(requireData, &rec)
//向下传递
if err = r.handle.HandleMatch(ctx, client, &rec, requireData); err != nil {
if err = r.handle.HandleMatch(ctx, client, requireData); err != nil {
log.Errorf("任务处理失败: %s", err.Error())
return
}
return
}
func (r *AiRouterBiz) SetRec(ctx context.Context, requireData *entitys.RequireData) (match entitys.Recognize, sys do.PromptOption, err error) {
// 参数空值检查
if requireData == nil || requireData.Req == nil {
return match, sys, errors.NewBusinessErr(500, "请求参数为空")
}
// 对应不同的appKey, 配置不同的系统提示词
switch requireData.Sys.AppKey {
default:
sys = &do.WithSys{Config: r.config}
}
// 1. 系统提示词
match.SystemPrompt = requireData.Sys.SysPrompt
// 2. 用户输入和文件处理
match.UserContent, err = r.buildUserContent(requireData)
if err != nil {
log.Errorf("构建用户内容失败: %s", err.Error())
return
}
// 3. 聊天记录 - 只有在有历史记录时才构建
if len(requireData.Histories) > 0 {
match.ChatHis = r.buildChatHistory(requireData)
}
// 4. 任务列表 - 预分配切片容量
if len(requireData.Tasks) > 0 {
match.Tasks = make([]entitys.RegistrationTask, 0, len(requireData.Tasks))
for _, task := range requireData.Tasks {
taskConfig := entitys.TaskConfigDetail{}
if err = json.Unmarshal([]byte(task.Config), &taskConfig); err != nil {
log.Errorf("解析任务配置失败: %s, 任务ID: %s", err.Error(), task.Index)
continue // 解析失败时跳过该任务,而不是直接返回错误
}
match.Tasks = append(match.Tasks, entitys.RegistrationTask{
Name: task.Index,
Desc: task.Desc,
TaskConfigDetail: taskConfig, // 直接使用解析后的配置,避免重复构建
})
}
}
match.Ch = requireData.Ch
return
}
// buildUserContent 构建用户内容
func (r *AiRouterBiz) buildUserContent(requireData *entitys.RequireData) (*entitys.RecognizeUserContent, error) {
// 预分配文件切片容量最多2个文件File和Img
files := make([]*entitys.RecognizeFile, 0, 2)
// 处理文件和图片
fileUrls := []string{requireData.Req.File, requireData.Req.Img}
for _, item := range fileUrls {
// 处理逗号分隔的多个URL
urlList := strings.Split(item, ",")
for _, url := range urlList {
if url != "" {
files = append(files, &entitys.RecognizeFile{FileUrl: url})
}
}
}
// 构建并返回用户内容
return &entitys.RecognizeUserContent{
Text: requireData.Req.Text,
File: files,
ActionCardUrl: "", // TODO: 后续实现操作卡片功能
Tag: requireData.Req.Tags,
}, nil
}
// buildChatHistory 构建聊天历史
func (r *AiRouterBiz) buildChatHistory(requireData *entitys.RequireData) entitys.ChatHis {
// 预分配消息切片容量每个历史记录生成2条消息
messages := make([]entitys.HisMessage, 0, len(requireData.Histories)*2)
// 构建聊天记录
for _, h := range requireData.Histories {
// 用户消息
messages = append(messages, entitys.HisMessage{
Role: constants.RoleUser, // 用户角色
Content: h.Ques, // 用户输入内容
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),
// })
}
// 构建聊天历史上下文
return entitys.ChatHis{
SessionId: requireData.Session,
Messages: messages,
Context: entitys.HisContext{
UserLanguage: constants.LangZhCN, // 默认中文
SystemMode: constants.SystemModeTechnicalSupport, // 默认技术支持模式
},
}
}
// 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
// }

122
internal/biz/router_test.go Normal file
View File

@ -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")
// }

View File

@ -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"
@ -18,16 +17,16 @@ import (
type SessionBiz struct {
sessionRepo *impl.SessionImpl
sysRepo *impl.SysImpl
chatHisRepo *impl.ChatHisImpl
chatRepo *impl.ChatImpl
conf *config.Config
}
func NewSessionBiz(conf *config.Config, sessionImpl *impl.SessionImpl, sysImpl *impl.SysImpl, chatImpl *impl.ChatHisImpl) *SessionBiz {
func NewSessionBiz(conf *config.Config, sessionImpl *impl.SessionImpl, sysImpl *impl.SysImpl, chatImpl *impl.ChatImpl) *SessionBiz {
return &SessionBiz{
sessionRepo: sessionImpl,
sysRepo: sysImpl,
chatHisRepo: chatImpl,
chatRepo: chatImpl,
conf: conf,
}
}
@ -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,
@ -82,10 +91,10 @@ func (s *SessionBiz) SessionInit(ctx context.Context, req *entitys.SessionInitRe
result.Prologue = sysConfig.Prologue
// 存在,返回会话历史
var chatList []model.AiChatHi
chatList, err = s.chatHisRepo.FindAll(
s.chatHisRepo.WithSessionId(session.SessionID), // 条件会话ID
s.chatHisRepo.OrderByDesc("create_at"), // 排序:按创建时间降序
s.chatHisRepo.WithLimit(constants.ChatHistoryLimit), // 限制返回条数
chatList, err = s.chatRepo.FindAll(
s.chatRepo.WithSessionId(session.SessionID), // 条件会话ID
s.chatRepo.OrderByDesc("create_at"), // 排序:按创建时间降序
s.chatRepo.WithLimit(constants.ChatHistoryLimit), // 限制返回条数
)
if err != nil {
return
@ -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
}

View File

@ -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
}

View File

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

View File

@ -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
}

View File

@ -1,9 +0,0 @@
package tools_regis
import (
"github.com/google/wire"
)
var ProviderToolsRegis = wire.NewSet(
NewToolsRegis,
)

View File

@ -1,30 +0,0 @@
package tools_regis
import (
"ai_scheduler/internal/data/constants"
"ai_scheduler/internal/data/impl"
"ai_scheduler/internal/data/model"
"xorm.io/builder"
)
type ToolRegis struct {
//待优化
BootTools []model.AiBotTool
}
func NewToolsRegis(botToolsImpl *impl.BotToolsImpl) *ToolRegis {
botTools := &ToolRegis{}
err := botTools.RegisTools(botToolsImpl)
if err != nil {
panic(err)
}
return botTools
}
func (t *ToolRegis) RegisTools(botToolsImpl *impl.BotToolsImpl) error {
cond := builder.NewCond()
cond = cond.And(builder.Eq{"status": constants.Enable})
err := botToolsImpl.GetRangeToMapStruct(&cond, &t.BootTools)
return err
}

View File

@ -1,7 +1,6 @@
package config
import (
"ai_scheduler/pkg"
"fmt"
"time"
@ -12,25 +11,14 @@ import (
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"`
// LLM *LLM `mapstructure:"llm"`
}
type SysPrompt struct {
@ -43,66 +31,15 @@ type DefaultPrompt struct {
}
type LLM struct {
Providers map[string]LLMProviderConfig `mapstructure:"providers"`
Capabilities map[string]LLMCapabilityConfig `mapstructure:"capabilities"`
}
type LLMProviderConfig struct {
Endpoint string `mapstructure:"endpoint"`
Timeout string `mapstructure:"timeout"`
Models []LLMModle `mapstructure:"models"`
}
type LLMModle struct {
ID string `mapstructure:"id"`
Name string `mapstructure:"name"`
Streaming bool `mapstructure:"streaming"`
Modalities []string `mapstructure:"modalities"`
MaxTokens int `mapstructure:"max_tokens"`
}
type LLMParameters struct {
Temperature float64 `mapstructure:"temperature"`
MaxTokens int `mapstructure:"max_tokens"`
Stream bool `mapstructure:"stream"`
}
type LLMCapabilityConfig struct {
Provider string `mapstructure:"provider"`
Model string `mapstructure:"model"`
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"`
Model string `mapstructure:"model"`
}
// SysConfig 系统配置
type SysConfig struct {
SessionLen int `mapstructure:"session_len"`
ChannelPoolLen int `mapstructure:"channel_pool_len"`
ChannelPoolSize int `mapstructure:"channel_pool_size"`
LlmPoolLen int `mapstructure:"llm_pool_len"`
HeartbeatInterval int `mapstructure:"heartbeat_interval"`
SessionLen int `mapstructure:"session_len"`
ChannelPoolLen int `mapstructure:"channel_pool_len"`
ChannelPoolSize int `mapstructure:"channel_pool_size"`
LlmPoolLen int `mapstructure:"llm_pool_len"`
}
// ServerConfig 服务器配置
@ -116,39 +53,10 @@ 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"`
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 +78,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"`
@ -197,11 +96,6 @@ type ToolsConfig struct {
ZltxOrderAfterSaleReseller ToolConfig `mapstructure:"zltxOrderAfterSaleReseller"`
// 下游批充订单售后
ZltxOrderAfterSaleResellerBatch ToolConfig `mapstructure:"zltxOrderAfterSaleResellerBatch"`
// Coze 快递查询工具
CozeExpress ToolConfig `mapstructure:"cozeExpress"`
// Coze 公司查询工具
CozeCompany ToolConfig `mapstructure:"cozeCompany"`
ZltxResellerAuthProductToManagerAndDefaultLossReason ToolConfig `mapstructure:"zltxResellerAuthProductToManagerAndDefaultLossReason"`
}
// ToolConfig 单个工具配置
@ -214,38 +108,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 +140,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
}

View File

@ -3,45 +3,5 @@ package constants
type BotTools string
const (
BotToolsBugOptimizationSubmit BotTools = "bug_optimization_submit" // 系统的bug/优化建议
)
type ChatStyle int
const (
ChatStyleNormal ChatStyle = 1 //正常
ChatStyleSerious ChatStyle = 2 //严肃
ChatStyleGentle ChatStyle = 3 //温柔
ChatStyleArrogance ChatStyle = 4 //傲慢
ChatStyleCute ChatStyle = 5 //可爱
ChatStyleAngry ChatStyle = 6 //愤怒
)
var ChatStyleMap = map[ChatStyle]string{
ChatStyleNormal: "正常",
ChatStyleSerious: "严肃",
ChatStyleGentle: "温柔",
ChatStyleArrogance: "傲慢",
ChatStyleCute: "可爱",
ChatStyleAngry: "愤怒",
}
type BotType int
const (
BotTypeDingTalk BotType = 1 // 系统的bug/优化建议
)
const DingTalkAuthBaseKeyPrefix = "dingTalk_auth"
const DingTalkAuthBaseKeyBotPrefix = "dingTalk_auth_bot"
const QywxAuthBaseKeyPrefix = "qywx_auth"
// PermissionType 工具使用权限
type PermissionType int32
const (
PermissionTypeNone = 1
PermissionTypeDept = 2
BotToolsBugOptimizationSubmit = "bug_optimization_submit" // 系统的bug/优化建议
)

View File

@ -13,14 +13,6 @@ const (
// 分页默认条数
ChatHistoryLimit = 10
// 语言
LangZhCN = "zh-CN" // 中文
// 系统模式
SystemModeDefault = "default" // 默认模式
// 系统模式 "technical_support", // 技术支持模式
SystemModeTechnicalSupport = "technical_support" // 技术支持模式
)
func (c Caller) String() string {

View File

@ -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"
)

View File

@ -11,13 +11,10 @@ const (
type TaskType int32
const (
TaskTypeApi TaskType = 1
TaskTypeKnowle TaskType = 2
TaskTypeFunc TaskType = 3
TaskTypeBot TaskType = 4
TaskTypeEinoWorkflow TaskType = 5 // eino 工作流
TaskTypeCozeWorkflow TaskType = 6 // coze 工作流
TaskTypeReport TaskType = 7 //报表
TaskTypeApi TaskType = 1
TaskTypeKnowle TaskType = 2
TaskTypeFunc TaskType = 3
TaskTypeBot TaskType = 4
)
type UseFul int32
@ -33,5 +30,3 @@ var UseFulMap = map[UseFul]string{
UseFulNotUnclear: "回答不明确",
UseFulNotError: "理解错误",
}
type BaseBool int32

View File

@ -1,80 +0,0 @@
package constants
import "net/url"
const DingTalkBseUrl = "https://oapi.dingtalk.com"
type RequestUrl string
const (
RequestUrlGetUserGet RequestUrl = "/topapi/v2/user/get"
RequestUrlGetDeptGet RequestUrl = "/topapi/v2/department/get"
)
func GetDingTalkRequestUrl(path RequestUrl, query map[string]string) string {
u, _ := url.Parse(DingTalkBseUrl + string(path))
q := u.Query()
for key, val := range query {
q.Add(key, val)
}
u.RawQuery = q.Encode()
return u.String()
}
// IsBoss 是否是老板
type IsBoss int
const (
IsBossTrue IsBoss = 1
IsBossFalse IsBoss = 0
)
// IsSenior 是否是老板
type IsSenior int
const (
IsSeniorTrue IsSenior = 1
IsSeniorFalse IsSenior = 0
)
type ConversationType string
const (
ConversationTypeSingle = "1" // 单聊
ConversationTypeGroup = "2" //群聊
)
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"
}
]
}`
)

View File

@ -1,52 +0,0 @@
package constants
type FileType string
const (
FileTypeUnknown FileType = "unknown"
FileTypeImage FileType = "image"
//FileTypeVideo FileType = "video"
FileTypeExcel FileType = "excel"
FileTypeWord FileType = "word"
FileTypeTxt FileType = "txt"
FileTypePDF FileType = "pdf"
FileTypePPT FileType = "ppt"
FileTypeCSV FileType = "csv"
)
var FileTypeMappings = map[FileType][]string{
FileTypeImage: {
"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml",
".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg",
},
FileTypeExcel: {
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".xls", ".xlsx",
},
FileTypeWord: {
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".doc", ".docx",
},
FileTypePDF: {
"application/pdf",
".pdf",
},
FileTypeTxt: {
"text/plain",
".txt",
},
FileTypePPT: {
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
".pptx",
},
FileTypeCSV: {
"text/csv",
".csv",
},
}
func (ft FileType) String() string {
return string(ft)
}

View File

@ -1,8 +0,0 @@
package constants
import "time"
const (
CACHE_KEY_LSXD_TOKEN = "ai_scheduler:lsxd_token"
EXPIRE_LSXD_TOKEN = time.Hour * 2 // 2小时
)

View File

@ -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不要输出任何解释性文字`
)

View File

@ -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",
}

View File

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

View File

@ -22,7 +22,7 @@ BaseModel 是一个泛型结构体用于封装GORM数据库通用操作。
// 定义受支持的PO类型集合可根据需要扩展, 只有包含表结构才能使用BaseModel避免使用出现问题
type PO interface {
model.AiChatHi |
model.AiSy | model.AiSession | model.AiTask | model.AiBotConfig
model.AiSy | model.AiSession | model.AiTask | model.AiBot
}
type BaseModel[P PO] struct {
@ -43,21 +43,17 @@ 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 // 限制返回条数
}
// PaginationResult 分页查询结果
@ -129,26 +125,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 +191,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 {
@ -246,17 +215,3 @@ func (this *BaseModel[P]) WithLimit(limit int) CondFunc {
return db.Limit(limit)
}
}
// 查询字段是否在列表中
func (this *BaseModel[P]) In(field string, values interface{}) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where(fmt.Sprintf("%s IN ?", field), values)
}
}
// select 字段
func (this *BaseModel[P]) Select(fields ...string) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Select(fields)
}
}

View File

@ -1,15 +0,0 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
)
type BotChatHisImpl struct {
dataTemp.DataTemp
}
func NewBotChatHisImpl(db *utils.Db) *BotChatHisImpl {
return &BotChatHisImpl{*dataTemp.NewDataTemp(db, new(model.AiBotChatHi))}
}

View File

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

View File

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

View File

@ -1,27 +0,0 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
"database/sql"
)
type BotGroupImpl struct {
dataTemp.DataTemp
}
func NewBotGroupImpl(db *utils.Db) *BotGroupImpl {
return &BotGroupImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotGroup)),
}
}
func (k BotGroupImpl) GetByConversationIdAndRobotCode(staffId string, robotCode string) (*model.AiBotGroup, error) {
var data model.AiBotGroup
err := k.Db.Model(k.Model).Where("conversation_id = ? and robot_code = ?", staffId, robotCode).Find(&data).Error
if data.GroupID == 0 {
err = sql.ErrNoRows
}
return &data, err
}

View File

@ -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)),
}
}

View File

@ -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)),
}
}

View File

@ -0,0 +1,28 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
"gorm.io/gorm"
)
type BotImpl struct {
dataTemp.DataTemp
BaseRepository[model.AiBot]
}
func NewBotImpl(db *utils.Db) *BotImpl {
return &BotImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBot)),
BaseRepository: NewBaseModel[model.AiBot](db.Client),
}
}
// WithSysId 系统id
func (s *BotImpl) WithSysId(sysId interface{}) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("sys_id = ?", sysId)
}
}

View File

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

View File

@ -1,27 +0,0 @@
package impl
import (
"ai_scheduler/internal/data/model"
"ai_scheduler/tmpl/dataTemp"
"ai_scheduler/utils"
"database/sql"
)
type BotUserImpl struct {
dataTemp.DataTemp
}
func NewBotUserImpl(db *utils.Db) *BotUserImpl {
return &BotUserImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiBotUser)),
}
}
func (k BotUserImpl) GetByStaffId(staffId string) (*model.AiBotUser, error) {
var data model.AiBotUser
err := k.Db.Model(k.Model).Where("staff_id = ?", staffId).Find(&data).Error
if data.UserID == 0 {
err = sql.ErrNoRows
}
return &data, err
}

View File

@ -11,14 +11,14 @@ import (
"gorm.io/gorm"
)
type ChatHisImpl struct {
type ChatImpl struct {
dataTemp.DataTemp
BaseRepository[model.AiChatHi]
chatChannel chan model.AiChatHi
}
func NewChatHisImpl(db *utils.Db) *ChatHisImpl {
return &ChatHisImpl{
func NewChatImpl(db *utils.Db) *ChatImpl {
return &ChatImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiChatHi)),
BaseRepository: NewBaseModel[model.AiChatHi](db.Client),
chatChannel: make(chan model.AiChatHi, 100),
@ -26,19 +26,19 @@ func NewChatHisImpl(db *utils.Db) *ChatHisImpl {
}
// WithSessionId 条件会话ID
func (impl *ChatHisImpl) WithSessionId(sessionId interface{}) CondFunc {
func (impl *ChatImpl) WithSessionId(sessionId interface{}) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("session_id = ?", sessionId)
}
}
// 异步添加会话历史
func (impl *ChatHisImpl) AsyncCreate(ctx context.Context, chat model.AiChatHi) {
func (impl *ChatImpl) AsyncCreate(ctx context.Context, chat model.AiChatHi) {
impl.chatChannel <- chat
}
// 异步处理会话历史
func (impl *ChatHisImpl) AsyncProcess(ctx context.Context) {
func (impl *ChatImpl) AsyncProcess(ctx context.Context) {
for {
select {
case chat := <-impl.chatChannel:
@ -55,10 +55,3 @@ func (impl *ChatHisImpl) AsyncProcess(ctx context.Context) {
}
}
}
// his_id 条件历史ID
func (impl *ChatHisImpl) WithHisId(hisId interface{}) CondFunc {
return func(db *gorm.DB) *gorm.DB {
return db.Where("his_id = ?", hisId)
}
}

View File

@ -4,18 +4,4 @@ import (
"github.com/google/wire"
)
var ProviderImpl = wire.NewSet(
NewSessionImpl,
NewSysImpl,
NewTaskImpl,
NewChatHisImpl,
NewBotConfigImpl,
NewBotDeptImpl,
NewBotUserImpl,
NewBotChatHisImpl,
NewBotToolsImpl,
NewBotGroupImpl,
NewBotGroupConfigImpl,
NewBotGroupQywxImpl,
NewReportDailyCacheImpl,
)
var ProviderImpl = wire.NewSet(NewSessionImpl, NewSysImpl, NewTaskImpl, NewChatImpl)

View File

@ -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)),
}
}

View File

@ -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")
}
}

View File

@ -8,12 +8,8 @@ import (
type TaskImpl struct {
dataTemp.DataTemp
BaseRepository[model.AiTask]
}
func NewTaskImpl(db *utils.Db) *TaskImpl {
return &TaskImpl{
DataTemp: *dataTemp.NewDataTemp(db, new(model.AiTask)),
BaseRepository: NewBaseModel[model.AiTask](db.Client),
}
return &TaskImpl{*dataTemp.NewDataTemp(db, new(model.AiTask))}
}

View File

@ -0,0 +1,29 @@
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
// Code generated by gorm.io/gen. DO NOT EDIT.
package model
import (
"time"
)
const TableNameAiBot = "ai_bot"
// AiBot mapped from table <ai_bot>
type AiBot 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" json:"bot_type"`
BotName string `gorm:"column:bot_name;not null" json:"bot_name"`
BotConfig string `gorm:"column:bot_config;not null" json:"bot_config"`
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"`
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
}
// TableName AiBot's table name
func (*AiBot) TableName() string {
return TableNameAiBot
}

View File

@ -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 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
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"`
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"`
}
// TableName AiBotChatHi's table name
func (*AiBotChatHi) TableName() string {
return TableNameAiBotChatHi
}

View File

@ -1,30 +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 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"`
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"` // 索引
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"`
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
}
// TableName AiBotConfig's table name
func (*AiBotConfig) TableName() string {
return TableNameAiBotConfig
}

View File

@ -1,27 +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 TableNameAiBotDept = "ai_bot_dept"
// AiBotDept mapped from table <ai_bot_dept>
type AiBotDept struct {
DeptID int32 `gorm:"column:dept_id;primaryKey;autoIncrement:true" json:"dept_id"`
DingtalkDeptID int32 `gorm:"column:dingtalk_dept_id;not null;comment:标记部门的唯一id钉钉钉钉侧提供的dept_id" json:"dingtalk_dept_id"` // 标记部门的唯一id钉钉钉钉侧提供的dept_id
Name string `gorm:"column:name;not null;comment:用户名称" json:"name"` // 用户名称
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"`
}
// TableName AiBotDept's table name
func (*AiBotDept) TableName() string {
return TableNameAiBotDept
}

View File

@ -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 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"` // 群名称
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 AiBotGroup's table name
func (*AiBotGroup) TableName() string {
return TableNameAiBotGroup
}

View File

@ -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
}

View File

@ -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
}

View File

@ -1,32 +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 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"` // 类型下所需路由以及参数
Type int32 `gorm:"column:type;not null;default:3" json:"type"`
Name string `gorm:"column:name;not null;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"`
DeleteAt time.Time `gorm:"column:delete_at" json:"delete_at"`
}
// TableName AiBotTool's table name
func (*AiBotTool) TableName() string {
return TableNameAiBotTool
}

View File

@ -1,33 +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 TableNameAiBotUser = "ai_bot_user"
// AiBotUser mapped from table <ai_bot_user>
type AiBotUser struct {
UserID int32 `gorm:"column:user_id;primaryKey" json:"user_id"`
StaffID string `gorm:"column:staff_id;not null;comment:标记用户用的唯一id钉钉钉钉侧提供的user_id" json:"staff_id"` // 标记用户用的唯一id钉钉钉钉侧提供的user_id
Name string `gorm:"column:name;not null;comment:用户名称" json:"name"` // 用户名称
Title string `gorm:"column:title;not null;comment:职位" json:"title"` // 职位
Extension string `gorm:"column:extension;not null;default:1;comment:信息面板" json:"extension"` // 信息面板
RoleList string `gorm:"column:role_list;not null;comment:角色列表。" json:"role_list"` // 角色列表。
DeptIDList string `gorm:"column:dept_id_list;not null;comment:所在部门id列表" json:"dept_id_list"` // 所在部门id列表
IsBoss int32 `gorm:"column:is_boss;not null;comment:是否是老板" json:"is_boss"` // 是否是老板
IsSenior int32 `gorm:"column:is_senior;not null;comment:是否是高管" json:"is_senior"` // 是否是高管
HiredDate time.Time `gorm:"column:hired_date;not null;default:CURRENT_TIMESTAMP;comment:入职时间" json:"hired_date"` // 入职时间
Status int32 `gorm:"column:status;not null" 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 AiBotUser's table name
func (*AiBotUser) TableName() string {
return TableNameAiBotUser
}

View File

@ -20,8 +20,6 @@ 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"` // 前端回传数据
}
// TableName AiChatHi's table name

View File

@ -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
}

View File

@ -1,11 +0,0 @@
package common
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/domain/llm"
)
func OptionsFromLLMParameters(p config.LLMParameters) llm.Options {
return llm.Options{Temperature: float32(p.Temperature), MaxTokens: p.MaxTokens, Stream: p.Stream}
}

View File

@ -1,4 +0,0 @@
package common
type KV map[string]any

View File

@ -1,37 +0,0 @@
package common
import (
"errors"
"strings"
"github.com/cloudwego/eino/schema"
)
type ImageInput struct {
URLs []string
}
func BuildVisionMessages(systemPrompt string, userText string, images ImageInput) ([]*schema.Message, error) {
if len(images.URLs) == 0 {
return nil, errors.New("vision requires at least one image url")
}
parts := make([]schema.MessageInputPart, 0, 1+len(images.URLs))
if strings.TrimSpace(userText) != "" {
parts = append(parts, schema.MessageInputPart{Type: schema.ChatMessagePartTypeText, Text: userText})
}
for _, u := range images.URLs {
if u == "" {
continue
}
if !strings.HasPrefix(u, "http://") && !strings.HasPrefix(u, "https://") {
continue
}
parts = append(parts, schema.MessageInputPart{Type: schema.ChatMessagePartTypeImageURL, Image: &schema.MessageInputImage{MessagePartCommon: schema.MessagePartCommon{URL: &u}, Detail: schema.ImageURLDetailHigh}})
}
if len(parts) == 0 {
return nil, errors.New("vision inputs invalid: no text or valid image urls")
}
msgs := []*schema.Message{schema.SystemMessage(systemPrompt)}
msgs = append(msgs, &schema.Message{Role: schema.User, UserInputMultiContent: parts})
return msgs, nil
}

View File

@ -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()
}

View File

@ -1,5 +0,0 @@
package callback
import "github.com/google/wire"
var ProviderSet = wire.NewSet(NewRedisManager, wire.Bind(new(Manager), new(*RedisManager)))

View File

@ -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,
}
}

View File

@ -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,
)

View File

@ -1,14 +0,0 @@
package llm
import (
"context"
"github.com/cloudwego/eino/schema"
)
type Service interface {
Chat(ctx context.Context, input []*schema.Message, opts Options) (*schema.Message, error)
ChatStream(ctx context.Context, input []*schema.Message, opts Options) (*schema.StreamReader[*schema.Message], error)
Vision(ctx context.Context, input []*schema.Message, opts Options) (*schema.Message, error)
Intent(ctx context.Context, input []*schema.Message, opts Options) (*schema.Message, error)
}

View File

@ -1,48 +0,0 @@
package capability
import (
"ai_scheduler/internal/config"
"ai_scheduler/internal/domain/llm"
"strings"
"time"
)
func Route(cfg *config.Config, ability Ability) (ProviderChoice, llm.Options, error) {
cap, ok := cfg.LLM.Capabilities[string(ability)]
if !ok {
return ProviderChoice{}, llm.Options{}, llm.ErrInvalidCapability
}
prov, ok := cfg.LLM.Providers[cap.Provider]
if !ok {
return ProviderChoice{}, llm.Options{}, llm.ErrProviderNotFound
}
var modelConf config.LLMModle
found := false
for _, m := range prov.Models {
if m.Name == cap.Model || m.ID == cap.Model {
modelConf = m
found = true
break
}
}
if !found {
return ProviderChoice{}, llm.Options{}, llm.ErrModelNotFound
}
to := llm.Options{}
to.Model = modelConf.Name
to.Stream = cap.Parameters.Stream || modelConf.Streaming
if to.Stream && !modelConf.Streaming {
to.Stream = false
}
to.MaxTokens = modelConf.MaxTokens
if cap.Parameters.MaxTokens > 0 && cap.Parameters.MaxTokens <= modelConf.MaxTokens {
to.MaxTokens = cap.Parameters.MaxTokens
}
to.Temperature = float32(cap.Parameters.Temperature)
to.Modalities = append([]string{}, modelConf.Modalities...)
d, _ := time.ParseDuration(strings.TrimSpace(prov.Timeout))
to.Timeout = d
to.Endpoint = prov.Endpoint
choice := ProviderChoice{Provider: cap.Provider, Model: to.Model}
return choice, to, nil
}

View File

@ -1,15 +0,0 @@
package capability
type Ability string
const (
Intent Ability = "intent"
Vision Ability = "vision"
Chat Ability = "chat"
)
type ProviderChoice struct {
Provider string
Model string
}

Some files were not shown because too many files have changed in this diff Show More