| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- // 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.
- /**
- * 云函数客户端:把案例包提交到 caseSubmit,落入待审清单。
- *
- * 契约(SHARED-CONTRACT.md 第 3 节):
- * 入参 {sourceType, idempotencyKey, title, summary, materialAssets[], groupInfo?,
- * schoolCanonical?, tags[], highlightTypes[], scenarioTags[], objectionTags[],
- * riskFlags[], authorizationStatus, submittedBy?}
- * 返回 {ok:true, result:{objectId, reviewStatus, readyForUse}}
- * 落库 reviewStatus='pending'、readyForUse=false —— 新案例先进待审清单,不进公共素材库。
- *
- * 安全:
- * - 云函数基址来自项目 CLAUDE.md:https://server.lumistedu.com/api/functions
- * - session token 从环境变量 / 本地配置读取,**仅内存持有**,不写文件、不进日志、不进报告。
- * - tenantId 由服务端从会话上下文解析,客户端不传(传了也会被服务端拒绝不匹配)。
- */
- const FUNCTIONS_URL = process.env.CASE_FUNCTIONS_URL
- || 'https://server.lumistedu.com/api/functions';
- const TOKEN_SOURCES = [
- { env: 'CASE_PARSE_SESSION_TOKEN', label: 'env:CASE_PARSE_SESSION_TOKEN' },
- { env: 'PARSE_SESSION_TOKEN', label: 'env:PARSE_SESSION_TOKEN' },
- ];
- function resolveToken(explicit) {
- if (explicit) return { token: explicit, source: 'explicit' };
- for (const item of TOKEN_SOURCES) {
- const value = process.env[item.env];
- if (value && value.trim()) return { token: value.trim(), source: item.label };
- }
- return { token: '', source: 'missing' };
- }
- export class CloudError extends Error {
- constructor(message, code, requestId) {
- super(message);
- this.name = 'CloudError';
- this.code = code || 'INTERNAL_ERROR';
- this.requestId = requestId || '';
- }
- }
- /**
- * 调用云函数。
- * @returns {Promise<{ok:true, result:object}|{ok:false, error:{code,message,requestId}}>}
- * 网络不可达时抛 CloudError(调用方转成 dry-run 提示,不伪造成功)。
- */
- export async function callFunction(name, params, options = {}) {
- const { token, source } = resolveToken(options.token);
- const headers = { 'Content-Type': 'application/json' };
- if (token) headers['X-Parse-Session-Token'] = token;
- const controller = new AbortController();
- const timer = setTimeout(() => controller.abort(), options.timeoutMs || 20000);
- let response;
- try {
- response = await fetch(FUNCTIONS_URL, {
- method: 'POST',
- headers,
- body: JSON.stringify({ path: `/${name}`, params: params || {}, token }),
- signal: controller.signal,
- });
- } catch (error) {
- clearTimeout(timer);
- const reason = error.name === 'AbortError' ? '请求超时' : error.message;
- throw new CloudError(`云函数 ${name} 不可达:${reason}(token 来源:${source})`, 'NETWORK_UNAVAILABLE');
- }
- clearTimeout(timer);
- let payload;
- try {
- payload = await response.json();
- } catch {
- throw new CloudError(`云函数 ${name} 返回不是 JSON(HTTP ${response.status})`, 'INTERNAL_ERROR');
- }
- if (!response.ok || payload.error) {
- const error = payload.error;
- // 本契约的错误是对象 {code,message,requestId};若 error 是字符串/HTML,
- // 说明这个端点根本不提供本项目的函数服务(如定位器端点、无函数运行时)→ 供上层回落。
- const isContractError = error && typeof error === 'object';
- return {
- ok: false,
- endpointUnavailable: !isContractError,
- error: {
- code: (isContractError && error.code) || 'INTERNAL_ERROR',
- message: (isContractError && error.message) || `云函数 ${name} 调用失败(HTTP ${response.status})`,
- requestId: (isContractError && error.requestId) || '',
- },
- };
- }
- return { ok: true, result: payload.result || payload.data || {} };
- }
- /**
- * 提交案例到待审清单。
- * @param {object} casePackage 组装好的案例包(见 case-intake.mjs buildPayload)
- */
- export async function submitCase(casePackage, options = {}) {
- const payload = {
- sourceType: casePackage.sourceType,
- idempotencyKey: casePackage.idempotencyKey,
- title: casePackage.title,
- summary: casePackage.summary,
- materialAssets: casePackage.materialAssets,
- groupInfo: casePackage.groupInfo,
- schoolCanonical: casePackage.schoolCanonical,
- tags: casePackage.tags,
- highlightTypes: casePackage.highlightTypes,
- scenarioTags: casePackage.scenarioTags,
- objectionTags: casePackage.objectionTags,
- riskFlags: casePackage.riskFlags,
- authorizationStatus: casePackage.authorizationStatus,
- };
- if (options.submittedBy) payload.submittedBy = options.submittedBy;
- return callFunction('caseSubmit', payload, options);
- }
- /** 待审清单(只读,便于采集完立刻确认落库)。 */
- export async function pendingList(params = {}, options = {}) {
- return callFunction('casePendingList', params, options);
- }
|