ai_scheduler/doc/AI_DEV_GUIDE.md

437 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AI 数字销售助手 — AI 开发者快速指南
> **本文档面向 AI 编程助手**,帮助你在最短时间内理解项目全貌并继续开发。
---
## 项目目标(务必理解)
**构建一个能完全模仿真人销售在微信上与客户对话的 AI 系统。**
核心差异:
1. **数字销售分身**:上传销售真实聊天记录,AI 提取其个人风格(方言/句式/语气/性格),构建"数字分身"
2. **四维画像驱动**:基于 **销售风格 / 项目资料 / 聊天技巧 / 客户画像** 综合理解场景后生成回复
3. **自主运营**:自动回复(按客户等级分策略)、主动触达(生日/活动)、画像自迭代、等级动态评估
4. **最终愿景**:销售只处理 AI 搞不定的关键对话,日常维护/跟进/促单全部由 AI 代劳
**你写的每一行代码都应服务于:让 AI 更像那个销售、更懂那个客户、更专业地推进销售进程。**
---
## 技术栈
```
Go 1.26 + Fiber v2 + GORM(MySQL) + MongoDB + Redis + Wire DI + 原生前端(零构建)
```
调用链:
```
HTTP → Fiber中间件(JWT) → Router → Service → Biz → Impl/Mongo → LLM
↓
registerCommon 统一包装 {code:0, message, data}
```
---
## 目录结构(★ 标注当前活跃开发区域)
```
ai_scheduler/
├── cmd/server/
│ ├── main.go # 启动入口
│ ├── wire.go # Wire DI 声明(//go:build wireinject)
│ └── wire_gen.go # Wire 自动生成(禁止手动编辑)
├── config/config.yaml # MySQL/Mongo/Redis/JWT/微信协议地址
├── internal/
│ ├── biz/ # ★ 业务逻辑层
│ │ ├── advice_wx.go # 微信回调处理 + 消息流水 + 会话聚合 + ★ 托管自动注册
│ │ ├── advice_wx_send.go # 微信消息发送(含模拟真人打字延迟)
│ │ ├── advice_strategy.go # 智能策略(四级回复/主动对话)
│ │ ├── advice_client.go # 客户管理 + 等级评估
│ │ ├── advice_customer_new.go # ★ 微信好友 CRUD + 差集同步(ai_advice_customer)
│ │ ├── advice_label.go # ★ 标签同步(与 DB 比对增删改)
│ │ ├── advice_chat.go # AI 对话(LLM 调用 + ChatWithSession 复用优化)
│ │ ├── advice_evaluate.go # 客户等级评估
│ │ ├── advice_proactive.go # 主动触达
│ │ ├── advice_iterate.go # 画像自迭代
│ │ ├── handle/、llm_service/ # ⚠️ 历史死代码,忽略
│ │ └── provider_set.go
│ ├── services/advice/ # ★ HTTP Handler 层
│ │ ├── wxhook.go # 回调 + 会话 Handler
│ │ ├── wx_proxy.go # 微信 API 代理(分发到 wxDispatchMap)
│ │ ├── wx_ws_hub.go # WebSocket Hub(实时推送)
│ │ ├── customer_new.go # ★ 好友 CRUD Handler(Diff 同时返回标签)
│ │ ├── label.go # ★ 标签 Handler(List/Sync)
│ │ └── provider_set.go
│ ├── server/
│ │ ├── router/router.go # 路由总装 + registerCommon 中间件
│ │ ├── router/advicer.go # 全部业务路由(~80 个接口)
│ │ └── http.go # NewHTTPServer 参数列表需同步
│ ├── entitys/advicer_data.go # ★ 核心 DTO(最频繁修改)
│ ├── data/
│ │ ├── model/ # MySQL GORM 模型(*.gen.go 禁止手动编辑)
│ │ │ ├── ai_advice_customer.gen.go # ★ 微信好友表
│ │ │ └── ai_advice_label.gen.go # ★ 标签表
│ │ ├── impl/ # MySQL 数据访问(继承 DataTempBase[T])
│ │ │ ├── advice_customer_impl.go
│ │ │ └── advice_label_impl.go
│ │ ├── mongo_model/ # MongoDB 集合模型
│ │ └── constants/ # Prompt 模板、模型映射
│ ├── middleware/ # JWT 认证
│ ├── jobs/advice_task.go # 定时任务(cron)
│ └── pkg/wx/ # 微信协议封装(100+ API 端点)
├── web/ # ★ 前端(零构建,静态托管)
│ ├── project.html # ★ 版本号管理(修改 JS 后必须更新 ?v=N)
│ └── assets/
│ ├── css/theme.css # 深色科技风设计系统
│ └── js/
│ ├── core.js # 核心库(Core.api / Core.wxApi / Toast / Modal)
│ ├── wx.js # ★ 微信工作台核心(state/通讯录/标签/WS/好友同步)
│ ├── wx_login.js # ★ 登录模块(二维码/在线检测/登出)
│ └── wx_chat.js # ★ 聊天模块(会话/消息/联系人详情气泡)
└── sql/ # 数据库 DDL
├── 04_ai_advice_customer.sql
└── 05_ai_advice_label.sql
```
---
## 数据模型
### MySQL 核心表
| 表名 | 说明 | 关键字段 |
|---|---|---|
| `ai_advice_customer` | ★ 微信好友(`self_wxid` + `user_name` 唯一键) | label_list, session_id(当前活跃会话) |
| `ai_advice_label` | ★ 微信标签(`self_wxid` + `label_id` 唯一键) | label_id, label_name |
| `ai_advice_advicer` | 销售基本信息 | wx_device_id(即 appId) |
| `ai_advice_project` | 项目信息 | wx_token |
| `ai_advice_admin` | 管理员账号 | |
| `ai_advice_session` | AI 对话会话 | advicer_id(托管场景动态取最新版本) |
### MongoDB 核心集合
| 集合名 | 说明 | 关键字段 |
|---|---|---|
| `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, supportingFacilities, developerBacking |
| `advicer_project_data` | ★ 项目资料(扁平栏目) | projectId + 动态栏目(key=栏目名, value=内容),与 advicer_project 是**两个独立的集合** |
| `advicer_talk_skill` | 聊天技巧维度 | needsMining, painPointResponse, closingTechniques, communicationRhythm, valueBuilding |
| `advicer_activity` | 产品活动 | name, content, startAt, endAt |
### 命名差异
- MySQL:蛇形(`user_name`)
- MongoDB:驼峰 bson(`userName`)
- 前端 JS:驼峰(`userName`)
- API 响应:snake_case(`user_name`),前端手动映射
---
## 前端架构
### 核心机制
- **零构建**:`<script>` 标签按序引入,无打包工具
- **IIFE 模式**:每个 JS 文件 `(function(global){...})(window)` 包裹
- **版本号**:`<script src="xxx.js?v=42">` — **修改 JS/CSS 后必须更新 `project.html` 中的版本号**
- **全局命名空间**:`Core`(核心库)、`WxN`(微信模块共享)、`Wx`(挂载点)
### 微信工作台模块(WxN 命名空间)
| 文件 | 职责 |
|---|---|
| `wx.js` | state 管理 / 生命周期 / 通讯录 / 标签 / WebSocket / 好友差集同步 |
| `wx_login.js` | 二维码登录 / 在线检测 / 登出 |
| `wx_chat.js` | 会话列表 / 消息渲染 / 发送 / 联系人详情悬浮气泡 |
**架构要点**:
- `wx.js` 创建 `WxN` 命名空间,通过 `Object.defineProperty` 暴露 `state` getter
- 子模块通过 `WxN.state` 访问最新状态(不在闭包中缓存 state 引用)
- 子模块通过 `WxN.xxx = fn` 导出函数
- 加载顺序:`wx.js` → `wx_login.js` → `wx_chat.js`
### Core.js 速查
```javascript
Core.api(path, params) // 管理后台 API(POST JSON,JWT,自动解包 data)
// 完整路径:/api/v1/admin/advice/admin/{path}
Core.wxApi(path, params) // 微信代理 API(自动注入 projectId)
// 完整路径:/api/v1/admin/advice/admin/wx/{path}
Core.toast(msg, type) // type: "ok"|"err"|"warn"|"info"
Core.modal({title, body}) // 模态框
Core.confirm(msg) // 确认对话框,返回 Promise<boolean>
Core.esc(str) // HTML 转义
```
---
## 关键流程
### 初始化流程
```
页面加载 → mount() → freshState()
→ 解析 URL hash (#wx-advicer=advicerId)
→ fetchDeviceAndCheck(advicerId)
→ Core.api("advicer/info", {advicerId}) 获取 wx_device_id
→ state.app = { appId: wx_device_id } ← 始终从 API 获取,不缓存
→ checkOnline()
→ 在线 → checkOnlineLoop()(内含 connectWs())
loadProfile().then(loadConversations()) ← 需要 wxid
loadContacts() → syncCustomerDiff() ← diff 同时返回标签
loadLabels() ← 与微信同步标签到 DB
→ 离线 → getQrCode() 显示二维码
```
### 好友 + 标签同步流程
```
loadContacts() → syncCustomerDiff()
→ POST advicer/customer/diff { selfWxid, userNames: friends }
→ 后端返回 { list: [已有好友], diff: [缺失userName], labels: [标签列表] }
→ 用 list 填充 state.brief(联系人简要)
→ 用 labels 填充 state.labels + state.labelMap
→ diff 不为空时:分批 getBriefInfo → batch_add 入库
loadLabels()(并行)
→ Core.wxApi("label/list") 获取微信标签
→ POST advicer/label/sync 同步到 DB(比对增删改)
→ loadLabelsFromDb() 重新加载
```
### 消息回调流程
```
上游协议服务 → POST /api/v1/advicer/wx/callback
→ advice_wx.go:解析 AddMsg → 过滤 gh_ 公众号/msgType=other
→ 去重(AppId + NewMsgId)→ 入库 MongoDB advicer_wx_msg
→ WebSocket 广播 new_message(按 appId 匹配推送)
→ 触发智能策略(客户文本消息异步 AI 决策)
→ 触发托管自动注册(文本私聊消息异步检查)
```
### 托管自动注册流程
```
消息回调 → 文本私聊 → goroutine tryHostingAutoRegis
→ (1) 查 advicer(FindByWxDeviceId)→ hosting_enabled=1?
→ (2) 查 projectInfo → 取项目级 wxToken
→ (3) 若 peerWxid == "filehelper" 且 advicer.reply_filehelper=1 → 跳过标签检查,直接进入回复流程
否则调微信 contacts/getDetailInfo 取好友 labelList("1,2,3" 形式)
labelList 为空 → 直接 return,不做回复
→ (4) 取 ai_advice_label 中该销售 hosting=1 的标签 ID 列表
→ (5) 判断好友 labelList 与托管标签是否有交集,无交集 → return
→ (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 回复 → 拆段 → 模拟打字延迟 → 分段发送
```
**关键设计**:
- 标签判断改用微信实时返回的 labelList,不再依赖 customer 表缓存的标签
- projectInfo 提前到步骤 2 查询(后续 getDetailInfo 和 Chat 都需要 wxToken)
- 客户昵称优先使用微信备注名(Remark),回退到 NickName,最后才用 customer 表中的昵称
- 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 字,按换行/句末标点拆分)
- 初始延迟 3~8 秒(模拟阅读 + 思考)
- 每条消息间延迟 = 思考时间(2~5s) + 打字时间(每字 300~500ms)
- 提示词约束:每条消息 10~30 字,用 `\n` 分隔
### WebSocket 实时通信
```
前端 connectWs() → /api/v1/advicer/ws?token=JWT
→ onopen 发送 {"action":"subscribe","appId": state.app.appId}
→ 后端 Broadcast(appId, event, data) → 匹配 client.appId → 推送
→ 前端 onmessage → handleWsEvent()
→ "new_message" → WxN.handleNewMessage(data)
→ "online_status" → WxN.handleOnlineStatus(data)
```
---
## 当前开发状态(截至 2026-09-27)
### 已完成
- [x] 微信工作台:登录(二维码/在线检测)、通讯录、会话、聊天、AI 建议
- [x] 好友差集同步:`ai_advice_customer` 表 + `customer/diff` + `batch_add/batch_update`
- [x] 标签同步:`ai_advice_label` 表 + `label/sync` + `label/list`
- [x] 标签在 diff 接口一并返回(进入工作台时一次请求拿到好友 + 标签)
- [x] 标签 tab:展开/折叠、好友计数、loading 状态
- [x] 好友/会话列表展示联系人标签小标签
- [x] 联系人详情悬浮气泡(hover popup,含视口边界检测)
- [x] WebSocket 实时推送新消息 + 在线状态
- [x] 去掉 localStorage 缓存(brief/labels 全部内存 state)
- [x] 托管标签自动会话注册:检查标签交集 → 自动注册 AI 会话 → 回写 session_id
- [x] 托管自动回复:注册会话 → Chat → 拆段 → 模拟真人打字延迟 → 分段发送
- [x] 项目级 wx_token 支持:SendTextWithToken 方法,托管场景使用项目级 token
- [x] 查询优化:消除重复查询(projectInfo/advicer/session),新注册路径从 ~16 次降至 ~10 次 DB
- [x] 文件传输助手自动回复:销售级开关 `reply_filehelper`,开启后跳过标签检查直接 AI 回复
- [x] filehelper 普通客户化改造:走标准 customer 流程复用 session_id
- [x] ChatData 全量数据注入:Regis 时加载全部数据(项目/销售/客户/技巧/人设),Chat 时从 Redis 取出全量注入提示词
- [x] 提示词 token 优化:`compactJSON` 递归去空值,减少无效 token
### 待开发(下一步)
#### 1. 聊天算法深度优化
**目标**:进一步提升 AI 回复的真实感,让对话更自然、更像真人销售。
**优化方向**:
- **回复节奏优化**:根据消息内容复杂度动态调整延迟(简单问候快一些,复杂问题慢一些)
- **上下文感知回复**:结合客户画像、历史对话、当前时间段等因素调整回复风格
- **多轮对话连贯性**:确保多轮对话中话题过渡自然,不重复、不突兀
- **情绪感知**:识别客户情绪(犹豫/急迫/不满),动态调整回复语气和节奏
- **回复长度自适应**:根据客户消息长度调整回复长度(客户发短句,AI 也回短句)
- **提示词迭代**:根据实际对话效果持续优化 BasePrompt,减少 AI 味、增强人味
**涉及文件**:
- `internal/data/constants/prompt.go`(提示词模板)
- `internal/biz/advice_wx.go`(splitReplyToChunks / hostingChatAndSend)
- `internal/biz/advice_wx_send.go`(typingDelay 算法)
- `internal/biz/advice_chat.go`(Chat / ChatWithSession)
- `internal/biz/advice_strategy.go`(智能策略回复)
#### 2. 标签标记为客户标签 → 客户管理联动
**目标**:给标签打上"客户标记",被标记标签下的好友自动识别为客户,在客户管理模块体现,并在销售托管(自动回复)开启时仅对这些客户生效。
**设计思路**:
- `ai_advice_label` 表新增 `is_customer_tag TINYINT(1) DEFAULT 0` 字段(1=客户标签)
- 新增 API:`advicer/label/mark_customer` { selfWxid, labelId, isCustomerTag: bool }
- 前端标签 tab:每个标签右侧增加"客户标签"开关/标记按钮
- 客户管理模块(`advice_client.go` / `client.go`):
- 查询客户时,筛选 `ai_advice_customer.label_list` 包含已标记标签 ID 的记录
- 客户列表展示来源标签名称
- 智能策略(`advice_strategy.go`):
- 自动回复/主动触达判断时,仅对"客户标签下的好友"生效(非客户标签的好友不触发 AI 自动回复)
- 读取 `ai_advice_label` 中 `is_customer_tag=1` 的标签 ID 集合,与消息发送者的 `label_list` 取交集
**涉及文件**:
- 后端:`ai_advice_label.gen.go`(加字段)、`advice_label_impl.go`(加 MarkCustomer 方法)、`advice_label.go`(加 MarkCustomer biz)、`label.go`(加 handler)、`advicer_data.go`(加 DTO)、`advice_strategy.go`(过滤逻辑)
- 前端:`wx.js`(标签 tab 渲染标记按钮)、`theme.css`(标记样式)
- SQL:`06_ai_advice_label_customer_tag.sql`(ALTER TABLE 或迁移脚本)
#### 3. 好友/会话列表 → 给好友打标签
**目标**:在好友列表和会话列表中,可以对好友(非 chatroom)添加到指定标签,类似微信原生标签管理。
**设计思路**:
- 利用微信协议 API `contacts/modifyFriendLabel`(或类似端点)修改好友的标签
- 前端:联系人详情悬浮气泡(popup)中增加"管理标签"入口,点击弹出标签选择弹窗
- 展示当前好友已有的标签(从 `state.brief[wxid].labelList` 解析)
- 展示所有可用标签(`state.labels`)
- 支持勾选/取消,确认后调用微信 API 修改,再更新 DB(`ai_advice_customer.label_list`)
- 修改后需同步更新 `state.brief[wxid].labelList` 并重新渲染
**涉及文件**:
- 后端:可能需要新增微信代理 API(`pkg/wx/qs_*.go` + `api.go` + `wx_proxy.go` wxDispatchMap)
- 前端:`wx_chat.js`(popup 中加"管理标签"按钮 + 标签选择弹窗)、`wx.js`(标签更新逻辑)
- DB:更新 `ai_advice_customer.label_list` 字段
---
## Wire 依赖注入须知
新增模块后必须:
1. 各层 `provider_set.go` 添加 Provider
2. `http.go` 的 `NewHTTPServer` 参数列表添加 Service
3. `router.go` 的 `SetupRoutes` 参数列表添加 Service
4. `router/advicer.go` 的 `AdvicerRouterRegist` 参数列表添加 Service
5. 执行 `cd cmd/server && wire` 重新生成 `wire_gen.go`
6. `go build ./cmd/server` 编译验证
---
## 避坑清单
1. **`*.gen.go` 和 `wire_gen.go` 禁止手动编辑**
2. **Mongo 全量更新是 `$set` 语义** — 必须"先读后合并再写",否则清空未提交字段
3. **微信代理响应必须设 `skip_response_wrap`** — 否则被 `registerCommon` 二次包装成 `{data:{code,data}}`
4. **前端修改必须更新版本号** — `project.html` 中 `?v=N`,不更新则浏览器用旧缓存
5. **`state.app.appId` 不缓存** — 每次 mount 从 `advicer/info` API 实时获取
6. **`checkOnlineLoop()` 已内含 `connectWs()`** — 不要额外调用,否则连接级联断开
7. **`loadProfile()` 返回 Promise** — 依赖 `state.app.wxid` 的操作必须在其 `.then()` 中执行
8. **子模块通过 `WxN.state` getter 访问状态** — 不要在闭包中缓存 state 引用
9. **`builder.Cond` 查询用指针** — DataTemp 方法接受 `*builder.Cond` 指针
10. **`dataTemp.ReqPageBo` 分页字段是 `Limit`** — 不是 `PageSize`
11. **新增 Service 后同步三个文件** — `http.go`、`router.go`、`advicer.go` 函数签名都要加参数
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 后必须更新版本号** — 否则浏览器用旧缓存,看起来像没改
---
## 编译与部署
```bash
# 编译(仅验证业务代码)
go build ./internal/entitys/ ./internal/services/advice/ ./cmd/server/
# 本地运行
go run cmd/server/main.go
# Wire 重新生成
cd cmd/server && wire
# Docker 部署
docker-compose up -d
```
---
*最后更新:2026-09-27*