local-knowledge-graph 1.8.0 → 1.8.1
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/lib/db.js +43 -7
- package/mcp/core.js +39 -5
- package/package.json +1 -1
- package/server.js +3 -1
package/lib/db.js
CHANGED
|
@@ -484,26 +484,46 @@ function entNameIn(id) {
|
|
|
484
484
|
|
|
485
485
|
// 批量应用OpenCode操作(单事务原子提交,任一违规整体回滚,保持上一个合规版本)
|
|
486
486
|
// ref占位符解析:add_entity可带ref,后续关系用ref引用新实体
|
|
487
|
+
// add_relation 支持按名称引用(source_name/target_name):主名或别名精确匹配,事务内可见同批新建实体
|
|
487
488
|
function applyAgentOps(ops) {
|
|
488
489
|
const applied = [];
|
|
489
490
|
const refMap = {};
|
|
490
491
|
return tx(() => {
|
|
491
|
-
for (
|
|
492
|
-
|
|
492
|
+
for (let opIdx = 0; opIdx < ops.length; opIdx++) {
|
|
493
|
+
const op = ops[opIdx];
|
|
494
|
+
try {
|
|
495
|
+
if (!op || typeof op !== 'object') throw badOp('每个操作必须为JSON对象');
|
|
493
496
|
switch (op.op) {
|
|
494
497
|
case 'add_entity': {
|
|
495
498
|
const row = addEntityCore({ name: op.name, category: op.category, attributes: op.attributes }, 'OpenCode');
|
|
499
|
+
if (Array.isArray(op.aliases)) {
|
|
500
|
+
// 同事务内联写别名(addAlias 内部自带事务,嵌套会失败)
|
|
501
|
+
for (const raw of op.aliases) {
|
|
502
|
+
const n = cleanAlias(raw);
|
|
503
|
+
if (!n || n.length > 200 || n === row.name) continue;
|
|
504
|
+
const dup = db.prepare('SELECT id FROM aliases WHERE entity_id = ? AND alias = ?').get(row.id, n);
|
|
505
|
+
if (dup) continue;
|
|
506
|
+
if (aliasConflict(n, row.id)) continue;
|
|
507
|
+
const info = db.prepare('INSERT INTO aliases (entity_id, alias) VALUES (?, ?)').run(row.id, n);
|
|
508
|
+
const arow = db.prepare('SELECT id, entity_id, alias, created_at FROM aliases WHERE id = ?').get(Number(info.lastInsertRowid));
|
|
509
|
+
logOp('ADD_ALIAS', { alias: arow }, 'OpenCode');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
496
512
|
if (op.ref !== undefined) {
|
|
497
513
|
if (typeof op.ref !== 'string' || !op.ref.trim()) throw badOp('ref必须为非空字符串');
|
|
498
514
|
refMap[op.ref.trim()] = row.id;
|
|
499
515
|
}
|
|
500
|
-
applied.push({ op: 'add_entity', id: row.id, name: row.name });
|
|
516
|
+
applied.push({ op: 'add_entity', id: row.id, name: row.name, aliases_added: Array.isArray(op.aliases) ? op.aliases.length : 0 });
|
|
501
517
|
break;
|
|
502
518
|
}
|
|
503
519
|
case 'add_relation': {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
520
|
+
let sid;
|
|
521
|
+
let tid;
|
|
522
|
+
if (op.source_name !== undefined && op.source_id === undefined && op.source_ref === undefined) sid = resolveEntityName(op.source_name);
|
|
523
|
+
else sid = resolveRef(op.source_id, op.source_ref, refMap);
|
|
524
|
+
if (op.target_name !== undefined && op.target_id === undefined && op.target_ref === undefined) tid = resolveEntityName(op.target_name);
|
|
525
|
+
else tid = resolveRef(op.target_id, op.target_ref, refMap);
|
|
526
|
+
const row = addRelationCore({ source_id: sid, target_id: tid, name: op.name, category: op.category, confidence: op.confidence, source_ref: op.evidence_ref }, 'OpenCode');
|
|
507
527
|
applied.push({ op: 'add_relation', id: row.id, name: row.name });
|
|
508
528
|
break;
|
|
509
529
|
}
|
|
@@ -521,7 +541,7 @@ function applyAgentOps(ops) {
|
|
|
521
541
|
}
|
|
522
542
|
case 'update_relation': {
|
|
523
543
|
const patch = {};
|
|
524
|
-
for (const k of ['source_id', 'target_id', 'name', 'category']) if (op[k] !== undefined) patch[k] = op[k];
|
|
544
|
+
for (const k of ['source_id', 'target_id', 'name', 'category', 'confidence', 'source_ref']) if (op[k] !== undefined) patch[k] = op[k];
|
|
525
545
|
const row = updateRelationCore(Number(op.id), patch, 'OpenCode');
|
|
526
546
|
applied.push({ op: 'update_relation', id: row.id, name: row.name });
|
|
527
547
|
break;
|
|
@@ -534,6 +554,11 @@ function applyAgentOps(ops) {
|
|
|
534
554
|
default:
|
|
535
555
|
throw badOp(`未知操作类型"${op.op}",仅支持 add_entity/add_relation/update_entity/delete_entity/update_relation/delete_relation`);
|
|
536
556
|
}
|
|
557
|
+
} catch (e) {
|
|
558
|
+
const opKind = (op && op.op) || '未知';
|
|
559
|
+
e.message = `第${opIdx + 1}条操作(${opKind})失败: ${e.message}`;
|
|
560
|
+
throw e;
|
|
561
|
+
}
|
|
537
562
|
}
|
|
538
563
|
return applied;
|
|
539
564
|
});
|
|
@@ -548,6 +573,17 @@ function applyAgentOps(ops) {
|
|
|
548
573
|
if (!Number.isInteger(n) || n <= 0) throw badOp(`source_id/target_id必须为正整数或使用ref占位符`);
|
|
549
574
|
return n;
|
|
550
575
|
}
|
|
576
|
+
// 按名称解析实体id:主名或别名精确匹配;重名返回候选
|
|
577
|
+
function resolveEntityName(name) {
|
|
578
|
+
const hits = findNameConflicts(String(name).trim());
|
|
579
|
+
if (hits.length === 1) return hits[0].id;
|
|
580
|
+
if (hits.length > 1) {
|
|
581
|
+
const e = badOp(`实体名 "${name}" 存在${hits.length}个候选(${hits.map((h) => `id=${h.id}`).join(', ')}),请改用 source_id/target_id`);
|
|
582
|
+
e.candidates = hits.map((h) => ({ id: h.id, name: h.name, category: h.category, via: h.via }));
|
|
583
|
+
throw e;
|
|
584
|
+
}
|
|
585
|
+
throw badOp(`实体 "${name}" 不存在(本批或库中均未找到)`);
|
|
586
|
+
}
|
|
551
587
|
function badOp(msg) { const e = new Error('OpenCode操作校验未通过: ' + msg); e.status = 400; return e; }
|
|
552
588
|
}
|
|
553
589
|
|
package/mcp/core.js
CHANGED
|
@@ -32,7 +32,7 @@ const TOOLS = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: 'kg_get_entity',
|
|
35
|
-
description: '按id
|
|
35
|
+
description: '按id或名称获取单个实体详情(含其全部关系与别名)',
|
|
36
36
|
inputSchema: {
|
|
37
37
|
type: 'object',
|
|
38
38
|
properties: { id: { type: 'number' }, name: { type: 'string' } },
|
|
@@ -115,13 +115,22 @@ const TOOLS = [
|
|
|
115
115
|
},
|
|
116
116
|
{
|
|
117
117
|
name: 'kg_apply_ops',
|
|
118
|
-
description: '写入图谱操作(kg-ops协议JSON数组):add_entity/add_relation/update_entity/update_relation/delete_entity/delete_relation
|
|
118
|
+
description: '写入图谱操作(kg-ops协议JSON数组):add_entity/add_relation/update_entity/update_relation/delete_entity/delete_relation。add_relation 支持按名称引用(source_name/target_name,含别名,重名时返回候选要求改用id);实体间同批引用可用 ref 占位符(add_entity 传 ref,add_relation 传 source_ref/target_ref,仅同一次调用内有效);关系可带 confidence(确证/推测/存疑)与 evidence_ref(来源引用文本)。add_entity 支持 aliases 数组。所有写入均记入operation_logs并自动git保存点',
|
|
119
119
|
inputSchema: {
|
|
120
120
|
type: 'object',
|
|
121
121
|
properties: { ops: { type: 'array', items: { type: 'object' } } },
|
|
122
122
|
required: ['ops'],
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
name: 'kg_reset',
|
|
127
|
+
description: '清空图谱全部实体与关系(级联删除,自动保存点)。仅用于重建场景,必须显式传 confirm:true 才会执行',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: { confirm: { type: 'boolean', description: '必须为true才执行清空' } },
|
|
131
|
+
required: ['confirm'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
125
134
|
];
|
|
126
135
|
|
|
127
136
|
function createCore({ readonly = false, source = 'MCP' } = {}) {
|
|
@@ -133,7 +142,17 @@ function createCore({ readonly = false, source = 'MCP' } = {}) {
|
|
|
133
142
|
throw new Error(`实体id "${key}" 不存在`);
|
|
134
143
|
}
|
|
135
144
|
const hits = ents.filter((e) => e.name === key);
|
|
136
|
-
if (hits.length === 0)
|
|
145
|
+
if (hits.length === 0) {
|
|
146
|
+
const aliasMap = db.aliasMap();
|
|
147
|
+
const aliasHits = ents.filter((e) => (aliasMap[e.id] || []).some((a) => a === key));
|
|
148
|
+
if (aliasHits.length === 1) return { id: aliasHits[0].id };
|
|
149
|
+
if (aliasHits.length > 1) {
|
|
150
|
+
const e = new Error(`别名 "${key}" 对应${aliasHits.length}个实体,请改用id`);
|
|
151
|
+
e.candidates = aliasHits.map((h) => ({ id: h.id, name: h.name, category: h.category }));
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
throw new Error(`实体 "${key}" 不存在`);
|
|
155
|
+
}
|
|
137
156
|
if (hits.length > 1) {
|
|
138
157
|
const e = new Error(`实体名 "${key}" 存在${hits.length}个候选,请改用id`);
|
|
139
158
|
e.candidates = hits.map((h) => ({ id: h.id, name: h.name, category: h.category }));
|
|
@@ -142,6 +161,10 @@ function createCore({ readonly = false, source = 'MCP' } = {}) {
|
|
|
142
161
|
return { id: hits[0].id };
|
|
143
162
|
}
|
|
144
163
|
|
|
164
|
+
// add_relation 的名称引用解析已下沉到 db.applyAgentOps(事务内可见同批新建实体)
|
|
165
|
+
|
|
166
|
+
const aliasById = () => db.aliasMap();
|
|
167
|
+
|
|
145
168
|
const HANDLERS = {
|
|
146
169
|
kg_stats: async () => {
|
|
147
170
|
const c = db.counts();
|
|
@@ -155,7 +178,9 @@ function createCore({ readonly = false, source = 'MCP' } = {}) {
|
|
|
155
178
|
const total = ents.length;
|
|
156
179
|
const limit = Math.max(1, Math.min(Number(args.limit) || 100, 500));
|
|
157
180
|
const offset = Math.max(0, Number(args.offset) || 0);
|
|
158
|
-
|
|
181
|
+
const amap = aliasById();
|
|
182
|
+
const page = ents.slice(offset, offset + limit).map((e) => ({ ...e, aliases: amap[e.id] || [] }));
|
|
183
|
+
return { total, count: Math.min(limit, total - offset), offset, entities: page };
|
|
159
184
|
},
|
|
160
185
|
kg_get_entity: async (args) => {
|
|
161
186
|
let e = null;
|
|
@@ -167,7 +192,7 @@ function createCore({ readonly = false, source = 'MCP' } = {}) {
|
|
|
167
192
|
}
|
|
168
193
|
if (!e) throw new Error('实体不存在(请提供id或name)');
|
|
169
194
|
const rels = db.listRelations().filter((r) => r.source_id === e.id || r.target_id === e.id);
|
|
170
|
-
return { entity: e, relations: rels };
|
|
195
|
+
return { entity: { ...e, aliases: aliasById()[e.id] || [] }, relations: rels };
|
|
171
196
|
},
|
|
172
197
|
kg_get_graph: async (args) => {
|
|
173
198
|
const g = db.getGraph();
|
|
@@ -219,6 +244,15 @@ function createCore({ readonly = false, source = 'MCP' } = {}) {
|
|
|
219
244
|
try { git.savepoint(`${source}写入: ${applied.length} 项操作`, source); } catch (_) { /* 数据目录无git时忽略 */ }
|
|
220
245
|
return { applied_count: applied.length, applied };
|
|
221
246
|
},
|
|
247
|
+
kg_reset: async (args) => {
|
|
248
|
+
if (readonly) throw new Error('MCP运行于只读模式,清空被拒绝(可在应用设置中关闭只读)');
|
|
249
|
+
if (args.confirm !== true) throw new Error('清空图谱是危险操作,必须显式传 confirm:true 执行');
|
|
250
|
+
const ids = db.listEntities().map((e) => e.id);
|
|
251
|
+
if (!ids.length) return { deleted_entities: 0, message: '图谱已为空' };
|
|
252
|
+
db.applyAgentOps(ids.map((id) => ({ op: 'delete_entity', id })));
|
|
253
|
+
try { git.savepoint(`${source}清空: 删除${ids.length}个实体`, source); } catch (_) { /* 忽略 */ }
|
|
254
|
+
return { deleted_entities: ids.length, message: '图谱已清空,可通过保存点回溯恢复' };
|
|
255
|
+
},
|
|
222
256
|
};
|
|
223
257
|
|
|
224
258
|
return { TOOLS, HANDLERS };
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -42,7 +42,9 @@ const mcpHandler = createMcpHandler({
|
|
|
42
42
|
const PORT = Number(process.env.PORT || 3000);
|
|
43
43
|
const app = express();
|
|
44
44
|
app.use(express.json({ limit: '30mb' }));
|
|
45
|
-
app.use(express.static(path.join(__dirname, 'public')
|
|
45
|
+
app.use(express.static(path.join(__dirname, 'public'), {
|
|
46
|
+
setHeaders: (res) => { res.setHeader('Cache-Control', 'no-cache, must-revalidate'); }
|
|
47
|
+
}));
|
|
46
48
|
app.use('/uploads', express.static(path.join(git.DATA_DIR, 'uploads')));
|
|
47
49
|
|
|
48
50
|
const api = express.Router();
|