本文梳理 DGJ2.0 中与智能对话相关的五个子系统:旧版微信 AI 报价、RobotV2 多平台会话状态机、腾讯 IM 群聊与业务消息、AI 控制台辅助能力、Udesk 账号资料同步。
这五部分共享客户、商品、价格、库存和销售订单数据,但并不是同一个系统。
| 子系统 | 主要入口 | 主要职责 | 当前事实源 |
|---|---|---|---|
| 旧版微信机器人 | 微信回调、RobotInquirySer | VIN/关键词/图片/语音识别、报价、下单 | MySQL + Redis 会话 |
| RobotV2 | RobotFacade | 多平台适配、Intent、状态机、统一回复 | 平台会话表 + Context KV |
| 腾讯 IM | IM Center + MQ | 群成员、用户、业务消息、机器人回复 | IM Center + DGJ 关系数据 |
| AI 控制台 | PC 工作台 | 会话场景下查客户、销售、配送等辅助信息 | DGJ 业务表 + 外部服务 |
| Udesk | MQ/CLI 同步 | 将站管家账号资料同步成 Udesk customer | SYS_ADMIN + Udesk |
重要边界:仓库内没有找到 RobotV2 会话直接“转 Udesk 人工坐席”的完整实现。能确认的是 Udesk customer 创建/更新。人工接管是否由 Udesk 页面、IM Center 或其他服务完成,需要结合外部系统确认。
1. 业务目标
- 接收微信、腾讯 IM 等渠道的文本、VIN、图片和语音消息。
- 将不同平台消息转换为统一上下文。
- 识别用户意图和当前会话状态。
- 查询车型、品类、品牌、商品、价格、库存和报价范围。
- 生成文本、图片、卡片或组合回复。
- 支持多轮选择、报价、下单和会话超时。
- 根据门店和账号权限维护 IM 群成员。
- 把站管家账号资料同步到 Udesk,供外部客服系统识别。
- 保留日志、搜索记录和会话上下文以支持排查。
flowchart LR
WX["微信消息"] --> ADAPTER["输入适配器"]
TIM["腾讯IM消息"] --> MQ["IM Center MQ"] --> ADAPTER
API["API调用"] --> ADAPTER
ADAPTER --> INTENT["Handler责任链/Intent"]
INTENT --> SM["RobotV2状态机"]
SM --> QUERY["VIN/商品/价格/库存"]
QUERY --> REPLY["ReplyData"]
REPLY --> SENDER["平台发送器"]
SENDER --> WX
SENDER --> TIM
ADMIN["站管家账号"] --> UDESK["Udesk customer同步"]
2. 适用场景
| 问题 | 重点章节 |
|---|---|
| 微信机器人不回复 | 10、18、27 |
| 腾讯 IM 有消息但机器人不回复 | 11、12、25、28 |
| 同一消息重复回复 | 12、25、30 |
| VIN 解析后会话卡住 | 13、14、29 |
| 多车型选择无效 | 14、29 |
| 图片/语音识别失败 | 15、29 |
| 商品有库存但机器人不展示 | 16、29 |
| 报价一直等待服务站 | 17、31 |
| 回复序号不能下单 | 14、17、29 |
| 分仓账号不在客户群 | 20、32 |
| 禁用账号仍在群里 | 20、32 |
| Udesk 无账号或资料旧 | 23、33 |
| 想接入新聊天平台 | 9、10、34 |
| 修改 RobotV2 后评估影响面 | 35、36 |
3. 系统边界
3.1 哪些数据在 DGJ
| 数据 | DGJ 是否主存 | 说明 |
|---|---|---|
| 服务站、账号、客户、门店关系 | 是 | IM 群成员和 Udesk 同步来源 |
| 机器人会话状态和上下文 | 是 | RobotV2 使用独立会话/Context 表 |
| 机器人关键词、报价范围 | 是 | 决定搜索和展示 |
| 商品、价格、库存 | 是/缓存与外部组合 | 查询链跨物料缓存、服务站价和库存服务 |
| 腾讯 IM 用户、群和消息 | 否 | 主存位于 IM Center/腾讯 IM |
| 微信机器人发送能力 | 否 | 通过 WechatRobot Provider |
| 语音/OCR/意图模型结果 | 否 | 来自 AI Center |
| Udesk customer | 否 | DGJ 只负责推送资料 |
3.2 数据流责任图
flowchart TB
subgraph DGJ["DGJ2.0"]
CONV[("会话/Context")]
GOODS[("商品/价格/库存")]
ORG[("账号/门店/客户关系")]
ROBOT["RobotV2"]
end
IMC["IM Center / 腾讯IM"] --> ROBOT
WXR["微信机器人服务"] --> ROBOT
AIC["AI Center"] <--> ROBOT
ROBOT <--> CONV
ROBOT <--> GOODS
ROBOT <--> ORG
ROBOT --> IMC
ROBOT --> WXR
ORG --> U["Udesk"]
4. 版本关系:V1、RobotV2 与旧 AI
4.1 三条实现并存
| 实现 | 关键类 | 会话特点 | 当前用途 |
|---|---|---|---|
| 旧 AI 智能询价 | SmartInquirySer、AiSer | Redis Hash 保存通知、VIN、sessionKey | AI Center 异步意图和报价 |
| Robot V1 | RobotInquirySer、RobotInquiryBaseSer | 旧会话表 + 大量业务方法 | 微信链路和 V2 兼容依赖 |
| RobotV2 | RobotFacade、Adapters、Handlers、States | 平台会话 + Context KV + 状态机 | 腾讯 IM 主入口及逐步承接微信 |
RobotV2 并未彻底摆脱 V1。以下模块仍直接依赖 RobotInquirySer:
ProductQueryServiceOrderServiceMessageFormatterService- 电池、维保、全车件、产品码等 Strategy
- 多个展示过滤和查询后处理模块
因此不能把 V1 判断为“废代码”后整体删除。
flowchart LR
V1["Robot V1 大服务"] --> DATA["成熟查询/下单能力"]
V2["RobotV2 编排层"] --> STATE["Adapter + Intent + State"]
V2 --> DATA
OLD["旧 AiSer"] --> AIC["AI Center异步回调"]
V2 --> AIC
5. 代码入口地图
5.1 消息入口
| 入口 | 文件 | 用途 |
|---|---|---|
| 腾讯 IM MQ | application/controllers/tasks/ImCenterNotify.php | 消费 IM 业务消息并调用 RobotV2 |
| 微信内部入口 | application/controllers/inner/moveMall/Robot.php | 微信消息、异步处理、RobotV2 AI 回调 |
| 移动商城入口 | application/controllers/moveMall/RobotController.php | 机器人页面/接口入口 |
| 老会话任务 | application/controllers/tasks/RobotConversation.php | Redis 延迟消息和机器人会话任务 |
| 旧 AI 回调 | application/controllers/Ai.php | AI Center 通知和报价展示 |
| IM 工作台 | application/controllers/moveMall/ImWorkbench.php | 语音解析等工作台能力 |
| AI 控台配送 | application/controllers/aiConsole/Courier.php | 骑手/配送辅助,详见第 38 篇 |
5.2 RobotV2 目录职责
| 目录 | 职责 |
|---|---|
Adapters/Input | 平台消息转 MessageContext |
Handlers | 按优先级识别 Intent |
Intents | 表达用户意图和解析结果 |
States | 多轮会话业务状态 |
Conversation | 会话策略、Repository、Context |
Services/Query | 维保、易损、全车件、电池查询 |
Pipelines/Filters | 品类、价格、库存过滤 |
Services/Display | 分组、展示、过滤原因 |
Adapters/Output | 平台回复格式和发送 |
Strategies | 复用 V1 的场景查询/下单策略 |
database | 会话和 Context 迁移脚本 |
5.3 外部 Provider
| Provider | 外部能力 |
|---|---|
ImCenterProvider | UserSig、发消息、群用户、同步结果 |
IMProvider | 用户批建、群成员变更、群详情/状态 |
AiCenterProvider | 文本意图、语音、音频转换、OCR |
WechatRobotProvider | 微信群/私聊发送 |
Udesk Customers Create/UpdateProvider | customer 创建和更新 |
6. 核心表
6.1 旧机器人与报价表
| 常量/物理表 | 用途 |
|---|---|
SCM_ROBOT_CONVERSATION / t_scm_robot_conversation | 旧机器人会话 |
SCM_ROBOT_PRE_CONVERSATION / t_scm_robot_pre_conversation | 旧预会话 |
SCM_ROBOT_CONVERSATION_RECORD | 对话记录 |
SCM_ROBOT_SEARCH_LOG | 搜索日志 |
SCM_ROBOT_IMAGE_PARSE | 微信图片解析 |
SCM_ROBOT_CONVERSATION_KEYWORD | 会话关键词 |
SCM_ROBOT_NOCAR_KEYWORD | 无车关键词 |
SCM_ROBOT_POSITION_KEYWORD | 方位关键词 |
SCM_ROBOT_FRIEND / SCM_ROBOT_FRIEND_BIND | 好友与真实 wxid 绑定 |
SCM_ROBOT_OFFER_ORDER_APPLY | 等待服务站报价的明细 |
SCM_OFFER_ORDER / SCM_OFFER_ORDER_INFO | 报价单主/明细 |
T_SCM_ROBOT_QUOTE_CONFIG* | 报价范围主/明细 |
6.2 RobotV2 会话表
仓库 SQL 显示至少有以下目标结构:
| 表 | 平台 | 主键/查找键 | 主要状态 |
|---|---|---|---|
t_robot_conversation | 微信 | id、cv_key | status、state、expires_at |
t_robot_conversation_tencent_im | 腾讯 IM | id、cv_key | 同上 |
t_robot_conversation_context | 通用 Context | (conversation_id,key) 唯一 | JSON value、类型、过期时间 |
t_robot_conversation_pre | RobotV2 异步预会话 | 回调关联键 | pending/processing/completed/cancelled/timeout |
erDiagram
ROBOT_CONVERSATION ||--o{ ROBOT_CONVERSATION_CONTEXT : owns
ROBOT_CONVERSATION ||--o{ ROBOT_PRE_CONVERSATION : waits_for
ROBOT_CONVERSATION {
bigint id
string cv_key
int sid
int contact_id
string wxid
string chat_group_id
string status
string state
datetime expires_at
}
ROBOT_CONVERSATION_CONTEXT {
bigint id
bigint conversation_id
string key
mediumtext value
string type
datetime expires_at
}
ROBOT_PRE_CONVERSATION {
bigint id
bigint conversation_id
string status
datetime created_at
}
6.3 表版本警告
application/config/tables.php 仍定义 t_scm_robot_conversation,RobotV2 SQL 则创建 t_robot_conversation 和腾讯 IM 独立表。实际 Repository 使用哪张表必须以运行代码和生产 DDL 为准。
不要只根据 SQL 文件执行迁移;目录中同时存在瘦身、修复、去唯一索引、数据迁移等多版脚本。
7. Redis 数据
7.1 旧会话 Hash
| Key/Field | TTL | 内容 |
|---|---|---|
会话 Key + notify | 默认消息 TTL 600 秒 | 原始微信通知,用于异步回调后回复 |
vin | 会话期 | 当前 VIN |
compressId | 会话期 | 已选车型 ID |
sessionKey | 会话期 | AI Center 多轮 Session |
goodsMap | 会话期 | 回复序号到商品/数量映射 |
storage_id | 会话期 | 供货仓上下文 |
sid/uid/user_name | 会话期 | 业务身份 |
7.2 机器人公共 Key
| 常量 | 用途 |
|---|---|
ROBOT_MSG_CACHE | 机器人消息缓存 |
ROBOT_LINK_CACHE | 链接缓存,默认 7200 秒 |
ROBOT_KEYWORD_COMPARISON | 关键词比对缓存 |
HASH_ROBOT_WECHAT_INFO | 机器人微信信息 |
HASH_ROBOT_REPLY_BLACK_LIST | 回复黑名单 |
ROBOT_INQUIRY_MSG | 询价异步消息 |
ROBOT_INQUIRY_DELAY_QUEUE | Redis 延迟队列 |
ROBOT_INQUIRY_REPLY_FLAG | 是否已回复标记 |
flowchart LR
MSG["微信消息"] --> HASH["会话Redis Hash"]
HASH --> AI["AI Center异步请求"]
AI --> CALLBACK["AI回调"]
CALLBACK --> HASH
HASH -->|"仍有notify"| REPLY["回原群"]
HASH -->|"已过期"| DROP["抛会话已过期"]
8. 会话标识与生命周期
8.1 cv_key
RobotV2 SQL 注释给出的微信会话键格式:
{sid}_{contact_id}_{wxid}_{chat_group_id}
不同平台由各自 ConversationPolicy 生成和判定,不应在业务代码手拼字符串。
8.2 会话状态 status
| 值 | 含义 |
|---|---|
active | 活跃会话 |
closed | 用户主动关闭或自然结束 |
expired | 超时 |
deleted | 逻辑删除 |
8.3 默认过期时间
| 平台 | SQL/策略说明 |
|---|---|
| 微信 | 默认 1 小时 |
| 腾讯 IM | 默认 1 小时 |
| API | 默认 5 分钟 |
| 旧报价等待 | 10 分钟 CONVERSATION_INQUIRY_TIME |
cv_key 在目标 SQL 中是普通索引,不是唯一索引,允许同一个业务键历史上存在多条会话。Repository 查询必须同时限制 active/status/过期时间。
9. RobotV2 总体架构
flowchart TD
RAW["原始平台消息"] --> AF["MessageAdapterFactory"]
AF --> CTX["MessageContext"]
CTX --> HC["Handler Chain"]
HC --> INTENT["Intent"]
INTENT --> CF["ConversationFactory"]
CF --> CM["ConversationManager"]
CM --> SM["State Machine"]
SM --> QS["Query/Order/AI Service"]
QS --> RD["ReplyData"]
RD --> SF["ResponseSenderFactory"]
SF --> OUT["微信或腾讯IM"]
CM <--> DB[("Conversation + Context")]
核心设计原则:
- 平台差异只留在 Adapter、Policy、Repository、Formatter、Sender。
- 业务输入统一为
MessageContext。 - Handler 只负责判断意图,不直接承载完整多轮业务。
- State 负责当前阶段行为和状态迁移。
- 跨请求业务数据进入
ConversationContext,不继续扩大会话主表。
10. 输入适配器
MessageAdapterFactory 当前注册顺序:
TencentImAdapterWechatAdapter- 无匹配时
DefaultAdapter
钉钉 Adapter 文件存在,但默认注册被注释,不能认为线上已支持钉钉。
10.1 统一上下文包含什么
| 类别 | 示例 |
|---|---|
| 追踪 | traceId、原消息 ID |
| 平台 | wechat、tencent_im、api |
| 用户 | userId、角色、wxid/IM ID |
| 会话 | 群 ID、服务站、客户、当前会话 |
| 消息 | 文本、图片、语音、业务 JSON |
| 业务 | VIN、Intent、Metadata、Reply 格式 |
10.2 腾讯 IM 消息示例
{
"msgContext": {
"msgId": "im-message-example",
"msgType": "TIMCustomElem",
"msgTime": 1784185200
},
"fromAccount": {
"userId": "garage-user-example",
"role": "garage",
"bizExtInfo": "{\"sid\":9999,\"contactId\":12345}"
},
"toAccount": [
{
"userId": "station-user-example",
"role": "station"
}
],
"msgContent": {
"text": "查询前刹车片"
}
}
11. Handler 责任链与 Intent 优先级
RobotFacade::initHandlerChain() 注释定义的优先级:
| 顺序 | Handler/意图 | 示例 |
|---|---|---|
| 1 | 特殊指令 | 重置、诊断类指令 |
| 2 | AI 回调 | 图片/AI 异步结果 |
| 3 | 车型选择 | 用户回复车型序号 |
| 4 | 图片消息 | VIN 图片、旧件图片 |
| 5 | VIN 码 | 17 位合法 VIN |
| 6 | 产品码 | 原厂码/产品码 |
| 7 | 无 VIN 关键词 | 轮胎、电池、油品等 |
| 8 | 序列号 | 从上次列表选商品/下单 |
| 9 | 查询条件 | 品类、品牌、方位等 |
| 10 | Fallback | 无法识别或静默忽略 |
flowchart TD
M["MessageContext"] --> S{"特殊指令?"}
S -->|否| A{"AI回调?"}
A -->|否| C{"车型选择?"}
C -->|否| I{"图片?"}
I -->|否| V{"VIN?"}
V -->|否| P{"产品码?"}
P -->|否| N{"无车关键词?"}
N -->|否| O{"序号下单?"}
O -->|否| Q{"查询条件?"}
Q -->|否| F["Fallback"]
S --> R["IntentResult"]
A --> R
C --> R
I --> R
V --> R
P --> R
N --> R
O --> R
Q --> R
优先级的价值是避免同一文本被多个规则同时命中。例如 17 位 VIN 也可能被通用产品码正则误判,必须让 VIN 先处理。
12. 腾讯 IM 消息主流程
Destination:imcenter_notify,Routing Key:im_app_biz_message。
sequenceDiagram
participant TIM as 腾讯IM
participant IC as IM Center
participant MQ as RabbitMQ
participant C as ImCenterNotify
participant R as RobotFacade
participant DB as 会话/业务数据
TIM->>IC: 用户业务消息
IC->>MQ: im_app_biz_message
MQ->>C: messageData
C->>IC: msgSyncResult(msgId)
C->>R: handleMessage(tencent_im,json,true)
R->>DB: 加载/创建会话和上下文
R->>R: Adapter -> Intent -> State
R->>IC: TencentImResponseSender发送回复
R-->>C: 处理结果
C-->>MQ: ACK
12.1 ACK/NACK 规则
| 异常 | 结果 |
|---|---|
| 正常 | ACK |
InvalidArgumentException | ACK,不重试 |
BizErrorException | ACK,不重试 |
ParamsErrorException | ACK,不重试 |
错误码 400/404/422 | ACK,不重试 |
| 其他系统异常 | NACK,可重试 |
12.2 两层异常语义冲突
ImCenterNotify 希望 RobotFacade 抛出系统异常以便 NACK;但 RobotFacade::handleMessage() 自己 catch Exception 后返回 formatError(),不会继续抛出。
这意味着大量内部异常可能被消费者当成“正常返回”并 ACK。只有未被 catch 的 Throwable 才能到达消费者重试逻辑。
12.3 同步确认时点
消费者在 RobotFacade 处理前就调用 msgSyncResult(msgId),而且确认失败只记 warning,不阻断处理。
这个接口表示“已收到”还是“已处理成功”需要与 IM Center 确认。若外部将其理解为处理成功,则后续 Robot 失败不会再补发。
13. RobotV2 状态机
13.1 状态字典
| 状态 | 类型 | 含义 |
|---|---|---|
idle | 稳定 | 等待新意图 |
vin_parsing | 瞬态 | 解析 VIN |
vin_parsed | 稳定 | 已有车型,等待查询条件 |
car_model_confirming | 稳定 | 多车型待用户选择 |
product_querying | 瞬态 | 查询商品 |
product_list | 稳定 | 已展示商品,等待筛选/序号 |
order_placing | 瞬态 | 创建订单 |
image_recognition | 瞬态 | 图片识别 |
ai_parsing | 瞬态 | AI 异步解析 |
expired | 稳定 | 会话过期 |
stateDiagram-v2
[*] --> idle
idle --> vin_parsing: VIN
vin_parsing --> vin_parsed: 单车型
vin_parsing --> car_model_confirming: 多车型
car_model_confirming --> vin_parsed: 选择车型
vin_parsed --> product_querying: 品类/关键词
idle --> product_querying: 无车查询/产品码
product_querying --> product_list: 查询成功
product_list --> product_querying: 继续筛选
product_list --> order_placing: 回复序号
order_placing --> product_list: 下单完成或返回列表
idle --> image_recognition: 图片
image_recognition --> ai_parsing: 异步AI
ai_parsing --> product_querying: 回调得到条件
idle --> expired: 超时
vin_parsed --> expired: 超时
product_list --> expired: 超时
13.2 状态循环
to() 表示立即进入下一状态继续执行,stay() 表示停止并等待下一条用户消息。单条消息最多循环 10 次,防止状态转换死循环。
flowchart TD
A["确定当前状态"] --> B["handleMessage"]
B --> C{"Transition"}
C -->|stay| END["保存最后回复并结束"]
C -->|to next| D["onExit/onEnter"]
D --> E["持久化conversation.state"]
E --> F{"迭代 < 10?"}
F -->|是| B
F -->|否| WARN["告警并强制结束"]
13.3 状态持久化失败
saveStateToConversation() 更新失败只记 error,不抛异常。当前请求仍可能回复成功,但下一条消息会从旧状态继续,表现为“机器人忘了上一轮”。
14. 会话 Context
Context 使用 (conversation_id,key) 唯一约束保存 JSON。关键类别:
| Context Key | 内容 |
|---|---|
cv_ctx.vin_info | VIN、品牌、车型、解析数据 |
cv_ctx.vin_raw_parse_result | 原始 VIN 多车型结果 |
cv_ctx.car_models | 待选择车型列表 |
cv_ctx.selected_car_model_index | 用户选择序号 |
cv_ctx.confirmed_car_model | 最终车型 |
cv_ctx.last_query_context | 最近查询条件 |
cv_ctx.query_history | 查询历史,注释约定最多 10 条 |
cv_ctx.user_filters | 用户筛选 |
cv_ctx.query_result_summary | 总数、分页摘要 |
cv_ctx.last_query_result | 最近商品结果 |
cv_ctx.history_query_result | 历史商品,按 invId 去重 |
cv_ctx.query_products | 当前展示商品列表 |
14.1 为什么用 Context 表
flowchart LR
BEFORE["会话主表不断加VIN/品类/Intent字段"] --> PROBLEM["平台耦合、迁移频繁"]
PROBLEM --> AFTER["主表只留身份和状态"]
AFTER --> KV["业务数据进入Context KV"]
KV --> BENEFIT["按Key演进、统一序列化、独立过期"]
14.2 使用约束
- 所有 Key 应在
ConversationContextKeys定义。 - 禁止业务代码随意写字符串 Key。
value为 JSON,读取时按type还原。- 大列表进入
MEDIUMTEXT,但仍应控制体积;商品结果过大将增加每轮 DB I/O。 - 删除会话时是否级联清理 Context 需以生产 DDL/任务确认。
15. VIN、图片和语音
15.1 VIN
旧枚举使用:
/^[A-HJ-NPR-Za-hj-npr-z0-9]{17}$/
同时排除以 XD/XS 开头的业务单号,避免把销售/出库单误识别为 VIN。
VIN 解析可能返回一个车型或多个车型。多车型进入 car_model_confirming,用户回复序号后保存确认车型。
15.2 语音
sequenceDiagram
participant P as 微信/腾讯IM
participant A as Input Adapter
participant AI as AI Center
participant R as RobotV2
P->>A: voice URL/消息
A->>AI: parseVoice
AI-->>A: translation
A->>R: 识别文本进入Intent链
AiSer::parseVoice() 直接取 translation,无内容时返回空字符串。腾讯 IM 和微信 Adapter 都会调用它。
15.3 旧件图片
AiSer::parsePart() 调 AI Center OCR,返回品牌、名称、型号、规格、OE 和其他信息。RobotV2 对油品/轮胎旧件识别失败有短路逻辑:
- 客户配置
old_part_reply=1:回复“识别失败,请用 VIN 或联系人工”。 - 未开启:静默忽略。
15.4 异步预会话
图片/AI 处理可能先创建 pre_conversation,回调再关联正式 conversation_id。状态通常为:
stateDiagram-v2
[*] --> pending
pending --> processing
processing --> completed
pending --> cancelled
processing --> cancelled
pending --> timeout: 超过约5分钟
processing --> timeout: 超过约5分钟
TencentImPreConversationRepository 提供 findTimeoutList(300) 和 markTimeout(300);清理任务是否部署需环境确认。
16. 商品查询和展示
RobotV2 查询层按场景分发:
| Query Service | 场景 |
|---|---|
UpkeepPartsQueryService | 维保件 |
WearingPartsQueryService | 易损件 |
CarPartsQueryService | 全车件 |
BatteryQueryService | 蓄电池 |
NoVinKeywordQueryService | 无车关键词 |
16.1 典型查询链
flowchart TD
I["Intent/QueryContext"] --> D["QueryDispatcher"]
D --> S["场景QueryService"]
S --> L["旧RobotInquiry能力/商品服务"]
L --> P["Pipeline"]
P --> C["CategoryFilter"]
C --> PR["PriceRangeFilter"]
PR --> ST["StockFilter"]
ST --> PP["PostProcessor排序/仓库明细"]
PP --> DS["DisplayService"]
DS --> F["平台Formatter"]
16.2 展示不等于原始查询结果
商品会继续经过:
- 报价范围过滤;
- 价格范围过滤;
- 库存过滤;
- 最小用量、配置项和仓库明细处理;
- OE/配置/标准分类分组;
- 平台卡片容量限制。
所以“搜索服务有商品但机器人不展示”应比较每层输入输出,不能只查 ES 或数据库。
17. 旧 AI 报价单流程
17.1 AI 意图回调
AiSer::aiCenterNotify() 根据 intentionBucket:
| Bucket | 动作 |
|---|---|
PURCHASE | 构建报价单并进入报价/下单 |
QUOTE | 同上 |
JUSTCHATTING | 丢弃,不回复 |
MULTIPLECHATS | 直接返回 AI 多轮响应 |
| 其他 | 交给 OilService |
回调先用 sid + chatId + wxId 生成 Redis 会话 Key。若 notify 已过期,抛“会话已过期”,无法回原群。
17.2 报价生成
sequenceDiagram
participant AI as AI Center
participant AS as AiSer
participant P as 价格服务
participant DB as 报价表
participant WX as 微信机器人
AI->>AS: PURCHASE/QUOTE + items
AS->>P: 查询移动商城价和历史最高报价
AS->>DB: 开事务
AS->>DB: 插报价主单/明细/待报价申请
alt 至少一个商品有价格
AS->>WX: 报价单已生成 + 小程序链接
else 全部待报
AS->>WX: 等待服务站报价
end
AS->>DB: 提交
报价单号格式为 BD + sid + 时间 + 随机数。
17.3 服务站补报价
sendOfferSheet():
- 前端提交
applyId + salePrice。 - 校验申请仍存在、价格大于 0、只提交一张报价单。
- 要求保存的
conversation_key仍能从 Redis 取到notify。 - 更新申请、报价主单和明细。
- 生成小程序链接并发回原微信群。
当前该方法没有显式数据库事务。若数据库更新后微信群发送失败,报价已生效但客户未收到链接。
18. 微信机器人主流程
sequenceDiagram
actor C as 客户
participant WR as 微信机器人服务
participant IN as inner/moveMall/Robot
participant V1 as RobotInquirySer
participant V2 as RobotFacade
participant DB as 会话/业务数据
C->>WR: 文本/图片/语音
WR->>IN: 机器人通知
IN->>V1: onMessageAsync/预处理
V1->>V2: 部分场景进入V2
V2->>DB: 会话、VIN、查询、报价
V2-->>WR: 微信格式回复
WR-->>C: 群/私聊消息
微信入口文件较大,包含旧链、RobotV2 回调、测试/兼容方法。排查时用 requestID/traceId 串联日志,而不是按类名推断唯一流程。
19. ReplyData 与多平台发送
RobotV2 统一回复类型:
| 类型 | 类 | 用途 |
|---|---|---|
| 文本 | TextReply | 提示、错误、普通结果 |
| 图片 | ImageReply | 图片结果 |
| 卡片 | CardReply | 商品/车型卡片 |
| 自定义 | CustomReply | 平台业务消息 |
| 组合 | CompositeReply | 多段混合回复 |
ResponseSenderFactory 当前注册:
wechat -> WechatResponseSendertencent_im -> TencentImResponseSender
钉钉发送器存在但未注册。
flowchart LR
RD["ReplyData"] --> FF["平台Formatter"]
FF -->|wechat| WF["Wechat格式"]
FF -->|tencent_im| TF["腾讯IM JSON/卡片"]
WF --> WS["WechatResponseSender"]
TF --> TS["TencentImResponseSender"]
20. IM 群成员维护
群成员不是静态配置,而是由“客户分仓关系 + 账号门店权限 + 账号启停”共同计算。
20.1 客户切换分仓
IMService::changeRoomUserByStore():
- 读取新门店全部账号。
- 首次分配时,把这些账号加入客户群。
- 切换时读取旧门店账号。
- 通过集合差计算入群和退群账号。
- 调 IM Center
changegroupmember。
flowchart TD
A["客户分仓关系变更"] --> N["新门店账号集合"]
A --> O["旧门店账号集合"]
N --> ADD["N - O = 入群"]
O --> REMOVE["O - N = 退群"]
ADD --> IM["IM Center changeGroupMember"]
REMOVE --> IM
20.2 账号门店权限变更
changeRoomUserByRight() 计算新增/删除门店,再查询这些门店关联客户,将账号加入或移出对应客户群。
总仓账号的特殊规则:若 storeDefault 在变更集合中,会加载服务站全部客户。
20.3 账号启用/禁用
changeRoomUserByAdmin():
- 启用:加入当前门店权限涉及的客户群;
- 禁用:退出这些客户群;
- 总仓账号同样可能涉及全部客户。
20.4 E站 APP 查询群内服务站账号
getRoomUserByContactId() 返回分仓账号 + 总仓账号。
当前代码使用 $userList + $defaultUserList 做数字索引数组合并。PHP 的 + 是键并集,索引相同的右侧元素会被丢弃;这可能导致部分总仓账号缺失。应改为 array_merge() 后 array_unique()。
21. IM Center 接口
| 接口 | Provider 方法 | 用途 |
|---|---|---|
/imcenter/ext/zgj/user/batchcreate | IMProvider::userBatchCreate | 批量创建 IM 用户 |
/imcenter/ext/zgj/group/changegroupmember | changeGroupMember | 入群/退群 |
/imcenter/ext/zgj/group/getstatus | groupStatus | 群状态 |
/imcenter/ext/zgj/group/getwithmembers | groupInfo | 群详情和成员 |
/imcenter/ext/zgj/user/getusersig | ImCenterProvider::getUserSig | 腾讯 IM 登录签名 |
/imcenter/ext/zgj/user/sendimmessage | sendImMessage | 发送业务回复 |
/imcenter/ext/zgj/user/getGarageUserByImGroupId | getRepairShopUserByImGroupId | 反查维修厂成员 |
/imcenter/ext/zgj/user/msgsyncresult | msgSyncResult | 消息接收确认 |
IMProvider 默认对用户批建和群成员变更吞掉异常并返回 null,只有 groupInfo/groupStatus 把 throw=true。调用方如果不检查返回值,可能把外部失败当成功。
22. AI 控制台
当前 application/controllers/aiConsole 只发现 Courier.php,主要是配送辅助:
| 方法 | 用途 |
|---|---|
lists | 查询配送员轨迹列表 |
pushTmsTransportCreate | 手工补推 TMS 配送单 |
getTransportByTmsNo | 按 TMS 单号查询 |
getTransportReceiverList | 查询收货人配送列表 |
AI IM 工作台页面还组合客户会话、今日销售出库和三方配送组件,但这些属于页面编排,不代表 aiConsole 目录承载完整 AI 机器人后端。
详见 38_配送履约_自提骑手与三方配送.md。
23. Udesk 同步
23.1 实际同步对象
Udesk 代码从 SYS_ADMIN a 读取站管家账号,并连接同 sid 主账号 SYS_ADMIN b(roleid=0) 获取服务站资料。
因此这里的 Udesk customer 实际代表“站管家账号/服务站用户”,不是 t_bs_contact 修理厂客户。
23.2 字段映射
| Udesk 字段 | DGJ 来源 |
|---|---|
nick_name | 子账号 name |
open_api_token | md5(sid + uid) |
web_token | 同上 |
| 服务站编码自定义字段 | sid |
| 服务站名称 | 主账号 name |
| 手机号 | 子账号 mobile |
| 省/市/区 | 主账号区域编码转名称 |
| 状态 | 正常/禁用 |
23.3 异步更新
Destination:dgj_udesk,Routing Key:udesk_customer_async。
sequenceDiagram
participant DGJ as 账号编辑业务
participant MQ as RabbitMQ
participant C as UdeskNotify
participant S as CustomerService
participant DB as SYS_ADMIN/区域表
participant U as Udesk
DGJ->>MQ: {sids:[...]}
MQ->>C: udesk_customer_async
C->>S: update(messageData)
loop 每批100个账号
S->>DB: 查询账号、主账号和区域
S->>U: PUT by token
end
S-->>MQ: ACK
23.4 全量 CLI
UdeskAsync 提供:
createCustomer():全量创建;updateCustomer():全量更新;updateCustomerByUid($uid):单 UID 修复。
创建/更新逐条 sleep(1),全量任务耗时与账号量线性增长。
23.5 人工接管边界
仓库证据只能确认 Udesk customer 资料同步。未找到:
- RobotV2 创建 Udesk 工单;
- 会话状态改为“人工接管”;
- Udesk 坐席回复回流 RobotV2;
- 机器人恢复接管的状态转换。
因此人工接管流程必须标为外部待确认,不能在本文中假定已完整实现。
24. Udesk 数据风险
| 风险 | 代码表现 | 影响 |
|---|---|---|
| Token 可预测 | md5(sid . uid) 无盐 | 若外部以此作安全凭据,强度不足 |
| 区域可能不存在 | 直接访问 $areaCollection[code]['name'] | 未配置区域时 warning/空字段 |
| 单条异常被吞 | foreach 内 catch 后继续 | MQ 最终 ACK,失败账号不自动重试 |
| 无同步结果表 | 只输出/日志 | 难以统计最后成功时间和失败原因 |
| 全量任务慢 | 每账号 sleep 1 秒 | 大量账号时窗口长 |
| 仅按 sids 更新 | 一个站点全部账号扫描 | 小改动也可能产生大量外部请求 |
Udesk 自定义字段 ID 是外部配置的一部分。若外部重建字段,代码常量必须同步更新。
25. 幂等、重复消息和并发
25.1 IM 消息
msgId 被设置为日志 Request ID,但仓库中未看到 DGJ 本地“已处理 msgId”唯一记录。
RabbitMQ NACK 重投时,RobotFacade 可能再次查询、再次回复或再次下单。是否由 IM Center/发送端按 msgId 去重,需要外部确认。
25.2 会话状态
同一会话并发处理两条消息时可能出现:
sequenceDiagram
participant M1 as 消息1
participant M2 as 消息2
participant DB as Conversation
M1->>DB: 读取 state=product_list
M2->>DB: 读取 state=product_list
M1->>DB: 写 state=order_placing
M2->>DB: 写 state=product_querying
Note over DB: 后写覆盖前写,Context也可能交叉
目标 SQL 没有 version 字段,Repository 是否通过事务/锁串行化需确认。
25.3 报价和下单
goodsMap 存在 Redis,默认会话短 TTL。并发刷新列表后,用户回复旧序号可能映射到新商品。回复卡片应携带稳定商品 ID 或查询版本,而不是只依赖会话内序号。
26. 可观测性
26.1 追踪标识
| 标识 | 来源 | 用途 |
|---|---|---|
msgId | IM Center | MQ 消息和接收确认 |
traceId | MessageContext | RobotV2 全链追踪 |
conversation_id | 会话表 | 状态和 Context |
cv_key | 会话策略 | 按用户/群查活跃会话 |
requestID | 微信入口 | 微信链日志 |
billNo | 报价/销售单 | 业务结果追踪 |
26.2 RobotFacade 调试输出
handleMessage() 在主路径中包含大量 echo,包括适配器、用户、状态和异常堆栈。这对 CLI 调试有帮助,但在常驻消费者中会产生大量标准输出,并可能暴露消息元数据。
建议全部改为结构化 Logger,并按日志级别控制。
27. 微信不回复排查
flowchart TD
A["微信未回复"] --> B{"入口是否收到通知?"}
B -->|否| C["查微信机器人/长连接入口"]
B -->|是| D{"回复黑名单/配置?"}
D -->|禁用| E["符合静默规则"]
D -->|允许| F{"走V1还是V2?"}
F -->|V1| G["查RobotInquiry日志/Redis会话"]
F -->|V2| H["查Adapter/Intent/State/Reply"]
G --> I{"外部AI异步?"}
H --> I
I -->|是| J["查pre_conversation/回调/notify TTL"]
I -->|否| K["查商品过滤或发送Provider"]
检查顺序:
- 原始消息是否进入 DGJ。
- 机器人 wxid、群 ID、客户绑定是否正确。
- 是否在回复黑名单或配置为静默。
- Redis 会话是否仍存在。
- 图片/语音/AI 是否处于预会话等待。
- Intent 和状态是否符合预期。
- 是否产生 ReplyData。
- 微信发送 Provider 是否成功。
28. 腾讯 IM 不回复排查
28.1 分层检查
| 层 | 检查 |
|---|---|
| 腾讯 IM | 消息是否进入 IM Center |
| MQ | imcenter_notify.im_app_biz_message 是否有消息 |
| Consumer | ImCenterNotify 是否常驻、是否 NACK 重试 |
| Adapter | TencentImAdapter::supports/adapt 是否成功 |
| 身份 | bizExtInfo 能否解析 sid/contactId/group |
| 会话 | 腾讯 IM 会话表是否 active、未过期 |
| Intent | Handler 命中哪一类 |
| State | 状态链和最后 ReplyData |
| Sender | sendImMessage 返回是否为 true |
28.2 常用日志关键词
rg -n "收到IM业务消息|消息同步结果通知|IM业务消息处理失败" application/controllers/tasks/ImCenterNotify.php
rg -n "适配完成|责任链完成|状态转换|状态持久化失败|响应发送完成" application/Services/MoveMall/RobotV2/RobotFacade.php
rg -n "TencentImAdapter|TencentImResponseSender|sendImMessage" application/Services/MoveMall/RobotV2 application/Providers/ImCenter
28.3 “Consumer ACK 但没回复”
重点看 RobotFacade 是否返回了 success=false 的格式化错误。由于异常可能在 Facade 内被吞,Consumer 日志仍可能记录“处理完成”并 ACK。
29. 会话卡住排查
29.1 SQL
实际表名先以环境为准。
SELECT id, cv_key, sid, contact_id, wxid, chat_group_id,
status, state, type, created_at, updated_at, expires_at
FROM t_robot_conversation_tencent_im
WHERE cv_key = :cv_key
ORDER BY id DESC
LIMIT 20;
SELECT id, conversation_id, `key`, `type`,
CHAR_LENGTH(`value`) AS value_chars,
updated_at, expires_at
FROM t_robot_conversation_context
WHERE conversation_id = :conversation_id
ORDER BY `key`;
SELECT id, conversation_id, status, created_at, updated_at
FROM t_robot_conversation_pre
WHERE conversation_id = :conversation_id
ORDER BY id DESC;
29.2 判断方法
| 状态 | 可能原因 |
|---|---|
长期 vin_parsing | VIN 外部调用异常、状态未持久化 |
长期 car_model_confirming | 用户序号没被识别或车型 Context 丢失 |
长期 product_querying | 查询异常后失败结果未转换状态 |
长期 image_recognition/ai_parsing | AI 回调未到或预会话未关联 |
product_list 但无商品 Context | 状态写成功、Context 写失败 |
expired 仍被继续使用 | Repository active 判断或时钟问题 |
29.3 安全修复原则
不要只改 state。至少一起检查:
- 会话
status/expires_at; - VIN/车型 Context;
- 最近查询条件和商品列表;
- 预会话状态;
- 是否已经创建报价或订单。
30. 重复回复/重复下单排查
flowchart TD
A["重复回复或下单"] --> B{"msgId相同?"}
B -->|是| C["MQ重投/消费者并发/无幂等"]
B -->|否| D["平台重复推送或用户重复发送"]
C --> E["查同msgId日志次数"]
D --> F["查消息时间和内容"]
E --> G{"是否产生业务单?"}
F --> G
G -->|是| H["按来源消息/会话/商品查重复订单"]
G -->|否| I["仅发送重复,查Sender幂等"]
推荐幂等键:
| 动作 | 建议幂等键 |
|---|---|
| 消息消费 | platform + msgId |
| AI 回调 | 外部 callback ID 或 preConversation ID |
| 报价单 | conversation_id + source_msg_id |
| 序号下单 | conversation_id + list_version + source_msg_id |
| 平台回复 | source_msg_id + reply_type + business_id |
31. 报价排查
31.1 一直“等待服务站报价”
- 查询报价主单和明细。
- 检查商品价格是否为 0、
obsolete=2。 - 查
t_scm_robot_offer_order_apply状态。 - 查服务站页面是否提交
salePrice > 0。 - 检查
conversation_key对应 Redisnotify是否已超过 10 分钟。
31.2 SQL
SELECT billNo, sid, buId, uid, createTime
FROM t_scm_offer_order
WHERE billNo = :bill_no;
SELECT billNo, invId, skuId, price, num, obsolete, isDelete
FROM t_scm_offer_order_info
WHERE sid = :sid AND billNo = :bill_no;
SELECT id, sid, bu_id, bill_no, inv_id, num, status,
source_type, conversation_key
FROM t_scm_robot_offer_order_apply
WHERE bill_no = :bill_no;
31.3 发送失败的一致性
sendOfferSheet() 先更新数据库,再调用微信发送,且没有事务/Outbox。应增加发送状态和补发任务,避免价格已经生效但客户没有收到链接。
32. IM 群成员排查
32.1 期望成员计算
客户当前分仓可用账号
+ 服务站总仓可用账号
- 已禁用/删除账号
= 期望群成员
32.2 查询顺序
- 客户与门店关系是否正确。
- 账号
storeLever/storeDefault/status。 getAccountByStoreLevel()返回。- DGJ 计算的 join/quit 请求日志。
- IM Center
groupInfo()实际成员。
32.3 已确认的集合风险
| 位置 | 风险 |
|---|---|
getRoomUserByContactId | 数字数组用 + 合并会丢右侧同索引账号 |
changeRoomUserByStore | 旧账号为空时差集赋值受 $oldAccounts && 限制,需验证新账号是否未加入 |
| 总仓规则 | storeDefault 与变更集合判断复杂,门店切换需覆盖全客户回归 |
| Provider | 默认吞异常,调用方未必知道入群失败 |
33. Udesk 排查
33.1 单账号未同步
SYS_ADMIN是否存在、未删除。- 同 sid 是否有
roleid=0主账号。 - 主账号省市区编码能否转名称。
- MQ 消息是否包含
sids数组。 dgj_udesk.udesk_customer_async是否消费。- Udesk 按
md5(sid.uid)token 是否能查到 customer。 - 日志中是否有逐账号异常但任务最终 ACK。
33.2 只读 SQL
SELECT a.uid, a.sid, a.username, a.name, a.status, a.mobile,
a.isDelete, b.name AS station_name,
b.province, b.eparchy, b.city
FROM t_sys_admin a
LEFT JOIN t_sys_admin b
ON a.sid = b.sid AND b.roleid = 0
WHERE a.uid = :uid;
33.3 修复入口
单账号可使用 UdeskAsync::updateCustomerByUid($uid),但执行前应确认外部环境和账号,避免本地配置指向非预期 Udesk。
34. 新平台接入方法
接入新平台至少实现:
MessageAdapter:原消息到MessageContext。ConversationPolicy:会话 Key、TTL 和活跃规则。ConversationRepository:平台会话存储。- 平台 Formatter:各类 ReplyData 的表现。
ResponseSender:发送接口。- 在三个 Factory 注册。
- 消息入口和认证。
- 幂等、重试和监控。
flowchart LR
NP["新平台"] --> IA["Input Adapter"]
IA --> CORE["RobotV2 Core"]
CORE --> CP["Conversation Policy/Repo"]
CORE --> OF["Output Formatter"]
OF --> RS["Response Sender"]
RS --> NP
不要在 State 中写平台 if/else;平台差异应留在适配和输出层。
35. 已确认风险清单
| 级别 | 风险 | 影响 |
|---|---|---|
| P0 | RobotFacade catch 后返回错误,不抛给 Consumer | 系统异常可能被 ACK 丢失 |
| P0 | IM 消费未见本地 msgId 幂等表 | 重投可能重复回复/下单 |
| P0 | 并发消息无可见会话 version/锁 | 状态和 Context 相互覆盖 |
| P1 | 状态持久化失败只记日志 | 当前回复成功、下轮从旧状态继续 |
| P1 | V1/V2 仍深度混用 | 删除旧代码会破坏 V2 查询/下单 |
| P1 | sendOfferSheet DB 更新和微信发送非原子 | 报价成功但客户未收到 |
| P1 | IM 用户列表用数组 + 合并 | 总仓账号被丢弃 |
| P1 | 群成员 Provider 默认吞异常 | 业务侧误判同步成功 |
| P1 | Udesk 逐条异常吞掉并最终 ACK | 失败账号无自动重试 |
| P1 | Udesk token 是可预测 MD5 | 不应作为高强度安全令牌 |
| P2 | RobotFacade 大量 echo/异常堆栈 | 日志污染和元数据暴露 |
| P2 | Context 可存大商品列表 | DB I/O 和会话体积持续增长 |
| P2 | preConversation 清理部署未知 | pending/processing 长期堆积 |
| P2 | 钉钉类存在但未注册 | 容易误判为已支持 |
| P2 | Udesk 人工接管链未在仓库闭环 | 业务能力认知偏差 |
36. 改动影响面
36.1 修改 Adapter
回归:平台身份、群 ID、消息类型、语音/图片、业务扩展 JSON、traceId。
36.2 修改 Handler 顺序
回归:VIN 与产品码冲突、数字序号与普通文本、图片与 AI 回调、特殊命令、Fallback 静默。
36.3 修改 State
回归:新会话、历史会话、瞬态连续转换、最大 10 次循环、状态保存失败、过期恢复。
36.4 修改查询/过滤
回归:维保、易损、全车件、电池、无车、价格范围、库存、门店仓、商品分组。
36.5 修改 Context Key
必须同时处理:旧 Key 迁移、读兼容、写新 Key、过期 Context、历史活跃会话。
36.6 修改 V1
先用 rg 查 RobotV2 的 Strategy/Service 是否调用对应方法。V1 不是一个可独立下线的孤岛。
37. 回归测试清单
37.1 输入与 Intent
- [ ] 微信文本、腾讯 IM 文本都进入统一 Context。
- [ ] 合法 17 位 VIN 正确识别。
- [ ]
XD/XS业务单号不误判为 VIN。 - [ ] 产品码、无车关键词、品牌、品类、方位正确识别。
- [ ] 数字序号仅在有商品列表时作为选择/下单。
- [ ] 未知消息按配置回复或静默。
- [ ] 图片和语音识别成功/失败分支完整。
37.2 会话状态
- [ ] 新会话从 idle 开始。
- [ ] 单车型 VIN 进入 vin_parsed。
- [ ] 多车型进入 car_model_confirming 并可选中。
- [ ] 商品查询进入 product_list。
- [ ] 继续筛选合并上轮 QueryContext。
- [ ] 序号下单进入 order_placing 且只创建一次。
- [ ] 会话过期后不复用旧 Context。
- [ ] 单消息状态循环不超过 10 次。
- [ ] 状态保存失败能告警且不误导为完整成功。
37.3 平台回复
- [ ] 文本、图片、卡片、组合回复在微信可用。
- [ ] 同类回复在腾讯 IM 可用。
- [ ]
autoSend=false只返回 ReplyData,不发消息。 - [ ] Sender 失败可被上层识别和重试。
- [ ] 同 msgId 重投不重复回复。
37.4 商品和订单
- [ ] 维保、易损、全车件、电池、无车查询正确。
- [ ] 报价范围、价格、库存过滤有明确原因。
- [ ] 服务站门店仓上下文正确。
- [ ] 列表序号与商品稳定映射。
- [ ] 报价有价/待报价两种分支正确。
- [ ] 补报价后客户收到链接。
- [ ] 报价转订单与普通订单状态一致。
37.5 IM 群
- [ ] 客户首次分仓后正确拉群。
- [ ] 客户切换分仓只增删差集账号。
- [ ] 账号新增/删除门店权限更新对应群。
- [ ] 总仓账号进入所有应有客户群。
- [ ] 账号禁用退出群、启用重新入群。
- [ ] 分仓账号和总仓账号合并不丢成员。
- [ ] Provider 失败不会被当成功。
37.6 Udesk
- [ ] 新账号创建 customer。
- [ ] 姓名、手机号、区域、状态更新。
- [ ] 缺区域编码时有默认值而非 warning。
- [ ] 单账号失败可记录并重试。
- [ ] 全量任务可断点续跑。
- [ ] 禁用/删除账号的外部状态符合业务规则。
38. 常用 SQL 与 Redis
38.1 查旧机器人会话
SELECT *
FROM t_scm_robot_conversation
WHERE sid = :sid
AND contact_id = :contact_id
ORDER BY id DESC
LIMIT 20;
38.2 查搜索日志
SELECT id, sid, contact_id, create_time
FROM t_scm_robot_search_log
WHERE sid = :sid
AND contact_id = :contact_id
ORDER BY id DESC
LIMIT 100;
字段以生产 DDL 为准,先 DESC 再补充查询列。
38.3 Redis 只读
redis-cli HGETALL '<conversation-key>'
redis-cli TTL '<conversation-key>'
redis-cli HGET '<prefix>hash_robot_reply_black_list' '<field>'
redis-cli ZCARD '<prefix>robot_inquiry:delay_queue'
会话数据可能含用户消息、手机号、VIN 等信息。排查截图和公开文档必须脱敏。
39. 常用代码定位
rg -n "handleMessage|initHandlerChain|executeStateMachine|saveStateToConversation" application/Services/MoveMall/RobotV2/RobotFacade.php
rg -n "class .*Handler|new .*Handler" application/Services/MoveMall/RobotV2/Handlers application/Services/MoveMall/RobotV2/RobotFacade.php
rg -n "StateNames::|StateTransition::|stay\(|to\(" application/Services/MoveMall/RobotV2/States
rg -n "ConversationContextKeys::" application/Services/MoveMall/RobotV2 | sort
rg -n "RobotInquirySer" application/Services/MoveMall/RobotV2
rg -n "DEST_IMCENTER_NOTIFY|TYPE_IMCENTER_APP_BIZ_MESSAGE|msgSyncResult" application
rg -n "changeRoomUserByStore|changeRoomUserByRight|changeRoomUserByAdmin" application
rg -n "DEST_UDESK|udesk_customer_async|CustomerService" application
rg -n "showOfferSheet|aiCenterNotify|offerSheet|sendOfferSheet|parseVoice|parsePart" application/Services/Ai application/controllers
40. 发布与运行检查
| 检查 | 方式 |
|---|---|
| IM Consumer | tasks/imcenternotify/consume 常驻状态和最新日志 |
| Udesk Consumer | tasks/udesknotify/consume 常驻状态 |
| 微信入口 | 测试机器人是否在线并能收消息 |
| AI 回调 | 各环境回调 URL 可达,不公开具体内网地址 |
| 会话 DDL | 表、字段、索引与 Repository 一致 |
| Context 清理 | 过期任务最近运行时间 |
| preConversation 清理 | pending/processing 超时数量 |
| Redis | 会话 Key 前缀、TTL 和延迟队列 |
| IM Center | UserSig、群详情和发消息只读/测试账号验证 |
| Udesk | 测试账号 create/update 验证 |
41. 监控指标建议
| 指标 | 建议告警 |
|---|---|
| IM MQ 消费失败/NACK | 连续增长或超过重试阈值 |
| 同 msgId 处理次数 | >1 记录并检查是否幂等 |
RobotFacade success=false 但 Consumer ACK | >0 立即告警 |
| 状态持久化失败 | >0 立即告警 |
| 状态机达到 10 次上限 | >0 立即告警 |
| preConversation pending 超 5 分钟 | 持续增长告警 |
| 会话 Context 平均/最大体积 | 超基线告警 |
| 机器人回复成功率和 P95 延迟 | 按平台和 Intent 统计 |
| 群成员同步失败率 | >0 应可重试 |
| Udesk 同步失败账号数 | >0 进入失败队列 |
| 报价生成后未发送链接数 | >0 补发 |
42. 证据、推断与待确认
42.1 代码已确认
- RobotV2 当前输入支持腾讯 IM、微信,Default Adapter 兜底。
- 会话工厂支持微信、腾讯 IM、API 三种 Policy/Repository。
- 输出发送器注册微信和腾讯 IM,钉钉未启用。
- Handler 有明确优先级,状态机最多循环 10 次。
- 状态分瞬态与稳定态,Context 采用 KV JSON。
- 腾讯 IM 通过
imcenter_notify.im_app_biz_message进入。 - IM 消费者按业务异常 ACK、系统异常 NACK。
- RobotFacade 自己会格式化捕获的
Exception。 - IM 群成员由门店关系和账号权限变化驱动。
- Udesk 同步来源为
SYS_ADMIN,不是修理厂联系人表。
42.2 根据代码推断
- RobotFacade 内部错误可能被消费者 ACK,需要用真实异常测试确认。
- 同会话并发消息可能覆盖状态/Context,需要看 Repository 和队列分区。
- Udesk token 如果参与鉴权则安全强度不足;若只是外部幂等标识,风险较低。
- V1 仍是 V2 的业务能力库,短期不能整体下线。
42.3 待环境确认
- 生产实际会话表名、DDL 和迁移版本。
- IM MQ 是否按群/会话保序、NACK 最大重试和 DLQ。
- IM Center 是否按 msgId 去重发送。
- preConversation 超时任务是否部署。
- RobotConversation Redis 延迟任务是否仍在线使用。
- Udesk 是否有坐席接管机器人会话的外部能力。
- 微信机器人 V1/V2 流量比例和灰度开关。
- AI Center 回调 SLA、重试和幂等 ID。
- 会话/消息数据保留周期与隐私要求。
43. 一页式排查卡
1. 拿到平台、sid、contactId、群ID、msgId/traceId、发生时间。
2. 确认入口:
微信 -> inner/moveMall/Robot
腾讯IM -> imcenter_notify.im_app_biz_message
旧AI -> AiSer / AI Center callback
Udesk -> dgj_udesk.udesk_customer_async
3. 确认链路:
Adapter -> Intent -> Conversation -> State -> Query/Order -> Reply -> Sender
4. 查会话:
status/state/expires_at
Context中的VIN、车型、查询条件、商品列表
preConversation是否超时
5. 查结果:
有Reply无消息 -> Sender/外部平台
无Reply且success -> Fallback或静默配置
success=false仍ACK -> RobotFacade异常吞掉
重复回复/下单 -> msgId幂等和并发
6. 群成员问题:
客户分仓 + 账号门店权限 + 启停状态
对比DGJ期望集合与IM Center实际成员
7. Udesk问题:
注意同步对象是SYS_ADMIN账号
查区域、MQ、逐账号异常和外部token记录
44. 相关文档
06_移动商城_E站_智能询价与机器人.md:移动商城总体入口。13_MQ回调与补偿任务地图.md:IM/Udesk MQ 运行视角。21_搜索缓存与索引.md:商品搜索、缓存和索引。24_本地开发联调和日志定位.md:CLI Consumer 与日志。25_高风险公共文件影响面.md:修改机器人公共服务的影响评估。38_配送履约_自提骑手与三方配送.md:AI 控台配送能力。41_报价_智能询价_报价规则专题.md:报价规则深挖。
45. 最终结论
智能对话主链应被理解为:
平台消息
-> 统一上下文
-> Intent优先级
-> 平台隔离会话
-> 状态机与Context
-> 商品/价格/库存/订单能力
-> 统一ReplyData
-> 平台发送
维护时最重要的四个原则:
- 平台差异留在边界层:不要把微信/腾讯 IM 判断散落到业务 State。
- 状态与 Context 同步演进:只改状态或只改业务数据都会让下一轮对话失忆。
- 消息和业务动作必须幂等:MQ 重试、AI 回调和用户连发都是正常场景。
- V1/V2/Udesk 边界要以调用证据为准:存在同名类或目录不代表功能已经迁移或人工接管已经闭环。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| IM 入站 | IM Center MQ/长链路 | platform、用户、会话、消息、message ID | tasks/ImCenterNotify.php | Robot/IM Service | conversation ID | 用户消息进入会话记录与状态机 |
| RobotV2/AI | /inner/moveMall/Robot/Ai.php | conversation/request ID、场景、上下文 | Robot/Ai Controller | RobotFacade/AI Provider | request+conversation ID | AI 请求、异步结果和回复记录 |
| 人工工作台 | moveMall/ImWorkbench/AI console | 会话、客服、接管/转派动作 | Workbench Controller | Conversation/Udesk Provider | conversation/udesk ID | 机器人转人工和客服处理状态 |
| 定时会话任务 | tasks/RobotConversation | 超时窗、会话状态、批次 | CLI task | Conversation Service | conversation ID | 关闭超时会话或补发消息 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | IM 消费 | ImCenterNotify | message ID、platform、user、conversation | ACK 且消息记录一次写入 | 重复消息、用户/会话映射失败 | conversation ID 进入 Robot | | AI 调用/回调 | Robot/Ai/Provider | request ID、conversation ID、model/scenario | 外部受理,回调完成 | timeout、解析失败、旧回调 | request ID 关联记录/状态 | | 人工接管 | Workbench/Udesk Provider | conversation、agent、udesk ticket ID | owner/state 改为人工并收到回执 | 双重接管、外部工单未建 | ticket ID + conversation | | 超时任务 | RobotConversation task | task batch、conversation、last activity | 仅超时会话关闭/提醒 | 活跃会话误关、重复推送 | 批次+会话状态回查 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 接收消息 | ImCenterNotify/ConversationManager | 消费事务 | message 幂等键、用户/活跃会话 | SCM_ROBOT_CONVERSATION(_RECORD)/预会话 | record insert;会话 last_time 更新 | message ID、conversation、记录数 |
| 机器人分发 | RobotFacade/QueryDispatcher | 会话事务 | state、scenario、历史消息 | 会话状态、搜索/图片解析日志、AI 请求 | state old -> waiting;request ID/日志 insert | conversation/request ID |
| AI 结果 | Robot callback Service | 回调事务 | waiting request、回调版本 | conversation/record、IM MQ | waiting -> completed/failed;reply record insert | request ID、回复条数、MQ ACK |
| 人工接管 | ImWorkbench/Udesk | 本地与外部非原子 | 当前 owner/state、客服 | 会话/好友绑定、外部 ticket | owner robot -> agent;ticket ID 保存 | conversation+agent+ticket |
| 超时关闭 | RobotConversation task | 每会话事务 | last activity、当前 state | conversation、通知 MQ | active/waiting -> closed/timeout;已关闭不重复通知 | batch、会话前后态 |
子模块追踪:im-ingress 腾讯 IM 消息入站
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 消费消息 | IM Center 投递文本/媒体事件 | message ID、conversation/user、event time | application/controllers/tasks/ImCenterNotify.php | 消息幂等、用户绑定、活跃/预会话和事件顺序 | 单消息本地事务 insert conversation record,last_time old -> event time | message ID + conversation/user + record ID | 重复 0 新记录;旧事件不回退会话时间/状态 |
| 分发后置 | 新消息进入机器人/人工路由 | conversation/record ID、owner/state | application/Services/MoveMall/RobotV2/RobotFacade.php | 当前 owner、scenario、上下文和接管态 | 会话本地事务 state old -> processing/waiting;AI/IM 出站事务外 | message + conversation + request ID | 本地已入站只补分发/回复,不重插消息 |
子模块追踪:robot-context RobotV2 会话 Context 与状态机
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 建立上下文 | 首条/后续用户消息 | conversation ID、scenario、record/message ID | application/Services/MoveMall/RobotV2/RobotFacade.php | 用户站点、历史消息、当前 state/intent 和预会话 | 会话本地事务 context/version old -> new、state -> waiting | conversation + context version + request ID | 并发用版本/锁防覆盖;关闭会话不接受旧消息推进 |
| 状态回查 | 会话卡 waiting/状态错 | conversation、last request、records | application/controllers/tasks/RobotConversation.php | 状态历史、请求结果、人工 owner 和 last_time | 查询只读 不写 | conversation + request IDs + timeline | 先定位缺失 AI 回调/IM 发送/超时段,再定向补偿 |
子模块追踪:robot-media VIN、图片与语音解析
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 媒体解析 | 用户发送 VIN/图片/语音 | message/conversation ID、media ID/type | application/Services/MoveMall/RobotV2/RobotFacade.php | 文件可访问、媒体类型、会话和解析缓存 | 外部 AI/识别事务外;本地 request/log none -> pending/completed/failed | message + conversation + media/request ID | 文件/识别失败明确降级,不伪造 VIN/文本 |
| 解析回填 | 异步识别结果 | request ID、result version | application/controllers/moveMall/RobotController.php | waiting request、当前会话和重复回调 | 回调本地事务写解析记录、context old -> parsed | request + conversation + result version | 旧结果不覆盖新上下文;敏感 VIN/媒体日志脱敏 |
子模块追踪:robot-query 机器人商品查询与展示
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 商品查询 | 解析意图后查配件/库存/价格 | conversation/request、sid、query/SKU | application/Services/MoveMall/RobotV2/RobotFacade.php | context、商品索引、站点价格库存和业务可用性 | 查询只读;保存查询日志/展示快照,不改商品库存 | request + conversation + query + result IDs | 搜索无结果与商品不可售分开;缓存旧只补索引/缓存 |
| 展示发送 | 结果卡片发 IM | conversation、result IDs、out message ID | application/controllers/inner/moveMall/Robot.php | 查询快照、会话 owner/state 和发送状态 | IM 调用事务外;本地 reply record pending -> sent/failed | conversation + request/out message IDs | 发送失败只补回复,不重复查询/下单 |
子模块追踪:robot-quote 旧 AI 报价与人工报价
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| AI 报价 | 旧 Ai 控制器/机器人请求报价 | quote/request ID、conversation、items | application/controllers/Ai.php | 用户、商品、价格规则、旧报价和状态 | 报价本地事务 insert/update status none -> pending/completed;AI 调用事务外 | request/quote + conversation + item count | 超时不视为无价;按 request ID 回查后再重试 |
| 人工报价 | 客服接管编辑/确认报价 | quote ID、agent、price/version | application/controllers/aiConsole/Courier.php | 当前报价、权限、版本和客户会话 | 本地事务 owner robot -> agent、price/version old -> new | quote + agent + before/after version | 并发版本冲突不覆盖;发送失败只补消息 |
子模块追踪:wechat-robot 微信机器人消息链
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 微信入站 | 长连接/网关转发微信消息 | platform message ID、user/openId hash、conversation | application/controllers/moveMall/RobotController.php | 去重、用户绑定、会话和消息类型 | 入站本地事务 insert record、会话激活;后续 Kafka/IM/AI 事务外 | platform/message + conversation + record ID | 重复消息 0 新记录;凭证和 openId 脱敏 |
| 回复出站 | 机器人/人工结果发微信 | conversation、reply/message ID | application/Services/MoveMall/RobotV2/RobotFacade.php | reply record、通道状态和幂等键 | 外部发送事务外;本地 pending -> sent/failed | conversation + reply/external message IDs | 超时查通道送达;只补发送,不重复业务动作 |
子模块追踪:im-group IM 群成员维护
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 成员变更 | 建群、加人、移除、角色调整 | group ID、user IDs、action/version | application/controllers/moveMall/ImWorkbench.php | 本地成员、权限、群状态和外部映射 | 本地关系事务 old member set -> new;IM 调用事务外 | request ID + group + user/action + external code | 外部 timeout 按 group/user 查实际成员,避免重复添加 |
| 一致性回查 | 本地/腾讯 IM 群成员不一致 | group ID、both member sets | application/controllers/tasks/ImCenterNotify.php | 两端集合、事件版本和最后同步时间 | 查询只读;确认后幂等 upsert/delete 本地关系 | group + diff + message/version | 外部为通信事实,本地权限仍需校验;小批修复 |
子模块追踪:udesk-sync Udesk 同步与人工接管
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 人工接管 | 会话转 Udesk 客服 | conversation、agent/ticket ID、reason | application/controllers/moveMall/ImWorkbench.php | 当前 robot owner/state、客服和既有 ticket | 本地会话事务 owner robot -> agent/pending;Udesk 调用事务外 | request ID + conversation + local/external ticket | timeout 按 conversation 查 ticket,禁止重复工单 |
| 回调同步 | Udesk 工单/消息/结束回调 | message ID、ticket/conversation、status | application/controllers/tasks/ImCenterNotify.php | 映射、当前 owner/state 和回调版本 | 单回调本地事务 pending/agent -> active/closed、写人工消息 | message + ticket/conversation + status | 乱序不回退 closed;只补消息/状态,不重建会话 |
子模块追踪:robot-timeout 重复消息、并发与超时任务
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 并发幂等 | 同 message/request 多次到达 | message/request/conversation IDs | application/Services/MoveMall/RobotV2/RobotFacade.php | 已有 record/request、context version 和当前 state | 单消息本地事务命中已处理则 0 新增;版本条件更新 context | all IDs + affected rows | 随机回调 ID 不替代原业务键;旧结果不覆盖新轮次 |
| 超时关闭 | 定时扫描 active/waiting 会话 | task batch、last_time、conversation | application/controllers/tasks/RobotConversation.php | 当前 state、最后活动、人工接管和 pending request | 每会话本地事务 active/waiting -> timeout/closed,通知 commit 后 | task + conversation + old/new state | 已关闭重跑 0 通知;AI 结果不确定先查 request 终态 |