update:1.重构项目数据库初始化和迁移逻辑,使用Alembic数据库管理工具
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
# Alembic 数据库迁移指南
|
||||
|
||||
本项目支持 **PostgreSQL** 和 **SQLite** 两种数据库,使用独立的 Alembic 配置管理迁移。
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
backend/
|
||||
├── alembic-postgres.ini # PostgreSQL 配置文件
|
||||
├── alembic-sqlite.ini # SQLite 配置文件
|
||||
├── alembic/
|
||||
│ ├── postgres/ # PostgreSQL 迁移脚本目录
|
||||
│ │ ├── env.py
|
||||
│ │ ├── script.py.mako
|
||||
│ │ └── versions/ # PostgreSQL 迁移版本
|
||||
│ │ ├── 20251226_1008_ee0a189f1532_初始数据库结构.py
|
||||
│ │ └── 20251226_1102_e411428f00c0_初始化预置数据.py
|
||||
│ └── sqlite/ # SQLite 迁移脚本目录
|
||||
│ ├── env.py
|
||||
│ ├── script.py.mako
|
||||
│ └── versions/ # SQLite 迁移版本
|
||||
│ └── 20251226_1322_fbeb1038c728_初始化sqlite数据库.py
|
||||
```
|
||||
|
||||
## 🚀 使用方法
|
||||
|
||||
### 1. PostgreSQL 数据库
|
||||
|
||||
#### 配置环境变量
|
||||
```bash
|
||||
# .env 文件
|
||||
DATABASE_URL=postgresql+asyncpg://username:password@localhost:5432/database_name
|
||||
```
|
||||
|
||||
#### 生成迁移脚本
|
||||
```bash
|
||||
cd backend
|
||||
alembic -c alembic-postgres.ini revision --autogenerate -m "描述信息"
|
||||
```
|
||||
|
||||
#### 应用迁移
|
||||
```bash
|
||||
alembic -c alembic-postgres.ini upgrade head
|
||||
```
|
||||
|
||||
#### 回退迁移
|
||||
```bash
|
||||
alembic -c alembic-postgres.ini downgrade -1
|
||||
```
|
||||
|
||||
#### 查看迁移历史
|
||||
```bash
|
||||
alembic -c alembic-postgres.ini history
|
||||
alembic -c alembic-postgres.ini current
|
||||
```
|
||||
|
||||
### 2. SQLite 数据库
|
||||
|
||||
#### 配置环境变量
|
||||
```bash
|
||||
# .env 文件
|
||||
DATABASE_URL=sqlite+aiosqlite:///./data/mumuai.db
|
||||
```
|
||||
|
||||
#### 生成迁移脚本
|
||||
```bash
|
||||
cd backend
|
||||
alembic -c alembic-sqlite.ini revision --autogenerate -m "描述信息"
|
||||
```
|
||||
|
||||
#### 应用迁移
|
||||
```bash
|
||||
alembic -c alembic-sqlite.ini upgrade head
|
||||
```
|
||||
|
||||
#### 回退迁移
|
||||
```bash
|
||||
alembic -c alembic-sqlite.ini downgrade -1
|
||||
```
|
||||
|
||||
#### 查看迁移历史
|
||||
```bash
|
||||
alembic -c alembic-sqlite.ini history
|
||||
alembic -c alembic-sqlite.ini current
|
||||
```
|
||||
|
||||
## ⚙️ 关键配置差异
|
||||
|
||||
### PostgreSQL (alembic/postgres/env.py)
|
||||
- `render_as_batch=False` - 直接支持 ALTER TABLE
|
||||
- 使用 `server_default=sa.text('now()')`
|
||||
|
||||
### SQLite (alembic/sqlite/env.py)
|
||||
- `render_as_batch=True` - 通过重建表实现 ALTER TABLE
|
||||
- 使用 `server_default=sa.text('(CURRENT_TIMESTAMP)')` - SQLite 格式
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
### SQLite 限制
|
||||
1. **并发写入**:同时只允许一个写操作
|
||||
2. **ALTER TABLE 限制**:某些操作需要重建表(Alembic 的批处理模式会自动处理)
|
||||
3. **类型映射**:
|
||||
- `JSON` → `TEXT` (SQLAlchemy 自动处理)
|
||||
- `BOOLEAN` → `INTEGER` (0/1)
|
||||
- `DEFAULT now()` → `DEFAULT CURRENT_TIMESTAMP`
|
||||
|
||||
### PostgreSQL 优势
|
||||
1. **高并发支持**:多用户同时读写
|
||||
2. **完整的 ALTER TABLE 支持**
|
||||
3. **高级特性**:全文搜索、JSON 操作符、数组类型等
|
||||
|
||||
## 🔄 切换数据库
|
||||
|
||||
只需修改 `.env` 文件中的 `DATABASE_URL`,然后使用对应的配置文件执行迁移:
|
||||
|
||||
```bash
|
||||
# 切换到 SQLite
|
||||
DATABASE_URL=sqlite+aiosqlite:///./data/mumuai.db
|
||||
alembic -c alembic-sqlite.ini upgrade head
|
||||
|
||||
# 切换到 PostgreSQL
|
||||
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/db
|
||||
alembic -c alembic-postgres.ini upgrade head
|
||||
```
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
1. **开发环境**:使用 SQLite(简单、无需额外服务)
|
||||
2. **生产环境**:使用 PostgreSQL(性能、并发、稳定性)
|
||||
3. **保持同步**:两个数据库的模型定义必须一致
|
||||
4. **测试迁移**:在两种数据库上都测试迁移脚本
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: 迁移脚本生成后可以通用吗?
|
||||
A: 不行。PostgreSQL 和 SQLite 的迁移脚本是独立的,因为:
|
||||
- SQL 语法差异(如 DEFAULT 值)
|
||||
- 类型差异(如 JSON、BOOLEAN)
|
||||
- ALTER TABLE 能力差异
|
||||
|
||||
### Q: 如何从 PostgreSQL 迁移数据到 SQLite?
|
||||
A: 需要编写数据导出/导入脚本,不能直接复用迁移脚本。
|
||||
|
||||
### Q: 为什么 SQLite 迁移这么慢?
|
||||
A: SQLite 的 ALTER TABLE 限制导致需要重建表,这在大表时会很慢。
|
||||
@@ -0,0 +1,2 @@
|
||||
# 此文件确保 versions 目录被 Git 追踪
|
||||
# 迁移版本文件将存放在此目录
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Alembic 环境配置文件 - PostgreSQL"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from alembic import context
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 导入应用配置
|
||||
from app.config import settings
|
||||
|
||||
# 导入 Base 和所有模型
|
||||
from app.database import Base
|
||||
from app.models import (
|
||||
Project, Outline, Character, Chapter, GenerationHistory,
|
||||
Settings, WritingStyle, ProjectDefaultStyle,
|
||||
RelationshipType, CharacterRelationship, Organization, OrganizationMember,
|
||||
StoryMemory, PlotAnalysis, AnalysisTask, BatchGenerationTask,
|
||||
RegenerationTask, Career, CharacterCareer, User, MCPPlugin, PromptTemplate
|
||||
)
|
||||
|
||||
# Alembic Config 对象
|
||||
config = context.config
|
||||
|
||||
# 设置数据库连接字符串(从环境变量读取)
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
# 配置日志
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 设置 target_metadata 为应用的 Base.metadata
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""在'离线'模式下运行迁移"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
"""执行迁移的核心函数 - PostgreSQL 专用"""
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
render_as_batch=False, # PostgreSQL 不需要批处理模式
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
"""在'在线'模式下运行异步迁移"""
|
||||
configuration = config.get_section(config.config_ini_section, {})
|
||||
configuration["sqlalchemy.url"] = settings.database_url
|
||||
|
||||
connectable = async_engine_from_config(
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""在'在线'模式下运行迁移"""
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
# 根据上下文选择运行模式
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,554 @@
|
||||
"""初始数据库结构
|
||||
|
||||
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 ###
|
||||
@@ -0,0 +1,181 @@
|
||||
"""初始化预置数据
|
||||
|
||||
Revision ID: e411428f00c0
|
||||
Revises: ee0a189f1532
|
||||
Create Date: 2025-12-26 11:02:24.080526
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from datetime import datetime
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import table, column, String, Integer, Float, Text, Boolean, DateTime
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'e411428f00c0'
|
||||
down_revision: Union[str, None] = 'ee0a189f1532'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""插入预置数据"""
|
||||
|
||||
# ==================== 1. 插入关系类型数据 ====================
|
||||
relationship_types_table = table(
|
||||
'relationship_types',
|
||||
column('name', String),
|
||||
column('category', String),
|
||||
column('reverse_name', String),
|
||||
column('intimacy_range', String),
|
||||
column('icon', String),
|
||||
column('description', Text),
|
||||
)
|
||||
|
||||
relationship_types_data = [
|
||||
# 家庭关系
|
||||
{"name": "父亲", "category": "family", "reverse_name": "子女", "intimacy_range": "high", "icon": "👨", "description": "父子/父女关系"},
|
||||
{"name": "母亲", "category": "family", "reverse_name": "子女", "intimacy_range": "high", "icon": "👩", "description": "母子/母女关系"},
|
||||
{"name": "兄弟", "category": "family", "reverse_name": "兄弟", "intimacy_range": "high", "icon": "👬", "description": "兄弟关系"},
|
||||
{"name": "姐妹", "category": "family", "reverse_name": "姐妹", "intimacy_range": "high", "icon": "👭", "description": "姐妹关系"},
|
||||
{"name": "子女", "category": "family", "reverse_name": "父母", "intimacy_range": "high", "icon": "👶", "description": "子女关系"},
|
||||
{"name": "配偶", "category": "family", "reverse_name": "配偶", "intimacy_range": "high", "icon": "💑", "description": "夫妻关系"},
|
||||
{"name": "恋人", "category": "family", "reverse_name": "恋人", "intimacy_range": "high", "icon": "💕", "description": "恋爱关系"},
|
||||
|
||||
# 社交关系
|
||||
{"name": "师父", "category": "social", "reverse_name": "徒弟", "intimacy_range": "high", "icon": "🎓", "description": "师徒关系(师父视角)"},
|
||||
{"name": "徒弟", "category": "social", "reverse_name": "师父", "intimacy_range": "high", "icon": "📚", "description": "师徒关系(徒弟视角)"},
|
||||
{"name": "朋友", "category": "social", "reverse_name": "朋友", "intimacy_range": "medium", "icon": "🤝", "description": "朋友关系"},
|
||||
{"name": "同学", "category": "social", "reverse_name": "同学", "intimacy_range": "medium", "icon": "🎒", "description": "同学关系"},
|
||||
{"name": "邻居", "category": "social", "reverse_name": "邻居", "intimacy_range": "low", "icon": "🏘️", "description": "邻居关系"},
|
||||
{"name": "知己", "category": "social", "reverse_name": "知己", "intimacy_range": "high", "icon": "💙", "description": "知心好友"},
|
||||
|
||||
# 职业关系
|
||||
{"name": "上司", "category": "professional", "reverse_name": "下属", "intimacy_range": "low", "icon": "👔", "description": "上下级关系(上司视角)"},
|
||||
{"name": "下属", "category": "professional", "reverse_name": "上司", "intimacy_range": "low", "icon": "💼", "description": "上下级关系(下属视角)"},
|
||||
{"name": "同事", "category": "professional", "reverse_name": "同事", "intimacy_range": "medium", "icon": "🤵", "description": "同事关系"},
|
||||
{"name": "合作伙伴", "category": "professional", "reverse_name": "合作伙伴", "intimacy_range": "medium", "icon": "🤜🤛", "description": "合作关系"},
|
||||
|
||||
# 敌对关系
|
||||
{"name": "敌人", "category": "hostile", "reverse_name": "敌人", "intimacy_range": "low", "icon": "⚔️", "description": "敌对关系"},
|
||||
{"name": "仇人", "category": "hostile", "reverse_name": "仇人", "intimacy_range": "low", "icon": "💢", "description": "仇恨关系"},
|
||||
{"name": "竞争对手", "category": "hostile", "reverse_name": "竞争对手", "intimacy_range": "low", "icon": "🎯", "description": "竞争关系"},
|
||||
{"name": "宿敌", "category": "hostile", "reverse_name": "宿敌", "intimacy_range": "low", "icon": "⚡", "description": "宿命之敌"},
|
||||
]
|
||||
|
||||
op.bulk_insert(relationship_types_table, relationship_types_data)
|
||||
print(f"✅ 已插入 {len(relationship_types_data)} 条关系类型数据")
|
||||
|
||||
|
||||
# ==================== 2. 插入全局写作风格预设 ====================
|
||||
# 注意:这里需要从 WritingStyleManager 获取预设配置
|
||||
# 为了避免导入应用代码,我们直接硬编码预设风格
|
||||
|
||||
writing_styles_table = table(
|
||||
'writing_styles',
|
||||
column('user_id', String),
|
||||
column('name', String),
|
||||
column('style_type', String),
|
||||
column('preset_id', String),
|
||||
column('description', Text),
|
||||
column('prompt_content', Text),
|
||||
column('order_index', Integer),
|
||||
)
|
||||
|
||||
writing_styles_data = [
|
||||
{
|
||||
"user_id": None, # NULL 表示全局预设
|
||||
"name": "自然流畅",
|
||||
"style_type": "preset",
|
||||
"preset_id": "natural",
|
||||
"description": "自然流畅的叙事风格,适合现代都市、现实题材",
|
||||
"prompt_content": """写作风格要求:
|
||||
1. 语言简洁明快,贴近现代口语
|
||||
2. 多用短句,节奏流畅
|
||||
3. 注重情感细节的自然流露
|
||||
4. 避免过度修饰和复杂句式""",
|
||||
"order_index": 1
|
||||
},
|
||||
{
|
||||
"user_id": None,
|
||||
"name": "古典优雅",
|
||||
"style_type": "preset",
|
||||
"preset_id": "classical",
|
||||
"description": "古典文雅的写作风格,适合古装、仙侠题材",
|
||||
"prompt_content": """写作风格要求:
|
||||
1. 使用文言、半文言或典雅的白话
|
||||
2. 适当运用古典诗词意象
|
||||
3. 注重意境营造和韵味
|
||||
4. 对话和描写保持古典美感""",
|
||||
"order_index": 2
|
||||
},
|
||||
{
|
||||
"user_id": None,
|
||||
"name": "现代简约",
|
||||
"style_type": "preset",
|
||||
"preset_id": "modern",
|
||||
"description": "现代简约风格,适合轻小说、网文快节奏叙事",
|
||||
"prompt_content": """写作风格要求:
|
||||
1. 语言直白简练,信息密度高
|
||||
2. 多用对话推进情节
|
||||
3. 避免冗长描写,突出关键动作
|
||||
4. 节奏明快,适合快速阅读""",
|
||||
"order_index": 3
|
||||
},
|
||||
{
|
||||
"user_id": None,
|
||||
"name": "文艺细腻",
|
||||
"style_type": "preset",
|
||||
"preset_id": "literary",
|
||||
"description": "文艺细腻风格,注重心理描写和氛围营造",
|
||||
"prompt_content": """写作风格要求:
|
||||
1. 注重心理活动和情感细节
|
||||
2. 善用环境描写烘托氛围
|
||||
3. 语言优美,富有文学性
|
||||
4. 适当使用比喻、象征等修辞手法""",
|
||||
"order_index": 4
|
||||
},
|
||||
{
|
||||
"user_id": None,
|
||||
"name": "紧张悬疑",
|
||||
"style_type": "preset",
|
||||
"preset_id": "suspense",
|
||||
"description": "紧张悬疑风格,适合推理、惊悚题材",
|
||||
"prompt_content": """写作风格要求:
|
||||
1. 营造紧张压迫的氛围
|
||||
2. 多用短句加快节奏
|
||||
3. 善于设置悬念和伏笔
|
||||
4. 注重细节描写,为推理埋下线索""",
|
||||
"order_index": 5
|
||||
},
|
||||
{
|
||||
"user_id": None,
|
||||
"name": "幽默诙谐",
|
||||
"style_type": "preset",
|
||||
"preset_id": "humorous",
|
||||
"description": "幽默诙谐风格,适合轻松搞笑题材",
|
||||
"prompt_content": """写作风格要求:
|
||||
1. 语言活泼风趣,善用俏皮话
|
||||
2. 注重对话的喜剧效果
|
||||
3. 适当夸张和反转制造笑点
|
||||
4. 保持轻松愉快的基调""",
|
||||
"order_index": 6
|
||||
},
|
||||
]
|
||||
|
||||
op.bulk_insert(writing_styles_table, writing_styles_data)
|
||||
print(f"✅ 已插入 {len(writing_styles_data)} 条全局写作风格预设")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""删除预置数据"""
|
||||
|
||||
# 删除写作风格预设(只删除全局预设)
|
||||
op.execute("DELETE FROM writing_styles WHERE user_id IS NULL")
|
||||
print("✅ 已删除全局写作风格预设")
|
||||
|
||||
# 删除关系类型
|
||||
op.execute("DELETE FROM relationship_types")
|
||||
print("✅ 已删除关系类型数据")
|
||||
@@ -0,0 +1,2 @@
|
||||
# 此文件确保 versions 目录被 Git 追踪
|
||||
# 迁移版本文件将存放在此目录
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Alembic 环境配置文件 - SQLite"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from alembic import context
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
if project_root not in sys.path:
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
# 导入应用配置
|
||||
from app.config import settings
|
||||
|
||||
# 导入 Base 和所有模型
|
||||
from app.database import Base
|
||||
from app.models import (
|
||||
Project, Outline, Character, Chapter, GenerationHistory,
|
||||
Settings, WritingStyle, ProjectDefaultStyle,
|
||||
RelationshipType, CharacterRelationship, Organization, OrganizationMember,
|
||||
StoryMemory, PlotAnalysis, AnalysisTask, BatchGenerationTask,
|
||||
RegenerationTask, Career, CharacterCareer, User, MCPPlugin, PromptTemplate
|
||||
)
|
||||
|
||||
# Alembic Config 对象
|
||||
config = context.config
|
||||
|
||||
# 设置数据库连接字符串(从环境变量读取)
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
# 配置日志
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 设置 target_metadata 为应用的 Base.metadata
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""在'离线'模式下运行迁移"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
render_as_batch=True, # SQLite 必须启用批处理模式
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
"""执行迁移的核心函数 - SQLite 专用"""
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=target_metadata,
|
||||
compare_type=True,
|
||||
compare_server_default=True,
|
||||
render_as_batch=True, # SQLite 必须启用批处理模式
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations() -> None:
|
||||
"""在'在线'模式下运行异步迁移"""
|
||||
configuration = config.get_section(config.config_ini_section, {})
|
||||
configuration["sqlalchemy.url"] = settings.database_url
|
||||
|
||||
connectable = async_engine_from_config(
|
||||
configuration,
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
"""在'在线'模式下运行迁移"""
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
# 根据上下文选择运行模式
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,616 @@
|
||||
"""初始化SQLite数据库
|
||||
|
||||
Revision ID: fbeb1038c728
|
||||
Revises:
|
||||
Create Date: 2025-12-26 13:22:53.151546
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'fbeb1038c728'
|
||||
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('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('mcp_plugins', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_user_enabled', ['user_id', 'enabled'], unique=False)
|
||||
batch_op.create_index('idx_user_plugin', ['user_id', 'plugin_name'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_mcp_plugins_user_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.CheckConstraint("outline_mode IN ('one-to-one', 'one-to-many')", name='check_outline_mode'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('projects', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_projects_user_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('prompt_templates', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_user_template', ['user_id', 'template_key'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_prompt_templates_user_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('relationship_types', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_relationship_types_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('settings', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_user_id', ['user_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_settings_user_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.PrimaryKeyConstraint('user_id')
|
||||
)
|
||||
with op.batch_alter_table('user_passwords', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_user_passwords_user_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('last_login', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='最后登录时间'),
|
||||
sa.PrimaryKeyConstraint('user_id')
|
||||
)
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_users_linuxdo_id'), ['linuxdo_id'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_users_user_id'), ['user_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_users_username'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('careers', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_project_id', ['project_id'], unique=False)
|
||||
batch_op.create_index('idx_type', ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), 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')
|
||||
)
|
||||
with op.batch_alter_table('analysis_tasks', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_chapter_id_created', ['chapter_id', 'created_at'], unique=False)
|
||||
batch_op.create_index('idx_status', ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.ForeignKeyConstraint(['career_id'], ['careers.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('character_careers', schema=None) as batch_op:
|
||||
batch_op.create_index('idx_career_type', ['career_type'], unique=False)
|
||||
batch_op.create_index('idx_character_career', ['character_id', 'career_id'], unique=True)
|
||||
batch_op.create_index('idx_character_id', ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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')
|
||||
)
|
||||
with op.batch_alter_table('character_relationships', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_character_relationships_character_from_id'), ['character_from_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_character_relationships_character_to_id'), ['character_to_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_character_relationships_project_id'), ['project_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_character_relationships_relationship_type_id'), ['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('(CURRENT_TIMESTAMP)'), 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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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')
|
||||
)
|
||||
with op.batch_alter_table('organizations', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_organizations_project_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='分析时间'),
|
||||
sa.ForeignKeyConstraint(['chapter_id'], ['chapters.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('plot_analysis', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_plot_analysis_chapter_id'), ['chapter_id'], unique=True)
|
||||
batch_op.create_index(batch_op.f('ix_plot_analysis_project_id'), ['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('(CURRENT_TIMESTAMP)'), 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')
|
||||
)
|
||||
with op.batch_alter_table('regeneration_tasks', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_regeneration_tasks_chapter_id'), ['chapter_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_regeneration_tasks_project_id'), ['project_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_regeneration_tasks_user_id'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), 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')
|
||||
)
|
||||
with op.batch_alter_table('story_memories', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_story_memories_chapter_id'), ['chapter_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_story_memories_memory_type'), ['memory_type'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_story_memories_project_id'), ['project_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_story_memories_story_timeline'), ['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('(CURRENT_TIMESTAMP)'), nullable=True, comment='创建时间'),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True, comment='更新时间'),
|
||||
sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('organization_members', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_organization_members_character_id'), ['character_id'], unique=False)
|
||||
batch_op.create_index(batch_op.f('ix_organization_members_organization_id'), ['organization_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('organization_members', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_organization_members_organization_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_organization_members_character_id'))
|
||||
|
||||
op.drop_table('organization_members')
|
||||
with op.batch_alter_table('story_memories', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_story_memories_story_timeline'))
|
||||
batch_op.drop_index(batch_op.f('ix_story_memories_project_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_story_memories_memory_type'))
|
||||
batch_op.drop_index(batch_op.f('ix_story_memories_chapter_id'))
|
||||
|
||||
op.drop_table('story_memories')
|
||||
with op.batch_alter_table('regeneration_tasks', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_regeneration_tasks_user_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_regeneration_tasks_project_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_regeneration_tasks_chapter_id'))
|
||||
|
||||
op.drop_table('regeneration_tasks')
|
||||
with op.batch_alter_table('plot_analysis', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_plot_analysis_project_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_plot_analysis_chapter_id'))
|
||||
|
||||
op.drop_table('plot_analysis')
|
||||
with op.batch_alter_table('organizations', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_organizations_project_id'))
|
||||
|
||||
op.drop_table('organizations')
|
||||
op.drop_table('generation_history')
|
||||
with op.batch_alter_table('character_relationships', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_character_relationships_relationship_type_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_character_relationships_project_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_character_relationships_character_to_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_character_relationships_character_from_id'))
|
||||
|
||||
op.drop_table('character_relationships')
|
||||
with op.batch_alter_table('character_careers', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_character_id')
|
||||
batch_op.drop_index('idx_character_career')
|
||||
batch_op.drop_index('idx_career_type')
|
||||
|
||||
op.drop_table('character_careers')
|
||||
with op.batch_alter_table('analysis_tasks', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_status')
|
||||
batch_op.drop_index('idx_chapter_id_created')
|
||||
|
||||
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')
|
||||
with op.batch_alter_table('careers', schema=None) as batch_op:
|
||||
batch_op.drop_index('idx_type')
|
||||
batch_op.drop_index('idx_project_id')
|
||||
|
||||
op.drop_table('careers')
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_users_username'))
|
||||
batch_op.drop_index(batch_op.f('ix_users_user_id'))
|
||||
batch_op.drop_index(batch_op.f('ix_users_linuxdo_id'))
|
||||
|
||||
op.drop_table('users')
|
||||
with op.batch_alter_table('user_passwords', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_user_passwords_user_id'))
|
||||
|
||||
op.drop_table('user_passwords')
|
||||
with op.batch_alter_table('settings', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_settings_user_id'))
|
||||
batch_op.drop_index('idx_user_id')
|
||||
|
||||
op.drop_table('settings')
|
||||
with op.batch_alter_table('relationship_types', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_relationship_types_id'))
|
||||
|
||||
op.drop_table('relationship_types')
|
||||
with op.batch_alter_table('prompt_templates', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_prompt_templates_user_id'))
|
||||
batch_op.drop_index('idx_user_template')
|
||||
|
||||
op.drop_table('prompt_templates')
|
||||
with op.batch_alter_table('projects', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_projects_user_id'))
|
||||
|
||||
op.drop_table('projects')
|
||||
with op.batch_alter_table('mcp_plugins', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_mcp_plugins_user_id'))
|
||||
batch_op.drop_index('idx_user_plugin')
|
||||
batch_op.drop_index('idx_user_enabled')
|
||||
|
||||
op.drop_table('mcp_plugins')
|
||||
op.drop_table('batch_generation_tasks')
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user