#5 规则 DELETE + id:暴露 RuleView.id(PK);新增 DELETE /projects/{id}/rules/{rule_id} (项目/规则不存在→404,成功→204,按 (id,project_id) 限定);rule_repo 加 list_for_project/delete;RulesPage 每条加删除(乐观删+回滚+toast)。assemble 侧 RuleView(缓存前缀)不动,列表另立 RuleListItemView。 #7 codex 角色 relations:写侧本已持久化、读端点 _existing_characters 硬编码 []。 加 _relations_from_jsonb 解析 {name,kind,note},CodexPage 渲染关系 chip。 #8 角色入库幂等:SqlCharacterWriteRepo.create 改 (project_id,name) app 层 upsert—— 重复入库改更新而非插入;不加 UNIQUE/迁移(线上已有重复行会让约束迁移失败)。 #1 大纲卷过滤:GET /outline 支持可选 ?volume(无参=全部,向后兼容);OutlineEditor 加「查看:全部/卷N」筛选,与生成目标卷解耦。 H3/#9 文风回炉锚点:StyleDriftSegment 加 text(逐字命中段),style.md 指示审稿输出; 前端按内容锚点定位回炉目标(idx 仅排序),命中失败 → 提示「无法定位该段」而非 静默 no-op。style golden fixture 已重生成。 契约变更已 pnpm gen:api(RuleView.id / DELETE rules / outline ?volume)。无迁移 (alembic 无漂移)。门禁绿:ruff/mypy(210)/alembic/pytest 760 · 前端 tsc/lint/vitest 329。
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: Tier;
|
||
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: Tier; 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 }));
|
||
}
|