@runweave/server-linux-x64 0.1.24 → 0.1.25
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/manifest.json +189 -174
- package/native.tar.gz +0 -0
- package/package.json +1 -1
- package/payload/app-server/build-info.json +3 -3
- package/payload/app-server/index.cjs +1 -1
- package/payload/backend/build-info.json +4 -4
- package/payload/backend/index.cjs +2062 -1629
- package/payload/frontend/dist/assets/{_baseUniq-Ba3pSqKA.js → _baseUniq-pNlq_DCQ.js} +1 -1
- package/payload/frontend/dist/assets/{access-C1Ww0gdD.js → access-BJmFgyT9.js} +1 -1
- package/payload/frontend/dist/assets/{advanced-BZJuI68m.js → advanced-B1_D3p4w.js} +1 -1
- package/payload/frontend/dist/assets/{appearance-Btma-0NG.js → appearance-CSQVEVTh.js} +1 -1
- package/payload/frontend/dist/assets/{arc-B44-ZMF9.js → arc-DE1dO7p3.js} +1 -1
- package/payload/frontend/dist/assets/{architectureDiagram-Q4EWVU46-CJ7zCB_3.js → architectureDiagram-Q4EWVU46-QKJomhdt.js} +1 -1
- package/payload/frontend/dist/assets/{blockDiagram-DXYQGD6D-l7dgY79m.js → blockDiagram-DXYQGD6D-CV58X8VO.js} +1 -1
- package/payload/frontend/dist/assets/{c4Diagram-AHTNJAMY-aNNQInyV.js → c4Diagram-AHTNJAMY-DPu-RnDw.js} +1 -1
- package/payload/frontend/dist/assets/channel-DWLRGtq3.js +1 -0
- package/payload/frontend/dist/assets/{chunk-4BX2VUAB-2W54u8LL.js → chunk-4BX2VUAB-COwHdW0x.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-4TB4RGXK-CJuTStu-.js → chunk-4TB4RGXK-StHcdOx9.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-55IACEB6-DcOWjj1X.js → chunk-55IACEB6-D7PMYZ35.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-EDXVE4YY-Depu37hh.js → chunk-EDXVE4YY-D84apqdf.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-FMBD7UC4-CTzWCktW.js → chunk-FMBD7UC4-CJmwcVvj.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-OYMX7WX6-DryiNt82.js → chunk-OYMX7WX6-DQhh6xQp.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-QZHKN3VN-DVwx49kx.js → chunk-QZHKN3VN-C6X5cUuc.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-YZCP3GAM-X1PkQV8g.js → chunk-YZCP3GAM-CryVThNG.js} +1 -1
- package/payload/frontend/dist/assets/classDiagram-6PBFFD2Q-B2WrDPFz.js +1 -0
- package/payload/frontend/dist/assets/classDiagram-v2-HSJHXN6E-B2WrDPFz.js +1 -0
- package/payload/frontend/dist/assets/clone-BJQLpgjC.js +1 -0
- package/payload/frontend/dist/assets/{connection-zVFEL2VH.js → connection-425RE3U4.js} +1 -1
- package/payload/frontend/dist/assets/{connections-BYoc3Px5.js → connections-Dx05bPzL.js} +1 -1
- package/payload/frontend/dist/assets/{cose-bilkent-S5V4N54A-D5JSjBwS.js → cose-bilkent-S5V4N54A-DqICEhiJ.js} +1 -1
- package/payload/frontend/dist/assets/{dagre-KV5264BT-C7wEsUEu.js → dagre-KV5264BT-SyIBcIz3.js} +1 -1
- package/payload/frontend/dist/assets/{desktop-DlSOatR2.js → desktop-DBzea7Bt.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-5BDNPKRD-BJVOoWUV.js → diagram-5BDNPKRD-xS_FVHQR.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-G4DWMVQ6-B-dyOrNQ.js → diagram-G4DWMVQ6-CLuycLkG.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-MMDJMWI5-DoTfUnHA.js → diagram-MMDJMWI5-BTNd9OzE.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-TYMM5635-yVeltO6V.js → diagram-TYMM5635-DyxLhyoy.js} +1 -1
- package/payload/frontend/dist/assets/{erDiagram-SMLLAGMA-BHIOahrf.js → erDiagram-SMLLAGMA-C2a9uz8_.js} +1 -1
- package/payload/frontend/dist/assets/{flowDiagram-DWJPFMVM-Bnkh3X2r.js → flowDiagram-DWJPFMVM-DndbO3ER.js} +1 -1
- package/payload/frontend/dist/assets/{ganttDiagram-T4ZO3ILL-BRFZUfp9.js → ganttDiagram-T4ZO3ILL-BSV1btDD.js} +1 -1
- package/payload/frontend/dist/assets/{gitGraphDiagram-UUTBAWPF-wXS21tDN.js → gitGraphDiagram-UUTBAWPF-DnYc3Xm2.js} +1 -1
- package/payload/frontend/dist/assets/{graph-Cwj2x7na.js → graph-iOswNacw.js} +1 -1
- package/payload/frontend/dist/assets/{image-CXpMpz27.js → image-BaXi5ATy.js} +1 -1
- package/payload/frontend/dist/assets/{index-B2PyTNyh.js → index-BRwhFF-d.js} +6 -6
- package/payload/frontend/dist/assets/{infoDiagram-42DDH7IO-BzTeaphH.js → infoDiagram-42DDH7IO-BoWdmtBx.js} +1 -1
- package/payload/frontend/dist/assets/{ishikawaDiagram-UXIWVN3A-C_i1EUdW.js → ishikawaDiagram-UXIWVN3A-B8KC_SgA.js} +1 -1
- package/payload/frontend/dist/assets/{journeyDiagram-VCZTEJTY-B1fcPcA4.js → journeyDiagram-VCZTEJTY-CN_MxT78.js} +1 -1
- package/payload/frontend/dist/assets/{kanban-definition-6JOO6SKY-BC5fXbSI.js → kanban-definition-6JOO6SKY-j17QaQ4r.js} +1 -1
- package/payload/frontend/dist/assets/{layout-dRQexo61.js → layout-CLR2o63S.js} +1 -1
- package/payload/frontend/dist/assets/{linear-DpsIhjfr.js → linear-CL0d3XrR.js} +1 -1
- package/payload/frontend/dist/assets/{main-BFEoEwax.js → main-DWkxAGaw.js} +110 -110
- package/payload/frontend/dist/assets/{min-Rr06WGlZ.js → min-2aYi0G6U.js} +1 -1
- package/payload/frontend/dist/assets/{mindmap-definition-QFDTVHPH-CAecNSWV.js → mindmap-definition-QFDTVHPH-CxVQlE5J.js} +1 -1
- package/payload/frontend/dist/assets/{monitoring-D9LcExp-.js → monitoring-GwSez8fI.js} +1 -1
- package/payload/frontend/dist/assets/{notifications-BgW6p0Rk.js → notifications-Bx24ROH2.js} +1 -1
- package/payload/frontend/dist/assets/{pieDiagram-DEJITSTG-CJgyNjut.js → pieDiagram-DEJITSTG-CpRHmWOC.js} +1 -1
- package/payload/frontend/dist/assets/{quadrantDiagram-34T5L4WZ-C_60Mu33.js → quadrantDiagram-34T5L4WZ-CZG_QGX1.js} +1 -1
- package/payload/frontend/dist/assets/{record-content-BEOv45kJ.js → record-content-Di3pmXSa.js} +1 -1
- package/payload/frontend/dist/assets/{requirementDiagram-MS252O5E-C3HwQcbl.js → requirementDiagram-MS252O5E-C1PCFhMe.js} +1 -1
- package/payload/frontend/dist/assets/{sankeyDiagram-XADWPNL6-RJ6EzD70.js → sankeyDiagram-XADWPNL6-RxkobNrx.js} +1 -1
- package/payload/frontend/dist/assets/{sequenceDiagram-FGHM5R23-QiYuqsIk.js → sequenceDiagram-FGHM5R23-Bem61Ef-.js} +1 -1
- package/payload/frontend/dist/assets/{stateDiagram-FHFEXIEX-DscVhCkw.js → stateDiagram-FHFEXIEX-DaHV-La3.js} +1 -1
- package/payload/frontend/dist/assets/stateDiagram-v2-QKLJ7IA2-VVMK0Gty.js +1 -0
- package/payload/frontend/dist/assets/{status-TMGwGaid.js → status-DytAqsKi.js} +1 -1
- package/payload/frontend/dist/assets/suiji-ClmlHdOg.js +1 -0
- package/payload/frontend/dist/assets/{svg-BR3UkgEE.js → svg-BNFCoAlV.js} +1 -1
- package/payload/frontend/dist/assets/{tabs-FQPVRwwQ.js → tabs-CNVBHmw-.js} +1 -1
- package/payload/frontend/dist/assets/{timeline-definition-GMOUNBTQ-CrYFg2HY.js → timeline-definition-GMOUNBTQ-xVCcgT05.js} +1 -1
- package/payload/frontend/dist/assets/{vennDiagram-DHZGUBPP-Do4I-ilO.js → vennDiagram-DHZGUBPP-D4YvcLlt.js} +1 -1
- package/payload/frontend/dist/assets/{wardley-RL74JXVD-CA9NRCtl.js → wardley-RL74JXVD-WtdSbuj2.js} +1 -1
- package/payload/frontend/dist/assets/{wardleyDiagram-NUSXRM2D-gpCyIXuK.js → wardleyDiagram-NUSXRM2D-Br4RBb8X.js} +1 -1
- package/payload/frontend/dist/assets/{xychartDiagram-5P7HB3ND-BN662VL0.js → xychartDiagram-5P7HB3ND-Dx8RJOfi.js} +1 -1
- package/payload/frontend/dist/build-info.json +4 -4
- package/payload/frontend/dist/companion.html +1 -1
- package/payload/frontend/dist/index.html +2 -2
- package/payload/plugins/toolkit/.codex-plugin/plugin.json +1 -1
- package/payload/plugins/toolkit/README.md +3 -3
- package/payload/plugins/toolkit/skills/acceptance/SKILL.md +4 -2
- package/payload/plugins/toolkit/skills/acceptance/references/delivery.md +11 -1
- package/payload/plugins/toolkit/skills/acceptance/references/gallery-contract.md +62 -39
- package/payload/plugins/toolkit/skills/acceptance/references/gallery-copy.md +8 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/delivery_check.py +151 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/gallery.py +67 -11
- package/payload/plugins/toolkit/skills/acceptance/scripts/preview.py +21 -2
- package/payload/plugins/toolkit/skills/acceptance/templates/reader.css +1 -14
- package/payload/plugins/toolkit/skills/acceptance/templates/reader.html +23 -4
- package/payload/plugins/toolkit/skills/acceptance/templates/reader.js +76 -3
- package/payload/plugins/toolkit/skills/acceptance/templates/report.css +80 -0
- package/payload/plugins/toolkit/skills/playwright-cli/SKILL.md +19 -6
- package/payload/plugins/toolkit/skills/review-only/SKILL.md +29 -4
- package/payload/plugins/toolkit/skills/review-only/references/delivery-verification.md +38 -0
- package/payload/plugins/toolkit/skills/using-rw/SKILL.md +37 -1
- package/payload/plugins/toolkit-release.json +1 -1
- package/payload/pty-host/index.cjs +72 -10
- package/payload/frontend/dist/assets/channel-DVHsGC9c.js +0 -1
- package/payload/frontend/dist/assets/classDiagram-6PBFFD2Q-CmciHFmk.js +0 -1
- package/payload/frontend/dist/assets/classDiagram-v2-HSJHXN6E-CmciHFmk.js +0 -1
- package/payload/frontend/dist/assets/clone-DxGSjF8z.js +0 -1
- package/payload/frontend/dist/assets/stateDiagram-v2-QKLJ7IA2-CBosnJqY.js +0 -1
- package/payload/frontend/dist/assets/suiji-BFib0tK9.js +0 -1
|
@@ -1,20 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 图文材料与验收报告数据合同
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 输入与兼容
|
|
4
4
|
|
|
5
5
|
UTF-8 JSON,拒绝重复键、未知字段、NaN 和未知版本。文件路径相对 `--input-root`;`--input` 默认 `task.json`。根目录必须已存在。ID 为 1–80 位字母、数字、`-`、`_`,首位为字母或数字。
|
|
6
6
|
|
|
7
|
+
新报告使用 `schemaVersion: 2`。旧版 `schemaVersion: 1` 输入继续支持,不修改历史产物,也不根据旧页的 observed 自动推导通过结论。v1 不接受 report 或 item.summary;升级时必须按实际记录补齐报告,不能仅改版本号。
|
|
8
|
+
|
|
7
9
|
| 对象 | 字段 |
|
|
8
10
|
| ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
-
| root | `schemaVersion: 1`、`task`、`assets: []`、`pages: []`
|
|
11
|
+
| root | `schemaVersion: 1/2`、`task`、`assets: []`、`pages: []`;v2 必须有 `report` |
|
|
10
12
|
| task | `id`、`title`、`scope`、`materialKind: current/historical`、`limitations: string[]` |
|
|
11
13
|
| asset | 唯一 `id`、`kind: image/text`、`path`、`label`、`origin` |
|
|
12
14
|
| origin | `surface`、`capturedAt`、`sourceRef`,每项为非空字符串或 `null` |
|
|
13
15
|
| page | 唯一 `id`、`type`、`title`、`description`、`evidenceState`、`items`、`sourceIds: string[]`;可选 `lookAt`、`limitations: string[]` |
|
|
14
|
-
| item | `assetId`、`label`;图片项必需 `alt`,可选 `crop: {x,y,width,height}
|
|
16
|
+
| item | `assetId`、`label`;图片项必需 `alt`,可选 `crop: {x,y,width,height}`;v2 文字项可选 `summary`,不接受 alt/crop |
|
|
17
|
+
| report | `summary`、非空数组 `checks` |
|
|
18
|
+
| check | 唯一 `id`、`title`、`expected`、`actual`、`execution`、`outcome`、`pageIds: string[]` |
|
|
15
19
|
|
|
16
20
|
文字必填项须为非空字符串。时间按实际采集记录填写,建议带时区 ISO 8601;不知道则 `null`。版本、设备、对象身份等条件写在 scope、description 或关联文字记录中,`sourceRef` 给其原始位置或记录标识。各素材允许不同来源,但不能因此宣称同一事件。
|
|
17
21
|
|
|
22
|
+
## 验收项与首屏总览(v2)
|
|
23
|
+
|
|
24
|
+
- `report.summary`:作者写的一段总体判断及关键保留条件;只总结本次真实记录,不由生成器计算“整体通过”。task.limitations 直接可见,不折叠关键边界。
|
|
25
|
+
- 每项 check 对应一个可判断的用户目标。`expected` 写标准,`actual` 写操作后观察或未执行/阻塞原因。不要把“桌面 UI 检查”和“真机扫码成功”合为一项再统一标通过。
|
|
26
|
+
- `execution` 为 `real`(实际执行)、`mock`(确实在 Mock/模拟环境执行)、`not-run`(没有执行)。没有证据不能凭空改称 Mock;运行端及浏览器模拟/真机的具体边界仍需写明。
|
|
27
|
+
- `outcome` 为 `pass`、`fail`、`blocked`、`not-run`,是报告作者记录的**自检结果**,不代表用户验收确认。实际执行后观察到不符合标准可记 fail;无法得出结果时记 blocked 并说明原因。
|
|
28
|
+
- `pageIds` 只引用本报告已有证据页且不重复,可为空。pass/fail 必须关联至少一页 observed;pass 不可引用 insufficient/not-run 的页作为支持。not-run 结果只能配 not-run 执行;未执行不能标 pass/fail。
|
|
29
|
+
- 失败、阻塞、未执行项在总览中优先且默认展开;通过项显示标题及执行方式,按需展开预期、实际和证据链接。是否有图不影响缺口的可见性。
|
|
30
|
+
- v2 允许 `pages: []`、`assets: []`,用于只有阻塞或未执行项的报告,不要求制造截图或无意义文字文件。v1 仍至少需要一页。
|
|
31
|
+
|
|
32
|
+
校验只检查结构、引用和显式状态是否自洽,无法判定截图真假、目标是否遗漏或作者结论是否正确。报告必须覆盖本次约定的目标,包括没有执行的部分;不能仅列成功项。
|
|
33
|
+
|
|
34
|
+
## 证据页与素材
|
|
35
|
+
|
|
18
36
|
| type | items 数量 | 素材 kind |
|
|
19
37
|
| -------- | ---------- | --------- |
|
|
20
38
|
| single | 1 | image |
|
|
@@ -22,63 +40,68 @@ UTF-8 JSON,拒绝重复键、未知字段、NaN 和未知版本。文件路径
|
|
|
22
40
|
| sequence | 2–4 | image |
|
|
23
41
|
| text | 1 | text |
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
页面数组顺序为证据阅读顺序。页面 ID 用于 `#page=<id>`,修改描述时保留 ID。
|
|
44
|
+
|
|
45
|
+
`evidenceState` 为 `observed`(有观察材料)、`insufficient`(证据不足)、`not-run`(未执行),不含业务通过/失败判定。后两项的具体缺口写在正文或 limitations 中。observed 同样可以记录失败,不能根据页数计算通过率。
|
|
26
46
|
|
|
27
|
-
`
|
|
47
|
+
v2 文字项的 `summary` 是面向用户的简短摘要,按正常正文排版;原始文字不被替换或截断,点击“打开原文”即可核对。不填 summary 时仍显示完整等宽原文。来源区只列出处和打开入口,不重复铺开全文。所有文本均为纯文本,不解析 Markdown/HTML。
|
|
28
48
|
|
|
29
|
-
`sourceIds` 只引用 text 素材,可为空。PNG/JPEG 图片最大 32 MiB、宽高各最多 32768px;文字 TXT/MD/JSON 及输入 JSON 最大 2 MiB。JSON 素材也要求合法 JSON。PNG 校验结构和 CRC;JPEG 检查容器与常用 SOF
|
|
49
|
+
`sourceIds` 只引用 text 素材,可为空。PNG/JPEG 图片最大 32 MiB、宽高各最多 32768px;文字 TXT/MD/JSON 及输入 JSON 最大 2 MiB。JSON 素材也要求合法 JSON。PNG 校验结构和 CRC;JPEG 检查容器与常用 SOF 尺寸,完整解码由浏览器验证。素材上限不等于 Runweave 交付上限,见下文预检。
|
|
30
50
|
|
|
31
51
|
`crop` 为原 PNG 整数像素坐标,x/y 非负、width/height 为正且完整位于原图内。JPEG 仅允许完整展示,避免 EXIF 方向与坐标歧义。原图不被重采样或写回。
|
|
32
52
|
|
|
33
53
|
## 最小可执行输入
|
|
34
54
|
|
|
35
|
-
|
|
55
|
+
以下示例明确表示没有执行;保存为 `task.json` 即可生成无素材的范围说明报告。使用时将目标、标准和原因改为本次事实。补充真实证据之后才能改结果,不要复制示例冒充完成验收。
|
|
36
56
|
|
|
37
57
|
```json
|
|
38
58
|
{
|
|
39
|
-
"schemaVersion":
|
|
59
|
+
"schemaVersion": 2,
|
|
40
60
|
"task": {
|
|
41
61
|
"id": "current-task",
|
|
42
|
-
"title": "
|
|
43
|
-
"scope": "
|
|
62
|
+
"title": "当前任务验收报告",
|
|
63
|
+
"scope": "填写本次版本、环境与目标范围",
|
|
44
64
|
"materialKind": "current",
|
|
45
|
-
"limitations": []
|
|
65
|
+
"limitations": ["尚未连接目标设备,不能确认真机行为。"]
|
|
46
66
|
},
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"items": [{ "assetId": "observation", "label": "结果原文" }],
|
|
64
|
-
"sourceIds": []
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
+
"report": {
|
|
68
|
+
"summary": "真机路径尚未执行,本报告不作通过结论。",
|
|
69
|
+
"checks": [
|
|
70
|
+
{
|
|
71
|
+
"id": "phone-flow",
|
|
72
|
+
"title": "手机完成目标操作",
|
|
73
|
+
"expected": "在目标真机完成操作并核对保存后的状态。",
|
|
74
|
+
"actual": "未连接目标设备,没有执行此路径。",
|
|
75
|
+
"execution": "not-run",
|
|
76
|
+
"outcome": "blocked",
|
|
77
|
+
"pageIds": []
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"assets": [],
|
|
82
|
+
"pages": []
|
|
67
83
|
}
|
|
68
84
|
```
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
正文图片、标题旁的原图/原文和来源链接都在阅读器内部打开,不依赖弹窗或页面跳转。图片可切换实际尺寸并滚动;关闭后保持原来的页码和焦点。原图与原文文件仍单独保留,复制成果目录后可直接使用。Backend 的 HTML 预览沙箱与真机阅读仍须通过实际入口核对,不能仅凭本地打开推断。
|
|
86
|
+
有真实记录时按上方 asset/page 合同补充素材。文字项示例为 `{"assetId":"observation","label":"执行记录","summary":"填写该记录支持的实际观察与限制。"}`;check.pageIds 引用对应页的 id,而不是素材 id。
|
|
73
87
|
|
|
74
|
-
##
|
|
88
|
+
## 交付预检、输出与错误
|
|
75
89
|
|
|
76
|
-
`validate` 返回
|
|
90
|
+
- `validate` 返回 `{valid, pages, assets}`,只验证输入,不写文件;不证明可通过 Backend 交付。
|
|
91
|
+
- `build` 默认 `--target runweave`,生成临时成果后按 Runweave 的 HTML 1 MiB、单资源 5 MiB 上限检查完整清单。不可交付则退出 1 并指出文件,最终目录不落盘;不读取凭据或申请票据。
|
|
92
|
+
- 明确需要离线大文件时使用 `build --target portable`。仍保留原素材并返回 `deliveryCheck`,其中 ready:false 表示不能通过当前 Runweave 入口,不能当作已交付成功。
|
|
93
|
+
- `build` 返回 `{output, index, pages, target, deliveryCheck}`。`deliveryCheck` 含 `target`、`ready`、`filesChecked`、`issues`;本地通过不证明资源解码、浏览器加载或手机可达。
|
|
94
|
+
- 已生成报告可运行 `gallery.py preflight --directory <成果目录>`,输出本地检查 JSON,ready 为 false 时退出 1。详细入口与票据流程见 [预览与检查](delivery.md)。
|
|
95
|
+
- 成功退出 0;输入、文件或运行错误退出 1,命令参数错误退出 2,stderr 给出原因。`serve` 前台长驻,第一行返回 `{url,pid,directory,stop}`;Ctrl-C 正常停止。
|
|
77
96
|
|
|
78
|
-
输出文件:`index.html`、`reader.css`、`viewer.css`、`reader.js`、`task.json`、`manifest.json` 与 `assets
|
|
97
|
+
输出文件:`index.html`、`reader.css`、`report.css`、`viewer.css`、`reader.js`、`task.json`、`manifest.json` 与 `assets/`。文字内嵌进页面,不依赖 fetch 或外网;原始文字与图片也单独保留。task.json 保持输入合同,path 改为成果内相对路径。manifest 保存 schemaVersion、templateVersion、原始输入摘要及复制素材 SHA-256。
|
|
79
98
|
|
|
80
99
|
生成失败不留下最终目录,已有输出拒绝覆盖。生成器用同目标的临时锁串行化生成;进程被强杀可能留下 `.gallery-lock` 和 `.gallery-*`,核对没有相同目标构建在运行后,才清理这些本次临时文件或换新输出目录。不要删除旧成果绕过覆盖检查。
|
|
81
100
|
|
|
82
|
-
|
|
101
|
+
每份报告带模板副本,后续升级不改变旧报告。生成器只处理显式列出的素材,不自动爬取来源、执行命令或读整个历史。
|
|
102
|
+
|
|
103
|
+
## 原始素材查看与本地服务
|
|
104
|
+
|
|
105
|
+
正文图片、标题旁的原图/原文和来源链接都在阅读器内部打开,不依赖弹窗或页面跳转。图片可切换实际尺寸并滚动;关闭后保持原来的页码和焦点。原图与原文文件仍单独保留,复制成果目录后可直接使用。Backend 的 HTML 预览沙箱与真机阅读仍须通过实际入口核对,不能仅凭本地打开推断。
|
|
83
106
|
|
|
84
107
|
`serve` 只绑定 `127.0.0.1`,只允许清单中的文件,不开放目录列表和写接口;根目录越界及符号链接逃逸均拒绝。素材路径不接受绝对路径、`..`、反斜线、冒号或远程 URL。页面文本转义显示,不支持任务传入 HTML。输入输出根目录应由当前用户控制,不将此本机工具部署为多用户公共服务。
|
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
|
|
22
22
|
对照前核对目标、运行端、版本、尺寸与条件;不同条件必须说明。有结果图但没有前图时退回单图,不能生成假前图。需要真实运动或闪烁证据的任务不以截图模板替代原采集方式。
|
|
23
23
|
|
|
24
|
+
## 验收总览与正文
|
|
25
|
+
|
|
26
|
+
新报告先填写 report.summary:本次验证到什么程度、哪些结论受限。逐项列用户目标、预期、实际与对应证据,未执行和阻塞也必须列出。通过结论只适用于明确执行的范围;桌面 UI、浏览器模拟和真机路径分别判断。真实/Mock 是执行条件,不是证据缺失的代称。
|
|
27
|
+
|
|
28
|
+
总览优先呈现失败、阻塞、未执行;不以“是否有截图”决定重要性。代码变更清单和技术实现按需作为辅助材料,不占据用户验收主线。不要把 Agent 自检记录写成用户已确认。
|
|
29
|
+
|
|
30
|
+
文字页可用 item.summary 写面向用户的简短摘要;原始日志保留在素材与原文查看器中,不在摘要、正文、来源区反复粘贴全文。不根据文件后缀猜测总结,不改写或截断原始证据。
|
|
31
|
+
|
|
24
32
|
## 文案如何写
|
|
25
33
|
|
|
26
34
|
标题说明本页要核对的结果;建议 12–24 个中文字。正文以 1–2 句交代“条件、动作、实际观察”,建议 40–100 字。软长度目标不覆盖准确性要求。
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Local checks for generated galleries; no credentials, network or HTML crawling."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import re
|
|
5
|
+
import stat
|
|
6
|
+
|
|
7
|
+
# Keep the delivery limits aligned with backend/src/terminal/preview/html-preview.ts.
|
|
8
|
+
MAX_HTML_BYTES = 1024 * 1024
|
|
9
|
+
MAX_RESOURCE_BYTES = 5 * 1024 * 1024
|
|
10
|
+
MAX_MANIFEST_BYTES = 2 * 1024 * 1024
|
|
11
|
+
_TEMPLATE_VERSIONS = ('1.0.0', '1.1.0', '1.2.0')
|
|
12
|
+
_SHA256 = re.compile(r'[0-9a-fA-F]{64}')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def check_gallery(directory: Path) -> dict:
|
|
16
|
+
"""Check only fixed files and explicit manifest assets, counting unique paths.
|
|
17
|
+
|
|
18
|
+
ready means these local files meet delivery limits, not that a browser loaded
|
|
19
|
+
their contents or a phone can reach the Backend. Empty asset lists are valid.
|
|
20
|
+
"""
|
|
21
|
+
result = {'target': 'runweave-html', 'ready': False, 'filesChecked': 0, 'issues': []}
|
|
22
|
+
|
|
23
|
+
def issue(path, reason, **details):
|
|
24
|
+
result['issues'].append({'path': path, 'reason': reason, **details})
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
root = directory.resolve(strict=True)
|
|
28
|
+
if not root.is_dir():
|
|
29
|
+
raise ValueError('Gallery root must be a directory')
|
|
30
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
31
|
+
issue(str(directory), str(error))
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
checked = {}
|
|
35
|
+
|
|
36
|
+
def inspect(relative, limit):
|
|
37
|
+
if relative in checked:
|
|
38
|
+
return checked[relative]
|
|
39
|
+
checked[relative] = None
|
|
40
|
+
result['filesChecked'] += 1
|
|
41
|
+
if (not relative or '\\' in relative or '\x00' in relative or
|
|
42
|
+
any(part in ('', '.', '..') for part in relative.split('/'))):
|
|
43
|
+
issue(relative, 'Invalid relative file path; directory escapes are not allowed')
|
|
44
|
+
return None
|
|
45
|
+
try:
|
|
46
|
+
resolved = (root / relative).resolve(strict=True)
|
|
47
|
+
if not resolved.is_relative_to(root):
|
|
48
|
+
issue(relative, 'File or symlink escapes the gallery directory')
|
|
49
|
+
return None
|
|
50
|
+
info = resolved.stat()
|
|
51
|
+
if not stat.S_ISREG(info.st_mode):
|
|
52
|
+
issue(relative, 'Expected a regular file')
|
|
53
|
+
return None
|
|
54
|
+
if info.st_size > limit:
|
|
55
|
+
issue(relative, 'File exceeds local delivery size limit',
|
|
56
|
+
bytes=info.st_size, limitBytes=limit)
|
|
57
|
+
return None
|
|
58
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
59
|
+
issue(relative, 'Missing or inaccessible file: ' + str(error))
|
|
60
|
+
return None
|
|
61
|
+
checked[relative] = resolved
|
|
62
|
+
return resolved
|
|
63
|
+
|
|
64
|
+
for name in ('index.html', 'reader.css', 'reader.js', 'task.json', 'manifest.json'):
|
|
65
|
+
limit = (MAX_HTML_BYTES if name == 'index.html' else
|
|
66
|
+
MAX_MANIFEST_BYTES if name == 'manifest.json' else MAX_RESOURCE_BYTES)
|
|
67
|
+
inspect(name, limit)
|
|
68
|
+
manifest_path = checked['manifest.json']
|
|
69
|
+
if manifest_path is None:
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
def unique_object(pairs):
|
|
73
|
+
obj = {}
|
|
74
|
+
for key, value in pairs:
|
|
75
|
+
if key in obj:
|
|
76
|
+
raise ValueError('Duplicate JSON key: ' + key)
|
|
77
|
+
obj[key] = value
|
|
78
|
+
return obj
|
|
79
|
+
|
|
80
|
+
def reject_constant(value):
|
|
81
|
+
raise ValueError('Invalid JSON constant: ' + value)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Bound the read as well as the stat, even if the file changed meanwhile.
|
|
85
|
+
with manifest_path.open('rb') as stream:
|
|
86
|
+
data = stream.read(MAX_MANIFEST_BYTES + 1)
|
|
87
|
+
if len(data) > MAX_MANIFEST_BYTES:
|
|
88
|
+
issue('manifest.json', 'Manifest exceeds local parsing limit',
|
|
89
|
+
bytes=len(data), limitBytes=MAX_MANIFEST_BYTES)
|
|
90
|
+
return result
|
|
91
|
+
manifest = json.loads(data.decode('utf-8'), object_pairs_hook=unique_object,
|
|
92
|
+
parse_constant=reject_constant)
|
|
93
|
+
except (OSError, ValueError, RecursionError) as error:
|
|
94
|
+
issue('manifest.json', 'Invalid or unreadable manifest JSON: ' + str(error))
|
|
95
|
+
return result
|
|
96
|
+
if not isinstance(manifest, dict):
|
|
97
|
+
issue('manifest.json', 'Manifest must be an object')
|
|
98
|
+
return result
|
|
99
|
+
if type(manifest.get('schemaVersion')) is not int or manifest['schemaVersion'] != 1:
|
|
100
|
+
issue('manifest.json', 'Expected manifest schemaVersion 1')
|
|
101
|
+
version = manifest.get('templateVersion')
|
|
102
|
+
if version not in _TEMPLATE_VERSIONS:
|
|
103
|
+
issue('manifest.json', 'Unsupported or missing templateVersion')
|
|
104
|
+
if version in ('1.1.0', '1.2.0'):
|
|
105
|
+
inspect('viewer.css', MAX_RESOURCE_BYTES)
|
|
106
|
+
if version == '1.2.0':
|
|
107
|
+
inspect('report.css', MAX_RESOURCE_BYTES)
|
|
108
|
+
if not isinstance(manifest.get('inputSha256'), str) or not _SHA256.fullmatch(manifest['inputSha256']):
|
|
109
|
+
issue('manifest.json', 'inputSha256 must be a SHA-256 hex string')
|
|
110
|
+
assets = manifest.get('assets')
|
|
111
|
+
if not isinstance(assets, list):
|
|
112
|
+
issue('manifest.json', 'assets must be an array (it may be empty)')
|
|
113
|
+
return result
|
|
114
|
+
ids, paths = set(), set()
|
|
115
|
+
for index, asset in enumerate(assets):
|
|
116
|
+
location = f'manifest.json assets[{index}]'
|
|
117
|
+
if not isinstance(asset, dict):
|
|
118
|
+
issue('manifest.json', location + ' must be an object')
|
|
119
|
+
continue
|
|
120
|
+
asset_id = asset.get('id')
|
|
121
|
+
if not isinstance(asset_id, str) or not asset_id.strip():
|
|
122
|
+
issue('manifest.json', location + '.id must be a nonempty string')
|
|
123
|
+
elif asset_id in ids:
|
|
124
|
+
issue('manifest.json', location + '.id is duplicated')
|
|
125
|
+
else:
|
|
126
|
+
ids.add(asset_id)
|
|
127
|
+
if not isinstance(asset.get('sha256'), str) or not _SHA256.fullmatch(asset['sha256']):
|
|
128
|
+
issue('manifest.json', location + '.sha256 must be a SHA-256 hex string')
|
|
129
|
+
relative = asset.get('path')
|
|
130
|
+
if not isinstance(relative, str) or not relative:
|
|
131
|
+
issue('manifest.json', location + '.path must be a nonempty relative path')
|
|
132
|
+
continue
|
|
133
|
+
if relative in paths:
|
|
134
|
+
issue(relative, 'Duplicate manifest asset path')
|
|
135
|
+
paths.add(relative)
|
|
136
|
+
inspect(relative, MAX_RESOURCE_BYTES)
|
|
137
|
+
result['ready'] = not result['issues']
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def require_ready(result: dict) -> None:
|
|
142
|
+
"""Stop delivery on failed local checks and identify the affected files."""
|
|
143
|
+
if result.get('ready') is True:
|
|
144
|
+
return
|
|
145
|
+
details = []
|
|
146
|
+
for item in result.get('issues', []):
|
|
147
|
+
description = f'{item["path"]}: {item["reason"]}'
|
|
148
|
+
if 'bytes' in item and 'limitBytes' in item:
|
|
149
|
+
description += f' ({item["bytes"]} bytes; limit {item["limitBytes"]} bytes)'
|
|
150
|
+
details.append(description)
|
|
151
|
+
raise ValueError('Runweave HTML local preflight failed: ' + ('; '.join(details) or 'not ready'))
|
|
@@ -16,7 +16,10 @@ import zlib
|
|
|
16
16
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
17
17
|
from urllib.parse import unquote, urlsplit
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
sys.dont_write_bytecode = True
|
|
20
|
+
from delivery_check import check_gallery, require_ready
|
|
21
|
+
|
|
22
|
+
TEMPLATE_VERSION = '1.2.0'
|
|
20
23
|
TEMPLATES = Path(__file__).resolve().parent.parent / 'templates'
|
|
21
24
|
ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$')
|
|
22
25
|
MAX_FILE = 32 * 1024 * 1024
|
|
@@ -136,8 +139,10 @@ def parse_json(data):
|
|
|
136
139
|
def load_task(root, input_name):
|
|
137
140
|
raw = read_file(local_file(root, input_name), MAX_TEXT)
|
|
138
141
|
task = parse_json(raw)
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
require(isinstance(task, dict), 'root', '需要对象')
|
|
143
|
+
version = task.get('schemaVersion')
|
|
144
|
+
require(type(version) is int and version in (1, 2), 'schemaVersion', '仅支持版本 1/2')
|
|
145
|
+
fields(task, 'root', ['schemaVersion', 'task', 'assets', 'pages'] + (['report'] if version == 2 else []))
|
|
141
146
|
meta = task['task']
|
|
142
147
|
fields(meta, 'task', ['id', 'title', 'scope', 'materialKind', 'limitations'])
|
|
143
148
|
identifier(meta['id'], 'task.id')
|
|
@@ -170,7 +175,8 @@ def load_task(root, input_name):
|
|
|
170
175
|
parse_json(data)
|
|
171
176
|
assets[asset['id']] = asset
|
|
172
177
|
payloads[asset['id']] = (data, suffix)
|
|
173
|
-
require(isinstance(task['pages'], list)
|
|
178
|
+
require(isinstance(task['pages'], list), 'pages', '需要数组')
|
|
179
|
+
require(version == 2 or len(task['pages']) > 0, 'pages', '版本 1 至少需要一页')
|
|
174
180
|
seen = set()
|
|
175
181
|
for i, page in enumerate(task['pages']):
|
|
176
182
|
where = f'pages[{i}]'
|
|
@@ -190,7 +196,7 @@ def load_task(root, input_name):
|
|
|
190
196
|
require(low <= len(items) <= high, where, f'当前页型需要 {low}–{high} 个素材项')
|
|
191
197
|
for j, item in enumerate(items):
|
|
192
198
|
iw = where + f'.items[{j}]'
|
|
193
|
-
fields(item, iw, ['assetId', 'label'], ['alt', 'crop'])
|
|
199
|
+
fields(item, iw, ['assetId', 'label'], ['alt', 'crop'] + (['summary'] if version == 2 else []))
|
|
194
200
|
string(item['assetId'], iw + '.assetId')
|
|
195
201
|
string(item['label'], iw + '.label')
|
|
196
202
|
require(item['assetId'] in assets, iw, '素材 ID 不存在')
|
|
@@ -201,6 +207,9 @@ def load_task(root, input_name):
|
|
|
201
207
|
string(item.get('alt'), iw + '.alt')
|
|
202
208
|
else:
|
|
203
209
|
require('crop' not in item and 'alt' not in item, iw, '文字项不接受 crop/alt')
|
|
210
|
+
if 'summary' in item:
|
|
211
|
+
require(kind == 'text', iw, 'summary 仅用于文字素材摘要')
|
|
212
|
+
string(item['summary'], iw + '.summary')
|
|
204
213
|
if 'crop' in item:
|
|
205
214
|
crop = item['crop']
|
|
206
215
|
fields(crop, iw + '.crop', ['x', 'y', 'width', 'height'])
|
|
@@ -213,14 +222,47 @@ def load_task(root, input_name):
|
|
|
213
222
|
strings(page['sourceIds'], where + '.sourceIds')
|
|
214
223
|
for source_id in page['sourceIds']:
|
|
215
224
|
require(source_id in assets and assets[source_id]['kind'] == 'text', where, 'sourceIds 只能引用存在的文字素材')
|
|
225
|
+
if version == 2:
|
|
226
|
+
validate_report(task['report'], task['pages'])
|
|
216
227
|
return task, payloads, image_sizes, hashlib.sha256(raw).hexdigest()
|
|
217
228
|
|
|
218
229
|
|
|
230
|
+
def validate_report(report, pages):
|
|
231
|
+
fields(report, 'report', ['summary', 'checks'])
|
|
232
|
+
string(report['summary'], 'report.summary')
|
|
233
|
+
require(isinstance(report['checks'], list) and report['checks'], 'report.checks', '至少需要一项验收目标')
|
|
234
|
+
by_id = {page['id']: page for page in pages}
|
|
235
|
+
seen = set()
|
|
236
|
+
for i, check in enumerate(report['checks']):
|
|
237
|
+
where = f'report.checks[{i}]'
|
|
238
|
+
fields(check, where, ['id', 'title', 'expected', 'actual', 'execution', 'outcome', 'pageIds'])
|
|
239
|
+
identifier(check['id'], where + '.id')
|
|
240
|
+
require(check['id'] not in seen, where, '重复验收项 ID')
|
|
241
|
+
seen.add(check['id'])
|
|
242
|
+
for key in ('title', 'expected', 'actual'):
|
|
243
|
+
string(check[key], where + '.' + key)
|
|
244
|
+
execution, outcome = check['execution'], check['outcome']
|
|
245
|
+
require(execution in ('real', 'mock', 'not-run'), where, 'execution 需要 real/mock/not-run')
|
|
246
|
+
require(outcome in ('pass', 'fail', 'blocked', 'not-run'), where, 'outcome 需要 pass/fail/blocked/not-run')
|
|
247
|
+
strings(check['pageIds'], where + '.pageIds')
|
|
248
|
+
require(len(set(check['pageIds'])) == len(check['pageIds']), where, '重复证据页 ID')
|
|
249
|
+
require(all(id_ in by_id for id_ in check['pageIds']), where, '证据页 ID 不存在')
|
|
250
|
+
if outcome == 'not-run':
|
|
251
|
+
require(execution == 'not-run', where, '未执行结果不能声明已实际执行或 Mock 执行')
|
|
252
|
+
if execution == 'not-run':
|
|
253
|
+
require(outcome in ('not-run', 'blocked'), where, '未执行不能声明通过或失败')
|
|
254
|
+
if outcome in ('pass', 'fail'):
|
|
255
|
+
evidence = [by_id[id_]['evidenceState'] for id_ in check['pageIds']]
|
|
256
|
+
require('observed' in evidence, where, '通过或失败需要关联实际观察的证据页')
|
|
257
|
+
if outcome == 'pass':
|
|
258
|
+
require(all(state == 'observed' for state in evidence), where, '证据不足或未执行的页不能支持通过结论')
|
|
259
|
+
|
|
260
|
+
|
|
219
261
|
def dump(path, value):
|
|
220
262
|
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
|
|
221
263
|
|
|
222
264
|
|
|
223
|
-
def build(loaded, output):
|
|
265
|
+
def build(loaded, output, target='runweave'):
|
|
224
266
|
task, payloads, sizes, input_hash = loaded
|
|
225
267
|
output = output.absolute()
|
|
226
268
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -257,10 +299,13 @@ def build(loaded, output):
|
|
|
257
299
|
before, after = template.split('@@DATA@@')
|
|
258
300
|
before = before.replace('@@TITLE@@', html.escape(task['task']['title']))
|
|
259
301
|
(stage / 'index.html').write_text(before + embedded + after, encoding='utf-8')
|
|
260
|
-
for name in ('reader.css', 'reader.js', 'viewer.css'):
|
|
302
|
+
for name in ('reader.css', 'reader.js', 'viewer.css', 'report.css'):
|
|
261
303
|
shutil.copyfile(TEMPLATES / name, stage / name)
|
|
262
304
|
dump(stage / 'task.json', snapshot)
|
|
263
305
|
dump(stage / 'manifest.json', manifest)
|
|
306
|
+
delivery = check_gallery(stage)
|
|
307
|
+
if target == 'runweave':
|
|
308
|
+
require_ready(delivery)
|
|
264
309
|
require(not output.exists() and not output.is_symlink(), 'output', '目标已存在')
|
|
265
310
|
stage.rename(output)
|
|
266
311
|
stage = None
|
|
@@ -268,7 +313,8 @@ def build(loaded, output):
|
|
|
268
313
|
if stage is not None:
|
|
269
314
|
shutil.rmtree(stage)
|
|
270
315
|
lock.unlink()
|
|
271
|
-
return {'output': str(output), 'index': str(output / 'index.html'), 'pages': len(task['pages'])
|
|
316
|
+
return {'output': str(output), 'index': str(output / 'index.html'), 'pages': len(task['pages']),
|
|
317
|
+
'target': target, 'deliveryCheck': delivery}
|
|
272
318
|
|
|
273
319
|
|
|
274
320
|
def serve(directory, port):
|
|
@@ -276,8 +322,10 @@ def serve(directory, port):
|
|
|
276
322
|
manifest = parse_json(read_file(root / 'manifest.json', MAX_TEXT))
|
|
277
323
|
require(manifest.get('schemaVersion') == 1 and isinstance(manifest.get('assets'), list), 'directory', '不是生成的成果目录')
|
|
278
324
|
allowed = {'index.html', 'reader.css', 'reader.js', 'task.json', 'manifest.json'}
|
|
279
|
-
if manifest.get('templateVersion')
|
|
325
|
+
if manifest.get('templateVersion') in ('1.1.0', TEMPLATE_VERSION):
|
|
280
326
|
allowed.add('viewer.css')
|
|
327
|
+
if manifest.get('templateVersion') == TEMPLATE_VERSION:
|
|
328
|
+
allowed.add('report.css')
|
|
281
329
|
allowed.update(asset['path'] for asset in manifest['assets'])
|
|
282
330
|
for relative in allowed:
|
|
283
331
|
local_file(root, relative)
|
|
@@ -324,7 +372,7 @@ def serve(directory, port):
|
|
|
324
372
|
|
|
325
373
|
|
|
326
374
|
def main():
|
|
327
|
-
parser = argparse.ArgumentParser(description='本地图文验收阅读器:validate / build / serve')
|
|
375
|
+
parser = argparse.ArgumentParser(description='本地图文验收阅读器:validate / build / preflight / serve')
|
|
328
376
|
sub = parser.add_subparsers(dest='command', required=True)
|
|
329
377
|
for name in ('validate', 'build'):
|
|
330
378
|
command = sub.add_parser(name)
|
|
@@ -332,6 +380,10 @@ def main():
|
|
|
332
380
|
command.add_argument('--input', default='task.json')
|
|
333
381
|
if name == 'build':
|
|
334
382
|
command.add_argument('--output', type=Path, required=True)
|
|
383
|
+
command.add_argument('--target', choices=('runweave', 'portable'), default='runweave',
|
|
384
|
+
help='默认按 Runweave 预览限额预检;portable 只生成离线成果并报告限制')
|
|
385
|
+
command = sub.add_parser('preflight')
|
|
386
|
+
command.add_argument('--directory', type=Path, required=True)
|
|
335
387
|
command = sub.add_parser('serve')
|
|
336
388
|
command.add_argument('--directory', type=Path, required=True)
|
|
337
389
|
command.add_argument('--port', type=int, default=0)
|
|
@@ -341,8 +393,12 @@ def main():
|
|
|
341
393
|
require(0 <= args.port <= 65535, 'port', '端口范围 0–65535')
|
|
342
394
|
serve(args.directory, args.port)
|
|
343
395
|
return
|
|
396
|
+
if args.command == 'preflight':
|
|
397
|
+
result = check_gallery(args.directory)
|
|
398
|
+
print(json.dumps(result, ensure_ascii=False))
|
|
399
|
+
sys.exit(0 if result['ready'] else 1)
|
|
344
400
|
loaded = load_task(args.input_root.resolve(strict=True), args.input)
|
|
345
|
-
result = build(loaded, args.output) if args.command == 'build' else {'valid': True, 'pages': len(loaded[0]['pages']), 'assets': len(loaded[0]['assets'])}
|
|
401
|
+
result = build(loaded, args.output, args.target) if args.command == 'build' else {'valid': True, 'pages': len(loaded[0]['pages']), 'assets': len(loaded[0]['assets'])}
|
|
346
402
|
print(json.dumps(result, ensure_ascii=False))
|
|
347
403
|
except (Invalid, OSError, ValueError, KeyError, TypeError) as error:
|
|
348
404
|
print(f'gallery: {error}', file=sys.stderr)
|
|
@@ -9,6 +9,7 @@ import sys
|
|
|
9
9
|
import urllib.parse
|
|
10
10
|
sys.dont_write_bytecode = True
|
|
11
11
|
from common import base_url, http, write_json
|
|
12
|
+
from delivery_check import MAX_HTML_BYTES, check_gallery, require_ready
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def configure_parser(parser, default_kind='auto'):
|
|
@@ -25,7 +26,8 @@ def configure_parser(parser, default_kind='auto'):
|
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
def preview(args):
|
|
28
|
-
|
|
29
|
+
requested = args.file.absolute()
|
|
30
|
+
file = requested.resolve(strict=True)
|
|
29
31
|
if not file.is_file():
|
|
30
32
|
raise ValueError('Preview input must be a regular file')
|
|
31
33
|
kind = args.kind
|
|
@@ -38,6 +40,23 @@ def preview(args):
|
|
|
38
40
|
raise FileExistsError('Use a new output path for each preview ticket')
|
|
39
41
|
if not args.output.parent.is_dir():
|
|
40
42
|
raise ValueError('Preview output parent must already exist')
|
|
43
|
+
local_check = None
|
|
44
|
+
if kind == 'html':
|
|
45
|
+
manifest = requested.parent / 'manifest.json'
|
|
46
|
+
if requested.name == 'index.html' and (manifest.exists() or manifest.is_symlink()):
|
|
47
|
+
local_check = check_gallery(requested.parent)
|
|
48
|
+
local_check['scope'] = 'gallery-fixed-files-and-manifest-assets'
|
|
49
|
+
else:
|
|
50
|
+
size = file.stat().st_size
|
|
51
|
+
issues = []
|
|
52
|
+
if size > MAX_HTML_BYTES:
|
|
53
|
+
issues.append({'path': str(requested), 'reason': 'HTML entry exceeds preview size limit',
|
|
54
|
+
'bytes': size, 'limitBytes': MAX_HTML_BYTES})
|
|
55
|
+
local_check = {'target': 'runweave-html', 'ready': not issues, 'filesChecked': 1,
|
|
56
|
+
'issues': issues, 'scope': 'html-entry-only',
|
|
57
|
+
'resources': 'not-checked; arbitrary HTML resources are not inspected'}
|
|
58
|
+
# Fail locally before accessing credentials or contacting either Backend URL.
|
|
59
|
+
require_ready(local_check)
|
|
41
60
|
if args.token_env:
|
|
42
61
|
if not args.backend:
|
|
43
62
|
raise ValueError('--token-env requires --backend')
|
|
@@ -89,7 +108,7 @@ def preview(args):
|
|
|
89
108
|
status, _, content = http(url)
|
|
90
109
|
if status != 200 or content != file.read_bytes():
|
|
91
110
|
raise ValueError('Anonymous HTML GET did not match local bytes')
|
|
92
|
-
receipt.update(htmlStatus=200, htmlMatchesLocal=True,
|
|
111
|
+
receipt.update(htmlStatus=200, htmlMatchesLocal=True, localPreflight=local_check,
|
|
93
112
|
resources='not-verified; check styles/scripts/images in a real browser')
|
|
94
113
|
write_json(args.output, receipt, private=True)
|
|
95
114
|
print(json.dumps({k: v for k, v in receipt.items() if k != 'url'}, ensure_ascii=False))
|
|
@@ -128,14 +128,6 @@ ul {
|
|
|
128
128
|
margin-top: 8px;
|
|
129
129
|
max-width: 960px;
|
|
130
130
|
}
|
|
131
|
-
.masthead details {
|
|
132
|
-
font-size: 13px;
|
|
133
|
-
color: var(--muted);
|
|
134
|
-
margin-top: 10px;
|
|
135
|
-
}
|
|
136
|
-
.masthead details ul {
|
|
137
|
-
margin: 6px 0;
|
|
138
|
-
}
|
|
139
131
|
.workspace {
|
|
140
132
|
max-width: 1400px;
|
|
141
133
|
margin: auto;
|
|
@@ -361,11 +353,6 @@ figcaption a {
|
|
|
361
353
|
.source-item p {
|
|
362
354
|
margin-top: 4px;
|
|
363
355
|
}
|
|
364
|
-
.source-item pre {
|
|
365
|
-
margin-top: 8px;
|
|
366
|
-
max-height: 32vh;
|
|
367
|
-
font-size: 12px;
|
|
368
|
-
}
|
|
369
356
|
.navigation {
|
|
370
357
|
position: fixed;
|
|
371
358
|
bottom: 0;
|
|
@@ -516,7 +503,7 @@ figcaption a {
|
|
|
516
503
|
}
|
|
517
504
|
.text-evidence {
|
|
518
505
|
padding: 12px;
|
|
519
|
-
font-size:
|
|
506
|
+
font-size: 14px;
|
|
520
507
|
}
|
|
521
508
|
.notice {
|
|
522
509
|
bottom: 82px;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
8
|
<title>@@TITLE@@</title>
|
|
9
9
|
<link rel="stylesheet" href="reader.css" />
|
|
10
|
+
<link rel="stylesheet" href="report.css" />
|
|
10
11
|
<link rel="stylesheet" href="viewer.css" />
|
|
11
12
|
<script id="gallery-data" type="application/json">
|
|
12
13
|
@@DATA@@
|
|
@@ -25,13 +26,31 @@
|
|
|
25
26
|
<button id="expand" type="button" aria-pressed="false">展开全部</button>
|
|
26
27
|
</div>
|
|
27
28
|
<p id="task-scope"></p>
|
|
28
|
-
<details id="task-limits" hidden>
|
|
29
|
-
<summary>本次材料的范围</summary>
|
|
30
|
-
<ul></ul>
|
|
31
|
-
</details>
|
|
32
29
|
</header>
|
|
30
|
+
<section
|
|
31
|
+
id="report-overview"
|
|
32
|
+
class="report-overview"
|
|
33
|
+
aria-labelledby="report-heading"
|
|
34
|
+
>
|
|
35
|
+
<h2 id="report-heading" tabindex="-1">验收总览</h2>
|
|
36
|
+
<p id="report-summary" class="description"></p>
|
|
37
|
+
<section
|
|
38
|
+
id="task-limits"
|
|
39
|
+
class="limits"
|
|
40
|
+
aria-label="本次范围与限制"
|
|
41
|
+
hidden
|
|
42
|
+
>
|
|
43
|
+
<h3>本次范围与限制</h3>
|
|
44
|
+
<ul></ul>
|
|
45
|
+
</section>
|
|
46
|
+
<p class="report-note">
|
|
47
|
+
以下为报告作者记录的自检结果,不代表用户已确认验收。有观察材料不等于通过。
|
|
48
|
+
</p>
|
|
49
|
+
<ul id="report-checks" class="report-checks"></ul>
|
|
50
|
+
</section>
|
|
33
51
|
<div class="workspace">
|
|
34
52
|
<aside class="directory">
|
|
53
|
+
<button id="back-overview" type="button">返回验收总览</button>
|
|
35
54
|
<details id="directory-details" open>
|
|
36
55
|
<summary>阅读目录</summary>
|
|
37
56
|
<nav aria-label="页面目录" id="directory"></nav>
|