fix(admin): secure proxied model mutations
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user