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/LICENSE +3 -2
- package/README.md +55 -7
- package/bench/synthetic.js +1 -1
- package/client.js +576 -55
- package/docs/install-windows.md +3 -3
- package/docs/multimodal.md +1 -1
- package/index.js +17 -2
- package/package.json +14 -2
- package/src/api/client.js +5 -2
- package/src/api/routes.js +88 -0
- package/src/core/config.js +8 -0
- package/src/core/feature-overrides.js +2 -2
- package/src/engines/life.js +182 -19
- package/src/host/runtime.js +25 -16
- package/src/host/scheduler.js +177 -0
- package/src/llm/adapter.js +65 -4
- package/src/prompt/assemble.js +8 -3
- package/src/self/emotion.js +4 -2
- package/src/self/evolution.js +15 -1
- package/src/self/identity.js +10 -1
- package/src/service/cortex.js +676 -8
- package/src/store/database.js +10 -0
- package/src/store/repositories/summon.js +138 -0
- package/src/store/schema.sql +45 -0
- package/src/tools/definitions.js +13 -3
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
|
|
1634
|
-
if (
|
|
1635
|
-
|
|
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('
|
|
1641
|
-
h('
|
|
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', {}, '
|
|
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
|
|
1658
|
-
|
|
1659
|
-
|
|
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('
|
|
1670
|
-
h('button', { key: '
|
|
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 } }, [
|
|
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 = (
|
|
1829
|
-
const
|
|
1830
|
-
return
|
|
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' },
|
|
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-
|
|
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-
|
|
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
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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('
|
|
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') {
|