diff --git a/AGENTS.md b/AGENTS.md index 7ab9cb5..8bf23bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - 新建的mono包必须在根目录安装 - 使用nuxt时遇到不清楚的,必须先加载nuxt-remote查询文档再进行分析 - 能够用toast提示的尽量用toast +- 数据库schema编写完成之后,必须使用命令进行迁移,不能自己生成 ## 代码抽离与复用 diff --git a/app/pages/admin.vue b/app/pages/admin.vue index ba41561..2f54a53 100644 --- a/app/pages/admin.vue +++ b/app/pages/admin.vue @@ -25,6 +25,7 @@ const adminNav: NavItem[] = [ children: [ { label: '全局配置', to: '/admin/global-config' }, { label: '用户管理', to: '/admin/users' }, + { label: '模型配置', to: '/admin/llm-config' }, ] }, ] diff --git a/app/pages/admin/llm-config/index.vue b/app/pages/admin/llm-config/index.vue new file mode 100644 index 0000000..81830ab --- /dev/null +++ b/app/pages/admin/llm-config/index.vue @@ -0,0 +1,1446 @@ + + + + + diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index d7a1223..bd7412c 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ diff --git a/packages/drizzle-pkg/lib/schema/llm.ts b/packages/drizzle-pkg/lib/schema/llm.ts new file mode 100644 index 0000000..fcc33d9 --- /dev/null +++ b/packages/drizzle-pkg/lib/schema/llm.ts @@ -0,0 +1,58 @@ +import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; + +export const LlmProviderStatuses = ["active", "disabled"] as const; +export type LlmProviderStatus = (typeof LlmProviderStatuses)[number]; + +export const LlmParseModes = ["openai", "anthropic"] as const; +export type LlmParseMode = (typeof LlmParseModes)[number]; + +export const LlmModelTypes = ["text", "vision", "multimodal"] as const; +export type LlmModelType = (typeof LlmModelTypes)[number]; + +export const llmProviders = sqliteTable( + "llm_providers", + { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name", { length: 100 }).notNull().unique(), + slug: text("slug", { length: 100 }).notNull().unique(), + baseUrl: text("base_url", { length: 500 }), + parseMode: text("parse_mode", { enum: LlmParseModes }).notNull().default("openai"), + apiKey: text("api_key"), + status: text("status", { enum: LlmProviderStatuses }).notNull().default("active"), + description: text("description"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [ + index("idx_llm_provider_slug").on(table.slug), + index("idx_llm_provider_status").on(table.status), + ], +); + +export const llmModels = sqliteTable( + "llm_models", + { + id: integer("id").primaryKey({ autoIncrement: true }), + providerId: integer("provider_id") + .notNull() + .references(() => llmProviders.id, { onDelete: "cascade" }), + name: text("name", { length: 200 }).notNull(), + modelId: text("model_id", { length: 200 }).notNull(), + type: text("type", { enum: LlmModelTypes }).notNull().default("text"), + enabled: integer("enabled").default(1).notNull(), + description: text("description"), + maxTokens: integer("max_tokens"), + createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(), + updatedAt: integer("updated_at", { mode: "timestamp_ms" }) + .defaultNow() + .$onUpdate(() => new Date()) + .notNull(), + }, + (table) => [ + index("idx_llm_model_provider").on(table.providerId), + index("idx_llm_model_type").on(table.type), + ], +); diff --git a/packages/drizzle-pkg/migrations/0012_funny_santa_claus.sql b/packages/drizzle-pkg/migrations/0012_funny_santa_claus.sql new file mode 100644 index 0000000..cd7d1d3 --- /dev/null +++ b/packages/drizzle-pkg/migrations/0012_funny_santa_claus.sql @@ -0,0 +1,33 @@ +CREATE TABLE `llm_models` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `provider_id` integer NOT NULL, + `name` text(200) NOT NULL, + `model_id` text(200) NOT NULL, + `type` text DEFAULT 'text' NOT NULL, + `enabled` integer DEFAULT 1 NOT NULL, + `description` text, + `max_tokens` integer, + `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + FOREIGN KEY (`provider_id`) REFERENCES `llm_providers`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `idx_llm_model_provider` ON `llm_models` (`provider_id`);--> statement-breakpoint +CREATE INDEX `idx_llm_model_type` ON `llm_models` (`type`);--> statement-breakpoint +CREATE TABLE `llm_providers` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `name` text(100) NOT NULL, + `slug` text(100) NOT NULL, + `base_url` text(500), + `parse_mode` text DEFAULT 'openai' NOT NULL, + `api_key` text, + `status` text DEFAULT 'active' NOT NULL, + `description` text, + `created_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL, + `updated_at` integer DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer)) NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `llm_providers_name_unique` ON `llm_providers` (`name`);--> statement-breakpoint +CREATE UNIQUE INDEX `llm_providers_slug_unique` ON `llm_providers` (`slug`);--> statement-breakpoint +CREATE INDEX `idx_llm_provider_slug` ON `llm_providers` (`slug`);--> statement-breakpoint +CREATE INDEX `idx_llm_provider_status` ON `llm_providers` (`status`); \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/0012_snapshot.json b/packages/drizzle-pkg/migrations/meta/0012_snapshot.json new file mode 100644 index 0000000..50dd863 --- /dev/null +++ b/packages/drizzle-pkg/migrations/meta/0012_snapshot.json @@ -0,0 +1,1659 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "20c54319-abd6-41f9-956b-39d5b6d9e222", + "prevId": "076d52b7-d536-402e-9602-7cbbf8f8c616", + "tables": { + "oauth_accounts": { + "name": "oauth_accounts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_user_id": { + "name": "provider_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "nickname": { + "name": "nickname", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "users_username_unique": { + "name": "users_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "app_configs": { + "name": "app_configs", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user_configs": { + "name": "user_configs", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "user_configs_user_id_idx": { + "name": "user_configs_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "user_configs_user_id_users_id_fk": { + "name": "user_configs_user_id_users_id_fk", + "tableFrom": "user_configs", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_configs_user_id_key_pk": { + "columns": [ + "user_id", + "key" + ], + "name": "user_configs_user_id_key_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "article_cards": { + "name": "article_cards", + "columns": { + "article_id": { + "name": "article_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_article_card_article": { + "name": "idx_article_card_article", + "columns": [ + "article_id" + ], + "isUnique": false + }, + "idx_article_card_card": { + "name": "idx_article_card_card", + "columns": [ + "card_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "article_cards_article_id_articles_id_fk": { + "name": "article_cards_article_id_articles_id_fk", + "tableFrom": "article_cards", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_cards_card_id_cards_id_fk": { + "name": "article_cards_card_id_cards_id_fk", + "tableFrom": "article_cards", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "article_card_pk": { + "columns": [ + "article_id", + "card_id" + ], + "name": "article_card_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "articles": { + "name": "articles", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "title": { + "name": "title", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cover": { + "name": "cover", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_article_status": { + "name": "idx_article_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_article_created": { + "name": "idx_article_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "card_images": { + "name": "card_images", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": { + "idx_card_image_card": { + "name": "idx_card_image_card", + "columns": [ + "card_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "card_images_card_id_cards_id_fk": { + "name": "card_images_card_id_cards_id_fk", + "tableFrom": "card_images", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "card_tags": { + "name": "card_tags", + "columns": { + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_card_tag_card": { + "name": "idx_card_tag_card", + "columns": [ + "card_id" + ], + "isUnique": false + }, + "idx_card_tag_tag": { + "name": "idx_card_tag_tag", + "columns": [ + "tag_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "card_tags_card_id_cards_id_fk": { + "name": "card_tags_card_id_cards_id_fk", + "tableFrom": "card_tags", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "card_tags_tag_id_tags_id_fk": { + "name": "card_tags_tag_id_tags_id_fk", + "tableFrom": "card_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "card_tag_pk": { + "columns": [ + "card_id", + "tag_id" + ], + "name": "card_tag_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cards": { + "name": "cards", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aspect_ratio": { + "name": "aspect_ratio", + "type": "real", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_card_category": { + "name": "idx_card_category", + "columns": [ + "category_id" + ], + "isUnique": false + }, + "idx_card_type": { + "name": "idx_card_type", + "columns": [ + "type" + ], + "isUnique": false + }, + "idx_card_created": { + "name": "idx_card_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "cards_category_id_categories_id_fk": { + "name": "cards_category_id_categories_id_fk", + "tableFrom": "cards", + "tableTo": "categories", + "columnsFrom": [ + "category_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "categories": { + "name": "categories", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_category_slug": { + "name": "idx_category_slug", + "columns": [ + "slug" + ], + "isUnique": true + }, + "idx_category_parent": { + "name": "idx_category_parent", + "columns": [ + "parent_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chat_messages": { + "name": "chat_messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "nickname": { + "name": "nickname", + "type": "text(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "idx_chat_msg_created": { + "name": "idx_chat_msg_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "favorites": { + "name": "favorites", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "card_id": { + "name": "card_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_favorite_user": { + "name": "idx_favorite_user", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_favorite_card": { + "name": "idx_favorite_card", + "columns": [ + "card_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "favorites_user_id_users_id_fk": { + "name": "favorites_user_id_users_id_fk", + "tableFrom": "favorites", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "favorites_card_id_cards_id_fk": { + "name": "favorites_card_id_cards_id_fk", + "tableFrom": "favorites", + "tableTo": "cards", + "columnsFrom": [ + "card_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "favorite_pk": { + "columns": [ + "user_id", + "card_id" + ], + "name": "favorite_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ideas": { + "name": "ideas", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "author": { + "name": "author", + "type": "text(30)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "platform": { + "name": "platform", + "type": "text(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_idea_created": { + "name": "idx_idea_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text(1000)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_project_created": { + "name": "idx_project_created", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_tag_slug": { + "name": "idx_tag_slug", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tools": { + "name": "tools", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_models": { + "name": "llm_models", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "provider_id": { + "name": "provider_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text(200)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "text(200)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'text'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_tokens": { + "name": "max_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "idx_llm_model_provider": { + "name": "idx_llm_model_provider", + "columns": [ + "provider_id" + ], + "isUnique": false + }, + "idx_llm_model_type": { + "name": "idx_llm_model_type", + "columns": [ + "type" + ], + "isUnique": false + } + }, + "foreignKeys": { + "llm_models_provider_id_llm_providers_id_fk": { + "name": "llm_models_provider_id_llm_providers_id_fk", + "tableFrom": "llm_models", + "tableTo": "llm_providers", + "columnsFrom": [ + "provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_providers": { + "name": "llm_providers", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_url": { + "name": "base_url", + "type": "text(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "parse_mode": { + "name": "parse_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'openai'" + }, + "api_key": { + "name": "api_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": { + "llm_providers_name_unique": { + "name": "llm_providers_name_unique", + "columns": [ + "name" + ], + "isUnique": true + }, + "llm_providers_slug_unique": { + "name": "llm_providers_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "idx_llm_provider_slug": { + "name": "idx_llm_provider_slug", + "columns": [ + "slug" + ], + "isUnique": false + }, + "idx_llm_provider_status": { + "name": "idx_llm_provider_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scheduled_tasks": { + "name": "scheduled_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "function_name": { + "name": "function_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "function_payload": { + "name": "function_payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_method": { + "name": "http_method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_url": { + "name": "http_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_headers": { + "name": "http_headers", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "http_body": { + "name": "http_body", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "catch_up": { + "name": "catch_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "max_retries": { + "name": "max_retries", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "retry_delay_seconds": { + "name": "retry_delay_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "timeout_seconds": { + "name": "timeout_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 300 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "task_execution_logs": { + "name": "task_execution_logs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast((julianday('now') - 2440587.5)*86400000 as integer))" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_summary": { + "name": "result_summary", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "task_execution_logs_task_id_scheduled_tasks_id_fk": { + "name": "task_execution_logs_task_id_scheduled_tasks_id_fk", + "tableFrom": "task_execution_logs", + "tableTo": "scheduled_tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/drizzle-pkg/migrations/meta/_journal.json b/packages/drizzle-pkg/migrations/meta/_journal.json index 2b094c8..5778112 100644 --- a/packages/drizzle-pkg/migrations/meta/_journal.json +++ b/packages/drizzle-pkg/migrations/meta/_journal.json @@ -85,6 +85,13 @@ "when": 1782825462705, "tag": "0011_add_projects_table", "breakpoints": true + }, + { + "idx": 12, + "version": "6", + "when": 1782963564430, + "tag": "0012_funny_santa_claus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/api/admin/llm/models/[providerId].get.ts b/server/api/admin/llm/models/[providerId].get.ts new file mode 100644 index 0000000..b9cb88a --- /dev/null +++ b/server/api/admin/llm/models/[providerId].get.ts @@ -0,0 +1,14 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { listModelsByProvider } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const providerId = Number(getRouterParam(event, "providerId")); + if (!providerId) { + return R.throwError(400, "无效的供应商ID", null); + } + + const result = await listModelsByProvider(providerId); + return R.success(result); +}); diff --git a/server/api/admin/llm/models/[providerId].post.ts b/server/api/admin/llm/models/[providerId].post.ts new file mode 100644 index 0000000..b0e1633 --- /dev/null +++ b/server/api/admin/llm/models/[providerId].post.ts @@ -0,0 +1,29 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { createModel } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const providerId = Number(getRouterParam(event, "providerId")); + if (!providerId) { + return R.throwError(400, "无效的供应商ID", null); + } + + const body = await readBody(event); + const { name, modelId, type, enabled, description, maxTokens } = body; + + if (!name || !modelId) { + return R.throwError(422, "模型名称和模型ID不能为空", null); + } + + const result = await createModel({ + providerId, + name, + modelId, + type: type || "text", + enabled: enabled ?? 1, + description, + maxTokens, + }); + return R.success(result); +}); diff --git a/server/api/admin/llm/models/detail/[id].delete.ts b/server/api/admin/llm/models/detail/[id].delete.ts new file mode 100644 index 0000000..6789d9a --- /dev/null +++ b/server/api/admin/llm/models/detail/[id].delete.ts @@ -0,0 +1,14 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { deleteModel } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const id = Number(getRouterParam(event, "id")); + if (!id) { + return R.throwError(400, "无效的模型ID", null); + } + + await deleteModel(id); + return R.success(null); +}); diff --git a/server/api/admin/llm/models/detail/[id].put.ts b/server/api/admin/llm/models/detail/[id].put.ts new file mode 100644 index 0000000..097fe08 --- /dev/null +++ b/server/api/admin/llm/models/detail/[id].put.ts @@ -0,0 +1,24 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { updateModel } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const id = Number(getRouterParam(event, "id")); + if (!id) { + return R.throwError(400, "无效的模型ID", null); + } + + const body = await readBody(event); + const { name, modelId, type, enabled, description, maxTokens } = body; + + await updateModel(id, { + name, + modelId, + type, + enabled, + description, + maxTokens, + }); + return R.success(null); +}); diff --git a/server/api/admin/llm/providers/[id].delete.ts b/server/api/admin/llm/providers/[id].delete.ts new file mode 100644 index 0000000..f75adb3 --- /dev/null +++ b/server/api/admin/llm/providers/[id].delete.ts @@ -0,0 +1,14 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { deleteProvider } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const id = Number(getRouterParam(event, "id")); + if (!id) { + return R.throwError(400, "无效的供应商ID", null); + } + + await deleteProvider(id); + return R.success(null); +}); diff --git a/server/api/admin/llm/providers/[id].get.ts b/server/api/admin/llm/providers/[id].get.ts new file mode 100644 index 0000000..a8d03ba --- /dev/null +++ b/server/api/admin/llm/providers/[id].get.ts @@ -0,0 +1,17 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { getProviderWithModels } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const id = Number(getRouterParam(event, "id")); + if (!id) { + return R.throwError(400, "无效的供应商ID", null); + } + + const result = await getProviderWithModels(id); + if (!result) { + return R.throwError(404, "供应商不存在", null); + } + return R.success(result); +}); diff --git a/server/api/admin/llm/providers/[id].models.get.ts b/server/api/admin/llm/providers/[id].models.get.ts new file mode 100644 index 0000000..a6211f8 --- /dev/null +++ b/server/api/admin/llm/providers/[id].models.get.ts @@ -0,0 +1,50 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { getProviderById } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const id = Number(getRouterParam(event, "id")); + if (!id) { + return R.throwError(400, "无效的供应商 ID", null); + } + + const provider = await getProviderById(id); + if (!provider) { + return R.throwError(404, "供应商不存在", null); + } + + const baseUrl = provider.baseUrl?.replace(/\/+$/, "") || ""; + if (!baseUrl) { + return R.success([]); + } + + const headers: Record = { + "Content-Type": "application/json", + }; + + if (provider.parseMode === "anthropic") { + if (provider.apiKey) { + headers["x-api-key"] = provider.apiKey; + } + headers["anthropic-version"] = "2023-06-01"; + } else { + if (provider.apiKey) { + headers["Authorization"] = `Bearer ${provider.apiKey}`; + } + } + + try { + const res = await $fetch<{ data?: { id: string; name?: string }[] }>( + `${baseUrl}/models`, + { headers, timeout: 8000 }, + ); + const models = (res?.data ?? []).map((m) => ({ + id: m.id, + name: m.name || m.id, + })); + return R.success(models); + } catch { + return R.success([]); + } +}); diff --git a/server/api/admin/llm/providers/[id].put.ts b/server/api/admin/llm/providers/[id].put.ts new file mode 100644 index 0000000..ec94f99 --- /dev/null +++ b/server/api/admin/llm/providers/[id].put.ts @@ -0,0 +1,25 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { updateProvider } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const id = Number(getRouterParam(event, "id")); + if (!id) { + return R.throwError(400, "无效的供应商ID", null); + } + + const body = await readBody(event); + const { name, slug, baseUrl, parseMode, apiKey, status, description } = body; + + await updateProvider(id, { + name, + slug, + baseUrl, + parseMode, + apiKey, + status, + description, + }); + return R.success(null); +}); diff --git a/server/api/admin/llm/providers/index.get.ts b/server/api/admin/llm/providers/index.get.ts new file mode 100644 index 0000000..93b2044 --- /dev/null +++ b/server/api/admin/llm/providers/index.get.ts @@ -0,0 +1,15 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { listProviders } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const query = getQuery(event); + const page = Number(query.page) || 1; + const pageSize = Number(query.pageSize) || 20; + const search = query.search as string | undefined; + const status = query.status as string | undefined; + + const result = await listProviders(page, pageSize, search, status as any); + return R.success(result); +}); diff --git a/server/api/admin/llm/providers/index.post.ts b/server/api/admin/llm/providers/index.post.ts new file mode 100644 index 0000000..0938a69 --- /dev/null +++ b/server/api/admin/llm/providers/index.post.ts @@ -0,0 +1,24 @@ +import { requireAdmin } from "#server/utils/admin-guard"; +import { createProvider } from "#server/service/llm"; + +export default defineWrappedResponseHandler(async (event) => { + await requireAdmin(event); + + const body = await readBody(event); + const { name, slug, baseUrl, parseMode, apiKey, status, description } = body; + + if (!name || !slug) { + return R.throwError(422, "供应商名称和标识不能为空", null); + } + + const result = await createProvider({ + name, + slug, + baseUrl, + parseMode: parseMode || "openai", + apiKey, + status: status || "active", + description, + }); + return R.success(result); +}); diff --git a/server/service/llm/index.ts b/server/service/llm/index.ts new file mode 100644 index 0000000..dcbaea1 --- /dev/null +++ b/server/service/llm/index.ts @@ -0,0 +1,228 @@ +import { dbGlobal } from "drizzle-pkg/lib/db"; +import { llmProviders, llmModels } from "drizzle-pkg/lib/schema/llm"; +import { eq, desc, like, and, sql } from "drizzle-orm"; +import type { LlmParseMode, LlmProviderStatus, LlmModelType } from "drizzle-pkg/lib/schema/llm"; + +export interface LlmProviderRow { + id: number; + name: string; + slug: string; + baseUrl: string | null; + parseMode: LlmParseMode; + apiKey: string | null; + status: LlmProviderStatus; + description: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface LlmModelRow { + id: number; + providerId: number; + name: string; + modelId: string; + type: LlmModelType; + enabled: number; + description: string | null; + maxTokens: number | null; + createdAt: Date; + updatedAt: Date; +} + +export interface LlmProviderWithModels extends Omit { + apiKeySet: boolean; + models: LlmModelRow[]; +} + +export async function listProviders( + page = 1, + pageSize = 20, + search?: string, + status?: LlmProviderStatus, +) { + const conditions = []; + if (search) { + conditions.push(like(llmProviders.name, `%${search}%`)); + } + if (status) { + conditions.push(eq(llmProviders.status, status)); + } + + const where = conditions.length > 0 ? and(...conditions) : undefined; + + const [countResult] = await dbGlobal + .select({ count: sql`count(*)` }) + .from(llmProviders) + .where(where); + + const total = countResult?.count ?? 0; + const totalPages = Math.ceil(total / pageSize); + + const rows = await dbGlobal + .select() + .from(llmProviders) + .where(where) + .orderBy(desc(llmProviders.createdAt)) + .limit(pageSize) + .offset((page - 1) * pageSize); + + const sanitized = rows.map(({ apiKey, ...rest }) => ({ + ...rest, + apiKeySet: !!apiKey, + })); + + return { list: sanitized, total, page, pageSize, totalPages }; +} + +export async function getProviderById(id: number) { + const [row] = await dbGlobal + .select() + .from(llmProviders) + .where(eq(llmProviders.id, id)) + .limit(1); + return row ?? null; +} + +export async function getProviderWithModels(id: number): Promise { + const provider = await getProviderById(id); + if (!provider) return null; + + const models = await dbGlobal + .select() + .from(llmModels) + .where(eq(llmModels.providerId, id)) + .orderBy(desc(llmModels.createdAt)); + + const { apiKey, ...rest } = provider; + return { + ...rest, + apiKeySet: !!apiKey, + models, + }; +} + +export async function createProvider(data: { + name: string; + slug: string; + baseUrl?: string; + parseMode: LlmParseMode; + apiKey?: string; + status?: LlmProviderStatus; + description?: string; +}) { + const [inserted] = await dbGlobal + .insert(llmProviders) + .values({ + name: data.name, + slug: data.slug, + baseUrl: data.baseUrl || null, + parseMode: data.parseMode, + apiKey: data.apiKey || null, + status: data.status || "active", + description: data.description || null, + }) + .returning({ id: llmProviders.id }); + return inserted!; +} + +export async function updateProvider( + id: number, + data: { + name?: string; + slug?: string; + baseUrl?: string; + parseMode?: LlmParseMode; + apiKey?: string; + status?: LlmProviderStatus; + description?: string; + }, +) { + const updates: Record = {}; + if (data.name !== undefined) updates.name = data.name; + if (data.slug !== undefined) updates.slug = data.slug; + if (data.baseUrl !== undefined) updates.baseUrl = data.baseUrl; + if (data.parseMode !== undefined) updates.parseMode = data.parseMode; + if (data.status !== undefined) updates.status = data.status; + if (data.description !== undefined) updates.description = data.description; + if (data.apiKey !== undefined) { + if (data.apiKey === "") { + updates.apiKey = null; + } else if (data.apiKey !== "__unchanged__") { + updates.apiKey = data.apiKey; + } + } + + if (Object.keys(updates).length === 0) return; + + await dbGlobal + .update(llmProviders) + .set(updates) + .where(eq(llmProviders.id, id)); +} + +export async function deleteProvider(id: number) { + await dbGlobal.delete(llmProviders).where(eq(llmProviders.id, id)); +} + +export async function listModelsByProvider(providerId: number) { + return dbGlobal + .select() + .from(llmModels) + .where(eq(llmModels.providerId, providerId)) + .orderBy(desc(llmModels.createdAt)); +} + +export async function createModel(data: { + providerId: number; + name: string; + modelId: string; + type: LlmModelType; + enabled?: number; + description?: string; + maxTokens?: number; +}) { + const [inserted] = await dbGlobal + .insert(llmModels) + .values({ + providerId: data.providerId, + name: data.name, + modelId: data.modelId, + type: data.type, + enabled: data.enabled ?? 1, + description: data.description || null, + maxTokens: data.maxTokens || null, + }) + .returning({ id: llmModels.id }); + return inserted!; +} + +export async function updateModel( + id: number, + data: { + name?: string; + modelId?: string; + type?: LlmModelType; + enabled?: number; + description?: string; + maxTokens?: number; + }, +) { + const updates: Record = {}; + if (data.name !== undefined) updates.name = data.name; + if (data.modelId !== undefined) updates.modelId = data.modelId; + if (data.type !== undefined) updates.type = data.type; + if (data.enabled !== undefined) updates.enabled = data.enabled; + if (data.description !== undefined) updates.description = data.description; + if (data.maxTokens !== undefined) updates.maxTokens = data.maxTokens; + + if (Object.keys(updates).length === 0) return; + + await dbGlobal + .update(llmModels) + .set(updates) + .where(eq(llmModels.id, id)); +} + +export async function deleteModel(id: number) { + await dbGlobal.delete(llmModels).where(eq(llmModels.id, id)); +}