223845b0ed
Co-authored-by: Copilot <copilot@github.com>
196 lines
4.8 KiB
TypeScript
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;
|
|
};
|