| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- #!/usr/bin/env node
- /**
- * 会话记录软删清理(一次性运维脚本)
- *
- * ## 用途
- *
- * 把当前账号名下的**会话目录记录**(`FmodeChatThread` 系列表)全部软删,
- * 用于清理测试过程中堆出来的会话。
- *
- * ## 安全
- *
- * · 默认 **dry-run**:只列出将删除的记录,**不发任何写请求**
- * · 必须显式传 `--apply` 才真正删除
- * · 一律走 `op: 'remove'`(**软删**,`isDeleted=true`),不做物理删除
- * · **不碰 Matrix 房间与消息** —— 只删目录索引(与 App 内「删会话」一致)
- *
- * ## 用法
- *
- * ```bash
- * node scripts/purge-chatthreads.mjs # 只列出
- * node scripts/purge-chatthreads.mjs --apply # 真的软删
- * ```
- *
- * 凭据从 `.claude/dev-login.json` 读取,绝不打印。
- */
- import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
- import { dirname, resolve } from 'node:path';
- import { fileURLToPath } from 'node:url';
- const here = dirname(fileURLToPath(import.meta.url));
- const root = resolve(here, '..');
- const FUNCTIONS_URL = 'https://server.fmode.cn/api/functions';
- /** 会话目录云函数(与 `parse-api.ts` 的 `FN.chatThreads` 同源) */
- const FN_CHAT_THREADS = 'oR5O67i1Cm';
- const FN_AGENT_STATUS = 'xlosi3m46q';
- const APPLY = process.argv.includes('--apply');
- /**
- * 连群聊一起删(默认**不删**)。
- *
- * 🔴 2026-11-09 教训:用户说「删日常对话」,脚本却把群会话也删了。
- * 现在默认只删 `kind !== 'group'` 的日常对话,群聊需要显式加
- * `--include-groups` 才会一起处理。
- */
- const INCLUDE_GROUPS = process.argv.includes('--include-groups');
- function loadToken() {
- const p = resolve(root, '.claude/dev-login.json');
- try {
- const t = JSON.parse(readFileSync(p, 'utf8'))?.token;
- if (typeof t === 'string' && t.startsWith('r:')) return t;
- } catch {
- /* fallthrough */
- }
- console.error(`❌ 读不到会话 Token(期望 ${p} 的 token 且以 "r:" 开头)`);
- process.exit(2);
- }
- const TOKEN = loadToken();
- async function callFn(id, params) {
- const res = await fetch(FUNCTIONS_URL, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id, token: TOKEN, params }),
- });
- const text = await res.text();
- try {
- return JSON.parse(text);
- } catch {
- return { code: res.status, mess: text.slice(0, 200) };
- }
- }
- /** 拉当前账号名下的全部容器 */
- async function listAgents() {
- const d = await callFn(FN_AGENT_STATUS, { op: 'query', scope: 'mine' });
- if (d.code !== 200) throw new Error(`容器列表读取失败: ${d.mess ?? d.code}`);
- return (d.agents ?? []).map((a) => ({ id: a.objectId, name: a.name, agentId: a.agentId }));
- }
- /** 拉某容器下未删除的会话记录 */
- async function listThreads(agentId) {
- const d = await callFn(FN_CHAT_THREADS, { op: 'list', agentId });
- if (d.code !== 200) throw new Error(`会话目录读取失败(${agentId}): ${d.mess ?? d.code}`);
- const all = (d.threads ?? []).filter((t) => !t.isDeleted);
- // 群聊默认保护(用户的「日常对话」不包括群聊)
- return INCLUDE_GROUPS ? all : all.filter((t) => t.kind !== 'group');
- }
- /** 该容器下被脚本跳过的群聊数量(仅用于报告) */
- async function countGroupsSkipped(agentId) {
- const d = await callFn(FN_CHAT_THREADS, { op: 'list', agentId });
- if (d.code !== 200) return 0;
- return (d.threads ?? []).filter((t) => !t.isDeleted && t.kind === 'group').length;
- }
- async function main() {
- console.log(APPLY ? '🔴 APPLY 模式:将真正执行软删' : '🟡 DRY-RUN:只列出,不删除');
- const agents = await listAgents();
- console.log(`\n当前账号下容器 ${agents.length} 个\n`);
- const plan = [];
- for (const a of agents) {
- let threads = [];
- try {
- threads = await listThreads(a.agentId);
- } catch (e) {
- console.log(` ⚠️ ${a.name} (${a.agentId}) 跳过:${e.message}`);
- continue;
- }
- if (threads.length === 0) {
- const g = await countGroupsSkipped(a.agentId).catch(() => 0);
- if (g > 0) console.log(` ${a.name.padEnd(28)} 跳过 ${g} 个群聊(未加 --include-groups)`);
- continue;
- }
- console.log(` ${a.name.padEnd(28)} ${String(threads.length).padStart(3)} 条`);
- const g = INCLUDE_GROUPS ? 0 : await countGroupsSkipped(a.agentId).catch(() => 0);
- if (g > 0) console.log(` ${''.padEnd(28)} (另有 ${g} 个群聊被保护,未加 --include-groups)`);
- for (const t of threads) {
- plan.push({
- agentId: a.agentId,
- agentName: a.name,
- threadRoot: t.threadRoot,
- roomId: t.roomId,
- objectId: t.objectId,
- title: t.title ?? '',
- messageCount: t.messageCount ?? 0,
- });
- }
- }
- console.log(`\n合计待软删:${plan.length} 条`);
- const outDir = resolve(root, 'test-results/purge-chatthreads');
- mkdirSync(outDir, { recursive: true });
- const stamp = new Date().toISOString().replace(/[:.]/g, '-');
- const dumpPath = resolve(outDir, `plan-${stamp}.json`);
- writeFileSync(dumpPath, JSON.stringify(plan, null, 2), 'utf8');
- console.log(`清单已落盘:${dumpPath}`);
- if (plan.length === 0) {
- console.log('没有需要清理的会话。');
- return;
- }
- if (!APPLY) {
- console.log('\n加 --apply 才会真正删除。');
- return;
- }
- let ok = 0;
- let fail = 0;
- for (const [i, p] of plan.entries()) {
- const r = await callFn(FN_CHAT_THREADS, {
- op: 'remove',
- threadRoot: p.threadRoot,
- roomId: p.roomId,
- agentId: p.agentId,
- });
- if (r.code === 200) {
- ok++;
- if ((i + 1) % 10 === 0 || i + 1 === plan.length) {
- console.log(` 进度 ${i + 1}/${plan.length}`);
- }
- } else {
- fail++;
- console.log(` ❌ ${p.objectId} ${p.threadRoot}: ${r.mess ?? r.code}`);
- }
- }
- console.log(`\n软删完成:成功 ${ok} 条,失败 ${fail} 条`);
- // 复核:重新拉一遍,确认列表已不含这些记录
- let remain = 0;
- for (const a of agents) {
- try {
- remain += (await listThreads(a.agentId)).length;
- } catch {
- /* 忽略 */
- }
- }
- console.log(`复核:剩余未删除会话 ${remain} 条`);
- writeFileSync(
- resolve(outDir, `result-${stamp}.json`),
- JSON.stringify({ total: plan.length, ok, fail, remain }, null, 2),
- 'utf8',
- );
- }
- main().catch((e) => {
- console.error('❌ 执行失败:', e.message);
- process.exit(1);
- });
|