fix(web-probe): add mdtodo visual sentinel coverage
This commit is contained in:
@@ -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。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
@@ -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 与节点边界
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")),
|
||||
|
||||
Reference in New Issue
Block a user