dsh-cortex-memory 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -8,6 +8,34 @@ window.__ModuleLoader__.load({
8
8
  const NS = 'cortex';
9
9
  const API = '/api/cortex';
10
10
 
11
+ const FEATURE_META = {
12
+ recall: { zh: '自动召回', en: 'Auto recall', descZh: '每轮前查找相关记忆', descEn: 'Find relevant memories before each turn' },
13
+ capture: { zh: '自动捕获', en: 'Auto capture', descZh: '对话自动写入记忆', descEn: 'Write turns into memory' },
14
+ reflect: { zh: '反思整理', en: 'Reflection', descZh: '摘要、反思与巩固', descEn: 'Summaries, reflection and consolidation' },
15
+ growth: { zh: '成长评估', en: 'Growth', descZh: '评估阶段与成长指标', descEn: 'Evaluate stage and growth metrics' },
16
+ vector: { zh: '向量检索', en: 'Vector search', descZh: '相似记忆向量检索', descEn: 'Semantic vector retrieval' },
17
+ vectorAnn: { zh: '本地近似索引', en: 'Local ANN', descZh: 'HNSW 加速大量向量', descEn: 'HNSW acceleration for many vectors' },
18
+ vectorAnnExternal: { zh: '外置近似索引', en: 'External ANN', descZh: '调用外置 ANN 服务', descEn: 'Use an external ANN service' },
19
+ attachmentReader: { zh: '读取附件', en: 'Attachments', descZh: '读取本地附件内容', descEn: 'Read local attachment content' },
20
+ graph: { zh: '关系图', en: 'Graph', descZh: '时间关系图激活', descEn: 'Temporal graph activation' },
21
+ llm: { zh: '模型调用', en: 'LLM', descZh: '允许调用模型', descEn: 'Allow model calls' },
22
+ autoDiary: { zh: '自动日记', en: 'Auto diary', descZh: '每天 12:00 / 22:00 写', descEn: 'Write at 12:00 and 22:00' },
23
+ autoDream: { zh: '自动做梦', en: 'Auto dream', descZh: '每天 22:05 做一次', descEn: 'One dream at 22:05' },
24
+ autoFeeling: { zh: '自动感受', en: 'Auto feeling', descZh: '空闲时每小时写感受', descEn: 'Hourly feelings while idle' },
25
+ autoEvolve: { zh: '自动迭代', en: 'Auto evolve', descZh: '策略自动提案并激活', descEn: 'Auto-propose and activate policies' },
26
+ aiOnlyLife: { zh: 'AI 接管生活', en: 'AI-only life', descZh: '日记/梦只由 AI 写,不套模板', descEn: 'Diary/dream are AI-only, no template fallback' },
27
+ autoGraphCandidates: { zh: 'AI 接管候选边', en: 'Auto graph candidates', descZh: '高置信候选边自动入图', descEn: 'Approve high-confidence candidate edges' },
28
+ aiEdgeReview: { zh: 'AI 逐条审核边', en: 'AI edge review', descZh: '每条候选边由 LLM 判断', descEn: 'LLM reviews each candidate edge' },
29
+ webSearch: { zh: '联网资料', en: 'Web search', descZh: '日记/梦参考外部来源', descEn: 'Use external sources for diary/dreams' },
30
+ api: { zh: '本机接口', en: 'Local API', descZh: '本机 HTTP 接口,锁定开启', descEn: 'Local HTTP API, locked on' },
31
+ autoFacts: { zh: '自动事实', en: 'Auto facts', descZh: '写入时自动抽事实', descEn: 'Extract facts on write' },
32
+ llmFacts: { zh: '模型事实', en: 'LLM facts', descZh: '用模型抽取事实', descEn: 'Extract facts with the model' },
33
+ tieredRecall: { zh: '分层召回', en: 'Tiered recall', descZh: '按 perm / hot / cold 取记忆', descEn: 'Recall by perm / hot / cold tiers' },
34
+ consolidateOnSleep: { zh: '睡眠整理', en: 'Sleep consolidate', descZh: '睡一觉时整理记忆', descEn: 'Consolidate during sleep' },
35
+ consolidateOnTick: { zh: '心跳整理', en: 'Tick consolidate', descZh: '每次心跳整理一次', descEn: 'Consolidate on each heartbeat' },
36
+ rerank: { zh: '远程重排', en: 'Rerank', descZh: '模型对候选记忆重排', descEn: 'Rerank candidate memories' },
37
+ };
38
+
11
39
  const DICT = {
12
40
  zh: {
13
41
  navLabel: 'Cortex 记忆',
@@ -25,6 +53,7 @@ window.__ModuleLoader__.load({
25
53
  tabTimeline: '时间线',
26
54
  tabAudit: '审计',
27
55
  tabRetention: '留存',
56
+ tabMemoryEdit: '召唤册',
28
57
  tabWorking: '工作/目标',
29
58
  tabEmotion: '情绪',
30
59
  tabLife: '生活',
@@ -226,6 +255,8 @@ window.__ModuleLoader__.load({
226
255
  rerankHint: '不配置就回退原融合顺序;失败也回退。',
227
256
  permissionClick: '点击切换',
228
257
  permissionApiLocked: '本机接口不能从自身关闭',
258
+ permissionReadOnly: '只读快照',
259
+ autoEvolveNote: '自动迭代已开:策略会自动提案并激活,可回滚;身份变更仍单独审批。',
229
260
  vector: '向量',
230
261
  scope: 'scope',
231
262
  dataRoot: 'dataRoot',
@@ -288,6 +319,7 @@ window.__ModuleLoader__.load({
288
319
  tabTimeline: 'Timeline',
289
320
  tabAudit: 'Audit',
290
321
  tabRetention: 'Retention',
322
+ tabMemoryEdit: 'Summon book',
291
323
  tabWorking: 'Work/Goals',
292
324
  tabEmotion: 'Emotion',
293
325
  tabLife: 'Life',
@@ -489,6 +521,8 @@ window.__ModuleLoader__.load({
489
521
  rerankHint: 'Leave off to keep fusion order; failures fall back.',
490
522
  permissionClick: 'click to toggle',
491
523
  permissionApiLocked: 'The local API cannot disable itself from this panel',
524
+ permissionReadOnly: 'read-only snapshot',
525
+ autoEvolveNote: 'Autonomous evolution is on: policies can be proposed and activated automatically, with rollback. Identity changes still require separate approval.',
492
526
  vector: 'Vector',
493
527
  scope: 'scope',
494
528
  dataRoot: 'dataRoot',
@@ -552,6 +586,44 @@ window.__ModuleLoader__.load({
552
586
  .cortex-tab.on{color:var(--dsw-alias-brand-primary,#2f6fed);border-bottom-color:var(--dsw-alias-brand-primary,#2f6fed);font-weight:600}
553
587
  .cortex-panel{min-height:120px}
554
588
  .cortex-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}
589
+ .cortex-settings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;align-items:start}
590
+ .cortex-settings > .cortex-card--wide{grid-column:1/-1}
591
+ .cortex-feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:8px;margin-top:6px}
592
+ .cortex-feature{border:1px solid var(--dsw-alias-border-l1,#e5e7eb);background:var(--dsw-alias-bg-layer-2,#f7f8fa);border-radius:10px;padding:8px 10px;min-width:0}
593
+ .cortex-feature-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
594
+ .cortex-feature-name{font-size:12px;font-weight:600}
595
+ .cortex-feature-desc{font-size:11px;line-height:1.4;color:var(--dsw-alias-label-secondary,#6b7280);margin:3px 0 6px}
596
+ .cortex-feature .cortex-btn{padding:2px 8px;font-size:11px}
597
+ .cortex-memory-toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;justify-content:space-between;margin-bottom:12px}
598
+ .cortex-memory-layout{display:grid;grid-template-columns:210px minmax(0,1fr);gap:12px;align-items:start}
599
+ .cortex-memory-days{max-height:640px;overflow:auto;padding:8px}
600
+ .cortex-memory-day{appearance:none;border:0;background:transparent;display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;padding:7px 8px;border-radius:8px;color:var(--dsw-alias-label-primary,#1f2329);font:inherit;cursor:pointer;text-align:left}
601
+ .cortex-memory-day:hover{background:var(--dsw-alias-bg-layer-2,#f7f8fa)}
602
+ .cortex-memory-day.on{background:var(--dsw-alias-bg-layer-2,#f7f8fa);box-shadow:inset 2px 0 var(--dsw-alias-brand-primary,#2f6fed)}
603
+ .cortex-memory-emotion{border-left:3px solid #d97706}
604
+ .cortex-memory-work{border-left:3px solid var(--dsw-alias-brand-primary,#2f6fed)}
605
+ .cortex-memory-event{border-top:1px solid var(--dsw-alias-border-l1,#e5e7eb);padding:8px 0}
606
+ .cortex-memory-event:first-of-type{border-top:0}
607
+ .cortex-memory-trace{border-top:1px solid var(--dsw-alias-border-l1,#e5e7eb);padding:9px 0}
608
+ .cortex-memory-trace:first-of-type{border-top:0}
609
+ .cortex-memory-title{font-size:12px;font-weight:600;margin:2px 0 4px}
610
+ .cortex-memory-text{font-size:12px;line-height:1.55;white-space:pre-wrap;word-break:break-word;max-height:260px;overflow:auto}
611
+ .cortex-memory-edit-form{display:grid;gap:6px;margin:6px 0}
612
+ .cortex-memory-edit-form textarea{resize:vertical;min-height:120px}
613
+ .cortex-memory-extra{margin-top:10px;font-size:12px}
614
+ .cortex-memory-extra summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#6b7280)}
615
+ .cortex-summon-left{border-left:3px solid #db7c9b}
616
+ .cortex-summon-right{border-left:3px solid var(--dsw-alias-brand-primary,#2f6fed)}
617
+ .cortex-summon-card{border-top:1px solid var(--dsw-alias-border-l1,#e5e7eb);padding:4px 0 8px}
618
+ .cortex-summon-card:first-of-type{border-top:0}
619
+ .cortex-summon-title{appearance:none;border:0;background:transparent;padding:7px 4px;width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;text-align:left;cursor:pointer;font:inherit;color:inherit;border-radius:8px}
620
+ .cortex-summon-title:hover{background:var(--dsw-alias-bg-layer-2,#f7f8fa)}
621
+ .cortex-summon-title span:first-child{font-size:12px;font-weight:600}
622
+ .cortex-summon-body{padding:2px 4px 4px}
623
+ .cortex-summon-volume{margin-top:6px;font-size:12px}
624
+ .cortex-summon-volume summary{cursor:pointer;color:var(--dsw-alias-brand-primary,#2f6fed)}
625
+ .cortex-diagnostics .cortex-row{flex-wrap:wrap}
626
+ @media (max-width:900px){.cortex-settings{grid-template-columns:1fr}.cortex-settings > .cortex-card--wide{grid-column:auto}.cortex-memory-layout{grid-template-columns:1fr}.cortex-memory-days{max-height:200px}}
555
627
  .cortex-card{border:1px solid var(--dsw-alias-border-l1,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#fff);border-radius:12px;padding:12px 14px;min-width:0}
556
628
  .cortex-card h4{margin:0 0 6px;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);font-weight:600}
557
629
  .cortex-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
@@ -848,6 +920,19 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
848
920
  ]),
849
921
  h('div', { key: 'drives', className: 'cortex-card', style: { marginTop: 10 } }, [h('h4', {}, '驱力'), ...Object.entries(self.drives).map(([name, value]) => h(Meter, { key: name, label: name, value }))]),
850
922
  h('div', { key: 'id', className: 'cortex-card', style: { marginTop: 10 } }, [h('h4', {}, `身份 v${identity?.version ?? '—'}`), h('div', { className: 'cortex-pre' }, identity?.content?.narrative ?? '—')]),
923
+ h('div', { key: 'capacity', className: 'cortex-card', style: { marginTop: 10 } }, [
924
+ h('h4', {}, '容量上限'),
925
+ data.capacity ? h('div', {}, [
926
+ h('div', { key: 'l0', className: 'cortex-row' }, `人设 L0:${data.capacity.identityTokens} / ${data.capacity.identityBudget} tokens${data.capacity.identityTokens > data.capacity.identityBudget ? '(超出预算,注入会截断)' : ''}`),
927
+ h('div', { key: 'l1', className: 'cortex-row' }, `工作记忆 L1:${data.capacity.workingBudget} tokens`),
928
+ h('div', { key: 'prompt', className: 'cortex-row' }, `系统提示总预算:${data.capacity.promptBudget} tokens`),
929
+ h('div', { key: 'ctx', className: 'cortex-row' }, `检索上下文:${data.capacity.contextBudget} tokens`),
930
+ h('div', { key: 'tiers', className: 'cortex-row' }, `分层召回:perm ${data.capacity.tiers?.perm} / hot ${data.capacity.tiers?.hot} / cold ${data.capacity.tiers?.cold} tokens`),
931
+ h('div', { key: 'search', className: 'cortex-row' }, `单次检索:${data.capacity.resultLimit} 条;FTS 候选 ${data.capacity.ftsLimit} 条`),
932
+ h('div', { key: 'shape', className: 'cortex-row' }, `人设结构:values ≤ ${data.capacity.valuesLimit};boundaries ≤ ${data.capacity.boundariesLimit}`),
933
+ h('div', { key: 'l23', className: 'cortex-note' }, 'L2 火花卡 / L3 专题没有固定条数上限,受检索、注入预算和每轮选择约束。'),
934
+ ]) : h('div', { className: 'cortex-note' }, '容量数据暂不可用。'),
935
+ ]),
851
936
  ]);
852
937
  }
853
938
 
@@ -949,7 +1034,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
949
1034
  h(Field, { key: 'tag', label: t('filterTag'), hint: t('filterTagsHint') }, h('input', {
950
1035
  className: 'cortex-input',
951
1036
  value: filters.tag,
952
- placeholder: '白饭, 主人',
1037
+ placeholder: '主题, 对象',
953
1038
  'aria-label': t('filterTag'),
954
1039
  onChange: (event) => change('tag', event.target.value),
955
1040
  onKeyDown: enter,
@@ -1626,52 +1711,81 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1626
1711
 
1627
1712
  function EmotionTab({ t }) {
1628
1713
  const [data, setData] = useState(null);
1629
- const [text, setText] = useState('');
1630
1714
  const [error, setError] = useState('');
1715
+ const [busy, setBusy] = useState(false);
1631
1716
  const load = useCallback(async () => { try { setData(await api('/emotion')); } catch (issue) { setError(String(issue.message ?? issue)); } }, []);
1632
1717
  useEffect(() => { load(); }, [load]);
1633
- const record = async () => {
1634
- if (!text.trim()) return;
1635
- try { await api('/emotion/record', { method: 'POST', body: JSON.stringify({ text, target: '主人' }) }); setText(''); load(); } catch (issue) { setError(String(issue.message ?? issue)); }
1718
+ const generate = async () => {
1719
+ if (busy) return;
1720
+ setBusy(true);
1721
+ try { await api('/emotion/generate', { method: 'POST', body: '{}' }); await load(); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
1636
1722
  };
1637
1723
  return h('div', {}, [
1638
1724
  error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed') }) : null,
1639
1725
  h('div', { key: 'line', className: 'cortex-line' }, [
1640
- h('input', { key: 'i', className: 'cortex-input', value: text, placeholder: '本鲸现在有点…', 'aria-label': '情绪记录', onChange: (event) => setText(event.target.value), onKeyDown: (event) => { if (event.key === 'Enter') record(); } }),
1641
- h('button', { key: 'b', type: 'button', className: 'cortex-btn primary', onClick: record }, '记一下'),
1726
+ h('button', { key: 'b', type: 'button', className: 'cortex-btn primary', disabled: busy, onClick: generate }, busy ? '本鲸在感受…' : '让本鲸感受一下'),
1727
+ h('span', { key: 'note', className: 'cortex-meta' }, '空闲时每小时自动写一次;感受由本鲸自己判断'),
1642
1728
  ]),
1643
1729
  data ? h('div', {}, [
1644
1730
  h('div', { key: 't', className: 'cortex-card' }, [h('h4', {}, '情感态度'), h('div', { className: 'cortex-big' }, data.text)]),
1645
1731
  h('div', { key: 'targets', className: 'cortex-card', style: { marginTop: 10 } }, [h('h4', {}, '对象'), ...(data.targets.length ? data.targets.map((item) => h('div', { key: item.target, className: 'cortex-row' }, `${item.target} · ${item.attitude} · ${item.count} 次`)) : [h('div', { key: 'none', className: 'cortex-empty' }, t('empty'))])]),
1646
- h('div', { key: 'events', className: 'cortex-card', style: { marginTop: 10 } }, [h('h4', {}, '最近情绪事件'), ...(data.events.length ? data.events.slice(0, 8).map((item) => h('div', { key: item.id, className: 'cortex-row' }, `${item.emotions.map((emotion) => emotion.name).join('、')} · ${item.text.slice(0, 60)}`)) : [h('div', { key: 'none', className: 'cortex-empty' }, t('empty'))])]),
1732
+ h('div', { key: 'events', className: 'cortex-card', style: { marginTop: 10 } }, [h('h4', {}, '最近感受'), ...(data.events.length ? data.events.slice(0, 8).map((item) => h('div', { key: item.id, className: 'cortex-row' }, `${item.emotions.map((emotion) => emotion.name).join('、')} · ${item.text.slice(0, 120)}`)) : [h('div', { key: 'none', className: 'cortex-empty' }, t('empty'))])]),
1647
1733
  ]) : h(LoadingBlock, { key: 'loading', label: t('loading') }),
1648
1734
  ]);
1649
1735
  }
1650
1736
 
1651
1737
  function LifeTab({ t }) {
1652
1738
  const [data, setData] = useState(null);
1653
- const [text, setText] = useState('');
1654
1739
  const [error, setError] = useState('');
1740
+ const [busy, setBusy] = useState(false);
1741
+ const settings = useEndpoint('/settings');
1655
1742
  const load = useCallback(async () => { try { setData(await api('/life')); } catch (issue) { setError(String(issue.message ?? issue)); } }, []);
1656
1743
  useEffect(() => { load(); }, [load]);
1657
- const diary = async () => {
1658
- if (!text.trim()) return;
1659
- try { await api('/life/diary', { method: 'POST', body: JSON.stringify({ text }) }); setText(''); load(); } catch (issue) { setError(String(issue.message ?? issue)); }
1744
+ const autoValue = (key) => settings.data?.overrides?.[key] ?? settings.data?.features?.[key] ?? false;
1745
+ const toggleAuto = async (key) => {
1746
+ if (!window.confirm(t('permissionConfirm'))) return;
1747
+ try {
1748
+ await api('/permissions', { method: 'POST', body: JSON.stringify({ key, value: !autoValue(key), confirm: true }) });
1749
+ settings.reload();
1750
+ } catch (issue) { setError(String(issue.message ?? issue)); }
1751
+ };
1752
+ const writeDiary = async () => {
1753
+ if (busy) return;
1754
+ setBusy(true);
1755
+ try { await api('/life/diary/generate', { method: 'POST', body: '{}' }); await load(); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
1756
+ };
1757
+ const dream = async () => {
1758
+ if (busy) return;
1759
+ setBusy(true);
1760
+ try { await api('/life/dream', { method: 'POST', body: '{}' }); await load(); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
1761
+ };
1762
+ const sleep = async () => {
1763
+ if (busy) return;
1764
+ setBusy(true);
1765
+ try { await api('/life/sleep', { method: 'POST', body: '{}' }); await load(); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
1660
1766
  };
1661
- const dream = async () => { try { await api('/life/dream', { method: 'POST', body: '{}' }); load(); } catch (issue) { setError(String(issue.message ?? issue)); } };
1662
- const sleep = async () => { try { await api('/life/sleep', { method: 'POST', body: '{}' }); load(); } catch (issue) { setError(String(issue.message ?? issue)); } };
1663
1767
  return h('div', {}, [
1664
1768
  error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed') }) : null,
1665
- h('div', { key: 'line', className: 'cortex-line' }, [h('button', { key: 'd', type: 'button', className: 'cortex-btn', onClick: dream }, '做个梦'), h('button', { key: 's', type: 'button', className: 'cortex-btn', onClick: sleep }, '睡一觉')]),
1769
+ h('div', { key: 'line', className: 'cortex-line' }, [h('button', { key: 'd', type: 'button', className: 'cortex-btn', disabled: busy, onClick: dream }, busy ? '梦里…' : '做个梦'), h('button', { key: 's', type: 'button', className: 'cortex-btn', disabled: busy, onClick: sleep }, busy ? '睡着…' : '睡一觉'), h('button', { key: 'ai', type: 'button', className: 'cortex-btn', onClick: () => toggleAuto('aiOnlyLife') }, `不用模板:${autoValue('aiOnlyLife') ? '开' : '关'}`)]),
1666
1770
  h('div', { key: 'diary', className: 'cortex-card' }, [
1667
1771
  h('h4', {}, '日记'),
1668
1772
  h('div', { key: 'line', className: 'cortex-line' }, [
1669
- h('input', { key: 'i', className: 'cortex-input', value: text, placeholder: '今天想记住…', 'aria-label': '日记', onChange: (event) => setText(event.target.value), onKeyDown: (event) => { if (event.key === 'Enter') diary(); } }),
1670
- h('button', { key: 'b', type: 'button', className: 'cortex-btn primary', onClick: diary }, '写'),
1773
+ h('button', { key: 'b', type: 'button', className: 'cortex-btn primary', disabled: busy, onClick: writeDiary }, busy ? '本鲸在想…' : '让本鲸写日记'),
1774
+ h('button', { key: 'auto', type: 'button', className: 'cortex-btn', onClick: () => toggleAuto('autoDiary') }, `AI 接管日记:${autoValue('autoDiary') ? '开' : '关'}`),
1775
+ h('button', { key: 'web', type: 'button', className: 'cortex-btn', onClick: () => toggleAuto('webSearch') }, `联网:${autoValue('webSearch') ? '开' : '关'}`),
1776
+ h('span', { key: 'note', className: 'cortex-meta' }, '本鲸自己思考后写;不是主人代笔'),
1671
1777
  ]),
1672
1778
  ...((data?.diary ?? []).length ? data.diary.map((item) => h('div', { key: item.id, className: 'cortex-row' }, `${item.at.slice(0, 10)} ${item.text}`)) : [h('div', { key: 'none', className: 'cortex-empty' }, t('empty'))]),
1673
1779
  ]),
1674
- h('div', { key: 'dreams', className: 'cortex-card', style: { marginTop: 10 } }, [h('h4', {}, '梦'), ...((data?.dreams ?? []).length ? data.dreams.map((item) => h('div', { key: item.id, className: 'cortex-snippet' }, item.text)) : [h('div', { key: 'none', className: 'cortex-empty' }, t('empty'))])]),
1780
+ h('div', { key: 'dreams', className: 'cortex-card', style: { marginTop: 10 } }, [
1781
+ h('h4', {}, '梦'),
1782
+ h('div', { key: 'auto-line', className: 'cortex-line' }, [
1783
+ h('button', { key: 'auto', type: 'button', className: 'cortex-btn', onClick: () => toggleAuto('autoDream') }, `AI 接管梦:${autoValue('autoDream') ? '开' : '关'}`),
1784
+ h('button', { key: 'web', type: 'button', className: 'cortex-btn', onClick: () => toggleAuto('webSearch') }, `联网:${autoValue('webSearch') ? '开' : '关'}`),
1785
+ h('span', { key: 'note', className: 'cortex-meta' }, '22:05 自动生成;失败不回退模板'),
1786
+ ]),
1787
+ ...((data?.dreams ?? []).length ? data.dreams.map((item) => h('div', { key: item.id, className: 'cortex-snippet' }, item.text)) : [h('div', { key: 'none', className: 'cortex-empty' }, t('empty'))]),
1788
+ ]),
1675
1789
  ]);
1676
1790
  }
1677
1791
 
@@ -1818,6 +1932,9 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1818
1932
  const { data, error, busy, reload } = useEndpoint('/graph');
1819
1933
  const { data: edgeData } = useEndpoint('/graph/edges');
1820
1934
  const candidates = useEndpoint('/graph/candidates');
1935
+ const settings = useEndpoint('/settings');
1936
+ const autoGraph = settings.data?.overrides?.autoGraphCandidates ?? settings.data?.features?.autoGraphCandidates ?? false;
1937
+ const aiEdgeReview = settings.data?.overrides?.aiEdgeReview ?? settings.data?.features?.aiEdgeReview ?? false;
1821
1938
  const [candidateNotice, setCandidateNotice] = useState('');
1822
1939
  const [candidateBusy, setCandidateBusy] = useState('');
1823
1940
  const top = Array.isArray(data?.top) ? data.top : [];
@@ -1825,10 +1942,11 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1825
1942
  const positions = new Map(layout.map((node) => [node.id, node]));
1826
1943
  const edges = Array.isArray(edgeData?.edges) ? edgeData.edges.filter((edge) => positions.has(edge.from) && positions.has(edge.to)).slice(0, 120) : [];
1827
1944
  const pendingEdges = Array.isArray(candidates.data) ? candidates.data : [];
1828
- const shortLabel = (id) => {
1829
- const value = String(id ?? '');
1830
- return value.length > 18 ? `${value.slice(0, 16)}…` : value;
1945
+ const shortLabel = (value) => {
1946
+ const text = String(value ?? '');
1947
+ return text.length > 18 ? `${text.slice(0, 16)}…` : text;
1831
1948
  };
1949
+ const labelOf = (id) => shortLabel(positions.get(id)?.name || id);
1832
1950
  const decideCandidate = async (id, action) => {
1833
1951
  setCandidateBusy(id);
1834
1952
  setCandidateNotice('');
@@ -1843,10 +1961,42 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1843
1961
  setCandidateBusy('');
1844
1962
  }
1845
1963
  };
1964
+ const toggleAutoGraph = async () => {
1965
+ if (!window.confirm(t('permissionConfirm'))) return;
1966
+ try {
1967
+ await api('/permissions', { method: 'POST', body: JSON.stringify({ key: 'autoGraphCandidates', value: !autoGraph, confirm: true }) });
1968
+ settings.reload();
1969
+ } catch (issue) {
1970
+ setCandidateNotice(String(issue.message ?? issue));
1971
+ }
1972
+ };
1973
+ const toggleAiEdgeReview = async () => {
1974
+ if (!window.confirm(t('permissionConfirm'))) return;
1975
+ try {
1976
+ await api('/permissions', { method: 'POST', body: JSON.stringify({ key: 'aiEdgeReview', value: !aiEdgeReview, confirm: true }) });
1977
+ settings.reload();
1978
+ } catch (issue) {
1979
+ setCandidateNotice(String(issue.message ?? issue));
1980
+ }
1981
+ };
1982
+ const labelEntities = async () => {
1983
+ setCandidateBusy('entity-label');
1984
+ setCandidateNotice('');
1985
+ try {
1986
+ const result = await api('/entities/label', { method: 'POST', body: JSON.stringify({ limit: 5 }) });
1987
+ setCandidateNotice(`已审阅 ${result.scanned ?? 0} 个坏名,改名 ${result.count ?? 0} 个`);
1988
+ reload();
1989
+ } catch (issue) {
1990
+ setCandidateNotice(String(issue.message ?? issue));
1991
+ } finally {
1992
+ setCandidateBusy('');
1993
+ }
1994
+ };
1846
1995
  return h('div', {}, [
1847
1996
  h('div', { key: 'hint', className: 'cortex-note' }, t('graphHint')),
1848
1997
  h('div', { key: 'line', className: 'cortex-line' }, [
1849
1998
  h('button', { key: 'r', type: 'button', className: 'cortex-btn', disabled: busy, onClick: reload }, t('refresh')),
1999
+ h('button', { key: 'label', type: 'button', className: 'cortex-btn', disabled: candidateBusy === 'entity-label', onClick: labelEntities }, '自动命名实体'),
1850
2000
  ]),
1851
2001
  error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed'), onRetry: reload, retryLabel: t('retry') }) : null,
1852
2002
  busy ? h(LoadingBlock, { key: 'loading', label: t('loading') }) : null,
@@ -1888,14 +2038,14 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1888
2038
  });
1889
2039
  }),
1890
2040
  ...layout.map((node) => h('g', { key: node.id, className: 'cortex-graph-node' }, [
1891
- h('title', { key: 't' }, `${node.id} · ${t('graphRank')} ${formatNumber(node.score, 5)}`),
2041
+ h('title', { key: 't' }, `${node.name || node.id} · ${t('graphRank')} ${formatNumber(node.score, 5)}`),
1892
2042
  h('circle', { key: 'c', className: 'cortex-graph-dot', cx: node.x, cy: node.y, r: node.r }),
1893
- h('text', { key: 'l', className: 'cortex-graph-label', x: node.x, y: node.y + node.r + 13, textAnchor: 'middle' }, shortLabel(node.id)),
2043
+ h('text', { key: 'l', className: 'cortex-graph-label', x: node.x, y: node.y + node.r + 13, textAnchor: 'middle' }, labelOf(node.id)),
1894
2044
  ])),
1895
2045
  ]),
1896
2046
  h('div', { key: 'note', className: 'cortex-meta' }, t('graphScaleNote')),
1897
2047
  h('div', { key: 'top', className: 'cortex-graph-top' }, top.map((node) => h('div', { key: node.id, className: 'cortex-graph-top-row' }, [
1898
- h('span', { key: 'i', title: node.id }, node.id),
2048
+ h('span', { key: 'i', title: node.id }, node.name || node.id),
1899
2049
  h('span', { key: 's' }, formatNumber(node.score, 5)),
1900
2050
  ]))),
1901
2051
  ]) : null,
@@ -1906,6 +2056,11 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1906
2056
  h('span', { key: 'count', className: 'cortex-meta' }, pendingEdges.length),
1907
2057
  ]),
1908
2058
  h('div', { key: 'hint', className: 'cortex-meta' }, t('graphCandidatesHint')),
2059
+ h('div', { key: 'ai-line', className: 'cortex-line' }, [
2060
+ h('button', { key: 'ai', type: 'button', className: 'cortex-btn', onClick: toggleAutoGraph }, `AI 接管候选边:${autoGraph ? '开' : '关'}`),
2061
+ h('button', { key: 'ai-review', type: 'button', className: 'cortex-btn', onClick: toggleAiEdgeReview }, `AI 全权审核:${aiEdgeReview ? '开' : '关'}`),
2062
+ h('span', { key: 'ai-note', className: 'cortex-meta' }, '全权审核会逐条让 LLM 判断;可回滚'),
2063
+ ]),
1909
2064
  candidates.busy ? h(LoadingBlock, { key: 'loading', label: t('loading') }) : null,
1910
2065
  pendingEdges.length ? h('div', { key: 'list', style: { marginTop: 6 } }, pendingEdges.map((edge) => h('div', {
1911
2066
  key: edge.id,
@@ -1913,7 +2068,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
1913
2068
  style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
1914
2069
  }, [
1915
2070
  h('span', { key: 'label', style: { flex: '1 1 240px', minWidth: 0 } },
1916
- `${shortLabel(edge.fromId)} → ${shortLabel(edge.toId)} · ${edge.rel} · ${t('graphCandidateConfidence')} ${formatNumber(edge.metadata?.confidence ?? edge.weight ?? 0, 2)}`),
2071
+ `${shortLabel(edge.fromName || positions.get(edge.fromId)?.name || edge.fromId)} → ${shortLabel(edge.toName || positions.get(edge.toId)?.name || edge.toId)} · ${edge.rel} · ${t('graphCandidateConfidence')} ${formatNumber(edge.metadata?.confidence ?? edge.weight ?? 0, 2)}`),
1917
2072
  h('button', {
1918
2073
  key: 'approve',
1919
2074
  type: 'button',
@@ -2017,6 +2172,366 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2017
2172
  ]);
2018
2173
  }
2019
2174
 
2175
+ function MemoryEditTab({ t }) {
2176
+ const days = useEndpoint('/memory/days?limit=60');
2177
+ const lock = useEndpoint('/memory/edit');
2178
+ const [selected, setSelected] = useState('');
2179
+ const [day, setDay] = useState(null);
2180
+ const [error, setError] = useState('');
2181
+ const [busy, setBusy] = useState(false);
2182
+ const [editing, setEditing] = useState(null);
2183
+ const [draft, setDraft] = useState({ title: '', content: '', tags: '' });
2184
+ const [editingEmotion, setEditingEmotion] = useState(null);
2185
+ const [emotionDraft, setEmotionDraft] = useState({ text: '', target: '' });
2186
+ const [emotionOpen, setEmotionOpen] = useState(false);
2187
+ const [workOpen, setWorkOpen] = useState(true);
2188
+ const unlocked = lock.data?.enabled === true;
2189
+ const dayList = Array.isArray(days.data) ? days.data : [];
2190
+ const loadDay = useCallback(async (value) => {
2191
+ if (!value) { setDay(null); return; }
2192
+ setBusy(true); setError('');
2193
+ try { setDay(await api(`/memory/day?date=${encodeURIComponent(value)}`)); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
2194
+ }, []);
2195
+ useEffect(() => {
2196
+ if (!selected && dayList.length) setSelected(dayList[0].date);
2197
+ }, [selected, dayList]);
2198
+ useEffect(() => { loadDay(selected); }, [selected, loadDay]);
2199
+ useEffect(() => {
2200
+ if (!selected) return undefined;
2201
+ const sync = () => { loadDay(selected); };
2202
+ const timer = setInterval(sync, 10_000);
2203
+ const onFocus = () => { sync(); days.reload(); };
2204
+ window.addEventListener('focus', onFocus);
2205
+ return () => { clearInterval(timer); window.removeEventListener('focus', onFocus); };
2206
+ }, [selected, loadDay, days.reload]);
2207
+ const moveDay = (offset) => {
2208
+ const index = dayList.findIndex((item) => item.date === selected);
2209
+ const next = dayList[index + offset];
2210
+ if (next) setSelected(next.date);
2211
+ };
2212
+ const toggleLock = async () => {
2213
+ if (!window.confirm(unlocked ? '关闭编辑权限,恢复只读?' : '打开编辑权限?开启后可以修改记忆正文;会写入审计。')) return;
2214
+ try { await api('/memory/edit', { method: 'POST', body: JSON.stringify({ enabled: !unlocked }) }); lock.reload(); } catch (issue) { setError(String(issue.message ?? issue)); }
2215
+ };
2216
+ const startEdit = (trace) => {
2217
+ if (!unlocked) return;
2218
+ setEditing(trace.id);
2219
+ setDraft({ title: trace.title ?? '', content: trace.content ?? '', tags: (trace.tags ?? []).join(', ') });
2220
+ };
2221
+ const saveEdit = async () => {
2222
+ if (!editing || !unlocked) return;
2223
+ setBusy(true); setError('');
2224
+ try {
2225
+ await api('/memory/trace', { method: 'POST', body: JSON.stringify({
2226
+ id: editing,
2227
+ title: draft.title,
2228
+ content: draft.content,
2229
+ tags: draft.tags.split(/[,,\s]+/).filter(Boolean),
2230
+ }) });
2231
+ setEditing(null);
2232
+ await loadDay(selected);
2233
+ } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
2234
+ };
2235
+ const startEmotionEdit = (event) => {
2236
+ if (!unlocked) return;
2237
+ setEditingEmotion(event.id);
2238
+ setEmotionDraft({ text: event.text ?? '', target: event.target ?? '' });
2239
+ };
2240
+ const saveEmotionEdit = async () => {
2241
+ if (!editingEmotion || !unlocked) return;
2242
+ setBusy(true); setError('');
2243
+ try {
2244
+ await api('/memory/emotion', { method: 'POST', body: JSON.stringify({ id: editingEmotion, ...emotionDraft }) });
2245
+ setEditingEmotion(null);
2246
+ await loadDay(selected);
2247
+ } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
2248
+ };
2249
+ const emotionEvents = day?.emotion ?? [];
2250
+ const work = day?.work ?? { traces: [], facts: [], summaries: [], diary: [], dreams: [] };
2251
+ const counts = day?.counts ?? {};
2252
+ return h('div', { className: 'cortex-memory-edit' }, [
2253
+ error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed') }) : null,
2254
+ h('div', { key: 'toolbar', className: 'cortex-memory-toolbar' }, [
2255
+ h('div', { key: 'lock', className: 'cortex-line' }, [
2256
+ h('span', { key: 'label', className: `cortex-badge${unlocked ? ' on' : ''}` }, `编辑权限:${unlocked ? '开' : '关(只读)'}`),
2257
+ h('button', { key: 'toggle', type: 'button', className: 'cortex-btn', onClick: toggleLock }, unlocked ? '关闭编辑' : '开启编辑权限'),
2258
+ h('span', { key: 'hint', className: 'cortex-meta' }, '关闭时只能查询;开启后可改正文,改动进审计'),
2259
+ ]),
2260
+ h('div', { key: 'nav', className: 'cortex-line' }, [
2261
+ h('button', { key: 'prev', type: 'button', className: 'cortex-btn', onClick: () => moveDay(1), disabled: !dayList.length }, '上一天'),
2262
+ h('input', { key: 'date', type: 'date', className: 'cortex-input', value: selected, onChange: (event) => setSelected(event.target.value), 'aria-label': '选择日期' }),
2263
+ h('button', { key: 'next', type: 'button', className: 'cortex-btn', onClick: () => moveDay(-1), disabled: !dayList.length }, '下一天'),
2264
+ h('button', { key: 'refresh', type: 'button', className: 'cortex-btn', onClick: () => { days.reload(); loadDay(selected); } }, t('refresh')),
2265
+ h('span', { key: 'sync', className: 'cortex-meta' }, '自动同步 10 秒;编辑后立即刷新'),
2266
+ ]),
2267
+ ]),
2268
+ h('div', { key: 'layout', className: 'cortex-memory-layout' }, [
2269
+ h('aside', { key: 'days', className: 'cortex-card cortex-memory-days' }, [
2270
+ h('h4', {}, '按天'),
2271
+ dayList.length ? dayList.map((item) => h('button', {
2272
+ key: item.date,
2273
+ type: 'button',
2274
+ className: `cortex-memory-day${item.date === selected ? ' on' : ''}`,
2275
+ onClick: () => setSelected(item.date),
2276
+ }, [
2277
+ h('span', { key: 'd' }, item.date),
2278
+ h('span', { key: 'c', className: 'cortex-meta' }, `情 ${item.emotion} · 工 ${item.work}`),
2279
+ ])) : h('div', { className: 'cortex-empty' }, '还没有按天记录'),
2280
+ dayList.length ? h('div', { key: 'earliest', className: 'cortex-note', style: { marginTop: 8 } }, `最早可查 ${dayList[dayList.length - 1].date};Cortex 初始化前没有原始痕迹。`) : null,
2281
+ ]),
2282
+ h('div', { key: 'main', className: 'cortex-memory-main' }, [
2283
+ busy ? h(LoadingBlock, { key: 'loading', label: t('loading') }) : null,
2284
+ h('section', { key: 'emotion', className: 'cortex-card cortex-memory-emotion' }, [
2285
+ h('div', { key: 'head', className: 'cortex-card-head' }, [
2286
+ h('h4', {}, `情感 · ${selected || '—'}`),
2287
+ h('div', { key: 'actions', className: 'cortex-line' }, [
2288
+ h('span', { key: 'count', className: 'cortex-meta' }, counts.emotion ?? 0),
2289
+ h('button', { key: 'toggle', type: 'button', className: 'cortex-btn', onClick: () => setEmotionOpen(!emotionOpen) }, emotionOpen ? '收起情感' : '展开情感'),
2290
+ ]),
2291
+ ]),
2292
+ !emotionOpen ? h('div', { key: 'collapsed', className: 'cortex-note' }, `情感已收起(${counts.emotion ?? 0} 条);展开后再看,下面是工作记忆。`) : emotionEvents.length ? emotionEvents.map((event) => h('div', { key: event.id, className: 'cortex-memory-event' }, [
2293
+ h('div', { key: 'meta', className: 'cortex-line' }, [
2294
+ h('span', { key: 'time', className: 'cortex-meta' }, formatTime(event.at)),
2295
+ h('span', { key: 'target', className: 'cortex-badge' }, event.target ?? '—'),
2296
+ ...(event.emotions ?? []).map((emotion) => h('span', { key: `${event.id}-${emotion.name}`, className: 'cortex-badge on' }, `${emotion.name}${emotion.score ? ` ${emotion.score}` : ''}`)),
2297
+ unlocked ? h('button', { key: 'edit', type: 'button', className: 'cortex-btn', onClick: () => startEmotionEdit(event) }, '编辑') : null,
2298
+ ]),
2299
+ editingEmotion === event.id ? h('div', { key: 'form', className: 'cortex-memory-edit-form' }, [
2300
+ h('input', { key: 'target', className: 'cortex-input', value: emotionDraft.target, onChange: (e) => setEmotionDraft({ ...emotionDraft, target: e.target.value }), placeholder: '对象' }),
2301
+ h('textarea', { key: 'text', className: 'cortex-input', rows: 4, value: emotionDraft.text, onChange: (e) => setEmotionDraft({ ...emotionDraft, text: e.target.value }), placeholder: '感受正文' }),
2302
+ h('div', { key: 'buttons', className: 'cortex-line' }, [
2303
+ h('button', { key: 'save', type: 'button', className: 'cortex-btn primary', disabled: busy, onClick: saveEmotionEdit }, '保存修改'),
2304
+ h('button', { key: 'cancel', type: 'button', className: 'cortex-btn', onClick: () => setEditingEmotion(null) }, '取消'),
2305
+ ]),
2306
+ ]) : h('div', { key: 'text', className: 'cortex-memory-text' }, event.text),
2307
+ ])) : h('div', { key: 'empty', className: 'cortex-empty' }, '这天没有情感记录'),
2308
+ ]),
2309
+ h('section', { key: 'work', className: 'cortex-card cortex-memory-work' }, [
2310
+ h('div', { key: 'head', className: 'cortex-card-head' }, [
2311
+ h('h4', {}, `工作记忆 · ${selected || '—'}`),
2312
+ h('div', { key: 'actions', className: 'cortex-line' }, [
2313
+ h('span', { key: 'count', className: 'cortex-meta' }, `${counts.traces ?? 0} 痕迹`),
2314
+ h('button', { key: 'toggle', type: 'button', className: 'cortex-btn', onClick: () => setWorkOpen(!workOpen) }, workOpen ? '收起工作' : '展开工作'),
2315
+ ]),
2316
+ ]),
2317
+ !workOpen ? h('div', { key: 'collapsed', className: 'cortex-note' }, `工作记忆已收起(${counts.traces ?? 0} 条痕迹);点“展开工作”查看。`) : h('div', { key: 'body' }, [
2318
+ work.traces.length ? work.traces.map((trace) => h('div', { key: trace.id, className: 'cortex-memory-trace' }, [
2319
+ h('div', { key: 'meta', className: 'cortex-line' }, [
2320
+ h('span', { key: 'time', className: 'cortex-meta' }, formatTime(trace.recordedAt)),
2321
+ h('span', { key: 'kind', className: 'cortex-badge' }, trace.kind),
2322
+ h('span', { key: 'imp', className: 'cortex-meta' }, `重要度 ${formatNumber(trace.importance, 2)}`),
2323
+ unlocked ? h('button', { key: 'edit', type: 'button', className: 'cortex-btn', onClick: () => startEdit(trace) }, '编辑') : h('span', { key: 'ro', className: 'cortex-meta' }, '只读'),
2324
+ ]),
2325
+ h('div', { key: 'title', className: 'cortex-memory-title' }, trace.title || '(无标题)'),
2326
+ editing === trace.id ? h('div', { key: 'form', className: 'cortex-memory-edit-form' }, [
2327
+ h('input', { key: 'title', className: 'cortex-input', value: draft.title, onChange: (event) => setDraft({ ...draft, title: event.target.value }), placeholder: '标题' }),
2328
+ h('textarea', { key: 'content', className: 'cortex-input', rows: 8, value: draft.content, onChange: (event) => setDraft({ ...draft, content: event.target.value }), placeholder: '正文' }),
2329
+ h('input', { key: 'tags', className: 'cortex-input', value: draft.tags, onChange: (event) => setDraft({ ...draft, tags: event.target.value }), placeholder: '标签,用逗号分隔' }),
2330
+ h('div', { key: 'buttons', className: 'cortex-line' }, [
2331
+ h('button', { key: 'save', type: 'button', className: 'cortex-btn primary', disabled: busy, onClick: saveEdit }, '保存修改'),
2332
+ h('button', { key: 'cancel', type: 'button', className: 'cortex-btn', onClick: () => setEditing(null) }, '取消'),
2333
+ ]),
2334
+ ]) : h('div', { key: 'text', className: 'cortex-memory-text' }, trace.content),
2335
+ (trace.tags ?? []).length ? h('div', { key: 'tags', className: 'cortex-chips' }, trace.tags.map((tag) => h('span', { key: tag, className: 'cortex-badge' }, tag))) : null,
2336
+ ])) : h('div', { key: 'empty', className: 'cortex-empty' }, '这天没有工作记忆'),
2337
+ h('details', { key: 'facts', className: 'cortex-memory-extra' }, [
2338
+ h('summary', {}, `事实 ${counts.facts ?? 0} · 摘要 ${counts.summaries ?? 0} · 日记 ${counts.diary ?? 0} · 梦 ${counts.dreams ?? 0}`),
2339
+ ...(work.facts ?? []).map((fact) => h('div', { key: fact.id, className: 'cortex-row' }, `${fact.subjectId ?? '—'} ${fact.predicate} ${fact.objectLiteral ?? fact.objectId ?? '—'}`)),
2340
+ ...(work.summaries ?? []).map((summary) => h('div', { key: summary.id, className: 'cortex-snippet' }, summary.content)),
2341
+ ...(work.diary ?? []).map((item) => h('div', { key: item.id, className: 'cortex-snippet' }, item.text)),
2342
+ ...(work.dreams ?? []).map((item) => h('div', { key: item.id, className: 'cortex-snippet' }, item.text)),
2343
+ ]),
2344
+ ]),
2345
+ ]),
2346
+ ]),
2347
+ ]),
2348
+ ]);
2349
+ }
2350
+
2351
+ function SummonBookTab({ t }) {
2352
+ const lock = useEndpoint('/memory/edit');
2353
+ const [book, setBook] = useState(null);
2354
+ const [selected, setSelected] = useState('');
2355
+ const [error, setError] = useState('');
2356
+ const [busy, setBusy] = useState(false);
2357
+ const [leftOpen, setLeftOpen] = useState(true);
2358
+ const [rightOpen, setRightOpen] = useState(true);
2359
+ const [volumesOpen, setVolumesOpen] = useState(false);
2360
+ const [openCard, setOpenCard] = useState(null);
2361
+ const [editing, setEditing] = useState(null);
2362
+ const [draft, setDraft] = useState({ title: '', content: '', triggers: '' });
2363
+ const [feedbackNotice, setFeedbackNotice] = useState('');
2364
+ const unlocked = lock.data?.enabled === true;
2365
+ const load = useCallback(async (date) => {
2366
+ setBusy(true); setError('');
2367
+ try { setBook(await api(`/summon/book${date ? `?date=${encodeURIComponent(date)}` : ''}`)); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
2368
+ }, []);
2369
+ useEffect(() => { load(selected); }, [load, selected]);
2370
+ useEffect(() => {
2371
+ const timer = setInterval(() => load(selected), 10_000);
2372
+ const onFocus = () => load(selected);
2373
+ window.addEventListener('focus', onFocus);
2374
+ return () => { clearInterval(timer); window.removeEventListener('focus', onFocus); };
2375
+ }, [load, selected]);
2376
+ const days = book?.days ?? [];
2377
+ const chooseDay = (date) => setSelected(date);
2378
+ const toggleLock = async () => {
2379
+ if (!window.confirm(unlocked ? '关闭召唤册编辑权限?' : '开启召唤册编辑权限?改动会写审计。')) return;
2380
+ try { await api('/memory/edit', { method: 'POST', body: JSON.stringify({ enabled: !unlocked }) }); lock.reload(); } catch (issue) { setError(String(issue.message ?? issue)); }
2381
+ };
2382
+ const rebuild = async () => {
2383
+ setBusy(true); setError('');
2384
+ try { await api('/summon/rebuild', { method: 'POST', body: '{}' }); await load(selected); } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
2385
+ };
2386
+ const startEdit = (card) => {
2387
+ if (!unlocked) return;
2388
+ setEditing(card.id);
2389
+ setDraft({ title: card.title, content: card.content, triggers: (card.triggers ?? []).join(', ') });
2390
+ setOpenCard(card.id);
2391
+ };
2392
+ const save = async () => {
2393
+ if (!editing || !unlocked) return;
2394
+ setBusy(true); setError('');
2395
+ try {
2396
+ await api('/summon/card', { method: 'POST', body: JSON.stringify({ id: editing, title: draft.title, content: draft.content, triggers: draft.triggers.split(/[,,\s]+/).filter(Boolean) }) });
2397
+ setEditing(null);
2398
+ await load(selected);
2399
+ } catch (issue) { setError(String(issue.message ?? issue)); } finally { setBusy(false); }
2400
+ };
2401
+ const sendFeedback = async (card, signal) => {
2402
+ try {
2403
+ await api('/summon/feedback', { method: 'POST', body: JSON.stringify({ id: card.id, signal }) });
2404
+ setFeedbackNotice(`${card.title}:${signal === 'helpful' ? '已标记有用' : '已标记忽略'}`);
2405
+ } catch (issue) { setError(String(issue.message ?? issue)); }
2406
+ };
2407
+ const cardList = (cards, side) => cards.length ? cards.map((card) => h('div', { key: card.id, className: 'cortex-summon-card' }, [
2408
+ h('button', { key: 'title', type: 'button', className: 'cortex-summon-title', onClick: () => setOpenCard(openCard === card.id ? null : card.id) }, [
2409
+ h('span', { key: 'n' }, card.title),
2410
+ h('span', { key: 't', className: 'cortex-meta' }, `${card.day} · ${card.category ?? side}`),
2411
+ ]),
2412
+ openCard === card.id ? h('div', { key: 'body', className: 'cortex-summon-body' }, [
2413
+ editing === card.id ? h('div', { key: 'form', className: 'cortex-memory-edit-form' }, [
2414
+ h('input', { key: 'title', className: 'cortex-input', value: draft.title, onChange: (event) => setDraft({ ...draft, title: event.target.value }), placeholder: '卡片名' }),
2415
+ h('textarea', { key: 'content', className: 'cortex-input', rows: 6, value: draft.content, onChange: (event) => setDraft({ ...draft, content: event.target.value }), placeholder: '卡片内容' }),
2416
+ h('input', { key: 'triggers', className: 'cortex-input', value: draft.triggers, onChange: (event) => setDraft({ ...draft, triggers: event.target.value }), placeholder: '触发词,逗号分隔' }),
2417
+ h('div', { key: 'buttons', className: 'cortex-line' }, [
2418
+ h('button', { key: 'save', type: 'button', className: 'cortex-btn primary', disabled: busy, onClick: save }, '保存'),
2419
+ h('button', { key: 'cancel', type: 'button', className: 'cortex-btn', onClick: () => setEditing(null) }, '取消'),
2420
+ ]),
2421
+ ]) : h('div', { key: 'content', className: 'cortex-memory-text' }, card.content),
2422
+ h('div', { key: 'triggers', className: 'cortex-chips' }, (card.triggers ?? []).slice(0, 16).map((trigger) => h('span', { key: trigger, className: 'cortex-badge' }, trigger))),
2423
+ h('div', { key: 'feedback', className: 'cortex-line' }, [
2424
+ h('button', { key: 'helpful', type: 'button', className: 'cortex-btn', onClick: () => sendFeedback(card, 'helpful') }, '这张卡有用'),
2425
+ h('button', { key: 'ignored', type: 'button', className: 'cortex-btn', onClick: () => sendFeedback(card, 'ignored') }, '忽略'),
2426
+ ]),
2427
+ unlocked && editing !== card.id ? h('div', { key: 'edit', className: 'cortex-line' }, [h('button', { type: 'button', className: 'cortex-btn', onClick: () => startEdit(card) }, '编辑这张卡')]) : null,
2428
+ ]) : null,
2429
+ ])) : h('div', { key: 'empty', className: 'cortex-empty' }, '这一天还没有召唤卡');
2430
+ return h('div', { className: 'cortex-summon-book' }, [
2431
+ error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed') }) : null,
2432
+ h('div', { key: 'toolbar', className: 'cortex-memory-toolbar' }, [
2433
+ h('div', { key: 'lock', className: 'cortex-line' }, [
2434
+ h('span', { key: 'state', className: `cortex-badge${unlocked ? ' on' : ''}` }, `编辑权限:${unlocked ? '开' : '关(只读)'}`),
2435
+ h('button', { key: 'toggle', type: 'button', className: 'cortex-btn', onClick: toggleLock }, unlocked ? '关闭编辑' : '开启编辑权限'),
2436
+ h('span', { key: 'sync', className: 'cortex-meta' }, '召唤册 10 秒同步;召唤卡由记忆自动生成'),
2437
+ ]),
2438
+ h('div', { key: 'nav', className: 'cortex-line' }, [
2439
+ h('input', { key: 'date', type: 'date', className: 'cortex-input', value: selected, onChange: (event) => setSelected(event.target.value), 'aria-label': '选择召唤日期' }),
2440
+ h('button', { key: 'rebuild', type: 'button', className: 'cortex-btn', disabled: busy, onClick: rebuild }, '回填旧记忆为召唤卡'),
2441
+ h('button', { key: 'refresh', type: 'button', className: 'cortex-btn', onClick: () => load(selected) }, t('refresh')),
2442
+ ]),
2443
+ ]),
2444
+ feedbackNotice ? h('div', { key: 'feedbackNotice', className: 'cortex-note' }, feedbackNotice) : null,
2445
+ h('div', { key: 'layout', className: 'cortex-memory-layout' }, [
2446
+ h('aside', { key: 'days', className: 'cortex-card cortex-memory-days' }, [
2447
+ h('h4', {}, '召唤日'),
2448
+ days.length ? days.map((day) => h('button', { key: day, type: 'button', className: `cortex-memory-day${day === (selected || book?.date) ? ' on' : ''}`, onClick: () => chooseDay(day) }, day)) : h('div', { className: 'cortex-empty' }, '还没有召唤卡'),
2449
+ h('div', { key: 'note', className: 'cortex-note', style: { marginTop: 8 } }, '召唤卡从情感/工作记忆回填;召唤策按类别合册。'),
2450
+ ]),
2451
+ h('div', { key: 'main', className: 'cortex-memory-main' }, [
2452
+ busy ? h(LoadingBlock, { key: 'loading', label: t('loading') }) : null,
2453
+ h('section', { key: 'left', className: 'cortex-card cortex-summon-left' }, [
2454
+ h('div', { key: 'head', className: 'cortex-card-head' }, [
2455
+ h('h4', {}, `左脑 · 情感卡 · ${book?.date || selected || '—'}`),
2456
+ h('div', { key: 'a', className: 'cortex-line' }, [h('span', { className: 'cortex-meta' }, book?.left?.length ?? 0), h('button', { type: 'button', className: 'cortex-btn', onClick: () => setLeftOpen(!leftOpen) }, leftOpen ? '收起' : '展开')]),
2457
+ ]),
2458
+ leftOpen ? cardList(book?.left ?? [], '情感') : h('div', { className: 'cortex-note' }, '左脑召唤卡已收起'),
2459
+ ]),
2460
+ h('section', { key: 'right', className: 'cortex-card cortex-summon-right' }, [
2461
+ h('div', { key: 'head', className: 'cortex-card-head' }, [
2462
+ h('h4', {}, `右脑 · 工作卡 · ${book?.date || selected || '—'}`),
2463
+ h('div', { key: 'a', className: 'cortex-line' }, [h('span', { className: 'cortex-meta' }, book?.right?.length ?? 0), h('button', { type: 'button', className: 'cortex-btn', onClick: () => setRightOpen(!rightOpen) }, rightOpen ? '收起' : '展开')]),
2464
+ ]),
2465
+ rightOpen ? cardList(book?.right ?? [], '工作') : h('div', { className: 'cortex-note' }, '右脑召唤卡已收起'),
2466
+ ]),
2467
+ h('section', { key: 'volumes', className: 'cortex-card cortex-summon-volumes' }, [
2468
+ h('div', { key: 'head', className: 'cortex-card-head' }, [
2469
+ h('h4', {}, `召唤策 · ${book?.volumes?.length ?? 0}`),
2470
+ h('button', { key: 'toggle', type: 'button', className: 'cortex-btn', onClick: () => setVolumesOpen(!volumesOpen) }, volumesOpen ? '收起召唤策' : '展开召唤策'),
2471
+ ]),
2472
+ volumesOpen ? (book?.volumes ?? []).map((volume) => h('details', { key: volume.id, className: 'cortex-summon-volume' }, [
2473
+ h('summary', {}, `${volume.side === 'left' ? '左脑册' : '右脑册'} · ${volume.title}`),
2474
+ h('div', { className: 'cortex-memory-text' }, volume.content),
2475
+ ])) : h('div', { className: 'cortex-note' }, '召唤策收起中;展开后按左右脑与类别看合册。'),
2476
+ ]),
2477
+ ]),
2478
+ ]),
2479
+ ]);
2480
+ }
2481
+
2482
+ function DiagnosticsCard({ t }) {
2483
+ const diagnostics = useEndpoint('/diagnostics');
2484
+ const [busy, setBusy] = useState('');
2485
+ const [notice, setNotice] = useState('');
2486
+ const data = diagnostics.data;
2487
+ const act = async (kind, id, decision = 'dismiss') => {
2488
+ if (!window.confirm('这会写审计记录;确认处理这一项?')) return;
2489
+ setBusy(id); setNotice('');
2490
+ try {
2491
+ if (kind === 'fact') {
2492
+ await api('/facts/resolve', { method: 'POST', body: JSON.stringify({ id, decision, reason: '主人从诊断框处理' }) });
2493
+ setNotice(decision === 'confirm' ? '已确认有效' : '已降级为无效,内容保留');
2494
+ } else if (kind === 'suggest') {
2495
+ const result = await api('/skills/suggest', { method: 'POST', body: '{}' });
2496
+ setNotice(`已提炼 ${result.count} 个技能候选;候选不会自动启用`);
2497
+ } else if (kind === 'promote') {
2498
+ await api('/skills/promote', { method: 'POST', body: JSON.stringify({ id }) });
2499
+ setNotice('技能已启用');
2500
+ }
2501
+ diagnostics.reload();
2502
+ } catch (issue) { setNotice(String(issue.message ?? issue)); } finally { setBusy(''); }
2503
+ };
2504
+ return h('section', { key: 'diagnostics', className: 'cortex-card cortex-diagnostics', style: { marginTop: 10 } }, [
2505
+ h('div', { key: 'head', className: 'cortex-card-head' }, [
2506
+ h('h4', {}, '诊断'),
2507
+ h('span', { key: 'score', className: `cortex-badge${data?.score === 100 ? ' on' : ''}` }, `健康 ${data?.score ?? '—'}/100`),
2508
+ ]),
2509
+ notice ? h('div', { key: 'notice', className: 'cortex-note' }, notice) : null,
2510
+ !data ? h('div', { key: 'loading', className: 'cortex-note' }, t('loading')) : h('div', {}, [
2511
+ (data.issues ?? []).length ? h('div', { key: 'issues' }, [
2512
+ h('div', { key: 'label', className: 'cortex-meta' }, '检测到的问题'),
2513
+ ...data.issues.map((issue) => h('div', { key: issue.code ?? issue.message, className: 'cortex-row' }, `${issue.code ?? 'ISSUE'} · ${issue.message ?? ''}`)),
2514
+ ]) : h('div', { key: 'ok', className: 'cortex-note' }, '没有发现问题。'),
2515
+ h('div', { key: 'disputes', style: { marginTop: 10 } }, [
2516
+ h('div', { key: 'label', className: 'cortex-meta' }, `争议事实 ${data.disputes?.length ?? 0}`),
2517
+ ...(data.disputes ?? []).map((fact) => h('div', { key: fact.id, className: 'cortex-row cortex-diagnostic-row' }, [
2518
+ h('span', { key: 'text', style: { flex: '1 1 260px' } }, `${fact.subject} ${fact.predicate} ${fact.objectLiteral ?? fact.objectId ?? '—'} · 置信 ${formatNumber(fact.confidence, 2)}`),
2519
+ h('button', { key: 'confirm', type: 'button', className: 'cortex-btn', disabled: busy === fact.id, onClick: () => act('fact', fact.id, 'confirm') }, '确认有效'),
2520
+ h('button', { key: 'dismiss', type: 'button', className: 'cortex-btn', disabled: busy === fact.id, onClick: () => act('fact', fact.id, 'dismiss') }, '降级无效'),
2521
+ ])),
2522
+ ]),
2523
+ h('div', { key: 'skills', style: { marginTop: 10 } }, [
2524
+ h('div', { key: 'label', className: 'cortex-meta' }, `技能:active ${data.skills?.active ?? 0} · 候选 ${data.skills?.candidates ?? 0}`),
2525
+ h('button', { key: 'suggest', type: 'button', className: 'cortex-btn', disabled: busy === 'suggest', onClick: () => act('suggest') }, '从痕迹提炼技能候选'),
2526
+ ...(data.skills?.items ?? []).filter((skill) => skill.state !== 'active').slice(0, 6).map((skill) => h('div', { key: skill.id, className: 'cortex-row cortex-diagnostic-row' }, [
2527
+ h('span', { key: 'name', style: { flex: '1 1 220px' } }, `${skill.name} · ${skill.state}`),
2528
+ h('button', { key: 'promote', type: 'button', className: 'cortex-btn', disabled: busy === skill.id, onClick: () => act('promote', skill.id) }, '启用'),
2529
+ ])),
2530
+ ]),
2531
+ ]),
2532
+ ]);
2533
+ }
2534
+
2020
2535
  function SettingsTab({ t }) {
2021
2536
  const settings = useEndpoint('/settings');
2022
2537
  const metrics = useEndpoint('/metrics');
@@ -2069,7 +2584,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2069
2584
  settings.error ? h(ErrorBlock, { key: 'serr', message: settings.error, prefix: t('requestFailed'), onRetry: settings.reload, retryLabel: t('retry') }) : null,
2070
2585
  metrics.error ? h(ErrorBlock, { key: 'merr', message: metrics.error, prefix: t('requestFailed'), onRetry: metrics.reload, retryLabel: t('retry') }) : null,
2071
2586
  settings.busy && !setting ? h(LoadingBlock, { key: 'sload', label: t('loading') }) : null,
2072
- setting ? h('div', { key: 'settings', className: 'cortex-grid' }, [
2587
+ setting ? h('div', { key: 'settings', className: 'cortex-settings' }, [
2073
2588
  h('section', { key: 'runtime', className: 'cortex-card' }, [
2074
2589
  h('h4', {}, t('runtime')),
2075
2590
  h(KeyValueList, { rows: [
@@ -2081,25 +2596,37 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2081
2596
  ['promptBudget', t('promptBudget'), setting.promptBudget],
2082
2597
  ].map(([key, label, value]) => [key, label, displayValue(value)]) }),
2083
2598
  ]),
2084
- h('section', { key: 'features', className: 'cortex-card' }, [
2599
+ h('section', { key: 'features', className: 'cortex-card cortex-card--wide' }, [
2085
2600
  h('h4', {}, t('features')),
2086
2601
  Object.keys(setting.features ?? {}).length
2087
- ? h('div', { className: 'cortex-chips' }, Object.entries(setting.features).map(([name, value]) => {
2602
+ ? h('div', { className: 'cortex-feature-grid' }, Object.entries(setting.features).map(([name, value]) => {
2088
2603
  const current = setting.overrides?.[name] ?? value;
2089
2604
  const editable = name !== 'api';
2090
- return h('button', {
2091
- key: name,
2092
- type: 'button',
2093
- className: `cortex-badge${current ? ' on' : ''}`,
2094
- 'aria-pressed': Boolean(current),
2095
- disabled: !editable,
2096
- title: editable ? `${t('permissionClick')} · ${t('permissionRestart')}` : t('permissionApiLocked'),
2097
- onClick: editable ? () => changePermission(name, !current) : undefined,
2098
- }, `${name} ${current ? 'on' : 'off'}`);
2605
+ const meta = FEATURE_META[name] ?? { zh: name, en: name, descZh: '', descEn: '' };
2606
+ const zh = t('permissionOn') === '开';
2607
+ const label = zh ? meta.zh : meta.en;
2608
+ const desc = zh ? meta.descZh : meta.descEn;
2609
+ return h('div', { key: name, className: 'cortex-feature' }, [
2610
+ h('div', { key: 'head', className: 'cortex-feature-head' }, [
2611
+ h('span', { key: 'name', className: 'cortex-feature-name' }, label),
2612
+ h('span', { key: 'state', className: `cortex-badge${current ? ' on' : ''}` }, current ? t('permissionOn') : t('permissionOff')),
2613
+ ]),
2614
+ h('div', { key: 'desc', className: 'cortex-feature-desc' }, desc),
2615
+ h('button', {
2616
+ key: 'toggle',
2617
+ type: 'button',
2618
+ className: 'cortex-btn',
2619
+ disabled: !editable,
2620
+ 'aria-pressed': Boolean(current),
2621
+ title: editable ? `${t('permissionClick')} · ${t('permissionRestart')}` : t('permissionReadOnly'),
2622
+ onClick: editable ? () => changePermission(name, !current) : undefined,
2623
+ }, editable ? (current ? '关闭' : '开启') : '锁定'),
2624
+ ]);
2099
2625
  }))
2100
2626
  : h(EmptyBlock, {}, t('empty')),
2627
+ setting.features?.autoEvolve ? h('div', { key: 'auto-evolve-note', className: 'cortex-note' }, t('autoEvolveNote')) : null,
2101
2628
  ]),
2102
- h('section', { key: 'permissions', className: 'cortex-card' }, [
2629
+ h('section', { key: 'permissions', className: 'cortex-card cortex-card--wide' }, [
2103
2630
  h('h4', {}, t('permissions')),
2104
2631
  h('div', { className: 'cortex-chips' }, [
2105
2632
  h(Badge, { key: 'local', tone: setting.permissions?.localApi ? 'on' : '' }, `${t('permissionLocal')} ${setting.permissions?.localApi ? t('permissionOn') : t('permissionOff')}`),
@@ -2108,23 +2635,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2108
2635
  h(Badge, { key: 'rerank', tone: setting.permissions?.network?.rerank ? 'on' : '' }, `rerank ${setting.permissions?.network?.rerank ? t('permissionOn') : t('permissionOff')}`),
2109
2636
  h(Badge, { key: 'vector', tone: setting.permissions?.vector ? 'on' : '' }, `vector ${setting.permissions?.vector ? t('permissionOn') : t('permissionOff')}`),
2110
2637
  ]),
2111
- h('div', { className: 'cortex-grid', style: { marginTop: 10 } }, [
2112
- ['llm', 'LLM'], ['rerank', 'rerank'], ['capture', 'capture'], ['recall', 'recall'],
2113
- ['vector', t('vector')], ['autoFacts', 'autoFacts'],
2114
- ].map(([key, label]) => {
2115
- const current = setting.overrides?.[key] ?? setting.features?.[key] ?? (key === 'rerank' ? setting.permissions?.network?.rerank : false);
2116
- return h('div', { key, className: 'cortex-row', style: { justifyContent: 'space-between', gap: 8 } }, [
2117
- h('span', { key: 'label' }, label),
2118
- h('button', {
2119
- key: 'toggle',
2120
- type: 'button',
2121
- className: 'cortex-btn',
2122
- 'aria-pressed': Boolean(current),
2123
- title: `${t('permissionClick')} · ${t('permissionRestart')}`,
2124
- onClick: () => changePermission(key, !current),
2125
- }, `${current ? t('permissionOn') : t('permissionOff')} · ${t('permissionClick')}`),
2126
- ]);
2127
- })),
2638
+ h('p', { key: 'ro', className: 'cortex-note' }, t('permissionReadOnly')),
2128
2639
  h('div', { className: 'cortex-line', style: { marginTop: 8 } }, [
2129
2640
  h('button', { key: 'undo', type: 'button', className: 'cortex-btn', onClick: rollbackPermission }, t('permissionRollback')),
2130
2641
  h('span', { key: 'note', className: 'cortex-meta' }, t('permissionRestart')),
@@ -2164,6 +2675,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2164
2675
  ]),
2165
2676
  h(ModelSettings, { key: 'models', t }),
2166
2677
  ]) : null,
2678
+ h(DiagnosticsCard, { key: 'diagnostics', t }),
2167
2679
  h('div', { key: 'metrics', className: 'cortex-card', style: { marginTop: 10 } }, [
2168
2680
  h('div', { key: 'head', className: 'cortex-card-head' }, [
2169
2681
  h('h4', {}, t('metricsTitle')),
@@ -2218,6 +2730,12 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2218
2730
  }, []);
2219
2731
 
2220
2732
  useEffect(() => { loadSelf(); }, [loadSelf]);
2733
+ useEffect(() => {
2734
+ const timer = setInterval(async () => {
2735
+ try { setSelf(await api('/self')); } catch { /* keep last snapshot */ }
2736
+ }, 15_000);
2737
+ return () => clearInterval(timer);
2738
+ }, []);
2221
2739
 
2222
2740
  const openTrace = useCallback((value) => {
2223
2741
  const id = typeof value === 'string' ? value : value?.id;
@@ -2245,6 +2763,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2245
2763
  ['playbook', t('tabPlaybook')],
2246
2764
  ['audit', t('tabAudit')],
2247
2765
  ['retention', t('tabRetention')],
2766
+ ['memory', t('tabMemoryEdit')],
2248
2767
  ['health', t('tabHealth')],
2249
2768
  ['evolution', t('tabEvolution')],
2250
2769
  ['settings', t('tabSettings')],
@@ -2287,6 +2806,8 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
2287
2806
  content = h(AuditTab, { t });
2288
2807
  } else if (tab === 'retention') {
2289
2808
  content = h(RetentionTab, { t, onSelect: openTrace });
2809
+ } else if (tab === 'memory') {
2810
+ content = h(SummonBookTab, { t });
2290
2811
  } else if (tab === 'playbook') {
2291
2812
  content = h(PlaybookTab, { t });
2292
2813
  } else if (tab === 'health') {