clear-deliverable-urls.mjs 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. #!/usr/bin/env node
  2. /**
  3. * 清空会话成果链接(一次性运维脚本,2026-11-10)
  4. *
  5. * ## 背景
  6. *
  7. * 旧版本把「用户自己上传的文件」也写进了会话记录的 `deliverableUrls`,
  8. * 结果右栏「成果」里出现了用户自己的图片。用户裁决:**全部清空重来**。
  9. *
  10. * 根因已在写入端修复(只认 `sender === 'agent'` + 拒收 /user/…/chat/… 链接),
  11. * 本脚本只负责把存量脏数据抹掉,之后容器产出的成果会重新积累。
  12. *
  13. * ## 做法
  14. *
  15. * 不新增云函数 op,直接复用既有 `upsert`:
  16. * `opUpsert` 只在**显式传入** `deliverableUrls` 数组时才写入,
  17. * 传 `[]` 即清空;其余字段不传就是不改(标题/归属/消息数都原样保留)。
  18. *
  19. * ## 安全
  20. *
  21. * · 默认 **dry-run**:只列出将被清空的会话,**不发任何写请求**
  22. * · 必须显式传 `--apply` 才真正清空
  23. * · 只清 `deliverableUrls`,**不删会话、不碰 Matrix 消息**
  24. * · 凭据从 `.claude/dev-login.json` 读取,绝不打印
  25. *
  26. * ## 用法
  27. *
  28. * ```bash
  29. * node scripts/clear-deliverable-urls.mjs # 只看清单
  30. * node scripts/clear-deliverable-urls.mjs --apply # 真的清空
  31. * ```
  32. */
  33. import { readFileSync } from 'node:fs';
  34. import { dirname, resolve } from 'node:path';
  35. import { fileURLToPath } from 'node:url';
  36. const here = dirname(fileURLToPath(import.meta.url));
  37. const root = resolve(here, '..');
  38. const FUNCTIONS_URL = 'https://server.fmode.cn/api/functions';
  39. const APP_ID = 'ncloudmaster';
  40. /** 会话目录云函数(与 `parse-api.ts` 的 `FN.chatThreads` 同源) */
  41. const FN_CHAT_THREADS = 'oR5O67i1Cm';
  42. /** 容器状态云函数:用来枚举账号下的容器,逐个拉会话目录 */
  43. const FN_AGENT_STATUS = 'xlosi3m46q';
  44. const APPLY = process.argv.includes('--apply');
  45. function loadToken() {
  46. const p = resolve(root, '.claude/dev-login.json');
  47. let raw;
  48. try {
  49. raw = JSON.parse(readFileSync(p, 'utf8'));
  50. } catch {
  51. console.error(`✗ 读不到登录凭据:${p}`);
  52. process.exit(2);
  53. }
  54. const token = (raw.token || '').trim();
  55. if (!token) {
  56. console.error('✗ 凭据里没有 token');
  57. process.exit(2);
  58. }
  59. return token;
  60. }
  61. async function callFn(token, id, params) {
  62. const res = await fetch(FUNCTIONS_URL, {
  63. method: 'POST',
  64. headers: {
  65. 'Content-Type': 'application/json',
  66. 'X-Parse-Application-Id': APP_ID,
  67. 'X-Parse-Session-Token': token,
  68. },
  69. body: JSON.stringify({ id, params }),
  70. });
  71. const json = await res.json().catch(() => ({}));
  72. if (json.code !== 200) throw new Error(`${id}: ${json.mess || JSON.stringify(json)}`);
  73. return json;
  74. }
  75. async function main() {
  76. const token = loadToken();
  77. // ① 账号下有哪些容器
  78. const agents = (await callFn(token, FN_AGENT_STATUS, { op: 'query' })).agents || [];
  79. const agentIds = [...new Set(agents.map((a) => a.agentId).filter(Boolean))];
  80. console.log(`容器 ${agentIds.length} 个:${agentIds.join(', ')}\n`);
  81. // ② 逐容器拉会话目录,收集「有成果链接」的会话
  82. const targets = [];
  83. for (const agentId of agentIds) {
  84. let threads = [];
  85. try {
  86. threads = (await callFn(token, FN_CHAT_THREADS, { op: 'list', agentId })).threads || [];
  87. } catch (e) {
  88. console.warn(`⚠ ${agentId} 拉取失败:${e.message}`);
  89. continue;
  90. }
  91. for (const t of threads) {
  92. const urls = t.deliverableUrls || [];
  93. if (urls.length === 0) continue;
  94. targets.push({ agentId, thread: t, urls });
  95. }
  96. }
  97. if (targets.length === 0) {
  98. console.log('✓ 没有需要清空的会话');
  99. return;
  100. }
  101. // ③ 清单(用户要先过目)
  102. console.log(`将清空 ${targets.length} 条会话的成果链接:\n`);
  103. for (const { agentId, thread, urls } of targets) {
  104. console.log(`· 「${thread.title || '(无标题)'}」 ${thread.threadRoot}`);
  105. console.log(` agentId=${agentId} ${urls.length} 条:`);
  106. for (const u of urls) console.log(` ${u}`);
  107. }
  108. console.log('');
  109. if (!APPLY) {
  110. console.log('(dry-run)未做任何修改。确认无误后加 --apply 执行。');
  111. return;
  112. }
  113. // ④ 逐条清空:只传 roomId + threadRoot + deliverableUrls:[]
  114. let ok = 0;
  115. let failed = 0;
  116. for (const { thread } of targets) {
  117. try {
  118. await callFn(token, FN_CHAT_THREADS, {
  119. op: 'upsert',
  120. items: [
  121. {
  122. roomId: thread.roomId,
  123. threadRoot: thread.threadRoot,
  124. deliverableUrls: [],
  125. },
  126. ],
  127. });
  128. ok++;
  129. console.log(`✓ 已清空 ${thread.threadRoot}`);
  130. } catch (e) {
  131. failed++;
  132. console.warn(`✗ ${thread.threadRoot}:${e.message}`);
  133. }
  134. }
  135. console.log(`\n完成:成功 ${ok},失败 ${failed}`);
  136. if (failed > 0) process.exit(1);
  137. }
  138. main().catch((e) => {
  139. console.error('✗', e.message);
  140. process.exit(1);
  141. });