upload.mjs 3.5 KB

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