ai-chatbot/lib/db/queries.ts
Your Name ee4bb14736
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add banner settings to admin panel for configuring ad banner text and link
2026-01-26 22:26:44 +08:00

1641 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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() {
try {
const [settings] = await db.select().from(siteSettings).limit(1);
return settings || {
siteName: "AI Chatbot",
siteLogo: null,
siteLogoDark: null,
favicon: null,
defaultTheme: "system",
bannerEnabled: true,
bannerText: "升级会员, Nano Banana Pro 免费365天!",
bannerLink: null
};
} catch (_error) {
return {
siteName: "AI Chatbot",
siteLogo: null,
siteLogoDark: null,
favicon: null,
defaultTheme: "system",
bannerEnabled: true,
bannerText: "升级会员, Nano Banana Pro 免费365天!",
bannerLink: null
};
}
}
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 };
}
}