delete-company-from-graph-db.js
5.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env node
/**
* delete-company-from-graph-db.js — 彻底删掉一家公司在 graph.sqlite 里的全部行
*
* 跟 scripts/sync-company-to-graph-db.js 是一对:那个脚本负责把公司的行搬进另一个库,
* 这个脚本负责把公司的行从一个库里删干净。用于清理僵尸/废弃公司——比如
* `data/companies/{cid}/company.json` 已经不存在(company-service.list() 靠这个文件
* 判断"这是不是一家公司",文件没了这家公司在界面上永远不可见,但 graph.sqlite 里的行、
* `data/companies/{cid}/` 目录下的残留文件不会跟着自动消失)。
*
* 只管 graph.sqlite 里的行,不碰 data/companies/{cid}/ 目录——目录是普通文件,
* 删之前自己看一眼里面还有没有想留的东西,确认没有再 `rm -rf`。
*
* 删除顺序是插入顺序的镜像(先删子表,最后删 system_agents,因为前面所有表都有外键指它):
* graph_runs.parent_node_run_id 先置 NULL(打破跟 graph_run_nodes 的循环外键)
* → graph_edge_contracts → graph_run_loop_records → graph_run_nodes → graph_runs
* → graph_files → work_graph_edges → work_graph_nodes → work_graphs
* → org_graph_edges → org_graph_nodes → company_agents → tasks → goals
* → system_agents(只删 owner_company_id = 这家公司、且已经没有任何 org/work 节点或雇佣关系
* 引用的行——像 org-coder / org-planner 这类按角色名生成 id 的本体,实测在本仓库的存量
* 数据里被多家公司共用同一行,owner_company_id 只反映最后一次雇它的是谁,见 2026-08-28
* 删 mycompany 时的教训。公共模板 scope='public' 的行本来就不会被这条 WHERE 命中)
*
* 用法:
* node scripts/delete-company-from-graph-db.js --db data/graph.sqlite --company mycompany [--dry-run]
*
* --dry-run 整个删除跑在一个事务里,最后主动抛出回滚,不留任何改动;控制台照常打印各表行数。
*/
const Database = require('better-sqlite3');
function parseArgs(argv) {
const out = { dryRun: argv.includes('--dry-run') };
for (const flag of ['--db', '--company']) {
const i = argv.indexOf(flag);
if (i !== -1) out[flag.slice(2)] = argv[i + 1];
}
return out;
}
const { db: dbPath, company, dryRun } = parseArgs(process.argv.slice(2));
if (!dbPath || !company) {
console.error('usage: node scripts/delete-company-from-graph-db.js --db <path> --company <id> [--dry-run]');
process.exit(1);
}
const db = new Database(dbPath);
db.pragma('foreign_keys = ON');
const log = (...a) => console.log(dryRun ? '[dry-run]' : '[delete]', ...a);
function del(table, whereSql, params = [company]) {
const info = db.prepare(`DELETE FROM ${table} ${whereSql}`).run(...params);
log(`${table}: ${info.changes} rows`);
}
const tx = db.transaction(() => {
// 打破 graph_runs ⇄ graph_run_nodes 的循环外键,否则删 graph_run_nodes 时会被
// 还指着它的 graph_runs.parent_node_run_id 拦住。
db.prepare(`UPDATE graph_runs SET parent_node_run_id = NULL WHERE company_id = ?`).run(company);
del('graph_edge_contracts',
`WHERE edge_layer = 'org' AND edge_id IN (SELECT CAST(id AS TEXT) FROM org_graph_edges WHERE company_id = ?)`);
del('graph_edge_contracts',
`WHERE edge_layer = 'work' AND edge_id IN (
SELECT id FROM work_graph_edges WHERE work_graph_id IN (SELECT id FROM work_graphs WHERE company_id = ?)
)`);
del('graph_run_loop_records', `WHERE run_id IN (SELECT id FROM graph_runs WHERE company_id = ?)`);
del('graph_run_nodes', `WHERE run_id IN (SELECT id FROM graph_runs WHERE company_id = ?)`);
del('graph_runs', `WHERE company_id = ?`);
del('graph_files', `WHERE company_id = ?`);
del('work_graph_edges', `WHERE work_graph_id IN (SELECT id FROM work_graphs WHERE company_id = ?)`);
del('work_graph_nodes', `WHERE work_graph_id IN (SELECT id FROM work_graphs WHERE company_id = ?)`);
del('work_graphs', `WHERE company_id = ?`);
del('org_graph_edges', `WHERE company_id = ?`);
del('org_graph_nodes', `WHERE company_id = ?`);
del('company_agents', `WHERE company_id = ?`);
del('tasks', `WHERE company_id = ?`);
del('goals', `WHERE company_id = ?`);
// system_agents 的 id 有时被多家公司共用同一行(角色名撞了就共用,见文件头注释更新)——
// owner_company_id 只反映"最后一次雇它的是谁",不代表没有别的公司还在用同一个 id。
// 只删这家公司名下、且已经没有任何 org/work 节点或雇佣关系再引用的行,避免删出悬空外键。
del('system_agents', `
WHERE owner_company_id = ?
AND id NOT IN (SELECT system_agent_id FROM org_graph_nodes)
AND id NOT IN (SELECT system_agent_id FROM work_graph_nodes)
AND id NOT IN (SELECT system_agent_id FROM company_agents)
`);
const fkErrors = db.pragma('foreign_key_check');
if (fkErrors.length) {
throw new Error('foreign_key_check failed: ' + JSON.stringify(fkErrors));
}
if (dryRun) throw new Error('__DRY_RUN_ROLLBACK__');
});
let failed = false;
try {
tx();
console.log('delete complete');
} catch (e) {
if (e.message === '__DRY_RUN_ROLLBACK__') {
log('rolled back (dry run) — no rows removed, foreign_key_check passed');
console.log('dry run complete, no changes persisted');
} else {
console.error(e.message);
failed = true;
}
} finally {
db.close();
}
if (failed) process.exit(1);