feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复

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>
This commit is contained in:
Yaojia Wang
2026-06-20 10:39:58 +02:00
parent 5fb7bfb1de
commit 765dbdfbd4
161 changed files with 17330 additions and 208 deletions

View File

@@ -0,0 +1,123 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import type { StyleFingerprintResponse } from "@/lib/api/types";
import { useJobPoll } from "@/lib/jobs/useJobPoll";
import { styleLearnResult } from "@/lib/jobs/job";
import {
buildLearnRequest,
normalizeFingerprint,
type Fingerprint,
type StyleLearnMode,
} from "./style";
export interface UseStyleLearn {
// 提交中POST /style 受理)或轮询中。
busy: boolean;
pollStatus: ReturnType<typeof useJobPoll>["status"] | "idle";
progress: number;
fingerprint: Fingerprint | null;
// 学文风POST /style → 拿 job_id → 轮询 → done 后拉最新指纹。
learn: (
projectId: string,
samples: string[],
mode: StyleLearnMode,
) => Promise<boolean>;
}
function errorCode(error: unknown): string | undefined {
if (typeof error !== "object" || error === null) return undefined;
const env = error as { error?: { code?: unknown } };
return typeof env.error?.code === "string" ? env.error.code : undefined;
}
// 学文风编排受理202 job_id→ useJobPoll 轮询 → done 拉 GET /style 展示指纹。
export function useStyleLearn(initial: Fingerprint | null): UseStyleLearn {
const [fingerprint, setFingerprint] = useState<Fingerprint | null>(initial);
const [submitting, setSubmitting] = useState(false);
const [pollStatus, setPollStatus] = useState<
ReturnType<typeof useJobPoll>["status"] | "idle"
>("idle");
const [projectId, setProjectId] = useState<string | null>(null);
const poll = useJobPoll();
const toast = useToast();
// 仅在提交过一次学文风后才反映轮询状态initialPollState.status 默认 "polling")。
const startedRef = useRef(false);
// 轮询完成 → 拉最新指纹;失败 → toast。
useEffect(() => {
if (!startedRef.current) return;
setPollStatus(poll.status);
if (poll.status === "done" && projectId) {
void refetchFingerprint(projectId).then((fp) => {
if (fp) setFingerprint(fp);
toast("文风指纹已更新。", "success");
});
}
if (poll.status === "error") {
toast(`学文风失败:${poll.error ?? "未知原因"}`, "error");
}
// 仅在 poll.status 变化时反应。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [poll.status]);
const learn = useCallback<UseStyleLearn["learn"]>(
async (pid, samples, mode) => {
setSubmitting(true);
setProjectId(pid);
try {
const { data, error } = await api.POST("/projects/{project_id}/style", {
params: { path: { project_id: pid } },
body: buildLearnRequest(samples, mode),
});
if (error || !data) {
const code = errorCode(error);
toast(
code === "LLM_UNAVAILABLE"
? "未配置提供商,请先去设置页连一家。"
: "学文风受理失败,请稍后重试。",
"error",
);
return false;
}
startedRef.current = true;
poll.poll(data.job_id);
setPollStatus("polling");
return true;
} finally {
setSubmitting(false);
}
},
[poll, toast],
);
return {
busy: submitting || pollStatus === "polling",
pollStatus,
progress: poll.progress,
fingerprint,
learn,
};
}
// 客户端拉最新指纹done 后刷新展示404/失败 → null。
async function refetchFingerprint(
projectId: string,
): Promise<Fingerprint | null> {
const { data, error } = await api.GET("/projects/{project_id}/style", {
params: { path: { project_id: projectId } },
});
if (error || !data) return null;
return normalizeFingerprint(data as StyleFingerprintResponse);
}
// 仅供测试/复用:保证 job done result 的版本回显(不阻断 UI
export function learnSummary(
poll: ReturnType<typeof useJobPoll>,
): { version: number | null; dimsCount: number | null } | null {
if (poll.status !== "done" || !poll.job) return null;
return styleLearnResult(poll.job);
}