diff --git a/app/components/TopNav.vue b/app/components/TopNav.vue
index 3cd6e29..0929731 100644
--- a/app/components/TopNav.vue
+++ b/app/components/TopNav.vue
@@ -79,7 +79,8 @@ watch(() => route.path, () => {
注册
- 管理
+ 设置
+ 管理
登出
diff --git a/app/pages/admin.vue b/app/pages/admin.vue
index 2f54a53..ba41561 100644
--- a/app/pages/admin.vue
+++ b/app/pages/admin.vue
@@ -25,7 +25,6 @@ const adminNav: NavItem[] = [
children: [
{ label: '全局配置', to: '/admin/global-config' },
{ label: '用户管理', to: '/admin/users' },
- { label: '模型配置', to: '/admin/llm-config' },
]
},
]
diff --git a/app/pages/settings.vue b/app/pages/settings.vue
new file mode 100644
index 0000000..cfc84a2
--- /dev/null
+++ b/app/pages/settings.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
diff --git a/app/pages/admin/llm-config/index.vue b/app/pages/settings/llm-config/index.vue
similarity index 97%
rename from app/pages/admin/llm-config/index.vue
rename to app/pages/settings/llm-config/index.vue
index 81830ab..ce48700 100644
--- a/app/pages/admin/llm-config/index.vue
+++ b/app/pages/settings/llm-config/index.vue
@@ -29,7 +29,7 @@ const searchQuery = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
-const { data, refresh, pending } = await useHttpFetch('/api/admin/llm/providers', {
+const { data, refresh, pending } = await useHttpFetch('/api/llm/providers', {
query: computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
@@ -133,7 +133,7 @@ async function handleProviderSave() {
providerLoading.value = true
try {
if (editingProvider.value) {
- await $fetch(`/api/admin/llm/providers/${editingProvider.value.id}` as string, {
+ await $fetch(`/api/llm/providers/${editingProvider.value.id}` as string, {
method: 'PUT' as const,
body: {
...providerForm.value,
@@ -142,7 +142,7 @@ async function handleProviderSave() {
})
showToast('供应商更新成功')
} else {
- await $fetch('/api/admin/llm/providers', {
+ await $fetch('/api/llm/providers', {
method: 'POST',
body: providerForm.value,
})
@@ -160,7 +160,7 @@ async function handleProviderSave() {
async function toggleProviderStatus(p: LlmProvider) {
const newStatus = p.status === 'active' ? 'disabled' : 'active'
try {
- await $fetch(`/api/admin/llm/providers/${p.id}` as string, {
+ await $fetch(`/api/llm/providers/${p.id}` as string, {
method: 'PUT' as const,
body: { status: newStatus },
})
@@ -179,7 +179,7 @@ async function loadModels(providerId: number) {
expandedProviderId.value = providerId
modelsLoading.value = true
try {
- const res = await $fetch(`/api/admin/llm/models/${providerId}`)
+ const res = await $fetch(`/api/llm/models/${providerId}`)
providerModels.value.set(providerId, (res as any)?.data ?? [])
} catch {
providerModels.value.set(providerId, [])
@@ -192,7 +192,7 @@ async function fetchAvailableModels(providerId: number) {
availableModelsLoading.value = true
availableModels.value = []
try {
- const res = await $fetch(`/api/admin/llm/providers/${providerId}/models`)
+ const res = await $fetch(`/api/llm/providers/${providerId}/models`)
const models = (res as any)?.data ?? []
if (models.length > 0) {
availableModels.value = models
@@ -246,13 +246,13 @@ async function handleModelSave() {
modelLoading.value = true
try {
if (editingModel.value) {
- await $fetch(`/api/admin/llm/models/detail/${editingModel.value.id}` as string, {
+ await $fetch(`/api/llm/models/detail/${editingModel.value.id}` as string, {
method: 'PUT' as const,
body: modelForm.value,
})
showToast('模型更新成功')
} else {
- await $fetch(`/api/admin/llm/models/${currentProviderId.value}` as string, {
+ await $fetch(`/api/llm/models/${currentProviderId.value}` as string, {
method: 'POST' as const,
body: modelForm.value,
})
@@ -272,7 +272,7 @@ async function handleModelSave() {
async function toggleModelEnabled(m: LlmModel) {
const newEnabled = m.enabled ? 0 : 1
try {
- await $fetch(`/api/admin/llm/models/detail/${m.id}` as string, {
+ await $fetch(`/api/llm/models/detail/${m.id}` as string, {
method: 'PUT' as const,
body: { enabled: newEnabled },
})
@@ -295,13 +295,13 @@ async function handleDelete() {
deleteLoading.value = true
try {
if (deleteTarget.value.type === 'provider') {
- await $fetch(`/api/admin/llm/providers/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
+ await $fetch(`/api/llm/providers/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
if (expandedProviderId.value === deleteTarget.value.id) {
expandedProviderId.value = null
}
await refresh()
} else {
- await $fetch(`/api/admin/llm/models/detail/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
+ await $fetch(`/api/llm/models/detail/${deleteTarget.value.id}` as string, { method: 'DELETE' as const })
if (expandedProviderId.value) {
await loadModels(expandedProviderId.value)
}
diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite
index bd7412c..c5eb703 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
index fcc33d9..a741128 100644
--- a/packages/drizzle-pkg/lib/schema/llm.ts
+++ b/packages/drizzle-pkg/lib/schema/llm.ts
@@ -1,4 +1,5 @@
-import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
+import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core";
+import { users } from "./auth";
export const LlmProviderStatuses = ["active", "disabled"] as const;
export type LlmProviderStatus = (typeof LlmProviderStatuses)[number];
@@ -13,8 +14,11 @@ 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(),
+ userId: integer("user_id")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ name: text("name", { length: 100 }).notNull(),
+ slug: text("slug", { length: 100 }).notNull(),
baseUrl: text("base_url", { length: 500 }),
parseMode: text("parse_mode", { enum: LlmParseModes }).notNull().default("openai"),
apiKey: text("api_key"),
@@ -27,7 +31,8 @@ export const llmProviders = sqliteTable(
.notNull(),
},
(table) => [
- index("idx_llm_provider_slug").on(table.slug),
+ uniqueIndex("idx_llm_provider_user_slug").on(table.userId, table.slug),
+ index("idx_llm_provider_user").on(table.userId),
index("idx_llm_provider_status").on(table.status),
],
);
diff --git a/packages/drizzle-pkg/migrations/0013_flaky_marvel_boy.sql b/packages/drizzle-pkg/migrations/0013_flaky_marvel_boy.sql
new file mode 100644
index 0000000..12a6437
--- /dev/null
+++ b/packages/drizzle-pkg/migrations/0013_flaky_marvel_boy.sql
@@ -0,0 +1,24 @@
+DROP INDEX `llm_providers_name_unique`;--> statement-breakpoint
+DROP INDEX `llm_providers_slug_unique`;--> statement-breakpoint
+DROP INDEX `idx_llm_provider_slug`;--> statement-breakpoint
+ALTER TABLE `llm_providers` ADD `user_id` integer REFERENCES users(id);--> statement-breakpoint
+UPDATE `llm_providers` SET `user_id` = (SELECT MIN(id) FROM `users`) WHERE `user_id` IS NULL;--> statement-breakpoint
+CREATE TABLE `_llm_providers_tmp` (
+ `id` integer PRIMARY KEY AUTOINCREMENT,
+ `user_id` integer NOT NULL REFERENCES users(id),
+ `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 NOT NULL,
+ `updated_at` integer NOT NULL
+);--> statement-breakpoint
+INSERT INTO `_llm_providers_tmp` SELECT `id`, `user_id`, `name`, `slug`, `base_url`, `parse_mode`, `api_key`, `status`, `description`, `created_at`, `updated_at` FROM `llm_providers`;--> statement-breakpoint
+DROP TABLE `llm_providers`;--> statement-breakpoint
+ALTER TABLE `_llm_providers_tmp` RENAME TO `llm_providers`;--> statement-breakpoint
+CREATE UNIQUE INDEX `idx_llm_provider_user_slug` ON `llm_providers` (`user_id`,`slug`);--> statement-breakpoint
+CREATE INDEX `idx_llm_provider_user` ON `llm_providers` (`user_id`);--> statement-breakpoint
+CREATE INDEX `idx_llm_provider_status` ON `llm_providers` (`status`);
diff --git a/packages/drizzle-pkg/migrations/meta/0013_snapshot.json b/packages/drizzle-pkg/migrations/meta/0013_snapshot.json
new file mode 100644
index 0000000..e6448d7
--- /dev/null
+++ b/packages/drizzle-pkg/migrations/meta/0013_snapshot.json
@@ -0,0 +1,1674 @@
+{
+ "version": "6",
+ "dialect": "sqlite",
+ "id": "f4ec8869-c90b-426e-972a-2e1dc8d4e79f",
+ "prevId": "20c54319-abd6-41f9-956b-39d5b6d9e222",
+ "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
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "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
+ },
+ "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": {
+ "idx_llm_provider_user_slug": {
+ "name": "idx_llm_provider_user_slug",
+ "columns": [
+ "user_id",
+ "slug"
+ ],
+ "isUnique": true
+ },
+ "idx_llm_provider_user": {
+ "name": "idx_llm_provider_user",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ },
+ "idx_llm_provider_status": {
+ "name": "idx_llm_provider_status",
+ "columns": [
+ "status"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {
+ "llm_providers_user_id_users_id_fk": {
+ "name": "llm_providers_user_id_users_id_fk",
+ "tableFrom": "llm_providers",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "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 5778112..0d0ec79 100644
--- a/packages/drizzle-pkg/migrations/meta/_journal.json
+++ b/packages/drizzle-pkg/migrations/meta/_journal.json
@@ -92,6 +92,13 @@
"when": 1782963564430,
"tag": "0012_funny_santa_claus",
"breakpoints": true
+ },
+ {
+ "idx": 13,
+ "version": "6",
+ "when": 1782969066349,
+ "tag": "0013_flaky_marvel_boy",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/server/api/admin/llm/models/[providerId].get.ts b/server/api/llm/models/[providerId].get.ts
similarity index 65%
rename from server/api/admin/llm/models/[providerId].get.ts
rename to server/api/llm/models/[providerId].get.ts
index b9cb88a..986b649 100644
--- a/server/api/admin/llm/models/[providerId].get.ts
+++ b/server/api/llm/models/[providerId].get.ts
@@ -1,14 +1,14 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { listModelsByProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const providerId = Number(getRouterParam(event, "providerId"));
if (!providerId) {
return R.throwError(400, "无效的供应商ID", null);
}
- const result = await listModelsByProvider(providerId);
+ const result = await listModelsByProvider(providerId, user.id);
return R.success(result);
});
diff --git a/server/api/admin/llm/models/[providerId].post.ts b/server/api/llm/models/[providerId].post.ts
similarity index 74%
rename from server/api/admin/llm/models/[providerId].post.ts
rename to server/api/llm/models/[providerId].post.ts
index b0e1633..73d859f 100644
--- a/server/api/admin/llm/models/[providerId].post.ts
+++ b/server/api/llm/models/[providerId].post.ts
@@ -1,8 +1,8 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { createModel } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const providerId = Number(getRouterParam(event, "providerId"));
if (!providerId) {
@@ -16,7 +16,7 @@ export default defineWrappedResponseHandler(async (event) => {
return R.throwError(422, "模型名称和模型ID不能为空", null);
}
- const result = await createModel({
+ const result = await createModel(user.id, {
providerId,
name,
modelId,
@@ -25,5 +25,8 @@ export default defineWrappedResponseHandler(async (event) => {
description,
maxTokens,
});
+ if (!result) {
+ return R.throwError(403, "无权操作该供应商", null);
+ }
return R.success(result);
});
diff --git a/server/api/admin/llm/models/detail/[id].delete.ts b/server/api/llm/models/detail/[id].delete.ts
similarity index 67%
rename from server/api/admin/llm/models/detail/[id].delete.ts
rename to server/api/llm/models/detail/[id].delete.ts
index 6789d9a..8632e05 100644
--- a/server/api/admin/llm/models/detail/[id].delete.ts
+++ b/server/api/llm/models/detail/[id].delete.ts
@@ -1,14 +1,14 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { deleteModel } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的模型ID", null);
}
- await deleteModel(id);
+ await deleteModel(id, user.id);
return R.success(null);
});
diff --git a/server/api/admin/llm/models/detail/[id].put.ts b/server/api/llm/models/detail/[id].put.ts
similarity index 78%
rename from server/api/admin/llm/models/detail/[id].put.ts
rename to server/api/llm/models/detail/[id].put.ts
index 097fe08..610f7c1 100644
--- a/server/api/admin/llm/models/detail/[id].put.ts
+++ b/server/api/llm/models/detail/[id].put.ts
@@ -1,8 +1,8 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { updateModel } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
@@ -12,7 +12,7 @@ export default defineWrappedResponseHandler(async (event) => {
const body = await readBody(event);
const { name, modelId, type, enabled, description, maxTokens } = body;
- await updateModel(id, {
+ await updateModel(id, user.id, {
name,
modelId,
type,
diff --git a/server/api/admin/llm/providers/[id].delete.ts b/server/api/llm/providers/[id].delete.ts
similarity index 67%
rename from server/api/admin/llm/providers/[id].delete.ts
rename to server/api/llm/providers/[id].delete.ts
index f75adb3..dcec566 100644
--- a/server/api/admin/llm/providers/[id].delete.ts
+++ b/server/api/llm/providers/[id].delete.ts
@@ -1,14 +1,14 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { deleteProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的供应商ID", null);
}
- await deleteProvider(id);
+ await deleteProvider(id, user.id);
return R.success(null);
});
diff --git a/server/api/admin/llm/providers/[id].get.ts b/server/api/llm/providers/[id].get.ts
similarity index 70%
rename from server/api/admin/llm/providers/[id].get.ts
rename to server/api/llm/providers/[id].get.ts
index a8d03ba..040820a 100644
--- a/server/api/admin/llm/providers/[id].get.ts
+++ b/server/api/llm/providers/[id].get.ts
@@ -1,15 +1,15 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { getProviderWithModels } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的供应商ID", null);
}
- const result = await getProviderWithModels(id);
+ const result = await getProviderWithModels(id, user.id);
if (!result) {
return R.throwError(404, "供应商不存在", null);
}
diff --git a/server/api/admin/llm/providers/[id].models.get.ts b/server/api/llm/providers/[id].models.get.ts
similarity index 88%
rename from server/api/admin/llm/providers/[id].models.get.ts
rename to server/api/llm/providers/[id].models.get.ts
index a6211f8..b1e1406 100644
--- a/server/api/admin/llm/providers/[id].models.get.ts
+++ b/server/api/llm/providers/[id].models.get.ts
@@ -1,15 +1,15 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { getProviderById } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
return R.throwError(400, "无效的供应商 ID", null);
}
- const provider = await getProviderById(id);
+ const provider = await getProviderById(id, user.id);
if (!provider) {
return R.throwError(404, "供应商不存在", null);
}
diff --git a/server/api/admin/llm/providers/[id].put.ts b/server/api/llm/providers/[id].put.ts
similarity index 78%
rename from server/api/admin/llm/providers/[id].put.ts
rename to server/api/llm/providers/[id].put.ts
index ec94f99..35e6385 100644
--- a/server/api/admin/llm/providers/[id].put.ts
+++ b/server/api/llm/providers/[id].put.ts
@@ -1,8 +1,8 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { updateProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const id = Number(getRouterParam(event, "id"));
if (!id) {
@@ -12,7 +12,7 @@ export default defineWrappedResponseHandler(async (event) => {
const body = await readBody(event);
const { name, slug, baseUrl, parseMode, apiKey, status, description } = body;
- await updateProvider(id, {
+ await updateProvider(id, user.id, {
name,
slug,
baseUrl,
diff --git a/server/api/admin/llm/providers/index.get.ts b/server/api/llm/providers/index.get.ts
similarity index 67%
rename from server/api/admin/llm/providers/index.get.ts
rename to server/api/llm/providers/index.get.ts
index 93b2044..69e759b 100644
--- a/server/api/admin/llm/providers/index.get.ts
+++ b/server/api/llm/providers/index.get.ts
@@ -1,8 +1,8 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { listProviders } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const query = getQuery(event);
const page = Number(query.page) || 1;
@@ -10,6 +10,6 @@ export default defineWrappedResponseHandler(async (event) => {
const search = query.search as string | undefined;
const status = query.status as string | undefined;
- const result = await listProviders(page, pageSize, search, status as any);
+ const result = await listProviders(user.id, page, pageSize, search, status as any);
return R.success(result);
});
diff --git a/server/api/admin/llm/providers/index.post.ts b/server/api/llm/providers/index.post.ts
similarity index 78%
rename from server/api/admin/llm/providers/index.post.ts
rename to server/api/llm/providers/index.post.ts
index 0938a69..88f729f 100644
--- a/server/api/admin/llm/providers/index.post.ts
+++ b/server/api/llm/providers/index.post.ts
@@ -1,8 +1,8 @@
-import { requireAdmin } from "#server/utils/admin-guard";
+import { requireUser } from "#server/utils/context";
import { createProvider } from "#server/service/llm";
export default defineWrappedResponseHandler(async (event) => {
- await requireAdmin(event);
+ const user = await requireUser(event);
const body = await readBody(event);
const { name, slug, baseUrl, parseMode, apiKey, status, description } = body;
@@ -11,7 +11,7 @@ export default defineWrappedResponseHandler(async (event) => {
return R.throwError(422, "供应商名称和标识不能为空", null);
}
- const result = await createProvider({
+ const result = await createProvider(user.id, {
name,
slug,
baseUrl,
diff --git a/server/service/llm/index.ts b/server/service/llm/index.ts
index dcbaea1..b972d26 100644
--- a/server/service/llm/index.ts
+++ b/server/service/llm/index.ts
@@ -5,6 +5,7 @@ import type { LlmParseMode, LlmProviderStatus, LlmModelType } from "drizzle-pkg/
export interface LlmProviderRow {
id: number;
+ userId: number;
name: string;
slug: string;
baseUrl: string | null;
@@ -35,12 +36,13 @@ export interface LlmProviderWithModels extends Omit {
}
export async function listProviders(
+ userId: number,
page = 1,
pageSize = 20,
search?: string,
status?: LlmProviderStatus,
) {
- const conditions = [];
+ const conditions = [eq(llmProviders.userId, userId)];
if (search) {
conditions.push(like(llmProviders.name, `%${search}%`));
}
@@ -48,7 +50,7 @@ export async function listProviders(
conditions.push(eq(llmProviders.status, status));
}
- const where = conditions.length > 0 ? and(...conditions) : undefined;
+ const where = and(...conditions);
const [countResult] = await dbGlobal
.select({ count: sql`count(*)` })
@@ -74,17 +76,17 @@ export async function listProviders(
return { list: sanitized, total, page, pageSize, totalPages };
}
-export async function getProviderById(id: number) {
+export async function getProviderById(id: number, userId: number) {
const [row] = await dbGlobal
.select()
.from(llmProviders)
- .where(eq(llmProviders.id, id))
+ .where(and(eq(llmProviders.id, id), eq(llmProviders.userId, userId)))
.limit(1);
return row ?? null;
}
-export async function getProviderWithModels(id: number): Promise {
- const provider = await getProviderById(id);
+export async function getProviderWithModels(id: number, userId: number): Promise {
+ const provider = await getProviderById(id, userId);
if (!provider) return null;
const models = await dbGlobal
@@ -101,7 +103,7 @@ export async function getProviderWithModels(id: number): Promise = {};
if (data.name !== undefined) updates.name = data.name;
if (data.modelId !== undefined) updates.modelId = data.modelId;
@@ -223,6 +247,9 @@ export async function updateModel(
.where(eq(llmModels.id, id));
}
-export async function deleteModel(id: number) {
+export async function deleteModel(id: number, userId: number) {
+ const model = await getModelById(id, userId);
+ if (!model) return;
+
await dbGlobal.delete(llmModels).where(eq(llmModels.id, id));
}