- 薄自建 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 走通闭环
75 lines
2.0 KiB
TypeScript
75 lines
2.0 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||
|
||
import { api } from "@/lib/api/client";
|
||
import { useToast } from "@/components/Toast";
|
||
import {
|
||
AUTOSAVE_DELAY_MS,
|
||
createDebouncedSaver,
|
||
formatSavedAt,
|
||
type DebouncedSaver,
|
||
} from "./autosave";
|
||
|
||
export type SaveStatus = "idle" | "saving" | "saved" | "error";
|
||
|
||
export interface UseAutosave {
|
||
status: SaveStatus;
|
||
savedLabel: string | null;
|
||
onChange: (text: string) => void;
|
||
flush: () => void;
|
||
}
|
||
|
||
// 防抖自动保存草稿:乐观置 saving → PUT 成功置 saved(落「保存于 hh:mm」);
|
||
// 失败回滚状态 + toast,不丢正文(已在编辑器里)。
|
||
export function useAutosave(
|
||
projectId: string,
|
||
chapterNo: number,
|
||
): UseAutosave {
|
||
const [status, setStatus] = useState<SaveStatus>("idle");
|
||
const [savedLabel, setSavedLabel] = useState<string | null>(null);
|
||
const toast = useToast();
|
||
const lastSavedRef = useRef<string | null>(null);
|
||
|
||
const save = useCallback(
|
||
async (text: string): Promise<void> => {
|
||
if (text === lastSavedRef.current) return;
|
||
setStatus("saving");
|
||
const { error } = await api.PUT(
|
||
"/projects/{project_id}/chapters/{chapter_no}/draft",
|
||
{
|
||
params: { path: { project_id: projectId, chapter_no: chapterNo } },
|
||
body: { text },
|
||
},
|
||
);
|
||
if (error) {
|
||
setStatus("error");
|
||
toast("自动保存失败,稍后重试(正文未丢失)", "error");
|
||
return;
|
||
}
|
||
lastSavedRef.current = text;
|
||
setSavedLabel(formatSavedAt(new Date()));
|
||
setStatus("saved");
|
||
},
|
||
[projectId, chapterNo, toast],
|
||
);
|
||
|
||
const saver: DebouncedSaver = useMemo(
|
||
() => createDebouncedSaver(save, AUTOSAVE_DELAY_MS),
|
||
[save],
|
||
);
|
||
|
||
useEffect(() => () => saver.cancel(), [saver]);
|
||
|
||
const onChange = useCallback(
|
||
(text: string) => {
|
||
saver.schedule(text);
|
||
},
|
||
[saver],
|
||
);
|
||
|
||
const flush = useCallback(() => saver.flush(), [saver]);
|
||
|
||
return { status, savedLabel, onChange, flush };
|
||
}
|