fix(web-probe): add mdtodo visual sentinel coverage

This commit is contained in:
Codex
2026-06-27 02:03:27 +00:00
parent 4b6f0c745f
commit ac5864a05d
12 changed files with 296 additions and 10 deletions
+3 -1
View File
@@ -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。
+1
View File
@@ -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)。
+1
View File
@@ -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 <route> 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 与节点边界
+3
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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<string, unknown>, 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;
+49 -9
View File
@@ -419,17 +419,20 @@ function checkSqlite(db: Database): Record<string, unknown> {
function buildObserveCommandPlan(config: WebProbeSentinelServiceConfig, scenario: Record<string, unknown>): 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<string, unknown>, 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<string, unknown> {
return {
enabledScenarios: config.scenarios.filter((item) => boolAt(item, "enabled")).map((item) => stringAt(item, "id")),