Project skill 给的 fact + 当前 lib/ 实际代码扫描得到 4 个 deepening 候选。
每条用 deletion test 验过(假装删了,复杂度是消失还是分散到 N 个 caller)。
本仓 175 execution flows + 2098 symbols(gitnexus index),候选按修改成本 / 影响 / 收益排序。
三个 channel 的 chat handler 干同样的事:get-or-create conversation → persist inbound message → callBotChat →
parseEscalate → 持久化 reply → 异步起 generateTicket。callBotChat
抽出来了(底层 transport),但上层流水线没抽。结果是每次加一个能力(比如 fire-and-forget 工单生成 / metadata 标准化 / 流式回复
/ 跨 conversation 续接)都要在 3 个文件里手动同步。
lib/widget/chat.ts 和 lib/feishu/chat.ts 删了:复杂度消失?
不会——逻辑会立刻散到 webhook route 里。这说明它们不是 pass-through。但 3 份代码同时存在没有任何 leverage:
改一处不会自动改另两处,且 wechat 跑过的优化(MM 双客户端 / fire-and-forget ticket)其它两个 channel 拿不到。
这是"假深"——每个文件单看是深的,但三份合起来是浅的。一个 deep ChatPipeline + 3 个 ChannelAdapter 才是真深。
抽 lib/messaging/chat-pipeline.ts(已有 lib/messaging/ 空目录,signal):
接收一个 ChatContext + ChannelAdapter,跑统一流水线。
Channel-specific 的事(怎么拿/建 conversation / 怎么读 user meta / 怎么 send back / 怎么 mapMessageId)走 adapter 接口。
wechat/chat.ts:
handleChat(openid, content, opts)
└─ conv = getOrCreate(openid, bot)
└─ Message.create(inbound)
└─ result = callBotChat(bot, {...})
└─ parseEscalateAnalysis(result.reply)
└─ Message.create(outbound)
└─ generateTicket(conv.id) .catch()
feishu/chat.ts: // ~同上,8 处不同
widget/chat.ts: // ~同上,5 处不同
新增能力 X 时 → 改 3 个文件
messaging/chat-pipeline.ts (deep):
runChat(adapter, input) {
conv = adapter.openConversation(input)
inMsg = adapter.persistInbound(conv, input)
res = callBotChat(adapter.bot, mapReq(input, conv))
parsed = parseEscalateAnalysis(res.reply)
adapter.persistOutbound(conv, parsed)
generateTicketAsync(conv.id)
return adapter.formatReply(parsed)
}
wechat/adapter.ts ~30 行
feishu/adapter.ts ~25 行
widget/adapter.ts ~25 行
新增能力 X → 只改 chat-pipeline.ts
__integration__/ 现在 17 个 case 主要测 wechat 路径,feishu/widget 实际没 case
"状态机"在 cspy 里没有显式存在。它的实现散在:
① generate-ticket.ts 里 ACTIVE_STATUSES 数组 + "是否复用 existing" 的判定;
② execute-ticket.ts 里 status awaiting_confirm → executing → awaiting_user_push 的硬编码转换;
③ actions.ts 里 13 个 server action 各自检查 + 写不同 status;
④ admin UI 里读 status 显示不同按钮。没有任何一处文档/代码说"合法转换是哪些"。
Skill 里记的 pitfall #1("need_human 漏在 ACTIVE_STATUSES")就是这种散开必然的结果:
改了一处忘了另一处。
ACTIVE_STATUSES 这个数组删了:复杂度分散到 N 处需要判定"工单是否进行中"的 caller。
但反过来:把 generate-ticket / execute-ticket / actions.ts 里所有 status 转换集中到一个 TicketLifecycle module?
复杂度消失成一个状态机定义。这是典型的"应该集中"信号。
抽 lib/ticket/lifecycle.ts(已有 lib/ticket/ 空目录,signal):
显式状态机 + transition function。所有 status 转换必须过这一层,callers 不能直接 db.ticket.update({status})。
状态机内部知道 "这个转换需要发什么 event / 通知谁 / 触发哪个 hook"。
// generate-ticket.ts:21
const ACTIVE_STATUSES = ["new","analyzing",
"analyzed","awaiting_confirm","executing"];
// 没列 need_human → bug #1
// actions.ts:158
changeTicketStatus(id, status) {
await db.ticket.update({where:{id}, data:{status}});
// 没记 event,没触发 notification
}
// actions.ts:69
submitAdminReply(...) {
// 内嵌 status="replied" 转换 + 写 message
// + 推 fanout + 写 event ... 200 行
}
// lib/ticket/lifecycle.ts
type TicketEvent =
| { type: "user_message_received", ... }
| { type: "admin_reply_submitted", ... }
| { type: "ai_suggestion_pushed", ... }
| ...
export async function applyEvent(
ticketId: string,
event: TicketEvent
): Promise<{ticket: Ticket, sideEffects: SideEffect[]}>
// caller 只表达"发生了什么",
// 不决定"应该转到哪个 status / 写什么 event"
// 状态机内部维护 transition table
callBotChat 接口形式上很深(传 bot + req,得 reply),
但实现里的复杂度集中在 mm/client.ts 单文件 424 行:
ws 路径(sendAndWaitViaWs)+ http 路径(sendAndWaitViaHttp) + transport 选择 + edit-aware quiescence(skill 里记的 Bug A 修复)。
这是真正的 deep module(高 leverage),但 424 行混在一起难审。
sendAndWait 单一对外接口对 caller 来说已经足够深;拆开主要为了可读性,不是可测/可维护性。
除非你计划再加第 3 个 transport(如直接 socket / 第三方平台),拆它的 ROI 不高。
把 mm/client.ts 按 transport 切:
lib/mm/transport.ts(对外 sendAndWait,做 transport 选择 + 失败兜底)
+ lib/mm/ws-transport.ts
+ lib/mm/http-transport.ts。
每个 transport 满足同一个 Transport interface = 真 seam(skill 原话:"两个 adapter 才是真 seam")。
MM 切换上线时 skill 写了 "lib/relay/* 仍保留供 lib/relay/sync.ts / lib/consult/index.ts 等模块使用"。
但实际 consult/index.ts 现在 import 的是 mm 不是 relay(我刚刚 grep 验证)。
真正还活着的只有 relayUploadMedia 给 wechat 图片消息上传媒体用。
① 把 relayUploadMedia 这个具体函数搬到 lib/wechat/media-upload.ts(它是个 wechat 专属能力)
② 删 lib/relay/client.ts 余下 220 行 + sync.ts + config.ts
③ 顺手清 .env 里 RELAY_BASE_URL / RELAY_ADMIN_TOKEN / RELAY_CHANNEL_ID(skill 里还在,但已无 caller)
原因:
db.ticket.update({status}) 全部替换成 applyEvent(...))。Candidate 1 涉及 3 个 channel + 多入口,Phase 化派 CC 更复杂反对意见(我自己 review):cspy 当前节奏是"派 CC 加 feature",抽状态机意味着插一段"refactor 不出 feature"。 如果客户/业务侧有强 deadline,这条放下个 sprint。但每加一个新 ticket 状态都会让回滚成本翻倍,越拖越贵。
lib/__integration__ 三文件够清,不动