@hyzyn/dsh-safe 0.7.0 → 0.9.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 +125 -1
- package/lib/cli.js +1 -49
- package/lib/dshpaths.js +3 -0
- package/lib/i18n.js +16 -4
- package/lib/wrap.js +18 -2
- 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 [--file <path>]` | interpret a
|
|
50
|
+
| `dsh-safe explain [--profile <name> \| --file <path>]` | interpret a boot failure with AI: defaults to the last failure record, `--profile` boots the profile live, `--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 [--file <path>]`**:
|
|
102
|
+
- **`dsh-safe explain [--profile <name> | --file <path>]`**: interprets the most recent boot failure by default (stderr is persisted to `$DSH_HOME/dsh-safe/last-failure-<profile>.log` on every failed wrapped boot, for humans too); `--profile` boots that profile live and interprets (60s timeout); `--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 [--file <路径>]` | 用 AI
|
|
50
|
+
| `dsh-safe explain [--profile <名> \| --file <路径>]` | 用 AI 解读启动失败:默认解读最近一次失败记录,`--profile` 现场试启并解读,`--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 [--file <路径>]
|
|
102
|
+
- **`dsh-safe explain [--profile <名> | --file <路径>] [-- <dsh 参数…>]`**:默认解读最近一次启动失败(失败时 stderr 自动持久化到 `$DSH_HOME/dsh-safe/last-failure-<profile>.log`,人也可直接翻阅);`--profile` 现场试启该 profile 并解读(60 秒超时,`--` 之后可透传 dsh 启动参数,如 `-- --port 3084`);`--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
|
@@ -11,8 +11,12 @@
|
|
|
11
11
|
* 的输出必须由调用方经 matchFailures 对照真实 patch 行后才生效。
|
|
12
12
|
* 发送前脱敏:用户 home 目录路径替换为 ~。
|
|
13
13
|
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
14
16
|
import { homedir } from 'node:os'
|
|
15
|
-
import {
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { dshHome, resolveDshSpawnTarget } from './dshpaths.js'
|
|
19
|
+
import { getLocale, t } from './i18n.js'
|
|
16
20
|
|
|
17
21
|
export const aiEnabled = () => Boolean(process.env.DSH_SAFE_AI_KEY)
|
|
18
22
|
|
|
@@ -104,3 +108,123 @@ export async function detectFailureWithAI(stderr, knownRows) {
|
|
|
104
108
|
return []
|
|
105
109
|
}
|
|
106
110
|
}
|
|
111
|
+
|
|
112
|
+
/** 试启一个 profile 并捕获 stderr(60s 超时;超时留下的部分 stderr 也可解读)。 */
|
|
113
|
+
function bootProfileForExplain(profile, extraArgs, { spawn }) {
|
|
114
|
+
const target = resolveDshSpawnTarget('dsh')
|
|
115
|
+
const { status, stderr } = spawn(target.file, [...target.prefix, '--profile', profile, ...extraArgs], {
|
|
116
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
117
|
+
env: process.env,
|
|
118
|
+
shell: target.shell,
|
|
119
|
+
timeout: 60_000,
|
|
120
|
+
encoding: 'utf8',
|
|
121
|
+
})
|
|
122
|
+
return { code: status ?? 'timeout', stderr: stderr ?? '' }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** $DSH_HOME/dsh-safe/ 下最近修改的 last-failure-*.log。 */
|
|
126
|
+
function findLatestFailureFile() {
|
|
127
|
+
const dir = join(dshHome(), 'dsh-safe')
|
|
128
|
+
let best
|
|
129
|
+
try {
|
|
130
|
+
for (const name of readdirSync(dir)) {
|
|
131
|
+
if (!name.startsWith('last-failure-') || !name.endsWith('.log')) continue
|
|
132
|
+
const path = join(dir, name)
|
|
133
|
+
const mtime = statSync(path).mtimeMs
|
|
134
|
+
if (!best || mtime > best.mtime) best = { path, mtime }
|
|
135
|
+
}
|
|
136
|
+
} catch {}
|
|
137
|
+
return best
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `dsh-safe explain`:解读启动失败。输入优先级:
|
|
142
|
+
* --file <路径> > --profile <名>(现场试启并解读) >
|
|
143
|
+
* 最近一次失败记录(包装启动失败时自动持久化的 last-failure-*.log) >
|
|
144
|
+
* stdin 管道。纯只读——不碰 patch/台账。
|
|
145
|
+
* @param {string[]} args
|
|
146
|
+
* @param {{
|
|
147
|
+
* spawn?: typeof spawnSync,
|
|
148
|
+
* isTTY?: boolean,
|
|
149
|
+
* readStdin?: () => string,
|
|
150
|
+
* log?: (line: string) => void,
|
|
151
|
+
* write?: (line: string) => void,
|
|
152
|
+
* }} [hooks] 可注入(测试)
|
|
153
|
+
* @returns {Promise<number>} 退出码
|
|
154
|
+
*/
|
|
155
|
+
export async function cmdExplain(args, {
|
|
156
|
+
spawn = spawnSync,
|
|
157
|
+
isTTY = process.stdin.isTTY,
|
|
158
|
+
readStdin = () => readFileSync(0, 'utf8'),
|
|
159
|
+
log = (line) => process.stderr.write(`${line}\n`),
|
|
160
|
+
write = (line) => process.stdout.write(`${line}\n`),
|
|
161
|
+
} = {}) {
|
|
162
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
163
|
+
write(t('helpText', {}))
|
|
164
|
+
return 0
|
|
165
|
+
}
|
|
166
|
+
let file
|
|
167
|
+
let profile
|
|
168
|
+
const bootArgs = []
|
|
169
|
+
let passthrough = false
|
|
170
|
+
for (let i = 0; i < args.length; i++) {
|
|
171
|
+
const a = args[i]
|
|
172
|
+
if (passthrough) { bootArgs.push(a); continue }
|
|
173
|
+
if (a === '--') { passthrough = true; continue }
|
|
174
|
+
if (a === '--file') file = args[++i]
|
|
175
|
+
else if (a.startsWith('--file=')) file = args[i].slice('--file='.length)
|
|
176
|
+
else if (a === '--profile') profile = args[++i]
|
|
177
|
+
else if (a.startsWith('--profile=')) profile = args[i].slice('--profile='.length)
|
|
178
|
+
else {
|
|
179
|
+
// 严格解析:未知参数绝不静默吞(否则用户传给 dsh 的意图会被丢弃)
|
|
180
|
+
log(t('explainUnknownArg', { arg: a }))
|
|
181
|
+
return 2
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (bootArgs.length && profile === undefined) {
|
|
185
|
+
log(t('explainArgsNeedProfile'))
|
|
186
|
+
return 2
|
|
187
|
+
}
|
|
188
|
+
if (!aiEnabled()) {
|
|
189
|
+
log(t('aiDisabled'))
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
let input = ''
|
|
193
|
+
if (file !== undefined) {
|
|
194
|
+
try {
|
|
195
|
+
input = readFileSync(file, 'utf8')
|
|
196
|
+
} catch {
|
|
197
|
+
log(t('aiFileUnreadable', { file }))
|
|
198
|
+
return 2
|
|
199
|
+
}
|
|
200
|
+
} else if (profile !== undefined) {
|
|
201
|
+
log(t('explainBooting', { profile }))
|
|
202
|
+
const { code, stderr } = bootProfileForExplain(profile, bootArgs, { spawn })
|
|
203
|
+
if (code === 0 || !stderr?.trim()) {
|
|
204
|
+
log(t('explainBootOk', { profile }))
|
|
205
|
+
return 0
|
|
206
|
+
}
|
|
207
|
+
input = stderr
|
|
208
|
+
} else if (!isTTY) {
|
|
209
|
+
input = readStdin()
|
|
210
|
+
} else {
|
|
211
|
+
const latest = findLatestFailureFile()
|
|
212
|
+
if (!latest) {
|
|
213
|
+
log(t('explainNoSource'))
|
|
214
|
+
return 2
|
|
215
|
+
}
|
|
216
|
+
log(t('explainUsingLast', { file: latest.path }))
|
|
217
|
+
input = readFileSync(latest.path, 'utf8')
|
|
218
|
+
}
|
|
219
|
+
if (!input.trim()) {
|
|
220
|
+
log(t('aiNoInput'))
|
|
221
|
+
return 2
|
|
222
|
+
}
|
|
223
|
+
const answer = await explainFailure(input)
|
|
224
|
+
if (!answer) {
|
|
225
|
+
log(t('aiExplainFailed'))
|
|
226
|
+
return 1
|
|
227
|
+
}
|
|
228
|
+
write(answer)
|
|
229
|
+
return 0
|
|
230
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
* dsh-safe help | --version
|
|
9
9
|
*/
|
|
10
10
|
import { createRequire } from 'node:module'
|
|
11
|
-
import { readFileSync } from 'node:fs'
|
|
12
11
|
import { loadLedger, restoreQuarantine } from './quarantine.js'
|
|
13
12
|
import { runWrapped } from './wrap.js'
|
|
14
13
|
import { cmdUpdate, cmdUpdateAndBoot, maybeNotifySelfUpdate } from './update.js'
|
|
15
14
|
import { cmdRepair } from './repair.js'
|
|
16
15
|
import { printDoctor } from './doctor.js'
|
|
17
|
-
import {
|
|
16
|
+
import { cmdExplain } from './ai.js'
|
|
18
17
|
import { t } from './i18n.js'
|
|
19
18
|
|
|
20
19
|
const require = createRequire(import.meta.url)
|
|
@@ -28,53 +27,6 @@ function printVersion() {
|
|
|
28
27
|
process.stdout.write(`${version}\n`)
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
/**
|
|
32
|
-
* `dsh-safe explain [--file <path>]`:AI 解读一段启动失败的 stderr。
|
|
33
|
-
* 纯只读——不写任何文件;未配置 DSH_SAFE_AI_KEY 时给出启用指引。
|
|
34
|
-
*/
|
|
35
|
-
async function cmdExplain(args) {
|
|
36
|
-
if (args.includes('-h') || args.includes('--help')) {
|
|
37
|
-
printHelp()
|
|
38
|
-
return 0
|
|
39
|
-
}
|
|
40
|
-
let file
|
|
41
|
-
for (let i = 0; i < args.length; i++) {
|
|
42
|
-
if (args[i] === '--file') file = args[++i]
|
|
43
|
-
else if (args[i].startsWith('--file=')) file = args[i].slice('--file='.length)
|
|
44
|
-
}
|
|
45
|
-
if (!aiEnabled()) {
|
|
46
|
-
process.stderr.write(`${t('aiDisabled')}\n`)
|
|
47
|
-
return 1
|
|
48
|
-
}
|
|
49
|
-
let input = ''
|
|
50
|
-
if (file !== undefined) {
|
|
51
|
-
try {
|
|
52
|
-
input = readFileSync(file, 'utf8')
|
|
53
|
-
} catch {
|
|
54
|
-
process.stderr.write(`${t('aiFileUnreadable', { file })}\n`)
|
|
55
|
-
return 2
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
if (process.stdin.isTTY) process.stderr.write(`${t('explainStdinHint')}\n`)
|
|
59
|
-
try {
|
|
60
|
-
input = readFileSync(0, 'utf8')
|
|
61
|
-
} catch {
|
|
62
|
-
input = ''
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
if (!input.trim()) {
|
|
66
|
-
process.stderr.write(`${t('aiNoInput')}\n`)
|
|
67
|
-
return 2
|
|
68
|
-
}
|
|
69
|
-
const answer = await explainFailure(input)
|
|
70
|
-
if (!answer) {
|
|
71
|
-
process.stderr.write(`${t('aiExplainFailed')}\n`)
|
|
72
|
-
return 1
|
|
73
|
-
}
|
|
74
|
-
process.stdout.write(`${answer}\n`)
|
|
75
|
-
return 0
|
|
76
|
-
}
|
|
77
|
-
|
|
78
30
|
/** 解析 `--profile <名>` / `--profile=<名>`,返回 [值, 剩余参数]。 */
|
|
79
31
|
function takeProfile(args) {
|
|
80
32
|
let profile
|
package/lib/dshpaths.js
CHANGED
|
@@ -28,6 +28,9 @@ export const homePatchPath = () => join(dshHome(), 'cordis.patch.yml')
|
|
|
28
28
|
/** dsh-safe 的隔离台账。 */
|
|
29
29
|
export const ledgerFile = () => join(dshHome(), 'dsh-safe', 'quarantine.json')
|
|
30
30
|
|
|
31
|
+
/** 启动失败 stderr 的持久化文件(按 profile 一份,供 explain 默认解读)。 */
|
|
32
|
+
export const lastFailureFile = (profile) => join(dshHome(), 'dsh-safe', `last-failure-${profile}.log`)
|
|
33
|
+
|
|
31
34
|
/** dsh-safe 的更新检查时间戳缓存(每天最多提示一次新版)。 */
|
|
32
35
|
export const updateCheckFile = () => join(dshHome(), 'dsh-safe', 'update-check.json')
|
|
33
36
|
|
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 [--file <路径>] 用 AI
|
|
27
|
+
dsh-safe explain [--profile <名> | --file <路径>] 用 AI 解读启动失败(默认解读最近一次失败)
|
|
28
28
|
dsh-safe repair [id] [--profile <名>] [--to <版本>] [-y] [--dry-run]
|
|
29
29
|
重装/升级被隔离的插件并自动恢复(限模块解析失败类;省略 id 时只有一条记录则直接修)
|
|
30
30
|
dsh-safe help 显示本帮助
|
|
@@ -108,8 +108,14 @@ const ZH = {
|
|
|
108
108
|
aiNoInput: '[dsh-safe] 没有读到任何 stderr 内容:用 --file <路径>,或从 stdin 粘贴后按 Ctrl-D 结束。',
|
|
109
109
|
aiExplainFailed: '[dsh-safe] AI 解读失败(网络或接口异常);未修改任何文件。',
|
|
110
110
|
aiFileUnreadable: '[dsh-safe] 无法读取文件: {file}',
|
|
111
|
-
explainStdinHint: '[dsh-safe] 正在从 stdin 读取:粘贴 stderr 后按 Ctrl-D(EOF)结束;也可以改用 --file <路径>。',
|
|
112
111
|
aiRecovered: '[dsh-safe] AI 兜底识别出 {count} 个可疑坏插件(结果仍走同一隔离管线)',
|
|
112
|
+
explainHint: '[dsh-safe] 可运行 dsh-safe explain 解读这次失败',
|
|
113
|
+
explainBooting: '[dsh-safe] 正在试启 profile {profile}(60 秒超时;不修改任何文件)…',
|
|
114
|
+
explainArgsNeedProfile: '[dsh-safe] 透传的启动参数需要与 --profile 一起使用。',
|
|
115
|
+
explainBootOk: '[dsh-safe] profile {profile} 当前能正常启动,没有失败可解读。',
|
|
116
|
+
explainUsingLast: '[dsh-safe] 解读最近一次失败记录: {file}',
|
|
117
|
+
explainNoSource: '[dsh-safe] 没有可解读的内容。用法:dsh-safe explain [--profile <名> | --file <路径>],或从 stdin 管道输入。\n 包装启动失败时 stderr 会自动存到 $DSH_HOME/dsh-safe/last-failure-<profile>.log。',
|
|
118
|
+
explainUnknownArg: '[dsh-safe] explain 无法识别的参数: {arg}(透传给 dsh 的启动参数请放在 -- 之后,如 -- --port 3084)',
|
|
113
119
|
excludedByList: '[dsh-safe] 按豁免名单跳过 {label}(不自动禁用)',
|
|
114
120
|
doctorSelf: 'dsh-safe {version}',
|
|
115
121
|
doctorDsh: 'dsh {name} {version}',
|
|
@@ -153,7 +159,7 @@ Usage:
|
|
|
153
159
|
re-enable auto-disabled plugins (after a fixed plugin upgrade)
|
|
154
160
|
dsh-safe update [-y] [--to <ver>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
155
161
|
upgrade dsh and dsh-safe itself, auto-restore quarantined plugins
|
|
156
|
-
dsh-safe explain [--file <path>] interpret
|
|
162
|
+
dsh-safe explain [--profile <name> | --file <path>] interpret the last boot failure with AI
|
|
157
163
|
dsh-safe repair [id] [--profile <name>] [--to <ver>] [-y] [--dry-run]
|
|
158
164
|
reinstall/upgrade a quarantined plugin and auto-restore it (module-resolution failures only; omit id when there is exactly one record)
|
|
159
165
|
dsh-safe help show this help
|
|
@@ -240,8 +246,14 @@ Notes:
|
|
|
240
246
|
aiNoInput: '[dsh-safe] no stderr content was read: use --file <path>, or paste to stdin and press Ctrl-D.',
|
|
241
247
|
aiExplainFailed: '[dsh-safe] AI interpretation failed (network or API error); no files were modified.',
|
|
242
248
|
aiFileUnreadable: '[dsh-safe] cannot read file: {file}',
|
|
243
|
-
explainStdinHint: '[dsh-safe] reading from stdin: paste the stderr and press Ctrl-D (EOF) to finish, or use --file <path>.',
|
|
244
249
|
aiRecovered: '[dsh-safe] AI fallback identified {count} suspected broken plugin(s); the same quarantine pipeline applies',
|
|
250
|
+
explainHint: '[dsh-safe] run dsh-safe explain to interpret this failure',
|
|
251
|
+
explainBooting: '[dsh-safe] booting profile {profile} (60s timeout; no files are modified)…',
|
|
252
|
+
explainArgsNeedProfile: '[dsh-safe] passthrough boot args require --profile.',
|
|
253
|
+
explainBootOk: '[dsh-safe] profile {profile} boots fine right now — nothing failed to interpret.',
|
|
254
|
+
explainUsingLast: '[dsh-safe] interpreting the last failure record: {file}',
|
|
255
|
+
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.',
|
|
256
|
+
explainUnknownArg: '[dsh-safe] unrecognized argument for explain: {arg} (put dsh boot args after --, e.g. -- --port 3084)',
|
|
245
257
|
excludedByList: '[dsh-safe] skipped {label} per the exclusion list (never auto-disabled)',
|
|
246
258
|
doctorSelf: 'dsh-safe {version}',
|
|
247
259
|
doctorDsh: 'dsh {name} {version}',
|
package/lib/wrap.js
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* 再启动(见 dshpaths.js),其余平台原样 spawn。
|
|
12
12
|
*/
|
|
13
13
|
import { spawn } from 'node:child_process'
|
|
14
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { dirname } from 'node:path'
|
|
14
16
|
import { summarizeLine, parseFailureReport } from './failures.js'
|
|
15
17
|
import { collectKnownRows, matchFailures } from './knownrows.js'
|
|
16
|
-
import { detectInvocation, resolveDshSpawnTarget } from './dshpaths.js'
|
|
18
|
+
import { detectInvocation, lastFailureFile, resolveDshSpawnTarget } from './dshpaths.js'
|
|
17
19
|
import { writeQuarantine } from './quarantine.js'
|
|
18
20
|
import { loadConfig } from './config.js'
|
|
19
21
|
import { aiEnabled, detectFailureWithAI } from './ai.js'
|
|
@@ -24,6 +26,16 @@ const FIRST_PARTY_PREFIX = '@deepseek-ai/'
|
|
|
24
26
|
|
|
25
27
|
const isFirstParty = (name) => typeof name === 'string' && name.startsWith(FIRST_PARTY_PREFIX)
|
|
26
28
|
|
|
29
|
+
/** 启动失败时把捕获的 stderr 存到 last-failure-<profile>.log(供 explain 默认解读与人工翻阅)。 */
|
|
30
|
+
function persistFailure(profile, stderr) {
|
|
31
|
+
try {
|
|
32
|
+
if (!stderr?.trim()) return
|
|
33
|
+
const file = lastFailureFile(profile)
|
|
34
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
35
|
+
writeFileSync(file, stderr)
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
const aiRecoverEnabled = () => process.env.DSH_SAFE_AI_RECOVER === '1' && aiEnabled()
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -104,6 +116,7 @@ export async function runWrapped(options) {
|
|
|
104
116
|
log(t('noProfile'))
|
|
105
117
|
return code
|
|
106
118
|
}
|
|
119
|
+
if (!dryRun) persistFailure(invocation.profile, stderr)
|
|
107
120
|
const known = collectKnownRows(invocation.profile)
|
|
108
121
|
const report = parseFailureReport(stderr)
|
|
109
122
|
let hits = matchFailures(report, known)
|
|
@@ -128,7 +141,10 @@ export async function runWrapped(options) {
|
|
|
128
141
|
log(t('skipFirstParty', { name: hit.name, id: hit.id }))
|
|
129
142
|
}
|
|
130
143
|
if (!quarantinable.length) {
|
|
131
|
-
if (!firstParty.length)
|
|
144
|
+
if (!firstParty.length) {
|
|
145
|
+
log(t('nothingFound'))
|
|
146
|
+
log(t('explainHint'))
|
|
147
|
+
}
|
|
132
148
|
return code
|
|
133
149
|
}
|
|
134
150
|
if (attempt >= maxRetries) {
|