- setExtendConfig({ ...extendConfig, apiKey: e.target.value })
- }
- placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
- type="password"
- value={extendConfig.apiKey}
- />
-
@@ -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();
+ });
+});