1658 lines
38 KiB
TypeScript
1658 lines
38 KiB
TypeScript
import "server-only";
|
||
|
||
import {
|
||
and,
|
||
asc,
|
||
count,
|
||
desc,
|
||
eq,
|
||
gt,
|
||
gte,
|
||
inArray,
|
||
like,
|
||
lt,
|
||
or,
|
||
sql,
|
||
type SQL,
|
||
} from "drizzle-orm";
|
||
import { drizzle } from "drizzle-orm/postgres-js";
|
||
import postgres from "postgres";
|
||
import type { ArtifactKind } from "@/components/artifact";
|
||
import type { VisibilityType } from "@/components/visibility-selector";
|
||
import { ChatSDKError } from "../errors";
|
||
import type { AppUsage } from "../usage";
|
||
import { generateUUID } from "../utils";
|
||
import {
|
||
type Chat,
|
||
chat,
|
||
type DBMessage,
|
||
document,
|
||
message,
|
||
type Suggestion,
|
||
stream,
|
||
suggestion,
|
||
systemSettings,
|
||
toolConfig,
|
||
type ToolConfig,
|
||
type SystemSettings,
|
||
type User,
|
||
user,
|
||
vote,
|
||
aiProvider,
|
||
type AiProvider,
|
||
aiModel,
|
||
type AiModel,
|
||
storageConfig,
|
||
type StorageConfig,
|
||
siteSettings,
|
||
navLinks,
|
||
authSettings,
|
||
loginToken,
|
||
generatedImage,
|
||
} from "./schema";
|
||
import { generateHashedPassword } from "./utils";
|
||
|
||
// Optionally, if not using email/pass login, you can
|
||
// use the Drizzle adapter for Auth.js / NextAuth
|
||
// https://authjs.dev/reference/adapter/drizzle
|
||
|
||
// biome-ignore lint: Forbidden non-null assertion.
|
||
import { db } from "@/lib/db";
|
||
|
||
|
||
export async function getUser(email: string): Promise<User[]> {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(user)
|
||
.where(eq(user.email, email))
|
||
.orderBy(desc(user.createdAt));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get user by email"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function createUser(email: string, password: string) {
|
||
const hashedPassword = generateHashedPassword(password);
|
||
|
||
try {
|
||
return await db.insert(user).values({ email, password: hashedPassword });
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to create user");
|
||
}
|
||
}
|
||
|
||
export async function createGuestUser() {
|
||
const email = `guest-${Date.now()}`;
|
||
const password = generateHashedPassword(generateUUID());
|
||
|
||
try {
|
||
return await db.insert(user).values({ email, password }).returning({
|
||
id: user.id,
|
||
email: user.email,
|
||
});
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to create guest user"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function saveChat({
|
||
id,
|
||
userId,
|
||
title,
|
||
visibility,
|
||
}: {
|
||
id: string;
|
||
userId: string;
|
||
title: string;
|
||
visibility: VisibilityType;
|
||
}) {
|
||
try {
|
||
return await db.insert(chat).values({
|
||
id,
|
||
createdAt: new Date(),
|
||
userId,
|
||
title,
|
||
visibility,
|
||
});
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to save chat");
|
||
}
|
||
}
|
||
|
||
export async function deleteChatById({ id }: { id: string }) {
|
||
try {
|
||
await db.delete(vote).where(eq(vote.chatId, id));
|
||
await db.delete(message).where(eq(message.chatId, id));
|
||
await db.delete(stream).where(eq(stream.chatId, id));
|
||
|
||
const [chatsDeleted] = await db
|
||
.delete(chat)
|
||
.where(eq(chat.id, id))
|
||
.returning();
|
||
return chatsDeleted;
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete chat by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteAllChatsByUserId({ userId }: { userId: string }) {
|
||
try {
|
||
const userChats = await db
|
||
.select({ id: chat.id })
|
||
.from(chat)
|
||
.where(eq(chat.userId, userId));
|
||
|
||
if (userChats.length === 0) {
|
||
return { deletedCount: 0 };
|
||
}
|
||
|
||
const chatIds = userChats.map((c) => c.id);
|
||
|
||
await db.delete(vote).where(inArray(vote.chatId, chatIds));
|
||
await db.delete(message).where(inArray(message.chatId, chatIds));
|
||
await db.delete(stream).where(inArray(stream.chatId, chatIds));
|
||
|
||
const deletedChats = await db
|
||
.delete(chat)
|
||
.where(eq(chat.userId, userId))
|
||
.returning();
|
||
|
||
return { deletedCount: deletedChats.length };
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete all chats by user id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getChatsByUserId({
|
||
id,
|
||
limit,
|
||
startingAfter,
|
||
endingBefore,
|
||
}: {
|
||
id: string;
|
||
limit: number;
|
||
startingAfter: string | null;
|
||
endingBefore: string | null;
|
||
}) {
|
||
try {
|
||
const extendedLimit = limit + 1;
|
||
|
||
const query = (whereCondition?: SQL<any>) =>
|
||
db
|
||
.select()
|
||
.from(chat)
|
||
.where(
|
||
whereCondition
|
||
? and(whereCondition, eq(chat.userId, id))
|
||
: eq(chat.userId, id)
|
||
)
|
||
.orderBy(desc(chat.createdAt))
|
||
.limit(extendedLimit);
|
||
|
||
let filteredChats: Chat[] = [];
|
||
|
||
if (startingAfter) {
|
||
const [selectedChat] = await db
|
||
.select()
|
||
.from(chat)
|
||
.where(eq(chat.id, startingAfter))
|
||
.limit(1);
|
||
|
||
if (!selectedChat) {
|
||
throw new ChatSDKError(
|
||
"not_found:database",
|
||
`Chat with id ${startingAfter} not found`
|
||
);
|
||
}
|
||
|
||
filteredChats = await query(gt(chat.createdAt, selectedChat.createdAt));
|
||
} else if (endingBefore) {
|
||
const [selectedChat] = await db
|
||
.select()
|
||
.from(chat)
|
||
.where(eq(chat.id, endingBefore))
|
||
.limit(1);
|
||
|
||
if (!selectedChat) {
|
||
throw new ChatSDKError(
|
||
"not_found:database",
|
||
`Chat with id ${endingBefore} not found`
|
||
);
|
||
}
|
||
|
||
filteredChats = await query(lt(chat.createdAt, selectedChat.createdAt));
|
||
} else {
|
||
filteredChats = await query();
|
||
}
|
||
|
||
const hasMore = filteredChats.length > limit;
|
||
|
||
return {
|
||
chats: hasMore ? filteredChats.slice(0, limit) : filteredChats,
|
||
hasMore,
|
||
};
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get chats by user id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getChatById({ id }: { id: string }) {
|
||
try {
|
||
const [selectedChat] = await db.select().from(chat).where(eq(chat.id, id));
|
||
if (!selectedChat) {
|
||
return null;
|
||
}
|
||
|
||
return selectedChat;
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to get chat by id");
|
||
}
|
||
}
|
||
|
||
export async function saveMessages({ messages }: { messages: DBMessage[] }) {
|
||
try {
|
||
return await db.insert(message).values(messages);
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to save messages");
|
||
}
|
||
}
|
||
|
||
export async function updateMessageContent({
|
||
messageId,
|
||
chatId,
|
||
newContent,
|
||
}: {
|
||
messageId: string;
|
||
chatId: string;
|
||
newContent: string;
|
||
}) {
|
||
try {
|
||
// 更新消息的 parts 字段(将新内容作为 text part)
|
||
return await db
|
||
.update(message)
|
||
.set({
|
||
parts: [{ type: "text", text: newContent }],
|
||
})
|
||
.where(and(eq(message.id, messageId), eq(message.chatId, chatId)));
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to update message content");
|
||
}
|
||
}
|
||
|
||
export async function getMessagesByChatId({ id }: { id: string }) {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(message)
|
||
.where(eq(message.chatId, id))
|
||
.orderBy(asc(message.createdAt));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get messages by chat id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function voteMessage({
|
||
chatId,
|
||
messageId,
|
||
type,
|
||
}: {
|
||
chatId: string;
|
||
messageId: string;
|
||
type: "up" | "down";
|
||
}) {
|
||
try {
|
||
const [existingVote] = await db
|
||
.select()
|
||
.from(vote)
|
||
.where(and(eq(vote.messageId, messageId)));
|
||
|
||
if (existingVote) {
|
||
return await db
|
||
.update(vote)
|
||
.set({ isUpvoted: type === "up" })
|
||
.where(and(eq(vote.messageId, messageId), eq(vote.chatId, chatId)));
|
||
}
|
||
return await db.insert(vote).values({
|
||
chatId,
|
||
messageId,
|
||
isUpvoted: type === "up",
|
||
});
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to vote message");
|
||
}
|
||
}
|
||
|
||
export async function getVotesByChatId({ id }: { id: string }) {
|
||
try {
|
||
return await db.select().from(vote).where(eq(vote.chatId, id));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get votes by chat id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function saveDocument({
|
||
id,
|
||
title,
|
||
kind,
|
||
content,
|
||
userId,
|
||
}: {
|
||
id: string;
|
||
title: string;
|
||
kind: ArtifactKind;
|
||
content: string;
|
||
userId: string;
|
||
}) {
|
||
try {
|
||
return await db
|
||
.insert(document)
|
||
.values({
|
||
id,
|
||
title,
|
||
kind,
|
||
content,
|
||
userId,
|
||
createdAt: new Date(),
|
||
})
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to save document");
|
||
}
|
||
}
|
||
|
||
export async function getDocumentsById({ id }: { id: string }) {
|
||
try {
|
||
const documents = await db
|
||
.select()
|
||
.from(document)
|
||
.where(eq(document.id, id))
|
||
.orderBy(asc(document.createdAt));
|
||
|
||
return documents;
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get documents by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getDocumentById({ id }: { id: string }) {
|
||
try {
|
||
const [selectedDocument] = await db
|
||
.select()
|
||
.from(document)
|
||
.where(eq(document.id, id))
|
||
.orderBy(desc(document.createdAt));
|
||
|
||
return selectedDocument;
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get document by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteDocumentsByIdAfterTimestamp({
|
||
id,
|
||
timestamp,
|
||
}: {
|
||
id: string;
|
||
timestamp: Date;
|
||
}) {
|
||
try {
|
||
await db
|
||
.delete(suggestion)
|
||
.where(
|
||
and(
|
||
eq(suggestion.documentId, id),
|
||
gt(suggestion.documentCreatedAt, timestamp)
|
||
)
|
||
);
|
||
|
||
return await db
|
||
.delete(document)
|
||
.where(and(eq(document.id, id), gt(document.createdAt, timestamp)))
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete documents by id after timestamp"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function saveSuggestions({
|
||
suggestions,
|
||
}: {
|
||
suggestions: Suggestion[];
|
||
}) {
|
||
try {
|
||
return await db.insert(suggestion).values(suggestions);
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to save suggestions"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getSuggestionsByDocumentId({
|
||
documentId,
|
||
}: {
|
||
documentId: string;
|
||
}) {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(suggestion)
|
||
.where(eq(suggestion.documentId, documentId));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get suggestions by document id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getMessageById({ id }: { id: string }) {
|
||
try {
|
||
return await db.select().from(message).where(eq(message.id, id));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get message by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteMessagesByChatIdAfterTimestamp({
|
||
chatId,
|
||
timestamp,
|
||
}: {
|
||
chatId: string;
|
||
timestamp: Date;
|
||
}) {
|
||
try {
|
||
const messagesToDelete = await db
|
||
.select({ id: message.id })
|
||
.from(message)
|
||
.where(
|
||
and(eq(message.chatId, chatId), gte(message.createdAt, timestamp))
|
||
);
|
||
|
||
const messageIds = messagesToDelete.map(
|
||
(currentMessage) => currentMessage.id
|
||
);
|
||
|
||
if (messageIds.length > 0) {
|
||
await db
|
||
.delete(vote)
|
||
.where(
|
||
and(eq(vote.chatId, chatId), inArray(vote.messageId, messageIds))
|
||
);
|
||
|
||
return await db
|
||
.delete(message)
|
||
.where(
|
||
and(eq(message.chatId, chatId), inArray(message.id, messageIds))
|
||
);
|
||
}
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete messages by chat id after timestamp"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function updateChatVisibilityById({
|
||
chatId,
|
||
visibility,
|
||
}: {
|
||
chatId: string;
|
||
visibility: "private" | "public";
|
||
}) {
|
||
try {
|
||
return await db.update(chat).set({ visibility }).where(eq(chat.id, chatId));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to update chat visibility by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function updateChatLastContextById({
|
||
chatId,
|
||
context,
|
||
}: {
|
||
chatId: string;
|
||
// Store merged server-enriched usage object
|
||
context: AppUsage;
|
||
}) {
|
||
try {
|
||
return await db
|
||
.update(chat)
|
||
.set({ lastContext: context })
|
||
.where(eq(chat.id, chatId));
|
||
} catch (error) {
|
||
console.warn("Failed to update lastContext for chat", chatId, error);
|
||
return;
|
||
}
|
||
}
|
||
|
||
export async function getMessageCountByUserId({
|
||
id,
|
||
differenceInHours,
|
||
}: {
|
||
id: string;
|
||
differenceInHours: number;
|
||
}) {
|
||
try {
|
||
const twentyFourHoursAgo = new Date(
|
||
Date.now() - differenceInHours * 60 * 60 * 1000
|
||
);
|
||
|
||
const [stats] = await db
|
||
.select({ count: count(message.id) })
|
||
.from(message)
|
||
.innerJoin(chat, eq(message.chatId, chat.id))
|
||
.where(
|
||
and(
|
||
eq(chat.userId, id),
|
||
gte(message.createdAt, twentyFourHoursAgo),
|
||
eq(message.role, "user")
|
||
)
|
||
)
|
||
.execute();
|
||
|
||
return stats?.count ?? 0;
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get message count by user id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function createStreamId({
|
||
streamId,
|
||
chatId,
|
||
}: {
|
||
streamId: string;
|
||
chatId: string;
|
||
}) {
|
||
try {
|
||
await db
|
||
.insert(stream)
|
||
.values({ id: streamId, chatId, createdAt: new Date() });
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to create stream id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getStreamIdsByChatId({ chatId }: { chatId: string }) {
|
||
try {
|
||
const streamIds = await db
|
||
.select({ id: stream.id })
|
||
.from(stream)
|
||
.where(eq(stream.chatId, chatId))
|
||
.orderBy(asc(stream.createdAt))
|
||
.execute();
|
||
|
||
return streamIds.map(({ id }) => id);
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get stream ids by chat id"
|
||
);
|
||
}
|
||
}
|
||
|
||
// ==================== Admin Functions ====================
|
||
|
||
// User Management
|
||
export async function getAllUsers({
|
||
limit = 10,
|
||
offset = 0,
|
||
search = "",
|
||
}: {
|
||
limit?: number;
|
||
offset?: number;
|
||
search?: string;
|
||
}) {
|
||
try {
|
||
// 构建搜索条件
|
||
const searchConditions = search
|
||
? or(
|
||
like(user.email, `%${search}%`),
|
||
like(user.name, `%${search}%`)
|
||
)
|
||
: undefined;
|
||
|
||
// 获取用户列表
|
||
const users = await db
|
||
.select({
|
||
id: user.id,
|
||
email: user.email,
|
||
name: user.name,
|
||
department: user.department,
|
||
dingtalkUserId: user.dingtalkUserId,
|
||
role: user.role,
|
||
isActive: user.isActive,
|
||
monthlyImageLimit: user.monthlyImageLimit,
|
||
createdAt: user.createdAt,
|
||
updatedAt: user.updatedAt,
|
||
})
|
||
.from(user)
|
||
.where(searchConditions)
|
||
.orderBy(desc(user.createdAt))
|
||
.limit(limit)
|
||
.offset(offset);
|
||
|
||
// 获取总数
|
||
const [{ count }] = await db
|
||
.select({ count: sql<number>`count(*)::int` })
|
||
.from(user)
|
||
.where(searchConditions);
|
||
|
||
return { users, total: count };
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get all users"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getUserById({ id }: { id: string }) {
|
||
try {
|
||
const users = await db.select().from(user).where(eq(user.id, id)).limit(1);
|
||
return users[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to get user by id");
|
||
}
|
||
}
|
||
|
||
export async function updateUser({
|
||
id,
|
||
email,
|
||
role,
|
||
isActive,
|
||
monthlyImageLimit,
|
||
}: {
|
||
id: string;
|
||
email?: string;
|
||
role?: "guest" | "regular" | "admin";
|
||
isActive?: boolean;
|
||
monthlyImageLimit?: number | null;
|
||
}) {
|
||
try {
|
||
const updateData: Record<string, unknown> = { updatedAt: new Date() };
|
||
if (email !== undefined) updateData.email = email;
|
||
if (role !== undefined) updateData.role = role;
|
||
if (isActive !== undefined) updateData.isActive = isActive;
|
||
if (monthlyImageLimit !== undefined) {
|
||
// 0 或空转为 null(表示无限制)
|
||
updateData.monthlyImageLimit = monthlyImageLimit || null;
|
||
}
|
||
|
||
return await db
|
||
.update(user)
|
||
.set(updateData)
|
||
.where(eq(user.id, id))
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to update user");
|
||
}
|
||
}
|
||
|
||
export async function deleteUserById({ id }: { id: string }) {
|
||
try {
|
||
return await db.delete(user).where(eq(user.id, id)).returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError("bad_request:database", "Failed to delete user");
|
||
}
|
||
}
|
||
|
||
// System Settings
|
||
export async function getSystemSetting({ key }: { key: string }) {
|
||
try {
|
||
const settings = await db
|
||
.select()
|
||
.from(systemSettings)
|
||
.where(eq(systemSettings.key, key))
|
||
.limit(1);
|
||
return settings[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get system setting"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getAllSystemSettings() {
|
||
try {
|
||
return await db.select().from(systemSettings);
|
||
} catch (_error) {
|
||
console.error("getAllSystemSettings error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function upsertSystemSetting({
|
||
key,
|
||
value,
|
||
updatedBy,
|
||
}: {
|
||
key: string;
|
||
value: any;
|
||
updatedBy: string;
|
||
}) {
|
||
try {
|
||
return await db
|
||
.insert(systemSettings)
|
||
.values({
|
||
key,
|
||
value,
|
||
updatedBy,
|
||
updatedAt: new Date(),
|
||
})
|
||
.onConflictDoUpdate({
|
||
target: systemSettings.key,
|
||
set: {
|
||
value,
|
||
updatedBy,
|
||
updatedAt: new Date(),
|
||
},
|
||
})
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to upsert system setting"
|
||
);
|
||
}
|
||
}
|
||
|
||
// Tool Configuration
|
||
export async function getAllToolConfigs() {
|
||
try {
|
||
return await db.select().from(toolConfig).orderBy(asc(toolConfig.toolName));
|
||
} catch (_error) {
|
||
// Log to surface the actual database error for debugging
|
||
console.error("getAllToolConfigs error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getToolConfig({ toolName }: { toolName: string }) {
|
||
try {
|
||
const configs = await db
|
||
.select()
|
||
.from(toolConfig)
|
||
.where(eq(toolConfig.toolName, toolName))
|
||
.limit(1);
|
||
return configs[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get tool config"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function upsertToolConfig({
|
||
toolName,
|
||
isEnabled,
|
||
description,
|
||
config,
|
||
updatedBy,
|
||
}: {
|
||
toolName: string;
|
||
isEnabled: boolean;
|
||
description?: string;
|
||
config?: any;
|
||
updatedBy: string;
|
||
}) {
|
||
try {
|
||
return await db
|
||
.insert(toolConfig)
|
||
.values({
|
||
toolName,
|
||
isEnabled,
|
||
description,
|
||
config,
|
||
updatedBy,
|
||
updatedAt: new Date(),
|
||
})
|
||
.onConflictDoUpdate({
|
||
target: toolConfig.toolName,
|
||
set: {
|
||
isEnabled,
|
||
description,
|
||
config,
|
||
updatedBy,
|
||
updatedAt: new Date(),
|
||
},
|
||
})
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to upsert tool config"
|
||
);
|
||
}
|
||
}
|
||
|
||
// ==================== AI Provider CRUD ====================
|
||
|
||
export async function getAllAiProviders(): Promise<AiProvider[]> {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(aiProvider)
|
||
.orderBy(asc(aiProvider.createdAt));
|
||
} catch (_error) {
|
||
console.error("getAllAiProviders error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getEnabledAiProviders(): Promise<AiProvider[]> {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(aiProvider)
|
||
.where(eq(aiProvider.isEnabled, true))
|
||
.orderBy(asc(aiProvider.createdAt));
|
||
} catch (_error) {
|
||
console.error("getEnabledAiProviders error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getAiProviderById({
|
||
id,
|
||
}: {
|
||
id: string;
|
||
}): Promise<AiProvider | undefined> {
|
||
try {
|
||
const providers = await db
|
||
.select()
|
||
.from(aiProvider)
|
||
.where(eq(aiProvider.id, id))
|
||
.limit(1);
|
||
return providers[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get AI provider by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function createAiProvider({
|
||
name,
|
||
type,
|
||
baseUrl,
|
||
apiKey,
|
||
isEnabled,
|
||
updatedBy,
|
||
}: {
|
||
name: string;
|
||
type: "alibaba" | "openai" | "custom" | "local" | "gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image";
|
||
baseUrl: string;
|
||
apiKey: string;
|
||
isEnabled?: boolean;
|
||
updatedBy?: string;
|
||
}): Promise<AiProvider[]> {
|
||
try {
|
||
return await db
|
||
.insert(aiProvider)
|
||
.values({
|
||
name,
|
||
type,
|
||
baseUrl,
|
||
apiKey,
|
||
isEnabled: isEnabled ?? true,
|
||
updatedBy,
|
||
})
|
||
.returning();
|
||
} catch (_error) {
|
||
console.error("createAiProvider error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function updateAiProvider({
|
||
id,
|
||
name,
|
||
type,
|
||
baseUrl,
|
||
apiKey,
|
||
isEnabled,
|
||
updatedBy,
|
||
}: {
|
||
id: string;
|
||
name?: string;
|
||
type?: "alibaba" | "openai" | "custom" | "local" | "gemini" | "qianwen-video" | "volcano-vision" | "volcano-video" | "alibaba-image";
|
||
baseUrl?: string;
|
||
apiKey?: string;
|
||
isEnabled?: boolean;
|
||
updatedBy?: string;
|
||
}): Promise<AiProvider[]> {
|
||
try {
|
||
const updateData: any = { updatedAt: new Date() };
|
||
if (name !== undefined) updateData.name = name;
|
||
if (type !== undefined) updateData.type = type;
|
||
if (baseUrl !== undefined) updateData.baseUrl = baseUrl;
|
||
if (apiKey !== undefined) updateData.apiKey = apiKey;
|
||
if (isEnabled !== undefined) updateData.isEnabled = isEnabled;
|
||
if (updatedBy !== undefined) updateData.updatedBy = updatedBy;
|
||
|
||
return await db
|
||
.update(aiProvider)
|
||
.set(updateData)
|
||
.where(eq(aiProvider.id, id))
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to update AI provider"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteAiProvider({ id }: { id: string }) {
|
||
try {
|
||
return await db.delete(aiProvider).where(eq(aiProvider.id, id)).returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete AI provider"
|
||
);
|
||
}
|
||
}
|
||
|
||
// ==================== AI Model CRUD ====================
|
||
|
||
export async function getAllAiModels(): Promise<AiModel[]> {
|
||
try {
|
||
// 暂时移除排序以避免 sortOrder 类型问题
|
||
return await db.select().from(aiModel);
|
||
} catch (_error) {
|
||
console.error("getAllAiModels error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getEnabledAiModels(): Promise<AiModel[]> {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(aiModel)
|
||
.where(eq(aiModel.isEnabled, true));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get enabled AI models"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getAiModelsByProviderId({
|
||
providerId,
|
||
}: {
|
||
providerId: string;
|
||
}): Promise<AiModel[]> {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(aiModel)
|
||
.where(eq(aiModel.providerId, providerId));
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get AI models by provider"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getAiModelById({
|
||
id,
|
||
}: {
|
||
id: string;
|
||
}): Promise<AiModel | undefined> {
|
||
try {
|
||
const models = await db
|
||
.select()
|
||
.from(aiModel)
|
||
.where(eq(aiModel.id, id))
|
||
.limit(1);
|
||
return models[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get AI model by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getDefaultAiModel(): Promise<AiModel | undefined> {
|
||
try {
|
||
const models = await db
|
||
.select()
|
||
.from(aiModel)
|
||
.where(and(eq(aiModel.isDefault, true), eq(aiModel.isEnabled, true)))
|
||
.limit(1);
|
||
return models[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get default AI model"
|
||
);
|
||
}
|
||
}
|
||
|
||
// 图片生成模型特性配置类型
|
||
interface ImageGenConfig {
|
||
maxImages?: number;
|
||
supportsSize?: boolean;
|
||
supportsPromptExtend?: boolean;
|
||
supportsNegativePrompt?: boolean;
|
||
supportsWatermark?: boolean;
|
||
supportsSeed?: boolean;
|
||
enableLoadingPlaceholder?: boolean;
|
||
loadingColor?: string;
|
||
}
|
||
|
||
export async function createAiModel({
|
||
providerId,
|
||
modelId,
|
||
displayName,
|
||
description,
|
||
supportsVision,
|
||
supportsImageGen,
|
||
isEnabled,
|
||
isDefault,
|
||
sortOrder,
|
||
fallbackModelId1,
|
||
fallbackModelId2,
|
||
imageGenConfig,
|
||
}: {
|
||
providerId: string;
|
||
modelId: string;
|
||
displayName: string;
|
||
description?: string;
|
||
supportsVision?: boolean;
|
||
supportsImageGen?: boolean;
|
||
isEnabled?: boolean;
|
||
isDefault?: boolean;
|
||
sortOrder?: number;
|
||
fallbackModelId1?: string;
|
||
fallbackModelId2?: string;
|
||
imageGenConfig?: ImageGenConfig;
|
||
}): Promise<AiModel[]> {
|
||
try {
|
||
// 如果设置为默认,先取消其他默认
|
||
if (isDefault) {
|
||
await db
|
||
.update(aiModel)
|
||
.set({ isDefault: false })
|
||
.where(eq(aiModel.isDefault, true));
|
||
}
|
||
|
||
return await db
|
||
.insert(aiModel)
|
||
.values({
|
||
providerId,
|
||
modelId,
|
||
displayName,
|
||
description,
|
||
supportsVision: supportsVision ?? false,
|
||
supportsImageGen: supportsImageGen ?? false,
|
||
isEnabled: isEnabled ?? true,
|
||
isDefault: isDefault ?? false,
|
||
sortOrder: sortOrder ?? 0,
|
||
fallbackModelId1: fallbackModelId1 || null,
|
||
fallbackModelId2: fallbackModelId2 || null,
|
||
imageGenConfig: imageGenConfig || null,
|
||
})
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to create AI model"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function updateAiModel({
|
||
id,
|
||
providerId,
|
||
modelId,
|
||
displayName,
|
||
description,
|
||
supportsVision,
|
||
supportsImageGen,
|
||
isEnabled,
|
||
isDefault,
|
||
sortOrder,
|
||
fallbackModelId1,
|
||
fallbackModelId2,
|
||
imageGenConfig,
|
||
}: {
|
||
id: string;
|
||
providerId?: string;
|
||
modelId?: string;
|
||
displayName?: string;
|
||
description?: string;
|
||
supportsVision?: boolean;
|
||
supportsImageGen?: boolean;
|
||
isEnabled?: boolean;
|
||
isDefault?: boolean;
|
||
sortOrder?: number;
|
||
fallbackModelId1?: string;
|
||
fallbackModelId2?: string;
|
||
imageGenConfig?: ImageGenConfig;
|
||
}): Promise<AiModel[]> {
|
||
try {
|
||
// 如果设置为默认,先取消其他默认
|
||
if (isDefault) {
|
||
await db
|
||
.update(aiModel)
|
||
.set({ isDefault: false })
|
||
.where(eq(aiModel.isDefault, true));
|
||
}
|
||
|
||
const updateData: any = { updatedAt: new Date() };
|
||
if (providerId !== undefined) updateData.providerId = providerId;
|
||
if (modelId !== undefined) updateData.modelId = modelId;
|
||
if (displayName !== undefined) updateData.displayName = displayName;
|
||
if (description !== undefined) updateData.description = description;
|
||
if (supportsVision !== undefined) updateData.supportsVision = supportsVision;
|
||
if (supportsImageGen !== undefined)
|
||
updateData.supportsImageGen = supportsImageGen;
|
||
if (isEnabled !== undefined) updateData.isEnabled = isEnabled;
|
||
if (isDefault !== undefined) updateData.isDefault = isDefault;
|
||
if (sortOrder !== undefined) updateData.sortOrder = sortOrder;
|
||
// 备用模型 ID(空字符串转为 null)
|
||
if (fallbackModelId1 !== undefined) updateData.fallbackModelId1 = fallbackModelId1 || null;
|
||
if (fallbackModelId2 !== undefined) updateData.fallbackModelId2 = fallbackModelId2 || null;
|
||
// 🆕 图片生成配置
|
||
if (imageGenConfig !== undefined) updateData.imageGenConfig = imageGenConfig || null;
|
||
|
||
return await db
|
||
.update(aiModel)
|
||
.set(updateData)
|
||
.where(eq(aiModel.id, id))
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to update AI model"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteAiModel({ id }: { id: string }) {
|
||
try {
|
||
return await db.delete(aiModel).where(eq(aiModel.id, id)).returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete AI model"
|
||
);
|
||
}
|
||
}
|
||
|
||
// ==================== Storage Config CRUD ====================
|
||
|
||
export async function getAllStorageConfigs(): Promise<StorageConfig[]> {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(storageConfig)
|
||
.orderBy(asc(storageConfig.createdAt));
|
||
} catch (_error) {
|
||
console.error("getAllStorageConfigs error:", _error);
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
typeof _error === "object" ? JSON.stringify(_error) : String(_error)
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getDefaultStorageConfig(): Promise<
|
||
StorageConfig | undefined
|
||
> {
|
||
try {
|
||
const configs = await db
|
||
.select()
|
||
.from(storageConfig)
|
||
.where(
|
||
and(eq(storageConfig.isDefault, true), eq(storageConfig.isEnabled, true))
|
||
)
|
||
.limit(1);
|
||
return configs[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get default storage config"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getStorageConfigById({
|
||
id,
|
||
}: {
|
||
id: string;
|
||
}): Promise<StorageConfig | undefined> {
|
||
try {
|
||
const configs = await db
|
||
.select()
|
||
.from(storageConfig)
|
||
.where(eq(storageConfig.id, id))
|
||
.limit(1);
|
||
return configs[0];
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get storage config by id"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function createStorageConfig({
|
||
name,
|
||
type,
|
||
endpoint,
|
||
region,
|
||
bucket,
|
||
accessKeyId,
|
||
secretAccessKey,
|
||
publicUrl,
|
||
isDefault,
|
||
isEnabled,
|
||
updatedBy,
|
||
}: {
|
||
name: string;
|
||
type: "s3" | "local";
|
||
endpoint?: string;
|
||
region?: string;
|
||
bucket?: string;
|
||
accessKeyId?: string;
|
||
secretAccessKey?: string;
|
||
publicUrl?: string;
|
||
isDefault?: boolean;
|
||
isEnabled?: boolean;
|
||
updatedBy?: string;
|
||
}): Promise<StorageConfig[]> {
|
||
try {
|
||
// 如果设置为默认,先取消其他默认
|
||
if (isDefault) {
|
||
await db
|
||
.update(storageConfig)
|
||
.set({ isDefault: false })
|
||
.where(eq(storageConfig.isDefault, true));
|
||
}
|
||
|
||
return await db
|
||
.insert(storageConfig)
|
||
.values({
|
||
name,
|
||
type,
|
||
endpoint,
|
||
region,
|
||
bucket,
|
||
accessKeyId,
|
||
secretAccessKey,
|
||
publicUrl,
|
||
isDefault: isDefault ?? false,
|
||
isEnabled: isEnabled ?? true,
|
||
updatedBy,
|
||
})
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to create storage config"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function updateStorageConfig({
|
||
id,
|
||
name,
|
||
type,
|
||
endpoint,
|
||
region,
|
||
bucket,
|
||
accessKeyId,
|
||
secretAccessKey,
|
||
publicUrl,
|
||
isDefault,
|
||
isEnabled,
|
||
updatedBy,
|
||
}: {
|
||
id: string;
|
||
name?: string;
|
||
type?: "s3" | "local";
|
||
endpoint?: string;
|
||
region?: string;
|
||
bucket?: string;
|
||
accessKeyId?: string;
|
||
secretAccessKey?: string;
|
||
publicUrl?: string;
|
||
isDefault?: boolean;
|
||
isEnabled?: boolean;
|
||
updatedBy?: string;
|
||
}): Promise<StorageConfig[]> {
|
||
try {
|
||
// 如果设置为默认,先取消其他默认
|
||
if (isDefault) {
|
||
await db
|
||
.update(storageConfig)
|
||
.set({ isDefault: false })
|
||
.where(eq(storageConfig.isDefault, true));
|
||
}
|
||
|
||
const updateData: any = { updatedAt: new Date() };
|
||
if (name !== undefined) updateData.name = name;
|
||
if (type !== undefined) updateData.type = type;
|
||
if (endpoint !== undefined) updateData.endpoint = endpoint;
|
||
if (region !== undefined) updateData.region = region;
|
||
if (bucket !== undefined) updateData.bucket = bucket;
|
||
if (accessKeyId !== undefined) updateData.accessKeyId = accessKeyId;
|
||
if (secretAccessKey !== undefined)
|
||
updateData.secretAccessKey = secretAccessKey;
|
||
if (publicUrl !== undefined) updateData.publicUrl = publicUrl;
|
||
if (isDefault !== undefined) updateData.isDefault = isDefault;
|
||
if (isEnabled !== undefined) updateData.isEnabled = isEnabled;
|
||
if (updatedBy !== undefined) updateData.updatedBy = updatedBy;
|
||
|
||
return await db
|
||
.update(storageConfig)
|
||
.set(updateData)
|
||
.where(eq(storageConfig.id, id))
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to update storage config"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function deleteStorageConfig({ id }: { id: string }) {
|
||
try {
|
||
return await db
|
||
.delete(storageConfig)
|
||
.where(eq(storageConfig.id, id))
|
||
.returning();
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to delete storage config"
|
||
);
|
||
}
|
||
}
|
||
|
||
// 获取启用的 AI 模型及其提供商信息(用于前端展示)
|
||
export async function getEnabledAiModelsWithProvider(includeSubModels: boolean = false) {
|
||
try {
|
||
const conditions = [
|
||
eq(aiModel.isEnabled, true),
|
||
eq(aiProvider.isEnabled, true),
|
||
];
|
||
|
||
// 如果不包含子模型,添加过滤条件
|
||
if (!includeSubModels) {
|
||
conditions.push(eq(aiModel.isSubModel, false));
|
||
}
|
||
|
||
const models = await db
|
||
.select({
|
||
model: aiModel,
|
||
provider: aiProvider,
|
||
})
|
||
.from(aiModel)
|
||
.innerJoin(aiProvider, eq(aiModel.providerId, aiProvider.id))
|
||
.where(and(...conditions));
|
||
|
||
return models;
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get enabled AI models with provider"
|
||
);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 🆕 获取指定父模型的所有子模型
|
||
* 用于火山引擎视频等需要智能选择模型的场景
|
||
*/
|
||
export async function getSubModelsByParentModelId(parentModelId: string) {
|
||
try {
|
||
const subModels = await db
|
||
.select({
|
||
model: aiModel,
|
||
provider: aiProvider,
|
||
})
|
||
.from(aiModel)
|
||
.innerJoin(aiProvider, eq(aiModel.providerId, aiProvider.id))
|
||
.where(
|
||
and(
|
||
eq(aiModel.isEnabled, true),
|
||
eq(aiProvider.isEnabled, true),
|
||
eq(aiModel.isSubModel, true),
|
||
eq(aiModel.parentModelId, parentModelId)
|
||
)
|
||
);
|
||
|
||
return subModels;
|
||
} catch (_error) {
|
||
throw new ChatSDKError(
|
||
"bad_request:database",
|
||
"Failed to get sub models"
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function getSiteSettings() {
|
||
const defaultSettings = {
|
||
siteName: "AI Chatbot",
|
||
siteLogo: null,
|
||
siteLogoDark: null,
|
||
favicon: null,
|
||
defaultTheme: "system",
|
||
bannerEnabled: true,
|
||
bannerText: "升级会员, Nano Banana Pro 免费365天!",
|
||
bannerLink: null,
|
||
toolbox1Name: "工具箱",
|
||
toolbox1Enabled: true,
|
||
toolbox2Name: "工具箱2",
|
||
toolbox2Enabled: false,
|
||
tutorialVideoUrl: null,
|
||
tutorialEnabled: false,
|
||
rankingEnabled: true,
|
||
};
|
||
try {
|
||
const [settings] = await db.select().from(siteSettings).limit(1);
|
||
return settings || defaultSettings;
|
||
} catch (_error) {
|
||
return defaultSettings;
|
||
}
|
||
}
|
||
|
||
export async function getNavLinks() {
|
||
try {
|
||
return await db
|
||
.select()
|
||
.from(navLinks)
|
||
.where(eq(navLinks.isEnabled, true))
|
||
.orderBy(navLinks.order);
|
||
} catch (_error) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export async function getAuthSettings() {
|
||
try {
|
||
const [settings] = await db.select().from(authSettings).limit(1);
|
||
|
||
if (settings) {
|
||
return {
|
||
...settings,
|
||
dingTalkOrganizations: settings.dingTalkOrganizations ?? [],
|
||
};
|
||
}
|
||
|
||
return {
|
||
dingTalkAppKey: "",
|
||
dingTalkAppSecret: "",
|
||
dingTalkOrganizations: [],
|
||
isDingTalkEnabled: false,
|
||
isRegistrationEnabled: true,
|
||
};
|
||
} catch (_error) {
|
||
return {
|
||
dingTalkAppKey: "",
|
||
dingTalkAppSecret: "",
|
||
dingTalkOrganizations: [],
|
||
isDingTalkEnabled: false,
|
||
isRegistrationEnabled: true,
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户当月已生成图片数量
|
||
*/
|
||
export async function getUserMonthlyImageCount(userId: string): Promise<number> {
|
||
try {
|
||
const now = new Date();
|
||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||
|
||
const result = await db
|
||
.select({ count: sql<number>`count(*)::int` })
|
||
.from(generatedImage)
|
||
.where(and(
|
||
eq(generatedImage.userId, userId),
|
||
gte(generatedImage.createdAt, startOfMonth)
|
||
));
|
||
|
||
return result[0]?.count ?? 0;
|
||
} catch (_error) {
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检查用户图片生成配额
|
||
* @returns 配额状态对象
|
||
*/
|
||
export async function checkUserImageQuota(userId: string): Promise<{
|
||
allowed: boolean;
|
||
used: number;
|
||
limit: number | null;
|
||
remaining: number | null;
|
||
}> {
|
||
try {
|
||
// 获取用户信息
|
||
const [userData] = await db
|
||
.select({ monthlyImageLimit: user.monthlyImageLimit })
|
||
.from(user)
|
||
.where(eq(user.id, userId));
|
||
|
||
const limit = userData?.monthlyImageLimit;
|
||
const used = await getUserMonthlyImageCount(userId);
|
||
|
||
// null 或 0 表示无限制
|
||
if (!limit || limit === 0) {
|
||
return {
|
||
allowed: true,
|
||
used,
|
||
limit: null,
|
||
remaining: null
|
||
};
|
||
}
|
||
|
||
return {
|
||
allowed: used < limit,
|
||
used,
|
||
limit,
|
||
remaining: Math.max(0, limit - used),
|
||
};
|
||
} catch (_error) {
|
||
// 出错时默认允许
|
||
return { allowed: true, used: 0, limit: null, remaining: null };
|
||
}
|
||
}
|
||
|
||
// 获取视频教程列表
|
||
export async function getTutorialVideos() {
|
||
try {
|
||
const { tutorialVideos } = await import("./schema");
|
||
const { eq, asc } = await import("drizzle-orm");
|
||
|
||
const videos = await db
|
||
.select()
|
||
.from(tutorialVideos)
|
||
.where(eq(tutorialVideos.isEnabled, true))
|
||
.orderBy(asc(tutorialVideos.order));
|
||
|
||
return videos;
|
||
} catch (_error) {
|
||
return [];
|
||
}
|
||
}
|