fix: bind image extension to Alibaba provider key
This commit is contained in:
parent
821b8658a8
commit
f9d1b46d81
@ -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>
|
||||
|
||||
@ -275,7 +275,6 @@ export type EnhanceConfig = {
|
||||
|
||||
export type ExtendConfig = {
|
||||
provider: "vip98k" | "dashscope";
|
||||
apiKey: string;
|
||||
xScale: string;
|
||||
yScale: string;
|
||||
};
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
25
lib/ai/extend/config.ts
Normal 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;
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
26
tests/routes/extend-provider-config.test.ts
Normal file
26
tests/routes/extend-provider-config.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user