#!/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} )。`); 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 参数,或提供 --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();