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; 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; 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; 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(), }); export type Chat = InferSelectModel; // DEPRECATED: Legacy message storage kept for historical data migration. // New internal generation flows use Message_v2 parts and attachments. 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; 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; // DEPRECATED: Legacy vote storage tied to the historical Message table. // New vote records reference Message_v2 through vote. 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; 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; 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; 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; 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; 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; 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; export const systemSettingSnapshot = pgTable("SystemSettingSnapshot", { id: uuid("id").primaryKey().notNull().defaultRandom(), scope: varchar("scope", { length: 64 }).notNull(), label: varchar("label", { length: 128 }), summary: jsonb("summary"), data: jsonb("data").notNull(), createdAt: timestamp("createdAt").notNull().defaultNow(), createdBy: uuid("createdBy").references(() => user.id), }); export type SystemSettingSnapshot = InferSelectModel; 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; // 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", "snapgen-image", "openai-image", "bananaimage", "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; // 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), // 不参与故障迁移 excludeFromHealthBan: boolean("excludeFromHealthBan").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; 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; // S3 存储配置表 export const storageConfig = pgTable("StorageConfig", { id: uuid("id").primaryKey().notNull().defaultRandom(), name: varchar("name", { length: 128 }).notNull(), // 配置名称 type: varchar("type", { enum: ["s3", "local", "qiniu-kodo"] }).notNull().default("s3"), endpoint: text("endpoint"), // S3/七牛 Kodo 兼容端点 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; // 阿里云 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; // AI 生成图片临时地址归档任务表 export const imageArchiveTask = pgTable("ImageArchiveTask", { id: uuid("id").primaryKey().notNull().defaultRandom(), sourceUrl: text("sourceUrl").notNull().unique(), targetUrl: text("targetUrl"), targetStorageConfigId: uuid("targetStorageConfigId").references(() => storageConfig.id), status: varchar("status", { length: 32 }).notNull().default("pending"), attemptCount: integer("attemptCount").notNull().default(0), lastError: text("lastError"), archiveAfterAt: timestamp("archiveAfterAt").notNull(), replaceAfterAt: timestamp("replaceAfterAt").notNull(), archivedAt: timestamp("archivedAt"), replacedAt: timestamp("replacedAt"), metadata: jsonb("metadata"), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type ImageArchiveTask = InferSelectModel; // 网站设置表 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("限时特惠:升级会员最高立享 45% OFF!"), // 广告横幅文字 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; // 导航链接表 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; // 视频教程表 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; // 认证配置表 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; // 临时登录令牌表 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; 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; // 生成图片记录表 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; export const externalImageApiRequestLog = pgTable("ExternalImageApiRequestLog", { id: uuid("id").primaryKey().notNull().defaultRandom(), apiKeyId: varchar("apiKeyId", { length: 64 }), apiKeyName: varchar("apiKeyName", { length: 128 }), ownerUserId: uuid("ownerUserId").references(() => user.id), modelDbId: uuid("modelDbId").references(() => aiModel.id, { onDelete: "set null", }), modelRuntimeId: varchar("modelRuntimeId", { length: 128 }), modelDisplayName: varchar("modelDisplayName", { length: 128 }), prompt: text("prompt"), negativePrompt: text("negativePrompt"), imageCount: integer("imageCount").notNull().default(1), aspectRatio: varchar("aspectRatio", { length: 32 }), imageSize: varchar("imageSize", { length: 32 }), status: varchar("status", { length: 32 }).notNull().default("pending"), requesterIp: varchar("requesterIp", { length: 64 }), requesterUserAgent: text("requesterUserAgent"), requestPayload: jsonb("requestPayload"), responsePayload: jsonb("responsePayload"), generatedImageIds: jsonb("generatedImageIds"), generatedAssetIds: jsonb("generatedAssetIds"), generatedImageUrls: jsonb("generatedImageUrls"), errorMessage: text("errorMessage"), durationMs: integer("durationMs"), createdAt: timestamp("createdAt").notNull().defaultNow(), completedAt: timestamp("completedAt"), }); export type ExternalImageApiRequestLog = InferSelectModel< typeof externalImageApiRequestLog >; export const clientPerformanceEvent = pgTable("ClientPerformanceEvent", { id: uuid("id").primaryKey().notNull().defaultRandom(), userId: uuid("userId").references(() => user.id, { onDelete: "set null" }), chatId: uuid("chatId").references(() => chat.id, { onDelete: "set null" }), projectId: varchar("projectId", { length: 64 }), eventType: varchar("eventType", { length: 64 }).notNull(), section: varchar("section", { length: 32 }).notNull().default("unknown"), action: varchar("action", { length: 64 }), path: varchar("path", { length: 256 }), durationMs: integer("durationMs").notNull().default(0), blockingDurationMs: integer("blockingDurationMs").notNull().default(0), domNodeCount: integer("domNodeCount").notNull().default(0), visibleImageCount: integer("visibleImageCount").notNull().default(0), loadedImageCount: integer("loadedImageCount").notNull().default(0), viewport: jsonb("viewport"), browser: varchar("browser", { length: 128 }), metadata: jsonb("metadata"), requesterIp: varchar("requesterIp", { length: 64 }), requesterUserAgent: text("requesterUserAgent"), occurredAt: timestamp("occurredAt"), createdAt: timestamp("createdAt").notNull().defaultNow(), }); export type ClientPerformanceEvent = InferSelectModel< typeof clientPerformanceEvent >; export const accessIpEvent = pgTable("AccessIpEvent", { id: uuid("id").primaryKey().notNull().defaultRandom(), eventType: varchar("eventType", { length: 32 }).notNull().default("request"), ip: varchar("ip", { length: 64 }).notNull(), userId: uuid("userId").references(() => user.id, { onDelete: "set null" }), userEmail: varchar("userEmail", { length: 128 }), userRole: varchar("userRole", { length: 32 }), method: varchar("method", { length: 12 }), path: varchar("path", { length: 256 }), routeGroup: varchar("routeGroup", { length: 64 }), statusCode: integer("statusCode"), outcome: varchar("outcome", { length: 32 }).notNull().default("allowed"), severity: varchar("severity", { length: 16 }).notNull().default("info"), reasons: jsonb("reasons").$type(), userAgent: text("userAgent"), referer: text("referer"), metadata: jsonb("metadata"), occurredAt: timestamp("occurredAt").notNull().defaultNow(), createdAt: timestamp("createdAt").notNull().defaultNow(), }); export type AccessIpEvent = InferSelectModel; export const accessIpBlocklist = pgTable("AccessIpBlocklist", { id: uuid("id").primaryKey().notNull().defaultRandom(), ip: varchar("ip", { length: 128 }).notNull(), reason: text("reason"), isActive: boolean("isActive").notNull().default(true), expiresAt: timestamp("expiresAt"), createdBy: uuid("createdBy").references(() => user.id, { onDelete: "set null" }), updatedBy: uuid("updatedBy").references(() => user.id, { onDelete: "set null" }), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type AccessIpBlocklist = InferSelectModel; // 风格化分类表 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; // 风格化表 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>(), 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; // 风格化槽位表(每个槽位=一套提示词 + 一组参考图 + 一张用户上传图) 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>(), 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; // ============================================ // 无限画布相关表 // ============================================ // 画布项目表 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; selectedAgentPresetId?: string | null; currentStage?: "workflow" | "mode"; }; }>(), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type CanvasProject = InferSelectModel; // 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 | null>(), uiSurfaceLatestReview: jsonb("uiSurfaceLatestReview").$type | null>(), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type CanvasDetailArchive = InferSelectModel; // 画布图片生成任务表(异步队列) 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; export const canvasCapabilityTask = pgTable("CanvasCapabilityTask", { id: uuid("id").primaryKey().notNull().defaultRandom(), requestId: varchar("requestId", { length: 180 }).notNull().unique(), userId: uuid("userId") .notNull() .references(() => user.id, { onDelete: "cascade" }), projectId: uuid("projectId") .notNull() .references(() => canvasProject.id, { onDelete: "cascade" }), capabilityId: varchar("capabilityId", { length: 96 }).notNull(), source: varchar("source", { length: 16 }).notNull(), status: varchar("status", { length: 16 }).notNull().default("pending"), version: integer("version").notNull().default(1), 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 CanvasCapabilityTask = InferSelectModel; 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; 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; // 画布节点表(可选,用于更细粒度的查询) 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().default([]), parentId: uuid("parentId"), // 父节点ID,用于对话上下文 createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type CanvasNode = InferSelectModel; // 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 | 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; // 九宫格视频分镜任务主表 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; // 九宫格视频分镜子任务表 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; // 会话工作记忆表(用于连续对话中的图片/资产目标定位) 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().notNull().default([]), currentFocusImageUrls: jsonb("currentFocusImageUrls").$type().notNull().default([]), selectedAssetIds: jsonb("selectedAssetIds").$type().notNull().default([]), selectedImageUrls: jsonb("selectedImageUrls").$type().notNull().default([]), lastUploadedAssetIds: jsonb("lastUploadedAssetIds").$type().notNull().default([]), lastUploadedImageUrls: jsonb("lastUploadedImageUrls").$type().notNull().default([]), lastGeneratedAssetIds: jsonb("lastGeneratedAssetIds").$type().notNull().default([]), lastGeneratedImageUrls: jsonb("lastGeneratedImageUrls").$type().notNull().default([]), lastReferencedAssetIds: jsonb("lastReferencedAssetIds").$type().notNull().default([]), lastReferencedImageUrls: jsonb("lastReferencedImageUrls").$type().notNull().default([]), lastResolvedTargetAssetIds: jsonb("lastResolvedTargetAssetIds") .$type() .notNull() .default([]), lastResolvedTargetImageUrls: jsonb("lastResolvedTargetImageUrls") .$type() .notNull() .default([]), activeCanvasNodeIds: jsonb("activeCanvasNodeIds").$type().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; // 仅为数据库迁移兼容保留;旧版 AI-CAD 页面和接口已移除。 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[]>().notNull().default([]), versions: jsonb("versions").$type[]>().notNull().default([]), activeVersionId: varchar("activeVersionId", { length: 64 }), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type AiCadWorkspace = InferSelectModel; export type FlowBoardSnapshotData = { children: Record[]; viewport?: Record | null; theme?: Record | 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() .notNull() .default({ children: [] }), createdAt: timestamp("createdAt").notNull().defaultNow(), updatedAt: timestamp("updatedAt").notNull().defaultNow(), }); export type FlowBoardDraft = InferSelectModel; // 每轮消息的目标资产/图片定位审计表 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().notNull().default([]), uploadedImageUrls: jsonb("uploadedImageUrls").$type().notNull().default([]), selectedAssetIds: jsonb("selectedAssetIds").$type().notNull().default([]), selectedImageUrls: jsonb("selectedImageUrls").$type().notNull().default([]), candidateAssetIds: jsonb("candidateAssetIds").$type().notNull().default([]), candidateImageUrls: jsonb("candidateImageUrls").$type().notNull().default([]), resolvedAssetIds: jsonb("resolvedAssetIds").$type().notNull().default([]), resolvedImageUrls: jsonb("resolvedImageUrls").$type().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; // 统一媒体资产表(上传图 / 生成图 / 视频 / 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; // 媒体资产关系表(版本链 / 画布节点绑定 / 来源关系) 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; // 资产记忆快照表(视觉摘要 / 主体标签 / 场景标签等可复用缓存) 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().notNull().default([]), sceneTags: jsonb("sceneTags").$type().notNull().default([]), styleTags: jsonb("styleTags").$type().notNull().default([]), lightingTags: jsonb("lightingTags").$type().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; // 风格化参考图绑定表 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; // 风格化执行快照表 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>(), 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 | null; requireUpload?: boolean; uploadLabel?: string | null; referenceAssetIds: string[]; referenceImageUrls: string[]; }> >() .notNull() .default([]), referenceAssetIds: jsonb("referenceAssetIds").$type().notNull().default([]), referenceImageUrls: jsonb("referenceImageUrls").$type().notNull().default([]), userInputText: text("userInputText"), requestPayload: jsonb("requestPayload").$type>(), createdAt: timestamp("createdAt").notNull().defaultNow(), }); export type AgentPresetExecutionSnapshot = InferSelectModel< typeof agentPresetExecutionSnapshot >;