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:
110
apps/web/components/style/StyleUpload.tsx
Normal file
110
apps/web/components/style/StyleUpload.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState, type ChangeEvent } from "react";
|
||||
|
||||
import { hasUsableSamples, type StyleLearnMode } from "@/lib/style/style";
|
||||
import { useToast } from "@/components/Toast";
|
||||
|
||||
interface StyleUploadProps {
|
||||
busy: boolean;
|
||||
pollStatus: "idle" | "polling" | "done" | "error";
|
||||
progress: number;
|
||||
// 是否已有指纹(决定默认 mode = update)。
|
||||
hasFingerprint: boolean;
|
||||
onLearn: (samples: string[], mode: StyleLearnMode) => void;
|
||||
}
|
||||
|
||||
// 学文风输入(UX §6.9):多段文本粘贴 + 文件选择(在浏览器读成文本)。
|
||||
// 样本以正文文本入 body(无对象存储,确认决策)。
|
||||
export function StyleUpload({
|
||||
busy,
|
||||
pollStatus,
|
||||
progress,
|
||||
hasFingerprint,
|
||||
onLearn,
|
||||
}: StyleUploadProps) {
|
||||
const [text, setText] = useState("");
|
||||
const toast = useToast();
|
||||
|
||||
// 读取选中的文本文件,追加到文本框(多文件用空行分隔)。
|
||||
const onFiles = useCallback(
|
||||
async (e: ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
const files = Array.from(e.target.files ?? []);
|
||||
if (files.length === 0) return;
|
||||
try {
|
||||
const contents = await Promise.all(files.map((f) => f.text()));
|
||||
setText((prev) => {
|
||||
const joined = contents.join("\n\n");
|
||||
return prev.trim().length > 0 ? `${prev}\n\n${joined}` : joined;
|
||||
});
|
||||
} catch {
|
||||
toast("读取文件失败,请改用粘贴。", "error");
|
||||
} finally {
|
||||
e.target.value = ""; // 允许重复选同一文件。
|
||||
}
|
||||
},
|
||||
[toast],
|
||||
);
|
||||
|
||||
const submit = (): void => {
|
||||
// 以空行切分成多段样本(对齐后端 samples:list[str])。
|
||||
const samples = text
|
||||
.split(/\n{2,}/)
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
if (!hasUsableSamples(samples)) {
|
||||
toast("请粘贴或选择至少一段样本正文。", "error");
|
||||
return;
|
||||
}
|
||||
onLearn(samples, hasFingerprint ? "update" : "create");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="style-samples"
|
||||
className="mb-1 block text-sm font-semibold text-ink"
|
||||
>
|
||||
样本正文(空行分段;可粘贴多段)
|
||||
</label>
|
||||
<textarea
|
||||
id="style-samples"
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
placeholder="粘贴你想学习文风的章节正文…"
|
||||
className="min-h-[30vh] w-full resize-y rounded border border-line bg-panel p-3 font-serif text-[15px] leading-[1.9] text-ink focus:border-cinnabar focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="cursor-pointer rounded border border-line px-3 py-1.5 text-sm text-ink hover:border-cinnabar hover:text-cinnabar">
|
||||
选择文本文件
|
||||
<input
|
||||
type="file"
|
||||
accept=".txt,.md,text/plain,text/markdown"
|
||||
multiple
|
||||
onChange={(e) => void onFiles(e)}
|
||||
className="sr-only"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={busy}
|
||||
className="rounded bg-cinnabar px-4 py-1.5 text-sm text-panel hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{hasFingerprint ? "重新学习文风" : "学习文风"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{pollStatus === "polling" ? (
|
||||
<p className="text-xs text-info" aria-live="polite">
|
||||
提取文风指纹中…({progress}%)
|
||||
</p>
|
||||
) : pollStatus === "error" ? (
|
||||
<p className="text-xs text-conflict">学文风失败,请稍后重试。</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user