@harmonyos-arkts/d2h 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +40 -0
- package/agents/android-to-hmos-00-orchestrator.md +3 -315
- package/agents/d2h.md +53 -38
- package/contracts/workflow/artifacts.md +22 -0
- package/contracts/workflow/delivery.md +54 -0
- package/contracts/workflow/dispatch.md +26 -0
- package/contracts/workflow/execution.md +125 -0
- package/contracts/workflow/generation.md +40 -0
- package/contracts/workflow/orchestration.md +49 -0
- package/contracts/workflow/overview.md +58 -0
- package/contracts/workflow/plan.md +119 -0
- package/contracts/workflow/scope.md +151 -0
- package/dist/bin/install-opencode.js +34 -15
- package/dist/src/install-codex.js +131 -0
- package/dist/src/install-opencode.js +5 -5
- package/dist/src/workflow.js +54 -0
- package/docs/architecture-plan-guide.zh-CN.md +70 -0
- package/docs/incremental-spec-evaluation-design.md +670 -0
- package/docs/spec-plan-guide.md +226 -0
- package/docs/workflow-decoupling.md +59 -0
- package/opencode/interactive-tools/advance-progress-demo.mjs +21 -0
- package/opencode/interactive-tools/browser-host.ts +110 -0
- package/opencode/interactive-tools/core.ts +242 -0
- package/opencode/interactive-tools/demo-workflow.ts +55 -0
- package/opencode/interactive-tools/demo.mjs +37 -0
- package/opencode/interactive-tools/package-lock.json +1876 -0
- package/opencode/interactive-tools/package.json +23 -0
- package/opencode/interactive-tools/panel.ts +40 -0
- package/opencode/interactive-tools/prepare-demo.mjs +17 -0
- package/opencode/interactive-tools/prepare-progress-demo.mjs +13 -0
- package/opencode/interactive-tools/progress.ts +118 -0
- package/opencode/interactive-tools/server-legacy.mjs +2 -0
- package/opencode/interactive-tools/server.mjs +40 -0
- package/opencode/interactive-tools/tui.tsx +230 -0
- package/package.json +9 -1
- package/schemas/native-libs-analysis-report.schema.json +695 -0
- package/schemas/native-libs-verify-report.schema.json +566 -0
- package/schemas/spec-native-libs-shard.schema.json +110 -0
- package/skills/arkts-icon-sizing/SKILL.md +0 -10
- package/skills/cpp/d2h-native-library-analyse/SKILL.md +91 -0
- package/skills/cpp/d2h-native-library-analyse/scripts/analyse-android-so.py +630 -0
- package/skills/cpp/d2h-native-library-build/SKILL.md +403 -0
- package/skills/cpp/d2h-native-library-build/references/build-config-mapping.md +180 -0
- package/skills/cpp/d2h-native-library-build/references/jni-to-napi-mapping.md +74 -0
- package/skills/cpp/d2h-native-library-build/scripts/check-android-so.py +580 -0
- package/skills/cpp/d2h-native-library-verify/SKILL.md +111 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-build-sign-run.md +67 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/app.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/AppScope/resources/base/element/string.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/README.md +25 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/build-profile.json5 +24 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/build-profile.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/hvigorfile.ts +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/oh-package.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/entryability/EntryAbility.ets +5 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/Index.ets +3 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/ets/pages/NativeVerifyDemo.ets +276 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/module.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/color.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/element/string.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/main/resources/base/profile/main_pages.json +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/FFmpegVerify.test.ets +176 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/ets/test/List.test.ets +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/entry/src/ohosTest/module.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigor/hvigor-config.json5 +1 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/hvigorfile.ts +2 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-project-template/oh-package.json5 +9 -0
- package/skills/cpp/d2h-native-library-verify/references/demo-template.md +176 -0
- package/skills/cpp/d2h-native-library-verify/references/hypium-test-template.md +163 -0
- package/skills/cpp/d2h-native-library-verify/scripts/__pycache__/native_common.cpython-313.pyc +0 -0
- package/skills/cpp/d2h-native-library-verify/scripts/device-verify.py +304 -0
- package/skills/cpp/d2h-native-library-verify/scripts/native_common.py +341 -0
- package/skills/cpp/d2h-native-library-verify/scripts/verify-ohos-native.py +1099 -0
- package/skills/d2h-app-solution/SKILL.md +198 -0
- package/skills/d2h-app-solution/agents/openai.yaml +4 -0
- package/skills/d2h-app-solution/references/framework-evidence.md +117 -0
- package/skills/d2h-app-solution/references/hierarchy-rules.md +88 -0
- package/skills/d2h-app-solution/references/inventory-schema.md +305 -0
- package/skills/d2h-app-solution/references/technical-analysis.md +101 -0
- package/skills/d2h-app-solution/scripts/scan_repositories.py +350 -0
- package/skills/d2h-app-solution/scripts/validate_inventory.py +337 -0
- package/skills/d2h-architecture-plan/SKILL.md +35 -0
- package/skills/d2h-architecture-plan/references/directory-creation.md +43 -0
- package/skills/d2h-architecture-plan/references/hybrid-design.md +84 -0
- package/skills/d2h-architecture-plan/references/layout-reference.md +83 -0
- package/skills/d2h-architecture-plan/references/plan-contract.md +51 -0
- package/skills/d2h-architecture-plan/references/source-review.md +50 -0
- package/skills/d2h-cloud-knowledge/SKILL.md +28 -7
- package/skills/d2h-cloud-knowledge/kb_query.mjs +8 -0
- package/skills/d2h-dashboard/SKILL.md +56 -0
- package/skills/d2h-product-family-migration/SKILL.md +103 -0
- package/skills/d2h-product-family-migration/agents/openai.yaml +3 -0
- package/skills/d2h-product-family-migration/assets/incremental-plan.template.md +55 -0
- package/skills/d2h-product-family-migration/references/advanced/behavioral-verification.md +59 -0
- package/skills/d2h-product-family-migration/references/advanced/existing-skill-integration.md +30 -0
- package/skills/d2h-product-family-migration/references/advanced/packaging-and-verification.md +43 -0
- package/skills/d2h-product-family-migration/references/advanced/platform-probes.md +26 -0
- package/skills/d2h-product-family-migration/references/advanced/product-line-design.md +40 -0
- package/skills/d2h-product-family-migration/references/baseline-and-delta.md +50 -0
- package/skills/d2h-product-family-migration/tests/markdown-cases.md +76 -0
- package/skills/{d2h-page-develop → d2h-spec-develop}/SKILL.md +143 -126
- package/skills/d2h-spec-develop/agents/openai.yaml +7 -0
- package/skills/d2h-spec-develop/references/worker-workflow.md +26 -0
- package/skills/{d2h-page-spec-generate → d2h-spec-generate}/SKILL.md +24 -13
- package/skills/d2h-spec-generate/assets/spec-template.md +109 -0
- package/skills/d2h-spec-generate/references/from-plan.md +19 -0
- package/skills/d2h-spec-generate/references/spec-content.md +34 -0
- package/skills/{d2h-page-spec-generate → d2h-spec-generate}/scripts/build_batch_report.py +35 -84
- package/skills/d2h-spec-plan/SKILL.md +38 -0
- package/skills/d2h-spec-plan/agents/openai.yaml +4 -0
- package/skills/d2h-spec-plan/references/architecture-handoff.md +9 -0
- package/skills/d2h-spec-plan/references/plan-contract.md +13 -0
- package/skills/d2h-spec-plan/references/planning-method.md +46 -0
- package/skills/d2h-spec-plan/references/round-scope-assessment.md +49 -0
- package/skills/d2h-spec-plan/references/work-packets.md +61 -0
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +2 -2
- package/skills/d2h-utg-ai-generate/references/page-schema.json +2 -2
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +2 -2
- package/skills/d2h-utg-static-generate/SKILL.md +2 -2
- package/skills/managing-d2h-toolchain/environment-requirements.json +1 -1
- package/skills/managing-d2h-toolchain/toolchain.json +1 -1
- package/skills/preparing-migration-workspace/SKILL.md +32 -2
- package/workflow/__pycache__/artifact_bindings.cpython-313.pyc +0 -0
- package/workflow/__pycache__/spec_runtime.cpython-313.pyc +0 -0
- package/workflow/artifact_bindings.py +224 -0
- package/workflow/assets/dashboard.html +88 -0
- package/workflow/build_batch_report.py +8 -0
- package/workflow/collect_code_metrics.py +75 -0
- package/workflow/dashboard_overview.py +109 -0
- package/workflow/delivery_view.py +82 -0
- package/workflow/feature_plan.py +234 -0
- package/workflow/page_batches.py +223 -0
- package/workflow/spec_generation.py +120 -0
- package/workflow/spec_runtime.py +890 -0
- package/workflow/work_packets.py +85 -0
- package/skills/d2h-page-develop/agents/openai.yaml +0 -7
- package/skills/harmonyos-migration-playbook/SKILL.md +0 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +0 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +0 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +0 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +0 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +0 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +0 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +0 -38
- package/skills/native-library-substitution/SKILL.md +0 -385
- package/skills/native-library-substitution/references/native-library-substitution.json +0 -56906
- package/skills/native-library-substitution/references/native-library-substitution.md +0 -163
- package/skills/porting-cmake-native-libraries/SKILL.md +0 -57
- package/skills/porting-cmake-native-libraries/references/platform-adaptation.md +0 -49
- package/skills/porting-cmake-native-libraries/references/report-and-build.md +0 -60
|
@@ -19,14 +19,14 @@ description: 迁移主流程的 Android 静态分析入口,在 Skill 内调用
|
|
|
19
19
|
droid2hmos analyze android <android-source>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
2. 确认本次生成的 `<migration-workspace>/spec/index.json`、`spec/facts
|
|
22
|
+
2. 确认本次生成的 `<migration-workspace>/spec/index.json`、`spec/facts/`(含 `facts/native-libs.json` 原生库分片)和 `appgraph/app-graph.json` 存在,核对源码与分析结果。复用这份 AppGraph,不再执行第二次 `appgraph build`:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
25
|
python <skill>/scripts/generate.py --project-root <android-source> --workspace <migration-workspace>/utg --appgraph <migration-workspace>/appgraph/app-graph.json
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
3. 按下文完成源码核验、独立 AI 修补和统一重建。已有 UTG 工作区需显式使用 `--force`;源码或 AppGraph 变化时重新核验旧修补,不能沿用过期指纹。
|
|
29
|
-
4. 将 Facts 位置、受管分析结果、UTG 产物及未解析项交回主 Agent,由主 Agent 接续报告初始化与 Task Plan
|
|
29
|
+
4. 将 Facts 位置、受管分析结果、UTG 产物及未解析项交回主 Agent,由主 Agent 接续报告初始化与 Task Plan。`facts/native-libs.json` 的 `present`/`usageCase`/`libraries`/`loadCalls` 原样转交(其处置归 `d2h-native-library-build`,本 Skill 不做 ELF/符号深检)。UTG discoveries 不替代 Kernel 的 correction/review,也不替代受管分析证据。
|
|
30
30
|
|
|
31
31
|
Android 分析不运行 Gradle、不启用 `--resolve-dependencies`,不需要 APK 或 Android 设备。已有图缺失时报告分析产物不完整,不在主流程模式中静默切换到独立建图。
|
|
32
32
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"java": null,
|
|
9
9
|
"python-json5": null,
|
|
10
10
|
"appgraph": { "minimum": "0.0.11", "maximumExclusive": "0.0.12" },
|
|
11
|
-
"droid2hmos": { "minimum": "0.0.
|
|
11
|
+
"droid2hmos": { "minimum": "0.0.17", "maximumExclusive": "0.0.18" },
|
|
12
12
|
"hvigor": null,
|
|
13
13
|
"ohpm": null,
|
|
14
14
|
"devecocli": null
|
|
@@ -9,10 +9,38 @@ description: 在 Android 转 HarmonyOS 迁移开始或恢复时,通过 droid2h
|
|
|
9
9
|
|
|
10
10
|
工具安装、精确版本和升级由 `managing-d2h-toolchain` 负责。本 Skill 只消费已检查的 CLI;发现版本或能力不符时返回该 Skill 处理,不运行 npm。
|
|
11
11
|
|
|
12
|
-
只调用 `droid2hmos` 的 `status`、`preflight`、`config init/validate` 和按需的 `device check`。不要直接调用 Node 脚本、Gradle、Hvigor、Hypium、ADB、HDC 或 AppGraph 分析命令,也不要在 `ohos/` 尚不存在时猜测 HarmonyOS 构建配置。
|
|
12
|
+
只调用 `droid2hmos` 的 `status`、`preflight`、`config init/validate`、`progress init/update` 和按需的 `device check`。不要直接调用 Node/Python 脚本、Gradle、Hvigor、Hypium、ADB、HDC 或 AppGraph 分析命令,也不要在 `ohos/` 尚不存在时猜测 HarmonyOS 构建配置。
|
|
13
13
|
|
|
14
14
|
迁移产物(`.migration/`)统一位于 Android 工程父目录下的 `<工程名>-migration/.migration/`,由 CLI 创建和维护,不写入 Android 工程内部。下文所有 `.migration/...` 路径均相对该迁移工作区根目录。
|
|
15
15
|
|
|
16
|
+
## 阶段进度文件
|
|
17
|
+
|
|
18
|
+
CLI 确认迁移工作区且 `.migration/` 已存在后,立即初始化阶段进度文件:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
droid2hmos progress init <ANDROID_PROJECT>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
该命令原子创建 `.migration/progress.json`;恢复迁移时若文件已存在,只校验结构并保留全部已有时间和状态,禁止覆盖重置。文件固定包含以下七个有序阶段:
|
|
25
|
+
|
|
26
|
+
1. 初始化工程(`workspace-initialization`)
|
|
27
|
+
2. 功能清单生成(`feature-inventory`)
|
|
28
|
+
3. Android 工程分析(`android-analysis`)
|
|
29
|
+
4. HarmonyOS 工程架构设计(`harmony-architecture`)
|
|
30
|
+
5. spec 设计与生成(`spec-design-generation`)
|
|
31
|
+
6. 代码转换(`code-conversion`)
|
|
32
|
+
7. 测试与交付(`testing-delivery`)
|
|
33
|
+
|
|
34
|
+
每个阶段记录 `status`、`startedAt`、`endedAt`、`completed`、`lastUpdatedAt`、`details` 和 `blockers`;顶层 `summary` 汇总总数及 `pending/in_progress/blocked/completed` 数量和当前阶段。所有时间使用带时区偏移的 ISO 8601。初始化时“初始化工程”为 `in_progress`,其余阶段为 `pending`。
|
|
35
|
+
|
|
36
|
+
本 Skill 只更新“初始化工程”阶段。Preflight 和配置准备达到 `baselineReady=true` 时执行:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
droid2hmos progress update <ANDROID_PROJECT> --stage workspace-initialization --status completed --details "workspace, configuration, and non-device baseline are ready"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
若工程定位、配置或必要工具链使 `allowedNonDeviceWork=false`,将该阶段更新为 `blocked` 并用 `--blocker` 原样记录 CLI 返回的阻塞原因;设备单独不可用不阻止初始化阶段完成。后续编排者在对应阶段开始、完成或受阻时用同一 `update` 命令更新 `progress.json`,不得直接手工改写、删除阶段或伪造时间。
|
|
43
|
+
|
|
16
44
|
## 输入与事实来源
|
|
17
45
|
|
|
18
46
|
输入必须包含 Android Gradle 工程根目录。根目录应有 Gradle 标记,应用模块中应有 `AndroidManifest.xml`。
|
|
@@ -112,7 +140,7 @@ droid2hmos config validate <ANDROID_PROJECT>
|
|
|
112
140
|
],
|
|
113
141
|
"allowedNonDeviceWork": true,
|
|
114
142
|
"allowedActions": [],
|
|
115
|
-
"evidence": [".migration/preflight-report.json"],
|
|
143
|
+
"evidence": [".migration/preflight-report.json", ".migration/progress.json"],
|
|
116
144
|
"suggestedRemediation": []
|
|
117
145
|
}
|
|
118
146
|
```
|
|
@@ -128,4 +156,6 @@ droid2hmos config validate <ANDROID_PROJECT>
|
|
|
128
156
|
- `evidence`:本次读取或生成的状态、配置和 Preflight 报告;
|
|
129
157
|
- `suggestedRemediation`:CLI 给出的可执行补救,不臆造成功结论。
|
|
130
158
|
|
|
159
|
+
结构化结果的 `evidence` 必须包含本次读取或初始化的 `.migration/progress.json`。
|
|
160
|
+
|
|
131
161
|
本 Skill 不推进外部状态机、不更新迁移报告、不创建 checkpoint、不调用 `complete`,也不决定定向测试或全量回归。调用方根据上述结果决定下一步。
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Read-only bindings between analysis reports, versioned requirements and dashboard metadata."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
FEATURE_LIST_REQUIRED = ["trigger", "result", "priority", "evidence"]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def validate_legacy_report(data):
|
|
12
|
+
errors = []
|
|
13
|
+
for field in ("project", "feature_list", "summary"):
|
|
14
|
+
if field not in data:
|
|
15
|
+
errors.append("缺少必填字段: {}".format(field))
|
|
16
|
+
if errors:
|
|
17
|
+
return errors
|
|
18
|
+
if not isinstance(data["project"], str) or not data["project"].strip():
|
|
19
|
+
errors.append("project 必须是非空字符串")
|
|
20
|
+
if not isinstance(data["feature_list"], list) or not data["feature_list"]:
|
|
21
|
+
errors.append("feature_list 必须是非空数组")
|
|
22
|
+
else:
|
|
23
|
+
for idx, row in enumerate(data["feature_list"], 1):
|
|
24
|
+
if not isinstance(row, dict):
|
|
25
|
+
errors.append("feature_list 第 {} 行不是对象".format(idx))
|
|
26
|
+
continue
|
|
27
|
+
for field in FEATURE_LIST_REQUIRED:
|
|
28
|
+
value = str(row.get(field, "") or "").strip()
|
|
29
|
+
if not value:
|
|
30
|
+
errors.append("feature_list 第 {} 行缺少必填字段 {}".format(idx, field))
|
|
31
|
+
if str(row.get("priority", "")).strip() not in ("P0", "P1", "P2"):
|
|
32
|
+
errors.append("feature_list 第 {} 行 priority 必须是 P0/P1/P2".format(idx))
|
|
33
|
+
if not isinstance(data["summary"], list) or not data["summary"]:
|
|
34
|
+
errors.append("summary 必须是非空数组")
|
|
35
|
+
return errors
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def validate_inventory_contract(data):
|
|
39
|
+
"""Validate the exchanged artifact, not a producer's source-analysis workflow."""
|
|
40
|
+
errors = []
|
|
41
|
+
def require(value, message):
|
|
42
|
+
if not value:
|
|
43
|
+
errors.append(message)
|
|
44
|
+
require(data.get('schema_version') == '1.1', 'schema_version must be 1.1')
|
|
45
|
+
require(data.get('max_depth') == 4, 'max_depth must be 4')
|
|
46
|
+
repositories = data.get('repositories')
|
|
47
|
+
require(isinstance(repositories, list) and bool(repositories), 'repositories must be non-empty')
|
|
48
|
+
ids = []
|
|
49
|
+
for repo in repositories if isinstance(repositories, list) else []:
|
|
50
|
+
if not isinstance(repo, dict):
|
|
51
|
+
errors.append('repository must be an object')
|
|
52
|
+
continue
|
|
53
|
+
identity = repo.get('id')
|
|
54
|
+
require(isinstance(identity, str) and bool(identity), 'repository id required')
|
|
55
|
+
if isinstance(identity, str):
|
|
56
|
+
ids.append(identity)
|
|
57
|
+
require(isinstance(repo.get('path'), str) and Path(repo['path']).is_absolute(), 'repository path must be absolute')
|
|
58
|
+
require(len(set(ids)) == len(ids), 'duplicate repository id')
|
|
59
|
+
rows = data.get('features')
|
|
60
|
+
require(isinstance(rows, list) and bool(rows), 'features must be non-empty')
|
|
61
|
+
seen = set()
|
|
62
|
+
for i, row in enumerate(rows if isinstance(rows, list) else []):
|
|
63
|
+
label = f'features[{i}]'
|
|
64
|
+
if not isinstance(row, dict):
|
|
65
|
+
errors.append(label + ' must be an object')
|
|
66
|
+
continue
|
|
67
|
+
path = row.get('path')
|
|
68
|
+
valid_path = isinstance(path, list) and 1 <= len(path) <= 4 and all(isinstance(p, str) and bool(p.strip()) for p in path)
|
|
69
|
+
require(valid_path, label + '.path must contain 1-4 non-empty names')
|
|
70
|
+
context = row.get('entry_context')
|
|
71
|
+
require(isinstance(context, str) and bool(context.strip()), label + '.entry_context required')
|
|
72
|
+
if valid_path and isinstance(context, str):
|
|
73
|
+
key = (context, tuple(path))
|
|
74
|
+
require(key not in seen, label + ': duplicate path/context')
|
|
75
|
+
seen.add(key)
|
|
76
|
+
require(row.get('release_stage') in ('mvp', 'core', 'full', 'uncertain'), label + '.release_stage invalid')
|
|
77
|
+
require(isinstance(row.get('release_rationale'), str) and bool(row['release_rationale'].strip()), label + '.release_rationale required')
|
|
78
|
+
implementations = row.get('implementations')
|
|
79
|
+
require(isinstance(implementations, dict) and set(implementations) == set(ids), label + '.implementations must match repositories')
|
|
80
|
+
for repo_id, impl in implementations.items() if isinstance(implementations, dict) else []:
|
|
81
|
+
if not isinstance(impl, dict):
|
|
82
|
+
errors.append(label + ': implementation must be an object')
|
|
83
|
+
continue
|
|
84
|
+
require(impl.get('status') in ('implemented', 'partial', 'mock', 'unreachable', 'absent', 'uncertain'), label + ': status invalid')
|
|
85
|
+
require(impl.get('confidence') in ('high', 'medium', 'low'), label + ': confidence invalid')
|
|
86
|
+
technology = impl.get('technology_stack')
|
|
87
|
+
require(isinstance(technology, list) and all(isinstance(t, str) for t in technology), label + ': technology_stack must be strings')
|
|
88
|
+
require(isinstance(impl.get('evidence', []), list), label + ': evidence must be an array')
|
|
89
|
+
for field in ('external_dependencies', 'android_permissions'):
|
|
90
|
+
section = impl.get(field)
|
|
91
|
+
require(isinstance(section, dict), label + ': ' + field + ' must be an object')
|
|
92
|
+
if not isinstance(section, dict):
|
|
93
|
+
continue
|
|
94
|
+
require(section.get('status') in ('present', 'none', 'uncertain', 'not-applicable'), label + ': technical status invalid')
|
|
95
|
+
items = section.get('items', [])
|
|
96
|
+
require(isinstance(items, list) and all(isinstance(item, dict) and isinstance(item.get('name'), str) and bool(item['name']) for item in items), label + ': technical items invalid')
|
|
97
|
+
if field == 'external_dependencies':
|
|
98
|
+
require(isinstance(items, list) and all(isinstance(item, dict) and item.get('kind') in ('sdk', 'so') for item in items), label + ': dependency kind invalid')
|
|
99
|
+
return errors
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def inventory_view(data):
|
|
103
|
+
"""Read-only presentation projection; source references keep /features/N."""
|
|
104
|
+
result = {'project': data.get('title', ''), 'feature_list': [], 'summary': [],
|
|
105
|
+
'tech_stack': [], 'sdk_dependencies': [], 'permissions': [], 'user_inputs': [],
|
|
106
|
+
'uncovered': [{'component': 'inventory question', 'reason': str(q), 'evidence': ''}
|
|
107
|
+
for q in data.get('open_questions', [])]}
|
|
108
|
+
for i, feature in enumerate(data['features']):
|
|
109
|
+
evidence = []
|
|
110
|
+
implementations = feature.get('implementations', {})
|
|
111
|
+
for repo_id, impl in implementations.items():
|
|
112
|
+
evidence.extend(f"{repo_id}:{e.get('path', '')}:{e.get('line_start', '')}" for e in impl.get('evidence', []))
|
|
113
|
+
for tech in impl.get('technology_stack', []):
|
|
114
|
+
result['tech_stack'].append({'item': tech, 'conclusion': repo_id, 'evidence': '; '.join(evidence), 'impact': ' / '.join(feature['path'])})
|
|
115
|
+
for dep in impl.get('external_dependencies', {}).get('items', []):
|
|
116
|
+
result['sdk_dependencies'].append({'sdk': dep['name'], 'category': dep['kind'],
|
|
117
|
+
'harmony_support': '待确认', 'alternative': dep.get('purpose', ''),
|
|
118
|
+
'evidence': f"/features/{i}/implementations/{repo_id}/external_dependencies", 'provenance': dep.get('provenance', 'unknown')})
|
|
119
|
+
result['feature_list'].append({'trigger': feature.get('trigger', ''), 'result': feature.get('effect', ''),
|
|
120
|
+
'condition': ' / '.join(feature.get('conditions', [])), 'carrier': ' / '.join(feature['path']),
|
|
121
|
+
'priority': feature.get('release_stage', 'uncertain'), 'note': feature.get('release_rationale', ''),
|
|
122
|
+
'evidence': '; '.join(evidence), 'implementations': implementations})
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_report(path):
|
|
127
|
+
path = Path(path).resolve()
|
|
128
|
+
raw = path.read_bytes()
|
|
129
|
+
data = json.loads(raw)
|
|
130
|
+
if not isinstance(data, dict):
|
|
131
|
+
raise ValueError('feature-analysis report must be an object')
|
|
132
|
+
if data.get('schema_version') == '1.1':
|
|
133
|
+
errors = validate_inventory_contract(data)
|
|
134
|
+
if errors:
|
|
135
|
+
raise ValueError('inventory validation failed: ' + '; '.join(errors))
|
|
136
|
+
if not data.get('features'):
|
|
137
|
+
raise ValueError('inventory requires at least one feature for migration planning')
|
|
138
|
+
return {'path': str(path), 'sha256': hashlib.sha256(raw).hexdigest(), 'pointer_prefix': '/features/',
|
|
139
|
+
'source_schema': 'inventory/1.1', 'inventory': data, 'data': inventory_view(data)}
|
|
140
|
+
errors = validate_legacy_report(data)
|
|
141
|
+
if errors:
|
|
142
|
+
raise ValueError('; '.join(errors))
|
|
143
|
+
fingerprint = data.get('fingerprint')
|
|
144
|
+
if not isinstance(fingerprint, dict) or not isinstance(fingerprint.get('value'), str) or not fingerprint['value'].strip() or fingerprint.get('algorithm') == 'none':
|
|
145
|
+
raise ValueError('feature-analysis source fingerprint required; rerun analysis')
|
|
146
|
+
for key in ('tech_stack', 'sdk_dependencies', 'user_inputs', 'permissions', 'uncovered'):
|
|
147
|
+
if not isinstance(data.get(key, []), list) or any(not isinstance(row, dict) for row in data.get(key, [])):
|
|
148
|
+
raise ValueError('feature-analysis ' + key + ' must be an object array')
|
|
149
|
+
return {'path': str(path), 'sha256': hashlib.sha256(raw).hexdigest(), 'pointer_prefix': '/feature_list/', 'data': data}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def validate_links(features):
|
|
154
|
+
errors = []
|
|
155
|
+
for source in features.get('sources', []):
|
|
156
|
+
if not isinstance(source, dict) or source.get('kind') not in ('inventory', 'source') or not isinstance(source.get('locator'), str) or Path(source['locator']).name not in ('feature-analysis.json', 'inventory.json'):
|
|
157
|
+
continue
|
|
158
|
+
try:
|
|
159
|
+
if not Path(source['locator']).is_absolute():
|
|
160
|
+
raise ValueError('feature-analysis locator must be absolute')
|
|
161
|
+
report = load_report(source['locator'])
|
|
162
|
+
if source.get('revision') != 'sha256:' + report['sha256']:
|
|
163
|
+
raise ValueError('feature-analysis report hash changed; revise upstream plan before Spec dispatch')
|
|
164
|
+
expected = {report['pointer_prefix'] + str(i) for i in range(len(report['data']['feature_list']))}
|
|
165
|
+
seen = set()
|
|
166
|
+
for feature in features.get('features', []):
|
|
167
|
+
for ref in feature.get('source_refs', []):
|
|
168
|
+
if ref.get('source_id') == source['source_id']:
|
|
169
|
+
pointer = ref.get('ref')
|
|
170
|
+
if pointer not in expected:
|
|
171
|
+
raise ValueError('feature-analysis ref must identify an existing ' + report['pointer_prefix'] + 'N row')
|
|
172
|
+
seen.add(pointer)
|
|
173
|
+
if seen != expected:
|
|
174
|
+
raise ValueError('every analysis feature row needs explicit planning coverage, including deferred/excluded/unresolved')
|
|
175
|
+
except (OSError, ValueError, KeyError, TypeError) as error:
|
|
176
|
+
errors.append(str(error))
|
|
177
|
+
return errors
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def dashboard_report(path, features):
|
|
181
|
+
report = load_report(path)
|
|
182
|
+
sources = [s for s in features['sources'] if s.get('kind') in ('inventory', 'source') and Path(s.get('locator', '')).resolve() == Path(report['path']) and s.get('revision') == 'sha256:' + report['sha256']]
|
|
183
|
+
if not sources:
|
|
184
|
+
raise ValueError('--feature-analysis must be bound by the current features.json source path and report hash')
|
|
185
|
+
errors = validate_links(features)
|
|
186
|
+
if errors:
|
|
187
|
+
raise ValueError('; '.join(errors))
|
|
188
|
+
data = report['data']
|
|
189
|
+
overview = {'schema': 'd2h-overview/v1', 'generated_at': datetime.now(timezone.utc).isoformat(),
|
|
190
|
+
'source_revision': 'analysis-report-sha256:' + report['sha256'],
|
|
191
|
+
'sources': [{'source_id': 'feature-analysis', 'path': report['path'], 'sha256': report['sha256']}],
|
|
192
|
+
'tech_stack': [], 'dependencies': [], 'tools': [], 'blockers': []}
|
|
193
|
+
refs = ['feature-analysis']
|
|
194
|
+
stack_path = Path(report['path']).with_name('stack.json')
|
|
195
|
+
if report.get('source_schema') != 'inventory/1.1' and stack_path.exists():
|
|
196
|
+
stack_raw = stack_path.read_bytes()
|
|
197
|
+
stack = json.loads(stack_raw)
|
|
198
|
+
fp = data['fingerprint']
|
|
199
|
+
if not isinstance(stack, dict) or any(stack.get('fingerprint', {}).get(k) != fp.get(k) for k in ('algorithm', 'value')):
|
|
200
|
+
raise ValueError('stack.json fingerprint differs from feature-analysis; rerun consistent analysis')
|
|
201
|
+
languages = stack.get('languages')
|
|
202
|
+
if not isinstance(languages, dict) or any(not isinstance(row, dict) or any(type(row.get(k)) is not int or row[k] < 0 for k in ('files', 'lines')) for row in languages.values()):
|
|
203
|
+
raise ValueError('stack.json language counts invalid')
|
|
204
|
+
overview['sources'].append({'source_id': 'analysis-stack', 'path': str(stack_path), 'sha256': hashlib.sha256(stack_raw).hexdigest()})
|
|
205
|
+
overview['code'] = {'files': sum(row['files'] for row in languages.values()), 'lines': sum(row['lines'] for row in languages.values()),
|
|
206
|
+
'languages': [dict(name=name, files=row['files'], lines=row['lines']) for name, row in languages.items()],
|
|
207
|
+
'method': 'legacy stack report source extensions and physical lines; completeness not independently verified',
|
|
208
|
+
'source_refs': ['analysis-stack']}
|
|
209
|
+
|
|
210
|
+
for row in data.get('tech_stack', []):
|
|
211
|
+
overview['tech_stack'].append({'name': row.get('item') or '分析项未命名', 'scope': str(row.get('conclusion', '')), 'evidence': row.get('evidence', ''), 'impact': row.get('impact', ''), 'source_refs': refs})
|
|
212
|
+
for i, row in enumerate(data.get('sdk_dependencies', [])):
|
|
213
|
+
name = str(row.get('sdk') or '未命名依赖')
|
|
214
|
+
category = str(row.get('category', '')).lower()
|
|
215
|
+
kind = 'so' if 'so' in category or name.lower().endswith('.so') else 'sdk' if 'sdk' in category or 'sdk' in name.lower() else 'library'
|
|
216
|
+
overview['dependencies'].append({'name': name, 'kind': kind, 'platform': 'Android → HarmonyOS', 'provision': 'unknown',
|
|
217
|
+
'status': '报告适配结论:' + str(row.get('harmony_support') or '待确认'),
|
|
218
|
+
'reason': '尚未确认自编译或供应商产物处置;' + str(row.get('alternative', '')),
|
|
219
|
+
'evidence': row.get('evidence', ''), 'analysis_ref': '/sdk_dependencies/' + str(i), 'source_refs': refs})
|
|
220
|
+
for row in data.get('user_inputs', []):
|
|
221
|
+
overview['blockers'].append({'name': row.get('item') or '用户待提供项', 'category': row.get('kind') or '接入准备',
|
|
222
|
+
'status': 'open', 'reason': row.get('reason') or '报告记录待提供,实际准备状态待复核',
|
|
223
|
+
'action': row.get('usage') or '核对报告并补齐所需配置', 'evidence': row.get('evidence', ''), 'source_refs': refs})
|
|
224
|
+
return report, overview
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>d2h · Spec 交付看板</title><style>
|
|
2
|
+
:root{font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC",sans-serif;color:#233144;background:#f5f7fa;--muted:#738093;--line:#e5eaf0;--blue:#2463eb;--green:#159474}*{box-sizing:border-box}body{margin:0}button,input,select{font:inherit}button,select{cursor:pointer}button,a,select,input{outline-offset:3px}button{border:1px solid #dce3ec;background:#fff;color:#34445c;border-radius:7px;padding:7px 12px}button:hover{border-color:#8aaaf2;background:#f0f5ff}button:disabled{opacity:.45;cursor:default}button.primary{background:var(--blue);color:#fff;border-color:var(--blue)}aside{position:fixed;width:214px;inset:0 auto 0 0;background:#fff;border-right:1px solid var(--line);padding:25px 16px;display:flex;flex-direction:column}.brand{font-size:22px;font-weight:750;letter-spacing:-1px;padding:0 12px}.brand b{color:var(--blue)}.brand small{display:block;font-size:11px;color:var(--muted);font-weight:500;letter-spacing:1.6px;margin:0 0 32px}nav{display:grid;gap:6px}nav button{text-align:left;border:0;background:none;padding:11px 14px}nav button.active{background:#edf3ff;color:var(--blue);font-weight:600}nav span{display:inline-block;width:28px;color:#8191a8}.side-note{margin-top:auto;font-size:12px;color:var(--muted);padding:12px}.side-note strong{color:#41536e}.shell{margin-left:214px}header{height:73px;background:#fff;border-bottom:1px solid var(--line);padding:0 32px;display:flex;align-items:center;justify-content:space-between;gap:12px}header strong{font-size:15px}small,.muted{color:var(--muted)}small{font-size:12px}.tag{font-size:11px;padding:3px 8px;border-radius:5px;background:#eef2f6;color:#5d6c80;white-space:nowrap}.tag.green{background:#e6f6ef;color:#15825e}.tag.blue{background:#eaf1ff;color:#245cc6}.tag.orange{background:#fff2df;color:#a15c14}.tag.red{background:#fff0f0;color:#bc4a47}main{max-width:1550px;margin:auto;padding:26px 32px 48px}.page-heading{display:flex;justify-content:space-between;align-items:center;gap:16px;margin-bottom:22px}h1{font-size:25px;letter-spacing:-.7px;margin:0 0 3px}h2{font-size:15px;margin:0 0 15px}h3{font-size:14px;margin:0 0 7px}p{margin:8px 0}.flow{display:grid;grid-template-columns:repeat(5,1fr);border:1px solid var(--line);border-radius:10px;background:#fff;margin-bottom:24px;overflow:hidden}.flow div{padding:13px 16px;border-right:1px solid var(--line);color:var(--muted);font-size:12px}.flow div:last-child{border:0}.flow div.current{background:#eef4ff;color:#245cc6}.flow b{display:block;font-size:13px;margin-bottom:2px}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:20px}.card,.panel{border:1px solid var(--line);border-radius:10px;background:#fff}.card{padding:19px}.card strong{display:block;font-size:29px;font-weight:650;letter-spacing:-1px;margin:6px 0}.card strong em{font-size:15px;font-style:normal;color:var(--muted);font-weight:400;letter-spacing:0}.card .muted{font-size:12px}.bar{height:6px;background:#edf1f7;border-radius:6px;overflow:hidden;margin:10px 0}.bar i{display:block;height:100%;background:var(--blue);border-radius:6px}.bar i.green{background:var(--green)}.panel{padding:22px;margin-bottom:20px}.columns{display:grid;grid-template-columns:1.8fr 1fr;gap:20px}.row{display:flex;align-items:center;justify-content:space-between;gap:12px}.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 18px}.toolbar .push{margin-left:auto}input[type=search]{padding:8px 11px;min-width:220px;border:1px solid #dce3ec;border-radius:7px}select{padding:5px 8px;border:1px solid #dce3ec;border-radius:6px;background:#fff;color:#34445c}.preset.active{background:#edf3ff;color:#2463eb;border-color:#90b2f9}table{width:100%;border-collapse:collapse;text-align:left;white-space:nowrap;font-size:13px}th{font-size:12px;color:var(--muted);font-weight:500;background:#f9fafc}td,th{padding:12px;border-bottom:1px solid #edf0f4}td small{display:block}td.title{white-space:normal;min-width:180px}td select{font-size:12px}.table-wrap{overflow:auto}input[type=checkbox]{accent-color:var(--blue);width:15px;height:15px;cursor:pointer}.scope{padding-left:36px!important;color:#61728a;font-size:12px}.scope:before{content:'↳';margin-right:9px;color:#acb6c5}.notice{background:#fff8e9;color:#976520;border:1px solid #fae8bf;border-radius:7px;padding:11px 14px;font-size:12px;margin-bottom:20px}.notice.blue{background:#f0f5ff;border-color:#dce7fc;color:#526b98}.task{padding:16px 0;border-top:1px solid #edf0f4}.task:first-of-type{border-top:0}.task p{font-size:12px;color:var(--muted);margin-bottom:0}.task strong{font-size:13px}.link{border:0;background:none;color:var(--blue);padding:0;font-size:12px}.layer{padding:13px 0;border-top:1px solid #edf0f4}.layer:first-of-type{border:0}.layer.indent{padding-left:20px}.layer small{font-size:11px}.layer .bar{margin:7px 0}.timeline{border-left:2px solid #e6ecf6;padding-left:16px;margin:12px 0 0 5px}.timeline p{font-size:12px}.timeline strong{color:#41536e}.empty{text-align:center;padding:35px;color:var(--muted)}dialog{border:1px solid var(--line);border-radius:12px;max-width:660px;width:90%;padding:26px;color:#233144;box-shadow:0 18px 80px #182a4930}dialog::backdrop{background:#14243c55}dialog p{white-space:pre-line}.status-line{font-size:12px;color:var(--muted);margin-top:20px}.toast{position:fixed;bottom:24px;right:24px;background:#223247;color:#fff;padding:12px 20px;border-radius:8px;box-shadow:0 5px 20px #0002;z-index:3}.review-select{max-width:140px}.analysis-step{padding:18px 0;border-top:1px solid #edf0f4}.analysis-step:first-of-type{border:0}.kicker{font-size:11px;text-transform:uppercase;letter-spacing:1.5px;color:#8a99ae;margin-bottom:5px}a{color:var(--blue)}@media(min-width:1500px){main{padding-top:32px}}@media(max-width:1050px){aside{width:180px}.shell{margin-left:180px}main{padding:24px}.cards{grid-template-columns:repeat(2,1fr)}.columns{grid-template-columns:1fr}}@media(max-width:700px){aside{position:static;width:auto;padding:14px}.brand small,.side-note{display:none}nav{display:flex;overflow:auto;margin-top:12px}nav button{white-space:nowrap;padding:8px}nav span{display:none}.shell{margin:0}header{padding:12px 18px;height:auto;flex-wrap:wrap}main{padding:18px}.flow{overflow:auto;display:flex}.flow div{min-width:140px}.cards{gap:10px}.card{padding:14px}.page-heading{align-items:flex-start;flex-direction:column}.panel{padding:16px}.toolbar .push{margin-left:0}.card strong{font-size:24px}}
|
|
3
|
+
|
|
4
|
+
.overview-grid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:16px}.overview-grid .panel{margin:0;min-width:0}.span-3{grid-column:span 3}.span-4{grid-column:span 4}.span-6{grid-column:span 6}.span-8{grid-column:span 8}.board-value{font-size:30px;font-weight:650;margin:6px 0 12px}.board-value small{font-weight:400;font-size:12px}.code-numbers{display:flex;gap:32px;margin:16px 0}.code-numbers strong{font-size:28px;font-weight:650;display:block}.code-numbers small{display:block}.compact-item{display:flex;gap:10px;justify-content:space-between;align-items:center;padding:9px 0;border-bottom:1px solid #eef1f5;font-size:12px}.compact-item .link{text-align:left}.board-entry{width:100%;margin-top:16px;font-size:12px}.language-list{display:grid;gap:7px;margin:14px 0}.next-up,.analysis-inline{padding-top:16px;margin-top:16px;border-top:1px solid #edf0f4}.current-title{font-size:14px;font-weight:600;text-align:left}.activity-rail{height:7px;background:#edf1f7;border-radius:8px;overflow:hidden;margin:12px 0}.activity-rail i{display:block;height:100%;background:#2463eb;border-radius:8px}.activity-rail.is-indeterminate i{width:35%;opacity:0}.activity-rail.is-active.is-indeterminate i{opacity:1;animation:d2h-indeterminate 1.5s ease-in-out infinite}.activity-rail.is-active:not(.is-indeterminate) i{background-image:linear-gradient(45deg,#ffffff30 25%,transparent 25%,transparent 50%,#ffffff30 50%,#ffffff30 75%,transparent 75%);background-size:24px 24px;animation:d2h-stripes 1s linear infinite}.activity-rail.is-stale i{animation-play-state:paused!important;opacity:.35}.overview-stage h2{margin-bottom:8px}.analysis-inline small{overflow-wrap:anywhere}@keyframes d2h-indeterminate{0%{transform:translateX(-110%)}100%{transform:translateX(390%)}}@keyframes d2h-stripes{to{background-position:24px 0}}@media(prefers-reduced-motion:reduce){.activity-rail i{animation:none!important;transform:none!important}}@media(max-width:1100px){.span-3{grid-column:span 6}.span-8{grid-column:span 12}.span-4{grid-column:span 6}}@media(max-width:700px){.span-4,.span-6,.span-8{grid-column:span 12}.code-numbers{gap:24px}}@media(max-width:430px){.span-3{grid-column:span 12}}
|
|
5
|
+
|
|
6
|
+
.section-index{font-size:11px;color:#8b9ab2;letter-spacing:1px;margin-right:10px}.status-overview{padding-bottom:0}.status-overview>.row h2{margin:0}.total-time{display:flex;align-items:center;gap:10px}.total-time strong{font-size:20px}.phase-track{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin:20px 0}.phase-cell{text-align:left;padding:13px 14px;background:#f8fafc;border:1px solid #e9edf3;min-width:0}.phase-cell strong,.phase-cell small{display:block}.phase-cell strong{font-size:12px;margin:6px 0;font-weight:500}.phase-cell small{font-size:11px}.phase-label{font-weight:650;font-size:13px}.phase-active{background:#edf4ff;border-color:#b6ccfa}.live-dot{display:inline-block;width:7px;height:7px;background:#2463eb;border-radius:50%;margin-right:6px}.overview-sentence{background:#f8fafc;margin:0 -22px;padding:14px 22px;border-top:1px solid #edf0f4;display:flex;align-items:center;justify-content:space-between;gap:14px;border-radius:0 0 10px 10px}.overview-sentence strong{font-size:13px;font-weight:500}.overview-sentence small{display:block;margin-top:3px;font-size:11px}.task-overview,.project-overview{margin-bottom:18px}.task-overview .panel,.project-overview .panel{padding:20px}.summary-counts{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin:20px 0 14px}.summary-counts strong{display:block;font-size:26px;font-weight:650}.summary-counts small{font-size:11px}.blue-number{color:#2463eb}.current-brief{padding-top:5px}.idle-task{padding-top:10px;min-height:92px;display:flex;flex-direction:column;justify-content:center;gap:7px;color:#65758d}.current-title{font-size:14px}.project-metrics,.environment-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:15px 0}.environment-summary{grid-template-columns:repeat(4,1fr)}.project-metrics strong,.environment-summary strong{display:block;font-size:24px;font-weight:600}.project-metrics small,.environment-summary small{font-size:11px}.stack-pills{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0 8px}.blocker-item{display:flex;gap:12px;align-items:flex-start;padding:14px 0;border-top:1px solid #edf0f4}.blocker-item>div{flex:1;min-width:0}.blocker-item strong{font-size:12px}.blocker-item p{font-size:12px;color:#6e7e92;margin:4px 0;overflow-wrap:anywhere}.blocker-item>.link{flex-shrink:0;margin-top:3px}.blocker-dot{width:6px;height:6px;background:#d9993a;border-radius:50%;margin-top:9px}.clear-blockers{font-size:13px;color:#547064}.clear-blockers small{display:block;margin-top:5px}@media(min-width:1101px){.task-overview .span-3 h2{font-size:14px}.task-overview .span-3 .row{gap:5px}}@media(max-width:1100px){.task-overview .span-6{grid-column:span 12}}@media(max-width:760px){.phase-track{grid-template-columns:repeat(3,minmax(0,1fr))}.overview-sentence{display:block}.overview-sentence>.tag{display:inline-block;margin-top:10px}}@media(max-width:700px){.overview-sentence{margin:0 -16px;padding:14px 16px}.project-overview .span-6{grid-column:span 12}}@media(max-width:430px){.phase-track{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
7
|
+
|
|
8
|
+
/* Restrained color: section accents identify areas; status colors keep their meaning. */
|
|
9
|
+
:root{--blue:#3766dc;--green:#168168;--line:#e2e8f1;background:#f3f6fb;color:#26364d}body{background:#f3f6fb}.panel{box-shadow:0 2px 8px #29456a03}header{background:#ffffffeb}nav button.active{background:#eaf0ff;color:#2d59c2;box-shadow:inset 3px 0 #3766dc}nav button.active span{color:#3766dc}.kicker{color:#6d83a9}.section-index{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:22px;border-radius:6px;color:#4566a4;background:#eaf0fc;font-weight:650;letter-spacing:0}.status-overview{border-top:3px solid #6285df;background:linear-gradient(110deg,#fbfcff,#fff 65%)}.total-time{padding:5px 11px;border-radius:7px;background:#f0f4fc}.total-time strong{color:#4d638b}.phase-cell{background:#f7f9fc;border-color:#e5eaf3}.phase-cell:hover{background:#f0f4fd;border-color:#adc1ea}.phase-active{background:#eaf1ff;border-color:#b5c9f7;box-shadow:inset 0 3px #4c77dc}.phase-active .phase-label,.phase-active strong{color:#2858c4}.phase-done{background:#edf8f3;border-color:#c6e8d9;box-shadow:inset 0 3px #55a68a}.phase-done .phase-label,.phase-done strong{color:#177657}.phase-error{background:#fff0f0;border-color:#f0c8c8;box-shadow:inset 0 3px #d26969}.phase-error strong{color:#b54545}.overview-sentence{background:#f0f4fc;border-top-color:#e0e8f7}.overview-sentence strong{color:#3b5279}.task-overview>.panel:first-child{border-top:3px solid #5e85dc;background:linear-gradient(125deg,#f8fbff,#fff 70%)}.task-overview>.panel:nth-child(2){border-top:3px solid #8c85c2}.task-overview>.panel:nth-child(3){border-top:3px solid #61a6a1}.summary-counts>div:first-child strong{color:#168168}.summary-counts>div:last-child strong{color:#66768b}.summary-counts .blue-number{color:#3766dc}.task-overview>.panel:nth-child(3) .bar i{background:#3e9d94}.project-overview>.panel:first-child{border-top:3px solid #65a7a3;background:linear-gradient(125deg,#f6fbfa,#fff 70%)}.project-overview>.panel:first-child .section-index{color:#39867f;background:#e7f5f1}.project-metrics strong{color:#356b69}.project-overview>.panel:last-child{border-top:3px solid #95a4b9}.stack-pills .tag{color:#48736e;background:#edf5f2}.environment-summary>div{padding:7px 9px;border-radius:7px;background:#f4f6fa}.environment-summary>div:nth-child(1){background:#edf8f2}.environment-summary>div:nth-child(1) strong{color:#168168}.environment-summary>div:nth-child(2) strong{color:#bc4a47}.environment-summary>div:nth-child(3) strong{color:#a96825}.environment-summary>div:nth-child(4) strong{color:#7c8798}.environment-summary.is-report-stale>div{background:#f5f3ef}.environment-summary.is-report-stale strong{color:#8c785c}.blockers-overview.has-blockers{border-top:3px solid #d6a25a;background:linear-gradient(110deg,#fffcf6,#fff 75%)}.blockers-overview .section-index{color:#9d7239;background:#fff0d9}.blockers-overview .blocker-item{border-top-color:#f0e6d7}.blockers-overview .blocker-item strong{color:#6c5435}.blockers-overview .blocker-dot{background:#cc8e39}.blockers-overview.all-clear{border-top:3px solid #78ad97}.blockers-overview.all-clear .section-index{background:#eaf6ef;color:#417e62}.tag.green{background:#e8f6ef;color:#177657}.tag.blue{background:#eaf0ff;color:#305fc1}.tag.orange{background:#fff0da;color:#986221}.tag.red{background:#fdecec;color:#b14646}@media(max-width:430px){.environment-summary>div{padding:7px 4px}}
|
|
10
|
+
|
|
11
|
+
/* Blockers and preparation gaps use a consistent red treatment. */
|
|
12
|
+
.blockers-overview.has-blockers{border-top-color:#cf6262;border-color:#edc7c7;border-top-width:3px;background:linear-gradient(110deg,#fff4f3,#fffafa 75%)}.blockers-overview.has-blockers .section-index{color:#aa4141;background:#fbe3e3}.blockers-overview.has-blockers h2{color:#9f3f3f}.blockers-overview.has-blockers .tag{background:#fbe4e4;color:#ad4242}.blockers-overview.has-blockers .blocker-item{border-top-color:#f0d7d7}.blockers-overview.has-blockers .blocker-item strong{color:#994343}.blockers-overview.has-blockers .blocker-dot{background:#cc5555}.blockers-overview.has-blockers .link{color:#b14848}
|
|
13
|
+
|
|
14
|
+
/* Stronger emphasis for recorded blockers, without flashing or motion. */
|
|
15
|
+
.blockers-overview.has-blockers{background:#fff0f0;border:1px solid #e9a2a2;border-top:4px solid #cc3434;border-left:6px solid #cc3434;box-shadow:0 3px 12px #b4231810}.blockers-overview.has-blockers h2{color:#aa2020;font-size:17px;font-weight:750}.blockers-overview.has-blockers h2:before{content:'!';display:inline-flex;align-items:center;justify-content:center;width:23px;height:23px;margin-right:9px;border-radius:50%;background:#c52e2e;color:#fff;font-size:16px;font-weight:800;vertical-align:middle}.blockers-overview.has-blockers .section-index{background:#f9cece;color:#9e2020}.blockers-overview.has-blockers h2>.tag{background:#c52e2e;color:#fff;font-weight:750;min-width:24px;text-align:center}.blockers-overview.has-blockers .blocker-item{border-top-color:#eababa;padding-top:17px;padding-bottom:17px}.blockers-overview.has-blockers .blocker-item strong{color:#a82020;font-size:14px;font-weight:700}.blockers-overview.has-blockers .blocker-item p{color:#874444}.blockers-overview.has-blockers .blocker-dot{width:8px;height:8px;background:#c52e2e;flex-shrink:0;box-shadow:0 0 0 3px #f8d1d1}.blockers-overview.has-blockers .link{color:#ac2424;font-weight:600}.blockers-overview.has-blockers>.row>.link{border:1px solid #e1a4a4;background:#fff7f7;border-radius:6px;padding:5px 9px}
|
|
16
|
+
</style></head><body>
|
|
17
|
+
<aside><div class="brand"><b>d2h</b> workbench<small>SPEC PLAN / DELIVERY</small></div><nav id="nav" aria-label="主导航"></nav><div class="side-note"><strong>统一派生视图</strong><br>执行真源:execution.json<br>范围真源:features.json<br>计划真源:pages.json</div></aside>
|
|
18
|
+
<div class="shell"><header><strong>迁移工作台</strong><div><span id="connection" class="tag orange">离线快照</span> <button id="open-json">导入运行快照</button><input type="file" id="file" accept=".json,application/json" hidden></div></header><main id="main"></main></div>
|
|
19
|
+
<dialog id="detail"><div class="row"><h2 id="detail-title"></h2><button id="close-detail" aria-label="关闭详情">关闭</button></div><pre id="detail-body"></pre></dialog><div class="toast" id="toast" role="status" hidden></div>
|
|
20
|
+
<script>
|
|
21
|
+
let data=/*__D2H_SNAPSHOT__*/null;
|
|
22
|
+
const $=id=>document.getElementById(id),esc=v=>String(v??'—').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
23
|
+
const pages=[['overview','◫','迁移总览'],['analysis','⌕','分析与报告'],['features','☷','功能清单'],['specs','▤','规格清单'],['tasks','☑','Spec 执行'],['project','▧','工程与环境'],['review','◎','交付 Review']],layerNames={mvp:'MVP 基础',core:'核心新增',full:'全量补齐'},targetNames={mvp:'最小化实现',core:'核心实现',full:'全量实现'};
|
|
24
|
+
let page='overview',draft=null,priorityDraft=null,draftHash=null,draftIdentity=null,draftTarget=null,query='',filter='',stale=false,pollUrl=null;
|
|
25
|
+
const tag=(v,c='')=>`<span class="tag ${c}">${esc(v)}</span>`,ratio=r=>r?`${esc(r.accepted)} / ${esc(r.total)}`:'未启用逐项追踪';
|
|
26
|
+
const bar=r=>r?`<div class="bar"><i style="width:${Number.isFinite(r.percent)?Math.max(0,Math.min(100,r.percent)):0}%"></i></div>`:'';
|
|
27
|
+
const card=(title,value,note,r)=>`<div class="card"><small>${title}</small><strong>${value}</strong>${bar(r)}<small>${note}</small></div>`;
|
|
28
|
+
function notify(text){$('toast').textContent=text;$('toast').hidden=false;clearTimeout(window.toastTimer);window.toastTimer=setTimeout(()=>$('toast').hidden=true,4000)}
|
|
29
|
+
function show(title,value){$('detail-title').textContent=title;$('detail-body').textContent=typeof value==='string'?value:JSON.stringify(value,null,2);$('detail').showModal()}
|
|
30
|
+
function valid(s){if(!s||s.schema!=='d2h-dashboard/v1'||!s.derived?.delivery_progress||!s.feature_plan||!s.plan||!s.execution||!Array.isArray(s.plan.specs)||!Array.isArray(s.feature_plan.features)||!Array.isArray(s.feature_plan.sources)||!Array.isArray(s.derived.running)||!Array.isArray(s.review?.items)||!Array.isArray(s.derived.validation_debt?.specs)||!s.execution.tasks||!s.derived.tasks||!Number.isInteger(s.feature_plan.revision)||!Number.isInteger(s.plan.revision)||!Number.isInteger(s.event_sequence))throw Error('需要当前 runtime 导出的 d2h-dashboard/v1 快照');return s}
|
|
31
|
+
function selected(s=data){return new Set(s.feature_plan.features.flatMap(f=>(f.scope_items||[]).filter(i=>f.scope_status==='included'&&!f.needs_clarification&&i.status==='included'&&i.milestone!=='unassigned').map(i=>JSON.stringify([f.feature_id,i.scope_id]))))}
|
|
32
|
+
function identity(){return JSON.stringify([data.execution.feature_hash,data.execution.plan_hash,data.event_sequence,data.execution.candidate])}
|
|
33
|
+
function deliveryPresetKeys(featurePlan,target){const tiers=['mvp','core','full'],cap=tiers.indexOf(target);if(cap<0||!featurePlan.delivery)return[];const eligible=new Set(featurePlan.features.filter(f=>f.scope_status!=='excluded').flatMap(f=>(f.scope_items||[]).filter(i=>i.status!=='excluded').map(i=>JSON.stringify([f.feature_id,i.scope_id]))));return [...new Set((Array.isArray(featurePlan.delivery.scope_layers)?featurePlan.delivery.scope_layers:[]).filter(r=>tiers.indexOf(r.layer)>=0&&tiers.indexOf(r.layer)<=cap).map(r=>JSON.stringify([r.feature_id,r.scope_id])).filter(k=>eligible.has(k)))];}
|
|
34
|
+
function presetSummary(){const fp=data.feature_plan;if(!fp.delivery)return '<div class="notice blue">尚未配置交付分层,三个预设暂不可用。请由功能清单技能补齐最低交付层并修订版本;仍可手动调整范围,不从 P0/P1/P2 猜分层。</div>';const counts=['mvp','core','full'].map(k=>deliveryPresetKeys(fp,k).length);return `<div class="notice ${counts.every(n=>n>0)?'blue':''}">累计可选范围:最小化 ${counts[0]} 项 · 核心 ${counts[1]} 项 · 全量 ${counts[2]} 项。${counts.some(n=>n===0)?'存在空预设,分层尚未可用;点击空预设将保留当前选择,请修订 scope_layers。':'预设只修改草稿,仍需主 Agent 核验依赖并更新规划。'}</div>`}
|
|
35
|
+
function resetDraft(){draft=selected();priorityDraft=Object.fromEntries(data.feature_plan.features.map(f=>[f.feature_id,f.user_priority]));draftHash=data.execution.feature_hash;draftIdentity=identity();draftTarget=data.feature_plan.delivery?.target??null}
|
|
36
|
+
function scopeRows(){return data.feature_plan.features.flatMap(f=>(f.scope_items||[]).map(i=>({...i,feature_id:f.feature_id,feature_title:f.title,feature_status:f.scope_status,key:JSON.stringify([f.feature_id,i.scope_id]),progress:data.derived.feature_progress[f.feature_id]?.items.find(x=>x.scope_id===i.scope_id)})))}
|
|
37
|
+
function stats(){const d=data.derived,rows=scopeRows().filter(i=>i.feature_status!=='excluded'&&i.status!=='excluded'),refs=new Set(data.plan.specs.filter(s=>d.relevant_specs.includes(s.spec_id)).flatMap(s=>(s.scope_refs||[]).map(i=>JSON.stringify([i.feature_id,i.scope_id]))));const xs=rows.filter(i=>refs.has(i.key));return {total:xs.length,accepted:xs.filter(i=>i.progress?.accepted).length}}
|
|
38
|
+
function heading(title,note){return `<div class="page-heading"><div><div class="kicker">D2H / ${page}</div><h1>${title}</h1><small>${note}</small></div></div>`}
|
|
39
|
+
function gate(v){return tag(v?'当前通过':'尚未通过',v?'green':'orange')}
|
|
40
|
+
function layers(){const g=data.derived.delivery_progress;if(!g.configured)return '<p class="muted">未配置 MVP / 核心 / 全量映射。保留原始里程碑,不按轮次名称猜测分层。</p>';return Object.entries(g.layers).map(([key,l])=>`<div class="layer"><div class="row"><strong>${layerNames[key]}</strong><span>新增 ${ratio(l.increment)}</span></div>${bar(l.cumulative)}<small>累计 ${ratio(l.cumulative)} · ${esc(l.cumulative.percent??'—')}% · 未规划 ${esc(l.cumulative.unplanned)}</small></div>`).join('')}
|
|
41
|
+
function overviewData(){return data.overview||{available:false,code:null,dependencies:[],tech_stack:[],tools:[]}}
|
|
42
|
+
function provisionLabel(value){return {'self-build':'计划自编译','provide-binary':'需要提供二进制','platform-sdk':'平台 SDK','replace':'计划替换',unknown:'处置待确认'}[value]||'处置待确认'}
|
|
43
|
+
function reportBadge(){const m=overviewData();return tag(m.stale?'来源已变化 · 需复核':m.available?'已采集':'未采集',m.stale?'orange':'')}
|
|
44
|
+
function activityRail(percent,active,at){const width=Number.isFinite(percent)?Math.max(0,Math.min(100,percent)):null;return `<div class="activity-rail ${active?'is-active':''} ${width===null?'is-indeterminate':''}" ${active?`data-live-at="${esc(at)}"`:''} role="progressbar" aria-label="${width===null?'活动状态,完成比例未知':'已处理进度'}" ${width===null?'':`aria-valuenow="${width}" aria-valuemin="0" aria-valuemax="100"`}><i style="${width===null?'':'width:'+width+'%'}"></i></div>`}
|
|
45
|
+
function analysisBlock(){const a=data.analysis,e=a?.latest;if(!e)return `<div class="muted">${esc(a?.message||'尚未接入分析事件。')}可读取 AppGraph 的独立进度文件;不从规格数量推算分析进度。</div>`;
|
|
46
|
+
const names={'index.scanning':'扫描源码','index.parsing':'解析代码','index.storing':'存储索引','index.resolving':'解析引用','graph.modules':'模块关系','graph.communities':'功能聚类','graph.capabilities-and-manifest':'能力与清单','graph.semantics':'语义分析','graph.rule-extensions':'规则分析','graph.ui-components':'界面结构','graph.entry-points':'入口分析','graph.behavior-slices':'行为切片','graph.interactions':'交互关系','facts.generate':'生成 Facts','facts.write':'输出 Facts','facts.validate':'校验 Facts',analysis:'分析运行'};
|
|
47
|
+
const running=a.state==='running'&&!a.stale&&['started','progress'].includes(e.status),percent=Number.isFinite(e.total)&&e.total>0&&Number.isFinite(e.current)?Math.min(100,e.current/e.total*100):null;
|
|
48
|
+
return `<div class="row"><strong>${esc(names[e.phase]||e.phase)}</strong>${tag(a.stale?'暂无新事件':a.state==='completed'?'分析已结束':a.state==='failed'?'分析失败':'记录进行中',a.state==='failed'?'red':running?'blue':'')}</div>${activityRail(percent,running,e.timestamp)}<small>${e.current===null||e.current===undefined?'处理数量未提供':esc(e.current)+' / '+(e.total===null?'总量未知':esc(e.total))} · ${esc(e.unit||'阶段通知')}<br>最后通知 ${esc(new Date(e.timestamp).toLocaleString())}${e.item?'<br>'+esc(e.item):''}${e.message?'<br>'+esc(e.message):''}${e.project_root?'<br>分析工程:'+esc(e.project_root):''}</small>${(a.warnings||[]).length?'<p>'+tag('分析警告 '+a.warnings.length,'orange')+'<small>'+esc(a.warnings.slice(-2).map(w=>w.message||w.phase).join(';'))+'</small></p>':''}`}
|
|
49
|
+
function duration(ms){if(!Number.isFinite(ms)||ms<0)return '未记录';if(ms<1000)return Math.round(ms)+' 毫秒';const sec=Math.floor(ms/1000),h=Math.floor(sec/3600),m=Math.floor(sec%3600/60);return h?h+' 小时 '+m+' 分':m?m+' 分 '+sec%60+' 秒':sec+' 秒'}
|
|
50
|
+
function taskElapsed(id){const at=data.timing?.task_started_at?.[id];return at?duration(Date.parse(data.captured_at)-Date.parse(at)):'未记录'}
|
|
51
|
+
function blockers(){const m=overviewData(),rows=[];for(const r of m.blockers||[])if(r.status!=='resolved')rows.push({title:r.name,reason:r.reason||'处置待确认',kind:r.category||'外部准备',action:r.action||'待明确解决方式',go:'project'});for(const r of m.dependencies||[])if(r.blocking===true)rows.push({title:r.name,reason:r.reason,kind:'SDK / SO',action:r.action||'补齐目标平台产物或确认替代方案',go:'project'});for(const r of m.tools||[])if(['failed','missing'].includes(r.status))rows.push({title:r.name,reason:r.message||'检查未通过',kind:m.stale?'历史环境异常':'环境异常',action:'复查环境报告',go:'project'});for(const [id,t] of Object.entries(data.derived.tasks)){for(const reason of t.external_blockers||[])rows.push({title:id,reason,kind:'外部阻塞',spec:id});for(const reason of t.readiness_gaps||[])rows.push({title:id,reason,kind:'工作包缺口',spec:id})}return rows.filter((r,i,a)=>a.findIndex(x=>x.title===r.title&&x.reason===r.reason)===i)}
|
|
52
|
+
function blockerRows(rows){return rows.map(r=>`<div class="blocker-item"><span class="blocker-dot"></span><div><strong>${esc(r.title)}</strong> ${tag(r.kind,'orange')}<p>${esc(r.reason)}</p>${r.action?'<small>下一步:'+esc(r.action)+'</small>':''}</div><button class="link" ${r.spec?'data-spec="'+esc(r.spec)+'"':'data-blocker="'+blockers().findIndex(x=>x.title===r.title&&x.reason===r.reason)+'"'}>查看详情 →</button></div>`).join('')}
|
|
53
|
+
function overview(){
|
|
54
|
+
const d=data.derived,m=overviewData(),fs=data.feature_plan.features,bs=blockers(),ids=d.running.length?d.running:(d.active_batch?.spec_ids||[]).filter(id=>!d.tasks[id].accepted),accepted=Object.values(d.tasks).filter(t=>t.accepted).length;
|
|
55
|
+
const featureDone=fs.filter(f=>f.scope_status!=='excluded'&&d.feature_progress[f.feature_id]?.complete).length,featureRunning=fs.filter(f=>!d.feature_progress[f.feature_id]?.complete&&data.plan.specs.some(s=>d.running.includes(s.spec_id)&&s.feature_refs.includes(f.feature_id))).length;
|
|
56
|
+
const a=data.analysis,anActive=a?.state==='running',anDone=a?.state==='completed',devActive=d.running.length>0,devDone=accepted===data.plan.specs.length&&accepted>0;
|
|
57
|
+
const stages=[['分析',anDone?'最近运行已结束':a?.state==='failed'?'最近运行失败':anActive?'记录进行中':data.feature_analysis?'已有分析报告':'未接入记录',a?.latest?duration(a.latest.elapsed_ms):'未记录','analysis',anActive&&!a.stale],['功能范围',fs.length+' 项 · 已有清单','未记录','features',false],['Spec 规划',data.plan.specs.length+' 项 · 已有规划','未记录','specs',false],['开发与验证',devActive?'正在实施':devDone?'Spec 已验收':d.validation_debt.specs.length?'待验证 / 修复':'待执行','未记录','tasks',devActive],['交付 Review','待人工复核','未记录','review',false]];
|
|
58
|
+
const sentence=anActive?'最近一次分析尚在处理,阶段状态以通知为准。':devActive?'正在实施 '+d.running.length+' 个 Spec,已验收 '+accepted+' / '+data.plan.specs.length+' 个。':d.validation_debt.specs.length?'有 '+d.validation_debt.specs.length+' 个 Spec 等待验证或修复。':'已有 '+fs.length+' 个功能、'+data.plan.specs.length+' 个 Spec,'+(data.execution.candidate?'当前等待下一步调度。':'尚未开始实现。');
|
|
59
|
+
return heading('迁移总览','阶段 → 任务 → 工程与环境 → 阻塞')+`
|
|
60
|
+
<section class="panel status-overview" aria-label="第一层:状态总览"><div class="row"><h2><span class="section-index">01</span>状态总览</h2><div class="total-time"><small>总用时</small><strong>未记录</strong></div></div><div class="phase-track">${stages.map(([name,status,time,go,active])=>`<button class="phase-cell ${active?'phase-active':go==='analysis'&&a?.state==='failed'?'phase-error':go==='analysis'&&anDone||go==='tasks'&&devDone?'phase-done':''}" data-go="${go}"><span class="phase-label">${active?'<i class="live-dot"></i>':''}${name}</span><strong>${esc(status)}</strong><small>${go==='analysis'?'最近运行用时':'用时'} ${esc(time)}</small></button>`).join('')}</div><div class="overview-sentence"><div><strong>${esc(sentence)}</strong><small>${bs.length?'另有 '+bs.length+' 项已记录阻塞或准备缺口。':'尚无已记录阻塞;未检查项见环境。'} 阶段未有完成凭据时,仅显示已有产物。</small></div>${ids.length?`<span class="tag blue">当前任务用时 ${esc(taskElapsed(ids[0]))}</span>`:''}</div></section>
|
|
61
|
+
<div class="overview-grid task-overview" aria-label="第二层:任务总览">
|
|
62
|
+
<section class="panel span-6"><div class="row"><h2><span class="section-index">02</span>当前任务</h2><button class="link" data-go="tasks">执行详情 →</button></div>${ids.length?ids.slice(0,2).map(id=>{const sp=data.plan.specs.find(s=>s.spec_id===id);return `<div class="current-brief"><div class="row"><button class="link current-title" data-spec="${esc(id)}">${esc(sp?.scope?.join(';')||id)}</button>${tag(d.running.includes(id)?'执行中':'已领取','blue')}</div><small>${esc(id)} · 本次派发已历时 ${esc(taskElapsed(id))}</small>${activityRail(null,d.running.includes(id),data.captured_at)}</div>`}).join(''):anActive?`<strong>AppGraph 分析</strong>${analysisBlock()}`:`<div class="idle-task"><span>当前无在途任务</span><small>${d.ready_batches?.length?'建议批次:'+esc(d.ready_batches[0]):d.ready.length?'建议下一项:'+esc(d.ready[0]):'等待调度或补齐前置条件'}</small></div>`}</section>
|
|
63
|
+
<section class="panel span-3"><div class="row"><h2>Spec 任务</h2><button class="link" data-go="tasks">全部 →</button></div><div class="summary-counts"><div><strong>${accepted}</strong><small>完成</small></div><div><strong class="blue-number">${d.running.length}</strong><small>进行中</small></div><div><strong>${data.plan.specs.length}</strong><small>总数</small></div></div>${bar({percent:data.plan.specs.length?100*accepted/data.plan.specs.length:0})}<small>完成按当前有效验收计</small></section>
|
|
64
|
+
<section class="panel span-3"><div class="row"><h2>功能清单</h2><button class="link" data-go="features">全部 →</button></div><div class="summary-counts"><div><strong>${featureDone}</strong><small>完成</small></div><div><strong class="blue-number">${featureRunning}</strong><small>进行中</small></div><div><strong>${fs.length}</strong><small>总数</small></div></div>${bar({percent:fs.length?100*featureDone/fs.length:0})}<small>${fs.filter(f=>f.scope_status==='excluded').length?'总数含排除项;':''}完成要求所有未排除范围通过</small></section></div>
|
|
65
|
+
<div class="overview-grid project-overview" aria-label="第三层:工程与环境">
|
|
66
|
+
<section class="panel span-6"><div class="row"><h2><span class="section-index">03</span>安卓工程</h2><button class="link" data-go="project">工程详情 →</button></div><div class="project-metrics"><div><strong>${esc(m.code?.files??'—')}</strong><small>源文件</small></div><div><strong>${esc(m.code?.lines?.toLocaleString()??'—')}</strong><small>物理行数</small></div><div><strong>${(m.dependencies||[]).length}</strong><small>已记录依赖</small></div></div><div class="stack-pills">${(m.tech_stack||[]).slice(0,3).map(r=>tag(r.name)).join('')||'<small>技术栈未采集</small>'}</div><small>${m.stale?'来源已变化,需重新采集':m.code?.complete===false?'代码统计不完整':'依赖数为已记录条目,完整扫描状态见详情'}</small></section>
|
|
67
|
+
<section class="panel span-6"><div class="row"><h2>环境检查</h2><button class="link" data-go="project">检查详情 →</button></div><div class="environment-summary ${m.stale?'is-report-stale':''}">${['passed','failed','missing','unknown'].map(k=>`<div><strong>${(m.tools||[]).filter(r=>r.status===k).length}</strong><small>${{passed:'通过',failed:'失败',missing:'缺失',unknown:'未检查'}[k]}</small></div>`).join('')}</div><div class="row"><small>${m.tools?.length?'检查范围:'+esc(m.tools.slice(0,3).map(r=>r.name).join(' / ')):'尚未载入环境检查报告'}</small>${m.stale?tag('历史结果','orange'):tag((m.tools||[]).some(r=>r.status==='unknown')?'有未检查项':m.tools?.length?'按报告展示':'未载入报告','orange')}</div></section></div>
|
|
68
|
+
<section class="panel blockers-overview ${bs.length?'has-blockers':'all-clear'}" aria-label="第四层:阻塞情况"><div class="row"><h2><span class="section-index">04</span>阻塞与准备缺口 ${tag(bs.length,bs.length?'orange':'green')}</h2><button class="link" data-go="project">全部详情 →</button></div>${bs.length?blockerRows(bs.slice(0,3)):'<div class="clear-blockers">尚无已记录阻塞 <small>闭源 SDK/SO、平台配置与环境尚未完成全面核验,不能据此判断已全部就绪。</small></div>'}${bs.length>3?'<small>另有 '+(bs.length-3)+' 项,进入详情查看。</small>':''}</section>`;
|
|
69
|
+
}
|
|
70
|
+
function project(){const m=overviewData();return heading('工程与环境','规模、技术依赖、环境检查及外部准备的完整记录')+`<section class="panel"><div class="row"><h2>代码规模与技术栈</h2>${reportBadge()}</div><p>${esc(m.code?.files??'—')} 个源码文件 · ${esc(m.code?.lines??'—')} 物理行</p><p>${esc(m.code?.method||'统计口径未采集')}</p><button class="link" data-overview="code">统计来源 →</button><div class="language-list">${(m.tech_stack||[]).map((r,i)=>`<div class="row"><button class="link" data-stack="${i}">${esc(r.name)}</button><small>${esc(r.version||r.scope||'版本未记录')}</small></div>`).join('')}</div></section><section class="panel"><h2>SDK / SO 依赖</h2><div class="table-wrap"><table><thead><tr><th>依赖 / 平台</th><th>类型</th><th>迁移处置</th><th>准备情况</th></tr></thead><tbody>${(m.dependencies||[]).map((r,i)=>`<tr><td><button class="link" data-dependency="${i}">${esc(r.name)}</button><small>${esc(r.platform)}</small></td><td>${esc(r.kind.toUpperCase())}</td><td>${tag(provisionLabel(r.provision))}</td><td class="title">${esc(r.status||'未验证')}<small>${esc(r.reason)}</small></td></tr>`).join('')}</tbody></table></div><small>清单为空不代表完成扫描;计划自编译不代表编译成功,Android 产物不等于 HarmonyOS 产物。</small></section><section class="panel"><h2>环境检查</h2>${(m.tools||[]).map((r,i)=>`<div class="compact-item"><div><button class="link" data-tool="${i}">${esc(r.name)}</button><small> ${esc(r.version||r.message||'')}</small></div>${tag((m.stale?'上次:':'')+({passed:'通过',failed:'失败',missing:'缺失',unknown:'未检查'}[r.status]),r.status==='passed'&&!m.stale?'green':'orange')}</div>`).join('')}<small>工具可执行、版本合规、目标 SDK 可用及设备构建是不同检查。</small></section><section class="panel"><h2>阻塞与准备缺口</h2>${blockerRows(blockers())||'<small>尚无已记录阻塞,完整核验仍需执行。</small>'}</section>`}
|
|
71
|
+
function analysisReport(){const report=data.feature_analysis;if(!report)return '<div class="notice blue">尚未接入功能分析产物;当前显示已有规划来源。新项目可通过 --feature-analysis 导出。</div>';const a=report.data,sourceIds=new Set(data.feature_plan.sources.filter(s=>s.locator===report.path&&s.revision==='sha256:'+report.sha256).map(s=>s.source_id));return `<section class="panel"><h2>功能清单分析 · ${esc(a.project)}</h2><p><small>分析行为行 ${a.feature_list.length} · 规划功能 ${data.feature_plan.features.length} · 未覆盖候选 ${(a.uncovered||[]).length} · 用户待办 ${(a.user_inputs||[]).length}</small></p><div class="notice blue">清单中的阶段/分析建议不代表用户授权或验收;范围与当前状态以版本化规划和执行证据为准,源码新鲜度在恢复时核验。</div><div class="table-wrap"><table><thead><tr><th>行为与条件</th><th>阶段或分析建议 / 可行性</th><th>来源证据</th><th>关联规划功能 / Spec</th></tr></thead><tbody>${a.feature_list.map((row,i)=>{const fs=data.feature_plan.features.filter(f=>f.source_refs.some(r=>sourceIds.has(r.source_id)&&r.ref===(report.pointer_prefix||'/feature_list/')+i)),ids=new Set(fs.map(f=>f.feature_id)),specs=data.plan.specs.filter(s=>s.feature_refs.some(id=>ids.has(id)));return `<tr><td class="title">${esc(row.trigger)} → ${esc(row.result)}<small>${esc(row.condition||'条件未限定')} · ${esc(row.carrier||'承载未注明')}</small></td><td>${tag(row.priority)}<small>${esc(row.feasibility||'未标注限制')} ${esc(row.note||'')}</small></td><td class="title">${esc(row.evidence)}</td><td class="title">${fs.map(f=>`<button class="link" data-go="features">${esc(f.title)}</button>`).join(' / ')}<small>关联功能的 Spec:</small>${specs.map(s=>`<button class="link" data-spec="${esc(s.spec_id)}">${esc(s.spec_id)}</button>`).join(' / ')||'<small>尚未规划或未分配范围</small>'}</td></tr>`}).join('')}</tbody></table></div><p><small>报告:${esc(report.path)} · SHA256 ${esc(report.sha256)}</small></p></section><section class="panel"><h2>未覆盖与待确认</h2>${(a.uncovered||[]).map(r=>`<p>${esc(r.component)} · ${esc(r.reason)}<br><small>${esc(r.evidence)}</small></p>`).join('')||'<small>报告未记录未覆盖候选;这不证明业务已全部验收。</small>'}</section>`}
|
|
72
|
+
function analysis(){return heading('分析与报告','结构化分析通知与带来源的项目摘要')+analysisReport()+`<section class="panel"><h2>分析进展</h2>${analysisBlock()}<p><small>阶段通知不会改变 Facts。事件总量未知时只显示处理数量;分析结束也可能存在覆盖警告。</small></p></section><section class="panel"><h2>功能规划输入来源</h2><div class="table-wrap"><table><thead><tr><th>来源</th><th>类型</th><th>定位 / 版本</th></tr></thead><tbody>${data.feature_plan.sources.map(s=>`<tr><td>${esc(s.source_id)}</td><td>${esc(s.kind)}</td><td class="title">${esc(s.locator)}<small>${esc(s.revision)}</small></td></tr>`).join('')}</tbody></table></div></section>`}
|
|
73
|
+
function updateActivity(){const now=Date.now();document.querySelectorAll('[data-live-at]').forEach(e=>{const old=now-Date.parse(e.dataset.liveAt)>30000;e.classList.toggle('is-stale',stale||(!data?.demo&&old))})}
|
|
74
|
+
function features(){const config=data.feature_plan.delivery,expired=draftIdentity!==identity();return heading('功能清单','勾选与优先级修改形成版本绑定草稿,由主 Agent 核对后修订')+presetSummary()+`<div class="notice ${expired?'':'blue'}">${expired?'运行基线已变化,草稿已过期;请撤回重载后合并。':'这里不直接保存工程。取消项形成延期建议;依赖、Spec 和在途窗口需由主 Agent 核验。'}</div><section class="panel"><div class="toolbar">${['mvp','core','full'].map(k=>`<button data-preset="${k}" ${config?'':'disabled'}>${targetNames[k]}${config?' · '+deliveryPresetKeys(data.feature_plan,k).length+' 项':''}</button>`).join('')}<button id="reset-draft">撤回并重载</button><button class="primary" id="export-draft" ${expired||!data.derived.scope_tracking?'disabled':''}>预览并导出修改建议</button><small id="draft-count">已选 ${draft.size} 范围项</small></div><div class="table-wrap"><table><thead><tr><th>本次选用</th><th>功能 / 范围</th><th>最低层 / 原处置</th><th>用户优先级</th><th>当前有效验收</th></tr></thead><tbody>${data.feature_plan.features.map(f=>`<tr><td><input type="checkbox" data-feature="${esc(f.feature_id)}" aria-label="选择功能${esc(f.title)}" ${!f.scope_items||f.scope_status==='excluded'?'disabled':''}></td><td><strong>${esc(f.title)}</strong><small>${esc(f.feature_id)}</small></td><td>${esc(f.scope_status)}</td><td><select data-priority="${esc(f.feature_id)}" aria-label="${esc(f.title)}优先级">${['must','normal','later'].map(k=>`<option ${priorityDraft[f.feature_id]===k?'selected':''}>${k}</option>`).join('')}</select></td><td>${tag(data.derived.feature_progress[f.feature_id]?.complete?'全功能已完成':'有未完成范围')}</td></tr>${(f.scope_items||[]).map(i=>{const key=JSON.stringify([f.feature_id,i.scope_id]),layer=config?.scope_layers.find(x=>x.feature_id===f.feature_id&&x.scope_id===i.scope_id)?.layer;return `<tr><td><input type="checkbox" data-scope="${esc(key)}" aria-label="选择${esc(i.description)}" ${draft.has(key)?'checked':''} ${f.scope_status==='excluded'||i.status==='excluded'?'disabled':''}></td><td class="scope title">${esc(i.description)}<small>${esc(i.scope_id)}</small></td><td>${esc(layerNames[layer]??'未配置')}<small>${esc(i.status)} / ${esc(i.milestone)}</small></td><td>沿用功能约束</td><td>${gate(data.derived.feature_progress[f.feature_id]?.items.find(x=>x.scope_id===i.scope_id)?.accepted)}</td></tr>`}).join('')}`).join('')}</tbody></table></div><small>预设保留手动优先级;包含未决/未授权项时只是修改建议,不能自动开工。</small></section>`}
|
|
75
|
+
function specs(){const d=data.derived;return heading('规格清单','里程碑 → 执行轮次 → 页面计划批次 → Spec;实际窗口单独记录')+`<section class="panel"><h2>分层交付与门禁</h2>${layers()}<p>阶段回归 ${gate(d.milestone_regression_passed)} 阶段验收 ${gate(d.milestone_accepted)}</p></section><section class="panel"><h2>全部规格</h2><div class="table-wrap"><table><thead><tr><th>Spec / 页面</th><th>范围映射</th><th>能力依赖</th><th>当前验收</th></tr></thead><tbody>${data.plan.specs.map(s=>`<tr><td><button class="link" data-spec="${esc(s.spec_id)}">${esc(s.spec_id)}</button><small>${esc(s.page_id)} / ${esc(s.milestone)}</small></td><td class="title">${esc((s.scope_refs||[]).map(r=>r.feature_id+'/'+r.scope_id).join(', ')||s.scope.join(';'))}</td><td class="title">${esc(s.requires.join(', ')||'无前置能力')}</td><td>${gate(d.tasks[s.spec_id].accepted)}</td></tr>`).join('')}</tbody></table></div></section><section class="panel"><h2>轮次与历史交付</h2><div class="table-wrap"><table><thead><tr><th>轮次 / 类型</th><th>目标</th><th>历史交付</th><th>当前验收</th></tr></thead><tbody>${Object.entries(d.rounds||{}).map(([id,r])=>`<tr><td>${esc(id)}<small>${esc(r.kind)}</small></td><td class="title">${esc(r.goal)}</td><td>${tag(r.delivered?'曾交付':'未交付')}</td><td>${gate(r.current_accepted)}</td></tr>`).join('')}</tbody></table></div></section><section class="panel"><h2>计划批次与当前建议</h2>${Object.entries(d.page_batches||{}).map(([id,b])=>`<details><summary>${esc(id)} · ${esc(b.round_id)} / ${esc(b.page_id)}</summary><p>计划成员:${esc(b.spec_ids.join(', '))}</p><p>当前建议:${esc(b.dispatchable_spec_ids.join(', '))}</p><p>实际窗口:${esc(d.active_batch?.batch_id===id?d.active_batch.spec_ids.join(', '):'未领取')}</p>${b.spec_ids.map(id=>`<p><button class="link" data-spec="${esc(id)}">${esc(id)} → 查看契约</button></p>`).join('')}</details>`).join('')||'<p class="muted">当前不是页面批次模式。</p>'}</section>`}
|
|
76
|
+
function tasks(){return heading('Spec 执行','实现、记录验证、记录验收与当前有效验收并列,保留所有阻塞原因')+`<section class="panel"><div class="toolbar"><input id="query" type="search" aria-label="搜索 Spec" placeholder="搜索 Spec / 范围"><select id="filter" aria-label="筛选 Spec"><option value="">全部 Spec</option><option value="running">正在实施</option><option value="gaps">工作包有缺口</option><option value="relevant">当前目标及必要依赖</option></select></div><div id="task-table" class="table-wrap"></div></section>`}
|
|
77
|
+
function taskTable(){const d=data.derived,xs=data.plan.specs.filter(s=>{const t=d.tasks[s.spec_id];return(s.spec_id+' '+s.scope.join(' ')).toLowerCase().includes(query.toLowerCase())&&(!filter||(filter==='gaps'&&t.readiness_gaps.length)||(filter==='running'&&d.running.includes(s.spec_id))||(filter==='relevant'&&d.relevant_specs.includes(s.spec_id)))});$('task-table').innerHTML=xs.length?`<table><thead><tr><th>Spec</th><th>实现 / 验证 / 验收记录</th><th>当前有效验收</th><th>工作包 / 接口</th><th>原因</th></tr></thead><tbody>${xs.map(s=>{const t=d.tasks[s.spec_id],raw=data.execution.tasks[s.spec_id];return `<tr><td><button class="link" data-spec="${esc(s.spec_id)}">${esc(s.spec_id)}</button><small>${esc(s.milestone)} / ${esc(s.page_id)}</small></td><td>${esc(raw.implementation)}<small>${esc(raw.validation)} / ${esc(raw.acceptance)}</small></td><td>${gate(t.accepted)}<small>缺 ${t.missing_checks.length} checks</small></td><td>${tag(!data.plan.work_packet_policy?'未启用门禁':t.work_packet_ready?'工作包齐备':'工作包待补')}<small>提供接口待交接 ${t.missing_interfaces.length}</small></td><td class="title">${esc([...t.blocked_by,...t.external_blockers,...t.readiness_gaps].join(';')||'无已知阻塞')}</td></tr>`}).join('')}</tbody></table>`:'<div class="empty">没有符合条件的 Spec</div>'}
|
|
78
|
+
function review(){return heading('交付 Review','原始基线 → 最新约定 → 实现与证据;自动对照尚未完成人工复核')+`<div class="notice">${data.review.baseline_available?'已提供原始范围基线。':'原始范围基线缺失,不能判断新增/修改;重新导出时提供 --baseline-features。'} 人工结论记录到 delivery-review.md;本表不补造测试 PASS。</div><section class="panel"><div class="table-wrap"><table><thead><tr><th>验收范围</th><th>基线差异</th><th>最新约定</th><th>Spec / 当前验收</th><th>人工 Review</th></tr></thead><tbody>${data.review.items.map((i,n)=>`<tr><td class="title"><button class="link" data-review="${n}">${esc(i.description)}</button><small>${esc(i.feature_id)} / ${esc(i.scope_id)}</small></td><td>${esc({'baseline-unavailable':'基线缺失',added:'新增',changed:'变更',unchanged:'无需求变化','missing-from-current':'当前清单缺失'}[i.difference])}</td><td>${esc(i.status)}</td><td>${esc(i.spec_id??'未规划')}<small>${i.accepted?'当前证据通过':'尚无有效通过'}</small></td><td>${tag('待复核','orange')}</td></tr>`).join('')}</tbody></table></div><small>同一 ID 的文本/验收差异只用于定位。拆分、合并、实际新增行为和代码语义需主 Agent 核验;不能靠名称自动判定等价。</small></section>`}
|
|
79
|
+
function syncDraft(){if(page!=='features')return;$('draft-count').textContent='已选 '+draft.size+' 范围项';document.querySelectorAll('[data-scope]').forEach(e=>e.checked=draft.has(e.dataset.scope));document.querySelectorAll('[data-feature]').forEach(e=>{const ids=scopeRows().filter(i=>i.feature_id===e.dataset.feature&&i.status!=='excluded'&&i.feature_status!=='excluded').map(i=>i.key),n=ids.filter(k=>draft.has(k)).length;e.checked=ids.length>0&&n===ids.length;e.indeterminate=n>0&&n<ids.length})}
|
|
80
|
+
function render(){if(!data){$('main').innerHTML=heading('尚未载入运行快照','使用 spec_runtime.py dashboard 导出 HTML,或导入同次运行 JSON。');return}if(!draft)resetDraft();$('nav').innerHTML=pages.map(([id,icon,label])=>`<button data-go="${id}" class="${page===id?'active':''}"><span>${icon}</span>${label}</button>`).join('');$('main').innerHTML=(data.demo?'<div class="notice">演示数据 · 使用回归 fixture 生成,非真实应用实现或测试结果。</div>':'')+({overview,analysis,features,specs,tasks,project,review})[page]()+`<div class="status-line">快照时间:${esc(new Date(data.captured_at).toLocaleString())} · ${stale?'连接失败,保留旧数据':pollUrl?'从同源端点刷新':'离线快照,需重新导出或导入更新'} · 功能 ${data.feature_plan.features.length} / Spec ${data.plan.specs.length}</div>`;if(page==='tasks'){query='';filter='';taskTable()}syncDraft();updateActivity()}
|
|
81
|
+
function download(value){const url=URL.createObjectURL(new Blob([JSON.stringify(value,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=url;a.download='scope-change-request.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000)}
|
|
82
|
+
$('close-detail').onclick=()=>$('detail').close();$('open-json').onclick=()=>$('file').click();$('file').onchange=async event=>{try{const next=valid(JSON.parse(await event.target.files[0].text()));if(pollUrl)throw Error('在线模式请通过端点更新,或移除 snapshot 参数后导入');data=next;draft=null;render();notify('已导入快照,未写入工程')}catch(error){notify(error.message)}};
|
|
83
|
+
document.addEventListener('click',event=>{const b=event.target.closest('button');if(!b)return;if(b.dataset.go){page=b.dataset.go;render();window.scrollTo(0,0)}if(b.dataset.blocker!==undefined)show('阻塞与处理建议',blockers()[Number(b.dataset.blocker)]);if(b.dataset.overview)show('代码规模与来源',{report:overviewData()[b.dataset.overview],sources:overviewData().sources,source_checks:overviewData().source_checks});if(b.dataset.dependency!==undefined)show('依赖依据与迁移处置',{item:overviewData().dependencies[Number(b.dataset.dependency)],sources:overviewData().sources});if(b.dataset.tool!==undefined)show('工具检查原始摘要',{item:overviewData().tools[Number(b.dataset.tool)],sources:overviewData().sources});if(b.dataset.stack!==undefined)show('技术栈来源',{item:overviewData().tech_stack[Number(b.dataset.stack)],sources:overviewData().sources});if(b.dataset.spec){const id=b.dataset.spec;show(id,{plan:data.plan.specs.find(s=>s.spec_id===id),recorded_execution:data.execution.tasks[id],current_derived:data.derived.tasks[id]})}if(b.dataset.review!==undefined)show('逐项 Review 输入',data.review.items[Number(b.dataset.review)]);if(b.id==='reset-draft'){resetDraft();render()}if(b.dataset.preset){if(draftIdentity!==identity()){notify('运行基线已变化,请撤回并重载后选择预设。');return}const keys=deliveryPresetKeys(data.feature_plan,b.dataset.preset);if(!keys.length){notify('该预设没有可选范围,交付分层尚未配置完整;已保留当前选择,请修订 scope_layers。');return}draft=new Set(keys);draftTarget=b.dataset.preset;render()}if(b.id==='export-draft'){const old=selected(),added=[...draft].filter(k=>!old.has(k)),removed=[...old].filter(k=>!draft.has(k));show('范围修改建议 · 依赖影响尚待主 Agent 核验',`新增 ${added.length} 项,移出本次 ${removed.length} 项;优先级修改保留在草稿。\n不会直接应用计划或改动执行窗口。`);const reason=document.createElement('input');reason.placeholder='修改原因(必填)';reason.setAttribute('aria-label','修改原因');const save=document.createElement('button');save.textContent='导出给主 Agent';save.onclick=()=>{if(!reason.value.trim()){notify('请填写修改原因');return}if(draftIdentity!==identity()){notify('草稿已过期,请重载后合并');return}download({schema:'d2h-scope-change-request/v1',feature_hash:draftHash,plan_hash:data.execution.plan_hash,event_sequence:data.event_sequence,candidate:data.execution.candidate,target:draftTarget,scope_refs:[...draft].map(k=>{const [feature_id,scope_id]=JSON.parse(k);return{feature_id,scope_id}}),user_priorities:priorityDraft,reason:reason.value.trim()});$('detail').close()};$('detail').append(reason,save);$('detail').addEventListener('close',()=>{reason.remove();save.remove()},{once:true})}});
|
|
84
|
+
document.addEventListener('change',event=>{const e=event.target;if(e.dataset.scope){e.checked?draft.add(e.dataset.scope):draft.delete(e.dataset.scope);syncDraft()}if(e.dataset.feature){scopeRows().filter(i=>i.feature_id===e.dataset.feature&&i.status!=='excluded'&&i.feature_status!=='excluded').forEach(i=>e.checked?draft.add(i.key):draft.delete(i.key));syncDraft()}if(e.dataset.priority)priorityDraft[e.dataset.priority]=e.value;if(e.id==='filter'){filter=e.value;taskTable()}});document.addEventListener('input',event=>{if(event.target.id==='query'){query=event.target.value;taskTable()}});
|
|
85
|
+
try{if(data)valid(data);const requested=new URL(location.href).searchParams.get('snapshot');if(requested){const url=new URL(requested,location.href);if(!['http:','https:'].includes(url.protocol)||url.origin!==location.origin)throw Error('快照端点必须为同源 HTTP(S)');pollUrl=url.href}render()}catch(error){notify(error.message);pollUrl=null;render()}
|
|
86
|
+
async function poll(){if(!pollUrl)return;try{const response=await fetch(pollUrl,{cache:'no-store'});if(!response.ok)throw Error('HTTP '+response.status);data=valid(await response.json());stale=false;$('connection').textContent='已连接 · 约 3 秒刷新';render()}catch(error){stale=true;$('connection').textContent='连接失败 · 保留最后快照';render()}finally{setTimeout(poll,3000)}}poll();
|
|
87
|
+
setInterval(updateActivity,3000);
|
|
88
|
+
</script></body></html>
|