with one click
playwright-interactive
通过 `js_repl` 进行持久化的浏览器与 Electron 交互,用于快速迭代式的 UI 调试。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
通过 `js_repl` 进行持久化的浏览器与 Electron 交互,用于快速迭代式的 UI 调试。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
审计工作、用具体证据核验声明,并用五年级生也能懂的简单语言解释结果。当用户要求 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 启动的,并在需要时以该方式重启。