dsh-dsml-rescue 0.2.1 → 0.3.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.md +3 -2
- package/README_zh.md +2 -2
- package/locale/zh.json +1 -1
- package/package.json +7 -1
- package/scripts/doctor.mjs +275 -0
package/README.md
CHANGED
|
@@ -49,6 +49,7 @@ This plugin types it.
|
|
|
49
49
|
## What it does
|
|
50
50
|
|
|
51
51
|
- Continues a stalled turn: DSML markup at the end of the reasoning, and nothing else produced.
|
|
52
|
+
Neither half alone is enough — an empty turn without the markup is a different failure.
|
|
52
53
|
- Sends one short, neutral line — carry on, or wrap up if the work is done.
|
|
53
54
|
- Backs off while you are typing.
|
|
54
55
|
- Gives up after a few attempts instead of burning requests.
|
|
@@ -60,8 +61,8 @@ This plugin types it.
|
|
|
60
61
|
dsh plugin --profile web add dsh-dsml-rescue
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
Then restart `dsh --profile web`. There is nothing to configure — the defaults are the
|
|
64
|
-
behaviour.
|
|
64
|
+
Then restart `dsh --profile web`. There is nothing you need to configure — the defaults are the
|
|
65
|
+
intended behaviour.
|
|
65
66
|
|
|
66
67
|
Working from source: clone, `pnpm install && pnpm run build`, then
|
|
67
68
|
`dsh plugin --profile web add "$PWD"`.
|
package/README_zh.md
CHANGED
|
@@ -45,7 +45,7 @@ DSML(DeepSeek Markup Language)是 DeepSeek 表示工具调用的协议标记
|
|
|
45
45
|
|
|
46
46
|
## 它做什么
|
|
47
47
|
|
|
48
|
-
- 只救空转的轮次:推理过程末尾带着 DSML
|
|
48
|
+
- 只救空转的轮次:推理过程末尾带着 DSML 标记,而且没有产出任何别的内容。两个条件缺一不可——空转但末尾没有标记,是另一种失败。
|
|
49
49
|
- 发一句很短、很中性的话:接着做,或者已经做完就收尾。
|
|
50
50
|
- 你正在打字时它让开。
|
|
51
51
|
- 连着几次没救回来就收手,不会一直烧请求。
|
|
@@ -57,7 +57,7 @@ DSML(DeepSeek Markup Language)是 DeepSeek 表示工具调用的协议标记
|
|
|
57
57
|
dsh plugin --profile web add dsh-dsml-rescue
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
然后重启 `dsh --profile web
|
|
60
|
+
然后重启 `dsh --profile web`。不需要你配置什么,默认值就是预期行为。
|
|
61
61
|
|
|
62
62
|
从源码装:先 clone,`pnpm install && pnpm run build`,再 `dsh plugin --profile web add "$PWD"`。
|
|
63
63
|
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dsml-rescue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Auto-continues turns stalled by DSML tool-call markup leaking into reasoning.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"type": "module",
|
|
24
24
|
"main": "./lib/index.js",
|
|
25
25
|
"types": "./lib/index.d.ts",
|
|
26
|
+
"bin": {
|
|
27
|
+
"dsh-dsml-rescue-doctor": "./scripts/doctor.mjs"
|
|
28
|
+
},
|
|
26
29
|
"exports": {
|
|
27
30
|
".": {
|
|
28
31
|
"types": "./lib/index.d.ts",
|
|
@@ -37,6 +40,7 @@
|
|
|
37
40
|
"icon.svg",
|
|
38
41
|
"locale/*.json",
|
|
39
42
|
"cordis.patch.yml",
|
|
43
|
+
"scripts/doctor.mjs",
|
|
40
44
|
"README.md",
|
|
41
45
|
"LICENSE",
|
|
42
46
|
"README_zh.md"
|
|
@@ -81,6 +85,8 @@
|
|
|
81
85
|
"lint": "eslint .",
|
|
82
86
|
"format": "prettier --write .",
|
|
83
87
|
"format:check": "prettier --check .",
|
|
88
|
+
"doctor": "node scripts/doctor.mjs",
|
|
89
|
+
"check:declaration": "node scripts/check-declaration.mjs",
|
|
84
90
|
"check:release": "node scripts/check-release.mjs",
|
|
85
91
|
"check": "pnpm run typecheck && pnpm run build && pnpm run test && pnpm run check:release"
|
|
86
92
|
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Install check — will this plugin actually run, and is it the code you just wrote?
|
|
4
|
+
*
|
|
5
|
+
* Why this exists, and why it is **not** a runtime warning: an unmounted plugin has no code
|
|
6
|
+
* running, so it cannot report its own absence. The failure is silent by construction. A profile
|
|
7
|
+
* can list the package under `dependencies` and still never load it — the loader only mounts a
|
|
8
|
+
* package whose name is in `dsh.profile.bundles` (or that a profile patch inserts explicitly).
|
|
9
|
+
* A dependency alone contributes no patch layer and produces no diagnostic anywhere.
|
|
10
|
+
*
|
|
11
|
+
* Worse, the obvious repair does not work: re-running `dsh plugin add <same package>` skips
|
|
12
|
+
* packages that are already dependencies, so a lost bundle row is never restored by re-adding.
|
|
13
|
+
* The repair is remove-then-add, and this check prints exactly that.
|
|
14
|
+
*
|
|
15
|
+
* Two groups of checks:
|
|
16
|
+
* - **Mount state** — reads a profile's `package.json` and `cordis.patch.yml`.
|
|
17
|
+
* - **Artifact freshness** — a `link:` install runs `lib/` from this checkout, so a stale build
|
|
18
|
+
* silently runs code that is not in `src/`. Only applies when run from a repository checkout.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* node scripts/doctor.mjs # $DSH_HOME, $DSH_PROFILE, else ~/.dsh + web
|
|
22
|
+
* node scripts/doctor.mjs --profile web --home <DSH_HOME>
|
|
23
|
+
* node scripts/doctor.mjs --json # machine-readable
|
|
24
|
+
*
|
|
25
|
+
* Exit codes: 0 = mounted and current / 1 = not mounted, or the build is stale / 2 = precondition
|
|
26
|
+
* missing (no readable profile manifest).
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-dsml-rescue/doctor
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
32
|
+
import { homedir } from 'node:os'
|
|
33
|
+
import { dirname, join, resolve } from 'node:path'
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
35
|
+
|
|
36
|
+
/** The package this doctor is about; also the bundle-list entry the loader needs. */
|
|
37
|
+
export const PACKAGE = 'dsh-dsml-rescue'
|
|
38
|
+
|
|
39
|
+
/** One line of the report. */
|
|
40
|
+
export const OK = 'ok'
|
|
41
|
+
export const FAIL = 'fail'
|
|
42
|
+
export const NOTE = 'note'
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read one file as UTF-8, or undefined when it is absent or unreadable.
|
|
46
|
+
* @param {string} path - file to read.
|
|
47
|
+
* @returns {string | undefined} the text, or undefined.
|
|
48
|
+
*/
|
|
49
|
+
function readText(path) {
|
|
50
|
+
try {
|
|
51
|
+
return readFileSync(path, 'utf8')
|
|
52
|
+
} catch {
|
|
53
|
+
return undefined
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read and parse a JSON file.
|
|
59
|
+
* @param {string} path - file to read.
|
|
60
|
+
* @returns {{ value?: any, error?: string }} the parsed value, or why it failed.
|
|
61
|
+
*/
|
|
62
|
+
function readJson(path) {
|
|
63
|
+
const text = readText(path)
|
|
64
|
+
if (text === undefined) return { error: `cannot read ${path}` }
|
|
65
|
+
try {
|
|
66
|
+
return { value: JSON.parse(text) }
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { error: `${path} is not valid JSON: ${String(error)}` }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a profile patch inserts this package as a row of its own.
|
|
74
|
+
*
|
|
75
|
+
* A patch row is a second, legitimate way to mount the plugin, so a profile that uses it does not
|
|
76
|
+
* need the bundle-list entry. The check is textual on purpose: reading YAML would mean shipping a
|
|
77
|
+
* parser to answer one question, and a comment mentioning the name must not count as a mount —
|
|
78
|
+
* hence the requirement that the name sits on an `id:`/`name:` line that starts a list item.
|
|
79
|
+
* @param {string | undefined} patchText - the profile's cordis.patch.yml.
|
|
80
|
+
* @param {string} name - package name.
|
|
81
|
+
* @returns {boolean} whether a row mounts it.
|
|
82
|
+
*/
|
|
83
|
+
function patchMounts(patchText, name) {
|
|
84
|
+
if (patchText === undefined) return false
|
|
85
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
86
|
+
const row = new RegExp(`^\\s*-?\\s*(?:id|name)\\s*:\\s*['"]?${escaped}['"]?\\s*$`, 'm')
|
|
87
|
+
return row.test(patchText)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Judge whether the plugin is mounted, from the profile's own two files.
|
|
92
|
+
*
|
|
93
|
+
* Pure apart from nothing: it takes the manifest and the patch text and returns findings, so the
|
|
94
|
+
* rules can be tested without a home directory.
|
|
95
|
+
* @param {{ manifest: any, patchText: string | undefined, name?: string }} input - the profile state.
|
|
96
|
+
* @returns {{ level: string, message: string, hint?: string }[]} findings, most important first.
|
|
97
|
+
*/
|
|
98
|
+
export function inspectProfile({ manifest, patchText, name = PACKAGE }) {
|
|
99
|
+
const findings = []
|
|
100
|
+
const dependencies = manifest?.dependencies ?? {}
|
|
101
|
+
const bundles = manifest?.dsh?.profile?.bundles ?? []
|
|
102
|
+
const inDependencies = Object.hasOwn(dependencies, name)
|
|
103
|
+
const inBundles = Array.isArray(bundles) && bundles.includes(name)
|
|
104
|
+
const viaPatch = patchMounts(patchText, name)
|
|
105
|
+
|
|
106
|
+
if (!inDependencies && !viaPatch) {
|
|
107
|
+
findings.push({
|
|
108
|
+
level: FAIL,
|
|
109
|
+
message: `${name} is not in this profile at all`,
|
|
110
|
+
hint: `install it: dsh plugin --profile <profile> add ${name}`,
|
|
111
|
+
})
|
|
112
|
+
return findings
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (inBundles) {
|
|
116
|
+
findings.push({
|
|
117
|
+
level: OK,
|
|
118
|
+
message: `${name} is in dsh.profile.bundles — the loader mounts it`,
|
|
119
|
+
})
|
|
120
|
+
} else if (viaPatch) {
|
|
121
|
+
findings.push({
|
|
122
|
+
level: OK,
|
|
123
|
+
message: `${name} is mounted by a row in the profile's cordis.patch.yml`,
|
|
124
|
+
hint: 'a patch row is enough; the bundle-list entry is only needed for a bundle install',
|
|
125
|
+
})
|
|
126
|
+
} else {
|
|
127
|
+
findings.push({
|
|
128
|
+
level: FAIL,
|
|
129
|
+
message: `${name} is a dependency but is NOT in dsh.profile.bundles — it will not load`,
|
|
130
|
+
hint: 'reinstalling does not repair this: `add` skips packages that are already dependencies. Run remove, then add.',
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!inDependencies && viaPatch) {
|
|
135
|
+
findings.push({
|
|
136
|
+
level: NOTE,
|
|
137
|
+
message: 'no dependency entry: the loader resolves the row from somewhere else',
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return findings
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether the built artifact predates the newest source file.
|
|
146
|
+
*
|
|
147
|
+
* A `link:` install reads `lib/` straight out of the checkout, so the build is the code that runs.
|
|
148
|
+
* Editing `src/` without rebuilding therefore changes nothing at runtime, with no error anywhere.
|
|
149
|
+
* Reports nothing when either side is absent (an installed copy has no `src/`).
|
|
150
|
+
* @param {string} repoRoot - the package directory.
|
|
151
|
+
* @returns {{ level: string, message: string, hint?: string }[]} findings.
|
|
152
|
+
*/
|
|
153
|
+
export function inspectArtifacts(repoRoot) {
|
|
154
|
+
const libDir = join(repoRoot, 'lib')
|
|
155
|
+
const srcDir = join(repoRoot, 'src')
|
|
156
|
+
if (!existsSync(libDir) || !existsSync(srcDir)) return []
|
|
157
|
+
|
|
158
|
+
/** The newest mtime of every file under `dir` with one of the given extensions. */
|
|
159
|
+
const newest = (dir, extensions) => {
|
|
160
|
+
let time = 0
|
|
161
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
162
|
+
const path = join(dir, entry.name)
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
time = Math.max(time, newest(path, extensions))
|
|
165
|
+
continue
|
|
166
|
+
}
|
|
167
|
+
if (!extensions.some((extension) => entry.name.endsWith(extension))) continue
|
|
168
|
+
try {
|
|
169
|
+
time = Math.max(time, statSync(path).mtimeMs)
|
|
170
|
+
} catch {
|
|
171
|
+
/* a file that vanished mid-scan is not a finding */
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return time
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const built = newest(libDir, ['.js'])
|
|
178
|
+
const source = newest(srcDir, ['.ts'])
|
|
179
|
+
if (built === 0) {
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
level: FAIL,
|
|
183
|
+
message: 'lib/ holds no built JavaScript, so a link install has nothing to load',
|
|
184
|
+
hint: 'run `pnpm run build`',
|
|
185
|
+
},
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
if (source > built) {
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
level: FAIL,
|
|
192
|
+
message: 'src/ is newer than lib/ — a link install runs the stale build',
|
|
193
|
+
hint: 'run `pnpm run build`, then reload the plugin entry or restart the host',
|
|
194
|
+
},
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
return [{ level: OK, message: 'the built artifact is current with src/' }]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the profile directory this run should inspect.
|
|
202
|
+
* @param {{ profile?: string, home?: string }} options - command-line options.
|
|
203
|
+
* @returns {{ profile: string, dir: string, home: string }} the resolved location.
|
|
204
|
+
*/
|
|
205
|
+
export function resolveProfile(options = {}) {
|
|
206
|
+
const home = resolve(options.home ?? process.env['DSH_HOME'] ?? join(homedir(), '.dsh'))
|
|
207
|
+
const profile = options.profile ?? process.env['DSH_PROFILE'] ?? 'web'
|
|
208
|
+
return { profile, dir: join(home, 'profiles', profile), home }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Parse `--key value` and `--key=value` flags. */
|
|
212
|
+
function parseArgs(argv) {
|
|
213
|
+
const options = { json: false }
|
|
214
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
215
|
+
const argument = argv[index] ?? ''
|
|
216
|
+
if (argument === '--json') {
|
|
217
|
+
options.json = true
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
const match = /^--(profile|home)(?:=(.*))?$/.exec(argument)
|
|
221
|
+
if (match === null) continue
|
|
222
|
+
const key = match[1]
|
|
223
|
+
const inline = match[2]
|
|
224
|
+
if (inline !== undefined) options[key] = inline
|
|
225
|
+
else {
|
|
226
|
+
options[key] = argv[index + 1]
|
|
227
|
+
index += 1
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return options
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Run the report. */
|
|
234
|
+
function main() {
|
|
235
|
+
const options = parseArgs(process.argv.slice(2))
|
|
236
|
+
const { profile, dir, home } = resolveProfile(options)
|
|
237
|
+
const manifestPath = join(dir, 'package.json')
|
|
238
|
+
const parsed = readJson(manifestPath)
|
|
239
|
+
if (parsed.error !== undefined) {
|
|
240
|
+
process.stderr.write(`doctor: ${parsed.error}\n`)
|
|
241
|
+
process.stderr.write(`doctor: is the profile name right? (home ${home}, profile ${profile})\n`)
|
|
242
|
+
process.exit(2)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
246
|
+
const findings = [
|
|
247
|
+
...inspectProfile({
|
|
248
|
+
manifest: parsed.value,
|
|
249
|
+
patchText: readText(join(dir, 'cordis.patch.yml')),
|
|
250
|
+
}),
|
|
251
|
+
...inspectArtifacts(repoRoot),
|
|
252
|
+
]
|
|
253
|
+
const failed = findings.some((finding) => finding.level === FAIL)
|
|
254
|
+
|
|
255
|
+
if (options.json) {
|
|
256
|
+
process.stdout.write(
|
|
257
|
+
`${JSON.stringify({ profile, home, mounted: !failed, findings }, undefined, 2)}\n`,
|
|
258
|
+
)
|
|
259
|
+
} else {
|
|
260
|
+
process.stdout.write(`doctor: profile ${profile} (${dir})\n`)
|
|
261
|
+
for (const finding of findings) {
|
|
262
|
+
const mark = finding.level === OK ? 'ok ' : finding.level === FAIL ? 'FAIL' : 'note'
|
|
263
|
+
process.stdout.write(` ${mark} ${finding.message}\n`)
|
|
264
|
+
if (finding.hint !== undefined) process.stdout.write(` ${finding.hint}\n`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
process.exit(failed ? 1 : 0)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (
|
|
271
|
+
process.argv[1] !== undefined &&
|
|
272
|
+
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
|
|
273
|
+
) {
|
|
274
|
+
main()
|
|
275
|
+
}
|