Files
medical-mall/server/tools/deploy-cloudfunc.js
not-like-juvenile 427010f7db 云服务推送
2026-02-27 16:02:44 +08:00

234 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// 打包并可选上传云函数到指定上传接口的工具脚本(通用模版)
// 依赖: archiver, node-fetch, form-data
const fs = require('fs');
const path = require('path');
async function ensureDeps() {
try {
require.resolve('archiver');
require.resolve('node-fetch');
require.resolve('form-data');
} catch (e) {
console.error('\n依赖缺失请先在仓库根或 server 目录安装依赖:');
console.error(' cd server');
console.error(' npm install archiver node-fetch form-data');
process.exit(1);
}
}
const archiver = require('archiver');
const fetch = require('node-fetch');
const FormData = require('form-data');
const { spawnSync } = require('child_process');
function packDirToZip(srcDir, outPath) {
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(outPath);
const archive = archiver('zip', { zlib: { level: 9 } });
output.on('close', () => resolve({ bytes: archive.pointer(), path: outPath }));
archive.on('error', err => reject(err));
archive.pipe(output);
archive.directory(srcDir, false);
archive.finalize();
});
}
async function uploadZip(url, token, zipPath, extraFields = {}) {
const form = new FormData();
form.append('file', fs.createReadStream(zipPath));
for (const k of Object.keys(extraFields)) form.append(k, extraFields[k]);
const headers = token ? { Authorization: `Bearer ${token}` } : {};
const resp = await fetch(url, { method: 'POST', headers: Object.assign(headers, form.getHeaders()), body: form });
const text = await resp.text();
let json;
try { json = JSON.parse(text); } catch (e) { json = { statusText: text }; }
return { ok: resp.ok, status: resp.status, body: json };
}
function runUnicloudUpload(spaceId, funcDir, funcName) {
try {
// 尝试使用全局 unicloud
let args = ['upload', '-p', spaceId, '-f', funcName, funcDir];
console.log('尝试使用全局 `unicloud` CLI', ['unicloud', ...args].join(' '));
let r = spawnSync('unicloud', args, { stdio: 'inherit' });
if (r && r.status === 0) return { ok: true };
// 否则尝试使用 npx 调用包内命令(不要求全局安装)
args = ['@dcloudio/unicloud-cli', 'upload', '-p', spaceId, '-f', funcName, funcDir];
console.log('尝试使用 npx 调用 unicloud-cli', ['npx', ...args].join(' '));
r = spawnSync('npx', args, { stdio: 'inherit' });
if (r && r.status === 0) return { ok: true };
return { ok: false, error: 'unicloud CLI 上传失败,返回码: ' + (r && r.status) };
} catch (e) {
return { ok: false, error: String(e) };
}
}
async function callDeployApi(deployApi, deployToken, uploadResult) {
try {
const headers = { 'Content-Type': 'application/json' };
if (deployToken) headers.Authorization = `Bearer ${deployToken}`;
const body = Object.assign({ uploadResult }, {});
const resp = await fetch(deployApi, { method: 'POST', headers, body: JSON.stringify(body) });
const txt = await resp.text();
let json;
try { json = JSON.parse(txt); } catch (e) { json = { statusText: txt }; }
return { ok: resp.ok, status: resp.status, body: json };
} catch (e) {
return { ok: false, error: String(e) };
}
}
async function invokeCloudFunction(funcUrl, pushToken, testCid) {
try {
const body = { token: pushToken || process.env.PUSH_TOKEN || null };
if (testCid) body.push_clientid = testCid;
body.title = 'smoke-test';
body.content = 'smoke-test';
const resp = await fetch(funcUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
const txt = await resp.text();
let json;
try { json = JSON.parse(txt); } catch (e) { json = { statusText: txt }; }
return { ok: resp.ok, status: resp.status, body: json };
} catch (e) {
return { ok: false, error: String(e) };
}
}
async function main() {
await ensureDeps();
const argv = process.argv.slice(2);
const args = {};
// 明确哪些参数需要值;若缺失则报错并退出,避免像 --upload 没给 URL 的情况
const expectsValue = new Set(['upload','deployApi','deployToken','invokeUrl','funcInvokeUrl','pushToken','testCid','dir','spaceId','name']);
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith('--')) continue;
const key = a.replace('--','');
const next = argv[i+1] && !argv[i+1].startsWith('--') ? argv[i+1] : undefined;
if (expectsValue.has(key)) {
if (!next) {
console.error(`参数错误:--${key} 需要一个值(例如 --${key} <value>)。`);
process.exit(1);
}
args[key] = next;
i++; // skip value
} else {
args[key] = true;
}
}
const funcDir = args.dir || process.env.CLOUD_FUNCTION_DIR || path.join(__dirname, '..', '..', 'uniCloud-alipay', 'cloudfunctions', 'testUnipush2');
const outDir = path.join(__dirname, '..', 'dist');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const zipPath = path.join(outDir, path.basename(funcDir) + '.zip');
console.log('打包目录:', funcDir);
try {
const r = await packDirToZip(funcDir, zipPath);
console.log('打包完成 ->', zipPath, `(${r.bytes} bytes)`);
} catch (e) {
console.error('打包失败', e);
process.exit(2);
}
let uploadUrl = args.upload || process.env.CLOUD_UPLOAD_URL;
const uploadToken = process.env.CLOUD_UPLOAD_TOKEN || process.env.CLOUD_TOKEN;
// 如果没有提供 upload URL但提供了 UniCloud 的 spaceId则默认使用官方上传 API
const spaceId = args.spaceId || process.env.CLOUD_UNICLOUD_SPACEID || process.env.SPACE_ID || null;
if (!uploadUrl && spaceId) {
uploadUrl = process.env.CLOUD_UPLOAD_URL || 'https://unicloud.dcloud.net.cn/api/uni-cloud/function/upload';
console.log('未指定 --upload检测到 spaceId使用 UniCloud 上传 API', uploadUrl);
}
if (!uploadUrl) {
console.log('\n未配置上传地址 (CLOUD_UPLOAD_URL),只生成 zip 文件。');
console.log('若要上传,请设置环境变量 CLOUD_UPLOAD_URL 与 CLOUD_UPLOAD_TOKEN或使用 --upload <URL> 参数,或提供 --spaceId 使用 unicloud 上传 API。');
process.exit(0);
}
console.log('上传到:', uploadUrl);
try {
const extra = {};
if (process.env.UNI_PUSH_APPID) extra.appId = process.env.UNI_PUSH_APPID;
// 若使用 UniCloud 官方上传接口,需要传入 spaceId 与 name
if (spaceId) extra.spaceId = spaceId;
const funcName = args.name || path.basename(funcDir);
if (funcName) extra.name = funcName;
// 本地文件校验
if (!fs.existsSync(zipPath)) {
console.error('打包文件不存在:', zipPath);
process.exit(7);
}
let res = await uploadZip(uploadUrl, uploadToken, zipPath, extra);
console.log('上传响应:', res.status, JSON.stringify(res.body));
// 若 HTTP 上传被拒绝(例如 405或不被接受且提供了 spaceId则尝试使用 unicloud CLI 上传回退
if ((!res.ok || res.status === 405) && spaceId) {
console.log('HTTP 上传失败或被拒绝,尝试使用 unicloud CLI 上传回退...');
const funcName = args.name || path.basename(funcDir);
const cli = runUnicloudUpload(spaceId, funcDir, funcName);
if (cli.ok) {
console.log('unicloud CLI 上传成功(回退)。');
// 将 published 设为 true后续进行 smoke-test
res = { ok: true, status: 0, body: { source: 'unicloud-cli' } };
} else {
console.error('unicloud CLI 上传也失败:', cli.error);
process.exit(8);
}
}
if (!res.ok) process.exit(3);
// 常见错误检测:如果把包 POST 到了函数的 invoke URL云函数通常直接返回业务层错误
// e.g. {"errCode":400,"errMsg":"push_clientid required"}
try {
const probe = JSON.stringify(res.body || {});
if (/push_clientid|required/i.test(probe) && /errCode|errMsg|error/i.test(probe)) {
console.error('\n检测到上传目标看起来像云函数的调用invokeURL而不是上传/发布 API。');
console.error('请使用提供商的上传/发布接口(不是函数的 /test 或类似 invoke 路径)。');
console.error('建议:在 HBuilderX 或 控制台 的 Network 面板中捕获 “上传/发布” 请求,或联系平台文档获取上传 API。');
console.error('当前响应(供参考):', JSON.stringify(res.body));
process.exit(6);
}
} catch (e) { /* ignore probe errors */ }
// 简单判断上传响应是否已经标记为已发布/上线(根据不同厂商响应结构调整)
let published = false;
try {
const b = res.body || {};
if (b.errCode === 0 || b.errMsg === 'success' || /success|online|published/i.test(JSON.stringify(b))) published = true;
} catch (e) { /* ignore */ }
// 如果未明确发布且提供了 deploy API则调用之以触发发布
const deployApi = args.deployApi || process.env.CLOUD_DEPLOY_API || null;
const deployToken = args.deployToken || process.env.CLOUD_DEPLOY_TOKEN || null;
let deployResult = null;
if (!published && deployApi) {
console.log('调用部署 API:', deployApi);
deployResult = await callDeployApi(deployApi, deployToken, res.body);
console.log('部署 API 响应:', deployResult && (deployResult.body || deployResult.error));
if (deployResult && deployResult.ok) published = true;
}
// 可选:若指定了 funcInvokeUrl 或 CLOUD_FUNC_URL则在上传/发布后做一次 smoke-test 调用
const funcInvokeUrl = args.invokeUrl || args.funcInvokeUrl || process.env.CLOUD_FUNC_URL || null;
const pushToken = args.pushToken || process.env.PUSH_TOKEN || null;
const testCid = args.testCid || process.env.TEST_DEVICE_CID || null;
if (funcInvokeUrl) {
console.log('调用云函数做 smoke-test:', funcInvokeUrl);
const inv = await invokeCloudFunction(funcInvokeUrl, pushToken, testCid);
console.log('smoke-test 响应:', inv && (inv.body || inv.error));
if (!inv.ok) process.exit(5);
}
} catch (e) {
console.error('上传失败', e);
process.exit(4);
}
}
if (require.main === module) main();