sage-mem 0.6.3 → 0.7.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.md +282 -242
- package/lib/{dream-audit.js → autodream-audit.js} +31 -13
- package/lib/{dream-prompt.js → autodream-prompt.js} +22 -14
- package/lib/{dream-tools.js → autodream-tools.js} +159 -26
- package/lib/autodream.js +1722 -0
- package/lib/client.js +460 -142
- package/lib/index.js +219 -67
- package/lib/typert.host.js +182 -54
- package/package.json +89 -87
- package/lib/dream.js +0 -886
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 1. 按问题检索注入:system-prompt/assemble 时扫 memory 目录,按双字匹配
|
|
7
7
|
* 选相关文件,读全文注入 system prompt,让 agent 第一轮就「想起」
|
|
8
8
|
* 2. TypertRemoteService:给设置页「记忆管理」提供文件列表/读/写/删
|
|
9
|
-
* 3.
|
|
9
|
+
* 3. autodream(自动做梦):可选的记忆整理,见 lib/autodream.js
|
|
10
10
|
*
|
|
11
11
|
* 写入靠 AGENTS.md 的「记忆使用」规则引导 agent 自己判断 + 用文件工具写
|
|
12
12
|
* memory 目录——透明、可检查、防膨胀。无 worker、无 SQLite、无端口。
|
|
@@ -17,54 +17,94 @@
|
|
|
17
17
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
18
18
|
import { homedir } from 'node:os'
|
|
19
19
|
import { readFile, readdir, writeFile, rename, unlink, stat } from 'node:fs/promises'
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { readFileSync } from 'node:fs'
|
|
21
|
+
import { join, basename, dirname } from 'node:path'
|
|
22
|
+
import { AutodreamEngine } from './autodream.js'
|
|
22
23
|
|
|
23
24
|
const MEMORY_DIR = process.env.SAGE_MEM_DIR || join(homedir(), '.sage-mem', 'memory')
|
|
24
25
|
/**
|
|
25
|
-
* DSH 会话记录根目录。
|
|
26
|
+
* DSH 会话记录根目录。autodream 的门控靠它数「上次整理之后有几个会话更新」,
|
|
26
27
|
* `memory+sessions` 输入源下也会在里面做定向搜索。取不到就是空串,
|
|
27
28
|
* 门控会退化成「会话数 0 → 不触发」,而不是报错。
|
|
28
29
|
*/
|
|
29
30
|
const SESSIONS_ROOT = process.env.DSH_HOME ? join(process.env.DSH_HOME, 'sessions') : ''
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
|
|
32
|
+
/** 读一个整数型环境变量并在范围内收敛;取不到或非法就用默认值。 */
|
|
33
|
+
function envInt(name, dflt, lo, hi) {
|
|
34
|
+
const raw = process.env[name]
|
|
35
|
+
if (raw === undefined || raw === '') return dflt
|
|
36
|
+
const n = Number(raw)
|
|
37
|
+
if (!Number.isFinite(n)) return dflt
|
|
38
|
+
return Math.max(lo, Math.min(hi, Math.round(n)))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 一次注入最多带几条记忆。
|
|
43
|
+
*
|
|
44
|
+
* 5 → 10(2026-10-01):**记忆条数 ÷ 注入名额 ≈ 命中率上限**,名额 5 时那个上限低到
|
|
45
|
+
* 的根因就在这个比值上。名额翻倍是提升召回最直接的一手,代价是每轮多约 7.5K
|
|
46
|
+
* 字符的上下文。**这个数字该由实测决定**,所以留了 SAGE_MEM_MAX_RESULTS 覆写口
|
|
47
|
+
* (比较两档的命中率与 token 账单,别拍脑袋)。
|
|
48
|
+
*/
|
|
49
|
+
const MAX_RESULTS = envInt('SAGE_MEM_MAX_RESULTS', 10, 1, 30)
|
|
50
|
+
|
|
51
|
+
/** 单条记忆注入的字符上限。 */
|
|
52
|
+
const MAX_CHARS_PER_FILE = envInt('SAGE_MEM_MAX_CHARS', 1500, 200, 20000)
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* baseline 名额。
|
|
56
|
+
*
|
|
57
|
+
* baseline 条目通常不多,卡在 5 只会让靠后的条目永不出现——
|
|
58
|
+
* 而 baseline 恰恰是短问题、恢复会话时唯一的兜底。现在放到 8,
|
|
59
|
+
* 排序见下面「按显式优先级排,不按文件名」那段。
|
|
60
|
+
*/
|
|
61
|
+
const MAX_BASELINE = envInt('SAGE_MEM_MAX_BASELINE', 8, 1, 50)
|
|
33
62
|
|
|
34
63
|
/**
|
|
35
|
-
*
|
|
64
|
+
* 结构性保留名:设置页的写/删方法一律不得触碰。
|
|
36
65
|
*
|
|
37
|
-
*
|
|
38
|
-
* 1. MEMORY.md
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* buildFrontmatter() 重建 frontmatter 的,原有内容一个字都留不下。
|
|
42
|
-
* 2. project_heartscape.md / project_self-cognition.md —— 人格连续性的手写载体
|
|
43
|
-
* (AGENTS.md 指定的落盘目标),单副本、纯人工维护;表单写还会把
|
|
44
|
-
* frontmatter 里的 originSessionId / birthday 等字段一并抹掉。
|
|
66
|
+
* 只列**结构决定必须保护**的两个:
|
|
67
|
+
* 1. `MEMORY.md` —— 索引。它被整份覆盖就等于索引没了(「+ 添加记忆」表单是照
|
|
68
|
+
* buildFrontmatter() 重建 frontmatter 的,原有内容一个字都留不下)。
|
|
69
|
+
* 2. `session-log.md` —— 追加型流水,体量最大,同样经不起一份重建。
|
|
45
70
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
71
|
+
* ⚠️ **除了这两个,别把任何具体记忆的文件名写死在这里。**
|
|
72
|
+
* 「哪几条记忆特别要紧」是**使用者的私事**——把它编进随包发布的代码,等于把作者的
|
|
73
|
+
* 记忆主题发给每一个装这个插件的人。
|
|
74
|
+
* 用户自己的保护名单放在 `<状态根>/reserved.json`(JSON 字符串数组),见下。
|
|
50
75
|
*
|
|
51
76
|
* ⚠️ 全部小写存放,比较一律走 nameKey()(NTFS 不区分大小写,集合里放 'MEMORY.md'
|
|
52
77
|
* 而拿 'memory.md' 去 has() 是查不到的 —— 曾经就是这个漏洞:表单里填 memory.md
|
|
53
|
-
*
|
|
78
|
+
* 就能整份覆盖索引,deleteFile 还能不可逆删掉它)。
|
|
54
79
|
*/
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
const STATE_ROOT = process.env.SAGE_MEM_STATE_DIR || join(dirname(MEMORY_DIR), '.sage-mem')
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 读使用者自己声明的额外保护名单:`<状态根>/reserved.json`,形如 `["a.md","b.md"]`。
|
|
84
|
+
*
|
|
85
|
+
* 读不到、JSON 坏、不是数组 —— 一律当空数组返回。**一份可选的保护名单,
|
|
86
|
+
* 绝不该有能力让插件加载失败。**
|
|
87
|
+
*/
|
|
88
|
+
function readExtraReserved() {
|
|
89
|
+
try {
|
|
90
|
+
const arr = JSON.parse(readFileSync(join(STATE_ROOT, 'reserved.json'), 'utf8'))
|
|
91
|
+
if (!Array.isArray(arr)) return []
|
|
92
|
+
return arr
|
|
93
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
94
|
+
.filter((s) => s.endsWith('.md') && !s.includes('/') && !s.includes('\\') && !s.startsWith('..'))
|
|
95
|
+
} catch {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const RESERVED_FILES = new Set(['memory.md', 'session-log.md', ...readExtraReserved()])
|
|
61
101
|
|
|
62
102
|
/**
|
|
63
103
|
* 非记忆条目:不参与检索 / 不进星图 / 不出现在设置页列表里,读写也一律不通。
|
|
64
104
|
*
|
|
65
105
|
* 是 RESERVED_FILES 的子集,区别在「列不列出来」:MEMORY.md 是索引、
|
|
66
|
-
* session-log.md
|
|
67
|
-
*
|
|
106
|
+
* session-log.md 是追加型流水,两者都不是一条记忆;
|
|
107
|
+
* 而使用者手写维护的重要记忆是正常记忆条目(它们该被检索到)。
|
|
68
108
|
* (要出现在列表与星图里、要能读),只是不许从表单写/删。
|
|
69
109
|
*/
|
|
70
110
|
const NON_ENTRY_FILES = new Set([
|
|
@@ -74,7 +114,7 @@ const NON_ENTRY_FILES = new Set([
|
|
|
74
114
|
|
|
75
115
|
/**
|
|
76
116
|
* 单文件体积上限(字节)。定 512 KB 的理由:
|
|
77
|
-
* -
|
|
117
|
+
* - 单条正文通常在几十 KB 以内,索引与流水都排除在外,
|
|
78
118
|
* 512 KB 是它的 26 倍,任何正常记忆都够用;
|
|
79
119
|
* - 记忆是要塞进 system prompt 的,一条 512 KB 的记忆本身就等于把上下文撑爆——
|
|
80
120
|
* 到这个量级基本可以判定是写错了目标(比如把日志、代码贴进来)。
|
|
@@ -139,7 +179,9 @@ async function scanMemoryFiles(ctx, sessionId) {
|
|
|
139
179
|
file: name,
|
|
140
180
|
content,
|
|
141
181
|
description: meta.description || '',
|
|
182
|
+
aliases: meta.aliases || [],
|
|
142
183
|
baseline: parseBaseline(content),
|
|
184
|
+
baselinePriority: meta.baselinePriority || 0,
|
|
143
185
|
}
|
|
144
186
|
} catch (err) {
|
|
145
187
|
// 只有 ENOENT 是正常状态(扫到读之间被删)。其余——权限错、编码坏、盘掉线——
|
|
@@ -159,16 +201,17 @@ async function scanMemoryFiles(ctx, sessionId) {
|
|
|
159
201
|
* 相关性打分只用 frontmatter 的 description + 文件名,不扫全文。
|
|
160
202
|
*
|
|
161
203
|
* 旧版拿 query 的双字去全文里找,得分是「覆盖率」,分母是 query,不惩罚
|
|
162
|
-
* 文件长度 ——
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* 反被挤掉。description 是「一句话说清这条是什么」的精准摘要,用它当检索
|
|
204
|
+
* 文件长度 —— 越长的记忆越容易蒙中。实测:长记忆在全文匹配下对任何问题都能
|
|
205
|
+
* 蒙中足量双字组,连寒暄问句都能注入上千 token;而真正该命中的那条反被
|
|
206
|
+
* 挤掉。description 是「一句话说清这条是什么」的精准摘要,用它当检索
|
|
166
207
|
* 信号,噪音大幅下降。
|
|
167
208
|
*/
|
|
168
209
|
function selectRelevant(query, files) {
|
|
169
210
|
return files
|
|
170
211
|
.map(f => {
|
|
171
|
-
|
|
212
|
+
// aliases 一并进检索面:它是专门为「换个说法也能命中」准备的字段。
|
|
213
|
+
const aliasText = Array.isArray(f.aliases) && f.aliases.length ? ' ' + f.aliases.join(' ') : ''
|
|
214
|
+
const hay = f.description + aliasText + ' ' + f.file.replace(/\.md$/, '').replace(/[_-]/g, ' ')
|
|
172
215
|
return { ...f, score: score(query, hay) }
|
|
173
216
|
})
|
|
174
217
|
.filter(f => f.score > 0)
|
|
@@ -176,6 +219,19 @@ function selectRelevant(query, files) {
|
|
|
176
219
|
.slice(0, MAX_RESULTS)
|
|
177
220
|
}
|
|
178
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 选 baseline 条目:按显式 `baselinePriority` 排,不按文件名。
|
|
224
|
+
*
|
|
225
|
+
* 文件名排序等于让 `feedback_*` 无条件压过 `user_*`,与「哪条更该出现在每场
|
|
226
|
+
* 会话开头」毫无关系。没写这个字段的老条目一律算 0,行为与从前一致。
|
|
227
|
+
*/
|
|
228
|
+
function selectBaseline(files) {
|
|
229
|
+
return files
|
|
230
|
+
.filter(f => f.baseline)
|
|
231
|
+
.sort((a, b) => (b.baselinePriority || 0) - (a.baselinePriority || 0) || a.file.localeCompare(b.file))
|
|
232
|
+
.slice(0, MAX_BASELINE)
|
|
233
|
+
}
|
|
234
|
+
|
|
179
235
|
/**
|
|
180
236
|
* 解析 frontmatter 的 `tags`(自定义标签,用于列表页筛选)。
|
|
181
237
|
*
|
|
@@ -208,9 +264,9 @@ function parseFrontmatter(content) {
|
|
|
208
264
|
// 换行必须容忍 CRLF:任何用 Windows 编辑器碰过一次的文件都会被换成 \r\n,
|
|
209
265
|
// 而旧版这里写的是 /^---\n/ —— 匹配失败 → description/type 全空 →
|
|
210
266
|
// 「baseline 走 parseBaseline 那条带 \r?\n 的正则照旧命中,但这条记忆永远检索不到」,
|
|
211
|
-
//
|
|
267
|
+
// 症状极具欺骗性。下文定义的 parseBaseline 与 extractTitle 早已是 \r?\n。
|
|
212
268
|
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
213
|
-
if (!m) return { name: '', description: '', type: '', tags: [] }
|
|
269
|
+
if (!m) return { name: '', description: '', type: '', tags: [], aliases: [], baselinePriority: 0 }
|
|
214
270
|
const body = m[1]
|
|
215
271
|
const grab = (key) => {
|
|
216
272
|
// [ \t] / [ \t\r] 而不是 \s:\s 会连 CRLF 的 \r 一起吃进捕获组,值尾留一个 \r
|
|
@@ -224,7 +280,52 @@ function parseFrontmatter(content) {
|
|
|
224
280
|
const nested = body.match(/^metadata:\s*\r?\n(?:\s+[^\r\n]+\r?\n)*?\s+type:\s*(\S+)/m)
|
|
225
281
|
if (nested) type = nested[1]
|
|
226
282
|
}
|
|
227
|
-
return {
|
|
283
|
+
return {
|
|
284
|
+
name: grab('name'),
|
|
285
|
+
description: grab('description'),
|
|
286
|
+
type,
|
|
287
|
+
tags: parseTags(body),
|
|
288
|
+
aliases: parseAliases(body),
|
|
289
|
+
baselinePriority: parseBaselinePriority(body),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 解析 frontmatter 的 `aliases`(别称 / 同义词 / 我可能用的另一种叫法)。
|
|
295
|
+
*
|
|
296
|
+
* 为什么要有它:检索是**词面重合**的,不是语义的——问「我有什么待办」而文件里
|
|
297
|
+
* 写的是「代办清单」,差一个字就完全不命中。`description` 回答「我会怎么问它」,
|
|
298
|
+
* `aliases` 补上「我还可能怎么问」。写法与 tags 完全一致(flow / block / 逗号串三种都收),
|
|
299
|
+
* 免得手写时「时灵时不灵」。
|
|
300
|
+
*/
|
|
301
|
+
function parseAliases(body) {
|
|
302
|
+
const clean = (arr) =>
|
|
303
|
+
arr
|
|
304
|
+
.map((s) => String(s).trim().replace(/^["']|["']$/g, '').trim())
|
|
305
|
+
.filter(Boolean)
|
|
306
|
+
.slice(0, 24)
|
|
307
|
+
.map((s) => s.slice(0, 60))
|
|
308
|
+
const flow = body.match(/^aliases:[ \t]*\[([^\]]*)\]/m)
|
|
309
|
+
if (flow) return clean(flow[1].split(','))
|
|
310
|
+
const block = body.match(/^aliases:[ \t]*\r?\n((?:[ \t]+-[^\r\n]*\r?\n?)+)/m)
|
|
311
|
+
if (block) return clean(block[1].split(/\r?\n/).map((l) => l.replace(/^[ \t]*-[ \t]*/, '')))
|
|
312
|
+
const inline = body.match(/^aliases:[ \t]*(.+?)[ \t\r]*$/m)
|
|
313
|
+
if (inline && inline[1].trim()) return clean(inline[1].split(','))
|
|
314
|
+
return []
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 解析 frontmatter 的 `baselinePriority`(数字,越大越先注入)。
|
|
319
|
+
*
|
|
320
|
+
* 旧版 baseline 是 `MAX_BASELINE = 5` + **按文件名排序**——`feedback_*` 永远排在
|
|
321
|
+
* `user_*` 前面,与「哪条真的更该在每场会话开头出现」毫无关系。缺失当 0,
|
|
322
|
+
* 于是没写这个字段的老条目行为不变。
|
|
323
|
+
*/
|
|
324
|
+
function parseBaselinePriority(body) {
|
|
325
|
+
const hit = body.match(/^baselinePriority:[ \t]*(.+?)[ \t\r]*$/m)
|
|
326
|
+
if (!hit) return 0
|
|
327
|
+
const n = Number(hit[1].replace(/^["']|["']$/g, '').trim())
|
|
328
|
+
return Number.isFinite(n) ? n : 0
|
|
228
329
|
}
|
|
229
330
|
|
|
230
331
|
/** 解析 frontmatter 的 `baseline` 标记:会话第一回合无条件注入。 */
|
|
@@ -286,7 +387,7 @@ function isMemoryEntry(name) {
|
|
|
286
387
|
*
|
|
287
388
|
* MemoryGateway.readFile / StarmapGateway.readFile 都走这里,不再各写一套
|
|
288
389
|
* —— 旧版 starmap 那套内联校验只排除了 'MEMORY.md'(大小写敏感)且**完全不排除
|
|
289
|
-
* session-log.md**,于是 `starmap.readFile('session-log.md')`
|
|
390
|
+
* session-log.md**,于是 `starmap.readFile('session-log.md')` 能把整份流水
|
|
290
391
|
* 整个拉进浏览器。
|
|
291
392
|
*
|
|
292
393
|
* 不复用 nameKey 的原因:这里要保留磁盘上的原始大小写(返回值直接 join 进路径)。
|
|
@@ -325,22 +426,26 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
325
426
|
// 一并挂载星图 remote(同一记忆目录,纯只读)—— starmap.* 服务
|
|
326
427
|
new StarmapGateway(ctx)
|
|
327
428
|
|
|
328
|
-
//
|
|
429
|
+
// autodream(夜间记忆整理):引擎 + remote 面 + 自动触发的定时器。
|
|
329
430
|
//
|
|
330
|
-
// 整个包在 try 里:
|
|
431
|
+
// 整个包在 try 里:autodream 起不来(比如 timer 服务缺失、状态目录没权限)
|
|
331
432
|
// 不该把 sage-mem 最核心的那件事——「记忆按问题注入」——一起拖死。
|
|
332
433
|
// 这是本插件唯一一处「降级而不是失败」的设计,理由就在这里。
|
|
333
434
|
try {
|
|
334
|
-
const engine = new
|
|
335
|
-
|
|
435
|
+
const engine = new AutodreamEngine(ctx, {
|
|
436
|
+
memoryDir: MEMORY_DIR,
|
|
437
|
+
sessionsRoot: SESSIONS_ROOT,
|
|
438
|
+
reservedFiles: [...RESERVED_FILES],
|
|
439
|
+
})
|
|
440
|
+
new AutodreamGateway(ctx, engine)
|
|
336
441
|
engine.start()
|
|
337
442
|
// 卸载时把定时器与锁收干净。effect 拿不到就交给 unref 兜底(不阻止进程退出)。
|
|
338
443
|
if (typeof ctx.effect === 'function') ctx.effect(() => () => engine.dispose())
|
|
339
444
|
} catch (err) {
|
|
340
|
-
warnLog(ctx, `
|
|
445
|
+
warnLog(ctx, `autodream 初始化失败(记忆本体不受影响):${err?.message ?? err}`)
|
|
341
446
|
// 异常路径额外打一行到 stderr:warnLog 走 ctx.logger,某些启动阶段它还没接上,
|
|
342
|
-
// 而「
|
|
343
|
-
console.error('[sage-mem]
|
|
447
|
+
// 而「autodream 静默不工作」正是最难查的那种故障(2026-09-27 为此绕了一大圈)。
|
|
448
|
+
console.error('[sage-mem] autodream init failed:', err && err.stack ? err.stack : err)
|
|
344
449
|
}
|
|
345
450
|
|
|
346
451
|
// 同一轮对话里问题不变、注入内容也不变,但 DSH 每一步都会往 session 追加
|
|
@@ -398,7 +503,7 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
398
503
|
// baseline:每个会话注入一次,**不看消息条数**。
|
|
399
504
|
//
|
|
400
505
|
// 旧写法 `if (userMessages.length === 0)` 依赖一个真实存在的时序:assemble
|
|
401
|
-
// 触发时当前这条用户消息还没写进 session。已在 DSH
|
|
506
|
+
// 触发时当前这条用户消息还没写进 session。已在 DSH 0.1.7-rc.1 上核实为真 ——
|
|
402
507
|
// dsh-agent-loop/lib/index.js:906 const claimed = this.inbox.claim(...) // 只从 pending 队列取走
|
|
403
508
|
// dsh-agent-loop/lib/index.js:907 await this.loopCtx.systemPrompt.assemble(...) // ← 本监听器在这里跑
|
|
404
509
|
// dsh-agent-loop/lib/index.js:1046 this.session.append("user/message", ...) // 落 session 在 preStep 之后
|
|
@@ -410,10 +515,7 @@ export class MemoryGateway extends TypertRemoteService {
|
|
|
410
515
|
let baselineTaken = 0
|
|
411
516
|
const baselineKey = session.id ? session.id : session
|
|
412
517
|
if (!baselineInjected.has(baselineKey)) {
|
|
413
|
-
files
|
|
414
|
-
.filter(f => f.baseline)
|
|
415
|
-
.sort((a, b) => a.file.localeCompare(b.file))
|
|
416
|
-
.slice(0, MAX_BASELINE)
|
|
518
|
+
selectBaseline(files)
|
|
417
519
|
.forEach(f => {
|
|
418
520
|
const before = picked.length
|
|
419
521
|
take(f)
|
|
@@ -610,14 +712,14 @@ markRemote(StarmapGateway, 'listStars', 'listStars')
|
|
|
610
712
|
markRemote(StarmapGateway, 'readFile', 'readFile')
|
|
611
713
|
|
|
612
714
|
/**
|
|
613
|
-
*
|
|
715
|
+
* autodream 的 remote 面:设置页「自动做梦」标签页唯一的数据通道。
|
|
614
716
|
*
|
|
615
|
-
* 这一层刻意很薄——全部判断(门控、模式、白名单)都在
|
|
616
|
-
* 这里只负责把 ctx 里的引擎接到 typert 的调用表上。想看行为就读
|
|
717
|
+
* 这一层刻意很薄——全部判断(门控、模式、白名单)都在 AutodreamEngine 里,
|
|
718
|
+
* 这里只负责把 ctx 里的引擎接到 typert 的调用表上。想看行为就读 autodream.js。
|
|
617
719
|
*/
|
|
618
|
-
export class
|
|
720
|
+
export class AutodreamGateway extends TypertRemoteService {
|
|
619
721
|
constructor(ctx, engine) {
|
|
620
|
-
super(ctx, '
|
|
722
|
+
super(ctx, 'autodream')
|
|
621
723
|
this.engine = engine
|
|
622
724
|
}
|
|
623
725
|
|
|
@@ -641,14 +743,15 @@ export class DreamGateway extends TypertRemoteService {
|
|
|
641
743
|
*
|
|
642
744
|
* force 默认为 true —— 人点了按钮,就不该再被「还没到 24 小时」挡回来。
|
|
643
745
|
*
|
|
644
|
-
* **故意不 await**:一趟
|
|
746
|
+
* **故意不 await**:一趟 autodream 可能跑几分钟(多轮模型调用 + 工具),
|
|
645
747
|
* remote 调用挂在那儿等会直接撞上前端超时,界面上表现为「点了没反应」。
|
|
646
748
|
* 所以这里只负责点火,进度与结果都从 status() 轮询 —— run() 自己会把
|
|
647
749
|
* lastResult 落盘,界面刷新几次就能看到。
|
|
648
750
|
*/
|
|
649
|
-
async runNow(
|
|
650
|
-
if (this.engine.running) return { ok: false, error: '已有一趟
|
|
651
|
-
|
|
751
|
+
async runNow(opts) {
|
|
752
|
+
if (this.engine.running) return { ok: false, error: '已有一趟 autodream 在跑' }
|
|
753
|
+
const reason = typeof opts?.reason === 'string' && opts.reason.trim() ? opts.reason.trim().slice(0, 120) : '设置页手动触发'
|
|
754
|
+
this.engine.run({ reason, force: true }).catch(() => {})
|
|
652
755
|
return { ok: true }
|
|
653
756
|
}
|
|
654
757
|
|
|
@@ -666,14 +769,63 @@ export class DreamGateway extends TypertRemoteService {
|
|
|
666
769
|
async listSnapshots() {
|
|
667
770
|
return this.engine.listSnapshots()
|
|
668
771
|
}
|
|
772
|
+
|
|
773
|
+
/** 运行记录列表(面板的「整理声明」区)。 */
|
|
774
|
+
async listRuns(opts) {
|
|
775
|
+
return this.engine.listRuns(opts?.limit)
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/** 读某次运行的完整整理声明(人读 markdown + 机读 manifest)。 */
|
|
779
|
+
async readDeclaration(opts) {
|
|
780
|
+
return this.engine.readDeclaration(opts?.runId)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* 回滚到某个回滚点。
|
|
785
|
+
*
|
|
786
|
+
* 与 runNow 相反,这里**必须 await**:回滚是几十毫秒级的文件操作,
|
|
787
|
+
* 而「点完按钮不知道到底滚没滚」正是回滚这种动作最不能接受的体验。
|
|
788
|
+
*/
|
|
789
|
+
async rollback(opts) {
|
|
790
|
+
return this.engine.rollback({ snapshotId: opts?.snapshotId, scope: opts?.scope })
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** 可选的整理模型路线(设置面板的模型下拉)。 */
|
|
794
|
+
async listModels() {
|
|
795
|
+
return this.engine.listModels()
|
|
796
|
+
}
|
|
669
797
|
}
|
|
670
798
|
|
|
671
|
-
markRemote(
|
|
672
|
-
markRemote(
|
|
673
|
-
markRemote(
|
|
674
|
-
markRemote(
|
|
675
|
-
markRemote(
|
|
676
|
-
markRemote(
|
|
677
|
-
markRemote(
|
|
799
|
+
markRemote(AutodreamGateway, 'getConfig', 'getConfig')
|
|
800
|
+
markRemote(AutodreamGateway, 'setConfig', 'setConfig')
|
|
801
|
+
markRemote(AutodreamGateway, 'status', 'status')
|
|
802
|
+
markRemote(AutodreamGateway, 'runNow', 'runNow')
|
|
803
|
+
markRemote(AutodreamGateway, 'listReports', 'listReports')
|
|
804
|
+
markRemote(AutodreamGateway, 'readReport', 'readReport')
|
|
805
|
+
markRemote(AutodreamGateway, 'listSnapshots', 'listSnapshots')
|
|
806
|
+
markRemote(AutodreamGateway, 'listRuns', 'listRuns')
|
|
807
|
+
markRemote(AutodreamGateway, 'readDeclaration', 'readDeclaration')
|
|
808
|
+
markRemote(AutodreamGateway, 'rollback', 'rollback')
|
|
809
|
+
markRemote(AutodreamGateway, 'listModels', 'listModels')
|
|
678
810
|
|
|
679
811
|
export default MemoryGateway
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* **仅供测试**的只读出口。
|
|
815
|
+
*
|
|
816
|
+
* 检索这条链(frontmatter 解析 → 双字打分 → 名额裁剪)没有任何对外可见的调用面:
|
|
817
|
+
* 想验证「写了 aliases 之后换个说法能不能命中」,否则只能起一个真的 DSH 端到端跑一遍。
|
|
818
|
+
* 把这些纯函数露出来是为了能写断言,**不是给外部用的 API**,别依赖。
|
|
819
|
+
*/
|
|
820
|
+
export const __testables = {
|
|
821
|
+
bigrams,
|
|
822
|
+
score,
|
|
823
|
+
parseFrontmatter,
|
|
824
|
+
parseAliases,
|
|
825
|
+
parseBaselinePriority,
|
|
826
|
+
parseBaseline,
|
|
827
|
+
parseTags,
|
|
828
|
+
selectRelevant,
|
|
829
|
+
selectBaseline,
|
|
830
|
+
limits: { MAX_RESULTS, MAX_CHARS_PER_FILE, MAX_BASELINE },
|
|
831
|
+
}
|