把后端既有但未接线的 text-replay 能力在前端打通:前端直连模型生成正文时, 把生成过程/结果持久化到后端任务,支持刷新重放与任务中心可见。 后端(最小配套,text-replay 本为前端自管设计): - 新增 TaskStatusTextReplay 任务状态:不排入 worker 队列、不计费、不执行, 仅作为正文增量(text-deltas)的存储容器; - CreateTask 识别 input.replay=true 的请求,走 createTextReplayTask 轻量创建; - 新增 POST /tasks/:id/text-replay-complete: 写入最终正文、置为 succeeded、 触发 text-deltas 归并,使 queryTaskTextReplay 能读到 finalText。 前端: - 新增 creation-text-replay.ts 发布器: start 建任务、publish 节流上报增量、 finish 存最终全文,失败静默降级不阻塞生成; - create 文本模式生成时接入发布器,正文流不经展示改动、额外持久化到后端。 测试: 新增 isTextReplayTaskRequest 纯函数测(本地)、text-replay 闭环仓测 (建任务后写增量 -> complete -> succeeded + finalText,CI/cgo 验证)。 build/vet/tsc 全绿。
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import { appendTaskTextDelta, completeTextReplayTask, createGenerationTask } from "@/services/api/task-center";
|
||
import type { AiConfig } from "@/stores/use-config-store";
|
||
|
||
/**
|
||
* text-replay 前端发布器:为前端直连模型生成的文本,在后端维护一个
|
||
* text_replay 任务作持久化存档 + 回放。
|
||
*
|
||
* - start():创建后端 text_replay 任务(不排队、不计费),拿到 taskId;
|
||
* - publish(fullText):按阈值节流地把新增片段通过 appendTaskTextDelta 上报
|
||
* (生成中途刷新页面也能读到已生成部分);
|
||
* - finish(finalText):把最终正文写入任务并置为 succeeded,queryTaskTextReplay
|
||
* 即可读到 finalText。
|
||
*
|
||
* 全部失败静默降级:文本直连生成是主流程,后端持久化只是增强,失败不阻塞生成。
|
||
*/
|
||
export function createTextReplayPublisher(config: AiConfig, prompt: string) {
|
||
let taskId: string | null = null;
|
||
let lastLen = 0;
|
||
let buffered = "";
|
||
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
||
let closed = false;
|
||
|
||
// 增量累积到该阈值才落一次库,避免逐 token 高频写。
|
||
const FLUSH_THRESHOLD = 2048;
|
||
const flush = (force = false) => {
|
||
if (flushTimer) {
|
||
clearTimeout(flushTimer);
|
||
flushTimer = null;
|
||
}
|
||
if (closed || !taskId) return;
|
||
if (buffered.length >= FLUSH_THRESHOLD || force) {
|
||
const chunk = buffered;
|
||
buffered = "";
|
||
if (chunk.trim()) void appendTaskTextDelta(taskId, chunk).catch(() => {});
|
||
} else if (buffered) {
|
||
flushTimer = setTimeout(() => flush(true), 1000);
|
||
}
|
||
};
|
||
|
||
const start = async () => {
|
||
try {
|
||
const task = await createGenerationTask({
|
||
type: "text",
|
||
prompt: prompt.trim() || "文本创作",
|
||
model: config.model,
|
||
input: {
|
||
mode: "text",
|
||
replay: true,
|
||
prompt: prompt.trim() || "文本创作",
|
||
config: {
|
||
model: config.model,
|
||
baseUrl: config.baseUrl,
|
||
apiKey: config.apiKey,
|
||
apiFormat: config.apiFormat,
|
||
},
|
||
},
|
||
});
|
||
taskId = task?.id || null;
|
||
} catch {
|
||
taskId = null; // 降级:无持久化,不影响主流程
|
||
}
|
||
};
|
||
|
||
const publish = (fullText: string) => {
|
||
if (!taskId) return;
|
||
if (fullText.length <= lastLen) return;
|
||
buffered += fullText.slice(lastLen);
|
||
lastLen = fullText.length;
|
||
flush();
|
||
};
|
||
|
||
const finish = (finalText: string) => {
|
||
closed = true;
|
||
if (flushTimer) {
|
||
clearTimeout(flushTimer);
|
||
flushTimer = null;
|
||
}
|
||
if (!taskId) return;
|
||
void appendTaskTextDelta(taskId, buffered).catch(() => {});
|
||
if (finalText && finalText.trim()) {
|
||
void completeTextReplayTask(taskId, finalText).catch(() => {});
|
||
}
|
||
};
|
||
|
||
return { start, publish, finish };
|
||
}
|