| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- #!/usr/bin/env node
- /**
- * 云函数热更脚本(本地使用,不部署、不上传)
- *
- * ## 用途
- *
- * 把 `cloud/functions/*.js` 的最新源码推到线上 Parse 的 `Function` 表。
- * 热更免重启(`PUT /parse/classes/Function/<objectId>`)。
- *
- * ## 为什么需要 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);
- });
|