feat: 移除图片生成记录中的尺寸字段,调整归档逻辑以支持异步处理
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
- 所有返回给前端的 `msg` 必须使用本地业务表达,不提及“上游”“NewAPI”“Lsky”“供应商”“token”“key”等内部来源;内部异常统一返回“服务器内部错误”。
|
- 所有返回给前端的 `msg` 必须使用本地业务表达,不提及“上游”“NewAPI”“Lsky”“供应商”“token”“key”等内部来源;内部异常统一返回“服务器内部错误”。
|
||||||
- 注释和日志要解释意图、边界和排查信息,不输出密码、cookie、完整 key、token、图片二进制或完整 prompt。
|
- 注释和日志要解释意图、边界和排查信息,不输出密码、cookie、完整 key、token、图片二进制或完整 prompt。
|
||||||
- 已存在的说明性注释不要随意删除,尤其是 `server/utils/openai.ts` 里的模型调用、视觉输入、流式解析等上下文注释。
|
- 已存在的说明性注释不要随意删除,尤其是 `server/utils/openai.ts` 里的模型调用、视觉输入、流式解析等上下文注释。
|
||||||
|
- 不要写任何可能阻塞整个进程的代码。
|
||||||
|
|
||||||
## API 路由规范
|
## API 路由规范
|
||||||
|
|
||||||
@@ -59,17 +60,21 @@
|
|||||||
|
|
||||||
## 生图、图床与历史记录
|
## 生图、图床与历史记录
|
||||||
|
|
||||||
- `POST /api/images/generate` 当前同步等待生图完成后返回,不向前端透传流式进度。
|
- `POST /api/images/generate` 当前同步等待生图完成后立即返回,不向前端透传流式进度,也不等待图床归档完成。
|
||||||
- 生图成功后返回:
|
- 生图成功后返回:
|
||||||
- `imageUrl`:生成图片访问地址,前端当前优先展示。
|
- `imageUrl`:生成图片访问地址,前端当前优先展示。
|
||||||
- `hostedImageUrl`:Lsky 图床归档后的 URL,归档失败时为 `null`。
|
|
||||||
- `revisedPrompt`:流式 chat completions 当前没有等价字段,通常为空。
|
- `revisedPrompt`:流式 chat completions 当前没有等价字段,通常为空。
|
||||||
|
- 生图主链路先把记录落库为 `SUCCEEDED`,并清空 `hostedImageUrl`、`hostedResponse`、`imageMimeType`、`errorMessage`;之后再由响应后的后台任务异步上传 Lsky。
|
||||||
|
- 历史记录中的图床归档状态约定:
|
||||||
|
- `status === "SUCCEEDED"` 且 `hostedImageUrl === null` 且 `errorMessage === null`:表示图片已生成、仍在归档中。
|
||||||
|
- `status === "SUCCEEDED"` 且 `hostedImageUrl === null` 且 `errorMessage !== null`:表示归档失败。
|
||||||
|
- `hostedImageUrl !== null`:表示归档成功。
|
||||||
- Lsky 归档逻辑集中在 `server/utils/lsky.ts`:
|
- Lsky 归档逻辑集中在 `server/utils/lsky.ts`:
|
||||||
- 配置从 `LSKY_BASE_URL`、`LSKY_TOKEN`、`LSKY_STORAGE_ID` 读取。
|
- 配置从 `LSKY_BASE_URL`、`LSKY_TOKEN`、`LSKY_STORAGE_ID` 读取。
|
||||||
- 上传时先下载上游图片,再用 `multipart/form-data` 调 Lsky `/upload`。
|
- 上传时先下载上游图片,再用 `multipart/form-data` 调 Lsky `/upload`。
|
||||||
- 文件名格式为 `userId_username_timestamp_recordId.ext`。
|
- 文件名格式为 `userId_username_timestamp_recordId.ext`。
|
||||||
- tags 固定包含 `AIArtStudio`、`user:{userId}`、`record:{recordId}`、`model:gpt-image-2`。
|
- tags 固定包含 `AIArtStudio`、`user:{userId}`、`record:{recordId}`、`model:gpt-image-2`。
|
||||||
- 图床上传失败不影响本次生图成功:接口仍返回 `imageUrl`,数据库记录归档失败提示;前端可见文案只能说“图片归档失败”等本地描述。
|
- 图床上传失败不影响本次生图成功:接口仍立即返回 `imageUrl`,数据库记录归档失败提示;前端可见文案只能说“图片归档失败”等本地描述。
|
||||||
- 数据库不保存图片 base64;不要恢复 `image_base64` 或类似大文本存图方案。
|
- 数据库不保存图片 base64;不要恢复 `image_base64` 或类似大文本存图方案。
|
||||||
- 历史列表和详情都不向前端返回完整上游响应、图床响应或内部错误详情;这些内容可以入库供服务端排查,但 API 响应应固定隐藏或置空。
|
- 历史列表和详情都不向前端返回完整上游响应、图床响应或内部错误详情;这些内容可以入库供服务端排查,但 API 响应应固定隐藏或置空。
|
||||||
|
|
||||||
@@ -82,10 +87,13 @@
|
|||||||
- 登录或 `/api/auth/me` 成功后会 upsert `User` 快照,主键使用 NewAPI 用户 id。
|
- 登录或 `/api/auth/me` 成功后会 upsert `User` 快照,主键使用 NewAPI 用户 id。
|
||||||
- 生图记录写入 `ImageGeneration`:
|
- 生图记录写入 `ImageGeneration`:
|
||||||
- 开始时写 `RUNNING`。
|
- 开始时写 `RUNNING`。
|
||||||
- 成功时写 `SUCCEEDED`、上游 URL、图床 URL、MIME、完整上游响应、完整图床响应、耗时。
|
- 生成成功时先写 `SUCCEEDED`、上游 URL、完整上游响应、耗时。
|
||||||
|
- 后台归档成功后再补写图床 URL、MIME、完整图床响应。
|
||||||
|
- 后台归档失败时只补写本地错误文案,不把记录改回失败。
|
||||||
- 失败时写 `FAILED`、错误信息、耗时。
|
- 失败时写 `FAILED`、错误信息、耗时。
|
||||||
- 删除历史采用软删除 `deletedAt`。
|
- 删除历史采用软删除 `deletedAt`。
|
||||||
- `GenerationStats` 使用固定 id `global` 记录全局统计。
|
- `GenerationStats` 使用固定 id `global` 记录全局统计。
|
||||||
|
- `runningRequests` 只表示生图进行中,不包含图床归档阶段。
|
||||||
- 统计更新失败只记录日志,不应阻断用户拿到已经生成的图片。
|
- 统计更新失败只记录日志,不应阻断用户拿到已经生成的图片。
|
||||||
- 需要变更数据库结构时,同时维护 Prisma migration 和 `create_tables.sql`。
|
- 需要变更数据库结构时,同时维护 Prisma migration 和 `create_tables.sql`。
|
||||||
|
|
||||||
@@ -100,8 +108,8 @@
|
|||||||
- `create_running_record`
|
- `create_running_record`
|
||||||
- `get_api_key`
|
- `get_api_key`
|
||||||
- `call_image_stream_api`
|
- `call_image_stream_api`
|
||||||
- `upload_lsky`
|
|
||||||
- `finish_success_record`
|
- `finish_success_record`
|
||||||
|
- 图床归档改为响应后的后台日志阶段,继续打印 `recordId`、归档是否成功、失败摘要等信息,但不影响主请求返回。
|
||||||
- 不记录密码、cookie、完整 API key、Lsky token、完整 prompt、图片二进制、base64。
|
- 不记录密码、cookie、完整 API key、Lsky token、完整 prompt、图片二进制、base64。
|
||||||
- 全局请求日志中间件只记录 method、path、status、耗时,不记录请求体和响应体。
|
- 全局请求日志中间件只记录 method、path、status、耗时,不记录请求体和响应体。
|
||||||
|
|
||||||
@@ -131,7 +139,7 @@
|
|||||||
- 用户服务在 `app/services/user_service.ts`,图片服务在 `app/services/image_service.ts`。
|
- 用户服务在 `app/services/user_service.ts`,图片服务在 `app/services/image_service.ts`。
|
||||||
- 登录成功后可以调用 `UserReady()` 准备环境;`/api/auth/me` 恢复登录状态时不自动调用 ready。
|
- 登录成功后可以调用 `UserReady()` 准备环境;`/api/auth/me` 恢复登录状态时不自动调用 ready。
|
||||||
- 当前首页生图组件为 `app/components/ImageGenerateCom.vue`,优先展示响应中的 `imageUrl`。
|
- 当前首页生图组件为 `app/components/ImageGenerateCom.vue`,优先展示响应中的 `imageUrl`。
|
||||||
- `hostedImageUrl` 作为图床归档地址返回,后续需要切换展示来源时再调整前端策略。
|
- `hostedImageUrl` 不在生成接口中返回,只在历史记录中作为归档结果字段保留;后续需要切换展示来源时再调整前端策略。
|
||||||
|
|
||||||
## 代码风格
|
## 代码风格
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ CREATE TABLE IF NOT EXISTS `image_generations` (
|
|||||||
`prompt` TEXT NOT NULL COMMENT '用户输入的生图提示词',
|
`prompt` TEXT NOT NULL COMMENT '用户输入的生图提示词',
|
||||||
`status` ENUM('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED') NOT NULL DEFAULT 'RUNNING' COMMENT '生图状态',
|
`status` ENUM('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED') NOT NULL DEFAULT 'RUNNING' COMMENT '生图状态',
|
||||||
`model` VARCHAR(64) NOT NULL DEFAULT 'gpt-image-2' COMMENT '生图模型',
|
`model` VARCHAR(64) NOT NULL DEFAULT 'gpt-image-2' COMMENT '生图模型',
|
||||||
`size` VARCHAR(32) NOT NULL DEFAULT '1024x1024' COMMENT '图片尺寸',
|
|
||||||
`started_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '生图开始时间',
|
`started_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '生图开始时间',
|
||||||
`ended_at` DATETIME(3) NULL COMMENT '生图结束时间',
|
`ended_at` DATETIME(3) NULL COMMENT '生图结束时间',
|
||||||
`duration_ms` INT NULL COMMENT '生图耗时,单位毫秒',
|
`duration_ms` INT NULL COMMENT '生图耗时,单位毫秒',
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `image_generations`
|
||||||
|
DROP COLUMN `size`;
|
||||||
@@ -39,7 +39,6 @@ model ImageGeneration {
|
|||||||
prompt String @db.Text
|
prompt String @db.Text
|
||||||
status ImageGenerationStatus @default(RUNNING)
|
status ImageGenerationStatus @default(RUNNING)
|
||||||
model String @default("gpt-image-2") @db.VarChar(64)
|
model String @default("gpt-image-2") @db.VarChar(64)
|
||||||
size String @default("1024x1024") @db.VarChar(32)
|
|
||||||
startedAt DateTime @default(now()) @map("started_at")
|
startedAt DateTime @default(now()) @map("started_at")
|
||||||
endedAt DateTime? @map("ended_at")
|
endedAt DateTime? @map("ended_at")
|
||||||
durationMs Int? @map("duration_ms")
|
durationMs Int? @map("duration_ms")
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
// server/api/images/generate.post.ts - 图片生成接口:创建记录、流式调用上游生图、归档图床并返回图片地址。
|
// server/api/images/generate.post.ts - 图片生成接口:创建记录、流式调用上游生图并在响应后异步归档图床。
|
||||||
|
import { setImmediate } from "node:timers";
|
||||||
import type {
|
import type {
|
||||||
IImageGenerateData,
|
IImageGenerateData,
|
||||||
IImageGenerateRequest
|
IImageGenerateRequest
|
||||||
} from "#shared/types/openai";
|
} from "#shared/types/openai";
|
||||||
|
import {
|
||||||
|
finishImageGenerationArchiveFailed,
|
||||||
|
finishImageGenerationArchiveSuccess
|
||||||
|
} from "~~/server/utils";
|
||||||
|
|
||||||
type ApiLogger = ReturnType<typeof createApiLogger>;
|
type ApiLogger = ReturnType<typeof createApiLogger>;
|
||||||
|
|
||||||
@@ -15,9 +20,9 @@ type ApiLogger = ReturnType<typeof createApiLogger>;
|
|||||||
* 3. 创建 RUNNING 生图记录,并递增全局请求/进行中统计。
|
* 3. 创建 RUNNING 生图记录,并递增全局请求/进行中统计。
|
||||||
* 4. 在服务端确保并读取 AIArtStudio 完整 key,完整 key 不返回前端。
|
* 4. 在服务端确保并读取 AIArtStudio 完整 key,完整 key 不返回前端。
|
||||||
* 5. 调用 Chat Completions 流式生图接口,累积 SSE delta content 并提取最终图片 URL。
|
* 5. 调用 Chat Completions 流式生图接口,累积 SSE delta content 并提取最终图片 URL。
|
||||||
* 6. 尝试把上游图片下载后上传到 Lsky 图床;图床失败不阻断本次生图成功。
|
* 6. 上游返回图片 URL 后立刻把生图结果落库为 SUCCEEDED,并马上返回前端。
|
||||||
* 7. 成功时写入上游 URL、图床 URL、完整上游响应、图床响应和耗时。
|
* 7. 响应返回后再用后台异步任务上传 Lsky;归档失败只补写记录和日志,不影响本次响应。
|
||||||
* 8. 失败时把记录标记为 FAILED;鉴权失败会清理本地登录态并返回 401。
|
* 8. 主链路失败时把记录标记为 FAILED;鉴权失败会清理本地登录态并返回 401。
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const logger = createApiLogger("images.generate");
|
const logger = createApiLogger("images.generate");
|
||||||
@@ -85,41 +90,32 @@ export default defineEventHandler(async (event) => {
|
|||||||
hasUsage: hasStreamUsage(result.upstreamResponse)
|
hasUsage: hasStreamUsage(result.upstreamResponse)
|
||||||
});
|
});
|
||||||
|
|
||||||
stage = "upload_lsky";
|
|
||||||
// 归档图床用于长期保存;失败时仍继续返回 NewAPI 上游图片 URL。
|
|
||||||
const archiveResult = await archiveGeneratedImage({
|
|
||||||
imageUrl: result.imageUrl,
|
|
||||||
userId,
|
|
||||||
recordId: record.id,
|
|
||||||
createdAt: record.startedAt,
|
|
||||||
logger
|
|
||||||
});
|
|
||||||
logger.info("图床归档完成", {
|
|
||||||
recordId: record.id.toString(),
|
|
||||||
hosted: Boolean(archiveResult.hostedImageUrl),
|
|
||||||
mimeType: archiveResult.imageMimeType
|
|
||||||
});
|
|
||||||
|
|
||||||
stage = "finish_success_record";
|
stage = "finish_success_record";
|
||||||
await finishImageGenerationSuccess(record.id, record.startedAt, {
|
await finishImageGenerationSuccess(record.id, record.startedAt, {
|
||||||
imageUrl: result.imageUrl,
|
imageUrl: result.imageUrl,
|
||||||
hostedImageUrl: archiveResult.hostedImageUrl,
|
|
||||||
imageMimeType: archiveResult.imageMimeType,
|
|
||||||
revisedPrompt: result.revisedPrompt,
|
revisedPrompt: result.revisedPrompt,
|
||||||
upstreamResponse: result.upstreamResponse,
|
upstreamResponse: result.upstreamResponse
|
||||||
hostedResponse: archiveResult.hostedResponse,
|
|
||||||
errorMessage: archiveResult.errorMessage
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.done("成功", {
|
logger.done("成功", {
|
||||||
recordId: record.id.toString(),
|
recordId: record.id.toString(),
|
||||||
hosted: Boolean(archiveResult.hostedImageUrl)
|
archiveScheduled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const finishedRecord = record;
|
||||||
|
setImmediate(() => {
|
||||||
|
void archiveGeneratedImageInBackground({
|
||||||
|
imageUrl: result.imageUrl,
|
||||||
|
userId,
|
||||||
|
recordId: finishedRecord.id,
|
||||||
|
createdAt: finishedRecord.startedAt,
|
||||||
|
logger
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return createSuccessResponse<IImageGenerateData>(
|
return createSuccessResponse<IImageGenerateData>(
|
||||||
{
|
{
|
||||||
imageUrl: result.imageUrl,
|
imageUrl: result.imageUrl,
|
||||||
hostedImageUrl: archiveResult.hostedImageUrl,
|
|
||||||
revisedPrompt: result.revisedPrompt
|
revisedPrompt: result.revisedPrompt
|
||||||
},
|
},
|
||||||
"图片生成成功"
|
"图片生成成功"
|
||||||
@@ -154,8 +150,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 将生成图上传到 Lsky,返回图床地址;归档失败时降级为空结果 */
|
/** 响应返回后异步归档图片,成功则补写图床字段,失败则补写本地错误文案 */
|
||||||
const archiveGeneratedImage = async ({
|
const archiveGeneratedImageInBackground = async ({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
userId,
|
userId,
|
||||||
recordId,
|
recordId,
|
||||||
@@ -167,14 +163,12 @@ const archiveGeneratedImage = async ({
|
|||||||
recordId: bigint;
|
recordId: bigint;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
logger: ApiLogger;
|
logger: ApiLogger;
|
||||||
}): Promise<{
|
}) => {
|
||||||
hostedImageUrl: string | null;
|
logger.info("后台归档开始", {
|
||||||
imageMimeType: string | null;
|
recordId: recordId.toString()
|
||||||
hostedResponse: unknown;
|
});
|
||||||
errorMessage: string | null;
|
|
||||||
}> => {
|
|
||||||
try {
|
try {
|
||||||
// 图床归档失败不能阻断本次生成结果,前端仍可使用上游图片 URL。
|
|
||||||
const identity = await getUserArchiveIdentity(userId);
|
const identity = await getUserArchiveIdentity(userId);
|
||||||
const uploaded = await uploadImageFromUrl({
|
const uploaded = await uploadImageFromUrl({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
@@ -184,24 +178,29 @@ const archiveGeneratedImage = async ({
|
|||||||
createdAt
|
createdAt
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
await finishImageGenerationArchiveSuccess(recordId, {
|
||||||
hostedImageUrl: uploaded.publicUrl,
|
hostedImageUrl: uploaded.publicUrl,
|
||||||
imageMimeType: uploaded.mimetype,
|
imageMimeType: uploaded.mimetype,
|
||||||
hostedResponse: uploaded.response,
|
hostedResponse: uploaded.response
|
||||||
errorMessage: null
|
});
|
||||||
};
|
|
||||||
|
logger.info("后台归档成功", {
|
||||||
|
recordId: recordId.toString(),
|
||||||
|
mimeType: uploaded.mimetype,
|
||||||
|
hosted: true
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("图床归档失败", {
|
logger.error("后台归档失败", {
|
||||||
recordId: recordId.toString(),
|
recordId: recordId.toString(),
|
||||||
error: toSafeLogError(error)
|
error: toSafeLogError(error)
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
await finishImageGenerationArchiveFailed(recordId).catch((recordError) => {
|
||||||
hostedImageUrl: null,
|
logger.error("更新归档失败记录失败", {
|
||||||
imageMimeType: null,
|
recordId: recordId.toString(),
|
||||||
hostedResponse: null,
|
error: toSafeLogError(recordError)
|
||||||
errorMessage: "图片生成成功,但图片归档失败"
|
});
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const MAX_PAGE_SIZE = 50;
|
|||||||
* 1. 从 httpOnly cookie 读取当前用户 ID。
|
* 1. 从 httpOnly cookie 读取当前用户 ID。
|
||||||
* 2. 读取 page/size 查询参数,并限制最大 pageSize,避免一次查太多。
|
* 2. 读取 page/size 查询参数,并限制最大 pageSize,避免一次查太多。
|
||||||
* 3. 只查询当前用户且未软删除的记录,按创建时间倒序返回。
|
* 3. 只查询当前用户且未软删除的记录,按创建时间倒序返回。
|
||||||
* 4. 列表不返回完整上游响应或图床响应,详情接口再返回这些调试数据。
|
* 4. 列表只返回页面展示所需字段,不返回内部响应或供应商调试数据。
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const logger = createApiLogger("images.history.list");
|
const logger = createApiLogger("images.history.list");
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// server/api/images/history/[id].get.ts - 生图历史详情接口:返回当前用户单条记录和完整响应。
|
// server/api/images/history/[id].get.ts - 生图历史详情接口:返回当前用户单条记录的展示字段与归档状态。
|
||||||
import type { IImageHistoryDetail } from "#shared/types/openai";
|
import type { IImageHistoryDetail } from "#shared/types/openai";
|
||||||
import {
|
import {
|
||||||
clearNewApiAuthCookies,
|
clearNewApiAuthCookies,
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
* 1. 从 httpOnly cookie 读取当前用户 ID。
|
* 1. 从 httpOnly cookie 读取当前用户 ID。
|
||||||
* 2. 校验路由参数 id 必须是数字,并转成 BigInt 查询。
|
* 2. 校验路由参数 id 必须是数字,并转成 BigInt 查询。
|
||||||
* 3. 只允许读取当前用户、未软删除的记录。
|
* 3. 只允许读取当前用户、未软删除的记录。
|
||||||
* 4. 返回详情时包含完整上游响应和 Lsky 响应,便于排查单次生图问题。
|
* 4. 详情接口继续隐藏内部响应,只返回页面展示和归档状态所需字段。
|
||||||
*/
|
*/
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const logger = createApiLogger("images.history.detail");
|
const logger = createApiLogger("images.history.detail");
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ import { prisma } from "~~/server/utils/prisma";
|
|||||||
|
|
||||||
const GLOBAL_STATS_ID = "global";
|
const GLOBAL_STATS_ID = "global";
|
||||||
const DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
const DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
||||||
const DEFAULT_IMAGE_SIZE = "1024x1024";
|
|
||||||
|
|
||||||
interface IFinishImageGenerationSuccessInput {
|
interface IFinishImageGenerationSuccessInput {
|
||||||
/** NewAPI 上游返回的生成图片 URL */
|
/** NewAPI 上游返回的生成图片 URL */
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
/** Lsky 图床归档后的图片 URL,归档失败时为空 */
|
|
||||||
hostedImageUrl: string | null;
|
|
||||||
/** 图片 MIME 类型,优先来自图床上传结果 */
|
|
||||||
imageMimeType: string | null;
|
|
||||||
/** 上游返回的修订提示词,流式生图通常为空 */
|
/** 上游返回的修订提示词,流式生图通常为空 */
|
||||||
revisedPrompt?: string | null;
|
revisedPrompt?: string | null;
|
||||||
/** 完整上游响应,当前为 chat completions 流式聚合对象 */
|
/** 完整上游响应,当前为 chat completions 流式聚合对象 */
|
||||||
upstreamResponse: unknown;
|
upstreamResponse: unknown;
|
||||||
/** 完整 Lsky 上传响应,归档失败时为空 */
|
}
|
||||||
|
|
||||||
|
interface IFinishImageGenerationArchiveSuccessInput {
|
||||||
|
/** Lsky 图床归档后的图片 URL */
|
||||||
|
hostedImageUrl: string;
|
||||||
|
/** 图片 MIME 类型,优先来自图床上传结果 */
|
||||||
|
imageMimeType: string | null;
|
||||||
|
/** 完整 Lsky 上传响应 */
|
||||||
hostedResponse: unknown;
|
hostedResponse: unknown;
|
||||||
/** 成功状态下的非阻断提示,例如图床归档失败 */
|
|
||||||
errorMessage?: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 登录或恢复登录时保存 NewAPI 用户快照 */
|
/** 登录或恢复登录时保存 NewAPI 用户快照 */
|
||||||
@@ -97,8 +97,7 @@ export const createRunningImageGeneration = async (
|
|||||||
userId,
|
userId,
|
||||||
prompt,
|
prompt,
|
||||||
status: ImageGenerationStatus.RUNNING,
|
status: ImageGenerationStatus.RUNNING,
|
||||||
model: DEFAULT_IMAGE_MODEL,
|
model: DEFAULT_IMAGE_MODEL
|
||||||
size: DEFAULT_IMAGE_SIZE
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,7 +113,7 @@ export const createRunningImageGeneration = async (
|
|||||||
return record;
|
return record;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 将生图记录标记为成功,并保存上游 URL、图床 URL、完整响应和耗时 */
|
/** 将生图记录标记为成功,并先写入上游结果;图床字段稍后由后台归档补写 */
|
||||||
export const finishImageGenerationSuccess = async (
|
export const finishImageGenerationSuccess = async (
|
||||||
recordId: bigint,
|
recordId: bigint,
|
||||||
startedAt: Date,
|
startedAt: Date,
|
||||||
@@ -131,15 +130,12 @@ export const finishImageGenerationSuccess = async (
|
|||||||
endedAt,
|
endedAt,
|
||||||
durationMs: getDurationMs(startedAt, endedAt),
|
durationMs: getDurationMs(startedAt, endedAt),
|
||||||
imageUrl: input.imageUrl,
|
imageUrl: input.imageUrl,
|
||||||
hostedImageUrl: input.hostedImageUrl,
|
hostedImageUrl: null,
|
||||||
imageMimeType: input.imageMimeType,
|
imageMimeType: null,
|
||||||
revisedPrompt: input.revisedPrompt || null,
|
revisedPrompt: input.revisedPrompt || null,
|
||||||
upstreamResponse: input.upstreamResponse as Prisma.InputJsonValue,
|
upstreamResponse: input.upstreamResponse as Prisma.InputJsonValue,
|
||||||
hostedResponse:
|
hostedResponse: Prisma.DbNull,
|
||||||
input.hostedResponse === null
|
errorMessage: null
|
||||||
? Prisma.DbNull
|
|
||||||
: (input.hostedResponse as Prisma.InputJsonValue),
|
|
||||||
errorMessage: input.errorMessage || null
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -186,6 +182,39 @@ export const finishImageGenerationFailed = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 后台归档成功后回写图床地址、MIME 和图床响应 */
|
||||||
|
export const finishImageGenerationArchiveSuccess = async (
|
||||||
|
recordId: bigint,
|
||||||
|
input: IFinishImageGenerationArchiveSuccessInput
|
||||||
|
) => {
|
||||||
|
await prisma.imageGeneration.update({
|
||||||
|
where: {
|
||||||
|
id: recordId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
hostedImageUrl: input.hostedImageUrl,
|
||||||
|
imageMimeType: input.imageMimeType,
|
||||||
|
hostedResponse: input.hostedResponse as Prisma.InputJsonValue,
|
||||||
|
errorMessage: null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 后台归档失败时仅补写本地错误文案,不影响已成功的生图状态 */
|
||||||
|
export const finishImageGenerationArchiveFailed = async (
|
||||||
|
recordId: bigint,
|
||||||
|
errorMessage: string = "图片归档失败"
|
||||||
|
) => {
|
||||||
|
await prisma.imageGeneration.update({
|
||||||
|
where: {
|
||||||
|
id: recordId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
errorMessage
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/** 查询当前用户未删除的生图历史列表,不返回完整上游/图床响应 */
|
/** 查询当前用户未删除的生图历史列表,不返回完整上游/图床响应 */
|
||||||
export const listImageGenerationHistory = async ({
|
export const listImageGenerationHistory = async ({
|
||||||
userId,
|
userId,
|
||||||
@@ -221,7 +250,7 @@ export const listImageGenerationHistory = async ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 查询当前用户单条生图历史详情,包含完整上游与图床响应 */
|
/** 查询当前用户单条生图历史详情,内部响应字段固定隐藏为 null */
|
||||||
export const getImageGenerationDetail = async (
|
export const getImageGenerationDetail = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
recordId: bigint
|
recordId: bigint
|
||||||
@@ -342,7 +371,6 @@ const mapImageGenerationItem = (record: {
|
|||||||
prompt: string;
|
prompt: string;
|
||||||
status: ImageGenerationStatus;
|
status: ImageGenerationStatus;
|
||||||
model: string;
|
model: string;
|
||||||
size: string;
|
|
||||||
startedAt: Date;
|
startedAt: Date;
|
||||||
endedAt: Date | null;
|
endedAt: Date | null;
|
||||||
durationMs: number | null;
|
durationMs: number | null;
|
||||||
@@ -358,7 +386,6 @@ const mapImageGenerationItem = (record: {
|
|||||||
prompt: record.prompt,
|
prompt: record.prompt,
|
||||||
status: record.status,
|
status: record.status,
|
||||||
model: record.model,
|
model: record.model,
|
||||||
size: record.size,
|
|
||||||
startedAt: record.startedAt.toISOString(),
|
startedAt: record.startedAt.toISOString(),
|
||||||
endedAt: record.endedAt?.toISOString() ?? null,
|
endedAt: record.endedAt?.toISOString() ?? null,
|
||||||
durationMs: record.durationMs,
|
durationMs: record.durationMs,
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ export interface IImageGenerateRequest {
|
|||||||
* 图片生成成功后返回给前端的数据。
|
* 图片生成成功后返回给前端的数据。
|
||||||
*/
|
*/
|
||||||
export interface IImageGenerateData {
|
export interface IImageGenerateData {
|
||||||
/** NewAPI 上游返回的图片访问地址,前端当前优先展示这个地址 */
|
/** 生成图片访问地址,前端当前优先展示这个地址 */
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
/** Lsky 图床归档后的图片访问地址,归档失败时为空 */
|
|
||||||
hostedImageUrl?: string | null;
|
|
||||||
/** 上游返回的修订提示词,可能为空 */
|
/** 上游返回的修订提示词,可能为空 */
|
||||||
revisedPrompt?: string;
|
revisedPrompt?: string;
|
||||||
}
|
}
|
||||||
@@ -50,8 +48,6 @@ export interface IImageHistoryItem {
|
|||||||
status: ImageGenerationStatus;
|
status: ImageGenerationStatus;
|
||||||
/** 生图模型 */
|
/** 生图模型 */
|
||||||
model: string;
|
model: string;
|
||||||
/** 图片尺寸 */
|
|
||||||
size: string;
|
|
||||||
/** 生图开始时间,ISO 字符串 */
|
/** 生图开始时间,ISO 字符串 */
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
/** 生图结束时间,ISO 字符串,未结束时为 null */
|
/** 生图结束时间,ISO 字符串,未结束时为 null */
|
||||||
@@ -60,11 +56,11 @@ export interface IImageHistoryItem {
|
|||||||
durationMs: number | null;
|
durationMs: number | null;
|
||||||
/** NewAPI 上游返回的图片访问地址 */
|
/** NewAPI 上游返回的图片访问地址 */
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
/** Lsky 图床归档后的图片访问地址 */
|
/** Lsky 图床归档后的图片访问地址;为空且没有错误提示时表示仍在归档中 */
|
||||||
hostedImageUrl: string | null;
|
hostedImageUrl: string | null;
|
||||||
/** 上游返回的修订提示词,可能为空 */
|
/** 上游返回的修订提示词,可能为空 */
|
||||||
revisedPrompt: string | null;
|
revisedPrompt: string | null;
|
||||||
/** 面向前端的本地泛化错误提示,不包含上游或内部服务细节 */
|
/** 面向前端的本地泛化错误提示;为空且 hostedImageUrl 也为空时表示仍在归档中 */
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
/** 记录创建时间,ISO 字符串 */
|
/** 记录创建时间,ISO 字符串 */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user