@max-null/dsh-memory 0.5.0 → 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.md +36 -12
- package/client.js +318 -15
- package/dist/bm25.d.ts +25 -1
- package/dist/bm25.js +99 -20
- package/dist/engine.d.ts +115 -3
- package/dist/engine.js +388 -41
- package/dist/index.d.ts +1 -1
- package/dist/index.js +205 -12
- package/dist/injection.d.ts +34 -0
- package/dist/injection.js +52 -0
- package/dist/prompt-files.d.ts +77 -0
- package/dist/prompt-files.js +188 -0
- package/dist/remote.d.ts +9 -5
- package/dist/remote.js +11 -4
- package/dist/routes.js +90 -2
- package/dist/self.js +2 -2
- package/package.json +1 -1
package/dist/engine.js
CHANGED
|
@@ -5,13 +5,15 @@
|
|
|
5
5
|
* is always created `suggested` and becomes effective only through `setStatus`.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
10
|
import { join } from 'node:path';
|
|
10
11
|
import { Service } from '@deepseek-ai/cordis';
|
|
11
12
|
import { z } from 'zod';
|
|
12
13
|
import { defineDomain, domainTable, DomainFacility } from '@deepseek-ai/dsh-storage-domain';
|
|
13
14
|
import { JsonStorageBackend } from '@deepseek-ai/dsh-storage-json';
|
|
14
|
-
import {
|
|
15
|
+
import { bm25FieldScores, cosineSimilarity, rrfFuse } from "./bm25.js";
|
|
16
|
+
import { parsePromptFile, scanPromptDir, writePromptFile } from "./prompt-files.js";
|
|
15
17
|
/** Brand a string as a {@link MemoryId} (compile-time only). */
|
|
16
18
|
export function MemoryId(id) {
|
|
17
19
|
return id;
|
|
@@ -24,6 +26,20 @@ const blockSchema = z.object({
|
|
|
24
26
|
keywords: z.array(z.string()),
|
|
25
27
|
createdAt: z.number(),
|
|
26
28
|
updatedAt: z.number(),
|
|
29
|
+
lastUsedAt: z.number().optional(),
|
|
30
|
+
vector: z.array(z.number()).optional(),
|
|
31
|
+
kind: z.enum(['fact', 'prompt']).optional(),
|
|
32
|
+
meta: z.object({
|
|
33
|
+
seq: z.number().optional(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
dimension: z.string().optional(),
|
|
36
|
+
difficulty: z.string().optional(),
|
|
37
|
+
tags: z.array(z.string()),
|
|
38
|
+
summary: z.string(),
|
|
39
|
+
path: z.string(),
|
|
40
|
+
mtime: z.number(),
|
|
41
|
+
source: z.enum(['user', 'agent']),
|
|
42
|
+
}).optional(),
|
|
27
43
|
});
|
|
28
44
|
/** Shared table shape; the two domains differ only by name and backend route. */
|
|
29
45
|
function memorySpec(name) {
|
|
@@ -33,6 +49,30 @@ function memorySpec(name) {
|
|
|
33
49
|
tables: { blocks: domainTable(blockSchema) },
|
|
34
50
|
});
|
|
35
51
|
}
|
|
52
|
+
/** 读 unit 文件头的 name(文件不存在 → undefined;非法 JSON/结构异常抛错)。 */
|
|
53
|
+
function unitHeaderName(path) {
|
|
54
|
+
if (!existsSync(path))
|
|
55
|
+
return undefined;
|
|
56
|
+
const document = JSON.parse(readFileSync(path, 'utf8'));
|
|
57
|
+
return typeof document.unit?.name === 'string' ? document.unit.name : undefined;
|
|
58
|
+
}
|
|
59
|
+
/** 改写 unit 文件头的 name(保持 2 空格缩进 + 尾换行,与 storage-json/format 一致)。 */
|
|
60
|
+
function rewriteUnitName(path, name) {
|
|
61
|
+
const document = JSON.parse(readFileSync(path, 'utf8'));
|
|
62
|
+
if (document.unit === undefined)
|
|
63
|
+
throw new Error(`unit '${path}': missing unit header`);
|
|
64
|
+
document.unit.name = name;
|
|
65
|
+
writeFileSync(path, `${JSON.stringify(document, null, 2)}\n`);
|
|
66
|
+
}
|
|
67
|
+
/** 迁移旧 unit 文件:校验头名 → 改写为新名并落位规范路径 → 删除旧文件。 */
|
|
68
|
+
function migrateUnit(legacyPath, canonicalPath, legacyName, canonicalName) {
|
|
69
|
+
const document = JSON.parse(readFileSync(legacyPath, 'utf8'));
|
|
70
|
+
if (document.unit?.name !== legacyName)
|
|
71
|
+
throw new Error(`unit '${legacyPath}': unexpected unit header`);
|
|
72
|
+
document.unit.name = canonicalName;
|
|
73
|
+
writeFileSync(canonicalPath, `${JSON.stringify(document, null, 2)}\n`);
|
|
74
|
+
rmSync(legacyPath);
|
|
75
|
+
}
|
|
36
76
|
/**
|
|
37
77
|
* 旧 schema 迁移(0.3.0,设计文档「迁移规则」):
|
|
38
78
|
* - 旧 `auto` → approved + injected:true(行为不变:仍常驻注入)
|
|
@@ -48,7 +88,7 @@ function normalizeBlock(block) {
|
|
|
48
88
|
return { status: block.status, injected: block.injected ?? false };
|
|
49
89
|
}
|
|
50
90
|
function toRecord(id, block) {
|
|
51
|
-
|
|
91
|
+
const base = {
|
|
52
92
|
id: MemoryId(id),
|
|
53
93
|
namespace: block.namespace,
|
|
54
94
|
...normalizeBlock(block),
|
|
@@ -56,6 +96,13 @@ function toRecord(id, block) {
|
|
|
56
96
|
keywords: block.keywords,
|
|
57
97
|
createdAt: block.createdAt,
|
|
58
98
|
updatedAt: block.updatedAt,
|
|
99
|
+
...block.lastUsedAt === undefined ? {} : { lastUsedAt: block.lastUsedAt },
|
|
100
|
+
};
|
|
101
|
+
// 0.6.0 模板库字段(缺省 fact;prompt 元数据透传)
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
...block.kind === undefined ? {} : { kind: block.kind },
|
|
105
|
+
...block.meta === undefined ? {} : { meta: block.meta },
|
|
59
106
|
};
|
|
60
107
|
}
|
|
61
108
|
/** Harness-home root for `global` memories. */
|
|
@@ -130,13 +177,37 @@ export class MemoryEngine extends Service {
|
|
|
130
177
|
if (cached !== undefined)
|
|
131
178
|
return cached;
|
|
132
179
|
const backendName = this.projectBackendName(key);
|
|
180
|
+
// 0.5.2:旧实现 domain 名多一层 memory_project_ 前缀(文件名双重前缀,
|
|
181
|
+
// 且 unit 文件头 name 同为旧名)。打开前统一迁移到规范名:
|
|
182
|
+
// ①规范名已存在 + 头为旧名(0.5.2 首版只 rename 未改 header 的中间态)→ 改写头;
|
|
183
|
+
// ②旧名文件存在且头匹配 → 改写头并落位规范名;③其他情况回退旧域名(不动数据)。
|
|
184
|
+
const legacyDomainName = `memory_project_${backendName}`;
|
|
185
|
+
const root = this.projectRootFor(key);
|
|
186
|
+
const canonicalFile = join(root, `${backendName}.json`);
|
|
187
|
+
const legacyFile = join(root, `${legacyDomainName}.json`);
|
|
188
|
+
let domainName = backendName;
|
|
189
|
+
try {
|
|
190
|
+
const canonicalHeader = unitHeaderName(canonicalFile);
|
|
191
|
+
if (canonicalHeader === legacyDomainName) {
|
|
192
|
+
rewriteUnitName(canonicalFile, backendName);
|
|
193
|
+
}
|
|
194
|
+
else if (canonicalHeader === undefined && unitHeaderName(legacyFile) === legacyDomainName) {
|
|
195
|
+
migrateUnit(legacyFile, canonicalFile, legacyDomainName, backendName);
|
|
196
|
+
}
|
|
197
|
+
else if (canonicalHeader === undefined && existsSync(legacyFile)) {
|
|
198
|
+
domainName = legacyDomainName; // 外域/未知内容:保持旧名路径继续打开,不触碰数据
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
domainName = legacyDomainName; // 迁移出错:回退旧名打开,数据不动(后续可重试)
|
|
203
|
+
}
|
|
133
204
|
if (!this.registeredProjectBackends.has(backendName)) {
|
|
134
|
-
const backend = new JsonStorageBackend(
|
|
205
|
+
const backend = new JsonStorageBackend(root);
|
|
135
206
|
this.ctx.storage.backend.register(backendName, backend);
|
|
136
207
|
this.registeredProjectBackends.add(backendName);
|
|
137
208
|
}
|
|
138
209
|
const facility = new DomainFacility(this.ctx, { backend: backendName, routes: {} });
|
|
139
|
-
const domain = await facility.open(memorySpec(
|
|
210
|
+
const domain = await facility.open(memorySpec(domainName));
|
|
140
211
|
const table = domain.table('blocks');
|
|
141
212
|
this.projectFacilities.set(key, facility);
|
|
142
213
|
this.projectTables.set(key, table);
|
|
@@ -182,16 +253,126 @@ export class MemoryEngine extends Service {
|
|
|
182
253
|
async list(filter, projectCwd) {
|
|
183
254
|
const records = await this.allRecords(filter?.namespace, projectCwd);
|
|
184
255
|
return records.filter(record => (filter?.status === undefined || record.status === filter.status)
|
|
185
|
-
&& (filter?.injected === undefined || record.injected === filter.injected)
|
|
256
|
+
&& (filter?.injected === undefined || record.injected === filter.injected)
|
|
257
|
+
&& (filter?.kind === undefined || (record.kind ?? 'fact') === filter.kind));
|
|
186
258
|
}
|
|
187
259
|
async search(query, filter, projectCwd) {
|
|
188
260
|
const records = await this.list(filter, projectCwd);
|
|
189
|
-
|
|
190
|
-
const scores =
|
|
191
|
-
|
|
261
|
+
// 0.5.2:content 与 keywords 分离打加权 BM25(人工关键词命中权重更高)
|
|
262
|
+
const scores = bm25FieldScores(query, records.map(record => ({
|
|
263
|
+
body: record.content,
|
|
264
|
+
tags: record.keywords.join(' '),
|
|
265
|
+
})));
|
|
266
|
+
const bm25Ranked = records
|
|
192
267
|
.map((record, index) => ({ record, score: scores[index] ?? 0 }))
|
|
193
268
|
.filter(hit => hit.score > 0)
|
|
194
269
|
.sort((left, right) => right.score - left.score);
|
|
270
|
+
// 0.5.3:命中即标记使用(冷热追踪)——写回按命中快照之后执行,不影响本次结果
|
|
271
|
+
await this.markUsed(bm25Ranked.map(hit => hit.record.id), projectCwd);
|
|
272
|
+
// 0.5.2:配置 embeddings 时与语义结果 RRF 融合;缺省/失败降级回纯 BM25
|
|
273
|
+
if (this.config.embeddings === undefined)
|
|
274
|
+
return bm25Ranked;
|
|
275
|
+
try {
|
|
276
|
+
const semantic = await this.semanticRanking(query, records.map(record => record.id), projectCwd);
|
|
277
|
+
if (semantic.length === 0)
|
|
278
|
+
return bm25Ranked;
|
|
279
|
+
const byId = new Map(records.map(record => [String(record.id), record]));
|
|
280
|
+
return rrfFuse([
|
|
281
|
+
bm25Ranked.map(hit => String(hit.record.id)),
|
|
282
|
+
semantic.map(hit => String(hit.id)),
|
|
283
|
+
]).flatMap(([id, score]) => {
|
|
284
|
+
const record = byId.get(id);
|
|
285
|
+
return record === undefined ? [] : [{ record, score }];
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
// 语义通道失败降级:不影响检索可用性(BM25 结果照常返回)
|
|
290
|
+
this.ctx.logger?.warn?.(`dsh-memory: semantic search degraded: ${error instanceof Error ? error.message : String(error)}`);
|
|
291
|
+
return bm25Ranked;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* 语义排序(0.5.2):查询向量 + 候选向量的余弦相似度,仅在有
|
|
296
|
+
* embeddings 配置时被调用。缺失向量(新增/更新过的记录)在此补生成
|
|
297
|
+
* 并持久化(vector 字段);生成失败抛给调用方降级。
|
|
298
|
+
*/
|
|
299
|
+
async semanticRanking(query, ids, projectCwd) {
|
|
300
|
+
const config = this.config.embeddings;
|
|
301
|
+
if (config === undefined)
|
|
302
|
+
return [];
|
|
303
|
+
const similarity = config.similarity ?? cosineSimilarity;
|
|
304
|
+
const topK = this.config.semanticTopK ?? 5;
|
|
305
|
+
const queryVector = (await config.embed([query]))?.[0];
|
|
306
|
+
if (queryVector === undefined)
|
|
307
|
+
return [];
|
|
308
|
+
// 缺失向量批量补写(本轮直接用本地生成的向量参与打分,写回不阻塞结果)
|
|
309
|
+
const missing = [];
|
|
310
|
+
for (const id of ids) {
|
|
311
|
+
const table = this.tableOf(id, projectCwd);
|
|
312
|
+
const block = table?.get(id);
|
|
313
|
+
if (table === undefined || block === undefined)
|
|
314
|
+
continue;
|
|
315
|
+
if (block.vector === undefined)
|
|
316
|
+
missing.push({ table, id, content: block.content });
|
|
317
|
+
}
|
|
318
|
+
const fresh = new Map();
|
|
319
|
+
if (missing.length > 0) {
|
|
320
|
+
const vectors = await config.embed(missing.map(missingRecord => missingRecord.content));
|
|
321
|
+
for (const [index, missingRecord] of missing.entries()) {
|
|
322
|
+
const vector = vectors[index];
|
|
323
|
+
if (vector === undefined)
|
|
324
|
+
continue;
|
|
325
|
+
fresh.set(String(missingRecord.id), [...vector]);
|
|
326
|
+
const block = missingRecord.table.get(missingRecord.id);
|
|
327
|
+
if (block !== undefined) {
|
|
328
|
+
await missingRecord.table.put(missingRecord.id, { ...block, vector: [...vector] });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const scored = [];
|
|
333
|
+
for (const id of ids) {
|
|
334
|
+
const block = this.tableOf(id, projectCwd)?.get(id);
|
|
335
|
+
const vector = fresh.get(String(id)) ?? block?.vector;
|
|
336
|
+
if (vector === undefined)
|
|
337
|
+
continue;
|
|
338
|
+
scored.push({ id, score: similarity(queryVector, vector) });
|
|
339
|
+
}
|
|
340
|
+
return scored
|
|
341
|
+
.filter(hit => hit.score > 0)
|
|
342
|
+
.sort((left, right) => right.score - left.score)
|
|
343
|
+
.slice(0, topK);
|
|
344
|
+
}
|
|
345
|
+
/** 按已知 id 定位记录所在表(global 优先,其次当前工作区表)。 */
|
|
346
|
+
tableOf(id, projectCwd) {
|
|
347
|
+
const global = this.requireTable('global');
|
|
348
|
+
if (global.get(id) !== undefined)
|
|
349
|
+
return global;
|
|
350
|
+
if (projectCwd === undefined || projectCwd === '')
|
|
351
|
+
return undefined;
|
|
352
|
+
const project = this.projectTables.get(join(projectCwd));
|
|
353
|
+
return project !== undefined && project.get(id) !== undefined ? project : undefined;
|
|
354
|
+
}
|
|
355
|
+
/** 命中标记:把 lastUsedAt 写回(命中记录不存在时跳过;不 emit 事件——统计性字段)。 */
|
|
356
|
+
async markUsed(ids, projectCwd) {
|
|
357
|
+
if (ids.length === 0)
|
|
358
|
+
return;
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
const global = this.requireTable('global');
|
|
361
|
+
const project = projectCwd === undefined || projectCwd === ''
|
|
362
|
+
? undefined
|
|
363
|
+
: this.projectTables.get(join(projectCwd));
|
|
364
|
+
for (const id of ids) {
|
|
365
|
+
const globalBlock = global.get(id);
|
|
366
|
+
if (globalBlock !== undefined) {
|
|
367
|
+
await global.put(id, { ...globalBlock, lastUsedAt: now });
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (project !== undefined) {
|
|
371
|
+
const block = project.get(id);
|
|
372
|
+
if (block !== undefined)
|
|
373
|
+
await project.put(id, { ...block, lastUsedAt: now });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
195
376
|
}
|
|
196
377
|
async forget(id, projectCwd) {
|
|
197
378
|
if (await this.requireTable('global').delete(id)) {
|
|
@@ -243,36 +424,35 @@ export class MemoryEngine extends Service {
|
|
|
243
424
|
* 「常驻注入」开关调用(remote.setInjected)。
|
|
244
425
|
*/
|
|
245
426
|
async setInjected(id, injected, projectCwd) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const record = toRecord(id, updated);
|
|
256
|
-
this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
|
|
257
|
-
return record;
|
|
258
|
-
}
|
|
259
|
-
const project = await this.projectTableFor(projectCwd);
|
|
260
|
-
if (project !== undefined) {
|
|
261
|
-
const block = project.get(id);
|
|
262
|
-
if (block !== undefined) {
|
|
263
|
-
const updated = {
|
|
264
|
-
...block,
|
|
265
|
-
status: normalizeBlock(block).status,
|
|
266
|
-
injected,
|
|
267
|
-
updatedAt: Date.now(),
|
|
268
|
-
};
|
|
269
|
-
await project.put(id, updated);
|
|
270
|
-
const record = toRecord(id, updated);
|
|
271
|
-
this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
|
|
272
|
-
return record;
|
|
427
|
+
const locate = async () => {
|
|
428
|
+
const global = this.requireTable('global').get(id);
|
|
429
|
+
if (global !== undefined)
|
|
430
|
+
return { table: this.requireTable('global'), block: global };
|
|
431
|
+
const project = await this.projectTableFor(projectCwd);
|
|
432
|
+
if (project !== undefined) {
|
|
433
|
+
const block = project.get(id);
|
|
434
|
+
if (block !== undefined)
|
|
435
|
+
return { table: project, block };
|
|
273
436
|
}
|
|
437
|
+
return undefined;
|
|
438
|
+
};
|
|
439
|
+
const found = await locate();
|
|
440
|
+
if (found === undefined)
|
|
441
|
+
throw new Error(`cannot set injected of unknown memory '${id}'`);
|
|
442
|
+
// 模板永不注入(0.6.0):prompt 记录拒绝开关(UI 对该类不展示开关,此处兜底)
|
|
443
|
+
if ((found.block.kind ?? 'fact') === 'prompt') {
|
|
444
|
+
throw new Error(`cannot set injected of prompt record '${id}': prompt templates are never injected`);
|
|
274
445
|
}
|
|
275
|
-
|
|
446
|
+
const updated = {
|
|
447
|
+
...found.block,
|
|
448
|
+
status: normalizeBlock(found.block).status,
|
|
449
|
+
injected,
|
|
450
|
+
updatedAt: Date.now(),
|
|
451
|
+
};
|
|
452
|
+
await found.table.put(id, updated);
|
|
453
|
+
const record = toRecord(id, updated);
|
|
454
|
+
this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
|
|
455
|
+
return record;
|
|
276
456
|
}
|
|
277
457
|
/**
|
|
278
458
|
* 修改一条记忆的内容/关键词(0.3.1 整理记忆用)。内容被模型改动后
|
|
@@ -313,12 +493,32 @@ export class MemoryEngine extends Service {
|
|
|
313
493
|
throw new Error(`cannot update unknown memory '${id}'`);
|
|
314
494
|
}
|
|
315
495
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
496
|
+
* 预热某工作区的 project 表(异步打开并缓存)。system prompt 的
|
|
497
|
+
* `memory:recall` provider 是同步回调——只读已打开的表;预热让
|
|
498
|
+
* 会话创建/首次请求后本轮或下一轮即可注入工作区记忆。
|
|
318
499
|
*/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
500
|
+
async ensureProjectOpen(projectCwd) {
|
|
501
|
+
const key = join(projectCwd);
|
|
502
|
+
if (this.projectTables.has(key))
|
|
503
|
+
return;
|
|
504
|
+
await this.projectTableFor(key);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* 注入专用(0.3.4 + 0.5.1):system prompt 的 `memory:recall` context
|
|
508
|
+
* provider 可经 AssembleContext.agent 拿到当前会话 header.cwd,因此
|
|
509
|
+
* 注入 = global 的 approved+injected + 当前会话工作区的 approved+injected。
|
|
510
|
+
* provider 是同步回调:只读已打开/缓存的表,未打开的工作区本轮为空
|
|
511
|
+
* (`ensureProjectOpen` 提供预热,工具/面板路径也会打开)。
|
|
512
|
+
*/
|
|
513
|
+
recallRecords(projectCwd) {
|
|
514
|
+
const injected = (table) => this.recordsOf(table).filter(record => record.status === 'approved' && record.injected);
|
|
515
|
+
const global = injected(this.requireTable('global'));
|
|
516
|
+
if (projectCwd === undefined || projectCwd === '')
|
|
517
|
+
return global;
|
|
518
|
+
const project = this.projectTables.get(join(projectCwd));
|
|
519
|
+
if (project === undefined)
|
|
520
|
+
return global;
|
|
521
|
+
return [...global, ...injected(project)];
|
|
322
522
|
}
|
|
323
523
|
async allRecords(namespace, projectCwd) {
|
|
324
524
|
if (namespace === 'project') {
|
|
@@ -347,4 +547,151 @@ export class MemoryEngine extends Service {
|
|
|
347
547
|
throw new Error('memory engine is not started yet');
|
|
348
548
|
return table;
|
|
349
549
|
}
|
|
550
|
+
// ── 提示词模板库(0.6.0):md 文件是事实源,索引记录 kind='prompt' ──────
|
|
551
|
+
/** 全局模板根(默认 `$DSH_HOME/prompt-library`,可配置覆盖)。 */
|
|
552
|
+
promptGlobalDir() {
|
|
553
|
+
return this.config.promptGlobalRoot ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'prompt-library');
|
|
554
|
+
}
|
|
555
|
+
/** project 模板根(随工作区 `<workspace>/.dsh/prompt-library`,与记忆同构)。 */
|
|
556
|
+
promptProjectDir(projectCwd) {
|
|
557
|
+
if (projectCwd === undefined || projectCwd === '')
|
|
558
|
+
return '';
|
|
559
|
+
return join(projectCwd, '.dsh', 'prompt-library');
|
|
560
|
+
}
|
|
561
|
+
/** 稳定索引 id(按路径 hash;文件删除后随刷新清理)。 */
|
|
562
|
+
promptIdOf(path) {
|
|
563
|
+
let h = 5381;
|
|
564
|
+
const text = path.toLowerCase();
|
|
565
|
+
for (let i = 0; i < text.length; i++)
|
|
566
|
+
h = ((h << 5) + h + text.charCodeAt(i)) | 0;
|
|
567
|
+
return MemoryId(`prompt-${Math.abs(h).toString(36)}`);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* 扫描模板目录并同步索引(mtime 惰性:未变条目零写;目录缺失 = 空库)。
|
|
571
|
+
* @returns 统计(扫描数/变更数/清理数 + 坏文件警告)。
|
|
572
|
+
*/
|
|
573
|
+
async refreshPromptIndex(projectCwd) {
|
|
574
|
+
const globalDir = this.promptGlobalDir();
|
|
575
|
+
const projectDir = this.promptProjectDir(projectCwd);
|
|
576
|
+
const globalScan = scanPromptDir(globalDir);
|
|
577
|
+
const projectScan = scanPromptDir(projectDir);
|
|
578
|
+
const scanned = [...globalScan.files, ...projectScan.files];
|
|
579
|
+
const warnings = [...globalScan.warnings, ...projectScan.warnings];
|
|
580
|
+
const globalTable = this.requireTable('global');
|
|
581
|
+
const projectTable = projectCwd !== undefined && projectCwd !== ''
|
|
582
|
+
? await this.projectTableFor(projectCwd)
|
|
583
|
+
: undefined;
|
|
584
|
+
const existing = await this.list({ kind: 'prompt' }, projectCwd);
|
|
585
|
+
const existingById = new Map(existing.map(record => [String(record.id), record]));
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
let changed = 0;
|
|
588
|
+
let removed = 0;
|
|
589
|
+
const now = Date.now();
|
|
590
|
+
for (const file of scanned) {
|
|
591
|
+
const id = this.promptIdOf(file.path);
|
|
592
|
+
seen.add(String(id));
|
|
593
|
+
const isGlobal = file.path.startsWith(globalDir);
|
|
594
|
+
const table = isGlobal ? globalTable : projectTable;
|
|
595
|
+
if (table === undefined)
|
|
596
|
+
continue;
|
|
597
|
+
const prev = existingById.get(String(id));
|
|
598
|
+
if (prev?.meta !== undefined && prev.meta.mtime === file.mtime)
|
|
599
|
+
continue; // mtime 未变:零写
|
|
600
|
+
const keywords = [
|
|
601
|
+
file.meta.name,
|
|
602
|
+
...(file.meta.dimension !== undefined ? [file.meta.dimension] : []),
|
|
603
|
+
...(file.meta.difficulty !== undefined ? [file.meta.difficulty] : []),
|
|
604
|
+
...file.meta.tags,
|
|
605
|
+
].filter(keyword => keyword !== '').map(keyword => keyword.toLowerCase());
|
|
606
|
+
const block = {
|
|
607
|
+
namespace: isGlobal ? 'global' : 'project',
|
|
608
|
+
status: 'approved', // 文件存在即生效(设计文档 §三:模板不做审核流)
|
|
609
|
+
injected: false, // 模板永不注入
|
|
610
|
+
content: [file.meta.name, file.meta.dimension ?? '', file.meta.difficulty ?? '', file.meta.tags.join(' '), file.summary]
|
|
611
|
+
.filter(part => part !== '').join(' '),
|
|
612
|
+
keywords,
|
|
613
|
+
createdAt: prev?.createdAt ?? now,
|
|
614
|
+
updatedAt: now,
|
|
615
|
+
kind: 'prompt',
|
|
616
|
+
meta: {
|
|
617
|
+
seq: file.meta.seq,
|
|
618
|
+
name: file.meta.name,
|
|
619
|
+
dimension: file.meta.dimension,
|
|
620
|
+
difficulty: file.meta.difficulty,
|
|
621
|
+
tags: file.meta.tags,
|
|
622
|
+
summary: file.summary,
|
|
623
|
+
path: file.path,
|
|
624
|
+
mtime: file.mtime,
|
|
625
|
+
source: file.meta.source,
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
await table.put(id, block);
|
|
629
|
+
changed++;
|
|
630
|
+
}
|
|
631
|
+
for (const record of existing) {
|
|
632
|
+
if (seen.has(String(record.id)))
|
|
633
|
+
continue;
|
|
634
|
+
if (record.meta === undefined)
|
|
635
|
+
continue;
|
|
636
|
+
const isGlobal = record.meta.path.startsWith(globalDir);
|
|
637
|
+
const table = isGlobal ? globalTable : projectTable;
|
|
638
|
+
if (table !== undefined) {
|
|
639
|
+
await table.delete(record.id);
|
|
640
|
+
removed++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { scan: scanned.length, changed, removed, warnings };
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 取模板(id 或名称/路径片段定位),返回 md 全文(直读文件,忽略索引缓存)。
|
|
647
|
+
* @throws 未命中或文件解析失败(错误信息可读)。
|
|
648
|
+
*/
|
|
649
|
+
async promptGet(nameOrId, projectCwd) {
|
|
650
|
+
const records = await this.list({ kind: 'prompt' }, projectCwd);
|
|
651
|
+
const needle = nameOrId.trim();
|
|
652
|
+
const hit = records.find(record => {
|
|
653
|
+
if (record.meta === undefined)
|
|
654
|
+
return false;
|
|
655
|
+
const stem = (record.meta.path.split(/[\\/]/).pop() ?? '').replace(/\.md$/i, '');
|
|
656
|
+
const afterSeq = stem.replace(/^\d+_/, ''); // 去掉序号前缀:36_名称 → 名称
|
|
657
|
+
return String(record.id) === needle || afterSeq === needle || afterSeq.includes(needle);
|
|
658
|
+
});
|
|
659
|
+
if (hit === undefined || hit.meta === undefined) {
|
|
660
|
+
throw new Error(`prompt '${nameOrId}' not found(可用 prompt_list 查看全部)`);
|
|
661
|
+
}
|
|
662
|
+
const text = readFileSync(hit.meta.path, 'utf8');
|
|
663
|
+
const parsed = parsePromptFile(hit.meta.path, text);
|
|
664
|
+
if (!parsed.ok)
|
|
665
|
+
throw new Error(`prompt file ${hit.meta.path} parse failed: ${parsed.error}`);
|
|
666
|
+
return parsed.file;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* 新增模板:写 md 文件(序号自动分配/文件名安全化/同名防覆盖)+ 刷新索引。
|
|
670
|
+
* 返回索引记录(模型新增 source='agent',UI 角标提示)。
|
|
671
|
+
*/
|
|
672
|
+
async promptAdd(input, projectCwd) {
|
|
673
|
+
const dir = input.namespace === 'project' ? this.promptProjectDir(projectCwd) : this.promptGlobalDir();
|
|
674
|
+
if (dir === '')
|
|
675
|
+
throw new Error('cannot write project prompt without a workspace cwd');
|
|
676
|
+
const path = writePromptFile(dir, input);
|
|
677
|
+
await this.refreshPromptIndex(projectCwd);
|
|
678
|
+
const found = (await this.list({ kind: 'prompt' }, projectCwd))
|
|
679
|
+
.find(record => String(record.id) === String(this.promptIdOf(path)));
|
|
680
|
+
if (found === undefined)
|
|
681
|
+
throw new Error('prompt written but index refresh failed');
|
|
682
|
+
return found;
|
|
683
|
+
}
|
|
684
|
+
/** 删除模板:md 文件与索引一并删除;未命中返回 false。 */
|
|
685
|
+
async promptRemove(idOrName, projectCwd) {
|
|
686
|
+
let file = null;
|
|
687
|
+
try {
|
|
688
|
+
file = await this.promptGet(idOrName, projectCwd);
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
rmSync(file.path, { force: true });
|
|
694
|
+
await this.refreshPromptIndex(projectCwd);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
350
697
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { MemoryConfig } from './engine.ts';
|
|
|
9
9
|
export { MemoryEngine } from './engine.ts';
|
|
10
10
|
export type { MemoryChange, MemoryConfig, MemoryFilter, MemoryHit, MemoryNamespace, MemoryRecord, MemoryStatus, MemoryWrite, } from './engine.ts';
|
|
11
11
|
export { MemoryId } from './engine.ts';
|
|
12
|
-
export { bm25Scores, tokenize } from './bm25.ts';
|
|
12
|
+
export { bm25FieldScores, bm25Scores, cosineSimilarity, rrfFuse, tokenize } from './bm25.ts';
|
|
13
13
|
export declare const name = "dsh-memory";
|
|
14
14
|
export declare const inject: string[];
|
|
15
15
|
export declare function apply(ctx: Context, config?: MemoryConfig): Promise<void>;
|