| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- #!/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);
|