| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- // Copyright (c) 未来飞马
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this
- // file, You can obtain one at https://mozilla.org/MPL/2.0/.
- //
- // Trademark Notice:
- // The MPL-2.0 license grants copyright permissions for source code only.
- // It does NOT grant any rights to use trademarks including "未来飞马",
- // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
- // Any use of these trademarks requires separate written permission.
- // 端点常量 —— 仅在此处定义,禁止在其他地方硬编码
- export const ENDPOINTS = {
- functions: 'https://server.fmode.cn/api/functions',
- publicBase: 'https://s3.fmode.cn',
- };
- const FUNCTION_ID = 'AlP56LCKFm'; // fmodeagent-upload-url v0.2.2
- /**
- * 向云函数申请预签名 PUT URL。
- * @param {object} opts
- * @param {string} opts.sessionToken - 用户 session token (r:xxx)
- * @param {string} opts.key - 相对路径,如 log/20260923/1400.json
- * @param {number} opts.size - 文件字节数
- * @returns {Promise<{uploadUrl, key, publicUrl, headers, expiresAt}>}
- */
- export async function getUploadUrl({ sessionToken, key, size }) {
- const filename = key.split('/').pop();
- const res = await fetch(ENDPOINTS.functions, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- token: sessionToken,
- id: FUNCTION_ID,
- params: {
- filename,
- mimeType: 'application/json',
- size,
- namespace: 'log',
- key,
- },
- }),
- signal: AbortSignal.timeout(15000),
- });
- if (!res.ok) {
- const text = await res.text().catch(() => '');
- throw new Error(`云函数请求失败 HTTP ${res.status}: ${text.slice(0, 200)}`);
- }
- const data = await res.json();
- if (data.code !== 200) {
- throw new Error(`云函数返回错误 code=${data.code}: ${JSON.stringify(data).slice(0, 300)}`);
- }
- return data; // { uploadUrl, key, publicUrl, headers, expiresAt }
- }
- /**
- * 采集 JSON 对象并直传到个人 S3 空间。
- * @param {object} opts
- * @param {string} opts.sessionToken - 用户 session token
- * @param {string} opts.key - 对象相对路径,如 log/20260923/1400.json
- * @param {object} opts.content - 要上传的 JSON 对象
- * @returns {Promise<{publicUrl, key}>}
- */
- export async function uploadJson({ sessionToken, key, content }) {
- const buf = Buffer.from(JSON.stringify(content, null, 2), 'utf-8');
- const size = buf.length;
- const { uploadUrl, headers, publicUrl, key: serverKey } = await getUploadUrl({ sessionToken, key, size });
- // PUT 时必须携带云函数返回的 headers(Content-Type + x-obs-acl),否则 403
- const putRes = await fetch(uploadUrl, {
- method: 'PUT',
- headers: { ...headers },
- body: buf,
- signal: AbortSignal.timeout(30000),
- });
- if (!putRes.ok) {
- const text = await putRes.text().catch(() => '');
- throw new Error(`PUT 上传失败 HTTP ${putRes.status}: ${text.slice(0, 300)}`);
- }
- return { publicUrl, key: serverKey };
- }
- /**
- * 尝试列举旧日志(滚动删除用)。
- * 注:OBS 的 S3 列举需 AK/SK 鉴权,无法通过预签名仅读操作完成;
- * 此函数仅为 API 兼容占位,总是返回空列表并附提示。
- */
- export async function listOldLogs(userid, cutoffDay) {
- // 旧日志自动删除需 AK/SK 权限,当前免密钥通道不支持列举操作。
- // 运维001 可通过 OBS 控制台或 obsutil 定期清理 user/<userid>/log/ 目录。
- return { skipped: true, reason: '列举操作需 AK/SK 权限,云函数通道不支持;请在 OBS 控制台或 obsutil 手动清理旧日志' };
- }
|