M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。 M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变); 网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则; 前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。 K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径; coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。 本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400); GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化; 字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。 门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useReducer, useRef } from "react";
|
||
|
||
import { api } from "@/lib/api/client";
|
||
import {
|
||
initialPollState,
|
||
isTerminal,
|
||
narrowJob,
|
||
reducePoll,
|
||
type PollState,
|
||
} from "./job";
|
||
|
||
const POLL_INTERVAL_MS = 1500;
|
||
|
||
export interface UseJobPoll extends PollState {
|
||
// 开始轮询某个 job(替换正在轮询的 job)。
|
||
poll: (jobId: string) => void;
|
||
// 复位到初始(停止当前轮询)。
|
||
reset: () => void;
|
||
}
|
||
|
||
// 轮询 GET /jobs/{job_id}:setTimeout 循环,done/failed 终态停止,卸载清理。
|
||
// 纯状态机在 job.ts(可单测);本 hook 只负责定时器 + fetch 编排。
|
||
export function useJobPoll(): UseJobPoll {
|
||
const [state, dispatch] = useReducer(reducePoll, initialPollState);
|
||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
const activeRef = useRef<string | null>(null);
|
||
|
||
const clearTimer = useCallback((): void => {
|
||
if (timerRef.current !== null) {
|
||
clearTimeout(timerRef.current);
|
||
timerRef.current = null;
|
||
}
|
||
}, []);
|
||
|
||
const tick = useCallback(
|
||
async (jobId: string): Promise<void> => {
|
||
if (activeRef.current !== jobId) return;
|
||
try {
|
||
const { data, error } = await api.GET("/jobs/{job_id}", {
|
||
params: { path: { job_id: jobId } },
|
||
});
|
||
if (activeRef.current !== jobId) return; // 中途切换/卸载。
|
||
if (error || !data) {
|
||
dispatch({ type: "fail", error: "轮询任务状态失败" });
|
||
return;
|
||
}
|
||
const job = narrowJob(data);
|
||
dispatch({ type: "tick", job });
|
||
if (isTerminal(job.status)) {
|
||
activeRef.current = null;
|
||
return;
|
||
}
|
||
timerRef.current = setTimeout(() => void tick(jobId), POLL_INTERVAL_MS);
|
||
} catch (err: unknown) {
|
||
if (activeRef.current !== jobId) return;
|
||
const message = err instanceof Error ? err.message : "未知网络错误";
|
||
dispatch({ type: "fail", error: message });
|
||
}
|
||
},
|
||
[],
|
||
);
|
||
|
||
const poll = useCallback(
|
||
(jobId: string): void => {
|
||
clearTimer();
|
||
activeRef.current = jobId;
|
||
void tick(jobId);
|
||
},
|
||
[clearTimer, tick],
|
||
);
|
||
|
||
const reset = useCallback((): void => {
|
||
clearTimer();
|
||
activeRef.current = null;
|
||
}, [clearTimer]);
|
||
|
||
// 卸载清理。
|
||
useEffect(() => {
|
||
return () => {
|
||
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
||
activeRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
return { ...state, poll, reset };
|
||
}
|