fix(admin): secure proxied model mutations
Staging Backend Quality Gate / validate (push) Successful in 19m41s
Staging Backend Quality Gate / publish (push) Successful in 28m13s

This commit is contained in:
Jesse_Chen
2026-08-08 01:26:21 +08:00
parent c30b4b906b
commit 99e2abe9bc
9 changed files with 275 additions and 58 deletions
@@ -24,7 +24,6 @@ import {
import { useCallback, useEffect, useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { ReasonActionModal } from "./reason-action-modal";
import { formatAdminDate } from "./resource-table";
const { Text } = Typography;
@@ -76,7 +75,6 @@ type ProviderForm = {
type ModelForm = Omit<ModelVersion, "id" | "configId" | "version" | "providerCode" | "status" | "createdAt" | "publishedAt" | "settings"> & {
versionId?: string;
settingsJson: string;
reason: string;
};
type ModelsPayload = { data: ModelVersion[]; total: number; providers: Provider[] };
@@ -121,7 +119,6 @@ export default function ModelManagement() {
const [discoveredModels, setDiscoveredModels] = useState<DiscoveredModel[]>([]);
const [actingId, setActingId] = useState<string | null>(null);
const [versionAction, setVersionAction] = useState<VersionAction | null>(null);
const [pendingProvider, setPendingProvider] = useState<Record<string, unknown> | null>(null);
const [filters, setFilters] = useState<ModelFilters>({});
const canWrite = Boolean(identity?.permissions.includes("models.write"));
const canTest = Boolean(identity?.permissions.includes("models.test"));
@@ -187,7 +184,6 @@ export default function ModelManagement() {
isDefault: model.isDefault,
fallbackModelId: model.fallbackModelId,
settingsJson: JSON.stringify(model.settings, null, 2),
reason: "",
} : {
modelId: "",
providerId: providerId ?? providers[0]?.id,
@@ -203,37 +199,32 @@ export default function ModelManagement() {
isDefault: false,
fallbackModelId: null,
settingsJson: "{}",
reason: "",
});
setModelOpen(true);
}
function prepareProviderSave(values: ProviderForm) {
async function saveProvider(values: ProviderForm) {
const apiKey = values.apiKey?.trim();
setPendingProvider({
action: "saveProvider",
id: editingProvider?.id ?? null,
name: values.name.trim(),
providerType: values.providerType,
baseUrl: values.providerType === "openai-compatible" ? values.baseUrl?.trim() : null,
...(apiKey ? { apiKey } : {}),
enabled: values.enabled,
});
}
async function saveProvider(reason: string) {
if (!pendingProvider) return;
setSaving(true);
try {
await adminRequestJson("/api/admin/models", {
method: "POST",
body: JSON.stringify({ ...pendingProvider, reason }),
body: JSON.stringify({
action: "saveProvider",
id: editingProvider?.id ?? null,
name: values.name.trim(),
providerType: values.providerType,
baseUrl: values.providerType === "openai-compatible" ? values.baseUrl?.trim() : null,
...(apiKey ? { apiKey } : {}),
enabled: values.enabled,
}),
});
message.success("供应商配置已保存");
setPendingProvider(null);
setProviderOpen(false);
providerForm.resetFields();
await load();
} catch (error) {
message.error(error instanceof Error ? error.message : "保存失败");
} finally {
setSaving(false);
}
@@ -296,7 +287,6 @@ export default function ModelManagement() {
isDefault: values.isDefault,
fallbackModelId: values.fallbackModelId?.trim() || null,
settings,
reason: values.reason.trim(),
}),
});
message.success("模型草稿已保存");
@@ -320,14 +310,18 @@ export default function ModelManagement() {
}
}
async function submitVersionAction(reason: string) {
async function submitVersionAction() {
if (!versionAction) return;
const { action, model } = versionAction;
await act(action === "publish"
? { action, versionId: model.id, reason }
: { action, configId: model.configId, targetVersion: model.version, reason },
action === "publish" ? "模型已发布" : "模型已回滚", model.id);
setVersionAction(null);
try {
await act(action === "publish"
? { action, versionId: model.id }
: { action, configId: model.configId, targetVersion: model.version },
action === "publish" ? "模型已发布" : "模型已回滚", model.id);
setVersionAction(null);
} catch (error) {
message.error(error instanceof Error ? error.message : action === "publish" ? "发布失败" : "回滚失败");
}
}
async function testVersion(item: ModelVersion) {
@@ -411,8 +405,8 @@ export default function ModelManagement() {
</Card>
</Space>
<Modal title={editingProvider ? "编辑供应商" : "新增供应商"} open={providerOpen} okText="继续验证" cancelText="取消" confirmLoading={saving} onOk={() => providerForm.submit()} onCancel={() => { setProviderOpen(false); providerForm.resetFields(); }} destroyOnHidden>
<Form<ProviderForm> form={providerForm} layout="vertical" onFinish={prepareProviderSave}>
<Modal title={editingProvider ? "编辑供应商" : "新增供应商"} open={providerOpen} okText="保存" cancelText="取消" confirmLoading={saving} onOk={() => providerForm.submit()} onCancel={() => { setProviderOpen(false); providerForm.resetFields(); }} destroyOnHidden>
<Form<ProviderForm> form={providerForm} layout="vertical" onFinish={saveProvider}>
<Row gutter={16}><Col xs={24} md={12}><Form.Item name="name" label="名称" rules={[{ required: true }]}><Input /></Form.Item></Col><Col xs={24} md={12}><Form.Item label="代码预览(服务端生成)"><Input readOnly value={editingProvider?.code ?? "保存后由服务端自动生成"} /></Form.Item></Col></Row>
<Form.Item name="providerType" label="类型" rules={[{ required: true }]}><Select options={Object.entries(providerTypeLabels).map(([value, label]) => ({ value, label }))} /></Form.Item>
<Form.Item noStyle shouldUpdate={(before, after) => before.providerType !== after.providerType}>{({ getFieldValue }) => getFieldValue("providerType") === "openai-compatible" ? <Form.Item name="baseUrl" label="Base URL" rules={[{ required: true }, { type: "url" }]}><Input /></Form.Item> : null}</Form.Item>
@@ -454,25 +448,23 @@ export default function ModelManagement() {
<Form.Item name="fallbackModelId" label="回退模型 ID"><Input allowClear /></Form.Item>
<Form.Item name="settingsJson" label="设置 JSON" rules={[{ required: true }]}><Input.TextArea rows={5} spellCheck={false} /></Form.Item>
<Space size="large"><Form.Item name="enabled" label="启用" valuePropName="checked"><Switch /></Form.Item><Form.Item name="isDefault" label="默认模型" valuePropName="checked" dependencies={["enabled"]} rules={[({ getFieldValue }) => ({ validator(_, value) { return value && !getFieldValue("enabled") ? Promise.reject(new Error("默认模型必须启用")) : Promise.resolve(); } })]}><Switch /></Form.Item></Space>
<Form.Item name="reason" label="修改原因" rules={[{ required: true }, { max: 500 }]}><Input.TextArea rows={2} /></Form.Item>
</Form>
</Modal>
<ReasonActionModal
open={Boolean(pendingProvider)}
title="保存模型供应商"
okText="保存"
confirmLoading={saving}
onCancel={() => setPendingProvider(null)}
onSubmit={saveProvider}
/>
<ReasonActionModal
<Modal
open={Boolean(versionAction)}
title={versionAction?.action === "rollback" ? `回滚到 v${versionAction.model.version}` : "发布模型版本"}
okText={versionAction?.action === "rollback" ? "确认回滚" : "确认发布"}
danger={versionAction?.action === "rollback"}
okButtonProps={{ danger: versionAction?.action === "rollback" }}
confirmLoading={Boolean(actingId)}
onCancel={() => setVersionAction(null)}
onSubmit={submitVersionAction}
/>
onOk={() => void submitVersionAction()}
destroyOnHidden
>
<Text>
{versionAction?.action === "rollback"
? "确认将此历史版本恢复为新的已发布版本?"
: "确认发布此模型版本?"}
</Text>
</Modal>
</List>;
}