09-uploadManager.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /**
  2. * Cloud function: uploadManager
  3. * Signs Qiniu direct-upload tokens. File bytes are uploaded from the browser
  4. * directly to Qiniu, so the cloud function never handles large payloads.
  5. *
  6. * actions:
  7. * createUploadToken -> { uploadUrl, token, key, url, mimeType, kind, expiresAt }
  8. */
  9. async function handler(request, response) {
  10. try {
  11. const action = pickParam(request, 'action') || 'createUploadToken';
  12. if (action !== 'createUploadToken') {
  13. return response.json({ code: 400, success: false, error: `Unknown action: ${action}` });
  14. }
  15. const filename = String(pickParam(request, 'filename', 'fileName') || `asset-${Date.now()}`);
  16. const mimeType = String(pickParam(request, 'mimeType', 'contentType') || 'application/octet-stream');
  17. const requestedKind = String(pickParam(request, 'kind') || '').trim();
  18. const size = Number(pickParam(request, 'size') || 0);
  19. const kind = normalizeKind(requestedKind, mimeType);
  20. const key = buildDigitalHumanAssetKey(filename, kind);
  21. const { token, deadline } = await buildQiniuUploadToken(key);
  22. const url = `${QINIU_CDN_DOMAIN.replace(/\/$/, '')}/${key}`;
  23. response.json({
  24. code: 200,
  25. success: true,
  26. data: {
  27. uploadUrl: QINIU_UPLOAD_URL,
  28. token,
  29. key,
  30. url,
  31. mimeType,
  32. kind,
  33. size,
  34. expiresAt: deadline * 1000,
  35. },
  36. });
  37. } catch (error) {
  38. console.error('uploadManager failed:', error && error.message ? error.message : error);
  39. response.json({
  40. code: 500,
  41. success: false,
  42. error: error && error.message ? error.message : 'uploadManager failed',
  43. });
  44. }
  45. }
  46. const QINIU_ACCESS_KEY = readEnv('QINIU_AK') || readEnv('QINIU_ACCESS_KEY') || 'EXsA-z_n4LGmWrwC088bygcGJtAditnWQe2nH-ZE';
  47. const QINIU_SECRET_KEY = readEnv('QINIU_SK') || readEnv('QINIU_SECRET_KEY') || 'HWTL92OL-Tup0-8ex8A9jnG3OaJzTxlF4OwiiDsX';
  48. const QINIU_BUCKET = readEnv('QINIU_BUCKET') || 'nova-repos';
  49. const QINIU_CDN_DOMAIN = readEnv('QINIU_CDN_DOMAIN') || 'https://repos.fmode.cn';
  50. const QINIU_CDN_PREFIX = readEnv('QINIU_CDN_PREFIX') || 'x/openclaw-skills';
  51. const QINIU_UPLOAD_URL = readEnv('QINIU_UPLOAD_URL') || 'https://up-z2.qiniup.com';
  52. async function buildQiniuUploadToken(key) {
  53. if (!QINIU_ACCESS_KEY || !QINIU_SECRET_KEY) {
  54. throw new Error('Qiniu AK/SK is not configured');
  55. }
  56. const deadline = Math.floor(Date.now() / 1000) + 3600;
  57. const putPolicy = {
  58. scope: `${QINIU_BUCKET}:${key}`,
  59. deadline,
  60. };
  61. const encodedPutPolicy = toBase64Url(JSON.stringify(putPolicy));
  62. const sign = await hmacSha1Base64Url(QINIU_SECRET_KEY, encodedPutPolicy);
  63. return {
  64. token: `${QINIU_ACCESS_KEY}:${sign}:${encodedPutPolicy}`,
  65. deadline,
  66. };
  67. }
  68. function buildDigitalHumanAssetKey(fileName, kind) {
  69. const rawName = String(fileName || `asset-${Date.now()}`);
  70. const extMatch = rawName.match(/(\.[a-z0-9]+)$/i);
  71. const ext = extMatch ? extMatch[1].toLowerCase() : '';
  72. const safeExt = ext && /^[.a-z0-9]+$/i.test(ext) ? ext : '';
  73. const baseName = rawName
  74. .replace(/\\/g, '/')
  75. .split('/')
  76. .pop()
  77. .replace(/\.[^.]*$/, '')
  78. .replace(/[^a-zA-Z0-9_-]/g, '_') || `asset-${Date.now()}`;
  79. const date = new Date();
  80. const yyyy = date.getFullYear();
  81. const mm = String(date.getMonth() + 1).padStart(2, '0');
  82. const dd = String(date.getDate()).padStart(2, '0');
  83. const timestamp = `${yyyy}${mm}${dd}-${Date.now()}`;
  84. return `${QINIU_CDN_PREFIX}/digital-human/${kind}/${yyyy}${mm}${dd}/${timestamp}-${baseName}${safeExt}`;
  85. }
  86. function normalizeKind(kind, mimeType) {
  87. if (kind === 'audio' || kind === 'video' || kind === 'image') return kind;
  88. if (mimeType.startsWith('audio/')) return 'audio';
  89. if (mimeType.startsWith('video/')) return 'video';
  90. return 'image';
  91. }
  92. function toBase64Url(input) {
  93. return base64UrlEncode(bytesFromString(input));
  94. }
  95. function pickParam(request, ...names) {
  96. const sources = [request.params, request.body, request];
  97. for (const src of sources) {
  98. if (!src || typeof src !== 'object') continue;
  99. for (const n of names) {
  100. const v = src[n];
  101. if (v !== undefined && v !== null && v !== '') return v;
  102. }
  103. }
  104. return null;
  105. }
  106. function readEnv(name) {
  107. if (typeof process !== 'undefined' && process.env && process.env[name]) {
  108. return process.env[name];
  109. }
  110. return '';
  111. }
  112. async function hmacSha1Base64Url(secret, message) {
  113. const subtle = typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle;
  114. if (subtle && typeof TextEncoder !== 'undefined') {
  115. const encoder = new TextEncoder();
  116. const key = await subtle.importKey(
  117. 'raw',
  118. encoder.encode(secret),
  119. { name: 'HMAC', hash: 'SHA-1' },
  120. false,
  121. ['sign']
  122. );
  123. const signature = await subtle.sign('HMAC', key, encoder.encode(message));
  124. return base64UrlEncode(new Uint8Array(signature));
  125. }
  126. if (typeof require === 'function') {
  127. const nodeCrypto = require('crypto');
  128. return nodeCrypto
  129. .createHmac('sha1', secret)
  130. .update(message)
  131. .digest('base64')
  132. .replace(/\+/g, '-')
  133. .replace(/\//g, '_');
  134. }
  135. return base64UrlEncode(hmacSha1Bytes(bytesFromString(secret), bytesFromString(message)));
  136. }
  137. function bytesFromString(input) {
  138. if (typeof TextEncoder !== 'undefined') {
  139. return new TextEncoder().encode(input);
  140. }
  141. if (typeof Buffer !== 'undefined') {
  142. return Buffer.from(input, 'utf8');
  143. }
  144. throw new Error('No UTF-8 encoder is available in this cloud runtime');
  145. }
  146. function base64UrlEncode(bytes) {
  147. let base64;
  148. if (typeof Buffer !== 'undefined') {
  149. base64 = Buffer.from(bytes).toString('base64');
  150. } else if (typeof btoa === 'function') {
  151. let binary = '';
  152. for (let i = 0; i < bytes.length; i++) {
  153. binary += String.fromCharCode(bytes[i]);
  154. }
  155. base64 = btoa(binary);
  156. } else {
  157. base64 = base64EncodeBytes(bytes);
  158. }
  159. return base64
  160. .replace(/\+/g, '-')
  161. .replace(/\//g, '_');
  162. }
  163. function base64EncodeBytes(bytes) {
  164. const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  165. let output = '';
  166. for (let i = 0; i < bytes.length; i += 3) {
  167. const b1 = bytes[i];
  168. const b2 = i + 1 < bytes.length ? bytes[i + 1] : 0;
  169. const b3 = i + 2 < bytes.length ? bytes[i + 2] : 0;
  170. const triplet = (b1 << 16) | (b2 << 8) | b3;
  171. output += alphabet[(triplet >> 18) & 0x3f];
  172. output += alphabet[(triplet >> 12) & 0x3f];
  173. output += i + 1 < bytes.length ? alphabet[(triplet >> 6) & 0x3f] : '=';
  174. output += i + 2 < bytes.length ? alphabet[triplet & 0x3f] : '=';
  175. }
  176. return output;
  177. }
  178. function hmacSha1Bytes(secretBytes, messageBytes) {
  179. const blockSize = 64;
  180. let key = toByteArray(secretBytes);
  181. if (key.length > blockSize) {
  182. key = sha1Bytes(key);
  183. }
  184. const paddedKey = new Uint8Array(blockSize);
  185. paddedKey.set(key);
  186. const innerPad = new Uint8Array(blockSize);
  187. const outerPad = new Uint8Array(blockSize);
  188. for (let i = 0; i < blockSize; i++) {
  189. innerPad[i] = paddedKey[i] ^ 0x36;
  190. outerPad[i] = paddedKey[i] ^ 0x5c;
  191. }
  192. return sha1Bytes(concatBytes(outerPad, sha1Bytes(concatBytes(innerPad, messageBytes))));
  193. }
  194. function sha1Bytes(inputBytes) {
  195. const bytes = toByteArray(inputBytes);
  196. const bitLength = bytes.length * 8;
  197. const paddedLength = (((bytes.length + 9 + 63) >> 6) << 6);
  198. const padded = new Uint8Array(paddedLength);
  199. padded.set(bytes);
  200. padded[bytes.length] = 0x80;
  201. const view = new DataView(padded.buffer);
  202. view.setUint32(paddedLength - 8, Math.floor(bitLength / 0x100000000), false);
  203. view.setUint32(paddedLength - 4, bitLength >>> 0, false);
  204. let h0 = 0x67452301;
  205. let h1 = 0xefcdab89;
  206. let h2 = 0x98badcfe;
  207. let h3 = 0x10325476;
  208. let h4 = 0xc3d2e1f0;
  209. const w = new Uint32Array(80);
  210. for (let offset = 0; offset < paddedLength; offset += 64) {
  211. for (let i = 0; i < 16; i++) {
  212. w[i] = view.getUint32(offset + i * 4, false);
  213. }
  214. for (let i = 16; i < 80; i++) {
  215. w[i] = rotateLeft(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
  216. }
  217. let a = h0;
  218. let b = h1;
  219. let c = h2;
  220. let d = h3;
  221. let e = h4;
  222. for (let i = 0; i < 80; i++) {
  223. let f;
  224. let k;
  225. if (i < 20) {
  226. f = (b & c) | (~b & d);
  227. k = 0x5a827999;
  228. } else if (i < 40) {
  229. f = b ^ c ^ d;
  230. k = 0x6ed9eba1;
  231. } else if (i < 60) {
  232. f = (b & c) | (b & d) | (c & d);
  233. k = 0x8f1bbcdc;
  234. } else {
  235. f = b ^ c ^ d;
  236. k = 0xca62c1d6;
  237. }
  238. const temp = (rotateLeft(a, 5) + f + e + k + w[i]) >>> 0;
  239. e = d;
  240. d = c;
  241. c = rotateLeft(b, 30);
  242. b = a;
  243. a = temp;
  244. }
  245. h0 = (h0 + a) >>> 0;
  246. h1 = (h1 + b) >>> 0;
  247. h2 = (h2 + c) >>> 0;
  248. h3 = (h3 + d) >>> 0;
  249. h4 = (h4 + e) >>> 0;
  250. }
  251. const output = new Uint8Array(20);
  252. const outputView = new DataView(output.buffer);
  253. outputView.setUint32(0, h0, false);
  254. outputView.setUint32(4, h1, false);
  255. outputView.setUint32(8, h2, false);
  256. outputView.setUint32(12, h3, false);
  257. outputView.setUint32(16, h4, false);
  258. return output;
  259. }
  260. function concatBytes(a, b) {
  261. const left = toByteArray(a);
  262. const right = toByteArray(b);
  263. const out = new Uint8Array(left.length + right.length);
  264. out.set(left);
  265. out.set(right, left.length);
  266. return out;
  267. }
  268. function toByteArray(bytes) {
  269. return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
  270. }
  271. function rotateLeft(value, bits) {
  272. return ((value << bits) | (value >>> (32 - bits))) >>> 0;
  273. }