Files
writer-work-flow/apps/web/lib/settings/providers.ts
Yaojia Wang bf39f50b2f fix(qa): 实施 4 组设计型 QA 项——规则删除/codex 角色/大纲卷过滤/文风回炉锚点
#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。
2026-06-25 12:53:03 +02:00

104 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 提供商鉴权方式api_key凭据行或 oauthdevice-flow 连接按钮,如 Kimi Code
export type ProviderAuthKind = "api_key" | "oauth";
export interface KnownProvider {
id: string;
label: string;
auth: ProviderAuthKind;
// OAuth 提供商在档位路由里用的默认 modelAPI-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 CodeOAuth",
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 在路由里使用的默认 modelOAuth 提供商有固定 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 的默认 modelOAuth 固定 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 }));
}