upload.mjs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. // Copyright (c) 未来飞马
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at https://mozilla.org/MPL/2.0/.
  6. //
  7. // Trademark Notice:
  8. // The MPL-2.0 license grants copyright permissions for source code only.
  9. // It does NOT grant any rights to use trademarks including "未来飞马",
  10. // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
  11. // Any use of these trademarks requires separate written permission.
  12. // 端点常量 —— 仅在此处定义,禁止在其他地方硬编码
  13. export const ENDPOINTS = {
  14. functions: 'https://server.fmode.cn/api/functions',
  15. publicBase: 'https://s3.fmode.cn',
  16. };
  17. const FUNCTION_ID = 'AlP56LCKFm'; // fmodeagent-upload-url v0.2.2
  18. /**
  19. * 向云函数申请预签名 PUT URL。
  20. * @param {object} opts
  21. * @param {string} opts.sessionToken - 用户 session token (r:xxx)
  22. * @param {string} opts.key - 相对路径,如 log/20260923/1400.json
  23. * @param {number} opts.size - 文件字节数
  24. * @returns {Promise<{uploadUrl, key, publicUrl, headers, expiresAt}>}
  25. */
  26. export async function getUploadUrl({ sessionToken, key, size }) {
  27. const filename = key.split('/').pop();
  28. const res = await fetch(ENDPOINTS.functions, {
  29. method: 'POST',
  30. headers: { 'Content-Type': 'application/json' },
  31. body: JSON.stringify({
  32. token: sessionToken,
  33. id: FUNCTION_ID,
  34. params: {
  35. filename,
  36. mimeType: 'application/json',
  37. size,
  38. namespace: 'log',
  39. key,
  40. },
  41. }),
  42. signal: AbortSignal.timeout(15000),
  43. });
  44. if (!res.ok) {
  45. const text = await res.text().catch(() => '');
  46. throw new Error(`云函数请求失败 HTTP ${res.status}: ${text.slice(0, 200)}`);
  47. }
  48. const data = await res.json();
  49. if (data.code !== 200) {
  50. throw new Error(`云函数返回错误 code=${data.code}: ${JSON.stringify(data).slice(0, 300)}`);
  51. }
  52. return data; // { uploadUrl, key, publicUrl, headers, expiresAt }
  53. }
  54. /**
  55. * 采集 JSON 对象并直传到个人 S3 空间。
  56. * @param {object} opts
  57. * @param {string} opts.sessionToken - 用户 session token
  58. * @param {string} opts.key - 对象相对路径,如 log/20260923/1400.json
  59. * @param {object} opts.content - 要上传的 JSON 对象
  60. * @returns {Promise<{publicUrl, key}>}
  61. */
  62. export async function uploadJson({ sessionToken, key, content }) {
  63. const buf = Buffer.from(JSON.stringify(content, null, 2), 'utf-8');
  64. const size = buf.length;
  65. const { uploadUrl, headers, publicUrl, key: serverKey } = await getUploadUrl({ sessionToken, key, size });
  66. // PUT 时必须携带云函数返回的 headers(Content-Type + x-obs-acl),否则 403
  67. const putRes = await fetch(uploadUrl, {
  68. method: 'PUT',
  69. headers: { ...headers },
  70. body: buf,
  71. signal: AbortSignal.timeout(30000),
  72. });
  73. if (!putRes.ok) {
  74. const text = await putRes.text().catch(() => '');
  75. throw new Error(`PUT 上传失败 HTTP ${putRes.status}: ${text.slice(0, 300)}`);
  76. }
  77. return { publicUrl, key: serverKey };
  78. }
  79. /**
  80. * 尝试列举旧日志(滚动删除用)。
  81. * 注:OBS 的 S3 列举需 AK/SK 鉴权,无法通过预签名仅读操作完成;
  82. * 此函数仅为 API 兼容占位,总是返回空列表并附提示。
  83. */
  84. export async function listOldLogs(userid, cutoffDay) {
  85. // 旧日志自动删除需 AK/SK 权限,当前免密钥通道不支持列举操作。
  86. // 运维001 可通过 OBS 控制台或 obsutil 定期清理 user/<userid>/log/ 目录。
  87. return { skipped: true, reason: '列举操作需 AK/SK 权限,云函数通道不支持;请在 OBS 控制台或 obsutil 手动清理旧日志' };
  88. }