Files
writer-work-flow/apps/web/lib/api/server.ts
Yaojia Wang f43ccd293f feat(toolbox): T6 创作工具箱通用生成器框架 — 8 新生成器 + 声明驱动落地页 + P2 收尾
通用执行路径驱动全部生成器("加生成器=加一份声明"):
- @llm: ww_agents +7 输出 schema + 7 spec(book-title/blurb/name/golden-finger/
  glossary/opening/fine-outline,只声明 tier)+ build_outline_chapter_context
- @backend: ww_skills GeneratorTool 描述符 + TOOLBOX(11) + get_tool;3 通用端点
  GET /skills/toolbox · POST .../skills/{tool_key}/generate(预览不写库,仅记账) ·
  POST .../ingest(复用 continuity 409 + partition_writes 白名单);纯 context 派发
- @frontend: 工具箱落地页 RSC + 声明驱动 GeneratorRunner + lib/toolbox 纯函数
  + LeftNav「工具箱」+ ⌘K nav-toolbox/action-gen-*;legacy 3 跳现页
- @qa: tests/test_t6_toolbox_e2e.py 5 用例真 pg + mock 网关零 token,无端点 bug
- P2 收尾: 限流→decisions.md 记延后(单用户原型);noopener/Committable 早已修

守不变量 #2(只声明 tier)/#3(预览不写库,入库经验收 gate)/#9(缓存前缀)。无 DB 迁移。
门禁绿: 后端 ruff/format/mypy 195/alembic 无漂移/pytest 583;前端 lint/tsc/vitest 279/build。
spec 回写 PRODUCT_SPEC §7 + ARCHITECTURE §7.2 端点表。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:37:55 +02:00

162 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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