lovart3/lib/db/schema.ts

441 lines
18 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 type { InferSelectModel } from "drizzle-orm";
import {
boolean,
foreignKey,
integer,
json,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
uuid,
varchar,
} from "drizzle-orm/pg-core";
import type { AppUsage } from "../usage";
export const user = pgTable("User", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
email: varchar("email", { length: 64 }).notNull(),
password: varchar("password", { length: 64 }),
name: varchar("name", { length: 100 }), // 用户姓名(用于显示)
department: varchar("department", { length: 256 }), // 部门信息
dingtalkUserId: varchar("dingtalkUserId", { length: 128 }), // 钉钉用户ID
role: varchar("role", { enum: ["guest", "regular", "admin"] })
.notNull()
.default("regular"),
isActive: boolean("isActive").notNull().default(true),
monthlyImageLimit: integer("monthlyImageLimit"), // 每月出图限额,NULL或0表示无限制
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
export type User = InferSelectModel<typeof user>;
export const chat = pgTable("Chat", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
createdAt: timestamp("createdAt").notNull(),
title: text("title").notNull(),
userId: uuid("userId")
.notNull()
.references(() => user.id),
visibility: varchar("visibility", { enum: ["public", "private"] })
.notNull()
.default("private"),
lastContext: jsonb("lastContext").$type<AppUsage | null>(),
});
export type Chat = InferSelectModel<typeof chat>;
// DEPRECATED: The following schema is deprecated and will be removed in the future.
// Read the migration guide at https://chat-sdk.dev/docs/migration-guides/message-parts
export const messageDeprecated = pgTable("Message", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
role: varchar("role").notNull(),
content: json("content").notNull(),
createdAt: timestamp("createdAt").notNull(),
});
export type MessageDeprecated = InferSelectModel<typeof messageDeprecated>;
export const message = pgTable("Message_v2", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
role: varchar("role").notNull(),
parts: json("parts").notNull(),
attachments: json("attachments").notNull(),
createdAt: timestamp("createdAt").notNull(),
// 🆕 视频存储相关字段
videoStorageStatus: varchar("video_storage_status", { length: 20 }).default("temporary"),
temporaryVideoUrl: text("temporary_video_url"),
permanentVideoUrl: text("permanent_video_url"),
archivedAt: timestamp("archived_at"),
videoMetadata: jsonb("video_metadata"),
});
export type DBMessage = InferSelectModel<typeof message>;
// DEPRECATED: The following schema is deprecated and will be removed in the future.
// Read the migration guide at https://chat-sdk.dev/docs/migration-guides/message-parts
export const voteDeprecated = pgTable(
"Vote",
{
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
messageId: uuid("messageId")
.notNull()
.references(() => messageDeprecated.id),
isUpvoted: boolean("isUpvoted").notNull(),
},
(table) => {
return {
pk: primaryKey({ columns: [table.chatId, table.messageId] }),
};
}
);
export type VoteDeprecated = InferSelectModel<typeof voteDeprecated>;
export const vote = pgTable(
"Vote_v2",
{
chatId: uuid("chatId")
.notNull()
.references(() => chat.id),
messageId: uuid("messageId")
.notNull()
.references(() => message.id),
isUpvoted: boolean("isUpvoted").notNull(),
},
(table) => {
return {
pk: primaryKey({ columns: [table.chatId, table.messageId] }),
};
}
);
export type Vote = InferSelectModel<typeof vote>;
export const document = pgTable(
"Document",
{
id: uuid("id").notNull().defaultRandom(),
createdAt: timestamp("createdAt").notNull(),
title: text("title").notNull(),
content: text("content"),
kind: varchar("text", { enum: ["text", "code", "image", "sheet"] })
.notNull()
.default("text"),
userId: uuid("userId")
.notNull()
.references(() => user.id),
},
(table) => {
return {
pk: primaryKey({ columns: [table.id, table.createdAt] }),
};
}
);
export type Document = InferSelectModel<typeof document>;
export const suggestion = pgTable(
"Suggestion",
{
id: uuid("id").notNull().defaultRandom(),
documentId: uuid("documentId").notNull(),
documentCreatedAt: timestamp("documentCreatedAt").notNull(),
originalText: text("originalText").notNull(),
suggestedText: text("suggestedText").notNull(),
description: text("description"),
isResolved: boolean("isResolved").notNull().default(false),
userId: uuid("userId")
.notNull()
.references(() => user.id),
createdAt: timestamp("createdAt").notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.id] }),
documentRef: foreignKey({
columns: [table.documentId, table.documentCreatedAt],
foreignColumns: [document.id, document.createdAt],
}),
})
);
export type Suggestion = InferSelectModel<typeof suggestion>;
export const stream = pgTable(
"Stream",
{
id: uuid("id").notNull().defaultRandom(),
chatId: uuid("chatId").notNull(),
createdAt: timestamp("createdAt").notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.id] }),
chatRef: foreignKey({
columns: [table.chatId],
foreignColumns: [chat.id],
}),
})
);
export type Stream = InferSelectModel<typeof stream>;
export const systemSettings = pgTable("SystemSettings", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
key: varchar("key", { length: 128 }).notNull().unique(),
value: jsonb("value").notNull(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});
export type SystemSettings = InferSelectModel<typeof systemSettings>;
export const toolConfig = pgTable("ToolConfig", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
toolName: varchar("toolName", { length: 128 }).notNull().unique(),
isEnabled: boolean("isEnabled").notNull().default(true),
description: text("description"),
config: jsonb("config"),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});
export type ToolConfig = InferSelectModel<typeof toolConfig>;
// AI 提供商配置表
export const aiProvider = pgTable("AiProvider", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
name: varchar("name", { length: 128 }).notNull(), // 提供商名称,如 "阿里云通义千问"
type: varchar("type", { enum: ["alibaba", "openai", "custom", "local", "gemini", "huanke-gemini", "qianwen-video", "volcano-vision", "volcano-video", "alibaba-image"] })
.notNull()
.default("custom"), // 提供商类型
baseUrl: text("baseUrl").notNull(), // API 基础 URL
apiKey: text("apiKey").notNull(), // API Key (加密存储)
isEnabled: boolean("isEnabled").notNull().default(true),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});
export type AiProvider = InferSelectModel<typeof aiProvider>;
// AI 模型配置表
export const aiModel = pgTable("AiModel", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
providerId: uuid("providerId")
.notNull()
.references(() => aiProvider.id, { onDelete: "cascade" }),
modelId: varchar("modelId", { length: 128 }).notNull(), // 模型 ID,如 "qwen-turbo"
displayName: varchar("displayName", { length: 128 }).notNull(), // 显示名称
description: text("description"), // 模型描述
supportsVision: boolean("supportsVision").notNull().default(false), // 是否支持图片输入
supportsImageGen: boolean("supportsImageGen").notNull().default(false), // 是否支持图片生成
isEnabled: boolean("isEnabled").notNull().default(true), // 是否启用
isDefault: boolean("isDefault").notNull().default(false), // 是否为默认模型
sortOrder: integer("sortOrder").notNull().default(0), // 排序顺序
capabilities: jsonb("capabilities"), // 🆕 模型能力配置(用于视频模型)
defaultParams: jsonb("defaultParams"), // 🆕 默认参数配置
// 🆕 子模型相关字段(用于火山引擎视频等需要智能选择的模型)
isSubModel: boolean("is_sub_model").notNull().default(false), // 是否为子模型(不在模型列表中显示)
parentModelId: uuid("parent_model_id"), // 父模型ID(如果是子模型),注:暂不添加外键约束避免循环引用
// 🆕 备用模型 ID(当主模型请求失败时自动切换)
fallbackModelId1: varchar("fallbackModelId1", { length: 128 }), // 备用模型 ID 1
fallbackModelId2: varchar("fallbackModelId2", { length: 128 }), // 备用模型 ID 2
// 🆕 图片生成模型特性配置(用于阿里生图、Gemini等)
imageGenConfig: jsonb("imageGenConfig"), // { maxImages: 6, supportsSize: true, supportsPromptExtend: true, supportsNegativePrompt: true, enableLoadingPlaceholder: true }
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
export type AiModel = InferSelectModel<typeof aiModel>;
// S3 存储配置表
export const storageConfig = pgTable("StorageConfig", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
name: varchar("name", { length: 128 }).notNull(), // 配置名称
type: varchar("type", { enum: ["s3", "local"] }).notNull().default("s3"),
endpoint: text("endpoint"), // S3 端点
region: varchar("region", { length: 64 }), // 区域
bucket: varchar("bucket", { length: 128 }), // 存储桶名称
accessKeyId: text("accessKeyId"), // Access Key ID
secretAccessKey: text("secretAccessKey"), // Secret Access Key
publicUrl: text("publicUrl"), // 公开访问 URL 前缀
isDefault: boolean("isDefault").notNull().default(false), // 是否为默认存储
isEnabled: boolean("isEnabled").notNull().default(true),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});
export type StorageConfig = InferSelectModel<typeof storageConfig>;
// 网站设置表
export const siteSettings = pgTable("SiteSettings", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
siteName: varchar("siteName", { length: 128 }).notNull().default("AI Chatbot"),
siteLogo: text("siteLogo"), // 默认/白天模式 LOGO
siteLogoDark: text("siteLogoDark"), // 黑夜模式 LOGO
favicon: text("favicon"),
showAutoModel: boolean("showAutoModel").notNull().default(true), // 是否显示"自动"模型选项
defaultTheme: varchar("defaultTheme", { length: 16 }).notNull().default("system"), // 默认主题:system | light | dark
// 使用时间限制
usageTimeEnabled: boolean("usageTimeEnabled").notNull().default(false), // 是否启用时间限制
usageTimeStart: varchar("usageTimeStart", { length: 5 }).default("08:00"), // 开始时间(HH:mm)
usageTimeEnd: varchar("usageTimeEnd", { length: 5 }).default("20:00"), // 结束时间(HH:mm)
usageTimeMessage: text("usageTimeMessage").default("由壹点累了也是需要休息的"), // 超时提示词
// 广告横幅设置
bannerEnabled: boolean("bannerEnabled").notNull().default(true), // 是否显示广告横幅
bannerText: text("bannerText").default("升级会员, Nano Banana Pro 免费365天!"), // 广告横幅文字
bannerLink: text("bannerLink"), // 广告横幅链接(可选)
// 工具箱设置
toolbox1Name: varchar("toolbox1Name", { length: 64 }).default("工具箱"), // 工具箱1名称
toolbox1Enabled: boolean("toolbox1Enabled").notNull().default(true), // 是否显示工具箱1
toolbox2Name: varchar("toolbox2Name", { length: 64 }).default("工具箱2"), // 工具箱2名称
toolbox2Enabled: boolean("toolbox2Enabled").notNull().default(false), // 是否显示工具箱2
// 功能教程设置
tutorialVideoUrl: text("tutorialVideoUrl"), // 功能教程视频链接
tutorialEnabled: boolean("tutorialEnabled").notNull().default(false), // 是否显示功能教程按钮
// 精选榜设置
rankingEnabled: boolean("rankingEnabled").notNull().default(true), // 是否显示精选榜按钮
// 无限画布设置
canvasEnabled: boolean("canvasEnabled").notNull().default(true), // 是否启用无限画布功能
canvasDefaultModelId: varchar("canvasDefaultModelId", { length: 128 }), // 画布默认选择的模型ID(null表示智能自动)
canvasAutoModeDefaultModelId: varchar("canvasAutoModeDefaultModelId", { length: 128 }), // 智能自动模式下实际使用的默认生图模型ID
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});
export type SiteSettings = InferSelectModel<typeof siteSettings>;
// 导航链接表
export const navLinks = pgTable("NavLinks", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
name: varchar("name", { length: 64 }).notNull(),
url: text("url").notNull(),
order: integer("order").notNull().default(0),
isEnabled: boolean("isEnabled").notNull().default(true),
toolboxGroup: integer("toolboxGroup").notNull().default(1), // 属于哪个工具箱:1 或 2
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
createdBy: uuid("createdBy").references(() => user.id),
});
export type NavLink = InferSelectModel<typeof navLinks>;
// 视频教程表
export const tutorialVideos = pgTable("TutorialVideos", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
title: varchar("title", { length: 128 }).notNull(), // 视频标题
videoUrl: text("videoUrl").notNull(), // 视频播放链接
coverUrl: text("coverUrl"), // 视频封面图片链接
description: text("description"), // 视频描述
order: integer("order").notNull().default(0), // 排序
isEnabled: boolean("isEnabled").notNull().default(true), // 是否启用
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
createdBy: uuid("createdBy").references(() => user.id),
});
export type TutorialVideo = InferSelectModel<typeof tutorialVideos>;
// 认证配置表
export const authSettings = pgTable("AuthSettings", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
dingTalkAppKey: text("dingTalkAppKey"),
dingTalkAppSecret: text("dingTalkAppSecret"),
// 多组织钉钉配置,存储为 JSON 数组:[{ name, appKey, appSecret }]
dingTalkOrganizations: jsonb("dingTalkOrganizations").$type<
{ name: string; appKey: string; appSecret: string }[] | null
>(),
isDingTalkEnabled: boolean("isDingTalkEnabled").notNull().default(false),
isRegistrationEnabled: boolean("isRegistrationEnabled").notNull().default(true),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
updatedBy: uuid("updatedBy").references(() => user.id),
});
export type AuthSettings = InferSelectModel<typeof authSettings>;
// 临时登录令牌表
export const loginToken = pgTable("LoginToken", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
token: text("token").notNull().unique(),
email: varchar("email", { length: 64 }).notNull(),
expiresAt: timestamp("expiresAt").notNull(),
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
export type LoginToken = InferSelectModel<typeof loginToken>;
// 生成图片记录表
export const generatedImage = pgTable("GeneratedImage", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId")
.notNull()
.references(() => user.id),
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
messageId: uuid("messageId"),
thumbnailUrl: text("thumbnailUrl").notNull(),
originalUrl: text("originalUrl").notNull(),
imageSize: varchar("imageSize", { length: 10 }).default("2K"),
prompt: text("prompt"),
modelId: varchar("modelId", { length: 128 }),
isFeatured: boolean("isFeatured").notNull().default(false), // 是否精选
featuredAt: timestamp("featuredAt"), // 精选时间
featuredBy: uuid("featuredBy").references(() => user.id), // 精选操作人
createdAt: timestamp("createdAt").notNull().defaultNow(),
});
export type GeneratedImage = InferSelectModel<typeof generatedImage>;
// ============================================
// 无限画布相关表
// ============================================
// 画布项目表
export const canvasProject = pgTable("CanvasProject", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
userId: uuid("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
title: varchar("title", { length: 256 }).notNull().default("未命名画布"),
description: text("description"),
thumbnail: text("thumbnail"),
// 存储完整画布状态(节点、边、视口等)
canvasData: jsonb("canvasData").$type<{
nodes: any[];
edges: any[];
viewport: { x: number; y: number; zoom: number };
}>(),
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
export type CanvasProject = InferSelectModel<typeof canvasProject>;
// 画布节点表(可选,用于更细粒度的查询)
export const canvasNode = pgTable("CanvasNode", {
id: uuid("id").primaryKey().notNull().defaultRandom(),
projectId: uuid("projectId")
.notNull()
.references(() => canvasProject.id, { onDelete: "cascade" }),
type: varchar("type", { length: 32 }).notNull(), // 'message' | 'image' | 'video' | 'text' | 'scene-fusion'
position: jsonb("position").$type<{ x: number; y: number }>(),
size: jsonb("size").$type<{ width: number; height: number }>(),
data: jsonb("data"), // 节点具体数据
connections: jsonb("connections").$type<string[]>().default([]),
parentId: uuid("parentId"), // 父节点ID,用于对话上下文
createdAt: timestamp("createdAt").notNull().defaultNow(),
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
});
export type CanvasNode = InferSelectModel<typeof canvasNode>;