fix: 支持动态 provider profile slug

This commit is contained in:
Codex
2026-06-08 04:19:58 +08:00
parent bda8e3bb1e
commit 509c2aa6fd
6 changed files with 181 additions and 47 deletions
+72 -14
View File
@@ -1,7 +1,7 @@
import { createHash, randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { AgentRunError } from "../common/errors.js";
import { backendProfileSpec, backendProfileSpecs } from "../common/backend-profiles.js";
import { backendProfileSpec, backendProfileSpecs, isBackendProfileSlug } from "../common/backend-profiles.js";
import type { AgentRunStore } from "./store.js";
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue } from "../common/types.js";
import { asRecord, validateBackendProfile } from "../common/validation.js";
@@ -13,6 +13,7 @@ import { runnerJobStatusSummary } from "./runner-job-status.js";
const defaultNamespace = "agentrun-v01";
const credentialAnnotationPrefix = "agentrun.pikastech.local/provider-profile";
const kubectlLastAppliedAnnotation = "kubectl.kubernetes.io/last-applied-configuration";
const providerSecretNamePrefix = "agentrun-v01-provider-";
export interface ProviderProfileOptions {
namespace?: string;
@@ -40,7 +41,8 @@ interface RenderedConfig {
}
export async function listProviderProfiles(options: ProviderProfileOptions = {}): Promise<JsonRecord> {
const items = await Promise.all(backendProfileSpecs.map((spec) => providerProfileStatus(spec.profile, options)));
const profiles = await listProviderProfileIds(options);
const items = await Promise.all(profiles.map((profile) => providerProfileStatus(profile, options)));
return { items, count: items.length, valuesPrinted: false };
}
@@ -81,10 +83,8 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
const delegatedBy = delegatedBySummary(record.delegatedBy);
const namespace = profileNamespace(options);
const existingSecret = await kubectlGetSecret(spec.defaultSecretName, namespace, options.kubectlCommand ?? "kubectl");
if (!existingSecret) throw new AgentRunError("secret-unavailable", `provider profile ${profile} Secret is not configured`, { httpStatus: 404, details: { profile, secretRef: secretRefSummary(profile, namespace) } });
const existingData = asOptionalRecord(existingSecret.data);
const existingData = asOptionalRecord(existingSecret?.data);
const authJsonData = dataKey(existingData, "auth.json");
if (!authJsonData) throw new AgentRunError("secret-unavailable", `provider profile ${profile} auth.json is not configured`, { httpStatus: 400, details: { profile, secretRef: secretRefSummary(profile, namespace), key: "auth.json" } });
const credentialHashSuffix = hashDataKey(existingData, "auth.json");
const secretManifest: JsonRecord = {
apiVersion: "v1",
@@ -105,30 +105,28 @@ export async function setProviderProfileConfig(profileValue: string, body: unkno
},
},
type: "Opaque",
data: {
"auth.json": authJsonData,
"config.toml": base64Data(configToml),
},
data: providerProfileSecretData({ authJsonData, configToml }),
};
const applied = await kubectlUpsertSecret(secretManifest, options.kubectlCommand ?? "kubectl");
return {
action: "provider-profile-config-updated",
mutation: true,
profile,
configured: true,
configured: Boolean(authJsonData),
secretRef: secretRefSummary(profile, namespace),
resourceVersion: objectPath(applied, ["metadata", "resourceVersion"]),
credentialHashSuffix,
credentialHashSuffix: credentialHashSuffix ?? null,
configHashSuffix: shortHash(configToml),
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
delegatedBy,
requiresExternalBridgeUpdate: profile === "deepseek" || profile === "dsflash-go",
requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, configToml),
configTomlPrinted: false,
credentialValuesPrinted: false,
valuesPrinted: false,
pollCommands: {
config: `./scripts/agentrun provider-profiles config ${profile}`,
show: `./scripts/agentrun provider-profiles show ${profile}`,
setKey: `./scripts/agentrun provider-profiles set-key ${profile} --key-stdin`,
validate: `./scripts/agentrun provider-profiles validate ${profile} --wait --timeout-ms 120000`,
},
};
@@ -180,7 +178,7 @@ export async function setProviderProfileCredential(profileValue: string, body: u
updatedAt: objectPath(applied, ["metadata", "annotations", `${credentialAnnotationPrefix}-updated-at`]) ?? new Date().toISOString(),
configSummary: rendered.config.configSummary,
delegatedBy,
requiresExternalBridgeUpdate: profile === "deepseek" || profile === "dsflash-go",
requiresExternalBridgeUpdate: profileUsesMoonBridge(profile, rendered.config.configToml),
valuesPrinted: false,
pollCommands: {
show: `./scripts/agentrun provider-profiles show ${profile}`,
@@ -284,6 +282,46 @@ async function providerProfileStatus(profile: BackendProfile, options: ProviderP
};
}
async function listProviderProfileIds(options: ProviderProfileOptions): Promise<BackendProfile[]> {
const profiles = new Set<BackendProfile>(backendProfileSpecs.map((spec) => spec.profile));
const namespace = profileNamespace(options);
const secrets = await kubectlListSecrets(namespace, options.kubectlCommand ?? "kubectl");
for (const item of secrets) {
const secretProfile = providerProfileFromSecret(item);
if (secretProfile) profiles.add(secretProfile);
}
return [...profiles].sort(compareProviderProfiles);
}
function compareProviderProfiles(left: string, right: string): number {
const leftIndex = backendProfileSpecs.findIndex((item) => item.profile === left);
const rightIndex = backendProfileSpecs.findIndex((item) => item.profile === right);
if (leftIndex >= 0 || rightIndex >= 0) {
if (leftIndex < 0) return 1;
if (rightIndex < 0) return -1;
if (leftIndex !== rightIndex) return leftIndex - rightIndex;
}
return left.localeCompare(right);
}
function providerProfileFromSecret(secret: JsonRecord): BackendProfile | null {
const metadata = asOptionalRecord(secret.metadata);
const labels = asOptionalRecord(metadata?.labels);
const annotatedProfile = stringPath(asOptionalRecord(metadata?.annotations), [`${credentialAnnotationPrefix}-profile`]);
const labeledProfile = stringPath(labels, ["agentrun.pikastech.local/profile"]);
const namedProfile = profileFromSecretName(stringPath(metadata, ["name"]));
for (const candidate of [annotatedProfile, labeledProfile, namedProfile]) {
if (candidate && isBackendProfileSlug(candidate)) return candidate;
}
return null;
}
function profileFromSecretName(name: string | null): string | null {
if (!name || !name.startsWith(providerSecretNamePrefix)) return null;
const profile = name.slice(providerSecretNamePrefix.length);
return profile.length > 0 ? profile : null;
}
function renderCredential(apiKey: string, config: RenderedConfig): { authJson: string; config: RenderedConfig } {
const authJson = `${JSON.stringify(authPayload(apiKey))}\n`;
return {
@@ -292,6 +330,12 @@ function renderCredential(apiKey: string, config: RenderedConfig): { authJson: s
};
}
function providerProfileSecretData(input: { authJsonData?: string | null; configToml: string }): JsonRecord {
const data: JsonRecord = { "config.toml": base64Data(input.configToml) };
if (input.authJsonData) data["auth.json"] = input.authJsonData;
return data;
}
function authPayload(apiKey: string): JsonRecord {
return { OPENAI_API_KEY: apiKey };
}
@@ -445,6 +489,10 @@ function defaultConfig(profile: BackendProfile): ProfileConfig {
};
}
function profileUsesMoonBridge(profile: BackendProfile, configToml: string): boolean {
return profile === "deepseek" || profile === "dsflash-go" || configToml.includes("hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local");
}
function validateBaseUrl(profile: BackendProfile, value: string): void {
let url: URL;
try {
@@ -525,7 +573,7 @@ function secretRefSummary(profile: BackendProfile, namespace: string): JsonRecor
function requiredSpec(profile: BackendProfile) {
const spec = backendProfileSpec(profile);
if (!spec) throw new AgentRunError("schema-invalid", `backendProfile ${profile} is not supported in v0.1`, { httpStatus: 400 });
if (!spec) throw new AgentRunError("schema-invalid", `backendProfile ${profile} must be a lowercase slug`, { httpStatus: 400 });
return spec;
}
@@ -543,6 +591,16 @@ async function kubectlGetSecret(name: string, namespace: string, kubectlCommand:
return parseKubectlObject(result.stdout, "secret");
}
async function kubectlListSecrets(namespace: string, kubectlCommand: string): Promise<JsonRecord[]> {
const result = await runKubectl(kubectlCommand, ["get", "secrets", "-n", namespace, "-o", "json"]);
if (result.code !== 0) {
throw new AgentRunError("infra-failed", `kubectl get secrets ${namespace} failed with code ${result.code}`, { httpStatus: 502, details: redactJson({ stderr: redactText(result.stderr.slice(-2000)), stdout: redactText(result.stdout.slice(-1000)) }) });
}
const parsed = parseKubectlObject(result.stdout, "secret-list");
const items = Array.isArray(parsed.items) ? parsed.items : [];
return items.filter((item): item is JsonRecord => typeof item === "object" && item !== null && !Array.isArray(item));
}
async function kubectlUpsertSecret(manifest: JsonRecord, kubectlCommand: string): Promise<JsonRecord> {
const name = stringPath(manifest, ["metadata", "name"]) ?? "<unknown>";
const namespace = stringPath(manifest, ["metadata", "namespace"]) ?? defaultNamespace;