dsh-prime-memory 0.18.2 → 0.19.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.
Files changed (51) hide show
  1. package/CHANGELOG.en.md +21 -0
  2. package/CHANGELOG.ja.md +21 -0
  3. package/CHANGELOG.ko.md +21 -0
  4. package/CHANGELOG.md +21 -0
  5. package/INSTALL.en.md +1 -1
  6. package/INSTALL.ja.md +1 -1
  7. package/INSTALL.ko.md +1 -1
  8. package/INSTALL.md +1 -1
  9. package/README.en.md +7 -2
  10. package/README.ja.md +7 -2
  11. package/README.ko.md +7 -2
  12. package/README.md +7 -2
  13. package/cordis.patch.yml +6 -0
  14. package/dist/client.js +36 -9
  15. package/dist/config.d.ts +37 -0
  16. package/dist/config.js +7 -0
  17. package/dist/contract.d.ts +46 -0
  18. package/dist/hooks/capture-recovery.d.ts +59 -0
  19. package/dist/hooks/capture-recovery.js +88 -0
  20. package/dist/hooks/capture.js +110 -1
  21. package/dist/hooks/recall-ack.d.ts +48 -0
  22. package/dist/hooks/recall-ack.js +79 -0
  23. package/dist/hooks/recall.d.ts +19 -0
  24. package/dist/hooks/recall.js +116 -7
  25. package/dist/index.d.ts +22 -0
  26. package/dist/index.js +4 -0
  27. package/dist/pipeline/l1.d.ts +4 -0
  28. package/dist/pipeline/l1.js +9 -2
  29. package/dist/pipeline/reconcile.js +4 -0
  30. package/dist/pipeline/runner.js +62 -0
  31. package/dist/prompts/boundary.d.ts +17 -0
  32. package/dist/prompts/boundary.js +20 -0
  33. package/dist/prompts/graph-projection.js +4 -1
  34. package/dist/prompts/l1-dedup.d.ts +3 -3
  35. package/dist/prompts/l1-dedup.js +9 -1
  36. package/dist/prompts/l1-extraction.d.ts +3 -3
  37. package/dist/prompts/l1-extraction.js +12 -0
  38. package/dist/prompts/persona.js +6 -0
  39. package/dist/prompts/scene.js +7 -1
  40. package/dist/stats.js +9 -0
  41. package/dist/store/l0.d.ts +4 -0
  42. package/dist/store/l0.js +8 -0
  43. package/dist/store/sqlite.d.ts +12 -0
  44. package/dist/store/sqlite.js +37 -0
  45. package/dist/store/trace.d.ts +39 -0
  46. package/dist/store/trace.js +149 -0
  47. package/dist/tools/index.js +25 -1
  48. package/dist/util/redact.d.ts +27 -0
  49. package/dist/util/redact.js +228 -0
  50. package/dsh.plugin.json +2 -2
  51. package/package.json +23 -22
@@ -0,0 +1,149 @@
1
+ /**
2
+ * §F 结构化追踪(memorax-absorb Wave 4 / task_28)——recall/distill 事件流。
3
+ *
4
+ * 模块级单例(token-cost.ts 同款):trace() 埋点拿不到 store 实例(store 在
5
+ * index.ts 运行时才有 dataDir),initTraceStore 启动时注入;未初始化或
6
+ * trace.enabled=false 时 trace() 为零副作用 no-op。
7
+ *
8
+ * 存储:按天 JSONL(`dataDir/trace/trace-YYYY-MM-DD.jsonl`,appendJsonl 原语),
9
+ * 文件名即日期 → 保留期清理在切日时触发(删除超 retentionDays 的旧文件);
10
+ * 单文件 >5MB 停写 + 写入一行 trace_truncated marker(防无限增长);
11
+ * 写失败静默(filelog 同款纪律——追踪绝不影响主链路)。
12
+ *
13
+ * 隐私默认 metadata-only:recall 事件只带 queryChars+querySha,
14
+ * captureContent=true 才存 queryText(截断 200)——由埋点方决定字段,本模块不感知。
15
+ */
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { appendJsonl, dayKey, readJsonl } from '../util/io.js';
19
+ const TRACE_DIR = 'trace';
20
+ const DEFAULT_MAX_FILE_BYTES = 5 * 1024 * 1024;
21
+ export class TraceStore {
22
+ dataDir;
23
+ opts;
24
+ /** 已停写的日期(5MB 上限触发;当日内后续事件丢弃,次日自动恢复)。 */
25
+ truncatedDays = new Set();
26
+ lastPruneDay = '';
27
+ chain = Promise.resolve();
28
+ constructor(dataDir, opts) {
29
+ this.dataDir = dataDir;
30
+ this.opts = opts;
31
+ }
32
+ /** 追加事件(异步串行链,失败静默)。 */
33
+ append(event) {
34
+ const day = dayKey(event.ts);
35
+ if (this.truncatedDays.has(day))
36
+ return;
37
+ this.chain = this.chain
38
+ .then(() => this.writeOne(day, event))
39
+ .catch(() => {
40
+ /* 静默:追踪失败不影响主链路 */
41
+ });
42
+ }
43
+ /** 等待挂起的写入排空(dispose 序用)。 */
44
+ flush() {
45
+ return this.chain;
46
+ }
47
+ /**
48
+ * 尾部读取:从最近的日期文件倒序收集,按 kind 过滤,返回至多 `lines` 条
49
+ * (按写入顺序 = 时间升序)。坏行跳过(readJsonl 同款)。
50
+ */
51
+ async tail(lines, kind) {
52
+ const limit = Math.max(1, Math.min(500, Math.round(lines) || 100));
53
+ const files = this.recentFiles();
54
+ const collected = [];
55
+ for (let i = files.length - 1; i >= 0 && collected.length < limit; i--) {
56
+ const rows = await readJsonl(files[i]);
57
+ for (let j = rows.length - 1; j >= 0 && collected.length < limit; j--) {
58
+ const row = rows[j];
59
+ if (!row || typeof row !== 'object' || !row.kind)
60
+ continue;
61
+ if (kind && row.kind !== kind)
62
+ continue;
63
+ collected.push(row);
64
+ }
65
+ }
66
+ return collected.reverse();
67
+ }
68
+ dir() {
69
+ return path.join(this.dataDir, TRACE_DIR);
70
+ }
71
+ fileFor(day) {
72
+ return path.join(this.dir(), `trace-${day}.jsonl`);
73
+ }
74
+ recentFiles() {
75
+ try {
76
+ return fs
77
+ .readdirSync(this.dir())
78
+ .filter((f) => f.startsWith('trace-') && f.endsWith('.jsonl'))
79
+ .sort()
80
+ .map((f) => path.join(this.dir(), f));
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ }
86
+ async writeOne(day, event) {
87
+ if (day !== this.lastPruneDay) {
88
+ this.lastPruneDay = day;
89
+ this.pruneOldDays(day);
90
+ }
91
+ const file = this.fileFor(day);
92
+ try {
93
+ const stat = await fs.promises.stat(file);
94
+ if (stat.size > (this.opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES)) {
95
+ this.truncatedDays.add(day);
96
+ await appendJsonl(file, [
97
+ { kind: 'trace_truncated', ts: event.ts, note: '单日文件超上限,当日停写(次日自动恢复)' },
98
+ ]);
99
+ return;
100
+ }
101
+ }
102
+ catch {
103
+ /* 文件不存在 = 首写,继续 */
104
+ }
105
+ await appendJsonl(file, [event]);
106
+ }
107
+ /** 删除超过保留期的旧日文件(retentionDays 0 = 永久)。 */
108
+ pruneOldDays(today) {
109
+ if (this.opts.retentionDays <= 0)
110
+ return;
111
+ let files;
112
+ try {
113
+ files = fs.readdirSync(this.dir());
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ const todayMs = Date.parse(`${today}T00:00:00`);
119
+ for (const f of files) {
120
+ const m = /^trace-(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(f);
121
+ if (!m)
122
+ continue;
123
+ const dayMs = Date.parse(`${m[1]}T00:00:00`);
124
+ if (!Number.isFinite(dayMs) || !Number.isFinite(todayMs))
125
+ continue;
126
+ if (todayMs - dayMs > this.opts.retentionDays * 86_400_000) {
127
+ fs.promises.rm(path.join(this.dir(), f), { force: true }).catch(() => { });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ // ── 模块级单例(token-cost.ts 同款) ──
133
+ let store = null;
134
+ /** 插件启动时注入(index.ts 调用);enabled=false 或 0 天保留不影响初始化本身。 */
135
+ export function initTraceStore(dataDir, opts) {
136
+ store = new TraceStore(dataDir, opts);
137
+ }
138
+ /** 测试/卸载:清空单例。 */
139
+ export function resetTraceStore() {
140
+ store = null;
141
+ }
142
+ /** 埋点入口:未初始化 = no-op。fire-and-forget,永不抛。 */
143
+ export function trace(event) {
144
+ store?.append(event);
145
+ }
146
+ /** RPC tail 数据源;未初始化返回 undefined(端点回空集)。 */
147
+ export function getTraceStore() {
148
+ return store;
149
+ }
@@ -4,9 +4,30 @@ import { listConflictPairs, renderConflictResolution, renderConflicts, resolveCo
4
4
  import { WING_CATALOG, WING_FALLBACK, normPersistence, normScope, resolveRecordScope } from '../types.js';
5
5
  import { scopeFilterOf, workspaceIdOf } from '../workspace.js';
6
6
  import { GRAPH_STATUS_LABELS } from '../prompts/graph-projection.js';
7
+ import { redactSecrets } from '../util/redact.js';
7
8
  const OFF_NOTICE = '本会话的记忆档位为"关闭":该会话对记忆系统完全隐身,不读取也不写入记忆。';
8
9
  const WRITE_ONLY_NOTICE = '本会话为只写模式:记忆照常沉淀,但不读取。';
9
10
  const GLOBAL_OFF_NOTICE = '记忆注入已全局停用:本会话不读取记忆(沉淀照常)。';
11
+ /** §C 一次性告警标记:手动写入路径的脱敏失败只提示一次(不阻断写入)。 */
12
+ let manualRedactWarnedOnce = false;
13
+ /**
14
+ * §C 手动写入路径脱敏(memorax-absorb / task_18):memory_add / memory_import
15
+ * 与捕获路径走同一 8 类词表,两条 L1 写入边界语义一致。
16
+ * 红线:脱敏失败放行原文 + 一次性 warn(不阻断写入)。
17
+ */
18
+ function applyManualRedaction(record, cfg, logger) {
19
+ if (cfg.capture?.redactSecrets === false)
20
+ return;
21
+ try {
22
+ record.content = redactSecrets(record.content).text;
23
+ }
24
+ catch (err) {
25
+ if (!manualRedactWarnedOnce) {
26
+ manualRedactWarnedOnce = true;
27
+ logger.warn(`[memory] 手动写入脱敏失败,放行原文(一次性提示): ${err instanceof Error ? err.message : String(err)}`);
28
+ }
29
+ }
30
+ }
10
31
  export function registerMemoryTools(ctx, cfg, stores, logger, modes, live,
11
32
  /** 反刍控制器(可选:未装配时 ruminate 工具返回未启用提示)。 */
12
33
  ruminate) {
@@ -362,6 +383,7 @@ ruminate) {
362
383
  return { notice: 'content 为空,未写入' };
363
384
  const scene = typeof args.scene === 'string' && args.scene.trim() ? args.scene.trim().slice(0, 120) : '__manual__';
364
385
  const record = buildRecord(args, scene, Date.now(), workspaceIdOf(exec));
386
+ applyManualRedaction(record, cfg, logger);
365
387
  await stores.l1.appendNew([record]);
366
388
  logger.info(`[memory] 高权限写入记忆(${record.type}${record.metadata?.hall ? '/' + String(record.metadata.hall) : ''},时间轴 ${record.persistence ?? '?'}):${record.content.slice(0, 120)}`);
367
389
  return { id: record.id };
@@ -455,7 +477,9 @@ ruminate) {
455
477
  return;
456
478
  }
457
479
  seen.add(key);
458
- records.push(buildRecord(item, scene, now, workspaceIdOf(exec)));
480
+ const record = buildRecord(item, scene, now, workspaceIdOf(exec));
481
+ applyManualRedaction(record, cfg, logger);
482
+ records.push(record);
459
483
  });
460
484
  if (records.length > 0)
461
485
  await stores.l1.appendNew(records);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * §C 载荷脱敏(memorax-absorb Wave 2 / task_14)——捕获边界的密钥擦除。
3
+ *
4
+ * 用户在对话里贴入的密钥(PEM/Bearer/JWT/Cookie/厂商 API key/邮箱/长号/高熵 ID)
5
+ * 若原样进 L0/L1,会被逐轮召回注入上下文并在 UI 展示。本模块在捕获单点把它们
6
+ * 替换为类型化占位符 `[REDACTED:<KIND>]`——占位符保留类别语义,关键词检索仍可命中。
7
+ *
8
+ * 词表/正则/allowlist/span 合并算法以 memorax `payload-redaction.ts` 为基准逐条移植
9
+ * (全文快照见计划 evidence/payload-redaction-snapshot.ts;findings.md 有等价内联)。
10
+ *
11
+ * 契约(sanitize 同款,tests/redact.test.ts 钉住):
12
+ * - 纯函数,零 I/O 零时钟零随机;输入原样返回(未命中)= 引用不变无关紧要,字符串不可变;
13
+ * - **幂等**:已是 `[REDACTED:KIND]` 的值不会被二次匹配;
14
+ * - 安全值 allowlist 命中不脱(`example`/`change-me`/`$VAR`/`${{…}}` 等);
15
+ * - 本函数**永不抛错**的保证由调用方 try/catch 兜底(capture 接线层,§C 红线:失败放行原文)。
16
+ */
17
+ export type RedactionKind = 'PRIVATE_KEY' | 'AUTH_TOKEN' | 'COOKIE' | 'API_KEY' | 'CREDENTIAL' | 'EMAIL' | 'LONG_NUMBER' | 'OPAQUE_ID';
18
+ export interface RedactionResult {
19
+ text: string;
20
+ counts: Partial<Record<RedactionKind, number>>;
21
+ redacted: boolean;
22
+ }
23
+ /**
24
+ * 对文本执行 8 类脱敏。规则各自扫描 → 收集值级 span → 合并重叠 → 重拼插入占位符。
25
+ * 同一位置被多条规则命中时取更长/更高优先级的 span。
26
+ */
27
+ export declare function redactSecrets(text: string): RedactionResult;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * §C 载荷脱敏(memorax-absorb Wave 2 / task_14)——捕获边界的密钥擦除。
3
+ *
4
+ * 用户在对话里贴入的密钥(PEM/Bearer/JWT/Cookie/厂商 API key/邮箱/长号/高熵 ID)
5
+ * 若原样进 L0/L1,会被逐轮召回注入上下文并在 UI 展示。本模块在捕获单点把它们
6
+ * 替换为类型化占位符 `[REDACTED:<KIND>]`——占位符保留类别语义,关键词检索仍可命中。
7
+ *
8
+ * 词表/正则/allowlist/span 合并算法以 memorax `payload-redaction.ts` 为基准逐条移植
9
+ * (全文快照见计划 evidence/payload-redaction-snapshot.ts;findings.md 有等价内联)。
10
+ *
11
+ * 契约(sanitize 同款,tests/redact.test.ts 钉住):
12
+ * - 纯函数,零 I/O 零时钟零随机;输入原样返回(未命中)= 引用不变无关紧要,字符串不可变;
13
+ * - **幂等**:已是 `[REDACTED:KIND]` 的值不会被二次匹配;
14
+ * - 安全值 allowlist 命中不脱(`example`/`change-me`/`$VAR`/`${{…}}` 等);
15
+ * - 本函数**永不抛错**的保证由调用方 try/catch 兜底(capture 接线层,§C 红线:失败放行原文)。
16
+ */
17
+ const PLACEHOLDER_VALUE_PATTERN = /^\[REDACTED:[A-Z_]+\]$/;
18
+ 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)`;
19
+ 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]+)`;
20
+ /** 安全值 allowlist:命中即放行(示例/占位/环境变量引用/类型名等,脱了只会毁记忆)。 */
21
+ 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;
22
+ const RULES = [
23
+ {
24
+ // PEM 私钥块:至对应 END,截断文件时取到文末
25
+ pattern: /-----BEGIN ((?:(?:RSA|EC|DSA|OPENSSH|ENCRYPTED) )?PRIVATE KEY|PGP PRIVATE KEY BLOCK)-----[\s\S]*?(?:-----END \1-----|$)/gi,
26
+ kind: 'PRIVATE_KEY',
27
+ priority: 100,
28
+ },
29
+ {
30
+ pattern: /(?:^|[\r\n])[ \t]*(?:Proxy-)?Authorization[ \t]*:[ \t]*([^ \t\r\n][^\r\n]*)/gim,
31
+ group: 1,
32
+ kind: 'AUTH_TOKEN',
33
+ priority: 95,
34
+ },
35
+ {
36
+ pattern: /(?:^|[^A-Za-z0-9_-])(?:Bearer|Basic)\s+["']?([A-Za-z0-9._~+/=-]{16,})(?![A-Za-z0-9._~+/=-])/gi,
37
+ group: 1,
38
+ kind: 'AUTH_TOKEN',
39
+ priority: 94,
40
+ },
41
+ {
42
+ // JWT 三段形态
43
+ pattern: /\beyJ[A-Za-z0-9_-]{5,}\.eyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{16,}\b/g,
44
+ kind: 'AUTH_TOKEN',
45
+ priority: 94,
46
+ },
47
+ {
48
+ pattern: /(?:^|[\r\n])[ \t]*(?:Set-Cookie|Cookie)\s*:\s*([^\r\n]+)/gim,
49
+ group: 1,
50
+ kind: 'COOKIE',
51
+ priority: 92,
52
+ },
53
+ {
54
+ // 声明式赋值:key: value / key = value(含 export/const/let/var 前缀与引号键)
55
+ pattern: new RegExp(String.raw `(?:^|[\r\n;,{}])[ \t]*(?:-[ \t]+)?(?:(?:export|const|let|var)[ \t]+)*["']?${SENSITIVE_KEY_SOURCE}["']?[ \t]*[:=][ \t]*(${CREDENTIAL_VALUE_SOURCE})`, 'gim'),
56
+ group: 1,
57
+ kind: 'CREDENTIAL',
58
+ priority: 85,
59
+ accept: (value) => !isSafeCredentialValue(value),
60
+ },
61
+ {
62
+ // CLI 形态:--api-key value / --api-key=value
63
+ pattern: new RegExp(String.raw `--${SENSITIVE_KEY_SOURCE}(?![A-Za-z0-9_$-])(?:[ \t]+|=[ \t]*)(${CREDENTIAL_VALUE_SOURCE})`, 'gi'),
64
+ group: 1,
65
+ kind: 'CREDENTIAL',
66
+ priority: 85,
67
+ accept: (value) => !isSafeCredentialValue(value),
68
+ },
69
+ {
70
+ // URL query 形态:?api_key=...
71
+ pattern: new RegExp(String.raw `[?&]${SENSITIVE_KEY_SOURCE}(?![A-Za-z0-9_$-])=([^&#\s"'<>]+)`, 'gi'),
72
+ group: 1,
73
+ kind: 'CREDENTIAL',
74
+ priority: 85,
75
+ accept: (value) => !isSafeCredentialValue(value),
76
+ },
77
+ {
78
+ // URL userinfo 形态:scheme://user:pass@
79
+ pattern: /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s/:@]*:([^\s/@]+)@/g,
80
+ group: 1,
81
+ kind: 'CREDENTIAL',
82
+ priority: 85,
83
+ accept: (value) => !isSafeCredentialValue(value),
84
+ },
85
+ // ── 已知厂商 API key 形态(9 条) ──
86
+ { pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}[A-Za-z0-9]\b/g, kind: 'API_KEY', priority: 80 },
87
+ { pattern: /\bsk_[A-Za-z0-9_-]{8,}\b/g, kind: 'API_KEY', priority: 80 },
88
+ { pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})\b/g, kind: 'API_KEY', priority: 80 },
89
+ { pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, kind: 'API_KEY', priority: 80 },
90
+ { pattern: /\bxox[baprs]-[A-Za-z0-9-]{16,}[A-Za-z0-9]\b/g, kind: 'API_KEY', priority: 80 },
91
+ { pattern: /\b(?:(?:sk|rk)_live_[A-Za-z0-9]{16,}|whsec_[A-Za-z0-9]{16,})\b/g, kind: 'API_KEY', priority: 80 },
92
+ { pattern: /\bglpat-[A-Za-z0-9_-]{16,}[A-Za-z0-9]\b/g, kind: 'API_KEY', priority: 80 },
93
+ { pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g, kind: 'API_KEY', priority: 80 },
94
+ { pattern: /\bnpm_[A-Za-z0-9]{36}\b/g, kind: 'API_KEY', priority: 80 },
95
+ {
96
+ // 邮箱(前后向断言防误切)
97
+ 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,
98
+ kind: 'EMAIL',
99
+ priority: 70,
100
+ },
101
+ {
102
+ // UUID
103
+ 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,
104
+ kind: 'OPAQUE_ID',
105
+ priority: 60,
106
+ },
107
+ {
108
+ // hex 64/40/32(含字母才脱——纯数字是普通长号,交 LONG_NUMBER)
109
+ 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,
110
+ kind: 'OPAQUE_ID',
111
+ priority: 60,
112
+ accept: (value) => /[A-Fa-f]/.test(value),
113
+ },
114
+ {
115
+ // ≥24 位字母数字高熵启发式(熵参数见 looksLikeHighEntropyOpaqueId)
116
+ pattern: /(?<![A-Za-z0-9])[A-Za-z0-9]{24,}(?![A-Za-z0-9])/g,
117
+ kind: 'OPAQUE_ID',
118
+ priority: 60,
119
+ accept: looksLikeHighEntropyOpaqueId,
120
+ },
121
+ {
122
+ // ≥9 位数字串(含电话形);日期形放行
123
+ pattern: /(?<![A-Za-z0-9.])\+?(?:\d|\(\d)[\d ()-]*\d[Xx]?\)?(?![A-Za-z0-9]|\.\d)/g,
124
+ kind: 'LONG_NUMBER',
125
+ priority: 50,
126
+ accept: (value) => {
127
+ if (value.replace(/\D/g, '').length < 9)
128
+ return false;
129
+ const normalized = value.trim().replace(/[()]/g, '');
130
+ return (!/^(?:19|20)\d{2}-\d{1,2}-\d{1,2}(?:[ T-]\d{1,2})?$/.test(normalized) &&
131
+ !/^(?:19|20)\d{2}\s+\d{1,2}\s+\d{1,2}(?:\s+\d{1,2})?$/.test(normalized));
132
+ },
133
+ },
134
+ ];
135
+ /**
136
+ * 对文本执行 8 类脱敏。规则各自扫描 → 收集值级 span → 合并重叠 → 重拼插入占位符。
137
+ * 同一位置被多条规则命中时取更长/更高优先级的 span。
138
+ */
139
+ export function redactSecrets(text) {
140
+ if (!text)
141
+ return { text, counts: {}, redacted: false };
142
+ const spans = [];
143
+ for (const rule of RULES)
144
+ collectRuleSpans(text, rule, spans);
145
+ const selected = mergeOverlappingSpans(spans);
146
+ if (selected.length === 0)
147
+ return { text, counts: {}, redacted: false };
148
+ const counts = {};
149
+ const output = [];
150
+ let offset = 0;
151
+ for (const span of selected) {
152
+ output.push(text.slice(offset, span.start), `[REDACTED:${span.kind}]`);
153
+ counts[span.kind] = (counts[span.kind] ?? 0) + 1;
154
+ offset = span.end;
155
+ }
156
+ output.push(text.slice(offset));
157
+ return { text: output.join(''), counts, redacted: true };
158
+ }
159
+ function collectRuleSpans(text, rule, spans) {
160
+ rule.pattern.lastIndex = 0;
161
+ for (const match of text.matchAll(rule.pattern)) {
162
+ const value = match[rule.group ?? 0];
163
+ if (match.index === undefined || !value || PLACEHOLDER_VALUE_PATTERN.test(value.trim()))
164
+ continue;
165
+ if (rule.accept && !rule.accept(value))
166
+ continue;
167
+ const relativeStart = match[0].lastIndexOf(value);
168
+ if (relativeStart < 0)
169
+ continue;
170
+ spans.push({
171
+ start: match.index + relativeStart,
172
+ end: match.index + relativeStart + value.length,
173
+ kind: rule.kind,
174
+ priority: rule.priority,
175
+ });
176
+ }
177
+ }
178
+ function isSafeCredentialValue(rawValue) {
179
+ let value = rawValue.trim();
180
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
181
+ value = value.slice(1, -1).trim();
182
+ }
183
+ return !value || SAFE_CREDENTIAL_VALUE_PATTERN.test(value);
184
+ }
185
+ /** 高熵判定:小写≥2 ∧ 大写≥2 ∧ 数字≥2 ∧ 字符类切换≥4 ∧ 最长连续字母段≤5 ∧ 去重字符≥12。 */
186
+ function looksLikeHighEntropyOpaqueId(value) {
187
+ let lowercase = 0;
188
+ let uppercase = 0;
189
+ let digits = 0;
190
+ let transitions = 0;
191
+ let letterRun = 0;
192
+ let longestLetterRun = 0;
193
+ let previousWasDigit;
194
+ const unique = new Set();
195
+ for (const character of value) {
196
+ const isDigit = character >= '0' && character <= '9';
197
+ if (isDigit) {
198
+ digits += 1;
199
+ letterRun = 0;
200
+ }
201
+ else {
202
+ if (character >= 'a' && character <= 'z')
203
+ lowercase += 1;
204
+ else
205
+ uppercase += 1;
206
+ letterRun += 1;
207
+ longestLetterRun = Math.max(longestLetterRun, letterRun);
208
+ }
209
+ if (previousWasDigit !== undefined && previousWasDigit !== isDigit)
210
+ transitions += 1;
211
+ previousWasDigit = isDigit;
212
+ unique.add(character);
213
+ }
214
+ return lowercase >= 2 && uppercase >= 2 && digits >= 2 && transitions >= 4 && longestLetterRun <= 5 && unique.size >= 12;
215
+ }
216
+ function mergeOverlappingSpans(spans) {
217
+ const sorted = [...spans].sort((left, right) => left.start - right.start || right.end - left.end || right.priority - left.priority);
218
+ const merged = [];
219
+ for (const span of sorted) {
220
+ const previous = merged.at(-1);
221
+ if (!previous || span.start >= previous.end) {
222
+ merged.push({ ...span });
223
+ continue;
224
+ }
225
+ previous.end = Math.max(previous.end, span.end);
226
+ }
227
+ return merged;
228
+ }
package/dsh.plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dsh-prime-memory",
3
3
  "name": "dsh-prime-memory",
4
- "version": "0.17.0-dsh0.1.7.3",
4
+ "version": "0.19.0-dsh0.2.0.1",
5
5
  "description": {
6
6
  "en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
7
7
  "zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "license": "MIT",
15
15
  "engines": {
16
- "dsh": ">=0.1.7-rc.1 <0.1.8-0"
16
+ "dsh": ">=0.2.0-rc.1 <0.2.1-0"
17
17
  },
18
18
  "components": {
19
19
  "host": "dist/index.js",
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.18.2",
3
+ "version": "0.19.0",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",
7
- "tag": "dsh-0.1.7"
7
+ "tag": "dsh-0.2.0"
8
8
  },
9
9
  "description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
10
10
  "type": "module",
@@ -61,7 +61,7 @@
61
61
  },
62
62
  "engines": {
63
63
  "node": ">=22.16.0",
64
- "dsh": ">=0.1.7-rc.1 <0.1.8-0"
64
+ "dsh": ">=0.2.0-rc.1 <0.2.1-0"
65
65
  },
66
66
  "keywords": [
67
67
  "dsh",
@@ -83,15 +83,15 @@
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@deepseek-ai/cordis": "^4.0.1",
86
- "@deepseek-ai/dsh-agent": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0",
87
- "@deepseek-ai/dsh-client-connection": ">=0.1.7-rc.1 <0.1.8-0",
88
- "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.1.8-0",
89
- "@deepseek-ai/dsh-home-paths": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0",
90
- "@deepseek-ai/dsh-llm": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0",
91
- "@deepseek-ai/dsh-session": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0",
92
- "@deepseek-ai/dsh-settings": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0",
93
- "@deepseek-ai/dsh-system-prompt": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0",
94
- "@deepseek-ai/dsh-tools": ">=0.1.1-rc.2 <0.1.2-0 || >=0.1.2-rc.1 <0.1.3-0 || >=0.1.3-rc.1 <0.1.4-0 || >=0.1.5-rc.2 <0.1.8-0"
86
+ "@deepseek-ai/dsh-agent": ">=0.2.0-rc.1 <0.2.1-0",
87
+ "@deepseek-ai/dsh-client-connection": ">=0.2.0-rc.1 <0.2.1-0",
88
+ "@deepseek-ai/dsh-client-ui-primitives": ">=0.2.0-rc.1 <0.2.1-0",
89
+ "@deepseek-ai/dsh-home-paths": ">=0.2.0-rc.1 <0.2.1-0",
90
+ "@deepseek-ai/dsh-llm": ">=0.2.0-rc.1 <0.2.1-0",
91
+ "@deepseek-ai/dsh-session": ">=0.2.0-rc.1 <0.2.1-0",
92
+ "@deepseek-ai/dsh-settings": ">=0.2.0-rc.1 <0.2.1-0",
93
+ "@deepseek-ai/dsh-system-prompt": ">=0.2.0-rc.1 <0.2.1-0",
94
+ "@deepseek-ai/dsh-tools": ">=0.2.0-rc.1 <0.2.1-0"
95
95
  },
96
96
  "peerDependenciesMeta": {
97
97
  "@deepseek-ai/dsh-agent": {
@@ -125,15 +125,15 @@
125
125
  "devDependencies": {
126
126
  "@deepseek-ai/cordis": "^4.0.4",
127
127
  "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
128
- "@deepseek-ai/dsh-agent": "0.1.1-rc.2",
129
- "@deepseek-ai/dsh-agent-default-model": "0.1.1-rc.2",
130
- "@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
131
- "@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
132
- "@deepseek-ai/dsh-llm": "0.1.1-rc.2",
133
- "@deepseek-ai/dsh-session": "0.1.1-rc.2",
134
- "@deepseek-ai/dsh-settings": "0.1.1-rc.2",
135
- "@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
136
- "@deepseek-ai/dsh-tools": "0.1.1-rc.2",
128
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.1",
129
+ "@deepseek-ai/dsh-agent-default-model": "0.2.0-rc.1",
130
+ "@deepseek-ai/dsh-client-connection": "0.2.0-rc.1",
131
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.1",
132
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.1",
133
+ "@deepseek-ai/dsh-session": "0.2.0-rc.1",
134
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.1",
135
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.1",
136
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
137
137
  "@deepseek-ai/schemastery": "^3.18.4",
138
138
  "@eslint/js": "^10.0.1",
139
139
  "@types/node": "^22.0.0",
@@ -143,6 +143,7 @@
143
143
  "globals": "^17.12.0",
144
144
  "typescript": "^5.6.0",
145
145
  "typescript-eslint": "^8.69.0",
146
- "vitest": "^4.1.11"
146
+ "vitest": "^4.1.11",
147
+ "@deepseek-ai/dsh-compaction": "0.2.0-rc.1"
147
148
  }
148
149
  }