feat: 补充注释

This commit is contained in:
2026-04-26 01:13:38 +08:00
parent e26e2f5491
commit 4c58f409e9
23 changed files with 206 additions and 95 deletions
+9 -26
View File
@@ -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
View File
@@ -1,3 +1,4 @@
// server/utils/fetch.ts - NewAPI 请求封装:集中维护 baseURL、raw fetch 和服务端鉴权请求。
import type { H3Event } from "h3";
import { createError, getCookie } from "h3";
import {
+21 -3
View File
@@ -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
View File
@@ -1,3 +1,4 @@
// server/utils/index.ts - 服务端工具统一导出口,方便 API handler 从同一入口导入。
export * from "./createApiResponse";
export * from "./fetch";
export * from "./imageGenerationRecords";
+3
View File
@@ -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;
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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";
+3
View File
@@ -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(/^\//, ""));