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(
|
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>
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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(() => ({}));
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user