#!/usr/bin/env node /** * 云函数热更脚本(本地使用,不部署、不上传) * * ## 用途 * * 把 `cloud/functions/*.js` 的最新源码推到线上 Parse 的 `Function` 表。 * 热更免重启(`PUT /parse/classes/Function/`)。 * * ## 为什么需要 Master-Key * * 写 `Function` 表属于平台管理操作,只有 Master-Key 有权限。 * 🔴 **Master-Key 绝不能写进代码或提交进 git**,只能通过环境变量传入: * * ```bash * PARSE_MASTER_KEY=xxx node scripts/deploy-cloud-fn.mjs * PARSE_MASTER_KEY=xxx node scripts/deploy-cloud-fn.mjs --dry-run * ``` * * ## 安全 * * · 默认 **dry-run**:只打印将要更新什么,不发请求 * · `--apply` 才真正 PUT * · 不发请求前先 GET 一次,对比线上版本,避免误覆盖别人改过的代码 * * ## 频率限制 * * 实测:`PUT` 连续请求会返回 403,需退避重试(见操作追踪 :3125)。 */ import { readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, '..'); const APP_ID = 'ncloudmaster'; const BASE = 'https://server.fmode.cn/parse'; const MK = process.env.PARSE_MASTER_KEY; const APPLY = process.argv.includes('--apply'); /** 要部署的函数清单 */ const TARGETS = [ { file: 'cloud/functions/fmodeagent-chatthreads.js', id: 'oR5O67i1Cm', name: 'fmodeagent-chatthreads', version: '0.1.9', }, ]; function authHeaders() { return { 'X-Parse-Application-Id': APP_ID, 'X-Parse-Master-Key': MK, 'Content-Type': 'application/json', }; } async function getRemote(id) { const res = await fetch(`${BASE}/classes/Function/${id}`, { headers: authHeaders() }); if (!res.ok) throw new Error(`读取失败 HTTP ${res.status}`); return res.json(); } /** PUT 有频率限制,403 时退避重试 */ async function putRemote(id, body, attempt = 1) { const res = await fetch(`${BASE}/classes/Function/${id}`, { method: 'PUT', headers: authHeaders(), body: JSON.stringify(body), }); if (res.status === 403 && attempt <= 4) { const wait = 1000 * attempt; console.log(` ⚠️ 403(频率限制),${wait}ms 后重试 ${attempt}/4`); await new Promise((r) => setTimeout(r, wait)); return putRemote(id, body, attempt + 1); } const text = await res.text(); if (!res.ok) throw new Error(`PUT 失败 HTTP ${res.status}: ${text.slice(0, 200)}`); try { return JSON.parse(text); } catch { return { raw: text.slice(0, 200) }; } } async function main() { if (!MK) { console.error('❌ 缺少 PARSE_MASTER_KEY 环境变量'); console.error(' 用法:PARSE_MASTER_KEY=xxx node scripts/deploy-cloud-fn.mjs [--apply]'); process.exit(2); } console.log(APPLY ? '🔴 APPLY:将热更线上云函数' : '🟡 DRY-RUN:只对比,不写入'); for (const t of TARGETS) { const code = readFileSync(resolve(root, t.file), 'utf8'); console.log(`\n=== ${t.name} (${t.id}) ===`); console.log(` 本地源码:${t.file},${code.length} 字符,v${t.version}`); let remote; try { remote = await getRemote(t.id); } catch (e) { console.log(` ⚠️ 读取线上失败:${e.message}(跳过对比)`); remote = null; } if (remote) { const rc = remote.code || ''; console.log(` 线上源码:${rc.length} 字符,v${remote.version ?? '?'}`); if (rc === code) { console.log(' ✅ 与线上完全一致,无需部署'); continue; } if (rc.length && code.length < rc.length * 0.7) { console.log(' 🔴 警告:本地源码明显小于线上,可能回退功能,已跳过'); continue; } } if (!APPLY) { console.log(' → 加 --apply 才会真正热更'); continue; } const r = await putRemote(t.id, { code, version: t.version, enabled: true }); console.log(` ✅ 已热更:${JSON.stringify(r).slice(0, 120)}(免重启,立即生效)`); } } main().catch((e) => { console.error('❌ 执行失败:', e.message); process.exit(1); });