/** * 清运行数据 —— 开发期反复用:把跑过的单清空,配置原样留着。 * * pnpm --filter @app/api reset-runs * * **清的是「跑出来的」**:任务、工作流、卡、任务记忆、事件、监听、台账行、活动记忆… * **留的是「配置好的」**:员工与岗位授权、会话、工作组、流程包、活动库、工具、 * 触发器、通道、模型实例、纪要、**邮箱账号**(它身上的游标与拉取状态是运行痕,跟着清)。 * * 直接走 SQL 而不绕模块:模块出口里没有、也不该有「删掉所有运行痕迹」这种动作 —— * 留痕只增不改是系统的规矩,清库是**开发期的外科手术**,不是系统的一项能力。 */ import { Pool } from 'pg'; import { loadConfig } from './config.ts'; /** 顺序无所谓:一条 TRUNCATE 里的表会一起截断,外键互指也不冲突 */ const RUN_TABLES = [ 'sys.task', 'sys.task_memory', 'sys.task_memory_seal', 'sys.task_view', 'sys.work_order', 'sys.chat_card', 'sys.chat_message', 'sys.system_message', /* * 沟通记录与上传件是**跑出来的**,而且两张表都没有指向 task 的外键 —— * `truncate ... cascade` 带不走它们(实测漏了 53 条沟通记录、1 个上传件)。 * 漏下的是指向已删任务的孤儿:那封信还在沟通记录里躺着,点开却没有单。 */ 'sys.task_comm', 'sys.upload', // 产出的资料文件存档 —— 同样是跑出来的、同样没有指向 task 的外键 'sys.task_asset_file', 'sys.node_instance', // 全链 Trace:span 是运行痕,与工作流同生共死 —— 清运行数据不清它,观测面会挂着一堆孤儿 'sys.trace_span', 'sys.workflow', 'sys.flow_state', 'sys.loop_state', 'sys.listener', 'sys.action_intent', 'sys.event', 'sys.event_delivery', 'sys.event_consumption', 'sys.outbound_mail', // 邮件收发的留痕是跑出来的;账号配置(sys.mail_account)留着 'sys.mail_inbound', 'sys.mail_inbound_delivery', 'sys.mail_outbound', 'sys.outbound_ref', 'sys.trigger_claim', // 待确认单是运行痕:命中未确认的那一票随运行数据一起清 —— 配置(trigger/node_owner)不动 'sys.trigger_pending', 'sys.report', 'sys.replay_model', 'sys.replay_tool', /* 活动记忆是跑出来的蒸馏物,跟着运行数据一起清 */ 'sys.activity_memory', 'sys.asset_increment', /* 纪要是人记的,但开发期同属「跑出来的东西」—— 清库要清得干净,不留半场旧会 */ 'sys.minute', 'sys.minute_resolution', /* 试跑日志是试跑的存档:单没了,存档留着只会是一堆点不进去的空目录 */ 'sys.testbed_run', ]; export async function resetRuns(): Promise { const pool = new Pool({ connectionString: loadConfig().databaseUrl, max: 2 }); try { /* * 台账的业务表是动态建的,名字问库不写死。 * * **主数据留着**(2026-07-31):客户 / 项目 / 供应商 / 市场是先于活动存在的档案, * 活动装了卸了它都在 —— 它属于「配置好的」那一半,不是「跑出来的」。 * 从前这里把整个 ledger schema 一并 truncate,后果是每清一次运行数据, * 样例客户就没了,「老客户来函」那条样例第二天就试不出来(而它本该是常在的夹具)。 * `__object_meta` 同理:性质登记随声明走,不随跑单走;抹了它, * 治理台上全部表都成了「无人声明」,主数据那一区直接空掉。 * 单据(record)、派生(derived)、没登记性质的,照旧清 —— 那些确是跑出来的。 */ const ledger = await pool.query<{ name: string }>( `select quote_ident(t.schemaname) || '.' || quote_ident(t.tablename) as name from pg_tables t left join ledger.__object_meta m on m.domain || '__' || m.object = t.tablename where t.schemaname = 'ledger' and t.tablename <> '__object_meta' and (m.kind is null or m.kind <> 'master')`, ); const all = [...RUN_TABLES, ...ledger.rows.map((r) => r.name)]; await pool.query(`truncate table ${all.join(', ')} restart identity cascade`); /* * 邮件账号是**配置**(留着),但它身上挂着的**游标是运行痕** —— 清了 `sys.mail_inbound` * 却留着游标,下一轮就从游标处往后拉:wecom 那十分钟重叠窗会把最近的信当新信**再投一次** * (库里已经没有那一行了,唯一键挡不住),可能凭空再起一单。 * 游标清空 = 下一轮重锚在当下、不回填,与「第一次配上账号」同一个起点。 * 上次拉取 / 错误 / 最近来信也一并清:它们说的都是刚被清掉的那些信。 */ await pool.query( `update sys.mail_account set cursor = null, last_polled_at = null, last_poll_error = null, last_inbound_at = null, polling_until = null`, ); const kept = await pool.query<{ n: string }>( `select count(*)::text as n from ledger.__object_meta where kind = 'master'`, ); process.stdout.write( `已清运行数据:${all.length} 张表(配置与组织数据原样保留;主数据 ${kept.rows[0]?.n ?? '0'} 张表未动)\n`, ); } finally { await pool.end(); } } if (import.meta.url === `file://${process.argv[1]}`) { await resetRuns(); process.exit(0); }