feat: M1 — 立项→写章草稿(SSE)→自动保存;连一家 provider
- 薄自建 LLM 网关:OpenAI 兼容适配器(DeepSeek) + instructor 结构化输出 + usage_ledger 记账 + 档位路由 - 记忆服务 assemble:确定性选择(显式+主角+近况) + 渲染卡 + 缓存断点(中性文本) - LangGraph 写章节点 + Postgres checkpointer + SSE 归一(token/done/error) - API:立项 + 写章 draft(SSE) + PUT 自动保存 + 提供商凭据(Fernet 加密/测试连接) - 前端:AppShell + 作品库 + 5 步立项向导 + 写作工作台(流式打字机+自动保存) + 设置页 - M1 E2E:真实 DB + mock 网关零 token 走通闭环
This commit is contained in:
62
apps/web/lib/wizard/wizard.ts
Normal file
62
apps/web/lib/wizard/wizard.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { ProjectCreateRequest } from "@/lib/api/types";
|
||||
|
||||
// 立项向导 5 步(UX §6.2)。表单状态用字符串/数组,提交时归一为 ProjectCreateRequest。
|
||||
export interface WizardForm {
|
||||
title: string;
|
||||
genre: string;
|
||||
logline: string;
|
||||
sellingPoints: string[];
|
||||
structure: string;
|
||||
premise: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
export const WIZARD_STEPS = 5;
|
||||
|
||||
export const emptyWizardForm: WizardForm = {
|
||||
title: "",
|
||||
genre: "",
|
||||
logline: "",
|
||||
sellingPoints: [],
|
||||
structure: "",
|
||||
premise: "",
|
||||
theme: "",
|
||||
};
|
||||
|
||||
export const GENRES = ["玄幻", "仙侠", "都市", "科幻", "历史", "悬疑"];
|
||||
export const STRUCTURES = ["三幕", "故事圈", "雪花"];
|
||||
export const SELLING_POINT_PRESETS = ["逆袭", "打脸", "系统流", "双男主", "群像"];
|
||||
|
||||
// 每一步可否前进:仅第 1 步(书名)必填,其余可空着继续(草稿式立项)。
|
||||
export function canAdvance(step: number, form: WizardForm): boolean {
|
||||
if (step === 1) return form.title.trim().length > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 整个向导可否提交:书名必填。
|
||||
export function canSubmit(form: WizardForm): boolean {
|
||||
return form.title.trim().length > 0;
|
||||
}
|
||||
|
||||
export function clampStep(step: number): number {
|
||||
if (step < 1) return 1;
|
||||
if (step > WIZARD_STEPS) return WIZARD_STEPS;
|
||||
return step;
|
||||
}
|
||||
|
||||
// 归一为后端请求体(snake_case,空值落 null/省略)。
|
||||
export function toCreateRequest(form: WizardForm): ProjectCreateRequest {
|
||||
const trim = (s: string): string | null => {
|
||||
const v = s.trim();
|
||||
return v.length > 0 ? v : null;
|
||||
};
|
||||
return {
|
||||
title: form.title.trim(),
|
||||
genre: trim(form.genre),
|
||||
logline: trim(form.logline),
|
||||
premise: trim(form.premise),
|
||||
theme: trim(form.theme),
|
||||
selling_points: form.sellingPoints,
|
||||
structure: trim(form.structure),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user