memory-eternal 0.9.14 → 0.9.16
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 +34 -34
- package/.codex-plugin/plugin.json +29 -29
- package/.cursor-plugin/plugin.json +28 -28
- package/README.en.md +366 -365
- package/README.md +390 -389
- package/index.js +65 -6
- package/lib/api.js +3 -0
- package/lib/client.js +17 -17
- package/lib/minhash.js +99 -99
- package/lib/vault.js +941 -941
- package/package.json +131 -131
- package/web/app.js +19 -19
package/index.js
CHANGED
|
@@ -179,7 +179,7 @@ function schemaDefaults(schema) {
|
|
|
179
179
|
return {}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
function bindSettings(ctx, schema, config) {
|
|
182
|
+
export function bindSettings(ctx, schema, config) {
|
|
183
183
|
const service = typeof ctx.get === 'function' ? ctx.get('settings') : ctx.settings
|
|
184
184
|
if (service && typeof service.register === 'function') {
|
|
185
185
|
return service.register('memory-eternal', schema, { base: config ?? {} })
|
|
@@ -194,16 +194,57 @@ function bindSettings(ctx, schema, config) {
|
|
|
194
194
|
try { return service.configure({ auto: false }, ctx.fiber) } catch { return () => {} }
|
|
195
195
|
}, 'memory-eternal: settings presentation')
|
|
196
196
|
}
|
|
197
|
+
// 宿主「volatile 回流」可能滞后甚至不回流(issue #12:改了保存不上)。两步兜底:
|
|
198
|
+
// ① 写成功后把 patch 叠加在本地视图上 → 面板重新加载立刻是新值;
|
|
199
|
+
// ② 等宿主快照追上(值相等)后自动摘除,避免长期掩盖宿主的真实状态。
|
|
200
|
+
const overlay = {}
|
|
201
|
+
const snapshotWithoutOverlay = () => (config === undefined || config === null ? fallback : plainConfig(config))
|
|
202
|
+
const pruneOverlay = (snapshot) => {
|
|
203
|
+
for (const key of Object.keys(overlay)) {
|
|
204
|
+
if (snapshot && snapshot[key] !== undefined && JSON.stringify(snapshot[key]) === JSON.stringify(overlay[key])) delete overlay[key]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const readMerged = () => {
|
|
208
|
+
const base = snapshotWithoutOverlay()
|
|
209
|
+
pruneOverlay(base)
|
|
210
|
+
return Object.keys(overlay).length ? { ...base, ...overlay } : base
|
|
211
|
+
}
|
|
197
212
|
return {
|
|
198
|
-
get:
|
|
213
|
+
get: readMerged,
|
|
199
214
|
watch(listener) {
|
|
200
|
-
const handler = () => { try { listener(
|
|
215
|
+
const handler = () => { try { listener(readMerged()) } catch { /* 监听器异常不影响宿主 */ } }
|
|
201
216
|
ctx.on('loader/volatile-update', handler)
|
|
202
217
|
return () => { try { ctx.off('loader/volatile-update', handler) } catch { /* 已卸载 */ } }
|
|
203
218
|
},
|
|
219
|
+
/**
|
|
220
|
+
* 写配置。
|
|
221
|
+
* 宿主 write() 用「describe 返回的 revision 必须完全一致」做乐观并发(dsh-settings
|
|
222
|
+
* SettingsConflictError),而面板持有的 revision 可能已经过期(上一次写入、宿主重建配置等)。
|
|
223
|
+
* 这里遇到冲突就取一次最新 revision 重试一次,而不是把 409 直接抛给用户。
|
|
224
|
+
* @returns {Promise<{revision:number, retried:boolean}>}
|
|
225
|
+
*/
|
|
204
226
|
async update(patch, expectedRevision) {
|
|
205
227
|
if (!service || typeof service.update !== 'function') throw new Error('当前 DSH 版本不支持写配置')
|
|
206
|
-
|
|
228
|
+
const call = (rev) => service.update(entryId, patch, rev)
|
|
229
|
+
let retried = false
|
|
230
|
+
let used = expectedRevision
|
|
231
|
+
try {
|
|
232
|
+
await call(expectedRevision)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
const msg = String((error && error.message) || error)
|
|
235
|
+
if (!/revision|changed since|conflict/i.test(msg)) throw error
|
|
236
|
+
let fresh
|
|
237
|
+
try {
|
|
238
|
+
const list = typeof service.describe === 'function' ? service.describe() : []
|
|
239
|
+
fresh = (list.find((d) => d && d.ns === entryId) || {}).revision
|
|
240
|
+
} catch { fresh = undefined }
|
|
241
|
+
if (fresh === undefined) throw error
|
|
242
|
+
used = fresh
|
|
243
|
+
retried = true
|
|
244
|
+
await call(fresh)
|
|
245
|
+
}
|
|
246
|
+
Object.assign(overlay, patch)
|
|
247
|
+
return { revision: used, retried }
|
|
207
248
|
},
|
|
208
249
|
}
|
|
209
250
|
}
|
|
@@ -766,6 +807,13 @@ export function apply(ctx, config) {
|
|
|
766
807
|
// 多库(#10):配置页可直接编辑
|
|
767
808
|
vaultProfiles: Array.isArray(cfg.vaultProfiles) ? cfg.vaultProfiles : [],
|
|
768
809
|
activeVault: cfg.activeVault || '',
|
|
810
|
+
// 配置覆盖补齐(用户要求「配置按钮全覆盖」):这些以前只能手改配置文件
|
|
811
|
+
enabled: cfg.enabled !== false,
|
|
812
|
+
vaultDir: cfg.vaultDir || '',
|
|
813
|
+
captureProvider: cfg.captureProvider || '',
|
|
814
|
+
captureModel: cfg.captureModel || '',
|
|
815
|
+
recallEmbedding: cfg.recallEmbedding || '',
|
|
816
|
+
sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
|
|
769
817
|
}
|
|
770
818
|
const descriptor = (ctx.get('settings') ?? {}).describe?.({ redactSecrets: true }) ?? []
|
|
771
819
|
const me = descriptor.find((d) => d.ns === 'memory-eternal')
|
|
@@ -799,10 +847,21 @@ export function apply(ctx, config) {
|
|
|
799
847
|
if (Object.keys(clean).length === 0) return json(res, 400, { ok: false, error: '无可写入字段' })
|
|
800
848
|
if (typeof settings.update === 'function') {
|
|
801
849
|
try {
|
|
802
|
-
await settings.update(clean, expectedRevision)
|
|
850
|
+
const writeResult = await settings.update(clean, expectedRevision)
|
|
803
851
|
// 把完整配置写入共享文件,让独立 web / MCP hook 与 DSH 设置同步(不同步修复)
|
|
804
852
|
syncConfigFile()
|
|
805
|
-
|
|
853
|
+
// 回读校验:宿主 volatile 回流可能滞后(issue #12),此时明确告知而不是假装成功
|
|
854
|
+
const now = settings.get() ?? {}
|
|
855
|
+
const pending = Object.keys(clean).filter((k) => JSON.stringify(now[k]) !== JSON.stringify(clean[k]))
|
|
856
|
+
return json(res, 200, {
|
|
857
|
+
ok: true,
|
|
858
|
+
applied: Object.keys(clean),
|
|
859
|
+
pending,
|
|
860
|
+
retried: !!(writeResult && writeResult.retried),
|
|
861
|
+
note: pending.length
|
|
862
|
+
? '已写入;宿主尚未回流这些值(面板已本地生效,重启 DSH 后以配置文件为准)'
|
|
863
|
+
: '已保存。autoWebMode/watchdogAutoSpawn 等需重启 DSH 生效',
|
|
864
|
+
})
|
|
806
865
|
} catch (e) {
|
|
807
866
|
if (e && e.code === 'SETTINGS_CONFLICT') return json(res, 409, { ok: false, error: '配置已被外部修改,请刷新后重试(revision conflict)' })
|
|
808
867
|
return json(res, 500, { ok: false, error: String(e?.message || e) })
|
package/lib/api.js
CHANGED
|
@@ -376,6 +376,9 @@ export function createApi(deps = {}) {
|
|
|
376
376
|
autoWeb: cfg.autoWeb ?? env.MEMORY_AUTO_WEB !== '0', autoWebMode: cfg.autoWebMode || 'init', webPort: Number(cfg.webPort) || Number(env.MEMORY_WEB_PORT) || 7999, webCheckIntervalMs: Number(cfg.webCheckIntervalMs) || 5000, webMaxRestart: Number(cfg.webMaxRestart) || 10, watchdogAutoSpawn: cfg.watchdogAutoSpawn ?? env.MEMORY_WATCHDOG === '1', autoMcpSetup: cfg.autoMcpSetup ?? false,
|
|
377
377
|
auditMode: cfg.auditMode ?? 'all', auditExemptAgents: cfg.auditExemptAgents ?? [], auditExemptKinds: cfg.auditExemptKinds ?? [], recycleRetentionDays: cfg.recycleRetentionDays ?? 30,
|
|
378
378
|
vaultProfiles: Array.isArray(cfg.vaultProfiles) ? cfg.vaultProfiles : [], activeVault: cfg.activeVault || '',
|
|
379
|
+
enabled: cfg.enabled !== false, vaultDir: cfg.vaultDir || '', captureProvider: cfg.captureProvider || '',
|
|
380
|
+
captureModel: cfg.captureModel || '', recallEmbedding: cfg.recallEmbedding || '', sessionBudgetChars: cfg.sessionBudgetChars ?? 80000,
|
|
381
|
+
webPort: cfg.webPort ?? 7999, webCheckIntervalMs: cfg.webCheckIntervalMs ?? 5000, webMaxRestart: cfg.webMaxRestart ?? 10,
|
|
379
382
|
}
|
|
380
383
|
json(res, 200, { ok: true, config: safe, revision: 0, writable: false, readonly: true, version: env.MEMORY_ETERNAL_VERSION || '' })
|
|
381
384
|
return
|