dsh-memu-memory 1.1.3 → 1.2.1
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 +1 -1
- package/README.md +39 -6
- package/README.zh.md +39 -7
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +3 -3
- package/docs/emotion-model-spec.md +281 -0
- package/docs/env-and-commands.md +8 -0
- package/docs/host-api.md +2 -2
- package/docs/memory-semantics.md +12 -10
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +396 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +23 -6
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +335 -35
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +144 -0
- package/lib/curator.js +86 -31
- package/lib/cycle.js +40 -6
- package/lib/graph.js +139 -5
- package/lib/index.js +33 -1
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +534 -88
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +430 -43
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +54 -5
- package/lib/state.js +29 -12
- package/lib/text-similarity.js +106 -0
- package/lib/tools.js +113 -10
- package/lib/vector-migration.js +28 -3
- package/lib/vector-profile.js +51 -0
- package/package.json +4 -2
- package/scripts/acceptance-local.mjs +2 -1
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +11 -1
- package/scripts/check-all.mjs +10 -1
- package/scripts/replay.mjs +573 -0
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +216 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +331 -0
- package/tests/consolidation-feed.test.mjs +96 -0
- package/tests/consolidation-fork.test.mjs +489 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +360 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-commit.test.mjs +277 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-cascade.test.mjs +190 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +94 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +122 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +591 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/recall-dedup.test.mjs +417 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +212 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +790 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +165 -0
- package/tests/windows-env.test.mjs +89 -0
package/lib/client.js
CHANGED
|
@@ -131,15 +131,39 @@ window.__ModuleLoader__.load({
|
|
|
131
131
|
logKindVector: '向量',
|
|
132
132
|
logKindError: '错误',
|
|
133
133
|
logKindSystem: '系统',
|
|
134
|
+
logKindCheck: '主动检查',
|
|
134
135
|
logClear: '清空日志',
|
|
135
136
|
runLogTitle: '整理记录',
|
|
136
137
|
runLogEmpty: '还没有整理记录。',
|
|
138
|
+
dedupedNotice: '未写入:库里已有几乎相同的记忆 [{id}]。要补充内容请在原条目上编辑合并;确属两次独立观测时才需要 allow_duplicate 重写。',
|
|
139
|
+
consolidateFeedTitle: '整理提醒',
|
|
140
|
+
consolidateFailedCol: '失败',
|
|
141
|
+
consolidateOkCol: '成功',
|
|
142
|
+
consolidateFailed: '失败',
|
|
143
|
+
consolidateDone: '完成',
|
|
144
|
+
consolidateFailedCount: '失败 {n}',
|
|
145
|
+
consolidateJumpHint: '在记忆工作室中打开这条记忆所在的库',
|
|
146
|
+
consolidateExpand: '展开',
|
|
147
|
+
consolidateCollapse: '收起',
|
|
148
|
+
consolidateClear: '清除失败记录',
|
|
149
|
+
consolidateClearHint: '只删除失败的整理日志;成功记录与其它日志不动',
|
|
150
|
+
consolidateCleared: '已清除 {n} 条失败记录',
|
|
137
151
|
allLibraries: '全部记忆库',
|
|
138
152
|
searchMemories: '搜索标题、内容或标签',
|
|
139
153
|
poolReview: '待复核',
|
|
140
154
|
poolInactive: '已失效',
|
|
141
155
|
wallReadError: '部分记忆库读取失败',
|
|
142
156
|
wallLimit: '仅展示最近 300 条',
|
|
157
|
+
wallScopePrivate: '私有',
|
|
158
|
+
wallScopeGlobal: '全局',
|
|
159
|
+
multiSelect: '多选',
|
|
160
|
+
wallSelected: '已选 {n} 条',
|
|
161
|
+
wallSelectAll: '全选本页',
|
|
162
|
+
wallClearSelection: '清空选择',
|
|
163
|
+
wallMultiExit: '退出多选',
|
|
164
|
+
viewMemory: '查看此记忆',
|
|
165
|
+
entityDelete: '删除实体',
|
|
166
|
+
entityDeleteForceHint: '仍被记忆或关系引用;再点一次将连同引用一起删除',
|
|
143
167
|
openMemu: '打开 MemU WebUI',
|
|
144
168
|
saved: '已保存',
|
|
145
169
|
copied: '已复制',
|
|
@@ -637,15 +661,39 @@ window.__ModuleLoader__.load({
|
|
|
637
661
|
logKindVector: 'Vector',
|
|
638
662
|
logKindError: 'Error',
|
|
639
663
|
logKindSystem: 'System',
|
|
664
|
+
logKindCheck: 'Check',
|
|
640
665
|
logClear: 'Clear log',
|
|
641
666
|
runLogTitle: 'Runs',
|
|
642
667
|
runLogEmpty: 'No runs recorded yet.',
|
|
668
|
+
dedupedNotice: 'Not written: the library already holds a near-identical memory [{id}]. Edit and merge into that entry instead; pass allow_duplicate only for two genuinely independent observations.',
|
|
669
|
+
consolidateFeedTitle: 'Consolidation alerts',
|
|
670
|
+
consolidateFailedCol: 'Failed',
|
|
671
|
+
consolidateOkCol: 'Succeeded',
|
|
672
|
+
consolidateFailed: 'Fail',
|
|
673
|
+
consolidateDone: 'Done',
|
|
674
|
+
consolidateFailedCount: '{n} failed',
|
|
675
|
+
consolidateJumpHint: 'Open the library holding this memory in the memory studio',
|
|
676
|
+
consolidateExpand: 'Expand',
|
|
677
|
+
consolidateCollapse: 'Collapse',
|
|
678
|
+
consolidateClear: 'Clear failures',
|
|
679
|
+
consolidateClearHint: 'Deletes only failed consolidation logs; successes and other logs stay',
|
|
680
|
+
consolidateCleared: 'Cleared {n} failure logs',
|
|
643
681
|
allLibraries: 'All libraries',
|
|
644
682
|
searchMemories: 'Search titles, content or tags',
|
|
645
683
|
poolReview: 'Review',
|
|
646
684
|
poolInactive: 'Inactive',
|
|
647
685
|
wallReadError: 'Some libraries could not be read',
|
|
648
686
|
wallLimit: 'Showing the latest 300 entries',
|
|
687
|
+
wallScopePrivate: 'Private',
|
|
688
|
+
wallScopeGlobal: 'Global',
|
|
689
|
+
multiSelect: 'Multi-select',
|
|
690
|
+
wallSelected: '{n} selected',
|
|
691
|
+
wallSelectAll: 'Select page',
|
|
692
|
+
wallClearSelection: 'Clear selection',
|
|
693
|
+
wallMultiExit: 'Exit select',
|
|
694
|
+
viewMemory: 'View',
|
|
695
|
+
entityDelete: 'Delete entity',
|
|
696
|
+
entityDeleteForceHint: 'Still referenced by memories or relations; click again to delete with its links',
|
|
649
697
|
openMemu: 'Open MemU WebUI',
|
|
650
698
|
saved: 'Saved',
|
|
651
699
|
copied: 'Copied',
|
|
@@ -1460,8 +1508,11 @@ window.__ModuleLoader__.load({
|
|
|
1460
1508
|
* Note `window.open(..., 'noopener')` returns null even on success, which is
|
|
1461
1509
|
* why the blocked check must not pass that flag.
|
|
1462
1510
|
*/
|
|
1463
|
-
function openWebUiPage(page = null) {
|
|
1464
|
-
|
|
1511
|
+
function openWebUiPage(page = null, { libraryId = null } = {}) {
|
|
1512
|
+
// libraryId 走 query:SettingsPage 的 standalone 分支从 search 里取 browseLibId,
|
|
1513
|
+
// 跳进去就停在出错记忆所在的库;hash 只决定落到哪个页面。
|
|
1514
|
+
const query = libraryId ? '?libraryId=' + encodeURIComponent(libraryId) : ''
|
|
1515
|
+
let url = '/dsh-memu-memory/ui' + query + '#/settings/' + (page || 'memories')
|
|
1465
1516
|
try { url = new URL(url, window.location.href).toString() } catch {}
|
|
1466
1517
|
let win = null
|
|
1467
1518
|
try { win = window.open(url, '_blank') } catch { win = null }
|
|
@@ -1765,6 +1816,20 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
1765
1816
|
return enumText(t, DOMAIN_LABELS, raw)
|
|
1766
1817
|
}
|
|
1767
1818
|
|
|
1819
|
+
// 情绪构成的短标签:同样的 valence 可能来自怒,也可能来自哀,一个数字分不出来。
|
|
1820
|
+
// 四轴固定且术语极短,就地映射,不为四个轴各加一个 i18n key。
|
|
1821
|
+
const EMOTION_TAG_LABELS = { joy: '喜', anger: '怒', sorrow: '哀', fear: '惧' }
|
|
1822
|
+
function emotionTagText(emotion) {
|
|
1823
|
+
if (!emotion || typeof emotion !== 'object') return ''
|
|
1824
|
+
return Object.entries(EMOTION_TAG_LABELS)
|
|
1825
|
+
.map(([axis, label]) => ({ label, share: Number(emotion[axis]) || 0 }))
|
|
1826
|
+
.filter((item) => item.share >= 0.05)
|
|
1827
|
+
.sort((a, b) => b.share - a.share)
|
|
1828
|
+
.slice(0, 2)
|
|
1829
|
+
.map((item) => `${item.label}${item.share.toFixed(1)}`)
|
|
1830
|
+
.join('/')
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1768
1833
|
// Tags are labels, not decoration — except when the label is a warning. A memory
|
|
1769
1834
|
// marked "never say out loud" has to look different from one that can be said,
|
|
1770
1835
|
// otherwise the badge is just more grey text to skim past.
|
|
@@ -2034,6 +2099,8 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2034
2099
|
const [entityName, setEntityName] = React.useState('')
|
|
2035
2100
|
const [entityKind, setEntityKind] = React.useState('person')
|
|
2036
2101
|
const [armed, setArmed] = React.useState('')
|
|
2102
|
+
// deleteEntity 被后端拒绝(仍被引用)的实体 id:此时再点确认升级为 force。
|
|
2103
|
+
const [deleteRefused, setDeleteRefused] = React.useState('')
|
|
2037
2104
|
const [error, setError] = React.useState('')
|
|
2038
2105
|
const requestId = React.useRef(0)
|
|
2039
2106
|
const mounted = React.useRef(true)
|
|
@@ -2121,7 +2188,28 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2121
2188
|
} },
|
|
2122
2189
|
h('input', { name: 'name', defaultValue: entity.name, 'aria-label': t('entityName') }),
|
|
2123
2190
|
h('input', { name: 'aliases', defaultValue: (entity.aliases || []).join(', '), placeholder: t('entityAliases'), 'aria-label': t('entityAliases') }),
|
|
2124
|
-
h('button', { type: 'submit', className: 'dshm-btn', style: S.btn }, t('save'))
|
|
2191
|
+
h('button', { type: 'submit', className: 'dshm-btn', style: S.btn }, t('save')),
|
|
2192
|
+
// 删除实体:两段确认。仍被 links/关系快照引用时后端默认拒绝(错误如实显示),
|
|
2193
|
+
// 此时再点一次升级为 force——连同引用一起删,这是 force 明示的代价。
|
|
2194
|
+
h('button', {
|
|
2195
|
+
type: 'button',
|
|
2196
|
+
className: `dshm-graph-unlink${armed === 'entity:' + entity.id ? ' is-armed' : ''}`,
|
|
2197
|
+
title: deleteRefused === entity.id ? t('entityDeleteForceHint') : t('entityDelete'),
|
|
2198
|
+
onClick: async () => {
|
|
2199
|
+
const key = 'entity:' + entity.id
|
|
2200
|
+
if (armed !== key) { setArmed(key); return }
|
|
2201
|
+
try {
|
|
2202
|
+
await call('deleteEntity', { libraryId, entityId: entity.id, force: deleteRefused === entity.id })
|
|
2203
|
+
setArmed(''); setDeleteRefused('')
|
|
2204
|
+
notifyMemoryWrite('relation', libraryId)
|
|
2205
|
+
if (mounted.current) await refresh()
|
|
2206
|
+
} catch (failure) {
|
|
2207
|
+
const message = String(failure?.message || failure)
|
|
2208
|
+
if (/referenced|引用/.test(message)) setDeleteRefused(entity.id)
|
|
2209
|
+
if (mounted.current) setError(message)
|
|
2210
|
+
}
|
|
2211
|
+
},
|
|
2212
|
+
}, armed === 'entity:' + entity.id ? (deleteRefused === entity.id ? t('relationConfirm') + '!' : t('relationConfirm')) : '×'))))
|
|
2125
2213
|
}
|
|
2126
2214
|
|
|
2127
2215
|
function InboundToolsInput({ names, onChange }) {
|
|
@@ -2150,7 +2238,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2150
2238
|
item.attachmentCount > 1 ? h('span', { className: 'dshm-wall-cover-count' }, '▧ ' + item.attachmentCount) : null)
|
|
2151
2239
|
}
|
|
2152
2240
|
|
|
2153
|
-
function MemoryWallPanel({ call, libraries, revision, stamp, onOpen, t, indexed = null }) {
|
|
2241
|
+
function MemoryWallPanel({ call, libraries, revision, stamp, onOpen, t, indexed = null, focus = null }) {
|
|
2154
2242
|
const [data, setData] = React.useState({ items: [], total: 0, errors: [] })
|
|
2155
2243
|
const [libraryId, setLibraryId] = React.useState('')
|
|
2156
2244
|
const [view, setView] = React.useState('memories')
|
|
@@ -2160,11 +2248,25 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2160
2248
|
const [query, setQuery] = React.useState('')
|
|
2161
2249
|
const [error, setError] = React.useState('')
|
|
2162
2250
|
const [version, setVersion] = React.useState(0)
|
|
2251
|
+
// 组分类:私有 / 全局两块,默认私有、私有靠前。全局条目只读,所以多选只在私有块。
|
|
2252
|
+
const [scope, setScope] = React.useState('private')
|
|
2253
|
+
const [multi, setMulti] = React.useState(false)
|
|
2254
|
+
const [selectedIds, setSelectedIds] = React.useState(() => new Set())
|
|
2255
|
+
const [wallBusy, setWallBusy] = React.useState(false)
|
|
2256
|
+
const [deleteArmedMulti, setDeleteArmedMulti] = React.useState(false)
|
|
2163
2257
|
// Opens in place instead of jumping to the browser: one card, one request, and
|
|
2164
2258
|
// the wall stays where the reader left it. `null` means the drawer is closed.
|
|
2165
2259
|
const [detail, setDetail] = React.useState(null)
|
|
2166
2260
|
const [deleteArmed, setDeleteArmed] = React.useState(false)
|
|
2167
2261
|
const reload = () => setVersion(value => value + 1)
|
|
2262
|
+
// 全局记忆页"查看此记忆"的落点:带着 id 进来,定位到对应块并直接开抽屉。
|
|
2263
|
+
React.useEffect(() => {
|
|
2264
|
+
if (!focus?.id) return
|
|
2265
|
+
setMulti(false)
|
|
2266
|
+
setSelectedIds(new Set())
|
|
2267
|
+
setScope(focus.libraryId ? 'private' : 'global')
|
|
2268
|
+
void openDetail({ id: focus.id, libraryId: focus.libraryId || null, title: focus.title || focus.id })
|
|
2269
|
+
}, [focus])
|
|
2168
2270
|
React.useEffect(() => {
|
|
2169
2271
|
let alive = true
|
|
2170
2272
|
void call('browseWall', { libraryId: libraryId || null, limit: 300 }).then(value => { if (alive) { setData(value); setError('') } })
|
|
@@ -2191,7 +2293,11 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2191
2293
|
const poolOf = item => item.status === 'pending_review' ? 'review' : item.status === 'archived' ? 'archived'
|
|
2192
2294
|
: ['suppressed', 'expired', 'superseded'].includes(item.status) || item.expiresAt && Date.parse(item.expiresAt) <= Date.now() ? 'inactive' : 'active'
|
|
2193
2295
|
const items = Array.isArray(data?.items) ? data.items : []
|
|
2194
|
-
const
|
|
2296
|
+
const inScope = item => scope === 'global' ? item.libraryId === null : item.libraryId !== null
|
|
2297
|
+
const scoped = items.filter(inScope)
|
|
2298
|
+
const rows = scoped.filter(item => poolOf(item) === pool)
|
|
2299
|
+
const keyOf = item => (item.libraryId || 'global') + item.id
|
|
2300
|
+
const selectedRows = visible => visible.filter(item => selectedIds.has(keyOf(item)))
|
|
2195
2301
|
const layerMatch = (item, value) => value === 'all' || (value === 'anchor' ? item.layer === 'must_remember' && poolOf(item) === 'active' : item.layer === value)
|
|
2196
2302
|
const text = String(query || '').toLowerCase()
|
|
2197
2303
|
const visible = rows.filter(item => layerMatch(item, layer) && (!text || [item.title, item.preview, (item.tags || []).join(' ')].join(' ').toLowerCase().includes(text)))
|
|
@@ -2282,6 +2388,32 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2282
2388
|
}
|
|
2283
2389
|
}
|
|
2284
2390
|
|
|
2391
|
+
// —— 多选批量操作(只对私有块生效;全局只读)——
|
|
2392
|
+
// 逐条走既有 RPC(updateMemory patch.status / deleteMemory),循环里失败的条目
|
|
2393
|
+
// 记下来一次性报错:部分成功不能说成全部成功,也不该让一条失败吞掉其余的成果。
|
|
2394
|
+
const exitMulti = () => { setMulti(false); setSelectedIds(new Set()); setDeleteArmedMulti(false) }
|
|
2395
|
+
const bulkApply = async (action) => {
|
|
2396
|
+
const targets = selectedRows(visible).filter(item => action === 'archive'
|
|
2397
|
+
? ['active', 'inactive'].includes(poolOf(item))
|
|
2398
|
+
: action === 'restore' || action === 'delete' ? item.status === 'archived' : false)
|
|
2399
|
+
if (targets.length === 0) return
|
|
2400
|
+
setWallBusy(true); setError('')
|
|
2401
|
+
let done = 0
|
|
2402
|
+
const failures = []
|
|
2403
|
+
for (const item of targets) {
|
|
2404
|
+
try {
|
|
2405
|
+
if (action === 'delete') await call('deleteMemory', { libraryId: item.libraryId, id: item.id })
|
|
2406
|
+
else await call('updateMemory', { libraryId: item.libraryId, id: item.id, patch: { status: action === 'archive' ? 'archived' : 'active' } })
|
|
2407
|
+
done += 1
|
|
2408
|
+
} catch (failure) { failures.push(`${item.title || item.id}: ${String(failure?.message || failure)}`) }
|
|
2409
|
+
}
|
|
2410
|
+
setWallBusy(false)
|
|
2411
|
+
setDeleteArmedMulti(false)
|
|
2412
|
+
setSelectedIds(new Set())
|
|
2413
|
+
if (done > 0) { notifyMemoryWrite('memory', libraryId || null); reload() }
|
|
2414
|
+
if (failures.length > 0) setError(`${done}/${targets.length} · ` + failures.slice(0, 3).join(';'))
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2285
2417
|
const shown = detail ? { ...detail.item, ...(detail.file || {}) } : null
|
|
2286
2418
|
const detailForm = detail && detail.file && !detail.readonly
|
|
2287
2419
|
? h('div', { className: 'dshm-drawer-form' },
|
|
@@ -2435,19 +2567,59 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2435
2567
|
view === 'relations'
|
|
2436
2568
|
? graphView
|
|
2437
2569
|
: h(React.Fragment, null,
|
|
2570
|
+
// 组分类:私有 / 全局(默认私有、私有靠前)。计数按各自的块算,切换即重置多选。
|
|
2571
|
+
h('div', { className: 'dshm-nav-list is-row', role: 'tablist' },
|
|
2572
|
+
[['private', 'wallScopePrivate'], ['global', 'wallScopeGlobal']].map(([value, key]) =>
|
|
2573
|
+
h('button', {
|
|
2574
|
+
key: value, type: 'button', role: 'tab', 'aria-selected': scope === value,
|
|
2575
|
+
className: `dshm-nav-item${scope === value ? ' on' : ''}`,
|
|
2576
|
+
onClick: () => { setScope(value); exitMulti() },
|
|
2577
|
+
}, t(key) + ' (' + items.filter(item => value === 'global' ? item.libraryId === null : item.libraryId !== null).length + ')'))),
|
|
2438
2578
|
data?.total > items.length ? h('p', null, t('wallLimit') + ` (${items.length}/${data.total})`) : null,
|
|
2439
2579
|
h('div', { className: 'dshm-graph-tools' },
|
|
2440
2580
|
h('input', { type: 'search', value: query, placeholder: t('searchMemories'), 'aria-label': t('searchMemories'), onChange: event => setQuery(event.target.value) }),
|
|
2441
|
-
h('select', { value: libraryId, title: t('currentLibrary'), onChange: event => setLibraryId(event.target.value) },
|
|
2442
|
-
h('option', { value: '' }, t('allLibraries')), libraries.map(lib => h('option', { key: lib.id, value: lib.id }, lib.name || lib.id)))
|
|
2581
|
+
scope === 'private' ? h('select', { value: libraryId, title: t('currentLibrary'), onChange: event => setLibraryId(event.target.value) },
|
|
2582
|
+
h('option', { value: '' }, t('allLibraries')), libraries.map(lib => h('option', { key: lib.id, value: lib.id }, lib.name || lib.id))) : null,
|
|
2583
|
+
// 多选只对私有块:全局条目只读,批量操作没有意义。
|
|
2584
|
+
scope === 'private' ? h(Button, { style: S.ghost, onClick: () => exitMulti() + setMulti(v => !v) }, t('multiSelect')) : null),
|
|
2585
|
+
multi && scope === 'private' ? (() => {
|
|
2586
|
+
const chosen = selectedRows(visible)
|
|
2587
|
+
const archiveN = chosen.filter(item => ['active', 'inactive'].includes(poolOf(item))).length
|
|
2588
|
+
const restoreN = chosen.filter(item => item.status === 'archived').length
|
|
2589
|
+
const deleteN = restoreN
|
|
2590
|
+
return h('div', { className: 'dshm-graph-tools', role: 'toolbar', 'aria-label': t('multiSelect') },
|
|
2591
|
+
h('span', { style: S.muted }, t('wallSelected').replace('{n}', String(chosen.length))),
|
|
2592
|
+
h(Button, { style: S.ghost, disabled: chosen.length === visible.length, onClick: () => setSelectedIds(new Set(visible.map(keyOf))) }, t('wallSelectAll')),
|
|
2593
|
+
h(Button, { style: S.ghost, disabled: chosen.length === 0, onClick: () => setSelectedIds(new Set()) }, t('wallClearSelection')),
|
|
2594
|
+
h(Button, { style: S.ghost, disabled: wallBusy || archiveN === 0, onClick: () => { void bulkApply('archive') } }, t('archive') + ` (${archiveN})`),
|
|
2595
|
+
h(Button, { style: S.ghost, disabled: wallBusy || restoreN === 0, onClick: () => { void bulkApply('restore') } }, t('restore') + ` (${restoreN})`),
|
|
2596
|
+
deleteArmedMulti
|
|
2597
|
+
? h(React.Fragment, null,
|
|
2598
|
+
h(Button, { style: { ...S.ghost, color: V.danger, borderColor: V.danger }, disabled: wallBusy || deleteN === 0, onClick: () => { void bulkApply('delete') } }, t('confirmDelete') + ` (${deleteN})`),
|
|
2599
|
+
h(Button, { style: S.ghost, disabled: wallBusy, onClick: () => setDeleteArmedMulti(false) }, t('cancelDelete')))
|
|
2600
|
+
: h(Button, { style: { ...S.ghost, color: V.danger }, disabled: wallBusy || deleteN === 0, title: t('browseArchivedHint'), onClick: () => setDeleteArmedMulti(true) }, t('deleteForever') + ` (${deleteN})`),
|
|
2601
|
+
h(Button, { style: S.ghost, onClick: exitMulti }, t('wallMultiExit')))
|
|
2602
|
+
})() : null,
|
|
2443
2603
|
h('div', { className: 'dshm-nav-list is-row', role: 'tablist' }, [['active', 'browseActiveTab'], ['archived', 'browseArchivedTab'], ['review', 'poolReview'], ['inactive', 'poolInactive']].map(([value, key]) =>
|
|
2444
2604
|
h('button', { key: value, type: 'button', role: 'tab', 'aria-selected': pool === value, className: `dshm-nav-item${pool === value ? ' on' : ''}`, onClick: () => setPool(value) },
|
|
2445
|
-
t(key) + ' (' +
|
|
2605
|
+
t(key) + ' (' + scoped.filter(item => poolOf(item) === value).length + ')'))),
|
|
2446
2606
|
h('div', { className: 'dshm-nav-list is-row', role: 'tablist' }, ['all', 'must_remember', 'long_term', 'short_term', 'anchor'].map(value =>
|
|
2447
2607
|
h('button', { key: value, type: 'button', role: 'tab', 'aria-selected': layer === value, className: `dshm-nav-item${layer === value ? ' on' : ''}`, onClick: () => setLayer(value) },
|
|
2448
2608
|
(value === 'all' ? t('allLibraries') : enumText(t, LAYER_LABELS, value)) + ' (' + rows.filter(item => layerMatch(item, value)).length + ')'))),
|
|
2449
2609
|
visible.length ? h('div', { className: 'dshm-wall-grid' }, visible.map(item =>
|
|
2450
|
-
h('button', {
|
|
2610
|
+
h('button', {
|
|
2611
|
+
key: keyOf(item), type: 'button', className: 'dshm-wall-item' + (multi && selectedIds.has(keyOf(item)) ? ' is-selected' : ''),
|
|
2612
|
+
style: multi && selectedIds.has(keyOf(item)) ? { outline: '2px solid ' + V.accent, outlineOffset: -2 } : undefined,
|
|
2613
|
+
onClick: () => {
|
|
2614
|
+
// 多选模式下点卡片 = 勾选/取消勾选;正常模式打开抽屉。
|
|
2615
|
+
if (multi && scope === 'private') {
|
|
2616
|
+
setSelectedIds(prev => { const next = new Set(prev); if (next.has(keyOf(item))) next.delete(keyOf(item)); else next.add(keyOf(item)); return next })
|
|
2617
|
+
return
|
|
2618
|
+
}
|
|
2619
|
+
void openDetail(item)
|
|
2620
|
+
},
|
|
2621
|
+
},
|
|
2622
|
+
multi && scope === 'private' ? h(Tag, { tone: selectedIds.has(keyOf(item)) ? 'info' : null }, selectedIds.has(keyOf(item)) ? '✓' : '□') : null,
|
|
2451
2623
|
h(WallCover, { item }),
|
|
2452
2624
|
h('div', { className: 'dshm-wall-meta' }, h('span', null, item.libraryId ? item.libraryName : t('globalMemory')), h('span', null, enumText(t, LAYER_LABELS, item.layer))),
|
|
2453
2625
|
h('strong', null, item.title || item.id),
|
|
@@ -2489,6 +2661,8 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
2489
2661
|
const [busy, setBusy] = React.useState(false)
|
|
2490
2662
|
const [vectorOperation, setVectorOperation] = React.useState('')
|
|
2491
2663
|
const [vectorError, setVectorError] = React.useState('')
|
|
2664
|
+
// 全局记忆页"查看此记忆"的接力棒:带着 id 切到 MemU 记忆页,墙直接开对应抽屉。
|
|
2665
|
+
const [wallFocus, setWallFocus] = React.useState(null)
|
|
2492
2666
|
// Success messages need their own slot: reusing `error` would paint a good
|
|
2493
2667
|
// outcome in red and make the next real failure easy to miss.
|
|
2494
2668
|
const [notice, setNotice] = React.useState('')
|
|
@@ -3103,7 +3277,13 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3103
3277
|
payload.pinReason = 'user_explicit_note'
|
|
3104
3278
|
payload.pinSource = 'user_explicit'
|
|
3105
3279
|
}
|
|
3106
|
-
await call('addMemory', payload)
|
|
3280
|
+
const created = await call('addMemory', payload)
|
|
3281
|
+
// 事实级去重:几乎相同的重写被拒。表单留着、
|
|
3282
|
+
// 说明给足,否则用户看到的是"点了没反应"。
|
|
3283
|
+
if (created?.deduped === true) {
|
|
3284
|
+
setError(t('dedupedNotice').replace('{id}', created.existingId || '?'))
|
|
3285
|
+
return
|
|
3286
|
+
}
|
|
3107
3287
|
notifyMemoryWrite('memory', browseLibId)
|
|
3108
3288
|
setNewMem({ title: '', content: '', layer: 'long_term', salientDate: '' })
|
|
3109
3289
|
await openBrowser(browseLibId)
|
|
@@ -3774,18 +3954,22 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3774
3954
|
h(Field, { label: t('autoInject') },
|
|
3775
3955
|
h('select', { style: S.input, value: settings.autoInject || 'smart', onChange: (e) => setSetting('autoInject', e.target.value) },
|
|
3776
3956
|
['smart', 'anchor-only', 'per-turn', 'off'].map((v) => h('option', { key: v, value: v }, enumText(t, INJECT_LABELS, v))))),
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3957
|
+
// 下面这些 ?? 回落值必须与 lib/state.js 的 DEFAULT_SETTINGS 一致:浏览器 bundle 不能
|
|
3958
|
+
// import 服务端模块,这里是客户端唯一的一份副本。它只在首次 getState 回来之前显示,
|
|
3959
|
+
// 但用户恰好在这段窗口里点「保存」就会把回落值持久化,等于悄悄改掉默认值。
|
|
3960
|
+
// 一致性由 tests/ui-defaults.test.mjs 守着。
|
|
3961
|
+
h(Field, { label: t('privateTopK') }, h('input', { type: 'number', style: S.input, value: settings.privateTopK ?? 5, onChange: (e) => setSetting('privateTopK', Number(e.target.value)) })),
|
|
3962
|
+
h(Field, { label: t('globalTopK') }, h('input', { type: 'number', style: S.input, value: settings.globalTopK ?? 5, onChange: (e) => setSetting('globalTopK', Number(e.target.value)) })),
|
|
3963
|
+
h(Field, { label: t('mustRememberCap') }, h('input', { type: 'number', style: S.input, value: settings.mustRememberCap ?? 12, onChange: (e) => setSetting('mustRememberCap', Number(e.target.value)) })),
|
|
3780
3964
|
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)) })),
|
|
3781
3965
|
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
3966
|
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
3967
|
h(Field, { label: t('injectionTotalTokens') }, h('input', { type: 'number', style: S.input, value: settings.injectionTotalTokens ?? 2000, onChange: (e) => setSetting('injectionTotalTokens', Number(e.target.value)) })),
|
|
3784
|
-
h(Field, { label: t('dedupeTurns') }, h('input', { type: 'number', style: S.input, value: settings.dedupeTurns ??
|
|
3785
|
-
h(Field, { label: t('globalEveryNTurns') }, h('input', { type: 'number', style: S.input, value: settings.globalEveryNTurns ??
|
|
3968
|
+
h(Field, { label: t('dedupeTurns') }, h('input', { type: 'number', style: S.input, value: settings.dedupeTurns ?? 16, onChange: (e) => setSetting('dedupeTurns', Number(e.target.value)) })),
|
|
3969
|
+
h(Field, { label: t('globalEveryNTurns') }, h('input', { type: 'number', style: S.input, value: settings.globalEveryNTurns ?? 6, onChange: (e) => setSetting('globalEveryNTurns', Number(e.target.value)) })),
|
|
3786
3970
|
h(Field, { label: t('minUserChars') }, h('input', { type: 'number', style: S.input, value: settings.minUserChars ?? 6, onChange: (e) => setSetting('minUserChars', Number(e.target.value)) })),
|
|
3787
3971
|
h(Field, { label: t('relevanceThreshold') }, h('input', { type: 'number', step: '0.05', min: 0, max: 1, style: S.input, value: settings.relevanceThreshold ?? 0.4, onChange: (e) => setSetting('relevanceThreshold', Number(e.target.value)) })),
|
|
3788
|
-
h(Field, { label: t('randomCueChance') }, h('input', { type: 'number', step: '0.01', min: 0, max: 1, style: S.input, value: settings.randomCueChance ?? 0.
|
|
3972
|
+
h(Field, { label: t('randomCueChance') }, h('input', { type: 'number', step: '0.01', min: 0, max: 1, style: S.input, value: settings.randomCueChance ?? 0.02, onChange: (e) => setSetting('randomCueChance', Number(e.target.value)) })),
|
|
3789
3973
|
h(Field, { label: t('autoInstall') }, h('input', { type: 'checkbox', checked: settings.autoInstallEngine !== false, onChange: (e) => setSetting('autoInstallEngine', e.target.checked) })),
|
|
3790
3974
|
h(Field, { label: t('newSessionPrompt') }, h('input', { type: 'checkbox', checked: settings.newSessionPromptDismissed !== true, onChange: (e) => setSetting('newSessionPromptDismissed', !e.target.checked) })),
|
|
3791
3975
|
h(Field, { label: t('habitsInjection') }, h('input', { type: 'checkbox', checked: settings.habitsInjection !== false, onChange: (e) => setSetting('habitsInjection', e.target.checked) })),
|
|
@@ -3795,7 +3979,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3795
3979
|
names: settings.inboundToolNames || [], onChange: values => setSetting('inboundToolNames', values),
|
|
3796
3980
|
})),
|
|
3797
3981
|
h(Field, { label: t('sleepConsolidation') }, h('input', { type: 'checkbox', checked: settings.sleepConsolidation === true, onChange: (e) => setSetting('sleepConsolidation', e.target.checked) })),
|
|
3798
|
-
h(Field, { label: t('sleepAfterIdleMinutes') }, h('input', { type: 'number', min: 1, style: S.input, value: settings.sleepAfterIdleMinutes ??
|
|
3982
|
+
h(Field, { label: t('sleepAfterIdleMinutes') }, h('input', { type: 'number', min: 1, style: S.input, value: settings.sleepAfterIdleMinutes ?? 3, onChange: (e) => setSetting('sleepAfterIdleMinutes', Number(e.target.value)) })),
|
|
3799
3983
|
h(Field, { label: t('autoCurateEveryHours') }, h('input', { type: 'number', min: 0, style: S.input, value: settings.autoCurateEveryHours ?? 0, onChange: (e) => setSetting('autoCurateEveryHours', Number(e.target.value)) })),
|
|
3800
3984
|
h(Field, { label: t('autoCurateSessionLimit') }, h('input', { type: 'number', min: 1, max: 30, style: S.input, value: settings.autoCurateSessionLimit ?? 8, onChange: (e) => setSetting('autoCurateSessionLimit', Number(e.target.value)) })),
|
|
3801
3985
|
h(Field, { label: t('purgeArchivedAfterDays') }, h('input', { type: 'number', min: 0, style: S.input, value: settings.purgeArchivedAfterDays ?? 0, onChange: (e) => setSetting('purgeArchivedAfterDays', Number(e.target.value)) })),
|
|
@@ -3852,9 +4036,15 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
3852
4036
|
const renderGlobalItem = (f) => h('div', { key: f.id, style: S.item },
|
|
3853
4037
|
h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8 } },
|
|
3854
4038
|
h('strong', null, f.title || f.id),
|
|
3855
|
-
h('div', { style: { display: 'flex', gap: 6 } },
|
|
4039
|
+
h('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
|
|
3856
4040
|
h(Tag, null, domainText(t, f.domain)),
|
|
3857
|
-
h(Tag, null, `conf ${f.confidence ?? '?'}`)
|
|
4041
|
+
h(Tag, null, `conf ${f.confidence ?? '?'}`),
|
|
4042
|
+
// 查看此记忆:切到 MemU 记忆页的全局块并直接打开对应条目的抽屉。
|
|
4043
|
+
h(Button, {
|
|
4044
|
+
style: S.ghost,
|
|
4045
|
+
title: t('viewMemory'),
|
|
4046
|
+
onClick: () => { setWallFocus({ id: f.id, libraryId: null, title: f.title || f.id, at: Date.now() }); setPage(PAGE.memories) },
|
|
4047
|
+
}, t('viewMemory')))),
|
|
3858
4048
|
h('div', { style: { ...S.muted, marginTop: 4, whiteSpace: 'pre-wrap' } },
|
|
3859
4049
|
String(f.content || '').slice(0, 400))
|
|
3860
4050
|
)
|
|
@@ -4023,6 +4213,9 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4023
4213
|
const set = (key, value) => setBrowseDraft((prev) => ({
|
|
4024
4214
|
...prev, [f.id]: { ...(prev[f.id] || {}), [key]: value },
|
|
4025
4215
|
}))
|
|
4216
|
+
// 有构成就讲构成;没有就回到旧的单值标签,老条目照旧可读。
|
|
4217
|
+
const moodText = emotionTagText(f.emotion)
|
|
4218
|
+
|| (typeof f.moodValence === 'number' && f.moodValence !== 0 ? f.moodValence.toFixed(2) : '')
|
|
4026
4219
|
return h('div', { key: f.id, style: S.item },
|
|
4027
4220
|
h('div', { style: { display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center' } },
|
|
4028
4221
|
h('div', { style: S.row },
|
|
@@ -4051,8 +4244,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4051
4244
|
f.salientDate
|
|
4052
4245
|
? h(Tag, null, `${/^\d{4}-\d{2}-\d{2}$/.test(f.salientDate) ? t('dueDateTag') : t('anniversaryTag')} ${f.salientDate}`)
|
|
4053
4246
|
: null,
|
|
4054
|
-
|
|
4055
|
-
? h(Tag, null, `${t('moodTag')} ${f.moodValence.toFixed(2)}`) : null,
|
|
4247
|
+
moodText ? h(Tag, null, `${t('moodTag')} ${moodText}`) : null,
|
|
4056
4248
|
typeof f.accuracy === 'number' && typeof f.confidence === 'number' && f.accuracy <= 0.6 && f.accuracy < f.confidence - 0.1
|
|
4057
4249
|
? h(Tag, { tone: 'warn' }, `${t('accuracyTag')} ${f.accuracy}`) : null,
|
|
4058
4250
|
h(Tag, null, enumText(t, STATUS_LABELS, f.status || 'active'))),
|
|
@@ -4435,6 +4627,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4435
4627
|
pageDefs.push({ label: t('pageLogs'), overline: 'ACTIVITY', desc: t('descLogs'), icon: GlyphText })
|
|
4436
4628
|
const memoriesBlock = h(MemoryWallPanel, { call, libraries: state.libraries || [], revision: state.revision, stamp: state.stamp, t,
|
|
4437
4629
|
indexed: localVector.vector?.indexed || null,
|
|
4630
|
+
focus: wallFocus,
|
|
4438
4631
|
onOpen: item => {
|
|
4439
4632
|
if (item.libraryId) { setBrowseLibId(item.libraryId); setPage(PAGE.browser); void openBrowser(item.libraryId) }
|
|
4440
4633
|
else setPage(PAGE.global)
|
|
@@ -4442,7 +4635,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4442
4635
|
|
|
4443
4636
|
const LOG_KIND_LABELS = {
|
|
4444
4637
|
recall: t('logKindRecall'), write: t('logKindWrite'), consolidate: t('logKindConsolidate'),
|
|
4445
|
-
curate: t('logKindCurate'), vector: t('logKindVector'), error: t('logKindError'), system: t('logKindSystem'),
|
|
4638
|
+
curate: t('logKindCurate'), vector: t('logKindVector'), error: t('logKindError'), system: t('logKindSystem'), check: t('logKindCheck'),
|
|
4446
4639
|
}
|
|
4447
4640
|
const logsBlock = h(React.Fragment, null,
|
|
4448
4641
|
h(Section, {
|
|
@@ -4460,7 +4653,12 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4460
4653
|
h(Button, {
|
|
4461
4654
|
style: S.ghost,
|
|
4462
4655
|
onClick: async () => {
|
|
4463
|
-
try {
|
|
4656
|
+
try {
|
|
4657
|
+
await call('clearActivityLog')
|
|
4658
|
+
// 跨面板即时生效:侧边栏的整理提醒不用等下一个 20s 轮询。
|
|
4659
|
+
broadcastMemoryWrite('logs-cleared')
|
|
4660
|
+
await loadLogs()
|
|
4661
|
+
} catch (error) { setLogNotice(String(error?.message || error)) }
|
|
4464
4662
|
},
|
|
4465
4663
|
}, t('logClear'))),
|
|
4466
4664
|
},
|
|
@@ -4472,7 +4670,7 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4472
4670
|
onChange: (event) => setLogKind(event.target.value),
|
|
4473
4671
|
},
|
|
4474
4672
|
h('option', { value: '' }, t('logAllKinds')),
|
|
4475
|
-
['recall', 'write', 'consolidate', 'curate', 'vector', 'error'].map(kind => h('option', { key: kind, value: kind }, LOG_KIND_LABELS[kind]))),
|
|
4673
|
+
['recall', 'write', 'consolidate', 'curate', 'vector', 'error', 'check'].map(kind => h('option', { key: kind, value: kind }, LOG_KIND_LABELS[kind]))),
|
|
4476
4674
|
h('input', {
|
|
4477
4675
|
style: S.input,
|
|
4478
4676
|
value: logQueryInput,
|
|
@@ -4493,8 +4691,14 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4493
4691
|
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', minWidth: 0, flexWrap: 'wrap' } },
|
|
4494
4692
|
h(Tag, { tone: row.level === 'error' ? 'warn' : undefined }, LOG_KIND_LABELS[row.kind] || row.kind),
|
|
4495
4693
|
h('span', { style: S.muted }, new Date(row.at).toLocaleString()),
|
|
4496
|
-
|
|
4497
|
-
|
|
4694
|
+
// 整理/提炼日志挂上会话标题与文件夹名,裸 id 只做回退:日志页要回答
|
|
4695
|
+
// "这是哪个对话、哪个库",光靠 sessionId 后 10 位查不动。
|
|
4696
|
+
row.sessionTitle
|
|
4697
|
+
? h('span', { style: { fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 260 }, title: row.sessionId || '' }, row.sessionTitle)
|
|
4698
|
+
: row.sessionId ? h('span', { style: S.muted }, row.sessionId.slice(-10)) : null,
|
|
4699
|
+
row.libraryName
|
|
4700
|
+
? h(Tag, null, row.libraryName)
|
|
4701
|
+
: row.libraryId ? h('span', { style: S.muted }, row.libraryId) : null),
|
|
4498
4702
|
h('div', { style: { display: 'flex', gap: 6, flexShrink: 0 } },
|
|
4499
4703
|
row.libraryId ? h(Button, {
|
|
4500
4704
|
style: S.ghost,
|
|
@@ -4576,6 +4780,65 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4576
4780
|
return h(MemoryTab, { ctx, t, sessionId })
|
|
4577
4781
|
}
|
|
4578
4782
|
|
|
4783
|
+
// 整理提醒:跨会话(不按当前对话过滤),放在记忆设置卡之下、整理记录之上。
|
|
4784
|
+
// 默认收起只显示最新一条(有失败优先失败),点头部展开成失败/成功两列(各最新 6 条)。
|
|
4785
|
+
// 失败列带"清除失败记录":只删 consolidate 且 ok=false 的日志行,成功流水不动。
|
|
4786
|
+
function ConsolidationAlerts({ feed, t, onJump, onClear }) {
|
|
4787
|
+
const [open, setOpen] = React.useState(false)
|
|
4788
|
+
if (!feed) return null
|
|
4789
|
+
const failures = Array.isArray(feed.failures) ? feed.failures : []
|
|
4790
|
+
const successes = Array.isArray(feed.successes) ? feed.successes : []
|
|
4791
|
+
if (failures.length === 0 && successes.length === 0) return null
|
|
4792
|
+
const shortId = (id) => String(id || '').slice(-8)
|
|
4793
|
+
const label = (row) => row.sessionTitle || shortId(row.sessionId)
|
|
4794
|
+
const when = (row) => {
|
|
4795
|
+
try { return new Date(row.at).toLocaleString([], { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) } catch { return row.at }
|
|
4796
|
+
}
|
|
4797
|
+
const rowEl = (row, tone) => h('div', { key: row.id, style: { minWidth: 0 } },
|
|
4798
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', minWidth: 0 } },
|
|
4799
|
+
h(Tag, { tone: tone === 'fail' ? 'warn' : undefined }, tone === 'fail' ? t('consolidateFailed') : t('consolidateDone')),
|
|
4800
|
+
h('span', { style: { fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', minWidth: 0, flex: 1 }, title: row.message }, label(row)),
|
|
4801
|
+
h('span', { style: { ...S.muted, flexShrink: 0, fontSize: 11 } }, when(row)),
|
|
4802
|
+
tone === 'fail' ? h(Button, {
|
|
4803
|
+
style: { ...S.ghost, padding: '1px 7px', flexShrink: 0 },
|
|
4804
|
+
title: t('consolidateJumpHint'),
|
|
4805
|
+
onClick: () => onJump(row),
|
|
4806
|
+
}, '↗') : null),
|
|
4807
|
+
tone === 'fail' && row.message
|
|
4808
|
+
? h('div', { style: { ...S.muted, fontSize: 11, marginTop: 1, overflow: 'hidden', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical' } }, row.message)
|
|
4809
|
+
: null)
|
|
4810
|
+
const column = (title, rows, tone) => h('div', { style: { minWidth: 0, flex: 1 } },
|
|
4811
|
+
h('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginBottom: 4 } },
|
|
4812
|
+
h('span', { style: { ...S.muted, fontWeight: 600, color: tone === 'fail' ? V.danger : undefined } }, title),
|
|
4813
|
+
tone === 'fail' && failures.length > 0 ? h(Button, {
|
|
4814
|
+
style: { ...S.ghost, padding: '1px 6px', fontSize: 11, flexShrink: 0 },
|
|
4815
|
+
title: t('consolidateClearHint'),
|
|
4816
|
+
onClick: onClear,
|
|
4817
|
+
}, t('consolidateClear')) : null),
|
|
4818
|
+
rows.length === 0
|
|
4819
|
+
? h('div', { style: S.muted }, '—')
|
|
4820
|
+
: h('div', { style: { display: 'flex', flexDirection: 'column', gap: 5 } }, rows.map(row => rowEl(row, tone))))
|
|
4821
|
+
const latest = failures.length > 0 ? failures[0] : successes[0]
|
|
4822
|
+
const latestTone = failures.length > 0 ? 'fail' : 'ok'
|
|
4823
|
+
const head = h('button', {
|
|
4824
|
+
type: 'button',
|
|
4825
|
+
'aria-expanded': open,
|
|
4826
|
+
title: open ? t('consolidateCollapse') : t('consolidateExpand'),
|
|
4827
|
+
onClick: () => setOpen(v => !v),
|
|
4828
|
+
style: { display: 'flex', gap: 8, alignItems: 'center', width: '100%', padding: 0, border: 'none', background: 'transparent', cursor: 'pointer', textAlign: 'left', color: 'inherit', font: 'inherit' },
|
|
4829
|
+
},
|
|
4830
|
+
h('span', { style: { fontWeight: 600, fontSize: 12.5 } }, t('consolidateFeedTitle')),
|
|
4831
|
+
failures.length > 0 ? h(Tag, { tone: 'warn' }, t('consolidateFailedCount').replace('{n}', String(failures.length))) : null,
|
|
4832
|
+
h('span', { style: { ...S.muted, marginLeft: 'auto', fontSize: 11 } }, open ? t('consolidateCollapse') : t('consolidateExpand')))
|
|
4833
|
+
return h('div', { style: { ...S.section, padding: '10px 12px', marginTop: 12 }, role: 'status' },
|
|
4834
|
+
head,
|
|
4835
|
+
open
|
|
4836
|
+
? h('div', { style: { display: 'flex', gap: 14, marginTop: 7 } },
|
|
4837
|
+
column(t('consolidateFailedCol'), failures, 'fail'),
|
|
4838
|
+
column(t('consolidateOkCol'), successes, 'ok'))
|
|
4839
|
+
: h('div', { style: { marginTop: 7 } }, rowEl(latest, latestTone)))
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4579
4842
|
function MemoryTab({ ctx, t, sessionId }) {
|
|
4580
4843
|
const call = useRpc(ctx)
|
|
4581
4844
|
const sessionsState = useSessionList(ctx)
|
|
@@ -4646,14 +4909,6 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4646
4909
|
}, [sessionId, tabPage, sessionsLoaded, loaded, loadSidebarSessions])
|
|
4647
4910
|
|
|
4648
4911
|
React.useEffect(() => { void refresh() }, [refresh])
|
|
4649
|
-
// Instant path: another pane of this window wrote a memory (browser add /
|
|
4650
|
-
// save / archive / import, review resolution). Measured in milliseconds,
|
|
4651
|
-
// not in "reopen the tab".
|
|
4652
|
-
React.useEffect(() => {
|
|
4653
|
-
const onWrite = () => { void refresh() }
|
|
4654
|
-
memoryWriteListeners.add(onWrite)
|
|
4655
|
-
return () => { memoryWriteListeners.delete(onWrite) }
|
|
4656
|
-
}, [refresh])
|
|
4657
4912
|
// Slow path: writes we cannot hear about — the model calling
|
|
4658
4913
|
// memu_private_remember mid-turn, a curator job, a second window. The shared
|
|
4659
4914
|
// beat polls the cheap marker and refetches only when it moved; a hidden
|
|
@@ -4705,7 +4960,49 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4705
4960
|
return () => clearInterval(timer)
|
|
4706
4961
|
}, [busy, sessionId, loadRunLog])
|
|
4707
4962
|
|
|
4708
|
-
|
|
4963
|
+
// 整理提醒(跨会话):挂载即拉一次,之后慢轮询;手动整理结束/进行中时靠 loadRunLog
|
|
4964
|
+
// 的轮询节拍顺带刷新,不在这里叠加高频轮询。不依赖 sessionId —— 没选中对话也要显示。
|
|
4965
|
+
const [feed, setFeed] = React.useState(null)
|
|
4966
|
+
const loadFeed = React.useCallback(async () => {
|
|
4967
|
+
try { setFeed(await call('getConsolidationFeed', { limit: 6 })) } catch {}
|
|
4968
|
+
}, [call])
|
|
4969
|
+
React.useEffect(() => {
|
|
4970
|
+
void loadFeed()
|
|
4971
|
+
// unref:浏览器里没有这个方法(可选调用跳过),Node 测试环境里别让轮询拖住进程退出。
|
|
4972
|
+
const timer = setInterval(() => { void loadFeed() }, 20000)
|
|
4973
|
+
try { timer.unref?.() } catch {}
|
|
4974
|
+
return () => clearInterval(timer)
|
|
4975
|
+
}, [loadFeed])
|
|
4976
|
+
React.useEffect(() => {
|
|
4977
|
+
if (!busy) return undefined
|
|
4978
|
+
const timer = setInterval(() => { void loadFeed() }, 4000)
|
|
4979
|
+
try { timer.unref?.() } catch {}
|
|
4980
|
+
return () => clearInterval(timer)
|
|
4981
|
+
}, [busy, loadFeed])
|
|
4982
|
+
// Instant path: another pane wrote a memory — or the WebUI cleared the
|
|
4983
|
+
// activity log (kind: 'logs-cleared'): the consolidation feed must empty
|
|
4984
|
+
// the same instant instead of waiting for the next 20s poll.
|
|
4985
|
+
React.useEffect(() => {
|
|
4986
|
+
const onWrite = () => { void refresh(); void loadFeed() }
|
|
4987
|
+
memoryWriteListeners.add(onWrite)
|
|
4988
|
+
return () => { memoryWriteListeners.delete(onWrite) }
|
|
4989
|
+
}, [refresh, loadFeed])
|
|
4990
|
+
const jumpToLibrary = React.useCallback((row) => {
|
|
4991
|
+
// 对应记忆界面 = 这条记忆所在的库;没带 libraryId(全局/旧事件)就落到总览。
|
|
4992
|
+
openWebUiPage(row?.libraryId ? 'browser' : 'memories', { libraryId: row?.libraryId || null })
|
|
4993
|
+
}, [])
|
|
4994
|
+
const clearFailures = React.useCallback(async () => {
|
|
4995
|
+
// 只删 consolidate 且 ok=false 的行;成功流水与其余日志不动。结果直接回填 feed。
|
|
4996
|
+
try {
|
|
4997
|
+
const result = await call('clearConsolidationFailures')
|
|
4998
|
+
if (result?.cleared) setNotice(t('consolidateCleared').replace('{n}', String(result.cleared)))
|
|
4999
|
+
if (result?.feed) setFeed(result.feed)
|
|
5000
|
+
} catch (e) { setError(String(e?.message || e)) }
|
|
5001
|
+
}, [call, t])
|
|
5002
|
+
const alertBlock = h(ConsolidationAlerts, { feed, t, onJump: jumpToLibrary, onClear: clearFailures })
|
|
5003
|
+
|
|
5004
|
+
// (保持单行提前 return:tests/hook-order.test.mjs 以这一行锚定 hook 顺序守卫。)
|
|
5005
|
+
if (!sessionId) return h('div', { style: S.tab }, h('div', { style: { ...S.tab, padding: '12px' } }, t('loading')))
|
|
4709
5006
|
const binding = state?.binding
|
|
4710
5007
|
const summary = sessionsState.byId?.[sessionId]
|
|
4711
5008
|
const scopeKey = String(binding?.scopeKey || '')
|
|
@@ -4817,6 +5114,9 @@ const SOURCE_LABELS = { user_said: 'sourceUserSaid', inferred: 'sourceInferred',
|
|
|
4817
5114
|
.map((d) => h('option', { key: d, value: d }, domainText(t, d))))),
|
|
4818
5115
|
h('div', { style: { display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' } },
|
|
4819
5116
|
h(Button, { onClick: organize, disabled: busy, primary: true }, t('organize'))),
|
|
5117
|
+
// 整理提醒在记忆设置卡之下、整理记录之上:跨会话的失败提醒离整理入口最近,
|
|
5118
|
+
// 但不再压住页头。数据不按当前对话过滤,展开后失败/成功两列各看最新 6 条。
|
|
5119
|
+
alertBlock,
|
|
4820
5120
|
h('div', { style: { marginTop: 12 } },
|
|
4821
5121
|
h('div', { style: { ...S.muted, marginBottom: 4, fontWeight: 600 } }, t('runLogTitle')),
|
|
4822
5122
|
runLog.length === 0
|