import { type Kysely, sql } from "kysely"; import { entityTable, execDdl, oneOf } from "../src/migration-helpers.ts"; // W7-A Pi 上游:pi_upstream(同一 provider 可多个账号并存),凭据(API Key 或 OAuth token 包 JSON)在 secret 表, // 这里只存 secret_ref。agent_run.upstream_id 记录本次运行用的上游(runAgent 可按 upstreamId 覆盖默认上游)。 // 一次性迁移:旧 settings.pi(v0.1 单上游字段)若已保存过,转成一条上游并设为默认;旧 apiKeySecretRef 的所有权 // 移交给新上游(settings 里清空,避免旧设置页清除密钥时误删上游凭据)。 const AUTH_KINDS = ["api_key", "oauth", "none"] as const; const STATUSES = ["ok", "auth_required", "expired", "error"] as const; const THINKING = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const; // biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型 export async function up(db: Kysely): Promise { await execDdl( db, ` ${entityTable( "pi_upstream", ` name text not null, provider text not null, auth_kind text not null ${oneOf("auth_kind", AUTH_KINDS)}, secret_ref uuid references secret (id) on delete set null, base_url text, default_model text, thinking_level text not null default 'medium' ${oneOf("thinking_level", THINKING)}, extra jsonb not null default '{}'::jsonb, enabled boolean not null default true, status text not null default 'auth_required' ${oneOf("status", STATUSES)}, last_error text, last_verified_at timestamptz, oauth_expires_at timestamptz, constraint pi_upstream_name_key unique (name)`, )} create index pi_upstream_oauth_expiry_idx on pi_upstream (oauth_expires_at) where auth_kind = 'oauth' and secret_ref is not null; alter table agent_run add column upstream_id uuid references pi_upstream (id) on delete set null; `, ); // 旧 settings.pi → 一条上游 const row = await sql<{ value: Record }>`select value from settings where key = 'pi'`.execute(db); const pi = row.rows[0]?.value; if (!pi || typeof pi !== "object" || typeof pi.provider !== "string" || pi.defaultUpstreamId) return; const legacy = pi.provider; const provider = legacy === "openai-compatible" || legacy === "fake" || legacy === "anthropic" ? legacy : null; if (!provider) return; const secretRef = typeof pi.apiKeySecretRef === "string" ? pi.apiKeySecretRef : null; const authKind = provider === "fake" ? "none" : "api_key"; const status = provider === "fake" || secretRef ? "ok" : "auth_required"; const thinking = THINKING.includes(pi.thinkingLevel as never) ? (pi.thinkingLevel as string) : "medium"; const inserted = await sql<{ id: string }>` insert into pi_upstream (name, provider, auth_kind, secret_ref, base_url, default_model, thinking_level, status) values (${`默认上游(${provider})`}, ${provider}, ${authKind}, (select id from secret where id = ${secretRef}::uuid), ${typeof pi.baseUrl === "string" ? pi.baseUrl : null}, ${typeof pi.model === "string" ? pi.model : provider === "fake" ? "fake-model" : null}, ${thinking}, ${status}) returning id`.execute(db); const id = inserted.rows[0]!.id; await sql`update settings set value = value || ${JSON.stringify({ defaultUpstreamId: id, apiKeySecretRef: null })}::jsonb, updated_at = now() where key = 'pi'`.execute(db); } // biome-ignore lint/suspicious/noExplicitAny: 同上 export async function down(db: Kysely): Promise { // 默认上游的密钥交还给旧字段,再删表 await sql`update settings s set value = (s.value - 'defaultUpstreamId') || jsonb_build_object('apiKeySecretRef', u.secret_ref) from pi_upstream u where s.key = 'pi' and u.id::text = s.value->>'defaultUpstreamId' and u.auth_kind = 'api_key'`.execute(db); await sql`update settings set value = value - 'defaultUpstreamId' where key = 'pi'`.execute(db); await execDdl( db, "alter table agent_run drop column if exists upstream_id;\ndrop table if exists pi_upstream cascade;", ); }