234 lines
10 KiB
JavaScript
234 lines
10 KiB
JavaScript
#!/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检测到上传目标看起来像云函数的调用(invoke)URL,而不是上传/发布 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();
|