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