dsh-prime-memory 0.24.2 → 0.25.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 +11 -0
- package/CHANGELOG.ja.md +11 -0
- package/CHANGELOG.ko.md +11 -0
- package/CHANGELOG.md +11 -0
- package/dist/client.js +155 -171
- package/dist/config.d.ts +12 -0
- package/dist/config.js +4 -0
- package/dist/contract.d.ts +14 -0
- package/dist/keyword-backfill.d.ts +46 -0
- package/dist/keyword-backfill.js +143 -0
- package/dist/metadata-validators.d.ts +9 -0
- package/dist/metadata-validators.js +31 -0
- package/dist/stats.js +15 -0
- package/dist/store/l1.d.ts +6 -0
- package/dist/store/l1.js +8 -0
- package/dist/store/memory-backend-worker.d.ts +1 -0
- package/dist/store/memory-backend-worker.js +3 -0
- package/dist/store/memory-backend.d.ts +3 -0
- package/dist/store/memory-backend.js +3 -0
- package/dist/store/memory-worker.js +3 -0
- package/dist/store/sqlite.d.ts +11 -0
- package/dist/store/sqlite.js +33 -0
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.en.md
CHANGED
|
@@ -7,6 +7,17 @@
|
|
|
7
7
|
|
|
8
8
|
This file covers the **0.12.0** release notes and the current **unreleased** changes in English. For the full history, see [CHANGELOG.md](./CHANGELOG.md) (Chinese).
|
|
9
9
|
|
|
10
|
+
## [0.25.0] — 2026-10-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Keyword-distillation backfill (producer for `metadata.dsh_keywords`)**: the `keywords` axis reserved in `l1_fts` had no producer and stayed dormant. New independent batch scan `src/keyword-backfill.ts` (mirrors wing-backfill: single-flight / background / bounded / failures leave records untouched) sends existing L1 records missing `dsh_keywords` to the LLM in chunks, normalizes via `normKeywords` (≤5, dedupe, length clamp) and writes back. New endpoint `dsh-memory/keyword-backfill`; a "Keyword Backfill" button in the WingWheel panel. Config `keyword.enabled` defaults **off** — off means no backfill and `l1_fts` byte-for-byte unchanged (zero drift).
|
|
15
|
+
- **`patchMetadataAndReindex`**: `patchMetadata` only updates `l1_records`, never `l1_fts`, so backfilling through it would write keywords BM25 never sees. Added per-row FTS re-indexing (threaded through MemoryDb→L1Store→backend→InProc/Worker→worker dispatch) so new keywords enter the retrieval axis immediately. Test includes a reverse control (plain patchMetadata does not match; AndReindex does).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **Input-area popup no longer renders the session recall list**: WingWheel drops `SessionRecalledArea` — recall display is now owned by brief-sidebar, so a second copy in the input popup was redundant (component file kept for rollback).
|
|
20
|
+
|
|
10
21
|
## [0.24.2] — 2026-10-14
|
|
11
22
|
|
|
12
23
|
### Added
|
package/CHANGELOG.ja.md
CHANGED
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
|
|
10
10
|
本ファイルは **0.12.0** リリースノートと現在の**未リリース**変更の日本語版です。全履歴は [CHANGELOG.md](./CHANGELOG.md)(中文)を参照してください。
|
|
11
11
|
|
|
12
|
+
## [0.25.0] — 2026-10-14
|
|
13
|
+
|
|
14
|
+
### 追加
|
|
15
|
+
|
|
16
|
+
- **キーワード蒸留フィッシュバック(`metadata.dsh_keywords` の産出側)**:`l1_fts` に予約された `keywords` 检索軸は産出側がなく休止状態でした。新モジュール `src/keyword-backfill.ts`(wing 回填を镜像:単飛/後台/有界/失敗時原記録不変)で `dsh_keywords` 缺失の存量 L1 を chunk で LLM に贈り、`normKeywords`(≤5、重複除去、長さ鉗制)で正规化して書き戻す。新端点 `dsh-memory/keyword-backfill`、パネル(WingWheel)に「キーワード回填」ボタン。設定 `keyword.enabled` はデフォルト**关**——关時は回填なし、`l1_fts` 逐字不変(ゼロドリフト)。
|
|
17
|
+
- **`patchMetadataAndReindex`**:`patchMetadata` は `l1_records` のみ更新し `l1_fts` に触れないため、これで回填すると BM25 に永遠に見えない。行別 FTS 重索引(MemoryDb→L1Store→backend→InProc/Worker→worker 分派を貫通)を追加し、新キーワードが即座に检索軸に入る。テストは反向対照(純 patchMetadata は不命中 / AndReindex は命中)入り。
|
|
18
|
+
|
|
19
|
+
### 変更
|
|
20
|
+
|
|
21
|
+
- **输入区ポップアップで本会话召回リストの描画を廃止**:WingWheel から `SessionRecalledArea` を除去——召回表示は brief-sidebar が接管済みのため重复排除(コンポーネントファイルはロールバック用に保持)。
|
|
22
|
+
|
|
12
23
|
## [0.24.2] — 2026-10-14
|
|
13
24
|
|
|
14
25
|
### 追加
|
package/CHANGELOG.ko.md
CHANGED
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
|
|
10
10
|
이 파일은 **0.12.0** 릴리스 노트와 현재 **미출시** 변경 사항의 한국어판입니다. 전체 이력은 [CHANGELOG.md](./CHANGELOG.md)(中文)를 참조하세요.
|
|
11
11
|
|
|
12
|
+
## [0.25.0] — 2026-10-14
|
|
13
|
+
|
|
14
|
+
### 신규
|
|
15
|
+
|
|
16
|
+
- **키워드 증류 백필(`metadata.dsh_keywords` 산출측)**: `l1_fts` 에 예약된 `keywords` 축은 산출측이 없어 휴면이었습니다. 신규 모듈 `src/keyword-backfill.ts`(wing 백필 미러: 단일비행/후대/유계/실패시 원 기록불변)가 `dsh_keywords` 누락 L1 을 chunk 로 LLM 에 보내 키워드와 사용 시나리오를 뽑고 `normKeywords`(≤5、중복 제거、길이鉗制)로 정규화 후 기입。新 엔드 `dsh-memory/keyword-backfill`、패널(WingWheel)에 “키워드 백필” 버튼。설정 `keyword.enabled` **기본关**——关 시 백필 없음、`l1_fts` 글자 그대로 동일(영 드리프트)。
|
|
17
|
+
- **`patchMetadataAndReindex`**: `patchMetadata` 는 `l1_records` 만更新하고 `l1_fts` 에 안 닿아서 이대로 백필하면 BM25 가 영영 못 봄。행별 FTS 재인덱싱(MemoryDb→L1Store→backend→InProc/Worker→worker 분派 관통)추가, 새 키워드가 즉시 검색축에。테스트에역方向 대조(순수 patchMetadata 불命中 / AndReindex 命中) 포함。
|
|
18
|
+
|
|
19
|
+
### 변경
|
|
20
|
+
|
|
21
|
+
- **입력영 팝업에서 본 대화召回 목록 렌더를中止**: WingWheel 의 `SessionRecalledArea` 제거——召回 表示은 brief-sidebar 가接管,重复 제거(컴포넌트 파일은 롤백용 유지)。
|
|
22
|
+
|
|
12
23
|
## [0.24.2] — 2026-10-14
|
|
13
24
|
|
|
14
25
|
### 신규
|
package/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
- **建议状态(`longtask-hint-get`)以纯端点提供**:判定 = 上下文占用达 `contextThresholdPct` 或 todo 漂移达 `driftThreshold`(部署配置可调);配合 `longtask-compress-tail` 作手动压缩入口。
|
|
18
18
|
- **撤下浮动球 UI(`client/src/longtask-fab.ts`;beta.1 起曾随包发布)**:悬浮建议面板的归属本在 context-compression 侧(「建议启动压缩」是其领域),pm 属误载——2026-10-02 用户裁定撤下。撤下不违座位契约(settings.section 恰一个 + 输入栏 pill,body 单例不占座位),但全应用级 overlay 会与其他插件的悬浮 UI 抢占屏幕角落;长任务开关与手动压缩入口待长任务定版时落记忆设置分节。
|
|
19
19
|
- **`session-mode-set` 的 `mode` 转为可选**:与 `longTask` 二选一必传——纯切长任务开关不再被迫发送档位(旧客户端恒传 mode,行为不变)。响应 `mode` 改回当前生效值。
|
|
20
|
+
## [0.25.0] — 2026-10-14
|
|
21
|
+
|
|
22
|
+
### 新增
|
|
23
|
+
|
|
24
|
+
- **关键词蒸馏回填(`metadata.dsh_keywords` 产出方)**:此前 `l1_fts` 预留的 `keywords` 检索轴因无产出方而休眠。新增独立回填批扫 `src/keyword-backfill.ts`(镜像 wing 回填:单飞/后台/有界/失败原记录不动),对缺 `dsh_keywords` 的存量 L1 逐块送 LLM 提炼关键词与使用场景,经 `normKeywords`(≤5、去重、长度钳)归一后写回。端点 `dsh-memory/keyword-backfill`(立即返回 `{started,running,enabled,updated,failed}`),面板(WingWheel)加「关键词回填」按钮。配置 `keyword.enabled` **默认关**——关时不回填、`l1_fts` 逐字不变(零漂移)。
|
|
25
|
+
- **`patchMetadataAndReindex`(关键词轴真能生效的关键)**:`patchMetadata` 只改 `l1_records`、不碰 `l1_fts`,回填若走它则写了关键词但 BM25 永远看不到。新增按行重索引(贯穿 MemoryDb→L1Store→backend 接口→InProc/Worker→worker 分派),回填写完元数据顺带重跑该行 fts 插入,新关键词立即进检索轴。测试含反向对照(纯 patchMetadata 不命中 / AndReindex 命中)。
|
|
26
|
+
|
|
27
|
+
### 变更
|
|
28
|
+
|
|
29
|
+
- **输入区弹窗不再渲染本会话召回列表**:WingWheel 移除 `SessionRecalledArea`——召回展示已由 brief-sidebar 接管,输入区弹窗再渲染一份属重复(组件文件保留以备回滚)。
|
|
30
|
+
|
|
20
31
|
## [0.24.2] — 2026-10-14
|
|
21
32
|
|
|
22
33
|
### 新增
|
package/dist/client.js
CHANGED
|
@@ -2078,7 +2078,7 @@ var __defProp = Object.defineProperty;
|
|
|
2078
2078
|
const showInput = scope === "all" || scope === "input";
|
|
2079
2079
|
const effNote = layers.map((l) => eff[l[0]] || "?").join(" / ");
|
|
2080
2080
|
const rowLabel = (key) => key === "extract" ? "抽取输出" : key === "dedup" ? "去重输出" : key === "l2" ? "L2 输出" : "L3 输出";
|
|
2081
|
-
const
|
|
2081
|
+
const rowStyle = { display: "flex", alignItems: "center", gap: 8 };
|
|
2082
2082
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
|
|
2083
2083
|
showOutputs ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: { marginTop: 12 }, children: [
|
|
2084
2084
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
@@ -2089,7 +2089,7 @@ var __defProp = Object.defineProperty;
|
|
|
2089
2089
|
children: "输出预算"
|
|
2090
2090
|
}
|
|
2091
2091
|
),
|
|
2092
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: layers.map((l) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style:
|
|
2092
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: layers.map((l) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
|
|
2093
2093
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { width: 68, flexShrink: 0, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: rowLabel(l[0]) }),
|
|
2094
2094
|
inputBox(
|
|
2095
2095
|
l[0],
|
|
@@ -2111,7 +2111,7 @@ var __defProp = Object.defineProperty;
|
|
|
2111
2111
|
children: "输入预算"
|
|
2112
2112
|
}
|
|
2113
2113
|
),
|
|
2114
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style:
|
|
2114
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: rowStyle, children: [
|
|
2115
2115
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { width: 68, flexShrink: 0, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: "单次输入" }),
|
|
2116
2116
|
inputBox(
|
|
2117
2117
|
"input",
|
|
@@ -5869,7 +5869,7 @@ var __defProp = Object.defineProperty;
|
|
|
5869
5869
|
}
|
|
5870
5870
|
|
|
5871
5871
|
// client/src/pill/MemoryModePill.tsx
|
|
5872
|
-
var
|
|
5872
|
+
var import_react25 = require("react");
|
|
5873
5873
|
|
|
5874
5874
|
// src/util/context-occupancy.ts
|
|
5875
5875
|
var CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
|
|
@@ -6185,7 +6185,7 @@ var __defProp = Object.defineProperty;
|
|
|
6185
6185
|
}
|
|
6186
6186
|
|
|
6187
6187
|
// client/src/pill/WingWheel.tsx
|
|
6188
|
-
var
|
|
6188
|
+
var import_react24 = require("react");
|
|
6189
6189
|
|
|
6190
6190
|
// client/src/pill/SessionInfoArea.tsx
|
|
6191
6191
|
var import_react23 = require("react");
|
|
@@ -6296,101 +6296,8 @@ var __defProp = Object.defineProperty;
|
|
|
6296
6296
|
] });
|
|
6297
6297
|
}
|
|
6298
6298
|
|
|
6299
|
-
// client/src/pill/SessionRecalledArea.tsx
|
|
6300
|
-
var import_react24 = require("react");
|
|
6301
|
-
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
6302
|
-
var SHOW_CAP = 6;
|
|
6303
|
-
var POLL_MS2 = 4e3;
|
|
6304
|
-
var MATURITY_LABEL = { high: "成熟·高", mid: "成熟·中", low: "成熟·低" };
|
|
6305
|
-
function SessionRecalledArea(props) {
|
|
6306
|
-
const rpc = props.rpc;
|
|
6307
|
-
const sessionId = props.sessionId;
|
|
6308
|
-
const [records, setRecords] = (0, import_react24.useState)(void 0);
|
|
6309
|
-
(0, import_react24.useEffect)(() => {
|
|
6310
|
-
if (!rpc || !sessionId) return void 0;
|
|
6311
|
-
let alive = true;
|
|
6312
|
-
let timer = null;
|
|
6313
|
-
let seq = 0;
|
|
6314
|
-
const tick = () => {
|
|
6315
|
-
const token = ++seq;
|
|
6316
|
-
rpc("dsh-memory/session-recalled", { sessionId }).then((r) => {
|
|
6317
|
-
if (!alive || token !== seq) return;
|
|
6318
|
-
if (r && r.ok && r.value) {
|
|
6319
|
-
registerTypeCatalog(r.value.typeCatalog);
|
|
6320
|
-
setRecords(r.value.records);
|
|
6321
|
-
}
|
|
6322
|
-
}).catch(() => {
|
|
6323
|
-
}).then(() => {
|
|
6324
|
-
if (alive) timer = setTimeout(tick, POLL_MS2);
|
|
6325
|
-
});
|
|
6326
|
-
};
|
|
6327
|
-
tick();
|
|
6328
|
-
return () => {
|
|
6329
|
-
alive = false;
|
|
6330
|
-
if (timer) clearTimeout(timer);
|
|
6331
|
-
};
|
|
6332
|
-
}, [rpc, sessionId]);
|
|
6333
|
-
if (records === void 0) return null;
|
|
6334
|
-
const rows = records.filter((r) => r.superseded !== true || r.retired === true);
|
|
6335
|
-
const recentFirst = [...rows].reverse();
|
|
6336
|
-
const shown = recentFirst.slice(0, SHOW_CAP);
|
|
6337
|
-
const hidden = recentFirst.length - shown.length;
|
|
6338
|
-
const allSuperseded = rows.length === 0 && records.length > 0;
|
|
6339
|
-
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { className: "dsh-mem-sinfo", children: [
|
|
6340
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6341
|
-
"div",
|
|
6342
|
-
{
|
|
6343
|
-
style: { fontSize: 11, color: "var(--dsh-mem-text-3)", marginBottom: 6 },
|
|
6344
|
-
title: "本会话召回、且正文仍在上下文里的记忆(正文被摘要顶掉的已折叠不显示;清空会话才清除;不是常驻注入)",
|
|
6345
|
-
children: "本会话召回 " + rows.length + " 条"
|
|
6346
|
-
}
|
|
6347
|
-
),
|
|
6348
|
-
allSuperseded ? (
|
|
6349
|
-
// 不是“还没召回到”——那批确实注入过,只是正文已被摘要顶掉。说谎的空态比不显示更糟。
|
|
6350
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)", lineHeight: "16px" }, children: "本会话注入过的 " + records.length + " 条记忆正文已被摘要取代" })
|
|
6351
|
-
) : records.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)", lineHeight: "16px" }, children: "还没召回到记忆(聊几轮后按相关性自动浮现)" }) : /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
6352
|
-
shown.map((r) => /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: rowStyle, title: rowTitle(r), children: [
|
|
6353
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6354
|
-
"span",
|
|
6355
|
-
{
|
|
6356
|
-
className: tagClass(r.type),
|
|
6357
|
-
style: { flexShrink: 0, whiteSpace: "nowrap" },
|
|
6358
|
-
children: typeLabel(r.type)
|
|
6359
|
-
}
|
|
6360
|
-
),
|
|
6361
|
-
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "dsh-mem-sinfo-val", style: r.retired === true ? STALE_TEXT_STYLE : textStyle, children: r.content }),
|
|
6362
|
-
r.retired === true ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 10, color: "var(--dsh-mem-text-3)", whiteSpace: "nowrap", flexShrink: 0 }, children: "已判过时" }) : null,
|
|
6363
|
-
r.maturity ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 10, color: "var(--dsh-mem-text-3)", whiteSpace: "nowrap", flexShrink: 0 }, children: MATURITY_LABEL[r.maturity] }) : null
|
|
6364
|
-
] }, r.id)),
|
|
6365
|
-
hidden > 0 ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "+ " + hidden + " 条更早召回(详见面板「记忆」页)" }) : null
|
|
6366
|
-
] })
|
|
6367
|
-
] });
|
|
6368
|
-
}
|
|
6369
|
-
var rowStyle = {
|
|
6370
|
-
display: "flex",
|
|
6371
|
-
alignItems: "flex-start",
|
|
6372
|
-
gap: 6,
|
|
6373
|
-
lineHeight: "16px"
|
|
6374
|
-
};
|
|
6375
|
-
var textStyle = {
|
|
6376
|
-
fontSize: 12,
|
|
6377
|
-
lineHeight: "16px",
|
|
6378
|
-
fontWeight: 400,
|
|
6379
|
-
minWidth: 0,
|
|
6380
|
-
display: "-webkit-box",
|
|
6381
|
-
WebkitLineClamp: 2,
|
|
6382
|
-
WebkitBoxOrient: "vertical",
|
|
6383
|
-
overflow: "hidden"
|
|
6384
|
-
};
|
|
6385
|
-
var STALE_TEXT_STYLE = { ...textStyle, textDecoration: "line-through" };
|
|
6386
|
-
function rowTitle(r) {
|
|
6387
|
-
const head = "[" + typeLabel(r.type) + "] " + r.content;
|
|
6388
|
-
const sc = r.usageScenario && r.usageScenario.length > 0 ? "\n使用场景:" + r.usageScenario.join(";") : "";
|
|
6389
|
-
return head + sc;
|
|
6390
|
-
}
|
|
6391
|
-
|
|
6392
6299
|
// client/src/pill/WingWheel.tsx
|
|
6393
|
-
var
|
|
6300
|
+
var import_jsx_runtime24 = require("react/jsx-runtime");
|
|
6394
6301
|
var SIZE = 196;
|
|
6395
6302
|
var CENTER = SIZE / 2;
|
|
6396
6303
|
var R = 64;
|
|
@@ -6433,17 +6340,19 @@ var __defProp = Object.defineProperty;
|
|
|
6433
6340
|
function WingWheel(props) {
|
|
6434
6341
|
ensureThemeStyle();
|
|
6435
6342
|
const isOff = props.mode === "off";
|
|
6436
|
-
const [overview, setOverview] = (0,
|
|
6437
|
-
const [backfillBusy, setBackfillBusy] = (0,
|
|
6438
|
-
const [
|
|
6439
|
-
const
|
|
6440
|
-
(0,
|
|
6343
|
+
const [overview, setOverview] = (0, import_react24.useState)(null);
|
|
6344
|
+
const [backfillBusy, setBackfillBusy] = (0, import_react24.useState)(false);
|
|
6345
|
+
const [kwBusy, setKwBusy] = (0, import_react24.useState)(false);
|
|
6346
|
+
const [kwSummary, setKwSummary] = (0, import_react24.useState)(null);
|
|
6347
|
+
const [localError, setLocalError] = (0, import_react24.useState)(null);
|
|
6348
|
+
const timersRef = (0, import_react24.useRef)([]);
|
|
6349
|
+
(0, import_react24.useEffect)(
|
|
6441
6350
|
() => () => {
|
|
6442
6351
|
for (const t of timersRef.current) window.clearTimeout(t);
|
|
6443
6352
|
},
|
|
6444
6353
|
[]
|
|
6445
6354
|
);
|
|
6446
|
-
(0,
|
|
6355
|
+
(0, import_react24.useEffect)(() => {
|
|
6447
6356
|
let alive = true;
|
|
6448
6357
|
props.rpc("dsh-memory/wing-overview", {}).then((r) => {
|
|
6449
6358
|
if (!alive) return;
|
|
@@ -6489,28 +6398,80 @@ var __defProp = Object.defineProperty;
|
|
|
6489
6398
|
finish();
|
|
6490
6399
|
});
|
|
6491
6400
|
};
|
|
6492
|
-
const
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6401
|
+
const keywordBackfill = () => {
|
|
6402
|
+
if (kwBusy) return;
|
|
6403
|
+
setKwBusy(true);
|
|
6404
|
+
setKwSummary(null);
|
|
6405
|
+
setLocalError(null);
|
|
6406
|
+
let polls = 0;
|
|
6407
|
+
const finish = () => setKwBusy(false);
|
|
6408
|
+
const showResult = (v) => {
|
|
6409
|
+
const fail = v.failed ?? 0;
|
|
6410
|
+
setKwSummary(`关键词回填完成:成功 ${v.updated ?? 0} 条${fail > 0 ? `,失败 ${fail} 条` : ""}`);
|
|
6411
|
+
};
|
|
6412
|
+
const failMsg = (m) => {
|
|
6413
|
+
setLocalError(m);
|
|
6414
|
+
finish();
|
|
6415
|
+
};
|
|
6416
|
+
const tick = () => {
|
|
6417
|
+
polls++;
|
|
6418
|
+
props.rpc("dsh-memory/keyword-backfill", {}).then((r) => {
|
|
6419
|
+
const v = r && r.ok ? r.value : null;
|
|
6420
|
+
if (!v) {
|
|
6421
|
+
failMsg("关键词回填失败");
|
|
6422
|
+
return;
|
|
6423
|
+
}
|
|
6424
|
+
if (!v.enabled) {
|
|
6425
|
+
failMsg("关键词回填未开启:请先在设置里打开 keyword.enabled");
|
|
6426
|
+
return;
|
|
6427
|
+
}
|
|
6428
|
+
if (!v.running) {
|
|
6429
|
+
showResult(v);
|
|
6430
|
+
finish();
|
|
6431
|
+
return;
|
|
6432
|
+
}
|
|
6433
|
+
if (polls >= 20) {
|
|
6434
|
+
showResult(v);
|
|
6435
|
+
finish();
|
|
6436
|
+
return;
|
|
6437
|
+
}
|
|
6438
|
+
timersRef.current.push(window.setTimeout(tick, 3e3));
|
|
6439
|
+
}).catch(() => failMsg("关键词回填失败"));
|
|
6440
|
+
};
|
|
6441
|
+
props.rpc("dsh-memory/keyword-backfill", {}).then((r) => {
|
|
6442
|
+
if (!r || !r.ok) {
|
|
6443
|
+
failMsg(r && r.error ? "关键词回填失败:" + r.error.message : "关键词回填失败");
|
|
6444
|
+
return;
|
|
6445
|
+
}
|
|
6446
|
+
if (!r.value || !r.value.enabled) {
|
|
6447
|
+
failMsg("关键词回填未开启:请先在设置里打开 keyword.enabled");
|
|
6448
|
+
return;
|
|
6449
|
+
}
|
|
6450
|
+
timersRef.current.push(window.setTimeout(tick, 3e3));
|
|
6451
|
+
}).catch((e) => failMsg("关键词回填失败:" + String(e && e.message || e)));
|
|
6452
|
+
};
|
|
6453
|
+
const boxRef = (0, import_react24.useRef)(null);
|
|
6454
|
+
const startRef = (0, import_react24.useRef)(null);
|
|
6455
|
+
const movedRef = (0, import_react24.useRef)(false);
|
|
6456
|
+
const downIndexRef = (0, import_react24.useRef)(null);
|
|
6457
|
+
const dragTargetRef = (0, import_react24.useRef)(null);
|
|
6458
|
+
const [drag, setDrag] = (0, import_react24.useState)(null);
|
|
6498
6459
|
const lockedIndex = (() => {
|
|
6499
6460
|
if (props.halls.length !== 1 || !overview) return null;
|
|
6500
6461
|
const i = overview.corners.findIndex((c) => c.id === props.halls[0]);
|
|
6501
6462
|
return i >= 0 ? i : null;
|
|
6502
6463
|
})();
|
|
6503
6464
|
const activeCorner = drag ? drag.index : lockedIndex;
|
|
6504
|
-
const blockPosRef = (0,
|
|
6465
|
+
const blockPosRef = (0, import_react24.useRef)(
|
|
6505
6466
|
activeCorner != null ? cornerXY(activeCorner) : { x: CENTER, y: CENTER }
|
|
6506
6467
|
);
|
|
6507
|
-
const blockTargetRef = (0,
|
|
6508
|
-
const [blockPos, setBlockPos] = (0,
|
|
6509
|
-
(0,
|
|
6468
|
+
const blockTargetRef = (0, import_react24.useRef)(blockPosRef.current);
|
|
6469
|
+
const [blockPos, setBlockPos] = (0, import_react24.useState)(blockPosRef.current);
|
|
6470
|
+
(0, import_react24.useEffect)(() => {
|
|
6510
6471
|
blockTargetRef.current = activeCorner != null ? cornerXY(activeCorner) : { x: CENTER, y: CENTER };
|
|
6511
6472
|
}, [activeCorner]);
|
|
6512
|
-
const reducedMotionRef = (0,
|
|
6513
|
-
(0,
|
|
6473
|
+
const reducedMotionRef = (0, import_react24.useRef)(false);
|
|
6474
|
+
(0, import_react24.useEffect)(() => {
|
|
6514
6475
|
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
|
|
6515
6476
|
const apply2 = () => {
|
|
6516
6477
|
reducedMotionRef.current = mq.matches;
|
|
@@ -6519,7 +6480,7 @@ var __defProp = Object.defineProperty;
|
|
|
6519
6480
|
mq.addEventListener("change", apply2);
|
|
6520
6481
|
return () => mq.removeEventListener("change", apply2);
|
|
6521
6482
|
}, []);
|
|
6522
|
-
(0,
|
|
6483
|
+
(0, import_react24.useEffect)(() => {
|
|
6523
6484
|
let raf = 0;
|
|
6524
6485
|
const loop = () => {
|
|
6525
6486
|
const cur = blockPosRef.current;
|
|
@@ -6608,8 +6569,8 @@ var __defProp = Object.defineProperty;
|
|
|
6608
6569
|
}).join(" ");
|
|
6609
6570
|
const grayStyle = isOff ? { opacity: 0.45, pointerEvents: "none" } : {};
|
|
6610
6571
|
const boundariesDisabled = props.halls.length === 0 || isOff;
|
|
6611
|
-
return /* @__PURE__ */ (0,
|
|
6612
|
-
/* @__PURE__ */ (0,
|
|
6572
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { width: SIZE }, children: [
|
|
6573
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6613
6574
|
"div",
|
|
6614
6575
|
{
|
|
6615
6576
|
ref: boxRef,
|
|
@@ -6619,7 +6580,7 @@ var __defProp = Object.defineProperty;
|
|
|
6619
6580
|
onPointerUp: onBoxUp,
|
|
6620
6581
|
onPointerCancel: onBoxUp,
|
|
6621
6582
|
children: [
|
|
6622
|
-
/* @__PURE__ */ (0,
|
|
6583
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6623
6584
|
"svg",
|
|
6624
6585
|
{
|
|
6625
6586
|
width: SIZE,
|
|
@@ -6627,7 +6588,7 @@ var __defProp = Object.defineProperty;
|
|
|
6627
6588
|
style: { position: "absolute", inset: 0, pointerEvents: "none" },
|
|
6628
6589
|
"aria-hidden": "true",
|
|
6629
6590
|
children: [
|
|
6630
|
-
/* @__PURE__ */ (0,
|
|
6591
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6631
6592
|
"polygon",
|
|
6632
6593
|
{
|
|
6633
6594
|
points: polyPoints,
|
|
@@ -6638,7 +6599,7 @@ var __defProp = Object.defineProperty;
|
|
|
6638
6599
|
opacity: 0.45
|
|
6639
6600
|
}
|
|
6640
6601
|
),
|
|
6641
|
-
/* @__PURE__ */ (0,
|
|
6602
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6642
6603
|
"polygon",
|
|
6643
6604
|
{
|
|
6644
6605
|
points: polyPoints,
|
|
@@ -6649,7 +6610,7 @@ var __defProp = Object.defineProperty;
|
|
|
6649
6610
|
),
|
|
6650
6611
|
[0, 1, 2, 3, 4, 5, 6, 7].map((i) => {
|
|
6651
6612
|
const p = cornerPos(i);
|
|
6652
|
-
return /* @__PURE__ */ (0,
|
|
6613
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6653
6614
|
"line",
|
|
6654
6615
|
{
|
|
6655
6616
|
x1: CENTER,
|
|
@@ -6665,7 +6626,7 @@ var __defProp = Object.defineProperty;
|
|
|
6665
6626
|
]
|
|
6666
6627
|
}
|
|
6667
6628
|
),
|
|
6668
|
-
/* @__PURE__ */ (0,
|
|
6629
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6669
6630
|
"div",
|
|
6670
6631
|
{
|
|
6671
6632
|
"aria-hidden": "true",
|
|
@@ -6684,7 +6645,7 @@ var __defProp = Object.defineProperty;
|
|
|
6684
6645
|
}
|
|
6685
6646
|
}
|
|
6686
6647
|
),
|
|
6687
|
-
/* @__PURE__ */ (0,
|
|
6648
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6688
6649
|
"button",
|
|
6689
6650
|
{
|
|
6690
6651
|
type: "button",
|
|
@@ -6718,7 +6679,7 @@ var __defProp = Object.defineProperty;
|
|
|
6718
6679
|
const dim = activeCorner != null && !active;
|
|
6719
6680
|
const empty = count === 0;
|
|
6720
6681
|
const p = cornerPos(i);
|
|
6721
|
-
return /* @__PURE__ */ (0,
|
|
6682
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6722
6683
|
"div",
|
|
6723
6684
|
{
|
|
6724
6685
|
title: id ? `「${label}」${count ?? 0} 条
|
|
@@ -6747,8 +6708,8 @@ var __defProp = Object.defineProperty;
|
|
|
6747
6708
|
zIndex: 2
|
|
6748
6709
|
},
|
|
6749
6710
|
children: [
|
|
6750
|
-
/* @__PURE__ */ (0,
|
|
6751
|
-
/* @__PURE__ */ (0,
|
|
6711
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { children: label }),
|
|
6712
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 8, opacity: 0.75, fontVariantNumeric: "tabular-nums" }, children: count === null ? " " : empty ? "空角" : `${count} 条` })
|
|
6752
6713
|
]
|
|
6753
6714
|
},
|
|
6754
6715
|
id ?? i
|
|
@@ -6757,7 +6718,7 @@ var __defProp = Object.defineProperty;
|
|
|
6757
6718
|
]
|
|
6758
6719
|
}
|
|
6759
6720
|
),
|
|
6760
|
-
/* @__PURE__ */ (0,
|
|
6721
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6761
6722
|
"div",
|
|
6762
6723
|
{
|
|
6763
6724
|
title: props.halls.length === 0 ? "选定某个角(单 Wing)后,这两项才生效" : "锁定 Wing 时两类无角记忆是否参与召回",
|
|
@@ -6771,9 +6732,9 @@ var __defProp = Object.defineProperty;
|
|
|
6771
6732
|
pointerEvents: boundariesDisabled ? "none" : void 0
|
|
6772
6733
|
},
|
|
6773
6734
|
children: [
|
|
6774
|
-
/* @__PURE__ */ (0,
|
|
6775
|
-
/* @__PURE__ */ (0,
|
|
6776
|
-
/* @__PURE__ */ (0,
|
|
6735
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: overview ? `另有 ${overview.unlabeled} 条未打标` : "未打标" }),
|
|
6736
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 6 }, children: [
|
|
6737
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6777
6738
|
Segmented,
|
|
6778
6739
|
{
|
|
6779
6740
|
value: props.hallIncludeUnlabeled ? "in" : "ex",
|
|
@@ -6784,7 +6745,7 @@ var __defProp = Object.defineProperty;
|
|
|
6784
6745
|
onChange: (key) => props.onCommitWingBoundaries({ includeUnlabeled: key === "in" })
|
|
6785
6746
|
}
|
|
6786
6747
|
),
|
|
6787
|
-
/* @__PURE__ */ (0,
|
|
6748
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6788
6749
|
Segmented,
|
|
6789
6750
|
{
|
|
6790
6751
|
value: props.hallIncludeGeneral ? "in" : "ex",
|
|
@@ -6799,7 +6760,7 @@ var __defProp = Object.defineProperty;
|
|
|
6799
6760
|
]
|
|
6800
6761
|
}
|
|
6801
6762
|
),
|
|
6802
|
-
/* @__PURE__ */ (0,
|
|
6763
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Row, { label: "会话", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6803
6764
|
Segmented,
|
|
6804
6765
|
{
|
|
6805
6766
|
value: isOff ? "off" : "on",
|
|
@@ -6814,7 +6775,7 @@ var __defProp = Object.defineProperty;
|
|
|
6814
6775
|
onChange: (key) => props.onCommit(key === "off" ? "off" : "auto")
|
|
6815
6776
|
}
|
|
6816
6777
|
) }),
|
|
6817
|
-
props.recall !== void 0 && props.onCommitRecall ? /* @__PURE__ */ (0,
|
|
6778
|
+
props.recall !== void 0 && props.onCommitRecall ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(Row, { label: "注入", children: /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6818
6779
|
Segmented,
|
|
6819
6780
|
{
|
|
6820
6781
|
value: props.recall === null ? "follow" : props.recall ? "on" : "off",
|
|
@@ -6827,7 +6788,7 @@ var __defProp = Object.defineProperty;
|
|
|
6827
6788
|
onChange: (key) => props.onCommitRecall(key === "on" ? true : key === "off" ? false : null)
|
|
6828
6789
|
}
|
|
6829
6790
|
) }) : null,
|
|
6830
|
-
/* @__PURE__ */ (0,
|
|
6791
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6831
6792
|
"div",
|
|
6832
6793
|
{
|
|
6833
6794
|
style: {
|
|
@@ -6838,8 +6799,8 @@ var __defProp = Object.defineProperty;
|
|
|
6838
6799
|
marginTop: 10
|
|
6839
6800
|
},
|
|
6840
6801
|
children: [
|
|
6841
|
-
/* @__PURE__ */ (0,
|
|
6842
|
-
/* @__PURE__ */ (0,
|
|
6802
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: overview ? `未打标 ${overview.unlabeled} 条` : "未打标计数加载中" }),
|
|
6803
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6843
6804
|
ActionButton,
|
|
6844
6805
|
{
|
|
6845
6806
|
label: backfillBusy ? "回填中…" : "回填",
|
|
@@ -6851,13 +6812,36 @@ var __defProp = Object.defineProperty;
|
|
|
6851
6812
|
]
|
|
6852
6813
|
}
|
|
6853
6814
|
),
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6815
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6816
|
+
"div",
|
|
6817
|
+
{
|
|
6818
|
+
style: {
|
|
6819
|
+
display: "flex",
|
|
6820
|
+
justifyContent: "space-between",
|
|
6821
|
+
alignItems: "center",
|
|
6822
|
+
gap: 8,
|
|
6823
|
+
marginTop: 10
|
|
6824
|
+
},
|
|
6825
|
+
children: [
|
|
6826
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: kwSummary || "为存量记忆补检索关键词" }),
|
|
6827
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
|
|
6828
|
+
ActionButton,
|
|
6829
|
+
{
|
|
6830
|
+
label: kwBusy ? "回填中…" : "关键词回填",
|
|
6831
|
+
title: "对缺 dsh_keywords 的存量记忆批量补关键词,喂 FTS 关键词检索轴(需先开 keyword.enabled)",
|
|
6832
|
+
disabled: kwBusy,
|
|
6833
|
+
onClick: () => keywordBackfill()
|
|
6834
|
+
}
|
|
6835
|
+
)
|
|
6836
|
+
]
|
|
6837
|
+
}
|
|
6838
|
+
),
|
|
6839
|
+
props.error || localError ? /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 8 }, children: props.error || localError }) : null,
|
|
6840
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)(SessionInfoArea, { rpc: props.rpc, sessionId: props.sessionId })
|
|
6857
6841
|
] });
|
|
6858
6842
|
}
|
|
6859
6843
|
function Row(props) {
|
|
6860
|
-
return /* @__PURE__ */ (0,
|
|
6844
|
+
return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
|
|
6861
6845
|
"div",
|
|
6862
6846
|
{
|
|
6863
6847
|
style: {
|
|
@@ -6867,7 +6851,7 @@ var __defProp = Object.defineProperty;
|
|
|
6867
6851
|
marginTop: 10
|
|
6868
6852
|
},
|
|
6869
6853
|
children: [
|
|
6870
|
-
/* @__PURE__ */ (0,
|
|
6854
|
+
/* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: props.label }),
|
|
6871
6855
|
props.children
|
|
6872
6856
|
]
|
|
6873
6857
|
}
|
|
@@ -6875,9 +6859,9 @@ var __defProp = Object.defineProperty;
|
|
|
6875
6859
|
}
|
|
6876
6860
|
var LABEL_FALLBACK = ["工作", "人际", "学习", "创作娱乐", "居家", "健康", "财务", "出行"];
|
|
6877
6861
|
function useViewportClamp(popRef) {
|
|
6878
|
-
const shiftRef = (0,
|
|
6879
|
-
const [shiftX, setShiftX] = (0,
|
|
6880
|
-
(0,
|
|
6862
|
+
const shiftRef = (0, import_react24.useRef)(0);
|
|
6863
|
+
const [shiftX, setShiftX] = (0, import_react24.useState)(0);
|
|
6864
|
+
(0, import_react24.useEffect)(() => {
|
|
6881
6865
|
const clamp = () => {
|
|
6882
6866
|
const el = popRef.current;
|
|
6883
6867
|
if (!el) return;
|
|
@@ -6907,24 +6891,24 @@ var __defProp = Object.defineProperty;
|
|
|
6907
6891
|
}
|
|
6908
6892
|
|
|
6909
6893
|
// client/src/pill/MemoryModePill.tsx
|
|
6910
|
-
var
|
|
6894
|
+
var import_jsx_runtime25 = require("react/jsx-runtime");
|
|
6911
6895
|
function MemoryModePill(props) {
|
|
6912
6896
|
const rpc = props.rpc;
|
|
6913
6897
|
const sessionId = props.sessionId || props.session && props.session.sessionId;
|
|
6914
|
-
const [mode, setMode] = (0,
|
|
6915
|
-
const [recall, setRecall] = (0,
|
|
6916
|
-
const [recallResolved, setRecallResolved] = (0,
|
|
6917
|
-
const [halls, setWings] = (0,
|
|
6918
|
-
const [hallIncludeUnlabeled, setWingIncludeUnlabeled] = (0,
|
|
6919
|
-
const [hallIncludeGeneral, setWingIncludeGeneral] = (0,
|
|
6920
|
-
const [wingLabels, setHallLabels] = (0,
|
|
6921
|
-
const [error, setError] = (0,
|
|
6922
|
-
const [open, setOpen] = (0,
|
|
6923
|
-
const wrapRef = (0,
|
|
6924
|
-
const popRef = (0,
|
|
6898
|
+
const [mode, setMode] = (0, import_react25.useState)(null);
|
|
6899
|
+
const [recall, setRecall] = (0, import_react25.useState)(null);
|
|
6900
|
+
const [recallResolved, setRecallResolved] = (0, import_react25.useState)(true);
|
|
6901
|
+
const [halls, setWings] = (0, import_react25.useState)([]);
|
|
6902
|
+
const [hallIncludeUnlabeled, setWingIncludeUnlabeled] = (0, import_react25.useState)(true);
|
|
6903
|
+
const [hallIncludeGeneral, setWingIncludeGeneral] = (0, import_react25.useState)(false);
|
|
6904
|
+
const [wingLabels, setHallLabels] = (0, import_react25.useState)({});
|
|
6905
|
+
const [error, setError] = (0, import_react25.useState)(null);
|
|
6906
|
+
const [open, setOpen] = (0, import_react25.useState)(false);
|
|
6907
|
+
const wrapRef = (0, import_react25.useRef)(null);
|
|
6908
|
+
const popRef = (0, import_react25.useRef)(null);
|
|
6925
6909
|
const shiftX = useViewportClamp(popRef);
|
|
6926
|
-
const seqRef = (0,
|
|
6927
|
-
const load = (0,
|
|
6910
|
+
const seqRef = (0, import_react25.useRef)(0);
|
|
6911
|
+
const load = (0, import_react25.useCallback)(() => {
|
|
6928
6912
|
if (!sessionId || !rpc) return;
|
|
6929
6913
|
const token = ++seqRef.current;
|
|
6930
6914
|
setError(null);
|
|
@@ -6949,13 +6933,13 @@ var __defProp = Object.defineProperty;
|
|
|
6949
6933
|
}).catch(() => {
|
|
6950
6934
|
});
|
|
6951
6935
|
}, [sessionId, rpc]);
|
|
6952
|
-
(0,
|
|
6936
|
+
(0, import_react25.useEffect)(() => {
|
|
6953
6937
|
load();
|
|
6954
6938
|
}, [load]);
|
|
6955
|
-
(0,
|
|
6939
|
+
(0, import_react25.useEffect)(() => {
|
|
6956
6940
|
watchSidebarIcon();
|
|
6957
6941
|
}, []);
|
|
6958
|
-
(0,
|
|
6942
|
+
(0, import_react25.useEffect)(() => {
|
|
6959
6943
|
initOccupancyIndicator(
|
|
6960
6944
|
(endpoint, payload) => rpc(endpoint, payload)
|
|
6961
6945
|
);
|
|
@@ -6963,7 +6947,7 @@ var __defProp = Object.defineProperty;
|
|
|
6963
6947
|
watchContextMeter();
|
|
6964
6948
|
noteOccupancySession(sessionId ?? null);
|
|
6965
6949
|
}, [sessionId, rpc]);
|
|
6966
|
-
(0,
|
|
6950
|
+
(0, import_react25.useEffect)(() => {
|
|
6967
6951
|
if (!open) return;
|
|
6968
6952
|
const onDown = (e) => {
|
|
6969
6953
|
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
|
|
@@ -7103,8 +7087,8 @@ var __defProp = Object.defineProperty;
|
|
|
7103
7087
|
pillStyle.boxShadow = "0 0 12px color-mix(in srgb, " + info.color + " 30%, transparent)";
|
|
7104
7088
|
pillStyle["--dsh-mem-pill-tint"] = info.color;
|
|
7105
7089
|
}
|
|
7106
|
-
return /* @__PURE__ */ (0,
|
|
7107
|
-
/* @__PURE__ */ (0,
|
|
7090
|
+
return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { ref: wrapRef, style: { position: "relative", display: "inline-flex" }, children: [
|
|
7091
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
|
|
7108
7092
|
"button",
|
|
7109
7093
|
{
|
|
7110
7094
|
type: "button",
|
|
@@ -7117,11 +7101,11 @@ var __defProp = Object.defineProperty;
|
|
|
7117
7101
|
style: pillStyle,
|
|
7118
7102
|
children: [
|
|
7119
7103
|
"记忆 · ",
|
|
7120
|
-
/* @__PURE__ */ (0,
|
|
7104
|
+
/* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { children: faceLabel })
|
|
7121
7105
|
]
|
|
7122
7106
|
}
|
|
7123
7107
|
),
|
|
7124
|
-
open ? /* @__PURE__ */ (0,
|
|
7108
|
+
open ? /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
7125
7109
|
"div",
|
|
7126
7110
|
{
|
|
7127
7111
|
ref: popRef,
|
|
@@ -7132,12 +7116,12 @@ var __defProp = Object.defineProperty;
|
|
|
7132
7116
|
transform: "translateX(calc(-50% + " + shiftX + "px))",
|
|
7133
7117
|
zIndex: 1e3
|
|
7134
7118
|
},
|
|
7135
|
-
children: /* @__PURE__ */ (0,
|
|
7119
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
7136
7120
|
"div",
|
|
7137
7121
|
{
|
|
7138
7122
|
className: "dsh-mem-popover",
|
|
7139
7123
|
style: { position: "relative", padding: "10px 12px" },
|
|
7140
|
-
children: /* @__PURE__ */ (0,
|
|
7124
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
|
|
7141
7125
|
WingWheel,
|
|
7142
7126
|
{
|
|
7143
7127
|
mode: mode || "auto",
|
package/dist/config.d.ts
CHANGED
|
@@ -193,6 +193,18 @@ export interface MemoryConfig {
|
|
|
193
193
|
usageScenario: {
|
|
194
194
|
enabled: boolean;
|
|
195
195
|
};
|
|
196
|
+
/**
|
|
197
|
+
* 关键词蒸馏回填(keyword 检索轴的产出方):**独立回填批扫**,为缺 `metadata.dsh_keywords` 的
|
|
198
|
+
* L1 记录批量补关键词,喂 `l1_fts` 的关键词轴(`ftsKeywordText` 读该键)。由
|
|
199
|
+
* `dsh-memory/keyword-backfill` 端点触发,镜像 wing 回填(单飞/后台/有界)。
|
|
200
|
+
*
|
|
201
|
+
* **默认关**:关时不回填、不产该键、`l1_fts.keywords` 列逐字不变(零漂移)。护栏复用 `backfill`。
|
|
202
|
+
* 注:写回走 `patchMetadata` 只改主表,回填会**额外重索引该记录的 fts 行**(见 keyword-backfill),
|
|
203
|
+
* 否则关键词轴对存量记录不生效。
|
|
204
|
+
*/
|
|
205
|
+
keyword: {
|
|
206
|
+
enabled: boolean;
|
|
207
|
+
};
|
|
196
208
|
/**
|
|
197
209
|
* 类型叙述模板(G2):抽取时按 7 类型各自的“前因后果”骨架写 content。
|
|
198
210
|
*
|
package/dist/config.js
CHANGED
|
@@ -226,6 +226,10 @@ export const memorySchema = Schema.object({
|
|
|
226
226
|
usageScenario: Schema.object({
|
|
227
227
|
enabled: Schema.boolean().default(false),
|
|
228
228
|
}),
|
|
229
|
+
// 关键词蒸馏回填:默认关(关时不回填、不产 dsh_keywords、FTS 逐字不变);护栏复用 backfill。
|
|
230
|
+
keyword: Schema.object({
|
|
231
|
+
enabled: Schema.boolean().default(false),
|
|
232
|
+
}),
|
|
229
233
|
// 类型叙述模板(G2):默认**开**;开时抽取 prompt 追加分类型 content 骨架(关时仍逐字不变)。
|
|
230
234
|
// 默认值从关改开的理由写在上方接口注释:只给定义实测骨架齐备 0/23,
|
|
231
235
|
// 而"没前因后果就不算可用"是用户定的判据,所以默认值要按可用性定而不是按保守定。
|
package/dist/contract.d.ts
CHANGED
|
@@ -647,6 +647,18 @@ export interface WingBackfillResponse {
|
|
|
647
647
|
started: boolean;
|
|
648
648
|
running: boolean;
|
|
649
649
|
}
|
|
650
|
+
/** dsh-memory/keyword-backfill(关键词蒸馏回填,后台任务;门控关时 enabled=false 不启动)。 */
|
|
651
|
+
export interface KeywordBackfillResponse {
|
|
652
|
+
/** true = 本次触发启动了新任务;false = 已有任务在跑或门控关。 */
|
|
653
|
+
started: boolean;
|
|
654
|
+
running: boolean;
|
|
655
|
+
/** 门控 `cfg.keyword.enabled`;关时不启动,面板据此提示先开开关。 */
|
|
656
|
+
enabled: boolean;
|
|
657
|
+
/** 本进程累计成功回填条数(重启归零;观测用)。 */
|
|
658
|
+
updated: number;
|
|
659
|
+
/** 失败/跳过数。 */
|
|
660
|
+
failed: number;
|
|
661
|
+
}
|
|
650
662
|
/** dsh-memory/session-stats(悬浮卡信息区;热路径端点)。 */
|
|
651
663
|
export interface SessionStatsRequest {
|
|
652
664
|
sessionId: string;
|
|
@@ -1650,6 +1662,7 @@ export interface DshMemoryRequestMap {
|
|
|
1650
1662
|
'dsh-memory/room-register': RoomRegisterRequest;
|
|
1651
1663
|
'dsh-memory/room-admin': RoomAdminRequest;
|
|
1652
1664
|
'dsh-memory/wing-backfill': Record<string, never>;
|
|
1665
|
+
'dsh-memory/keyword-backfill': Record<string, never>;
|
|
1653
1666
|
'dsh-memory/session-stats': SessionStatsRequest;
|
|
1654
1667
|
'dsh-memory/settings-get': Record<string, never>;
|
|
1655
1668
|
'dsh-memory/settings-set': SettingsSetRequest;
|
|
@@ -1712,6 +1725,7 @@ export interface DshMemoryResponseMap {
|
|
|
1712
1725
|
'dsh-memory/room-register': RoomRegisterResponse;
|
|
1713
1726
|
'dsh-memory/room-admin': RoomAdminResponse;
|
|
1714
1727
|
'dsh-memory/wing-backfill': WingBackfillResponse;
|
|
1728
|
+
'dsh-memory/keyword-backfill': KeywordBackfillResponse;
|
|
1715
1729
|
'dsh-memory/session-stats': SessionStatsResponse;
|
|
1716
1730
|
'dsh-memory/settings-get': SettingsGetResponse;
|
|
1717
1731
|
'dsh-memory/settings-set': SettingsSetResponse;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 关键词蒸馏回填(keyword axis 产出方)。
|
|
3
|
+
*
|
|
4
|
+
* 职责:从 L1 记录中找「缺 dsh_keywords」的存量记录,按 chunk 20 条一批送 LLM 提炼关键词
|
|
5
|
+
* 与使用场景,归一后写 `metadata.dsh_keywords` + `metadata.dsh_usage_scenario`,
|
|
6
|
+
* 并用 `patchMetadataAndReindex` 保证新关键词**立即进 FTS BM25 轴**。
|
|
7
|
+
*
|
|
8
|
+
* 安全边界(继承 wing-backfill):
|
|
9
|
+
* - 单飞:同时最多一个回填任务,重复触发直接返回 running(不排队不叠加)
|
|
10
|
+
* - 后台执行:端点立即返回,逐块推进(每块 20 条),不阻塞 RPC/管线
|
|
11
|
+
* - 失败可退:LLM 失败/输出不合法只跳过当前块,原记录零改动(标签只写成功的)
|
|
12
|
+
* - 有界:单次任务最多处理 cfg.backfill.maxRecords 条,防止一次跑飞;其余可再次触发
|
|
13
|
+
* - 门控关(`cfg.keyword.enabled = false`):run() 入口立即跳过——无回填、FTS 逐字不变
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { MemoryLogger, MemoryRecord } from './types.js';
|
|
17
|
+
import type { MemoryBackend } from './store/memory-backend.js';
|
|
18
|
+
import type { MemoryConfig } from './config.js';
|
|
19
|
+
export interface KeywordBackfillDeps {
|
|
20
|
+
ctx: Context;
|
|
21
|
+
cfg: MemoryConfig;
|
|
22
|
+
backend: MemoryBackend;
|
|
23
|
+
logger: MemoryLogger;
|
|
24
|
+
}
|
|
25
|
+
export interface KeywordBackfillState {
|
|
26
|
+
running: boolean;
|
|
27
|
+
updated: number;
|
|
28
|
+
failed: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function keywordBackfillState(): KeywordBackfillState;
|
|
31
|
+
/** 启动后台关键词回填;已在运行返回 false(单飞)。 */
|
|
32
|
+
export declare function startKeywordBackfill(deps: KeywordBackfillDeps): {
|
|
33
|
+
started: boolean;
|
|
34
|
+
running: boolean;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* 回填主体(导出供单测 await 到完成;生产走 `startKeywordBackfill` 的单飞后台包装)。
|
|
38
|
+
* 门控关(`cfg.keyword.enabled = false`)时入口立即返回,不碰任何记录与 fts。
|
|
39
|
+
*/
|
|
40
|
+
export declare function runKeywordBackfillOnce(deps: KeywordBackfillDeps): Promise<void>;
|
|
41
|
+
/** 关键词标注器(导出供单测)。 */
|
|
42
|
+
export declare function labelKeywordChunk(ctx: Context, cfg: MemoryConfig, logger: MemoryLogger, chunk: MemoryRecord[]): Promise<Array<{
|
|
43
|
+
record: MemoryRecord;
|
|
44
|
+
keywords: string[];
|
|
45
|
+
usageScenario?: string[];
|
|
46
|
+
}>>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { callLLM, parseJsonLogged } from './llm.js';
|
|
2
|
+
import { normKeywords, normUsageScenario, KEYWORDS_METADATA_KEY } from './metadata-validators.js';
|
|
3
|
+
import { yieldLoop } from './util/yield.js';
|
|
4
|
+
/** 每块条数(一次 LLM 调用判定的记录数;与 wing-backfill 同口径)。 */
|
|
5
|
+
const CHUNK = 20;
|
|
6
|
+
/** 扫描页大小(轻量投影分页;与 wing-backfill 一致)。 */
|
|
7
|
+
const PAGE = 200;
|
|
8
|
+
/** 单次任务默认处理上限(cfg.backfill.maxRecords 优先)。 */
|
|
9
|
+
const DEFAULT_MAX_RECORDS = 300;
|
|
10
|
+
/** 墙钟默认预算(cfg.backfill.timeBudgetMs 优先)。 */
|
|
11
|
+
const DEFAULT_TIME_BUDGET_MS = 120_000;
|
|
12
|
+
const state = { running: false, updated: 0, failed: 0 };
|
|
13
|
+
export function keywordBackfillState() {
|
|
14
|
+
return { ...state };
|
|
15
|
+
}
|
|
16
|
+
// ── SYSTEM 提示词(定稿版,由 tools/keyword-distill.prompt.md 的 ```text 块迁移) ──
|
|
17
|
+
const SYSTEM_PROMPT = [
|
|
18
|
+
'你是记忆库的关键词与情景标注器。给定若干条记忆(每条含 id、标题、正文摘要),为每条提炼:',
|
|
19
|
+
'1) keywords:最能代表该记忆的检索词,用于精确命中;2) usage_scenario:未来「何时应当浮现这条记忆」的场景短语。',
|
|
20
|
+
'',
|
|
21
|
+
'严格规则:',
|
|
22
|
+
'- keywords 的唯一判据:**用户未来会输入什么词把这条记忆捞出来**。优先具体名词性标识——项目/模块/文件/函数/配置项/命令/错误码/版本号/数字阈值/工具名/专有实体名。',
|
|
23
|
+
'- **禁止**把正文里的动宾短语压缩成抽象黑话当关键词(反例:把"判定批量流量"压成"批量流量判定"、把"做窗口滑动归因"压成"窗口滑动归因"、把"保持只读约束"压成"只读约束")——这类词没人会拿去搜,一律换成它指向的具体实体/标识,或直接删掉。',
|
|
24
|
+
'- keywords:2–5 个(宁少勿滥,够区分即可);每个 2–6 词或 1 个复合术语;不得与标题字面重复、不得互相近义堆叠、不收停用词/整句。',
|
|
25
|
+
'- usage_scenario:1–3 条;每条 5–10 词的短场景描述(不是完整命令句);彼此必须可区分,相似的合并成更泛化的一条。',
|
|
26
|
+
'- 语言跟随该记忆正文:中文记忆产出中文(技术标识符、API 名、错误码原样保留),英文记忆产出英文。',
|
|
27
|
+
'- id 必须原样照抄,不得改写、截断或改用序号;一条记忆只产一组,不要拆分。',
|
|
28
|
+
'',
|
|
29
|
+
'输出:只输出一个 JSON 数组,不要多余文字、不要解释。每项形如:',
|
|
30
|
+
'{"id":"<原id>","keywords":["…","…"],"usage_scenario":["…","…"]}',
|
|
31
|
+
].join('\n');
|
|
32
|
+
/** 启动后台关键词回填;已在运行返回 false(单飞)。 */
|
|
33
|
+
export function startKeywordBackfill(deps) {
|
|
34
|
+
if (state.running)
|
|
35
|
+
return { started: false, running: true };
|
|
36
|
+
state.running = true;
|
|
37
|
+
state.updated = 0;
|
|
38
|
+
state.failed = 0;
|
|
39
|
+
void runKeywordBackfillOnce(deps).finally(() => {
|
|
40
|
+
state.running = false;
|
|
41
|
+
deps.logger.info(`[memory] keyword 回填结束:成功 ${state.updated} 条,失败 ${state.failed} 条`);
|
|
42
|
+
});
|
|
43
|
+
return { started: true, running: true };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 回填主体(导出供单测 await 到完成;生产走 `startKeywordBackfill` 的单飞后台包装)。
|
|
47
|
+
* 门控关(`cfg.keyword.enabled = false`)时入口立即返回,不碰任何记录与 fts。
|
|
48
|
+
*/
|
|
49
|
+
export async function runKeywordBackfillOnce(deps) {
|
|
50
|
+
const { cfg, backend, logger } = deps;
|
|
51
|
+
if (!cfg.keyword?.enabled) {
|
|
52
|
+
logger.info('[memory] keyword 回填跳过:keyword.enabled = false(功能关,不回填)');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const MAX_RECORDS = cfg.backfill?.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
56
|
+
const TIME_BUDGET_MS = cfg.backfill?.timeBudgetMs ?? DEFAULT_TIME_BUDGET_MS;
|
|
57
|
+
const startedAt = Date.now();
|
|
58
|
+
// ── 扫描阶段(轻量投影,不反序列化正文)──
|
|
59
|
+
const total = await backend.size();
|
|
60
|
+
const pendingIds = [];
|
|
61
|
+
let scanned = 0;
|
|
62
|
+
for (let offset = 0;; offset += PAGE) {
|
|
63
|
+
const page = await backend.allLite(PAGE, offset);
|
|
64
|
+
if (page.length === 0)
|
|
65
|
+
break;
|
|
66
|
+
for (const r of page) {
|
|
67
|
+
const kw = r.metadata?.[KEYWORDS_METADATA_KEY];
|
|
68
|
+
if (!(Array.isArray(kw) && kw.length > 0))
|
|
69
|
+
pendingIds.push(r.id);
|
|
70
|
+
}
|
|
71
|
+
scanned += page.length;
|
|
72
|
+
if (Date.now() - startedAt > TIME_BUDGET_MS) {
|
|
73
|
+
logger.info(`[memory] keyword 回填:墙钟预算(${TIME_BUDGET_MS / 1000}s)在扫描阶段用尽(已扫 ${scanned}/${total}),留待再触发`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await yieldLoop();
|
|
77
|
+
}
|
|
78
|
+
let budget = Math.min(pendingIds.length, MAX_RECORDS);
|
|
79
|
+
logger.info(`[memory] keyword 回填启动:缺关键词 ${pendingIds.length} 条,本次上限 ${budget} 条`);
|
|
80
|
+
// ── 标注阶段(补水→LLM→归一→写回)──
|
|
81
|
+
for (let offset = 0; offset < pendingIds.length && budget > 0; offset += CHUNK) {
|
|
82
|
+
if (Date.now() - startedAt > TIME_BUDGET_MS) {
|
|
83
|
+
logger.info(`[memory] keyword 回填:墙钟预算用尽,剩余 ${pendingIds.length - offset} 条留待再触发`);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
const hydrated = await backend.getByIds(pendingIds.slice(offset, offset + CHUNK));
|
|
87
|
+
const chunk = hydrated.filter((r) => r.content && r.content.trim() !== '');
|
|
88
|
+
if (chunk.length === 0)
|
|
89
|
+
continue;
|
|
90
|
+
budget -= chunk.length;
|
|
91
|
+
const labeled = await labelKeywordChunk(deps.ctx, cfg, logger, chunk);
|
|
92
|
+
for (const { record, keywords, usageScenario } of labeled) {
|
|
93
|
+
const meta = { ...(record.metadata ?? {}) };
|
|
94
|
+
meta[KEYWORDS_METADATA_KEY] = keywords;
|
|
95
|
+
if (usageScenario && usageScenario.length > 0)
|
|
96
|
+
meta.dsh_usage_scenario = usageScenario;
|
|
97
|
+
if (await backend.patchMetadataAndReindex(record.id, meta))
|
|
98
|
+
state.updated++;
|
|
99
|
+
else
|
|
100
|
+
state.failed++;
|
|
101
|
+
await yieldLoop();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** 关键词标注器(导出供单测)。 */
|
|
106
|
+
export async function labelKeywordChunk(ctx, cfg, logger, chunk) {
|
|
107
|
+
const list = chunk.map((r, i) => `${i + 1}. id=${r.id}\n标题:${r.name ?? ''}\n正文:${r.content.slice(0, 300)}`).join('\n\n');
|
|
108
|
+
const user = '记忆列表:\n' + list;
|
|
109
|
+
try {
|
|
110
|
+
const raw = await callLLM(ctx, cfg, {
|
|
111
|
+
system: SYSTEM_PROMPT,
|
|
112
|
+
user,
|
|
113
|
+
maxTokens: 4096,
|
|
114
|
+
layer: 'l1-extract',
|
|
115
|
+
logger,
|
|
116
|
+
});
|
|
117
|
+
const parsed = parseJsonLogged(raw, 'keyword 回填', logger);
|
|
118
|
+
if (!Array.isArray(parsed))
|
|
119
|
+
return [];
|
|
120
|
+
const byId = new Map(chunk.map((r) => [r.id, r]));
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const item of parsed) {
|
|
123
|
+
const record = typeof item?.id === 'string' ? byId.get(item.id) : undefined;
|
|
124
|
+
if (!record) {
|
|
125
|
+
logger.warn(`[memory] keyword 回填:id 配对失败,已丢弃(id=${String(item?.id).slice(0, 40)})`);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const keywords = normKeywords(item.keywords, 5);
|
|
129
|
+
if (keywords.length === 0) {
|
|
130
|
+
logger.warn(`[memory] keyword 回填:normKeywords 结果为空,已跳过(id=${record.id})`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const usageScenario = normUsageScenario(item.usage_scenario, 3);
|
|
134
|
+
out.push({ record, keywords, usageScenario: usageScenario.length > 0 ? usageScenario : undefined });
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
logger.warn(`[memory] keyword 回填块失败(跳过 ${chunk.length} 条,原记录未改): ${err instanceof Error ? err.message : String(err)}`);
|
|
140
|
+
state.failed += chunk.length;
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -32,6 +32,15 @@ export { isCognitiveHall, COGNITIVE_HALLS, type CognitiveHall } from './cognitiv
|
|
|
32
32
|
* 非数组或空→ `[]`(缺字段中性,不抛——防单条抽取失败波及整切片重试)。
|
|
33
33
|
*/
|
|
34
34
|
export declare function normUsageScenario(v: unknown, max?: number): string[];
|
|
35
|
+
/** 关键词检索轴的磁盘保留键(喂 `l1_fts.keywords` 列,读取侧见 `sqlite.ftsKeywordText`)。 */
|
|
36
|
+
export declare const KEYWORDS_METADATA_KEY = "dsh_keywords";
|
|
37
|
+
/**
|
|
38
|
+
* 关键词归一 + 去重(忽略大小写)+ 长度钳 + 上限。**所有 `dsh_keywords` 写回点必须过这里**。
|
|
39
|
+
*
|
|
40
|
+
* 与 `normUsageScenario` 同范式(关键词是自然语言术语/标识符,非 slug,不做字符集校验),
|
|
41
|
+
* 只是上限更紧(≤5)、单条更短。非数组/空→`[]`(缺字段中性,不抛——防单条产出失败波及整批)。
|
|
42
|
+
*/
|
|
43
|
+
export declare function normKeywords(v: unknown, max?: number): string[];
|
|
35
44
|
/**
|
|
36
45
|
* 读 **Wing(生活域)**。⚠️ 磁盘键是 `metadata.hall`——历史别名,存的其实是 **wing 值**。
|
|
37
46
|
* 只认词表内的合法 wing(8 角 + general),否则 undefined。
|
|
@@ -92,6 +92,37 @@ export function normUsageScenario(v, max = 3) {
|
|
|
92
92
|
}
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
|
+
/** 关键词检索轴的磁盘保留键(喂 `l1_fts.keywords` 列,读取侧见 `sqlite.ftsKeywordText`)。 */
|
|
96
|
+
export const KEYWORDS_METADATA_KEY = 'dsh_keywords';
|
|
97
|
+
/** 单个关键词最大字符数:按“词”太主观,用字符上限兜住整句泄漏(CJK 复合术语通常 ≤12)。 */
|
|
98
|
+
const KEYWORD_MAX_CHARS = 24;
|
|
99
|
+
/**
|
|
100
|
+
* 关键词归一 + 去重(忽略大小写)+ 长度钳 + 上限。**所有 `dsh_keywords` 写回点必须过这里**。
|
|
101
|
+
*
|
|
102
|
+
* 与 `normUsageScenario` 同范式(关键词是自然语言术语/标识符,非 slug,不做字符集校验),
|
|
103
|
+
* 只是上限更紧(≤5)、单条更短。非数组/空→`[]`(缺字段中性,不抛——防单条产出失败波及整批)。
|
|
104
|
+
*/
|
|
105
|
+
export function normKeywords(v, max = 5) {
|
|
106
|
+
if (!Array.isArray(v) || max <= 0)
|
|
107
|
+
return [];
|
|
108
|
+
const out = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (const raw of v) {
|
|
111
|
+
if (typeof raw !== 'string')
|
|
112
|
+
continue;
|
|
113
|
+
const t = raw.trim();
|
|
114
|
+
if (!t)
|
|
115
|
+
continue;
|
|
116
|
+
const key = t.toLowerCase();
|
|
117
|
+
if (seen.has(key))
|
|
118
|
+
continue;
|
|
119
|
+
seen.add(key);
|
|
120
|
+
out.push(t.slice(0, KEYWORD_MAX_CHARS));
|
|
121
|
+
if (out.length >= max)
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
95
126
|
/**
|
|
96
127
|
* 读 **Wing(生活域)**。⚠️ 磁盘键是 `metadata.hall`——历史别名,存的其实是 **wing 值**。
|
|
97
128
|
* 只认词表内的合法 wing(8 角 + general),否则 undefined。
|
package/dist/stats.js
CHANGED
|
@@ -33,6 +33,7 @@ import { isRoomSlug } from './metadata-validators.js';
|
|
|
33
33
|
import { InProcMemoryBackend } from './store/memory-backend.js';
|
|
34
34
|
import { isWingCorner } from './store/session-modes.js';
|
|
35
35
|
import { startWingBackfill } from './wing-backfill.js';
|
|
36
|
+
import { startKeywordBackfill, keywordBackfillState } from './keyword-backfill.js';
|
|
36
37
|
import { errDetail } from './util/filelog.js';
|
|
37
38
|
import { snapshotTokenCost } from './token-cost.js';
|
|
38
39
|
import { computeMaturity } from './util/maturity.js';
|
|
@@ -70,6 +71,7 @@ export const MEMORY_ENDPOINTS = [
|
|
|
70
71
|
'dsh-memory/room-register',
|
|
71
72
|
'dsh-memory/room-admin',
|
|
72
73
|
'dsh-memory/wing-backfill',
|
|
74
|
+
'dsh-memory/keyword-backfill',
|
|
73
75
|
'dsh-memory/session-stats',
|
|
74
76
|
'dsh-memory/settings-get',
|
|
75
77
|
'dsh-memory/settings-set',
|
|
@@ -1140,6 +1142,19 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1140
1142
|
});
|
|
1141
1143
|
return r;
|
|
1142
1144
|
}
|
|
1145
|
+
case 'dsh-memory/keyword-backfill': {
|
|
1146
|
+
// 门控关:不启动,如实报 enabled=false(面板据此提示“先开关键词回填开关”)。
|
|
1147
|
+
if (!cfg.keyword?.enabled)
|
|
1148
|
+
return { started: false, running: false, enabled: false, updated: 0, failed: 0 };
|
|
1149
|
+
const r = startKeywordBackfill({
|
|
1150
|
+
ctx: deps.ctx,
|
|
1151
|
+
cfg: deps.cfg,
|
|
1152
|
+
backend: stores.backend ?? new InProcMemoryBackend(stores.l1),
|
|
1153
|
+
logger: deps.logger,
|
|
1154
|
+
});
|
|
1155
|
+
const st = keywordBackfillState();
|
|
1156
|
+
return { started: r.started, running: r.running, enabled: true, updated: st.updated, failed: st.failed };
|
|
1157
|
+
}
|
|
1143
1158
|
// ── 会话级统计(悬浮卡信息区;热路径端点,见 SessionInfoSource 的零 I/O 硬规则) ──
|
|
1144
1159
|
case 'dsh-memory/session-stats': {
|
|
1145
1160
|
if (!sessionInfo)
|
package/dist/store/l1.d.ts
CHANGED
|
@@ -127,6 +127,12 @@ export declare class L1Store {
|
|
|
127
127
|
* 返回 false 表示 id 不存在或写入失败——调用方须记账,不许静默。
|
|
128
128
|
*/
|
|
129
129
|
patchMetadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* 写 metadata_json + 顺带重索引 `l1_fts`(关键词回填专用)。
|
|
132
|
+
* 与 `patchMetadata` 的区分:后者只改主表(适合 tags/room/relabel 这类不进 fts 的元数据);
|
|
133
|
+
* 本方法额外重跑该行的 fts 插入,让新写的 `dsh_keywords` 立刻进 BM25 关键词轴。
|
|
134
|
+
*/
|
|
135
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): boolean;
|
|
130
136
|
/** repo 归属修补(ADR-0015 T3.3;人工消歧用)。CAS:expectRepoKeyName 非空时要求当前值匹配。 */
|
|
131
137
|
patchRepoKey(id: string, patch: {
|
|
132
138
|
repoKeyName?: string;
|
package/dist/store/l1.js
CHANGED
|
@@ -156,6 +156,14 @@ export class L1Store {
|
|
|
156
156
|
patchMetadata(id, metadata) {
|
|
157
157
|
return this.db.patchL1Metadata(id, metadata);
|
|
158
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* 写 metadata_json + 顺带重索引 `l1_fts`(关键词回填专用)。
|
|
161
|
+
* 与 `patchMetadata` 的区分:后者只改主表(适合 tags/room/relabel 这类不进 fts 的元数据);
|
|
162
|
+
* 本方法额外重跑该行的 fts 插入,让新写的 `dsh_keywords` 立刻进 BM25 关键词轴。
|
|
163
|
+
*/
|
|
164
|
+
patchMetadataAndReindex(id, metadata) {
|
|
165
|
+
return this.db.patchL1MetadataAndReindex(id, metadata);
|
|
166
|
+
}
|
|
159
167
|
/** repo 归属修补(ADR-0015 T3.3;人工消歧用)。CAS:expectRepoKeyName 非空时要求当前值匹配。 */
|
|
160
168
|
patchRepoKey(id, patch, expectRepoKeyName) {
|
|
161
169
|
const changed = this.db.patchRepoKey(id, patch, expectRepoKeyName);
|
|
@@ -29,6 +29,7 @@ export declare class WorkerMemoryBackend implements MemoryBackend {
|
|
|
29
29
|
getByIds(ids: string[]): Promise<MemoryRecord[]>;
|
|
30
30
|
list(opts: L1ListOpts): Promise<L1ListResult>;
|
|
31
31
|
patchMetadata(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
32
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
32
33
|
size(): Promise<number>;
|
|
33
34
|
dispose(): Promise<void>;
|
|
34
35
|
}
|
|
@@ -100,6 +100,9 @@ export class WorkerMemoryBackend {
|
|
|
100
100
|
async patchMetadata(id, metadata) {
|
|
101
101
|
return this.call('patchMetadata', [id, metadata]);
|
|
102
102
|
}
|
|
103
|
+
async patchMetadataAndReindex(id, metadata) {
|
|
104
|
+
return this.call('patchMetadataAndReindex', [id, metadata]);
|
|
105
|
+
}
|
|
103
106
|
async size() {
|
|
104
107
|
return this.call('size', []);
|
|
105
108
|
}
|
|
@@ -50,6 +50,8 @@ export interface MemoryBackend {
|
|
|
50
50
|
list(opts: L1ListOpts): Promise<L1ListResult>;
|
|
51
51
|
/** **只**更新 metadata_json(不动 content,不重算嵌入)。 */
|
|
52
52
|
patchMetadata(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
53
|
+
/** 写 metadata_json + 重索引该行的 `l1_fts`(关键词回填专用;新 `dsh_keywords` 要立刻进 BM25 轴)。 */
|
|
54
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
53
55
|
/** L1 记录总数(巡检进度分母)。 */
|
|
54
56
|
size(): Promise<number>;
|
|
55
57
|
/** 释放后端(worker 实现会 terminate 线程)。 */
|
|
@@ -68,6 +70,7 @@ export declare class InProcMemoryBackend implements MemoryBackend {
|
|
|
68
70
|
getByIds(ids: string[]): Promise<MemoryRecord[]>;
|
|
69
71
|
list(opts: L1ListOpts): Promise<L1ListResult>;
|
|
70
72
|
patchMetadata(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
73
|
+
patchMetadataAndReindex(id: string, metadata: Record<string, unknown>): Promise<boolean>;
|
|
71
74
|
size(): Promise<number>;
|
|
72
75
|
dispose(): Promise<void>;
|
|
73
76
|
}
|
|
@@ -21,6 +21,9 @@ export class InProcMemoryBackend {
|
|
|
21
21
|
async patchMetadata(id, metadata) {
|
|
22
22
|
return this.l1.patchMetadata(id, metadata);
|
|
23
23
|
}
|
|
24
|
+
async patchMetadataAndReindex(id, metadata) {
|
|
25
|
+
return this.l1.patchMetadataAndReindex(id, metadata);
|
|
26
|
+
}
|
|
24
27
|
async size() {
|
|
25
28
|
return this.l1.size;
|
|
26
29
|
}
|
|
@@ -60,6 +60,9 @@ else {
|
|
|
60
60
|
case 'patchMetadata':
|
|
61
61
|
reply(id, true, handle.patchL1Metadata(String(args[0]), args[1]));
|
|
62
62
|
return;
|
|
63
|
+
case 'patchMetadataAndReindex':
|
|
64
|
+
reply(id, true, handle.patchL1MetadataAndReindex(String(args[0]), args[1]));
|
|
65
|
+
return;
|
|
63
66
|
case 'size':
|
|
64
67
|
reply(id, true, handle.countL1());
|
|
65
68
|
return;
|
package/dist/store/sqlite.d.ts
CHANGED
|
@@ -305,6 +305,17 @@ export declare class MemoryDb {
|
|
|
305
305
|
/** 打标作业完成/失败(失败保留行供诊断;重算由下一次触发再入队)。 */
|
|
306
306
|
finishSceneRecluster(jobId: string, ok: boolean): void;
|
|
307
307
|
patchL1Metadata(id: string, metadata: Record<string, unknown>): boolean;
|
|
308
|
+
/**
|
|
309
|
+
* 更新 metadata_json 后**顺带重索引该行的 `l1_fts`**(关键词回填专用)。
|
|
310
|
+
*
|
|
311
|
+
* `patchL1Metadata` 本身不碰 fts——多数写回点(tags / room / relabel)不需要、也不想付这个代价;
|
|
312
|
+
* 但关键词轴若只写元数据不重索引,`ftsKeywordText` 读不到新 `dsh_keywords`,那条 BM25 轴
|
|
313
|
+
* 对存量记录永远空。故把「写元数据 + 重索引」焊在一个方法里,杜绝“写了关键词但检索看不到”。
|
|
314
|
+
*
|
|
315
|
+
* 退场/不存在的行:只从 fts 删除(幂等),不插入。fts 不可用时降级为纯 `patchL1Metadata`。
|
|
316
|
+
* 参数列表与 `stmtL1FtsInsert` / `backfillL1Fts` 逐位对齐(同三处列清单,漏一列静默丢索引)。
|
|
317
|
+
*/
|
|
318
|
+
patchL1MetadataAndReindex(id: string, metadata: Record<string, unknown>): boolean;
|
|
308
319
|
/** 全量读取(调试/迁移/重嵌入用;检索请走 FTS/向量)。 */
|
|
309
320
|
getAllL1(): MemoryRecord[];
|
|
310
321
|
getL1ByIds(ids: string[]): MemoryRecord[];
|
package/dist/store/sqlite.js
CHANGED
|
@@ -1509,6 +1509,39 @@ export class MemoryDb {
|
|
|
1509
1509
|
return false;
|
|
1510
1510
|
}
|
|
1511
1511
|
}
|
|
1512
|
+
/**
|
|
1513
|
+
* 更新 metadata_json 后**顺带重索引该行的 `l1_fts`**(关键词回填专用)。
|
|
1514
|
+
*
|
|
1515
|
+
* `patchL1Metadata` 本身不碰 fts——多数写回点(tags / room / relabel)不需要、也不想付这个代价;
|
|
1516
|
+
* 但关键词轴若只写元数据不重索引,`ftsKeywordText` 读不到新 `dsh_keywords`,那条 BM25 轴
|
|
1517
|
+
* 对存量记录永远空。故把「写元数据 + 重索引」焊在一个方法里,杜绝“写了关键词但检索看不到”。
|
|
1518
|
+
*
|
|
1519
|
+
* 退场/不存在的行:只从 fts 删除(幂等),不插入。fts 不可用时降级为纯 `patchL1Metadata`。
|
|
1520
|
+
* 参数列表与 `stmtL1FtsInsert` / `backfillL1Fts` 逐位对齐(同三处列清单,漏一列静默丢索引)。
|
|
1521
|
+
*/
|
|
1522
|
+
patchL1MetadataAndReindex(id, metadata) {
|
|
1523
|
+
const ok = this.patchL1Metadata(id, metadata);
|
|
1524
|
+
if (!ok || this.degraded || !this.ftsAvailable)
|
|
1525
|
+
return ok;
|
|
1526
|
+
try {
|
|
1527
|
+
const r = this.db
|
|
1528
|
+
.prepare(`SELECT record_id, content, type, priority, scene_name, session_id, version,
|
|
1529
|
+
timestamp_str, timestamp_start, timestamp_end, metadata_json, family,
|
|
1530
|
+
scope, workspace_id FROM l1_records
|
|
1531
|
+
WHERE record_id = ? AND NOT (${retiredL1Sql()})`)
|
|
1532
|
+
.get(id);
|
|
1533
|
+
// 退场/缺失行:只确保从 fts 剔除(幂等),不插入;活跃行:删旧行再按当前(含新关键词)元数据重插。
|
|
1534
|
+
this.stmtL1FtsDelete.run(id);
|
|
1535
|
+
if (r) {
|
|
1536
|
+
this.stmtL1FtsInsert.run(tokenizeForFts(ftsKeywordText(parseMetadataJson(String(r.metadata_json ?? '{}')))), tokenizeForFts(String(r.content ?? '')), String(r.content ?? ''), String(r.record_id ?? ''), String(r.type ?? ''), Number(r.priority ?? 50), String(r.scene_name ?? ''), String(r.session_id ?? 'default'), Number(r.version ?? 0), String(r.timestamp_str ?? ''), String(r.timestamp_start ?? ''), String(r.timestamp_end ?? ''), String(r.metadata_json ?? '{}'), String(r.family ?? 'chat'), normScope(r.scope), String(r.workspace_id ?? ''));
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
catch (err) {
|
|
1540
|
+
// 重索引失败不回滚元数据写(关键词已落主表);下次全量 FTS 重建会对齐,并留痕。
|
|
1541
|
+
this.logger?.warn(`${TAG} 关键词重索引失败(id=${id},元数据已写、fts 待重建对齐): ${err instanceof Error ? err.message : String(err)}`);
|
|
1542
|
+
}
|
|
1543
|
+
return ok;
|
|
1544
|
+
}
|
|
1512
1545
|
/** 全量读取(调试/迁移/重嵌入用;检索请走 FTS/向量)。 */
|
|
1513
1546
|
getAllL1() {
|
|
1514
1547
|
if (this.degraded)
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-prime-memory",
|
|
3
3
|
"name": "dsh-prime-memory",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.25.0",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
|
|
7
7
|
"zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|