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/CHANGELOG.md +418 -0
- package/README.en.md +53 -13
- package/README.md +69 -12
- package/common/skills/crwu-dws/SKILL.md +4 -4
- package/common/skills/crwu-h3yun-login/SKILL.md +3 -3
- package/common/skills/crwu-h3yun-query/SKILL.md +5 -5
- package/lib/client.js +979 -109
- package/lib/index.js +5470 -1389
- package/package.json +21 -9
- package/scripts/lib/cli-entry.mjs +112 -0
- package/scripts/prepare.mjs +56 -16
- package/skills/README.md +19 -0
- package/skills/crwu/crwu-audit/references/14-orchestration-workflow.md +15 -0
- package/skills/crwu/crwu-audit/scripts/README.md +2 -2
- package/skills/crwu/crwu-dev-audit-optimize/references/03-AI/344/272/272/345/267/245/345/267/256/350/267/235/345/210/206/346/236/220/346/265/201/347/250/213.md +1 -1
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/01-kb-source-discovery.md +1 -1
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/02-child-skill-contract.md +1 -1
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/04-registry-and-mapping-update.md +1 -1
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/05-validation-and-delivery.md +2 -2
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/06-live-routing-reconciliation.md +1 -1
- package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/README.md +2 -2
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
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5374
|
+
const failed = credentialPermissionHint(result.permission, { onlyFailures: true });
|
|
5375
|
+
if (failed !== "") setHint(failed);
|
|
4847
5376
|
return;
|
|
4848
5377
|
}
|
|
4849
5378
|
setReplacing(false);
|
|
4850
|
-
|
|
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
|
|
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.
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
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
|
|
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
|
-
|
|
5738
|
-
|
|
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
|
-
*
|
|
7859
|
-
*
|
|
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)
|
|
8488
|
+
const workspace = text(workspacePath);
|
|
7864
8489
|
const seq = text(seqNo).trim();
|
|
7865
8490
|
if (workspace === "" || seq === "") return "";
|
|
7866
|
-
|
|
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 :
|
|
8936
|
-
|
|
8937
|
-
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
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 !==
|
|
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
|
-
|
|
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.
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
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:
|
|
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);
|