dsh-prime-memory 0.22.1 → 0.24.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.en.md +401 -0
- package/CHANGELOG.ja.md +401 -0
- package/CHANGELOG.ko.md +401 -0
- package/CHANGELOG.md +100 -0
- package/dist/client.js +1951 -1188
- package/dist/config.d.ts +176 -1240
- package/dist/config.js +113 -2
- package/dist/conflict-service.d.ts +12 -0
- package/dist/conflict-service.js +24 -0
- package/dist/contract.d.ts +327 -0
- package/dist/domain-gate.d.ts +19 -0
- package/dist/domain-gate.js +45 -0
- package/dist/graph/constraints.d.ts +4 -2
- package/dist/graph/constraints.js +4 -2
- package/dist/hooks/obsolete-notice.d.ts +26 -0
- package/dist/hooks/obsolete-notice.js +64 -0
- package/dist/hooks/recall.d.ts +41 -0
- package/dist/hooks/recall.js +127 -28
- package/dist/hooks/skill-recall.d.ts +25 -0
- package/dist/hooks/skill-recall.js +72 -0
- package/dist/index.d.ts +1 -1029
- package/dist/index.js +55 -2
- package/dist/memory-types.d.ts +69 -0
- package/dist/memory-types.js +165 -0
- package/dist/memport/format.d.ts +75 -0
- package/dist/memport/format.js +345 -0
- package/dist/memport/prefilter.d.ts +62 -0
- package/dist/memport/prefilter.js +132 -0
- package/dist/memport/scan.d.ts +13 -0
- package/dist/memport/scan.js +100 -0
- package/dist/metadata-validators.d.ts +22 -0
- package/dist/metadata-validators.js +45 -0
- package/dist/pipeline/l1.d.ts +44 -2
- package/dist/pipeline/l1.js +177 -9
- package/dist/pipeline/md-sync.d.ts +65 -0
- package/dist/pipeline/md-sync.js +456 -0
- package/dist/pipeline/ruminate.d.ts +46 -1
- package/dist/pipeline/ruminate.js +150 -1
- package/dist/pipeline/runner.d.ts +10 -0
- package/dist/pipeline/runner.js +7 -1
- package/dist/pipeline/slot-migrate.d.ts +45 -0
- package/dist/pipeline/slot-migrate.js +45 -0
- package/dist/pipeline/workspace-relink.d.ts +60 -0
- package/dist/pipeline/workspace-relink.js +73 -0
- package/dist/projection/recall.d.ts +112 -0
- package/dist/projection/recall.js +183 -0
- package/dist/projection/slots.d.ts +5 -0
- package/dist/prompts/examples.d.ts +73 -0
- package/dist/prompts/examples.js +178 -0
- package/dist/prompts/graph-projection.js +3 -0
- package/dist/prompts/l1-dedup.d.ts +9 -3
- package/dist/prompts/l1-dedup.js +34 -13
- package/dist/prompts/l1-extraction.d.ts +9 -4
- package/dist/prompts/l1-extraction.js +76 -8
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +15 -1
- package/dist/skills/crystallize.d.ts +41 -0
- package/dist/skills/crystallize.js +75 -0
- package/dist/stats.d.ts +30 -1
- package/dist/stats.js +504 -13
- package/dist/store/embedding-source.d.ts +8 -0
- package/dist/store/embedding-source.js +39 -2
- package/dist/store/file-versions.d.ts +4 -0
- package/dist/store/file-versions.js +4 -0
- package/dist/store/graph-store.d.ts +14 -0
- package/dist/store/graph-store.js +35 -0
- package/dist/store/l1-snapshot.d.ts +26 -1
- package/dist/store/l1-snapshot.js +53 -8
- package/dist/store/l1.d.ts +77 -3
- package/dist/store/l1.js +128 -3
- package/dist/store/memory-md-archive.d.ts +84 -0
- package/dist/store/memory-md-archive.js +155 -0
- package/dist/store/memory-md-disable.d.ts +23 -0
- package/dist/store/memory-md-disable.js +106 -0
- package/dist/store/memory-md-export.d.ts +50 -0
- package/dist/store/memory-md-export.js +200 -0
- package/dist/store/memory-md-ledger.d.ts +94 -0
- package/dist/store/memory-md-ledger.js +243 -0
- package/dist/store/memory-md-orphan.d.ts +27 -0
- package/dist/store/memory-md-orphan.js +153 -0
- package/dist/store/memory-md-parse.d.ts +35 -0
- package/dist/store/memory-md-parse.js +85 -0
- package/dist/store/memory-md-skeleton.d.ts +18 -0
- package/dist/store/memory-md-skeleton.js +61 -0
- package/dist/store/memory-md-types.d.ts +174 -0
- package/dist/store/memory-md-types.js +10 -0
- package/dist/store/memory-md.d.ts +108 -0
- package/dist/store/memory-md.js +219 -0
- package/dist/store/memory-sources.d.ts +59 -0
- package/dist/store/memory-sources.js +214 -0
- package/dist/store/obsolete-notices.d.ts +39 -0
- package/dist/store/obsolete-notices.js +153 -0
- package/dist/store/recall-feedback.d.ts +75 -0
- package/dist/store/recall-feedback.js +213 -0
- package/dist/store/recall-usage.d.ts +11 -0
- package/dist/store/recall-usage.js +29 -0
- package/dist/store/session-recalled.d.ts +60 -0
- package/dist/store/session-recalled.js +310 -0
- package/dist/store/skill-store.d.ts +99 -0
- package/dist/store/skill-store.js +330 -0
- package/dist/store/sqlite.d.ts +71 -0
- package/dist/store/sqlite.js +259 -15
- package/dist/store/supersede.d.ts +1 -1
- package/dist/store/supersede.js +1 -1
- package/dist/tools/index.d.ts +4 -13
- package/dist/tools/index.js +80 -12
- package/dist/tools/skills.d.ts +13 -0
- package/dist/tools/skills.js +130 -0
- package/dist/types.d.ts +30 -7
- package/dist/types.js +10 -2
- package/dist/util/maturity.d.ts +44 -0
- package/dist/util/maturity.js +43 -0
- package/dist/util/md-name.d.ts +129 -0
- package/dist/util/md-name.js +366 -0
- package/dist/util/md-normalize.d.ts +23 -0
- package/dist/util/md-normalize.js +131 -0
- package/dist/wing-backfill.js +7 -4
- package/dist/workspace-route.d.ts +22 -0
- package/dist/workspace-route.js +66 -0
- package/dsh.plugin.json +1 -1
- package/package.json +6 -5
package/dist/stats.js
CHANGED
|
@@ -15,10 +15,12 @@ import { createRequire } from 'node:module';
|
|
|
15
15
|
import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { EFFORT_CHOICES, resolveDataDir } from './config.js';
|
|
18
|
+
import { typeCatalogEntries } from './memory-types.js';
|
|
18
19
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
19
20
|
import { emptyRecallStats } from './hooks/recall.js';
|
|
20
21
|
import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChainOrNull, resolveModelContextWindow, resolveModelEfforts, resolveModelRoute } from './llm.js';
|
|
21
22
|
import { mergeRoom, renameRoom } from './room-review.js';
|
|
23
|
+
import { scanSources } from './memport/scan.js';
|
|
22
24
|
import { projectDistillChain, validateDistillChain } from './settings.js';
|
|
23
25
|
import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
|
|
24
26
|
import { getTraceStore } from './store/trace.js';
|
|
@@ -33,6 +35,16 @@ import { isWingCorner } from './store/session-modes.js';
|
|
|
33
35
|
import { startWingBackfill } from './wing-backfill.js';
|
|
34
36
|
import { errDetail } from './util/filelog.js';
|
|
35
37
|
import { snapshotTokenCost } from './token-cost.js';
|
|
38
|
+
import { computeMaturity } from './util/maturity.js';
|
|
39
|
+
import { isFeedbackScore } from './store/recall-feedback.js';
|
|
40
|
+
import { relinkWorkspaces } from './pipeline/workspace-relink.js';
|
|
41
|
+
import { exportRecords, reconcileLedger } from './store/memory-md-export.js';
|
|
42
|
+
import { disableProjectionFile } from './store/memory-md-disable.js';
|
|
43
|
+
import { MEMORY_TYPE_DIRS, UNSORTED_DIR, resolveMemoriesRoot, targetFor } from './store/memory-md.js';
|
|
44
|
+
import { ensureTypeSkeleton } from './store/memory-md-skeleton.js';
|
|
45
|
+
import { sweepOrphanFiles } from './store/memory-md-orphan.js';
|
|
46
|
+
import { collectProjectionFiles } from './store/memory-md-archive.js';
|
|
47
|
+
import { syncFromFiles } from './pipeline/md-sync.js';
|
|
36
48
|
const require = createRequire(import.meta.url);
|
|
37
49
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
38
50
|
/**
|
|
@@ -93,6 +105,19 @@ export const MEMORY_ENDPOINTS = [
|
|
|
93
105
|
'dsh-memory/cleanup-retired',
|
|
94
106
|
'dsh-memory/snapshots-list',
|
|
95
107
|
'dsh-memory/snapshot-restore',
|
|
108
|
+
'dsh-memory/memory-sources-list',
|
|
109
|
+
'dsh-memory/memory-sources-add',
|
|
110
|
+
'dsh-memory/memory-sources-retire',
|
|
111
|
+
'dsh-memory/memport-scan',
|
|
112
|
+
'dsh-memory/skills-list',
|
|
113
|
+
'dsh-memory/session-recalled',
|
|
114
|
+
'dsh-memory/recall-feedback',
|
|
115
|
+
'dsh-memory/workspace-relink',
|
|
116
|
+
'dsh-memory/mem-export',
|
|
117
|
+
'dsh-memory/mem-reconcile',
|
|
118
|
+
'dsh-memory/mem-sync',
|
|
119
|
+
'dsh-memory/mem-orphans',
|
|
120
|
+
'dsh-memory/skills-action',
|
|
96
121
|
];
|
|
97
122
|
/** HTTP 路由前缀(客户端 fetch `/dsh-memory/rpc/<短方法名>`)。 */
|
|
98
123
|
const RPC_ROUTE_PREFIX = '/dsh-memory/rpc';
|
|
@@ -129,6 +154,23 @@ function writeJson(res, status, body) {
|
|
|
129
154
|
res.writeHead(status, { 'content-type': 'application/json' });
|
|
130
155
|
res.end(JSON.stringify(body));
|
|
131
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* 面板检索软超时(用户裁决:删除后检索面短暂变慢不得挂死面板):竞速到期即
|
|
159
|
+
* 返回业务错误(客户端显示可重试的提示,而不是永远「加载中」)。底层检索
|
|
160
|
+
* 稍后自然完成时其结果被 race 吸收,不产生未处理拒绝。
|
|
161
|
+
*/
|
|
162
|
+
function raceTimeout(p, ms, message) {
|
|
163
|
+
let timer;
|
|
164
|
+
const timeout = new Promise((_, reject) => {
|
|
165
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
166
|
+
});
|
|
167
|
+
return Promise.race([p, timeout]).finally(() => {
|
|
168
|
+
if (timer !== undefined)
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/** 面板关键词检索的软超时上限:正常检索是毫秒级,8s 足够宽裕。 */
|
|
173
|
+
const SEARCH_SOFT_TIMEOUT_MS = 8_000;
|
|
132
174
|
/** 读取 JSON 请求体(上限 8MB:面板最大载荷为 list-records 分页与图谱检索)。 */
|
|
133
175
|
function readJsonBody(req) {
|
|
134
176
|
return new Promise((resolve, reject) => {
|
|
@@ -173,6 +215,8 @@ export function buildEndpointDeps(base, sources, controller) {
|
|
|
173
215
|
sessionInfo: sources.sessionInfo,
|
|
174
216
|
ruminate: controller,
|
|
175
217
|
roomRegistry: sources.roomRegistry,
|
|
218
|
+
memorySources: sources.memorySources,
|
|
219
|
+
skillStore: sources.skillStore,
|
|
176
220
|
};
|
|
177
221
|
return { ...base, ...injected };
|
|
178
222
|
}
|
|
@@ -180,7 +224,11 @@ export function registerMemoryRpc(ctx, cfg, stores, logger, status, live, modes,
|
|
|
180
224
|
/** 反刍控制器(存储降级时为 undefined):经 buildEndpointDeps 落入 deps.ruminate。 */
|
|
181
225
|
ruminate,
|
|
182
226
|
/** Room 分类管理依赖(注册表;缺省 = rooms-get 不带 registry、rooms-export 恒 404 语义)。 */
|
|
183
|
-
roomRegistry
|
|
227
|
+
roomRegistry,
|
|
228
|
+
/** 外部来源注册表(轨3 Connect;经 buildEndpointDeps 落入 deps.memorySources)。 */
|
|
229
|
+
memorySources,
|
|
230
|
+
/** 技能存储(轨2;经 buildEndpointDeps 落入 deps.skillStore)。 */
|
|
231
|
+
skillStore) {
|
|
184
232
|
/** 当前是否持有一段有效注册(dispose 完成后清空,允许服务重上线时重注册)。 */
|
|
185
233
|
let holding = false;
|
|
186
234
|
/** 当前 handle 绑定的 connection 实例(internal/service 第二参;用于识别实例替换)。 */
|
|
@@ -201,7 +249,7 @@ roomRegistry) {
|
|
|
201
249
|
const value = await handleEndpoint(endpoint, payload, buildEndpointDeps({ ctx, cfg, stores, logger },
|
|
202
250
|
// roomRegistry 必须随 sources 下发——漏带 = 面板侧 room-* 恒"未装配"
|
|
203
251
|
// (工具面直传不受影响;2026-10-02 beta.9 实测回归)
|
|
204
|
-
{ status, live, modes, dataDir, rebuild, embedManager, sessionInfo, roomRegistry }, ruminate));
|
|
252
|
+
{ status, live, modes, dataDir, rebuild, embedManager, sessionInfo, roomRegistry, memorySources, skillStore }, ruminate));
|
|
205
253
|
return { ok: true, value };
|
|
206
254
|
}
|
|
207
255
|
catch (err) {
|
|
@@ -349,7 +397,7 @@ function sanitizeSettings(s) {
|
|
|
349
397
|
}
|
|
350
398
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
351
399
|
export async function handleEndpoint(endpoint, payload, deps) {
|
|
352
|
-
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, roomRegistry } = deps;
|
|
400
|
+
const { cfg, stores, status, live, modes, dataDir, rebuild, ruminate, embedManager, sessionInfo, roomRegistry, memorySources, skillStore } = deps;
|
|
353
401
|
switch (endpoint) {
|
|
354
402
|
case 'dsh-memory/stats':
|
|
355
403
|
return buildStats(cfg, stores, status);
|
|
@@ -386,6 +434,234 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
386
434
|
};
|
|
387
435
|
return v;
|
|
388
436
|
}
|
|
437
|
+
case 'dsh-memory/session-recalled': {
|
|
438
|
+
// G-live:返回本会话(按 sessionKey,与投影 header.id / pill sessionId 同源)已注入、
|
|
439
|
+
// 尚未被压缩覆盖的召回记忆。
|
|
440
|
+
const p = (payload ?? {});
|
|
441
|
+
const sessionId = expectSessionId(p.sessionId);
|
|
442
|
+
const ids = stores.recalled?.list(sessionId) ?? [];
|
|
443
|
+
if (ids.length === 0)
|
|
444
|
+
return { sessionId, records: [], typeCatalog: typeCatalogEntries() };
|
|
445
|
+
// 压缩水位之上的那批:正文已被摘要取代。与 retired 正交(真实实现见 session-recalled.ts)。
|
|
446
|
+
const superseded = stores.recalled?.supersededOf(sessionId);
|
|
447
|
+
const halfLife = effectiveCfg(cfg, live).recall?.decayHalfLifeDays ?? 0;
|
|
448
|
+
const byId = new Map(stores.l1.getByIds(ids).map((r) => [r.id, r]));
|
|
449
|
+
const records = ids
|
|
450
|
+
.map((id) => byId.get(id))
|
|
451
|
+
.filter((r) => r !== undefined)
|
|
452
|
+
.map((r) => {
|
|
453
|
+
const ui = hitToUiRecord(r, {
|
|
454
|
+
agingWeight: agingWeightOf(r.updatedAt, halfLife),
|
|
455
|
+
usage: stores.usage?.get(r.id) ?? null,
|
|
456
|
+
userRelevance: stores.feedback?.meanOf(r.id) ?? null,
|
|
457
|
+
userMark: stores.feedback?.lastOf(r.id) ?? null,
|
|
458
|
+
});
|
|
459
|
+
return superseded?.has(r.id) === true ? { ...ui, superseded: true } : ui;
|
|
460
|
+
});
|
|
461
|
+
return { sessionId, records, typeCatalog: typeCatalogEntries() };
|
|
462
|
+
}
|
|
463
|
+
case 'dsh-memory/recall-feedback': {
|
|
464
|
+
// 人在召回卡上给一条召回记忆打分:0..5 = 相关度(只聚合,不改写任何排序分);
|
|
465
|
+
// -1 = 声明过时,做两件事:退场该记录(软删可恢复)+ 排一条下一轮向模型的播报
|
|
466
|
+
//(取走即离队,不常驻)。条目**不**从本会话活召回集摘掉——它得留在列表里显示删除线。
|
|
467
|
+
if (cfg.recallFeedback?.enabled !== true)
|
|
468
|
+
throw new Error('召回反馈未开启(cfg.recallFeedback.enabled)');
|
|
469
|
+
const feedback = stores.feedback;
|
|
470
|
+
if (feedback === undefined)
|
|
471
|
+
throw new Error('召回反馈存储未装配');
|
|
472
|
+
const p = (payload ?? {});
|
|
473
|
+
const sessionId = expectSessionId(typeof p.sessionId === 'string' ? p.sessionId : undefined);
|
|
474
|
+
if (typeof p.id !== 'string' || p.id.length === 0)
|
|
475
|
+
throw new Error('id 缺失');
|
|
476
|
+
if (p.id.length > 512)
|
|
477
|
+
throw new Error('id 过长(≤512 字符)');
|
|
478
|
+
const id = p.id;
|
|
479
|
+
if (!isFeedbackScore(p.score))
|
|
480
|
+
throw new Error('score 必须是 -1 或 0..5');
|
|
481
|
+
// 结构性护栏(代替全局高权限模式):只能给**本会话真召回过、且还活着**的记录打分。
|
|
482
|
+
// 否则一个面对 web 的写端点就能退场任意 id;而这张卡上本来也只列得出这些。
|
|
483
|
+
const alive = stores.recalled?.list(sessionId) ?? [];
|
|
484
|
+
if (!alive.includes(id))
|
|
485
|
+
throw new Error('该记忆不在本会话的召回集里(可能已被压缩覆盖)');
|
|
486
|
+
const entry = feedback.rate(id, p.score);
|
|
487
|
+
let retired = false;
|
|
488
|
+
let announced = false;
|
|
489
|
+
// 退场**前**的记录快照(带 workspaceId/name)——失效联动要靠它算落点,
|
|
490
|
+
// 而 `retire` 只改 `valid_to`,不会动这两列,所以取在退场前后等价,取在前更直白。
|
|
491
|
+
let rec;
|
|
492
|
+
if (p.score === -1) {
|
|
493
|
+
rec = stores.l1.getByIds([id])[0];
|
|
494
|
+
const retiredAt = new Date().toISOString();
|
|
495
|
+
retired = stores.l1.retire([id], { at: retiredAt, reason: 'obsolete' }) > 0;
|
|
496
|
+
// 不再摘除:条目要留在列表里显示删除线。检索侧已由 detachL1FromRetrieval 阻断再注入。
|
|
497
|
+
// 并且标成**受保护**:这张列表不再被压缩清掉,但仍有上限淘汰——判过时的条目
|
|
498
|
+
// 是人的裁决凭证,不能因为“后面又召回了几条”而被挤出去。
|
|
499
|
+
stores.recalled?.protect(sessionId, [id]);
|
|
500
|
+
if (cfg.recallFeedback?.announce !== false && stores.notices !== undefined && rec !== undefined) {
|
|
501
|
+
const title = (rec.content.split('\n').find((l) => l.trim().length > 0) ?? rec.content).trim().slice(0, 120);
|
|
502
|
+
stores.notices.push(sessionId, { id, title, at: Date.now() });
|
|
503
|
+
announced = true;
|
|
504
|
+
}
|
|
505
|
+
deps.logger.info(`[memory] 用户声明记忆过时并退场(session=${sessionId}, ${retired ? '已退场' : '本已退场'}): ${id}`);
|
|
506
|
+
// 失效联动(N11,用户裁决改异步):退场成立后**后台**去动投射文件——
|
|
507
|
+
// 文件改名/建凭证不再阻塞本 RPC(面板只等库侧结果);它内部已收敛所有
|
|
508
|
+
// 失败为 'failed',这里再兜一层 catch 只落日志。响应不再带 fileDisabled
|
|
509
|
+
// (面板无消费方),磁盘凭证的对账走 mem-reconcile。
|
|
510
|
+
if (retired && rec !== undefined && cfg.memExport?.enabled === true && stores.mdLedger !== undefined) {
|
|
511
|
+
void disableProjectionFile({
|
|
512
|
+
// 传退场**后**的快照:渲染出的 frontmatter 才带 `retired: true`,
|
|
513
|
+
// 凭证自己就能说清它为什么存在。
|
|
514
|
+
record: { ...rec, validTo: Date.parse(retiredAt) || Date.now() },
|
|
515
|
+
cfgRoot: cfg.memExport.root,
|
|
516
|
+
dataDir: deps.dataDir,
|
|
517
|
+
ledger: stores.mdLedger,
|
|
518
|
+
logger: deps.logger,
|
|
519
|
+
})
|
|
520
|
+
.then((outcome) => deps.logger.info(`[memory] 投射文件失效联动(后台): ${id} → ${outcome}`))
|
|
521
|
+
.catch((err) => deps.logger.warn(`[memory] 投射文件失效联动(后台)异常: ${err instanceof Error ? err.message : String(err)}`));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
id,
|
|
526
|
+
score: p.score,
|
|
527
|
+
rating: { last: entry.last, rated: entry.rated, mean: entry.mean, obsolete: entry.obsolete },
|
|
528
|
+
retired,
|
|
529
|
+
announced,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
case 'dsh-memory/workspace-relink': {
|
|
533
|
+
// 存量记忆的 workspace 回填(内容路由)。**缺省干跑**:先看命中率与分布报告,再决定真写。
|
|
534
|
+
// 真写额外要求高权限门(它改的是库里的列,虽然只改 `workspace_id`)。
|
|
535
|
+
const wsSrc = effectiveCfg(cfg, live).workspaceSource;
|
|
536
|
+
if (wsSrc?.enabled !== true)
|
|
537
|
+
throw new Error('内容路由未开启(cfg.workspaceSource.enabled)');
|
|
538
|
+
const p = (payload ?? {});
|
|
539
|
+
const dryRun = p.dryRun !== false;
|
|
540
|
+
if (!dryRun && live?.get().memoryMutate !== true)
|
|
541
|
+
throw new Error('真写归属列需要高权限模式(memoryMutate)');
|
|
542
|
+
return relinkWorkspaces({
|
|
543
|
+
l1: {
|
|
544
|
+
list: (o) => stores.l1.list(o),
|
|
545
|
+
setWorkspaceIds: (items) => stores.l1.setWorkspaceIds(items),
|
|
546
|
+
},
|
|
547
|
+
routes: wsSrc.routes,
|
|
548
|
+
dryRun,
|
|
549
|
+
maxRecords: wsSrc.maxRecords,
|
|
550
|
+
timeBudgetMs: wsSrc.timeBudgetMs,
|
|
551
|
+
logger: deps.logger,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
case 'dsh-memory/mem-export': {
|
|
555
|
+
// 单向导出:库 → md 文件 + 账本。**不写检索面**(用例钉住:导出前后 hashRecords 相同)。
|
|
556
|
+
// 先查配置门再查装配门:“整面默认关”时该听到的是“没开”,不是“没装配”。
|
|
557
|
+
const exp = cfg.memExport;
|
|
558
|
+
if (exp?.enabled !== true)
|
|
559
|
+
throw new Error('md 投射未开启(cfg.memExport.enabled)');
|
|
560
|
+
const md = stores.mdLedger;
|
|
561
|
+
if (md === undefined)
|
|
562
|
+
throw new Error('md 投射账本未装配');
|
|
563
|
+
// 命名回填:没名字的记录先拿到全库唯一的 `name`,再算落点。
|
|
564
|
+
// 不回填就是真机那个形状:1166 条 name 全空、42 条挤进 17 个同名文件,
|
|
565
|
+
// 文件互相覆盖 + 导出每轮重刷 + 反向同步把一份内容回灌给多条。
|
|
566
|
+
// 它只写 `name` 列(不在快照哈希字段表里),所以 hashRecords 仍逐字不变。
|
|
567
|
+
const all = stores.l1.all();
|
|
568
|
+
const naming = stores.l1.assignMissingNames(all, { persist: true });
|
|
569
|
+
if (naming.failed > 0) {
|
|
570
|
+
deps.logger?.warn(`[memory] md 投射命名回填失败 ${naming.failed} 条(下轮导出会重试)`);
|
|
571
|
+
}
|
|
572
|
+
// 类型目录骨架(pm-name-distill Phase 4):37 类目录各放一个 `_index.md`(类名 + 库里条数)。
|
|
573
|
+
// 目录集合从词表真源派生(在 ensureTypeSkeleton 内),这里只喂 countsByType 与 roots。
|
|
574
|
+
// 单根必补;workspace 根**按需**——只给本轮 `targetFor` 算出真有落点(inWorkspace)的根补,
|
|
575
|
+
// 不建空 workspace 根(消失的 workspace 已由 targetFor 护栏退回单根,这里不绕过它造目录树)。
|
|
576
|
+
// 配置门在前(enabled!==true 已在上头抛错),故关着时该端点根本到不了这里 → 骨架零动作。
|
|
577
|
+
const countsByType = new Map();
|
|
578
|
+
const skeletonRoots = new Set([resolveMemoriesRoot(exp.root, deps.dataDir, deps.logger)]);
|
|
579
|
+
for (const r of all) {
|
|
580
|
+
const dir = MEMORY_TYPE_DIRS.has(r.type) ? r.type : UNSORTED_DIR;
|
|
581
|
+
countsByType.set(dir, (countsByType.get(dir) ?? 0) + 1);
|
|
582
|
+
const t = targetFor(r, exp.root, deps.dataDir, deps.logger);
|
|
583
|
+
if (t.inWorkspace)
|
|
584
|
+
skeletonRoots.add(t.root);
|
|
585
|
+
}
|
|
586
|
+
await ensureTypeSkeleton({ roots: [...skeletonRoots], countsByType, logger: deps.logger });
|
|
587
|
+
const rep = await exportRecords({
|
|
588
|
+
records: all,
|
|
589
|
+
cfgRoot: exp.root,
|
|
590
|
+
dataDir: deps.dataDir,
|
|
591
|
+
ledger: md,
|
|
592
|
+
maxRecords: exp.maxRecords,
|
|
593
|
+
timeBudgetMs: exp.timeBudgetMs,
|
|
594
|
+
logger: deps.logger,
|
|
595
|
+
});
|
|
596
|
+
return { ...rep, named: naming.named, nameFailed: naming.failed };
|
|
597
|
+
}
|
|
598
|
+
case 'dsh-memory/mem-reconcile': {
|
|
599
|
+
// 只读对账:不写库、不写文件,只按账本基线出四态报告(所以不需高权限门)。
|
|
600
|
+
const exp = cfg.memExport;
|
|
601
|
+
if (exp?.enabled !== true)
|
|
602
|
+
throw new Error('md 投射未开启(cfg.memExport.enabled)');
|
|
603
|
+
const md = stores.mdLedger;
|
|
604
|
+
if (md === undefined)
|
|
605
|
+
throw new Error('md 投射账本未装配');
|
|
606
|
+
return reconcileLedger({
|
|
607
|
+
records: stores.l1.all(),
|
|
608
|
+
cfgRoot: exp.root,
|
|
609
|
+
dataDir: deps.dataDir,
|
|
610
|
+
ledger: md,
|
|
611
|
+
maxRecords: exp.maxRecords,
|
|
612
|
+
timeBudgetMs: exp.timeBudgetMs,
|
|
613
|
+
logger: deps.logger,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
case 'dsh-memory/mem-sync': {
|
|
617
|
+
// P2 反向同步:文件 → 库。与导出的**风险不对称**——导出只写文件,本项会写库,
|
|
618
|
+
// 所以多两道门:要先开投射(没导出过 = 账本为空 = 判不出“谁变了”),要高权限。
|
|
619
|
+
const syn = cfg.mdSync;
|
|
620
|
+
if (syn?.enabled !== true)
|
|
621
|
+
throw new Error('md 反向同步未开启(cfg.mdSync.enabled)');
|
|
622
|
+
if (cfg.memExport?.enabled !== true)
|
|
623
|
+
throw new Error('md 反向同步需先开启 md 投射(cfg.memExport.enabled)');
|
|
624
|
+
if (!live?.get().memoryMutate)
|
|
625
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能回灌文件侧修改。');
|
|
626
|
+
const md = stores.mdLedger;
|
|
627
|
+
if (md === undefined)
|
|
628
|
+
throw new Error('md 投射账本未装配');
|
|
629
|
+
return syncFromFiles({
|
|
630
|
+
store: stores.l1,
|
|
631
|
+
ledger: md,
|
|
632
|
+
cfgRoot: cfg.memExport.root,
|
|
633
|
+
dataDir: deps.dataDir,
|
|
634
|
+
maxFiles: syn.maxFiles,
|
|
635
|
+
timeBudgetMs: syn.timeBudgetMs,
|
|
636
|
+
logger: deps.logger,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
case 'dsh-memory/mem-orphans': {
|
|
640
|
+
// P4.4 孤儿归档:只搬“账本记着我们写过、但现在没记录认领它”的投射文件。
|
|
641
|
+
// 门序与同步侧一致:配置门 → 装配门 → 高权限门(干跑不需要)。**默认干跑**:
|
|
642
|
+
// 搬文件在用户眼里和删文件差不离,必须显式要。
|
|
643
|
+
const exp = cfg.memExport;
|
|
644
|
+
if (exp?.enabled !== true)
|
|
645
|
+
throw new Error('md 投射未开启(cfg.memExport.enabled)');
|
|
646
|
+
const md = stores.mdLedger;
|
|
647
|
+
if (md === undefined)
|
|
648
|
+
throw new Error('md 投射账本未装配');
|
|
649
|
+
const p = (payload ?? {});
|
|
650
|
+
const dryRun = p.dryRun !== false;
|
|
651
|
+
if (!dryRun && live?.get().memoryMutate !== true) {
|
|
652
|
+
throw new Error('孤儿文件归档需要高权限模式(memoryMutate)');
|
|
653
|
+
}
|
|
654
|
+
return sweepOrphanFiles({
|
|
655
|
+
records: stores.l1.all(),
|
|
656
|
+
cfgRoot: exp.root,
|
|
657
|
+
dataDir: deps.dataDir,
|
|
658
|
+
ledger: md,
|
|
659
|
+
dryRun,
|
|
660
|
+
maxFiles: exp.maxRecords,
|
|
661
|
+
timeBudgetMs: exp.timeBudgetMs,
|
|
662
|
+
logger: deps.logger,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
389
665
|
case 'dsh-memory/session-mode-set': {
|
|
390
666
|
if (!modes)
|
|
391
667
|
throw new Error('档位存储未初始化');
|
|
@@ -499,6 +775,150 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
499
775
|
};
|
|
500
776
|
return v;
|
|
501
777
|
}
|
|
778
|
+
// ── 轨3 外部记忆来源(Connect):登记/列表/退役/只读扫描。整面默认关(cfg.memport.enabled)。
|
|
779
|
+
case 'dsh-memory/memory-sources-list': {
|
|
780
|
+
const enabled = cfg.memport?.enabled === true;
|
|
781
|
+
const v = {
|
|
782
|
+
sources: enabled && memorySources
|
|
783
|
+
? memorySources.list().map((e) => ({
|
|
784
|
+
id: e.id,
|
|
785
|
+
kind: e.kind,
|
|
786
|
+
path: e.path,
|
|
787
|
+
label: e.label,
|
|
788
|
+
lastScanAt: e.lastScanAt,
|
|
789
|
+
lastImportAt: e.lastImportAt,
|
|
790
|
+
disabled: e.disabled,
|
|
791
|
+
status: e.status,
|
|
792
|
+
}))
|
|
793
|
+
: [],
|
|
794
|
+
memoryMutate: live?.get().memoryMutate === true,
|
|
795
|
+
};
|
|
796
|
+
return v;
|
|
797
|
+
}
|
|
798
|
+
case 'dsh-memory/memory-sources-add': {
|
|
799
|
+
if (cfg.memport?.enabled !== true)
|
|
800
|
+
throw new Error('外部记忆来源未启用(config memport.enabled)。');
|
|
801
|
+
if (!memorySources)
|
|
802
|
+
throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
|
|
803
|
+
if (!live?.get().memoryMutate)
|
|
804
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能登记外部来源。');
|
|
805
|
+
const p = (payload ?? {});
|
|
806
|
+
const r = await memorySources.register({
|
|
807
|
+
id: String(p.id ?? '').trim(),
|
|
808
|
+
kind: String(p.kind ?? 'file') ?? 'file',
|
|
809
|
+
path: String(p.path ?? '').trim(),
|
|
810
|
+
label: typeof p.label === 'string' ? p.label : undefined,
|
|
811
|
+
});
|
|
812
|
+
const v = {
|
|
813
|
+
id: r.entry.id,
|
|
814
|
+
created: r.created,
|
|
815
|
+
notice: r.created ? '已登记来源。' : '来源已存在(已按新 path/label 原地更新)。',
|
|
816
|
+
};
|
|
817
|
+
return v;
|
|
818
|
+
}
|
|
819
|
+
case 'dsh-memory/memory-sources-retire': {
|
|
820
|
+
if (cfg.memport?.enabled !== true)
|
|
821
|
+
throw new Error('外部记忆来源未启用(config memport.enabled)。');
|
|
822
|
+
if (!memorySources)
|
|
823
|
+
throw new Error('外部来源注册表未装配(需要插件重载以初始化)。');
|
|
824
|
+
if (!live?.get().memoryMutate)
|
|
825
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能管理外部来源。');
|
|
826
|
+
const p = (payload ?? {});
|
|
827
|
+
const changed = await memorySources.setStatus(String(p.id ?? '').trim(), p.active === true ? 'active' : 'retired');
|
|
828
|
+
const v = {
|
|
829
|
+
changed,
|
|
830
|
+
notice: !changed
|
|
831
|
+
? '未找到该来源。'
|
|
832
|
+
: p.active === true
|
|
833
|
+
? '已恢复来源。'
|
|
834
|
+
: '已退役来源(不删已导入数据;来源路径与 ext_key 仍在,可重放)。',
|
|
835
|
+
};
|
|
836
|
+
return v;
|
|
837
|
+
}
|
|
838
|
+
case 'dsh-memory/memport-scan': {
|
|
839
|
+
if (cfg.memport?.enabled !== true) {
|
|
840
|
+
const v = { enabled: false, sources: [], notice: '外部记忆扫描未启用(config memport.enabled)。' };
|
|
841
|
+
return v;
|
|
842
|
+
}
|
|
843
|
+
if (!memorySources) {
|
|
844
|
+
const v = { enabled: true, sources: [], notice: '外部来源注册表未装配。' };
|
|
845
|
+
return v;
|
|
846
|
+
}
|
|
847
|
+
const p = (payload ?? {});
|
|
848
|
+
const all = memorySources.listEnabled();
|
|
849
|
+
const targets = p.sourceId ? all.filter((e) => e.id === p.sourceId) : all;
|
|
850
|
+
const maxRecords = cfg.memport?.scanMaxRecords ?? 5000;
|
|
851
|
+
// 只读扫描:不写库(写走 memory_import)。逐个标扫描时间(失败不影响预览)。
|
|
852
|
+
const sources = await scanSources({ sources: targets, l1: stores.l1, maxRecords });
|
|
853
|
+
const nowIso = new Date().toISOString();
|
|
854
|
+
for (const s of targets)
|
|
855
|
+
await memorySources.markScanned(s.id, nowIso);
|
|
856
|
+
const v = { enabled: true, sources };
|
|
857
|
+
return v;
|
|
858
|
+
}
|
|
859
|
+
// ── 轨2 技能(SkillsTab):列表 + 人工审阅动作(晋升/归档/pin/改档)。整面默认关(cfg.skills.enabled)。
|
|
860
|
+
case 'dsh-memory/skills-list': {
|
|
861
|
+
const enabled = cfg.skills?.enabled === true || live?.get().skillsEnabled === true;
|
|
862
|
+
const v = {
|
|
863
|
+
enabled,
|
|
864
|
+
allowPromote: cfg.skills?.allowPromote === true,
|
|
865
|
+
memoryMutate: live?.get().memoryMutate === true,
|
|
866
|
+
skills: enabled && skillStore
|
|
867
|
+
? skillStore.list().map((s) => ({
|
|
868
|
+
id: s.id,
|
|
869
|
+
title: s.title,
|
|
870
|
+
status: s.status,
|
|
871
|
+
tier: s.injectionTier,
|
|
872
|
+
pinned: s.pinned,
|
|
873
|
+
sessionCount: skillStore.sessionCount(s.id),
|
|
874
|
+
updatedAt: s.updatedAt,
|
|
875
|
+
}))
|
|
876
|
+
: [],
|
|
877
|
+
};
|
|
878
|
+
return v;
|
|
879
|
+
}
|
|
880
|
+
case 'dsh-memory/skills-action': {
|
|
881
|
+
if (!(cfg.skills?.enabled === true || live?.get().skillsEnabled === true))
|
|
882
|
+
throw new Error('技能功能未启用(在概览页开启「技能」开关,或 config skills.enabled)。');
|
|
883
|
+
if (!skillStore)
|
|
884
|
+
throw new Error('技能存储未装配。');
|
|
885
|
+
if (!live?.get().memoryMutate)
|
|
886
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启「高权限模式」后才能管理技能。');
|
|
887
|
+
const p = (payload ?? {});
|
|
888
|
+
const id = String(p.id ?? '').trim();
|
|
889
|
+
if (!id)
|
|
890
|
+
throw new Error('id 不能为空');
|
|
891
|
+
const minSessions = cfg.skills.minSessions;
|
|
892
|
+
const v = { changed: false, notice: '' };
|
|
893
|
+
if (p.action === 'promote') {
|
|
894
|
+
// 人工审阅晋升:不受自动闸 allowPromote 约束,但仍需证据达阈(不凭空的晋升)。
|
|
895
|
+
if (skillStore.sessionCount(id) < minSessions)
|
|
896
|
+
return { changed: false, notice: `证据会话数不足(<${minSessions}),暂不能晋升。` };
|
|
897
|
+
const ok = await skillStore.tryPromote(id, minSessions, { allowPromote: true });
|
|
898
|
+
return { changed: ok, notice: ok ? '已晋升为 active。' : '未找到候选或状态已变。' };
|
|
899
|
+
}
|
|
900
|
+
if (p.action === 'archive') {
|
|
901
|
+
const ok = await skillStore.archive(id, String(p.reason ?? 'manual').slice(0, 120) || 'manual');
|
|
902
|
+
return { changed: ok, notice: ok ? '已归档(不再注入,可恢复)。' : '未找到或已归档。' };
|
|
903
|
+
}
|
|
904
|
+
if (p.action === 'pin') {
|
|
905
|
+
const ok = await skillStore.update(id, { pinned: true });
|
|
906
|
+
return { changed: ok, notice: ok ? '已固定。' : '未找到技能。' };
|
|
907
|
+
}
|
|
908
|
+
if (p.action === 'unpin') {
|
|
909
|
+
const ok = await skillStore.update(id, { pinned: false });
|
|
910
|
+
return { changed: ok, notice: ok ? '已取消固定。' : '未找到技能。' };
|
|
911
|
+
}
|
|
912
|
+
if (p.action === 'set-tier') {
|
|
913
|
+
const tier = p.tier === 'full' || p.tier === 'excerpt' || p.tier === 'hint' ? p.tier : null;
|
|
914
|
+
if (!tier)
|
|
915
|
+
throw new Error('tier 非法(仅 full/excerpt/hint)');
|
|
916
|
+
const ok = await skillStore.update(id, { injectionTier: tier });
|
|
917
|
+
return { changed: ok, notice: ok ? `已设为 ${tier} 档。` : '未找到技能。' };
|
|
918
|
+
}
|
|
919
|
+
void v;
|
|
920
|
+
throw new Error(`未知 action: ${String(p.action)}`);
|
|
921
|
+
}
|
|
502
922
|
case 'dsh-memory/room-register': {
|
|
503
923
|
if (!roomRegistry)
|
|
504
924
|
throw new Error('Room 注册表未装配(需要插件重载以初始化)。');
|
|
@@ -812,6 +1232,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
812
1232
|
embedRemoteBaseURL: '', embedRemoteApiKey: '', embedRemoteModel: '', embedRemoteDimensions: 0,
|
|
813
1233
|
memoryMutate: false,
|
|
814
1234
|
conflictFreeze: live?.get()?.conflictFreeze === true,
|
|
1235
|
+
skillsEnabled: live?.get()?.skillsEnabled === true,
|
|
815
1236
|
}),
|
|
816
1237
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
817
1238
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
@@ -850,7 +1271,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
850
1271
|
const patch = (payload ?? {});
|
|
851
1272
|
const clean = {};
|
|
852
1273
|
// 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
|
|
853
|
-
for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate']) {
|
|
1274
|
+
for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'skillsEnabled', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate']) {
|
|
854
1275
|
if (typeof patch[key] === 'boolean')
|
|
855
1276
|
clean[key] = patch[key];
|
|
856
1277
|
}
|
|
@@ -1005,12 +1426,16 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1005
1426
|
const wingCatalog = offset === 0
|
|
1006
1427
|
? [...WING_CATALOG.map((h) => ({ id: h.id, label: h.label })), { id: WING_FALLBACK, label: '跨域' }]
|
|
1007
1428
|
: undefined;
|
|
1429
|
+
// 类型词表目录随首屏下发(task_1.5:单一真源在 `memory-types.ts`,client 不手抄)。
|
|
1430
|
+
const typeCatalog = offset === 0 ? typeCatalogEntries() : undefined;
|
|
1008
1431
|
// 关键词路径:复用检索唯一缝(与召回同源),取回后做场景/Wing 过滤 + 手工分页。
|
|
1009
1432
|
// 检索侧单次上限 200:分页窗口触达上限时显式标记 truncated(结果可能不完整)。
|
|
1010
1433
|
if (p.query && p.query.trim()) {
|
|
1011
1434
|
const SEARCH_CAP = 200;
|
|
1012
1435
|
const wanted = offset + limit + 1;
|
|
1013
|
-
|
|
1436
|
+
// 软超时护栏(用户裁决):删除/繁重后台动作后的短暂检索迟滞不得把面板
|
|
1437
|
+
// 永久钉在「加载中」。到期返回可重试的业务错误。
|
|
1438
|
+
const hits = await raceTimeout(stores.l1.search(p.query, Math.min(wanted, SEARCH_CAP), { type: p.type || undefined }), SEARCH_SOFT_TIMEOUT_MS, `检索超时(${Math.round(SEARCH_SOFT_TIMEOUT_MS / 1000)}s):检索面短暂繁忙,请重试或缩小关键词`);
|
|
1014
1439
|
let filtered = p.scene ? hits.filter((h) => h.scene_name === p.scene) : hits;
|
|
1015
1440
|
// Wing / Room 过滤(检索命中不含 metadata):按 id 批量取回元数据后过滤
|
|
1016
1441
|
let metaById = null;
|
|
@@ -1043,18 +1468,40 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1043
1468
|
return true;
|
|
1044
1469
|
});
|
|
1045
1470
|
}
|
|
1046
|
-
const
|
|
1047
|
-
const
|
|
1471
|
+
const page = filtered.slice(offset, offset + limit);
|
|
1472
|
+
const pageIds = page.map((h) => h.id);
|
|
1473
|
+
// 检索命中(`L1SearchHit`)只带 7 个字段,不含 createdAt/timestamps/version/
|
|
1474
|
+
// metadata/validTo——卡片上的「创建/活跃时间」与 R7 来源锚点因此恒为占位值。
|
|
1475
|
+
// 旧缺陷有两层:① 这些字段从未补过;② `metaById` 只在勾了 Wing/tag 过滤时才建,
|
|
1476
|
+
// 于是连 metadata 都不补——锚点在默认视图下永远显示 `-`。这里按 id 回查主表
|
|
1477
|
+
// 做完整水合,与浏览路径同口径;正文/得分仍以命中为准(FTS 可能给的是摘要)。
|
|
1478
|
+
const fullById = new Map(stores.l1.getByIds(pageIds).map((r) => [r.id, r]));
|
|
1048
1479
|
const resp = {
|
|
1049
|
-
items:
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1480
|
+
items: page.map((h) => {
|
|
1481
|
+
const full = fullById.get(h.id);
|
|
1482
|
+
return hitToUiRecord({
|
|
1483
|
+
...full,
|
|
1484
|
+
id: h.id,
|
|
1485
|
+
content: h.content,
|
|
1486
|
+
type: h.type,
|
|
1487
|
+
priority: h.priority,
|
|
1488
|
+
scene_name: h.scene_name,
|
|
1489
|
+
family: h.family,
|
|
1490
|
+
score: h.score,
|
|
1491
|
+
metadata: full?.metadata ?? metaById?.get(h.id),
|
|
1492
|
+
}, {
|
|
1493
|
+
agingWeight: agingWeightOf(full?.updatedAt, stores.l1.decayHalfLife),
|
|
1494
|
+
usage: stores.usage?.get(h.id) ?? null,
|
|
1495
|
+
userRelevance: stores.feedback?.meanOf(h.id) ?? null,
|
|
1496
|
+
userMark: stores.feedback?.lastOf(h.id) ?? null,
|
|
1497
|
+
});
|
|
1498
|
+
}),
|
|
1053
1499
|
hasMore: filtered.length > offset + limit,
|
|
1054
1500
|
total: null,
|
|
1055
1501
|
truncated: wanted > SEARCH_CAP,
|
|
1056
1502
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
1057
1503
|
wingCatalog,
|
|
1504
|
+
typeCatalog,
|
|
1058
1505
|
usageSummary: stores.usage?.summary() ?? null,
|
|
1059
1506
|
};
|
|
1060
1507
|
return resp;
|
|
@@ -1066,12 +1513,15 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1066
1513
|
items: items.map((r) => hitToUiRecord(r, {
|
|
1067
1514
|
agingWeight: agingWeightOf(r.updatedAt, stores.l1.decayHalfLife),
|
|
1068
1515
|
usage: stores.usage?.get(r.id) ?? null,
|
|
1516
|
+
userRelevance: stores.feedback?.meanOf(r.id) ?? null,
|
|
1517
|
+
userMark: stores.feedback?.lastOf(r.id) ?? null,
|
|
1069
1518
|
})),
|
|
1070
1519
|
hasMore: offset + items.length < total,
|
|
1071
1520
|
total,
|
|
1072
1521
|
truncated: false,
|
|
1073
1522
|
scenes: offset === 0 ? stores.l1.distinctScenes() : undefined,
|
|
1074
1523
|
wingCatalog,
|
|
1524
|
+
typeCatalog,
|
|
1075
1525
|
usageSummary: stores.usage?.summary() ?? null,
|
|
1076
1526
|
};
|
|
1077
1527
|
return resp;
|
|
@@ -1107,7 +1557,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1107
1557
|
// (live.conflictFreeze 覆盖静态 cfg.conflictFreeze.enabled)。此前这里直接读
|
|
1108
1558
|
// cfg.conflictFreeze.enabled —— 面板开关写的是 live,而部署静态值恒 false,
|
|
1109
1559
|
// 于是开关已开、settings.yaml 已落 true,本页仍报"矛盾冻结未开启"。
|
|
1110
|
-
return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true }, { limit: Number(p.limit) || undefined });
|
|
1560
|
+
return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true, usage: stores.usage, decayHalfLifeDays: effectiveCfg(cfg, live).recall?.decayHalfLifeDays, feedback: stores.feedback }, { limit: Number(p.limit) || undefined });
|
|
1111
1561
|
}
|
|
1112
1562
|
// ── §C 矛盾冻结裁决(task_25):与 memory_resolve_conflict 工具共用同一形状 ──
|
|
1113
1563
|
// 端点层同样不给"提示文案"出口的例外只有一条:**队列未开启**不是调用错误而是
|
|
@@ -1228,6 +1678,14 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1228
1678
|
}
|
|
1229
1679
|
}
|
|
1230
1680
|
if (dryRun) {
|
|
1681
|
+
// 待删清单要给人看:`targets` 只是计数,人无法从计数判断“这批是不是该删的那批”。
|
|
1682
|
+
// 取前 50 条预览(全量列表会把面板撑爆,而判据只需抽看)。
|
|
1683
|
+
const preview = stores.l1.getByIds(targets.slice(0, 50)).map((r) => ({
|
|
1684
|
+
id: r.id,
|
|
1685
|
+
name: r.name ?? '',
|
|
1686
|
+
type: r.type,
|
|
1687
|
+
retiredAt: r.validTo === undefined ? '' : new Date(r.validTo).toISOString(),
|
|
1688
|
+
}));
|
|
1231
1689
|
const resp = {
|
|
1232
1690
|
dryRun: true,
|
|
1233
1691
|
targets: targets.length,
|
|
@@ -1236,6 +1694,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1236
1694
|
dir: '',
|
|
1237
1695
|
name: '',
|
|
1238
1696
|
diffs: [],
|
|
1697
|
+
items: preview,
|
|
1239
1698
|
};
|
|
1240
1699
|
return resp;
|
|
1241
1700
|
}
|
|
@@ -1243,7 +1702,18 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1243
1702
|
const resp = { dryRun: false, targets: 0, purged: 0, aborted: false, dir: '', name: '', diffs: [] };
|
|
1244
1703
|
return resp;
|
|
1245
1704
|
}
|
|
1246
|
-
|
|
1705
|
+
// 删前先把该批记录抓在手里:删完再读就什么都读不到了(归档正文与预览清单都要用它们)。
|
|
1706
|
+
const purgeRecords = stores.l1.getByIds(targets);
|
|
1707
|
+
// N12:投射面开着时把该批的投射文件(含 `.disable`)一并收进快照目录。
|
|
1708
|
+
// 关着时 `files` 为 undefined ⇒ `exportThenPurge` 行为与改动前逐字一致。
|
|
1709
|
+
const files = cfg.memExport?.enabled === true
|
|
1710
|
+
? await collectProjectionFiles(purgeRecords, cfg.memExport.root, deps.dataDir, deps.logger)
|
|
1711
|
+
: undefined;
|
|
1712
|
+
const r = await stores.l1.purgeRetired(targets, 'cleanup-retired', { files });
|
|
1713
|
+
// F19:图投影的两张索引表全仓原本**只增不减**。记录行删了而投影行还在,
|
|
1714
|
+
// 从快照恢复回来的记录会被当成“已投影”而永不重投影(检索图静默缺一条,不报错)。
|
|
1715
|
+
// 只在真删成功时抹:干跑与中止都不动图——图是派生索引,不该因为一次预览被改。
|
|
1716
|
+
const graphForgotten = !r.aborted && r.purged > 0 ? (stores.graph?.forgetRecords(targets) ?? 0) : 0;
|
|
1247
1717
|
const resp = {
|
|
1248
1718
|
dryRun: false,
|
|
1249
1719
|
targets: targets.length,
|
|
@@ -1252,6 +1722,16 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1252
1722
|
dir: r.dir,
|
|
1253
1723
|
name: r.name,
|
|
1254
1724
|
diffs: r.diffs,
|
|
1725
|
+
otherChanged: r.otherChanged,
|
|
1726
|
+
archiveMd: r.archiveMd,
|
|
1727
|
+
files: r.files,
|
|
1728
|
+
graphForgotten,
|
|
1729
|
+
items: purgeRecords.slice(0, 50).map((x) => ({
|
|
1730
|
+
id: x.id,
|
|
1731
|
+
name: x.name ?? '',
|
|
1732
|
+
type: x.type,
|
|
1733
|
+
retiredAt: x.validTo === undefined ? '' : new Date(x.validTo).toISOString(),
|
|
1734
|
+
})),
|
|
1255
1735
|
};
|
|
1256
1736
|
return resp;
|
|
1257
1737
|
}
|
|
@@ -1714,6 +2194,17 @@ export function hitToUiRecord(r, extra) {
|
|
|
1714
2194
|
retiredReason: retired ? (mark?.reason ?? 'unknown') : null,
|
|
1715
2195
|
agingWeight: extra?.agingWeight ?? null,
|
|
1716
2196
|
usage: extra?.usage ?? null,
|
|
2197
|
+
// G3 成熟度:读时派生(priority + used + 老化 + 人工最近一笔),无额外端点/存储。
|
|
2198
|
+
maturity: computeMaturity({
|
|
2199
|
+
priority: r.priority ?? 60,
|
|
2200
|
+
usage: extra?.usage ?? null,
|
|
2201
|
+
agingWeight: extra?.agingWeight ?? null,
|
|
2202
|
+
mark: extra?.userMark ?? null,
|
|
2203
|
+
}),
|
|
2204
|
+
// G1 使用场景:从命名空间保留键读出(功能关/无键 → undefined)。
|
|
2205
|
+
usageScenario: Array.isArray(r.metadata?.dsh_usage_scenario) ? r.metadata?.dsh_usage_scenario : undefined,
|
|
2206
|
+
// 人给相关度均值:读时从 sidecar 派生(不进存储/快照);未打分 → 字段缺席。
|
|
2207
|
+
userRelevance: extra?.userRelevance ?? undefined,
|
|
1717
2208
|
};
|
|
1718
2209
|
}
|
|
1719
2210
|
/**
|