dsh-crwu-workbench 0.0.13 → 0.0.15

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/lib/client.js CHANGED
@@ -2551,10 +2551,36 @@ body[data-ds-dark-theme] .crwu-audit-root {
2551
2551
  envVersionText: "版本",
2552
2552
  envState: "状态",
2553
2553
  envDetail: "详情",
2554
+ envDiagAccess: "最近的本机访问",
2555
+ envDiagAccessNone: "(本次运行还没有本机访问)",
2556
+ envDiagAccessCount: "条(最多列 8 条)",
2557
+ envDiagAccessOk: "ok",
2554
2558
  envProbe: "实测",
2555
2559
  envAuditRoot: "审核根会话",
2556
2560
  envAuditRootNone: "尚未创建(第一次发起审核时自动建在工作空间里)",
2557
2561
  envAuditRootStale: "(已失效,下次发起审核会自动新建一个)",
2562
+ envAuditRootSandbox: "沙箱 ",
2563
+ dwsLocalTitle: "钉钉本机目录",
2564
+ dwsLocalCheck: "检查本机目录",
2565
+ dwsLocalUnchecked: "未检查",
2566
+ dwsLocalNeedsFailure: "体检只回答「刚才那次为什么失败」:请先复现一次(例如再点一次登录或取数),再回来检查",
2567
+ dwsLocalCheckFailed: "检查没有完成",
2568
+ dwsLocalNoDirectory: "还没有在本机登录过(目录不存在)",
2569
+ dwsLocalWritable: "可读写,正常",
2570
+ dwsLocalNotWritable: "当前账户改不了这个目录",
2571
+ dwsLocalWrongOwner: "目录属于别的账户:需要管理员处理",
2572
+ dwsLocalLocked: "文件被别的程序占用:请先关闭正在使用钉钉的程序",
2573
+ /** 目录能写、但 `.data.lock` 自己不可写 —— 原始报错常常正是这一条。 */
2574
+ dwsLocalLockNotWritable: "锁文件(.data.lock)当前账户改不了:其它目录状态正常",
2575
+ dwsLocalKeychain: "系统钥匙串不允许访问:请在弹出的系统提示上允许,或在「钥匙串访问」里检查",
2576
+ dwsLocalNotLoggedIn: "还没有登录(本机没有登录凭据)",
2577
+ dwsLocalUnknown: "没查出确定结论",
2578
+ dwsLocalRepair: "修复权限",
2579
+ dwsLocalRepairConfirm: "这会只修改本机钉钉目录($HOME/.dws)的权限:目录设为 700、锁文件设为 600。不会改动上级目录、不会改变所有者、不会删除锁文件。确定继续吗?",
2580
+ dwsLocalRepairConfirmYes: "确认修复",
2581
+ dwsLocalRepairConfirmNo: "取消",
2582
+ dwsLocalRepaired: "已修复 ",
2583
+ envAuditRootApproval: "审批 ",
2558
2584
  envSessionWorkspace: "当前会话",
2559
2585
  envParentSession: "父会话 id",
2560
2586
  envSessionCwd: "会话 cwd",
@@ -2938,12 +2964,28 @@ body[data-ds-dark-theme] .crwu-audit-root {
2938
2964
  envStepHintWorkspace: "审核产物写到这个目录下的「报告流水号」文件夹里。",
2939
2965
  envStepDoneSummary: "四项配置都已完成。你可以从左侧进入报告审核。",
2940
2966
  envAccountsHint: "这些是你自己的账号:氚云扫码、钉钉登录。密钥类配置在后面的步骤里。",
2941
- envConsentTitle: "一次性授权:允许插件读取本机凭据",
2942
- envConsentDone: "已授权读取本机凭据(一次授权、长期有效)",
2943
- envConsentWhy: "氚云会话与钉钉登录态存在本机凭据存储里,授权后插件才能读它们。未授权时读到的「未登录」不可信,所以插件不会猜。",
2944
- envConsentAgree: "同意并继续",
2945
- envConsentDeclined: "你拒绝了授权。要使用氚云与钉钉相关能力,需要重新授权。",
2946
- envConsentRegrant: "重新授权",
2967
+ envConsentTitle: "允许中瑞世联工作台访问本机账号和配置",
2968
+ envConsentIntro: "工作台需要为下面这些固定功能访问本机账号和配置。这不是「授予插件所有权限」——范围就是这张清单,超出的部分一律访问不到。",
2969
+ envConsentScopeTitle: "这次允许覆盖的功能",
2970
+ envConsentCapH3yun: "读取和更新氚云(H3Yun)系统凭据",
2971
+ envConsentCapDws: "读取和更新 .dws 里的钉钉登录状态",
2972
+ envConsentCapOss: "读写 .ossutilconfig(交付件回传用的 AccessKey)",
2973
+ envConsentCapIfind: "读写 .dsh/crwu-workbench 里的工作台状态和 iFinD API-Key",
2974
+ envConsentCapSystem: "执行插件内置的 crwu / dws / ossutil;登录时打开系统浏览器;写系统剪贴板",
2975
+ envConsentBoundary: "不会把凭据交给 Agent;不会向 Agent 开放任意系统命令;可以随时在工作台撤销;权限范围升级后会重新询问。审核产物只写进你选定的工作空间。",
2976
+ envConsentDone: "已允许工作台访问本机账号和配置(一次允许、长期有效)",
2977
+ envConsentGrantedAt: "允许时间",
2978
+ envConsentAgree: "允许并继续",
2979
+ envConsentDecline: "暂不允许",
2980
+ envConsentBusy: "正在保存授权…",
2981
+ envConsentDeclined: "你选择了暂不允许。账号连接、报告审核与交付回传保持禁用;需要时点下面的「重新允许一次」。",
2982
+ envConsentRegrant: "重新允许一次",
2983
+ envConsentRevoke: "撤销授权",
2984
+ envConsentRevoked: "已撤销:本机账号与配置的访问已关闭,账号连接与审核功能不可用。",
2985
+ envConsentPersistFailed: "撤销没能写入磁盘:本次运行已经关闭访问,重启后请再撤销一次。",
2986
+ envConsentPersistFailedGrant: "授权没能写入磁盘,所以本次没有获得任何本机访问权限:请检查磁盘空间或文件权限后重试。",
2987
+ envConsentSchemaMismatch: "界面与宿主的权限说明版本不一致:请完全退出并重新打开 DeepSeek Harness,再在「账号连接」里按新的范围允许一次。",
2988
+ envConsentOutdated: "授权范围已更新:请按上面的清单重新允许一次。",
2947
2989
  envOssCardTitle: "阿里云 OSS 交付凭据",
2948
2990
  envOssCardHint: "用于上传审核交付件,请向管理员获取 AccessKey ID 和 AccessKey Secret。",
2949
2991
  envOssAkLabel: "AccessKey ID",
@@ -2962,6 +3004,9 @@ body[data-ds-dark-theme] .crwu-audit-root {
2962
3004
  envOssErrPermission: "AccessKey 可以登录,但这个账号没有交付目录的访问权限,请联系管理员开通。",
2963
3005
  envOssErrConfig: "交付目录的配置有问题(Bucket 或 Endpoint),请联系管理员核对。",
2964
3006
  envOssErrInfrastructure: "暂时无法连接 OSS,请稍后重新验证;这不代表 AccessKey 有误。",
3007
+ envCredInheritedWindowsAcl: "使用当前 Windows 账户 ACL;POSIX 0600 不适用。",
3008
+ envCredInherited: "凭据权限由现有机制负责,没有执行额外收紧。",
3009
+ envCredHardeningFailed: "凭据已保存,但权限没有收紧成功,请检查文件权限。",
2965
3010
  envIfindCardTitle: "同花顺 iFinD API-Key",
2966
3011
  envIfindCardHint: "用于查询外部金融数据。API-Key 由插件保存在本机,页面不回显;每次检查都会真的读取一条测试数据。",
2967
3012
  envIfindSecretLabel: "同花顺 iFinD API-Key",
@@ -3060,6 +3105,33 @@ body[data-ds-dark-theme] .crwu-audit-root {
3060
3105
  dwsLogin: "钉钉登录",
3061
3106
  dwsLoginDevice: "设备码登录(浏览器打不开时)",
3062
3107
  stopAudit: "停止这条审核",
3108
+ stopPhaseRequested: "正在请求停止审核…",
3109
+ stopPhaseAborting: "正在取消子会话…",
3110
+ stopPhaseWaiting: "正在确认子会话已完全停止…",
3111
+ stopPhaseStillWaiting: "仍在等待子会话退出,请勿启动新的审核…",
3112
+ stopPhaseQuiesced: "已停止",
3113
+ stopPhaseTimeout: "暂时无法确认子会话已停止",
3114
+ stopPhaseFailed: "停止过程发生错误",
3115
+ stopWaitBudget: "最多等待约 8 秒",
3116
+ stopElapsedPrefix: "已等待 ",
3117
+ stopElapsedSuffix: " 秒",
3118
+ stopNoRestartHint: "在确认停止前,不能启动新的审核。",
3119
+ stopKeptHint: "当前审核仍被保留,未释放占用。",
3120
+ stopActionKeepWaiting: "继续等待",
3121
+ stopActionStopAgain: "再次停止",
3122
+ stopActionCopyDiagnostics: "复制诊断",
3123
+ stopActionOpenSession: "打开当前会话",
3124
+ stopUnknownHint: "宿主没有回报停止状态(旧版本):正在等待下一次状态刷新。",
3125
+ currentAuditTitle: "当前审核",
3126
+ currentAuditChildTail: "会话 ",
3127
+ currentAuditAttempt: "第 ",
3128
+ currentAuditAttemptSuffix: " 次",
3129
+ currentAuditStarted: "开始于 ",
3130
+ openAuditSession: "打开审核会话",
3131
+ openAuditSessionPending: "会话正在建立",
3132
+ openAuditSessionHint: "打开真正执行本次审核的子会话(与「分析审核结果」不是同一个会话)。",
3133
+ copyDiagnosticsDone: "诊断信息已复制",
3134
+ copyDiagnosticsFailed: "复制失败:请从下方诊断文字手动复制。",
3063
3135
  allOk: "环境就绪",
3064
3136
  noRows: "暂无报告",
3065
3137
  noResults: "暂无 AI 审核结果",
@@ -3634,8 +3706,12 @@ body[data-ds-dark-theme] .crwu-audit-root {
3634
3706
  workspace: (args) => call("workspace", args),
3635
3707
  workspaceAuto: () => call("workspace-auto"),
3636
3708
  bindSession: (args) => call("bind-session", args),
3637
- trust: (args) => call("trust", args),
3709
+ localAccessGrant: (args) => call("local-access-grant", args),
3710
+ localAccessRevoke: () => call("local-access-revoke", {}),
3638
3711
  session: () => call("session"),
3712
+ accessDiagnostics: () => call("access-diagnostics"),
3713
+ dwsLocalDoctor: () => call("dws-local-doctor"),
3714
+ dwsLocalPermissionRepair: (args) => call("dws-local-permission-repair", args),
3639
3715
  relogin: () => call("relogin"),
3640
3716
  dwsLogin: (args) => call("dws-login", args),
3641
3717
  clipboard: (args) => call("clipboard", args),
@@ -3903,6 +3979,128 @@ body[data-ds-dark-theme] .crwu-audit-root {
3903
3979
  })] });
3904
3980
  }
3905
3981
  //#endregion
3982
+ //#region src/shared/utils/value.ts
3983
+ /** 将可空外部值转换为稳定字符串。 */
3984
+ function text(value) {
3985
+ return value === void 0 || value === null ? "" : String(value);
3986
+ }
3987
+ //#endregion
3988
+ //#region src/shared/utils/local-path.ts
3989
+ /**
3990
+ * **本地展示路径**的纯函数适配器(不引用 `node:path`)。
3991
+ *
3992
+ * ## 为什么需要它
3993
+ *
3994
+ * Host 与 Client 都要算「案例目录」「文件名」这类**展示路径**,而共享层不能引入 Node 模块
3995
+ * (Client 产物是要进浏览器的)。于是历史实现各处都用 `/` 硬拼,在 Windows 上产生三类错误:
3996
+ *
3997
+ * 1. **拼接**:`C:\Work` + `/` + `S1` 得到 `C:\Work/S1`,进 shell(PowerShell 的 `-Path`)
3998
+ * 与进提示词(模型照着写命令)都是混用分隔符;
3999
+ * 2. **取末段**:`'C:\\Work\\S1'.split('/')` 得到整串,界面上的案例名变成一整条路径;
4000
+ * 3. **裁剪**:`replace(/\/+$/, '')` 对 `C:\Work\` 无效;对 `\\server\share\` 稍有不慎就裁成
4001
+ * 无法解析的 `\\server`。
4002
+ *
4003
+ * ## 边界(改这个文件前先读)
4004
+ *
4005
+ * - 这里处理的是**本地路径**:盘符、UNC、POSIX 根。**OSS object key 永远用 `/`**,
4006
+ * 不属于这里(见 `host/oss/ops.ts` 里 `objectKey` 的命名)。
4007
+ * - **安全性判断不要用这里的函数**:`..`、符号链接、Windows 盘符大小写都会让字符串比较失效,
4008
+ * 父目录/子目录判定一律走 `ctx.fs.contains`(后端 realpath 之后比较)。
4009
+ * 这里只负责「拼一条给人看、给命令用的路径」。
4010
+ * - `joinLocalPath` 对**不受信任的片段**是拒绝而不是清洗:绝对路径、`..` 片段都抛错。
4011
+ * 静默清洗会让调用方以为拿到了一条安全路径(最危险的形态)。
4012
+ */
4013
+ /**
4014
+ * 盘符**风格**:`C:\` / `C:/`,也接受裸 `C:`。
4015
+ *
4016
+ * `C:` 是「盘符相对路径」(这个盘上的当前目录),它**不是**绝对路径 —— 风格与绝对必须分开判,
4017
+ * 见 `isAbsoluteLocalPath`。
4018
+ */
4019
+ const DRIVE_STYLE = /^[A-Za-z]:([\\/]|$)/;
4020
+ /** 盘符**绝对**:盘符后必须有分隔符(`C:\` / `C:/`)。裸 `C:` 不算。 */
4021
+ const DRIVE_ROOTED = /^[A-Za-z]:[\\/]/;
4022
+ /** UNC / 双分隔符开头:`\\server\share` 或 `//server/share`。 */
4023
+ const UNC = /^[\\/]{2}[^\\/]/;
4024
+ /** 这个字符串看起来是 Windows 风格路径(盘符或 UNC)。**不表示它是绝对路径。** */
4025
+ function isWindowsStylePath(value) {
4026
+ const raw = text(value);
4027
+ return DRIVE_STYLE.test(raw) || UNC.test(raw);
4028
+ }
4029
+ /**
4030
+ * 是否是绝对本地路径(POSIX `/…`、Windows 盘符根、UNC)。
4031
+ *
4032
+ * **裸 `C:` 不是绝对路径**(2026-09-28 复查修):早先这里复用了「Windows 风格」的判断,
4033
+ * 于是 `requireCaseDir('C:')` 会把盘符相对路径当绝对路径放行 —— 案例目录随后随进程 cwd 漂移,
4034
+ * 产物落点不可预测。`C:` 与 `C:\` 是完全不同的位置,两条正则不能共用。
4035
+ */
4036
+ function isAbsoluteLocalPath(value) {
4037
+ const raw = text(value);
4038
+ return raw.startsWith("/") || DRIVE_ROOTED.test(raw) || UNC.test(raw);
4039
+ }
4040
+ /**
4041
+ * 去掉尾部路径分隔符,但**保留根**:
4042
+ *
4043
+ * - `'/work/'` → `'/work'`;`'/'` → `'/'`;
4044
+ * - `'C:\\Work\\'` → `'C:\\Work'`;`'C:\\'` → `'C:\\'`(裁成 `'C:'` 会变成「盘符相对路径」);
4045
+ * - `'\\\\server\\share\\'` → `'\\\\server\\share'`;
4046
+ * `'\\\\server\\'` → `'\\\\server\\'`(裁掉尾部分隔符就只剩服务器名,不是一个可解析的路径)。
4047
+ */
4048
+ function trimTrailingSeparators(value) {
4049
+ const raw = text(value);
4050
+ if (raw === "") return "";
4051
+ if (/^\/+$/.test(raw)) return "/";
4052
+ if (/^[A-Za-z]:[\\/]$/.test(raw)) return raw;
4053
+ if (/^[\\/]{2}[^\\/]+[\\/]$/.test(raw)) return raw;
4054
+ let end = raw.length;
4055
+ while (end > 0 && (raw[end - 1] === "/" || raw[end - 1] === "\\")) end -= 1;
4056
+ return end === 0 ? raw : raw.slice(0, end);
4057
+ }
4058
+ /** 该路径风格用哪个分隔符;非 Windows 风格一律 `/`。 */
4059
+ function localSeparator(value) {
4060
+ return isWindowsStylePath(value) ? "\\" : "/";
4061
+ }
4062
+ /** 一个片段是不是 `..`(只看整段,不看 `a..b` 这种文件名)。 */
4063
+ function isParentSegment(segment) {
4064
+ return segment.split(/[\\/]+/).some((part) => part === "..");
4065
+ }
4066
+ /**
4067
+ * 拼接一条本地路径:分隔符随 `root` 的风格。
4068
+ *
4069
+ * - `joinLocalPath('C:\\Work', 'a', 'b')` → `'C:\\Work\\a\\b'`
4070
+ * - `joinLocalPath('/work', 'a', 'b')` → `'/work/a/b'`
4071
+ * - `joinLocalPath('/work/', 'a')` → `'/work/a'`(根上的尾分隔符不产生双斜杠)
4072
+ *
4073
+ * **拒绝**(抛 `RangeError`)而不是清洗:
4074
+ * - 片段是绝对路径(`/x`、`C:\x`、`\\server\x`)—— 那会静默替换掉整条路径;
4075
+ * - 片段含 `..` 整段 —— 那会静默越出调用方以为的目录。
4076
+ *
4077
+ * 空片段(`''`)跳过;片段内部的另一种分隔符会被规范成目标分隔符。
4078
+ *
4079
+ * 裸盘符根(`'C:'`)会被规范成 `'C:\\'` —— 「往 `C:` 后面接一段」在 Windows 上的实际含义
4080
+ * 就是该盘的根,而不是「这个盘上的当前目录」;要判「是不是绝对路径」用 `isAbsoluteLocalPath`,
4081
+ * 那一条对裸 `C:` 明确返回 `false`。
4082
+ */
4083
+ function joinLocalPath(root, ...segments) {
4084
+ const base = trimTrailingSeparators(root);
4085
+ const sep = localSeparator(base);
4086
+ const parts = [];
4087
+ if (base !== "") parts.push(base);
4088
+ for (const segment of segments) {
4089
+ const raw = text(segment);
4090
+ if (raw === "") continue;
4091
+ if (isAbsoluteLocalPath(raw)) throw new RangeError(`joinLocalPath 拒绝绝对路径片段:${raw}`);
4092
+ if (isParentSegment(raw)) throw new RangeError(`joinLocalPath 拒绝含 .. 的片段:${raw}`);
4093
+ const cleaned = raw.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
4094
+ if (cleaned === "") continue;
4095
+ parts.push(sep === "\\" ? cleaned.replace(/\//g, "\\") : cleaned.replace(/\\/g, "/"));
4096
+ }
4097
+ if (parts.length === 0) return "";
4098
+ if (parts.length === 1) return parts[0];
4099
+ const [head, ...tail] = parts;
4100
+ const joint = head !== void 0 && /[\\/]$/.test(head) ? "" : sep;
4101
+ return `${head ?? ""}${joint}${tail.join(sep)}`;
4102
+ }
4103
+ //#endregion
3906
4104
  //#region src/client/features/workbench/open-session.ts
3907
4105
  /** 可尝试的 mode 顺序:记录里的优先,然后是两个已知取值。 */
3908
4106
  function modeCandidates(recorded) {
@@ -4287,9 +4485,12 @@ body[data-ds-dark-theme] .crwu-audit-root {
4287
4485
  canAuto: missing || (workspace?.source ?? "") === "manual"
4288
4486
  };
4289
4487
  }
4488
+ /** 提示文案里的占位段:员工看到的是「<工作空间>\<报告流水号>\」。 */
4489
+ const ARTIFACT_HINT_PLACEHOLDER = "<报告流水号>";
4290
4490
  /** 审核产物的落盘位置说明(用户最常问的问题:东西写到哪了)。 */
4291
4491
  function artifactHint(path) {
4292
- return `${zhCN.wsArtifactPrefix}${path}/<报告流水号>/。${zhCN.wsCwdNote}`;
4492
+ const sep = localSeparator(path);
4493
+ return `${zhCN.wsArtifactPrefix}${joinLocalPath(path, ARTIFACT_HINT_PLACEHOLDER)}${sep}。${zhCN.wsCwdNote}`;
4293
4494
  }
4294
4495
  //#endregion
4295
4496
  //#region src/client/features/workbench/WorkspaceCard.tsx
@@ -4491,6 +4692,49 @@ body[data-ds-dark-theme] .crwu-audit-root {
4491
4692
  });
4492
4693
  }
4493
4694
  //#endregion
4695
+ //#region src/client/features/environment/credential-permission.ts
4696
+ /**
4697
+ * 把 Host 的结构化权限结论翻成**一句给员工看的话**(协议 17)。
4698
+ *
4699
+ * 三种结局各自对应不同的动作,混在一起说就会误导:
4700
+ *
4701
+ * - `verified`:真的收紧了(POSIX 0600 已生效)→ 不需要说什么;
4702
+ * - `inherited`:**没有**执行收紧命令(Windows 上 POSIX 0600 不适用,由当前账户 ACL 负责)
4703
+ * → 必须说清楚,否则员工会以为这份文件被 0600 保护着;
4704
+ * - `failed`:试图收紧但失败(chmod 报错 / 被沙箱拒绝)→ 这是安全边界没守住,逐字回原因。
4705
+ *
4706
+ * 界面**不得**把 `inherited` 显示成成功,也不得把「不适用」显示成失败。
4707
+ *
4708
+ * `onlyFailures: true` 用在「本来就有更重要的提示」的分支(例如 OSS 连通性失败):
4709
+ * 那种时候再叠一句 ACL 说明只会把真正要处理的问题挤掉。
4710
+ */
4711
+ function credentialPermissionHint(raw, options = {}) {
4712
+ const permission = asPermission(raw);
4713
+ if (permission === null) return "";
4714
+ if (permission.status === "failed") return permission.message !== "" ? permission.message : zhCN.envCredHardeningFailed;
4715
+ if (options.onlyFailures === true) return "";
4716
+ if (permission.status === "inherited") return permission.mechanism === "windows-acl" ? zhCN.envCredInheritedWindowsAcl : permission.message !== "" ? permission.message : zhCN.envCredInherited;
4717
+ return "";
4718
+ }
4719
+ /** 边界收窄:Host 应答是外部输入,字段缺失或形状不对时按「没有结论」处理。 */
4720
+ function asPermission(raw) {
4721
+ if (raw === null || typeof raw !== "object") return null;
4722
+ const record = raw;
4723
+ const status = record.status;
4724
+ if (status !== "verified" && status !== "inherited" && status !== "failed") return null;
4725
+ const mechanism = record.mechanism;
4726
+ if (mechanism !== "posix-0600" && mechanism !== "windows-acl" && mechanism !== "host-store") return {
4727
+ status,
4728
+ mechanism: "posix-0600",
4729
+ message: typeof record.message === "string" ? record.message : ""
4730
+ };
4731
+ return {
4732
+ status,
4733
+ mechanism,
4734
+ message: typeof record.message === "string" ? record.message : ""
4735
+ };
4736
+ }
4737
+ //#endregion
4494
4738
  //#region src/client/features/environment/IfindAuthCard.tsx
4495
4739
  /** Host 回的状态 → 中文标签。**认不出的状态按未配置处理**(不假装已认证)。 */
4496
4740
  function ifindStateLabel(state) {
@@ -4598,8 +4842,9 @@ body[data-ds-dark-theme] .crwu-audit-root {
4598
4842
  return;
4599
4843
  }
4600
4844
  setReplacing(false);
4601
- if (result.chmodOk === false) setHint(result.chmodError);
4602
4845
  showProbe(result.probe);
4846
+ const permissionHint = credentialPermissionHint(result.permission);
4847
+ if (permissionHint !== "") setHint(permissionHint);
4603
4848
  }).catch((cause) => {
4604
4849
  if (mounted.current) {
4605
4850
  setError(cause instanceof Error ? cause.message : String(cause));
@@ -4798,6 +5043,289 @@ body[data-ds-dark-theme] .crwu-audit-root {
4798
5043
  });
4799
5044
  }
4800
5045
  //#endregion
5046
+ //#region src/client/features/environment/DwsLocalCard.tsx
5047
+ /**
5048
+ * 「钉钉本机目录」体检与最小权限修复(协议 18 · 子项目 D)。
5049
+ *
5050
+ * ## 为什么按钮是**条件渲染**的
5051
+ *
5052
+ * 设计 §D3 明确列出:沙箱拒绝、沙箱降级、凭据存储被拒、认证失败、所有者不对、文件锁 ——
5053
+ * 这些情形下**不渲染**修复按钮。理由不是"藏起来好看",而是这几类问题改文件权限**解决不了**,
5054
+ * 渲染一个按下去必然失败(或被服务端拒绝)的按钮,只会让员工以为是权限问题、反复点。
5055
+ * 服务端仍然自己判一次(客户端可以伪造请求),界面这一层只是不给假希望。
5056
+ *
5057
+ * ## 为什么修复要**二次确认**
5058
+ *
5059
+ * 它写的是员工机器上的目录权限。第一次确认(本机访问授权)说的是"允许读本机账号和配置",
5060
+ * 不等于"允许改我的目录权限" —— 两件事必须分开问。
5061
+ */
5062
+ /** 修复按钮的渲染条件:**只有**确诊"本机文件权限问题"才出现。 */
5063
+ function repairOffered(doctor) {
5064
+ if (doctor === null || !doctor.ok) return false;
5065
+ return doctor.classification === "os-filesystem-permission";
5066
+ }
5067
+ /**
5068
+ * 员工能看懂的一句话结论(技术细节留给开发者诊断)。
5069
+ *
5070
+ * **顺序就是优先级**:先报"员工必须先处理、且与可写性无关"的那几类(所有者不对、锁被占用、
5071
+ * 钥匙串被拒),最后才轮到"目录可写"这个好消息。
5072
+ * 反过来的话,"锁被占用 + 目录恰好可写"会被说成「可读写,正常」—— 那是把真问题盖掉
5073
+ * (2026-09-29 由 `client-dws-local.test.mjs` 的排序用例抓到)。
5074
+ */
5075
+ function dwsLocalSummary(doctor) {
5076
+ if (!doctor.ok) return doctor.error === "" ? zhCN.dwsLocalCheckFailed : doctor.error;
5077
+ if (!doctor.directoryExists) return zhCN.dwsLocalNoDirectory;
5078
+ if (doctor.ownerMatchesCurrentUser === false) return zhCN.dwsLocalWrongOwner;
5079
+ if (doctor.classification === "file-lock") return zhCN.dwsLocalLocked;
5080
+ if (doctor.credentialStoreState === "interaction-denied" || doctor.credentialStoreState === "access-denied") return zhCN.dwsLocalKeychain;
5081
+ if (doctor.lockWritable === false) return zhCN.dwsLocalLockNotWritable;
5082
+ if (doctor.currentUserCanModify === true) return zhCN.dwsLocalWritable;
5083
+ if (doctor.currentUserCanModify === false) return zhCN.dwsLocalNotWritable;
5084
+ if (doctor.credentialStoreState === "missing-secret") return zhCN.dwsLocalNotLoggedIn;
5085
+ return zhCN.dwsLocalUnknown;
5086
+ }
5087
+ /** 修复结果的员工口径。 */
5088
+ function repairSummary(result) {
5089
+ if (!result.ok) return result.error;
5090
+ const done = result.repaired.length;
5091
+ const skipped = result.skipped.length === 0 ? "" : `(${result.skipped.length} 项没做成)`;
5092
+ return `${zhCN.dwsLocalRepaired}${String(done)}${skipped}`;
5093
+ }
5094
+ function DwsLocalCard(props) {
5095
+ const { doctor } = props;
5096
+ const offered = repairOffered(doctor);
5097
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5098
+ className: WORKBENCH_CLASSES.item,
5099
+ "data-crwu-dws-local": "1",
5100
+ children: [
5101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5102
+ className: WORKBENCH_CLASSES.itemHead,
5103
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5104
+ className: WORKBENCH_CLASSES.itemName,
5105
+ children: zhCN.dwsLocalTitle
5106
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
5107
+ text: doctor === null ? zhCN.dwsLocalUnchecked : dwsLocalSummary(doctor),
5108
+ tone: doctor === null ? "idle" : offered ? "bad" : "ok"
5109
+ })]
5110
+ }),
5111
+ props.error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5112
+ className: WORKBENCH_CLASSES.itemFix,
5113
+ children: props.error
5114
+ }),
5115
+ doctor === null && props.error === "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5116
+ className: WORKBENCH_CLASSES.itemNote,
5117
+ "data-crwu-dws-local-note": "1",
5118
+ "data-crwu-dws-local-disabled": props.canDiagnose === false ? "1" : "0",
5119
+ children: zhCN.dwsLocalNeedsFailure
5120
+ }) : null,
5121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5122
+ className: WORKBENCH_CLASSES.layerActions,
5123
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5124
+ label: zhCN.dwsLocalCheck,
5125
+ small: true,
5126
+ disabled: props.busy || props.canDiagnose === false,
5127
+ onClick: props.onCheck
5128
+ }), offered && !props.confirming ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5129
+ label: zhCN.dwsLocalRepair,
5130
+ small: true,
5131
+ disabled: props.busy,
5132
+ onClick: props.onAskRepair
5133
+ }) : null]
5134
+ }),
5135
+ offered && props.confirming ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5136
+ className: WORKBENCH_CLASSES.itemFix,
5137
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: zhCN.dwsLocalRepairConfirm }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5138
+ className: WORKBENCH_CLASSES.layerActions,
5139
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5140
+ label: zhCN.dwsLocalRepairConfirmYes,
5141
+ small: true,
5142
+ disabled: props.busy,
5143
+ onClick: props.onConfirmRepair
5144
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5145
+ label: zhCN.dwsLocalRepairConfirmNo,
5146
+ small: true,
5147
+ disabled: props.busy,
5148
+ onClick: props.onCancelRepair
5149
+ })]
5150
+ })]
5151
+ }) : null,
5152
+ props.repair === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5153
+ className: WORKBENCH_CLASSES.itemFix,
5154
+ children: repairSummary(props.repair)
5155
+ })
5156
+ ]
5157
+ });
5158
+ }
5159
+ //#endregion
5160
+ //#region src/shared/access/types.ts
5161
+ /**
5162
+ * 工作台需要访问本机的**固定能力清单**(顺序即界面与收据的规范顺序)。
5163
+ *
5164
+ * 这五项对应「员工看得见的功能」,不是内部实现分类:
5165
+ * - `h3yun-credential-store`:操作系统凭据存储里的氚云会话;
5166
+ * - `dws-profile`:`%USERPROFILE%\.dws`(钉钉登录态与 DWS 状态目录);
5167
+ * - `oss-config`:`%USERPROFILE%\.ossutilconfig`(交付件回传的 AK/SK);
5168
+ * - `ifind-credential`:`.dsh/crwu-workbench` 下的 iFinD API-Key 与工作台状态;
5169
+ * - `system-integration`:打开系统浏览器(登录)、写系统剪贴板、在文件管理器里定位案例目录。
5170
+ */
5171
+ const LOCAL_ACCESS_CAPABILITIES = [
5172
+ "h3yun-credential-store",
5173
+ "dws-profile",
5174
+ "oss-config",
5175
+ "ifind-credential",
5176
+ "system-integration"
5177
+ ];
5178
+ //#endregion
5179
+ //#region src/client/features/environment/local-access.ts
5180
+ /**
5181
+ * 本机访问授权的**客户端纯逻辑**。
5182
+ *
5183
+ * 判据只有一份、而且来自 Host(`env.localAccess` 与 `boot.localAccess`):
5184
+ * 客户端不许自己从「有没有凭据 / 是不是登录过」倒推授权状态 —— 那正是"未授权时报未登录"
5185
+ * 那类假结论的来源。
5186
+ *
5187
+ * 这一层只做三件事:取视图、判「能不能放行」、判「要不要立刻把员工送到授权卡上」。
5188
+ */
5189
+ /** 还没拿到 Host 结论时的空视图:按**未授权**处理(fail closed)。 */
5190
+ const MISSING_LOCAL_ACCESS = {
5191
+ state: "missing",
5192
+ schemaVersion: 0,
5193
+ requiredSchemaVersion: 1,
5194
+ grantedAt: "",
5195
+ capabilities: [],
5196
+ reason: ""
5197
+ };
5198
+ /** 取环境应答里的授权视图;旧宿主 / 还没自检回来时按未授权处理。 */
5199
+ function consentOf(env) {
5200
+ const view = env?.localAccess;
5201
+ return view === void 0 || view === null ? MISSING_LOCAL_ACCESS : view;
5202
+ }
5203
+ /** 唯一放行判据。 */
5204
+ function consentGranted(env) {
5205
+ return consentOf(env).state === "granted";
5206
+ }
5207
+ /** 授权卡上的固定能力清单(顺序与收据一致,界面不许自己重排)。 */
5208
+ function consentCapabilities() {
5209
+ return LOCAL_ACCESS_CAPABILITIES;
5210
+ }
5211
+ /** 这次提交的能力清单(规范顺序,逐字来自共享常量)。 */
5212
+ function consentRequestCapabilities() {
5213
+ return [...LOCAL_ACCESS_CAPABILITIES];
5214
+ }
5215
+ //#endregion
5216
+ //#region src/client/features/environment/LocalAccessConsentCard.tsx
5217
+ /** capability → 界面文案;缺一项就是漏了一条要展示的授权范围,测试会盯着这里的覆盖。 */
5218
+ function capabilityLabel(capability) {
5219
+ if (capability === "h3yun-credential-store") return zhCN.envConsentCapH3yun;
5220
+ if (capability === "dws-profile") return zhCN.envConsentCapDws;
5221
+ if (capability === "oss-config") return zhCN.envConsentCapOss;
5222
+ if (capability === "ifind-credential") return zhCN.envConsentCapIfind;
5223
+ return zhCN.envConsentCapSystem;
5224
+ }
5225
+ /** 授权态的短标签(Chip 用)。 */
5226
+ function consentChipText(consent) {
5227
+ if (consent.state === "granted") return zhCN.envItemOk;
5228
+ if (consent.state === "revoked") return zhCN.envConsentRevoke;
5229
+ return zhCN.envItemMissing;
5230
+ }
5231
+ function consentNote(consent) {
5232
+ if (consent.state === "granted") return zhCN.envConsentDone;
5233
+ if (consent.state === "revoked") return zhCN.envConsentRevoked;
5234
+ if (consent.state === "persist-failed") return zhCN.envConsentPersistFailed;
5235
+ if (consent.state === "outdated") return consent.reason || zhCN.envConsentOutdated;
5236
+ return zhCN.envConsentIntro;
5237
+ }
5238
+ function LocalAccessConsentCard(props) {
5239
+ const { consent } = props;
5240
+ const granted = consent.state === "granted";
5241
+ const blocked = props.schemaMismatch === true;
5242
+ const agreeLabel = props.declined ? zhCN.envConsentRegrant : zhCN.envConsentAgree;
5243
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5244
+ className: WORKBENCH_CLASSES.item,
5245
+ "data-crwu-env-item": "consent",
5246
+ "data-crwu-consent-state": consent.state,
5247
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusDot, { tone: granted ? "ok" : "bad" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5248
+ className: WORKBENCH_CLASSES.itemMain,
5249
+ children: [
5250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5251
+ className: WORKBENCH_CLASSES.itemHead,
5252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5253
+ className: WORKBENCH_CLASSES.itemName,
5254
+ children: zhCN.envConsentTitle
5255
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
5256
+ text: consentChipText(consent),
5257
+ tone: granted ? "ok" : "bad"
5258
+ })]
5259
+ }),
5260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5261
+ className: WORKBENCH_CLASSES.itemNote,
5262
+ "data-crwu-consent-note": "1",
5263
+ children: consentNote(consent)
5264
+ }),
5265
+ granted ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5266
+ "data-crwu-consent-scope": "1",
5267
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5268
+ className: WORKBENCH_CLASSES.itemNote,
5269
+ children: zhCN.envConsentScopeTitle
5270
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5271
+ className: WORKBENCH_CLASSES.muted,
5272
+ children: consentCapabilities().map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capabilityLabel(capability) }, capability))
5273
+ })]
5274
+ }),
5275
+ granted ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5276
+ className: WORKBENCH_CLASSES.itemNote,
5277
+ children: [
5278
+ zhCN.envConsentGrantedAt,
5279
+ ":",
5280
+ consent.grantedAt
5281
+ ]
5282
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5283
+ className: WORKBENCH_CLASSES.itemNote,
5284
+ children: zhCN.envConsentBoundary
5285
+ }),
5286
+ props.error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
5287
+ tone: "warn",
5288
+ children: props.error
5289
+ }),
5290
+ !granted && props.declined ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
5291
+ tone: "warn",
5292
+ children: zhCN.envConsentDeclined
5293
+ }) : null,
5294
+ blocked ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
5295
+ tone: "warn",
5296
+ children: zhCN.envConsentSchemaMismatch
5297
+ }) : null,
5298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5299
+ className: WORKBENCH_CLASSES.row,
5300
+ children: [
5301
+ granted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5302
+ label: zhCN.envConsentRevoke,
5303
+ small: true,
5304
+ disabled: props.busy || blocked,
5305
+ onClick: props.onRevoke
5306
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5307
+ label: agreeLabel,
5308
+ tone: "primary",
5309
+ disabled: props.busy || blocked,
5310
+ onClick: props.onGrant
5311
+ }),
5312
+ !granted && !props.declined ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5313
+ label: zhCN.envConsentDecline,
5314
+ small: true,
5315
+ disabled: props.busy,
5316
+ onClick: props.onDecline
5317
+ }) : null,
5318
+ props.busy ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5319
+ className: WORKBENCH_CLASSES.muted,
5320
+ children: zhCN.envConsentBusy
5321
+ }) : null
5322
+ ]
5323
+ })
5324
+ ]
5325
+ })]
5326
+ });
5327
+ }
5328
+ //#endregion
4801
5329
  //#region src/client/features/environment/OssCredCard.tsx
4802
5330
  /** 失败归因 → 员工该做什么(四类文案互不相同)。 */
4803
5331
  function ossErrorHint(errorKind) {
@@ -4843,11 +5371,13 @@ body[data-ds-dark-theme] .crwu-audit-root {
4843
5371
  if (result.ok !== true) {
4844
5372
  setError(String(result.error ?? outcome?.detail ?? zhCN.envOssNotReady));
4845
5373
  setHint(ossErrorHint(String(outcome?.errorKind ?? result.errorKind ?? "")));
4846
- if (result.chmodOk === false) setHint(String(result.chmodError ?? ""));
5374
+ const failed = credentialPermissionHint(result.permission, { onlyFailures: true });
5375
+ if (failed !== "") setHint(failed);
4847
5376
  return;
4848
5377
  }
4849
5378
  setReplacing(false);
4850
- if (result.chmodOk === false) setHint(String(result.chmodError ?? ""));
5379
+ const permissionHint = credentialPermissionHint(result.permission);
5380
+ if (permissionHint !== "") setHint(permissionHint);
4851
5381
  if (outcome?.ok === true) {
4852
5382
  setMessage(zhCN.envOssSaved);
4853
5383
  props.onSaved();
@@ -5032,7 +5562,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
5032
5562
  function setupStepInput(env, authorized) {
5033
5563
  const setup = env.state?.userSetup;
5034
5564
  return {
5035
- authorized: authorized ?? env.trust.credentials,
5565
+ authorized: authorized ?? consentGranted(env),
5036
5566
  h3yun: setup?.h3yun ?? MISSING,
5037
5567
  dingtalk: setup?.dingtalk ?? MISSING,
5038
5568
  oss: setup?.aliyunOss ?? MISSING,
@@ -5158,7 +5688,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
5158
5688
  * 开发者诊断的单一事实源:页面与复制文本都从这些行生成,避免二者遗漏不同字段。
5159
5689
  * 这里只接收 Host 已脱敏的视图,绝不放入 API-Key / AccessKey 明文。
5160
5690
  */
5161
- function diagnosticRows(env, checkedAt, build) {
5691
+ function diagnosticRows(env, checkedAt, build, access = []) {
5162
5692
  const health = env.state?.systemHealth;
5163
5693
  const integrity = env.packageIntegrity;
5164
5694
  const runtime = env.runtime;
@@ -5181,6 +5711,8 @@ body[data-ds-dark-theme] .crwu-audit-root {
5181
5711
  root.title || root.sessionId,
5182
5712
  `${shortSessionId(root.sessionId)}…`,
5183
5713
  root.workspacePath,
5714
+ root.sandboxMode === "" ? "" : `${zhCN.envAuditRootSandbox}${root.sandboxMode}`,
5715
+ root.approvalPolicy === "" ? "" : `${zhCN.envAuditRootApproval}${root.approvalPolicy}`,
5184
5716
  root.usable ? "" : `${zhCN.envAuditRootStale}${root.reason === "" ? "" : `(${root.reason})`}`
5185
5717
  ].filter((item) => item !== "").join(" · ");
5186
5718
  return [
@@ -5286,9 +5818,35 @@ body[data-ds-dark-theme] .crwu-audit-root {
5286
5818
  {
5287
5819
  label: zhCN.envProbe,
5288
5820
  value: env.delivery.probe.state === "" ? env.delivery.probe.ok ? zhCN.envPass : zhCN.envFail : env.delivery.probe.state
5821
+ },
5822
+ {
5823
+ label: zhCN.envDiagAccess,
5824
+ value: access.length === 0 ? zhCN.envDiagAccessNone : `${String(access.length)} ${zhCN.envDiagAccessCount}`,
5825
+ details: access.slice(0, 8).map(accessDiagnosticLine)
5289
5826
  }
5290
5827
  ];
5291
5828
  }
5829
+ /**
5830
+ * 一条本机访问诊断 → **一行**可抄进验收记录的脱敏事实。
5831
+ *
5832
+ * 字段顺序就是设计 §11 要求的证据顺序(操作名 / 来源 / 三个模式 / 两个布尔 / 归因 /
5833
+ * 进程是否起过 / 时刻)。**不**包含命令原文、路径与任何凭据 —— 那些在诊断里根本不存在。
5834
+ */
5835
+ function accessDiagnosticLine(entry) {
5836
+ const verdict = entry.errorClass === "" ? zhCN.envDiagAccessOk : entry.errorClass;
5837
+ return [
5838
+ entry.operation,
5839
+ entry.source,
5840
+ `req=${entry.requestedMode === "" ? zhCN.envUnset : entry.requestedMode}`,
5841
+ `res=${entry.resolvedMode === "" ? zhCN.envUnset : entry.resolvedMode}`,
5842
+ `ran=${entry.ranMode === "" ? zhCN.envUnset : entry.ranMode}`,
5843
+ `denied=${String(entry.sandboxDenied)}`,
5844
+ `runnerFailed=${String(entry.runnerFailed)}`,
5845
+ `started=${String(entry.processStarted)}`,
5846
+ verdict,
5847
+ localTime(entry.at)
5848
+ ].join(" · ");
5849
+ }
5292
5850
  function diagnosticText(rows) {
5293
5851
  return rows.map((row) => [`${row.label}: ${row.value}`, ...(row.details ?? []).map((detail) => ` ${detail}`)].join("\n")).join("\n");
5294
5852
  }
@@ -5471,62 +6029,29 @@ body[data-ds-dark-theme] .crwu-audit-root {
5471
6029
  /** 账号连接步骤:一次性授权 + 氚云 + 钉钉。 */
5472
6030
  function AccountsStep(props) {
5473
6031
  const authorized = props.authorized;
6032
+ /** 「暂不允许」之后点「重新允许一次」只是把本地状态翻回去,真正的同意仍是同一次提交。 */
6033
+ const grant = () => {
6034
+ props.onRegrant?.();
6035
+ props.onGrantCredentials?.();
6036
+ };
5474
6037
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
5475
6038
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5476
6039
  className: WORKBENCH_CLASSES.stepLead,
5477
6040
  children: zhCN.envStepHintAccounts
5478
6041
  }),
5479
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5480
- className: WORKBENCH_CLASSES.item,
5481
- "data-crwu-env-item": "consent",
5482
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatusDot, { tone: authorized ? "ok" : "bad" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5483
- className: WORKBENCH_CLASSES.itemMain,
5484
- children: [
5485
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5486
- className: WORKBENCH_CLASSES.itemHead,
5487
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5488
- className: WORKBENCH_CLASSES.itemName,
5489
- children: zhCN.envConsentTitle
5490
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chip, {
5491
- text: authorized ? zhCN.envItemOk : zhCN.envItemMissing,
5492
- tone: authorized ? "ok" : "bad"
5493
- })]
5494
- }),
5495
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5496
- className: WORKBENCH_CLASSES.itemNote,
5497
- children: zhCN.envConsentWhy
5498
- }),
5499
- props.authError === void 0 || props.authError === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
5500
- tone: "warn",
5501
- children: props.authError
5502
- }),
5503
- !authorized && props.authDeclined === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
5504
- tone: "warn",
5505
- children: zhCN.envConsentDeclined
5506
- }) : null,
5507
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5508
- className: WORKBENCH_CLASSES.row,
5509
- children: authorized ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5510
- className: WORKBENCH_CLASSES.authGranted,
5511
- children: zhCN.envConsentDone
5512
- }) : props.authDeclined === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5513
- label: zhCN.envConsentRegrant,
5514
- small: true,
5515
- disabled: props.authBusy === true,
5516
- onClick: () => {
5517
- props.onRegrant?.();
5518
- }
5519
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5520
- label: zhCN.envConsentAgree,
5521
- tone: "primary",
5522
- disabled: props.authBusy === true,
5523
- onClick: () => {
5524
- props.onGrantCredentials?.();
5525
- }
5526
- })
5527
- })
5528
- ]
5529
- })]
6042
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LocalAccessConsentCard, {
6043
+ consent: consentOf(props.env),
6044
+ busy: props.authBusy === true,
6045
+ error: props.authError ?? "",
6046
+ declined: props.authDeclined === true,
6047
+ ...props.authSchemaMismatch === true ? { schemaMismatch: true } : {},
6048
+ onGrant: grant,
6049
+ onDecline: () => {
6050
+ props.onDeclineCredentials?.();
6051
+ },
6052
+ onRevoke: () => {
6053
+ props.onRevokeCredentials?.();
6054
+ }
5530
6055
  }),
5531
6056
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SetupRow, {
5532
6057
  id: props.env.services.find((service) => service.id === "h3yun")?.label ?? "氚云(H3Yun)员工会话",
@@ -5541,6 +6066,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
5541
6066
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5542
6067
  label: zhCN.envLoginH3yun,
5543
6068
  small: true,
6069
+ disabled: !authorized,
5544
6070
  onClick: props.onRelogin
5545
6071
  })
5546
6072
  })
@@ -5558,10 +6084,12 @@ body[data-ds-dark-theme] .crwu-audit-root {
5558
6084
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5559
6085
  label: zhCN.dwsLogin,
5560
6086
  small: true,
6087
+ disabled: !authorized,
5561
6088
  onClick: props.onDwsLogin
5562
6089
  }), props.onDwsLoginDevice === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
5563
6090
  label: zhCN.dwsLoginDevice,
5564
6091
  small: true,
6092
+ disabled: !authorized,
5565
6093
  onClick: props.onDwsLoginDevice
5566
6094
  })]
5567
6095
  })
@@ -5570,6 +6098,26 @@ body[data-ds-dark-theme] .crwu-audit-root {
5570
6098
  className: WORKBENCH_CLASSES.itemFix,
5571
6099
  style: { whiteSpace: "pre-wrap" },
5572
6100
  children: props.loginMessage
6101
+ }),
6102
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DwsLocalCard, {
6103
+ doctor: props.dwsLocal ?? null,
6104
+ canDiagnose: props.canDiagnoseDws,
6105
+ repair: props.dwsLocalRepair ?? null,
6106
+ busy: props.dwsLocalBusy === true,
6107
+ confirming: props.dwsLocalConfirming === true,
6108
+ error: props.dwsLocalError ?? "",
6109
+ onCheck: () => {
6110
+ props.onDwsLocalCheck?.();
6111
+ },
6112
+ onAskRepair: () => {
6113
+ props.onDwsLocalAskRepair?.();
6114
+ },
6115
+ onCancelRepair: () => {
6116
+ props.onDwsLocalCancelRepair?.();
6117
+ },
6118
+ onConfirmRepair: () => {
6119
+ props.onDwsLocalConfirmRepair?.();
6120
+ }
5573
6121
  })
5574
6122
  ] });
5575
6123
  }
@@ -5588,8 +6136,9 @@ body[data-ds-dark-theme] .crwu-audit-root {
5588
6136
  buildKind: "",
5589
6137
  builtAt: "",
5590
6138
  protocol: null,
6139
+ permissionSchemaVersion: null,
5591
6140
  parentSessionId: ""
5592
- });
6141
+ }, props.access);
5593
6142
  const [copied, setCopied] = react.useState(false);
5594
6143
  const [copyError, setCopyError] = react.useState("");
5595
6144
  const copy = () => {
@@ -5674,6 +6223,35 @@ body[data-ds-dark-theme] .crwu-audit-root {
5674
6223
  function EnvironmentPane(props) {
5675
6224
  const { env } = props;
5676
6225
  const [developerDiagnosticsOpen, setDeveloperDiagnosticsOpen] = react.useState(false);
6226
+ const [accessDiagnostics, setAccessDiagnostics] = react.useState([]);
6227
+ /**
6228
+ * 「检查本机目录」能不能点 —— **由 Host 给的**事实(`dwsDiagnosable`)。
6229
+ *
6230
+ * 挂载时问一次:它是一次零副作用的只读 RPC(同一份环形缓冲),但决定了按钮可不可点,
6231
+ * 所以不能等用户点下去才知道。`undefined` = 还没问到 → 保持可点(Host 会是那道真门禁)。
6232
+ */
6233
+ const [canDiagnoseDws, setCanDiagnoseDws] = react.useState(void 0);
6234
+ react.useEffect(() => {
6235
+ let alive = true;
6236
+ workbenchApi.accessDiagnostics().then((result) => {
6237
+ if (alive) setCanDiagnoseDws(result.ok ? result.dwsDiagnosable : void 0);
6238
+ }).catch(() => {
6239
+ if (alive) setCanDiagnoseDws(void 0);
6240
+ });
6241
+ return () => {
6242
+ alive = false;
6243
+ };
6244
+ }, []);
6245
+ const toggleDiagnostics = () => {
6246
+ const next = !developerDiagnosticsOpen;
6247
+ setDeveloperDiagnosticsOpen(next);
6248
+ if (!next) return;
6249
+ workbenchApi.accessDiagnostics().then((result) => {
6250
+ setAccessDiagnostics(result.ok ? result.entries : []);
6251
+ }).catch(() => {
6252
+ setAccessDiagnostics([]);
6253
+ });
6254
+ };
5677
6255
  /**
5678
6256
  * 用户手动选过的步骤。
5679
6257
  *
@@ -5699,7 +6277,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
5699
6277
  }
5700
6278
  const gate = props.gate ?? "idle";
5701
6279
  const setup = env.state?.userSetup;
5702
- const authorized = props.authorized ?? env.trust.credentials;
6280
+ const authorized = props.authorized ?? consentGranted(env);
5703
6281
  const activeStep = pickStep(setupSteps(setupStepInput(env, authorized), manualStep), manualStep);
5704
6282
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
5705
6283
  props.error === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
@@ -5726,6 +6304,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
5726
6304
  ...props,
5727
6305
  env,
5728
6306
  authorized,
6307
+ canDiagnoseDws,
5729
6308
  activeStep,
5730
6309
  onPick: setManualStep
5731
6310
  }),
@@ -5734,9 +6313,8 @@ body[data-ds-dark-theme] .crwu-audit-root {
5734
6313
  build: props.build,
5735
6314
  checkedAt: props.checkedAt ?? "",
5736
6315
  open: developerDiagnosticsOpen,
5737
- onToggle: () => {
5738
- setDeveloperDiagnosticsOpen(!developerDiagnosticsOpen);
5739
- }
6316
+ access: accessDiagnostics,
6317
+ onToggle: toggleDiagnostics
5740
6318
  })
5741
6319
  ]
5742
6320
  })
@@ -5770,7 +6348,8 @@ body[data-ds-dark-theme] .crwu-audit-root {
5770
6348
  active === "accounts" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountsStep, {
5771
6349
  ...props,
5772
6350
  env,
5773
- authorized: props.authorized
6351
+ authorized: props.authorized,
6352
+ canDiagnoseDws: props.canDiagnoseDws
5774
6353
  }) : null,
5775
6354
  active === "oss" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OssCredCard, {
5776
6355
  delivery: env.delivery,
@@ -6744,6 +7323,55 @@ body[data-ds-dark-theme] .crwu-audit-root {
6744
7323
  });
6745
7324
  }
6746
7325
  //#endregion
7326
+ //#region src/client/features/report-audit/stop-view.ts
7327
+ const PHASE_LABEL = {
7328
+ idle: "",
7329
+ requested: zhCN.stopPhaseRequested,
7330
+ aborting: zhCN.stopPhaseAborting,
7331
+ "waiting-quiescence": zhCN.stopPhaseWaiting,
7332
+ quiesced: zhCN.stopPhaseQuiesced,
7333
+ timeout: zhCN.stopPhaseTimeout,
7334
+ failed: zhCN.stopPhaseFailed
7335
+ };
7336
+ const BUSY_PHASES = [
7337
+ "requested",
7338
+ "aborting",
7339
+ "waiting-quiescence"
7340
+ ];
7341
+ /** 等待超过这个秒数就把文案换成"仍在等待…,请勿启动新的审核"。 */
7342
+ const STILL_WAITING_AFTER_MS = 3e3;
7343
+ function stopPresentationOf(input) {
7344
+ const stop = input.stop;
7345
+ const known = stop !== void 0;
7346
+ const localRequested = typeof input.requestedAt === "number" ? input.requestedAt : 0;
7347
+ const requestedAt = Math.max(stop?.requestedAt ?? 0, localRequested);
7348
+ const elapsedMs = requestedAt === 0 ? 0 : Math.max(stop?.elapsedMs ?? 0, input.now - requestedAt);
7349
+ const elapsedSeconds = Math.floor(elapsedMs / 1e3);
7350
+ const rawPhase = known ? stop.phase ?? "idle" : "idle";
7351
+ const phase = rawPhase === "idle" && localRequested !== 0 ? "requested" : rawPhase;
7352
+ const busy = BUSY_PHASES.includes(phase);
7353
+ const label = busy && elapsedMs >= STILL_WAITING_AFTER_MS && phase === "waiting-quiescence" ? zhCN.stopPhaseStillWaiting : PHASE_LABEL[phase];
7354
+ return {
7355
+ phase,
7356
+ known,
7357
+ tone: phase === "quiesced" ? "success" : phase === "timeout" || phase === "failed" ? "error" : busy ? "info" : "warn",
7358
+ label,
7359
+ hint: phase === "timeout" || phase === "failed" ? `${stop?.error === void 0 || stop.error === "" ? "" : `${stop.error} `}${zhCN.stopNoRestartHint}${zhCN.stopKeptHint}` : busy ? zhCN.stopNoRestartHint : known ? "" : zhCN.stopUnknownHint,
7360
+ busy,
7361
+ stopDisabled: busy,
7362
+ canStartNext: stop?.canStartNext === true,
7363
+ elapsedText: busy && elapsedSeconds > 0 ? `${zhCN.stopElapsedPrefix}${elapsedSeconds}${zhCN.stopElapsedSuffix}` : "",
7364
+ waitHint: busy ? zhCN.stopWaitBudget : "",
7365
+ notes: Array.isArray(stop?.notes) ? stop.notes : [],
7366
+ actions: {
7367
+ keepWaiting: phase === "timeout",
7368
+ stopAgain: phase === "timeout",
7369
+ openSession: phase === "timeout" || phase === "failed",
7370
+ copyDiagnostics: phase === "timeout" || phase === "failed"
7371
+ }
7372
+ };
7373
+ }
7374
+ //#endregion
6747
7375
  //#region src/client/features/workbench/Handoff.tsx
6748
7376
  /** 手工提示词全文。字段缺失时给出可操作的提示,而不是留空。 */
6749
7377
  function handoffPrompt(task) {
@@ -7586,12 +8214,6 @@ body[data-ds-dark-theme] .crwu-audit-root {
7586
8214
  });
7587
8215
  }
7588
8216
  //#endregion
7589
- //#region src/shared/utils/value.ts
7590
- /** 将可空外部值转换为稳定字符串。 */
7591
- function text(value) {
7592
- return value === void 0 || value === null ? "" : String(value);
7593
- }
7594
- //#endregion
7595
8217
  //#region src/client/features/report-audit/row.ts
7596
8218
  /**
7597
8219
  * 报告页的判定规则。
@@ -7844,7 +8466,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
7844
8466
  //#endregion
7845
8467
  //#region src/shared/utils/case-dir.ts
7846
8468
  /**
7847
- * 一条报告的**案例目录**:`<工作空间>/<流水号>`。
8469
+ * 一条报告的**案例目录**:`<工作空间>/<流水号>`(Windows 上是 `<工作空间>\<流水号>`)。
7848
8470
  *
7849
8471
  * 这是 Host 与 Client 都依赖的唯一约定,所以只在这里定义一次:
7850
8472
  * - Host 的审核提示词拿它告诉审核子代理「本案例目录必须是 …」(`host/audit/prompt.ts`);
@@ -7855,15 +8477,22 @@ body[data-ds-dark-theme] .crwu-audit-root {
7855
8477
  * 猜成了 `<工作空间>/cases/<流水号>`:既扫了用户的磁盘,又把材料下到了错的地方。
7856
8478
  * 两处各写一份拼接逻辑就是这种漂移的温床,所以宁可多一个共享模块。
7857
8479
  *
7858
- * 拼接方式与 Host 原实现逐字一致(用 `/` 连接、去掉工作空间末尾的斜杠),
7859
- * 因为提示词里给出的是**跨平台都一样的字符串**,不引入 `node:path` 的平台差异。
7860
- * 任一侧为空时返回空串,调用方据此**不写这一段**(宁可不给,也不给一个半截路径)。
8480
+ * **拼接交给 `joinLocalPath`**(2026-09-28 Windows 适配):分隔符随工作空间的风格走 ——
8481
+ * 在 Windows 上硬拼 `/` 会得到 `C:\Work/S1` 这种混用形式,而这条路径既进提示词
8482
+ * (模型照着写 PowerShell 命令)又进 `ctx.shell` 的 `workdir`。
8483
+ *
8484
+ * 任一侧为空、或流水号不是安全的一段(绝对路径 / 含 `..`)时返回空串,
8485
+ * 调用方据此**不写这一段**(宁可不给,也不给一个半截或越界的路径)。
7861
8486
  */
7862
8487
  function caseDirOf(workspacePath, seqNo) {
7863
- const workspace = text(workspacePath).replace(/\/+$/, "");
8488
+ const workspace = text(workspacePath);
7864
8489
  const seq = text(seqNo).trim();
7865
8490
  if (workspace === "" || seq === "") return "";
7866
- return `${workspace}/${seq}`;
8491
+ try {
8492
+ return joinLocalPath(workspace, seq);
8493
+ } catch (error) {
8494
+ return "";
8495
+ }
7867
8496
  }
7868
8497
  //#endregion
7869
8498
  //#region src/client/features/report-audit/ReportPane.tsx
@@ -8932,23 +9561,100 @@ body[data-ds-dark-theme] .crwu-audit-root {
8932
9561
  className: WORKBENCH_CLASSES.muted,
8933
9562
  children: state.childAliveHint
8934
9563
  }),
8935
- state.activeKey === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Notice, {
8936
- tone: "warn",
8937
- children: [zhCN.activePrefix + state.activeKey + zhCN.activeSuffix, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8938
- className: WORKBENCH_CLASSES.row,
8939
- style: { marginTop: "6px" },
8940
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
8941
- label: zhCN.stopAudit,
8942
- tone: "warn",
8943
- onClick: () => {
8944
- props.onStop("");
8945
- }
8946
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8947
- className: WORKBENCH_CLASSES.grow,
8948
- children: zhCN.stopHint
8949
- })]
8950
- })]
8951
- }),
9564
+ state.activeKey === "" ? null : (() => {
9565
+ const activeRecord = state.audits[state.activeKey];
9566
+ const stop = stopPresentationOf({
9567
+ stop: activeRecord?.stop,
9568
+ requestedAt: props.stopRequestedAt ?? 0,
9569
+ now: Date.now()
9570
+ });
9571
+ const childId = activeRecord?.childId ?? "";
9572
+ const canOpen = childId !== "";
9573
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Notice, {
9574
+ tone: stop.tone === "success" ? "ok" : "warn",
9575
+ children: [
9576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9577
+ className: WORKBENCH_CLASSES.row,
9578
+ children: [
9579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: zhCN.currentAuditTitle }),
9580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9581
+ className: WORKBENCH_CLASSES.grow,
9582
+ children: `${state.activeKey}${activeRecord?.project === void 0 || activeRecord.project === "" ? "" : ` · ${activeRecord.project}`}`
9583
+ }),
9584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9585
+ label: canOpen ? zhCN.openAuditSession : zhCN.openAuditSessionPending,
9586
+ onClick: () => {
9587
+ props.onOpenSession(state.activeKey);
9588
+ },
9589
+ disabled: !canOpen,
9590
+ title: zhCN.openAuditSessionHint
9591
+ })
9592
+ ]
9593
+ }),
9594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9595
+ className: WORKBENCH_CLASSES.cellSub,
9596
+ children: `${activeRecord?.status ?? ""}${activeRecord === void 0 ? "" : ` · ${zhCN.currentAuditAttempt}${activeRecord.attempt}${zhCN.currentAuditAttemptSuffix}`}${activeRecord?.startedAt === void 0 || activeRecord.startedAt === "" ? "" : ` · ${zhCN.currentAuditStarted}${formatDateTime(activeRecord.startedAt)}`}${childId === "" ? "" : ` · ${zhCN.currentAuditChildTail}${childId.slice(-8)}`}`
9597
+ }),
9598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
9599
+ className: WORKBENCH_CLASSES.row,
9600
+ style: { marginTop: "6px" },
9601
+ children: [
9602
+ activeRecord === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: WORKBENCH_CLASSES.grow }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
9603
+ "aria-live": "polite",
9604
+ role: "status",
9605
+ className: WORKBENCH_CLASSES.grow,
9606
+ children: [
9607
+ stop.label,
9608
+ stop.elapsedText === "" ? "" : ` · ${stop.elapsedText}`,
9609
+ stop.waitHint === "" ? "" : ` · ${stop.waitHint}`
9610
+ ]
9611
+ }),
9612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9613
+ label: zhCN.stopAudit,
9614
+ tone: "warn",
9615
+ onClick: () => {
9616
+ props.onStop("");
9617
+ },
9618
+ disabled: stop.stopDisabled
9619
+ }),
9620
+ stop.actions.keepWaiting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9621
+ label: zhCN.stopActionKeepWaiting,
9622
+ onClick: () => {
9623
+ props.onRefreshStatus?.();
9624
+ }
9625
+ }) : null,
9626
+ stop.actions.stopAgain ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9627
+ label: zhCN.stopActionStopAgain,
9628
+ tone: "warn",
9629
+ onClick: () => {
9630
+ props.onStop("");
9631
+ }
9632
+ }) : null,
9633
+ stop.actions.openSession ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9634
+ label: zhCN.stopActionOpenSession,
9635
+ onClick: () => {
9636
+ props.onOpenSession(state.activeKey);
9637
+ }
9638
+ }) : null,
9639
+ stop.actions.copyDiagnostics ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
9640
+ label: zhCN.stopActionCopyDiagnostics,
9641
+ onClick: () => {
9642
+ props.onCopyDiagnostics?.(state.activeKey);
9643
+ }
9644
+ }) : null
9645
+ ]
9646
+ }),
9647
+ activeRecord === void 0 || stop.hint === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9648
+ className: WORKBENCH_CLASSES.cellSub,
9649
+ children: stop.hint
9650
+ }),
9651
+ stop.notes.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
9652
+ className: WORKBENCH_CLASSES.cellSub,
9653
+ children: stop.notes.join(";")
9654
+ })
9655
+ ]
9656
+ });
9657
+ })(),
8952
9658
  state.ossIndexError === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Notice, {
8953
9659
  tone: "warn",
8954
9660
  children: zhCN.cloudFailed + state.ossIndexError
@@ -9732,6 +10438,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
9732
10438
  buildKind: "",
9733
10439
  builtAt: "",
9734
10440
  protocol: null,
10441
+ permissionSchemaVersion: null,
9735
10442
  parentSessionId: ""
9736
10443
  };
9737
10444
  function createBuildStore() {
@@ -9767,6 +10474,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
9767
10474
  buildKind: result.buildKind === "dev" || result.buildKind === "installed" ? result.buildKind : "",
9768
10475
  builtAt: typeof result.builtAt === "string" ? result.builtAt : "",
9769
10476
  protocol: typeof result.protocol === "number" ? result.protocol : null,
10477
+ permissionSchemaVersion: typeof result.permissionSchemaVersion === "number" ? result.permissionSchemaVersion : null,
9770
10478
  parentSessionId: typeof result.parentSessionId === "string" ? result.parentSessionId : ""
9771
10479
  });
9772
10480
  return snapshot;
@@ -9796,7 +10504,18 @@ body[data-ds-dark-theme] .crwu-audit-root {
9796
10504
  }
9797
10505
  /** 宿主与客户端是不是同一代(不同代时**必须**拦住发起审核,见 AGENTS.md §7.12)。 */
9798
10506
  function hostIsStale(snapshot) {
9799
- return snapshot.protocol !== null && snapshot.protocol !== 16;
10507
+ return snapshot.protocol !== null && snapshot.protocol !== 19;
10508
+ }
10509
+ /**
10510
+ * 宿主执行的权限说明版本与客户端是否一致(协议 18)。
10511
+ *
10512
+ * 与 `hostIsStale` 分开判是刻意的:协议号回答「是不是同一代」,这一条回答
10513
+ * 「我这份界面上写的授权范围,和宿主实际执行的判据是不是同一版」。
10514
+ * 版本不同(含旧宿主根本没给这个字段)时,**任何本机凭据操作都必须停住** ——
10515
+ * 让一个执行旧范围的宿主看起来授权成功,比拦住更危险。
10516
+ */
10517
+ function hostPermissionSchemaStale(snapshot) {
10518
+ return snapshot.permissionSchemaVersion !== 1;
9800
10519
  }
9801
10520
  /**
9802
10521
  * 当前运行的版本号(**不带 `v` 前缀**):`0.0.11` / dev 形态回空串。
@@ -10383,6 +11102,19 @@ body[data-ds-dark-theme] .crwu-audit-root {
10383
11102
  const [authError, setAuthError] = react.useState("");
10384
11103
  const [pending, setPending] = react.useState(null);
10385
11104
  const [audits, setAudits] = react.useState({});
11105
+ /**
11106
+ * 本地记下的"我刚点过停止"的时刻(F1)。
11107
+ *
11108
+ * `audit-stop` 是两阶段的:RPC 只**接受**请求,真正停下要等后台。而状态轮询是 10 秒一次 ——
11109
+ * 只靠轮询的话,用户点完会盯着没反应的按钮最多 10 秒。本地这个时刻让界面立刻显示
11110
+ * 「正在请求停止审核…」,阶段与结论仍然以 Host 回报为准。
11111
+ */
11112
+ const [stopRequestedAt, setStopRequestedAt] = react.useState(0);
11113
+ /**
11114
+ * Host 说"现在能不能启动下一条审核"(F4)。**只信 Host**:
11115
+ * 缺字段(旧宿主)按"没有正在停的审核"处理,否则老版本界面会被永久禁用。
11116
+ */
11117
+ const [canStartNext, setCanStartNext] = react.useState(true);
10386
11118
  const [activeKey, setActiveKey] = react.useState("");
10387
11119
  const [activeChildId, setActiveChildId] = react.useState("");
10388
11120
  const [ossIndex, setOssIndex] = react.useState({});
@@ -10409,6 +11141,11 @@ body[data-ds-dark-theme] .crwu-audit-root {
10409
11141
  const drawerTimer = react.useRef(null);
10410
11142
  const [wsBusy, setWsBusy] = react.useState(false);
10411
11143
  const [wsMessage, setWsMessage] = react.useState("");
11144
+ const [dwsLocal, setDwsLocal] = react.useState(null);
11145
+ const [dwsLocalRepair, setDwsLocalRepair] = react.useState(null);
11146
+ const [dwsLocalBusy, setDwsLocalBusy] = react.useState(false);
11147
+ const [dwsLocalError, setDwsLocalError] = react.useState("");
11148
+ const [dwsLocalConfirming, setDwsLocalConfirming] = react.useState(false);
10412
11149
  const [escalateAvailable, setEscalateAvailable] = react.useState(false);
10413
11150
  const [handoff, setHandoff] = react.useState(null);
10414
11151
  const [handoffCopied, setHandoffCopied] = react.useState(false);
@@ -10508,6 +11245,45 @@ body[data-ds-dark-theme] .crwu-audit-root {
10508
11245
  }
10509
11246
  }, []);
10510
11247
  const [auditsReady, setAuditsReady] = react.useState(false);
11248
+ /**
11249
+ * 「复制诊断」:把当前审核的停止诊断整理成一段可粘贴的文本。
11250
+ *
11251
+ * 只放**维护者需要的事实**(key / childId 尾 / 阶段 / 错误 / 观察 / attempt / 开始时间)——
11252
+ * 路径、凭据一律不进(脱敏口径见 AGENTS §4.4.2)。
11253
+ */
11254
+ const copyStopDiagnostics = react.useCallback((key) => {
11255
+ const record = audits[key];
11256
+ if (record === void 0) return;
11257
+ const stop = record.stop;
11258
+ const lines = [
11259
+ `key=${key}`,
11260
+ `child=${record.childId === "" ? "(空)" : record.childId.slice(-8)}`,
11261
+ `attempt=${record.attempt}`,
11262
+ `status=${record.status}`,
11263
+ `startedAt=${record.startedAt}`,
11264
+ `stop.phase=${stop?.phase ?? "(未知)"}`,
11265
+ `stop.quiesced=${String(stop?.quiesced ?? "(未知)")}`,
11266
+ `stop.aborted=${String(stop?.aborted ?? "(未知)")}`,
11267
+ `stop.disposed=${String(stop?.disposed ?? "(未知)")}`,
11268
+ `stop.elapsedMs=${String(stop?.elapsedMs ?? 0)}`,
11269
+ `stop.error=${stop?.error === void 0 || stop.error === "" ? "(无)" : stop.error}`,
11270
+ `stop.notes=${stop?.notes === void 0 || stop.notes.length === 0 ? "(无)" : stop.notes.join(";")}`
11271
+ ].join("\n");
11272
+ const clipboard = globalThis.navigator?.clipboard;
11273
+ if (clipboard?.writeText !== void 0) {
11274
+ clipboard.writeText(lines).then(() => {
11275
+ setNotice(zhCN.copyDiagnosticsDone);
11276
+ }).catch(() => {
11277
+ setNotice(zhCN.copyDiagnosticsFailed);
11278
+ });
11279
+ return;
11280
+ }
11281
+ workbenchApi.clipboard({ text: lines }).then((result) => {
11282
+ setNotice(result.ok ? zhCN.copyDiagnosticsDone : result.error);
11283
+ }).catch((cause) => {
11284
+ setNotice(describe(cause));
11285
+ });
11286
+ }, [audits]);
10511
11287
  const refreshAudits = react.useCallback(async () => {
10512
11288
  try {
10513
11289
  const result = await workbenchApi.auditStatus({});
@@ -10518,6 +11294,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
10518
11294
  setAudits(next);
10519
11295
  setActiveKey(result.active.key);
10520
11296
  setActiveChildId(result.active.childId);
11297
+ setCanStartNext(result.canStartNext !== false);
10521
11298
  } catch (cause) {}
10522
11299
  }, []);
10523
11300
  react.useEffect(() => {
@@ -10637,14 +11414,24 @@ body[data-ds-dark-theme] .crwu-audit-root {
10637
11414
  });
10638
11415
  }
10639
11416
  };
10640
- const authorized = env !== null && env.trust.credentials === true;
11417
+ consentOf(env);
11418
+ const authorized = env !== null && consentGranted(env);
11419
+ /**
11420
+ * 允许本机访问。
11421
+ *
11422
+ * 提交的是**规范能力清单 + 权限说明版本**(不是 `{credentials: true}`):Host 逐字核对清单,
11423
+ * 所以「界面显示的清单」与「落盘的收据」不可能不一致。返回的 `consent` 是回读结果 ——
11424
+ * 写盘失败时它不是 granted,界面就照实说"没有获得权限",而不是一句成功提示。
11425
+ */
10641
11426
  const grantCredentials = () => {
10642
11427
  setAuthBusy(true);
10643
11428
  setAuthError("");
10644
- workbenchApi.trust({ credentials: true }).then((result) => {
10645
- const persistError = typeof result.persistError === "string" ? result.persistError : "";
10646
- if (persistError !== "") {
10647
- setAuthError(persistError);
11429
+ workbenchApi.localAccessGrant({
11430
+ schemaVersion: 1,
11431
+ capabilities: consentRequestCapabilities()
11432
+ }).then((result) => {
11433
+ if (result.ok !== true) {
11434
+ setAuthError(result.error || zhCN.envConsentPersistFailedGrant);
10648
11435
  return null;
10649
11436
  }
10650
11437
  setAuthDeclined(false);
@@ -10655,7 +11442,25 @@ body[data-ds-dark-theme] .crwu-audit-root {
10655
11442
  setAuthBusy(false);
10656
11443
  });
10657
11444
  };
11445
+ /** 撤销本机访问:Host 侧**内存先关**,写盘失败也 fail closed(只在界面上如实报错)。 */
11446
+ const revokeCredentials = () => {
11447
+ setAuthBusy(true);
11448
+ setAuthError("");
11449
+ workbenchApi.localAccessRevoke().then((result) => {
11450
+ setAuthDeclined(false);
11451
+ if (result.ok !== true) {
11452
+ setAuthError(result.error || zhCN.envConsentPersistFailed);
11453
+ return;
11454
+ }
11455
+ return envStatus.refresh();
11456
+ }).catch((cause) => {
11457
+ setAuthError(describe(cause));
11458
+ }).finally(() => {
11459
+ setAuthBusy(false);
11460
+ });
11461
+ };
10658
11462
  const hostStale = hostIsStale(build);
11463
+ const permissionStale = hostPermissionSchemaStale(build);
10659
11464
  /**
10660
11465
  * 是不是"第一次进来、还没拿到自检结论"。
10661
11466
  *
@@ -10681,6 +11486,10 @@ body[data-ds-dark-theme] .crwu-audit-root {
10681
11486
  ...auditBusy === "" ? {} : { auditBusy },
10682
11487
  stopBusy,
10683
11488
  retryBusy,
11489
+ ...canStartNext ? {} : {
11490
+ canStart: false,
11491
+ gateReason: zhCN.stopNoRestartHint
11492
+ },
10684
11493
  ...hostStale ? {
10685
11494
  canDispatch: false,
10686
11495
  gateReason: zhCN.hostStaleGate
@@ -10712,6 +11521,40 @@ body[data-ds-dark-theme] .crwu-audit-root {
10712
11521
  notice,
10713
11522
  childAliveHint
10714
11523
  };
11524
+ /**
11525
+ * 只读体检:**不是随时可跑的健康检查**,而是"刚才那次为什么失败"的事后归因。
11526
+ *
11527
+ * 设计 §D2 规定它只在"刚刚发生过一次 DWS 失败、且结构化事实排除沙箱"之后运行;
11528
+ * Host 在没有任何可归因失败时直接拒绝(`not-applicable`),界面如实显示那句话。
11529
+ * 这里不自己判断"该不该给按钮" —— 那是 Host 的判据,客户端再判一次就会有两套口径。
11530
+ */
11531
+ const checkDwsLocal = () => {
11532
+ setDwsLocalBusy(true);
11533
+ setDwsLocalError("");
11534
+ workbenchApi.dwsLocalDoctor().then((view) => {
11535
+ setDwsLocal(view);
11536
+ if (!view.ok) setDwsLocalError(view.error);
11537
+ }).catch((error) => {
11538
+ setDwsLocalError(error instanceof Error ? error.message : String(error));
11539
+ }).finally(() => {
11540
+ setDwsLocalBusy(false);
11541
+ });
11542
+ };
11543
+ /** 修复:只有走完二次确认才会到这里。 */
11544
+ const repairDwsLocal = () => {
11545
+ setDwsLocalConfirming(false);
11546
+ setDwsLocalBusy(true);
11547
+ setDwsLocalError("");
11548
+ workbenchApi.dwsLocalPermissionRepair({ confirm: true }).then((result) => {
11549
+ setDwsLocalRepair(result);
11550
+ setDwsLocal(result.doctor);
11551
+ if (!result.ok) setDwsLocalError(result.error);
11552
+ }).catch((error) => {
11553
+ setDwsLocalError(error instanceof Error ? error.message : String(error));
11554
+ }).finally(() => {
11555
+ setDwsLocalBusy(false);
11556
+ });
11557
+ };
10715
11558
  const envPane = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnvironmentPane, {
10716
11559
  env,
10717
11560
  build,
@@ -10742,10 +11585,29 @@ body[data-ds-dark-theme] .crwu-audit-root {
10742
11585
  authError,
10743
11586
  authDeclined,
10744
11587
  onGrantCredentials: grantCredentials,
11588
+ onRevokeCredentials: revokeCredentials,
11589
+ onDeclineCredentials: () => {
11590
+ setAuthError("");
11591
+ setAuthDeclined(true);
11592
+ },
11593
+ authSchemaMismatch: permissionStale,
10745
11594
  onRegrant: () => {
10746
11595
  setAuthError("");
10747
11596
  setAuthDeclined(false);
10748
- }
11597
+ },
11598
+ dwsLocal,
11599
+ dwsLocalRepair,
11600
+ dwsLocalBusy,
11601
+ dwsLocalError,
11602
+ dwsLocalConfirming,
11603
+ onDwsLocalCheck: checkDwsLocal,
11604
+ onDwsLocalAskRepair: () => {
11605
+ setDwsLocalConfirming(true);
11606
+ },
11607
+ onDwsLocalCancelRepair: () => {
11608
+ setDwsLocalConfirming(false);
11609
+ },
11610
+ onDwsLocalConfirmRepair: repairDwsLocal
10749
11611
  });
10750
11612
  /**
10751
11613
  * 报告审核模块被门禁挡住时的正文。
@@ -10931,15 +11793,24 @@ body[data-ds-dark-theme] .crwu-audit-root {
10931
11793
  },
10932
11794
  onStop: (childId) => {
10933
11795
  setStopBusy(true);
11796
+ setStopRequestedAt(Date.now());
10934
11797
  workbenchApi.auditStop(childId === "" ? {} : { childId }).then((result) => {
10935
11798
  setNotice(result.ok ? "" : result.error);
10936
11799
  return refreshAudits();
10937
11800
  }).catch((cause) => {
11801
+ setStopRequestedAt(0);
10938
11802
  setNotice(describe(cause));
10939
11803
  }).finally(() => {
10940
11804
  setStopBusy(false);
10941
11805
  });
10942
11806
  },
11807
+ stopRequestedAt,
11808
+ onRefreshStatus: () => {
11809
+ refreshAudits();
11810
+ },
11811
+ onCopyDiagnostics: (key) => {
11812
+ copyStopDiagnostics(key);
11813
+ },
10943
11814
  onRetryUpload: (key) => {
10944
11815
  setRetryBusy(true);
10945
11816
  workbenchApi.ossUpload({ key }).then((result) => {
@@ -10966,7 +11837,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
10966
11837
  setNotice("这条记录还没有本地 HTML");
10967
11838
  return;
10968
11839
  }
10969
- workbenchApi.openPath({ path: `${record.casePath.replace(/\/+$/, "")}/${record.htmlFile}` }).then((result) => {
11840
+ workbenchApi.openPath({ path: joinLocalPath(record.casePath, record.htmlFile) }).then((result) => {
10970
11841
  if (!result.ok) setNotice(result.error);
10971
11842
  });
10972
11843
  },
@@ -11008,8 +11879,7 @@ body[data-ds-dark-theme] .crwu-audit-root {
11008
11879
  workbenchApi.pending({
11009
11880
  ...query === "" ? {} : { query },
11010
11881
  page: reportState.page,
11011
- size: reportState.pageSize,
11012
- escalate: true
11882
+ size: reportState.pageSize
11013
11883
  }).then((result) => {
11014
11884
  setPending(result);
11015
11885
  setEscalateAvailable(result.escalateAvailable === true);