dsh-reference-session-collector 0.2.1 → 0.2.2

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/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-reference-session-collector",
3
3
  "name": "dsh-reference-session-collector",
4
- "version": "0.2.1",
4
+ "version": "0.2.2",
5
5
  "description": {
6
6
  "en": "Session reference projector (DRS): read-only scan of ~/.dsh/sessions logs, decoded into a private SQLite (segments/replaces/FTS5) with surface flags, recall-pair accounting and a Chinese-capable search face, served via /api/dsh-reference-session-collector. Never writes session files.",
7
7
  "zh": "会话原文投影同步器(DRS):只读扫描 ~/.dsh/sessions 会话日志,解码后投影进自有 SQLite(segments/replaces/FTS5),经 /api/dsh-reference-session-collector 提供 surface 标记、召回对账与中文可用的内容检索面。绝不写会话文件。"
package/lib/index.d.ts CHANGED
@@ -690,12 +690,14 @@ interface DrsHandlerDeps {
690
690
  trustedHosts?: readonly string[];
691
691
  }
692
692
  /**
693
- * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
693
+ * op 分发:GET `?op=…`;POST `?op=sync` 触发后台同步。
694
+ * POST 的 op 只从 **query** 读(`{op:'sync'}` 落进 body 会 400 unknown POST op)。
694
695
  * 未知 op / 缺参 → 400(不留空壳)。
695
696
  */
696
697
  declare function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessage, res: ServerResponse): Promise<void>;
697
698
  /**
698
- * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
699
+ * 插件主体:apply() 立即返回,重活(开库/建 sync/注册路由)推迟到 setImmediate。
700
+ * 宿主插件装载串行同步——apply 不返回则整列插件都装不上,前端一直 Loading plugins…。
699
701
  * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
700
702
  */
701
703
  declare function apply(ctx: Context): void;
package/lib/index.js CHANGED
@@ -2234,7 +2234,8 @@ function parseTail(url) {
2234
2234
  return Math.floor(parsed);
2235
2235
  }
2236
2236
  /**
2237
- * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
2237
+ * op 分发:GET `?op=…`;POST `?op=sync` 触发后台同步。
2238
+ * POST 的 op 只从 **query** 读(`{op:'sync'}` 落进 body 会 400 unknown POST op)。
2238
2239
  * 未知 op / 缺参 → 400(不留空壳)。
2239
2240
  */
2240
2241
  async function handleDrsRequest(deps, req, res) {
@@ -2245,6 +2246,13 @@ async function handleDrsRequest(deps, req, res) {
2245
2246
  });
2246
2247
  return;
2247
2248
  }
2249
+ if (deps.query === void 0) {
2250
+ writeJson(res, 503, {
2251
+ ok: false,
2252
+ error: "initializing"
2253
+ });
2254
+ return;
2255
+ }
2248
2256
  let url;
2249
2257
  try {
2250
2258
  url = new URL(req.url ?? "/api/dsh-reference-session-collector", "http://localhost");
@@ -2277,7 +2285,8 @@ async function handleDrsRequest(deps, req, res) {
2277
2285
  case "status":
2278
2286
  writeJson(res, 200, {
2279
2287
  ok: true,
2280
- ...status(deps.query, deps.sync.lastSyncResult ?? null)
2288
+ ...status(deps.query, deps.sync.lastSyncResult ?? null),
2289
+ syncing: deps.sync.isSyncing
2281
2290
  });
2282
2291
  return;
2283
2292
  case "sessions": {
@@ -2428,7 +2437,8 @@ function safeReadService(ctx, name) {
2428
2437
  }
2429
2438
  }
2430
2439
  /**
2431
- * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
2440
+ * 插件主体:apply() 立即返回,重活(开库/建 sync/注册路由)推迟到 setImmediate。
2441
+ * 宿主插件装载串行同步——apply 不返回则整列插件都装不上,前端一直 Loading plugins…。
2432
2442
  * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
2433
2443
  */
2434
2444
  function apply(ctx) {
@@ -2440,43 +2450,12 @@ function apply(ctx) {
2440
2450
  else console[level]("[dsh-reference-session-collector]", message, ...args);
2441
2451
  } catch {}
2442
2452
  };
2443
- const storeDir = resolveIndexDir();
2444
2453
  let store;
2445
2454
  let query;
2446
- try {
2447
- store = openProjectionStore(storeDir);
2448
- query = createQueryContext(store);
2449
- } catch (error) {
2450
- log("warn", "projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s", String(error?.message ?? error));
2451
- return;
2452
- }
2453
- const sync = new ProjectionSync({
2454
- store: createStorePort(store),
2455
- project: (events, options) => projectSessionEvents(events, {
2456
- dirId: options.dirId,
2457
- ...options.header === void 0 ? {} : { header: options.header },
2458
- ...options.exactCounter === void 0 ? {} : { exactCounter: (text) => {
2459
- const count = options.exactCounter?.(text);
2460
- return count !== void 0 && count.kind === "exact-tokenizer" ? count.tokens : Math.ceil(text.length / 4);
2461
- } }
2462
- }),
2463
- log: (level, message, ...args) => log(level, message, ...args)
2464
- });
2455
+ let sync;
2465
2456
  let bootTimer;
2466
2457
  let disposeInterval = () => {};
2467
- ctx.effect(() => {
2468
- bootTimer = setTimeout(() => {
2469
- sync.syncAll().then((result) => {
2470
- log("info", "boot sync done: updated=%d skipped=%d basis=%s %dms", result.updated, result.skipped, result.basis, result.durationMs);
2471
- }).catch((error) => log("warn", "boot sync failed: %s", String(error?.message ?? error)));
2472
- }, BOOT_SYNC_DELAY_MS);
2473
- disposeInterval = startIntervalSync(sync);
2474
- return () => {
2475
- if (bootTimer !== void 0) clearTimeout(bootTimer);
2476
- disposeInterval();
2477
- store.close();
2478
- };
2479
- }, "dsh-reference-session-collector: projection lifecycle");
2458
+ let registered = false;
2480
2459
  const readStartupFace = () => {
2481
2460
  const direct = safeReadService(ctx, "webStartup") ?? safeReadService(ctx, "webRuntime");
2482
2461
  return direct !== null && typeof direct === "object" ? direct : void 0;
@@ -2489,7 +2468,6 @@ function apply(ctx) {
2489
2468
  ...startup?.trustedHosts !== void 0 ? { trustedHosts: startup.trustedHosts } : {}
2490
2469
  };
2491
2470
  };
2492
- let registered = false;
2493
2471
  const register = (webServer, channel) => {
2494
2472
  if (registered) return;
2495
2473
  try {
@@ -2510,24 +2488,58 @@ function apply(ctx) {
2510
2488
  log("warn", "drs api route registration failed (%s): %s", channel, String(error?.message ?? error));
2511
2489
  }
2512
2490
  };
2513
- const readService = (name) => safeReadService(ctx, name);
2514
- const active = asWebServer(readService("webServer"));
2515
- if (active !== void 0) {
2516
- register(active, "direct");
2517
- if (registered) return;
2518
- }
2519
- try {
2520
- ctx.inject(["webServer"], (injected) => {
2521
- const webServer = asWebServer(injected.webServer);
2522
- if (webServer === void 0) {
2523
- log("warn", "webServer exposes no register() — drs api route not registered (projection sync keeps running)");
2524
- return;
2491
+ ctx.effect(() => {
2492
+ return () => {
2493
+ if (bootTimer !== void 0) clearTimeout(bootTimer);
2494
+ disposeInterval();
2495
+ store?.close();
2496
+ };
2497
+ }, "dsh-reference-session-collector: projection lifecycle");
2498
+ setImmediate(() => {
2499
+ try {
2500
+ store = openProjectionStore(resolveIndexDir());
2501
+ query = createQueryContext(store);
2502
+ sync = new ProjectionSync({
2503
+ store: createStorePort(store),
2504
+ project: (events, options) => projectSessionEvents(events, {
2505
+ dirId: options.dirId,
2506
+ ...options.header === void 0 ? {} : { header: options.header },
2507
+ ...options.exactCounter === void 0 ? {} : { exactCounter: (text) => {
2508
+ const count = options.exactCounter?.(text);
2509
+ return count !== void 0 && count.kind === "exact-tokenizer" ? count.tokens : Math.ceil(text.length / 4);
2510
+ } }
2511
+ }),
2512
+ log: (level, message, ...args) => log(level, message, ...args)
2513
+ });
2514
+ bootTimer = setTimeout(() => {
2515
+ sync.syncAll().then((result) => {
2516
+ log("info", "boot sync done: updated=%d skipped=%d basis=%s %dms", result.updated, result.skipped, result.basis, result.durationMs);
2517
+ }).catch((error) => log("warn", "boot sync failed: %s", String(error?.message ?? error)));
2518
+ }, BOOT_SYNC_DELAY_MS);
2519
+ disposeInterval = startIntervalSync(sync);
2520
+ const readService = (name) => safeReadService(ctx, name);
2521
+ const active = asWebServer(readService("webServer"));
2522
+ if (active !== void 0) {
2523
+ register(active, "direct");
2524
+ if (registered) return;
2525
2525
  }
2526
- register(webServer, "inject");
2527
- });
2528
- } catch (error) {
2529
- log("warn", "webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常", String(error?.message ?? error));
2530
- }
2526
+ try {
2527
+ ctx.inject(["webServer"], (injected) => {
2528
+ const webServer = asWebServer(injected.webServer);
2529
+ if (webServer === void 0) {
2530
+ log("warn", "webServer exposes no register() — drs api route not registered (projection sync keeps running)");
2531
+ return;
2532
+ }
2533
+ register(webServer, "inject");
2534
+ });
2535
+ } catch (error) {
2536
+ log("warn", "webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常", String(error?.message ?? error));
2537
+ }
2538
+ log("info", "deferred init done (store opened, sync started, routes registered)");
2539
+ } catch (error) {
2540
+ log("warn", "deferred init failed — projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s", String(error?.message ?? error));
2541
+ }
2542
+ });
2531
2543
  }
2532
2544
  //#endregion
2533
2545
  export { BOOT_SYNC_DELAY_MS, DEFAULT_SYNC_INTERVAL_MS, DISCOVERY_LIMIT, DRS_API_PATH, INJECTED_BLOCK_TAGS, LEDGER_LRU_CAP, LedgerCache, PROJECTION_SCHEMA_VERSION, ProjectionSync, SYNC_INTERVAL_ENV, apply, buildSearchSnippet, classifySessionHeader, createQueryContext, createStorePort, decodeSessionLogBytes, decodeSessionLogFile, discoverSessionLogs, extractSearchEventText, extractTitleFromEvents, generationLogFilename, handleDrsRequest, inject, listSessions, openProjectionStore, projectSessionEvents, readArchivedSessionIds, readSessionGenerations, recallReport, reloadSummaries, resolveDshHome, resolveExactCounter, resolveIndexDir, resolveSyncIntervalMs, sanitizeFtsQuery, search, searchGrouped, segmentForIndex, segmentQueryTerm, sessionDetail, startIntervalSync, status, stripInjectedBlocks, surfaceLedger };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-reference-session-collector",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "tag": "beta",
package/src/index.ts CHANGED
@@ -195,7 +195,8 @@ function parseTail(url: URL): number | undefined {
195
195
  }
196
196
 
197
197
  /**
198
- * op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
198
+ * op 分发:GET `?op=…`;POST `?op=sync` 触发后台同步。
199
+ * POST 的 op 只从 **query** 读(`{op:'sync'}` 落进 body 会 400 unknown POST op)。
199
200
  * 未知 op / 缺参 → 400(不留空壳)。
200
201
  */
201
202
  export async function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessage, res: ServerResponse): Promise<void> {
@@ -203,6 +204,11 @@ export async function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessag
203
204
  writeJson(res, 403, { ok: false, error: 'forbidden' })
204
205
  return
205
206
  }
207
+ // 延迟初始化未完成——投影面不可用(503)。
208
+ if (deps.query === undefined) {
209
+ writeJson(res, 503, { ok: false, error: 'initializing' })
210
+ return
211
+ }
206
212
  let url: URL
207
213
  try {
208
214
  url = new URL(req.url ?? DRS_API_PATH, 'http://localhost')
@@ -227,7 +233,9 @@ export async function handleDrsRequest(deps: DrsHandlerDeps, req: IncomingMessag
227
233
 
228
234
  switch (op) {
229
235
  case 'status': {
230
- writeJson(res, 200, { ok: true, ...status(deps.query, deps.sync.lastSyncResult ?? null) })
236
+ // syncing:真实在跑旗标(单次触发即回、后台跑完才落 last_sync),
237
+ // 消费方(dsh-search-index 深度同步)靠它判断慢扫何时落定。
238
+ writeJson(res, 200, { ok: true, ...status(deps.query, deps.sync.lastSyncResult ?? null), syncing: deps.sync.isSyncing })
231
239
  return
232
240
  }
233
241
  case 'sessions': {
@@ -345,7 +353,8 @@ function safeReadService(ctx: Context, name: string): unknown {
345
353
  }
346
354
 
347
355
  /**
348
- * 插件主体:开库 → 装配 sync(启动延迟首轮 + interval 轮)→ 双通道注册路由。
356
+ * 插件主体:apply() 立即返回,重活(开库/建 sync/注册路由)推迟到 setImmediate。
357
+ * 宿主插件装载串行同步——apply 不返回则整列插件都装不上,前端一直 Loading plugins…。
349
358
  * @param ctx - host 插件上下文(webServer 声明注入;webStartup/webRuntime 回退臂)。
350
359
  */
351
360
  export function apply(ctx: Context): void {
@@ -358,53 +367,13 @@ export function apply(ctx: Context): void {
358
367
  } catch { /* 日志面永不致命 */ }
359
368
  }
360
369
 
361
- const storeDir = resolveIndexDir()
362
- let store: ProjectionStore
363
- let query: QueryContext
364
- try {
365
- store = openProjectionStore(storeDir)
366
- query = createQueryContext(store)
367
- } catch (error) {
368
- log('warn', 'projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s',
369
- String((error as Error)?.message ?? error))
370
- return
371
- }
372
-
373
- const sync = new ProjectionSync({
374
- store: createStorePort(store),
375
- project: (events, options) => projectSessionEvents(events, {
376
- dirId: options.dirId,
377
- ...(options.header === undefined ? {} : { header: options.header }),
378
- // 端口计数器返 TokenCount;投影只需数值臂(CCI verify 同款降级)。
379
- ...(options.exactCounter === undefined ? {} : {
380
- exactCounter: (text: string): number => {
381
- const count = options.exactCounter?.(text)
382
- return count !== undefined && count.kind === 'exact-tokenizer'
383
- ? count.tokens
384
- : Math.ceil(text.length / 4)
385
- },
386
- }),
387
- }),
388
- log: (level, message, ...args) => log(level, message, ...args),
389
- })
390
-
391
- // 生命周期:dispose 清定时器 + 关库。
370
+ // 延迟初始化状态(setImmediate 臂赋值;请求面兜底 503)。
371
+ let store: ProjectionStore | undefined
372
+ let query: QueryContext | undefined
373
+ let sync: ProjectionSync | undefined
392
374
  let bootTimer: ReturnType<typeof setTimeout> | undefined
393
375
  let disposeInterval: () => void = () => {}
394
- ctx.effect(() => {
395
- bootTimer = setTimeout(() => {
396
- void sync.syncAll().then(result => {
397
- log('info', 'boot sync done: updated=%d skipped=%d basis=%s %dms',
398
- result.updated, result.skipped, result.basis, result.durationMs)
399
- }).catch((error: unknown) => log('warn', 'boot sync failed: %s', String((error as Error)?.message ?? error)))
400
- }, BOOT_SYNC_DELAY_MS)
401
- disposeInterval = startIntervalSync(sync)
402
- return () => {
403
- if (bootTimer !== undefined) clearTimeout(bootTimer)
404
- disposeInterval()
405
- store.close()
406
- }
407
- }, 'dsh-reference-session-collector: projection lifecycle')
376
+ let registered = false
408
377
 
409
378
  // trustedHosts best-effort(双名回退链,属性/get 两层 safe;缺席=放行)。
410
379
  const readStartupFace = (): WebStartupLike | undefined => {
@@ -415,13 +384,12 @@ export function apply(ctx: Context): void {
415
384
  const depsFor = (): DrsHandlerDeps => {
416
385
  const startup = readStartupFace()
417
386
  return {
418
- query,
419
- sync,
387
+ query: query!,
388
+ sync: sync!,
420
389
  ...(startup?.trustedHosts !== undefined ? { trustedHosts: startup.trustedHosts } : {}),
421
390
  }
422
391
  }
423
392
 
424
- let registered = false
425
393
  const register = (webServer: WebServerLike, channel: 'direct' | 'inject'): void => {
426
394
  if (registered) return
427
395
  try {
@@ -445,24 +413,72 @@ export function apply(ctx: Context): void {
445
413
  }
446
414
  }
447
415
 
448
- const readService = (name: string): unknown => safeReadService(ctx, name)
416
+ // 生命周期:dispose 清定时器 + 关库(同步登记,cleanup 在 dispose 时执行)。
417
+ ctx.effect(() => {
418
+ return () => {
419
+ if (bootTimer !== undefined) clearTimeout(bootTimer)
420
+ disposeInterval()
421
+ store?.close()
422
+ }
423
+ }, 'dsh-reference-session-collector: projection lifecycle')
449
424
 
450
- const active = asWebServer(readService('webServer'))
451
- if (active !== undefined) {
452
- register(active, 'direct')
453
- if (registered) return
454
- }
425
+ // 延迟初始化:开库/建 sync/注册路由全部推迟,保证 apply 立即返回。
426
+ setImmediate(() => {
427
+ try {
428
+ const storeDir = resolveIndexDir()
429
+ store = openProjectionStore(storeDir)
430
+ query = createQueryContext(store)
455
431
 
456
- try {
457
- ctx.inject(['webServer'], (injected) => {
458
- const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
459
- if (webServer === undefined) {
460
- log('warn', 'webServer exposes no register() — drs api route not registered (projection sync keeps running)')
461
- return
432
+ sync = new ProjectionSync({
433
+ store: createStorePort(store),
434
+ project: (events, options) => projectSessionEvents(events, {
435
+ dirId: options.dirId,
436
+ ...(options.header === undefined ? {} : { header: options.header }),
437
+ ...(options.exactCounter === undefined ? {} : {
438
+ exactCounter: (text: string): number => {
439
+ const count = options.exactCounter?.(text)
440
+ return count !== undefined && count.kind === 'exact-tokenizer'
441
+ ? count.tokens
442
+ : Math.ceil(text.length / 4)
443
+ },
444
+ }),
445
+ }),
446
+ log: (level, message, ...args) => log(level, message, ...args),
447
+ })
448
+
449
+ // 启动延迟首轮 sync + interval 轮。
450
+ bootTimer = setTimeout(() => {
451
+ void sync!.syncAll().then(result => {
452
+ log('info', 'boot sync done: updated=%d skipped=%d basis=%s %dms',
453
+ result.updated, result.skipped, result.basis, result.durationMs)
454
+ }).catch((error: unknown) => log('warn', 'boot sync failed: %s', String((error as Error)?.message ?? error)))
455
+ }, BOOT_SYNC_DELAY_MS)
456
+ disposeInterval = startIntervalSync(sync)
457
+
458
+ // 双通道注册路由。
459
+ const readService = (name: string): unknown => safeReadService(ctx, name)
460
+ const active = asWebServer(readService('webServer'))
461
+ if (active !== undefined) {
462
+ register(active, 'direct')
463
+ if (registered) return
462
464
  }
463
- register(webServer, 'inject')
464
- })
465
- } catch (error) {
466
- log('warn', 'webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常', String((error as Error)?.message ?? error))
467
- }
465
+ try {
466
+ ctx.inject(['webServer'], (injected) => {
467
+ const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
468
+ if (webServer === undefined) {
469
+ log('warn', 'webServer exposes no register() — drs api route not registered (projection sync keeps running)')
470
+ return
471
+ }
472
+ register(webServer, 'inject')
473
+ })
474
+ } catch (error) {
475
+ log('warn', 'webServer inject arm unavailable: %s — HTTP 面缺席,同步本体照常', String((error as Error)?.message ?? error))
476
+ }
477
+
478
+ log('info', 'deferred init done (store opened, sync started, routes registered)')
479
+ } catch (error) {
480
+ log('warn', 'deferred init failed — projection store 不可用(node:sqlite 缺席/目录不可写):插件停用—— %s',
481
+ String((error as Error)?.message ?? error))
482
+ }
483
+ })
468
484
  }