统一能力调度架构方案(CapabilityDispatcher)
文档版本:v2.0
创建日期:2026-06-23
作者:BOSSAGENTS 团队
状态:方案确认中(已根据评审意见修正 v1.0 中的问题)
一、背景与目标
1.1 业务背景
BOSSAGENTS 数字员工平台提供 8 种基础能力(identify/repair/optimize/compare/generate/create/validate/inspect),支撑库存管家、供应链管家、质量巡检等多种业务场景。当前系统服务于三个前端渠道:
| 渠道 | 说明 | 典型场景 |
|------|------|----------|
| 网页端 | Web 管理界面 | 管理员通过对话式界面创建Part、查询库存 |
| 飞书端 | 飞书机器人 | 用户在群聊中@机器人发送"帮我查一下库存" |
| 小程序端 | 微信小程序 | 移动端用户查询轻量级数据 |
1.2 核心目标
- 架构统一:三条调用链路合并为单一调度入口,消除代码重复
- 行为一致:同一能力在任何渠道执行结果和行为完全相同
- 审计可追溯:所有能力执行均有完整审计日志,可追溯来源和用户
- 权限集中管控:能力执行权限在统一层校验
- 可测试可回滚:每步改动独立验证,出问题可快速回滚
二、现状详细分析
2.1 当前架构(三套并行)
┌──────────────────────────────────────────────────────────────────────┐
│ 当前架构(三套并行) │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ HTTP API ┌────────────────────────┐ │
│ │ 网页端 │ ──────────────────────→│ /api/capability/* │ │
│ │ useCapability│ (fetch) │ server/routes/ │ │
│ │ Pipeline.js │ │ capability-api.js │ │
│ └─────────────┘ └───────────┬────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ CapabilityRuntime │ │
│ │ server/core/ │ │
│ └───────────┬────────────┘ │
│ │ │
├───────────────────────────────────────────────────────┼────────────────┤
│ │ │
│ ┌─────────────┐ 直接调用 ┌──────────────▼───────────┐ │
│ │ 飞书端 │ ─────────────────────→│ LiteScheduler._runWorker │ │
│ │ feishu- │ (方法引用) │ lite-scheduler.js:849 │ │
│ │ router.js │ │ 直接调用 capability[cap] │ │
│ └─────────────┘ └──────────────────────────┘ │
│ │
├───────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ 小程序端 │ ─────────────────────────→│ HTTP(已正常工作) │ │
│ │ miniapp.js │ HTTP API 调用 │ ai-chat/bom/agent │ │
│ └─────────────┘ └──────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────┘
2.2 各端代码路径
#### 2.2.1 网页端(HTTP链路)
| 层级 | 文件 | 行号 | 说明 |
|------|------|------|------|
| 前端调用 | src/composables/useCapabilityPipeline.js | 137-195 | fetch('/api/capability/repair', ...) |
| 后端路由 | server/routes/capability-api.js | 66-89 | handleCapabilityRequest() |
| 能力执行 | server/core/capability-runtime.js | 504-538 | identify() / repair() 等方法 |
关键代码:
// useCapabilityPipeline.js L137-150
async repair(ctx) {
await fetch('/api/capability/repair', {
method: 'POST',
body: JSON.stringify({ item_type: ctx.item_type, data: ctx.data, ... })
});
}
// capability-api.js L66-89
async function handleCapabilityRequest(req, res, pathname, bodyStr) {
const capability = pathname.replace('/api/capability/', '').split('?')[0];
req.body = JSON.parse(bodyStr || '{}');
await handleCapability(req, res, capability);
}
#### 2.2.2 飞书端(直接调用)
| 层级 | 文件 | 行号 | 说明 |
|------|------|------|------|
| 路由入口 | server/boss-scheduler/feishu-router.js | 306-309 | runWithTimeout() |
| 调度执行 | server/boss-scheduler/lite-scheduler.js | 776-855 | _runWorker() |
| 能力调用 | server/boss-scheduler/lite-scheduler.js | 849 | capability[capName]() |
关键代码:
// feishu-router.js L306-309
async function runWithTimeout(staffId, text, parameters = {}, timeoutMs = 60000) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('EXEC_TIMEOUT')), timeoutMs));
return Promise.race([
_scheduler.runStaffOnce(staffId, text, parameters), // ← 直接调用,跳过HTTP
timeout
]);
}
// lite-scheduler.js L849
const stepResult = await capability[capName](stepContext); // ← 直接方法调用
#### 2.2.3 小程序端(已有 HTTP 通路)
注意:§2.2.3 与 v1.0 有差别。小程序已经有能力调用链路,并非"无能力执行"。
| 功能 | 实际调用路径 |
|------|-------------|
| 对象创建(数字员工委托) | ai-chat/index.vue → POST /api/digital-staff/run → lite-scheduler → SCSAI-creator |
| BOM 查看 | bom-structure.vue → GET /api/bom/search / POST /api/bom/tree |
| AI 对话 | ai-chat/index.vue → POST /api/ai-agent/chat → AI agent → 调度器 |
小程序通过 HTTP 已经走通了能力调用链路,与飞书端(直接方法引用)不同。
2.3 关键问题定位
| # | 问题 | 文件 | 行号 | 影响 |
|---|---|---|---|---|
| 1 | 飞书端绕过统一层 | feishu-router.js | 308 | 审计缺失 |
| 2 | 调度器绕过统一层 | lite-scheduler.js | 849 | 审计缺失 |
| 3 | 审计日志分散 | 各文件 | - | 无法统一查询 |
| 4 | 权限校验缺失 | capability-api.js | - | 安全性风险 |
| 5 | 前端 API 端点不统一 | useCapabilityPipeline.js | 137 | 后端改了前端也要改 |
三、问题根因分析
3.1 调用路径不统一
根本原因:系统演进时未统一规划
阶段1:只考虑网页端HTTP调用
└─→ 设计了 /api/capability/* HTTP API
阶段2:飞书端集成
└─→ 飞书作为"内部系统",直接调用 CapabilityRuntime
└─→ 绕过HTTP层(性能考虑)
阶段3:调度器集成
└─→ 同样是内部调用,直接调用 CapabilityRuntime
3.2 Bug修复成本高
假设 identify 能力发现一个bug需要修复:
需要检查的位置:
1. server/routes/capability-api.js (HTTP路由)
2. server/core/capability-runtime.js (能力实现)
3. lite-scheduler.js (_runWorker 调用路径)
4. feishu-router.js (runWithTimeout)
结果:只修HTTP层,飞书端和调度器仍有bug
3.3 无法统一审计
网页端日志格式:
{ staffId: 'admin', action: 'identify', status: 'success' }
飞书端日志格式:
{ staffId: 'DS-DATA-001', action: 'pipeline.identify', status: 'success' }
无法做到:统一查询"所有identify操作,按渠道、按时间排序"
四、改进方案
⚠️ 术语说明:本方案中的后端 CapabilityDispatcher 与项目中已有的前端 usePipeline.js
(前端请求去重/取消/重试 composable)是两个独立的、互补的抽象层级。
- 前端 pipeline:负责 HTTP 请求层的去重/取消/重试
- 后端 CapabilityDispatcher:负责调度路由 + 审计 + 权限
两者的详细对比和协作方式见附录 C。
4.1 目标架构
┌─────────────────────────────────────────────────────────────────────────────┐
│ 目标架构(统一调度入口) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ 网页端 │ ─────────────────────────────────────────────────┐ │
│ │ useCapability │ POST /api/pipeline/execute │ │
│ │ Pipeline.js │ │ │
│ └─────────────────┘ │ │
│ │
│ ┌─────────────────┐ │
│ │ 飞书端 │ ─────────────────────────────────────────────────┤ │
│ │ feishu-router │ CapabilityDispatcher.execute() │ │
│ └─────────────────┘ (内部调用,不走HTTP) │ │
│ │
│ ┌─────────────────┐ │
│ │ 小程序端 │ ─────────────────────────────────────────────────┤ │
│ │ miniapp.js │ POST /api/pipeline/execute │ │
│ └─────────────────┘ │ │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ CapabilityDispatcher (统一调度入口) │ │
│ │ server/core/capability-dispatcher.js │ │
│ │ │ │
│ │ 1. 参数归一化 (normalizeParams) │ │
│ │ 2. 审计日志 (auditLog) │ │
│ │ 3. 执行调度 (dispatch) │ │
│ │ 4. 结果包装 (wrapResult) │ │
│ └──────────────────────┬────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────┐ │
│ │ CapabilityRuntime (执行引擎) │ │
│ │ server/core/capability-runtime.js │ │
│ │ │ │
│ │ identify / repair / optimize / compare / │ │
│ │ generate / create / validate / inspect │ │
│ └────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
4.2 核心组件设计
#### 4.2.1 CapabilityDispatcher 职责
| 职责 | 说明 | P0 | P1 | P2 |
|------|------|----|----|----|
| 参数归一化 | 不同端传来的参数格式不同,统一转换 | ✅ | - | - |
| 审计日志 | 记录每次执行的 source/capability/userId/duration | ✅ | - | - |
| 执行调度 | 调用 CapabilityRuntime 执行能力 | ✅ | - | - |
| 结果包装 | 统一响应格式 { success, capability, source, traceId, duration, data } | ✅ | - | - |
| 权限校验 | 外部调用需校验 | - | - | ✅ |
⚠️ 重要变更:权限校验从 P0 移出至 P2。v1.0 中的 checkPermission 仅有 return true 桩代码,
在 P0-P1 阶段不会实际被测试到,属于死代码。P2 引入真正的权限系统时再一并实现。
#### 4.2.2 新增文件清单
| 文件 | 说明 | 优先级 |
|------|------|--------|
| server/core/capability-dispatcher.js | 统一调度分发器核心类 | P0 |
| server/services/audit-log.js | 审计日志服务 | P0 |
| server/services/permission-service.js | 权限服务 | P2(从 P0 移出) |
#### 4.2.3 修改文件清单
| 文件 | 修改内容 | 优先级 |
|------|----------|--------|
| server.js | 添加 POST /api/pipeline/execute 路由 | P0 |
| server/boss-scheduler/feishu-router.js | runWithTimeout 改用 CapabilityDispatcher.execute() | P0 |
| server/routes/capability-api.js | 改造为调用 CapabilityDispatcher(纯业务调用,HTTP 层留在路由) | P0 |
| src/composables/useCapabilityPipeline.js | API 端点改为 /api/pipeline/execute,参数格式对齐 | P0 |
| server/boss-scheduler/lite-scheduler.js | 外部包装层接入审计,内部逻辑不做侵入式修改 | P1 |
| bossagents-miniapp/server/miniapp-routes.js | 添加 /api/pipeline/execute 路由(复用主服务的) | P1 |
| bossagents-miniapp/src/api/miniapp.js | 添加 executeCapability(capability, params) 方法 | P1 |
五、具体实施步骤(含预估工作量)
5.1 P0 阶段:核心架构(预估:3~4 人天)
#### 步骤 1:创建 CapabilityDispatcher(纯业务层,不关心 HTTP)
文件:server/core/capability-dispatcher.js(新建)
const { getRuntime } = require('./capability-runtime');
const auditLog = require('../services/audit-log');
class CapabilityDispatcher {
/**
* 统一能力调度入口
*
* @param {Object} request
* @param {string} request.source 来源标识: web | feishu | miniapp | scheduler
* @param {string} request.capability 能力名称
* @param {Object} request.params 执行参数
* @param {string} [request.userId] 用户 ID
* @param {string} [request.traceId] 追踪 ID
* @param {Object} [request.context] 执行上下文(仅 scheduler 使用,用于传递 prompt/协作链状态)
* @returns {{ success: boolean, capability: string, source: string, traceId: string,
* executedAt: string, duration: number, data?: any, error?: string }}
*/
static async execute(request) {
const startTime = Date.now();
const traceId = request.traceId
|| `trace_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
try {
// step 1: 参数归一化 —— 先铺默认值,再用入参覆盖
// 注意:...params 必须在前面,显式覆盖字段在后面,避免无意义的 undefined 覆盖默认值
const normalized = this.normalizeParams(request);
// step 2: 审计日志 START
const auditId = auditLog.start({
traceId,
source: normalized.source,
capability: normalized.capability,
userId: normalized.userId,
itemType: normalized.params.item_type,
});
// step 3: 能力校验
const runtime = getRuntime();
if (!runtime[normalized.capability]) {
throw Object.assign(
new Error(`能力 "${normalized.capability}" 不存在`),
{ statusCode: 404 }
);
}
// step 4: 执行能力
// scheduler 传入的 context 透传给能力函数,支撑 prompt 渲染、协作链状态等
const result = await runtime[normalized.capability]({
...normalized.params,
...(normalized.context ? { _context: normalized.context } : {}),
});
// step 5: 审计日志 COMPLETE
auditLog.complete(auditId, {
success: true,
duration: Date.now() - startTime,
});
// step 6: 统一响应
return {
success: true,
capability: normalized.capability,
source: normalized.source,
traceId,
executedAt: new Date().toISOString(),
duration: Date.now() - startTime,
data: result,
};
} catch (error) {
const duration = Date.now() - startTime;
// 错误也要有审计
if (auditLog && traceId) {
auditLog.complete(`audit_${startTime}`, {
success: false,
error: error.message,
duration,
});
}
// 将已经构建的成功响应和数据信息抛出
throw Object.assign(
error,
{
statusCode: error.statusCode || 500,
_pipelineTraceId: traceId,
_pipelineSource: request.source,
}
);
}
}
/**
* 参数归一化
*
* 核心原则:...spread 放在前面,显式字段放在后面覆盖,
* 避免 undefined 值意外覆盖默认值(v1.0 的设计存在此 bug)。
*/
static normalizeParams(request) {
const { source, capability, params = {}, userId, traceId, context } = request;
// step 1: 基础字段
const result = {
source: source || 'unknown',
capability,
userId: userId || null,
traceId: traceId || null,
context: context || null,
};
// step 2: 参数归一化 — 先把入参展开,再用显式处理覆盖
const itemType = params.item_type
|| (params.itemType)
|| (params.data && params.data.item_type)
|| 'Part';
const data = params.data || params;
const intent = params.intent || params.text || '';
result.params = {
...params,
item_type: itemType,
data,
intent,
};
return result;
}
static async dispatch(runtime, request) {
return await runtime[request.capability](request.params);
}
static logStart(request, traceId) {
console.log(
`[CapabilityDispatcher] ▶ ${request.source}/${request.capability} [${traceId}]`
);
}
static logComplete(entry, { success, error, duration }) {
const icon = success ? '✅' : '❌';
console.log(
`[CapabilityDispatcher] ${icon} ${entry.source}/${entry.capability} [${entry.traceId}] ${duration}ms`
);
}
}
module.exports = CapabilityDispatcher;
修正点说明(vs v1.0):
normalizeParams修复:...params放在前,显式字段放在后,避免 undefined 覆盖默认值handleHttpRequest移除:CapabilityDispatcher 是纯业务对象,不关心res和 HTTP 状态码context字段:scheduler 可以通过它传递 prompt 上下文/协作链状态traceId增加随机后缀:避免极高并发下的冲突checkPermission完全移除:P0 不做权限,P2 再引入
#### 步骤 2:主服务添加统一路由 + HTTP 分离
文件:server.js
CapabilityDispatcher 不处理 HTTP,所以路由层需要自己构建请求对象并处理响应:
// ========== POST /api/pipeline/execute ==========
// HTTP 处理在路由层,不侵入 CapabilityDispatcher
if (pathname === '/api/pipeline/execute' && req.method === 'POST') {
const { CapabilityDispatcher } = require('./server/core/capability-dispatcher');
const auditLog = require('./server/services/audit-log');
collectBody(req).then(async (bodyStr) => {
try {
const body = JSON.parse(bodyStr || '{}');
const result = await CapabilityDispatcher.execute({
source: body.source || 'web',
capability: body.capability,
params: body.params || body,
userId: body.userId || (req.auth && req.auth.userId),
traceId: body.traceId,
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (error) {
const statusCode = error.statusCode || 500;
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
// Dispatcher 抛出的错误携带了 traceId,回传前端帮助排查
res.end(JSON.stringify({
success: false,
error: error.message,
traceId: error._pipelineTraceId,
}));
}
}).catch(catchHandler(req, res, 'pipeline-execute'));
return;
}
#### 步骤 3:修改 capability-api.js
文件:server/routes/capability-api.js
CapabilityDispatcher 不包含 HTTP 处理,所以路由直接构建业务对象调用 .execute(),然后自己序列化响应:
const { CapabilityDispatcher } = require('../core/capability-dispatcher');
async function handleCapabilityRequest(req, res, pathname, bodyStr) {
const capability = pathname.replace('/api/capability/', '').split('?')[0];
try {
const body = JSON.parse(bodyStr || '{}');
const result = await CapabilityDispatcher.execute({
source: body.source || 'web',
capability,
params: body,
userId: req.auth && req.auth.userId,
});
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(result));
} catch (error) {
res.writeHead(error.statusCode || 500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
error: error.message,
traceId: error._pipelineTraceId,
}));
}
}
修正点说明(vs v1.0):
不再调用 PipelineDispatcher.handleHttpRequest(),而是路由层直接构造 request 对象,
调用 CapabilityDispatcher.execute(),自己控制 HTTP 写入。未来切换到 gRPC/WebSocket/消息队列时,
只需要换路由层代码,CapabilityDispatcher 无需任何改动。
#### 步骤 4:改造飞书端
文件:server/boss-scheduler/feishu-router.js
const { CapabilityDispatcher } = require('../core/capability-dispatcher');
async function runWithTimeout(staffId, text, parameters = {}, timeoutMs = 60000) {
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('EXEC_TIMEOUT')), timeoutMs));
const { capability = 'identify', item_type = 'Part' } = parameters;
return Promise.race([
CapabilityDispatcher.execute({
source: 'feishu',
capability,
params: { item_type, intent: text, data: parameters },
userId: staffId,
traceId: `feishu_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
}),
timeout
]);
}
#### 步骤 5:改造前端 useCapabilityPipeline.js
文件:src/composables/useCapabilityPipeline.js
// 改前
async repair(ctx) {
await fetch('/api/capability/repair', {
method: 'POST',
body: JSON.stringify({ item_type: ctx.item_type, data: ctx.data, ... })
});
}
// 改后
async repair(ctx) {
const res = await fetch('/api/pipeline/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: 'web',
capability: 'repair',
params: { item_type: ctx.item_type, data: ctx.data, ... },
}),
});
const json = await res.json();
if (!json.success) throw new Error(json.error);
return json.data;
}
前端使用现有的 usePipeline.js(已完成的前端 pipeline composable)包装这个调用,
自动获得去重、取消、重试能力:
> const pipeline = usePipeline();
> async function repair(ctx) {
> return pipeline.request({
> type: 'capability:repair',
> handler: async (signal) => {
> const res = await fetch('/api/pipeline/execute', { signal, ... });
> return (await res.json()).data;
> },
> });
> }
> 5.2 P1 阶段:调度器接入 + 小程序统一(预估:2~3 人天)
#### 步骤 6:lite-scheduler 外部包装(不做侵入式修改)
文件:server/boss-scheduler/lite-scheduler.js
重要:v1.0 中直接将 _runWorker 的内部 capability[capName]() 替换为
PipelineDispatcher.execute(),这会丢失 prompt 渲染、协作链状态、变量注入等执行上下文。
本版改为在外部(runStaffOnce 层)加审计拦截,内部执行逻辑保持不变。
class LiteScheduler {
async runOnce(staffId, intent, parameters) {
const staff = this._resolveStaff(staffId);
if (!staff) throw new Error('员工不存在');
// ★ 提取 _skipAudit 标志并清理,避免泄露到 _runWorker
const skipAudit = !!(parameters && parameters._skipAudit);
if (parameters) delete parameters._skipAudit;
// ★ 审计 START(Dispatcher 调用的跳过,避免重复审计)
let auditId = null;
const traceId = `scheduler_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
if (!skipAudit) {
auditId = auditLog.start({
traceId, source: 'scheduler',
capability: (parameters && parameters.capability) || staff.capability || 'identify',
userId: staffId,
itemType: (parameters && parameters.item_type) || staff.itemType || 'Part',
});
}
return this.executionContext.withRunContext(staff.id, 'manual', async () => {
const { skipped, result } = await this._withStaffMutex(staff.id, () =>
this._runWorker(staff, intent, parameters)
);
// ★ 审计 COMPLETE
if (!skipAudit && !skipped) {
auditLog.complete(auditId, {
success: result && !(result && result.type === 'confirm'),
duration: 0,
});
}
});
}
}
// Dispatcher 调用时传入 _skipAudit,避免 scheduler 重复审计:
// CapabilityDispatcher.execute({
// source: 'feishu',
// ...
// params: { ...parameters, _skipAudit: true },
// });
// → scheduler.runStaffOnce(...) → runOnce() 跳过 auditLog.start()
// → 审计由 Dispatcher 统一完成
为什么不在 _runWorker 内部替换能力调用:
| 能力上下文 | 来源 | 替换后是否保留 |
|-----------|------|---------------|
| Prompt 模板渲染 | _runWorker → buildPrompt() | ❌ 丢失 |
| 变量注入 | _runWorker → resolveVariables() | ❌ 丢失 |
| Collaboration chain 状态 | _runWorker → _getChainContext() | ❌ 丢失 |
| Worker 运行状态标记(isRunning) | _runWorker → _markRunning() | ❌ 丢失 |
| Step 级别错误重试 | CapabilityRuntime 内部 | ✅ 保留 |
P1 暂不侵入 _runWorker,P2 再考虑是否将 CapabilityRuntime 调用也统一收口到 CapabilityDispatcher。
#### 步骤 7:小程序端接入
文件:bossagents-miniapp/src/api/miniapp.js(新增方法)
export async function executeCapability(capability, params = {}) {
const { request } = require('./request');
return request.post('/api/pipeline/execute', {
source: 'miniapp',
capability,
params,
});
}
小程序走 HTTP 调用 POST /api/pipeline/execute,和网页端走同一个路由。不另外开独立服务器。
文件:src/pages/ai-chat/index.vue(改造示例)
- const res = await fetch('/api/digital-staff/run', { ... });
+ const res = await executeCapability('create', { item_type: 'Part', data });
旧 API 端点保留直到 P2 迁移完成。
5.3 P2 阶段:权限 + 全量迁移(预估:2~3 人天)
#### 步骤 8:创建权限服务(P2 才启用)
文件:server/services/permission-service.js(新建)
class PermissionService {
static CAPABILITY_ROLES = {
'identify': ['user', 'admin', 'staff'],
'create': ['admin', 'staff'],
'repair': ['admin', 'staff'],
'optimize': ['admin'],
'compare': ['user', 'admin', 'staff'],
'generate': ['user', 'admin', 'staff'],
'validate': ['admin', 'staff'],
'inspect': ['admin', 'staff'],
};
async check(userId, capability) {
if (!userId) return false;
const role = await this.getUserRole(userId);
const allowed = PermissionService.CAPABILITY_ROLES[capability] || [];
return allowed.includes(role);
}
async getUserRole(userId) {
// TODO: 从数据库/SSO 获取
return 'admin';
}
}
module.exports = new PermissionService();
在 CapabilityDispatcher.execute() 中加入权限拦截:
if (!['scheduler', 'feishu'].includes(normalized.source)) {
const permitted = await permissionService.check(normalized.userId, normalized.capability);
if (!permitted) {
throw Object.assign(new Error('权限不足'), { statusCode: 403 });
}
}
#### 步骤 9:清理旧 API 端点
P2 阶段确认迁移稳定后,逐步废弃旧端点:
| 旧端点 | 新端点 | 废弃策略 |
|--------|--------|----------|
| POST /api/capability/:name | POST /api/pipeline/execute | 保留兼容头,日志警告 |
| POST /api/digital-staff/run | POST /api/pipeline/execute | 保留兼容头,日志警告 |
| POST /api/ai-agent/chat | POST /api/pipeline/execute | 保留兼容头,日志警告 |
六、预期效果
6.1 架构指标对比
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 调用路径数 | 3条独立 | 1条统一 |
| 能力实现位置 | 1处 | 1处 |
| 审计日志位置 | 分散 | 统一服务 |
| Bug修复位置 | 需改多处 | 只改 Runtime |
| 新增能力修改 | 各端都要改 | 只改 Runtime + routing 表 |
6.2 响应格式统一
{
"success": true,
"capability": "identify",
"source": "web",
"traceId": "trace_20260623_abc123",
"executedAt": "2026-06-23T10:30:00.000Z",
"duration": 1234,
"data": { ... }
}
6.3 审计日志格式
{"id":"audit_xxx","traceId":"trace_xxx","source":"web","capability":"identify","event":"START","timestamp":"..."}
{"id":"audit_xxx","event":"COMPLETE","success":true,"duration":1234,"timestamp":"..."}
{"id":"audit_yyy","traceId":"feishu_yyy","source":"feishu","capability":"repair","event":"START","timestamp":"..."}
{"id":"audit_yyy","event":"COMPLETE","success":false,"error":"目标不存在","duration":500,"timestamp":"..."}
七、风险与回滚
7.1 风险识别
| 风险 | 概率 | 影响 | 缓解 |
|------|------|------|------|
| Dispatcher 有 bug | 中 | 高 | Feature Flag 降级 |
| 性能下降 | 低 | 低 | 内部调用不走 HTTP |
| 审计日志写入失败 | 低 | 中 | 降级到 console.log |
| _runWorker 上下文丢失 | 高 | 高 | P0-P1 不做侵入式修改,只在外部加钩子 |
| 前端用例遗漏 | 中 | 中 | 逐步迁移,旧端点保留兼容 |
7.2 回滚方案
环境变量控制:
# .env
ENABLE_CAPABILITY_DISPATCHER=false # 设为 false 时降级到直接调用
ENABLE_PIPELINE_AUDIT=false # 单独关闭审计(性能原因)
降级逻辑:
static async execute(request) {
if (process.env.ENABLE_CAPABILITY_DISPATCHER === 'false') {
// 降级:直接调用 CapabilityRuntime,不回滚到旧 API
const runtime = getRuntime();
const result = await runtime[request.capability](request.params);
return { success: true, data: result, degraded: true };
}
// 正常流程
}
7.3 灰度发布
| 阶段 | 范围 | 观察期 |
|------|------|--------|
| 1 | 飞书端 10%(审计 + 调度经过 Dispatcher) | 7 天 |
| 2 | 网页端 50% | 7 天 |
| 3 | 调度器 + 小程序 | 7 天 |
| 4 | 全量 | 30 天 |
八、评审清单
- [ ] 架构设计是否合理?
- [ ] CapabilityDispatcher 职责是否清晰?(不含 HTTP、不含权限)
- [ ] 权限模型是否满足需求?(P2 再做)
- [ ] 审计日志格式是否够用?
- [ ] 回滚方案是否可行?
- [ ] 灰度发布策略是否合适?
- [ ] 是否遗漏了
_runWorker执行上下文的保护?(P0-P1 不侵入) - [ ] 性能影响是否可接受?(Dispatcher 仅做参数归一化和日志写入)
九、附录
A. 术语表
| 术语 | 说明 |
|---|---|
| CapabilityRuntime | 能力执行引擎,提供 8 种基础能力 |
| CapabilityDispatcher | 统一调度分发器,所有能力调用经过此层 |
| source | 来源标识,区分 web/feishu/miniapp/scheduler |
| traceId | 追踪 ID,用于关联一次完整执行的所有日志 |
| usePipeline.js | 前端 composable,处理 HTTP 请求去重/取消/重试 |
| useCapabilityPipeline.js | 前端 composable,封装能力调用的前端逻辑 |
B. 相关文件路径
server/
├── core/
│ ├── capability-runtime.js # 能力执行引擎(不变)
│ └── capability-dispatcher.js # 统一调度分发器(新建)
├── routes/
│ └── capability-api.js # HTTP API 路由(改造为调用 Dispatcher)
├── boss-scheduler/
│ ├── lite-scheduler.js # 调度器(P1 加外部审计钩子)
│ └── feishu-router.js # 飞书路由(P0 改)
└── services/
├── audit-log.js # 审计日志(新建)
└── permission-service.js # 权限服务(P2 新建)
src/composables/
├── usePipeline.js # 前端 pipeline(已有,不变)
└── useCapabilityPipeline.js # 前端能力调用(P0 改 API 端点)
bossagents-miniapp/src/api/
└── miniapp.js # 小程序 API 层(P1 加方法)
C. 前端 pipeline vs 后端 CapabilityDispatcher 对比
这是项目中容易混淆的两个概念,放在一起说明:
| 维度 | 前端 usePipeline.js | 后端 CapabilityDispatcher |
|------|----------------------|---------------------------|
| 层次 | 浏览器端 composable | 服务器端 Node.js 类 |
| 职责 | HTTP 请求去重、取消、重试 | 参数归一化、审计日志、结果统一包装 |
| 解决什么问题 | 用户多次点击、页面快速切换、网络抖动 | 调用路径不统一、审计缺失、响应格式不一致 |
| 影响范围 | 组件级别 | 全系统(所有渠道) |
| 改写为 CapabilityRuntime 调用 | 不涉及 | 是核心职责 |
| 与对方的关系 | 调用方:经过 pipeline 的 HTTP 请求最终到达后端的 Dispatcher | 被调用方:接收来自前端 + 其他端的调用 |
用户操作
│
▼
前端 usePipeline.js ──HTTP POST──→ /api/pipeline/execute
(去重/取消/重试) │
▼
CapabilityDispatcher.execute()
(参数归一化 + 审计 + 调度)
│
▼
CapabilityRuntime
(identify / repair / ...)
两者不重叠,前端 pipeline 是对外的"栅栏",后端 Dispatcher 是对内的"枢纽"。
D. 实施总览(时间线)
| 阶段 | 内容 | 预估人天 | 关键交付物 |
|---|---|---|---|
| P0 | CapabilityDispatcher + 审计服务 + 飞书/网页端 | 3~4 天 | capability-dispatcher.js, audit-log.js |
| P1 | 调度器接入 + 小程序端 | 2~3 天 | 外部审计钩子, executeCapability 方法 |
| P2 | 权限系统 + 全量迁移 | 2~3 天 | permission-service.js, 旧 API 废弃公告 |
| 合计 | 7~10 天 |
E. 版本变更记录
| 版本 | 日期 | 变更 |
|---|---|---|
| v1.0 | 2026-06-23 | 初版(已评审) |
| v2.0 | 2026-06-23 | 修正 v1.0 问题:修复 normalizeParams bug;分离 HTTP 层;重命名避免命名冲突;权限从 P0 移出;scheduler 不做侵入式修改;增加前端改造计划;增加前端/后端 pipeline 对比说明;增加工作量预估和版本变更记录 |
BossAgents