matrix-check.mjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/env node
  2. /**
  3. * Matrix 接入体检(只读)
  4. *
  5. * 用途:给定一个容器 agentId,判断它「缺哪一步」才能在本 App 里收发消息。
  6. * 依据:《docs/03-interface/新容器接入Matrix-端到端流程-20261103.md》
  7. *
  8. * 用法:
  9. * node scripts/matrix-check.mjs <agentId>
  10. * node scripts/matrix-check.mjs # 不给参数:列当前账号下全部容器及其 Matrix 状态
  11. *
  12. * 🔴 全程只读:只调 `xlosi3m46q` 查询 与 Matrix 的 joined_members。
  13. * **不调 provision**(provision 有写副作用:会建号、建房、写绑定表)。
  14. * 凭据从 `~/.fmode/config/user.json` 的 sessionToken 读取,绝不打印。
  15. *
  16. * 退出码:0 = 全部检查通过;1 = 存在未通过的检查;2 = 用法/凭据错误
  17. */
  18. import { readFileSync } from 'node:fs';
  19. import { homedir } from 'node:os';
  20. import { join } from 'node:path';
  21. const FUNCTIONS_URL = 'https://server.fmode.cn/api/functions';
  22. const FN_STATUS = 'xlosi3m46q';
  23. const MATRIX_HS = 'https://matrix.fmode.cn';
  24. function loadToken() {
  25. const p = join(homedir(), '.fmode', 'config', 'user.json');
  26. try {
  27. const t = JSON.parse(readFileSync(p, 'utf8'))?.sessionToken;
  28. if (typeof t === 'string' && t.startsWith('r:')) return t;
  29. } catch {
  30. /* fallthrough */
  31. }
  32. console.error(`❌ 读不到平台 sessionToken(期望 ${p} 的 sessionToken 且以 "r:" 开头)`);
  33. process.exit(2);
  34. }
  35. const TOKEN = loadToken();
  36. /** 调云函数(与前端 `ParseApi.callFunction` 同形状:POST {id, token, params}) */
  37. async function callFn(id, params) {
  38. const res = await fetch(FUNCTIONS_URL, {
  39. method: 'POST',
  40. headers: { 'Content-Type': 'application/json' },
  41. body: JSON.stringify({ id, token: TOKEN, params }),
  42. });
  43. const text = await res.text();
  44. try {
  45. return JSON.parse(text);
  46. } catch {
  47. return { code: res.status, mess: text.slice(0, 200) };
  48. }
  49. }
  50. /** channels 在云端是 JSON 字符串,宽容解析 */
  51. function parseChannels(raw) {
  52. if (!raw) return {};
  53. if (typeof raw === 'object') return raw;
  54. try {
  55. return JSON.parse(raw) ?? {};
  56. } catch {
  57. return {};
  58. }
  59. }
  60. const ok = (s) => `✅ ${s}`;
  61. const bad = (s) => `❌ ${s}`;
  62. const warn = (s) => `⚠️ ${s}`;
  63. const rows = [];
  64. /** 列全部容器 */
  65. async function listAll() {
  66. const d = await callFn(FN_STATUS, { op: 'query', scope: 'mine' });
  67. if (d.code !== 200) {
  68. console.error(bad(`查询容器列表失败:${d.code} ${d.mess ?? ''}`));
  69. process.exit(2);
  70. }
  71. const agents = d.agents ?? [];
  72. console.log(`当前账号下共 ${agents.length} 个容器:\n`);
  73. console.log('agentId'.padEnd(24), 'matrix'.padEnd(8), 'matrixConfig.userId');
  74. console.log('-'.repeat(72));
  75. for (const a of agents) {
  76. const ch = parseChannels(a.channels);
  77. const uid = ch?.matrixConfig?.userId ?? '';
  78. const mark = uid ? '✅' : '❌';
  79. console.log(String(a.agentId ?? '').padEnd(24), `${mark} ${String(!!ch.matrix).padEnd(6)}`, uid || '—');
  80. }
  81. console.log('\n提示:对某个容器做完整体检 → node scripts/matrix-check.mjs <agentId>');
  82. }
  83. /** 单容器体检(只读) */
  84. async function checkOne(agentId) {
  85. console.log(`\n=== Matrix 接入体检:${agentId} ===\n`);
  86. // 检查 1:FmodeAgent 表里有没有这条记录
  87. const q = await callFn(FN_STATUS, { op: 'query', scope: 'mine', agentId });
  88. const agent = q.code === 200 ? q.agent : null;
  89. if (!agent) {
  90. console.log(bad(`检查1 FmodeAgent 记录:${q.code} ${q.mess ?? '未找到'}`));
  91. console.log(' → 后果:provision 会对该 agentId 返回 500「Object not found」。');
  92. console.log(' → 先按流程 §三 补录(agentId = 容器 hostname),再进第 2 步。');
  93. rows.push(['FmodeAgent 记录', false]);
  94. report();
  95. return;
  96. }
  97. const objectId = agent.objectId ?? '';
  98. const expectedMxid = objectId ? `@${objectId.toLowerCase()}:fmode.cn` : '';
  99. console.log(ok(`检查1 FmodeAgent 记录存在 objectId=${objectId || '(未返回)'}`));
  100. rows.push(['FmodeAgent 记录', true]);
  101. // 检查 2:channels.matrixConfig.userId(App 端 isMatrixReady 的判定依据)
  102. const ch = parseChannels(agent.channels);
  103. const uid = ch?.matrixConfig?.userId ?? '';
  104. if (uid) {
  105. console.log(ok(`检查2 App 可见性:matrixConfig.userId=${uid}(App 里会显示为「可聊天」)`));
  106. if (expectedMxid && uid !== expectedMxid) {
  107. console.log(warn(` 注意:与 objectId 派生的 ${expectedMxid} 不一致 —— 可能是手工注册的历史账号`));
  108. }
  109. rows.push(['App 可见性', true]);
  110. } else {
  111. console.log(bad('检查2 App 可见性:matrixConfig.userId 为空'));
  112. console.log(' → App 的 isMatrixReady() 为 false,列表里点不进聊天。');
  113. console.log(' → 需要先跑 provision(第 2 步,有写副作用,本脚本不做)。');
  114. rows.push(['App 可见性', false]);
  115. }
  116. // 检查 3:Agent 是否在房间里
  117. //
  118. // 只读做法:App 登录态的「房间成员」接口需要用户 token,而用户 token 只能由
  119. // provision 签发 —— 本脚本刻意不调 provision。所以这里退一步:
  120. // 用 channels 里 reporter 上报的 listener/state 做**间接**判断,并明确标注其局限。
  121. const state = ch?.matrixConfig?.channelState ?? '';
  122. const listenerOk = ch?.matrixConfig?.listenerOk;
  123. if (state === 'connected' && listenerOk !== false) {
  124. console.log(ok(`检查3 容器侧上报:matrixState=${state}${listenerOk === undefined ? '' : ` listenerOk=${listenerOk}`}`));
  125. console.log(' 局限:这是容器自报,不等于「已入房」。入房必须按流程 §第3步 用 joined_members 回读。');
  126. rows.push(['容器侧上报', true]);
  127. } else {
  128. console.log(warn(`检查3 容器侧上报:matrixState=${state || '(无)'} listenerOk=${listenerOk ?? '(无)'}`));
  129. console.log(' → 容器可能没配 MATRIX_* env,或 gateway 没重启。见流程 §第4步 提示词。');
  130. rows.push(['容器侧上报', false]);
  131. }
  132. // 检查 4:收发闭环 —— 必须实测,脚本不做(需要用户 token 发消息)
  133. console.log(warn('检查4 收发闭环:需要人工按流程 §第6步 实测(本脚本不代发消息)'));
  134. rows.push(['收发闭环(人工)', null]);
  135. report();
  136. }
  137. function report() {
  138. console.log('\n--- 结论 ---');
  139. for (const [name, v] of rows) {
  140. console.log(` ${v === true ? '✅' : v === false ? '❌' : '⚠️ '} ${name}`);
  141. }
  142. const failed = rows.filter(([, v]) => v === false).length;
  143. const pending = rows.filter(([, v]) => v === null).length;
  144. if (failed === 0 && pending === 0) {
  145. console.log('\n自动可判项全部通过。');
  146. } else if (failed === 0) {
  147. console.log(`\n无失败项,但有 ${pending} 项需人工实测 —— 只有「收发闭环」通过才算真的通了。`);
  148. } else {
  149. console.log(`\n${failed} 项未通过,按上面 → 指向的步骤处理。`);
  150. }
  151. process.exit(failed === 0 ? 0 : 1);
  152. }
  153. const arg = process.argv[2];
  154. if (!arg) {
  155. await listAll();
  156. } else {
  157. await checkOne(arg);
  158. }