diff --git a/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx b/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx index 9cdb535..15460b3 100644 --- a/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx +++ b/frontend-web/src/app/admin/(secure)/provider-accounts/page.tsx @@ -1,75 +1,254 @@ "use client"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { CheckCircle2, PlugZap } from "lucide-react"; import { useState } from "react"; import { api } from "@/lib/api"; -const fieldLabels: Record = { - provider_code: "供应商编码", - provider_name: "供应商名称", - api_format: "接口协议", - base_url: "接口地址", - api_key: "接口密钥", - api_secret: "接口密钥补充", - webhook_secret: "回调密钥", - timeout_seconds: "超时时间(秒)", - max_retries: "最大重试次数", - status: "状态", - remark: "备注", +type ProviderAccountRow = { + id: number; + providerCode: string; + providerName: string; + apiFormat: string; + baseUrl: string; + timeoutSeconds: number; + maxRetries: number; + status: number; + remark: string; + updatedAt: string; }; +const protocolOptions = [ + { + value: "openai_official_video", + label: "OpenAI 兼容视频", + note: "/v1/videos", + }, + { + value: "seedance_video_generation", + label: "Seedance 旧版", + note: "/v1/video/generations", + }, +]; + export default function ProviderAccountsPage() { const [form, setForm] = useState({ - provider_code: "openai-backup", - provider_name: "OpenAI 备用账号", - api_format: "openai_official_video", - base_url: "mock://openai", - api_key: "mock", - api_secret: "", - webhook_secret: "", - timeout_seconds: 120, - max_retries: 3, + providerCode: "newapi-seedance", + providerName: "NewAPI Seedance", + apiFormat: "openai_official_video", + baseUrl: "https://your-newapi-domain.com", + apiKey: "", status: 1, - remark: "备用线路", + remark: "NewAPI 上游", }); + + const [feedback, setFeedback] = useState(""); + const query = useQuery({ queryKey: ["provider-accounts"], - queryFn: () => api.get("/api/v1/admin/provider-accounts"), + queryFn: () => api.get("/api/v1/admin/provider-accounts"), }); + const createMutation = useMutation({ - mutationFn: () => api.post("/api/v1/admin/provider-accounts", form), - onSuccess: () => query.refetch(), + mutationFn: () => + api.post("/api/v1/admin/provider-accounts", { + provider_code: form.providerCode.trim(), + provider_name: form.providerName.trim(), + api_format: form.apiFormat, + base_url: form.baseUrl.trim().replace(/\/+$/, ""), + api_key: form.apiKey.trim(), + api_secret: "", + webhook_secret: "", + timeout_seconds: 120, + max_retries: 3, + status: form.status, + remark: form.remark.trim(), + }), + async onSuccess() { + setFeedback("渠道已创建。"); + await query.refetch(); + }, + onError() { + setFeedback("创建失败,请检查渠道编码是否重复或密钥是否为空。"); + }, }); return (
-

新增供应商账号

-
- {Object.entries(form).map(([key, value]) => ( -
+
-

当前账号

-
{JSON.stringify(query.data ?? [], null, 2)}
+
+
+ 渠道列表 +

当前供应商账号

+
+ {query.data?.length ?? 0} 个渠道 +
+ +
+ {query.data?.length ? ( + query.data.map((item) => ( +
+
+
+ {item.providerName} +
{item.providerCode}
+
+ + {item.status === 1 ? "启用" : "停用"} + +
+
+ {protocolOptions.find((option) => option.value === item.apiFormat)?.label ?? item.apiFormat} + {item.baseUrl} + 超时 {item.timeoutSeconds}s +
+ {item.remark ?

{item.remark}

: null} +
+ )) + ) : ( +
还没有供应商账号。
+ )} +
+ +
+ + NewAPI 上游通常选择 OpenAI 兼容视频,接口地址填写域名根地址即可。 +
); diff --git a/frontend-web/src/app/admin/(secure)/provider-models/page.tsx b/frontend-web/src/app/admin/(secure)/provider-models/page.tsx index 25948a4..08115df 100644 --- a/frontend-web/src/app/admin/(secure)/provider-models/page.tsx +++ b/frontend-web/src/app/admin/(secure)/provider-models/page.tsx @@ -1,49 +1,300 @@ "use client"; import { useMutation, useQuery } from "@tanstack/react-query"; +import { Boxes, Plus } from "lucide-react"; +import { useMemo, useState } from "react"; import { api } from "@/lib/api"; +type ProviderAccountRow = { + id: number; + providerName: string; + providerCode: string; + status: number; +}; + +type ProviderModelRow = { + id: number; + providerAccountId: number; + providerName: string; + modelCode: string; + modelName: string; + defaultRatio: string; + defaultResolution: string; + minDuration: number; + maxDuration: number; + status: number; +}; + +function parseModelCodes(value: string) { + return value + .split(/[\n,,]/) + .map((item) => item.trim()) + .filter(Boolean); +} + export default function ProviderModelsPage() { - const form = { - provider_account_id: 1, - model_code: "sora-2-pro", - model_name: "Sora 2 Pro", - request_content_type: "multipart/form-data", - supports_text_to_video: true, - supports_image_to_video: true, - supports_video_reference: false, - supports_audio_reference: false, - supports_generate_audio: true, - supports_remix: false, - supports_webhook: true, - min_duration: 4, - max_duration: 12, - default_ratio: "16:9", - default_resolution: "1280x720", + const [form, setForm] = useState({ + providerAccountId: 0, + modelCodes: "seedance, seedance-fast", + defaultRatio: "16:9", + defaultResolution: "1280x720", + minDuration: 4, + maxDuration: 12, status: 1, - }; - const query = useQuery({ - queryKey: ["provider-models"], - queryFn: () => api.get("/api/v1/admin/provider-models"), }); + const [feedback, setFeedback] = useState(""); + + const accountsQuery = useQuery({ + queryKey: ["provider-accounts"], + queryFn: () => api.get("/api/v1/admin/provider-accounts"), + }); + + const modelsQuery = useQuery({ + queryKey: ["provider-models"], + queryFn: () => api.get("/api/v1/admin/provider-models"), + }); + + const enabledAccounts = useMemo( + () => (accountsQuery.data ?? []).filter((item) => item.status === 1), + [accountsQuery.data], + ); + + const activeProviderAccountId = + form.providerAccountId || enabledAccounts[0]?.id || accountsQuery.data?.[0]?.id || 0; + + const modelCodes = parseModelCodes(form.modelCodes); + const uniqueModelCodes = Array.from(new Set(modelCodes)); + const createMutation = useMutation({ - mutationFn: () => api.post("/api/v1/admin/provider-models", form), - onSuccess: () => query.refetch(), + mutationFn: async () => { + return Promise.all( + uniqueModelCodes.map((modelCode) => + api.post("/api/v1/admin/provider-models", { + provider_account_id: activeProviderAccountId, + model_code: modelCode, + model_name: modelCode, + request_content_type: "application/json", + supports_text_to_video: true, + supports_image_to_video: true, + supports_video_reference: true, + supports_audio_reference: false, + supports_generate_audio: true, + supports_remix: false, + supports_webhook: false, + min_duration: form.minDuration, + max_duration: form.maxDuration, + default_ratio: form.defaultRatio, + default_resolution: form.defaultResolution, + status: form.status, + }), + ), + ); + }, + async onSuccess() { + setFeedback(`已创建 ${uniqueModelCodes.length} 个模型。`); + await modelsQuery.refetch(); + }, + onError() { + setFeedback("创建失败,请检查模型名是否重复或渠道是否可用。"); + }, }); return (
-

新增供应商模型

-
{JSON.stringify(form, null, 2)}
- +
+
+ 模型配置 +

新增供应商模型

+

像 NewAPI 一样,把模型名粘进来即可。

+
+ +
+ +
+ + +