fix: settle fallback model billing and worldlabs key pool
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-20 15:45:07 +08:00
parent 6628a6b2d0
commit 1cda4fa23e
4 changed files with 211 additions and 26 deletions

View File

@ -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>

View File

@ -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,

View File

@ -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(() => ({}));

View File

@ -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;