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.
Files changed (2) hide show
  1. package/index.mjs +186 -18
  2. 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
- export function readHostVersion(dshHome) {
201
- // ① 运行入口锚定(最真):argv[1] 是宿主自己的入口脚本——CLI 下是
202
- // @deepseek-ai/dsh 包里的 bin/入口,桌面应用下是 app.asar/dsh 内的入口
203
- // (Electron 主进程的 fs 能透读 asar)。向上找最近的 @deepseek-ai/dsh
204
- // package.json,读到的就是当前进程真实版本。home 全局安装的版本可能
205
- // 陈旧(例如 CLI 升级前留下的 rc.7),绝不能只信它。
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
- let dir = path.dirname(path.resolve(process.argv?.[1] ?? ''))
208
- for (let i = 0; i < 8 && dir && dir !== path.dirname(dir); i++, dir = path.dirname(dir)) {
209
- const pkgPath = path.join(dir, 'package.json')
210
- if (!existsSync(pkgPath)) continue
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
- } catch { /* 入口缺失或不可读时落下一档 */ }
217
- // ② Electron 主进程:桌面应用版本即其内置 dsh 的版本。纯 Node 下 require
218
- // ('electron') 得到的是路径字符串(或直接找不到),类型守卫天然挡掉。
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
- if (typeof v === 'string' && v !== '') return v
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 { /* 非 Electron 环境常态 */ }
226
- // ③ 兜底:home 全局安装(老 CLI 链路;可能陈旧,只在①②都拿不到时兜底)
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
- return typeof pkg?.version === 'string' && pkg.version !== '' ? pkg.version : null
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 的排前由客户端做,这里保序即可)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-maoying",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "毛颖 — DSH 网文创作辅助插件:主编 + 五帮手编排(大纲/写手/审校/设定/考据),novel/ 结构化文件为数据库",
5
5
  "type": "module",
6
6
  "license": "MIT",