local-knowledge-graph 1.5.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/.opencode/skill/kg-triples/SKILL.md +55 -0
- package/HELP.md +112 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/cli.js +86 -0
- package/lib/agent.js +211 -0
- package/lib/ask.js +290 -0
- package/lib/db.js +613 -0
- package/lib/embeddings.js +166 -0
- package/lib/git.js +175 -0
- package/lib/importer.js +54 -0
- package/lib/inference.js +123 -0
- package/lib/paths.js +53 -0
- package/lib/rdf.js +64 -0
- package/lib/updater.js +144 -0
- package/lib/validator.js +146 -0
- package/lib/vectors.js +68 -0
- package/lib/viewer.js +33 -0
- package/lib/viewer_template.html +283 -0
- package/mcp/server.js +262 -0
- package/package.json +44 -0
- package/public/app.js +1761 -0
- package/public/index.html +238 -0
- package/public/style.css +374 -0
- package/public/vendor/OrbitControls.js +1045 -0
- package/public/vendor/three.min.js +6 -0
- package/server.js +625 -0
- package/tools/ego.js +122 -0
- package/tools/search.js +101 -0
package/tools/ego.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// 只读中心层级子图查询工具(供 OpenCode Agent 与命令行使用)
|
|
5
|
+
// 用法: node tools/ego.js <中心实体id或名称> [层数] [--json]
|
|
6
|
+
// 层数省略 = 全部层级;名称多义时列出候选并以退出码2结束
|
|
7
|
+
// 退出码: 0成功 / 1实体不存在或参数错误 / 2名称多义 / 3数据库异常
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
11
|
+
|
|
12
|
+
const { DB_PATH } = require('../lib/paths');
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const args = { center: null, depth: null, json: false };
|
|
16
|
+
for (const a of argv) {
|
|
17
|
+
if (a === '--json') { args.json = true; continue; }
|
|
18
|
+
if (args.center === null) args.center = a;
|
|
19
|
+
else if (args.depth === null) args.depth = a;
|
|
20
|
+
}
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail(msg, code) {
|
|
25
|
+
console.error(msg);
|
|
26
|
+
process.exit(code);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function main() {
|
|
30
|
+
const args = parseArgs(process.argv.slice(2));
|
|
31
|
+
if (!args.center) fail('用法: node tools/ego.js <中心实体id或名称> [层数] [--json]', 1);
|
|
32
|
+
|
|
33
|
+
let db;
|
|
34
|
+
try {
|
|
35
|
+
db = new DatabaseSync(DB_PATH, { readOnly: true });
|
|
36
|
+
} catch (e) {
|
|
37
|
+
fail(`无法以只读方式打开数据库 ${DB_PATH}: ${e.message}`, 3);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let depth = null;
|
|
41
|
+
if (args.depth !== null) {
|
|
42
|
+
depth = Number(args.depth);
|
|
43
|
+
if (!Number.isInteger(depth) || depth < 0) fail('层数必须为非负整数(省略或0表示全部层级)', 1);
|
|
44
|
+
if (depth === 0) depth = null; // 0 = 全部层级
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const centerKey = String(args.center).trim();
|
|
48
|
+
let center = null;
|
|
49
|
+
if (/^\d+$/.test(centerKey)) {
|
|
50
|
+
center = db.prepare('SELECT id, name, category FROM entities WHERE id = ?').get(Number(centerKey)) || null;
|
|
51
|
+
}
|
|
52
|
+
if (!center) {
|
|
53
|
+
const hits = db.prepare('SELECT id, name, category FROM entities WHERE name = ? ORDER BY id').all(centerKey);
|
|
54
|
+
if (hits.length === 0) fail(`实体"${centerKey}"不存在`, 1);
|
|
55
|
+
if (hits.length > 1) {
|
|
56
|
+
console.error(`实体名"${centerKey}"存在${hits.length}个候选,请改用id:`);
|
|
57
|
+
for (const h of hits) console.error(` id=${h.id} "${h.name}" [${h.category}]`);
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
center = hits[0];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 双向BFS:level = 到中心的最短跳数
|
|
64
|
+
const maxDepth = depth === null ? Infinity : depth;
|
|
65
|
+
const relations = db.prepare('SELECT id, source_id, target_id, name, category FROM relations ORDER BY id').all();
|
|
66
|
+
const adj = new Map();
|
|
67
|
+
const touch = (id) => { if (!adj.has(id)) adj.set(id, []); };
|
|
68
|
+
for (const r of relations) {
|
|
69
|
+
touch(r.source_id); touch(r.target_id);
|
|
70
|
+
adj.get(r.source_id).push(r);
|
|
71
|
+
adj.get(r.target_id).push(r);
|
|
72
|
+
}
|
|
73
|
+
const level = new Map([[center.id, 0]]);
|
|
74
|
+
let frontier = [center.id];
|
|
75
|
+
while (frontier.length) {
|
|
76
|
+
const next = [];
|
|
77
|
+
for (const id of frontier) {
|
|
78
|
+
const cur = level.get(id);
|
|
79
|
+
if (cur >= maxDepth) continue;
|
|
80
|
+
for (const r of adj.get(id) || []) {
|
|
81
|
+
const other = r.source_id === id ? r.target_id : r.source_id;
|
|
82
|
+
if (!level.has(other)) { level.set(other, cur + 1); next.push(other); }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
frontier = next;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const entities = db.prepare('SELECT id, name, category, attributes, source FROM entities ORDER BY id').all()
|
|
89
|
+
.filter((e) => level.has(e.id))
|
|
90
|
+
.map((e) => ({ ...e, level: level.get(e.id) }));
|
|
91
|
+
const subRels = relations.filter((r) => level.has(r.source_id) && level.has(r.target_id));
|
|
92
|
+
db.close();
|
|
93
|
+
|
|
94
|
+
if (args.json) {
|
|
95
|
+
console.log(JSON.stringify({ center: { ...center, level: 0 }, depth: depth === null ? null : depth, entities, relations: subRels }, null, 2));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nameById = new Map(entities.map((e) => [e.id, e.name]));
|
|
100
|
+
console.log(`中心实体: id=${center.id} "${center.name}" [${center.category}]`);
|
|
101
|
+
console.log(`层级范围: ${depth === null ? '全部' : depth} 层 | 子图规模: ${entities.length} 实体 / ${subRels.length} 关系`);
|
|
102
|
+
const byLevel = new Map();
|
|
103
|
+
for (const e of entities) {
|
|
104
|
+
if (!byLevel.has(e.level)) byLevel.set(e.level, []);
|
|
105
|
+
byLevel.get(e.level).push(e);
|
|
106
|
+
}
|
|
107
|
+
for (const lv of [...byLevel.keys()].sort((a, b) => a - b)) {
|
|
108
|
+
console.log(`\n── 第 ${lv} 层(${lv === 0 ? '中心' : `经${lv}跳关联`})──`);
|
|
109
|
+
for (const e of byLevel.get(lv)) {
|
|
110
|
+
let attrs = {};
|
|
111
|
+
try { attrs = JSON.parse(e.attributes || '{}'); } catch (_) {}
|
|
112
|
+
const a = Object.keys(attrs).length ? ` 属性:${JSON.stringify(attrs)}` : '';
|
|
113
|
+
console.log(`- id=${e.id} "${e.name}" [${e.category}]${a}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log('\n── 子图内关系 ──');
|
|
117
|
+
for (const r of subRels) {
|
|
118
|
+
console.log(`- id=${r.id} e${r.source_id}"${nameById.get(r.source_id)}" --(${r.category}/${r.name})--> e${r.target_id}"${nameById.get(r.target_id)}"`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main();
|
package/tools/search.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// 只读检索工具(供 OpenCode Agent 与命令行使用)
|
|
5
|
+
// 用法:
|
|
6
|
+
// node tools/search.js keyword <关键词> [数量上限,默认10]
|
|
7
|
+
// node tools/search.js cypher "<MATCH (a)-[r:类型]->(b) WHERE ... RETURN ... [LIMIT n]>"
|
|
8
|
+
// 退出码: 0成功 / 1参数错误 / 2查询被拒(Cypher语法仅支持只读MATCH) / 3数据库异常
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
12
|
+
|
|
13
|
+
const { DB_PATH } = require('../lib/paths');
|
|
14
|
+
|
|
15
|
+
function fail(msg, code) {
|
|
16
|
+
console.error(msg);
|
|
17
|
+
process.exit(code);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function main() {
|
|
21
|
+
const [mode, a1, a2] = process.argv.slice(2);
|
|
22
|
+
if (!mode) fail('用法: node tools/search.js keyword <词> [topK] | cypher "<MATCH...>"', 1);
|
|
23
|
+
|
|
24
|
+
let db;
|
|
25
|
+
try {
|
|
26
|
+
db = new DatabaseSync(DB_PATH, { readOnly: true });
|
|
27
|
+
} catch (e) {
|
|
28
|
+
fail(`无法以只读方式打开数据库 ${DB_PATH}: ${e.message}`, 3);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const entities = db.prepare('SELECT * FROM entities ORDER BY id').all();
|
|
32
|
+
const relations = db.prepare('SELECT * FROM relations ORDER BY id').all();
|
|
33
|
+
const byId = new Map(entities.map((e) => [e.id, e]));
|
|
34
|
+
|
|
35
|
+
if (mode === 'keyword') {
|
|
36
|
+
const q = String(a1 || '').trim();
|
|
37
|
+
if (!q) fail('keyword模式需要关键词', 1);
|
|
38
|
+
const limit = Math.max(1, Math.min(Number(a2) || 10, 50));
|
|
39
|
+
const terms = q.toLowerCase().split(/\s+/);
|
|
40
|
+
const scored = [];
|
|
41
|
+
for (const e of entities) {
|
|
42
|
+
let score = 0;
|
|
43
|
+
const name = e.name.toLowerCase();
|
|
44
|
+
let attrText = '';
|
|
45
|
+
try { attrText = JSON.stringify(JSON.parse(e.attributes || '{}')); } catch (_) { attrText = String(e.attributes || ''); }
|
|
46
|
+
for (const t of terms) {
|
|
47
|
+
if (name === t) score += 10;
|
|
48
|
+
else if (name.includes(t)) score += 5;
|
|
49
|
+
else if (attrText.toLowerCase().includes(t)) score += 1;
|
|
50
|
+
}
|
|
51
|
+
if (score > 0) scored.push({ e, score });
|
|
52
|
+
}
|
|
53
|
+
scored.sort((x, y) => y.score - x.score);
|
|
54
|
+
const out = scored.slice(0, limit).map(({ e }) => ({
|
|
55
|
+
id: e.id, name: e.name, category: e.category, attributes: e.attributes,
|
|
56
|
+
relations: relations.filter((r) => r.source_id === e.id || r.target_id === e.id)
|
|
57
|
+
.map((r) => `${byId.get(r.source_id)?.name || '#' + r.source_id} —[${r.name}]→ ${byId.get(r.target_id)?.name || '#' + r.target_id}`),
|
|
58
|
+
}));
|
|
59
|
+
console.log(JSON.stringify(out, null, 1));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (mode === 'cypher') {
|
|
64
|
+
const q = String(a1 || '').trim();
|
|
65
|
+
if (!q) fail('cypher模式需要查询语句', 1);
|
|
66
|
+
const { execSync } = require('child_process');
|
|
67
|
+
// 复用主库的miniCypher实现:临时以子进程加载lib/db(其内部有自己的打开逻辑),这里直接内联最小实现
|
|
68
|
+
const m = q.match(/MATCH\s*\(\s*(\w+)?\s*\)\s*-\s*\[\s*(\w+)?\s*(?::\s*([^\]]+?))?\s*\]\s*->\s*\(\s*(\w+)?\s*\)\s*(WHERE\s+[\s\S]+?)?\s*RETURN\s+([\s\S]+?)(?:\s+LIMIT\s+(\d+))?\s*$/i);
|
|
69
|
+
if (!m) fail('仅支持形如 MATCH (a)-[r:类型]->(b) WHERE ... RETURN ... LIMIT n 的只读查询', 2);
|
|
70
|
+
const relType = (m[3] || '').trim();
|
|
71
|
+
const where = (m[5] || '').trim();
|
|
72
|
+
const limit = Math.min(Number(m[7]) || 50, 50);
|
|
73
|
+
const conds = where ? where.replace(/^WHERE\s+/i, '').split(/\s+AND\s+/i) : [];
|
|
74
|
+
const hits = [];
|
|
75
|
+
for (const r of relations) {
|
|
76
|
+
if (relType && r.category !== relType && r.name !== relType) continue;
|
|
77
|
+
const s = byId.get(r.source_id), t = byId.get(r.target_id);
|
|
78
|
+
if (!s || !t) continue;
|
|
79
|
+
let ok = true;
|
|
80
|
+
for (const cond of conds) {
|
|
81
|
+
const cm = cond.match(/(\w+)\.(name|category|source_id|target_id)\s*(=|contains)\s*['"]?([^'"]+?)['"]?\s*$/i);
|
|
82
|
+
if (!cm) { ok = false; break; }
|
|
83
|
+
const [, , field, op, rawVal] = cm;
|
|
84
|
+
const map = { a: s, b: t };
|
|
85
|
+
const node = map[cm[1].toLowerCase()];
|
|
86
|
+
if (!node) { ok = false; break; }
|
|
87
|
+
const val = String(node[field] ?? '');
|
|
88
|
+
const target = op.toLowerCase() === '=' ? rawVal.trim() : rawVal.trim().toLowerCase();
|
|
89
|
+
if (op.toLowerCase() === '=' ? val !== target : !val.toLowerCase().includes(target)) { ok = false; break; }
|
|
90
|
+
}
|
|
91
|
+
if (ok) hits.push({ source: s.name, source_id: s.id, relation: r.name, relation_id: r.id, category: r.category, target: t.name, target_id: t.id });
|
|
92
|
+
if (hits.length >= limit) break;
|
|
93
|
+
}
|
|
94
|
+
console.log(JSON.stringify(hits, null, 1));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fail(`未知模式"${mode}",可用: keyword / cypher`, 1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
main();
|