# nexora-loop 客服系统现状审计 vs 方案 B 差异对照报告 > 审计对象:远程机器 235,`/home/claw/repos/nexora-loop`,分支 `feat/cs-agent`(内容同 dev)。 > 方式:只读源码,未做任何修改/提交。所有结论均带文件路径 + 函数名作为证据。 > 说明:本报告只覆盖当前分支已读到的代码;标注"未确认"处表示证据不足,未臆测。 --- ## 一、现有客服系统架构地图 ### 1. 客户提问从进入到出结果的完整数据流(widget 通道) 以 widget(商城/ERP 挂的浏览器组件)通道为例,串起来是: 1. **入口 API**:`app/api/widget/messages/route.ts`(POST,Bearer 鉴权)→ 调用 `dispatchWidgetMessage`。 2. **分发**:`lib/widget/dispatcher.ts::dispatchWidgetMessage`(薄路由,MVP 只有 `text` 一种 kind)→ `handleWidgetText`。 3. **解析 bot + 会话**:`lib/widget/handler-text.ts::handleWidgetText` - `resolveBotForWidget({ publicKey })`(`lib/channel/router.ts`):按 WidgetApp.publicKey 查 active 的 WidgetApp → 取 botId → 查 Bot。 - `resolveWidgetConversation(...)`(`lib/identity/resolve.ts`,未逐行读,`platform_externalUserId_botId` 唯一键定位/新建 Conversation)。 - 调 `runWidgetChat({ conversation, userText })`。 4. **核心编排**:`lib/widget/chat.ts::runWidgetChat`(本系统真正的"热路径"编排器): - `getOrCreateSession(db, convId)`(`lib/session/slicer.ts`,24h 间隔切 session)。 - 落库入站 Message(direction=`in`, platform=`widget`)。 - 若本 session 上有 `replied` 状态工单 → 降级回 `working` 并记 `user_followup` 事件(与飞书通道对齐)。 - `publish(convId, {type:"typing"})` → SSE 推"正在输入"。 - **调 bot**:`callBotChat(bot, {...}, {onDelta})`(`lib/bot/chat-client.ts`)。onDelta 每帧 `parseEscalateAnalysis` 去掉标签后以 `delta` 事件流式推给浏览器(打字机效果)。 - 拿到完整 reply 后 `parseEscalateAnalysis(reply)` 解析 `[ESCALATE:...]` / `[ANALYSIS:...]` 前缀标签。 - 落库出站 Message(`visible`=去标签后的可见文本)。 - `publish(convId, {type:"message", text: visible})` → SSE 推最终回复。 - **若解析出 `escalateReason`** → `generateTicketForConversation(convId, {trigger:"ai_escalate", reason, problemAnalysis, sessionId})` 建工单 → `publish escalated` 事件。 5. **出结果**:浏览器通过 SSE(`app/api/widget/stream/route.ts`)实时收到 typing/delta/message/escalated 事件。 > 另有飞书通道(`lib/feishu/chat.ts` 等,被 `runWidgetChat` 注释引为"镜像对象")和微信通道(`lib/wechat/`)走同一套 bot/ticket 底座。本报告聚焦 widget,因为方案 B 的 MVP 也是 widget。 ### 2. 现有 bot 是几个角色?chat/analyze/execute 各干什么?怎么被调用的? 现有是**三角色**设计(`lib/bot/types.ts` + 三个 client): | 角色 | client 文件 | 职责 | Bot 上的字段 | 谁调用 | |---|---|---|---|---| | **chat** | `lib/bot/chat-client.ts::callBotChat` | 客服应答(热路径)。若 `bot.mmBotUserId` 存在走 MM 传输(`callMmChat`→`sendAndWait`),否则走 `bot.chatEndpoint` HTTP。 | `chatEndpoint` / `mmBotUserId` / `apiKey` / `defaultParams` | `runWidgetChat`、飞书/微信 chat | | **analyze** | `lib/bot/analyze-client.ts::callBotAnalyze` | 对工单跑分析,产出 `analysis` JSON。 | `analyzeEndpoint` | `lib/ai/analyze-ticket.ts::analyzeTicket`(**已 Deprecated**,见下) | | **execute** | `lib/bot/execute-client.ts::callBotExecute` | 执行/顾问动作。 | `executeEndpoint` | 未见直接调用(consult 走的是 MM sendAndWait,非此 client;**未确认还有活引用**) | 关键点:**这三个 client 本身不含任何 LLM 调用**,它们只是把请求转发给 `bot.chatEndpoint/analyzeEndpoint/executeEndpoint` 这些**外部 HTTP 端点**,或转发给 **Mattermost(MM)**。真正的模型推理发生在本仓库之外(见第 5 节)。 此外 Bot 表还有 `role`(`internal`/`engineer`/客服)+ `parentCustomerServiceId` 自关联(`CSToEngineers`),即"一个客服 bot 下挂多个 engineer bot"的多角色 + 分诊结构。 ### 3. 现有工单生成链路 **主链路**:`lib/ai/generate-ticket.ts::generateTicketForConversation(conversationId, opts)`(275 行,核心): - 加载 conversation(含 bot、全部 messages、已有 tickets)+ `loadProjectContext`(projectNote 背景注入)。 - **1:1 ESCALATE→ticket 策略**:`user_request` 复用活跃工单(open/working/replied);`ai_escalate`/`manual` 总是新建。 - **切片**:以上一张工单的 `updatedAt` 为界,只取之后的消息(session 维度),保证一张工单对应一段对话。 - **草稿生成**:`getTicketGenerator().generate(...)`(`lib/ai/ticket-generator.ts`)。当前默认是 **`StubTicketGenerator`**(`TICKET_GENERATOR` 环境变量,默认 `stub`)——**纯规则**:标题取首条用户消息前 40 字,优先级靠正则(`紧急|urgent` → urgent,`bug|故障|无法` → high),category 靠关键词命中 `categoryPool`。`LlmTicketGenerator` **是未实现的桩**(`throw "not implemented yet"`)。 - **自动指派**:在 `parentCustomerServiceId === conv.botId` 的 engineer bot 里,按 `categoryTags` 命中指派,命中不到就 fallback 到第一个 engineer。 - **落库**:`db.ticket.create`(status=`open`, triggerSource, problemAnalysis, assigneeBotId, attachments...)+ 写 `TicketEvent`(`ai_generate` / `auto_assign`)。 - **通知**:`broadcastNotification`(站内)。 - **飞书同步**:`syncTicketCreatedToFeishu(...)`,try/catch 包住,失败只 log 不阻塞。 ### 4. escalate(转人工)机制怎么实现的 现有 escalate 有**两条触发路径**,都汇入 `generateTicketForConversation`: 1. **AI 自动 escalate**(`trigger:"ai_escalate"`):客服 bot 在回复里输出 `[ESCALATE:原因]\n[ANALYSIS:...]` 前缀标签 → `lib/ai/parse-escalate.ts::parseEscalateAnalysis` 用两个正则(`ESCALATE_RE`/`ANALYSIS_RE`)从 reply 头部剥离 → `runWidgetChat` 检测到 `escalateReason` 就建工单并 SSE 推 `escalated`。 2. **用户手动转人工**(`trigger:"user_request"`):`app/api/widget/escalate/route.ts`(POST)→ `generateTicketForConversation(..., {trigger:"user_request"})`,会复用活跃工单而非新建。 即:**escalate = 建工单 + 通知,从"客服 bot 应答"切换到"人工/工程师工单流程"的分界动作**。`analyze-ticket.ts` 顶部注释明确:P8 起客服 bot 在 escalate 时直接产出 `problemAnalysis`,`analyzeTicket` 已停用(不再改 status,仅保留 manual debug)。escalate 后的多轮顾问追问走 `lib/consult/index.ts`(`initConsult`→`sendConsult`),通过 MM `sendAndWait` 找工程师 bot 出诊断。 ### 5. 模型怎么调的?现在用什么模型/provider?配置在哪? **本仓库里没有直接的 LLM SDK 调用,也没有 deepseek / OpenAI baseURL / chat completions 端点。** grep `deepseek|chat/completions|OPENAI|gpt-|claude-` 在 lib/app 下**零命中业务 LLM 调用**。模型推理全部外包给外部服务: - **Mattermost(MM)路径**(主力):`lib/mm/client.ts::sendAndWait` → MM v4 API(默认 `https://mm.cn.restry.cn`,`lib/mm/config.ts`)。bot 是 MM 里的一个机器人用户(`bot.mmBotUserId`),客服"回复"是通过 DM 发消息 + 轮询/WS 等 bot 回帖实现的("OpenClaw block streaming",见 `client.ts` 顶部注释)。**真正的模型藏在 MM 后面的 OpenClaw agent 里,本仓不可见。** - **HTTP endpoint 路径**(备用):`bot.chatEndpoint` + `bot.apiKey`,`callBotChat` 直接 POST,body 混入 `bot.defaultParams`。具体 model 名由 `defaultParams`(DB 里 Bot 记录)决定,**代码里看不到**。 - **Relay 路径**:`lib/relay/config.ts`(`RELAY_BASE_URL` 默认 `https://relay.restry.cn`,`/api/chat`)——疑似遗留/另一套转发,未确认在 widget 热路径上被调。 **结论:现状 = 模型 provider/model 名都在 DB(Bot.defaultParams)或 MM 后端的 OpenClaw,环境变量里全是 MM_*/RELAY_*,没有任何 deepseek 相关配置。方案 B 要求的"代码直连 deepseek chat/completions"目前完全不存在。** ### 6. Prisma schema 里工单/对话/租户相关 model 与关键字段 (`prisma/schema.prisma`,362 行,表名前缀 `wbt_`/`nl_`) - **Bot**(`wbt_bot`):`chatEndpoint`/`analyzeEndpoint`/`executeEndpoint`/`apiKey`/`defaultParams(Json)`、`mmBotUserId`、`isCustomerService`、`role`(默认 `internal`)、`categoryTags(String[])`、`parentCustomerServiceId`(自关联 `CSToEngineers`)、`projectId`、`platform`。 - **Conversation**(`wbt_conversation`):`botId`、`botSessionId`、`platform`(默认 wechat)、`visitorId`、`widgetAppId`、`externalUserId`、`chatType`;唯一键 `platform_externalUserId_botId`。 - **Session**(`wbt_session`):conversationId + startedAt/lastActivityAt(24h 切片)。 - **Message**(`wbt_message`):`direction`(in/out)、`content`、`attachmentUrl/Mime`、`platform`、sessionId。 - **Ticket**(`wbt_ticket`)关键字段:`title`、`summary`、`detailedSummary`、`attachments(Json)`、`category`、`priority`(默认 normal)、**`status`(默认 `open`)**、`aiAnalysis(Json)`、`aiActionLog(Json)`、`consultInitialized`、`problemAnalysis`、`triggerSource`、`sessionId`、`assigneeId`、`assigneeBotId`、**`feishuTaskGuid`**、`deletedAt`、`mergedIntoTicketId`。 - **TicketEvent**(`wbt_ticket_event`):type/payload/actor(审计流水)。 - **Project / Environment / ProjectNote / Feature / Notification / SystemConfig / WidgetApp / WidgetUpload / WechatUser / AdminUser**。 **工单 status 枚举**(`lib/ticket/state-machine.ts`,注意与方案 B 完全不同):`open` → `working` → `replied` → `closed`(外加互相回退)。**没有 `pending/analyzed/dispatched`**。priority 值域:`low|normal|high|urgent`。 **没有独立 Tenant model。** 多租户是靠 **Project + Bot.projectId + WidgetApp** 隐式表达的(见第 10 节)。 ### 7. widget 侧 SSE 流式怎么实现的 - **stream-bus**(`lib/widget/stream-bus.ts`):进程内 pub/sub,按 conversationId 分组。`InMemoryStreamBus` 维护 `listeners: Map>` + `buffers`(ring buffer,20 条 / 30s TTL,供晚到订阅者回放;`delta` 不入 buffer)。事件类型:`message | delta | typing | escalated`。注释明确单实例;多实例需换 Redis 适配器。 - **SSE 路由**(`app/api/widget/stream/route.ts`,`runtime=nodejs`, `dynamic=force-dynamic`):EventSource 不能带 Authorization header → 用**一次性 stream ticket**(`lib/widget/stream-ticket.ts`,10s TTL,单进程 Map)换取连接。GET `?st=` → `consumeStreamTicket` → 校验 Origin 白名单 + 限流 → `replayBuffer` 回放 → `subscribe` 实时推 → 15s heartbeat 保活。 - **stream-token 路由**(`app/api/widget/stream-token/route.ts`):widget 先 POST JWT 换 ticket。 ### 8. lib/mm 现在还在被谁 import 引用(删了会不会炸) grep `@/lib/mm` 结果——**删 lib/mm 会炸,且是热路径依赖**: - `lib/bot/chat-client.ts:3` → `sendAndWait`(**客服应答主路径**)。 - `lib/consult/index.ts:12,17` → `sendAndWait` + `dm-history`(工单顾问多轮)。 - `app/api/mm/events/route.ts:3,4` → `resolveDmChannelId` + `subscribeChannel/ws-client`(MM 事件 SSE)。 - `app/(authed)/settings/actions.ts:8`、`app/(authed)/bots/actions.ts:6,7,8`、`app/(authed)/bots/_chat-panel.tsx:9`(后台设置/调试面板)。 - 另有测试文件引用。 **结论:lib/mm 是当前 chat 与 consult 两条链路的实际"模型接入层",直接删会让客服应答与顾问全部失效。方案 B 要"删 mm"必须先把 chat/consult 切到 deepseek 直连,再摘 mm 引用,不能先删。** ### 9. lib/feishu/task.ts 现有飞书工单同步(大概率可复用) `lib/feishu/task.ts`(177 行)**做的就是方案 B 要的"Postgres → 飞书 Task 投影"**,可直接复用: - `loadConfig()`:读 `FEISHU_TASK_SYNC_ENABLED`(开关)、`FEISHU_APP_ID/SECRET`、`FEISHU_TASKLIST_GUID`、`FEISHU_DEFAULT_ASSIGNEE_OPEN_ID`。缺配置静默跳过。 - `syncTicketCreatedToFeishu(ticket)`:`getTenantAccessToken` → POST `/open-apis/task/v2/tasks` 建任务(summary≤80、description=原始诉求+初诊摘要+Loop 链接、tasklists、members[assignee])→ 把返回 `guid` 写回 `ticket.feishuTaskGuid`。失败仅 log。 - `completeFeishuTaskForTicket(ticketId)`:工单关闭 → PATCH 任务 `completed_at`。 - `lib/feishu/access-token.ts`:`tenant_access_token` 带缓存。 **这正好是方案 B 第 9 条飞书字段映射的基础设施**,只需扩展字段(标题 `[工单#N]{根因}`、紧急度→优先级映射、@李浩南 open_id)。**默认走 `tenant_access_token`**(方案 B 的 search_kb 要求 user token,是另一回事,见差异表)。 ### 10. 多租户在代码里怎么隔离的?囍铺怎么标识? - **没有显式 Tenant 表/ tenantId 字段。** 租户边界是隐式的:`Project`(`nl_project`,有 `code`/`name`)→ `Bot.projectId` → `WidgetApp.botId` → `Conversation` → `Ticket.projectId`。 - widget 入口靠 `publicKey`(WidgetApp)→ 定位唯一 Bot → 唯一 Project,天然把一个 widget 的流量锁在一个 project/bot 下。 - **"囍铺"标识未在代码里硬编码**(grep `囍铺|xipu|tenant` 只命中飞书 `tenant_access_token` 无关词)。囍铺应是 DB 里的一条 Project + Bot + WidgetApp 记录(seed/运营数据),**代码层无租户常量,属"未确认具体记录"**。 - 方案 B 要求的"search_kb 后端锁死 tenant、不信 LLM 传参"目前**无对应实现**,因为 search_kb 工具本身不存在。 --- ## 二、现状 vs 方案 B 差异对照表 | 能力点 | 方案 B 要求 | dev 现状 | 差距 | 复用/改造/新建 | |---|---|---|---|---| | **Fast Agent 循环** | ReAct 原生 function-calling 单循环,≤4 步兜底 | 无 agent 循环。chat 是"发给 MM/HTTP 端点拿一句 reply"的单次转发(`callBotChat`),推理在外部 OpenClaw | 大:无 function-calling、无 ReAct、无步数控制 | **新建** `lib/agent/fast` | | **2 个工具 search_kb / create_ticket** | 只有这俩工具 | 无任何 tool 定义(grep 零命中);create_ticket 逻辑散在 `generateTicketForConversation` | 大:search_kb 完全没有;建单逻辑存在但非工具形态 | search_kb **新建**;create_ticket **改造复用** `generate-ticket.ts` | | **砍掉 escalate 转人工** | 只有 search_kb / create_ticket 两条出路,无 escalate | escalate 是核心机制(`[ESCALATE]` 标签 + `/api/widget/escalate` + `trigger:ai_escalate/user_request`) | 冲突最大点之一:现状"escalate=建单",方案要去掉 escalate 概念但保留建单 | **改造**:把 escalate 触发点收敛成"Fast Agent 调 create_ticket" | | **Deep Worker 异步触发** | create_ticket 落库后同进程 fire-and-forget 自动触发,强模型 high thinking 深分析 | 无。`analyzeTicket` 已 Deprecated 且需手动调;建单后只发通知 + 飞书同步 | 大:无自动深分析、无 fire-and-forget | **新建** `lib/agent/deep`(可参考 `analyze-ticket.ts` / `execute-prompt-builder.ts` 的 prompt 拼装) | | **10 分钟兜底扫描** | 每 10min 扫 pending 超时未 analyzed 工单补触发 | 无任何 cron/定时器扫工单(grep 仅命中限流/心跳 setInterval) | 大:无定时兜底 | **新建**(cron / 外部调度 / setInterval + status 查询) | | **deepseek-v4-flash 模型** | Fast/Deep 都用它,代码直连 `api.deepseek.com/chat/completions`,OpenAI 兼容 | 无 deepseek。模型在 DB(Bot.defaultParams)/MM 后端 OpenClaw,env 里全是 MM_*/RELAY_* | 大:无 deepseek 客户端、无 reasoning 控制 | **新建** deepseek client(OpenAI 兼容 fetch) | | **单一真相源 = Postgres,status pending→analyzed→dispatched** | Postgres 权威,飞书只是投影 | Postgres 已是真相源、飞书已是单向投影(✓);但 status 是 `open→working→replied→closed`,语义不同 | 中:真相源架构对,但状态机枚举要换 | **改造** `state-machine.ts` + schema status 语义 | | **租户锁死(后端锁 tenant,不信 LLM)** | search_kb 只搜本租户,tenant 后端锁死 | 有隐式 project/bot 边界,但无 tenant 字段、无 search_kb,谈不上"锁死" | 中:需在 search_kb 里把 tenant 从 bot/project 后端注入 | **新建**(复用 Bot.projectId 作 tenant 锚) | | **飞书工单同步** | 标题[工单#N]{根因}、描述含根因/紧急度/duplicate_of/confidence、@李浩南、紧急度→优先级 | `syncTicketCreatedToFeishu` 已能建任务 + 写回 guid + assignee + 完成同步(✓基础完备) | 小:字段/标题格式要扩,缺 duplicate_of/confidence 字段 | **改造复用** `lib/feishu/task.ts` | | **花名册单人(李浩南)** | 只挂一个开发,Deep 判完直接派他 | 有 engineer bot + categoryTags 分诊 + fallback;无李浩南 open_id 常量 | 小:现有分诊过重,方案只要单人直派 | **改造/简化**:用 `FEISHU_DEFAULT_ASSIGNEE_OPEN_ID` 挂李浩南,砍分诊 | --- ## 三、改造建议(关键产出) ### 3.1 走"改造现有"还是"另起 lib/agent 平行实现"? **建议:新建 `lib/agent/`(Fast + Deep)平行实现核心智能层,同时最大化复用现有的"外围基础设施"。** 理由: 1. **智能内核与现状是两套哲学**:现状 chat 是"转发给外部 OpenClaw 的无状态单次调用 + `[ESCALATE]` 标签正则";方案 B 是"代码内 ReAct function-calling 循环 + deepseek 直连"。这部分**没有可改造的存量**(`LlmTicketGenerator` 都还是桩),强行改 `chat-client.ts` 只会把两套语义缠在一起。平行新建更干净。 2. **但外围一大半能直接复用**,不该另起炉灶:Prisma 全套、widget SSE(stream-bus/stream-ticket/stream route)、`generateTicketForConversation` 的切片+落库+事件+通知骨架、`lib/feishu/task.ts` 飞书投影、session slicer、鉴权/限流/CORS(`lib/widget/http.ts`、`rate-limit.ts`)。 3. **落地边界清晰**:`runWidgetChat` 里把 `callBotChat` 换成 `runFastAgent`,把 `parseEscalateAnalysis`+escalate 分支换成"Fast Agent 内部调 create_ticket 工具"。改动收敛在一个编排文件 + 新增 `lib/agent/`。 ### 3.2 可直接复用的现有代码 - **Prisma schema 主体**:Conversation / Session / Message / Ticket / TicketEvent / Bot / WidgetApp / Project 全部复用(仅 Ticket.status 语义调整 + 可能加 `duplicateOfTicketId`/`confidence` 字段)。 - **widget SSE**:`lib/widget/stream-bus.ts`、`stream-ticket.ts`、`app/api/widget/stream|stream-token/route.ts` 原样复用(热路径极简正好符合方案 B"热路径保延迟")。 - **建单骨架**:`generate-ticket.ts` 里的 session 切片、attachments 收集、TicketEvent 审计、broadcastNotification —— 抽成 create_ticket 工具的内部实现。 - **飞书投影**:`lib/feishu/task.ts` + `access-token.ts` —— 改字段即可。 - **鉴权/限流/session**:`lib/widget/http.ts`、`rate-limit.ts`、`lib/session/slicer.ts` 原样用。 ### 3.3 要改的 - **`runWidgetChat`(`lib/widget/chat.ts`)**:核心改造点。`callBotChat` → `runFastAgent`;删掉 `parseEscalateAnalysis` + `escalateReason` 分支(escalate 砍掉),改由 Fast Agent 决定是否调 create_ticket。 - **`state-machine.ts` + Ticket.status**:`open/working/replied/closed` → `pending/analyzed/dispatched`(或做映射层)。 - **`generateTicketForConversation`**:`trigger` 枚举去掉 `ai_escalate`;简化自动指派为"单人直派李浩南";落库后追加 fire-and-forget 触发 Deep Worker。 - **`lib/feishu/task.ts`**:标题/描述字段扩展,加紧急度→飞书优先级映射,assignee 固定李浩南 open_id。 - **`getTicketGenerator`**:Stub 规则生成 → 由 Fast Agent 的 create_ticket 参数直接给(或 Deep Worker 精修)。 ### 3.4 要新建的 - `lib/agent/deepseek-client.ts`:OpenAI 兼容 fetch,读 env key,`api.deepseek.com/chat/completions`,支持 reasoning 开/压。 - `lib/agent/fast/`:ReAct function-calling 单循环,≤4 步,注册 `search_kb` + `create_ticket` 两工具。 - `lib/agent/tools/search-kb.ts`:飞书文档+记忆搜索(**走 user token**,注意现有 `access-token.ts` 是 tenant token,需另加 user token 获取),后端锁 tenant。 - `lib/agent/deep/`:Deep Worker,读全 transcript、重查 KB、查重复工单、定根因/紧急度/指派/duplicate_of/confidence,失败不吞单。 - **兜底扫描器**:每 10min 扫 `status=pending` 且超时未 analyzed 的工单(cron 或 API route + 外部调度)。 ### 3.5 与方案冲突最大的 3 个点及处理 1. **escalate 是现状核心、方案要砍**(`parse-escalate.ts` + `/api/widget/escalate` + `trigger:ai_escalate/user_request`)。 → 处理:保留"建单"能力,但把触发从"bot 输出 `[ESCALATE]` 标签"改成"Fast Agent 主动调 create_ticket 工具"。删 `[ESCALATE]`/`[ANALYSIS]` 标签解析,`/api/widget/escalate` 路由可保留为"用户显式建单"入口但去掉"转人工"语义,或直接下线。 2. **模型接入层是 MM/OpenClaw、方案要 deepseek 直连**(`chat-client.ts` → `lib/mm`,热路径依赖,删了会炸)。 → 处理:**先接后删**。先建 deepseek client + Fast Agent 并在 `runWidgetChat` 切过去跑通,验证后再摘 `chat-client.ts` 对 `lib/mm/client` 的 import;consult 也切走后才能整体下线 lib/mm。切忌先删。 3. **工单状态机语义不同**(`open/working/replied/closed` vs `pending/analyzed/dispatched`)。 → 处理:状态机是"单一真相源"的核心,不能含糊。要么全量迁移枚举(改 `state-machine.ts` + 数据迁移 + 所有引用点),要么加一层映射(现存字段保留、新增 analysis 阶段字段)。建议全量迁移到方案 B 语义,避免双套状态并存的长期债。 ### 3.6 建议落地步骤清单(有序) 1. **建 deepseek client**:新增 `lib/agent/deepseek-client.ts`(OpenAI 兼容,env key,reasoning 开关)+ 单测跑通一次真实调用。 2. **建 create_ticket 工具**:把 `generate-ticket.ts` 建单骨架抽成 `lib/agent/tools/create-ticket.ts`(落 Postgres + TicketEvent + 飞书同步),status 用 `pending`。 3. **建 search_kb 工具**:`lib/agent/tools/search-kb.ts`,先接飞书文档搜索(补 user token 获取),后端从 bot/project 锁 tenant(囍铺)。 4. **建 Fast Agent**:`lib/agent/fast/`,ReAct 单循环 ≤4 步,注册两工具,压 reasoning。 5. **切编排**:改 `lib/widget/chat.ts::runWidgetChat`,`callBotChat`→`runFastAgent`,删 escalate 分支。widget SSE 不动。 6. **改状态机**:`lib/ticket/state-machine.ts` → `pending→analyzed→dispatched`,写数据迁移。 7. **建 Deep Worker**:`lib/agent/deep/`,create_ticket 落库后 fire-and-forget 触发;深分析产出根因/紧急度/duplicate_of/confidence,回写工单 status=`analyzed`,失败不吞单。 8. **建 10min 兜底扫描**:扫 pending 超时补触发 Deep Worker。 9. **扩飞书同步**:改 `lib/feishu/task.ts` 字段/标题/优先级映射,assignee 固定李浩南 open_id,status=`dispatched`。 10. **摘 MM 依赖**:确认 chat + consult 都不再走 lib/mm 后,移除 `chat-client.ts`/`consult` 对 `@/lib/mm` 的 import,再下线 lib/mm(最后一步)。 --- ## 附:证据索引(关键文件) - 热路径编排:`lib/widget/chat.ts::runWidgetChat` - bot 三角色:`lib/bot/{chat,analyze,execute}-client.ts` + `types.ts` - escalate 解析:`lib/ai/parse-escalate.ts`;escalate 路由:`app/api/widget/escalate/route.ts` - 建单:`lib/ai/generate-ticket.ts`、`lib/ai/ticket-generator.ts`(LlmTicketGenerator 是桩) - analyze 已废:`lib/ai/analyze-ticket.ts`(顶部注释) - 状态机:`lib/ticket/state-machine.ts`(open/working/replied/closed) - 飞书投影:`lib/feishu/task.ts`、`lib/feishu/access-token.ts`(tenant token) - 模型层在 MM:`lib/mm/client.ts`、`lib/mm/config.ts`(默认 mm.cn.restry.cn);无 deepseek - widget SSE:`lib/widget/stream-bus.ts`、`stream-ticket.ts`、`app/api/widget/stream/route.ts` - schema:`prisma/schema.prisma`(无 Tenant,多租户靠 Project/Bot.projectId/WidgetApp)