From 77ef569d34d47e945c06cb64baf3418019f7087f Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Thu, 18 Dec 2025 11:27:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(db):=20=E6=B7=BB=E5=8A=A0=20Drizzle=20ORM?= =?UTF-8?q?=20=E6=95=B0=E6=8D=AE=E5=BA=93=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 drizzle.config.ts 配置文件 - 添加数据库 schema 定义(会话、消息、设置等表) - 添加数据库连接配置 - 添加数据库迁移文件 - 添加种子数据脚本 --- drizzle.config.ts | 15 + src/drizzle/db.ts | 35 + src/drizzle/migrations/0000_white_warlock.sql | 87 +++ src/drizzle/migrations/0001_daffy_paladin.sql | 4 + .../migrations/meta/0000_snapshot.json | 571 +++++++++++++++++ .../migrations/meta/0001_snapshot.json | 596 ++++++++++++++++++ src/drizzle/migrations/meta/_journal.json | 20 + src/drizzle/schema.ts | 190 ++++++ src/drizzle/seed.ts | 182 ++++++ 9 files changed, 1700 insertions(+) create mode 100644 drizzle.config.ts create mode 100644 src/drizzle/db.ts create mode 100644 src/drizzle/migrations/0000_white_warlock.sql create mode 100644 src/drizzle/migrations/0001_daffy_paladin.sql create mode 100644 src/drizzle/migrations/meta/0000_snapshot.json create mode 100644 src/drizzle/migrations/meta/0001_snapshot.json create mode 100644 src/drizzle/migrations/meta/_journal.json create mode 100644 src/drizzle/schema.ts create mode 100644 src/drizzle/seed.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..98bcc84 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/drizzle/schema.ts', + out: './src/drizzle/migrations', + dialect: 'postgresql', + dbCredentials: { + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '35433'), + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: process.env.DB_NAME || 'cchcode_ui', + ssl: false, + }, +}); diff --git a/src/drizzle/db.ts b/src/drizzle/db.ts new file mode 100644 index 0000000..bb3d7f8 --- /dev/null +++ b/src/drizzle/db.ts @@ -0,0 +1,35 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import * as schema from './schema'; + +// 创建连接池 +const pool = new Pool({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT || '35433'), + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'postgres', + database: process.env.DB_NAME || 'cchcode_ui', + max: 10, // 最大连接数 + idleTimeoutMillis: 30000, // 空闲超时 + connectionTimeoutMillis: 5000, // 连接超时 + ssl: false, // 禁用 SSL +}); + +// 创建 Drizzle 实例 +export const db = drizzle(pool, { schema }); + +// 导出 pool 以便需要时直接使用 +export { pool }; + +// 健康检查函数 +export async function checkDatabaseConnection(): Promise { + try { + const client = await pool.connect(); + await client.query('SELECT 1'); + client.release(); + return true; + } catch (error) { + console.error('Database connection check failed:', error); + return false; + } +} diff --git a/src/drizzle/migrations/0000_white_warlock.sql b/src/drizzle/migrations/0000_white_warlock.sql new file mode 100644 index 0000000..dab10c7 --- /dev/null +++ b/src/drizzle/migrations/0000_white_warlock.sql @@ -0,0 +1,87 @@ +CREATE TABLE "conversations" ( + "id" serial PRIMARY KEY NOT NULL, + "conversation_id" varchar(64) NOT NULL, + "title" varchar(255) DEFAULT '新对话' NOT NULL, + "summary" text, + "model" varchar(64) NOT NULL, + "tools" jsonb DEFAULT '[]'::jsonb, + "enable_thinking" boolean DEFAULT false, + "message_count" integer DEFAULT 0, + "total_tokens" integer DEFAULT 0, + "is_archived" boolean DEFAULT false, + "is_pinned" boolean DEFAULT false, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + "last_message_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "conversations_conversation_id_unique" UNIQUE("conversation_id") +); +--> statement-breakpoint +CREATE TABLE "messages" ( + "id" serial PRIMARY KEY NOT NULL, + "message_id" varchar(64) NOT NULL, + "conversation_id" varchar(64) NOT NULL, + "role" varchar(20) NOT NULL, + "content" text NOT NULL, + "thinking_content" text, + "thinking_collapsed" boolean DEFAULT true, + "tool_calls" jsonb, + "tool_results" jsonb, + "input_tokens" integer DEFAULT 0, + "output_tokens" integer DEFAULT 0, + "status" varchar(20) DEFAULT 'completed', + "error_message" text, + "feedback" varchar(10), + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "messages_message_id_unique" UNIQUE("message_id") +); +--> statement-breakpoint +CREATE TABLE "models" ( + "id" serial PRIMARY KEY NOT NULL, + "model_id" varchar(128) NOT NULL, + "name" varchar(64) NOT NULL, + "display_name" varchar(128) NOT NULL, + "description" text, + "supports_tools" boolean DEFAULT true, + "supports_thinking" boolean DEFAULT true, + "supports_vision" boolean DEFAULT false, + "max_tokens" integer DEFAULT 8192, + "context_window" integer DEFAULT 200000, + "is_enabled" boolean DEFAULT true, + "is_default" boolean DEFAULT false, + "sort_order" integer DEFAULT 0, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "models_model_id_unique" UNIQUE("model_id") +); +--> statement-breakpoint +CREATE TABLE "tools" ( + "id" serial PRIMARY KEY NOT NULL, + "tool_id" varchar(64) NOT NULL, + "name" varchar(64) NOT NULL, + "display_name" varchar(128) NOT NULL, + "description" text, + "icon" varchar(64), + "input_schema" jsonb NOT NULL, + "is_enabled" boolean DEFAULT true, + "is_default" boolean DEFAULT false, + "sort_order" integer DEFAULT 0, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "tools_tool_id_unique" UNIQUE("tool_id") +); +--> statement-breakpoint +CREATE TABLE "user_settings" ( + "id" serial PRIMARY KEY NOT NULL, + "cch_url" varchar(512) DEFAULT 'http://localhost:13500' NOT NULL, + "cch_api_key" varchar(512), + "cch_api_key_configured" boolean DEFAULT false, + "default_model" varchar(64) DEFAULT 'claude-sonnet-4-20250514', + "default_tools" jsonb DEFAULT '["web_search","code_execution","web_fetch"]'::jsonb, + "theme" varchar(20) DEFAULT 'light', + "language" varchar(10) DEFAULT 'zh-CN', + "enable_thinking" boolean DEFAULT false, + "save_chat_history" boolean DEFAULT true, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now() +); diff --git a/src/drizzle/migrations/0001_daffy_paladin.sql b/src/drizzle/migrations/0001_daffy_paladin.sql new file mode 100644 index 0000000..c8741e7 --- /dev/null +++ b/src/drizzle/migrations/0001_daffy_paladin.sql @@ -0,0 +1,4 @@ +ALTER TABLE "conversations" ADD COLUMN "system_prompt" text;--> statement-breakpoint +ALTER TABLE "conversations" ADD COLUMN "temperature" varchar(10);--> statement-breakpoint +ALTER TABLE "user_settings" ADD COLUMN "system_prompt" text;--> statement-breakpoint +ALTER TABLE "user_settings" ADD COLUMN "temperature" varchar(10) DEFAULT '0.7'; \ No newline at end of file diff --git a/src/drizzle/migrations/meta/0000_snapshot.json b/src/drizzle/migrations/meta/0000_snapshot.json new file mode 100644 index 0000000..5d4bb62 --- /dev/null +++ b/src/drizzle/migrations/meta/0000_snapshot.json @@ -0,0 +1,571 @@ +{ + "id": "03e6937f-d493-44f6-8109-704693f38906", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'新对话'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "enable_thinking": { + "name": "enable_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "conversations_conversation_id_unique": { + "name": "conversations_conversation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "conversation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thinking_content": { + "name": "thinking_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thinking_collapsed": { + "name": "thinking_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "tool_calls": { + "name": "tool_calls", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tool_results": { + "name": "tool_results", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback": { + "name": "feedback", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "messages_message_id_unique": { + "name": "messages_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models": { + "name": "models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supports_tools": { + "name": "supports_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "supports_thinking": { + "name": "supports_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "supports_vision": { + "name": "supports_vision", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 8192 + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 200000 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "models_model_id_unique": { + "name": "models_model_id_unique", + "nullsNotDistinct": false, + "columns": [ + "model_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tools": { + "name": "tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "tool_id": { + "name": "tool_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tools_tool_id_unique": { + "name": "tools_tool_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tool_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cch_url": { + "name": "cch_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "'http://localhost:13500'" + }, + "cch_api_key": { + "name": "cch_api_key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cch_api_key_configured": { + "name": "cch_api_key_configured", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "default": "'claude-sonnet-4-20250514'" + }, + "default_tools": { + "name": "default_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[\"web_search\",\"code_execution\",\"web_fetch\"]'::jsonb" + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'light'" + }, + "language": { + "name": "language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'zh-CN'" + }, + "enable_thinking": { + "name": "enable_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "save_chat_history": { + "name": "save_chat_history", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/drizzle/migrations/meta/0001_snapshot.json b/src/drizzle/migrations/meta/0001_snapshot.json new file mode 100644 index 0000000..1537307 --- /dev/null +++ b/src/drizzle/migrations/meta/0001_snapshot.json @@ -0,0 +1,596 @@ +{ + "id": "dc7ae80d-12dc-4e13-9c7b-101b9f79b198", + "prevId": "03e6937f-d493-44f6-8109-704693f38906", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.conversations": { + "name": "conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "default": "'新对话'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "tools": { + "name": "tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "enable_thinking": { + "name": "enable_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "temperature": { + "name": "temperature", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "total_tokens": { + "name": "total_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "is_archived": { + "name": "is_archived", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "is_pinned": { + "name": "is_pinned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "conversations_conversation_id_unique": { + "name": "conversations_conversation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "conversation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.messages": { + "name": "messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thinking_content": { + "name": "thinking_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thinking_collapsed": { + "name": "thinking_collapsed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "tool_calls": { + "name": "tool_calls", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tool_results": { + "name": "tool_results", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'completed'" + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback": { + "name": "feedback", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "messages_message_id_unique": { + "name": "messages_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.models": { + "name": "models", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "model_id": { + "name": "model_id", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "supports_tools": { + "name": "supports_tools", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "supports_thinking": { + "name": "supports_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "supports_vision": { + "name": "supports_vision", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 8192 + }, + "context_window": { + "name": "context_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 200000 + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "models_model_id_unique": { + "name": "models_model_id_unique", + "nullsNotDistinct": false, + "columns": [ + "model_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tools": { + "name": "tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "tool_id": { + "name": "tool_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "input_schema": { + "name": "input_schema", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tools_tool_id_unique": { + "name": "tools_tool_id_unique", + "nullsNotDistinct": false, + "columns": [ + "tool_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_settings": { + "name": "user_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "cch_url": { + "name": "cch_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "default": "'http://localhost:13500'" + }, + "cch_api_key": { + "name": "cch_api_key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "cch_api_key_configured": { + "name": "cch_api_key_configured", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "default_model": { + "name": "default_model", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "default": "'claude-sonnet-4-20250514'" + }, + "default_tools": { + "name": "default_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[\"web_search\",\"code_execution\",\"web_fetch\"]'::jsonb" + }, + "system_prompt": { + "name": "system_prompt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "temperature": { + "name": "temperature", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'0.7'" + }, + "theme": { + "name": "theme", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "default": "'light'" + }, + "language": { + "name": "language", + "type": "varchar(10)", + "primaryKey": false, + "notNull": false, + "default": "'zh-CN'" + }, + "enable_thinking": { + "name": "enable_thinking", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "save_chat_history": { + "name": "save_chat_history", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/src/drizzle/migrations/meta/_journal.json b/src/drizzle/migrations/meta/_journal.json new file mode 100644 index 0000000..268c870 --- /dev/null +++ b/src/drizzle/migrations/meta/_journal.json @@ -0,0 +1,20 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1765985557199, + "tag": "0000_white_warlock", + "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1765991206886, + "tag": "0001_daffy_paladin", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/drizzle/schema.ts b/src/drizzle/schema.ts new file mode 100644 index 0000000..43d0e2f --- /dev/null +++ b/src/drizzle/schema.ts @@ -0,0 +1,190 @@ +import { + pgTable, + serial, + varchar, + text, + boolean, + integer, + timestamp, + jsonb, +} from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +// ============================================ +// 用户设置表 +// ============================================ +export const userSettings = pgTable('user_settings', { + id: serial('id').primaryKey(), + // CCH 配置 + cchUrl: varchar('cch_url', { length: 512 }).notNull().default('http://localhost:13500'), + cchApiKey: varchar('cch_api_key', { length: 512 }), + cchApiKeyConfigured: boolean('cch_api_key_configured').default(false), + // 默认设置 + defaultModel: varchar('default_model', { length: 64 }).default('claude-sonnet-4-20250514'), + defaultTools: jsonb('default_tools').$type().default(['web_search', 'code_execution', 'web_fetch']), + // AI 行为设置 + systemPrompt: text('system_prompt'), // 系统提示词 + temperature: varchar('temperature', { length: 10 }).default('0.7'), // 温度参数 (0-1) + // 偏好设置 + theme: varchar('theme', { length: 20 }).default('light'), + language: varchar('language', { length: 10 }).default('zh-CN'), + enableThinking: boolean('enable_thinking').default(false), + saveChatHistory: boolean('save_chat_history').default(true), + // 时间戳 + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}); + +// ============================================ +// 对话表 +// ============================================ +export const conversations = pgTable('conversations', { + id: serial('id').primaryKey(), + // 对话唯一标识 + conversationId: varchar('conversation_id', { length: 64 }).notNull().unique(), + // 对话信息 + title: varchar('title', { length: 255 }).notNull().default('新对话'), + summary: text('summary'), + // 模型和工具配置 + model: varchar('model', { length: 64 }).notNull(), + tools: jsonb('tools').$type().default([]), + enableThinking: boolean('enable_thinking').default(false), + // AI 行为设置(对话级别,可覆盖全局设置) + systemPrompt: text('system_prompt'), // 对话专属系统提示词 + temperature: varchar('temperature', { length: 10 }), // 对话专属温度参数 + // 统计信息 + messageCount: integer('message_count').default(0), + totalTokens: integer('total_tokens').default(0), + // 状态 + isArchived: boolean('is_archived').default(false), + isPinned: boolean('is_pinned').default(false), + // 时间戳 + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), + lastMessageAt: timestamp('last_message_at', { withTimezone: true }).defaultNow(), +}); + +// ============================================ +// 消息表 +// ============================================ +export const messages = pgTable('messages', { + id: serial('id').primaryKey(), + // 消息唯一标识 + messageId: varchar('message_id', { length: 64 }).notNull().unique(), + // 关联对话 + conversationId: varchar('conversation_id', { length: 64 }).notNull(), + // 消息内容 + role: varchar('role', { length: 20 }).notNull(), // user, assistant, system + content: text('content').notNull(), + // 思考内容 + thinkingContent: text('thinking_content'), + thinkingCollapsed: boolean('thinking_collapsed').default(true), + // 工具调用记录 + toolCalls: jsonb('tool_calls').$type(), + toolResults: jsonb('tool_results').$type(), + // Token 统计 + inputTokens: integer('input_tokens').default(0), + outputTokens: integer('output_tokens').default(0), + // 状态 + status: varchar('status', { length: 20 }).default('completed'), // pending, streaming, completed, error + errorMessage: text('error_message'), + // 用户反馈 + feedback: varchar('feedback', { length: 10 }), // thumbs_up, thumbs_down + // 时间戳 + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}); + +// ============================================ +// 工具表 +// ============================================ +export const tools = pgTable('tools', { + id: serial('id').primaryKey(), + // 工具唯一标识 + toolId: varchar('tool_id', { length: 64 }).notNull().unique(), + // 工具信息 + name: varchar('name', { length: 64 }).notNull(), + displayName: varchar('display_name', { length: 128 }).notNull(), + description: text('description'), + icon: varchar('icon', { length: 64 }), + // Claude Tool Schema + inputSchema: jsonb('input_schema').notNull(), + // 配置 + isEnabled: boolean('is_enabled').default(true), + isDefault: boolean('is_default').default(false), + sortOrder: integer('sort_order').default(0), + // 时间戳 + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}); + +// ============================================ +// 模型表 +// ============================================ +export const models = pgTable('models', { + id: serial('id').primaryKey(), + // 模型唯一标识 + modelId: varchar('model_id', { length: 128 }).notNull().unique(), + // 模型信息 + name: varchar('name', { length: 64 }).notNull(), + displayName: varchar('display_name', { length: 128 }).notNull(), + description: text('description'), + // 模型特性 + supportsTools: boolean('supports_tools').default(true), + supportsThinking: boolean('supports_thinking').default(true), + supportsVision: boolean('supports_vision').default(false), + maxTokens: integer('max_tokens').default(8192), + contextWindow: integer('context_window').default(200000), + // 配置 + isEnabled: boolean('is_enabled').default(true), + isDefault: boolean('is_default').default(false), + sortOrder: integer('sort_order').default(0), + // 时间戳 + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}); + +// ============================================ +// 关系定义 +// ============================================ +export const conversationsRelations = relations(conversations, ({ many }) => ({ + messages: many(messages), +})); + +export const messagesRelations = relations(messages, ({ one }) => ({ + conversation: one(conversations, { + fields: [messages.conversationId], + references: [conversations.conversationId], + }), +})); + +// ============================================ +// 类型定义 +// ============================================ +export interface ToolCall { + id: string; + name: string; + input: Record; +} + +export interface ToolResult { + toolUseId: string; + content: string; + isError?: boolean; +} + +// 导出类型 +export type UserSettings = typeof userSettings.$inferSelect; +export type NewUserSettings = typeof userSettings.$inferInsert; + +export type Conversation = typeof conversations.$inferSelect; +export type NewConversation = typeof conversations.$inferInsert; + +export type Message = typeof messages.$inferSelect; +export type NewMessage = typeof messages.$inferInsert; + +export type Tool = typeof tools.$inferSelect; +export type NewTool = typeof tools.$inferInsert; + +export type Model = typeof models.$inferSelect; +export type NewModel = typeof models.$inferInsert; diff --git a/src/drizzle/seed.ts b/src/drizzle/seed.ts new file mode 100644 index 0000000..8f627bb --- /dev/null +++ b/src/drizzle/seed.ts @@ -0,0 +1,182 @@ +import { db } from './db'; +import { userSettings, tools, models } from './schema'; +import { eq } from 'drizzle-orm'; + +// 初始化用户设置 +async function seedUserSettings() { + const existing = await db.query.userSettings.findFirst({ + where: eq(userSettings.id, 1), + }); + + if (!existing) { + await db.insert(userSettings).values({ + id: 1, + cchUrl: 'http://localhost:13500', + cchApiKeyConfigured: false, + defaultModel: 'claude-sonnet-4-5-20250929', + defaultTools: ['web_search', 'code_execution', 'web_fetch'], + theme: 'light', + language: 'zh-CN', + enableThinking: false, + saveChatHistory: true, + }); + console.log('✓ User settings initialized'); + } else { + console.log('→ User settings already exist'); + } +} + +// 初始化工具 +async function seedTools() { + const toolsData = [ + { + toolId: 'web_search', + name: 'web_search', + displayName: 'Web Search', + description: '搜索互联网获取最新信息', + icon: 'Search', + inputSchema: { + type: 'object', + properties: { + query: { type: 'string', description: '搜索查询' }, + }, + required: ['query'], + }, + isEnabled: true, + isDefault: true, + sortOrder: 1, + }, + { + toolId: 'code_execution', + name: 'code_execution', + displayName: 'Code Execution', + description: '执行代码片段并返回结果', + icon: 'Terminal', + inputSchema: { + type: 'object', + properties: { + code: { type: 'string', description: '要执行的代码' }, + language: { type: 'string', description: '编程语言' }, + }, + required: ['code', 'language'], + }, + isEnabled: true, + isDefault: true, + sortOrder: 2, + }, + { + toolId: 'web_fetch', + name: 'web_fetch', + displayName: 'Web Fetch', + description: '获取网页内容', + icon: 'Globe', + inputSchema: { + type: 'object', + properties: { + url: { type: 'string', description: '要获取的URL' }, + }, + required: ['url'], + }, + isEnabled: true, + isDefault: true, + sortOrder: 3, + }, + ]; + + for (const tool of toolsData) { + const existing = await db.query.tools.findFirst({ + where: eq(tools.toolId, tool.toolId), + }); + + if (!existing) { + await db.insert(tools).values(tool); + console.log(`✓ Tool "${tool.displayName}" initialized`); + } else { + console.log(`→ Tool "${tool.displayName}" already exists`); + } + } +} + +// 初始化模型 +async function seedModels() { + const modelsData = [ + { + modelId: 'claude-sonnet-4-5-20250929', + name: 'claude-sonnet-4-5-20250929', + displayName: 'Default (recommended)', + description: '使用默认模型(Sonnet 4.5),$3/$15 per Mtok', + supportsTools: true, + supportsThinking: true, + supportsVision: true, + maxTokens: 8192, + contextWindow: 200000, + isEnabled: true, + isDefault: true, + sortOrder: 1, + }, + { + modelId: 'claude-opus-4-5-20251101', + name: 'claude-opus-4-5-20251101', + displayName: 'Opus', + description: 'Opus 4.5 - 最强大的复杂工作,$5/$25 per Mtok', + supportsTools: true, + supportsThinking: true, + supportsVision: true, + maxTokens: 8192, + contextWindow: 200000, + isEnabled: true, + isDefault: false, + sortOrder: 2, + }, + { + modelId: 'claude-haiku-4-5-20251001', + name: 'claude-haiku-4-5-20251001', + displayName: 'Haiku', + description: 'Haiku 4.5 - 快速回答,$1/$5 per Mtok', + supportsTools: true, + supportsThinking: false, + supportsVision: false, + maxTokens: 8192, + contextWindow: 200000, + isEnabled: true, + isDefault: false, + sortOrder: 3, + }, + ]; + + for (const model of modelsData) { + const existing = await db.query.models.findFirst({ + where: eq(models.modelId, model.modelId), + }); + + if (!existing) { + await db.insert(models).values(model); + console.log(`✓ Model "${model.displayName}" initialized`); + } else { + console.log(`→ Model "${model.displayName}" already exists`); + } + } +} + +// 主函数 +export async function seed() { + console.log('\n🌱 Starting database seed...\n'); + + try { + await seedUserSettings(); + await seedTools(); + await seedModels(); + + console.log('\n✅ Database seed completed!\n'); + } catch (error) { + console.error('\n❌ Database seed failed:', error); + throw error; + } +} + +// 如果直接运行此文件 +if (require.main === module) { + seed() + .then(() => process.exit(0)) + .catch(() => process.exit(1)); +}