|
|
|
@@ -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[] {
|
|
|
|
|