From 31106a2ee0f844b476bfbe8f0a00b1e03809da5e Mon Sep 17 00:00:00 2001 From: pikastech Date: Sat, 18 Jul 2026 05:36:52 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=90=88=E5=B9=B6=E5=B9=B6=E8=A1=8C?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=BA=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/skills/unidesk-cicd/SKILL.md | 12 +- .../references/incident-recovery.md | 106 ++++++++++++++++++ .../unidesk-cicd/references/platform-ops.md | 43 +++++-- .agents/skills/unidesk-devlevel/SKILL.md | 5 + .agents/skills/unidesk-oa/SKILL.md | 12 +- .../references/troubleshooting-accounts.md | 1 + .agents/skills/unidesk-webdev/SKILL.md | 21 ++-- .../unidesk-webdev/references/design.md | 14 ++- .../unidesk-webdev/references/web-probe.md | 1 + AGENTS.md | 5 + config/hwlab-node-lanes.yaml | 104 +++++++++++++++++ config/platform-infra/sub2api-codex-pool.yaml | 2 +- docs/MDTODO/agentrun-runtime-reliability.md | 3 + .../details/hwlab-caserun/R4.7_Task_Report.md | 29 +++++ .../R6.10.2_Task_Report.md | 22 ++++ .../R6.6_Task_Report.md | 63 +++++++++++ .../R6.7_Task_Report.md | 38 +++++++ .../R6.8_Task_Report.md | 52 +++++++++ .../R6.9_Task_Report.md | 54 +++++++++ .../R1.1_Task_Report.md | 76 +++++++++++++ .../R2.12.1_Task_Report.md | 25 +++++ .../R2.18.2_Task_Report.md | 50 +++++++++ docs/MDTODO/hwlab-caserun.md | 4 + docs/MDTODO/pikaoa-enterprise-platform.md | 26 ++++- docs/MDTODO/platform-resource-governance.md | 4 + .../pr-merge-driven-automatic-delivery.md | 3 + docs/MDTODO/sub2api-upstream-reliability.md | 10 ++ .../PJ2026-01/specs/PJ2026-0103-harness-rl.md | 14 ++- scripts/src/hwlab-native-development.ts | 55 ++++++--- scripts/src/hwlab-node-help.ts | 3 +- scripts/src/hwlab-node-lanes.ts | 24 ++++ ...ab-node-web-probe-native-readiness.test.ts | 14 ++- .../hwlab-node-web-probe-native-readiness.ts | 53 ++++++++- scripts/src/hwlab-node/web-probe-observe.ts | 19 ++++ scripts/src/hwlab-node/web-probe-origin.ts | 15 ++- 35 files changed, 929 insertions(+), 53 deletions(-) create mode 100644 docs/MDTODO/details/hwlab-caserun/R4.7_Task_Report.md create mode 100644 docs/MDTODO/details/pikaoa-enterprise-platform/R6.10.2_Task_Report.md create mode 100644 docs/MDTODO/details/pikaoa-enterprise-platform/R6.6_Task_Report.md create mode 100644 docs/MDTODO/details/pikaoa-enterprise-platform/R6.7_Task_Report.md create mode 100644 docs/MDTODO/details/pikaoa-enterprise-platform/R6.8_Task_Report.md create mode 100644 docs/MDTODO/details/pikaoa-enterprise-platform/R6.9_Task_Report.md create mode 100644 docs/MDTODO/details/platform-resource-governance/R1.1_Task_Report.md create mode 100644 docs/MDTODO/details/sub2api-upstream-reliability/R2.12.1_Task_Report.md create mode 100644 docs/MDTODO/details/sub2api-upstream-reliability/R2.18.2_Task_Report.md diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index 88a4322d..1e0915bb 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -3,9 +3,11 @@ name: unidesk-cicd description: >- UniDesk CI/CD 控制面,覆盖 PaC consumer 首发 bootstrap、Tekton/Argo、GitOps、 git-mirror、PR 自动交付、Secret、observability、CI tools image、PipelineRun 清理、 - AgentRun 与 HWLAB 部署,以及 branch-follower 退役只读诊断。 + Tekton 大对象与 Kine/SQLite 控制面退化、AgentRun 与 HWLAB 部署, + 以及 branch-follower 退役只读诊断。 用户提到 CI/CD、deploy、rollout、PipelineRun、PaC、bootstrap、GitOps、Tekton、 git-mirror、control-plane、k8s/k3s 部署、branch follower、agentrun、hwlab g14、 + PipelineRun 对象膨胀、Slow SQL、NodeNotReady、CI/CD 快速恢复、 monitor-prs 或 trigger-current 时使用。 任何需要把代码变更推送部署到 G14 k3s 的操作都必须走本 skill。 --- @@ -67,6 +69,12 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - 首次失败需要 TaskRun 或日志证据时,只执行输出中的 `debug-first-failure` 精确下钻; - 命令固定 `mutation=false`、`automatic=false`,只提供 review-required 回退候选,绝不执行回退或生成补链动作。 +- Tekton 大对象与节点控制面退化: + - PipelineRun 重复内联大型 `taskSpec` 时,必须同时检查对象总大小、`spec`、`status.pipelineSpec`、Kine compaction、API timeout、node lease 和 kubelet 调和; + - PipelineRun 已成功且 GitOps commit 已生成、但 Argo 仍停在旧 revision 时,先按传播延迟只读观察,不得立即重启 k3s、人工 Argo sync 或补链; + - 只有节点/API/kubelet 已实际退化且 Pod 级调和无效时,才进入有界快速恢复;重启和终态对象清理只恢复控制面,不得作为最终修复证据; + - 根因判定、快速恢复、长期防复发和原入口验收统一见 [references/incident-recovery.md](references/incident-recovery.md)。 + - CI/CD 只读等待: - 默认使用 `closeout --wait --timeout 120s` 观察 Tekton、Argo 和 runtime 收敛;已处于成功或失败终态时立即返回。 - 未完成时由目标控制面的 Kubernetes watch 持有单次等待,不得在调用侧每 `1s`、`5s` 重复执行 `status`。 @@ -281,4 +289,4 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help - Secret、observability、CI tools image、PipelineRun/PV 清理:读 [references/platform-ops.md](references/platform-ops.md)。 - AgentRun v0.1 或 YAML-only lane 部署:读 [references/agentrun.md](references/agentrun.md)。 - 退役 branch-follower 的 `status|events|logs|debug-step` 历史只读诊断:读 [references/branch-follower.md](references/branch-follower.md)。 -- 生产事故、共同失败指纹、回退决策或公共服务面边界:读 [references/incident-recovery.md](references/incident-recovery.md)。 +- 生产事故、共同失败指纹、Tekton 大对象、Kine/SQLite 退化、快速恢复、回退决策或公共服务面边界:读 [references/incident-recovery.md](references/incident-recovery.md)。 diff --git a/.agents/skills/unidesk-cicd/references/incident-recovery.md b/.agents/skills/unidesk-cicd/references/incident-recovery.md index 1316d090..8afe6eef 100644 --- a/.agents/skills/unidesk-cicd/references/incident-recovery.md +++ b/.agents/skills/unidesk-cicd/references/incident-recovery.md @@ -93,6 +93,112 @@ - 不得通过人工 mirror sync、直接 Gitea push、人工 PipelineRun、Argo sync、bootstrap、apply 或运行面热补补齐验收。 - 回退事件恢复健康后,才允许从最新健康基线创建独立前向修复 PR。 +## Tekton 大对象与 Kine/SQLite 控制面退化 + +### 现象分层 + +- 先区分自动链传播延迟与节点控制面事故: + - PipelineRun 已成功并生成 GitOps commit、Argo 仍停在旧 revision, + 但 node、API、lease 和 runtime workload 健康时,按传播延迟处理; + - 只读记录 PipelineRun completion、GitOps commit、Argo reconcile/operation、 + Deployment rollout 和 runtime ready 时间,等待自动链自行收敛; + - host 无法解析 `*.svc.cluster.local` 只说明查询平面错误, + 不能证明集群内 git mirror 或 Argo repo-server 不可用; + - node 进入 `NotReady`、Service endpoint 消失,并同时出现 API handler timeout、 + node lease 延迟、kubelet housekeeping timeout 或持续 Kine Slow SQL 时, + 才按节点控制面事故处理。 +- 不得只凭 Pod Pending、Argo `Synced/Healthy` 或单条 Slow SQL 下结论: + - `Synced/Healthy` 必须同时核对实际 revision,避免把旧 revision 健康误判为新交付完成; + - 周期性 compaction 或孤立 Slow SQL 只记录为余压; + - 同窗口出现 lease、API、kubelet 和业务 endpoint 退化,才构成同类故障段。 + +### 根因判定 + +- 对疑似大型 PipelineRun,在目标 k8s 内计算短摘要: + - 对象总字节数; + - `spec` 字节数; + - `status.pipelineSpec` 字节数; + - Pipeline task、matrix TaskRun 和普通 TaskRun 数量; + - 最大字段及其 renderer/source 路径。 +- Tekton 会把解析后的 Pipeline spec 持久化到 `status.pipelineSpec`: + - renderer 为每个服务重复展开相同大型 inline `taskSpec.steps[].script` 时, + 对象会在 `spec` 与 `status` 双份放大; + - 运行期间的状态更新会重复写入大型对象,进一步放大 Kine/SQLite 单写者压力; + - TaskRun results 只有在字节证据成立时才可判为主因,禁止按字段名称猜测。 +- 将对象证据与同窗口控制面证据关联: + - Kine compaction 耗时和 Slow SQL; + - API handler timeout 与 stale resource-version; + - node/peer lease 写入延迟; + - kubelet housekeeping 和 pod lifecycle 延迟; + - k3s cgroup task、内存、swap 与数据库/WAL 压力。 +- 对象大小、耗时和留存阈值必须由 owning YAML 控制: + - 状态与 OTel 只输出 `blocking=false` warning; + - 禁止把可见性阈值升级成阻断 PipelineRun、GitOps、Argo 或业务 `/health` 的门禁。 + +### 快速恢复 + +按以下顺序恢复用户入口,禁止跳过取证后直接重启: + +1. 保存最小证据: + - 执行 `cicd status --node `; + - 对选中 consumer 执行 `pipelines-as-code status` 和 id-specific `history`; + - 记录 node condition、受影响 Service endpoint、异常 Pod owner、 + Kine/API/lease/kubelet 的有界日志计数; + - 所有 Kubernetes 大对象在目标侧计算大小和摘要,不把完整 JSON 拉回本机。 +2. 判断是否仍可由 Kubernetes 自行调和: + - API 与 kubelet 可用时,先等待 Deployment/StatefulSet controller 创建替代 Pod; + - 只处理已被驱逐、终态、卡在删除态且已不属于 Service endpoint 的旧 Pod; + - 删除前确认 owner、替代副本和 endpoint,禁止 broad selector、全 namespace 强删或触碰有状态单例。 +3. 只在节点控制面已退化且 Pod 级调和无效时重启 k3s: + - 该动作属于 `$unidesk-daddev` P2 紧急恢复,必须有用户授权; + - 单节点目标通过 host 受控 route 执行: + + ```bash + trans :/root/unidesk systemctl restart k3s + trans :k3s kubectl get nodes + ``` + + - 禁止使用 Compose 替代 YAML 选中的 k8s 运行面; + - API 恢复后立即复查 node Ready、controller 调和和业务 endpoint, + healthy 后不再扰动 backend-core 或其他已恢复 workload。 +4. 控制面稳定后释放终态 Tekton 压力: + - 先对 owning cleanup 入口执行 dry-run; + - 再以 `--confirm --wait` 有界删除符合 YAML retention 的终态 PipelineRun; + - 必须保留 active run、最新成功证据、业务 PVC、Secret、runtime workload 和 GitOps desired state。 +5. 恢复只以用户原入口结束: + - node `Ready`; + - workload ready 且无持续重启; + - Service endpoint 存在; + - backend/provider/public `/health` 返回成功; + - 恢复动作、根因假设和待持久化修复进入 issue 与运维 MDTODO。 + +### 长期防复发 + +- 从 renderer/source 消除重复内联: + - 同构多服务构建优先使用 Tekton matrix,只在 PipelineRun 中保留一份 task 实现; + - 跨 Pipeline 共享的稳定职责使用 reusable Task,并确保 Task reconciler 是正式 owner; + - 没有 Task reconciler 时不得以手工 apply 维持 TaskRef,改用 matrix 或 source 中的原生脚本文件; + - 禁止用压缩字符串、编码载荷、第二 Pipeline 或 fallback 隐藏对象膨胀。 +- 为 source artifact 和 renderer 增加结构验证: + - 统计对象总大小、`spec` 和 `status.pipelineSpec`; + - 检查重复 inline script、task 数量和 matrix 展开语义; + - 保证每个服务仍有独立 TaskRun、结果收集和失败归属; + - 结构检查只形成有界 warning,真正缺少渲染必需输入时才 fail-closed。 +- 保持 YAML-owned retention: + - 每个 consumer 声明终态 PipelineRun/TaskRun 留存策略; + - cleanup 必须保护 active、latest success 和审计证据; + - 定期清理降低对象 churn,但不能替代 renderer 减载。 +- 补齐非阻塞可观测性: + - status/history 输出对象字节、Kine/WAL、compaction、API、lease、kubelet 和 node pressure 摘要; + - CI/CD OTel span/event 记录 source commit、PipelineRun、GitOps commit、Argo revision、 + runtime digest、阶段时间和对象压力 warning; + - exporter 或 provenance 漂移只标记 evidence gap,禁止改变业务成功终态。 +- 长效修复必须由正常 PR 自动事件验收: + - 新 PipelineRun 成功且对象显著减小; + - GitOps commit、Argo revision、runtime digest 与 source identity 对齐; + - PipelineRun 执行和后续 compaction 窗口内 node、API、lease、kubelet 与业务 endpoint 保持健康; + - 不得用 k3s restart、Pod 删除、人工 PipelineRun、Argo sync 或 mirror flush 作为最终修复证据。 + ## 案例:删除工作目录引发 `getcwd()` 失败 - 共同指纹:多个 PaC consumer 的后续 step 在业务逻辑执行前统一报 `getcwd()`。 diff --git a/.agents/skills/unidesk-cicd/references/platform-ops.md b/.agents/skills/unidesk-cicd/references/platform-ops.md index ca42944c..45dd30b0 100644 --- a/.agents/skills/unidesk-cicd/references/platform-ops.md +++ b/.agents/skills/unidesk-cicd/references/platform-ops.md @@ -1,8 +1,8 @@ -# Platform Ops +# 平台运维 本文件覆盖 Secret、runtime migration、observability、platform infra、CI tools image、PipelineRun 清理和 rollout 补记。 -## Secret +## Secret 管理 ```bash bun scripts/cli.ts hwlab g14 secret status --lane v02 \ @@ -28,7 +28,7 @@ Secret 只通过 YAML sourceRef/targetKey 和受控 CLI 下发;输出只披露 - `--secret-id ` 是单个声明的稳定下钻;`--full` 与 `--raw` 才请求 exhaustive probe/capture。 - `secret-sync` 是 YAML 声明的配置维护入口,不是 source delivery 或 CI/CD 恢复入口。 -## Runtime Migration +## 运行时迁移 ```bash bun scripts/cli.ts hwlab g14 control-plane runtime-migration \ @@ -37,7 +37,7 @@ bun scripts/cli.ts hwlab g14 control-plane runtime-migration \ 通过 runtime 容器内 migration CLI 执行迁移。不要用手工 pod shell 或裸数据库命令作为长期入口。 -## Observability +## 可观测性 ```bash bun scripts/cli.ts hwlab g14 observability status|apply|query|targets|boundary|closeout \ @@ -46,7 +46,7 @@ bun scripts/cli.ts hwlab g14 observability status|apply|query|targets|boundary|c 管理 G14 Prometheus 基础设施和 HWLAB lane 监控 closeout。状态、耗时、失败原因、trace、命令结果或关键证据不可见时,先补 CLI/日志/状态输出再继续。 -## Platform Infra +## 平台基础设施 ```bash bun scripts/cli.ts platform-infra sub2api plan|apply|status|validate @@ -63,9 +63,21 @@ bun scripts/cli.ts platform-infra wechat-archive plan|apply|status|validate|pull bun scripts/cli.ts platform-infra wechat-archive wcf-host-status|collector-plan|collector-apply|collector-status ``` -`platform-infra` 是 UniDesk 运维的平台基础设施控制面;新增平台服务优先进入该命名空间或对应 YAML target,旧 `devops-infra` 只作为渐进迁移来源。Gitea mirror 和 Pipelines-as-Code 的 source-of-truth 分别是 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml`。Migrated consumer 的唯一交付触发是 GitHub PR merge,默认入口只有 Gitea/PaC status、history 与只读 debug。`closeout` 只在 PaC compatibility diagnostics scope 中保留为只读历史观察;会 POST hook test 的 `webhook-test` 已删除。`apply` 只属于首次平台 bootstrap,不能补齐 source PR 合并后的交付。任何读取失败都必须显式报告,不能误报为空表成功。其他平台服务继续遵循各自 owning YAML 与专项 skill。 +- `platform-infra` 是 UniDesk 运维的平台基础设施控制面: + - 新增平台服务优先进入该命名空间或对应 YAML target; + - 旧 `devops-infra` 只作为渐进迁移来源; + - 其他平台服务继续遵循各自 owning YAML 与专项 skill。 +- Gitea mirror 和 Pipelines-as-Code 的 source-of-truth: + - 分别为 `config/platform-infra/gitea.yaml` 与 `config/platform-infra/pipelines-as-code.yaml`; + - migrated consumer 的唯一交付触发是 GitHub PR merge; + - 默认入口只有 Gitea/PaC status、history 与只读 debug。 +- 平台维护入口边界: + - `closeout` 只在 PaC compatibility diagnostics scope 中保留为只读历史观察; + - 会 POST hook test 的 `webhook-test` 已删除; + - `apply` 只属于首次平台 bootstrap,不能补齐 source PR 合并后的交付; + - 任何读取失败都必须显式报告,不能误报为空表成功。 -## CI Tools Image +## CI 工具镜像 ```bash bun scripts/cli.ts hwlab g14 tools-image status @@ -76,7 +88,7 @@ bun scripts/cli.ts hwlab g14 tools-image build \ tools image 构建只在目标 host 和本地 registry 的受控路径中执行,不把 master server 当通用构建机。 -## PipelineRun Cleanup +## PipelineRun 清理 ```bash bun scripts/cli.ts hwlab g14 control-plane cleanup-runs \ @@ -89,9 +101,20 @@ bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs \ --lane all [--limit N] [--dry-run|--confirm] ``` -清理只删除已完成 PipelineRun 及其 Tekton TaskRun/Pod/PVC 链路,不触碰 registry storage、业务 PVC、Secret、runtime workload 或 GitOps desired state。带 `--pipeline-run` 或 `--source-commit` 的定点清理必须直接查询目标对象;不存在返回 `target-pipelinerun-not-found`,未完成返回 `target-pipelinerun-not-terminal`。 +- 清理范围: + - 只删除已完成 PipelineRun 及其 Tekton TaskRun/Pod/PVC 链路; + - 不触碰 registry storage、业务 PVC、Secret、runtime workload 或 GitOps desired state。 +- 定点清理: + - 带 `--pipeline-run` 或 `--source-commit` 时必须直接查询目标对象; + - 对象不存在返回 `target-pipelinerun-not-found`; + - 对象未完成返回 `target-pipelinerun-not-terminal`。 -## Rollout Record +- 大型 PipelineRun 造成 Kine/SQLite 压力时: + - cleanup 只属于控制面快速缓解; + - 长期修复必须消除 renderer 中重复内联的 `taskSpec`,并通过正常 PR 自动事件验收; + - 根因判定、k3s 紧急恢复和防复发规则见 [incident-recovery.md](incident-recovery.md#tekton-大对象与-kinesqlite-控制面退化)。 + +## Rollout 记录 ```bash bun scripts/cli.ts hwlab g14 record-rollout --pr --source-commit diff --git a/.agents/skills/unidesk-devlevel/SKILL.md b/.agents/skills/unidesk-devlevel/SKILL.md index 16c445a8..d79e87e3 100644 --- a/.agents/skills/unidesk-devlevel/SKILL.md +++ b/.agents/skills/unidesk-devlevel/SKILL.md @@ -43,6 +43,10 @@ description: >- ## L1 Native - 在 native 环境独立启动当前功能需要的 API、Worker、基础依赖和 HMR Web。 +- 每个 L1 服务必须使用 owning YAML 或项目规格声明的固定本地端口;启动前只识别同一服务的旧 L1 进程,若该固定端口已被它占用,先停止旧进程,再在同一端口启动新进程。 +- 不得因为端口冲突改用临时端口,也不得停止其他 L1、其他 worktree 或无关服务;无法确认端口占用者属于当前服务时先报告冲突。 +- 固定端口、bind/probe/public host、状态目录和服务组成必须由 YAML-first 配置解析;代码和命令行不得补隐式端口默认值。 +- L1 API、Worker、Temporal 开发依赖和 Web 的启动、停止、重启、状态、日志必须由项目 CLI 管理;`npm run`、`bun run`、`vite` 或裸脚本只允许作为 CLI 的内部实现,不是用户操作入口。 - CLI 显式使用项目 native `--over-api` transport,经 native API 调用 Worker。 - Web 使用 `$unidesk-webdev` 的 `web-probe` custom/local 入口访问 native Web。 - 微服务项目只启动当前微服务的前端、API、Worker及必要依赖。 @@ -52,6 +56,7 @@ description: >- - `127.0.0.1` 只用于本机 probe 或进程间代理; - bind/probe 地址不得作为 L1 用户入口返回; - 公网 IP、API port 和 Web port 缺失时必须明确失败,不得回退到 localhost 或代码默认值。 +- L1 验收命令必须实际从该公网 IP 打开页面和 API;本机 `127.0.0.1`/`localhost` 只可作为进程健康 probe,不构成 L1 证据。 - 适合前后端联调、异步作业、Workflow、网络接口和页面交互的快速开发。 ## L2 Development diff --git a/.agents/skills/unidesk-oa/SKILL.md b/.agents/skills/unidesk-oa/SKILL.md index 16ea0c6c..eadd7935 100644 --- a/.agents/skills/unidesk-oa/SKILL.md +++ b/.agents/skills/unidesk-oa/SKILL.md @@ -1,12 +1,22 @@ --- name: unidesk-oa -description: UniDesk 项目管理运行技能。用户提到 UniDesk 项目管理目录、HWLAB OA、总项目规格、总规划、重大项目/方向/课题/子课题层级、任务书、实施方案、测试规格、阶段报告、项目偏离,或通过 project-management 与 GitHub issue 管理 HWLAB Cloud M1 / UniDesk / AgentRun 跨仓项目治理时使用。 +description: UniDesk 项目管理运行技能。用户提到 UniDesk 项目管理目录、HWLAB OA、总项目规格、总规划、重大项目/方向/课题/子课题层级、任务书、实施方案、测试规格、阶段报告、项目偏离、产品/前端设计 spec、CaseRun、HarnessRL、AgentRun、Temporal、native 调试,或通过 project-management 与 GitHub issue 管理 HWLAB Cloud M1 / UniDesk / AgentRun 跨仓项目治理时使用。 --- # UniDesk OA HWLAB Cloud M1 / UniDesk / AgentRun 跨仓项目治理锚定在 UniDesk 仓库的 `project-management/` 项目编号目录和 GitHub issue 执行记录上。 +## 触发与前置 + +出现以下任一语义时必须加载本 skill,并先读取对应 `project-management/PJ*/specs/` 规格再写代码: + +- HWLAB、UniDesk、AgentRun、HarnessRL、CaseRun、Temporal、跨仓库或 v0.2/v0.3 能力迁移。 +- 新页面、前端重设计、交互/视觉设计、Web/API/CLI 统一入口、native/L1 调试形态。 +- 新增或改变长期 API、数据模型、运行生命周期、验收契约、架构边界或多阶段交付。 + +仅有一次性修复时,也要先确认所属 L1 规格和主责边界;找不到对应规格时先登记规格缺口,不得用源码注释或 issue 正文替代稳定需求正文。 + ## 高频规则 - 项目管理稳定事实源按项目编号维护在 `project-management/PJ*/`,GitHub issue 承载执行流和证据索引。 diff --git a/.agents/skills/unidesk-sub2api/references/troubleshooting-accounts.md b/.agents/skills/unidesk-sub2api/references/troubleshooting-accounts.md index 2dd7a4e0..221abb71 100644 --- a/.agents/skills/unidesk-sub2api/references/troubleshooting-accounts.md +++ b/.agents/skills/unidesk-sub2api/references/troubleshooting-accounts.md @@ -5,6 +5,7 @@ 1. 先用 `runtime errors --since ` 获取全账号同窗口证据: - 以 Sub2API 原生 dashboard overview 的请求分母、客户错误率和上游错误率为主。 - 以 account availability、concurrency 和 upstream errors 解释账号可用性、排队、状态码与根因。 + - 内部 monitor 使用真实 API key 和正式入口产生的错误必须纳入正式错误率、账号归因、trace 和调优判断;只单独标注调用方类别,不得因其不是外部客户而剔除或降级为测试噪声。 - 原生 system-log 索引缺少策略事件时,才使用 target runtime 日志补临时不可调度、failover、`account_select_failed` 和 `forward_failed`,并保留 fallback 披露。 2. 从客户可见错误中选有限 request id 运行 `trace`,按请求链区分: - 上游失败后成功切号。 diff --git a/.agents/skills/unidesk-webdev/SKILL.md b/.agents/skills/unidesk-webdev/SKILL.md index 61c075e9..7be8938c 100644 --- a/.agents/skills/unidesk-webdev/SKILL.md +++ b/.agents/skills/unidesk-webdev/SKILL.md @@ -8,7 +8,8 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web 本技能是 UniDesk/HWLAB Web 开发和受控浏览器复测的唯一入口,并遵循 Skill(cli-spec)。需求真相源仍是 `project-management/PJ2026-01/specs/`;本技能只规定如何开发、采集、复现和验收。 - 当任务使用 `$unidesk-devlevel` 描述开发方式时: - - L1 使用 custom/local 受控入口验证 native Web; +- L1 使用 custom/local 受控入口验证 native Web; + - L1 交付验证必须使用 owning YAML 解析出的公网 IP 和固定 port;`127.0.0.1`/`localhost` 只能用于本机进程 probe,不能作为 L1 浏览器入口或用户反馈 URL; - L2 使用 development semantic origin; - L3 使用 production semantic origin; - 本 skill 负责各运行面的浏览器操作,不解释项目处于哪个等级。 @@ -45,7 +46,7 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web - 用户目标只要求 CLI/API/CaseRun 终态时,同 dispatcher 的 CLI E2E 可以完成该目标,但不得冒充浏览器或视觉验收。 - 调试 HWLAB Cloud Web/Workbench 业务或功能 bug 时,使用 YAML 声明的 `--origin internal`;验收 public exposure、DNS、FRP、Caddy 或公网用户入口时,显式使用 `--origin public`。 - `web-probe screenshot` 的 semantic origin 示例:`bun scripts/cli.ts web-probe screenshot --node --lane --origin internal --path /workbench --viewport 1440x900`。 -- `web-probe screenshot` 的 custom/local 示例: +- `web-probe screenshot` 的 custom/local 示例仅用于本机 preflight,不得作为 L1 交付或用户入口证据: - custom:`bun scripts/cli.ts web-probe screenshot --node --lane --url 'https://custom.example/workbench' --viewport 1440x900`; - local:`bun scripts/cli.ts web-probe screenshot --node --lane --url 'http://127.0.0.1:4173/workbench' --viewport 1440x900`; - `--url` 已包含完整路径,与 `--path` 互斥;参数错误时按 CLI 返回的 `correctionCommands` 移除 `--path`。 @@ -79,12 +80,16 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web - YAML 声明的 L1 端口被占用时: - 同一服务的旧 L1 进程只通过项目 CLI 停止或重启; - 其他服务不得停止、复用或接管; - - 先用有界监听状态确认一个空闲端口; - - 再修改本 L1 服务的 owning YAML 端口并通过 parser/CLI 读取新值; - - 禁止用命令行覆盖、临时环境变量或隐藏 fallback 绕过 YAML; - - 端口退让后继续启动,并从 YAML 新端口执行 `web-probe native-readiness`; - - L1 进程的启动、停止、重启、状态和日志只通过项目 CLI 管理; + - 必须等待旧进程退出,再在 YAML 声明的同一端口启动; + - 禁止改用临时端口、命令行覆盖或隐藏 fallback; + - L1 进程的启动、停止、重启、状态和日志必须通过项目 CLI,禁止把裸 `npm run`、`bun run`、`vite` 或临时脚本作为用户入口; - Vite 必须绑定可访问的 native 端口并保持 HMR,浏览器端 HMR 地址不得写成 `0.0.0.0`。 +- HWLAB 工具页面默认使用受限工作区: + - AppShell 和页面根容器不得产生 document 级或主内容区全局滚动; + - 页面头、命令栏和状态摘要保持稳定可见; + - 列表、时间线、日志、证据面板和正文分别在所属 pane 内滚动; + - 移动端允许工作区内部纵向滚动,但不得把滚动责任退回 document 或 AppShell; + - 历史页面按风险逐步迁移,新页面和重设计页面必须直接采用该合同。 - L1 公网调试入口交给用户前必须完成浏览器 readiness 验收: - 使用 `bun scripts/cli.ts web-probe native-readiness --node --lane --profile ` 一次完成验收; - 公网 IP、port、path、DOM、交互和失败判据只从 owning YAML 读取,禁止用 `--url` 或 `--origin` 覆盖; @@ -95,7 +100,7 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web - L1 未通过时禁止向用户披露入口为“可用”,必须继续定位并修复,或明确报告 browser readiness blocker。 - Native 阶段采用短反馈循环: - 修改后先执行最小语法检查和目标功能验证; - - 使用 `web-probe` 的 custom/local 受控入口验证真实 DOM、交互、布局和截图; + - 使用 `web-probe` 从 YAML 选择的公网 IP 和固定 port 验证真实 DOM、交互、布局和截图;custom/local 仅在公网入口尚未具备时记录为 preflight,不能宣称 L1 通过; - 每轮优先复用仍在运行的 HMR 前端和测试后端,不重复构建镜像或触发 CI/CD; - 复杂数据、长文本、空态、错误态、折叠、滚动、缩放和目标桌面/移动视口必须在 native 阶段覆盖。 - 只有 native 功能与视觉验收收口后,才进入 CI/CD 发布阶段: diff --git a/.agents/skills/unidesk-webdev/references/design.md b/.agents/skills/unidesk-webdev/references/design.md index 7775dd9b..acd29861 100644 --- a/.agents/skills/unidesk-webdev/references/design.md +++ b/.agents/skills/unidesk-webdev/references/design.md @@ -1,13 +1,21 @@ -# UniDesk Web Design Defaults +# UniDesk Web 设计默认规范 本文件记录 UniDesk/HWLAB Web 工具页的长期设计默认。`SKILL.md` 只保留入口索引;实际做前端布局、Workbench/Project Management 页面或视觉验收时读取本文件。 - UniDesk/HWLAB 的工具页默认是工作界面,不是营销页:首屏直接呈现可操作对象、状态和主要任务流,避免 hero、装饰卡片和说明性大段文案。 - 信息密度优先但要稳定:header 保持窄,source/file/project 等上下文选择放在顶部工具栏或状态栏下拉;计数、来源、taskRef、更新时间等元信息收敛到详情/感叹号弹窗,不长期占主版面。 - 主内容区必须占优:树、大纲、列表和导航只作为左侧辅助视图,默认约 30% 或更小,并支持折叠或调整宽度;正文、报告、trace、消息或表格等主工作内容应占页面主要空间。 -- Workbench 类三栏/双栏工作区优先复用通用 bounded layout 组件或抽取通用组件后再接入新页面;不要为每个页面重新堆叠一套 `aside + main + report` CSS。根容器和 workspace 使用稳定高度、`min-height: 0` 和内部滚动,避免 document 级长页或外层横向溢出;窄桌面仍应保持可操作分栏,移动断点再退化为纵向布局。 +- Workbench 类三栏/双栏工作区优先复用通用受限布局组件或抽取通用组件后再接入新页面;不要为每个页面重新堆叠一套 `aside + main + report` CSS。 +- HWLAB 页面滚动边界: + - AppShell、主内容区和页面根容器保持视口内稳定高度,使用 `min-height: 0` 和 `overflow: hidden`; + - document 和主内容区不得承担工具页面的全局纵向或横向滚动; + - 页面头、命令栏、状态摘要和主要操作保持稳定可见; + - 树、列表、正文、事件流、时间线、日志和证据检查器在各自 pane 内滚动,并使用 `overscroll-behavior: contain`; + - 窄桌面可把次要 pane 移到工作区下一行,移动端可改为工作区内部纵向布局,但滚动容器仍是 workspace 或 pane; + - 新页面和重设计页面必须遵守;历史页面通过 AppShell 路由元数据逐步迁移,禁止一次全局切换造成回归。 +- 页面可见文案默认使用中文;协议字段、实体名、命令、路径、`runId`、`traceId`、`SHA256`、`HWPOD` 等机器或领域标识可保留原文,状态码旁应提供中文含义,禁止整块工作区只显示英文标题和状态。 - 新建、配置、probe、reindex 等低频表单用对话框或抽屉承载;默认版面不要常驻新建框、配置表或调试面板。高频命令保留为稳定按钮或图标按钮,并配清晰状态。 - 文档/任务类页面优先就地阅读和就地编辑:标题和正文双击或显式编辑态切换,不为常规查看单独铺满输入框;正文渲染态必须可见,不能只显示 taskRef、文件名或更新时间。 - 报告、Markdown、日志和 JSON 片段要用成熟 Markdown/代码块渲染,代码块采用浅色 Workbench 风格、可滚动且不撑破布局;不要把裸 JSON 或长日志直接挤爆页面。 - 页面状态要可深链:重要 source/file/task/report/session 选择进入 RESTful URL,并能刷新后恢复;联动其他功能只通过公共 API、稳定 id 和 URL,不把两个功能互相嵌入成强依赖。 -- 设计验收必须用截图或 web-probe 样本检查真实版面:正文是否可见、主区域是否足够、弹窗是否遮挡流程、长文本是否溢出、移动/窄屏是否仍可操作。桌面默认视口是 `1920x1080`;项目/MDTODO/报告类页面还应按风险覆盖紧凑桌面视口(例如 960x600)的深链默认态、左侧树折叠态、右侧报告关闭态。涉及报告渲染或布局重写时还必须覆盖右侧报告栏和报告全屏态。记录 document overflow、pane 宽高、正文/报告可见性、按钮是否被遮挡,以及截图 SHA。 +- 设计验收必须用截图或 web-probe 样本检查真实版面:正文是否可见、主区域是否足够、弹窗是否遮挡流程、长文本是否溢出、移动/窄屏是否仍可操作。桌面默认视口是 `1920x1080`;项目/MDTODO/报告类页面还应按风险覆盖紧凑桌面视口(例如 960x600)的深链默认态、左侧树折叠态、右侧报告关闭态。涉及报告渲染或布局重写时还必须覆盖右侧报告栏和报告全屏态。记录 document overflow 必须为零、主内容区不得滚动、pane 宽高和滚动范围、正文/报告可见性、按钮是否被遮挡,以及截图 SHA。 diff --git a/.agents/skills/unidesk-webdev/references/web-probe.md b/.agents/skills/unidesk-webdev/references/web-probe.md index cbcd4194..7355919a 100644 --- a/.agents/skills/unidesk-webdev/references/web-probe.md +++ b/.agents/skills/unidesk-webdev/references/web-probe.md @@ -10,6 +10,7 @@ - 单次命令必须同时判定 DOM 挂载、基础交互、`console.error`、`pageerror`、关键失败响应和截图; - 顶层 `status=pass` 才能向用户披露入口可用,`blocked` 必须继续修复或报告 browser readiness blocker; - 命令不接受 `--url` 或 `--origin`,避免绕过 owning YAML 选择其他运行面。 + - L1 浏览器必须打开 owning YAML 解析出的公网 IP 和固定 port;`127.0.0.1`、`localhost` 和临时 `--url` 只能作为本机 preflight,不能替代 L1 readiness。 - Cloud Console 页面矩阵: - 使用 `bun scripts/cli.ts web-probe console-verify --node --lane --origin internal|public --profile `; - 配置真相位于 `config/hwlab-node-lanes.yaml#templates.*.webProbeWorkbench.consoleVerificationProfiles`; diff --git a/AGENTS.md b/AGENTS.md index 1b274fcc..c7d5ede7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,10 @@ - 过长单行随见随拆,且必须拆成结构化子列表,禁止自然换行; - 命令、路径、代码字段、协议名和上游原文引用可保留原文; - 细则见 `$docs-spec`。 +- P0: HWLAB/UniDesk/AgentRun/HarnessRL/CaseRun、跨仓库能力、长期 API/数据模型、前端页面设计或 native/L1 调试涉及代码时,必须先加载 `$unidesk-oa`,读取对应 `project-management/PJ*/specs/` 规格并确认主责 L1;没有规格先补规格,不得直接以 issue 或源码开始实现。 +- P0: L1 native 每个服务使用固定端口;若端口被确认属于同一服务的旧 L1 进程,先停止旧进程再同端口启动,不得换临时端口或停止其他服务;规则详见 `$unidesk-devlevel`。 +- P0: L1 固定端口、host、状态目录和服务组成必须来自 YAML-first;启动、停止、重启、状态和日志必须走项目 CLI,禁止裸起 `npm run`、`bun run`、`vite` 或临时脚本;规则详见 `$unidesk-devlevel` 与 `$unidesk-webdev`。 +- P0: L1 浏览器和用户入口必须使用 owning YAML 解析出的公网 IP 与固定 port;`127.0.0.1`/`localhost` 只能做本机健康 probe,不能作为 L1 验收或交付 URL。 - P0: NC01 UniDesk server/backend-core 运行面规则: - 只认 `config/unidesk-host-k8s.yaml` 选中的 k8s `unidesk` 运行面; - 禁止用 Compose 替代; @@ -105,6 +109,7 @@ - 优先走 `$unidesk-trans` 的 `trans apply-patch`; - route/cwd 见 `docs/reference/cli.md`。 - P0: 专项技能路由: + - 项目规格、前端产品设计 spec、CaseRun/HarnessRL、AgentRun、Temporal、跨仓库实现和 native/L1 开发先走 `$unidesk-oa`,再按 Web、Temporal、AgentRun 或 devlevel 继续加载专项技能; - Nginx 负载均衡、logger API 和 PK01 host-Docker 运维走 `$unidesk-nginx`; - Web、网页登录、截图、移动端、Workbench 和浏览器验收走 `$unidesk-webdev` 的 `web-probe` 受控入口,禁止直接调用 Playwright; - OTel/Tempo/trace 走 `$unidesk-otel`; diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 74d4cde2..6f83bcba 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -162,6 +162,17 @@ lanes: traceTimeline: autoExpandRunning: false autoCollapseTerminal: false + caserun: + publicHostRef: config/unidesk-host-k8s.yaml#runtime.publicHost + stateDir: .state/caserun-native/services + api: + bindHost: 0.0.0.0 + probeHost: 127.0.0.1 + port: 4316 + web: + bindHost: 0.0.0.0 + probeHost: 127.0.0.1 + port: 5173 sourceAuthority: extends: templates.hwlabV03.sourceAuthorityGiteaSnapshot giteaMirrorRepoKey: hwlab-nc01-v03 @@ -796,6 +807,99 @@ templates: errors: 20 failedResponses: 20 textPreviewChars: 160 + caserun: + scheme: http + path: /caserun + navigationTimeoutMs: 30000 + settleMs: 700 + commandTimeoutSeconds: 120 + viewport: + width: 1920 + height: 1080 + readySelectors: + - "#caserun-page" + - "#caserun-panel" + minBodyTextLength: 40 + interaction: + selector: ".caserun-case-row:nth-child(2)" + observedSelector: ".caserun-case-row:nth-child(2)" + observedAttribute: class + failedResponseStatus: 400 + criticalPathPrefixes: + - /v1/caserun + screenshotName: native-caserun-ready.png + layout: &caserunNativeLayout + requireNoDocumentOverflow: true + requireNoMainContentOverflow: true + mainContentSelector: .platform-content + internalScrollSelectors: + - .caserun-case-list + - .caserun-events + - .evidence-body + outputLimits: + errors: 20 + failedResponses: 20 + textPreviewChars: 160 + caserun-compact: + scheme: http + path: /caserun + navigationTimeoutMs: 30000 + settleMs: 700 + commandTimeoutSeconds: 120 + viewport: + width: 1180 + height: 800 + readySelectors: + - "#caserun-page" + - "#caserun-panel" + minBodyTextLength: 40 + interaction: + selector: ".caserun-case-row:nth-child(2)" + observedSelector: ".caserun-case-row:nth-child(2)" + observedAttribute: class + failedResponseStatus: 400 + criticalPathPrefixes: + - /v1/caserun + screenshotName: native-caserun-compact-ready.png + layout: *caserunNativeLayout + outputLimits: + errors: 20 + failedResponses: 20 + textPreviewChars: 160 + caserun-mobile: + scheme: http + path: /caserun + navigationTimeoutMs: 30000 + settleMs: 700 + commandTimeoutSeconds: 120 + viewport: + width: 390 + height: 844 + readySelectors: + - "#caserun-page" + - "#caserun-panel" + minBodyTextLength: 40 + interaction: + selector: ".caserun-case-row:nth-child(2)" + observedSelector: ".caserun-case-row:nth-child(2)" + observedAttribute: class + failedResponseStatus: 400 + criticalPathPrefixes: + - /v1/caserun + screenshotName: native-caserun-mobile-ready.png + layout: + requireNoDocumentOverflow: true + requireNoMainContentOverflow: true + mainContentSelector: .platform-content + internalScrollSelectors: + - .caserun-workspace + - .caserun-case-list + - .caserun-events + - .evidence-body + outputLimits: + errors: 20 + failedResponses: 20 + textPreviewChars: 160 defaultConsoleVerificationProfile: cloud-console consoleVerificationProfiles: cloud-console: diff --git a/config/platform-infra/sub2api-codex-pool.yaml b/config/platform-infra/sub2api-codex-pool.yaml index 0dce0f91..af66c7f4 100644 --- a/config/platform-infra/sub2api-codex-pool.yaml +++ b/config/platform-infra/sub2api-codex-pool.yaml @@ -44,7 +44,7 @@ runtime: durationMinutes: 1 description: 上游 API-key 查询依赖或请求传输内部失败时短时冷却当前账号。 - statusCode: 502 - keywords: [upstream service temporarily unavailable, upstream request failed, service temporarily unavailable, overloaded, concurrency limit exceeded] + keywords: [upstream service temporarily unavailable, upstream request failed, service temporarily unavailable, overloaded, concurrency limit exceeded, upstream rate limit exceeded] durationMinutes: 1 description: 明确命中 502 上游不可用、过载或并发限制时短时冷却当前账号。 - statusCode: 503 diff --git a/docs/MDTODO/agentrun-runtime-reliability.md b/docs/MDTODO/agentrun-runtime-reliability.md index 90584216..cde9aef2 100644 --- a/docs/MDTODO/agentrun-runtime-reliability.md +++ b/docs/MDTODO/agentrun-runtime-reliability.md @@ -140,6 +140,9 @@ 解决 [UniDesk #2103](https://github.com/pikasTech/unidesk/issues/2103):将 `agentrun events` 的主工作流改为保留 `--after-seq` 分片游标并使用 `--expect `、`--timeout ` 的 manager/API 长轮询;达到期望新增事件数立即返回,超时返回已观察事件及明确 timeout/next cursor,禁止 CLI 本地高频忙轮询或无输出,并同步 CLI、manager、测试与 `unidesk-agentrun` skill 文档,完成任务后将详细报告写入[任务报告](./details/agentrun-runtime-reliability/R8_Task_Report.md)。 +### R8.1 + +解决 [AgentRun #377](https://github.com/pikasTech/agentrun/issues/377):定位并修复 `events --expect/--timeout` 并发长轮询期间 manager 健康探针超时、KafkaJS 负 timeout 与 durable dispatch 无 runner 的因果链;保持既有 PostgreSQL/Kafka event authority、dispatcher/reconciler 与只读长轮询合同,不新增第二 authority、scheduler、租约或客户端 retry,通过正常 PR/自动 CI/CD 和并发 long-poll、health latency、dispatch/restart recovery 验收,且不得由 Artificer 递归修复自身,完成任务后将详细报告写入[任务报告](./details/agentrun-runtime-reliability/R8.1_Task_Report.md)。 ## R9 [completed] 解决 [UniDesk #2176](https://github.com/pikasTech/unidesk/issues/2176):调查 post-task dispatch 将 R8、R7.1、R7.2 三个不同 Artificer task 误判为 Issue #1888 重复任务并统一取消的 P0 事故;追溯引入提交,错误 PR 先语义回滚,再以完整 logical-operation fingerprint 约束去重范围。不同 payload/workspace/MDTODO 必须互不影响,identity 不完整或观察陈旧时只 warning 且禁止 destructive cancellation;同一操作重放返回原 task。补 OTel typed evidence 与四任务回归,禁止锁、租约、第二状态库、PK01、Vitest和人工 CI/CD,正常 PR merge 后从原入口验收,完成任务后将详细报告写入[任务报告](./details/agentrun-runtime-reliability/R9_Task_Report.md)。 diff --git a/docs/MDTODO/details/hwlab-caserun/R4.7_Task_Report.md b/docs/MDTODO/details/hwlab-caserun/R4.7_Task_Report.md new file mode 100644 index 00000000..11393a74 --- /dev/null +++ b/docs/MDTODO/details/hwlab-caserun/R4.7_Task_Report.md @@ -0,0 +1,29 @@ +# R4.7 任务报告 + +## 结果 + +- CaseRun 从 Workbench 内嵌面板迁移为 `/caserun` 与 `/caserun/runs/:runId` 独立页面,保留正式 Cloud Web AppShell、导航、Router 和设计系统。 +- 页面提供 case catalog、运行身份与阶段、事件流、blocker、aggregate、artifact、trace 与 HWPOD 证据检查器;终态只读取 CaseRun API 投影。 +- L1 API/Web 使用 `config/hwlab-node-lanes.yaml` 声明的公网 `152.53.229.148`、固定端口 `4316/5173`、bind/probe host 和状态目录。 +- `hwlab-cli caserun service api|web start|stop|restart|status|logs` 管理服务生命周期;restart 等待旧进程组退出,必要时强制结束,再复用同一固定端口。 +- CLI 缺少 `CASERUN_PUBLIC_HOST` 时返回 `native_exposure_config_required`;Web 到 API 的内部 proxy host/port 也必须由 YAML profile 注入,不再存在 localhost 或固定端口代码回退。 +- Caserun native Vite 模式仅在测试开关启用时注入正式 AppShell 所需 runtime config,并以 `204` 接收非阻塞 Web RUM 上报,生产路径不变。 +- UniDesk 的 OA、DevLevel、WebDev、AGENTS 与 HarnessRL SPEC 已固化:L1 用户和浏览器入口必须使用 YAML 解析出的公网 IP,localhost 仅允许内部 probe。 + +## L1 验证 + +- 公网 API smoke:`HWLAB_CASERUN_NATIVE_URL=http://152.53.229.148:4316 bun scripts/caserun-native-smoke.ts`,completed aggregate、runId 和 SHA 通过。 +- 公网 Web readiness:`caserun`(1920x1080)、`caserun-compact`(1180x800)、`caserun-mobile`(390x844)全部通过。 +- 三档 readiness 均确认 `#caserun-page`、`#caserun-panel` 已挂载,case 选择交互生效,console error、pageerror 与关键失败响应均为 0,并生成截图 artifact。 +- `bun test scripts/caserun-native-fixtures.test.ts`:2/2 通过。 +- `bunx vitest run src/stores/caserun.test.ts`:2/2 通过。 +- `bun scripts/caserun-native-reload-smoke.ts`:API supervisor 热重载、原子保存窗口与无端口占用错误通过。 +- UniDesk native readiness focused test:5/5 通过。 +- 缺少公网 host 的 L0 启动检查按预期拒绝;修复后的 CLI restart 约 2 秒完成,日志无旧进程尾写污染。 +- `git diff --check` 通过。 + +## 边界 + +- 未修改 CI/CD 基础设施,也未执行 L2/L3 rollout。 +- 全量 `vue-tsc` 中本任务新增的 CaseRun、router、Vite 与 supervisor finding 已清零;剩余 finding 位于既有 TaskTree 与 Agent Observer 文件,不属于本任务。 +- API 与 Web 当前继续由项目 CLI 管理运行,交付入口为 `http://152.53.229.148:5173/caserun`。 diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R6.10.2_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.10.2_Task_Report.md new file mode 100644 index 00000000..545c0029 --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.10.2_Task_Report.md @@ -0,0 +1,22 @@ +# R6.10.2 任务报告 + +## 结果 + +- 在 `pikainc/pikaoa` 合并 PR [#96](https://github.com/pikainc/pikaoa/pull/96),merge commit 为 `ce000a3f84f53eef07c52dac416f5ce954b1b2fb`。 +- 新增共享 `FormField` 与 `FormFieldLabel`,统一字段名称、必填/选填徽标和辅助提示。 +- 合同、合同版本、发票、伙伴、甲方分类、员工、附件上传、PDF 导入校对及登录表单已接入。 +- `ResourceSelect`/`SearchSelect` 支持 `required`、`aria-required` 和必填不可清空;筛选栏默认不展示字段要求标识。 + +## 验证 + +- 定向组件测试:`3 passed, 8 skipped`。 +- Vite 构建通过:`1665 modules transformed`。 +- L1 受控 `web-probe` 登录页截图 SHA-256:`4c4955467b525ca4c51fbe078a830d3a469234e86f0513af48857bf59a6fd5d2`。 +- L1 页面确认账号、密码必填徽标可见,无 page error、失败请求或横向溢出;favicon 404 为非阻塞 warning。 +- 完整旧测试文件的 7 个 Vue/Test Utils 兼容断言失败作为既有非阻塞 warning,不扩大本次范围。 + +## 未完成边界 + +- 自动 CI/CD 已接到 source commit `ce000a3f84f5`,但 `pikaoa-dev-nc01` 在 229 秒时仍为 `Running`,超过 owning YAML 的 120 秒交付预算。 +- 一次定点 history 读取返回 `pac-read-timeout`;未人工触发 PipelineRun、mirror 或 Argo sync,也未修改公共 CI/CD。 +- L2 CLI/Web 回归尚未完成,因此 MDTODO 保持进行中;待自动链恢复终态后继续受控验收。 diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R6.6_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.6_Task_Report.md new file mode 100644 index 00000000..0a012a4d --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.6_Task_Report.md @@ -0,0 +1,63 @@ +# R6.6 阶段报告 + +## 目标与状态 + +PikaOA Temporal/native 改造、native smoke、development 自动 CI/CD 和部署态 CLI/Temporal/OTel/Prometheus 已完成。API、Worker 与 Web 已独立部署并 ready;最终部署态 Web 复验因远端 web-probe 浏览器连续出现 command timeout 和 Chromium 关闭而 blocked,R6.6 保持进行中,不把探针基础设施失败冒充业务验收通过。 + +## Native 验收 + +- CLI 默认 local,显式 `--over-api` 走同一 dispatcher/REST 合同。 +- native Temporal Worker、native API、Web HMR 与 typed web-probe smoke 已通过。 +- native Web smoke report SHA:`sha256:3249fc88f037ca790e0e19649e2ef08a8b7820b079dedf01a0e4dfc83c919bf3`。 +- native smoke artifact SHA:`sha256:d65342e96f1411c9b6cf2e44656623488cee8c80c79c5db03d37ed31d78580cb`。 +- `nativeIterationCount`:2 次完整 typed smoke;此前单步 CLI/HMR 迭代未统一计量。 +- `nativeElapsedSeconds`:缺少统一开始时钟,记为缺失。 + +## 自动交付与运行面 + +- PikaOA #84:`72dfa4c0faae95bcef66cf77b3b0b16095f4604a`,交付 Temporal/native 主改造。 +- PikaOA #85:`af999b46c2548d0748c186f29701e22d1760d39f`,刷新 development source artifact。 +- UniDesk #2443:`70703e45fb2e93d45ee4c592cade4204f2782d93`,补齐 Worker runtime SecretRef。 +- PikaOA #86:`2aba1a0dcc232ee00a28442c54802dec5fea3701`,交付 Worker runtime env。 +- UniDesk #2444:`616c1bdb67300e1359c8b8c88f398f5a757447cc`,补齐 Worker 附件 PVC/fsGroup。 +- PikaOA #87:`d299a94305e4965a216fa9f2e3ad0231143658a1`,交付最终 Worker volume 声明。 +- 最终 PipelineRun:`pikaoa-dev-nc01-d299a94305e4965a216fa9f2e3ad0231143658a1-jc59z`,`Succeeded`,耗时 478 秒。 +- Argo Application `pikaoa-dev-nc01`:`Synced/Healthy`,revision `c5487d316150473581fdf244dc4cc16fc28c1b08`。 +- `pikaoa-api`、`pikaoa-worker`、`pikaoa-web` 均为 `1/1` ready,镜像均使用 sha256 digest pin。 +- 未执行 bootstrap、人工 PipelineRun、mirror sync、Argo refresh/sync 或 CI/CD 公共基础设施修改。 + +## 部署态 CLI 与附件恢复 + +- `health --ready`:`status=ready`。 +- `metrics`:`metricFamilies=59`,`databaseReady=1`,`migrationReady=1`。 +- 7 个模块 active;合同 1 条、发票 1 条、附件引用 3 条。 +- 首次部署态 Web smoke 的合同、发票、附件路由和名称链接均通过,合同 PDF 返回 503。 +- CLI 定点下载确认两条新引用只有元数据、文件已随旧 API 临时文件系统滚动丢失;API 当前已把 `/var/lib/pikaoa/attachments` 挂载到 Bound PVC `pikaoa-attachments`。 +- 通过 CLI unlink 两条悬空引用,审计操作保留;随后通过同一公网 API 重新上传用户提供的合同和发票原 PDF,保持原发票 ID、合同版本 ID 关联。 +- 新发票附件 ID:`019f6ef5-c26a-7c59-8205-627808db351f`。 +- 新合同附件 ID:`019f6ef5-ca2e-72d7-a6ac-6073187ff4e4`。 +- CLI 下载 SHA 与原 PDF 完全一致:发票 `a2bddbabc2501c667233799e073f18a4b880d6d8de4e5ff0e69bc07d3ccb8e08`,合同 `d81de2f608bba719376e3626123b61317f68eb624a9501f0c176c455d1b611f9`。 + +## Temporal 与可观测性 + +- Worker 日志持续记录 workflow `pikaoa-dev-outbox-drain` 的 activity 完成。 +- development Worker Prometheus 实例 `10.42.0.189:8081`:`pikaoa_outbox_processed_total=7`、`pikaoa_outbox_pending=0`。 +- development API 实例 `10.42.0.188:8080`:`pikaoa_database_ready=1`;API 与 Worker scrape 实例均可见。 +- 上传 trace `4b05f0f027b9a371dc26b3a0278bcbf0` 已在 Tempo 找到:服务 `pikaoa-dev-default-api`,17 spans,0 error spans,包含 authorization、PostgreSQL、attachment storage 与 upload span。 +- observability closeout 的 status、validate、metrics 与 trace 最终均通过;观测缺口只按 non-blocking warning 处理。 + +## 效率证据 + +- `deliveryRunCount`:4 次 development PipelineRun,均成功。 +- `pipelineElapsedSeconds`:218 + 262 + 608 + 478 = 1566 秒。 +- `deliveryElapsedSeconds`:缺少四次 GitHub merge 的统一起点采集,不以 PipelineRun start 代替,记为缺失。 +- `rolloutCount`:缺少每轮 runtime rollout 的统一计数,记为缺失。 +- `meteredResourceCost`:执行面未提供账单或资源计量,记为缺失。 +- native 阶段未触发正式流水线;没有同类受控基线,因此不计算提速百分比或降本金额。 + +## 剩余阻塞 + +- PDF 恢复后第一次 web-probe 在 Chromium 已启动后 `web-probe-command-timeout`。 +- 按约束只重试一次;第二次 Chromium 在登录阶段关闭,被分类为 `auth-login-failed/target-auth-bug`,与此前同一命令已成功登录并进入 PDF 断言相冲突。 +- 不继续重复浏览器启动,不修改 web-probe 或 CI/CD 公共基础设施。 +- 恢复后只需单步重跑:`bun scripts/cli.ts web-probe product-smoke --product pikaoa --target NC01 --profile admin-mvp --runtime development`;通过后再 complete R6.6。 diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R6.7_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.7_Task_Report.md new file mode 100644 index 00000000..bf8cf980 --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.7_Task_Report.md @@ -0,0 +1,38 @@ +# R6.7 任务报告 + +## 结果 + +- PikaOA L1 native API、Temporal Worker 与 Web HMR 已在 NC01 独立启动。 +- Web 公网测试入口为 `http://152.53.229.148:4173`,Vite 监听 `0.0.0.0:4173`,API 代理到 `127.0.0.1:28080`。 +- 新增共享 `ResourceWorkspace`,并把合同、发票、伙伴、甲方分类、员工、附件、审计七个资源页统一接入。 +- 列表请求统一使用 `AsyncState loadingMode="silent"`,刷新时保留既有列表且不显示 `.spin`;详情请求使用独立 `detailLoading`,只在右侧详情栏显示加载状态。 +- 桌面端详情面板以 `.shell-content` 为定位容器,使用 `top: 0; right: 0; bottom: 0` 占满可用高度;移动端回退单栏详情。 + +## 运行面 + +- API:`127.0.0.1:28080`,PID `716275`。 +- Worker metrics:`127.0.0.1:29090`,PID `711628`。 +- Web HMR:`0.0.0.0:4173`,native lifecycle PID `726053`。 +- Temporal namespace:`unidesk`;task queue:`pikaoa-l1-shared-detail`。 +- 产品提交:`c1a428e`(共享资源工作区)与 `2a53c77`(公网 HMR 监听),已推送 `feat/l1-shared-detail-workspace`。 + +## 验证 + +- `go test ./...`:通过。 +- 定向 Vitest `资源工作区共享详情状态且静默刷新保留列表内容`:1 passed。 +- `bun run build`:通过,1659 modules transformed。 +- CLI `--over-api health --ready`:ready;8 个模块 active。 +- CLI `contracts list`:1 份合同;`invoices list --status all`:2 张发票,其中 1 张 active、1 张 voided。 +- 公网 `curl -I http://152.53.229.148:4173/`:HTTP 200。 +- WebProbe typed command `product-smoke --product pikaoa --target NC01 --profile admin-mvp --runtime native`:登录、合同、发票、附件路由及名称/ID 关联检查通过;报告 SHA `sha256:a41686e9dd429f78459c86a680096455943916f26e77a8afc4f3d74bc7eebbd0`,截图 SHA `sha256:ee54b6613515f1553a063dc847fc6b8bc3b06d6378aabc839705dd0d5183d905`。 + +## 非阻塞 Warning + +- WebProbe 最终状态被合同 PDF 内容请求 `503 attachment_storage_error` 标记为 blocked。原因是 L1 native 配置使用独立 `.state/pikaoa-l1-native/attachments`,数据库中已有附件元数据但该临时目录没有对应 PDF 文件。本次资源工作区、路由、登录、列表和关联检查均已执行;此项不作为本次 UI 工作区交付门禁。 +- 全量前端 Vitest 的历史 `resource-list-components.test.ts` 依赖兼容失败不计入本任务;本次新增定向用例单独通过。 + +## 效率证据 + +- `nativeIterationCount=1`(接管后以现有 L1 运行面完成验证与公网 HMR 收口)。 +- `deliveryRunCount=0`、`rolloutCount=0`:本任务未触发 CI/CD 或 Kubernetes rollout。 +- `nativeElapsedSeconds`、`pipelineElapsedSeconds`、`deliveryElapsedSeconds` 和 `meteredResourceCost` 未采集,不虚构数值。 diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R6.8_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.8_Task_Report.md new file mode 100644 index 00000000..6beb2f48 --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.8_Task_Report.md @@ -0,0 +1,52 @@ +# R6.8 任务报告 + +## 结果 + +PikaOA 共享资源工作区已完成 L2 Development 自动交付和原入口验收。唯一交付触发为产品 PR #88 合并到 `master`,未执行人工 mirror sync、PipelineRun、Argo sync、GitOps apply 或运行面 patch。 + +## 交付证据 + +- PR:`pikainc/pikaoa#88`。 +- source merge commit:`7526ddc66d812b339d98c1f4d3b7fb0cf6945ba4`。 +- PipelineRun:`pikaoa-dev-nc01-7526ddc66d812b339d98c1f4d3b7fb0cf6945ba4-hqjlb`。 +- PipelineRun 状态:`Succeeded`;开始 `2026-07-17T10:29:20Z`,结束 `2026-07-17T10:32:29Z`,流水线耗时 189 秒。 +- Argo application:`pikaoa-dev-nc01`,`Synced/Healthy`,operation `Succeeded`,GitOps revision `ab8612a475562de2e57f567b8aa7edd2499e9447`。 +- namespace:`pikaoa-dev`。 +- `pikaoa-api`、`pikaoa-worker`、`pikaoa-web` 均为 `Ready 1/1`,并在本次交付后重建。 + +## CLI 验收 + +通过 development 原入口 `https://oa-dev.hwpod.com` 使用同一项目 CLI `--over-api` 验证: + +- `health --ready`:ready。 +- `modules`:8 个模块 active。 +- `contracts list`:1 份合同。 +- `invoices list --status all`:2 张发票,包含 1 张 active 和 1 张 voided,废弃事实保持。 + +## Web 验收 + +受控命令:`web-probe product-smoke --product pikaoa --target NC01 --profile admin-mvp --runtime development`。 + +- 状态:pass。 +- 公网入口:`https://oa-dev.hwpod.com`。 +- 合同、发票、附件路由通过。 +- 合同 PDF、发票 PDF 和附件内容通过。 +- 桌面与移动端紧凑列表布局通过。 +- 资源筛选名称/ID 搜索、名称链接与 ID 路由身份保持通过。 +- report SHA:`sha256:a05eb705d95551e0908204c7c9075bfac642ba3075c03c23939b9b5d9e50b8c0`。 +- desktop screenshot SHA:`sha256:78428162c9ea5bd594acd4e9987bfec0eb3c17139f59734d47a1f0244d40e9e8`。 +- mobile screenshot SHA:`sha256:61104ded7189bcb09d72d5c2898badc9cf6885f46c5bd3e3a133f332401dc3d0`。 + +## 非阻塞 Warning + +- UniDesk PaC `status/history` 聚合读取对多个 consumer 普遍出现 `pac-read-timeout` 或 `pac-read-remote-failure`。本次通过 exact PipelineRun、Argo、Pod rollout、CLI 和 WebProbe 独立证明 PikaOA 自动交付成功;未修改共享 CI/CD 基础设施。 +- 同一 master merge 仍观察到历史 `pikaoa-test-nc01` consumer 被触发。它不影响 `pikaoa-dev-nc01` 的 L2 成功,本任务未扩展到旧 consumer 退役。 + +## 效率证据 + +- `nativeIterationCount=1`,复用 R6.7 已完成的 L1 结果。 +- `deliveryRunCount=2`(观察到 development 与历史 test consumer 各 1 次自动事件)。 +- `pipelineElapsedSeconds=189` 仅记录选中 development PipelineRun。 +- `rolloutCount=1`(development API/worker/Web 同一 GitOps revision 收敛)。 +- `deliveryElapsedSeconds` 未建立完整统一时间戳,未虚构。 +- `meteredResourceCost` 未提供。 diff --git a/docs/MDTODO/details/pikaoa-enterprise-platform/R6.9_Task_Report.md b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.9_Task_Report.md new file mode 100644 index 00000000..d2ff8ee0 --- /dev/null +++ b/docs/MDTODO/details/pikaoa-enterprise-platform/R6.9_Task_Report.md @@ -0,0 +1,54 @@ +# R6.9 任务报告 + +## 结果 + +PikaOA 已完成 L3 Production 自动交付。生产公网入口为 `https://oa.hwpod.com`,namespace 为 `pikaoa`。唯一生产运行面触发为产品 release PR merge,未执行人工 PipelineRun、mirror sync、Argo sync、GitOps apply 或生产 patch。 + +## Release 整合 + +- 初始 PR #89 使用 `master -> release`,因分支冲突关闭,未合并、未触发生产交付。 +- 从 `master@7526ddc66d812b339d98c1f4d3b7fb0cf6945ba4` 建立独立整合分支,合入旧 release 历史。 +- 冲突仅位于 README;保留 master v0.9/Temporal/CLI-first 内容,并更新当前 `pikaoa-dev`、`pikaoa`、`oa-dev.hwpod.com`、`oa.hwpod.com` 交付事实。 +- 发现 release 私有 `.tekton/pikaoa-nc01-pac.yaml` 漂移后,通过受控 `source-artifact write` 从 `config/pikaoa.yaml#releaseRuntime.targets.NC01` 重新渲染;校验 `desired=source`、provenance 对齐。 +- 正式 PR:`pikainc/pikaoa#90`。 +- release merge commit:`db377d188d65f86e199e0bd3c24dd292a77ff6ab`。 + +## 自动交付证据 + +- PipelineRun:`pikaoa-nc01-db377d188d65f86e199e0bd3c24dd292a77ff6ab-h9kk7`。 +- PipelineRun:`Succeeded`;开始 `2026-07-17T12:49:27Z`,结束 `2026-07-17T12:52:24Z`,耗时 177 秒。 +- Argo application:`pikaoa-nc01`,`Synced/Healthy`,operation `Succeeded`。 +- GitOps revision:`98af53c74288850cb6231982b97c2da7ef586906`。 +- Argo 开始 `2026-07-17T12:54:05Z`,完成 `2026-07-17T12:54:17Z`。 +- `pikaoa-api`、`pikaoa-worker`、`pikaoa-web` 均为 `Ready 1/1`,镜像 digest 全部更新。 +- `source-artifact verify-runtime`:clean exact worktree、`commitMatch=true`、embedded PipelineRun provenance 对齐、runtime commit 精确为 `db377d1`。 + +## CLI 验收 + +production 原入口 `https://oa.hwpod.com`: + +- `health --ready`:ready。 +- `modules`:8 个模块 active。 +- `contracts list`:请求成功,当前 production 数据库 0 条。 +- `invoices list --status all`:请求成功,当前 production 数据库 0 条。 + +## Web 验收 + +受控命令:`web-probe product-smoke --product pikaoa --target NC01 --profile admin-mvp --runtime release`。 + +- 登录通过。 +- 合同、发票、附件三条 production 路由通过。 +- 页面加载新 `ResourceWorkspace` 和目标合同/发票 chunk,HTTP 200。 +- production 数据库为空,因此固定样本 `CLI-2026-001`、发票和附件/PDF 断言无法执行,typed smoke 最终为 blocked/unmet-expectation。 +- 未为通过探针向生产写入测试数据;按开发初期旧数据可丢弃的要求,此项作为非阻塞数据夹具 warning。 +- report SHA:`sha256:6375c226d4dc9b059b5b0dd1673f68beefc15c1829601724773d192d5db6b3bd`。 +- screenshot SHA:`sha256:365de00c95225faf79c333cf8ab3f154b77f7a7b6be6422f736b7b2998d4ebd0`。 + +## 效率证据 + +- `deliveryRunCount=1`:选中 production consumer 产生 1 个 PipelineRun。 +- `pipelineElapsedSeconds=177`。 +- `deliveryElapsedSeconds=302`:从 PR merge `12:49:15Z` 到 Argo operation 完成 `12:54:17Z`。 +- `rolloutCount=1`:production API/worker/Web 同一 GitOps revision 收敛。 +- `nativeIterationCount=1`:复用 R6.7 L1 和 R6.8 L2 验证结果。 +- `meteredResourceCost` 未提供。 diff --git a/docs/MDTODO/details/platform-resource-governance/R1.1_Task_Report.md b/docs/MDTODO/details/platform-resource-governance/R1.1_Task_Report.md new file mode 100644 index 00000000..1d844dcc --- /dev/null +++ b/docs/MDTODO/details/platform-resource-governance/R1.1_Task_Report.md @@ -0,0 +1,76 @@ +# R1.1 NC01 Tekton 大对象事故修复报告 + +## 结论 + +2026-07-17,NC01 UniDesk 中断已恢复,造成控制面压力的 HWLAB Tekton PipelineRun 重复内联实现已通过正常 PR 和 PaC 自动事件完成长期减载。新 PipelineRun 总对象缩小约 62%,spec 与 status.pipelineSpec 分别缩小约 75%;真实构建成功,NC01 节点与 UniDesk 原入口持续健康。 + +## 事故影响与短期恢复 + +- 事故窗口:2026-07-17 10:15-10:57 CEST。 +- 影响:NC01 单节点 k3s 进入 NodeNotReady,unidesk namespace 的 backend-core、frontend、Decision Center 和 Todo Note 被驱逐,backend-core Service 一度无 endpoint,master server/provider ingress 不可用。 +- 恢复动作: + - 清理已被驱逐、卡在删除态及无 sandbox 的遗留 backend-core Pod; + - 在 Pod 级恢复无效后重启本机 k3s,恢复 API、kubelet 与 containerd 调和; + - 通过受控 cleanup 删除 7 个已终态 PipelineRun,保留 active run、最新成功证据、业务 PVC、Secret 与 runtime workload。 +- 短期缓解不作为最终修复依据;长期修复由后续正常 PR 自动交付事件验收。 + +## 根因 + +故障对象 hwlab-ci/hwlab-nc01-v03-ci-poll-p9m8z 的 JSON 约 1.01 MB,其中 spec 约 501 KB,Tekton 又将完整 Pipeline spec 写入 status.pipelineSpec,约 504 KB。HWLAB renderer 为 14 个服务分别生成相同的大型 taskSpec.steps[].script,导致单个 PipelineRun 在 spec/status 双份持久化并随状态更新反复写入。 + +事故窗口证据: + +- Kine compaction 单次持续 8.54 秒; +- node lease 写入延迟约 5.72 秒; +- 853 次 Slow SQL、1430 次 API Handler timeout; +- 633 次 stale resource-version、303 次 kubelet housekeeping timeout; +- k3s cgroup 曾有 1619 个任务,内存峰值约 7.9 GiB,swap 使用约 4.6 GiB; +- state.db 约 400 MB,WAL 约 357 MB。 + +结论是大型 PipelineRun、高频状态写入与 Kine/SQLite compaction 叠加,阻塞 API、node lease 和 kubelet 调和,最终导致 UniDesk 中断。TaskRun results 不是本次对象膨胀的主因。 + +## 长效修复 + +- HWLAB PR #2635,merge commit 7b3c53dde85dc361d71655e12c40dc9096d5aabe: + - 增加 reusable Task renderer、RBAC 与 reconciler 支持; + - NC01 v03 当前没有对应 control-plane reconciler,因此 TaskRef 不是本次实际运行路径。 +- HWLAB PR #2636,merge commit 6995394830834ad6af1f20d144d5300efa993f69: + - 将 14 个 per-service build task 收敛为一个 Tekton matrix task; + - 首次自动事件暴露 service-id 同时由 task params 与 matrix.params 注入的 admission 错误。 +- HWLAB PR #2637,merge commit e388061a3b748e05424fe6a293ed6b8a168f32a2: + - 删除重复参数声明,只由 matrix.params 注入 service-id; + - 正常 PaC 自动事件 hwlab-nc01-v03-ci-poll-zjdrs 成功,14 个 matrix TaskRun 全部成功。 + +matrix 是当前实际减载机制:大型脚本只在 PipelineRun 中保留一份,14 个服务仍保持独立执行语义,不依赖人工 PipelineRun、手工 apply、mirror sync 或补链。 + +## 验证 + +- renderer 测试:5 pass,0 fail。 +- 新 PipelineRun: + - 状态:Succeeded; + - commit:e388061a3b748e05424fe6a293ed6b8a168f32a2; + - 开始:2026-07-17T11:31:45Z; + - 完成:2026-07-17T11:34:39Z; + - 受控 history 显示 plan-artifacts、collect-artifacts、gitops-promote 均成功。 +- 对象体积: + - 旧对象:总计约 1,010,000 bytes,spec 约 501,000 bytes,status.pipelineSpec 约 504,000 bytes; + - 新对象:总计 383,570 bytes,spec 126,076 bytes,status.pipelineSpec 127,871 bytes; + - 总对象缩小约 62%,两份 Pipeline spec 分别缩小约 75%。 +- source artifact: + - 精确 source commit 与 embedded PipelineRun 对齐; + - live standalone Pipeline 仍有旧默认 services/provenance 漂移; + - PaC 实际执行消费 embedded spec,故该漂移记录为 blocking=false 的残余 warning,不阻塞业务。 +- 最终 UniDesk 原入口: + - NC01 node Ready; + - backend-core、frontend、Decision Center、Todo Note 全部 1/1 Running; + - backend-core /health 返回 ok=true、dbReady=true; + - frontend /health 返回 HTTP 200、ok=true; + - provider ingress /health 返回 ok=true。 + +## 残余边界 + +- standalone Pipeline 漂移后续应由正常 source/GitOps 路径自然收敛,不人工 apply。 +- reusable Task/TaskRef 基础能力已保留,但在 NC01 v03 reconciler 未成为正式 owner 前不启用第二运行权威。 +- 对象体积、Kine/WAL、API 延迟与 retention 继续作为运维 warning 和事故证据,不新增阻塞业务的阈值门禁或自动重启机制。 + +关联:[UniDesk #2458](https://github.com/pikasTech/unidesk/issues/2458)、[HWLAB #2635](https://github.com/pikasTech/HWLAB/pull/2635)、[HWLAB #2636](https://github.com/pikasTech/HWLAB/pull/2636)、[HWLAB #2637](https://github.com/pikasTech/HWLAB/pull/2637)。 diff --git a/docs/MDTODO/details/sub2api-upstream-reliability/R2.12.1_Task_Report.md b/docs/MDTODO/details/sub2api-upstream-reliability/R2.12.1_Task_Report.md new file mode 100644 index 00000000..d10cc6e7 --- /dev/null +++ b/docs/MDTODO/details/sub2api-upstream-reliability/R2.12.1_Task_Report.md @@ -0,0 +1,25 @@ +# R2.12.1 任务报告 + +## 结论 + +- PK01 管理面显示的 CPU 92.3% 不代表当前持续高占用;现场两轮 `vmstat` 采样显示 CPU idle 分别为 83% 至 98% 和 65% 至 94%,load average 约 0.4,主机为 2 vCPU。 +- Sub2API 原生 diagnosis 复查已不再出现 CPU critical,仅保留 TTFT 和上游错误率告警,说明 CPU 告警已结束或属于瞬时/上一刷新窗口。 +- 未发现可安全终止的非 Sub2API 异常高 CPU 进程,因此没有杀进程。 + +## 进程归属 + +- `sub2api-app` 容器 CPU 约 11.4%,宿主 `/app/sub2api` 进程累计视图约 3.8%;容器 healthy。 +- PostgreSQL、`sub2api-redis`、Caddy、Docker/containerd 属于 Sub2API 直接依赖。 +- `unidesk-provider-gateway-pk01` 的 Bun 进程、Hysteria host proxy、frps、云安全 `YDService`、云监控 `barad_agent` 属于 PK01 平台基础设施,不可终止。 +- `pikanode`、Nginx logger、`met_server` 等其他容器当前 CPU 均低于 1%,没有异常终止依据。 + +## 验证 + +- `platform-infra sub2api status --target PK01`:target ok,app healthy,Redis running,local health 和 Caddy 均正常。 +- `platform-infra sub2api validate --target PK01`:app、Redis、PostgreSQL、本地健康和公网 Caddy 健康均通过。 +- 8 秒连续采样没有持续 runnable queue 或 I/O wait,最高单秒 CPU busy 约 35%,随后恢复到 6% 至 25%。 + +## 边界 + +- 未停止任何进程,未修改 Sub2API 源码、版本、账号池、配置、容器、代理或外部哨兵。 +- 若 CPU 92% 再次持续出现,应在告警仍活跃时立即采集同窗口进程和容器 CPU;本次已结束的瞬时峰值不能反推具体进程责任。 diff --git a/docs/MDTODO/details/sub2api-upstream-reliability/R2.18.2_Task_Report.md b/docs/MDTODO/details/sub2api-upstream-reliability/R2.18.2_Task_Report.md new file mode 100644 index 00000000..5d764220 --- /dev/null +++ b/docs/MDTODO/details/sub2api-upstream-reliability/R2.18.2_Task_Report.md @@ -0,0 +1,50 @@ +# R2.18.2 任务报告 + +## 结论 + +Sub2API 评分查询会给 PK01 带来显著的周期性 CPU 峰值,尤其是 ApiState 每 10 分钟执行的 8 小时账号评分。它不足以解释持续高 CPU,但与生产流量或人工刷新重叠时,足以把 2 vCPU 的 PK01 推近 90% 以上。应把评分、去重、TTFT 分位数、成本和 failover 关联迁到 NC01;PK01 只保留原生分页查询和最小必要数据库聚合。 + +## 实测证据 + +- PK01 为 2 vCPU、约 4 GiB 内存;调查结束时 load average 约 0.84/0.60/0.46,Sub2API 基线约 2% 至 4%,Redis约 0.2% 至 0.4%。 +- 原生 `ops diagnosis --time-range 1h` 约 2.4 秒,属于较轻的 dashboard 聚合。 +- `runtime errors --all-groups --since 1h` 约 17.1 秒;远端 job 约 16.0 秒。查询期间 Sub2API 峰值约 13%,远端 Python 约 9%,PostgreSQL 活动连接通常 1 至 2 个。 +- 单组 `runtime errors --group unidesk-codex-pool --since 8h` 约 31 秒。采样观察到: + - PostgreSQL 单 backend 瞬时约 70.2% CPU; + - Sub2API 容器约 12.5% 至 22.2% CPU; + - PK01 远端 Python 聚合约 8% 至 11.3% CPU; + - Redis保持约 0.2% 至 0.4%,不是主因。 +- ApiState 最近一次生产评分从 2026-07-17T09:06:02.920Z 到 09:06:58.786Z,耗时约 55.9 秒。 +- ApiState 配置为 8 小时窗口、10 分钟刷新;实现先调用一次 `--all-groups`,再以并发 2 对全部组执行详细评分。当前有 3 个组,两个 OpenAI 组各有 13 个账号。 + +## 根因 + +- UniDesk `runtime errors` 通过 `runRemoteCodexPoolScript` 把完整 Python 脚本放到 PK01 执行。 +- 详细评分在 PK01 内完成以下工作: + - 每组读取 dashboard overview、账号可用性和并发; + - 分页读取 request-errors; + - 对 7 类 system-log marker 分别分页搜索; + - 对组内每个账号逐页读取 `/api/v1/admin/usage?page_size=100`; + - 必要时读取容器日志回退; + - 在 PK01 Python 中完成事件关联、TTFT 分位数、评分和成本聚合。 +- 8 小时窗口当前约 1 万次请求,仅 usage 的理论分页下限就超过 100 页;逐账号分页会进一步增加请求次数。 +- ApiState 的并发 2 会让两个 OpenAI 组的重查询同时落到 PK01。共享账号最终虽在 NC01 合并为一行,但组内 usage、错误和日志查询仍分别执行。 +- Web 人工刷新当前由 API 进程直接执行,Temporal 周期刷新由 worker 执行;两者没有跨进程 single-flight,存在重叠风险。 + +## 优化方案 + +1. 保留 Sub2API 原生 admin/ops API 和数据库为事实源,不改 Sub2API 源码或版本。 +2. 将受控 CLI 拆为“PK01 薄采集 + NC01 本地聚合”: + - PK01 只执行登录、原生分页读取和最小字段裁剪; + - 原始分页结果返回 NC01; + - 账号去重、错误归因、failover join、TTFT 分位数、评分、成本和表格渲染全部在 NC01 执行。 +3. usage 改为每组/窗口一次分页,不再按账号逐个分页;在 NC01 按 `account_id` 分桶。 +4. 在 NC01 保存 8 小时滚动缓存和原生记录高水位,后续刷新只拉增量;窗口过期数据本地剔除。 +5. ApiState 保留 Temporal 为唯一周期刷新 owner;人工刷新也提交到同一 Temporal 路径并合并进行中的刷新,避免 API 与 worker 重叠。 +6. 迁移完成前的保守缓解是把详细组查询并发从 2 降为 1,并阻止人工刷新与周期刷新重叠。该措施只削峰,不解决远端聚合根因。 + +## 边界 + +- 本轮仅只读调查和源码分析。 +- 未修改 Sub2API 源码、版本、runtime、YAML、外部哨兵或生产运行面。 +- 未停止任何进程。 diff --git a/docs/MDTODO/hwlab-caserun.md b/docs/MDTODO/hwlab-caserun.md index 9975f157..9e4de748 100644 --- a/docs/MDTODO/hwlab-caserun.md +++ b/docs/MDTODO/hwlab-caserun.md @@ -87,3 +87,7 @@ P4 native smoke:启动本地 Temporal、HarnessRL API/worker 与前端测试 ### R4.6 [completed] P5 CI/CD 与原入口回归:补齐服务制品和 GitOps,仅以 v0.3 PR merge 触发 PaC/Tekton/GitOps/Argo,验证 selected public origin CaseRun software-smoke 且原 CI/CD 可用;依赖 R4.5,完成任务后将详细报告写入[任务报告](./details/hwlab-caserun/R4.6_Task_Report.md)。 + +### R4.7 [completed] + +执行 [HWLAB #2614](https://github.com/pikasTech/HWLAB/issues/2614) 的 CaseRun 独立 Web 页面与 L1 公网 native 调试收口:从 owning YAML 解析固定公网 IP、API/Web 端口和状态目录,由项目 CLI 管理 API/Web 启停重启状态日志,修复正式 AppShell native bootstrap,完成桌面、紧凑和移动端受控 readiness;禁止 localhost 交付入口、裸起进程或修改 CI/CD 基础设施,完成任务后将详细报告写入[任务报告](./details/hwlab-caserun/R4.7_Task_Report.md)。 diff --git a/docs/MDTODO/pikaoa-enterprise-platform.md b/docs/MDTODO/pikaoa-enterprise-platform.md index 8338394c..dd1ce12c 100644 --- a/docs/MDTODO/pikaoa-enterprise-platform.md +++ b/docs/MDTODO/pikaoa-enterprise-platform.md @@ -292,6 +292,30 @@ #### R6.5.1 [completed] 修正 [UniDesk #2438](https://github.com/pikasTech/unidesk/issues/2438) 的 PikaOA native PDF 预览探针误判,不降低其他业务 smoke,复用 PR #2436 完成定向测试与完整 native admin-mvp 验收,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.5.1_Task_Report.md)。 -### R6.6 +### R6.6 [in_progress] 回归正常 CI/CD、development API/worker/Web ready、部署态 CLI `--over-api` 与 Web 原入口,记录 native/CICD 效率证据并收口 issue,依赖 R6.5,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.6_Task_Report.md)。 + +### R6.7 [completed] + +实现 PikaOA 共享资源工作区:列表静默增量刷新、行切换只显示详情加载动画、详情栏占满 shell-content,并启动可公网访问且支持 HMR 的 L1 native API/worker/Web 测试端口,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.7_Task_Report.md)。 + +### R6.8 [completed] + +完成 PikaOA 共享资源工作区的 L2 Development 自动交付:由 PR #88 merge 唯一触发 `pikaoa-dev-nc01` PaC/GitOps,验证 development API/worker/Web、CLI `--over-api` 与 WebProbe 原入口;共享 CI/CD 观察面异常只做只读诊断,不修改公共基础设施,完成任务后将详细报告写入任务报告,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.8_Task_Report.md)。 + +### R6.9 [completed] + +在 R6.8 L2 Development 通过且用户明确授权后,将 PikaOA 当前 `master` 通过 `master -> release` PR 推广到 L3 Production,由 `pikaoa-nc01` PaC/GitOps 自动交付,并验证 production API/worker/Web、CLI `--over-api`、PDF/附件和 WebProbe 原入口;禁止人工 PipelineRun、mirror、Argo sync 或生产 patch,完成任务后将详细报告写入任务报告,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.9_Task_Report.md)。 + +### R6.10 [in_progress] + +修复 production 新建伙伴后详情读取失败:定位 `Cannot read properties of null (reading 'join')` 的 API/前端空数组边界,在共享资源归一化层修复并保持列表与详情通用工作区行为;先完成 L1 CLI/Web 验证,再经 master 自动交付 L2,L2 通过后按本次生产故障修复授权推广 L3,完成任务后将详细报告写入任务报告,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.10_Task_Report.md)。 + +#### R6.10.1 [in_progress] + +通过 [PikaOA Issue #91](https://github.com/pikainc/pikaoa/issues/91) 建立通用资源单选/多选下拉,按名称或 ID 模糊搜索且默认展示名称;替换所有要求手填全局资源 ID 的表单;合同与发票登记同时支持直接上传新 PDF 附件或搜索并关联已有附件,完成任务后将详细报告写入任务报告,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.10.1_Task_Report.md)。 + +#### R6.10.2 [in_progress] + +通过 [PikaOA Issue #95](https://github.com/pikainc/pikaoa/issues/95) 建立统一表单字段组件/模板,在新增、编辑、上传和 PDF 校对表单中明确标注必填与选填,并让资源选择器的可见标识、`aria-required`、清空行为与后端约束一致,完成任务后将详细报告写入[任务报告](./details/pikaoa-enterprise-platform/R6.10.2_Task_Report.md)。 diff --git a/docs/MDTODO/platform-resource-governance.md b/docs/MDTODO/platform-resource-governance.md index ac2afb01..b5045127 100644 --- a/docs/MDTODO/platform-resource-governance.md +++ b/docs/MDTODO/platform-resource-governance.md @@ -9,3 +9,7 @@ ## R1 [completed] 完成 [UniDesk #1710](https://github.com/pikasTech/unidesk/issues/1710) 的 NC01 平台资源治理首阶段:修复 Decision Center 子进程泄露,提供一次调用可见物理内存、PSS、swap、僵尸进程归属与 Kubernetes 对象压力的 overview;YAML-first 调优 Kafka 资源预算、扩容 64 GiB swap 安全垫,并把五个 CI namespace 的 Tekton 历史对象纳入自动留存,最终以真实 `MemAvailable` 超过 4 GiB、自动定时回收成功且不触碰 Webterm/PK01 为验收,历史实施明细引用 Web Observe R1.2.1-R1.2.8,完成任务后将详细报告写入[任务报告](./details/platform-resource-governance/R1_Task_Report.md)。 + +### R1.1 [completed] + +执行 [UniDesk #2458](https://github.com/pikasTech/unidesk/issues/2458):修复 NC01 上 HWLAB PaC/Tekton 大型内联 PipelineRun spec 被复制到 status.pipelineSpec、持续以约 1 MB 对象写入 Kine/SQLite并在 compaction 窗口阻塞 node lease,最终导致 k3s NodeNotReady 和 UniDesk backend-core endpoint 中断;将大段脚本拆入原生文件或复用 Task,补齐对象体积、Kine/WAL、API 延迟和 retention 的受控可见性,经正常 PR 自动交付并用 NC01 原入口验收,不以人工重启 k3s 或强删 Pod 作为最终修复,完成任务后将详细报告写入[任务报告](./details/platform-resource-governance/R1.1_Task_Report.md)。 diff --git a/docs/MDTODO/pr-merge-driven-automatic-delivery.md b/docs/MDTODO/pr-merge-driven-automatic-delivery.md index 42941c25..cab57e4f 100644 --- a/docs/MDTODO/pr-merge-driven-automatic-delivery.md +++ b/docs/MDTODO/pr-merge-driven-automatic-delivery.md @@ -123,6 +123,9 @@ ### R8.2 [completed] 继续解决 [UniDesk #2165](https://github.com/pikasTech/unidesk/issues/2165):单 consumer `status` 在默认 `taskrun-status-only` 模式未同步读取 plan logs 时,把预期缺失的 source observation 误报为阻断型 `pac-source-observation-inconsistent`,且 compact JSON 仍以 10,636 字节超过 10 KiB 输出预算;仅将可证明由默认日志延后造成的缺口降级为 `pac-artifact-log-evidence-deferred`、`blocking=false`,真实矛盾在显式日志证据存在时继续 fail-closed,并收敛默认 JSON 到预算内,保留 id-specific history 深度证据与现有 live authority,通过 L0 fixture、L1 native status text/JSON、正常 PR 自动交付验收,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R8.2_Task_Report.md)。 +### R8.3 [in_progress] + +继续解决 [UniDesk #2165](https://github.com/pikasTech/unidesk/issues/2165) 的 single-consumer `status` 可见性回归:远端观察只输出 started 后结束且缺少 success/partial/timeout/failure 终态摘要,而同 consumer `history` 已能返回 typed `pac-read-timeout`;保持目标 Kubernetes/Tekton/Argo/runtime 唯一 authority,不新增状态源或交付 mutation,修复所有退出分支的有界终态输出,并以一次目标侧最小 status smoke 验收,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R8.3_Task_Report.md)。 ## R9 [in_progress] 建立 [UniDesk #2181](https://github.com/pikasTech/unidesk/issues/2181):在 NC01 建立单一共享 HTTPS 边缘,将 HWLAB v0.3、HWLAB release、UniDesk 与 WebTerm 分别迁移至 `lab-dev.hwpod.com`、`lab.hwpod.com`、`unidesk.hwpod.com`、`term.hwpod.com`;保持 PikaOA 现有站点无中断,全部公开事实由职责明确的 owning YAML 与 `configRef` 唯一声明,原子生成、校验、reload,共享 Caddy 不形成第二 authority,禁止修改 PK01、禁止 Vitest,最终通过正常 PR 与自动 CI/CD/GitOps 收敛,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R9_Task_Report.md)。 diff --git a/docs/MDTODO/sub2api-upstream-reliability.md b/docs/MDTODO/sub2api-upstream-reliability.md index 39a65b54..5866f5cd 100644 --- a/docs/MDTODO/sub2api-upstream-reliability.md +++ b/docs/MDTODO/sub2api-upstream-reliability.md @@ -216,6 +216,9 @@ ### R2.12 [completed] 只读巡检 PK01 Sub2API 最近 2 小时运维情况:使用原生状态、智能诊断和所有分组客户可见错误概要,再对异常分组下钻账号评分、TTFT、错误归因、重试与切号效果;区分需立即调优、继续观察和非上游问题,给出配置优化建议但不修改源码、版本、runtime、YAML 或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.12_Task_Report.md)。 +#### R2.12.1 [completed] + +调查并缓解 PK01 宿主机 CPU 持续高占用:通过受控 Sub2API 状态和 `trans PK01` 只读采集容器、进程树、CPU 累计与短窗口占用,明确 Sub2API 及其 PostgreSQL、Redis、Caddy、代理、Docker/containerd/systemd 等依赖边界;只有确认属于非 Sub2API、非平台基础设施且可安全终止的异常进程时才停止,并立即复测 CPU、Sub2API status/validate 和用户入口,不修改 Sub2API 源码、版本、账号池或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.12.1_Task_Report.md)。 ### R2.13 [completed] 将 PK01 Sub2API 从 v0.1.155 受控升级到 v0.1.156:只修改 PK01 target 的 owning YAML 镜像版本,完成 status、plan、apply dry-run、镜像预拉、受控 apply、状态、validate、公开健康和既有消费配置最小 Responses smoke 验收;保持账号池 runtime、Codex pool、代理、外部哨兵与其他服务原样,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.13_Task_Report.md)。 @@ -234,6 +237,13 @@ ### R2.18 [completed] 基于 R2.16 优先级和 R2.17 通用 failover 模板下发后约 3 小时的新窗口,再按 PK01 Sub2API 所有分组的用户可见错误与人民币边际成本调优全局 priority:使用原生 runtime errors 聚合完整账号名称、评分、请求、Token、API USD、成本、可归因失败、TTFT、规则命中、切号和置信度,区分盈利池与自用池;只对新窗口证据充分的账号做精准 dry-run、确认写入和回读,不重复计入旧错误,不修改版本、源码、容量、代理、凭据、分组、可调度、临时规则或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.18_Task_Report.md)。 +#### R2.18.1 [in_progress] + +继续调优 PK01 Sub2API 用户可见错误和全局 priority:内部 monitor 使用真实 API key 和正式入口产生的错误与外部客户错误同等纳入正式分析,只标注调用方而不剔除;以近 2 小时所有分组错误确认主因,以近 8 小时账号评分、可归因失败率、TTFT、Token、API USD、人民币边际成本、切号恢复和置信度形成候选;仅通过官方 runtime/YAML 能力做精准 dry-run、确认和回读,不修改源码、版本、代理、凭据、分组或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.18.1_Task_Report.md)。 + +#### R2.18.2 [completed] + +只读量化 Sub2API 查询操作对 PK01 的 CPU、PostgreSQL、Redis、网络和查询时延影响,区分原生 API/数据库聚合与 CLI 远端聚合;若确认聚合在 PK01 且产生显著压力,设计把最小原生分页数据拉到 NC01 本地聚合的方案,不修改生产、Sub2API 源码、版本、runtime、YAML 或外部哨兵,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.18.2_Task_Report.md)。 #### R2.18.3 [completed] 按 L0→L1→L2→L3 将 Sub2API 账号评分改为 NC01 本地聚合:ApiState 通过原生 admin API 按组分页采集最小数据,NC01 完成账号分桶、错误归因、TTFT、成本、failover 和跨分组去重;周期与手工刷新统一走 Temporal single-flight,先完成函数和 native 回归,再通过标准 CI/CD 完成开发集群验证,并在取得本次生产授权后滚动 L3,完成任务后将详细报告写入[任务报告](./details/sub2api-upstream-reliability/R2.18.3_Task_Report.md)。 diff --git a/project-management/PJ2026-01/specs/PJ2026-0103-harness-rl.md b/project-management/PJ2026-01/specs/PJ2026-0103-harness-rl.md index 9c68a236..6fbef91c 100644 --- a/project-management/PJ2026-01/specs/PJ2026-0103-harness-rl.md +++ b/project-management/PJ2026-01/specs/PJ2026-0103-harness-rl.md @@ -206,6 +206,18 @@ sequenceDiagram 取消必须进入 Temporal signal/cancel 语义,并由 workflow 在 registry 中写入 durable `canceled` 终态。API 进程重启不得影响 workflow;worker 重启后必须从 Temporal history 恢复并继续未完成 activity。activity retry 不得复制 AgentRun command、HWPOD operation 或 artifact;已有下游 identity 必须通过稳定幂等键恢复。 +### 5.4 Web CaseRun 独立页面与设计契约 + +Web CaseRun 必须作为独立产品页面提供,入口为 `/caserun`,运行详情支持稳定深链 `/caserun/runs/:runId`。它不是 Workbench 的附属面板,也不得要求用户先进入代码编辑或 Agent 对话上下文。导航、直接访问和刷新深链都必须使用同一 `workbench.code` 权限边界与同一 CaseRun API。 + +页面采用面向重复操作的实验室控制台布局:顶部命令栏承载页面身份、模式/来源、刷新、复制 runId 和启动动作;状态摘要集中显示当前 run 的终态、阶段、耗时和证据完整度;主体使用左侧 case/HWPOD 选择、中间运行时间线与事件流、右侧 aggregate/artifact/trace/HWPOD 证据检查器的三栏结构。窄屏按“选择上下文→运行状态→证据”顺序折叠,不能依靠横向滚动才能完成核心操作。 + +页面可见文案应以中文为主;`CaseRun`、`HarnessRL`、`HWPOD`、`runId`、`traceId`、`SHA256` 和 API 字段等领域标识可以保留原文,但状态、阶段、操作、空态和错误说明必须提供中文表达。页面必须采用受限工作区:AppShell 主内容区和 CaseRun 页面根容器不产生全局滚动,命令栏与状态摘要稳定可见,case 列表、事件流、阶段轨道和证据检查器只在所属 pane 内滚动;移动端按既定顺序在工作区内部纵向滚动,不把滚动责任退回 document。 + +视觉设计应服务于硬件执行的扫描、比较和故障定位:沿用 Cloud Web 控制台 token、清晰的状态色、紧凑的 IBM Plex Sans/Mono 信息层级、稳定的表格/时间线间距和可识别的图标按钮;不得使用营销式 hero、紫色渐变、装饰性背景块、嵌套卡片或把关键结果藏在视觉装饰中。按钮、标签、状态、时间线和证据区必须在桌面、紧凑桌面与移动视口下保持文本不重叠、控件不跳动和可读。 + +页面只投影 API 返回的 `status`、`stage`、`terminal`、`events`、`aggregate` 和 `references`。客户端不得依据 elapsed time、HTTP 状态码、事件数量、fixture 名称或本地计时器合成终态、成功或证据完整度。native test fixture 只能作为显式 `mode=native-test` 的开发输入,并且页面应与真实 API 的 DTO 和错误契约保持一致。 + ## 6. 原子需求 ### 6.1 HARNESS-L1-REQ-001 CaseRun 执行语义 @@ -270,6 +282,6 @@ Web CaseRun 的执行事实必须按职责分仓。AgentRun run、command、trac Web CaseRun 的最小完成标准应先覆盖 compile-only case:同一 run 能从 Web 或 web-probe 提交,终态包含 caseId、runId、hwpodId、nodeId、AgentRun traceId、HWPOD operation result 引用、artifact manifest hash 和 aggregate。download+UART 与 Arm2D 场景可作为增强阶段,但其证据仍必须进入同一 CaseRun manifest 和 aggregate 语义。 -CaseRun 必须提供 repo-native 敏捷开发形态。后端应能在 Kubernetes 外独立启动 Temporal 开发运行时、HarnessRL API 和 worker,并分别支持 watch/reload;CLI 应提供默认 local 模式和显式 `--over-api` 模式,两者共享 application service、DTO、typed error 和 submit-and-poll 合同;前端应能独立启动 Vite HMR 与可热重载的 native CaseRun test API,覆盖 queued、running、completed、failed、blocked、canceled、events 和 aggregate。native fixture 必须显式标记 `mode=native-test`,不得成为生产 fallback 或通过判定来源。 +CaseRun 必须提供 repo-native 敏捷开发形态。后端应能在 Kubernetes 外独立启动 Temporal 开发运行时、HarnessRL API 和 worker,并分别支持 watch/reload;CLI 应提供默认 local 模式和显式 `--over-api` 模式,两者共享 application service、DTO、typed error 和 submit-and-poll 合同;前端应能独立启动 Vite HMR 与可热重载的 native CaseRun test API,覆盖 queued、running、completed、failed、blocked、canceled、events 和 aggregate。native fixture 必须显式标记 `mode=native-test`,不得成为生产 fallback 或通过判定来源。L1 的固定端口、host、状态目录和服务组成必须由 YAML-first 配置声明,API、worker 和 Web 的启动、停止、重启、状态和日志必须由项目 CLI 管理;裸起脚本只作为 CLI 内部实现。L1 浏览器和用户入口必须从 owning YAML 解析出的公网 IP 与固定 port 访问,localhost 只允许作为进程健康 probe。 交付顺序必须先完成 native smoke,再回归原 CI/CD。native smoke 至少验证 software-smoke、cancel、API restart、worker restart recovery 和 CLI local/`--over-api` 合同等价;生产回归只由 `v0.3` PR merge 触发既有 GitHub webhook、Gitea snapshot、PaC、Tekton、GitOps/Argo 自动链,不得人工补跑或同步。 diff --git a/scripts/src/hwlab-native-development.ts b/scripts/src/hwlab-native-development.ts index 730f723f..0e7d0820 100644 --- a/scripts/src/hwlab-native-development.ts +++ b/scripts/src/hwlab-native-development.ts @@ -2,6 +2,7 @@ import { runCommand } from "./command"; import { readEnvSourceFile, requiredEnvValue } from "./secrets"; import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes"; +type Application = "workbench" | "caserun"; type Service = "api" | "worker" | "web"; type Action = "start" | "stop" | "restart" | "status" | "logs"; @@ -11,6 +12,7 @@ export function hwlabNativeDevelopmentHelp(): Record { command: "hwlab nodes native-development workbench", usage: [ "bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node --lane ", + "bun scripts/cli.ts hwlab nodes native-development caserun api|web start|stop|restart|status|logs --node --lane ", ], contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的公网 IP 和 port;bind/probe 地址不作为用户入口。", configPath: hwlabRuntimeLaneConfigPath(), @@ -19,37 +21,33 @@ export function hwlabNativeDevelopmentHelp(): Record { export function runHwlabNativeDevelopmentCommand(args: string[]): Record { if (args.length === 0 || args.includes("--help") || args.includes("-h")) return hwlabNativeDevelopmentHelp(); - const [application, serviceRaw, actionRaw] = args; - if (application !== "workbench") throw new Error("native-development currently supports workbench"); - const service = parseService(serviceRaw); + const [applicationRaw, serviceRaw, actionRaw] = args; + const application = parseApplication(applicationRaw); + const service = parseService(serviceRaw, application); const action = parseAction(actionRaw); const node = requiredOption(args, "--node"); const lane = requiredOption(args, "--lane"); if (!isHwlabRuntimeLane(lane)) throw new Error(`--lane must be declared in ${hwlabRuntimeLaneConfigPath()}`); const spec = hwlabRuntimeLaneSpecForNode(lane, node); - const native = spec.nativeDevelopment?.workbench; - if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench is required`); - const sourceCommitResult = runCommand(["git", "rev-parse", "HEAD"], spec.workspace, { timeoutMs: 5_000 }); - if (sourceCommitResult.exitCode !== 0 || !/^[0-9a-f]{40}$/iu.test(sourceCommitResult.stdout.trim())) { - throw new Error(`cannot resolve current HWLAB source commit from ${spec.workspace}`); - } - const env = hwlabNativeDevelopmentEnvironment(native, spec, sourceCommitResult.stdout.trim()); - const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", "service", service, action]; + const native = spec.nativeDevelopment?.[application]; + if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.${application} is required`); + const env = application === "workbench" ? workbenchEnvironment(spec, native as HwlabRuntimeNativeDevelopmentSpec["workbench"]) : hwlabCaseRunNativeDevelopmentEnvironment(native as NonNullable); + const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", application, "service", service, action]; const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } }); const payload = parsePayload(result.stdout); const publicEndpoint = service === "api" ? { ip: native.publicHost, port: native.api.port, url: `http://${native.publicHost}:${native.api.port}` } : service === "web" - ? { ip: native.publicHost, port: native.web.port, url: `http://${native.publicHost}:${native.web.port}/workbench` } + ? { ip: native.publicHost, port: native.web.port, url: `http://${native.publicHost}:${native.web.port}/${application === "workbench" ? "workbench" : "caserun"}` } : null; return { ok: result.exitCode === 0 && payload.ok !== false, - command: `hwlab nodes native-development workbench ${service} ${action}`, + command: `hwlab nodes native-development ${application} ${service} ${action}`, node, lane, service, action, - configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench`, + configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.${application}`, publicHostSource: native.publicHostRef, userEndpoint: publicEndpoint, serviceResult: payload, @@ -57,6 +55,12 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record { return parsed as Record; } -function parseService(value: string | undefined): Service { - if (value === "api" || value === "worker" || value === "web") return value; - throw new Error("native-development workbench service must be api, worker, or web"); +function parseApplication(value: string | undefined): Application { + if (value === "workbench" || value === "caserun") return value; + throw new Error("native-development application must be workbench or caserun"); +} + +function parseService(value: string | undefined, application: Application): Service { + if (value === "api" || value === "web" || (application === "workbench" && value === "worker")) return value; + throw new Error(`native-development ${application} service must be ${application === "workbench" ? "api, worker, or web" : "api or web"}`); } function parseAction(value: string | undefined): Action { @@ -131,3 +140,15 @@ function requiredOption(args: string[], option: string): string { if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`); return value; } + +function hwlabCaseRunNativeDevelopmentEnvironment(native: NonNullable): NodeJS.ProcessEnv { + return { + CASERUN_NATIVE_SERVICE_STATE_DIR: native.stateDir, + CASERUN_PUBLIC_HOST: native.publicHost, + CASERUN_PROBE_HOST: native.api.probeHost, + CASERUN_API_BIND_HOST: native.api.bindHost, + CASERUN_API_PORT: String(native.api.port), + CASERUN_WEB_BIND_HOST: native.web.bindHost, + CASERUN_WEB_PORT: String(native.web.port), + }; +} diff --git a/scripts/src/hwlab-node-help.ts b/scripts/src/hwlab-node-help.ts index d83528e0..6e803d24 100644 --- a/scripts/src/hwlab-node-help.ts +++ b/scripts/src/hwlab-node-help.ts @@ -50,6 +50,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu "bun scripts/cli.ts hwlab nodes hwpod-node plan --node G14-WSL --lane v03", "bun scripts/cli.ts hwlab nodes fake-model-provider plan --node D518 --lane v03 --provider fake-echo", "bun scripts/cli.ts hwlab nodes native-development workbench web status --node --lane ", + "bun scripts/cli.ts hwlab nodes native-development caserun api status --node --lane ", "bun scripts/cli.ts hwlab nodes secret status --node --lane --name ", "bun scripts/cli.ts hwlab nodes test-accounts status --node --lane ", "bun scripts/cli.ts hwlab nodes observability performance-summary --node --lane ", @@ -63,7 +64,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu "hwpod-preinstall": "Render YAML-first HWPOD preinstall configRefs, runtime mount targets, PM MDTODO source, and gateway profile status.", "hwpod-node": "通过 YAML 和 trans 部署、查询 Windows 原生 Python HWPOD 节点。", "fake-model-provider": "Materialize and operate YAML-declared fake Responses model providers for HWLAB/AgentRun sentinel checks.", - "native-development": "按 owning YAML 独立管理 L1 API、Worker 与 Web,并为 API/Web 披露公网 IP 和 port。", + "native-development": "按 owning YAML 独立管理 L1 Workbench/CaseRun API、Worker 与 Web,并为 API/Web 披露公网 IP 和 port。", secret: "Inspect YAML-declared runtime Secrets without printing secret values; writes are documented only by platform-maintenance help.", "test-accounts": "Prepare YAML-declared HWLAB admin/test account API keys with redacted sourceRef/fingerprint output.", observability: "Read runtime metrics and authenticated Web Performance summaries.", diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index 9800723e..10403eec 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -708,6 +708,13 @@ export interface HwlabRuntimeNativeDevelopmentSpec { }; }; }; + readonly caserun?: { + readonly publicHostRef: string; + readonly publicHost: string; + readonly stateDir: string; + readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number }; + readonly web: { readonly bindHost: string; readonly probeHost: string; readonly port: number }; + }; } export interface HwlabRuntimePipelineProvenanceSpec { @@ -1527,6 +1534,23 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati }, }, }, + ...(raw.caserun === undefined ? {} : { caserun: nativeCaseRunConfig(raw.caserun, `${path}.caserun`) }), + }; +} + +function nativeCaseRunConfig(value: unknown, path: string): NonNullable { + const raw = asRecord(value, path); + const publicHostRef = stringField(raw, "publicHostRef", path); + const publicHost = resolveConfigRefString(publicHostRef, `${path}.publicHostRef`); + if (isIP(publicHost) === 0) throw new Error(`${path}.publicHostRef must resolve to a public IP address`); + const api = asRecord(raw.api, `${path}.api`); + const web = asRecord(raw.web, `${path}.web`); + return { + publicHostRef, + publicHost, + stateDir: relativeWorkspacePathField(stringField(raw, "stateDir", path), `${path}.stateDir`), + api: { bindHost: stringField(api, "bindHost", `${path}.api`), probeHost: stringField(api, "probeHost", `${path}.api`), port: numberField(api, "port", `${path}.api`) }, + web: { bindHost: stringField(web, "bindHost", `${path}.web`), probeHost: stringField(web, "probeHost", `${path}.web`), port: numberField(web, "port", `${path}.web`) }, }; } diff --git a/scripts/src/hwlab-node-web-probe-native-readiness.test.ts b/scripts/src/hwlab-node-web-probe-native-readiness.test.ts index 10898e7f..9bdbfe6a 100644 --- a/scripts/src/hwlab-node-web-probe-native-readiness.test.ts +++ b/scripts/src/hwlab-node-web-probe-native-readiness.test.ts @@ -22,6 +22,12 @@ function fixtureProfile(): Record { criticalPathPrefixes: ["/api/"], screenshotName: "ready.png", outputLimits: { errors: 10, failedResponses: 10, textPreviewChars: 120 }, + layout: { + requireNoDocumentOverflow: true, + requireNoMainContentOverflow: true, + mainContentSelector: ".platform-content", + internalScrollSelectors: [".records"], + }, }; } @@ -35,7 +41,9 @@ test("native readiness profile is owned by lane YAML", () => { assert.equal(profile.interaction.observedSelector, ".platform-sidebar"); assert.equal(profile.interaction.observedAttribute, "data-collapsed"); assert.equal(spec.nativeDevelopment?.workbench.publicHost, "152.53.229.148"); - assert.equal(spec.nativeDevelopment?.workbench.web.port, 5173); + assert.equal(spec.nativeDevelopment?.workbench.web.port, 5174); + assert.equal(spec.webProbe?.nativeReadinessProfiles?.caserun?.layout?.requireNoDocumentOverflow, true); + assert.deepEqual(spec.webProbe?.nativeReadinessProfiles?.caserun?.layout?.internalScrollSelectors, [".caserun-case-list", ".caserun-events", ".evidence-body"]); }); test("native readiness profile rejects unknown fields", () => { @@ -51,6 +59,8 @@ test("generated native readiness script is valid and generic", () => { assert.match(script, /page\.on\("pageerror"/u); assert.match(script, /page\.on\("requestfailed"/u); assert.match(script, /interactionChanged/u); + assert.match(script, /noDocumentOverflow/u); + assert.match(script, /internalScrollPanesReady/u); assert.match(script, /jsonArtifact\("native-web-readiness\.json"/u); const source = readFileSync(rootPath("scripts/src/hwlab-node-web-probe-native-readiness.ts"), "utf8"); assert.doesNotMatch(source, /NC01|v03|152\.53\.229\.148|5173|#workspace|desktop-sidebar-toggle/u); @@ -60,7 +70,7 @@ test("native-readiness is one managed command resolved from owning YAML", () => const options = parseNodeWebProbeOptions(["native-readiness", "--node", "NC01", "--lane", "v03", "--profile", "workbench"]); assert.equal(options.action, "script"); if (options.action !== "script") return; - assert.equal(options.url, "http://152.53.229.148:5173"); + assert.equal(options.url, "http://152.53.229.148:5174"); assert.equal(options.originName, "custom"); assert.equal(options.viewport, "1920x1080"); assert.equal(options.commandTimeoutSeconds, 120); diff --git a/scripts/src/hwlab-node-web-probe-native-readiness.ts b/scripts/src/hwlab-node-web-probe-native-readiness.ts index 04ccd852..18f5e860 100644 --- a/scripts/src/hwlab-node-web-probe-native-readiness.ts +++ b/scripts/src/hwlab-node-web-probe-native-readiness.ts @@ -22,6 +22,12 @@ export interface HwlabRuntimeWebProbeNativeReadinessProfileSpec { readonly failedResponses: number; readonly textPreviewChars: number; }; + readonly layout?: { + readonly requireNoDocumentOverflow: boolean; + readonly requireNoMainContentOverflow: boolean; + readonly mainContentSelector: string; + readonly internalScrollSelectors: readonly string[]; + }; } export function parseWebProbeNativeReadinessProfiles( @@ -89,7 +95,7 @@ export default async function nativeReadiness({ page, goto, wait, screenshot, js await goto(config.path, { selectors: config.readySelectors, readinessTimeoutMs: config.navigationTimeoutMs, attempts: 2 }); if (config.settleMs > 0) await wait(config.settleMs); const before = await page.locator(config.interaction.observedSelector).getAttribute(config.interaction.observedAttribute); - const mounted = await page.evaluate(({ selectors, minBodyTextLength, textPreviewChars }) => { + const mounted = await page.evaluate(({ selectors, minBodyTextLength, textPreviewChars, layout }) => { const visible = (selector) => { const element = document.querySelector(selector); const rect = element?.getBoundingClientRect(); @@ -97,6 +103,27 @@ export default async function nativeReadiness({ page, goto, wait, screenshot, js return Boolean(rect && rect.width > 0 && rect.height > 0 && style?.display !== "none" && style?.visibility !== "hidden"); }; const bodyText = (document.body?.textContent || "").replace(/\s+/gu, " ").trim(); + const overflow = (element) => element === null ? null : ({ + clientWidth: element.clientWidth, + clientHeight: element.clientHeight, + scrollWidth: element.scrollWidth, + scrollHeight: element.scrollHeight, + horizontal: element.scrollWidth > element.clientWidth, + vertical: element.scrollHeight > element.clientHeight, + }); + const documentOverflow = overflow(document.documentElement); + const bodyOverflow = overflow(document.body); + const mainContentOverflow = layout ? overflow(document.querySelector(layout.mainContentSelector)) : null; + const internalScrollPanes = layout ? layout.internalScrollSelectors.map((selector) => { + const element = document.querySelector(selector); + const style = element ? getComputedStyle(element) : null; + return { selector, found: element !== null, overflow: overflow(element), overflowX: style?.overflowX ?? null, overflowY: style?.overflowY ?? null }; + }) : []; + const noDocumentOverflow = !layout?.requireNoDocumentOverflow || Boolean(documentOverflow && bodyOverflow + && !documentOverflow.horizontal && !documentOverflow.vertical && !bodyOverflow.horizontal && !bodyOverflow.vertical); + const noMainContentOverflow = !layout?.requireNoMainContentOverflow || Boolean(mainContentOverflow + && !mainContentOverflow.horizontal && !mainContentOverflow.vertical); + const internalScrollPanesReady = !layout || internalScrollPanes.every((item) => item.found && ["auto", "scroll"].includes(item.overflowY)); return { readyState: document.readyState, selectorsVisible: selectors.map((selector) => ({ selector, visible: visible(selector) })), @@ -105,15 +132,17 @@ export default async function nativeReadiness({ page, goto, wait, screenshot, js bodyTextSufficient: bodyText.length >= minBodyTextLength, finalPath: location.pathname + location.search, title: document.title, + layout: { documentOverflow, bodyOverflow, mainContentOverflow, internalScrollPanes, noDocumentOverflow, noMainContentOverflow, internalScrollPanesReady }, }; - }, { selectors: config.readySelectors, minBodyTextLength: config.minBodyTextLength, textPreviewChars: config.outputLimits.textPreviewChars }); + }, { selectors: config.readySelectors, minBodyTextLength: config.minBodyTextLength, textPreviewChars: config.outputLimits.textPreviewChars, layout: config.layout }); await page.locator(config.interaction.selector).click({ timeout: config.navigationTimeoutMs }); if (config.settleMs > 0) await wait(config.settleMs); const after = await page.locator(config.interaction.observedSelector).getAttribute(config.interaction.observedAttribute); const interactionChanged = before !== after; const screenshotArtifact = await screenshot(config.screenshotName); const selectorsReady = mounted.selectorsVisible.every((item) => item.visible); - const ok = selectorsReady && mounted.bodyTextSufficient && interactionChanged + const layoutReady = mounted.layout.noDocumentOverflow && mounted.layout.noMainContentOverflow && mounted.layout.internalScrollPanesReady; + const ok = selectorsReady && mounted.bodyTextSufficient && interactionChanged && layoutReady && consoleErrorCount === 0 && pageErrorCount === 0 && failedResponseCount === 0; const evidence = { ok, @@ -132,7 +161,7 @@ export default async function nativeReadiness({ page, goto, wait, screenshot, js valuesRedacted: true, }; const report = await jsonArtifact("native-web-readiness.json", evidence); - recordStep("native-web-readiness", { ok, selectorsReady, bodyTextSufficient: mounted.bodyTextSufficient, interactionChanged, consoleErrorCount, pageErrorCount, failedResponseCount }); + recordStep("native-web-readiness", { ok, selectorsReady, bodyTextSufficient: mounted.bodyTextSufficient, interactionChanged, layoutReady, consoleErrorCount, pageErrorCount, failedResponseCount }); return { ...evidence, report }; } `; @@ -140,7 +169,7 @@ export default async function nativeReadiness({ page, goto, wait, screenshot, js function nativeReadinessProfile(value: unknown, path: string): HwlabRuntimeWebProbeNativeReadinessProfileSpec { const raw = object(value, path); - onlyKeys(raw, ["scheme", "path", "navigationTimeoutMs", "settleMs", "commandTimeoutSeconds", "viewport", "readySelectors", "minBodyTextLength", "interaction", "failedResponseStatus", "criticalPathPrefixes", "screenshotName", "outputLimits"], path); + onlyKeys(raw, ["scheme", "path", "navigationTimeoutMs", "settleMs", "commandTimeoutSeconds", "viewport", "readySelectors", "minBodyTextLength", "interaction", "failedResponseStatus", "criticalPathPrefixes", "screenshotName", "outputLimits", "layout"], path); const viewport = object(raw.viewport, `${path}.viewport`); onlyKeys(viewport, ["width", "height"], `${path}.viewport`); const interaction = object(raw.interaction, `${path}.interaction`); @@ -148,6 +177,7 @@ function nativeReadinessProfile(value: unknown, path: string): HwlabRuntimeWebPr const limits = object(raw.outputLimits, `${path}.outputLimits`); onlyKeys(limits, ["errors", "failedResponses", "textPreviewChars"], `${path}.outputLimits`); const screenshotName = boundedText(raw.screenshotName, `${path}.screenshotName`, 120); + const layout = raw.layout === undefined ? undefined : nativeReadinessLayout(raw.layout, `${path}.layout`); if (!/^[A-Za-z0-9._-]+\.png$/u.test(screenshotName)) throw new Error(`${path}.screenshotName must be a safe PNG filename`); return { scheme: enumText(raw.scheme, `${path}.scheme`, ["http", "https"]), @@ -167,6 +197,18 @@ function nativeReadinessProfile(value: unknown, path: string): HwlabRuntimeWebPr criticalPathPrefixes: textArray(raw.criticalPathPrefixes, `${path}.criticalPathPrefixes`, 1, 20, 300).map((item, index) => absolutePath(item, `${path}.criticalPathPrefixes[${index}]`)), screenshotName, outputLimits: { errors: integer(limits.errors, `${path}.outputLimits.errors`, 1, 100), failedResponses: integer(limits.failedResponses, `${path}.outputLimits.failedResponses`, 1, 100), textPreviewChars: integer(limits.textPreviewChars, `${path}.outputLimits.textPreviewChars`, 20, 2000) }, + layout, + }; +} + +function nativeReadinessLayout(value: unknown, path: string): NonNullable { + const raw = object(value, path); + onlyKeys(raw, ["requireNoDocumentOverflow", "requireNoMainContentOverflow", "mainContentSelector", "internalScrollSelectors"], path); + return { + requireNoDocumentOverflow: boolean(raw.requireNoDocumentOverflow, `${path}.requireNoDocumentOverflow`), + requireNoMainContentOverflow: boolean(raw.requireNoMainContentOverflow, `${path}.requireNoMainContentOverflow`), + mainContentSelector: boundedText(raw.mainContentSelector, `${path}.mainContentSelector`, 500), + internalScrollSelectors: textArray(raw.internalScrollSelectors, `${path}.internalScrollSelectors`, 1, 20, 500), }; } @@ -175,6 +217,7 @@ function onlyKeys(value: Record, allowed: readonly string[], pa function boundedText(value: unknown, path: string, max: number): string { if (typeof value !== "string" || value.length < 1 || value.length > max) throw new Error(`${path} must be a 1-${max} character string`); return value; } function simpleId(value: string, path: string): string { if (!/^[a-z][a-z0-9-]*$/u.test(value)) throw new Error(`${path} must be a lower-case id`); return value; } function integer(value: unknown, path: string, min: number, max: number): number { if (!Number.isInteger(value) || Number(value) < min || Number(value) > max) throw new Error(`${path} must be an integer from ${min} to ${max}`); return Number(value); } +function boolean(value: unknown, path: string): boolean { if (typeof value !== "boolean") throw new Error(`${path} must be a boolean`); return value; } function enumText(value: unknown, path: string, allowed: readonly T[]): T { if (typeof value !== "string" || !allowed.includes(value as T)) throw new Error(`${path} must be one of ${allowed.join(", ")}`); return value as T; } function absolutePath(value: unknown, path: string): string { const text = boundedText(value, path, 1000); if (!text.startsWith("/")) throw new Error(`${path} must start with /`); return text; } function textArray(value: unknown, path: string, min: number, max: number, itemMax: number): string[] { if (!Array.isArray(value) || value.length < min || value.length > max) throw new Error(`${path} must contain ${min}-${max} items`); return value.map((item, index) => boundedText(item, `${path}[${index}]`, itemMax)); } diff --git a/scripts/src/hwlab-node/web-probe-observe.ts b/scripts/src/hwlab-node/web-probe-observe.ts index 66d6198e..512fd574 100644 --- a/scripts/src/hwlab-node/web-probe-observe.ts +++ b/scripts/src/hwlab-node/web-probe-observe.ts @@ -1048,6 +1048,12 @@ function compactWebProbeScreenshotPageSummary(value: Record | n }, consoleCount: value.consoleCount ?? null, requestFailureCount: value.requestFailureCount ?? null, + pageErrorCount: value.pageErrorCount ?? null, + failedResponseCount: value.failedResponseCount ?? null, + consoleMessages: Array.isArray(value.consoleMessages) ? value.consoleMessages.slice(0, 5).map(record) : [], + requestFailures: Array.isArray(value.requestFailures) ? value.requestFailures.slice(0, 5).map(record) : [], + pageErrors: Array.isArray(value.pageErrors) ? value.pageErrors.slice(0, 5).map(record) : [], + failedResponses: Array.isArray(value.failedResponses) ? value.failedResponses.slice(0, 5).map(record) : [], }; } @@ -1108,6 +1114,8 @@ if (!url || !screenshotPath) throw new Error("missing screenshot URL or path"); const consoleMessages = []; const requestFailures = []; +const pageErrors = []; +const failedResponses = []; const launchOptions = { headless: true, args: ["--disable-gpu", "--no-sandbox"], @@ -1125,6 +1133,13 @@ page.on("console", (message) => { page.on("requestfailed", (request) => { if (requestFailures.length < 20) requestFailures.push({ url: request.url().slice(0, 240), method: request.method(), failure: request.failure()?.errorText || null }); }); +page.on("pageerror", (error) => { + if (pageErrors.length < 20) pageErrors.push({ message: String(error?.message || error).slice(0, 240) }); +}); +page.on("response", (response) => { + if (response.status() < 400 || failedResponses.length >= 20) return; + failedResponses.push({ url: response.url().slice(0, 240), status: response.status(), method: response.request().method() }); +}); let status = null; const response = await page.goto(url, { timeout, waitUntil }); @@ -1190,8 +1205,12 @@ let status = null; layout, consoleCount: consoleMessages.length, requestFailureCount: requestFailures.length, + pageErrorCount: pageErrors.length, + failedResponseCount: failedResponses.length, consoleMessages: consoleMessages.slice(0, 5), requestFailures: requestFailures.slice(0, 5), + pageErrors: pageErrors.slice(0, 5), + failedResponses: failedResponses.slice(0, 5), valuesRedacted: true, })); } finally { diff --git a/scripts/src/hwlab-node/web-probe-origin.ts b/scripts/src/hwlab-node/web-probe-origin.ts index 34a33a05..e8a4bf9a 100644 --- a/scripts/src/hwlab-node/web-probe-origin.ts +++ b/scripts/src/hwlab-node/web-probe-origin.ts @@ -92,7 +92,7 @@ function resolveCustomWebProbeOrigin(spec: HwlabRuntimeLaneSpec, value: string): originName: "custom", originMode: "custom-url", originConfigPath: null, - url: normalizeHttpOrigin(value, "web-probe --url"), + url: normalizeCustomHttpUrl(value, "web-probe --url"), browserProxyMode, browserProxyModeSource: spec.webProbe?.browserProxyMode === undefined ? "default" : "yaml-web-probe", }; @@ -120,3 +120,16 @@ function normalizeHttpOrigin(value: string, source: string): string { if (parsed.username || parsed.password || parsed.search || parsed.hash) throw new Error(`${source} must not contain credentials, query, or fragment`); return parsed.toString().replace(/\/$/u, ""); } + +function normalizeCustomHttpUrl(value: string, source: string): string { + let parsed: URL; + try { + parsed = new URL(value); + } catch (error) { + throw new Error(`${source} must be an absolute HTTP(S) URL: ${error instanceof Error ? error.message : String(error)}`); + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`${source} must use http or https`); + if (parsed.username || parsed.password || parsed.hash) throw new Error(`${source} must not contain credentials or fragment`); + if (parsed.pathname === "/" && parsed.search === "") return parsed.toString().replace(/\/$/u, ""); + return parsed.toString(); +}