Webchat 采购流程不通问题排查报告

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.jsrunStaffOnce(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.jsrunProcurement() 调用 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 机制

  1. 把确认文件写入 data/confirmations/confirm-{product}.json,status='waiting'
  2. 轮询该文件,每 1 秒读一次
  3. 直到 status 变为 'approved'/'rejected',或超时后 auto-approve(默认 1 分钟)
  4. 文件何时被改写? 需要通过 /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.jsboss-scheduler/index.js
workerrunProcurement() 硬编码workers/procurement.js 参数化
phasefull(阻塞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:保持双路径,修复旧路径

  1. 把旧路径也改成 _phase: 'pre_confirm' + 通过 return {type:'confirm'} 而非 waitForBossConfirmation
  2. 让前端的 runProcurementViaStaff(已有 confirm modal)对所有数字员工通用

方案C:最小修复

只修改 digital-staff/index.jsrunProcurement(),让它也使用 _phase: 'pre_confirm' 并返回确认数据,不走阻塞的 waitForBossConfirmation。然后在 server.js POST handler 中检查返回结果是否有 needsConfirmation 并返回 {type:'confirm'}


关键文件位置

文件行号用途
src/views/AiWorkbench.vue26516sendToStaff 函数
src/views/AiWorkbench.vue~1369runProcurementViaStaff 函数
src/views/AiWorkbench.vue~635checkKeywordRoute 函数
server.js143178POST /api/digital-staff/run handler
server.js94965GET handler (old, broken)
server.js2448SSE /api/digital-staff/logs/stream
server.js2498POST /api/digital-staff/resume
server/digital-staff/index.js1595runStaffOnce(staffId) 旧路径入口
server/digital-staff/index.js2066runProcurement() 旧采购函数
server/boss-scheduler/index.js1823runStaffOnce(staffId,intent,params) 新路径入口
server/boss-scheduler/lite-scheduler.js811_runWorker 核心执行
server/boss-scheduler/lite-scheduler.js59ConfirmNeededError 定义
server/boss-scheduler/lite-scheduler.js1006ConfirmNeededError 捕获+挂起
server/boss-scheduler/workers/procurement.js全文件LiteScheduler 采购 worker (两阶段)
server/tools/procurement-workflow-v3.js开头核心采购工作流 (三阶段)
server/tools/procurement-workflow-v3.js616生成确认请求
server/tools/procurement-workflow-v3.js~25207waitForBossConfirmation 阻塞轮询
← 返回案例列表
分享:
🤖 Try Now →
🤖
🎁