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>
This commit is contained in:
jocay
2026-10-18 02:04:40 +08:00
commit 3c9b65d982
106 changed files with 16286 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
CREATE TABLE `agents` (
`id` text PRIMARY KEY NOT NULL,
`project_id` text NOT NULL,
`role` text NOT NULL,
`name` text NOT NULL,
`model_id` text,
`system_prompt` text DEFAULT '' NOT NULL,
`style_notes` text DEFAULT '' NOT NULL,
`created_by` text DEFAULT 'user' NOT NULL,
`enabled` integer DEFAULT true NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`model_id`) REFERENCES `models`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE INDEX `agents_project_idx` ON `agents` (`project_id`);--> statement-breakpoint
CREATE TABLE `chapter_versions` (
`id` text PRIMARY KEY NOT NULL,
`chapter_id` text NOT NULL,
`version` integer NOT NULL,
`file_path` text NOT NULL,
`word_count` integer DEFAULT 0 NOT NULL,
`writer_agent_id` text,
`writer_name` text DEFAULT '' NOT NULL,
`model_label` text DEFAULT '' NOT NULL,
`feedback` text DEFAULT '' NOT NULL,
`status` text DEFAULT 'writing' NOT NULL,
`created_at` integer NOT NULL,
FOREIGN KEY (`chapter_id`) REFERENCES `chapters`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `versions_chapter_idx` ON `chapter_versions` (`chapter_id`);--> statement-breakpoint
CREATE TABLE `chapters` (
`id` text PRIMARY KEY NOT NULL,
`project_id` text NOT NULL,
`index` integer NOT NULL,
`title` text DEFAULT '' NOT NULL,
`synopsis` text DEFAULT '' NOT NULL,
`brief` text DEFAULT '' NOT NULL,
`writer_agent_id` text,
`writer_locked` integer DEFAULT false NOT NULL,
`editor_notes` text DEFAULT '' NOT NULL,
`review_notes` text DEFAULT '' NOT NULL,
`status` text DEFAULT 'planned' NOT NULL,
`active_version_id` text,
`summary` text DEFAULT '' NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade,
FOREIGN KEY (`writer_agent_id`) REFERENCES `agents`(`id`) ON UPDATE no action ON DELETE set null
);
--> statement-breakpoint
CREATE INDEX `chapters_project_idx` ON `chapters` (`project_id`,`index`);--> statement-breakpoint
CREATE TABLE `characters` (
`id` text PRIMARY KEY NOT NULL,
`project_id` text NOT NULL,
`name` text NOT NULL,
`identity` text DEFAULT '' NOT NULL,
`description` text DEFAULT '' NOT NULL,
`personality` text DEFAULT '' NOT NULL,
`speech_examples` text DEFAULT '' NOT NULL,
`notes` text DEFAULT '' NOT NULL,
`avatar` text DEFAULT '' NOT NULL,
`source_card` text,
`sort_order` integer DEFAULT 0 NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `characters_project_idx` ON `characters` (`project_id`);--> statement-breakpoint
CREATE TABLE `jobs` (
`id` text PRIMARY KEY NOT NULL,
`project_id` text NOT NULL,
`chapter_id` text,
`type` text NOT NULL,
`status` text DEFAULT 'running' NOT NULL,
`agent_name` text DEFAULT '' NOT NULL,
`model_label` text DEFAULT '' NOT NULL,
`input_tokens` integer DEFAULT 0 NOT NULL,
`output_tokens` integer DEFAULT 0 NOT NULL,
`error` text DEFAULT '' NOT NULL,
`log` text DEFAULT '' NOT NULL,
`prompt` text,
`created_at` integer NOT NULL,
`finished_at` integer,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `jobs_project_idx` ON `jobs` (`project_id`,`created_at`);--> statement-breakpoint
CREATE TABLE `lore_entries` (
`id` text PRIMARY KEY NOT NULL,
`project_id` text NOT NULL,
`title` text DEFAULT '' NOT NULL,
`keys` text NOT NULL,
`content` text DEFAULT '' NOT NULL,
`constant` integer DEFAULT false NOT NULL,
`enabled` integer DEFAULT true NOT NULL,
`source` text DEFAULT 'user' NOT NULL,
`sort_order` integer DEFAULT 100 NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE INDEX `lore_project_idx` ON `lore_entries` (`project_id`);--> statement-breakpoint
CREATE TABLE `models` (
`id` text PRIMARY KEY NOT NULL,
`provider_id` text NOT NULL,
`label` text NOT NULL,
`model_id` text NOT NULL,
`temperature` real,
`top_p` real,
`max_output_tokens` integer,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL,
FOREIGN KEY (`provider_id`) REFERENCES `providers`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `projects` (
`id` text PRIMARY KEY NOT NULL,
`title` text NOT NULL,
`description` text DEFAULT '' NOT NULL,
`world_setting` text DEFAULT '' NOT NULL,
`story_outline` text DEFAULT '' NOT NULL,
`requirements` text NOT NULL,
`settings` text NOT NULL,
`memory` text NOT NULL,
`user_name` text DEFAULT '' NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
--> statement-breakpoint
CREATE TABLE `providers` (
`id` text PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL,
`base_url` text DEFAULT '' NOT NULL,
`api_key_enc` text DEFAULT '' NOT NULL,
`created_at` integer NOT NULL,
`updated_at` integer NOT NULL
);
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1792257891167,
"tag": "0000_init",
"breakpoints": true
}
]
}
+142
View File
@@ -0,0 +1,142 @@
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)])