diff --git a/app/(admin)/settings/_components/local-ai-sections/image-pipeline-sections.tsx b/app/(admin)/settings/_components/local-ai-sections/image-pipeline-sections.tsx index 638f686ce..61cc133a1 100644 --- a/app/(admin)/settings/_components/local-ai-sections/image-pipeline-sections.tsx +++ b/app/(admin)/settings/_components/local-ai-sections/image-pipeline-sections.tsx @@ -2377,13 +2377,10 @@ export function LocalAiImagePipelineSections({ items={[ { label: - extendConfig.provider === "vip98k" || extendConfig.apiKey - ? "Key 已配置" - : "Key 待配置", - tone: - extendConfig.provider === "vip98k" || extendConfig.apiKey - ? "enabled" - : "warning", + extendConfig.provider === "vip98k" + ? "VIP98K 服务商" + : "阿里云服务商", + tone: "enabled", }, { label: `${extendConfig.xScale}x / ${extendConfig.yScale}x`, @@ -2400,22 +2397,11 @@ export function LocalAiImagePipelineSections({ VIP98K 服务商读取,仅在这里维护扩展比例。 ) : ( -
- - - setExtendConfig({ ...extendConfig, apiKey: e.target.value }) - } - placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - type="password" - value={extendConfig.apiKey} - /> -
- 请输入阿里云Dashscope API密钥,例如: - sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx +
+ 使用阿里云 DashScope 的 image-out-painting 模型。API Key + 从“AI 模型设置”中已启用的阿里云官方生图服务商读取, + 仅在这里维护拓展比例。
-
)}
@@ -2470,7 +2456,7 @@ export function LocalAiImagePipelineSections({ https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting

• 支持的图片格式:JPG, PNG, WebP等常见格式

-

• 最大图片大小:4MB

+

• 最大图片大小:10MB

• 处理时间:约5-15秒/张

• 放大倍数范围:1.0-2.0(推荐1.5倍等比例扩展)

diff --git a/app/(admin)/settings/_lib/types.ts b/app/(admin)/settings/_lib/types.ts index 1ac65384d..521541cbd 100644 --- a/app/(admin)/settings/_lib/types.ts +++ b/app/(admin)/settings/_lib/types.ts @@ -275,7 +275,6 @@ export type EnhanceConfig = { export type ExtendConfig = { provider: "vip98k" | "dashscope"; - apiKey: string; xScale: string; yScale: string; }; diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx index 2cc67412e..ec4273759 100644 --- a/app/(admin)/settings/admin-settings-client.tsx +++ b/app/(admin)/settings/admin-settings-client.tsx @@ -2633,8 +2633,7 @@ export function AdminSettingsClient() { // 图像拓展API配置状态 const [extendConfig, setExtendConfig] = useState({ - provider: "vip98k" as "vip98k" | "dashscope", - apiKey: "", + provider: "dashscope" as "vip98k" | "dashscope", xScale: "1.5", // X轴放大倍数 yScale: "1.5", // Y轴放大倍数 }); @@ -2953,13 +2952,11 @@ export function AdminSettingsClient() { if (extendConfigData?.setting?.value) { const config = extendConfigData.setting.value as { provider?: "vip98k" | "dashscope"; - apiKey?: string; xScale?: number; yScale?: number; }; setExtendConfig({ provider: config.provider === "vip98k" ? "vip98k" : "dashscope", - apiKey: config.apiKey || "", xScale: config.xScale ? String(config.xScale) : "1.5", yScale: config.yScale ? String(config.yScale) : "1.5", }); @@ -5556,11 +5553,6 @@ export function AdminSettingsClient() { // --- 图像拓展API配置逻辑 --- const handleSaveExtendConfig = async () => { try { - if (extendConfig.provider !== "vip98k" && !extendConfig.apiKey.trim()) { - toast.error("请输入API密钥"); - return; - } - const xScale = Number.parseFloat(extendConfig.xScale); const yScale = Number.parseFloat(extendConfig.yScale); @@ -5581,7 +5573,6 @@ export function AdminSettingsClient() { key: "extend_api_config", value: { provider: extendConfig.provider, - apiKey: extendConfig.apiKey.trim(), xScale, yScale, }, @@ -9000,7 +8991,7 @@ export function AdminSettingsClient() { key: "extend", label: "图像拓展", group: "图像处理", - tone: extendConfig.apiKey ? "enabled" : "warning", + tone: "enabled", }, { key: "watermark-remove", diff --git a/app/api/extend/route.ts b/app/api/extend/route.ts index 2e44f06b1..196d74172 100644 --- a/app/api/extend/route.ts +++ b/app/api/extend/route.ts @@ -182,6 +182,11 @@ export async function POST(request: NextRequest) { ); } catch (error) { if (error instanceof ExtendServiceError) { + console.error("[Extend API] Request failed:", { + status: error.status, + message: error.message, + detail: error.detail, + }); if (error.status === 429 || isRateLimitError(error)) { return NextResponse.json( { diff --git a/lib/ai/extend/config.ts b/lib/ai/extend/config.ts new file mode 100644 index 000000000..2baf28205 --- /dev/null +++ b/lib/ai/extend/config.ts @@ -0,0 +1,25 @@ +export type ExtendProviderCredential = { + apiKey?: string | null; + updatedAt?: Date | string | null; +}; + +function getUpdatedAtTimestamp(value: Date | string | null | undefined): number { + if (!value) return 0; + const timestamp = value instanceof Date ? value.getTime() : Date.parse(value); + return Number.isFinite(timestamp) ? timestamp : 0; +} + +export function resolveAlibabaExtendApiKey( + providers: ExtendProviderCredential[], + legacyApiKey?: string | null, +): string | null { + const currentProvider = providers + .filter((provider) => Boolean(provider.apiKey?.trim())) + .sort( + (left, right) => + getUpdatedAtTimestamp(right.updatedAt) - + getUpdatedAtTimestamp(left.updatedAt), + )[0]; + + return currentProvider?.apiKey?.trim() || legacyApiKey?.trim() || null; +} diff --git a/lib/ai/extend/service.ts b/lib/ai/extend/service.ts index f2189e236..f0ed39603 100644 --- a/lib/ai/extend/service.ts +++ b/lib/ai/extend/service.ts @@ -1,9 +1,11 @@ import { createMediaAssetRelation, + getAiProvidersByType, getMediaAssetsByUserAndUrls, getSystemSetting, upsertMediaAsset, } from "@/lib/db/queries"; +import { resolveAlibabaExtendApiKey } from "@/lib/ai/extend/config"; import { executeConfiguredVip98kTask } from "@/lib/ai/vip98k-runtime"; import { db } from "@/lib/db"; import { generatedImage } from "@/lib/db/schema"; @@ -331,6 +333,7 @@ async function extendImage( config: ExtendConfig, xScale: number, yScale: number, + requestId?: string | null, ): Promise { if (config.provider === "vip98k") { try { @@ -386,16 +389,36 @@ async function extendImage( }, ); - const initialResult = await response.json(); - if (initialResult.code) { + const initialResult = await response.json().catch(() => null); + if (!response.ok || initialResult?.code) { + const detail = + initialResult?.message || + initialResult?.code || + `${response.status} ${response.statusText}`; + if (response.status === 401 || initialResult?.code === "InvalidApiKey") { + throw new ExtendServiceError( + "图像拓展服务鉴权失败,请在AI模型设置中更新阿里云官方生图Key", + 503, + detail, + ); + } throw new ExtendServiceError( "图像拓展API错误", - 500, - initialResult.message || JSON.stringify(initialResult), + 502, + detail, ); } - const taskId = initialResult.output.task_id; + const taskId = initialResult?.output?.task_id; + if (!taskId) { + throw new ExtendServiceError("图像拓展API未返回任务ID", 502); + } + console.info("[Extend Service] DashScope task accepted", { + requestId, + taskId, + xScale, + yScale, + }); const taskStatusUrl = `https://dashscope.aliyuncs.com/api/v1/tasks/${taskId}`; await new Promise((resolve) => setTimeout(resolve, 3000)); @@ -405,11 +428,23 @@ async function extendImage( Authorization: `Bearer ${config.apiKey}`, }, }); - const result = await statusResponse.json(); - if (result.output?.task_status === "SUCCEEDED") { + const result = await statusResponse.json().catch(() => null); + if (!statusResponse.ok || result?.code) { + throw new ExtendServiceError( + "查询图像拓展任务失败", + 502, + result?.message || + result?.code || + `${statusResponse.status} ${statusResponse.statusText}`, + ); + } + if (result?.output?.task_status === "SUCCEEDED") { + if (!result.output.output_image_url) { + throw new ExtendServiceError("图像拓展任务未返回图片", 502); + } return result.output.output_image_url; } - if (result.output?.task_status === "FAILED") { + if (result?.output?.task_status === "FAILED") { throw new ExtendServiceError( "图像拓展任务失败", 500, @@ -434,15 +469,28 @@ export function inferExtendOptionsFromText(text: string): Pick { const extendConfig = await getSystemSetting({ key: "extend_api_config" }); - if (!extendConfig || !extendConfig.value) { - throw new ExtendServiceError("图像拓展API配置未设置,请在系统设置中配置", 400); + const config = (extendConfig?.value || {}) as ExtendConfig; + if (config.provider === "vip98k") { + return config; } - const config = extendConfig.value as ExtendConfig; - if (config.provider !== "vip98k" && !config.apiKey) { - throw new ExtendServiceError("图像拓展API密钥未配置", 400); + const alibabaProviders = await getAiProvidersByType({ + type: "alibaba-image", + enabledOnly: true, + }); + const apiKey = resolveAlibabaExtendApiKey(alibabaProviders, config.apiKey); + if (!apiKey) { + throw new ExtendServiceError( + "图像拓展API密钥未配置,请在AI模型设置中配置阿里云官方生图服务商", + 400, + ); } - return config; + + return { + ...config, + provider: "dashscope", + apiKey, + }; } export async function executeExtendJob( @@ -484,6 +532,7 @@ export async function executeExtendJob( config, xScale, yScale, + requestId, ); const imageResponse = await fetch(extendedImageUrl); if (!imageResponse.ok) { diff --git a/playwright.helpers.config.ts b/playwright.helpers.config.ts index 6b26eb94d..e03170b42 100644 --- a/playwright.helpers.config.ts +++ b/playwright.helpers.config.ts @@ -33,6 +33,7 @@ export default defineConfig({ /routes\/openapi-access-key\.test\.ts/, /routes\/vip98k-reference-routing\.test\.ts/, /routes\/ordinal-image-references\.test\.ts/, + /routes\/extend-provider-config\.test\.ts/, ], fullyParallel: true, forbidOnly: !!process.env.CI, diff --git a/tests/routes/extend-provider-config.test.ts b/tests/routes/extend-provider-config.test.ts new file mode 100644 index 000000000..24ff7c2b3 --- /dev/null +++ b/tests/routes/extend-provider-config.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "@playwright/test"; +import { resolveAlibabaExtendApiKey } from "@/lib/ai/extend/config"; + +test.describe("image extension provider configuration", () => { + test("uses the most recently updated Alibaba image provider key", () => { + expect( + resolveAlibabaExtendApiKey( + [ + { apiKey: "older-key", updatedAt: "2026-09-01T00:00:00.000Z" }, + { apiKey: "current-key", updatedAt: "2026-09-10T00:00:00.000Z" }, + ], + "stale-extend-setting-key", + ), + ).toBe("current-key"); + }); + + test("only falls back to a legacy key when no provider key exists", () => { + expect( + resolveAlibabaExtendApiKey( + [{ apiKey: "", updatedAt: "2026-09-10T00:00:00.000Z" }], + "legacy-key", + ), + ).toBe("legacy-key"); + expect(resolveAlibabaExtendApiKey([], "")).toBeNull(); + }); +});