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

@@ -4,15 +4,25 @@ import { useState } from "react";
import { api } from "@/lib/api/client";
import { useToast } from "@/components/Toast";
import { KNOWN_PROVIDERS, TIER_LABELS } from "@/lib/settings/providers";
import { KimiCodeOauth } from "@/components/settings/KimiCodeOauth";
import {
API_KEY_PROVIDERS,
KNOWN_PROVIDERS,
TIER_LABELS,
applyProviderChange,
draftsToRoutingInput,
toRoutingDrafts,
type RoutingDraft,
} from "@/lib/settings/providers";
import type {
CapabilitiesView,
ProvidersResponse,
TierRoutingView,
} from "@/lib/api/types";
interface ProvidersSettingsProps {
initial: ProvidersResponse;
// Kimi Code OAuth 连接态GET .../oauth/status进页 Server Component 取)。
kimiOauth: { connected: boolean; expiresAt: string | null };
}
interface TestResult {
@@ -20,11 +30,17 @@ interface TestResult {
capabilities: CapabilitiesView;
}
// 设置页主体UX §6.10):档位路由(只读展示)+ 凭据行(脱敏 / 添加更新 / 测试连接
export function ProvidersSettings({ initial }: ProvidersSettingsProps) {
// 设置页主体UX §6.10):档位路由(可编辑)+ API-key 凭据行 + Kimi Code OAuth 连接
export function ProvidersSettings({
initial,
kimiOauth,
}: ProvidersSettingsProps) {
const toast = useToast();
const [providers, setProviders] = useState(initial.providers ?? []);
const tierRouting: TierRoutingView[] = initial.tier_routing ?? [];
const [routing, setRouting] = useState<RoutingDraft[]>(
toRoutingDrafts(initial.tier_routing ?? []),
);
const [savingRouting, setSavingRouting] = useState(false);
const [drafts, setDrafts] = useState<Record<string, string>>({});
const [savingId, setSavingId] = useState<string | null>(null);
const [testingId, setTestingId] = useState<string | null>(null);
@@ -33,6 +49,35 @@ export function ProvidersSettings({ initial }: ProvidersSettingsProps) {
const maskedFor = (id: string): string | null =>
providers.find((p) => p.provider === id)?.masked_key ?? null;
const updateRouting = (tier: string, providerId: string): void => {
setRouting((prev) =>
prev.map((d) =>
d.tier === tier ? applyProviderChange(d, providerId) : d,
),
);
};
const updateRoutingModel = (tier: string, model: string): void => {
setRouting((prev) =>
prev.map((d) => (d.tier === tier ? { ...d, model } : d)),
);
};
const saveRouting = async (): Promise<void> => {
setSavingRouting(true);
const { data, error } = await api.PUT("/settings/providers", {
body: { tier_routing: draftsToRoutingInput(routing) },
});
setSavingRouting(false);
if (error || !data) {
toast("保存档位路由失败,请重试", "error");
return;
}
setProviders(data.providers ?? []);
setRouting(toRoutingDrafts(data.tier_routing ?? []));
toast("档位路由已保存", "success");
};
const saveCredential = async (providerId: string): Promise<void> => {
const apiKey = (drafts[providerId] ?? "").trim();
if (!apiKey) {
@@ -74,34 +119,61 @@ export function ProvidersSettings({ initial }: ProvidersSettingsProps) {
<div>
<section className="mb-10">
<h2 className="mb-3 font-serif text-xl text-ink"></h2>
{tierRouting.length === 0 ? (
<p className="rounded border border-dashed border-line bg-panel p-4 text-sm text-ink-soft">
使
</p>
) : (
<ul className="divide-y divide-line rounded border border-line bg-panel">
{tierRouting.map((t) => (
<li
key={t.tier}
className="flex items-center gap-4 px-4 py-3 text-sm"
<ul className="divide-y divide-line rounded border border-line bg-panel">
{routing.map((row) => (
<li
key={row.tier}
className="flex flex-wrap items-center gap-3 px-4 py-3 text-sm"
>
<span className="w-20 text-ink">
{TIER_LABELS[row.tier] ?? row.tier}
</span>
<label className="sr-only" htmlFor={`route-provider-${row.tier}`}>
{TIER_LABELS[row.tier] ?? row.tier}
</label>
<select
id={`route-provider-${row.tier}`}
value={row.provider}
onChange={(e) => updateRouting(row.tier, e.target.value)}
className="rounded border border-line bg-bg px-2 py-1.5 text-sm text-ink focus:border-cinnabar focus:outline-none"
>
<span className="w-20 text-ink">
{TIER_LABELS[t.tier] ?? t.tier}
</span>
<span className="font-mono text-ink-soft">
{t.provider} · {t.model}
</span>
{t.fallback && t.fallback.length > 0 ? (
<span className="ml-auto font-mono text-xs text-ink-soft">
退 {t.fallback.join(", ")}
</span>
) : null}
</li>
))}
</ul>
)}
<option value=""></option>
{KNOWN_PROVIDERS.map((p) => (
<option key={p.id} value={p.id}>
{p.label}
</option>
))}
</select>
<label className="sr-only" htmlFor={`route-model-${row.tier}`}>
{TIER_LABELS[row.tier] ?? row.tier}
</label>
<input
id={`route-model-${row.tier}`}
value={row.model}
onChange={(e) => updateRoutingModel(row.tier, e.target.value)}
placeholder="model"
className="min-w-[10rem] flex-1 rounded border border-line bg-bg px-3 py-1.5 font-mono text-sm text-ink focus:border-cinnabar focus:outline-none"
/>
</li>
))}
</ul>
<div className="mt-3 flex justify-end">
<button
type="button"
onClick={() => void saveRouting()}
disabled={savingRouting}
className="rounded bg-cinnabar px-3 py-1.5 text-sm text-panel disabled:opacity-40"
>
{savingRouting ? "保存中…" : "保存档位路由"}
</button>
</div>
</section>
<KimiCodeOauth
initialConnected={kimiOauth.connected}
initialExpiresAt={kimiOauth.expiresAt}
/>
<section>
<h2 className="mb-3 font-serif text-xl text-ink"></h2>
{providers.length === 0 ? (
@@ -111,7 +183,7 @@ export function ProvidersSettings({ initial }: ProvidersSettingsProps) {
</p>
) : null}
<ul className="divide-y divide-line rounded border border-line bg-panel">
{KNOWN_PROVIDERS.map((prov) => {
{API_KEY_PROVIDERS.map((prov) => {
const masked = maskedFor(prov.id);
const result = results[prov.id];
return (