fix: settle fallback model billing and worldlabs key pool
This commit is contained in:
parent
6628a6b2d0
commit
1cda4fa23e
@ -1790,6 +1790,7 @@ export function AdminSettingsClient() {
|
||||
const [worldlabsKeyPoolConfig, setWorldlabsKeyPoolConfig] = useState(
|
||||
defaultWorldLabsKeyPoolConfig
|
||||
);
|
||||
const [worldlabsApiKeysDraft, setWorldlabsApiKeysDraft] = useState("");
|
||||
const [threeDModelAliasConfig, setThreeDModelAliasConfig] = useState(
|
||||
defaultThreeDModelAliasConfig()
|
||||
);
|
||||
@ -5091,6 +5092,102 @@ export function AdminSettingsClient() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddWorldlabs3dPoolKey = async () => {
|
||||
try {
|
||||
const trimmedName = worldlabs3dConfig.name.trim() || "World Labs Key";
|
||||
const trimmedBaseUrl =
|
||||
worldlabs3dConfig.baseUrl.trim() ||
|
||||
getProviderDefaultBaseUrl("worldlabs-3d");
|
||||
const trimmedApiKey = worldlabs3dConfig.apiKey.trim();
|
||||
|
||||
if (!trimmedApiKey) {
|
||||
toast.error("请填写要加入 Key 池的 World Labs API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/worldlabs/key-pool/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
keys: [
|
||||
{
|
||||
apiKey: trimmedApiKey,
|
||||
name: trimmedName,
|
||||
baseUrl: trimmedBaseUrl,
|
||||
isEnabled: worldlabs3dConfig.isEnabled,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload?.success === false) {
|
||||
throw new Error(
|
||||
String(payload?.error || payload?.message || "添加 World Labs Key 失败")
|
||||
);
|
||||
}
|
||||
|
||||
const importedCount = Number(payload?.data?.importedCount || 0);
|
||||
const skippedCount = Number(payload?.data?.skippedCount || 0);
|
||||
toast.success(
|
||||
importedCount > 0
|
||||
? "World Labs Key 已加入池"
|
||||
: skippedCount > 0
|
||||
? "这个 World Labs Key 已存在,已跳过重复添加"
|
||||
: "World Labs Key 处理完成"
|
||||
);
|
||||
setWorldlabs3dConfig((prev) => ({ ...prev, apiKey: "" }));
|
||||
mutateProviders();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "添加 World Labs Key 失败"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportWorldlabs3dPoolKeys = async () => {
|
||||
try {
|
||||
const apiKeysText = worldlabsApiKeysDraft.trim();
|
||||
if (!apiKeysText) {
|
||||
toast.error("请先粘贴要批量导入的 World Labs API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch("/api/worldlabs/key-pool/import", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
apiKeysText,
|
||||
namePrefix: worldlabs3dConfig.name.trim() || "WorldLabsKey",
|
||||
baseUrl:
|
||||
worldlabs3dConfig.baseUrl.trim() ||
|
||||
getProviderDefaultBaseUrl("worldlabs-3d"),
|
||||
isEnabled: worldlabs3dConfig.isEnabled,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || payload?.success === false) {
|
||||
throw new Error(
|
||||
String(payload?.error || payload?.message || "批量导入 World Labs Key 失败")
|
||||
);
|
||||
}
|
||||
|
||||
const importedCount = Number(payload?.data?.importedCount || 0);
|
||||
const skippedCount = Number(payload?.data?.skippedCount || 0);
|
||||
toast.success(
|
||||
`World Labs Key 导入完成:新增 ${importedCount} 个,跳过 ${skippedCount} 个`
|
||||
);
|
||||
setWorldlabsApiKeysDraft("");
|
||||
setWorldlabs3dConfig((prev) => ({ ...prev, apiKey: "" }));
|
||||
mutateProviders();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : "批量导入 World Labs Key 失败"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const updateThreeDModelAliasEntries = (
|
||||
key: "tripoModels" | "worldModels",
|
||||
updater: (entries: ThreeDModelAliasEntry[]) => ThreeDModelAliasEntry[]
|
||||
@ -15071,9 +15168,37 @@ export function AdminSettingsClient() {
|
||||
<code className="mx-1 rounded bg-muted px-1 py-0.5 text-[11px]">
|
||||
/api/worldlabs/key-pool/import
|
||||
</code>
|
||||
批量导入新 Key,保存到数据库系统设置。
|
||||
批量导入新 Key;后台管理员在本页面导入时不需要额外填写令牌。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 rounded-xl border border-dashed p-4">
|
||||
<div className="flex flex-col gap-1 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<Label htmlFor="worldlabs3dApiKeysDraft">
|
||||
批量导入 Key
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
每行一个 World Labs API Key,导入时会自动跳过已存在或重复的
|
||||
Key。
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleImportWorldlabs3dPoolKeys}
|
||||
>
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
批量导入
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
id="worldlabs3dApiKeysDraft"
|
||||
value={worldlabsApiKeysDraft}
|
||||
onChange={(e) => setWorldlabsApiKeysDraft(e.target.value)}
|
||||
rows={5}
|
||||
placeholder={"wlt_xxxxxxxxx\nwlt_yyyyyyyyy\nwlt_zzzzzzzzz"}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 rounded-xl border p-4">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
@ -15191,9 +15316,14 @@ export function AdminSettingsClient() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleSaveWorldlabs3dConfig}>
|
||||
<Save className="mr-2 h-4 w-4" /> 保存 World Labs 配置
|
||||
</Button>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button onClick={handleSaveWorldlabs3dConfig}>
|
||||
<Save className="mr-2 h-4 w-4" /> 保存 World Labs 配置
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleAddWorldlabs3dPoolKey}>
|
||||
<Plus className="mr-2 h-4 w-4" /> 添加为新 Key
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -2592,6 +2592,20 @@ function resolveModelDisplayNameByModelId(
|
||||
return String(match?.model?.displayName || "");
|
||||
}
|
||||
|
||||
function resolveModelRefByRuntimeModelId(
|
||||
models: Awaited<ReturnType<typeof getEnabledAiModelsWithProvider>> | undefined,
|
||||
modelId: string | undefined,
|
||||
fallbackRef?: string,
|
||||
): string {
|
||||
const normalizedModelId = String(modelId || "").trim();
|
||||
if (!normalizedModelId) return fallbackRef || "";
|
||||
|
||||
const match = models?.find(
|
||||
(item) => item.model.modelId === normalizedModelId || item.model.id === normalizedModelId,
|
||||
);
|
||||
return String(match?.model?.id || fallbackRef || normalizedModelId || "");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析生图 Markdown 并保存图片记录到数据库
|
||||
* 支持三种格式:thumb|||url|||size、thumb$$url$$size、thumb|url|size
|
||||
@ -9403,7 +9417,11 @@ export async function POST(request: Request) {
|
||||
id,
|
||||
messageId,
|
||||
userPrompt,
|
||||
finalModelId,
|
||||
resolveModelRefByRuntimeModelId(
|
||||
allModels,
|
||||
usedModelId,
|
||||
usedModelId === actualModelId ? finalModelId : undefined,
|
||||
),
|
||||
{
|
||||
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||
@ -9987,7 +10005,11 @@ export async function POST(request: Request) {
|
||||
id,
|
||||
messageId,
|
||||
userPrompt,
|
||||
finalModelId,
|
||||
resolveModelRefByRuntimeModelId(
|
||||
allModels,
|
||||
usedModelId,
|
||||
usedModelId === actualModelId ? finalModelId : undefined,
|
||||
),
|
||||
{
|
||||
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||
@ -10384,7 +10406,11 @@ export async function POST(request: Request) {
|
||||
id,
|
||||
messageId,
|
||||
userPrompt,
|
||||
finalModelId,
|
||||
resolveModelRefByRuntimeModelId(
|
||||
allModels,
|
||||
usedModelId,
|
||||
usedModelId === actualModelId ? finalModelId : undefined,
|
||||
),
|
||||
{
|
||||
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||
@ -11243,7 +11269,11 @@ export async function POST(request: Request) {
|
||||
id,
|
||||
messageId,
|
||||
userPrompt,
|
||||
finalModelId,
|
||||
resolveModelRefByRuntimeModelId(
|
||||
allModels,
|
||||
usedModelId,
|
||||
usedModelId === actualModelId ? finalModelId : undefined,
|
||||
),
|
||||
{
|
||||
sourceAssetIds: requestTargetResolution?.targetAssetIds,
|
||||
sourceImageUrls: requestTargetResolution?.targetImageUrls,
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
getAiProvidersByType,
|
||||
} from "@/lib/db/queries";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import { requireAdmin } from "@/lib/middleware/admin";
|
||||
import {
|
||||
sanitizeWorldLabsApiKey,
|
||||
sanitizeWorldLabsBaseUrl,
|
||||
@ -108,23 +109,34 @@ export async function POST(request: Request) {
|
||||
try {
|
||||
const dbConfig = await getWorldLabsKeyPoolConfig();
|
||||
const configuredToken = dbConfig.importToken || getConfiguredImportToken();
|
||||
if (!configuredToken) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"服务端未配置 World Labs key 池导入令牌,无法使用专用导入接口",
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
const requestToken = readImportToken(request);
|
||||
|
||||
let authorizedByAdmin = false;
|
||||
try {
|
||||
await requireAdmin();
|
||||
authorizedByAdmin = true;
|
||||
} catch (_error) {
|
||||
authorizedByAdmin = false;
|
||||
}
|
||||
|
||||
const requestToken = readImportToken(request);
|
||||
if (!requestToken || requestToken !== configuredToken) {
|
||||
return Response.json(
|
||||
{ success: false, error: "导入令牌无效" },
|
||||
{ status: 401 }
|
||||
);
|
||||
if (!authorizedByAdmin) {
|
||||
if (!configuredToken) {
|
||||
return Response.json(
|
||||
{
|
||||
success: false,
|
||||
error:
|
||||
"服务端未配置 World Labs key 池导入令牌,无法使用专用导入接口",
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!requestToken || requestToken !== configuredToken) {
|
||||
return Response.json(
|
||||
{ success: false, error: "导入令牌无效" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const rawBody = await request.json().catch(() => ({}));
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
import type { WorldLabsProviderConfig } from "./types";
|
||||
|
||||
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
|
||||
let worldLabsProviderCursor = 0;
|
||||
|
||||
export function sanitizeWorldLabsApiKey(value: string): string {
|
||||
return String(value || "")
|
||||
@ -73,9 +74,21 @@ export async function getWorldLabsProviderConfig(
|
||||
): Promise<WorldLabsProviderConfig> {
|
||||
const providers = await getWorldLabsProviderConfigs();
|
||||
const preferredId = String(preferredProviderId || "").trim();
|
||||
const worldLabsProvider = preferredId
|
||||
? providers.find((item) => item.providerId === preferredId) || null
|
||||
: providers[0] || null;
|
||||
if (preferredId) {
|
||||
const matchedProvider =
|
||||
providers.find((item) => item.providerId === preferredId) || null;
|
||||
if (matchedProvider) {
|
||||
return matchedProvider;
|
||||
}
|
||||
}
|
||||
|
||||
const worldLabsProvider =
|
||||
providers.length > 0
|
||||
? providers[worldLabsProviderCursor % providers.length]
|
||||
: null;
|
||||
if (providers.length > 0) {
|
||||
worldLabsProviderCursor = (worldLabsProviderCursor + 1) % providers.length;
|
||||
}
|
||||
|
||||
if (worldLabsProvider) {
|
||||
return worldLabsProvider;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user