Files
ai-novel-studio/server/db/schema.ts
T
jocay 3c9b65d982 AI 小说工坊:主编 + 作家多模型协作写作应用
- Nuxt 4 + Nuxt UI 4 + AI SDK 7 + Drizzle/SQLite
- 主编规划大纲与章节简报、选派作家;作家流式写入章节文件并实时推送前端
- 逐章确认模式(默认)与自动模式;摘要与故事记忆
- 酒馆角色卡(PNG/JSON,V1-V3)与世界书导入
- 自定义最高优先级系统提示词、审稿/修改/多版本、任务日志与提示词检视
- 模拟 LLM 服务与端到端测试脚本

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 02:04:40 +08:00

143 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { sqliteTable, text, integer, real, index } from 'drizzle-orm/sqlite-core'
import type {
ProviderType, ProjectRequirements, ProjectSettings, ProjectMemory,
AgentRole, ChapterStatus, VersionStatus, JobType, JobStatus, LoreEntrySource
} from '../../shared/types'
const id = () => text('id').primaryKey()
const createdAt = () => integer('created_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date())
const updatedAt = () => integer('updated_at', { mode: 'timestamp_ms' }).notNull().$defaultFn(() => new Date()).$onUpdateFn(() => new Date())
/** API 提供方(OpenAI 兼容 / Anthropic / Google) */
export const providers = sqliteTable('providers', {
id: id(),
name: text('name').notNull(),
type: text('type').$type<ProviderType>().notNull(),
baseUrl: text('base_url').notNull().default(''),
apiKeyEnc: text('api_key_enc').notNull().default(''),
createdAt: createdAt(),
updatedAt: updatedAt()
})
/** 模型预设:某个提供方下的具体模型 + 采样参数 */
export const models = sqliteTable('models', {
id: id(),
providerId: text('provider_id').notNull().references(() => providers.id, { onDelete: 'cascade' }),
label: text('label').notNull(),
modelId: text('model_id').notNull(),
temperature: real('temperature'),
topP: real('top_p'),
maxOutputTokens: integer('max_output_tokens'),
createdAt: createdAt(),
updatedAt: updatedAt()
})
export const projects = sqliteTable('projects', {
id: id(),
title: text('title').notNull(),
description: text('description').notNull().default(''),
worldSetting: text('world_setting').notNull().default(''),
storyOutline: text('story_outline').notNull().default(''),
requirements: text('requirements', { mode: 'json' }).$type<ProjectRequirements>().notNull(),
settings: text('settings', { mode: 'json' }).$type<ProjectSettings>().notNull(),
memory: text('memory', { mode: 'json' }).$type<ProjectMemory>().notNull(),
userName: text('user_name').notNull().default(''),
createdAt: createdAt(),
updatedAt: updatedAt()
})
export const characters = sqliteTable('characters', {
id: id(),
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
identity: text('identity').notNull().default(''),
description: text('description').notNull().default(''),
personality: text('personality').notNull().default(''),
speechExamples: text('speech_examples').notNull().default(''),
notes: text('notes').notNull().default(''),
avatar: text('avatar').notNull().default(''),
sourceCard: text('source_card', { mode: 'json' }).$type<Record<string, unknown> | null>(),
sortOrder: integer('sort_order').notNull().default(0),
createdAt: createdAt(),
updatedAt: updatedAt()
}, t => [index('characters_project_idx').on(t.projectId)])
/** 世界书条目:常驻条目每次注入,关键词条目在章节简报/大纲命中时注入 */
export const loreEntries = sqliteTable('lore_entries', {
id: id(),
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
title: text('title').notNull().default(''),
keys: text('keys', { mode: 'json' }).$type<string[]>().notNull(),
content: text('content').notNull().default(''),
constant: integer('constant', { mode: 'boolean' }).notNull().default(false),
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
source: text('source').$type<LoreEntrySource>().notNull().default('user'),
sortOrder: integer('sort_order').notNull().default(100),
createdAt: createdAt(),
updatedAt: updatedAt()
}, t => [index('lore_project_idx').on(t.projectId)])
/** AI 成员:主编 / 作家 / 摘要员 */
export const agents = sqliteTable('agents', {
id: id(),
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
role: text('role').$type<AgentRole>().notNull(),
name: text('name').notNull(),
modelId: text('model_id').references(() => models.id, { onDelete: 'set null' }),
systemPrompt: text('system_prompt').notNull().default(''),
styleNotes: text('style_notes').notNull().default(''),
createdBy: text('created_by').$type<'user' | 'editor'>().notNull().default('user'),
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
createdAt: createdAt(),
updatedAt: updatedAt()
}, t => [index('agents_project_idx').on(t.projectId)])
export const chapters = sqliteTable('chapters', {
id: id(),
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
index: integer('index').notNull(),
title: text('title').notNull().default(''),
synopsis: text('synopsis').notNull().default(''),
brief: text('brief').notNull().default(''),
writerAgentId: text('writer_agent_id').references(() => agents.id, { onDelete: 'set null' }),
writerLocked: integer('writer_locked', { mode: 'boolean' }).notNull().default(false),
editorNotes: text('editor_notes').notNull().default(''),
reviewNotes: text('review_notes').notNull().default(''),
status: text('status').$type<ChapterStatus>().notNull().default('planned'),
activeVersionId: text('active_version_id'),
summary: text('summary').notNull().default(''),
createdAt: createdAt(),
updatedAt: updatedAt()
}, t => [index('chapters_project_idx').on(t.projectId, t.index)])
export const chapterVersions = sqliteTable('chapter_versions', {
id: id(),
chapterId: text('chapter_id').notNull().references(() => chapters.id, { onDelete: 'cascade' }),
version: integer('version').notNull(),
filePath: text('file_path').notNull(),
wordCount: integer('word_count').notNull().default(0),
writerAgentId: text('writer_agent_id'),
writerName: text('writer_name').notNull().default(''),
modelLabel: text('model_label').notNull().default(''),
feedback: text('feedback').notNull().default(''),
status: text('status').$type<VersionStatus>().notNull().default('writing'),
createdAt: createdAt()
}, t => [index('versions_chapter_idx').on(t.chapterId)])
export const jobs = sqliteTable('jobs', {
id: id(),
projectId: text('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
chapterId: text('chapter_id'),
type: text('type').$type<JobType>().notNull(),
status: text('status').$type<JobStatus>().notNull().default('running'),
agentName: text('agent_name').notNull().default(''),
modelLabel: text('model_label').notNull().default(''),
inputTokens: integer('input_tokens').notNull().default(0),
outputTokens: integer('output_tokens').notNull().default(0),
error: text('error').notNull().default(''),
log: text('log').notNull().default(''),
prompt: text('prompt', { mode: 'json' }).$type<{ system: string, prompt: string } | null>(),
createdAt: createdAt(),
finishedAt: integer('finished_at', { mode: 'timestamp_ms' })
}, t => [index('jobs_project_idx').on(t.projectId, t.createdAt)])