From 7e04425f7dd385bc0b80a954e0d1cf561e93f961 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Thu, 2 Jul 2026 12:54:35 +0800 Subject: [PATCH] feat(llm): add CRUD operations for providers and models - Implemented GET, POST, PUT, DELETE endpoints for LLM providers and models. - Added validation for provider and model IDs. - Introduced functions to list, create, update, and delete providers and models in the service layer. - Enhanced error handling for invalid inputs and non-existent resources. - Updated database schema to include new provider and model management features. --- AGENTS.md | 1 + app/pages/admin.vue | 1 + app/pages/admin/llm-config/index.vue | 1446 +++++++++++++++++ packages/drizzle-pkg/db.sqlite | Bin 282624 -> 282624 bytes packages/drizzle-pkg/lib/schema/llm.ts | 58 + .../migrations/0012_funny_santa_claus.sql | 33 + .../drizzle-pkg/migrations/meta/0012_snapshot.json | 1659 ++++++++++++++++++++ packages/drizzle-pkg/migrations/meta/_journal.json | 7 + server/api/admin/llm/models/[providerId].get.ts | 14 + server/api/admin/llm/models/[providerId].post.ts | 29 + server/api/admin/llm/models/detail/[id].delete.ts | 14 + server/api/admin/llm/models/detail/[id].put.ts | 24 + server/api/admin/llm/providers/[id].delete.ts | 14 + server/api/admin/llm/providers/[id].get.ts | 17 + server/api/admin/llm/providers/[id].models.get.ts | 50 + server/api/admin/llm/providers/[id].put.ts | 25 + server/api/admin/llm/providers/index.get.ts | 15 + server/api/admin/llm/providers/index.post.ts | 24 + server/service/llm/index.ts | 228 +++ 19 files changed, 3659 insertions(+) create mode 100644 app/pages/admin/llm-config/index.vue create mode 100644 packages/drizzle-pkg/lib/schema/llm.ts create mode 100644 packages/drizzle-pkg/migrations/0012_funny_santa_claus.sql create mode 100644 packages/drizzle-pkg/migrations/meta/0012_snapshot.json create mode 100644 server/api/admin/llm/models/[providerId].get.ts create mode 100644 server/api/admin/llm/models/[providerId].post.ts create mode 100644 server/api/admin/llm/models/detail/[id].delete.ts create mode 100644 server/api/admin/llm/models/detail/[id].put.ts create mode 100644 server/api/admin/llm/providers/[id].delete.ts create mode 100644 server/api/admin/llm/providers/[id].get.ts create mode 100644 server/api/admin/llm/providers/[id].models.get.ts create mode 100644 server/api/admin/llm/providers/[id].put.ts create mode 100644 server/api/admin/llm/providers/index.get.ts create mode 100644 server/api/admin/llm/providers/index.post.ts create mode 100644 server/service/llm/index.ts 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 d7a12231fb5d8f0fc193e4b6318e676473c0985a..bd7412c6f550507073bfe1fd96c5d1b9196490bc 100644 GIT binary patch delta 2912 zcmeHJdu&r>6u?+=gk_z}Dy17TCrfC0Q6_ZYz^ldT)1cq1mHd+bttL zx&Zy>YK)N)4J4>M(m(?m1q{R}0ZA}QA_i0hacrQVBoOtxy>t(@A^+==^EKah z&iQ`leCM9uS>KSZZ%8-rjP5LkVICwJBoZVwgA48YvsiJ@4ykYgx6z6%h5PUa+&J`gLDb`O) zTUlp-aXH;qYl)+kwK}aV>vS=0*5N2|I7^*&tKDw5a4yd8a@skoD_~*#u2x40YjykG zu2Rm1@1m}c?o6_ILy~zU9LL}R{0YCoO}Gv}!uN0yF2FhX0#3p4q=9+|q*)RSCzGel z6=6v>vHV^6P_~Ic5epgDAgM-Dg`^V6QX~~fguU=vmb4ta0CSYkfuZ|9hwtDR9DuiB zC%gn5==LTskV=U`+1}pa>j<%2do)`iF{ZcSNT?^sa*=3q=eeG2*(~fDrmf-?mkU(d z2Gy(NFt$ZeH8|*zZKULSvmw#7hG3J|F6T<{swDo?b{Szc^1ADJiI%lQPV%E07YY0j zKRDEq_<9YoD*4*bC-~-DM{eOb9Y;}SXC1@job<4Cvvgk8F>NjRGx@T5M4eQZs!l4m zDnULhAHd3yA*K9~vBDCI#k5eRq{B=EoeS?IDySOIvPy4V zGi3~gxlSf%r0Q2TQ}y1uI>DL=2YqeaCebv}x0-mGH*06}xx+d9>fw`dm8sP&*INyE zKOXF4x!xe#>l-VBFSaS1&PFUs*~92->uWqOQ1nz&np#;eW+gh%R3R-~L7O5jx!#RH z^pAkVQL)F~Y795J+O#$7lUL(DP4v*|ePiB$h|e^L>iB+Ocv7RWtHSl@8&xXLT^WzF4S@>x|M= zurtPO;3A1DYl)S@H+w(XFme!2p%pcxt4D*g_nk37pPCUt)GjdYnjVT&1(6UR)}5S_4*x+tylS^}GB<4!he~ z!~__Bk;NTwSvi}{&bV9E_^z6JUB!q7e!Xc<-8w_)Bu3#TvI_$>93Z>MVQ`S2Y0JsC zv`4iA@U1on)@y&zZh&p5WoyYq*CoPKSSa!0PlA>@d5NeX(g3g3=U|+yCBQfkgZnK^iN(eSO8nMRCQ!;Tt+8MahXNhm6B1W3t~(gq zL4jb{;W_wKC{#=Gl^JXyK$Yw?rtQ)y)NiN_s*e@>6~*%X@-o?JG&c6h;@#zP{bD2D zpEIci#~LOYV`AOWaYjOvTRnXjKH@Uoh8YF=zzb>dKH3y2lk1%bHcrlf+uprl5^B}I zp(2NwFa?$Al_he01wx#k2F%!>B{N7TF#8<@9#cwkl_CZ1v1C#I093=Rpa1{> delta 20316 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 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)); +}