feat: 补充注释
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
// server/utils/createApiResponse.ts - 统一封装后端接口成功/失败响应,并把内部异常转换为安全前端文案。
|
||||
import type { ICommonResponse } from "#shared/types";
|
||||
|
||||
const INTERNAL_SERVER_ERROR_MESSAGE = "服务器内部错误";
|
||||
|
||||
/**
|
||||
* 构造统一成功响应。
|
||||
*
|
||||
@@ -39,37 +42,17 @@ export const createErrorResponse = (
|
||||
};
|
||||
|
||||
/**
|
||||
* 将上游请求错误转换为统一响应。
|
||||
* 将上游或内部异常转换为统一响应。
|
||||
*
|
||||
* @param error ofetch 抛出的错误对象
|
||||
* @param fallbackMessage 当上游没有提供可用错误信息时的兜底提示
|
||||
* @returns 符合 ICommonResponse 格式的错误响应
|
||||
* 调用方必须先在服务端日志中记录 error。这里永远不把上游 message、响应体、
|
||||
* statusMessage 或 data 返回给前端,避免暴露内部服务、供应商、token 使用细节。
|
||||
*/
|
||||
export const createUpstreamErrorResponse = (
|
||||
error: unknown,
|
||||
fallbackMessage: string = "请求失败"
|
||||
_fallbackMessage: string = INTERNAL_SERVER_ERROR_MESSAGE
|
||||
): ICommonResponse => {
|
||||
const fetchError = error as {
|
||||
data?: unknown;
|
||||
message?: string;
|
||||
response?: {
|
||||
_data?: unknown;
|
||||
status?: number;
|
||||
};
|
||||
statusCode?: number;
|
||||
};
|
||||
|
||||
const errorData = fetchError.data ?? fetchError.response?._data ?? null;
|
||||
const errorMessage =
|
||||
typeof errorData === "string"
|
||||
? errorData
|
||||
: fetchError.message || fallbackMessage;
|
||||
|
||||
return createErrorResponse(
|
||||
fetchError.statusCode ?? fetchError.response?.status ?? 500,
|
||||
errorMessage,
|
||||
errorData
|
||||
);
|
||||
void error;
|
||||
return createErrorResponse(500, INTERNAL_SERVER_ERROR_MESSAGE);
|
||||
};
|
||||
|
||||
/** 判断上游或本地鉴权错误是否为 401,用于统一清理失效登录态 */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/fetch.ts - NewAPI 请求封装:集中维护 baseURL、raw fetch 和服务端鉴权请求。
|
||||
import type { H3Event } from "h3";
|
||||
import { createError, getCookie } from "h3";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/imageGenerationRecords.ts - 生图相关数据库操作:用户快照、生成记录、历史查询和统计。
|
||||
import type { IUserBasicData } from "#shared/types";
|
||||
import type {
|
||||
IImageGenerationStatsData,
|
||||
@@ -238,8 +239,8 @@ export const getImageGenerationDetail = async (
|
||||
return {
|
||||
...mapImageGenerationItem(record),
|
||||
imageMimeType: record.imageMimeType,
|
||||
upstreamResponse: record.upstreamResponse,
|
||||
hostedResponse: record.hostedResponse
|
||||
upstreamResponse: null,
|
||||
hostedResponse: null
|
||||
};
|
||||
};
|
||||
|
||||
@@ -295,6 +296,7 @@ const upsertGenerationStats = (update: Prisma.GenerationStatsUpdateInput) => {
|
||||
});
|
||||
};
|
||||
|
||||
/** 更新统计失败不能影响主链路,避免统计表问题导致生图接口失败 */
|
||||
const safeUpsertGenerationStats = async (
|
||||
update: Prisma.GenerationStatsUpdateInput
|
||||
) => {
|
||||
@@ -307,6 +309,7 @@ const safeUpsertGenerationStats = async (
|
||||
}
|
||||
};
|
||||
|
||||
/** upsert 创建统计行时,把本次 increment 转换为初始计数 */
|
||||
const buildStatsCreateInput = (update: Prisma.GenerationStatsUpdateInput) => {
|
||||
return {
|
||||
totalRequests: getIncrementValue(update.totalRequests),
|
||||
@@ -318,6 +321,7 @@ const buildStatsCreateInput = (update: Prisma.GenerationStatsUpdateInput) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 从 Prisma increment 操作里取出增量;非 increment 操作在创建时按 0 处理 */
|
||||
const getIncrementValue = (value: unknown): number => {
|
||||
if (
|
||||
value &&
|
||||
@@ -331,6 +335,7 @@ const getIncrementValue = (value: unknown): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
/** 将 Prisma 记录转换为前端类型,BigInt/Date 在这里统一序列化 */
|
||||
const mapImageGenerationItem = (record: {
|
||||
id: bigint;
|
||||
userId: number;
|
||||
@@ -360,17 +365,30 @@ const mapImageGenerationItem = (record: {
|
||||
imageUrl: record.imageUrl,
|
||||
hostedImageUrl: record.hostedImageUrl,
|
||||
revisedPrompt: record.revisedPrompt,
|
||||
errorMessage: record.errorMessage,
|
||||
errorMessage: getPublicRecordMessage(record.status, record.errorMessage),
|
||||
createdAt: record.createdAt.toISOString()
|
||||
};
|
||||
};
|
||||
|
||||
/** 计算耗时并兜底为非负数,避免系统时间抖动导致负值 */
|
||||
const getDurationMs = (startedAt: Date, endedAt: Date) => {
|
||||
return Math.max(0, endedAt.getTime() - startedAt.getTime());
|
||||
};
|
||||
|
||||
/** 保存到数据库的错误信息只保留可读摘要,不保存复杂错误对象 */
|
||||
const getSafeErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) return error.message;
|
||||
if (typeof error === "string") return error;
|
||||
return "图片生成失败";
|
||||
};
|
||||
|
||||
/** 返回给前端的记录错误只保留本地泛化文案,具体内部错误留在数据库和服务端日志 */
|
||||
const getPublicRecordMessage = (
|
||||
status: ImageGenerationStatus,
|
||||
errorMessage: string | null
|
||||
): string | null => {
|
||||
if (!errorMessage) return null;
|
||||
return status === ImageGenerationStatus.SUCCEEDED
|
||||
? "图片归档失败"
|
||||
: "图片生成失败";
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/index.ts - 服务端工具统一导出口,方便 API handler 从同一入口导入。
|
||||
export * from "./createApiResponse";
|
||||
export * from "./fetch";
|
||||
export * from "./imageGenerationRecords";
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// server/utils/logging.ts - API 调试日志工具:为每次请求生成 requestId 并输出安全错误摘要。
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { consola } from "consola";
|
||||
|
||||
type LogMeta = Record<string, unknown>;
|
||||
|
||||
/** 创建带 requestId、scope 和耗时统计的接口日志器 */
|
||||
export const createApiLogger = (scope: string) => {
|
||||
const requestId = randomUUID();
|
||||
const startedAt = Date.now();
|
||||
@@ -36,6 +38,7 @@ export const createApiLogger = (scope: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 将未知错误转换为可写入日志的安全摘要,避免把 cookie/key 等响应体细节打进日志 */
|
||||
export const toSafeLogError = (error: unknown) => {
|
||||
const maybeError = error as {
|
||||
message?: string;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/lsky.ts - Lsky 图床上传工具:下载上游图片、重命名、打标签并上传归档。
|
||||
import { createError } from "h3";
|
||||
|
||||
interface ILskyUploadInput {
|
||||
@@ -29,6 +30,14 @@ export interface ILskyUploadedImage {
|
||||
response: ILskyUploadResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程图片 URL 下载图片并上传到 Lsky。
|
||||
*
|
||||
* 注意:
|
||||
* - Lsky 地址、Token、storage_id 都来自环境变量,代码不写死密钥。
|
||||
* - 文件名包含 userId、username、时间戳、recordId,方便图床侧追踪来源。
|
||||
* - 返回完整 Lsky 响应供数据库保存,但调用方不要把 token 写入日志。
|
||||
*/
|
||||
export const uploadImageFromUrl = async (
|
||||
input: ILskyUploadInput
|
||||
): Promise<ILskyUploadedImage> => {
|
||||
@@ -85,6 +94,7 @@ export const uploadImageFromUrl = async (
|
||||
};
|
||||
};
|
||||
|
||||
/** 读取并校验 Lsky 环境变量配置 */
|
||||
const getLskyConfig = () => {
|
||||
const baseUrl = process.env.LSKY_BASE_URL?.replace(/\/+$/, "");
|
||||
const token = process.env.LSKY_TOKEN;
|
||||
@@ -109,6 +119,7 @@ const getLskyConfig = () => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 下载上游图片二进制,保留 content-type 供上传和入库使用 */
|
||||
const downloadImage = async (imageUrl: string) => {
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
@@ -121,6 +132,7 @@ const downloadImage = async (imageUrl: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
/** 构造图床文件名:userId_username_timestamp_recordId.ext */
|
||||
const buildArchiveFilename = ({
|
||||
userId,
|
||||
username,
|
||||
@@ -139,6 +151,7 @@ const buildArchiveFilename = ({
|
||||
return `${userId}_${safeUsername}_${timestamp}_${recordId.toString()}.${extension}`;
|
||||
};
|
||||
|
||||
/** 给图片打固定标签,便于后续在图床里按项目、用户、记录和模型筛选 */
|
||||
const buildArchiveTags = (userId: number, recordId: bigint) => {
|
||||
return [
|
||||
"AIArtStudio",
|
||||
@@ -148,6 +161,7 @@ const buildArchiveTags = (userId: number, recordId: bigint) => {
|
||||
];
|
||||
};
|
||||
|
||||
/** 清理文件名片段,避免中文/英文/数字/下划线/短横线以外的字符影响上传 */
|
||||
const sanitizeFilenamePart = (value: string) => {
|
||||
const sanitized = value
|
||||
.trim()
|
||||
@@ -158,10 +172,12 @@ const sanitizeFilenamePart = (value: string) => {
|
||||
return sanitized || "user";
|
||||
};
|
||||
|
||||
/** 文件名使用紧凑 UTC 时间戳,避免冒号等字符影响跨平台兼容性 */
|
||||
const formatTimestamp = (date: Date) => {
|
||||
return date.toISOString().replace(/\D/g, "").slice(0, 14);
|
||||
};
|
||||
|
||||
/** 优先按 MIME 推断扩展名,MIME 不认识时再从 URL 路径兜底 */
|
||||
const getImageExtension = (mimeType: string, imageUrl: string) => {
|
||||
const fromMimeType = mimeType.split(";")[0]?.trim().toLowerCase();
|
||||
if (fromMimeType === "image/jpeg") return "jpg";
|
||||
@@ -174,6 +190,7 @@ const getImageExtension = (mimeType: string, imageUrl: string) => {
|
||||
return extension && /^[a-z0-9]+$/.test(extension) ? extension : "png";
|
||||
};
|
||||
|
||||
/** 兼容 Lsky 可能返回的布尔或字符串成功状态 */
|
||||
const isSuccessStatus = (status: unknown) => {
|
||||
return status === true || status === "success" || status === "ok";
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/newApiAuthCookies.ts - NewAPI 登录态 cookie 工具:提取上游 session 并用 httpOnly 保存。
|
||||
import type { H3Event } from "h3";
|
||||
import { createError, deleteCookie, getCookie, setCookie } from "h3";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/newApiTokens.ts - NewAPI Token 工具:准备 AIArtStudio Token 并在服务端读取完整 key。
|
||||
import type { H3Event } from "h3";
|
||||
import { createError } from "h3";
|
||||
import type { IUserReadyData } from "#shared/types";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// server/utils/openai.ts
|
||||
// server/utils/openai.ts - OpenAI/NewAPI 调用工具:文本、视觉、Responses 流式和 Chat Completions 流式生图。
|
||||
import OpenAI from "openai";
|
||||
import type { BaseOptions, IImageGenerateData } from "#shared/types/openai";
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// server/utils/prisma.ts - Prisma 客户端初始化:使用 MariaDB adapter 并在开发热更新中复用连接池。
|
||||
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
|
||||
import { PrismaClient } from "~~/app/generated/prisma/client";
|
||||
|
||||
@@ -16,6 +17,7 @@ type MariaDbPoolConfig = Exclude<
|
||||
string
|
||||
>;
|
||||
|
||||
/** 从 DATABASE_URL 查询参数读取连接池配置,非法值回退到默认值 */
|
||||
function getNumberParam(url: URL, name: string, fallback: number) {
|
||||
const value = url.searchParams.get(name);
|
||||
if (!value) {
|
||||
@@ -26,6 +28,7 @@ function getNumberParam(url: URL, name: string, fallback: number) {
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
/** 将 DATABASE_URL 拆成 Prisma MariaDB adapter 需要的连接池配置 */
|
||||
function createMariaDbConfig(urlString: string): MariaDbPoolConfig {
|
||||
const url = new URL(urlString);
|
||||
const database = decodeURIComponent(url.pathname.replace(/^\//, ""));
|
||||
|
||||
Reference in New Issue
Block a user