import type { IncomingMessage, ServerResponse } from 'node:http'; import type { HostApiContext } from '../context'; import { parseJsonBody, sendJson } from '../route-utils'; import { WORKS_SQUARE_CONFIG } from '../works-config'; import { getValidWorksSquareAccessToken } from '../../services/works-square-session'; import { proxyAwareFetch } from '../../utils/proxy-fetch'; const LOCAL_ROOT = '/api/works/learning'; const UPSTREAM_ROOT = '/api/learning'; type Dependencies = { fetchImpl?: typeof fetch; getAccessToken?: typeof getValidWorksSquareAccessToken; apiBaseUrl?: string; }; function isLearningPath(pathname: string): boolean { return pathname === `${LOCAL_ROOT}/courses` || pathname === `${LOCAL_ROOT}/courses/mine` || pathname === `${LOCAL_ROOT}/progress` || /^\/api\/works\/learning\/courses\/[^/]+$/.test(pathname) || /^\/api\/works\/learning\/courses\/[^/]+\/progress$/.test(pathname) || pathname === `${LOCAL_ROOT}/generations` || /^\/api\/works\/learning\/generations\/[^/]+$/.test(pathname) || /^\/api\/works\/learning\/generations\/[^/]+\/(?:cancel|resume|finalize)$/.test(pathname); } function upstreamPath(pathname: string): string { return `${UPSTREAM_ROOT}${pathname.slice(LOCAL_ROOT.length)}`; } function allowedQuery(url: URL): string { const query = new URLSearchParams(); for (const key of ['limit', 'offset']) { const value = url.searchParams.get(key); if (value && /^\d{1,3}$/.test(value)) query.set(key, value); } const encoded = query.toString(); return encoded ? `?${encoded}` : ''; } function errorFields(payload: unknown, status: number): { code: string; error: string } { const record = payload && typeof payload === 'object' && !Array.isArray(payload) ? payload as Record : {}; const detail = record.detail && typeof record.detail === 'object' && !Array.isArray(record.detail) ? record.detail as Record : {}; return { code: typeof detail.code === 'string' ? detail.code : `LEARNING_HTTP_${status}`, error: typeof detail.message === 'string' ? detail.message : typeof record.detail === 'string' ? record.detail : typeof record.error === 'string' ? record.error : '学习服务暂时不可用', }; } export function createLearningRouteHandler(dependencies: Dependencies = {}) { const fetchImpl = dependencies.fetchImpl ?? proxyAwareFetch; const getAccessToken = dependencies.getAccessToken ?? getValidWorksSquareAccessToken; const apiBaseUrl = (dependencies.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/+$/, ''); return async function handleLearningRoutes( req: IncomingMessage, res: ServerResponse, url: URL, _ctx: HostApiContext, ): Promise { if (!isLearningPath(url.pathname)) return false; const isProgressWrite = req.method === 'PUT' && /^\/api\/works\/learning\/courses\/[^/]+\/progress$/.test(url.pathname); const isGenerationStart = req.method === 'POST' && url.pathname === `${LOCAL_ROOT}/generations`; const isGenerationFinalize = req.method === 'POST' && /^\/api\/works\/learning\/generations\/[^/]+\/finalize$/.test(url.pathname); const isGenerationControl = req.method === 'POST' && /^\/api\/works\/learning\/generations\/[^/]+\/(?:cancel|resume)$/.test(url.pathname); if (req.method !== 'GET' && !isProgressWrite && !isGenerationStart && !isGenerationFinalize && !isGenerationControl) { sendJson(res, 405, { success: false, code: 'LEARNING_METHOD_NOT_ALLOWED', error: '不支持的学习请求' }); return true; } try { const token = await getAccessToken({ fetchImpl }); if (!token) { sendJson(res, 401, { success: false, code: 'LEARNING_AUTH_REQUIRED', error: '请先登录' }); return true; } const body = isProgressWrite || isGenerationStart ? await parseJsonBody(req) : undefined; const request = (accessToken: string) => fetchImpl( `${apiBaseUrl}${upstreamPath(url.pathname)}${allowedQuery(url)}`, { method: req.method, headers: { Accept: 'application/json', Authorization: `Bearer ${accessToken}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), }, ...(body === undefined ? {} : { body: JSON.stringify(body) }), redirect: 'manual', }, ); let response = await request(token); if (response.status === 401) { await response.body?.cancel().catch(() => undefined); const refreshed = await getAccessToken({ fetchImpl, forceRefresh: true }); if (refreshed) response = await request(refreshed); } const payload = await response.json().catch(() => null); if (!response.ok || payload === null) { const fields = errorFields(payload, response.status || 502); sendJson(res, response.status || 502, { success: false, status: response.status, ...fields }); return true; } sendJson(res, response.status, { success: true, data: payload }); return true; } catch (error) { sendJson(res, 502, { success: false, status: 502, code: 'LEARNING_UNAVAILABLE', error: error instanceof Error ? error.message : '学习服务暂时不可用', }); return true; } }; } export const handleLearningRoutes = createLearningRouteHandler();