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/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: 4px;
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 7px; font-size: 11px; flex-shrink: 0; min-width: 0; }
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) => res.json(db.addEntity(req.body, '手工')));
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 }); }