feat: 对接生图功能

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-25 19:14:58 +08:00
parent 5cb72f8579
commit 223845b0ed
12 changed files with 268 additions and 30 deletions
+17
View File
@@ -71,3 +71,20 @@ export const createUpstreamErrorResponse = (
errorData
);
};
/** 判断上游或本地鉴权错误是否为 401,用于统一清理失效登录态 */
export const isUnauthorizedError = (error: unknown): boolean => {
const fetchError = error as {
response?: {
status?: number;
};
status?: number;
statusCode?: number;
};
return (
fetchError.statusCode === 401 ||
fetchError.status === 401 ||
fetchError.response?.status === 401
);
};
+1
View File
@@ -2,3 +2,4 @@ export * from "./createApiResponse";
export * from "./fetch";
export * from "./newApiAuthCookies";
export * from "./newApiTokens";
export * from "./openai";
+49 -6
View File
@@ -41,6 +41,11 @@ interface INewApiWrappedResponse<T> {
success?: boolean;
}
interface INewApiTokenKeyData {
/** NewAPI 返回的完整 Token Key,仅允许服务端内部使用 */
key: string;
}
const AI_ART_STUDIO_TOKEN_PAYLOAD = {
remain_quota: 0,
remain_amount: 0,
@@ -58,8 +63,8 @@ const AI_ART_STUDIO_TOKEN_PAYLOAD = {
export const ensureAiArtStudioToken = async (
event: H3Event
): Promise<IUserReadyData> => {
const hasToken = await hasActiveAiArtStudioToken(event);
if (hasToken) {
const token = await findActiveAiArtStudioToken(event);
if (token) {
return {
ready: true,
created: false
@@ -74,8 +79,45 @@ export const ensureAiArtStudioToken = async (
};
};
/** 确保 AIArtStudio Token 可用,并获取完整 key,完整 key 不允许透出到前端 */
export const getAiArtStudioTokenKey = async (
event: H3Event
): Promise<string> => {
let token = await findActiveAiArtStudioToken(event);
if (!token) {
await createAiArtStudioToken(event);
token = await findActiveAiArtStudioToken(event);
}
if (!token) {
throw createError({
statusCode: 502,
statusMessage: "环境初始化失败"
});
}
const response = await newApiAuthedFetch<
INewApiWrappedResponse<INewApiTokenKeyData>
>(event, `/api/token/${token.id}/key`, {
method: "POST"
});
const data = unwrapNewApiResponse(response, "环境读取失败");
if (typeof data.key !== "string" || !data.key) {
throw createError({
statusCode: 502,
statusMessage: "环境读取失败"
});
}
return data.key;
};
/** 分页读取 Token 列表,只认名称为 AIArtStudio 且状态启用的未删除 Token */
const hasActiveAiArtStudioToken = async (event: H3Event): Promise<boolean> => {
const findActiveAiArtStudioToken = async (
event: H3Event
): Promise<INewApiTokenItem | null> => {
let page = 1;
let total = Number.POSITIVE_INFINITY;
@@ -87,15 +129,16 @@ const hasActiveAiArtStudioToken = async (event: H3Event): Promise<boolean> => {
});
const data = unwrapNewApiResponse(response, "环境检查失败");
if (data.items.some(isActiveAiArtStudioToken)) {
return true;
const token = data.items.find(isActiveAiArtStudioToken);
if (token) {
return token;
}
total = data.total;
page += 1;
}
return false;
return null;
};
/** 创建默认 AIArtStudio Token,参数固定,避免 handler 内重复维护 */
+49 -1
View File
@@ -1,6 +1,22 @@
// server/utils/openai.ts
import OpenAI from "openai";
import type { BaseOptions } from "#shared/types/openai";
import type { BaseOptions, IImageGenerateData } from "#shared/types/openai";
const IMAGE_GENERATION_URL = "https://api.qflink.xyz/v1/images/generations";
const IMAGE_GENERATION_MODEL = "gpt-image-2";
const IMAGE_GENERATION_SIZE = "1024x1024";
interface IImageGenerationResponse {
/** 上游创建时间 */
created?: number;
/** 上游图片生成结果列表 */
data?: Array<{
/** 上游返回的修订提示词,可能为空 */
revised_prompt?: string;
/** 生成图片地址 */
url?: string;
}>;
}
/** 通用 AI 调用函数(支持文本 / 图文 / 多模态) */
export const askAI = async ({
@@ -84,6 +100,38 @@ export const askVision = async (
return res.output_text;
};
/** 调用图片生成接口,完整 API Key 只在服务端使用 */
export const askImg = async ({
apiKey,
prompt
}: {
apiKey: string;
prompt: string;
}): Promise<IImageGenerateData> => {
const result = await $fetch<IImageGenerationResponse>(IMAGE_GENERATION_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json"
},
body: {
model: IMAGE_GENERATION_MODEL,
prompt,
size: IMAGE_GENERATION_SIZE
}
});
const image = result.data?.[0];
if (!image?.url) {
throw new Error("图片生成失败");
}
return {
imageUrl: image.url,
revisedPrompt: image.revised_prompt
};
};
/** 调用流式接口 */
export const askStream = async (
options: BaseOptions & {