dsh-maoying 0.1.4 → 0.1.6
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/index.mjs +186 -18
- package/package.json +1 -1
package/index.mjs
CHANGED
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
* 限额 JSON 读体、client.css 现读下发。
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
54
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, rmSync, writeFileSync } from 'node:fs'
|
|
55
|
+
import { createHash } from 'node:crypto'
|
|
55
56
|
import { readFile } from 'node:fs/promises'
|
|
56
57
|
import { createRequire } from 'node:module'
|
|
57
58
|
import os from 'node:os'
|
|
@@ -197,42 +198,186 @@ export function hostRejectsInlineDelegation(version) {
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
/** 宿主包版本:从 <home>/node_modules require('@deepseek-ai/dsh/package.json')。 */
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
201
|
+
/**
|
|
202
|
+
* 裸读 asar 内文件(零依赖,不依赖 Electron 的 asar fs 补丁)。asar 格式:
|
|
203
|
+
* 头部 [0..4]=pickle 定长、[12..16]=头 JSON 字节数,头 JSON 是文件树(每个
|
|
204
|
+
* 文件带 offset/size),文件区从 16+jsonSize 起按 offset 裸存。桌面应用的
|
|
205
|
+
* 运行时进程里 existsSync(asar 内路径) 行为不可靠(实测探针轨迹:existsSync
|
|
206
|
+
* 为真但 readFileSync 拿不到内容),按字节切是唯一确定性读法。
|
|
207
|
+
* @returns {object|null} 解析后的 JSON;任何一步失败返回 null
|
|
208
|
+
*/
|
|
209
|
+
function asarReadJson(asarPath, innerPath) {
|
|
210
|
+
let fd = null
|
|
206
211
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
212
|
+
fd = openSync(asarPath, 'r')
|
|
213
|
+
const head = Buffer.alloc(16)
|
|
214
|
+
if (readSync(fd, head, 0, 16, 0) !== 16) return null
|
|
215
|
+
const jsonSize = head.readUInt32LE(12)
|
|
216
|
+
const header = Buffer.alloc(jsonSize)
|
|
217
|
+
if (readSync(fd, header, 0, jsonSize, 16) !== jsonSize) return null
|
|
218
|
+
let node = JSON.parse(header.toString('utf8'))
|
|
219
|
+
for (const seg of innerPath.split('/')) {
|
|
220
|
+
node = node?.files?.[seg]
|
|
221
|
+
if (!node) return null
|
|
222
|
+
}
|
|
223
|
+
if (node.files || typeof node.offset !== 'string' || typeof node.size !== 'number') return null
|
|
224
|
+
// 桌面壳的 asar 文件区带块对齐位移(实测 +2 字节),头里的 integrity.hash
|
|
225
|
+
// (文件 SHA256)就是权威标尺:读一带余量窗口,滑窗找到哈希命中的精确切片。
|
|
226
|
+
const size = node.size
|
|
227
|
+
const want = typeof node.integrity?.hash === 'string' ? node.integrity.hash : null
|
|
228
|
+
const slack = 64
|
|
229
|
+
const win = Buffer.alloc(size + 2 * slack)
|
|
230
|
+
const base = 16 + jsonSize
|
|
231
|
+
const at = base + Number(node.offset) - slack
|
|
232
|
+
if (readSync(fd, win, 0, win.length, at) !== win.length) return null
|
|
233
|
+
for (let off = 0; off + size <= win.length; off++) {
|
|
234
|
+
if (win[off] !== 0x7b) continue // JSON 必以 { 开头,先粗筛再哈希
|
|
235
|
+
if (want !== null && createHash('sha256').update(win.subarray(off, off + size)).digest('hex') !== want) continue
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(win.toString('utf8', off, off + size))
|
|
238
|
+
} catch { /* 哈希没配或解析失败继续滑 */ }
|
|
239
|
+
}
|
|
240
|
+
return null
|
|
241
|
+
} catch {
|
|
242
|
+
return null
|
|
243
|
+
} finally {
|
|
244
|
+
if (fd !== null) { try { closeSync(fd) } catch { /* 关闭失败无所谓 */ } }
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** 从 start 向上最多 8 层找最近的 @deepseek-ai/dsh package.json,命中记轨迹 */
|
|
249
|
+
function anchorVersion(start, probe, trace) {
|
|
250
|
+
let dir = path.dirname(path.resolve(start))
|
|
251
|
+
for (let i = 0; i < 8 && dir && dir !== path.dirname(dir); i++, dir = path.dirname(dir)) {
|
|
252
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
253
|
+
if (!existsSync(pkgPath)) continue
|
|
254
|
+
try {
|
|
211
255
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
212
256
|
if (pkg?.name === '@deepseek-ai/dsh' && typeof pkg.version === 'string' && pkg.version !== '') {
|
|
257
|
+
trace.push({ probe, hit: pkg.version, via: pkgPath })
|
|
213
258
|
return pkg.version
|
|
214
259
|
}
|
|
260
|
+
} catch { /* 单个坏 package.json 跳过继续走 */ }
|
|
261
|
+
}
|
|
262
|
+
trace.push({ probe, hit: null })
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 宿主版本探测(带轨迹)。五级优先,前四级都锚定「当前进程的真实宿主」,
|
|
268
|
+
* home 全局那份(老 CLI 残留,版本可能差几个大版本)只做最后兜底:
|
|
269
|
+
* ① argv[1]——CLI 下就是 @deepseek-ai/dsh 包内入口;
|
|
270
|
+
* ② 全 argv 扫描——Electron 主进程常见 argv 里带 app 入口或 .asar 路径;
|
|
271
|
+
* ③ app.asar 锚定——桌面应用 execPath 反推 Resources/app.asar/dsh(Electron
|
|
272
|
+
* 主进程 fs 可透读 asar);
|
|
273
|
+
* ④ Electron app.getVersion()——桌面应用版本即其内置 dsh 版本;
|
|
274
|
+
* ⑤ home 全局安装兜底。
|
|
275
|
+
* 轨迹经 trace 数组透出(host-debug 路由用),方便现场诊断每级看到了什么。
|
|
276
|
+
*/
|
|
277
|
+
function collectHostVersion(dshHome, trace) {
|
|
278
|
+
// ① argv[1]
|
|
279
|
+
const argv1 = process.argv?.[1]
|
|
280
|
+
if (typeof argv1 === 'string' && argv1 !== '') {
|
|
281
|
+
const hit = anchorVersion(argv1, 'argv[1]', trace)
|
|
282
|
+
if (hit) return hit
|
|
283
|
+
} else {
|
|
284
|
+
trace.push({ probe: 'argv[1]', hit: null, note: 'argv[1] 缺席' })
|
|
285
|
+
}
|
|
286
|
+
// ② 全 argv 扫描
|
|
287
|
+
let scanned = 0
|
|
288
|
+
for (const arg of process.argv ?? []) {
|
|
289
|
+
if (typeof arg !== 'string' || arg === argv1 || arg.startsWith('-')) continue
|
|
290
|
+
if (!/\.asar($|\/)|\.(mjs|cjs|js)$/.test(arg)) continue
|
|
291
|
+
scanned++
|
|
292
|
+
const hit = anchorVersion(arg, 'argv-scan', trace)
|
|
293
|
+
if (hit) return hit
|
|
294
|
+
}
|
|
295
|
+
if (scanned === 0) trace.push({ probe: 'argv-scan', hit: null, note: '无可锚定的 argv 项' })
|
|
296
|
+
// ③ app.asar 裸读锚定(桌面应用的确定性读法,见 asarReadJson 说明)
|
|
297
|
+
if (process.versions?.electron) {
|
|
298
|
+
const execDir = path.dirname(process.execPath)
|
|
299
|
+
const asarPaths = [
|
|
300
|
+
path.resolve(execDir, '..', 'Resources', 'app.asar'),
|
|
301
|
+
path.resolve(execDir, 'resources', 'app.asar'),
|
|
302
|
+
]
|
|
303
|
+
let sawAsar = false
|
|
304
|
+
for (const asarPath of asarPaths) {
|
|
305
|
+
if (!existsSync(asarPath)) continue
|
|
306
|
+
sawAsar = true
|
|
307
|
+
for (const inner of ['dsh/package.json', 'package.json']) {
|
|
308
|
+
const pkg = asarReadJson(asarPath, inner)
|
|
309
|
+
// 桌面壳内置 dsh 的包名是 @deepseek-ai/dsh-desktop-runtime(实测),CLI
|
|
310
|
+
// 源码包叫 @deepseek-ai/dsh——两者都算命中。
|
|
311
|
+
const okName = pkg?.name === '@deepseek-ai/dsh' || pkg?.name === '@deepseek-ai/dsh-desktop-runtime'
|
|
312
|
+
if (okName && typeof pkg.version === 'string' && pkg.version !== '') {
|
|
313
|
+
trace.push({ probe: 'asar', hit: pkg.version, via: `${asarPath}!${inner}` })
|
|
314
|
+
return pkg.version
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
trace.push({ probe: 'asar', hit: null, note: sawAsar ? 'asar 存在但未读到 @deepseek-ai/dsh' : 'asar 路径不存在' })
|
|
319
|
+
} else {
|
|
320
|
+
trace.push({ probe: 'asar', hit: null, note: '非 Electron 进程' })
|
|
321
|
+
}
|
|
322
|
+
// ③′ 桌面运行时 payload 版本:primary-runtime/runtime.json 的 desktopVersion
|
|
323
|
+
// (真实文件系统,任何上下文可读;比 asar 略弱——payload 可能落后应用壳)
|
|
324
|
+
{
|
|
325
|
+
const execDir = path.dirname(process.execPath)
|
|
326
|
+
const runtimeJsons = (process.argv ?? [])
|
|
327
|
+
.filter((a) => typeof a === 'string' && a !== '' && !a.startsWith('-'))
|
|
328
|
+
.map((a) => path.join(a, 'runtime.json'))
|
|
329
|
+
runtimeJsons.push(path.resolve(execDir, '..', 'Resources', 'runtime', 'primary-runtime', 'runtime.json'))
|
|
330
|
+
for (const rj of runtimeJsons) {
|
|
331
|
+
try {
|
|
332
|
+
if (!existsSync(rj)) continue
|
|
333
|
+
const meta = JSON.parse(readFileSync(rj, 'utf8'))
|
|
334
|
+
const v = meta?.desktopVersion
|
|
335
|
+
if (typeof v === 'string' && v !== '') {
|
|
336
|
+
trace.push({ probe: 'desktop-runtime', hit: v, via: rj })
|
|
337
|
+
return v
|
|
338
|
+
}
|
|
339
|
+
} catch { /* 单个坏文件换下一个 */ }
|
|
215
340
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
//
|
|
341
|
+
trace.push({ probe: 'desktop-runtime', hit: null })
|
|
342
|
+
}
|
|
343
|
+
// ④ Electron app.getVersion()。纯 Node 下 require('electron') 得到路径字符串
|
|
344
|
+
// 或直接找不到,类型守卫天然挡掉。
|
|
219
345
|
try {
|
|
220
346
|
const electron = createRequire(import.meta.url)('electron')
|
|
221
347
|
if (electron && typeof electron === 'object' && typeof electron.app?.getVersion === 'function') {
|
|
222
348
|
const v = electron.app.getVersion()
|
|
223
|
-
|
|
349
|
+
const hit = typeof v === 'string' && v !== '' ? v : null
|
|
350
|
+
trace.push({ probe: 'electron-api', hit })
|
|
351
|
+
if (hit) return hit
|
|
352
|
+
} else {
|
|
353
|
+
trace.push({ probe: 'electron-api', hit: null, note: 'require 结果非 API 对象' })
|
|
224
354
|
}
|
|
225
|
-
} catch {
|
|
226
|
-
|
|
355
|
+
} catch {
|
|
356
|
+
trace.push({ probe: 'electron-api', hit: null, note: 'require 失败' })
|
|
357
|
+
}
|
|
358
|
+
// ⑤ home 全局兜底
|
|
227
359
|
try {
|
|
228
360
|
const req = createRequire(path.join(dshHome, 'node_modules', 'package.json'))
|
|
229
361
|
const pkg = req('@deepseek-ai/dsh/package.json')
|
|
230
|
-
|
|
362
|
+
const hit = typeof pkg?.version === 'string' && pkg.version !== '' ? pkg.version : null
|
|
363
|
+
trace.push({ probe: 'home-global', hit, note: hit ? '兜底命中——home 全局版本可能陈旧' : null })
|
|
364
|
+
return hit
|
|
231
365
|
} catch {
|
|
366
|
+
trace.push({ probe: 'home-global', hit: null })
|
|
232
367
|
return null
|
|
233
368
|
}
|
|
234
369
|
}
|
|
235
370
|
|
|
371
|
+
export function readHostVersion(dshHome) {
|
|
372
|
+
return collectHostVersion(dshHome, [])
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** host-debug 路由用:版本 + 每级探针轨迹 */
|
|
376
|
+
export function readHostVersionDebug(dshHome) {
|
|
377
|
+
const trace = []
|
|
378
|
+
return { version: collectHostVersion(dshHome, trace), trace }
|
|
379
|
+
}
|
|
380
|
+
|
|
236
381
|
// ── 预设源解析 ──────────────────────────────────────────────────────────────
|
|
237
382
|
|
|
238
383
|
/**
|
|
@@ -666,6 +811,29 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
666
811
|
return
|
|
667
812
|
}
|
|
668
813
|
|
|
814
|
+
if (route === 'host-debug' && req.method === 'GET') {
|
|
815
|
+
// 现场诊断:宿主版本五级探针轨迹 + 形状判定。与 client.css 同级信任
|
|
816
|
+
// 围栏(loopback + 可信 Host 才放行),只暴露版本与路径类信息。
|
|
817
|
+
const home = resolveDshHome()
|
|
818
|
+
const dbg = readHostVersionDebug(home)
|
|
819
|
+
let shape = null
|
|
820
|
+
try { shape = composeManagedRows(home, ctx.logger).shape } catch { /* 诊断信息尽力而为 */ }
|
|
821
|
+
json(res, 200, {
|
|
822
|
+
ok: true,
|
|
823
|
+
hostVersion: dbg.version,
|
|
824
|
+
trace: dbg.trace,
|
|
825
|
+
shape,
|
|
826
|
+
runtime: {
|
|
827
|
+
argv: process.argv,
|
|
828
|
+
execPath: process.execPath,
|
|
829
|
+
cwd: process.cwd(),
|
|
830
|
+
electron: process.versions?.electron ?? null,
|
|
831
|
+
node: process.version,
|
|
832
|
+
},
|
|
833
|
+
})
|
|
834
|
+
return
|
|
835
|
+
}
|
|
836
|
+
|
|
669
837
|
if (route === 'workspaces' && req.method === 'GET') {
|
|
670
838
|
// 工作区清单:宿主注册表逐根探测 novel/project.yml,面板顶部
|
|
671
839
|
// 工作区选择器取数(hasNovel 的排前由客户端做,这里保序即可)。
|