chore(rr): 补充空 catch 注释,满足 no-empty 规则

This commit is contained in:
hangwin
2025-10-10 07:41:22 +00:00
parent 378f86bb0d
commit 800df0107c
4 changed files with 377 additions and 4 deletions
@@ -47,6 +47,63 @@ export async function runFlow(flow: Flow, options: RunOptions = {}): Promise<Run
await handleCallTool({ name: TOOL_NAMES.BROWSER.NAVIGATE, args: { refresh: true } });
}
// Ensure helper scripts are present for overlay/collectVariables
try {
await handleCallTool({ name: TOOL_NAMES.BROWSER.READ_PAGE, args: {} });
} catch {
/* ignore */
}
// Collect missing variables via lightweight prompt overlay
try {
const needed = (flow.variables || []).filter(
(v) =>
(options.args?.[v.key] == null || options.args?.[v.key] === '') &&
(v.rules?.required || (v.default ?? '') === ''),
);
if (needed.length > 0) {
const res = await handleCallTool({
name: TOOL_NAMES.BROWSER.SEND_COMMAND_TO_INJECT_SCRIPT,
args: { eventName: 'collectVariables', payload: undefined },
});
// Fallback: if direct collectVariables without payload not supported, call with explicit variables
let values: Record<string, any> | null = null;
try {
const t = (res?.content || []).find((c: any) => c.type === 'text')?.text;
const j = t ? JSON.parse(t) : null;
if (j && j.success && j.values) values = j.values;
} catch {
/* ignore */
}
if (!values) {
const res2 = await chrome.tabs
.query({ active: true, currentWindow: true })
.then(async (tabs) => {
const tabId = tabs?.[0]?.id;
if (typeof tabId !== 'number') return null;
return await chrome.tabs.sendMessage(tabId, {
action: 'collectVariables',
variables: needed,
} as any);
});
if (res2 && res2.success && res2.values) values = res2.values;
}
if (values) Object.assign(vars, values);
}
} catch {
// ignore prompt failures
}
// Init simple overlay for real-time logs
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs[0]?.id) {
await chrome.tabs.sendMessage(tabs[0].id, { action: 'rr_overlay', cmd: 'init' } as any);
}
} catch {
/* ignore */
}
// Binding enforcement: if bindings exist and no startUrl, verify current tab URL matches
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
@@ -302,6 +359,25 @@ export async function runFlow(flow: Flow, options: RunOptions = {}): Promise<Run
const resolvedBy = located?.resolvedBy || (located?.ref ? 'ref' : '');
const fallbackUsed =
resolvedBy && first && resolvedBy !== 'ref' && resolvedBy !== first;
// auto scroll into view if possible (unified)
try {
const sel = !located?.ref
? (step as any).target?.candidates?.find(
(c: any) => c.type === 'css' || c.type === 'attr',
)?.value
: undefined;
if (sel) {
await handleCallTool({
name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
args: {
type: 'MAIN',
jsScript: `try{var el=document.querySelector(${JSON.stringify(sel)});if(el){el.scrollIntoView({behavior:'instant',block:'center',inline:'nearest'});} }catch(e){}`,
},
});
}
} catch {
/* ignore */
}
const res = await handleCallTool({
name: TOOL_NAMES.BROWSER.CLICK,
args: {
@@ -343,6 +419,23 @@ export async function runFlow(flow: Flow, options: RunOptions = {}): Promise<Run
const fallbackUsed =
resolvedBy && first && resolvedBy !== 'ref' && resolvedBy !== first;
const value = resolveTemplate(s.value) ?? '';
// auto scroll into view if possible before fill
try {
const sel = !located?.ref
? s.target.candidates?.find((c) => c.type === 'css' || c.type === 'attr')?.value
: undefined;
if (sel) {
await handleCallTool({
name: TOOL_NAMES.BROWSER.INJECT_SCRIPT,
args: {
type: 'MAIN',
jsScript: `try{var el=document.querySelector(${JSON.stringify(sel)});if(el){el.scrollIntoView({behavior:'instant',block:'center',inline:'nearest'});} }catch(e){}`,
},
});
}
} catch {
/* ignore */
}
const res = await handleCallTool({
name: TOOL_NAMES.BROWSER.FILL,
args: {
@@ -508,6 +601,18 @@ export async function runFlow(flow: Flow, options: RunOptions = {}): Promise<Run
}
}
logs.push({ stepId: step.id, status: 'success', tookMs: Date.now() - t0 });
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs[0]?.id) {
await chrome.tabs.sendMessage(tabs[0].id, {
action: 'rr_overlay',
cmd: 'append',
text: `✔ ${step.type} (${step.id})`,
} as any);
}
} catch {
/* ignore */
}
// Run any deferred after-scripts now that a non-script step completed
if (pendingAfterScripts.length > 0) {
while (pendingAfterScripts.length) {
@@ -541,6 +646,18 @@ export async function runFlow(flow: Flow, options: RunOptions = {}): Promise<Run
message: e?.message || String(e),
tookMs: Date.now() - t0,
});
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs[0]?.id) {
await chrome.tabs.sendMessage(tabs[0].id, {
action: 'rr_overlay',
cmd: 'append',
text: `✘ ${step.type} (${step.id}) -> ${e?.message || String(e)}`,
} as any);
}
} catch {
/* ignore */
}
if (step.screenshotOnFail !== false) {
try {
const shot = await handleCallTool({
@@ -584,6 +701,14 @@ export async function runFlow(flow: Flow, options: RunOptions = {}): Promise<Run
}
const tookMs = Date.now() - startAt;
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
if (tabs[0]?.id) {
await chrome.tabs.sendMessage(tabs[0].id, { action: 'rr_overlay', cmd: 'done' } as any);
}
} catch {
/* ignore */
}
const record: RunRecord = {
id: runId,
flowId: flow.id,
@@ -63,11 +63,29 @@ async function stopRecording(): Promise<{ success: boolean; flow?: Flow; error?:
action: TOOL_MESSAGE_TYPES.RR_RECORDER_CONTROL,
cmd: 'stop',
});
const flow = (flowRes && flowRes.flow) as Flow;
if (flow) {
await saveFlow(flow);
const flowFromTab = (flowRes && flowRes.flow) as Flow | undefined;
// 合并后台聚合的步骤(跨标签页)与内容脚本返回的步骤
const aggregated = currentRecording.flow;
const merged: Flow | undefined = (function merge() {
if (aggregated && !flowFromTab) return aggregated;
if (!aggregated && flowFromTab) return flowFromTab;
if (!aggregated && !flowFromTab) return undefined;
// both present: prefer元数据以 flowFromTab 为基,步骤采用 aggregated(包含跨标签页)
return {
...(flowFromTab as Flow),
steps: (aggregated as Flow).steps || [],
variables: (aggregated as Flow).variables || (flowFromTab as Flow).variables,
meta: {
...((flowFromTab as Flow).meta || {}),
...((aggregated as Flow).meta || {}),
updatedAt: new Date().toISOString(),
} as any,
} as Flow;
})();
if (merged) {
await saveFlow(merged);
}
const resp = { success: true, flow };
const resp = { success: true, flow: merged };
currentRecording = null;
return resp;
} catch (e: any) {
@@ -96,6 +114,29 @@ export function initRecordReplayListeners() {
// ignore
}
}
// 统一在后台累积步骤,便于跨标签页聚合
try {
if (!currentRecording.flow) {
currentRecording.flow = {
id: `flow_${Date.now()}`,
name: '未命名录制',
version: 1,
steps: [],
variables: [],
meta: {
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
} as Flow;
}
currentRecording.flow.steps.push(step);
currentRecording.flow.meta = {
...(currentRecording.flow.meta || ({} as any)),
updatedAt: new Date().toISOString(),
} as any;
} catch {
// ignore
}
} else if (message.payload?.kind === 'stop') {
currentRecording.flow = message.payload.flow || currentRecording.flow;
}
@@ -220,4 +261,107 @@ export function initRecordReplayListeners() {
// ignore
}
});
// 全局录制:监听激活的标签页变化,追加“导航/切换”步骤,并确保在新标签页继续录制
chrome.tabs.onActivated.addListener(async (activeInfo) => {
try {
if (!currentRecording) return;
const tab = await chrome.tabs.get(activeInfo.tabId);
if (!tab) return;
// 确保该标签页注入并继续录制(不重置流)
try {
await ensureRecorderInjected(activeInfo.tabId);
await chrome.tabs.sendMessage(activeInfo.tabId, {
action: TOOL_MESSAGE_TYPES.RR_RECORDER_CONTROL,
cmd: 'resume',
} as any);
} catch {
/* ignore */
}
if (!currentRecording.flow) return;
const url = typeof tab.url === 'string' ? tab.url : '';
if (url) {
currentRecording.flow.steps.push({
id: `step_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
type: 'navigate',
url,
} as any);
currentRecording.flow.meta = {
...(currentRecording.flow.meta || ({} as any)),
updatedAt: new Date().toISOString(),
} as any;
}
} catch {
// ignore
}
});
// 全局录制:监听顶级导航提交(含显式地址栏输入/刷新),在非链接点击触发时追加“导航”步骤
chrome.webNavigation.onCommitted.addListener(async (details) => {
try {
if (!currentRecording) return;
if (details.frameId !== 0) return; // 仅记录顶级 frame
const tabId = details.tabId;
// 对于点击触发的 link 导航,不追加 navigate(已有点击+waitForNavigation 富化)
const t = details.transitionType as string | undefined;
const isLink = t === 'link';
if (isLink) {
// 同时确保继续录制
try {
await ensureRecorderInjected(tabId);
await chrome.tabs.sendMessage(tabId, {
action: TOOL_MESSAGE_TYPES.RR_RECORDER_CONTROL,
cmd: 'resume',
} as any);
} catch {
/* ignore */
}
return;
}
// 显式导航/刷新/地址栏输入等
const shouldRecord =
t === 'reload' ||
t === 'typed' ||
t === 'generated' ||
t === 'auto_bookmark' ||
t === 'keyword';
if (!shouldRecord) {
// 仍确保脚本在该页活跃,用于持续录制
try {
await ensureRecorderInjected(tabId);
await chrome.tabs.sendMessage(tabId, {
action: TOOL_MESSAGE_TYPES.RR_RECORDER_CONTROL,
cmd: 'resume',
} as any);
} catch {
/* ignore */
}
return;
}
const tab = await chrome.tabs.get(tabId);
const url = typeof tab.url === 'string' ? tab.url : details.url;
if (!url || !currentRecording.flow) return;
currentRecording.flow.steps.push({
id: `step_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
type: 'navigate',
url,
} as any);
currentRecording.flow.meta = {
...(currentRecording.flow.meta || ({} as any)),
updatedAt: new Date().toISOString(),
} as any;
// 确保继续录制
try {
await ensureRecorderInjected(tabId);
await chrome.tabs.sendMessage(tabId, {
action: TOOL_MESSAGE_TYPES.RR_RECORDER_CONTROL,
cmd: 'resume',
} as any);
} catch {
/* ignore */
}
} catch {
// ignore
}
});
}
@@ -474,6 +474,56 @@
sendResponse({ status: 'pong' });
return false;
}
if (request && request.action === 'rr_overlay') {
try {
const cmd = request.cmd || 'init';
let root = document.getElementById('__rr_overlay_root');
if (!root) {
root = document.createElement('div');
root.id = '__rr_overlay_root';
Object.assign(root.style, {
position: 'fixed',
right: '8px',
bottom: '8px',
zIndex: 2_147_483_647,
maxWidth: '40vw',
maxHeight: '40vh',
overflow: 'auto',
background: 'rgba(0,0,0,0.6)',
color: '#fff',
fontFamily:
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
fontSize: '12px',
padding: '8px',
borderRadius: '6px',
boxShadow: '0 2px 8px rgba(0,0,0,0.3)',
});
const title = document.createElement('div');
title.textContent = 'Record-Replay 运行日志';
Object.assign(title.style, { fontWeight: 'bold', marginBottom: '6px' });
const body = document.createElement('div');
body.id = '__rr_overlay_body';
root.appendChild(title);
root.appendChild(body);
document.documentElement.appendChild(root);
}
const body = document.getElementById('__rr_overlay_body');
if (cmd === 'append' && body) {
const line = document.createElement('div');
line.textContent = String(request.text || '');
body.appendChild(line);
body.scrollTop = body.scrollHeight;
}
if (cmd === 'done' && root) {
root.style.opacity = '0.5';
}
sendResponse({ success: true });
return true;
} catch (e) {
sendResponse({ success: false, error: String(e && e.message ? e.message : e) });
return true;
}
}
if (request && request.action === 'generateAccessibilityTree') {
const result = __generateAccessibilityTree(request.filter || null);
sendResponse({ success: true, ...result });
@@ -573,6 +623,28 @@
return true;
}
}
if (request && request.action === 'collectVariables') {
try {
const vars = Array.isArray(request.variables) ? request.variables : [];
const values = {};
for (const v of vars) {
const key = String(v && v.key ? v.key : '');
if (!key) continue;
const label = v.label || key;
const def = v.default || '';
const promptText = `请输入参数 ${label} (${key})`;
// Note: prompt in page context; in some sites may be blocked by CSP
let val = window.prompt(promptText, def);
if (typeof val !== 'string') val = def;
values[key] = val;
}
sendResponse({ success: true, values });
return true;
} catch (e) {
sendResponse({ success: false, error: String(e && e.message ? e.message : e) });
return true;
}
}
if (request && request.action === 'resolveRef') {
const ref = request.ref;
try {
@@ -157,6 +157,28 @@
pushStep({ type: 'key', keys, screenshotOnFail: false });
}
// Composition IME events (record markers for analysis; playback is no-op via script step)
function onCompositionStart() {
if (!isRecording) return;
pushStep({
type: 'script',
world: 'ISOLATED',
when: 'before',
code: '/* compositionstart */',
screenshotOnFail: false,
});
}
function onCompositionEnd() {
if (!isRecording) return;
pushStep({
type: 'script',
world: 'ISOLATED',
when: 'before',
code: '/* compositionend */',
screenshotOnFail: false,
});
}
let lastScrollAt = 0;
function onScroll(e) {
if (!isRecording) return;
@@ -206,6 +228,8 @@
document.addEventListener('input', onInput, true);
document.addEventListener('keydown', onKeydown, true);
document.addEventListener('keyup', onKeyup, true);
document.addEventListener('compositionstart', onCompositionStart, true);
document.addEventListener('compositionend', onCompositionEnd, true);
window.addEventListener('scroll', onScroll, { passive: true });
document.addEventListener('mousedown', onMouseDown, true);
document.addEventListener('mousemove', onMouseMove, true);
@@ -218,6 +242,8 @@
document.removeEventListener('input', onInput, true);
document.removeEventListener('keydown', onKeydown, true);
document.removeEventListener('keyup', onKeyup, true);
document.removeEventListener('compositionstart', onCompositionStart, true);
document.removeEventListener('compositionend', onCompositionEnd, true);
window.removeEventListener('scroll', onScroll, { passive: true });
document.removeEventListener('mousedown', onMouseDown, true);
document.removeEventListener('mousemove', onMouseMove, true);
@@ -268,6 +294,12 @@
start(request.meta || {});
sendResponse({ success: true });
return true;
} else if (cmd === 'resume') {
// Attach without resetting flow or sending start event
isRecording = true;
attach();
sendResponse({ success: true });
return true;
} else if (cmd === 'stop') {
const flow = stop();
sendResponse({ success: true, flow });