与
backend/internal/api实现一致。所有接口前缀/api/v1,默认监听:8080(cmd/server)。 单进程:API + 进程内队列 + Worker 合一,不依赖 Redis(见 DESIGN §2.1)。
| 项 | 说明 |
|---|---|
| Base URL | http://localhost:8080/api/v1 |
| 请求体 | JSON(Content-Type: application/json) |
| 成功响应 | JSON 或 YAML(取剧本/设定集为 YAML 文本) |
| 错误响应 | { "error": "原因" },配合对应 HTTP 状态码 |
| CORS | 允许任意来源(Access-Control-Allow-Origin: *,方法 GET/POST/PUT/OPTIONS),前端开发可直连 |
| 鉴权 | 无(单用户本地工具) |
queued → analyzing(Phase0) → converting(Phase1: i/N) → assembling(Phase2) → done
▲ │ └──────────(任一阶段出错)──────────┘→ failed
│ resume │ pause
└──────── paused ◀─────────────┘ (paused / failed 都可 resume,从断点章续跑)
| 状态 | 含义 |
|---|---|
queued |
已入队,待处理 |
analyzing |
Phase 0:全局预分析(StoryBible) |
converting |
Phase 1:逐章转换(看 doneChapters/totalChapters) |
assembling |
Phase 2:汇总成完整剧本 |
done |
完成,可取 screenplay |
failed |
失败,error 字段有原因(可 resume 从断点续跑) |
paused |
已暂停(pause 置位,已完成章节保留,可 resume) |
GET /api/v1/health
200
{ "status": "ok" }POST /api/v1/conversions
把整本小说原文提交转换。异步:立即返回 jobId,转换在后台跑。
请求体
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
text |
string | ✓ | 小说原文(≥3 章效果最佳;服务端做章节切分) |
title |
string | 作品标题(用于剧本 title) |
{ "title": "兄妹", "text": "第一章 归来\n林夜推门而入……\n\n第二章 重逢\n……" }响应
| 码 | 含义 | 体 |
|---|---|---|
202 Accepted |
已入队 | { "jobId": "uuid" } |
400 |
请求体非法(如缺 text) |
{ "error": "..." } |
500 |
建作业/存原文失败 | { "error": "..." } |
503 |
队列已满 | { "error": "..." } |
curl -XPOST localhost:8080/api/v1/conversions \
-H 'Content-Type: application/json' \
-d '{"title":"兄妹","text":"第一章 ...\n\n第二章 ..."}'
# => {"jobId":"9fd61ae3-45ac-4cd4-ba0c-dd06cf9fa9b9"}POST /api/v1/conversions/upload # multipart/form-data
与 §2 等价,但用文件上传代替 JSON 正文。后端解析文件 → 文本后建作业。
表单字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file |
file | ✓ | 小说文件,支持 .txt / .md / .epub(≤32MB) |
title |
string | 标题(缺省取文件名去扩展名) |
- 编码:
.txt/.md自动识别 UTF-8 / GB18030 / GBK(中文小说常见),去 BOM。 - epub:按 OPF spine 阅读顺序解包提取正文(标准库
archive/zip)。
响应:同 §2(202 {jobId} / 400 文件缺失或解析失败 / 413 过大 / 503 队列满)。
curl -XPOST localhost:8080/api/v1/conversions/upload \
-F 'file=@骆驼祥子.txt' -F 'title=骆驼祥子'
# => {"jobId":"..."}GET /api/v1/conversions/{id}
200
{
"id": "9fd61ae3-...",
"title": "兄妹",
"status": "converting",
"totalChapters": 12,
"doneChapters": 3,
"error": "",
"createdAt": 1749200000,
"updatedAt": 1749200200
}404{ "error": "作业不存在" }。- 前端可每 1~2s 轮询此接口作为 SSE 的兜底(权威进度在此)。
GET /api/v1/conversions
返回全部作业,按 createdAt 倒序,元素同 §3。供前端做历史/续跑入口。
200 —— JSON 数组(无作业时 [])。
POST /api/v1/conversions/{id}/pause # 暂停进行中的作业
POST /api/v1/conversions/{id}/resume # 继续(暂停或失败的作业,从断点章续跑)
pause:真正取消正在跑的作业(停止 LLM 调用),已完成章节保留,置paused。resume:置回queued重新入队,从doneChapters的下一章续跑;paused与failed都可 resume。
| 码 | 含义 |
|---|---|
200 |
成功,返回最新状态(同 §3) |
404 |
作业不存在 |
409 |
状态不允许(如对已 done 的作业 pause,或对未 paused/failed 的作业 resume) |
curl -XPOST localhost:8080/api/v1/conversions/$ID/pause
curl -XPOST localhost:8080/api/v1/conversions/$ID/resumeGET /api/v1/conversions/{id}/events
text/event-stream。连接后先推一次当前 status,随后持续推送进度事件,直到 done/failed 或客户端断开。
事件类型(SSE event: 字段)
| event | data | 说明 |
|---|---|---|
status |
状态字符串 | 连接时的当前状态;暂停/继续时也会推一次(如 paused/queued) |
progress |
"i/N" |
第 i 章完成(共 N 章) |
delta |
文本分片 | 模型流式吐字(打字机效果,本章生成中) |
done |
空 | 作业完成 |
failed |
错误信息 | 作业失败 |
SSE 是"临时态",事件可能丢;权威进度始终以 §3 轮询为准。
浏览器用法
const es = new EventSource(`/api/v1/conversions/${id}/events`);
es.addEventListener("delta", e => append(e.data)); // 打字机
es.addEventListener("progress", e => setProgress(e.data));
es.addEventListener("done", () => { es.close(); fetchScreenplay(); });
es.addEventListener("failed", e => { es.close(); showError(e.data); });GET /api/v1/conversions/{id}/screenplay # YAML(默认)
GET /api/v1/conversions/{id}/screenplay?format=json # JSON
按需汇总:从已生成的场景实时汇总成完整剧本(合并各章 + 角色名→ID 消解 + 校验)。
| 码 | 含义 |
|---|---|
200 |
YAML(application/x-yaml)或 JSON |
404 |
作业不存在,或尚无可汇总场景(未就绪) |
剧本结构(YAML,详见 docs/SCHEMA.md)
schema_version: "1.0"
title: 兄妹
logline: (待补)
meta:
source_chapters: [1, 2]
chapter_count: 2
generated_by: NovelToScript
generated_at: "2026-06-06T14:28:35Z"
characters:
- id: char_001
name: 林夜
aliases: [小夜]
role: protagonist # protagonist/antagonist/supporting/minor(可空)
description: 归来的空宅主人
scenes:
- id: s1_1
chapter: 1 # 来源章节(溯源)
order: 1 # 全局顺序(严格递增)
heading: { setting: INT, location: 林家老宅, time: 夜 } # setting: INT/EXT/INT-EXT
synopsis: "" # 可空;自动兜底场景会标 "[自动兜底…请人工整理]"
characters: [char_001] # 出场角色 ID
elements:
- { type: action, text: "林夜推门而入。" }
- { type: dialogue, character: char_001, line: "谁在那儿?", parenthetical: "压低声音" }
- { type: narration, text: "风从窗缝钻入。" }
transition: "切至:" # 可空GET /api/v1/conversions/{id}/characters
返回 CharacterKB 全量角色(比剧本里的 characters 更详细,含逐章累积的状态)。
200 —— JSON 数组,每个元素:
{
"id": "char_001",
"name": "林夜",
"aliases": ["小夜"],
"one_line": "归来的空宅主人",
"personality": ["念旧", "沉稳", "警觉"],
"appearance": "",
"abilities": [{ "name": "御剑术", "since_chapter": 2 }],
"key_actions": [{ "chapter": 1, "event": "点亮油灯" }],
"relationships": { "林溪": { "relation": "妹妹", "status": "重逢" } },
"arc_state": "",
"emotional_state": "激动",
"first_seen": 1,
"last_seen": 3
}未生成时返回 []。
GET /api/v1/conversions/{id}/story-bible # YAML(默认)
GET /api/v1/conversions/{id}/story-bible?format=json # JSON(结构化,供前端可视化)
200 —— Phase 0 产出的 StoryBible。?format=json 输出同结构的 JSON(键为 snake_case);默认输出 YAML 原样:
synopsis: 一个关于兄妹乱世重逢的故事
theme: 亲情
tone: 沉郁
genre: 古装
characters:
- { name: 林夜, aliases: [小夜], role: 主角, description: ... }
timeline: ["林夜归来", "兄妹重逢"]
glossary:
- { term: 御剑术, definition: ... }404{ "error": "设定集尚未就绪" }(Phase 0 未完成)。
PUT /api/v1/conversions/{id}/screenplay
当前未实现。规划:接收前端编辑后的
screenplay.yaml,校验后落库覆盖。需先给 store 增加剧本槽。前端可先做只读预览。
前端 后端
│ POST /conversions {text} ──►│ 建作业(queued)+存原文+入队 → 202 {jobId}
│◄────────── {jobId} ─────────│
│ open SSE /{id}/events ─────►│ 推 status/progress/delta…
│ (并行)轮询 GET /{id} ─────►│ 返回 status/i/N(兜底)
│ │ Worker: Phase0→Phase1(逐章)→Phase2 → done
│◄──── event: done ───────────│
│ GET /{id}/screenplay ──────►│ 按需汇总 → YAML
│◄──────── screenplay.yaml ───│