#!/usr/bin/env node /** * 清空会话成果链接(一次性运维脚本,2026-11-10) * * ## 背景 * * 旧版本把「用户自己上传的文件」也写进了会话记录的 `deliverableUrls`, * 结果右栏「成果」里出现了用户自己的图片。用户裁决:**全部清空重来**。 * * 根因已在写入端修复(只认 `sender === 'agent'` + 拒收 /user/…/chat/… 链接), * 本脚本只负责把存量脏数据抹掉,之后容器产出的成果会重新积累。 * * ## 做法 * * 不新增云函数 op,直接复用既有 `upsert`: * `opUpsert` 只在**显式传入** `deliverableUrls` 数组时才写入, * 传 `[]` 即清空;其余字段不传就是不改(标题/归属/消息数都原样保留)。 * * ## 安全 * * · 默认 **dry-run**:只列出将被清空的会话,**不发任何写请求** * · 必须显式传 `--apply` 才真正清空 * · 只清 `deliverableUrls`,**不删会话、不碰 Matrix 消息** * · 凭据从 `.claude/dev-login.json` 读取,绝不打印 * * ## 用法 * * ```bash * node scripts/clear-deliverable-urls.mjs # 只看清单 * node scripts/clear-deliverable-urls.mjs --apply # 真的清空 * ``` */ import { readFileSync } 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'; const APP_ID = 'ncloudmaster'; /** 会话目录云函数(与 `parse-api.ts` 的 `FN.chatThreads` 同源) */ const FN_CHAT_THREADS = 'oR5O67i1Cm'; /** 容器状态云函数:用来枚举账号下的容器,逐个拉会话目录 */ const FN_AGENT_STATUS = 'xlosi3m46q'; const APPLY = process.argv.includes('--apply'); function loadToken() { const p = resolve(root, '.claude/dev-login.json'); let raw; try { raw = JSON.parse(readFileSync(p, 'utf8')); } catch { console.error(`✗ 读不到登录凭据:${p}`); process.exit(2); } const token = (raw.token || '').trim(); if (!token) { console.error('✗ 凭据里没有 token'); process.exit(2); } return token; } async function callFn(token, id, params) { const res = await fetch(FUNCTIONS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID, 'X-Parse-Session-Token': token, }, body: JSON.stringify({ id, params }), }); const json = await res.json().catch(() => ({})); if (json.code !== 200) throw new Error(`${id}: ${json.mess || JSON.stringify(json)}`); return json; } async function main() { const token = loadToken(); // ① 账号下有哪些容器 const agents = (await callFn(token, FN_AGENT_STATUS, { op: 'query' })).agents || []; const agentIds = [...new Set(agents.map((a) => a.agentId).filter(Boolean))]; console.log(`容器 ${agentIds.length} 个:${agentIds.join(', ')}\n`); // ② 逐容器拉会话目录,收集「有成果链接」的会话 const targets = []; for (const agentId of agentIds) { let threads = []; try { threads = (await callFn(token, FN_CHAT_THREADS, { op: 'list', agentId })).threads || []; } catch (e) { console.warn(`⚠ ${agentId} 拉取失败:${e.message}`); continue; } for (const t of threads) { const urls = t.deliverableUrls || []; if (urls.length === 0) continue; targets.push({ agentId, thread: t, urls }); } } if (targets.length === 0) { console.log('✓ 没有需要清空的会话'); return; } // ③ 清单(用户要先过目) console.log(`将清空 ${targets.length} 条会话的成果链接:\n`); for (const { agentId, thread, urls } of targets) { console.log(`· 「${thread.title || '(无标题)'}」 ${thread.threadRoot}`); console.log(` agentId=${agentId} ${urls.length} 条:`); for (const u of urls) console.log(` ${u}`); } console.log(''); if (!APPLY) { console.log('(dry-run)未做任何修改。确认无误后加 --apply 执行。'); return; } // ④ 逐条清空:只传 roomId + threadRoot + deliverableUrls:[] let ok = 0; let failed = 0; for (const { thread } of targets) { try { await callFn(token, FN_CHAT_THREADS, { op: 'upsert', items: [ { roomId: thread.roomId, threadRoot: thread.threadRoot, deliverableUrls: [], }, ], }); ok++; console.log(`✓ 已清空 ${thread.threadRoot}`); } catch (e) { failed++; console.warn(`✗ ${thread.threadRoot}:${e.message}`); } } console.log(`\n完成:成功 ${ok},失败 ${failed}`); if (failed > 0) process.exit(1); } main().catch((e) => { console.error('✗', e.message); process.exit(1); });