dsh-crwu-workbench 0.0.38 → 0.0.40

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/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@
7
7
  `cordis_define` + `cordis_run` 装配,版本号用 DSH 的 `pkg-N`);它已在本仓收尾时删除
8
8
  (见 `0.0.1` 一节),下面 `legacy · pkg-43` 及更早的记录是它的历史。
9
9
 
10
+ ## package · 0.0.40 · 2026-10-05 · fix · 普通 DSH 会话支持完整手工审核(协议 26)
11
+
12
+ - 修复复制提示词到新会话后,脚本、知识库、附件下载、OSS 发布、钉钉归档和本人通知因缺少审核子会话身份被统一拒绝的问题。普通会话按选定工作空间下的 `<工作空间>/<流水号>` 校验案例目录,无需登记审核记录或讨论范围;氚云访问仍使用员工凭据与远端权限。
13
+ - 统一案例访问入口:托管审核子会话仍校验本轮案例与附件白名单,已结束、已停止、不完整及待接管状态不得降级为普通会话;已登记讨论会话可以使用全部案例工具,记录和附件清单查询仅允许登记的 ObjectId。
14
+ - `crwu_run_python_script` 通过调用者 Agent 解析 DSH Python,首次手工审核无需先运行一次工作台自动审核来填充缓存。
15
+ - 手工兜底提示词携带选定工作空间的案例路径,先用 `crwu_audit_case_bootstrap` 刷新输入快照,再完成两阶段审核和各交付通道。普通会话遵循自己的 DSH 沙箱策略,本机访问授权与托管审核隔离保持生效。
16
+ - 新增普通会话、托管审核和登记讨论的工具对照回归,覆盖 Windows 路径、写后校验、通知幂等与越界零进程。
17
+
18
+ ## package · 0.0.39 · 2026-10-05 · feat · 审核 Skill 默认发布最终 HTML 与 JSON 到 OSS
19
+
20
+ - 新增 `crwu-audit/references/15-oss-result-publish.md`,把默认 OSS 双文件发布、精确文件名逐项写后验证、受信目标和非阻塞失败收尾纳入 Skill,而非仅依赖插件提示词。
21
+ - 编排顺序为步骤 14 成对校验与渲染最终结果 → 15 OSS 发布 → 16 独立钉钉归档与通知;完整保留两阶段复核与必要隔离补审,不上传会话内容。
22
+ - 插件提示词显式传入 HTML/JSON `files`,不再把结果 JSON 当可选文件。交付认证/授权失败仅停止受影响通道,保留本地审核成果并分别报告,不能冒充完全交付成功。
23
+ - 沿用现有 OSS Tool 的受信配置、流水号对象前缀和同名覆盖行为;不新增年月/时间戳归档,不改变 Host 审核范围与主会话绑定机制。
24
+ - 更新 Skill 路由、CLI 守卫与提示词契约测试,新增双文件 Tool 成功、JSON 缺失与写后大小不符用例。
25
+
10
26
  ## package · 0.0.38 · 2026-09-30 · fix · 发起失败的手工兜底必须先把会话切成「完全权限」(并把怎么切写清楚)
11
27
 
12
28
  用户口径(2026-09-30):「手工兜底那里要加上『打开完全权限』—— 用户可能不知道这个怎么打开,
package/README.en.md CHANGED
@@ -12,6 +12,8 @@ auto-upload deliverables to Aliyun OSS → open the cloud-hosted audit opinion.
12
12
  The audit process itself is **not** in this repository; the `crwu-audit` skill family runs it.
13
13
  This repository only *dispatches, watches, and ships back*.
14
14
 
15
+ The audit Skill's completion contract generates and validates final HTML/JSON after both audit phases, then explicitly uploads and verifies both files under the [OSS publishing contract](skills/crwu/crwu-audit/references/15-oss-result-publish.md). OSS failure preserves local results and is reported separately while independent DingTalk archive and notification continue. Conversation content is not uploaded.
16
+
15
17
  One form only: `src/` is the single source, bundled by tsdown into `lib/index.js` (Host) and
16
18
  `lib/client.js` (Client), distributed as a DSH **package** plugin (npm / tarball / git).
17
19
  Migration history: [`PORTING.md`](PORTING.md).
@@ -110,6 +112,12 @@ Building the tarball yourself (from the `crwu-ai` repository): `make plugin-pack
110
112
  > root and child sessions are always `workspace-write` with `approval=never`, and the account-connection
111
113
  > step also offers a read-only `.dws` checkup with a second-confirmed permission repair.
112
114
 
115
+ ### Manual audit in a new DSH session
116
+
117
+ The fallback prompt carries the selected workspace's case directory and refreshes the record/attachment input snapshot with `crwu_audit_case_bootstrap`. Ordinary sessions can run scripts, materialize knowledge, download attachments, publish to OSS, archive to DingTalk, and notify themselves without registering an audit child or discussion session. Keep the workspace, case path, employee logins, and local-access consent ready; the session's own DSH sandbox policy still applies.
118
+
119
+ Managed audit children remain bound to their current case and attachment manifest, including after stopping. Registered discussions can use case tools and query their own report. After updating, fully quit and restart DSH, then copy the prompt again.
120
+
113
121
  ### Updating CRWU (self-update, from 0.0.12)
114
122
 
115
123
  **Where to look:** the version badge in the sidebar card header and in the panel header (`dev` / `v0.0.12`)
package/README.md CHANGED
@@ -10,6 +10,8 @@ AI 审核子会话 → 盯住它的运行状态、可随时停止/重启 → 交
10
10
 
11
11
  审核流程本体不在本仓:它由 `crwu-audit` 技能族执行(见「依赖」)。本仓只负责**发起、盯状态、交付件回传**。
12
12
 
13
+ 审核 Skill 的收尾契约:完整两阶段审核后生成并校验最终 HTML/JSON,按 [OSS 发布契约](skills/crwu/crwu-audit/references/15-oss-result-publish.md) 显式上传双文件并逐项写后验证。OSS 失败保留本地成果、如实报告,继续独立的钉钉归档与通知;不上传会话内容。
14
+
13
15
  源码在 `src/`,构建产物是 `lib/index.js` + `lib/client.js`,按 **DSH 包插件**(npm)分发安装。
14
16
  维护规范见 [`AGENTS.md`](AGENTS.md),完整发版步骤见
15
17
  [`docs/releasing.md`](https://github.com/mmungdong/crwu-ai/blob/main/plugins/dsh-crwu-workbench/docs/releasing.md),
@@ -173,6 +175,13 @@ node <仓库>/install/browser-check.mjs \
173
175
 
174
176
  `DSH_PERMISSION_MODE=danger-full-access` 只在**开发自测**(§「真实验证」里那个临时 profile)才需要。
175
177
 
178
+ ### 在 DSH 新会话中手工审核
179
+
180
+ 手工兜底提示词携带选定工作空间的案例目录,并先用 `crwu_audit_case_bootstrap` 刷新记录和附件输入快照。普通会话无需登记审核子会话或讨论范围,即可运行脚本、下载知识与附件、发布 OSS、钉钉归档和通知本人。请先准备工作空间、案例路径、员工登录和本机访问授权;执行仍遵循该会话自己的 DSH 沙箱策略。
181
+
182
+ 托管审核子会话继续限定本轮案例和附件清单,停止后也不得转为普通权限。已登记讨论会话可使用案例工具并查询自己的报告。更新插件后完整退出并重启 DSH,再重新复制提示词。
183
+
184
+
176
185
  ### 不需要任何 PATH 配置(2026-09-25 起,且**不要再加回来**)
177
186
 
178
187
  `crwu` / `dws` / `ossutil` 随插件发布在包内 `bin/<平台>/`,插件用**包内绝对路径**经 `ctx.shell`
@@ -428,65 +437,23 @@ CI(`.github/workflows/ci.yml`)在 Ubuntu + Windows × Node 22/24 上跑同
428
437
 
429
438
  ## 五、npm 发布(包形态的正式分发)
430
439
 
431
- 包形态是长期维护的那一半,按 npm 包分发。仓库已配好发布链路,**不要手工 `npm publish`**:
432
-
433
- ```bash
434
- npm run version:set 0.0.2 # 改 package.json + VERSION(并同步 lockfile 根版本)
435
- # 在 CHANGELOG.md 加一节 `## package · 0.0.2 · <日期>`
436
- npm run check # 本地门禁:version:check + typecheck + test + build + smoke:built
437
- npm run pack:assert # 核对真正打进 tarball 的文件清单
438
- git commit -am "release(dsh-crwu-workbench): 0.0.2" && git push
439
- git tag plugin-v0.0.2 && git push origin plugin-v0.0.2
440
- ```
441
-
442
- `plugin-v*` tag 会触发仓根的 [`.github/workflows/release.yml`](../../.github/workflows/release.yml)
443
- (`v*` 留给仓里的 Go CLI,两条发布线分开):先断言
444
- **tag 与 `package.json` / `VERSION` 一致**,再跑完整门禁与产物自检,最后发布。
445
-
446
- **当前发布方式(0.0.12 及以后):只走 tag → GitHub Actions → CI 发布,禁止在本机手工 `npm publish`。**
447
- 推 `plugin-v0.0.12` 这类 tag 时,`release.yml` 会校验 tag 与 `package.json` / `VERSION` 一致,
448
- 再由 CI 用仓库 Secret `NPM_TOKEN` 执行 `npm publish --provenance`(provenance 来自 GitHub Actions OIDC);
449
- **Trusted Publishing 尚未启用**,它只是 `docs/releasing.md` §3.3 记录的未来迁移方案。
450
- 本机只允许 `npm publish --dry-run`(dry-run 不是发布)。
451
-
452
- 下面这段是 **0.0.10 初次建包时的历史记录**,只用于解释 provenance 的本机限制,
453
- **不得**照它去发 0.0.12 或任何后续版本(那时包还不存在,才必须在本机建包):
454
-
455
- **首次发布(引导)必须在本机做,而且不能带 `--provenance`**(2026-09-28 实测):
456
- `--provenance` 只在受支持的 CI(GitHub Actions 的 OIDC)里成立,本机 provider 是 `null`,
457
- npm 会直接以 `EUSAGE: Automatic provenance generation not supported for provider: null` 拒绝发布。
458
- 所以 `publishConfig` 里**不要**写 `provenance: true`(有测试钉住这一点),本机首发用:
440
+ 正式发布优先走 `plugin-v<version>` tag 触发的 GitHub Actions。版本查看、修改、登录检查、空跑与
441
+ 应急手动发布都由仓库根目录 Makefile 提供统一入口:
459
442
 
460
443
  ```bash
461
- npm login --registry=https://registry.npmjs.org
462
- npm run build && npm run check && npm run pack:assert:strict
463
- npm publish # 不带 --provenance
444
+ make plugin-version
445
+ make plugin-version-set PLUGIN_RELEASE_VERSION=<version>
446
+ make plugin-pack
447
+ make plugin-publish-dry-run
464
448
  ```
465
449
 
466
- **当前真实工作流(0.0.12 仍在用)**:`.github/workflows/release.yml` 读 GitHub Secret
467
- `NPM_TOKEN`(`NODE_AUTH_TOKEN`),在 tag `plugin-v<版本>` 上执行 `npm publish --provenance` ——
468
- provenance(构建来源证明)由 GitHub Actions 的 OIDC 在 CI 里产生,**不是**本机发布。
469
- **Trusted Publishing 目前没有启用**:它只是 `docs/releasing.md` §3.3 记录的**未来可迁移方案**,
470
- 在真的改完工作流之前,不要把"已采用无 token 发布"当成当前事实。
471
-
472
- 未来若迁移到 Trusted Publisher(npm 网页给这个包配 repo `mmungdong/crwu-ai`
473
- + workflow `release.yml`);之后的版本由 CI 用 **OIDC** 发布 —— 不需要任何长期 token,
474
- npm 会**自动**附带 provenance attestation,`NPM_TOKEN` secret 也可以删掉。
475
- 也可以在 Actions 里用 `workflow_dispatch` 跑一次 dry-run:只打包与校验,不发。
476
-
477
- **`prepublishOnly` 会挡住不该发的包**:先 `pack:assert`(缺入口、误打 `tests/`/`install/`
478
- 一律失败),再 `check`。所以哪怕有人绕过 tag 手工发布,也过不了这两道。
450
+ `make plugin-version` 会显示本地版本、npm `latest` 和建议的下一个 patch 版本。`plugin-pack` 会运行完整
451
+ 门禁并核对真实 tarball;`plugin-publish-dry-run` 会先检查干净工作树、npm 登录和版本递增,再构建并空跑。
452
+ 插件名默认由 Makefile 顶部的 `PLUGIN` 指定,也可在命令行覆盖。
479
453
 
480
- **发布目标钉在官方 registry**:`publishConfig.registry = https://registry.npmjs.org`,与 CI 里
481
- `setup-node` 的 `registry-url` 一致(`tests/unit/host-package.test.mjs` 会核对两者相同)。
482
- 不钉的话,本机 `~/.npmrc` 若指向镜像(国内开发机常见),手工 `npm publish` 会往镜像上发,
483
- 而 `--provenance` 在镜像上根本不成立 —— 一条命令同时踩两个坑。装依赖仍然走你的镜像,不受影响。
484
-
485
- 发之前可以先空跑一次,它会真的跑完 `prepublishOnly` 并打印将要发布的清单(不会上传):
486
-
487
- ```bash
488
- npm publish --dry-run # 期望看到:Publishing to https://registry.npmjs.org …(dry-run)
489
- ```
454
+ 只有 tag 流程不可用且维护者明确选择应急发布时,才运行带精确确认值的
455
+ `make plugin-publish CONFIRM_PUBLISH=<package>@<version>`。完整步骤、门禁和失败恢复见
456
+ [`docs/releasing.md`](docs/releasing.md)。
490
457
 
491
458
  ### npm 分发路径也实测过
492
459
 
Binary file
package/bin/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": "crwu.plugin-bin-manifest.v1",
3
- "generatedAt": "2026-09-29T23:54:02.539Z",
3
+ "generatedAt": "2026-10-05T07:55:04.905Z",
4
4
  "platforms": [
5
5
  {
6
6
  "platform": "darwin-arm64",
@@ -12,9 +12,9 @@
12
12
  "source": "repo-build",
13
13
  "sourceVersion": "0.0.1",
14
14
  "target": "darwin/arm64",
15
- "buildCommit": "19cc856be361d1c7eb9284e5c561ccb1f895a189",
15
+ "buildCommit": "4a2cd69f23a20dd3424d58b1f8a35d13d2f3419e",
16
16
  "size": 7351330,
17
- "sha256": "af3da82f605a146333100e7981cdcb45e847691b4abaf5682f4540240e24ea7f"
17
+ "sha256": "baecdedd7294c707c4be85679e3257f464c304d7d06e3214b710715d17f06d59"
18
18
  },
19
19
  {
20
20
  "tool": "ossutil",
@@ -50,9 +50,9 @@
50
50
  "source": "repo-build",
51
51
  "sourceVersion": "0.0.1",
52
52
  "target": "windows/amd64",
53
- "buildCommit": "19cc856be361d1c7eb9284e5c561ccb1f895a189",
53
+ "buildCommit": "4a2cd69f23a20dd3424d58b1f8a35d13d2f3419e",
54
54
  "size": 8128000,
55
- "sha256": "690bc62316cafd575eb65a1092bea339c3674cd4f1a060b68687b80078b5f476"
55
+ "sha256": "d62601e00513b83a72dbccdec1b6ec18d8849b26d8e9e1325acac285e7bec6a8"
56
56
  },
57
57
  {
58
58
  "tool": "ossutil",
Binary file
package/lib/client.js CHANGED
@@ -7500,19 +7500,66 @@ body[data-ds-dark-theme] .crwu-audit-root {
7500
7500
  };
7501
7501
  }
7502
7502
  //#endregion
7503
+ //#region src/shared/utils/case-dir.ts
7504
+ /**
7505
+ * 一条报告的**案例目录**:`<工作空间>/<流水号>`(Windows 上是 `<工作空间>\<流水号>`)。
7506
+ *
7507
+ * 这是 Host 与 Client 都依赖的唯一约定,所以只在这里定义一次:
7508
+ * - Host 的审核提示词拿它告诉审核子代理「本案例目录必须是 …」(`host/audit/prompt.ts`);
7509
+ * - Client 的讨论 / 分析提示词拿它告诉模型「下载与读取只允许在这个目录里」。
7510
+ *
7511
+ * **为什么必须共用一份**:2026-09-25 实测到漂移的代价 —— 审核链路一直用
7512
+ * `<工作空间>/<流水号>`,而讨论会话的模型没人告诉它目录,于是它自己 `ls` + `find` 去猜,
7513
+ * 猜成了 `<工作空间>/cases/<流水号>`:既扫了用户的磁盘,又把材料下到了错的地方。
7514
+ * 两处各写一份拼接逻辑就是这种漂移的温床,所以宁可多一个共享模块。
7515
+ *
7516
+ * **拼接交给 `joinLocalPath`**(2026-09-28 Windows 适配):分隔符随工作空间的风格走 ——
7517
+ * 在 Windows 上硬拼 `/` 会得到 `C:\Work/S1` 这种混用形式,而这条路径既进提示词
7518
+ * (模型照着写 PowerShell 命令)又进 `ctx.shell` 的 `workdir`。
7519
+ *
7520
+ * 任一侧为空、或流水号不是安全的一段(绝对路径 / 含 `..`)时返回空串,
7521
+ * 调用方据此**不写这一段**(宁可不给,也不给一个半截或越界的路径)。
7522
+ */
7523
+ function caseDirOf(workspacePath, seqNo) {
7524
+ const workspace = text(workspacePath);
7525
+ const seq = text(seqNo).trim();
7526
+ if (workspace === "" || seq === "") return "";
7527
+ try {
7528
+ return joinLocalPath(workspace, seq);
7529
+ } catch (error) {
7530
+ return "";
7531
+ }
7532
+ }
7533
+ //#endregion
7503
7534
  //#region src/client/features/workbench/Handoff.tsx
7504
7535
  /** 手工提示词全文。字段缺失时给出可操作的提示,而不是留空。 */
7505
- function handoffPrompt(task) {
7536
+ function handoffPrompt(task, workspacePath = "") {
7537
+ const caseDir = caseDirOf(workspacePath, task.seqNo);
7506
7538
  return [
7507
7539
  zhCN.handoffIntro,
7508
7540
  "",
7509
7541
  `- ${zhCN.handoffObjectId}${task.id === "" ? zhCN.handoffObjectIdMissing : task.id}`,
7510
7542
  `- ${zhCN.handoffSeqNo}${task.seqNo === "" ? task.name : task.seqNo}`,
7511
- `- ${zhCN.handoffProject}${task.project === "" ? zhCN.handoffProjectMissing : task.project}`
7543
+ `- ${zhCN.handoffProject}${task.project === "" ? zhCN.handoffProjectMissing : task.project}`,
7544
+ "",
7545
+ "这是普通会话中的手工审核,无需登记为审核子会话。先准备本轮输入快照,再执行技能的完整两阶段审核。",
7546
+ ...caseDir === "" ? ["请先在工作台选定工作空间并确认报告流水号,然后重新复制提示词;不要猜案例目录。"] : [
7547
+ `本次唯一案例目录:${caseDir}`,
7548
+ "若案例目录不存在,只在上述工作空间下创建这个目录;所有材料、脚本与交付件均放在其中,不复用旧审核产物。",
7549
+ ...task.id === "" ? [] : [`先调用 crwu_audit_case_bootstrap(${JSON.stringify({
7550
+ objectId: task.id,
7551
+ seqNo: task.seqNo,
7552
+ caseDir,
7553
+ refresh: true
7554
+ })})。`, "成功后只读取它返回的 snapshotPath、attachmentsPath、metadataPath;失败则停止并报告结构化错误。"],
7555
+ "附件按清单通过 crwu_h3yun_file_get 逐件下载,知识清单用 crwu_audit_knowledge_materialize 实时下载。",
7556
+ "技能脚本用 crwu_run_python_script;运行时仅由 load_workspace_dependencies 解析,不查找或改用系统 Python。",
7557
+ "最终 HTML/JSON 校验通过后,分别调用 crwu_audit_oss_publish、crwu_audit_dingtalk_archive、crwu_audit_dingtalk_notify_self,独立报告各通道交付结果。"
7558
+ ]
7512
7559
  ].join("\n");
7513
7560
  }
7514
7561
  function Handoff(props) {
7515
- const prompt = handoffPrompt(props.task);
7562
+ const prompt = handoffPrompt(props.task, props.workspacePath);
7516
7563
  const copy = () => {
7517
7564
  const fallback = () => {
7518
7565
  workbenchApi.clipboard({ text: prompt });
@@ -8661,37 +8708,6 @@ body[data-ds-dark-theme] .crwu-audit-root {
8661
8708
  };
8662
8709
  }
8663
8710
  //#endregion
8664
- //#region src/shared/utils/case-dir.ts
8665
- /**
8666
- * 一条报告的**案例目录**:`<工作空间>/<流水号>`(Windows 上是 `<工作空间>\<流水号>`)。
8667
- *
8668
- * 这是 Host 与 Client 都依赖的唯一约定,所以只在这里定义一次:
8669
- * - Host 的审核提示词拿它告诉审核子代理「本案例目录必须是 …」(`host/audit/prompt.ts`);
8670
- * - Client 的讨论 / 分析提示词拿它告诉模型「下载与读取只允许在这个目录里」。
8671
- *
8672
- * **为什么必须共用一份**:2026-09-25 实测到漂移的代价 —— 审核链路一直用
8673
- * `<工作空间>/<流水号>`,而讨论会话的模型没人告诉它目录,于是它自己 `ls` + `find` 去猜,
8674
- * 猜成了 `<工作空间>/cases/<流水号>`:既扫了用户的磁盘,又把材料下到了错的地方。
8675
- * 两处各写一份拼接逻辑就是这种漂移的温床,所以宁可多一个共享模块。
8676
- *
8677
- * **拼接交给 `joinLocalPath`**(2026-09-28 Windows 适配):分隔符随工作空间的风格走 ——
8678
- * 在 Windows 上硬拼 `/` 会得到 `C:\Work/S1` 这种混用形式,而这条路径既进提示词
8679
- * (模型照着写 PowerShell 命令)又进 `ctx.shell` 的 `workdir`。
8680
- *
8681
- * 任一侧为空、或流水号不是安全的一段(绝对路径 / 含 `..`)时返回空串,
8682
- * 调用方据此**不写这一段**(宁可不给,也不给一个半截或越界的路径)。
8683
- */
8684
- function caseDirOf(workspacePath, seqNo) {
8685
- const workspace = text(workspacePath);
8686
- const seq = text(seqNo).trim();
8687
- if (workspace === "" || seq === "") return "";
8688
- try {
8689
- return joinLocalPath(workspace, seq);
8690
- } catch (error) {
8691
- return "";
8692
- }
8693
- }
8694
- //#endregion
8695
8711
  //#region src/client/features/report-audit/ReportPane.tsx
8696
8712
  /**
8697
8713
  * 复用的搜索组件(报告列表与 AI 审核列表是**同一个**)。
@@ -9791,6 +9807,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
9791
9807
  }),
9792
9808
  state.handoff === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Handoff, {
9793
9809
  task: state.handoff,
9810
+ workspacePath: props.workspace?.path,
9794
9811
  copied: props.handoffCopied,
9795
9812
  onCopied: props.onHandoffCopied
9796
9813
  }),
@@ -10742,7 +10759,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
10742
10759
  }
10743
10760
  /** 宿主与客户端是不是同一代(不同代时**必须**拦住发起审核,见 AGENTS.md §7.12)。 */
10744
10761
  function hostIsStale(snapshot) {
10745
- return snapshot.protocol !== null && snapshot.protocol !== 25;
10762
+ return snapshot.protocol !== null && snapshot.protocol !== 26;
10746
10763
  }
10747
10764
  /**
10748
10765
  * 宿主执行的权限说明版本与客户端是否一致(协议 18)。
package/lib/index.js CHANGED
@@ -231,7 +231,7 @@ const PLUGIN_INJECT = [
231
231
  * 还会让打包器的 JSON 插件成为隐式依赖。代价是升版本时要同时改这里 ——
232
232
  * `tests/unit/host-package.test.mjs` 有一条断言盯着它必须等于 `package.json` 的 version。
233
233
  */
234
- const PLUGIN_VERSION = "0.0.38";
234
+ const PLUGIN_VERSION = "0.0.40";
235
235
  /**
236
236
  * `ping` / `boot` 应答里的版本指纹,形如 `pkg-0.0.5`。
237
237
  *
@@ -2451,6 +2451,7 @@ function toolSection() {
2451
2451
  "随包 vendored 的 DWS 技能(`dingtalk-*`)**不参与**本次自动审核编排:自动审核的取数与交付只走上表 Tool。",
2452
2452
  "",
2453
2453
  "**登录与授权(必须照做)**:任何 Tool 返回「需要先允许工作台访问本机账号和配置」(`not-authorized`)或「未登录 / 会话过期」时:**立即停止本次审核**,在汇报里写明「需要员工回到工作台完成账号连接(氚云 / 钉钉)或允许本机访问」,并把已经完成的步骤列清楚。",
2454
+ "上述审核停止规则用于取数与审核阶段;步骤 14 已成功生成并校验本地 HTML/JSON 后,交付通道的认证/授权失败只停止该通道,保留本地成果并继续其他独立交付,分别报告失败。",
2454
2455
  "",
2455
2456
  "**绝对不许**:自己执行登录(`login` 类命令 / 打开浏览器扫码 / 让用户扫码)、去系统钥匙串或用户目录里翻找凭据、改 `PATH` 或去找别的命令、把「登录失败」当成本次审核的结论。你的审批策略是 `never`:任何需要审批的动作都只会被确定性拒绝。",
2456
2457
  ""
@@ -2621,14 +2622,14 @@ function legacyAuditPrompt(task) {
2621
2622
  L.push("调用一次:");
2622
2623
  L.push("");
2623
2624
  L.push("```text");
2624
- L.push("crwu_audit_oss_publish({ caseDir: \"" + caseDir + "\", seqNo: \"" + seq + "\" })");
2625
+ L.push("crwu_audit_oss_publish({ caseDir: \"" + caseDir + "\", seqNo: \"" + seq + "\", files: [\"审核意见." + seq + ".html\", \"审核结果." + seq + ".json\"] })");
2625
2626
  L.push("```");
2626
2627
  L.push("");
2627
2628
  L.push("要求:");
2628
- L.push("1. **HTML 必须上传**(`审核意见." + seq + ".html`)。结果 JSON(`审核结果." + seq + ".json`)存在就一并上传;不存在时 Tool 会跳过它,你需要在汇报里说明「结果 JSON 未生成」。");
2629
+ L.push("1. 先读取 crwu-audit 的 `references/15-oss-result-publish.md`:完整两阶段审核及必要补审后,步骤 14 必须成对生成并校验最终 HTML/JSON;显式上传上面两个文件,不省略 `files`,不上传初审、中间产物或会话。成对生成或校验失败则停止本地交付,不调用上传。");
2629
2630
  L.push("2. bucket / endpoint / 对象前缀由 Tool 从部署配置读取,**不要提交、也不要自己拼 `oss://` 地址**;凭据已经配在本机,**不要问我要 AccessKey,不要回显任何密钥**。");
2630
- L.push("3. Tool 会在上传后**真的列举一次**核对目标对象与字节数;你必须在汇报里写出 Tool 返回的 `key`、`sizeBytes` 与 `ok`。");
2631
- L.push("4. 上传失败**不要静默略过**,把 Tool 返回的 `error` 原文(已脱敏)贴出来。");
2631
+ L.push("3. Tool 会在上传后**真的列举一次**核对目标对象与字节数;只有整体 `ok:true`、`results` 按精确 `name` 唯一命中两个预期文件且各项 `ok:true`、`key` 非空、`sizeBytes` 非零并无失败/缺项,才报告 OSS 双文件上传成功。分别汇报 `name`、`key`、`sizeBytes`、`ok`,不能只看 `uploaded` 或 HTML 成功。");
2632
+ L.push("4. 上传失败/部分成功**不要静默略过**,保留本地成果,报告 Tool 返回的 `errorKind`、`error`(已脱敏)与失败/缺项;继续独立的钉钉归档与通知,不改写审核结论、不循环重试、不绕过权限。明确区分本地审核完成与 OSS 交付状态,不冒充完全交付成功。");
2632
2633
  }
2633
2634
  if (seq !== "" && caseDir !== "") {
2634
2635
  L.push("");
@@ -3170,10 +3171,11 @@ function samePathText(a, b) {
3170
3171
  */
3171
3172
  function isAuditChild(state, childId, parentSessionId = "") {
3172
3173
  if (childId === "") return false;
3174
+ if (parentSessionId !== "" && parentSessionId === state.auditRoot?.sessionId) return true;
3173
3175
  for (const record of Object.values(state.audits ?? {})) {
3174
3176
  if (record === void 0) continue;
3175
- if (record.childId === childId) return true;
3176
- if (record.pending === true && record.childId === "" && parentSessionId !== "" && record.parentSessionId === parentSessionId) return true;
3177
+ if (record.childId === childId || record.replacedChildId === childId) return true;
3178
+ if (parentSessionId !== "" && record.parentSessionId === parentSessionId) return true;
3177
3179
  }
3178
3180
  return false;
3179
3181
  }
@@ -3188,7 +3190,7 @@ function isAuditChild(state, childId, parentSessionId = "") {
3188
3190
  * 退路问的是会话存储本身:**审核根的孩子里有没有这个调用者**。
3189
3191
  * 两条都拿不到才算"问不到",由门禁 fail closed(不猜)。
3190
3192
  *
3191
- * 只对**待接管**的候选问(正常最多一条):多条 pending 是状态异常,直接不认。
3193
+ * Query known audit roots so replaced children retain their managed identity.
3192
3194
  */
3193
3195
  async function callerParentSessionId(ctx, state, exec) {
3194
3196
  const { childId, parentSessionId } = callerIdentity(exec);
@@ -3196,13 +3198,10 @@ async function callerParentSessionId(ctx, state, exec) {
3196
3198
  if (parentSessionId !== "") return parentSessionId;
3197
3199
  const subagents = ctx.get("subagents");
3198
3200
  if (subagents === void 0 || typeof subagents.listChildren !== "function") return "";
3199
- const candidates = Object.values(state.audits ?? {}).filter((record) => record !== void 0 && record.pending === true && record.childId === "" && record.parentSessionId !== "");
3200
- if (candidates.length !== 1) return "";
3201
- const parentId = candidates[0]?.parentSessionId ?? "";
3202
- if (parentId === "") return "";
3203
- try {
3201
+ const candidates = new Set([text(state.auditRoot?.sessionId), ...Object.values(state.audits ?? {}).map((record) => text(record?.parentSessionId))].filter((id) => id !== ""));
3202
+ for (const parentId of candidates) try {
3204
3203
  const children = await subagents.listChildren(parentId);
3205
- if (!Array.isArray(children)) return "";
3204
+ if (!Array.isArray(children)) continue;
3206
3205
  for (const child of children) {
3207
3206
  if (child === null || typeof child !== "object") continue;
3208
3207
  const record = child;
@@ -5365,6 +5364,182 @@ function nodeSize(item) {
5365
5364
  }
5366
5365
  return null;
5367
5366
  }
5367
+ function createDiscussionScopeRegistry(options = {}) {
5368
+ const now = options.now ?? (() => Date.now());
5369
+ const ttlMs = options.ttlMs ?? 432e5;
5370
+ const limit = options.limit ?? 32;
5371
+ const scopes = /* @__PURE__ */ new Map();
5372
+ /** 过期的一律先清掉:判据只有 `expiresAt` 与当前时刻。 */
5373
+ const sweep = () => {
5374
+ const at = now();
5375
+ for (const [id, scope] of scopes) if (scope.expiresAt <= at) scopes.delete(id);
5376
+ };
5377
+ /** 超出上限时丢**最久没用过**的(不是最早建的:人可能一直在用一条老会话)。 */
5378
+ const trim = () => {
5379
+ while (scopes.size > limit) {
5380
+ let oldestId = "";
5381
+ let oldestAt = Number.POSITIVE_INFINITY;
5382
+ for (const [id, scope] of scopes) if (scope.lastUsedAt < oldestAt) {
5383
+ oldestAt = scope.lastUsedAt;
5384
+ oldestId = id;
5385
+ }
5386
+ if (oldestId === "") return;
5387
+ scopes.delete(oldestId);
5388
+ }
5389
+ };
5390
+ return {
5391
+ register(input) {
5392
+ const at = now();
5393
+ const scope = {
5394
+ sessionId: text(input.sessionId),
5395
+ seqNo: text(input.seqNo),
5396
+ objectId: text(input.objectId),
5397
+ caseDir: text(input.caseDir),
5398
+ allowedAttachmentIds: [...input.allowedAttachmentIds].map((id) => text(id)).filter((id) => id !== ""),
5399
+ createdAt: at,
5400
+ lastUsedAt: at,
5401
+ expiresAt: at + ttlMs
5402
+ };
5403
+ scopes.set(scope.sessionId, scope);
5404
+ trim();
5405
+ return scope;
5406
+ },
5407
+ use(sessionId) {
5408
+ sweep();
5409
+ const id = text(sessionId);
5410
+ if (id === "") return void 0;
5411
+ const scope = scopes.get(id);
5412
+ if (scope === void 0) return void 0;
5413
+ const touched = {
5414
+ ...scope,
5415
+ lastUsedAt: now()
5416
+ };
5417
+ scopes.set(id, touched);
5418
+ return touched;
5419
+ },
5420
+ peek(sessionId) {
5421
+ sweep();
5422
+ return scopes.get(text(sessionId));
5423
+ },
5424
+ drop(sessionId) {
5425
+ scopes.delete(text(sessionId));
5426
+ },
5427
+ list() {
5428
+ sweep();
5429
+ return [...scopes.values()];
5430
+ },
5431
+ clear() {
5432
+ scopes.clear();
5433
+ },
5434
+ size() {
5435
+ sweep();
5436
+ return scopes.size;
5437
+ }
5438
+ };
5439
+ }
5440
+ /**
5441
+ * 案例内 Tool 的**统一门禁**:认证身份 → 解析范围 → 校验模型提交的标识。
5442
+ *
5443
+ * 与 `requireAuditScope` 的关系:后者是"审核子会话"这一支的实现,这里在它之前再加一支
5444
+ * "已登记的讨论会话"。**讨论会话绝不复用审核 scope**(见文件头第 2 条)。
5445
+ */
5446
+ async function requireMaterialScope(ctx, state, scopes, exec, args, options = {}) {
5447
+ const { childId } = callerIdentity(exec);
5448
+ if (childId === "") return { ...failure$1("policy", "无法确认调用者身份:案例内的操作只允许由本次审核的子会话或已登记的报告讨论会话发起") };
5449
+ if (auditScopeFor(state, childId) !== void 0) {
5450
+ const checked = await requireAuditScope(ctx, state, exec, args, options);
5451
+ if (!checked.ok) return checked;
5452
+ return {
5453
+ ok: true,
5454
+ material: {
5455
+ kind: "audit",
5456
+ scope: checked.scope
5457
+ }
5458
+ };
5459
+ }
5460
+ const scope = scopes.use(childId);
5461
+ if (scope === void 0) return { ...failure$1("policy", "调用者既不是进行中的审核子会话,也不是已登记的报告讨论会话:案例内的操作一律拒绝(报告讨论请在工作台点「与 DeepSeek 讨论报告」重新登记材料范围)") };
5462
+ const seqNo = text(args.seqNo).trim();
5463
+ if (seqNo !== "" && seqNo !== scope.seqNo) return { ...failure$1("input", "流水号与本次登记的材料范围不一致:拒绝操作") };
5464
+ const objectId = text(args.objectId).trim();
5465
+ if (objectId !== "" && objectId !== scope.objectId) return { ...failure$1("input", "记录标识与本次登记的材料范围不一致:拒绝操作") };
5466
+ const caseDir = text(args.caseDir).trim();
5467
+ if (caseDir === "") {
5468
+ if (options.requireCaseDir === false) return {
5469
+ ok: true,
5470
+ material: {
5471
+ kind: "discussion",
5472
+ scope
5473
+ }
5474
+ };
5475
+ return { ...failure$1("input", "缺少案例目录:案例内的操作必须带上本轮案例目录") };
5476
+ }
5477
+ if (fileSystem(ctx) === void 0) return { ...failure$1("infrastructure", "Host 文件服务不可用") };
5478
+ try {
5479
+ const given = await resolveTarget(ctx, caseDir);
5480
+ const expected = await resolveTarget(ctx, scope.caseDir);
5481
+ if (text(given.targetKey) === "" || given.targetKey !== expected.targetKey) return { ...failure$1("policy", "案例目录必须是本次登记的那一个(不许用工作空间根、兄弟案例或子目录)") };
5482
+ } catch (error) {
5483
+ return { ...failure$1("infrastructure", `案例目录解析失败:${error instanceof Error ? error.message : String(error)}`) };
5484
+ }
5485
+ return {
5486
+ ok: true,
5487
+ material: {
5488
+ kind: "discussion",
5489
+ scope
5490
+ }
5491
+ };
5492
+ }
5493
+ //#endregion
5494
+ //#region src/host/audit/case-access.ts
5495
+ async function requireCaseAccess(ctx, state, discussions, exec, args) {
5496
+ const { childId } = callerIdentity(exec);
5497
+ if (childId === "") return failure$1("policy", "无法确认调用者身份:案例操作需要会话上下文");
5498
+ const parentId = await callerParentSessionId(ctx, state, exec);
5499
+ if (isAuditChild(state, childId, parentId)) {
5500
+ const checked = await requireAuditScope(ctx, state, exec, args);
5501
+ if (!checked.ok) return checked;
5502
+ return {
5503
+ ok: true,
5504
+ kind: "audit",
5505
+ casePath: checked.casePath,
5506
+ allowedAttachmentIds: checked.scope.allowedAttachmentIds
5507
+ };
5508
+ }
5509
+ const header = exec.agent?.session?.header;
5510
+ if (Object.values(state.audits ?? {}).some((record) => record?.pending === true && record.childId === "") && parentId === "" && header === void 0) return failure$1("policy", "审核子会话正在建立,尚无法确认调用者的会话范围:请稍后重试");
5511
+ if (discussions.peek(childId) !== void 0) {
5512
+ const checked = await requireMaterialScope(ctx, state, discussions, exec, args);
5513
+ if (!checked.ok) return checked;
5514
+ const scope = checked.material.scope;
5515
+ return {
5516
+ ok: true,
5517
+ kind: checked.material.kind,
5518
+ casePath: checked.material.kind === "audit" ? checked.material.scope.casePath : checked.material.scope.caseDir,
5519
+ allowedAttachmentIds: scope.allowedAttachmentIds
5520
+ };
5521
+ }
5522
+ const root = allowedCaseRootOf(state);
5523
+ const checked = await requireCaseDir(ctx, args.caseDir, { allowedRoot: root });
5524
+ if (!checked.ok) return checked;
5525
+ const seqNo = text(args.seqNo).trim() || basenameLocalPath(checked.path);
5526
+ if (seqNo === "." || seqNo === ".." || /[\\/]/.test(seqNo)) return failure$1("input", "流水号必须是单段案例目录名,不能包含路径分隔符");
5527
+ const expectedPath = caseDirOf(root, seqNo);
5528
+ if (expectedPath === "") return failure$1("input", "流水号必须是有效的案例目录名");
5529
+ try {
5530
+ const given = await resolveTarget(ctx, checked.path);
5531
+ const expected = await resolveTarget(ctx, expectedPath);
5532
+ const workspace = await resolveTarget(ctx, root);
5533
+ if (text(given.targetKey) === "" || given.targetKey === workspace.targetKey || given.targetKey !== expected.targetKey) return failure$1("policy", "案例目录必须是当前工作空间下的 `<工作空间>/<流水号>`,不能使用工作空间根或嵌套目录");
5534
+ } catch (error) {
5535
+ return failure$1("infrastructure", `案例目录解析失败:${error instanceof Error ? error.message : String(error)}`);
5536
+ }
5537
+ return {
5538
+ ok: true,
5539
+ kind: "session",
5540
+ casePath: checked.path
5541
+ };
5542
+ }
5368
5543
  //#endregion
5369
5544
  //#region src/host/dws/run.ts
5370
5545
  /** 允许出现在产品域之前的**全局**取值 flag;值必须紧随其后。 */
@@ -5766,7 +5941,7 @@ function dingtalkTools(deps) {
5766
5941
  steps
5767
5942
  };
5768
5943
  const ctx = toolContext(deps.ctx, exec);
5769
- const caseCheck = await requireAuditScope(ctx, deps.state, exec, {
5944
+ const caseCheck = await requireCaseAccess(ctx, deps.state, deps.discussionScopes, exec, {
5770
5945
  caseDir: args.caseDir,
5771
5946
  seqNo: args.seqNo
5772
5947
  });
@@ -6152,7 +6327,7 @@ function dingtalkTools(deps) {
6152
6327
  openDingId: "",
6153
6328
  steps
6154
6329
  };
6155
- const caseCheck = await requireAuditScope(ctx, deps.state, exec, {
6330
+ const caseCheck = await requireCaseAccess(ctx, deps.state, deps.discussionScopes, exec, {
6156
6331
  caseDir: args.caseDir,
6157
6332
  seqNo: args.seqNo
6158
6333
  });
@@ -6480,136 +6655,6 @@ function locateMessage(payload, fileName) {
6480
6655
  async function persistNotifyState(ctx, path, state, options) {
6481
6656
  return (await writeCaseText(ctx, path, `${JSON.stringify(state, null, 2)}\n`, options)).ok;
6482
6657
  }
6483
- function createDiscussionScopeRegistry(options = {}) {
6484
- const now = options.now ?? (() => Date.now());
6485
- const ttlMs = options.ttlMs ?? 432e5;
6486
- const limit = options.limit ?? 32;
6487
- const scopes = /* @__PURE__ */ new Map();
6488
- /** 过期的一律先清掉:判据只有 `expiresAt` 与当前时刻。 */
6489
- const sweep = () => {
6490
- const at = now();
6491
- for (const [id, scope] of scopes) if (scope.expiresAt <= at) scopes.delete(id);
6492
- };
6493
- /** 超出上限时丢**最久没用过**的(不是最早建的:人可能一直在用一条老会话)。 */
6494
- const trim = () => {
6495
- while (scopes.size > limit) {
6496
- let oldestId = "";
6497
- let oldestAt = Number.POSITIVE_INFINITY;
6498
- for (const [id, scope] of scopes) if (scope.lastUsedAt < oldestAt) {
6499
- oldestAt = scope.lastUsedAt;
6500
- oldestId = id;
6501
- }
6502
- if (oldestId === "") return;
6503
- scopes.delete(oldestId);
6504
- }
6505
- };
6506
- return {
6507
- register(input) {
6508
- const at = now();
6509
- const scope = {
6510
- sessionId: text(input.sessionId),
6511
- seqNo: text(input.seqNo),
6512
- objectId: text(input.objectId),
6513
- caseDir: text(input.caseDir),
6514
- allowedAttachmentIds: [...input.allowedAttachmentIds].map((id) => text(id)).filter((id) => id !== ""),
6515
- createdAt: at,
6516
- lastUsedAt: at,
6517
- expiresAt: at + ttlMs
6518
- };
6519
- scopes.set(scope.sessionId, scope);
6520
- trim();
6521
- return scope;
6522
- },
6523
- use(sessionId) {
6524
- sweep();
6525
- const id = text(sessionId);
6526
- if (id === "") return void 0;
6527
- const scope = scopes.get(id);
6528
- if (scope === void 0) return void 0;
6529
- const touched = {
6530
- ...scope,
6531
- lastUsedAt: now()
6532
- };
6533
- scopes.set(id, touched);
6534
- return touched;
6535
- },
6536
- peek(sessionId) {
6537
- sweep();
6538
- return scopes.get(text(sessionId));
6539
- },
6540
- drop(sessionId) {
6541
- scopes.delete(text(sessionId));
6542
- },
6543
- list() {
6544
- sweep();
6545
- return [...scopes.values()];
6546
- },
6547
- clear() {
6548
- scopes.clear();
6549
- },
6550
- size() {
6551
- sweep();
6552
- return scopes.size;
6553
- }
6554
- };
6555
- }
6556
- /**
6557
- * 案例内 Tool 的**统一门禁**:认证身份 → 解析范围 → 校验模型提交的标识。
6558
- *
6559
- * 与 `requireAuditScope` 的关系:后者是"审核子会话"这一支的实现,这里在它之前再加一支
6560
- * "已登记的讨论会话"。**讨论会话绝不复用审核 scope**(见文件头第 2 条)。
6561
- */
6562
- async function requireMaterialScope(ctx, state, scopes, exec, args, options = {}) {
6563
- const { childId } = callerIdentity(exec);
6564
- if (childId === "") return { ...failure$1("policy", "无法确认调用者身份:案例内的操作只允许由本次审核的子会话或已登记的报告讨论会话发起") };
6565
- if (auditScopeFor(state, childId) !== void 0) {
6566
- const checked = await requireAuditScope(ctx, state, exec, args, options);
6567
- if (!checked.ok) return checked;
6568
- return {
6569
- ok: true,
6570
- material: {
6571
- kind: "audit",
6572
- scope: checked.scope
6573
- }
6574
- };
6575
- }
6576
- const scope = scopes.use(childId);
6577
- if (scope === void 0) return { ...failure$1("policy", "调用者既不是进行中的审核子会话,也不是已登记的报告讨论会话:案例内的操作一律拒绝(报告讨论请在工作台点「与 DeepSeek 讨论报告」重新登记材料范围)") };
6578
- const seqNo = text(args.seqNo).trim();
6579
- if (seqNo !== "" && seqNo !== scope.seqNo) return { ...failure$1("input", "流水号与本次登记的材料范围不一致:拒绝操作") };
6580
- const objectId = text(args.objectId).trim();
6581
- if (objectId !== "" && objectId !== scope.objectId) return { ...failure$1("input", "记录标识与本次登记的材料范围不一致:拒绝操作") };
6582
- const caseDir = text(args.caseDir).trim();
6583
- if (caseDir === "") {
6584
- if (options.requireCaseDir === false) return {
6585
- ok: true,
6586
- material: {
6587
- kind: "discussion",
6588
- scope
6589
- }
6590
- };
6591
- return { ...failure$1("input", "缺少案例目录:案例内的操作必须带上本轮案例目录") };
6592
- }
6593
- if (fileSystem(ctx) === void 0) return { ...failure$1("infrastructure", "Host 文件服务不可用") };
6594
- try {
6595
- const given = await resolveTarget(ctx, caseDir);
6596
- const expected = await resolveTarget(ctx, scope.caseDir);
6597
- if (text(given.targetKey) === "" || given.targetKey !== expected.targetKey) return { ...failure$1("policy", "案例目录必须是本次登记的那一个(不许用工作空间根、兄弟案例或子目录)") };
6598
- } catch (error) {
6599
- return { ...failure$1("infrastructure", `案例目录解析失败:${error instanceof Error ? error.message : String(error)}`) };
6600
- }
6601
- return {
6602
- ok: true,
6603
- material: {
6604
- kind: "discussion",
6605
- scope
6606
- }
6607
- };
6608
- }
6609
- /** 这次调用是不是**已登记**的报告讨论会话(给它开材料权限的那一条身份判据)。 */
6610
- function isRegisteredDiscussion(scopes, sessionId) {
6611
- return scopes.peek(sessionId) !== void 0;
6612
- }
6613
6658
  //#endregion
6614
6659
  //#region src/host/tools/h3yun.ts
6615
6660
  /**
@@ -6745,10 +6790,17 @@ function h3yunTools(deps) {
6745
6790
  ...failure$1("policy", "审核子会话不能直接查询氚云记录:本轮记录与附件清单已在输入快照里"),
6746
6791
  ...base
6747
6792
  };
6748
- if (isRegisteredDiscussion(deps.discussionScopes, callerIdentity(exec).childId)) return {
6749
- ...failure$1("policy", "报告讨论会话不能查询氚云记录:材料范围只包含登记时取到的那批附件"),
6750
- ...base
6751
- };
6793
+ const discussion = deps.discussionScopes.peek(callerIdentity(exec).childId);
6794
+ if (discussion !== void 0) {
6795
+ const checked = await requireMaterialScope(ctx, deps.state, deps.discussionScopes, exec, {
6796
+ caseDir: args.caseDir,
6797
+ objectId
6798
+ }, { requireCaseDir: false });
6799
+ if (!checked.ok) return {
6800
+ ...checked,
6801
+ ...base
6802
+ };
6803
+ }
6752
6804
  const platform = await deps.world.platform();
6753
6805
  const gap = await requireCrwu(ctx, platform);
6754
6806
  if (gap !== null) return {
@@ -6761,7 +6813,7 @@ function h3yunTools(deps) {
6761
6813
  ...base
6762
6814
  };
6763
6815
  const requested = text(args.caseDir).trim();
6764
- let workdir = await deps.world.workdir();
6816
+ let workdir = discussion?.caseDir || await deps.world.workdir();
6765
6817
  if (requested !== "") {
6766
6818
  const caseCheck = await requireCaseDir(ctx, requested, { allowedRoot: allowedCaseRootOf(deps.state) });
6767
6819
  if (!caseCheck.ok) return {
@@ -6884,11 +6936,18 @@ function h3yunTools(deps) {
6884
6936
  count: 0,
6885
6937
  files: []
6886
6938
  };
6887
- if (isRegisteredDiscussion(deps.discussionScopes, callerIdentity(exec).childId)) return {
6888
- ...failure$1("policy", "报告讨论会话不能列举氚云附件:材料范围只包含登记时取到的那批附件"),
6889
- count: 0,
6890
- files: []
6891
- };
6939
+ const discussion = deps.discussionScopes.peek(callerIdentity(exec).childId);
6940
+ if (discussion !== void 0) {
6941
+ const checked = await requireMaterialScope(ctx, deps.state, deps.discussionScopes, exec, {
6942
+ caseDir: args.caseDir,
6943
+ objectId
6944
+ }, { requireCaseDir: false });
6945
+ if (!checked.ok) return {
6946
+ ...checked,
6947
+ count: 0,
6948
+ files: []
6949
+ };
6950
+ }
6892
6951
  const platform = await deps.world.platform();
6893
6952
  const gap = await requireCrwu(ctx, platform);
6894
6953
  if (gap !== null) return {
@@ -6903,7 +6962,7 @@ function h3yunTools(deps) {
6903
6962
  files: []
6904
6963
  };
6905
6964
  const requested = text(args.caseDir).trim();
6906
- let workdir = await deps.world.workdir();
6965
+ let workdir = discussion?.caseDir || await deps.world.workdir();
6907
6966
  if (requested !== "") {
6908
6967
  const caseCheck = await requireCaseDir(ctx, requested, { allowedRoot: allowedCaseRootOf(deps.state) });
6909
6968
  if (!caseCheck.ok) return {
@@ -6966,7 +7025,7 @@ function h3yunTools(deps) {
6966
7025
  name: TOOL_NAMES.h3yunFileGet,
6967
7026
  description: [
6968
7027
  "按 fileId **单附件定向下载**氚云记录附件到**本轮案例目录**内的相对路径。",
6969
- "只接受本轮输入快照(附件清单)里登记过的 fileId;不在清单里的附件在起进程之前就被拒绝。",
7028
+ "普通会话可按员工权限下载;托管审核与已登记讨论会话只接受本次材料清单里的 fileId。",
6970
7029
  "目标必须落在给定案例目录之下(越界直接拒绝)。",
6971
7030
  "本工具没有「整单下载」路径:一次只取一个附件,失败就是失败,不会退化成批量下载。"
6972
7031
  ].join(" "),
@@ -6974,7 +7033,7 @@ function h3yunTools(deps) {
6974
7033
  fileId: {
6975
7034
  type: "string",
6976
7035
  required: true,
6977
- description: "附件 fileId(只接受本轮输入快照登记过的附件)"
7036
+ description: "附件 fileId(托管审核与已登记讨论会话须在本次材料清单内)"
6978
7037
  },
6979
7038
  caseDir: {
6980
7039
  type: "string",
@@ -7012,22 +7071,22 @@ function h3yunTools(deps) {
7012
7071
  async execute(args, exec) {
7013
7072
  const ctx = toolContext(deps.ctx, exec);
7014
7073
  const fileId = text(args.fileId).trim();
7015
- const materialCheck = await requireMaterialScope(ctx, deps.state, deps.discussionScopes, exec, { caseDir: args.caseDir });
7074
+ const materialCheck = await requireCaseAccess(ctx, deps.state, deps.discussionScopes, exec, { caseDir: args.caseDir });
7016
7075
  if (!materialCheck.ok) return {
7017
7076
  ...materialCheck,
7018
7077
  fileId,
7019
7078
  path: "",
7020
7079
  sizeBytes: 0
7021
7080
  };
7022
- const material = materialCheck.material;
7023
- const casePath = material.kind === "audit" ? material.scope.casePath : material.scope.caseDir;
7081
+ const material = materialCheck;
7082
+ const casePath = material.casePath;
7024
7083
  if (fileId === "") return {
7025
7084
  ...failure$1("input", "fileId 不能为空"),
7026
7085
  fileId,
7027
7086
  path: "",
7028
7087
  sizeBytes: 0
7029
7088
  };
7030
- if (!material.scope.allowedAttachmentIds.includes(fileId)) return {
7089
+ if (material.kind !== "session" && !material.allowedAttachmentIds?.includes(fileId)) return {
7031
7090
  ...failure$1("policy", material.kind === "audit" ? "这个附件不在本轮审核的输入快照里:只允许下载本次登记的附件" : "这个附件不在本次登记的材料范围里:只允许下载登记时取到的那批附件(请在工作台重新点「与 DeepSeek 讨论报告」刷新)"),
7032
7091
  fileId,
7033
7092
  path: "",
@@ -9405,7 +9464,7 @@ function knowledgeTools(deps) {
9405
9464
  ...failure$1("input", "paths 不能为空"),
9406
9465
  ...empty
9407
9466
  };
9408
- const caseCheck = await requireAuditScope(ctx, deps.state, exec, { caseDir: args.caseDir });
9467
+ const caseCheck = await requireCaseAccess(ctx, deps.state, deps.discussionScopes, exec, { caseDir: args.caseDir });
9409
9468
  if (!caseCheck.ok) return {
9410
9469
  ...caseCheck,
9411
9470
  ...empty
@@ -9982,7 +10041,7 @@ function ossTools(deps) {
9982
10041
  ...failure$1("input", `流水号形状不对(不放进 OSS 路径):${seqNo === "" ? "(空)" : seqNo}`),
9983
10042
  ...empty
9984
10043
  };
9985
- const caseCheck = await requireAuditScope(ctx, deps.state, exec, {
10044
+ const caseCheck = await requireCaseAccess(ctx, deps.state, deps.discussionScopes, exec, {
9986
10045
  caseDir: args.caseDir,
9987
10046
  seqNo: args.seqNo
9988
10047
  });
@@ -10409,7 +10468,7 @@ function pythonScriptTools(deps) {
10409
10468
  caseDir: {
10410
10469
  required: true,
10411
10470
  type: "string",
10412
- description: "本轮案例目录(Host 用它做审核范围校验)。"
10471
+ description: "本轮案例目录(Host 校验工作空间与案例范围)。"
10413
10472
  },
10414
10473
  script: {
10415
10474
  required: true,
@@ -10521,7 +10580,7 @@ function pythonScriptTools(deps) {
10521
10580
  async execute(args, exec) {
10522
10581
  const ctx = toolContext(deps.ctx, exec);
10523
10582
  const base = idleResult();
10524
- const caseCheck = await requireAuditScope(ctx, deps.state, exec, { caseDir: args.caseDir });
10583
+ const caseCheck = await requireCaseAccess(ctx, deps.state, deps.discussionScopes, exec, { caseDir: args.caseDir });
10525
10584
  if (!caseCheck.ok) return {
10526
10585
  ...base,
10527
10586
  ...caseCheck
@@ -10552,7 +10611,7 @@ function pythonScriptTools(deps) {
10552
10611
  ...base,
10553
10612
  ...failure$1("capability-gap", "DSH 自带 Python 解析器没有装配:本插件实例不能执行技能脚本")
10554
10613
  };
10555
- const runtime = await deps.python.check();
10614
+ const runtime = await deps.python.check({ agent: exec.agent });
10556
10615
  if (!runtime.ok) return {
10557
10616
  ...base,
10558
10617
  ...failure$1("capability-gap", `DSH 自带 Python 不可用(${runtime.state}):${runtime.error}`),
@@ -16312,7 +16371,7 @@ function createCoreOperations(ctx, config, state, world, resolvers, extra = {})
16312
16371
  buildKind: HOST_BUILD_KIND,
16313
16372
  at: (/* @__PURE__ */ new Date()).toISOString(),
16314
16373
  builtAt: HOST_BUILD_STAMP,
16315
- protocol: 25
16374
+ protocol: 26
16316
16375
  }),
16317
16376
  boot: async () => {
16318
16377
  const home = await world.home();
@@ -16331,7 +16390,7 @@ function createCoreOperations(ctx, config, state, world, resolvers, extra = {})
16331
16390
  });
16332
16391
  return {
16333
16392
  ok: true,
16334
- protocol: 25,
16393
+ protocol: 26,
16335
16394
  permissionSchemaVersion: 1,
16336
16395
  localAccess: await syncLocalAccessConsent({
16337
16396
  ctx,
@@ -16432,7 +16491,7 @@ function createCoreOperations(ctx, config, state, world, resolvers, extra = {})
16432
16491
  */
16433
16492
  trust: async () => ({
16434
16493
  ok: false,
16435
- error: `客户端与宿主的权限说明版本不一致:本机访问授权已改为版本化收据(协议 ${String(25)}),请完全退出并重新打开 DeepSeek Harness 后在「账号连接」里按新的范围允许一次。`,
16494
+ error: `客户端与宿主的权限说明版本不一致:本机访问授权已改为版本化收据(协议 ${String(26)}),请完全退出并重新打开 DeepSeek Harness 后在「账号连接」里按新的范围允许一次。`,
16436
16495
  protocolMismatch: true,
16437
16496
  consent: await readLocalAccessConsent({
16438
16497
  ctx,
@@ -18746,7 +18805,7 @@ function apply(ctx, pluginConfig) {
18746
18805
  "oss-config": ossConfigPath
18747
18806
  },
18748
18807
  hostVersion: PLUGIN_VERSION,
18749
- protocolVersion: 25
18808
+ protocolVersion: 26
18750
18809
  });
18751
18810
  subscribeAuditEvents(ctx, state);
18752
18811
  const form = new H3yunFormResolver({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-crwu-workbench",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "中瑞世联工作台 / CRWU audit workbench for DeepSeek Harness: pick a pending audit report, dispatch one AI audit subagent, watch and stop/restart it, auto-upload deliverables to Aliyun OSS.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -53,7 +53,8 @@ H0 的机制 owner 是 `references/00-input-and-route-profile.md`,隐藏区的
53
53
  13. [11-html-delivery-spec.md](references/11-html-delivery-spec.md):阶段一定稿冻结后、阶段二对照与交付(步骤 14)时读取;**送达与交付层正文**(CRWU 审核意见 HTML 送达规范 v1.6:AuditResult 单一事实源、JSON 校验先行、员工单文件 HTML + 同源监控 JSON、双证据链、两阶段门禁、客观命中率与验收清单)。
54
54
  14. [12-leaf-common-contract.md](references/12-leaf-common-contract.md):加载任一 `crwu-audit-asset-*` / `crwu-audit-biz-*` 叶子时读取;**叶子共同约束**(轴边界、输入、一级根装配、二级选择、执行顺序、条目状态、来源优先级、证据出处、capability gap)。公共规则只在该文件写一份,叶子不各自复述;叶子与它冲突时以它为准。
55
55
  15. [13-dingtalk-result-publish.md](references/13-dingtalk-result-publish.md):步骤 14 已生成最终态监控 JSON 后读取;定义固定组织/团队空间/结果目录的精确解析、按审核年月建目录、带生成时间戳文件名、DWS 上传与写后验证契约。
56
- 16. [14-orchestration-workflow.md](references/14-orchestration-workflow.md):**正式执行前必读**;router 运行时执行顺序与条件分支的唯一落点——脚本运行时(Python)、路由流程步骤 1–15、KB 兼容与运行时装配边界;步骤 14 的交付细节仍以 `11-html-delivery-spec.md` 为权威、钉钉回传仍以 `13-dingtalk-result-publish.md` 为权威,本文件不复述它们的正文。
56
+ 16. [15-oss-result-publish.md](references/15-oss-result-publish.md):步骤 14 成对生成并校验最终 HTML/JSON 后、步骤 15 上传前读取;定义默认双文件 OSS 发布、受信目标、逐文件写后验证和非阻塞失败契约。
57
+ 17. [14-orchestration-workflow.md](references/14-orchestration-workflow.md):**正式执行前必读**;router 运行时执行顺序与条件分支的唯一落点——脚本运行时(Python)、路由流程步骤 1–16、KB 兼容与运行时装配边界;交付、OSS 与钉钉细节分别以 11、15、13 为权威,本文件不复述它们的正文。
57
58
 
58
59
  ## 输入
59
60
 
@@ -81,10 +82,10 @@ H0 的机制 owner 是 `references/00-input-and-route-profile.md`,隐藏区的
81
82
 
82
83
  router 的运行时执行顺序与条件分支**只在** [14-orchestration-workflow.md](references/14-orchestration-workflow.md) 维护;入口不复述、不并行维护第二份。
83
84
 
84
- 1. **正式执行前必须读取** `references/14-orchestration-workflow.md`——Python 运行时、路由流程步骤 1–15、KB 兼容与运行时装配边界都在那里。
85
- 2. 必须**按 14 的步骤 1–15 顺序执行**:不得跳步、不得调序、不得改写已冻结的阶段产物。
85
+ 1. **正式执行前必须读取** `references/14-orchestration-workflow.md`——Python 运行时、路由流程步骤 1–16、KB 兼容与运行时装配边界都在那里。
86
+ 2. 必须**按 14 的步骤 1–16 顺序执行**:不得跳步、不得调序、不得改写已冻结的阶段产物。
86
87
  3. **阶段一隔离与冻结、阶段二复核不得合并**:读取任何复核记录前,阶段一正式意见与 `route_profile` 必须已定稿并冻结。
87
- 4. Python 运行时与 KB 装配边界服从 14;交付与送达服从 [11-html-delivery-spec.md](references/11-html-delivery-spec.md)(**JSON 校验先行**、`validate` → `render --json-out` 调用序列与字段契约以 11 为唯一权威);钉钉回传服从 [13-dingtalk-result-publish.md](references/13-dingtalk-result-publish.md)。
88
+ 4. Python 运行时与 KB 装配边界服从 14;交付与送达服从 [11-html-delivery-spec.md](references/11-html-delivery-spec.md)(**JSON 校验先行**、`validate` → `render --json-out` 调用序列与字段契约以 11 为唯一权威);OSS 发布服从 [15-oss-result-publish.md](references/15-oss-result-publish.md);钉钉回传服从 [13-dingtalk-result-publish.md](references/13-dingtalk-result-publish.md)。
88
89
  5. 任一步门禁失败**按「失败与冲突」处理**,不得静默跳过,也不得降级叙述成"材料缺失"。
89
90
 
90
91
  ## 失败与冲突
@@ -95,6 +96,7 @@ router 的运行时执行顺序与条件分支**只在** [14-orchestration-workf
95
96
  - ROUTE001–004 只按 06 挂起受影响的业务/范围/资产标签、基准日或价值类型及其依赖规则;未受影响的技能继续执行。证据冲突不得静默任选一边。
96
97
  - 单个专业技能 `pending`、未注册、下载失败或执行失败时逐标签记录原因,其他 `available` 技能和满足条件的公共能力继续执行。
97
98
  - 钉钉回传的目标组织、团队空间或结果根目录零命中/多命中时停止回传;只有审核年份和月份目录允许缺失后创建。回传失败不删除本地交付件,也不得换组织、选相似目录或覆盖同名远端文件。
99
+ - OSS 交付失败只停止该通道,保留已验证本地双文件并继续钉钉交付;各通道分别报告,不能把本地审核完成冒充完全交付成功。
98
100
  - 只有所有分发维度均无可靠命中时,才停止专业审核结论,输出已知画像、证据、缺口与候选,请求人工确认;表格等不依赖画像的公共能力仍可按条件执行,但不得冒充专业结论。
99
101
 
100
102
  ## 输出
@@ -148,5 +150,6 @@ dispatch.skills_to_load = stable_unique(
148
150
  - `references/11-html-delivery-spec.md`(v1.6 送达规范)要求的逐条裁定、复核对照与综合对比、《本次审核记录清单》、未检查项,以及最终**员工单文件 HTML + 内部同源监控 JSON**(AuditResult 单一事实源渲染)。
149
151
  - 脚本目录、用法与维护入口见 [scripts/README.md](scripts/README.md)(脚本清单、命令用法、强制校验规则与维护规则);交付字段与编排层调用映射仍以 `references/11-html-delivery-spec.md` 为唯一权威,本入口不复述。
150
152
  - `references/13-dingtalk-result-publish.md` 要求的钉钉回传状态:成功时保留精确远端路径与节点 ID;失败时保留失败层级与真实原因。
153
+ - `references/15-oss-result-publish.md` 要求的 OSS 状态:双文件分别保留验证结果与对象键;失败/部分成功时保留真实错误与缺项,不改写审核结论。
151
154
 
152
155
  无专业技能可用时仍交付画像、逐标签 gap 与已执行公共能力结果,不以“无能力”空返。
@@ -876,6 +876,6 @@ renderer 只能按上表映射展示,不能新增业务字段或另造统计
876
876
 
877
877
  约定:`digest` 与 `render` 使用同一规范化序列化(排序键、UTF-8、无多余空白),因此冻结指纹可复现、`fileTrace.sourceDigest` 可与冻结摘要互校;脚本仅依赖 Python 标准库,退出码 `0` 通过 / `1` 失败。`render` 的执行顺序固定为:载入输入 → **JSON 校验** → 内存渲染 → 提取 HTML 内嵌对象 → 渲染态校验 → 成对写出;禁止在 JSON 合规前进入 HTML 渲染逻辑。
878
878
 
879
- ### 14.2 最终 JSON 钉钉回传
879
+ ### 14.2 最终结果远端发布
880
880
 
881
- 步骤 14 只负责产生员工 HTML 与最终态内部监控 JSON;其后的远端归档属于独立传输边界。步骤 15 必须读取本技能 `references/13-dingtalk-result-publish.md`,只把已经通过渲染后校验的配套 JSON 回传到固定组织与团队空间。远端失败不得反向改写 AuditResult、HTML 或审核结论,也不得用“本地已生成”代替“远端已验证”。
881
+ 步骤 14 只负责产生员工 HTML 与最终态内部监控 JSON;其后的远端发布属于独立传输边界。步骤 15 读取本技能 `references/15-oss-result-publish.md`,显式发布已经通过最终态校验的 HTML/JSON 双文件;步骤 16 读取 `references/13-dingtalk-result-publish.md`,只把配套 JSON 归档到固定组织与团队空间,再把 HTML 发给自己并 DING。各通道独立报告,OSS 失败不阻断钉钉交付。远端失败不得反向改写 AuditResult、HTML 或审核结论,也不得用“本地已生成”代替“远端已验证”。
@@ -5,9 +5,9 @@
5
5
  >
6
6
  > 本文件**不复制**以下权威正文:输入字段与五轴画像定义见 `00-input-and-route-profile.md`;
7
7
  > 交付与送达规范见 `11-html-delivery-spec.md`(步骤 14 只回指);钉钉回传契约见
8
- > `13-dingtalk-result-publish.md`;并集算法与公共能力触发事实源见 `08-union-dispatch-rules.md`。
8
+ > `13-dingtalk-result-publish.md`;OSS 发布契约见 `15-oss-result-publish.md`;并集算法与公共能力触发事实源见 `08-union-dispatch-rules.md`。
9
9
  >
10
- > **导航**:§1 脚本运行时(Python) → §2 路由流程(步骤 1–15) → §3 KB 兼容与运行时装配边界
10
+ > **导航**:§1 脚本运行时(Python) → §2 路由流程(步骤 1–16) → §3 KB 兼容与运行时装配边界
11
11
 
12
12
  ## 1. 脚本运行时(Python)
13
13
 
@@ -88,7 +88,7 @@ Invoke-DshPython '<load_workspace_dependencies 返回的绝对 Python 路径>' @
88
88
  这条兼容层只针对 Windows 子代理脚本;macOS / Linux 仍按上面的 POSIX 命令直接执行。
89
89
  不能因为包装器失败就改用系统解释器、搜索 PATH 或取消 `workspace-write`。
90
90
 
91
- ## 2. 路由流程(步骤 1–15)
91
+ ## 2. 路由流程(步骤 1–16)
92
92
 
93
93
  1. **消费 Host 已准备的输入快照(不要再定位、不要重复取数)**:报告已由 Host 按精确 ObjectId 定位并取数一次,结果落在案例目录的 `输入快照/` 下——完整记录 `报告记录.json`、附件清单 `附件清单.json`、元数据 `快照元数据.json`。`schemaCode` 是 Host 的基础设施状态:**核验记录事实一律以 `报告记录.json` 为准**,不要提交、不要猜测 `schemaCode`,也不要再调 `records list` / 搜表单 / 重新取记录。仅在启动指令明确说明「输入快照缺失」时,才允许**一次**兜底:调 `crwu_h3yun_record_get({objectId,caseDir})`(它由 Host 自己解析 `schemaCode`)。附件元数据直接读 `附件清单.json`(字段:附件字段、文件名、类型、大小、`fileId`)并分类;只有快照缺失时才对同一 `objectId` 调一次 `crwu_h3yun_files_list({objectId,caseDir})`;附件名只用于隔离决策和待抽验提示。(工具**不返回**带会话鉴权的下载 URL,也不需要。)
94
94
  2. **阶段一安全下载门禁**:先把一至四级复核意见、质控意见、底稿/在线底稿意见、外审意见、答复文件等归为复核记录。源材料**只按单附件定向下载**取回:对每个源材料附件调 `crwu_h3yun_file_get({fileId, caseDir, relativePath: '材料-源/<文件名>'})`(fileId 来自步骤 1 的元数据);**复核件绝不下载、绝不进入阶段一工作集**——只在《阶段一排除清单》记元数据。**本链路没有「整单下载」这个能力**:`crwu_h3yun_file_get` 一次只取一个附件,失败就是失败,不存在退化成批量下载的路径。若 `crwu_h3yun_file_get` 不可见或返回 capability gap、或记录含无法可靠区分源材料/复核件的附件,立即停止并记录 capability gap,绝不能接触复核正文。**被排除的复核件必须落盘《阶段一排除清单》**(`排除清单.json`:`[{fileId,name,sizeBytes}]`),供阶段二按 `scripts/fetch_review_records.py --manifest 排除清单.json` 定向取回——取回只认该清单、落 `复核-人工/`、禁写 `材料-源/`、逐件校验字节数(闭环,防越权取件)。
@@ -107,7 +107,8 @@ Invoke-DshPython '<load_workspace_dependencies 返回的绝对 Python 路径>' @
107
107
  12. **阶段二复核对照**:AI 意见固定后才在阶段二上下文读取复核记录。**先抽复核件媒体证据**:复核件经 `scripts/fetch_review_records.py` 落 `复核-人工/` 后,跑 `python3 scripts/prepare_materials.py --case <案例目录> --src 复核-人工 --label 复核`(产出 `复核盘点.json` / `复核媒体索引.json` / `媒体证据-复核/`,**绝不覆盖阶段一冻结的 `材料盘点.json`**),把复核意见附件里的图与独立图片同样导出并交宿主多模态读图——人工用截图提出的问题必须按其 `locator` 逐条进入复核事项全集,不得因"复核件里只有图、没有文字"而漏进 `B·AI 新增`。按发生顺序登记 `reviewFiles[]`,并把每条复核意见写入 `reviewItems[]`,记录复核级次、问题模块、`exact/partial/miss`、关联阶段一 issueId、复核原文和处理结果;再做双向三条带:AI∩复核、AI 新增、复核独有。**复核独有项必须先做「在件核验」,再定性**:逐条核验该事项在被审件**最终版**(定稿/终稿)中是否已落实,核验须给出**在件位置**(文件 + 行号/单元格)作为证据,与复核原文出处并列留痕。核验结论四态:`L-resolved`(在件已落实 → 不计漏检、不进命中率分母,仅作人工工作成果呈现。**凡 AI 未命中(`matchStatus=miss`)的复核意见,必须先判断是否因该事项“已被修改/已不存在”而导致 AI 不可能命中:属此情形记 `L-resolved` 并从命中率分母中剔除,且必须给出在件原文(`inFileEvidence.quote`)作为“确已修改”的证明;无法证明已修改的,只能记 `L-open` 并计入漏检**);`L-open`(在件未落实 → 漏检候选,进步骤 13 隔离补审);`L-uncheckable`(材料不可读或缺失 → 记未检查项,不臆断);`L-unclosed`(答复称已改但被审件未落地 → 优先回客户,并同时给 `closureEvidence` 与 `inFileEvidence`)。**禁止**跳过在件核验直接按复核独有项启动补审或计入漏检;不得拿复核文本直接改写阶段一意见。**能力边界与分母剔除(强制)**:①交付件必须显式声明「本工具当前暂不支持底稿文件审核」,底稿类复核意见(`reviewComparison.outOfScopeItems[]`)不计入 AI 命中率、不计漏检,但必须登记备查、可见可展开;②交付件必须逐条列出**不计入命中率分母的条目及其剔除依据**(已修改/已落实、材料缺失或不可读、能力边界三类),使分母可审计;③被剔除的 `L-resolved` 项若为 AI 未命中,须给在件原文证明。客观指标按明细重算:`evaluable=total-L-resolved-L-uncheckable`;**命中率=`(exact+partial)/evaluable`**(**部分命中计为命中,只是层次较低**),并同时输出**命中层次**:精确命中率=`exact/evaluable`、部分命中率=`partial/evaluable`、未命中率=`miss/evaluable`,全部用百分数;综合率先汇总分子分母,不平均各组百分比。**禁止**以 `exact/evaluable` 作为「命中率」主指标(那等于把部分命中当未命中)。每条复核意见必须给出 `hitExplanation`(`reviewerScope` 笼统/具体、`matchedAspects` AI 命中内容、`unmatchedAspects` 未命中内容、`rationale` 判定理由):复核条目**笼统**(员工写得宽)而 AI 的发现已覆盖其全部实质诉求、只是更细更深时,判 **exact**,不得因深度差异降为 partial;只有复核条目含多个诉求、AI 确有一部分未触及时才判 **partial**。人工指出或在件核验发现的 AI 错误追加进 `selfAuditErrors[]`(`discoveredAt=复审`)。
108
108
  13. **隔离补审**:发现漏检候选后,只能由阶段二编排器创建不继承复核文本和阶段二历史的新子任务或隔离执行上下文。该上下文只接收阶段一源材料工作集、已验证的 DWS 规则快照,以及步骤 11 在复核读取前已冻结的完整命中模块集合;应重跑全部阶段一命中模块或完整阶段一审核。禁止传复核原文、摘要、结论,也禁止用复核事项派生、改写、提示、选择或缩窄补审范围。隔离执行先产出并固定不可变的补审结果,再回到阶段二上下文对照。无法建立该隔离上下文时不补审,记录 capability gap 并交人工复核;需要长期修复时登记 `crwu-dev-audit-optimize`。
109
109
  14. **交付与回填**:按技能内 `references/11-html-delivery-spec.md`(v1.6 送达规范)执行,**不再引用知识库输出契约**。以 **AuditResult JSON 为单一事实源**汇总阶段一 findings、适用规则集快照、逐条裁定、复核文件与逐条对照、客观命中率及《本次审核记录清单》(AI 检查项、知识库业务/资产必检项、评估数据核查、监管覆盖核查、风险覆盖核查、未检查项及阶段二对照/归因,逐条给证据,对比项两端都给依据)。**每条问题的 `problemDescription` 必须按 §4.6 写成员工可直接核对的「两段式」**:首句 ≤60 字一句话说清问题是什么;其后另起一行写 2–4 行明细(写明差异与影响,并给出本次材料中的具体文件、sheet、单元格或页码)。首句与明细均不得出现规则编号、知识库路径、内部代号或绝对路径——规则叫什么、出自哪本规则库,属于默认折叠的「展开判断依据与规则」,不占员工首屏。报告读者是资产评估师:读不懂的描述等于没审出来,校验器会直接拦截并拒绝渲染。发布前必须通过送达规范 **JSON 校验**:规则性缺陷双证据链齐备、五段式判定完整、问题描述两段式可读、未检查项显式、统计可重算、无绝对路径/`nodeId`/凭据。**每个项目向员工只交付一个自包含单文件 HTML** `审核意见.<项目ID>.html`;内部同步留档 `审核结果.<项目ID>.json` 供审核监控统计,且必须与 HTML 内嵌 AuditResult 完全一致。页面首屏使用三行行动摘要,计数卡可跳转;正文按序呈现 AI 问题、需要人工确认、AI 外部核验、人工复核与客观评分卡;问题位置醒目展示,完整说明与专业轨迹按需展开。落地实现见本技能 `scripts/audit_delivery.py`、`scripts/audit_result.schema.json` 与 `template/audit-report.html`。**编排层交付调用序列(按序执行,不得跳步)**:① 汇总产出 AuditResult 输入 JSON;② `python3 scripts/audit_delivery.py validate 审核意见.<项目ID>.json`(失败 → 停止交付、逐条报告精确 JSON 路径,HTML 与配套 JSON 均不得生成);③ 仅在通过后执行 `python3 scripts/audit_delivery.py render 审核意见.<项目ID>.json --out 审核意见.<项目ID>.html --json-out 审核结果.<项目ID>.json`(脚本再次 JSON 校验并做渲染后自检,全部通过才成对落盘);④ 员工侧交付 HTML,内部监控读取配套 JSON。仅当运行时技能副本缺少上述脚本、Schema 或模板时记 capability gap,此时只保留输入 JSON 与校验错误报告,**不得跳过 JSON 校验直接出 HTML**。契约 04 统计台账仍须由本次 `crwu-dws` 实时下载后回填。
110
- 15. **钉钉结果回传**:步骤 14 的最终态 `审核结果.<项目ID>.json` 生成后,调 `crwu_audit_dingtalk_archive({caseDir, seqNo})`(契约见 `references/13-dingtalk-result-publish.md`);再把 HTML 发给自己并 DING:调 `crwu_audit_dingtalk_notify_self({caseDir, seqNo})`。组织、团队空间、结果根目录、`YYYY/MM` 两级目录全部由 Tool 按契约精确解析(只有年月允许自动创建),**不要提交这些标识、不要手工指定目录**。远端文件名追加 `fileTrace.generatedAt` 时间戳,已存在同名文件时拒绝覆盖。上传失败保留本地交付件并显式报告“钉钉回传失败”,不得把审核完成等同于回传成功。
110
+ 15. **OSS 双文件发布**:步骤 14 成对生成并校验最终 HTML 与监控 JSON 后,必须读取并执行 `references/15-oss-result-publish.md`,调 `crwu_audit_oss_publish({caseDir, seqNo, files})`,显式列出本案例的 `审核意见.<报告流水号>.html` 与 `审核结果.<报告流水号>.json`。只发布完整两阶段审核(含必要的隔离补审)后的最终结果;成功必须按精确文件名核对两项写后验证,不能只看整体 `ok` 或上传数量。失败保留本地双文件,显式报告失败/部分成功及真实原因,继续步骤 16,不改写审核结论、不绕过 Host 权限。
111
+ 16. **钉钉结果回传**:步骤 14 的最终态 `审核结果.<项目ID>.json` 生成后,独立于 OSS 成败调 `crwu_audit_dingtalk_archive({caseDir, seqNo})`(契约见 `references/13-dingtalk-result-publish.md`);再把 HTML 发给自己并 DING:调 `crwu_audit_dingtalk_notify_self({caseDir, seqNo})`。组织、团队空间、结果根目录、`YYYY/MM` 两级目录全部由 Tool 按契约精确解析(只有年月允许自动创建),**不要提交这些标识、不要手工指定目录**。远端文件名追加 `fileTrace.generatedAt` 时间戳,已存在同名文件时拒绝覆盖。上传失败保留本地交付件并显式报告“钉钉回传失败”,不得把审核完成等同于回传成功。
111
112
 
112
113
  ## 3. KB 兼容与运行时装配边界
113
114
 
@@ -0,0 +1,49 @@
1
+ # 最终审核结果 OSS 发布契约(结构化 Tool)
2
+
3
+ 本契约只负责上传步骤 14 成对生成并通过最终态校验的 HTML 与内部监控 JSON。默认执行,不因启动提示词未提及而省略;不上传会话、推理记录、源材料、冻结快照或 AuditResult 输入 JSON,不参与审核判断。
4
+
5
+ ## 输入与执行时点
6
+
7
+ - 完成阶段一冻结、阶段二复核对照及必要的隔离补审后,按 `11-html-delivery-spec.md` 执行 `validate` → `render --json-out`。只发布本轮最终结果,不提前发布初审或中间版本。
8
+ - 两个文件必须来自同一次成功渲染:`审核意见.<报告流水号>.html` 与 `审核结果.<报告流水号>.json`,均非空,HTML 内嵌 AuditResult 与配套 JSON 完全一致。文件名中的项目 ID 使用本案例的报告流水号,与 `seqNo` 一致;不读取旧轮产物补齐缺件。
9
+ - 成对生成或最终态校验失败时,不调用上传;按步骤 14 处理本地交付失败,不能只传 HTML 冒充完整结果。
10
+
11
+ **执行入口只有 `crwu_audit_oss_publish`,必须显式列出两个文件:**
12
+
13
+ ```text
14
+ crwu_audit_oss_publish({
15
+ caseDir: "<案例目录绝对路径>",
16
+ seqNo: "<报告流水号>",
17
+ files: ["审核意见.<报告流水号>.html", "审核结果.<报告流水号>.json"]
18
+ })
19
+ ```
20
+
21
+ `caseDir` 使用本次 Host 交接的案例目录,`seqNo` 使用已核验的报告流水号。`files` 只提交这两个案例内相对文件名,不省略、不扩大清单:Tool 的默认清单会跳过缺失文件,不满足本契约的双文件要求。
22
+
23
+ ## 目标与 Host 边界
24
+
25
+ Tool 是 Workbench Host 提供的外部能力,不随单独复制 Skill 自动安装。它核验当前调用会话的审核范围、案例目录及流水号,按受信配置解析 `oss.enabled`、bucket、endpoint 和 prefix;模型不得提交存储目标、基础设施标识或凭据,不自行拼 `oss://` 地址。
26
+
27
+ 对象键沿用现有 Tool 契约:`<受信 oss.prefix>/<报告流水号>/<文件名>`。同一案例同名对象会覆盖为本轮结果;**没有年月目录、生成时间戳文件名或禁止覆盖保证**。钉钉的年月与时间戳规则仅属于 `13-dingtalk-result-publish.md`,不能套用到 OSS。本步骤不提供跨版本留档或双对象原子提交保证。
28
+
29
+ 手工创建主会话后粘贴提示词,不等于已取得 Host 审核范围。Tool 拒绝该调用时保留拒绝事实,不伪造范围、不换案例目录、不绕过授权;本契约不改变会话绑定机制。
30
+
31
+ ## 写后验证与成功判据
32
+
33
+ Tool 对每个文件上传后实际列举目标对象,核对对象存在、远端字节数与本地一致且非零。模型不用另行调用命令行验证。
34
+
35
+ 只有以下条件**同时成立**才报告“OSS 双文件上传成功”:
36
+
37
+ 1. 返回结构化结果,整体 `ok:true`。
38
+ 2. `results[]` 按精确 `name` 分别唯一命中本次请求的 HTML、JSON;两项均 `ok:true`,且包含真实 `key` 和非零 `sizeBytes`(Tool 写后验证所得)。
39
+ 3. 没有失败项或缺项;不以退出码、`uploaded` 数量、整体 `ok:true` 或仅 HTML 成功替代逐文件核对。
40
+
41
+ 汇报每个文件的 `name`、`key`、`sizeBytes`、`ok`。`key` 只引用 Tool 返回的普通对象键;不输出签名 URL、凭据、私有 endpoint 或未经脱敏的底层日志。
42
+
43
+ ## 失败与收尾
44
+
45
+ - Tool 不可见、能力缺失、配置关闭/缺失、认证/权限/范围拒绝、上传失败或写后验证不通过,都属于 OSS 交付失败。停止本通道,不自动循环重试、不改目标;需要重试时先解决失败原因,结果重新生成则重新成对校验再发布。
46
+ - 保留本地已验证 HTML 与 JSON,不回滚审核、不改写已冻结产物或审核结论。只证明一个文件成功时报告“OSS 部分成功”,列出成功项与失败/缺项;无法确认的对象状态标记为“未确认”,不声称未上传或成功。
47
+ - 对 Tool 失败,保留返回的 `errorKind`、已脱敏 `error` 及逐文件结果;对返回不完整,列明缺失的预期文件/验证字段。Tool 不可见时写明 `capability gap:缺少 crwu_audit_oss_publish`,不虚构返回。
48
+ - **OSS 失败不阻断步骤 16 的钉钉归档与通知**;各通道独立调用、独立报告结果。即使交付阶段遇到认证/授权失败,也只停止受影响通道,不把已完成的本地审核称为失败;不得自行登录、查找凭据或改走 shell / Python / 裸业务命令。
49
+ - 最终回复分开说明本地审核、OSS、钉钉归档、钉钉通知。例如:“本地审核完成;OSS 上传失败/部分成功(具体原因);钉钉归档与通知各自状态。”允许本地审核完成但云端交付失败,**不得冒充所有通道完全交付成功**。
@@ -22,8 +22,10 @@
22
22
  | 能力 gap 与待建提案 | `10-capability-gap-proposal.md` |
23
23
  | 送达与交付层规范(AuditResult/单文件 HTML、双证据链、两阶段门禁、验收清单) | `11-html-delivery-spec.md` |
24
24
  | 最终 AuditResult 的钉钉组织门禁、年月归档、文件命名与上传验证 | `13-dingtalk-result-publish.md` |
25
+ | 最终 HTML/JSON 的 OSS 双文件发布、写后验证与失败收尾 | `15-oss-result-publish.md` |
25
26
  | HTML 页面结构、左侧目录、CSS 与打印样式 | `template/audit-report.html` |
26
- | 总体 workflow、加载指针与输出步骤 | `SKILL.md` |
27
+ | 总体 workflow 与运行时执行顺序 | `14-orchestration-workflow.md` |
28
+ | 加载指针与输出门禁 | `SKILL.md` |
27
29
 
28
30
  规则正文和检查点不属于本技能族的 owner;只允许按编号和知识库层级路径引用。**例外**:送达与交付层正文(CRWU 审核意见 HTML 送达规范 v1.4)为本技能内正式规范,owner 是 `11-html-delivery-spec.md`,不经知识库下载。
29
31
 
@@ -4,6 +4,7 @@
4
4
  规范正文(唯一事实源):本技能 `references/11-html-delivery-spec.md`。
5
5
 
6
6
  > **归档与通知只走结构化 Tool,本目录不含任何业务 CLI 执行路径。**
7
+ > OSS 双文件发布由 `crwu_audit_oss_publish` 完成(契约见 [15-oss-result-publish.md](../references/15-oss-result-publish.md))。
7
8
  > 钉钉回传由 `crwu_audit_dingtalk_archive`(契约见 `references/13-dingtalk-result-publish.md`)与
8
9
  > `crwu_audit_dingtalk_notify_self` 完成;Skill 自带脚本**不得**用 `subprocess` / `child_process` /
9
10
  > shell / 裸命令调用 `crwu` / `dws` / `ossutil`。这条边界由 `npm run skills:cli-guard`
@@ -27,7 +28,7 @@
27
28
  | `examples/audit-result.sample.json` | 【示意】样例(数值与名称为占位,禁止当真值使用) | §9.3 |
28
29
  | `test_audit_delivery.py` | 契约测试(Schema 语义、门禁、证据链、统计可重算、隐私、模板、目录、渲染确定性、转义、打印、空态) | §13.4 |
29
30
  | `test_fetch_review_records.py` | `fetch_review_records.py` 的契约测试(清单边界、字节数校验、禁写源材料目录) | [14-orchestration-workflow.md](../references/14-orchestration-workflow.md) 步骤 2、步骤 12 |
30
- | `test_audit_multiaxis_router.py` | 多轴 router 契约测试(注册表、并集派发、装配路径键、渐进披露与入口尺寸) | [SKILL.md](../SKILL.md) + `references/00–14` |
31
+ | `test_audit_multiaxis_router.py` | 多轴 router 契约测试(注册表、并集派发、装配路径键、渐进披露与入口尺寸) | [SKILL.md](../SKILL.md) + `references/00–15` |
31
32
 
32
33
  ## 用法
33
34
 
@@ -1669,7 +1669,7 @@ class DeliveryContractDocumentationTest(unittest.TestCase):
1669
1669
 
1670
1670
  router_test_owner = _owner_cell("test_audit_multiaxis_router.py")
1671
1671
  self.assertIn("SKILL.md", router_test_owner, "router 测试必须回指 router 入口")
1672
- self.assertIn("references/00–14", router_test_owner, "router 测试必须回指 router references 全段")
1672
+ self.assertIn("references/00–15", router_test_owner, "router 测试必须回指 router references 全段")
1673
1673
  self.assertNotIn("§13.4", router_test_owner, "router 测试不归交付规范 §13.4")
1674
1674
 
1675
1675
 
@@ -95,6 +95,7 @@ EXPECTED_REFERENCE_FILES = (
95
95
  "12-leaf-common-contract.md",
96
96
  "13-dingtalk-result-publish.md",
97
97
  "14-orchestration-workflow.md",
98
+ "15-oss-result-publish.md",
98
99
  "99-maintenance.md",
99
100
  )
100
101
 
@@ -401,11 +402,11 @@ class AuditMultiaxisRouterContractTest(unittest.TestCase):
401
402
  f"references/{ORCHESTRATION_REFERENCE} 必须承载运行时细节 {token}",
402
403
  )
403
404
 
404
- # 4) 14 必须按序保留步骤 1–15(执行顺序不得丢步或改序)。
405
+ # Execution order includes OSS publishing before DingTalk delivery.
405
406
  steps = [int(number) for number in re.findall(r"(?m)^(\d+)\.\s", reference)]
406
407
  self.assertEqual(
407
- list(range(1, 16)), steps,
408
- f"references/{ORCHESTRATION_REFERENCE} 必须按序保留 1–15 全部步骤,实际={steps}",
408
+ list(range(1, 17)), steps,
409
+ f"references/{ORCHESTRATION_REFERENCE} 必须按序保留 1–16 全部步骤,实际={steps}",
409
410
  )
410
411
 
411
412
  # 5) 14 必须保留阶段隔离、Python 运行时与 KB 装配边界的稳定标识。