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 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 (const op of ops) {
492
- if (!op || typeof op !== 'object') throw badOp('每个操作必须为JSON对象');
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
- const sid = resolveRef(op.source_id, op.source_ref, refMap);
505
- const tid = resolveRef(op.target_id, op.target_ref, refMap);
506
- const row = addRelationCore({ source_id: sid, target_id: tid, name: op.name, category: op.category }, 'OpenCode');
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。所有写入均记入operation_logs并自动git保存点',
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) throw new Error(`实体 "${key}" 不存在`);
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
- return { total, count: Math.min(limit, total - offset), offset, entities: ents.slice(offset, offset + limit) };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "local-knowledge-graph",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "本地知识图谱整合器:3D可视化 + AI对话建图 + 智能检索 + Git回溯 + RDF合规,全流程本地运行",
5
5
  "main": "server.js",
6
6
  "bin": {
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();