| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- /**
- * Cloud function: uploadManager
- * Signs Qiniu direct-upload tokens. File bytes are uploaded from the browser
- * directly to Qiniu, so the cloud function never handles large payloads.
- *
- * actions:
- * createUploadToken -> { uploadUrl, token, key, url, mimeType, kind, expiresAt }
- */
- async function handler(request, response) {
- try {
- const action = pickParam(request, 'action') || 'createUploadToken';
- if (action !== 'createUploadToken') {
- return response.json({ code: 400, success: false, error: `Unknown action: ${action}` });
- }
- const filename = String(pickParam(request, 'filename', 'fileName') || `asset-${Date.now()}`);
- const mimeType = String(pickParam(request, 'mimeType', 'contentType') || 'application/octet-stream');
- const requestedKind = String(pickParam(request, 'kind') || '').trim();
- const size = Number(pickParam(request, 'size') || 0);
- const kind = normalizeKind(requestedKind, mimeType);
- const key = buildDigitalHumanAssetKey(filename, kind);
- const { token, deadline } = await buildQiniuUploadToken(key);
- const url = `${QINIU_CDN_DOMAIN.replace(/\/$/, '')}/${key}`;
- response.json({
- code: 200,
- success: true,
- data: {
- uploadUrl: QINIU_UPLOAD_URL,
- token,
- key,
- url,
- mimeType,
- kind,
- size,
- expiresAt: deadline * 1000,
- },
- });
- } catch (error) {
- console.error('uploadManager failed:', error && error.message ? error.message : error);
- response.json({
- code: 500,
- success: false,
- error: error && error.message ? error.message : 'uploadManager failed',
- });
- }
- }
- const QINIU_ACCESS_KEY = readEnv('QINIU_AK') || readEnv('QINIU_ACCESS_KEY') || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE';
- const QINIU_SECRET_KEY = readEnv('QINIU_SK') || readEnv('QINIU_SECRET_KEY') || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX';
- const QINIU_BUCKET = readEnv('QINIU_BUCKET') || 'nova-repos';
- const QINIU_CDN_DOMAIN = readEnv('QINIU_CDN_DOMAIN') || 'https://repos.fmode.cn';
- const QINIU_CDN_PREFIX = readEnv('QINIU_CDN_PREFIX') || 'x/openclaw-skills';
- const QINIU_UPLOAD_URL = readEnv('QINIU_UPLOAD_URL') || 'https://up-z2.qiniup.com';
- async function buildQiniuUploadToken(key) {
- if (!QINIU_ACCESS_KEY || !QINIU_SECRET_KEY) {
- throw new Error('Qiniu AK/SK is not configured');
- }
- const deadline = Math.floor(Date.now() / 1000) + 3600;
- const putPolicy = {
- scope: `${QINIU_BUCKET}:${key}`,
- deadline,
- };
- const encodedPutPolicy = toBase64Url(JSON.stringify(putPolicy));
- const sign = await hmacSha1Base64Url(QINIU_SECRET_KEY, encodedPutPolicy);
- return {
- token: `${QINIU_ACCESS_KEY}:${sign}:${encodedPutPolicy}`,
- deadline,
- };
- }
- function buildDigitalHumanAssetKey(fileName, kind) {
- const rawName = String(fileName || `asset-${Date.now()}`);
- const extMatch = rawName.match(/(\.[a-z0-9]+)$/i);
- const ext = extMatch ? extMatch[1].toLowerCase() : '';
- const safeExt = ext && /^[.a-z0-9]+$/i.test(ext) ? ext : '';
- const baseName = rawName
- .replace(/\\/g, '/')
- .split('/')
- .pop()
- .replace(/\.[^.]*$/, '')
- .replace(/[^a-zA-Z0-9_-]/g, '_') || `asset-${Date.now()}`;
- const date = new Date();
- const yyyy = date.getFullYear();
- const mm = String(date.getMonth() + 1).padStart(2, '0');
- const dd = String(date.getDate()).padStart(2, '0');
- const timestamp = `${yyyy}${mm}${dd}-${Date.now()}`;
- return `${QINIU_CDN_PREFIX}/digital-human/${kind}/${yyyy}${mm}${dd}/${timestamp}-${baseName}${safeExt}`;
- }
- function normalizeKind(kind, mimeType) {
- if (kind === 'audio' || kind === 'video' || kind === 'image') return kind;
- if (mimeType.startsWith('audio/')) return 'audio';
- if (mimeType.startsWith('video/')) return 'video';
- return 'image';
- }
- function toBase64Url(input) {
- return base64UrlEncode(bytesFromString(input));
- }
- function pickParam(request, ...names) {
- const sources = [request.params, request.body, request];
- for (const src of sources) {
- if (!src || typeof src !== 'object') continue;
- for (const n of names) {
- const v = src[n];
- if (v !== undefined && v !== null && v !== '') return v;
- }
- }
- return null;
- }
- function readEnv(name) {
- if (typeof process !== 'undefined' && process.env && process.env[name]) {
- return process.env[name];
- }
- return '';
- }
- async function hmacSha1Base64Url(secret, message) {
- const subtle = typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle;
- if (subtle && typeof TextEncoder !== 'undefined') {
- const encoder = new TextEncoder();
- const key = await subtle.importKey(
- 'raw',
- encoder.encode(secret),
- { name: 'HMAC', hash: 'SHA-1' },
- false,
- ['sign']
- );
- const signature = await subtle.sign('HMAC', key, encoder.encode(message));
- return base64UrlEncode(new Uint8Array(signature));
- }
- if (typeof require === 'function') {
- const nodeCrypto = require('crypto');
- return nodeCrypto
- .createHmac('sha1', secret)
- .update(message)
- .digest('base64')
- .replace(/\+/g, '-')
- .replace(/\//g, '_');
- }
- return base64UrlEncode(hmacSha1Bytes(bytesFromString(secret), bytesFromString(message)));
- }
- function bytesFromString(input) {
- if (typeof TextEncoder !== 'undefined') {
- return new TextEncoder().encode(input);
- }
- if (typeof Buffer !== 'undefined') {
- return Buffer.from(input, 'utf8');
- }
- throw new Error('No UTF-8 encoder is available in this cloud runtime');
- }
- function base64UrlEncode(bytes) {
- let base64;
- if (typeof Buffer !== 'undefined') {
- base64 = Buffer.from(bytes).toString('base64');
- } else if (typeof btoa === 'function') {
- let binary = '';
- for (let i = 0; i < bytes.length; i++) {
- binary += String.fromCharCode(bytes[i]);
- }
- base64 = btoa(binary);
- } else {
- base64 = base64EncodeBytes(bytes);
- }
- return base64
- .replace(/\+/g, '-')
- .replace(/\//g, '_');
- }
- function base64EncodeBytes(bytes) {
- const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
- let output = '';
- for (let i = 0; i < bytes.length; i += 3) {
- const b1 = bytes[i];
- const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
- const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
- const triplet = (b1 << 16) | (b2 << 8) | b3;
- output += alphabet[(triplet >> 18) & 0x3f];
- output += alphabet[(triplet >> 12) & 0x3f];
- output += i + 1 < bytes.length ? alphabet[(triplet >> 6) & 0x3f] : '=';
- output += i + 2 < bytes.length ? alphabet[triplet & 0x3f] : '=';
- }
- return output;
- }
- function hmacSha1Bytes(secretBytes, messageBytes) {
- const blockSize = 64;
- let key = toByteArray(secretBytes);
- if (key.length > blockSize) {
- key = sha1Bytes(key);
- }
- const paddedKey = new Uint8Array(blockSize);
- paddedKey.set(key);
- const innerPad = new Uint8Array(blockSize);
- const outerPad = new Uint8Array(blockSize);
- for (let i = 0; i < blockSize; i++) {
- innerPad[i] = paddedKey[i] ^ 0x36;
- outerPad[i] = paddedKey[i] ^ 0x5c;
- }
- return sha1Bytes(concatBytes(outerPad, sha1Bytes(concatBytes(innerPad, messageBytes))));
- }
- function sha1Bytes(inputBytes) {
- const bytes = toByteArray(inputBytes);
- const bitLength = bytes.length * 8;
- const paddedLength = (((bytes.length + 9 + 63) >> 6) << 6);
- const padded = new Uint8Array(paddedLength);
- padded.set(bytes);
- padded[bytes.length] = 0x80;
- const view = new DataView(padded.buffer);
- view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
- view.setUint32(paddedLength - 4, bitLength >>> 0, false);
- let h0 = 0x67452301;
- let h1 = 0xefcdab89;
- let h2 = 0x98badcfe;
- let h3 = 0x10325476;
- let h4 = 0xc3d2e1f0;
- const w = new Uint32Array(80);
- for (let offset = 0; offset < paddedLength; offset += 64) {
- for (let i = 0; i < 16; i++) {
- w[i] = view.getUint32(offset + i * 4, false);
- }
- for (let i = 16; i < 80; i++) {
- w[i] = rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
- }
- let a = h0;
- let b = h1;
- let c = h2;
- let d = h3;
- let e = h4;
- for (let i = 0; i < 80; i++) {
- let f;
- let k;
- if (i < 20) {
- f = (b & c) | (~b & d);
- k = 0x5a827999;
- } else if (i < 40) {
- f = b ^ c ^ d;
- k = 0x6ed9eba1;
- } else if (i < 60) {
- f = (b & c) | (b & d) | (c & d);
- k = 0x8f1bbcdc;
- } else {
- f = b ^ c ^ d;
- k = 0xca62c1d6;
- }
- const temp = (rotateLeft(a, 5) + f + e + k + w[i]) >>> 0;
- e = d;
- d = c;
- c = rotateLeft(b, 30);
- b = a;
- a = temp;
- }
- h0 = (h0 + a) >>> 0;
- h1 = (h1 + b) >>> 0;
- h2 = (h2 + c) >>> 0;
- h3 = (h3 + d) >>> 0;
- h4 = (h4 + e) >>> 0;
- }
- const output = new Uint8Array(20);
- const outputView = new DataView(output.buffer);
- outputView.setUint32(0, h0, false);
- outputView.setUint32(4, h1, false);
- outputView.setUint32(8, h2, false);
- outputView.setUint32(12, h3, false);
- outputView.setUint32(16, h4, false);
- return output;
- }
- function concatBytes(a, b) {
- const left = toByteArray(a);
- const right = toByteArray(b);
- const out = new Uint8Array(left.length + right.length);
- out.set(left);
- out.set(right, left.length);
- return out;
- }
- function toByteArray(bytes) {
- return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
- }
- function rotateLeft(value, bits) {
- return ((value << bits) | (value >>> (32 - bits))) >>> 0;
- }
|