| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- #!/usr/bin/env node
- /**
- * Matrix 接入体检(只读)
- *
- * 用途:给定一个容器 agentId,判断它「缺哪一步」才能在本 App 里收发消息。
- * 依据:《docs/03-interface/新容器接入Matrix-端到端流程-20261103.md》
- *
- * 用法:
- * node scripts/matrix-check.mjs <agentId>
- * 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 <agentId>');
- }
- /** 单容器体检(只读) */
- 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);
- }
|