This commit is contained in:
parent
26060b7985
commit
7b96c79ab4
|
|
@ -116,8 +116,9 @@ ai_scheduler/
|
|||
| `advicer_wx_msg` | ★ 微信消息流水 | appId, selfWxid, wxid, direction, msgType, content, read |
|
||||
| `advicer_version` | 销售画像 | dialectFeatures, sentencePatterns, toneTags, personalityTags |
|
||||
| `advicer_client` | 客户画像 | clientLevel, personalInfo, purchasePurpose, coreDemands |
|
||||
| `advicer_project` | 项目资料维度 | regionValue, competitionComparison, coreSellingPoints |
|
||||
| `advicer_talk_skill` | 聊天技巧维度 | needsMining, painPointResponse, closingTechniques |
|
||||
| `advicer_project` | 项目资料维度(结构化) | regionValue, competitionComparison, coreSellingPoints, supportingFacilities, developerBacking |
|
||||
| `advicer_project_data` | ★ 项目资料(扁平栏目) | projectId + 动态栏目(key=栏目名, value=内容),与 advicer_project 是**两个独立的集合** |
|
||||
| `advicer_talk_skill` | 聊天技巧维度 | needsMining, painPointResponse, closingTechniques, communicationRhythm, valueBuilding |
|
||||
| `advicer_activity` | 产品活动 | name, content, startAt, endAt |
|
||||
|
||||
### 命名差异
|
||||
|
|
@ -221,8 +222,16 @@ loadLabels()(并行)
|
|||
labelList 为空 → 直接 return,不做回复
|
||||
→ (4) 取 ai_advice_label 中该销售 hosting=1 的标签 ID 列表
|
||||
→ (5) 判断好友 labelList 与托管标签是否有交集,无交集 → return
|
||||
→ (6) 查 ai_advice_customer 取 session_id(无则自动注册;filehelper 不在 customer 表,每次都自动注册)
|
||||
→ (7) hostingChatAndSend(Chat 生成 AI 回复 → 拆段 → 模拟打字延迟 → 分段发送)
|
||||
→ (6) 查 ai_advice_customer 取 session_id
|
||||
→ (7) 无会话 → autoRegisSession 全量加载数据并注册:
|
||||
- 加载版本数据(advicer_version)
|
||||
- 加载销售技巧(advicer_talk_skill,有 HostingSkillId 按 ID 取,无则按 projectId 取最新)
|
||||
- 加载客户画像(advicer_client,按 projectId+advicerId+wxid 查)
|
||||
- 加载项目资料(advicer_project 结构化 + advicer_project_data 扁平栏目)
|
||||
- 构建 ChatData → Regis 存入 Redis(key=chatdata:{sessionId})
|
||||
→ (8) hostingChatAndSend:
|
||||
- 从 Redis 取 ChatData → buildChatPromptResponse 全量注入提示词
|
||||
- Chat 生成 AI 回复 → 拆段 → 模拟打字延迟 → 分段发送
|
||||
```
|
||||
|
||||
**关键设计**:
|
||||
|
|
@ -230,7 +239,45 @@ loadLabels()(并行)
|
|||
- projectInfo 提前到步骤 2 查询(后续 getDetailInfo 和 Chat 都需要 wxToken)
|
||||
- 客户昵称优先使用微信备注名(Remark),回退到 NickName,最后才用 customer 表中的昵称
|
||||
- customer 表仅用于存取 session_id,不再承担标签判断职责
|
||||
- 文件传输助手(filehelper)特殊路径:销售开启 `reply_filehelper` 后,跳过标签检查,直接作为客户进行 AI 回复;由于 filehelper 是微信系统账号,不在 customer 表,因此不写回 session_id,每条消息都新建一个会话
|
||||
- 文件传输助手(filehelper)特殊路径:销售开启 `reply_filehelper` 后,跳过标签检查,直接作为客户进行 AI 回复
|
||||
|
||||
### ChatData 与全量提示词注入
|
||||
|
||||
**ChatData 结构**(`entitys/advicer.go`):
|
||||
```go
|
||||
type ChatData struct {
|
||||
ClientInfo *AdvicerClientMongoEntity // 客户画像(advicer_client)
|
||||
TalkSkill *AdvicerTalkSkillMongoEntity // 销售技巧(advicer_talk_skill)
|
||||
ProjectInfo *AdvicerProjectMongoEntity // 项目资料-结构化(advicer_project)
|
||||
ProjectData map[string]interface{} // 项目资料-扁平栏目(advicer_project_data)
|
||||
AdvicerInfo *AiAdviceAdvicerEntity // 销售信息(MySQL)
|
||||
AdvicerVersion map[string]interface{} // 销售人设维度数据(advicer_version)
|
||||
RuleDimension string // 项目风控红线
|
||||
}
|
||||
```
|
||||
|
||||
**提示词构建流程**(`buildChatPromptResponse`):
|
||||
```
|
||||
System Prompt = BasePrompt + Mission + RuleDimension + BasePrompt2
|
||||
+ [项目信息] (advicer_project JSON)
|
||||
+ [项目资料] (advicer_project_data JSON)
|
||||
+ [销售信息] (advicer MySQL JSON)
|
||||
+ [销售人设风格] (advicer_version JSON)
|
||||
+ [客户信息] (advicer_client JSON)
|
||||
+ [销售技巧] (advicer_talk_skill JSON)
|
||||
+ [输出格式] (永远在最后)
|
||||
---
|
||||
System Prompt = 聊天记录(客户/我 对话格式)
|
||||
---
|
||||
User Message = 当前消息
|
||||
```
|
||||
|
||||
**数据流**:
|
||||
- Regis 时:`autoRegisSession` 全量加载所有数据 → 构建 ChatData → `json.Marshal` 存入 Redis(key=`chatdata:{sessionId}`,TTL 1h)
|
||||
- Chat 时:从 Redis 取 ChatData → `compactJSON`(递归去空值省 token)→ 注入 system prompt
|
||||
- `compactJSON`:序列化前递归去除空字符串、null、空数组、空对象,减少无效 token
|
||||
|
||||
**注意**:`advicer_project` 和 `advicer_project_data` 是**两个独立的 MongoDB 集合**,前者存结构化维度(区域价值/竞品对比等),后者存扁平栏目数据(key=栏目名)。两者都需要加载。
|
||||
|
||||
**模拟真人发送算法**:
|
||||
- AI 回复拆分为多条短消息(每条 ≤80 字,按换行/句末标点拆分)
|
||||
|
|
@ -251,7 +298,7 @@ loadLabels()(并行)
|
|||
|
||||
---
|
||||
|
||||
## 当前开发状态(截至 2026-09-25)
|
||||
## 当前开发状态(截至 2026-09-27)
|
||||
|
||||
### 已完成
|
||||
|
||||
|
|
@ -268,7 +315,10 @@ loadLabels()(并行)
|
|||
- [x] 托管自动回复:注册会话 → Chat → 拆段 → 模拟真人打字延迟 → 分段发送
|
||||
- [x] 项目级 wx_token 支持:SendTextWithToken 方法,托管场景使用项目级 token
|
||||
- [x] 查询优化:消除重复查询(projectInfo/advicer/session),新注册路径从 ~16 次降至 ~10 次 DB
|
||||
- [x] 文件传输助手自动回复:销售级开关 `reply_filehelper`,开启后 peerWxid=filehelper 跳过标签检查直接作为客户进行 AI 回复
|
||||
- [x] 文件传输助手自动回复:销售级开关 `reply_filehelper`,开启后跳过标签检查直接 AI 回复
|
||||
- [x] filehelper 普通客户化改造:走标准 customer 流程复用 session_id
|
||||
- [x] ChatData 全量数据注入:Regis 时加载全部数据(项目/销售/客户/技巧/人设),Chat 时从 Redis 取出全量注入提示词
|
||||
- [x] 提示词 token 优化:`compactJSON` 递归去空值,减少无效 token
|
||||
|
||||
### 待开发(下一步)
|
||||
|
||||
|
|
@ -358,6 +408,10 @@ loadLabels()(并行)
|
|||
12. **会话/消息查询用 `selfWxid` 筛选** — 不用 `appId`(appId 会变)
|
||||
13. **WebSocket 订阅 appId 必须与广播 appId 一致** — 前端用 `wx_device_id`,后端用回调的 `Appid` 字段
|
||||
14. **历史遗留代码**(`internal/biz/handle`、`llm_service`)是死代码,可忽略
|
||||
15. **`advicer_project` 和 `advicer_project_data` 是两个独立的 MongoDB 集合** — 前者存结构化维度(RegionValue/CompetitionComparison 等),后者存扁平栏目数据(key=栏目名, value=内容)。加载项目数据时两者都要查
|
||||
16. **HostingSkillId 为空时取最新** — 用 `VersionList(projectId)` 取第一条,不是跳过
|
||||
17. **PreviousResponseID 续写不继承 system prompt** — Chat 时必须从 Redis 取 ChatData 重新注入全部数据,不能依赖 context cache 续写
|
||||
18. **修改 JS/CSS 后必须更新版本号** — 否则浏览器用旧缓存,看起来像没改
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -379,4 +433,4 @@ docker-compose up -d
|
|||
|
||||
---
|
||||
|
||||
*最后更新:2026-09-25*
|
||||
*最后更新:2026-09-27*
|
||||
|
|
|
|||
|
|
@ -102,6 +102,14 @@ func (a *AdviceAdvicerBiz) UpdateWxDeviceId(ctx context.Context, advicerId int32
|
|||
return a.advicerImpl.UpdateByCond(&cond, updates)
|
||||
}
|
||||
|
||||
// UpdateWxId 更新销售的登录微信 wxid
|
||||
func (a *AdviceAdvicerBiz) UpdateWxId(ctx context.Context, advicerId int32, wxId string) error {
|
||||
updates := map[string]interface{}{"wx_id": wxId}
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"advicer_id": advicerId})
|
||||
return a.advicerImpl.UpdateByCond(&cond, updates)
|
||||
}
|
||||
|
||||
// HostingUpdate 更新销售的托管配置
|
||||
func (a *AdviceAdvicerBiz) HostingUpdate(ctx context.Context, req *entitys.AdvicerHostingUpdateReq) error {
|
||||
updates := map[string]interface{}{
|
||||
|
|
|
|||
|
|
@ -146,3 +146,13 @@ func (a *AdviceCustomerBiz) FindBySelfWxidAndUserName(ctx context.Context, selfW
|
|||
func (a *AdviceCustomerBiz) UpdateSessionId(ctx context.Context, customerId int32, sessionId string) error {
|
||||
return a.impl.UpdateSessionId(ctx, customerId, sessionId)
|
||||
}
|
||||
|
||||
// FindBySelfWxids 查询多个 selfWxid 下的所有客户记录
|
||||
func (a *AdviceCustomerBiz) FindBySelfWxids(ctx context.Context, selfWxids []string) ([]model.AiAdviceCustomer, error) {
|
||||
return a.impl.FindBySelfWxids(ctx, selfWxids)
|
||||
}
|
||||
|
||||
// UpdateFromWxDetail 根据微信好友详情更新 ai_advice_customer 记录
|
||||
func (a *AdviceCustomerBiz) UpdateFromWxDetail(ctx context.Context, selfWxid, userName string, updates map[string]interface{}) error {
|
||||
return a.impl.UpdateFromWxDetail(ctx, selfWxid, userName, updates)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ type AdviceProactiveBiz struct {
|
|||
activityBiz *AdviceActivityBiz
|
||||
strategyBiz *AdviceStrategyBiz
|
||||
sendBiz *AdviceWxSendBiz
|
||||
projectBiz *AdviceProjectBiz
|
||||
}
|
||||
|
||||
func NewAdviceProactiveBiz(
|
||||
|
|
@ -45,6 +46,7 @@ func NewAdviceProactiveBiz(
|
|||
activityBiz *AdviceActivityBiz,
|
||||
strategyBiz *AdviceStrategyBiz,
|
||||
sendBiz *AdviceWxSendBiz,
|
||||
projectBiz *AdviceProjectBiz,
|
||||
) *AdviceProactiveBiz {
|
||||
return &AdviceProactiveBiz{
|
||||
cfg: cfg,
|
||||
|
|
@ -54,9 +56,20 @@ func NewAdviceProactiveBiz(
|
|||
activityBiz: activityBiz,
|
||||
strategyBiz: strategyBiz,
|
||||
sendBiz: sendBiz,
|
||||
projectBiz: projectBiz,
|
||||
}
|
||||
}
|
||||
|
||||
// getProjectWxToken 获取项目级 wx_token,优先项目配置,回退全局配置
|
||||
func (a *AdviceProactiveBiz) getProjectWxToken(ctx context.Context, projectId int32) string {
|
||||
if a.projectBiz != nil && projectId != 0 {
|
||||
if info, err := a.projectBiz.BaseInfo(projectId); err == nil && info.WxToken != "" {
|
||||
return info.WxToken
|
||||
}
|
||||
}
|
||||
return a.cfg.Advicer.WxToken
|
||||
}
|
||||
|
||||
// Scan 主动触达扫描(生日祝福 + 新活动推送),定时任务每小时调用,也可后台手动触发。
|
||||
// 自动链路:受 AutoReply 总开关与活跃时段(active_hour_start/end)限制;Force=true 忽略两者。
|
||||
func (a *AdviceProactiveBiz) Scan(ctx context.Context, param *entitys.AdvicerProactiveRunReq) (res *entitys.AdvicerProactiveRunRes, err error) {
|
||||
|
|
@ -72,9 +85,7 @@ func (a *AdviceProactiveBiz) Scan(ctx context.Context, param *entitys.AdvicerPro
|
|||
return res, nil
|
||||
}
|
||||
}
|
||||
if !a.sendBiz.Available() {
|
||||
return res, errors.New("未配置 advicer.wx_token,无法发送消息")
|
||||
}
|
||||
// wx_token 从项目级获取,此处不再做全局 Available 检查
|
||||
|
||||
filter := bson.M{"aiEnabled": true, "wxid": bson.M{"$ne": ""}}
|
||||
if param.ProjectId != 0 {
|
||||
|
|
@ -110,7 +121,7 @@ func (a *AdviceProactiveBiz) Scan(ctx context.Context, param *entitys.AdvicerPro
|
|||
}
|
||||
// 1) 生日触达
|
||||
if normalizeBirthday(item.PersonalInfo.Birthday) == todayMD {
|
||||
stat, detail := a.sendBirthday(ctx, &item, param)
|
||||
stat, detail := a.sendBirthday(ctx, &item, param, a.getProjectWxToken(ctx, item.ProjectId))
|
||||
if detail != "" {
|
||||
res.Details = append(res.Details, detail)
|
||||
}
|
||||
|
|
@ -124,7 +135,7 @@ func (a *AdviceProactiveBiz) Scan(ctx context.Context, param *entitys.AdvicerPro
|
|||
}
|
||||
}
|
||||
// 2) 活动触达
|
||||
stat, detail := a.sendActivityForClient(ctx, &item, param)
|
||||
stat, detail := a.sendActivityForClient(ctx, &item, param, a.getProjectWxToken(ctx, item.ProjectId))
|
||||
if detail != "" {
|
||||
res.Details = append(res.Details, detail)
|
||||
}
|
||||
|
|
@ -141,7 +152,7 @@ func (a *AdviceProactiveBiz) Scan(ctx context.Context, param *entitys.AdvicerPro
|
|||
}
|
||||
|
||||
// sendBirthday 生日祝福触达(同一客户每年度一次)
|
||||
func (a *AdviceProactiveBiz) sendBirthday(ctx context.Context, c *mongo_model.AdvicerClientItem, param *entitys.AdvicerProactiveRunReq) (stat int, detail string) {
|
||||
func (a *AdviceProactiveBiz) sendBirthday(ctx context.Context, c *mongo_model.AdvicerClientItem, param *entitys.AdvicerProactiveRunReq, wxToken string) (stat int, detail string) {
|
||||
clientId := c.Id.Hex()
|
||||
refId := time.Now().Format("2006")
|
||||
if a.hasLog(ctx, clientId, mongo_model.ProactiveTypeBirthday, refId) {
|
||||
|
|
@ -153,12 +164,15 @@ func (a *AdviceProactiveBiz) sendBirthday(ctx context.Context, c *mongo_model.Ad
|
|||
if param.DryRun {
|
||||
return proactiveSkipped, clientName(c) + ":今日生日,待发送祝福(dry-run)"
|
||||
}
|
||||
if len(wxToken) == 0 {
|
||||
return proactiveFailed, clientName(c) + ":项目未配置 wx_token,无法发送消息"
|
||||
}
|
||||
replies, err := a.strategyBiz.GenerateReply(ctx, c, constants.StrategyBirthdayMission, "", "")
|
||||
if err != nil || len(replies) == 0 {
|
||||
a.logContact(ctx, c, mongo_model.ProactiveTypeBirthday, refId, "", err)
|
||||
return proactiveFailed, fmt.Sprintf("%s:生日祝福生成失败: %v", clientName(c), err)
|
||||
}
|
||||
sentList, e := a.sendBiz.SendMulti(ctx, c.AppId, c.Wxid, replies, mongo_model.WxMsgSourceWeb)
|
||||
sentList, e := a.sendBiz.SendMultiWithToken(ctx, wxToken, c.AppId, c.Wxid, replies, mongo_model.WxMsgSourceWeb)
|
||||
a.logContact(ctx, c, mongo_model.ProactiveTypeBirthday, refId, strings.Join(sentList, "\n"), e)
|
||||
if e != nil {
|
||||
return proactiveFailed, fmt.Sprintf("%s:生日祝福发送失败: %v", clientName(c), e)
|
||||
|
|
@ -167,7 +181,7 @@ func (a *AdviceProactiveBiz) sendBirthday(ctx context.Context, c *mongo_model.Ad
|
|||
}
|
||||
|
||||
// sendActivityForClient 活动推送触达(同一客户同一活动一次,每类型每天一次)
|
||||
func (a *AdviceProactiveBiz) sendActivityForClient(ctx context.Context, c *mongo_model.AdvicerClientItem, param *entitys.AdvicerProactiveRunReq) (stat int, detail string) {
|
||||
func (a *AdviceProactiveBiz) sendActivityForClient(ctx context.Context, c *mongo_model.AdvicerClientItem, param *entitys.AdvicerProactiveRunReq, wxToken string) (stat int, detail string) {
|
||||
acts, _ := a.activityBiz.ActiveList(ctx, c.ProjectId, c.ClientLevel)
|
||||
if len(acts) == 0 {
|
||||
return proactiveNone, ""
|
||||
|
|
@ -185,19 +199,22 @@ func (a *AdviceProactiveBiz) sendActivityForClient(ctx context.Context, c *mongo
|
|||
if param.DryRun {
|
||||
return proactiveSkipped, fmt.Sprintf("%s:待推送活动「%s」(dry-run)", clientName(c), act.Name)
|
||||
}
|
||||
return a.pushActivity(ctx, c, act.AdvicerActivityMongo, refId)
|
||||
return a.pushActivity(ctx, c, act.AdvicerActivityMongo, refId, wxToken)
|
||||
}
|
||||
return proactiveNone, ""
|
||||
}
|
||||
|
||||
// pushActivity 生成活动推送内容并发送(单客户),返回结果状态
|
||||
func (a *AdviceProactiveBiz) pushActivity(ctx context.Context, c *mongo_model.AdvicerClientItem, act mongo_model.AdvicerActivityMongo, refId string) (stat int, detail string) {
|
||||
func (a *AdviceProactiveBiz) pushActivity(ctx context.Context, c *mongo_model.AdvicerClientItem, act mongo_model.AdvicerActivityMongo, refId string, wxToken string) (stat int, detail string) {
|
||||
if len(wxToken) == 0 {
|
||||
return proactiveFailed, clientName(c) + ":项目未配置 wx_token,无法发送消息"
|
||||
}
|
||||
replies, err := a.strategyBiz.GenerateReply(ctx, c, constants.StrategyProactiveActivityMission, buildActivityCtx(act), "")
|
||||
if err != nil || len(replies) == 0 {
|
||||
a.logContact(ctx, c, mongo_model.ProactiveTypeActivity, refId, "", err)
|
||||
return proactiveFailed, fmt.Sprintf("%s:活动推送生成失败: %v", clientName(c), err)
|
||||
}
|
||||
sentList, e := a.sendBiz.SendMulti(ctx, c.AppId, c.Wxid, replies, mongo_model.WxMsgSourceWeb)
|
||||
sentList, e := a.sendBiz.SendMultiWithToken(ctx, wxToken, c.AppId, c.Wxid, replies, mongo_model.WxMsgSourceWeb)
|
||||
a.logContact(ctx, c, mongo_model.ProactiveTypeActivity, refId, strings.Join(sentList, "\n"), e)
|
||||
if e != nil {
|
||||
return proactiveFailed, fmt.Sprintf("%s:活动推送发送失败: %v", clientName(c), e)
|
||||
|
|
@ -215,9 +232,7 @@ func (a *AdviceProactiveBiz) PushActivity(ctx context.Context, param *entitys.Ad
|
|||
if err != nil {
|
||||
return res, fmt.Errorf("活动不存在: %w", err)
|
||||
}
|
||||
if !a.sendBiz.Available() {
|
||||
return res, errors.New("未配置 advicer.wx_token,无法发送消息")
|
||||
}
|
||||
// wx_token 从项目级获取,不再做全局 Available 检查
|
||||
|
||||
filter := bson.M{"aiEnabled": true, "wxid": bson.M{"$ne": ""}}
|
||||
if act.ProjectId != 0 {
|
||||
|
|
@ -264,7 +279,8 @@ func (a *AdviceProactiveBiz) PushActivity(ctx context.Context, param *entitys.Ad
|
|||
res.Details = append(res.Details, fmt.Sprintf("%s:待推送活动「%s」(dry-run)", clientName(&item), act.Name))
|
||||
continue
|
||||
}
|
||||
stat, detail := a.pushActivity(ctx, &item, act, param.Id)
|
||||
wxToken := a.getProjectWxToken(ctx, item.ProjectId)
|
||||
stat, detail := a.pushActivity(ctx, &item, act, param.Id, wxToken)
|
||||
if detail != "" {
|
||||
res.Details = append(res.Details, detail)
|
||||
}
|
||||
|
|
@ -302,9 +318,7 @@ func (a *AdviceProactiveBiz) SendToClient(ctx context.Context, param *entitys.Ad
|
|||
if len(client.Wxid) == 0 {
|
||||
return res, errors.New("客户未绑定微信 wxid")
|
||||
}
|
||||
if !a.sendBiz.Available() {
|
||||
return res, errors.New("未配置 advicer.wx_token,无法发送消息")
|
||||
}
|
||||
// wx_token 从项目级获取,不再做全局 Available 检查
|
||||
item := &mongo_model.AdvicerClientItem{AdvicerClientMongo: client}
|
||||
|
||||
var replies []string
|
||||
|
|
@ -343,7 +357,11 @@ func (a *AdviceProactiveBiz) SendToClient(ctx context.Context, param *entitys.Ad
|
|||
if len(replies) == 0 {
|
||||
return res, errors.New("未生成可发送内容")
|
||||
}
|
||||
sentList, e := a.sendBiz.SendMulti(ctx, client.AppId, client.Wxid, replies, mongo_model.WxMsgSourceWeb)
|
||||
wxToken := a.getProjectWxToken(ctx, client.ProjectId)
|
||||
if len(wxToken) == 0 {
|
||||
return res, errors.New("项目未配置 wx_token,无法发送消息")
|
||||
}
|
||||
sentList, e := a.sendBiz.SendMultiWithToken(ctx, wxToken, client.AppId, client.Wxid, replies, mongo_model.WxMsgSourceWeb)
|
||||
a.logContactByWxid(ctx, client, refId, strings.Join(sentList, "\n"), e)
|
||||
if e != nil {
|
||||
return res, e
|
||||
|
|
|
|||
|
|
@ -272,6 +272,9 @@ func (a *AdviceProjectBiz) BaseInfo(projectId int32) (baseInfo model.AiAdvicePro
|
|||
if projectId == 0 {
|
||||
return
|
||||
}
|
||||
if a.adviceProjectImpl == nil {
|
||||
return baseInfo, errors.New("adviceProjectImpl 未初始化")
|
||||
}
|
||||
cond := builder.NewCond()
|
||||
cond = cond.And(builder.Eq{"project_id": projectId})
|
||||
err = a.adviceProjectImpl.GetOneBySearchToStrut(&cond, &baseInfo)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -342,6 +343,40 @@ func (a *AdviceWxBiz) tryHostingAutoRegis(ctx context.Context, appId, selfWxid,
|
|||
return
|
||||
}
|
||||
|
||||
// 5.5 更新客户信息到 ai_advice_customer(保持客户数据与微信实时同步)
|
||||
updateMap := map[string]interface{}{
|
||||
"nick_name": detail.NickName,
|
||||
"py_initial": detail.PyInitial,
|
||||
"quan_pin": detail.QuanPin,
|
||||
"sex": detail.Sex,
|
||||
"remark": detail.Remark,
|
||||
"remark_py_initial": detail.RemarkPyInitial,
|
||||
"remark_quan_pin": detail.RemarkQuanPin,
|
||||
"signature": detail.Signature,
|
||||
"alias": detail.Alias,
|
||||
"big_head_img_url": detail.BigHeadImgUrl,
|
||||
"small_head_img_url": detail.SmallHeadImgUrl,
|
||||
"label_list": friendLabelList,
|
||||
"province": detail.Province,
|
||||
"city": detail.City,
|
||||
}
|
||||
switch v := detail.PhoneNumList.(type) {
|
||||
case string:
|
||||
updateMap["phone_num_list"] = cleanPhoneStr(v)
|
||||
case []interface{}:
|
||||
var phones []string
|
||||
for _, p := range v {
|
||||
if s, ok := p.(string); ok {
|
||||
s = cleanPhoneStr(s)
|
||||
if s != "" {
|
||||
phones = append(phones, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
updateMap["phone_num_list"] = strings.Join(phones, ",")
|
||||
}
|
||||
_ = a.adviceCustomerBiz.UpdateFromWxDetail(ctx, selfWxid, peerWxid, updateMap)
|
||||
|
||||
// 6. 查 ai_advice_customer 取 session_id
|
||||
customer, err := a.adviceCustomerBiz.FindBySelfWxidAndUserName(ctx, selfWxid, peerWxid)
|
||||
if err != nil {
|
||||
|
|
@ -957,6 +992,14 @@ func toMap(b []byte) map[string]interface{} {
|
|||
return m
|
||||
}
|
||||
|
||||
// cleanPhoneRe 匹配控制字符(ASCII 0x00-0x1F 和 0x7F)
|
||||
var cleanPhoneRe = regexp.MustCompile(`[\x00-\x1f\x7f]`)
|
||||
|
||||
// cleanPhoneStr 清除手机号中的控制字符并去除首尾空白
|
||||
func cleanPhoneStr(s string) string {
|
||||
return strings.TrimSpace(cleanPhoneRe.ReplaceAllString(s, ""))
|
||||
}
|
||||
|
||||
// parseTimeStr 容错解析时间字符串(支持多种常见格式)
|
||||
func parseTimeStr(s string) (time.Time, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
|
|
|
|||
|
|
@ -78,3 +78,27 @@ func (a *AdviceCustomerImpl) UpdateSessionId(ctx context.Context, customerId int
|
|||
Where("customer_id = ?", customerId).
|
||||
Update("session_id", sessionId).Error
|
||||
}
|
||||
|
||||
// FindBySelfWxids 查询多个 selfWxid 下的所有客户记录
|
||||
func (a *AdviceCustomerImpl) FindBySelfWxids(ctx context.Context, selfWxids []string) ([]model.AiAdviceCustomer, error) {
|
||||
if len(selfWxids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var list []model.AiAdviceCustomer
|
||||
err := a.Db.WithContext(ctx).
|
||||
Where("self_wxid IN ?", selfWxids).
|
||||
Order("create_at desc").
|
||||
Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
// UpdateFromWxDetail 根据微信好友详情更新客户记录(回调时保持客户信息最新)
|
||||
func (a *AdviceCustomerImpl) UpdateFromWxDetail(ctx context.Context, selfWxid, userName string, updates map[string]interface{}) error {
|
||||
if len(updates) == 0 {
|
||||
return nil
|
||||
}
|
||||
return a.Db.WithContext(ctx).
|
||||
Model(&model.AiAdviceCustomer{}).
|
||||
Where("self_wxid = ? AND user_name = ?", selfWxid, userName).
|
||||
Updates(updates).Error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type AiAdviceAdvicer struct {
|
|||
WorkingYears int32 `gorm:"column:working_years;not null;default:1;comment:工作年限" json:"working_years"` // 工作年限
|
||||
JoinDate time.Time `gorm:"column:join_date;comment:入职时间" json:"join_date"` // 入职时间
|
||||
WxDeviceId string `gorm:"column:wx_device_id;comment:微信设备ID" json:"wx_device_id"` // 微信设备ID
|
||||
WxId string `gorm:"column:wx_id;comment:登录微信wxid" json:"wx_id"` // 登录微信wxid
|
||||
HostingEnabled int32 `gorm:"column:hosting_enabled;not null;default:0;comment:托管开关 0关 1开" json:"hosting_enabled"`
|
||||
HostingVersionId string `gorm:"column:hosting_version_id;not null;default:'';comment:指定版本ID,空=最新" json:"hosting_version_id"`
|
||||
HostingSkillId string `gorm:"column:hosting_skill_id;not null;default:'';comment:指定技巧ID,空=最新" json:"hosting_skill_id"`
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ type AdvicerWxDeviceIdUpdateReq struct {
|
|||
WxDeviceId string `json:"wxDeviceId"`
|
||||
}
|
||||
|
||||
// AdvicerWxIdUpdateReq 更新销售的登录微信 wxid
|
||||
type AdvicerWxIdUpdateReq struct {
|
||||
AdvicerId int32 `json:"advicerId" validate:"required" zh:"销售ID"`
|
||||
WxId string `json:"wxId"`
|
||||
}
|
||||
|
||||
// AdvicerHostingUpdateReq 更新销售的托管配置
|
||||
type AdvicerHostingUpdateReq struct {
|
||||
AdvicerId int32 `json:"advicerId" validate:"required" zh:"销售ID"`
|
||||
|
|
@ -535,6 +541,18 @@ type AdvicerCustomerUpdateLabelsReq struct {
|
|||
LabelList string `json:"labelList"`
|
||||
}
|
||||
|
||||
// AdvicerCustomerHostedListReq 查询托管客户列表
|
||||
type AdvicerCustomerHostedListReq struct {
|
||||
ProjectId int32 `json:"projectId" validate:"required" zh:"项目ID"`
|
||||
}
|
||||
|
||||
// HostedCustomerItem 托管客户条目(包含客户信息 + 标签名 + 销售名)
|
||||
type HostedCustomerItem struct {
|
||||
model.AiAdviceCustomer
|
||||
LabelNames string `json:"label_names"` // 托管标签名称(逗号分隔)
|
||||
AdvicerName string `json:"advicer_name"` // 归属销售姓名
|
||||
}
|
||||
|
||||
// ==================== 标签同步 ====================
|
||||
|
||||
// AdvicerLabelSyncReq 标签同步请求(前端从微信获取标签后提交)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func AdvicerRouterRegist(
|
|||
advicer.Post("advicer/add", adviceData.AdvicerUpdate)
|
||||
advicer.Post("advicer/update", adviceData.AdvicerUpdate)
|
||||
advicer.Post("advicer/wx_device/update", adviceData.AdvicerWxDeviceIdUpdate)
|
||||
advicer.Post("advicer/wx_id/update", adviceData.AdvicerWxIdUpdate)
|
||||
advicer.Post("advicer/hosting/update", adviceData.HostingUpdate)
|
||||
advicer.Post("advicer/list", adviceData.AdvicerList)
|
||||
advicer.Post("advicer/info", adviceData.AdvicerInfo)
|
||||
|
|
@ -132,6 +133,7 @@ func AdvicerRouterRegist(
|
|||
advicer.Post("advicer/customer/batch_add", adviceCustomer.BatchAdd)
|
||||
advicer.Post("advicer/customer/batch_update", adviceCustomer.BatchUpdate)
|
||||
advicer.Post("advicer/customer/update_labels", adviceCustomer.UpdateLabels)
|
||||
advicer.Post("advicer/customer/hosted", adviceCustomer.HostedList)
|
||||
|
||||
// 微信标签(ai_advice_label)
|
||||
advicer.Post("advicer/label/list", adviceLabel.List)
|
||||
|
|
|
|||
|
|
@ -71,6 +71,19 @@ func (d *AdvicerService) AdvicerWxDeviceIdUpdate(c *fiber.Ctx) error {
|
|||
return pkg.HandleResponse(c, nil, err)
|
||||
}
|
||||
|
||||
// AdvicerWxIdUpdate 更新销售的登录微信 wxid
|
||||
func (d *AdvicerService) AdvicerWxIdUpdate(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerWxIdUpdateReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.AdvicerId == 0 {
|
||||
return errorcode.ParamErr("销售ID不能为空")
|
||||
}
|
||||
err := d.adviceBiz.UpdateWxId(c.UserContext(), req.AdvicerId, req.WxId)
|
||||
return pkg.HandleResponse(c, nil, err)
|
||||
}
|
||||
|
||||
// HostingUpdate 更新销售的托管配置
|
||||
func (d *AdvicerService) HostingUpdate(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerHostingUpdateReq{}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"ai_scheduler/internal/pkg"
|
||||
"ai_scheduler/internal/pkg/wx"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
|
@ -16,10 +17,11 @@ type CustomerService struct {
|
|||
customerBiz *biz.AdviceCustomerBiz
|
||||
labelBiz *biz.AdviceLabelBiz
|
||||
projectBiz *biz.AdviceProjectBiz
|
||||
advicerBiz *biz.AdviceAdvicerBiz
|
||||
}
|
||||
|
||||
func NewCustomerService(customerBiz *biz.AdviceCustomerBiz, labelBiz *biz.AdviceLabelBiz, projectBiz *biz.AdviceProjectBiz) *CustomerService {
|
||||
return &CustomerService{customerBiz: customerBiz, labelBiz: labelBiz, projectBiz: projectBiz}
|
||||
func NewCustomerService(customerBiz *biz.AdviceCustomerBiz, labelBiz *biz.AdviceLabelBiz, projectBiz *biz.AdviceProjectBiz, advicerBiz *biz.AdviceAdvicerBiz) *CustomerService {
|
||||
return &CustomerService{customerBiz: customerBiz, labelBiz: labelBiz, projectBiz: projectBiz, advicerBiz: advicerBiz}
|
||||
}
|
||||
|
||||
// Add 新增单条好友
|
||||
|
|
@ -134,3 +136,89 @@ func (s *CustomerService) UpdateLabels(c *fiber.Ctx) error {
|
|||
|
||||
return pkg.HandleResponse(c, nil, nil)
|
||||
}
|
||||
|
||||
// HostedList 查询项目下所有托管客户(在托管标签分组内的客户)
|
||||
func (s *CustomerService) HostedList(c *fiber.Ctx) error {
|
||||
req := &entitys.AdvicerCustomerHostedListReq{}
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := c.UserContext()
|
||||
|
||||
// 1. 查项目下所有销售
|
||||
advicers, err := s.advicerBiz.List(ctx, &entitys.AdvicerListReq{ProjectId: req.ProjectId})
|
||||
if err != nil {
|
||||
return pkg.HandleResponse(c, nil, err)
|
||||
}
|
||||
|
||||
// 构建 selfWxid -> advicerName 映射,收集所有 selfWxid
|
||||
// 关联逻辑:advicer.wx_id = ai_advice_customer.self_wxid
|
||||
advicerMap := make(map[string]string)
|
||||
var selfWxids []string
|
||||
for _, adv := range advicers {
|
||||
wxId, _ := adv["wx_id"].(string)
|
||||
name, _ := adv["name"].(string)
|
||||
if wxId != "" {
|
||||
advicerMap[wxId] = name
|
||||
selfWxids = append(selfWxids, wxId)
|
||||
}
|
||||
}
|
||||
if len(selfWxids) == 0 {
|
||||
return pkg.HandleResponse(c, []entitys.HostedCustomerItem{}, nil)
|
||||
}
|
||||
|
||||
// 2. 获取每个销售的托管标签 ID,构建 selfWxid -> hostingLabelIds 和 labelId -> labelName 映射
|
||||
hostingLabelsMap := make(map[string]map[int]bool)
|
||||
labelNameMap := make(map[int]string)
|
||||
for _, selfWxid := range selfWxids {
|
||||
hostIds, _ := s.labelBiz.GetHostingLabelIds(ctx, selfWxid)
|
||||
if len(hostIds) == 0 {
|
||||
continue
|
||||
}
|
||||
hostingLabelsMap[selfWxid] = make(map[int]bool)
|
||||
for _, id := range hostIds {
|
||||
hostingLabelsMap[selfWxid][id] = true
|
||||
}
|
||||
labels, _ := s.labelBiz.List(ctx, &entitys.AdvicerLabelListReq{SelfWxid: selfWxid})
|
||||
for _, l := range labels {
|
||||
labelNameMap[int(l.LabelId)] = l.LabelName
|
||||
}
|
||||
}
|
||||
if len(hostingLabelsMap) == 0 {
|
||||
return pkg.HandleResponse(c, []entitys.HostedCustomerItem{}, nil)
|
||||
}
|
||||
|
||||
// 3. 查询这些 selfWxid 下的所有客户,筛选出在托管标签下的
|
||||
allCustomers, err := s.customerBiz.FindBySelfWxids(ctx, selfWxids)
|
||||
if err != nil {
|
||||
return pkg.HandleResponse(c, nil, err)
|
||||
}
|
||||
|
||||
var result []entitys.HostedCustomerItem
|
||||
for _, cust := range allCustomers {
|
||||
hostLabels, ok := hostingLabelsMap[cust.SelfWxid]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// 判断客户的 label_list 与托管标签是否有交集
|
||||
var matchedNames []string
|
||||
for _, ls := range strings.Split(cust.LabelList, ",") {
|
||||
id, e := strconv.Atoi(strings.TrimSpace(ls))
|
||||
if e == nil && hostLabels[id] {
|
||||
if name, exists := labelNameMap[id]; exists {
|
||||
matchedNames = append(matchedNames, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(matchedNames) == 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, entitys.HostedCustomerItem{
|
||||
AiAdviceCustomer: cust,
|
||||
LabelNames: strings.Join(matchedNames, ","),
|
||||
AdvicerName: advicerMap[cust.SelfWxid],
|
||||
})
|
||||
}
|
||||
|
||||
return pkg.HandleResponse(c, result, nil)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,12 @@ import (
|
|||
// 每个接口使用强类型请求/响应结构体,符合项目本身的输入输出规范
|
||||
type WxProxyService struct {
|
||||
projectBiz *biz.AdviceProjectBiz
|
||||
advicerBiz *biz.AdviceAdvicerBiz
|
||||
}
|
||||
|
||||
// NewWxProxyService 创建微信代理服务
|
||||
func NewWxProxyService(projectBiz *biz.AdviceProjectBiz) *WxProxyService {
|
||||
return &WxProxyService{projectBiz: projectBiz}
|
||||
func NewWxProxyService(projectBiz *biz.AdviceProjectBiz, advicerBiz *biz.AdviceAdvicerBiz) *WxProxyService {
|
||||
return &WxProxyService{projectBiz: projectBiz, advicerBiz: advicerBiz}
|
||||
}
|
||||
|
||||
// wxDispatcher 微信接口调度函数类型
|
||||
|
|
@ -206,10 +207,19 @@ func (s *WxProxyService) Proxy(c *fiber.Ctx) error {
|
|||
return pkg.HandleResponse(c, nil, fiber.NewError(fiber.StatusBadGateway, "微信接口调用失败: "+err.Error()))
|
||||
}
|
||||
|
||||
// TODO: 临时调试日志,排查完成后删除
|
||||
if sub == "contacts/getDetailInfo" {
|
||||
if raw, jerr := json.Marshal(data); jerr == nil {
|
||||
log.Printf("[DEBUG] contacts/getDetailInfo raw response: %s", string(raw))
|
||||
// 登录成功时自动回写销售的 wx_id
|
||||
if sub == "login/checkLogin" {
|
||||
if resData, ok := data.(wx.CheckLoginResData); ok && resData.Status == 2 && resData.LoginInfo != nil && resData.LoginInfo.Wxid != "" {
|
||||
var appId string
|
||||
if v, ok := reqBody["appId"].(string); ok {
|
||||
appId = v
|
||||
}
|
||||
if appId != "" {
|
||||
advicer, err := s.advicerBiz.FindByWxDeviceId(c.UserContext(), appId)
|
||||
if err == nil && advicer.AdvicerID != 0 {
|
||||
_ = s.advicerBiz.UpdateWxId(c.UserContext(), advicer.AdvicerID, resData.LoginInfo.Wxid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
server.exe
BIN
server.exe
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
|||
-- 销售表新增 wx_id 字段(登录微信后回写的个人 wxid)
|
||||
ALTER TABLE ai_advice_advicer ADD COLUMN wx_id VARCHAR(128) NULL DEFAULT '' COMMENT '登录微信wxid' AFTER wx_device_id;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_chat.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
s := string(b)
|
||||
|
||||
// Add diagnostic log at the start of formatPersonaPrompt, after the nil check
|
||||
old := ` if v == nil || len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder`
|
||||
|
||||
new := ` if v == nil || len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
// 诊断:打印每个维度的实际类型
|
||||
for k, val := range v {
|
||||
log.Infof("[人设诊断] key=%q type=%T val=%v", k, val, fmt.Sprintf("%.80v", val))
|
||||
}
|
||||
var b strings.Builder`
|
||||
|
||||
if strings.Contains(s, old) {
|
||||
s = strings.Replace(s, old, new, 1)
|
||||
os.WriteFile(f, []byte(s), 0644)
|
||||
println("done - added persona diagnostic log")
|
||||
} else {
|
||||
println("ERROR: block not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Check advice_wx.go for project info code
|
||||
f1 := "internal/biz/advice_wx.go"
|
||||
b1, _ := os.ReadFile(f1)
|
||||
lines1 := strings.Split(string(b1), "\n")
|
||||
for i, line := range lines1 {
|
||||
if strings.Contains(line, "ProjectInfo") || strings.Contains(line, "项目信息") || strings.Contains(line, "projectDesc") {
|
||||
visible := strings.ReplaceAll(lines1[i], "\t", "→")
|
||||
fmt.Printf("wx L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("---")
|
||||
|
||||
// Check advice_chat.go for persona nil check
|
||||
f2 := "internal/biz/advice_chat.go"
|
||||
b2, _ := os.ReadFile(f2)
|
||||
lines2 := strings.Split(string(b2), "\n")
|
||||
for i, line := range lines2 {
|
||||
if strings.Contains(line, "v == nil") || strings.Contains(line, "var b strings.Builder") {
|
||||
if i > 340 && i < 380 {
|
||||
visible := strings.ReplaceAll(lines2[i], "\t", "→")
|
||||
fmt.Printf("chat L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
lines := strings.Split(string(b), "\n")
|
||||
// Show talkSkill loading code
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "talkSkillEntity") {
|
||||
for j := i - 1; j < i+6 && j < len(lines); j++ {
|
||||
visible := strings.ReplaceAll(lines[j], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", j+1, visible)
|
||||
}
|
||||
fmt.Println("---")
|
||||
}
|
||||
}
|
||||
// Show HostingSkillId usage
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "HostingSkillId") {
|
||||
visible := strings.ReplaceAll(lines[i], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// === Fix 1: advice_wx.go - project info from MySQL Base.Name ===
|
||||
f1 := "internal/biz/advice_wx.go"
|
||||
b1, _ := os.ReadFile(f1)
|
||||
lines1 := strings.Split(string(b1), "\n")
|
||||
var out1 []string
|
||||
for i := 0; i < len(lines1); i++ {
|
||||
line := lines1[i]
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// Replace the project info block (L468-L477 area)
|
||||
if trimmed == `if pi := projectInfo.ConfigInfo.ProjectInfo; pi.Name != "" || pi.Address != "" {` {
|
||||
// Get the indent
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, "\t"))]
|
||||
// Replace with new code that uses Base.Name
|
||||
out1 = append(out1, indent+`// 项目信息:优先用 MySQL 的 Base.Name(一定有),MongoDB 的 ProjectInfo 作为补充`)
|
||||
out1 = append(out1, indent+`projectDesc := "\n[项目信息]"`)
|
||||
out1 = append(out1, indent+`hasProjectInfo := false`)
|
||||
out1 = append(out1, indent+`if projectInfo.Base.Name != "" {`)
|
||||
out1 = append(out1, indent+` projectDesc += "\n项目名称:" + projectInfo.Base.Name`)
|
||||
out1 = append(out1, indent+` hasProjectInfo = true`)
|
||||
out1 = append(out1, indent+`}`)
|
||||
out1 = append(out1, indent+`if pi := projectInfo.ConfigInfo.ProjectInfo; pi.Address != "" {`)
|
||||
out1 = append(out1, indent+` projectDesc += "\n项目地址:" + pi.Address`)
|
||||
out1 = append(out1, indent+` hasProjectInfo = true`)
|
||||
out1 = append(out1, indent+`}`)
|
||||
out1 = append(out1, indent+`if hasProjectInfo {`)
|
||||
out1 = append(out1, indent+` mission = mission + projectDesc`)
|
||||
// Skip the old block (find closing })
|
||||
braceCount := 0
|
||||
for i < len(lines1)-1 {
|
||||
i++
|
||||
for _, ch := range lines1[i] {
|
||||
if ch == '{' {
|
||||
braceCount++
|
||||
}
|
||||
if ch == '}' {
|
||||
braceCount--
|
||||
}
|
||||
}
|
||||
if braceCount < 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
out1 = append(out1, line)
|
||||
}
|
||||
os.WriteFile(f1, []byte(strings.Join(out1, "\n")), 0644)
|
||||
println("fix1: project info updated")
|
||||
|
||||
// === Fix 2: advice_chat.go - add persona diagnostic log ===
|
||||
f2 := "internal/biz/advice_chat.go"
|
||||
b2, _ := os.ReadFile(f2)
|
||||
s2 := string(b2)
|
||||
|
||||
old2 := "\tif v == nil || len(v) == 0 {\n\t\treturn \"\"\n\t}\n\tvar b strings.Builder"
|
||||
new2 := "\tif v == nil || len(v) == 0 {\n\t\treturn \"\"\n\t}\n" +
|
||||
"\t// 诊断:打印每个维度的实际类型\n" +
|
||||
"\tfor _k, _val := range v {\n" +
|
||||
"\t\tlog.Infof(\"[人设诊断] key=%q type=%T\", _k, _val)\n" +
|
||||
"\t}\n" +
|
||||
"\tvar b strings.Builder"
|
||||
|
||||
if strings.Contains(s2, old2) {
|
||||
s2 = strings.Replace(s2, old2, new2, 1)
|
||||
os.WriteFile(f2, []byte(s2), 0644)
|
||||
println("fix2: persona diagnostic log added")
|
||||
} else {
|
||||
println("fix2: WARNING - block not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
s := string(b)
|
||||
|
||||
// Fix: add missing closing brace for "if hasProjectInfo"
|
||||
old := "\tif hasProjectInfo {\n\t\tmission = mission + projectDesc\n\t// 将销售人格拼接到 mission"
|
||||
new := "\tif hasProjectInfo {\n\t\tmission = mission + projectDesc\n\t}\n\t// 将销售人格拼接到 mission"
|
||||
|
||||
if strings.Contains(s, old) {
|
||||
s = strings.Replace(s, old, new, 1)
|
||||
os.WriteFile(f, []byte(s), 0644)
|
||||
println("done - fixed missing brace")
|
||||
} else {
|
||||
println("ERROR: block not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
s := string(b)
|
||||
ok := true
|
||||
|
||||
// === 1. autoRegisSession 签名加 peerWxid ===
|
||||
old1 := `func (a *AdviceWxBiz) autoRegisSession(ctx context.Context,
|
||||
advicer dbmodel.AiAdviceAdvicer,
|
||||
projectInfo *entitys.AdvicerProjectInfoRes,
|
||||
customerId int32, customerNickName string,
|
||||
) (string, *dbmodel.AiAdviceSession, *dbmodel.AiAdviceModelSup, error) {`
|
||||
new1 := `func (a *AdviceWxBiz) autoRegisSession(ctx context.Context,
|
||||
advicer dbmodel.AiAdviceAdvicer,
|
||||
projectInfo *entitys.AdvicerProjectInfoRes,
|
||||
customerId int32, customerNickName string,
|
||||
peerWxid string,
|
||||
) (string, *dbmodel.AiAdviceSession, *dbmodel.AiAdviceModelSup, error) {`
|
||||
if strings.Contains(s, old1) {
|
||||
s = strings.Replace(s, old1, new1, 1)
|
||||
println("1. autoRegisSession signature: peerWxid added")
|
||||
} else {
|
||||
println("1. ERROR: signature not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 2. 调用点1(普通托管路径)加 peerWxid ===
|
||||
old2 := `sessionId, cachedSession, cachedModelInfo, err = a.autoRegisSession(ctx, advicer, projectInfo, customer.CustomerID, customerNickName)
|
||||
if err != nil {
|
||||
// autoRegisSession error`
|
||||
new2 := `sessionId, cachedSession, cachedModelInfo, err = a.autoRegisSession(ctx, advicer, projectInfo, customer.CustomerID, customerNickName, peerWxid)
|
||||
if err != nil {
|
||||
// autoRegisSession error`
|
||||
if strings.Contains(s, old2) {
|
||||
s = strings.Replace(s, old2, new2, 1)
|
||||
println("2. call site 1: peerWxid added")
|
||||
} else {
|
||||
println("2. ERROR: call site 1 not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 3. 调用点2(filehelper路径)加 peerWxid ===
|
||||
old3 := `sessionId, cachedSession, cachedModelInfo, err = a.autoRegisSession(ctx, advicer, projectInfo, customer.CustomerID, customerNickName)
|
||||
if err != nil {
|
||||
// filehelper autoRegis error`
|
||||
new3 := `sessionId, cachedSession, cachedModelInfo, err = a.autoRegisSession(ctx, advicer, projectInfo, customer.CustomerID, customerNickName, peerWxid)
|
||||
if err != nil {
|
||||
// filehelper autoRegis error`
|
||||
if strings.Contains(s, old3) {
|
||||
s = strings.Replace(s, old3, new3, 1)
|
||||
println("3. call site 2 (filehelper): peerWxid added")
|
||||
} else {
|
||||
println("3. ERROR: call site 2 not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 4. ChatData 构造:加载 TalkSkill + ClientInfo + 补全 ProjectInfo.Name ===
|
||||
old4 := ` // 构建 ChatData(直接使用传入的 advicer,不再重复查询)
|
||||
chatData := &entitys.ChatData{
|
||||
AdvicerInfo: advicer.Entity(),
|
||||
AdvicerVersion: versionInfo.Data,
|
||||
ProjectInfo: projectInfo.ConfigInfo.Entity(),
|
||||
RuleDimension: projectInfo.Base.RuleDimension,
|
||||
}`
|
||||
new4 := ` // 加载销售技巧
|
||||
var talkSkillEntity *mongo_model.AdvicerTalkSkillMongoEntity
|
||||
if advicer.HostingSkillId != "" {
|
||||
if skillInfo, e := a.adviceSkillBiz.Info(ctx, &entitys.AdvicerTalkSkillInfoReq{Id: advicer.HostingSkillId}); e == nil && skillInfo.ProjectId > 0 {
|
||||
talkSkillEntity = skillInfo.Entity()
|
||||
}
|
||||
}
|
||||
// 加载客户信息(MongoDB advicer_client)
|
||||
var clientEntity *mongo_model.AdvicerClientMongoEntity
|
||||
if peerWxid != "" {
|
||||
var clientDoc mongo_model.AdvicerClientMongo
|
||||
filter := bson.M{"projectId": projectInfo.Base.ProjectID, "advicerId": advicer.AdvicerID, "wxid": peerWxid}
|
||||
if e := a.mongo.Co(mongo_model.NewAdvicerClientMongo()).FindOne(ctx, filter).Decode(&clientDoc); e == nil {
|
||||
clientEntity = clientDoc.Entity()
|
||||
}
|
||||
}
|
||||
// 构建完整 ProjectInfo(确保 MySQL 项目名不丢)
|
||||
projEntity := projectInfo.ConfigInfo.Entity()
|
||||
if projEntity.ProjectInfo.Name == "" && projectInfo.Base.Name != "" {
|
||||
projEntity.ProjectInfo.Name = projectInfo.Base.Name
|
||||
}
|
||||
|
||||
// 构建 ChatData(全量数据注入)
|
||||
chatData := &entitys.ChatData{
|
||||
AdvicerInfo: advicer.Entity(),
|
||||
AdvicerVersion: versionInfo.Data,
|
||||
ProjectInfo: projEntity,
|
||||
RuleDimension: projectInfo.Base.RuleDimension,
|
||||
TalkSkill: talkSkillEntity,
|
||||
ClientInfo: clientEntity,
|
||||
}`
|
||||
if strings.Contains(s, old4) {
|
||||
s = strings.Replace(s, old4, new4, 1)
|
||||
println("4. ChatData: TalkSkill + ClientInfo + ProjectInfo.Name fixed")
|
||||
} else {
|
||||
println("4. ERROR: ChatData block not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
if ok {
|
||||
os.WriteFile(f, []byte(s), 0644)
|
||||
println("\nALL DONE - advice_wx.go updated")
|
||||
} else {
|
||||
println("\nFAILED - some blocks not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_chat.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
s := string(b)
|
||||
|
||||
// Fix buildChatPromptResponse: put output format AFTER everything
|
||||
old := ` Content: a.taskPrompt(session) + outputFormatHint,`
|
||||
new := ` Content: a.taskPrompt(session) + "\n" + outputFormatHint,`
|
||||
|
||||
// Actually the real fix: taskPrompt already has mission, we just need
|
||||
// to ensure outputFormatHint is separated by a newline for clarity.
|
||||
// But the real structural issue is that taskPrompt dumps everything.
|
||||
// Let's restructure: taskPrompt returns context only,
|
||||
// buildChatPromptResponse assembles: context + mission + format
|
||||
|
||||
// Revert taskPrompt to NOT include mission
|
||||
oldTask := ` // 注入 mission(含人格 + 项目信息),确保每次 Chat 模型都能看到
|
||||
if session.Mission != "" {
|
||||
b.WriteString("\n" + session.Mission)
|
||||
}
|
||||
return b.String()`
|
||||
newTask := ` return b.String()`
|
||||
|
||||
if strings.Contains(s, oldTask) {
|
||||
s = strings.Replace(s, oldTask, newTask, 1)
|
||||
println("reverted taskPrompt")
|
||||
}
|
||||
|
||||
// Now fix buildChatPromptResponse to include mission BEFORE output format
|
||||
oldBuild := ` Content: a.taskPrompt(session) + outputFormatHint,`
|
||||
newBuild := ` Content: a.taskPrompt(session) + "\n" + session.Mission + outputFormatHint,`
|
||||
|
||||
if strings.Contains(s, oldBuild) {
|
||||
s = strings.Replace(s, oldBuild, newBuild, 1)
|
||||
println("fixed buildChatPromptResponse order")
|
||||
}
|
||||
|
||||
os.WriteFile(f, []byte(s), 0644)
|
||||
println("done")
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// === 1. ChatData 加 ProjectData 字段 ===
|
||||
f1 := "internal/entitys/advicer.go"
|
||||
b1, _ := os.ReadFile(f1)
|
||||
s1 := string(b1)
|
||||
|
||||
old1 := `type ChatData struct {
|
||||
ClientInfo *mongo_model.AdvicerClientMongoEntity ` + "`json:\"clientInfo\"`" + `
|
||||
TalkSkill *mongo_model.AdvicerTalkSkillMongoEntity ` + "`json:\"talkSkill\"`" + `
|
||||
ProjectInfo *mongo_model.AdvicerProjectMongoEntity ` + "`json:\"projectInfo\"`" + `
|
||||
AdvicerInfo *model.AiAdviceAdvicerEntity ` + "`json:\"advicerInfo\"`" + `
|
||||
AdvicerVersion map[string]interface{} ` + "`json:\"advicerVersion\"`" + ` // 扁平维度数据
|
||||
// RuleDimension 项目风控红线(来自项目模板 rule_dimension,可为空)
|
||||
RuleDimension string ` + "`json:\"ruleDimension\"`" + `
|
||||
}`
|
||||
new1 := `type ChatData struct {
|
||||
ClientInfo *mongo_model.AdvicerClientMongoEntity ` + "`json:\"clientInfo\"`" + `
|
||||
TalkSkill *mongo_model.AdvicerTalkSkillMongoEntity ` + "`json:\"talkSkill\"`" + `
|
||||
ProjectInfo *mongo_model.AdvicerProjectMongoEntity ` + "`json:\"projectInfo\"`" + `
|
||||
ProjectData map[string]interface{} ` + "`json:\"projectData\"`" + ` // advicer_project_data 扁平栏目数据
|
||||
AdvicerInfo *model.AiAdviceAdvicerEntity ` + "`json:\"advicerInfo\"`" + `
|
||||
AdvicerVersion map[string]interface{} ` + "`json:\"advicerVersion\"`" + ` // 扁平维度数据
|
||||
// RuleDimension 项目风控红线(来自项目模板 rule_dimension,可为空)
|
||||
RuleDimension string ` + "`json:\"ruleDimension\"`" + `
|
||||
}`
|
||||
if strings.Contains(s1, old1) {
|
||||
s1 = strings.Replace(s1, old1, new1, 1)
|
||||
os.WriteFile(f1, []byte(s1), 0644)
|
||||
println("1. ChatData: ProjectData field added")
|
||||
} else {
|
||||
println("1. ERROR: ChatData struct not found")
|
||||
return
|
||||
}
|
||||
|
||||
// === 2. autoRegisSession: 加载 ProjectData 并填入 ChatData ===
|
||||
f2 := "internal/biz/advice_wx.go"
|
||||
b2, _ := os.ReadFile(f2)
|
||||
s2 := string(b2)
|
||||
|
||||
old2 := ` // 构建 ChatData(全量数据注入)
|
||||
chatData := &entitys.ChatData{
|
||||
AdvicerInfo: advicer.Entity(),
|
||||
AdvicerVersion: versionInfo.Data,
|
||||
ProjectInfo: projEntity,
|
||||
RuleDimension: projectInfo.Base.RuleDimension,
|
||||
TalkSkill: talkSkillEntity,
|
||||
ClientInfo: clientEntity,
|
||||
}`
|
||||
new2 := ` // 加载项目资料(advicer_project_data 集合,扁平栏目数据)
|
||||
var projectData map[string]interface{}
|
||||
if projData, e := a.adviceProjectBiz.ProjectDataLoad(ctx, projectInfo.Base.ProjectID); e == nil && len(projData.Data) > 0 {
|
||||
projectData = projData.Data
|
||||
}
|
||||
|
||||
// 构建 ChatData(全量数据注入)
|
||||
chatData := &entitys.ChatData{
|
||||
AdvicerInfo: advicer.Entity(),
|
||||
AdvicerVersion: versionInfo.Data,
|
||||
ProjectInfo: projEntity,
|
||||
ProjectData: projectData,
|
||||
RuleDimension: projectInfo.Base.RuleDimension,
|
||||
TalkSkill: talkSkillEntity,
|
||||
ClientInfo: clientEntity,
|
||||
}`
|
||||
if strings.Contains(s2, old2) {
|
||||
s2 = strings.Replace(s2, old2, new2, 1)
|
||||
os.WriteFile(f2, []byte(s2), 0644)
|
||||
println("2. autoRegisSession: ProjectData load added")
|
||||
} else {
|
||||
println("2. ERROR: ChatData construction block not found")
|
||||
return
|
||||
}
|
||||
|
||||
// === 3. buildChatPromptResponse: 注入 ProjectData ===
|
||||
f3 := "internal/biz/advice_chat.go"
|
||||
b3, _ := os.ReadFile(f3)
|
||||
s3 := string(b3)
|
||||
|
||||
old3 := ` if chatData.ProjectInfo != nil {
|
||||
sys.WriteString("\n[项目信息]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.ProjectInfo))
|
||||
}`
|
||||
new3 := ` if chatData.ProjectInfo != nil {
|
||||
sys.WriteString("\n[项目信息]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.ProjectInfo))
|
||||
}
|
||||
if chatData.ProjectData != nil && len(chatData.ProjectData) > 0 {
|
||||
sys.WriteString("\n[项目资料]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.ProjectData))
|
||||
}`
|
||||
if strings.Contains(s3, old3) {
|
||||
s3 = strings.Replace(s3, old3, new3, 1)
|
||||
os.WriteFile(f3, []byte(s3), 0644)
|
||||
println("3. buildChatPromptResponse: ProjectData injection added")
|
||||
} else {
|
||||
println("3. ERROR: ProjectInfo block in buildChatPromptResponse not found")
|
||||
return
|
||||
}
|
||||
|
||||
println("\nALL DONE")
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
s := string(b)
|
||||
|
||||
// Fix: Use Base.Name (MySQL) instead of ConfigInfo.ProjectInfo.Name (MongoDB)
|
||||
// The MySQL ai_advice_project table has the project name, MongoDB might not have projectInfo sub-doc
|
||||
old := ` if pi := projectInfo.ConfigInfo.ProjectInfo; pi.Name != "" || pi.Address != "" {
|
||||
projectDesc := "\n[项目信息]"
|
||||
if pi.Name != "" {
|
||||
projectDesc += "\n项目名称:" + pi.Name
|
||||
}
|
||||
if pi.Address != "" {
|
||||
projectDesc += "\n项目地址:" + pi.Address
|
||||
}
|
||||
mission = mission + projectDesc
|
||||
log.Infof("[托管] 项目信息已注入 mission advicerId=%d projectName=%s", advicer.AdvicerID, pi.Name)
|
||||
}`
|
||||
|
||||
new := ` // 项目信息:优先用 MySQL 的 Base.Name(一定有),MongoDB 的 ProjectInfo 作为补充
|
||||
projectDesc := "\n[项目信息]"
|
||||
hasProjectInfo := false
|
||||
if projectInfo.Base.Name != "" {
|
||||
projectDesc += "\n项目名称:" + projectInfo.Base.Name
|
||||
hasProjectInfo = true
|
||||
}
|
||||
if pi := projectInfo.ConfigInfo.ProjectInfo; pi.Address != "" {
|
||||
projectDesc += "\n项目地址:" + pi.Address
|
||||
hasProjectInfo = true
|
||||
}
|
||||
if hasProjectInfo {
|
||||
mission = mission + projectDesc
|
||||
}`
|
||||
|
||||
if strings.Contains(s, old) {
|
||||
s = strings.Replace(s, old, new, 1)
|
||||
os.WriteFile(f, []byte(s), 0644)
|
||||
println("done - fixed project info to use Base.Name")
|
||||
} else {
|
||||
println("ERROR: project info block not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_chat.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
s := string(b)
|
||||
ok := true
|
||||
|
||||
// === 1. Regis: 存 ChatData 到 Redis ===
|
||||
old1 := ` err = a.rdb.Rdb.SetEx(ctx, sessionId, promptJson, 3600*time.Second).Err()
|
||||
|
||||
return sessionId, err
|
||||
}`
|
||||
new1 := ` err = a.rdb.Rdb.SetEx(ctx, sessionId, promptJson, 3600*time.Second).Err()
|
||||
// 存储 ChatData 供后续 Chat 全量注入提示词
|
||||
if chatDataJson, e := json.Marshal(chatData); e == nil {
|
||||
_ = a.rdb.Rdb.SetEx(ctx, "chatdata:"+sessionId, string(chatDataJson), 3600*time.Second).Err()
|
||||
}
|
||||
return sessionId, err
|
||||
}`
|
||||
if strings.Contains(s, old1) {
|
||||
s = strings.Replace(s, old1, new1, 1)
|
||||
println("1. Regis: ChatData store added")
|
||||
} else {
|
||||
println("1. ERROR: Regis block not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 2. Chat: 从 Redis 取 ChatData,传给 buildChatPromptResponse ===
|
||||
old2 := ` prompt, err := a.buildChatPromptResponse(ctx, chat, &session, chatHis)
|
||||
if err != nil {
|
||||
return assistant, err
|
||||
}
|
||||
log.Infof("[提示词] sessionId=%s prompt=%s", session.SessionID, pkg.JsonStringIgonErr(prompt))
|
||||
resContent, err := a.callLlmResponse(ctx, prompt, modelInfo.Key, modelInfo.URL, modelInfo.ChatModel, session.ContextCache)`
|
||||
new2 := ` var chatData entitys.ChatData
|
||||
if cdStr, e := a.rdb.Rdb.Get(ctx, "chatdata:"+session.SessionID).Result(); e == nil {
|
||||
_ = json.Unmarshal([]byte(cdStr), &chatData)
|
||||
}
|
||||
prompt, err := a.buildChatPromptResponse(ctx, chat, &session, chatHis, &chatData)
|
||||
if err != nil {
|
||||
return assistant, err
|
||||
}
|
||||
log.Infof("[提示词] sessionId=%s prompt=%s", session.SessionID, pkg.JsonStringIgonErr(prompt))
|
||||
resContent, err := a.callLlmResponse(ctx, prompt, modelInfo.Key, modelInfo.URL, modelInfo.ChatModel, session.ContextCache)`
|
||||
if strings.Contains(s, old2) {
|
||||
s = strings.Replace(s, old2, new2, 1)
|
||||
println("2. Chat: ChatData load + pass added")
|
||||
} else {
|
||||
println("2. ERROR: Chat block not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 3. ChatWithSession: 同样从 Redis 取 ChatData ===
|
||||
old3 := ` prompt, err := a.buildChatPromptResponse(ctx, chat, session, chatHis)
|
||||
if err != nil {
|
||||
return assistant, err
|
||||
}
|
||||
log.Infof("[提示词] sessionId=%s prompt=%s", session.SessionID, pkg.JsonStringIgonErr(prompt))
|
||||
resContent, err := a.callLlmResponse(ctx, prompt, modelInfo.Key, modelInfo.URL, modelInfo.ChatModel, session.ContextCache)`
|
||||
new3 := ` var chatData entitys.ChatData
|
||||
if cdStr, e := a.rdb.Rdb.Get(ctx, "chatdata:"+session.SessionID).Result(); e == nil {
|
||||
_ = json.Unmarshal([]byte(cdStr), &chatData)
|
||||
}
|
||||
prompt, err := a.buildChatPromptResponse(ctx, chat, session, chatHis, &chatData)
|
||||
if err != nil {
|
||||
return assistant, err
|
||||
}
|
||||
log.Infof("[提示词] sessionId=%s prompt=%s", session.SessionID, pkg.JsonStringIgonErr(prompt))
|
||||
resContent, err := a.callLlmResponse(ctx, prompt, modelInfo.Key, modelInfo.URL, modelInfo.ChatModel, session.ContextCache)`
|
||||
if strings.Contains(s, old3) {
|
||||
s = strings.Replace(s, old3, new3, 1)
|
||||
println("3. ChatWithSession: ChatData load + pass added")
|
||||
} else {
|
||||
println("3. ERROR: ChatWithSession block not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 4. buildChatPromptResponse: 全量数据注入 ===
|
||||
old4 := `func (a *AdviceChatBiz) buildChatPromptResponse(ctx context.Context, chat *entitys.AdvicerChatReq, session *dbmodel.AiAdviceSession, chatList []mongo_model.AdvicerChatHisMongoEntity) ([]openai.ResponseInputMessage, error) {
|
||||
// 输出格式指令:模型使用 response_format=json_object 时,prompt 必须包含 "json" 关键词,
|
||||
// 同时明确告诉模型返回 JSON 结构,便于下游 json.Unmarshal 解析为 Assistant。
|
||||
const outputFormatHint = "\n[输出格式]请用 json 格式返回你的回复,结构为 {\"result\":\"回复内容\",\"mission_status\":\"in_progress|completed|fail\",\"mission_complete_desc\":\"本轮进展小结\"}。"
|
||||
message := []openai.ResponseInputMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: a.taskPrompt(session) + outputFormatHint,
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: formatChatHis(chatList),
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: chat.Content,
|
||||
},
|
||||
}
|
||||
return message, nil
|
||||
}`
|
||||
new4 := `func (a *AdviceChatBiz) buildChatPromptResponse(ctx context.Context, chat *entitys.AdvicerChatReq, session *dbmodel.AiAdviceSession, chatList []mongo_model.AdvicerChatHisMongoEntity, chatData *entitys.ChatData) ([]openai.ResponseInputMessage, error) {
|
||||
const outputFormatHint = "\n[输出格式]请用 json 格式返回你的回复,结构为 {\"result\":\"回复内容\",\"mission_status\":\"in_progress|completed|fail\",\"mission_complete_desc\":\"本轮进展小结\"}。"
|
||||
var sys strings.Builder
|
||||
// 基础规则 + 任务 + 风控红线
|
||||
sys.WriteString(constants.BasePrompt)
|
||||
if session.Mission != "" {
|
||||
sys.WriteString(session.Mission)
|
||||
sys.WriteString("\n")
|
||||
}
|
||||
if chatData != nil && chatData.RuleDimension != "" {
|
||||
sys.WriteString(constants.RulePromptPrefix)
|
||||
sys.WriteString(chatData.RuleDimension)
|
||||
sys.WriteString("\n")
|
||||
}
|
||||
sys.WriteString(constants.BasePrompt2)
|
||||
// 全量结构化数据注入
|
||||
if chatData != nil {
|
||||
if chatData.ProjectInfo != nil {
|
||||
sys.WriteString("\n[项目信息]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.ProjectInfo))
|
||||
}
|
||||
if chatData.AdvicerInfo != nil {
|
||||
sys.WriteString("\n[销售信息]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.AdvicerInfo))
|
||||
}
|
||||
if chatData.AdvicerVersion != nil && len(chatData.AdvicerVersion) > 0 {
|
||||
sys.WriteString("\n[销售人设风格]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.AdvicerVersion))
|
||||
}
|
||||
if chatData.ClientInfo != nil {
|
||||
sys.WriteString("\n[客户信息]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.ClientInfo))
|
||||
}
|
||||
if chatData.TalkSkill != nil {
|
||||
sys.WriteString("\n[销售技巧]\n")
|
||||
sys.WriteString(pkg.JsonStringIgonErr(chatData.TalkSkill))
|
||||
}
|
||||
}
|
||||
sys.WriteString(outputFormatHint)
|
||||
message := []openai.ResponseInputMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: sys.String(),
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
Content: formatChatHis(chatList),
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
Content: chat.Content,
|
||||
},
|
||||
}
|
||||
return message, nil
|
||||
}`
|
||||
if strings.Contains(s, old4) {
|
||||
s = strings.Replace(s, old4, new4, 1)
|
||||
println("4. buildChatPromptResponse: full data injection done")
|
||||
} else {
|
||||
println("4. ERROR: buildChatPromptResponse block not found")
|
||||
ok = false
|
||||
}
|
||||
|
||||
// === 5. 删掉 taskPrompt 中的 session.Mission 注入(避免重复,已在 buildChatPromptResponse 中处理)===
|
||||
old5 := ` // 注入 mission(含人格 + 项目信息),确保每次 Chat 模型都能看到
|
||||
if session.Mission != "" {
|
||||
b.WriteString("\n" + session.Mission)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (a *AdviceChatBiz) buildBasePromptResponse`
|
||||
new5 := ` return b.String()
|
||||
}
|
||||
|
||||
func (a *AdviceChatBiz) buildBasePromptResponse`
|
||||
if strings.Contains(s, old5) {
|
||||
s = strings.Replace(s, old5, new5, 1)
|
||||
println("5. taskPrompt: removed redundant mission injection")
|
||||
} else {
|
||||
println("5. SKIP: taskPrompt already clean")
|
||||
}
|
||||
|
||||
if ok {
|
||||
os.WriteFile(f, []byte(s), 0644)
|
||||
println("\nALL DONE - advice_chat.go updated")
|
||||
} else {
|
||||
println("\nFAILED - some blocks not found")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
lines := strings.Split(string(b), "\n")
|
||||
// Show chatData construction in autoRegisSession
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "chatData := &entitys.ChatData") {
|
||||
for j := i - 2; j < i+15 && j < len(lines); j++ {
|
||||
visible := strings.ReplaceAll(lines[j], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", j+1, visible)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Println("---")
|
||||
// Show function signature of autoRegisSession
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "func (a *AdviceWxBiz) autoRegisSession") {
|
||||
for j := i; j < i+8 && j < len(lines); j++ {
|
||||
visible := strings.ReplaceAll(lines[j], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", j+1, visible)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Println("---")
|
||||
// Show what variables are available (customer, projectInfo, advicer)
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "customerNickName") && strings.Contains(line, "=") && i < 450 {
|
||||
visible := strings.ReplaceAll(lines[i], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
lines := strings.Split(string(b), "\n")
|
||||
fmt.Printf("Total lines: %d\n", len(lines))
|
||||
// Show the area around the project info fix (L460-520)
|
||||
for i := 455; i < 520 && i < len(lines); i++ {
|
||||
visible := strings.ReplaceAll(lines[i], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
lines := strings.Split(string(b), "\n")
|
||||
for i := 510; i < 530 && i < len(lines); i++ {
|
||||
visible := strings.ReplaceAll(lines[i], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_chat.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
lines := strings.Split(string(b), "\n")
|
||||
// Show taskPrompt and buildChatPromptResponse
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "func (a *AdviceChatBiz) taskPrompt") ||
|
||||
strings.Contains(line, "func (a *AdviceChatBiz) buildChatPromptResponse") {
|
||||
for j := i; j < i+25 && j < len(lines); j++ {
|
||||
visible := strings.ReplaceAll(lines[j], "\t", "→")
|
||||
fmt.Printf("L%d: %s\n", j+1, visible)
|
||||
}
|
||||
fmt.Println("---")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
f := "internal/biz/advice_wx.go"
|
||||
b, _ := os.ReadFile(f)
|
||||
lines := strings.Split(string(b), "\n")
|
||||
// Show hostingChatAndSend function signature and call
|
||||
for i, line := range lines {
|
||||
if strings.Contains(line, "func (a *AdviceWxBiz) hostingChatAndSend") ||
|
||||
strings.Contains(line, "a.hostingChatAndSend(") ||
|
||||
strings.Contains(line, "ChatWithSession") {
|
||||
visible := strings.ReplaceAll(lines[i], "\t", "→")
|
||||
fmt.Printf("wx L%d: %s\n", i+1, visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,13 +38,16 @@ function renderData() {
|
|||
}
|
||||
|
||||
function renderDataBody() {
|
||||
console.log("[renderDataBody] tab=" + state.data.tab);
|
||||
var body = $("#data-body");
|
||||
if (!body) { console.error("[renderDataBody] #data-body not found!"); return; }
|
||||
body.innerHTML = '<div class="empty"><span class="spin"></span></div>';
|
||||
var tab = state.data.tab;
|
||||
if (tab === "advicers") return renderTabAdvicers(body);
|
||||
if (tab === "versions") return renderTabVersions(body);
|
||||
if (tab === "skills") return renderTabSkills(body);
|
||||
if (tab === "clients") return renderTabClients(body);
|
||||
console.warn("[renderDataBody] unknown tab: " + tab);
|
||||
}
|
||||
|
||||
function dataHead(title, sub, addBtnId, addBtnText) {
|
||||
|
|
@ -969,16 +972,42 @@ function skillAiGenerateDialog(parentModal, skDims, skItems) {
|
|||
}
|
||||
|
||||
/* ---------- Tab 4:客户 ---------- */
|
||||
var CLIENT_SUB_TABS = [["profiles", "\u5ba2\u6237\u753b\u50cf"], ["hosted", "\u6258\u7ba1\u5ba2\u6237"]];
|
||||
|
||||
function renderTabClients(body) {
|
||||
console.log("[renderTabClients] called, subTab=" + (state.data.clientSubTab || "profiles"));
|
||||
var subTab = state.data.clientSubTab || "profiles";
|
||||
body.innerHTML =
|
||||
dataHead("客户", "客户画像(身份/目的/需求/顾虑/决策)", "cli-add", "新增客户") +
|
||||
'<div class="row mb-2"><span class="fs-13 c-main" style="font-weight:700">\u5ba2\u6237</span>' +
|
||||
'<span class="fs-12 c-dim">\u5ba2\u6237\u753b\u50cf\u4e0e\u6258\u7ba1\u5ba2\u6237\u7ba1\u7406</span><span class="spacer"></span>' +
|
||||
'<button class="btn primary sm" id="cli-add">+ \u65b0\u589e\u5ba2\u6237</button></div>' +
|
||||
'<div class="tabs" id="cli-sub-tabs" style="margin-bottom:10px">' +
|
||||
CLIENT_SUB_TABS.map(function (t) {
|
||||
return '<div class="tab' + (subTab === t[0] ? " active" : "") + '" data-sub="' + t[0] + '">' + t[1] + '</div>';
|
||||
}).join('') + '</div>' +
|
||||
'<div id="cli-list"></div>';
|
||||
$("#cli-add").onclick = function () { clientForm(null); };
|
||||
loadClients();
|
||||
// 使用 body 内部查询(body 可能脱离 document,document.querySelector 找不到)
|
||||
var addBtn = body.querySelector("#cli-add");
|
||||
var tabsEl = body.querySelector("#cli-sub-tabs");
|
||||
if (tabsEl) {
|
||||
Array.prototype.slice.call(tabsEl.children).forEach(function (t) {
|
||||
t.onclick = function () {
|
||||
state.data.clientSubTab = t.dataset.sub;
|
||||
Array.prototype.slice.call(tabsEl.children).forEach(function (x) { x.classList.toggle("active", x === t); });
|
||||
if (addBtn) addBtn.style.display = (t.dataset.sub === "profiles") ? "" : "none";
|
||||
if (t.dataset.sub === "profiles") { loadClients(listBox); }
|
||||
else { loadHostedCustomers(listBox); }
|
||||
};
|
||||
});
|
||||
}
|
||||
if (addBtn) addBtn.onclick = function () { clientForm(null); };
|
||||
var listBox = body.querySelector("#cli-list");
|
||||
if (subTab === "profiles") { loadClients(listBox); }
|
||||
else { if (addBtn) addBtn.style.display = "none"; loadHostedCustomers(listBox); }
|
||||
}
|
||||
|
||||
function loadClients() {
|
||||
var box = $("#cli-list");
|
||||
function loadClients(box) {
|
||||
if (!box) box = document.querySelector("#cli-list");
|
||||
if (!box) return;
|
||||
box.innerHTML = '<div class="empty"><span class="spin"></span></div>';
|
||||
Core.api("client/list", { projectId: state.project.projectId }).then(function (list) {
|
||||
|
|
@ -1046,6 +1075,52 @@ function loadClients() {
|
|||
}
|
||||
|
||||
/* ---------- 客户等级常量 / 微信绑定 / 等级评估 ---------- */
|
||||
|
||||
/* ---------- 托管客户列表 ---------- */
|
||||
function loadHostedCustomers(box) {
|
||||
if (!box) box = document.querySelector("#cli-list");
|
||||
if (!box) return;
|
||||
box.innerHTML = '<div class="empty"><span class="spin"></span></div>';
|
||||
Core.api("advicer/customer/hosted", { projectId: state.project.projectId }).then(function (list) {
|
||||
list = list || [];
|
||||
if (!list.length) {
|
||||
box.innerHTML = '<div class="empty"><div class="big">\u25c9</div>\u5c1a\u65e0\u6258\u7ba1\u5ba2\u6237\uff0c\u5f00\u542f\u6258\u7ba1\u6807\u7b7e\u540e\u6d88\u606f\u56de\u8c03\u65f6\u81ea\u52a8\u540c\u6b65</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = '<div class="fs-12 c-dim" style="margin-bottom:8px">\u5171 ' + list.length + ' \u4f4d\u6258\u7ba1\u5ba2\u6237\uff08\u5904\u4e8e\u6258\u7ba1\u6807\u7b7e\u5206\u7ec4\u5185\u7684\u5fae\u4fe1\u597d\u53cb\uff0c\u6bcf\u6b21\u6d88\u606f\u56de\u8c03\u65f6\u81ea\u52a8\u540c\u6b65\u6700\u65b0\u4fe1\u606f\uff09</div>' +
|
||||
'<div class="table-wrap"><table class="table"><thead><tr>' +
|
||||
'<th>\u5934\u50cf</th><th>\u6635\u79f0</th><th>\u5907\u6ce8</th><th>\u6258\u7ba1\u6807\u7b7e</th><th>\u6027\u522b</th><th>\u5730\u533a</th><th>\u5f52\u5c5e\u9500\u552e</th><th>\u4f1a\u8bdd\u72b6\u6001</th><th>\u66f4\u65b0\u65f6\u95f4</th></tr></thead><tbody>' +
|
||||
list.map(function (c) {
|
||||
var displayName = c.remark || c.nick_name || c.user_name || "-";
|
||||
var avatar = c.big_head_img_url || c.small_head_img_url || "";
|
||||
var avatarCell = avatar
|
||||
? '<img src="' + Core.esc(avatar) + '" style="width:32px;height:32px;border-radius:50%;object-fit:cover" onerror="this.style.display=\'none\'">'
|
||||
: '<span style="display:inline-flex;width:32px;height:32px;border-radius:50%;background:var(--surface-2);align-items:center;justify-content:center;font-size:14px;color:var(--dim)">\u25c9</span>';
|
||||
var sex = c.sex === 1 ? "\u7537" : c.sex === 2 ? "\u5973" : "-";
|
||||
var area = [c.province, c.city].filter(function (s) { return s; }).join(" ") || "-";
|
||||
var sessionStatus = c.session_id
|
||||
? '<span class="badge green">\u5df2\u6258\u7ba1</span>'
|
||||
: '<span class="badge gray">\u672a\u6ce8\u518c</span>';
|
||||
var labels = (c.label_names || "").split(",").filter(function (s) { return s; });
|
||||
var labelHtml = labels.map(function (l) {
|
||||
return '<span class="badge blue fs-11">' + Core.esc(l) + '</span>';
|
||||
}).join(" ");
|
||||
return "<tr>" +
|
||||
'<td>' + avatarCell + '</td>' +
|
||||
'<td><span class="name">' + Core.esc(displayName) + '</span>' +
|
||||
(c.user_name ? '<div class="mono fs-11 c-dim">' + Core.esc(c.user_name) + '</div>' : '') + '</td>' +
|
||||
'<td class="fs-12">' + Core.esc(c.remark || "-") + '</td>' +
|
||||
'<td>' + (labelHtml || "-") + '</td>' +
|
||||
'<td>' + sex + '</td>' +
|
||||
'<td class="fs-12">' + Core.esc(area) + '</td>' +
|
||||
'<td class="fs-12">' + Core.esc(c.advicer_name || "-") + '</td>' +
|
||||
'<td>' + sessionStatus + '</td>' +
|
||||
'<td class="fs-12">' + Core.fmtTime(c.update_at) + '</td></tr>';
|
||||
}).join("") + "</tbody></table></div>";
|
||||
}).catch(function (e) {
|
||||
box.innerHTML = '<div class="empty"><div class="big">\u26a0</div>' + Core.esc(e.message) + '</div>';
|
||||
});
|
||||
}
|
||||
var CLIENT_LEVELS = { regular: "熟客", intent: "意向客户", sleeping: "沉睡客户", non: "非客户" };
|
||||
var CLIENT_LEVEL_CLS = { regular: "purple", intent: "green", sleeping: "yellow", non: "gray" };
|
||||
|
||||
|
|
|
|||
|
|
@ -204,6 +204,14 @@
|
|||
Core.toast("微信登录成功:" + (app.nickName || app.wxid), "ok");
|
||||
renderLoginCard();
|
||||
checkOnlineLoop();
|
||||
// 登录成功后回写 wx_id 到销售记录
|
||||
var hashMatch2 = (location.hash || '').match(/wx-advicer=(\d+)/);
|
||||
var advicerId2 = hashMatch2 ? Number(hashMatch2[1]) : 0;
|
||||
if (advicerId2 && app.wxid) {
|
||||
Core.api("advicer/wx_id/update", { advicerId: advicerId2, wxId: app.wxid })
|
||||
.then(function () { console.log("[login] wx_id updated: " + app.wxid); })
|
||||
.catch(function (e) { console.warn("[login] update wx_id failed:", e); });
|
||||
}
|
||||
WxN.loadProfile().then(function () { WxN.loadConversations(); });
|
||||
WxN.loadContacts();
|
||||
WxN.loadLabels();
|
||||
|
|
|
|||
|
|
@ -93,13 +93,13 @@
|
|||
<script src="assets/js/core.js?v=21"></script>
|
||||
<script src="assets/js/admin_industry.js?v=18"></script>
|
||||
<script src="assets/js/wx.js?v=68"></script>
|
||||
<script src="assets/js/wx_login.js?v=45"></script>
|
||||
<script src="assets/js/wx_login.js?v=46"></script>
|
||||
<script src="assets/js/wx_chat.js?v=46"></script>
|
||||
<script src="assets/js/project.js?v=21"></script>
|
||||
<script src="assets/js/project_list.js?v=18"></script>
|
||||
<script src="assets/js/project_detail.js?v=27"></script>
|
||||
<script src="assets/js/project_analysis.js?v=21"></script>
|
||||
<script src="assets/js/project_data.js?v=37"></script>
|
||||
<script src="assets/js/project_data.js?v=40"></script>
|
||||
<script src="assets/js/project_activity.js?v=18"></script>
|
||||
<script src="assets/js/project_drill.js?v=19"></script>
|
||||
<script src="assets/js/project_smart.js?v=18"></script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue