hangwin--mcp-chrome
23 KiB
23 KiB
模块 B — 录制与回放 技术设计(design.md)
版本:v1.0(与 record_replay.prd.md 配套)
目标读者:前端架构、扩展工程、MCP 工具维护者
1. 架构概览
- 现有能力复用:
- DOM 读取:
chrome_read_page(可得ref、可见元素、可推导候选选择器)。 - 交互执行:
chrome_computer(CDP 鼠标/键盘/滚动/等待/截图等),chrome_click_element,chrome_fill_or_select,chrome_keyboard。 - 注入执行:
chrome_inject_script(MAIN/ISOLATED)、chrome_send_command_to_inject_script。 - 抓包:
chrome_network_capture_start/stop、chrome_network_debugger_start/stop。 - 截图与坐标映射:
screenshot.ts+screenshot-context(已包含域名校验)。
- DOM 读取:
- 新增模块(建议路径):
app/chrome-extension/inject-scripts/recorder.js(内容脚本,事件捕获与序列化)。app/chrome-extension/entrypoints/background/record-replay/flow-store.ts(存储抽象)。app/chrome-extension/entrypoints/background/record-replay/flow-runner.ts(回放编排器)。app/chrome-extension/entrypoints/background/record-replay/selector-engine.ts(多策略定位与回退)。app/chrome-extension/entrypoints/background/record-replay/types.ts(类型定义)。- UI:Popup 下新增“录制/回放/列表”视图与“流编辑器”(Vue 组件),路径沿用
app/chrome-extension/entrypoints/popup/*。
说明:先以“扩展本地工作”落地,导入导出通过 JSON;如需长期持久化到文件系统,可在原生宿主(native-server)侧新增文件读写 API(与 file-handler.ts 模式一致)。
2. 数据模型(TS 接口)
// design note: comments in English
export type SelectorType = 'css' | 'xpath' | 'attr' | 'aria' | 'text';
export interface SelectorCandidate {
type: SelectorType;
value: string; // literal selector or text/aria expression
weight?: number; // user-adjustable priority; higher first
}
export interface TargetLocator {
ref?: string; // ephemeral ref from read_page
candidates: SelectorCandidate[]; // ordered by priority
}
export type StepType =
| 'click'
| 'dblclick'
| 'fill'
| 'key'
| 'scroll'
| 'drag'
| 'wait'
| 'assert'
| 'script'
| 'navigate';
export interface StepBase {
id: string;
type: StepType;
timeoutMs?: number; // default 10000
retry?: { count: number; intervalMs: number; backoff?: 'none' | 'exp' };
screenshotOnFail?: boolean; // default true
}
export interface StepClick extends StepBase {
type: 'click' | 'dblclick';
target: TargetLocator;
before?: { scrollIntoView?: boolean; waitForSelector?: boolean };
after?: { waitForNavigation?: boolean; waitForNetworkIdle?: boolean };
}
export interface StepFill extends StepBase {
type: 'fill';
target: TargetLocator;
value: string; // may contain {var}
}
export interface StepKey extends StepBase {
type: 'key';
keys: string; // e.g. "Backspace Enter" or "cmd+a"
target?: TargetLocator; // optional focus target
}
export interface StepScroll extends StepBase {
type: 'scroll';
mode: 'element' | 'offset' | 'container';
target?: TargetLocator; // when mode = element / container
offset?: { x?: number; y?: number };
}
export interface StepDrag extends StepBase {
type: 'drag';
start: TargetLocator;
end: TargetLocator;
path?: Array<{ x: number; y: number }>; // sampled trajectory
}
export interface StepWait extends StepBase {
type: 'wait';
condition:
| { selector: string; visible?: boolean }
| { text: string; appear?: boolean }
| { navigation: true }
| { networkIdle: true };
}
export interface StepAssert extends StepBase {
type: 'assert';
assert:
| { exists: string }
| { visible: string }
| { textPresent: string }
| { attribute: { selector: string; name: string; equals?: string; matches?: string } };
}
export interface StepScript extends StepBase {
type: 'script';
world?: 'MAIN' | 'ISOLATED';
code: string; // user script string
when?: 'before' | 'after';
}
export type Step =
| StepClick
| StepFill
| StepKey
| StepScroll
| StepDrag
| StepWait
| StepAssert
| StepScript
| (StepBase & { type: 'navigate'; url: string });
export interface VariableDef {
key: string;
label?: string;
sensitive?: boolean;
default?: string;
rules?: { required?: boolean; pattern?: string };
}
export interface Flow {
id: string;
name: string;
description?: string;
version: number;
meta?: {
createdAt: string;
updatedAt: string;
domain?: string;
tags?: string[];
bindings?: Array<{ type: 'domain' | 'path' | 'url'; value: string }>;
};
variables?: VariableDef[];
steps: Step[];
}
export interface RunLogEntry {
stepId: string;
status: 'success' | 'failed' | 'retrying';
message?: string;
tookMs?: number;
screenshotBase64?: string; // small thumbnail (optional)
consoleSnippets?: string[]; // critical lines
networkSnippets?: Array<{ method: string; url: string; status?: number; ms?: number }>;
}
export interface RunRecord {
id: string;
flowId: string;
startedAt: string;
finishedAt?: string;
success?: boolean;
entries: RunLogEntry[];
}
3.1(V2)可视化节点编排(DAG 模型)
- 兼容策略:现有线性
steps[]作为“链式节点图”(head→...→tail)。V2 引入节点与连线定义,保留对线性的完全兼容。 - 核心接口:
// Node/Edge graph model (V2)
export type NodeType =
| 'click'
| 'fill'
| 'key'
| 'wait'
| 'assert'
| 'script'
| 'navigate'
| 'openTab'
| 'switchTab'
| 'closeTab'
| 'http'
| 'extract'
| 'foreach'
| 'if'
| 'while'
| 'datasetWrite'
| 'clipboard'
| 'delay';
export interface NodeBase {
id: string;
type: NodeType;
name?: string;
disabled?: boolean;
config?: any;
ui?: { x: number; y: number };
}
export interface Edge {
id: string;
from: string;
to: string;
label?: 'default' | 'true' | 'false' | 'onError';
}
export interface FlowV2 extends Flow {
nodes?: NodeBase[];
edges?: Edge[]; // when present, runner uses graph mode
// optional subflows for foreach/while to encapsulate loops
subflows?: Record<string, { nodes: NodeBase[]; edges: Edge[] }>;
}
// Node registry
export interface NodeRuntime<T = any> {
// Validate config and infer IO schema
validate(config: T): { ok: boolean; errors?: string[] };
// Execute with context
run(ctx: NodeContext, config: T): Promise<NodeResult>;
}
export interface NodeContext {
tabId: number;
vars: Record<string, any>;
outputs: Record<string, any>;
runId: string;
logger: (e: RunLogEntry) => void;
}
export interface NodeResult {
output?: any;
error?: string;
}
- 执行语义:按 edges 拓扑驱动;对有条件边(true/false)、错误边(onError)进行分支控制;支持
foreach/while的子流程封装。 - 工具复用映射:
- click/fill/key/wait/assert/script/navigate:映射到现有工具(chrome_computer / read_page / wait-helper / inject_script)。
- openTab/switchTab/closeTab:映射 window/navigate 工具。
- http:映射
network_request工具;响应裁剪字段输出到上下文。 - extract:复用 accessibility-tree + DOM 查询抽取文本/属性。
- datasetWrite/clipboard/delay:新建工具或复用浏览器 API(clipboard 需权限)。
5.1(V2)执行器与兼容
- 线性兼容:当
nodes/edges缺省时,将steps[]转换为线性节点图执行。 - OnError:每个节点可定义错误策略(停止/重试/跳转 onError 边);重试按
retry。 - 并发:仅
foreach节点允许并发执行其子流(受全局并发上限约束)。
3. 录制器(recorder.js)
- 注入方式:ISOLATED world 内容脚本 + 事件监听;必要时在 MAIN world 执行最小代理(通过已有
inject-bridge.js)。 - 事件捕获:
- click/dblclick:使用捕获阶段监听,提取
target;调用accessibility-tree-helper的generateSelector逻辑生成候选;同时基于标签、role、aria-label、文本生成aria/text候选。 - input/change:对可填写元素读取
name/id/data-testid/type;值不入库,默认转变量(密码/信用卡自动标记sensitive)。 - keydown/keyup:记录组合键(ctrl/cmd/alt/shift + key);IME 记录 compositionstart/update/end,回放时先 focus 再
insertText。 - scroll:记录目标(页面/容器/元素),节流每 200ms 或每 120px 采样一次。
- drag&drop:记录起点、终点与离散路径(节流 50ms)。
- 导航/刷新/标签页切换:由 background 统一感知(tabs.onUpdated/onActivated)并记录。
- click/dblclick:使用捕获阶段监听,提取
- 元素标识:
- 优先生成
ref(复用__claudeElementMap机制,在 recorder 首次引用时确保注册)。 - 同步生成多策略候选(见 SelectorCandidate)。
- 优先生成
- 安全与隐私:
- 密码类输入统一转
{var};默认sensitive=true;实际值仅出现在回放参数弹窗,不写入 Flow。
- 密码类输入统一转
- 与后台通信:通过
chrome.runtime.sendMessage发送序列化 Step,将录制缓存到flow-store(未命名的新建 Flow 草稿)。
(V2 扩展)
- 录制为节点:录制完成后将线性 steps 转换为
nodes/edges链路,便于在画布上编辑;点击/输入节点自动生成target与候选选择器;可选择合并相邻 wait 为 click.after.wait。
4. 选择器引擎(selector-engine.ts)
- 定位顺序(可配置/可在编辑器中调整优先级):
ref(若仍可解析,最稳)。id/data-testid/name类唯一选择器。- 唯一 CSS(短路径优先)。
- ARIA role + name(
aria/*)。 - XPath(最后兜底,成本高)。
- 自动回退:若首选失败,依次尝试;成功后记录“回退路径”(用于提示“建议更新”)。
- 可见性与可交互校验:找到元素后,
scrollIntoView+isVisible校验;失败即认为本候选不可用。
5. 回放编排(flow-runner.ts)
- 执行策略:串行逐步执行(未来可支持分段并发,如多 Tab)。
- 核心流程:
- 初始化上下文:根据 PRD 选择目标 Tab/新建 Tab,必要时刷新;若启用抓包,则开始网络监听。
- 变量解析:合并默认值与用户输入;
sensitive变量只驻内存。 - 逐步执行:
- 定位:
selector-engine.locate(step.target) - 自动等待:可见/可交互;必要时
wait-helper.js的waitForText/selector。 - 执行:优先复用现有工具:
- click/dblclick/drag/scroll:
chrome_computer对应动作(CDP 精准坐标/按钮/路径)。 - fill:
chrome_computer的fill或chrome_fill_or_select(两者保留一条黄金路径,建议统一到chrome_computer以减少脚本注入次数)。 - key:
chrome_computer的key(带组合键),或降级keyboardTool。 - wait/assert:使用
wait-helper.js与read_page结合;assert 失败策略按 Step 配置。 - script:
chrome_inject_script(MAIN/ISOLATED)+chrome_send_command_to_inject_script。
- click/dblclick/drag/scroll:
- 导航判断:click 后若监听到 URL 变化或 beforeunload,自动进入导航等待(默认 10~15s)。
- 日志:记录耗时、状态、必要的截图(失败必带截图,成功缩略图可按需)。
- 定位:
- 收尾:关闭抓包,生成 RunRecord。
- 重试与超时:Step 级重试(次数/间隔/指数退避),全局超时保护。
- 错误分级:选择器失败/可见性失败/交互失败/导航超时/脚本异常,分类统计便于 UX 展示。
5.2(V2/V3)触发器与调度
- 触发器类型:
- manual:手动触发(UI 按钮)。
- onPage: URL 匹配(域/路径/URL 正则),基于
tabs.onUpdated,可指定目标 flow 与参数默认值。 - cron: 定时(
chrome.alarms),支持一次/重复;浏览器重启后恢复。 - hotkey: 快捷键(
commands)。
- 存储:
triggers:{id},包含类型、匹配规则、目标 flowId、默认变量。 - 安全:onPage/cron 执行前可弹出确认开关(策略项)。
5.3(V2)变量与表达式引擎
- 模板:支持 Mustache 风格
{var}与${path}(JSONPath/简化路径)混用;提供“选择上游节点输出字段” UI(自动生成路径)。 - 实现:
- 变量表:
vars(初始为 flow.variables + 用户输入)。 - 输出表:
outputs[nodeId] = any(节点运行结果)。 - 解析:在节点运行前对其 config 中的可绑定字段做模板替换与路径解析(只读、无代码执行,防 XSS)。
- 变量表:
5.4(V2)数据集(Dataset)与导出
- 数据集模型:
datasets:{flowId}-> rows(列动态,基于第一行 schema 推断)。 - 节点:
datasetWrite将对象写入数据集;datasetExport导出 CSV/JSON;clipboard复制文本。 - 限制:数据集行数/单元大小限额(如 5k 行/单单元 32KB),超限提示分段导出。
5.5(V3)并发与限流
foreach节点接受concurrency配置(默认 3,最大 10),使用 PromisePool 执行子流实例;- 全局并发上限控制避免资源枯竭;域级速率限制(同一域名内请求间隔)。
6. 存储与导入导出(flow-store.ts)
- 首选:
chrome.storage.local(键:flows:{id}、runs:{flowId}、schedules)。 - 导出:合并导出为单一 JSON(包含 flows 与基本 runs 索引),不含敏感值与截图大对象(可选内嵌小缩略图)。
- 导入:支持版本迁移(
version字段);ID 冲突策略(新建副本或覆盖)。 - 可选文件持久化:通过 native-server 新增
saveFlow/readFlowAPI 落地到工作目录,便于 Git 版本管理。
(V2 扩展)
- 版本与迁移:Flow
version增量;当检测到nodes/edges缺失时保持线性模式;从线性迁移到 DAG 时保留steps作为备份字段,支持回滚。
7. UI 交互(Popup + 编辑器)
- Popup:
- 顶部:开始/暂停/停止录制;范围选择(当前/全局);抓包开关。
- 中部:当前页面资源 -> 绑定的 Flow 快捷入口;最近运行记录。
- 底部:进入“录制流列表”。
- 流编辑器:
- 左侧时间线列表,支持拖拽排序、批量删除/禁用步骤。
- 右侧属性面板:目标定位/等待/断言/参数化/脚本节点/重试配置。
- 顶部工具栏:回放此流、从某步开始回放、保存、导出。
- 回放面板(浮层):实时高亮当前步骤,显示日志项与失败截图;一键“在失败点重试”。
(V2 扩展)画布编辑器:
- 底层:建议 Cytoscape.js / Elk + Vue 封装;支持节点拖拽、连线、分支条件标签(true/false/onError)、子流程折叠;
- 属性面板:节点类型选择、输入映射(变量/上游输出)、错误策略与重试;
- 调试:从任一节点开始执行、单步/断点、查看节点输出快照。
8. 安全与隐私
- 变量敏感值仅驻内存;不入库、不导出。
- 坐标点击使用
screenshot-context的域名校验(已有实现),防止跨站误点击。 - 跨域 iframe:V1 不注入;检测到则弹提示。
- MAIN world 注入严格按需、最小化代码面。
(V3 扩展)凭据与密钥:
- HTTP 节点引用“凭据”占位(key id),实际值仅在内存中使用;如需持久化,优先调用 native-server 提供的加密存储(可选)。
9. 兼容性与降级
- DevTools/调试器占用:无法开启 Debugger 抓包时,退回 WebRequest 捕获或关闭抓包,并提示。
- 选择器劣化:
ref失效 -> 候选优先级回退 -> 文本/ARIA -> 失败。 - IME:回放优先
Input.insertText(CDP)+ DOM 事件兜底。
(V2 扩展)
- DAG 无法执行情况(循环/断边):在编辑器即时校验并给出修复建议;运行时保护最大步数与最大深度。
10. 性能与可靠性
- 录制阶段:所有监听都节流/去抖;最大轨迹点数量可配置(默认 150)。
- 回放阶段:
- 默认等待策略“快失败 + 回退重试”,避免长时间卡死。
- 日志与截图大小控制(缩略图 base64 < 200KB)。
- 存储清理:运行记录只保留最近 N 条(默认 10)。
(V2/V3 扩展)
- 并发的内存/CPU 保护:超过资源阈值自动降并发并警告;
- 节点运行超时监控:长耗时节点(http/脚本)默认 30s 超时,可配置。
11. 与现有代码的集成点
accessibility-tree-helper.js:- 直接复用
generateSelector与__claudeElementMap,recorder 首次访问目标时确保注册以生成ref。
- 直接复用
computer.ts:- 作为主要执行通道;注意其已做 hostname 校验与
read_pageref 解析能力,可显著提升稳定性。
- 作为主要执行通道;注意其已做 hostname 校验与
wait-helper.js:- 用于
waitForText/selector,封装在flow-runner的等待环节。
- 用于
screenshot.ts:- 失败时调用获取截图;也可用于生成步骤缩略图(裁切元素)。
- 网络捕获:
- 按 PRD 开关选择
network-capture-web-request(轻量)或network-capture-debugger(深度)。
- 按 PRD 开关选择
12. 开发分期(Milestones)
-
M1(基础闭环):
- 录制 click/fill/key/wait + 导出 JSON;回放串行执行;基本列表与回放日志;变量输入弹窗;失败截图。
-
M2(鲁棒性与编辑):
- 选择器多策略与回退;流编辑器(时间线 + 属性面板);断言/脚本节点;绑定页面入口;导入导出。
-
M3(高级能力):
- drag/scroll/全局录制/标签页切换;抓包与网络片段;定时回放;失败点重试。
-
M4(可视化与节点化):
- DAG 画布(Cytoscape/Elk)、节点注册表、线性到 DAG 自动转换;If/Foreach/While、Open/Switch/CloseTab;HTTP/Extract/Dataset/Clipboard/Delay;OnError 边与节点级错误策略;表达式/变量映射与上游输出选择器;并发与全局限流;触发器中心(manual/onPage/cron/hotkey)。
16. MCP 动态工具集成(注册 / Schema / 执行 / 热更新)
16.1 总体思路
- 保持扩展侧“执行”收口为一个通用入口(建议新增 background 工具:
flow_run),以flowId + args运行; - 原生宿主(native-server)侧负责“动态注册 MCP 工具”,为每个 Flow 发布一个独立工具名(proxy),调用时转发到通用入口。
16.2 注册路径(native-server)
- 组件:
app/native-server/src/mcp/register-tools.ts - 新增职责:
- 从
chrome.storage.local(通过扩展消息)或本地工作区文件加载“已发布” Flow 列表; - 为每个 Flow 生成一个 MCP Tool 定义:
name:flow.<slug>;description: 来自meta.tool.description或 Flow 描述;inputSchema: 由 variables + 运行选项生成(见 16.3);onCall: 调用“通用入口”并等待结果(透传 runId/summary/outputs)。
- 监听扩展的“flow_publish/flow_unpublish/flow_update”事件,动态增删改 MCP 工具并刷新工具列表。
- 从
16.3 输入 Schema 生成
- 变量到 JSON Schema:
- type:
string|number|boolean|array|enum; - default/required/sensitive(敏感仅在 Schema 上标注说明,实际必须在调用参数中提供)。
- type:
- 运行选项 Schema:
{
"type": "object",
"properties": {
"tabTarget": { "type": "string", "enum": ["current", "new"], "default": "current" },
"refresh": { "type": "boolean", "default": false },
"captureNetwork": { "type": "boolean", "default": false },
"returnLogs": { "type": "boolean", "default": false },
"timeoutMs": { "type": "number", "minimum": 0 },
},
}
- 合并:将变量 Schema 与运行选项合并为工具最终
inputSchema(变量字段在根级,或置于vars字段——建议根级,调用简单)。
16.4 执行桥接
- MCP 工具(proxy)被调用 → native-server
onCall→ 通过原生消息发给扩展 background:- action:
record_replay.run_flow - payload:
{ flowId, args }
- action:
- background 调用
flow-runner.runFlow()执行;完成后返回统一结果体:{ runId, success, summary, url, outputs, logs? }。
16.5 热更新与版本
- Flow 保存为“已发布” → 扩展发送
flow_publish(flow)给原生宿主 → 注册工具; - Flow 更新:覆盖当前,替换
flow.<slug>的inputSchema/description并刷新; - 取消发布:原生宿主移除对应工具。
16.6 安全与边界
- 绑定域/路径校验:背景执行前校验当前 Tab/导航域是否符合,否则拒绝或在“新标签页 + startUrl”模式下执行;
- 敏感变量:只允许从调用参数进入,不在任何持久化中出现;
- 执行超时:尊重
timeoutMs,到时强制结束并返回失败; - 错误返回:保留 error category(selector/navigation/script 等),便于客户端指引修复。
13. 关键伪代码
// flow-runner.ts (core execution sketch)
async function runFlow(
flow: Flow,
runId: string,
options: {
tabTarget: 'current' | 'new';
refresh?: boolean;
captureNetwork?: boolean;
vars?: Record<string, string>;
},
) {
const tab = await ensureTab(options);
const run: RunRecord = {
id: runId,
flowId: flow.id,
startedAt: new Date().toISOString(),
entries: [],
};
try {
if (options.captureNetwork) await startNetworkCapture(tab.id!);
const vars = resolveVariables(flow.variables, options.vars);
for (const step of flow.steps) {
const start = performance.now();
try {
await executeStep(tab, step, vars);
run.entries.push({ stepId: step.id, status: 'success', tookMs: performance.now() - start });
} catch (e: any) {
const img = await safeScreenshot(tab.id!); // best-effort
run.entries.push({
stepId: step.id,
status: 'failed',
message: e?.message || String(e),
tookMs: performance.now() - start,
screenshotBase64: img,
});
if (!shouldRetry(step, e)) throw e;
await retryWithBackoff(() => executeStep(tab, step, vars), step.retry);
}
}
run.success = true;
} catch (e) {
run.success = false;
} finally {
if (options.captureNetwork) await stopNetworkCapture(tab.id!);
run.finishedAt = new Date().toISOString();
await saveRun(run);
}
}
14. 风险与对策
- 选择器脆弱:通过
ref+ 多候选 + 用户提示更新,降低回放失败概率。 - 页面异步/动画:默认增加短暂 wait 与
wait-helper,并允许编辑器细化等待条件。 - 录制脚本与站点脚本冲突:ISOLATED world + 最小化事件写入,避免污染页面。
15. 验收清单(技术)
- 典型登录用例 10 次回放 ≥ 95% 成功。
- 选择器回退路径在 UI 明确可见并可一键更新。
- 敏感变量不落盘;导出文件不含任何敏感值。
- 失败报告包含失败截图与错误分级。
Note: 本设计严格贴合现有代码结构与工具能力,优先复用 chrome_read_page / chrome_computer / wait-helper 等模块,控制新增复杂度并确保可落地性。