dsh-reference-session-collector 0.1.0 → 0.2.0
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 +1 -1
- package/lib/index.d.ts +50 -15
- package/lib/index.js +444 -61
- package/package.json +3 -2
- package/src/blocks.ts +45 -0
- package/src/index.ts +37 -0
- package/src/redact.ts +279 -0
- package/src/schema.ts +48 -40
- package/src/surface.ts +117 -27
- package/src/sync.ts +20 -3
package/lib/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DatabaseSync } from "node:sqlite";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import zlib from "node:zlib";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
7
|
import { Tokenizer } from "@huggingface/tokenizers";
|
|
8
8
|
//#region src/segment.ts
|
|
9
9
|
/**
|
|
@@ -159,16 +159,56 @@ function parseTurnRange(raw) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
162
|
+
* v5 读侧列集:hash 引用列名对(缺省原文臂 / redact=1 脱敏臂)——
|
|
163
|
+
* 常数字面量拼入 SQL,无注入面(R3「redact=1 切 redacted 列」)。
|
|
164
|
+
*/
|
|
165
|
+
const PLAIN_FACE = {
|
|
166
|
+
textHash: "text_hash",
|
|
167
|
+
searchTextHash: "search_text_hash"
|
|
168
|
+
};
|
|
169
|
+
const REDACTED_FACE = {
|
|
170
|
+
textHash: "redacted_text_hash",
|
|
171
|
+
searchTextHash: "redacted_search_text_hash"
|
|
172
|
+
};
|
|
173
|
+
/** v4 segments DDL 15 列的还原 SELECT(v5 逐字段同形回归地基;列序一致;
|
|
174
|
+
* 取材列对 bt/bs/hb 由 faceJoins 按 redact 臂选定,本模板不感知)。 */
|
|
175
|
+
const DETAIL_COLUMNS = `s.session_id, s.seq, s.turn, s.step, s.kind, s.role, s.time,
|
|
176
|
+
COALESCE(bt.body, '') AS text, s.tokens,
|
|
177
|
+
COALESCE(bs.body, '') AS search_text,
|
|
178
|
+
s.on_surface, s.compressed,
|
|
179
|
+
CASE WHEN s.compressed = 1 THEN COALESCE(hb.body, '') ELSE NULL END AS surface_text,
|
|
180
|
+
s.surface_tokens, s.replaced_by`;
|
|
181
|
+
/** blocks/链头两跳 JOIN 子句(replaced_by 恒带 session_id,撞键防御)。 */
|
|
182
|
+
function faceJoins(face) {
|
|
183
|
+
return `FROM segments s
|
|
184
|
+
LEFT JOIN blocks bt ON bt.hash = s.${face.textHash}
|
|
185
|
+
LEFT JOIN blocks bs ON bs.hash = s.${face.searchTextHash}
|
|
186
|
+
LEFT JOIN segments h ON h.session_id = s.session_id AND h.seq = s.replaced_by AND s.compressed = 1
|
|
187
|
+
LEFT JOIN blocks hb ON hb.hash = h.${face.textHash}`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* segments 明细(v4 逐字段同形还原)。
|
|
191
|
+
* @param opts.turn - `turn=` 区间过滤(`a-b` 闭区间或单值)。
|
|
192
|
+
* @param opts.fields - `'search'` 返检索面字段(time/search_text/on_surface)。
|
|
193
|
+
* @param opts.tail - 尾窗 N:先取 seq 降序 N 行再翻升序(与会话文件真实尾部对拍)。
|
|
194
|
+
* @param opts.redact - 脱敏臂:text/search_text/surface_text 取 redacted_* 列。
|
|
165
195
|
*/
|
|
166
196
|
function sessionDetail(ctx, sessionId, opts = {}) {
|
|
167
197
|
const store = ctx.store;
|
|
168
198
|
const session = store.summaries.get(sessionId);
|
|
199
|
+
const face = opts.redact === true ? REDACTED_FACE : PLAIN_FACE;
|
|
169
200
|
const range = parseTurnRange(opts.turn);
|
|
170
|
-
const
|
|
171
|
-
const
|
|
201
|
+
const tail = typeof opts.tail === "number" && Number.isFinite(opts.tail) && opts.tail > 0 ? Math.floor(opts.tail) : void 0;
|
|
202
|
+
const where = range === void 0 ? "WHERE s.session_id=?" : "WHERE s.session_id=? AND s.turn BETWEEN ? AND ?";
|
|
203
|
+
const params = range === void 0 ? [sessionId] : [
|
|
204
|
+
sessionId,
|
|
205
|
+
range.min,
|
|
206
|
+
range.max
|
|
207
|
+
];
|
|
208
|
+
const order = tail === void 0 ? "ORDER BY s.seq ASC" : "ORDER BY s.seq DESC LIMIT ?";
|
|
209
|
+
if (tail !== void 0) params.push(tail);
|
|
210
|
+
const fetched = store.db.prepare(`SELECT ${DETAIL_COLUMNS} ${faceJoins(face)} ${where} ${order}`).all(...params);
|
|
211
|
+
const rows = tail === void 0 ? fetched : fetched.reverse();
|
|
172
212
|
if (opts.fields !== "search") return {
|
|
173
213
|
session,
|
|
174
214
|
rows
|
|
@@ -194,8 +234,17 @@ function surfaceLedger(ctx, sessionId) {
|
|
|
194
234
|
if (cached !== void 0) return cached;
|
|
195
235
|
const value = {
|
|
196
236
|
session_id: sessionId,
|
|
197
|
-
on_surface: store.db.prepare(
|
|
198
|
-
|
|
237
|
+
on_surface: store.db.prepare(`SELECT s.seq, s.turn, s.step, s.kind, s.role, s.time, s.tokens,
|
|
238
|
+
COALESCE(b.body, '') AS text
|
|
239
|
+
FROM segments s
|
|
240
|
+
LEFT JOIN blocks b ON b.hash = s.text_hash
|
|
241
|
+
WHERE s.session_id=? AND s.on_surface=1 ORDER BY s.seq`).all(sessionId),
|
|
242
|
+
compressed_pointers: store.db.prepare(`SELECT s.seq, s.replaced_by,
|
|
243
|
+
COALESCE(hb.body, '') AS surface_text, s.surface_tokens
|
|
244
|
+
FROM segments s
|
|
245
|
+
LEFT JOIN segments h ON h.session_id = s.session_id AND h.seq = s.replaced_by
|
|
246
|
+
LEFT JOIN blocks hb ON hb.hash = h.text_hash
|
|
247
|
+
WHERE s.session_id=? AND s.compressed=1 ORDER BY s.seq`).all(sessionId)
|
|
199
248
|
};
|
|
200
249
|
ctx.surfaceLru.set(key, value);
|
|
201
250
|
return value;
|
|
@@ -239,7 +288,11 @@ function recallReport(ctx, sessionId) {
|
|
|
239
288
|
ctx.recallLru.set(key, value);
|
|
240
289
|
return value;
|
|
241
290
|
}
|
|
242
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* FTS5 BM25 内容搜索(中英同构切词,走 sanitizeFtsQuery)。
|
|
293
|
+
* v5:fts 仍索引原文(R3);snippet 经 blocks 还原,`redact` 只切 snippet
|
|
294
|
+
* 取材列(redacted_search_text_hash),命中/排序语义两臂一致。
|
|
295
|
+
*/
|
|
243
296
|
function search(ctx, query, opts = {}) {
|
|
244
297
|
const match = sanitizeFtsQuery(query);
|
|
245
298
|
if (match === "") return {
|
|
@@ -249,9 +302,10 @@ function search(ctx, query, opts = {}) {
|
|
|
249
302
|
};
|
|
250
303
|
const limit = Math.min(Math.max(1, Math.floor(opts.limit ?? 20)), 100);
|
|
251
304
|
const sql = `SELECT segments_fts.session_id AS session_id, segments_fts.seq AS seq, segments_fts.kind AS kind,
|
|
252
|
-
bm25(segments_fts) AS rank, substr(
|
|
305
|
+
bm25(segments_fts) AS rank, substr(COALESCE(b.body, ''), 1, 200) AS snippet, s.time, s.on_surface, s.turn
|
|
253
306
|
FROM segments_fts
|
|
254
307
|
JOIN segments s ON s.session_id = segments_fts.session_id AND s.seq = segments_fts.seq
|
|
308
|
+
LEFT JOIN blocks b ON b.hash = s.${(opts.redact === true ? REDACTED_FACE : PLAIN_FACE).searchTextHash}
|
|
255
309
|
WHERE segments_fts MATCH ? ${opts.session !== void 0 && opts.session !== "" ? "AND segments_fts.session_id = ?" : ""}
|
|
256
310
|
ORDER BY rank LIMIT ?`;
|
|
257
311
|
const stmt = ctx.store.db.prepare(sql);
|
|
@@ -297,6 +351,7 @@ function buildSearchSnippet(text, query, max = SEARCH_SNIPPET_CHARS) {
|
|
|
297
351
|
* 会话分组全文搜索(op=search&group=session):BM25 候选窗 → 按会话取
|
|
298
352
|
* 加权最强命中 → 排序分页。数学/视界与 steward engine.search 逐字同构:
|
|
299
353
|
* score = −rank × weight(type),时间排序键是会话 updated_at(非命中 time)。
|
|
354
|
+
* snippet 锚定文本经 blocks 还原(redact 臂切 redacted_search_text_hash)。
|
|
300
355
|
*/
|
|
301
356
|
function searchGrouped(ctx, request) {
|
|
302
357
|
const match = sanitizeFtsQuery(request.query);
|
|
@@ -319,27 +374,31 @@ function searchGrouped(ctx, request) {
|
|
|
319
374
|
dateClauses.push("AND sess.last_time_ms < ?");
|
|
320
375
|
dateParams.push(request.to);
|
|
321
376
|
}
|
|
377
|
+
const dateClause = dateClauses.join(" ");
|
|
322
378
|
const sql = `SELECT seg.session_id AS session_id, seg.seq AS seq, seg.kind AS kind, seg.time AS time,
|
|
323
|
-
|
|
379
|
+
COALESCE(b.body, '') AS search_text, bm25(segments_fts) AS rank,
|
|
324
380
|
sess.title AS title, sess.last_time_ms AS updated_at
|
|
325
381
|
FROM segments_fts
|
|
326
382
|
JOIN segments seg ON seg.session_id = segments_fts.session_id AND seg.seq = segments_fts.seq
|
|
383
|
+
LEFT JOIN blocks b ON b.hash = seg.${(request.redact === true ? REDACTED_FACE : PLAIN_FACE).searchTextHash}
|
|
327
384
|
JOIN sessions sess ON sess.session_id = seg.session_id
|
|
328
385
|
WHERE segments_fts MATCH ?
|
|
329
|
-
AND seg.kind IN (${placeholders}) ${surfaceClause} ${archivedClause} ${
|
|
386
|
+
AND seg.kind IN (${placeholders}) ${surfaceClause} ${archivedClause} ${dateClause}
|
|
330
387
|
ORDER BY rank LIMIT ?`;
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
388
|
+
const scored = ctx.store.db.prepare(sql).all(match, ...kinds, ...dateParams, SEARCH_SCAN_LIMIT).map((doc) => ({
|
|
389
|
+
doc,
|
|
390
|
+
score: -Number(doc.rank) * (SEARCH_TYPE_WEIGHT[doc.kind] ?? 1)
|
|
391
|
+
}));
|
|
392
|
+
let grouped;
|
|
393
|
+
if (request.expand === true) grouped = scored;
|
|
394
|
+
else {
|
|
395
|
+
const bestBySession = /* @__PURE__ */ new Map();
|
|
396
|
+
for (const entry of scored) {
|
|
397
|
+
const best = bestBySession.get(entry.doc.session_id);
|
|
398
|
+
if (best === void 0 || entry.score > best.score) bestBySession.set(entry.doc.session_id, entry);
|
|
399
|
+
}
|
|
400
|
+
grouped = [...bestBySession.values()];
|
|
341
401
|
}
|
|
342
|
-
const grouped = [...bestBySession.values()];
|
|
343
402
|
if (request.sortBy === "time") grouped.sort((a, b) => b.doc.updated_at - a.doc.updated_at || b.score - a.score);
|
|
344
403
|
else grouped.sort((a, b) => b.score - a.score);
|
|
345
404
|
const hits = grouped.slice(0, limit).map(({ doc }) => ({
|
|
@@ -357,22 +416,301 @@ function searchGrouped(ctx, request) {
|
|
|
357
416
|
};
|
|
358
417
|
}
|
|
359
418
|
//#endregion
|
|
419
|
+
//#region src/blocks.ts
|
|
420
|
+
/**
|
|
421
|
+
* 字块字典(schema v5,2026-10-20 用户定标「整条一起存相同字块」):
|
|
422
|
+
* segments 的 text/search_text/redacted_* 一律以 sha256 全串等值 hash 入
|
|
423
|
+
* 本字典,行内只存引用;同文=同 hash=单份存储(INSERT OR IGNORE 幂等)。
|
|
424
|
+
*
|
|
425
|
+
* 选型依据(真机实测):surface_text 1768 行仅 23 distinct(99.2%);
|
|
426
|
+
* text 11% / search_text 12% 整段重复;重复组字节跨度大→前缀键/分块切分
|
|
427
|
+
* 会撕裂等值判定,全串 hash 是唯一可靠粒度。hash 用 node:crypto sha256
|
|
428
|
+
* (零新增依赖;弃 blake3)。
|
|
429
|
+
*
|
|
430
|
+
* 纪律:字典只增不删(deleteSession 清行不清块——共享块归属不可知),
|
|
431
|
+
* 投影可重建、体积有上界(语料 distinct 集);GC 属明示不做项。
|
|
432
|
+
*/
|
|
433
|
+
/** blocks 表 DDL(单一事实源:schema.ts 与测试拼库都引本常量)。 */
|
|
434
|
+
const BLOCKS_DDL = `CREATE TABLE IF NOT EXISTS blocks(
|
|
435
|
+
hash TEXT PRIMARY KEY, body TEXT NOT NULL, bytes INTEGER NOT NULL,
|
|
436
|
+
created_at TEXT DEFAULT (datetime('now')));`;
|
|
437
|
+
/** sha256 hex(64 字符)——全串等值键。 */
|
|
438
|
+
function hashOf(text) {
|
|
439
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
440
|
+
}
|
|
441
|
+
var BlocksStore = class {
|
|
442
|
+
insert;
|
|
443
|
+
constructor(db) {
|
|
444
|
+
this.insert = db.prepare("INSERT OR IGNORE INTO blocks(hash, body, bytes) VALUES(?,?,?)");
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 入字典并返回引用 hash;空文本不入典返回 null(hash 列存 NULL,读侧还原
|
|
448
|
+
* 为 ''——「未落行文本」与「空文本」在 v4 就靠此区分,语义不变)。
|
|
449
|
+
* 幂等:同文重复 ensure 只落一份(INSERT OR IGNORE + hash 主键)。
|
|
450
|
+
*/
|
|
451
|
+
ensure(text) {
|
|
452
|
+
if (text === "") return null;
|
|
453
|
+
const hash = hashOf(text);
|
|
454
|
+
this.insert.run(hash, text, Buffer.byteLength(text, "utf8"));
|
|
455
|
+
return hash;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
//#endregion
|
|
459
|
+
//#region src/redact.ts
|
|
460
|
+
const PLACEHOLDER_VALUE_PATTERN = /^\[REDACTED:[A-Z_]+\]$/;
|
|
461
|
+
const SENSITIVE_KEY_SOURCE = String.raw`(?:[A-Za-z_$][A-Za-z0-9_$-]*)?(?:password|passwd|pwd|client[-_$]?secret|consumer[-_$]?secret|api[-_$]?key|access[-_$]?token|refresh[-_$]?token|auth[-_$]?token|id[-_$]?token|private[-_$]?key|secret[-_$]?access[-_$]?key|secret[-_$]?key|credential|secret|token)`;
|
|
462
|
+
const CREDENTIAL_VALUE_SOURCE = String.raw`(?:\[REDACTED:[A-Z_]+\]|\$\{\{[^\r\n]*?\}\}|\{\{[^\r\n]*?\}\}|\$\{[^}\r\n]+\}|\$[A-Za-z_][A-Za-z0-9_]*|"(?:\\.|[^"\\\r\n])*"|'(?:\\.|[^'\\\r\n])*'|[^,;#}\]\)\r\n]+)`;
|
|
463
|
+
/** 安全值 allowlist:命中即放行(示例/占位/环境变量引用/类型名等,脱了只会毁记忆)。 */
|
|
464
|
+
const SAFE_CREDENTIAL_VALUE_PATTERN = /^(?:\[REDACTED:[A-Z_]+\]|\$\{\{[^\r\n]*\}\}|\{\{[^\r\n]*\}\}|\$\{[A-Z_][A-Z0-9_]*\}|\$[A-Z_][A-Z0-9_]*|(?:process\.)?env\.[A-Z_][A-Z0-9_]*|<[^>]+>|--\S+|x{2,}|\*{2,}|\.{3}|sk[_-]x+|example|sample|change-?me|(?:your|replace)[-_][a-z0-9_-]+|string|str|number|boolean|unknown|any|null|undefined|none)$/i;
|
|
465
|
+
const RULES = [
|
|
466
|
+
{
|
|
467
|
+
pattern: /-----BEGIN ((?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED) )?PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----[\s\S]*?(?:-----END \1-----|$)/gi,
|
|
468
|
+
kind: "PRIVATE_KEY",
|
|
469
|
+
priority: 100
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
pattern: /(?:^|[\r\n])[ \t]*(?:Proxy-)?Authorization[ \t]*:[ \t]*([^ \t\r\n][^\r\n]*)/gim,
|
|
473
|
+
group: 1,
|
|
474
|
+
kind: "AUTH_TOKEN",
|
|
475
|
+
priority: 95
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
pattern: /(?:^|[^A-Za-z0-9_-])(?:Bearer|Basic)\s+["']?([A-Za-z0-9._~+/=-]{16,})(?![A-Za-z0-9._~+/=-])/gi,
|
|
479
|
+
group: 1,
|
|
480
|
+
kind: "AUTH_TOKEN",
|
|
481
|
+
priority: 94
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
pattern: /\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{16,}\b/g,
|
|
485
|
+
kind: "AUTH_TOKEN",
|
|
486
|
+
priority: 94
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
pattern: /(?:^|[\r\n])[ \t]*(?:Set-Cookie|Cookie)\s*:\s*([^\r\n]+)/gim,
|
|
490
|
+
group: 1,
|
|
491
|
+
kind: "COOKIE",
|
|
492
|
+
priority: 92
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
pattern: new RegExp(String.raw`(?:^|[\r\n;,{}])[ \t]*(?:-[ \t]+)?(?:(?:export|const|let|var)[ \t]+)*["']?${SENSITIVE_KEY_SOURCE}["']?[ \t]*[:=][ \t]*(${CREDENTIAL_VALUE_SOURCE})`, "gim"),
|
|
496
|
+
group: 1,
|
|
497
|
+
kind: "CREDENTIAL",
|
|
498
|
+
priority: 85,
|
|
499
|
+
accept: (value) => !isSafeCredentialValue(value)
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
pattern: new RegExp(String.raw`--${SENSITIVE_KEY_SOURCE}(?![A-Za-z0-9_$-])(?:[ \t]+|=[ \t]*)(${CREDENTIAL_VALUE_SOURCE})`, "gi"),
|
|
503
|
+
group: 1,
|
|
504
|
+
kind: "CREDENTIAL",
|
|
505
|
+
priority: 85,
|
|
506
|
+
accept: (value) => !isSafeCredentialValue(value)
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
pattern: new RegExp(String.raw`[?&]${SENSITIVE_KEY_SOURCE}(?![A-Za-z0-9_$-])=([^&#\s"'<>]+)`, "gi"),
|
|
510
|
+
group: 1,
|
|
511
|
+
kind: "CREDENTIAL",
|
|
512
|
+
priority: 85,
|
|
513
|
+
accept: (value) => !isSafeCredentialValue(value)
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
pattern: /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:@]*:([^\s/@]+)@/g,
|
|
517
|
+
group: 1,
|
|
518
|
+
kind: "CREDENTIAL",
|
|
519
|
+
priority: 85,
|
|
520
|
+
accept: (value) => !isSafeCredentialValue(value)
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}[A-Za-z0-9]\b/g,
|
|
524
|
+
kind: "API_KEY",
|
|
525
|
+
priority: 80
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
pattern: /\bsk_[A-Za-z0-9_-]{8,}\b/g,
|
|
529
|
+
kind: "API_KEY",
|
|
530
|
+
priority: 80
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g,
|
|
534
|
+
kind: "API_KEY",
|
|
535
|
+
priority: 80
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
|
539
|
+
kind: "API_KEY",
|
|
540
|
+
priority: 80
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
pattern: /\bxox[baprs]-[A-Za-z0-9-]{16,}[A-Za-z0-9]\b/g,
|
|
544
|
+
kind: "API_KEY",
|
|
545
|
+
priority: 80
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
pattern: /\b(?:(?:sk|rk)_live_[A-Za-z0-9]{16,}|whsec_[A-Za-z0-9]{16,})\b/g,
|
|
549
|
+
kind: "API_KEY",
|
|
550
|
+
priority: 80
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
pattern: /\bglpat-[A-Za-z0-9_-]{16,}[A-Za-z0-9]\b/g,
|
|
554
|
+
kind: "API_KEY",
|
|
555
|
+
priority: 80
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
559
|
+
kind: "API_KEY",
|
|
560
|
+
priority: 80
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
pattern: /\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
564
|
+
kind: "API_KEY",
|
|
565
|
+
priority: 80
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
pattern: /(?<![A-Z0-9._%+-])[A-Z0-9._%+-]{1,64}@[A-Z0-9](?:[A-Z0-9.-]{0,251}[A-Z0-9])?\.[A-Z]{2,63}(?![A-Z0-9-]|\.[A-Z0-9])/gi,
|
|
569
|
+
kind: "EMAIL",
|
|
570
|
+
priority: 70
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
pattern: /(?<![A-Za-z0-9])[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}(?![A-Za-z0-9])/g,
|
|
574
|
+
kind: "OPAQUE_ID",
|
|
575
|
+
priority: 60
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
pattern: /(?<![A-Za-z0-9])(?:[0-9A-Fa-f]{64}|[0-9A-Fa-f]{40}|[0-9A-Fa-f]{32})(?![A-Za-z0-9])/g,
|
|
579
|
+
kind: "OPAQUE_ID",
|
|
580
|
+
priority: 60,
|
|
581
|
+
accept: (value) => /[A-Fa-f]/.test(value)
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
pattern: /(?<![A-Za-z0-9])[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g,
|
|
585
|
+
kind: "OPAQUE_ID",
|
|
586
|
+
priority: 60,
|
|
587
|
+
accept: looksLikeHighEntropyOpaqueId
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
pattern: /(?<![A-Za-z0-9.])\+?(?:\d|\(\d)[\d ()-]*\d[Xx]?\)?(?![A-Za-z0-9]|\.\d)/g,
|
|
591
|
+
kind: "LONG_NUMBER",
|
|
592
|
+
priority: 50,
|
|
593
|
+
accept: (value) => {
|
|
594
|
+
if (value.replace(/\D/g, "").length < 9) return false;
|
|
595
|
+
const normalized = value.trim().replace(/[()]/g, "");
|
|
596
|
+
return !/^(?:19|20)\d{2}-\d{1,2}-\d{1,2}(?:[ T-]\d{1,2})?$/.test(normalized) && !/^(?:19|20)\d{2}\s+\d{1,2}\s+\d{1,2}(?:\s+\d{1,2})?$/.test(normalized);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
];
|
|
600
|
+
/**
|
|
601
|
+
* 对文本执行 8 类脱敏。规则各自扫描 → 收集值级 span → 合并重叠 → 重拼插入占位符。
|
|
602
|
+
* 同一位置被多条规则命中时取更长/更高优先级的 span。
|
|
603
|
+
*/
|
|
604
|
+
function redactSecrets(text) {
|
|
605
|
+
if (!text) return {
|
|
606
|
+
text,
|
|
607
|
+
counts: {},
|
|
608
|
+
redacted: false
|
|
609
|
+
};
|
|
610
|
+
const spans = [];
|
|
611
|
+
for (const rule of RULES) collectRuleSpans(text, rule, spans);
|
|
612
|
+
const selected = mergeOverlappingSpans(spans);
|
|
613
|
+
if (selected.length === 0) return {
|
|
614
|
+
text,
|
|
615
|
+
counts: {},
|
|
616
|
+
redacted: false
|
|
617
|
+
};
|
|
618
|
+
const counts = {};
|
|
619
|
+
const output = [];
|
|
620
|
+
let offset = 0;
|
|
621
|
+
for (const span of selected) {
|
|
622
|
+
output.push(text.slice(offset, span.start), `[REDACTED:${span.kind}]`);
|
|
623
|
+
counts[span.kind] = (counts[span.kind] ?? 0) + 1;
|
|
624
|
+
offset = span.end;
|
|
625
|
+
}
|
|
626
|
+
output.push(text.slice(offset));
|
|
627
|
+
return {
|
|
628
|
+
text: output.join(""),
|
|
629
|
+
counts,
|
|
630
|
+
redacted: true
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function collectRuleSpans(text, rule, spans) {
|
|
634
|
+
rule.pattern.lastIndex = 0;
|
|
635
|
+
for (const match of text.matchAll(rule.pattern)) {
|
|
636
|
+
const value = match[rule.group ?? 0];
|
|
637
|
+
if (match.index === void 0 || !value || PLACEHOLDER_VALUE_PATTERN.test(value.trim())) continue;
|
|
638
|
+
if (rule.accept && !rule.accept(value)) continue;
|
|
639
|
+
const relativeStart = match[0].lastIndexOf(value);
|
|
640
|
+
if (relativeStart < 0) continue;
|
|
641
|
+
spans.push({
|
|
642
|
+
start: match.index + relativeStart,
|
|
643
|
+
end: match.index + relativeStart + value.length,
|
|
644
|
+
kind: rule.kind,
|
|
645
|
+
priority: rule.priority
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function isSafeCredentialValue(rawValue) {
|
|
650
|
+
let value = rawValue.trim();
|
|
651
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1).trim();
|
|
652
|
+
return !value || SAFE_CREDENTIAL_VALUE_PATTERN.test(value);
|
|
653
|
+
}
|
|
654
|
+
/** 高熵判定:小写≥2 ∧ 大写≥2 ∧ 数字≥2 ∧ 字符类切换≥4 ∧ 最长连续字母段≤5 ∧ 去重字符≥12。 */
|
|
655
|
+
function looksLikeHighEntropyOpaqueId(value) {
|
|
656
|
+
let lowercase = 0;
|
|
657
|
+
let uppercase = 0;
|
|
658
|
+
let digits = 0;
|
|
659
|
+
let transitions = 0;
|
|
660
|
+
let letterRun = 0;
|
|
661
|
+
let longestLetterRun = 0;
|
|
662
|
+
let previousWasDigit;
|
|
663
|
+
const unique = /* @__PURE__ */ new Set();
|
|
664
|
+
for (const character of value) {
|
|
665
|
+
const isDigit = character >= "0" && character <= "9";
|
|
666
|
+
if (isDigit) {
|
|
667
|
+
digits += 1;
|
|
668
|
+
letterRun = 0;
|
|
669
|
+
} else {
|
|
670
|
+
if (character >= "a" && character <= "z") lowercase += 1;
|
|
671
|
+
else uppercase += 1;
|
|
672
|
+
letterRun += 1;
|
|
673
|
+
longestLetterRun = Math.max(longestLetterRun, letterRun);
|
|
674
|
+
}
|
|
675
|
+
if (previousWasDigit !== void 0 && previousWasDigit !== isDigit) transitions += 1;
|
|
676
|
+
previousWasDigit = isDigit;
|
|
677
|
+
unique.add(character);
|
|
678
|
+
}
|
|
679
|
+
return lowercase >= 2 && uppercase >= 2 && digits >= 2 && transitions >= 4 && longestLetterRun <= 5 && unique.size >= 12;
|
|
680
|
+
}
|
|
681
|
+
function mergeOverlappingSpans(spans) {
|
|
682
|
+
const sorted = [...spans].sort((left, right) => left.start - right.start || right.end - left.end || right.priority - left.priority);
|
|
683
|
+
const merged = [];
|
|
684
|
+
for (const span of sorted) {
|
|
685
|
+
const previous = merged.at(-1);
|
|
686
|
+
if (!previous || span.start >= previous.end) {
|
|
687
|
+
merged.push({ ...span });
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
previous.end = Math.max(previous.end, span.end);
|
|
691
|
+
}
|
|
692
|
+
return merged;
|
|
693
|
+
}
|
|
694
|
+
//#endregion
|
|
360
695
|
//#region src/schema.ts
|
|
361
696
|
/**
|
|
362
|
-
* 投影库装配(spec §DDL):node:sqlite
|
|
363
|
-
* sessions 汇总常驻 Map
|
|
364
|
-
*
|
|
697
|
+
* 投影库装配(spec §DDL):node:sqlite 五表一虚表(sessions/segments/replaces/
|
|
698
|
+
* blocks/meta + segments_fts)+ WAL/mmap 缓存层 1 + sessions 汇总常驻 Map
|
|
699
|
+
* (缓存层 2)。写纪律:每 session 全量重写式替换(事务内 DELETE+INSERT),
|
|
700
|
+
* 幂等;字块一律走 blocks 字典 hash 引用(v5),脱敏派生臂同源生成。
|
|
365
701
|
*
|
|
366
702
|
* 本库只落在 ~/.dsh-reference-session-collector/projection.db ——绝不触碰会话文件。
|
|
367
703
|
*/
|
|
368
704
|
/** schema 版本(meta 表;断裂式迁移的比对基线)。
|
|
369
705
|
* v2=加 sessions.format_generation;
|
|
370
|
-
* v3=search_text
|
|
371
|
-
* v4
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
|
|
706
|
+
* v3=search_text 视界变更(剥离宿主注入样板段);
|
|
707
|
+
* v4=账目双修(data.content 双臂 + replaces.live);
|
|
708
|
+
* v5=字块共享 + 脱敏派生臂(2026-10-20 用户定标):text/search_text/
|
|
709
|
+
* surface_text 实列退役→blocks 字典 hash 引用(surface 经 replaced_by
|
|
710
|
+
* 链派生);新 redacted_text_hash/redacted_search_text_hash(redactSecrets
|
|
711
|
+
* 派生臂,CCI 口径 text 的 token 数学不动);旧库列形状断裂→DROP 重建
|
|
712
|
+
* + VACUUM(投影可重建,丢行无损)。 */
|
|
713
|
+
const PROJECTION_SCHEMA_VERSION = "5";
|
|
376
714
|
const DDL = `
|
|
377
715
|
CREATE TABLE IF NOT EXISTS sessions(
|
|
378
716
|
session_id TEXT PRIMARY KEY, dir_id TEXT NOT NULL, title TEXT DEFAULT '',
|
|
@@ -391,11 +729,12 @@ CREATE TABLE IF NOT EXISTS segments(
|
|
|
391
729
|
session_id TEXT NOT NULL, seq INTEGER NOT NULL, turn INTEGER DEFAULT 0,
|
|
392
730
|
step INTEGER DEFAULT 0, kind TEXT NOT NULL, role TEXT DEFAULT '',
|
|
393
731
|
time INTEGER DEFAULT 0,
|
|
394
|
-
|
|
395
|
-
|
|
732
|
+
text_hash TEXT, tokens INTEGER DEFAULT 0,
|
|
733
|
+
search_text_hash TEXT,
|
|
734
|
+
redacted_text_hash TEXT, redacted_search_text_hash TEXT,
|
|
396
735
|
on_surface INTEGER DEFAULT 1,
|
|
397
736
|
compressed INTEGER DEFAULT 0,
|
|
398
|
-
|
|
737
|
+
surface_tokens INTEGER, replaced_by INTEGER,
|
|
399
738
|
PRIMARY KEY(session_id, seq)) WITHOUT ROWID;
|
|
400
739
|
CREATE INDEX IF NOT EXISTS idx_segments_turn ON segments(session_id, turn, step);
|
|
401
740
|
CREATE TABLE IF NOT EXISTS replaces(
|
|
@@ -410,6 +749,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS segments_fts USING fts5(
|
|
|
410
749
|
index_text, session_id UNINDEXED, seq UNINDEXED, kind UNINDEXED,
|
|
411
750
|
tokenize='unicode61');
|
|
412
751
|
CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT);
|
|
752
|
+
${BLOCKS_DDL}
|
|
413
753
|
`;
|
|
414
754
|
function summaryToRow(s, artifactBytes, artifactMtimeMs, extra) {
|
|
415
755
|
return {
|
|
@@ -451,24 +791,21 @@ PRAGMA cache_size=-32000;
|
|
|
451
791
|
PRAGMA mmap_size=268435456;
|
|
452
792
|
PRAGMA application_id=0x44525343;
|
|
453
793
|
`);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
db.exec("BEGIN");
|
|
460
|
-
try {
|
|
461
|
-
db.prepare("DELETE FROM sessions").run();
|
|
462
|
-
db.prepare("DELETE FROM segments").run();
|
|
463
|
-
db.prepare("DELETE FROM replaces").run();
|
|
464
|
-
db.prepare("DELETE FROM segments_fts").run();
|
|
465
|
-
db.exec("COMMIT");
|
|
466
|
-
} catch (error) {
|
|
467
|
-
db.exec("ROLLBACK");
|
|
468
|
-
throw error;
|
|
469
|
-
}
|
|
794
|
+
let previous;
|
|
795
|
+
try {
|
|
796
|
+
previous = db.prepare("SELECT value FROM meta WHERE key='schema_version'").get();
|
|
797
|
+
} catch {
|
|
798
|
+
previous = void 0;
|
|
470
799
|
}
|
|
471
|
-
|
|
800
|
+
if (previous !== void 0 && previous.value !== "5") db.exec(`DROP TABLE IF EXISTS segments_fts;
|
|
801
|
+
DROP TABLE IF EXISTS segments;
|
|
802
|
+
DROP TABLE IF EXISTS replaces;
|
|
803
|
+
DROP TABLE IF EXISTS sessions;
|
|
804
|
+
DROP TABLE IF EXISTS blocks;
|
|
805
|
+
DROP TABLE IF EXISTS meta`);
|
|
806
|
+
db.exec(DDL);
|
|
807
|
+
if (previous !== void 0 && previous.value !== "5") db.exec("VACUUM");
|
|
808
|
+
db.prepare("INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run("schema_version", "5");
|
|
472
809
|
const summaries = /* @__PURE__ */ new Map();
|
|
473
810
|
const loadAll = () => {
|
|
474
811
|
summaries.clear();
|
|
@@ -477,9 +814,11 @@ PRAGMA application_id=0x44525343;
|
|
|
477
814
|
};
|
|
478
815
|
loadAll();
|
|
479
816
|
const insertSegment = db.prepare(`INSERT INTO segments(
|
|
480
|
-
session_id, seq, turn, step, kind, role, time,
|
|
481
|
-
|
|
482
|
-
|
|
817
|
+
session_id, seq, turn, step, kind, role, time, text_hash, tokens, search_text_hash,
|
|
818
|
+
redacted_text_hash, redacted_search_text_hash,
|
|
819
|
+
on_surface, compressed, surface_tokens, replaced_by)
|
|
820
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`);
|
|
821
|
+
const blocks = new BlocksStore(db);
|
|
483
822
|
const insertReplace = db.prepare(`INSERT INTO replaces(
|
|
484
823
|
session_id, seq, start_seq, end_seq, a_tokens, b_tokens, kind, turn, step, live)
|
|
485
824
|
VALUES(?,?,?,?,?,?,?,?,?,?)`);
|
|
@@ -511,7 +850,11 @@ PRAGMA application_id=0x44525343;
|
|
|
511
850
|
db.prepare("DELETE FROM segments WHERE session_id=?").run(row.session_id);
|
|
512
851
|
db.prepare("DELETE FROM replaces WHERE session_id=?").run(row.session_id);
|
|
513
852
|
db.prepare("DELETE FROM segments_fts WHERE session_id=?").run(row.session_id);
|
|
514
|
-
for (const seg of segments)
|
|
853
|
+
for (const seg of segments) {
|
|
854
|
+
const redText = redactSecrets(seg.text).text;
|
|
855
|
+
const redSearch = redactSecrets(seg.searchText).text;
|
|
856
|
+
insertSegment.run(row.session_id, seg.seq, seg.turn, seg.step, seg.kind, seg.role, seg.time, blocks.ensure(seg.text), seg.tokens, blocks.ensure(seg.searchText), blocks.ensure(redText), blocks.ensure(redSearch), seg.onSurface, seg.compressed, seg.surfaceTokens, seg.replacedBy);
|
|
857
|
+
}
|
|
515
858
|
for (const rep of replaces) insertReplace.run(row.session_id, rep.seq, rep.startSeq, rep.endSeq, rep.aTokens, rep.bTokens, rep.kind, rep.turn, rep.step, rep.live);
|
|
516
859
|
for (const seg of segments) {
|
|
517
860
|
if (seg.searchText === "") continue;
|
|
@@ -1656,6 +1999,8 @@ var ProjectionSync = class {
|
|
|
1656
1999
|
const discovered = discoverSessionLogs(dshHome, limit);
|
|
1657
2000
|
const exact = this.counterMode === "off" ? void 0 : resolveExactCounter();
|
|
1658
2001
|
const basis = exact !== void 0 ? "ds-token" : "chars/4";
|
|
2002
|
+
const dict = this.store.blocksWatermark();
|
|
2003
|
+
const dictEvicted = dict.current < dict.last;
|
|
1659
2004
|
const result = {
|
|
1660
2005
|
ok: true,
|
|
1661
2006
|
busy: false,
|
|
@@ -1673,7 +2018,8 @@ var ProjectionSync = class {
|
|
|
1673
2018
|
this.syncOne(found, {
|
|
1674
2019
|
archivedSet,
|
|
1675
2020
|
exact,
|
|
1676
|
-
result
|
|
2021
|
+
result,
|
|
2022
|
+
dictEvicted
|
|
1677
2023
|
});
|
|
1678
2024
|
} catch (error) {
|
|
1679
2025
|
result.failures.push({
|
|
@@ -1704,6 +2050,7 @@ var ProjectionSync = class {
|
|
|
1704
2050
|
}
|
|
1705
2051
|
}
|
|
1706
2052
|
result.durationMs = Date.now() - startedAt;
|
|
2053
|
+
this.store.setBlocksWatermark(this.store.blocksWatermark().current);
|
|
1707
2054
|
this.lastResult = result;
|
|
1708
2055
|
return result;
|
|
1709
2056
|
} finally {
|
|
@@ -1712,7 +2059,7 @@ var ProjectionSync = class {
|
|
|
1712
2059
|
}
|
|
1713
2060
|
syncOne(found, ctx) {
|
|
1714
2061
|
const record = this.store.getSessionByDir(found.dirSessionId);
|
|
1715
|
-
const statHit = record !== void 0 && record.artifactBytes === found.artifact.bytes && record.artifactMtimeMs === found.artifact.mtimeMs;
|
|
2062
|
+
const statHit = !ctx.dictEvicted && record !== void 0 && record.artifactBytes === found.artifact.bytes && record.artifactMtimeMs === found.artifact.mtimeMs;
|
|
1716
2063
|
if (record !== void 0) {
|
|
1717
2064
|
const archived = ctx.archivedSet.has(record.sessionId) ? 1 : 0;
|
|
1718
2065
|
if (record.archived !== archived) this.store.setSessionArchived(record.sessionId, archived);
|
|
@@ -1810,6 +2157,8 @@ function isTrustedRequest(req, trustedHosts) {
|
|
|
1810
2157
|
if (typeof host !== "string") return false;
|
|
1811
2158
|
return trustedHosts.some((trusted) => trusted.toLowerCase() === host.toLowerCase());
|
|
1812
2159
|
}
|
|
2160
|
+
/** R5 水位在 meta 表的键(DDL 冻结不动,kv 面落)。 */
|
|
2161
|
+
const BLOCKS_WATERMARK_META_KEY = "blocks_watermark";
|
|
1813
2162
|
/** sync 端口 → ProjectionStore 真身适配(dir 水位经常驻 Map 扫描)。 */
|
|
1814
2163
|
function createStorePort(store) {
|
|
1815
2164
|
return {
|
|
@@ -1840,6 +2189,18 @@ function createStorePort(store) {
|
|
|
1840
2189
|
},
|
|
1841
2190
|
deleteSession(sessionId) {
|
|
1842
2191
|
store.deleteSession(sessionId);
|
|
2192
|
+
},
|
|
2193
|
+
blocksWatermark() {
|
|
2194
|
+
const current = Number(store.db.prepare("SELECT COALESCE(MAX(rowid), 0) AS m FROM blocks").get().m);
|
|
2195
|
+
const row = store.db.prepare("SELECT value FROM meta WHERE key=?").get(BLOCKS_WATERMARK_META_KEY);
|
|
2196
|
+
const parsed = row === void 0 ? 0 : Number(row.value);
|
|
2197
|
+
return {
|
|
2198
|
+
current,
|
|
2199
|
+
last: Number.isFinite(parsed) ? parsed : 0
|
|
2200
|
+
};
|
|
2201
|
+
},
|
|
2202
|
+
setBlocksWatermark(value) {
|
|
2203
|
+
store.db.prepare("INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value").run(BLOCKS_WATERMARK_META_KEY, String(value));
|
|
1843
2204
|
}
|
|
1844
2205
|
};
|
|
1845
2206
|
}
|
|
@@ -1859,6 +2220,20 @@ function resolveLatestSessionId(query) {
|
|
|
1859
2220
|
return bestId;
|
|
1860
2221
|
}
|
|
1861
2222
|
/**
|
|
2223
|
+
* v5 参数解析(R3/R4):`redact` 仅 '1' 为真,其余/非法=0(缺省原文臂);
|
|
2224
|
+
* `tail` 正整数生效,非法/≤0=忽略(全量)。
|
|
2225
|
+
*/
|
|
2226
|
+
function parseRedact(url) {
|
|
2227
|
+
return url.searchParams.get("redact") === "1";
|
|
2228
|
+
}
|
|
2229
|
+
function parseTail(url) {
|
|
2230
|
+
const raw = url.searchParams.get("tail");
|
|
2231
|
+
if (raw === null) return void 0;
|
|
2232
|
+
const parsed = Number(raw);
|
|
2233
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return void 0;
|
|
2234
|
+
return Math.floor(parsed);
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
1862
2237
|
* op 分发:GET `?op=…`;POST `{op:'sync'}` 触发后台同步。
|
|
1863
2238
|
* 未知 op / 缺参 → 400(不留空壳)。
|
|
1864
2239
|
*/
|
|
@@ -1924,9 +2299,13 @@ async function handleDrsRequest(deps, req, res) {
|
|
|
1924
2299
|
}
|
|
1925
2300
|
const turn = url.searchParams.get("turn") ?? void 0;
|
|
1926
2301
|
const fields = url.searchParams.get("fields") ?? void 0;
|
|
2302
|
+
const redact = parseRedact(url);
|
|
2303
|
+
const tail = parseTail(url);
|
|
1927
2304
|
const detail = sessionDetail(deps.query, sessionId, {
|
|
1928
2305
|
...turn === void 0 ? {} : { turn },
|
|
1929
|
-
...fields === void 0 ? {} : { fields }
|
|
2306
|
+
...fields === void 0 ? {} : { fields },
|
|
2307
|
+
...tail === void 0 ? {} : { tail },
|
|
2308
|
+
...redact ? { redact: true } : {}
|
|
1930
2309
|
});
|
|
1931
2310
|
if (detail.session === void 0) {
|
|
1932
2311
|
writeJson(res, 200, {
|
|
@@ -1993,6 +2372,7 @@ async function handleDrsRequest(deps, req, res) {
|
|
|
1993
2372
|
});
|
|
1994
2373
|
return;
|
|
1995
2374
|
}
|
|
2375
|
+
const redact = parseRedact(url);
|
|
1996
2376
|
if (url.searchParams.get("group") === "session") {
|
|
1997
2377
|
const kindsRaw = url.searchParams.get("kinds") ?? "";
|
|
1998
2378
|
const archivedRaw = url.searchParams.get("archived");
|
|
@@ -2009,7 +2389,9 @@ async function handleDrsRequest(deps, req, res) {
|
|
|
2009
2389
|
...toRaw !== null && Number.isFinite(Number(toRaw)) ? { to: Number(toRaw) } : {},
|
|
2010
2390
|
...url.searchParams.get("surface") === "0" ? { surfaceOnly: false } : {},
|
|
2011
2391
|
...url.searchParams.get("sort") === "time" ? { sortBy: "time" } : {},
|
|
2012
|
-
...limitRaw !== null && Number.isFinite(Number(limitRaw)) ? { limit: Number(limitRaw) } : {}
|
|
2392
|
+
...limitRaw !== null && Number.isFinite(Number(limitRaw)) ? { limit: Number(limitRaw) } : {},
|
|
2393
|
+
...url.searchParams.get("expand") === "1" ? { expand: true } : {},
|
|
2394
|
+
...redact ? { redact: true } : {}
|
|
2013
2395
|
})
|
|
2014
2396
|
});
|
|
2015
2397
|
return;
|
|
@@ -2019,7 +2401,8 @@ async function handleDrsRequest(deps, req, res) {
|
|
|
2019
2401
|
ok: true,
|
|
2020
2402
|
...search(deps.query, q, {
|
|
2021
2403
|
...sessionId === "" ? {} : { session: sessionId },
|
|
2022
|
-
...limitRaw === null ? {} : { limit: Number(limitRaw) }
|
|
2404
|
+
...limitRaw === null ? {} : { limit: Number(limitRaw) },
|
|
2405
|
+
...redact ? { redact: true } : {}
|
|
2023
2406
|
})
|
|
2024
2407
|
});
|
|
2025
2408
|
return;
|