Webchat 采购流程不通问题排查报告
时间:2026-06-17 19:07
方法:纯代码阅读,未做任何修改
核心发现:两套并行路径,前端→后端走的都是旧路径
Webchat 中输入"采购10部电机"时,前端通过 checkKeywordRoute 关键词匹配("采购"命中 DS-PROC-001 的 keywords),调用的不是 ai-agent/LiteScheduler 路径,而是:
前端 sendToStaff()
→ POST /api/digital-staff/run
→ server.js POST handler
→ digital-staff/index.js (旧模块) ← 忽略 intent/params
→ runProcurement()
→ procurement-workflow-v3.js (phase='full') ← 阻塞1-2分钟
飞书端走的是:
飞书消息 → feishu-router.js 关键词匹配 → DS-PROC-001 调度
→ boss-scheduler/index.js (新模块)
→ lite-scheduler.js _runWorker
→ workers/procurement.js (LiteScheduler Worker)
→ procurement-workflow-v3.js (phase='pre_confirm') ← 返回确认,不阻塞
两套路径完全不同。 以下详述。
发现1:digital-staff/index.js 的 runStaffOnce(staffId) 忽略 intent 和 parameters
文件:server/digital-staff/index.js:1595
async function runStaffOnce(staffId) { // ← 只接受 1 个参数!
// ...查找 staff ...
return await withRunContext(staff.id, 'manual', async () => {
switch (staff.id) {
case 'DS-PROC-001': return await runProcurement();
// ...
}
});
}
而 server.js 的 POST handler(行 143178)传入 3 个参数:
const result = await digitalStaff.runStaffOnce(staffId, payload.intent, payload.parameters);
// ↑ intent/params 被忽略!
后果:无论用户说"采购10部电机"还是"椅子",实际执行的都是硬编码的 {product:'伺服电机', quantity:10, budget:90000}。
发现2:旧路径执行 phase='full' 模式,阻塞等待确认(1-2分钟)
digital-staff/index.js 的 runProcurement() 调用 runProcurementWorkflowV3(params, staffLogger) 不传 phase 参数,默认为 phase='full'。
在 full 模式下,procurement-workflow-v3.js 执行完整的 8 步工作流,包括第7步 waitForBossConfirmation:
// procurement-workflow-v3.js
if (phase === 'full') {
workflowSteps.push(
{ name: 'wait_confirmation', // ← 阻塞!
handler: 'waitForBossConfirmation',
params: { confirmTimeoutMin: 1 } },
{ name: 'generate_po', ... },
);
}
waitForBossConfirmation 机制:
- 把确认文件写入
data/confirmations/confirm-{product}.json,status='waiting' - 轮询该文件,每 1 秒读一次
- 直到 status 变为 'approved'/'rejected',或超时后 auto-approve(默认 1 分钟)
- 文件何时被改写? 需要通过
/api/digital-staff/confirm端点(或POST /api/digital-staff/resume)
后果:
- POST 请求阻塞 1-2 分钟(默认 confirmTimeout=1 min + waitTimeout=2 min)
- 如果无人调用 resume/confirm 端点,超时后会自动批准
- 这解释了为什么用户觉得"狂转然后没反应"——前端在等 POST 返回
发现3:LiteScheduler 新路径使用 pre_confirm + ConfirmNeededError 两阶段
文件:server/boss-scheduler/workers/procurement.js
// 第一阶段:pre_confirm
const result = await runProcurementWorkflowV3({
product, quantity, budget,
_phase: 'pre_confirm', // ← 只跑前6步,无阻塞
});
// 弹出确认
requestConfirmation(result.confirmQuestion, ['✅ 批准', '❌ 拒绝'], {
product, quantity, budget,
_phase: 'post_confirm',
});
// ↑ 抛出 ConfirmNeededError,由 LiteScheduler 捕获
LiteScheduler 捕获后在 _pendingConfirmations 中保存 state,返回 {type:'confirm', resumeId, question} 到上层。
飞书走这条路径,所以是好的。 但飞书前端的确认是通过 feishu-router.js 处理的(文本关键词 -> resume),而 Webchat 前端没有对应的 resume 集成。
发现4:前端有两个不同的采购调用函数
| 函数 | 调用来源 | POST 端点 | confirm 处理 |
|------|----------|-----------|-------------|
| sendToStaff(staff, intent) | 关键词匹配输入框文本 → checkKeywordRoute | POST /api/digital-staff/run | SSE 等待确认事件,设 pendingConfirm |
| runProcurementViaStaff() | 快捷按钮"采购助手" | POST /api/digital-staff/run | 检查返回 type==='confirm' → 弹 modal |
两函数 POST 到同一后端端点,但 confirm 处理机制不同。且两函数都走的是旧路径(digital-staff/index.js)。
发现5:SSE 消息处理在 runProcurementViaStaff 中有间隙
runProcurementViaStaff 在步骤2(POST)之后才设置 eventSource.onmessage。如果步骤1(SSE open)和步骤2响应之间 SSE 有日志推送,这些消息不会被处理(onmessage 还没绑定)。
相比之下,sendToStaff 在 POST 之前就设置了 es.onmessage,不会丢消息。
总结:根本原因是两条路径分立
| 对比项 | Webchat (旧路径) | 飞书 (新路径) |
|---|---|---|
| 后端入口 | digital-staff/index.js | boss-scheduler/index.js |
| worker | runProcurement() 硬编码 | workers/procurement.js 参数化 |
| phase | full(阻塞1-2分钟) | pre_confirm(即时返回) |
| confirm | 文件轮询 + 超时自动批准 | ConfirmNeededError + resume |
| intent/params | 忽略 | 从上下文提取 |
修复方案(仅供参考,不动代码)
方案A:重构后端路由(推荐)
将 POST /api/digital-staff/run 的 handler 从 digital-staff/index.js 改接到 boss-scheduler/index.js:
// server.js POST handler 中
const scheduler = require('./server/boss-scheduler/index.js');
const result = await scheduler.runStaffOnce(staffId, intent, parameters);
这样 Webchat 和飞书走同一路径,获得统一的 ConfirmNeededError + resume 机制。
方案B:保持双路径,修复旧路径
- 把旧路径也改成
_phase: 'pre_confirm'+ 通过 return{type:'confirm'}而非waitForBossConfirmation - 让前端的
runProcurementViaStaff(已有 confirm modal)对所有数字员工通用
方案C:最小修复
只修改 digital-staff/index.js 的 runProcurement(),让它也使用 _phase: 'pre_confirm' 并返回确认数据,不走阻塞的 waitForBossConfirmation。然后在 server.js POST handler 中检查返回结果是否有 needsConfirmation 并返回 {type:'confirm'}。
关键文件位置
| 文件 | 行号 | 用途 |
|---|---|---|
| src/views/AiWorkbench.vue | 26516 | sendToStaff 函数 |
| src/views/AiWorkbench.vue | ~1369 | runProcurementViaStaff 函数 |
| src/views/AiWorkbench.vue | ~635 | checkKeywordRoute 函数 |
| server.js | 143178 | POST /api/digital-staff/run handler |
| server.js | 94965 | GET handler (old, broken) |
| server.js | 2448 | SSE /api/digital-staff/logs/stream |
| server.js | 2498 | POST /api/digital-staff/resume |
| server/digital-staff/index.js | 1595 | runStaffOnce(staffId) 旧路径入口 |
| server/digital-staff/index.js | 2066 | runProcurement() 旧采购函数 |
| server/boss-scheduler/index.js | 1823 | runStaffOnce(staffId,intent,params) 新路径入口 |
| server/boss-scheduler/lite-scheduler.js | 811 | _runWorker 核心执行 |
| server/boss-scheduler/lite-scheduler.js | 59 | ConfirmNeededError 定义 |
| server/boss-scheduler/lite-scheduler.js | 1006 | ConfirmNeededError 捕获+挂起 |
| server/boss-scheduler/workers/procurement.js | 全文件 | LiteScheduler 采购 worker (两阶段) |
| server/tools/procurement-workflow-v3.js | 开头 | 核心采购工作流 (三阶段) |
| server/tools/procurement-workflow-v3.js | 616 | 生成确认请求 |
| server/tools/procurement-workflow-v3.js | ~25207 | waitForBossConfirmation 阻塞轮询 |
BossAgents