/** * Graph Database — SQLite for the Graph Agent architecture (P0 + P2) * * 表结构: * system_agents 全局 / 模板 / 公司专属 agent 元数据(TEXT PRIMARY KEY = 目录名) * org_graph_nodes 公司组织图节点(company 对某个 system_agent 的引用 + 角色名) * org_graph_edges 公司组织图**执行流**边(P2 起:condition + edge_kind,与 work_graph_edges 同构) * * P2(27-p2-org-graph.md §3.2)的三处改造: * 1. org_graph_nodes 追加 status(软删除,供 fire 用)+ reports_to_node_id(汇报关系降级为节点属性) * 2. 全量唯一索引 idx_org_graph_nodes_company_role → 部分唯一索引 ..._active * (fire 成 status='removed' 之后,同角色能重新 hire) * 3. org_graph_edges 从「汇报关系」重建为「执行流边」 * * 本文件所有 DDL 都是幂等的,getDb() 可重复调用 / 进程可反复重启。 */ const Database = require('better-sqlite3'); const path = require('path'); const fs = require('fs'); const DATA_DIR = path.resolve(__dirname, '../../data'); const DB_PATH = process.env.GRAPH_DB_PATH || path.join(DATA_DIR, 'graph.sqlite'); let db = null; /** 表是否已有某列(SQLite 的 ALTER TABLE ADD COLUMN 对已存在的列会直接报错) */ function hasColumn(database, table, column) { return database.pragma(`table_info(${table})`).some((c) => c.name === column); } /** * 存量库迁移。新建库走上面的 CREATE TABLE 就已经是目标形态,这里全部是 no-op。 */ function migrate(database) { // 1. org_graph_nodes 追加两列 if (!hasColumn(database, 'org_graph_nodes', 'status')) { database.exec(`ALTER TABLE org_graph_nodes ADD COLUMN status TEXT NOT NULL DEFAULT 'active'`); } if (!hasColumn(database, 'org_graph_nodes', 'reports_to_node_id')) { // 默认值必须是 NULL,SQLite 才允许 ADD COLUMN 带 REFERENCES database.exec( `ALTER TABLE org_graph_nodes ADD COLUMN reports_to_node_id INTEGER REFERENCES org_graph_nodes(id)` ); } // 2. P1 建的全量唯一索引挡住了「fire 后重新 hire 同角色」,换成只约束 active 行的部分索引 // 注意 graph-service.js 的 ensureOrgGraphNodeIndex() 也必须同步改成建这个部分索引, // 否则这里 DROP 掉的索引会被它重新建出来。 database.exec(` DROP INDEX IF EXISTS idx_org_graph_nodes_company_role; CREATE UNIQUE INDEX IF NOT EXISTS idx_org_graph_nodes_company_role_active ON org_graph_nodes(company_id, role_name) WHERE status = 'active'; `); // 3. 老的 org_graph_edges(relation_type 汇报关系)从建表至今零数据,直接 DROP 重建,无迁移负担 if (hasColumn(database, 'org_graph_edges', 'relation_type')) { database.exec(`DROP TABLE org_graph_edges`); createEdgesTable(database); } database.exec(` CREATE UNIQUE INDEX IF NOT EXISTS idx_org_graph_edges_dedupe_active ON org_graph_edges(company_id, from_node_id, to_node_id, condition) WHERE status = 'active'; `); } function createEdgesTable(database) { database.exec(` CREATE TABLE IF NOT EXISTS org_graph_edges ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id TEXT NOT NULL, from_node_id INTEGER NOT NULL REFERENCES org_graph_nodes(id), to_node_id INTEGER NOT NULL REFERENCES org_graph_nodes(id), condition TEXT NOT NULL CHECK (condition IN ('success','fail')), edge_kind TEXT NOT NULL DEFAULT 'forward' CHECK (edge_kind IN ('forward','backward')), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL ); `); } function getDb() { if (db) return db; if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } db = new Database(DB_PATH); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS system_agents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, scope TEXT NOT NULL CHECK (scope IN ('public','company')), owner_company_id TEXT, source_kind TEXT NOT NULL CHECK (source_kind IN ('global_agent','template','company_hired')), legacy_path TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', benchmark_score REAL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS org_graph_nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, company_id TEXT NOT NULL, system_agent_id TEXT NOT NULL REFERENCES system_agents(id), role_name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', reports_to_node_id INTEGER REFERENCES org_graph_nodes(id), created_at TEXT NOT NULL ); `); createEdgesTable(db); migrate(db); return db; } module.exports = { getDb, };