DigitalStaff 统一 Pipeline 重构方案
1. 现状分析
当前架构:无统一 pipeline
DigitalStaff.vue (3051 行, 20+ 个独立 fetch)
│
├── fetchData() → GET /api/digital-staff/list
├── fetchTasks() → GET /api/digital-staff/tasks
│ → GET /api/digital-staff/task-stats
├── fetchStaffLogs(id) → GET /api/digital-staff/logs
├── fetchStaffReports(s) → GET /api/reports/list
├── confirmSummon() → POST /api/digital-staff/run
├── checkPendingConfirms()→ GET /api/digital-staff/resume
├── createTask() → POST /api/digital-staff/tasks
├── assignTask() → POST /api/digital-staff/tasks/:id/assign
├── completeTask() → POST /api/digital-staff/tasks/:id/complete
├── deleteTask() → DELETE /api/digital-staff/tasks/:id
└── ... 其他 N 个独立 fetch
核心问题
| 问题 | 说明 | 后果 |
|---|---|---|
| 无请求去重 | 切换员工时 fetchStaffReports 重复拉全量报告 | 性能浪费 |
| 无请求取消 | 快速切换员工时旧请求覆盖新状态 | 竞态 → .filter 报错 |
| 无批处理保证 | selectStaff 同时 fire logs + reports,一个成功另一个失败 | 面板数据不一致 |
| 无重试/退避 | 失败立即结束,不回退 | 服务器尖峰时全部失败 |
| 无统一错误处理 | 每个函数自己 try/catch,各自管理 loading 状态 | 错误重复,状态混乱 |
| 状态碎片化 | 18 个独立 ref + 4 个 computed,无一致性契约 | 调试困难 |
2. Pipeline 架构设计
2.1 核心概念
pipeline.request({ type, id, depends: [] })
→ 去重: 相同 type+id 的进行中请求被消除
→ 取消: 旧 type 被新 type 替代时自动 abort 旧请求
→ 分组: depends 用于表达原子性需求(全组成功或回滚)
→ 重试: 失败后按指数退避重试(最多 3 次)
→ 状态更新: 成功/失败后自动更新对应 ref
2.2 API 设计
// composables/usePipeline.js
interface PipelineOptions {
retry?: number // 重试次数,默认 2
retryDelay?: number // 初始退避延迟 ms,默认 500
abortSignal?: AbortSignal
}
interface RequestSpec<T = any> {
type: string // 'staff:list' | 'staff:logs' | 'staff:reports' | 'tasks:list' | ...
id?: string // 逻辑 id,用于去重和取消
group?: string // 批次组,同组内任一失败则整组回滚
handler: () => Promise<T> // 实际 fetch 函数
onSuccess?: (data: T) => void
onError?: (err: Error) => void
}
function usePipeline() {
const active: Ref<Map<string, AbortController>>
const pending: Ref<Set<string>>
const errors: Ref<Map<string, Error>>
function request<T>(spec: RequestSpec<T>, opts?: PipelineOptions): Promise<T>
function cancelGroup(group: string): void // 取消整组进行中的请求
function cancelAll(): void // 取消全部
function getError(type: string): Error | null // 获取最近错误
function clearErrors(): void
}
2.3 DigitalStaff Pipeline 层
// composables/useDigitalStaffPipeline.js
// 在 DigitalStaff.vue 之上加一层 pipeline 封装
export function useDigitalStaffPipeline() {
const pipeline = usePipeline()
// 状态 ref — 组件只消费这些
const state = reactive({
staffList: [],
selectedStaff: null,
staffLogs: [],
staffReports: [],
taskList: [],
taskStats: {},
loading: { staff: false, logs: false, reports: false, tasks: false },
errors: {}
})
// ✅ 原子性操作:selectStaff = logs + reports 要么全成功要么全失败
async function selectStaff(staff) {
// 取消旧的操作
pipeline.cancelGroup('staff-detail')
state.selectedStaff = staff
if (!staff?.id) return
// 统一批次请求: 同 group 内任一失败整组回滚
const results = await Promise.allSettled([
pipeline.request({
type: 'staff:logs',
id: staff.id,
group: 'staff-detail',
handler: () => fetch(`/api/digital-staff/logs?staff_id=${staff.id}`).then(r => r.json()),
onSuccess: (data) => { state.staffLogs = data.data || [] },
}),
pipeline.request({
type: 'staff:reports',
id: staff.id,
group: 'staff-detail',
handler: () => fetch('/api/reports/list').then(r => r.json()),
onSuccess: (data) => { state.staffReports = filterReports(data, staff.name) },
})
])
// 整组失败处理:重置相关状态
if (results.some(r => r.status === 'rejected')) {
state.staffLogs = []
state.staffReports = []
state.errors['staff-detail'] = '部分数据加载失败'
}
}
// ✅ 去重:快速切换 tab 时不重复请求
async function fetchTasks() {
return pipeline.request({
type: 'tasks:list',
handler: async () => {
const [listRes, statsRes] = await Promise.all([
fetch('/api/digital-staff/tasks'),
fetch('/api/digital-staff/task-stats')
])
const listJson = await listRes.json()
const statsJson = await statsRes.json()
state.taskList = (listJson.data?.tasks || []).filter(x => x != null)
state.taskStats = statsJson.data || {}
},
})
}
return { state, selectStaff, fetchTasks, /* ... 其他方法 */, pipeline }
}
3. Pipeline 核心实现
3.1 composables/usePipeline.js
import { ref, reactive, shallowRef } from 'vue'
export function usePipeline(options = {}) {
const defaultRetry = options.retry ?? 2
const defaultRetryDelay = options.retryDelay ?? 500
// 进行中的请求: type → AbortController
const active = shallowRef(new Map())
// 去重 key 集
const pending = ref(new Set())
function dedupKey(spec) {
return spec.group ? `${spec.group}:${spec.type}:${spec.id ?? ''}`
: spec.type + (spec.id ? `:${spec.id}` : '')
}
async function request(spec, opts = {}) {
const retry = opts.retry ?? defaultRetry
const retryDelay = opts.retryDelay ?? defaultRetryDelay
const key = dedupKey(spec)
// 去重:相同 key 的请求进行中则跳过
if (pending.value.has(key)) {
throw new Error(`DUPLICATE: ${key}`)
}
// 取消:旧 group 请求自动 abort
if (spec.group) {
cancelGroup(spec.group)
}
const controller = new AbortController()
pending.value = new Set(pending.value).add(key)
active.value = new Map(active.value).set(key, controller)
let lastError = null
for (let attempt = 0; attempt <= retry; attempt++) {
try {
const result = await spec.handler(controller.signal)
spec.onSuccess?.(result)
return result
} catch (err) {
lastError = err
if (err.name === 'AbortError') break // 主动取消不重试
if (attempt < retry) {
await delay(retryDelay * Math.pow(2, attempt))
continue
}
spec.onError?.(err)
}
}
throw lastError
// cleanup
finally {
pending.value = new Set([...pending.value].filter(k => k !== key))
active.value = new Map([...active.value].filter(([k]) => k !== key))
}
}
function cancelGroup(group) {
for (const [key, controller] of active.value) {
if (key.startsWith(`${group}:`)) {
controller.abort()
}
}
}
function cancelAll() {
for (const controller of active.value.values()) {
controller.abort()
}
pending.value = new Set()
active.value = new Map()
}
return { active, pending, request, cancelGroup, cancelAll }
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
3.2 DigitalStaff 接入方式
DigitalStaff.vue 保留模板不变, 改为:
// 之前: 20+ 独立 ref, 15 个独立 fetch 函数
// 之后:
import { useDigitalStaffPipeline } from '../composables/useDigitalStaffPipeline'
const ds = useDigitalStaffPipeline()
const { state, selectStaff, fetchTasks, /* ... */ } = ds
// 模板中的 staffList → state.staffList
// staffLogs → state.staffLogs
// loading → state.loading.staff
// ...
// 之前:
// function selectStaff(staff) {
// selectedStaff.value = staff
// fetchStaffLogs(staff.id)
// fetchStaffReports(staff)
// }
// 之后:
// function selectStaff(staff) {
// ds.selectStaff(staff)
// }
4. 实施计划
Phase 1: 创建 pipeline 基础设施
- 新建
composables/usePipeline.js(通用 pipeline 核心) - 新建
composables/useDigitalStaffPipeline.js(DigitalStaff 专属 pipeline 层) - 新文件,不修改现有代码
Phase 2: DigitalStaff 接入
- 修改
DigitalStaff.vue的 - 用
ds.state替换现有独立 ref - 用
ds.selectStaff()替换原有selectStaff - 同理替换
fetchTasks,fetchStaffLogs,fetchStaffReports - 模板不需要改动(变量名映射一致)
Phase 3: 验证
- 手动测试:切换到 backend 崩溃时是否还能复现
.filter错误 - 单元测试:pipeline 去重、取消、重试逻辑
- 性能对比:切换员工时的请求数从 N → 1
5. 与现有 useCapabilityPipeline.js 的关系
| | useCapabilityPipeline | 新的 usePipeline |
|---|---|---|
| 用途 | SCSAI 智能创建流程编排 | 通用 fetch 请求管理 |
| 粒度 | 业务步骤(intent→schema→llm→execute) | 单个 API 请求 |
| 通用性 | 绑定 Sciot 系统 | 纯通用,无业务依赖 |
| 关系 | — | useDigitalStaffPipeline 依赖 usePipeline |
BossAgents