dsh-jev-guard 0.5.1
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 +285 -0
- package/CHANGELOG.zh-CN.md +271 -0
- package/DEPLOY.md +202 -0
- package/DEPLOY.zh-CN.md +200 -0
- package/LICENSE +21 -0
- package/README.md +316 -0
- package/README.zh-CN.md +315 -0
- package/START-HERE.md +97 -0
- package/START-HERE.zh-CN.md +97 -0
- package/adapters/README.md +37 -0
- package/adapters/README.zh-CN.md +37 -0
- package/adapters/dsh/index.js +502 -0
- package/bin/guard.mjs +634 -0
- package/config.example.json +52 -0
- package/cordis.patch.yml +120 -0
- package/docs/AGENT-TASK-dsh.md +134 -0
- package/docs/AGENT-TASK-dsh.zh-CN.md +131 -0
- package/docs/ARCHITECTURE.md +118 -0
- package/docs/ARCHITECTURE.zh-CN.md +117 -0
- package/docs/DECISIONS.md +469 -0
- package/docs/DECISIONS.zh-CN.md +449 -0
- package/docs/DSH-INTEGRATION.md +178 -0
- package/docs/DSH-INTEGRATION.zh-CN.md +171 -0
- package/docs/MEASUREMENTS.md +433 -0
- package/docs/MEASUREMENTS.zh-CN.md +450 -0
- package/docs/USER-INTERVENTION.md +141 -0
- package/docs/USER-INTERVENTION.zh-CN.md +143 -0
- package/docs/VERIFICATION.md +279 -0
- package/docs/VERIFICATION.zh-CN.md +278 -0
- package/lib/audit.js +228 -0
- package/lib/gate.js +720 -0
- package/lib/i18n.js +575 -0
- package/lib/quota.js +389 -0
- package/lib/rules.js +174 -0
- package/lib/token.js +154 -0
- package/lib/verdict.js +285 -0
- package/package.json +82 -0
- package/tools/check-doc-pairs.mjs +158 -0
- package/tools/extract-commands.mjs +156 -0
- package/tools/gate-cli.mjs +240 -0
- package/tools/probe-prompt-lang.mjs +238 -0
- package/tools/probe-scripts.mjs +143 -0
- package/tools/report-result.mjs +146 -0
- package/tools/selftest-audit.mjs +93 -0
- package/tools/selftest-entry.mjs +177 -0
- package/tools/selftest-i18n.mjs +177 -0
- package/tools/selftest-quota.mjs +260 -0
- package/tools/selftest-reason.mjs +266 -0
- package/tools/selftest-rules.mjs +107 -0
- package/tools/selftest-token.mjs +100 -0
- package/tools/smoke-dsh-adapter.mjs +295 -0
- package/tools/smoke-dsh-pipeline.mjs +146 -0
package/lib/i18n.js
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bilingual message catalog (zh-CN / en).
|
|
3
|
+
*
|
|
4
|
+
* Why a catalog instead of inline strings: the same verdict text is read by three
|
|
5
|
+
* different audiences — the human in an approval dialog, the model that gets the
|
|
6
|
+
* denial, and whoever reads the audit log afterwards — and a deployment can be
|
|
7
|
+
* either Chinese- or English-speaking. Keeping every human-facing sentence in one
|
|
8
|
+
* place makes "is this translation complete?" a checkable question (see
|
|
9
|
+
* `tools/selftest-i18n.mjs`) instead of a code review.
|
|
10
|
+
*
|
|
11
|
+
* Scope: **text a user or a model can see.** Verdict reasons, rule reasons, CLI
|
|
12
|
+
* output, degradation warnings, the judge question. Code comments and the labels
|
|
13
|
+
* inside the developer tools (`tools/`) stay Chinese on purpose: they are read by
|
|
14
|
+
* maintainers of a Chinese-first codebase, and translating them doubles the upkeep
|
|
15
|
+
* of every future change without changing what the product says.
|
|
16
|
+
*
|
|
17
|
+
* Language resolution (highest first):
|
|
18
|
+
* 1. an explicit `lang` in config.json / the cordis patch (`setLang(cfg.lang)`)
|
|
19
|
+
* 2. `JEV_GUARD_LANG`
|
|
20
|
+
* 3. `LC_ALL` / `LC_MESSAGES` / `LANG`, when they name a language we ship
|
|
21
|
+
* 4. `zh-CN` — the fallback, and the language every measurement was taken in
|
|
22
|
+
*
|
|
23
|
+
* `lang: 'auto'` (the default) runs steps 2-4.
|
|
24
|
+
*
|
|
25
|
+
* **Why `Intl` is deliberately NOT in that chain.** It was, and it bit us on the
|
|
26
|
+
* first real deployment: the DSH plugin runs inside WSL where `LANG=C.UTF-8` means
|
|
27
|
+
* "no locale preference", so the chain fell through to `Intl`, which in a Node
|
|
28
|
+
* started with `C.UTF-8` reports `en-US` — Node's own ICU default, not the
|
|
29
|
+
* operator's preference. Result: the reason text silently switched to English
|
|
30
|
+
* inside the session while the Windows-side CLI (whose Node reports `zh-CN`) stayed
|
|
31
|
+
* Chinese — the same machine speaking two languages depending on which process you
|
|
32
|
+
* asked. A `C`/`POSIX`/unset locale is *absence of a signal*, so the honest move is
|
|
33
|
+
* to fall back to the project's primary language, and to get English you say so
|
|
34
|
+
* (`JEV_GUARD_LANG=en`, `"lang": "en"`, or `--lang en`). Locales that do name a
|
|
35
|
+
* language (`en_US.UTF-8`, `zh_CN.UTF-8`) still resolve on their own.
|
|
36
|
+
*
|
|
37
|
+
* Nothing here touches the judge service: the question sent to Jev has its own
|
|
38
|
+
* switch (`promptLang`) because the thresholds were calibrated against the Chinese
|
|
39
|
+
* question (see lib/gate.js).
|
|
40
|
+
*
|
|
41
|
+
* @module jev-guard/i18n
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** Languages this package ships. */
|
|
45
|
+
export const LANGS = Object.freeze(['zh-CN', 'en'])
|
|
46
|
+
|
|
47
|
+
/** Used when nothing else resolves, and as the per-key fallback. */
|
|
48
|
+
export const FALLBACK_LANG = 'zh-CN'
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every string a user or a model can see, except the L0 rule reasons (which live
|
|
52
|
+
* next to their regex in `lib/rules.js` so a rule stays one self-contained unit).
|
|
53
|
+
*
|
|
54
|
+
* `{name}` placeholders are filled by {@link t}. Keep both languages in step:
|
|
55
|
+
* `tools/selftest-i18n.mjs` fails the build when a key exists in only one of them.
|
|
56
|
+
*/
|
|
57
|
+
const MESSAGES = {
|
|
58
|
+
'zh-CN': {
|
|
59
|
+
// — verdict.js: the one-line reason —
|
|
60
|
+
'verdict.line': 'Jev 安全阀门[{head}] {detail}。{tail}{auto}{warn}{token} 命令:{command}',
|
|
61
|
+
'verdict.head.allow': '放行',
|
|
62
|
+
'verdict.head.revise': '暂缓:证据不足以安全执行',
|
|
63
|
+
'verdict.head.block': '拦截',
|
|
64
|
+
'verdict.head.prompted': '需要人工确认',
|
|
65
|
+
'verdict.detail.rule': '命中硬规则 `{id}`({why})',
|
|
66
|
+
'verdict.detail.degraded': '降级放行(本条未经过语义判定)',
|
|
67
|
+
'verdict.detail.prefilter': '确定性预筛',
|
|
68
|
+
'verdict.detail.probability': 'Jev 判定风险概率 {pct}',
|
|
69
|
+
'verdict.askWait': '这条命令需要人工确认,宿主已向用户发起审批请求 —— 请等用户在弹窗里决定,不要换写法重试。',
|
|
70
|
+
'verdict.tail.revise': '请改用下面任一更安全的形式后重试;若都不适用,请向用户说明并请求人工介入。',
|
|
71
|
+
'verdict.tail.block': '这条命令在本机被禁止自动执行。请改用安全替代方案,或由用户手动执行。',
|
|
72
|
+
'verdict.tail.escalate': '本会话没有审批提示,或该操作必须由人确认。请让用户手动执行。',
|
|
73
|
+
'verdict.autoNote': '(这是自动判定,不是用户手动拒绝;请勿尝试绕开。)',
|
|
74
|
+
'verdict.tokenHint': ' 【如需只放行这一次:把下面这行交给用户,请他在自己的终端里执行'
|
|
75
|
+
+ '(授权只在交互终端生效,AI 自己跑会被拒{winNote}):'
|
|
76
|
+
+ ' node {cli} allow {quoted} —— 然后重试同一条命令。令牌 {token}】',
|
|
77
|
+
'verdict.winNote': ',用 PowerShell',
|
|
78
|
+
'verdict.reviseHeading': '可以尝试的更安全形式:',
|
|
79
|
+
'verdict.templateLine': '- {text} 例:{examples}',
|
|
80
|
+
'verdict.shell.posix': 'POSIX shell(bash 等)',
|
|
81
|
+
|
|
82
|
+
// — verdict.js: the three deterministic downgrade templates —
|
|
83
|
+
'template.dry-run.text': '先跑只读/演练形态:',
|
|
84
|
+
'template.dry-run.example.0': 'git diff <file> 代替 git checkout -- <file>',
|
|
85
|
+
'template.dry-run.example.1': 'terraform plan 代替 terraform apply',
|
|
86
|
+
'template.dry-run.example.2': 'npm publish --dry-run 代替 npm publish',
|
|
87
|
+
'template.dry-run.example.3': 'rsync -n --delete 代替 rsync --delete',
|
|
88
|
+
'template.narrow.text': '把作用域缩到你真正要动的那一部分:',
|
|
89
|
+
'template.narrow.example.0': '具体文件/子目录代替整个目录',
|
|
90
|
+
'template.narrow.example.1': '加 --exclude 排除掉不该动的',
|
|
91
|
+
'template.narrow.example.2': '限定 --resource / 单条 SQL 的 WHERE 条件',
|
|
92
|
+
'template.backup-first.text': '先制造一个回退点再执行:',
|
|
93
|
+
'template.backup-first.example.0': 'git 仓库:先 commit 或 stash',
|
|
94
|
+
'template.backup-first.example.1': '普通目录:先复制到磁盘上(不是 /tmp)',
|
|
95
|
+
'template.backup-first.example.2': '数据库:先 dump 到磁盘',
|
|
96
|
+
|
|
97
|
+
// — quota.js: failure kinds —
|
|
98
|
+
'quota.quota.label': '判定服务额度已用尽',
|
|
99
|
+
'quota.quota.hint': '充值/提高额度后无需任何操作:下一个探测窗口会自动恢复(最长等一个冷却周期)。',
|
|
100
|
+
'quota.quota.cliHint.0': '立刻停用:`… allow` 不受影响',
|
|
101
|
+
'quota.quota.cliHint.1': '查状态:`guard status`',
|
|
102
|
+
'quota.auth.label': '判定服务的密钥无效或被撤销',
|
|
103
|
+
'quota.auth.hint': '检查 secrets.json / 环境变量里的 TYPESAFE_API_KEY(别把密钥贴进对话)。',
|
|
104
|
+
'quota.no-key.label': '没有解析到判定服务的密钥',
|
|
105
|
+
'quota.no-key.hint': '三种录入方式(任选):① 跑 `node bin/guard.mjs key set`(从标准输入读,不进 shell 历史与进程列表);② 设环境变量 `TYPESAFE_API_KEY`;③ 在包根写 `secrets.json`(相对路径的 apiKeyFile 按**包根**解析,与 cwd 无关)。录入后不需要重启。别把密钥贴进对话。',
|
|
106
|
+
'quota.rate-limit.label': '被判定服务限流(429)',
|
|
107
|
+
'quota.rate-limit.hint': '通常是瞬时的;命令会短暂回到"只过 L0"。',
|
|
108
|
+
'quota.server.label': '判定服务返回 5xx',
|
|
109
|
+
'quota.server.hint': '对方侧故障,逐次 fail-open。',
|
|
110
|
+
'quota.timeout.label': '判定超时',
|
|
111
|
+
'quota.timeout.hint': '超时一律放行;若持续出现,调大 timeoutMs 或检查网络/代理。',
|
|
112
|
+
'quota.network.label': '网络不通',
|
|
113
|
+
'quota.network.hint': '代理/出口问题;逐次 fail-open。',
|
|
114
|
+
'quota.shape.label': '返回体不符合预期',
|
|
115
|
+
'quota.shape.hint': '可能对方改了 API 形状;判定不可信,已放行。',
|
|
116
|
+
'quota.unknown.label': '未知错误',
|
|
117
|
+
'quota.unknown.hint': '看审计日志里的原文。',
|
|
118
|
+
'quota.policy.off': '整条阀门已暂停',
|
|
119
|
+
'quota.policy.l0-only': '仍在跑 L0 免费规则 + 预筛',
|
|
120
|
+
'quota.warning': '⚠️ Jev 安全阀门已降级:{label}(自 {since}Z,已失败 {failures} 次)。'
|
|
121
|
+
+ '联网语义判定暂停 {mins} 分钟;{policy}。',
|
|
122
|
+
'quota.warning.sticky': '⚠️ Jev 安全阀门已降级:{label}(自 {since}Z,已失败 {failures} 次)。'
|
|
123
|
+
+ '联网语义判定暂停;这个状态不靠时间结束,靠它描述的**条件消失**(如密钥一出现);{policy}。',
|
|
124
|
+
'quota.status.ok': '✅ Jev 安全阀门:正常',
|
|
125
|
+
'quota.status.ok.layers': ' L0 静态硬规则 + 预筛 + Jev 语义判定,四态齐全。',
|
|
126
|
+
'quota.status.ok.noKey': ' ⚠️ 但是:当前没有解析到 API 密钥(`guard judge` 会退回 L0/预筛)。',
|
|
127
|
+
'quota.status.degraded.title': '⚠️ Jev 安全阀门:已降级({kind})',
|
|
128
|
+
'quota.status.degraded.reason': ' 原因:{label}',
|
|
129
|
+
'quota.status.degraded.counters': ' 开始:{since} 失败:{failures} 次 探测:{probes} 次',
|
|
130
|
+
'quota.status.degraded.recovery': ' 恢复:{mins} 分钟后自动探测一次;成功即恢复,失败则继续降级。',
|
|
131
|
+
'quota.status.degraded.sticky': ' 恢复:不需要等待 —— 它不靠时间结束;条件一消失(例如密钥一出现)会**当场自动恢复**。',
|
|
132
|
+
'quota.status.degraded.scope': ' 影响范围:仅本入口({scope})—— 其它入口各自的密钥解析不受影响。',
|
|
133
|
+
'quota.status.degraded.now.off': '整条阀门暂停(全部放行)',
|
|
134
|
+
'quota.status.degraded.now.l0-only': '联网语义判定暂停,L0 免费规则 + 预筛仍在工作',
|
|
135
|
+
'quota.status.degraded.now': ' 现在:{now}',
|
|
136
|
+
'quota.status.degraded.action': ' 处理:{hint}',
|
|
137
|
+
'quota.status.degraded.detail': ' 原始错误:{detail}',
|
|
138
|
+
'quota.status.degraded.retry': ' 立即重试:guard status --clear (然后跑一条命令,会重新尝试联网判定)',
|
|
139
|
+
'quota.reason.degraded': '降级放行({kind}):联网语义判定暂停,{policy}',
|
|
140
|
+
|
|
141
|
+
// — 会话内 notice(DSH 适配器注入;纯 host 插件唯一能让用户真看到的渠道,见 D15) —
|
|
142
|
+
// summary 会渲染成折叠行的标题,受 120 字符上限约束(自检会查)。
|
|
143
|
+
'notice.no-key.summary': '没有解析到 Jev 密钥:语义判定已停,只剩 L0 硬规则;请录入密钥。',
|
|
144
|
+
'notice.no-key.body': 'Jev 安全阀门:没有解析到判定服务的密钥,已进入降级。\n'
|
|
145
|
+
+ '\n现在:联网语义判定层暂停,只保留免费的 L0 静态硬规则 + 预筛 —— mkfs、dd of=/dev/*、'
|
|
146
|
+
+ 'git push --force 这类不可逆操作照拦,但"灰区"判断没有了。\n'
|
|
147
|
+
+ '\n录入密钥(任选一种):\n'
|
|
148
|
+
+ ' 1) node {cli} key set 从标准输入读密钥(不回显、不进 shell 历史与进程列表)\n'
|
|
149
|
+
+ ' 2) 设环境变量 TYPESAFE_API_KEY\n'
|
|
150
|
+
+ ' 3) 在包根写 secrets.json(键名 TYPESAFE_API_KEY)\n'
|
|
151
|
+
+ '\n录入后不用重启、也不用等冷却:下一条受管命令就会自动恢复(这条降级状态当场清除)。\n'
|
|
152
|
+
+ '密钥只存在你本机,不会被打印,也不会写进日志。',
|
|
153
|
+
'notice.degraded.summary': 'Jev 安全阀门已降级:{label}({policy})',
|
|
154
|
+
'notice.degraded.body': 'Jev 安全阀门进入降级:{label}。\n{recovery}\n'
|
|
155
|
+
+ '现在:{policy}。\n'
|
|
156
|
+
+ '查状态:node {cli} status',
|
|
157
|
+
'notice.recovered.summary': 'Jev 安全阀门已恢复:四态判定重新生效。',
|
|
158
|
+
'notice.recovered.body': 'Jev 安全阀门已从降级状态恢复:联网语义判定重新生效,L0 + 预筛 + Jev 语义判定四态齐全。',
|
|
159
|
+
|
|
160
|
+
// — gate.js: what the judge service is told —
|
|
161
|
+
'gate.state.command': '命令',
|
|
162
|
+
'gate.state.script': '脚本内容',
|
|
163
|
+
'gate.state.pkgScript': '包脚本',
|
|
164
|
+
'gate.state.pkgScriptBody': '包脚本内容',
|
|
165
|
+
'gate.state.notRead': '(未读取: {why})',
|
|
166
|
+
'gate.truncated': '… (已截断)',
|
|
167
|
+
'gate.skip.sensitive': '疑似敏感文件,未上传内容',
|
|
168
|
+
'gate.skip.notFile': '不是普通文件',
|
|
169
|
+
'gate.skip.tooLarge': '文件过大 ({bytes} bytes)',
|
|
170
|
+
'gate.skip.binary': '二进制内容',
|
|
171
|
+
'gate.skip.readError': '读取失败: {error}',
|
|
172
|
+
'gate.question.instructions': '这条命令会不可逆地删除或覆盖用户的真实数据吗?(可重建的缓存、临时文件、未使用的镜像不算)',
|
|
173
|
+
'gate.question.true': '命令会不可逆地删除或覆盖真实数据:用户文档、源码、相册、日志、数据库、块设备内容、版本历史',
|
|
174
|
+
'gate.question.false': '命令只读,或只影响可重建的缓存、临时文件、未使用的镜像与构建产物',
|
|
175
|
+
|
|
176
|
+
// — bin/guard.mjs —
|
|
177
|
+
'cli.usage': '用法: guard <judge|log|status|allow|key|selftest|rules> [选项] [--lang zh-CN|en]',
|
|
178
|
+
'cli.key.usage': '用法: guard key set|status [--key-file 路径] [--lang zh-CN|en]',
|
|
179
|
+
'cli.key.set.needsTty': '请在一个交互式终端里跑 `guard key set`:密钥只从标准输入读(它不接受命令行参数 —— 那会进 shell 历史与进程列表)。',
|
|
180
|
+
'cli.key.set.prompt': '把 TypeSafe Jev 密钥粘进来后回车(不回显、不进 shell 历史;Ctrl-C 放弃):',
|
|
181
|
+
'cli.key.set.empty': '没有读到内容,什么都没写。',
|
|
182
|
+
'cli.key.set.whitespace': '读到的内容里有空白字符,已放弃 —— 密钥本身不该含空格或换行(粘贴时多带了换行是常见原因,请只粘密钥本身)。',
|
|
183
|
+
'cli.key.set.written': '✅ 已写入 {path}(权限 0600,文件里现有 {count} 个键)。密钥长度 {len},不回显。',
|
|
184
|
+
'cli.key.set.hint': '这是密钥的第三来源:DSH 插件会读它;凭据层与环境变量优先级更高。DSH 不需要重启即可生效。',
|
|
185
|
+
'cli.key.status.env': '来源:环境变量 {name}(长度 {len})',
|
|
186
|
+
'cli.key.status.file': '来源:文件 {path}(长度 {len})',
|
|
187
|
+
'cli.key.status.none': '❌ 没有解析到密钥:环境变量 {name} 未设置,{path} 里也没有。跑 `node {cli} key set` 录入。',
|
|
188
|
+
'cli.key.status.staleState': ' 注意:检测到一条"没有密钥"的粘性降级状态,而密钥现在已可解析 —— 已顺手清除,不需要重启。',
|
|
189
|
+
'cli.selftest.header': 'L0 规则: deny {deny} 条 / ask {ask} 条',
|
|
190
|
+
'cli.selftest.pass': 'selftest: {total} 项全部通过(未联网)',
|
|
191
|
+
'cli.selftest.fail': 'selftest: {failed} 项失败',
|
|
192
|
+
'cli.rules.denyHeader': '# L0 deny(永不放行)',
|
|
193
|
+
'cli.rules.askHeader': '# L0 ask(必须人工确认)',
|
|
194
|
+
'cli.reasonIndent': ' ↳ {reason}',
|
|
195
|
+
'cli.degraded.stateDetail': ' 状态详情:node {cli} status',
|
|
196
|
+
'cli.log.header': '日志: {path}',
|
|
197
|
+
'cli.log.recent': '日志: {path} (最近 {count} 条)',
|
|
198
|
+
'cli.log.emptyRange': '近 {hours} 小时没有记录。',
|
|
199
|
+
'cli.log.emptyFile': '还没有记录。装好阀门后,每个判定都会写到这里。',
|
|
200
|
+
'cli.log.writeError': '⚠️ 最近一次写入失败:{error}',
|
|
201
|
+
'cli.log.writeErrorNote': ' (审计写入失败会被静默吞掉以免影响判定,所以在这里显式提示)',
|
|
202
|
+
'cli.log.total': '近 {hours} 小时共 {total} 条 ({first} → {last})',
|
|
203
|
+
'cli.log.failOpen': ' fail-open(判定失败但放行): {count}',
|
|
204
|
+
'cli.log.degraded': ' ⚠️ 降级放行(额度/密钥类,没花钱): {count} 条',
|
|
205
|
+
'cli.log.degradedLast': ' 最近:{kind} @ {at}Z',
|
|
206
|
+
'cli.log.byAction': '按动作',
|
|
207
|
+
'cli.log.bySource': '按来源',
|
|
208
|
+
'cli.log.byRule': '按规则',
|
|
209
|
+
'cli.log.byErrorKind': '失败分类',
|
|
210
|
+
'cli.log.cost': ' 语义判定成本: 约 ${cost} ({tokens} 输入 token,{priced} 次调用有 usage 记录 ≈ 全部记录的 {coverage}%;输出按官方说明免费)',
|
|
211
|
+
'cli.log.costUnknown': ' 语义判定成本: 未记录(最近的调用没有返回 usage;升级前写入的旧记录也不含)',
|
|
212
|
+
'cli.status.cleared': '已清除降级状态。下一条命令会重新尝试联网判定(失败会再次进入降级)。',
|
|
213
|
+
'cli.status.nothingToClear': '当前没有降级状态,无需清除。',
|
|
214
|
+
'cli.status.stateFile': ' 状态文件: {path}{missing}',
|
|
215
|
+
'cli.status.stateFileMissing': '(不存在 = 健康)',
|
|
216
|
+
'cli.status.explainer': ' 说明:降级 = 停用**要花钱的语义判定**;免费的 L0 规则与预筛照常工作(当前 degradePolicy={policy})。',
|
|
217
|
+
'cli.status.probeDue': ' 注意:冷却已到期,下一条命令会自动发一次探测请求(成功即恢复)。',
|
|
218
|
+
'cli.allow.needsTty': '授权必须在交互终端里执行:当前检测到非交互环境(不是 TTY)。\n'
|
|
219
|
+
+ '这是有意的 —— 否则被监管的 AI 就能给自己授权,阀门形同虚设。\n\n'
|
|
220
|
+
+ '请让用户在最普通的终端窗口({shell})里复制执行下面这一行:\n\n'
|
|
221
|
+
+ ' {line}\n\n'
|
|
222
|
+
+ 'Windows 的 cmd.exe 不支持上面这种引号写法 —— 那种情况改用与 shell 无关的入口:\n'
|
|
223
|
+
+ '把命令原文**原样**写进一个文件(比如 cmd.txt),然后执行\n\n'
|
|
224
|
+
+ ' node {cli} allow --command-file cmd.txt\n\n'
|
|
225
|
+
+ '然后让 AI 重试同一条命令,即可放行一次。',
|
|
226
|
+
'cli.allow.revoked': '已撤销 {removed} 个令牌(剩余 {remaining})',
|
|
227
|
+
'cli.allow.notFound': '没找到该令牌(当前共 {remaining} 个)',
|
|
228
|
+
'cli.allow.fileHeader': '令牌文件: {path}',
|
|
229
|
+
'cli.allow.fileEmpty': ' (空)',
|
|
230
|
+
'cli.allow.readFileError': '读不到 --command-file 指定的文件:{path}({error})',
|
|
231
|
+
'cli.allow.usage': '用法: guard allow \'<命令原文>\' | --command-file <文件> | --list | --revoke ALLOW-XXXXXXXXXX',
|
|
232
|
+
'cli.allow.already': '这条命令已有令牌:{token}(重试同一条命令即可放行一次)',
|
|
233
|
+
'cli.allow.granted': '已写入一次性令牌:{token}',
|
|
234
|
+
'cli.allow.grantedFile': ' 文件:{path}',
|
|
235
|
+
'cli.allow.grantedNote': ' 下一次执行**完全相同的命令**时生效,用掉即删除。',
|
|
236
|
+
'cli.lang.unknown': '⚠️ 未知语言 {lang},可选项:{langs}(继续用 {used})',
|
|
237
|
+
|
|
238
|
+
// — tools/report-result.mjs:验证结果汇总(它是**入库给人读**的产物,所以文案也在目录里) —
|
|
239
|
+
'summary.title': '# 验证结果汇总(DSH)',
|
|
240
|
+
'summary.generatedBy': '由 `tools/report-result.mjs` 自动生成。编号对照表在 `docs/VERIFICATION.md`。',
|
|
241
|
+
'summary.colHost': '宿主',
|
|
242
|
+
'summary.colItem': '验证项',
|
|
243
|
+
'summary.colStatus': '结论',
|
|
244
|
+
'summary.colEvidence': '证据',
|
|
245
|
+
'summary.colTime': '时间',
|
|
246
|
+
'summary.empty': '还没有任何结论',
|
|
247
|
+
'summary.blockedHeading': '## 需要主控 AI / 人介入的问题',
|
|
248
|
+
'summary.blockedItem': '- **{host} / 第 {item} 项**:{question}',
|
|
249
|
+
'summary.indexHeading': '## 验证项编号(详见 docs/VERIFICATION.md)',
|
|
250
|
+
'summary.index.0': '6-pre 适配器冒烟 + 真实工具管线 | 6 安装后 probe 被拦 | 7 误报防线 | 8/8-fix 审计日志',
|
|
251
|
+
'summary.index.1': '9 令牌闭环 | 10 授权入口与理由文案 | 11 人工三通道 | 12 宿主审批通道',
|
|
252
|
+
'summary.index.2': '13 额度降级(离线) | 14 降级在真实会话可见 | 15 ask 分支文案 | 16 跨平台入口守卫 | 17 Windows 引号',
|
|
253
|
+
'summary.index.3': '18 收窄为 DSH 专用 | 19 清除非 DSH 痕迹 | 20 包内现状核对 | 21 改名后重启激活核对',
|
|
254
|
+
'summary.index.4': '22 判定动作随审批模式分叉(ask 转人工 / never 拦死 / L0 绝对闸门) | 23 双语文案与语言开关',
|
|
255
|
+
'summary.channels': 'U1–U3:人工介入三通道(令牌 / 宿主审批 / 人工手动执行)',
|
|
256
|
+
'summary.publish': '发布与仓库核对记录见 [publish.json](./publish.json)(不在本表的验证项内)。',
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
en: {
|
|
260
|
+
// — verdict.js: the one-line reason —
|
|
261
|
+
'verdict.line': 'Jev guard [{head}] {detail}. {tail}{auto}{warn}{token} Command: {command}',
|
|
262
|
+
'verdict.head.allow': 'allow',
|
|
263
|
+
'verdict.head.revise': 'hold: not enough evidence to run it safely',
|
|
264
|
+
'verdict.head.block': 'blocked',
|
|
265
|
+
'verdict.head.prompted': 'needs human confirmation',
|
|
266
|
+
'verdict.detail.rule': 'hard rule `{id}` hit ({why})',
|
|
267
|
+
'verdict.detail.degraded': 'allowed while degraded (no semantic judgment for this one)',
|
|
268
|
+
'verdict.detail.prefilter': 'deterministic pre-screen',
|
|
269
|
+
'verdict.detail.probability': 'Jev risk probability {pct}',
|
|
270
|
+
'verdict.askWait': 'This command needs human confirmation and the host has raised an approval request — wait for the user to decide in that prompt instead of rephrasing and retrying.',
|
|
271
|
+
'verdict.tail.revise': 'Retry in one of the safer forms below; if none fits, explain it to the user and ask for manual intervention.',
|
|
272
|
+
'verdict.tail.block': 'This command is not allowed to run automatically on this machine. Use a safe alternative, or have the user run it by hand.',
|
|
273
|
+
'verdict.tail.escalate': 'This session has no approval prompt, or the operation requires a human. Ask the user to run it by hand.',
|
|
274
|
+
'verdict.autoNote': ' (This was an automatic decision, not the user rejecting it by hand; do not try to work around it.)',
|
|
275
|
+
'verdict.tokenHint': ' [To allow this once: hand the user the line below and have them run it in their own terminal'
|
|
276
|
+
+ ' (authorisation only works from an interactive terminal — if the AI runs it itself it is refused{winNote}):'
|
|
277
|
+
+ ' node {cli} allow {quoted} — then retry the same command. Token {token}]',
|
|
278
|
+
'verdict.winNote': ', using PowerShell',
|
|
279
|
+
'verdict.reviseHeading': 'Safer forms to try:',
|
|
280
|
+
'verdict.templateLine': '- {text} e.g. {examples}',
|
|
281
|
+
'verdict.shell.posix': 'a POSIX shell (bash etc.)',
|
|
282
|
+
|
|
283
|
+
// — verdict.js: the three deterministic downgrade templates —
|
|
284
|
+
'template.dry-run.text': 'run a read-only or rehearsal form first:',
|
|
285
|
+
'template.dry-run.example.0': 'git diff <file> instead of git checkout -- <file>',
|
|
286
|
+
'template.dry-run.example.1': 'terraform plan instead of terraform apply',
|
|
287
|
+
'template.dry-run.example.2': 'npm publish --dry-run instead of npm publish',
|
|
288
|
+
'template.dry-run.example.3': 'rsync -n --delete instead of rsync --delete',
|
|
289
|
+
'template.narrow.text': 'narrow the scope to the part you actually mean to touch:',
|
|
290
|
+
'template.narrow.example.0': 'a specific file/subdirectory instead of the whole directory',
|
|
291
|
+
'template.narrow.example.1': 'add --exclude to leave out what should not be touched',
|
|
292
|
+
'template.narrow.example.2': 'one --resource / a WHERE clause on the SQL instead of all rows',
|
|
293
|
+
'template.backup-first.text': 'create a rollback point before executing:',
|
|
294
|
+
'template.backup-first.example.0': 'git repo: commit or stash first',
|
|
295
|
+
'template.backup-first.example.1': 'plain directory: copy it to disk first (not to /tmp)',
|
|
296
|
+
'template.backup-first.example.2': 'database: dump it to disk first',
|
|
297
|
+
|
|
298
|
+
// — quota.js: failure kinds —
|
|
299
|
+
'quota.quota.label': 'the judging service is out of credit',
|
|
300
|
+
'quota.quota.hint': 'After topping up there is nothing to do: the next probe window recovers on its own (at most one cooldown period).',
|
|
301
|
+
'quota.quota.cliHint.0': 'to stop right away: `… allow` is unaffected',
|
|
302
|
+
'quota.quota.cliHint.1': 'to check: `guard status`',
|
|
303
|
+
'quota.auth.label': 'the judging service rejected or revoked the key',
|
|
304
|
+
'quota.auth.hint': 'Check TYPESAFE_API_KEY in secrets.json / the environment (never paste a key into a conversation).',
|
|
305
|
+
'quota.no-key.label': 'no key for the judging service could be resolved',
|
|
306
|
+
'quota.no-key.hint': 'Three ways to set it (pick one): (1) run `node bin/guard.mjs key set` (reads the key from stdin — never shell history or the process list); (2) set the environment variable `TYPESAFE_API_KEY`; (3) write `secrets.json` in the package root (a relative apiKeyFile resolves against the **package root**, independent of cwd). No restart is needed afterwards. Never paste a key into a conversation.',
|
|
307
|
+
'quota.rate-limit.label': 'rate-limited by the judging service (429)',
|
|
308
|
+
'quota.rate-limit.hint': 'usually transient; commands briefly fall back to "L0 only".',
|
|
309
|
+
'quota.server.label': 'the judging service returned 5xx',
|
|
310
|
+
'quota.server.hint': 'a fault on their side; each call fails open.',
|
|
311
|
+
'quota.timeout.label': 'judgment timed out',
|
|
312
|
+
'quota.timeout.hint': 'a timeout always allows; if it keeps happening, raise timeoutMs or check the network/proxy.',
|
|
313
|
+
'quota.network.label': 'network unreachable',
|
|
314
|
+
'quota.network.hint': 'a proxy/egress problem; each call fails open.',
|
|
315
|
+
'quota.shape.label': 'unexpected response shape',
|
|
316
|
+
'quota.shape.hint': 'they may have changed the API shape; the judgment is not trustworthy, so it was allowed.',
|
|
317
|
+
'quota.unknown.label': 'unknown error',
|
|
318
|
+
'quota.unknown.hint': 'read the raw error in the audit log.',
|
|
319
|
+
'quota.policy.off': 'the whole valve is suspended',
|
|
320
|
+
'quota.policy.l0-only': 'the free L0 rules + pre-screen are still running',
|
|
321
|
+
'quota.warning': '⚠️ Jev guard is degraded: {label} (since {since}Z, {failures} failures).'
|
|
322
|
+
+ ' Online semantic judgment paused for {mins} min; {policy}.',
|
|
323
|
+
'quota.warning.sticky': '⚠️ Jev guard is degraded: {label} (since {since}Z, {failures} failures).'
|
|
324
|
+
+ ' Online semantic judgment is paused; this state does not end with time — it ends when the condition it describes goes away (a key appearing, for instance); {policy}.',
|
|
325
|
+
'quota.status.ok': '✅ Jev guard: healthy',
|
|
326
|
+
'quota.status.ok.layers': ' L0 static rules + pre-screen + Jev semantic judgment, all four states available.',
|
|
327
|
+
'quota.status.ok.noKey': ' ⚠️ But: no API key is currently resolved (`guard judge` falls back to L0/pre-screen).',
|
|
328
|
+
'quota.status.degraded.title': '⚠️ Jev guard: degraded ({kind})',
|
|
329
|
+
'quota.status.degraded.reason': ' reason: {label}',
|
|
330
|
+
'quota.status.degraded.counters': ' since: {since} failures: {failures} probes: {probes}',
|
|
331
|
+
'quota.status.degraded.recovery': ' recovery: one automatic probe in {mins} min; success restores it, failure keeps it degraded.',
|
|
332
|
+
'quota.status.degraded.sticky': ' recovery: nothing to wait for — it does not end with time; the moment the condition clears (a key appearing, for instance) it **recovers on the spot**.',
|
|
333
|
+
'quota.status.degraded.scope': ' affects: this entry only ({scope}) — other entries resolve their own keys and are unaffected.',
|
|
334
|
+
'quota.status.degraded.now.off': 'the whole valve is suspended (everything allowed)',
|
|
335
|
+
'quota.status.degraded.now.l0-only': 'online semantic judgment paused; the free L0 rules + pre-screen still work',
|
|
336
|
+
'quota.status.degraded.now': ' now: {now}',
|
|
337
|
+
'quota.status.degraded.action': ' what to do: {hint}',
|
|
338
|
+
'quota.status.degraded.detail': ' raw error: {detail}',
|
|
339
|
+
'quota.status.degraded.retry': ' retry now: guard status --clear (then run a command; it will try the online judge again)',
|
|
340
|
+
'quota.reason.degraded': 'allowed while degraded ({kind}): online semantic judgment paused, {policy}',
|
|
341
|
+
|
|
342
|
+
// — in-session notice (injected by the DSH adapter; the only channel a host-only plugin has, D15) —
|
|
343
|
+
// `summary` becomes the collapsed row's title and is bounded to 120 chars (a self-test checks it).
|
|
344
|
+
'notice.no-key.summary': 'No Jev API key resolved: semantic judgment is off, L0 rules only. Please add the key.',
|
|
345
|
+
'notice.no-key.body': 'Jev guard: no key for the judging service could be resolved, so the valve is degraded.\n'
|
|
346
|
+
+ '\nRight now: the online semantic layer is paused and only the free L0 static rules + pre-screen run —'
|
|
347
|
+
+ ' mkfs, dd of=/dev/*, git push --force and the like are still blocked, but the "grey zone" judgment is gone.\n'
|
|
348
|
+
+ '\nTo set the key (pick one):\n'
|
|
349
|
+
+ ' 1) node {cli} key set reads the key from stdin (no echo, no shell history, no process list)\n'
|
|
350
|
+
+ ' 2) set the environment variable TYPESAFE_API_KEY\n'
|
|
351
|
+
+ ' 3) write secrets.json in the package root (key name TYPESAFE_API_KEY)\n'
|
|
352
|
+
+ '\nNo restart and no cooldown: the next gated command recovers automatically and this state is cleared on the spot.\n'
|
|
353
|
+
+ 'The key stays on your machine; it is never printed and never written to the log.',
|
|
354
|
+
'notice.degraded.summary': 'Jev guard is degraded: {label} ({policy})',
|
|
355
|
+
'notice.degraded.body': 'Jev guard entered a degraded state: {label}.\n{recovery}\n'
|
|
356
|
+
+ 'Now: {policy}.\n'
|
|
357
|
+
+ 'Check it with: node {cli} status',
|
|
358
|
+
'notice.recovered.summary': 'Jev guard recovered: all four verdict states work again.',
|
|
359
|
+
'notice.recovered.body': 'Jev guard is out of its degraded state: online semantic judgment works again — L0 + pre-screen + Jev judgment, all four states available.',
|
|
360
|
+
|
|
361
|
+
// — gate.js: what the judge service is told —
|
|
362
|
+
'gate.state.command': 'command',
|
|
363
|
+
'gate.state.script': 'script',
|
|
364
|
+
'gate.state.pkgScript': 'package_script',
|
|
365
|
+
'gate.state.pkgScriptBody': 'package_script_body',
|
|
366
|
+
'gate.state.notRead': '(not read: {why})',
|
|
367
|
+
'gate.truncated': '… (truncated)',
|
|
368
|
+
'gate.skip.sensitive': 'looks like a sensitive file, contents not uploaded',
|
|
369
|
+
'gate.skip.notFile': 'not a regular file',
|
|
370
|
+
'gate.skip.tooLarge': 'file too large ({bytes} bytes)',
|
|
371
|
+
'gate.skip.binary': 'binary content',
|
|
372
|
+
'gate.skip.readError': 'read failed: {error}',
|
|
373
|
+
'gate.question.instructions': 'Will this command irreversibly delete or overwrite the user\'s real data? (rebuildable caches, temporary files and unused images do not count)',
|
|
374
|
+
'gate.question.true': 'The command irreversibly deletes or overwrites real data: user documents, source code, photos, logs, databases, block-device contents, version history',
|
|
375
|
+
'gate.question.false': 'The command is read-only, or only affects rebuildable caches, temporary files, unused images and build output',
|
|
376
|
+
|
|
377
|
+
// — bin/guard.mjs —
|
|
378
|
+
'cli.usage': 'usage: guard <judge|log|status|allow|key|selftest|rules> [options] [--lang zh-CN|en]',
|
|
379
|
+
'cli.key.usage': 'usage: guard key set|status [--key-file PATH] [--lang zh-CN|en]',
|
|
380
|
+
'cli.key.set.needsTty': 'Run `guard key set` in an interactive terminal: the key is read from stdin only (it takes no command-line argument — that would land in your shell history and the process list).',
|
|
381
|
+
'cli.key.set.prompt': 'Paste the TypeSafe Jev key and press Enter (not echoed, not in shell history; Ctrl-C to abort):',
|
|
382
|
+
'cli.key.set.empty': 'nothing was read, so nothing was written.',
|
|
383
|
+
'cli.key.set.whitespace': 'what was read contains whitespace, so nothing was written — a key should hold no spaces or newlines (a stray newline from pasting is the usual cause; paste the key alone).',
|
|
384
|
+
'cli.key.set.written': '✅ wrote {path} (mode 0600; the file now holds {count} key(s)). Key length {len}, never echoed.',
|
|
385
|
+
'cli.key.set.hint': 'This is the third key source: the DSH plugin reads it too; the credentials layer and the environment win over it. DSH picks it up without a restart.',
|
|
386
|
+
'cli.key.status.env': 'source: environment variable {name} (length {len})',
|
|
387
|
+
'cli.key.status.file': 'source: file {path} (length {len})',
|
|
388
|
+
'cli.key.status.none': '❌ no key resolved: {name} is not set in the environment and {path} holds none. Set it with `node {cli} key set`.',
|
|
389
|
+
'cli.key.status.staleState': ' note: a sticky "no key" degraded state was found while a key now resolves — cleared it, no restart needed.',
|
|
390
|
+
'cli.selftest.header': 'L0 rules: {deny} deny / {ask} ask',
|
|
391
|
+
'cli.selftest.pass': 'selftest: all {total} checks passed (offline)',
|
|
392
|
+
'cli.selftest.fail': 'selftest: {failed} failed',
|
|
393
|
+
'cli.rules.denyHeader': '# L0 deny (never allowed)',
|
|
394
|
+
'cli.rules.askHeader': '# L0 ask (human confirmation required)',
|
|
395
|
+
'cli.reasonIndent': ' ↳ {reason}',
|
|
396
|
+
'cli.degraded.stateDetail': ' details: node {cli} status',
|
|
397
|
+
'cli.log.header': 'log: {path}',
|
|
398
|
+
'cli.log.recent': 'log: {path} (last {count})',
|
|
399
|
+
'cli.log.emptyRange': 'no records in the last {hours} hours.',
|
|
400
|
+
'cli.log.emptyFile': 'no records yet. Once the valve is installed every judgment is written here.',
|
|
401
|
+
'cli.log.writeError': '⚠️ last write failed: {error}',
|
|
402
|
+
'cli.log.writeErrorNote': ' (audit write failures are swallowed so they cannot affect a judgment, hence this explicit notice)',
|
|
403
|
+
'cli.log.total': '{total} records in the last {hours} hours ({first} → {last})',
|
|
404
|
+
'cli.log.failOpen': ' fail-open (judgment failed, allowed anyway): {count}',
|
|
405
|
+
'cli.log.degraded': ' ⚠️ allowed while degraded (credit/key class, no money spent): {count}',
|
|
406
|
+
'cli.log.degradedLast': ' last: {kind} @ {at}Z',
|
|
407
|
+
'cli.log.byAction': 'by action',
|
|
408
|
+
'cli.log.bySource': 'by source',
|
|
409
|
+
'cli.log.byRule': 'by rule',
|
|
410
|
+
'cli.log.byErrorKind': 'failure breakdown',
|
|
411
|
+
'cli.log.cost': ' semantic judgment cost: ≈ ${cost} ({tokens} input tokens, {priced} calls carried usage ≈ {coverage}% of all records; output is free per the vendor)',
|
|
412
|
+
'cli.log.costUnknown': ' semantic judgment cost: not recorded (recent calls returned no usage; records written before the upgrade carry none either)',
|
|
413
|
+
'cli.status.cleared': 'Degradation state cleared. The next command will try the online judge again (a failure re-enters degradation).',
|
|
414
|
+
'cli.status.nothingToClear': 'Nothing to clear: the valve is not degraded.',
|
|
415
|
+
'cli.status.stateFile': ' state file: {path}{missing}',
|
|
416
|
+
'cli.status.stateFileMissing': ' (absent = healthy)',
|
|
417
|
+
'cli.status.explainer': ' note: degraded = the **paid semantic judgment** is off; the free L0 rules and pre-screen keep working (currently degradePolicy={policy}).',
|
|
418
|
+
'cli.status.probeDue': ' note: the cooldown has expired, so the next command sends one probe request (success restores it).',
|
|
419
|
+
'cli.allow.needsTty': 'Authorisation must be run from an interactive terminal: this is a non-interactive environment (not a TTY).\n'
|
|
420
|
+
+ 'That is deliberate — otherwise the supervised AI could authorise itself and the valve would be decorative.\n\n'
|
|
421
|
+
+ 'Ask the user to copy this line into an ordinary terminal window ({shell}):\n\n'
|
|
422
|
+
+ ' {line}\n\n'
|
|
423
|
+
+ 'Windows cmd.exe does not support that quoting — in that case use the shell-independent entry point:\n'
|
|
424
|
+
+ 'write the command text **verbatim** into a file (cmd.txt, say) and run\n\n'
|
|
425
|
+
+ ' node {cli} allow --command-file cmd.txt\n\n'
|
|
426
|
+
+ 'Then have the AI retry the same command; it is allowed once.',
|
|
427
|
+
'cli.allow.revoked': 'Revoked {removed} token(s) ({remaining} left)',
|
|
428
|
+
'cli.allow.notFound': 'No such token ({remaining} currently)',
|
|
429
|
+
'cli.allow.fileHeader': 'token file: {path}',
|
|
430
|
+
'cli.allow.fileEmpty': ' (empty)',
|
|
431
|
+
'cli.allow.readFileError': 'cannot read the file given to --command-file: {path} ({error})',
|
|
432
|
+
'cli.allow.usage': 'usage: guard allow \'<command text>\' | --command-file <file> | --list | --revoke ALLOW-XXXXXXXXXX',
|
|
433
|
+
'cli.allow.already': 'This command already has a token: {token} (retry the same command to pass once)',
|
|
434
|
+
'cli.allow.granted': 'One-shot token written: {token}',
|
|
435
|
+
'cli.allow.grantedFile': ' file: {path}',
|
|
436
|
+
'cli.allow.grantedNote': ' It takes effect on the next execution of the **exact same command**, and is deleted once used.',
|
|
437
|
+
'cli.lang.unknown': '⚠️ unknown language {lang}; available: {langs} (continuing in {used})',
|
|
438
|
+
|
|
439
|
+
// — tools/report-result.mjs: the verification ledger (a committed, human-facing artifact) —
|
|
440
|
+
'summary.title': '# Verification results (DSH)',
|
|
441
|
+
'summary.generatedBy': 'Generated by `tools/report-result.mjs`. The item index lives in `docs/VERIFICATION.md`.',
|
|
442
|
+
'summary.colHost': 'host',
|
|
443
|
+
'summary.colItem': 'item',
|
|
444
|
+
'summary.colStatus': 'result',
|
|
445
|
+
'summary.colEvidence': 'evidence',
|
|
446
|
+
'summary.colTime': 'time',
|
|
447
|
+
'summary.empty': 'no results recorded yet',
|
|
448
|
+
'summary.blockedHeading': '## Needs the supervising AI / a human',
|
|
449
|
+
'summary.blockedItem': '- **{host} / item {item}**: {question}',
|
|
450
|
+
'summary.indexHeading': '## Item index (see docs/VERIFICATION.md)',
|
|
451
|
+
'summary.index.0': '6-pre adapter smoke test + the real tool pipeline | 6 the probe is blocked after install | 7 the false-positive guard | 8/8-fix the audit log',
|
|
452
|
+
'summary.index.1': '9 the token loop | 10 the authorisation entry point and its wording | 11 the three human channels | 12 the host approval channel',
|
|
453
|
+
'summary.index.2': '13 credit exhaustion (offline) | 14 degradation visible in a real session | 15 wording of the ask branch | 16 the cross-platform entry guard | 17 Windows quoting',
|
|
454
|
+
'summary.index.3': '18 narrowed to DSH-only | 19 non-DSH traces removed from the package | 20 the package describes DSH and nothing else | 21 reactivation check after the rename',
|
|
455
|
+
'summary.index.4': '22 the verdict action branches with the approval policy (ask → human / never → refuse / L0 → absolute gate) | 23 bilingual messages and the language switch',
|
|
456
|
+
'summary.channels': 'U1–U3: the three human channels (one-shot token / host approval / running it by hand)',
|
|
457
|
+
'summary.publish': 'Publishing and repository checks are recorded in [publish.json](./publish.json) (not a verification item).',
|
|
458
|
+
},
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Normalise anything locale-shaped onto a language we ship. */
|
|
462
|
+
export function normalizeLang(value) {
|
|
463
|
+
const raw = String(value ?? '').trim().toLowerCase()
|
|
464
|
+
if (raw === '') return undefined
|
|
465
|
+
if (raw.startsWith('zh')) return 'zh-CN'
|
|
466
|
+
if (raw.startsWith('en')) return 'en'
|
|
467
|
+
return undefined
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Pick a language from the environment. Exported because the CLI prints which
|
|
472
|
+
* language it chose when it cannot honour an explicit request.
|
|
473
|
+
*
|
|
474
|
+
* Values that do not name a language we ship (`C`, `C.UTF-8`, `POSIX`, empty) are
|
|
475
|
+
* treated as **no signal** rather than as English — see the module doc for the
|
|
476
|
+
* deployment this decision comes from.
|
|
477
|
+
*
|
|
478
|
+
* @param env - environment bag (defaults to `process.env`).
|
|
479
|
+
* @returns one of {@link LANGS}, or undefined when nothing matched.
|
|
480
|
+
*/
|
|
481
|
+
export function detectLang(env = process.env) {
|
|
482
|
+
const explicit = normalizeLang(env?.JEV_GUARD_LANG)
|
|
483
|
+
if (explicit) return explicit
|
|
484
|
+
for (const name of ['LC_ALL', 'LC_MESSAGES', 'LANG']) {
|
|
485
|
+
const found = normalizeLang(env?.[name])
|
|
486
|
+
if (found) return found
|
|
487
|
+
}
|
|
488
|
+
return undefined
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
let current = FALLBACK_LANG
|
|
492
|
+
let currentReason = 'default'
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Set the language for every subsequent {@link t} call.
|
|
496
|
+
* @param value - a language tag, or `'auto'` to resolve from the environment.
|
|
497
|
+
* @returns `{ lang, known }` — `known: false` means the request was not a language
|
|
498
|
+
* we ship and the previous one was kept.
|
|
499
|
+
*/
|
|
500
|
+
export function setLang(value) {
|
|
501
|
+
if (value === undefined || value === null || value === 'auto') {
|
|
502
|
+
const detected = detectLang()
|
|
503
|
+
current = detected ?? FALLBACK_LANG
|
|
504
|
+
currentReason = detected ? 'auto' : 'fallback'
|
|
505
|
+
return { lang: current, known: true }
|
|
506
|
+
}
|
|
507
|
+
const wanted = normalizeLang(value)
|
|
508
|
+
if (wanted === undefined) {
|
|
509
|
+
// 不认识的请求(比如 `--lang klingon`)退回**自动解析**,而不是停在某个历史值上:
|
|
510
|
+
// 调用方已经被告知 `known: false`,但用户看到的仍应是这台机器的正常语言。
|
|
511
|
+
const detected = detectLang()
|
|
512
|
+
current = detected ?? FALLBACK_LANG
|
|
513
|
+
currentReason = detected ? 'auto' : 'fallback'
|
|
514
|
+
return { lang: current, known: false }
|
|
515
|
+
}
|
|
516
|
+
current = wanted
|
|
517
|
+
currentReason = 'explicit'
|
|
518
|
+
return { lang: current, known: true }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** @returns the language in force. */
|
|
522
|
+
export function getLang() {
|
|
523
|
+
return current
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** @returns why it is in force (`explicit` | `auto` | `fallback` | `default`) — for `--lang` diagnostics. */
|
|
527
|
+
export function getLangReason() {
|
|
528
|
+
return currentReason
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Look up one message and fill its `{placeholders}`.
|
|
533
|
+
* @param key - catalog key.
|
|
534
|
+
* @param params - placeholder values; unknown placeholders are left as-is.
|
|
535
|
+
* @returns the localised text (falling back to the fallback language, then the key).
|
|
536
|
+
*/
|
|
537
|
+
export function t(key, params = undefined) {
|
|
538
|
+
return tIn(current, key, params)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Look a message up in an **explicit** language rather than the one in force.
|
|
543
|
+
*
|
|
544
|
+
* Why this exists: the text sent to the judging service is not UI text. Its language
|
|
545
|
+
* is `promptLang`, which deliberately defaults to the calibrated Chinese question
|
|
546
|
+
* even on an English deployment — so it must not follow the interface language.
|
|
547
|
+
*
|
|
548
|
+
* @param lang - language to read from (unknown tags fall back to `zh-CN`).
|
|
549
|
+
* @param key - catalog key.
|
|
550
|
+
* @param params - placeholder values.
|
|
551
|
+
* @returns the localised text (or the key when no language defines it).
|
|
552
|
+
*/
|
|
553
|
+
export function tIn(lang, key, params = undefined) {
|
|
554
|
+
const table = MESSAGES[lang] ?? MESSAGES[FALLBACK_LANG]
|
|
555
|
+
const raw = table[key] ?? MESSAGES[FALLBACK_LANG][key] ?? key
|
|
556
|
+
if (params === undefined) return raw
|
|
557
|
+
return raw.replace(/\{(\w+)\}/g, (whole, name) => (
|
|
558
|
+
Object.prototype.hasOwnProperty.call(params, name) ? String(params[name]) : whole
|
|
559
|
+
))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Every key in a language — used by the completeness self-check, which fails when
|
|
564
|
+
* a key exists in one language but not the other.
|
|
565
|
+
* @param lang - language to enumerate.
|
|
566
|
+
* @returns sorted keys.
|
|
567
|
+
*/
|
|
568
|
+
export function keysOf(lang) {
|
|
569
|
+
return Object.keys(MESSAGES[lang] ?? {}).sort()
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** @returns the shipped languages (a copy, so callers cannot mutate the catalog). */
|
|
573
|
+
export function langs() {
|
|
574
|
+
return [...LANGS]
|
|
575
|
+
}
|