Allow dynamic AgentRun provider profiles

This commit is contained in:
Codex Agent
2026-06-08 03:22:26 +08:00
parent 97998ffec7
commit 34788702dc
15 changed files with 154 additions and 83 deletions
+7 -1
View File
@@ -127,7 +127,13 @@ export function App(): ReactElement {
void fetchText("/help.md", { timeoutMs: 8000 }).then((response) => setHelp(response.ok ? response.data ?? "" : `帮助内容加载失败:${response.error ?? "unknown"}`));
}, [route]);
const modelLabel = useMemo(() => store.state.providerProfile === "codex-api" ? "Codex API" : store.state.providerProfile === "minimax-m3" ? "MiniMax-M3" : "DeepSeek", [store.state.providerProfile]);
const modelLabel = useMemo(() => {
if (store.state.providerProfile === "codex-api") return "Codex API";
if (store.state.providerProfile === "minimax-m3") return "MiniMax-M3";
if (store.state.providerProfile === "dsflash-go") return "DeepSeek V4 Flash";
if (store.state.providerProfile === "deepseek") return "DeepSeek";
return store.state.providerProfile;
}, [store.state.providerProfile]);
const sessionSidebarVisible = workspaceRoute && !leftCollapsed;
const hwpodSidebarVisible = workspaceRoute && !rightCollapsed;
const shellStyle = useMemo(() => ({
@@ -64,6 +64,7 @@ export function CommandBar({ pickedDraft, onPickedDraftConsumed, disabledReason,
<span></span>
<select id="code-agent-provider-profile" aria-label="Code Agent 模型通道" value={props.providerProfile} disabled={interactionDisabled} onChange={(event) => props.onProviderProfile(event.currentTarget.value as ProviderProfile)}>
<option value="deepseek">DeepSeek</option>
<option value="dsflash-go">DeepSeek V4 Flash</option>
<option value="codex-api">Codex API</option>
<option value="minimax-m3">MiniMax-M3</option>
</select>
@@ -21,22 +21,27 @@ interface ManagementState {
validatingProfile: ProviderProfile | null;
configDialog: ConfigDialogState | null;
profiles: ProviderProfileStatusItem[];
inputs: Record<ProviderProfile, string>;
validations: Partial<Record<ProviderProfile, ProviderProfileValidation>>;
inputs: Record<string, string>;
validations: Partial<Record<string, ProviderProfileValidation>>;
error: string | null;
notice: string | null;
}
const profileOrder: ProviderProfile[] = ["deepseek", "codex-api", "minimax-m3"];
const profileLabels: Record<ProviderProfile, string> = {
const profileOrder: ProviderProfile[] = ["deepseek", "dsflash-go", "codex-api", "minimax-m3"];
const profileLabels: Record<string, string> = {
deepseek: "DeepSeek",
"dsflash-go": "DeepSeek V4 Flash",
"codex-api": "Codex API",
"minimax-m3": "MiniMax-M3"
};
const initialInputs: Record<ProviderProfile, string> = { deepseek: "", "codex-api": "", "minimax-m3": "" };
const initialInputs: Record<string, string> = { deepseek: "", "dsflash-go": "", "codex-api": "", "minimax-m3": "" };
const initialState: ManagementState = { loading: false, savingProfile: null, validatingProfile: null, configDialog: null, profiles: [], inputs: initialInputs, validations: {}, error: null, notice: null };
function profileLabel(profile: ProviderProfile): string {
return profileLabels[profile] ?? profile;
}
export function ManagementView(): ReactElement {
const [state, setState] = useState<ManagementState>(initialState);
@@ -56,7 +61,7 @@ export function ManagementView(): ReactElement {
setState((current) => ({ ...current, savingProfile: profile, error: null, notice: null }));
const response = await api.setProviderProfileKey(profile, apiKey);
if (!response.ok) {
setState((current) => ({ ...current, savingProfile: null, error: response.error ?? `${profileLabels[profile]} 保存失败` }));
setState((current) => ({ ...current, savingProfile: null, error: response.error ?? `${profileLabel(profile)} 保存失败` }));
return;
}
const updated = profileItem(response.data);
@@ -65,7 +70,7 @@ export function ManagementView(): ReactElement {
savingProfile: null,
inputs: { ...current.inputs, [profile]: "" },
profiles: updated ? mergeProfile(current.profiles, updated) : current.profiles,
notice: `${profileLabels[profile]} 已保存`,
notice: `${profileLabel(profile)} 已保存`,
error: null
}));
if (!updated) void load();
@@ -76,7 +81,7 @@ export function ManagementView(): ReactElement {
setState((current) => ({ ...current, configDialog: { profile, configToml: "", loading: true, saving: false, error: null }, error: null, notice: null }));
const response = await api.providerProfileConfig(profile);
if (!response.ok) {
setState((current) => ({ ...current, configDialog: current.configDialog?.profile === profile ? { ...current.configDialog, loading: false, error: response.error ?? `${profileLabels[profile]} config.toml 加载失败` } : current.configDialog }));
setState((current) => ({ ...current, configDialog: current.configDialog?.profile === profile ? { ...current.configDialog, loading: false, error: response.error ?? `${profileLabel(profile)} config.toml 加载失败` } : current.configDialog }));
return;
}
const config = configPayload(response.data, profile);
@@ -89,7 +94,7 @@ export function ManagementView(): ReactElement {
setState((current) => ({ ...current, configDialog: current.configDialog ? { ...current.configDialog, saving: true, error: null } : null, error: null, notice: null }));
const response = await api.setProviderProfileConfig(dialog.profile, dialog.configToml);
if (!response.ok) {
setState((current) => ({ ...current, configDialog: current.configDialog ? { ...current.configDialog, saving: false, error: response.error ?? `${profileLabels[dialog.profile]} config.toml 保存失败` } : null }));
setState((current) => ({ ...current, configDialog: current.configDialog ? { ...current.configDialog, saving: false, error: response.error ?? `${profileLabel(dialog.profile)} config.toml 保存失败` } : null }));
return;
}
const updated = profileItem(response.data);
@@ -97,7 +102,7 @@ export function ManagementView(): ReactElement {
...current,
configDialog: null,
profiles: updated ? mergeProfile(current.profiles, updated) : current.profiles,
notice: `${profileLabels[dialog.profile]} config.toml 已保存`,
notice: `${profileLabel(dialog.profile)} config.toml 已保存`,
error: null
}));
if (!updated) void load();
@@ -117,13 +122,13 @@ export function ManagementView(): ReactElement {
setState((current) => ({ ...current, validatingProfile: profile, error: null, notice: null }));
const submitted = await api.validateProviderProfile(profile);
if (!submitted.ok) {
setState((current) => ({ ...current, validatingProfile: null, error: submitted.error ?? `${profileLabels[profile]} 验证失败` }));
setState((current) => ({ ...current, validatingProfile: null, error: submitted.error ?? `${profileLabel(profile)} 验证失败` }));
return;
}
const first = validationPayload(submitted.data, profile);
setValidation(profile, first);
if (!first.validationId) {
setState((current) => ({ ...current, validatingProfile: null, notice: `${profileLabels[profile]} 验证已提交` }));
setState((current) => ({ ...current, validatingProfile: null, notice: `${profileLabel(profile)} 验证已提交` }));
return;
}
let latest = first;
@@ -131,13 +136,13 @@ export function ManagementView(): ReactElement {
await sleep(2000);
const polled = await api.providerProfileValidation(profile, first.validationId);
if (!polled.ok) {
setState((current) => ({ ...current, validatingProfile: null, error: polled.error ?? `${profileLabels[profile]} 验证查询失败` }));
setState((current) => ({ ...current, validatingProfile: null, error: polled.error ?? `${profileLabel(profile)} 验证查询失败` }));
return;
}
latest = validationPayload(polled.data, profile);
setValidation(profile, latest);
}
setState((current) => ({ ...current, validatingProfile: null, notice: `${profileLabels[profile]} 验证 ${latest.status ?? "已更新"}` }));
setState((current) => ({ ...current, validatingProfile: null, notice: `${profileLabel(profile)} 验证 ${latest.status ?? "已更新"}` }));
}
function setValidation(profile: ProviderProfile, validation: ProviderProfileValidation): void {
@@ -166,11 +171,11 @@ export function ManagementView(): ReactElement {
<div className="management-grid" aria-live="polite">
{visibleProfiles.map((profile) => <ProfilePanel key={profile.profile} profile={profile} inputValue={state.inputs[profile.profile] ?? ""} validation={state.validations[profile.profile] ?? profile.lastValidation ?? null} saving={state.savingProfile === profile.profile} validating={state.validatingProfile === profile.profile} configuring={state.configDialog?.profile === profile.profile && (state.configDialog.loading || state.configDialog.saving)} busy={Boolean(state.savingProfile || state.validatingProfile || state.configDialog?.loading || state.configDialog?.saving)} onInput={setInput} onSave={(item) => void saveKey(item)} onConfig={(item) => void openConfig(item)} onValidate={(item) => void validateProfile(item)} />)}
</div>
{state.configDialog ? <WorkbenchDialog id="management-config-dialog" title={`${profileLabels[state.configDialog.profile]} config.toml`} onClose={closeConfig}>
{state.configDialog ? <WorkbenchDialog id="management-config-dialog" title={`${profileLabel(state.configDialog.profile)} config.toml`} onClose={closeConfig}>
<div className="management-config-editor">
{state.configDialog.error ? <p className="management-config-error" role="alert">{state.configDialog.error}</p> : null}
<label className="management-config-label" htmlFor="management-config-toml">config.toml</label>
<textarea id="management-config-toml" className="mono" value={state.configDialog.configToml} disabled={state.configDialog.loading || state.configDialog.saving} aria-label={`${profileLabels[state.configDialog.profile]} config.toml`} spellCheck={false} onChange={(event) => setConfigToml(event.currentTarget.value)} />
<textarea id="management-config-toml" className="mono" value={state.configDialog.configToml} disabled={state.configDialog.loading || state.configDialog.saving} aria-label={`${profileLabel(state.configDialog.profile)} config.toml`} spellCheck={false} onChange={(event) => setConfigToml(event.currentTarget.value)} />
<div className="management-config-actions">
<button className="command-button secondary" type="button" disabled={state.configDialog.saving} onClick={closeConfig}></button>
<button className="command-button" type="button" disabled={state.configDialog.loading || state.configDialog.saving || !state.configDialog.configToml.trim()} onClick={() => void saveConfig()}>{state.configDialog.saving ? "保存中" : state.configDialog.loading ? "加载中" : "保存"}</button>
@@ -185,13 +190,13 @@ export function ManagementView(): ReactElement {
function ProfilePanel({ profile, inputValue, validation, saving, validating, configuring, busy, onInput, onSave, onConfig, onValidate }: { profile: ProviderProfileStatusItem; inputValue: string; validation: ProviderProfileValidation | null; saving: boolean; validating: boolean; configuring: boolean; busy: boolean; onInput(profile: ProviderProfile, value: string): void; onSave(profile: ProviderProfile): void; onConfig(profile: ProviderProfile): void; onValidate(profile: ProviderProfile): void }): ReactElement {
const tone = profile.configured ? "ok" : "blocked";
const validationTone = validationToneFor(validation);
const bridge = profile.profile === "deepseek" ? profile.bridge ?? { route: "HWLAB Moon Bridge -> DeepSeek official upstream", serviceUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1", responsesPath: "/v1/responses" } : null;
const bridge = profile.bridge ?? (profile.profile === "deepseek" ? { route: "HWLAB Moon Bridge -> DeepSeek official upstream", serviceUrl: "http://hwlab-deepseek-proxy.hwlab-v02.svc.cluster.local:4000/v1", responsesPath: "/v1/responses" } : null);
const validationJob = validation?.runnerJobs?.[0]?.jobName ?? validation?.result?.agentRun?.jobName ?? "-";
const disabled = busy && !saving && !validating;
return (
<article className="management-profile" data-profile={profile.profile} aria-labelledby={`management-profile-${profile.profile}`}>
<header className="management-profile-head">
<div><p className="eyebrow">{profile.backendKind ?? profile.profile}</p><h3 id={`management-profile-${profile.profile}`}>{profileLabels[profile.profile]}</h3></div>
<div><p className="eyebrow">{profile.backendKind ?? profile.profile}</p><h3 id={`management-profile-${profile.profile}`}>{profileLabel(profile.profile)}</h3></div>
<StateTag tone={tone}>{profile.configured ? "已配置" : "未配置"}</StateTag>
</header>
<dl className="management-meta-grid">
@@ -209,7 +214,7 @@ function ProfilePanel({ profile, inputValue, validation, saving, validating, con
<button className="command-button" type="button" disabled={disabled || saving || validating || inputValue.trim().length < 8} onClick={() => onSave(profile.profile)}>{saving ? "保存中" : "保存"}</button>
<button className="command-button secondary" type="button" disabled={disabled || saving || validating || profile.configured !== true} onClick={() => onValidate(profile.profile)}>{validating ? "验证中" : "验证"}</button>
</div>
<section className="management-validation" aria-label={`${profileLabels[profile.profile]} validation`}>
<section className="management-validation" aria-label={`${profileLabel(profile.profile)} validation`}>
<div className="management-section-head"><h4></h4><StateTag tone={validationTone}>{validation?.status ?? "未运行"}</StateTag></div>
<dl className="management-validation-grid">
<div><dt>validation</dt><dd className="mono wrap">{validation?.validationId ?? "-"}</dd></div>
@@ -250,7 +255,9 @@ function configPayload(payload: unknown, profile: ProviderProfile): ProviderProf
function orderedProfiles(items: ProviderProfileStatusItem[]): ProviderProfileStatusItem[] {
const byProfile = new Map(items.map((item) => [item.profile, item]));
return profileOrder.map((profile) => byProfile.get(profile) ?? { profile, configured: false, valuesPrinted: false });
const ordered = profileOrder.map((profile) => byProfile.get(profile) ?? { profile, configured: false, valuesPrinted: false });
const extras = items.filter((item) => !profileOrder.includes(item.profile)).sort((left, right) => left.profile.localeCompare(right.profile));
return [...ordered, ...extras];
}
function mergeProfile(items: ProviderProfileStatusItem[], updated: ProviderProfileStatusItem): ProviderProfileStatusItem[] {
@@ -23,6 +23,7 @@ export function SettingsView({ providerProfile, codeAgentTimeoutMs, gatewayShell
<span></span>
<select id="settings-code-agent-provider-profile" aria-label="Code Agent 模型通道" value={providerProfile} onChange={(event) => onProviderProfile(event.currentTarget.value as ProviderProfile)}>
<option value="deepseek">DeepSeek</option>
<option value="dsflash-go">DeepSeek V4 Flash</option>
<option value="codex-api">Codex API</option>
<option value="minimax-m3">MiniMax-M3</option>
</select>
@@ -143,7 +143,7 @@ export function readStoredNumber(key: string, fallback: number, min: number, max
return Math.min(Math.max(Math.trunc(value), min), max);
}
export function readProviderProfile(): "codex-api" | "minimax-m3" | "deepseek" {
export function readProviderProfile(): string {
const value = readStoredString("hwlab.workbench.codeAgentProviderProfile.v1");
return value === "codex-api" || value === "minimax-m3" || value === "deepseek" ? value : "deepseek";
return /^[a-z0-9][a-z0-9-]{0,63}$/u.test(value) && value !== "runtime-default" ? value : "deepseek";
}
+1 -1
View File
@@ -4,7 +4,7 @@ export type { LiveBuildBuild, LiveBuildCommit, LiveBuildImage, LiveBuildRecord,
export type Tone = "ok" | "source" | "pending" | "blocked" | "error" | "warn" | "dev-live" | "dry-run";
export type RouteId = "workspace" | "performance" | "skills" | "management" | "access" | "gate" | "help" | "settings";
export type AuthState = "checking" | "login" | "authenticated";
export type ProviderProfile = "deepseek" | "codex-api" | "minimax-m3";
export type ProviderProfile = string;
export interface AuthUser {
id?: string;