com um clique
playwright-interactive
通过 `js_repl` 进行持久化的浏览器与 Electron 交互,用于快速迭代式的 UI 调试。
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Menu
通过 `js_repl` 进行持久化的浏览器与 Electron 交互,用于快速迭代式的 UI 调试。
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Baseado na classificação ocupacional SOC
审计工作、用具体证据核验声明,并用五年级生也能懂的简单语言解释结果。当用户要求 review、审计、检查、核验、解释一处改动、解释一处修复、总结测试结果、验证某个东西是否有效,或把技术性发现翻译成给非技术读者看的大白话时使用。
当用户想为任何页面撰写、重写或改进营销文案时使用——包括首页、落地页、定价页、功能页、关于页或产品页。也在用户说"write copy for""improve this copy""rewrite this page""marketing copy""headline help""CTA copy""value proposition""tagline""subheadline""hero section copy""above the fold""this copy is weak""make this more compelling"或"help me describe my product"时使用。只要有人在处理需要说服或转化的网站文本,就用它。邮件文案见 email-sequence;弹窗文案见 popup-cro;编辑已有文案见 copy-editing。
仅起草的 Gmail 客户支持分诊,并为每封草稿创建对应的 Codex 项目线程。当用户要求运行客户邮件自动化、检查未读/最近的支持邮件、准备 Gmail 草稿回复、对客户/个人邮件分诊同时跳过自动化邮件,或为已起草邮件的跟进创建 agent/项目线程时使用。
对照适用的操作手册、草稿安全、证据、改动(mutation)和提交范围要求,核验客户支持工作。在每次 Gmail/客户支持分诊、账单/取消/退款/账户/访问权限调查、支持交接或草稿复核任务之后、给出最终回复之前使用;也在用户要求对照操作手册、清单、skill 或要求来核验支持工作时使用。
创建一份周期性的每日 UI 灵感采集。当用户要求运行、刷新、打包或校验带日期的 UI 灵感集时使用,尤其针对 `articles/YYYY-MM-DD-ui-inspiration-capture/` 产物、Framer/Dribbble 落地页灵感、动效研究截图/视频、AI-builder 提示词、重复检查,或把项目操作手册转成可重复的工作流程。
使用本地语音配置,从脚本或内联文本生成 ElevenLabs 文本转语音音频。当用户要 ElevenLabs、text-to-speech、TTS、旁白、配音、语音音频或语音生成时使用;语音名称、voice id、邮箱、所有者和账户特定默认值只从 skill 之外的本地配置载入。
| name | playwright-interactive |
| description | 通过 `js_repl` 进行持久化的浏览器与 Electron 交互,用于快速迭代式的 UI 调试。 |
使用持久化的 js_repl Playwright 会话来调试本地 web 或 Electron 应用,在多次迭代之间保持同一批句柄存活,并在不重启整套工具链(除非进程归属发生变化)的情况下运行功能与视觉 QA。
js_repl。js_repl,在你的 Codex 配置文件中启用它:[features]
js_repl = true
--enable js_repl 启动新会话(等价于 -c features.js_repl=true)。js_repl 后,启动一个新的 Codex 会话以刷新工具列表。--sandbox danger-full-access(或对应的 sandbox_mode=danger-full-access 配置)启动 Codex。这是一个临时要求,因为沙箱内的 js_repl + Playwright 支持仍在完善中。js_repl_reset 当作恢复工具,而非日常清理。重置内核会销毁你的 Playwright 句柄。test -f package.json || npm init -y
npm install playwright
# Web-only, for headed Chromium or mobile emulation:
# npx playwright install chromium
# Electron-only, and only if the target workspace is the app itself:
# npm install --save-dev electron
node -e "import('playwright').then(() => console.log('playwright import ok')).catch((error) => { console.error(error); process.exit(1); })"
如果你之后切换到另一个工作区,在那里重复设置。
var chromium;
var electronLauncher;
var browser;
var context;
var page;
var mobileContext;
var mobilePage;
var electronApp;
var appWindow;
try {
({ chromium, _electron: electronLauncher } = await import("playwright"));
console.log("Playwright loaded");
} catch (error) {
throw new Error(
`Could not load playwright from the current js_repl cwd. Run the setup commands from this workspace first. Original error: ${error}`
);
}
绑定规则:
var,因为后续的 js_repl cell 会复用它们。undefined 并重新运行该 cell,而不是到处添加恢复逻辑。page、mobilePage、appWindow)优先保留一个命名句柄,而不是反复从 context 重新发现页面。共享 web 辅助函数:
var resetWebHandles = function () {
context = undefined;
page = undefined;
mobileContext = undefined;
mobilePage = undefined;
};
var ensureWebBrowser = async function () {
if (browser && !browser.isConnected()) {
browser = undefined;
resetWebHandles();
}
browser ??= await chromium.launch({ headless: false });
return browser;
};
var reloadWebContexts = async function () {
for (const currentContext of [context, mobileContext]) {
if (!currentContext) continue;
for (const p of currentContext.pages()) {
await p.reload({ waitUntil: "domcontentloaded" });
}
}
console.log("Reloaded existing web tabs");
};
对于 web 应用,默认使用显式视口,并把原生窗口模式当作单独的一轮验证。
deviceScaleFactor,而不是直接切换到原生窗口模式。viewport: null)做单独的一轮 headed 检查。noDefaultViewport 启动,所以把它当作真实的桌面窗口,在调整任何尺寸之前先检查启动时的大小和布局。context 复用到原生窗口一轮,反之亦然;关闭旧的 page 和 context,再为新模式创建新的。桌面与移动 web 会话共享同一个 browser、辅助函数和 QA 流程。主要区别在于你创建哪对 context 和 page。
把 TARGET_URL 设为你正在调试的应用。对于本地服务器,优先使用 127.0.0.1 而非 localhost。
var TARGET_URL = "http://127.0.0.1:3000";
if (page?.isClosed()) page = undefined;
await ensureWebBrowser();
context ??= await browser.newContext({
viewport: { width: 1600, height: 900 },
});
page ??= await context.newPage();
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded:", await page.title());
如果 context 或 page 失效,设 context = page = undefined 并重新运行该 cell。
当 TARGET_URL 已存在时复用它;否则直接设置一个移动目标。
var MOBILE_TARGET_URL = typeof TARGET_URL === "string"
? TARGET_URL
: "http://127.0.0.1:3000";
if (mobilePage?.isClosed()) mobilePage = undefined;
await ensureWebBrowser();
mobileContext ??= await browser.newContext({
viewport: { width: 390, height: 844 },
isMobile: true,
hasTouch: true,
});
mobilePage ??= await mobileContext.newPage();
await mobilePage.goto(MOBILE_TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded mobile:", await mobilePage.title());
如果 mobileContext 或 mobilePage 失效,设 mobileContext = mobilePage = undefined 并重新运行该 cell。
var TARGET_URL = "http://127.0.0.1:3000";
await ensureWebBrowser();
await page?.close().catch(() => {});
await context?.close().catch(() => {});
page = undefined;
context = undefined;
browser ??= await chromium.launch({ headless: false });
context = await browser.newContext({ viewport: null });
page = await context.newPage();
await page.goto(TARGET_URL, { waitUntil: "domcontentloaded" });
console.log("Loaded native window:", await page.title());
当当前工作区就是 Electron 应用、且 package.json 的 main 指向正确的入口文件时,把 ELECTRON_ENTRY 设为 .。如果你需要直接针对某个特定的主进程文件,改用像 ./main.js 这样的路径。
var ELECTRON_ENTRY = ".";
if (appWindow?.isClosed()) appWindow = undefined;
if (!appWindow && electronApp) {
await electronApp.close().catch(() => {});
electronApp = undefined;
}
electronApp ??= await electronLauncher.launch({
args: [ELECTRON_ENTRY],
});
appWindow ??= await electronApp.firstWindow();
console.log("Loaded Electron window:", await appWindow.title());
如果 js_repl 不是从 Electron 应用工作区启动的,在启动时显式传入 cwd。
如果应用进程看起来失效,设 electronApp = appWindow = undefined 并重新运行该 cell。
如果你已经有一个 Electron 会话,但在主进程、preload 或启动变更后需要一个全新进程,改用下一节的重启 cell,而不是重新运行这个。
尽可能让同一个会话保持存活。
Web 渲染层重载:
await reloadWebContexts();
Electron 仅渲染层重载:
await appWindow.reload({ waitUntil: "domcontentloaded" });
console.log("Reloaded Electron window");
主进程、preload 或启动变更后的 Electron 重启:
await electronApp.close().catch(() => {});
electronApp = undefined;
appWindow = undefined;
electronApp = await electronLauncher.launch({
args: [ELECTRON_ENTRY],
});
appWindow = await electronApp.firstWindow();
console.log("Relaunched Electron window:", await appWindow.title());
如果你的启动需要显式 cwd,在这里也带上同样的 cwd。
默认姿态:
js_repl cell 简短,专注于一次交互爆发。browser、context、page、electronApp、appWindow),而不是重新声明它们。electronApp.evaluate(...)。js_repl 一次,然后在多次迭代之间保持同一批 Playwright 句柄存活。page.evaluate(...) 和 electronApp.evaluate(...) 可以检查或预置状态,但它们不算作签收输入。如果你打算通过 codex.emitImage(...) 发出截图,默认使用下一节中经过 CSS 归一化的路径。那些是将被模型解读、或用于基于坐标的后续动作的截图的标准示例。仅把原始捕获作为对保真度敏感的调试的例外;原始例外的示例出现在归一化指导之后。
如果你将用 codex.emitImage(...) 发出截图供模型解读,在发出前把它归一化为你所捕获的确切区域的 CSS 像素。这能让返回的坐标与 Playwright CSS 像素对齐(如果回复后来被用于点击),也能减小图像负载大小和模型 token 成本。
默认不要发出原始的原生窗口截图。只有在你明确需要设备像素保真度时才跳过归一化,例如 Retina 或 DPI 伪影调试、像素精确的渲染检查,或其他原始像素比负载大小更重要的保真度敏感场景。对于不会发给模型的仅本地检查,原始捕获没问题。
不要假定 page.screenshot({ scale: "css" }) 在原生窗口模式(viewport: null)下就够了。在 macOS Retina 显示器上的 Chromium 中,即使请求了 scale: "css",headed 原生窗口截图仍可能以设备像素尺寸返回。同样的注意事项适用于通过 Playwright 启动的 Electron 窗口,因为 Electron 以 noDefaultViewport 运行,appWindow.screenshot({ scale: "css" }) 可能仍返回设备像素输出。
对 web 页面和 Electron 窗口使用不同的归一化路径:
page.screenshot({ scale: "css" })。如果原生窗口 Chromium 仍返回设备像素输出,在当前页面内用 canvas 重新调整尺寸;不需要临时页面。appWindow.context().newPage() 或 electronApp.context().newPage() 作为临时页面。Electron context 不可靠地支持那条路径。在主进程中用 BrowserWindow.capturePage(...) 捕获,用 nativeImage.resize(...) 调整尺寸,并直接发出那些字节。共享辅助函数与约定:
var emitJpeg = async function (bytes) {
await codex.emitImage({
bytes,
mimeType: "image/jpeg",
detail: "original",
});
};
var emitWebJpeg = async function (surface, options = {}) {
await emitJpeg(await surface.screenshot({
type: "jpeg",
quality: 85,
scale: "css",
...options,
}));
};
var clickCssPoint = async function ({ surface, x, y, clip }) {
await surface.mouse.click(
clip ? clip.x + x : x,
clip ? clip.y + y : y
);
};
var tapCssPoint = async function ({ page, x, y, clip }) {
await page.touchscreen.tap(
clip ? clip.x + x : x,
clip ? clip.y + y : y
);
};
page 或 mobilePage,对 Electron 使用 appWindow,作为 surface。clip 当作渲染层中 getBoundingClientRect() 的 CSS 像素。quality: 85 的 JPEG。{ x, y }。显式视口 context 首选的 web 路径,通常也适用于一般的 web:
await emitWebJpeg(page);
移动 web 使用同样的路径;把 page 替换为 mobilePage:
await emitWebJpeg(mobilePage);
如果模型返回 { x, y },直接点击它:
await clickCssPoint({ surface: page, x, y });
移动 web 点击路径:
await tapCssPoint({ page: mobilePage, x, y });
对于这条正常路径中的 web clip 截图或元素截图,scale: "css" 通常直接可用。点击时加回区域原点。
await emitWebJpeg(page, { clip })await emitWebJpeg(mobilePage, { clip })await clickCssPoint({ surface: page, clip, x, y })await tapCssPoint({ page: mobilePage, clip, x, y })const box = await locator.boundingBox() 之后使用 await clickCssPoint({ surface: page, clip: box, x, y })当 scale: "css" 仍以设备像素尺寸返回时的 web 原生窗口后备方案:
var emitWebScreenshotCssScaled = async function ({ page, clip, quality = 0.85 } = {}) {
var NodeBuffer = (await import("node:buffer")).Buffer;
const target = clip
? { width: clip.width, height: clip.height }
: await page.evaluate(() => ({
width: window.innerWidth,
height: window.innerHeight,
}));
const screenshotBuffer = await page.screenshot({
type: "png",
...(clip ? { clip } : {}),
});
const bytes = await page.evaluate(
async ({ imageBase64, targetWidth, targetHeight, quality }) => {
const image = new Image();
image.src = `data:image/png;base64,${imageBase64}`;
await image.decode();
const canvas = document.createElement("canvas");
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.drawImage(image, 0, 0, targetWidth, targetHeight);
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, "image/jpeg", quality)
);
return new Uint8Array(await blob.arrayBuffer());
},
{
imageBase64: NodeBuffer.from(screenshotBuffer).toString("base64"),
targetWidth: target.width,
targetHeight: target.height,
quality,
}
);
await emitJpeg(bytes);
};
对整个视口的后备捕获,把返回的 { x, y } 当作直接的 CSS 坐标:
await emitWebScreenshotCssScaled({ page });
await clickCssPoint({ surface: page, x, y });
对裁剪的后备捕获,加回裁剪原点:
await emitWebScreenshotCssScaled({ page, clip });
await clickCssPoint({ surface: page, clip, x, y });
对于 Electron,在主进程中归一化,而不是打开一个临时的 Playwright 页面。下面的辅助函数返回整个内容区或某个裁剪 CSS 像素区域的 CSS 缩放字节。把 clip 当作内容区 CSS 像素,例如从渲染层 getBoundingClientRect() 取得的值。
var emitElectronScreenshotCssScaled = async function ({ electronApp, clip, quality = 85 } = {}) {
const bytes = await electronApp.evaluate(async ({ BrowserWindow }, { clip, quality }) => {
const win = BrowserWindow.getAllWindows()[0];
const image = clip ? await win.capturePage(clip) : await win.capturePage();
const target = clip
? { width: clip.width, height: clip.height }
: (() => {
const [width, height] = win.getContentSize();
return { width, height };
})();
const resized = image.resize({
width: target.width,
height: target.height,
quality: "best",
});
return resized.toJPEG(quality);
}, { clip, quality });
await emitJpeg(bytes);
};
完整 Electron 窗口:
await emitElectronScreenshotCssScaled({ electronApp });
await clickCssPoint({ surface: appWindow, x, y });
用渲染层 CSS 像素裁剪的 Electron 区域:
var clip = await appWindow.evaluate(() => {
const rect = document.getElementById("board").getBoundingClientRect();
return {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
};
});
await emitElectronScreenshotCssScaled({ electronApp, clip });
await clickCssPoint({ surface: appWindow, clip, x, y });
仅当原始像素比 CSS 坐标对齐更重要时使用这些,例如 Retina 或 DPI 伪影调试、像素精确的渲染检查,或其他对保真度敏感的审阅。
Web 桌面原始发出:
await codex.emitImage({
bytes: await page.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
Electron 原始发出:
await codex.emitImage({
bytes: await appWindow.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
移动 web context 已在运行后的移动原始发出:
await codex.emitImage({
bytes: await mobilePage.screenshot({ type: "jpeg", quality: 85 }),
mimeType: "image/jpeg",
detail: "original",
});
不要仅因为主控件可见就假定截图可接受。在签收前,用截图审阅和数值检查两者,显式验证意图中的初始视图符合产品要求。
Web 或渲染层检查:
console.log(await page.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));
Electron 检查:
console.log(await appWindow.evaluate(() => ({
innerWidth: window.innerWidth,
innerHeight: window.innerHeight,
clientWidth: document.documentElement.clientWidth,
clientHeight: document.documentElement.clientHeight,
scrollWidth: document.documentElement.scrollWidth,
scrollHeight: document.documentElement.scrollHeight,
canScrollX: document.documentElement.scrollWidth > document.documentElement.clientWidth,
canScrollY: document.documentElement.scrollHeight > document.documentElement.clientHeight,
})));
当裁切是现实的失败模式时,在你的具体 UI 中用 getBoundingClientRect() 检查那些必需的可见区域,以增强数值检查;对固定外壳而言,仅有文档级指标是不够的。
对本地 web 调试,把应用保持运行在持久化的 TTY 会话中。不要依赖来自短命 shell 的一次性后台命令。
使用项目的正常启动命令,例如:
npm start
在 page.goto(...) 之前,验证所选端口正在监听且应用有响应。
对 Electron 调试,从 js_repl 通过 _electron.launch(...) 启动应用,这样同一个会话拥有该进程。如果 Electron 渲染层依赖单独的开发服务器(例如 Vite 或 Next),把该服务器保持运行在持久化 TTY 会话中,然后从 js_repl 重启或重载 Electron 应用。
仅在任务真正完成时才运行清理:
js_repl 会话都不会隐式运行 electronApp.close()、context.close() 或 browser.close()。if (electronApp) {
await electronApp.close().catch(() => {});
}
if (mobileContext) {
await mobileContext.close().catch(() => {});
}
if (context) {
await context.close().catch(() => {});
}
if (browser) {
await browser.close().catch(() => {});
}
browser = undefined;
context = undefined;
page = undefined;
mobileContext = undefined;
mobilePage = undefined;
electronApp = undefined;
appWindow = undefined;
console.log("Playwright session closed");
如果你打算在调试后立即退出 Codex,先运行清理 cell 并等到 "Playwright session closed" 日志出现后再退出。
Cannot find module 'playwright':在当前工作区运行一次性设置,并在使用 js_repl 之前验证 import。npx playwright install chromium。page.goto: net::ERR_CONNECTION_REFUSED:确保开发服务器仍运行在持久化 TTY 会话中,重新检查端口,并优先使用 http://127.0.0.1:<port>。electron.launch 挂起、超时或立即退出:验证本地 electron 依赖,确认 args 目标,并确保任何渲染层开发服务器在启动前已运行。Identifier has already been declared:复用已有的顶层绑定、选择一个新名字,或把代码包在 { ... } 里。仅当内核真正卡住时才用 js_repl_reset。browserContext.newPage: Protocol error (Target.createTarget): Not supported:不要用 appWindow.context().newPage() 或 electronApp.context().newPage() 作为临时页面;使用绑定模型截图一节中特定于 Electron 的截图归一化流程。js_repl 超时或重置:重新运行引导 cell,用更短、更聚焦的 cell 重建会话。--sandbox danger-full-access 启动的,并在需要时以该方式重启。