feat(contracts,db): Pi upstream contracts and pi_upstream table
- contracts/pi.ts: 41 builtin provider ids + openai-compatible + fake, auth kinds, upstream row/view/create/update, provider capability table (oauth modes, termsNote), test/models/oauth start/complete schemas - settings.pi gains defaultUpstreamId; legacy single-upstream fields kept for fallback - api.ts: /pi/providers, /pi/upstreams CRUD, test, models, oauth start/complete/logout (writes and login are console-session only) - migration 0027: pi_upstream, agent_run.upstream_id, one-time conversion of saved legacy settings.pi into a default upstream Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
39a001c099
commit
b9dbd54e37
8 files changed
+665
-4
No files matched your search
@@ -0,0 +1,76 @@
|
||||
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<any>): Promise<void> {
|
||||
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<string, unknown> }>`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<any>): Promise<void> {
|
||||
// 默认上游的密钥交还给旧字段,再删表
|
||||
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;",
|
||||
);
|
||||
}
|
||||
@@ -182,6 +182,9 @@ export type PathBindingBindingKind = "entity" | "season";
|
||||
export type PathBindingStatus = "active" | "conflict" | "superseded";
|
||||
export type PathMappingViewKind = "downloader" | "media_server";
|
||||
export type PathMappingVerifyStatus = "unverified" | "ok" | "failed";
|
||||
export type PiUpstreamAuthKind = "api_key" | "oauth" | "none";
|
||||
export type PiUpstreamThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
||||
export type PiUpstreamStatus = "ok" | "auth_required" | "expired" | "error";
|
||||
export type PolicyRevisionKind = "acquisition" | "preference" | "retention" | "cleanup" | "instance" | "automation";
|
||||
export type PolicyRevisionStatus = "draft" | "published" | "superseded" | "rolled_back" | "archived";
|
||||
export type PreferenceTagKind = "soft" | "hard" | "version" | "target";
|
||||
@@ -340,6 +343,7 @@ export interface AgentRunTable {
|
||||
export interface AgentRunEventTable {
|
||||
run_id: string;
|
||||
seq: number;
|
||||
upstream_id: string | null;
|
||||
type: AgentRunEventType;
|
||||
at: GeneratedTimestamptz;
|
||||
payload: Generated<Json>;
|
||||
@@ -996,6 +1000,25 @@ export interface PolicyRevisionTable {
|
||||
policy_key: string;
|
||||
revision: number;
|
||||
kind: PolicyRevisionKind;
|
||||
export interface PiUpstreamTable {
|
||||
id: Generated<string>;
|
||||
name: string;
|
||||
provider: string;
|
||||
auth_kind: PiUpstreamAuthKind;
|
||||
secret_ref: string | null;
|
||||
base_url: string | null;
|
||||
default_model: string | null;
|
||||
thinking_level: Generated<PiUpstreamThinkingLevel>;
|
||||
extra: Generated<Json>;
|
||||
enabled: Generated<boolean>;
|
||||
status: Generated<PiUpstreamStatus>;
|
||||
last_error: string | null;
|
||||
last_verified_at: Timestamptz | null;
|
||||
oauth_expires_at: Timestamptz | null;
|
||||
created_at: GeneratedTimestamptz;
|
||||
updated_at: GeneratedTimestamptz;
|
||||
}
|
||||
|
||||
status: Generated<PolicyRevisionStatus>;
|
||||
title: string;
|
||||
summary: string | null;
|
||||
@@ -1446,6 +1469,7 @@ export type AgentRunMessageRow = Selectable<AgentRunMessageTable>;
|
||||
export type NewAgentRunMessage = Insertable<AgentRunMessageTable>;
|
||||
export type AgentRunMessageUpdate = Updateable<AgentRunMessageTable>;
|
||||
export type ApiKeyRow = Selectable<ApiKeyTable>;
|
||||
pi_upstream: PiUpstreamTable;
|
||||
export type NewApiKey = Insertable<ApiKeyTable>;
|
||||
export type ApiKeyUpdate = Updateable<ApiKeyTable>;
|
||||
export type ApprovalRow = Selectable<ApprovalTable>;
|
||||
@@ -1599,6 +1623,9 @@ export type SubscriptionRow = Selectable<SubscriptionTable>;
|
||||
export type NewSubscription = Insertable<SubscriptionTable>;
|
||||
export type SubscriptionUpdate = Updateable<SubscriptionTable>;
|
||||
export type SubscriptionHistoryRow = Selectable<SubscriptionHistoryTable>;
|
||||
export type PiUpstreamRow = Selectable<PiUpstreamTable>;
|
||||
export type NewPiUpstream = Insertable<PiUpstreamTable>;
|
||||
export type PiUpstreamUpdate = Updateable<PiUpstreamTable>;
|
||||
export type NewSubscriptionHistory = Insertable<SubscriptionHistoryTable>;
|
||||
export type SubscriptionHistoryUpdate = Updateable<SubscriptionHistoryTable>;
|
||||
export type TaskRow = Selectable<TaskTable>;
|
||||
|
||||
Reference in new issue
Block a user