fix: bind image extension to Alibaba provider key

This commit is contained in:
134 2026-09-10 12:48:27 +08:00
parent 821b8658a8
commit f9d1b46d81
8 changed files with 131 additions and 49 deletions

View File

@ -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 服务商读取,仅在这里维护扩展比例。
</div>
) : (
<div className="space-y-2">
<Label htmlFor="extendApiKey">API密钥 *</Label>
<Input
id="extendApiKey"
onChange={(e) =>
setExtendConfig({ ...extendConfig, apiKey: e.target.value })
}
placeholder="sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
type="password"
value={extendConfig.apiKey}
/>
<div className="text-muted-foreground text-xs">
请输入阿里云Dashscope API密钥,例如:
sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
<div className="rounded-lg border bg-muted/40 p-4 text-sm">
使用阿里云 DashScope 的 image-out-painting 模型。API Key
从“AI 模型设置”中已启用的阿里云官方生图服务商读取,
仅在这里维护拓展比例。
</div>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
@ -2470,7 +2456,7 @@ export function LocalAiImagePipelineSections({
https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting
</p>
<p>• 支持的图片格式:JPG, PNG, WebP等常见格式</p>
<p>• 最大图片大小:4MB</p>
<p>• 最大图片大小:10MB</p>
<p>• 处理时间:约5-15秒/张</p>
<p>• 放大倍数范围:1.0-2.0(推荐1.5倍等比例扩展)</p>
</div>

View File

@ -275,7 +275,6 @@ export type EnhanceConfig = {
export type ExtendConfig = {
provider: "vip98k" | "dashscope";
apiKey: string;
xScale: string;
yScale: string;
};

View File

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

View File

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

25
lib/ai/extend/config.ts Normal file
View File

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

View File

@ -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<string> {
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<ExecuteExtendJobP
async function getValidatedExtendConfig(): Promise<ExtendConfig> {
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) {

View File

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

View File

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