dsh-reference-session-collector 0.2.1 → 0.2.3
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 +2 -2
- package/lib/index.d.ts +4 -2
- package/lib/index.js +66 -54
- package/package.json +2 -2
- package/src/index.ts +84 -68
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.
|
|
4
|
+
"version": "0.2.3",
|
|
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 标记、召回对账与中文可用的内容检索面。绝不写会话文件。"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"engines": {
|
|
16
|
-
"dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3"
|
|
16
|
+
"dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4"
|
|
17
17
|
},
|
|
18
18
|
"components": {
|
|
19
19
|
"host": "lib/index.js",
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
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.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org",
|
|
6
6
|
"tag": "beta",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
],
|
|
73
73
|
"engines": {
|
|
74
74
|
"node": ">=22",
|
|
75
|
-
"dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3"
|
|
75
|
+
"dsh": "0.1.0-rc.2 || 0.1.0-rc.3 || 0.1.0-rc.6 || 0.1.0-rc.7 || 0.1.0-rc.8 || 0.1.1-rc.1 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.7-rc.1 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.0-rc.2 || 0.2.1-rc.1 || 0.2.1-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.4"
|
|
76
76
|
},
|
|
77
77
|
"license": "MIT",
|
|
78
78
|
"dependencies": {
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
362
|
-
let store: ProjectionStore
|
|
363
|
-
let query: QueryContext
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
425
|
+
// 延迟初始化:开库/建 sync/注册路由全部推迟,保证 apply 立即返回。
|
|
426
|
+
setImmediate(() => {
|
|
427
|
+
try {
|
|
428
|
+
const storeDir = resolveIndexDir()
|
|
429
|
+
store = openProjectionStore(storeDir)
|
|
430
|
+
query = createQueryContext(store)
|
|
455
431
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
}
|