dsh-lite-memory 0.18.42 → 0.18.44
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/docs/BENCH.md +14 -0
- package/docs/RELEASE-0.18.43.md +18 -0
- package/docs/RELEASE-0.18.44.md +21 -0
- package/package.json +3 -3
- package/state.js +2 -1
- package/test/anchor.test.mjs +0 -795
- package/test/client.test.mjs +0 -400
- package/test/contract.test.mjs +0 -1342
- package/test/control.test.mjs +0 -324
- package/test/crossing.test.mjs +0 -471
- package/test/eval-seed.test.mjs +0 -113
- package/test/evolution-read.test.mjs +0 -658
- package/test/fixtures/legacy-v1/archive/mem_444444444444.json +0 -18
- package/test/fixtures/legacy-v1/entries/mem_111111111111.md +0 -2
- package/test/fixtures/legacy-v1/entries/mem_222222222222.md +0 -1
- package/test/fixtures/legacy-v1/entries/mem_333333333333.md +0 -1
- package/test/fixtures/legacy-v1/index.json +0 -63
- package/test/ha.test.mjs +0 -186
- package/test/layer-read.test.mjs +0 -293
- package/test/learning.test.mjs +0 -1450
- package/test/lexical.test.mjs +0 -68
- package/test/palace.test.mjs +0 -104
- package/test/recall.test.mjs +0 -1039
- package/test/record.test.mjs +0 -130
- package/test/resident.test.mjs +0 -46
- package/test/runtime-context.test.mjs +0 -192
- package/test/safety.test.mjs +0 -119
- package/test/scenario.test.mjs +0 -184
- package/test/state.test.mjs +0 -663
- package/test/store.test.mjs +0 -1487
- package/test/view.test.mjs +0 -264
|
@@ -1,658 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-evolution-read-'));
|
|
7
|
-
const { apply } = await import('../index.js');
|
|
8
|
-
const store = await import('../store.js');
|
|
9
|
-
const state = await import('../state.js');
|
|
10
|
-
const anchor = await import('../anchor.js');
|
|
11
|
-
const view = await import('../view.js');
|
|
12
|
-
|
|
13
|
-
const effect = (fn) => {
|
|
14
|
-
const dispose = fn();
|
|
15
|
-
return typeof dispose === 'function' ? dispose : () => {};
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
function boot(mode = 'adaptive') {
|
|
19
|
-
const defs = new Map();
|
|
20
|
-
const handlers = new Map();
|
|
21
|
-
const promptContexts = [];
|
|
22
|
-
const ctx = {
|
|
23
|
-
tools: { register: (def) => { defs.set(def.name, def); return () => {}; } },
|
|
24
|
-
effect,
|
|
25
|
-
inject: () => () => {},
|
|
26
|
-
get: (name) => (name === 'systemPrompt'
|
|
27
|
-
? { context: (entry) => { promptContexts.push(entry); return () => {}; } }
|
|
28
|
-
: name === 'agents' ? { currentInitiator: () => null } : undefined),
|
|
29
|
-
on: (event, handler) => {
|
|
30
|
-
const list = handlers.get(event) || [];
|
|
31
|
-
list.push(handler);
|
|
32
|
-
handlers.set(event, list);
|
|
33
|
-
return () => {};
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
apply(ctx, { mode });
|
|
37
|
-
const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
|
|
38
|
-
assert.ok(provider, 'recall provider registered');
|
|
39
|
-
/* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
|
|
40
|
-
globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
|
|
41
|
-
const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
|
|
42
|
-
const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
|
|
43
|
-
const text = (agent, signal) => provider.text({ agent, signal });
|
|
44
|
-
return { defs, handlers, provider, exec, preStep, text, debug: globalThis.__liteMemoryRecall };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const signal = () => new AbortController().signal;
|
|
48
|
-
let seq = 0;
|
|
49
|
-
const human = (body) => {
|
|
50
|
-
seq += 1;
|
|
51
|
-
return { role: 'user', id: `msg-${seq}`, source: { kind: 'user' }, content: [{ type: 'text', text: body }] };
|
|
52
|
-
};
|
|
53
|
-
const enter = (...messages) => ({ kind: 'enter', messages });
|
|
54
|
-
const mkdir = (label) => fs.mkdtempSync(path.join(os.tmpdir(), `lite-memory-evolution-${label}-`));
|
|
55
|
-
const makeAgent = (id, dir) => ({ id, cwd: dir, session: { id: `session-${id}`, header: { cwd: dir } } });
|
|
56
|
-
|
|
57
|
-
const save = (scope, fields) => {
|
|
58
|
-
const result = store.upsert(scope, {
|
|
59
|
-
applicability: 'all_in_scope',
|
|
60
|
-
body: `正文:${fields.title}`,
|
|
61
|
-
resolution: 'distinct',
|
|
62
|
-
...fields,
|
|
63
|
-
summary: fields.summary ?? `${fields.title}的摘要`,
|
|
64
|
-
});
|
|
65
|
-
return { ...result, id: result.record?.id ?? result.id };
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/** 直接种一条实验来源条目,绕开 PR4 的整理入口。 */
|
|
69
|
-
function seedRule(scope, { title, body, summary, conditions = [], learning = { state: 'active', specDigest: 'seeded' }, topicKey, tags = [] }) {
|
|
70
|
-
const saved = save(scope, { title, body, summary, tags });
|
|
71
|
-
state.transact(scope, {}, (draft) => {
|
|
72
|
-
const row = draft.entries.find((entry) => entry.id === saved.id);
|
|
73
|
-
row.origin = 'project_evolution';
|
|
74
|
-
row.current.conditions = conditions;
|
|
75
|
-
row.maintenance.learning = { ...learning };
|
|
76
|
-
if (topicKey) row.topicKey = topicKey;
|
|
77
|
-
return { changed: true };
|
|
78
|
-
});
|
|
79
|
-
return saved.id;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const enable = (scope, defaults) => {
|
|
83
|
-
const turned = store.setEvolution(scope, true, { operationId: `op-on-${Math.random().toString(16).slice(2)}` });
|
|
84
|
-
assert.equal(turned.ok, true, turned.reason);
|
|
85
|
-
assert.equal(turned.enabled, true);
|
|
86
|
-
if (defaults) {
|
|
87
|
-
const applied = store.setTaskDefault(scope, defaults);
|
|
88
|
-
assert.equal(applied.ok, true, applied.reason);
|
|
89
|
-
}
|
|
90
|
-
return turned;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const scopeOf = (dir) => store.scopeOf(dir);
|
|
94
|
-
|
|
95
|
-
/* L01:默认关闭时,自动读取与普通召回完全不动。 */
|
|
96
|
-
{
|
|
97
|
-
const dir = mkdir('off');
|
|
98
|
-
const scope = scopeOf(dir);
|
|
99
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
100
|
-
const runtime = boot();
|
|
101
|
-
const agent = makeAgent('a-off', dir);
|
|
102
|
-
const sig = signal();
|
|
103
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
|
|
104
|
-
assert.equal(runtime.text(agent, sig), '');
|
|
105
|
-
assert.equal(runtime.debug.windowOf(agent).experiment, null);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/* L02:开关打开但没有任何栏目,不注入任何领域问题,也不给空上下文。 */
|
|
109
|
-
{
|
|
110
|
-
const dir = mkdir('clarify');
|
|
111
|
-
const scope = scopeOf(dir);
|
|
112
|
-
enable(scope);
|
|
113
|
-
const runtime = boot();
|
|
114
|
-
const agent = makeAgent('a-clarify', dir);
|
|
115
|
-
const sig = signal();
|
|
116
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期怎么开头')));
|
|
117
|
-
const text = runtime.text(agent, sig);
|
|
118
|
-
assert.equal(text, '');
|
|
119
|
-
assert.doesNotMatch(text, /栏目|Vlog|口播|视频/);
|
|
120
|
-
const experiment = runtime.debug.windowOf(agent).experiment;
|
|
121
|
-
assert.equal(experiment ? experiment.injectedIds.length : 0, 0);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/* L03:未绑定任务只给栏目索引,不超过三条;短规则不进正文。 */
|
|
125
|
-
{
|
|
126
|
-
const dir = mkdir('index');
|
|
127
|
-
const scope = scopeOf(dir);
|
|
128
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播视频' }] });
|
|
129
|
-
seedRule(scope, { title: '口播收尾', body: '结尾只留一个行动。', conditions: [{ key: 'seriesId', value: '口播视频' }] });
|
|
130
|
-
seedRule(scope, { title: 'Vlog 节奏', body: '每段不超过二十秒。', conditions: [{ key: 'seriesId', value: 'Vlog' }] });
|
|
131
|
-
enable(scope);
|
|
132
|
-
const runtime = boot();
|
|
133
|
-
const agent = makeAgent('a-index', dir);
|
|
134
|
-
const sig = signal();
|
|
135
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('视频开头怎么改')));
|
|
136
|
-
const text = runtime.text(agent, sig);
|
|
137
|
-
assert.match(text, /这些记忆范围可能适用/);
|
|
138
|
-
assert.doesNotMatch(text, /Vlog/);
|
|
139
|
-
assert.doesNotMatch(text, /开场三句内给结论/);
|
|
140
|
-
const listed = text.split('\n').filter((line) => line.startsWith('- '));
|
|
141
|
-
assert.equal(listed.length <= 3, true);
|
|
142
|
-
assert.equal(Buffer.byteLength(text) <= 600 + 160, true, 'index stays small');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/* L03b:项目有视频栏目,本轮是修代码:无关栏目与选择提示都不进上下文。 */
|
|
146
|
-
{
|
|
147
|
-
const dir = mkdir('index-unrelated');
|
|
148
|
-
const scope = scopeOf(dir);
|
|
149
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
150
|
-
seedRule(scope, { title: 'Vlog 节奏', body: '每段不超过二十秒。', conditions: [{ key: 'seriesId', value: 'Vlog' }] });
|
|
151
|
-
enable(scope);
|
|
152
|
-
const runtime = boot();
|
|
153
|
-
const agent = makeAgent('a-index-unrelated', dir);
|
|
154
|
-
const sig = signal();
|
|
155
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('修一下 dsh-unlag 的重新载入问题')));
|
|
156
|
-
const injected = runtime.text(agent, sig);
|
|
157
|
-
assert.equal(injected, '');
|
|
158
|
-
const experiment = runtime.debug.windowOf(agent).experiment;
|
|
159
|
-
assert.equal(experiment ? experiment.injectedIds.length : 0, 0);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/* L04:项目默认绑定后给规则正文,带 id 与版本。 */
|
|
163
|
-
{
|
|
164
|
-
const dir = mkdir('default');
|
|
165
|
-
const scope = scopeOf(dir);
|
|
166
|
-
const id = seedRule(scope, { title: '默认口播开头', body: '先给结论再解释。', conditions: [{ key: 'seriesId', value: '口播' }, { key: 'taskKind', value: 'video' }] });
|
|
167
|
-
enable(scope, { taskKind: 'video', seriesId: '口播' });
|
|
168
|
-
const runtime = boot();
|
|
169
|
-
const agent = makeAgent('a-default', dir);
|
|
170
|
-
const sig = signal();
|
|
171
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期开头怎么改')));
|
|
172
|
-
const text = runtime.text(agent, sig);
|
|
173
|
-
assert.match(text, /项目实验规则/);
|
|
174
|
-
assert.match(text, /先给结论再解释/);
|
|
175
|
-
assert.ok(!text.includes(id), '实验规则也只给标题,不给内部编号');
|
|
176
|
-
assert.doesNotMatch(text, /第 .* 版/);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/* L05:长规则只给用途与 memory_read 指针,不把正文塞进上下文。 */
|
|
180
|
-
{
|
|
181
|
-
const dir = mkdir('long');
|
|
182
|
-
const scope = scopeOf(dir);
|
|
183
|
-
const longBody = `完整流程:${'先做这一步再检查,'.repeat(30)}`;
|
|
184
|
-
const id = seedRule(scope, { title: '长流程口播', body: longBody, summary: '完整流程按顺序做', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
185
|
-
enable(scope, { seriesId: '口播' });
|
|
186
|
-
const runtime = boot();
|
|
187
|
-
const agent = makeAgent('a-long', dir);
|
|
188
|
-
const sig = signal();
|
|
189
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播怎么做')));
|
|
190
|
-
const text = runtime.text(agent, sig);
|
|
191
|
-
assert.ok(!text.includes(id), '实验规则也只给标题,不给内部编号');
|
|
192
|
-
assert.match(text, /memory_read/);
|
|
193
|
-
assert.equal(text.includes('先做这一步再检查'), false);
|
|
194
|
-
assert.equal(Buffer.byteLength(text) <= 1200 + 200, true);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/* L06:道谢不注入规则,只留一个可续的指针。 */
|
|
198
|
-
{
|
|
199
|
-
const dir = mkdir('thanks');
|
|
200
|
-
const scope = scopeOf(dir);
|
|
201
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
202
|
-
enable(scope, { seriesId: '口播' });
|
|
203
|
-
const runtime = boot();
|
|
204
|
-
const agent = makeAgent('a-thanks', dir);
|
|
205
|
-
const sig = signal();
|
|
206
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('谢谢!')));
|
|
207
|
-
const text = runtime.text(agent, sig);
|
|
208
|
-
assert.equal(text.includes('开场三句内给结论'), false);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/* L07:再做一期是新的一集,继承栏目与风格,不继承旧主题。 */
|
|
212
|
-
{
|
|
213
|
-
const dir = mkdir('continue');
|
|
214
|
-
const scope = scopeOf(dir);
|
|
215
|
-
seedRule(scope, { title: '风格规则', body: '口播全程用短句。', conditions: [{ key: 'seriesId', value: '口播' }, { key: 'phase', value: 'brief' }] });
|
|
216
|
-
seedRule(scope, { title: '本期主题', body: '这一期讲春季收纳。', conditions: [{ key: 'seriesId', value: '口播' }, { key: 'phase', value: 'theme' }] });
|
|
217
|
-
enable(scope, { seriesId: '口播' });
|
|
218
|
-
const selected = anchor.applySelection(store.taskState(scope), {
|
|
219
|
-
sessionId: 'session-a-continue',
|
|
220
|
-
selection: { seriesId: '口播', phase: 'theme' },
|
|
221
|
-
batchKey: anchor.requestBatchKey(['seed-theme']),
|
|
222
|
-
messageIds: ['seed-theme'],
|
|
223
|
-
});
|
|
224
|
-
const seededEpisode = selected.tasks.episodes[selected.episode.episodeId];
|
|
225
|
-
seededEpisode.status = 'active';
|
|
226
|
-
selected.tasks.sessions['session-a-continue'].status = 'active';
|
|
227
|
-
assert.equal(store.saveTaskState(scope, selected.tasks, { operationId: 'op-seed-theme' }).ok, true);
|
|
228
|
-
const runtime = boot();
|
|
229
|
-
const agent = makeAgent('a-continue', dir);
|
|
230
|
-
const first = signal();
|
|
231
|
-
await runtime.preStep({ agent, signal: first, turn: 't1' }, enter(human('这期讲春季收纳')));
|
|
232
|
-
assert.match(runtime.text(agent, first), /这一期讲春季收纳/);
|
|
233
|
-
const second = signal();
|
|
234
|
-
await runtime.preStep({ agent, signal: second, turn: 't2' }, enter(human('再做一期')));
|
|
235
|
-
const text = runtime.text(agent, second);
|
|
236
|
-
assert.match(text, /口播全程用短句/);
|
|
237
|
-
assert.equal(text.includes('这一期讲春季收纳'), false);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/* L08:改一版沿用同一集,不新建集。 */
|
|
241
|
-
{
|
|
242
|
-
const dir = mkdir('revision');
|
|
243
|
-
const scope = scopeOf(dir);
|
|
244
|
-
enable(scope, { seriesId: '口播' });
|
|
245
|
-
const runtime = boot();
|
|
246
|
-
const agent = makeAgent('a-revision', dir);
|
|
247
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播开头快点')));
|
|
248
|
-
const before = store.taskState(scope).episodes;
|
|
249
|
-
const beforeIds = Object.keys(before);
|
|
250
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't2' }, enter(human('再改一下开头')));
|
|
251
|
-
const after = store.taskState(scope).episodes;
|
|
252
|
-
assert.deepEqual(Object.keys(after).sort(), beforeIds.sort());
|
|
253
|
-
assert.equal(Object.values(after)[0].status, 'continuable');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/* L09:改做别的类型不把视频规则带过去。 */
|
|
257
|
-
{
|
|
258
|
-
const dir = mkdir('switch');
|
|
259
|
-
const scope = scopeOf(dir);
|
|
260
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
261
|
-
enable(scope, { seriesId: '口播' });
|
|
262
|
-
const runtime = boot();
|
|
263
|
-
const agent = makeAgent('a-switch', dir);
|
|
264
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播开头')));
|
|
265
|
-
const sig = signal();
|
|
266
|
-
await runtime.preStep({ agent, signal: sig, turn: 't2' }, enter(human('改做 README 文案')));
|
|
267
|
-
const text = runtime.text(agent, sig);
|
|
268
|
-
assert.equal(text.includes('开场三句内给结论'), false);
|
|
269
|
-
const current = Object.values(store.taskState(scope).episodes).find((episode) => episode.status === 'unbound');
|
|
270
|
-
assert.ok(current, 'switch leaves an unbound episode');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/* L09b:明确改为排查数据库后,旧视频规则和栏目提示都不再注入。 */
|
|
274
|
-
{
|
|
275
|
-
const dir = mkdir('switch-db');
|
|
276
|
-
const scope = scopeOf(dir);
|
|
277
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
278
|
-
enable(scope, { seriesId: '口播' });
|
|
279
|
-
const runtime = boot();
|
|
280
|
-
const agent = makeAgent('a-switch-db', dir);
|
|
281
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播开头')));
|
|
282
|
-
const sig = signal();
|
|
283
|
-
await runtime.preStep({ agent, signal: sig, turn: 't2' }, enter(human('这期不做了,改为排查 PostgreSQL 慢查询')));
|
|
284
|
-
const text = runtime.text(agent, sig);
|
|
285
|
-
assert.equal(text.includes('开场三句内给结论'), false);
|
|
286
|
-
assert.equal(/栏目|点名/.test(text), false);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/* L10:没有本请求上下文时,普通检索看不到实验来源。 */
|
|
290
|
-
{
|
|
291
|
-
const dir = mkdir('nocx');
|
|
292
|
-
const scope = scopeOf(dir);
|
|
293
|
-
const id = seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
294
|
-
enable(scope, { seriesId: '口播' });
|
|
295
|
-
const runtime = boot();
|
|
296
|
-
const agent = makeAgent('a-nocx', dir);
|
|
297
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播开头')));
|
|
298
|
-
const value = await runtime.exec('memory_search', { query: '口播开头' }, agent);
|
|
299
|
-
assert.equal((value.refs || []).some((ref) => ref.id === id), false);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/* L11:真人原话点名栏目就直接生效;工具绑定仍只认这句原话。 */
|
|
303
|
-
{
|
|
304
|
-
const dir = mkdir('bind');
|
|
305
|
-
const scope = scopeOf(dir);
|
|
306
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
307
|
-
enable(scope);
|
|
308
|
-
const runtime = boot();
|
|
309
|
-
const agent = makeAgent('a-bind', dir);
|
|
310
|
-
const sig = signal();
|
|
311
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播怎么开头')));
|
|
312
|
-
const held = runtime.debug.windowOf(agent).experiment;
|
|
313
|
-
assert.ok(held && held.contextId, 'context leaked');
|
|
314
|
-
const value = await runtime.exec('memory_search', { query: '口播', contextId: held.contextId, bindForRequest: true }, agent);
|
|
315
|
-
assert.equal(value.status, 'ok');
|
|
316
|
-
assert.ok((value.text || '').includes('口播'));
|
|
317
|
-
const episodes = Object.values(store.taskState(scope).episodes);
|
|
318
|
-
assert.equal(episodes.length, 1);
|
|
319
|
-
assert.equal(episodes[0].status, 'active');
|
|
320
|
-
assert.match(runtime.text(agent, sig), /开场三句内给结论/);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/* L11b:模型自己写 query 不能建绑定;只认本轮真人原话。 */
|
|
324
|
-
{
|
|
325
|
-
const dir = mkdir('bind-model');
|
|
326
|
-
const scope = scopeOf(dir);
|
|
327
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
328
|
-
enable(scope);
|
|
329
|
-
const runtime = boot();
|
|
330
|
-
const agent = makeAgent('a-bind-model', dir);
|
|
331
|
-
const sig = signal();
|
|
332
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期怎么开头')));
|
|
333
|
-
const held = runtime.debug.windowOf(agent).experiment;
|
|
334
|
-
const value = await runtime.exec('memory_search', { query: '口播', contextId: held.contextId, bindForRequest: true }, agent);
|
|
335
|
-
assert.equal(value.status, 'needs_context');
|
|
336
|
-
assert.equal((value.text || '').includes('口播'), false);
|
|
337
|
-
assert.equal((value.text || '').includes('开场三句内给结论'), false);
|
|
338
|
-
const episodes = Object.values(store.taskState(scope).episodes);
|
|
339
|
-
assert.equal(episodes.some((episode) => episode.status === 'proposed'), false);
|
|
340
|
-
assert.equal(runtime.text(agent, sig).includes('开场三句内给结论'), false);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/* L12:跨项目上下文直接拒绝。 */
|
|
344
|
-
{
|
|
345
|
-
const dirA = mkdir('cross-a');
|
|
346
|
-
const dirB = mkdir('cross-b');
|
|
347
|
-
const scopeA = scopeOf(dirA);
|
|
348
|
-
seedRule(scopeA, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
349
|
-
enable(scopeA);
|
|
350
|
-
const runtime = boot();
|
|
351
|
-
const agentA = makeAgent('a-cross-a', dirA);
|
|
352
|
-
await runtime.preStep({ agent: agentA, signal: signal(), turn: 't1' }, enter(human('这期怎么开头')));
|
|
353
|
-
const held = runtime.debug.windowOf(agentA).experiment;
|
|
354
|
-
const agentB = makeAgent('a-cross-b', dirB);
|
|
355
|
-
const value = await runtime.exec('memory_search', { query: '口播', contextId: held.contextId }, agentB);
|
|
356
|
-
assert.equal(value.status, 'needs_context');
|
|
357
|
-
assert.match(value.text, /另一个项目/);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/* L13:跨请求复用旧上下文算过期。 */
|
|
361
|
-
{
|
|
362
|
-
const dir = mkdir('stale');
|
|
363
|
-
const scope = scopeOf(dir);
|
|
364
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
365
|
-
enable(scope);
|
|
366
|
-
const runtime = boot();
|
|
367
|
-
const agent = makeAgent('a-stale', dir);
|
|
368
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期怎么开头')));
|
|
369
|
-
const old = runtime.debug.windowOf(agent).experiment.contextId;
|
|
370
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't2' }, enter(human('再问一次')));
|
|
371
|
-
const value = await runtime.exec('memory_search', { query: '口播', contextId: old }, agent);
|
|
372
|
-
assert.equal(value.status, 'needs_context');
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/* L14:绑定必须带 contextId,不能凭空绑。 */
|
|
376
|
-
{
|
|
377
|
-
const dir = mkdir('bind-missing');
|
|
378
|
-
const scope = scopeOf(dir);
|
|
379
|
-
enable(scope);
|
|
380
|
-
const runtime = boot();
|
|
381
|
-
const agent = makeAgent('a-bind-missing', dir);
|
|
382
|
-
const value = await runtime.exec('memory_search', { query: '口播', bindForRequest: true }, agent);
|
|
383
|
-
assert.equal(value.status, 'needs_context');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/* L15:暂停的实验条目即使有上下文也不给正文。 */
|
|
387
|
-
{
|
|
388
|
-
const dir = mkdir('paused');
|
|
389
|
-
const scope = scopeOf(dir);
|
|
390
|
-
const id = seedRule(scope, {
|
|
391
|
-
title: '暂停口播规则',
|
|
392
|
-
body: '这条已经暂停,不该给正文。',
|
|
393
|
-
conditions: [{ key: 'seriesId', value: '口播' }],
|
|
394
|
-
learning: { state: 'paused', blockedReason: 'needs_revalidation', specDigest: 'seeded' },
|
|
395
|
-
});
|
|
396
|
-
enable(scope, { seriesId: '口播' });
|
|
397
|
-
const runtime = boot();
|
|
398
|
-
const agent = makeAgent('a-paused', dir);
|
|
399
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播怎么做')));
|
|
400
|
-
const held = runtime.debug.windowOf(agent).experiment;
|
|
401
|
-
assert.ok(held);
|
|
402
|
-
const value = await runtime.exec('memory_read', { id, contextId: held.contextId }, agent);
|
|
403
|
-
assert.equal(value.status, 'needs_context');
|
|
404
|
-
assert.equal(value.body, '');
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/* L16:工具模式不自动注入,窗口仍然开。 */
|
|
408
|
-
{
|
|
409
|
-
const dir = mkdir('tool-only');
|
|
410
|
-
const scope = scopeOf(dir);
|
|
411
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
412
|
-
enable(scope, { seriesId: '口播' });
|
|
413
|
-
const runtime = boot('tool_only');
|
|
414
|
-
const agent = makeAgent('a-tool-only', dir);
|
|
415
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播开头')));
|
|
416
|
-
assert.equal(runtime.text(agent, signal()), '');
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/* L16b:工具模式下前台点名栏目可按需绑定,仍然不自动注入。 */
|
|
420
|
-
{
|
|
421
|
-
const dir = mkdir('tool-only-bind');
|
|
422
|
-
const scope = scopeOf(dir);
|
|
423
|
-
const id = seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
424
|
-
enable(scope, { seriesId: '口播' });
|
|
425
|
-
const runtime = boot('tool_only');
|
|
426
|
-
const agent = makeAgent('a-tool-only-bind', dir);
|
|
427
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播开头')));
|
|
428
|
-
const value = await runtime.exec('memory_search', { query: '口播开头', bindForRequest: true }, agent);
|
|
429
|
-
assert.equal(value.status, 'ok');
|
|
430
|
-
assert.equal((value.refs || []).some((ref) => ref.title === '口播开头'), true);
|
|
431
|
-
const episodes = Object.values(store.taskState(scope).episodes);
|
|
432
|
-
assert.equal(episodes.length, 1);
|
|
433
|
-
assert.equal(episodes[0].status, 'proposed');
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/* L17:固定规则照常召回,实验规则不混进普通自动召回。 */
|
|
437
|
-
{
|
|
438
|
-
const dir = mkdir('fixed');
|
|
439
|
-
const scope = scopeOf(dir);
|
|
440
|
-
save(scope, { title: '固定发布检查', body: '发版前跑一遍检查。', tags: ['发布'] });
|
|
441
|
-
seedRule(scope, { title: '实验发布检查', body: '实验规则:发版前先备份。', tags: ['发布'], learning: { state: 'active', specDigest: 'seeded' } });
|
|
442
|
-
enable(scope);
|
|
443
|
-
const runtime = boot();
|
|
444
|
-
const agent = makeAgent('a-fixed', dir);
|
|
445
|
-
const sig = signal();
|
|
446
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('发版前检查什么')));
|
|
447
|
-
const text = runtime.text(agent, sig);
|
|
448
|
-
assert.match(text, /固定发布检查/);
|
|
449
|
-
assert.equal(text.includes('实验规则:发版前先备份'), false);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/* L18:工具绑定零命中/多命中时只回一句读取状态,不要求用户点名栏目。 */
|
|
453
|
-
{
|
|
454
|
-
const dir = mkdir('bind-none');
|
|
455
|
-
const scope = scopeOf(dir);
|
|
456
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
457
|
-
seedRule(scope, { title: 'Vlog 节奏', body: '每段不超过二十秒。', conditions: [{ key: 'seriesId', value: 'Vlog' }] });
|
|
458
|
-
enable(scope);
|
|
459
|
-
const runtime = boot();
|
|
460
|
-
const agent = makeAgent('a-bind-none', dir);
|
|
461
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期怎么开头')));
|
|
462
|
-
const held = runtime.debug.windowOf(agent).experiment;
|
|
463
|
-
const none = await runtime.exec('memory_search', { query: '修一下构建脚本', contextId: held.contextId, bindForRequest: true }, agent);
|
|
464
|
-
assert.equal(none.status, 'needs_context');
|
|
465
|
-
assert.doesNotMatch(none.text, /点名|先问|栏目/);
|
|
466
|
-
const many = await runtime.exec('memory_search', { query: '口播和 Vlog 都要', contextId: held.contextId, bindForRequest: true }, agent);
|
|
467
|
-
assert.equal(many.status, 'needs_context');
|
|
468
|
-
assert.doesNotMatch(many.text, /点名|先问|栏目/);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/* L19:试用到期后不再自动注入,和检索、读取用同一档可用性。 */
|
|
472
|
-
{
|
|
473
|
-
const dir = mkdir('trial-expired');
|
|
474
|
-
const scope = scopeOf(dir);
|
|
475
|
-
const id = seedRule(scope, {
|
|
476
|
-
title: '过期口播规则',
|
|
477
|
-
body: '这条试用已经到期,不该自动出现。',
|
|
478
|
-
conditions: [{ key: 'seriesId', value: '口播' }],
|
|
479
|
-
learning: { state: 'trial', specDigest: 'seeded', trialUntil: new Date(Date.now() - 60_000).toISOString() },
|
|
480
|
-
});
|
|
481
|
-
enable(scope, { seriesId: '口播' });
|
|
482
|
-
const runtime = boot();
|
|
483
|
-
const agent = makeAgent('a-trial-expired', dir);
|
|
484
|
-
const sig = signal();
|
|
485
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播怎么做')));
|
|
486
|
-
const text = runtime.text(agent, sig);
|
|
487
|
-
assert.equal(text.includes('这条试用已经到期,不该自动出现。'), false);
|
|
488
|
-
const held = runtime.debug.windowOf(agent).experiment;
|
|
489
|
-
assert.deepEqual(held ? held.injectedIds : [], []);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/* L19b:准备时还在试用期、请求中途到期,同一请求也不能再输出正文。 */
|
|
493
|
-
{
|
|
494
|
-
const dir = mkdir('trial-expiring');
|
|
495
|
-
const scope = scopeOf(dir);
|
|
496
|
-
seedRule(scope, {
|
|
497
|
-
title: '快到期口播规则',
|
|
498
|
-
body: '这条试用马上到期,过期后同一请求也不能再出现。',
|
|
499
|
-
conditions: [{ key: 'seriesId', value: '口播' }],
|
|
500
|
-
learning: { state: 'trial', specDigest: 'seeded', trialUntil: new Date(Date.now() + 700).toISOString() },
|
|
501
|
-
});
|
|
502
|
-
enable(scope, { seriesId: '口播' });
|
|
503
|
-
const runtime = boot();
|
|
504
|
-
const agent = makeAgent('a-trial-expiring', dir);
|
|
505
|
-
const sig = signal();
|
|
506
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播怎么做')));
|
|
507
|
-
assert.match(runtime.text(agent, sig), /马上到期/);
|
|
508
|
-
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
509
|
-
assert.equal(runtime.text(agent, sig), '');
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/* L19c:一条试用到期只撤这一条,不把同请求里其他可用规则一起清掉。 */
|
|
513
|
-
{
|
|
514
|
-
const dir = mkdir('trial-parts');
|
|
515
|
-
const scope = scopeOf(dir);
|
|
516
|
-
seedRule(scope, {
|
|
517
|
-
title: '快到期的主题规则',
|
|
518
|
-
body: '这条试用马上到期。',
|
|
519
|
-
conditions: [{ key: 'seriesId', value: '口播' }],
|
|
520
|
-
learning: { state: 'trial', specDigest: 'seeded', trialUntil: new Date(Date.now() + 700).toISOString() },
|
|
521
|
-
topicKey: 'theme',
|
|
522
|
-
});
|
|
523
|
-
seedRule(scope, {
|
|
524
|
-
title: '长期口播开头',
|
|
525
|
-
body: '开场三句内给结论。',
|
|
526
|
-
conditions: [{ key: 'seriesId', value: '口播' }],
|
|
527
|
-
learning: { state: 'active', specDigest: 'seeded' },
|
|
528
|
-
topicKey: 'opening',
|
|
529
|
-
});
|
|
530
|
-
enable(scope, { seriesId: '口播' });
|
|
531
|
-
const runtime = boot();
|
|
532
|
-
const agent = makeAgent('a-trial-parts', dir);
|
|
533
|
-
const sig = signal();
|
|
534
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播怎么做')));
|
|
535
|
-
const first = runtime.text(agent, sig);
|
|
536
|
-
assert.match(first, /快到期的主题规则/);
|
|
537
|
-
assert.match(first, /长期口播开头/);
|
|
538
|
-
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
539
|
-
const later = runtime.text(agent, sig);
|
|
540
|
-
assert.doesNotMatch(later, /快到期的主题规则/);
|
|
541
|
-
assert.match(later, /长期口播开头/, '无关的试用到期不该清掉别的规则');
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/* L20:自进化关掉后,已经准备好的实验正文立刻不再输出;重开也不复活。 */
|
|
545
|
-
{
|
|
546
|
-
const dir = mkdir('off-after-prepare');
|
|
547
|
-
const scope = scopeOf(dir);
|
|
548
|
-
seedRule(scope, { title: '口播开头', body: '开场三句内给结论。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
549
|
-
enable(scope, { seriesId: '口播' });
|
|
550
|
-
const runtime = boot();
|
|
551
|
-
const agent = makeAgent('a-off-after-prepare', dir);
|
|
552
|
-
const sig = signal();
|
|
553
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播怎么做')));
|
|
554
|
-
assert.match(runtime.text(agent, sig), /开场三句内给结论/);
|
|
555
|
-
assert.equal(store.setEvolution(scope, false, { operationId: 'evo-off-after-prepare' }).ok, true);
|
|
556
|
-
assert.equal(runtime.text(agent, sig), '');
|
|
557
|
-
assert.equal(store.setEvolution(scope, true, { operationId: 'evo-on-again' }).ok, true);
|
|
558
|
-
assert.equal(runtime.text(agent, sig), '');
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/* L21:检索没有可用命中、但有条件未确认的候选时,只说状态,不把标题递给模型。 */
|
|
562
|
-
{
|
|
563
|
-
const dir = mkdir('search-withheld');
|
|
564
|
-
const scope = scopeOf(dir);
|
|
565
|
-
save(scope, {
|
|
566
|
-
title: '发布前检查清单',
|
|
567
|
-
body: '按清单逐项确认。',
|
|
568
|
-
summary: '发布前逐项确认',
|
|
569
|
-
applicability: 'unresolved',
|
|
570
|
-
});
|
|
571
|
-
const runtime = boot();
|
|
572
|
-
const agent = makeAgent('a-search-withheld', dir);
|
|
573
|
-
const value = await runtime.exec('memory_search', { query: '发布前检查' }, agent);
|
|
574
|
-
assert.equal(value.count, 0);
|
|
575
|
-
assert.equal(value.status, 'needs_context');
|
|
576
|
-
assert.equal(value.reasonCode, 'applicability_unresolved');
|
|
577
|
-
assert.equal(value.text.includes('发布前检查清单'), false);
|
|
578
|
-
assert.match(value.text, /适用条件还没确认/);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
/* L22:自动续写轮没有新的真人消息,仍沿用本会话上一次的请求上下文读正文。 */
|
|
582
|
-
{
|
|
583
|
-
const dir = mkdir('continuation');
|
|
584
|
-
const scope = scopeOf(dir);
|
|
585
|
-
const saved = save(scope, { title: '口播发布清单', body: '按清单逐项确认。' });
|
|
586
|
-
state.transact(scope, {}, (draft) => {
|
|
587
|
-
const row = draft.entries.find((entry) => entry.id === saved.id);
|
|
588
|
-
row.current.conditions = [{ key: 'seriesId', value: '口播' }];
|
|
589
|
-
return { changed: true };
|
|
590
|
-
});
|
|
591
|
-
enable(scope, { seriesId: '口播' });
|
|
592
|
-
const runtime = boot();
|
|
593
|
-
const agent = makeAgent('a-continuation', dir);
|
|
594
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't1' }, enter(human('这期口播怎么做')));
|
|
595
|
-
const first = await runtime.exec('memory_read', { id: saved.id }, agent);
|
|
596
|
-
await runtime.preStep({ agent, signal: signal(), turn: 't2' }, enter());
|
|
597
|
-
assert.equal(first.status, 'ok');
|
|
598
|
-
assert.match(first.body, /按清单逐项确认/);
|
|
599
|
-
const second = await runtime.exec('memory_read', { id: saved.id }, agent);
|
|
600
|
-
assert.equal(second.status, 'ok');
|
|
601
|
-
assert.equal(second.repeat, true);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
console.log('evolution-read.test.mjs: all passed');
|
|
605
|
-
|
|
606
|
-
/* L23:被取代 / 已过期的实验规则不再从实验通道注入正文。 */
|
|
607
|
-
{
|
|
608
|
-
const dir = mkdir('lifecycle');
|
|
609
|
-
const scope = scopeOf(dir);
|
|
610
|
-
const superseded = seedRule(scope, { title: '旧开场规则', body: '旧规则正文:开场先自我介绍。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
611
|
-
const expired = seedRule(scope, { title: '过期规则', body: '过期规则正文:开场念固定slogan。', conditions: [{ key: 'seriesId', value: '口播' }] });
|
|
612
|
-
enable(scope, { seriesId: '口播' });
|
|
613
|
-
state.transact(scope, {}, (draft) => {
|
|
614
|
-
const first = draft.entries.find((entry) => entry.id === superseded);
|
|
615
|
-
first.supersededBy = 'mem_deadbeef';
|
|
616
|
-
const second = draft.entries.find((entry) => entry.id === expired);
|
|
617
|
-
second.validUntil = new Date(Date.now() - 60_000).toISOString();
|
|
618
|
-
return { changed: true };
|
|
619
|
-
});
|
|
620
|
-
const runtime = boot();
|
|
621
|
-
const agent = makeAgent('a-lifecycle', dir);
|
|
622
|
-
const sig = signal();
|
|
623
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播开场怎么改')));
|
|
624
|
-
const text = runtime.text(agent, sig);
|
|
625
|
-
assert.equal(text.includes('旧规则正文'), false, '被取代的规则不能注入');
|
|
626
|
-
assert.equal(text.includes('过期规则正文'), false, '过期规则不能注入');
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/* L24:没有声明条件的实验规则,问题不相关时也不靠「有规则」硬注入。 */
|
|
630
|
-
{
|
|
631
|
-
const dir = mkdir('unbound');
|
|
632
|
-
const scope = scopeOf(dir);
|
|
633
|
-
seedRule(scope, { title: '导出参数', body: '导出统一用 H.264。', conditions: [] });
|
|
634
|
-
enable(scope, { seriesId: '口播' });
|
|
635
|
-
const runtime = boot();
|
|
636
|
-
const agent = makeAgent('a-unbound', dir);
|
|
637
|
-
const sig = signal();
|
|
638
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期封面标题怎么写')));
|
|
639
|
-
assert.equal(runtime.text(agent, sig).includes('导出统一用 H.264'), false, '无关问题不注入宽泛规则');
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
/* L25:只声明环境条件(系统/路径)的规则不算任务绑定,问题不相关时不注入。 */
|
|
643
|
-
{
|
|
644
|
-
const dir = mkdir('env-only');
|
|
645
|
-
const scope = scopeOf(dir);
|
|
646
|
-
const host = view.hostTargetOs();
|
|
647
|
-
assert.ok(host, '本机系统可识别');
|
|
648
|
-
seedRule(scope, { title: '导出参数', body: '导出统一用 H.264。', conditions: [{ key: 'targetOs', value: host }] });
|
|
649
|
-
enable(scope, { seriesId: '口播' });
|
|
650
|
-
const runtime = boot();
|
|
651
|
-
const agent = makeAgent('a-env-only', dir);
|
|
652
|
-
const sig = signal();
|
|
653
|
-
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期封面标题怎么写')));
|
|
654
|
-
assert.equal(runtime.text(agent, sig).includes('导出统一用 H.264'), false, '只有环境条件也要过词面门');
|
|
655
|
-
const sig2 = signal();
|
|
656
|
-
await runtime.preStep({ agent, signal: sig2, turn: 't2' }, enter(human('这期口播导出参数怎么配')));
|
|
657
|
-
assert.equal(runtime.text(agent, sig2).includes('导出统一用 H.264'), true, '相关时环境条件规则照常注入');
|
|
658
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "mem_444444444444",
|
|
3
|
-
"title": "旧归档条目",
|
|
4
|
-
"summary": "已经归档的一条",
|
|
5
|
-
"when": "",
|
|
6
|
-
"kind": "note",
|
|
7
|
-
"tags": [],
|
|
8
|
-
"created": "2026-01-04T00:00:00.000Z",
|
|
9
|
-
"updated": "2026-01-04T00:00:00.000Z",
|
|
10
|
-
"lastUsed": "2026-01-04T00:00:00.000Z",
|
|
11
|
-
"revision": 2,
|
|
12
|
-
"status": "archived",
|
|
13
|
-
"source": "user_explicit",
|
|
14
|
-
"hits": 3,
|
|
15
|
-
"merges": 1,
|
|
16
|
-
"versions": [],
|
|
17
|
-
"body": "归档正文"
|
|
18
|
-
}
|