dsh-maoying 0.1.5 → 0.1.7

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 +122 -17
  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,6 +198,53 @@ export function hostRejectsInlineDelegation(version) {
197
198
  }
198
199
 
199
200
  /** 宿主包版本:从 <home>/node_modules require('@deepseek-ai/dsh/package.json')。 */
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
211
+ try {
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
+
200
248
  /** 从 start 向上最多 8 层找最近的 @deepseek-ai/dsh package.json,命中记轨迹 */
201
249
  function anchorVersion(start, probe, trace) {
202
250
  let dir = path.dirname(path.resolve(start))
@@ -245,31 +293,53 @@ function collectHostVersion(dshHome, trace) {
245
293
  if (hit) return hit
246
294
  }
247
295
  if (scanned === 0) trace.push({ probe: 'argv-scan', hit: null, note: '无可锚定的 argv 项' })
248
- // ③ app.asar 锚定(仅 Electron 进程内有意义;纯 Node 的 fs 读不了 asar)
296
+ // ③ app.asar 裸读锚定(桌面应用的确定性读法,见 asarReadJson 说明)
249
297
  if (process.versions?.electron) {
250
298
  const execDir = path.dirname(process.execPath)
251
- const candidates = [
252
- path.resolve(execDir, '..', 'Resources', 'app.asar', 'dsh', 'package.json'),
253
- path.resolve(execDir, 'resources', 'app.asar', 'dsh', 'package.json'),
254
- path.resolve(execDir, '..', 'Resources', 'app.asar', 'package.json'),
255
- path.resolve(execDir, 'resources', 'app.asar', 'package.json'),
299
+ const asarPaths = [
300
+ path.resolve(execDir, '..', 'Resources', 'app.asar'),
301
+ path.resolve(execDir, 'resources', 'app.asar'),
256
302
  ]
257
- let probed = false
258
- for (const pkgPath of candidates) {
259
- try {
260
- if (!existsSync(pkgPath)) continue
261
- probed = true
262
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
263
- if (pkg?.name === '@deepseek-ai/dsh' && typeof pkg.version === 'string' && pkg.version !== '') {
264
- trace.push({ probe: 'asar', hit: pkg.version, via: pkgPath })
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}` })
265
314
  return pkg.version
266
315
  }
267
- } catch { /* 单个候选坏掉换下一个 */ }
316
+ }
268
317
  }
269
- if (!probed) trace.push({ probe: 'asar', hit: null, note: 'execPath 反推的 asar 路径均不可读' })
318
+ trace.push({ probe: 'asar', hit: null, note: sawAsar ? 'asar 存在但未读到 @deepseek-ai/dsh' : 'asar 路径不存在' })
270
319
  } else {
271
320
  trace.push({ probe: 'asar', hit: null, note: '非 Electron 进程' })
272
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 { /* 单个坏文件换下一个 */ }
340
+ }
341
+ trace.push({ probe: 'desktop-runtime', hit: null })
342
+ }
273
343
  // ④ Electron app.getVersion()。纯 Node 下 require('electron') 得到路径字符串
274
344
  // 或直接找不到,类型守卫天然挡掉。
275
345
  try {
@@ -748,11 +818,46 @@ export async function registerCreatorRoutes(ctx) {
748
818
  const dbg = readHostVersionDebug(home)
749
819
  let shape = null
750
820
  try { shape = composeManagedRows(home, ctx.logger).shape } catch { /* 诊断信息尽力而为 */ }
821
+ // 预设注册表内窥:每张 preset 的激活诊断只有注册表自己知道,防御式
822
+ // 探测其服务面(方法名在宿主版本间可能变动,全部吞错尽力而为)。
823
+ const jsonSafe = (v, depth = 0) => {
824
+ if (depth > 4) return '…'
825
+ try {
826
+ if (v === null || v === undefined) return v
827
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return v
828
+ if (typeof v === 'function') return `[fn ${v.name || 'anon'}]`
829
+ if (Array.isArray(v)) return v.slice(0, 50).map((x) => jsonSafe(x, depth + 1))
830
+ if (v instanceof Map) return { __map: [...v.keys()].slice(0, 50) }
831
+ if (v instanceof Set) return { __set: [...v].slice(0, 50).map((x) => jsonSafe(x, depth + 1)) }
832
+ const out = {}
833
+ for (const k of Object.keys(v).slice(0, 80)) out[k] = jsonSafe(v[k], depth + 1)
834
+ return out
835
+ } catch { return '[unserializable]' }
836
+ }
837
+ const registryDump = {}
838
+ for (const svcName of ['agent-preset-registry', 'agentPresets', 'presets']) {
839
+ try {
840
+ const svc = typeof ctx.get === 'function' ? ctx.get(svcName) : null
841
+ if (!svc) continue
842
+ const entry = { __type: typeof svc, __keys: Object.keys(svc).slice(0, 60) }
843
+ for (const m of ['list', 'read', 'all', 'definitions', 'inventory', 'entries']) {
844
+ if (typeof svc[m] === 'function') {
845
+ try {
846
+ const r = svc[m]()
847
+ entry[m] = r && typeof r.then === 'function' ? '[async]' : jsonSafe(r)
848
+ } catch (e) { entry[m] = `throw: ${e?.message ?? e}` }
849
+ }
850
+ }
851
+ registryDump[svcName] = entry
852
+ } catch { /* 逐个吞错 */ }
853
+ }
751
854
  json(res, 200, {
752
855
  ok: true,
753
856
  hostVersion: dbg.version,
754
857
  trace: dbg.trace,
755
858
  shape,
859
+ registry: registryDump,
860
+ ctxKeys: (() => { try { return Object.keys(ctx).slice(0, 80) } catch { return [] } })(),
756
861
  runtime: {
757
862
  argv: process.argv,
758
863
  execPath: process.execPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-maoying",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "毛颖 — DSH 网文创作辅助插件:主编 + 五帮手编排(大纲/写手/审校/设定/考据),novel/ 结构化文件为数据库",
5
5
  "type": "module",
6
6
  "license": "MIT",