@kenz1117/dsh-engram 0.4.7 → 0.5.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/README.en.md +16 -14
- package/README.md +15 -13
- package/lib/client.js +1 -2
- package/lib/client.js.map +1 -1
- package/lib/index.js +461 -51
- package/package.json +8 -16
package/README.en.md
CHANGED
|
@@ -30,11 +30,11 @@ Zero configuration after install (stores and model cache default to `~/.dsh/engr
|
|
|
30
30
|
|
|
31
31
|
## Features
|
|
32
32
|
|
|
33
|
-
- **Cross-session memory**: a user memory profile is injected at session start (
|
|
34
|
-
- **Dual-scope stores**: `user.db` shared globally; `project-<
|
|
35
|
-
- **Hybrid retrieval**: FTS5 (unicode61 + Chinese 2-gram pre-tokenization) fused with local vectors (`Xenova/bge-small-zh-v1.5`, 512-dim, q8) via RRF, plus one-hop expansion over relation edges; the embedding model runs offline, and a failed download degrades to keyword-only retrieval with an explicit marker.
|
|
33
|
+
- **Cross-session memory**: a user memory profile is injected at session start (entry cap plus token budget, both configurable), so the agent naturally knows who you are and what you are building; tools recall facts across sessions.
|
|
34
|
+
- **Dual-scope stores**: `user.db` shared globally; `project-<hash>.db` isolated per git origin identity (falls back to a working-directory encoding without git; legacy stores migrate automatically) — personal preferences follow the person, project conventions follow the repo.
|
|
35
|
+
- **Hybrid retrieval**: FTS5 (unicode61 + Chinese 2-gram pre-tokenization) fused with local vectors (`Xenova/bge-small-zh-v1.5`, 512-dim, q8) via RRF, plus one-hop expansion over relation edges and a multiplicative recency/proof ranking boost; the embedding model runs offline, and a failed download degrades to keyword-only retrieval with an explicit marker.
|
|
36
36
|
- **Knowledge flywheel**: capture/save → contradiction candidates (high-similarity neighbors create `contradicts` edges on write, for the model/user to adjudicate) → hit reinforcement (confidence +0.05) → distillation (topic clusters merge into higher-level rules, supersedes chains, confidence inheritance) → decay (low-importance, long-unaccessed entries archive; restorable).
|
|
37
|
-
- **Automatic capture** (when `ingest` is enabled): each new turn's first step extracts candidate facts from the previous turn out of the session log, written with low confidence and deduplicated by embedding — memories accumulate without you saying "remember this".
|
|
37
|
+
- **Automatic capture** (when `ingest` is enabled): each new turn's first step extracts candidate facts from the previous turn out of the session log, and session end captures the final turn too (failures leave a pending key that the next session replays; capture is idempotent per session+turn), written with low confidence and deduplicated by embedding — memories accumulate without you saying "remember this".
|
|
38
38
|
- **Provenance audit**: every memory records its source session, turn, and event seq; `engram_review` traces the full source chain, supersede chain, contradictions, and operation log; all writes/edits/forgets/distills/decays land in the operation log table.
|
|
39
39
|
- **Web management panel**: a "Memory Library" tab in Settings — stat cards, filter by status/kind/content, inline detail and edit (through the supersede chain), forget/restore, Markdown/JSON export. UI copy is bilingual zh/en and follows the host language setting live.
|
|
40
40
|
- **Portable data**: `engram_export` exports Markdown / JSON files in one step.
|
|
@@ -63,15 +63,18 @@ Optional configuration (cordis.yml):
|
|
|
63
63
|
config:
|
|
64
64
|
dbDir: '~/.dsh/engram' # store and model-cache root directory
|
|
65
65
|
injectProfile: true # inject the user memory profile at session start
|
|
66
|
-
profileTopN: 8 # injection cap (1-64)
|
|
66
|
+
profileTopN: 8 # injection entry cap (1-64)
|
|
67
|
+
injectTokenBudget: 1024 # injection token budget (128-8192, estimated ceil(len/4); over-budget entries degrade to index lines)
|
|
67
68
|
modelCacheDir: '~/.dsh/engram/models' # embedding model cache directory
|
|
68
69
|
hfEndpoint: 'https://huggingface.co' # model download endpoint; set a mirror behind restricted networks
|
|
69
70
|
ingest: 'off' # automatic capture: off | light (user messages only, ≤2/turn) | eager (assistant messages too, ≤5/turn)
|
|
70
71
|
# provider and model must be given as a pair: aux-LLM route override for capture/distill (parsed from the session log by default)
|
|
71
72
|
# provider: 'deepseek'
|
|
72
73
|
# model: 'deepseek-v4-flash'
|
|
73
|
-
decayAfterDays: 30 # decay: last access older than this many days
|
|
74
|
+
decayAfterDays: 30 # decay: last access older than this many days (also the recency-boost decay window)
|
|
74
75
|
decayImportanceBelow: 0.3 # decay: and importance below this → archive (restorable)
|
|
76
|
+
rankRecencyWeight: 0.2 # retrieval recency boost weight (0-2, 0 disables)
|
|
77
|
+
rankProofWeight: 0.1 # retrieval hit-count boost weight (0-2, 0 disables)
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
## How It Works
|
|
@@ -82,15 +85,15 @@ The plugin has a host half (Node) and a browser half (React):
|
|
|
82
85
|
Session agent Host half (Node)
|
|
83
86
|
│ │
|
|
84
87
|
├─ every turn, first step ◀─────────────── ├─ user memory profile injection (plugin-source user snapshot)
|
|
85
|
-
├─ engram_save / search / review … ──────▶ ├─ SQLite dual stores (user.db / project-<
|
|
86
|
-
│ ├─ FTS5 keyword track + local vector track, RRF fusion
|
|
88
|
+
├─ engram_save / search / review … ──────▶ ├─ SQLite dual stores (user.db / project-<origin hash>.db)
|
|
89
|
+
│ ├─ FTS5 keyword track + local vector track, RRF fusion + ranking boost
|
|
87
90
|
├─ engram_distill ───────────────────────▶ ├─ aux-LLM distillation (cluster merge → supersedes chain)
|
|
88
|
-
│ └─ automatic capture: session log → candidate facts (ingest on)
|
|
91
|
+
│ └─ automatic capture: session log → candidate facts (incl. the final turn at session end; ingest on)
|
|
89
92
|
└─ Settings "Memory Library" tab ◀──────── ─── loopback API /api/engram/* (writes verify loopback Origin)
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
- **Dual-scope stores**: `user.db` shared globally; `project-<
|
|
93
|
-
- **Automatic capture** (when `ingest` is on): each new turn's first step extracts candidate facts from the previous turn out of the session log (the read source is the session log; aux-call request auditing goes to the plugin's own operation log — unknown events are never appended to the session log
|
|
95
|
+
- **Dual-scope stores**: `user.db` shared globally; `project-<hash>.db` named by the normalized git origin URL hash (`git@github.com:a/b.git` and `https://github.com/a/b` share one store; worktrees resolve through the pointer to the main repo's origin); without git or an origin it falls back to a working-directory encoding, and legacy cwd-named stores are renamed in place at startup (if both exist, nothing moves and a warning is logged).
|
|
96
|
+
- **Automatic capture** (when `ingest` is on): each new turn's first step extracts candidate facts from the previous turn out of the session log; session end (`session/disposed`) captures the final turn with a 5-second timeout, and on failure/timeout a pending key lands in the operation log for the next session's first step to replay; captured (session, turn) pairs are idempotent. The read source is the session log; aux-call request auditing goes to the plugin's own operation log — unknown events are never appended to the session log. Candidates are written with low confidence and deduplicated by embedding.
|
|
94
97
|
- **Provenance chain**: every memory records its source session, turn, and event seq, fully traceable via `engram_review`; the operation log table records every write/edit/forget/distill/decay.
|
|
95
98
|
- **Offline embeddings**: the model downloads once (q8, ~50MB; mirror endpoint configurable), then runs fully offline; on failure the plugin keeps working and retrieval degrades to keyword-only with an explicit marker.
|
|
96
99
|
- **UI localization**: the client half registers zh/en dictionaries through the host locale service and follows the host language setting live; data-level enums (status/kind) are mapped only in the display layer and stay English in storage.
|
|
@@ -114,11 +117,11 @@ pnpm bundle
|
|
|
114
117
|
|
|
115
118
|
#### What the model sees
|
|
116
119
|
|
|
117
|
-
Each turn's first step appends a plugin-source user snapshot: `User memory profile (dsh-engram, cross-session):` followed by the user-scope memory list (up to 8 by default; `injectProfile: false` disables). Tool results are plain text lines (with `id=`, scope/kind annotations, contradiction-candidate hints, and degradation notes). Automatic capture and distillation each make one aux-LLM call (billed independently of the main conversation path, with purpose attribution).
|
|
120
|
+
Each turn's first step appends a plugin-source user snapshot: `User memory profile (dsh-engram, cross-session):` followed by the user-scope memory list (up to 8 entries within a 1024-token budget by default; over-budget entries degrade to `#id` index lines; `injectProfile: false` disables). Tool results are plain text lines (with `id=`, scope/kind annotations, contradiction-candidate hints, and degradation notes). Automatic capture and distillation each make one aux-LLM call (billed independently of the main conversation path, with purpose attribution).
|
|
118
121
|
|
|
119
122
|
#### Token effect
|
|
120
123
|
|
|
121
|
-
Profile injection is a conditional fixed cost (
|
|
124
|
+
Profile injection is a conditional fixed cost (bounded by both the entry cap and the token budget); the tool schemas are a standing cost (9 narrow-parameter tools).
|
|
122
125
|
|
|
123
126
|
#### KV Cache effect
|
|
124
127
|
|
|
@@ -126,7 +129,6 @@ Profile text changes as the memory store changes — changes only land at turn b
|
|
|
126
129
|
|
|
127
130
|
## Known Limitations and Deferred Work
|
|
128
131
|
|
|
129
|
-
- **Last-turn blind spot in automatic capture** — capture is triggered by the next turn's first step, so a session's final turn is never captured; a session-end event hook is future work.
|
|
130
132
|
- **No LLM adjudication of contradiction candidates** — writes only report candidates by vector similarity (≥0.88) and create edges; semantic-contradiction confirmation is left to model/user adjudication and distillation.
|
|
131
133
|
- **Memories written during embedder degradation have no vectors** — memories written before the model is ready do not participate in the semantic track; after semantics come online run `pnpm backfill` once to backfill existing vectors (after `pnpm build` has warmed the model cache; `HF_ENDPOINT` configurable).
|
|
132
134
|
|
package/README.md
CHANGED
|
@@ -30,11 +30,11 @@ dsh plugin --profile web add @kenz1117/dsh-engram
|
|
|
30
30
|
|
|
31
31
|
## 特性
|
|
32
32
|
|
|
33
|
-
-
|
|
34
|
-
- **双层分库**:`user.db` 全局共享;`project-<
|
|
35
|
-
- **混合检索**:FTS5(unicode61 + 中文 2-gram 预切词)与本地向量(`Xenova/bge-small-zh-v1.5`,512 维,q8)RRF 融合 +
|
|
33
|
+
- **跨会话记忆**:会话开始注入用户画像摘要(条数 + token 预算双重上限,可配),Agent 天然"记得"你是谁、在做什么;工具检索跨会话召回历史事实。
|
|
34
|
+
- **双层分库**:`user.db` 全局共享;`project-<hash>.db` 按 git origin 标识隔离(无 git 时回退工作目录编码,旧库自动迁移)——个人偏好跟人走,项目约定跟仓库走。
|
|
35
|
+
- **混合检索**:FTS5(unicode61 + 中文 2-gram 预切词)与本地向量(`Xenova/bge-small-zh-v1.5`,512 维,q8)RRF 融合 + 关系边一跳扩展 + 新鲜度/命中次数乘性排序 boost;嵌入模型离线运行,下载失败自动降级纯关键词并显式标记。
|
|
36
36
|
- **知识飞轮**:摄取/保存 → 矛盾候选(写入时高相似近邻建 `contradicts` 边并报告,模型/用户裁决)→ 命中强化(confidence +0.05)→ 蒸馏(同主题簇合并为高层规律、supersedes 取代链、置信度继承)→ 衰减(低重要性且长期未访问归档,可恢复)。
|
|
37
|
-
- **自动摄取**(`ingest`
|
|
37
|
+
- **自动摄取**(`ingest` 配置开启时):新一轮第一步从会话日志提取上一轮的候选事实,会话结束时补摄取最后一轮(失败留 pending 键,下次会话自动补做,幂等不重复),低 confidence 写入并按嵌入去重——不说"记住"也能攒记忆。
|
|
38
38
|
- **来源审计**:每条记忆记录来源会话、轮次与事件 seq,`engram_review` 完整回查来源链、取代链、矛盾与操作日志;全部写入/修改/遗忘/蒸馏/衰减入操作日志表。
|
|
39
39
|
- **Web 管理面板**:设置页「记忆库」tab——统计卡片、按状态/种类/内容过滤、行内详情与编辑(走取代链)、遗忘/恢复、导出 Markdown/JSON。界面文案中英双语,跟随宿主语言设置实时切换。
|
|
40
40
|
- **数据可携带**:`engram_export` 一键导出 Markdown / JSON 文件。
|
|
@@ -64,14 +64,17 @@ dsh plugin --profile web add @kenz1117/dsh-engram
|
|
|
64
64
|
dbDir: '~/.dsh/engram' # 分库与模型缓存根目录
|
|
65
65
|
injectProfile: true # 会话开始注入用户画像摘要
|
|
66
66
|
profileTopN: 8 # 注入条数上限(1-64)
|
|
67
|
+
injectTokenBudget: 1024 # 注入 token 预算(128-8192,估算 ceil(len/4),超预算条目降级为索引行)
|
|
67
68
|
modelCacheDir: '~/.dsh/engram/models' # 嵌入模型缓存目录
|
|
68
69
|
hfEndpoint: 'https://huggingface.co' # 模型下载端点,网络受限可配镜像
|
|
69
70
|
ingest: 'off' # 自动摄取:off | light(仅用户消息,每轮≤2条)| eager(含助手消息,每轮≤5条)
|
|
70
71
|
# provider 与 model 必须成对提供:摄取/蒸馏的辅助 LLM 路由覆盖(缺省从会话日志解析)
|
|
71
72
|
# provider: 'deepseek'
|
|
72
73
|
# model: 'deepseek-v4-flash'
|
|
73
|
-
decayAfterDays: 30 #
|
|
74
|
+
decayAfterDays: 30 # 衰减:最近访问超过该天数(同时是检索 recency boost 的衰减窗口)
|
|
74
75
|
decayImportanceBelow: 0.3 # 衰减:且 importance 低于该值 → 归档(可恢复)
|
|
76
|
+
rankRecencyWeight: 0.2 # 检索排序新鲜度因子权重(0-2,0 关闭)
|
|
77
|
+
rankProofWeight: 0.1 # 检索排序命中次数因子权重(0-2,0 关闭)
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
## 工作原理
|
|
@@ -82,15 +85,15 @@ dsh plugin --profile web add @kenz1117/dsh-engram
|
|
|
82
85
|
会话 Agent 宿主半(Node)
|
|
83
86
|
│ │
|
|
84
87
|
├─ 每轮第一步 ◀─────────────────────────── ├─ 用户画像快照注入(plugin 来源 user 快照)
|
|
85
|
-
├─ engram_save / search / review … ──────▶ ├─ SQLite 双库(user.db / project-<
|
|
86
|
-
│ ├─ FTS5 关键词道 + 本地向量道 RRF 融合
|
|
88
|
+
├─ engram_save / search / review … ──────▶ ├─ SQLite 双库(user.db / project-<origin hash>.db)
|
|
89
|
+
│ ├─ FTS5 关键词道 + 本地向量道 RRF 融合 + 排序 boost
|
|
87
90
|
├─ engram_distill ───────────────────────▶ ├─ 辅助 LLM 蒸馏(簇合并 → supersedes 链)
|
|
88
|
-
│ └─ 自动摄取:会话日志 →
|
|
91
|
+
│ └─ 自动摄取:会话日志 → 候选事实(含会话结束的末轮,ingest 开启时)
|
|
89
92
|
└─ 设置页「记忆库」tab ◀────────────────── ─── 回环 API /api/engram/*(写操作校验回环 Origin)
|
|
90
93
|
```
|
|
91
94
|
|
|
92
|
-
- **双层分库**:`user.db` 全局共享;`project-<
|
|
93
|
-
- **自动摄取**(`ingest`
|
|
95
|
+
- **双层分库**:`user.db` 全局共享;`project-<hash>.db` 按 git origin URL 归一化哈希命名(`git@github.com:a/b.git` 与 `https://github.com/a/b` 同库;worktree 沿指针解析到主仓库 origin);无 git 或无 origin 时回退工作目录编码命名,旧的 cwd 命名库在启动时自动 rename 迁移(新旧并存则不动并告警)。
|
|
96
|
+
- **自动摄取**(`ingest` 开启时):新一轮第一步从会话日志提取上一轮的候选事实;会话结束(session/disposed)补摄取最后一轮,5 秒超时,失败/超时把 pending 键写入操作日志,下次会话首步自动重放补做;已摄取的 (会话, 轮次) 幂等去重。读取源是会话日志;辅助调用的请求审计走插件自身操作日志,不向会话日志 append 未知事件。候选以低 confidence 写入并按嵌入去重。
|
|
94
97
|
- **来源链**:每条记忆记录来源会话、轮次与事件 seq,`engram_review` 可完整回查;操作日志表记录全部写入/修改/遗忘/蒸馏/衰减。
|
|
95
98
|
- **嵌入离线**:模型首次使用需联网下载(q8 约 50MB,端点可配镜像),此后完全离线;失败时插件照常工作,检索降级纯关键词并显式标记。
|
|
96
99
|
- **界面本地化**:client 半经宿主 locale 服务注册 zh/en 词典,跟随宿主语言设置实时切换;状态/种类等数据枚举仅在显示层映射,存储值保持英文。
|
|
@@ -114,11 +117,11 @@ pnpm bundle
|
|
|
114
117
|
|
|
115
118
|
#### What the model sees
|
|
116
119
|
|
|
117
|
-
会话每轮第一步追加一条 plugin 来源的 user 快照:`User memory profile (dsh-engram, cross-session):` 加用户级记忆列表(默认至多 8
|
|
120
|
+
会话每轮第一步追加一条 plugin 来源的 user 快照:`User memory profile (dsh-engram, cross-session):` 加用户级记忆列表(默认至多 8 条且整段不超过 1024 token 预算,超预算条目降级为 `#id` 索引行,`injectProfile: false` 关闭)。工具调用结果为纯文本行列表(含 `id=`、scope/kind 标注、矛盾候选提示与降级说明)。自动摄取与蒸馏各产生一次辅助 LLM 调用(独立于主对话计费路径,带 purpose 归因)。
|
|
118
121
|
|
|
119
122
|
#### Token effect
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
画像注入为条件性固定成本(受条数上限与 token 预算双重约束);工具 schema 为常驻成本(9 个窄参数工具)。
|
|
122
125
|
|
|
123
126
|
#### KV Cache effect
|
|
124
127
|
|
|
@@ -126,7 +129,6 @@ pnpm bundle
|
|
|
126
129
|
|
|
127
130
|
## Known Limitations and Deferred Work
|
|
128
131
|
|
|
129
|
-
- **自动摄取的最后一轮盲区** —— 摄取由下一轮的第一步触发,会话最后一轮不摄取;会话结束事件钩子是后续工作。
|
|
130
132
|
- **矛盾候选无 LLM 判定** —— 写入时仅按向量相似度(≥0.88)报告候选并建边,语义矛盾的确认留给模型/用户裁决与蒸馏。
|
|
131
133
|
- **嵌入器降级期间的记忆无向量** —— 模型未就绪时写入的记忆不参与语义道;语义上线后跑一次 `pnpm backfill` 补算存量向量(`pnpm build` 的模型缓存就绪后执行,可经 `HF_ENDPOINT` 配镜像)。
|
|
132
134
|
|
package/lib/client.js
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({id:`@kenz1117/dsh-engram`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=e("react/jsx-runtime"),a=`@kenz1117/dsh-engram/panel.module.css`;if(typeof document<`u`&&document.querySelector(`style[data-plugin-css=`+JSON.stringify(a)+`]`)===null){let e=document.createElement(`style`);e.dataset.plugin=`@kenz1117/dsh-engram`,e.dataset.pluginCss=a,e.textContent=`.bWwFiW_panel{max-width:980px}.bWwFiW_toolbar{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:14px;display:flex}.bWwFiW_scopeTabs{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:flex-end;gap:18px;margin-right:auto;display:flex}.bWwFiW_scopeOn,.bWwFiW_scopeOff{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;padding:7px 1px 9px;font-size:13px;line-height:20px;position:relative}.bWwFiW_scopeOn,.bWwFiW_scopeOff:hover{color:var(--dsw-alias-label-primary)}.bWwFiW_scopeOn:after{background:var(--dsw-alias-brand-primary);content:"";border-radius:2px 2px 0 0;height:2px;position:absolute;bottom:-1px;left:0;right:0}.bWwFiW_button{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;border-radius:8px;padding:5px 12px;font-size:13px}.bWwFiW_button:hover{border-color:var(--dsw-alias-brand-primary)}.bWwFiW_button:disabled{opacity:.45;cursor:default}.bWwFiW_primary{background:var(--dsw-alias-button-primary-fill);border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.bWwFiW_cards{grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:14px;display:grid}.bWwFiW_card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);text-align:left;cursor:pointer;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;padding:10px 14px}.bWwFiW_card b{font-size:20px;display:block}.bWwFiW_card span{color:var(--dsw-alias-label-secondary);font-size:12px}.bWwFiW_filters{flex-wrap:wrap;gap:8px;margin-bottom:14px;display:flex}.bWwFiW_input{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;font-size:13px}.bWwFiW_filters .bWwFiW_input{flex:1;min-width:160px}.bWwFiW_item{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:10px;padding:12px 14px;transition:border-color .12s}.bWwFiW_itemSelected{border-color:var(--dsw-alias-brand-primary)}.bWwFiW_row1{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:6px;display:flex}.bWwFiW_badge{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-tsp-secondary);border-radius:99px;padding:1px 8px;font-size:11px}.bWwFiW_active{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.bWwFiW_archived{color:var(--dsw-alias-state-warn-primary);border-color:var(--dsw-alias-state-warn-primary)}.bWwFiW_forgotten{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.bWwFiW_content{white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-primary)}.bWwFiW_meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:14px;margin-top:6px;font-size:12px;display:flex}.bWwFiW_ops{gap:8px;margin-top:8px;display:flex}.bWwFiW_itemCheck{accent-color:var(--dsw-alias-brand-primary);cursor:pointer;flex:none;width:15px;height:15px;margin-top:2px}.bWwFiW_batchBar{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:12px;padding:8px 12px;display:flex}.bWwFiW_batchBar span{color:var(--dsw-alias-label-primary);margin-right:auto;font-size:13px}.bWwFiW_danger{background:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}.bWwFiW_pager{color:var(--dsw-alias-label-secondary);justify-content:center;align-items:center;gap:10px;margin-top:14px;display:flex}.bWwFiW_empty{text-align:center;color:var(--dsw-alias-label-secondary);padding:40px 0}.bWwFiW_expand{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:10px;padding-top:10px}.bWwFiW_expandLoading{color:var(--dsw-alias-label-secondary);font-size:13px}.bWwFiW_editForm{flex-direction:column;display:flex}.bWwFiW_editForm .bWwFiW_input{box-sizing:border-box;width:100%}.bWwFiW_fieldLabel{color:var(--dsw-alias-label-secondary);margin:10px 0 4px;font-size:12px;display:block}.bWwFiW_modalFoot{justify-content:flex-end;gap:8px;margin-top:12px;display:flex}.bWwFiW_review{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);white-space:pre-wrap;max-height:320px;color:var(--dsw-alias-label-primary);border-radius:8px;margin:0;padding:10px;font-size:12px;overflow:auto}`,document.head.appendChild(e)}var o={cards:`bWwFiW_cards`,ops:`bWwFiW_ops`,scopeTabs:`bWwFiW_scopeTabs`,button:`bWwFiW_button`,modalFoot:`bWwFiW_modalFoot`,expandLoading:`bWwFiW_expandLoading`,editForm:`bWwFiW_editForm`,active:`bWwFiW_active`,expand:`bWwFiW_expand`,toolbar:`bWwFiW_toolbar`,scopeOn:`bWwFiW_scopeOn`,input:`bWwFiW_input`,meta:`bWwFiW_meta`,pager:`bWwFiW_pager`,fieldLabel:`bWwFiW_fieldLabel`,primary:`bWwFiW_primary`,itemCheck:`bWwFiW_itemCheck`,forgotten:`bWwFiW_forgotten`,panel:`bWwFiW_panel`,batchBar:`bWwFiW_batchBar`,empty:`bWwFiW_empty`,review:`bWwFiW_review`,card:`bWwFiW_card`,content:`bWwFiW_content`,item:`bWwFiW_item`,scopeOff:`bWwFiW_scopeOff`,row1:`bWwFiW_row1`,danger:`bWwFiW_danger`,filters:`bWwFiW_filters`,archived:`bWwFiW_archived`,itemSelected:`bWwFiW_itemSelected`,badge:`bWwFiW_badge`};let s=[`fact`,`preference`,`decision`,`episode`,`skill`],c={active:`statusActive`,archived:`statusArchived`,forgotten:`statusForgotten`},l={fact:`kindFact`,preference:`kindPreference`,decision:`kindDecision`,episode:`kindEpisode`,skill:`kindSkill`},u={user:`scopeUser`,project:`scopeProject`},d={write:`opWrite`,update:`opUpdate`,forget:`opForget`,restore:`opRestore`,decay:`opDecay`,superseded:`opSuperseded`,"ingest-request":`opIngestRequest`,"distill-request":`opDistillRequest`},f={supersededBy:`relSupersededBy`,supersedes:`relSupersedes`,contradicts:`relContradicts`,related:`relRelated`},p={kind:`labelKind`,scope:`labelScope`,status:`labelStatus`};function m(e,t){let n=l[t];return n===void 0?t:e(n)}function h(e,t){let n=d[t];return n===void 0?t:e(n)}function g(e,t,n){if(n===null)return``;if(t!==`write`&&t!==`update`&&t!==`superseded`)return` ${n}`;try{let t=JSON.parse(n),r=Object.entries(t);return r.length===0?` ${n}`:` `+r.map(([t,n])=>{let r=p[t];return r===void 0?`${t}=${String(n)}`:t===`kind`&&typeof n==`string`?`${e(r)}=${m(e,n)}`:t===`scope`&&(n===`user`||n===`project`)?`${e(r)}=${e(u[n])}`:`${e(r)}=${String(n)}`}).join(` · `)}catch{return` ${n}`}}function _(e,t){if(t.sourceSessionId===null)return e(`sourceExplicit`);let n=`${t.sourceSessionId.slice(0,16)}…`;return e(`sourceSession`,{id:t.sourceRound===null?n:`${n} ${e(`round`,{n:t.sourceRound})}`})}async function v(e,t){let n=await fetch(`/api/engram/${e}`,t),r=await n.json();if(!n.ok)throw Error(r.error??`HTTP ${String(n.status)}`);return r}function y(e){return new Date(e).toLocaleString(navigator.language||`zh-CN`,{hour12:!1})}function b({t:e,recordId:t,scope:n}){let[a,s]=(0,r.useState)(null);return(0,r.useEffect)(()=>{let r=!1;return v(`review?scope=${n}&id=${encodeURIComponent(t)}`).then(t=>{if(r)return;let n=t.record,i=[`${e(`labelContent`)}: ${n.content}`,`${e(`detailAttributes`)}: ${e(`labelKind`)}=${m(e,n.kind)}, ${e(`labelStatus`)}=${e(c[n.status])}, ${e(`importance`)}=${n.importance.toFixed(2)}, ${e(`confidence`)}=${n.confidence.toFixed(2)}, ${e(`accessCount`,{n:n.accessCount})}`,`${e(`detailSource`)}: ${_(e,n)}`];for(let n of[`supersededBy`,`supersedes`,`contradicts`,`related`]){let r=t[n],a=f[n];r!==void 0&&r.length>0&&i.push(`${a===void 0?n:e(a)}: ${r.join(`, `)}`)}let a=t.operations;a!==void 0&&a.length>0&&(i.push(`${e(`detailOperations`)}:`),a.forEach(t=>i.push(` ${y(t.at)} ${h(e,t.op)}${g(e,t.op,t.detail)}`))),s(i.join(`
|
|
2
|
-
`))}).catch(t=>{r||s(e(`loadFailed`,{msg:t.message}))}),()=>{r=!0}},[t,n,e]),a===null?(0,i.jsx)(`div`,{className:o.expandLoading,children:e(`loading`)}):(0,i.jsx)(`pre`,{className:o.review,children:a})}function x({t:e,record:t,onClose:n,onSaved:a}){let[c,l]=(0,r.useState)(t.content),[u,d]=(0,r.useState)(t.kind),[f,p]=(0,r.useState)(t.importance);return(0,i.jsxs)(`div`,{className:o.editForm,children:[(0,i.jsx)(`label`,{className:o.fieldLabel,children:e(`labelContent`)}),(0,i.jsx)(`textarea`,{className:o.input,rows:3,value:c,onChange:e=>l(e.target.value)}),(0,i.jsx)(`label`,{className:o.fieldLabel,children:e(`labelKind`)}),(0,i.jsx)(`select`,{className:o.input,value:u,onChange:e=>d(e.target.value),children:s.map(t=>(0,i.jsx)(`option`,{value:t,children:m(e,t)},t))}),(0,i.jsx)(`label`,{className:o.fieldLabel,children:`${e(`importance`)} ${f.toFixed(2)}`}),(0,i.jsx)(`input`,{type:`range`,min:0,max:1,step:.05,value:f,onChange:e=>p(Number(e.target.value))}),(0,i.jsxs)(`div`,{className:o.modalFoot,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:n,children:e(`cancel`)}),(0,i.jsx)(`button`,{type:`button`,className:`${o.button} ${o.primary}`,onClick:()=>{v(`update`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope,content:c,kind:u,importance:f})}).then(a).catch(e=>{alert(e.message)})},children:e(`saveWithHint`)})]})]})}function S({t:e}){let[t,n]=(0,r.useState)(`user`),[a,l]=(0,r.useState)(`all`),[d,f]=(0,r.useState)(`all`),[p,h]=(0,r.useState)(``),[g,S]=(0,r.useState)(0),[C,w]=(0,r.useState)(null),[T,E]=(0,r.useState)(null),[D,O]=(0,r.useState)(null),[k,A]=(0,r.useState)(null),[j,M]=(0,r.useState)(0),[N,P]=(0,r.useState)(new Set),[F,I]=(0,r.useState)(!1),L=(0,r.useCallback)(()=>{M(e=>e+1)},[]);(0,r.useEffect)(()=>{let e=!1,n=new URLSearchParams({scope:t,status:a,kind:d,limit:`20`,offset:String(g)});return p!==``&&n.set(`q`,p),v(`list?${n.toString()}`).then(t=>{e||(w(t),O(null))}).catch(t=>{e||O(t.message)}),v(`stats`).then(t=>{e||E(t.parts)}).catch(()=>{}),()=>{e=!0}},[t,a,d,p,g,j]);let R=(e,t)=>{v(e,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope})}).then(L).catch(e=>{alert(e.message)})},z=(e,t)=>{A(n=>n!==null&&n.kind===e&&n.record.id===t.id?null:{kind:e,record:t})},B=C?.records??[],V=B.filter(e=>N.has(e.id)),H=V.filter(e=>e.status===`active`).length,U=V.length-H,W=B.length>0&&B.every(e=>N.has(e.id)),G=e=>{P(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},K=()=>{P(new Set),I(!1)},q=()=>{P(e=>{let t=new Set(e);return W?B.forEach(e=>t.delete(e.id)):B.forEach(e=>t.add(e.id)),t})},J=e=>{let t=e===`forget`?V.filter(e=>e.status===`active`):V.filter(e=>e.status!==`active`);t.length!==0&&Promise.allSettled(t.map(t=>v(e,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope})}))).then(()=>{K(),L()})},Y=()=>{if(F){J(`forget`);return}I(!0)},X=Math.floor(g/20)+1,Z=C===null?1:Math.max(1,Math.ceil(C.total/20));return(0,i.jsxs)(`div`,{className:o.panel,children:[(0,i.jsxs)(`div`,{className:o.toolbar,children:[(0,i.jsx)(`div`,{className:o.scopeTabs,children:[`user`,`project`].map(r=>(0,i.jsx)(`button`,{type:`button`,className:t===r?o.scopeOn:o.scopeOff,onClick:()=>{n(r),S(0),A(null),K()},children:e(u[r])},r))}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:L,children:e(`refresh`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{location.href=`/api/engram/export?scope=${t}&format=markdown`},children:e(`exportMd`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{location.href=`/api/engram/export?scope=${t}&format=json`},children:e(`exportJson`)})]}),(0,i.jsx)(`div`,{className:o.cards,children:(T??[]).map(r=>(0,i.jsxs)(`button`,{type:`button`,className:o.card,style:r.scope===t?{borderColor:`var(--dsw-alias-brand-primary)`}:void 0,onClick:()=>{n(r.scope),S(0),K()},children:[(0,i.jsx)(`b`,{children:r.stats.total}),(0,i.jsx)(`span`,{children:`${e(u[r.scope])} · ${e(`cardActive`,{n:r.stats.active})} · ${e(`signalRatio`,{n:Math.round(r.stats.signalRatio*100)})}`})]},r.scope))}),(0,i.jsxs)(`div`,{className:o.filters,children:[(0,i.jsxs)(`select`,{className:o.input,value:a,onChange:e=>{l(e.target.value),S(0),A(null),K()},children:[(0,i.jsx)(`option`,{value:`all`,children:e(`allStatuses`)}),(0,i.jsx)(`option`,{value:`active`,children:e(`statusActive`)}),(0,i.jsx)(`option`,{value:`archived`,children:e(`statusArchived`)}),(0,i.jsx)(`option`,{value:`forgotten`,children:e(`statusForgotten`)})]}),(0,i.jsxs)(`select`,{className:o.input,value:d,onChange:e=>{f(e.target.value),S(0),A(null),K()},children:[(0,i.jsx)(`option`,{value:`all`,children:e(`allKinds`)}),s.map(t=>(0,i.jsx)(`option`,{value:t,children:m(e,t)},t))]}),(0,i.jsx)(`input`,{className:o.input,placeholder:e(`searchPlaceholder`),value:p,onChange:e=>{h(e.target.value.trim()),S(0),A(null),K()}})]}),N.size>0&&(0,i.jsxs)(`div`,{className:o.batchBar,children:[(0,i.jsx)(`span`,{children:e(`selectedCount`,{n:N.size})}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:q,children:e(W?`deselectAll`:`selectAll`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:U===0,onClick:()=>J(`restore`),children:e(`batchRestore`,{n:U})}),(0,i.jsx)(`button`,{type:`button`,className:F?`${o.button} ${o.danger}`:o.button,disabled:H===0,onClick:Y,children:e(F?`batchForgetConfirm`:`batchForget`,{n:H})}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:K,children:e(`clearSelection`)})]}),D!==null&&(0,i.jsx)(`div`,{className:o.empty,children:e(`loadFailed`,{msg:D})}),D===null&&C!==null&&C.records.length===0&&(0,i.jsx)(`div`,{className:o.empty,children:e(`empty`)}),(C?.records??[]).map(t=>(0,i.jsxs)(`div`,{className:N.has(t.id)?`${o.item} ${o.itemSelected}`:o.item,children:[(0,i.jsxs)(`div`,{className:o.row1,children:[(0,i.jsx)(`input`,{type:`checkbox`,className:o.itemCheck,checked:N.has(t.id),"aria-label":t.content.slice(0,24),onChange:()=>G(t.id)}),(0,i.jsx)(`span`,{className:`${o.badge} ${o[t.status]}`,children:e(c[t.status])}),(0,i.jsx)(`span`,{className:o.badge,children:m(e,t.kind)}),(0,i.jsx)(`span`,{className:o.badge,children:e(u[t.scope])})]}),(0,i.jsx)(`div`,{className:o.content,children:t.content}),(0,i.jsxs)(`div`,{className:o.meta,children:[(0,i.jsx)(`span`,{children:`${e(`importance`)} ${t.importance.toFixed(2)}`}),(0,i.jsx)(`span`,{children:`${e(`confidence`)} ${t.confidence.toFixed(2)}`}),(0,i.jsx)(`span`,{children:e(`accessCount`,{n:t.accessCount})}),(0,i.jsx)(`span`,{children:_(e,t)}),(0,i.jsx)(`span`,{children:y(t.createdAt)})]}),(0,i.jsxs)(`div`,{className:o.ops,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{z(`review`,t)},children:e(`detail`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{z(`edit`,t)},children:e(`edit`)}),t.status===`active`?(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{R(`forget`,t)},children:e(`forget`)}):(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{R(`restore`,t)},children:e(`restore`)})]}),k!==null&&k.record.id===t.id&&(0,i.jsx)(`div`,{className:o.expand,children:k.kind===`review`?(0,i.jsx)(b,{t:e,recordId:t.id,scope:t.scope}):(0,i.jsx)(x,{t:e,record:t,onClose:()=>{A(null)},onSaved:()=>{A(null),L()}})})]},t.id)),(0,i.jsxs)(`div`,{className:o.pager,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:g===0,onClick:()=>{S(Math.max(0,g-20)),A(null),K()},children:e(`prevPage`)}),(0,i.jsx)(`span`,{children:e(`pagerInfo`,{page:X,pages:Z,total:C?.total??0})}),(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:C===null||g+20>=C.total,onClick:()=>{S(g+20),A(null),K()},children:e(`nextPage`)})]})]})}let C=`engram`,w={nav:`记忆库`,scopeUser:`用户级`,scopeProject:`项目级`,refresh:`刷新`,exportMd:`导出 MD`,exportJson:`导出 JSON`,allStatuses:`全部状态`,allKinds:`全部种类`,searchPlaceholder:`按内容搜索…`,statusActive:`生效中`,statusArchived:`已归档`,statusForgotten:`已遗忘`,kindFact:`事实`,kindPreference:`偏好`,kindDecision:`决策`,kindEpisode:`事件`,kindSkill:`技能`,labelContent:`内容`,labelKind:`种类`,labelStatus:`状态`,labelScope:`作用域`,importance:`重要性`,confidence:`置信`,accessCount:`访问 {n} 次`,sourceExplicit:`显式保存`,sourceSession:`来源 {id}`,round:`第 {n} 轮`,cardActive:`生效 {n}`,signalRatio:`信噪比 {n}%`,detail:`详情`,edit:`编辑`,forget:`遗忘`,restore:`恢复`,empty:`没有符合条件的记忆`,loadFailed:`加载失败:{msg}`,loading:`加载中…`,pagerInfo:`第 {page} / {pages} 页 · 共 {total} 条`,prevPage:`上一页`,nextPage:`下一页`,cancel:`取消`,saveWithHint:`保存(旧条目归档)`,detailAttributes:`属性`,detailSource:`来源`,detailRelations:`关系`,detailOperations:`最近操作`,relSupersededBy:`被取代`,relSupersedes:`取代`,relContradicts:`矛盾`,relRelated:`关联`,opWrite:`写入`,opUpdate:`更新`,opForget:`遗忘`,opRestore:`恢复`,opDecay:`衰减归档`,opSuperseded:`被取代`,opIngestRequest:`摄取请求`,opDistillRequest:`蒸馏请求`,selectAll:`全选本页`,deselectAll:`取消全选`,selectedCount:`已选 {n} 条`,batchForget:`批量遗忘({n})`,batchForgetConfirm:`确认遗忘 {n} 条?`,batchRestore:`批量恢复({n})`,clearSelection:`取消选择`},T={nav:`Memory Library`,scopeUser:`User`,scopeProject:`Project`,refresh:`Refresh`,exportMd:`Export MD`,exportJson:`Export JSON`,allStatuses:`All statuses`,allKinds:`All kinds`,searchPlaceholder:`Search content…`,statusActive:`active`,statusArchived:`archived`,statusForgotten:`forgotten`,kindFact:`fact`,kindPreference:`preference`,kindDecision:`decision`,kindEpisode:`episode`,kindSkill:`skill`,labelContent:`Content`,labelKind:`Kind`,labelStatus:`Status`,labelScope:`Scope`,importance:`Importance`,confidence:`Confidence`,accessCount:`{n}× accessed`,sourceExplicit:`Explicitly saved`,sourceSession:`Source {id}`,round:`round {n}`,cardActive:`{n} active`,signalRatio:`signal {n}%`,detail:`Details`,edit:`Edit`,forget:`Forget`,restore:`Restore`,empty:`No matching memories`,loadFailed:`Failed to load: {msg}`,loading:`Loading…`,pagerInfo:`Page {page} / {pages} · {total} records`,prevPage:`Prev`,nextPage:`Next`,cancel:`Cancel`,saveWithHint:`Save (archives old entry)`,detailAttributes:`Attributes`,detailSource:`Source`,detailRelations:`Relations`,detailOperations:`Recent operations`,relSupersededBy:`Superseded by`,relSupersedes:`Supersedes`,relContradicts:`Contradicts`,relRelated:`Related`,opWrite:`write`,opUpdate:`update`,opForget:`forget`,opRestore:`restore`,opDecay:`decay`,opSuperseded:`superseded`,opIngestRequest:`ingest-request`,opDistillRequest:`distill-request`,selectAll:`Select all on page`,deselectAll:`Deselect all`,selectedCount:`{n} selected`,batchForget:`Forget selected ({n})`,batchForgetConfirm:`Forget {n} entries?`,batchRestore:`Restore selected ({n})`,clearSelection:`Clear selection`},E=[`slots`,`locale`];function D(e){e.effect(()=>e.locale.register(C,{zh:w,en:T}),`dsh-engram: dictionaries`);let t=e.locale.bind(C);e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`engram`,order:20,label:()=>t(`nav`),locale:C},S))}return n.apply=D,n.inject=E,t.exports}});
|
|
1
|
+
window.__ModuleLoader__.load({id:`@kenz1117/dsh-engram`,factory:e=>{var t={exports:{}},n=t.exports;Object.defineProperty(n,Symbol.toStringTag,{value:`Module`});let r=e("react"),i=e("react/jsx-runtime"),a=`@kenz1117/dsh-engram/panel.module.css`;if(typeof document<`u`&&document.querySelector(`style[data-plugin-css=`+JSON.stringify(a)+`]`)===null){let e=document.createElement(`style`);e.dataset.plugin=`@kenz1117/dsh-engram`,e.dataset.pluginCss=a,e.textContent=`.bWwFiW_panel{max-width:920px;animation:.24s both bWwFiW_rise}@keyframes bWwFiW_rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.bWwFiW_toolbar{flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:16px;display:flex}.bWwFiW_scopeTabs{border-bottom:1px solid var(--dsw-alias-border-l2);align-items:flex-end;gap:20px;margin-right:auto;display:flex}.bWwFiW_scopeOn,.bWwFiW_scopeOff{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:0;padding:7px 1px 9px;font-size:13px;line-height:20px;transition:color .14s;position:relative}.bWwFiW_scopeOn,.bWwFiW_scopeOff:hover{color:var(--dsw-alias-label-primary)}.bWwFiW_scopeOn:after{background:var(--dsw-alias-brand-primary);content:"";border-radius:2px 2px 0 0;height:2px;position:absolute;bottom:-1px;left:0;right:0}.bWwFiW_button{color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;background:0 0;border-radius:8px;padding:5px 12px;font-size:13px;line-height:18px;transition:color .14s,border-color .14s,background .14s,transform .1s}.bWwFiW_button:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 7%, transparent)}.bWwFiW_button:active{transform:scale(.97)}.bWwFiW_button:disabled{opacity:.4;cursor:default;border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:0 0;transform:none}.bWwFiW_primary{background:var(--dsw-alias-button-primary-fill);border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.bWwFiW_primary:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);filter:brightness(1.08)}.bWwFiW_cards{grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:16px;display:grid}.bWwFiW_card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);text-align:left;cursor:pointer;color:var(--dsw-alias-label-primary);font:inherit;border-radius:12px;padding:14px 16px 12px;transition:border-color .16s,transform .16s,box-shadow .16s;position:relative;overflow:hidden}.bWwFiW_card:hover{border-color:var(--dsw-alias-border-l1,var(--dsw-alias-brand-primary));box-shadow:0 6px 18px -8px color-mix(in srgb, var(--dsw-alias-label-primary) 22%, transparent);transform:translateY(-1px)}.bWwFiW_card b{font-variant-numeric:tabular-nums;letter-spacing:-.01em;font-size:26px;line-height:1.15;display:block}.bWwFiW_card span{color:var(--dsw-alias-label-secondary);font-size:12px}.bWwFiW_cardOn{border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 55%, transparent);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 5%, var(--dsw-alias-bg-layer-1))}.bWwFiW_cardOn:before{background:var(--dsw-alias-brand-primary);content:"";width:3px;position:absolute;top:0;bottom:0;left:0}.bWwFiW_cardMeter{background:color-mix(in srgb, var(--dsw-alias-label-tertiary) 18%, transparent);border-radius:2px;height:3px;margin-top:10px;display:block;overflow:hidden}.bWwFiW_cardMeter i{background:var(--dsw-alias-brand-primary);border-radius:2px;height:100%;transition:width .3s;display:block}.bWwFiW_filters{flex-wrap:wrap;gap:8px;margin-bottom:16px;display:flex}.bWwFiW_input{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 10px;font-size:13px;transition:border-color .14s,box-shadow .14s}.bWwFiW_input:focus-visible{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary) 18%, transparent);outline:none}.bWwFiW_filters .bWwFiW_input{flex:1;min-width:160px}.bWwFiW_filters select.bWwFiW_input{flex:0 auto;min-width:120px}.bWwFiW_item{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;margin-bottom:10px;padding:13px 16px 11px;transition:border-color .16s,background .16s,box-shadow .16s;animation:.26s both bWwFiW_rise;position:relative}.bWwFiW_item:hover{border-color:color-mix(in srgb, var(--dsw-alias-brand-primary) 40%, var(--dsw-alias-border-l2));box-shadow:0 8px 22px -12px color-mix(in srgb, var(--dsw-alias-label-primary) 26%, transparent)}.bWwFiW_itemSelected{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 4%, var(--dsw-alias-bg-layer-1))}.bWwFiW_row1{flex-wrap:wrap;align-items:center;gap:10px;margin-bottom:7px;display:flex}.bWwFiW_itemCheck{accent-color:var(--dsw-alias-brand-primary);cursor:pointer;flex:none;width:15px;height:15px}.bWwFiW_statusWrap{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;display:inline-flex}.bWwFiW_statusWrap:before{background:var(--dsw-alias-label-tertiary);content:"";border-radius:50%;width:7px;height:7px}.bWwFiW_statusWrap.bWwFiW_active{color:var(--dsw-alias-state-success-primary)}.bWwFiW_statusWrap.bWwFiW_active:before{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 0 3px color-mix(in srgb, var(--dsw-alias-state-success-primary) 16%, transparent)}.bWwFiW_statusWrap.bWwFiW_archived{color:var(--dsw-alias-state-warn-primary)}.bWwFiW_statusWrap.bWwFiW_archived:before{background:var(--dsw-alias-state-warn-primary)}.bWwFiW_statusWrap.bWwFiW_forgotten{color:var(--dsw-alias-state-error-primary)}.bWwFiW_statusWrap.bWwFiW_forgotten:before{background:var(--dsw-alias-state-error-primary)}.bWwFiW_chip{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 10%, transparent);border-radius:6px;padding:0 8px;font-size:11px;line-height:18px}.bWwFiW_scopeTag{color:var(--dsw-alias-label-tertiary);font-size:11px}.bWwFiW_timeTag{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;margin-left:auto;font-size:12px}.bWwFiW_content{white-space:pre-wrap;word-break:break-word;color:var(--dsw-alias-label-primary);-webkit-line-clamp:4;-webkit-box-orient:vertical;font-size:14px;line-height:1.7;display:-webkit-box;overflow:hidden}.bWwFiW_meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;align-items:center;gap:16px;margin-top:8px;font-size:12px;display:flex}.bWwFiW_meterField{font-variant-numeric:tabular-nums;align-items:center;gap:7px;display:inline-flex}.bWwFiW_meter{background:color-mix(in srgb, var(--dsw-alias-label-tertiary) 18%, transparent);border-radius:2px;width:52px;height:4px;overflow:hidden}.bWwFiW_meter i{background:var(--dsw-alias-brand-primary);border-radius:2px;height:100%;display:block}.bWwFiW_meter.bWwFiW_conf i{background:var(--dsw-alias-state-success-primary)}.bWwFiW_ops{opacity:.55;gap:6px;margin-top:9px;transition:opacity .16s;display:flex}.bWwFiW_item:hover .bWwFiW_ops,.bWwFiW_item:focus-within .bWwFiW_ops{opacity:1}.bWwFiW_ops .bWwFiW_button{background:var(--dsw-alias-fill-tsp-secondary);border-color:#0000;padding:3px 10px;font-size:12px}.bWwFiW_ops .bWwFiW_button:hover{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 8%, transparent)}.bWwFiW_batchBar{z-index:5;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 82%, transparent);backdrop-filter:blur(10px);border:1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));box-shadow:0 10px 28px -12px color-mix(in srgb, var(--dsw-alias-label-primary) 30%, transparent);border-radius:12px;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:12px;padding:9px 14px;animation:.2s both bWwFiW_barUp;display:flex;position:sticky;bottom:12px}@keyframes bWwFiW_barUp{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.bWwFiW_batchBar>span{color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums;margin-right:auto;font-size:13px}.bWwFiW_danger{background:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground)}.bWwFiW_danger:hover{background:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-label-primary-foreground);filter:brightness(1.08)}.bWwFiW_skeleton{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;margin-bottom:10px;padding:13px 16px}.bWwFiW_skeleton i{background:linear-gradient(90deg, color-mix(in srgb, var(--dsw-alias-label-tertiary) 12%, transparent) 25%, color-mix(in srgb, var(--dsw-alias-label-tertiary) 24%, transparent) 50%, color-mix(in srgb, var(--dsw-alias-label-tertiary) 12%, transparent) 75%);background-size:200% 100%;border-radius:6px;height:12px;margin:8px 0;animation:1.3s linear infinite bWwFiW_shimmer;display:block}.bWwFiW_skeleton i:first-child{width:34%;margin-top:2px}.bWwFiW_skeleton i:last-child{width:62%}@keyframes bWwFiW_shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.bWwFiW_empty{text-align:center;color:var(--dsw-alias-label-secondary);padding:48px 0 52px}.bWwFiW_empty:before{border:1px solid var(--dsw-alias-border-l2);background:linear-gradient(135deg, color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent), transparent 70%);content:"";border-radius:10px;width:34px;height:34px;margin:0 auto 12px;display:block;transform:rotate(45deg)scale(.8)}.bWwFiW_expand{border-top:1px dashed var(--dsw-alias-border-l2);margin-top:10px;padding-top:12px;animation:.18s both bWwFiW_rise}.bWwFiW_expandLoading{color:var(--dsw-alias-label-secondary);font-size:13px}.bWwFiW_attrGrid{grid-template-columns:max-content 1fr;align-items:center;gap:7px 16px;font-size:13px;display:grid}.bWwFiW_attrGrid>dt{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}.bWwFiW_attrGrid>dd{color:var(--dsw-alias-label-primary);flex-wrap:wrap;align-items:center;gap:8px;margin:0;display:flex}.bWwFiW_relBlock{margin-top:12px}.bWwFiW_relBlock>h5{color:var(--dsw-alias-label-tertiary);margin:0 0 6px;font-size:12px;font-weight:500}.bWwFiW_relChips{flex-wrap:wrap;gap:6px;display:flex}.bWwFiW_relChip{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-base);border-radius:6px;padding:2px 8px;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:11px}.bWwFiW_timeline{margin:12px 0 0;padding:0 0 0 14px;list-style:none;position:relative}.bWwFiW_timeline:before{background:var(--dsw-alias-border-l2);content:"";width:1px;position:absolute;top:6px;bottom:6px;left:3px}.bWwFiW_timeline li{color:var(--dsw-alias-label-secondary);padding:5px 0 5px 12px;font-size:12px;line-height:1.6;position:relative}.bWwFiW_timeline li:before{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-brand-primary);content:"";border-radius:50%;width:7px;height:7px;position:absolute;top:10px;left:-14px}.bWwFiW_timeline time{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-tertiary);margin-right:8px}.bWwFiW_timeline b{color:var(--dsw-alias-label-primary);font-weight:500}.bWwFiW_editForm{flex-direction:column;display:flex}.bWwFiW_editForm .bWwFiW_input{box-sizing:border-box;width:100%}.bWwFiW_fieldLabel{color:var(--dsw-alias-label-secondary);margin:12px 0 5px;font-size:12px;display:block}.bWwFiW_editForm input[type=range]{accent-color:var(--dsw-alias-brand-primary)}.bWwFiW_modalFoot{justify-content:flex-end;gap:8px;margin-top:14px;display:flex}.bWwFiW_pager{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;justify-content:center;align-items:center;gap:12px;margin-top:18px;font-size:13px;display:flex}`,document.head.appendChild(e)}var o={attrGrid:`bWwFiW_attrGrid`,relChips:`bWwFiW_relChips`,editForm:`bWwFiW_editForm`,modalFoot:`bWwFiW_modalFoot`,pager:`bWwFiW_pager`,item:`bWwFiW_item`,scopeTabs:`bWwFiW_scopeTabs`,toolbar:`bWwFiW_toolbar`,archived:`bWwFiW_archived`,empty:`bWwFiW_empty`,meta:`bWwFiW_meta`,skeleton:`bWwFiW_skeleton`,expand:`bWwFiW_expand`,rise:`bWwFiW_rise`,cardOn:`bWwFiW_cardOn`,forgotten:`bWwFiW_forgotten`,relBlock:`bWwFiW_relBlock`,timeline:`bWwFiW_timeline`,button:`bWwFiW_button`,card:`bWwFiW_card`,itemSelected:`bWwFiW_itemSelected`,cardMeter:`bWwFiW_cardMeter`,filters:`bWwFiW_filters`,active:`bWwFiW_active`,relChip:`bWwFiW_relChip`,cards:`bWwFiW_cards`,row1:`bWwFiW_row1`,scopeTag:`bWwFiW_scopeTag`,scopeOn:`bWwFiW_scopeOn`,itemCheck:`bWwFiW_itemCheck`,scopeOff:`bWwFiW_scopeOff`,timeTag:`bWwFiW_timeTag`,content:`bWwFiW_content`,meterField:`bWwFiW_meterField`,conf:`bWwFiW_conf`,batchBar:`bWwFiW_batchBar`,barUp:`bWwFiW_barUp`,chip:`bWwFiW_chip`,danger:`bWwFiW_danger`,shimmer:`bWwFiW_shimmer`,expandLoading:`bWwFiW_expandLoading`,fieldLabel:`bWwFiW_fieldLabel`,meter:`bWwFiW_meter`,input:`bWwFiW_input`,primary:`bWwFiW_primary`,ops:`bWwFiW_ops`,panel:`bWwFiW_panel`,statusWrap:`bWwFiW_statusWrap`};let s=[`fact`,`preference`,`decision`,`episode`,`skill`],c={active:`statusActive`,archived:`statusArchived`,forgotten:`statusForgotten`},l={fact:`kindFact`,preference:`kindPreference`,decision:`kindDecision`,episode:`kindEpisode`,skill:`kindSkill`},u={user:`scopeUser`,project:`scopeProject`},d={write:`opWrite`,update:`opUpdate`,forget:`opForget`,restore:`opRestore`,decay:`opDecay`,superseded:`opSuperseded`,"ingest-request":`opIngestRequest`,"distill-request":`opDistillRequest`},f={supersededBy:`relSupersededBy`,supersedes:`relSupersedes`,contradicts:`relContradicts`,related:`relRelated`},p={kind:`labelKind`,scope:`labelScope`,status:`labelStatus`};function m(e,t){let n=l[t];return n===void 0?t:e(n)}function h(e,t){let n=d[t];return n===void 0?t:e(n)}function g(e,t,n){if(n===null)return``;if(t!==`write`&&t!==`update`&&t!==`superseded`)return` ${n}`;try{let t=JSON.parse(n),r=Object.entries(t);return r.length===0?` ${n}`:` `+r.map(([t,n])=>{let r=p[t];return r===void 0?`${t}=${String(n)}`:t===`kind`&&typeof n==`string`?`${e(r)}=${m(e,n)}`:t===`scope`&&(n===`user`||n===`project`)?`${e(r)}=${e(u[n])}`:`${e(r)}=${String(n)}`}).join(` · `)}catch{return` ${n}`}}function _(e,t){if(t.sourceSessionId===null)return e(`sourceExplicit`);let n=`${t.sourceSessionId.slice(0,16)}…`;return e(`sourceSession`,{id:t.sourceRound===null?n:`${n} ${e(`round`,{n:t.sourceRound})}`})}async function v(e,t){let n=await fetch(`/api/engram/${e}`,t),r=await n.json();if(!n.ok)throw Error(r.error??`HTTP ${String(n.status)}`);return r}function y(e){return new Date(e).toLocaleString(navigator.language||`zh-CN`,{hour12:!1})}function b(e,t){let n=Math.max(0,Date.now()-t),r=Math.floor(n/6e4);if(r<1)return e(`timeJustNow`);if(r<60)return e(`timeMinutesAgo`,{n:r});let i=Math.floor(r/60);return i<48?e(`timeHoursAgo`,{n:i}):e(`timeDaysAgo`,{n:Math.floor(i/24)})}function x({value:e,conf:t}){return(0,i.jsx)(`span`,{className:t===!0?`${o.meter} ${o.conf}`:o.meter,children:(0,i.jsx)(`i`,{style:{width:`${String(Math.round(Math.min(1,Math.max(0,e))*100))}%`}})})}function S({ids:e}){return(0,i.jsx)(`div`,{className:o.relChips,children:e.map(e=>(0,i.jsx)(`span`,{className:o.relChip,title:e,children:e.slice(0,18)},e))})}function C({t:e,recordId:t,scope:n}){let[a,s]=(0,r.useState)(null),[l,u]=(0,r.useState)(null);if((0,r.useEffect)(()=>{let e=!1;return v(`review?scope=${n}&id=${encodeURIComponent(t)}`).then(t=>{e||s(t)}).catch(t=>{e||u(t.message)}),()=>{e=!0}},[t,n,e]),l!==null)return(0,i.jsx)(`div`,{className:o.expandLoading,children:e(`loadFailed`,{msg:l})});if(a===null)return(0,i.jsx)(`div`,{className:o.expandLoading,children:e(`loading`)});let{record:d}=a,p=[`supersededBy`,`supersedes`,`contradicts`,`related`].map(e=>({key:e,label:f[e],ids:a[e]??[]})).filter(e=>e.ids.length>0),b=a.operations??[];return(0,i.jsxs)(`div`,{children:[(0,i.jsxs)(`dl`,{className:o.attrGrid,children:[(0,i.jsx)(`dt`,{children:e(`labelContent`)}),(0,i.jsx)(`dd`,{children:d.content}),(0,i.jsx)(`dt`,{children:e(`labelKind`)}),(0,i.jsx)(`dd`,{children:(0,i.jsx)(`span`,{className:o.chip,children:m(e,d.kind)})}),(0,i.jsx)(`dt`,{children:e(`labelStatus`)}),(0,i.jsx)(`dd`,{children:(0,i.jsx)(`span`,{className:`${o.statusWrap} ${o[d.status]}`,children:e(c[d.status])})}),(0,i.jsx)(`dt`,{children:e(`importance`)}),(0,i.jsxs)(`dd`,{children:[d.importance.toFixed(2),(0,i.jsx)(x,{value:d.importance})]}),(0,i.jsx)(`dt`,{children:e(`confidence`)}),(0,i.jsxs)(`dd`,{children:[d.confidence.toFixed(2),(0,i.jsx)(x,{value:d.confidence,conf:!0})]}),(0,i.jsx)(`dt`,{children:e(`detailSource`)}),(0,i.jsx)(`dd`,{children:_(e,d)}),(0,i.jsx)(`dt`,{children:e(`labelCreated`)}),(0,i.jsx)(`dd`,{children:y(d.createdAt)})]}),p.map(({key:t,label:n,ids:r})=>(0,i.jsxs)(`div`,{className:o.relBlock,children:[(0,i.jsx)(`h5`,{children:n===void 0?t:e(n)}),(0,i.jsx)(S,{ids:r})]},t)),b.length>0&&(0,i.jsxs)(`div`,{className:o.relBlock,children:[(0,i.jsx)(`h5`,{children:e(`detailOperations`)}),(0,i.jsx)(`ul`,{className:o.timeline,children:b.map((t,n)=>(0,i.jsxs)(`li`,{children:[(0,i.jsx)(`time`,{children:y(t.at)}),(0,i.jsx)(`b`,{children:h(e,t.op)}),g(e,t.op,t.detail)]},n))})]})]})}function w({t:e,record:t,onClose:n,onSaved:a}){let[c,l]=(0,r.useState)(t.content),[u,d]=(0,r.useState)(t.kind),[f,p]=(0,r.useState)(t.importance);return(0,i.jsxs)(`div`,{className:o.editForm,children:[(0,i.jsx)(`label`,{className:o.fieldLabel,children:e(`labelContent`)}),(0,i.jsx)(`textarea`,{className:o.input,rows:3,value:c,onChange:e=>l(e.target.value)}),(0,i.jsx)(`label`,{className:o.fieldLabel,children:e(`labelKind`)}),(0,i.jsx)(`select`,{className:o.input,value:u,onChange:e=>d(e.target.value),children:s.map(t=>(0,i.jsx)(`option`,{value:t,children:m(e,t)},t))}),(0,i.jsx)(`label`,{className:o.fieldLabel,children:`${e(`importance`)} ${f.toFixed(2)}`}),(0,i.jsx)(`input`,{type:`range`,min:0,max:1,step:.05,value:f,onChange:e=>p(Number(e.target.value))}),(0,i.jsxs)(`div`,{className:o.modalFoot,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:n,children:e(`cancel`)}),(0,i.jsx)(`button`,{type:`button`,className:`${o.button} ${o.primary}`,onClick:()=>{v(`update`,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope,content:c,kind:u,importance:f})}).then(a).catch(e=>{alert(e.message)})},children:e(`saveWithHint`)})]})]})}function T({t:e}){let[t,n]=(0,r.useState)(`user`),[a,l]=(0,r.useState)(`all`),[d,f]=(0,r.useState)(`all`),[p,h]=(0,r.useState)(``),[g,S]=(0,r.useState)(0),[T,E]=(0,r.useState)(null),[D,O]=(0,r.useState)(null),[k,A]=(0,r.useState)(null),[j,M]=(0,r.useState)(null),[N,P]=(0,r.useState)(0),[F,I]=(0,r.useState)(new Set),[L,R]=(0,r.useState)(!1),z=(0,r.useCallback)(()=>{P(e=>e+1)},[]);(0,r.useEffect)(()=>{let e=!1,n=new URLSearchParams({scope:t,status:a,kind:d,limit:`20`,offset:String(g)});return p!==``&&n.set(`q`,p),v(`list?${n.toString()}`).then(t=>{e||(E(t),A(null))}).catch(t=>{e||A(t.message)}),v(`stats`).then(t=>{e||O(t.parts)}).catch(()=>{}),()=>{e=!0}},[t,a,d,p,g,N]);let B=(e,t)=>{v(e,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope})}).then(z).catch(e=>{alert(e.message)})},V=(e,t)=>{M(n=>n!==null&&n.kind===e&&n.record.id===t.id?null:{kind:e,record:t})},H=T?.records??[],U=H.filter(e=>F.has(e.id)),W=U.filter(e=>e.status===`active`).length,G=U.length-W,K=H.length>0&&H.every(e=>F.has(e.id)),q=e=>{I(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},J=()=>{I(new Set),R(!1)},Y=()=>{I(e=>{let t=new Set(e);return K?H.forEach(e=>t.delete(e.id)):H.forEach(e=>t.add(e.id)),t})},X=e=>{let t=e===`forget`?U.filter(e=>e.status===`active`):U.filter(e=>e.status!==`active`);t.length!==0&&Promise.allSettled(t.map(t=>v(e,{method:`POST`,headers:{"content-type":`application/json`},body:JSON.stringify({id:t.id,scope:t.scope})}))).then(()=>{J(),z()})},Z=()=>{if(L){X(`forget`);return}R(!0)},Q=Math.floor(g/20)+1,$=T===null?1:Math.max(1,Math.ceil(T.total/20));return(0,i.jsxs)(`div`,{className:o.panel,children:[(0,i.jsxs)(`div`,{className:o.toolbar,children:[(0,i.jsx)(`div`,{className:o.scopeTabs,children:[`user`,`project`].map(r=>(0,i.jsx)(`button`,{type:`button`,className:t===r?o.scopeOn:o.scopeOff,onClick:()=>{n(r),S(0),M(null),J()},children:e(u[r])},r))}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:z,children:e(`refresh`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{location.href=`/api/engram/export?scope=${t}&format=markdown`},children:e(`exportMd`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{location.href=`/api/engram/export?scope=${t}&format=json`},children:e(`exportJson`)})]}),(0,i.jsx)(`div`,{className:o.cards,children:(D??[]).map(r=>(0,i.jsxs)(`button`,{type:`button`,className:r.scope===t?`${o.card} ${o.cardOn}`:o.card,onClick:()=>{n(r.scope),S(0),J()},children:[(0,i.jsx)(`b`,{children:r.stats.total}),(0,i.jsx)(`span`,{children:`${e(u[r.scope])} · ${e(`cardActive`,{n:r.stats.active})} · ${e(`signalRatio`,{n:Math.round(r.stats.signalRatio*100)})}`}),(0,i.jsx)(`span`,{className:o.cardMeter,children:(0,i.jsx)(`i`,{style:{width:`${String(Math.round(r.stats.signalRatio*100))}%`}})})]},r.scope))}),(0,i.jsxs)(`div`,{className:o.filters,children:[(0,i.jsxs)(`select`,{className:o.input,value:a,onChange:e=>{l(e.target.value),S(0),M(null),J()},children:[(0,i.jsx)(`option`,{value:`all`,children:e(`allStatuses`)}),(0,i.jsx)(`option`,{value:`active`,children:e(`statusActive`)}),(0,i.jsx)(`option`,{value:`archived`,children:e(`statusArchived`)}),(0,i.jsx)(`option`,{value:`forgotten`,children:e(`statusForgotten`)})]}),(0,i.jsxs)(`select`,{className:o.input,value:d,onChange:e=>{f(e.target.value),S(0),M(null),J()},children:[(0,i.jsx)(`option`,{value:`all`,children:e(`allKinds`)}),s.map(t=>(0,i.jsx)(`option`,{value:t,children:m(e,t)},t))]}),(0,i.jsx)(`input`,{className:o.input,placeholder:e(`searchPlaceholder`),value:p,onChange:e=>{h(e.target.value.trim()),S(0),M(null),J()}})]}),F.size>0&&(0,i.jsxs)(`div`,{className:o.batchBar,children:[(0,i.jsx)(`span`,{children:e(`selectedCount`,{n:F.size})}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:Y,children:e(K?`deselectAll`:`selectAll`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:G===0,onClick:()=>X(`restore`),children:e(`batchRestore`,{n:G})}),(0,i.jsx)(`button`,{type:`button`,className:L?`${o.button} ${o.danger}`:o.button,disabled:W===0,onClick:Z,children:e(L?`batchForgetConfirm`:`batchForget`,{n:W})}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:J,children:e(`clearSelection`)})]}),k!==null&&(0,i.jsx)(`div`,{className:o.empty,children:e(`loadFailed`,{msg:k})}),k===null&&T===null&&[0,1,2].map(e=>(0,i.jsxs)(`div`,{className:o.skeleton,children:[(0,i.jsx)(`i`,{}),(0,i.jsx)(`i`,{}),(0,i.jsx)(`i`,{})]},e)),k===null&&T!==null&&T.records.length===0&&(0,i.jsx)(`div`,{className:o.empty,children:e(`empty`)}),(T?.records??[]).map((t,n)=>(0,i.jsxs)(`div`,{className:F.has(t.id)?`${o.item} ${o.itemSelected}`:o.item,style:{animationDelay:`${String(Math.min(n,12)*28)}ms`},children:[(0,i.jsxs)(`div`,{className:o.row1,children:[(0,i.jsx)(`input`,{type:`checkbox`,className:o.itemCheck,checked:F.has(t.id),"aria-label":t.content.slice(0,24),onChange:()=>q(t.id)}),(0,i.jsx)(`span`,{className:`${o.statusWrap} ${o[t.status]}`,children:e(c[t.status])}),(0,i.jsx)(`span`,{className:o.chip,children:m(e,t.kind)}),(0,i.jsx)(`span`,{className:o.scopeTag,children:e(u[t.scope])}),(0,i.jsx)(`span`,{className:o.timeTag,title:y(t.createdAt),children:b(e,t.createdAt)})]}),(0,i.jsx)(`div`,{className:o.content,children:t.content}),(0,i.jsxs)(`div`,{className:o.meta,children:[(0,i.jsxs)(`span`,{className:o.meterField,children:[e(`importance`),(0,i.jsx)(x,{value:t.importance}),t.importance.toFixed(2)]}),(0,i.jsxs)(`span`,{className:o.meterField,children:[e(`confidence`),(0,i.jsx)(x,{value:t.confidence,conf:!0}),t.confidence.toFixed(2)]}),(0,i.jsx)(`span`,{children:e(`accessCount`,{n:t.accessCount})}),(0,i.jsx)(`span`,{children:_(e,t)})]}),(0,i.jsxs)(`div`,{className:o.ops,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{V(`review`,t)},children:e(`detail`)}),(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{V(`edit`,t)},children:e(`edit`)}),t.status===`active`?(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{B(`forget`,t)},children:e(`forget`)}):(0,i.jsx)(`button`,{type:`button`,className:o.button,onClick:()=>{B(`restore`,t)},children:e(`restore`)})]}),j!==null&&j.record.id===t.id&&(0,i.jsx)(`div`,{className:o.expand,children:j.kind===`review`?(0,i.jsx)(C,{t:e,recordId:t.id,scope:t.scope}):(0,i.jsx)(w,{t:e,record:t,onClose:()=>{M(null)},onSaved:()=>{M(null),z()}})})]},t.id)),(0,i.jsxs)(`div`,{className:o.pager,children:[(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:g===0,onClick:()=>{S(Math.max(0,g-20)),M(null),J()},children:e(`prevPage`)}),(0,i.jsx)(`span`,{children:e(`pagerInfo`,{page:Q,pages:$,total:T?.total??0})}),(0,i.jsx)(`button`,{type:`button`,className:o.button,disabled:T===null||g+20>=T.total,onClick:()=>{S(g+20),M(null),J()},children:e(`nextPage`)})]})]})}let E=`engram`,D={nav:`记忆库`,scopeUser:`用户级`,scopeProject:`项目级`,refresh:`刷新`,exportMd:`导出 MD`,exportJson:`导出 JSON`,allStatuses:`全部状态`,allKinds:`全部种类`,searchPlaceholder:`按内容搜索…`,statusActive:`生效中`,statusArchived:`已归档`,statusForgotten:`已遗忘`,kindFact:`事实`,kindPreference:`偏好`,kindDecision:`决策`,kindEpisode:`事件`,kindSkill:`技能`,labelContent:`内容`,labelKind:`种类`,labelStatus:`状态`,labelScope:`作用域`,importance:`重要性`,confidence:`置信`,accessCount:`访问 {n} 次`,labelCreated:`创建时间`,timeJustNow:`刚刚`,timeMinutesAgo:`{n} 分钟前`,timeHoursAgo:`{n} 小时前`,timeDaysAgo:`{n} 天前`,sourceExplicit:`显式保存`,sourceSession:`来源 {id}`,round:`第 {n} 轮`,cardActive:`生效 {n}`,signalRatio:`信噪比 {n}%`,detail:`详情`,edit:`编辑`,forget:`遗忘`,restore:`恢复`,empty:`没有符合条件的记忆`,loadFailed:`加载失败:{msg}`,loading:`加载中…`,pagerInfo:`第 {page} / {pages} 页 · 共 {total} 条`,prevPage:`上一页`,nextPage:`下一页`,cancel:`取消`,saveWithHint:`保存(旧条目归档)`,detailAttributes:`属性`,detailSource:`来源`,detailRelations:`关系`,detailOperations:`最近操作`,relSupersededBy:`被取代`,relSupersedes:`取代`,relContradicts:`矛盾`,relRelated:`关联`,opWrite:`写入`,opUpdate:`更新`,opForget:`遗忘`,opRestore:`恢复`,opDecay:`衰减归档`,opSuperseded:`被取代`,opIngestRequest:`摄取请求`,opDistillRequest:`蒸馏请求`,selectAll:`全选本页`,deselectAll:`取消全选`,selectedCount:`已选 {n} 条`,batchForget:`批量遗忘({n})`,batchForgetConfirm:`确认遗忘 {n} 条?`,batchRestore:`批量恢复({n})`,clearSelection:`取消选择`},O={nav:`Memory Library`,scopeUser:`User`,scopeProject:`Project`,refresh:`Refresh`,exportMd:`Export MD`,exportJson:`Export JSON`,allStatuses:`All statuses`,allKinds:`All kinds`,searchPlaceholder:`Search content…`,statusActive:`active`,statusArchived:`archived`,statusForgotten:`forgotten`,kindFact:`fact`,kindPreference:`preference`,kindDecision:`decision`,kindEpisode:`episode`,kindSkill:`skill`,labelContent:`Content`,labelKind:`Kind`,labelStatus:`Status`,labelScope:`Scope`,importance:`Importance`,confidence:`Confidence`,accessCount:`{n}× accessed`,labelCreated:`Created`,timeJustNow:`just now`,timeMinutesAgo:`{n} min ago`,timeHoursAgo:`{n} hr ago`,timeDaysAgo:`{n} d ago`,sourceExplicit:`Explicitly saved`,sourceSession:`Source {id}`,round:`round {n}`,cardActive:`{n} active`,signalRatio:`signal {n}%`,detail:`Details`,edit:`Edit`,forget:`Forget`,restore:`Restore`,empty:`No matching memories`,loadFailed:`Failed to load: {msg}`,loading:`Loading…`,pagerInfo:`Page {page} / {pages} · {total} records`,prevPage:`Prev`,nextPage:`Next`,cancel:`Cancel`,saveWithHint:`Save (archives old entry)`,detailAttributes:`Attributes`,detailSource:`Source`,detailRelations:`Relations`,detailOperations:`Recent operations`,relSupersededBy:`Superseded by`,relSupersedes:`Supersedes`,relContradicts:`Contradicts`,relRelated:`Related`,opWrite:`write`,opUpdate:`update`,opForget:`forget`,opRestore:`restore`,opDecay:`decay`,opSuperseded:`superseded`,opIngestRequest:`ingest-request`,opDistillRequest:`distill-request`,selectAll:`Select all on page`,deselectAll:`Deselect all`,selectedCount:`{n} selected`,batchForget:`Forget selected ({n})`,batchForgetConfirm:`Forget {n} entries?`,batchRestore:`Restore selected ({n})`,clearSelection:`Clear selection`},k=[`slots`,`locale`];function A(e){e.effect(()=>e.locale.register(E,{zh:D,en:O}),`dsh-engram: dictionaries`);let t=e.locale.bind(E);e.slots.inject(`settings.section`,()=>e.slots.register({name:`settings.section`,id:`engram`,order:20,label:()=>t(`nav`),locale:E},T))}return n.apply=A,n.inject=k,t.exports}});
|
|
3
2
|
//# sourceMappingURL=client.js.map
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useState","styles","useCallback"],"sources":["../src/client/EngramPanel.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * 记忆库设置面板:统计卡片、过滤列表、批量操作、编辑(取代链)、遗忘/恢复、导出。\n * 数据经回环 API(/api/engram/*)读写;详情与编辑为条目下方行内展开\n * (不嵌套弹窗);文案全部走宿主 locale 词典(zh/en),语言切换自动\n * 重渲染;数据层英文枚举(status/kind/op)只在显示层映射;内容节点\n * 一律 DOM/JSX 构建(防 XSS)。\n * @module @kenz1117/dsh-engram/client/EngramPanel\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport styles from './panel.module.css'\nimport { NS, type EngramKey } from './locales.ts'\n\n/** 列表/详情用的记忆视图(宿主 /api/engram/list 的行结构)。 */\ninterface MemoryRow {\n readonly id: string\n readonly scope: 'user' | 'project'\n readonly kind: string\n readonly content: string\n readonly importance: number\n readonly confidence: number\n readonly status: 'active' | 'archived' | 'forgotten'\n readonly createdAt: number\n readonly accessCount: number\n readonly sourceSessionId: string | null\n readonly sourceRound: number | null\n}\n\ninterface ListResult {\n readonly records: MemoryRow[]\n readonly total: number\n}\n\ninterface StatsPart {\n readonly scope: 'user' | 'project'\n readonly stats: {\n readonly total: number\n readonly active: number\n readonly archived: number\n readonly forgotten: number\n readonly signalRatio: number\n }\n}\n\n/** 面板内部传递的翻译函数(渲染器按注册的 locale 声明合成的 t 的窄化签名)。 */\ntype T = (key: EngramKey, params?: Record<string, unknown>) => string\n\n/** kind 数据值(存储层英文枚举,编辑表单下拉的 value 保持英文)。 */\nconst KINDS = ['fact', 'preference', 'decision', 'episode', 'skill'] as const\nconst PAGE_SIZE = 20\n\n/** 数据层枚举 → 词典键映射(显示层本地化,存储值不变)。 */\nconst STATUS_KEY: Record<MemoryRow['status'], EngramKey> = {\n active: 'statusActive',\n archived: 'statusArchived',\n forgotten: 'statusForgotten',\n}\nconst KIND_KEY: Record<string, EngramKey> = {\n fact: 'kindFact',\n preference: 'kindPreference',\n decision: 'kindDecision',\n episode: 'kindEpisode',\n skill: 'kindSkill',\n}\nconst SCOPE_KEY: Record<MemoryRow['scope'], EngramKey> = {\n user: 'scopeUser',\n project: 'scopeProject',\n}\nconst OP_KEY: Record<string, EngramKey> = {\n write: 'opWrite',\n update: 'opUpdate',\n forget: 'opForget',\n restore: 'opRestore',\n decay: 'opDecay',\n superseded: 'opSuperseded',\n 'ingest-request': 'opIngestRequest',\n 'distill-request': 'opDistillRequest',\n}\nconst REL_KEY: Record<string, EngramKey> = {\n supersededBy: 'relSupersededBy',\n supersedes: 'relSupersedes',\n contradicts: 'relContradicts',\n related: 'relRelated',\n}\n/** 操作日志 detail JSON 的已知键 → 词典键(write/update/superseded 的小对象)。 */\nconst DETAIL_KEY: Record<string, EngramKey> = {\n kind: 'labelKind',\n scope: 'labelScope',\n status: 'labelStatus',\n}\n\n/** kind 数据值 → 本地化标签(未知值回退原文)。 */\nfunction kindLabel(t: T, kind: string): string {\n const key = KIND_KEY[kind]\n return key === undefined ? kind : t(key)\n}\n\n/** op 数据值 → 本地化标签(未知值回退原文)。 */\nfunction opLabel(t: T, op: string): string {\n const key = OP_KEY[op]\n return key === undefined ? op : t(key)\n}\n\n/** 操作日志 detail 的本地化:write/update/superseded 的小对象转中文键值;摄取/蒸馏请求的完整 JSON 保持原样(审计诚实性优先)。 */\nfunction formatOpDetail(t: T, op: string, detail: string | null): string {\n if (detail === null) return ''\n if (op !== 'write' && op !== 'update' && op !== 'superseded') return ` ${detail}`\n try {\n const parsed = JSON.parse(detail) as Record<string, unknown>\n const entries = Object.entries(parsed)\n if (entries.length === 0) return ` ${detail}`\n return ' ' + entries.map(([key, value]) => {\n const label = DETAIL_KEY[key]\n if (label === undefined) return `${key}=${String(value)}`\n if (key === 'kind' && typeof value === 'string') return `${t(label)}=${kindLabel(t, value)}`\n if (key === 'scope' && (value === 'user' || value === 'project')) return `${t(label)}=${t(SCOPE_KEY[value])}`\n return `${t(label)}=${String(value)}`\n }).join(' · ')\n } catch {\n return ` ${detail}`\n }\n}\n\n/** 来源会话展示片段:截短的会话 id + 可选轮次。 */\nfunction sourceLabel(t: T, record: Pick<MemoryRow, 'sourceSessionId' | 'sourceRound'>): string {\n if (record.sourceSessionId === null) return t('sourceExplicit')\n const id = `${record.sourceSessionId.slice(0, 16)}…`\n const withRound = record.sourceRound === null\n ? id\n : `${id} ${t('round', { n: record.sourceRound })}`\n return t('sourceSession', { id: withRound })\n}\n\nasync function api<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await fetch(`/api/engram/${path}`, init)\n const body = (await response.json()) as T & { error?: string }\n if (!response.ok) throw new Error(body.error ?? `HTTP ${String(response.status)}`)\n return body\n}\n\nfunction fmtTime(ms: number): string {\n // 时间格式跟随浏览器环境语言(宿主界面语言通常与浏览器一致)。\n return new Date(ms).toLocaleString(navigator.language || 'zh-CN', { hour12: false })\n}\n\n/** 行内审计区:来源链、关系与最近操作日志(review API)。 */\nfunction ReviewBody({ t, recordId, scope }: {\n t: T\n recordId: string\n scope: 'user' | 'project'\n}): React.ReactElement {\n const [text, setText] = useState<string | null>(null)\n useEffect(() => {\n let cancelled = false\n api<Record<string, unknown>>(`review?scope=${scope}&id=${encodeURIComponent(recordId)}`)\n .then((view) => {\n if (cancelled) return\n const record = view.record as MemoryRow\n const lines = [\n `${t('labelContent')}: ${record.content}`,\n `${t('detailAttributes')}: ${t('labelKind')}=${kindLabel(t, record.kind)}, ${t('labelStatus')}=${t(STATUS_KEY[record.status])}, ${t('importance')}=${record.importance.toFixed(2)}, ${t('confidence')}=${record.confidence.toFixed(2)}, ${t('accessCount', { n: record.accessCount })}`,\n `${t('detailSource')}: ${sourceLabel(t, record)}`,\n ]\n for (const key of ['supersededBy', 'supersedes', 'contradicts', 'related'] as const) {\n const ids = view[key] as string[] | undefined\n const relKey = REL_KEY[key]\n if (ids !== undefined && ids.length > 0) lines.push(`${relKey === undefined ? key : t(relKey)}: ${ids.join(', ')}`)\n }\n const operations = view.operations as { at: number; op: string; detail: string | null }[] | undefined\n if (operations !== undefined && operations.length > 0) {\n lines.push(`${t('detailOperations')}:`)\n operations.forEach(op => lines.push(` ${fmtTime(op.at)} ${opLabel(t, op.op)}${formatOpDetail(t, op.op, op.detail)}`))\n }\n setText(lines.join('\\n'))\n })\n .catch((error: Error) => { if (!cancelled) setText(t('loadFailed', { msg: error.message })) })\n return () => { cancelled = true }\n }, [recordId, scope, t])\n if (text === null) return <div className={styles.expandLoading}>{t('loading')}</div>\n return <pre className={styles.review}>{text}</pre>\n}\n\n/** 行内编辑表单:内容/种类/重要性 → POST update(旧条目归档,取代链保留)。 */\nfunction EditForm({ t, record, onClose, onSaved }: {\n t: T\n record: MemoryRow\n onClose: () => void\n onSaved: () => void\n}): React.ReactElement {\n const [content, setContent] = useState(record.content)\n const [kind, setKind] = useState(record.kind)\n const [importance, setImportance] = useState(record.importance)\n const save = (): void => {\n api('update', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: record.id, scope: record.scope, content, kind, importance }),\n })\n .then(onSaved)\n .catch((error: Error) => { alert(error.message) })\n }\n return (\n <div className={styles.editForm}>\n <label className={styles.fieldLabel}>{t('labelContent')}</label>\n <textarea className={styles.input} rows={3} value={content} onChange={event => setContent(event.target.value)} />\n <label className={styles.fieldLabel}>{t('labelKind')}</label>\n {/* option 的 value 保持数据层英文枚举,仅显示文本本地化。 */}\n <select className={styles.input} value={kind} onChange={event => setKind(event.target.value)}>\n {KINDS.map(option => <option key={option} value={option}>{kindLabel(t, option)}</option>)}\n </select>\n <label className={styles.fieldLabel}>{`${t('importance')} ${importance.toFixed(2)}`}</label>\n <input type=\"range\" min={0} max={1} step={0.05} value={importance}\n onChange={event => setImportance(Number(event.target.value))} />\n <div className={styles.modalFoot}>\n <button type=\"button\" className={styles.button} onClick={onClose}>{t('cancel')}</button>\n <button type=\"button\" className={`${styles.button} ${styles.primary}`} onClick={save}>{t('saveWithHint')}</button>\n </div>\n </div>\n )\n}\n\n/** 设置页「记忆库」section 主组件(t 由渲染器按 locale: NS 声明合成)。 */\nexport function EngramSection({ t }: PropsLocale<typeof NS>): React.ReactElement {\n const [scope, setScope] = useState<'user' | 'project'>('user')\n const [status, setStatus] = useState('all')\n const [kind, setKind] = useState('all')\n const [q, setQ] = useState('')\n const [offset, setOffset] = useState(0)\n const [list, setList] = useState<ListResult | null>(null)\n const [stats, setStats] = useState<StatsPart[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n /** 行内展开态:互斥的唯一展开条目(review 或 edit)。 */\n const [expanded, setExpanded] = useState<{ kind: 'review' | 'edit'; record: MemoryRow } | null>(null)\n const [reloadTick, setReloadTick] = useState(0)\n /** 批量选择:条目 id 集合(scope/过滤/翻页/搜索变更时清空)。 */\n const [selected, setSelected] = useState<ReadonlySet<string>>(new Set())\n /** 批量遗忘的两段式确认(3 秒未确认自动复位)。 */\n const [confirmForget, setConfirmForget] = useState(false)\n\n const reload = useCallback((): void => { setReloadTick(tick => tick + 1) }, [])\n\n useEffect(() => {\n let cancelled = false\n const qs = new URLSearchParams({\n scope, status, kind, limit: String(PAGE_SIZE), offset: String(offset),\n })\n if (q !== '') qs.set('q', q)\n api<ListResult>(`list?${qs.toString()}`)\n .then((data) => { if (!cancelled) { setList(data); setError(null) } })\n .catch((loadError: Error) => { if (!cancelled) setError(loadError.message) })\n api<{ parts: StatsPart[] }>('stats')\n .then((data) => { if (!cancelled) setStats(data.parts) })\n .catch(() => { /* 统计失败不影响列表 */ })\n return () => { cancelled = true }\n }, [scope, status, kind, q, offset, reloadTick])\n\n const act = (route: string, record: MemoryRow): void => {\n api(route, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: record.id, scope: record.scope }),\n })\n .then(reload)\n .catch((actError: Error) => { alert(actError.message) })\n }\n\n const toggle = (expandKind: 'review' | 'edit', record: MemoryRow): void => {\n setExpanded(current => (current !== null && current.kind === expandKind && current.record.id === record.id)\n ? null\n : { kind: expandKind, record })\n }\n\n const pageRecords = list?.records ?? []\n const selectedRecords = pageRecords.filter(record => selected.has(record.id))\n const forgetable = selectedRecords.filter(record => record.status === 'active').length\n const restorable = selectedRecords.length - forgetable\n const allSelected = pageRecords.length > 0 && pageRecords.every(record => selected.has(record.id))\n\n const toggleSelect = (id: string): void => {\n setSelected(current => {\n const next = new Set(current)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n }\n\n const clearSelection = (): void => { setSelected(new Set()); setConfirmForget(false) }\n\n const toggleAllPage = (): void => {\n setSelected(current => {\n const next = new Set(current)\n if (allSelected) pageRecords.forEach(record => next.delete(record.id))\n else pageRecords.forEach(record => next.add(record.id))\n return next\n })\n }\n\n /** 批量执行:逐条调单条 API(回环毫秒级),单条失败不阻塞其余;完成即刷新并清空选择。 */\n const runBatch = (mode: 'forget' | 'restore'): void => {\n const targets = mode === 'forget'\n ? selectedRecords.filter(record => record.status === 'active')\n : selectedRecords.filter(record => record.status !== 'active')\n if (targets.length === 0) return\n void Promise.allSettled(targets.map(target => api(mode, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: target.id, scope: target.scope }),\n }))).then(() => { clearSelection(); reload() })\n }\n\n const armBatchForget = (): void => {\n // 确认态保持到用户点击确认执行或清空选择,不做自动复位(3 秒窗口曾导致执行落空)。\n if (confirmForget) { runBatch('forget'); return }\n setConfirmForget(true)\n }\n\n const page = Math.floor(offset / PAGE_SIZE) + 1\n const pages = list === null ? 1 : Math.max(1, Math.ceil(list.total / PAGE_SIZE))\n\n return (\n <div className={styles.panel}>\n <div className={styles.toolbar}>\n <div className={styles.scopeTabs}>\n {(['user', 'project'] as const).map(option => (\n <button key={option} type=\"button\"\n className={scope === option ? styles.scopeOn : styles.scopeOff}\n onClick={() => { setScope(option); setOffset(0); setExpanded(null); clearSelection() }}>\n {t(SCOPE_KEY[option])}\n </button>\n ))}\n </div>\n <button type=\"button\" className={styles.button} onClick={reload}>{t('refresh')}</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { location.href = `/api/engram/export?scope=${scope}&format=markdown` }}>{t('exportMd')}</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { location.href = `/api/engram/export?scope=${scope}&format=json` }}>{t('exportJson')}</button>\n </div>\n <div className={styles.cards}>\n {(stats ?? []).map(part => (\n <button key={part.scope} type=\"button\" className={styles.card}\n style={part.scope === scope ? { borderColor: 'var(--dsw-alias-brand-primary)' } : undefined}\n onClick={() => { setScope(part.scope); setOffset(0); clearSelection() }}>\n <b>{part.stats.total}</b>\n <span>{`${t(SCOPE_KEY[part.scope])} · ${t('cardActive', { n: part.stats.active })} · ${t('signalRatio', { n: Math.round(part.stats.signalRatio * 100) })}`}</span>\n </button>\n ))}\n </div>\n <div className={styles.filters}>\n <select className={styles.input} value={status}\n onChange={event => { setStatus(event.target.value); setOffset(0); setExpanded(null); clearSelection() }}>\n <option value=\"all\">{t('allStatuses')}</option>\n <option value=\"active\">{t('statusActive')}</option>\n <option value=\"archived\">{t('statusArchived')}</option>\n <option value=\"forgotten\">{t('statusForgotten')}</option>\n </select>\n <select className={styles.input} value={kind}\n onChange={event => { setKind(event.target.value); setOffset(0); setExpanded(null); clearSelection() }}>\n <option value=\"all\">{t('allKinds')}</option>\n {KINDS.map(option => <option key={option} value={option}>{kindLabel(t, option)}</option>)}\n </select>\n <input className={styles.input} placeholder={t('searchPlaceholder')} value={q}\n onChange={event => { setQ(event.target.value.trim()); setOffset(0); setExpanded(null); clearSelection() }} />\n </div>\n {selected.size > 0 && (\n <div className={styles.batchBar}>\n <span>{t('selectedCount', { n: selected.size })}</span>\n <button type=\"button\" className={styles.button} onClick={toggleAllPage}>\n {allSelected ? t('deselectAll') : t('selectAll')}\n </button>\n <button type=\"button\" className={styles.button} disabled={restorable === 0}\n onClick={() => runBatch('restore')}>{t('batchRestore', { n: restorable })}</button>\n <button type=\"button\"\n className={confirmForget ? `${styles.button} ${styles.danger}` : styles.button}\n disabled={forgetable === 0}\n onClick={armBatchForget}>\n {confirmForget ? t('batchForgetConfirm', { n: forgetable }) : t('batchForget', { n: forgetable })}\n </button>\n <button type=\"button\" className={styles.button} onClick={clearSelection}>{t('clearSelection')}</button>\n </div>\n )}\n {error !== null && <div className={styles.empty}>{t('loadFailed', { msg: error })}</div>}\n {error === null && list !== null && list.records.length === 0 && (\n <div className={styles.empty}>{t('empty')}</div>\n )}\n {(list?.records ?? []).map(record => (\n <div key={record.id} className={selected.has(record.id) ? `${styles.item} ${styles.itemSelected}` : styles.item}>\n <div className={styles.row1}>\n <input type=\"checkbox\" className={styles.itemCheck} checked={selected.has(record.id)}\n aria-label={record.content.slice(0, 24)} onChange={() => toggleSelect(record.id)} />\n <span className={`${styles.badge} ${styles[record.status]}`}>{t(STATUS_KEY[record.status])}</span>\n <span className={styles.badge}>{kindLabel(t, record.kind)}</span>\n <span className={styles.badge}>{t(SCOPE_KEY[record.scope])}</span>\n </div>\n <div className={styles.content}>{record.content}</div>\n <div className={styles.meta}>\n <span>{`${t('importance')} ${record.importance.toFixed(2)}`}</span>\n <span>{`${t('confidence')} ${record.confidence.toFixed(2)}`}</span>\n <span>{t('accessCount', { n: record.accessCount })}</span>\n <span>{sourceLabel(t, record)}</span>\n <span>{fmtTime(record.createdAt)}</span>\n </div>\n <div className={styles.ops}>\n <button type=\"button\" className={styles.button}\n onClick={() => { toggle('review', record) }}>{t('detail')}</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { toggle('edit', record) }}>{t('edit')}</button>\n {record.status === 'active'\n ? <button type=\"button\" className={styles.button}\n onClick={() => { act('forget', record) }}>{t('forget')}</button>\n : <button type=\"button\" className={styles.button}\n onClick={() => { act('restore', record) }}>{t('restore')}</button>}\n </div>\n {expanded !== null && expanded.record.id === record.id && (\n <div className={styles.expand}>\n {expanded.kind === 'review'\n ? <ReviewBody t={t} recordId={record.id} scope={record.scope} />\n : <EditForm t={t} record={record} onClose={() => { setExpanded(null) }}\n onSaved={() => { setExpanded(null); reload() }} />}\n </div>\n )}\n </div>\n ))}\n <div className={styles.pager}>\n <button type=\"button\" className={styles.button} disabled={offset === 0}\n onClick={() => { setOffset(Math.max(0, offset - PAGE_SIZE)); setExpanded(null); clearSelection() }}>{t('prevPage')}</button>\n <span>{t('pagerInfo', { page, pages, total: list?.total ?? 0 })}</span>\n <button type=\"button\" className={styles.button}\n disabled={list === null || offset + PAGE_SIZE >= list.total}\n onClick={() => { setOffset(offset + PAGE_SIZE); setExpanded(null); clearSelection() }}>{t('nextPage')}</button>\n </div>\n </div>\n )\n}\n","/**\n * 「记忆库」client 词典:zh/en 键并集必须完全一致(类型化注册的编译约束)。\n * 数据层英文枚举(status/kind/op 等)只在显示层映射,存储值保持英文不变。\n * @module @kenz1117/dsh-engram/client/locales\n */\n\n/** 词典命名空间(同时用作 LocaleNamespaceMap 的声明合并键)。 */\nexport const NS = 'engram'\n\n/** 「记忆库」全部界面文案键。 */\nexport type EngramKey =\n | 'nav'\n | 'scopeUser'\n | 'scopeProject'\n | 'refresh'\n | 'exportMd'\n | 'exportJson'\n | 'allStatuses'\n | 'allKinds'\n | 'searchPlaceholder'\n | 'statusActive'\n | 'statusArchived'\n | 'statusForgotten'\n | 'kindFact'\n | 'kindPreference'\n | 'kindDecision'\n | 'kindEpisode'\n | 'kindSkill'\n | 'labelContent'\n | 'labelKind'\n | 'labelStatus'\n | 'labelScope'\n | 'importance'\n | 'confidence'\n | 'accessCount'\n | 'sourceExplicit'\n | 'sourceSession'\n | 'round'\n | 'cardActive'\n | 'signalRatio'\n | 'detail'\n | 'edit'\n | 'forget'\n | 'restore'\n | 'empty'\n | 'loadFailed'\n | 'loading'\n | 'pagerInfo'\n | 'prevPage'\n | 'nextPage'\n | 'cancel'\n | 'saveWithHint'\n | 'detailAttributes'\n | 'detailSource'\n | 'detailRelations'\n | 'detailOperations'\n | 'relSupersededBy'\n | 'relSupersedes'\n | 'relContradicts'\n | 'relRelated'\n | 'opWrite'\n | 'opUpdate'\n | 'opForget'\n | 'opRestore'\n | 'opDecay'\n | 'opSuperseded'\n | 'opIngestRequest'\n | 'opDistillRequest'\n | 'selectAll'\n | 'deselectAll'\n | 'selectedCount'\n | 'batchForget'\n | 'batchForgetConfirm'\n | 'batchRestore'\n | 'clearSelection'\n\n/** 中文词典(宿主默认语言)。 */\nexport const zh: Record<EngramKey, string> = {\n nav: '记忆库',\n scopeUser: '用户级',\n scopeProject: '项目级',\n refresh: '刷新',\n exportMd: '导出 MD',\n exportJson: '导出 JSON',\n allStatuses: '全部状态',\n allKinds: '全部种类',\n searchPlaceholder: '按内容搜索…',\n statusActive: '生效中',\n statusArchived: '已归档',\n statusForgotten: '已遗忘',\n kindFact: '事实',\n kindPreference: '偏好',\n kindDecision: '决策',\n kindEpisode: '事件',\n kindSkill: '技能',\n labelContent: '内容',\n labelKind: '种类',\n labelStatus: '状态',\n labelScope: '作用域',\n importance: '重要性',\n confidence: '置信',\n accessCount: '访问 {n} 次',\n sourceExplicit: '显式保存',\n sourceSession: '来源 {id}',\n round: '第 {n} 轮',\n cardActive: '生效 {n}',\n signalRatio: '信噪比 {n}%',\n detail: '详情',\n edit: '编辑',\n forget: '遗忘',\n restore: '恢复',\n empty: '没有符合条件的记忆',\n loadFailed: '加载失败:{msg}',\n loading: '加载中…',\n pagerInfo: '第 {page} / {pages} 页 · 共 {total} 条',\n prevPage: '上一页',\n nextPage: '下一页',\n cancel: '取消',\n saveWithHint: '保存(旧条目归档)',\n detailAttributes: '属性',\n detailSource: '来源',\n detailRelations: '关系',\n detailOperations: '最近操作',\n relSupersededBy: '被取代',\n relSupersedes: '取代',\n relContradicts: '矛盾',\n relRelated: '关联',\n opWrite: '写入',\n opUpdate: '更新',\n opForget: '遗忘',\n opRestore: '恢复',\n opDecay: '衰减归档',\n opSuperseded: '被取代',\n opIngestRequest: '摄取请求',\n opDistillRequest: '蒸馏请求',\n selectAll: '全选本页',\n deselectAll: '取消全选',\n selectedCount: '已选 {n} 条',\n batchForget: '批量遗忘({n})',\n batchForgetConfirm: '确认遗忘 {n} 条?',\n batchRestore: '批量恢复({n})',\n clearSelection: '取消选择',\n}\n\n/** 英文词典。 */\nexport const en: Record<EngramKey, string> = {\n nav: 'Memory Library',\n scopeUser: 'User',\n scopeProject: 'Project',\n refresh: 'Refresh',\n exportMd: 'Export MD',\n exportJson: 'Export JSON',\n allStatuses: 'All statuses',\n allKinds: 'All kinds',\n searchPlaceholder: 'Search content…',\n statusActive: 'active',\n statusArchived: 'archived',\n statusForgotten: 'forgotten',\n kindFact: 'fact',\n kindPreference: 'preference',\n kindDecision: 'decision',\n kindEpisode: 'episode',\n kindSkill: 'skill',\n labelContent: 'Content',\n labelKind: 'Kind',\n labelStatus: 'Status',\n labelScope: 'Scope',\n importance: 'Importance',\n confidence: 'Confidence',\n accessCount: '{n}× accessed',\n sourceExplicit: 'Explicitly saved',\n sourceSession: 'Source {id}',\n round: 'round {n}',\n cardActive: '{n} active',\n signalRatio: 'signal {n}%',\n detail: 'Details',\n edit: 'Edit',\n forget: 'Forget',\n restore: 'Restore',\n empty: 'No matching memories',\n loadFailed: 'Failed to load: {msg}',\n loading: 'Loading…',\n pagerInfo: 'Page {page} / {pages} · {total} records',\n prevPage: 'Prev',\n nextPage: 'Next',\n cancel: 'Cancel',\n saveWithHint: 'Save (archives old entry)',\n detailAttributes: 'Attributes',\n detailSource: 'Source',\n detailRelations: 'Relations',\n detailOperations: 'Recent operations',\n relSupersededBy: 'Superseded by',\n relSupersedes: 'Supersedes',\n relContradicts: 'Contradicts',\n relRelated: 'Related',\n opWrite: 'write',\n opUpdate: 'update',\n opForget: 'forget',\n opRestore: 'restore',\n opDecay: 'decay',\n opSuperseded: 'superseded',\n opIngestRequest: 'ingest-request',\n opDistillRequest: 'distill-request',\n selectAll: 'Select all on page',\n deselectAll: 'Deselect all',\n selectedCount: '{n} selected',\n batchForget: 'Forget selected ({n})',\n batchForgetConfirm: 'Forget {n} entries?',\n batchRestore: 'Restore selected ({n})',\n clearSelection: 'Clear selection',\n}\n","/**\n * dsh-engram client half: 注册设置页「记忆库」section。\n * 数据经回环 API(/api/engram/*)读取与操作,与 host 半的路由对齐。\n * 文案经宿主 locale 服务(zh/en 词典,语言切换由渲染器自动重渲染)。\n * @module @kenz1117/dsh-engram/client\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: ctx.locale 服务声明(宿主 locale 插件的 Context merge)。\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: ctx.slots(renderer 的 SlotMap 渲染接线)。\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: settings.section 槽位的 SlotMap merge(ui-settings 契约)。\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\n// Type-only: LocaleNamespaceMap 声明合并目标(t 席位的键类型检查)。\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { EngramSection } from './EngramPanel.tsx'\nimport { NS, en, zh, type EngramKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n [NS]: EngramKey\n }\n}\n\n/** 必需服务:slots 渲染接线 + locale 词典注册。 */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: 注册 zh/en 词典与设置页「记忆库」section(order 20,\n * 排在 general/models/plugins 之后)。\n * @param ctx - client root context.\n */\nexport function apply(ctx: Context): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-engram: dictionaries')\n const t = ctx.locale.bind(NS)\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'engram',\n order: 20,\n label: () => t('nav'),\n locale: NS,\n }, EngramSection))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["styles","useState","useCallback"],"sources":["../src/client/EngramPanel.tsx","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * 记忆库设置面板:统计卡片、过滤列表、批量操作、编辑(取代链)、遗忘/恢复、导出。\n * 数据经回环 API(/api/engram/*)读写;详情与编辑为条目下方行内展开\n * (不嵌套弹窗);文案全部走宿主 locale 词典(zh/en),语言切换自动\n * 重渲染;数据层英文枚举(status/kind/op)只在显示层映射;内容节点\n * 一律 DOM/JSX 构建(防 XSS)。\n * @module @kenz1117/dsh-engram/client/EngramPanel\n */\n\nimport { useCallback, useEffect, useState } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport styles from './panel.module.css'\nimport { NS, type EngramKey } from './locales.ts'\n\n/** 列表/详情用的记忆视图(宿主 /api/engram/list 的行结构)。 */\ninterface MemoryRow {\n readonly id: string\n readonly scope: 'user' | 'project'\n readonly kind: string\n readonly content: string\n readonly importance: number\n readonly confidence: number\n readonly status: 'active' | 'archived' | 'forgotten'\n readonly createdAt: number\n readonly accessCount: number\n readonly sourceSessionId: string | null\n readonly sourceRound: number | null\n}\n\ninterface ListResult {\n readonly records: MemoryRow[]\n readonly total: number\n}\n\ninterface StatsPart {\n readonly scope: 'user' | 'project'\n readonly stats: {\n readonly total: number\n readonly active: number\n readonly archived: number\n readonly forgotten: number\n readonly signalRatio: number\n }\n}\n\n/** review API 的返回结构(详情行内展开区的数据源)。 */\ninterface ReviewView {\n readonly record: MemoryRow\n readonly supersededBy?: readonly string[]\n readonly supersedes?: readonly string[]\n readonly contradicts?: readonly string[]\n readonly related?: readonly string[]\n readonly operations?: readonly { at: number; op: string; detail: string | null }[]\n}\n\n/** 面板内部传递的翻译函数(渲染器按注册的 locale 声明合成的 t 的窄化签名)。 */\ntype T = (key: EngramKey, params?: Record<string, unknown>) => string\n\n/** kind 数据值(存储层英文枚举,编辑表单下拉的 value 保持英文)。 */\nconst KINDS = ['fact', 'preference', 'decision', 'episode', 'skill'] as const\nconst PAGE_SIZE = 20\n\n/** 数据层枚举 → 词典键映射(显示层本地化,存储值不变)。 */\nconst STATUS_KEY: Record<MemoryRow['status'], EngramKey> = {\n active: 'statusActive',\n archived: 'statusArchived',\n forgotten: 'statusForgotten',\n}\nconst KIND_KEY: Record<string, EngramKey> = {\n fact: 'kindFact',\n preference: 'kindPreference',\n decision: 'kindDecision',\n episode: 'kindEpisode',\n skill: 'kindSkill',\n}\nconst SCOPE_KEY: Record<MemoryRow['scope'], EngramKey> = {\n user: 'scopeUser',\n project: 'scopeProject',\n}\nconst OP_KEY: Record<string, EngramKey> = {\n write: 'opWrite',\n update: 'opUpdate',\n forget: 'opForget',\n restore: 'opRestore',\n decay: 'opDecay',\n superseded: 'opSuperseded',\n 'ingest-request': 'opIngestRequest',\n 'distill-request': 'opDistillRequest',\n}\nconst REL_KEY: Record<string, EngramKey> = {\n supersededBy: 'relSupersededBy',\n supersedes: 'relSupersedes',\n contradicts: 'relContradicts',\n related: 'relRelated',\n}\n/** 操作日志 detail JSON 的已知键 → 词典键(write/update/superseded 的小对象)。 */\nconst DETAIL_KEY: Record<string, EngramKey> = {\n kind: 'labelKind',\n scope: 'labelScope',\n status: 'labelStatus',\n}\n\n/** kind 数据值 → 本地化标签(未知值回退原文)。 */\nfunction kindLabel(t: T, kind: string): string {\n const key = KIND_KEY[kind]\n return key === undefined ? kind : t(key)\n}\n\n/** op 数据值 → 本地化标签(未知值回退原文)。 */\nfunction opLabel(t: T, op: string): string {\n const key = OP_KEY[op]\n return key === undefined ? op : t(key)\n}\n\n/** 操作日志 detail 的本地化:write/update/superseded 的小对象转中文键值;摄取/蒸馏请求的完整 JSON 保持原样(审计诚实性优先)。 */\nfunction formatOpDetail(t: T, op: string, detail: string | null): string {\n if (detail === null) return ''\n if (op !== 'write' && op !== 'update' && op !== 'superseded') return ` ${detail}`\n try {\n const parsed = JSON.parse(detail) as Record<string, unknown>\n const entries = Object.entries(parsed)\n if (entries.length === 0) return ` ${detail}`\n return ' ' + entries.map(([key, value]) => {\n const label = DETAIL_KEY[key]\n if (label === undefined) return `${key}=${String(value)}`\n if (key === 'kind' && typeof value === 'string') return `${t(label)}=${kindLabel(t, value)}`\n if (key === 'scope' && (value === 'user' || value === 'project')) return `${t(label)}=${t(SCOPE_KEY[value])}`\n return `${t(label)}=${String(value)}`\n }).join(' · ')\n } catch {\n return ` ${detail}`\n }\n}\n\n/** 来源会话展示片段:截短的会话 id + 可选轮次。 */\nfunction sourceLabel(t: T, record: Pick<MemoryRow, 'sourceSessionId' | 'sourceRound'>): string {\n if (record.sourceSessionId === null) return t('sourceExplicit')\n const id = `${record.sourceSessionId.slice(0, 16)}…`\n const withRound = record.sourceRound === null\n ? id\n : `${id} ${t('round', { n: record.sourceRound })}`\n return t('sourceSession', { id: withRound })\n}\n\nasync function api<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await fetch(`/api/engram/${path}`, init)\n const body = (await response.json()) as T & { error?: string }\n if (!response.ok) throw new Error(body.error ?? `HTTP ${String(response.status)}`)\n return body\n}\n\n/** 绝对时间(审计时间线等需要精确时刻的位置)。 */\nfunction fmtTime(ms: number): string {\n // 时间格式跟随浏览器环境语言(宿主界面语言通常与浏览器一致)。\n return new Date(ms).toLocaleString(navigator.language || 'zh-CN', { hour12: false })\n}\n\n/** 列表用的相对时间:1 分钟内「刚刚」,其后按分钟/小时/天取整。 */\nfunction relTime(t: T, ms: number): string {\n const diff = Math.max(0, Date.now() - ms)\n const minutes = Math.floor(diff / 60_000)\n if (minutes < 1) return t('timeJustNow')\n if (minutes < 60) return t('timeMinutesAgo', { n: minutes })\n const hours = Math.floor(minutes / 60)\n if (hours < 48) return t('timeHoursAgo', { n: hours })\n return t('timeDaysAgo', { n: Math.floor(hours / 24) })\n}\n\n/** 细仪表条:重要性(品牌色)与置信(成功色)共用,conf 变体换色。 */\nfunction Meter({ value, conf }: { value: number; conf?: boolean }): React.ReactElement {\n return (\n <span className={conf === true ? `${styles.meter} ${styles.conf}` : styles.meter}>\n <i style={{ width: `${String(Math.round(Math.min(1, Math.max(0, value)) * 100))}%` }} />\n </span>\n )\n}\n\n/** 关系 id 列表 → 等宽字体 chips(完整 id 放 title)。 */\nfunction RelChips({ ids }: { ids: readonly string[] }): React.ReactElement {\n return (\n <div className={styles.relChips}>\n {ids.map(id => <span key={id} className={styles.relChip} title={id}>{id.slice(0, 18)}</span>)}\n </div>\n )\n}\n\n/** 行内审计区:属性网格 + 关系 chips + 操作时间线(review API)。 */\nfunction ReviewBody({ t, recordId, scope }: {\n t: T\n recordId: string\n scope: 'user' | 'project'\n}): React.ReactElement {\n const [view, setView] = useState<ReviewView | null>(null)\n const [failed, setFailed] = useState<string | null>(null)\n useEffect(() => {\n let cancelled = false\n api<ReviewView>(`review?scope=${scope}&id=${encodeURIComponent(recordId)}`)\n .then((data) => { if (!cancelled) setView(data) })\n .catch((error: Error) => { if (!cancelled) setFailed(error.message) })\n return () => { cancelled = true }\n }, [recordId, scope, t])\n if (failed !== null) return <div className={styles.expandLoading}>{t('loadFailed', { msg: failed })}</div>\n if (view === null) return <div className={styles.expandLoading}>{t('loading')}</div>\n const { record } = view\n const relations = (['supersededBy', 'supersedes', 'contradicts', 'related'] as const)\n .map(key => ({ key, label: REL_KEY[key], ids: view[key] ?? [] }))\n .filter(entry => entry.ids.length > 0)\n const operations = view.operations ?? []\n return (\n <div>\n <dl className={styles.attrGrid}>\n <dt>{t('labelContent')}</dt>\n <dd>{record.content}</dd>\n <dt>{t('labelKind')}</dt>\n <dd><span className={styles.chip}>{kindLabel(t, record.kind)}</span></dd>\n <dt>{t('labelStatus')}</dt>\n <dd><span className={`${styles.statusWrap} ${styles[record.status]}`}>{t(STATUS_KEY[record.status])}</span></dd>\n <dt>{t('importance')}</dt>\n <dd>{record.importance.toFixed(2)}<Meter value={record.importance} /></dd>\n <dt>{t('confidence')}</dt>\n <dd>{record.confidence.toFixed(2)}<Meter value={record.confidence} conf /></dd>\n <dt>{t('detailSource')}</dt>\n <dd>{sourceLabel(t, record)}</dd>\n <dt>{t('labelCreated')}</dt>\n <dd>{fmtTime(record.createdAt)}</dd>\n </dl>\n {relations.map(({ key, label, ids }) => (\n <div key={key} className={styles.relBlock}>\n <h5>{label === undefined ? key : t(label)}</h5>\n <RelChips ids={ids} />\n </div>\n ))}\n {operations.length > 0 && (\n <div className={styles.relBlock}>\n <h5>{t('detailOperations')}</h5>\n <ul className={styles.timeline}>\n {operations.map((op, index) => (\n <li key={index}>\n <time>{fmtTime(op.at)}</time>\n <b>{opLabel(t, op.op)}</b>\n {formatOpDetail(t, op.op, op.detail)}\n </li>\n ))}\n </ul>\n </div>\n )}\n </div>\n )\n}\n\n/** 行内编辑表单:内容/种类/重要性 → POST update(旧条目归档,取代链保留)。 */\nfunction EditForm({ t, record, onClose, onSaved }: {\n t: T\n record: MemoryRow\n onClose: () => void\n onSaved: () => void\n}): React.ReactElement {\n const [content, setContent] = useState(record.content)\n const [kind, setKind] = useState(record.kind)\n const [importance, setImportance] = useState(record.importance)\n const save = (): void => {\n api('update', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: record.id, scope: record.scope, content, kind, importance }),\n })\n .then(onSaved)\n .catch((error: Error) => { alert(error.message) })\n }\n return (\n <div className={styles.editForm}>\n <label className={styles.fieldLabel}>{t('labelContent')}</label>\n <textarea className={styles.input} rows={3} value={content} onChange={event => setContent(event.target.value)} />\n <label className={styles.fieldLabel}>{t('labelKind')}</label>\n {/* option 的 value 保持数据层英文枚举,仅显示文本本地化。 */}\n <select className={styles.input} value={kind} onChange={event => setKind(event.target.value)}>\n {KINDS.map(option => <option key={option} value={option}>{kindLabel(t, option)}</option>)}\n </select>\n <label className={styles.fieldLabel}>{`${t('importance')} ${importance.toFixed(2)}`}</label>\n <input type=\"range\" min={0} max={1} step={0.05} value={importance}\n onChange={event => setImportance(Number(event.target.value))} />\n <div className={styles.modalFoot}>\n <button type=\"button\" className={styles.button} onClick={onClose}>{t('cancel')}</button>\n <button type=\"button\" className={`${styles.button} ${styles.primary}`} onClick={save}>{t('saveWithHint')}</button>\n </div>\n </div>\n )\n}\n\n/** 设置页「记忆库」section 主组件(t 由渲染器按 locale: NS 声明合成)。 */\nexport function EngramSection({ t }: PropsLocale<typeof NS>): React.ReactElement {\n const [scope, setScope] = useState<'user' | 'project'>('user')\n const [status, setStatus] = useState('all')\n const [kind, setKind] = useState('all')\n const [q, setQ] = useState('')\n const [offset, setOffset] = useState(0)\n const [list, setList] = useState<ListResult | null>(null)\n const [stats, setStats] = useState<StatsPart[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n /** 行内展开态:互斥的唯一展开条目(review 或 edit)。 */\n const [expanded, setExpanded] = useState<{ kind: 'review' | 'edit'; record: MemoryRow } | null>(null)\n const [reloadTick, setReloadTick] = useState(0)\n /** 批量选择:条目 id 集合(scope/过滤/翻页/搜索变更时清空)。 */\n const [selected, setSelected] = useState<ReadonlySet<string>>(new Set())\n /** 批量遗忘的两段式确认。 */\n const [confirmForget, setConfirmForget] = useState(false)\n\n const reload = useCallback((): void => { setReloadTick(tick => tick + 1) }, [])\n\n useEffect(() => {\n let cancelled = false\n const qs = new URLSearchParams({\n scope, status, kind, limit: String(PAGE_SIZE), offset: String(offset),\n })\n if (q !== '') qs.set('q', q)\n api<ListResult>(`list?${qs.toString()}`)\n .then((data) => { if (!cancelled) { setList(data); setError(null) } })\n .catch((loadError: Error) => { if (!cancelled) setError(loadError.message) })\n api<{ parts: StatsPart[] }>('stats')\n .then((data) => { if (!cancelled) setStats(data.parts) })\n .catch(() => { /* 统计失败不影响列表 */ })\n return () => { cancelled = true }\n }, [scope, status, kind, q, offset, reloadTick])\n\n const act = (route: string, record: MemoryRow): void => {\n api(route, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: record.id, scope: record.scope }),\n })\n .then(reload)\n .catch((actError: Error) => { alert(actError.message) })\n }\n\n const toggle = (expandKind: 'review' | 'edit', record: MemoryRow): void => {\n setExpanded(current => (current !== null && current.kind === expandKind && current.record.id === record.id)\n ? null\n : { kind: expandKind, record })\n }\n\n const pageRecords = list?.records ?? []\n const selectedRecords = pageRecords.filter(record => selected.has(record.id))\n const forgetable = selectedRecords.filter(record => record.status === 'active').length\n const restorable = selectedRecords.length - forgetable\n const allSelected = pageRecords.length > 0 && pageRecords.every(record => selected.has(record.id))\n\n const toggleSelect = (id: string): void => {\n setSelected(current => {\n const next = new Set(current)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n return next\n })\n }\n\n const clearSelection = (): void => { setSelected(new Set()); setConfirmForget(false) }\n\n const toggleAllPage = (): void => {\n setSelected(current => {\n const next = new Set(current)\n if (allSelected) pageRecords.forEach(record => next.delete(record.id))\n else pageRecords.forEach(record => next.add(record.id))\n return next\n })\n }\n\n /** 批量执行:逐条调单条 API(回环毫秒级),单条失败不阻塞其余;完成即刷新并清空选择。 */\n const runBatch = (mode: 'forget' | 'restore'): void => {\n const targets = mode === 'forget'\n ? selectedRecords.filter(record => record.status === 'active')\n : selectedRecords.filter(record => record.status !== 'active')\n if (targets.length === 0) return\n void Promise.allSettled(targets.map(target => api(mode, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ id: target.id, scope: target.scope }),\n }))).then(() => { clearSelection(); reload() })\n }\n\n const armBatchForget = (): void => {\n // 确认态保持到用户点击确认执行或清空选择,不做自动复位(3 秒窗口曾导致执行落空)。\n if (confirmForget) { runBatch('forget'); return }\n setConfirmForget(true)\n }\n\n const page = Math.floor(offset / PAGE_SIZE) + 1\n const pages = list === null ? 1 : Math.max(1, Math.ceil(list.total / PAGE_SIZE))\n\n return (\n <div className={styles.panel}>\n <div className={styles.toolbar}>\n <div className={styles.scopeTabs}>\n {(['user', 'project'] as const).map(option => (\n <button key={option} type=\"button\"\n className={scope === option ? styles.scopeOn : styles.scopeOff}\n onClick={() => { setScope(option); setOffset(0); setExpanded(null); clearSelection() }}>\n {t(SCOPE_KEY[option])}\n </button>\n ))}\n </div>\n <button type=\"button\" className={styles.button} onClick={reload}>{t('refresh')}</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { location.href = `/api/engram/export?scope=${scope}&format=markdown` }}>{t('exportMd')}</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { location.href = `/api/engram/export?scope=${scope}&format=json` }}>{t('exportJson')}</button>\n </div>\n <div className={styles.cards}>\n {(stats ?? []).map(part => (\n <button key={part.scope} type=\"button\"\n className={part.scope === scope ? `${styles.card} ${styles.cardOn}` : styles.card}\n onClick={() => { setScope(part.scope); setOffset(0); clearSelection() }}>\n <b>{part.stats.total}</b>\n <span>{`${t(SCOPE_KEY[part.scope])} · ${t('cardActive', { n: part.stats.active })} · ${t('signalRatio', { n: Math.round(part.stats.signalRatio * 100) })}`}</span>\n <span className={styles.cardMeter}>\n <i style={{ width: `${String(Math.round(part.stats.signalRatio * 100))}%` }} />\n </span>\n </button>\n ))}\n </div>\n <div className={styles.filters}>\n <select className={styles.input} value={status}\n onChange={event => { setStatus(event.target.value); setOffset(0); setExpanded(null); clearSelection() }}>\n <option value=\"all\">{t('allStatuses')}</option>\n <option value=\"active\">{t('statusActive')}</option>\n <option value=\"archived\">{t('statusArchived')}</option>\n <option value=\"forgotten\">{t('statusForgotten')}</option>\n </select>\n <select className={styles.input} value={kind}\n onChange={event => { setKind(event.target.value); setOffset(0); setExpanded(null); clearSelection() }}>\n <option value=\"all\">{t('allKinds')}</option>\n {KINDS.map(option => <option key={option} value={option}>{kindLabel(t, option)}</option>)}\n </select>\n <input className={styles.input} placeholder={t('searchPlaceholder')} value={q}\n onChange={event => { setQ(event.target.value.trim()); setOffset(0); setExpanded(null); clearSelection() }} />\n </div>\n {selected.size > 0 && (\n <div className={styles.batchBar}>\n <span>{t('selectedCount', { n: selected.size })}</span>\n <button type=\"button\" className={styles.button} onClick={toggleAllPage}>\n {allSelected ? t('deselectAll') : t('selectAll')}\n </button>\n <button type=\"button\" className={styles.button} disabled={restorable === 0}\n onClick={() => runBatch('restore')}>{t('batchRestore', { n: restorable })}</button>\n <button type=\"button\"\n className={confirmForget ? `${styles.button} ${styles.danger}` : styles.button}\n disabled={forgetable === 0}\n onClick={armBatchForget}>\n {confirmForget ? t('batchForgetConfirm', { n: forgetable }) : t('batchForget', { n: forgetable })}\n </button>\n <button type=\"button\" className={styles.button} onClick={clearSelection}>{t('clearSelection')}</button>\n </div>\n )}\n {error !== null && <div className={styles.empty}>{t('loadFailed', { msg: error })}</div>}\n {error === null && list === null && [0, 1, 2].map(index => (\n <div key={index} className={styles.skeleton}><i /><i /><i /></div>\n ))}\n {error === null && list !== null && list.records.length === 0 && (\n <div className={styles.empty}>{t('empty')}</div>\n )}\n {(list?.records ?? []).map((record, index) => (\n <div key={record.id}\n className={selected.has(record.id) ? `${styles.item} ${styles.itemSelected}` : styles.item}\n style={{ animationDelay: `${String(Math.min(index, 12) * 28)}ms` }}>\n <div className={styles.row1}>\n <input type=\"checkbox\" className={styles.itemCheck} checked={selected.has(record.id)}\n aria-label={record.content.slice(0, 24)} onChange={() => toggleSelect(record.id)} />\n <span className={`${styles.statusWrap} ${styles[record.status]}`}>{t(STATUS_KEY[record.status])}</span>\n <span className={styles.chip}>{kindLabel(t, record.kind)}</span>\n <span className={styles.scopeTag}>{t(SCOPE_KEY[record.scope])}</span>\n <span className={styles.timeTag} title={fmtTime(record.createdAt)}>{relTime(t, record.createdAt)}</span>\n </div>\n <div className={styles.content}>{record.content}</div>\n <div className={styles.meta}>\n <span className={styles.meterField}>{t('importance')}<Meter value={record.importance} />{record.importance.toFixed(2)}</span>\n <span className={styles.meterField}>{t('confidence')}<Meter value={record.confidence} conf />{record.confidence.toFixed(2)}</span>\n <span>{t('accessCount', { n: record.accessCount })}</span>\n <span>{sourceLabel(t, record)}</span>\n </div>\n <div className={styles.ops}>\n <button type=\"button\" className={styles.button}\n onClick={() => { toggle('review', record) }}>{t('detail')}</button>\n <button type=\"button\" className={styles.button}\n onClick={() => { toggle('edit', record) }}>{t('edit')}</button>\n {record.status === 'active'\n ? <button type=\"button\" className={styles.button}\n onClick={() => { act('forget', record) }}>{t('forget')}</button>\n : <button type=\"button\" className={styles.button}\n onClick={() => { act('restore', record) }}>{t('restore')}</button>}\n </div>\n {expanded !== null && expanded.record.id === record.id && (\n <div className={styles.expand}>\n {expanded.kind === 'review'\n ? <ReviewBody t={t} recordId={record.id} scope={record.scope} />\n : <EditForm t={t} record={record} onClose={() => { setExpanded(null) }}\n onSaved={() => { setExpanded(null); reload() }} />}\n </div>\n )}\n </div>\n ))}\n <div className={styles.pager}>\n <button type=\"button\" className={styles.button} disabled={offset === 0}\n onClick={() => { setOffset(Math.max(0, offset - PAGE_SIZE)); setExpanded(null); clearSelection() }}>{t('prevPage')}</button>\n <span>{t('pagerInfo', { page, pages, total: list?.total ?? 0 })}</span>\n <button type=\"button\" className={styles.button}\n disabled={list === null || offset + PAGE_SIZE >= list.total}\n onClick={() => { setOffset(offset + PAGE_SIZE); setExpanded(null); clearSelection() }}>{t('nextPage')}</button>\n </div>\n </div>\n )\n}\n","/**\n * 「记忆库」client 词典:zh/en 键并集必须完全一致(类型化注册的编译约束)。\n * 数据层英文枚举(status/kind/op 等)只在显示层映射,存储值保持英文不变。\n * @module @kenz1117/dsh-engram/client/locales\n */\n\n/** 词典命名空间(同时用作 LocaleNamespaceMap 的声明合并键)。 */\nexport const NS = 'engram'\n\n/** 「记忆库」全部界面文案键。 */\nexport type EngramKey =\n | 'nav'\n | 'scopeUser'\n | 'scopeProject'\n | 'refresh'\n | 'exportMd'\n | 'exportJson'\n | 'allStatuses'\n | 'allKinds'\n | 'searchPlaceholder'\n | 'statusActive'\n | 'statusArchived'\n | 'statusForgotten'\n | 'kindFact'\n | 'kindPreference'\n | 'kindDecision'\n | 'kindEpisode'\n | 'kindSkill'\n | 'labelContent'\n | 'labelKind'\n | 'labelStatus'\n | 'labelScope'\n | 'importance'\n | 'confidence'\n | 'accessCount'\n | 'labelCreated'\n | 'timeJustNow'\n | 'timeMinutesAgo'\n | 'timeHoursAgo'\n | 'timeDaysAgo'\n | 'sourceExplicit'\n | 'sourceSession'\n | 'round'\n | 'cardActive'\n | 'signalRatio'\n | 'detail'\n | 'edit'\n | 'forget'\n | 'restore'\n | 'empty'\n | 'loadFailed'\n | 'loading'\n | 'pagerInfo'\n | 'prevPage'\n | 'nextPage'\n | 'cancel'\n | 'saveWithHint'\n | 'detailAttributes'\n | 'detailSource'\n | 'detailRelations'\n | 'detailOperations'\n | 'relSupersededBy'\n | 'relSupersedes'\n | 'relContradicts'\n | 'relRelated'\n | 'opWrite'\n | 'opUpdate'\n | 'opForget'\n | 'opRestore'\n | 'opDecay'\n | 'opSuperseded'\n | 'opIngestRequest'\n | 'opDistillRequest'\n | 'selectAll'\n | 'deselectAll'\n | 'selectedCount'\n | 'batchForget'\n | 'batchForgetConfirm'\n | 'batchRestore'\n | 'clearSelection'\n\n/** 中文词典(宿主默认语言)。 */\nexport const zh: Record<EngramKey, string> = {\n nav: '记忆库',\n scopeUser: '用户级',\n scopeProject: '项目级',\n refresh: '刷新',\n exportMd: '导出 MD',\n exportJson: '导出 JSON',\n allStatuses: '全部状态',\n allKinds: '全部种类',\n searchPlaceholder: '按内容搜索…',\n statusActive: '生效中',\n statusArchived: '已归档',\n statusForgotten: '已遗忘',\n kindFact: '事实',\n kindPreference: '偏好',\n kindDecision: '决策',\n kindEpisode: '事件',\n kindSkill: '技能',\n labelContent: '内容',\n labelKind: '种类',\n labelStatus: '状态',\n labelScope: '作用域',\n importance: '重要性',\n confidence: '置信',\n accessCount: '访问 {n} 次',\n labelCreated: '创建时间',\n timeJustNow: '刚刚',\n timeMinutesAgo: '{n} 分钟前',\n timeHoursAgo: '{n} 小时前',\n timeDaysAgo: '{n} 天前',\n sourceExplicit: '显式保存',\n sourceSession: '来源 {id}',\n round: '第 {n} 轮',\n cardActive: '生效 {n}',\n signalRatio: '信噪比 {n}%',\n detail: '详情',\n edit: '编辑',\n forget: '遗忘',\n restore: '恢复',\n empty: '没有符合条件的记忆',\n loadFailed: '加载失败:{msg}',\n loading: '加载中…',\n pagerInfo: '第 {page} / {pages} 页 · 共 {total} 条',\n prevPage: '上一页',\n nextPage: '下一页',\n cancel: '取消',\n saveWithHint: '保存(旧条目归档)',\n detailAttributes: '属性',\n detailSource: '来源',\n detailRelations: '关系',\n detailOperations: '最近操作',\n relSupersededBy: '被取代',\n relSupersedes: '取代',\n relContradicts: '矛盾',\n relRelated: '关联',\n opWrite: '写入',\n opUpdate: '更新',\n opForget: '遗忘',\n opRestore: '恢复',\n opDecay: '衰减归档',\n opSuperseded: '被取代',\n opIngestRequest: '摄取请求',\n opDistillRequest: '蒸馏请求',\n selectAll: '全选本页',\n deselectAll: '取消全选',\n selectedCount: '已选 {n} 条',\n batchForget: '批量遗忘({n})',\n batchForgetConfirm: '确认遗忘 {n} 条?',\n batchRestore: '批量恢复({n})',\n clearSelection: '取消选择',\n}\n\n/** 英文词典。 */\nexport const en: Record<EngramKey, string> = {\n nav: 'Memory Library',\n scopeUser: 'User',\n scopeProject: 'Project',\n refresh: 'Refresh',\n exportMd: 'Export MD',\n exportJson: 'Export JSON',\n allStatuses: 'All statuses',\n allKinds: 'All kinds',\n searchPlaceholder: 'Search content…',\n statusActive: 'active',\n statusArchived: 'archived',\n statusForgotten: 'forgotten',\n kindFact: 'fact',\n kindPreference: 'preference',\n kindDecision: 'decision',\n kindEpisode: 'episode',\n kindSkill: 'skill',\n labelContent: 'Content',\n labelKind: 'Kind',\n labelStatus: 'Status',\n labelScope: 'Scope',\n importance: 'Importance',\n confidence: 'Confidence',\n accessCount: '{n}× accessed',\n labelCreated: 'Created',\n timeJustNow: 'just now',\n timeMinutesAgo: '{n} min ago',\n timeHoursAgo: '{n} hr ago',\n timeDaysAgo: '{n} d ago',\n sourceExplicit: 'Explicitly saved',\n sourceSession: 'Source {id}',\n round: 'round {n}',\n cardActive: '{n} active',\n signalRatio: 'signal {n}%',\n detail: 'Details',\n edit: 'Edit',\n forget: 'Forget',\n restore: 'Restore',\n empty: 'No matching memories',\n loadFailed: 'Failed to load: {msg}',\n loading: 'Loading…',\n pagerInfo: 'Page {page} / {pages} · {total} records',\n prevPage: 'Prev',\n nextPage: 'Next',\n cancel: 'Cancel',\n saveWithHint: 'Save (archives old entry)',\n detailAttributes: 'Attributes',\n detailSource: 'Source',\n detailRelations: 'Relations',\n detailOperations: 'Recent operations',\n relSupersededBy: 'Superseded by',\n relSupersedes: 'Supersedes',\n relContradicts: 'Contradicts',\n relRelated: 'Related',\n opWrite: 'write',\n opUpdate: 'update',\n opForget: 'forget',\n opRestore: 'restore',\n opDecay: 'decay',\n opSuperseded: 'superseded',\n opIngestRequest: 'ingest-request',\n opDistillRequest: 'distill-request',\n selectAll: 'Select all on page',\n deselectAll: 'Deselect all',\n selectedCount: '{n} selected',\n batchForget: 'Forget selected ({n})',\n batchForgetConfirm: 'Forget {n} entries?',\n batchRestore: 'Restore selected ({n})',\n clearSelection: 'Clear selection',\n}\n","/**\n * dsh-engram client half: 注册设置页「记忆库」section。\n * 数据经回环 API(/api/engram/*)读取与操作,与 host 半的路由对齐。\n * 文案经宿主 locale 服务(zh/en 词典,语言切换由渲染器自动重渲染)。\n * @module @kenz1117/dsh-engram/client\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: ctx.locale 服务声明(宿主 locale 插件的 Context merge)。\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: ctx.slots(renderer 的 SlotMap 渲染接线)。\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: settings.section 槽位的 SlotMap merge(ui-settings 契约)。\nimport type {} from '@deepseek-ai/dsh-client-ui-settings/client'\n// Type-only: LocaleNamespaceMap 声明合并目标(t 席位的键类型检查)。\nimport type {} from '@deepseek-ai/dsh-client-ui-slots'\nimport { EngramSection } from './EngramPanel.tsx'\nimport { NS, en, zh, type EngramKey } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n [NS]: EngramKey\n }\n}\n\n/** 必需服务:slots 渲染接线 + locale 词典注册。 */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: 注册 zh/en 词典与设置页「记忆库」section(order 20,\n * 排在 general/models/plugins 之后)。\n * @param ctx - client root context.\n */\nexport function apply(ctx: Context): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-engram: dictionaries')\n const t = ctx.locale.bind(NS)\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'engram',\n order: 20,\n label: () => t('nav'),\n locale: NS,\n }, EngramSection))\n}\n"],"mappings":"w2ZA2DA,IAAM,EAAQ,CAAC,OAAQ,aAAc,WAAY,UAAW,OAAO,EAI7D,EAAqD,CACzD,OAAQ,eACR,SAAU,iBACV,UAAW,iBACb,EACM,EAAsC,CAC1C,KAAM,WACN,WAAY,iBACZ,SAAU,eACV,QAAS,cACT,MAAO,WACT,EACM,EAAmD,CACvD,KAAM,YACN,QAAS,cACX,EACM,EAAoC,CACxC,MAAO,UACP,OAAQ,WACR,OAAQ,WACR,QAAS,YACT,MAAO,UACP,WAAY,eACZ,iBAAkB,kBAClB,kBAAmB,kBACrB,EACM,EAAqC,CACzC,aAAc,kBACd,WAAY,gBACZ,YAAa,iBACb,QAAS,YACX,EAEM,EAAwC,CAC5C,KAAM,YACN,MAAO,aACP,OAAQ,aACV,EAGA,SAAS,EAAU,EAAM,EAAsB,CAC7C,IAAM,EAAM,EAAS,GACrB,OAAO,IAAQ,IAAA,GAAY,EAAO,EAAE,CAAG,CACzC,CAGA,SAAS,EAAQ,EAAM,EAAoB,CACzC,IAAM,EAAM,EAAO,GACnB,OAAO,IAAQ,IAAA,GAAY,EAAK,EAAE,CAAG,CACvC,CAGA,SAAS,EAAe,EAAM,EAAY,EAA+B,CACvE,GAAI,IAAW,KAAM,MAAO,GAC5B,GAAI,IAAO,SAAW,IAAO,UAAY,IAAO,aAAc,MAAO,IAAI,IACzE,GAAI,CACF,IAAM,EAAS,KAAK,MAAM,CAAM,EAC1B,EAAU,OAAO,QAAQ,CAAM,EAErC,OADI,EAAQ,SAAW,EAAU,IAAI,IAC9B,IAAM,EAAQ,KAAK,CAAC,EAAK,KAAW,CACzC,IAAM,EAAQ,EAAW,GAIzB,OAHI,IAAU,IAAA,GAAkB,GAAG,EAAI,GAAG,OAAO,CAAK,IAClD,IAAQ,QAAU,OAAO,GAAU,SAAiB,GAAG,EAAE,CAAK,EAAE,GAAG,EAAU,EAAG,CAAK,IACrF,IAAQ,UAAY,IAAU,QAAU,IAAU,WAAmB,GAAG,EAAE,CAAK,EAAE,GAAG,EAAE,EAAU,EAAM,IACnG,GAAG,EAAE,CAAK,EAAE,GAAG,OAAO,CAAK,GACpC,CAAC,CAAC,CAAC,KAAK,KAAK,CACf,MAAQ,CACN,MAAO,IAAI,GACb,CACF,CAGA,SAAS,EAAY,EAAM,EAAoE,CAC7F,GAAI,EAAO,kBAAoB,KAAM,OAAO,EAAE,gBAAgB,EAC9D,IAAM,EAAK,GAAG,EAAO,gBAAgB,MAAM,EAAG,EAAE,EAAE,GAIlD,OAAO,EAAE,gBAAiB,CAAE,GAHV,EAAO,cAAgB,KACrC,EACA,GAAG,EAAG,GAAG,EAAE,QAAS,CAAE,EAAG,EAAO,WAAY,CAAC,GACP,CAAC,CAC7C,CAEA,eAAe,EAAO,EAAc,EAAgC,CAClE,IAAM,EAAW,MAAM,MAAM,eAAe,IAAQ,CAAI,EAClD,EAAQ,MAAM,EAAS,KAAK,EAClC,GAAI,CAAC,EAAS,GAAI,MAAU,MAAM,EAAK,OAAS,QAAQ,OAAO,EAAS,MAAM,GAAG,EACjF,OAAO,CACT,CAGA,SAAS,EAAQ,EAAoB,CAEnC,OAAO,IAAI,KAAK,CAAE,CAAC,CAAC,eAAe,UAAU,UAAY,QAAS,CAAE,OAAQ,EAAM,CAAC,CACrF,CAGA,SAAS,EAAQ,EAAM,EAAoB,CACzC,IAAM,EAAO,KAAK,IAAI,EAAG,KAAK,IAAI,EAAI,CAAE,EAClC,EAAU,KAAK,MAAM,EAAO,GAAM,EACxC,GAAI,EAAU,EAAG,OAAO,EAAE,aAAa,EACvC,GAAI,EAAU,GAAI,OAAO,EAAE,iBAAkB,CAAE,EAAG,CAAQ,CAAC,EAC3D,IAAM,EAAQ,KAAK,MAAM,EAAU,EAAE,EAErC,OADI,EAAQ,GAAW,EAAE,eAAgB,CAAE,EAAG,CAAM,CAAC,EAC9C,EAAE,cAAe,CAAE,EAAG,KAAK,MAAM,EAAQ,EAAE,CAAE,CAAC,CACvD,CAGA,SAAS,EAAM,CAAE,QAAO,QAA+D,CACrF,OACE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,IAAS,GAAO,GAAGA,EAAO,MAAM,GAAGA,EAAO,OAASA,EAAO,MACzE,UAAA,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,MAAO,CAAE,MAAO,GAAG,OAAO,KAAK,MAAM,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAK,CAAC,EAAI,GAAG,CAAC,EAAE,EAAG,CAAI,CAAA,CACnF,CAAA,CAEV,CAGA,SAAS,EAAS,CAAE,OAAuD,CACzE,OACE,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,SACpB,SAAA,EAAI,IAAI,IAAM,EAAA,EAAA,IAAA,CAAC,OAAD,CAAe,UAAWA,EAAO,QAAS,MAAO,EAAK,SAAA,EAAG,MAAM,EAAG,EAAE,CAAQ,EAAjE,CAAiE,CAAC,CACzF,CAAA,CAET,CAGA,SAAS,EAAW,CAAE,IAAG,WAAU,SAIZ,CACrB,GAAM,CAAC,EAAM,IAAA,EAAWC,EAAAA,SAAAA,CAA4B,IAAI,EAClD,CAAC,EAAQ,IAAA,EAAaA,EAAAA,SAAAA,CAAwB,IAAI,EAQxD,IAPA,EAAA,EAAA,UAAA,KAAgB,CACd,IAAI,EAAY,GAIhB,OAHA,EAAgB,gBAAgB,EAAM,MAAM,mBAAmB,CAAQ,GAAG,CAAC,CACxE,KAAM,GAAS,CAAO,GAAW,EAAQ,CAAI,CAAE,CAAC,CAAC,CACjD,MAAO,GAAiB,CAAO,GAAW,EAAU,EAAM,OAAO,CAAE,CAAC,MAC1D,CAAE,EAAY,EAAK,CAClC,EAAG,CAAC,EAAU,EAAO,CAAC,CAAC,EACnB,IAAW,KAAM,OAAO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWD,EAAO,cAAgB,SAAA,EAAE,aAAc,CAAE,IAAK,CAAO,CAAC,CAAO,CAAA,EACzG,GAAI,IAAS,KAAM,OAAO,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,cAAgB,SAAA,EAAE,SAAS,CAAO,CAAA,EACnF,GAAM,CAAE,UAAW,EACb,EAAa,CAAC,eAAgB,aAAc,cAAe,SAAS,CAAC,CACxE,IAAI,IAAQ,CAAE,MAAK,MAAO,EAAQ,GAAM,IAAK,EAAK,IAAQ,CAAC,CAAE,EAAE,CAAC,CAChE,OAAO,GAAS,EAAM,IAAI,OAAS,CAAC,EACjC,EAAa,EAAK,YAAc,CAAC,EACvC,OACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAA,SAAA,EACE,EAAA,EAAA,KAAA,CAAC,KAAD,CAAI,UAAWA,EAAO,SAAtB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,cAAc,CAAM,CAAA,GAC3B,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAO,OAAY,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,WAAW,CAAM,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,UAAI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,KAAO,SAAA,EAAU,EAAG,EAAO,IAAI,CAAQ,CAAA,CAAK,CAAA,GACxE,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,aAAa,CAAM,CAAA,GAC1B,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,UAAI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,GAAGA,EAAO,WAAW,GAAGA,EAAO,EAAO,UAAY,SAAA,EAAE,EAAW,EAAO,OAAO,CAAQ,CAAA,CAAK,CAAA,GAC/G,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,YAAY,CAAM,CAAA,GACzB,EAAA,EAAA,KAAA,CAAC,KAAD,CAAA,SAAA,CAAK,EAAO,WAAW,QAAQ,CAAC,GAAE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAO,MAAO,EAAO,UAAa,CAAA,CAAK,CAAA,CAAA,GACzE,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,YAAY,CAAM,CAAA,GACzB,EAAA,EAAA,KAAA,CAAC,KAAD,CAAA,SAAA,CAAK,EAAO,WAAW,QAAQ,CAAC,GAAE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAO,MAAO,EAAO,WAAY,KAAA,EAAM,CAAA,CAAK,CAAA,CAAA,GAC9E,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,cAAc,CAAM,CAAA,GAC3B,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAY,EAAG,CAAM,CAAM,CAAA,GAChC,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,cAAc,CAAM,CAAA,GAC3B,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAQ,EAAO,SAAS,CAAM,CAAA,CACjC,IACH,EAAU,KAAK,CAAE,MAAK,QAAO,UAC5B,EAAA,EAAA,KAAA,CAAC,MAAD,CAAe,UAAWA,EAAO,SAAjC,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,IAAU,IAAA,GAAY,EAAM,EAAE,CAAK,CAAM,CAAA,GAC9C,EAAA,EAAA,IAAA,CAAC,EAAD,CAAe,KAAM,CAAA,CAClB,CAHK,EAAA,CAGL,CACN,EACA,EAAW,OAAS,IACnB,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,SAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,KAAD,CAAA,SAAK,EAAE,kBAAkB,CAAM,CAAA,GAC/B,EAAA,EAAA,IAAA,CAAC,KAAD,CAAI,UAAWA,EAAO,SACnB,SAAA,EAAW,KAAK,EAAI,KACnB,EAAA,EAAA,KAAA,CAAC,KAAD,CAAA,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAQ,EAAG,EAAE,CAAQ,CAAA,GAC5B,EAAA,EAAA,IAAA,CAAC,IAAD,CAAA,SAAI,EAAQ,EAAG,EAAG,EAAE,CAAK,CAAA,EACxB,EAAe,EAAG,EAAG,GAAI,EAAG,MAAM,CACjC,CAAA,EAJK,CAIL,CACL,CACC,CAAA,CACD,GAEJ,CAAA,CAAA,CAET,CAGA,SAAS,EAAS,CAAE,IAAG,SAAQ,UAAS,WAKjB,CACrB,GAAM,CAAC,EAAS,IAAA,EAAcC,EAAAA,SAAAA,CAAS,EAAO,OAAO,EAC/C,CAAC,EAAM,IAAA,EAAWA,EAAAA,SAAAA,CAAS,EAAO,IAAI,EACtC,CAAC,EAAY,IAAA,EAAiBA,EAAAA,SAAAA,CAAS,EAAO,UAAU,EAU9D,OACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWD,EAAO,SAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,UAAWA,EAAO,WAAa,SAAA,EAAE,cAAc,CAAS,CAAA,GAC/D,EAAA,EAAA,IAAA,CAAC,WAAD,CAAU,UAAWA,EAAO,MAAO,KAAM,EAAG,MAAO,EAAS,SAAU,GAAS,EAAW,EAAM,OAAO,KAAK,CAAI,CAAA,GAChH,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,UAAWA,EAAO,WAAa,SAAA,EAAE,WAAW,CAAS,CAAA,GAE5D,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,UAAWA,EAAO,MAAO,MAAO,EAAM,SAAU,GAAS,EAAQ,EAAM,OAAO,KAAK,EACxF,SAAA,EAAM,IAAI,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAqB,MAAO,EAAS,SAAA,EAAU,EAAG,CAAM,CAAU,EAArD,CAAqD,CAAC,CAClF,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,UAAWA,EAAO,WAAa,SAAA,GAAG,EAAE,YAAY,EAAE,GAAG,EAAW,QAAQ,CAAC,GAAW,CAAA,GAC3F,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,KAAK,QAAQ,IAAK,EAAG,IAAK,EAAG,KAAM,IAAM,MAAO,EACrD,SAAU,GAAS,EAAc,OAAO,EAAM,OAAO,KAAK,CAAC,CAAI,CAAA,GACjE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,UAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OAAQ,QAAS,EAAU,SAAA,EAAE,QAAQ,CAAU,CAAA,GACvF,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAW,GAAGA,EAAO,OAAO,GAAGA,EAAO,UAAW,YAvBpD,CACvB,EAAI,SAAU,CACZ,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,GAAI,EAAO,GAAI,MAAO,EAAO,MAAO,UAAS,OAAM,YAAW,CAAC,CACxF,CAAC,CAAC,CACC,KAAK,CAAO,CAAC,CACb,MAAO,GAAiB,CAAE,MAAM,EAAM,OAAO,CAAE,CAAC,CACrD,EAe6F,SAAA,EAAE,cAAc,CAAU,CAAA,CAC9G,GACF,GAET,CAGA,SAAgB,EAAc,CAAE,KAAiD,CAC/E,GAAM,CAAC,EAAO,IAAA,EAAYC,EAAAA,SAAAA,CAA6B,MAAM,EACvD,CAAC,EAAQ,IAAA,EAAaA,EAAAA,SAAAA,CAAS,KAAK,EACpC,CAAC,EAAM,IAAA,EAAWA,EAAAA,SAAAA,CAAS,KAAK,EAChC,CAAC,EAAG,IAAA,EAAQA,EAAAA,SAAAA,CAAS,EAAE,EACvB,CAAC,EAAQ,IAAA,EAAaA,EAAAA,SAAAA,CAAS,CAAC,EAChC,CAAC,EAAM,IAAA,EAAWA,EAAAA,SAAAA,CAA4B,IAAI,EAClD,CAAC,EAAO,IAAA,EAAYA,EAAAA,SAAAA,CAA6B,IAAI,EACrD,CAAC,EAAO,IAAA,EAAYA,EAAAA,SAAAA,CAAwB,IAAI,EAEhD,CAAC,EAAU,IAAA,EAAeA,EAAAA,SAAAA,CAAgE,IAAI,EAC9F,CAAC,EAAY,IAAA,EAAiBA,EAAAA,SAAAA,CAAS,CAAC,EAExC,CAAC,EAAU,IAAA,EAAeA,EAAAA,SAAAA,CAA8B,IAAI,GAAK,EAEjE,CAAC,EAAe,IAAA,EAAoBA,EAAAA,SAAAA,CAAS,EAAK,EAElD,GAAA,EAASC,EAAAA,YAAAA,KAAwB,CAAE,EAAc,GAAQ,EAAO,CAAC,CAAE,EAAG,CAAC,CAAC,GAE9E,EAAA,EAAA,UAAA,KAAgB,CACd,IAAI,EAAY,GACV,EAAK,IAAI,gBAAgB,CAC7B,QAAO,SAAQ,OAAM,MAAO,KAAmB,OAAQ,OAAO,CAAM,CACtE,CAAC,EAQD,OAPI,IAAM,IAAI,EAAG,IAAI,IAAK,CAAC,EAC3B,EAAgB,QAAQ,EAAG,SAAS,GAAG,CAAC,CACrC,KAAM,GAAS,CAAO,IAAa,EAAQ,CAAI,EAAG,EAAS,IAAI,EAAI,CAAC,CAAC,CACrE,MAAO,GAAqB,CAAO,GAAW,EAAS,EAAU,OAAO,CAAE,CAAC,EAC9E,EAA4B,OAAO,CAAC,CACjC,KAAM,GAAS,CAAO,GAAW,EAAS,EAAK,KAAK,CAAE,CAAC,CAAC,CACxD,UAAY,CAAkB,CAAC,MACrB,CAAE,EAAY,EAAK,CAClC,EAAG,CAAC,EAAO,EAAQ,EAAM,EAAG,EAAQ,CAAU,CAAC,EAE/C,IAAM,GAAO,EAAe,IAA4B,CACtD,EAAI,EAAO,CACT,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,GAAI,EAAO,GAAI,MAAO,EAAO,KAAM,CAAC,CAC7D,CAAC,CAAC,CACC,KAAK,CAAM,CAAC,CACZ,MAAO,GAAoB,CAAE,MAAM,EAAS,OAAO,CAAE,CAAC,CAC3D,EAEM,GAAU,EAA+B,IAA4B,CACzE,EAAY,GAAY,IAAY,MAAQ,EAAQ,OAAS,GAAc,EAAQ,OAAO,KAAO,EAAO,GACpG,KACA,CAAE,KAAM,EAAY,QAAO,CAAC,CAClC,EAEM,EAAc,GAAM,SAAW,CAAC,EAChC,EAAkB,EAAY,OAAO,GAAU,EAAS,IAAI,EAAO,EAAE,CAAC,EACtE,EAAa,EAAgB,OAAO,GAAU,EAAO,SAAW,QAAQ,CAAC,CAAC,OAC1E,EAAa,EAAgB,OAAS,EACtC,EAAc,EAAY,OAAS,GAAK,EAAY,MAAM,GAAU,EAAS,IAAI,EAAO,EAAE,CAAC,EAE3F,EAAgB,GAAqB,CACzC,EAAY,GAAW,CACrB,IAAM,EAAO,IAAI,IAAI,CAAO,EAG5B,OAFI,EAAK,IAAI,CAAE,EAAG,EAAK,OAAO,CAAE,EAC3B,EAAK,IAAI,CAAE,EACT,CACT,CAAC,CACH,EAEM,MAA6B,CAAE,EAAY,IAAI,GAAK,EAAG,EAAiB,EAAK,CAAE,EAE/E,MAA4B,CAChC,EAAY,GAAW,CACrB,IAAM,EAAO,IAAI,IAAI,CAAO,EAG5B,OAFI,EAAa,EAAY,QAAQ,GAAU,EAAK,OAAO,EAAO,EAAE,CAAC,EAChE,EAAY,QAAQ,GAAU,EAAK,IAAI,EAAO,EAAE,CAAC,EAC/C,CACT,CAAC,CACH,EAGM,EAAY,GAAqC,CACrD,IAAM,EAAU,IAAS,SACrB,EAAgB,OAAO,GAAU,EAAO,SAAW,QAAQ,EAC3D,EAAgB,OAAO,GAAU,EAAO,SAAW,QAAQ,EAC3D,EAAQ,SAAW,GACvB,QAAa,WAAW,EAAQ,IAAI,GAAU,EAAI,EAAM,CACtD,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,GAAI,EAAO,GAAI,MAAO,EAAO,KAAM,CAAC,CAC7D,CAAC,CAAC,CAAC,CAAC,CAAC,SAAW,CAAE,EAAe,EAAG,EAAO,CAAE,CAAC,CAChD,EAEM,MAA6B,CAEjC,GAAI,EAAe,CAAE,EAAS,QAAQ,EAAG,MAAO,CAChD,EAAiB,EAAI,CACvB,EAEM,EAAO,KAAK,MAAM,EAAS,EAAS,EAAI,EACxC,EAAQ,IAAS,KAAO,EAAI,KAAK,IAAI,EAAG,KAAK,KAAK,EAAK,MAAQ,EAAS,CAAC,EAE/E,OACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWF,EAAO,MAAvB,SAAA,EACE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,QAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,UACnB,SAAA,CAAC,OAAQ,SAAS,CAAC,CAAW,IAAI,IAClC,EAAA,EAAA,IAAA,CAAC,SAAD,CAAqB,KAAK,SACxB,UAAW,IAAU,EAASA,EAAO,QAAUA,EAAO,SACtD,YAAe,CAAE,EAAS,CAAM,EAAG,EAAU,CAAC,EAAG,EAAY,IAAI,EAAG,EAAe,CAAE,EACpF,SAAA,EAAE,EAAU,EAAO,CACd,EAJK,CAIL,CACT,CACE,CAAA,GACL,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OAAQ,QAAS,EAAS,SAAA,EAAE,SAAS,CAAU,CAAA,GACvF,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,YAAe,CAAE,SAAS,KAAO,4BAA4B,EAAM,iBAAkB,EAAI,SAAA,EAAE,UAAU,CAAU,CAAA,GACjH,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,YAAe,CAAE,SAAS,KAAO,4BAA4B,EAAM,aAAc,EAAI,SAAA,EAAE,YAAY,CAAU,CAAA,CAC5G,KACL,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,MACnB,UAAA,GAAS,CAAC,EAAA,CAAG,IAAI,IACjB,EAAA,EAAA,KAAA,CAAC,SAAD,CAAyB,KAAK,SAC5B,UAAW,EAAK,QAAU,EAAQ,GAAGA,EAAO,KAAK,GAAGA,EAAO,SAAWA,EAAO,KAC7E,YAAe,CAAE,EAAS,EAAK,KAAK,EAAG,EAAU,CAAC,EAAG,EAAe,CAAE,EAFxE,SAAA,EAGE,EAAA,EAAA,IAAA,CAAC,IAAD,CAAA,SAAI,EAAK,MAAM,KAAS,CAAA,GACxB,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,GAAG,EAAE,EAAU,EAAK,MAAM,EAAE,KAAK,EAAE,aAAc,CAAE,EAAG,EAAK,MAAM,MAAO,CAAC,EAAE,KAAK,EAAE,cAAe,CAAE,EAAG,KAAK,MAAM,EAAK,MAAM,YAAc,GAAG,CAAE,CAAC,GAAU,CAAA,GACjK,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,UACtB,UAAA,EAAA,EAAA,IAAA,CAAC,IAAD,CAAG,MAAO,CAAE,MAAO,GAAG,OAAO,KAAK,MAAM,EAAK,MAAM,YAAc,GAAG,CAAC,EAAE,EAAG,CAAI,CAAA,CAC1E,CAAA,CACA,CARK,EAAA,EAAK,KAQV,CACT,CACE,CAAA,GACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,QAAvB,SAAA,EACE,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAWA,EAAO,MAAO,MAAO,EACtC,SAAU,GAAS,CAAE,EAAU,EAAM,OAAO,KAAK,EAAG,EAAU,CAAC,EAAG,EAAY,IAAI,EAAG,EAAe,CAAE,EADxG,SAAA,EAEE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,MAAO,SAAA,EAAE,aAAa,CAAU,CAAA,GAC9C,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,SAAU,SAAA,EAAE,cAAc,CAAU,CAAA,GAClD,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,WAAY,SAAA,EAAE,gBAAgB,CAAU,CAAA,GACtD,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,YAAa,SAAA,EAAE,iBAAiB,CAAU,CAAA,CAClD,KACR,EAAA,EAAA,KAAA,CAAC,SAAD,CAAQ,UAAWA,EAAO,MAAO,MAAO,EACtC,SAAU,GAAS,CAAE,EAAQ,EAAM,OAAO,KAAK,EAAG,EAAU,CAAC,EAAG,EAAY,IAAI,EAAG,EAAe,CAAE,EADtG,SAAA,EAEE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,MAAM,MAAO,SAAA,EAAE,UAAU,CAAU,CAAA,EAC1C,EAAM,IAAI,IAAU,EAAA,EAAA,IAAA,CAAC,SAAD,CAAqB,MAAO,EAAS,SAAA,EAAU,EAAG,CAAM,CAAU,EAArD,CAAqD,CAAC,CAClF,KACR,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,UAAWA,EAAO,MAAO,YAAa,EAAE,mBAAmB,EAAG,MAAO,EAC1E,SAAU,GAAS,CAAE,EAAK,EAAM,OAAO,MAAM,KAAK,CAAC,EAAG,EAAU,CAAC,EAAG,EAAY,IAAI,EAAG,EAAe,CAAE,CAAI,CAAA,CAC3G,IACJ,EAAS,KAAO,IACf,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,SAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAE,gBAAiB,CAAE,EAAG,EAAS,IAAK,CAAC,CAAQ,CAAA,GACtD,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OAAQ,QAAS,EACtD,SAAc,EAAd,EAAgB,cAAmB,WAAW,CACzC,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OAAQ,SAAU,IAAe,EACvE,YAAe,EAAS,SAAS,EAAI,SAAA,EAAE,eAAgB,CAAE,EAAG,CAAW,CAAC,CAAU,CAAA,GACpF,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SACX,UAAW,EAAgB,GAAGA,EAAO,OAAO,GAAGA,EAAO,SAAWA,EAAO,OACxE,SAAU,IAAe,EACzB,QAAS,EACR,SAAgB,EAAhB,EAAkB,qBAA6C,cAAvB,CAAE,EAAG,CAAW,CAAuC,CAC1F,CAAA,GACR,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OAAQ,QAAS,EAAiB,SAAA,EAAE,gBAAgB,CAAU,CAAA,CACnG,IAEN,IAAU,OAAQ,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,MAAQ,SAAA,EAAE,aAAc,CAAE,IAAK,CAAM,CAAC,CAAO,CAAA,EACtF,IAAU,MAAQ,IAAS,MAAQ,CAAC,EAAG,EAAG,CAAC,CAAC,CAAC,IAAI,IAChD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAiB,UAAWA,EAAO,SAAnC,SAAA,EAA6C,EAAA,EAAA,IAAA,CAAC,IAAD,CAAI,CAAA,GAAC,EAAA,EAAA,IAAA,CAAC,IAAD,CAAI,CAAA,GAAC,EAAA,EAAA,IAAA,CAAC,IAAD,CAAI,CAAA,CAAM,CAAvD,EAAA,CAAuD,CAClE,EACA,IAAU,MAAQ,IAAS,MAAQ,EAAK,QAAQ,SAAW,IAC1D,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,MAAQ,SAAA,EAAE,OAAO,CAAO,CAAA,GAE/C,GAAM,SAAW,CAAC,EAAA,CAAG,KAAK,EAAQ,KAClC,EAAA,EAAA,KAAA,CAAC,MAAD,CACE,UAAW,EAAS,IAAI,EAAO,EAAE,EAAI,GAAGA,EAAO,KAAK,GAAGA,EAAO,eAAiBA,EAAO,KACtF,MAAO,CAAE,eAAgB,GAAG,OAAO,KAAK,IAAI,EAAO,EAAE,EAAI,EAAE,EAAE,GAAI,EAFnE,SAAA,EAGE,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,KAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,QAAD,CAAO,KAAK,WAAW,UAAWA,EAAO,UAAW,QAAS,EAAS,IAAI,EAAO,EAAE,EACjF,aAAY,EAAO,QAAQ,MAAM,EAAG,EAAE,EAAG,aAAgB,EAAa,EAAO,EAAE,CAAI,CAAA,GACrF,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,GAAGA,EAAO,WAAW,GAAGA,EAAO,EAAO,UAAY,SAAA,EAAE,EAAW,EAAO,OAAO,CAAQ,CAAA,GACtG,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,KAAO,SAAA,EAAU,EAAG,EAAO,IAAI,CAAQ,CAAA,GAC/D,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,SAAW,SAAA,EAAE,EAAU,EAAO,MAAM,CAAQ,CAAA,GACpE,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,QAAS,MAAO,EAAQ,EAAO,SAAS,EAAI,SAAA,EAAQ,EAAG,EAAO,SAAS,CAAQ,CAAA,CACpG,KACL,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,QAAU,SAAA,EAAO,OAAa,CAAA,GACrD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,KAAvB,SAAA,EACE,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,WAAxB,SAAA,CAAqC,EAAE,YAAY,GAAE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAO,MAAO,EAAO,UAAa,CAAA,EAAE,EAAO,WAAW,QAAQ,CAAC,CAAQ,KAC5H,EAAA,EAAA,KAAA,CAAC,OAAD,CAAM,UAAWA,EAAO,WAAxB,SAAA,CAAqC,EAAE,YAAY,GAAE,EAAA,EAAA,IAAA,CAAC,EAAD,CAAO,MAAO,EAAO,WAAY,KAAA,EAAM,CAAA,EAAE,EAAO,WAAW,QAAQ,CAAC,CAAQ,KACjI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAE,cAAe,CAAE,EAAG,EAAO,WAAY,CAAC,CAAQ,CAAA,GACzD,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAY,EAAG,CAAM,CAAQ,CAAA,CACjC,KACL,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,IAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,YAAe,CAAE,EAAO,SAAU,CAAM,CAAE,EAAI,SAAA,EAAE,QAAQ,CAAU,CAAA,GACpE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,YAAe,CAAE,EAAO,OAAQ,CAAM,CAAE,EAAI,SAAA,EAAE,MAAM,CAAU,CAAA,EAC/D,EAAO,SAAW,UACf,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,YAAe,CAAE,EAAI,SAAU,CAAM,CAAE,EAAI,SAAA,EAAE,QAAQ,CAAU,CAAA,GACjE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,YAAe,CAAE,EAAI,UAAW,CAAM,CAAE,EAAI,SAAA,EAAE,SAAS,CAAU,CAAA,CACpE,IACJ,IAAa,MAAQ,EAAS,OAAO,KAAO,EAAO,KAClD,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,OACpB,SAAA,EAAS,OAAS,UACf,EAAA,EAAA,IAAA,CAAC,EAAD,CAAe,IAAG,SAAU,EAAO,GAAI,MAAO,EAAO,KAAQ,CAAA,GAC7D,EAAA,EAAA,IAAA,CAAC,EAAD,CAAa,IAAW,SAAQ,YAAe,CAAE,EAAY,IAAI,CAAE,EACjE,YAAe,CAAE,EAAY,IAAI,EAAG,EAAO,CAAE,CAAI,CAAA,CACpD,CAAA,CAEJ,CArCK,EAAA,EAAO,EAqCZ,CACN,GACD,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAWA,EAAO,MAAvB,SAAA,EACE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OAAQ,SAAU,IAAW,EACnE,YAAe,CAAE,EAAU,KAAK,IAAI,EAAG,EAAS,EAAS,CAAC,EAAG,EAAY,IAAI,EAAG,EAAe,CAAE,EAAI,SAAA,EAAE,UAAU,CAAU,CAAA,GAC7H,EAAA,EAAA,IAAA,CAAC,OAAD,CAAA,SAAO,EAAE,YAAa,CAAE,OAAM,QAAO,MAAO,GAAM,OAAS,CAAE,CAAC,CAAQ,CAAA,GACtE,EAAA,EAAA,IAAA,CAAC,SAAD,CAAQ,KAAK,SAAS,UAAWA,EAAO,OACtC,SAAU,IAAS,MAAQ,EAAS,IAAa,EAAK,MACtD,YAAe,CAAE,EAAU,EAAS,EAAS,EAAG,EAAY,IAAI,EAAG,EAAe,CAAE,EAAI,SAAA,EAAE,UAAU,CAAU,CAAA,CAC7G,GACF,GAET,CCtfA,IAAa,EAAK,SA2EL,EAAgC,CAC3C,IAAK,MACL,UAAW,MACX,aAAc,MACd,QAAS,KACT,SAAU,QACV,WAAY,UACZ,YAAa,OACb,SAAU,OACV,kBAAmB,SACnB,aAAc,MACd,eAAgB,MAChB,gBAAiB,MACjB,SAAU,KACV,eAAgB,KAChB,aAAc,KACd,YAAa,KACb,UAAW,KACX,aAAc,KACd,UAAW,KACX,YAAa,KACb,WAAY,MACZ,WAAY,MACZ,WAAY,KACZ,YAAa,WACb,aAAc,OACd,YAAa,KACb,eAAgB,UAChB,aAAc,UACd,YAAa,SACb,eAAgB,OAChB,cAAe,UACf,MAAO,UACP,WAAY,SACZ,YAAa,WACb,OAAQ,KACR,KAAM,KACN,OAAQ,KACR,QAAS,KACT,MAAO,YACP,WAAY,aACZ,QAAS,OACT,UAAW,qCACX,SAAU,MACV,SAAU,MACV,OAAQ,KACR,aAAc,YACd,iBAAkB,KAClB,aAAc,KACd,gBAAiB,KACjB,iBAAkB,OAClB,gBAAiB,MACjB,cAAe,KACf,eAAgB,KAChB,WAAY,KACZ,QAAS,KACT,SAAU,KACV,SAAU,KACV,UAAW,KACX,QAAS,OACT,aAAc,MACd,gBAAiB,OACjB,iBAAkB,OAClB,UAAW,OACX,YAAa,OACb,cAAe,WACf,YAAa,YACb,mBAAoB,cACpB,aAAc,YACd,eAAgB,MAClB,EAGa,EAAgC,CAC3C,IAAK,iBACL,UAAW,OACX,aAAc,UACd,QAAS,UACT,SAAU,YACV,WAAY,cACZ,YAAa,eACb,SAAU,YACV,kBAAmB,kBACnB,aAAc,SACd,eAAgB,WAChB,gBAAiB,YACjB,SAAU,OACV,eAAgB,aAChB,aAAc,WACd,YAAa,UACb,UAAW,QACX,aAAc,UACd,UAAW,OACX,YAAa,SACb,WAAY,QACZ,WAAY,aACZ,WAAY,aACZ,YAAa,gBACb,aAAc,UACd,YAAa,WACb,eAAgB,cAChB,aAAc,aACd,YAAa,YACb,eAAgB,mBAChB,cAAe,cACf,MAAO,YACP,WAAY,aACZ,YAAa,cACb,OAAQ,UACR,KAAM,OACN,OAAQ,SACR,QAAS,UACT,MAAO,uBACP,WAAY,wBACZ,QAAS,WACT,UAAW,0CACX,SAAU,OACV,SAAU,OACV,OAAQ,SACR,aAAc,4BACd,iBAAkB,aAClB,aAAc,SACd,gBAAiB,YACjB,iBAAkB,oBAClB,gBAAiB,gBACjB,cAAe,aACf,eAAgB,cAChB,WAAY,UACZ,QAAS,QACT,SAAU,SACV,SAAU,SACV,UAAW,UACX,QAAS,QACT,aAAc,aACd,gBAAiB,iBACjB,iBAAkB,kBAClB,UAAW,qBACX,YAAa,eACb,cAAe,eACf,YAAa,wBACb,mBAAoB,sBACpB,aAAc,yBACd,eAAgB,iBAClB,ECvMa,EAAS,CAAC,QAAS,QAAQ,EAOxC,SAAgB,EAAM,EAAoB,CACxC,EAAI,WAAa,EAAI,OAAO,SAAS,EAAI,CAAE,KAAI,IAAG,CAAC,EAAG,0BAA0B,EAChF,IAAM,EAAI,EAAI,OAAO,KAAK,CAAE,EAC5B,EAAI,MAAM,OAAO,uBAA0B,EAAI,MAAM,SAAS,CAC5D,KAAM,mBACN,GAAI,SACJ,MAAO,GACP,UAAa,EAAE,KAAK,EACpB,OAAQ,CACV,EAAG,CAAa,CAAC,CACnB"}
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
2
3
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
5
5
|
import z from "@deepseek-ai/schemastery";
|
|
6
|
-
import {
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
8
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
8
9
|
//#region src/config.ts
|
|
9
10
|
/**
|
|
@@ -21,7 +22,10 @@ const CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
21
22
|
"provider",
|
|
22
23
|
"model",
|
|
23
24
|
"decayAfterDays",
|
|
24
|
-
"decayImportanceBelow"
|
|
25
|
+
"decayImportanceBelow",
|
|
26
|
+
"injectTokenBudget",
|
|
27
|
+
"rankRecencyWeight",
|
|
28
|
+
"rankProofWeight"
|
|
25
29
|
]);
|
|
26
30
|
const INGEST_MODES = /* @__PURE__ */ new Set([
|
|
27
31
|
"off",
|
|
@@ -39,13 +43,16 @@ const Config = z.object({
|
|
|
39
43
|
provider: z.string(),
|
|
40
44
|
model: z.string(),
|
|
41
45
|
decayAfterDays: z.number().step(1).min(1).max(3650),
|
|
42
|
-
decayImportanceBelow: z.number().min(0).max(1)
|
|
46
|
+
decayImportanceBelow: z.number().min(0).max(1),
|
|
47
|
+
injectTokenBudget: z.number().step(1).min(128).max(8192),
|
|
48
|
+
rankRecencyWeight: z.number().min(0).max(2),
|
|
49
|
+
rankProofWeight: z.number().min(0).max(2)
|
|
43
50
|
});
|
|
44
51
|
/**
|
|
45
52
|
* 显式 resolve 步骤:默认值只在唯一的此处落地,非法值 loud 失败。
|
|
46
53
|
* @param config - cordis.yml 传入的未校验配置。
|
|
47
54
|
* @returns 完整解析配置。
|
|
48
|
-
* @throws 未知键、ingest 档位非法、provider/model 只给其一、decay
|
|
55
|
+
* @throws 未知键、ingest 档位非法、provider/model 只给其一、decay/预算/排序权重越界时抛错。
|
|
49
56
|
*/
|
|
50
57
|
function resolveConfig(config = {}) {
|
|
51
58
|
for (const key of Object.keys(config)) if (!CONFIG_KEYS.has(key)) throw new Error(`dsh-engram: unknown config key "${key}"`);
|
|
@@ -56,6 +63,9 @@ function resolveConfig(config = {}) {
|
|
|
56
63
|
if (hasProvider !== hasModel) throw new Error("dsh-engram: provider and model must be supplied together");
|
|
57
64
|
if (config.decayAfterDays !== void 0 && (!Number.isInteger(config.decayAfterDays) || config.decayAfterDays < 1 || config.decayAfterDays > 3650)) throw new Error("dsh-engram: decayAfterDays must be an integer in [1, 3650]");
|
|
58
65
|
if (config.decayImportanceBelow !== void 0 && (config.decayImportanceBelow < 0 || config.decayImportanceBelow > 1)) throw new Error("dsh-engram: decayImportanceBelow must be in [0, 1]");
|
|
66
|
+
if (config.injectTokenBudget !== void 0 && (!Number.isInteger(config.injectTokenBudget) || config.injectTokenBudget < 128 || config.injectTokenBudget > 8192)) throw new Error("dsh-engram: injectTokenBudget must be an integer in [128, 8192]");
|
|
67
|
+
if (config.rankRecencyWeight !== void 0 && (config.rankRecencyWeight < 0 || config.rankRecencyWeight > 2)) throw new Error("dsh-engram: rankRecencyWeight must be in [0, 2]");
|
|
68
|
+
if (config.rankProofWeight !== void 0 && (config.rankProofWeight < 0 || config.rankProofWeight > 2)) throw new Error("dsh-engram: rankProofWeight must be in [0, 2]");
|
|
59
69
|
const dbDir = config.dbDir ?? join(homedir(), ".dsh", "engram");
|
|
60
70
|
return {
|
|
61
71
|
dbDir,
|
|
@@ -69,7 +79,10 @@ function resolveConfig(config = {}) {
|
|
|
69
79
|
model: config.model
|
|
70
80
|
} : void 0,
|
|
71
81
|
decayAfterDays: config.decayAfterDays ?? 30,
|
|
72
|
-
decayImportanceBelow: config.decayImportanceBelow ?? .3
|
|
82
|
+
decayImportanceBelow: config.decayImportanceBelow ?? .3,
|
|
83
|
+
injectTokenBudget: config.injectTokenBudget ?? 1024,
|
|
84
|
+
rankRecencyWeight: config.rankRecencyWeight ?? .2,
|
|
85
|
+
rankProofWeight: config.rankProofWeight ?? .1
|
|
73
86
|
};
|
|
74
87
|
}
|
|
75
88
|
//#endregion
|
|
@@ -246,6 +259,27 @@ const MODE_LIMITS = {
|
|
|
246
259
|
};
|
|
247
260
|
/** 摄取输出 token 上限(提取 JSON 数组,短输出足够)。 */
|
|
248
261
|
const INGEST_MAX_TOKENS = 600;
|
|
262
|
+
/** op_log 幂等键 op:该 (sessionId, turn) 已完成摄取。 */
|
|
263
|
+
const INGEST_DONE_OP = "ingest-done";
|
|
264
|
+
/** op_log pending 键 op:disposed 末轮摄取失败/超时,待下次会话重放补做。 */
|
|
265
|
+
const INGEST_PENDING_OP = "ingest-pending";
|
|
266
|
+
/** disposed 末轮摄取的超时(fire-and-forget 观察器,进程退出可能打断,必须限时)。 */
|
|
267
|
+
const FINAL_INGEST_TIMEOUT_MS = 5e3;
|
|
268
|
+
/** 幂等键编码:`${sessionId}#${turn}`。 */
|
|
269
|
+
function encodeTurnKey(sessionId, turn) {
|
|
270
|
+
return `${sessionId}#${turn}`;
|
|
271
|
+
}
|
|
272
|
+
/** 幂等键解码;损坏的键返回 undefined(调用方直接出队)。 */
|
|
273
|
+
function decodeTurnKey(detail) {
|
|
274
|
+
const sep = detail.lastIndexOf("#");
|
|
275
|
+
if (sep <= 0) return void 0;
|
|
276
|
+
const turn = Number(detail.slice(sep + 1));
|
|
277
|
+
if (!Number.isInteger(turn) || turn < 0) return void 0;
|
|
278
|
+
return {
|
|
279
|
+
sessionId: detail.slice(0, sep),
|
|
280
|
+
turn
|
|
281
|
+
};
|
|
282
|
+
}
|
|
249
283
|
const INGEST_SYSTEM = [
|
|
250
284
|
"从对话记录中提取值得跨会话长期记住的用户信息(事实/偏好/决策/经历/做事方法)。",
|
|
251
285
|
"只输出一个 JSON 数组,每项形如 {\"content\": \"一句话完整表述\", \"kind\": \"fact|preference|decision|episode|skill\", \"importance\": 0到1的小数}。",
|
|
@@ -273,27 +307,77 @@ function collectTexts(events, includeAssistant) {
|
|
|
273
307
|
minSeq
|
|
274
308
|
};
|
|
275
309
|
}
|
|
310
|
+
/** 各 turn/start 事件的下标与轮次号(缺 data.turn 时轮次为 undefined)。 */
|
|
311
|
+
function turnStarts(events) {
|
|
312
|
+
const starts = [];
|
|
313
|
+
for (let i = 0; i < events.length; i++) {
|
|
314
|
+
if (events[i]?.type !== "turn/start") continue;
|
|
315
|
+
const turn = events[i].data?.turn;
|
|
316
|
+
starts.push({
|
|
317
|
+
index: i,
|
|
318
|
+
turn: typeof turn === "number" ? turn : void 0
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return starts;
|
|
322
|
+
}
|
|
276
323
|
/** 上一轮事件切片:最后一个 turn/start 之前的那一轮(含其中的全部事件)。 */
|
|
277
324
|
function previousTurnSlice(events) {
|
|
278
|
-
const starts =
|
|
279
|
-
for (let i = 0; i < events.length; i++) if (events[i]?.type === "turn/start") starts.push(i);
|
|
325
|
+
const starts = turnStarts(events);
|
|
280
326
|
if (starts.length < 2) return [];
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
327
|
+
return events.slice(starts[starts.length - 2].index, starts[starts.length - 1].index);
|
|
328
|
+
}
|
|
329
|
+
/** 末轮事件切片:最后一个 turn/start 到日志末尾(session/disposed 摄取用)。 */
|
|
330
|
+
function lastTurnSlice(events) {
|
|
331
|
+
const starts = turnStarts(events);
|
|
332
|
+
if (starts.length === 0) return [];
|
|
333
|
+
return events.slice(starts[starts.length - 1].index);
|
|
334
|
+
}
|
|
335
|
+
/** 最后一个 turn/start 的轮次号;无 turn/start 或缺 data.turn 时 undefined。 */
|
|
336
|
+
function lastTurnNumber(events) {
|
|
337
|
+
const starts = turnStarts(events);
|
|
338
|
+
return starts.length === 0 ? void 0 : starts[starts.length - 1].turn;
|
|
339
|
+
}
|
|
340
|
+
/** 指定轮次的事件切片:该轮 turn/start 到下一轮 turn/start(无下一轮则到日志末尾)。 */
|
|
341
|
+
function turnSlice(events, turn) {
|
|
342
|
+
const starts = turnStarts(events);
|
|
343
|
+
const position = starts.findIndex((start) => start.turn === turn);
|
|
344
|
+
if (position === -1) return [];
|
|
345
|
+
const end = position + 1 < starts.length ? starts[position + 1].index : events.length;
|
|
346
|
+
return events.slice(starts[position].index, end);
|
|
284
347
|
}
|
|
285
348
|
/**
|
|
286
|
-
*
|
|
349
|
+
* 执行一次摄取(默认上一轮;slice 指定末轮或显式轮次)。幂等:op_log 已存在
|
|
350
|
+
* 该 (sessionId, turn) 的 done 键时直接跳过;成功完成后写入 done 键。
|
|
287
351
|
* @returns 结果摘要;异常由调用方捕获计数(不重试)。
|
|
288
352
|
*/
|
|
289
353
|
async function ingestPreviousTurn(deps) {
|
|
290
354
|
const limits = MODE_LIMITS[deps.mode];
|
|
291
|
-
const
|
|
355
|
+
const sliceMode = deps.slice ?? "previous";
|
|
356
|
+
let slice;
|
|
357
|
+
let round;
|
|
358
|
+
if (sliceMode === "previous") {
|
|
359
|
+
slice = previousTurnSlice(deps.events);
|
|
360
|
+
round = Math.max(0, deps.turn - 1);
|
|
361
|
+
} else if (sliceMode === "last") {
|
|
362
|
+
slice = lastTurnSlice(deps.events);
|
|
363
|
+
round = lastTurnNumber(deps.events) ?? deps.turn;
|
|
364
|
+
} else {
|
|
365
|
+
slice = turnSlice(deps.events, sliceMode);
|
|
366
|
+
round = sliceMode;
|
|
367
|
+
}
|
|
292
368
|
if (slice.length === 0) return {
|
|
293
369
|
scannedEvents: 0,
|
|
294
370
|
candidates: 0,
|
|
295
371
|
written: 0,
|
|
296
|
-
skipped: "no-previous-turn"
|
|
372
|
+
skipped: typeof sliceMode === "number" ? "no-such-turn" : "no-previous-turn"
|
|
373
|
+
};
|
|
374
|
+
const store = await deps.openStore();
|
|
375
|
+
const doneKey = encodeTurnKey(deps.sessionId, round);
|
|
376
|
+
if (await store.hasAudit("ingest-done", doneKey)) return {
|
|
377
|
+
scannedEvents: slice.length,
|
|
378
|
+
candidates: 0,
|
|
379
|
+
written: 0,
|
|
380
|
+
skipped: "already-ingested"
|
|
297
381
|
};
|
|
298
382
|
const { texts, minSeq } = collectTexts(slice, limits.includeAssistant);
|
|
299
383
|
if (texts.length === 0) return {
|
|
@@ -312,7 +396,7 @@ async function ingestPreviousTurn(deps) {
|
|
|
312
396
|
const userText = `从下面这轮对话(JSON 数组)提取值得长期记住的信息:\n${JSON.stringify(texts)}`;
|
|
313
397
|
deps.logRequest({
|
|
314
398
|
route,
|
|
315
|
-
round
|
|
399
|
+
round,
|
|
316
400
|
userText,
|
|
317
401
|
maxTokens: INGEST_MAX_TOKENS,
|
|
318
402
|
mode: deps.mode
|
|
@@ -331,7 +415,6 @@ async function ingestPreviousTurn(deps) {
|
|
|
331
415
|
written: 0,
|
|
332
416
|
skipped: "unparseable-output"
|
|
333
417
|
};
|
|
334
|
-
const store = await deps.openStore();
|
|
335
418
|
const embedder = await deps.embedder;
|
|
336
419
|
const writtenContents = [];
|
|
337
420
|
let written = 0;
|
|
@@ -359,13 +442,14 @@ async function ingestPreviousTurn(deps) {
|
|
|
359
442
|
importance,
|
|
360
443
|
confidence: limits.confidence,
|
|
361
444
|
sourceSessionId: deps.sessionId,
|
|
362
|
-
sourceRound:
|
|
445
|
+
sourceRound: round,
|
|
363
446
|
...minSeq === null ? {} : { sourceSeq: minSeq },
|
|
364
447
|
...embedder === void 0 ? {} : { embedding: (await embedder.embed([content]))[0] }
|
|
365
448
|
});
|
|
366
449
|
writtenContents.push(content);
|
|
367
450
|
written += 1;
|
|
368
451
|
}
|
|
452
|
+
await store.audit(INGEST_DONE_OP, deps.sessionId, doneKey);
|
|
369
453
|
return {
|
|
370
454
|
scannedEvents: slice.length,
|
|
371
455
|
candidates: parsed.length,
|
|
@@ -373,6 +457,78 @@ async function ingestPreviousTurn(deps) {
|
|
|
373
457
|
skipped: null
|
|
374
458
|
};
|
|
375
459
|
}
|
|
460
|
+
/**
|
|
461
|
+
* 写入 pending 键(done 或 pending 已存在时不重复写)。仅在末轮摄取失败/超时后调用。
|
|
462
|
+
*/
|
|
463
|
+
async function markPendingIngest(store, sessionId, turn) {
|
|
464
|
+
const key = encodeTurnKey(sessionId, turn);
|
|
465
|
+
if (await store.hasAudit("ingest-done", key)) return;
|
|
466
|
+
if (await store.hasAudit("ingest-pending", key)) return;
|
|
467
|
+
await store.audit(INGEST_PENDING_OP, sessionId, key);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* 会话结束时的末轮摄取:切片为最后一个 turn/start 到日志末尾,复用提炼管线。
|
|
471
|
+
* 失败/超时只告警并把 (sessionId, turn) pending 键写入 op_log(下次会话首次
|
|
472
|
+
* pre-step 重放补做),绝不影响对话。
|
|
473
|
+
* @returns 摄取结果;无末轮或失败(已落 pending)时返回 null。
|
|
474
|
+
*/
|
|
475
|
+
async function ingestFinalTurn(deps) {
|
|
476
|
+
const round = lastTurnNumber(deps.events);
|
|
477
|
+
if (round === void 0) return null;
|
|
478
|
+
try {
|
|
479
|
+
return await ingestPreviousTurn({
|
|
480
|
+
...deps,
|
|
481
|
+
slice: "last"
|
|
482
|
+
});
|
|
483
|
+
} catch (error) {
|
|
484
|
+
try {
|
|
485
|
+
await markPendingIngest(await deps.openStore(), deps.sessionId, round);
|
|
486
|
+
} catch {}
|
|
487
|
+
console.warn("[dsh-engram] 会话结束的末轮摄取失败(已记入待补做队列,不影响对话):", error);
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* 重放待补做的末轮摄取(下次会话首次 pre-step 调用)。已有 done 标记或键损坏的
|
|
493
|
+
* pending 直接出队;事件源不可得的保留到下次。单个键失败抛出,剩余键留待下次。
|
|
494
|
+
*/
|
|
495
|
+
async function replayPendingIngests(deps) {
|
|
496
|
+
const store = await deps.openStore();
|
|
497
|
+
const pendings = await store.listAuditDetails(INGEST_PENDING_OP);
|
|
498
|
+
let replayed = 0;
|
|
499
|
+
let kept = 0;
|
|
500
|
+
for (const detail of pendings) {
|
|
501
|
+
const key = decodeTurnKey(detail);
|
|
502
|
+
if (key === void 0 || await store.hasAudit("ingest-done", detail)) {
|
|
503
|
+
await store.clearAudit(INGEST_PENDING_OP, detail);
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
const events = await deps.resolveEvents(key.sessionId);
|
|
507
|
+
if (events === void 0) {
|
|
508
|
+
kept += 1;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
await ingestPreviousTurn({
|
|
512
|
+
events,
|
|
513
|
+
sessionId: key.sessionId,
|
|
514
|
+
turn: key.turn,
|
|
515
|
+
slice: key.turn,
|
|
516
|
+
openStore: deps.openStore,
|
|
517
|
+
embedder: deps.embedder,
|
|
518
|
+
mode: deps.mode,
|
|
519
|
+
routeOverride: deps.routeOverride,
|
|
520
|
+
call: deps.call,
|
|
521
|
+
logRequest: deps.logRequest,
|
|
522
|
+
signal: deps.signal
|
|
523
|
+
});
|
|
524
|
+
await store.clearAudit(INGEST_PENDING_OP, detail);
|
|
525
|
+
replayed += 1;
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
replayed,
|
|
529
|
+
kept
|
|
530
|
+
};
|
|
531
|
+
}
|
|
376
532
|
//#endregion
|
|
377
533
|
//#region src/routes.ts
|
|
378
534
|
/** 回环 peer:IPv4 127/8、IPv6 ::1、IPv4-mapped IPv6。 */
|
|
@@ -566,6 +722,129 @@ function registerEngramRoutes(ctx, deps) {
|
|
|
566
722
|
}), "dsh-engram: api routes");
|
|
567
723
|
}
|
|
568
724
|
//#endregion
|
|
725
|
+
//#region src/project/identity.ts
|
|
726
|
+
/**
|
|
727
|
+
* 项目标识:git origin URL 归一化 → sha256 短哈希分库名;无 git 或无 origin 时
|
|
728
|
+
* 回退 cwd 编码(v0.4 现状算法)。纯文件读(.git/config、worktree 的 gitdir/commondir
|
|
729
|
+
* 指针),不起子进程。启动时负责旧库文件向新标识的 rename 迁移。
|
|
730
|
+
* @module @kenz1117/dsh-engram/project/identity
|
|
731
|
+
*/
|
|
732
|
+
/** v0.4 旧命名:cwd 的 hex 编码前 24 位(无 git 时仍是兜底命名)。 */
|
|
733
|
+
function legacyProjectDbName(cwd) {
|
|
734
|
+
return `project-${Buffer.from(cwd).toString("hex").slice(0, 24)}.db`;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* 归一化 git origin URL:去协议与凭证、host 小写、去尾部 `.git` 与 `/`,
|
|
738
|
+
* 使 `git@github.com:a/b.git` 与 `https://github.com/a/b` 等价。无法解析返回 undefined。
|
|
739
|
+
*/
|
|
740
|
+
function normalizeOriginUrl(raw) {
|
|
741
|
+
const trimmed = raw.trim();
|
|
742
|
+
if (trimmed === "") return void 0;
|
|
743
|
+
const scp = /^[^@\s]+@([^:\s]+):(.+)$/.exec(trimmed);
|
|
744
|
+
let host;
|
|
745
|
+
let path;
|
|
746
|
+
if (scp !== null) {
|
|
747
|
+
host = scp[1];
|
|
748
|
+
path = scp[2];
|
|
749
|
+
} else {
|
|
750
|
+
const withScheme = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
751
|
+
try {
|
|
752
|
+
const url = new URL(withScheme);
|
|
753
|
+
host = url.host;
|
|
754
|
+
path = url.pathname;
|
|
755
|
+
} catch {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const normalizedPath = path.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/, "").replace(/\/+$/, "");
|
|
760
|
+
if (host === "" || normalizedPath === "") return void 0;
|
|
761
|
+
return `${host.toLowerCase()}/${normalizedPath}`;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* 定位真 git config:`.git` 为目录时取其 config;为文件(worktree/submodule)时
|
|
765
|
+
* 沿 `gitdir:` 指针找到 gitdir,再沿其中的 `commondir` 指针回到主 git 目录。
|
|
766
|
+
* 任一环节缺失返回 undefined。
|
|
767
|
+
*/
|
|
768
|
+
function resolveGitConfigPath(cwd) {
|
|
769
|
+
const dotGit = join(cwd, ".git");
|
|
770
|
+
let stat;
|
|
771
|
+
try {
|
|
772
|
+
stat = statSync(dotGit);
|
|
773
|
+
} catch {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (stat.isDirectory()) return existsSync(join(dotGit, "config")) ? join(dotGit, "config") : void 0;
|
|
777
|
+
if (!stat.isFile()) return void 0;
|
|
778
|
+
const pointer = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, "utf8"));
|
|
779
|
+
if (pointer === null) return void 0;
|
|
780
|
+
const gitdir = resolve(cwd, pointer[1].trim());
|
|
781
|
+
let common = gitdir;
|
|
782
|
+
try {
|
|
783
|
+
const commondir = readFileSync(join(gitdir, "commondir"), "utf8").trim();
|
|
784
|
+
if (commondir !== "") common = resolve(gitdir, commondir);
|
|
785
|
+
} catch {}
|
|
786
|
+
const configPath = join(common, "config");
|
|
787
|
+
return existsSync(configPath) ? configPath : void 0;
|
|
788
|
+
}
|
|
789
|
+
/** 从 git config 文本提取 `[remote "origin"]` 段的 url(手写 INI 行解析,不引依赖)。 */
|
|
790
|
+
function readOriginUrl(configPath) {
|
|
791
|
+
let text;
|
|
792
|
+
try {
|
|
793
|
+
text = readFileSync(configPath, "utf8");
|
|
794
|
+
} catch {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
let inOrigin = false;
|
|
798
|
+
for (const line of text.split("\n")) {
|
|
799
|
+
const trimmed = line.trim();
|
|
800
|
+
if (trimmed.startsWith("[")) {
|
|
801
|
+
inOrigin = /^\[remote\s+"origin"\]$/i.test(trimmed);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (inOrigin) {
|
|
805
|
+
const entry = /^url\s*=\s*(.+)$/.exec(trimmed);
|
|
806
|
+
if (entry !== null) return entry[1].trim();
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* 解析项目标识:origin URL 归一化后取 sha256 hex 前 24 位;无 git、无 origin
|
|
812
|
+
* 或 URL 无法解析时回退 cwd 旧算法。
|
|
813
|
+
*/
|
|
814
|
+
function resolveProjectIdentity(cwd) {
|
|
815
|
+
const legacyDbName = legacyProjectDbName(cwd);
|
|
816
|
+
const configPath = resolveGitConfigPath(cwd);
|
|
817
|
+
const origin = configPath === void 0 ? void 0 : readOriginUrl(configPath);
|
|
818
|
+
const normalized = origin === void 0 ? void 0 : normalizeOriginUrl(origin);
|
|
819
|
+
if (normalized === void 0) return {
|
|
820
|
+
dbName: legacyDbName,
|
|
821
|
+
source: "cwd",
|
|
822
|
+
legacyDbName
|
|
823
|
+
};
|
|
824
|
+
return {
|
|
825
|
+
dbName: `project-${createHash("sha256").update(normalized).digest("hex").slice(0, 24)}.db`,
|
|
826
|
+
source: "origin",
|
|
827
|
+
legacyDbName
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* 旧库迁移:origin 库不存在而 cwd 旧库存在时同目录 rename(零数据搬运);
|
|
832
|
+
* 两者都存在时不合并、不动文件,返回 kept-both 由调用方告警。仅 origin 标识下有意义。
|
|
833
|
+
*/
|
|
834
|
+
function migrateProjectDb(dbDir, identity) {
|
|
835
|
+
if (identity.source !== "origin" || identity.dbName === identity.legacyDbName) return "none";
|
|
836
|
+
const next = join(dbDir, identity.dbName);
|
|
837
|
+
const legacy = join(dbDir, identity.legacyDbName);
|
|
838
|
+
const hasNext = existsSync(next);
|
|
839
|
+
const hasLegacy = existsSync(legacy);
|
|
840
|
+
if (hasNext && hasLegacy) return "kept-both";
|
|
841
|
+
if (!hasNext && hasLegacy) {
|
|
842
|
+
renameSync(legacy, next);
|
|
843
|
+
return "renamed";
|
|
844
|
+
}
|
|
845
|
+
return "none";
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
569
848
|
//#region src/store/sqlite.ts
|
|
570
849
|
/**
|
|
571
850
|
* EngramStore 的 node:sqlite 实现:节点表 + 边表 + FTS5(unicode61 + 中文 2-gram 预切词)
|
|
@@ -651,13 +930,20 @@ function ftsMatchExpression(text) {
|
|
|
651
930
|
if (tokens.length === 0) return void 0;
|
|
652
931
|
return tokens.map((token) => `"${token.replaceAll("\"", "\"\"")}"`).join(" OR ");
|
|
653
932
|
}
|
|
933
|
+
/** 缺省不加 boost(测试与脚本直开库时保持旧排序行为)。 */
|
|
934
|
+
const NO_BOOST = {
|
|
935
|
+
recencyWeight: 0,
|
|
936
|
+
proofWeight: 0,
|
|
937
|
+
decayAfterDays: 30
|
|
938
|
+
};
|
|
654
939
|
/**
|
|
655
940
|
* 打开(必要时创建)一个 scope 分库。
|
|
656
941
|
* @param path - SQLite 文件路径;目录不存在会自动创建(0o700)。
|
|
942
|
+
* @param rankBoost - 排序 boost 参数;缺省不乘任何因子。
|
|
657
943
|
* @returns 就绪的 EngramStore。
|
|
658
944
|
* @throws EngramError(code=SCHEMA_INCOMPATIBLE) 当库的 schema 版本高于当前实现。
|
|
659
945
|
*/
|
|
660
|
-
async function openEngramStore(path) {
|
|
946
|
+
async function openEngramStore(path, rankBoost = NO_BOOST) {
|
|
661
947
|
await mkdir(dirname(path), {
|
|
662
948
|
recursive: true,
|
|
663
949
|
mode: 448
|
|
@@ -707,6 +993,9 @@ async function openEngramStore(path) {
|
|
|
707
993
|
const sqlTouch = db.prepare(`UPDATE nodes SET access_count = access_count + 1, last_accessed_at = ?,
|
|
708
994
|
confidence = MIN(1, confidence + ${CONFIDENCE_BUMP}) WHERE id = ?`);
|
|
709
995
|
const sqlLog = db.prepare("INSERT INTO op_log (at, op, target_id, detail) VALUES (?, ?, ?, ?)");
|
|
996
|
+
const sqlHasAudit = db.prepare("SELECT 1 AS x FROM op_log WHERE op = ? AND detail = ? LIMIT 1");
|
|
997
|
+
const sqlListAudit = db.prepare("SELECT detail FROM op_log WHERE op = ? AND detail IS NOT NULL ORDER BY seq");
|
|
998
|
+
const sqlClearAudit = db.prepare("DELETE FROM op_log WHERE op = ? AND detail = ?");
|
|
710
999
|
const sqlOpLogById = db.prepare("SELECT at, op, detail FROM op_log WHERE target_id = ? ORDER BY seq DESC LIMIT ?");
|
|
711
1000
|
const sqlTopActive = db.prepare("SELECT * FROM nodes WHERE scope = ? AND status = 'active' ORDER BY importance DESC, confidence DESC LIMIT ?");
|
|
712
1001
|
const sqlEdgeUpsert = db.prepare("INSERT OR IGNORE INTO edges (from_id, to_id, type, created_at) VALUES (?, ?, ?, ?)");
|
|
@@ -814,6 +1103,17 @@ async function openEngramStore(path) {
|
|
|
814
1103
|
});
|
|
815
1104
|
});
|
|
816
1105
|
}
|
|
1106
|
+
if (rankBoost.recencyWeight !== 0 || rankBoost.proofWeight !== 0) {
|
|
1107
|
+
const now = Date.now();
|
|
1108
|
+
for (const [id, info] of scores) {
|
|
1109
|
+
const row = getRow(id);
|
|
1110
|
+
if (row === void 0) continue;
|
|
1111
|
+
const daysSince = Math.max(0, (now - row.last_accessed_at) / 864e5);
|
|
1112
|
+
const recency = 1 + rankBoost.recencyWeight * Math.max(0, 1 - daysSince / rankBoost.decayAfterDays);
|
|
1113
|
+
const proof = 1 + rankBoost.proofWeight * Math.log2(1 + row.access_count);
|
|
1114
|
+
info.score *= recency * proof;
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
817
1117
|
const topIds = [...scores.entries()].sort((a, b) => b[1].score - a[1].score).slice(0, limit).map(([id]) => id);
|
|
818
1118
|
const viaEdgeOf = /* @__PURE__ */ new Map();
|
|
819
1119
|
if (topIds.length > 0) {
|
|
@@ -1002,6 +1302,15 @@ async function openEngramStore(path) {
|
|
|
1002
1302
|
async audit(op, targetId, detail) {
|
|
1003
1303
|
sqlLog.run(Date.now(), op, targetId, detail);
|
|
1004
1304
|
},
|
|
1305
|
+
async hasAudit(op, detail) {
|
|
1306
|
+
return sqlHasAudit.get(op, detail) !== void 0;
|
|
1307
|
+
},
|
|
1308
|
+
async listAuditDetails(op) {
|
|
1309
|
+
return sqlListAudit.all(op).map((row) => row.detail);
|
|
1310
|
+
},
|
|
1311
|
+
async clearAudit(op, detail) {
|
|
1312
|
+
sqlClearAudit.run(op, detail);
|
|
1313
|
+
},
|
|
1005
1314
|
async purge() {
|
|
1006
1315
|
withTransaction(() => {
|
|
1007
1316
|
sqlPurgeNodes.run();
|
|
@@ -1672,45 +1981,95 @@ function createEngramTools(deps) {
|
|
|
1672
1981
|
const name = "dsh-engram";
|
|
1673
1982
|
/** 必需服务:工具注册表与 LLM 流式端点(摄取/蒸馏的辅助调用)。 */
|
|
1674
1983
|
const inject = ["tools", "llm"];
|
|
1675
|
-
/**
|
|
1676
|
-
|
|
1984
|
+
/**
|
|
1985
|
+
* 会话开始注入的画像渲染:按重要性降序在 token 预算内整行装填(估算 ceil(len/4),
|
|
1986
|
+
* 超预算的行跳过不截断、继续试更短行);装不下的条目降级为索引行(#id + 前 40 字),
|
|
1987
|
+
* 索引行也装不下的折成末尾 `+N more; use engram_search` 计数行。
|
|
1988
|
+
* @param records - 候选条目(调用方已按重要性排序、按条数截断)。
|
|
1989
|
+
* @param tokenBudget - 整段画像的 token 预算(含首尾固定行)。
|
|
1990
|
+
* @returns 注入文本。
|
|
1991
|
+
*/
|
|
1992
|
+
function renderProfile(records, tokenBudget) {
|
|
1993
|
+
const estimate = (text) => Math.ceil(text.length / 4);
|
|
1994
|
+
const header = "User memory profile (dsh-engram, cross-session):";
|
|
1995
|
+
const footer = "Use engram_search to recall details; use engram_save to persist new facts.";
|
|
1996
|
+
let remaining = Math.max(0, tokenBudget - estimate(header) - estimate(footer));
|
|
1997
|
+
const lines = [];
|
|
1998
|
+
const overflow = [];
|
|
1999
|
+
for (const record of records) {
|
|
2000
|
+
const line = `- [${record.kind}] ${record.content}`;
|
|
2001
|
+
const cost = estimate(line);
|
|
2002
|
+
if (cost <= remaining) {
|
|
2003
|
+
lines.push(line);
|
|
2004
|
+
remaining -= cost;
|
|
2005
|
+
} else overflow.push(record);
|
|
2006
|
+
}
|
|
2007
|
+
let more = 0;
|
|
2008
|
+
for (const record of overflow) {
|
|
2009
|
+
const line = `- [${record.kind}] #${record.id} ${record.content.slice(0, 40)}…`;
|
|
2010
|
+
const cost = estimate(line);
|
|
2011
|
+
if (cost <= remaining) {
|
|
2012
|
+
lines.push(line);
|
|
2013
|
+
remaining -= cost;
|
|
2014
|
+
} else more += 1;
|
|
2015
|
+
}
|
|
2016
|
+
if (more > 0) lines.push(`+${more} more; use engram_search`);
|
|
1677
2017
|
return [
|
|
1678
|
-
|
|
1679
|
-
...
|
|
1680
|
-
|
|
2018
|
+
header,
|
|
2019
|
+
...lines,
|
|
2020
|
+
footer
|
|
1681
2021
|
].join("\n");
|
|
1682
2022
|
}
|
|
1683
2023
|
/**
|
|
1684
2024
|
* agent/pre-step waterfall:每轮第一步注入画像;同时 fire-and-forget 触发
|
|
1685
|
-
*
|
|
1686
|
-
*
|
|
2025
|
+
* 上一轮的自动摄取(不阻塞请求);进程内首次第一步重放待补做的末轮摄取。
|
|
2026
|
+
* 必须调用 next() 委托链路;reject 决策原样透传,记忆库为空或非首轮时不追加消息。
|
|
1687
2027
|
*/
|
|
1688
|
-
async function preStep(ctx, openStore, resolved, embedder, { agent, step, turn, signal }, next) {
|
|
2028
|
+
async function preStep(ctx, openStore, resolved, embedder, state, logRequest, { agent, step, turn, signal }, next) {
|
|
1689
2029
|
const decision = await next();
|
|
1690
2030
|
if (decision.kind === "reject") return decision;
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
2031
|
+
const mode = resolved.ingest;
|
|
2032
|
+
if (step === 1 && mode !== "off") {
|
|
2033
|
+
if (!state.pendingReplayed) {
|
|
2034
|
+
state.pendingReplayed = true;
|
|
2035
|
+
replayPendingIngests({
|
|
2036
|
+
openStore: () => openStore("user"),
|
|
2037
|
+
resolveEvents: makeEventResolver(ctx, agent),
|
|
2038
|
+
embedder,
|
|
2039
|
+
mode,
|
|
2040
|
+
routeOverride: resolved.routeOverride,
|
|
2041
|
+
call: (params) => streamText(ctx, {
|
|
2042
|
+
...params,
|
|
2043
|
+
sessionId: agent.session.id
|
|
2044
|
+
}),
|
|
2045
|
+
logRequest,
|
|
2046
|
+
signal
|
|
2047
|
+
}).catch((error) => {
|
|
2048
|
+
console.warn("[dsh-engram] 待补做摄取重放失败(保留 pending,不影响对话):", error);
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
if (turn > 1) ingestPreviousTurn({
|
|
2052
|
+
events: agent.session.events,
|
|
2053
|
+
sessionId: String(agent.id),
|
|
2054
|
+
turn,
|
|
2055
|
+
openStore: () => openStore("user"),
|
|
2056
|
+
embedder,
|
|
2057
|
+
mode,
|
|
2058
|
+
routeOverride: resolved.routeOverride,
|
|
2059
|
+
call: (params) => streamText(ctx, {
|
|
2060
|
+
...params,
|
|
2061
|
+
sessionId: agent.session.id
|
|
2062
|
+
}),
|
|
2063
|
+
logRequest,
|
|
2064
|
+
signal
|
|
2065
|
+
}).catch((error) => {
|
|
2066
|
+
console.warn("[dsh-engram] 本轮自动摄取失败(已跳过,不影响对话):", error);
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
1710
2069
|
if (step !== 1) return decision;
|
|
1711
2070
|
const top = await (await openStore("user")).topActive("user", resolved.profileTopN);
|
|
1712
2071
|
if (top.length === 0) return decision;
|
|
1713
|
-
const text = renderProfile(top);
|
|
2072
|
+
const text = renderProfile(top, resolved.injectTokenBudget);
|
|
1714
2073
|
return {
|
|
1715
2074
|
...decision,
|
|
1716
2075
|
messages: [...decision.messages, createUserMessage({
|
|
@@ -1731,22 +2090,47 @@ async function preStep(ctx, openStore, resolved, embedder, { agent, step, turn,
|
|
|
1731
2090
|
};
|
|
1732
2091
|
}
|
|
1733
2092
|
/**
|
|
2093
|
+
* pending 重放的事件源解析器:pending 属于当前会话时直接用其事件快照;
|
|
2094
|
+
* 其余会话经可选的 sessionPersistence 服务读持久化日志(服务缺席或读取失败
|
|
2095
|
+
* 返回 undefined,pending 保留到下次,不报错)。
|
|
2096
|
+
*/
|
|
2097
|
+
function makeEventResolver(ctx, agent) {
|
|
2098
|
+
return async (sessionId) => {
|
|
2099
|
+
if (sessionId === String(agent.id)) return agent.session.events;
|
|
2100
|
+
const persistence = ctx.get("sessionPersistence");
|
|
2101
|
+
if (persistence === void 0) return void 0;
|
|
2102
|
+
try {
|
|
2103
|
+
return (await persistence.load(sessionId)).events;
|
|
2104
|
+
} catch {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
1734
2110
|
* 插件体:预热分库与嵌入器,注册 9 个工具、画像注入、自动摄取与衰减调度。
|
|
1735
2111
|
* @param ctx - host 上下文。
|
|
1736
2112
|
* @param config - cordis.yml 传入的可选配置;非法值在加载时 loud 失败。
|
|
1737
2113
|
*/
|
|
1738
2114
|
function apply(ctx, config = {}) {
|
|
1739
2115
|
const resolved = resolveConfig(config);
|
|
1740
|
-
|
|
2116
|
+
mkdirSync(resolved.dbDir, {
|
|
1741
2117
|
recursive: true,
|
|
1742
2118
|
mode: 448
|
|
1743
2119
|
});
|
|
1744
|
-
const
|
|
2120
|
+
const identity = resolveProjectIdentity(process.cwd());
|
|
2121
|
+
const migration = migrateProjectDb(resolved.dbDir, identity);
|
|
2122
|
+
if (migration === "renamed") console.warn(`[dsh-engram] 项目记忆库已从 cwd 命名迁移到 git origin 标识:${identity.dbName}`);
|
|
2123
|
+
else if (migration === "kept-both") console.warn(`[dsh-engram] 检测到新旧两个项目记忆库并存,未合并(保留新库 ${identity.dbName};旧库 ${identity.legacyDbName} 请人工处理后删除)`);
|
|
2124
|
+
const rankBoost = {
|
|
2125
|
+
recencyWeight: resolved.rankRecencyWeight,
|
|
2126
|
+
proofWeight: resolved.rankProofWeight,
|
|
2127
|
+
decayAfterDays: resolved.decayAfterDays
|
|
2128
|
+
};
|
|
1745
2129
|
const stores = /* @__PURE__ */ new Map();
|
|
1746
2130
|
const openStore = (scope) => {
|
|
1747
2131
|
const existing = stores.get(scope);
|
|
1748
2132
|
if (existing !== void 0) return existing;
|
|
1749
|
-
const created = openEngramStore(scope === "user" ?
|
|
2133
|
+
const created = openEngramStore(scope === "user" ? join(resolved.dbDir, "user.db") : join(resolved.dbDir, identity.dbName), rankBoost);
|
|
1750
2134
|
stores.set(scope, created);
|
|
1751
2135
|
return created;
|
|
1752
2136
|
};
|
|
@@ -1769,7 +2153,33 @@ function apply(ctx, config = {}) {
|
|
|
1769
2153
|
exportDir: `${resolved.dbDir}/exports`
|
|
1770
2154
|
});
|
|
1771
2155
|
});
|
|
1772
|
-
|
|
2156
|
+
const logIngestRequest = (data) => {
|
|
2157
|
+
openStore("user").then((store) => store.audit("ingest-request", "AUX", JSON.stringify(data))).catch(() => {});
|
|
2158
|
+
};
|
|
2159
|
+
if (resolved.injectProfile || resolved.ingest !== "off") {
|
|
2160
|
+
const state = { pendingReplayed: false };
|
|
2161
|
+
ctx.on("agent/pre-step", (payload, next) => preStep(ctx, openStore, resolved, embedder, state, logIngestRequest, payload, next), { prepend: true });
|
|
2162
|
+
}
|
|
2163
|
+
if (resolved.ingest !== "off") ctx.on("session/disposed", (session) => {
|
|
2164
|
+
const mode = resolved.ingest;
|
|
2165
|
+
if (mode === "off") return;
|
|
2166
|
+
ingestFinalTurn({
|
|
2167
|
+
events: session.events,
|
|
2168
|
+
sessionId: String(session.id),
|
|
2169
|
+
turn: 0,
|
|
2170
|
+
slice: "last",
|
|
2171
|
+
openStore: () => openStore("user"),
|
|
2172
|
+
embedder,
|
|
2173
|
+
mode,
|
|
2174
|
+
routeOverride: resolved.routeOverride,
|
|
2175
|
+
call: (params) => streamText(ctx, {
|
|
2176
|
+
...params,
|
|
2177
|
+
sessionId: session.id
|
|
2178
|
+
}),
|
|
2179
|
+
logRequest: logIngestRequest,
|
|
2180
|
+
signal: AbortSignal.timeout(FINAL_INGEST_TIMEOUT_MS)
|
|
2181
|
+
});
|
|
2182
|
+
});
|
|
1773
2183
|
const runDecay = async () => {
|
|
1774
2184
|
for (const scope of ["user", "project"]) {
|
|
1775
2185
|
const archived = await (await openStore(scope)).decay({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kenz1117/dsh-engram",
|
|
3
3
|
"description": "Cross-session long-term memory for DeepSeek Harness: dual-scope SQLite memory graph, hybrid retrieval, provenance audit, and a knowledge flywheel.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -37,14 +37,6 @@
|
|
|
37
37
|
"platform": "web"
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
|
-
"scripts": {
|
|
41
|
-
"prepare": "node scripts/link-peers.mjs",
|
|
42
|
-
"bundle": "tsdown",
|
|
43
|
-
"watch": "tsdown --watch",
|
|
44
|
-
"test": "vitest run",
|
|
45
|
-
"typecheck": "tsc --noEmit",
|
|
46
|
-
"backfill": "node scripts/backfill-embeddings.mjs"
|
|
47
|
-
},
|
|
48
40
|
"license": "MIT",
|
|
49
41
|
"keywords": [
|
|
50
42
|
"dsh-plugin",
|
|
@@ -78,11 +70,11 @@
|
|
|
78
70
|
"typescript": "^5.9.3",
|
|
79
71
|
"vitest": "^4.1.11"
|
|
80
72
|
},
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
"scripts": {
|
|
74
|
+
"bundle": "tsdown",
|
|
75
|
+
"watch": "tsdown --watch",
|
|
76
|
+
"test": "vitest run",
|
|
77
|
+
"typecheck": "tsc --noEmit",
|
|
78
|
+
"backfill": "node scripts/backfill-embeddings.mjs"
|
|
87
79
|
}
|
|
88
|
-
}
|
|
80
|
+
}
|