diff --git a/app/components/ImageGenerateCom.vue b/app/components/ImageGenerateCom.vue
new file mode 100644
index 0000000..31ddcf1
--- /dev/null
+++ b/app/components/ImageGenerateCom.vue
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+ 立即生成
+
+
+
+ {{ errorMessage }}
+
+
+
![生成图片]()
+
+
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 16a6d81..0190c2a 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,14 +1,11 @@
- 123
+
+
+
diff --git a/app/services/image_service.ts b/app/services/image_service.ts
new file mode 100644
index 0000000..c1483fc
--- /dev/null
+++ b/app/services/image_service.ts
@@ -0,0 +1,19 @@
+import type {
+ ICommonResponse,
+ IImageGenerateData,
+ IImageGenerateRequest
+} from "#shared/types";
+
+export class ImageService {
+ public static basePath = "/api/images";
+
+ public static GenerateImage(request: IImageGenerateRequest) {
+ return $fetch>(
+ `${ImageService.basePath}/generate`,
+ {
+ method: "POST",
+ body: request
+ }
+ );
+ }
+}
diff --git a/app/services/index.ts b/app/services/index.ts
index b23ced4..8c70732 100644
--- a/app/services/index.ts
+++ b/app/services/index.ts
@@ -1 +1,2 @@
+export * from "./image_service";
export * from "./user_service";
diff --git a/server/api/auth/ready.get.ts b/server/api/auth/ready.get.ts
index 09b53c9..3e3a43c 100644
--- a/server/api/auth/ready.get.ts
+++ b/server/api/auth/ready.get.ts
@@ -1,4 +1,5 @@
import type { IUserReadyData } from "#shared/types";
+import { isUnauthorizedError } from "~~/server/utils";
export default defineEventHandler(async (event) => {
try {
@@ -17,20 +18,3 @@ export default defineEventHandler(async (event) => {
return createUpstreamErrorResponse(error, "环境准备失败");
}
});
-
-/** 鉴权失败时清理本地 cookie,避免继续携带失效的 NewAPI 登录态 */
-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
- );
-};
diff --git a/server/api/images/generate.post.ts b/server/api/images/generate.post.ts
new file mode 100644
index 0000000..39a9098
--- /dev/null
+++ b/server/api/images/generate.post.ts
@@ -0,0 +1,44 @@
+import { consola } from "consola";
+
+import type {
+ IImageGenerateData,
+ IImageGenerateRequest
+} from "#shared/types/openai";
+import { isUnauthorizedError } from "~~/server/utils";
+
+export default defineEventHandler(async (event) => {
+ const requestBody = await readBody | null>(
+ event
+ );
+
+ // 只接受 JSON 对象,避免数组、字符串等无效 body 被用于生成请求。
+ if (
+ requestBody !== null &&
+ (typeof requestBody !== "object" || Array.isArray(requestBody))
+ ) {
+ return createErrorResponse(400, "请求体必须是 JSON 对象");
+ }
+
+ const prompt = requestBody?.prompt?.trim();
+ if (!prompt) {
+ return createErrorResponse(400, "请输入图片描述");
+ }
+
+ try {
+ const apiKey = await getAiArtStudioTokenKey(event);
+ const result = await askImg({
+ apiKey,
+ prompt
+ });
+
+ consola.info("图片生成成功", { prompt, imageUrl: result.imageUrl });
+ return createSuccessResponse(result, "图片生成成功");
+ } catch (error) {
+ if (isUnauthorizedError(error)) {
+ clearNewApiAuthCookies(event);
+ return createErrorResponse(401, "未登录");
+ }
+
+ return createUpstreamErrorResponse(error, "图片生成失败");
+ }
+});
diff --git a/server/utils/createApiResponse.ts b/server/utils/createApiResponse.ts
index 1f719d1..eb4d631 100644
--- a/server/utils/createApiResponse.ts
+++ b/server/utils/createApiResponse.ts
@@ -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
+ );
+};
diff --git a/server/utils/index.ts b/server/utils/index.ts
index f433a37..f72fe27 100644
--- a/server/utils/index.ts
+++ b/server/utils/index.ts
@@ -2,3 +2,4 @@ export * from "./createApiResponse";
export * from "./fetch";
export * from "./newApiAuthCookies";
export * from "./newApiTokens";
+export * from "./openai";
diff --git a/server/utils/newApiTokens.ts b/server/utils/newApiTokens.ts
index 1d04f54..c914d83 100644
--- a/server/utils/newApiTokens.ts
+++ b/server/utils/newApiTokens.ts
@@ -41,6 +41,11 @@ interface INewApiWrappedResponse {
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 => {
- 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 => {
+ 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
+ >(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 => {
+const findActiveAiArtStudioToken = async (
+ event: H3Event
+): Promise => {
let page = 1;
let total = Number.POSITIVE_INFINITY;
@@ -87,15 +129,16 @@ const hasActiveAiArtStudioToken = async (event: H3Event): Promise => {
});
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 内重复维护 */
diff --git a/server/utils/openai.ts b/server/utils/openai.ts
index 02be53b..c33c294 100644
--- a/server/utils/openai.ts
+++ b/server/utils/openai.ts
@@ -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 => {
+ const result = await $fetch(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 & {
diff --git a/shared/types/index.ts b/shared/types/index.ts
index 2382c8f..b5ae07d 100644
--- a/shared/types/index.ts
+++ b/shared/types/index.ts
@@ -17,4 +17,5 @@ export type ISuccessResponse = ICommonResponse & { code: 0 };
/** 语义化的错误响应类型 */
export type IErrorResponse = ICommonResponse & { code: number };
+export * from "./openai";
export * from "./user";
diff --git a/shared/types/openai.ts b/shared/types/openai.ts
index cfb123f..3a57d57 100644
--- a/shared/types/openai.ts
+++ b/shared/types/openai.ts
@@ -6,3 +6,21 @@ export type BaseOptions = {
/** 自定义 baseURL */
baseURL?: string;
};
+
+/**
+ * 图片生成请求体。
+ */
+export interface IImageGenerateRequest {
+ /** 图片提示词 */
+ prompt?: string;
+}
+
+/**
+ * 图片生成成功后返回给前端的数据。
+ */
+export interface IImageGenerateData {
+ /** 生成图片的访问地址 */
+ imageUrl: string;
+ /** 上游返回的修订提示词,可能为空 */
+ revisedPrompt?: string;
+}