#!/usr/bin/env node /** * 云函数线上/本地一致性校验(只读,不写线上) * * 解决的问题:线上函数被热更过,工作区源码却停在旧版本。 * 直接部署旧源码会把线上已有功能**回退**(2026-11-06 实际发生过: * 线上已是 v0.1.6,工作区还停在 v0.1.5,缺 kind/memberIds)。 * * 做法:不比对源码(读源码需要 Master Key,前端项目不持有), * 而是**比对行为契约**——调用线上函数,看它返回的字段集是否覆盖本源码声明的字段。 * * 用法: * node scripts/verify-cloud-fn.mjs * # 需要 ~/.fmode/config/user.json 的 sessionToken(与 bind-container 同源) * * 退出码:0 = 一致;1 = 线上缺少本地声明的字段(禁止部署);2 = 无法探测 */ import { readFileSync } from 'node:fs'; import { homedir } from 'node:os'; import { join } from 'node:path'; const FN_ID = 'oR5O67i1Cm'; const FUNCTIONS_URL = 'https://server.fmode.cn/api/functions'; /** 本地源码声明 opList 必须返回的字段(改动 opList 时同步这里) */ const EXPECTED_THREAD_FIELDS = [ 'objectId', 'agentId', 'roomId', 'threadRoot', 'title', 'titleLocked', 'messageCount', 'lastMessageAt', 'archived', 'lastPreview', 'workspaceId', // v0.1.6 'kind', 'memberIds', ]; 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})`); process.exit(2); } const token = loadToken(); async function call(params) { const res = await fetch(FUNCTIONS_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: FN_ID, token, params }), }); return res.json(); } const out = await call({ op: 'list' }); const threads = Array.isArray(out) ? out : out?.threads; if (!Array.isArray(threads)) { console.error('❌ 线上 list 未返回 threads:', JSON.stringify(out).slice(0, 200)); process.exit(2); } if (threads.length === 0) { console.error('⚠️ 该账号下没有会话记录,无法比对字段集。请用有数据的账号再跑一次。'); process.exit(2); } // 取所有记录的字段并集(空值字段在某些实现里可能被省略) const actual = new Set(); for (const t of threads) for (const k of Object.keys(t)) actual.add(k); const missing = EXPECTED_THREAD_FIELDS.filter((f) => !actual.has(f)); const extra = [...actual].filter((f) => !EXPECTED_THREAD_FIELDS.includes(f)); console.log(`线上返回字段(${threads.length} 条记录并集):`); console.log(' ' + [...actual].sort().join(', ')); if (missing.length === 0) { console.log(`\n✅ 一致:本地声明的 ${EXPECTED_THREAD_FIELDS.length} 个字段线上都有。`); if (extra.length) console.log(`ℹ️ 线上多出:${extra.join(', ')}(本地源码未声明,建议补齐)`); process.exit(0); } console.log(`\n❌ 线上缺少本地声明的字段:${missing.join(', ')}`); console.log(' → 说明工作区源码比线上新,或线上被回退。部署前请确认。'); process.exit(1);