左帮右臂 P2 技术设计文档
版本: V1.0
日期: 2026-06-24
对应需求: spec-p2.md (T33-T45)
原则: 只写 HOW(架构与实现),不写 WHAT(业务行为,见 spec-p2.md)
1. 实现模型
1.1 上下文视图
P2 在 BossAgents 平台中的定位:补齐协作可视化、租户配额、文章管理、循环依赖诊断、语音降级、实时通信六个维度的功能缺口,实现从"功能可用"到"运维可控"的跃迁。
┌─────────────────────────────────────────────────────────────────────┐
│ BossAgents 平台(P2 扩展) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ P2-1 协作链 │ │ P2-2 租户配额 │ │ P2-3 文章管理 │ │
│ │ 可视化 │ │ 限制 │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────┴───────┐ ┌──────┴───────┐ ┌──────┴───────┐ │
│ │ P2-4 BOM循环 │ │ P2-5 阿里云 │ │ P2-6 WebSocket│ │
│ │ 依赖可视化 │ │ ASR降级 │ │ 实时推送 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ─── 依赖的 P0/P1 模块 ─── │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ T24 协作调度器 │ │ T09 租户中间件 │ │ T17 通知同步 │ │
│ │ T25 协作链API │ │ T15 数据库表 │ │ T22 BomTreeGraph│ │
│ │ T07 BOM导入 │ │ T11 环境变量 │ │ T01 VoiceInput │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
外部系统交互
| 外部系统 | 交互方式 | P2模块 | 说明 |
|---------|---------|--------|------|
| 阿里云智能语音 | HTTPS REST API | P2-5 | ASR 第二后端,AccessKeyId/Secret/AppKey 认证 |
| WebSocket 客户端 | ws:// 协议 | P2-6 | 网页端原生 WebSocket / 小程序 uni.connectSocket |
| 微信公众平台 | HTTPS REST API | P2-3 | 已有 wechat-publish.js,P2 增加发布记录管理 |
1.2 服务/组件总体架构
1.2.1 后端服务架构
server.js (HTTP Server, port 3006)
├── server/middleware/tenant-middleware.js [P0-T09, P2修改]
│ ├── tenantInject() — 注入租户上下文
│ ├── tenantDataGuard() — 跨租户访问防护
│ └── quotaGuard(resourceType) [P2-T35新增] — 配额检查拦截
│
├── server/services/
│ ├── tenant-quota-service.js [P2-T34新增]
│ ├── published-article-service.js [P2-T37新增]
│ ├── websocket-service.js [P2-T42新增]
│ ├── asr-service.js [P0-T01, P2-T40修改] — 增加阿里云后端+降级链
│ ├── bom-import-service.js [P0-T07, P2-T39修改] — 返回循环路径数据
│ ├── notification-sync.js [P1-T17, P2-T42修改] — WebSocket推送集成
│ └── wechat-publish.js [P0, P2-T37修改] — 发布成功后写入记录
│
├── server/routes/
│ ├── admin-quota.js [P2-T36新增]
│ ├── content-api.js [P0, P2-T37修改] — 新增已发布文章API
│ └── digital-staff-routes.js [P1, P2-T33修改] — 协作链API
│
└── server/digital-staff/
└── collaboration-scheduler.js [P1-T24, P2-T33依赖] — getCollaborationChain()
1.2.2 前端组件架构
src/
├── components/
│ ├── digital-staff/
│ │ └── CollaborationChainGraph.vue [P2-T33新增]
│ └── bom/
│ └── BomTreeGraph.vue [P1-T22, P2-T39修改] — 增加循环依赖高亮
│
├── composables/
│ └── useWebSocket.js [P2-T43新增]
│
├── views/
│ ├── DigitalStaff.vue [P1, P2-T33修改] — 集成协作链图
│ ├── ContentManager.vue [P0, P2-T38修改] — 增加"已发布"标签页
│ └── AdminQuotaManager.vue [P2-T45新增]
│
├── api/
│ ├── content.js [P0, P2-T38修改] — 新增已发布文章API调用
│ └── admin.js [P2-T45新增/修改] — 配额管理API调用
│
└── stores/
└── (notification store) [P2-T43修改] — 使用WebSocket接收通知
bossagents-miniapp/
└── src/
├── utils/
│ └── websocket.js [P2-T44新增]
└── store/
└── index.js [P2-T44修改] — 集成WebSocket
1.2.3 三端统一架构
遵循 PREFERENCE_1(三端统一架构),P2 各模块在三端的实现策略:
| P2模块 | 网页端(Vue3) | 飞书端 | 小程序端(uni-app) |
|--------|-------------|--------|------------------|
| 协作链可视化 | CollaborationChainGraph.vue | 飞书消息卡片(静态) | 暂不实现 |
| 租户配额 | AdminQuotaManager.vue | 同网页端(iframe) | 暂不实现 |
| 文章管理 | ContentManager.vue 已发布标签 | 同网页端 | 暂不实现 |
| BOM循环依赖 | BomTreeGraph.vue 增强 | 同网页端 | 暂不实现 |
| ASR降级 | 服务端统一处理 | 服务端统一处理 | VoiceInput.vue(已有) |
| WebSocket | useWebSocket.js + 原生WS | 飞书事件订阅(已有) | websocket.js + uni.connectSocket |
1.3 实现设计文档
1.3.1 P2-1 协作链可视化(T33)
架构决策:使用 SVG 水平流程图渲染,复用 BomTreeGraph 的 SVG 渲染模式,但采用水平布局(从左到右)而非树形布局。
组件结构:
CollaborationChainGraph.vue
├── Props:
│ chain: Array<{ taskId, assignedTo, status, title, result }>
│ currentTaskId: string
├── Emits:
│ node-click(nodeData)
├── 内部状态:
│ hoveredNodeId: ref<string|null>
│ autoRefreshTimer: ref<number|null>
├── 渲染逻辑:
│ SVG 水平流程图
│ 节点 = 圆角矩形卡片 (180×60)
│ 边 = 带箭头连线
│ 状态颜色: completed=绿, running=蓝+脉冲, pending=灰, failed=红
└── 自动刷新:
当 chain 中有 running 状态节点时,每5秒轮询更新
与 P0/P1 集成点:
- 依赖
CollaborationScheduler.getCollaborationChain(taskId)获取链路数据 - 依赖
GET /api/digital-staff/collaboration/chain/:taskIdAPI(T25 已实现) - 在
DigitalStaff.vue任务看板区域集成
关键伪代码:
// CollaborationChainGraph.vue — 布局算法
function layoutChain(chain, currentTaskId) {
const nodeW = 180, nodeH = 60, gapX = 40
const startX = 0
return chain.map((node, i) => ({
...node,
x: startX + i * (nodeW + gapX),
y: 0,
isCurrent: node.taskId === currentTaskId,
statusClass: statusToClass(node.status)
}))
}
// 状态映射
function statusToClass(status) {
const map = {
completed: 'node-completed', // 绿色 ✅
running: 'node-running', // 蓝色+脉冲 🔄
pending: 'node-pending', // 灰色 ⏳
failed: 'node-failed' // 红色 ❌
}
return map[status] || 'node-pending'
}
1.3.2 P2-2 租户配额限制(T34/T35/T36/T45)
架构决策:配置驱动(PREFERENCE_5),配额上限通过数据库存储,默认值通过代码常量定义,管理员可通过 API 和界面修改。
服务层架构:
TenantQuotaService (server/services/tenant-quota-service.js)
├── 单例模式,module.exports = instance
├── 依赖: db-adapter (SQLite/MySQL)
├── 核心方法:
│ ensureTable() — 建表
│ checkQuota(enterpriseId, resourceType) — 配额检查
│ incrementUsage(enterpriseId, resourceType, delta)
│ syncQuotaUsage(enterpriseId) — 单租户同步
│ syncAllQuotas() — 全量同步
│ getQuota(enterpriseId) — 获取配额信息
│ updateQuota(enterpriseId, fields) — 修改配额上限
│ getAllQuotas() — 管理员查看所有
└── 默认配额常量:
DEFAULT_QUOTAS = {
maxParts: 10000,
maxBoms: 1000,
maxUsers: 50,
maxStaffTasks: 5000
}
中间件集成:
// server/middleware/tenant-middleware.js — quotaGuard 新增
function quotaGuard(resourceType) {
return function(req, res, next) {
const enterpriseId = req.tenantContext?.enterpriseId
const userRole = req.tenantContext?.role
// 管理员角色跳过阻断,仅附加警告
const result = tenantQuotaService.checkQuota(enterpriseId, resourceType)
if (!result.allowed && userRole !== 'admin') {
// 记录审计日志
logAudit({ action: 'quota_exceeded', enterprise_id: enterpriseId, resource_type: resourceType, current: result.current, max: result.max })
res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' })
res.end(JSON.stringify({ success: false, error: `已达到${resourceType}配额上限(${result.max}),请联系管理员升级` }))
return
}
// 80%警告附加到响应头
if (result.warning) {
res.setHeader('X-Quota-Warning', result.warning)
if (userRole === 'admin' && !result.allowed) {
res.setHeader('X-Quota-Exceeded', 'true')
}
}
// 注入配额信息到请求上下文
req.quotaResult = result
if (next) next()
}
}
server.js 路由集成点:
POST /api/parts/create → quotaGuard('parts') → handler → incrementUsage('parts')
POST /api/bom/create → quotaGuard('boms') → handler → incrementUsage('boms')
POST /api/users/create → quotaGuard('users') → handler → incrementUsage('users')
POST /api/digital-staff/task/create → quotaGuard('staffTasks') → handler → incrementUsage('staffTasks')
配额管理 API 路由:
server/routes/admin-quota.js
├── GET /api/admin/quotas — 所有租户配额列表
├── GET /api/admin/quotas/:enterpriseId — 指定租户配额
├── PUT /api/admin/quotas/:enterpriseId — 修改配额上限
├── POST /api/admin/quotas/init — 批量初始化默认配额
└── POST /api/admin/quotas/:enterpriseId/sync — 手动同步配额使用量
前端管理界面:
AdminQuotaManager.vue
├── 顶部: 租户选择下拉框 + "批量初始化"按钮
├── 主体: 配额使用情况表格
│ 列: 租户名称 | Part(当前/上限/占比) | BOM(...) | 用户(...) | 月任务(...) | 操作
│ 占比>80% 标黄, >100% 标红
├── 编辑弹窗: maxParts/maxBoms/maxUsers/maxStaffTasks 表单
│ 新配额<当前使用量 → 警告弹窗
└── 操作: 编辑配额 | 手动同步
1.3.3 P2-3 已发布文章管理(T37/T38)
架构决策:在现有 wechat-publish.js 的发布流程中增加记录保存钩子,不重构发布服务本身。
服务层:
PublishedArticleService (server/services/published-article-service.js)
├── 单例模式
├── 依赖: db-adapter
├── 核心方法:
│ ensureTable() — 建表
│ recordPublish(data) — 发布成功后记录
│ getPublishedArticles(filters) — 分页查询
│ getPublishedArticle(id) — 单条记录
│ updatePublishStatus(id, status, extra) — 更新发布状态
└── 与 wechat-publish.js 集成:
publishArticle() 成功后 → recordPublish()
publishArticle() 失败后 → updatePublishStatus(id, 'failed')
wechat-publish.js 修改点:
// 在 publishArticle() 函数末尾增加记录保存
async function publishArticle(title, content, options = {}) {
// ... 现有逻辑 ...
if (autoPublish) {
const publishResult = await _retryPublish(draft.mediaId)
if (publishResult.success) {
// [P2-T37新增] 保存发布记录
try {
const publishedArticleService = require('./published-article-service')
await publishedArticleService.recordPublish({
articleId: options.articleId || '',
publishId: publishResult.publishId,
mediaId: draft.mediaId,
title,
publishTime: new Date().toISOString(),
publishUrl: '', // 微信异步返回,需轮询获取
status: 'published',
enterpriseId: options.enterpriseId || 'default'
})
} catch (e) {
console.error('[WeChat] 发布记录保存失败:', e.message)
}
return { success: true, mediaId: draft.mediaId, publishId: publishResult.publishId, status: 'published' }
}
// 失败记录
try {
const publishedArticleService = require('./published-article-service')
await publishedArticleService.updatePublishStatus(options.articleId, 'failed', { error: publishResult.error })
} catch (e) {}
return { success: false, ... }
}
}
前端 ContentManager.vue 修改:
ContentManager.vue
├── 现有标签页: 生成 | 草稿 | 设置
└── [P2-T38新增] "已发布"标签页
├── 调用 GET /api/content/published-articles 获取列表
├── 表格列: 标题 | 发布时间 | 发布状态 | 操作
│ 状态: 🟢已 published | 🟡publishing | 🔴failed
└── 操作: "查看链接"(新窗口) | "重新发布"(仅failed)
1.3.4 P2-4 BOM 循环依赖可视化(T39)
架构决策:在 BomTreeGraph.vue 中增加 cyclePaths prop,通过 CSS 类实现循环高亮,不改变现有渲染逻辑。
BomTreeGraph.vue 修改设计:
新增 Props:
cyclePaths: Array<Array<string>> — 循环路径数组 [[nodeId1, nodeId2, ...], ...]
新增 Computed:
cycleNodeIds: Set<string> — 从 cyclePaths 提取所有循环节点 ID
cycleEdgeIds: Set<string> — 循环路径的边 ID 集合
修改 flattenTree():
节点增加 status: 'cycle' 标记 (当 id in cycleNodeIds)
修改 edges computed:
循环边增加 class: 'edge-cycle' (红色加粗)
新增 CSS:
.node-cycle .node-rect { fill: #fce4ec; stroke: #f44336; stroke-width: 2; animation: pulse 1.5s infinite; }
.edge-cycle { stroke: #f44336; stroke-width: 3; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.6; } }
循环路径颜色区分(多循环场景):
const CYCLE_COLORS = ['#f44336', '#ff9800', '#9c27b0'] // 红/橙/紫
function getCycleColor(cycleIndex) {
return CYCLE_COLORS[cycleIndex % CYCLE_COLORS.length]
}
bom-import-service.js 修改:
// _detectCycle 改为返回循环路径而非布尔值
_detectCycleWithPaths(items) {
const parentMap = new Map()
for (const item of items) {
if (item.parent) parentMap.set(item.item_number, item.parent)
}
const cyclePaths = []
const visited = new Set()
for (const item of items) {
if (visited.has(item.item_number)) continue
const path = []
const inStack = new Set()
function dfs(node) {
if (inStack.has(node)) {
// 找到循环,提取循环路径
const cycleStart = path.indexOf(node)
const cycle = [...path.slice(cycleStart), node]
cyclePaths.push(cycle)
return
}
if (visited.has(node)) return
visited.add(node)
inStack.add(node)
path.push(node)
const parent = parentMap.get(node)
if (parent) dfs(parent)
path.pop()
inStack.delete(node)
}
dfs(item.item_number)
}
return cyclePaths // [[A, B, C, A], ...]
}
// buildBomTree 修改返回值
buildBomTree(rows, mapping) {
// ... 现有逻辑 ...
const cyclePaths = this._detectCycleWithPaths(items)
if (cyclePaths.length > 0) {
return { error: '检测到循环依赖', items: [], cyclePaths }
}
// ...
}
1.3.5 P2-5 阿里云 ASR 第二后端(T40/T41)
架构决策:配置驱动(PREFERENCE_5),通过 config.yaml 的 asr.providers 数组定义后端优先级,运行时按顺序尝试。
asr-service.js 修改架构:
ASR 降级链:
config.yaml: asr.providers: [{name: iflytek, priority: 1}, {name: aliyun, priority: 2}, {name: echo, priority: 3}]
recognize(audioBuffer, fileName):
1. 读取 providers 配置,按 priority 排序
2. 遍历 enabled 的 providers
3. 对每个 provider 调用 recognizeXxx()
4. 成功 → 返回 { text, provider }
5. 失败 → 记录日志,尝试下一个
6. 全部失败 → 返回错误
新增方法:
recognizeAliyun(audioBuffer, format, sampleRate) — 阿里云 REST API
checkProviderHealth(providerName) — 健康检查
getStats() — 降级统计
阿里云 ASR 接口调用:
API: https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr
认证: AccessKeyId + AccessKeySecret 签名
参数: appKey, format(pcm/wav), sampleRate(16000)
返回: { result: "识别文本", ... }
环境变量覆盖: ALIYUN_ASR_ACCESS_KEY_ID / ALIYUN_ASR_ACCESS_KEY_SECRET / ALIYUN_ASR_APP_KEY
config.yaml 新增配置节:
asr:
provider: iflytek
timeout: 15000
providers:
- name: iflytek
priority: 1
enabled: true
- name: aliyun
priority: 2
enabled: true
- name: echo
priority: 3
enabled: true
aliyun:
access_key_id: ""
access_key_secret: ""
app_key: ""
降级链伪代码:
async function recognize(audioBuffer, fileName = '') {
const cfg = loadConfig()
const providers = (cfg.providers || [])
.filter(p => p.enabled !== false)
.sort((a, b) => (a.priority || 99) - (b.priority || 99))
if (providers.length === 0) {
providers.push({ name: cfg.provider || 'echo' })
}
const info = detectAudioFormat(audioBuffer)
let lastError = null
for (const provider of providers) {
try {
let text
switch (provider.name) {
case 'iflytek':
text = await recognizeIflytek(audioBuffer, info.format, info.sampleRate)
break
case 'aliyun':
text = await recognizeAliyun(audioBuffer, info.format, info.sampleRate)
break
case 'echo':
text = await recognizeEcho(audioBuffer, info.format)
break
default:
continue
}
if (text && text.trim()) {
degradeStats[provider.name].success++
return { text: text.trim(), provider: provider.name }
}
} catch (e) {
lastError = e
degradeStats[provider.name].fail++
console.warn(`[ASR] ${provider.name} 识别失败,切换到下一个: ${e.message}`)
}
}
throw new Error(`语音识别服务暂时不可用,请稍后重试。最后错误: ${lastError?.message}`)
}
健康检查机制:
const healthCache = new Map() // provider → { healthy: boolean, checkedAt: number }
const HEALTH_TTL = 5 * 60 * 1000 // 5分钟
async function checkProviderHealth(providerName) {
const cached = healthCache.get(providerName)
if (cached && Date.now() - cached.checkedAt < HEALTH_TTL) {
return cached.healthy
}
let healthy = false
try {
switch (providerName) {
case 'iflytek':
// 轻量级检查:验证配置完整性
healthy = !!(cfg.iflytek?.app_id && cfg.iflytek?.api_key && cfg.iflytek?.api_secret)
break
case 'aliyun':
// 阿里云:调用 DescribeTask 接口或验证配置
healthy = !!(cfg.aliyun?.access_key_id && cfg.aliyun?.access_key_secret && cfg.aliyun?.app_key)
break
case 'echo':
healthy = true // Echo 永远可用
break
}
} catch (e) {
healthy = false
}
healthCache.set(providerName, { healthy, checkedAt: Date.now() })
return healthy
}
1.3.6 P2-6 WebSocket 实时推送(T42/T43/T44)
架构决策:使用 ws 库与现有 HTTP 服务共存于同一端口,通过 upgrade 事件实现 WebSocket 握手。
服务端架构:
WebSocketService (server/services/websocket-service.js)
├── 依赖: ws (npm包), jsonwebtoken
├── 核心数据结构:
│ connections: Map<userId, Set<ws>> — 用户连接映射
│ tenantConnections: Map<enterpriseId, Set<userId>> — 租户连接映射
│ messageBuffer: Map<userId, Array<message>> — 离线消息缓冲
│
├── 初始化:
│ init(httpServer):
│ wss = new WebSocket.Server({ noServer: true })
│ httpServer.on('upgrade', (req, socket, head) => {
│ if (pathname === '/ws') {
│ 验证 JWT → wss.handleUpgrade(req, socket, head, ws)
│ }
│ })
│
├── 连接管理:
│ handleConnection(ws, req):
│ 从 JWT 解析 userId + enterpriseId
│ connections.get(userId).add(ws)
│ tenantConnections.get(enterpriseId).add(userId)
│ 单用户最多5个连接,全局最多1000个
│
├── 消息推送:
│ broadcastToUser(userId, message):
│ 查找 connections.get(userId) → 逐个 ws.send(JSON.stringify(message))
│ broadcastToTenant(enterpriseId, message):
│ 查找 tenantConnections.get(enterpriseId) → 逐用户推送
│
├── 消息补发:
│ 客户端重连时发送 lastMessageId
│ 服务端从 notifications 表查询该 ID 之后的消息并补发
│
├── 心跳保活:
│ 服务端每30秒发送 ping
│ 客户端10秒内未响应 pong → 关闭连接
│
└── 降级:
连接数>1000 → 新连接返回 503
JWT无效 → 关闭连接,返回 4001
server.js 集成:
// 在 HTTP 服务器创建后
const websocketService = require('./server/services/websocket-service')
websocketService.init(server)
// 在通知推送时调用
notificationSync.push(userId, message, channels)
websocketService.broadcastToUser(userId, {
type: 'notification',
data: message,
timestamp: Date.now()
})
前端 composable 设计:
useWebSocket.js (src/composables/useWebSocket.js)
├── 状态:
│ ws: ref<WebSocket|null>
│ connected: ref<boolean>
│ usePolling: ref<boolean>
│ retryCount: ref<number>
│ messageHandlers: Array<Function>
│
├── 方法:
│ connect(token):
│ ws = new WebSocket(`ws://${host}/ws?token=${token}`)
│ ws.onopen → connected=true, usePolling=false, console.log('[WS] 连接成功')
│ ws.onmessage → 解析JSON → 调用 messageHandlers
│ ws.onclose → connected=false → 2秒后重连(最多5次,指数退避)
│ ws.onerror → 标记 usePolling=true
│
│ disconnect():
│ ws.close()
│
│ onMessage(handler):
│ messageHandlers.push(handler)
│
│ send(data):
│ ws.send(JSON.stringify(data))
│
└── 降级逻辑:
usePolling=true 时,每5秒调用 GET /api/notifications 轮询
WebSocket 重连成功后切换回实时模式
小程序 WebSocket 设计:
websocket.js (bossagents-miniapp/src/utils/websocket.js)
├── 使用 uni.connectSocket() 建立连接
├── connect(token):
│ uni.connectSocket({ url: `ws://${host}/ws?token=${token}` })
│ uni.onSocketOpen → 标记已连接
│ uni.onSocketMessage → 回调处理
│ uni.onSocketError + uni.onSocketClose → 触发重连
├── 重连: 2秒后重试,最多5次
└── 降级: 连接失败时切换到5秒轮询
消息格式:
// WebSocket 消息格式
interface WsMessage {
type: 'notification' | 'task_update' | 'chain_update' | 'ping' | 'pong'
data: any
timestamp: number
messageId: string // 用于消息补发
}
2. 接口设计
2.1 总体设计
P2 新增/修改的 API 接口遵循现有项目约定:
- POST 路由使用
collectBody(req)读取请求体 - 路由处理函数签名:
handleXxxRoute(req, res, pathname, query, bodyStr) - 响应格式:
{ success: boolean, data?: any, error?: string } - 管理员接口需验证
req.userRole === 'admin' - 租户隔离通过
req.tenantContext.enterpriseId实现
2.2 接口清单
2.2.1 协作链可视化 API
| 方法 | 路径 | 参数 | 返回值 | 说明 |
|---|---|---|---|---|
| GET | /api/digital-staff/collaboration/chain/:taskId | taskId(路径参数) | { success: true, data: { chain: [{taskId, assignedTo, status, title, result}] } } | 获取协作链路数据(T25已有,T33消费) |
2.2.2 租户配额管理 API
| 方法 | 路径 | 参数 | 返回值 | 说明 |
|------|------|------|--------|------|
| GET | /api/admin/quotas | — | { success: true, data: { quotas: [{enterpriseId, maxParts, currentParts, ...}] } } | 所有租户配额列表 |
| GET | /api/admin/quotas/:enterpriseId | enterpriseId(路径) | { success: true, data: { enterpriseId, maxParts, currentParts, ... } } | 指定租户配额 |
| PUT | /api/admin/quotas/:enterpriseId | enterpriseId(路径), body: {maxParts?, maxBoms?, maxUsers?, maxStaffTasks?} | { success: true } | 修改配额上限 |
| POST | /api/admin/quotas/init | — | { success: true, data: { initialized: N } } | 批量初始化默认配额 |
| POST | /api/admin/quotas/:enterpriseId/sync | enterpriseId(路径) | { success: true, data: { synced: true } } | 手动同步配额使用量 |
配额检查接口(中间件内部调用,非独立API):
// TenantQuotaService.checkQuota 返回值
interface QuotaCheckResult {
allowed: boolean
current: number
max: number
warning: string | null // 80%警告
usagePercent: number
}
2.2.3 已发布文章 API
| 方法 | 路径 | 参数 | 返回值 | 说明 |
|------|------|------|--------|------|
| GET | /api/content/published-articles | page?, pageSize?, enterpriseId? | { success: true, data: { items: [PublishedArticle], total, page, pageSize } } | 已发布文章列表 |
| GET | /api/content/published-articles/:id | id(路径) | { success: true, data: PublishedArticle } | 单条发布记录 |
2.2.4 ASR 降级 API
| 方法 | 路径 | 参数 | 返回值 | 说明 |
|---|---|---|---|---|
| POST | /api/asr/recognize | audio(文件), format?, sampleRate? | { code: 0, text: string, provider: string } | 语音识别(已有,增加provider字段和降级链) |
| GET | /api/asr/stats | — | { success: true, data: { iflytek: {success, fail}, aliyun: {success, fail}, echo: {count} } } | 降级统计 |
2.2.5 WebSocket 连接
| 协议 | 路径 | 参数 | 说明 |
|------|------|------|------|
| WS | /ws?token=xxx | token(JWT) | 建立WebSocket连接 |
| WS | /ws?token=xxx&lastMessageId=yyy | token, lastMessageId | 重连+消息补发 |
WebSocket 消息类型:
| type | data | 方向 | 说明 |
|------|------|------|------|
| notification | { id, title, message, type, source } | 服务端→客户端 | 通知推送 |
| task_update | { taskId, status, result } | 服务端→客户端 | 数字员工任务状态更新 |
| chain_update | { taskId, chain } | 服务端→客户端 | 协作链状态更新 |
| ping | — | 服务端→客户端 | 心跳 |
| pong | — | 客户端→服务端 | 心跳响应 |
3. 数据模型
3.1 设计目标
- 与现有 db-adapter.js 的 SQLite/MySQL 双后端兼容
- 多租户隔离:所有表包含 enterprise_id 字段
- 审计追踪:关键操作记录到审计日志
- 配额数据一致性:创建时实时+1,每日全量同步
3.2 模型实现
3.2.1 tenant_quotas 表
CREATE TABLE IF NOT EXISTS tenant_quotas (
enterprise_id TEXT PRIMARY KEY NOT NULL,
max_parts INTEGER NOT NULL DEFAULT 10000,
max_boms INTEGER NOT NULL DEFAULT 1000,
max_users INTEGER NOT NULL DEFAULT 50,
max_staff_tasks INTEGER NOT NULL DEFAULT 5000,
current_parts INTEGER NOT NULL DEFAULT 0,
current_boms INTEGER NOT NULL DEFAULT 0,
current_users INTEGER NOT NULL DEFAULT 0,
current_staff_tasks INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 索引(MySQL模式下需要)
CREATE INDEX IF NOT EXISTS idx_tenant_quotas_enterprise ON tenant_quotas(enterprise_id);
字段说明:
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| enterprise_id | TEXT | PK | 租户唯一标识 |
| max_parts | INTEGER | DEFAULT 10000, >0 | Part数量上限 |
| max_boms | INTEGER | DEFAULT 1000, >0 | BOM数量上限 |
| max_users | INTEGER | DEFAULT 50, >0 | 用户数量上限 |
| max_staff_tasks | INTEGER | DEFAULT 5000, >0 | 月度数字员工任务上限 |
| current_parts | INTEGER | DEFAULT 0, >=0 | 当前Part数量(系统自动维护) |
| current_boms | INTEGER | DEFAULT 0, >=0 | 当前BOM数量 |
| current_users | INTEGER | DEFAULT 0, >=0 | 当前用户数量 |
| current_staff_tasks | INTEGER | DEFAULT 0, >=0 | 当月数字员工任务数量(每月重置) |
| updated_at | TEXT | DEFAULT now | 最后更新时间 |
3.2.2 published_articles 表
CREATE TABLE IF NOT EXISTS published_articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id TEXT NOT NULL DEFAULT '',
publish_id TEXT DEFAULT '',
media_id TEXT DEFAULT '',
title TEXT NOT NULL DEFAULT '',
publish_time TEXT DEFAULT '',
publish_url TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'draft',
retry_count INTEGER NOT NULL DEFAULT 0,
enterprise_id TEXT NOT NULL DEFAULT 'default',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_published_articles_enterprise ON published_articles(enterprise_id);
CREATE INDEX IF NOT EXISTS idx_published_articles_status ON published_articles(status);
字段说明:
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | INTEGER | PK, AUTO | 唯一标识 |
| article_id | TEXT | NOT NULL | 文章内容ID,关联内容表 |
| publish_id | TEXT | — | 微信返回的发布ID,发布成功后必填 |
| media_id | TEXT | — | 草稿媒体ID,发布前必填 |
| title | TEXT | NOT NULL | 文章标题 |
| publish_time | TEXT | — | 发布时间,发布成功后必填 |
| publish_url | TEXT | — | 发布链接,发布成功后由微信返回 |
| status | TEXT | NOT NULL | 枚举: draft / publishing / published / failed |
| retry_count | INTEGER | DEFAULT 0 | 发布重试次数 |
| enterprise_id | TEXT | DEFAULT 'default' | 租户ID,多租户隔离 |
| created_at | TEXT | DEFAULT now | 创建时间 |
| updated_at | TEXT | DEFAULT now | 更新时间 |
3.2.3 notifications 表(已有,P2扩展)
-- 已有表结构(notification-sync.js 已创建),P2 不修改表结构
-- WebSocket 消息补发通过查询 notifications 表实现
-- 新增字段: message_id (TEXT, 用于消息补发的唯一标识)
ALTER TABLE notifications ADD COLUMN message_id TEXT DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_notifications_message_id ON notifications(message_id);
3.2.4 ASR 配置(config.yaml,非数据库表)
asr:
provider: iflytek
timeout: 15000
providers:
- name: iflytek
priority: 1
enabled: true
- name: aliyun
priority: 2
enabled: true
- name: echo
priority: 3
enabled: true
iflytek:
app_id: ""
api_key: ""
api_secret: ""
aliyun:
access_key_id: ""
access_key_secret: ""
app_key: ""
环境变量覆盖:
| 环境变量 | 对应配置 | 说明 |
|---------|---------|------|
| ALIYUN_ASR_ACCESS_KEY_ID | asr.aliyun.access_key_id | 阿里云 AccessKey ID |
| ALIYUN_ASR_ACCESS_KEY_SECRET | asr.aliyun.access_key_secret | 阿里云 AccessKey Secret |
| ALIYUN_ASR_APP_KEY | asr.aliyun.app_key | 阿里云智能语音 AppKey |
| ASR_PROVIDER | asr.provider | 默认ASR提供商 |
4. 关键算法与流程
4.1 配额检查与自动同步流程
┌─────────────────────────────────────────────────────┐
│ 系统启动 │
│ └── TenantQuotaService.syncAllQuotas() │
│ 遍历所有租户 → 统计实际资源数量 → 更新配额表 │
│ │
│ 每日凌晨 (cron) │
│ └── TenantQuotaService.syncAllQuotas() │
│ │
│ 资源创建请求 │
│ └── quotaGuard(resourceType) │
│ ├── checkQuota(enterpriseId, resourceType) │
│ │ ├── 无记录 → 使用默认配额 │
│ │ ├── current < max*0.8 → allowed=true │
│ │ ├── current >= max*0.8 → allowed=true │
│ │ │ + warning="已达80%" │
│ │ └── current >= max → allowed=false │
│ │ (管理员: allowed=true+警告) │
│ ├── allowed=false → 403 + 审计日志 │
│ └── allowed=true → handler → incrementUsage │
└─────────────────────────────────────────────────────┘
4.2 ASR 降级链流程
客户端 POST /api/asr/recognize (音频)
│
├── 读取 asr.providers 配置,按 priority 排序
│
├── [1] iflytek (priority=1)
│ ├── 健康检查通过? → recognizeIflytek()
│ ├── 成功 → 返回 { text, provider: 'iflytek' }
│ └── 失败 → 记录日志 + degradeStats.iflytek.fail++
│
├── [2] aliyun (priority=2)
│ ├── 配置完整? → recognizeAliyun()
│ ├── 成功 → 返回 { text, provider: 'aliyun' }
│ └── 失败/配置缺失 → 记录日志 + degradeStats.aliyun.fail++
│
├── [3] echo (priority=3)
│ ├── recognizeEcho() → 返回模拟文本
│ └── { text, provider: 'echo', degraded: true }
│
└── 全部失败 → 返回错误 "语音识别服务暂时不可用"
4.3 WebSocket 连接生命周期
客户端 服务端
│ │
├── ws://host/ws?token=jwt ───→ │ 验证JWT
│ ├── 解析 userId + enterpriseId
│ ├── 检查连接数限制 (全局<1000, 单用户<5)
│←── 连接成功 ──────────────────┤
│ │
│ ├── 每30秒: ping ──→
│←── pong ──────────────────────┤ 10秒内未响应 → 关闭
│ │
│←── notification消息 ──────────┤ broadcastToUser()
│←── task_update消息 ───────────┤
│ │
│── 断连 ──────────────────────→│ 从连接映射移除
│ │ 消息暂存到 notifications 表
│ │
│── 2秒后重连 ─────────────────→│ 验证JWT
│ (lastMessageId) ├── 查询该ID之后的消息
│←── 补发消息 ──────────────────┤
│←── 连接成功 ──────────────────┤
4.4 BOM 循环依赖检测算法
// DFS 检测循环并记录完整路径
function detectCyclePaths(items) {
const parentMap = new Map()
for (const item of items) {
if (item.parent) parentMap.set(item.item_number, item.parent)
}
const cyclePaths = []
const visited = new Set()
for (const item of items) {
if (visited.has(item.item_number)) continue
const path = []
const inStack = new Set()
function dfs(nodeId) {
if (inStack.has(nodeId)) {
// 发现循环:提取从循环起点到当前节点的路径
const cycleStart = path.indexOf(nodeId)
const cycle = path.slice(cycleStart).concat([nodeId])
// 截断超过10个节点的循环路径
if (cycle.length > 11) { // 10个节点+回到起点
cycle.splice(10, cycle.length - 11)
cycle.push('...')
}
cyclePaths.push(cycle)
return
}
if (visited.has(nodeId)) return
visited.add(nodeId)
inStack.add(nodeId)
path.push(nodeId)
const parent = parentMap.get(nodeId)
if (parent) dfs(parent)
path.pop()
inStack.delete(nodeId)
}
dfs(item.item_number)
}
return cyclePaths
}
5. 与 P0/P1 集成点
5.1 集成点汇总
| P2任务 | 依赖的P0/P1 | 集成文件 | 集成方式 |
|--------|------------|---------|---------|
| T33 协作链可视化 | T24 协作调度器 | collaboration-scheduler.js | 调用 getCollaborationChain(taskId) |
| T33 协作链可视化 | T25 协作链API | digital-staff-routes.js | GET /api/digital-staff/collaboration/chain/:taskId |
| T34 租户配额后端 | T09 租户中间件 | tenant-middleware.js | 新增 quotaGuard() 方法 |
| T34 租户配额后端 | T15 数据库表 | db-adapter.js | 新增 tenant_quotas 建表 |
| T35 配额检查中间件 | T34 配额服务 | tenant-middleware.js | 调用 checkQuota() |
| T35 配额检查中间件 | server.js | server.js | 在创建类路由中增加配额检查 |
| T36 配额管理API | T34 配额服务 | admin-quota.js | 调用配额服务方法 |
| T37 已发布文章后端 | T11 环境变量 | wechat-publish.js | 发布成功后调用 recordPublish() |
| T37 已发布文章后端 | T31 发布重试 | wechat-publish.js | 失败后调用 updatePublishStatus() |
| T37 已发布文章后端 | T15 数据库表 | db-adapter.js | 新增 published_articles 建表 |
| T39 BOM循环可视化 | T22 BomTreeGraph | BomTreeGraph.vue | 新增 cyclePaths prop |
| T39 BOM循环可视化 | T07 BOM导入 | bom-import-service.js | _detectCycle 返回循环路径 |
| T40 阿里云ASR | T01 VoiceInput | asr-service.js | 增加阿里云后端+降级链 |
| T42 WebSocket服务 | T17 通知同步 | notification-sync.js | 推送时调用 broadcastToUser() |
| T43 WebSocket前端 | T17 通知 | stores/ | WebSocket消息更新store |
5.2 server.js 修改点汇总
// 1. 新增路由注册 (T36)
const adminQuotaRoutes = require('./server/routes/admin-quota')
// 在 handleRequest 中:
if (pathname.startsWith('/api/admin/quotas')) {
return adminQuotaRoutes.handleAdminQuotaRoute(req, res, pathname, query, bodyStr)
}
// 2. 配额检查中间件集成 (T35)
// 在 Part/BOM/User/数字员工任务创建路由中:
const { quotaGuard } = require('./server/middleware/tenant-middleware')
// 创建前: quotaGuard('parts')(req, res, () => { /* 原有创建逻辑 */ })
// 创建后: tenantQuotaService.incrementUsage(enterpriseId, 'parts')
// 3. WebSocket 服务初始化 (T42)
const websocketService = require('./server/services/websocket-service')
// 在 server 创建后:
websocketService.init(server)
// 4. 通知推送集成 WebSocket (T42)
// 在通知推送逻辑中增加:
websocketService.broadcastToUser(userId, { type: 'notification', data: message, timestamp: Date.now() })
5.3 db-adapter.js 修改点汇总
// 在 createDatabase() 中新增建表语句:
// tenant_quotas (T34)
db.exec(`CREATE TABLE IF NOT EXISTS tenant_quotas (
enterprise_id TEXT PRIMARY KEY NOT NULL,
max_parts INTEGER NOT NULL DEFAULT 10000,
max_boms INTEGER NOT NULL DEFAULT 1000,
max_users INTEGER NOT NULL DEFAULT 50,
max_staff_tasks INTEGER NOT NULL DEFAULT 5000,
current_parts INTEGER NOT NULL DEFAULT 0,
current_boms INTEGER NOT NULL DEFAULT 0,
current_users INTEGER NOT NULL DEFAULT 0,
current_staff_tasks INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)`)
// published_articles (T37)
db.exec(`CREATE TABLE IF NOT EXISTS published_articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id TEXT NOT NULL DEFAULT '',
publish_id TEXT DEFAULT '',
media_id TEXT DEFAULT '',
title TEXT NOT NULL DEFAULT '',
publish_time TEXT DEFAULT '',
publish_url TEXT DEFAULT '',
status TEXT NOT NULL DEFAULT 'draft',
retry_count INTEGER NOT NULL DEFAULT 0,
enterprise_id TEXT NOT NULL DEFAULT 'default',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)`)
6. 新增文件清单
| 文件路径 | 任务 | 类型 | 说明 |
|---------|------|------|------|
| src/components/digital-staff/CollaborationChainGraph.vue | T33 | 新增 | 协作链流程图组件 |
| server/services/tenant-quota-service.js | T34 | 新增 | 租户配额后端服务 |
| server/routes/admin-quota.js | T36 | 新增 | 配额管理API路由 |
| server/services/published-article-service.js | T37 | 新增 | 已发布文章服务 |
| src/views/AdminQuotaManager.vue | T45 | 新增 | 配额管理前端界面 |
| src/api/admin.js | T45 | 新增 | 配额管理API调用 |
| server/services/websocket-service.js | T42 | 新增 | WebSocket服务端 |
| src/composables/useWebSocket.js | T43 | 新增 | WebSocket前端composable |
| bossagents-miniapp/src/utils/websocket.js | T44 | 新增 | 小程序WebSocket工具 |
修改文件清单
| 文件路径 | 任务 | 修改内容 |
|---|---|---|
| server/middleware/tenant-middleware.js | T35 | 新增 quotaGuard(resourceType) |
| server.js | T35/T36/T42 | 配额检查路由集成 + WebSocket初始化 |
| server/db-adapter.js | T34/T37 | 新增 tenant_quotas + published_articles 建表 |
| server/services/wechat-publish.js | T37 | 发布成功后调用 recordPublish() |
| server/routes/content-api.js | T37 | 新增已发布文章API |
| src/views/ContentManager.vue | T38 | 增加"已发布"标签页 |
| src/api/content.js | T38 | 新增已发布文章API调用 |
| src/components/bom/BomTreeGraph.vue | T39 | 新增 cyclePaths prop + 循环高亮CSS |
| server/services/bom-import-service.js | T39 | _detectCycle 返回循环路径 |
| server/services/asr-service.js | T40 | 增加阿里云后端 + 降级链 + 健康检查 |
| config.yaml | T40 | 新增 asr.providers + asr.aliyun 配置节 |
| src/views/DigitalStaff.vue | T33 | 集成 CollaborationChainGraph |
| src/router/index.js | T45 | 新增 /admin/quotas 路由 |
| bossagents-miniapp/src/store/index.js | T44 | 集成 WebSocket |
7. 依赖安装
# T42: WebSocket 服务端
npm install ws
# T42: JWT 验证(如未安装)
npm install jsonwebtoken
注意:项目使用 npm 安装依赖(约束要求),不使用 pnpm
8. DFX 实现策略
8.1 性能
| 指标 | 目标 | 实现策略 |
|------|------|---------|
| 协作链流程图渲染 | <1秒(10节点) | SVG 直接渲染,无虚拟DOM diff;节点数>20时启用懒加载 |
| 配额检查 | <50ms | SQLite 内存数据库查询;配额数据缓存在 TenantQuotaService 实例中 |
| WebSocket 推送延迟 | <200ms | 同进程推送,无网络跳转;消息 JSON 序列化优化 |
| ASR 降级切换 | 增加延迟<2秒 | 健康检查缓存(5分钟);优先使用最近健康检查通过的后端 |
8.2 可靠性
| 指标 | 实现策略 |
|---|---|
| WebSocket 断连重连 | 客户端自动重连(2秒间隔,最多5次,指数退避);服务端暂存离线消息 |
| ASR 降级不丢音频 | 音频 buffer 在降级链中传递,不重新采集 |
| 配额数据一致性 | 创建时实时 +1;每日凌晨全量同步;启动时全量同步 |
8.3 安全性
| 指标 | 实现策略 |
|---|---|
| WebSocket JWT 认证 | upgrade 事件中验证 JWT;无效 token 返回 4001 关闭连接 |
| 配额管理接口权限 | 验证 req.userRole === 'admin';非管理员返回 403 |
| 配额超限审计日志 | 调用 logAudit({ action: 'quota_exceeded', ... }) |
8.4 兼容性
| 指标 | 实现策略 |
|---|---|
| WebSocket 降级轮询 | useWebSocket.js 检测连接失败 → usePolling=true → 5秒轮询 |
| ASR 接口兼容 | 保持 /api/asr/recognize 接口格式不变,增加 provider 字段 |
| 存量租户兼容 | 配额表无记录时使用默认配额,不阻断操作 |
文档状态: 已完成
文件路径: C:\bossagents\docs\design-p2.md
BossAgents