@hyzyn/dsh-safe 0.8.0 → 0.10.0
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/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/ai.js +121 -46
- package/lib/i18n.js +8 -6
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -47,7 +47,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
47
47
|
| `dsh-safe list [--profile <name>] [--json]` | show quarantined plugins (`--json` outputs structured JSON; defaults to all profiles) |
|
|
48
48
|
| `dsh-safe doctor` | environment check: versions, DSH_HOME, profiles, ledger, patch health |
|
|
49
49
|
| `dsh-safe restore [--profile <name>] (--id <id> \| --all) [--dry-run]` | re-enable auto-disabled plugins (omit `--profile` to cover every profile in the ledger) |
|
|
50
|
-
| `dsh-safe explain [--profile <name> \| --file <path>]` | interpret
|
|
50
|
+
| `dsh-safe explain [id] [--profile <name> \| --file <path>]` | interpret failure info with AI: an `id` interprets that quarantine record (with repair advice); defaults to the last failure log, falling back to the ledger; `--file`/stdin read any log (needs `DSH_SAFE_AI_KEY`) |
|
|
51
51
|
| `dsh-safe repair [id] [--profile <name>] [--to <version>] [-y] [--dry-run]` | reinstall/upgrade a quarantined plugin and auto-restore it (module-resolution failures only; installs via `dsh plugin`'s pnpm channel) |
|
|
52
52
|
| `dsh-safe help` (`-h` / `--help`) | show help |
|
|
53
53
|
| `dsh-safe --version` (`-V`) | show version |
|
|
@@ -99,7 +99,7 @@ How upgrading works: `dsh-safe update` auto-detects the dsh package name and ins
|
|
|
99
99
|
|
|
100
100
|
Enabled by setting `DSH_SAFE_AI_KEY` (defaults to DeepSeek; OpenAI-compatible — swap providers via `DSH_SAFE_AI_BASE_URL` / `DSH_SAFE_AI_MODEL`):
|
|
101
101
|
|
|
102
|
-
- **`dsh-safe explain [--profile <name> | --file <path>]`**: interprets the most recent boot failure
|
|
102
|
+
- **`dsh-safe explain [id] [--profile <name> | --file <path>]`**: interprets the failure info dsh-safe knows — an `id` interprets that quarantine record and suggests `repair`; by default it interprets the most recent boot failure (stderr persisted to `$DSH_HOME/dsh-safe/last-failure-<profile>.log`), falling back to the ledger; `--file`/stdin read any log. Strictly read-only — never touches the patch or ledger.
|
|
103
103
|
- **AI fallback identification** (`DSH_SAFE_AI_RECOVER=1`): when the regex signatures can't identify the broken plugin (e.g. after a dsh upgrade changes formats), the AI picks the culprit from the stderr — **its output must pass the exact same validation pipeline** (match against real patch rows, first-party protection, dry-run preview); unmatched picks are passed through as before. Only invoked on startup failure.
|
|
104
104
|
- Privacy: home paths are redacted to `~` before sending; any AI failure degrades silently.
|
|
105
105
|
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
47
47
|
| `dsh-safe list [--profile <名>] [--json]` | 查看隔离名单(`--json` 输出结构化 JSON,缺省全部 profile) |
|
|
48
48
|
| `dsh-safe doctor` | 环境体检:版本、DSH_HOME、profiles、台账、各 patch 健康度 |
|
|
49
49
|
| `dsh-safe restore [--profile <名>] (--id <id> \| --all) [--dry-run]` | 恢复被自动禁用的插件(省略 `--profile` 时遍历台账全部 profile) |
|
|
50
|
-
| `dsh-safe explain [--profile <名> \| --file <路径>]` | 用 AI
|
|
50
|
+
| `dsh-safe explain [id] [--profile <名> \| --file <路径>]` | 用 AI 解读失败信息:指定 `id` 解读该条隔离记录(给 repair 建议);默认解读最近一次失败日志,无日志则解读隔离台账;`--file`/stdin 读任意日志(需 `DSH_SAFE_AI_KEY`) |
|
|
51
51
|
| `dsh-safe repair [id] [--profile <名>] [--to <版本>] [-y] [--dry-run]` | 重装/升级被隔离的插件并自动恢复(限模块解析失败类;经 `dsh plugin` 的 pnpm 通道安装) |
|
|
52
52
|
| `dsh-safe help`(`-h` / `--help`) | 显示帮助 |
|
|
53
53
|
| `dsh-safe --version`(`-V`) | 显示版本 |
|
|
@@ -99,7 +99,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
99
99
|
|
|
100
100
|
设置 `DSH_SAFE_AI_KEY` 后启用(默认对接 DeepSeek,OpenAI 兼容接口,可用 `DSH_SAFE_AI_BASE_URL` / `DSH_SAFE_AI_MODEL` 换任何兼容服务):
|
|
101
101
|
|
|
102
|
-
- **`dsh-safe explain [--profile <名> | --file <路径>]
|
|
102
|
+
- **`dsh-safe explain [id] [--profile <名> | --file <路径>]`**:解读 dsh-safe 所知的失败信息——指定 `id` 时解读该条隔离记录并给出 `repair` 建议;默认解读最近一次启动失败(stderr 自动持久化到 `$DSH_HOME/dsh-safe/last-failure-<profile>.log`);无日志时解读隔离台账;`--file`/stdin 读任意日志。纯只读,不碰 patch/台账。
|
|
103
103
|
- **AI 兜底识别**(`DSH_SAFE_AI_RECOVER=1`):正则特征识别不出坏插件时(如 dsh 升级换格式),让 AI 从 stderr 里挑元凶——**结果必须仍走同一验证管线**(对照真实 patch 行、第一方保护、dry-run 预览),命中不了照旧透传。仅在启动失败时调用。
|
|
104
104
|
- 隐私:发送前 home 路径脱敏为 `~`;AI 任何失败都静默降级。
|
|
105
105
|
|
package/lib/ai.js
CHANGED
|
@@ -16,6 +16,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
|
16
16
|
import { homedir } from 'node:os'
|
|
17
17
|
import { join } from 'node:path'
|
|
18
18
|
import { dshHome, resolveDshSpawnTarget } from './dshpaths.js'
|
|
19
|
+
import { loadLedger } from './quarantine.js'
|
|
19
20
|
import { getLocale, t } from './i18n.js'
|
|
20
21
|
|
|
21
22
|
export const aiEnabled = () => Boolean(process.env.DSH_SAFE_AI_KEY)
|
|
@@ -57,14 +58,14 @@ async function chat(messages, { timeoutMs = 30_000 } = {}) {
|
|
|
57
58
|
* @returns {Promise<string | null>}
|
|
58
59
|
*/
|
|
59
60
|
export async function explainFailure(stderr) {
|
|
60
|
-
const
|
|
61
|
+
const zh = getLocale() === 'zh'
|
|
61
62
|
const prompt = [
|
|
62
|
-
|
|
63
|
-
?
|
|
64
|
-
:
|
|
65
|
-
`1) ${
|
|
66
|
-
`2) ${
|
|
67
|
-
`3) ${
|
|
63
|
+
zh
|
|
64
|
+
? '以下是一段 dsh(DeepSeek Harness)启动失败时捕获的 stderr。请用中文回答,控制在 200 字内:'
|
|
65
|
+
: 'The following stderr was captured from a failed dsh (DeepSeek Harness) boot. Answer in English, within 200 words:',
|
|
66
|
+
`1) ${zh ? '一句话说明最可能的失败原因;' : 'One sentence on the most likely cause;'}`,
|
|
67
|
+
`2) ${zh ? '指出可疑的插件包名或配置项;' : 'Point out the suspicious plugin package or config;'}`,
|
|
68
|
+
`3) ${zh ? '给出 1-3 条具体修复建议(命令或配置修改)。不要编造不存在的插件。' : 'Give 1-3 concrete fixes (commands or config changes). Do not invent plugins that are not present.'}`,
|
|
68
69
|
'',
|
|
69
70
|
'--- stderr ---',
|
|
70
71
|
redact(stderr),
|
|
@@ -109,17 +110,23 @@ export async function detectFailureWithAI(stderr, knownRows) {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
/**
|
|
113
|
-
function
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
113
|
+
/** 解读被隔离插件记录(单条或多条):失败原因分析 + 现在该怎么办的建议。 */
|
|
114
|
+
async function interpretLedgerEntries(entries) {
|
|
115
|
+
const zh = getLocale() === 'zh'
|
|
116
|
+
const rows = entries.map(
|
|
117
|
+
(e) => `- id: ${e.id} ${zh ? '包名' : 'package'}: ${e.name ?? '-'} ${zh ? '隔离于' : 'quarantined at'}: ${e.quarantinedAt ?? '-'} ${zh ? '原因' : 'reason'}: ${redact(e.reason ?? '-')}`,
|
|
118
|
+
)
|
|
119
|
+
const prompt = [
|
|
120
|
+
zh
|
|
121
|
+
? '以下是 dsh-safe(DeepSeek Harness 的启动保险丝)自动隔离的插件记录:启动失败时对应 patch 行被自动禁用。请用中文回答,控制在 200 字内:'
|
|
122
|
+
: 'The following plugins were auto-quarantined by dsh-safe (a startup fuse for DeepSeek Harness): their patch rows were auto-disabled after boot failures. Answer in English, within 200 words:',
|
|
123
|
+
`1) ${zh ? '逐条说明当时最可能的失败原因(模块解析失败 / 配置或代码错误 / 依赖等待);' : 'Per record, the most likely cause of failure (module resolution / config or code error / dependency wait);'}`,
|
|
124
|
+
`2) ${zh ? '给出当前建议:用 dsh-safe repair <id> 重装修复、升级 dsh 后重试、还是保持禁用。不要编造记录里没有的信息。' : 'Advise what to do now: dsh-safe repair <id> reinstall, retry after an upgrade, or keep disabled. Do not invent facts beyond the records.'}`,
|
|
125
|
+
'',
|
|
126
|
+
zh ? '--- 隔离记录 ---' : '--- quarantine records ---',
|
|
127
|
+
...rows,
|
|
128
|
+
].join('\n')
|
|
129
|
+
return chat([{ role: 'user', content: prompt }])
|
|
123
130
|
}
|
|
124
131
|
|
|
125
132
|
/** $DSH_HOME/dsh-safe/ 下最近修改的 last-failure-*.log。 */
|
|
@@ -138,13 +145,13 @@ function findLatestFailureFile() {
|
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
/**
|
|
141
|
-
* `dsh-safe explain
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
148
|
+
* `dsh-safe explain [id]`:AI 解读 dsh-safe 所知的失败信息。
|
|
149
|
+
* - 指定 <id>:解读该条隔离记录(台账 reason + 上下文,给出 repair 建议);
|
|
150
|
+
* - 默认(无 id):优先解读最近一次启动失败(last-failure-*.log),
|
|
151
|
+
* 没有日志时解读隔离台账;--profile/--file/stdin 另见参数。
|
|
152
|
+
* 纯只读——不碰 patch/台账。
|
|
145
153
|
* @param {string[]} args
|
|
146
154
|
* @param {{
|
|
147
|
-
* spawn?: typeof spawnSync,
|
|
148
155
|
* isTTY?: boolean,
|
|
149
156
|
* readStdin?: () => string,
|
|
150
157
|
* log?: (line: string) => void,
|
|
@@ -153,7 +160,6 @@ function findLatestFailureFile() {
|
|
|
153
160
|
* @returns {Promise<number>} 退出码
|
|
154
161
|
*/
|
|
155
162
|
export async function cmdExplain(args, {
|
|
156
|
-
spawn = spawnSync,
|
|
157
163
|
isTTY = process.stdin.isTTY,
|
|
158
164
|
readStdin = () => readFileSync(0, 'utf8'),
|
|
159
165
|
log = (line) => process.stderr.write(`${line}\n`),
|
|
@@ -165,48 +171,117 @@ export async function cmdExplain(args, {
|
|
|
165
171
|
}
|
|
166
172
|
let file
|
|
167
173
|
let profile
|
|
174
|
+
let id
|
|
168
175
|
for (let i = 0; i < args.length; i++) {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
else if (
|
|
172
|
-
else if (
|
|
176
|
+
const a = args[i]
|
|
177
|
+
if (a === '--file') file = args[++i]
|
|
178
|
+
else if (a.startsWith('--file=')) file = a.slice('--file='.length)
|
|
179
|
+
else if (a === '--profile') profile = args[++i]
|
|
180
|
+
else if (a.startsWith('--profile=')) profile = a.slice('--profile='.length)
|
|
181
|
+
else if (!a.startsWith('-') && id === undefined) id = a
|
|
182
|
+
else {
|
|
183
|
+
// 严格解析:未知旗标/多余位置参数绝不静默吞
|
|
184
|
+
log(t('explainUnknownArg', { arg: a }))
|
|
185
|
+
return 2
|
|
186
|
+
}
|
|
173
187
|
}
|
|
174
188
|
if (!aiEnabled()) {
|
|
175
189
|
log(t('aiDisabled'))
|
|
176
190
|
return 1
|
|
177
191
|
}
|
|
178
|
-
|
|
192
|
+
|
|
193
|
+
// ① 指定隔离记录:解读台账条目(给出 repair 建议)
|
|
194
|
+
if (id !== undefined) {
|
|
195
|
+
const matches = []
|
|
196
|
+
for (const [p, entries] of Object.entries(loadLedger().profiles)) {
|
|
197
|
+
if (profile && p !== profile) continue
|
|
198
|
+
for (const entry of entries ?? []) {
|
|
199
|
+
if (entry.id === id) matches.push({ profile: p, entry })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (!matches.length) {
|
|
203
|
+
log(t('repairEntryMissing', { id }))
|
|
204
|
+
return 1
|
|
205
|
+
}
|
|
206
|
+
if (matches.length > 1) {
|
|
207
|
+
log(t('repairAmbiguous', { id, profiles: matches.map((m) => m.profile).join(', ') }))
|
|
208
|
+
return 2
|
|
209
|
+
}
|
|
210
|
+
log(t('explainEntry', { id, profile: matches[0].profile }))
|
|
211
|
+
const answer = await interpretLedgerEntries([matches[0].entry])
|
|
212
|
+
if (!answer) {
|
|
213
|
+
log(t('aiExplainFailed'))
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
write(answer)
|
|
217
|
+
return 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ② 指定日志文件
|
|
179
221
|
if (file !== undefined) {
|
|
222
|
+
let input
|
|
180
223
|
try {
|
|
181
224
|
input = readFileSync(file, 'utf8')
|
|
182
225
|
} catch {
|
|
183
226
|
log(t('aiFileUnreadable', { file }))
|
|
184
227
|
return 2
|
|
185
228
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
if (!input.trim()) {
|
|
230
|
+
log(t('aiNoInput'))
|
|
231
|
+
return 2
|
|
232
|
+
}
|
|
233
|
+
const answer = await explainFailure(input)
|
|
234
|
+
if (!answer) {
|
|
235
|
+
log(t('aiExplainFailed'))
|
|
236
|
+
return 1
|
|
192
237
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
238
|
+
write(answer)
|
|
239
|
+
return 0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ③ 管道输入
|
|
243
|
+
if (!isTTY) {
|
|
244
|
+
const input = readStdin()
|
|
245
|
+
if (!input.trim()) {
|
|
246
|
+
log(t('aiNoInput'))
|
|
200
247
|
return 2
|
|
201
248
|
}
|
|
249
|
+
const answer = await explainFailure(input)
|
|
250
|
+
if (!answer) {
|
|
251
|
+
log(t('aiExplainFailed'))
|
|
252
|
+
return 1
|
|
253
|
+
}
|
|
254
|
+
write(answer)
|
|
255
|
+
return 0
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ④ 默认(交互终端):最近一次失败日志 → 隔离台账
|
|
259
|
+
const latest = findLatestFailureFile()
|
|
260
|
+
if (latest) {
|
|
202
261
|
log(t('explainUsingLast', { file: latest.path }))
|
|
203
|
-
input = readFileSync(latest.path, 'utf8')
|
|
262
|
+
const input = readFileSync(latest.path, 'utf8')
|
|
263
|
+
if (!input.trim()) {
|
|
264
|
+
log(t('aiNoInput'))
|
|
265
|
+
return 2
|
|
266
|
+
}
|
|
267
|
+
const answer = await explainFailure(input)
|
|
268
|
+
if (!answer) {
|
|
269
|
+
log(t('aiExplainFailed'))
|
|
270
|
+
return 1
|
|
271
|
+
}
|
|
272
|
+
write(answer)
|
|
273
|
+
return 0
|
|
204
274
|
}
|
|
205
|
-
|
|
206
|
-
|
|
275
|
+
|
|
276
|
+
const entries = Object.entries(loadLedger().profiles)
|
|
277
|
+
.filter(([p]) => !profile || p === profile)
|
|
278
|
+
.flatMap(([, list]) => list ?? [])
|
|
279
|
+
if (!entries.length) {
|
|
280
|
+
log(t('explainNoSource'))
|
|
207
281
|
return 2
|
|
208
282
|
}
|
|
209
|
-
|
|
283
|
+
log(t('explainLedger', { count: entries.length }))
|
|
284
|
+
const answer = await interpretLedgerEntries(entries)
|
|
210
285
|
if (!answer) {
|
|
211
286
|
log(t('aiExplainFailed'))
|
|
212
287
|
return 1
|
package/lib/i18n.js
CHANGED
|
@@ -24,7 +24,7 @@ const ZH = {
|
|
|
24
24
|
恢复被自动禁用的插件(升级修复后使用)
|
|
25
25
|
dsh-safe update [-y] [--to <版本>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
26
26
|
升级 dsh 与 dsh-safe 自身,并自动恢复被隔离的插件
|
|
27
|
-
dsh-safe explain [--profile <名> | --file <路径>] 用 AI
|
|
27
|
+
dsh-safe explain [id] [--profile <名> | --file <路径>] 用 AI 解读:最近失败日志 / 隔离台账 / 任意日志
|
|
28
28
|
dsh-safe repair [id] [--profile <名>] [--to <版本>] [-y] [--dry-run]
|
|
29
29
|
重装/升级被隔离的插件并自动恢复(限模块解析失败类;省略 id 时只有一条记录则直接修)
|
|
30
30
|
dsh-safe help 显示本帮助
|
|
@@ -110,10 +110,11 @@ const ZH = {
|
|
|
110
110
|
aiFileUnreadable: '[dsh-safe] 无法读取文件: {file}',
|
|
111
111
|
aiRecovered: '[dsh-safe] AI 兜底识别出 {count} 个可疑坏插件(结果仍走同一隔离管线)',
|
|
112
112
|
explainHint: '[dsh-safe] 可运行 dsh-safe explain 解读这次失败',
|
|
113
|
-
explainBooting: '[dsh-safe] 正在试启 profile {profile}(60 秒超时;不修改任何文件)…',
|
|
114
|
-
explainBootOk: '[dsh-safe] profile {profile} 当前能正常启动,没有失败可解读。',
|
|
115
113
|
explainUsingLast: '[dsh-safe] 解读最近一次失败记录: {file}',
|
|
116
114
|
explainNoSource: '[dsh-safe] 没有可解读的内容。用法:dsh-safe explain [--profile <名> | --file <路径>],或从 stdin 管道输入。\n 包装启动失败时 stderr 会自动存到 $DSH_HOME/dsh-safe/last-failure-<profile>.log。',
|
|
115
|
+
explainLedger: '[dsh-safe] 解读隔离台账({count} 条)…',
|
|
116
|
+
explainEntry: '[dsh-safe] 解读隔离记录 {id}(profile: {profile})…',
|
|
117
|
+
explainUnknownArg: '[dsh-safe] explain 无法识别的参数: {arg}',
|
|
117
118
|
excludedByList: '[dsh-safe] 按豁免名单跳过 {label}(不自动禁用)',
|
|
118
119
|
doctorSelf: 'dsh-safe {version}',
|
|
119
120
|
doctorDsh: 'dsh {name} {version}',
|
|
@@ -157,7 +158,7 @@ Usage:
|
|
|
157
158
|
re-enable auto-disabled plugins (after a fixed plugin upgrade)
|
|
158
159
|
dsh-safe update [-y] [--to <ver>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
159
160
|
upgrade dsh and dsh-safe itself, auto-restore quarantined plugins
|
|
160
|
-
dsh-safe explain [--profile <name> | --file <path>] interpret
|
|
161
|
+
dsh-safe explain [id] [--profile <name> | --file <path>] interpret with AI: last failure log / ledger / any log
|
|
161
162
|
dsh-safe repair [id] [--profile <name>] [--to <ver>] [-y] [--dry-run]
|
|
162
163
|
reinstall/upgrade a quarantined plugin and auto-restore it (module-resolution failures only; omit id when there is exactly one record)
|
|
163
164
|
dsh-safe help show this help
|
|
@@ -246,10 +247,11 @@ Notes:
|
|
|
246
247
|
aiFileUnreadable: '[dsh-safe] cannot read file: {file}',
|
|
247
248
|
aiRecovered: '[dsh-safe] AI fallback identified {count} suspected broken plugin(s); the same quarantine pipeline applies',
|
|
248
249
|
explainHint: '[dsh-safe] run dsh-safe explain to interpret this failure',
|
|
249
|
-
explainBooting: '[dsh-safe] booting profile {profile} (60s timeout; no files are modified)…',
|
|
250
|
-
explainBootOk: '[dsh-safe] profile {profile} boots fine right now — nothing failed to interpret.',
|
|
251
250
|
explainUsingLast: '[dsh-safe] interpreting the last failure record: {file}',
|
|
252
251
|
explainNoSource: '[dsh-safe] nothing to interpret. Usage: dsh-safe explain [--profile <name> | --file <path>], or pipe via stdin.\n On wrapped-boot failures stderr is saved to $DSH_HOME/dsh-safe/last-failure-<profile>.log automatically.',
|
|
252
|
+
explainLedger: '[dsh-safe] interpreting the quarantine ledger ({count} record(s))…',
|
|
253
|
+
explainEntry: '[dsh-safe] interpreting quarantine record {id} (profile: {profile})…',
|
|
254
|
+
explainUnknownArg: '[dsh-safe] unrecognized argument for explain: {arg}',
|
|
253
255
|
excludedByList: '[dsh-safe] skipped {label} per the exclusion list (never auto-disabled)',
|
|
254
256
|
doctorSelf: 'dsh-safe {version}',
|
|
255
257
|
doctorDsh: 'dsh {name} {version}',
|