diff --git a/.agents/skills/unidesk-monitor/SKILL.md b/.agents/skills/unidesk-monitor/SKILL.md index 6c27780a..944ae962 100644 --- a/.agents/skills/unidesk-monitor/SKILL.md +++ b/.agents/skills/unidesk-monitor/SKILL.md @@ -1,12 +1,14 @@ --- name: unidesk-monitor -description: UniDesk monitoring and Web sentinel operations. Use when working on monitor.pikapython.com, HWLAB Web哨兵, web-probe sentinel status/report/dashboard, Prometheus/OTel monitoring, multi-sentinel runtime visibility, or monitoring-related issue triage and rollout evidence. +description: UniDesk monitoring and Web sentinel operations. Use when working on monitor.pikapython.com, HWLAB Web哨兵, web-probe sentinel status/report/dashboard, 定期巡检/周期巡检/哨兵巡检, 新建或调整 Web sentinel YAML, monitor/minitor requests, Prometheus/OTel monitoring, multi-sentinel runtime visibility, or monitoring-related issue triage and rollout evidence. --- # UniDesk Monitor 本技能是 UniDesk 监控与 Web 哨兵操作面的入口。它不替代 `$unidesk-webdev`、`$unidesk-cicd`、`$unidesk-ymalops`、`$unidesk-gh` 或 `$unidesk-otel`;遇到对应工作时同时加载那些技能。 +当用户提到 Web 哨兵、`web-probe sentinel`、`monitor.pikapython.com`、定期/周期巡检、新建巡检、巡检 dashboard/report/status,或误写为 `minitor` 时,必须加载本技能。 + ## Boundaries - Web 哨兵只 wrap 现有 `web-probe observe start/status/command/collect/analyze`,不得新增第二套 Playwright runner、采样器、报告器或 analyzer。 diff --git a/.agents/skills/unidesk-webdev/SKILL.md b/.agents/skills/unidesk-webdev/SKILL.md index 6b907b44..76ed4a2a 100644 --- a/.agents/skills/unidesk-webdev/SKILL.md +++ b/.agents/skills/unidesk-webdev/SKILL.md @@ -10,6 +10,7 @@ description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/H ## 快速规则 - 线上 Web bug、Workbench、Performance、前端状态投影、截图、fake-server 和 web-probe 任务必须先用本技能。 +- 涉及 Web 哨兵、`web-probe sentinel`、`monitor.pikapython.com`、定期/周期巡检或新建巡检时,必须同时加载 `$unidesk-monitor`。 - 真实用户入口验证优先;源码检查、构建通过或截图局部正常不能替代原入口验收。 - 禁止在本地或 master server 直接跑 `vue-tsc` / 前端全量 typecheck 作为默认验证;本地只做语法级检查和真实入口复测,完整类型检查交给 CI、PipelineRun 或明确指定的受控构建运行面。 - Web probe、Playwright、fake-server 的详细命令和历史判定口径见 [references/full.md](references/full.md)。 diff --git a/AGENTS.md b/AGENTS.md index 32c3d76d..1092e66b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台。本文 - P0: GitHub issue/PR 正式写入必须走 `$unidesk-gh` / `bun scripts/cli.ts gh ...`,禁止原生 `gh` 或手写 GitHub API 绕过;正文、评论和 closeout 默认中文。 - P0: 远端文本修改优先走 `$unidesk-trans` 的 `trans apply-patch`;route 定位和容器 cwd 规则见 `docs/reference/cli.md`。 - P0: Web、Workbench、Playwright/web-probe、前端状态投影和线上 Web bug 复测使用 `$unidesk-webdev`;OTel/Tempo/trace 追踪使用 `$unidesk-otel`。 +- P0: Web 哨兵、`web-probe sentinel`、`monitor.pikapython.com`、定期/周期巡检和新建巡检使用 `$unidesk-monitor`,涉及页面复现或截图时同时使用 `$unidesk-webdev`。 ## P0: HWLAB、AgentRun 与节点边界 diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 747e50a2..07a1f830 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -272,6 +272,9 @@ lanes: - id: workbench-auth-session-switch-2users enabled: true configRef: config/hwlab-web-probe-sentinels/d601-v03/workbench-auth-session-switch-2users.yaml#sentinel + - id: mdtodo-visual-regression + enabled: true + configRef: config/hwlab-web-probe-sentinels/d601-v03/mdtodo-visual-regression.yaml#sentinel workbench: enabled: true summaryPath: /v1/web-performance/summary diff --git a/config/hwlab-web-probe-sentinel/cicd.mdtodo.d601-v03.yaml b/config/hwlab-web-probe-sentinel/cicd.mdtodo.d601-v03.yaml new file mode 100644 index 00000000..793d78df --- /dev/null +++ b/config/hwlab-web-probe-sentinel/cicd.mdtodo.d601-v03.yaml @@ -0,0 +1,51 @@ +version: 1 +kind: HwlabWebProbeSentinelCicd +metadata: + id: d601-v03-web-probe-sentinel-mdtodo-cicd + owner: UniDesk + specRef: PJ2026-01060508 +sentinel: + cicd: + controlPlaneConfigRef: config/hwlab-node-control-plane.yaml#targets[0] + source: + repository: pikasTech/unidesk + branch: master + gitSshUrl: ssh://git@ssh.github.com:443/pikasTech/unidesk.git + gitMirrorReadUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/unidesk.git + buildContext: . + entrypoint: scripts/web-probe-sentinel-service.ts + checkoutPaths: + - scripts + - config + - package.json + - bun.lock + - bun.lockb + builder: + namespace: devops-infra + sourceMode: sparse-git-checkout + jobPrefix: web-probe-sentinel-mdtodo-publish + gitSshSecretName: git-mirror-github-ssh + dockerSocketPath: /var/run/docker.sock + activeDeadlineSeconds: 900 + ttlSecondsAfterFinished: 3600 + gitopsPath: deploy/gitops/node/d601/web-probe-sentinel-mdtodo + argo: + namespace: argocd + projectName: hwlab-d601 + applicationName: hwlab-web-probe-sentinel-mdtodo + repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git + targetRevision: v0.3-gitops + image: + repository: 127.0.0.1:5000/hwlab/web-probe-sentinel-mdtodo + tagSource: source-commit + baseImageRef: config/hwlab-node-control-plane.yaml#targets[0].tekton.toolsImage.output + envRecipeRef: config/hwlab-web-probe-sentinel/runtime.mdtodo.d601-v03.yaml#sentinel.runtime + maintenance: + startCommand: sentinel maintenance start + stopCommand: sentinel maintenance stop + confirmWait: + maxSeconds: 120 + targetValidation: + scenarioId: mdtodo-visual-regression + maxSeconds: 360 + serviceUnavailablePolicy: structured-failure diff --git a/config/hwlab-web-probe-sentinel/public-exposure.mdtodo.d601-v03.yaml b/config/hwlab-web-probe-sentinel/public-exposure.mdtodo.d601-v03.yaml new file mode 100644 index 00000000..4df80c31 --- /dev/null +++ b/config/hwlab-web-probe-sentinel/public-exposure.mdtodo.d601-v03.yaml @@ -0,0 +1,37 @@ +version: 1 +kind: HwlabWebProbeSentinelPublicExposure +metadata: + id: d601-v03-web-probe-sentinel-mdtodo-public-exposure + owner: UniDesk + specRef: PJ2026-01060508 +sentinel: + publicExposure: + enabled: true + mode: pk01-caddy-frp-path + publicBaseUrl: https://monitor.pikapython.com/sentinels/mdtodo-visual-regression + hostname: monitor.pikapython.com + routePrefix: /sentinels/mdtodo-visual-regression + expectedA: 82.156.23.220 + frpc: + deploymentName: hwlab-web-probe-sentinel-mdtodo-frpc + image: 127.0.0.1:5000/hwlab/frpc:v0.68.1 + serverAddr: 82.156.23.220 + serverPort: 22000 + tokenSourceRef: platform-infra/pk01-frp.env + tokenSourceKey: FRP_TOKEN + secretName: hwlab-web-probe-sentinel-mdtodo-frpc + secretKey: frpc.toml + tokenKey: token + httpProxy: + name: hwlab-d601-v03-web-probe-sentinel-mdtodo + remotePort: 22092 + localIP: hwlab-web-probe-sentinel-mdtodo.hwlab-v03.svc.cluster.local + localPort: 8080 + caddy: + route: PK01 + configPath: /etc/caddy/Caddyfile + serviceName: caddy + email: ops@pikapython.com + tls: auto + responseHeaderTimeoutSeconds: 600 + managedBlockOwner: hwlab-web-probe-sentinel-mdtodo-d601-v03 diff --git a/config/hwlab-web-probe-sentinel/runtime.mdtodo.d601-v03.yaml b/config/hwlab-web-probe-sentinel/runtime.mdtodo.d601-v03.yaml new file mode 100644 index 00000000..1de9c5d9 --- /dev/null +++ b/config/hwlab-web-probe-sentinel/runtime.mdtodo.d601-v03.yaml @@ -0,0 +1,33 @@ +version: 1 +kind: HwlabWebProbeSentinelRuntime +metadata: + id: d601-v03-web-probe-sentinel-mdtodo-runtime + owner: UniDesk + specRef: PJ2026-01060508 +sentinel: + runtime: + target: + node: D601 + lane: v03 + publicOriginRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.D601.public.webUrl + observeWrapperRef: config/hwlab-node-lanes.yaml#lanes.v03.targets.D601.observability.webProbe.sentinels[2] + namespace: hwlab-v03 + serviceAccountName: hwlab-web-probe-sentinel-mdtodo + deploymentName: hwlab-web-probe-sentinel-mdtodo + serviceName: hwlab-web-probe-sentinel-mdtodo + listenHost: 0.0.0.0 + servicePort: 8080 + pvcName: hwlab-web-probe-sentinel-mdtodo-state + pvcStorage: 10Gi + stateRoot: /var/lib/web-probe-sentinel-mdtodo + imageRef: 127.0.0.1:5000/hwlab/web-probe-sentinel-mdtodo:source-commit + replicas: 1 + healthPath: /api/health + metricsPath: /metrics + scheduler: + intervalMs: 900000 + heartbeatStaleSeconds: 900 + maxConcurrentRuns: 1 + sqlite: + path: /var/lib/web-probe-sentinel-mdtodo/index.sqlite + busyTimeoutMs: 2000 diff --git a/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml b/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml new file mode 100644 index 00000000..226ed2ea --- /dev/null +++ b/config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml @@ -0,0 +1,35 @@ +version: 1 +kind: HwlabWebProbeSentinelScenarios +metadata: + id: d601-v03-web-probe-sentinel-mdtodo-scenarios + owner: UniDesk + specRef: PJ2026-01060508 +sentinel: + scenarios: + - id: mdtodo-visual-regression + enabled: true + cadence: 15m + observeTargetPath: /projects/mdtodo + viewport: 390x844 + sampleIntervalMs: 1000 + screenshotIntervalMs: 60000 + maxRunSeconds: 360 + providerProfile: dsflash-go + providerProfileMode: exact + promptSetRef: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet + reportViewRef: config/hwlab-web-probe-sentinel/report-views.yaml#sentinel.reportViews + commandSequence: + - type: gotoProjectMdtodo + - type: selectMdtodoFile + filename: 20260609_频率判断_用户反馈.md + - type: screenshot + label: mdtodo-mobile-selected + - type: openMdtodoReportPreview + task: R1 + link: R1 + - type: screenshot + label: mdtodo-report-preview + - type: toggleMdtodoReportFullscreen + text: toggle + - type: screenshot + label: mdtodo-report-fullscreen diff --git a/config/hwlab-web-probe-sentinel/secrets.mdtodo.d601-v03.yaml b/config/hwlab-web-probe-sentinel/secrets.mdtodo.d601-v03.yaml new file mode 100644 index 00000000..9827e485 --- /dev/null +++ b/config/hwlab-web-probe-sentinel/secrets.mdtodo.d601-v03.yaml @@ -0,0 +1,26 @@ +version: 1 +kind: HwlabWebProbeSentinelSecrets +metadata: + id: d601-v03-web-probe-sentinel-mdtodo-secrets + owner: UniDesk + specRef: PJ2026-01060508 +sentinel: + secrets: + sources: + - purpose: bootstrap-admin + sourceRef: hwlab/d601-v03-bootstrap-admin.env + sourceKey: HWLAB_BOOTSTRAP_ADMIN_PASSWORD + - purpose: frp-token + sourceRef: platform-infra/pk01-frp.env + sourceKey: FRP_TOKEN + runtimeSecrets: + - name: hwlab-web-probe-sentinel-mdtodo-bootstrap + namespace: hwlab-v03 + data: + - sourcePurpose: bootstrap-admin + targetKey: bootstrap-admin-password + - name: hwlab-web-probe-sentinel-mdtodo-frpc + namespace: hwlab-v03 + data: + - sourcePurpose: frp-token + targetKey: token diff --git a/config/hwlab-web-probe-sentinels/d601-v03/mdtodo-visual-regression.yaml b/config/hwlab-web-probe-sentinels/d601-v03/mdtodo-visual-regression.yaml new file mode 100644 index 00000000..145da538 --- /dev/null +++ b/config/hwlab-web-probe-sentinels/d601-v03/mdtodo-visual-regression.yaml @@ -0,0 +1,18 @@ +version: 1 +kind: HwlabWebProbeSentinel +metadata: + id: d601-v03-mdtodo-visual-regression + owner: UniDesk + specRef: PJ2026-01060508 +sentinel: + id: mdtodo-visual-regression + enabled: true + mode: web-probe-observe-wrapper + configRefs: + runtime: config/hwlab-web-probe-sentinel/runtime.mdtodo.d601-v03.yaml#sentinel.runtime + scenarios: config/hwlab-web-probe-sentinel/scenarios.mdtodo.yaml#sentinel.scenarios + promptSet: config/hwlab-web-probe-sentinel/prompt-set.dsflash-go.yaml#sentinel.promptSet + reportViews: config/hwlab-web-probe-sentinel/report-views.yaml#sentinel.reportViews + publicExposure: config/hwlab-web-probe-sentinel/public-exposure.mdtodo.d601-v03.yaml#sentinel.publicExposure + cicd: config/hwlab-web-probe-sentinel/cicd.mdtodo.d601-v03.yaml#sentinel.cicd + secrets: config/hwlab-web-probe-sentinel/secrets.mdtodo.d601-v03.yaml#sentinel.secrets diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 2121d45a..a7df8861 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -2217,6 +2217,8 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou "--screenshot-interval-ms", String(numberAt(scenario, "screenshotIntervalMs")), "--command-timeout-seconds", "55", ]; + const viewport = stringAtNullable(scenario, "viewport"); + if (viewport !== null) startArgs.push("--viewport", viewport); const started = runChildCli(startArgs, remainingSeconds(deadline, 55), undefined, accountEnv.env); steps.push({ phase: "observe-start", ok: started.ok, result: started.result }); const observerId = observerIdFromText(String(record(started.result).stdoutPreview ?? "")); @@ -2291,6 +2293,7 @@ function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeou args.push("--text", prompts.prompts[promptIndex % prompts.prompts.length] ?? ""); promptIndex += 1; } + appendScenarioObserveCommandArgs(args, item, { skipText: type === "sendPrompt" }); const commandResult = runChildCli(args, remainingSeconds(deadline, 60)); steps.push({ phase: `observe-command-${type}`, ok: commandResult.ok, promptIndex: type === "sendPrompt" ? promptIndex : null, result: commandResult.result }); if (!commandResult.ok) { @@ -2452,6 +2455,42 @@ function runQuickVerifySessionInvarianceChecks( return { ok: true, promptIndex, checkCount: checks.length, warnings, valuesRedacted: true }; } +function appendScenarioObserveCommandArgs(args: string[], item: Record, options: { readonly skipText?: boolean } = {}): void { + const mappings: readonly (readonly [string, string])[] = [ + ["path", "--path"], + ["label", "--label"], + ["sessionId", "--session-id"], + ["provider", "--provider"], + ["accountId", "--account-id"], + ["fromAccountId", "--from-account-id"], + ["toAccountId", "--to-account-id"], + ["sourceId", "--source-id"], + ["fileRef", "--file-ref"], + ["filename", "--filename"], + ["taskRef", "--task-ref"], + ["taskId", "--task-id"], + ["task", "--task"], + ["field", "--field"], + ["link", "--link"], + ["title", "--title"], + ["body", "--body"], + ["status", "--status"], + ["hwpodId", "--hwpod-id"], + ["nodeId", "--node-id"], + ["workspaceRoot", "--workspace-root"], + ["root", "--root"], + ]; + for (const [key, flag] of mappings) { + if (args.includes(flag)) continue; + const value = stringAtNullable(item, key); + if (value !== null) args.push(flag, value); + } + if (options.skipText !== true && !args.includes("--text")) { + const text = stringAtNullable(item, "text") ?? stringAtNullable(item, "value"); + if (text !== null) args.push("--text", text); + } +} + function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { readonly runId: string; readonly scenarioId: string; diff --git a/scripts/src/hwlab-node-web-sentinel-service.ts b/scripts/src/hwlab-node-web-sentinel-service.ts index 1c4b5c7f..7880da91 100644 --- a/scripts/src/hwlab-node-web-sentinel-service.ts +++ b/scripts/src/hwlab-node-web-sentinel-service.ts @@ -419,17 +419,20 @@ function checkSqlite(db: Database): Record { function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario: Record): readonly CommandPlanStep[] { const targetPath = stringAt(scenario, "observeTargetPath"); + const startArgv = [ + "bun", "scripts/cli.ts", "web-probe", "observe", "start", + "--node", config.node, + "--lane", config.lane, + "--target-path", targetPath, + "--sample-interval-ms", String(numberAt(scenario, "sampleIntervalMs")), + "--screenshot-interval-ms", String(numberAt(scenario, "screenshotIntervalMs")), + "--command-timeout-seconds", "55", + ]; + const viewport = stringOrNull(scenario.viewport); + if (viewport !== null) startArgv.push("--viewport", viewport); const start: CommandPlanStep = { phase: "observe-start", - argv: [ - "bun", "scripts/cli.ts", "web-probe", "observe", "start", - "--node", config.node, - "--lane", config.lane, - "--target-path", targetPath, - "--sample-interval-ms", String(numberAt(scenario, "sampleIntervalMs")), - "--screenshot-interval-ms", String(numberAt(scenario, "screenshotIntervalMs")), - "--command-timeout-seconds", "55", - ], + argv: startArgv, stdinSource: "none", }; const commands = arrayAt(scenario, "commandSequence").map((item) => { @@ -447,6 +450,7 @@ function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario if (fromAccountId !== null) argv.push("--from-account-id", fromAccountId); if (toAccountId !== null) argv.push("--to-account-id", toAccountId); } + appendObserveCommandArgs(argv, item, { skipText: type === "sendPrompt" }); return { phase: `observe-command-${type}`, argv, stdinSource: type === "sendPrompt" ? "prompt-source" : "none" } satisfies CommandPlanStep; }); const analyze: CommandPlanStep = { @@ -457,6 +461,42 @@ function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario return [start, ...commands, analyze]; } +function appendObserveCommandArgs(argv: string[], item: Record, options: { readonly skipText?: boolean } = {}): void { + const mappings: readonly (readonly [string, string])[] = [ + ["path", "--path"], + ["label", "--label"], + ["sessionId", "--session-id"], + ["provider", "--provider"], + ["accountId", "--account-id"], + ["fromAccountId", "--from-account-id"], + ["toAccountId", "--to-account-id"], + ["sourceId", "--source-id"], + ["fileRef", "--file-ref"], + ["filename", "--filename"], + ["taskRef", "--task-ref"], + ["taskId", "--task-id"], + ["task", "--task"], + ["field", "--field"], + ["link", "--link"], + ["title", "--title"], + ["body", "--body"], + ["status", "--status"], + ["hwpodId", "--hwpod-id"], + ["nodeId", "--node-id"], + ["workspaceRoot", "--workspace-root"], + ["root", "--root"], + ]; + for (const [key, flag] of mappings) { + if (argv.includes(flag)) continue; + const value = stringOrNull(item[key]); + if (value !== null) argv.push(flag, value); + } + if (options.skipText !== true) { + const text = stringOrNull(item.text) ?? stringOrNull(item.value); + if (text !== null) argv.push("--text", text); + } +} + function schedulerSummary(config: WebProbeSentinelServiceConfig, db: Database): Record { return { enabledScenarios: config.scenarios.filter((item) => boolAt(item, "enabled")).map((item) => stringAt(item, "id")),