import { serverApiBase } from "./config"; import type { ChapterListItem, CharacterListResponse, DraftView, ForeshadowBoardResponse, OutlineChapterView, OutlineResponse, ProjectListResponse, ProjectResponse, OAuthStatusResponse, ProvidersResponse, ReviewHistoryResponse, RuleListResponse, SkillListResponse, StyleFingerprintResponse, TemplateResponse, ToolboxListResponse, WorldEntityListResponse, } from "./types"; // 解析 JSON 响应体并做最小运行时校验:非 null 对象/数组才放行, // 否则抛结构化错误(后端契约保证为 JSON 对象;防御被代理/网关污染的非 JSON 响应)。 // 校验通过后 narrow 到 T(OpenAPI 生成类型为权威形状,此处仅守边界非 null)。 async function parseJsonBody(res: Response, path: string): Promise { let body: unknown; try { body = await res.json(); } catch { throw new Error(`响应非 JSON:${path}`); } if (typeof body !== "object" || body === null) { throw new Error(`响应体形状异常(期望对象):${path}`); } return body as T; } // 服务端只读取数据(Server Components)。失败时抛出,由页面边界处理。 async function getJson(path: string): Promise { const res = await fetch(`${serverApiBase()}${path}`, { cache: "no-store" }); if (!res.ok) { throw new Error(`请求失败 ${res.status}: ${path}`); } return parseJsonBody(res, path); } // 像上面一样读取,但 404(无指纹)返回 null 而非抛出(首学前页面照常渲染)。 async function getJsonOrNull(path: string): Promise { const res = await fetch(`${serverApiBase()}${path}`, { cache: "no-store" }); if (res.status === 404) return null; if (!res.ok) { throw new Error(`请求失败 ${res.status}: ${path}`); } return parseJsonBody(res, path); } export async function fetchProjects(): Promise { return getJson("/projects"); } export async function fetchProject(projectId: string): Promise { return getJson(`/projects/${projectId}`); } export async function fetchProviders(): Promise { return getJson("/settings/providers"); } // Kimi Code OAuth 连接态(GET .../oauth/status;无 token 本体)。 export async function fetchKimiOauthStatus(): Promise { return getJson( "/settings/providers/kimi-code/oauth/status", ); } export async function fetchReviews( projectId: string, chapterNo: number, ): Promise { return getJson( `/projects/${projectId}/chapters/${chapterNo}/reviews`, ); } // 伏笔看板(全量,前端按 status 分四泳道)。 export async function fetchForeshadow( projectId: string, ): Promise { return getJson(`/projects/${projectId}/foreshadow`); } // 最新文风指纹(GET /style);无指纹(404)→ null(首学前页面照常渲染)。 export async function fetchStyleFingerprint( projectId: string, ): Promise { return getJsonOrNull( `/projects/${projectId}/style`, ); } // 规则列表(GET .../rules,规则页)。 export async function fetchRules( projectId: string, ): Promise { return getJson(`/projects/${projectId}/rules`); } // 技能库注册表(GET /skills,全局只读)。 export async function fetchSkills(): Promise { return getJson("/skills"); } // 创作工具箱描述符(GET /skills/toolbox,全局只读)。 // 任何错误(含后端未上线)降级为空列表,工具箱页照常渲染(不阻塞进页)。 export async function fetchToolbox(): Promise { try { return await getJson("/skills/toolbox"); } catch { return { tools: [] }; } } // 设定库 Codex:跨会话全量已入库角色(GET .../characters;无行→空列表,非 404)。 export async function fetchCharacters( projectId: string, ): Promise { return getJson(`/projects/${projectId}/characters`); } // 设定库 Codex:跨会话全量已入库世界观实体(GET .../world_entities;无行→空列表)。 export async function fetchWorldEntities( projectId: string, ): Promise { return getJson( `/projects/${projectId}/world_entities`, ); } // 已存大纲(GET .../outline,与 POST 同形,按 chapter_no 排序)。 // 无大纲→空列表;任何错误亦降级为空(进页不阻塞,生成流照常工作)。 export async function fetchOutline( projectId: string, ): Promise { try { const res = await getJson( `/projects/${projectId}/outline`, ); return res.chapters ?? []; } catch { return []; } } // 列章(GET .../chapters,裸数组):审稿选章枚举真实存在的章 + 可审/已审标记。 // 任何错误降级为空列表(进页不阻塞,回退到大纲/当前章导航)。 export async function fetchChapters( projectId: string, ): Promise { try { return await getJson(`/projects/${projectId}/chapters`); } catch { return []; } } // 提示词/模板库列表(GET /templates,全局只读;裸数组)。 // 任何错误(含后端未上线)降级为空列表,模板页照常渲染(进页不阻塞)。 export async function fetchTemplates(): Promise { try { return await getJson("/templates"); } catch { return []; } } // 已存草稿(GET .../draft,含正文,供工作台重访重载编辑器)。 // 404(尚无草稿,新章)→ null,由调用方当空编辑器处理,不抛错。 export async function fetchDraft( projectId: string, chapterNo: number, ): Promise { return getJsonOrNull( `/projects/${projectId}/chapters/${chapterNo}/draft`, ); }