import { createRoute, type RouteConfig } from "@hono/zod-openapi"; import { providerCatalog, type UpstreamClientOptions, UpstreamError } from "@umt/agent"; import { type Actor, Api, apiResponse, type EndpointDef, type PiModelsProbeRequest, type PiOAuthCompleteRequest, type PiOAuthStartRequest, type PiUpstreamCreate, type PiUpstreamTestRequest, type PiUpstreamUpdate, } from "@umt/contracts"; import type { z } from "zod"; import { requireAdminSession, requireScope } from "../../http/auth.ts"; import type { ModuleApp, ServerContext } from "../types.ts"; import { probeModels, readUpstreamModels, refreshUpstreamModels } from "./models.ts"; import type { OAuthSessions } from "./oauth.ts"; import { requireRow } from "./oauth.ts"; import { refreshUpstream, resolveExpiredDecision } from "./refresh.ts"; import { auditPi, clientFor, createUpstream, deleteUpstream, listUpstreams, updateUpstream } from "./service.ts"; import { getRow, setStatus, toView } from "./store.ts"; type Infer = T extends z.ZodType ? z.infer : Record; interface Input { params: Infer; query: Infer; body: Infer; actor: Actor | undefined; } /** 薄封装:合同 EndpointDef → OpenAPI 路由 + requireScope(+ session)+ 出参按合同 parse */ function route(app: ModuleApp, def: D, handler: (i: Input) => Promise) { const request: RouteConfig["request"] = {}; if (def.params) request.params = def.params as never; if (def.query) request.query = def.query as never; if (def.body) request.body = { content: { "application/json": { schema: def.body as never } }, required: true }; const r = createRoute({ method: def.method, path: def.path, summary: def.summary, tags: ["pi"], middleware: [requireScope(def.scope), ...(def.auth === "session" ? [requireAdminSession()] : [])] as never, request, responses: { 200: { description: def.summary, content: { "application/json": { schema: apiResponse(def.response) } } }, }, }); // biome-ignore lint/suspicious/noExplicitAny: 路由泛型在封装里擦除,入参已由 zod-openapi 校验 app.openapi(r, (async (c: any) => { const input = { params: def.params ? c.req.valid("param") : {}, query: def.query ? c.req.valid("query") : {}, body: def.body ? c.req.valid("json") : {}, actor: c.get("actor"), } as Input; const data = def.response.parse(await handler(input)); return c.json({ ok: true, data }, 200); // biome-ignore lint/suspicious/noExplicitAny: 同上 }) as any); } export function registerPiRoutes( app: ModuleApp, ctx: ServerContext, deps: { oauth: () => OAuthSessions; client?: UpstreamClientOptions }, ) { route(app, Api.pi.providers, async ({ query }) => providerCatalog({ models: query.models !== "false" })); route(app, Api.pi.list, async () => await listUpstreams(ctx)); route(app, Api.pi.get, async ({ params }) => await toView(ctx, await requireRow(ctx, params.id))); route(app, Api.pi.create, async ({ body, actor }) => await createUpstream(ctx, actor, body as PiUpstreamCreate)); route(app, Api.pi.update, async ({ params, body, actor }) => { const row = await requireRow(ctx, params.id); return await updateUpstream(ctx, actor, row, body as PiUpstreamUpdate, () => deps.oauth().cancelUpstream(row.id)); }); route(app, Api.pi.remove, async ({ params, actor }) => { const row = await requireRow(ctx, params.id); deps.oauth().cancelUpstream(row.id); await deleteUpstream(ctx, actor, row); return { ok: true as const }; }); route(app, Api.pi.test, async ({ params, body }) => { const row = await requireRow(ctx, params.id); const req = body as PiUpstreamTestRequest; const mode = req.mode ?? "auth"; const started = Date.now(); let out: { ok: boolean; source: string | null; model: string | null; errorKind: UpstreamError["kind"] | null; message: string | null; }; try { // OAuth 型先走与定时任务同一条刷新路径(失败会标 expired 并建待处理事项) if (row.auth_kind === "oauth" && row.secret_ref) { const r = await refreshUpstream(ctx, row, { windowMs: 0, ...(deps.client ? { client: deps.client } : {}) }); if (!r.ok) throw new UpstreamError(r.kind as UpstreamError["kind"], r.message); } const t = await clientFor(ctx, row, deps.client).test({ mode, ...(req.model ? { model: req.model } : {}) }); // 自定义 Base URL 的凭证检查不连端点:不据此改写可用状态,提示用最小补全确认可达(QA U2) const unprobed = mode === "auth" && !!row.base_url && row.auth_kind !== "oauth"; const notes = [ t.inCatalog ? null : `模型 ${t.model} 不在内置模型目录中,按手填 id 调用`, unprobed ? "只核对了凭证与模型,没有连接 Base URL;用「最小补全」确认端点可达" : null, ].filter(Boolean); out = { ok: true, source: t.source, model: t.model, errorKind: null, message: notes.join(";") || null }; if (!unprobed) await setStatus(ctx, row.id, { status: "ok", verified: true }); } catch (err) { const e = err instanceof UpstreamError ? err : new UpstreamError("other", (err as Error).message); out = { ok: false, source: null, model: req.model ?? row.default_model, errorKind: e.kind, message: e.message }; const fresh = await getRow(ctx, row.id); if (fresh && fresh.status !== "expired") await setStatus(ctx, row.id, { status: e.kind === "auth" ? "auth_required" : "error", lastError: e.message, }); } return { ...out, mode, latencyMs: Date.now() - started, upstream: await toView(ctx, (await getRow(ctx, row.id))!) }; }); route(app, Api.pi.models, async ({ params }) => await readUpstreamModels(ctx, await requireRow(ctx, params.id))); route(app, Api.pi.modelsRefresh, async ({ params, actor }) => { const row = await requireRow(ctx, params.id); const r = await refreshUpstreamModels(ctx, row); await auditPi(ctx, actor, "pi_upstream.models_refresh", row.id, { source: r.source, count: r.models.length, errorKind: r.errorKind, }); return r; }); route(app, Api.pi.modelsProbe, async ({ body }) => await probeModels(body as PiModelsProbeRequest)); route(app, Api.pi.oauthStart, async ({ params, body, actor }) => { const row = await requireRow(ctx, params.id); const r = await deps.oauth().start(row, body as PiOAuthStartRequest); await auditPi(ctx, actor, "pi_upstream.oauth_start", row.id, { mode: r.mode, sessionId: r.sessionId }); return r; }); route(app, Api.pi.oauthComplete, async ({ params, body, actor }) => { const row = await requireRow(ctx, params.id); const req = body as PiOAuthCompleteRequest; const r = await deps.oauth().complete(row.id, req); if (r.state === "succeeded") { await resolveExpiredDecision(ctx, row.id); await auditPi(ctx, actor, "pi_upstream.oauth_login", row.id, { sessionId: r.sessionId }); } return { ...r, upstream: await toView(ctx, (await getRow(ctx, row.id))!) }; }); route(app, Api.pi.oauthLogout, async ({ params, actor }) => { const row = await requireRow(ctx, params.id); deps.oauth().cancelUpstream(row.id); await clientFor(ctx, row, deps.client).logout(); await auditPi(ctx, actor, "pi_upstream.oauth_logout", row.id); return await toView(ctx, (await getRow(ctx, row.id))!); }); }