memory-eternal 0.9.17 → 0.9.23
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.en.md +4 -0
- package/README.md +4 -0
- package/index.js +43 -0
- package/lib/api.js +54 -1
- package/lib/client.js +17 -17
- package/lib/config-sync.js +89 -0
- package/lib/sse.js +51 -0
- package/lib/web.js +13 -2
- package/package.json +2 -2
- package/web/app.js +18 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "memory-eternal",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.23",
|
|
5
5
|
"description": "璁板繂鏍稿績锛圡emory Eternal锛夛細鐭ヨ瘑鍗¤嚜鍔ㄦ矇娣€/鍙洖锛岀粺涓€ ~/.dsh/memory-vault锛屼細璇濈粨鏉熻嚜鍔ㄥ叆搴撹繘寰呭鏍搞€?,
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "EternalNight996",
|
package/README.en.md
CHANGED
|
@@ -312,6 +312,10 @@ Diagnostics:
|
|
|
312
312
|
|
|
313
313
|
| Version | Date | Highlights |
|
|
314
314
|
|---|---|---|
|
|
315
|
+
| **v0.9.23** | 2026-09-30 | **No more save flicker.** After a successful save the client immediately re-fetches `/config`, but the host volatile echo lags, so inputs visibly reverted to the old values and then jumped back. The client now keeps a **local overlay** for fields that were saved but not yet echoed (`src/client/config-merge.js`, pure function + 4 checks), dropping each key once the host echoes it, and following the host value afterwards; resetting the form clears the overlay. `npm test` now runs **139 checks** |
|
|
316
|
+
| **v0.9.22** | 2026-09-30 | **Near-instant config sync (SSE push + file watching).** ① A new SSE hub (`lib/sse.js`): both hosts (DSH-embedded and standalone web) expose `/memory-eternal/api/events`, so a config change is pushed to every open page and the client reloads automatically — **without overwriting what you are currently typing** (dirty flag). ② Standalone save → DSH apply now triggers via `fs.watch` in **milliseconds** instead of a 5-second poll (the poll remains as a fallback). ③ The standalone page watches the shared config file, so a DSH-side save shows up there immediately. Adds `tests/sse.test.mjs` (5 checks); `npm test` now runs **135 checks** |
|
|
317
|
+
| **v0.9.19** | 2026-09-30 | **"Save failed" now says why (root cause: clearing a number box yields 0, the schema rejects it, and the host answered with an empty 500).** Reproduced against the live host: out-of-range value / cleared number box / wrong type → **HTTP 500 with an empty body** → the client `r.json()` throws → only the generic "Save failed" is shown. Fixes: ① clearing a number input **no longer silently becomes 0** (an empty string coerced to a number is 0, while `recycleRetentionDays` requires `min(1)`); ② pre-save **per-field range validation** that names the field ("recycleRetentionDays must be a number between 1 and 3650, now 0"); ③ defensive response parsing: on a non-JSON or empty body the panel shows the HTTP status and the raw snippet instead of "Save failed"; ④ the host POST is wrapped so it **never returns an empty body**, and returns 400 plus the field names for empty numeric values; ⑤ the standalone `/config` validates types and ranges so invalid pending values cannot be written; ⑥ `drainPendingConfig` counts failures and gives up (deleting the file) after 5 tries instead of retrying forever. `npm test` now runs **130 checks** |
|
|
318
|
+
| **v0.9.18** | 2026-09-30 | **The standalone web page can finally save config — and a read-only page no longer feels dead.** Root cause (the other half of issue #12): the standalone web server has **no DSH settings service**, so its `/config` always returned `writable:false`; the client `save()` starts with `if (readonly) return` and the button was `disabled` — clicking Save did **nothing at all**, independent of the plugin version. Fixes: ① new shared-file protocol in `lib/config-sync.js`: the standalone page writes `memory-eternal-config.pending.json` atomically, and the **DSH host applies it on activation and every 5 seconds** via `settings.update(patch, undefined)` (skipping the optimistic-concurrency check), deleting the file on success and retrying on failure; if DSH is not running the change waits in the file for the next start. ② The standalone `/config` now reports `writable:true, viaPending:true` and the page shows a blue note explaining the sync. ③ **The button is no longer disabled when read-only** — clicking always explains why (`🔒 Read-only` + "desktop app → Settings → Memory"), so "no reaction" can no longer happen. Adds `tests/config-sync.test.mjs` (4 checks). `npm test` now runs **125 checks** |
|
|
315
319
|
| **v0.9.17** | 2026-09-30 | **"I updated it but the UI is still old" — fixed for good.** ① The build now injects the plugin version into the client bundle and the settings page shows a "**Page script vX**" chip; ② when the page-script version differs from the on-disk version it says so loudly: "⚠ This page is still running an older script (page v{page} / disk v{disk}): **press Ctrl+F5**"; the running-vs-disk hint now also leads with "refresh first, restart the desktop app / DSH only if the host half is old". Root cause of issue #12 ("saving shows no feedback"): the page was still executing the JS loaded before the update — the host sends `Cache-Control: no-store`, but **an already-open page never reloads its scripts**, so without a refresh you keep seeing the old UI. The standalone web service on `:7999` (an old process started 9/29) was restarted and now reports `loaded=onDisk=latest` |
|
|
316
320
|
| **v0.9.16** | 2026-09-30 | **Fixed config saves silently not persisting (issue #12) + full config coverage.** ① **Save path**: the host `dsh-settings.write()` requires the revision held by the panel to match `describe()` **exactly**; once stale it throws `SettingsConflictError` and the save fails — the plugin now re-reads the latest revision and **retries once** (marked ♻ in the message). ② **Late volatile echo**: after a successful write the patch is overlaid on the local view (reopening the panel shows the new value immediately) and the overlay is dropped once the host snapshot catches up; the host POST now **re-reads after writing** and returns `pending` with an honest note ("written; restart DSH to apply from the config file") when the echo has not landed. ③ **Immediate save feedback** (requested earlier): ⏳ Saving / ✅ Saved / ⚠ error now appears right next to the Save button, including whether a retry or an echo was involved, and the button label no longer wrongly says "Exporting…". ④ **Full config coverage**: seven fields that previously could only be edited by hand are now in the panel — the **master switch `enabled`**, single-vault `vaultDir`, distillation route `captureProvider` / `captureModel`, cooldown `captureCooldownMs`, semantic recall `recallEmbedding` and `sessionBudgetChars`; the host `/config` gained 6 keys and the standalone web `/config` gained 9. ⑤ New **permanent guard tests**: `tests/config-coverage.test.mjs` asserts every Config field is covered by the host API, the standalone web API and the settings panel (missing one turns the suite red), and `tests/settings-save.test.mjs` (5 checks) pins revision retry, overlay fallback and "never swallow errors". `npm test` now runs **121 checks** |
|
|
317
321
|
| **v0.9.15** | 2026-09-29 | **Server-side graph speed-ups only — rendering is identical to v0.9.13.** ① **On-disk graph cache** keyed by fingerprint at `<vault>/memory-eternal-graph-cache.json` (atomic write, invalidated by fingerprint, skipped above 8MB, failures only cost a rebuild): measured cold start **422ms → 11ms** at 596 cards; ② **hashed bigrams + counting-sorted rare-gram index**: `bigramHashSet()` packs each bigram into a 32-bit integer (`Set<number>`; **capture de-duplication still uses the string version, so semantics are unchanged**), and the rare-gram index is built with a counting sort (202 → **54ms**); ③ **MinHash + LSH** candidate generation (48-dim signatures, 24 bands, unioned with the rare-gram index and then **verified exactly**): Jaccard calls 15,148 → 6,797 and the similarity stage 210 → 130ms. Full cold build **600 → 422ms**, edges unchanged from v0.9.13 (tag 2777 / similar 4). **Zero client changes** — no sprites, no static layer, no skeleton degradation (the v0.9.14 client work was reverted and deprecated because it hurt the feel). Adds `lib/minhash.js` + `tests/minhash.test.mjs` (4 checks); `npm test` runs **108 checks** |
|
package/README.md
CHANGED
|
@@ -336,6 +336,10 @@ curl http://127.0.0.1:7999/memory-eternal/api/web-info # 独立 web 是否活
|
|
|
336
336
|
|
|
337
337
|
| 版本 | 日期 | 关键改动 |
|
|
338
338
|
|---|---|---|
|
|
339
|
+
| **v0.9.23** | 2026-09-30 | **保存不再闪烁**。保存成功后客户端会立刻重拉 `/config`,而宿主 volatile 回流是滞后的 → 输入框会「先退回原配置、再跳回修改后的值」。现在客户端把「已保存但宿主未回显」的字段做**本地叠加**(新增 `src/client/config-merge.js` 纯函数 + 4 项单测),宿主回显一致后自动摘除,之后仍跟随宿主权威值;重置表单会清空叠加层。`npm test` 共 **139 项** |
|
|
340
|
+
| **v0.9.22** | 2026-09-30 | **配置准即时同步(SSE 推送 + 文件监听)**。① 新增 SSE 推送中心 `lib/sse.js`:DSH 内嵌宿主与独立 Web 宿主都暴露 `/memory-eternal/api/events`,配置一变更就推给所有已打开的页面,客户端订阅后自动重载 —— **正在编辑时不覆盖你的输入**(dirty 标记)。② 独立页保存 → DSH 应用:从 5 秒轮询升级为 `fs.watch` **毫秒级**触发(5 秒轮询保留为兜底)。③ 独立页监听共享配置文件:DSH 端一保存,独立页立刻同步。新增 `tests/sse.test.mjs`(5 项),`npm test` 共 **135 项** |
|
|
341
|
+
| **v0.9.19** | 2026-09-30 | **「保存失败」不再无信息量(根因:数字框清空=0 被 schema 拒绝,宿主回空 500)**。复现与实测:越界值 / 清空数字框 / 类型错误 → DSH 宿主返回 **HTTP 500 + 空响应体** → 客户端 `r.json()` 抛错 → 只能显示通用「保存失败」。修复:① **数字输入框清空不再静默变成 0**(JS 里空串转数字得 0,而 `recycleRetentionDays` 的 schema 是 `min(1)`,直接失败);② 保存前按 schema 范围**逐字段自检**,指名报错(例如「recycleRetentionDays」需要 1–3650 之间的数字,当前 0);③ 响应体兜底解析:非 JSON / 空响应体时显示 HTTP 状态码与原始片段,而不是「保存失败」;④ 宿主 POST 外层兜底,**绝不返回空响应体**,并对空字符串数值字段前置返回 400 + 字段名;⑤ 独立页 /config 增加类型与范围校验(拒绝空数值、类型不符),避免写入非法「待应用」值;⑥ `drainPendingConfig` 增加失败计数,连续 5 次失败后放弃并删除(不再每 5 秒无限重试)。`npm test` 共 **130 项** |
|
|
342
|
+
| **v0.9.18** | 2026-09-30 | **独立 Web 页终于能保存配置 + 只读状态不再「毫无反应」**。根因(issue #12 的另一半):独立 web server 进程**没有 DSH 的 settings 服务**,它的 `/config` 过去固定返回 `writable:false`,客户端 `save()` 第一行就 `if (readonly) return`,按钮还是 `disabled` —— 所以点保存**一点反应都没有**,与插件版本无关。修复:① 新增共享文件协议 `lib/config-sync.js`:独立页保存 → 原子写 `memory-eternal-config.pending.json` → **DSH 端激活时与每 5 秒轮询应用**(`settings.update(patch, undefined)` 跳过乐观并发校验),成功后删除,失败保留重试;DSH 没运行时改动留在文件里,下次启动生效。② 独立页 `/config` 返回 `writable:true, viaPending:true`,页面顶部显示蓝色说明「保存会写入待应用文件,由 DSH 自动同步」。③ **只读时按钮不再禁用**:点击必弹出明确原因(`🔒 只读` + 指引「桌面版 → 设置 → 记忆」),彻底消灭「按了没反应」。新增 `tests/config-sync.test.mjs`(4 项:读写往返、损坏内容容错、drain 成功即删/失败保留、clear 幂等)。`npm test` 共 **125 项** |
|
|
339
343
|
| **v0.9.17** | 2026-09-30 | **「更新了却还是旧界面」根治**。① 构建期把插件版本注入客户端 bundle,配置页新增「**页面脚本 vX**」徽标;② 当**页面脚本版本 ≠ 磁盘版本**时醒目提示「⚠ 当前页面仍在运行旧版脚本(页面 v{page} / 磁盘 v{disk}):**按 Ctrl+F5 刷新页面即生效**」;当「运行中(宿主加载)」落后于磁盘时,提示语也改为「先刷新页面,宿主半未更新才需重启桌面版 / DSH」。起因:issue #12「保存配置没有任何反馈」的真实原因之一,就是页面里跑的还是更新前加载的旧 JS —— 宿主已设 `Cache-Control: no-store`,但**已打开的页面不会自动重新加载脚本**,不刷新永远是旧的。另:独立 web 服务(`:7999`,9/29 启动的旧进程)已重启到新版并验证 `loaded=onDisk=latest` |
|
|
340
344
|
| **v0.9.16** | 2026-09-30 | **修复配置保存不生效(issue #12)+ 配置项全覆盖**。① **保存链路**:宿主 `dsh-settings.write()` 要求「面板持有的 revision 与 describe **完全一致**」,revision 一过期就抛 `SettingsConflictError` 导致保存失败 —— 现在遇到冲突会自动取最新 revision **重试一次**(提示里标 ♻)。② **宿主 volatile 回流滞后**:写成功后把改动叠加在本地视图(面板重开立刻是新值),等宿主快照追上再自动摘除;宿主 POST 新增**写入回读校验**,未回流时返回 `pending` 并提示「已写入,重启 DSH 后以配置文件为准」。③ **保存即时反馈**(你此前反馈过):保存按钮旁直接显示 ⏳ 保存中 / ✅ 已保存 / ⚠ 错误,并标明是否重试/是否回流;按钮文案修正(原来误显示「导出中…」)。④ **配置全覆盖**:补齐 7 个以前只能手改配置文件的字段 —— **总开关 `enabled`**、单库目录 `vaultDir`、蒸馏路由 `captureProvider` / `captureModel`、沉淀冷却 `captureCooldownMs`、语义召回 `recallEmbedding`、会话预算 `sessionBudgetChars`;宿主 `/config` 补 6 项、独立 web `/config` 补 9 项。⑤ 新增**永久守卫测试**:`tests/config-coverage.test.mjs` 保证每个 Config 字段都被「宿主 API / 独立 web API / 配置页」三处覆盖(漏一个就红),`tests/settings-save.test.mjs`(5 项)钉死 revision 重试、回流兜底与「错误不吞」。`npm test` 共 **121 项** |
|
|
341
345
|
| **v0.9.15** | 2026-09-29 | **只带服务器侧图谱提速(渲染与 v0.9.13 完全一致)**。① **建图落盘缓存**:按指纹缓存 nodes+edges 到 `<vault>/memory-eternal-graph-cache.json`(原子写、指纹失效、超 8MB 不缓存、写失败只影响下次重建),596 卡实测冷启动 **422ms → 11ms**;② **bigram 哈希化 + 稀有 gram 计数排序**:`bigramHashSet()` 把 bigram 压成 32 位整数入 `Set<number>`(**写卡去重仍用字符串版,判定语义不变**),稀有倒排改按 df 计数排序(202 → **54ms**);③ **MinHash + LSH 候选生成**(48 维签名 / 24 band,与稀有倒排取并集后**精确校验**):jaccard 调用 15,148 → 6,797,相似度段 210 → 130ms。全量冷建图 **600 → 422ms**,边数与 v0.9.13 一致(tag 2777 / similar 4)。**客户端零改动**:不做精灵/静态层/骨架降级(v0.9.14 那三项因影响手感已回退并 deprecated)。新增 `lib/minhash.js` + `tests/minhash.test.mjs`(4 项),`npm test` 共 **108 项** |
|
package/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { ensureVault, search } from './lib/vault.js'
|
|
|
29
29
|
import { migrateFromMarkdown, setAuditConfig, backupDb } from './lib/db.js'
|
|
30
30
|
import { evaluateStall, MAX_STAMPS } from './lib/stall.js'
|
|
31
31
|
import { resolveVaultDir, currentWorkspace } from './lib/vault-resolve.js'
|
|
32
|
+
import { createHub } from './lib/sse.js'
|
|
32
33
|
import { summarizeTurn, summarizeTurnDetailed, routeCandidates, extractLastTurn, sliceNewEvents, sessionEvents, sessionEventApi, createCaptureHealth, resolveRoute, captureCard, captureUpdate, pickNeighbors } from './lib/capture.js'
|
|
33
34
|
import { createApi, json, encodeBody } from './lib/api.js'
|
|
34
35
|
import { appendCaptureLog, readCaptureLog, rotateCaptureLog } from './lib/capture-log.js'
|
|
@@ -251,6 +252,9 @@ export function bindSettings(ctx, schema, config) {
|
|
|
251
252
|
|
|
252
253
|
export function apply(ctx, config) {
|
|
253
254
|
const settings = bindSettings(ctx, Config, config)
|
|
255
|
+
// SSE:配置变更即时推送到所有已打开的页面(DSH 内嵌页 + 独立 Web 页),不必手动刷新
|
|
256
|
+
const hub = createHub()
|
|
257
|
+
ctx.effect(() => () => hub.close(), 'memory-eternal: sse hub')
|
|
254
258
|
|
|
255
259
|
// 首次激活:自动从 .md 文件迁移到 SQLite(幂等,已有数据则跳过)
|
|
256
260
|
// vault 解析与所有独立进程共用同一套优先级(见 lib/vault-resolve.js):
|
|
@@ -277,6 +281,31 @@ export function apply(ctx, config) {
|
|
|
277
281
|
}
|
|
278
282
|
syncAudit()
|
|
279
283
|
settings.watch(syncAudit)
|
|
284
|
+
// 独立 Web 页保存的配置:它写「待应用」文件,这边应用后删除(DSH 没运行时下次启动生效)
|
|
285
|
+
const drainPending = async () => {
|
|
286
|
+
try {
|
|
287
|
+
const { drainPendingConfig } = await import('./lib/config-sync.js')
|
|
288
|
+
const applied = await drainPendingConfig(process.env, (patch) => settings.update(patch, undefined))
|
|
289
|
+
if (applied) {
|
|
290
|
+
syncConfigFile()
|
|
291
|
+
syncAudit()
|
|
292
|
+
try { console.error('[memory-eternal] 已应用独立 Web 端的配置改动: ' + Object.keys(applied).join(',')) } catch { /* 日志失败无妨 */ }
|
|
293
|
+
}
|
|
294
|
+
} catch { /* 失败保留文件,下一轮重试 */ }
|
|
295
|
+
}
|
|
296
|
+
drainPending()
|
|
297
|
+
// 5 秒轮询只是兜底;真正让它「准即时」的是下面这个文件监听(毫秒级)
|
|
298
|
+
const pendingTimer = setInterval(drainPending, 5000)
|
|
299
|
+
ctx.effect(() => () => clearInterval(pendingTimer), 'memory-eternal: pending config sync')
|
|
300
|
+
ctx.effect(() => {
|
|
301
|
+
let stop = () => {}
|
|
302
|
+
import('./lib/config-sync.js').then(({ watchPendingConfig }) => {
|
|
303
|
+
stop = watchPendingConfig(process.env, () => {
|
|
304
|
+
drainPending().then(() => hub.broadcast('config', { at: Date.now(), source: 'pending' })).catch(() => {})
|
|
305
|
+
})
|
|
306
|
+
}).catch(() => {})
|
|
307
|
+
return () => { try { stop() } catch { /* 已停止 */ } }
|
|
308
|
+
}, 'memory-eternal: pending config watch')
|
|
280
309
|
|
|
281
310
|
// 所有 profile 目录(当前激活 + 其余命名的),供跨库聚合。
|
|
282
311
|
const vaultRoots = () => {
|
|
@@ -763,6 +792,11 @@ export function apply(ctx, config) {
|
|
|
763
792
|
if (pathname === API_PREFIX + '/web-info') {
|
|
764
793
|
return json(res, 200, { ok: true, ...webInfo })
|
|
765
794
|
}
|
|
795
|
+
if (pathname === API_PREFIX + '/events') {
|
|
796
|
+
// SSE 长连接:不 end,保持推送(EventSource 自动重连)
|
|
797
|
+
hub.add(res)
|
|
798
|
+
return
|
|
799
|
+
}
|
|
766
800
|
if (pathname === API_PREFIX + '/setup-run') {
|
|
767
801
|
// 「补全 MCP」:真正执行 runSetup(写外部 agent 配置),返回每项结果,成功/失败可见
|
|
768
802
|
if (req.method !== 'POST') return json(res, 405, { ok: false, error: '需 POST' })
|
|
@@ -833,6 +867,7 @@ export function apply(ctx, config) {
|
|
|
833
867
|
return json(res, 200, { ok: true, config: safe, revision: me?.revision ?? 0, writable: true, readonly: false, schema: me?.schema ?? null, dsh: dshInfo, version: versionRef })
|
|
834
868
|
}
|
|
835
869
|
if (method === 'POST') {
|
|
870
|
+
try {
|
|
836
871
|
const cfgChunks = []
|
|
837
872
|
for await (const chunk of req) cfgChunks.push(chunk)
|
|
838
873
|
const raw = Buffer.concat(cfgChunks).toString('utf8')
|
|
@@ -845,12 +880,16 @@ export function apply(ctx, config) {
|
|
|
845
880
|
const clean = {}
|
|
846
881
|
for (const k of Object.keys(patch)) { if (allowed.has(k)) clean[k] = patch[k] }
|
|
847
882
|
if (Object.keys(clean).length === 0) return json(res, 400, { ok: false, error: '无可写入字段' })
|
|
883
|
+
// 数值字段前置校验:空串/NaN/非数字直接给「哪个字段不合法」,而不是让宿主 schema 抛错
|
|
884
|
+
const badNum = Object.entries(clean).filter(([k, v]) => typeof v === 'string' && v.trim() === '' && !/^(vaultDir|captureProvider|captureModel|recallEmbedding|activeVault)$/.test(k))
|
|
885
|
+
if (badNum.length) return json(res, 400, { ok: false, error: '字段不能为空:' + badNum.map(([k]) => k).join('、'), fields: badNum.map(([k]) => k) })
|
|
848
886
|
if (typeof settings.update === 'function') {
|
|
849
887
|
try {
|
|
850
888
|
const writeResult = await settings.update(clean, expectedRevision)
|
|
851
889
|
// 把完整配置写入共享文件,让独立 web / MCP hook 与 DSH 设置同步(不同步修复)
|
|
852
890
|
syncConfigFile()
|
|
853
891
|
// 回读校验:宿主 volatile 回流可能滞后(issue #12),此时明确告知而不是假装成功
|
|
892
|
+
hub.broadcast('config', { at: Date.now(), source: 'dsh', applied: Object.keys(clean) })
|
|
854
893
|
const now = settings.get() ?? {}
|
|
855
894
|
const pending = Object.keys(clean).filter((k) => JSON.stringify(now[k]) !== JSON.stringify(clean[k]))
|
|
856
895
|
return json(res, 200, {
|
|
@@ -868,6 +907,10 @@ export function apply(ctx, config) {
|
|
|
868
907
|
}
|
|
869
908
|
}
|
|
870
909
|
return json(res, 501, { ok: false, error: '当前环境不支持写配置' })
|
|
910
|
+
} catch (e) {
|
|
911
|
+
// 绝不返回空响应体:客户端拿到空 body 只能显示通用「保存失败」,无法定位(issue #12)
|
|
912
|
+
try { return json(res, 400, { ok: false, error: '配置写入失败:' + String(e?.message || e) }) } catch { return }
|
|
913
|
+
}
|
|
871
914
|
}
|
|
872
915
|
return json(res, 405, { ok: false, error: 'method not allowed' })
|
|
873
916
|
}
|
package/lib/api.js
CHANGED
|
@@ -19,6 +19,12 @@ import { compressExcerpt } from './capture.js'
|
|
|
19
19
|
import { buildDiagnostics } from './feedback.js'
|
|
20
20
|
import { getSetupStatus, runSetup, mcpAgentAction } from './setup.js'
|
|
21
21
|
import { backupDb } from './db.js'
|
|
22
|
+
import { createHub } from './sse.js'
|
|
23
|
+
|
|
24
|
+
// 独立 web 的 SSE 中心:配置/数据变更即时推给已打开的页面(无需手动刷新)
|
|
25
|
+
const hub = createHub()
|
|
26
|
+
/** 供 lib/web.js 在检测到共享配置变化时广播。 */
|
|
27
|
+
export function broadcast(event, data) { return hub.broadcast(event, data) }
|
|
22
28
|
|
|
23
29
|
export const API_PREFIX = '/memory-eternal/api'
|
|
24
30
|
|
|
@@ -365,6 +371,11 @@ export function createApi(deps = {}) {
|
|
|
365
371
|
}
|
|
366
372
|
return
|
|
367
373
|
}
|
|
374
|
+
case '/events': {
|
|
375
|
+
// SSE 长连接(不 end),EventSource 会自动重连
|
|
376
|
+
hub.add(res)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
368
379
|
case '/config': {
|
|
369
380
|
// 独立 web server(无 DSH settings)的配置读取兜底:env + 默认值。DSH host 的 /config 在其 webServer handler 拦截实现(可写),此处只读。
|
|
370
381
|
const cfg = getSettings() ?? {}
|
|
@@ -380,7 +391,49 @@ export function createApi(deps = {}) {
|
|
|
380
391
|
captureModel: cfg.captureModel || '', recallEmbedding: cfg.recallEmbedding || '', sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
|
|
381
392
|
webPort: cfg.webPort ?? 7999, webCheckIntervalMs: cfg.webCheckIntervalMs ?? 5000, webMaxRestart: cfg.webMaxRestart ?? 10,
|
|
382
393
|
}
|
|
383
|
-
|
|
394
|
+
const NUM_RANGE = {
|
|
395
|
+
dedupThreshold: [0, 1], captureMinChars: [0, 1000000], captureCooldownMs: [0, 1000000000],
|
|
396
|
+
maxCardsPerDay: [0, 1000000], captureMaxTokens: [100, 4000], recallMinScore: [0, 50],
|
|
397
|
+
recallLimit: [1, 20], recallSummaryLen: [40, 400], recycleRetentionDays: [1, 3650],
|
|
398
|
+
webPort: [1, 65535], webCheckIntervalMs: [1000, 600000], webMaxRestart: [1, 1000],
|
|
399
|
+
sessionBudgetChars: [0, 1000000000],
|
|
400
|
+
}
|
|
401
|
+
// POST:独立 web 没有 DSH settings 服务,改成写「待应用」文件,由 DSH 端自动应用
|
|
402
|
+
// (否则这里的保存按钮永远是死的 —— issue #12 的一半就栽在这)
|
|
403
|
+
if (req.method === 'POST') {
|
|
404
|
+
const body = await readBody(req)
|
|
405
|
+
const patch = body && typeof body.patch === 'object' && body.patch ? body.patch : null
|
|
406
|
+
if (!patch) return json(res, 400, { ok: false, error: '缺少 patch' })
|
|
407
|
+
const allowed = new Set(Object.keys(safe))
|
|
408
|
+
const clean = {}
|
|
409
|
+
const bad = []
|
|
410
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
411
|
+
if (!allowed.has(k)) continue
|
|
412
|
+
const base = safe[k]
|
|
413
|
+
if (Array.isArray(base)) { if (!Array.isArray(v)) bad.push(k); else clean[k] = v; continue }
|
|
414
|
+
if (typeof base === 'number') {
|
|
415
|
+
const n = typeof v === 'number' ? v : Number(v)
|
|
416
|
+
const rng = NUM_RANGE[k] || [-Infinity, Infinity]
|
|
417
|
+
const lo = rng[0], hi = rng[1]
|
|
418
|
+
if (v === '' || v === null || v === undefined || !Number.isFinite(n)) bad.push(k + ' 需要数字')
|
|
419
|
+
else if (n < lo || n > hi) bad.push(k + ' 需要 ' + lo + '-' + hi + ',当前 ' + n)
|
|
420
|
+
else clean[k] = n
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
if (typeof base === 'boolean') { if (typeof v !== 'boolean') bad.push(k); else clean[k] = v; continue }
|
|
424
|
+
if (typeof v !== 'string') bad.push(k); else clean[k] = v
|
|
425
|
+
}
|
|
426
|
+
if (bad.length) return json(res, 400, { ok: false, error: '字段值不合法:' + bad.join('、'), fields: bad })
|
|
427
|
+
if (!Object.keys(clean).length) return json(res, 400, { ok: false, error: '没有可写入的字段' })
|
|
428
|
+
const { writePendingConfig } = await import('./config-sync.js')
|
|
429
|
+
const out = writePendingConfig(process.env, clean)
|
|
430
|
+
hub.broadcast('config', { at: out.at, source: 'standalone', applied: Object.keys(clean) })
|
|
431
|
+
return json(res, 200, {
|
|
432
|
+
ok: true, applied: Object.keys(clean), pending: Object.keys(clean), retried: false, at: out.at,
|
|
433
|
+
note: '已写入待应用:DSH 端会自动同步(DSH 未运行时下次启动生效)',
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
json(res, 200, { ok: true, config: safe, revision: 0, writable: true, readonly: false, viaPending: true, version: env.MEMORY_ETERNAL_VERSION || '' })
|
|
384
437
|
return
|
|
385
438
|
}
|
|
386
439
|
case '/compress': {
|