# Electron 桌面客户端设计(全面替换 WXT 浏览器插件) **状态**:Draft v2.6 · 已按 2026-04-20 当前实现口径修订 **作者**:@liangxu + Claude(codex/gemini 交叉审校) **日期**:2026-04-18 **范围**:新增 `apps/desktop-client/`、`/api/desktop/*` 服务端路由、相关 DB schema;删除 `apps/browser-extension/` 与 `/api/plugin/*` 路由及相关代码。 > **v2.6 当前实现口径**:平台侧二次人工审核设计已废弃。SaaS 完成人工审核后才创建 publish job;desktop client 只负责执行发布与回写结果。`manual / waiting_user / parked / lease?from_parked=true / review-window` 相关设计全部作废。桌面端新增“发布管理”视图,用于查看待发布队列、历史发送结果,并允许对成功/失败/unknown/aborted 的发布任务手动再次发送。文档中若在历史修订记录里仍出现 parked/manual,均以本段为准。 --- ## 1. 背景与动机 现有 `apps/browser-extension/`(WXT MV3 Chrome 扩展,"GEO Publisher")同时承担两类工作: 1. 向 13 家中国媒体平台做内容分发(微信公众号、头条号、百家号、搜狐号、网易号、ZOL、懂车帝、知乎、企鹅号、简书、掘金、B 站、什么值得买)。 2. 监控主流 LLM(豆包、千问、DeepSeek、混元、Kimi)在给定查询下的输出,判定品牌 是否被引用。 当前问题: - **LLM 抓取不稳定**。监控模块依赖 DOM 选择器与 `webRequest.onBeforeRequest` 在 MV3 下受限,无法可靠拿到 SSE 响应 Body;平台 UI 一变就挂。 - **适配器受 MV3 约束**。后台 service worker 频繁休眠,长任务难以保证完成;跨平台复杂登录(QR、滑块、SMS)在扩展内体验差。 - **不可扩展到真正的系统级自动化**。系统托盘、开机自启、多账号并发 session 隔离、进程级 CDP 调试,这些 MV3 都做不到。 - **现有适配器实现覆盖不全**。仓库实测:发布侧 13 家里 `weixin_gzh / juejin / dongchedi / smzdm / wangyihao / bilibili / zol` 共 **7 家** 仍返回 `unsupportedPublishResult`(detect-only 占位);监控侧 DeepSeek 是 `defaultAdapter` 占位没实现 query。这部分要**从零实现**,不是迁移。 因此:**全面替换**为一个 Electron 桌面客户端,承担上述全部职责。浏览器扩展停止维护并下线。 --- ## 2. 设计硬约束(已确认) 1. **全面替换**,不做"先 monitoring 后 publishing"的分阶段替换。 2. **不做向后兼容**。因为是开发版本阶段,服务端协议、DB schema、认证字段按桌面客户端的最佳形态重新设计,不为插件保留旧字段或旧路由。 3. **登录走嵌入式 Web View**。用户在 Electron 真实平台登录页里自己完成扫码/密码/短信/滑块;我们只保存 cookie,不保存账号密码。 4. **Cookie 本地存储 并加密**(Electron `safeStorage`)。服务端**永不存储** Cookie 或任何平台凭据。Linux 上 `safeStorage` 可能退化为 `basic_text`,必须启动时检测并显式降级告警(见 §4.5)。 5. **LLM 监控一律走网页 + CDP 拦截**,不调用任何 LLM 官方 API(成本导向;多账号 = 多配额,线性成本比 token 计费友好)。后端 `llm.api_key` 是后端自用管线,与客户端监控完全解耦。 6. **不打包 Playwright**。复用 Electron 自带 Chromium,通过 `webContents.debugger` 直接用 CDP。包体越小越好。 7. **支持 5 目标**:Windows x64/arm64、macOS Intel+Apple Silicon、Linux x64/arm64。 8. **一个平台账号只绑定一个 workspace**(不支持跨租户共享账号)。该约束必须落到 DB 唯一约束与 API 级冲突校验,不依赖客户端自觉(见 §6.6 `platform_accounts` 的 `platform_uid` 字段 + 唯一索引)。**前置条件**:workspace 必须先升成后端一等安全边界(见 §12 Plan 0);仅靠新增 middleware 不够,因为现有代码里 `Actor/Claims/cache/monitoring` 多处仍按 tenant 粒度(v2.2 三轮审查发现的底座问题)。 9. **演进缝预留**:短期 `一账号 = 一 workspace = 一台 client` 是产品边界,不是代码硬写。本版 DDL 继续使用 `workspace_id` / `client_id` / `target_client_id` 命名(不提前引入 `credential_holder_*` / `account_home_*` 命名以避免 v2.4 前 spec 与 DDL 不一致);未来真正做"共享运营账号池 / 桌面端主备"时,通过 rename migration + 兼容视图平滑演进。 10. 要求最小 electron 体积包 --- ## 3. 架构总览 ### 3.1 进程拓扑 单 Electron 进程树(方案 A)。所有视图宿主统一用 `WebContentsView`(Electron 已把 `BrowserView` 标记为 deprecated)。 - **Main 进程**(Node):任务调度器、Session 注册表、加密凭据仓库、CDP debugger 管理、租约状态机、与后端 REST/SSE 通信、Tray & autoUpdater。 - **Renderer 进程**(托盘窗口 / 登录窗口 / 发布预览窗口):Vue UI,基于 `packages/ui-shared`(见 §3.3)。禁用 `nodeIntegration`,通过 `contextBridge` 暴露窄接口。 - **Utility 进程**:为 Patch 可执行代码沙箱化而预留(见 §5.5)。`utilityProcess` 不开 Node 能力,仅以 IPC 与主进程通信。 - **工作 WebContentsView**:每个登录账号一个 `persist:` session,挂在离屏 `BaseWindow` 上。 - **生命周期**:**两档**。Hot-tier(最近 N 分钟有任务或保活的账号)常驻 WebContentsView;Cold-tier 账号只保留 session partition 在磁盘,WebContentsView lazy-create。阈值默认 30 min,可配置。避免 10 账号场景下 RAM 被固定占用。 - 仅在实测 CDP 抓流阻塞 UI 线程时,才把 debugger 交互迁到 `utilityProcess`,其他模块保持不动。 ### 3.2 模块分层 新增 `apps/desktop-client/`,作为 pnpm workspace 的新包: ``` apps/desktop-client/ main/ bootstrap.ts # app ready, single-instance-lock, tray, autoUpdater session-registry.ts # account-id → (WebContentsView|null) + session view-pool.ts # Hot/Cold 两档 WebContentsView 生命周期 vault.ts # safeStorage 包 cookie/token + SQLite(本机恢复缓存) vault-export.ts # 用户交互式导出(口令二次加密)用于手工迁机器 scheduler.ts # 本地 cron + 服务端 SSE/pull 合流 lease-manager.ts # 任务租约:ack→attempt_id+lease_token→extend/cancel/result rate-limiter.ts # 每平台/账号 token bucket,共享 business/keep-alive/probe 预算 circuit-breaker.ts # 平台级熔断 keep-alive.ts # 探活 / 保活 / resume warm-up 错峰 crash-recovery.ts # 启动时按 task.kind 分派恢复策略 patch-loader/ registry.ts # 已加载 patch 版本表 signer.ts # manifest + Ed25519 + hash + 回滚序号校验 parser-dsl.ts # 受限 DSL 解释器(默认通道) vm-host.ts # utilityProcess 沙箱启动 + 冻结 global(备用通道) tracing/ recorder.ts # 结构化日志 + 关键截屏(Alpha 范围) trace-writer.ts # 轻量 zip(Alpha 仅 JSON 日志 + 截屏) redactor.ts # Cookie / Authorization / password 字段自动脱敏 # Playwright 兼容导出作为 Phase 2 能力,见 §5.6 transport/ api-client.ts # 复用 packages/http-client,注入 client_token sse-client.ts # /api/desktop/events 长连接 + 60s pull 兜底 preload/ bridge.ts # contextBridge 窄 API renderer/ # 极简 Vite 项目,仅消费 packages/ui-shared main.ts routes.ts views/ # 桌面端专属视图(控制台/发布管理/账号管理/任务中心/诊断) ``` ### 3.3 关键复用(更务实的口径) **可复用**: - `packages/shared-types`:平台枚举、任务类型、DTO。需要**扩展**:加入租约、platform_uid、patch manifest 等新类型。 - `packages/http-client`:HTTP 基座直接用;**v2.3 新增** `SseClient` 类——内置指数退避重连、`Last-Event-ID` 处理、首帧 snapshot 消费、事件去重。Web 和 Desktop 两端共用。[apps/admin-web/src/lib/api.ts](apps/admin-web/src/lib/api.ts) 现有的简陋 `subscribeSSE` 迁移到这里并补能力。 - **新建 `packages/ui-shared`**:从 `apps/admin-web` 抽出**通用组件、i18n、样式、utils**。admin-web 与 desktop-client 两边都只消费共享包。Renderer 是一个极简 Vite 项目,不是克隆+删页面。**必须抽出**的组件: - `MarkdownPreview.vue` + `markdown-it` 渲染器(让 Web 发布前预览和 Desktop manual-review 用同一套渲染,避免"Web 看到的"和"桌面端审核看到的"不一致)。 - `AccountCard.vue` / `TagMultiSelect.vue` / `HealthBadge.vue`(账号管理 + 发布 Modal 共用)。 - `SseSubscription.vue` composable(封装 `SseClient` 给 Vue 用)。 - i18n 文案、主题变量、toast/dialog 系统。 - 老适配器里的**选择器字符串**和**业务流程描述**("打开编辑器 → 填标题 → 粘正文 → 上传封面 → 点发布"这种语义)。 **不可复用、必须重写或新建**: - 所有适配器代码——老代码硬编码 `chrome.tabs / chrome.storage / chrome.runtime / chrome.webRequest / wxt` 框架依赖,全部换成 Electron `session / WebContentsView / webContents.debugger / Fetch domain`。 - **发布适配器中 7 家 detect-only 的平台**(微信公众号、掘金、懂车帝、什么值得买、网易号、B 站、ZOL)**需从零实现 publish**。 - **DeepSeek 监控**需从零实现 query。 - Trace 基建、Patch 通道、租约管理器、LeaseManager + sandbox-loader、ui-shared 抽取——全部新建。 --- ## 4. 账号 & 会话模型 ### 4.1 数据结构 ```ts type Account = { id: string // uuid,本地生成 platform: PlatformKind // 'wechat' | 'toutiao' | ... | 'doubao' | 'qwen' | 'deepseek' platformUid?: string // 平台侧真实账号 ID / OpenID / UIN;登录探针抓取 accountFingerprint?: string // 额外防伪:昵称/手机号尾号/账号主页 URL hash purpose: 'publish' | 'monitor' displayName: string // 用户自填,仅作本地展示 partitionKey: string // 'persist:acc-',Electron session 隔离 proxy?: ProxyConfig // 可选 session-level 代理 createdAt: Date lastSeenAt: Date verifiedAt?: Date // 上次成功 probe 的时间 health: 'live' | 'expired' | 'captcha' | 'risk' tenantId: string workspaceId: string keepAlive: { mode: 'probe-only' | 'heartbeat' | 'disabled' probeIntervalMs: number // 默认 30 min heartbeatIntervalMs?: number // 默认 8 min,仅 heartbeat 模式 lastProbeAt?: Date lastHeartbeatAt?: Date failureStreak: number } quota: { hourlyBudget: number // 默认 60/小时,含业务+保活+探活合计 usedThisHour: number resetAt: Date } tags?: string[] // 用户可自定义分组标签(如"品牌号"/"矩阵号"/"测试号"),Modal 按 tag 批量勾选 syncVersion: number // 单调递增,每次写入 +1,用于 client-server 一致性 CAS deletedAt?: Date // tombstone,软删除,resync 时用于判断 "server 已删" } type ProxyConfig = { scheme: 'http' | 'socks5' host: string port: number auth?: { user: string; pass: string } // 存入 vault,不明文落盘 } ``` ### 4.2 登录流程 1. 用户在 UI 点「新增账号 → 选平台」。 2. 主进程 `session-registry.create(platform)` 分配新 partition,打开一个**可见** `BrowserWindow`(登录专用),加载该平台登录页。 3. 用户自己用扫码 / 密码 / 短信 / 滑块登录。 4. 登录探针(preload 脚本 + URL 规则 + 成功态 DOM 标记)检测到已登录态后,在窗口顶部叠加"登录成功"遮罩层 1.5 s,然后关窗。 5. 探针在关窗前抓取平台侧 `platformUid`(如微信公众号的 `fakeid` / B 站的 `mid` / 知乎的 `hash_id`),写入 `Account.platformUid`。 6. Electron 自动把 cookies 持久化到 `userData/Partitions//`;同时主进程用 `safeStorage` 加密写本地 SQLite 作为**本机恢复缓存**(跨账号解绑、重装应用时 restore 用;**非迁机器备份**)。 7. 启动时强制 `safeStorage.isEncryptionAvailable()`;若在 Linux 上 `safeStorage.getSelectedStorageBackend()` 返回 `basic_text`,UI 显著告警、默认关闭加密快照(避免把平台 cookie 明文写盘)。 8. 账号进入"常驻池",hot-tier 分配 WebContentsView,cold-tier 仅保留 partition;按需激活 CDP。 ### 4.3 多账号并存 同一平台多账号完全隔离——partition 不同、cookies 不同、UA 不同、可通过 session 级 proxy 走不同 IP。代理来自 `Account.proxy` 字段,由 `_shared/proxy-helper.ts` 统一调用 `ses.setProxy`(§5.4)。调度器按账号维度限流(默认同一账号并发 1、同一平台并发 3,可配置)。 ### 4.4 Cookie/Session 保活策略 三个档位: | 档位 | 动作 | 频率 | 代价 | |---|---|---|---| | **探活** | 加载主页,检查 DOM 登录标志 | 30 min | 低 | | **保活** | 访问平台私有 API 触发 Set-Cookie/token 刷新 | 8 min(豆包/千问)| 中 | | **按需激活** | 任务到来前 pre-check | 任务驱动 | 低 | | 平台类型 | 账号示例 | 保活方式 | 节奏 | |---|---|---|---| | 媒体发布 | 微信/头条/B 站/知乎 等 13 家 | 只探活 | 30 min | | 豆包 | Doubao | 主动保活 | 8 min + 30 min 探活 | | 千问 | Qwen | 主动保活 | 8 min + 30 min 探活 | | DeepSeek | DeepSeek | 只探活 | 30 min | | **元宝**(腾讯)| Yuanbao · 入口 `yuanbao.tencent.com`(**不是** hunyuan.tencent.com,后者是开发者/模型页不是对话入口)· 登录方式:微信扫码 / QQ / 邮箱(微信涉及 `open.weixin.qq.com` iframe + 本地 loopback 检查)| 接入时按 §5.2 spike 矩阵复测;默认主动保活 | 8 min + 30 min 探活 | | Kimi | Moonshot · 入口 `www.kimi.com` · 登录方式:微信扫码 / 手机号快捷登录。实测请求是 `POST /apiv2/kimi.gateway.chat.v1.ChatService/Chat`(gRPC-Web binary-framed,**非 EventSource**,parser 需先 framing 解包,见 §5.2) | 接入时按 §5.2 spike 矩阵复测;默认主动保活 | 8 min + 30 min 探活 | 实施细节: - **错峰**:同一平台 N 个账号按 `hash(accountId) mod period` 分布时间槽 + ±30s 抖动;当 N > 槽数时使用随机顺延。 - **统一 token bucket**:每个 `(platform, account)` 维度一只 leaky bucket,桶容量 = `hourlyBudget`。**业务任务、保活、探活同桶扣减**——避免保活把业务流量配额吃掉或反之。 - **优先级**:业务任务 > 保活 > 探活;同槽冲突由优先级决定是否放行。 - **失败升级**:保活失败 1 次立即重试;连 2 次失败降级为探活一次确认状态;探活失败则 `health: expired`,托盘红点 + OS 通知 → 用户点击弹登录窗(partition 复用,不需要重建账号)。 - **429/验证码退避**:任何响应识别为 429/风控页/captcha 时,该账号进入**指数退避**(基础 30s,因子 2,上限 30 min),期间**完全暂停保活**,只保留"按需激活"(业务任务到来时才 pre-check)。 - **静音时段**:默认 0:00–7:00(**用户系统本地时区**)不保活,可配置关闭。 - **休眠感知**:`powerMonitor.on('suspend'|'resume')`,休眠暂停全部保活;**唤醒后错峰 warm-up**——按 `hash(accountId)` 分片,每 10s 一批探活(每批 ≤3 账号),避免同步流量尖峰。 - **配额池**:同一 LLM 多账号时,调度器按"最近使用 + 剩余配额"挑账号。 ### 4.5 凭据粒度与备份 - **不**保存账号密码。 - 只保存 Cookie + 平台侧必要 token(如 Doubao 的 `ms_token`、`fp`)。 - **`safeStorage` + SQLite** 是**本机恢复缓存**:userData 被清/重装应用可恢复,不保证可迁机器。 - **跨机器迁移**走独立的**用户交互式导出**流程(`vault-export.ts`):本机解密后用**用户输入的口令**二次加密导出为 `.vault` 文件;新机器同样输入口令解密、用新环境的 `safeStorage` 重加密落盘。导出的 `.vault` 文件不会自动上传服务端。 - 服务端**永远不**持有任何形式的 cookie 备份(硬约束 4)。 --- ## 5. 适配器模型 ### 5.1 接口定义 ```ts interface AdapterContext { accountId: string session: Session view: WebContentsView // 统一使用 WebContentsView debugger: DebuggerClient rateLimit: RateLimitHandle // 业务调用前 acquire logger: ScopedLogger signal: AbortSignal reportProgress: (stage: string) => void // 供状态机向 lease-manager 报进度 } interface PublishAdapter { platform: PlatformKind capabilities: { images: boolean; video: boolean; markdown: boolean; /* ... */ } probe(ctx: AdapterContext): Promise keepAlive(ctx: AdapterContext): Promise // 默认 no-op publish(ctx: AdapterContext, task: PublishTask): Promise } interface MonitorAdapter { provider: LLMProvider probe(ctx: AdapterContext): Promise keepAlive(ctx: AdapterContext): Promise query(ctx: AdapterContext, task: MonitorQueryTask): Promise } ``` ### 5.2 监控适配器:流式 SSE 抓取技术路径(**先 spike 后定案**) **v2.2 review 发现 `Fetch.takeResponseBodyAsStream` 与 `Fetch.continueResponse` 在 CDP 语义上互斥**:前者独占响应 body,后者让页面继续消费。因此**不能**写成"连续调用链"。v2.3 改为"分流 + 实测矩阵 + spike gate"。 **总方针**:不同平台用不同 domain,**不劫持能不劫持**(劫持会阻塞页面继续跑),只在必须看响应 body 时才用 Fetch 劫持。 **按传输类型分流**(每条路线的可用性需在 Plan A 先做 **spike 矩阵**确认): | 页面使用的机制 | 推荐 CDP 路径 | 约束 | |---|---|---| | `new EventSource(url)` | `Network.enable` → `Network.eventSourceMessageReceived` 监听每条 message | 仅监听、不劫持。最干净。 | | `fetch(url)` + `text/event-stream` + streaming body | `Network.enable` → `Network.streamResourceContent(requestId)` + `Network.dataReceived` 事件流式拼接 | **Chromium ≥ 120**(Electron 41+ 满足)。仅监听、不阻断页面。| | `fetch(url)` + chunked transfer + 自定义协议 | 同上 `streamResourceContent + dataReceived` | 同上 | | **gRPC-Web / binary-framed fetch-stream**(Kimi `/apiv2/kimi.gateway.chat.v1.ChatService/Chat`)| 同上 `streamResourceContent + dataReceived`,拿到 raw bytes 后 **provider parser 先做 gRPC-Web framing 解包**(5-byte 前缀:1B compressed flag + 4B message length)再解 payload | 引入 `adapters/_shared/framer.ts` 公共工具 | | 必须**劫持 / 替换** 响应(一般不需要,只在改写响应或注入时)| `Fetch.enable({patterns})` → `Fetch.requestPaused` → `Fetch.fulfillRequest`(**不再调 continueResponse**);若需读 body 再处理,用 `Fetch.getResponseBody`(**等响应结束**)后统一返回 | `takeResponseBodyAsStream` 后页面**不能继续正常消费该响应**,故仅用于"必须替换响应"场景,监控场景不选 | **Spike 矩阵(Plan A 必须先跑)**: ``` provider × transport × Electron 版本 × domain 可用性 × 实际效果 豆包 千问 DeepSeek 元宝/混元 Kimi EventSource ? ? ? ? ? streamResource ? ? ? ? ? dataReceived ? ? ? ? ? ``` 每个 cell 在开工前用 `electron/chrome://devtools` 实跑一次登录后的真实聊天请求,记录实际的 request 种类与可抓到的 body 行为。**没有 spike 结果就不能定任何 adapter 的实现细节**。 **Chromium/CDP 兼容矩阵文件**:`docs/superpowers/specs/electron-chromium-cdp-matrix.md`(Plan A 阶段新建),记录每次升 Electron 大版本的 domain 可用性回归结果。截至 2026-04-18 Electron stable 41.2.0(Chromium 146),上述三类 domain 全部可用。 实施步骤: ``` 1. ensureLoggedIn(ctx) → probe / 必要时触发保活 2. openChat(ctx) → 在 WebContentsView 里导航或复用 tab 3. detect protocol → 通过 DevTools Protocol 注入一小段探测脚本,返回 EventSource / fetch-stream 4. attach CDP Fetch or Network → 按 §5.2 表格选 domain 5. submit query → 触发网页发出请求 6. collect streaming body → eventSourceMessageReceived 或 IO.read 7. parse provider-specific payload → normalized MonitorResult 8. detach CDP(finally) ``` **Provider-specific parser**:每家 LLM 的 payload schema 不同(豆包 `{event: 'data', data: {text: ...}}` 这种结构),放 `adapters/doubao.ts` 等私有文件。Schema 变化通过 Patch 通道下发新 `parser`(§5.5)。 ### 5.3 发布适配器状态机 + 租约绑定 每个发布任务或每个账号带 `PublishMode`: ```ts type PublishState = | 'filling' // adapter 正在往编辑器填内容 | 'submitting' // 已经点了发布按钮,等平台回应 | 'done' // 平台明确返回成功 | 'failed' // 明确失败(可重试) | 'aborted' // 用户或系统取消 | 'unknown' // 崩溃/超时/无法判断(人工对账,见 §7.5) ``` **状态机与租约的绑定**(见 §6 协议): - SaaS 中的文章审核在创建 publish job 之前完成;desktop client **不再承接二次人工审核**。 - 进入 `filling / submitting` 时,适配器必须每 60 s 调 `lease-manager.extend(lease_token)` 续租(lease TTL 默认 10 min)。 - 用户取消或 signal abort → `aborted`。 - adapter 抛异常 → `failed`,带错误码。 - 进程崩溃 → 启动时按 §7.5 分派为 `unknown`(仅限 publish 任务的 `submitting` 阶段)。 - 需要再次发送的文章,不走 parked/resume,而是在桌面端“发布管理”里基于原 publish task 重新创建一次新的 publish job。 文件上传:`protocol.handle('file')` + CDP `Input.dispatchDragEvent` 或直接喂 ``,不经服务端中转。 ### 5.4 公共能力 `adapters/_shared/`: - `debugger-helper.ts`:封装 CDP attach/detach、Fetch/Network domain 选择、SSE 流式拼接、超时。 - `dom-helper.ts`:语义化选择器、重试、容错。 - `upload-helper.ts`:文件上传统一入口。 - `captcha-pause.ts`:遇滑块/点击验证时把 WebContentsView 转前台让用户过,通过后续跑。 - **`proxy-helper.ts`**(新):统一走 `Session.setProxy(Account.proxy)`,支持 socks5/http + 凭据存 vault。 ### 5.5 双轨更新:局部热更 + 全量(安全收紧) | 通道 | 内容 | 节奏 | 形式 | |---|---|---|---| | **Patch 通道(默认·纯数据 DSL)** | 适配器的 `selectors.json` + `parser.dsl`(受限的**声明式解析 DSL**,非 JS)| 分钟级按需 | 服务端签名 manifest + 补丁包;客户端 DSL 解释器运行,**无任意 JS 执行** | | **Patch 通道(备用·隔离执行)** | 个别场景确需写 JS parser 时的"逃生舱" | 仅在评审后发布 | 加载到 `utilityProcess`(无 Node 能力)+ `isolated-vm` 或 `vm.createContext` + 冻结 global + 禁用 `eval/Function/WebAssembly/dynamic import` + 仅允许消息传递返回结构化结果 | | **Release 通道** | 二进制、主进程、UI、适配器宿主框架 | 周/双周 | electron-updater 差分包 | **默认走纯数据 DSL 通道**——安全、可审、无 RCE 面。仅当平台响应结构复杂到 DSL 难以表达时,才考虑升级到备用通道,且每个 parser 必须经过安全评审。 **签名链(TUF 思路简化版)**: 每个 patch 打包为 `manifest.json + patch.data`,manifest 至少包含: ```json { "platform": "doubao", "patch_seq": 47, // 单调递增,防回滚 "hash": "sha256:", "expires_at": "2026-05-18T00:00:00Z", "min_client_version": "1.2.0", "max_client_version": null, // 可选 "key_id": "root-2026-01", // 支持根密钥轮换 "revokes": [], // 撤销之前的 patch_seq 列表 "sig": "" } ``` 客户端约束: - 内置 root key(可通过 Release 通道更新),支持 `key_id` 轮换。 - 下载时校验签名;**每次加载前再次校验 hash + 过期时间 + 未被 `revokes` 列表覆盖**。 - `patch_seq` 必须严格 > 已加载值,否则拒绝(防降级重放)。 - 服务端下发一个 `revoke ` 指令 → 客户端停用对应 patch、降级为前一版本,直到收到下一批 manifest。 - UI 每个适配器显示:`client 1.2.0 · doubao patch #47 (via DSL)`。 ### 5.6 Trace 基础设施(Alpha 范围 + Phase 2 兼容 Playwright)(不着急) **Alpha 范围(MVP)**: - 结构化日志(pino JSON)滚动写入 `userData/logs/`。 - 任务失败时自动截屏 3–5 张 + 存 Network 响应摘要。 - 本地内置极简 viewer(Vue 单页,离线),能读 `.trace.json` 文件。**加载与隔离要求**: - Viewer Vite 构建用 **hash 路由**(`createWebHashHistory`),避免 `file://` 协议下 history 路由 404。 - Viewer 跑在 **独立的 sandboxed `