左帮右臂 P2 技术设计文档

左帮右臂 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/:taskId API(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/:taskIdtaskId(路径参数){ 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/recognizeaudio(文件), 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 设计目标

  1. 与现有 db-adapter.js 的 SQLite/MySQL 双后端兼容
  2. 多租户隔离:所有表包含 enterprise_id 字段
  3. 审计追踪:关键操作记录到审计日志
  4. 配额数据一致性:创建时实时+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.jsT35新增 quotaGuard(resourceType)
server.jsT35/T36/T42配额检查路由集成 + WebSocket初始化
server/db-adapter.jsT34/T37新增 tenant_quotas + published_articles 建表
server/services/wechat-publish.jsT37发布成功后调用 recordPublish()
server/routes/content-api.jsT37新增已发布文章API
src/views/ContentManager.vueT38增加"已发布"标签页
src/api/content.jsT38新增已发布文章API调用
src/components/bom/BomTreeGraph.vueT39新增 cyclePaths prop + 循环高亮CSS
server/services/bom-import-service.jsT39_detectCycle 返回循环路径
server/services/asr-service.jsT40增加阿里云后端 + 降级链 + 健康检查
config.yamlT40新增 asr.providers + asr.aliyun 配置节
src/views/DigitalStaff.vueT33集成 CollaborationChainGraph
src/router/index.jsT45新增 /admin/quotas 路由
bossagents-miniapp/src/store/index.jsT44集成 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

← 返回案例列表
分享:
🤖 Try Now →
🤖
🎁