local-knowledge-graph 1.6.0 → 1.7.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/HELP.md +2 -0
- package/lib/agent.js +4 -4
- package/lib/ask.js +59 -1
- package/lib/db.js +204 -9
- package/lib/embeddings.js +5 -1
- package/lib/ingest.js +355 -0
- package/lib/rdf.js +5 -0
- package/lib/similar.js +81 -0
- package/lib/validator.js +26 -2
- package/mcp/server.js +18 -0
- package/package.json +3 -2
- package/public/app.js +367 -33
- package/public/index.html +30 -1
- package/public/style.css +28 -5
- package/server.js +95 -1
package/public/style.css
CHANGED
|
@@ -12,12 +12,25 @@
|
|
|
12
12
|
height: 50px; display: flex; align-items: center; gap: 10px; padding: 0 14px;
|
|
13
13
|
background: #101728; border-bottom: 1px solid #1e2a44; flex-shrink: 0;
|
|
14
14
|
}
|
|
15
|
-
header h1 { font-size: 15px; font-weight: 600; color: #7fd1ff; letter-spacing: 1px; }
|
|
15
|
+
header h1 { font-size: 15px; font-weight: 600; color: #7fd1ff; letter-spacing: 1px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
16
16
|
header .spacer { flex: 1; }
|
|
17
17
|
header .badge { font-size: 11px; color: #8fa3c0; border: 1px solid #2a3a5c; border-radius: 10px; padding: 2px 9px; }
|
|
18
|
+
header button, header .badge { flex-shrink: 0; }
|
|
18
19
|
header .badge.ok { color: #7ee787; border-color: #2ea04366; }
|
|
19
20
|
header .badge.off { color: #f0883e; border-color: #f0883e66; }
|
|
20
21
|
|
|
22
|
+
/* 置信度徽章与过滤条 */
|
|
23
|
+
.conf-badge { font-size: 10px; border: 1px solid; border-radius: 8px; padding: 0 6px; margin-left: 5px; vertical-align: 1px; }
|
|
24
|
+
.conf-filter { display: flex; gap: 5px; margin-bottom: 8px; flex-wrap: wrap; }
|
|
25
|
+
.cf-chip { font-size: 11px; color: #8fa3c0; border: 1px solid #2a3a5c; border-radius: 10px; padding: 2px 10px; cursor: pointer; user-select: none; }
|
|
26
|
+
.cf-chip.active { color: #7fd1ff; border-color: #58a6ff; background: #16233f; }
|
|
27
|
+
.alias-chip { display: inline-block; font-size: 11px; color: #cfe2ff; border: 1px solid #2a3a5c; border-radius: 8px; padding: 0 6px; margin-right: 4px; }
|
|
28
|
+
.alias-chip i { font-style: normal; color: #f0883e; cursor: pointer; margin-left: 3px; }
|
|
29
|
+
.p-path { margin-bottom: 10px; border-top: 1px dashed #24344f; padding-top: 6px; }
|
|
30
|
+
.p-path:first-child { border-top: none; padding-top: 0; }
|
|
31
|
+
.p-head2 { font-size: 11.5px; color: #7fd1ff; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
|
|
32
|
+
.ask-path-row { line-height: 1.7; }
|
|
33
|
+
|
|
21
34
|
button {
|
|
22
35
|
background: #1b2b4d; color: #cfe2ff; border: 1px solid #2f4a7a; border-radius: 5px;
|
|
23
36
|
padding: 5px 12px; cursor: pointer; font-size: 12px; font-family: inherit;
|
|
@@ -277,13 +290,15 @@
|
|
|
277
290
|
/* 主题设置移动端可达:按钮保留,面板收窄为屏幕内浮层 */
|
|
278
291
|
#style-panel { max-width: calc(100vw - 16px); width: 288px; top: 8px; right: 8px; }
|
|
279
292
|
|
|
280
|
-
/*
|
|
293
|
+
/* 隐藏大标题,所有功能按钮单行排布,最大化画布显示范围;
|
|
294
|
+
按钮过多时允许横向滑动兜底(360px以下窄屏),滚动条隐藏 */
|
|
281
295
|
header {
|
|
282
|
-
flex-wrap: nowrap; height: 44px; padding: 0 6px; gap:
|
|
283
|
-
overflow: hidden;
|
|
296
|
+
flex-wrap: nowrap; height: 44px; padding: 0 6px; gap: 3px;
|
|
297
|
+
overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
|
|
284
298
|
}
|
|
299
|
+
header::-webkit-scrollbar { display: none; }
|
|
285
300
|
header h1 { display: none; }
|
|
286
|
-
header button { padding: 5px
|
|
301
|
+
header button { padding: 5px 6px; font-size: 11px; flex-shrink: 0; min-width: 0; }
|
|
287
302
|
#btn-export { display: none; } /* 导出RDF移入文件菜单 */
|
|
288
303
|
#stat-badge, #agent-badge { display: none; }
|
|
289
304
|
|
|
@@ -481,3 +496,11 @@
|
|
|
481
496
|
html[data-theme="light"] #file-menu button:active { background: #eaf3ff; }
|
|
482
497
|
html[data-theme="light"] .m-btn { background: #eaf3ff; }
|
|
483
498
|
html[data-theme="light"] #m-legend { background: rgba(255, 255, 255, 0.92); }
|
|
499
|
+
|
|
500
|
+
/* 浅色主题:置信度/别名/路径新增元素适配 */
|
|
501
|
+
html[data-theme="light"] .cf-chip { color: #5a6b85; border-color: #c9d6e8; }
|
|
502
|
+
html[data-theme="light"] .cf-chip.active { color: #1f6feb; border-color: #1f6feb; background: #e8f1ff; }
|
|
503
|
+
html[data-theme="light"] .alias-chip { color: #2c3e57; border-color: #c9d6e8; }
|
|
504
|
+
html[data-theme="light"] .alias-chip i { color: #c44f27; }
|
|
505
|
+
html[data-theme="light"] .p-path { border-top-color: #dfe7f2; }
|
|
506
|
+
html[data-theme="light"] .p-head2 { color: #1a5dc8; }
|
package/server.js
CHANGED
|
@@ -43,7 +43,27 @@ function agentSessionInfo() {
|
|
|
43
43
|
|
|
44
44
|
// ---------- 实体 ----------
|
|
45
45
|
api.get('/entities', (req, res) => res.json(db.listEntities()));
|
|
46
|
-
api.post('/entities', (req, res) =>
|
|
46
|
+
api.post('/entities', (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const body = req.body || {};
|
|
49
|
+
const name = body.name;
|
|
50
|
+
// 同名消歧:主名或别名占用时,按 merge_into / force 分流
|
|
51
|
+
if (typeof name === 'string' && name.trim()) {
|
|
52
|
+
const conflicts = db.findNameConflicts(name);
|
|
53
|
+
if (conflicts.length) {
|
|
54
|
+
const mergeInto = Number(body.merge_into);
|
|
55
|
+
if (body.merge_into !== undefined && Number.isInteger(mergeInto) && mergeInto > 0) {
|
|
56
|
+
const alias = db.addAlias(mergeInto, name.trim(), '手工');
|
|
57
|
+
return res.json({ merged: true, alias, entity: db.getEntity(mergeInto) });
|
|
58
|
+
}
|
|
59
|
+
if (body.force !== true && body.force !== 1 && body.force !== '1') {
|
|
60
|
+
return res.status(409).json({ error: `名称"${name.trim()}"已被占用,可并入现有实体、改用限定名或强制创建`, conflicts });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
res.json(db.addEntity(body, '手工'));
|
|
65
|
+
} catch (e) { res.status(e.status || 500).json({ error: e.message, conflicts: e.conflicts }); }
|
|
66
|
+
});
|
|
47
67
|
api.put('/entities/:id', (req, res) => res.json(db.updateEntity(Number(req.params.id), req.body, '手工')));
|
|
48
68
|
api.delete('/entities/:id', (req, res) => {
|
|
49
69
|
const r = db.deleteEntity(Number(req.params.id), '手工');
|
|
@@ -81,6 +101,80 @@ api.get('/graph/path', (req, res) => {
|
|
|
81
101
|
});
|
|
82
102
|
|
|
83
103
|
// ---------- 撤销最近操作(快照逆向写入) ----------
|
|
104
|
+
api.get('/aliases', (req, res) => res.json(db.aliasMap()));
|
|
105
|
+
|
|
106
|
+
api.get('/aliases/records', (req, res) => res.json(db.aliasRecords()));
|
|
107
|
+
|
|
108
|
+
api.post('/aliases', (req, res) => {
|
|
109
|
+
try {
|
|
110
|
+
const { entity_id, alias } = req.body || {};
|
|
111
|
+
res.json(db.addAlias(Number(entity_id), alias, '手工'));
|
|
112
|
+
} catch (e) { res.status(e.status || 500).json({ error: e.message, conflicts: e.conflicts }); }
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
api.delete('/aliases/:id', (req, res) => {
|
|
116
|
+
try { res.json(db.removeAlias(Number(req.params.id), '手工')); }
|
|
117
|
+
catch (e) { res.status(e.status || 500).json({ error: e.message }); }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
api.get('/graph/paths', (req, res) => {
|
|
121
|
+
try {
|
|
122
|
+
const fromId = db.resolveKey(req.query.from);
|
|
123
|
+
const toId = db.resolveKey(req.query.to);
|
|
124
|
+
let maxHops = 4;
|
|
125
|
+
if (req.query.max !== undefined && String(req.query.max).trim() !== '') {
|
|
126
|
+
maxHops = Number(req.query.max);
|
|
127
|
+
if (!Number.isInteger(maxHops)) return res.status(400).json({ error: 'max必须为整数' });
|
|
128
|
+
}
|
|
129
|
+
res.json(db.findPaths(fromId, toId, { maxHops }));
|
|
130
|
+
} catch (e) { res.status(e.status || 500).json({ error: e.message, candidates: e.candidates }); }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// ---------- 相似实体(语义Top-K,结构降级) ----------
|
|
134
|
+
api.get('/similar/:id', async (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
const k = req.query.k !== undefined ? Number(req.query.k) : 8;
|
|
137
|
+
res.json(await require('./lib/similar').similarEntities(Number(req.params.id), Number.isInteger(k) ? k : 8));
|
|
138
|
+
} catch (e) { res.status(e.status || 500).json({ error: e.message }); }
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ---------- 文档批量入图 ----------
|
|
142
|
+
const ingest = require('./lib/ingest');
|
|
143
|
+
ingest.loadPersisted();
|
|
144
|
+
|
|
145
|
+
api.post('/ingest', async (req, res) => {
|
|
146
|
+
if (agentBusy) return res.status(429).json({ error: '已有AI任务执行中(问答、导入或智能检索),请稍后再试' });
|
|
147
|
+
const { filename, content_b64, auto_commit } = req.body || {};
|
|
148
|
+
if (!filename || !content_b64) return res.status(400).json({ error: 'filename 与 content_b64 必填' });
|
|
149
|
+
let buf;
|
|
150
|
+
try { buf = Buffer.from(content_b64, 'base64'); } catch (_) { return res.status(400).json({ error: 'base64解码失败' }); }
|
|
151
|
+
agentBusy = true;
|
|
152
|
+
try {
|
|
153
|
+
res.json(await ingest.createTask(String(filename), buf, {
|
|
154
|
+
autoCommit: auto_commit === true || auto_commit === 1 || auto_commit === '1',
|
|
155
|
+
onSettled: () => { agentBusy = false; },
|
|
156
|
+
}));
|
|
157
|
+
} catch (e) {
|
|
158
|
+
agentBusy = false;
|
|
159
|
+
res.status(e.status || 500).json({ error: e.message });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
api.get('/ingest/tasks', (req, res) => res.json(ingest.listTasks()));
|
|
164
|
+
api.get('/ingest/:id', (req, res) => {
|
|
165
|
+
const t = ingest.getTask(req.params.id);
|
|
166
|
+
if (!t) return res.status(404).json({ error: '任务不存在' });
|
|
167
|
+
res.json(t);
|
|
168
|
+
});
|
|
169
|
+
api.post('/ingest/:id/commit', (req, res) => {
|
|
170
|
+
try { res.json(ingest.commitTask(req.params.id, (req.body || {}).selected || null, '手工')); }
|
|
171
|
+
catch (e) { res.status(e.status || 500).json({ error: e.message }); }
|
|
172
|
+
});
|
|
173
|
+
api.delete('/ingest/:id', (req, res) => {
|
|
174
|
+
try { res.json(ingest.deleteTask(req.params.id)); }
|
|
175
|
+
catch (e) { res.status(e.status || 500).json({ error: e.message }); }
|
|
176
|
+
});
|
|
177
|
+
|
|
84
178
|
api.post('/undo', (req, res) => {
|
|
85
179
|
try { res.json({ ok: true, ...db.undoLast('手工'), counts: db.counts(), version: db.getVersion() }); }
|
|
86
180
|
catch (e) { res.status(e.status || 500).json({ error: e.message }); }
|