@rennimars/dsh-weknora-memory 0.2.1 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 — 写入侧随包发布(可选组件)
4
+
5
+ - **新增 `lib/weknora.js`**:读侧与写入侧共用的唯一 HTTP 客户端(base URL 拼接、`X-API-Key` 头、超时、`no_api_url|timeout|transport|http_<status>` 错误词表)。`lib/retrieve.js` 的两处内联 fetch 已改为调用它,59 个既有用例保持全绿。
6
+ - **新增 `writers/`(默认不启用)**:`ingest.mjs` 会话原文入库①、`distill.mjs` 结论蒸馏入库②、`dream.mjs` 事实卡裁决 S0–S4、`profile-writer.mjs` 画像唯一写入者。四者都支持 `--config` / `--dry-run` / `--verbose`,失败返回可读错误与明确退出码,`--dry-run` 不写库、不写文件、不写 state。
7
+ - **新增 `writers/register-tasks.ps1`** 与 **`writers/run-all.cmd`**:幂等注册/撤销计划任务(当前用户级,不需要管理员),以及手动跑一轮(可选单步)。
8
+ - **凭证纪律**:`writer.config.json` 的 `apiKey` 字段被忽略,只从 `WEKNORA_API_KEY` 环境变量读取;发布包内不含任何密钥、知识库 id 或本机绝对路径。
9
+ - **新增测试 `test/writers.test.mjs`(5 项)**:import 零副作用、`--help` 退出 0、桩地址 dry-run 不落盘且错误可读、`proposed→active`/`degrade→disputed` 只改状态字段、发布树无凭证/无机器路径。总计 64 个用例。
10
+ - 安装后**不会**自动注册任何计划任务,读侧行为与 0.2.2 完全一致。
11
+
12
+ ## 0.2.2 — 补上写入侧(文档)
13
+
14
+ - 新增「写入侧:这些记忆是谁写进去的」一节:三条管道(每小时会话原文入库① / 每日 04:30 结论蒸馏入库② / 每日 03:30 夜间做梦裁决事实卡)与画像写入器的闸门(`proposed` → 隔夜复核 → `active`,被标记 degrade 的卡转 `disputed`),含触发时间、配额与失败策略。
15
+ - 明确边界:这些定时任务属于**宿主侧**,不随本包分发;装上插件不会自动获得写入能力。写清这点是为了不让"记忆插件"的想象越界成实际行为。
16
+ - 补三库分工(原文 / 结论 / 原始材料)与"为什么写入必须独立于读侧"的取舍说明。
17
+ - 代码与默认配置未改动。
18
+
3
19
  ## 0.2.1 — 介绍重写(仅文档)
4
20
 
5
21
  - README 从“功能清单”改写为**机制说明**:三层上下文(常驻 / 画像 / 检索)各自的独立预算、一轮之内的完整检索管线(分库并发 → RRF → 来源权重 → 时效衰减 → 冷热分层 → 阈值 → 会话去重 → 邻块扩展 → 事实卡冲突 → 渲染),以及每处设计取舍的理由。
package/README.md CHANGED
@@ -48,7 +48,58 @@ agent/pre-step
48
48
 
49
49
  最后一步的细节很重要:注入块是**一条独立的 user-role 消息**,不是对你的提问的改写,模型能分辨"这是检索来的历史材料"而不是"用户刚说的新任务"。
50
50
 
51
- ## 四、几个刻意的设计取舍
51
+ ## 四、写入侧:这些记忆是谁写进去的
52
+
53
+ 本包只做**读**(每轮注入)和**人工编辑**(面板)。把原文、结论、画像写进 WeKnora 与 `data/profile.json` 的,是宿主侧的定时任务——**装上本插件不会自动获得这些任务**,它们是同一套体系的另一半,这里把契约写清楚,方便你自己实现或用别的工具替代。
54
+
55
+ | 任务 | 触发 | 做什么 | 写到哪里 |
56
+ |---|---|---|---|
57
+ | `DSH-Session-Ingest` | 每小时 | 把**已结束且空闲 ≥10 分钟**的会话转录**原样**入库,跳过子代理会话;单篇 ≤200,000 字符,最多保留 150 篇;失败重试 3 次,入库后轮询索引 ≤600 秒 | 库① 会话记忆 |
58
+ | `DSH-Session-Distill` | 每日 04:30 | 从最近会话里抽取结论 / 约束 / 偏好 / 教训,一次最多 3 个会话、3 篇文档,输入 ≤30,000 字符、每轮 ≤1500 字符、每会话 ≤8 条,语义去重阈值 0.66 | 库② 知识 |
59
+ | `DSH-Memory-Dream` | 每日 03:30 | **夜间做梦**:五阶段 S0 收拢 → S1 分组 → S2 裁决 → S3 落账 → S4 报告。裁决同主题事实卡的取代 / 冲突 / 重复,**只改 header 里的状态字段,正文逐字保留**;单夜 ≤60 次 LLM 调用、≤20 次更新;支持 `--dry-run` / `--no-llm` 零成本演练 | 库② 知识(事实卡状态) |
60
+ | `profile-writer` | 紧接做梦之后,且**只在做梦 rc=0 时** | `data/profile.json` 的**唯一写入者**:新卡一律 `proposed`,至少等一夜才复核;复核必须同时满足「有来源 / 不重复 / 不冲突 / 正文 ≤60 字」才转正,否则保持 `proposed` 并记日志;被插件标了 degrade 的 `active` 卡转 `disputed`,下一轮起不再进常驻块 | 常驻层画像 |
61
+ | `DSH-Memory-Weekly` | 每周 | 体检:跨库重复、失效文档、状态异常 | 三库 |
62
+
63
+ 三库的分工不是随手划的:
64
+
65
+ - **库① 会话记忆**存原文,回答"当时到底怎么说的";
66
+ - **库② 知识**存结论,一条结论一张事实卡,回答"这件事定了没有、为什么";
67
+ - **库③ 资料**存你手工入库的原始文件,回答"这份规范里怎么写的"。
68
+
69
+ **为什么写入侧必须独立于插件:** 写要 LLM、要数据库权限、要能被人工审计;而读侧每轮都跑,必须零依赖、可静默失败。两者混在一起,任何一次写入故障都会变成一次会话故障。
70
+
71
+ **为什么画像文件只允许一个写入者:** 常驻内容每轮注入、跨会话传播——一旦被污染,后面没有任何一次检索能把它纠正回来。所以插件侧只读,任何新内容只能走 `proposed → 隔夜复核 → active` 这条闸门。
72
+
73
+ ### 本包自带的写入侧参考实现(`writers/`,默认不启用)
74
+
75
+ 上面那张表是**它应该长什么样**;本包把同一套约定做成了可复制的实现,放在 `writers/`,**随包分发但默认不运行**——装上插件不会创建任何计划任务、不会写任何文档。
76
+
77
+ | 文件 | 作用 |
78
+ |---|---|
79
+ | `writers/ingest.mjs` | 会话转录 → 库①(幂等:同名文档走 PUT 复用 id) |
80
+ | `writers/distill.mjs` | 会话 → 结论 → 库②(含语义去重,LLM 走 WeKnora 会话网关) |
81
+ | `writers/dream.mjs` | 事实卡裁决 S0–S4,只改 header 状态;`--self-test` 可离线跑 |
82
+ | `writers/profile-writer.mjs` | `data/profile.json` 的唯一写入者;不带 `--write` 就只打印 diff |
83
+ | `writers/register-tasks.ps1` | 幂等注册/更新计划任务(`-Unregister` 撤销,`-Prefix`、`-Times` 可改) |
84
+ | `writers/run-all.cmd` | 手动跑一轮:`run-all.cmd [--dry-run] [ingest|distill|dream|profile]` |
85
+
86
+ 三步启用:
87
+
88
+ ```powershell
89
+ cd node_modules/@rennimars/dsh-weknora-memory
90
+ copy writers\config.example.json writers\writer.config.json # 填 apiUrl / sessionsRoot / kbs
91
+ $env:WEKNORA_API_KEY = "<你的 key>" # 只从环境变量读,不落文件
92
+ powershell -ExecutionPolicy Bypass -File writers\register-tasks.ps1
93
+ ```
94
+
95
+ 四条纪律:
96
+
97
+ - **凭证只在环境变量**:`writer.config.json` 里的 `apiKey` 字段会被忽略;缺 `WEKNORA_API_KEY` 时只提示,不带该请求头。
98
+ - **演练零成本**:四个脚本都支持 `--dry-run`(不写库、不写文件、不写 state);`dream.mjs --dry-run --no-llm` 连一次 LLM 都不发。
99
+ - **共用一份 HTTP**:读侧 `lib/retrieve.js` 与写入侧全部走 `lib/weknora.js`,`X-API-Key` 头与错误词表只有一处实现。
100
+ - **失败可见**:退出码 0/1/2/3 有明确语义,失败是一条可读错误而不是异常栈——这与读侧「失败即静默」刻意相反,因为定时任务没有人在看着。
101
+
102
+ ## 五、几个刻意的设计取舍
52
103
 
53
104
  **不改用户原话。** 注入永远走追加,避免任何"提示词污染"式的隐蔽改写。
54
105
 
@@ -62,7 +113,7 @@ agent/pre-step
62
113
 
63
114
  **自动写入与人工编辑分权。** `data/profile.json` 的自动更新属于夜间任务,插件本身只读;面板里的编辑是用户显式动作。这条边界防的是"每轮注入、每轮重算"的自我强化——一个被污染的事实否则会跨会话传播。
64
115
 
65
- ## 五、可观测性
116
+ ## 六、可观测性
66
117
 
67
118
  侧边栏「记忆」页签,三个视图:
68
119
 
@@ -74,7 +125,7 @@ agent/pre-step
74
125
 
75
126
  日志只记元数据:会话 id、耗时、命中数、字符数、结果。**不写提问原文,也不写片段正文**。
76
127
 
77
- ## 六、依赖
128
+ ## 七、依赖
78
129
 
79
130
  | 项 | 要求 |
80
131
  |---|---|
@@ -82,7 +133,7 @@ agent/pre-step
82
133
  | Node | `>= 20.11` |
83
134
  | WeKnora | 一个可访问实例(默认 `http://127.0.0.1:8080/api/v1`),以及至少一个知识库 |
84
135
 
85
- ## 七、安装
136
+ ## 八、安装
86
137
 
87
138
  **从插件市场**:插件页 → 插件市场 → 搜索 `weknora` → 安装。
88
139
 
@@ -96,7 +147,7 @@ git clone https://github.com/RenniMars/dsh-weknora-memory node_modules/@rennimar
96
147
 
97
148
  然后在同目录 `package.json` 的 `dsh.profile.bundles` 里加一行 `"@rennimars/dsh-weknora-memory"`,重启客户端。
98
149
 
99
- ## 八、配置
150
+ ## 九、配置
100
151
 
101
152
  复制模板,填三样东西:
102
153
 
@@ -122,13 +173,13 @@ cp wk-inject.config.example.json wk-inject.config.json
122
173
 
123
174
  其余参数(超时、字符上限、命中数、阈值、RRF k、邻块扩展、衰减半衰期与下限、冷热分层配额、画像卡配额、事实卡权重、日志路径)见 `lib/config.js` 的 `DEFAULT_CONFIG`,每一项都带注释说明为什么取这个值;一般不用动。
124
175
 
125
- ## 九、隐私与边界
176
+ ## 十、隐私与边界
126
177
 
127
178
  - `data/profile.json`、`data/profile-stats.json`、`data/tier-state.json`、`logs/inject.log` 全部留在本机,不进仓库、不进包(`.gitignore` / `.npmignore` 都排除了)。
128
179
  - 本插件不向任何第三方上报数据;所有请求只发往你自己配置的 `apiUrl`。
129
180
  - 子代理会话默认不检索(`skipSubagents: true`):子代理拿到的是被裁剪过的任务,用它的文本去检索只会污染主会话的记忆。
130
181
 
131
- ## 十、开发
182
+ ## 十一、开发
132
183
 
133
184
  ```bash
134
185
  npm test # node --test test/*.mjs,59 个用例
@@ -144,6 +195,8 @@ npm test # node --test test/*.mjs,59 个用例
144
195
  | `lib/decay.js` | 时效衰减与命中回写(原子 JSON 状态) |
145
196
  | `lib/config.js` | 配置加载、钳制、日志 |
146
197
  | `lib/panel.js` / `lib/client.js` | 面板同源路由 / GUI 侧渲染 |
198
+ | `lib/weknora.js` | 唯一 HTTP 客户端:读侧与写入侧共用 |
199
+ | `writers/*.mjs` | 可选写入侧:ingest / distill / dream / profile-writer |
147
200
 
148
201
  ## License
149
202
 
package/lib/retrieve.js CHANGED
@@ -25,6 +25,7 @@
25
25
  * that call. Any failure there is silent: the hit is injected exactly as it arrived.
26
26
  */
27
27
  import { TIMEOUT_CAP_MS } from "./config.js";
28
+ import { request } from "./weknora.js";
28
29
  import { applyDecay, readState } from "./decay.js";
29
30
  import { cleanText } from "./format.js";
30
31
 
@@ -158,32 +159,17 @@ function keyOf(hit, group) {
158
159
 
159
160
  /** One scoped search call; kbId "" means "no scope" (search everything). */
160
161
  async function searchOnce(query, config, kbId, deps) {
161
- const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
162
- const timeoutMs = timeoutOf(config);
163
- const base = String(config.apiUrl ?? "").replace(/\/+$/, "");
164
- if (!base) return { ok: false, reason: "no_api_url" };
165
- const url = base + "/knowledge-search";
166
- const signals = [AbortSignal.timeout(timeoutMs)];
167
- if (deps.signal) signals.push(deps.signal);
168
- const headers = { "Content-Type": "application/json", Accept: "application/json" };
169
- if (config.apiKey) headers["X-API-Key"] = config.apiKey;
170
162
  const body = { query, top_k: topKOf(config) };
171
163
  if (kbId) body.knowledge_base_ids = [kbId];
172
- try {
173
- const response = await fetchImpl(url, {
174
- method: "POST",
175
- headers,
176
- body: JSON.stringify(body),
177
- signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
178
- });
179
- if (!response.ok) return { ok: false, reason: "http_" + response.status };
180
- const payload = await response.json();
181
- const hits = Array.isArray(payload?.data) ? payload.data : [];
182
- return { ok: true, hits };
183
- } catch (error) {
184
- const name = error?.name;
185
- return { ok: false, reason: name === "TimeoutError" || name === "AbortError" ? "timeout" : "transport" };
186
- }
164
+ const result = await request(config, "POST", "/knowledge-search", {
165
+ body,
166
+ timeoutMs: timeoutOf(config),
167
+ signal: deps.signal,
168
+ fetchImpl: deps.fetchImpl,
169
+ });
170
+ if (!result.ok) return { ok: false, reason: result.reason };
171
+ const hits = Array.isArray(result.body?.data) ? result.body.data : [];
172
+ return { ok: true, hits };
187
173
  }
188
174
 
189
175
  /**
@@ -289,30 +275,19 @@ function sliceHead(text, limit) {
289
275
  * binarySearchPage() over the page index ranges instead of giving up.
290
276
  */
291
277
  async function fetchChunkPage(knowledgeId, page, config, deps, timeoutMs) {
292
- const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
293
- const base = String(config?.apiUrl ?? "").replace(/\/+$/, "");
294
- if (!base) return null;
295
- const url = base + "/chunks/" + encodeURIComponent(knowledgeId) + "?page=" + page + "&page_size=" + CHUNK_PAGE_SIZE;
296
- const headers = { Accept: "application/json" };
297
- if (config.apiKey) headers["X-API-Key"] = config.apiKey;
298
- const signals = [AbortSignal.timeout(Math.max(MIN_REQUEST_MS, timeoutMs))];
299
- if (deps.signal) signals.push(deps.signal);
300
- try {
301
- const response = await fetchImpl(url, {
302
- method: "GET",
303
- headers,
304
- signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],
305
- });
306
- if (!response.ok) return null;
307
- const payload = await response.json();
308
- return {
309
- list: Array.isArray(payload?.data) ? payload.data : [],
310
- total: Number.isFinite(payload?.total) ? Number(payload.total) : null,
311
- pageSize: Number.isFinite(payload?.page_size) && payload.page_size > 0 ? Number(payload.page_size) : null,
312
- };
313
- } catch {
314
- return null;
315
- }
278
+ const result = await request(
279
+ config,
280
+ "GET",
281
+ "/chunks/" + encodeURIComponent(knowledgeId) + "?page=" + page + "&page_size=" + CHUNK_PAGE_SIZE,
282
+ { timeoutMs: Math.max(MIN_REQUEST_MS, timeoutMs), signal: deps.signal, fetchImpl: deps.fetchImpl }
283
+ );
284
+ if (!result.ok) return null;
285
+ const payload = result.body;
286
+ return {
287
+ list: Array.isArray(payload?.data) ? payload.data : [],
288
+ total: Number.isFinite(payload?.total) ? Number(payload.total) : null,
289
+ pageSize: Number.isFinite(payload?.page_size) && payload.page_size > 0 ? Number(payload.page_size) : null,
290
+ };
316
291
  }
317
292
 
318
293
  /** One fetched page, indexed by chunk id and by chunk_index, with its index range. */
package/lib/weknora.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The one WeKnora HTTP client.
3
+ *
4
+ * Both sides of the memory system go through here: the injector (lib/retrieve.js,
5
+ * read path, every step) and the optional writers (writers/*, write path, scheduled).
6
+ * They were two hand-rolled fetch blocks before this module existed, which is exactly
7
+ * how a header or an error string drifts apart between the two.
8
+ *
9
+ * Nothing in here throws for transport, timeout or HTTP status: a call resolves to
10
+ * { ok, status, body, reason } and the caller decides whether that is fatal. The read
11
+ * path is required to degrade silently, the writers are required to log and exit
12
+ * non-zero, so the decision has to stay with them.
13
+ */
14
+
15
+ /** Request ceiling applied when a caller names no timeout of its own. */
16
+ export const DEFAULT_TIMEOUT_MS = 4000;
17
+
18
+ /** The configured base URL without trailing slashes, or "" when unset. */
19
+ export function baseUrlOf(config) {
20
+ return String(config?.apiUrl ?? "").replace(/\/+$/, "");
21
+ }
22
+
23
+ /** Join a base URL and a path, tolerating a missing or extra leading slash. */
24
+ export function joinUrl(base, path) {
25
+ const root = String(base ?? "").replace(/\/+$/, "");
26
+ const tail = String(path ?? "");
27
+ return root + (tail.startsWith("/") ? tail : "/" + tail);
28
+ }
29
+
30
+ /**
31
+ * Request headers: X-API-Key only when a key is configured, and the JSON content
32
+ * type only when a body is actually sent (a GET with Content-Type is noise).
33
+ */
34
+ export function headersFor(config, { json = false } = {}) {
35
+ const headers = json
36
+ ? { "Content-Type": "application/json", Accept: "application/json" }
37
+ : { Accept: "application/json" };
38
+ const key = config?.apiKey;
39
+ if (typeof key === "string" && key) headers["X-API-Key"] = key;
40
+ return headers;
41
+ }
42
+
43
+ /**
44
+ * One request. `reason` is one of no_api_url / timeout / transport / http_<status>,
45
+ * which is the vocabulary lib/retrieve.js already reports in its stats.
46
+ *
47
+ * @param {object} config clamped config (apiUrl / apiKey)
48
+ * @param {string} method HTTP method
49
+ * @param {string} path path below the base URL, e.g. "/knowledge-search"
50
+ * @param {object} [options]
51
+ * @param {object} [options.body] JSON body; omit for a body-less request
52
+ * @param {number} [options.timeoutMs] per-request ceiling; omitted means no deadline
53
+ * @param {AbortSignal} [options.signal] caller's own deadline/cancellation
54
+ * @param {Function} [options.fetchImpl] injected fetch (tests)
55
+ * @param {"json"|"none"} [options.expect] parse the body as JSON (default) or ignore it
56
+ */
57
+ export async function request(config, method, path, options = {}) {
58
+ const { body, timeoutMs, signal, fetchImpl = globalThis.fetch, expect = "json" } = options;
59
+ const base = baseUrlOf(config);
60
+ if (!base) return { ok: false, reason: "no_api_url" };
61
+
62
+ const signals = [];
63
+ if (Number.isFinite(timeoutMs) && timeoutMs > 0) signals.push(AbortSignal.timeout(timeoutMs));
64
+ if (signal) signals.push(signal);
65
+
66
+ const init = { method, headers: headersFor(config, { json: body !== undefined }) };
67
+ if (body !== undefined) init.body = JSON.stringify(body);
68
+ if (signals.length === 1) init.signal = signals[0];
69
+ else if (signals.length > 1) init.signal = AbortSignal.any(signals);
70
+
71
+ try {
72
+ const response = await fetchImpl(joinUrl(base, path), init);
73
+ if (!response.ok) return { ok: false, status: response.status, reason: "http_" + response.status };
74
+ if (expect === "none") return { ok: true, status: response.status, body: null };
75
+ return { ok: true, status: response.status, body: await response.json() };
76
+ } catch (error) {
77
+ const name = error?.name;
78
+ return { ok: false, reason: name === "TimeoutError" || name === "AbortError" ? "timeout" : "transport" };
79
+ }
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rennimars/dsh-weknora-memory",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Layered context engine for DeepSeek Harness: per-kb WeKnora retrieval fused by RRF, recency decay and hot/warm/cold tiering, standing profile cards and fact-conflict surfacing — budgeted, auditable, editable.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "lib",
14
+ "writers",
14
15
  "cordis.patch.yml",
15
16
  "wk-inject.config.example.json",
16
17
  "README.md",
@@ -0,0 +1,24 @@
1
+ # writers/ — 写入侧(可选)
2
+
3
+ 本目录是**参考实现**,不随插件默认启用,安装后不会注册任何计划任务。它把会话转录、结论、事实卡状态与画像写进 WeKnora 与 `data/profile.json`;插件的读侧(每轮注入)只读这些东西。
4
+
5
+ ## 契约(四个脚本都必须遵守)
6
+
7
+ 1. **配置**:默认读本目录的 `writer.config.json`,`--config <path>` 可覆盖;缺文件时用 `config.example.json` 的默认值并在日志里说明。
8
+ 2. **凭证**:`apiUrl` 形如 `http://127.0.0.1:8080/api/v1`(**含**版本前缀,与插件配置一致);`apiKey` 留空,从环境变量 `WEKNORA_API_KEY` 取;环境变量优先。
9
+ 3. **HTTP**:一律通过 `../lib/weknora.js` 的 `request()`,不自己写 fetch。
10
+ 4. **路径**:`profile.path` / `profile.statsPath` 相对**包根目录**(本目录的上一级)解析,与插件一致。
11
+ 5. **演练**:每个脚本都支持 `--dry-run`(只打印计划,不写库、不写文件)与 `--verbose`。
12
+ 6. **退出码**:0 成功;非 0 失败。失败必须写日志并返回非 0,不能静默。
13
+ 7. **幂等**:同一轮重复执行不得产生重复文档(按标题前缀 + 内容哈希判定)。
14
+
15
+ ## 脚本
16
+
17
+ | 文件 | 触发建议 | 作用 |
18
+ |---|---|---|
19
+ | `ingest.mjs` | 每小时 | 会话转录原样入库① |
20
+ | `distill.mjs` | 每日 04:30 | 会话 → 结论 → 库② |
21
+ | `dream.mjs` | 每日 03:30 | 事实卡裁决(S0..S4) |
22
+ | `profile-writer.mjs` | 紧接 dream(rc=0 后) | 画像唯一写入者 |
23
+
24
+ `register-tasks.ps1` 按上面的触发时间注册计划任务(幂等,`--unregister` 反注册);`run-all.cmd` 手动跑一轮。
@@ -0,0 +1,57 @@
1
+ {
2
+ "$comment": "Writers config. Copy to writer.config.json next to this file. No credential belongs in this file: leave apiKey out and set WEKNORA_API_KEY in the environment.",
3
+ "apiUrl": "http://127.0.0.1:8080/api/v1",
4
+ "apiKey": "",
5
+ "sessionsRoot": "",
6
+ "kbs": {
7
+ "session": "",
8
+ "knowledge": "",
9
+ "material": ""
10
+ },
11
+ "ingest": {
12
+ "enabled": true,
13
+ "titlePrefix": "会话记录 ",
14
+ "settleMinutes": 10,
15
+ "maxDocBytes": 200000,
16
+ "maxSessionDocs": 150,
17
+ "skipSubagentSessions": true,
18
+ "maxAttempts": 3,
19
+ "pollTimeoutSec": 600
20
+ },
21
+ "distill": {
22
+ "enabled": true,
23
+ "titlePrefix": "记忆 ",
24
+ "maxSessionsPerRun": 3,
25
+ "maxDocsPerRun": 3,
26
+ "maxInputChars": 30000,
27
+ "perTurnChars": 1500,
28
+ "maxEntriesPerSession": 8,
29
+ "semanticDedupe": {
30
+ "enabled": true,
31
+ "scoreMin": 0.66
32
+ }
33
+ },
34
+ "dream": {
35
+ "enabled": true,
36
+ "titlePrefix": "[fact] ",
37
+ "maxLlmCalls": 60,
38
+ "maxUpdates": 20,
39
+ "grouping": {
40
+ "searchTopK": 5,
41
+ "queryChars": 120,
42
+ "sameRootScore": 0.5,
43
+ "crossRootScore": 0.6,
44
+ "maxCluster": 5
45
+ }
46
+ },
47
+ "profile": {
48
+ "enabled": true,
49
+ "path": "data/profile.json",
50
+ "statsPath": "data/profile-stats.json"
51
+ },
52
+ "llm": {
53
+ "mode": "weknora-session-chat",
54
+ "modelName": "builtin-deepseek-flash",
55
+ "timeoutMs": 240000
56
+ }
57
+ }