diff --git a/.agents/skills/unidesk-webdev/SKILL.md b/.agents/skills/unidesk-webdev/SKILL.md index 5a848302..bb6c59ed 100644 --- a/.agents/skills/unidesk-webdev/SKILL.md +++ b/.agents/skills/unidesk-webdev/SKILL.md @@ -129,6 +129,7 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web - 再用 `web-probe screenshot` 从 owning YAML 选择的固定公网入口打开该 session 深链,观察用户可见的消息、终态和语义化错误; - 不把 WebProbe 的硬编码 DOM selector、长等待、临时 Playwright 脚本或探针自身超时作为 Workbench 业务终态的唯一门禁;探针异常时保留其作为工具证据,并以 CLI 终态和截图继续归因; - CLI 必须走同一产品 dispatcher 和身份语义,禁止以 direct manager、snapshot、result polling 或其他第二业务路径替代。 + - 一次性截图默认按 `domcontentloaded` 导航,持续 SSE 页面不得改用 `networkidle`;完整边界见 [references/web-probe.md](references/web-probe.md)。 - Native 阶段采用短反馈循环: - 修改后先执行最小语法检查和目标功能验证; - 使用 `web-probe` 从 YAML 选择的固定 HTTPS 或公网 HTTP 入口验证真实 DOM、交互、布局和截图;custom/local 仅记录为 preflight,不能宣称 L1 通过; diff --git a/.agents/skills/unidesk-webdev/references/web-probe.md b/.agents/skills/unidesk-webdev/references/web-probe.md index 3fcc2417..b6c19d9f 100644 --- a/.agents/skills/unidesk-webdev/references/web-probe.md +++ b/.agents/skills/unidesk-webdev/references/web-probe.md @@ -4,6 +4,10 @@ - 交互流程使用 `observe start`、`observe command`、`observe collect` 和 `observe analyze`; - `run` 或 `script` 只用于有界探测和 smoke; - 收尾证据引用 observer id、command id、artifact/report SHA、screenshot SHA 和关键有界字段。 +- 一次性截图的导航完成条件: + - `web-probe screenshot` 默认使用 `domcontentloaded`,适用于持续 SSE、WebSocket 和长轮询页面; + - 只有页面能够稳定进入网络静默时,才显式传 `--wait-until networkidle`; + - 需要业务 DOM 或交互判定时继续使用 typed readiness 或 observer,不能把导航完成当作业务通过。 - L1 native Web 公网 readiness: - 使用 `bun scripts/cli.ts web-probe native-readiness --node --lane --profile `; - 固定 HTTPS origin 来自 lane 的 `nativeDevelopment.publicExposure`,页面与判据来自 `nativeReadinessProfiles`; diff --git a/scripts/src/hwlab-node-help.ts b/scripts/src/hwlab-node-help.ts index 520542eb..1e5d7366 100644 --- a/scripts/src/hwlab-node-help.ts +++ b/scripts/src/hwlab-node-help.ts @@ -182,12 +182,14 @@ export function hwlabNodeWebProbeScreenshotHelp(): Record { "--path": "附加到 semantic origin 的绝对路径;与 --url 互斥。", "--url": "custom/local 一次性 URL,必须包含完整路径;与 --origin、--path 互斥。", "--viewport": "截图视口,默认 1440x900。", + "--wait-until": "导航完成条件,默认 domcontentloaded;静态页面可显式选择 networkidle。", "--selector": "等待指定 selector 后再截图。", "--full-page|--no-full-page": "选择整页或当前视口截图。", }, notes: [ "使用 --url 时不要再传 --path;custom/local URL 已包含完整路径。", "internal/public/native 运行面必须通过 owning YAML 的 --origin 选择,禁止用手写 URL 或 IP 替代。", + "持续 SSE、WebSocket 或长轮询页面使用默认 domcontentloaded;networkidle 只用于能够稳定进入网络静默的页面。", "该命令只采集证据,不修改浏览器能力、资源门禁或运行面。", ], mutation: false, diff --git a/scripts/src/hwlab-node/web-probe-help.test.ts b/scripts/src/hwlab-node/web-probe-help.test.ts index 3ce558da..693f45c1 100644 --- a/scripts/src/hwlab-node/web-probe-help.test.ts +++ b/scripts/src/hwlab-node/web-probe-help.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { test } from "bun:test"; import { hwlabNodeWebProbeHelp, hwlabNodeWebProbeScreenshotHelp } from "../hwlab-node-help"; +import { parseNodeWebProbeOptions } from "./web-probe"; test("web-probe help promotes reusable Workbench debug and product Trace commands", () => { const help = hwlabNodeWebProbeHelp(); @@ -21,5 +22,18 @@ test("web-probe screenshot scoped help explains semantic, custom and local URL u assert.match(usage, /--url 'https:\/\/custom\.example\/workbench'/u); assert.match(usage, /--url 'http:\/\/127\.0\.0\.1:4173\/workbench'/u); assert.match(notes, /--url.*不要再传 --path/u); + assert.match(notes, /持续 SSE.*domcontentloaded/u); assert.equal(help.mutation, false); }); + +test("web-probe screenshot defaults to DOM readiness and keeps network idle explicit", () => { + const defaults = parseNodeWebProbeOptions(["screenshot", "--node", "NC01", "--lane", "v03", "--origin", "native"]); + assert.equal(defaults.action, "screenshot"); + if (defaults.action !== "screenshot") throw new Error("expected screenshot options"); + assert.equal(defaults.waitUntil, "domcontentloaded"); + + const explicit = parseNodeWebProbeOptions(["screenshot", "--node", "NC01", "--lane", "v03", "--origin", "native", "--wait-until", "networkidle"]); + assert.equal(explicit.action, "screenshot"); + if (explicit.action !== "screenshot") throw new Error("expected screenshot options"); + assert.equal(explicit.waitUntil, "networkidle"); +}); diff --git a/scripts/src/hwlab-node/web-probe-observe.ts b/scripts/src/hwlab-node/web-probe-observe.ts index 512fd574..caa5cb50 100644 --- a/scripts/src/hwlab-node/web-probe-observe.ts +++ b/scripts/src/hwlab-node/web-probe-observe.ts @@ -1105,7 +1105,7 @@ const screenshotPath = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_PATH; const width = Number(process.env.UNIDESK_WEB_PROBE_SCREENSHOT_WIDTH || 1440); const height = Number(process.env.UNIDESK_WEB_PROBE_SCREENSHOT_HEIGHT || 900); const timeout = Number(process.env.UNIDESK_WEB_PROBE_SCREENSHOT_TIMEOUT_MS || 30000); -const waitUntil = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_WAIT_UNTIL || "networkidle"; +const waitUntil = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_WAIT_UNTIL || "domcontentloaded"; const fullPage = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_FULL_PAGE === "1"; const selector = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_SELECTOR || ""; const executablePath = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_EXECUTABLE_PATH || ""; diff --git a/scripts/src/hwlab-node/web-probe.ts b/scripts/src/hwlab-node/web-probe.ts index 18f6dcd0..24df2156 100644 --- a/scripts/src/hwlab-node/web-probe.ts +++ b/scripts/src/hwlab-node/web-probe.ts @@ -2296,7 +2296,7 @@ export function parseNodeWebProbeOptions(args: string[]): NodeWebProbeOptions { localDir: optionValue(args, "--local-dir") ?? "/tmp", name: parseWebProbeScreenshotName(optionValue(args, "--name") ?? `web-probe-${node.toLowerCase()}-${lane}.png`), timeoutMs, - waitUntil: parseWebProbeScreenshotWaitUntil(optionValue(args, "--wait-until") ?? "networkidle"), + waitUntil: parseWebProbeScreenshotWaitUntil(optionValue(args, "--wait-until") ?? "domcontentloaded"), fullPage: args.includes("--full-page") && !args.includes("--no-full-page"), selector: optionValue(args, "--selector") ?? null, keepRemote: args.includes("--keep-remote"),