#!/usr/bin/env node /** * Matrix 接入体检(只读) * * 用途:给定一个容器 agentId,判断它「缺哪一步」才能在本 App 里收发消息。 * 依据:《docs/03-interface/新容器接入Matrix-端到端流程-20261103.md》 * * 用法: * node scripts/matrix-check.mjs * node scripts/matrix-check.mjs # 不给参数:列当前账号下全部容器及其 Matrix 状态 * * 🔴 全程只读:只调 `xlosi3m46q` 查询 与 Matrix 的 joined_members。 * **不调 provision**(provision 有写副作用:会建号、建房、写绑定表)。 * 凭据从 `~/.fmode/config/user.json` 的 sessionToken 读取,绝不打印。 * * 退出码:0 = 全部检查通过;1 = 存在未通过的检查;2 = 用法/凭据错误 */ import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; const FUNCTIONS_URL = 'https://server.fmode.cn/api/functions'; const FN_STATUS = 'xlosi3m46q'; const MATRIX_HS = 'https://matrix.fmode.cn'; function loadToken() { const p = join(homedir(), '.fmode', 'config', 'user.json'); try { const t = JSON.parse(readFileSync(p, 'utf8'))?.sessionToken; if (typeof t === 'string' && t.startsWith('r:')) return t; } catch { /* fallthrough */ } console.error(`❌ 读不到平台 sessionToken(期望 ${p} 的 sessionToken 且以 "r:" 开头)`); process.exit(2); } const TOKEN = loadToken(); /** 调云函数(与前端 `ParseApi.callFunction` 同形状:POST {id, token, params}) */ 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) }; } } /** channels 在云端是 JSON 字符串,宽容解析 */ function parseChannels(raw) { if (!raw) return {}; if (typeof raw === 'object') return raw; try { return JSON.parse(raw) ?? {}; } catch { return {}; } } const ok = (s) => `✅ ${s}`; const bad = (s) => `❌ ${s}`; const warn = (s) => `⚠️ ${s}`; const rows = []; /** 列全部容器 */ async function listAll() { const d = await callFn(FN_STATUS, { op: 'query', scope: 'mine' }); if (d.code !== 200) { console.error(bad(`查询容器列表失败:${d.code} ${d.mess ?? ''}`)); process.exit(2); } const agents = d.agents ?? []; console.log(`当前账号下共 ${agents.length} 个容器:\n`); console.log('agentId'.padEnd(24), 'matrix'.padEnd(8), 'matrixConfig.userId'); console.log('-'.repeat(72)); for (const a of agents) { const ch = parseChannels(a.channels); const uid = ch?.matrixConfig?.userId ?? ''; const mark = uid ? '✅' : '❌'; console.log(String(a.agentId ?? '').padEnd(24), `${mark} ${String(!!ch.matrix).padEnd(6)}`, uid || '—'); } console.log('\n提示:对某个容器做完整体检 → node scripts/matrix-check.mjs '); } /** 单容器体检(只读) */ async function checkOne(agentId) { console.log(`\n=== Matrix 接入体检:${agentId} ===\n`); // 检查 1:FmodeAgent 表里有没有这条记录 const q = await callFn(FN_STATUS, { op: 'query', scope: 'mine', agentId }); const agent = q.code === 200 ? q.agent : null; if (!agent) { console.log(bad(`检查1 FmodeAgent 记录:${q.code} ${q.mess ?? '未找到'}`)); console.log(' → 后果:provision 会对该 agentId 返回 500「Object not found」。'); console.log(' → 先按流程 §三 补录(agentId = 容器 hostname),再进第 2 步。'); rows.push(['FmodeAgent 记录', false]); report(); return; } const objectId = agent.objectId ?? ''; const expectedMxid = objectId ? `@${objectId.toLowerCase()}:fmode.cn` : ''; console.log(ok(`检查1 FmodeAgent 记录存在 objectId=${objectId || '(未返回)'}`)); rows.push(['FmodeAgent 记录', true]); // 检查 2:channels.matrixConfig.userId(App 端 isMatrixReady 的判定依据) const ch = parseChannels(agent.channels); const uid = ch?.matrixConfig?.userId ?? ''; if (uid) { console.log(ok(`检查2 App 可见性:matrixConfig.userId=${uid}(App 里会显示为「可聊天」)`)); if (expectedMxid && uid !== expectedMxid) { console.log(warn(` 注意:与 objectId 派生的 ${expectedMxid} 不一致 —— 可能是手工注册的历史账号`)); } rows.push(['App 可见性', true]); } else { console.log(bad('检查2 App 可见性:matrixConfig.userId 为空')); console.log(' → App 的 isMatrixReady() 为 false,列表里点不进聊天。'); console.log(' → 需要先跑 provision(第 2 步,有写副作用,本脚本不做)。'); rows.push(['App 可见性', false]); } // 检查 3:Agent 是否在房间里 // // 只读做法:App 登录态的「房间成员」接口需要用户 token,而用户 token 只能由 // provision 签发 —— 本脚本刻意不调 provision。所以这里退一步: // 用 channels 里 reporter 上报的 listener/state 做**间接**判断,并明确标注其局限。 const state = ch?.matrixConfig?.channelState ?? ''; const listenerOk = ch?.matrixConfig?.listenerOk; if (state === 'connected' && listenerOk !== false) { console.log(ok(`检查3 容器侧上报:matrixState=${state}${listenerOk === undefined ? '' : ` listenerOk=${listenerOk}`}`)); console.log(' 局限:这是容器自报,不等于「已入房」。入房必须按流程 §第3步 用 joined_members 回读。'); rows.push(['容器侧上报', true]); } else { console.log(warn(`检查3 容器侧上报:matrixState=${state || '(无)'} listenerOk=${listenerOk ?? '(无)'}`)); console.log(' → 容器可能没配 MATRIX_* env,或 gateway 没重启。见流程 §第4步 提示词。'); rows.push(['容器侧上报', false]); } // 检查 4:收发闭环 —— 必须实测,脚本不做(需要用户 token 发消息) console.log(warn('检查4 收发闭环:需要人工按流程 §第6步 实测(本脚本不代发消息)')); rows.push(['收发闭环(人工)', null]); report(); } function report() { console.log('\n--- 结论 ---'); for (const [name, v] of rows) { console.log(` ${v === true ? '✅' : v === false ? '❌' : '⚠️ '} ${name}`); } const failed = rows.filter(([, v]) => v === false).length; const pending = rows.filter(([, v]) => v === null).length; if (failed === 0 && pending === 0) { console.log('\n自动可判项全部通过。'); } else if (failed === 0) { console.log(`\n无失败项,但有 ${pending} 项需人工实测 —— 只有「收发闭环」通过才算真的通了。`); } else { console.log(`\n${failed} 项未通过,按上面 → 指向的步骤处理。`); } process.exit(failed === 0 ? 0 : 1); } const arg = process.argv[2]; if (!arg) { await listAll(); } else { await checkOne(arg); }