1295 lines
55 KiB
TypeScript
1295 lines
55 KiB
TypeScript
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", "agent_admin"] })
|
||
.notNull()
|
||
.default("regular"),
|
||
isHighVip: boolean("isHighVip").notNull().default(false),
|
||
isActive: boolean("isActive").notNull().default(true),
|
||
activeSessionKey: varchar("activeSessionKey", { length: 64 }),
|
||
activeSessionCreatedAt: timestamp("activeSessionCreatedAt"),
|
||
activeSessionIp: varchar("activeSessionIp", { length: 64 }),
|
||
activeSessionUserAgent: text("activeSessionUserAgent"),
|
||
activeSessionLastSeenAt: timestamp("activeSessionLastSeenAt"),
|
||
monthlyImageLimit: integer("monthlyImageLimit"), // 每月出图限额,NULL或0表示无限制
|
||
pointsBalance: integer("pointsBalance").notNull().default(0), // 当前剩余积分
|
||
pointsGiftedTotal: integer("pointsGiftedTotal").notNull().default(0), // 管理员累计赠送积分
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type User = InferSelectModel<typeof user>;
|
||
|
||
export const pointsLedger = pgTable("PointsLedger", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
amount: integer("amount").notNull(), // 正数=赠送/充值,负数=消耗
|
||
reason: varchar("reason", { length: 64 }).notNull(), // gift | image_generation | recharge | adjust
|
||
modelId: varchar("modelId", { length: 128 }),
|
||
chatId: uuid("chatId"),
|
||
messageId: uuid("messageId"),
|
||
note: text("note"),
|
||
createdBy: uuid("createdBy").references(() => user.id),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type PointsLedger = InferSelectModel<typeof pointsLedger>;
|
||
|
||
export const paymentOrder = pgTable("PaymentOrder", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
provider: varchar("provider", { length: 32 }).notNull().default("dulupay"),
|
||
outTradeNo: varchar("outTradeNo", { length: 128 }).notNull().unique(),
|
||
tradeNo: varchar("tradeNo", { length: 128 }),
|
||
packageId: varchar("packageId", { length: 64 }),
|
||
points: integer("points").notNull().default(0),
|
||
amountCents: integer("amountCents").notNull().default(0),
|
||
currency: varchar("currency", { length: 8 }).notNull().default("CNY"),
|
||
payType: varchar("payType", { length: 32 }),
|
||
status: varchar("status", { length: 32 }).notNull().default("pending"),
|
||
paymentUrl: text("paymentUrl"),
|
||
rawCallback: jsonb("rawCallback"),
|
||
errorMessage: text("errorMessage"),
|
||
paidAt: timestamp("paidAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type PaymentOrder = InferSelectModel<typeof paymentOrder>;
|
||
|
||
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 systemSettingHistory = pgTable("SystemSettingHistory", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
key: varchar("key", { length: 128 }).notNull(),
|
||
previousValue: jsonb("previousValue"),
|
||
nextValue: jsonb("nextValue"),
|
||
reason: varchar("reason", { length: 128 }),
|
||
changedAt: timestamp("changedAt").notNull().defaultNow(),
|
||
changedBy: uuid("changedBy").references(() => user.id),
|
||
});
|
||
|
||
export type SystemSettingHistory = InferSelectModel<typeof systemSettingHistory>;
|
||
|
||
export const dingTalkNotificationTask = pgTable("DingTalkNotificationTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
title: varchar("title", { length: 100 }).notNull(),
|
||
content: text("content").notNull(),
|
||
status: varchar("status", { length: 24 }).notNull().default("pending"),
|
||
scheduledAt: timestamp("scheduledAt").notNull(),
|
||
attempts: integer("attempts").notNull().default(0),
|
||
lastAttemptAt: timestamp("lastAttemptAt"),
|
||
sentAt: timestamp("sentAt"),
|
||
cancelledAt: timestamp("cancelledAt"),
|
||
lastError: text("lastError"),
|
||
createdBy: uuid("createdBy").references(() => user.id),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type DingTalkNotificationTask = InferSelectModel<
|
||
typeof dingTalkNotificationTask
|
||
>;
|
||
|
||
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", "grsai-gpt-image", "grsai-video", "huanke-gemini", "qianwen-video", "doubao-video", "ruxa-video", "volcano-vision", "volcano-video", "alibaba-image", "ruxa-image", "fal-image", "openai-image", "stepfun-image", "midjourney", "fal-video", "zhipu", "tripo-3d", "worldlabs-3d", "kittycad-cad", "runway", "krea", "bfl-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(), // 显示名称
|
||
avatarUrl: text("avatarUrl"), // 模型头像(建议 400x400)
|
||
description: text("description"), // 模型描述
|
||
supportsVision: boolean("supportsVision").notNull().default(false), // 是否支持图片输入
|
||
supportsImageGen: boolean("supportsImageGen").notNull().default(false), // 是否支持图片生成
|
||
isEnabled: boolean("isEnabled").notNull().default(true), // 是否启用
|
||
isHidden: boolean("isHidden").notNull().default(false), // 是否在前端模型选择中隐藏
|
||
isDefault: boolean("isDefault").notNull().default(false), // 是否为默认模型
|
||
isDefaultTextEditModel: boolean("isDefaultTextEditModel").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
|
||
excludeFromFailover: boolean("excludeFromFailover").notNull().default(false), // 不参与故障迁移
|
||
healthStatus: varchar("healthStatus", {
|
||
enum: ["healthy", "degraded", "auto_disabled"],
|
||
})
|
||
.notNull()
|
||
.default("healthy"),
|
||
healthConsecutiveFailures: integer("healthConsecutiveFailures")
|
||
.notNull()
|
||
.default(0),
|
||
healthLastEventType: varchar("healthLastEventType", { length: 32 }),
|
||
healthLastError: text("healthLastError"),
|
||
healthLastFailureAt: timestamp("healthLastFailureAt"),
|
||
healthLastSuccessAt: timestamp("healthLastSuccessAt"),
|
||
healthAutoDisabledAt: timestamp("healthAutoDisabledAt"),
|
||
healthAutoDisabledReason: text("healthAutoDisabledReason"),
|
||
healthRecoverySuccessStreak: integer("healthRecoverySuccessStreak")
|
||
.notNull()
|
||
.default(0),
|
||
healthObservationStartedAt: timestamp("healthObservationStartedAt"),
|
||
healthUpdatedAt: timestamp("healthUpdatedAt").notNull().defaultNow(),
|
||
// 🆕 图片生成模型特性配置(用于阿里生图、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>;
|
||
|
||
export const aiModelHealthEvent = pgTable("AiModelHealthEvent", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
modelId: uuid("modelId").references(() => aiModel.id, { onDelete: "set null" }),
|
||
providerId: uuid("providerId").references(() => aiProvider.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
modelRuntimeId: varchar("modelRuntimeId", { length: 128 }),
|
||
modelDisplayName: varchar("modelDisplayName", { length: 128 }),
|
||
providerType: varchar("providerType", { length: 64 }),
|
||
providerName: varchar("providerName", { length: 128 }),
|
||
eventType: varchar("eventType", { length: 32 }).notNull().default("error"),
|
||
severity: varchar("severity", { length: 24 }).notNull().default("error"),
|
||
context: varchar("context", { length: 96 }),
|
||
counted: boolean("counted").notNull().default(true),
|
||
consecutiveFailures: integer("consecutiveFailures").notNull().default(0),
|
||
autoDisabled: boolean("autoDisabled").notNull().default(false),
|
||
elapsedMs: integer("elapsedMs"),
|
||
requestId: varchar("requestId", { length: 128 }),
|
||
errorMessage: text("errorMessage"),
|
||
metadata: jsonb("metadata"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AiModelHealthEvent = InferSelectModel<typeof aiModelHealthEvent>;
|
||
|
||
// 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>;
|
||
|
||
// 阿里云 OSS 图片迁移任务表
|
||
export const storageMigrationTask = pgTable("StorageMigrationTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
sourceUrl: text("sourceUrl").notNull().unique(),
|
||
sourcePath: text("sourcePath").notNull(),
|
||
sourceStorageConfigId: uuid("sourceStorageConfigId").references(() => storageConfig.id),
|
||
targetStorageConfigId: uuid("targetStorageConfigId").references(() => storageConfig.id),
|
||
targetUrl: text("targetUrl"),
|
||
status: varchar("status", { length: 32 }).notNull().default("pending"),
|
||
attemptCount: integer("attemptCount").notNull().default(0),
|
||
lastError: text("lastError"),
|
||
migrateAfterAt: timestamp("migrateAfterAt").notNull(),
|
||
cleanupAfterAt: timestamp("cleanupAfterAt").notNull(),
|
||
migratedAt: timestamp("migratedAt"),
|
||
cleanedAt: timestamp("cleanedAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type StorageMigrationTask = InferSelectModel<typeof storageMigrationTask>;
|
||
|
||
// 网站设置表
|
||
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), // 是否显示精选榜按钮
|
||
productPlanningEnabled: boolean("productPlanningEnabled").notNull().default(true), // 是否显示并启用商品策划工具
|
||
// 无限画布设置
|
||
canvasEnabled: boolean("canvasEnabled").notNull().default(true), // 是否启用无限画布功能
|
||
agentEnabled: boolean("agentEnabled").notNull().default(true), // 是否启用风格化功能
|
||
realtimeCanvasEnabled: boolean("realtimeCanvasEnabled").notNull().default(true), // 是否显示实时画布入口
|
||
canvasDefaultModelId: varchar("canvasDefaultModelId", { length: 128 }), // 画布默认选择的模型ID(null表示智能自动)
|
||
canvasAutoModeDefaultModelId: varchar("canvasAutoModeDefaultModelId", { length: 128 }), // 智能自动模式下实际使用的默认生图模型ID
|
||
agentWorkflowDefaultModelId: varchar("agentWorkflowDefaultModelId", { length: 128 }), // 风格化专用默认生图模型ID
|
||
canvasDetailDefaultModelId: varchar("canvasDetailDefaultModelId", { length: 128 }), // AI详情生成专用默认生图模型ID
|
||
canvasDetailAgentModelId: varchar("canvasDetailAgentModelId", { length: 128 }), // AI详情生成专用Agent模型ID(规划/复审)
|
||
lovartPromoText: text("lovartPromoText").default(
|
||
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠"
|
||
),
|
||
lovartPromoActionText: varchar("lovartPromoActionText", { length: 32 }).default("立即升级"),
|
||
lovartHeroTitle: text("lovartHeroTitle").default("由壹点AI 让设计更简单"),
|
||
lovartHeroSubtitle: text("lovartHeroSubtitle").default("懂你的设计代理,帮你搞定一切"),
|
||
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"),
|
||
googleClientId: text("googleClientId"),
|
||
googleClientSecret: text("googleClientSecret"),
|
||
authUrl: text("authUrl"),
|
||
// 多组织钉钉配置,存储为 JSON 数组:[{ name, appKey, appSecret }]
|
||
dingTalkOrganizations: jsonb("dingTalkOrganizations").$type<
|
||
{ name: string; appKey: string; appSecret: string }[] | null
|
||
>(),
|
||
isDingTalkEnabled: boolean("isDingTalkEnabled").notNull().default(false),
|
||
isGoogleEnabled: boolean("isGoogleEnabled").notNull().default(false),
|
||
isRegistrationEnabled: boolean("isRegistrationEnabled").notNull().default(true),
|
||
isEmailVerificationEnabled: boolean("isEmailVerificationEnabled")
|
||
.notNull()
|
||
.default(false),
|
||
emailVerificationExpiresMinutes: integer("emailVerificationExpiresMinutes")
|
||
.notNull()
|
||
.default(10),
|
||
resendApiKey: text("resendApiKey"),
|
||
resendFromEmail: text("resendFromEmail"),
|
||
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 emailVerificationCode = pgTable("EmailVerificationCode", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
email: varchar("email", { length: 64 }).notNull(),
|
||
purpose: varchar("purpose", { length: 32 }).notNull().default("register"),
|
||
codeHash: varchar("codeHash", { length: 128 }).notNull(),
|
||
expiresAt: timestamp("expiresAt").notNull(),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type EmailVerificationCode = InferSelectModel<typeof emailVerificationCode>;
|
||
|
||
// 生成图片记录表
|
||
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), // 是否精选
|
||
featuredCategory: varchar("featuredCategory", { length: 64 }), // 精选分类
|
||
featuredAt: timestamp("featuredAt"), // 精选时间
|
||
featuredBy: uuid("featuredBy").references(() => user.id), // 精选操作人
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type GeneratedImage = InferSelectModel<typeof generatedImage>;
|
||
|
||
// 风格化分类表
|
||
export const agentPresetCategory = pgTable("AgentPresetCategory", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
name: varchar("name", { length: 64 }).notNull(),
|
||
slug: varchar("slug", { length: 64 }).notNull(),
|
||
scope: varchar("scope", { enum: ["public", "private", "global"] })
|
||
.notNull()
|
||
.default("public"),
|
||
createdBy: uuid("createdBy").references(() => user.id, { onDelete: "set null" }),
|
||
sortOrder: integer("sortOrder").notNull().default(0),
|
||
isEnabled: boolean("isEnabled").notNull().default(true),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AgentPresetCategory = InferSelectModel<typeof agentPresetCategory>;
|
||
|
||
// 风格化表
|
||
export const agentPreset = pgTable("AgentPreset", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
ownerUserId: uuid("ownerUserId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
name: varchar("name", { length: 128 }).notNull(),
|
||
type: varchar("type", { enum: ["image", "video", "text"] })
|
||
.notNull()
|
||
.default("image"),
|
||
secondaryType: varchar("secondaryType", { length: 128 }),
|
||
visibility: varchar("visibility", { enum: ["public", "private"] })
|
||
.notNull()
|
||
.default("private"),
|
||
categoryId: uuid("categoryId").references(() => agentPresetCategory.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
categoryPath: jsonb("categoryPath").$type<{
|
||
sectionId?: string | null;
|
||
sectionName?: string | null;
|
||
level1Id?: string | null;
|
||
level1Name?: string | null;
|
||
level2Id?: string | null;
|
||
level2Name?: string | null;
|
||
level3Id?: string | null;
|
||
level3Name?: string | null;
|
||
productCategory?: string | null;
|
||
audienceTag?: string | null;
|
||
usageScenario?: string | null;
|
||
brand?: string | null;
|
||
}>(),
|
||
promptTemplate: text("promptTemplate").notNull().default(""),
|
||
description: text("description"),
|
||
defaultModelId: uuid("defaultModelId").references(() => aiModel.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
defaultParams: jsonb("defaultParams").$type<Record<string, any>>(),
|
||
slotCount: integer("slotCount").notNull().default(1),
|
||
requiredUploadCount: integer("requiredUploadCount").notNull().default(1),
|
||
coverAssetId: uuid("coverAssetId"),
|
||
status: varchar("status", { enum: ["active", "disabled", "hidden", "blocked"] })
|
||
.notNull()
|
||
.default("active"),
|
||
sortOrder: integer("sortOrder").notNull().default(0),
|
||
usageCount: integer("usageCount").notNull().default(0),
|
||
lastUsedAt: timestamp("lastUsedAt"),
|
||
forkedFromPresetId: uuid("forkedFromPresetId"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AgentPreset = InferSelectModel<typeof agentPreset>;
|
||
|
||
// 风格化槽位表(每个槽位=一套提示词 + 一组参考图 + 一张用户上传图)
|
||
export const agentPresetSlot = pgTable("AgentPresetSlot", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
presetId: uuid("presetId")
|
||
.notNull()
|
||
.references(() => agentPreset.id, { onDelete: "cascade" }),
|
||
slotIndex: integer("slotIndex").notNull(),
|
||
title: varchar("title", { length: 128 }),
|
||
promptTemplate: text("promptTemplate").notNull().default(""),
|
||
defaultParams: jsonb("defaultParams").$type<Record<string, any>>(),
|
||
requireUpload: boolean("requireUpload").notNull().default(true),
|
||
uploadLabel: varchar("uploadLabel", { length: 128 }),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AgentPresetSlot = InferSelectModel<typeof agentPresetSlot>;
|
||
|
||
// ============================================
|
||
// 无限画布相关表
|
||
// ============================================
|
||
|
||
// 画布项目表
|
||
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"),
|
||
workspaceType: varchar("workspaceType", { enum: ["canvas", "agent", "video"] })
|
||
.notNull()
|
||
.default("canvas"),
|
||
// 存储完整画布状态(节点、边、视口等)
|
||
canvasData: jsonb("canvasData").$type<{
|
||
nodes: any[];
|
||
edges?: any[];
|
||
selectedNodeIds?: string[];
|
||
viewport?: { x: number; y: number; zoom: number };
|
||
tool?: string;
|
||
suppressedMediaKeys?: string[];
|
||
workflowState?: {
|
||
inputs?: {
|
||
primarySku?: string;
|
||
secondarySku?: string;
|
||
brand?: string;
|
||
store?: string;
|
||
thirdCategory?: string;
|
||
fourthCategory?: string;
|
||
fifthCategory?: string;
|
||
priceBand?: string;
|
||
};
|
||
appearanceRecognitionEnabled?: boolean;
|
||
materials?: {
|
||
primary?: {
|
||
result?: {
|
||
query?: string;
|
||
matchedSku?: string;
|
||
materialId?: string | null;
|
||
productModel?: string | null;
|
||
zipUrl?: string | null;
|
||
fixedCount?: number;
|
||
otherCount?: number;
|
||
totalCount?: number;
|
||
images?: Array<{
|
||
id?: string;
|
||
key?: string;
|
||
label?: string;
|
||
fileName?: string;
|
||
imageUrl?: string;
|
||
assetId?: string | null;
|
||
mimeType?: string | null;
|
||
appearanceSummary?: string | null;
|
||
group?: "fixed" | "other";
|
||
sortOrder?: number;
|
||
}>;
|
||
} | null;
|
||
selectedImageIds?: string[];
|
||
};
|
||
secondary?: {
|
||
result?: {
|
||
query?: string;
|
||
matchedSku?: string;
|
||
materialId?: string | null;
|
||
productModel?: string | null;
|
||
zipUrl?: string | null;
|
||
fixedCount?: number;
|
||
otherCount?: number;
|
||
totalCount?: number;
|
||
images?: Array<{
|
||
id?: string;
|
||
key?: string;
|
||
label?: string;
|
||
fileName?: string;
|
||
imageUrl?: string;
|
||
assetId?: string | null;
|
||
mimeType?: string | null;
|
||
appearanceSummary?: string | null;
|
||
group?: "fixed" | "other";
|
||
sortOrder?: number;
|
||
}>;
|
||
} | null;
|
||
selectedImageIds?: string[];
|
||
};
|
||
};
|
||
selections?: Record<string, string>;
|
||
selectedAgentPresetId?: string | null;
|
||
currentStage?: "workflow" | "mode";
|
||
};
|
||
}>(),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type CanvasProject = InferSelectModel<typeof canvasProject>;
|
||
|
||
// AI详情项目存档表
|
||
export const canvasDetailArchive = pgTable("CanvasDetailArchive", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
projectId: uuid("projectId")
|
||
.notNull()
|
||
.references(() => canvasProject.id, { onDelete: "cascade" }),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
title: varchar("title", { length: 256 }).notNull().default("AI详情项目"),
|
||
prompt: text("prompt").notNull().default(""),
|
||
generationMode: varchar("generationMode", { length: 32 })
|
||
.notNull()
|
||
.default("classic"),
|
||
brief: jsonb("brief").notNull(),
|
||
slots: jsonb("slots").notNull(),
|
||
summary: text("summary"),
|
||
imageCount: integer("imageCount"),
|
||
coverUrl: text("coverUrl"),
|
||
uiSurfaceSnapshot: jsonb("uiSurfaceSnapshot").$type<Record<string, any> | null>(),
|
||
uiSurfaceLatestReview: jsonb("uiSurfaceLatestReview").$type<Record<string, any> | null>(),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type CanvasDetailArchive = InferSelectModel<typeof canvasDetailArchive>;
|
||
|
||
// 画布图片生成任务表(异步队列)
|
||
export const canvasImageTask = pgTable("CanvasImageTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
status: varchar("status", { length: 16 })
|
||
.notNull()
|
||
.default("pending"), // pending | running | succeeded | failed
|
||
request: jsonb("request").notNull(),
|
||
result: jsonb("result"),
|
||
error: text("error"),
|
||
attempts: integer("attempts").notNull().default(0),
|
||
startedAt: timestamp("startedAt"),
|
||
completedAt: timestamp("completedAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type CanvasImageTask = InferSelectModel<typeof canvasImageTask>;
|
||
|
||
export const enhanceProTask = pgTable("EnhanceProTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||
messageId: uuid("messageId"),
|
||
clientRequestId: varchar("clientRequestId", { length: 64 }),
|
||
providerTaskId: varchar("providerTaskId", { length: 128 }).notNull(),
|
||
taskKeyId: varchar("taskKeyId", { length: 64 }),
|
||
status: varchar("status", { length: 16 })
|
||
.notNull()
|
||
.default("pending"), // pending | running | finalizing | succeeded | failed
|
||
prompt: text("prompt"),
|
||
sourceImageUrl: text("sourceImageUrl").notNull(),
|
||
request: jsonb("request").notNull(),
|
||
result: jsonb("result"),
|
||
error: text("error"),
|
||
pointsCost: integer("pointsCost").notNull().default(0),
|
||
pointsCharged: boolean("pointsCharged").notNull().default(false),
|
||
chargedAt: timestamp("chargedAt"),
|
||
completedAt: timestamp("completedAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type EnhanceProTask = InferSelectModel<typeof enhanceProTask>;
|
||
|
||
export const agentPsdRenderTask = pgTable("AgentPsdRenderTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
status: varchar("status", { length: 16 })
|
||
.notNull()
|
||
.default("pending"), // pending | running | succeeded | failed
|
||
request: jsonb("request").notNull(),
|
||
result: jsonb("result"),
|
||
error: text("error"),
|
||
attempts: integer("attempts").notNull().default(0),
|
||
startedAt: timestamp("startedAt"),
|
||
completedAt: timestamp("completedAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AgentPsdRenderTask = InferSelectModel<typeof agentPsdRenderTask>;
|
||
|
||
// 画布节点表(可选,用于更细粒度的查询)
|
||
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' | 'model3d'
|
||
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>;
|
||
|
||
// 3D 资产库记录表(用于模型页资产库持久化)
|
||
export const model3dAsset = pgTable("Model3DAsset", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
projectId: uuid("projectId")
|
||
.notNull()
|
||
.references(() => canvasProject.id, { onDelete: "cascade" }),
|
||
taskId: varchar("taskId", { length: 128 }).notNull(),
|
||
provider: varchar("provider", { length: 32 }).notNull().default("tripo-3d"),
|
||
status: varchar("status", { length: 32 }).notNull().default("queued"),
|
||
mode: varchar("mode", { length: 32 }).notNull().default("text"),
|
||
prompt: text("prompt").notNull().default(""),
|
||
modelVersion: varchar("modelVersion", { length: 64 }).notNull().default(""),
|
||
progress: integer("progress"),
|
||
modelUrl: text("modelUrl"),
|
||
previewImageUrl: text("previewImageUrl"),
|
||
error: text("error"),
|
||
sourceTaskId: varchar("sourceTaskId", { length: 128 }),
|
||
exportFormat: varchar("exportFormat", { length: 32 }),
|
||
assetPayload: jsonb("assetPayload").$type<Record<string, unknown> | null>(),
|
||
shareEnabled: boolean("shareEnabled").notNull().default(false),
|
||
shareToken: varchar("shareToken", { length: 64 }),
|
||
sharedAt: timestamp("sharedAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type Model3DAsset = InferSelectModel<typeof model3dAsset>;
|
||
|
||
// 九宫格视频分镜任务主表
|
||
export const videoStoryboardTask = pgTable("VideoStoryboardTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||
messageId: uuid("messageId").references(() => message.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
providerType: varchar("providerType", { length: 64 })
|
||
.notNull()
|
||
.default("ruxa-video"),
|
||
modelRef: varchar("modelRef", { length: 128 }),
|
||
gridType: varchar("gridType", { length: 32 }),
|
||
gridRows: integer("gridRows"),
|
||
gridCols: integer("gridCols"),
|
||
sourceImageUrl: text("sourceImageUrl").notNull(),
|
||
isNineGrid: boolean("isNineGrid").notNull().default(false),
|
||
detectionConfidence: integer("detectionConfidence").notNull().default(0),
|
||
normalizedScript: jsonb("normalizedScript"),
|
||
status: varchar("status", { length: 32 }).notNull().default("pending"),
|
||
confirmationStatus: varchar("confirmationStatus", { length: 32 })
|
||
.notNull()
|
||
.default("draft"),
|
||
hdImageStageStatus: varchar("hdImageStageStatus", { length: 32 })
|
||
.notNull()
|
||
.default("pending"),
|
||
currentShot: integer("currentShot").notNull().default(0),
|
||
totalShots: integer("totalShots").notNull().default(9),
|
||
error: text("error"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
startedAt: timestamp("startedAt"),
|
||
confirmedAt: timestamp("confirmedAt"),
|
||
confirmedByMessageId: uuid("confirmedByMessageId").references(() => message.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
completedAt: timestamp("completedAt"),
|
||
});
|
||
|
||
export type VideoStoryboardTask = InferSelectModel<typeof videoStoryboardTask>;
|
||
|
||
// 九宫格视频分镜子任务表
|
||
export const videoStoryboardShotTask = pgTable("VideoStoryboardShotTask", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
parentTaskId: uuid("parentTaskId")
|
||
.notNull()
|
||
.references(() => videoStoryboardTask.id, { onDelete: "cascade" }),
|
||
shotIndex: integer("shotIndex").notNull(),
|
||
tileIndex: integer("tileIndex"),
|
||
tileImageUrl: text("tileImageUrl"),
|
||
upscaledImageUrl: text("upscaledImageUrl"),
|
||
hdImageUrl: text("hdImageUrl"),
|
||
hdImagePrompt: text("hdImagePrompt"),
|
||
prompt: text("prompt"),
|
||
modelId: varchar("modelId", { length: 128 }),
|
||
fallbackChain: jsonb("fallbackChain"),
|
||
attemptCount: integer("attemptCount").notNull().default(0),
|
||
status: varchar("status", { length: 32 }).notNull().default("pending"),
|
||
hdImageStatus: varchar("hdImageStatus", { length: 32 }).notNull().default("pending"),
|
||
videoStatus: varchar("videoStatus", { length: 32 }).notNull().default("pending"),
|
||
userApproved: boolean("userApproved").notNull().default(false),
|
||
userApprovedAt: timestamp("userApprovedAt"),
|
||
videoUrl: text("videoUrl"),
|
||
errorCode: varchar("errorCode", { length: 64 }),
|
||
errorMessage: text("errorMessage"),
|
||
startedAt: timestamp("startedAt"),
|
||
completedAt: timestamp("completedAt"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type VideoStoryboardShotTask = InferSelectModel<typeof videoStoryboardShotTask>;
|
||
|
||
// 会话工作记忆表(用于连续对话中的图片/资产目标定位)
|
||
export const conversationWorkingState = pgTable("ConversationWorkingState", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
chatId: uuid("chatId")
|
||
.notNull()
|
||
.references(() => chat.id, { onDelete: "cascade" })
|
||
.unique(),
|
||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
currentIntent: varchar("currentIntent", { length: 64 }),
|
||
currentSubIntent: varchar("currentSubIntent", { length: 64 }),
|
||
currentFocusAssetIds: jsonb("currentFocusAssetIds").$type<string[]>().notNull().default([]),
|
||
currentFocusImageUrls: jsonb("currentFocusImageUrls").$type<string[]>().notNull().default([]),
|
||
selectedAssetIds: jsonb("selectedAssetIds").$type<string[]>().notNull().default([]),
|
||
selectedImageUrls: jsonb("selectedImageUrls").$type<string[]>().notNull().default([]),
|
||
lastUploadedAssetIds: jsonb("lastUploadedAssetIds").$type<string[]>().notNull().default([]),
|
||
lastUploadedImageUrls: jsonb("lastUploadedImageUrls").$type<string[]>().notNull().default([]),
|
||
lastGeneratedAssetIds: jsonb("lastGeneratedAssetIds").$type<string[]>().notNull().default([]),
|
||
lastGeneratedImageUrls: jsonb("lastGeneratedImageUrls").$type<string[]>().notNull().default([]),
|
||
lastReferencedAssetIds: jsonb("lastReferencedAssetIds").$type<string[]>().notNull().default([]),
|
||
lastReferencedImageUrls: jsonb("lastReferencedImageUrls").$type<string[]>().notNull().default([]),
|
||
lastResolvedTargetAssetIds: jsonb("lastResolvedTargetAssetIds")
|
||
.$type<string[]>()
|
||
.notNull()
|
||
.default([]),
|
||
lastResolvedTargetImageUrls: jsonb("lastResolvedTargetImageUrls")
|
||
.$type<string[]>()
|
||
.notNull()
|
||
.default([]),
|
||
activeCanvasNodeIds: jsonb("activeCanvasNodeIds").$type<string[]>().notNull().default([]),
|
||
lastCanvasSnapshot: jsonb("lastCanvasSnapshot"),
|
||
lastTurnSummary: jsonb("lastTurnSummary"),
|
||
lastMessageId: uuid("lastMessageId"),
|
||
turnCount: integer("turnCount").notNull().default(0),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type ConversationWorkingState = InferSelectModel<typeof conversationWorkingState>;
|
||
|
||
export const aiCadWorkspace = pgTable("AiCadWorkspace", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
name: varchar("name", { length: 128 }).notNull().default("AI-CAD 工作台"),
|
||
prompt: text("prompt").notNull().default(""),
|
||
generationProvider: varchar("generationProvider", {
|
||
length: 32,
|
||
enum: ["kittycad-cad", "tripo-3d"],
|
||
})
|
||
.notNull()
|
||
.default("kittycad-cad"),
|
||
modelVersion: varchar("modelVersion", { length: 128 })
|
||
.notNull()
|
||
.default("Turbo-v1.0-20250506"),
|
||
viewerSettings: jsonb("viewerSettings").notNull(),
|
||
messages: jsonb("messages").$type<Record<string, unknown>[]>().notNull().default([]),
|
||
versions: jsonb("versions").$type<Record<string, unknown>[]>().notNull().default([]),
|
||
activeVersionId: varchar("activeVersionId", { length: 64 }),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AiCadWorkspace = InferSelectModel<typeof aiCadWorkspace>;
|
||
|
||
export type FlowBoardSnapshotData = {
|
||
children: Record<string, unknown>[];
|
||
viewport?: Record<string, unknown> | null;
|
||
theme?: Record<string, unknown> | null;
|
||
updatedAt?: string | null;
|
||
};
|
||
|
||
export const flowBoardDraft = pgTable("FlowBoardDraft", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
title: varchar("title", { length: 256 })
|
||
.notNull()
|
||
.default("未命名流程白板"),
|
||
snapshot: jsonb("snapshot")
|
||
.$type<FlowBoardSnapshotData>()
|
||
.notNull()
|
||
.default({ children: [] }),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type FlowBoardDraft = InferSelectModel<typeof flowBoardDraft>;
|
||
|
||
// 每轮消息的目标资产/图片定位审计表
|
||
export const messageTargetResolution = pgTable("MessageTargetResolution", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
chatId: uuid("chatId")
|
||
.notNull()
|
||
.references(() => chat.id, { onDelete: "cascade" }),
|
||
messageId: uuid("messageId").references(() => message.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
rawText: text("rawText").notNull(),
|
||
uploadedAssetIds: jsonb("uploadedAssetIds").$type<string[]>().notNull().default([]),
|
||
uploadedImageUrls: jsonb("uploadedImageUrls").$type<string[]>().notNull().default([]),
|
||
selectedAssetIds: jsonb("selectedAssetIds").$type<string[]>().notNull().default([]),
|
||
selectedImageUrls: jsonb("selectedImageUrls").$type<string[]>().notNull().default([]),
|
||
candidateAssetIds: jsonb("candidateAssetIds").$type<string[]>().notNull().default([]),
|
||
candidateImageUrls: jsonb("candidateImageUrls").$type<string[]>().notNull().default([]),
|
||
resolvedAssetIds: jsonb("resolvedAssetIds").$type<string[]>().notNull().default([]),
|
||
resolvedImageUrls: jsonb("resolvedImageUrls").$type<string[]>().notNull().default([]),
|
||
resolutionMode: varchar("resolutionMode", { length: 64 }).notNull(),
|
||
confidence: integer("confidence").notNull().default(0),
|
||
needsClarification: boolean("needsClarification").notNull().default(false),
|
||
reasoning: jsonb("reasoning"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type MessageTargetResolution = InferSelectModel<typeof messageTargetResolution>;
|
||
|
||
// 统一媒体资产表(上传图 / 生成图 / 视频 / 3D 预览的统一索引)
|
||
export const mediaAsset = pgTable("MediaAsset", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
mediaType: varchar("mediaType", { length: 32 }).notNull(),
|
||
sourceType: varchar("sourceType", { length: 32 }).notNull(),
|
||
status: varchar("status", { length: 32 }).notNull().default("ready"),
|
||
originalUrl: text("originalUrl").notNull(),
|
||
normalizedUrl: text("normalizedUrl").notNull(),
|
||
previewUrl: text("previewUrl"),
|
||
thumbnailUrl: text("thumbnailUrl"),
|
||
mimeType: varchar("mimeType", { length: 128 }),
|
||
width: integer("width"),
|
||
height: integer("height"),
|
||
durationMs: integer("durationMs"),
|
||
displayName: varchar("displayName", { length: 256 }),
|
||
prompt: text("prompt"),
|
||
modelId: varchar("modelId", { length: 128 }),
|
||
sourceMessageId: uuid("sourceMessageId").references(() => message.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
sourceTable: varchar("sourceTable", { length: 64 }),
|
||
sourceRowId: varchar("sourceRowId", { length: 128 }),
|
||
sourceNodeId: varchar("sourceNodeId", { length: 128 }),
|
||
rootAssetId: uuid("rootAssetId"),
|
||
primaryParentAssetId: uuid("primaryParentAssetId"),
|
||
summary: jsonb("summary"),
|
||
metadata: jsonb("metadata"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type MediaAsset = InferSelectModel<typeof mediaAsset>;
|
||
|
||
// 媒体资产关系表(版本链 / 画布节点绑定 / 来源关系)
|
||
export const mediaAssetRelation = pgTable("MediaAssetRelation", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
fromAssetId: uuid("fromAssetId").notNull().references(() => mediaAsset.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
toAssetId: uuid("toAssetId").notNull().references(() => mediaAsset.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
relationType: varchar("relationType", { length: 64 }).notNull(),
|
||
sourceMessageId: uuid("sourceMessageId").references(() => message.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
sourceNodeId: varchar("sourceNodeId", { length: 128 }),
|
||
confidence: integer("confidence").notNull().default(100),
|
||
metadata: jsonb("metadata"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type MediaAssetRelation = InferSelectModel<typeof mediaAssetRelation>;
|
||
|
||
// 资产记忆快照表(视觉摘要 / 主体标签 / 场景标签等可复用缓存)
|
||
export const assetMemorySnapshot = pgTable("AssetMemorySnapshot", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
assetId: uuid("assetId").references(() => mediaAsset.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "cascade" }),
|
||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
snapshotType: varchar("snapshotType", { length: 32 })
|
||
.notNull()
|
||
.default("visual_summary"),
|
||
status: varchar("status", { length: 32 }).notNull().default("ready"),
|
||
normalizedUrl: text("normalizedUrl").notNull(),
|
||
summaryText: text("summaryText"),
|
||
subjectTags: jsonb("subjectTags").$type<string[]>().notNull().default([]),
|
||
sceneTags: jsonb("sceneTags").$type<string[]>().notNull().default([]),
|
||
styleTags: jsonb("styleTags").$type<string[]>().notNull().default([]),
|
||
lightingTags: jsonb("lightingTags").$type<string[]>().notNull().default([]),
|
||
confidence: integer("confidence").notNull().default(0),
|
||
sourceModelId: varchar("sourceModelId", { length: 128 }),
|
||
metadata: jsonb("metadata"),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AssetMemorySnapshot = InferSelectModel<typeof assetMemorySnapshot>;
|
||
|
||
// 风格化参考图绑定表
|
||
export const agentPresetReferenceAsset = pgTable("AgentPresetReferenceAsset", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
presetId: uuid("presetId")
|
||
.notNull()
|
||
.references(() => agentPreset.id, { onDelete: "cascade" }),
|
||
slotId: uuid("slotId").references(() => agentPresetSlot.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
assetId: uuid("assetId")
|
||
.notNull()
|
||
.references(() => mediaAsset.id, { onDelete: "cascade" }),
|
||
sortOrder: integer("sortOrder").notNull().default(0),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AgentPresetReferenceAsset = InferSelectModel<typeof agentPresetReferenceAsset>;
|
||
|
||
// 风格化执行快照表
|
||
export const agentPresetExecutionSnapshot = pgTable("AgentPresetExecutionSnapshot", {
|
||
id: uuid("id").primaryKey().notNull().defaultRandom(),
|
||
presetId: uuid("presetId")
|
||
.notNull()
|
||
.references(() => agentPreset.id, { onDelete: "cascade" }),
|
||
projectId: uuid("projectId").references(() => canvasProject.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
chatId: uuid("chatId").references(() => chat.id, { onDelete: "set null" }),
|
||
messageId: uuid("messageId").references(() => message.id, { onDelete: "set null" }),
|
||
userId: uuid("userId")
|
||
.notNull()
|
||
.references(() => user.id, { onDelete: "cascade" }),
|
||
presetName: varchar("presetName", { length: 128 }).notNull(),
|
||
presetType: varchar("presetType", { enum: ["image", "video", "text"] }).notNull(),
|
||
secondaryType: varchar("secondaryType", { length: 128 }),
|
||
visibility: varchar("visibility", { enum: ["public", "private"] }).notNull(),
|
||
categoryId: uuid("categoryId").references(() => agentPresetCategory.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
categoryName: varchar("categoryName", { length: 64 }),
|
||
promptTemplate: text("promptTemplate").notNull().default(""),
|
||
defaultModelId: uuid("defaultModelId").references(() => aiModel.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
resolvedModelId: uuid("resolvedModelId").references(() => aiModel.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
defaultParams: jsonb("defaultParams").$type<Record<string, any>>(),
|
||
slotCount: integer("slotCount").notNull().default(1),
|
||
requiredUploadCount: integer("requiredUploadCount").notNull().default(1),
|
||
slots: jsonb("slots")
|
||
.$type<
|
||
Array<{
|
||
slotIndex: number;
|
||
title?: string | null;
|
||
promptTemplate: string;
|
||
defaultParams?: Record<string, any> | null;
|
||
requireUpload?: boolean;
|
||
uploadLabel?: string | null;
|
||
referenceAssetIds: string[];
|
||
referenceImageUrls: string[];
|
||
}>
|
||
>()
|
||
.notNull()
|
||
.default([]),
|
||
referenceAssetIds: jsonb("referenceAssetIds").$type<string[]>().notNull().default([]),
|
||
referenceImageUrls: jsonb("referenceImageUrls").$type<string[]>().notNull().default([]),
|
||
userInputText: text("userInputText"),
|
||
requestPayload: jsonb("requestPayload").$type<Record<string, any>>(),
|
||
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||
});
|
||
|
||
export type AgentPresetExecutionSnapshot = InferSelectModel<
|
||
typeof agentPresetExecutionSnapshot
|
||
>;
|