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>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
// 提供商鉴权方式:api_key(凭据行)或 oauth(device-flow 连接按钮,如 Kimi Code)。
|
||
export type ProviderAuthKind = "api_key" | "oauth";
|
||
|
||
export interface KnownProvider {
|
||
id: string;
|
||
label: string;
|
||
auth: ProviderAuthKind;
|
||
// OAuth 提供商在档位路由里用的默认 model(API-key 提供商由用户填写/后端默认)。
|
||
defaultModel?: string;
|
||
}
|
||
|
||
// 已知提供商(UX §6.10)。后端按 provider 字符串识别;这里只是 UI 候选列表。
|
||
// `kimi-code` 是 OAuth 订阅 plan 提供商(device flow,伪造客户端头,有封号风险),与 API-key
|
||
// 的 `kimi` 分离(K1.4)。`kimi-code-key` 是同一订阅 plan 的**静态 Console Key** 路径(ToS 合规、
|
||
// 无伪造头)——普通 api_key 提供商,固定 model `kimi-for-coding`。
|
||
export const KNOWN_PROVIDERS: KnownProvider[] = [
|
||
{ id: "anthropic", label: "Anthropic", auth: "api_key" },
|
||
{ id: "deepseek", label: "DeepSeek", auth: "api_key" },
|
||
{ id: "kimi", label: "Kimi", auth: "api_key" },
|
||
{ id: "openai", label: "OpenAI", auth: "api_key" },
|
||
{ id: "qwen", label: "通义千问", auth: "api_key" },
|
||
{ id: "glm", label: "智谱 GLM", auth: "api_key" },
|
||
{ id: "gemini", label: "Gemini", auth: "api_key" },
|
||
{
|
||
id: "kimi-code-key",
|
||
label: "Kimi Code(订阅 Key)",
|
||
auth: "api_key",
|
||
defaultModel: "kimi-for-coding",
|
||
},
|
||
{
|
||
id: "kimi-code",
|
||
label: "Kimi Code(OAuth)",
|
||
auth: "oauth",
|
||
defaultModel: "kimi-for-coding",
|
||
},
|
||
];
|
||
|
||
// API-key 凭据行只展示 api_key 提供商;OAuth 提供商有独立连接区。
|
||
export const API_KEY_PROVIDERS: KnownProvider[] = KNOWN_PROVIDERS.filter(
|
||
(p) => p.auth === "api_key",
|
||
);
|
||
|
||
export const TIER_LABELS: Record<string, string> = {
|
||
writer: "写手档",
|
||
analyst: "分析档",
|
||
light: "轻量档",
|
||
};
|
||
|
||
// 档位顺序(路由编辑器列出全部档位,缺省的也能配)。
|
||
export const TIERS = ["writer", "analyst", "light"] as const;
|
||
export type Tier = (typeof TIERS)[number];
|
||
|
||
// 某 provider 在路由里使用的默认 model:OAuth 提供商有固定 model,其余给空串占位。
|
||
export function defaultModelFor(providerId: string): string {
|
||
return (
|
||
KNOWN_PROVIDERS.find((p) => p.id === providerId)?.defaultModel ?? ""
|
||
);
|
||
}
|
||
|
||
// 单条档位路由的可编辑草稿(纯数据)。
|
||
export interface RoutingDraft {
|
||
tier: string;
|
||
provider: string;
|
||
model: string;
|
||
}
|
||
|
||
export interface TierRouting {
|
||
tier: string;
|
||
provider: string;
|
||
model: string;
|
||
fallback?: string[] | null;
|
||
}
|
||
|
||
// 把已存路由(可能缺档位)展开成「全部档位」的可编辑草稿(缺的给空)。
|
||
export function toRoutingDrafts(existing: TierRouting[]): RoutingDraft[] {
|
||
const byTier = new Map(existing.map((r) => [r.tier, r]));
|
||
return TIERS.map((tier) => {
|
||
const row = byTier.get(tier);
|
||
return {
|
||
tier,
|
||
provider: row?.provider ?? "",
|
||
model: row?.model ?? "",
|
||
};
|
||
});
|
||
}
|
||
|
||
// 选择 provider 时自动套用该 provider 的默认 model(OAuth 固定 model;否则保留已填值或清空)。
|
||
export function applyProviderChange(
|
||
draft: RoutingDraft,
|
||
providerId: string,
|
||
): RoutingDraft {
|
||
const def = defaultModelFor(providerId);
|
||
return { ...draft, provider: providerId, model: def !== "" ? def : "" };
|
||
}
|
||
|
||
// 草稿 → PUT body 的 tier_routing:只取选了 provider+model 的行(不可变)。
|
||
export function draftsToRoutingInput(
|
||
drafts: RoutingDraft[],
|
||
): { tier: string; provider: string; model: string }[] {
|
||
return drafts
|
||
.filter((d) => d.provider.trim() !== "" && d.model.trim() !== "")
|
||
.map((d) => ({ tier: d.tier, provider: d.provider, model: d.model }));
|
||
}
|