1239 lines
25 KiB
Markdown
1239 lines
25 KiB
Markdown
# 企业微信自动客服改造方案
|
||
|
||
## 1. 目标概述
|
||
|
||
当前项目已经具备企业微信消息管理能力,后端 `helper.exe` 通过 DLL 注入、Socket 回调、本地 HTTP 服务和 `requestdata` / `eventdata` 模板,能够接收企业微信事件并发送企业微信消息。
|
||
|
||
本次需求是在现有能力上增加“自动客服”模块:
|
||
|
||
- 前端提供一键开启入口。
|
||
- 后端自动监听企业微信消息。
|
||
- 私聊客户消息自动处理。
|
||
- 群聊消息仅在用户 @ 当前机器人时处理。
|
||
- 根据本地知识库检索内容。
|
||
- 调用 AI 根据知识库内容生成回复。
|
||
- 能回答则自动回复客户。
|
||
- 不能回答、AI 失败或知识库低匹配时,自动私信指定同事转人工处理。
|
||
|
||
## 2. 当前项目基础
|
||
|
||
### 2.1 技术结构
|
||
|
||
项目当前是:
|
||
|
||
- 桌面端:Wails
|
||
- 后端主程序:Go,`qiweimanager.exe`
|
||
- 辅助进程:Go,`helper.exe`
|
||
- 前端:Vue 3 + Vite + Element Plus
|
||
- 本地 HTTP 服务:默认 `http://localhost:10001`
|
||
- 企业微信底层通信:`Helper_4.1.33.6009.dll`、`Loader_4.1.33.6009.dll`
|
||
- 发送消息模板:`requestdata/*.json`
|
||
- 接收事件模板:`eventdata/*.json`
|
||
|
||
### 2.2 已有关键能力
|
||
|
||
现有项目已经支持:
|
||
|
||
- 启动/注入企业微信:`type=10000`
|
||
- 发送文本消息:`requestdata/sendVWorkTextMessage.json`
|
||
- 群 @ 消息:`requestdata/sendVWorkGroupAtMessage.json`
|
||
- 获取群列表:`requestdata/getVWorkGroupList.json`
|
||
- 获取内部好友列表:`requestdata/getVWorkInternalFriendList.json`
|
||
- 获取外部联系人列表:`requestdata/getVWorkExternalFriendList.json`
|
||
- 接收企业微信消息回调:`helper.go` 中的 `MyRecvCallback`
|
||
- 事件转换:`TransformData` 使用 `eventdata/<type>.json`
|
||
- 本地 HTTP 管理页面:`http://localhost:10001/`
|
||
- 操作日志和前端配置管理
|
||
|
||
### 2.3 最适合接入的位置
|
||
|
||
自动客服的核心入口建议放在:
|
||
|
||
```text
|
||
helper/helper.go
|
||
MyRecvCallback(...)
|
||
```
|
||
|
||
原因:
|
||
|
||
- 这里能拿到所有企业微信入站事件。
|
||
- 当前已经在这里做了消息解析、时间过滤、ResponseChannel 判断、回调转发。
|
||
- 自动客服需要的是“被动监听消息”,不是主动请求响应,所以这里最自然。
|
||
- 可保持现有 `/api/send-wxwork-data`、`/api/third-party-request` 和 callback 功能不被破坏。
|
||
|
||
## 3. 总体架构设计
|
||
|
||
### 3.1 新增模块
|
||
|
||
建议新增以下模块:
|
||
|
||
```text
|
||
config/
|
||
types.go # 增加 AutoReplyConfig 配置结构
|
||
|
||
helper/
|
||
auto_reply.go # 自动客服主流程
|
||
auto_reply_ai.go # AI 调用适配
|
||
auto_reply_knowledge.go # 本地知识库解析、索引、检索
|
||
auto_reply_handoff.go # 转人工通知
|
||
auto_reply_status.go # 状态统计、运行记录
|
||
auto_reply_http.go # helper 侧自动客服 HTTP 接口
|
||
|
||
frontend/src/
|
||
components/AutoReply.vue # 自动客服配置和状态页面
|
||
```
|
||
|
||
如果希望少建文件,也可以把 helper 侧先合并为 2 到 3 个文件,但不建议全部塞进 `helper.go`,否则后续维护会变得很吃力。
|
||
|
||
### 3.2 消息处理链路
|
||
|
||
整体链路如下:
|
||
|
||
```text
|
||
企业微信收到消息
|
||
↓
|
||
DLL Socket 回调
|
||
↓
|
||
helper.go / MyRecvCallback
|
||
↓
|
||
解析原始 JSON
|
||
↓
|
||
判断是否是主动请求响应
|
||
↓
|
||
如果是响应:继续走现有 ResponseChannel
|
||
↓
|
||
如果是独立入站消息:进入自动客服判断
|
||
↓
|
||
转换 eventdata 结构
|
||
↓
|
||
判断是否需要处理
|
||
↓
|
||
知识库检索
|
||
↓
|
||
AI 生成回复
|
||
↓
|
||
发送企业微信文本消息
|
||
↓
|
||
失败或无法回答时私信指定同事
|
||
```
|
||
|
||
### 3.3 不破坏现有功能的原则
|
||
|
||
自动客服必须做到:
|
||
|
||
- 不影响当前前端“启动企微”。
|
||
- 不影响现有 requestdata 模板调用。
|
||
- 不影响第三方 callback。
|
||
- 不影响本地 dashboard。
|
||
- 不影响主动请求等待回调的逻辑。
|
||
- 自动客服出错不能导致 `helper.exe` 崩溃。
|
||
- 自动客服处理必须异步,不阻塞 `MyRecvCallback`。
|
||
|
||
## 4. 配置设计
|
||
|
||
### 4.1 在 config 中新增 autoReplyConfig
|
||
|
||
建议在 `config/types.go` 中新增:
|
||
|
||
```go
|
||
type AutoReplyConfig struct {
|
||
Enabled bool `json:"enabled"`
|
||
|
||
Listen ListenConfig `json:"listen"`
|
||
Knowledge KnowledgeConfig `json:"knowledge"`
|
||
AI AIConfig `json:"ai"`
|
||
Handoff HandoffConfig `json:"handoff"`
|
||
ReplyPolicy ReplyPolicyConfig `json:"replyPolicy"`
|
||
}
|
||
|
||
type ListenConfig struct {
|
||
EnablePrivateChat bool `json:"enablePrivateChat"`
|
||
EnableGroupChat bool `json:"enableGroupChat"`
|
||
GroupTriggerMode string `json:"groupTriggerMode"`
|
||
IgnoreSelfMessage bool `json:"ignoreSelfMessage"`
|
||
DeduplicateSeconds int `json:"deduplicateSeconds"`
|
||
}
|
||
|
||
type KnowledgeConfig struct {
|
||
Directory string `json:"directory"`
|
||
IndexPath string `json:"indexPath"`
|
||
SupportedExtensions []string `json:"supportedExtensions"`
|
||
TopK int `json:"topK"`
|
||
MinScore float64 `json:"minScore"`
|
||
AutoRebuildOnStart bool `json:"autoRebuildOnStart"`
|
||
}
|
||
|
||
type AIConfig struct {
|
||
Provider string `json:"provider"`
|
||
BaseURL string `json:"baseUrl"`
|
||
APIKey string `json:"apiKey"`
|
||
Model string `json:"model"`
|
||
TimeoutSeconds int `json:"timeoutSeconds"`
|
||
Temperature float64 `json:"temperature"`
|
||
MaxTokens int `json:"maxTokens"`
|
||
}
|
||
|
||
type HandoffConfig struct {
|
||
HumanUserID string `json:"humanUserId"`
|
||
HumanConversationID string `json:"humanConversationId"`
|
||
MessageTemplate string `json:"messageTemplate"`
|
||
IncludeKnowledgeHits bool `json:"includeKnowledgeHits"`
|
||
}
|
||
|
||
type ReplyPolicyConfig struct {
|
||
UnknownAnswerToken string `json:"unknownAnswerToken"`
|
||
MaxQuestionLength int `json:"maxQuestionLength"`
|
||
CooldownSeconds int `json:"cooldownSeconds"`
|
||
}
|
||
```
|
||
|
||
并把它挂到现有配置:
|
||
|
||
```go
|
||
type Config struct {
|
||
CallbackConfig CallbackConfig `json:"callbackConfig"`
|
||
AutoReplyConfig AutoReplyConfig `json:"autoReplyConfig"`
|
||
LastUpdated int64 `json:"lastUpdated"`
|
||
}
|
||
```
|
||
|
||
### 4.2 默认配置
|
||
|
||
默认值建议:
|
||
|
||
```json
|
||
{
|
||
"autoReplyConfig": {
|
||
"enabled": false,
|
||
"listen": {
|
||
"enablePrivateChat": true,
|
||
"enableGroupChat": true,
|
||
"groupTriggerMode": "mention_only",
|
||
"ignoreSelfMessage": true,
|
||
"deduplicateSeconds": 300
|
||
},
|
||
"knowledge": {
|
||
"directory": "config/knowledge",
|
||
"indexPath": "config/knowledge/index.json",
|
||
"supportedExtensions": [".md", ".txt", ".csv", ".xlsx", ".docx", ".pdf"],
|
||
"topK": 5,
|
||
"minScore": 0.35,
|
||
"autoRebuildOnStart": false
|
||
},
|
||
"ai": {
|
||
"provider": "openai_compatible",
|
||
"baseUrl": "",
|
||
"apiKey": "",
|
||
"model": "",
|
||
"timeoutSeconds": 25,
|
||
"temperature": 0.2,
|
||
"maxTokens": 800
|
||
},
|
||
"handoff": {
|
||
"humanUserId": "",
|
||
"humanConversationId": "",
|
||
"messageTemplate": "客户问题需要人工处理:\n客户:{{customerName}}\n问题:{{question}}\n来源:{{source}}\n会话:{{conversationId}}\n原因:{{reason}}",
|
||
"includeKnowledgeHits": true
|
||
},
|
||
"replyPolicy": {
|
||
"unknownAnswerToken": "NO_ANSWER",
|
||
"maxQuestionLength": 1000,
|
||
"cooldownSeconds": 3
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 5. 前端改造方案
|
||
|
||
### 5.1 新增菜单
|
||
|
||
在 `frontend/src/App.vue` 左侧菜单新增:
|
||
|
||
```text
|
||
自动客服
|
||
```
|
||
|
||
建议位置:
|
||
|
||
```text
|
||
系统首页
|
||
企微账号
|
||
自动客服
|
||
操作记录
|
||
回调配置
|
||
```
|
||
|
||
### 5.2 新增 AutoReply.vue 页面
|
||
|
||
页面模块建议分为 6 块:
|
||
|
||
1. 运行状态
|
||
2. 一键开启/关闭
|
||
3. AI 配置
|
||
4. 知识库配置
|
||
5. 人工接管配置
|
||
6. 最近处理记录
|
||
|
||
### 5.3 运行状态区
|
||
|
||
显示:
|
||
|
||
- 自动客服状态:未开启 / 运行中 / 错误
|
||
- helper 连接状态
|
||
- 当前活跃企微账号数
|
||
- 知识库文件数
|
||
- 知识库切片数
|
||
- 今日处理消息数
|
||
- 今日自动回复数
|
||
- 今日转人工数
|
||
- 今日 AI 失败数
|
||
- 今日忽略消息数
|
||
|
||
### 5.4 一键开启按钮
|
||
|
||
按钮行为:
|
||
|
||
```text
|
||
点击“一键开启自动客服”
|
||
↓
|
||
保存当前自动客服配置
|
||
↓
|
||
调用启动企微 type=10000
|
||
↓
|
||
通知 helper reload 配置
|
||
↓
|
||
如果知识库索引不存在,提示并重建索引
|
||
↓
|
||
自动客服状态变为运行中
|
||
```
|
||
|
||
对应 Wails 方法:
|
||
|
||
```go
|
||
SetAutoReplyEnabled(true)
|
||
```
|
||
|
||
### 5.5 AI 配置区
|
||
|
||
字段:
|
||
|
||
- AI 类型:
|
||
- OpenAI 兼容接口
|
||
- 本地模型接口
|
||
- Base URL
|
||
- API Key
|
||
- Model
|
||
- Timeout
|
||
- Temperature
|
||
- Max Tokens
|
||
- 测试连接按钮
|
||
|
||
OpenAI 兼容接口示例:
|
||
|
||
```text
|
||
https://api.openai.com/v1
|
||
https://api.deepseek.com/v1
|
||
http://localhost:1234/v1
|
||
```
|
||
|
||
本地模型接口示例:
|
||
|
||
```text
|
||
http://localhost:11434
|
||
```
|
||
|
||
### 5.6 知识库配置区
|
||
|
||
字段:
|
||
|
||
- 本地知识库目录
|
||
- 支持格式展示:md、txt、csv、xlsx、docx、pdf
|
||
- TopK
|
||
- 最低匹配分数
|
||
- 重建知识库索引按钮
|
||
- 上次索引时间
|
||
- 索引结果
|
||
|
||
### 5.7 人工接管配置区
|
||
|
||
字段:
|
||
|
||
- 指定同事 user_id
|
||
- 指定同事 conversation_id
|
||
- 转人工消息模板
|
||
- 是否附带知识库命中片段
|
||
- 测试发送按钮
|
||
|
||
说明:
|
||
|
||
- 最稳妥方式是配置 `humanConversationId`。
|
||
- 如果只配置 `humanUserId`,后端按 `S:<robotId>_<humanUserId>` 推导 conversationId。
|
||
- 页面必须提供“测试发送”,确认能私信到指定同事。
|
||
|
||
## 6. Wails 后端接口方案
|
||
|
||
在 `app.go` 中新增方法:
|
||
|
||
```go
|
||
func (a *App) GetAutoReplyConfig() interface{}
|
||
func (a *App) SaveAutoReplyConfig(jsonData string) (bool, string)
|
||
func (a *App) SetAutoReplyEnabled(enabled bool) (bool, string)
|
||
func (a *App) GetAutoReplyStatus() interface{}
|
||
func (a *App) RebuildKnowledgeIndex() interface{}
|
||
func (a *App) TestAIConnection() interface{}
|
||
func (a *App) TestHumanHandoff() interface{}
|
||
```
|
||
|
||
这些方法主要负责:
|
||
|
||
- 读写 `config/config.json`
|
||
- 转发请求到 `helper.exe`
|
||
- 返回统一格式给前端
|
||
|
||
统一返回格式建议:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": "ok",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
失败时:
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "错误原因",
|
||
"data": null
|
||
}
|
||
```
|
||
|
||
## 7. helper HTTP 接口方案
|
||
|
||
在 helper 本地 HTTP 服务中新增:
|
||
|
||
```text
|
||
GET /api/auto-reply/status
|
||
POST /api/auto-reply/reload
|
||
POST /api/auto-reply/rebuild-knowledge
|
||
POST /api/auto-reply/test-ai
|
||
POST /api/auto-reply/test-handoff
|
||
```
|
||
|
||
### 7.1 GET /api/auto-reply/status
|
||
|
||
返回:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"enabled": true,
|
||
"running": true,
|
||
"lastError": "",
|
||
"knowledgeFileCount": 10,
|
||
"knowledgeChunkCount": 320,
|
||
"todayReceived": 100,
|
||
"todayReplied": 60,
|
||
"todayHandoff": 12,
|
||
"todayIgnored": 28,
|
||
"lastMessages": []
|
||
}
|
||
}
|
||
```
|
||
|
||
### 7.2 POST /api/auto-reply/reload
|
||
|
||
用途:
|
||
|
||
- 重新加载 `config/config.json`
|
||
- 重新初始化自动客服配置
|
||
- 不重启 helper
|
||
|
||
### 7.3 POST /api/auto-reply/rebuild-knowledge
|
||
|
||
用途:
|
||
|
||
- 扫描本地知识库目录
|
||
- 解析文档
|
||
- 构建索引
|
||
- 写入 `config/knowledge/index.json`
|
||
|
||
### 7.4 POST /api/auto-reply/test-ai
|
||
|
||
用途:
|
||
|
||
- 使用当前 AI 配置发起一次测试请求
|
||
- 不进入企业微信消息流程
|
||
|
||
### 7.5 POST /api/auto-reply/test-handoff
|
||
|
||
用途:
|
||
|
||
- 给指定同事发送一条测试私信
|
||
- 验证 `humanConversationId` 或 `humanUserId` 是否正确
|
||
|
||
## 8. 自动客服消息判断规则
|
||
|
||
### 8.1 只处理文本消息
|
||
|
||
当前文本消息模板主要是:
|
||
|
||
```text
|
||
eventdata/11041.json
|
||
event = 20002
|
||
```
|
||
|
||
自动客服初版只处理:
|
||
|
||
```json
|
||
{
|
||
"event": "20002",
|
||
"data": {
|
||
"message": "...",
|
||
"conversationId": "...",
|
||
"fromWxId": "...",
|
||
"toWxId": "...",
|
||
"fromNickName": "...",
|
||
"atWxIdList": []
|
||
}
|
||
}
|
||
```
|
||
|
||
非文本消息处理策略:
|
||
|
||
- 图片:转人工
|
||
- 文件:转人工
|
||
- 语音:转人工
|
||
- 视频:转人工
|
||
- 位置:转人工
|
||
- 链接:转人工
|
||
- 名片:转人工
|
||
|
||
### 8.2 私聊处理规则
|
||
|
||
私聊消息满足:
|
||
|
||
- 不是群聊 conversationId
|
||
- 不是机器人自己发出的消息
|
||
- 文本内容不为空
|
||
- 没有重复处理过
|
||
|
||
则自动处理。
|
||
|
||
### 8.3 群聊处理规则
|
||
|
||
群聊消息必须满足:
|
||
|
||
- 是群聊 conversationId
|
||
- 文本消息
|
||
- 不是机器人自己发出的消息
|
||
- `atWxIdList` 包含当前机器人 ID,或文本内容包含 @ 当前机器人昵称
|
||
- 未重复处理
|
||
|
||
未 @ 机器人时:
|
||
|
||
- 不回复
|
||
- 计入 ignored
|
||
- dashboard 可记录原因:`group message without mention`
|
||
|
||
### 8.4 自己消息过滤
|
||
|
||
如果满足以下任一条件,忽略:
|
||
|
||
- `fromWxId == robotId`
|
||
- `fromWxId == 当前登录企微 user_id`
|
||
- 原始消息标记为自己发送
|
||
- `sender == receiver` 且能确认是本机消息
|
||
|
||
### 8.5 去重策略
|
||
|
||
使用以下字段生成去重 key:
|
||
|
||
```text
|
||
robotId + conversationId + serverId
|
||
```
|
||
|
||
如果没有 `serverId`:
|
||
|
||
```text
|
||
robotId + conversationId + localId + sendTime + fromWxId
|
||
```
|
||
|
||
默认 300 秒内重复消息不处理。
|
||
|
||
## 9. 知识库方案
|
||
|
||
### 9.1 本地目录
|
||
|
||
默认目录:
|
||
|
||
```text
|
||
config/knowledge/
|
||
```
|
||
|
||
建议结构:
|
||
|
||
```text
|
||
config/knowledge/
|
||
faq.md
|
||
product.md
|
||
price.xlsx
|
||
after-sales.docx
|
||
policy.pdf
|
||
index.json
|
||
```
|
||
|
||
### 9.2 支持格式
|
||
|
||
用户选择“全都支持”,因此初版支持:
|
||
|
||
- `.md`
|
||
- `.txt`
|
||
- `.csv`
|
||
- `.xlsx`
|
||
- `.docx`
|
||
- `.pdf`
|
||
|
||
### 9.3 解析策略
|
||
|
||
不同文件策略:
|
||
|
||
- Markdown/TXT:按标题和段落切分。
|
||
- CSV:按行切分,每行拼接字段名和值。
|
||
- Excel:按 sheet + 行切分。
|
||
- DOCX:提取段落文本后切分。
|
||
- PDF:提取页面文本后切分。
|
||
|
||
每个 chunk 建议结构:
|
||
|
||
```json
|
||
{
|
||
"id": "hash",
|
||
"source": "faq.md",
|
||
"title": "售后政策",
|
||
"content": "具体内容",
|
||
"line": 12,
|
||
"page": 0,
|
||
"updatedAt": 1710000000,
|
||
"hash": "..."
|
||
}
|
||
```
|
||
|
||
### 9.4 检索策略
|
||
|
||
初版不引入向量库,使用本地关键词 / BM25 风格检索:
|
||
|
||
- 中文分词可先用 rune bigram + keyword 简化实现。
|
||
- 英文按空格和标点切词。
|
||
- 计算 query 和 chunk 的相关度。
|
||
- 返回 topK。
|
||
- 最高分低于 `minScore` 时认为知识库无法回答。
|
||
|
||
默认:
|
||
|
||
```text
|
||
topK = 5
|
||
minScore = 0.35
|
||
```
|
||
|
||
### 9.5 后续增强方向
|
||
|
||
后续可以升级为:
|
||
|
||
- 本地 embedding
|
||
- OpenAI embedding
|
||
- SQLite FTS5
|
||
- 向量数据库
|
||
- 混合检索
|
||
|
||
但初版不建议一开始引入复杂依赖,先把闭环跑通。
|
||
|
||
## 10. AI 调用方案
|
||
|
||
### 10.1 OpenAI 兼容接口
|
||
|
||
请求:
|
||
|
||
```text
|
||
POST {baseUrl}/chat/completions
|
||
Authorization: Bearer {apiKey}
|
||
```
|
||
|
||
如果用户填写的 `baseUrl` 是:
|
||
|
||
```text
|
||
https://api.openai.com/v1
|
||
```
|
||
|
||
则最终请求:
|
||
|
||
```text
|
||
https://api.openai.com/v1/chat/completions
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"model": "gpt-4.1-mini",
|
||
"temperature": 0.2,
|
||
"max_tokens": 800,
|
||
"messages": [
|
||
{
|
||
"role": "system",
|
||
"content": "你是企业微信客服助手,只能基于知识库回答。知识库没有答案时输出 NO_ANSWER。"
|
||
},
|
||
{
|
||
"role": "user",
|
||
"content": "客户问题 + 知识库片段"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 10.2 本地模型接口
|
||
|
||
默认适配 Ollama:
|
||
|
||
```text
|
||
POST {baseUrl}/api/chat
|
||
```
|
||
|
||
请求体:
|
||
|
||
```json
|
||
{
|
||
"model": "qwen2.5",
|
||
"stream": false,
|
||
"messages": [
|
||
{
|
||
"role": "system",
|
||
"content": "你是企业微信客服助手,只能基于知识库回答。知识库没有答案时输出 NO_ANSWER。"
|
||
},
|
||
{
|
||
"role": "user",
|
||
"content": "客户问题 + 知识库片段"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
如果本地模型提供 OpenAI-compatible 接口,则用户直接选择 OpenAI 兼容模式即可。
|
||
|
||
### 10.3 Prompt 约束
|
||
|
||
系统提示词建议:
|
||
|
||
```text
|
||
你是企业微信客服助手。
|
||
你只能根据提供的知识库片段回答客户问题。
|
||
如果知识库片段不能支持答案,必须只输出 NO_ANSWER。
|
||
不要编造政策、价格、承诺、库存、物流时效。
|
||
回答要简洁、礼貌、像真人客服。
|
||
如果客户要求人工、投诉、退款、合同、发票、价格特殊审批,也输出 NO_ANSWER。
|
||
```
|
||
|
||
用户消息格式:
|
||
|
||
```text
|
||
客户昵称:{{fromNickName}}
|
||
客户问题:{{question}}
|
||
|
||
知识库片段:
|
||
[1] {{chunk1}}
|
||
[2] {{chunk2}}
|
||
[3] {{chunk3}}
|
||
|
||
请基于知识库回答。
|
||
```
|
||
|
||
### 10.4 AI 无法回答判定
|
||
|
||
以下情况触发转人工:
|
||
|
||
- 知识库最高分低于阈值。
|
||
- AI 返回 `NO_ANSWER`。
|
||
- AI 返回空字符串。
|
||
- AI HTTP 调用失败。
|
||
- AI 超时。
|
||
- AI 返回结构无法解析。
|
||
- 消息是非文本。
|
||
- 客户明确要求人工客服。
|
||
- 客户问题超过最大长度。
|
||
- 命中敏感业务关键词,例如退款、投诉、合同、发票、赔偿等。
|
||
|
||
## 11. 自动回复发送方案
|
||
|
||
### 11.1 私聊回复
|
||
|
||
使用已有模板:
|
||
|
||
```text
|
||
requestdata/sendVWorkTextMessage.json
|
||
```
|
||
|
||
转换后的底层 type:
|
||
|
||
```json
|
||
{
|
||
"type": 11029,
|
||
"data": {
|
||
"conversation_id": "{{conversationId}}",
|
||
"content": "{{answer}}"
|
||
}
|
||
}
|
||
```
|
||
|
||
调用方式:
|
||
|
||
```go
|
||
sendTextToConversation(clientId, conversationId, answer)
|
||
```
|
||
|
||
### 11.2 群聊回复
|
||
|
||
群聊 @ 机器人后,默认回复到原群:
|
||
|
||
```text
|
||
conversationId = 原消息 conversationId
|
||
```
|
||
|
||
可选策略:
|
||
|
||
- 默认不 @ 提问人,只回复文本。
|
||
- 后续可以增加 `replyWithAtSender` 开关,使用 `sendVWorkGroupAtMessage`。
|
||
|
||
初版建议默认不 @,避免 atList 字段兼容问题带来发送失败。
|
||
|
||
## 12. 转人工私信方案
|
||
|
||
### 12.1 私信对象配置
|
||
|
||
用户要求:
|
||
|
||
```text
|
||
直接发私信给指定同事的企业微信
|
||
```
|
||
|
||
因此配置:
|
||
|
||
```json
|
||
{
|
||
"humanUserId": "指定同事user_id",
|
||
"humanConversationId": "指定同事私聊conversation_id"
|
||
}
|
||
```
|
||
|
||
优先级:
|
||
|
||
1. 如果配置了 `humanConversationId`,直接使用。
|
||
2. 如果没有配置,则使用 `S:<robotId>_<humanUserId>` 推导。
|
||
3. 如果推导失败或发送失败,记录错误并在前端状态展示。
|
||
|
||
### 12.2 转人工消息内容
|
||
|
||
默认模板:
|
||
|
||
```text
|
||
客户问题需要人工处理
|
||
|
||
客户:{{customerName}}
|
||
客户ID:{{fromWxId}}
|
||
来源:{{source}}
|
||
会话ID:{{conversationId}}
|
||
问题:{{question}}
|
||
原因:{{reason}}
|
||
时间:{{time}}
|
||
|
||
请及时处理。
|
||
```
|
||
|
||
如果开启 `includeKnowledgeHits`,追加:
|
||
|
||
```text
|
||
知识库候选:
|
||
1. {{source}} / score={{score}}
|
||
2. {{source}} / score={{score}}
|
||
```
|
||
|
||
### 12.3 转人工触发原因枚举
|
||
|
||
建议原因:
|
||
|
||
```text
|
||
knowledge_low_score
|
||
ai_no_answer
|
||
ai_timeout
|
||
ai_http_error
|
||
ai_parse_error
|
||
non_text_message
|
||
manual_keyword
|
||
send_reply_failed
|
||
config_missing
|
||
```
|
||
|
||
前端展示时转成中文。
|
||
|
||
## 13. 状态统计和运行记录
|
||
|
||
新增内存状态对象:
|
||
|
||
```go
|
||
type AutoReplyStatus struct {
|
||
Enabled bool
|
||
Running bool
|
||
LastError string
|
||
|
||
KnowledgeFileCount int
|
||
KnowledgeChunkCount int
|
||
KnowledgeLastIndexedAt int64
|
||
|
||
TodayReceived int
|
||
TodayReplied int
|
||
TodayHandoff int
|
||
TodayIgnored int
|
||
TodayAIFailed int
|
||
|
||
LastMessages []AutoReplyRecord
|
||
}
|
||
```
|
||
|
||
运行记录:
|
||
|
||
```go
|
||
type AutoReplyRecord struct {
|
||
ID int64
|
||
Time string
|
||
RobotID string
|
||
ConversationID string
|
||
Source string
|
||
FromWxID string
|
||
FromNickName string
|
||
Question string
|
||
Action string
|
||
Reason string
|
||
Answer string
|
||
}
|
||
```
|
||
|
||
`Action` 可选:
|
||
|
||
```text
|
||
ignored
|
||
replied
|
||
handoff
|
||
failed
|
||
```
|
||
|
||
## 14. 错误处理策略
|
||
|
||
### 14.1 自动客服不能影响主流程
|
||
|
||
自动客服所有处理都必须:
|
||
|
||
- `recover` panic
|
||
- 写日志
|
||
- 更新状态
|
||
- 不向外抛出导致 `MyRecvCallback` 异常
|
||
|
||
### 14.2 AI 超时
|
||
|
||
默认:
|
||
|
||
```text
|
||
25 秒
|
||
```
|
||
|
||
超时后:
|
||
|
||
- 不继续等待。
|
||
- 转人工。
|
||
- 状态计入 `TodayAIFailed` 和 `TodayHandoff`。
|
||
|
||
### 14.3 发送失败
|
||
|
||
自动回复发送失败时:
|
||
|
||
- 记录失败。
|
||
- 转人工通知。
|
||
- 如果转人工也失败,写入 `LastError`。
|
||
|
||
### 14.4 知识库索引失败
|
||
|
||
单个文件失败:
|
||
|
||
- 不影响其他文件。
|
||
- 在前端显示失败文件。
|
||
- 写入日志。
|
||
|
||
全部失败:
|
||
|
||
- 自动客服仍可开启。
|
||
- 所有问题都会因为知识库低分而转人工。
|
||
|
||
## 15. 实施步骤
|
||
|
||
### 阶段一:配置和接口骨架
|
||
|
||
1. 扩展 `config/types.go`,加入自动客服配置结构。
|
||
2. 修改默认配置生成逻辑。
|
||
3. 在 `app.go` 增加 Wails 方法:
|
||
- `GetAutoReplyConfig`
|
||
- `SaveAutoReplyConfig`
|
||
- `SetAutoReplyEnabled`
|
||
- `GetAutoReplyStatus`
|
||
4. 在 helper HTTP server 增加自动客服路由。
|
||
5. 确认现有编译不受影响。
|
||
|
||
### 阶段二:前端自动客服页面
|
||
|
||
1. 新增 `frontend/src/components/AutoReply.vue`。
|
||
2. 修改 `App.vue` 侧边栏,增加“自动客服”菜单。
|
||
3. 实现配置读取、保存、开关、状态刷新。
|
||
4. 实现测试 AI、测试人工私信、重建知识库按钮。
|
||
5. 保持当前 UI 风格,不做大规模视觉重构。
|
||
|
||
### 阶段三:知识库模块
|
||
|
||
1. 创建 `helper/auto_reply_knowledge.go`。
|
||
2. 实现本地目录扫描。
|
||
3. 实现 `.md/.txt/.csv` 解析。
|
||
4. 实现 `.xlsx/.docx/.pdf` 解析。
|
||
5. 实现 chunk 切分。
|
||
6. 实现索引保存和加载。
|
||
7. 实现关键词/BM25 风格检索。
|
||
8. 将统计信息暴露给状态接口。
|
||
|
||
### 阶段四:AI 模块
|
||
|
||
1. 创建 `helper/auto_reply_ai.go`。
|
||
2. 实现 OpenAI-compatible 调用。
|
||
3. 实现 Ollama 本地模型调用。
|
||
4. 实现 prompt 构造。
|
||
5. 实现 `NO_ANSWER` 判断。
|
||
6. 实现 AI 测试接口。
|
||
|
||
### 阶段五:消息监听和自动回复
|
||
|
||
1. 创建 `helper/auto_reply.go`。
|
||
2. 在 `MyRecvCallback` 中接入自动客服入口。
|
||
3. 确保 ResponseChannel 响应不触发自动客服。
|
||
4. 实现文本消息过滤。
|
||
5. 实现私聊自动处理。
|
||
6. 实现群聊 @ 机器人处理。
|
||
7. 实现去重。
|
||
8. 实现调用知识库 + AI + 发送回复。
|
||
|
||
### 阶段六:转人工私信
|
||
|
||
1. 创建 `helper/auto_reply_handoff.go`。
|
||
2. 实现转人工消息模板渲染。
|
||
3. 实现指定同事私信发送。
|
||
4. 实现测试发送。
|
||
5. 自动回复失败时触发转人工。
|
||
|
||
### 阶段七:测试和验证
|
||
|
||
1. 增加 Go 单元测试。
|
||
2. 增加 mock AI server 测试。
|
||
3. 前端执行 `npm run build`。
|
||
4. Go 执行 `go test ./...`。
|
||
5. 构建 helper。
|
||
6. 构建 Wails。
|
||
7. 实机验证私聊、群聊 @、群聊未 @、转人工。
|
||
|
||
## 16. 测试用例
|
||
|
||
### 16.1 私聊文本自动回复
|
||
|
||
输入:
|
||
|
||
```json
|
||
{
|
||
"event": "20002",
|
||
"data": {
|
||
"conversationId": "S:customer_robot",
|
||
"fromWxId": "customer",
|
||
"toWxId": "robot",
|
||
"message": "你们营业时间是几点?"
|
||
}
|
||
}
|
||
```
|
||
|
||
预期:
|
||
|
||
- 检索知识库。
|
||
- 调用 AI。
|
||
- 发送回复到原 conversationId。
|
||
- 状态 `TodayReplied +1`。
|
||
|
||
### 16.2 群聊未 @ 不回复
|
||
|
||
输入:
|
||
|
||
```json
|
||
{
|
||
"event": "20002",
|
||
"data": {
|
||
"conversationId": "R:group",
|
||
"fromWxId": "customer",
|
||
"message": "你们营业时间是几点?",
|
||
"atWxIdList": []
|
||
}
|
||
}
|
||
```
|
||
|
||
预期:
|
||
|
||
- 不调用 AI。
|
||
- 不发送消息。
|
||
- 状态 `TodayIgnored +1`。
|
||
|
||
### 16.3 群聊 @ 机器人自动回复
|
||
|
||
输入:
|
||
|
||
```json
|
||
{
|
||
"event": "20002",
|
||
"data": {
|
||
"conversationId": "R:group",
|
||
"fromWxId": "customer",
|
||
"message": "@机器人 你们营业时间是几点?",
|
||
"atWxIdList": ["robot"]
|
||
}
|
||
}
|
||
```
|
||
|
||
预期:
|
||
|
||
- 调用知识库。
|
||
- 调用 AI。
|
||
- 回复到原群。
|
||
|
||
### 16.4 知识库无答案转人工
|
||
|
||
输入:
|
||
|
||
```text
|
||
你们能不能帮我办理完全无关的问题?
|
||
```
|
||
|
||
预期:
|
||
|
||
- 检索低分。
|
||
- 不调用或不采纳 AI。
|
||
- 私信指定同事。
|
||
- 状态 `TodayHandoff +1`。
|
||
|
||
### 16.5 AI 返回 NO_ANSWER 转人工
|
||
|
||
AI 输出:
|
||
|
||
```text
|
||
NO_ANSWER
|
||
```
|
||
|
||
预期:
|
||
|
||
- 不回复客户。
|
||
- 私信指定同事。
|
||
- 状态 `TodayAIFailed +1`、`TodayHandoff +1`。
|
||
|
||
### 16.6 AI 超时转人工
|
||
|
||
预期:
|
||
|
||
- 超过 timeout 后触发转人工。
|
||
- 不阻塞后续消息处理。
|
||
|
||
### 16.7 重复消息不重复回复
|
||
|
||
同一个 `serverId` 连续进入两次。
|
||
|
||
预期:
|
||
|
||
- 第一次处理。
|
||
- 第二次忽略。
|
||
|
||
## 17. 验收标准
|
||
|
||
完成后应满足:
|
||
|
||
- 前端有“自动客服”页面。
|
||
- 能一键开启/关闭自动客服。
|
||
- 能配置 AI 接口。
|
||
- 能配置本地知识库目录。
|
||
- 能重建知识库索引。
|
||
- 能测试 AI 连接。
|
||
- 能配置指定人工同事并测试私信。
|
||
- 私聊客户文本消息能自动回复。
|
||
- 群聊只有 @ 机器人时才自动回复。
|
||
- 未 @ 的群消息不会回复。
|
||
- AI 无法回答时能私信指定同事。
|
||
- 自动客服处理失败不影响现有企业微信管理能力。
|
||
- 原有 `http://localhost:10001/` dashboard 和 requestdata 接口保持可用。
|
||
|
||
## 18. 风险点和注意事项
|
||
|
||
### 18.1 conversationId 推导风险
|
||
|
||
私信指定同事最好配置明确的 `humanConversationId`。
|
||
|
||
如果只配置 `humanUserId`,按 `S:<robotId>_<humanUserId>` 推导可能因企业微信内部规则不同而失败,因此必须提供“测试发送”。
|
||
|
||
### 18.2 群聊 @ 判断风险
|
||
|
||
有些事件可能 `atWxIdList` 不完整,因此群聊触发判断应同时支持:
|
||
|
||
- `atWxIdList` 包含机器人 ID
|
||
- 文本内容包含 `@机器人昵称`
|
||
|
||
### 18.3 AI 胡答风险
|
||
|
||
必须强制 AI 只基于知识库回答。
|
||
|
||
如果知识库没有答案,AI 必须输出 `NO_ANSWER`。
|
||
|
||
### 18.4 文件解析依赖风险
|
||
|
||
`.xlsx/.docx/.pdf` 支持会引入新依赖或额外解析逻辑。
|
||
|
||
如果希望第一版更稳,可以先实现 `.md/.txt/.csv`,但本需求要求“全都支持”,因此计划里按全格式支持实现。
|
||
|
||
### 18.5 不阻塞 DLL 回调
|
||
|
||
AI 调用和知识库检索都不能直接阻塞 `MyRecvCallback`。
|
||
|
||
应通过 goroutine + 队列处理。
|
||
|
||
## 19. 建议的默认上线策略
|
||
|
||
第一版建议:
|
||
|
||
- 先只开启私聊自动回复。
|
||
- 群聊只开 @ 机器人触发。
|
||
- 最低知识库分数先设置偏高,例如 `0.4`。
|
||
- AI 无答案严格转人工。
|
||
- 所有转人工消息都附带原始 conversationId 和客户 ID。
|
||
- 开启详细日志,方便回放和排查。
|
||
|
||
## 20. 后续增强方向
|
||
|
||
后续可以继续增加:
|
||
|
||
- 多机器人分别配置知识库。
|
||
- 不同群使用不同知识库。
|
||
- 客户画像和历史上下文。
|
||
- 工单系统对接。
|
||
- 向量检索。
|
||
- 对话记忆。
|
||
- 人工接管后暂停自动回复。
|
||
- 黑名单/白名单群配置。
|
||
- 敏感词和合规审核。
|
||
- 前端消息实时流展示。
|