28 KiB
28 KiB
Service Worker 通信与任务执行架构
本文档介绍 AI 输入框、Service Worker、工作流、IndexedDB、sw-debug.html 调试面板之间的时序关系、数据流向、通信方式和数据同步策略。
目录
架构概览
┌─────────────────────────────────────────────────────────────────────────┐
│ 主线程 (Main Thread) │
├─────────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ AIInputBar │ │ ChatDrawer │ │ WorkZone │ │
│ │ (用户输入) │ │ (对话展示) │ │ (步骤展示) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └───────────────────┼───────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ WorkflowContext (状态管理) │ │
│ └──────────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────┼──────────────────────────────────┐ │
│ │ useWorkflowSubmission / swChannelClient │ │
│ │ (工作流提交 / SW 通信客户端) │ │
│ └──────────────────────────────┬──────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────────┴──────────────────────────────────┐ │
│ │ sw-debug.html (调试面板 - 独立页面) │ │
│ │ ┌───────────────────────────────────────────────────────┐ │ │
│ │ │ duplex-client.js → LLM API 日志 / PostMessage 日志 │ │ │
│ │ └───────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────┬──────────────────────────────────┘ │
└─────────────────────────────────┼───────────────────────────────────────┘
│
postmessage-duplex (双工通信)
│
┌─────────────────────────────────┼───────────────────────────────────────┐
│ Service Worker │
├─────────────────────────────────┼───────────────────────────────────────┤
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ SWChannelManager (通道管理器) │ │
│ │ - 管理多客户端连接 │ │
│ │ - RPC 方法路由 │ │
│ │ - 事件广播 │ │
│ └────────────────────────────┬────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ SWTaskQueue │ │WorkflowExecutor│ │ LLM API Logger │ │
│ │ (任务队列) │ │ (工作流执行) │ │ (API 日志) │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └───────────────────┼──────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ taskQueueStorage (IndexedDB) │ │
│ │ - tasks: 任务数据 │ │
│ │ - workflows: 工作流数据 │ │
│ │ - config: API 配置 │ │
│ │ - llm-api-logs: LLM API 日志 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
核心组件
1. 主线程组件
| 组件 | 路径 | 职责 |
|---|---|---|
| AIInputBar | packages/drawnix/src/components/ai-input-bar/AIInputBar.tsx |
用户输入、参数配置、触发生成 |
| ChatDrawer | packages/drawnix/src/components/chat-drawer/ChatDrawer.tsx |
对话展示、步骤状态 |
| WorkZone | packages/drawnix/src/plugins/with-workzone.ts |
画布上的工作流步骤展示 |
| WorkflowContext | packages/drawnix/src/contexts/WorkflowContext.tsx |
工作流状态管理 |
| swChannelClient | packages/drawnix/src/services/sw-channel/client.ts |
SW 通信客户端 |
| sw-debug | apps/web/public/sw-debug/ |
调试面板(独立页面) |
2. Service Worker 组件
| 组件 | 路径 | 职责 |
|---|---|---|
| SWChannelManager | apps/web/src/sw/task-queue/channel-manager.ts |
多客户端通信管理 |
| SWTaskQueue | apps/web/src/sw/task-queue/queue.ts |
任务生命周期管理 |
| WorkflowExecutor | apps/web/src/sw/task-queue/workflow-executor.ts |
工作流执行引擎 |
| taskQueueStorage | apps/web/src/sw/task-queue/storage.ts |
IndexedDB 存储层 |
| LLM API Logger | apps/web/src/sw/task-queue/llm-api-logger.ts |
API 调用日志 |
通信机制
基于 postmessage-duplex 的双工通信
// 主线程 → SW (RPC 调用)
const response = await channel.call('workflow:submit', { workflow });
// SW → 主线程 (广播)
channelManager.broadcastToAll('task:completed', { taskId, result });
// SW → 特定客户端 (点对点)
channelManager.sendToTaskClient(taskId, 'task:progress', { progress: 50 });
通信模式对比
| 模式 | 方向 | 方法 | 用途 |
|---|---|---|---|
| RPC | 主线程 → SW | channel.call() |
提交任务/工作流、查询状态 |
| 广播 | SW → 所有客户端 | channel.broadcast() |
任务创建、配置变更 |
| 点对点 | SW → 特定客户端 | sendToMappedClient() |
任务进度、完成、失败 |
| 请求-响应 | SW ↔ 主线程 | channel.publish() + handler |
主线程工具请求 |
关键 RPC 方法
// 任务相关
'task:create' // 创建任务
'task:cancel' // 取消任务
'task:listPaginated' // 分页查询任务
// 工作流相关
'workflow:submit' // 提交工作流
'workflow:cancel' // 取消工作流
'workflow:getStatus' // 查询工作流状态
// 调试相关
'debug:getLLMApiLogs' // 获取 LLM API 日志
'debug:getCacheEntries' // 获取缓存条目
关键事件
// 任务事件
'task:created' // 任务创建(广播)
'task:progress' // 任务进度(点对点)
'task:completed' // 任务完成(点对点)
'task:failed' // 任务失败(点对点)
// 工作流事件
'workflow:status' // 工作流状态变更
'workflow:stepStatus' // 步骤状态变更
'workflow:completed' // 工作流完成
'workflow:stepsAdded' // 动态添加步骤
// 调试事件
'debug:llmLog' // LLM API 日志实时推送
'postmessage:log' // PostMessage 日志
数据流向
1. 图片/视频生成流程
用户输入 → AIInputBar
│
▼
workflow-converter.ts (转换为工作流定义)
│
▼
useWorkflowSubmission.submitToSW()
│ RPC: workflow:submit
▼
SW: WorkflowExecutor.submitWorkflow()
│
├─→ IndexedDB: 保存工作流
│
▼
executeStep() 循环
│
├─ generate_image/generate_video → SW 直接执行
│ │
│ ▼
│ llmFetch() → Gemini/Veo3 API
│ │
│ ├─→ LLM API Logger (记录日志)
│ │
│ ▼
│ ImageHandler/VideoHandler (处理结果)
│ │
│ ├─→ Cache Storage (缓存媒体)
│ │
│ ▼
│ broadcast: task:completed
│
└─ ai_analyze/canvas_insert → 委托主线程
│
▼
sendToolRequest() → 主线程处理
│
▼
返回结果 → 继续执行
2. Agent 流程(动态步骤)
用户输入 "画一只猫" + 选择 Agent 模式
│
▼
convertAgentFlowToWorkflow() → 仅包含 ai_analyze 步骤
│
▼
SW: 执行 ai_analyze
│
▼
主线程: AI 分析 → 返回 addSteps
│
▼
SW: 添加新步骤 → broadcast: workflow:stepsAdded
│
▼
主线程: WorkflowContext 更新 → ChatDrawer/WorkZone 渲染
│
▼
SW: 继续执行新步骤 (generate_image 等)
3. 调试面板数据流
sw-debug.html 页面加载
│
▼
duplex-client.js 初始化
│
├─→ RPC: debug:getLLMApiLogs (获取历史日志)
│
└─→ onBroadcast: debug:llmLog (订阅实时日志)
│
▼
llmapi-logs.js 渲染日志列表
│
▼
点击展开 → RPC: debug:getLLMApiLogById (懒加载详情)
数据存储
IndexedDB 数据库结构
sw-task-queue (主数据库)
├── tasks # 任务数据
│ ├── id (主键)
│ ├── type, status, createdAt (索引)
│ └── params, result, error, remoteId, ...
│
├── workflows # 工作流数据
│ ├── id (主键)
│ ├── status, createdAt (索引)
│ └── steps, context, ...
│
├── config # 配置数据
│ ├── gemini_config
│ ├── video_config
│ └── mcp_system_prompt
│
├── chat-workflows # 聊天工作流
│
├── pending-tool-requests # 待处理工具请求
│
└── pending-dom-operations # 待处理 DOM 操作
llm-api-logs (日志数据库)
└── logs
├── id (主键)
├── timestamp, taskType, status, taskId (索引)
└── endpoint, model, prompt, requestBody, responseBody, ...
存储策略
| 数据类型 | 存储位置 | 容量限制 | 清理策略 |
|---|---|---|---|
| 任务数据 | IndexedDB tasks | 无限制 | 用户手动删除 |
| 工作流数据 | IndexedDB workflows | 无限制 | 用户手动删除 |
| LLM API 日志 | IndexedDB llm-api-logs | 1000 条 | FIFO 自动清理 |
| 媒体文件 | Cache Storage | 受浏览器配额限制 | LRU 清理 |
| 内存缓存 | SW 内存 | 50 条日志 | FIFO |
批量写入优化
// storage.ts: 50ms 窗口期批量写入
private batchSaveTimer: ReturnType<typeof setTimeout> | null = null;
private pendingSaves: Map<string, SWTask> = new Map();
saveTask(task: SWTask): void {
this.pendingSaves.set(task.id, task);
if (!this.batchSaveTimer) {
this.batchSaveTimer = setTimeout(() => this.flushPendingSaves(), 50);
}
}
时序图
完整生成流程
主线程 Service Worker 外部 API
│ │ │
│ 1. 用户点击生成 │ │
│─────────────────────────────────────│ │
│ │ │
│ 2. RPC: workflow:submit │ │
│───────────────────────────────────>│ │
│ │ 3. 保存到 IndexedDB │
│ │─────────────────────> │
│ │ │
│ 4. broadcast: workflow:status │ │
│<───────────────────────────────────│ │
│ │ │
│ │ 5. 执行步骤 │
│ │ │
│ │ 6. 调用 LLM API │
│ │──────────────────────────────>│
│ │ │
│ │ 7. 记录 API 日志 │
│ │─────────────────────> │
│ │ │
│ 8. broadcast: debug:llmLog │ │
│<───────────────────────────────────│ │
│ │ │
│ │ 9. 收到 API 响应 │
│ │<──────────────────────────────│
│ │ │
│ │ 10. 缓存媒体到 Cache Storage │
│ │─────────────────────> │
│ │ │
│ 11. broadcast: task:completed │ │
│<───────────────────────────────────│ │
│ │ │
│ 12. 更新 UI │ │
│─────────────────────> │ │
页面刷新恢复流程
页面刷新后
│
│ 1. swChannelClient.initialize()
│───────────────────────────────────>│
│ │
│ │ 2. 从 IndexedDB 加载任务/工作流
│ │─────────────────────>
│ │
│ │ 3. 恢复中断的任务
│ │ - 有 remoteId: 恢复轮询
│ │ - 无 remoteId: 查 LLM 日志恢复
│ │
│ 4. broadcast: workflow:recovered │
│<───────────────────────────────────│
│ │
│ 5. 恢复 WorkflowContext 状态 │
│─────────────────────> │
数据同步策略
1. 任务状态同步
// 主线程: useTaskQueue.ts
useEffect(() => {
// 订阅任务更新事件
const subscription = taskQueueService.observeTaskUpdates().subscribe(() => {
setTasks(taskQueueService.getAllTasks());
});
// 从 SW 同步任务数据
const syncFromSW = async () => {
await swTaskQueueService.syncTasksFromSW();
const swTasks = swTaskQueueService.getAllTasks();
taskQueueService.restoreTasks(swTasks);
setTasks(taskQueueService.getAllTasks()); // 强制刷新
};
syncFromSW();
}, []);
2. 工作流状态同步
// 主线程: useWorkflowSubmission.ts
workflowSubmissionService.subscribeToWorkflow(workflowId, (event) => {
switch (event.type) {
case 'step':
workflowControl.updateStep(event.stepId, event.status, ...);
break;
case 'completed':
workflowControl.complete();
break;
case 'steps_added':
workflowControl.addSteps(event.steps);
break;
}
});
3. 多客户端同步
// SW: channel-manager.ts
// 任务创建 → 广播给所有客户端
sendTaskCreated(taskId, task) {
this.broadcastToAll(SW_EVENTS.TASK_CREATED, { taskId, task });
}
// 任务进度 → 仅发送给创建者
sendTaskProgress(taskId, progress) {
this.sendToMappedClient(this.taskChannels, taskId, 'task:progress', { taskId, progress }, true);
}
4. 数据一致性保障
| 场景 | 策略 |
|---|---|
| SW 重启 | 从 IndexedDB 恢复所有状态 |
| 页面刷新 | 重新订阅事件 + 同步当前状态 |
| 多页面同时打开 | 通过广播保持同步 |
| 网络断开 | 任务保持 PROCESSING 状态,重连后恢复 |
优化方案
当前问题分析
-
数据同步延迟
- 问题:任务队列首次打开时数据为空
- 原因:SW 数据同步是异步的,UI 先渲染空状态
- 已修复:添加
isLoading状态 + 强制刷新
-
init RPC 超时导致状态丢失
- 问题:
[SWChannelClient] init failed: timeout,任务完成后提示"任务未找到" - 原因链:
- SW 端
handleInit等待storageRestorePromise(IndexedDB 恢复) - IndexedDB 操作慢导致 init RPC 超时
swTaskQueueService.initialized = falsesyncTasksFromSW()检查失败后直接返回- 任务完成广播丢失(
taskChannels映射已失效)
- SW 端
- 已修复:
syncTasksFromSW不再依赖init成功,尝试重新初始化 channelinit方法超时延长至 60 秒,且超时返回失败结果而非抛异常- 添加 5 秒轮询机制检查运行中任务状态
- 问题:
-
taskChannels 映射丢失
- 问题:页面刷新后,任务完成的点对点通知无法送达
- 原因:
taskChannels映射只在任务创建时建立,刷新后丢失 - 已修复:
fallbackBroadcast: true确保广播给所有客户端 - 兜底方案:客户端轮询运行中任务状态
-
通信层复杂度
- 问题:
message-bus.ts与channel-manager.ts功能重叠 - 影响:代码维护成本高
- 问题:
-
大文件问题
channel-manager.ts: 2081 行client.ts: 1071 行- 维护难度高
-
LLM 日志数据量大
- 单条日志可能包含大型 requestBody/responseBody
- 分页查询时传输数据过大
优化方案
方案 1: 通信层统一(已部分完成)
// 统一使用 channel-manager.ts,移除 message-bus.ts
// 优点:减少代码重复
// 风险:需要更新多处引用
方案 2: 预加载 + 骨架屏
// 在 App 初始化时预加载任务数据
useEffect(() => {
// 应用启动时预热 SW 连接 + 加载任务
swTaskQueueService.initialize().then(() => {
swTaskQueueService.syncTasksFromSW();
});
}, []);
// 任务面板使用骨架屏而非空状态
{isLoading ? <TaskSkeleton count={5} /> : <TaskList tasks={tasks} />}
方案 3: 增量同步
// 当前:每次同步全量任务
await swTaskQueueService.syncTasksFromSW();
// 优化:仅同步变更的任务
interface TaskSyncResult {
updatedTasks: Task[];
deletedTaskIds: string[];
lastSyncTime: number;
}
// SW 端记录变更
const changes = await storage.getTaskChangesSince(lastSyncTime);
方案 4: 日志数据分层
// 当前:精简数据 vs 完整数据
// 优化:三层数据结构
interface LLMApiLogSummary { // 列表展示
id: string;
timestamp: number;
status: string;
duration: number;
}
interface LLMApiLogDetail { // 展开详情
...LLMApiLogSummary;
endpoint: string;
model: string;
prompt: string; // 截断到 500 字符
}
interface LLMApiLogFull { // 完整导出
...LLMApiLogDetail;
requestBody: string;
responseBody: string;
}
方案 5: 工作流执行优化
// 当前:串行执行步骤
// 优化:支持并行执行无依赖步骤
interface WorkflowStep {
id: string;
dependsOn?: string[]; // 依赖的步骤 ID
}
// 执行引擎:同时执行所有依赖已满足的步骤
const executableSteps = steps.filter(s =>
s.status === 'pending' &&
(s.dependsOn || []).every(depId =>
steps.find(d => d.id === depId)?.status === 'completed'
)
);
await Promise.all(executableSteps.map(step => executeStep(step)));
方案 6: 通道管理器模块化
channel-manager/
├── index.ts # 核心类 (~400 行)
├── constants.ts # 常量定义 (~130 行) ✅ 已完成
├── task-handlers.ts # 任务 RPC 处理器 (~200 行)
├── workflow-handlers.ts # 工作流 RPC 处理器 (~200 行)
├── debug-handlers.ts # 调试 RPC 处理器 (~400 行)
├── event-senders.ts # 事件发送方法 (~300 行)
└── types.ts # 类型定义
优先级建议
| 优化 | 优先级 | 收益 | 风险 |
|---|---|---|---|
| 预加载 + 骨架屏 | 高 | 用户体验 | 低 |
| 增量同步 | 中 | 性能 | 中 |
| 通道管理器模块化 | 中 | 可维护性 | 中 |
| 日志数据分层 | 低 | 调试体验 | 低 |
| 并行执行步骤 | 低 | 生成速度 | 高 |
故障排查
常见问题
1. [SWChannelClient] init failed: timeout
症状:任务完成后提示"任务未找到",sw-debug.html 能看到数据
原因:
- SW 端
handleInit等待 IndexedDB 恢复超时 - 客户端
initRPC 超时后,swTaskQueueService.initialized保持 false syncTasksFromSW()因此跳过同步
排查步骤:
- 检查 IndexedDB 大小(DevTools → Application → IndexedDB)
- 查看 SW Console 日志:
[SWTaskQueue] restoreFromStorage... - 检查
swChannelClient.isInitialized()返回值
解决方案:
- 已修复:
init超时延长至 60 秒 - 已修复:
syncTasksFromSW尝试重新初始化 channel - 兜底:5 秒轮询运行中任务状态
2. 任务完成广播丢失
症状:SW 日志显示任务完成,但应用页面无反应
原因:
taskChannels映射丢失(页面刷新后)- 客户端 channel 未建立或已断开
排查步骤:
- 检查 SW Console:
[SWChannelManager] sendTaskCompleted - 检查客户端 Console:是否有
task:completed事件 - 使用 sw-debug.html 的 PostMessage 日志面板
解决方案:
fallbackBroadcast: true确保广播给所有客户端- 客户端轮询运行中任务状态
3. 多页面数据不同步
症状:两个应用页面,一个有任务数据,另一个没有
原因:
- 初始化时序问题
- 点对点通信发送到错误的客户端
排查步骤:
- 检查两个页面的
swChannelClient.isInitialized() - 查看 SW Console 的 clients 数量
解决方案:
- 使用
broadcastToAll广播重要事件 - 页面可见时触发同步(
visibilitychange事件)
调试工具
- sw-debug.html:查看 LLM API 日志、PostMessage 日志
- DevTools → Application → Service Workers:查看 SW 状态
- DevTools → Application → IndexedDB:查看存储数据
- Console 过滤:
[SWChannelManager]、[SWTaskQueue]、[SWChannelClient]
附录
相关文档
- CODING_RULES.md - postmessage-duplex 使用规范
- UNIFIED_CACHE_DESIGN.md - 缓存设计
- SW_DEBUG_POSTMESSAGE_LOGGING.md - 调试日志
关键代码路径
# 主线程
packages/drawnix/src/components/ai-input-bar/AIInputBar.tsx
packages/drawnix/src/hooks/useWorkflowSubmission.ts
packages/drawnix/src/services/sw-channel/client.ts
packages/drawnix/src/hooks/useTaskQueue.ts
# Service Worker
apps/web/src/sw/task-queue/channel-manager.ts
apps/web/src/sw/task-queue/queue.ts
apps/web/src/sw/task-queue/storage.ts
apps/web/src/sw/task-queue/workflow-executor.ts
apps/web/src/sw/task-queue/llm-api-logger.ts
# 调试面板
apps/web/public/sw-debug/duplex-client.js
apps/web/public/sw-debug/llmapi-logs.js
apps/web/public/sw-debug/sw-communication.js