@objectivex666/dsh-settings-search 1.13.1 → 1.14.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/README.en.md +17 -10
- package/README.md +66 -56
- package/lib/client.js +280 -70
- package/lib/index.js +44 -5
- package/package.json +3 -2
- package/scripts/ai.test.mjs +352 -13
- package/scripts/compat.mjs +269 -0
- package/scripts/log.test.mjs +52 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Harness compatibility audit.
|
|
4
|
+
*
|
|
5
|
+
* This plugin talks to DSH through surfaces that are not part of any public npm
|
|
6
|
+
* API — client service names, Typert Remote descriptors, slot keys, theme
|
|
7
|
+
* tokens, the host Fetch route registry, and the DeepSeek adapter constants.
|
|
8
|
+
* `npm test` cannot see any of those, so run this against the Harness install
|
|
9
|
+
* you are actually targeting:
|
|
10
|
+
*
|
|
11
|
+
* node scripts/compat.mjs # auto-detect the install
|
|
12
|
+
* node scripts/compat.mjs --asar <app.asar> # explicit archive
|
|
13
|
+
* DSH_ASAR=<app.asar> node scripts/compat.mjs # or through the environment
|
|
14
|
+
*
|
|
15
|
+
* The archive is read read-only and nothing is written. The script exits
|
|
16
|
+
* non-zero as soon as an assumption breaks — that moment is exactly when a
|
|
17
|
+
* Harness upgrade needs this plugin adapted.
|
|
18
|
+
*/
|
|
19
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync } from 'node:fs'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
24
|
+
|
|
25
|
+
/** Candidate install locations, most specific first. */
|
|
26
|
+
const DEFAULT_ASAR_CANDIDATES = [
|
|
27
|
+
'E:\\Program Files\\dsh\\resources\\app.asar',
|
|
28
|
+
'C:\\Program Files\\dsh\\resources\\app.asar',
|
|
29
|
+
'/Applications/DeepSeek Harness.app/Contents/Resources/app.asar',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const argOf = (name) => {
|
|
33
|
+
const at = process.argv.indexOf(name)
|
|
34
|
+
return at >= 0 ? process.argv[at + 1] : undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A minimal read-only Electron asar reader: a pickle-framed JSON header with
|
|
39
|
+
* `{ size, offset }` per entry, the payload starting right after the header.
|
|
40
|
+
* @param archive - absolute path to `app.asar`.
|
|
41
|
+
*/
|
|
42
|
+
const openAsar = (archive) => {
|
|
43
|
+
const fd = openSync(archive, 'r')
|
|
44
|
+
const sizeBuf = Buffer.alloc(8)
|
|
45
|
+
readSync(fd, sizeBuf, 0, 8, 0)
|
|
46
|
+
const headerSize = sizeBuf.readUInt32LE(4)
|
|
47
|
+
const headerBuf = Buffer.alloc(headerSize)
|
|
48
|
+
readSync(fd, headerBuf, 0, headerSize, 8)
|
|
49
|
+
const jsonLength = headerBuf.readUInt32LE(4)
|
|
50
|
+
const header = JSON.parse(headerBuf.toString('utf8', 8, 8 + jsonLength))
|
|
51
|
+
const dataStart = 8 + headerSize
|
|
52
|
+
const files = new Map()
|
|
53
|
+
const walk = (node, prefix) => {
|
|
54
|
+
for (const [name, entry] of Object.entries(node.files ?? {})) {
|
|
55
|
+
const path = prefix === '' ? name : `${prefix}/${name}`
|
|
56
|
+
if (entry.files !== undefined) walk(entry, path)
|
|
57
|
+
else files.set(path, { size: entry.size, offset: Number(entry.offset) })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
walk(header, '')
|
|
61
|
+
const reader = {
|
|
62
|
+
archive,
|
|
63
|
+
files,
|
|
64
|
+
has: (path) => files.has(path),
|
|
65
|
+
read(path) {
|
|
66
|
+
const entry = files.get(path)
|
|
67
|
+
if (entry === undefined) throw new Error(`no entry ${JSON.stringify(path)}`)
|
|
68
|
+
const buffer = Buffer.alloc(entry.size)
|
|
69
|
+
readSync(fd, buffer, 0, entry.size, dataStart + entry.offset)
|
|
70
|
+
return buffer.toString('utf8')
|
|
71
|
+
},
|
|
72
|
+
close: () => closeSync(fd),
|
|
73
|
+
}
|
|
74
|
+
return reader
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Locate the archive to audit. */
|
|
78
|
+
const resolveAsar = () => {
|
|
79
|
+
const explicit = argOf('--asar') ?? process.env.DSH_ASAR
|
|
80
|
+
if (explicit !== undefined && explicit !== '') {
|
|
81
|
+
if (!existsSync(explicit)) throw new Error(`archive not found at ${explicit}`)
|
|
82
|
+
return explicit
|
|
83
|
+
}
|
|
84
|
+
const found = DEFAULT_ASAR_CANDIDATES.find((candidate) => existsSync(candidate))
|
|
85
|
+
if (found === undefined) {
|
|
86
|
+
throw new Error('could not locate a Harness install — pass --asar <app.asar> or set DSH_ASAR')
|
|
87
|
+
}
|
|
88
|
+
return found
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const checks = []
|
|
92
|
+
/**
|
|
93
|
+
* Run one compatibility check. A check returns a short detail string, `true`,
|
|
94
|
+
* or `undefined` when it passes, and throws (or returns `false`) when the
|
|
95
|
+
* Harness no longer matches an assumption this plugin makes.
|
|
96
|
+
*/
|
|
97
|
+
const check = (label, run) => {
|
|
98
|
+
try {
|
|
99
|
+
const detail = run()
|
|
100
|
+
checks.push({ label, ok: detail !== false, detail: detail === true || detail === undefined ? '' : String(detail) })
|
|
101
|
+
} catch (error) {
|
|
102
|
+
checks.push({ label, ok: false, detail: String(error?.message ?? error) })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The client-half source of one package, whatever the platform file is named. */
|
|
107
|
+
const clientSourceOf = (archive, name, cache) => {
|
|
108
|
+
if (cache.has(name)) return cache.get(name)
|
|
109
|
+
const candidates = [
|
|
110
|
+
`dsh/node_modules/@deepseek-ai/${name}/lib/client.js`,
|
|
111
|
+
`dsh/node_modules/@deepseek-ai/${name}/lib/client.browser.js`,
|
|
112
|
+
]
|
|
113
|
+
const found = candidates.find((path) => archive.has(path))
|
|
114
|
+
if (found === undefined) throw new Error(`${name} ships no client bundle`)
|
|
115
|
+
const text = archive.read(found)
|
|
116
|
+
cache.set(name, text)
|
|
117
|
+
return text
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Assert one substring, reporting the missing needle. */
|
|
121
|
+
const mustInclude = (text, needle, message) => {
|
|
122
|
+
if (!text.includes(needle)) throw new Error(message ?? `missing ${JSON.stringify(needle)}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Assert one pattern. Bundled client code is minified and re-emitted by every
|
|
127
|
+
* release, so it may quote a string with `"`, `'`, or a template literal
|
|
128
|
+
* without any behavioural change — match the code, not the quoting.
|
|
129
|
+
*/
|
|
130
|
+
const mustMatch = (text, pattern, message) => {
|
|
131
|
+
if (!pattern.test(text)) throw new Error(message ?? `missing ${pattern}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const main = () => {
|
|
135
|
+
const archivePath = resolveAsar()
|
|
136
|
+
const archive = openAsar(archivePath)
|
|
137
|
+
const cache = new Map()
|
|
138
|
+
try {
|
|
139
|
+
const connectionManifest = JSON.parse(archive.read('dsh/node_modules/@deepseek-ai/dsh-client-connection/package.json'))
|
|
140
|
+
const source = readFileSync(join(ROOT, 'lib/client.js'), 'utf8')
|
|
141
|
+
console.log(`Harness: ${archivePath}`)
|
|
142
|
+
console.log(`Version: ${connectionManifest.version}\n`)
|
|
143
|
+
|
|
144
|
+
check('client connection service is provided', () => {
|
|
145
|
+
mustMatch(
|
|
146
|
+
clientSourceOf(archive, 'dsh-client-connection', cache),
|
|
147
|
+
/provide\(\s*(['"`])connection\1/,
|
|
148
|
+
'the connection handle is gone',
|
|
149
|
+
)
|
|
150
|
+
return 'ctx.connection'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
check('Remote namespaces live at remote.<namespace>', () => {
|
|
154
|
+
const gateway = clientSourceOf(archive, 'dsh-api-gateway', cache)
|
|
155
|
+
mustMatch(gateway, /super\(\s*ctx\s*,\s*(['"`])remote\1\s*\)/, 'the `remote` service is gone')
|
|
156
|
+
mustInclude(gateway, 'remote.${namespace}', 'namespace service keys changed')
|
|
157
|
+
return 'remote.<ns>'
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
check('settings/describe answers { namespaces: [{ ns, value }] }', () => {
|
|
161
|
+
const settings = archive.read('dsh/node_modules/@deepseek-ai/dsh-api-settings-controller/lib/typert.remote-client.js')
|
|
162
|
+
mustInclude(settings, "'namespaces'", 'describe value lost `namespaces`')
|
|
163
|
+
mustInclude(settings, "'ns': z.string()", 'namespace view lost `ns`')
|
|
164
|
+
mustInclude(settings, "'value': z.json()", 'namespace view lost `value`')
|
|
165
|
+
return 'ok'
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
check('settings/describe takes no arguments', () => {
|
|
169
|
+
const settings = archive.read('dsh/node_modules/@deepseek-ai/dsh-api-settings-controller/lib/typert.remote-client.js')
|
|
170
|
+
const at = settings.indexOf("'settings/describe':")
|
|
171
|
+
if (at === -1) throw new Error('the settings/describe descriptor is gone')
|
|
172
|
+
if (!/parameters:\s*\[\s*\]/.test(settings.slice(at, at + 900))) throw new Error('settings/describe now takes parameters')
|
|
173
|
+
return 'ok'
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
check('llm/listConfigurableProviders keeps settingsNs/settingsPath', () => {
|
|
177
|
+
const llm = archive.read('dsh/node_modules/@deepseek-ai/dsh-llm/lib/typert.remote-client.js')
|
|
178
|
+
mustInclude(llm, "'settingsNs'", 'rows lost settingsNs')
|
|
179
|
+
mustInclude(llm, "'settingsPath'", 'rows lost settingsPath')
|
|
180
|
+
return 'ok'
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
check('pluginManager inventories keep the fields the fingerprint reads', () => {
|
|
184
|
+
const manager = archive.read('dsh/node_modules/@deepseek-ai/dsh-plugin-manager/lib/typert.remote-client.js')
|
|
185
|
+
for (const field of ["'name'", "'version'", "'enabled'"]) {
|
|
186
|
+
mustInclude(manager, field, `a bundle row lost ${field}`)
|
|
187
|
+
}
|
|
188
|
+
for (const field of ["'entryId'", "'moduleName'"]) {
|
|
189
|
+
mustInclude(manager, field, `a plugin row lost ${field}`)
|
|
190
|
+
}
|
|
191
|
+
return 'name/version/enabled + entryId/moduleName'
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
check('host connection still owns the exact-Fetch route registry', () => {
|
|
195
|
+
const host = archive.read('dsh/node_modules/@deepseek-ai/dsh-client-connection/lib/index.js')
|
|
196
|
+
mustInclude(host, 'registerFetchRoute', 'the host fetch registry is gone')
|
|
197
|
+
mustInclude(host, 'bodyMode === "streaming"', 'streaming exact routes are no longer supported')
|
|
198
|
+
const segment = /const ENDPOINT_SEGMENT_PATTERN = \/\^\[([^\]]+)\]/.exec(host)
|
|
199
|
+
if (segment === null) throw new Error('endpoint validation pattern not found')
|
|
200
|
+
const sample = 'settings-search-log'
|
|
201
|
+
if (!new RegExp(`^[${segment[1]}]+$`).test(sample)) throw new Error(`route segment ${sample} is no longer valid`)
|
|
202
|
+
return `/api/${sample}`
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
check('every theme token this plugin uses exists in the Harness theme', () => {
|
|
206
|
+
const theme = clientSourceOf(archive, 'dsh-client-ui-theme', cache)
|
|
207
|
+
const defined = new Set([...theme.matchAll(/--dsw-[a-z0-9-]+/g)].map((match) => match[0]))
|
|
208
|
+
const used = new Set([...source.matchAll(/--dsw-[a-z0-9-]+/g)].map((match) => match[0]))
|
|
209
|
+
const missing = [...used].filter((token) => !defined.has(token))
|
|
210
|
+
if (missing.length > 0) throw new Error(`missing: ${missing.join(', ')}`)
|
|
211
|
+
return `${used.size} tokens`
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
check('the settings seats this plugin indexes still exist', () => {
|
|
215
|
+
const seats = ['settings.header', 'settings.section', 'settings.plugins.tab', 'settings.general.item']
|
|
216
|
+
const bundles = [...archive.files.keys()]
|
|
217
|
+
.filter((path) => /^dsh\/node_modules\/@deepseek-ai\/dsh-client-ui-[^/]+\/lib\/client(\.browser)?\.js$/.test(path))
|
|
218
|
+
const text = bundles.map((path) => archive.read(path)).join('\n')
|
|
219
|
+
const missing = seats.filter((seat) => !text.includes(`"${seat}"`))
|
|
220
|
+
if (missing.length > 0) throw new Error(`undeclared: ${missing.join(', ')}`)
|
|
221
|
+
return `${seats.length} seats`
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
check('the Plugins inventory still renders the 会话/全局 groups this plugin mines', () => {
|
|
225
|
+
const inventory = clientSourceOf(archive, 'dsh-client-ui-settings-plugin-inventory', cache)
|
|
226
|
+
mustInclude(inventory, '"groupTitle"', 'the group title class hook is gone')
|
|
227
|
+
mustInclude(inventory, '"data-plugin-scope"', 'the group scope attribute is gone')
|
|
228
|
+
return 'groupTitle + data-plugin-scope'
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
check('the DeepSeek adapter constants the import fallback mirrors', () => {
|
|
232
|
+
const deepseek = archive.read('dsh/node_modules/@deepseek-ai/dsh-llm-deepseek/lib/index.js')
|
|
233
|
+
mustInclude(deepseek, '"https://api.deepseek.com/anthropic"', 'PUBLIC_BASE_URL changed')
|
|
234
|
+
mustInclude(deepseek, '"DEEPSEEK_BASE_URL"', 'BASE_URL_ENV changed')
|
|
235
|
+
mustInclude(
|
|
236
|
+
archive.read('dsh/node_modules/@deepseek-ai/dsh-llm-deepseek-api-key/lib/index.js'),
|
|
237
|
+
'"deepseek-official"',
|
|
238
|
+
'the deepseek-official provider id changed',
|
|
239
|
+
)
|
|
240
|
+
mustInclude(
|
|
241
|
+
archive.read('dsh/node_modules/@deepseek-ai/dsh-llm-deepseek-account/lib/index.js'),
|
|
242
|
+
'"deepseek-account"',
|
|
243
|
+
'the deepseek-account provider id changed',
|
|
244
|
+
)
|
|
245
|
+
return 'endpoint + two provider ids'
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
check('the client module loader still accepts this plugin\u2019s bundle format', () => {
|
|
249
|
+
mustInclude(clientSourceOf(archive, 'dsh-client-modules', cache), '__ModuleLoader__', 'the __ModuleLoader__ facade is gone')
|
|
250
|
+
mustInclude(source, 'window.__ModuleLoader__.load(', 'this plugin no longer uses the facade')
|
|
251
|
+
return 'window.__ModuleLoader__.load({ id, factory })'
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
const failed = checks.filter((entry) => !entry.ok)
|
|
255
|
+
const width = Math.max(...checks.map((entry) => entry.label.length))
|
|
256
|
+
for (const entry of checks) {
|
|
257
|
+
console.log(`${entry.ok ? 'PASS' : 'FAIL'} ${entry.label.padEnd(width)} ${entry.detail}`)
|
|
258
|
+
}
|
|
259
|
+
console.log(`\n${checks.length - failed.length}/${checks.length} compatibility checks passed`)
|
|
260
|
+
if (failed.length > 0) {
|
|
261
|
+
console.error('\nThis Harness needs the plugin adapted before it can be trusted here.')
|
|
262
|
+
process.exitCode = 1
|
|
263
|
+
}
|
|
264
|
+
} finally {
|
|
265
|
+
archive.close()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
main()
|
package/scripts/log.test.mjs
CHANGED
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
* that never deletes the active file, and a tail that never starts mid-line.
|
|
9
9
|
*/
|
|
10
10
|
import assert from 'node:assert/strict'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
closeSync, existsSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync,
|
|
13
|
+
rmSync, statSync, writeFileSync,
|
|
14
|
+
} from 'node:fs'
|
|
12
15
|
import { basename, join } from 'node:path'
|
|
13
16
|
import { tmpdir } from 'node:os'
|
|
14
17
|
import test from 'node:test'
|
|
@@ -228,6 +231,31 @@ test('a fresh store reads the count of a file written by an earlier run', () =>
|
|
|
228
231
|
}
|
|
229
232
|
})
|
|
230
233
|
|
|
234
|
+
test('a persisted log is counted in bounded chunks instead of one large allocation', () => {
|
|
235
|
+
const { dir, cleanup } = withStore({ now: () => day('2026-10-06') })
|
|
236
|
+
try {
|
|
237
|
+
const file = join(dir, 'settings-search-2026-10-06.log')
|
|
238
|
+
writeFileSync(file, 'entry\n'.repeat(20000), 'utf8')
|
|
239
|
+
let largestRequest = 0
|
|
240
|
+
let reads = 0
|
|
241
|
+
const io = {
|
|
242
|
+
openSync,
|
|
243
|
+
closeSync,
|
|
244
|
+
readSync(fd, buffer, offset, length, position) {
|
|
245
|
+
largestRequest = Math.max(largestRequest, length)
|
|
246
|
+
reads += 1
|
|
247
|
+
return readSync(fd, buffer, offset, length, position)
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
const reopened = createLogStore({ dir, now: () => day('2026-10-06'), io })
|
|
251
|
+
assert.equal(reopened.count(), 20000)
|
|
252
|
+
assert.ok(largestRequest <= 64 * 1024, `requested ${largestRequest} bytes`)
|
|
253
|
+
assert.ok(reads > 1, 'the file must be streamed across multiple reads')
|
|
254
|
+
} finally {
|
|
255
|
+
cleanup()
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
231
259
|
const send = async (route, method, body, query = '') => {
|
|
232
260
|
const request = new Request(`http://dsh.internal${route.path}${query}`, {
|
|
233
261
|
method,
|
|
@@ -268,7 +296,7 @@ test('the log route serves the tail and accepts append/clear only', async () =>
|
|
|
268
296
|
try {
|
|
269
297
|
assert.equal(route.path, '/api/settings-search-log')
|
|
270
298
|
assert.deepEqual(route.methods, ['GET', 'POST'])
|
|
271
|
-
assert.equal(route.requestBody, '
|
|
299
|
+
assert.equal(route.requestBody, 'streaming')
|
|
272
300
|
|
|
273
301
|
const appended = await send(route, 'POST', { action: 'append', lines: ['hello'] })
|
|
274
302
|
assert.equal(appended.status, 200)
|
|
@@ -290,6 +318,28 @@ test('the log route serves the tail and accepts append/clear only', async () =>
|
|
|
290
318
|
}
|
|
291
319
|
})
|
|
292
320
|
|
|
321
|
+
test('an over-budget request body is rejected before any action runs', async () => {
|
|
322
|
+
let actions = 0
|
|
323
|
+
const route = createLogRoute({
|
|
324
|
+
dir: 'unused',
|
|
325
|
+
file: 'unused',
|
|
326
|
+
ensure() {},
|
|
327
|
+
append() { actions += 1; return 0 },
|
|
328
|
+
count() { actions += 1; return 0 },
|
|
329
|
+
clear() { actions += 1; return true },
|
|
330
|
+
open() { actions += 1; return { failure: undefined, command: 'test' } },
|
|
331
|
+
read() { actions += 1; return { text: '', size: 0, truncated: false } },
|
|
332
|
+
})
|
|
333
|
+
const request = new Request('http://dsh.internal/api/settings-search-log', {
|
|
334
|
+
method: 'POST',
|
|
335
|
+
headers: { 'content-type': 'application/json' },
|
|
336
|
+
body: JSON.stringify({ action: 'append', lines: ['x'.repeat(257 * 1024)] }),
|
|
337
|
+
})
|
|
338
|
+
const response = await route.fetch(request)
|
|
339
|
+
assert.equal(response.status, 400)
|
|
340
|
+
assert.equal(actions, 0)
|
|
341
|
+
})
|
|
342
|
+
|
|
293
343
|
test('the folder launcher uses the OS shell open verb, never a hidden file manager', () => {
|
|
294
344
|
const win = folderLaunchCommand('win32', 'C:\\logs\\dsh-settings-search')
|
|
295
345
|
assert.equal(win.command, 'cmd.exe')
|