dsh-layered-memory 0.5.3 → 0.6.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 +211 -0
- package/README.md +13 -8
- package/dist/client.js +247 -70
- package/dist/config.d.ts +6 -0
- package/dist/config.js +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/llm.js +5 -1
- package/dist/pipeline/runner.js +16 -10
- package/dist/settings.d.ts +4 -0
- package/dist/settings.js +10 -3
- package/dist/stats.js +19 -4
- package/package.json +1 -1
package/README.en.md
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
[简体中文](README.md) | **English**
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./assets/readme/hero.svg" width="100%"
|
|
5
|
+
alt="dsh-layered-memory: conversations are automatically distilled into layered memories and injected before every model step (L0 raw conversation → L1 atomic memories → L2 scene blocks → L3 core persona)">
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
# dsh-layered-memory
|
|
9
|
+
|
|
10
|
+
A **layered distillation memory plugin** for DeepSeek Harness (persistent composition
|
|
11
|
+
plugin): conversations are processed in the background through L0 capture → L1 atomic
|
|
12
|
+
memories → L2 scene consolidation → L3 persona distillation, and relevant memories are
|
|
13
|
+
automatically injected into context before every model step — neither the user nor the
|
|
14
|
+
model needs to do anything. Ported from the pipeline design of
|
|
15
|
+
[MemoryCore](https://github.com/TencentDB-Agent-Memory) (TencentDB Agent Memory):
|
|
16
|
+
prompts are kept as-is; only the L2/L3 "LLM manipulates files" flow is adapted to
|
|
17
|
+
"LLM outputs, engineering side executes".
|
|
18
|
+
|
|
19
|
+
## Core Capabilities
|
|
20
|
+
|
|
21
|
+
### Layered Memory (L0–L3)
|
|
22
|
+
|
|
23
|
+
| Layer | Content | Storage |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| L0 | Per-turn user/assistant messages (cleaned, code blocks and injected tags stripped) | `conversations/YYYY-MM-DD.jsonl` + SQLite |
|
|
26
|
+
| L1 | Scene segmentation + memory extraction (chat: persona/episodic/instruction; work: work_fact/work_task/work_method/work_artifact) + conflict-detection dedup & merge, each record family-tagged | `records/YYYY-MM-DD.jsonl` + SQLite (FTS5 + optional vectors, family column) |
|
|
27
|
+
| L2 | New memories consolidated into Markdown scene documents (META blocks, heat management, merge caps), consolidated per family | `scenes/chat/*.md`, `scenes/work/*.md` |
|
|
28
|
+
| L3 | Persona distilled from changed scenes (chat: user persona ≤2000 chars; work: Team Operating Doctrine ≤1200 chars), one per family | `persona-chat.md`, `persona-work.md` |
|
|
29
|
+
|
|
30
|
+
Pipeline principles: all distillation calls reuse DSH's own `ctx.llm`; any stage
|
|
31
|
+
failure is logged only and never blocks the agent loop; recall injection is wrapped
|
|
32
|
+
in tags that are automatically stripped on the capture side, preventing feedback loops.
|
|
33
|
+
|
|
34
|
+
### Per-Session Memory Modes
|
|
35
|
+
|
|
36
|
+
Each session can independently choose a memory mode — **write and recall share the
|
|
37
|
+
same mode**:
|
|
38
|
+
|
|
39
|
+
| Mode | Distillation (write) | Recall (injection) |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `Auto` (default) | Single extraction with a merged-vocabulary prompt, all personal 3 + work 4 types enabled, family tag assigned by type prefix | Both families recalled; persona/scene navigation grouped by category and injected with structured `<domain>` blocks |
|
|
42
|
+
| `chat` | Narrow prompt with personal 3 types only, chat family only | chat memories only + chat persona/scene navigation |
|
|
43
|
+
| `work` | Narrow prompt with work 4 types only, work family only | work memories only + work persona/scene navigation |
|
|
44
|
+
| `Off` | No L0 writes, no distillation | No recall; the three memory tools return a "cloaked" notice |
|
|
45
|
+
|
|
46
|
+
- **Control**: the pill next to the mode selector in the input bar (`Memory · Auto`);
|
|
47
|
+
clicking opens a macOS-style sliding picker above — release to snap to the nearest
|
|
48
|
+
mode;
|
|
49
|
+
- **Default mode** = config `family` (`auto|chat|work`, default `auto`); each session's
|
|
50
|
+
choice is persisted by sessionId to `session-modes.json`, surviving restarts/session
|
|
51
|
+
restore; switching mid-session takes effect next turn, already-extracted memories
|
|
52
|
+
stay in their original family;
|
|
53
|
+
- Stacks with the global switches (global is the master gate); L2/L3 are fully
|
|
54
|
+
family-isolated — content never leaks across families.
|
|
55
|
+
|
|
56
|
+
### Memory Browser (Settings → Memory)
|
|
57
|
+
|
|
58
|
+
Multi-tab page with a mixed view of both families: **Overview** (per-layer counts +
|
|
59
|
+
memory mode switch panel, auto-refresh every 5s), **Memories** (L1 card list with
|
|
60
|
+
keyword/type/scene filters), **Scenes** (L2 full text), **Persona** (L3 full text),
|
|
61
|
+
**Log** (last 200 lines of `memory.log`). Switches go through the official settings
|
|
62
|
+
service (namespace `dsh-memory`, effective immediately, persisted across restarts);
|
|
63
|
+
effective rule = **static config (deployment ceiling) AND runtime switch**; data
|
|
64
|
+
channel is loopback RPC (`dsh-memory/*`).
|
|
65
|
+
|
|
66
|
+
## Getting Started
|
|
67
|
+
|
|
68
|
+
Requires Node ≥ 22.16. Two invocation styles — the `npx` prefix can replace `dsh` in
|
|
69
|
+
any command below:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Option 1: run the official CLI directly via npx (no pre-installed dsh; version can be pinned, e.g. dsh-layered-memory@0.5.4)
|
|
73
|
+
npx -y @deepseek-ai/dsh plugin --profile web add dsh-layered-memory
|
|
74
|
+
|
|
75
|
+
# Option 2: with the dsh CLI installed (dsh is a pnpm forwarder; npm i -g pnpm first if missing)
|
|
76
|
+
dsh plugin --profile web add dsh-layered-memory
|
|
77
|
+
|
|
78
|
+
# Alternative sources: GitHub repo / local path (dev & debugging, link: points at the repo; npm run build + restart dsh to apply)
|
|
79
|
+
dsh plugin --profile web add https://github.com/JunNanLYS/dsh-layered-memory
|
|
80
|
+
dsh plugin --profile web add /path/to/dsh-layered-memory
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This package declares a `dsh.bundle` composition layer (`cordis.patch.yml`); after
|
|
84
|
+
installation the **plugin entry is mounted automatically** — no need to hand-edit
|
|
85
|
+
`$DSH_HOME/profiles/web/cordis.patch.yml`. Then restart DeepSeek Harness and verify:
|
|
86
|
+
the appearance of `conversations/ records/ scenes/` and `memory.db` under
|
|
87
|
+
`~/.dsh/memory/` means the plugin applied successfully; the "Memory" page in settings
|
|
88
|
+
and the mode pill in the input bar mean the client half is ready.
|
|
89
|
+
|
|
90
|
+
> ⚠️ **Security note**: installing a plugin = running third-party code with your
|
|
91
|
+
> privileges. This plugin reads session content, writes files in its data directory,
|
|
92
|
+
> and calls the LLM/embedding services you configured; if that concerns you, review
|
|
93
|
+
> the source first (`src/`).
|
|
94
|
+
|
|
95
|
+
**Uninstall**: `dsh plugin --profile web remove dsh-layered-memory` + restart. Data
|
|
96
|
+
stays in `~/.dsh/memory/`; delete the whole directory manually if you don't need it.
|
|
97
|
+
|
|
98
|
+
### Development from Source
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/JunNanLYS/dsh-layered-memory
|
|
102
|
+
cd dsh-layered-memory
|
|
103
|
+
npm install && npm run build
|
|
104
|
+
dsh plugin --profile web add . # link: install; after code changes, npm run build + restart dsh
|
|
105
|
+
npm run smoke # smoke test (rebuild first: see command below)
|
|
106
|
+
npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution nodenext --target es2022 --strict --skipLibCheck --esModuleInterop
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
Override configs go into the profile's own `cordis.patch.yml` as a **top-level bare
|
|
112
|
+
patch entry** (direct `id:`, not wrapped in `insert:` — an insert with the same id as
|
|
113
|
+
the bundle layer appends and causes `duplicate loader entry id` startup failure):
|
|
114
|
+
|
|
115
|
+
```yaml
|
|
116
|
+
- id: dsh-memory
|
|
117
|
+
name: dsh-layered-memory
|
|
118
|
+
config: # keys replace whole lines (no deep merge); write out all keys you want to keep
|
|
119
|
+
family: auto # default mode for new sessions: auto | chat | work
|
|
120
|
+
llm: # distillation model route (falls back to the current default model if empty)
|
|
121
|
+
provider: ''
|
|
122
|
+
model: ''
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Field | Default | Description |
|
|
126
|
+
| --- | --- | --- |
|
|
127
|
+
| `family` | `auto` | Default memory mode for new sessions: `auto` (both families) \| `chat` (personal) \| `work` (work); switchable per session via the input-bar control |
|
|
128
|
+
| `dataDir` | `$DSH_HOME/memory` | Data directory |
|
|
129
|
+
| `capture.enabled` | `true` | L0 capture |
|
|
130
|
+
| `capture.stripCodeBlocks` | `true` | Strip code blocks from assistant messages |
|
|
131
|
+
| `capture.maxMessageChars` | `4000` | Max characters per message |
|
|
132
|
+
| `extract.enabled` | `true` | L1 extraction |
|
|
133
|
+
| `extract.minMessages` | `1` | Run L1 extraction after N new messages accumulate |
|
|
134
|
+
| `extract.backgroundMessages` | `10` | Background messages attached to extraction |
|
|
135
|
+
| `extract.candidatePool` | `5` | Dedup candidate pool size |
|
|
136
|
+
| `l2.enabled` | `true` | L2 scene consolidation |
|
|
137
|
+
| `l2.minNewMemories` | `5` | New-memory threshold since last L2 consolidation |
|
|
138
|
+
| `l2.maxScenes` | `12` | Scene block count cap |
|
|
139
|
+
| `l2.sceneContextLimit` | `3` | Max similar-scene full texts attached to the L2 prompt |
|
|
140
|
+
| `l3.enabled` | `true` | L3 persona distillation |
|
|
141
|
+
| `l3.interval` | `20` | L3 distillation interval (new-memory count) |
|
|
142
|
+
| `recall.enabled` | `true` | Auto recall |
|
|
143
|
+
| `recall.maxResults` | `5` | L1 records injected per step |
|
|
144
|
+
| `recall.strategy` | `hybrid` | Retrieval strategy: `keyword` / `embedding` / `hybrid` |
|
|
145
|
+
| `recall.scoreThreshold` | `0.3` | Recall score threshold (below is not injected; applies to keyword/embedding only, not pre-fusion hybrid; tool path unfiltered) |
|
|
146
|
+
| `embedding.enabled` | `false` | Vector retrieval switch; off = pure FTS |
|
|
147
|
+
| `embedding.baseUrl` | empty | OpenAI-compatible /embeddings endpoint (e.g. `https://api.siliconflow.cn/v1`) |
|
|
148
|
+
| `embedding.apiKey` | empty | API key |
|
|
149
|
+
| `embedding.model` | empty | embedding model name |
|
|
150
|
+
| `embedding.dimensions` | `0` | Vector dimensions (required when enabled; must match model output) |
|
|
151
|
+
| `llm.provider/model` | empty | Distillation model override (defaults to current selection) |
|
|
152
|
+
| `llm.maxTokens` | `20000` | Output token cap per distillation call (unified across stages; a reasoning model's reasoning shares this budget — too low gets fully consumed by thinking, leaving 0 chars of text) |
|
|
153
|
+
| `llm.temperature` | `0.3` | Distillation temperature |
|
|
154
|
+
| `llm.maxInputChars` | `700000` | Input character budget per distillation call (over-budget L1 inputs are chunked automatically) |
|
|
155
|
+
| `tools` | `true` | Whether to register model-callable memory tools |
|
|
156
|
+
|
|
157
|
+
## Storage Layout
|
|
158
|
+
|
|
159
|
+
Aligned with MemoryCore's official dual-write architecture: append-only JSONL files
|
|
160
|
+
(`conversations/`, `records/` sharded by day) are the source of truth for
|
|
161
|
+
backup/restore and are **never rewritten**; `memory.db` (node:sqlite + WAL + FTS5
|
|
162
|
+
BM25 + sqlite-vec cosine vectors) is the primary retrieval engine — updates/deletes
|
|
163
|
+
from dedup merges touch the retrieval DB only.
|
|
164
|
+
|
|
165
|
+
- **Three retrieval strategies** (`recall.strategy`): `keyword` (FTS5 BM25) /
|
|
166
|
+
`embedding` (vec0 cosine KNN) / `hybrid` (both lanes in parallel + RRF k=60
|
|
167
|
+
fusion, default); `conversation_search` (L0) uses the same fusion;
|
|
168
|
+
- **Vectors optional**: off by default (pure FTS). DSH's `ctx.llm` has no embeddings
|
|
169
|
+
endpoint; enabling requires any OpenAI-compatible `/embeddings` service (configure
|
|
170
|
+
`embedding.*`); config changes automatically drop the vector table and re-embed
|
|
171
|
+
everything in the background;
|
|
172
|
+
- **Degradation chain**: sqlite-vec load failure → pure FTS; embedding call failure
|
|
173
|
+
→ degrade to FTS for that call with a one-time warning; retrieval DB init failure
|
|
174
|
+
→ memory features disabled entirely but dsh itself starts normally;
|
|
175
|
+
- **Replacement seam**: `L1Store.search()` is the single retrieval entry point.
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
memory/
|
|
179
|
+
├── memory.db # SQLite retrieval DB (L0/L1 metadata + FTS5 + optional vectors; L1 has a family column)
|
|
180
|
+
├── conversations/2026-01-01.jsonl # L0 raw conversation source of truth (one file per day, append-only; not family-split)
|
|
181
|
+
├── records/2026-01-01.jsonl # L1 atomic memory source of truth (one file per day, append-only; family field included)
|
|
182
|
+
├── scenes/chat/*.md # L2 scene blocks (chat family)
|
|
183
|
+
├── scenes/work/*.md # L2 scene blocks (work family)
|
|
184
|
+
├── persona-chat.md # L3 persona (chat family: user persona)
|
|
185
|
+
├── persona-work.md # L3 persona (work family: Team Operating Doctrine)
|
|
186
|
+
├── state.json # pipeline checkpoint (v2 family-split: families.chat / families.work)
|
|
187
|
+
├── session-modes.json # session mode map (sessionId → mode, auto-pruned after >90 days)
|
|
188
|
+
└── memory.log # diagnostic log (info+, rotates to memory.log.1 beyond 2MB)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Logging & Troubleshooting
|
|
192
|
+
|
|
193
|
+
The dsh host prints plugin logs to the console; the plugin mirrors info and above to
|
|
194
|
+
`memory.log` in its data directory. The typical log path of one conversation turn:
|
|
195
|
+
`L0 捕获 turn=N …条` → `L0 落盘 N 条` → `蒸馏管线开始(…待重试 M 条)` →
|
|
196
|
+
`LLM 调用 provider/model:输入 x → 输出 y 字符(z s)` → `L1 抽取完成(…新增 Y 条)` →
|
|
197
|
+
`蒸馏管线结束`; the next turn shows `召回命中 N 条 L1`. Empty LLM output carries full
|
|
198
|
+
diagnostics (finish reason / token counts / reasoning excerpt); JSON parse failures
|
|
199
|
+
include the first 400 characters of the raw model output; all failure warns carry the
|
|
200
|
+
first stack frame.
|
|
201
|
+
|
|
202
|
+
## Differences from MemoryCore
|
|
203
|
+
|
|
204
|
+
- The full pipeline is embedded (no external Gateway); distillation reuses DSH's own LLM;
|
|
205
|
+
- L2/L3 changed from "LLM manipulates file tools" to "LLM outputs operation JSON / full documents, engineering side executes";
|
|
206
|
+
- Recall injection happens at `agent/pre-step` + agent-scoped `systemPrompt.context` (DSH-native events/services);
|
|
207
|
+
- Storage/retrieval is a single-machine slimmed version of the official sqlite backend (drops multi-tenant isolation columns, TCVDB cloud backend, audit tables; tokenization uses a bundled CJK bigram instead of jieba, keeping zero native dependencies — sqlite-vec is the only native extension, auto-degrading on load failure).
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
**简体中文** | [English](README.en.md)
|
|
2
|
+
|
|
1
3
|
<p align="center">
|
|
2
4
|
<img src="./assets/readme/hero.svg" width="100%"
|
|
3
5
|
alt="dsh-layered-memory:对话自动分层蒸馏成记忆,模型每步前自动召回注入(L0 原始对话 → L1 原子记忆 → L2 场景块 → L3 核心画像)">
|
|
@@ -44,24 +46,26 @@ Agent 循环;召回注入用标签包裹、捕获侧自动剥离,防止反
|
|
|
44
46
|
|
|
45
47
|
### 记忆浏览器(设置页 → 记忆)
|
|
46
48
|
|
|
47
|
-
多 Tab 页面,两族混合视图:**概览**(各层计数 +
|
|
49
|
+
多 Tab 页面,两族混合视图:**概览**(各层计数 + 记忆模式开关面板 + 蒸馏思考档位选择器,5 秒自动刷新)、
|
|
48
50
|
**记忆**(L1 卡片列表,关键词/类型/情境筛选)、**场景**(L2 全文)、**画像**(L3 全文)、
|
|
49
|
-
**日志**(`memory.log` 尾部 200
|
|
50
|
-
|
|
51
|
+
**日志**(`memory.log` 尾部 200 行)。开关与思考档位走官方 settings 服务(命名空间 `dsh-memory`,
|
|
52
|
+
实时生效、重启保留);开关生效规则 = **静态 config(部署上限)AND 运行时开关**;
|
|
53
|
+
思考档位为运行时覆盖(选择器选"跟随配置"则用部署配置 `llm.reasoningEffort` 作默认),
|
|
51
54
|
数据通道为 loopback RPC(`dsh-memory/*`)。
|
|
52
55
|
|
|
53
56
|
## 快速开始
|
|
54
57
|
|
|
55
|
-
需要 Node ≥ 22.16
|
|
58
|
+
需要 Node ≥ 22.16。两种调用方式任选(`npx` 前缀可替换下面任何 `dsh` 命令):
|
|
56
59
|
|
|
57
60
|
```bash
|
|
58
|
-
#
|
|
61
|
+
# 方式一:npx 直接跑官方 CLI(无需预装 dsh;可 pin 版本,如 dsh-layered-memory@0.5.4)
|
|
62
|
+
npx -y @deepseek-ai/dsh plugin --profile web add dsh-layered-memory
|
|
63
|
+
|
|
64
|
+
# 方式二:已装 dsh CLI(dsh 是 pnpm 转发器,未装 pnpm 时先 npm i -g pnpm)
|
|
59
65
|
dsh plugin --profile web add dsh-layered-memory
|
|
60
66
|
|
|
61
|
-
#
|
|
67
|
+
# 包源备选:GitHub 仓库 / 本地路径(开发调试,link: 指向仓库,npm run build + 重启 dsh 即生效)
|
|
62
68
|
dsh plugin --profile web add https://github.com/JunNanLYS/dsh-layered-memory
|
|
63
|
-
|
|
64
|
-
# 或从本地路径安装(开发/调试,link: 指向仓库,npm run build + 重启 dsh 即生效)
|
|
65
69
|
dsh plugin --profile web add /path/to/dsh-layered-memory
|
|
66
70
|
```
|
|
67
71
|
|
|
@@ -131,6 +135,7 @@ npx tsc src/smoke.ts --outDir dist-smoke --module nodenext --moduleResolution no
|
|
|
131
135
|
| `embedding.dimensions` | `0` | 向量维度(启用时必填,须与模型输出一致) |
|
|
132
136
|
| `llm.provider/model` | 空 | 蒸馏模型覆盖(默认用当前默认选择) |
|
|
133
137
|
| `llm.maxTokens` | `20000` | 单次蒸馏输出 token 上限(全阶段统一;推理模型的 reasoning 与正文共享该预算,过低会被思考吃光导致正文 0 字符) |
|
|
138
|
+
| `llm.reasoningEffort` | `off` | 蒸馏思考档位(部署默认):`off` / `high` / `max`,空串不传(跟随模型默认)。蒸馏是结构化抽取任务,默认关思考——推理模型(如 v4-flash)默认 high 档的思考可把任意输出预算全部吃光导致正文 0 字符;非推理模型不认识 effort 时需设为空串。运行时可在设置页 → 记忆 → 概览临时切换(选"跟随配置"即回退本值) |
|
|
134
139
|
| `llm.temperature` | `0.3` | 蒸馏温度 |
|
|
135
140
|
| `llm.maxInputChars` | `700000` | 单次蒸馏输入字符预算(超限的 L1 输入自动分块抽取) |
|
|
136
141
|
| `tools` | `true` | 是否注册模型可调用的记忆工具 |
|
package/dist/client.js
CHANGED
|
@@ -160,6 +160,31 @@ window.__ModuleLoader__.load({
|
|
|
160
160
|
padding: "4px 12px",
|
|
161
161
|
marginBottom: 14,
|
|
162
162
|
},
|
|
163
|
+
seg: {
|
|
164
|
+
display: "inline-flex",
|
|
165
|
+
border: "1px solid var(--dsw-alias-border-secondary, #e5e5e5)",
|
|
166
|
+
borderRadius: 8,
|
|
167
|
+
overflow: "hidden",
|
|
168
|
+
flexShrink: 0,
|
|
169
|
+
},
|
|
170
|
+
segBtn: {
|
|
171
|
+
padding: "4px 12px",
|
|
172
|
+
fontSize: 12,
|
|
173
|
+
lineHeight: "16px",
|
|
174
|
+
cursor: "pointer",
|
|
175
|
+
background: "transparent",
|
|
176
|
+
color: "inherit",
|
|
177
|
+
border: "none",
|
|
178
|
+
borderRight: "1px solid var(--dsw-alias-border-secondary, #e5e5e5)",
|
|
179
|
+
},
|
|
180
|
+
segBtnOn: {
|
|
181
|
+
background: "var(--dsw-alias-interactive-bg-primary, #1a7f37)",
|
|
182
|
+
color: "#fff",
|
|
183
|
+
fontWeight: 600,
|
|
184
|
+
},
|
|
185
|
+
segBtnOff: {
|
|
186
|
+
background: "var(--dsw-alias-bg-secondary, #f6f8fa)",
|
|
187
|
+
},
|
|
163
188
|
flexRow: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
|
|
164
189
|
grow: { flex: 1 },
|
|
165
190
|
sceneHead: { display: "flex", alignItems: "baseline", gap: 10, marginBottom: 6, flexWrap: "wrap" },
|
|
@@ -228,6 +253,36 @@ window.__ModuleLoader__.load({
|
|
|
228
253
|
);
|
|
229
254
|
}
|
|
230
255
|
|
|
256
|
+
// ── Segmented 组件:分段选择器(记忆设置页的蒸馏思考档位等单选项) ──
|
|
257
|
+
function Segmented(props) {
|
|
258
|
+
var value = props.value;
|
|
259
|
+
var disabled = !!props.disabled;
|
|
260
|
+
return react.createElement(
|
|
261
|
+
"div",
|
|
262
|
+
{ style: Object.assign({}, S.seg, disabled ? S.switchDisabled : null) },
|
|
263
|
+
props.options.map(function (opt, i) {
|
|
264
|
+
var on = opt.key === value;
|
|
265
|
+
return react.createElement(
|
|
266
|
+
"span",
|
|
267
|
+
{
|
|
268
|
+
key: opt.key,
|
|
269
|
+
style: Object.assign(
|
|
270
|
+
{},
|
|
271
|
+
S.segBtn,
|
|
272
|
+
on ? S.segBtnOn : S.segBtnOff,
|
|
273
|
+
i === props.options.length - 1 ? { borderRight: "none" } : null,
|
|
274
|
+
disabled ? { cursor: "not-allowed" } : null,
|
|
275
|
+
),
|
|
276
|
+
onClick: function () {
|
|
277
|
+
if (!disabled && !on && props.onChange) props.onChange(opt.key);
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
opt.label,
|
|
281
|
+
);
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
231
286
|
// ── 会话记忆档位控件(输入栏 pill + macOS 风格滑动选择器) ──
|
|
232
287
|
// 档位顺序即滑轨顺序:关闭 → chat → work → 自动(默认档"自动"居右)
|
|
233
288
|
var MODES = [
|
|
@@ -257,40 +312,54 @@ window.__ModuleLoader__.load({
|
|
|
257
312
|
return 3;
|
|
258
313
|
}
|
|
259
314
|
|
|
260
|
-
/** 滑动选择器浮层(参考 macOS 滑动器:拖拽圆头
|
|
315
|
+
/** 滑动选择器浮层(参考 macOS 滑动器:拖拽圆头 1:1 连续跟手,松手按动量投影吸附最近档)。 */
|
|
261
316
|
function ModeSlider(props) {
|
|
317
|
+
ensureFlowStyle(); // 玻璃材质 class 与流光共用同一张注入样式表
|
|
262
318
|
var trackRef = react.useRef(null);
|
|
319
|
+
// 拖拽状态:{ x: 圆头连续位置 px, lastX: 上次指针 clientX, t: 时间戳, v: 速度 px/ms(EMA 平滑) }
|
|
263
320
|
var dragState = react.useState(null);
|
|
264
|
-
var
|
|
265
|
-
var
|
|
266
|
-
|
|
267
|
-
var activeIdx = dragIdx === null ? modeIndex(props.mode) : dragIdx;
|
|
321
|
+
var drag = dragState[0];
|
|
322
|
+
var setDrag = dragState[1];
|
|
268
323
|
|
|
269
|
-
var
|
|
324
|
+
var clampX = function (x) {
|
|
325
|
+
if (x < 0) return 0;
|
|
326
|
+
if (x > INNER_W) return INNER_W;
|
|
327
|
+
return x;
|
|
328
|
+
};
|
|
329
|
+
var xFromClientX = function (clientX) {
|
|
270
330
|
var rect = trackRef.current.getBoundingClientRect();
|
|
271
|
-
|
|
272
|
-
if (x < 0) x = 0;
|
|
273
|
-
if (x > INNER_W) x = INNER_W;
|
|
274
|
-
return Math.round((x / INNER_W) * (MODES.length - 1));
|
|
331
|
+
return clampX(clientX - rect.left - THUMB / 2);
|
|
275
332
|
};
|
|
276
333
|
|
|
277
334
|
var onPointerDown = function (e) {
|
|
278
335
|
e.preventDefault();
|
|
279
336
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
280
|
-
|
|
337
|
+
setDrag({ x: xFromClientX(e.clientX), lastX: e.clientX, t: e.timeStamp, v: 0 });
|
|
281
338
|
};
|
|
282
339
|
var onPointerMove = function (e) {
|
|
283
|
-
if (
|
|
284
|
-
|
|
340
|
+
if (drag === null) return;
|
|
341
|
+
var dt = e.timeStamp - drag.t;
|
|
342
|
+
var instV = dt > 0 ? (e.clientX - drag.lastX) / dt : drag.v;
|
|
343
|
+
setDrag({
|
|
344
|
+
x: xFromClientX(e.clientX),
|
|
345
|
+
lastX: e.clientX,
|
|
346
|
+
t: e.timeStamp,
|
|
347
|
+
v: drag.v * 0.7 + instV * 0.3, // EMA:瞬时抖动不放大,松手投影用
|
|
348
|
+
});
|
|
285
349
|
};
|
|
286
350
|
var onPointerUp = function (e) {
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
351
|
+
if (drag === null) return;
|
|
352
|
+
// 动量投影(Designing Fluid Interfaces):按松手速度前瞻落点就近吸附;
|
|
353
|
+
// 投影量 clamp 到半档(±30px)——甩动最多把边界推到相邻档,绝不会跳两档
|
|
354
|
+
var projected = xFromClientX(e.clientX) + Math.max(-30, Math.min(30, drag.v * 120));
|
|
355
|
+
var idx = Math.round((clampX(projected) / INNER_W) * (MODES.length - 1));
|
|
356
|
+
setDrag(null);
|
|
290
357
|
props.onCommit(MODES[idx].key);
|
|
291
358
|
};
|
|
292
359
|
|
|
293
|
-
|
|
360
|
+
// 拖拽中圆头 1:1 跟指针(连续位置,不吸附);静止时停在档位中心
|
|
361
|
+
var thumbLeft = drag !== null ? drag.x : (modeIndex(props.mode) / (MODES.length - 1)) * INNER_W;
|
|
362
|
+
var activeIdx = Math.min(MODES.length - 1, Math.max(0, Math.round((thumbLeft / INNER_W) * (MODES.length - 1))));
|
|
294
363
|
var info = MODES[activeIdx];
|
|
295
364
|
|
|
296
365
|
var stops = [];
|
|
@@ -310,7 +379,8 @@ window.__ModuleLoader__.load({
|
|
|
310
379
|
width: 6,
|
|
311
380
|
height: 6,
|
|
312
381
|
borderRadius: "50%",
|
|
313
|
-
background: active ? MODES[i].color : "
|
|
382
|
+
background: active ? MODES[i].color : "rgba(128,140,150,0.55)",
|
|
383
|
+
zIndex: 2,
|
|
314
384
|
},
|
|
315
385
|
},
|
|
316
386
|
),
|
|
@@ -331,9 +401,10 @@ window.__ModuleLoader__.load({
|
|
|
331
401
|
cursor: "pointer",
|
|
332
402
|
whiteSpace: "nowrap",
|
|
333
403
|
fontWeight: active ? 600 : 400,
|
|
334
|
-
|
|
404
|
+
// 未激活标签走主题 caption 令牌(浅色 #400 灰蓝 / 暗色 #600),玻璃面上保持可读
|
|
405
|
+
color: active ? MODES[i].color : "var(--dsw-alias-label-caption, #888)",
|
|
335
406
|
},
|
|
336
|
-
onClick: function () { if (
|
|
407
|
+
onClick: function () { if (drag === null) props.onCommit(MODES[i].key); },
|
|
337
408
|
},
|
|
338
409
|
MODES[i].label,
|
|
339
410
|
),
|
|
@@ -344,6 +415,8 @@ window.__ModuleLoader__.load({
|
|
|
344
415
|
return react.createElement(
|
|
345
416
|
"div",
|
|
346
417
|
{
|
|
418
|
+
// 外壳只负责定位(带 transform 居中);玻璃材质拆到内层独立元素——
|
|
419
|
+
// Chromium 中 transform 元素上的 backdrop-filter 采样异常,玻璃会失效
|
|
347
420
|
style: {
|
|
348
421
|
position: "absolute",
|
|
349
422
|
bottom: "calc(100% + 8px)",
|
|
@@ -351,67 +424,134 @@ window.__ModuleLoader__.load({
|
|
|
351
424
|
left: "50%",
|
|
352
425
|
transform: "translateX(-50%)",
|
|
353
426
|
zIndex: 1000,
|
|
354
|
-
background: "var(--dsw-alias-bg-primary, #fff)",
|
|
355
|
-
border: "1px solid var(--dsw-alias-border-secondary, #e0e0e0)",
|
|
356
|
-
borderRadius: 10,
|
|
357
|
-
boxShadow: "0 8px 24px rgba(0,0,0,0.16)",
|
|
358
|
-
padding: "12px 16px 30px",
|
|
359
427
|
},
|
|
360
428
|
},
|
|
429
|
+
react.createElement("div", {
|
|
430
|
+
className: "dsh-mem-glass",
|
|
431
|
+
style: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, pointerEvents: "none" },
|
|
432
|
+
}),
|
|
361
433
|
react.createElement(
|
|
362
434
|
"div",
|
|
363
|
-
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
435
|
+
{ style: { position: "relative", padding: "12px 16px 30px" } },
|
|
436
|
+
react.createElement(
|
|
437
|
+
"div",
|
|
438
|
+
{
|
|
439
|
+
ref: trackRef,
|
|
440
|
+
style: {
|
|
441
|
+
position: "relative",
|
|
442
|
+
// 容器宽 = thumb 活动范围(0..INNER_W + THUMB),点击映射与视觉两端严格对齐
|
|
443
|
+
width: TRACK_W,
|
|
444
|
+
height: 16,
|
|
445
|
+
touchAction: "none",
|
|
446
|
+
cursor: drag === null ? "pointer" : "grabbing",
|
|
371
447
|
},
|
|
372
448
|
onPointerDown: onPointerDown,
|
|
373
449
|
onPointerMove: onPointerMove,
|
|
374
450
|
onPointerUp: onPointerUp,
|
|
375
451
|
onPointerCancel: onPointerUp,
|
|
376
452
|
},
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
453
|
+
// 线条(底层):只连首尾停点的中心,两端不再露出停点外;
|
|
454
|
+
// thumb(当前档定位球)压最上层,不再被线条与停点穿过。
|
|
455
|
+
// 中性半透明灰在玻璃材质上深浅主题都可辨
|
|
380
456
|
react.createElement("div", {
|
|
381
457
|
style: {
|
|
382
458
|
position: "absolute",
|
|
383
|
-
left:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
height:
|
|
387
|
-
borderRadius:
|
|
388
|
-
background: "
|
|
389
|
-
border: "1px solid " + info.color,
|
|
390
|
-
boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
|
|
459
|
+
left: THUMB / 2,
|
|
460
|
+
width: INNER_W,
|
|
461
|
+
top: 7,
|
|
462
|
+
height: 4,
|
|
463
|
+
borderRadius: 999,
|
|
464
|
+
background: "rgba(128,140,150,0.32)",
|
|
391
465
|
pointerEvents: "none",
|
|
392
|
-
|
|
466
|
+
zIndex: 1,
|
|
393
467
|
},
|
|
394
468
|
}),
|
|
469
|
+
stops,
|
|
395
470
|
react.createElement("div", {
|
|
396
471
|
style: {
|
|
397
472
|
position: "absolute",
|
|
398
|
-
left:
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
height:
|
|
402
|
-
borderRadius:
|
|
403
|
-
|
|
473
|
+
left: thumbLeft,
|
|
474
|
+
top: 1,
|
|
475
|
+
width: THUMB,
|
|
476
|
+
height: THUMB,
|
|
477
|
+
borderRadius: "50%",
|
|
478
|
+
// 微透白 + 上缘高光:与浮层玻璃材质同语言
|
|
479
|
+
background: "rgba(255,255,255,0.94)",
|
|
480
|
+
border: "1px solid " + info.color,
|
|
481
|
+
boxShadow: "0 1px 4px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.9)",
|
|
404
482
|
pointerEvents: "none",
|
|
483
|
+
transition: drag === null ? "left 120ms ease" : "none",
|
|
484
|
+
zIndex: 3,
|
|
405
485
|
},
|
|
406
486
|
}),
|
|
407
|
-
|
|
487
|
+
),
|
|
488
|
+
props.error
|
|
489
|
+
? react.createElement("div", { style: { fontSize: 11, color: "#cf222e", marginTop: 20, whiteSpace: "nowrap" } }, props.error)
|
|
490
|
+
: null,
|
|
408
491
|
),
|
|
409
|
-
props.error
|
|
410
|
-
? react.createElement("div", { style: { fontSize: 11, color: "#cf222e", marginTop: 20, whiteSpace: "nowrap" } }, props.error)
|
|
411
|
-
: null,
|
|
412
492
|
);
|
|
413
493
|
}
|
|
414
494
|
|
|
495
|
+
// ── auto 档边缘流光 + 浮层玻璃材质:inline style 放不了 @keyframes/@property/媒体查询,
|
|
496
|
+
// 惰性注入一次性样式表(id 防重复)。dsw 主题变量参考其前端令牌:暗色走 body[data-ds-dark-theme]。 ──
|
|
497
|
+
var FLOW_STYLE_ID = "dsh-mem-flow-style";
|
|
498
|
+
function ensureFlowStyle() {
|
|
499
|
+
if (document.getElementById(FLOW_STYLE_ID)) return;
|
|
500
|
+
var el = document.createElement("style");
|
|
501
|
+
el.id = FLOW_STYLE_ID;
|
|
502
|
+
el.textContent = [
|
|
503
|
+
// conic 角度动画需要 @property 注册才能插值;不支持的浏览器优雅降级为静态渐变边框
|
|
504
|
+
"@property --dsh-mem-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }",
|
|
505
|
+
"@keyframes dshMemFlow { to { --dsh-mem-angle: 360deg; } }",
|
|
506
|
+
// 边缘流光(双层背景):border 区画旋转 conic 冷蓝光带;内部必须是【不透明】底色盖住
|
|
507
|
+
// 光带(半透明内层会让 conic 透进按钮内部,文字被光斑干扰——实测事故)。
|
|
508
|
+
// 不透明底 = 主题底混 12% 冷蓝(color-mix 出来 alpha=1),静态、随主题
|
|
509
|
+
".dsh-mem-flow {",
|
|
510
|
+
" border: 1px solid transparent;",
|
|
511
|
+
" background:",
|
|
512
|
+
" linear-gradient(",
|
|
513
|
+
" color-mix(in srgb, var(--dsw-alias-bg-layer-2, #ffffff) 88%, #3b82f6),",
|
|
514
|
+
" color-mix(in srgb, var(--dsw-alias-bg-layer-2, #ffffff) 88%, #3b82f6)",
|
|
515
|
+
" ) padding-box,",
|
|
516
|
+
" conic-gradient(from var(--dsh-mem-angle),",
|
|
517
|
+
" rgba(8,145,178,0.9), rgba(59,130,246,0.9), rgba(125,211,252,1),",
|
|
518
|
+
" rgba(99,102,241,0.9), rgba(8,145,178,0.9)) border-box;",
|
|
519
|
+
" animation: dshMemFlow 3s linear infinite;",
|
|
520
|
+
" color: #0284c7;",
|
|
521
|
+
"}",
|
|
522
|
+
"body[data-ds-dark-theme] .dsh-mem-flow { color: #7dd3fc; }",
|
|
523
|
+
"@media (prefers-reduced-motion: reduce) { .dsh-mem-flow { animation: none; } }",
|
|
524
|
+
// 浮层玻璃材质(Apple 菜单配方):材质层必须是【无 transform 的独立元素】——
|
|
525
|
+
// Chromium 中 transform 元素上的 backdrop-filter 采样异常(玻璃失效,实测)。
|
|
526
|
+
// 低不透明度基底让底下内容透出 + 顶部光泽渐变 + blur/saturate + 亮缘 + 分主题阴影
|
|
527
|
+
".dsh-mem-glass {",
|
|
528
|
+
" border-radius: 12px;",
|
|
529
|
+
" background:",
|
|
530
|
+
" linear-gradient(rgba(255,255,255,0.34), rgba(255,255,255,0.05)),",
|
|
531
|
+
" rgba(248,249,251,0.55);",
|
|
532
|
+
" -webkit-backdrop-filter: blur(32px) saturate(180%);",
|
|
533
|
+
" backdrop-filter: blur(32px) saturate(180%);",
|
|
534
|
+
" border: 1px solid rgba(255,255,255,0.55);",
|
|
535
|
+
" box-shadow:",
|
|
536
|
+
" 0 12px 40px rgba(0,0,0,0.16), 0 2px 8px rgba(0,0,0,0.07),",
|
|
537
|
+
" inset 0 1px 0 rgba(255,255,255,0.55), inset 0 -1px 0 rgba(0,0,0,0.03);",
|
|
538
|
+
"}",
|
|
539
|
+
"body[data-ds-dark-theme] .dsh-mem-glass {",
|
|
540
|
+
" background:",
|
|
541
|
+
" linear-gradient(rgba(255,255,255,0.09), rgba(255,255,255,0.015)),",
|
|
542
|
+
" rgba(30,32,40,0.55);",
|
|
543
|
+
" border-color: rgba(255,255,255,0.14);",
|
|
544
|
+
" box-shadow:",
|
|
545
|
+
" 0 16px 48px rgba(0,0,0,0.55), 0 3px 12px rgba(0,0,0,0.35),",
|
|
546
|
+
" inset 0 1px 0 rgba(255,255,255,0.12);",
|
|
547
|
+
"}",
|
|
548
|
+
"@media (prefers-reduced-transparency: reduce) {",
|
|
549
|
+
" .dsh-mem-glass { background: var(--dsw-alias-bg-layer-2, #ffffff); backdrop-filter: none; -webkit-backdrop-filter: none; }",
|
|
550
|
+
"}",
|
|
551
|
+
].join("\n");
|
|
552
|
+
document.head.appendChild(el);
|
|
553
|
+
}
|
|
554
|
+
|
|
415
555
|
/** 输入栏 pill:点击展开滑动选择器;props 来自 conversation.input.left 的 zone 注入。 */
|
|
416
556
|
function MemoryModePill(props) {
|
|
417
557
|
var rpc = props.rpc;
|
|
@@ -474,6 +614,31 @@ window.__ModuleLoader__.load({
|
|
|
474
614
|
if (!sessionId || !rpc) return null;
|
|
475
615
|
var info = modeInfo(mode);
|
|
476
616
|
var loaded = mode !== null;
|
|
617
|
+
var isAuto = loaded && mode === "auto";
|
|
618
|
+
|
|
619
|
+
if (isAuto) ensureFlowStyle();
|
|
620
|
+
|
|
621
|
+
// auto 档的边框/背景/文字色均由 .dsh-mem-flow 提供(双层背景画流光边 + 分主题文字),
|
|
622
|
+
// inline 不能设置这些——inline 优先级会盖掉 class 里的流光
|
|
623
|
+
var pillStyle = {
|
|
624
|
+
display: "inline-flex",
|
|
625
|
+
alignItems: "center",
|
|
626
|
+
gap: 4,
|
|
627
|
+
height: 24,
|
|
628
|
+
padding: "0 10px",
|
|
629
|
+
borderRadius: 999,
|
|
630
|
+
fontSize: 12,
|
|
631
|
+
fontWeight: 500,
|
|
632
|
+
lineHeight: "20px",
|
|
633
|
+
cursor: "pointer",
|
|
634
|
+
};
|
|
635
|
+
if (isAuto) {
|
|
636
|
+
pillStyle.boxShadow = "0 0 12px rgba(56,189,248,0.30)";
|
|
637
|
+
} else {
|
|
638
|
+
pillStyle.border = "1px solid " + (loaded ? info.color + "66" : "var(--dsw-alias-border-secondary, #ccc)");
|
|
639
|
+
pillStyle.background = loaded ? info.color + "14" : "var(--dsw-alias-bg-secondary, #f6f8fa)";
|
|
640
|
+
pillStyle.color = loaded ? info.color : "var(--dsh-alias-label-tertiary, #888)";
|
|
641
|
+
}
|
|
477
642
|
|
|
478
643
|
return react.createElement(
|
|
479
644
|
"div",
|
|
@@ -484,21 +649,8 @@ window.__ModuleLoader__.load({
|
|
|
484
649
|
type: "button",
|
|
485
650
|
title: "本会话记忆档位(点击切换)",
|
|
486
651
|
onClick: function () { setOpen(!open); },
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
alignItems: "center",
|
|
490
|
-
gap: 4,
|
|
491
|
-
height: 24,
|
|
492
|
-
padding: "0 10px",
|
|
493
|
-
borderRadius: 999,
|
|
494
|
-
fontSize: 12,
|
|
495
|
-
fontWeight: 500,
|
|
496
|
-
lineHeight: "20px",
|
|
497
|
-
cursor: "pointer",
|
|
498
|
-
border: "1px solid " + (loaded ? info.color + "66" : "var(--dsw-alias-border-secondary, #ccc)"),
|
|
499
|
-
background: loaded ? info.color + "14" : "var(--dsw-alias-bg-secondary, #f6f8fa)",
|
|
500
|
-
color: loaded ? info.color : "var(--dsh-alias-label-tertiary, #888)",
|
|
501
|
-
},
|
|
652
|
+
className: isAuto ? "dsh-mem-flow" : null,
|
|
653
|
+
style: pillStyle,
|
|
502
654
|
},
|
|
503
655
|
"记忆·",
|
|
504
656
|
react.createElement("span", null, loaded ? info.label : "…"),
|
|
@@ -630,6 +782,31 @@ window.__ModuleLoader__.load({
|
|
|
630
782
|
disabled: !master,
|
|
631
783
|
onChange: function (v) { toggle("recall", v); },
|
|
632
784
|
}),
|
|
785
|
+
settingsData.effort
|
|
786
|
+
? react.createElement(
|
|
787
|
+
"div",
|
|
788
|
+
{ style: S.switchRow },
|
|
789
|
+
react.createElement(Segmented, {
|
|
790
|
+
value: settingsData.effort.current,
|
|
791
|
+
options: [
|
|
792
|
+
{ key: "", label: "跟随配置" },
|
|
793
|
+
{ key: "off", label: "off" },
|
|
794
|
+
{ key: "high", label: "high" },
|
|
795
|
+
{ key: "max", label: "max" },
|
|
796
|
+
],
|
|
797
|
+
disabled: !master,
|
|
798
|
+
onChange: function (v) { toggle("reasoningEffort", v); },
|
|
799
|
+
}),
|
|
800
|
+
react.createElement("div", null,
|
|
801
|
+
react.createElement("div", { style: S.switchLabel }, "蒸馏思考"),
|
|
802
|
+
react.createElement(
|
|
803
|
+
"div",
|
|
804
|
+
{ style: S.switchDesc },
|
|
805
|
+
"当前生效 " + (settingsData.effort.effective || "(不传,跟随模型默认)") +
|
|
806
|
+
(settingsData.effort.current ? "" : "(来自部署配置 llm.reasoningEffort)"),
|
|
807
|
+
)),
|
|
808
|
+
)
|
|
809
|
+
: null,
|
|
633
810
|
ceilingNote
|
|
634
811
|
? react.createElement("p", { style: S.hint }, ceilingNote)
|
|
635
812
|
: null,
|
package/dist/config.d.ts
CHANGED
|
@@ -72,6 +72,8 @@ export interface MemoryConfig {
|
|
|
72
72
|
model: string;
|
|
73
73
|
/** 单次蒸馏调用的输出 token 上限(推理模型的 reasoning 与正文共享该预算)。 */
|
|
74
74
|
maxTokens: number;
|
|
75
|
+
/** 蒸馏调用的思考档位;空串不传(跟随模型默认)。 */
|
|
76
|
+
reasoningEffort: string;
|
|
75
77
|
temperature: number;
|
|
76
78
|
/** 单次蒸馏调用的用户 prompt 字符预算(≈token 数,按中文 1 字≈1 token 保守估算)。 */
|
|
77
79
|
maxInputChars: number;
|
|
@@ -158,6 +160,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
158
160
|
provider: Schema<string, string>;
|
|
159
161
|
model: Schema<string, string>;
|
|
160
162
|
maxTokens: Schema<number, number>;
|
|
163
|
+
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
161
164
|
temperature: Schema<number, number>;
|
|
162
165
|
maxInputChars: Schema<number, number>;
|
|
163
166
|
timeoutMs: Schema<number, number>;
|
|
@@ -165,6 +168,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
165
168
|
provider: Schema<string, string>;
|
|
166
169
|
model: Schema<string, string>;
|
|
167
170
|
maxTokens: Schema<number, number>;
|
|
171
|
+
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
168
172
|
temperature: Schema<number, number>;
|
|
169
173
|
maxInputChars: Schema<number, number>;
|
|
170
174
|
timeoutMs: Schema<number, number>;
|
|
@@ -247,6 +251,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
247
251
|
provider: Schema<string, string>;
|
|
248
252
|
model: Schema<string, string>;
|
|
249
253
|
maxTokens: Schema<number, number>;
|
|
254
|
+
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
250
255
|
temperature: Schema<number, number>;
|
|
251
256
|
maxInputChars: Schema<number, number>;
|
|
252
257
|
timeoutMs: Schema<number, number>;
|
|
@@ -254,6 +259,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
254
259
|
provider: Schema<string, string>;
|
|
255
260
|
model: Schema<string, string>;
|
|
256
261
|
maxTokens: Schema<number, number>;
|
|
262
|
+
reasoningEffort: Schema<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
257
263
|
temperature: Schema<number, number>;
|
|
258
264
|
maxInputChars: Schema<number, number>;
|
|
259
265
|
timeoutMs: Schema<number, number>;
|
package/dist/config.js
CHANGED
|
@@ -51,6 +51,9 @@ export const memorySchema = Schema.object({
|
|
|
51
51
|
model: Schema.string().default(''),
|
|
52
52
|
// 推理模型(如 v4-flash)的 reasoning 计入输出预算:预算不足会被思考吃光导致正文 0 字符
|
|
53
53
|
maxTokens: Schema.number().default(20_000),
|
|
54
|
+
// 蒸馏是结构化抽取任务,默认关思考(off):v4-flash 默认 high 档的思考可把任意 maxTokens
|
|
55
|
+
// 预算全部吃光导致正文 0 字符;非推理模型不认识 effort 时会报 UNSUPPORTED_REASONING_EFFORT,设空串跳过
|
|
56
|
+
reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default('off'),
|
|
54
57
|
temperature: Schema.number().default(0.3),
|
|
55
58
|
// 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
|
|
56
59
|
maxInputChars: Schema.number().default(700_000),
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
85
85
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
86
86
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
87
87
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
88
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
88
89
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
89
90
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
90
91
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -92,6 +93,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
92
93
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
93
94
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
94
95
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
96
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
95
97
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
96
98
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
97
99
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -174,6 +176,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
174
176
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
175
177
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
176
178
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
179
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
177
180
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
178
181
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
179
182
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
@@ -181,6 +184,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
181
184
|
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
182
185
|
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
183
186
|
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
187
|
+
reasoningEffort: import("@deepseek-ai/schemastery").default<"" | "off" | "high" | "max", "" | "off" | "high" | "max">;
|
|
184
188
|
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
185
189
|
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
186
190
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
package/dist/llm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
1
|
+
import { createUserMessage, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
2
2
|
import { errDetail } from './util/filelog.js';
|
|
3
3
|
/** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
|
|
4
4
|
export async function resolveModelRoute(ctx, cfg) {
|
|
@@ -35,6 +35,10 @@ export async function callLLM(ctx, cfg, opts) {
|
|
|
35
35
|
messages: [createUserMessage({ content: [{ type: 'text', text: user }], source: { kind: 'user' } })],
|
|
36
36
|
temperature: opts.temperature ?? cfg.llm.temperature,
|
|
37
37
|
maxTokens: opts.maxTokens ?? cfg.llm.maxTokens,
|
|
38
|
+
// 默认 off:蒸馏是结构化抽取,high 思考可吃光全部输出预算致正文 0 字符;空串不传(非推理模型)
|
|
39
|
+
...(cfg.llm.reasoningEffort
|
|
40
|
+
? { reasoningEffort: ReasoningEffortId(cfg.llm.reasoningEffort) }
|
|
41
|
+
: {}),
|
|
38
42
|
signal,
|
|
39
43
|
});
|
|
40
44
|
const startedAt = Date.now();
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -51,20 +51,26 @@ export class MemoryRunner {
|
|
|
51
51
|
const turnStart = Date.now();
|
|
52
52
|
this.logger.info(`[memory] 蒸馏管线开始(session=${sessionId},mode=${mode},本轮 ${messages.length} 条消息,待重试 ${this.pendingCount} 条)`);
|
|
53
53
|
// ── L0:原始对话已由 capture 在 turn/end 即时落盘(不排蒸馏队列,防慢 LLM 阻塞/退出丢消息) ──
|
|
54
|
+
// ── 运行时调参视图:UI 选择器可临时覆盖蒸馏思考档位(空串回退静态 config 默认)。
|
|
55
|
+
// 浅拷贝只覆盖 llm 一层,其余键与 this.cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。 ──
|
|
56
|
+
const liveNow = this.live.get();
|
|
57
|
+
const cfg = liveNow.reasoningEffort
|
|
58
|
+
? { ...this.cfg, llm: { ...this.cfg.llm, reasoningEffort: liveNow.reasoningEffort } }
|
|
59
|
+
: this.cfg;
|
|
54
60
|
// ── L1:抽取 + 去重(按档分桶,失败按桶保留待重试) ──
|
|
55
61
|
let newRecords = [];
|
|
56
|
-
const distillOn =
|
|
57
|
-
if (
|
|
62
|
+
const distillOn = liveNow.enabled && liveNow.distill;
|
|
63
|
+
if (cfg.extract.enabled && distillOn) {
|
|
58
64
|
const bucket = this.pending[mode];
|
|
59
65
|
bucket.push(...messages);
|
|
60
66
|
if (bucket.length > 200)
|
|
61
67
|
bucket.splice(0, bucket.length - 200);
|
|
62
68
|
try {
|
|
63
69
|
const t = Date.now();
|
|
64
|
-
const result = await runExtraction(this.ctx,
|
|
70
|
+
const result = await runExtraction(this.ctx, cfg, this.stores.l1, this.states, bucket, this.background, this.logger, mode);
|
|
65
71
|
if (!result.skipped)
|
|
66
72
|
this.pending[mode] = [];
|
|
67
|
-
this.background = [...this.background.slice(-
|
|
73
|
+
this.background = [...this.background.slice(-cfg.extract.backgroundMessages), ...messages];
|
|
68
74
|
newRecords = result.newRecords;
|
|
69
75
|
this.logger.info(`[memory] L1 阶段完成(${Date.now() - t}ms)`);
|
|
70
76
|
}
|
|
@@ -84,16 +90,16 @@ export class MemoryRunner {
|
|
|
84
90
|
}
|
|
85
91
|
}
|
|
86
92
|
// ── L2/L3:按记录族各自判定与执行 ──
|
|
87
|
-
if (
|
|
93
|
+
if (cfg.l2.enabled && distillOn) {
|
|
88
94
|
for (const family of ['chat', 'work']) {
|
|
89
95
|
const familyRecords = newRecords.filter((r) => (r.family ?? 'chat') === family);
|
|
90
96
|
if (familyRecords.length === 0)
|
|
91
97
|
continue;
|
|
92
98
|
const fstate = this.states[family];
|
|
93
|
-
if (fstate.newMemoriesSinceL2 >=
|
|
99
|
+
if (fstate.newMemoriesSinceL2 >= cfg.l2.minNewMemories) {
|
|
94
100
|
try {
|
|
95
101
|
const t = Date.now();
|
|
96
|
-
const result = await runSceneConsolidation(this.ctx,
|
|
102
|
+
const result = await runSceneConsolidation(this.ctx, cfg, this.stores.scenes[family], familyRecords, this.logger, family);
|
|
97
103
|
fstate.lastL2At = Date.now();
|
|
98
104
|
fstate.newMemoriesSinceL2 = 0;
|
|
99
105
|
if (result.personaRequestedReason)
|
|
@@ -105,14 +111,14 @@ export class MemoryRunner {
|
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
else {
|
|
108
|
-
this.logger.debug?.(`[memory] L2 跳过(family=${family},本族新增 ${familyRecords.length} 条,累计未整合 ${fstate.newMemoriesSinceL2}/${
|
|
114
|
+
this.logger.debug?.(`[memory] L2 跳过(family=${family},本族新增 ${familyRecords.length} 条,累计未整合 ${fstate.newMemoriesSinceL2}/${cfg.l2.minNewMemories})`);
|
|
109
115
|
}
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
|
-
if (
|
|
118
|
+
if (cfg.l3.enabled && distillOn) {
|
|
113
119
|
for (const family of ['chat', 'work']) {
|
|
114
120
|
try {
|
|
115
|
-
await runPersona(this.ctx,
|
|
121
|
+
await runPersona(this.ctx, cfg, this.stores.scenes[family], this.stores.persona[family], this.states[family], this.logger, family);
|
|
116
122
|
}
|
|
117
123
|
catch (err) {
|
|
118
124
|
this.logger.warn(`[memory] L3 画像蒸馏失败(family=${family}): ${errDetail(err)}`);
|
package/dist/settings.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
7
|
import Schema from '@deepseek-ai/schemastery';
|
|
8
8
|
import type { MemoryLogger } from './types.js';
|
|
9
|
+
/** 蒸馏思考档位可选项:'' = 跟随静态 config(部署默认)。 */
|
|
10
|
+
export type EffortChoice = '' | 'off' | 'high' | 'max';
|
|
9
11
|
export interface MemoryLiveSettings {
|
|
10
12
|
/** 总开关:关 = 捕获/蒸馏/召回注入全停(数据保留) */
|
|
11
13
|
enabled: boolean;
|
|
@@ -15,6 +17,8 @@ export interface MemoryLiveSettings {
|
|
|
15
17
|
distill: boolean;
|
|
16
18
|
/** 召回注入(画像/记忆上下文) */
|
|
17
19
|
recall: boolean;
|
|
20
|
+
/** 蒸馏思考档位运行时覆盖:'' = 跟随静态 config(llm.reasoningEffort) */
|
|
21
|
+
reasoningEffort: EffortChoice;
|
|
18
22
|
}
|
|
19
23
|
export interface LiveSettingsHandle {
|
|
20
24
|
/** settings 服务是否可用(不可用时 UI 侧隐藏开关面板) */
|
package/dist/settings.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import Schema from '@deepseek-ai/schemastery';
|
|
2
2
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
3
3
|
const NS = settingsNamespace('dsh-memory');
|
|
4
|
-
const ALWAYS_ON = { enabled: true, capture: true, distill: true, recall: true };
|
|
4
|
+
const ALWAYS_ON = { enabled: true, capture: true, distill: true, recall: true, reasoningEffort: '' };
|
|
5
5
|
export function liveSettingsSchema() {
|
|
6
6
|
return Schema.object({
|
|
7
7
|
enabled: Schema.boolean().default(true),
|
|
8
8
|
capture: Schema.boolean().default(true),
|
|
9
9
|
distill: Schema.boolean().default(true),
|
|
10
10
|
recall: Schema.boolean().default(true),
|
|
11
|
+
reasoningEffort: Schema.union(['', 'off', 'high', 'max']).default(''),
|
|
11
12
|
});
|
|
12
13
|
}
|
|
13
14
|
export function registerLiveSettings(ctx, logger) {
|
|
@@ -27,7 +28,8 @@ export function registerLiveSettings(ctx, logger) {
|
|
|
27
28
|
scope.watch((next) => {
|
|
28
29
|
const prev = current;
|
|
29
30
|
current = resolveSettings(next);
|
|
30
|
-
logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}
|
|
31
|
+
logger.info(`[memory] 记忆模式开关更新:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
|
|
32
|
+
`,蒸馏思考=${current.reasoningEffort || '跟随配置'}(此前 总=${prev.enabled})`);
|
|
31
33
|
});
|
|
32
34
|
inner = {
|
|
33
35
|
supported: true,
|
|
@@ -36,7 +38,8 @@ export function registerLiveSettings(ctx, logger) {
|
|
|
36
38
|
await scope.update(patch);
|
|
37
39
|
},
|
|
38
40
|
};
|
|
39
|
-
logger.info(`[memory] 记忆模式开关就绪(settings 命名空间 dsh-memory,当前:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}
|
|
41
|
+
logger.info(`[memory] 记忆模式开关就绪(settings 命名空间 dsh-memory,当前:总=${current.enabled} 捕获=${current.capture} 蒸馏=${current.distill} 召回=${current.recall}` +
|
|
42
|
+
`,蒸馏思考=${current.reasoningEffort || '跟随配置'})`);
|
|
40
43
|
return true;
|
|
41
44
|
}
|
|
42
45
|
catch (err) {
|
|
@@ -64,10 +67,14 @@ function resolveSettings(value) {
|
|
|
64
67
|
if (!value || typeof value !== 'object')
|
|
65
68
|
return { ...ALWAYS_ON };
|
|
66
69
|
const v = value;
|
|
70
|
+
const efforts = ['', 'off', 'high', 'max'];
|
|
67
71
|
return {
|
|
68
72
|
enabled: v.enabled !== false,
|
|
69
73
|
capture: v.capture !== false,
|
|
70
74
|
distill: v.distill !== false,
|
|
71
75
|
recall: v.recall !== false,
|
|
76
|
+
reasoningEffort: typeof v.reasoningEffort === 'string' && efforts.includes(v.reasoningEffort)
|
|
77
|
+
? v.reasoningEffort
|
|
78
|
+
: '',
|
|
72
79
|
};
|
|
73
80
|
}
|
package/dist/stats.js
CHANGED
|
@@ -130,26 +130,41 @@ async function handleEndpoint(endpoint, payload, deps) {
|
|
|
130
130
|
deps.logger.info(`[memory] 会话档位设置 session=${p.sessionId} mode=${p.mode}`);
|
|
131
131
|
return { sessionId: p.sessionId, mode: p.mode };
|
|
132
132
|
}
|
|
133
|
-
case 'dsh-memory/settings-get':
|
|
133
|
+
case 'dsh-memory/settings-get': {
|
|
134
|
+
const s = live?.get();
|
|
134
135
|
return {
|
|
135
136
|
supported: live?.supported ?? false,
|
|
136
|
-
settings:
|
|
137
|
+
settings: s ?? { enabled: true, capture: true, distill: true, recall: true, reasoningEffort: '' },
|
|
137
138
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
138
139
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
140
|
+
// 蒸馏思考档位:current 是运行时覆盖('' = 跟随配置),effective 是实际生效值
|
|
141
|
+
effort: {
|
|
142
|
+
current: s?.reasoningEffort ?? '',
|
|
143
|
+
effective: s?.reasoningEffort || cfg.llm.reasoningEffort,
|
|
144
|
+
fallback: cfg.llm.reasoningEffort,
|
|
145
|
+
},
|
|
139
146
|
};
|
|
147
|
+
}
|
|
140
148
|
case 'dsh-memory/settings-set': {
|
|
141
149
|
if (!live)
|
|
142
150
|
throw new Error('开关通道未初始化');
|
|
143
151
|
const patch = (payload ?? {});
|
|
144
|
-
const allowed = ['enabled', 'capture', 'distill', 'recall'];
|
|
145
152
|
const clean = {};
|
|
146
|
-
for (const key of
|
|
153
|
+
for (const key of ['enabled', 'capture', 'distill', 'recall']) {
|
|
147
154
|
if (typeof patch[key] === 'boolean')
|
|
148
155
|
clean[key] = patch[key];
|
|
149
156
|
}
|
|
157
|
+
if (patch.reasoningEffort !== undefined) {
|
|
158
|
+
const v = String(patch.reasoningEffort);
|
|
159
|
+
if (!['', 'off', 'high', 'max'].includes(v)) {
|
|
160
|
+
throw new Error(`非法思考档位: ${v}(允许 ''/off/high/max)`);
|
|
161
|
+
}
|
|
162
|
+
clean.reasoningEffort = v;
|
|
163
|
+
}
|
|
150
164
|
if (Object.keys(clean).length === 0)
|
|
151
165
|
throw new Error('开关更新载荷为空');
|
|
152
166
|
await live.update(clean);
|
|
167
|
+
deps.logger.info(`[memory] 设置更新:${JSON.stringify(clean)}`);
|
|
153
168
|
return { ok: true, settings: live.get() };
|
|
154
169
|
}
|
|
155
170
|
case 'dsh-memory/list-records': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|