dsh-cortex-memory 0.1.0 → 0.2.0-rc.11
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 -3
- package/README.md +85 -146
- package/bench/ab-engine.mjs +60 -0
- package/bench/ann-bench.mjs +78 -0
- package/bench/hnsw-bench.mjs +79 -0
- package/bench/metrics.js +8 -0
- package/bench/migration-bench.mjs +69 -0
- package/bench/run.js +5 -2
- package/bench/synthetic.js +51 -7
- package/bench/vector-ab.mjs +76 -0
- package/bin/cortex.mjs +2 -3
- package/client.js +269 -4
- package/cordis.patch.yml +1 -1
- package/docs/install-windows.md +115 -27
- package/docs/models.md +165 -30
- package/docs/multimodal.md +112 -0
- package/docs/scopes.md +142 -0
- package/examples/README.md +72 -0
- package/examples/setup-cortex-bge.ps1 +66 -0
- package/examples/uninstall-cortex.ps1 +91 -0
- package/index.js +15 -1
- package/package.json +6 -13
- package/src/adapters/markdown-import.js +20 -1
- package/src/adapters/markdown.js +12 -2
- package/src/adapters/shiori.js +1 -1
- package/src/api/routes.js +144 -4
- package/src/api/validate.js +26 -0
- package/src/core/attachment-reader.js +31 -0
- package/src/core/attachments.js +39 -0
- package/src/core/config.js +36 -1
- package/src/core/model-config.js +3 -1
- package/src/core/profile.js +32 -0
- package/src/engines/develop.js +1 -1
- package/src/engines/encode.js +68 -30
- package/src/engines/extract.js +13 -8
- package/src/engines/growth.js +1 -1
- package/src/engines/reflect.js +1 -1
- package/src/engines/retrieve.js +44 -13
- package/src/host/runtime.js +1 -1
- package/src/memory/query.js +1 -1
- package/src/memory/tokenizer.js +19 -4
- package/src/security/scope-grants.js +73 -0
- package/src/self/appraisal.js +3 -3
- package/src/self/emotion.js +1 -1
- package/src/self/identity.js +1 -1
- package/src/self/relationship.js +2 -2
- package/src/service/cortex.js +191 -3
- package/src/store/database.js +33 -1
- package/src/store/repositories/edges.js +48 -4
- package/src/store/repositories/traces.js +23 -12
- package/src/store/schema.sql +1 -0
- package/src/tools/definitions.js +62 -5
- package/src/vector/external-ann.js +37 -0
- package/src/vector/hnsw.js +246 -0
- package/src/vector/index.js +85 -2
- package/docs/compatibility.md +0 -39
package/bench/synthetic.js
CHANGED
|
@@ -15,11 +15,30 @@ function mulberry32(seed) {
|
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const PLACES = ['
|
|
18
|
+
const PLACES = ['白饭', '插件', '记忆引擎', '鲸鱼', '主人', '月神', '直播', '语音', '项目', '代码', '测试', 'D:\\shiori-data', 'QQ', 'B站', '声音', '梦', '日记'];
|
|
19
19
|
const VERBS = ['修好了', '想起来了', '决定继续', '记录一下', '有点担心', '很开心', '测试了', '重新整理了', '发现了', '计划明天'];
|
|
20
20
|
const TOPICS = ['图检索', '向量检索', '遗忘曲线', '工作卡', '火花卡', '身份版本', '关系模型', '反思', '巩固', '守护', '备份', '权限'];
|
|
21
|
+
const PARAPHRASES = new Map([
|
|
22
|
+
['白饭', '米饭'],
|
|
23
|
+
['插件', '扩展程序'],
|
|
24
|
+
['记忆引擎', '记忆系统'],
|
|
25
|
+
['鲸鱼', '鲸'],
|
|
26
|
+
['直播', '实况'],
|
|
27
|
+
['测试', '验证'],
|
|
28
|
+
['图检索', '图谱查询'],
|
|
29
|
+
['向量检索', '向量查询'],
|
|
30
|
+
['遗忘曲线', '遗忘衰减'],
|
|
31
|
+
['工作卡', '任务卡'],
|
|
32
|
+
['火花卡', '灵感卡'],
|
|
33
|
+
['身份版本', '人设版本'],
|
|
34
|
+
['关系模型', '关系图谱'],
|
|
35
|
+
['巩固', '强化'],
|
|
36
|
+
['守护', '保护'],
|
|
37
|
+
['备份', '存档'],
|
|
38
|
+
['权限', '许可'],
|
|
39
|
+
]);
|
|
21
40
|
|
|
22
|
-
export function generateSynthetic({ count = 1000, queries = 50, seed = 42 } = {}) {
|
|
41
|
+
export function generateSynthetic({ count = 1000, queries = 50, seed = 42, task = 'needle' } = {}) {
|
|
23
42
|
const random = mulberry32(seed);
|
|
24
43
|
const pick = (list) => list[Math.floor(random() * list.length)];
|
|
25
44
|
const traces = [];
|
|
@@ -46,10 +65,35 @@ export function generateSynthetic({ count = 1000, queries = 50, seed = 42 } = {}
|
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
const queryList = [];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
if (task === 'needle') {
|
|
69
|
+
for (let i = 0; i < queries; i += 1) {
|
|
70
|
+
// A unique needle: the query names one specific row by its ordinal.
|
|
71
|
+
// This measures index/ranking correctness, not semantic paraphrase recall.
|
|
72
|
+
const targetIndex = Math.floor(random() * count);
|
|
73
|
+
const target = traces[targetIndex];
|
|
74
|
+
queryList.push({
|
|
75
|
+
query: `${target.title} 第 ${targetIndex} 条合成记忆`,
|
|
76
|
+
type: 'needle',
|
|
77
|
+
relevant: [target.id],
|
|
78
|
+
targetId: target.id,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} else if (task === 'theme' || task === 'paraphrase') {
|
|
82
|
+
const tokens = [...idByToken.keys()].filter((token) =>
|
|
83
|
+
(idByToken.get(token)?.length ?? 0) >= 2 && (task !== 'paraphrase' || PARAPHRASES.has(token)));
|
|
84
|
+
for (let i = 0; i < queries && tokens.length; i += 1) {
|
|
85
|
+
const token = pick(tokens);
|
|
86
|
+
const relevant = idByToken.get(token).slice();
|
|
87
|
+
const query = task === 'paraphrase' ? (PARAPHRASES.get(token) ?? token) : token;
|
|
88
|
+
queryList.push({
|
|
89
|
+
query,
|
|
90
|
+
type: task,
|
|
91
|
+
relevant,
|
|
92
|
+
targetToken: token,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(`unknown synthetic task: ${task}`);
|
|
53
97
|
}
|
|
54
|
-
return { traces, queries: queryList, seed, generatedAt: new Date().toISOString() };
|
|
98
|
+
return { traces, queries: queryList, seed, task, generatedAt: new Date().toISOString() };
|
|
55
99
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createCortex } from '../src/service/cortex.js';
|
|
5
|
+
import { createOpenAIEmbedder } from '../src/vector/providers.js';
|
|
6
|
+
import { generateSynthetic } from './synthetic.js';
|
|
7
|
+
import { evaluateQueries, percentile } from './metrics.js';
|
|
8
|
+
|
|
9
|
+
const count = Number(process.argv[2] ?? 300);
|
|
10
|
+
const queryCount = Number(process.argv[3] ?? 50);
|
|
11
|
+
const useVector = process.argv[4] !== 'fts';
|
|
12
|
+
const dir = mkdtempSync(path.join(os.tmpdir(), `cortex-vector-ab-${useVector ? 'bge' : 'fts'}-`));
|
|
13
|
+
const embedder = createOpenAIEmbedder({
|
|
14
|
+
baseUrl: 'http://127.0.0.1:11434/v1',
|
|
15
|
+
model: 'bge-m3',
|
|
16
|
+
dim: 1024,
|
|
17
|
+
timeoutMs: 30000,
|
|
18
|
+
});
|
|
19
|
+
const cortex = createCortex({
|
|
20
|
+
config: {
|
|
21
|
+
dataRoot: dir,
|
|
22
|
+
dbFile: path.join(dir, 'cortex.sqlite'),
|
|
23
|
+
contextBudget: 2200,
|
|
24
|
+
vectorEnabled: useVector,
|
|
25
|
+
features: useVector ? { vector: true } : {},
|
|
26
|
+
...(useVector ? { embedder } : {}),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
const dataset = generateSynthetic({ count, queries: queryCount, seed: 42, task: 'paraphrase' });
|
|
31
|
+
const written = cortex.writeMany(dataset.traces.map((trace) => ({
|
|
32
|
+
text: trace.text,
|
|
33
|
+
title: trace.title,
|
|
34
|
+
tags: trace.tags,
|
|
35
|
+
importance: trace.importance,
|
|
36
|
+
explicit: true,
|
|
37
|
+
operationId: `vector-ab:${trace.id}`,
|
|
38
|
+
now: Date.parse(trace.recordedAt),
|
|
39
|
+
})), { chunkSize: 250 });
|
|
40
|
+
const idMap = new Map();
|
|
41
|
+
for (let i = 0; i < written.length; i += 1) {
|
|
42
|
+
if (written[i].trace) idMap.set(dataset.traces[i].id, written[i].trace.id);
|
|
43
|
+
}
|
|
44
|
+
if (useVector) {
|
|
45
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
46
|
+
const rebuilt = await cortex.vector.rebuildAsync({ limit: count });
|
|
47
|
+
console.error(`vector rebuild: ${JSON.stringify(rebuilt)}`);
|
|
48
|
+
}
|
|
49
|
+
const results = [];
|
|
50
|
+
const latencies = [];
|
|
51
|
+
for (const query of dataset.queries) {
|
|
52
|
+
const start = performance.now();
|
|
53
|
+
const result = useVector
|
|
54
|
+
? await cortex.searchAsync(query.query, { limit: 10, budget: 2200 })
|
|
55
|
+
: cortex.search(query.query, { limit: 10, budget: 2200 });
|
|
56
|
+
latencies.push(performance.now() - start);
|
|
57
|
+
results.push({
|
|
58
|
+
items: result.items,
|
|
59
|
+
relevant: query.relevant.map((id) => idMap.get(id)).filter(Boolean),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const metrics = evaluateQueries(results, { k: 10 });
|
|
63
|
+
console.log(JSON.stringify({
|
|
64
|
+
task: 'paraphrase',
|
|
65
|
+
vector: useVector,
|
|
66
|
+
count,
|
|
67
|
+
queries: dataset.queries.length,
|
|
68
|
+
p50Ms: Math.round((percentile(latencies, 50) ?? 0) * 100) / 100,
|
|
69
|
+
p95Ms: Math.round((percentile(latencies, 95) ?? 0) * 100) / 100,
|
|
70
|
+
vectorStatus: cortex.vector?.status?.() ?? null,
|
|
71
|
+
...metrics,
|
|
72
|
+
}, null, 2));
|
|
73
|
+
} finally {
|
|
74
|
+
cortex.close();
|
|
75
|
+
rmSync(dir, { recursive: true, force: true });
|
|
76
|
+
}
|
package/bin/cortex.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createCortex } from '../src/service/cortex.js';
|
|
@@ -34,7 +34,7 @@ Commands:
|
|
|
34
34
|
self
|
|
35
35
|
health
|
|
36
36
|
guard
|
|
37
|
-
import-shiori --root <
|
|
37
|
+
import-shiori --root <shiori-data>
|
|
38
38
|
export-md --dir <target>
|
|
39
39
|
import-md --dir <vault> [--dry-run]
|
|
40
40
|
bench [--count 10000] [--queries 100]
|
|
@@ -94,4 +94,3 @@ function main() {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
main();
|
|
97
|
-
|
package/client.js
CHANGED
|
@@ -12,7 +12,7 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
zh: {
|
|
13
13
|
navLabel: 'Cortex 记忆',
|
|
14
14
|
title: 'Cortex 记忆',
|
|
15
|
-
subtitle: '
|
|
15
|
+
subtitle: '本鲸的分层记忆 · 自我状态 · 守护 · 进化',
|
|
16
16
|
refresh: '刷新',
|
|
17
17
|
loading: '读取中…',
|
|
18
18
|
retry: '重试',
|
|
@@ -96,6 +96,19 @@ window.__ModuleLoader__.load({
|
|
|
96
96
|
detailContent: '正文',
|
|
97
97
|
detailSummary: '摘要',
|
|
98
98
|
detailTags: '标签',
|
|
99
|
+
detailAttachments: '附件(仅元数据)',
|
|
100
|
+
attachmentAddTitle: '关联本地附件',
|
|
101
|
+
attachmentAddHint: '填 file:// 路径;“检查”走本地读取器(默认关),“关联”只写引用元数据。',
|
|
102
|
+
attachmentCheck: '检查',
|
|
103
|
+
attachmentSave: '关联',
|
|
104
|
+
attachmentSaved: '已关联',
|
|
105
|
+
extractTitle: '从本地提取到记忆',
|
|
106
|
+
extractHint: 'OCR/ASR 结果会写成一条派生记忆,不改原 trace。',
|
|
107
|
+
extractTarget: '目标记忆',
|
|
108
|
+
extractRun: '提取并写入',
|
|
109
|
+
extractSaved: '已写入派生记忆',
|
|
110
|
+
extractSuggested: '建议:当前这条',
|
|
111
|
+
extractChanged: '已改选目标',
|
|
99
112
|
detailKeywords: '关键词',
|
|
100
113
|
detailFeedback: '这条记忆有用吗',
|
|
101
114
|
feedbackUsed: '用到了',
|
|
@@ -156,6 +169,12 @@ window.__ModuleLoader__.load({
|
|
|
156
169
|
graphRank: 'PageRank',
|
|
157
170
|
graphScaleNote: '节点越大,PageRank 越高;轨道只是布局参考,不是边。',
|
|
158
171
|
graphTopCount: '展示',
|
|
172
|
+
graphCandidatesTitle: '候选边(待审核)',
|
|
173
|
+
graphCandidatesHint: '候选边不会进入图谱,批准后才生效。',
|
|
174
|
+
graphCandidatesEmpty: '没有待审核的候选边。',
|
|
175
|
+
graphApprove: '批准',
|
|
176
|
+
graphReject: '拒绝',
|
|
177
|
+
graphCandidateConfidence: '置信',
|
|
159
178
|
settingsTitle: '运行设置',
|
|
160
179
|
settingsHint: '设置与指标均为只读快照。',
|
|
161
180
|
runtime: '运行时',
|
|
@@ -168,6 +187,16 @@ window.__ModuleLoader__.load({
|
|
|
168
187
|
permissionConfirm: '修改权限会写入本机 cortex 数据目录,重启后生效。继续吗?',
|
|
169
188
|
permissionSaved: '已保存;重启 DSH 后生效。',
|
|
170
189
|
permissionRestart: '重启后生效',
|
|
190
|
+
scopeGrants: '共享写授权',
|
|
191
|
+
scopeGrantsHint: '只对带 x-cortex-agent 的 API 生效;撤销立即生效并持久化。工具路径不带 agent。',
|
|
192
|
+
scopeGrantName: '授权(scope:agent)',
|
|
193
|
+
scopeGrantUntil: '到期(ISO,可空)',
|
|
194
|
+
scopeGrantAdd: '添加授权',
|
|
195
|
+
scopeGrantRevoke: '撤销',
|
|
196
|
+
scopeGrantEmpty: '没有共享写授权。',
|
|
197
|
+
scopeGrantForever: '永久',
|
|
198
|
+
scopeGrantRevoked: '已撤销',
|
|
199
|
+
scopeGrantSaved: '已保存',
|
|
171
200
|
vectorEnable: '启用向量',
|
|
172
201
|
vectorDisabled: '向量默认关闭。启用后使用内置 hash-256(不需要下载模型),重启后生效。',
|
|
173
202
|
modelTitle: '嵌入 / 重排模型',
|
|
@@ -195,6 +224,7 @@ window.__ModuleLoader__.load({
|
|
|
195
224
|
vector: '向量',
|
|
196
225
|
scope: 'scope',
|
|
197
226
|
dataRoot: 'dataRoot',
|
|
227
|
+
readOnlyScopes: '只读 scope',
|
|
198
228
|
dbFile: 'dbFile',
|
|
199
229
|
contextBudget: 'contextBudget',
|
|
200
230
|
promptBudget: 'promptBudget',
|
|
@@ -324,6 +354,19 @@ window.__ModuleLoader__.load({
|
|
|
324
354
|
detailContent: 'Content',
|
|
325
355
|
detailSummary: 'Summary',
|
|
326
356
|
detailTags: 'Tags',
|
|
357
|
+
detailAttachments: 'Attachments (metadata only)',
|
|
358
|
+
attachmentAddTitle: 'Attach a local file',
|
|
359
|
+
attachmentAddHint: 'Enter a file:// path. Check uses the local reader (off by default); Attach only stores reference metadata.',
|
|
360
|
+
attachmentCheck: 'Check',
|
|
361
|
+
attachmentSave: 'Attach',
|
|
362
|
+
attachmentSaved: 'Attached',
|
|
363
|
+
extractTitle: 'Extract into memory',
|
|
364
|
+
extractHint: 'OCR/ASR output is stored as a derived trace; the original trace is unchanged.',
|
|
365
|
+
extractTarget: 'Target trace',
|
|
366
|
+
extractRun: 'Extract and write',
|
|
367
|
+
extractSaved: 'Derived trace written',
|
|
368
|
+
extractSuggested: 'Suggested: current trace',
|
|
369
|
+
extractChanged: 'Target changed',
|
|
327
370
|
detailKeywords: 'Keywords',
|
|
328
371
|
detailFeedback: 'Was this memory useful?',
|
|
329
372
|
feedbackUsed: 'Used',
|
|
@@ -384,6 +427,12 @@ window.__ModuleLoader__.load({
|
|
|
384
427
|
graphRank: 'PageRank',
|
|
385
428
|
graphScaleNote: 'Bigger nodes have higher PageRank; the orbits are layout guides, not edges.',
|
|
386
429
|
graphTopCount: 'Showing',
|
|
430
|
+
graphCandidatesTitle: 'Candidate edges (pending)',
|
|
431
|
+
graphCandidatesHint: 'Candidate edges never enter the graph until approved.',
|
|
432
|
+
graphCandidatesEmpty: 'No pending candidate edges.',
|
|
433
|
+
graphApprove: 'Approve',
|
|
434
|
+
graphReject: 'Reject',
|
|
435
|
+
graphCandidateConfidence: 'Confidence',
|
|
387
436
|
settingsTitle: 'Runtime settings',
|
|
388
437
|
settingsHint: 'Settings and metrics are read-only snapshots.',
|
|
389
438
|
runtime: 'Runtime',
|
|
@@ -396,6 +445,16 @@ window.__ModuleLoader__.load({
|
|
|
396
445
|
permissionConfirm: 'Permission changes write a local override and take effect after restarting DSH. Continue?',
|
|
397
446
|
permissionSaved: 'Saved; restart DSH to apply.',
|
|
398
447
|
permissionRestart: 'restart required',
|
|
448
|
+
scopeGrants: 'Shared write grants',
|
|
449
|
+
scopeGrantsHint: 'Only applies to API calls with x-cortex-agent; revoke is immediate and persisted. Tool calls do not send an agent header.',
|
|
450
|
+
scopeGrantName: 'Grant (scope:agent)',
|
|
451
|
+
scopeGrantUntil: 'Until (ISO, optional)',
|
|
452
|
+
scopeGrantAdd: 'Add grant',
|
|
453
|
+
scopeGrantRevoke: 'Revoke',
|
|
454
|
+
scopeGrantEmpty: 'No shared write grants.',
|
|
455
|
+
scopeGrantForever: 'forever',
|
|
456
|
+
scopeGrantRevoked: 'Revoked',
|
|
457
|
+
scopeGrantSaved: 'Saved',
|
|
399
458
|
vectorEnable: 'Enable vector',
|
|
400
459
|
vectorDisabled: 'Vector is off by default. Enabling uses the local hash-256 embedder (no model download); restart required.',
|
|
401
460
|
modelTitle: 'Embedding / rerank models',
|
|
@@ -423,6 +482,7 @@ window.__ModuleLoader__.load({
|
|
|
423
482
|
vector: 'Vector',
|
|
424
483
|
scope: 'scope',
|
|
425
484
|
dataRoot: 'dataRoot',
|
|
485
|
+
readOnlyScopes: 'Read-only scopes',
|
|
426
486
|
dbFile: 'dbFile',
|
|
427
487
|
contextBudget: 'contextBudget',
|
|
428
488
|
promptBudget: 'promptBudget',
|
|
@@ -879,7 +939,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
879
939
|
h(Field, { key: 'tag', label: t('filterTag'), hint: t('filterTagsHint') }, h('input', {
|
|
880
940
|
className: 'cortex-input',
|
|
881
941
|
value: filters.tag,
|
|
882
|
-
placeholder: '
|
|
942
|
+
placeholder: '白饭, 主人',
|
|
883
943
|
'aria-label': t('filterTag'),
|
|
884
944
|
onChange: (event) => change('tag', event.target.value),
|
|
885
945
|
onKeyDown: enter,
|
|
@@ -941,6 +1001,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
941
1001
|
h('div', { key: 'head', className: 'cortex-result-head' }, [
|
|
942
1002
|
h(Badge, { key: 'layer' }, item.layer ?? item.type ?? 'trace'),
|
|
943
1003
|
h('b', { key: 'title', className: 'cortex-result-title' }, item.title ?? item.id),
|
|
1004
|
+
item.attachments ? h('span', { key: 'attachments', className: 'cortex-meta' }, `📎 ${item.attachments}`) : null,
|
|
944
1005
|
h('span', { key: 'score', className: 'cortex-result-score' }, `score ${formatNumber(item.score, 3)}`),
|
|
945
1006
|
]),
|
|
946
1007
|
h('div', { key: 'meta', className: 'cortex-meta' }, `${item.id} · ${formatTime(item.recordedAt)}${item.why ? ` · ${item.why}` : ''}`),
|
|
@@ -977,6 +1038,110 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
977
1038
|
].map(([key, label, value]) => [key, label, displayValue(value)]);
|
|
978
1039
|
}
|
|
979
1040
|
|
|
1041
|
+
function AttachmentComposer({ trace, t, onSaved }) {
|
|
1042
|
+
const [uri, setUri] = useState('');
|
|
1043
|
+
const [note, setNote] = useState('');
|
|
1044
|
+
const [busy, setBusy] = useState(false);
|
|
1045
|
+
const inspect = async () => {
|
|
1046
|
+
if (!uri.trim()) return;
|
|
1047
|
+
setBusy(true); setNote('');
|
|
1048
|
+
try {
|
|
1049
|
+
const result = await api('/attachments/inspect', { method: 'POST', body: JSON.stringify({ uri: uri.trim() }) });
|
|
1050
|
+
setNote(`${result.name} · ${result.bytes} B · ${result.sha256.slice(0, 16)}…`);
|
|
1051
|
+
} catch (issue) {
|
|
1052
|
+
setNote(String(issue.message ?? issue));
|
|
1053
|
+
} finally { setBusy(false); }
|
|
1054
|
+
};
|
|
1055
|
+
const save = async () => {
|
|
1056
|
+
if (!uri.trim()) return;
|
|
1057
|
+
setBusy(true); setNote('');
|
|
1058
|
+
try {
|
|
1059
|
+
await api('/trace/attachments', { method: 'POST', body: JSON.stringify({ id: trace.id, attachments: [{ kind: 'file', uri: uri.trim() }] }) });
|
|
1060
|
+
setUri('');
|
|
1061
|
+
setNote(t('attachmentSaved'));
|
|
1062
|
+
onSaved?.();
|
|
1063
|
+
} catch (issue) {
|
|
1064
|
+
setNote(String(issue.message ?? issue));
|
|
1065
|
+
} finally { setBusy(false); }
|
|
1066
|
+
};
|
|
1067
|
+
return h('section', { className: 'cortex-detail-section' }, [
|
|
1068
|
+
h('h4', {}, t('attachmentAddTitle')),
|
|
1069
|
+
h('div', { className: 'cortex-note' }, t('attachmentAddHint')),
|
|
1070
|
+
h('div', { className: 'cortex-line' }, [
|
|
1071
|
+
h('input', { className: 'cortex-input', value: uri, placeholder: 'file:///C:/path/rice.png', onChange: (event) => setUri(event.target.value) }),
|
|
1072
|
+
h('button', { type: 'button', className: 'cortex-btn', disabled: busy || !uri.trim(), onClick: inspect }, t('attachmentCheck')),
|
|
1073
|
+
h('button', { type: 'button', className: 'cortex-btn', disabled: busy || !uri.trim(), onClick: save }, t('attachmentSave')),
|
|
1074
|
+
]),
|
|
1075
|
+
note ? h('div', { className: 'cortex-meta' }, note) : null,
|
|
1076
|
+
]);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function ExtractionComposer({ trace, t, onSaved }) {
|
|
1080
|
+
const [uri, setUri] = useState('');
|
|
1081
|
+
const [mode, setMode] = useState('ocr');
|
|
1082
|
+
const [language, setLanguage] = useState('');
|
|
1083
|
+
const [targetId, setTargetId] = useState(trace.id);
|
|
1084
|
+
const [candidates, setCandidates] = useState([{ id: trace.id, title: trace.title || trace.snippet || trace.id }]);
|
|
1085
|
+
const [busy, setBusy] = useState(false);
|
|
1086
|
+
const [note, setNote] = useState('');
|
|
1087
|
+
useEffect(() => {
|
|
1088
|
+
let alive = true;
|
|
1089
|
+
api(`/timeline?id=${encodeURIComponent(trace.id)}&before=5&after=5`)
|
|
1090
|
+
.then((value) => {
|
|
1091
|
+
if (!alive) return;
|
|
1092
|
+
const list = [];
|
|
1093
|
+
const seen = new Set();
|
|
1094
|
+
const push = (item) => {
|
|
1095
|
+
if (!item?.id || seen.has(item.id)) return;
|
|
1096
|
+
seen.add(item.id);
|
|
1097
|
+
list.push({ id: item.id, title: item.title || item.snippet || item.id });
|
|
1098
|
+
};
|
|
1099
|
+
push(value?.anchor);
|
|
1100
|
+
for (const item of value?.items ?? []) push(item);
|
|
1101
|
+
if (!list.length) push({ id: trace.id, title: trace.title || trace.id });
|
|
1102
|
+
setCandidates(list.slice(0, 8));
|
|
1103
|
+
})
|
|
1104
|
+
.catch(() => {});
|
|
1105
|
+
return () => { alive = false; };
|
|
1106
|
+
}, [trace.id]);
|
|
1107
|
+
const run = async () => {
|
|
1108
|
+
if (!uri.trim()) return;
|
|
1109
|
+
setBusy(true); setNote('');
|
|
1110
|
+
try {
|
|
1111
|
+
const result = await api('/attachments/extract', {
|
|
1112
|
+
method: 'POST',
|
|
1113
|
+
body: JSON.stringify({ uri: uri.trim(), mode, language: language.trim() || undefined, traceId: targetId }),
|
|
1114
|
+
});
|
|
1115
|
+
setNote(`${t('extractSaved')} ${result.extractedTraceId ?? ''}`.trim());
|
|
1116
|
+
setUri('');
|
|
1117
|
+
onSaved?.();
|
|
1118
|
+
} catch (issue) {
|
|
1119
|
+
setNote(String(issue.message ?? issue));
|
|
1120
|
+
} finally { setBusy(false); }
|
|
1121
|
+
};
|
|
1122
|
+
return h('section', { className: 'cortex-detail-section' }, [
|
|
1123
|
+
h('h4', {}, t('extractTitle')),
|
|
1124
|
+
h('div', { className: 'cortex-note' }, t('extractHint')),
|
|
1125
|
+
h('div', { className: 'cortex-line' }, [
|
|
1126
|
+
h('input', { className: 'cortex-input', value: uri, placeholder: 'file:///C:/path/rice.png', onChange: (event) => setUri(event.target.value) }),
|
|
1127
|
+
h('select', { className: 'cortex-input', value: mode, onChange: (event) => setMode(event.target.value) }, [
|
|
1128
|
+
h('option', { key: 'ocr', value: 'ocr' }, 'OCR'),
|
|
1129
|
+
h('option', { key: 'asr', value: 'asr' }, 'ASR'),
|
|
1130
|
+
]),
|
|
1131
|
+
h('input', { className: 'cortex-input', value: language, placeholder: mode === 'ocr' ? 'zh-Hans' : 'zh', onChange: (event) => setLanguage(event.target.value) }),
|
|
1132
|
+
]),
|
|
1133
|
+
h('label', { className: 'cortex-line', style: { marginTop: 8 } }, [
|
|
1134
|
+
t('extractTarget'),
|
|
1135
|
+
h('select', { className: 'cortex-input', value: targetId, onChange: (event) => setTargetId(event.target.value) }, candidates.map((item) => h('option', { key: item.id, value: item.id }, `${item.title} · ${item.id}`))),
|
|
1136
|
+
]),
|
|
1137
|
+
h('div', { className: 'cortex-line' }, [
|
|
1138
|
+
h('button', { type: 'button', className: 'cortex-btn', disabled: busy || !uri.trim(), onClick: run }, t('extractRun')),
|
|
1139
|
+
h('span', { className: 'cortex-meta' }, targetId === trace.id ? t('extractSuggested') : t('extractChanged')),
|
|
1140
|
+
]),
|
|
1141
|
+
note ? h('div', { className: 'cortex-meta' }, note) : null,
|
|
1142
|
+
]);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
980
1145
|
function TraceDetail({ id, t, onClose, onTimeline }) {
|
|
981
1146
|
const [trace, setTrace] = useState(null);
|
|
982
1147
|
const [loading, setLoading] = useState(true);
|
|
@@ -1106,6 +1271,12 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1106
1271
|
h('h4', {}, t('detailTags')),
|
|
1107
1272
|
h('div', { className: 'cortex-chips' }, trace.tags.map((tag, index) => h(Badge, { key: `${tag}-${index}` }, tag))),
|
|
1108
1273
|
]) : null,
|
|
1274
|
+
(trace.metadata?.attachments ?? []).length ? h('section', { key: 'attachments', className: 'cortex-detail-section' }, [
|
|
1275
|
+
h('h4', {}, t('detailAttachments')),
|
|
1276
|
+
h('div', { className: 'cortex-note' }, trace.metadata.attachments.map((item, index) => h('div', { key: `${item.uri}-${index}` }, `${item.kind} · ${item.name ?? item.uri}${item.mime ? ` · ${item.mime}` : ''}${item.bytes ? ` · ${item.bytes} B` : ''}`))),
|
|
1277
|
+
]) : null,
|
|
1278
|
+
h(AttachmentComposer, { key: 'attach', trace, t, onSaved: loadTrace }),
|
|
1279
|
+
h(ExtractionComposer, { key: 'extract', trace, t, onSaved: loadTrace }),
|
|
1109
1280
|
(trace.keywords ?? []).length ? h('section', { key: 'keywords', className: 'cortex-detail-section' }, [
|
|
1110
1281
|
h('h4', {}, t('detailKeywords')),
|
|
1111
1282
|
h('div', { className: 'cortex-chips' }, trace.keywords.map((keyword, index) => h(Badge, { key: `${keyword}-${index}` }, keyword))),
|
|
@@ -1444,12 +1615,12 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1444
1615
|
useEffect(() => { load(); }, [load]);
|
|
1445
1616
|
const record = async () => {
|
|
1446
1617
|
if (!text.trim()) return;
|
|
1447
|
-
try { await api('/emotion/record', { method: 'POST', body: JSON.stringify({ text, target: '
|
|
1618
|
+
try { await api('/emotion/record', { method: 'POST', body: JSON.stringify({ text, target: '主人' }) }); setText(''); load(); } catch (issue) { setError(String(issue.message ?? issue)); }
|
|
1448
1619
|
};
|
|
1449
1620
|
return h('div', {}, [
|
|
1450
1621
|
error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed') }) : null,
|
|
1451
1622
|
h('div', { key: 'line', className: 'cortex-line' }, [
|
|
1452
|
-
h('input', { key: 'i', className: 'cortex-input', value: text, placeholder: '
|
|
1623
|
+
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(); } }),
|
|
1453
1624
|
h('button', { key: 'b', type: 'button', className: 'cortex-btn primary', onClick: record }, '记一下'),
|
|
1454
1625
|
]),
|
|
1455
1626
|
data ? h('div', {}, [
|
|
@@ -1629,14 +1800,32 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1629
1800
|
function GraphTab({ t }) {
|
|
1630
1801
|
const { data, error, busy, reload } = useEndpoint('/graph');
|
|
1631
1802
|
const { data: edgeData } = useEndpoint('/graph/edges');
|
|
1803
|
+
const candidates = useEndpoint('/graph/candidates');
|
|
1804
|
+
const [candidateNotice, setCandidateNotice] = useState('');
|
|
1805
|
+
const [candidateBusy, setCandidateBusy] = useState('');
|
|
1632
1806
|
const top = Array.isArray(data?.top) ? data.top : [];
|
|
1633
1807
|
const layout = buildGraphLayout(top);
|
|
1634
1808
|
const positions = new Map(layout.map((node) => [node.id, node]));
|
|
1635
1809
|
const edges = Array.isArray(edgeData?.edges) ? edgeData.edges.filter((edge) => positions.has(edge.from) && positions.has(edge.to)).slice(0, 120) : [];
|
|
1810
|
+
const pendingEdges = Array.isArray(candidates.data) ? candidates.data : [];
|
|
1636
1811
|
const shortLabel = (id) => {
|
|
1637
1812
|
const value = String(id ?? '');
|
|
1638
1813
|
return value.length > 18 ? `${value.slice(0, 16)}…` : value;
|
|
1639
1814
|
};
|
|
1815
|
+
const decideCandidate = async (id, action) => {
|
|
1816
|
+
setCandidateBusy(id);
|
|
1817
|
+
setCandidateNotice('');
|
|
1818
|
+
try {
|
|
1819
|
+
await api('/graph/candidates/action', { method: 'POST', body: JSON.stringify({ action, id }) });
|
|
1820
|
+
candidates.reload();
|
|
1821
|
+
reload();
|
|
1822
|
+
setCandidateNotice(action === 'approve' ? t('graphApprove') : t('graphReject'));
|
|
1823
|
+
} catch (issue) {
|
|
1824
|
+
setCandidateNotice(String(issue.message ?? issue));
|
|
1825
|
+
} finally {
|
|
1826
|
+
setCandidateBusy('');
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1640
1829
|
return h('div', {}, [
|
|
1641
1830
|
h('div', { key: 'hint', className: 'cortex-note' }, t('graphHint')),
|
|
1642
1831
|
h('div', { key: 'line', className: 'cortex-line' }, [
|
|
@@ -1644,6 +1833,8 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1644
1833
|
]),
|
|
1645
1834
|
error ? h(ErrorBlock, { key: 'err', message: error, prefix: t('requestFailed'), onRetry: reload, retryLabel: t('retry') }) : null,
|
|
1646
1835
|
busy ? h(LoadingBlock, { key: 'loading', label: t('loading') }) : null,
|
|
1836
|
+
candidates.error ? h(ErrorBlock, { key: 'cerr', message: candidates.error, prefix: t('requestFailed'), onRetry: candidates.reload, retryLabel: t('retry') }) : null,
|
|
1837
|
+
candidateNotice ? h('div', { key: 'cnotice', className: 'cortex-meta' }, candidateNotice) : null,
|
|
1647
1838
|
data ? h('div', { key: 'data' }, [
|
|
1648
1839
|
h('div', { key: 'stats', className: 'cortex-grid' }, [
|
|
1649
1840
|
h('div', { key: 'nodes', className: 'cortex-card cortex-stat' }, [h('h4', {}, t('graphNodes')), h('div', { className: 'cortex-big' }, data.nodes ?? 0)]),
|
|
@@ -1692,6 +1883,36 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1692
1883
|
]))),
|
|
1693
1884
|
]) : null,
|
|
1694
1885
|
]) : null,
|
|
1886
|
+
h('div', { key: 'candidates', className: 'cortex-card', style: { marginTop: 10 } }, [
|
|
1887
|
+
h('div', { key: 'head', className: 'cortex-card-head' }, [
|
|
1888
|
+
h('h4', {}, t('graphCandidatesTitle')),
|
|
1889
|
+
h('span', { key: 'count', className: 'cortex-meta' }, pendingEdges.length),
|
|
1890
|
+
]),
|
|
1891
|
+
h('div', { key: 'hint', className: 'cortex-meta' }, t('graphCandidatesHint')),
|
|
1892
|
+
candidates.busy ? h(LoadingBlock, { key: 'loading', label: t('loading') }) : null,
|
|
1893
|
+
pendingEdges.length ? h('div', { key: 'list', style: { marginTop: 6 } }, pendingEdges.map((edge) => h('div', {
|
|
1894
|
+
key: edge.id,
|
|
1895
|
+
className: 'cortex-item',
|
|
1896
|
+
style: { display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' },
|
|
1897
|
+
}, [
|
|
1898
|
+
h('span', { key: 'label', style: { flex: '1 1 240px', minWidth: 0 } },
|
|
1899
|
+
`${shortLabel(edge.fromId)} → ${shortLabel(edge.toId)} · ${edge.rel} · ${t('graphCandidateConfidence')} ${formatNumber(edge.metadata?.confidence ?? edge.weight ?? 0, 2)}`),
|
|
1900
|
+
h('button', {
|
|
1901
|
+
key: 'approve',
|
|
1902
|
+
type: 'button',
|
|
1903
|
+
className: 'cortex-btn',
|
|
1904
|
+
disabled: candidateBusy === edge.id,
|
|
1905
|
+
onClick: () => decideCandidate(edge.id, 'approve'),
|
|
1906
|
+
}, t('graphApprove')),
|
|
1907
|
+
h('button', {
|
|
1908
|
+
key: 'reject',
|
|
1909
|
+
type: 'button',
|
|
1910
|
+
className: 'cortex-btn',
|
|
1911
|
+
disabled: candidateBusy === edge.id,
|
|
1912
|
+
onClick: () => decideCandidate(edge.id, 'reject'),
|
|
1913
|
+
}, t('graphReject')),
|
|
1914
|
+
]))) : h(EmptyBlock, { key: 'empty' }, t('graphCandidatesEmpty')),
|
|
1915
|
+
]),
|
|
1695
1916
|
]);
|
|
1696
1917
|
}
|
|
1697
1918
|
|
|
@@ -1782,7 +2003,28 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1782
2003
|
function SettingsTab({ t }) {
|
|
1783
2004
|
const settings = useEndpoint('/settings');
|
|
1784
2005
|
const metrics = useEndpoint('/metrics');
|
|
2006
|
+
const grants = useEndpoint('/security/grants');
|
|
1785
2007
|
const [notice, setNotice] = useState('');
|
|
2008
|
+
const [grantName, setGrantName] = useState('shared:agent-a');
|
|
2009
|
+
const [grantUntil, setGrantUntil] = useState('');
|
|
2010
|
+
const [grantNotice, setGrantNotice] = useState('');
|
|
2011
|
+
const [grantBusy, setGrantBusy] = useState(false);
|
|
2012
|
+
const addGrant = async () => {
|
|
2013
|
+
setGrantBusy(true); setGrantNotice('');
|
|
2014
|
+
try {
|
|
2015
|
+
await api('/security/grants', { method: 'POST', body: JSON.stringify({ action: 'add', grant: grantName.trim(), until: grantUntil.trim() || null }) });
|
|
2016
|
+
grants.reload();
|
|
2017
|
+
setGrantNotice(t('scopeGrantSaved'));
|
|
2018
|
+
} catch (issue) { setGrantNotice(String(issue.message ?? issue)); } finally { setGrantBusy(false); }
|
|
2019
|
+
};
|
|
2020
|
+
const revokeGrant = async (grant) => {
|
|
2021
|
+
setGrantBusy(true); setGrantNotice('');
|
|
2022
|
+
try {
|
|
2023
|
+
await api('/security/grants', { method: 'POST', body: JSON.stringify({ action: 'revoke', grant }) });
|
|
2024
|
+
grants.reload();
|
|
2025
|
+
setGrantNotice(t('scopeGrantSaved'));
|
|
2026
|
+
} catch (issue) { setGrantNotice(String(issue.message ?? issue)); } finally { setGrantBusy(false); }
|
|
2027
|
+
};
|
|
1786
2028
|
const refreshAll = () => { settings.reload(); metrics.reload(); };
|
|
1787
2029
|
const changePermission = async (key, value) => {
|
|
1788
2030
|
if (!window.confirm(t('permissionConfirm'))) return;
|
|
@@ -1817,6 +2059,7 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1817
2059
|
['scope', t('scope'), setting.scope],
|
|
1818
2060
|
['dataRoot', t('dataRoot'), setting.dataRoot],
|
|
1819
2061
|
['dbFile', t('dbFile'), setting.dbFile],
|
|
2062
|
+
['readOnlyScopes', t('readOnlyScopes'), (setting.readOnlyScopes ?? []).join(', ') || '—'],
|
|
1820
2063
|
['contextBudget', t('contextBudget'), setting.contextBudget],
|
|
1821
2064
|
['promptBudget', t('promptBudget'), setting.promptBudget],
|
|
1822
2065
|
].map(([key, label, value]) => [key, label, displayValue(value)]) }),
|
|
@@ -1871,6 +2114,28 @@ button.cortex-badge:disabled{cursor:not-allowed;opacity:.55}
|
|
|
1871
2114
|
]),
|
|
1872
2115
|
notice ? h('div', { className: 'cortex-note' }, notice) : null,
|
|
1873
2116
|
]),
|
|
2117
|
+
h('section', { key: 'grants', className: 'cortex-card' }, [
|
|
2118
|
+
h('h4', {}, t('scopeGrants')),
|
|
2119
|
+
h('p', { className: 'cortex-note' }, t('scopeGrantsHint')),
|
|
2120
|
+
grants.error ? h(ErrorBlock, { key: 'gerr', message: grants.error, prefix: t('requestFailed'), onRetry: grants.reload, retryLabel: t('retry') }) : null,
|
|
2121
|
+
grants.busy ? h(LoadingBlock, { key: 'gload', label: t('loading') }) : null,
|
|
2122
|
+
(grants.data?.grants ?? []).length
|
|
2123
|
+
? h('div', { key: 'glist', className: 'cortex-grid' }, grants.data.grants.map((item) => h('div', { key: item.grant, className: 'cortex-row', style: { justifyContent: 'space-between', gap: 8 } }, [
|
|
2124
|
+
h('code', { key: 'g', className: 'cortex-code' }, item.grant),
|
|
2125
|
+
h('span', { key: 'u', className: 'cortex-meta' }, item.until ? formatTime(item.until) : t('scopeGrantForever')),
|
|
2126
|
+
h('button', { key: 'r', type: 'button', className: 'cortex-btn', disabled: grantBusy, onClick: () => revokeGrant(item.grant) }, t('scopeGrantRevoke')),
|
|
2127
|
+
])))
|
|
2128
|
+
: h(EmptyBlock, { key: 'gempty' }, t('scopeGrantEmpty')),
|
|
2129
|
+
h('div', { key: 'gadd', className: 'cortex-grid', style: { marginTop: 8 } }, [
|
|
2130
|
+
h('label', { key: 'name' }, [t('scopeGrantName'), h('input', { className: 'cortex-input', value: grantName, placeholder: 'shared:agent-a', onChange: (event) => setGrantName(event.target.value) })]),
|
|
2131
|
+
h('label', { key: 'until' }, [t('scopeGrantUntil'), h('input', { className: 'cortex-input', value: grantUntil, placeholder: '2026-12-31T00:00:00Z', onChange: (event) => setGrantUntil(event.target.value) })]),
|
|
2132
|
+
]),
|
|
2133
|
+
h('div', { key: 'gline', className: 'cortex-line' }, [
|
|
2134
|
+
h('button', { key: 'add', type: 'button', className: 'cortex-btn', disabled: grantBusy, onClick: addGrant }, t('scopeGrantAdd')),
|
|
2135
|
+
grantNotice ? h('span', { key: 'n', className: 'cortex-meta' }, grantNotice) : null,
|
|
2136
|
+
]),
|
|
2137
|
+
(grants.data?.revoked ?? []).length ? h('div', { key: 'revoked', className: 'cortex-note' }, `${t('scopeGrantRevoked')}: ${grants.data.revoked.join(', ')}`) : null,
|
|
2138
|
+
]),
|
|
1874
2139
|
h('section', { key: 'vector', className: 'cortex-card' }, [
|
|
1875
2140
|
h('h4', {}, t('vector')),
|
|
1876
2141
|
setting.vector
|