"""初始数据库结构 Revision ID: ee0a189f1532 Revises: Create Date: 2025-12-26 10:08:55.432217 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision: str = 'ee0a189f1532' down_revision: Union[str, None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """升级数据库结构""" # ### commands auto generated by Alembic - please adjust! ### op.create_table('batch_generation_tasks', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False, comment='项目ID'), sa.Column('user_id', sa.String(length=100), nullable=False, comment='用户ID'), sa.Column('start_chapter_number', sa.Integer(), nullable=False, comment='起始章节序号'), sa.Column('chapter_count', sa.Integer(), nullable=False, comment='生成章节数量'), sa.Column('chapter_ids', sa.JSON(), nullable=False, comment='待生成的章节ID列表'), sa.Column('style_id', sa.Integer(), nullable=True, comment='使用的写作风格ID'), sa.Column('target_word_count', sa.Integer(), nullable=True, comment='目标字数'), sa.Column('enable_analysis', sa.Boolean(), nullable=True, comment='是否启用同步分析'), sa.Column('status', sa.String(length=20), nullable=True, comment='任务状态: pending/running/completed/failed/cancelled'), sa.Column('total_chapters', sa.Integer(), nullable=True, comment='总章节数'), sa.Column('completed_chapters', sa.Integer(), nullable=True, comment='已完成章节数'), sa.Column('failed_chapters', sa.JSON(), nullable=True, comment='失败的章节信息列表'), sa.Column('current_chapter_id', sa.String(length=36), nullable=True, comment='当前正在生成的章节ID'), sa.Column('current_chapter_number', sa.Integer(), nullable=True, comment='当前正在生成的章节序号'), sa.Column('current_retry_count', sa.Integer(), nullable=True, comment='当前章节重试次数'), sa.Column('max_retries', sa.Integer(), nullable=True, comment='最大重试次数'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('started_at', sa.DateTime(), nullable=True, comment='开始时间'), sa.Column('completed_at', sa.DateTime(), nullable=True, comment='完成时间'), sa.Column('error_message', sa.String(length=500), nullable=True, comment='错误信息'), sa.PrimaryKeyConstraint('id') ) op.create_table('mcp_plugins', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('user_id', sa.String(length=50), nullable=False, comment='用户ID'), sa.Column('plugin_name', sa.String(length=100), nullable=False, comment='插件名称(唯一标识)'), sa.Column('display_name', sa.String(length=200), nullable=False, comment='显示名称'), sa.Column('description', sa.Text(), nullable=True, comment='插件描述'), sa.Column('plugin_type', sa.String(length=50), nullable=True, comment='插件类型:http/stdio'), sa.Column('server_url', sa.String(length=500), nullable=True, comment='服务器URL(HTTP类型)'), sa.Column('command', sa.String(length=500), nullable=True, comment='启动命令(stdio类型)'), sa.Column('args', sa.JSON(), nullable=True, comment='命令参数(stdio类型)'), sa.Column('env', sa.JSON(), nullable=True, comment='环境变量'), sa.Column('headers', sa.JSON(), nullable=True, comment='HTTP请求头'), sa.Column('config', sa.JSON(), nullable=True, comment='插件特定配置(JSON)'), sa.Column('tools', sa.JSON(), nullable=True, comment='提供的工具列表'), sa.Column('enabled', sa.Boolean(), nullable=True, comment='是否启用'), sa.Column('status', sa.String(length=50), nullable=True, comment='状态:active/inactive/error'), sa.Column('last_error', sa.Text(), nullable=True, comment='最后错误信息'), sa.Column('last_test_at', sa.DateTime(), nullable=True, comment='最后测试时间'), sa.Column('category', sa.String(length=100), nullable=True, comment='分类'), sa.Column('sort_order', sa.Integer(), nullable=True, comment='排序顺序'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_user_enabled', 'mcp_plugins', ['user_id', 'enabled'], unique=False) op.create_index('idx_user_plugin', 'mcp_plugins', ['user_id', 'plugin_name'], unique=True) op.create_index(op.f('ix_mcp_plugins_user_id'), 'mcp_plugins', ['user_id'], unique=False) op.create_table('projects', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('user_id', sa.String(length=100), nullable=False, comment='用户ID'), sa.Column('title', sa.String(length=200), nullable=False, comment='项目标题'), sa.Column('description', sa.Text(), nullable=True, comment='项目简介'), sa.Column('theme', sa.Text(), nullable=True, comment='主题'), sa.Column('genre', sa.String(length=50), nullable=True, comment='小说类型'), sa.Column('target_words', sa.Integer(), nullable=True, comment='目标字数'), sa.Column('current_words', sa.Integer(), nullable=True, comment='当前字数'), sa.Column('status', sa.String(length=20), nullable=True, comment='创作状态'), sa.Column('wizard_status', sa.String(length=20), nullable=True, comment='向导完成状态: incomplete/completed'), sa.Column('wizard_step', sa.Integer(), nullable=True, comment='向导当前步骤: 0-4'), sa.Column('outline_mode', sa.String(length=20), nullable=False, comment='大纲章节模式: one-to-one(传统模式) 或 one-to-many(细化模式)'), sa.Column('world_time_period', sa.Text(), nullable=True, comment='时间背景'), sa.Column('world_location', sa.Text(), nullable=True, comment='地理位置'), sa.Column('world_atmosphere', sa.Text(), nullable=True, comment='氛围基调'), sa.Column('world_rules', sa.Text(), nullable=True, comment='世界规则'), sa.Column('chapter_count', sa.Integer(), nullable=True, comment='章节数量'), sa.Column('narrative_perspective', sa.String(length=50), nullable=True, comment='叙事视角:first_person/third_person/omniscient'), sa.Column('character_count', sa.Integer(), nullable=True, comment='角色数量'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.CheckConstraint("outline_mode IN ('one-to-one', 'one-to-many')", name='check_outline_mode'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_projects_user_id'), 'projects', ['user_id'], unique=False) op.create_table('prompt_templates', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('user_id', sa.String(length=50), nullable=False, comment='用户ID'), sa.Column('template_key', sa.String(length=100), nullable=False, comment='模板键名'), sa.Column('template_name', sa.String(length=200), nullable=False, comment='模板显示名称'), sa.Column('template_content', sa.Text(), nullable=False, comment='模板内容'), sa.Column('description', sa.Text(), nullable=True, comment='模板描述'), sa.Column('category', sa.String(length=50), nullable=True, comment='模板分类'), sa.Column('parameters', sa.Text(), nullable=True, comment='模板参数定义(JSON)'), sa.Column('is_active', sa.Boolean(), nullable=True, comment='是否启用'), sa.Column('is_system_default', sa.Boolean(), nullable=True, comment='是否为系统默认模板'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_user_template', 'prompt_templates', ['user_id', 'template_key'], unique=True) op.create_index(op.f('ix_prompt_templates_user_id'), 'prompt_templates', ['user_id'], unique=False) op.create_table('relationship_types', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('name', sa.String(length=50), nullable=False, comment='关系名称'), sa.Column('category', sa.String(length=20), nullable=False, comment='分类:family/social/hostile/professional'), sa.Column('reverse_name', sa.String(length=50), nullable=True, comment='反向关系名称'), sa.Column('intimacy_range', sa.String(length=20), nullable=True, comment='亲密度范围:high/medium/low'), sa.Column('icon', sa.String(length=50), nullable=True, comment='图标标识'), sa.Column('description', sa.Text(), nullable=True, comment='关系描述'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_relationship_types_id'), 'relationship_types', ['id'], unique=False) op.create_table('settings', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('user_id', sa.String(length=50), nullable=False, comment='用户ID'), sa.Column('api_provider', sa.String(length=50), nullable=True, comment='API提供商'), sa.Column('api_key', sa.String(length=500), nullable=True, comment='API密钥'), sa.Column('api_base_url', sa.String(length=500), nullable=True, comment='自定义API地址'), sa.Column('llm_model', sa.String(length=100), nullable=True, comment='模型名称'), sa.Column('temperature', sa.Float(), nullable=True, comment='温度参数'), sa.Column('max_tokens', sa.Integer(), nullable=True, comment='最大token数'), sa.Column('preferences', sa.Text(), nullable=True, comment='其他偏好设置(JSON)'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_user_id', 'settings', ['user_id'], unique=False) op.create_index(op.f('ix_settings_user_id'), 'settings', ['user_id'], unique=True) op.create_table('user_passwords', sa.Column('user_id', sa.String(length=100), nullable=False, comment='用户ID'), sa.Column('username', sa.String(length=100), nullable=False, comment='用户名'), sa.Column('password_hash', sa.String(length=64), nullable=False, comment='密码哈希(SHA256)'), sa.Column('has_custom_password', sa.Boolean(), nullable=True, comment='是否为自定义密码'), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.PrimaryKeyConstraint('user_id') ) op.create_index(op.f('ix_user_passwords_user_id'), 'user_passwords', ['user_id'], unique=False) op.create_table('users', sa.Column('user_id', sa.String(length=100), nullable=False, comment='用户ID,格式:linuxdo_{id} 或 local_{id}'), sa.Column('username', sa.String(length=100), nullable=False, comment='用户名'), sa.Column('display_name', sa.String(length=200), nullable=False, comment='显示名称'), sa.Column('avatar_url', sa.String(length=500), nullable=True, comment='头像URL'), sa.Column('trust_level', sa.Integer(), nullable=True, comment='信任等级(仅用于显示)'), sa.Column('is_admin', sa.Boolean(), nullable=True, comment='是否为管理员'), sa.Column('linuxdo_id', sa.String(length=100), nullable=False, comment='LinuxDO用户ID或本地用户ID'), sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('last_login', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True, comment='最后登录时间'), sa.PrimaryKeyConstraint('user_id') ) op.create_index(op.f('ix_users_linuxdo_id'), 'users', ['linuxdo_id'], unique=True) op.create_index(op.f('ix_users_user_id'), 'users', ['user_id'], unique=False) op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=False) op.create_table('careers', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=100), nullable=False, comment='职业名称'), sa.Column('type', sa.String(length=20), nullable=False, comment='职业类型: main(主职业)/sub(副职业)'), sa.Column('description', sa.Text(), nullable=True, comment='职业描述'), sa.Column('category', sa.String(length=50), nullable=True, comment='职业分类(如:战斗系、生产系、辅助系)'), sa.Column('stages', sa.Text(), nullable=False, comment="职业阶段列表(JSON): [{level:1, name:'', description:''}, ...]"), sa.Column('max_stage', sa.Integer(), nullable=False, comment='最大阶段数'), sa.Column('requirements', sa.Text(), nullable=True, comment='职业要求/限制'), sa.Column('special_abilities', sa.Text(), nullable=True, comment='特殊能力描述'), sa.Column('worldview_rules', sa.Text(), nullable=True, comment='世界观规则关联'), sa.Column('attribute_bonuses', sa.Text(), nullable=True, comment="属性加成(JSON): {strength: '+10%', intelligence: '+5%'}"), sa.Column('source', sa.String(length=20), nullable=True, comment='来源: ai/manual'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_project_id', 'careers', ['project_id'], unique=False) op.create_index('idx_type', 'careers', ['type'], unique=False) op.create_table('outlines', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('title', sa.String(length=200), nullable=False, comment='大纲标题'), sa.Column('content', sa.Text(), nullable=True, comment='大纲内容'), sa.Column('structure', sa.Text(), nullable=True, comment='结构化大纲数据(JSON)'), sa.Column('order_index', sa.Integer(), nullable=True, comment='排序序号'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_table('writing_styles', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('user_id', sa.String(length=255), nullable=True, comment='所属用户ID(NULL表示全局预设风格)'), sa.Column('name', sa.String(length=100), nullable=False, comment='风格名称'), sa.Column('style_type', sa.String(length=50), nullable=False, comment='风格类型:preset/custom'), sa.Column('preset_id', sa.String(length=50), nullable=True, comment='预设风格ID:natural/classical/modern等'), sa.Column('description', sa.Text(), nullable=True, comment='风格描述'), sa.Column('prompt_content', sa.Text(), nullable=False, comment='风格提示词内容'), sa.Column('order_index', sa.Integer(), nullable=True, comment='排序序号'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['user_id'], ['users.user_id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_table('chapters', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('chapter_number', sa.Integer(), nullable=False, comment='章节序号'), sa.Column('title', sa.String(length=200), nullable=False, comment='章节标题'), sa.Column('content', sa.Text(), nullable=True, comment='章节内容'), sa.Column('summary', sa.Text(), nullable=True, comment='章节摘要'), sa.Column('word_count', sa.Integer(), nullable=True, comment='字数统计'), sa.Column('status', sa.String(length=20), nullable=True, comment='章节状态'), sa.Column('outline_id', sa.String(length=36), nullable=True, comment='关联的大纲ID'), sa.Column('sub_index', sa.Integer(), nullable=True, comment='大纲下的子章节序号'), sa.Column('expansion_plan', sa.Text(), nullable=True, comment='展开规划详情(JSON): 包含key_events, character_focus, emotional_tone等'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['outline_id'], ['outlines.id'], ondelete='SET NULL'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_table('characters', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=100), nullable=False, comment='角色/组织名称'), sa.Column('age', sa.String(length=50), nullable=True, comment='年龄'), sa.Column('gender', sa.String(length=50), nullable=True, comment='性别'), sa.Column('is_organization', sa.Boolean(), nullable=True, comment='是否为组织'), sa.Column('role_type', sa.String(length=50), nullable=True, comment='角色类型'), sa.Column('personality', sa.Text(), nullable=True, comment='性格特点/组织特性'), sa.Column('background', sa.Text(), nullable=True, comment='背景故事'), sa.Column('appearance', sa.Text(), nullable=True, comment='外貌描述'), sa.Column('relationships', sa.Text(), nullable=True, comment='人物关系(JSON)'), sa.Column('organization_type', sa.String(length=100), nullable=True, comment='组织类型'), sa.Column('organization_purpose', sa.String(length=500), nullable=True, comment='组织目的'), sa.Column('organization_members', sa.Text(), nullable=True, comment='组织成员(JSON)'), sa.Column('main_career_id', sa.String(length=36), nullable=True, comment='主职业ID'), sa.Column('main_career_stage', sa.Integer(), nullable=True, comment='主职业当前阶段'), sa.Column('sub_careers', sa.Text(), nullable=True, comment='副职业列表(JSON): [{"career_id": "xxx", "stage": 3}, ...]'), sa.Column('avatar_url', sa.String(length=500), nullable=True, comment='头像URL'), sa.Column('traits', sa.Text(), nullable=True, comment='特征标签(JSON)'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['main_career_id'], ['careers.id'], ondelete='SET NULL'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_table('project_default_styles', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False, comment='项目ID'), sa.Column('style_id', sa.Integer(), nullable=False, comment='风格ID'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['style_id'], ['writing_styles.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('project_id', name='uix_project_default_style') ) op.create_table('analysis_tasks', sa.Column('id', sa.String(length=36), nullable=False, comment='任务ID'), sa.Column('chapter_id', sa.String(length=36), nullable=False, comment='章节ID'), sa.Column('user_id', sa.String(length=50), nullable=False, comment='用户ID'), sa.Column('project_id', sa.String(length=36), nullable=False, comment='项目ID'), sa.Column('status', sa.String(length=20), nullable=False, comment='任务状态: pending/running/completed/failed'), sa.Column('progress', sa.Integer(), nullable=True, comment='进度 0-100'), sa.Column('error_message', sa.Text(), nullable=True, comment='错误信息'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('started_at', sa.DateTime(), nullable=True, comment='开始执行时间'), sa.Column('completed_at', sa.DateTime(), nullable=True, comment='完成时间'), sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_chapter_id_created', 'analysis_tasks', ['chapter_id', 'created_at'], unique=False) op.create_index('idx_status', 'analysis_tasks', ['status'], unique=False) op.create_table('character_careers', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('character_id', sa.String(length=36), nullable=False), sa.Column('career_id', sa.String(length=36), nullable=False), sa.Column('career_type', sa.String(length=20), nullable=False, comment='main(主职业)/sub(副职业)'), sa.Column('current_stage', sa.Integer(), nullable=False, comment='当前阶段(对应职业中的数值)'), sa.Column('stage_progress', sa.Integer(), nullable=True, comment='阶段内进度(0-100)'), sa.Column('started_at', sa.String(length=100), nullable=True, comment='开始修炼时间(小说时间线)'), sa.Column('reached_current_stage_at', sa.String(length=100), nullable=True, comment='到达当前阶段时间'), sa.Column('notes', sa.Text(), nullable=True, comment='备注(如:修炼心得、特殊事件)'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['career_id'], ['careers.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index('idx_career_type', 'character_careers', ['career_type'], unique=False) op.create_index('idx_character_career', 'character_careers', ['character_id', 'career_id'], unique=True) op.create_index('idx_character_id', 'character_careers', ['character_id'], unique=False) op.create_table('character_relationships', sa.Column('id', sa.String(length=36), nullable=False, comment='关系ID'), sa.Column('project_id', sa.String(length=36), nullable=False, comment='项目ID'), sa.Column('character_from_id', sa.String(length=36), nullable=False, comment='角色A的ID'), sa.Column('character_to_id', sa.String(length=36), nullable=False, comment='角色B的ID'), sa.Column('relationship_type_id', sa.Integer(), nullable=True, comment='关系类型ID'), sa.Column('relationship_name', sa.String(length=100), nullable=True, comment='自定义关系名称'), sa.Column('intimacy_level', sa.Integer(), nullable=True, comment='亲密度:-100到100'), sa.Column('status', sa.String(length=20), nullable=True, comment='状态:active/broken/past/complicated'), sa.Column('description', sa.Text(), nullable=True, comment='关系详细描述'), sa.Column('started_at', sa.String(length=100), nullable=True, comment='关系开始时间(故事时间)'), sa.Column('ended_at', sa.String(length=100), nullable=True, comment='关系结束时间(故事时间)'), sa.Column('source', sa.String(length=20), nullable=True, comment='来源:ai/manual/imported'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['character_from_id'], ['characters.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['character_to_id'], ['characters.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['relationship_type_id'], ['relationship_types.id'], ), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_character_relationships_character_from_id'), 'character_relationships', ['character_from_id'], unique=False) op.create_index(op.f('ix_character_relationships_character_to_id'), 'character_relationships', ['character_to_id'], unique=False) op.create_index(op.f('ix_character_relationships_project_id'), 'character_relationships', ['project_id'], unique=False) op.create_index(op.f('ix_character_relationships_relationship_type_id'), 'character_relationships', ['relationship_type_id'], unique=False) op.create_table('generation_history', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('chapter_id', sa.String(length=36), nullable=True), sa.Column('prompt', sa.Text(), nullable=True, comment='使用的提示词'), sa.Column('generated_content', sa.Text(), nullable=True, comment='生成的内容'), sa.Column('model', sa.String(length=50), nullable=True, comment='使用的模型'), sa.Column('tokens_used', sa.Integer(), nullable=True, comment='消耗的token数'), sa.Column('generation_time', sa.Float(), nullable=True, comment='生成耗时(秒)'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], ondelete='SET NULL'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_table('organizations', sa.Column('id', sa.String(length=36), nullable=False, comment='组织ID'), sa.Column('character_id', sa.String(length=36), nullable=False, comment='关联的角色ID'), sa.Column('project_id', sa.String(length=36), nullable=False, comment='项目ID'), sa.Column('parent_org_id', sa.String(length=36), nullable=True, comment='父组织ID'), sa.Column('level', sa.Integer(), nullable=True, comment='组织层级'), sa.Column('power_level', sa.Integer(), nullable=True, comment='势力等级:0-100'), sa.Column('member_count', sa.Integer(), nullable=True, comment='成员数量'), sa.Column('location', sa.Text(), nullable=True, comment='所在地'), sa.Column('motto', sa.String(length=200), nullable=True, comment='宗旨/口号'), sa.Column('color', sa.String(length=100), nullable=True, comment='代表颜色'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['parent_org_id'], ['organizations.id'], ondelete='SET NULL'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('character_id') ) op.create_index(op.f('ix_organizations_project_id'), 'organizations', ['project_id'], unique=False) op.create_table('plot_analysis', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('chapter_id', sa.String(length=36), nullable=False), sa.Column('plot_stage', sa.String(length=50), nullable=True, comment='剧情阶段: 开端/发展/高潮/结局/过渡'), sa.Column('conflict_level', sa.Integer(), nullable=True, comment='冲突强度 1-10'), sa.Column('conflict_types', sa.JSON(), nullable=True, comment="冲突类型列表: ['人与人', '人与己', '人与环境']"), sa.Column('emotional_tone', sa.String(length=100), nullable=True, comment='主导情感: 紧张/温馨/悲伤/激昂/平静'), sa.Column('emotional_intensity', sa.Float(), nullable=True, comment='情感强度 0.0-1.0'), sa.Column('emotional_curve', sa.JSON(), nullable=True, comment='情感曲线: {start: 0.3, middle: 0.7, end: 0.5}'), sa.Column('hooks', sa.JSON(), nullable=True, comment='钩子列表 - 吸引读者的元素: [\n {\n "type": "悬念|情感|冲突|认知",\n "content": "具体内容",\n "strength": 8,\n "position": "开头|中段|结尾"\n }\n ]'), sa.Column('hooks_count', sa.Integer(), nullable=True, comment='钩子数量'), sa.Column('hooks_avg_strength', sa.Float(), nullable=True, comment='钩子平均强度'), sa.Column('foreshadows', sa.JSON(), nullable=True, comment='伏笔列表: [\n {\n "content": "伏笔内容",\n "type": "planted|resolved",\n "strength": 7,\n "subtlety": 8,\n "reference_chapter": 3\n }\n ]'), sa.Column('foreshadows_planted', sa.Integer(), nullable=True, comment='本章埋下的伏笔数量'), sa.Column('foreshadows_resolved', sa.Integer(), nullable=True, comment='本章回收的伏笔数量'), sa.Column('plot_points', sa.JSON(), nullable=True, comment='情节点列表: [\n {\n "content": "情节点描述",\n "importance": 0.9,\n "type": "revelation|conflict|resolution|transition",\n "impact": "对故事的影响描述"\n }\n ]'), sa.Column('plot_points_count', sa.Integer(), nullable=True, comment='情节点数量'), sa.Column('character_states', sa.JSON(), nullable=True, comment='角色状态变化: [\n {\n "character_id": "xxx",\n "character_name": "张三",\n "state_before": "犹豫不决",\n "state_after": "坚定信念",\n "psychological_change": "内心描述",\n "key_event": "触发事件",\n "relationship_changes": {"李四": "关系变化"}\n }\n ]'), sa.Column('scenes', sa.JSON(), nullable=True, comment="场景列表: [{location: '地点', atmosphere: '氛围', duration: '时长'}]"), sa.Column('pacing', sa.String(length=50), nullable=True, comment='节奏: slow|moderate|fast|varied'), sa.Column('overall_quality_score', sa.Float(), nullable=True, comment='整体质量评分 0.0-10.0'), sa.Column('pacing_score', sa.Float(), nullable=True, comment='节奏评分 0.0-10.0'), sa.Column('engagement_score', sa.Float(), nullable=True, comment='吸引力评分 0.0-10.0'), sa.Column('coherence_score', sa.Float(), nullable=True, comment='连贯性评分 0.0-10.0'), sa.Column('analysis_report', sa.Text(), nullable=True, comment='完整的文字分析报告'), sa.Column('suggestions', sa.JSON(), nullable=True, comment="改进建议列表: ['建议1', '建议2']"), sa.Column('word_count', sa.Integer(), nullable=True, comment='章节字数'), sa.Column('dialogue_ratio', sa.Float(), nullable=True, comment='对话占比 0.0-1.0'), sa.Column('description_ratio', sa.Float(), nullable=True, comment='描写占比 0.0-1.0'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='分析时间'), sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_plot_analysis_chapter_id'), 'plot_analysis', ['chapter_id'], unique=True) op.create_index(op.f('ix_plot_analysis_project_id'), 'plot_analysis', ['project_id'], unique=False) op.create_table('regeneration_tasks', sa.Column('id', sa.String(length=36), nullable=False), sa.Column('chapter_id', sa.String(length=36), nullable=False), sa.Column('analysis_id', sa.String(length=36), nullable=True, comment='关联的分析结果ID'), sa.Column('user_id', sa.String(length=50), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('modification_instructions', sa.Text(), nullable=False, comment='综合修改指令'), sa.Column('original_suggestions', sa.JSON(), nullable=True, comment='来自分析的原始建议列表'), sa.Column('selected_suggestion_indices', sa.JSON(), nullable=True, comment='用户选择的建议索引'), sa.Column('custom_instructions', sa.Text(), nullable=True, comment='用户自定义修改意见'), sa.Column('style_id', sa.Integer(), nullable=True, comment='写作风格ID'), sa.Column('target_word_count', sa.Integer(), nullable=True, comment='目标字数'), sa.Column('focus_areas', sa.JSON(), nullable=True, comment='重点优化方向'), sa.Column('preserve_elements', sa.JSON(), nullable=True, comment='需要保留的元素配置'), sa.Column('status', sa.String(length=20), nullable=True, comment='pending/running/completed/failed'), sa.Column('progress', sa.Integer(), nullable=True, comment='进度 0-100'), sa.Column('error_message', sa.Text(), nullable=True), sa.Column('original_content', sa.Text(), nullable=True, comment='原始章节内容快照'), sa.Column('original_word_count', sa.Integer(), nullable=True, comment='原始字数'), sa.Column('regenerated_content', sa.Text(), nullable=True, comment='重新生成的内容'), sa.Column('regenerated_word_count', sa.Integer(), nullable=True, comment='新内容字数'), sa.Column('version_number', sa.Integer(), nullable=True, comment='版本号'), sa.Column('version_note', sa.String(length=500), nullable=True, comment='版本说明'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), sa.Column('started_at', sa.DateTime(), nullable=True), sa.Column('completed_at', sa.DateTime(), nullable=True), sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_regeneration_tasks_chapter_id'), 'regeneration_tasks', ['chapter_id'], unique=False) op.create_index(op.f('ix_regeneration_tasks_project_id'), 'regeneration_tasks', ['project_id'], unique=False) op.create_index(op.f('ix_regeneration_tasks_user_id'), 'regeneration_tasks', ['user_id'], unique=False) op.create_table('story_memories', sa.Column('id', sa.String(length=100), nullable=False), sa.Column('project_id', sa.String(length=36), nullable=False), sa.Column('chapter_id', sa.String(length=36), nullable=True), sa.Column('memory_type', sa.String(length=50), nullable=False, comment='\n 记忆类型:\n - plot_point: 情节点\n - character_event: 角色事件\n - world_detail: 世界观细节\n - hook: 钩子(悬念/冲突)\n - foreshadow: 伏笔\n - dialogue: 重要对话\n - scene: 场景描写\n '), sa.Column('title', sa.String(length=200), nullable=True, comment='记忆标题/简述'), sa.Column('content', sa.Text(), nullable=False, comment='记忆内容摘要(100-500字)'), sa.Column('full_context', sa.Text(), nullable=True, comment='完整上下文(可选,用于详细记录)'), sa.Column('related_characters', sa.JSON(), nullable=True, comment="涉及角色ID列表: ['char_id_1', 'char_id_2']"), sa.Column('related_locations', sa.JSON(), nullable=True, comment="涉及地点列表: ['地点1', '地点2']"), sa.Column('tags', sa.JSON(), nullable=True, comment="标签列表: ['悬念', '转折', '伏笔', '高潮']"), sa.Column('importance_score', sa.Float(), nullable=True, comment='重要性评分 0.0-1.0'), sa.Column('story_timeline', sa.Integer(), nullable=False, comment='故事时间线位置(章节序号)'), sa.Column('chapter_position', sa.Integer(), nullable=True, comment='章节内位置(字符位置)'), sa.Column('text_length', sa.Integer(), nullable=True, comment='文本长度(字符数)'), sa.Column('is_foreshadow', sa.Integer(), nullable=True, comment='伏笔状态: 0=普通记忆, 1=已埋下伏笔, 2=伏笔已回收'), sa.Column('foreshadow_resolved_at', sa.String(length=100), nullable=True, comment='伏笔回收的章节ID'), sa.Column('foreshadow_strength', sa.Float(), nullable=True, comment='伏笔强度 0.0-1.0'), sa.Column('vector_id', sa.String(length=100), nullable=True, comment='向量数据库中的唯一ID'), sa.Column('embedding_model', sa.String(length=100), nullable=True, comment='使用的embedding模型'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['foreshadow_resolved_at'], ['chapters.id'], ondelete='SET NULL'), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('vector_id') ) op.create_index(op.f('ix_story_memories_chapter_id'), 'story_memories', ['chapter_id'], unique=False) op.create_index(op.f('ix_story_memories_memory_type'), 'story_memories', ['memory_type'], unique=False) op.create_index(op.f('ix_story_memories_project_id'), 'story_memories', ['project_id'], unique=False) op.create_index(op.f('ix_story_memories_story_timeline'), 'story_memories', ['story_timeline'], unique=False) op.create_table('organization_members', sa.Column('id', sa.String(length=36), nullable=False, comment='成员关系ID'), sa.Column('organization_id', sa.String(length=36), nullable=False, comment='组织ID'), sa.Column('character_id', sa.String(length=36), nullable=False, comment='角色ID'), sa.Column('position', sa.String(length=100), nullable=False, comment='职位名称'), sa.Column('rank', sa.Integer(), nullable=True, comment='职位等级'), sa.Column('status', sa.String(length=20), nullable=True, comment='状态:active/retired/expelled/deceased'), sa.Column('joined_at', sa.String(length=100), nullable=True, comment='加入时间(故事时间)'), sa.Column('left_at', sa.String(length=100), nullable=True, comment='离开时间(故事时间)'), sa.Column('loyalty', sa.Integer(), nullable=True, comment='忠诚度:0-100'), sa.Column('contribution', sa.Integer(), nullable=True, comment='贡献度:0-100'), sa.Column('source', sa.String(length=20), nullable=True, comment='来源:ai/manual'), sa.Column('notes', sa.Text(), nullable=True, comment='备注'), sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='创建时间'), sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True, comment='更新时间'), sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') ) op.create_index(op.f('ix_organization_members_character_id'), 'organization_members', ['character_id'], unique=False) op.create_index(op.f('ix_organization_members_organization_id'), 'organization_members', ['organization_id'], unique=False) # ### end Alembic commands ### def downgrade() -> None: """降级数据库结构""" # ### commands auto generated by Alembic - please adjust! ### op.drop_index(op.f('ix_organization_members_organization_id'), table_name='organization_members') op.drop_index(op.f('ix_organization_members_character_id'), table_name='organization_members') op.drop_table('organization_members') op.drop_index(op.f('ix_story_memories_story_timeline'), table_name='story_memories') op.drop_index(op.f('ix_story_memories_project_id'), table_name='story_memories') op.drop_index(op.f('ix_story_memories_memory_type'), table_name='story_memories') op.drop_index(op.f('ix_story_memories_chapter_id'), table_name='story_memories') op.drop_table('story_memories') op.drop_index(op.f('ix_regeneration_tasks_user_id'), table_name='regeneration_tasks') op.drop_index(op.f('ix_regeneration_tasks_project_id'), table_name='regeneration_tasks') op.drop_index(op.f('ix_regeneration_tasks_chapter_id'), table_name='regeneration_tasks') op.drop_table('regeneration_tasks') op.drop_index(op.f('ix_plot_analysis_project_id'), table_name='plot_analysis') op.drop_index(op.f('ix_plot_analysis_chapter_id'), table_name='plot_analysis') op.drop_table('plot_analysis') op.drop_index(op.f('ix_organizations_project_id'), table_name='organizations') op.drop_table('organizations') op.drop_table('generation_history') op.drop_index(op.f('ix_character_relationships_relationship_type_id'), table_name='character_relationships') op.drop_index(op.f('ix_character_relationships_project_id'), table_name='character_relationships') op.drop_index(op.f('ix_character_relationships_character_to_id'), table_name='character_relationships') op.drop_index(op.f('ix_character_relationships_character_from_id'), table_name='character_relationships') op.drop_table('character_relationships') op.drop_index('idx_character_id', table_name='character_careers') op.drop_index('idx_character_career', table_name='character_careers') op.drop_index('idx_career_type', table_name='character_careers') op.drop_table('character_careers') op.drop_index('idx_status', table_name='analysis_tasks') op.drop_index('idx_chapter_id_created', table_name='analysis_tasks') op.drop_table('analysis_tasks') op.drop_table('project_default_styles') op.drop_table('characters') op.drop_table('chapters') op.drop_table('writing_styles') op.drop_table('outlines') op.drop_index('idx_type', table_name='careers') op.drop_index('idx_project_id', table_name='careers') op.drop_table('careers') op.drop_index(op.f('ix_users_username'), table_name='users') op.drop_index(op.f('ix_users_user_id'), table_name='users') op.drop_index(op.f('ix_users_linuxdo_id'), table_name='users') op.drop_table('users') op.drop_index(op.f('ix_user_passwords_user_id'), table_name='user_passwords') op.drop_table('user_passwords') op.drop_index(op.f('ix_settings_user_id'), table_name='settings') op.drop_index('idx_user_id', table_name='settings') op.drop_table('settings') op.drop_index(op.f('ix_relationship_types_id'), table_name='relationship_types') op.drop_table('relationship_types') op.drop_index(op.f('ix_prompt_templates_user_id'), table_name='prompt_templates') op.drop_index('idx_user_template', table_name='prompt_templates') op.drop_table('prompt_templates') op.drop_index(op.f('ix_projects_user_id'), table_name='projects') op.drop_table('projects') op.drop_index(op.f('ix_mcp_plugins_user_id'), table_name='mcp_plugins') op.drop_index('idx_user_plugin', table_name='mcp_plugins') op.drop_index('idx_user_enabled', table_name='mcp_plugins') op.drop_table('mcp_plugins') op.drop_table('batch_generation_tasks') # ### end Alembic commands ###