Files
aiartstudio/server/utils/newApiTokens.ts
T
2026-04-25 19:14:58 +08:00

196 lines
4.8 KiB
TypeScript

import type { H3Event } from "h3";
import { createError } from "h3";
import type { IUserReadyData } from "#shared/types";
import { newApiAuthedFetch } from "~~/server/utils/fetch";
const AI_ART_STUDIO_TOKEN_NAME = "AIArtStudio";
const TOKEN_PAGE_SIZE = 100;
interface INewApiTokenItem {
/** NewAPI Token ID,后续获取完整 key 时会用到 */
id: number;
/** Token 所属用户 ID */
user_id: number;
/** 列表接口返回的脱敏 key,不对前端透出 */
key: string;
/** Token 状态:1 为启用,2 为禁用 */
status: number;
/** Token 名称 */
name: string;
/** 软删除时间,null 表示未删除 */
DeletedAt: unknown;
}
interface INewApiTokenPageData {
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** Token 总数 */
total: number;
/** 当前页 Token 列表 */
items: INewApiTokenItem[];
}
interface INewApiWrappedResponse<T> {
/** 上游业务数据 */
data?: T | null;
/** 上游提示信息 */
message?: string;
/** 上游业务成功状态 */
success?: boolean;
}
interface INewApiTokenKeyData {
/** NewAPI 返回的完整 Token Key,仅允许服务端内部使用 */
key: string;
}
const AI_ART_STUDIO_TOKEN_PAYLOAD = {
remain_quota: 0,
remain_amount: 0,
expired_time: -1,
unlimited_quota: true,
model_limits_enabled: false,
model_limits: "",
cross_group_retry: false,
name: AI_ART_STUDIO_TOKEN_NAME,
group: "",
allow_ips: ""
};
/** 检查当前登录用户是否已有可用 AIArtStudio Key,没有则创建一个 */
export const ensureAiArtStudioToken = async (
event: H3Event
): Promise<IUserReadyData> => {
const token = await findActiveAiArtStudioToken(event);
if (token) {
return {
ready: true,
created: false
};
}
await createAiArtStudioToken(event);
return {
ready: true,
created: true
};
};
/** 确保 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 findActiveAiArtStudioToken = async (
event: H3Event
): Promise<INewApiTokenItem | null> => {
let page = 1;
let total = Number.POSITIVE_INFINITY;
while ((page - 1) * TOKEN_PAGE_SIZE < total) {
const response = await newApiAuthedFetch<
INewApiWrappedResponse<INewApiTokenPageData>
>(event, `/api/token/?p=${page}&size=${TOKEN_PAGE_SIZE}`, {
method: "GET"
});
const data = unwrapNewApiResponse(response, "环境检查失败");
const token = data.items.find(isActiveAiArtStudioToken);
if (token) {
return token;
}
total = data.total;
page += 1;
}
return null;
};
/** 创建默认 AIArtStudio Token,参数固定,避免 handler 内重复维护 */
const createAiArtStudioToken = async (event: H3Event) => {
const response = await newApiAuthedFetch<INewApiWrappedResponse<null>>(
event,
"/api/token/",
{
method: "POST",
body: AI_ART_STUDIO_TOKEN_PAYLOAD,
headers: {
"Content-Type": "application/json"
}
}
);
unwrapNewApiResponse(response, "环境初始化失败", true);
};
/** 判断 Token 是否满足本项目 ready 条件 */
const isActiveAiArtStudioToken = (token: INewApiTokenItem): boolean => {
return (
token.name === AI_ART_STUDIO_TOKEN_NAME &&
token.status === 1 &&
token.DeletedAt === null
);
};
/** 统一处理 NewAPI 的 { success, message, data } 外壳 */
const unwrapNewApiResponse = <T>(
response: INewApiWrappedResponse<T>,
fallbackMessage: string,
allowNullData: boolean = false
): T => {
if (response.success !== true) {
throw createError({
statusCode: 502,
statusMessage: response.message || fallbackMessage,
data: response
});
}
if (response.data === null || response.data === undefined) {
if (allowNullData) return response.data as T;
throw createError({
statusCode: 502,
statusMessage: fallbackMessage,
data: response
});
}
return response.data;
};