dsh-memu-memory 1.1.2 → 1.1.3
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/AGENTS.md +8 -6
- package/README.md +21 -5
- package/README.zh.md +21 -5
- package/docs/contributing.md +5 -3
- package/docs/env-and-commands.md +13 -0
- package/docs/host-api.md +35 -3
- package/docs/memory-semantics.md +32 -5
- package/docs/troubleshooting.md +36 -7
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +630 -0
- package/lib/client.js +697 -49
- package/lib/curator.js +81 -37
- package/lib/cycle.js +3 -0
- package/lib/embedding-catalog.js +763 -0
- package/lib/embedding-download.js +407 -0
- package/lib/embedding-runtime.js +710 -0
- package/lib/engine.js +2 -2
- package/lib/index.js +36 -0
- package/lib/inject.js +131 -30
- package/lib/local-embedding-entry.js +145 -0
- package/lib/local-embedding-manager.js +249 -0
- package/lib/local-embedding-protocol.js +173 -0
- package/lib/local-embedding-worker.js +652 -0
- package/lib/local-vector-service.js +283 -0
- package/lib/memory.js +385 -45
- package/lib/memu-config.js +10 -22
- package/lib/memu.js +53 -19
- package/lib/paths.js +24 -0
- package/lib/private.js +139 -13
- package/lib/rpc.js +45 -4
- package/lib/state.js +109 -161
- package/lib/tools.js +9 -5
- package/lib/vector-migration.js +878 -0
- package/lib/vector-profile.js +941 -0
- package/package.json +3 -2
- package/scripts/acceptance-local.mjs +5 -4
- package/scripts/chain-matrix.mjs +527 -0
- package/scripts/check-all.mjs +19 -0
- package/scripts/check-doc-drift.mjs +353 -0
- package/scripts/diagnose-bindings.mjs +176 -0
- package/scripts/local-runtime-install.mjs +142 -0
- package/scripts/local-runtime-verify.mjs +56 -0
- package/scripts/local-vector-verify.mjs +93 -0
- package/scripts/recall-bench.mjs +813 -0
- package/scripts/repetition-ab-scenarios.mjs +93 -0
- package/scripts/repetition-detector-snapshot.mjs +314 -0
- package/scripts/repetition-eval-live.mjs +360 -0
- package/scripts/repetition-eval.mjs +702 -0
package/lib/client.js
CHANGED
|
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
const exports = module.exports
|
|
9
9
|
|
|
10
10
|
const NS = 'dsh-memu-memory'
|
|
11
|
-
const SETTINGS_PAGES = ['engine', 'settings', 'folders', 'libraries', 'browser', 'global', 'prompts', 'stats', 'memories']
|
|
11
|
+
const SETTINGS_PAGES = ['engine', 'connection', 'settings', 'folders', 'libraries', 'browser', 'global', 'prompts', 'stats', 'memories', 'logs']
|
|
12
12
|
const EMPTY_SESSIONS = Object.freeze({ byId: Object.freeze({}), ids: Object.freeze([]) })
|
|
13
13
|
const TAB_ID = `${NS}:memory`
|
|
14
14
|
const TAB_KIND = 'dsh-memu-memory'
|
|
@@ -37,11 +37,12 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
// panes therefore cost about one cheap RPC per second — the beat must never
|
|
38
38
|
// grow a payload (AGENTS §7.4).
|
|
39
39
|
const BEAT_MS = 2500
|
|
40
|
-
function useMemoryBeat({ call, markerRef, onBeat }) {
|
|
40
|
+
function useMemoryBeat({ call, markerRef, onBeat, force = false, intervalMs = BEAT_MS }) {
|
|
41
41
|
React.useEffect(() => {
|
|
42
42
|
const id = setInterval(async () => {
|
|
43
43
|
if (typeof document !== "undefined" && document.visibilityState === "hidden") return
|
|
44
44
|
try {
|
|
45
|
+
if (force) { await onBeat(); return }
|
|
45
46
|
const res = await call("getSidebarState", { revisionOnly: true })
|
|
46
47
|
// null means nothing has been painted yet; the mount fetch owns that case.
|
|
47
48
|
if (markerRef.current === null) return
|
|
@@ -51,11 +52,11 @@ window.__ModuleLoader__.load({
|
|
|
51
52
|
const result = await onBeat()
|
|
52
53
|
if (result !== false && markerRef.current === previous) markerRef.current = next
|
|
53
54
|
} catch {}
|
|
54
|
-
},
|
|
55
|
+
}, intervalMs)
|
|
55
56
|
// Never hold the host process open for a poll (matters in tests too).
|
|
56
57
|
if (id && typeof id.unref === "function") id.unref()
|
|
57
58
|
return () => clearInterval(id)
|
|
58
|
-
}, [call, markerRef, onBeat])
|
|
59
|
+
}, [call, markerRef, onBeat, force, intervalMs])
|
|
59
60
|
React.useEffect(() => {
|
|
60
61
|
if (typeof document === "undefined" || typeof document.addEventListener !== "function") return undefined
|
|
61
62
|
const onVisible = () => {
|
|
@@ -113,6 +114,26 @@ window.__ModuleLoader__.load({
|
|
|
113
114
|
reviewConditions: '说明不同适用条件(至少 8 字)',
|
|
114
115
|
pageMemories: 'MemU 记忆',
|
|
115
116
|
descMemories: '跨库记忆、来源与状态',
|
|
117
|
+
pageLogs: '日志',
|
|
118
|
+
descLogs: '召回、写入、整理与向量的活动记录,可筛选、复制、下载',
|
|
119
|
+
logSearch: '搜索 id / 标题 / 错误',
|
|
120
|
+
logRefresh: '刷新',
|
|
121
|
+
logCopyAll: '复制全部',
|
|
122
|
+
logDownload: '下载',
|
|
123
|
+
logCopyOne: '复制',
|
|
124
|
+
logLocate: '定位记忆',
|
|
125
|
+
logEmpty: '没有匹配的日志。',
|
|
126
|
+
logAllKinds: '全部类型',
|
|
127
|
+
logKindRecall: '召回',
|
|
128
|
+
logKindWrite: '写入',
|
|
129
|
+
logKindConsolidate: '整理',
|
|
130
|
+
logKindCurate: '提炼',
|
|
131
|
+
logKindVector: '向量',
|
|
132
|
+
logKindError: '错误',
|
|
133
|
+
logKindSystem: '系统',
|
|
134
|
+
logClear: '清空日志',
|
|
135
|
+
runLogTitle: '整理记录',
|
|
136
|
+
runLogEmpty: '还没有整理记录。',
|
|
116
137
|
allLibraries: '全部记忆库',
|
|
117
138
|
searchMemories: '搜索标题、内容或标签',
|
|
118
139
|
poolReview: '待复核',
|
|
@@ -121,6 +142,8 @@ window.__ModuleLoader__.load({
|
|
|
121
142
|
wallLimit: '仅展示最近 300 条',
|
|
122
143
|
openMemu: '打开 MemU WebUI',
|
|
123
144
|
saved: '已保存',
|
|
145
|
+
copied: '已复制',
|
|
146
|
+
copyFailed: '复制失败,请手动选择文本',
|
|
124
147
|
confirmAction: '确认此操作?',
|
|
125
148
|
relationState: '关系当前状态',
|
|
126
149
|
familiarity: '熟悉度(未知可留空)',
|
|
@@ -198,7 +221,8 @@ window.__ModuleLoader__.load({
|
|
|
198
221
|
mustRememberCap: '永久记忆软上限',
|
|
199
222
|
shortTermTtlHours: '短期记忆保留(小时,0=关闭)',
|
|
200
223
|
anchorTokens: '锚点 token',
|
|
201
|
-
turnTokens: '
|
|
224
|
+
turnTokens: '召回 token',
|
|
225
|
+
injectionTotalTokens: '注入总 token',
|
|
202
226
|
dedupeTurns: '去重轮数',
|
|
203
227
|
globalEveryNTurns: '全局每 N 轮',
|
|
204
228
|
minUserChars: '最短用户消息',
|
|
@@ -212,9 +236,23 @@ window.__ModuleLoader__.load({
|
|
|
212
236
|
memoryModeHint: 'local 把记忆留在本机;cloud 交给 MemU 官方后端,需要 ~/.memu/config.env 里的厂商凭据。',
|
|
213
237
|
memoryBackend: '记忆后端',
|
|
214
238
|
backendAgent: '本地(会话 Agent)',
|
|
215
|
-
backendApi: '
|
|
216
|
-
backendAgentHint: '由当前对话的 Agent
|
|
217
|
-
backendApiHint: '
|
|
239
|
+
backendApi: '启用向量库',
|
|
240
|
+
backendAgentHint: '由当前对话的 Agent 在本地 JSON/索引路径中总结与写入;不启用 MemU、worker 或 embedding。',
|
|
241
|
+
backendApiHint: '启用向量库后,选择本地受管模型或 API embedding;MemU 仍在本地存储与检索。',
|
|
242
|
+
vectorSource: 'Embedding 来源',
|
|
243
|
+
vectorSourceLocal: '本地',
|
|
244
|
+
vectorSourceApi: 'API',
|
|
245
|
+
vectorSourceHint: '本地使用已校验的 loopback embedding worker;API 使用下方 MemU embedding 服务配置。',
|
|
246
|
+
managedVectorStatus: '状态',
|
|
247
|
+
managedVectorDownloading: '下载中',
|
|
248
|
+
managedVectorVerifying: '校验中',
|
|
249
|
+
managedVectorFailed: '失败',
|
|
250
|
+
managedVectorProgress: '下载进度',
|
|
251
|
+
managedVectorCurrent: '当前文件',
|
|
252
|
+
managedVectorRetry: '重试',
|
|
253
|
+
managedVectorRebuilding: '重建中',
|
|
254
|
+
managedVectorGeneration: '代际',
|
|
255
|
+
managedVectorNoCatalog: '模型目录暂不可用,请稍后刷新。',
|
|
218
256
|
engineLatestVersions: '最新版本',
|
|
219
257
|
engineLatestHint: '从 PyPI 主动查询;插件只安装声明的推荐版本,不会自动追最新。',
|
|
220
258
|
engineQueryVersions: '查询最新版本',
|
|
@@ -246,7 +284,8 @@ window.__ModuleLoader__.load({
|
|
|
246
284
|
deleteForever: '删除',
|
|
247
285
|
confirmDelete: '确认删除',
|
|
248
286
|
cancelDelete: '取消',
|
|
249
|
-
descOverview: 'MemU
|
|
287
|
+
descOverview: 'MemU / Python 引擎的安装、修复、版本与备份。',
|
|
288
|
+
descConnection: '选择词法或向量召回;本地向量需先分别下载运行时与模型,再切换重建。',
|
|
250
289
|
descSettings: '注入频率、召回条数、衰减与自动化。',
|
|
251
290
|
descLibraries: '同文件夹的对话自动共用一个记忆库;删除对话不会删除记忆。',
|
|
252
291
|
descBrowser: '按库浏览、手写、修改、归档与导出记忆。',
|
|
@@ -278,6 +317,59 @@ window.__ModuleLoader__.load({
|
|
|
278
317
|
embedSourceLocal: '本地',
|
|
279
318
|
embedSourceRemote: '远程',
|
|
280
319
|
embedSourceLocalHint: '本机的 OpenAI 兼容向量服务(例如 Ollama:{url},模型 {model})。DSH 自身没有 embedding 能力,所以“本地”要求你自己先把该服务跑起来;不需要厂商 API key。',
|
|
320
|
+
managedVector: '受管本地向量模型',
|
|
321
|
+
managedVectorHint: '下载固定制品到插件数据目录;选择只记录待切换模型,切换会创建新向量空间并保留旧空间。',
|
|
322
|
+
managedVectorDownload: '下载并校验',
|
|
323
|
+
managedVectorSelect: '选择待切换',
|
|
324
|
+
managedVectorSwitch: '切换并重建',
|
|
325
|
+
managedVectorRollback: '回滚上一空间',
|
|
326
|
+
managedVectorTest: '测试当前模型',
|
|
327
|
+
managedVectorActive: '当前生效',
|
|
328
|
+
managedVectorRequested: '待切换',
|
|
329
|
+
managedVectorInstalled: '已安装',
|
|
330
|
+
managedVectorPending: '待核验',
|
|
331
|
+
managedVectorUnavailable: '不可用',
|
|
332
|
+
managedVectorSizePending: '大小待核验',
|
|
333
|
+
managedVectorFailure: '切换失败',
|
|
334
|
+
managedVectorRuntimeHint: '没有检测到已核实的本地运行时。点「下载运行时」把目录里 pin 住的制品装进插件数据目录:不需要手动设置 DSH_MEMU_EMBED_RUNTIME_DIR,也不需要另起 Ollama。',
|
|
335
|
+
managedVectorRuntimeMissing: '运行时未安装',
|
|
336
|
+
managedVectorRuntimeInstalling: '运行时安装中',
|
|
337
|
+
managedVectorRuntimeInstalled: '运行时已就绪(受管)',
|
|
338
|
+
managedVectorRuntimeConfigured: '运行时已就绪(环境变量)',
|
|
339
|
+
managedVectorRuntimeFailed: '运行时安装失败',
|
|
340
|
+
managedVectorRuntimeUnsupported: '此平台没有已核实的运行时制品',
|
|
341
|
+
managedVectorRuntimeInstall: '下载运行时',
|
|
342
|
+
managedVectorRuntimeTitle: '运行环境',
|
|
343
|
+
managedVectorModelsTitle: '模型',
|
|
344
|
+
managedVectorReady: '已就绪',
|
|
345
|
+
vectorRebuildTitle: '重建进度',
|
|
346
|
+
vectorStagePreparing: '准备中',
|
|
347
|
+
vectorStagePrepared: '已准备',
|
|
348
|
+
vectorStageSyncing: '写入向量',
|
|
349
|
+
vectorStageSwitching: '切换中',
|
|
350
|
+
vectorStageFailed: '失败',
|
|
351
|
+
vectorStageCommitted: '已切换',
|
|
352
|
+
managedVectorCommitted: '已写入',
|
|
353
|
+
managedVectorSkipped: '跳过',
|
|
354
|
+
managedVectorFailedCount: '失败',
|
|
355
|
+
vectorSwitchDoneHint: '新记忆会自动写入这个向量空间;召回已切到向量路径。',
|
|
356
|
+
vectorAbortedHint: '重建已停止,当前向量空间未改变;再次点击可续建。',
|
|
357
|
+
managedVectorAborted: '已停止',
|
|
358
|
+
vectorRouteLabel: '召回路由',
|
|
359
|
+
vectorRouteLocal: '本地向量',
|
|
360
|
+
vectorRouteRemote: '远程向量',
|
|
361
|
+
vectorRouteLexical: '词法',
|
|
362
|
+
vectorIndexedTag: '已入向量库',
|
|
363
|
+
vectorIndexedMigrated: '已入向量库(迁移)',
|
|
364
|
+
vectorIndexedNow: '当前已入向量库',
|
|
365
|
+
vectorIndexedUnit: '条',
|
|
366
|
+
vectorSwitchWrote: '切换时写入',
|
|
367
|
+
managedVectorConfirmDownload: '确认下载并校验模型制品?运行时需另行下载。',
|
|
368
|
+
managedVectorPrerequisites: '使用顺序:安装 MemU 引擎 → 下载运行时 → 下载并选择已核验模型 → 切换并重建。准备完成前保持词法召回;不需要 Ollama 或手动环境变量。',
|
|
369
|
+
managedVectorSwitchBlocked: '切换前请先下载已核验的运行时,并下载、选择模型。',
|
|
370
|
+
managedVectorConfirmSwitch: '确认切换?这会在新空间重建向量并短暂冻结写入。',
|
|
371
|
+
managedVectorConfirmRollback: '确认回滚到上一向量空间?',
|
|
372
|
+
managedVectorRetention: '备份最多保留 3 份,较旧的完整备份会自动清理。',
|
|
281
373
|
embedSourceRemoteHint: '用你自己填写的 provider / Base URL / API Key 调第三方向量服务。',
|
|
282
374
|
embedKeyNotNeeded: '本地向量服务无需 API Key',
|
|
283
375
|
embedKeyRemoteHint: '只有远程向量服务需要填写 API Key',
|
|
@@ -299,7 +391,8 @@ window.__ModuleLoader__.load({
|
|
|
299
391
|
testOk: '测试通过',
|
|
300
392
|
configPath: '配置文件',
|
|
301
393
|
configSaved: '已保存',
|
|
302
|
-
pageOverview: '
|
|
394
|
+
pageOverview: 'MemU 引擎',
|
|
395
|
+
pageConnection: '连接与向量',
|
|
303
396
|
pageSettings: '记忆设置',
|
|
304
397
|
pageLibraries: '记忆库',
|
|
305
398
|
pageGlobal: '全局记忆',
|
|
@@ -478,6 +571,7 @@ window.__ModuleLoader__.load({
|
|
|
478
571
|
privateMemory: '私有记忆',
|
|
479
572
|
currentLibrary: '当前记忆库',
|
|
480
573
|
domain: '领域',
|
|
574
|
+
domainPromptHint: '决定这次对话的提示词与记忆过滤;写代码请选 Coding,角色扮演选 RP。',
|
|
481
575
|
usePrivate: '使用私有记忆',
|
|
482
576
|
useGlobal: '使用全局记忆',
|
|
483
577
|
organize: '整理这次对话',
|
|
@@ -526,6 +620,26 @@ window.__ModuleLoader__.load({
|
|
|
526
620
|
reviewConditions: 'Explain distinct conditions (at least 8 characters)',
|
|
527
621
|
pageMemories: 'MemU Memory',
|
|
528
622
|
descMemories: 'Memories, provenance and status across libraries',
|
|
623
|
+
pageLogs: 'Logs',
|
|
624
|
+
descLogs: 'Recall, writes, consolidation and vector activity; filter, copy or download',
|
|
625
|
+
logSearch: 'Search id / title / error',
|
|
626
|
+
logRefresh: 'Refresh',
|
|
627
|
+
logCopyAll: 'Copy all',
|
|
628
|
+
logDownload: 'Download',
|
|
629
|
+
logCopyOne: 'Copy',
|
|
630
|
+
logLocate: 'Open memory',
|
|
631
|
+
logEmpty: 'No matching log entries.',
|
|
632
|
+
logAllKinds: 'All kinds',
|
|
633
|
+
logKindRecall: 'Recall',
|
|
634
|
+
logKindWrite: 'Write',
|
|
635
|
+
logKindConsolidate: 'Consolidate',
|
|
636
|
+
logKindCurate: 'Curate',
|
|
637
|
+
logKindVector: 'Vector',
|
|
638
|
+
logKindError: 'Error',
|
|
639
|
+
logKindSystem: 'System',
|
|
640
|
+
logClear: 'Clear log',
|
|
641
|
+
runLogTitle: 'Runs',
|
|
642
|
+
runLogEmpty: 'No runs recorded yet.',
|
|
529
643
|
allLibraries: 'All libraries',
|
|
530
644
|
searchMemories: 'Search titles, content or tags',
|
|
531
645
|
poolReview: 'Review',
|
|
@@ -534,6 +648,8 @@ window.__ModuleLoader__.load({
|
|
|
534
648
|
wallLimit: 'Showing the latest 300 entries',
|
|
535
649
|
openMemu: 'Open MemU WebUI',
|
|
536
650
|
saved: 'Saved',
|
|
651
|
+
copied: 'Copied',
|
|
652
|
+
copyFailed: 'Copy failed; select the text manually',
|
|
537
653
|
confirmAction: 'Confirm this action?',
|
|
538
654
|
relationState: 'Current relationship',
|
|
539
655
|
familiarity: 'Familiarity (blank = unknown)',
|
|
@@ -611,7 +727,8 @@ window.__ModuleLoader__.load({
|
|
|
611
727
|
mustRememberCap: 'Permanent memory cap',
|
|
612
728
|
shortTermTtlHours: 'Short-term TTL in hours (0=off)',
|
|
613
729
|
anchorTokens: 'Anchor tokens',
|
|
614
|
-
turnTokens: '
|
|
730
|
+
turnTokens: 'Recall tokens',
|
|
731
|
+
injectionTotalTokens: 'Injection total tokens',
|
|
615
732
|
dedupeTurns: 'Dedupe turns',
|
|
616
733
|
globalEveryNTurns: 'Global every N turns',
|
|
617
734
|
minUserChars: 'Min user chars',
|
|
@@ -625,9 +742,23 @@ window.__ModuleLoader__.load({
|
|
|
625
742
|
memoryModeHint: 'local keeps memory on this machine; cloud hands storage to the MemU backend and needs vendor credentials in ~/.memu/config.env.',
|
|
626
743
|
memoryBackend: 'Memory backend',
|
|
627
744
|
backendAgent: 'Local (session agent)',
|
|
628
|
-
backendApi: '
|
|
629
|
-
backendAgentHint: 'The
|
|
630
|
-
backendApiHint: '
|
|
745
|
+
backendApi: 'Enable vector store',
|
|
746
|
+
backendAgentHint: 'The session agent summarizes and writes through the local JSON/index path; no MemU, worker, or embedding is enabled.',
|
|
747
|
+
backendApiHint: 'Enable the vector store, then choose a managed local model or API embedding; MemU storage and retrieval remain local.',
|
|
748
|
+
vectorSource: 'Embedding source',
|
|
749
|
+
vectorSourceLocal: 'Local',
|
|
750
|
+
vectorSourceApi: 'API',
|
|
751
|
+
vectorSourceHint: 'Local uses the verified loopback embedding worker; API uses the MemU embedding service configured below.',
|
|
752
|
+
managedVectorStatus: 'Status',
|
|
753
|
+
managedVectorDownloading: 'Downloading',
|
|
754
|
+
managedVectorVerifying: 'Verifying',
|
|
755
|
+
managedVectorFailed: 'Failed',
|
|
756
|
+
managedVectorProgress: 'Download progress',
|
|
757
|
+
managedVectorCurrent: 'Current file',
|
|
758
|
+
managedVectorRetry: 'Retry',
|
|
759
|
+
managedVectorRebuilding: 'Rebuilding',
|
|
760
|
+
managedVectorGeneration: 'Generation',
|
|
761
|
+
managedVectorNoCatalog: 'The model catalog is temporarily unavailable. Refresh and try again.',
|
|
631
762
|
engineLatestVersions: 'Latest versions',
|
|
632
763
|
engineLatestHint: 'Queried from PyPI. The plugin still installs only its declared recommended version.',
|
|
633
764
|
engineQueryVersions: 'Check latest',
|
|
@@ -659,7 +790,8 @@ window.__ModuleLoader__.load({
|
|
|
659
790
|
deleteForever: 'Delete',
|
|
660
791
|
confirmDelete: 'Delete for good',
|
|
661
792
|
cancelDelete: 'Cancel',
|
|
662
|
-
descOverview: 'MemU engine
|
|
793
|
+
descOverview: 'MemU / Python engine installation, repair, versions, and backups.',
|
|
794
|
+
descConnection: 'Choose lexical or vector recall. Local vectors require separate runtime and model downloads before switching and rebuilding.',
|
|
663
795
|
descSettings: 'Injection cadence, recall depth, decay, and automation.',
|
|
664
796
|
descLibraries: 'Conversations in the same folder share one library; deleting a conversation never deletes memory.',
|
|
665
797
|
descBrowser: 'Browse, write, edit, archive, and export memories per library.',
|
|
@@ -691,6 +823,59 @@ window.__ModuleLoader__.load({
|
|
|
691
823
|
embedSourceLocal: 'Local',
|
|
692
824
|
embedSourceRemote: 'Remote',
|
|
693
825
|
embedSourceLocalHint: 'An OpenAI-compatible embedding server on this machine (for example Ollama: {url}, model {model}). DSH has no embedding capability of its own, so "local" still requires you to run that service; no vendor API key is needed.',
|
|
826
|
+
managedVector: 'Managed local vector model',
|
|
827
|
+
managedVectorHint: 'Download pinned artifacts into plugin data; selection only records a pending profile. Switching rebuilds a new vector space and keeps the old one.',
|
|
828
|
+
managedVectorDownload: 'Download and verify',
|
|
829
|
+
managedVectorSelect: 'Select pending',
|
|
830
|
+
managedVectorSwitch: 'Switch and rebuild',
|
|
831
|
+
managedVectorRollback: 'Rollback previous space',
|
|
832
|
+
managedVectorTest: 'Test active model',
|
|
833
|
+
managedVectorActive: 'Active',
|
|
834
|
+
managedVectorRequested: 'Pending switch',
|
|
835
|
+
managedVectorInstalled: 'Installed',
|
|
836
|
+
managedVectorPending: 'Pending verification',
|
|
837
|
+
managedVectorUnavailable: 'Unavailable',
|
|
838
|
+
managedVectorSizePending: 'Size pending verification',
|
|
839
|
+
managedVectorFailure: 'Switch failed',
|
|
840
|
+
managedVectorRuntimeHint: 'No verified local runtime was found. Download runtime fetches the pinned artifacts from the catalog into plugin data; no DSH_MEMU_EMBED_RUNTIME_DIR and no Ollama needed.',
|
|
841
|
+
managedVectorRuntimeMissing: 'Runtime not installed',
|
|
842
|
+
managedVectorRuntimeInstalling: 'Installing runtime',
|
|
843
|
+
managedVectorRuntimeInstalled: 'Runtime ready (managed)',
|
|
844
|
+
managedVectorRuntimeConfigured: 'Runtime ready (environment variable)',
|
|
845
|
+
managedVectorRuntimeFailed: 'Runtime install failed',
|
|
846
|
+
managedVectorRuntimeUnsupported: 'No verified runtime artifacts for this platform',
|
|
847
|
+
managedVectorRuntimeInstall: 'Download runtime',
|
|
848
|
+
managedVectorRuntimeTitle: 'Runtime',
|
|
849
|
+
managedVectorModelsTitle: 'Models',
|
|
850
|
+
managedVectorReady: 'Ready',
|
|
851
|
+
vectorRebuildTitle: 'Rebuild progress',
|
|
852
|
+
vectorStagePreparing: 'Preparing',
|
|
853
|
+
vectorStagePrepared: 'Prepared',
|
|
854
|
+
vectorStageSyncing: 'Writing vectors',
|
|
855
|
+
vectorStageSwitching: 'Switching',
|
|
856
|
+
vectorStageFailed: 'Failed',
|
|
857
|
+
vectorStageCommitted: 'Switched',
|
|
858
|
+
managedVectorCommitted: 'Written',
|
|
859
|
+
managedVectorSkipped: 'Skipped',
|
|
860
|
+
managedVectorFailedCount: 'Failed',
|
|
861
|
+
vectorSwitchDoneHint: 'New memories are written into this vector space; recall now uses it.',
|
|
862
|
+
vectorAbortedHint: 'The rebuild stopped; the active space is unchanged. Retry to resume.',
|
|
863
|
+
managedVectorAborted: 'Stopped',
|
|
864
|
+
vectorRouteLabel: 'Recall route',
|
|
865
|
+
vectorRouteLocal: 'Local vectors',
|
|
866
|
+
vectorRouteRemote: 'Remote vectors',
|
|
867
|
+
vectorRouteLexical: 'Lexical',
|
|
868
|
+
vectorIndexedTag: 'In vector store',
|
|
869
|
+
vectorIndexedMigrated: 'In vector store (from rebuild)',
|
|
870
|
+
vectorIndexedNow: 'Currently indexed',
|
|
871
|
+
vectorIndexedUnit: 'items',
|
|
872
|
+
vectorSwitchWrote: 'Written during switch',
|
|
873
|
+
managedVectorConfirmDownload: 'Download and verify this model artifact? Download the runtime separately.',
|
|
874
|
+
managedVectorPrerequisites: 'Install the MemU engine → download the runtime → download and select a verified model → switch and rebuild. Keep lexical recall until ready; no Ollama or manual environment variable is needed.',
|
|
875
|
+
managedVectorSwitchBlocked: 'Download a verified runtime, then download and select a model before switching.',
|
|
876
|
+
managedVectorConfirmSwitch: 'Switch now? This rebuilds a new vector space and briefly freezes writes.',
|
|
877
|
+
managedVectorConfirmRollback: 'Roll back to the previous vector space?',
|
|
878
|
+
managedVectorRetention: 'At most 3 complete backups are kept; older complete backups are cleaned up automatically.',
|
|
694
879
|
embedSourceRemoteHint: 'Call a third-party embedding service with the provider / Base URL / API Key you enter.',
|
|
695
880
|
embedKeyNotNeeded: 'No API key needed for a local embedding server',
|
|
696
881
|
embedKeyRemoteHint: 'Only remote embedding services require an API key',
|
|
@@ -712,7 +897,8 @@ window.__ModuleLoader__.load({
|
|
|
712
897
|
testOk: 'Test passed',
|
|
713
898
|
configPath: 'Config file',
|
|
714
899
|
configSaved: 'Saved',
|
|
715
|
-
pageOverview: '
|
|
900
|
+
pageOverview: 'MemU engine',
|
|
901
|
+
pageConnection: 'Connection & vectors',
|
|
716
902
|
pageSettings: 'Memory settings',
|
|
717
903
|
pageLibraries: 'Libraries',
|
|
718
904
|
pageGlobal: 'Global memory',
|
|
@@ -890,6 +1076,7 @@ window.__ModuleLoader__.load({
|
|
|
890
1076
|
privateMemory: 'Private memory',
|
|
891
1077
|
currentLibrary: 'Current library',
|
|
892
1078
|
domain: 'Domain',
|
|
1079
|
+
domainPromptHint: 'Chooses this conversation\'s prompt variant and memory filter. Pick Coding for engineering work, RP for roleplay.',
|
|
893
1080
|
usePrivate: 'Use private memory',
|
|
894
1081
|
useGlobal: 'Use global memory',
|
|
895
1082
|
organize: 'Organize this conversation',
|
|
@@ -1138,6 +1325,35 @@ window.__ModuleLoader__.load({
|
|
|
1138
1325
|
.dshm-card-ico{width:32px;height:32px;flex:none;border-radius:10px;display:grid;place-items:center;color:var(--dsw-alias-brand-primary,#2563eb);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2563eb) 12%,transparent)}
|
|
1139
1326
|
.dshm-card-title{font-size:13.5px;font-weight:600;line-height:1.3}
|
|
1140
1327
|
.dshm-card-sub{margin-top:3px;font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.8));max-width:60ch}
|
|
1328
|
+
/* 连接与向量:大选择卡 + 分步卡片 + 一个显眼的主操作。原来把切换按钮塞在标题栏里,
|
|
1329
|
+
三个小按钮和状态标签挤在一行,既不好点也看不出主次。 */
|
|
1330
|
+
.dshm-choice-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px;margin-top:10px}
|
|
1331
|
+
.dshm-choice{position:relative;display:flex;flex-direction:column;gap:5px;padding:14px 16px;border:1px solid var(--dsw-alias-border-l1,#dfe1e6);border-radius:14px;background:var(--dsw-alias-bg-layer-1,rgba(127,127,127,.04));color:inherit;font:inherit;text-align:left;cursor:pointer;transition:border-color .18s ease,box-shadow .18s ease,background .18s ease,transform .18s ease}
|
|
1332
|
+
.dshm-choice:hover:not(:disabled){transform:translateY(-1px);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2563eb) 42%,transparent)}
|
|
1333
|
+
.dshm-choice.on{border-color:var(--dsw-alias-brand-primary,#2563eb);background:color-mix(in srgb,var(--dsw-alias-brand-primary,#2563eb) 8%,transparent);box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary,#2563eb)}
|
|
1334
|
+
.dshm-choice:disabled{opacity:.6;cursor:not-allowed}
|
|
1335
|
+
.dshm-choice-top{display:flex;align-items:center;gap:8px}
|
|
1336
|
+
.dshm-choice-title{font-size:13.5px;font-weight:650;line-height:1.35}
|
|
1337
|
+
.dshm-choice-hint{font-size:11.5px;line-height:1.5;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.8))}
|
|
1338
|
+
.dshm-choice-mark{width:18px;height:18px;flex:none;border-radius:50%;display:grid;place-items:center;font-size:11px;font-weight:700;color:var(--dsw-alias-label-primary-inverted,#fff);background:var(--dsw-alias-brand-primary,#2563eb)}
|
|
1339
|
+
.dshm-step{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));border-radius:14px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,rgba(127,127,127,.04));display:flex;flex-direction:column;gap:8px}
|
|
1340
|
+
.dshm-step-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
1341
|
+
.dshm-step-title{font-size:12.5px;font-weight:650;line-height:1.35}
|
|
1342
|
+
.dshm-step-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
1343
|
+
.dshm-model-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:10px}
|
|
1344
|
+
.dshm-model{border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.16));border-radius:14px;padding:14px 16px;background:var(--dsw-alias-bg-layer-1,rgba(127,127,127,.04));display:flex;flex-direction:column;gap:8px}
|
|
1345
|
+
.dshm-model-meta{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:5px}
|
|
1346
|
+
.dshm-primary-lg{min-height:38px;padding:0 20px;font-size:13px;border-radius:11px}
|
|
1347
|
+
.dshm-cta-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:10px}
|
|
1348
|
+
.dshm-rebuild{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary,#2563eb) 28%,transparent)}
|
|
1349
|
+
.dshm-rebuild-bar{height:10px;border-radius:999px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14));overflow:hidden}
|
|
1350
|
+
.dshm-rebuild-bar>span{display:block;height:100%;border-radius:999px;background:var(--dsw-alias-brand-primary,#2563eb);transition:width .32s ease}
|
|
1351
|
+
.dshm-rebuild-meta{display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:11.5px;color:var(--dsw-alias-label-secondary,rgba(127,127,127,.8))}
|
|
1352
|
+
.dshm-rebuild .dshm-vector-progress{display:flex;flex-direction:column;gap:6px}
|
|
1353
|
+
.dshm-rebuild progress{width:100%;height:12px;border-radius:999px;appearance:none;-webkit-appearance:none;border:0;overflow:hidden;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14))}
|
|
1354
|
+
.dshm-rebuild progress::-webkit-progress-bar{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14));border-radius:999px}
|
|
1355
|
+
.dshm-rebuild progress::-webkit-progress-value{background:var(--dsw-alias-brand-primary,#2563eb);border-radius:999px;transition:width .3s ease}
|
|
1356
|
+
.dshm-rebuild progress::-moz-progress-bar{background:var(--dsw-alias-brand-primary,#2563eb);border-radius:999px}
|
|
1141
1357
|
.dshm-card-actions{margin-left:auto;display:flex;gap:8px;flex:none;align-items:center}
|
|
1142
1358
|
|
|
1143
1359
|
.dshm-root input:not([type=checkbox]):not([type=radio]):focus,.dshm-root textarea:focus,.dshm-root select:focus{border-color:var(--dsw-alias-brand-primary,#2563eb);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary,#2563eb) 18%,transparent)}
|
|
@@ -1934,7 +2150,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
1934
2150
|
item.attachmentCount > 1 ? h('span', { className: 'dshm-wall-cover-count' }, '▧ ' + item.attachmentCount) : null)
|
|
1935
2151
|
}
|
|
1936
2152
|
|
|
1937
|
-
function MemoryWallPanel({ call, libraries, revision, stamp, onOpen, t }) {
|
|
2153
|
+
function MemoryWallPanel({ call, libraries, revision, stamp, onOpen, t, indexed = null }) {
|
|
1938
2154
|
const [data, setData] = React.useState({ items: [], total: 0, errors: [] })
|
|
1939
2155
|
const [libraryId, setLibraryId] = React.useState('')
|
|
1940
2156
|
const [view, setView] = React.useState('memories')
|
|
@@ -2139,6 +2355,15 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2139
2355
|
h(Tag, { tone: shown.source === 'imagined' ? 'warn' : (shown.source === 'user_said' ? 'info' : undefined) },
|
|
2140
2356
|
enumText(t, SOURCE_LABELS, shown.source)),
|
|
2141
2357
|
h(Tag, null, enumText(t, STATUS_LABELS, shown.status)),
|
|
2358
|
+
// 这条记忆有没有进向量库,直接写在条目上(remember 提交成功后落盘)。
|
|
2359
|
+
shown.vector
|
|
2360
|
+
? h(Tag, { tone: 'info' }, `${t('vectorIndexedTag')}${shown.vector.model ? ' · ' + shown.vector.model : ''}${shown.vector.generation ? ' · g' + shown.vector.generation : ''}`)
|
|
2361
|
+
: (() => {
|
|
2362
|
+
// 迁移写库不碰索引,老条目没有 entry.vector;用账本判断它是否已入当前代际。
|
|
2363
|
+
const bucketKey = detail.item.libraryId || 'global'
|
|
2364
|
+
const ids = indexed && Array.isArray(indexed[bucketKey]) ? indexed[bucketKey] : null
|
|
2365
|
+
return ids && ids.includes(detail.item.id) ? h(Tag, { tone: 'info' }, t('vectorIndexedMigrated')) : null
|
|
2366
|
+
})(),
|
|
2142
2367
|
shown.openLoop ? h(Tag, null, t('openLoopTag')) : null,
|
|
2143
2368
|
shown.marginalized ? h(Tag, null, t('marginalizedTag')) : null)),
|
|
2144
2369
|
h('button', { type: 'button', className: 'dshm-x', 'aria-label': t('close'), onClick: () => setDetail(null) }, '×')),
|
|
@@ -2240,14 +2465,15 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2240
2465
|
function SettingsPage({ ctx, t, embedded, standalone = false }) {
|
|
2241
2466
|
const PAGE = {
|
|
2242
2467
|
overview: 0,
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2468
|
+
connection: 1,
|
|
2469
|
+
settings: 2,
|
|
2470
|
+
folders: 3,
|
|
2471
|
+
libraries: 4,
|
|
2472
|
+
browser: 5,
|
|
2473
|
+
global: 6,
|
|
2474
|
+
prompts: 7,
|
|
2475
|
+
stats: 8,
|
|
2476
|
+
memories: 9,
|
|
2251
2477
|
}
|
|
2252
2478
|
const call = useRpc(ctx)
|
|
2253
2479
|
const [state, setState] = React.useState(null)
|
|
@@ -2261,6 +2487,8 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2261
2487
|
const [job, setJob] = React.useState(null)
|
|
2262
2488
|
const [error, setError] = React.useState('')
|
|
2263
2489
|
const [busy, setBusy] = React.useState(false)
|
|
2490
|
+
const [vectorOperation, setVectorOperation] = React.useState('')
|
|
2491
|
+
const [vectorError, setVectorError] = React.useState('')
|
|
2264
2492
|
// Success messages need their own slot: reusing `error` would paint a good
|
|
2265
2493
|
// outcome in red and make the next real failure easy to miss.
|
|
2266
2494
|
const [notice, setNotice] = React.useState('')
|
|
@@ -2269,9 +2497,30 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2269
2497
|
const [configNotice, setConfigNotice] = React.useState('')
|
|
2270
2498
|
const [page, setPage] = React.useState(() => standalone
|
|
2271
2499
|
? (SETTINGS_PAGES.includes(String(window.location.hash || '').replace(/^#\/settings\//, ''))
|
|
2272
|
-
? SETTINGS_PAGES.indexOf(String(window.location.hash || '').replace(/^#\/settings\//, '')) :
|
|
2500
|
+
? SETTINGS_PAGES.indexOf(String(window.location.hash || '').replace(/^#\/settings\//, '')) : PAGE.memories)
|
|
2273
2501
|
: 0)
|
|
2274
2502
|
const pendingSettings = React.useRef({})
|
|
2503
|
+
// Hook order is a contract: every hook below must run on every render,
|
|
2504
|
+
// including the loading branch (`if (!state) return ...` further down).
|
|
2505
|
+
// Putting these next to the other state declarations is what keeps the
|
|
2506
|
+
// count stable — a hook after that early return is React error #310.
|
|
2507
|
+
const [logKind, setLogKind] = React.useState('')
|
|
2508
|
+
const [logQueryInput, setLogQueryInput] = React.useState('')
|
|
2509
|
+
const [logQuery, setLogQuery] = React.useState('')
|
|
2510
|
+
const [logRows, setLogRows] = React.useState([])
|
|
2511
|
+
const [logStats, setLogStats] = React.useState(null)
|
|
2512
|
+
const [logBusy, setLogBusy] = React.useState(false)
|
|
2513
|
+
const [logNotice, setLogNotice] = React.useState('')
|
|
2514
|
+
const loadLogs = React.useCallback(async () => {
|
|
2515
|
+
setLogBusy(true)
|
|
2516
|
+
try {
|
|
2517
|
+
const data = await call('getActivityLog', { limit: 300, kind: logKind || undefined, q: logQuery || undefined })
|
|
2518
|
+
setLogRows(Array.isArray(data?.events) ? data.events : [])
|
|
2519
|
+
setLogStats(data?.stats || null)
|
|
2520
|
+
setLogNotice('')
|
|
2521
|
+
} catch (error) { setLogNotice(String(error?.message || error)) } finally { setLogBusy(false) }
|
|
2522
|
+
}, [call, logKind, logQuery])
|
|
2523
|
+
React.useEffect(() => { void loadLogs() }, [loadLogs])
|
|
2275
2524
|
const settingsTimer = React.useRef(null)
|
|
2276
2525
|
const saveChain = React.useRef(Promise.resolve())
|
|
2277
2526
|
const [resetArmed, setResetArmed] = React.useState(false)
|
|
@@ -2550,7 +2799,8 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2550
2799
|
}, [refresh])
|
|
2551
2800
|
// Writes we cannot hear about over the channel — a tool call, a curator job,
|
|
2552
2801
|
// another host process — land through the on-disk half of the marker.
|
|
2553
|
-
|
|
2802
|
+
const vectorWorking = !!vectorOperation || ['downloading', 'verifying', 'installing'].includes(state?.localVector?.runtime?.status || '') || Object.values(state?.localVector?.installations || {}).some(item => ['downloading', 'verifying'].includes(item.status)) || ['preparing', 'prepared', 'syncing', 'switching'].includes(state?.localVector?.vector?.pending?.status)
|
|
2803
|
+
useMemoryBeat({ call, markerRef, onBeat: refresh, force: vectorWorking })
|
|
2554
2804
|
// Sizes need a directory walk; ask for them after the page has painted.
|
|
2555
2805
|
React.useEffect(() => {
|
|
2556
2806
|
let alive = true
|
|
@@ -2997,7 +3247,12 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2997
3247
|
const memoryBackend = settings.memoryBackend === 'api' ? 'api' : 'agent'
|
|
2998
3248
|
const latestVersions = (versionInfo?.versions || []).slice(0, 3)
|
|
2999
3249
|
const memuConfig = state.memuConfig || { providers: [] }
|
|
3000
|
-
const
|
|
3250
|
+
const localVector = state.localVector || { catalog: [], installations: {}, vector: {} }
|
|
3251
|
+
const requestedModel = localVector.vector?.requested?.catalogId
|
|
3252
|
+
const requestedInstall = localVector.installations?.[requestedModel] || {}
|
|
3253
|
+
const vectorCanSwitch = localVector.runtime?.ready === true && requestedInstall.installed === true && requestedInstall.fingerprintMatches !== false
|
|
3254
|
+
const vectorSource = settings.vectorSource || memuConfig.source || 'local'
|
|
3255
|
+
const isLocalEmbed = vectorSource === 'local'
|
|
3001
3256
|
const draft = configDraft || {
|
|
3002
3257
|
provider: memuConfig.provider || 'openai',
|
|
3003
3258
|
model: memuConfig.model || '',
|
|
@@ -3054,7 +3309,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3054
3309
|
} catch (e) { setError(String(e?.message || e)) } finally { setConfigBusy(false) }
|
|
3055
3310
|
}
|
|
3056
3311
|
const setEmbedSource = async (source) => {
|
|
3057
|
-
if (source ===
|
|
3312
|
+
if (source === vectorSource) return
|
|
3058
3313
|
setConfigBusy(true)
|
|
3059
3314
|
setConfigNotice('')
|
|
3060
3315
|
try {
|
|
@@ -3066,6 +3321,63 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3066
3321
|
await refresh()
|
|
3067
3322
|
} catch (e) { setError(String(e?.message || e)) } finally { setConfigBusy(false) }
|
|
3068
3323
|
}
|
|
3324
|
+
const managedVectorAction = async (endpoint, payload = {}) => {
|
|
3325
|
+
const labels = {
|
|
3326
|
+
downloadLocalVectorModel: 'managedVectorDownload',
|
|
3327
|
+
selectLocalVectorModel: 'managedVectorSelect',
|
|
3328
|
+
switchLocalVectorProfile: 'managedVectorSwitch',
|
|
3329
|
+
rollbackLocalVectorProfile: 'managedVectorRollback',
|
|
3330
|
+
installLocalVectorRuntime: 'managedVectorRuntimeInstall',
|
|
3331
|
+
}
|
|
3332
|
+
setBusy(true)
|
|
3333
|
+
setVectorOperation(labels[endpoint] || endpoint)
|
|
3334
|
+
setError('')
|
|
3335
|
+
setNotice('')
|
|
3336
|
+
setVectorError('')
|
|
3337
|
+
try {
|
|
3338
|
+
await call(endpoint, payload)
|
|
3339
|
+
setNotice(`${t('managedVectorStatus')}: ${t(labels[endpoint] || 'managedVectorStatus')}`)
|
|
3340
|
+
} catch (e) {
|
|
3341
|
+
const message = String(e?.message || e)
|
|
3342
|
+
setVectorError(message)
|
|
3343
|
+
setError(message)
|
|
3344
|
+
} finally {
|
|
3345
|
+
await refresh()
|
|
3346
|
+
setBusy(false)
|
|
3347
|
+
setVectorOperation('')
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
const managedVectorDownload = async (id) => {
|
|
3351
|
+
if (!confirmAction(`download:${id}`)) return
|
|
3352
|
+
await managedVectorAction('downloadLocalVectorModel', { id, confirmed: true })
|
|
3353
|
+
}
|
|
3354
|
+
const managedVectorSelect = async (id) => managedVectorAction('selectLocalVectorModel', { id })
|
|
3355
|
+
const managedVectorSwitch = async () => {
|
|
3356
|
+
if (!vectorCanSwitch) { setVectorError(t('managedVectorSwitchBlocked')); return }
|
|
3357
|
+
if (!confirmAction('switch')) return
|
|
3358
|
+
await managedVectorAction('switchLocalVectorProfile', { confirmed: true })
|
|
3359
|
+
}
|
|
3360
|
+
const managedVectorRollback = async () => {
|
|
3361
|
+
if (!confirmAction('rollback')) return
|
|
3362
|
+
await managedVectorAction('rollbackLocalVectorProfile', { confirmed: true })
|
|
3363
|
+
}
|
|
3364
|
+
const managedVectorRuntimeInstall = async () => {
|
|
3365
|
+
if (!confirmAction('runtime')) return
|
|
3366
|
+
await managedVectorAction('installLocalVectorRuntime', { confirmed: true })
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
const renderVectorProgress = (progress, migration = false) => {
|
|
3370
|
+
if (!progress) return null
|
|
3371
|
+
const total = Number(migration ? progress.total : progress.totalBytes) || 0
|
|
3372
|
+
const done = Number(migration ? progress.completed : progress.bytes) || 0
|
|
3373
|
+
const label = t(migration ? 'managedVectorRebuilding' : 'managedVectorProgress')
|
|
3374
|
+
const current = migration ? progress.current?.itemId : progress.current
|
|
3375
|
+
return h('div', { className: 'dshm-vector-progress' },
|
|
3376
|
+
h('div', { role: 'status', 'aria-live': 'polite' }, `${label}: ${done} / ${total}${migration ? ` · ${t('managedVectorGeneration')} ${progress.generation} · ${progress.stage}` : ' bytes'}`),
|
|
3377
|
+
h('progress', { max: total || 1, value: total > 0 ? Math.min(done, total) : undefined, 'aria-label': label }),
|
|
3378
|
+
current ? h('div', { style: S.muted }, `${t('managedVectorCurrent')}: ${current}`) : null)
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3069
3381
|
const engineStatusText = engine.status === 'ready' ? t('engineReady')
|
|
3070
3382
|
: engine.status === 'installing' ? t('engineInstalling')
|
|
3071
3383
|
: engine.status === 'error' ? t('engineError') : t('engineIdle')
|
|
@@ -3224,15 +3536,12 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3224
3536
|
style: { ...S.ghost, color: V.danger },
|
|
3225
3537
|
}, actionLabel('backup:' + b.name, t('backupDelete'))))))))),
|
|
3226
3538
|
|
|
3539
|
+
)
|
|
3540
|
+
|
|
3541
|
+
const connectionBlock = h(React.Fragment, null,
|
|
3227
3542
|
h(Section, {
|
|
3228
3543
|
title: t('memoryBackend'),
|
|
3229
3544
|
desc: memoryBackend === 'api' ? t('backendApiHint') : t('backendAgentHint'),
|
|
3230
|
-
right: memoryBackend === 'api'
|
|
3231
|
-
? h('div', { style: { display: 'flex', gap: 8 } },
|
|
3232
|
-
h(Button, { onClick: saveConfig, disabled: configBusy, primary: true }, t('saveConfig')),
|
|
3233
|
-
h(Button, { onClick: testConfig, disabled: configBusy }, t('testConfig')),
|
|
3234
|
-
h(Button, { onClick: clearKey, disabled: configBusy }, t('clearKey')))
|
|
3235
|
-
: null,
|
|
3236
3545
|
},
|
|
3237
3546
|
h('div', { style: { ...S.row, marginTop: 4 } },
|
|
3238
3547
|
h('div', { className: 'dshm-nav-list is-row', role: 'tablist' },
|
|
@@ -3245,7 +3554,13 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3245
3554
|
className: `dshm-nav-item${memoryBackend === value ? ' on' : ''}`,
|
|
3246
3555
|
onClick: () => setBackend(value),
|
|
3247
3556
|
}, label)))),
|
|
3248
|
-
memoryBackend === 'api'
|
|
3557
|
+
memoryBackend === 'api' ? h(Field, { label: t('vectorSource') },
|
|
3558
|
+
h('div', { className: 'dshm-nav-list is-row', role: 'tablist', 'aria-label': t('vectorSource') },
|
|
3559
|
+
[['local', t('vectorSourceLocal')], ['api', t('vectorSourceApi')]].map(([value, label]) => h('button', {
|
|
3560
|
+
key: value, type: 'button', role: 'tab', title: label, 'aria-selected': vectorSource === value,
|
|
3561
|
+
disabled: configBusy || busy, className: `dshm-nav-item${vectorSource === value ? ' on' : ''}`, onClick: () => setEmbedSource(value),
|
|
3562
|
+
}, label))), h('div', { style: S.muted }, t('vectorSourceHint'))) : null,
|
|
3563
|
+
memoryBackend === 'api' && !isLocalEmbed
|
|
3249
3564
|
? h(React.Fragment, null,
|
|
3250
3565
|
h('div', { style: S.muted }, `${t('configPath')}: ${memuConfig.path || '—'}`),
|
|
3251
3566
|
h(Field, { label: t('provider') },
|
|
@@ -3275,7 +3590,178 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3275
3590
|
onChange: (e) => setConfig('summaryApiKey', e.target.value),
|
|
3276
3591
|
})))
|
|
3277
3592
|
: null,
|
|
3278
|
-
configNotice ? h('div', { style: { ...S.muted, marginTop: 6 } }, configNotice) : null
|
|
3593
|
+
configNotice ? h('div', { style: { ...S.muted, marginTop: 6 } }, configNotice) : null,
|
|
3594
|
+
memoryBackend === 'api'
|
|
3595
|
+
? h('div', { className: 'dshm-cta-row' },
|
|
3596
|
+
h(Button, { onClick: saveConfig, disabled: configBusy, primary: true, className: 'dshm-primary-lg' }, t('saveConfig')),
|
|
3597
|
+
h(Button, { onClick: testConfig, disabled: configBusy }, t('testConfig')),
|
|
3598
|
+
h(Button, { onClick: clearKey, disabled: configBusy, style: S.ghost }, t('clearKey')))
|
|
3599
|
+
: null),
|
|
3600
|
+
memoryBackend === 'api' && isLocalEmbed ? h(Section, {
|
|
3601
|
+
title: t('managedVector'),
|
|
3602
|
+
desc: t('managedVectorHint'),
|
|
3603
|
+
right: h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', alignItems: 'center' } },
|
|
3604
|
+
vectorOperation ? h('span', { role: 'status', 'aria-live': 'polite' }, h(Tag, { tone: 'info' }, `${t('managedVectorStatus')}: ${t(vectorOperation)}`)) : null,
|
|
3605
|
+
localVector.vector?.active ? h(Tag, { tone: 'info' }, `${t('managedVectorActive')}: ${localVector.vector.active.catalogId}${localVector.vector.active.generation ? ' · g' + localVector.vector.active.generation : ''}`) : null,
|
|
3606
|
+
localVector.vector?.requested ? h(Tag, { tone: 'warn' }, `${t('managedVectorRequested')}: ${localVector.vector.requested.catalogId || ''}`) : null,
|
|
3607
|
+
h(Tag, localVector.vector?.active ? { tone: 'info' } : null,
|
|
3608
|
+
`${t('vectorRouteLabel')}: ${localVector.vector?.active ? (vectorSource === 'api' ? t('vectorRouteRemote') : t('vectorRouteLocal')) : t('vectorRouteLexical')}`)),
|
|
3609
|
+
},
|
|
3610
|
+
// 全页只有一个显眼的主操作。条件不满足时保持可见但禁用,并把原因写在旁边:
|
|
3611
|
+
// 把按钮藏起来,用户只会看到“什么都没发生”。
|
|
3612
|
+
h('div', { className: 'dshm-cta-row' },
|
|
3613
|
+
h(Button, {
|
|
3614
|
+
onClick: managedVectorSwitch,
|
|
3615
|
+
disabled: busy || !vectorCanSwitch || !localVector.vector?.requested,
|
|
3616
|
+
primary: true,
|
|
3617
|
+
className: 'dshm-primary-lg',
|
|
3618
|
+
title: t(vectorCanSwitch ? 'managedVectorSwitch' : 'managedVectorSwitchBlocked'),
|
|
3619
|
+
}, actionLabel('switch', t('managedVectorSwitch'))),
|
|
3620
|
+
localVector.vector?.active
|
|
3621
|
+
? h(Button, { onClick: managedVectorRollback, disabled: busy, title: t('managedVectorRollback'), style: S.ghost }, actionLabel('rollback', t('managedVectorRollback')))
|
|
3622
|
+
: null,
|
|
3623
|
+
h('span', { style: { ...S.muted, color: vectorCanSwitch ? V.textDim : V.warn } },
|
|
3624
|
+
t(vectorCanSwitch ? 'managedVectorPrerequisites' : 'managedVectorSwitchBlocked'))),
|
|
3625
|
+
// 重建进度单独成块。三种终态分开渲染:committed 是“已切换”,failed 是失败,
|
|
3626
|
+
// preparing/syncing 才是进行中。之前把 active.lastMigration 也当进度,成功之后
|
|
3627
|
+
// 还会一直显示“重建中 8/8”,让人无法判断到底切没切。
|
|
3628
|
+
(() => {
|
|
3629
|
+
const activeVector = localVector.vector?.active || null
|
|
3630
|
+
const pending = localVector.vector?.pending
|
|
3631
|
+
if (!pending) {
|
|
3632
|
+
const last = activeVector?.lastMigration || null
|
|
3633
|
+
if (!activeVector) return null
|
|
3634
|
+
return h('div', { className: 'dshm-step dshm-rebuild' },
|
|
3635
|
+
h('div', { className: 'dshm-step-head' },
|
|
3636
|
+
h('span', { className: 'dshm-step-title' }, t('vectorRebuildTitle')),
|
|
3637
|
+
h(Tag, { tone: 'info' }, t('vectorStageCommitted')),
|
|
3638
|
+
activeVector.generation ? h(Tag, null, `${t('managedVectorGeneration')} ${activeVector.generation}`) : null,
|
|
3639
|
+
activeVector.fingerprint ? h('span', { style: S.muted }, String(activeVector.fingerprint).slice(0, 8)) : null),
|
|
3640
|
+
last ? h('div', { style: { fontSize: 12.5 } }, `${t('vectorSwitchWrote')} ${last.committed || 0}${last.total ? ' / ' + last.total : ''}`) : null,
|
|
3641
|
+
(() => {
|
|
3642
|
+
const counts = localVector.vector?.indexed
|
|
3643
|
+
const total = counts ? Object.values(counts).reduce((sum, ids) => sum + (Array.isArray(ids) ? ids.length : 0), 0) : null
|
|
3644
|
+
return total === null ? null : h('div', { style: S.muted }, `${t('vectorIndexedNow')} ${total} ${t('vectorIndexedUnit')}`)
|
|
3645
|
+
})(),
|
|
3646
|
+
h('div', { style: S.muted }, t('vectorSwitchDoneHint')))
|
|
3647
|
+
}
|
|
3648
|
+
const progress = pending.report?.progress || null
|
|
3649
|
+
const status = String(pending.status || '')
|
|
3650
|
+
const failedItems = Array.isArray(pending.error?.failedItems) ? pending.error.failedItems : []
|
|
3651
|
+
const total = Number(progress?.total) || 0
|
|
3652
|
+
const done = Number(progress?.completed) || 0
|
|
3653
|
+
const committed = Number(progress?.committed) || 0
|
|
3654
|
+
const generation = progress?.generation ?? pending.generation
|
|
3655
|
+
const fingerprint = progress?.fingerprint ?? pending.fingerprint
|
|
3656
|
+
const head = (label, tone) => h('div', { className: 'dshm-step-head' },
|
|
3657
|
+
h('span', { className: 'dshm-step-title' }, t('vectorRebuildTitle')),
|
|
3658
|
+
label ? h(Tag, tone ? { tone } : null, label) : null,
|
|
3659
|
+
generation ? h(Tag, null, `${t('managedVectorGeneration')} ${generation}`) : null,
|
|
3660
|
+
fingerprint ? h('span', { style: S.muted }, String(fingerprint).slice(0, 8)) : null)
|
|
3661
|
+
if (status === 'committed') {
|
|
3662
|
+
return h('div', { className: 'dshm-step dshm-rebuild' },
|
|
3663
|
+
head(t('vectorStageCommitted'), 'info'),
|
|
3664
|
+
h('div', { style: { fontSize: 12.5 } }, `${t('managedVectorCommitted')} ${done}${total ? ' / ' + total : ''}`),
|
|
3665
|
+
h('div', { style: S.muted }, t('vectorSwitchDoneHint')))
|
|
3666
|
+
}
|
|
3667
|
+
if (status === 'aborted') {
|
|
3668
|
+
return h('div', { className: 'dshm-step dshm-rebuild' },
|
|
3669
|
+
head(t('managedVectorAborted'), 'warn'),
|
|
3670
|
+
h('div', { style: S.muted }, t('vectorAbortedHint')))
|
|
3671
|
+
}
|
|
3672
|
+
if (status === 'failed') {
|
|
3673
|
+
return h('div', { className: 'dshm-step dshm-rebuild' },
|
|
3674
|
+
head(t('vectorStageFailed'), 'warn'),
|
|
3675
|
+
total ? h('div', { style: S.muted }, `${t('managedVectorCommitted')} ${committed} / ${total}`) : null,
|
|
3676
|
+
failedItems.length
|
|
3677
|
+
? h('div', { style: { ...S.muted, color: V.danger, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
3678
|
+
failedItems.slice(0, 6).map(item => h('div', { key: String(item.itemId || item.libraryId) + item.code },
|
|
3679
|
+
`${item.itemId || item.libraryId || '?'} · ${item.code} · ${item.message || ''}`)))
|
|
3680
|
+
: null)
|
|
3681
|
+
}
|
|
3682
|
+
return h('div', { className: 'dshm-step dshm-rebuild' },
|
|
3683
|
+
head(progress ? t('vectorStageSyncing') : null, 'info'),
|
|
3684
|
+
renderVectorProgress(progress, true),
|
|
3685
|
+
h('div', { className: 'dshm-rebuild-meta' },
|
|
3686
|
+
committed ? h('span', null, `${t('managedVectorCommitted')} ${committed}`) : null,
|
|
3687
|
+
progress?.skipped ? h('span', null, `${t('managedVectorSkipped')} ${progress.skipped}`) : null,
|
|
3688
|
+
progress?.failed ? h('span', { style: { color: V.danger } }, `${t('managedVectorFailedCount')} ${progress.failed}`) : null,
|
|
3689
|
+
progress?.current?.itemId ? h('span', { style: S.muted }, String(progress.current.itemId)) : null),
|
|
3690
|
+
failedItems.length
|
|
3691
|
+
? h('div', { style: { ...S.muted, color: V.danger, display: 'flex', flexDirection: 'column', gap: 2 } },
|
|
3692
|
+
failedItems.slice(0, 6).map(item => h('div', { key: String(item.itemId || item.libraryId) + item.code },
|
|
3693
|
+
`${item.itemId || item.libraryId || '?'} · ${item.code} · ${item.message || ''}`)))
|
|
3694
|
+
: null)
|
|
3695
|
+
})(),
|
|
3696
|
+
(() => {
|
|
3697
|
+
const failure = vectorError || localVector.vector?.pending?.error?.message || ''
|
|
3698
|
+
if (!failure) return null
|
|
3699
|
+
return h('div', { role: 'alert', style: { ...S.muted, color: V.danger, marginTop: 4 } },
|
|
3700
|
+
h('strong', null, `${t('managedVectorFailure')}: `), failure)
|
|
3701
|
+
})(),
|
|
3702
|
+
// 步骤 1:运行环境
|
|
3703
|
+
(() => {
|
|
3704
|
+
const runtime = localVector.runtime || {}
|
|
3705
|
+
const busyRuntime = ['downloading', 'verifying', 'installing'].includes(runtime.status)
|
|
3706
|
+
const ready = runtime.ready === true
|
|
3707
|
+
const label = ready
|
|
3708
|
+
? (runtime.source === 'env' ? t('managedVectorRuntimeConfigured') : t('managedVectorRuntimeInstalled'))
|
|
3709
|
+
: runtime.status === 'unsupported' ? t('managedVectorRuntimeUnsupported')
|
|
3710
|
+
: runtime.status === 'failed' ? t('managedVectorRuntimeFailed')
|
|
3711
|
+
: busyRuntime ? t('managedVectorRuntimeInstalling') : t('managedVectorRuntimeMissing')
|
|
3712
|
+
const announce = busyRuntime || Boolean(runtime.error?.message)
|
|
3713
|
+
return h('div', { className: 'dshm-step', ...(announce ? { role: 'status', 'aria-live': 'polite' } : {}) },
|
|
3714
|
+
h('div', { className: 'dshm-step-head' },
|
|
3715
|
+
h('span', { className: 'dshm-step-title' }, '1 · ' + t('managedVectorRuntimeTitle')),
|
|
3716
|
+
h(Tag, ready ? { tone: 'info' } : { tone: 'warn' }, label)),
|
|
3717
|
+
ready
|
|
3718
|
+
? h('div', { style: S.muted }, runtime.version ? `v${runtime.version}` : t('managedVectorReady'))
|
|
3719
|
+
: h('div', { style: S.muted }, t('managedVectorRuntimeHint')),
|
|
3720
|
+
!ready && !busyRuntime && runtime.status !== 'unsupported'
|
|
3721
|
+
? h('div', { className: 'dshm-step-actions' },
|
|
3722
|
+
h(Button, { onClick: managedVectorRuntimeInstall, disabled: busy, primary: true, title: t('managedVectorRuntimeInstall') }, actionLabel('runtime', t('managedVectorRuntimeInstall'))))
|
|
3723
|
+
: null,
|
|
3724
|
+
runtime.error?.message ? h('div', { style: { ...S.muted, color: V.danger } }, runtime.error.message) : null,
|
|
3725
|
+
renderVectorProgress(runtime.progress))
|
|
3726
|
+
})(),
|
|
3727
|
+
// 步骤 2:模型
|
|
3728
|
+
h('div', { className: 'dshm-step' },
|
|
3729
|
+
h('div', { className: 'dshm-step-head' },
|
|
3730
|
+
h('span', { className: 'dshm-step-title' }, '2 · ' + t('managedVectorModelsTitle')),
|
|
3731
|
+
h(Tag, null, String((localVector.catalog || []).length))),
|
|
3732
|
+
(localVector.catalog || []).length === 0
|
|
3733
|
+
? h('div', { style: S.muted }, t('managedVectorNoCatalog'))
|
|
3734
|
+
: h('div', { className: 'dshm-model-grid' }, (localVector.catalog || []).map(entry => {
|
|
3735
|
+
const install = localVector.installations?.[entry.id] || {}
|
|
3736
|
+
const isActive = localVector.vector?.active?.catalogId === entry.id
|
|
3737
|
+
const isRequested = localVector.vector?.requested?.catalogId === entry.id
|
|
3738
|
+
const canSelect = install.installed === true && install.fingerprintMatches !== false
|
|
3739
|
+
const pendingArtifact = entry.availability === 'pending' || (entry.pendingReasons || []).length > 0
|
|
3740
|
+
const sizeText = Number(entry.totalBytes) > 0 ? `${Math.max(1, Math.round(entry.totalBytes / 1024))} KB` : t('managedVectorSizePending')
|
|
3741
|
+
const statusText = install.installed ? t('managedVectorInstalled') : pendingArtifact ? t('managedVectorPending') : (install.status || entry.availability || t('managedVectorUnavailable'))
|
|
3742
|
+
return h('div', { key: entry.id, className: 'dshm-model' },
|
|
3743
|
+
h('div', null,
|
|
3744
|
+
h('div', { className: 'dshm-step-title' }, entry.displayName || entry.id),
|
|
3745
|
+
h('div', { className: 'dshm-model-meta' },
|
|
3746
|
+
h(Tag, null, `${entry.dimension || '?'}d`),
|
|
3747
|
+
h(Tag, null, sizeText),
|
|
3748
|
+
h(Tag, install.installed ? { tone: 'info' } : pendingArtifact ? { tone: 'warn' } : null, statusText),
|
|
3749
|
+
isActive ? h(Tag, { tone: 'info' }, t('managedVectorActive')) : null,
|
|
3750
|
+
isRequested ? h(Tag, { tone: 'warn' }, t('managedVectorRequested')) : null)),
|
|
3751
|
+
h('div', { className: 'dshm-step-actions' },
|
|
3752
|
+
!install.installed && entry.availability === 'available'
|
|
3753
|
+
? h(Button, { onClick: () => managedVectorDownload(entry.id), disabled: busy, primary: true, title: t('managedVectorDownload') }, actionLabel(`download:${entry.id}`, t('managedVectorDownload')))
|
|
3754
|
+
: null,
|
|
3755
|
+
canSelect && !isActive
|
|
3756
|
+
? h(Button, { onClick: () => managedVectorSelect(entry.id), disabled: busy, title: t('managedVectorSelect'), style: S.ghost }, t('managedVectorSelect'))
|
|
3757
|
+
: null),
|
|
3758
|
+
install.error?.message || (entry.pendingReasons || []).length
|
|
3759
|
+
? h('div', { style: S.muted, role: 'status' }, install.error?.message || (entry.pendingReasons || []).join(';'))
|
|
3760
|
+
: null,
|
|
3761
|
+
renderVectorProgress(install.progress))
|
|
3762
|
+
})))
|
|
3763
|
+
) : null,
|
|
3764
|
+
|
|
3279
3765
|
)
|
|
3280
3766
|
|
|
3281
3767
|
const settingsBlock = h(React.Fragment, null, h(Section, {
|
|
@@ -3292,8 +3778,9 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3292
3778
|
h(Field, { label: t('globalTopK') }, h('input', { type: 'number', style: S.input, value: settings.globalTopK ?? 3, onChange: (e) => setSetting('globalTopK', Number(e.target.value)) })),
|
|
3293
3779
|
h(Field, { label: t('mustRememberCap') }, h('input', { type: 'number', style: S.input, value: settings.mustRememberCap ?? 16, onChange: (e) => setSetting('mustRememberCap', Number(e.target.value)) })),
|
|
3294
3780
|
h(Field, { label: t('shortTermTtlHours') }, h('input', { type: 'number', min: 0, style: S.input, value: settings.shortTermTtlHours ?? 72, onChange: (e) => setSetting('shortTermTtlHours', Number(e.target.value)) })),
|
|
3295
|
-
h(Field, { label: t('anchorTokens') }, h('input', { type: 'number', style: S.input, value: settings.anchorTokens ??
|
|
3296
|
-
h(Field, { label: t('turnTokens') }, h('input', { type: 'number', style: S.input, value: settings.turnTokens ??
|
|
3781
|
+
h(Field, { label: t('anchorTokens') }, h('input', { type: 'number', style: S.input, value: settings.anchorTokens ?? 800, onChange: (e) => setSetting('anchorTokens', Number(e.target.value)) })),
|
|
3782
|
+
h(Field, { label: t('turnTokens') }, h('input', { type: 'number', style: S.input, value: settings.turnTokens ?? 800, onChange: (e) => setSetting('turnTokens', Number(e.target.value)) })),
|
|
3783
|
+
h(Field, { label: t('injectionTotalTokens') }, h('input', { type: 'number', style: S.input, value: settings.injectionTotalTokens ?? 2000, onChange: (e) => setSetting('injectionTotalTokens', Number(e.target.value)) })),
|
|
3297
3784
|
h(Field, { label: t('dedupeTurns') }, h('input', { type: 'number', style: S.input, value: settings.dedupeTurns ?? 8, onChange: (e) => setSetting('dedupeTurns', Number(e.target.value)) })),
|
|
3298
3785
|
h(Field, { label: t('globalEveryNTurns') }, h('input', { type: 'number', style: S.input, value: settings.globalEveryNTurns ?? 3, onChange: (e) => setSetting('globalEveryNTurns', Number(e.target.value)) })),
|
|
3299
3786
|
h(Field, { label: t('minUserChars') }, h('input', { type: 'number', style: S.input, value: settings.minUserChars ?? 6, onChange: (e) => setSetting('minUserChars', Number(e.target.value)) })),
|
|
@@ -3903,8 +4390,39 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3903
4390
|
t('statsWindow').split('{days}').join(String(statsData.ratio.windowDays))))
|
|
3904
4391
|
: h(Empty, null, t('statsUnavailable'))))
|
|
3905
4392
|
|
|
4393
|
+
// Clipboard and download are the two things a person actually does with a
|
|
4394
|
+
// log: paste the row into an issue, or keep the file. execCommand is the
|
|
4395
|
+
// fallback because the standalone page can run without clipboard permission.
|
|
4396
|
+
const copyText = async (value) => {
|
|
4397
|
+
const text = String(value ?? '')
|
|
4398
|
+
try { await navigator.clipboard.writeText(text); return true } catch {}
|
|
4399
|
+
try {
|
|
4400
|
+
const area = document.createElement('textarea')
|
|
4401
|
+
area.value = text
|
|
4402
|
+
area.style.position = 'fixed'
|
|
4403
|
+
area.style.opacity = '0'
|
|
4404
|
+
document.body.appendChild(area)
|
|
4405
|
+
area.select()
|
|
4406
|
+
const ok = document.execCommand('copy')
|
|
4407
|
+
area.remove()
|
|
4408
|
+
return ok
|
|
4409
|
+
} catch { return false }
|
|
4410
|
+
}
|
|
4411
|
+
const downloadLogs = () => {
|
|
4412
|
+
try {
|
|
4413
|
+
const blob = new Blob([logRows.map(row => JSON.stringify(row)).join('\n')], { type: 'application/x-ndjson' })
|
|
4414
|
+
const url = URL.createObjectURL(blob)
|
|
4415
|
+
const link = document.createElement('a')
|
|
4416
|
+
link.href = url
|
|
4417
|
+
link.download = `dsh-memu-activity-${new Date().toISOString().slice(0, 10)}.jsonl`
|
|
4418
|
+
link.click()
|
|
4419
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
4420
|
+
} catch (error) { setLogNotice(String(error?.message || error)) }
|
|
4421
|
+
}
|
|
4422
|
+
|
|
3906
4423
|
const pageDefs = [
|
|
3907
4424
|
{ label: t('pageOverview'), overline: 'ENGINE', desc: t('descOverview'), icon: GlyphGauge },
|
|
4425
|
+
{ label: t('pageConnection'), overline: 'CONNECTION', desc: t('descConnection'), icon: GlyphGlobe },
|
|
3908
4426
|
{ label: t('pageSettings'), overline: 'BEHAVIOUR', desc: t('descSettings'), icon: GlyphSliders },
|
|
3909
4427
|
{ label: t('pageFolders'), overline: 'SCOPE', desc: t('descFolders'), icon: GlyphFolder },
|
|
3910
4428
|
{ label: t('pageLibraries'), overline: 'LIBRARIES', desc: t('descLibraries'), icon: GlyphStack },
|
|
@@ -3914,12 +4432,90 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3914
4432
|
{ label: t('pageStats'), overline: 'STATS', desc: t('descStats'), icon: GlyphChart },
|
|
3915
4433
|
]
|
|
3916
4434
|
pageDefs.push({ label: t('pageMemories'), overline: 'MEMU', desc: t('descMemories'), icon: GlyphLayers })
|
|
4435
|
+
pageDefs.push({ label: t('pageLogs'), overline: 'ACTIVITY', desc: t('descLogs'), icon: GlyphText })
|
|
3917
4436
|
const memoriesBlock = h(MemoryWallPanel, { call, libraries: state.libraries || [], revision: state.revision, stamp: state.stamp, t,
|
|
4437
|
+
indexed: localVector.vector?.indexed || null,
|
|
3918
4438
|
onOpen: item => {
|
|
3919
4439
|
if (item.libraryId) { setBrowseLibId(item.libraryId); setPage(PAGE.browser); void openBrowser(item.libraryId) }
|
|
3920
4440
|
else setPage(PAGE.global)
|
|
3921
4441
|
} })
|
|
3922
|
-
|
|
4442
|
+
|
|
4443
|
+
const LOG_KIND_LABELS = {
|
|
4444
|
+
recall: t('logKindRecall'), write: t('logKindWrite'), consolidate: t('logKindConsolidate'),
|
|
4445
|
+
curate: t('logKindCurate'), vector: t('logKindVector'), error: t('logKindError'), system: t('logKindSystem'),
|
|
4446
|
+
}
|
|
4447
|
+
const logsBlock = h(React.Fragment, null,
|
|
4448
|
+
h(Section, {
|
|
4449
|
+
title: t('pageLogs'),
|
|
4450
|
+
desc: t('descLogs'),
|
|
4451
|
+
right: h('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
4452
|
+
h(Button, {
|
|
4453
|
+
style: S.ghost,
|
|
4454
|
+
onClick: async () => {
|
|
4455
|
+
const ok = await copyText(logRows.map(row => JSON.stringify(row)).join('\n'))
|
|
4456
|
+
setLogNotice(ok ? t('copied') : t('copyFailed'))
|
|
4457
|
+
},
|
|
4458
|
+
}, t('logCopyAll')),
|
|
4459
|
+
h(Button, { style: S.ghost, onClick: downloadLogs }, t('logDownload')),
|
|
4460
|
+
h(Button, {
|
|
4461
|
+
style: S.ghost,
|
|
4462
|
+
onClick: async () => {
|
|
4463
|
+
try { await call('clearActivityLog'); await loadLogs() } catch (error) { setLogNotice(String(error?.message || error)) }
|
|
4464
|
+
},
|
|
4465
|
+
}, t('logClear'))),
|
|
4466
|
+
},
|
|
4467
|
+
h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', marginBottom: 10 } },
|
|
4468
|
+
h('select', {
|
|
4469
|
+
style: S.input,
|
|
4470
|
+
'aria-label': t('pageLogs'),
|
|
4471
|
+
value: logKind,
|
|
4472
|
+
onChange: (event) => setLogKind(event.target.value),
|
|
4473
|
+
},
|
|
4474
|
+
h('option', { value: '' }, t('logAllKinds')),
|
|
4475
|
+
['recall', 'write', 'consolidate', 'curate', 'vector', 'error'].map(kind => h('option', { key: kind, value: kind }, LOG_KIND_LABELS[kind]))),
|
|
4476
|
+
h('input', {
|
|
4477
|
+
style: S.input,
|
|
4478
|
+
value: logQueryInput,
|
|
4479
|
+
placeholder: t('logSearch'),
|
|
4480
|
+
'aria-label': t('logSearch'),
|
|
4481
|
+
onChange: (event) => setLogQueryInput(event.target.value),
|
|
4482
|
+
onKeyDown: (event) => { if (event.key === 'Enter') setLogQuery(logQueryInput) },
|
|
4483
|
+
}),
|
|
4484
|
+
h(Button, { onClick: () => { setLogQuery(logQueryInput); void loadLogs() }, disabled: logBusy }, t('logRefresh'))),
|
|
4485
|
+
logStats
|
|
4486
|
+
? h('div', { style: S.muted }, `${logStats.total} / ${logStats.capped} · ${Math.max(1, Math.round((logStats.bytes || 0) / 1024))} KB`)
|
|
4487
|
+
: null,
|
|
4488
|
+
logNotice ? h('div', { style: { ...S.muted, marginTop: 6, color: V.accent } }, logNotice) : null,
|
|
4489
|
+
logRows.length === 0
|
|
4490
|
+
? h(Empty, null, t('logEmpty'))
|
|
4491
|
+
: h('div', { style: S.list }, logRows.map(row => h('div', { key: row.id, style: S.item },
|
|
4492
|
+
h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center' } },
|
|
4493
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', minWidth: 0, flexWrap: 'wrap' } },
|
|
4494
|
+
h(Tag, { tone: row.level === 'error' ? 'warn' : undefined }, LOG_KIND_LABELS[row.kind] || row.kind),
|
|
4495
|
+
h('span', { style: S.muted }, new Date(row.at).toLocaleString()),
|
|
4496
|
+
row.sessionId ? h('span', { style: S.muted }, row.sessionId.slice(-10)) : null,
|
|
4497
|
+
row.libraryId ? h('span', { style: S.muted }, row.libraryId) : null),
|
|
4498
|
+
h('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
|
|
4499
|
+
row.libraryId ? h(Button, {
|
|
4500
|
+
style: S.ghost,
|
|
4501
|
+
// 纠错入口:从日志直接回到这条记忆所在的库,按 id 搜到它再改。
|
|
4502
|
+
onClick: () => { setBrowseLibId(row.libraryId); setPage(PAGE.browser); void openBrowser(row.libraryId) },
|
|
4503
|
+
}, t('logLocate')) : null,
|
|
4504
|
+
h(Button, {
|
|
4505
|
+
style: S.ghost,
|
|
4506
|
+
onClick: async () => {
|
|
4507
|
+
const ok = await copyText(JSON.stringify(row, null, 2))
|
|
4508
|
+
setLogNotice(ok ? t('copied') : t('copyFailed'))
|
|
4509
|
+
},
|
|
4510
|
+
}, t('logCopyOne')))),
|
|
4511
|
+
h('div', { style: { marginTop: 4, whiteSpace: 'pre-wrap' } }, row.message),
|
|
4512
|
+
Array.isArray(row.refs) && row.refs.length
|
|
4513
|
+
? h('div', { style: { ...S.muted, marginTop: 4 } }, row.refs.join(' · '))
|
|
4514
|
+
: null,
|
|
4515
|
+
row.detail
|
|
4516
|
+
? h('pre', { style: { ...S.muted, margin: '6px 0 0', fontSize: 11, whiteSpace: 'pre-wrap', wordBreak: 'break-all' } }, JSON.stringify(row.detail).slice(0, 600))
|
|
4517
|
+
: null)))))
|
|
4518
|
+
const pageBlocks = [engineBlock, connectionBlock, settingsBlock, foldersBlock, librariesBlock, browserBlock, globalBlock, promptsBlock, statsBlock, memoriesBlock, logsBlock]
|
|
3923
4519
|
|
|
3924
4520
|
// Settings uses a left nav with a sliding indicator (the sidebar keeps its
|
|
3925
4521
|
// top segments). The indicator is one absolutely-positioned element moved by
|
|
@@ -4089,6 +4685,26 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4089
4685
|
.catch((e) => setError(String(e?.message || e)))
|
|
4090
4686
|
}, [state?.binding?.scopeKey, sessionsState.byId, sessionId, call, refresh])
|
|
4091
4687
|
|
|
4688
|
+
// 整理记录:点击后显示在按钮下方,替代原来那行"已在后台子会话里整理"的提示。
|
|
4689
|
+
// 数据来自活动日志(getActivityLog),所以整理进行中能看到"已开始",结束后能看到
|
|
4690
|
+
// 完成/超时/失败和写入的记录,刷新页面也不会丢。
|
|
4691
|
+
// Hook 必须在该组件的每个渲染路径上都执行:`if (!sessionId) return ...` 在其后,
|
|
4692
|
+
// 所以这三个 hook 只能放在那行提前 return 之前(否则就是 React #310)。
|
|
4693
|
+
const [runLog, setRunLog] = React.useState([])
|
|
4694
|
+
const loadRunLog = React.useCallback(async () => {
|
|
4695
|
+
if (!sessionId) return
|
|
4696
|
+
try {
|
|
4697
|
+
const data = await call('getActivityLog', { sessionId, limit: 12 })
|
|
4698
|
+
setRunLog(Array.isArray(data?.events) ? data.events : [])
|
|
4699
|
+
} catch {}
|
|
4700
|
+
}, [call, sessionId])
|
|
4701
|
+
React.useEffect(() => { void loadRunLog() }, [loadRunLog])
|
|
4702
|
+
React.useEffect(() => {
|
|
4703
|
+
if (!busy || !sessionId) return undefined
|
|
4704
|
+
const timer = setInterval(() => { void loadRunLog() }, 1500)
|
|
4705
|
+
return () => clearInterval(timer)
|
|
4706
|
+
}, [busy, sessionId, loadRunLog])
|
|
4707
|
+
|
|
4092
4708
|
if (!sessionId) return h('div', { style: S.tab }, t('loading'))
|
|
4093
4709
|
const binding = state?.binding
|
|
4094
4710
|
const summary = sessionsState.byId?.[sessionId]
|
|
@@ -4131,12 +4747,9 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4131
4747
|
setNotice('')
|
|
4132
4748
|
try {
|
|
4133
4749
|
const result = await call('generatePrivate', { sessionId })
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
: t('organizedAgent'))
|
|
4138
|
-
: (result?.error || ''))
|
|
4139
|
-
} catch (e) { setError(String(e?.message || e)) } finally { setBusy(false) }
|
|
4750
|
+
// 失败不再走顶部提示:活动日志里有完整原因和 fork id,面板就在下面。
|
|
4751
|
+
if (result?.ok !== true && result?.error) setError(result.error)
|
|
4752
|
+
} catch (e) { setError(String(e?.message || e)) } finally { setBusy(false); void loadRunLog() }
|
|
4140
4753
|
}
|
|
4141
4754
|
const startCuration = async () => {
|
|
4142
4755
|
if (selected.length === 0) return
|
|
@@ -4155,13 +4768,24 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4155
4768
|
} catch (e) { setError(String(e?.message || e)) }
|
|
4156
4769
|
}
|
|
4157
4770
|
|
|
4771
|
+
const RUN_KIND_LABELS = {
|
|
4772
|
+
recall: t('logKindRecall'), write: t('logKindWrite'), consolidate: t('logKindConsolidate'),
|
|
4773
|
+
curate: t('logKindCurate'), vector: t('logKindVector'), error: t('logKindError'), system: t('logKindSystem'),
|
|
4774
|
+
}
|
|
4775
|
+
const renderRunRow = (row) => h('div', { key: row.id, style: S.item },
|
|
4776
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' } },
|
|
4777
|
+
h(Tag, { tone: row.level === 'error' ? 'warn' : undefined }, RUN_KIND_LABELS[row.kind] || row.kind),
|
|
4778
|
+
h('span', { style: S.muted }, new Date(row.at).toLocaleTimeString())),
|
|
4779
|
+
h('div', { style: { marginTop: 2, whiteSpace: 'pre-wrap', fontSize: 12 } }, row.message))
|
|
4780
|
+
|
|
4158
4781
|
const bindingBlock = !binding
|
|
4159
4782
|
? h(Section, { title: t('noBinding') },
|
|
4160
4783
|
h(Button, { onClick: init, disabled: busy || !summary?.cwd, primary: true }, t('initMemory')),
|
|
4161
4784
|
// A library is a folder, so an unset workspace is a real blocker, not a
|
|
4162
4785
|
// detail to discover by clicking.
|
|
4163
4786
|
!summary?.cwd ? h('div', { style: { ...S.muted, marginTop: 8 } }, t('needsWorkspaceFirst')) : null)
|
|
4164
|
-
: h(
|
|
4787
|
+
: h(React.Fragment, null,
|
|
4788
|
+
h('div', { style: S.section },
|
|
4165
4789
|
// Both layers off in the folder: say so, instead of showing two dead
|
|
4166
4790
|
// checkboxes the user cannot explain.
|
|
4167
4791
|
effective && !effective.enabled
|
|
@@ -4192,7 +4816,15 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4192
4816
|
['general', ...(state?.domains || [])].filter((d, i, list) => list.indexOf(d) === i)
|
|
4193
4817
|
.map((d) => h('option', { key: d, value: d }, domainText(t, d))))),
|
|
4194
4818
|
h('div', { style: { display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' } },
|
|
4195
|
-
h(Button, { onClick: organize, disabled: busy, primary: true }, t('organize')))
|
|
4819
|
+
h(Button, { onClick: organize, disabled: busy, primary: true }, t('organize'))),
|
|
4820
|
+
h('div', { style: { marginTop: 12 } },
|
|
4821
|
+
h('div', { style: { ...S.muted, marginBottom: 4, fontWeight: 600 } }, t('runLogTitle')),
|
|
4822
|
+
runLog.length === 0
|
|
4823
|
+
? h('div', { style: S.muted }, t('runLogEmpty'))
|
|
4824
|
+
: h('div', { style: S.list }, runLog.slice(0, 8).map(renderRunRow)))),
|
|
4825
|
+
// 作用域放在控制卡之后:它是一组低频字段,先让用户看到记忆开关和整理入口。
|
|
4826
|
+
h(ContextScope, { binding, sessionId, call, t })
|
|
4827
|
+
)
|
|
4196
4828
|
|
|
4197
4829
|
const renderPrivateItem = (f) => h('div', { key: f.id, style: S.item },
|
|
4198
4830
|
h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8 } },
|
|
@@ -4298,7 +4930,6 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4298
4930
|
}, p.label)))),
|
|
4299
4931
|
error ? h('div', { style: { ...S.muted, padding: '8px 12px 0', color: V.danger } }, error) : null,
|
|
4300
4932
|
notice ? h('div', { style: { ...S.muted, padding: '8px 12px 0' } }, notice) : null,
|
|
4301
|
-
h(ContextScope, { binding, sessionId, call, t }),
|
|
4302
4933
|
h('div', { style: S.tabBody }, pages[tabPage]?.node || null))
|
|
4303
4934
|
}
|
|
4304
4935
|
|
|
@@ -4331,6 +4962,10 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4331
4962
|
const [libraryId, setLibraryId] = React.useState('')
|
|
4332
4963
|
const [newName, setNewName] = React.useState('')
|
|
4333
4964
|
const [dontShow, setDontShow] = React.useState(false)
|
|
4965
|
+
// The domain decides both the prompt variant and the memory filter. It
|
|
4966
|
+
// used to live only in the sidebar, which is exactly the switch people
|
|
4967
|
+
// forget after starting a coding session in an RP workspace.
|
|
4968
|
+
const [domain, setDomain] = React.useState('rp')
|
|
4334
4969
|
const [busy, setBusy] = React.useState(false)
|
|
4335
4970
|
const [error, setError] = React.useState('')
|
|
4336
4971
|
|
|
@@ -4390,11 +5025,13 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4390
5025
|
sessionId: current,
|
|
4391
5026
|
cwd: summaryCwd || '',
|
|
4392
5027
|
libraries: (st.libraries || []).filter((l) => l.scopeKey !== '__default__'),
|
|
5028
|
+
domains: Array.isArray(st.domains) && st.domains.length ? st.domains : ['rp', 'coding', 'general', 'mixed'],
|
|
4393
5029
|
folder: st.folder || null,
|
|
4394
5030
|
defaultChoice: CARD_OF[String(st.settings?.newSessionDefault || 'both')] || 'both',
|
|
4395
5031
|
})
|
|
4396
5032
|
setChoice(CARD_OF[String(st.settings?.newSessionDefault || 'both')] || 'both')
|
|
4397
5033
|
setPick('folder')
|
|
5034
|
+
setDomain('rp')
|
|
4398
5035
|
setLibraryId((st.libraries || []).find((l) => l.scopeKey !== '__default__')?.id || '')
|
|
4399
5036
|
} catch (e) {
|
|
4400
5037
|
if (!cancelled) setError(String(e?.message || e))
|
|
@@ -4442,6 +5079,9 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4442
5079
|
// Always write both switches, so the sidebar reflects the card and not
|
|
4443
5080
|
// a policy default.
|
|
4444
5081
|
await call('setSessionScope', { sessionId: prompt.sessionId, ...layers })
|
|
5082
|
+
// The domain is written in the same confirm as the memory layers, so
|
|
5083
|
+
// the prompt cannot leave the new conversation on the old default.
|
|
5084
|
+
await call('setDomain', { sessionId: prompt.sessionId, domain })
|
|
4445
5085
|
decided.current.add(prompt.sessionId)
|
|
4446
5086
|
setPrompt(null)
|
|
4447
5087
|
} catch (e) {
|
|
@@ -4504,6 +5144,14 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4504
5144
|
},
|
|
4505
5145
|
h('div', { style: { ...S.muted, lineHeight: 1.6 } }, t('memoryPromptBody')),
|
|
4506
5146
|
error ? h('div', { style: { color: V.danger, fontSize: 12, marginTop: 8 } }, error) : null,
|
|
5147
|
+
h('div', { style: { marginTop: 12 } },
|
|
5148
|
+
h(Field, { label: t('domain'), hint: t('domainPromptHint') },
|
|
5149
|
+
h('select', {
|
|
5150
|
+
style: { ...S.input, flex: 1 },
|
|
5151
|
+
value: domain,
|
|
5152
|
+
disabled: busy,
|
|
5153
|
+
onChange: (e) => setDomain(e.target.value),
|
|
5154
|
+
}, (prompt.domains || []).map((d) => h('option', { key: d, value: d }, domainText(t, d)))))),
|
|
4507
5155
|
h('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, marginTop: 12 } },
|
|
4508
5156
|
cards.map((card) => h('button', {
|
|
4509
5157
|
key: card.id,
|