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( const [worldlabsKeyPoolConfig, setWorldlabsKeyPoolConfig] = useState(
defaultWorldLabsKeyPoolConfig defaultWorldLabsKeyPoolConfig
); );
const [worldlabsApiKeysDraft, setWorldlabsApiKeysDraft] = useState("");
const [threeDModelAliasConfig, setThreeDModelAliasConfig] = useState( const [threeDModelAliasConfig, setThreeDModelAliasConfig] = useState(
defaultThreeDModelAliasConfig() 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 = ( const updateThreeDModelAliasEntries = (
key: "tripoModels" | "worldModels", key: "tripoModels" | "worldModels",
updater: (entries: ThreeDModelAliasEntry[]) => ThreeDModelAliasEntry[] 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]"> <code className="mx-1 rounded bg-muted px-1 py-0.5 text-[11px]">
/api/worldlabs/key-pool/import /api/worldlabs/key-pool/import
</code> </code>
批量导入新 Key,保存到数据库系统设置。 批量导入新 Key;后台管理员在本页面导入时不需要额外填写令牌。
</div> </div>
</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="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 className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div> <div>
@ -15191,9 +15316,14 @@ export function AdminSettingsClient() {
</span> </span>
</div> </div>
</div> </div>
<Button onClick={handleSaveWorldlabs3dConfig}> <div className="flex flex-wrap gap-2">
<Save className="mr-2 h-4 w-4" /> 保存 World Labs 配置 <Button onClick={handleSaveWorldlabs3dConfig}>
</Button> <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> </CardContent>
</Card> </Card>

View File

@ -2592,6 +2592,20 @@ function resolveModelDisplayNameByModelId(
return String(match?.model?.displayName || ""); 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 并保存图片记录到数据库 * 解析生图 Markdown 并保存图片记录到数据库
* 支持三种格式:thumb|||url|||size、thumb$$url$$size、thumb|url|size * 支持三种格式:thumb|||url|||size、thumb$$url$$size、thumb|url|size
@ -9403,7 +9417,11 @@ export async function POST(request: Request) {
id, id,
messageId, messageId,
userPrompt, userPrompt,
finalModelId, resolveModelRefByRuntimeModelId(
allModels,
usedModelId,
usedModelId === actualModelId ? finalModelId : undefined,
),
{ {
sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceAssetIds: requestTargetResolution?.targetAssetIds,
sourceImageUrls: requestTargetResolution?.targetImageUrls, sourceImageUrls: requestTargetResolution?.targetImageUrls,
@ -9987,7 +10005,11 @@ export async function POST(request: Request) {
id, id,
messageId, messageId,
userPrompt, userPrompt,
finalModelId, resolveModelRefByRuntimeModelId(
allModels,
usedModelId,
usedModelId === actualModelId ? finalModelId : undefined,
),
{ {
sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceAssetIds: requestTargetResolution?.targetAssetIds,
sourceImageUrls: requestTargetResolution?.targetImageUrls, sourceImageUrls: requestTargetResolution?.targetImageUrls,
@ -10384,7 +10406,11 @@ export async function POST(request: Request) {
id, id,
messageId, messageId,
userPrompt, userPrompt,
finalModelId, resolveModelRefByRuntimeModelId(
allModels,
usedModelId,
usedModelId === actualModelId ? finalModelId : undefined,
),
{ {
sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceAssetIds: requestTargetResolution?.targetAssetIds,
sourceImageUrls: requestTargetResolution?.targetImageUrls, sourceImageUrls: requestTargetResolution?.targetImageUrls,
@ -11243,7 +11269,11 @@ export async function POST(request: Request) {
id, id,
messageId, messageId,
userPrompt, userPrompt,
finalModelId, resolveModelRefByRuntimeModelId(
allModels,
usedModelId,
usedModelId === actualModelId ? finalModelId : undefined,
),
{ {
sourceAssetIds: requestTargetResolution?.targetAssetIds, sourceAssetIds: requestTargetResolution?.targetAssetIds,
sourceImageUrls: requestTargetResolution?.targetImageUrls, sourceImageUrls: requestTargetResolution?.targetImageUrls,

View File

@ -4,6 +4,7 @@ import {
getAiProvidersByType, getAiProvidersByType,
} from "@/lib/db/queries"; } from "@/lib/db/queries";
import { ChatSDKError } from "@/lib/errors"; import { ChatSDKError } from "@/lib/errors";
import { requireAdmin } from "@/lib/middleware/admin";
import { import {
sanitizeWorldLabsApiKey, sanitizeWorldLabsApiKey,
sanitizeWorldLabsBaseUrl, sanitizeWorldLabsBaseUrl,
@ -108,23 +109,34 @@ export async function POST(request: Request) {
try { try {
const dbConfig = await getWorldLabsKeyPoolConfig(); const dbConfig = await getWorldLabsKeyPoolConfig();
const configuredToken = dbConfig.importToken || getConfiguredImportToken(); const configuredToken = dbConfig.importToken || getConfiguredImportToken();
if (!configuredToken) { const requestToken = readImportToken(request);
return Response.json(
{ let authorizedByAdmin = false;
success: false, try {
error: await requireAdmin();
"服务端未配置 World Labs key 池导入令牌,无法使用专用导入接口", authorizedByAdmin = true;
}, } catch (_error) {
{ status: 503 } authorizedByAdmin = false;
);
} }
const requestToken = readImportToken(request); if (!authorizedByAdmin) {
if (!requestToken || requestToken !== configuredToken) { if (!configuredToken) {
return Response.json( return Response.json(
{ success: false, error: "导入令牌无效" }, {
{ status: 401 } 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(() => ({})); const rawBody = await request.json().catch(() => ({}));

View File

@ -7,6 +7,7 @@ import {
import type { WorldLabsProviderConfig } from "./types"; import type { WorldLabsProviderConfig } from "./types";
const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai"; const DEFAULT_WORLDLABS_BASE_URL = "https://api.worldlabs.ai";
let worldLabsProviderCursor = 0;
export function sanitizeWorldLabsApiKey(value: string): string { export function sanitizeWorldLabsApiKey(value: string): string {
return String(value || "") return String(value || "")
@ -73,9 +74,21 @@ export async function getWorldLabsProviderConfig(
): Promise<WorldLabsProviderConfig> { ): Promise<WorldLabsProviderConfig> {
const providers = await getWorldLabsProviderConfigs(); const providers = await getWorldLabsProviderConfigs();
const preferredId = String(preferredProviderId || "").trim(); const preferredId = String(preferredProviderId || "").trim();
const worldLabsProvider = preferredId if (preferredId) {
? providers.find((item) => item.providerId === preferredId) || null const matchedProvider =
: providers[0] || null; 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) { if (worldLabsProvider) {
return worldLabsProvider; return worldLabsProvider;