diff --git a/.gitignore b/.gitignore index 78706c7950..c67fc08a01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,41 +1,47 @@ +# 依赖与构建产物 node_modules/ -dist/ -.turbo/ -.DS_Store -.env -.env.local +client/node_modules/ +server/node_modules/ +desktop/node_modules/ +shared/dist/ +server/dist/ +client/dist/ +desktop/dist/ +site/dist/ + +# 数据库(本地运行产生的) dev.db -client/.env -client/.env.local -server/.env -server/.env.local +dev.db-shm +dev.db-wal +*.db-journal server/dev.db server/dev.db-shm server/dev.db-wal -server/tmp_*.js -server/tmp/**/*.db -server/tmp/**/*.db* -server/tmp/**/*.db-* -server/tmp/**/*.sqlite -server/tmp/**/*.sqlite* -server/tmp/**/*.sqlite-* -server/storage/ -server/src/prisma/dev.db -server/src/prisma/dev.db-journal -.history/ -.histyory/ -.logs/ -.tmp/ -.codegraph/ -.trae/ -server/.tmp/ -desktop/build/ -desktop/runtime-migration-smoke/ -server/prisma-diff-migrations-to-schema.sql -# AI tool artifacts -.claude/plan/ -.claude/memory/ -.spec-workflow/ -.codex-backups/ -.cursor/ +# Android 构建产物 +android-app/.gradle/ +android-app/build/ +android-app/app/build/ +android-app/app/.cxx/ +android-app/local.properties +android-app/C:Androidbuild_done.flag +# node 资产(重二进制/可重建产物;保留手写源码 app.js / init-db.cjs) +android-app/app/src/main/assets/nodejs/node-project.zip +android-app/app/src/main/assets/nodejs/bundle.cjs +android-app/app/src/main/assets/nodejs/node_modules/ +android-app/app/src/main/assets/nodejs/dev.db +android-app/app/src/main/assets/nodejs/.extracted +android-app/app/src/main/assets/www/ +android-app/app/src/main/assets/www.zip +android-app/app/src/main/assets/*.zip +*.iml + +# 测试残留 +server/selftest.js +server/test-android.db +server/test-android.db-shm +server/test-android.db-wal + +# 其他 +ARM64/ +android-app/release.keystore diff --git a/README.md b/README.md index 0f37aa876d..d266336dc1 100644 --- a/README.md +++ b/README.md @@ -157,8 +157,49 @@ - 章节批次完成后,后台资源索引等收尾事件不会覆盖主任务的完成状态,页面会继续显示真实进度和章节范围。 - README 提供支付宝捐赠二维码,方便支持项目持续开发与维护。 +### 2026-08-03 + +- 安卓端构建资产全部入库,fork 仓库后按文档两步即可构建出可运行的 APK。 +- 漫画工作台渲染改用 WASM sharp,免原生库依赖,真机渲染稳定,APK 体积降至约 163MB。 +- 修复首次启动模型配置弹窗、安卓网络连接失败等问题;导出文件写入公共下载目录。 +- 手机端统一为桌面布局(横屏固定),操作入口与电脑端一致。 + > 查看完整更新历史:[docs/releases/release-notes.md](./docs/releases/release-notes.md) +## 安卓端构建(fork 后构建必读) + +安卓 APK 需要的全部内嵌资产已**入库**(`node-project.zip` 40MB / `noderuntime/` 48MB / `migrations.sqlite/`), +fork 后 clone 下来即可直接构建,无需从 Release 下载或从 APK 提取: + +```bash +git clone https://github.com/<你的 fork>/AI-Novel-Writing-Assistant.git +cd AI-Novel-Writing-Assistant +pnpm install # 安装依赖(esbuild / vite / prisma 等) +bash android/build-android-assets.sh # 生成 bundle.cjs + www.zip(node-project.zip 为固定资产,跳过重建) +cd android-app +# 首次需创建 local.properties 指向你的 Android SDK:sdk.dir=C:/Android/sdk +gradle clean assembleDebug +``` + +产物:`android-app/app/build/outputs/apk/debug/app-debug.apk`(约 166MB,含内嵌 Node 运行时)。 + +> ⚠️ **每次构建 APK 前都必须重跑 `build-android-assets.sh`**:NodeService 运行时解压的是 +> `www.zip`(不是 `assets/www/` 目录)。只改前端代码/只同步 `www/` 目录而不重新打 `www.zip`, +> 打包进 APK 的仍是旧前端(版本号、功能不更新)。前端、后端代码有任何改动,都要先跑 +> `bash android/build-android-assets.sh` 再 `gradle assembleDebug`。 +> +> 脚本会:① 构建 `@ai-novel/shared`(workspace 包,tsc 生成 dist);② esbuild 完整打包 +> `server/src/app.ts` → `bundle.cjs`(express 等打进包内,仅 sharp/better-sqlite3/@prisma/client +> 走原生加载);③ 打包前端 `www/` → `www.zip`;④ `node-project.zip` 是固定资产(依赖锁定, +> 含安卓 ELF better-sqlite3 + WASM sharp),存在则跳过重建。 +> +> 漫画工作台 sharp 使用 **WASM 版**(@img/sharp-wasm32 + @emnapi/runtime,免原生库): +> 原生 bionic 版依赖 libvips 全家,libstdc++ ABI 在真机不兼容(dlopen 失败),故已弃用。 +> 旧文档见 [android/sharp-android/README.md](./android/sharp-android/README.md)(历史参考)。 +> +> 构建环境需配置:Android SDK(compileSdk 35)、JDK 17、Gradle 8.9(或使用 wrapper)。 +> 构建脚本在 MSYS/Git-Bash / Linux / macOS 下均可运行(路径已兼容)。 + ## 功能预览 ### 功能概览中的95%以上编写都是AI完成 diff --git a/android-app/app/CMakeLists.txt b/android-app/app/CMakeLists.txt new file mode 100644 index 0000000000..556ebffe08 --- /dev/null +++ b/android-app/app/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.4.1) + +add_library(nodejs-mobile-native-lib + SHARED + src/main/cpp/native-lib.cpp + src/main/cpp/rn-bridge.cpp) + +include_directories(src/main/cpp/libnode/include/node/) +include_directories(src/main/cpp/) + +# nodejs-mobile 预编译运行时 +add_library(libnode SHARED IMPORTED) +set_target_properties(libnode PROPERTIES IMPORTED_LOCATION + ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libnode.so) + +find_library(log-lib log) + +target_link_libraries(nodejs-mobile-native-lib + libnode + ${log-lib}) diff --git a/android-app/app/build.gradle b/android-app/app/build.gradle new file mode 100644 index 0000000000..664872b2ff --- /dev/null +++ b/android-app/app/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.ainovel.assistant' + compileSdk 35 + + defaultConfig { + applicationId "com.ainovel.assistant" + minSdk 31 + targetSdk 35 + versionCode 9 + versionName "0.4.9" + ndk { + abiFilters 'arm64-v8a' + } + } + + signingConfigs { + release { + storeFile file('../release.keystore') + storePassword 'ainovel123' + keyAlias 'ainovel' + keyPassword 'ainovel123' + } + } + + buildTypes { + debug { + minifyEnabled false + applicationIdSuffix ".debug" + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.release + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + // 排除 nodejs/node_modules 冗余目录(已在 node-project.zip 内,仅作打包源用; + // 见 build-android-assets.sh 步骤 3。运行时只解压 zip) + sourceSets { + main { + assets { + srcDirs 'src/main/assets' + exclude 'nodejs/node_modules/**' + } + } + } + + aaptOptions { + noCompress 'node', 'js', 'so', 'zip' + } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.webkit:webkit:1.11.0' + implementation 'com.google.android.material:material:1.12.0' +} diff --git a/android-app/app/proguard-rules.pro b/android-app/app/proguard-rules.pro new file mode 100644 index 0000000000..4417bccc11 --- /dev/null +++ b/android-app/app/proguard-rules.pro @@ -0,0 +1,12 @@ +# 保留 nodejs-mobile JNI 绑定类 +-keep class com.nodejs.mobile.** { *; } +-keep class io.nodejs.mobile.** { *; } +-keep class nodejs.** { *; } + +# 保留 better-sqlite3 等 native 模块名 +-keep class better_sqlite3 { *; } + +# WebView / 反射调用不混淆 +-keepattributes *Annotation* +-keep class * extends android.app.Service +-keep class * extends android.app.Activity diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..89556b9a71 --- /dev/null +++ b/android-app/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/assets/nodejs/app.js b/android-app/app/src/main/assets/nodejs/app.js new file mode 100644 index 0000000000..7b053bc055 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/app.js @@ -0,0 +1,14 @@ +// 安卓端入口:设置运行环境后启动打包后的 server bundle +process.env.NODE_PATH = __dirname; +process.env.AI_NOVEL_RUNTIME = process.env.AI_NOVEL_RUNTIME || 'web'; +process.env.AI_NOVEL_DATABASE_MODE = process.env.AI_NOVEL_DATABASE_MODE || 'sqlite'; +process.env.PORT = process.env.PORT || '3000'; +process.env.HOST = process.env.HOST || '127.0.0.1'; +process.env.AI_NOVEL_APP_DATA_DIR = __dirname; + +const server = require('./bundle.cjs'); +// 安卓/web 模式由 NodeService 提前用 init-db.cjs 建表,这里直接启动监听 +server.startServer().catch((error) => { + console.error('[server] bootstrap failed.', error); + process.exit(1); +}); diff --git a/android-app/app/src/main/assets/nodejs/init-db.cjs b/android-app/app/src/main/assets/nodejs/init-db.cjs new file mode 100644 index 0000000000..2357278b4c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/init-db.cjs @@ -0,0 +1,61 @@ +// 安卓专用:用 better-sqlite3 直接执行 Prisma 的 SQL 迁移建表(不依赖 prisma CLI) +const fs = require('node:fs'); +const path = require('node:path'); +const Database = require('better-sqlite3'); + +const dbPath = path.join(__dirname, 'dev.db'); +const migrationsDir = path.join(__dirname, 'migrations.sqlite'); + +const db = new Database(dbPath); +db.pragma('journal_mode = WAL'); +db.pragma('foreign_keys = ON'); + +// 创建 prisma 迁移追踪表(与 prisma 兼容) +db.exec(` +CREATE TABLE IF NOT EXISTS "_prisma_migrations" ( + "id" TEXT PRIMARY KEY NOT NULL, + "checksum" TEXT NOT NULL, + "finished_at" DATETIME, + "migration_name" TEXT NOT NULL, + "logs" TEXT, + "rolled_back_at" DATETIME, + "started_at" DATETIME NOT NULL DEFAULT current_timestamp, + "applied_steps_count" INTEGER UNSIGNED NOT NULL DEFAULT 0 +); +`); + +if (!fs.existsSync(migrationsDir)) { + console.log('[init-db] no migrations dir, skip'); + process.exit(0); +} + +const names = fs.readdirSync(migrationsDir) + .filter((n) => fs.statSync(path.join(migrationsDir, n)).isDirectory()) + .sort(); + +const applied = new Set( + db.prepare(`SELECT migration_name FROM "_prisma_migrations" WHERE finished_at IS NOT NULL`).all().map((r) => r.migration_name) +); + +let count = 0; +for (const name of names) { + if (applied.has(name)) continue; + const sqlPath = path.join(migrationsDir, name, 'migration.sql'); + if (!fs.existsSync(sqlPath)) continue; + const sql = fs.readFileSync(sqlPath, 'utf8'); + const checksum = require('node:crypto').createHash('sha256').update(sql).digest('hex'); + const tx = db.transaction(() => { + db.prepare(`PRAGMA defer_foreign_keys = ON`).run(); + db.exec(sql); + db.prepare( + `INSERT INTO "_prisma_migrations" (id, checksum, migration_name, finished_at, applied_steps_count) + VALUES (?, ?, ?, current_timestamp, 1)` + ).run(require('node:crypto').randomUUID(), checksum, name); + }); + tx(); + count++; + console.log('[init-db] applied', name); +} + +console.log(`[init-db] done, applied ${count} new migration(s)`); +process.exit(0); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260313170000_init/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260313170000_init/migration.sql new file mode 100644 index 0000000000..6c460573e9 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260313170000_init/migration.sql @@ -0,0 +1,934 @@ +-- CreateTable +CREATE TABLE "Novel" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "writingMode" TEXT NOT NULL DEFAULT 'original', + "projectMode" TEXT, + "narrativePov" TEXT, + "pacePreference" TEXT, + "styleTone" TEXT, + "emotionIntensity" TEXT, + "aiFreedom" TEXT, + "defaultChapterLength" INTEGER, + "projectStatus" TEXT DEFAULT 'not_started', + "storylineStatus" TEXT DEFAULT 'not_started', + "outlineStatus" TEXT DEFAULT 'not_started', + "resourceReadyScore" INTEGER, + "sourceNovelId" TEXT, + "sourceKnowledgeDocumentId" TEXT, + "continuationBookAnalysisId" TEXT, + "continuationBookAnalysisSections" TEXT, + "outline" TEXT, + "structuredOutline" TEXT, + "genreId" TEXT, + "worldId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Novel_genreId_fkey" FOREIGN KEY ("genreId") REFERENCES "NovelGenre" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Novel_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Novel_sourceNovelId_fkey" FOREIGN KEY ("sourceNovelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Novel_sourceKnowledgeDocumentId_fkey" FOREIGN KEY ("sourceKnowledgeDocumentId") REFERENCES "KnowledgeDocument" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "Novel_continuationBookAnalysisId_fkey" FOREIGN KEY ("continuationBookAnalysisId") REFERENCES "BookAnalysis" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CreativeDecision" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "category" TEXT NOT NULL, + "content" TEXT NOT NULL, + "importance" TEXT NOT NULL DEFAULT 'normal', + "expiresAt" INTEGER, + "sourceType" TEXT, + "sourceRefId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CreativeDecision_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "NovelSnapshot" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "label" TEXT, + "snapshotData" TEXT NOT NULL, + "triggerType" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "NovelSnapshot_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Chapter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "content" TEXT DEFAULT '', + "order" INTEGER NOT NULL, + "generationState" TEXT NOT NULL DEFAULT 'planned', + "chapterStatus" TEXT DEFAULT 'unplanned', + "targetWordCount" INTEGER, + "conflictLevel" INTEGER, + "revealLevel" INTEGER, + "mustAvoid" TEXT, + "taskSheet" TEXT, + "sceneCards" TEXT, + "repairHistory" TEXT, + "qualityScore" INTEGER, + "continuityScore" INTEGER, + "characterScore" INTEGER, + "pacingScore" INTEGER, + "riskFlags" TEXT, + "hook" TEXT, + "expectation" TEXT, + "novelId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Chapter_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Character" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL, + "personality" TEXT, + "background" TEXT, + "development" TEXT, + "currentState" TEXT, + "currentGoal" TEXT, + "lastEvolvedAt" DATETIME, + "novelId" TEXT NOT NULL, + "baseCharacterId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Character_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CharacterTimeline" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "chapterId" TEXT, + "chapterOrder" INTEGER, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "source" TEXT NOT NULL DEFAULT 'auto', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterTimeline_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterTimeline_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterTimeline_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "BaseCharacter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL, + "personality" TEXT NOT NULL, + "background" TEXT NOT NULL, + "development" TEXT NOT NULL, + "appearance" TEXT, + "weaknesses" TEXT, + "interests" TEXT, + "keyEvents" TEXT, + "tags" TEXT NOT NULL DEFAULT '', + "category" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "ImageGenerationTask" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sceneType" TEXT NOT NULL DEFAULT 'character', + "baseCharacterId" TEXT, + "provider" TEXT NOT NULL, + "model" TEXT NOT NULL, + "prompt" TEXT NOT NULL, + "negativePrompt" TEXT, + "stylePreset" TEXT, + "size" TEXT NOT NULL DEFAULT '1024x1024', + "imageCount" INTEGER NOT NULL DEFAULT 1, + "seed" INTEGER, + "status" TEXT NOT NULL DEFAULT 'queued', + "progress" REAL NOT NULL DEFAULT 0, + "retryCount" INTEGER NOT NULL DEFAULT 0, + "maxRetries" INTEGER NOT NULL DEFAULT 2, + "heartbeatAt" DATETIME, + "currentStage" TEXT, + "currentItemKey" TEXT, + "currentItemLabel" TEXT, + "cancelRequestedAt" DATETIME, + "error" TEXT, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ImageGenerationTask_baseCharacterId_fkey" FOREIGN KEY ("baseCharacterId") REFERENCES "BaseCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ImageAsset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskId" TEXT NOT NULL, + "sceneType" TEXT NOT NULL DEFAULT 'character', + "baseCharacterId" TEXT, + "provider" TEXT NOT NULL, + "model" TEXT NOT NULL, + "url" TEXT NOT NULL, + "mimeType" TEXT, + "width" INTEGER, + "height" INTEGER, + "seed" INTEGER, + "prompt" TEXT, + "isPrimary" BOOLEAN NOT NULL DEFAULT false, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "metadata" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ImageAsset_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "ImageGenerationTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ImageAsset_baseCharacterId_fkey" FOREIGN KEY ("baseCharacterId") REFERENCES "BaseCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "NovelGenre" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "template" TEXT, + "parentId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "NovelGenre_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "NovelGenre" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "World" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "worldType" TEXT, + "templateKey" TEXT, + "axioms" TEXT, + "background" TEXT, + "geography" TEXT, + "cultures" TEXT, + "magicSystem" TEXT, + "politics" TEXT, + "races" TEXT, + "religions" TEXT, + "technology" TEXT, + "conflicts" TEXT, + "history" TEXT, + "economy" TEXT, + "factions" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "version" INTEGER NOT NULL DEFAULT 1, + "selectedDimensions" TEXT, + "selectedElements" TEXT, + "layerStates" TEXT, + "consistencyReport" TEXT, + "overviewSummary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "WorldPropertyLibrary" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "category" TEXT NOT NULL, + "worldType" TEXT, + "usageCount" INTEGER NOT NULL DEFAULT 0, + "sourceWorldId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "WorldPropertyLibrary_sourceWorldId_fkey" FOREIGN KEY ("sourceWorldId") REFERENCES "World" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "WorldSnapshot" ( + "id" TEXT NOT NULL PRIMARY KEY, + "worldId" TEXT NOT NULL, + "label" TEXT, + "data" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "WorldSnapshot_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "WorldDeepeningQA" ( + "id" TEXT NOT NULL PRIMARY KEY, + "worldId" TEXT NOT NULL, + "priority" TEXT NOT NULL DEFAULT 'recommended', + "question" TEXT NOT NULL, + "targetLayer" TEXT, + "targetField" TEXT, + "answer" TEXT, + "integratedSummary" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "WorldDeepeningQA_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "WorldConsistencyIssue" ( + "id" TEXT NOT NULL PRIMARY KEY, + "worldId" TEXT NOT NULL, + "severity" TEXT NOT NULL, + "code" TEXT NOT NULL, + "message" TEXT NOT NULL, + "detail" TEXT, + "source" TEXT NOT NULL DEFAULT 'rule', + "status" TEXT NOT NULL DEFAULT 'open', + "targetField" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "WorldConsistencyIssue_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "WritingFormula" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "sourceText" TEXT, + "content" TEXT, + "genre" TEXT, + "style" TEXT, + "toneVoice" TEXT, + "structure" TEXT, + "pacing" TEXT, + "paragraphPattern" TEXT, + "sentenceStructure" TEXT, + "vocabularyLevel" TEXT, + "rhetoricalDevices" TEXT, + "narrativeMode" TEXT, + "perspectivePoint" TEXT, + "characterVoice" TEXT, + "themes" TEXT, + "motifs" TEXT, + "emotionalTone" TEXT, + "uniqueFeatures" TEXT, + "formulaDescription" TEXT, + "formulaSteps" TEXT, + "applicationTips" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "TitleLibrary" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "description" TEXT, + "clickRate" REAL, + "keywords" TEXT, + "genreId" TEXT, + "usedCount" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "APIKey" ( + "id" TEXT NOT NULL PRIMARY KEY, + "provider" TEXT NOT NULL, + "key" TEXT NOT NULL, + "model" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "AppSetting" ( + "key" TEXT NOT NULL PRIMARY KEY, + "value" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "ModelRouteConfig" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskType" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "model" TEXT NOT NULL, + "temperature" REAL NOT NULL DEFAULT 0.7, + "maxTokens" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "NovelBible" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "coreSetting" TEXT, + "forbiddenRules" TEXT, + "mainPromise" TEXT, + "characterArcs" TEXT, + "worldRules" TEXT, + "rawContent" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "NovelBible_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "PlotBeat" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterOrder" INTEGER, + "beatType" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'planned', + "metadata" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "PlotBeat_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ChapterSummary" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "keyEvents" TEXT, + "characterStates" TEXT, + "hook" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ChapterSummary_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ChapterSummary_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "ConsistencyFact" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "category" TEXT NOT NULL DEFAULT 'plot', + "content" TEXT NOT NULL, + "source" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ConsistencyFact_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ConsistencyFact_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "GenerationJob" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "startOrder" INTEGER NOT NULL, + "endOrder" INTEGER NOT NULL, + "runMode" TEXT DEFAULT 'fast', + "autoReview" BOOLEAN NOT NULL DEFAULT true, + "autoRepair" BOOLEAN NOT NULL DEFAULT true, + "skipCompleted" BOOLEAN NOT NULL DEFAULT true, + "qualityThreshold" INTEGER, + "repairMode" TEXT DEFAULT 'light_repair', + "status" TEXT NOT NULL DEFAULT 'queued', + "progress" REAL NOT NULL DEFAULT 0, + "completedCount" INTEGER NOT NULL DEFAULT 0, + "totalCount" INTEGER NOT NULL DEFAULT 0, + "retryCount" INTEGER NOT NULL DEFAULT 0, + "maxRetries" INTEGER NOT NULL DEFAULT 2, + "heartbeatAt" DATETIME, + "currentStage" TEXT, + "currentItemKey" TEXT, + "currentItemLabel" TEXT, + "cancelRequestedAt" DATETIME, + "error" TEXT, + "lastErrorType" TEXT, + "payload" TEXT, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "GenerationJob_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AgentRun" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT, + "chapterId" TEXT, + "sessionId" TEXT NOT NULL, + "goal" TEXT NOT NULL, + "entryAgent" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'queued', + "currentStep" TEXT, + "currentAgent" TEXT, + "error" TEXT, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "metadataJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "AgentRun_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AgentStep" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runId" TEXT NOT NULL, + "seq" INTEGER NOT NULL, + "agentName" TEXT NOT NULL, + "stepType" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'succeeded', + "parentStepId" TEXT, + "idempotencyKey" TEXT, + "inputJson" TEXT, + "outputJson" TEXT, + "error" TEXT, + "errorCode" TEXT, + "provider" TEXT, + "model" TEXT, + "tokenUsageJson" TEXT, + "costUsd" REAL, + "durationMs" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "AgentStep_runId_fkey" FOREIGN KEY ("runId") REFERENCES "AgentRun" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "AgentApproval" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runId" TEXT NOT NULL, + "stepId" TEXT, + "approvalType" TEXT NOT NULL, + "targetType" TEXT NOT NULL, + "targetId" TEXT NOT NULL, + "diffSummary" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "expiresAt" DATETIME, + "decisionNote" TEXT, + "decider" TEXT, + "decidedAt" DATETIME, + "payloadJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "AgentApproval_runId_fkey" FOREIGN KEY ("runId") REFERENCES "AgentRun" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "AgentApproval_stepId_fkey" FOREIGN KEY ("stepId") REFERENCES "AgentStep" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CreativeHubThread" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL DEFAULT '新对话', + "archived" BOOLEAN NOT NULL DEFAULT false, + "status" TEXT NOT NULL DEFAULT 'idle', + "latestRunId" TEXT, + "latestError" TEXT, + "resourceBindingsJson" TEXT, + "metadataJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "CreativeHubCheckpoint" ( + "id" TEXT NOT NULL PRIMARY KEY, + "threadId" TEXT NOT NULL, + "checkpointId" TEXT NOT NULL, + "parentCheckpointId" TEXT, + "runId" TEXT, + "messageCount" INTEGER NOT NULL DEFAULT 0, + "preview" TEXT, + "messagesJson" TEXT NOT NULL, + "interruptsJson" TEXT, + "metadataJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "CreativeHubCheckpoint_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "CreativeHubThread" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "StorylineVersion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "content" TEXT NOT NULL, + "diffSummary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StorylineVersion_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "QualityReport" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "coherence" INTEGER NOT NULL, + "repetition" INTEGER NOT NULL, + "pacing" INTEGER NOT NULL, + "voice" INTEGER NOT NULL, + "engagement" INTEGER NOT NULL, + "overall" INTEGER NOT NULL, + "issues" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "QualityReport_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "QualityReport_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "KnowledgeDocument" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'enabled', + "activeVersionId" TEXT, + "activeVersionNumber" INTEGER NOT NULL DEFAULT 0, + "latestIndexStatus" TEXT NOT NULL DEFAULT 'idle', + "lastIndexedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "KnowledgeDocument_activeVersionId_fkey" FOREIGN KEY ("activeVersionId") REFERENCES "KnowledgeDocumentVersion" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "KnowledgeDocumentVersion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "documentId" TEXT NOT NULL, + "versionNumber" INTEGER NOT NULL, + "content" TEXT NOT NULL, + "contentHash" TEXT NOT NULL, + "charCount" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "KnowledgeDocumentVersion_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "KnowledgeDocument" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "KnowledgeBinding" ( + "id" TEXT NOT NULL PRIMARY KEY, + "targetType" TEXT NOT NULL, + "targetId" TEXT NOT NULL, + "documentId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "KnowledgeBinding_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "KnowledgeDocument" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "BookAnalysis" ( + "id" TEXT NOT NULL PRIMARY KEY, + "documentId" TEXT NOT NULL, + "documentVersionId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'queued', + "summary" TEXT, + "provider" TEXT, + "model" TEXT, + "temperature" REAL, + "maxTokens" INTEGER, + "progress" REAL NOT NULL DEFAULT 0, + "heartbeatAt" DATETIME, + "currentStage" TEXT, + "currentItemKey" TEXT, + "currentItemLabel" TEXT, + "cancelRequestedAt" DATETIME, + "attemptCount" INTEGER NOT NULL DEFAULT 0, + "maxAttempts" INTEGER NOT NULL DEFAULT 1, + "lastError" TEXT, + "lastRunAt" DATETIME, + "publishedDocumentId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysis_documentId_fkey" FOREIGN KEY ("documentId") REFERENCES "KnowledgeDocument" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "BookAnalysis_documentVersionId_fkey" FOREIGN KEY ("documentVersionId") REFERENCES "KnowledgeDocumentVersion" ("id") ON DELETE NO ACTION ON UPDATE CASCADE, + CONSTRAINT "BookAnalysis_publishedDocumentId_fkey" FOREIGN KEY ("publishedDocumentId") REFERENCES "KnowledgeDocument" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "BookAnalysisSection" ( + "id" TEXT NOT NULL PRIMARY KEY, + "analysisId" TEXT NOT NULL, + "sectionKey" TEXT NOT NULL, + "title" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'idle', + "aiContent" TEXT, + "editedContent" TEXT, + "notes" TEXT, + "structuredDataJson" TEXT, + "evidenceJson" TEXT, + "frozen" BOOLEAN NOT NULL DEFAULT false, + "sortOrder" INTEGER NOT NULL, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysisSection_analysisId_fkey" FOREIGN KEY ("analysisId") REFERENCES "BookAnalysis" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "KnowledgeChunk" ( + "id" TEXT NOT NULL PRIMARY KEY, + "tenantId" TEXT NOT NULL DEFAULT 'default', + "ownerType" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "novelId" TEXT, + "worldId" TEXT, + "title" TEXT, + "chunkText" TEXT NOT NULL, + "chunkHash" TEXT NOT NULL, + "chunkOrder" INTEGER NOT NULL, + "tokenEstimate" INTEGER NOT NULL DEFAULT 0, + "language" TEXT NOT NULL DEFAULT 'zh', + "metadataJson" TEXT, + "embedProvider" TEXT NOT NULL, + "embedModel" TEXT NOT NULL, + "embedVersion" INTEGER NOT NULL DEFAULT 1, + "indexedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "RagIndexJob" ( + "id" TEXT NOT NULL PRIMARY KEY, + "tenantId" TEXT NOT NULL DEFAULT 'default', + "jobType" TEXT NOT NULL, + "ownerType" TEXT NOT NULL, + "ownerId" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'queued', + "attempts" INTEGER NOT NULL DEFAULT 0, + "maxAttempts" INTEGER NOT NULL DEFAULT 5, + "runAfter" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "payloadJson" TEXT, + "lastError" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateIndex +CREATE INDEX "Novel_genreId_idx" ON "Novel"("genreId"); + +-- CreateIndex +CREATE INDEX "Novel_worldId_idx" ON "Novel"("worldId"); + +-- CreateIndex +CREATE INDEX "Novel_writingMode_idx" ON "Novel"("writingMode"); + +-- CreateIndex +CREATE INDEX "Novel_sourceNovelId_idx" ON "Novel"("sourceNovelId"); + +-- CreateIndex +CREATE INDEX "Novel_sourceKnowledgeDocumentId_idx" ON "Novel"("sourceKnowledgeDocumentId"); + +-- CreateIndex +CREATE INDEX "Novel_continuationBookAnalysisId_idx" ON "Novel"("continuationBookAnalysisId"); + +-- CreateIndex +CREATE INDEX "CreativeDecision_novelId_createdAt_idx" ON "CreativeDecision"("novelId", "createdAt"); + +-- CreateIndex +CREATE INDEX "NovelSnapshot_novelId_createdAt_idx" ON "NovelSnapshot"("novelId", "createdAt"); + +-- CreateIndex +CREATE INDEX "Chapter_novelId_order_idx" ON "Chapter"("novelId", "order"); + +-- CreateIndex +CREATE INDEX "Character_novelId_idx" ON "Character"("novelId"); + +-- CreateIndex +CREATE INDEX "Character_baseCharacterId_idx" ON "Character"("baseCharacterId"); + +-- CreateIndex +CREATE INDEX "CharacterTimeline_novelId_characterId_idx" ON "CharacterTimeline"("novelId", "characterId"); + +-- CreateIndex +CREATE INDEX "CharacterTimeline_characterId_chapterOrder_idx" ON "CharacterTimeline"("characterId", "chapterOrder"); + +-- CreateIndex +CREATE INDEX "CharacterTimeline_chapterId_idx" ON "CharacterTimeline"("chapterId"); + +-- CreateIndex +CREATE INDEX "ImageGenerationTask_sceneType_status_idx" ON "ImageGenerationTask"("sceneType", "status"); + +-- CreateIndex +CREATE INDEX "ImageGenerationTask_baseCharacterId_createdAt_idx" ON "ImageGenerationTask"("baseCharacterId", "createdAt"); + +-- CreateIndex +CREATE INDEX "ImageAsset_taskId_idx" ON "ImageAsset"("taskId"); + +-- CreateIndex +CREATE INDEX "ImageAsset_sceneType_createdAt_idx" ON "ImageAsset"("sceneType", "createdAt"); + +-- CreateIndex +CREATE INDEX "ImageAsset_baseCharacterId_isPrimary_createdAt_idx" ON "ImageAsset"("baseCharacterId", "isPrimary", "createdAt"); + +-- CreateIndex +CREATE INDEX "NovelGenre_parentId_idx" ON "NovelGenre"("parentId"); + +-- CreateIndex +CREATE INDEX "WorldPropertyLibrary_sourceWorldId_idx" ON "WorldPropertyLibrary"("sourceWorldId"); + +-- CreateIndex +CREATE INDEX "WorldSnapshot_worldId_createdAt_idx" ON "WorldSnapshot"("worldId", "createdAt"); + +-- CreateIndex +CREATE INDEX "WorldDeepeningQA_worldId_status_idx" ON "WorldDeepeningQA"("worldId", "status"); + +-- CreateIndex +CREATE INDEX "WorldConsistencyIssue_worldId_status_idx" ON "WorldConsistencyIssue"("worldId", "status"); + +-- CreateIndex +CREATE INDEX "WorldConsistencyIssue_worldId_severity_idx" ON "WorldConsistencyIssue"("worldId", "severity"); + +-- CreateIndex +CREATE UNIQUE INDEX "APIKey_provider_key" ON "APIKey"("provider"); + +-- CreateIndex +CREATE UNIQUE INDEX "ModelRouteConfig_taskType_key" ON "ModelRouteConfig"("taskType"); + +-- CreateIndex +CREATE UNIQUE INDEX "NovelBible_novelId_key" ON "NovelBible"("novelId"); + +-- CreateIndex +CREATE INDEX "PlotBeat_novelId_idx" ON "PlotBeat"("novelId"); + +-- CreateIndex +CREATE INDEX "PlotBeat_novelId_chapterOrder_idx" ON "PlotBeat"("novelId", "chapterOrder"); + +-- CreateIndex +CREATE UNIQUE INDEX "ChapterSummary_chapterId_key" ON "ChapterSummary"("chapterId"); + +-- CreateIndex +CREATE INDEX "ChapterSummary_novelId_idx" ON "ChapterSummary"("novelId"); + +-- CreateIndex +CREATE INDEX "ConsistencyFact_novelId_idx" ON "ConsistencyFact"("novelId"); + +-- CreateIndex +CREATE INDEX "ConsistencyFact_chapterId_idx" ON "ConsistencyFact"("chapterId"); + +-- CreateIndex +CREATE INDEX "ConsistencyFact_novelId_category_idx" ON "ConsistencyFact"("novelId", "category"); + +-- CreateIndex +CREATE INDEX "GenerationJob_novelId_idx" ON "GenerationJob"("novelId"); + +-- CreateIndex +CREATE INDEX "GenerationJob_novelId_status_idx" ON "GenerationJob"("novelId", "status"); + +-- CreateIndex +CREATE INDEX "AgentRun_status_updatedAt_idx" ON "AgentRun"("status", "updatedAt"); + +-- CreateIndex +CREATE INDEX "AgentRun_novelId_createdAt_idx" ON "AgentRun"("novelId", "createdAt"); + +-- CreateIndex +CREATE INDEX "AgentRun_novelId_chapterId_createdAt_idx" ON "AgentRun"("novelId", "chapterId", "createdAt"); + +-- CreateIndex +CREATE INDEX "AgentRun_sessionId_createdAt_idx" ON "AgentRun"("sessionId", "createdAt"); + +-- CreateIndex +CREATE INDEX "AgentStep_runId_idempotencyKey_idx" ON "AgentStep"("runId", "idempotencyKey"); + +-- CreateIndex +CREATE INDEX "AgentStep_runId_parentStepId_idx" ON "AgentStep"("runId", "parentStepId"); + +-- CreateIndex +CREATE UNIQUE INDEX "AgentStep_runId_seq_key" ON "AgentStep"("runId", "seq"); + +-- CreateIndex +CREATE INDEX "AgentApproval_runId_status_idx" ON "AgentApproval"("runId", "status"); + +-- CreateIndex +CREATE INDEX "AgentApproval_stepId_idx" ON "AgentApproval"("stepId"); + +-- CreateIndex +CREATE INDEX "AgentApproval_status_expiresAt_idx" ON "AgentApproval"("status", "expiresAt"); + +-- CreateIndex +CREATE INDEX "CreativeHubThread_archived_updatedAt_idx" ON "CreativeHubThread"("archived", "updatedAt"); + +-- CreateIndex +CREATE INDEX "CreativeHubThread_status_updatedAt_idx" ON "CreativeHubThread"("status", "updatedAt"); + +-- CreateIndex +CREATE INDEX "CreativeHubCheckpoint_threadId_createdAt_idx" ON "CreativeHubCheckpoint"("threadId", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "CreativeHubCheckpoint_threadId_checkpointId_key" ON "CreativeHubCheckpoint"("threadId", "checkpointId"); + +-- CreateIndex +CREATE INDEX "StorylineVersion_novelId_status_createdAt_idx" ON "StorylineVersion"("novelId", "status", "createdAt"); + +-- CreateIndex +CREATE UNIQUE INDEX "StorylineVersion_novelId_version_key" ON "StorylineVersion"("novelId", "version"); + +-- CreateIndex +CREATE INDEX "QualityReport_novelId_idx" ON "QualityReport"("novelId"); + +-- CreateIndex +CREATE INDEX "QualityReport_chapterId_idx" ON "QualityReport"("chapterId"); + +-- CreateIndex +CREATE INDEX "QualityReport_novelId_createdAt_idx" ON "QualityReport"("novelId", "createdAt"); + +-- CreateIndex +CREATE INDEX "KnowledgeDocument_status_updatedAt_idx" ON "KnowledgeDocument"("status", "updatedAt"); + +-- CreateIndex +CREATE INDEX "KnowledgeDocument_title_idx" ON "KnowledgeDocument"("title"); + +-- CreateIndex +CREATE INDEX "KnowledgeDocumentVersion_documentId_createdAt_idx" ON "KnowledgeDocumentVersion"("documentId", "createdAt"); + +-- CreateIndex +CREATE INDEX "KnowledgeDocumentVersion_contentHash_idx" ON "KnowledgeDocumentVersion"("contentHash"); + +-- CreateIndex +CREATE UNIQUE INDEX "KnowledgeDocumentVersion_documentId_versionNumber_key" ON "KnowledgeDocumentVersion"("documentId", "versionNumber"); + +-- CreateIndex +CREATE INDEX "KnowledgeBinding_targetType_targetId_idx" ON "KnowledgeBinding"("targetType", "targetId"); + +-- CreateIndex +CREATE INDEX "KnowledgeBinding_documentId_idx" ON "KnowledgeBinding"("documentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "KnowledgeBinding_targetType_targetId_documentId_key" ON "KnowledgeBinding"("targetType", "targetId", "documentId"); + +-- CreateIndex +CREATE INDEX "BookAnalysis_documentId_status_idx" ON "BookAnalysis"("documentId", "status"); + +-- CreateIndex +CREATE INDEX "BookAnalysis_documentVersionId_idx" ON "BookAnalysis"("documentVersionId"); + +-- CreateIndex +CREATE INDEX "BookAnalysis_status_updatedAt_idx" ON "BookAnalysis"("status", "updatedAt"); + +-- CreateIndex +CREATE INDEX "BookAnalysisSection_analysisId_sortOrder_idx" ON "BookAnalysisSection"("analysisId", "sortOrder"); + +-- CreateIndex +CREATE UNIQUE INDEX "BookAnalysisSection_analysisId_sectionKey_key" ON "BookAnalysisSection"("analysisId", "sectionKey"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_tenantId_ownerType_ownerId_idx" ON "KnowledgeChunk"("tenantId", "ownerType", "ownerId"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_tenantId_novelId_idx" ON "KnowledgeChunk"("tenantId", "novelId"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_tenantId_worldId_idx" ON "KnowledgeChunk"("tenantId", "worldId"); + +-- CreateIndex +CREATE INDEX "KnowledgeChunk_chunkHash_idx" ON "KnowledgeChunk"("chunkHash"); + +-- CreateIndex +CREATE INDEX "RagIndexJob_status_runAfter_idx" ON "RagIndexJob"("status", "runAfter"); + +-- CreateIndex +CREATE INDEX "RagIndexJob_tenantId_ownerType_ownerId_idx" ON "RagIndexJob"("tenantId", "ownerType", "ownerId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260318233000_book_analysis_source_cache/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260318233000_book_analysis_source_cache/migration.sql new file mode 100644 index 0000000000..66bcb3d7c0 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260318233000_book_analysis_source_cache/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "BookAnalysisSourceCache" ( + "id" TEXT NOT NULL PRIMARY KEY, + "documentVersionId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "model" TEXT NOT NULL, + "temperature" REAL NOT NULL, + "notesMaxTokens" INTEGER NOT NULL, + "segmentVersion" INTEGER NOT NULL DEFAULT 1, + "segmentCount" INTEGER NOT NULL, + "notesJson" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysisSourceCache_documentVersionId_fkey" FOREIGN KEY ("documentVersionId") REFERENCES "KnowledgeDocumentVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "BookAnalysisSourceCache_documentVersionId_provider_model_temperature_notesMaxTokens_segmentVersion_key" +ON "BookAnalysisSourceCache"("documentVersionId", "provider", "model", "temperature", "notesMaxTokens", "segmentVersion"); + +-- CreateIndex +CREATE INDEX "BookAnalysisSourceCache_documentVersionId_updatedAt_idx" +ON "BookAnalysisSourceCache"("documentVersionId", "updatedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319093000_task_center_archive/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319093000_task_center_archive/migration.sql new file mode 100644 index 0000000000..cd81855422 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319093000_task_center_archive/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "TaskCenterArchive" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskKind" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + "archivedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- CreateIndex +CREATE UNIQUE INDEX "TaskCenterArchive_taskKind_taskId_key" +ON "TaskCenterArchive"("taskKind", "taskId"); + +-- CreateIndex +CREATE INDEX "TaskCenterArchive_taskKind_archivedAt_idx" +ON "TaskCenterArchive"("taskKind", "archivedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319113000_novel_estimated_chapter_count/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319113000_novel_estimated_chapter_count/migration.sql new file mode 100644 index 0000000000..c08df2d4ff --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319113000_novel_estimated_chapter_count/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Novel" +ADD COLUMN "estimatedChapterCount" INTEGER; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319160000_world_structure_mvp/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319160000_world_structure_mvp/migration.sql new file mode 100644 index 0000000000..1256aff2ad --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260319160000_world_structure_mvp/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "World" ADD COLUMN "structureJson" TEXT; +ALTER TABLE "World" ADD COLUMN "bindingSupportJson" TEXT; +ALTER TABLE "World" ADD COLUMN "structureSchemaVersion" INTEGER NOT NULL DEFAULT 1; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260320113000_story_world_slice_cache/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260320113000_story_world_slice_cache/migration.sql new file mode 100644 index 0000000000..652ef18b4c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260320113000_story_world_slice_cache/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "Novel" ADD COLUMN "storyWorldSliceJson" TEXT; +ALTER TABLE "Novel" ADD COLUMN "storyWorldSliceOverridesJson" TEXT; +ALTER TABLE "Novel" ADD COLUMN "storyWorldSliceSchemaVersion" INTEGER NOT NULL DEFAULT 1; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260322173000_style_profile_extracted_features/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260322173000_style_profile_extracted_features/migration.sql new file mode 100644 index 0000000000..8cb0b88ce6 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260322173000_style_profile_extracted_features/migration.sql @@ -0,0 +1,106 @@ +CREATE TABLE "StyleProfile" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "category" TEXT, + "tagsJson" TEXT, + "applicableGenresJson" TEXT, + "sourceType" TEXT NOT NULL, + "sourceRefId" TEXT, + "sourceContent" TEXT, + "extractedFeaturesJson" TEXT, + "analysisMarkdown" TEXT, + "narrativeRulesJson" TEXT, + "characterRulesJson" TEXT, + "languageRulesJson" TEXT, + "rhythmRulesJson" TEXT, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE INDEX "StyleProfile_status_updatedAt_idx" +ON "StyleProfile"("status", "updatedAt"); + +CREATE INDEX "StyleProfile_sourceType_sourceRefId_idx" +ON "StyleProfile"("sourceType", "sourceRefId"); + +CREATE TABLE "StyleTemplate" ( + "id" TEXT NOT NULL PRIMARY KEY, + "key" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "category" TEXT NOT NULL, + "tagsJson" TEXT, + "applicableGenresJson" TEXT, + "analysisMarkdown" TEXT, + "narrativeRulesJson" TEXT, + "characterRulesJson" TEXT, + "languageRulesJson" TEXT, + "rhythmRulesJson" TEXT, + "defaultAntiAiRuleKeysJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE UNIQUE INDEX "StyleTemplate_key_key" +ON "StyleTemplate"("key"); + +CREATE TABLE "AntiAiRule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "key" TEXT NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "severity" TEXT NOT NULL, + "description" TEXT NOT NULL, + "detectPatternsJson" TEXT, + "rewriteSuggestion" TEXT, + "promptInstruction" TEXT, + "autoRewrite" BOOLEAN NOT NULL DEFAULT false, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE UNIQUE INDEX "AntiAiRule_key_key" +ON "AntiAiRule"("key"); + +CREATE INDEX "AntiAiRule_type_enabled_idx" +ON "AntiAiRule"("type", "enabled"); + +CREATE TABLE "StyleProfileAntiAiRule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "styleProfileId" TEXT NOT NULL, + "antiAiRuleId" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "weight" REAL NOT NULL DEFAULT 1, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StyleProfileAntiAiRule_styleProfileId_fkey" FOREIGN KEY ("styleProfileId") REFERENCES "StyleProfile" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "StyleProfileAntiAiRule_antiAiRuleId_fkey" FOREIGN KEY ("antiAiRuleId") REFERENCES "AntiAiRule" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "StyleProfileAntiAiRule_styleProfileId_antiAiRuleId_key" +ON "StyleProfileAntiAiRule"("styleProfileId", "antiAiRuleId"); + +CREATE INDEX "StyleProfileAntiAiRule_antiAiRuleId_idx" +ON "StyleProfileAntiAiRule"("antiAiRuleId"); + +CREATE TABLE "StyleBinding" ( + "id" TEXT NOT NULL PRIMARY KEY, + "styleProfileId" TEXT NOT NULL, + "targetType" TEXT NOT NULL, + "targetId" TEXT NOT NULL, + "priority" INTEGER NOT NULL DEFAULT 1, + "weight" REAL NOT NULL DEFAULT 1, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StyleBinding_styleProfileId_fkey" FOREIGN KEY ("styleProfileId") REFERENCES "StyleProfile" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "StyleBinding_targetType_targetId_enabled_idx" +ON "StyleBinding"("targetType", "targetId", "enabled"); + +CREATE INDEX "StyleBinding_styleProfileId_idx" +ON "StyleBinding"("styleProfileId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260324121500_novel_book_framing_fields/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260324121500_novel_book_framing_fields/migration.sql new file mode 100644 index 0000000000..c25f4c1842 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260324121500_novel_book_framing_fields/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE "Novel" ADD COLUMN "targetAudience" TEXT; +ALTER TABLE "Novel" ADD COLUMN "bookSellingPoint" TEXT; +ALTER TABLE "Novel" ADD COLUMN "competingFeel" TEXT; +ALTER TABLE "Novel" ADD COLUMN "first30ChapterPromise" TEXT; +ALTER TABLE "Novel" ADD COLUMN "commercialTagsJson" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260325103000_volume_plan_workbench/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260325103000_volume_plan_workbench/migration.sql new file mode 100644 index 0000000000..fabf2792a4 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260325103000_volume_plan_workbench/migration.sql @@ -0,0 +1,60 @@ +CREATE TABLE "VolumePlanVersion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "contentJson" TEXT NOT NULL, + "diffSummary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "VolumePlanVersion_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "VolumePlanVersion_novelId_version_key" ON "VolumePlanVersion"("novelId", "version"); +CREATE INDEX "VolumePlanVersion_novelId_status_createdAt_idx" ON "VolumePlanVersion"("novelId", "status", "createdAt"); + +CREATE TABLE "VolumePlan" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT, + "mainPromise" TEXT, + "escalationMode" TEXT, + "protagonistChange" TEXT, + "climax" TEXT, + "nextVolumeHook" TEXT, + "resetPoint" TEXT, + "openPayoffsJson" TEXT, + "status" TEXT NOT NULL DEFAULT 'active', + "sourceVersionId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "VolumePlan_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "VolumePlan_sourceVersionId_fkey" FOREIGN KEY ("sourceVersionId") REFERENCES "VolumePlanVersion" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "VolumePlan_novelId_sortOrder_key" ON "VolumePlan"("novelId", "sortOrder"); +CREATE INDEX "VolumePlan_novelId_status_sortOrder_idx" ON "VolumePlan"("novelId", "status", "sortOrder"); +CREATE INDEX "VolumePlan_sourceVersionId_idx" ON "VolumePlan"("sourceVersionId"); + +CREATE TABLE "VolumeChapterPlan" ( + "id" TEXT NOT NULL PRIMARY KEY, + "volumeId" TEXT NOT NULL, + "chapterOrder" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "purpose" TEXT, + "conflictLevel" INTEGER, + "revealLevel" INTEGER, + "targetWordCount" INTEGER, + "mustAvoid" TEXT, + "taskSheet" TEXT, + "payoffRefsJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "VolumeChapterPlan_volumeId_fkey" FOREIGN KEY ("volumeId") REFERENCES "VolumePlan" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "VolumeChapterPlan_volumeId_chapterOrder_key" ON "VolumeChapterPlan"("volumeId", "chapterOrder"); +CREATE INDEX "VolumeChapterPlan_volumeId_chapterOrder_idx" ON "VolumeChapterPlan"("volumeId", "chapterOrder"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260326110000_story_mode_management/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260326110000_story_mode_management/migration.sql new file mode 100644 index 0000000000..e8ff572908 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260326110000_story_mode_management/migration.sql @@ -0,0 +1,19 @@ +CREATE TABLE "NovelStoryMode" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "description" TEXT, + "template" TEXT, + "profileJson" TEXT NOT NULL, + "parentId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "NovelStoryMode_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "NovelStoryMode" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX "NovelStoryMode_parentId_idx" ON "NovelStoryMode"("parentId"); + +ALTER TABLE "Novel" ADD COLUMN "primaryStoryModeId" TEXT; +ALTER TABLE "Novel" ADD COLUMN "secondaryStoryModeId" TEXT; + +CREATE INDEX "Novel_primaryStoryModeId_idx" ON "Novel"("primaryStoryModeId"); +CREATE INDEX "Novel_secondaryStoryModeId_idx" ON "Novel"("secondaryStoryModeId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260328120000_schema_gap_backfill/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260328120000_schema_gap_backfill/migration.sql new file mode 100644 index 0000000000..77d0de3702 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260328120000_schema_gap_backfill/migration.sql @@ -0,0 +1,474 @@ +CREATE TABLE IF NOT EXISTS "CharacterRelation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "sourceCharacterId" TEXT NOT NULL, + "targetCharacterId" TEXT NOT NULL, + "surfaceRelation" TEXT NOT NULL, + "hiddenTension" TEXT, + "conflictSource" TEXT, + "secretAsymmetry" TEXT, + "dynamicLabel" TEXT, + "nextTurnPoint" TEXT, + "trustScore" INTEGER, + "conflictScore" INTEGER, + "intimacyScore" INTEGER, + "dependencyScore" INTEGER, + "evidence" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterRelation_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterRelation_sourceCharacterId_fkey" FOREIGN KEY ("sourceCharacterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterRelation_targetCharacterId_fkey" FOREIGN KEY ("targetCharacterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterCastOption" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "whyItWorks" TEXT, + "recommendedReason" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "sourceStoryInput" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterCastOption_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterCastOptionMember" ( + "id" TEXT NOT NULL PRIMARY KEY, + "optionId" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL, + "castRole" TEXT NOT NULL, + "relationToProtagonist" TEXT, + "storyFunction" TEXT NOT NULL, + "shortDescription" TEXT, + "outerGoal" TEXT, + "innerNeed" TEXT, + "fear" TEXT, + "wound" TEXT, + "misbelief" TEXT, + "secret" TEXT, + "moralLine" TEXT, + "firstImpression" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterCastOptionMember_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CharacterCastOption" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterCastOptionRelation" ( + "id" TEXT NOT NULL PRIMARY KEY, + "optionId" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL, + "sourceName" TEXT NOT NULL, + "targetName" TEXT NOT NULL, + "surfaceRelation" TEXT NOT NULL, + "hiddenTension" TEXT, + "conflictSource" TEXT, + "secretAsymmetry" TEXT, + "dynamicLabel" TEXT, + "nextTurnPoint" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterCastOptionRelation_optionId_fkey" FOREIGN KEY ("optionId") REFERENCES "CharacterCastOption" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterCandidate" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "sourceChapterId" TEXT, + "proposedName" TEXT NOT NULL, + "proposedRole" TEXT, + "summary" TEXT, + "evidenceJson" TEXT, + "matchedCharacterId" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "confidence" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterCandidate_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterCandidate_sourceChapterId_fkey" FOREIGN KEY ("sourceChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterCandidate_matchedCharacterId_fkey" FOREIGN KEY ("matchedCharacterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterVolumeAssignment" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "volumeId" TEXT NOT NULL, + "roleLabel" TEXT, + "responsibility" TEXT NOT NULL, + "appearanceExpectation" TEXT, + "plannedChapterOrdersJson" TEXT, + "isCore" BOOLEAN NOT NULL DEFAULT false, + "absenceWarningThreshold" INTEGER NOT NULL DEFAULT 3, + "absenceHighRiskThreshold" INTEGER NOT NULL DEFAULT 5, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterVolumeAssignment_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterVolumeAssignment_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterVolumeAssignment_volumeId_fkey" FOREIGN KEY ("volumeId") REFERENCES "VolumePlan" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterFactionTrack" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "volumeId" TEXT, + "chapterId" TEXT, + "chapterOrder" INTEGER, + "factionLabel" TEXT NOT NULL, + "stanceLabel" TEXT, + "summary" TEXT, + "sourceType" TEXT NOT NULL, + "confidence" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterFactionTrack_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterFactionTrack_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterFactionTrack_volumeId_fkey" FOREIGN KEY ("volumeId") REFERENCES "VolumePlan" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterFactionTrack_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterRelationStage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "relationId" TEXT, + "sourceCharacterId" TEXT NOT NULL, + "targetCharacterId" TEXT NOT NULL, + "volumeId" TEXT, + "chapterId" TEXT, + "chapterOrder" INTEGER, + "stageLabel" TEXT NOT NULL, + "stageSummary" TEXT NOT NULL, + "nextTurnPoint" TEXT, + "sourceType" TEXT NOT NULL, + "confidence" REAL, + "isCurrent" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterRelationStage_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterRelationStage_relationId_fkey" FOREIGN KEY ("relationId") REFERENCES "CharacterRelation" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterRelationStage_sourceCharacterId_fkey" FOREIGN KEY ("sourceCharacterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterRelationStage_targetCharacterId_fkey" FOREIGN KEY ("targetCharacterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterRelationStage_volumeId_fkey" FOREIGN KEY ("volumeId") REFERENCES "VolumePlan" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterRelationStage_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "StoryMacroPlan" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "storyInput" TEXT, + "expansionJson" TEXT, + "decompositionJson" TEXT, + "issuesJson" TEXT, + "lockedFieldsJson" TEXT, + "constraintEngineJson" TEXT, + "stateJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StoryMacroPlan_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "BookContract" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "readingPromise" TEXT NOT NULL, + "protagonistFantasy" TEXT NOT NULL, + "coreSellingPoint" TEXT NOT NULL, + "chapter3Payoff" TEXT NOT NULL, + "chapter10Payoff" TEXT NOT NULL, + "chapter30Payoff" TEXT NOT NULL, + "escalationLadder" TEXT NOT NULL, + "relationshipMainline" TEXT NOT NULL, + "absoluteRedLinesJson" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookContract_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "NovelWorkflowTask" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT, + "lane" TEXT NOT NULL, + "title" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'queued', + "progress" REAL NOT NULL DEFAULT 0, + "currentStage" TEXT, + "currentItemKey" TEXT, + "currentItemLabel" TEXT, + "checkpointType" TEXT, + "checkpointSummary" TEXT, + "resumeTargetJson" TEXT, + "seedPayloadJson" TEXT, + "milestonesJson" TEXT, + "pendingManualRecovery" BOOLEAN NOT NULL DEFAULT false, + "heartbeatAt" DATETIME, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "cancelRequestedAt" DATETIME, + "attemptCount" INTEGER NOT NULL DEFAULT 0, + "maxAttempts" INTEGER NOT NULL DEFAULT 3, + "lastError" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "NovelWorkflowTask_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "StoryStateSnapshot" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "sourceChapterId" TEXT, + "summary" TEXT, + "rawStateJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StoryStateSnapshot_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "StoryStateSnapshot_sourceChapterId_fkey" FOREIGN KEY ("sourceChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterState" ( + "id" TEXT NOT NULL PRIMARY KEY, + "snapshotId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "currentGoal" TEXT, + "emotion" TEXT, + "stressLevel" INTEGER, + "secretExposure" TEXT, + "knownFactsJson" TEXT, + "misbeliefsJson" TEXT, + "summary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterState_snapshotId_fkey" FOREIGN KEY ("snapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterState_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "RelationState" ( + "id" TEXT NOT NULL PRIMARY KEY, + "snapshotId" TEXT NOT NULL, + "sourceCharacterId" TEXT NOT NULL, + "targetCharacterId" TEXT NOT NULL, + "trustScore" INTEGER, + "intimacyScore" INTEGER, + "conflictScore" INTEGER, + "dependencyScore" INTEGER, + "summary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "RelationState_snapshotId_fkey" FOREIGN KEY ("snapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "RelationState_sourceCharacterId_fkey" FOREIGN KEY ("sourceCharacterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "RelationState_targetCharacterId_fkey" FOREIGN KEY ("targetCharacterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "InformationState" ( + "id" TEXT NOT NULL PRIMARY KEY, + "snapshotId" TEXT NOT NULL, + "holderType" TEXT NOT NULL, + "holderRefId" TEXT, + "fact" TEXT NOT NULL, + "status" TEXT NOT NULL, + "summary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "InformationState_snapshotId_fkey" FOREIGN KEY ("snapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "ForeshadowState" ( + "id" TEXT NOT NULL PRIMARY KEY, + "snapshotId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT, + "status" TEXT NOT NULL, + "setupChapterId" TEXT, + "payoffChapterId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ForeshadowState_snapshotId_fkey" FOREIGN KEY ("snapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ForeshadowState_setupChapterId_fkey" FOREIGN KEY ("setupChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "ForeshadowState_payoffChapterId_fkey" FOREIGN KEY ("payoffChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "OpenConflict" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "sourceSnapshotId" TEXT, + "sourceIssueId" TEXT, + "sourceType" TEXT NOT NULL, + "conflictType" TEXT NOT NULL, + "conflictKey" TEXT NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "severity" TEXT NOT NULL DEFAULT 'medium', + "status" TEXT NOT NULL DEFAULT 'open', + "evidenceJson" TEXT, + "affectedCharacterIdsJson" TEXT, + "resolutionHint" TEXT, + "lastSeenChapterOrder" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "OpenConflict_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "OpenConflict_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "OpenConflict_sourceSnapshotId_fkey" FOREIGN KEY ("sourceSnapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "StoryPlan" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "parentId" TEXT, + "sourceStateSnapshotId" TEXT, + "level" TEXT NOT NULL, + "planRole" TEXT, + "phaseLabel" TEXT, + "title" TEXT NOT NULL, + "objective" TEXT NOT NULL, + "participantsJson" TEXT, + "revealsJson" TEXT, + "riskNotesJson" TEXT, + "mustAdvanceJson" TEXT, + "mustPreserveJson" TEXT, + "sourceIssueIdsJson" TEXT, + "replannedFromPlanId" TEXT, + "hookTarget" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "externalRef" TEXT, + "rawPlanJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StoryPlan_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "StoryPlan_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "StoryPlan_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "StoryPlan" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "StoryPlan_sourceStateSnapshotId_fkey" FOREIGN KEY ("sourceStateSnapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "ChapterPlanScene" ( + "id" TEXT NOT NULL PRIMARY KEY, + "planId" TEXT NOT NULL, + "sortOrder" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "objective" TEXT, + "conflict" TEXT, + "reveal" TEXT, + "emotionBeat" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ChapterPlanScene_planId_fkey" FOREIGN KEY ("planId") REFERENCES "StoryPlan" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "ReplanRun" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "sourcePlanId" TEXT, + "triggerType" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "outputSummary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ReplanRun_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ReplanRun_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "ReplanRun_sourcePlanId_fkey" FOREIGN KEY ("sourcePlanId") REFERENCES "StoryPlan" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "AuditReport" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT NOT NULL, + "auditType" TEXT NOT NULL, + "overallScore" INTEGER, + "summary" TEXT, + "legacyScoreJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "AuditReport_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "AuditReport_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "AuditIssue" ( + "id" TEXT NOT NULL PRIMARY KEY, + "reportId" TEXT NOT NULL, + "auditType" TEXT NOT NULL, + "severity" TEXT NOT NULL, + "code" TEXT NOT NULL, + "description" TEXT NOT NULL, + "evidence" TEXT NOT NULL, + "fixSuggestion" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'open', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "AuditIssue_reportId_fkey" FOREIGN KEY ("reportId") REFERENCES "AuditReport" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX IF NOT EXISTS "CharacterRelation_novelId_updatedAt_idx" ON "CharacterRelation"("novelId", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterRelation_sourceCharacterId_idx" ON "CharacterRelation"("sourceCharacterId"); +CREATE INDEX IF NOT EXISTS "CharacterRelation_targetCharacterId_idx" ON "CharacterRelation"("targetCharacterId"); +CREATE UNIQUE INDEX IF NOT EXISTS "CharacterRelation_novelId_sourceCharacterId_targetCharacterId_key" ON "CharacterRelation"("novelId", "sourceCharacterId", "targetCharacterId"); + +CREATE INDEX IF NOT EXISTS "CharacterCastOption_novelId_updatedAt_idx" ON "CharacterCastOption"("novelId", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterCastOptionMember_optionId_sortOrder_idx" ON "CharacterCastOptionMember"("optionId", "sortOrder"); +CREATE INDEX IF NOT EXISTS "CharacterCastOptionRelation_optionId_sortOrder_idx" ON "CharacterCastOptionRelation"("optionId", "sortOrder"); + +CREATE INDEX IF NOT EXISTS "CharacterCandidate_novelId_status_updatedAt_idx" ON "CharacterCandidate"("novelId", "status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterCandidate_sourceChapterId_idx" ON "CharacterCandidate"("sourceChapterId"); +CREATE INDEX IF NOT EXISTS "CharacterCandidate_matchedCharacterId_idx" ON "CharacterCandidate"("matchedCharacterId"); + +CREATE INDEX IF NOT EXISTS "CharacterVolumeAssignment_novelId_volumeId_isCore_idx" ON "CharacterVolumeAssignment"("novelId", "volumeId", "isCore"); +CREATE INDEX IF NOT EXISTS "CharacterVolumeAssignment_volumeId_characterId_idx" ON "CharacterVolumeAssignment"("volumeId", "characterId"); +CREATE UNIQUE INDEX IF NOT EXISTS "CharacterVolumeAssignment_characterId_volumeId_key" ON "CharacterVolumeAssignment"("characterId", "volumeId"); + +CREATE INDEX IF NOT EXISTS "CharacterFactionTrack_novelId_characterId_createdAt_idx" ON "CharacterFactionTrack"("novelId", "characterId", "createdAt"); +CREATE INDEX IF NOT EXISTS "CharacterFactionTrack_volumeId_characterId_createdAt_idx" ON "CharacterFactionTrack"("volumeId", "characterId", "createdAt"); +CREATE INDEX IF NOT EXISTS "CharacterFactionTrack_chapterId_createdAt_idx" ON "CharacterFactionTrack"("chapterId", "createdAt"); + +CREATE INDEX IF NOT EXISTS "CharacterRelationStage_novelId_isCurrent_updatedAt_idx" ON "CharacterRelationStage"("novelId", "isCurrent", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterRelationStage_sourceCharacterId_targetCharacterId_isCurrent_idx" ON "CharacterRelationStage"("sourceCharacterId", "targetCharacterId", "isCurrent"); +CREATE INDEX IF NOT EXISTS "CharacterRelationStage_relationId_idx" ON "CharacterRelationStage"("relationId"); +CREATE INDEX IF NOT EXISTS "CharacterRelationStage_chapterId_idx" ON "CharacterRelationStage"("chapterId"); + +CREATE UNIQUE INDEX IF NOT EXISTS "StoryMacroPlan_novelId_key" ON "StoryMacroPlan"("novelId"); +CREATE UNIQUE INDEX IF NOT EXISTS "BookContract_novelId_key" ON "BookContract"("novelId"); + +CREATE INDEX IF NOT EXISTS "NovelWorkflowTask_novelId_status_updatedAt_idx" ON "NovelWorkflowTask"("novelId", "status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "NovelWorkflowTask_status_updatedAt_idx" ON "NovelWorkflowTask"("status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "NovelWorkflowTask_lane_updatedAt_idx" ON "NovelWorkflowTask"("lane", "updatedAt"); + +CREATE INDEX IF NOT EXISTS "StoryStateSnapshot_novelId_createdAt_idx" ON "StoryStateSnapshot"("novelId", "createdAt"); +CREATE INDEX IF NOT EXISTS "StoryStateSnapshot_sourceChapterId_idx" ON "StoryStateSnapshot"("sourceChapterId"); +CREATE UNIQUE INDEX IF NOT EXISTS "StoryStateSnapshot_novelId_sourceChapterId_key" ON "StoryStateSnapshot"("novelId", "sourceChapterId"); + +CREATE INDEX IF NOT EXISTS "CharacterState_characterId_createdAt_idx" ON "CharacterState"("characterId", "createdAt"); +CREATE UNIQUE INDEX IF NOT EXISTS "CharacterState_snapshotId_characterId_key" ON "CharacterState"("snapshotId", "characterId"); + +CREATE INDEX IF NOT EXISTS "RelationState_sourceCharacterId_targetCharacterId_idx" ON "RelationState"("sourceCharacterId", "targetCharacterId"); +CREATE UNIQUE INDEX IF NOT EXISTS "RelationState_snapshotId_sourceCharacterId_targetCharacterId_key" ON "RelationState"("snapshotId", "sourceCharacterId", "targetCharacterId"); + +CREATE INDEX IF NOT EXISTS "InformationState_snapshotId_holderType_idx" ON "InformationState"("snapshotId", "holderType"); + +CREATE INDEX IF NOT EXISTS "ForeshadowState_snapshotId_status_idx" ON "ForeshadowState"("snapshotId", "status"); +CREATE INDEX IF NOT EXISTS "ForeshadowState_setupChapterId_idx" ON "ForeshadowState"("setupChapterId"); +CREATE INDEX IF NOT EXISTS "ForeshadowState_payoffChapterId_idx" ON "ForeshadowState"("payoffChapterId"); + +CREATE INDEX IF NOT EXISTS "OpenConflict_novelId_status_updatedAt_idx" ON "OpenConflict"("novelId", "status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "OpenConflict_chapterId_status_idx" ON "OpenConflict"("chapterId", "status"); +CREATE INDEX IF NOT EXISTS "OpenConflict_sourceSnapshotId_idx" ON "OpenConflict"("sourceSnapshotId"); +CREATE INDEX IF NOT EXISTS "OpenConflict_sourceIssueId_idx" ON "OpenConflict"("sourceIssueId"); +CREATE UNIQUE INDEX IF NOT EXISTS "OpenConflict_novelId_chapterId_sourceType_conflictKey_key" ON "OpenConflict"("novelId", "chapterId", "sourceType", "conflictKey"); + +CREATE INDEX IF NOT EXISTS "StoryPlan_novelId_level_createdAt_idx" ON "StoryPlan"("novelId", "level", "createdAt"); +CREATE INDEX IF NOT EXISTS "StoryPlan_chapterId_createdAt_idx" ON "StoryPlan"("chapterId", "createdAt"); +CREATE INDEX IF NOT EXISTS "StoryPlan_externalRef_idx" ON "StoryPlan"("externalRef"); +CREATE INDEX IF NOT EXISTS "StoryPlan_sourceStateSnapshotId_idx" ON "StoryPlan"("sourceStateSnapshotId"); + +CREATE INDEX IF NOT EXISTS "ChapterPlanScene_planId_sortOrder_idx" ON "ChapterPlanScene"("planId", "sortOrder"); + +CREATE INDEX IF NOT EXISTS "ReplanRun_novelId_createdAt_idx" ON "ReplanRun"("novelId", "createdAt"); +CREATE INDEX IF NOT EXISTS "ReplanRun_chapterId_createdAt_idx" ON "ReplanRun"("chapterId", "createdAt"); + +CREATE INDEX IF NOT EXISTS "AuditReport_novelId_chapterId_auditType_createdAt_idx" ON "AuditReport"("novelId", "chapterId", "auditType", "createdAt"); +CREATE INDEX IF NOT EXISTS "AuditReport_chapterId_createdAt_idx" ON "AuditReport"("chapterId", "createdAt"); + +CREATE INDEX IF NOT EXISTS "AuditIssue_reportId_status_idx" ON "AuditIssue"("reportId", "status"); +CREATE INDEX IF NOT EXISTS "AuditIssue_auditType_severity_idx" ON "AuditIssue"("auditType", "severity"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402120000_character_gender_fields/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402120000_character_gender_fields/migration.sql new file mode 100644 index 0000000000..ccbda42876 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402120000_character_gender_fields/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "Character" ADD COLUMN "gender" TEXT NOT NULL DEFAULT 'unknown'; + +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "gender" TEXT NOT NULL DEFAULT 'unknown'; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402153000_provider_base_url_and_optional_key/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402153000_provider_base_url_and_optional_key/migration.sql new file mode 100644 index 0000000000..ff7a1ea237 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402153000_provider_base_url_and_optional_key/migration.sql @@ -0,0 +1,25 @@ +PRAGMA defer_foreign_keys=ON; +PRAGMA foreign_keys=OFF; + +CREATE TABLE "new_APIKey" ( + "id" TEXT NOT NULL PRIMARY KEY, + "provider" TEXT NOT NULL, + "key" TEXT, + "model" TEXT, + "baseURL" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +INSERT INTO "new_APIKey" ("createdAt", "id", "isActive", "key", "model", "provider", "updatedAt") +SELECT "createdAt", "id", "isActive", "key", "model", "provider", "updatedAt" +FROM "APIKey"; + +DROP TABLE "APIKey"; +ALTER TABLE "new_APIKey" RENAME TO "APIKey"; + +CREATE UNIQUE INDEX "APIKey_provider_key" ON "APIKey"("provider"); + +PRAGMA foreign_keys=ON; +PRAGMA defer_foreign_keys=OFF; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402170000_custom_provider_display_name/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402170000_custom_provider_display_name/migration.sql new file mode 100644 index 0000000000..6ac6fd5714 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260402170000_custom_provider_display_name/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "APIKey" ADD COLUMN "displayName" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260405113000_payoff_ledger/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260405113000_payoff_ledger/migration.sql new file mode 100644 index 0000000000..2747b328c6 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260405113000_payoff_ledger/migration.sql @@ -0,0 +1,37 @@ +CREATE TABLE "PayoffLedgerItem" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "ledgerKey" TEXT NOT NULL, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "scopeType" TEXT NOT NULL, + "currentStatus" TEXT NOT NULL, + "targetStartChapterOrder" INTEGER, + "targetEndChapterOrder" INTEGER, + "firstSeenChapterOrder" INTEGER, + "lastTouchedChapterOrder" INTEGER, + "lastTouchedChapterId" TEXT, + "setupChapterId" TEXT, + "payoffChapterId" TEXT, + "lastSnapshotId" TEXT, + "sourceRefsJson" TEXT, + "evidenceJson" TEXT, + "riskSignalsJson" TEXT, + "statusReason" TEXT, + "confidence" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "PayoffLedgerItem_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "PayoffLedgerItem_lastTouchedChapterId_fkey" FOREIGN KEY ("lastTouchedChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "PayoffLedgerItem_setupChapterId_fkey" FOREIGN KEY ("setupChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "PayoffLedgerItem_payoffChapterId_fkey" FOREIGN KEY ("payoffChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "PayoffLedgerItem_lastSnapshotId_fkey" FOREIGN KEY ("lastSnapshotId") REFERENCES "StoryStateSnapshot" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "PayoffLedgerItem_novelId_ledgerKey_key" ON "PayoffLedgerItem"("novelId", "ledgerKey"); +CREATE INDEX "PayoffLedgerItem_novelId_currentStatus_updatedAt_idx" ON "PayoffLedgerItem"("novelId", "currentStatus", "updatedAt"); +CREATE INDEX "PayoffLedgerItem_novelId_targetEndChapterOrder_idx" ON "PayoffLedgerItem"("novelId", "targetEndChapterOrder"); +CREATE INDEX "PayoffLedgerItem_lastTouchedChapterId_idx" ON "PayoffLedgerItem"("lastTouchedChapterId"); +CREATE INDEX "PayoffLedgerItem_setupChapterId_idx" ON "PayoffLedgerItem"("setupChapterId"); +CREATE INDEX "PayoffLedgerItem_payoffChapterId_idx" ON "PayoffLedgerItem"("payoffChapterId"); +CREATE INDEX "PayoffLedgerItem_lastSnapshotId_idx" ON "PayoffLedgerItem"("lastSnapshotId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260405203000_task_token_usage/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260405203000_task_token_usage/migration.sql new file mode 100644 index 0000000000..4db71afbe6 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260405203000_task_token_usage/migration.sql @@ -0,0 +1,11 @@ +ALTER TABLE "GenerationJob" ADD COLUMN "promptTokens" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "GenerationJob" ADD COLUMN "completionTokens" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "GenerationJob" ADD COLUMN "totalTokens" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "GenerationJob" ADD COLUMN "llmCallCount" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "GenerationJob" ADD COLUMN "lastTokenRecordedAt" DATETIME; + +ALTER TABLE "NovelWorkflowTask" ADD COLUMN "promptTokens" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "NovelWorkflowTask" ADD COLUMN "completionTokens" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "NovelWorkflowTask" ADD COLUMN "totalTokens" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "NovelWorkflowTask" ADD COLUMN "llmCallCount" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "NovelWorkflowTask" ADD COLUMN "lastTokenRecordedAt" DATETIME; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260408103000_provider_reasoning_enabled/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260408103000_provider_reasoning_enabled/migration.sql new file mode 100644 index 0000000000..8f315053f0 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260408103000_provider_reasoning_enabled/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "APIKey" ADD COLUMN "reasoningEnabled" BOOLEAN NOT NULL DEFAULT true; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260413153000_volume_chapter_scene_cards/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260413153000_volume_chapter_scene_cards/migration.sql new file mode 100644 index 0000000000..408bbd21a3 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260413153000_volume_chapter_scene_cards/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "VolumeChapterPlan" +ADD COLUMN "sceneCards" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260416190000_canonical_state_p0/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260416190000_canonical_state_p0/migration.sql new file mode 100644 index 0000000000..ff1726b977 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260416190000_canonical_state_p0/migration.sql @@ -0,0 +1,44 @@ +CREATE TABLE "CanonicalStateVersion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "sourceType" TEXT NOT NULL, + "sourceStage" TEXT, + "version" INTEGER NOT NULL, + "summary" TEXT NOT NULL, + "snapshotJson" TEXT NOT NULL, + "acceptedProposalIdsJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "CanonicalStateVersion_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CanonicalStateVersion_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "StateChangeProposal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "sourceSnapshotId" TEXT, + "sourceType" TEXT NOT NULL, + "sourceStage" TEXT, + "proposalType" TEXT NOT NULL, + "riskLevel" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'validated', + "summary" TEXT NOT NULL, + "payloadJson" TEXT NOT NULL, + "evidenceJson" TEXT, + "validationNotesJson" TEXT, + "committedVersionId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "StateChangeProposal_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "StateChangeProposal_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "StateChangeProposal_committedVersionId_fkey" FOREIGN KEY ("committedVersionId") REFERENCES "CanonicalStateVersion" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "CanonicalStateVersion_novelId_version_key" ON "CanonicalStateVersion"("novelId", "version"); +CREATE INDEX "CanonicalStateVersion_novelId_createdAt_idx" ON "CanonicalStateVersion"("novelId", "createdAt"); +CREATE INDEX "CanonicalStateVersion_chapterId_idx" ON "CanonicalStateVersion"("chapterId"); +CREATE INDEX "StateChangeProposal_novelId_createdAt_idx" ON "StateChangeProposal"("novelId", "createdAt"); +CREATE INDEX "StateChangeProposal_chapterId_createdAt_idx" ON "StateChangeProposal"("chapterId", "createdAt"); +CREATE INDEX "StateChangeProposal_status_riskLevel_createdAt_idx" ON "StateChangeProposal"("status", "riskLevel", "createdAt"); +CREATE INDEX "StateChangeProposal_committedVersionId_idx" ON "StateChangeProposal"("committedVersionId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260419123000_schema_column_backfill/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260419123000_schema_column_backfill/migration.sql new file mode 100644 index 0000000000..4ddac34d86 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260419123000_schema_column_backfill/migration.sql @@ -0,0 +1,19 @@ +ALTER TABLE "Character" ADD COLUMN "arcClimax" TEXT; +ALTER TABLE "Character" ADD COLUMN "arcEnd" TEXT; +ALTER TABLE "Character" ADD COLUMN "arcMidpoint" TEXT; +ALTER TABLE "Character" ADD COLUMN "arcStart" TEXT; +ALTER TABLE "Character" ADD COLUMN "castRole" TEXT; +ALTER TABLE "Character" ADD COLUMN "fear" TEXT; +ALTER TABLE "Character" ADD COLUMN "firstImpression" TEXT; +ALTER TABLE "Character" ADD COLUMN "innerNeed" TEXT; +ALTER TABLE "Character" ADD COLUMN "misbelief" TEXT; +ALTER TABLE "Character" ADD COLUMN "moralLine" TEXT; +ALTER TABLE "Character" ADD COLUMN "outerGoal" TEXT; +ALTER TABLE "Character" ADD COLUMN "relationToProtagonist" TEXT; +ALTER TABLE "Character" ADD COLUMN "secret" TEXT; +ALTER TABLE "Character" ADD COLUMN "storyFunction" TEXT; +ALTER TABLE "Character" ADD COLUMN "wound" TEXT; + +ALTER TABLE "BookAnalysis" ADD COLUMN "pendingManualRecovery" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "GenerationJob" ADD COLUMN "pendingManualRecovery" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "ImageGenerationTask" ADD COLUMN "pendingManualRecovery" BOOLEAN NOT NULL DEFAULT false; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421183000_style_extraction_task/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421183000_style_extraction_task/migration.sql new file mode 100644 index 0000000000..7e9af06792 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421183000_style_extraction_task/migration.sql @@ -0,0 +1,34 @@ +CREATE TABLE "StyleExtractionTask" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "category" TEXT, + "sourceText" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "model" TEXT, + "temperature" REAL, + "presetKey" TEXT NOT NULL DEFAULT 'balanced', + "status" TEXT NOT NULL DEFAULT 'queued', + "progress" REAL NOT NULL DEFAULT 0, + "retryCount" INTEGER NOT NULL DEFAULT 0, + "maxRetries" INTEGER NOT NULL DEFAULT 1, + "pendingManualRecovery" BOOLEAN NOT NULL DEFAULT false, + "heartbeatAt" DATETIME, + "currentStage" TEXT, + "currentItemKey" TEXT, + "currentItemLabel" TEXT, + "cancelRequestedAt" DATETIME, + "error" TEXT, + "summary" TEXT, + "createdStyleProfileId" TEXT, + "createdStyleProfileName" TEXT, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE INDEX "StyleExtractionTask_status_updatedAt_idx" +ON "StyleExtractionTask"("status", "updatedAt"); + +CREATE INDEX "StyleExtractionTask_createdStyleProfileId_idx" +ON "StyleExtractionTask"("createdStyleProfileId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421202000_style_extraction_task_usage/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421202000_style_extraction_task_usage/migration.sql new file mode 100644 index 0000000000..f64cf79cc7 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421202000_style_extraction_task_usage/migration.sql @@ -0,0 +1,14 @@ +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "promptTokens" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "completionTokens" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "totalTokens" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "llmCallCount" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "lastTokenRecordedAt" DATETIME; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421224500_style_profile_extraction_metadata/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421224500_style_profile_extraction_metadata/migration.sql new file mode 100644 index 0000000000..a6d0471c4a --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260421224500_style_profile_extraction_metadata/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "StyleProfile" ADD COLUMN "extractionPresetsJson" TEXT; +ALTER TABLE "StyleProfile" ADD COLUMN "extractionAntiAiRuleKeysJson" TEXT; +ALTER TABLE "StyleProfile" ADD COLUMN "selectedExtractionPresetKey" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260422113000_auto_director_follow_up_action_logs/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260422113000_auto_director_follow_up_action_logs/migration.sql new file mode 100644 index 0000000000..f296a8b7ee --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260422113000_auto_director_follow_up_action_logs/migration.sql @@ -0,0 +1,22 @@ +CREATE TABLE "AutoDirectorFollowUpActionLog" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskId" TEXT NOT NULL, + "actionCode" TEXT NOT NULL, + "sourceChannel" TEXT NOT NULL, + "sourceUser" TEXT, + "idempotencyKey" TEXT NOT NULL, + "resultCode" TEXT NOT NULL, + "failureReason" TEXT, + "metadataJson" TEXT, + "executedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX "AutoDirectorFollowUpActionLog_idempotencyKey_key" +ON "AutoDirectorFollowUpActionLog"("idempotencyKey"); + +CREATE INDEX "AutoDirectorFollowUpActionLog_taskId_executedAt_idx" +ON "AutoDirectorFollowUpActionLog"("taskId", "executedAt"); + +CREATE INDEX "AutoDirectorFollowUpActionLog_taskId_actionCode_executedAt_idx" +ON "AutoDirectorFollowUpActionLog"("taskId", "actionCode", "executedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260422164500_auto_director_follow_up_notification_logs/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260422164500_auto_director_follow_up_notification_logs/migration.sql new file mode 100644 index 0000000000..536283df3d --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260422164500_auto_director_follow_up_notification_logs/migration.sql @@ -0,0 +1,22 @@ +CREATE TABLE "AutoDirectorFollowUpNotificationLog" ( + "id" TEXT NOT NULL PRIMARY KEY, + "eventId" TEXT NOT NULL, + "eventType" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + "channelType" TEXT NOT NULL, + "target" TEXT, + "requestPayload" TEXT, + "responseBody" TEXT, + "responseStatus" INTEGER, + "attemptCount" INTEGER NOT NULL DEFAULT 1, + "deliveredAt" DATETIME, + "status" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE INDEX "AutoDirectorFollowUpNotificationLog_taskId_createdAt_idx" +ON "AutoDirectorFollowUpNotificationLog"("taskId", "createdAt"); + +CREATE INDEX "AutoDirectorFollowUpNotificationLog_eventId_channelType_createdAt_idx" +ON "AutoDirectorFollowUpNotificationLog"("eventId", "channelType", "createdAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260423143000_style_extraction_task_source/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260423143000_style_extraction_task_source/migration.sql new file mode 100644 index 0000000000..8e35012a68 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260423143000_style_extraction_task_source/migration.sql @@ -0,0 +1,20 @@ +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "sourceType" TEXT NOT NULL DEFAULT 'from_text'; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "sourceRefId" TEXT; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "sourceProcessingMode" TEXT NOT NULL DEFAULT 'full_text'; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "sourceInputText" TEXT; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "sourceInputCharLimit" INTEGER; + +ALTER TABLE "StyleExtractionTask" +ADD COLUMN "sourceInputCharCount" INTEGER; + +CREATE INDEX "StyleExtractionTask_sourceType_sourceRefId_idx" +ON "StyleExtractionTask"("sourceType", "sourceRefId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260424223000_character_resource_ledger/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260424223000_character_resource_ledger/migration.sql new file mode 100644 index 0000000000..7c8f42f213 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260424223000_character_resource_ledger/migration.sql @@ -0,0 +1,72 @@ +CREATE TABLE "CharacterResourceLedgerItem" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "resourceKey" TEXT NOT NULL, + "name" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "resourceType" TEXT NOT NULL, + "narrativeFunction" TEXT NOT NULL, + "ownerType" TEXT NOT NULL, + "ownerId" TEXT, + "ownerName" TEXT, + "ownerCharacterId" TEXT, + "holderCharacterId" TEXT, + "holderCharacterName" TEXT, + "status" TEXT NOT NULL, + "readerKnows" BOOLEAN NOT NULL DEFAULT false, + "holderKnows" BOOLEAN NOT NULL DEFAULT true, + "knownByCharacterIdsJson" TEXT, + "introducedChapterId" TEXT, + "introducedChapterOrder" INTEGER, + "lastTouchedChapterId" TEXT, + "lastTouchedChapterOrder" INTEGER, + "expectedUseStartChapterOrder" INTEGER, + "expectedUseEndChapterOrder" INTEGER, + "constraintsJson" TEXT, + "riskSignalsJson" TEXT, + "sourceRefsJson" TEXT, + "evidenceJson" TEXT, + "confidence" REAL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterResourceLedgerItem_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceLedgerItem_ownerCharacterId_fkey" FOREIGN KEY ("ownerCharacterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceLedgerItem_holderCharacterId_fkey" FOREIGN KEY ("holderCharacterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceLedgerItem_introducedChapterId_fkey" FOREIGN KEY ("introducedChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceLedgerItem_lastTouchedChapterId_fkey" FOREIGN KEY ("lastTouchedChapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "CharacterResourceEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "resourceId" TEXT NOT NULL, + "chapterId" TEXT, + "chapterOrder" INTEGER, + "eventType" TEXT NOT NULL, + "actorCharacterId" TEXT, + "fromHolderCharacterId" TEXT, + "toHolderCharacterId" TEXT, + "summary" TEXT NOT NULL, + "evidenceJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "CharacterResourceEvent_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceEvent_resourceId_fkey" FOREIGN KEY ("resourceId") REFERENCES "CharacterResourceLedgerItem" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceEvent_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceEvent_actorCharacterId_fkey" FOREIGN KEY ("actorCharacterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceEvent_fromHolderCharacterId_fkey" FOREIGN KEY ("fromHolderCharacterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterResourceEvent_toHolderCharacterId_fkey" FOREIGN KEY ("toHolderCharacterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "CharacterResourceLedgerItem_novelId_resourceKey_key" ON "CharacterResourceLedgerItem"("novelId", "resourceKey"); +CREATE INDEX "CharacterResourceLedgerItem_novelId_status_updatedAt_idx" ON "CharacterResourceLedgerItem"("novelId", "status", "updatedAt"); +CREATE INDEX "CharacterResourceLedgerItem_holderCharacterId_status_idx" ON "CharacterResourceLedgerItem"("holderCharacterId", "status"); +CREATE INDEX "CharacterResourceLedgerItem_ownerCharacterId_idx" ON "CharacterResourceLedgerItem"("ownerCharacterId"); +CREATE INDEX "CharacterResourceLedgerItem_novelId_lastTouchedChapterOrder_idx" ON "CharacterResourceLedgerItem"("novelId", "lastTouchedChapterOrder"); +CREATE INDEX "CharacterResourceLedgerItem_introducedChapterId_idx" ON "CharacterResourceLedgerItem"("introducedChapterId"); +CREATE INDEX "CharacterResourceLedgerItem_lastTouchedChapterId_idx" ON "CharacterResourceLedgerItem"("lastTouchedChapterId"); +CREATE INDEX "CharacterResourceEvent_novelId_createdAt_idx" ON "CharacterResourceEvent"("novelId", "createdAt"); +CREATE INDEX "CharacterResourceEvent_resourceId_createdAt_idx" ON "CharacterResourceEvent"("resourceId", "createdAt"); +CREATE INDEX "CharacterResourceEvent_chapterId_idx" ON "CharacterResourceEvent"("chapterId"); +CREATE INDEX "CharacterResourceEvent_actorCharacterId_idx" ON "CharacterResourceEvent"("actorCharacterId"); +CREATE INDEX "CharacterResourceEvent_fromHolderCharacterId_idx" ON "CharacterResourceEvent"("fromHolderCharacterId"); +CREATE INDEX "CharacterResourceEvent_toHolderCharacterId_idx" ON "CharacterResourceEvent"("toHolderCharacterId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260425160000_auto_director_auto_approval_records/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260425160000_auto_director_auto_approval_records/migration.sql new file mode 100644 index 0000000000..02efd58397 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260425160000_auto_director_auto_approval_records/migration.sql @@ -0,0 +1,25 @@ +CREATE TABLE "AutoDirectorAutoApprovalRecord" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskId" TEXT NOT NULL, + "novelId" TEXT NOT NULL, + "approvalPointCode" TEXT NOT NULL, + "approvalPointLabel" TEXT NOT NULL, + "checkpointType" TEXT NOT NULL, + "checkpointSummary" TEXT, + "summary" TEXT NOT NULL, + "stage" TEXT, + "scopeLabel" TEXT, + "eventId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "AutoDirectorAutoApprovalRecord_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "AutoDirectorAutoApprovalRecord_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "AutoDirectorAutoApprovalRecord_eventId_key" +ON "AutoDirectorAutoApprovalRecord"("eventId"); + +CREATE INDEX "AutoDirectorAutoApprovalRecord_novelId_createdAt_idx" +ON "AutoDirectorAutoApprovalRecord"("novelId", "createdAt"); + +CREATE INDEX "AutoDirectorAutoApprovalRecord_taskId_createdAt_idx" +ON "AutoDirectorAutoApprovalRecord"("taskId", "createdAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260426110000_character_library_sync/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260426110000_character_library_sync/migration.sql new file mode 100644 index 0000000000..56423511d9 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260426110000_character_library_sync/migration.sql @@ -0,0 +1,65 @@ +CREATE TABLE IF NOT EXISTS "BaseCharacterRevision" ( + "id" TEXT NOT NULL PRIMARY KEY, + "baseCharacterId" TEXT NOT NULL, + "version" INTEGER NOT NULL, + "snapshotJson" TEXT NOT NULL, + "changeSummary" TEXT, + "sourceType" TEXT NOT NULL DEFAULT 'manual', + "sourceRefId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BaseCharacterRevision_baseCharacterId_fkey" FOREIGN KEY ("baseCharacterId") REFERENCES "BaseCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterLibraryLink" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "baseCharacterId" TEXT NOT NULL, + "baseRevisionId" TEXT, + "syncPolicy" TEXT NOT NULL DEFAULT 'manual_review', + "linkStatus" TEXT NOT NULL DEFAULT 'linked', + "localOverridesJson" TEXT, + "lastSyncedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterLibraryLink_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterLibraryLink_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterLibraryLink_baseCharacterId_fkey" FOREIGN KEY ("baseCharacterId") REFERENCES "BaseCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterLibraryLink_baseRevisionId_fkey" FOREIGN KEY ("baseRevisionId") REFERENCES "BaseCharacterRevision" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "CharacterSyncProposal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT, + "characterId" TEXT, + "baseCharacterId" TEXT, + "baseRevisionId" TEXT, + "direction" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending_review', + "confidence" REAL, + "summary" TEXT NOT NULL, + "payloadJson" TEXT NOT NULL, + "safeUpdatesJson" TEXT, + "novelOnlyUpdatesJson" TEXT, + "riskyUpdatesJson" TEXT, + "recommendedAction" TEXT, + "sourceType" TEXT NOT NULL DEFAULT 'manual', + "sourceRefId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterSyncProposal_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterSyncProposal_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterSyncProposal_baseCharacterId_fkey" FOREIGN KEY ("baseCharacterId") REFERENCES "BaseCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterSyncProposal_baseRevisionId_fkey" FOREIGN KEY ("baseRevisionId") REFERENCES "BaseCharacterRevision" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS "BaseCharacterRevision_baseCharacterId_version_key" ON "BaseCharacterRevision"("baseCharacterId", "version"); +CREATE INDEX IF NOT EXISTS "BaseCharacterRevision_baseCharacterId_createdAt_idx" ON "BaseCharacterRevision"("baseCharacterId", "createdAt"); +CREATE UNIQUE INDEX IF NOT EXISTS "CharacterLibraryLink_characterId_key" ON "CharacterLibraryLink"("characterId"); +CREATE INDEX IF NOT EXISTS "CharacterLibraryLink_novelId_linkStatus_updatedAt_idx" ON "CharacterLibraryLink"("novelId", "linkStatus", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterLibraryLink_baseCharacterId_linkStatus_updatedAt_idx" ON "CharacterLibraryLink"("baseCharacterId", "linkStatus", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterLibraryLink_baseRevisionId_idx" ON "CharacterLibraryLink"("baseRevisionId"); +CREATE INDEX IF NOT EXISTS "CharacterSyncProposal_novelId_status_updatedAt_idx" ON "CharacterSyncProposal"("novelId", "status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterSyncProposal_characterId_status_updatedAt_idx" ON "CharacterSyncProposal"("characterId", "status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterSyncProposal_baseCharacterId_status_updatedAt_idx" ON "CharacterSyncProposal"("baseCharacterId", "status", "updatedAt"); +CREATE INDEX IF NOT EXISTS "CharacterSyncProposal_baseRevisionId_idx" ON "CharacterSyncProposal"("baseRevisionId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427090000_model_route_protocol_preferences/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427090000_model_route_protocol_preferences/migration.sql new file mode 100644 index 0000000000..08c2120241 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427090000_model_route_protocol_preferences/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "ModelRouteConfig" ADD COLUMN "requestProtocol" TEXT NOT NULL DEFAULT 'auto'; +ALTER TABLE "ModelRouteConfig" ADD COLUMN "structuredResponseFormat" TEXT NOT NULL DEFAULT 'auto'; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427170000_provider_request_limits/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427170000_provider_request_limits/migration.sql new file mode 100644 index 0000000000..c8326235f8 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427170000_provider_request_limits/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "APIKey" ADD COLUMN "concurrencyLimit" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "APIKey" ADD COLUMN "requestIntervalMs" INTEGER NOT NULL DEFAULT 0; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427173000_volume_plan_version_status_version_index/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427173000_volume_plan_version_status_version_index/migration.sql new file mode 100644 index 0000000000..67bd81b8d2 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427173000_volume_plan_version_status_version_index/migration.sql @@ -0,0 +1 @@ +CREATE INDEX IF NOT EXISTS "VolumePlanVersion_novelId_status_version_idx" ON "VolumePlanVersion"("novelId", "status", "version"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427174000_workflow_query_indexes/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427174000_workflow_query_indexes/migration.sql new file mode 100644 index 0000000000..09a901060f --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260427174000_workflow_query_indexes/migration.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS "ModelRouteConfig_provider_idx" ON "ModelRouteConfig"("provider"); +CREATE INDEX IF NOT EXISTS "StoryPlan_novelId_level_externalRef_idx" ON "StoryPlan"("novelId", "level", "externalRef"); +CREATE INDEX IF NOT EXISTS "StoryPlan_novelId_level_chapterId_idx" ON "StoryPlan"("novelId", "level", "chapterId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260428103000_director_runtime_ledger/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260428103000_director_runtime_ledger/migration.sql new file mode 100644 index 0000000000..b021f77817 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260428103000_director_runtime_ledger/migration.sql @@ -0,0 +1,110 @@ +CREATE TABLE "DirectorRun" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskId" TEXT NOT NULL, + "novelId" TEXT, + "entrypoint" TEXT, + "policyJson" TEXT NOT NULL, + "lastWorkspaceAnalysisJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorRun_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRun_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "DirectorStepRun" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runId" TEXT NOT NULL, + "taskId" TEXT NOT NULL, + "novelId" TEXT, + "idempotencyKey" TEXT NOT NULL, + "nodeKey" TEXT NOT NULL, + "label" TEXT NOT NULL, + "status" TEXT NOT NULL, + "targetType" TEXT, + "targetId" TEXT, + "startedAt" DATETIME NOT NULL, + "finishedAt" DATETIME, + "error" TEXT, + "producedArtifactsJson" TEXT, + "policyDecisionJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorStepRun_runId_fkey" FOREIGN KEY ("runId") REFERENCES "DirectorRun" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorStepRun_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "DirectorEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runId" TEXT, + "taskId" TEXT, + "novelId" TEXT, + "type" TEXT NOT NULL, + "nodeKey" TEXT, + "artifactId" TEXT, + "artifactType" TEXT, + "summary" TEXT NOT NULL, + "affectedScope" TEXT, + "severity" TEXT, + "metadataJson" TEXT, + "occurredAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DirectorEvent_runId_fkey" FOREIGN KEY ("runId") REFERENCES "DirectorRun" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorEvent_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorEvent_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "DirectorArtifact" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runId" TEXT, + "novelId" TEXT NOT NULL, + "taskId" TEXT, + "artifactType" TEXT NOT NULL, + "targetType" TEXT NOT NULL, + "targetId" TEXT, + "version" INTEGER NOT NULL DEFAULT 1, + "status" TEXT NOT NULL, + "source" TEXT NOT NULL, + "contentTable" TEXT NOT NULL, + "contentId" TEXT NOT NULL, + "contentHash" TEXT, + "schemaVersion" TEXT NOT NULL, + "promptAssetKey" TEXT, + "promptVersion" TEXT, + "modelRoute" TEXT, + "sourceStepRunId" TEXT, + "protectedUserContent" BOOLEAN, + "artifactUpdatedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorArtifact_runId_fkey" FOREIGN KEY ("runId") REFERENCES "DirectorRun" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorArtifact_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorArtifact_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "DirectorArtifactDependency" ( + "id" TEXT NOT NULL PRIMARY KEY, + "artifactId" TEXT NOT NULL, + "dependsOnArtifactId" TEXT NOT NULL, + "dependsOnVersion" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DirectorArtifactDependency_artifactId_fkey" FOREIGN KEY ("artifactId") REFERENCES "DirectorArtifact" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorArtifactDependency_dependsOnArtifactId_fkey" FOREIGN KEY ("dependsOnArtifactId") REFERENCES "DirectorArtifact" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "DirectorRun_taskId_key" ON "DirectorRun"("taskId"); +CREATE INDEX "DirectorRun_novelId_updatedAt_idx" ON "DirectorRun"("novelId", "updatedAt"); +CREATE UNIQUE INDEX "DirectorStepRun_idempotencyKey_key" ON "DirectorStepRun"("idempotencyKey"); +CREATE INDEX "DirectorStepRun_runId_status_updatedAt_idx" ON "DirectorStepRun"("runId", "status", "updatedAt"); +CREATE INDEX "DirectorStepRun_taskId_nodeKey_idx" ON "DirectorStepRun"("taskId", "nodeKey"); +CREATE INDEX "DirectorStepRun_novelId_updatedAt_idx" ON "DirectorStepRun"("novelId", "updatedAt"); +CREATE INDEX "DirectorEvent_runId_occurredAt_idx" ON "DirectorEvent"("runId", "occurredAt"); +CREATE INDEX "DirectorEvent_taskId_occurredAt_idx" ON "DirectorEvent"("taskId", "occurredAt"); +CREATE INDEX "DirectorEvent_novelId_occurredAt_idx" ON "DirectorEvent"("novelId", "occurredAt"); +CREATE INDEX "DirectorEvent_type_occurredAt_idx" ON "DirectorEvent"("type", "occurredAt"); +CREATE INDEX "DirectorArtifact_novelId_artifactType_status_idx" ON "DirectorArtifact"("novelId", "artifactType", "status"); +CREATE INDEX "DirectorArtifact_runId_updatedAt_idx" ON "DirectorArtifact"("runId", "updatedAt"); +CREATE INDEX "DirectorArtifact_taskId_updatedAt_idx" ON "DirectorArtifact"("taskId", "updatedAt"); +CREATE INDEX "DirectorArtifact_targetType_targetId_idx" ON "DirectorArtifact"("targetType", "targetId"); +CREATE INDEX "DirectorArtifact_sourceStepRunId_idx" ON "DirectorArtifact"("sourceStepRunId"); +CREATE UNIQUE INDEX "DirectorArtifactDependency_artifactId_dependsOnArtifactId_key" ON "DirectorArtifactDependency"("artifactId", "dependsOnArtifactId"); +CREATE INDEX "DirectorArtifactDependency_dependsOnArtifactId_idx" ON "DirectorArtifactDependency"("dependsOnArtifactId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260429213000_director_run_commands/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260429213000_director_run_commands/migration.sql new file mode 100644 index 0000000000..614014b565 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260429213000_director_run_commands/migration.sql @@ -0,0 +1,26 @@ +CREATE TABLE "DirectorRunCommand" ( + "id" TEXT NOT NULL PRIMARY KEY, + "taskId" TEXT NOT NULL, + "novelId" TEXT, + "commandType" TEXT NOT NULL, + "idempotencyKey" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'queued', + "leaseOwner" TEXT, + "leaseExpiresAt" DATETIME, + "attempt" INTEGER NOT NULL DEFAULT 0, + "runAfter" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "payloadJson" TEXT, + "errorMessage" TEXT, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorRunCommand_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRunCommand_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "DirectorRunCommand_taskId_commandType_idempotencyKey_key" ON "DirectorRunCommand"("taskId", "commandType", "idempotencyKey"); +CREATE INDEX "DirectorRunCommand_status_runAfter_updatedAt_idx" ON "DirectorRunCommand"("status", "runAfter", "updatedAt"); +CREATE INDEX "DirectorRunCommand_taskId_status_updatedAt_idx" ON "DirectorRunCommand"("taskId", "status", "updatedAt"); +CREATE INDEX "DirectorRunCommand_novelId_updatedAt_idx" ON "DirectorRunCommand"("novelId", "updatedAt"); +CREATE INDEX "DirectorRunCommand_leaseOwner_leaseExpiresAt_idx" ON "DirectorRunCommand"("leaseOwner", "leaseExpiresAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260430143000_director_llm_usage_records/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260430143000_director_llm_usage_records/migration.sql new file mode 100644 index 0000000000..f582d0f7de --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260430143000_director_llm_usage_records/migration.sql @@ -0,0 +1,33 @@ +CREATE TABLE "DirectorLlmUsageRecord" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT, + "taskId" TEXT, + "runId" TEXT, + "stepIdempotencyKey" TEXT, + "nodeKey" TEXT, + "promptAssetKey" TEXT, + "promptVersion" TEXT, + "modelRoute" TEXT, + "provider" TEXT, + "model" TEXT, + "status" TEXT NOT NULL DEFAULT 'recorded', + "attributionStatus" TEXT NOT NULL DEFAULT 'unattributed', + "durationMs" INTEGER, + "promptTokens" INTEGER NOT NULL DEFAULT 0, + "completionTokens" INTEGER NOT NULL DEFAULT 0, + "totalTokens" INTEGER NOT NULL DEFAULT 0, + "metadataJson" TEXT, + "recordedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DirectorLlmUsageRecord_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorLlmUsageRecord_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorLlmUsageRecord_runId_fkey" FOREIGN KEY ("runId") REFERENCES "DirectorRun" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorLlmUsageRecord_stepIdempotencyKey_fkey" FOREIGN KEY ("stepIdempotencyKey") REFERENCES "DirectorStepRun" ("idempotencyKey") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX "DirectorLlmUsageRecord_novelId_recordedAt_idx" ON "DirectorLlmUsageRecord"("novelId", "recordedAt"); +CREATE INDEX "DirectorLlmUsageRecord_taskId_recordedAt_idx" ON "DirectorLlmUsageRecord"("taskId", "recordedAt"); +CREATE INDEX "DirectorLlmUsageRecord_runId_recordedAt_idx" ON "DirectorLlmUsageRecord"("runId", "recordedAt"); +CREATE INDEX "DirectorLlmUsageRecord_stepIdempotencyKey_recordedAt_idx" ON "DirectorLlmUsageRecord"("stepIdempotencyKey", "recordedAt"); +CREATE INDEX "DirectorLlmUsageRecord_nodeKey_recordedAt_idx" ON "DirectorLlmUsageRecord"("nodeKey", "recordedAt"); +CREATE INDEX "DirectorLlmUsageRecord_attributionStatus_recordedAt_idx" ON "DirectorLlmUsageRecord"("attributionStatus", "recordedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260503090000_director_runtime_instances/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260503090000_director_runtime_instances/migration.sql new file mode 100644 index 0000000000..132128038e --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260503090000_director_runtime_instances/migration.sql @@ -0,0 +1,145 @@ +-- Additive runtime tables for multi-novel background director execution. + +CREATE TABLE "DirectorRuntimeInstance" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT, + "workflowTaskId" TEXT, + "runId" TEXT, + "runMode" TEXT, + "status" TEXT NOT NULL DEFAULT 'waiting_worker', + "currentStep" TEXT, + "currentChapterId" TEXT, + "checkpointVersion" INTEGER NOT NULL DEFAULT 0, + "cancelRequestedAt" DATETIME, + "lastHeartbeatAt" DATETIME, + "lastErrorClass" TEXT, + "lastErrorMessage" TEXT, + "workerMessage" TEXT, + "metadataJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorRuntimeInstance_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeInstance_workflowTaskId_fkey" FOREIGN KEY ("workflowTaskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeInstance_runId_fkey" FOREIGN KEY ("runId") REFERENCES "DirectorRun" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "DirectorRuntimeCommand" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runtimeId" TEXT NOT NULL, + "workflowTaskId" TEXT, + "novelId" TEXT, + "legacyCommandId" TEXT, + "commandType" TEXT NOT NULL, + "idempotencyKey" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'queued', + "priority" INTEGER NOT NULL DEFAULT 50, + "runAfter" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "leaseOwner" TEXT, + "leaseExpiresAt" DATETIME, + "attempt" INTEGER NOT NULL DEFAULT 0, + "payloadJson" TEXT, + "errorMessage" TEXT, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorRuntimeCommand_runtimeId_fkey" FOREIGN KEY ("runtimeId") REFERENCES "DirectorRuntimeInstance" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeCommand_workflowTaskId_fkey" FOREIGN KEY ("workflowTaskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeCommand_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "DirectorRuntimeExecution" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runtimeId" TEXT NOT NULL, + "commandId" TEXT, + "workflowTaskId" TEXT, + "novelId" TEXT, + "legacyCommandId" TEXT, + "activeLockKey" TEXT, + "workerId" TEXT, + "slotId" TEXT, + "status" TEXT NOT NULL DEFAULT 'leased', + "stepType" TEXT NOT NULL, + "resourceClass" TEXT, + "leaseExpiresAt" DATETIME, + "heartbeatAt" DATETIME, + "startedAt" DATETIME, + "finishedAt" DATETIME, + "errorClass" TEXT, + "errorMessage" TEXT, + "inputHash" TEXT, + "checkpointVersion" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DirectorRuntimeExecution_runtimeId_fkey" FOREIGN KEY ("runtimeId") REFERENCES "DirectorRuntimeInstance" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeExecution_commandId_fkey" FOREIGN KEY ("commandId") REFERENCES "DirectorRuntimeCommand" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeExecution_workflowTaskId_fkey" FOREIGN KEY ("workflowTaskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeExecution_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "DirectorRuntimeCheckpoint" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runtimeId" TEXT NOT NULL, + "commandId" TEXT, + "executionId" TEXT, + "version" INTEGER NOT NULL, + "stepType" TEXT NOT NULL, + "targetType" TEXT, + "targetId" TEXT, + "inputHash" TEXT, + "outputRefJson" TEXT, + "stateJson" TEXT, + "summary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DirectorRuntimeCheckpoint_runtimeId_fkey" FOREIGN KEY ("runtimeId") REFERENCES "DirectorRuntimeInstance" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeCheckpoint_commandId_fkey" FOREIGN KEY ("commandId") REFERENCES "DirectorRuntimeCommand" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeCheckpoint_executionId_fkey" FOREIGN KEY ("executionId") REFERENCES "DirectorRuntimeExecution" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "DirectorRuntimeEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "runtimeId" TEXT NOT NULL, + "commandId" TEXT, + "executionId" TEXT, + "workflowTaskId" TEXT, + "novelId" TEXT, + "type" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "severity" TEXT, + "metadataJson" TEXT, + "occurredAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DirectorRuntimeEvent_runtimeId_fkey" FOREIGN KEY ("runtimeId") REFERENCES "DirectorRuntimeInstance" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeEvent_commandId_fkey" FOREIGN KEY ("commandId") REFERENCES "DirectorRuntimeCommand" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeEvent_executionId_fkey" FOREIGN KEY ("executionId") REFERENCES "DirectorRuntimeExecution" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeEvent_workflowTaskId_fkey" FOREIGN KEY ("workflowTaskId") REFERENCES "NovelWorkflowTask" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DirectorRuntimeEvent_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "DirectorRuntimeCommand_legacyCommandId_key" ON "DirectorRuntimeCommand"("legacyCommandId"); +CREATE UNIQUE INDEX "DirectorRuntimeCommand_runtimeId_commandType_idempotencyKey_key" ON "DirectorRuntimeCommand"("runtimeId", "commandType", "idempotencyKey"); +CREATE UNIQUE INDEX "DirectorRuntimeExecution_activeLockKey_key" ON "DirectorRuntimeExecution"("activeLockKey"); +CREATE UNIQUE INDEX "DirectorRuntimeCheckpoint_runtimeId_version_key" ON "DirectorRuntimeCheckpoint"("runtimeId", "version"); +CREATE INDEX "DirectorRuntimeInstance_novelId_status_updatedAt_idx" ON "DirectorRuntimeInstance"("novelId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeInstance_workflowTaskId_idx" ON "DirectorRuntimeInstance"("workflowTaskId"); +CREATE INDEX "DirectorRuntimeInstance_runId_idx" ON "DirectorRuntimeInstance"("runId"); +CREATE INDEX "DirectorRuntimeInstance_status_updatedAt_idx" ON "DirectorRuntimeInstance"("status", "updatedAt"); +CREATE INDEX "DirectorRuntimeCommand_runtimeId_status_updatedAt_idx" ON "DirectorRuntimeCommand"("runtimeId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeCommand_status_priority_runAfter_createdAt_idx" ON "DirectorRuntimeCommand"("status", "priority", "runAfter", "createdAt"); +CREATE INDEX "DirectorRuntimeCommand_workflowTaskId_status_updatedAt_idx" ON "DirectorRuntimeCommand"("workflowTaskId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeCommand_novelId_status_updatedAt_idx" ON "DirectorRuntimeCommand"("novelId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeCommand_leaseOwner_leaseExpiresAt_idx" ON "DirectorRuntimeCommand"("leaseOwner", "leaseExpiresAt"); +CREATE INDEX "DirectorRuntimeExecution_runtimeId_status_updatedAt_idx" ON "DirectorRuntimeExecution"("runtimeId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeExecution_status_leaseExpiresAt_idx" ON "DirectorRuntimeExecution"("status", "leaseExpiresAt"); +CREATE INDEX "DirectorRuntimeExecution_workflowTaskId_status_updatedAt_idx" ON "DirectorRuntimeExecution"("workflowTaskId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeExecution_novelId_status_updatedAt_idx" ON "DirectorRuntimeExecution"("novelId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeExecution_workerId_status_updatedAt_idx" ON "DirectorRuntimeExecution"("workerId", "status", "updatedAt"); +CREATE INDEX "DirectorRuntimeCheckpoint_runtimeId_createdAt_idx" ON "DirectorRuntimeCheckpoint"("runtimeId", "createdAt"); +CREATE INDEX "DirectorRuntimeCheckpoint_commandId_idx" ON "DirectorRuntimeCheckpoint"("commandId"); +CREATE INDEX "DirectorRuntimeCheckpoint_executionId_idx" ON "DirectorRuntimeCheckpoint"("executionId"); +CREATE INDEX "DirectorRuntimeEvent_runtimeId_occurredAt_idx" ON "DirectorRuntimeEvent"("runtimeId", "occurredAt"); +CREATE INDEX "DirectorRuntimeEvent_commandId_occurredAt_idx" ON "DirectorRuntimeEvent"("commandId", "occurredAt"); +CREATE INDEX "DirectorRuntimeEvent_executionId_occurredAt_idx" ON "DirectorRuntimeEvent"("executionId", "occurredAt"); +CREATE INDEX "DirectorRuntimeEvent_workflowTaskId_occurredAt_idx" ON "DirectorRuntimeEvent"("workflowTaskId", "occurredAt"); +CREATE INDEX "DirectorRuntimeEvent_novelId_occurredAt_idx" ON "DirectorRuntimeEvent"("novelId", "occurredAt"); +CREATE INDEX "DirectorRuntimeEvent_type_occurredAt_idx" ON "DirectorRuntimeEvent"("type", "occurredAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260508120000_prompt_addendums/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260508120000_prompt_addendums/migration.sql new file mode 100644 index 0000000000..385981fef7 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260508120000_prompt_addendums/migration.sql @@ -0,0 +1,18 @@ +-- Additive table for user-managed prompt addendums. + +CREATE TABLE "PromptAddendum" ( + "id" TEXT NOT NULL PRIMARY KEY, + "scope" TEXT NOT NULL, + "novelId" TEXT, + "promptId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "PromptAddendum_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "PromptAddendum_scope_promptId_enabled_idx" ON "PromptAddendum"("scope", "promptId", "enabled"); +CREATE INDEX "PromptAddendum_novelId_promptId_enabled_idx" ON "PromptAddendum"("novelId", "promptId", "enabled"); +CREATE INDEX "PromptAddendum_updatedAt_idx" ON "PromptAddendum"("updatedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260510120000_character_visible_profile/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260510120000_character_visible_profile/migration.sql new file mode 100644 index 0000000000..1d8617714c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260510120000_character_visible_profile/migration.sql @@ -0,0 +1,6 @@ +ALTER TABLE "Character" ADD COLUMN "appearance" TEXT; +ALTER TABLE "Character" ADD COLUMN "physique" TEXT; +ALTER TABLE "Character" ADD COLUMN "attireStyle" TEXT; +ALTER TABLE "Character" ADD COLUMN "signatureDetail" TEXT; +ALTER TABLE "Character" ADD COLUMN "voiceTexture" TEXT; +ALTER TABLE "Character" ADD COLUMN "presenceImpression" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260511120000_anti_ai_global_baseline_enabled/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260511120000_anti_ai_global_baseline_enabled/migration.sql new file mode 100644 index 0000000000..c5fc323a3b --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260511120000_anti_ai_global_baseline_enabled/migration.sql @@ -0,0 +1,9 @@ +ALTER TABLE "AntiAiRule" ADD COLUMN "globalBaselineEnabled" BOOLEAN NOT NULL DEFAULT false; + +UPDATE "AntiAiRule" +SET "globalBaselineEnabled" = true +WHERE "enabled" = true + AND "type" IN ('forbidden', 'risk'); + +CREATE INDEX "AntiAiRule_globalBaselineEnabled_enabled_idx" +ON "AntiAiRule"("globalBaselineEnabled", "enabled"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260512110000_novel_post_generation_style_review/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260512110000_novel_post_generation_style_review/migration.sql new file mode 100644 index 0000000000..6f169fefa6 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260512110000_novel_post_generation_style_review/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "Novel" ADD COLUMN "postGenerationStyleReviewEnabled" BOOLEAN NOT NULL DEFAULT true; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260513130000_chapter_artifact_sync_checkpoint/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260513130000_chapter_artifact_sync_checkpoint/migration.sql new file mode 100644 index 0000000000..80a9b1d44f --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260513130000_chapter_artifact_sync_checkpoint/migration.sql @@ -0,0 +1,22 @@ +CREATE TABLE "ChapterArtifactSyncCheckpoint" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT NOT NULL, + "contentHash" TEXT NOT NULL, + "artifactType" TEXT NOT NULL, + "syncMode" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'succeeded', + "sourceType" TEXT, + "sourceStage" TEXT, + "metadataJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ChapterArtifactSyncCheckpoint_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ChapterArtifactSyncCheckpoint_chapterId_fkey" FOREIGN KEY ("chapterId") REFERENCES "Chapter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "ChapterArtifactSyncCheckpoint_novelId_chapterId_contentHash_artifactType_syncMode_key" + ON "ChapterArtifactSyncCheckpoint"("novelId", "chapterId", "contentHash", "artifactType", "syncMode"); + +CREATE INDEX "ChapterArtifactSyncCheckpoint_novelId_chapterId_artifactType_updatedAt_idx" + ON "ChapterArtifactSyncCheckpoint"("novelId", "chapterId", "artifactType", "updatedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260518090000_timeline_constraint_layer/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260518090000_timeline_constraint_layer/migration.sql new file mode 100644 index 0000000000..e66e76f2e8 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260518090000_timeline_constraint_layer/migration.sql @@ -0,0 +1,105 @@ +CREATE TABLE "StoryTimelineEvent" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "chapterIndex" INTEGER, + "eventOrder" INTEGER NOT NULL, + "storyDayIndex" INTEGER, + "storyTimeLabel" TEXT, + "title" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL, + "visibility" TEXT NOT NULL, + "source" TEXT NOT NULL, + "participantIdsJson" TEXT NOT NULL DEFAULT '[]', + "locationId" TEXT, + "factionIdsJson" TEXT NOT NULL DEFAULT '[]', + "prerequisiteIdsJson" TEXT NOT NULL DEFAULT '[]', + "consequenceIdsJson" TEXT NOT NULL DEFAULT '[]', + "stateChangesJson" TEXT NOT NULL DEFAULT '[]', + "eventKey" TEXT, + "confidence" REAL NOT NULL DEFAULT 1, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "ChapterTimeAnchor" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT NOT NULL, + "chapterIndex" INTEGER NOT NULL, + "storyDayIndex" INTEGER, + "timeLabel" TEXT NOT NULL, + "startsAfterIdsJson" TEXT NOT NULL DEFAULT '[]', + "plannedEventIdsJson" TEXT NOT NULL DEFAULT '[]', + "endedWithIdsJson" TEXT NOT NULL DEFAULT '[]', + "previousHookIdsJson" TEXT NOT NULL DEFAULT '[]', + "nextHookIdsJson" TEXT NOT NULL DEFAULT '[]', + "forbiddenEventIdsJson" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "TimelineHook" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "createdInChapterId" TEXT NOT NULL, + "createdInChapterIndex" INTEGER NOT NULL, + "expectedResolveByChapterIndex" INTEGER, + "resolvedInChapterId" TEXT, + "resolvedInChapterIndex" INTEGER, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "status" TEXT NOT NULL, + "priority" TEXT NOT NULL, + "relatedEventIdsJson" TEXT NOT NULL DEFAULT '[]', + "participantIdsJson" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "TimelineConstraint" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT, + "chapterIndex" INTEGER, + "type" TEXT NOT NULL, + "severity" TEXT NOT NULL, + "description" TEXT NOT NULL, + "relatedEventIdsJson" TEXT NOT NULL DEFAULT '[]', + "relatedHookIdsJson" TEXT NOT NULL DEFAULT '[]', + "relatedCharacterIdsJson" TEXT NOT NULL DEFAULT '[]', + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "TimelineCheckReport" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "chapterId" TEXT NOT NULL, + "chapterIndex" INTEGER NOT NULL, + "status" TEXT NOT NULL, + "score" REAL NOT NULL, + "issuesJson" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX "StoryTimelineEvent_novelId_chapterIndex_idx" ON "StoryTimelineEvent"("novelId", "chapterIndex"); +CREATE INDEX "StoryTimelineEvent_novelId_eventOrder_idx" ON "StoryTimelineEvent"("novelId", "eventOrder"); +CREATE INDEX "StoryTimelineEvent_novelId_status_idx" ON "StoryTimelineEvent"("novelId", "status"); +CREATE INDEX "StoryTimelineEvent_novelId_eventKey_idx" ON "StoryTimelineEvent"("novelId", "eventKey"); + +CREATE UNIQUE INDEX "ChapterTimeAnchor_novelId_chapterId_key" ON "ChapterTimeAnchor"("novelId", "chapterId"); +CREATE INDEX "ChapterTimeAnchor_novelId_chapterIndex_idx" ON "ChapterTimeAnchor"("novelId", "chapterIndex"); + +CREATE INDEX "TimelineHook_novelId_status_idx" ON "TimelineHook"("novelId", "status"); +CREATE INDEX "TimelineHook_novelId_createdInChapterIndex_idx" ON "TimelineHook"("novelId", "createdInChapterIndex"); +CREATE INDEX "TimelineHook_novelId_expectedResolveByChapterIndex_idx" ON "TimelineHook"("novelId", "expectedResolveByChapterIndex"); + +CREATE INDEX "TimelineConstraint_novelId_chapterIndex_idx" ON "TimelineConstraint"("novelId", "chapterIndex"); +CREATE INDEX "TimelineConstraint_novelId_active_idx" ON "TimelineConstraint"("novelId", "active"); + +CREATE INDEX "TimelineCheckReport_novelId_chapterIndex_idx" ON "TimelineCheckReport"("novelId", "chapterIndex"); +CREATE INDEX "TimelineCheckReport_novelId_status_idx" ON "TimelineCheckReport"("novelId", "status"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260518120000_character_hard_facts/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260518120000_character_hard_facts/migration.sql new file mode 100644 index 0000000000..2733f5e26e --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260518120000_character_hard_facts/migration.sql @@ -0,0 +1,20 @@ +ALTER TABLE "Character" ADD COLUMN "identityLabel" TEXT; +ALTER TABLE "Character" ADD COLUMN "factionLabel" TEXT; +ALTER TABLE "Character" ADD COLUMN "stanceLabel" TEXT; +ALTER TABLE "Character" ADD COLUMN "powerLevel" TEXT; +ALTER TABLE "Character" ADD COLUMN "realm" TEXT; +ALTER TABLE "Character" ADD COLUMN "currentLocation" TEXT; +ALTER TABLE "Character" ADD COLUMN "availability" TEXT; +ALTER TABLE "Character" ADD COLUMN "prohibitionsJson" TEXT NOT NULL DEFAULT '[]'; + +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "personality" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "background" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "development" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "identityLabel" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "factionLabel" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "stanceLabel" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "powerLevel" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "realm" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "currentLocation" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "availability" TEXT; +ALTER TABLE "CharacterCastOptionMember" ADD COLUMN "prohibitionsJson" TEXT NOT NULL DEFAULT '[]'; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260519100000_timeline_hook_resolve_mode/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260519100000_timeline_hook_resolve_mode/migration.sql new file mode 100644 index 0000000000..07d0861d15 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260519100000_timeline_hook_resolve_mode/migration.sql @@ -0,0 +1,7 @@ +ALTER TABLE "TimelineHook" +ADD COLUMN "resolveMode" TEXT NOT NULL DEFAULT 'long_arc'; + +ALTER TABLE "TimelineHook" +ADD COLUMN "blocking" BOOLEAN NOT NULL DEFAULT false; + +CREATE INDEX "TimelineHook_novelId_resolveMode_blocking_idx" ON "TimelineHook"("novelId", "resolveMode", "blocking"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260522150000_novel_cover_images/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260522150000_novel_cover_images/migration.sql new file mode 100644 index 0000000000..ea734de36b --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260522150000_novel_cover_images/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE "ImageGenerationTask" ADD COLUMN "novelId" TEXT REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ImageAsset" ADD COLUMN "novelId" TEXT REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE INDEX "ImageGenerationTask_novelId_createdAt_idx" ON "ImageGenerationTask"("novelId", "createdAt"); +CREATE INDEX "ImageAsset_novelId_isPrimary_createdAt_idx" ON "ImageAsset"("novelId", "isPrimary", "createdAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260526110000_novel_side_effect_jobs/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260526110000_novel_side_effect_jobs/migration.sql new file mode 100644 index 0000000000..a200c81bab --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260526110000_novel_side_effect_jobs/migration.sql @@ -0,0 +1,25 @@ +CREATE TABLE "NovelSideEffectJob" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT, + "jobType" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "idempotencyKey" TEXT NOT NULL, + "payloadVersion" INTEGER NOT NULL DEFAULT 1, + "payloadJson" TEXT NOT NULL, + "attempts" INTEGER NOT NULL DEFAULT 0, + "maxAttempts" INTEGER NOT NULL DEFAULT 5, + "runAfter" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "leaseOwner" TEXT, + "leaseExpiresAt" DATETIME, + "lastError" TEXT, + "finishedAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "NovelSideEffectJob_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "NovelSideEffectJob_idempotencyKey_key" ON "NovelSideEffectJob"("idempotencyKey"); +CREATE INDEX "NovelSideEffectJob_status_runAfter_idx" ON "NovelSideEffectJob"("status", "runAfter"); +CREATE INDEX "NovelSideEffectJob_novelId_status_updatedAt_idx" ON "NovelSideEffectJob"("novelId", "status", "updatedAt"); +CREATE INDEX "NovelSideEffectJob_leaseOwner_leaseExpiresAt_idx" ON "NovelSideEffectJob"("leaseOwner", "leaseExpiresAt"); +CREATE INDEX "NovelSideEffectJob_jobType_status_runAfter_idx" ON "NovelSideEffectJob"("jobType", "status", "runAfter"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260528110000_volume_chapter_plan_chapter_link/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260528110000_volume_chapter_plan_chapter_link/migration.sql new file mode 100644 index 0000000000..b03525b3d6 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260528110000_volume_chapter_plan_chapter_link/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "VolumeChapterPlan" ADD COLUMN "chapterId" TEXT; + +CREATE INDEX "VolumeChapterPlan_chapterId_idx" ON "VolumeChapterPlan"("chapterId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260529120000_novel_world_instance/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260529120000_novel_world_instance/migration.sql new file mode 100644 index 0000000000..de62c2cbf9 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260529120000_novel_world_instance/migration.sql @@ -0,0 +1,90 @@ +CREATE TABLE "NovelWorld" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "sourceWorldId" TEXT, + "sourceType" TEXT NOT NULL DEFAULT 'manual', + "title" TEXT, + "coverSummary" TEXT, + "structuredDataJson" TEXT, + "bindingContractJson" TEXT, + "storySliceJson" TEXT, + "storySliceOverridesJson" TEXT, + "storySliceSchemaVersion" INTEGER NOT NULL DEFAULT 1, + "storySliceBuiltAt" DATETIME, + "storySliceDigest" TEXT, + "syncEnabled" BOOLEAN NOT NULL DEFAULT false, + "syncDirection" TEXT NOT NULL DEFAULT 'none', + "syncBaseVersion" INTEGER, + "syncPendingChangesJson" TEXT, + "lastSyncedAt" DATETIME, + "generationPolicyJson" TEXT, + "generatedFromThemeJson" TEXT, + "savedToLibraryAt" DATETIME, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "NovelWorld_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "NovelWorld_sourceWorldId_fkey" FOREIGN KEY ("sourceWorldId") REFERENCES "World" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "WorldSyncRecord" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelWorldId" TEXT NOT NULL, + "sourceWorldId" TEXT, + "direction" TEXT NOT NULL, + "syncedFieldsJson" TEXT, + "diffSummary" TEXT, + "triggeredBy" TEXT NOT NULL DEFAULT 'user', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "WorldSyncRecord_novelWorldId_fkey" FOREIGN KEY ("novelWorldId") REFERENCES "NovelWorld" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "NovelWorld_novelId_key" ON "NovelWorld"("novelId"); +CREATE INDEX "NovelWorld_sourceWorldId_idx" ON "NovelWorld"("sourceWorldId"); +CREATE INDEX "NovelWorld_sourceType_idx" ON "NovelWorld"("sourceType"); +CREATE INDEX "WorldSyncRecord_novelWorldId_createdAt_idx" ON "WorldSyncRecord"("novelWorldId", "createdAt"); +CREATE INDEX "WorldSyncRecord_sourceWorldId_idx" ON "WorldSyncRecord"("sourceWorldId"); + +INSERT INTO "NovelWorld" ( + "id", + "novelId", + "sourceWorldId", + "sourceType", + "title", + "coverSummary", + "structuredDataJson", + "bindingContractJson", + "storySliceJson", + "storySliceOverridesJson", + "storySliceSchemaVersion", + "storySliceBuiltAt", + "storySliceDigest", + "syncEnabled", + "syncDirection", + "syncBaseVersion", + "createdAt", + "updatedAt" +) +SELECT + 'novel_world_' || n."id", + n."id", + n."worldId", + CASE WHEN n."worldId" IS NOT NULL THEN 'imported' ELSE 'manual' END, + w."name", + COALESCE(w."overviewSummary", w."description"), + w."structureJson", + w."bindingSupportJson", + n."storyWorldSliceJson", + n."storyWorldSliceOverridesJson", + n."storyWorldSliceSchemaVersion", + CASE WHEN n."storyWorldSliceJson" IS NOT NULL THEN n."updatedAt" ELSE NULL END, + NULL, + false, + 'none', + w."version", + n."createdAt", + n."updatedAt" +FROM "Novel" n +LEFT JOIN "World" w ON w."id" = n."worldId" +WHERE n."worldId" IS NOT NULL + OR n."storyWorldSliceJson" IS NOT NULL + OR n."storyWorldSliceOverridesJson" IS NOT NULL; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260529133000_world_assets/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260529133000_world_assets/migration.sql new file mode 100644 index 0000000000..fa98289065 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260529133000_world_assets/migration.sql @@ -0,0 +1,21 @@ +CREATE TABLE "WorldAsset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "worldId" TEXT, + "novelWorldId" TEXT, + "assetType" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "generationPrompt" TEXT, + "renderDataJson" TEXT, + "thumbnailUrl" TEXT, + "version" INTEGER NOT NULL DEFAULT 1, + "status" TEXT NOT NULL DEFAULT 'placeholder', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "WorldAsset_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "WorldAsset_novelWorldId_fkey" FOREIGN KEY ("novelWorldId") REFERENCES "NovelWorld" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "WorldAsset_worldId_assetType_idx" ON "WorldAsset"("worldId", "assetType"); +CREATE INDEX "WorldAsset_novelWorldId_assetType_idx" ON "WorldAsset"("novelWorldId", "assetType"); +CREATE INDEX "WorldAsset_assetType_idx" ON "WorldAsset"("assetType"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260609120000_drama_forge_pipeline/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260609120000_drama_forge_pipeline/migration.sql new file mode 100644 index 0000000000..6a1f90cb7c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260609120000_drama_forge_pipeline/migration.sql @@ -0,0 +1,159 @@ +CREATE TABLE IF NOT EXISTS "DramaProject" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "source" TEXT NOT NULL DEFAULT 'original', + "sourceRef" TEXT, + "sourceInput" TEXT, + "track" TEXT, + "theme" TEXT, + "orientation" TEXT NOT NULL DEFAULT 'vertical_paid', + "targetEpisodes" INTEGER NOT NULL DEFAULT 80, + "strategy" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS "DramaSourceBundle" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "synopsis" TEXT, + "beats" TEXT, + "worldNotes" TEXT, + "hardFacts" TEXT, + "rawText" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaSourceBundle_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaCharacter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "archetype" TEXT, + "persona" TEXT, + "speechStyle" TEXT, + "visualAnchor" TEXT, + "voiceProfile" TEXT, + "relations" TEXT, + "sourceCharacterRef" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaCharacter_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaEpisode" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT DEFAULT '', + "hookOpening" TEXT, + "cliffhanger" TEXT, + "hookType" TEXT, + "isPaywall" BOOLEAN NOT NULL DEFAULT false, + "emotionNet" INTEGER, + "beatSheet" TEXT, + "sourceMap" TEXT, + "durationSec" INTEGER, + "status" TEXT NOT NULL DEFAULT 'planned', + "qualityFlags" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaEpisode_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaFact" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "episodeOrder" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "category" TEXT NOT NULL DEFAULT 'completed', + "source" TEXT NOT NULL DEFAULT 'auto', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaFact_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaCharacterLibrary" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT, + "name" TEXT NOT NULL, + "archetype" TEXT, + "persona" TEXT, + "speechStyle" TEXT, + "visualAnchor" TEXT, + "voiceProfile" TEXT, + "relations" TEXT, + "tags" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaCharacterLibrary_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaStoryboard" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "episodeId" TEXT NOT NULL, + "version" INTEGER NOT NULL DEFAULT 1, + "status" TEXT NOT NULL DEFAULT 'draft', + "summary" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaStoryboard_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DramaStoryboard_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "DramaEpisode" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaShot" ( + "id" TEXT NOT NULL PRIMARY KEY, + "storyboardId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "shotSize" TEXT, + "cameraMove" TEXT, + "durationSec" INTEGER, + "location" TEXT, + "action" TEXT NOT NULL, + "dialogue" TEXT, + "characterRefs" TEXT, + "visualPrompt" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaShot_storyboardId_fkey" FOREIGN KEY ("storyboardId") REFERENCES "DramaStoryboard" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE IF NOT EXISTS "DramaVideoPrompt" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "episodeId" TEXT, + "shotId" TEXT, + "provider" TEXT NOT NULL DEFAULT 'mock', + "prompt" TEXT NOT NULL, + "negativePrompt" TEXT, + "aspectRatio" TEXT NOT NULL DEFAULT '9:16', + "durationSec" INTEGER, + "status" TEXT NOT NULL DEFAULT 'prompted', + "providerTaskId" TEXT, + "providerResult" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaVideoPrompt_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DramaVideoPrompt_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "DramaEpisode" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX IF NOT EXISTS "DramaSourceBundle_projectId_key" ON "DramaSourceBundle"("projectId"); +CREATE INDEX IF NOT EXISTS "DramaProject_source_idx" ON "DramaProject"("source"); +CREATE INDEX IF NOT EXISTS "DramaProject_status_idx" ON "DramaProject"("status"); +CREATE INDEX IF NOT EXISTS "DramaCharacter_projectId_idx" ON "DramaCharacter"("projectId"); +CREATE UNIQUE INDEX IF NOT EXISTS "DramaEpisode_projectId_order_key" ON "DramaEpisode"("projectId", "order"); +CREATE INDEX IF NOT EXISTS "DramaEpisode_projectId_status_idx" ON "DramaEpisode"("projectId", "status"); +CREATE INDEX IF NOT EXISTS "DramaFact_projectId_episodeOrder_idx" ON "DramaFact"("projectId", "episodeOrder"); +CREATE INDEX IF NOT EXISTS "DramaFact_projectId_category_idx" ON "DramaFact"("projectId", "category"); +CREATE INDEX IF NOT EXISTS "DramaCharacterLibrary_projectId_idx" ON "DramaCharacterLibrary"("projectId"); +CREATE INDEX IF NOT EXISTS "DramaCharacterLibrary_name_idx" ON "DramaCharacterLibrary"("name"); +CREATE INDEX IF NOT EXISTS "DramaStoryboard_projectId_idx" ON "DramaStoryboard"("projectId"); +CREATE INDEX IF NOT EXISTS "DramaStoryboard_episodeId_idx" ON "DramaStoryboard"("episodeId"); +CREATE UNIQUE INDEX IF NOT EXISTS "DramaShot_storyboardId_order_key" ON "DramaShot"("storyboardId", "order"); +CREATE INDEX IF NOT EXISTS "DramaShot_storyboardId_idx" ON "DramaShot"("storyboardId"); +CREATE INDEX IF NOT EXISTS "DramaVideoPrompt_projectId_idx" ON "DramaVideoPrompt"("projectId"); +CREATE INDEX IF NOT EXISTS "DramaVideoPrompt_episodeId_idx" ON "DramaVideoPrompt"("episodeId"); +CREATE INDEX IF NOT EXISTS "DramaVideoPrompt_provider_status_idx" ON "DramaVideoPrompt"("provider", "status"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260609170000_drama_video_task_projection/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260609170000_drama_video_task_projection/migration.sql new file mode 100644 index 0000000000..5c14acef39 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260609170000_drama_video_task_projection/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "DramaVideoPrompt" ADD COLUMN "resultUrl" TEXT; +ALTER TABLE "DramaVideoPrompt" ADD COLUMN "failureReason" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610090000_drama_shot_keyframes/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610090000_drama_shot_keyframes/migration.sql new file mode 100644 index 0000000000..b53da4b0a3 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610090000_drama_shot_keyframes/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "DramaShot" ADD COLUMN "keyframeData" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610110000_drama_batch_jobs/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610110000_drama_batch_jobs/migration.sql new file mode 100644 index 0000000000..9c606d14f7 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610110000_drama_batch_jobs/migration.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS "DramaBatchJob" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "episodeId" TEXT, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "progress" TEXT NOT NULL DEFAULT '{}', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "DramaBatchJob_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "DramaProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "DramaBatchJob_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "DramaEpisode" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX IF NOT EXISTS "DramaBatchJob_projectId_createdAt_idx" ON "DramaBatchJob"("projectId", "createdAt"); +CREATE INDEX IF NOT EXISTS "DramaBatchJob_episodeId_status_idx" ON "DramaBatchJob"("episodeId", "status"); +CREATE INDEX IF NOT EXISTS "DramaBatchJob_type_status_idx" ON "DramaBatchJob"("type", "status"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610130000_drama_dialogue_audio/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610130000_drama_dialogue_audio/migration.sql new file mode 100644 index 0000000000..412f145515 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610130000_drama_dialogue_audio/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "DramaShot" ADD COLUMN "dialogueAudioData" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610143000_drama_generation_versions/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610143000_drama_generation_versions/migration.sql new file mode 100644 index 0000000000..9648eeec9e --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260610143000_drama_generation_versions/migration.sql @@ -0,0 +1,4 @@ +ALTER TABLE "DramaVideoPrompt" ADD COLUMN "version" INTEGER NOT NULL DEFAULT 1; +ALTER TABLE "DramaVideoPrompt" ADD COLUMN "supersededById" TEXT; + +CREATE INDEX IF NOT EXISTS "DramaVideoPrompt_projectId_shotId_version_idx" ON "DramaVideoPrompt"("projectId", "shotId", "version"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260612000000_add_comic_module/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260612000000_add_comic_module/migration.sql new file mode 100644 index 0000000000..b7c469a794 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260612000000_add_comic_module/migration.sql @@ -0,0 +1,133 @@ +-- CreateTable: Comic 模块(AI 漫画) +-- 低耦合:对 Novel 零外键,sourceRef 软引用,保证可拆分 + +CREATE TABLE "ComicProject" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "sourceType" TEXT NOT NULL DEFAULT 'original', + "sourceRef" TEXT, + "sourceInput" TEXT, + "trackId" TEXT, + "stylePreset" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "ComicSourceBundle" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "bundleJson" TEXT NOT NULL, + "importedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ComicSourceBundle_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicCharacter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "persona" TEXT, + "visualAnchor" TEXT, + "sheetData" TEXT, + "sourceCharacterRef" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicCharacter_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicEpisode" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "title" TEXT, + "hookType" TEXT, + "cliffhanger" TEXT, + "isPaywalled" BOOLEAN NOT NULL DEFAULT false, + "outline" TEXT, + "sourceText" TEXT, + "status" TEXT NOT NULL DEFAULT 'draft', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicEpisode_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicPanel" ( + "id" TEXT NOT NULL PRIMARY KEY, + "episodeId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "panelType" TEXT, + "action" TEXT NOT NULL, + "dialogues" TEXT, + "characterRefs" TEXT, + "visualPrompt" TEXT, + "imageData" TEXT, + "letteredData" TEXT, + "motionData" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicPanel_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "ComicEpisode" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicFact" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "text" TEXT NOT NULL, + "category" TEXT NOT NULL DEFAULT 'completed', + "episodeOrder" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ComicFact_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicUploadAsset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "filePath" TEXT NOT NULL, + "refId" TEXT, + "metadata" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "ComicUploadAsset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicExportJob" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "episodeId" TEXT, + "format" TEXT NOT NULL, + "spec" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "artifacts" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicExportJob_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "ComicBatchJob" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "episodeId" TEXT, + "type" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'pending', + "progress" TEXT NOT NULL DEFAULT '{}', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicBatchJob_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ComicBatchJob_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "ComicEpisode" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "ComicSourceBundle_projectId_key" ON "ComicSourceBundle"("projectId"); +CREATE INDEX "ComicProject_sourceType_idx" ON "ComicProject"("sourceType"); +CREATE INDEX "ComicProject_status_idx" ON "ComicProject"("status"); +CREATE INDEX "ComicCharacter_projectId_idx" ON "ComicCharacter"("projectId"); +CREATE UNIQUE INDEX "ComicEpisode_projectId_order_key" ON "ComicEpisode"("projectId", "order"); +CREATE INDEX "ComicEpisode_projectId_status_idx" ON "ComicEpisode"("projectId", "status"); +CREATE UNIQUE INDEX "ComicPanel_episodeId_order_key" ON "ComicPanel"("episodeId", "order"); +CREATE INDEX "ComicPanel_episodeId_idx" ON "ComicPanel"("episodeId"); +CREATE INDEX "ComicFact_projectId_idx" ON "ComicFact"("projectId"); +CREATE INDEX "ComicUploadAsset_projectId_kind_idx" ON "ComicUploadAsset"("projectId", "kind"); +CREATE INDEX "ComicExportJob_projectId_createdAt_idx" ON "ComicExportJob"("projectId", "createdAt"); +CREATE INDEX "ComicExportJob_format_status_idx" ON "ComicExportJob"("format", "status"); +CREATE INDEX "ComicBatchJob_projectId_createdAt_idx" ON "ComicBatchJob"("projectId", "createdAt"); +CREATE INDEX "ComicBatchJob_episodeId_status_idx" ON "ComicBatchJob"("episodeId", "status"); +CREATE INDEX "ComicBatchJob_type_status_idx" ON "ComicBatchJob"("type", "status"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260616183000_comic_panel_prompt_metadata/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260616183000_comic_panel_prompt_metadata/migration.sql new file mode 100644 index 0000000000..d85dc96eeb --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260616183000_comic_panel_prompt_metadata/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE "ComicEpisode" ADD COLUMN "scriptConfig" TEXT; + +ALTER TABLE "ComicPanel" ADD COLUMN "densityLevel" TEXT; +ALTER TABLE "ComicPanel" ADD COLUMN "focus" TEXT; +ALTER TABLE "ComicPanel" ADD COLUMN "layoutData" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624120000_book_analysis_binding_source/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624120000_book_analysis_binding_source/migration.sql new file mode 100644 index 0000000000..a161e6a6f2 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624120000_book_analysis_binding_source/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "KnowledgeBinding" ADD COLUMN "sourceAnalysisId" TEXT; + +CREATE INDEX "KnowledgeBinding_sourceAnalysisId_idx" ON "KnowledgeBinding"("sourceAnalysisId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624123000_book_analysis_normalization_warnings/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624123000_book_analysis_normalization_warnings/migration.sql new file mode 100644 index 0000000000..64edc979a1 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624123000_book_analysis_normalization_warnings/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "BookAnalysisSection" ADD COLUMN "normalizationWarningsJson" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624130000_book_analysis_focus_instructions/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624130000_book_analysis_focus_instructions/migration.sql new file mode 100644 index 0000000000..cf7deab839 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624130000_book_analysis_focus_instructions/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "BookAnalysis" ADD COLUMN "userFocusInstruction" TEXT; +ALTER TABLE "BookAnalysisSection" ADD COLUMN "focusInstruction" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624142000_document_chapters/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624142000_document_chapters/migration.sql new file mode 100644 index 0000000000..3e1c67865c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624142000_document_chapters/migration.sql @@ -0,0 +1,17 @@ +CREATE TABLE "DocumentChapter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "documentVersionId" TEXT NOT NULL, + "chapterIndex" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "startOffset" INTEGER NOT NULL, + "endOffset" INTEGER NOT NULL, + "charCount" INTEGER NOT NULL, + "summary" TEXT, + "splitter" TEXT NOT NULL DEFAULT 'rule', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "DocumentChapter_documentVersionId_fkey" FOREIGN KEY ("documentVersionId") REFERENCES "KnowledgeDocumentVersion" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "DocumentChapter_documentVersionId_chapterIndex_key" ON "DocumentChapter"("documentVersionId", "chapterIndex"); +CREATE INDEX "DocumentChapter_documentVersionId_startOffset_idx" ON "DocumentChapter"("documentVersionId", "startOffset"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624153000_book_analysis_characters/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624153000_book_analysis_characters/migration.sql new file mode 100644 index 0000000000..b53a7d19c0 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624153000_book_analysis_characters/migration.sql @@ -0,0 +1,46 @@ +CREATE TABLE "BookAnalysisCharacter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "analysisId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL, + "generationDepth" TEXT NOT NULL DEFAULT 'standard', + "selectedDimensionsJson" TEXT, + "profileJson" TEXT NOT NULL, + "evidenceJson" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysisCharacter_analysisId_fkey" FOREIGN KEY ("analysisId") REFERENCES "BookAnalysis" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "BookAnalysisCharacterArc" ( + "id" TEXT NOT NULL PRIMARY KEY, + "characterId" TEXT NOT NULL, + "chapterIndex" INTEGER, + "stageLabel" TEXT NOT NULL, + "stateSnapshotJson" TEXT, + "evidenceJson" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysisCharacterArc_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "BookAnalysisCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "BookAnalysisCharacterScene" ( + "id" TEXT NOT NULL PRIMARY KEY, + "characterId" TEXT NOT NULL, + "sceneLabel" TEXT NOT NULL, + "sceneType" TEXT, + "performanceJson" TEXT, + "evidenceJson" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysisCharacterScene_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "BookAnalysisCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "BookAnalysisCharacter_analysisId_sortOrder_idx" ON "BookAnalysisCharacter"("analysisId", "sortOrder"); +CREATE INDEX "BookAnalysisCharacter_analysisId_name_idx" ON "BookAnalysisCharacter"("analysisId", "name"); +CREATE INDEX "BookAnalysisCharacterArc_characterId_sortOrder_idx" ON "BookAnalysisCharacterArc"("characterId", "sortOrder"); +CREATE INDEX "BookAnalysisCharacterArc_chapterIndex_idx" ON "BookAnalysisCharacterArc"("chapterIndex"); +CREATE INDEX "BookAnalysisCharacterScene_characterId_sortOrder_idx" ON "BookAnalysisCharacterScene"("characterId", "sortOrder"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624165000_book_analysis_character_images/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624165000_book_analysis_character_images/migration.sql new file mode 100644 index 0000000000..18f24cda96 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260624165000_book_analysis_character_images/migration.sql @@ -0,0 +1,9 @@ +ALTER TABLE "BaseCharacter" ADD COLUMN "sourceType" TEXT NOT NULL DEFAULT 'manual'; +ALTER TABLE "BaseCharacter" ADD COLUMN "sourceRefId" TEXT; + +ALTER TABLE "ImageGenerationTask" ADD COLUMN "bookAnalysisCharacterId" TEXT REFERENCES "BookAnalysisCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ImageAsset" ADD COLUMN "bookAnalysisCharacterId" TEXT REFERENCES "BookAnalysisCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE; + +CREATE INDEX "BaseCharacter_sourceType_sourceRefId_idx" ON "BaseCharacter"("sourceType", "sourceRefId"); +CREATE INDEX "ImageGenerationTask_bookAnalysisCharacterId_createdAt_idx" ON "ImageGenerationTask"("bookAnalysisCharacterId", "createdAt"); +CREATE INDEX "ImageAsset_bookAnalysisCharacterId_isPrimary_createdAt_idx" ON "ImageAsset"("bookAnalysisCharacterId", "isPrimary", "createdAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260625102000_book_analysis_source_range/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260625102000_book_analysis_source_range/migration.sql new file mode 100644 index 0000000000..217702e90c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260625102000_book_analysis_source_range/migration.sql @@ -0,0 +1,11 @@ +ALTER TABLE "BookAnalysis" ADD COLUMN "sourceStartChapterIndex" INTEGER; +ALTER TABLE "BookAnalysis" ADD COLUMN "sourceEndChapterIndex" INTEGER; +ALTER TABLE "BookAnalysis" ADD COLUMN "sourceStartOffset" INTEGER; +ALTER TABLE "BookAnalysis" ADD COLUMN "sourceEndOffset" INTEGER; +ALTER TABLE "BookAnalysis" ADD COLUMN "sourceScopeLabel" TEXT; + +ALTER TABLE "BookAnalysisSourceCache" ADD COLUMN "sourceScopeKey" TEXT NOT NULL DEFAULT 'full'; + +DROP INDEX IF EXISTS "BookAnalysisSourceCache_documentVersionId_provider_model_temperature_notesMaxTokens_segmentVersion_key"; +CREATE UNIQUE INDEX "BookAnalysisSourceCache_scope_unique" + ON "BookAnalysisSourceCache"("documentVersionId", "sourceScopeKey", "provider", "model", "temperature", "notesMaxTokens", "segmentVersion"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626110000_book_analysis_budget/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626110000_book_analysis_budget/migration.sql new file mode 100644 index 0000000000..a1d94d0b6c --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626110000_book_analysis_budget/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "BookAnalysis" ADD COLUMN "budgetTokens" INTEGER; +ALTER TABLE "BookAnalysis" ADD COLUMN "usedTokens" INTEGER; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626123000_knowledge_document_publish_kind/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626123000_knowledge_document_publish_kind/migration.sql new file mode 100644 index 0000000000..1ddbf2c6b9 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626123000_knowledge_document_publish_kind/migration.sql @@ -0,0 +1,39 @@ +ALTER TABLE "KnowledgeDocument" ADD COLUMN "kind" TEXT NOT NULL DEFAULT 'user_upload'; +ALTER TABLE "KnowledgeDocument" ADD COLUMN "sourceAnalysisId" TEXT; + +UPDATE "KnowledgeDocument" +SET "kind" = 'analysis_published' +WHERE EXISTS ( + SELECT 1 + FROM "KnowledgeBinding" + WHERE "KnowledgeBinding"."documentId" = "KnowledgeDocument"."id" + AND "KnowledgeBinding"."sourceAnalysisId" IS NOT NULL +); + +UPDATE "KnowledgeDocument" +SET "sourceAnalysisId" = ( + SELECT MIN("KnowledgeBinding"."sourceAnalysisId") + FROM "KnowledgeBinding" + WHERE "KnowledgeBinding"."documentId" = "KnowledgeDocument"."id" + AND "KnowledgeBinding"."sourceAnalysisId" IS NOT NULL +) +WHERE ( + SELECT COUNT(DISTINCT "KnowledgeBinding"."sourceAnalysisId") + FROM "KnowledgeBinding" + WHERE "KnowledgeBinding"."documentId" = "KnowledgeDocument"."id" + AND "KnowledgeBinding"."sourceAnalysisId" IS NOT NULL +) = 1 +AND NOT EXISTS ( + SELECT 1 + FROM "KnowledgeBinding" AS "duplicate_binding" + WHERE "duplicate_binding"."sourceAnalysisId" = ( + SELECT MIN("KnowledgeBinding"."sourceAnalysisId") + FROM "KnowledgeBinding" + WHERE "KnowledgeBinding"."documentId" = "KnowledgeDocument"."id" + AND "KnowledgeBinding"."sourceAnalysisId" IS NOT NULL + ) + AND "duplicate_binding"."documentId" <> "KnowledgeDocument"."id" +); + +CREATE UNIQUE INDEX "KnowledgeDocument_sourceAnalysisId_key" ON "KnowledgeDocument"("sourceAnalysisId"); +CREATE INDEX "KnowledgeDocument_kind_updatedAt_idx" ON "KnowledgeDocument"("kind", "updatedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626133000_knowledge_chunk_facets/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626133000_knowledge_chunk_facets/migration.sql new file mode 100644 index 0000000000..00bd51e570 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626133000_knowledge_chunk_facets/migration.sql @@ -0,0 +1,5 @@ +ALTER TABLE "KnowledgeChunk" ADD COLUMN "facetKeys" TEXT; +ALTER TABLE "KnowledgeChunk" ADD COLUMN "chapterAnchor" TEXT; + +CREATE INDEX "KnowledgeChunk_facetKeys_idx" ON "KnowledgeChunk"("facetKeys"); +CREATE INDEX "KnowledgeChunk_chapterAnchor_idx" ON "KnowledgeChunk"("chapterAnchor"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626143000_rag_retrieval_trace/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626143000_rag_retrieval_trace/migration.sql new file mode 100644 index 0000000000..dd1ce73142 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626143000_rag_retrieval_trace/migration.sql @@ -0,0 +1,19 @@ +CREATE TABLE "RagRetrievalTrace" ( + "id" TEXT NOT NULL PRIMARY KEY, + "tenantId" TEXT NOT NULL DEFAULT 'default', + "novelId" TEXT, + "worldId" TEXT, + "queryDigest" TEXT NOT NULL, + "queryPreview" TEXT, + "scopeJson" TEXT, + "candidateCounts" TEXT NOT NULL DEFAULT '{}', + "hitsJson" TEXT NOT NULL DEFAULT '[]', + "timingsJson" TEXT NOT NULL DEFAULT '{}', + "fallbackTriggered" BOOLEAN NOT NULL DEFAULT false, + "rerankerUsed" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX "RagRetrievalTrace_tenantId_createdAt_idx" ON "RagRetrievalTrace"("tenantId", "createdAt"); +CREATE INDEX "RagRetrievalTrace_tenantId_novelId_createdAt_idx" ON "RagRetrievalTrace"("tenantId", "novelId", "createdAt"); +CREATE INDEX "RagRetrievalTrace_queryDigest_idx" ON "RagRetrievalTrace"("queryDigest"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626170000_book_analysis_character_two_stage/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626170000_book_analysis_character_two_stage/migration.sql new file mode 100644 index 0000000000..de64e1c6a4 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260626170000_book_analysis_character_two_stage/migration.sql @@ -0,0 +1,63 @@ +PRAGMA foreign_keys=OFF; + +CREATE TABLE "new_BookAnalysisCharacter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "analysisId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "role" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'generated', + "briefDescription" TEXT, + "importance" TEXT, + "occurringChaptersJson" TEXT, + "lastGenerationError" TEXT, + "generationDepth" TEXT NOT NULL DEFAULT 'standard', + "selectedDimensionsJson" TEXT, + "profileJson" TEXT, + "evidenceJson" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "BookAnalysisCharacter_analysisId_fkey" FOREIGN KEY ("analysisId") REFERENCES "BookAnalysis" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +INSERT INTO "new_BookAnalysisCharacter" ( + "id", + "analysisId", + "name", + "role", + "status", + "generationDepth", + "selectedDimensionsJson", + "profileJson", + "evidenceJson", + "sortOrder", + "createdAt", + "updatedAt" +) +SELECT + "id", + "analysisId", + "name", + "role", + CASE + WHEN "profileJson" IS NOT NULL AND "profileJson" <> '{}' THEN 'generated' + ELSE 'candidate' + END, + "generationDepth", + "selectedDimensionsJson", + "profileJson", + "evidenceJson", + "sortOrder", + "createdAt", + "updatedAt" +FROM "BookAnalysisCharacter"; + +DROP TABLE "BookAnalysisCharacter"; +ALTER TABLE "new_BookAnalysisCharacter" RENAME TO "BookAnalysisCharacter"; + +CREATE INDEX "BookAnalysisCharacter_analysisId_sortOrder_idx" ON "BookAnalysisCharacter"("analysisId", "sortOrder"); +CREATE INDEX "BookAnalysisCharacter_analysisId_name_idx" ON "BookAnalysisCharacter"("analysisId", "name"); +CREATE INDEX "BookAnalysisCharacter_analysisId_status_idx" ON "BookAnalysisCharacter"("analysisId", "status"); + +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260627103000_book_analysis_character_depth_appearance/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260627103000_book_analysis_character_depth_appearance/migration.sql new file mode 100644 index 0000000000..f2736f1643 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260627103000_book_analysis_character_depth_appearance/migration.sql @@ -0,0 +1,76 @@ +ALTER TABLE "BookAnalysisCharacter" ADD COLUMN "depthMetadataJson" TEXT; +ALTER TABLE "BookAnalysisCharacter" ADD COLUMN "profileSectionsJson" TEXT; + +CREATE TABLE "BookAnalysisCharacterAppearance" ( + "id" TEXT NOT NULL PRIMARY KEY, + "characterId" TEXT NOT NULL, + "coveragePercent" INTEGER NOT NULL DEFAULT 0, + "consolidatedAppearanceJson" TEXT, + "variantPolicyJson" TEXT, + "lastIndexedChapterIndex" INTEGER, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BookAnalysisCharacterAppearance_characterId_fkey" + FOREIGN KEY ("characterId") REFERENCES "BookAnalysisCharacter" ("id") + ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "BookAnalysisCharacterAppearanceSnapshot" ( + "id" TEXT NOT NULL PRIMARY KEY, + "appearanceId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "chapterIndex" INTEGER NOT NULL, + "chapterTitle" TEXT, + "appearanceJson" TEXT, + "evidenceJson" TEXT, + "summaryCaption" TEXT, + "contextSceneRefsJson" TEXT, + "manuallyEdited" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BookAnalysisCharacterAppearanceSnapshot_appearanceId_fkey" + FOREIGN KEY ("appearanceId") REFERENCES "BookAnalysisCharacterAppearance" ("id") + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "BookAnalysisCharacterAppearanceSnapshot_characterId_fkey" + FOREIGN KEY ("characterId") REFERENCES "BookAnalysisCharacter" ("id") + ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "BookAnalysisCharacterAppearanceImage" ( + "id" TEXT NOT NULL PRIMARY KEY, + "snapshotId" TEXT NOT NULL, + "generationTaskId" TEXT, + "imageAssetId" TEXT, + "imagePromptJson" TEXT, + "referenceAssetIdsJson" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BookAnalysisCharacterAppearanceImage_snapshotId_fkey" + FOREIGN KEY ("snapshotId") REFERENCES "BookAnalysisCharacterAppearanceSnapshot" ("id") + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "BookAnalysisCharacterAppearanceImage_generationTaskId_fkey" + FOREIGN KEY ("generationTaskId") REFERENCES "ImageGenerationTask" ("id") + ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "BookAnalysisCharacterAppearanceImage_imageAssetId_fkey" + FOREIGN KEY ("imageAssetId") REFERENCES "ImageAsset" ("id") + ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "BookAnalysisCharacterAppearance_characterId_key" + ON "BookAnalysisCharacterAppearance"("characterId"); +CREATE INDEX "BookAnalysisCharacterAppearance_coveragePercent_idx" + ON "BookAnalysisCharacterAppearance"("coveragePercent"); +CREATE UNIQUE INDEX "BookAnalysisCharacterAppearanceSnapshot_characterId_chapterIndex_key" + ON "BookAnalysisCharacterAppearanceSnapshot"("characterId", "chapterIndex"); +CREATE INDEX "BookAnalysisCharacterAppearanceSnapshot_appearanceId_chapterIndex_idx" + ON "BookAnalysisCharacterAppearanceSnapshot"("appearanceId", "chapterIndex"); +CREATE INDEX "BookAnalysisCharacterAppearanceSnapshot_characterId_chapterIndex_idx" + ON "BookAnalysisCharacterAppearanceSnapshot"("characterId", "chapterIndex"); +CREATE INDEX "BookAnalysisCharacterAppearanceSnapshot_chapterIndex_idx" + ON "BookAnalysisCharacterAppearanceSnapshot"("chapterIndex"); +CREATE UNIQUE INDEX "BookAnalysisCharacterAppearanceImage_imageAssetId_key" + ON "BookAnalysisCharacterAppearanceImage"("imageAssetId"); +CREATE INDEX "BookAnalysisCharacterAppearanceImage_generationTaskId_idx" + ON "BookAnalysisCharacterAppearanceImage"("generationTaskId"); +CREATE INDEX "BookAnalysisCharacterAppearanceImage_snapshotId_idx" + ON "BookAnalysisCharacterAppearanceImage"("snapshotId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260628093000_book_analysis_appearance_terms/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260628093000_book_analysis_appearance_terms/migration.sql new file mode 100644 index 0000000000..5a33ecc3d9 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260628093000_book_analysis_appearance_terms/migration.sql @@ -0,0 +1,29 @@ +CREATE TABLE "BookAnalysisCharacterAppearanceTerm" ( + "id" TEXT NOT NULL PRIMARY KEY, + "characterId" TEXT NOT NULL, + "snapshotId" TEXT NOT NULL, + "chapterIndex" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "category" TEXT, + "confidence" REAL, + "stability" TEXT, + "evidenceJson" TEXT, + "status" TEXT NOT NULL DEFAULT 'pending', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "BookAnalysisCharacterAppearanceTerm_characterId_fkey" + FOREIGN KEY ("characterId") REFERENCES "BookAnalysisCharacter" ("id") + ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "BookAnalysisCharacterAppearanceTerm_snapshotId_fkey" + FOREIGN KEY ("snapshotId") REFERENCES "BookAnalysisCharacterAppearanceSnapshot" ("id") + ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "BookAnalysisCharacterAppearanceTerm_snapshotId_text_key" + ON "BookAnalysisCharacterAppearanceTerm"("snapshotId", "text"); +CREATE INDEX "BookAnalysisCharacterAppearanceTerm_characterId_status_updatedAt_idx" + ON "BookAnalysisCharacterAppearanceTerm"("characterId", "status", "updatedAt"); +CREATE INDEX "BookAnalysisCharacterAppearanceTerm_snapshotId_idx" + ON "BookAnalysisCharacterAppearanceTerm"("snapshotId"); +CREATE INDEX "BookAnalysisCharacterAppearanceTerm_chapterIndex_idx" + ON "BookAnalysisCharacterAppearanceTerm"("chapterIndex"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260628101500_image_task_reference_assets/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260628101500_image_task_reference_assets/migration.sql new file mode 100644 index 0000000000..b4f57410e1 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260628101500_image_task_reference_assets/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "ImageGenerationTask" ADD COLUMN "referenceImageAssetIdsJson" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260703173000_volume_chapter_conflict_level_source/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260703173000_volume_chapter_conflict_level_source/migration.sql new file mode 100644 index 0000000000..3aba3ad86a --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260703173000_volume_chapter_conflict_level_source/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "VolumeChapterPlan" ADD COLUMN "conflictLevelSource" TEXT; + diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260707103000_prompt_template_overrides/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260707103000_prompt_template_overrides/migration.sql new file mode 100644 index 0000000000..c91089c4c9 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260707103000_prompt_template_overrides/migration.sql @@ -0,0 +1,33 @@ +-- Additive tables for book-scoped advanced prompt templates. + +CREATE TABLE "PromptTemplateOverride" ( + "id" TEXT NOT NULL PRIMARY KEY, + "scope" TEXT NOT NULL DEFAULT 'novel', + "novelId" TEXT NOT NULL, + "promptId" TEXT NOT NULL, + "basePromptVersion" TEXT NOT NULL, + "mode" TEXT NOT NULL DEFAULT 'official', + "activeVersionId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "PromptTemplateOverride_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "PromptTemplateVersion" ( + "id" TEXT NOT NULL PRIMARY KEY, + "overrideId" TEXT NOT NULL, + "versionNo" INTEGER NOT NULL, + "templateJson" TEXT NOT NULL, + "contextRefsJson" TEXT NOT NULL DEFAULT '{}', + "compiledHash" TEXT NOT NULL, + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "PromptTemplateVersion_overrideId_fkey" FOREIGN KEY ("overrideId") REFERENCES "PromptTemplateOverride"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "PromptTemplateOverride_scope_novelId_promptId_key" ON "PromptTemplateOverride"("scope", "novelId", "promptId"); +CREATE INDEX "PromptTemplateOverride_promptId_idx" ON "PromptTemplateOverride"("promptId"); +CREATE INDEX "PromptTemplateOverride_novelId_promptId_idx" ON "PromptTemplateOverride"("novelId", "promptId"); +CREATE INDEX "PromptTemplateOverride_activeVersionId_idx" ON "PromptTemplateOverride"("activeVersionId"); +CREATE UNIQUE INDEX "PromptTemplateVersion_overrideId_versionNo_key" ON "PromptTemplateVersion"("overrideId", "versionNo"); +CREATE INDEX "PromptTemplateVersion_overrideId_createdAt_idx" ON "PromptTemplateVersion"("overrideId", "createdAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713143000_character_mind_snapshots/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713143000_character_mind_snapshots/migration.sql new file mode 100644 index 0000000000..e66234ef39 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713143000_character_mind_snapshots/migration.sql @@ -0,0 +1,29 @@ +-- Non-canonical, evidence-backed character mind snapshots. + +CREATE TABLE "CharacterMindSnapshot" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "sourceChapterId" TEXT, + "sourceType" TEXT NOT NULL, + "currentInterpretation" TEXT NOT NULL, + "privateIntent" TEXT, + "activePlan" TEXT, + "emotionalStance" TEXT, + "actionTendency" TEXT, + "decisionTrigger" TEXT, + "beliefsJson" TEXT NOT NULL DEFAULT '[]', + "misbeliefsJson" TEXT NOT NULL DEFAULT '[]', + "evidenceJson" TEXT NOT NULL DEFAULT '[]', + "confidence" REAL, + "isCurrent" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterMindSnapshot_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterMindSnapshot_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterMindSnapshot_sourceChapterId_fkey" FOREIGN KEY ("sourceChapterId") REFERENCES "Chapter"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX "CharacterMindSnapshot_novelId_characterId_isCurrent_updatedAt_idx" + ON "CharacterMindSnapshot"("novelId", "characterId", "isCurrent", "updatedAt"); +CREATE INDEX "CharacterMindSnapshot_sourceChapterId_idx" ON "CharacterMindSnapshot"("sourceChapterId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713210000_character_influence_proposals/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713210000_character_influence_proposals/migration.sql new file mode 100644 index 0000000000..7dc78a3f09 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713210000_character_influence_proposals/migration.sql @@ -0,0 +1,37 @@ +CREATE TABLE "CharacterInfluenceProposal" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "proposalSetId" TEXT NOT NULL, + "sourceMindSnapshotId" TEXT, + "title" TEXT NOT NULL, + "directionSummary" TEXT NOT NULL, + "recommendationReason" TEXT NOT NULL, + "isRecommended" BOOLEAN NOT NULL DEFAULT false, + "behaviorGuidance" TEXT NOT NULL, + "emotionalGuidance" TEXT, + "relationTension" TEXT, + "readerPayoff" TEXT NOT NULL, + "risk" TEXT NOT NULL, + "observableSignalsJson" TEXT NOT NULL DEFAULT '[]', + "evidenceJson" TEXT NOT NULL DEFAULT '[]', + "confidence" REAL, + "authorIntent" TEXT, + "targetStartChapterOrder" INTEGER NOT NULL, + "targetEndChapterOrder" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "acceptedAt" DATETIME, + "appliedAt" DATETIME, + "resolvedChapterId" TEXT, + "resolutionEvidenceJson" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterInfluenceProposal_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterInfluenceProposal_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterInfluenceProposal_sourceMindSnapshotId_fkey" FOREIGN KEY ("sourceMindSnapshotId") REFERENCES "CharacterMindSnapshot"("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterInfluenceProposal_resolvedChapterId_fkey" FOREIGN KEY ("resolvedChapterId") REFERENCES "Chapter"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX "CharacterInfluenceProposal_novelId_characterId_status_targetStartChapterOrder_targetEndChapterOrder_idx" ON "CharacterInfluenceProposal"("novelId", "characterId", "status", "targetStartChapterOrder", "targetEndChapterOrder"); +CREATE INDEX "CharacterInfluenceProposal_proposalSetId_idx" ON "CharacterInfluenceProposal"("proposalSetId"); +CREATE INDEX "CharacterInfluenceProposal_resolvedChapterId_idx" ON "CharacterInfluenceProposal"("resolvedChapterId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713223000_character_dialogue_layer/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713223000_character_dialogue_layer/migration.sql new file mode 100644 index 0000000000..0714a42ccd --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260713223000_character_dialogue_layer/migration.sql @@ -0,0 +1,56 @@ +CREATE TABLE "CharacterDialogueSession" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "sourceMindSnapshotId" TEXT, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterDialogueSession_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueSession_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueSession_sourceMindSnapshotId_fkey" FOREIGN KEY ("sourceMindSnapshotId") REFERENCES "CharacterMindSnapshot"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE TABLE "CharacterDialogueTurn" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sessionId" TEXT NOT NULL, + "role" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "CharacterDialogueTurn_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "CharacterDialogueSession"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "CharacterDialogueInfluence" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "sourceMindSnapshotId" TEXT, + "summary" TEXT NOT NULL, + "behaviorGuidance" TEXT NOT NULL, + "emotionalGuidance" TEXT, + "relationTension" TEXT, + "evidenceJson" TEXT NOT NULL DEFAULT '[]', + "confidence" REAL, + "targetStartChapterOrder" INTEGER NOT NULL, + "targetEndChapterOrder" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "activatedAt" DATETIME, + "appliedAt" DATETIME, + "resolvedChapterId" TEXT, + "resolutionEvidenceJson" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterDialogueInfluence_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "CharacterDialogueSession"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_sourceMindSnapshotId_fkey" FOREIGN KEY ("sourceMindSnapshotId") REFERENCES "CharacterMindSnapshot"("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_resolvedChapterId_fkey" FOREIGN KEY ("resolvedChapterId") REFERENCES "Chapter"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX "CharacterDialogueSession_novelId_characterId_status_updatedAt_idx" ON "CharacterDialogueSession"("novelId", "characterId", "status", "updatedAt"); +CREATE INDEX "CharacterDialogueSession_sourceMindSnapshotId_idx" ON "CharacterDialogueSession"("sourceMindSnapshotId"); +CREATE INDEX "CharacterDialogueTurn_sessionId_createdAt_idx" ON "CharacterDialogueTurn"("sessionId", "createdAt"); +CREATE INDEX "CharacterDialogueInfluence_novelId_characterId_status_targetStartChapterOrder_targetEndChapterOrder_idx" ON "CharacterDialogueInfluence"("novelId", "characterId", "status", "targetStartChapterOrder", "targetEndChapterOrder"); +CREATE INDEX "CharacterDialogueInfluence_sessionId_createdAt_idx" ON "CharacterDialogueInfluence"("sessionId", "createdAt"); +CREATE INDEX "CharacterDialogueInfluence_resolvedChapterId_idx" ON "CharacterDialogueInfluence"("resolvedChapterId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260714110000_universal_character_conversation/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260714110000_universal_character_conversation/migration.sql new file mode 100644 index 0000000000..bc1500db9e --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260714110000_universal_character_conversation/migration.sql @@ -0,0 +1,76 @@ +PRAGMA foreign_keys=OFF; + +CREATE TABLE "CharacterConversationSession" ( + "id" TEXT NOT NULL PRIMARY KEY, + "subjectKind" TEXT NOT NULL, + "subjectId" TEXT NOT NULL, + "scopeKind" TEXT NOT NULL, + "scopeId" TEXT, + "interactionPolicy" TEXT NOT NULL, + "chapterAnchor" INTEGER, + "sourceSnapshotJson" TEXT NOT NULL DEFAULT '{}', + "evidenceBoundaryJson" TEXT NOT NULL DEFAULT '[]', + "legacyDialogueSessionId" TEXT, + "status" TEXT NOT NULL DEFAULT 'active', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE TABLE "CharacterConversationTurn" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sessionId" TEXT NOT NULL, + "role" TEXT NOT NULL, + "content" TEXT NOT NULL, + "evidenceJson" TEXT NOT NULL DEFAULT '[]', + "uncertainty" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "CharacterConversationTurn_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "CharacterConversationSession"("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE TABLE "new_CharacterDialogueInfluence" ( + "id" TEXT NOT NULL PRIMARY KEY, + "novelId" TEXT NOT NULL, + "characterId" TEXT NOT NULL, + "sessionId" TEXT, + "conversationSessionId" TEXT, + "sourceMindSnapshotId" TEXT, + "summary" TEXT NOT NULL, + "behaviorGuidance" TEXT NOT NULL, + "emotionalGuidance" TEXT, + "relationTension" TEXT, + "evidenceJson" TEXT NOT NULL DEFAULT '[]', + "confidence" REAL, + "targetStartChapterOrder" INTEGER NOT NULL, + "targetEndChapterOrder" INTEGER NOT NULL, + "status" TEXT NOT NULL DEFAULT 'draft', + "activatedAt" DATETIME, + "appliedAt" DATETIME, + "resolvedChapterId" TEXT, + "resolutionEvidenceJson" TEXT NOT NULL DEFAULT '[]', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CharacterDialogueInfluence_novelId_fkey" FOREIGN KEY ("novelId") REFERENCES "Novel"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "Character"("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "CharacterDialogueSession"("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_conversationSessionId_fkey" FOREIGN KEY ("conversationSessionId") REFERENCES "CharacterConversationSession"("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_sourceMindSnapshotId_fkey" FOREIGN KEY ("sourceMindSnapshotId") REFERENCES "CharacterMindSnapshot"("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "CharacterDialogueInfluence_resolvedChapterId_fkey" FOREIGN KEY ("resolvedChapterId") REFERENCES "Chapter"("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +INSERT INTO "new_CharacterDialogueInfluence" ("id", "novelId", "characterId", "sessionId", "sourceMindSnapshotId", "summary", "behaviorGuidance", "emotionalGuidance", "relationTension", "evidenceJson", "confidence", "targetStartChapterOrder", "targetEndChapterOrder", "status", "activatedAt", "appliedAt", "resolvedChapterId", "resolutionEvidenceJson", "createdAt", "updatedAt") +SELECT "id", "novelId", "characterId", "sessionId", "sourceMindSnapshotId", "summary", "behaviorGuidance", "emotionalGuidance", "relationTension", "evidenceJson", "confidence", "targetStartChapterOrder", "targetEndChapterOrder", "status", "activatedAt", "appliedAt", "resolvedChapterId", "resolutionEvidenceJson", "createdAt", "updatedAt" +FROM "CharacterDialogueInfluence"; + +DROP TABLE "CharacterDialogueInfluence"; +ALTER TABLE "new_CharacterDialogueInfluence" RENAME TO "CharacterDialogueInfluence"; + +CREATE UNIQUE INDEX "CharacterConversationSession_legacyDialogueSessionId_key" ON "CharacterConversationSession"("legacyDialogueSessionId"); +CREATE INDEX "CharacterConversationSession_subjectKind_subjectId_scopeKind_scopeId_status_updatedAt_idx" ON "CharacterConversationSession"("subjectKind", "subjectId", "scopeKind", "scopeId", "status", "updatedAt"); +CREATE INDEX "CharacterConversationSession_scopeKind_scopeId_updatedAt_idx" ON "CharacterConversationSession"("scopeKind", "scopeId", "updatedAt"); +CREATE INDEX "CharacterConversationTurn_sessionId_createdAt_idx" ON "CharacterConversationTurn"("sessionId", "createdAt"); +CREATE INDEX "CharacterDialogueInfluence_novelId_characterId_status_targetStartChapterOrder_targetEndChapterOrder_idx" ON "CharacterDialogueInfluence"("novelId", "characterId", "status", "targetStartChapterOrder", "targetEndChapterOrder"); +CREATE INDEX "CharacterDialogueInfluence_sessionId_createdAt_idx" ON "CharacterDialogueInfluence"("sessionId", "createdAt"); +CREATE INDEX "CharacterDialogueInfluence_conversationSessionId_createdAt_idx" ON "CharacterDialogueInfluence"("conversationSessionId", "createdAt"); +CREATE INDEX "CharacterDialogueInfluence_resolvedChapterId_idx" ON "CharacterDialogueInfluence"("resolvedChapterId"); + +PRAGMA foreign_keys=ON; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260714120000_visual_asset_catalog/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260714120000_visual_asset_catalog/migration.sql new file mode 100644 index 0000000000..eb8e9f7e2a --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260714120000_visual_asset_catalog/migration.sql @@ -0,0 +1,39 @@ +CREATE TABLE "VisualAssetProjection" ( + "id" TEXT NOT NULL PRIMARY KEY, + "sourceDomain" TEXT NOT NULL, + "sourceType" TEXT NOT NULL, + "sourceId" TEXT NOT NULL, + "sourceVersion" TEXT NOT NULL DEFAULT 'current', + "sourceLabel" TEXT NOT NULL, + "scopeKind" TEXT NOT NULL DEFAULT 'global', + "scopeId" TEXT, + "scopeLabel" TEXT, + "kind" TEXT NOT NULL, + "origin" TEXT NOT NULL DEFAULT 'generated', + "url" TEXT NOT NULL, + "thumbnailUrl" TEXT, + "mimeType" TEXT, + "width" INTEGER, + "height" INTEGER, + "prompt" TEXT, + "provider" TEXT, + "model" TEXT, + "isPrimary" BOOLEAN NOT NULL DEFAULT false, + "metadata" TEXT, + "sourceCreatedAt" DATETIME NOT NULL, + "lastSeenAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +CREATE UNIQUE INDEX "VisualAssetProjection_sourceDomain_sourceType_sourceId_sourceVersion_key" +ON "VisualAssetProjection"("sourceDomain", "sourceType", "sourceId", "sourceVersion"); + +CREATE INDEX "VisualAssetProjection_scopeKind_scopeId_sourceCreatedAt_idx" +ON "VisualAssetProjection"("scopeKind", "scopeId", "sourceCreatedAt"); + +CREATE INDEX "VisualAssetProjection_kind_sourceCreatedAt_idx" +ON "VisualAssetProjection"("kind", "sourceCreatedAt"); + +CREATE INDEX "VisualAssetProjection_sourceDomain_sourceCreatedAt_idx" +ON "VisualAssetProjection"("sourceDomain", "sourceCreatedAt"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260728110000_simple_creation_experience/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260728110000_simple_creation_experience/migration.sql new file mode 100644 index 0000000000..0da0279f81 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260728110000_simple_creation_experience/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Novel" +ADD COLUMN "creationExperience" TEXT NOT NULL DEFAULT 'professional'; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803100000_comic_character_gender/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803100000_comic_character_gender/migration.sql new file mode 100644 index 0000000000..5d2b356530 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803100000_comic_character_gender/migration.sql @@ -0,0 +1,4 @@ +-- 补充 ComicCharacter.gender 列(20260402 的 character_gender_fields 迁移早于 +-- 20260612 add_comic_module 建表,漏掉了该列;此处对齐 schema.prisma) +ALTER TABLE "ComicCharacter" +ADD COLUMN "gender" TEXT NOT NULL DEFAULT 'unknown'; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803110000_comic_missing_tables/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803110000_comic_missing_tables/migration.sql new file mode 100644 index 0000000000..8f7ede6ab6 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803110000_comic_missing_tables/migration.sql @@ -0,0 +1,35 @@ +-- 补充 ComicCharacterAsset / ComicScene 建表(20260612 add_comic_module 迁移 +-- 漏建这两张表;schema.prisma 已有定义,此处对齐,修复迁移建库环境报 +-- "table does not exist") +CREATE TABLE "ComicCharacterAsset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "characterId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "assetType" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "imageData" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicCharacterAsset_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "ComicCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ComicCharacterAsset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "ComicCharacterAsset_characterId_idx" ON "ComicCharacterAsset"("characterId"); +CREATE INDEX "ComicCharacterAsset_projectId_idx" ON "ComicCharacterAsset"("projectId"); + +CREATE TABLE "ComicScene" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "sceneType" TEXT NOT NULL DEFAULT 'interior', + "bible" TEXT, + "sheetData" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicScene_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "ComicScene_projectId_idx" ON "ComicScene"("projectId"); diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803120000_comic_panel_sceneref/migration.sql b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803120000_comic_panel_sceneref/migration.sql new file mode 100644 index 0000000000..18361958a8 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/20260803120000_comic_panel_sceneref/migration.sql @@ -0,0 +1,4 @@ +-- 补充 ComicPanel.sceneRef 列(schema.prisma 已定义,但 add_comic_module 建表 +-- 时未包含,后续也无迁移补齐;此处对齐 schema,修复迁移建库环境报 +-- "column does not exist") +ALTER TABLE "ComicPanel" ADD COLUMN "sceneRef" TEXT; diff --git a/android-app/app/src/main/assets/nodejs/migrations.sqlite/migration_lock.toml b/android-app/app/src/main/assets/nodejs/migrations.sqlite/migration_lock.toml new file mode 100644 index 0000000000..2a5a44419d --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/migrations.sqlite/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/android-app/app/src/main/assets/nodejs/node-project.zip b/android-app/app/src/main/assets/nodejs/node-project.zip new file mode 100644 index 0000000000..12c260dc4a Binary files /dev/null and b/android-app/app/src/main/assets/nodejs/node-project.zip differ diff --git a/android-app/app/src/main/assets/nodejs/package.json b/android-app/app/src/main/assets/nodejs/package.json new file mode 100644 index 0000000000..653832cef2 --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/package.json @@ -0,0 +1,78 @@ +{ + "name": "@ai-novel/server", + "version": "0.1.0", + "private": true, + "license": "AGPL-3.0-only", + "main": "dist/app.js", + "files": [ + "dist", + "src/prisma/migrations", + "src/prisma/migrations.sqlite", + "src/prisma/schema.prisma", + "src/prisma/schema.sqlite.prisma" + ], + "prisma": { + "seed": "ts-node-dev --transpile-only src/db/seed.ts" + }, + "scripts": { + "dev": "pnpm run dev:api", + "dev:api": "node scripts/ensure-dev-prisma.cjs && node scripts/stop-stale-dev-server.cjs && ts-node-dev --respawn --transpile-only src/app.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/app.js", + "start:api": "node dist/app.js", + "typecheck": "pnpm prisma:generate && tsc -p tsconfig.json --noEmit", + "lint": "tsc -p tsconfig.json --noEmit", + "test": "pnpm --filter @ai-novel/shared build && pnpm run build && pnpm run test:node", + "test:node": "node scripts/run-tests.cjs fast", + "test:integration": "pnpm --filter @ai-novel/shared build && pnpm run build && node scripts/run-tests.cjs integration", + "test:all": "pnpm run test && pnpm run test:integration", + "test:planner": "pnpm --filter @ai-novel/shared build && pnpm run build && node --test tests/planner.test.js", + "test:tools": "pnpm --filter @ai-novel/shared build && pnpm run build && node --test tests/tools.test.js", + "test:runtime": "pnpm --filter @ai-novel/shared build && pnpm run build && node --test tests/runtime.test.js", + "test:routes": "pnpm --filter @ai-novel/shared build && pnpm run build && node --test tests/routes.test.js", + "test:book-analysis": "pnpm --filter @ai-novel/shared build && pnpm run build && node --test tests/bookAnalysis.test.js", + "rag:eval": "node scripts/rag-retrieval-eval.cjs", + "audit:director-recovery-samples": "node scripts/director-recovery-sample-audit.cjs", + "backfill:director-draft-baselines": "node scripts/director-chapter-draft-baseline-backfill.cjs", + "prisma": "prisma", + "prisma:generate": "prisma generate --config prisma.config.ts", + "prisma:push": "prisma db push --config prisma.config.ts", + "prisma:deploy": "prisma migrate deploy --config prisma.config.ts", + "prisma:migrate": "prisma migrate dev --name init --config prisma.config.ts", + "db:migrate": "pnpm prisma:migrate", + "db:seed": "prisma db seed --config prisma.config.ts", + "db:studio": "prisma studio --config prisma.config.ts", + "db:restore": "node scripts/restore-dev-data.cjs", + "db:prune-snapshots": "node scripts/prune-snapshots.cjs" + }, + "dependencies": { + "@ai-novel/shared": "file:../shared", + "@aws-sdk/client-s3": "^3.910.0", + "@langchain/community": "^1.1.21", + "@langchain/core": "^1.1.29", + "@langchain/langgraph": "^1.2.0", + "@langchain/openai": "^1.2.11", + "@prisma/adapter-better-sqlite3": "^7.4.2", + "@prisma/adapter-pg": "7.4.2", + "@prisma/client": "^7.4.2", + "better-sqlite3": "^12.6.2", + "cors": "^2.8.6", + "dotenv": "^17.3.1", + "express": "^5.2.1", + "helmet": "^8.1.0", + "langchain": "^1.2.28", + "morgan": "^1.10.1", + "sharp": "^0.35.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", + "@types/node": "^25.3.3", + "prisma": "^7.4.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.9.3" + } +} \ No newline at end of file diff --git a/android-app/app/src/main/assets/nodejs/prisma-schema.sqlite.prisma b/android-app/app/src/main/assets/nodejs/prisma-schema.sqlite.prisma new file mode 100644 index 0000000000..299ba2885b --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/prisma-schema.sqlite.prisma @@ -0,0 +1,3834 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" +} + +enum ChapterGenerationState { + planned + drafted + reviewed + repaired + approved + published +} + +enum PipelineJobStatus { + queued + running + succeeded + failed + cancelled +} + +enum BeatStatus { + planned + completed + skipped +} + +enum FactCategory { + world + character + timeline + plot + rule +} + +enum RagOwnerType { + novel + chapter + world + character + bible + chapter_summary + consistency_fact + character_timeline + world_library_item + knowledge_document + chat_message +} + +enum RagJobType { + upsert + delete + rebuild +} + +enum RagJobStatus { + queued + running + succeeded + failed + cancelled +} + +enum KnowledgeDocumentStatus { + enabled + disabled + archived +} + +enum KnowledgeDocumentKind { + user_upload + analysis_published +} + +enum KnowledgeIndexStatus { + idle + queued + running + succeeded + failed +} + +enum KnowledgeBindingTargetType { + novel + world +} + +enum BookAnalysisStatus { + draft + queued + running + succeeded + failed + cancelled + archived +} + +enum BookAnalysisSectionStatus { + idle + running + succeeded + failed +} + +enum ImageSceneType { + character + novel_cover + chapter_illustration + book_analysis_character +} + +enum ProjectMode { + ai_led + co_pilot + draft_mode + auto_pipeline +} + +enum CreationExperience { + simple + professional +} + +enum NarrativePov { + first_person + third_person + mixed +} + +enum PacePreference { + slow + balanced + fast +} + +enum EmotionIntensity { + low + medium + high +} + +enum AIFreedom { + low + medium + high +} + +enum ProjectProgressStatus { + not_started + in_progress + completed + rework + blocked +} + +enum StorylineVersionStatus { + draft + active + frozen +} + +enum VolumePlanVersionStatus { + draft + active + frozen +} + +enum ChapterStatus { + unplanned + pending_generation + generating + pending_review + needs_repair + completed +} + +enum PipelineRunMode { + fast + polish +} + +enum PipelineRepairMode { + detect_only + light_repair + heavy_repair + continuity_only + character_only + ending_only +} + +enum AgentRunStatus { + queued + running + waiting_approval + succeeded + failed + cancelled +} + +enum NovelWorkflowLane { + manual_create + auto_director +} + +enum NovelWorkflowTaskStatus { + queued + running + waiting_approval + succeeded + failed + cancelled +} + +enum AgentStepType { + planning + tool_call + tool_result + reasoning + write + approval + answer +} + +enum AgentStepStatus { + pending + running + succeeded + failed + cancelled +} + +enum AgentApprovalStatus { + pending + approved + rejected + expired +} + +enum CreativeHubThreadStatus { + idle + busy + interrupted + error +} + +enum StoryPlanLevel { + book + arc + chapter +} + +enum StoryPlanRole { + setup + progress + pressure + turn + payoff + cooldown +} + +enum AuditType { + continuity + character + plot + mode_fit +} + +enum AuditIssueStatus { + open + resolved + ignored +} + +enum PayoffLedgerScopeType { + book + volume + chapter +} + +enum PayoffLedgerStatus { + setup + hinted + pending_payoff + paid_off + failed + overdue +} + +enum StyleBindingTargetType { + novel + chapter + task +} + +enum AntiAiRuleType { + forbidden + risk + encourage +} + +enum AntiAiSeverity { + low + medium + high +} + +enum CharacterGender { + male + female + other + unknown +} + +model Novel { + id String @id @default(cuid()) + title String + description String? + targetAudience String? + bookSellingPoint String? + competingFeel String? + first30ChapterPromise String? + commercialTagsJson String? + status String @default("draft") + writingMode String @default("original") + projectMode ProjectMode? + creationExperience CreationExperience @default(professional) + narrativePov NarrativePov? + pacePreference PacePreference? + styleTone String? + emotionIntensity EmotionIntensity? + aiFreedom AIFreedom? + postGenerationStyleReviewEnabled Boolean @default(true) + defaultChapterLength Int? + estimatedChapterCount Int? + projectStatus ProjectProgressStatus? @default(not_started) + storylineStatus ProjectProgressStatus? @default(not_started) + outlineStatus ProjectProgressStatus? @default(not_started) + resourceReadyScore Int? + sourceNovelId String? + sourceKnowledgeDocumentId String? + continuationBookAnalysisId String? + continuationBookAnalysisSections String? + outline String? + structuredOutline String? + storyWorldSliceJson String? + storyWorldSliceOverridesJson String? + storyWorldSliceSchemaVersion Int @default(1) + genreId String? + primaryStoryModeId String? + secondaryStoryModeId String? + worldId String? + genre NovelGenre? @relation(fields: [genreId], references: [id]) + primaryStoryMode NovelStoryMode? @relation("NovelPrimaryStoryMode", fields: [primaryStoryModeId], references: [id], onDelete: SetNull) + secondaryStoryMode NovelStoryMode? @relation("NovelSecondaryStoryMode", fields: [secondaryStoryModeId], references: [id], onDelete: SetNull) + world World? @relation(fields: [worldId], references: [id], onDelete: SetNull) + sourceNovel Novel? @relation("NovelContinuation", fields: [sourceNovelId], references: [id], onDelete: SetNull) + continuationNovels Novel[] @relation("NovelContinuation") + sourceKnowledgeDocument KnowledgeDocument? @relation("NovelKnowledgeContinuation", fields: [sourceKnowledgeDocumentId], references: [id], onDelete: SetNull) + continuationBookAnalysis BookAnalysis? @relation("NovelContinuationBookAnalysis", fields: [continuationBookAnalysisId], references: [id], onDelete: SetNull) + chapters Chapter[] + characters Character[] + characterLibraryLinks CharacterLibraryLink[] + characterSyncProposals CharacterSyncProposal[] + characterRelations CharacterRelation[] + characterCastOptions CharacterCastOption[] + characterTimelines CharacterTimeline[] + characterCandidates CharacterCandidate[] + characterVolumeAssignments CharacterVolumeAssignment[] + characterFactionTracks CharacterFactionTrack[] + characterRelationStages CharacterRelationStage[] + characterMindSnapshots CharacterMindSnapshot[] + characterInfluenceProposals CharacterInfluenceProposal[] + characterDialogueSessions CharacterDialogueSession[] + characterDialogueInfluences CharacterDialogueInfluence[] + characterResourceItems CharacterResourceLedgerItem[] + characterResourceEvents CharacterResourceEvent[] + bible NovelBible? + plotBeats PlotBeat[] + chapterSummaries ChapterSummary[] + facts ConsistencyFact[] + generationJobs GenerationJob[] + agentRuns AgentRun[] + creativeDecisions CreativeDecision[] + novelSnapshots NovelSnapshot[] + storylineVersions StorylineVersion[] + volumePlans VolumePlan[] + volumePlanVersions VolumePlanVersion[] + qualityReports QualityReport[] + storyMacroPlan StoryMacroPlan? + bookContract BookContract? + storyStateSnapshots StoryStateSnapshot[] + canonicalStateVersions CanonicalStateVersion[] + stateChangeProposals StateChangeProposal[] + openConflicts OpenConflict[] + payoffLedgerItems PayoffLedgerItem[] + artifactSyncCheckpoints ChapterArtifactSyncCheckpoint[] + storyPlans StoryPlan[] + replanRuns ReplanRun[] + auditReports AuditReport[] + workflowTasks NovelWorkflowTask[] + autoApprovalRecords AutoDirectorAutoApprovalRecord[] + directorRuns DirectorRun[] + directorCommands DirectorRunCommand[] + directorEvents DirectorEvent[] + directorArtifacts DirectorArtifact[] + directorLlmUsageRecords DirectorLlmUsageRecord[] + promptAddendums PromptAddendum[] + promptSlotOverrides PromptSlotOverride[] + promptTemplateOverrides PromptTemplateOverride[] + imageTasks ImageGenerationTask[] + imageAssets ImageAsset[] + sideEffectJobs NovelSideEffectJob[] + novelWorld NovelWorld? + factEntries NovelFactEntry[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([genreId]) + @@index([primaryStoryModeId]) + @@index([secondaryStoryModeId]) + @@index([worldId]) + @@index([writingMode]) + @@index([sourceNovelId]) + @@index([sourceKnowledgeDocumentId]) + @@index([continuationBookAnalysisId]) +} + +model PromptAddendum { + id String @id @default(cuid()) + scope String + novelId String? + promptId String + title String + content String + enabled Boolean @default(true) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([scope, promptId, enabled]) + @@index([novelId, promptId, enabled]) + @@index([updatedAt]) +} + +model PromptSlotOverride { + id String @id @default(cuid()) + scope String + novelId String? + promptId String + baseVersion String + slots String @default("{}") + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([scope, novelId, promptId]) + @@index([promptId]) + @@index([novelId, promptId]) +} + +model PromptTemplateOverride { + id String @id @default(cuid()) + scope String @default("novel") + novelId String + promptId String + basePromptVersion String + mode String @default("official") + activeVersionId String? + versions PromptTemplateVersion[] + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([scope, novelId, promptId]) + @@index([promptId]) + @@index([novelId, promptId]) + @@index([activeVersionId]) +} + +model PromptTemplateVersion { + id String @id @default(cuid()) + overrideId String + versionNo Int + templateJson String + contextRefsJson String @default("{}") + compiledHash String + notes String? + override PromptTemplateOverride @relation(fields: [overrideId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([overrideId, versionNo]) + @@index([overrideId, createdAt]) +} + +model CreativeDecision { + id String @id @default(cuid()) + novelId String + chapterId String? + category String + content String + importance String @default("normal") + expiresAt Int? + sourceType String? + sourceRefId String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, createdAt]) +} + +model NovelSnapshot { + id String @id @default(cuid()) + novelId String + label String? + snapshotData String + triggerType String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) +} + +model Chapter { + id String @id @default(cuid()) + title String + content String? @default("") + order Int + generationState ChapterGenerationState @default(planned) + chapterStatus ChapterStatus? @default(unplanned) + targetWordCount Int? + conflictLevel Int? + revealLevel Int? + mustAvoid String? + taskSheet String? + sceneCards String? + repairHistory String? + qualityScore Int? + continuityScore Int? + characterScore Int? + pacingScore Int? + riskFlags String? + hook String? + expectation String? + novelId String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapterSummary ChapterSummary? + characterTimelines CharacterTimeline[] + characterCandidates CharacterCandidate[] @relation("CharacterCandidateSourceChapter") + characterFactionTracks CharacterFactionTrack[] + characterRelationStages CharacterRelationStage[] @relation("CharacterRelationStageChapter") + characterMindSnapshots CharacterMindSnapshot[] + resolvedCharacterInfluenceProposals CharacterInfluenceProposal[] @relation("CharacterInfluenceResolvedChapter") + resolvedCharacterDialogueInfluences CharacterDialogueInfluence[] @relation("CharacterDialogueInfluenceResolvedChapter") + facts ConsistencyFact[] + qualityReports QualityReport[] + storyStateSnapshots StoryStateSnapshot[] + canonicalStateVersions CanonicalStateVersion[] + stateChangeProposals StateChangeProposal[] + openConflicts OpenConflict[] + storyPlans StoryPlan[] + replanRuns ReplanRun[] + auditReports AuditReport[] + foreshadowSetupStates ForeshadowState[] @relation("ForeshadowSetupChapter") + foreshadowPayoffStates ForeshadowState[] @relation("ForeshadowPayoffChapter") + payoffLedgerLastTouchedItems PayoffLedgerItem[] @relation("PayoffLedgerLastTouchedChapter") + payoffLedgerSetupItems PayoffLedgerItem[] @relation("PayoffLedgerSetupChapter") + payoffLedgerPayoffItems PayoffLedgerItem[] @relation("PayoffLedgerPayoffChapter") + introducedCharacterResources CharacterResourceLedgerItem[] @relation("CharacterResourceIntroducedChapter") + lastTouchedCharacterResources CharacterResourceLedgerItem[] @relation("CharacterResourceLastTouchedChapter") + characterResourceEvents CharacterResourceEvent[] + artifactSyncCheckpoints ChapterArtifactSyncCheckpoint[] + volumeChapterPlans VolumeChapterPlan[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, order]) +} + +model ChapterArtifactSyncCheckpoint { + id String @id @default(cuid()) + novelId String + chapterId String + contentHash String + artifactType String + syncMode String + status String @default("succeeded") + sourceType String? + sourceStage String? + metadataJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, chapterId, contentHash, artifactType, syncMode]) + @@index([novelId, chapterId, artifactType, updatedAt]) +} + +model Character { + id String @id @default(cuid()) + name String + role String + gender CharacterGender @default(unknown) + castRole String? + storyFunction String? + relationToProtagonist String? + personality String? + background String? + development String? + identityLabel String? + factionLabel String? + stanceLabel String? + powerLevel String? + realm String? + currentLocation String? + availability String? + prohibitionsJson String @default("[]") + outerGoal String? + innerNeed String? + fear String? + wound String? + misbelief String? + secret String? + moralLine String? + firstImpression String? + appearance String? + physique String? + attireStyle String? + signatureDetail String? + voiceTexture String? + presenceImpression String? + arcStart String? + arcMidpoint String? + arcClimax String? + arcEnd String? + currentState String? + currentGoal String? + lastEvolvedAt DateTime? + novelId String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + baseCharacterId String? + libraryLink CharacterLibraryLink? + timelines CharacterTimeline[] + matchedCandidates CharacterCandidate[] @relation("CharacterCandidateMatchedCharacter") + volumeAssignments CharacterVolumeAssignment[] + factionTracks CharacterFactionTrack[] + outgoingRelationStages CharacterRelationStage[] @relation("CharacterRelationStageSource") + incomingRelationStages CharacterRelationStage[] @relation("CharacterRelationStageTarget") + mindSnapshots CharacterMindSnapshot[] + influenceProposals CharacterInfluenceProposal[] + dialogueSessions CharacterDialogueSession[] + dialogueInfluences CharacterDialogueInfluence[] + stateEntries CharacterState[] + outgoingRelations RelationState[] @relation("RelationStateSource") + incomingRelations RelationState[] @relation("RelationStateTarget") + outgoingCharacterRelations CharacterRelation[] @relation("CharacterRelationSource") + incomingCharacterRelations CharacterRelation[] @relation("CharacterRelationTarget") + ownedResourceItems CharacterResourceLedgerItem[] @relation("CharacterResourceOwner") + heldResourceItems CharacterResourceLedgerItem[] @relation("CharacterResourceHolder") + actedResourceEvents CharacterResourceEvent[] @relation("CharacterResourceEventActor") + fromResourceEvents CharacterResourceEvent[] @relation("CharacterResourceEventFromHolder") + toResourceEvents CharacterResourceEvent[] @relation("CharacterResourceEventToHolder") + syncProposals CharacterSyncProposal[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([baseCharacterId]) +} + +model CharacterMindSnapshot { + id String @id @default(cuid()) + novelId String + characterId String + sourceChapterId String? + sourceType String + currentInterpretation String + privateIntent String? + activePlan String? + emotionalStance String? + actionTendency String? + decisionTrigger String? + beliefsJson String @default("[]") + misbeliefsJson String @default("[]") + evidenceJson String @default("[]") + confidence Float? + isCurrent Boolean @default(true) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + sourceChapter Chapter? @relation(fields: [sourceChapterId], references: [id], onDelete: SetNull) + influenceProposals CharacterInfluenceProposal[] @relation("CharacterInfluenceSourceMind") + dialogueSessions CharacterDialogueSession[] @relation("CharacterDialogueSourceMind") + dialogueInfluences CharacterDialogueInfluence[] @relation("CharacterDialogueInfluenceSourceMind") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, isCurrent, updatedAt]) + @@index([sourceChapterId]) +} + +model CharacterInfluenceProposal { + id String @id @default(cuid()) + novelId String + characterId String + proposalSetId String + sourceMindSnapshotId String? + title String + directionSummary String + recommendationReason String + isRecommended Boolean @default(false) + behaviorGuidance String + emotionalGuidance String? + relationTension String? + readerPayoff String + risk String + observableSignalsJson String @default("[]") + evidenceJson String @default("[]") + confidence Float? + authorIntent String? + targetStartChapterOrder Int + targetEndChapterOrder Int + status String @default("draft") + acceptedAt DateTime? + appliedAt DateTime? + resolvedChapterId String? + resolutionEvidenceJson String @default("[]") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + sourceMindSnapshot CharacterMindSnapshot? @relation("CharacterInfluenceSourceMind", fields: [sourceMindSnapshotId], references: [id], onDelete: SetNull) + resolvedChapter Chapter? @relation("CharacterInfluenceResolvedChapter", fields: [resolvedChapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, status, targetStartChapterOrder, targetEndChapterOrder]) + @@index([proposalSetId]) + @@index([resolvedChapterId]) +} + +model CharacterDialogueSession { + id String @id @default(cuid()) + novelId String + characterId String + sourceMindSnapshotId String? + status String @default("active") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + sourceMindSnapshot CharacterMindSnapshot? @relation("CharacterDialogueSourceMind", fields: [sourceMindSnapshotId], references: [id], onDelete: SetNull) + turns CharacterDialogueTurn[] + influences CharacterDialogueInfluence[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, status, updatedAt]) + @@index([sourceMindSnapshotId]) +} + +model CharacterDialogueTurn { + id String @id @default(cuid()) + sessionId String + role String + content String + session CharacterDialogueSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([sessionId, createdAt]) +} + +model CharacterDialogueInfluence { + id String @id @default(cuid()) + novelId String + characterId String + sessionId String? + conversationSessionId String? + sourceMindSnapshotId String? + summary String + behaviorGuidance String + emotionalGuidance String? + relationTension String? + evidenceJson String @default("[]") + confidence Float? + targetStartChapterOrder Int + targetEndChapterOrder Int + status String @default("draft") + activatedAt DateTime? + appliedAt DateTime? + resolvedChapterId String? + resolutionEvidenceJson String @default("[]") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + session CharacterDialogueSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull) + conversationSession CharacterConversationSession? @relation(fields: [conversationSessionId], references: [id], onDelete: SetNull) + sourceMindSnapshot CharacterMindSnapshot? @relation("CharacterDialogueInfluenceSourceMind", fields: [sourceMindSnapshotId], references: [id], onDelete: SetNull) + resolvedChapter Chapter? @relation("CharacterDialogueInfluenceResolvedChapter", fields: [resolvedChapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, status, targetStartChapterOrder, targetEndChapterOrder]) + @@index([sessionId, createdAt]) + @@index([conversationSessionId, createdAt]) + @@index([resolvedChapterId]) +} + +model CharacterConversationSession { + id String @id @default(cuid()) + subjectKind String + subjectId String + scopeKind String + scopeId String? + interactionPolicy String + chapterAnchor Int? + sourceSnapshotJson String @default("{}") + evidenceBoundaryJson String @default("[]") + legacyDialogueSessionId String? @unique + status String @default("active") + turns CharacterConversationTurn[] + influences CharacterDialogueInfluence[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([subjectKind, subjectId, scopeKind, scopeId, status, updatedAt]) + @@index([scopeKind, scopeId, updatedAt]) +} + +model CharacterConversationTurn { + id String @id @default(cuid()) + sessionId String + role String + content String + evidenceJson String @default("[]") + uncertainty String? + session CharacterConversationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([sessionId, createdAt]) +} + +model CharacterRelation { + id String @id @default(cuid()) + novelId String + sourceCharacterId String + targetCharacterId String + surfaceRelation String + hiddenTension String? + conflictSource String? + secretAsymmetry String? + dynamicLabel String? + nextTurnPoint String? + trustScore Int? + conflictScore Int? + intimacyScore Int? + dependencyScore Int? + evidence String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceCharacter Character @relation("CharacterRelationSource", fields: [sourceCharacterId], references: [id], onDelete: Cascade) + targetCharacter Character @relation("CharacterRelationTarget", fields: [targetCharacterId], references: [id], onDelete: Cascade) + stages CharacterRelationStage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, sourceCharacterId, targetCharacterId]) + @@index([novelId, updatedAt]) + @@index([sourceCharacterId]) + @@index([targetCharacterId]) +} + +model CharacterCastOption { + id String @id @default(cuid()) + novelId String + title String + summary String + whyItWorks String? + recommendedReason String? + status String @default("draft") + sourceStoryInput String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + members CharacterCastOptionMember[] + relations CharacterCastOptionRelation[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, updatedAt]) +} + +model CharacterCastOptionMember { + id String @id @default(cuid()) + optionId String + sortOrder Int + name String + role String + gender CharacterGender @default(unknown) + castRole String + relationToProtagonist String? + storyFunction String + shortDescription String? + personality String? + background String? + development String? + identityLabel String? + factionLabel String? + stanceLabel String? + powerLevel String? + realm String? + currentLocation String? + availability String? + prohibitionsJson String @default("[]") + outerGoal String? + innerNeed String? + fear String? + wound String? + misbelief String? + secret String? + moralLine String? + firstImpression String? + option CharacterCastOption @relation(fields: [optionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([optionId, sortOrder]) +} + +model CharacterCastOptionRelation { + id String @id @default(cuid()) + optionId String + sortOrder Int + sourceName String + targetName String + surfaceRelation String + hiddenTension String? + conflictSource String? + secretAsymmetry String? + dynamicLabel String? + nextTurnPoint String? + option CharacterCastOption @relation(fields: [optionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([optionId, sortOrder]) +} + +model CharacterTimeline { + id String @id @default(cuid()) + novelId String + characterId String + chapterId String? + chapterOrder Int? + title String + content String + source String @default("auto") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId]) + @@index([characterId, chapterOrder]) + @@index([chapterId]) +} + +model CharacterCandidate { + id String @id @default(cuid()) + novelId String + sourceChapterId String? + proposedName String + proposedRole String? + summary String? + evidenceJson String? + matchedCharacterId String? + status String @default("pending") + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceChapter Chapter? @relation("CharacterCandidateSourceChapter", fields: [sourceChapterId], references: [id], onDelete: SetNull) + matchedCharacter Character? @relation("CharacterCandidateMatchedCharacter", fields: [matchedCharacterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([sourceChapterId]) + @@index([matchedCharacterId]) +} + +model CharacterVolumeAssignment { + id String @id @default(cuid()) + novelId String + characterId String + volumeId String + roleLabel String? + responsibility String + appearanceExpectation String? + plannedChapterOrdersJson String? + isCore Boolean @default(false) + absenceWarningThreshold Int @default(3) + absenceHighRiskThreshold Int @default(5) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + volume VolumePlan @relation(fields: [volumeId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([characterId, volumeId]) + @@index([novelId, volumeId, isCore]) + @@index([volumeId, characterId]) +} + +model CharacterFactionTrack { + id String @id @default(cuid()) + novelId String + characterId String + volumeId String? + chapterId String? + chapterOrder Int? + factionLabel String + stanceLabel String? + summary String? + sourceType String + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + volume VolumePlan? @relation(fields: [volumeId], references: [id], onDelete: SetNull) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, createdAt]) + @@index([volumeId, characterId, createdAt]) + @@index([chapterId, createdAt]) +} + +model CharacterRelationStage { + id String @id @default(cuid()) + novelId String + relationId String? + sourceCharacterId String + targetCharacterId String + volumeId String? + chapterId String? + chapterOrder Int? + stageLabel String + stageSummary String + nextTurnPoint String? + sourceType String + confidence Float? + isCurrent Boolean @default(true) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + relation CharacterRelation? @relation(fields: [relationId], references: [id], onDelete: SetNull) + sourceCharacter Character @relation("CharacterRelationStageSource", fields: [sourceCharacterId], references: [id], onDelete: Cascade) + targetCharacter Character @relation("CharacterRelationStageTarget", fields: [targetCharacterId], references: [id], onDelete: Cascade) + volume VolumePlan? @relation(fields: [volumeId], references: [id], onDelete: SetNull) + chapter Chapter? @relation("CharacterRelationStageChapter", fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, isCurrent, updatedAt]) + @@index([sourceCharacterId, targetCharacterId, isCurrent]) + @@index([relationId]) + @@index([chapterId]) +} + +model BaseCharacter { + id String @id @default(cuid()) + name String + role String + personality String + background String + development String + appearance String? + weaknesses String? + interests String? + keyEvents String? + tags String @default("") + category String + sourceType String @default("manual") + sourceRefId String? + revisions BaseCharacterRevision[] + libraryLinks CharacterLibraryLink[] + syncProposals CharacterSyncProposal[] + imageTasks ImageGenerationTask[] + imageAssets ImageAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sourceType, sourceRefId]) +} + +model BaseCharacterRevision { + id String @id @default(cuid()) + baseCharacterId String + version Int + snapshotJson String + changeSummary String? + sourceType String @default("manual") + sourceRefId String? + baseCharacter BaseCharacter @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + libraryLinks CharacterLibraryLink[] + syncProposals CharacterSyncProposal[] + createdAt DateTime @default(now()) + + @@unique([baseCharacterId, version]) + @@index([baseCharacterId, createdAt]) +} + +model CharacterLibraryLink { + id String @id @default(cuid()) + novelId String + characterId String @unique + baseCharacterId String + baseRevisionId String? + syncPolicy String @default("manual_review") + linkStatus String @default("linked") + localOverridesJson String? + lastSyncedAt DateTime? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + baseCharacter BaseCharacter @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + baseRevision BaseCharacterRevision? @relation(fields: [baseRevisionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, linkStatus, updatedAt]) + @@index([baseCharacterId, linkStatus, updatedAt]) + @@index([baseRevisionId]) +} + +model CharacterSyncProposal { + id String @id @default(cuid()) + novelId String? + characterId String? + baseCharacterId String? + baseRevisionId String? + direction String + status String @default("pending_review") + confidence Float? + summary String + payloadJson String + safeUpdatesJson String? + novelOnlyUpdatesJson String? + riskyUpdatesJson String? + recommendedAction String? + sourceType String @default("manual") + sourceRefId String? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character? @relation(fields: [characterId], references: [id], onDelete: SetNull) + baseCharacter BaseCharacter? @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + baseRevision BaseCharacterRevision? @relation(fields: [baseRevisionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([characterId, status, updatedAt]) + @@index([baseCharacterId, status, updatedAt]) + @@index([baseRevisionId]) +} + +model ImageGenerationTask { + id String @id @default(cuid()) + sceneType ImageSceneType @default(character) + baseCharacterId String? + novelId String? + bookAnalysisCharacterId String? + provider String + model String + prompt String + negativePrompt String? + stylePreset String? + referenceImageAssetIdsJson String? + size String @default("1024x1024") + imageCount Int @default(1) + seed Int? + status PipelineJobStatus @default(queued) + progress Float @default(0) + retryCount Int @default(0) + maxRetries Int @default(2) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + error String? + startedAt DateTime? + finishedAt DateTime? + baseCharacter BaseCharacter? @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + bookAnalysisCharacter BookAnalysisCharacter? @relation(fields: [bookAnalysisCharacterId], references: [id], onDelete: Cascade) + assets ImageAsset[] + bookAnalysisAppearanceImages BookAnalysisCharacterAppearanceImage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sceneType, status]) + @@index([baseCharacterId, createdAt]) + @@index([novelId, createdAt]) + @@index([bookAnalysisCharacterId, createdAt]) +} + +model StyleExtractionTask { + id String @id @default(cuid()) + name String + category String? + sourceText String + sourceType String @default("from_text") + sourceRefId String? + sourceProcessingMode String @default("full_text") + sourceInputText String? + sourceInputCharLimit Int? + sourceInputCharCount Int? + provider String + model String? + temperature Float? + presetKey String @default("balanced") + status PipelineJobStatus @default(queued) + progress Float @default(0) + retryCount Int @default(0) + maxRetries Int @default(1) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + error String? + summary String? + createdStyleProfileId String? + createdStyleProfileName String? + startedAt DateTime? + finishedAt DateTime? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + llmCallCount Int @default(0) + lastTokenRecordedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([createdStyleProfileId]) + @@index([sourceType, sourceRefId]) +} + +model ImageAsset { + id String @id @default(cuid()) + taskId String + sceneType ImageSceneType @default(character) + baseCharacterId String? + novelId String? + bookAnalysisCharacterId String? + provider String + model String + url String + mimeType String? + width Int? + height Int? + seed Int? + prompt String? + isPrimary Boolean @default(false) + sortOrder Int @default(0) + metadata String? + task ImageGenerationTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + baseCharacter BaseCharacter? @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + bookAnalysisCharacter BookAnalysisCharacter? @relation(fields: [bookAnalysisCharacterId], references: [id], onDelete: Cascade) + bookAnalysisAppearanceImages BookAnalysisCharacterAppearanceImage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([taskId]) + @@index([sceneType, createdAt]) + @@index([baseCharacterId, isPrimary, createdAt]) + @@index([novelId, isPrimary, createdAt]) + @@index([bookAnalysisCharacterId, isPrimary, createdAt]) +} + +// 项目级视觉资源索引。它只保存可展示、筛选与选择所需的投影, +// 原始业务事实与文件继续由 ImageAsset / Comic / Drama 等来源拥有。 +model VisualAssetProjection { + id String @id @default(cuid()) + sourceDomain String + sourceType String + sourceId String + sourceVersion String @default("current") + sourceLabel String + scopeKind String @default("global") + scopeId String? + scopeLabel String? + kind String + origin String @default("generated") + url String + thumbnailUrl String? + mimeType String? + width Int? + height Int? + prompt String? + provider String? + model String? + isPrimary Boolean @default(false) + metadata String? + sourceCreatedAt DateTime + lastSeenAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([sourceDomain, sourceType, sourceId, sourceVersion]) + @@index([scopeKind, scopeId, sourceCreatedAt]) + @@index([kind, sourceCreatedAt]) + @@index([sourceDomain, sourceCreatedAt]) +} + +model NovelGenre { + id String @id @default(cuid()) + name String + description String? + template String? + parentId String? + parent NovelGenre? @relation("GenreChildren", fields: [parentId], references: [id]) + children NovelGenre[] @relation("GenreChildren") + novels Novel[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([parentId]) +} + +model NovelStoryMode { + id String @id @default(cuid()) + name String + description String? + template String? + profileJson String + parentId String? + parent NovelStoryMode? @relation("StoryModeChildren", fields: [parentId], references: [id], onDelete: SetNull) + children NovelStoryMode[] @relation("StoryModeChildren") + primaryNovels Novel[] @relation("NovelPrimaryStoryMode") + secondaryNovels Novel[] @relation("NovelSecondaryStoryMode") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([parentId]) +} + +model World { + id String @id @default(cuid()) + name String + description String? + worldType String? + templateKey String? + axioms String? + background String? + geography String? + cultures String? + magicSystem String? + politics String? + races String? + religions String? + technology String? + conflicts String? + history String? + economy String? + factions String? + status String @default("draft") + version Int @default(1) + selectedDimensions String? + selectedElements String? + layerStates String? + consistencyReport String? + overviewSummary String? + structureJson String? + bindingSupportJson String? + structureSchemaVersion Int @default(1) + novels Novel[] + snapshots WorldSnapshot[] + deepeningQA WorldDeepeningQA[] + consistencyIssues WorldConsistencyIssue[] + libraryItems WorldPropertyLibrary[] + novelWorlds NovelWorld[] + assets WorldAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model NovelWorld { + id String @id @default(cuid()) + novelId String @unique + sourceWorldId String? + sourceType String @default("manual") + title String? + coverSummary String? + structuredDataJson String? + bindingContractJson String? + storySliceJson String? + storySliceOverridesJson String? + storySliceSchemaVersion Int @default(1) + storySliceBuiltAt DateTime? + storySliceDigest String? + syncEnabled Boolean @default(false) + syncDirection String @default("none") + syncBaseVersion Int? + syncPendingChangesJson String? + lastSyncedAt DateTime? + generationPolicyJson String? + generatedFromThemeJson String? + savedToLibraryAt DateTime? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceWorld World? @relation(fields: [sourceWorldId], references: [id], onDelete: SetNull) + syncRecords WorldSyncRecord[] + assets WorldAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sourceWorldId]) + @@index([sourceType]) +} + +model WorldSyncRecord { + id String @id @default(cuid()) + novelWorldId String + sourceWorldId String? + direction String + syncedFieldsJson String? + diffSummary String? + triggeredBy String @default("user") + novelWorld NovelWorld @relation(fields: [novelWorldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelWorldId, createdAt]) + @@index([sourceWorldId]) +} + +model WorldAsset { + id String @id @default(cuid()) + worldId String? + novelWorldId String? + assetType String + title String + description String? + generationPrompt String? + renderDataJson String? + thumbnailUrl String? + version Int @default(1) + status String @default("placeholder") + world World? @relation(fields: [worldId], references: [id], onDelete: Cascade) + novelWorld NovelWorld? @relation(fields: [novelWorldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([worldId, assetType]) + @@index([novelWorldId, assetType]) + @@index([assetType]) +} + +model WorldPropertyLibrary { + id String @id @default(cuid()) + name String + description String? + category String + worldType String? + usageCount Int @default(0) + sourceWorldId String? + sourceWorld World? @relation(fields: [sourceWorldId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sourceWorldId]) +} + +model WorldSnapshot { + id String @id @default(cuid()) + worldId String + label String? + data String + world World @relation(fields: [worldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([worldId, createdAt]) +} + +model WorldDeepeningQA { + id String @id @default(cuid()) + worldId String + priority String @default("recommended") + question String + targetLayer String? + targetField String? + answer String? + integratedSummary String? + status String @default("pending") + world World @relation(fields: [worldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([worldId, status]) +} + +model WorldConsistencyIssue { + id String @id @default(cuid()) + worldId String + severity String + code String + message String + detail String? + source String @default("rule") + status String @default("open") + targetField String? + world World @relation(fields: [worldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([worldId, status]) + @@index([worldId, severity]) +} + +model WritingFormula { + id String @id @default(cuid()) + name String + sourceText String? + content String? + genre String? + style String? + toneVoice String? + structure String? + pacing String? + paragraphPattern String? + sentenceStructure String? + vocabularyLevel String? + rhetoricalDevices String? + narrativeMode String? + perspectivePoint String? + characterVoice String? + themes String? + motifs String? + emotionalTone String? + uniqueFeatures String? + formulaDescription String? + formulaSteps String? + applicationTips String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model StyleProfile { + id String @id @default(cuid()) + name String + description String? + category String? + tagsJson String? + applicableGenresJson String? + sourceType String + sourceRefId String? + sourceContent String? + extractedFeaturesJson String? + extractionPresetsJson String? + extractionAntiAiRuleKeysJson String? + selectedExtractionPresetKey String? + analysisMarkdown String? + narrativeRulesJson String? + characterRulesJson String? + languageRulesJson String? + rhythmRulesJson String? + status String @default("active") + antiAiBindings StyleProfileAntiAiRule[] + bindings StyleBinding[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([sourceType, sourceRefId]) +} + +model StyleTemplate { + id String @id @default(cuid()) + key String @unique + name String + description String + category String + tagsJson String? + applicableGenresJson String? + analysisMarkdown String? + narrativeRulesJson String? + characterRulesJson String? + languageRulesJson String? + rhythmRulesJson String? + defaultAntiAiRuleKeysJson String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model AntiAiRule { + id String @id @default(cuid()) + key String @unique + name String + type AntiAiRuleType + severity AntiAiSeverity + description String + detectPatternsJson String? + rewriteSuggestion String? + promptInstruction String? + autoRewrite Boolean @default(false) + enabled Boolean @default(true) + globalBaselineEnabled Boolean @default(false) + profileBindings StyleProfileAntiAiRule[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([type, enabled]) + @@index([globalBaselineEnabled, enabled]) +} + +model StyleProfileAntiAiRule { + id String @id @default(cuid()) + styleProfileId String + antiAiRuleId String + enabled Boolean @default(true) + weight Float @default(1) + styleProfile StyleProfile @relation(fields: [styleProfileId], references: [id], onDelete: Cascade) + antiAiRule AntiAiRule @relation(fields: [antiAiRuleId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([styleProfileId, antiAiRuleId]) + @@index([antiAiRuleId]) +} + +model StyleBinding { + id String @id @default(cuid()) + styleProfileId String + targetType StyleBindingTargetType + targetId String + priority Int @default(1) + weight Float @default(1) + enabled Boolean @default(true) + styleProfile StyleProfile @relation(fields: [styleProfileId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([targetType, targetId, enabled]) + @@index([styleProfileId]) +} + +model TitleLibrary { + id String @id @default(cuid()) + title String + description String? + clickRate Float? + keywords String? + genreId String? + usedCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model APIKey { + id String @id @default(cuid()) + provider String + displayName String? + key String? + model String? + baseURL String? + isActive Boolean @default(true) + reasoningEnabled Boolean @default(true) + concurrencyLimit Int @default(0) + requestIntervalMs Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([provider]) +} + +model AppSetting { + key String @id + value String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ModelRouteConfig { + id String @id @default(cuid()) + taskType String @unique + provider String + model String + temperature Float @default(0.7) + maxTokens Int? + requestProtocol String @default("auto") + structuredResponseFormat String @default("auto") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([provider]) +} + +model NovelBible { + id String @id @default(cuid()) + novelId String @unique + coreSetting String? + forbiddenRules String? + mainPromise String? + characterArcs String? + worldRules String? + rawContent String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model PlotBeat { + id String @id @default(cuid()) + novelId String + chapterOrder Int? + beatType String + title String + content String + status BeatStatus @default(planned) + metadata String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([novelId, chapterOrder]) +} + +model ChapterSummary { + id String @id @default(cuid()) + novelId String + chapterId String @unique + summary String + keyEvents String? + characterStates String? + hook String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) +} + +model ConsistencyFact { + id String @id @default(cuid()) + novelId String + chapterId String? + category FactCategory @default(plot) + content String + source String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([chapterId]) + @@index([novelId, category]) +} + +model GenerationJob { + id String @id @default(cuid()) + novelId String + startOrder Int + endOrder Int + runMode PipelineRunMode? @default(fast) + autoReview Boolean @default(true) + autoRepair Boolean @default(true) + skipCompleted Boolean @default(true) + qualityThreshold Int? + repairMode PipelineRepairMode? @default(light_repair) + status PipelineJobStatus @default(queued) + progress Float @default(0) + completedCount Int @default(0) + totalCount Int @default(0) + retryCount Int @default(0) + maxRetries Int @default(2) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + error String? + lastErrorType String? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + llmCallCount Int @default(0) + lastTokenRecordedAt DateTime? + payload String? + startedAt DateTime? + finishedAt DateTime? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([novelId, status]) +} + +model AgentRun { + id String @id @default(cuid()) + novelId String? + chapterId String? + sessionId String + goal String + entryAgent String + status AgentRunStatus @default(queued) + currentStep String? + currentAgent String? + error String? + startedAt DateTime? + finishedAt DateTime? + metadataJson String? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + steps AgentStep[] + approvals AgentApproval[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([novelId, createdAt]) + @@index([novelId, chapterId, createdAt]) + @@index([sessionId, createdAt]) +} + +model AgentStep { + id String @id @default(cuid()) + runId String + seq Int + agentName String + stepType AgentStepType + status AgentStepStatus @default(succeeded) + parentStepId String? + idempotencyKey String? + inputJson String? + outputJson String? + error String? + errorCode String? + provider String? + model String? + tokenUsageJson String? + costUsd Float? + durationMs Int? + run AgentRun @relation(fields: [runId], references: [id], onDelete: Cascade) + approvals AgentApproval[] + createdAt DateTime @default(now()) + + @@unique([runId, seq]) + @@index([runId, idempotencyKey]) + @@index([runId, parentStepId]) +} + +model AgentApproval { + id String @id @default(cuid()) + runId String + stepId String? + approvalType String + targetType String + targetId String + diffSummary String + status AgentApprovalStatus @default(pending) + expiresAt DateTime? + decisionNote String? + decider String? + decidedAt DateTime? + payloadJson String? + run AgentRun @relation(fields: [runId], references: [id], onDelete: Cascade) + step AgentStep? @relation(fields: [stepId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([runId, status]) + @@index([stepId]) + @@index([status, expiresAt]) +} + +model CreativeHubThread { + id String @id @default(cuid()) + title String @default("新对话") + archived Boolean @default(false) + status CreativeHubThreadStatus @default(idle) + latestRunId String? + latestError String? + resourceBindingsJson String? + metadataJson String? + checkpoints CreativeHubCheckpoint[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([archived, updatedAt]) + @@index([status, updatedAt]) +} + +model CreativeHubCheckpoint { + id String @id @default(cuid()) + threadId String + checkpointId String + parentCheckpointId String? + runId String? + messageCount Int @default(0) + preview String? + messagesJson String + interruptsJson String? + metadataJson String? + thread CreativeHubThread @relation(fields: [threadId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([threadId, checkpointId]) + @@index([threadId, createdAt]) +} + +model StorylineVersion { + id String @id @default(cuid()) + novelId String + version Int + status StorylineVersionStatus @default(draft) + content String + diffSummary String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, version]) + @@index([novelId, status, createdAt]) +} + +model VolumePlanVersion { + id String @id @default(cuid()) + novelId String + version Int + status VolumePlanVersionStatus @default(draft) + contentJson String + diffSummary String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + volumes VolumePlan[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, version]) + @@index([novelId, status, createdAt]) + @@index([novelId, status, version]) +} + +model VolumePlan { + id String @id @default(cuid()) + novelId String + sortOrder Int + title String + summary String? + mainPromise String? + escalationMode String? + protagonistChange String? + climax String? + nextVolumeHook String? + resetPoint String? + openPayoffsJson String? + status String @default("active") + sourceVersionId String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceVersion VolumePlanVersion? @relation(fields: [sourceVersionId], references: [id], onDelete: SetNull) + chapters VolumeChapterPlan[] + characterAssignments CharacterVolumeAssignment[] + factionTracks CharacterFactionTrack[] + relationStages CharacterRelationStage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, sortOrder]) + @@index([novelId, status, sortOrder]) + @@index([sourceVersionId]) +} + +model VolumeChapterPlan { + id String @id @default(cuid()) + volumeId String + chapterId String? + chapterOrder Int + title String + summary String + purpose String? + conflictLevel Int? + conflictLevelSource String? + revealLevel Int? + targetWordCount Int? + mustAvoid String? + taskSheet String? + sceneCards String? + payoffRefsJson String? + volume VolumePlan @relation(fields: [volumeId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([volumeId, chapterOrder]) + @@index([volumeId, chapterOrder]) + @@index([chapterId]) +} + +model QualityReport { + id String @id @default(cuid()) + novelId String + chapterId String? + coherence Int + repetition Int + pacing Int + voice Int + engagement Int + overall Int + issues String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([chapterId]) + @@index([novelId, createdAt]) +} + +model StoryMacroPlan { + id String @id @default(cuid()) + novelId String @unique + storyInput String? + expansionJson String? + decompositionJson String? + issuesJson String? + lockedFieldsJson String? + constraintEngineJson String? + stateJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model BookContract { + id String @id @default(cuid()) + novelId String @unique + readingPromise String + protagonistFantasy String + coreSellingPoint String + chapter3Payoff String + chapter10Payoff String + chapter30Payoff String + escalationLadder String + relationshipMainline String + absoluteRedLinesJson String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model NovelWorkflowTask { + id String @id @default(cuid()) + novelId String? + lane NovelWorkflowLane + title String + status NovelWorkflowTaskStatus @default(queued) + progress Float @default(0) + currentStage String? + currentItemKey String? + currentItemLabel String? + checkpointType String? + checkpointSummary String? + resumeTargetJson String? + seedPayloadJson String? + milestonesJson String? + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + startedAt DateTime? + finishedAt DateTime? + cancelRequestedAt DateTime? + attemptCount Int @default(0) + maxAttempts Int @default(3) + lastError String? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + llmCallCount Int @default(0) + lastTokenRecordedAt DateTime? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + autoApprovalRecords AutoDirectorAutoApprovalRecord[] + directorRun DirectorRun? + directorCommands DirectorRunCommand[] + directorSteps DirectorStepRun[] + directorEvents DirectorEvent[] + directorArtifacts DirectorArtifact[] + directorLlmUsageRecords DirectorLlmUsageRecord[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([status, updatedAt]) + @@index([lane, updatedAt]) +} + +model DirectorRunCommand { + id String @id @default(cuid()) + taskId String + novelId String? + commandType String + idempotencyKey String + status String @default("queued") + leaseOwner String? + leaseExpiresAt DateTime? + attempt Int @default(0) + runAfter DateTime @default(now()) + payloadJson String? + errorMessage String? + startedAt DateTime? + finishedAt DateTime? + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([taskId, commandType, idempotencyKey]) + @@index([status, runAfter, updatedAt]) + @@index([taskId, status, updatedAt]) + @@index([novelId, updatedAt]) + @@index([leaseOwner, leaseExpiresAt]) +} + +model DirectorRuntimeInstance { + id String @id @default(cuid()) + novelId String? + workflowTaskId String? + runId String? + runMode String? + status String @default("waiting_worker") + currentStep String? + currentChapterId String? + checkpointVersion Int @default(0) + cancelRequestedAt DateTime? + lastHeartbeatAt DateTime? + lastErrorClass String? + lastErrorMessage String? + workerMessage String? + metadataJson String? + commands DirectorRuntimeCommand[] + executions DirectorRuntimeExecution[] + checkpoints DirectorRuntimeCheckpoint[] + events DirectorRuntimeEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([workflowTaskId]) + @@index([runId]) + @@index([status, updatedAt]) +} + +model DirectorRuntimeCommand { + id String @id @default(cuid()) + runtimeId String + workflowTaskId String? + novelId String? + legacyCommandId String? @unique + commandType String + idempotencyKey String + status String @default("queued") + priority Int @default(50) + runAfter DateTime @default(now()) + leaseOwner String? + leaseExpiresAt DateTime? + attempt Int @default(0) + payloadJson String? + errorMessage String? + startedAt DateTime? + finishedAt DateTime? + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + executions DirectorRuntimeExecution[] + checkpoints DirectorRuntimeCheckpoint[] + events DirectorRuntimeEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([runtimeId, commandType, idempotencyKey]) + @@index([runtimeId, status, updatedAt]) + @@index([status, priority, runAfter, createdAt]) + @@index([workflowTaskId, status, updatedAt]) + @@index([novelId, status, updatedAt]) + @@index([leaseOwner, leaseExpiresAt]) +} + +model DirectorRuntimeExecution { + id String @id @default(cuid()) + runtimeId String + commandId String? + workflowTaskId String? + novelId String? + legacyCommandId String? + activeLockKey String? @unique + workerId String? + slotId String? + status String @default("leased") + stepType String + resourceClass String? + leaseExpiresAt DateTime? + heartbeatAt DateTime? + startedAt DateTime? + finishedAt DateTime? + errorClass String? + errorMessage String? + inputHash String? + checkpointVersion Int @default(0) + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + command DirectorRuntimeCommand? @relation(fields: [commandId], references: [id], onDelete: SetNull) + checkpoints DirectorRuntimeCheckpoint[] + events DirectorRuntimeEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([runtimeId, status, updatedAt]) + @@index([status, leaseExpiresAt]) + @@index([workflowTaskId, status, updatedAt]) + @@index([novelId, status, updatedAt]) + @@index([workerId, status, updatedAt]) +} + +model DirectorRuntimeCheckpoint { + id String @id @default(cuid()) + runtimeId String + commandId String? + executionId String? + version Int + stepType String + targetType String? + targetId String? + inputHash String? + outputRefJson String? + stateJson String? + summary String? + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + command DirectorRuntimeCommand? @relation(fields: [commandId], references: [id], onDelete: SetNull) + execution DirectorRuntimeExecution? @relation(fields: [executionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@unique([runtimeId, version]) + @@index([runtimeId, createdAt]) + @@index([commandId]) + @@index([executionId]) +} + +model DirectorRuntimeEvent { + id String @id + runtimeId String + commandId String? + executionId String? + workflowTaskId String? + novelId String? + type String + summary String + severity String? + metadataJson String? + occurredAt DateTime + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + command DirectorRuntimeCommand? @relation(fields: [commandId], references: [id], onDelete: SetNull) + execution DirectorRuntimeExecution? @relation(fields: [executionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([runtimeId, occurredAt]) + @@index([commandId, occurredAt]) + @@index([executionId, occurredAt]) + @@index([workflowTaskId, occurredAt]) + @@index([novelId, occurredAt]) + @@index([type, occurredAt]) +} + +model DirectorRun { + id String @id + taskId String @unique + novelId String? + entrypoint String? + policyJson String + lastWorkspaceAnalysisJson String? + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + steps DirectorStepRun[] + events DirectorEvent[] + artifacts DirectorArtifact[] + llmUsageRecords DirectorLlmUsageRecord[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, updatedAt]) +} + +model DirectorStepRun { + id String @id @default(cuid()) + runId String + taskId String + novelId String? + idempotencyKey String @unique + nodeKey String + label String + status String + targetType String? + targetId String? + startedAt DateTime + finishedAt DateTime? + error String? + producedArtifactsJson String? + policyDecisionJson String? + run DirectorRun @relation(fields: [runId], references: [id], onDelete: Cascade) + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + llmUsageRecords DirectorLlmUsageRecord[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([runId, status, updatedAt]) + @@index([taskId, nodeKey]) + @@index([novelId, updatedAt]) +} + +model DirectorLlmUsageRecord { + id String @id @default(cuid()) + novelId String? + taskId String? + runId String? + stepIdempotencyKey String? + nodeKey String? + promptAssetKey String? + promptVersion String? + modelRoute String? + provider String? + model String? + status String @default("recorded") + attributionStatus String @default("unattributed") + durationMs Int? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + metadataJson String? + recordedAt DateTime @default(now()) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + task NovelWorkflowTask? @relation(fields: [taskId], references: [id], onDelete: Cascade) + run DirectorRun? @relation(fields: [runId], references: [id], onDelete: SetNull) + step DirectorStepRun? @relation(fields: [stepIdempotencyKey], references: [idempotencyKey], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([novelId, recordedAt]) + @@index([taskId, recordedAt]) + @@index([runId, recordedAt]) + @@index([stepIdempotencyKey, recordedAt]) + @@index([nodeKey, recordedAt]) + @@index([attributionStatus, recordedAt]) +} + +model DirectorEvent { + id String @id + runId String? + taskId String? + novelId String? + type String + nodeKey String? + artifactId String? + artifactType String? + summary String + affectedScope String? + severity String? + metadataJson String? + occurredAt DateTime + run DirectorRun? @relation(fields: [runId], references: [id], onDelete: Cascade) + task NovelWorkflowTask? @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([runId, occurredAt]) + @@index([taskId, occurredAt]) + @@index([novelId, occurredAt]) + @@index([type, occurredAt]) +} + +model DirectorArtifact { + id String @id + runId String? + novelId String + taskId String? + artifactType String + targetType String + targetId String? + version Int @default(1) + status String + source String + contentTable String + contentId String + contentHash String? + schemaVersion String + promptAssetKey String? + promptVersion String? + modelRoute String? + sourceStepRunId String? + protectedUserContent Boolean? + artifactUpdatedAt DateTime? + run DirectorRun? @relation(fields: [runId], references: [id], onDelete: SetNull) + task NovelWorkflowTask? @relation(fields: [taskId], references: [id], onDelete: SetNull) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + dependencies DirectorArtifactDependency[] @relation("DirectorArtifactDependencies") + dependents DirectorArtifactDependency[] @relation("DirectorArtifactDependents") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, artifactType, status]) + @@index([runId, updatedAt]) + @@index([taskId, updatedAt]) + @@index([targetType, targetId]) + @@index([sourceStepRunId]) +} + +model DirectorArtifactDependency { + id String @id @default(cuid()) + artifactId String + dependsOnArtifactId String + dependsOnVersion Int? + artifact DirectorArtifact @relation("DirectorArtifactDependencies", fields: [artifactId], references: [id], onDelete: Cascade) + dependsOn DirectorArtifact @relation("DirectorArtifactDependents", fields: [dependsOnArtifactId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([artifactId, dependsOnArtifactId]) + @@index([dependsOnArtifactId]) +} + +model AutoDirectorAutoApprovalRecord { + id String @id @default(cuid()) + taskId String + novelId String + approvalPointCode String + approvalPointLabel String + checkpointType String + checkpointSummary String? + summary String + stage String? + scopeLabel String? + eventId String @unique + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) + @@index([taskId, createdAt]) +} + + +model AutoDirectorFollowUpActionLog { + id String @id @default(cuid()) + taskId String + actionCode String + sourceChannel String + sourceUser String? + idempotencyKey String @unique + resultCode String + failureReason String? + metadataJson String? + executedAt DateTime @default(now()) + createdAt DateTime @default(now()) + + @@index([taskId, executedAt]) + @@index([taskId, actionCode, executedAt]) +} + +model AutoDirectorFollowUpNotificationLog { + id String @id @default(cuid()) + eventId String + eventType String + taskId String + channelType String + target String? + requestPayload String? + responseBody String? + responseStatus Int? + attemptCount Int @default(1) + deliveredAt DateTime? + status String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([taskId, createdAt]) + @@index([eventId, channelType, createdAt]) +} + +model StoryStateSnapshot { + id String @id @default(cuid()) + novelId String + sourceChapterId String? + summary String? + rawStateJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceChapter Chapter? @relation(fields: [sourceChapterId], references: [id], onDelete: SetNull) + characterStates CharacterState[] + relationStates RelationState[] + informationStates InformationState[] + foreshadowStates ForeshadowState[] + openConflicts OpenConflict[] + payoffLedgerItems PayoffLedgerItem[] @relation("PayoffLedgerLastSnapshot") + storyPlans StoryPlan[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, sourceChapterId]) + @@index([novelId, createdAt]) + @@index([sourceChapterId]) +} + +model CharacterState { + id String @id @default(cuid()) + snapshotId String + characterId String + currentGoal String? + emotion String? + stressLevel Int? + secretExposure String? + knownFactsJson String? + misbeliefsJson String? + summary String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([snapshotId, characterId]) + @@index([characterId, createdAt]) +} + +model RelationState { + id String @id @default(cuid()) + snapshotId String + sourceCharacterId String + targetCharacterId String + trustScore Int? + intimacyScore Int? + conflictScore Int? + dependencyScore Int? + summary String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + sourceCharacter Character @relation("RelationStateSource", fields: [sourceCharacterId], references: [id], onDelete: Cascade) + targetCharacter Character @relation("RelationStateTarget", fields: [targetCharacterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([snapshotId, sourceCharacterId, targetCharacterId]) + @@index([sourceCharacterId, targetCharacterId]) +} + +model InformationState { + id String @id @default(cuid()) + snapshotId String + holderType String + holderRefId String? + fact String + status String + summary String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([snapshotId, holderType]) +} + +model ForeshadowState { + id String @id @default(cuid()) + snapshotId String + title String + summary String? + status String + setupChapterId String? + payoffChapterId String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + setupChapter Chapter? @relation("ForeshadowSetupChapter", fields: [setupChapterId], references: [id], onDelete: SetNull) + payoffChapter Chapter? @relation("ForeshadowPayoffChapter", fields: [payoffChapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([snapshotId, status]) + @@index([setupChapterId]) + @@index([payoffChapterId]) +} + +model OpenConflict { + id String @id @default(cuid()) + novelId String + chapterId String? + sourceSnapshotId String? + sourceIssueId String? + sourceType String + conflictType String + conflictKey String + title String + summary String + severity String @default("medium") + status String @default("open") + evidenceJson String? + affectedCharacterIdsJson String? + resolutionHint String? + lastSeenChapterOrder Int? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + sourceSnapshot StoryStateSnapshot? @relation(fields: [sourceSnapshotId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, chapterId, sourceType, conflictKey]) + @@index([novelId, status, updatedAt]) + @@index([chapterId, status]) + @@index([sourceSnapshotId]) + @@index([sourceIssueId]) +} + +model PayoffLedgerItem { + id String @id @default(cuid()) + novelId String + ledgerKey String + title String + summary String + scopeType PayoffLedgerScopeType + currentStatus PayoffLedgerStatus + targetStartChapterOrder Int? + targetEndChapterOrder Int? + firstSeenChapterOrder Int? + lastTouchedChapterOrder Int? + lastTouchedChapterId String? + setupChapterId String? + payoffChapterId String? + lastSnapshotId String? + sourceRefsJson String? + evidenceJson String? + riskSignalsJson String? + statusReason String? + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + lastTouchedChapter Chapter? @relation("PayoffLedgerLastTouchedChapter", fields: [lastTouchedChapterId], references: [id], onDelete: SetNull) + setupChapter Chapter? @relation("PayoffLedgerSetupChapter", fields: [setupChapterId], references: [id], onDelete: SetNull) + payoffChapter Chapter? @relation("PayoffLedgerPayoffChapter", fields: [payoffChapterId], references: [id], onDelete: SetNull) + lastSnapshot StoryStateSnapshot? @relation("PayoffLedgerLastSnapshot", fields: [lastSnapshotId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, ledgerKey]) + @@index([novelId, currentStatus, updatedAt]) + @@index([novelId, targetEndChapterOrder]) + @@index([lastTouchedChapterId]) + @@index([setupChapterId]) + @@index([payoffChapterId]) + @@index([lastSnapshotId]) +} + +model CharacterResourceLedgerItem { + id String @id @default(cuid()) + novelId String + resourceKey String + name String + summary String + resourceType String + narrativeFunction String + ownerType String + ownerId String? + ownerName String? + ownerCharacterId String? + holderCharacterId String? + holderCharacterName String? + status String + readerKnows Boolean @default(false) + holderKnows Boolean @default(true) + knownByCharacterIdsJson String? + introducedChapterId String? + introducedChapterOrder Int? + lastTouchedChapterId String? + lastTouchedChapterOrder Int? + expectedUseStartChapterOrder Int? + expectedUseEndChapterOrder Int? + constraintsJson String? + riskSignalsJson String? + sourceRefsJson String? + evidenceJson String? + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + ownerCharacter Character? @relation("CharacterResourceOwner", fields: [ownerCharacterId], references: [id], onDelete: SetNull) + holderCharacter Character? @relation("CharacterResourceHolder", fields: [holderCharacterId], references: [id], onDelete: SetNull) + introducedChapter Chapter? @relation("CharacterResourceIntroducedChapter", fields: [introducedChapterId], references: [id], onDelete: SetNull) + lastTouchedChapter Chapter? @relation("CharacterResourceLastTouchedChapter", fields: [lastTouchedChapterId], references: [id], onDelete: SetNull) + events CharacterResourceEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, resourceKey]) + @@index([novelId, status, updatedAt]) + @@index([holderCharacterId, status]) + @@index([ownerCharacterId]) + @@index([novelId, lastTouchedChapterOrder]) + @@index([introducedChapterId]) + @@index([lastTouchedChapterId]) +} + +model CharacterResourceEvent { + id String @id @default(cuid()) + novelId String + resourceId String + chapterId String? + chapterOrder Int? + eventType String + actorCharacterId String? + fromHolderCharacterId String? + toHolderCharacterId String? + summary String + evidenceJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + resource CharacterResourceLedgerItem @relation(fields: [resourceId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + actorCharacter Character? @relation("CharacterResourceEventActor", fields: [actorCharacterId], references: [id], onDelete: SetNull) + fromHolderCharacter Character? @relation("CharacterResourceEventFromHolder", fields: [fromHolderCharacterId], references: [id], onDelete: SetNull) + toHolderCharacter Character? @relation("CharacterResourceEventToHolder", fields: [toHolderCharacterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) + @@index([resourceId, createdAt]) + @@index([chapterId]) + @@index([actorCharacterId]) + @@index([fromHolderCharacterId]) + @@index([toHolderCharacterId]) +} + +model CanonicalStateVersion { + id String @id @default(cuid()) + novelId String + chapterId String? + sourceType String + sourceStage String? + version Int + summary String + snapshotJson String + acceptedProposalIdsJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + committedProposals StateChangeProposal[] @relation("CommittedStateChangeVersion") + createdAt DateTime @default(now()) + + @@unique([novelId, version]) + @@index([novelId, createdAt]) + @@index([chapterId]) +} + +model StateChangeProposal { + id String @id @default(cuid()) + novelId String + chapterId String? + sourceSnapshotId String? + sourceType String + sourceStage String? + proposalType String + riskLevel String + status String @default("validated") + summary String + payloadJson String + evidenceJson String? + validationNotesJson String? + committedVersionId String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + committedVersion CanonicalStateVersion? @relation("CommittedStateChangeVersion", fields: [committedVersionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, createdAt]) + @@index([chapterId, createdAt]) + @@index([status, riskLevel, createdAt]) + @@index([committedVersionId]) +} + +model StoryPlan { + id String @id @default(cuid()) + novelId String + chapterId String? + parentId String? + sourceStateSnapshotId String? + level StoryPlanLevel + planRole StoryPlanRole? + phaseLabel String? + title String + objective String + participantsJson String? + revealsJson String? + riskNotesJson String? + mustAdvanceJson String? + mustPreserveJson String? + sourceIssueIdsJson String? + replannedFromPlanId String? + hookTarget String? + status String @default("draft") + externalRef String? + rawPlanJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + parent StoryPlan? @relation("StoryPlanHierarchy", fields: [parentId], references: [id], onDelete: SetNull) + children StoryPlan[] @relation("StoryPlanHierarchy") + sourceStateSnapshot StoryStateSnapshot? @relation(fields: [sourceStateSnapshotId], references: [id], onDelete: SetNull) + scenes ChapterPlanScene[] + replanRuns ReplanRun[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, level, createdAt]) + @@index([novelId, level, externalRef]) + @@index([novelId, level, chapterId]) + @@index([chapterId, createdAt]) + @@index([externalRef]) + @@index([sourceStateSnapshotId]) +} + +model ChapterPlanScene { + id String @id @default(cuid()) + planId String + sortOrder Int + title String + objective String? + conflict String? + reveal String? + emotionBeat String? + plan StoryPlan @relation(fields: [planId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([planId, sortOrder]) +} + +model ReplanRun { + id String @id @default(cuid()) + novelId String + chapterId String? + sourcePlanId String? + triggerType String + reason String + outputSummary String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + sourcePlan StoryPlan? @relation(fields: [sourcePlanId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) + @@index([chapterId, createdAt]) +} + +model AuditReport { + id String @id @default(cuid()) + novelId String + chapterId String + auditType AuditType + overallScore Int? + summary String? + legacyScoreJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) + issues AuditIssue[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, chapterId, auditType, createdAt]) + @@index([chapterId, createdAt]) +} + +model AuditIssue { + id String @id @default(cuid()) + reportId String + auditType AuditType + severity String + code String + description String + evidence String + fixSuggestion String + status AuditIssueStatus @default(open) + report AuditReport @relation(fields: [reportId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([reportId, status]) + @@index([auditType, severity]) +} + +model KnowledgeDocument { + id String @id @default(cuid()) + title String + fileName String + kind KnowledgeDocumentKind @default(user_upload) + sourceAnalysisId String? @unique + status KnowledgeDocumentStatus @default(enabled) + activeVersionId String? + activeVersionNumber Int @default(0) + latestIndexStatus KnowledgeIndexStatus @default(idle) + lastIndexedAt DateTime? + activeVersion KnowledgeDocumentVersion? @relation("KnowledgeDocumentActiveVersion", fields: [activeVersionId], references: [id], onDelete: SetNull) + versions KnowledgeDocumentVersion[] @relation("KnowledgeDocumentVersions") + bindings KnowledgeBinding[] + bookAnalyses BookAnalysis[] + publishedFromAnalyses BookAnalysis[] @relation("BookAnalysisPublishedDocument") + continuationNovels Novel[] @relation("NovelKnowledgeContinuation") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([kind, updatedAt]) + @@index([title]) +} + +model KnowledgeDocumentVersion { + id String @id @default(cuid()) + documentId String + versionNumber Int + content String + contentHash String + charCount Int + document KnowledgeDocument @relation("KnowledgeDocumentVersions", fields: [documentId], references: [id], onDelete: Cascade) + activeForDocuments KnowledgeDocument[] @relation("KnowledgeDocumentActiveVersion") + chapters DocumentChapter[] + bookAnalyses BookAnalysis[] + sourceCaches BookAnalysisSourceCache[] + createdAt DateTime @default(now()) + + @@unique([documentId, versionNumber]) + @@index([documentId, createdAt]) + @@index([contentHash]) +} + +model DocumentChapter { + id String @id @default(cuid()) + documentVersionId String + chapterIndex Int + title String + startOffset Int + endOffset Int + charCount Int + summary String? + splitter String @default("rule") + documentVersion KnowledgeDocumentVersion @relation(fields: [documentVersionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([documentVersionId, chapterIndex]) + @@index([documentVersionId, startOffset]) +} + +model KnowledgeBinding { + id String @id @default(cuid()) + targetType KnowledgeBindingTargetType + targetId String + documentId String + sourceAnalysisId String? + document KnowledgeDocument @relation(fields: [documentId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([targetType, targetId, documentId]) + @@index([targetType, targetId]) + @@index([documentId]) + @@index([sourceAnalysisId]) +} + +model BookAnalysis { + id String @id @default(cuid()) + documentId String + documentVersionId String + title String + status BookAnalysisStatus @default(queued) + summary String? + provider String? + model String? + temperature Float? + maxTokens Int? + budgetTokens Int? + usedTokens Int? @default(0) + userFocusInstruction String? + sourceStartChapterIndex Int? + sourceEndChapterIndex Int? + sourceStartOffset Int? + sourceEndOffset Int? + sourceScopeLabel String? + progress Float @default(0) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + attemptCount Int @default(0) + maxAttempts Int @default(1) + lastError String? + lastRunAt DateTime? + publishedDocumentId String? + document KnowledgeDocument @relation(fields: [documentId], references: [id], onDelete: Cascade) + documentVersion KnowledgeDocumentVersion @relation(fields: [documentVersionId], references: [id], onDelete: NoAction) + publishedDocument KnowledgeDocument? @relation("BookAnalysisPublishedDocument", fields: [publishedDocumentId], references: [id], onDelete: SetNull) + continuationNovels Novel[] @relation("NovelContinuationBookAnalysis") + sections BookAnalysisSection[] + characters BookAnalysisCharacter[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([documentId, status]) + @@index([documentVersionId]) + @@index([status, updatedAt]) +} + +model BookAnalysisSourceCache { + id String @id @default(cuid()) + documentVersionId String + provider String + model String + temperature Float + notesMaxTokens Int + sourceScopeKey String @default("full") + segmentVersion Int @default(1) + segmentCount Int + notesJson String + documentVersion KnowledgeDocumentVersion @relation(fields: [documentVersionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([documentVersionId, sourceScopeKey, provider, model, temperature, notesMaxTokens, segmentVersion]) + @@index([documentVersionId, updatedAt]) +} + +model BookAnalysisSection { + id String @id @default(cuid()) + analysisId String + sectionKey String + title String + status BookAnalysisSectionStatus @default(idle) + aiContent String? + editedContent String? + notes String? + focusInstruction String? + structuredDataJson String? + normalizationWarningsJson String? + evidenceJson String? + frozen Boolean @default(false) + sortOrder Int + analysis BookAnalysis @relation(fields: [analysisId], references: [id], onDelete: Cascade) + updatedAt DateTime @updatedAt + + @@unique([analysisId, sectionKey]) + @@index([analysisId, sortOrder]) +} + +model BookAnalysisCharacter { + id String @id @default(cuid()) + analysisId String + name String + role String + status String @default("candidate") + briefDescription String? + importance String? + occurringChaptersJson String? + lastGenerationError String? + generationDepth String @default("standard") + selectedDimensionsJson String? + profileJson String? + depthMetadataJson String? + profileSectionsJson String? + evidenceJson String? + sortOrder Int @default(0) + analysis BookAnalysis @relation(fields: [analysisId], references: [id], onDelete: Cascade) + arcs BookAnalysisCharacterArc[] + scenes BookAnalysisCharacterScene[] + appearance BookAnalysisCharacterAppearance? + appearanceSnapshots BookAnalysisCharacterAppearanceSnapshot[] + appearanceTerms BookAnalysisCharacterAppearanceTerm[] + imageTasks ImageGenerationTask[] + imageAssets ImageAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([analysisId, sortOrder]) + @@index([analysisId, name]) + @@index([analysisId, status]) +} + +model BookAnalysisCharacterAppearance { + id String @id @default(cuid()) + characterId String @unique + coveragePercent Int @default(0) + consolidatedAppearanceJson String? + variantPolicyJson String? + lastIndexedChapterIndex Int? + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + snapshots BookAnalysisCharacterAppearanceSnapshot[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([coveragePercent]) +} + +model BookAnalysisCharacterAppearanceSnapshot { + id String @id @default(cuid()) + appearanceId String + characterId String + chapterIndex Int + chapterTitle String? + appearanceJson String? + evidenceJson String? + summaryCaption String? + contextSceneRefsJson String? + manuallyEdited Boolean @default(false) + appearance BookAnalysisCharacterAppearance @relation(fields: [appearanceId], references: [id], onDelete: Cascade) + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + images BookAnalysisCharacterAppearanceImage[] + terms BookAnalysisCharacterAppearanceTerm[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([characterId, chapterIndex]) + @@index([appearanceId, chapterIndex]) + @@index([characterId, chapterIndex]) + @@index([chapterIndex]) +} + +model BookAnalysisCharacterAppearanceTerm { + id String @id @default(cuid()) + characterId String + snapshotId String + chapterIndex Int + text String + category String? + confidence Float? + stability String? + evidenceJson String? + status String @default("pending") + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + snapshot BookAnalysisCharacterAppearanceSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([snapshotId, text]) + @@index([characterId, status, updatedAt]) + @@index([snapshotId]) + @@index([chapterIndex]) +} + +model BookAnalysisCharacterAppearanceImage { + id String @id @default(cuid()) + snapshotId String + generationTaskId String? + imageAssetId String? + imagePromptJson String? + referenceAssetIdsJson String? + snapshot BookAnalysisCharacterAppearanceSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + generationTask ImageGenerationTask? @relation(fields: [generationTaskId], references: [id], onDelete: SetNull) + imageAsset ImageAsset? @relation(fields: [imageAssetId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([imageAssetId]) + @@index([generationTaskId]) + @@index([snapshotId]) +} + +model BookAnalysisCharacterArc { + id String @id @default(cuid()) + characterId String + chapterIndex Int? + stageLabel String + stateSnapshotJson String? + evidenceJson String? + sortOrder Int @default(0) + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([characterId, sortOrder]) + @@index([chapterIndex]) +} + +model BookAnalysisCharacterScene { + id String @id @default(cuid()) + characterId String + sceneLabel String + sceneType String? + performanceJson String? + evidenceJson String? + sortOrder Int @default(0) + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([characterId, sortOrder]) +} + +model KnowledgeChunk { + id String @id @default(cuid()) + tenantId String @default("default") + ownerType RagOwnerType + ownerId String + novelId String? + worldId String? + title String? + chunkText String + chunkHash String + chunkOrder Int + tokenEstimate Int @default(0) + language String @default("zh") + metadataJson String? + facetKeys String? + chapterAnchor String? + embedProvider String + embedModel String + embedVersion Int @default(1) + indexedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([tenantId, ownerType, ownerId]) + @@index([tenantId, novelId]) + @@index([tenantId, worldId]) + @@index([facetKeys]) + @@index([chapterAnchor]) + @@index([chunkHash]) +} + +model RagIndexJob { + id String @id @default(cuid()) + tenantId String @default("default") + jobType RagJobType + ownerType RagOwnerType + ownerId String + status RagJobStatus @default(queued) + attempts Int @default(0) + maxAttempts Int @default(5) + runAfter DateTime @default(now()) + payloadJson String? + lastError String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, runAfter]) + @@index([tenantId, ownerType, ownerId]) +} + +model RagRetrievalTrace { + id String @id @default(cuid()) + tenantId String @default("default") + novelId String? + worldId String? + queryDigest String + queryPreview String? + scopeJson String? + candidateCounts String @default("{}") + hitsJson String @default("[]") + timingsJson String @default("{}") + fallbackTriggered Boolean @default(false) + rerankerUsed Boolean @default(false) + createdAt DateTime @default(now()) + + @@index([tenantId, createdAt]) + @@index([tenantId, novelId, createdAt]) + @@index([queryDigest]) +} + +model NovelSideEffectJob { + id String @id @default(cuid()) + novelId String? + jobType String + status String @default("pending") + idempotencyKey String @unique + payloadVersion Int @default(1) + payloadJson String + attempts Int @default(0) + maxAttempts Int @default(5) + runAfter DateTime @default(now()) + leaseOwner String? + leaseExpiresAt DateTime? + lastError String? + finishedAt DateTime? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, runAfter]) + @@index([novelId, status, updatedAt]) + @@index([leaseOwner, leaseExpiresAt]) + @@index([jobType, status, runAfter]) +} + +model TaskCenterArchive { + id String @id @default(cuid()) + taskKind String + taskId String + archivedAt DateTime @default(now()) + + @@unique([taskKind, taskId]) + @@index([taskKind, archivedAt]) +} + +model StoryTimelineEvent { + id String @id @default(cuid()) + novelId String + chapterId String? + chapterIndex Int? + eventOrder Int + storyDayIndex Int? + storyTimeLabel String? + title String + summary String + type String + status String + visibility String + source String + participantIdsJson String @default("[]") + locationId String? + factionIdsJson String @default("[]") + prerequisiteIdsJson String @default("[]") + consequenceIdsJson String @default("[]") + stateChangesJson String @default("[]") + eventKey String? + confidence Float @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, chapterIndex]) + @@index([novelId, eventOrder]) + @@index([novelId, status]) + @@index([novelId, eventKey]) +} + +model ChapterTimeAnchor { + id String @id @default(cuid()) + novelId String + chapterId String + chapterIndex Int + storyDayIndex Int? + timeLabel String + startsAfterIdsJson String @default("[]") + plannedEventIdsJson String @default("[]") + endedWithIdsJson String @default("[]") + previousHookIdsJson String @default("[]") + nextHookIdsJson String @default("[]") + forbiddenEventIdsJson String @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, chapterId]) + @@index([novelId, chapterIndex]) +} + +model TimelineHook { + id String @id @default(cuid()) + novelId String + createdInChapterId String + createdInChapterIndex Int + expectedResolveByChapterIndex Int? + resolveMode String @default("long_arc") + blocking Boolean @default(false) + resolvedInChapterId String? + resolvedInChapterIndex Int? + title String + description String + status String + priority String + relatedEventIdsJson String @default("[]") + participantIdsJson String @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status]) + @@index([novelId, resolveMode, blocking]) + @@index([novelId, createdInChapterIndex]) + @@index([novelId, expectedResolveByChapterIndex]) +} + +model TimelineConstraint { + id String @id @default(cuid()) + novelId String + chapterId String? + chapterIndex Int? + type String + severity String + description String + relatedEventIdsJson String @default("[]") + relatedHookIdsJson String @default("[]") + relatedCharacterIdsJson String @default("[]") + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, chapterIndex]) + @@index([novelId, active]) +} + +model TimelineCheckReport { + id String @id @default(cuid()) + novelId String + chapterId String + chapterIndex Int + status String + score Float + issuesJson String @default("[]") + createdAt DateTime @default(now()) + + @@index([novelId, chapterIndex]) + @@index([novelId, status]) +} + +model NovelFactEntry { + id String @id @default(cuid()) + novelId String + chapterOrder Int + text String + category String @default("completed") + source String @default("auto") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelId, chapterOrder]) + @@index([novelId, category]) +} + +// ============================================================ +// 短剧创作模块(Drama)—— 独立 bounded context +// 所有 Drama* 表自成一套,不与 Novel 建外键关系(仅软引用 sourceRef)。 +// 与小说模块的唯一接触点是 services/drama/source/NovelSourceAdapter(只读)。 +// 分类字段统一用 String + 注释(不用 enum),降低双 schema 维护成本。 +// ============================================================ + +model DramaProject { + id String @id @default(cuid()) + title String + source String @default("original") + sourceRef String? + sourceInput String? + track String? + theme String? + orientation String @default("vertical_paid") + targetEpisodes Int @default(80) + strategy String? + status String @default("draft") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sourceBundle DramaSourceBundle? + characters DramaCharacter[] + episodes DramaEpisode[] + facts DramaFact[] + characterLibraryItems DramaCharacterLibrary[] + storyboards DramaStoryboard[] + videoPrompts DramaVideoPrompt[] + batchJobs DramaBatchJob[] + + @@index([source]) + @@index([status]) +} + +model DramaSourceBundle { + id String @id @default(cuid()) + projectId String @unique + synopsis String? + beats String? + worldNotes String? + hardFacts String? + rawText String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) +} + +model DramaCharacter { + id String @id @default(cuid()) + projectId String + name String + archetype String? + persona String? + speechStyle String? + visualAnchor String? + voiceProfile String? + relations String? + sourceCharacterRef String? + // 角色形象图 JSON: { status:"idle"|"generating"|"done"|"error", url?:string, prompt?:string, generatedAt?:string } + portraitData String? + // 三视图 JSON: [{ view:"front"|"side"|"back", url?:string, status:string, ... }, ...] + threeViewData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model DramaEpisode { + id String @id @default(cuid()) + projectId String + order Int + title String + content String? @default("") + hookOpening String? + cliffhanger String? + hookType String? + isPaywall Boolean @default(false) + emotionNet Int? + beatSheet String? + sourceMap String? + durationSec Int? + status String @default("planned") + qualityFlags String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + storyboards DramaStoryboard[] + videoPrompts DramaVideoPrompt[] + batchJobs DramaBatchJob[] + + @@unique([projectId, order]) + @@index([projectId, status]) +} + +model DramaFact { + id String @id @default(cuid()) + projectId String + episodeOrder Int + text String + category String @default("completed") + source String @default("auto") + createdAt DateTime @default(now()) + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId, episodeOrder]) + @@index([projectId, category]) +} + +model DramaCharacterLibrary { + id String @id @default(cuid()) + projectId String? + name String + archetype String? + persona String? + speechStyle String? + visualAnchor String? + voiceProfile String? + relations String? + tags String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject? @relation(fields: [projectId], references: [id], onDelete: SetNull) + + @@index([projectId]) + @@index([name]) +} + +model DramaStoryboard { + id String @id @default(cuid()) + projectId String + episodeId String + version Int @default(1) + status String @default("draft") + summary String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode DramaEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + shots DramaShot[] + + @@index([projectId]) + @@index([episodeId]) +} + +model DramaShot { + id String @id @default(cuid()) + storyboardId String + order Int + shotSize String? + cameraMove String? + durationSec Int? + location String? + action String + dialogue String? + characterRefs String? + visualPrompt String? + keyframeData String? + dialogueAudioData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + storyboard DramaStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade) + + @@unique([storyboardId, order]) + @@index([storyboardId]) +} + +model DramaVideoPrompt { + id String @id @default(cuid()) + projectId String + episodeId String? + shotId String? + provider String @default("mock") + prompt String + negativePrompt String? + aspectRatio String @default("9:16") + durationSec Int? + status String @default("prompted") + version Int @default(1) + supersededById String? + providerTaskId String? + resultUrl String? + failureReason String? + providerResult String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode DramaEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) + + @@index([projectId]) + @@index([episodeId]) + @@index([projectId, shotId, version]) + @@index([provider, status]) +} + +model DramaBatchJob { + id String @id @default(cuid()) + projectId String + episodeId String? + type String + status String @default("pending") + progress String @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode DramaEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) + + @@index([projectId, createdAt]) + @@index([episodeId, status]) + @@index([type, status]) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Comic(AI 漫画)模块 +// 低耦合要点:所有外键对 Novel 零引用,sourceRef 为软引用(不建外键,保证可拆分) +// ───────────────────────────────────────────────────────────────────────────── + +model ComicProject { + id String @id @default(cuid()) + title String + sourceType String @default("original") + sourceRef String? + sourceInput String? + trackId String? + stylePreset String? + status String @default("draft") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sourceBundle ComicSourceBundle? + characters ComicCharacter[] + characterAssets ComicCharacterAsset[] + scenes ComicScene[] + episodes ComicEpisode[] + facts ComicFact[] + uploadAssets ComicUploadAsset[] + exportJobs ComicExportJob[] + batchJobs ComicBatchJob[] + + @@index([sourceType]) + @@index([status]) +} + +model ComicSourceBundle { + id String @id @default(cuid()) + projectId String @unique + bundleJson String + importedAt DateTime @default(now()) + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) +} + +model ComicCharacter { + id String @id @default(cuid()) + projectId String + name String + gender String @default("unknown") + persona String? + visualAnchor String? + sheetData String? + sourceCharacterRef String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + assets ComicCharacterAsset[] + + @@index([projectId]) +} + +model ComicCharacterAsset { + id String @id @default(cuid()) + characterId String + projectId String + assetType String + name String + description String? + imageData String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + character ComicCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([characterId]) + @@index([projectId]) +} + +model ComicScene { + id String @id @default(cuid()) + projectId String + name String + sceneType String @default("interior") + bible String? + sheetData String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model ComicEpisode { + id String @id @default(cuid()) + projectId String + order Int + title String? + hookType String? + cliffhanger String? + isPaywalled Boolean @default(false) + outline String? + sourceText String? + status String @default("draft") + scriptConfig String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + panels ComicPanel[] + batchJobs ComicBatchJob[] + + @@unique([projectId, order]) + @@index([projectId, status]) +} + +model ComicPanel { + id String @id @default(cuid()) + episodeId String + order Int + panelType String? + action String + dialogues String? + characterRefs String? + sceneRef String? + visualPrompt String? + densityLevel String? + focus String? + layoutData String? + imageData String? + letteredData String? + motionData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + episode ComicEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + + @@unique([episodeId, order]) + @@index([episodeId]) +} + +model ComicFact { + id String @id @default(cuid()) + projectId String + text String + category String @default("completed") + episodeOrder Int? + createdAt DateTime @default(now()) + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model ComicUploadAsset { + id String @id @default(cuid()) + projectId String + kind String + filePath String + refId String? + metadata String? + createdAt DateTime @default(now()) + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId, kind]) +} + +model ComicExportJob { + id String @id @default(cuid()) + projectId String + episodeId String? + format String + spec String? + status String @default("pending") + artifacts String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId, createdAt]) + @@index([format, status]) +} + +model ComicBatchJob { + id String @id @default(cuid()) + projectId String + episodeId String? + type String + status String @default("pending") + progress String @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode ComicEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) + + @@index([projectId, createdAt]) + @@index([episodeId, status]) + @@index([type, status]) +} diff --git a/android-app/app/src/main/assets/nodejs/schema.prisma b/android-app/app/src/main/assets/nodejs/schema.prisma new file mode 100644 index 0000000000..299ba2885b --- /dev/null +++ b/android-app/app/src/main/assets/nodejs/schema.prisma @@ -0,0 +1,3834 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" +} + +enum ChapterGenerationState { + planned + drafted + reviewed + repaired + approved + published +} + +enum PipelineJobStatus { + queued + running + succeeded + failed + cancelled +} + +enum BeatStatus { + planned + completed + skipped +} + +enum FactCategory { + world + character + timeline + plot + rule +} + +enum RagOwnerType { + novel + chapter + world + character + bible + chapter_summary + consistency_fact + character_timeline + world_library_item + knowledge_document + chat_message +} + +enum RagJobType { + upsert + delete + rebuild +} + +enum RagJobStatus { + queued + running + succeeded + failed + cancelled +} + +enum KnowledgeDocumentStatus { + enabled + disabled + archived +} + +enum KnowledgeDocumentKind { + user_upload + analysis_published +} + +enum KnowledgeIndexStatus { + idle + queued + running + succeeded + failed +} + +enum KnowledgeBindingTargetType { + novel + world +} + +enum BookAnalysisStatus { + draft + queued + running + succeeded + failed + cancelled + archived +} + +enum BookAnalysisSectionStatus { + idle + running + succeeded + failed +} + +enum ImageSceneType { + character + novel_cover + chapter_illustration + book_analysis_character +} + +enum ProjectMode { + ai_led + co_pilot + draft_mode + auto_pipeline +} + +enum CreationExperience { + simple + professional +} + +enum NarrativePov { + first_person + third_person + mixed +} + +enum PacePreference { + slow + balanced + fast +} + +enum EmotionIntensity { + low + medium + high +} + +enum AIFreedom { + low + medium + high +} + +enum ProjectProgressStatus { + not_started + in_progress + completed + rework + blocked +} + +enum StorylineVersionStatus { + draft + active + frozen +} + +enum VolumePlanVersionStatus { + draft + active + frozen +} + +enum ChapterStatus { + unplanned + pending_generation + generating + pending_review + needs_repair + completed +} + +enum PipelineRunMode { + fast + polish +} + +enum PipelineRepairMode { + detect_only + light_repair + heavy_repair + continuity_only + character_only + ending_only +} + +enum AgentRunStatus { + queued + running + waiting_approval + succeeded + failed + cancelled +} + +enum NovelWorkflowLane { + manual_create + auto_director +} + +enum NovelWorkflowTaskStatus { + queued + running + waiting_approval + succeeded + failed + cancelled +} + +enum AgentStepType { + planning + tool_call + tool_result + reasoning + write + approval + answer +} + +enum AgentStepStatus { + pending + running + succeeded + failed + cancelled +} + +enum AgentApprovalStatus { + pending + approved + rejected + expired +} + +enum CreativeHubThreadStatus { + idle + busy + interrupted + error +} + +enum StoryPlanLevel { + book + arc + chapter +} + +enum StoryPlanRole { + setup + progress + pressure + turn + payoff + cooldown +} + +enum AuditType { + continuity + character + plot + mode_fit +} + +enum AuditIssueStatus { + open + resolved + ignored +} + +enum PayoffLedgerScopeType { + book + volume + chapter +} + +enum PayoffLedgerStatus { + setup + hinted + pending_payoff + paid_off + failed + overdue +} + +enum StyleBindingTargetType { + novel + chapter + task +} + +enum AntiAiRuleType { + forbidden + risk + encourage +} + +enum AntiAiSeverity { + low + medium + high +} + +enum CharacterGender { + male + female + other + unknown +} + +model Novel { + id String @id @default(cuid()) + title String + description String? + targetAudience String? + bookSellingPoint String? + competingFeel String? + first30ChapterPromise String? + commercialTagsJson String? + status String @default("draft") + writingMode String @default("original") + projectMode ProjectMode? + creationExperience CreationExperience @default(professional) + narrativePov NarrativePov? + pacePreference PacePreference? + styleTone String? + emotionIntensity EmotionIntensity? + aiFreedom AIFreedom? + postGenerationStyleReviewEnabled Boolean @default(true) + defaultChapterLength Int? + estimatedChapterCount Int? + projectStatus ProjectProgressStatus? @default(not_started) + storylineStatus ProjectProgressStatus? @default(not_started) + outlineStatus ProjectProgressStatus? @default(not_started) + resourceReadyScore Int? + sourceNovelId String? + sourceKnowledgeDocumentId String? + continuationBookAnalysisId String? + continuationBookAnalysisSections String? + outline String? + structuredOutline String? + storyWorldSliceJson String? + storyWorldSliceOverridesJson String? + storyWorldSliceSchemaVersion Int @default(1) + genreId String? + primaryStoryModeId String? + secondaryStoryModeId String? + worldId String? + genre NovelGenre? @relation(fields: [genreId], references: [id]) + primaryStoryMode NovelStoryMode? @relation("NovelPrimaryStoryMode", fields: [primaryStoryModeId], references: [id], onDelete: SetNull) + secondaryStoryMode NovelStoryMode? @relation("NovelSecondaryStoryMode", fields: [secondaryStoryModeId], references: [id], onDelete: SetNull) + world World? @relation(fields: [worldId], references: [id], onDelete: SetNull) + sourceNovel Novel? @relation("NovelContinuation", fields: [sourceNovelId], references: [id], onDelete: SetNull) + continuationNovels Novel[] @relation("NovelContinuation") + sourceKnowledgeDocument KnowledgeDocument? @relation("NovelKnowledgeContinuation", fields: [sourceKnowledgeDocumentId], references: [id], onDelete: SetNull) + continuationBookAnalysis BookAnalysis? @relation("NovelContinuationBookAnalysis", fields: [continuationBookAnalysisId], references: [id], onDelete: SetNull) + chapters Chapter[] + characters Character[] + characterLibraryLinks CharacterLibraryLink[] + characterSyncProposals CharacterSyncProposal[] + characterRelations CharacterRelation[] + characterCastOptions CharacterCastOption[] + characterTimelines CharacterTimeline[] + characterCandidates CharacterCandidate[] + characterVolumeAssignments CharacterVolumeAssignment[] + characterFactionTracks CharacterFactionTrack[] + characterRelationStages CharacterRelationStage[] + characterMindSnapshots CharacterMindSnapshot[] + characterInfluenceProposals CharacterInfluenceProposal[] + characterDialogueSessions CharacterDialogueSession[] + characterDialogueInfluences CharacterDialogueInfluence[] + characterResourceItems CharacterResourceLedgerItem[] + characterResourceEvents CharacterResourceEvent[] + bible NovelBible? + plotBeats PlotBeat[] + chapterSummaries ChapterSummary[] + facts ConsistencyFact[] + generationJobs GenerationJob[] + agentRuns AgentRun[] + creativeDecisions CreativeDecision[] + novelSnapshots NovelSnapshot[] + storylineVersions StorylineVersion[] + volumePlans VolumePlan[] + volumePlanVersions VolumePlanVersion[] + qualityReports QualityReport[] + storyMacroPlan StoryMacroPlan? + bookContract BookContract? + storyStateSnapshots StoryStateSnapshot[] + canonicalStateVersions CanonicalStateVersion[] + stateChangeProposals StateChangeProposal[] + openConflicts OpenConflict[] + payoffLedgerItems PayoffLedgerItem[] + artifactSyncCheckpoints ChapterArtifactSyncCheckpoint[] + storyPlans StoryPlan[] + replanRuns ReplanRun[] + auditReports AuditReport[] + workflowTasks NovelWorkflowTask[] + autoApprovalRecords AutoDirectorAutoApprovalRecord[] + directorRuns DirectorRun[] + directorCommands DirectorRunCommand[] + directorEvents DirectorEvent[] + directorArtifacts DirectorArtifact[] + directorLlmUsageRecords DirectorLlmUsageRecord[] + promptAddendums PromptAddendum[] + promptSlotOverrides PromptSlotOverride[] + promptTemplateOverrides PromptTemplateOverride[] + imageTasks ImageGenerationTask[] + imageAssets ImageAsset[] + sideEffectJobs NovelSideEffectJob[] + novelWorld NovelWorld? + factEntries NovelFactEntry[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([genreId]) + @@index([primaryStoryModeId]) + @@index([secondaryStoryModeId]) + @@index([worldId]) + @@index([writingMode]) + @@index([sourceNovelId]) + @@index([sourceKnowledgeDocumentId]) + @@index([continuationBookAnalysisId]) +} + +model PromptAddendum { + id String @id @default(cuid()) + scope String + novelId String? + promptId String + title String + content String + enabled Boolean @default(true) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([scope, promptId, enabled]) + @@index([novelId, promptId, enabled]) + @@index([updatedAt]) +} + +model PromptSlotOverride { + id String @id @default(cuid()) + scope String + novelId String? + promptId String + baseVersion String + slots String @default("{}") + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([scope, novelId, promptId]) + @@index([promptId]) + @@index([novelId, promptId]) +} + +model PromptTemplateOverride { + id String @id @default(cuid()) + scope String @default("novel") + novelId String + promptId String + basePromptVersion String + mode String @default("official") + activeVersionId String? + versions PromptTemplateVersion[] + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([scope, novelId, promptId]) + @@index([promptId]) + @@index([novelId, promptId]) + @@index([activeVersionId]) +} + +model PromptTemplateVersion { + id String @id @default(cuid()) + overrideId String + versionNo Int + templateJson String + contextRefsJson String @default("{}") + compiledHash String + notes String? + override PromptTemplateOverride @relation(fields: [overrideId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([overrideId, versionNo]) + @@index([overrideId, createdAt]) +} + +model CreativeDecision { + id String @id @default(cuid()) + novelId String + chapterId String? + category String + content String + importance String @default("normal") + expiresAt Int? + sourceType String? + sourceRefId String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, createdAt]) +} + +model NovelSnapshot { + id String @id @default(cuid()) + novelId String + label String? + snapshotData String + triggerType String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) +} + +model Chapter { + id String @id @default(cuid()) + title String + content String? @default("") + order Int + generationState ChapterGenerationState @default(planned) + chapterStatus ChapterStatus? @default(unplanned) + targetWordCount Int? + conflictLevel Int? + revealLevel Int? + mustAvoid String? + taskSheet String? + sceneCards String? + repairHistory String? + qualityScore Int? + continuityScore Int? + characterScore Int? + pacingScore Int? + riskFlags String? + hook String? + expectation String? + novelId String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapterSummary ChapterSummary? + characterTimelines CharacterTimeline[] + characterCandidates CharacterCandidate[] @relation("CharacterCandidateSourceChapter") + characterFactionTracks CharacterFactionTrack[] + characterRelationStages CharacterRelationStage[] @relation("CharacterRelationStageChapter") + characterMindSnapshots CharacterMindSnapshot[] + resolvedCharacterInfluenceProposals CharacterInfluenceProposal[] @relation("CharacterInfluenceResolvedChapter") + resolvedCharacterDialogueInfluences CharacterDialogueInfluence[] @relation("CharacterDialogueInfluenceResolvedChapter") + facts ConsistencyFact[] + qualityReports QualityReport[] + storyStateSnapshots StoryStateSnapshot[] + canonicalStateVersions CanonicalStateVersion[] + stateChangeProposals StateChangeProposal[] + openConflicts OpenConflict[] + storyPlans StoryPlan[] + replanRuns ReplanRun[] + auditReports AuditReport[] + foreshadowSetupStates ForeshadowState[] @relation("ForeshadowSetupChapter") + foreshadowPayoffStates ForeshadowState[] @relation("ForeshadowPayoffChapter") + payoffLedgerLastTouchedItems PayoffLedgerItem[] @relation("PayoffLedgerLastTouchedChapter") + payoffLedgerSetupItems PayoffLedgerItem[] @relation("PayoffLedgerSetupChapter") + payoffLedgerPayoffItems PayoffLedgerItem[] @relation("PayoffLedgerPayoffChapter") + introducedCharacterResources CharacterResourceLedgerItem[] @relation("CharacterResourceIntroducedChapter") + lastTouchedCharacterResources CharacterResourceLedgerItem[] @relation("CharacterResourceLastTouchedChapter") + characterResourceEvents CharacterResourceEvent[] + artifactSyncCheckpoints ChapterArtifactSyncCheckpoint[] + volumeChapterPlans VolumeChapterPlan[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, order]) +} + +model ChapterArtifactSyncCheckpoint { + id String @id @default(cuid()) + novelId String + chapterId String + contentHash String + artifactType String + syncMode String + status String @default("succeeded") + sourceType String? + sourceStage String? + metadataJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, chapterId, contentHash, artifactType, syncMode]) + @@index([novelId, chapterId, artifactType, updatedAt]) +} + +model Character { + id String @id @default(cuid()) + name String + role String + gender CharacterGender @default(unknown) + castRole String? + storyFunction String? + relationToProtagonist String? + personality String? + background String? + development String? + identityLabel String? + factionLabel String? + stanceLabel String? + powerLevel String? + realm String? + currentLocation String? + availability String? + prohibitionsJson String @default("[]") + outerGoal String? + innerNeed String? + fear String? + wound String? + misbelief String? + secret String? + moralLine String? + firstImpression String? + appearance String? + physique String? + attireStyle String? + signatureDetail String? + voiceTexture String? + presenceImpression String? + arcStart String? + arcMidpoint String? + arcClimax String? + arcEnd String? + currentState String? + currentGoal String? + lastEvolvedAt DateTime? + novelId String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + baseCharacterId String? + libraryLink CharacterLibraryLink? + timelines CharacterTimeline[] + matchedCandidates CharacterCandidate[] @relation("CharacterCandidateMatchedCharacter") + volumeAssignments CharacterVolumeAssignment[] + factionTracks CharacterFactionTrack[] + outgoingRelationStages CharacterRelationStage[] @relation("CharacterRelationStageSource") + incomingRelationStages CharacterRelationStage[] @relation("CharacterRelationStageTarget") + mindSnapshots CharacterMindSnapshot[] + influenceProposals CharacterInfluenceProposal[] + dialogueSessions CharacterDialogueSession[] + dialogueInfluences CharacterDialogueInfluence[] + stateEntries CharacterState[] + outgoingRelations RelationState[] @relation("RelationStateSource") + incomingRelations RelationState[] @relation("RelationStateTarget") + outgoingCharacterRelations CharacterRelation[] @relation("CharacterRelationSource") + incomingCharacterRelations CharacterRelation[] @relation("CharacterRelationTarget") + ownedResourceItems CharacterResourceLedgerItem[] @relation("CharacterResourceOwner") + heldResourceItems CharacterResourceLedgerItem[] @relation("CharacterResourceHolder") + actedResourceEvents CharacterResourceEvent[] @relation("CharacterResourceEventActor") + fromResourceEvents CharacterResourceEvent[] @relation("CharacterResourceEventFromHolder") + toResourceEvents CharacterResourceEvent[] @relation("CharacterResourceEventToHolder") + syncProposals CharacterSyncProposal[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([baseCharacterId]) +} + +model CharacterMindSnapshot { + id String @id @default(cuid()) + novelId String + characterId String + sourceChapterId String? + sourceType String + currentInterpretation String + privateIntent String? + activePlan String? + emotionalStance String? + actionTendency String? + decisionTrigger String? + beliefsJson String @default("[]") + misbeliefsJson String @default("[]") + evidenceJson String @default("[]") + confidence Float? + isCurrent Boolean @default(true) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + sourceChapter Chapter? @relation(fields: [sourceChapterId], references: [id], onDelete: SetNull) + influenceProposals CharacterInfluenceProposal[] @relation("CharacterInfluenceSourceMind") + dialogueSessions CharacterDialogueSession[] @relation("CharacterDialogueSourceMind") + dialogueInfluences CharacterDialogueInfluence[] @relation("CharacterDialogueInfluenceSourceMind") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, isCurrent, updatedAt]) + @@index([sourceChapterId]) +} + +model CharacterInfluenceProposal { + id String @id @default(cuid()) + novelId String + characterId String + proposalSetId String + sourceMindSnapshotId String? + title String + directionSummary String + recommendationReason String + isRecommended Boolean @default(false) + behaviorGuidance String + emotionalGuidance String? + relationTension String? + readerPayoff String + risk String + observableSignalsJson String @default("[]") + evidenceJson String @default("[]") + confidence Float? + authorIntent String? + targetStartChapterOrder Int + targetEndChapterOrder Int + status String @default("draft") + acceptedAt DateTime? + appliedAt DateTime? + resolvedChapterId String? + resolutionEvidenceJson String @default("[]") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + sourceMindSnapshot CharacterMindSnapshot? @relation("CharacterInfluenceSourceMind", fields: [sourceMindSnapshotId], references: [id], onDelete: SetNull) + resolvedChapter Chapter? @relation("CharacterInfluenceResolvedChapter", fields: [resolvedChapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, status, targetStartChapterOrder, targetEndChapterOrder]) + @@index([proposalSetId]) + @@index([resolvedChapterId]) +} + +model CharacterDialogueSession { + id String @id @default(cuid()) + novelId String + characterId String + sourceMindSnapshotId String? + status String @default("active") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + sourceMindSnapshot CharacterMindSnapshot? @relation("CharacterDialogueSourceMind", fields: [sourceMindSnapshotId], references: [id], onDelete: SetNull) + turns CharacterDialogueTurn[] + influences CharacterDialogueInfluence[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, status, updatedAt]) + @@index([sourceMindSnapshotId]) +} + +model CharacterDialogueTurn { + id String @id @default(cuid()) + sessionId String + role String + content String + session CharacterDialogueSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([sessionId, createdAt]) +} + +model CharacterDialogueInfluence { + id String @id @default(cuid()) + novelId String + characterId String + sessionId String? + conversationSessionId String? + sourceMindSnapshotId String? + summary String + behaviorGuidance String + emotionalGuidance String? + relationTension String? + evidenceJson String @default("[]") + confidence Float? + targetStartChapterOrder Int + targetEndChapterOrder Int + status String @default("draft") + activatedAt DateTime? + appliedAt DateTime? + resolvedChapterId String? + resolutionEvidenceJson String @default("[]") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + session CharacterDialogueSession? @relation(fields: [sessionId], references: [id], onDelete: SetNull) + conversationSession CharacterConversationSession? @relation(fields: [conversationSessionId], references: [id], onDelete: SetNull) + sourceMindSnapshot CharacterMindSnapshot? @relation("CharacterDialogueInfluenceSourceMind", fields: [sourceMindSnapshotId], references: [id], onDelete: SetNull) + resolvedChapter Chapter? @relation("CharacterDialogueInfluenceResolvedChapter", fields: [resolvedChapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, status, targetStartChapterOrder, targetEndChapterOrder]) + @@index([sessionId, createdAt]) + @@index([conversationSessionId, createdAt]) + @@index([resolvedChapterId]) +} + +model CharacterConversationSession { + id String @id @default(cuid()) + subjectKind String + subjectId String + scopeKind String + scopeId String? + interactionPolicy String + chapterAnchor Int? + sourceSnapshotJson String @default("{}") + evidenceBoundaryJson String @default("[]") + legacyDialogueSessionId String? @unique + status String @default("active") + turns CharacterConversationTurn[] + influences CharacterDialogueInfluence[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([subjectKind, subjectId, scopeKind, scopeId, status, updatedAt]) + @@index([scopeKind, scopeId, updatedAt]) +} + +model CharacterConversationTurn { + id String @id @default(cuid()) + sessionId String + role String + content String + evidenceJson String @default("[]") + uncertainty String? + session CharacterConversationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([sessionId, createdAt]) +} + +model CharacterRelation { + id String @id @default(cuid()) + novelId String + sourceCharacterId String + targetCharacterId String + surfaceRelation String + hiddenTension String? + conflictSource String? + secretAsymmetry String? + dynamicLabel String? + nextTurnPoint String? + trustScore Int? + conflictScore Int? + intimacyScore Int? + dependencyScore Int? + evidence String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceCharacter Character @relation("CharacterRelationSource", fields: [sourceCharacterId], references: [id], onDelete: Cascade) + targetCharacter Character @relation("CharacterRelationTarget", fields: [targetCharacterId], references: [id], onDelete: Cascade) + stages CharacterRelationStage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, sourceCharacterId, targetCharacterId]) + @@index([novelId, updatedAt]) + @@index([sourceCharacterId]) + @@index([targetCharacterId]) +} + +model CharacterCastOption { + id String @id @default(cuid()) + novelId String + title String + summary String + whyItWorks String? + recommendedReason String? + status String @default("draft") + sourceStoryInput String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + members CharacterCastOptionMember[] + relations CharacterCastOptionRelation[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, updatedAt]) +} + +model CharacterCastOptionMember { + id String @id @default(cuid()) + optionId String + sortOrder Int + name String + role String + gender CharacterGender @default(unknown) + castRole String + relationToProtagonist String? + storyFunction String + shortDescription String? + personality String? + background String? + development String? + identityLabel String? + factionLabel String? + stanceLabel String? + powerLevel String? + realm String? + currentLocation String? + availability String? + prohibitionsJson String @default("[]") + outerGoal String? + innerNeed String? + fear String? + wound String? + misbelief String? + secret String? + moralLine String? + firstImpression String? + option CharacterCastOption @relation(fields: [optionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([optionId, sortOrder]) +} + +model CharacterCastOptionRelation { + id String @id @default(cuid()) + optionId String + sortOrder Int + sourceName String + targetName String + surfaceRelation String + hiddenTension String? + conflictSource String? + secretAsymmetry String? + dynamicLabel String? + nextTurnPoint String? + option CharacterCastOption @relation(fields: [optionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([optionId, sortOrder]) +} + +model CharacterTimeline { + id String @id @default(cuid()) + novelId String + characterId String + chapterId String? + chapterOrder Int? + title String + content String + source String @default("auto") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId]) + @@index([characterId, chapterOrder]) + @@index([chapterId]) +} + +model CharacterCandidate { + id String @id @default(cuid()) + novelId String + sourceChapterId String? + proposedName String + proposedRole String? + summary String? + evidenceJson String? + matchedCharacterId String? + status String @default("pending") + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceChapter Chapter? @relation("CharacterCandidateSourceChapter", fields: [sourceChapterId], references: [id], onDelete: SetNull) + matchedCharacter Character? @relation("CharacterCandidateMatchedCharacter", fields: [matchedCharacterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([sourceChapterId]) + @@index([matchedCharacterId]) +} + +model CharacterVolumeAssignment { + id String @id @default(cuid()) + novelId String + characterId String + volumeId String + roleLabel String? + responsibility String + appearanceExpectation String? + plannedChapterOrdersJson String? + isCore Boolean @default(false) + absenceWarningThreshold Int @default(3) + absenceHighRiskThreshold Int @default(5) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + volume VolumePlan @relation(fields: [volumeId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([characterId, volumeId]) + @@index([novelId, volumeId, isCore]) + @@index([volumeId, characterId]) +} + +model CharacterFactionTrack { + id String @id @default(cuid()) + novelId String + characterId String + volumeId String? + chapterId String? + chapterOrder Int? + factionLabel String + stanceLabel String? + summary String? + sourceType String + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + volume VolumePlan? @relation(fields: [volumeId], references: [id], onDelete: SetNull) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, characterId, createdAt]) + @@index([volumeId, characterId, createdAt]) + @@index([chapterId, createdAt]) +} + +model CharacterRelationStage { + id String @id @default(cuid()) + novelId String + relationId String? + sourceCharacterId String + targetCharacterId String + volumeId String? + chapterId String? + chapterOrder Int? + stageLabel String + stageSummary String + nextTurnPoint String? + sourceType String + confidence Float? + isCurrent Boolean @default(true) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + relation CharacterRelation? @relation(fields: [relationId], references: [id], onDelete: SetNull) + sourceCharacter Character @relation("CharacterRelationStageSource", fields: [sourceCharacterId], references: [id], onDelete: Cascade) + targetCharacter Character @relation("CharacterRelationStageTarget", fields: [targetCharacterId], references: [id], onDelete: Cascade) + volume VolumePlan? @relation(fields: [volumeId], references: [id], onDelete: SetNull) + chapter Chapter? @relation("CharacterRelationStageChapter", fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, isCurrent, updatedAt]) + @@index([sourceCharacterId, targetCharacterId, isCurrent]) + @@index([relationId]) + @@index([chapterId]) +} + +model BaseCharacter { + id String @id @default(cuid()) + name String + role String + personality String + background String + development String + appearance String? + weaknesses String? + interests String? + keyEvents String? + tags String @default("") + category String + sourceType String @default("manual") + sourceRefId String? + revisions BaseCharacterRevision[] + libraryLinks CharacterLibraryLink[] + syncProposals CharacterSyncProposal[] + imageTasks ImageGenerationTask[] + imageAssets ImageAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sourceType, sourceRefId]) +} + +model BaseCharacterRevision { + id String @id @default(cuid()) + baseCharacterId String + version Int + snapshotJson String + changeSummary String? + sourceType String @default("manual") + sourceRefId String? + baseCharacter BaseCharacter @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + libraryLinks CharacterLibraryLink[] + syncProposals CharacterSyncProposal[] + createdAt DateTime @default(now()) + + @@unique([baseCharacterId, version]) + @@index([baseCharacterId, createdAt]) +} + +model CharacterLibraryLink { + id String @id @default(cuid()) + novelId String + characterId String @unique + baseCharacterId String + baseRevisionId String? + syncPolicy String @default("manual_review") + linkStatus String @default("linked") + localOverridesJson String? + lastSyncedAt DateTime? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + baseCharacter BaseCharacter @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + baseRevision BaseCharacterRevision? @relation(fields: [baseRevisionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, linkStatus, updatedAt]) + @@index([baseCharacterId, linkStatus, updatedAt]) + @@index([baseRevisionId]) +} + +model CharacterSyncProposal { + id String @id @default(cuid()) + novelId String? + characterId String? + baseCharacterId String? + baseRevisionId String? + direction String + status String @default("pending_review") + confidence Float? + summary String + payloadJson String + safeUpdatesJson String? + novelOnlyUpdatesJson String? + riskyUpdatesJson String? + recommendedAction String? + sourceType String @default("manual") + sourceRefId String? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + character Character? @relation(fields: [characterId], references: [id], onDelete: SetNull) + baseCharacter BaseCharacter? @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + baseRevision BaseCharacterRevision? @relation(fields: [baseRevisionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([characterId, status, updatedAt]) + @@index([baseCharacterId, status, updatedAt]) + @@index([baseRevisionId]) +} + +model ImageGenerationTask { + id String @id @default(cuid()) + sceneType ImageSceneType @default(character) + baseCharacterId String? + novelId String? + bookAnalysisCharacterId String? + provider String + model String + prompt String + negativePrompt String? + stylePreset String? + referenceImageAssetIdsJson String? + size String @default("1024x1024") + imageCount Int @default(1) + seed Int? + status PipelineJobStatus @default(queued) + progress Float @default(0) + retryCount Int @default(0) + maxRetries Int @default(2) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + error String? + startedAt DateTime? + finishedAt DateTime? + baseCharacter BaseCharacter? @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + bookAnalysisCharacter BookAnalysisCharacter? @relation(fields: [bookAnalysisCharacterId], references: [id], onDelete: Cascade) + assets ImageAsset[] + bookAnalysisAppearanceImages BookAnalysisCharacterAppearanceImage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sceneType, status]) + @@index([baseCharacterId, createdAt]) + @@index([novelId, createdAt]) + @@index([bookAnalysisCharacterId, createdAt]) +} + +model StyleExtractionTask { + id String @id @default(cuid()) + name String + category String? + sourceText String + sourceType String @default("from_text") + sourceRefId String? + sourceProcessingMode String @default("full_text") + sourceInputText String? + sourceInputCharLimit Int? + sourceInputCharCount Int? + provider String + model String? + temperature Float? + presetKey String @default("balanced") + status PipelineJobStatus @default(queued) + progress Float @default(0) + retryCount Int @default(0) + maxRetries Int @default(1) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + error String? + summary String? + createdStyleProfileId String? + createdStyleProfileName String? + startedAt DateTime? + finishedAt DateTime? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + llmCallCount Int @default(0) + lastTokenRecordedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([createdStyleProfileId]) + @@index([sourceType, sourceRefId]) +} + +model ImageAsset { + id String @id @default(cuid()) + taskId String + sceneType ImageSceneType @default(character) + baseCharacterId String? + novelId String? + bookAnalysisCharacterId String? + provider String + model String + url String + mimeType String? + width Int? + height Int? + seed Int? + prompt String? + isPrimary Boolean @default(false) + sortOrder Int @default(0) + metadata String? + task ImageGenerationTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + baseCharacter BaseCharacter? @relation(fields: [baseCharacterId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + bookAnalysisCharacter BookAnalysisCharacter? @relation(fields: [bookAnalysisCharacterId], references: [id], onDelete: Cascade) + bookAnalysisAppearanceImages BookAnalysisCharacterAppearanceImage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([taskId]) + @@index([sceneType, createdAt]) + @@index([baseCharacterId, isPrimary, createdAt]) + @@index([novelId, isPrimary, createdAt]) + @@index([bookAnalysisCharacterId, isPrimary, createdAt]) +} + +// 项目级视觉资源索引。它只保存可展示、筛选与选择所需的投影, +// 原始业务事实与文件继续由 ImageAsset / Comic / Drama 等来源拥有。 +model VisualAssetProjection { + id String @id @default(cuid()) + sourceDomain String + sourceType String + sourceId String + sourceVersion String @default("current") + sourceLabel String + scopeKind String @default("global") + scopeId String? + scopeLabel String? + kind String + origin String @default("generated") + url String + thumbnailUrl String? + mimeType String? + width Int? + height Int? + prompt String? + provider String? + model String? + isPrimary Boolean @default(false) + metadata String? + sourceCreatedAt DateTime + lastSeenAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([sourceDomain, sourceType, sourceId, sourceVersion]) + @@index([scopeKind, scopeId, sourceCreatedAt]) + @@index([kind, sourceCreatedAt]) + @@index([sourceDomain, sourceCreatedAt]) +} + +model NovelGenre { + id String @id @default(cuid()) + name String + description String? + template String? + parentId String? + parent NovelGenre? @relation("GenreChildren", fields: [parentId], references: [id]) + children NovelGenre[] @relation("GenreChildren") + novels Novel[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([parentId]) +} + +model NovelStoryMode { + id String @id @default(cuid()) + name String + description String? + template String? + profileJson String + parentId String? + parent NovelStoryMode? @relation("StoryModeChildren", fields: [parentId], references: [id], onDelete: SetNull) + children NovelStoryMode[] @relation("StoryModeChildren") + primaryNovels Novel[] @relation("NovelPrimaryStoryMode") + secondaryNovels Novel[] @relation("NovelSecondaryStoryMode") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([parentId]) +} + +model World { + id String @id @default(cuid()) + name String + description String? + worldType String? + templateKey String? + axioms String? + background String? + geography String? + cultures String? + magicSystem String? + politics String? + races String? + religions String? + technology String? + conflicts String? + history String? + economy String? + factions String? + status String @default("draft") + version Int @default(1) + selectedDimensions String? + selectedElements String? + layerStates String? + consistencyReport String? + overviewSummary String? + structureJson String? + bindingSupportJson String? + structureSchemaVersion Int @default(1) + novels Novel[] + snapshots WorldSnapshot[] + deepeningQA WorldDeepeningQA[] + consistencyIssues WorldConsistencyIssue[] + libraryItems WorldPropertyLibrary[] + novelWorlds NovelWorld[] + assets WorldAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model NovelWorld { + id String @id @default(cuid()) + novelId String @unique + sourceWorldId String? + sourceType String @default("manual") + title String? + coverSummary String? + structuredDataJson String? + bindingContractJson String? + storySliceJson String? + storySliceOverridesJson String? + storySliceSchemaVersion Int @default(1) + storySliceBuiltAt DateTime? + storySliceDigest String? + syncEnabled Boolean @default(false) + syncDirection String @default("none") + syncBaseVersion Int? + syncPendingChangesJson String? + lastSyncedAt DateTime? + generationPolicyJson String? + generatedFromThemeJson String? + savedToLibraryAt DateTime? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceWorld World? @relation(fields: [sourceWorldId], references: [id], onDelete: SetNull) + syncRecords WorldSyncRecord[] + assets WorldAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sourceWorldId]) + @@index([sourceType]) +} + +model WorldSyncRecord { + id String @id @default(cuid()) + novelWorldId String + sourceWorldId String? + direction String + syncedFieldsJson String? + diffSummary String? + triggeredBy String @default("user") + novelWorld NovelWorld @relation(fields: [novelWorldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelWorldId, createdAt]) + @@index([sourceWorldId]) +} + +model WorldAsset { + id String @id @default(cuid()) + worldId String? + novelWorldId String? + assetType String + title String + description String? + generationPrompt String? + renderDataJson String? + thumbnailUrl String? + version Int @default(1) + status String @default("placeholder") + world World? @relation(fields: [worldId], references: [id], onDelete: Cascade) + novelWorld NovelWorld? @relation(fields: [novelWorldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([worldId, assetType]) + @@index([novelWorldId, assetType]) + @@index([assetType]) +} + +model WorldPropertyLibrary { + id String @id @default(cuid()) + name String + description String? + category String + worldType String? + usageCount Int @default(0) + sourceWorldId String? + sourceWorld World? @relation(fields: [sourceWorldId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([sourceWorldId]) +} + +model WorldSnapshot { + id String @id @default(cuid()) + worldId String + label String? + data String + world World @relation(fields: [worldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([worldId, createdAt]) +} + +model WorldDeepeningQA { + id String @id @default(cuid()) + worldId String + priority String @default("recommended") + question String + targetLayer String? + targetField String? + answer String? + integratedSummary String? + status String @default("pending") + world World @relation(fields: [worldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([worldId, status]) +} + +model WorldConsistencyIssue { + id String @id @default(cuid()) + worldId String + severity String + code String + message String + detail String? + source String @default("rule") + status String @default("open") + targetField String? + world World @relation(fields: [worldId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([worldId, status]) + @@index([worldId, severity]) +} + +model WritingFormula { + id String @id @default(cuid()) + name String + sourceText String? + content String? + genre String? + style String? + toneVoice String? + structure String? + pacing String? + paragraphPattern String? + sentenceStructure String? + vocabularyLevel String? + rhetoricalDevices String? + narrativeMode String? + perspectivePoint String? + characterVoice String? + themes String? + motifs String? + emotionalTone String? + uniqueFeatures String? + formulaDescription String? + formulaSteps String? + applicationTips String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model StyleProfile { + id String @id @default(cuid()) + name String + description String? + category String? + tagsJson String? + applicableGenresJson String? + sourceType String + sourceRefId String? + sourceContent String? + extractedFeaturesJson String? + extractionPresetsJson String? + extractionAntiAiRuleKeysJson String? + selectedExtractionPresetKey String? + analysisMarkdown String? + narrativeRulesJson String? + characterRulesJson String? + languageRulesJson String? + rhythmRulesJson String? + status String @default("active") + antiAiBindings StyleProfileAntiAiRule[] + bindings StyleBinding[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([sourceType, sourceRefId]) +} + +model StyleTemplate { + id String @id @default(cuid()) + key String @unique + name String + description String + category String + tagsJson String? + applicableGenresJson String? + analysisMarkdown String? + narrativeRulesJson String? + characterRulesJson String? + languageRulesJson String? + rhythmRulesJson String? + defaultAntiAiRuleKeysJson String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model AntiAiRule { + id String @id @default(cuid()) + key String @unique + name String + type AntiAiRuleType + severity AntiAiSeverity + description String + detectPatternsJson String? + rewriteSuggestion String? + promptInstruction String? + autoRewrite Boolean @default(false) + enabled Boolean @default(true) + globalBaselineEnabled Boolean @default(false) + profileBindings StyleProfileAntiAiRule[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([type, enabled]) + @@index([globalBaselineEnabled, enabled]) +} + +model StyleProfileAntiAiRule { + id String @id @default(cuid()) + styleProfileId String + antiAiRuleId String + enabled Boolean @default(true) + weight Float @default(1) + styleProfile StyleProfile @relation(fields: [styleProfileId], references: [id], onDelete: Cascade) + antiAiRule AntiAiRule @relation(fields: [antiAiRuleId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([styleProfileId, antiAiRuleId]) + @@index([antiAiRuleId]) +} + +model StyleBinding { + id String @id @default(cuid()) + styleProfileId String + targetType StyleBindingTargetType + targetId String + priority Int @default(1) + weight Float @default(1) + enabled Boolean @default(true) + styleProfile StyleProfile @relation(fields: [styleProfileId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([targetType, targetId, enabled]) + @@index([styleProfileId]) +} + +model TitleLibrary { + id String @id @default(cuid()) + title String + description String? + clickRate Float? + keywords String? + genreId String? + usedCount Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model APIKey { + id String @id @default(cuid()) + provider String + displayName String? + key String? + model String? + baseURL String? + isActive Boolean @default(true) + reasoningEnabled Boolean @default(true) + concurrencyLimit Int @default(0) + requestIntervalMs Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([provider]) +} + +model AppSetting { + key String @id + value String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ModelRouteConfig { + id String @id @default(cuid()) + taskType String @unique + provider String + model String + temperature Float @default(0.7) + maxTokens Int? + requestProtocol String @default("auto") + structuredResponseFormat String @default("auto") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([provider]) +} + +model NovelBible { + id String @id @default(cuid()) + novelId String @unique + coreSetting String? + forbiddenRules String? + mainPromise String? + characterArcs String? + worldRules String? + rawContent String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model PlotBeat { + id String @id @default(cuid()) + novelId String + chapterOrder Int? + beatType String + title String + content String + status BeatStatus @default(planned) + metadata String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([novelId, chapterOrder]) +} + +model ChapterSummary { + id String @id @default(cuid()) + novelId String + chapterId String @unique + summary String + keyEvents String? + characterStates String? + hook String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) +} + +model ConsistencyFact { + id String @id @default(cuid()) + novelId String + chapterId String? + category FactCategory @default(plot) + content String + source String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([chapterId]) + @@index([novelId, category]) +} + +model GenerationJob { + id String @id @default(cuid()) + novelId String + startOrder Int + endOrder Int + runMode PipelineRunMode? @default(fast) + autoReview Boolean @default(true) + autoRepair Boolean @default(true) + skipCompleted Boolean @default(true) + qualityThreshold Int? + repairMode PipelineRepairMode? @default(light_repair) + status PipelineJobStatus @default(queued) + progress Float @default(0) + completedCount Int @default(0) + totalCount Int @default(0) + retryCount Int @default(0) + maxRetries Int @default(2) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + error String? + lastErrorType String? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + llmCallCount Int @default(0) + lastTokenRecordedAt DateTime? + payload String? + startedAt DateTime? + finishedAt DateTime? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([novelId, status]) +} + +model AgentRun { + id String @id @default(cuid()) + novelId String? + chapterId String? + sessionId String + goal String + entryAgent String + status AgentRunStatus @default(queued) + currentStep String? + currentAgent String? + error String? + startedAt DateTime? + finishedAt DateTime? + metadataJson String? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + steps AgentStep[] + approvals AgentApproval[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([novelId, createdAt]) + @@index([novelId, chapterId, createdAt]) + @@index([sessionId, createdAt]) +} + +model AgentStep { + id String @id @default(cuid()) + runId String + seq Int + agentName String + stepType AgentStepType + status AgentStepStatus @default(succeeded) + parentStepId String? + idempotencyKey String? + inputJson String? + outputJson String? + error String? + errorCode String? + provider String? + model String? + tokenUsageJson String? + costUsd Float? + durationMs Int? + run AgentRun @relation(fields: [runId], references: [id], onDelete: Cascade) + approvals AgentApproval[] + createdAt DateTime @default(now()) + + @@unique([runId, seq]) + @@index([runId, idempotencyKey]) + @@index([runId, parentStepId]) +} + +model AgentApproval { + id String @id @default(cuid()) + runId String + stepId String? + approvalType String + targetType String + targetId String + diffSummary String + status AgentApprovalStatus @default(pending) + expiresAt DateTime? + decisionNote String? + decider String? + decidedAt DateTime? + payloadJson String? + run AgentRun @relation(fields: [runId], references: [id], onDelete: Cascade) + step AgentStep? @relation(fields: [stepId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([runId, status]) + @@index([stepId]) + @@index([status, expiresAt]) +} + +model CreativeHubThread { + id String @id @default(cuid()) + title String @default("新对话") + archived Boolean @default(false) + status CreativeHubThreadStatus @default(idle) + latestRunId String? + latestError String? + resourceBindingsJson String? + metadataJson String? + checkpoints CreativeHubCheckpoint[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([archived, updatedAt]) + @@index([status, updatedAt]) +} + +model CreativeHubCheckpoint { + id String @id @default(cuid()) + threadId String + checkpointId String + parentCheckpointId String? + runId String? + messageCount Int @default(0) + preview String? + messagesJson String + interruptsJson String? + metadataJson String? + thread CreativeHubThread @relation(fields: [threadId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([threadId, checkpointId]) + @@index([threadId, createdAt]) +} + +model StorylineVersion { + id String @id @default(cuid()) + novelId String + version Int + status StorylineVersionStatus @default(draft) + content String + diffSummary String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, version]) + @@index([novelId, status, createdAt]) +} + +model VolumePlanVersion { + id String @id @default(cuid()) + novelId String + version Int + status VolumePlanVersionStatus @default(draft) + contentJson String + diffSummary String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + volumes VolumePlan[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, version]) + @@index([novelId, status, createdAt]) + @@index([novelId, status, version]) +} + +model VolumePlan { + id String @id @default(cuid()) + novelId String + sortOrder Int + title String + summary String? + mainPromise String? + escalationMode String? + protagonistChange String? + climax String? + nextVolumeHook String? + resetPoint String? + openPayoffsJson String? + status String @default("active") + sourceVersionId String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceVersion VolumePlanVersion? @relation(fields: [sourceVersionId], references: [id], onDelete: SetNull) + chapters VolumeChapterPlan[] + characterAssignments CharacterVolumeAssignment[] + factionTracks CharacterFactionTrack[] + relationStages CharacterRelationStage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, sortOrder]) + @@index([novelId, status, sortOrder]) + @@index([sourceVersionId]) +} + +model VolumeChapterPlan { + id String @id @default(cuid()) + volumeId String + chapterId String? + chapterOrder Int + title String + summary String + purpose String? + conflictLevel Int? + conflictLevelSource String? + revealLevel Int? + targetWordCount Int? + mustAvoid String? + taskSheet String? + sceneCards String? + payoffRefsJson String? + volume VolumePlan @relation(fields: [volumeId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([volumeId, chapterOrder]) + @@index([volumeId, chapterOrder]) + @@index([chapterId]) +} + +model QualityReport { + id String @id @default(cuid()) + novelId String + chapterId String? + coherence Int + repetition Int + pacing Int + voice Int + engagement Int + overall Int + issues String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId]) + @@index([chapterId]) + @@index([novelId, createdAt]) +} + +model StoryMacroPlan { + id String @id @default(cuid()) + novelId String @unique + storyInput String? + expansionJson String? + decompositionJson String? + issuesJson String? + lockedFieldsJson String? + constraintEngineJson String? + stateJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model BookContract { + id String @id @default(cuid()) + novelId String @unique + readingPromise String + protagonistFantasy String + coreSellingPoint String + chapter3Payoff String + chapter10Payoff String + chapter30Payoff String + escalationLadder String + relationshipMainline String + absoluteRedLinesJson String + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model NovelWorkflowTask { + id String @id @default(cuid()) + novelId String? + lane NovelWorkflowLane + title String + status NovelWorkflowTaskStatus @default(queued) + progress Float @default(0) + currentStage String? + currentItemKey String? + currentItemLabel String? + checkpointType String? + checkpointSummary String? + resumeTargetJson String? + seedPayloadJson String? + milestonesJson String? + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + startedAt DateTime? + finishedAt DateTime? + cancelRequestedAt DateTime? + attemptCount Int @default(0) + maxAttempts Int @default(3) + lastError String? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + llmCallCount Int @default(0) + lastTokenRecordedAt DateTime? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + autoApprovalRecords AutoDirectorAutoApprovalRecord[] + directorRun DirectorRun? + directorCommands DirectorRunCommand[] + directorSteps DirectorStepRun[] + directorEvents DirectorEvent[] + directorArtifacts DirectorArtifact[] + directorLlmUsageRecords DirectorLlmUsageRecord[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([status, updatedAt]) + @@index([lane, updatedAt]) +} + +model DirectorRunCommand { + id String @id @default(cuid()) + taskId String + novelId String? + commandType String + idempotencyKey String + status String @default("queued") + leaseOwner String? + leaseExpiresAt DateTime? + attempt Int @default(0) + runAfter DateTime @default(now()) + payloadJson String? + errorMessage String? + startedAt DateTime? + finishedAt DateTime? + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([taskId, commandType, idempotencyKey]) + @@index([status, runAfter, updatedAt]) + @@index([taskId, status, updatedAt]) + @@index([novelId, updatedAt]) + @@index([leaseOwner, leaseExpiresAt]) +} + +model DirectorRuntimeInstance { + id String @id @default(cuid()) + novelId String? + workflowTaskId String? + runId String? + runMode String? + status String @default("waiting_worker") + currentStep String? + currentChapterId String? + checkpointVersion Int @default(0) + cancelRequestedAt DateTime? + lastHeartbeatAt DateTime? + lastErrorClass String? + lastErrorMessage String? + workerMessage String? + metadataJson String? + commands DirectorRuntimeCommand[] + executions DirectorRuntimeExecution[] + checkpoints DirectorRuntimeCheckpoint[] + events DirectorRuntimeEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status, updatedAt]) + @@index([workflowTaskId]) + @@index([runId]) + @@index([status, updatedAt]) +} + +model DirectorRuntimeCommand { + id String @id @default(cuid()) + runtimeId String + workflowTaskId String? + novelId String? + legacyCommandId String? @unique + commandType String + idempotencyKey String + status String @default("queued") + priority Int @default(50) + runAfter DateTime @default(now()) + leaseOwner String? + leaseExpiresAt DateTime? + attempt Int @default(0) + payloadJson String? + errorMessage String? + startedAt DateTime? + finishedAt DateTime? + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + executions DirectorRuntimeExecution[] + checkpoints DirectorRuntimeCheckpoint[] + events DirectorRuntimeEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([runtimeId, commandType, idempotencyKey]) + @@index([runtimeId, status, updatedAt]) + @@index([status, priority, runAfter, createdAt]) + @@index([workflowTaskId, status, updatedAt]) + @@index([novelId, status, updatedAt]) + @@index([leaseOwner, leaseExpiresAt]) +} + +model DirectorRuntimeExecution { + id String @id @default(cuid()) + runtimeId String + commandId String? + workflowTaskId String? + novelId String? + legacyCommandId String? + activeLockKey String? @unique + workerId String? + slotId String? + status String @default("leased") + stepType String + resourceClass String? + leaseExpiresAt DateTime? + heartbeatAt DateTime? + startedAt DateTime? + finishedAt DateTime? + errorClass String? + errorMessage String? + inputHash String? + checkpointVersion Int @default(0) + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + command DirectorRuntimeCommand? @relation(fields: [commandId], references: [id], onDelete: SetNull) + checkpoints DirectorRuntimeCheckpoint[] + events DirectorRuntimeEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([runtimeId, status, updatedAt]) + @@index([status, leaseExpiresAt]) + @@index([workflowTaskId, status, updatedAt]) + @@index([novelId, status, updatedAt]) + @@index([workerId, status, updatedAt]) +} + +model DirectorRuntimeCheckpoint { + id String @id @default(cuid()) + runtimeId String + commandId String? + executionId String? + version Int + stepType String + targetType String? + targetId String? + inputHash String? + outputRefJson String? + stateJson String? + summary String? + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + command DirectorRuntimeCommand? @relation(fields: [commandId], references: [id], onDelete: SetNull) + execution DirectorRuntimeExecution? @relation(fields: [executionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@unique([runtimeId, version]) + @@index([runtimeId, createdAt]) + @@index([commandId]) + @@index([executionId]) +} + +model DirectorRuntimeEvent { + id String @id + runtimeId String + commandId String? + executionId String? + workflowTaskId String? + novelId String? + type String + summary String + severity String? + metadataJson String? + occurredAt DateTime + runtime DirectorRuntimeInstance @relation(fields: [runtimeId], references: [id], onDelete: Cascade) + command DirectorRuntimeCommand? @relation(fields: [commandId], references: [id], onDelete: SetNull) + execution DirectorRuntimeExecution? @relation(fields: [executionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([runtimeId, occurredAt]) + @@index([commandId, occurredAt]) + @@index([executionId, occurredAt]) + @@index([workflowTaskId, occurredAt]) + @@index([novelId, occurredAt]) + @@index([type, occurredAt]) +} + +model DirectorRun { + id String @id + taskId String @unique + novelId String? + entrypoint String? + policyJson String + lastWorkspaceAnalysisJson String? + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + steps DirectorStepRun[] + events DirectorEvent[] + artifacts DirectorArtifact[] + llmUsageRecords DirectorLlmUsageRecord[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, updatedAt]) +} + +model DirectorStepRun { + id String @id @default(cuid()) + runId String + taskId String + novelId String? + idempotencyKey String @unique + nodeKey String + label String + status String + targetType String? + targetId String? + startedAt DateTime + finishedAt DateTime? + error String? + producedArtifactsJson String? + policyDecisionJson String? + run DirectorRun @relation(fields: [runId], references: [id], onDelete: Cascade) + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + llmUsageRecords DirectorLlmUsageRecord[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([runId, status, updatedAt]) + @@index([taskId, nodeKey]) + @@index([novelId, updatedAt]) +} + +model DirectorLlmUsageRecord { + id String @id @default(cuid()) + novelId String? + taskId String? + runId String? + stepIdempotencyKey String? + nodeKey String? + promptAssetKey String? + promptVersion String? + modelRoute String? + provider String? + model String? + status String @default("recorded") + attributionStatus String @default("unattributed") + durationMs Int? + promptTokens Int @default(0) + completionTokens Int @default(0) + totalTokens Int @default(0) + metadataJson String? + recordedAt DateTime @default(now()) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + task NovelWorkflowTask? @relation(fields: [taskId], references: [id], onDelete: Cascade) + run DirectorRun? @relation(fields: [runId], references: [id], onDelete: SetNull) + step DirectorStepRun? @relation(fields: [stepIdempotencyKey], references: [idempotencyKey], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([novelId, recordedAt]) + @@index([taskId, recordedAt]) + @@index([runId, recordedAt]) + @@index([stepIdempotencyKey, recordedAt]) + @@index([nodeKey, recordedAt]) + @@index([attributionStatus, recordedAt]) +} + +model DirectorEvent { + id String @id + runId String? + taskId String? + novelId String? + type String + nodeKey String? + artifactId String? + artifactType String? + summary String + affectedScope String? + severity String? + metadataJson String? + occurredAt DateTime + run DirectorRun? @relation(fields: [runId], references: [id], onDelete: Cascade) + task NovelWorkflowTask? @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel? @relation(fields: [novelId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([runId, occurredAt]) + @@index([taskId, occurredAt]) + @@index([novelId, occurredAt]) + @@index([type, occurredAt]) +} + +model DirectorArtifact { + id String @id + runId String? + novelId String + taskId String? + artifactType String + targetType String + targetId String? + version Int @default(1) + status String + source String + contentTable String + contentId String + contentHash String? + schemaVersion String + promptAssetKey String? + promptVersion String? + modelRoute String? + sourceStepRunId String? + protectedUserContent Boolean? + artifactUpdatedAt DateTime? + run DirectorRun? @relation(fields: [runId], references: [id], onDelete: SetNull) + task NovelWorkflowTask? @relation(fields: [taskId], references: [id], onDelete: SetNull) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + dependencies DirectorArtifactDependency[] @relation("DirectorArtifactDependencies") + dependents DirectorArtifactDependency[] @relation("DirectorArtifactDependents") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, artifactType, status]) + @@index([runId, updatedAt]) + @@index([taskId, updatedAt]) + @@index([targetType, targetId]) + @@index([sourceStepRunId]) +} + +model DirectorArtifactDependency { + id String @id @default(cuid()) + artifactId String + dependsOnArtifactId String + dependsOnVersion Int? + artifact DirectorArtifact @relation("DirectorArtifactDependencies", fields: [artifactId], references: [id], onDelete: Cascade) + dependsOn DirectorArtifact @relation("DirectorArtifactDependents", fields: [dependsOnArtifactId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([artifactId, dependsOnArtifactId]) + @@index([dependsOnArtifactId]) +} + +model AutoDirectorAutoApprovalRecord { + id String @id @default(cuid()) + taskId String + novelId String + approvalPointCode String + approvalPointLabel String + checkpointType String + checkpointSummary String? + summary String + stage String? + scopeLabel String? + eventId String @unique + task NovelWorkflowTask @relation(fields: [taskId], references: [id], onDelete: Cascade) + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) + @@index([taskId, createdAt]) +} + + +model AutoDirectorFollowUpActionLog { + id String @id @default(cuid()) + taskId String + actionCode String + sourceChannel String + sourceUser String? + idempotencyKey String @unique + resultCode String + failureReason String? + metadataJson String? + executedAt DateTime @default(now()) + createdAt DateTime @default(now()) + + @@index([taskId, executedAt]) + @@index([taskId, actionCode, executedAt]) +} + +model AutoDirectorFollowUpNotificationLog { + id String @id @default(cuid()) + eventId String + eventType String + taskId String + channelType String + target String? + requestPayload String? + responseBody String? + responseStatus Int? + attemptCount Int @default(1) + deliveredAt DateTime? + status String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([taskId, createdAt]) + @@index([eventId, channelType, createdAt]) +} + +model StoryStateSnapshot { + id String @id @default(cuid()) + novelId String + sourceChapterId String? + summary String? + rawStateJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + sourceChapter Chapter? @relation(fields: [sourceChapterId], references: [id], onDelete: SetNull) + characterStates CharacterState[] + relationStates RelationState[] + informationStates InformationState[] + foreshadowStates ForeshadowState[] + openConflicts OpenConflict[] + payoffLedgerItems PayoffLedgerItem[] @relation("PayoffLedgerLastSnapshot") + storyPlans StoryPlan[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, sourceChapterId]) + @@index([novelId, createdAt]) + @@index([sourceChapterId]) +} + +model CharacterState { + id String @id @default(cuid()) + snapshotId String + characterId String + currentGoal String? + emotion String? + stressLevel Int? + secretExposure String? + knownFactsJson String? + misbeliefsJson String? + summary String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + character Character @relation(fields: [characterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([snapshotId, characterId]) + @@index([characterId, createdAt]) +} + +model RelationState { + id String @id @default(cuid()) + snapshotId String + sourceCharacterId String + targetCharacterId String + trustScore Int? + intimacyScore Int? + conflictScore Int? + dependencyScore Int? + summary String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + sourceCharacter Character @relation("RelationStateSource", fields: [sourceCharacterId], references: [id], onDelete: Cascade) + targetCharacter Character @relation("RelationStateTarget", fields: [targetCharacterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([snapshotId, sourceCharacterId, targetCharacterId]) + @@index([sourceCharacterId, targetCharacterId]) +} + +model InformationState { + id String @id @default(cuid()) + snapshotId String + holderType String + holderRefId String? + fact String + status String + summary String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([snapshotId, holderType]) +} + +model ForeshadowState { + id String @id @default(cuid()) + snapshotId String + title String + summary String? + status String + setupChapterId String? + payoffChapterId String? + snapshot StoryStateSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + setupChapter Chapter? @relation("ForeshadowSetupChapter", fields: [setupChapterId], references: [id], onDelete: SetNull) + payoffChapter Chapter? @relation("ForeshadowPayoffChapter", fields: [payoffChapterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([snapshotId, status]) + @@index([setupChapterId]) + @@index([payoffChapterId]) +} + +model OpenConflict { + id String @id @default(cuid()) + novelId String + chapterId String? + sourceSnapshotId String? + sourceIssueId String? + sourceType String + conflictType String + conflictKey String + title String + summary String + severity String @default("medium") + status String @default("open") + evidenceJson String? + affectedCharacterIdsJson String? + resolutionHint String? + lastSeenChapterOrder Int? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + sourceSnapshot StoryStateSnapshot? @relation(fields: [sourceSnapshotId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, chapterId, sourceType, conflictKey]) + @@index([novelId, status, updatedAt]) + @@index([chapterId, status]) + @@index([sourceSnapshotId]) + @@index([sourceIssueId]) +} + +model PayoffLedgerItem { + id String @id @default(cuid()) + novelId String + ledgerKey String + title String + summary String + scopeType PayoffLedgerScopeType + currentStatus PayoffLedgerStatus + targetStartChapterOrder Int? + targetEndChapterOrder Int? + firstSeenChapterOrder Int? + lastTouchedChapterOrder Int? + lastTouchedChapterId String? + setupChapterId String? + payoffChapterId String? + lastSnapshotId String? + sourceRefsJson String? + evidenceJson String? + riskSignalsJson String? + statusReason String? + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + lastTouchedChapter Chapter? @relation("PayoffLedgerLastTouchedChapter", fields: [lastTouchedChapterId], references: [id], onDelete: SetNull) + setupChapter Chapter? @relation("PayoffLedgerSetupChapter", fields: [setupChapterId], references: [id], onDelete: SetNull) + payoffChapter Chapter? @relation("PayoffLedgerPayoffChapter", fields: [payoffChapterId], references: [id], onDelete: SetNull) + lastSnapshot StoryStateSnapshot? @relation("PayoffLedgerLastSnapshot", fields: [lastSnapshotId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, ledgerKey]) + @@index([novelId, currentStatus, updatedAt]) + @@index([novelId, targetEndChapterOrder]) + @@index([lastTouchedChapterId]) + @@index([setupChapterId]) + @@index([payoffChapterId]) + @@index([lastSnapshotId]) +} + +model CharacterResourceLedgerItem { + id String @id @default(cuid()) + novelId String + resourceKey String + name String + summary String + resourceType String + narrativeFunction String + ownerType String + ownerId String? + ownerName String? + ownerCharacterId String? + holderCharacterId String? + holderCharacterName String? + status String + readerKnows Boolean @default(false) + holderKnows Boolean @default(true) + knownByCharacterIdsJson String? + introducedChapterId String? + introducedChapterOrder Int? + lastTouchedChapterId String? + lastTouchedChapterOrder Int? + expectedUseStartChapterOrder Int? + expectedUseEndChapterOrder Int? + constraintsJson String? + riskSignalsJson String? + sourceRefsJson String? + evidenceJson String? + confidence Float? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + ownerCharacter Character? @relation("CharacterResourceOwner", fields: [ownerCharacterId], references: [id], onDelete: SetNull) + holderCharacter Character? @relation("CharacterResourceHolder", fields: [holderCharacterId], references: [id], onDelete: SetNull) + introducedChapter Chapter? @relation("CharacterResourceIntroducedChapter", fields: [introducedChapterId], references: [id], onDelete: SetNull) + lastTouchedChapter Chapter? @relation("CharacterResourceLastTouchedChapter", fields: [lastTouchedChapterId], references: [id], onDelete: SetNull) + events CharacterResourceEvent[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, resourceKey]) + @@index([novelId, status, updatedAt]) + @@index([holderCharacterId, status]) + @@index([ownerCharacterId]) + @@index([novelId, lastTouchedChapterOrder]) + @@index([introducedChapterId]) + @@index([lastTouchedChapterId]) +} + +model CharacterResourceEvent { + id String @id @default(cuid()) + novelId String + resourceId String + chapterId String? + chapterOrder Int? + eventType String + actorCharacterId String? + fromHolderCharacterId String? + toHolderCharacterId String? + summary String + evidenceJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + resource CharacterResourceLedgerItem @relation(fields: [resourceId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + actorCharacter Character? @relation("CharacterResourceEventActor", fields: [actorCharacterId], references: [id], onDelete: SetNull) + fromHolderCharacter Character? @relation("CharacterResourceEventFromHolder", fields: [fromHolderCharacterId], references: [id], onDelete: SetNull) + toHolderCharacter Character? @relation("CharacterResourceEventToHolder", fields: [toHolderCharacterId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) + @@index([resourceId, createdAt]) + @@index([chapterId]) + @@index([actorCharacterId]) + @@index([fromHolderCharacterId]) + @@index([toHolderCharacterId]) +} + +model CanonicalStateVersion { + id String @id @default(cuid()) + novelId String + chapterId String? + sourceType String + sourceStage String? + version Int + summary String + snapshotJson String + acceptedProposalIdsJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + committedProposals StateChangeProposal[] @relation("CommittedStateChangeVersion") + createdAt DateTime @default(now()) + + @@unique([novelId, version]) + @@index([novelId, createdAt]) + @@index([chapterId]) +} + +model StateChangeProposal { + id String @id @default(cuid()) + novelId String + chapterId String? + sourceSnapshotId String? + sourceType String + sourceStage String? + proposalType String + riskLevel String + status String @default("validated") + summary String + payloadJson String + evidenceJson String? + validationNotesJson String? + committedVersionId String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + committedVersion CanonicalStateVersion? @relation("CommittedStateChangeVersion", fields: [committedVersionId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, createdAt]) + @@index([chapterId, createdAt]) + @@index([status, riskLevel, createdAt]) + @@index([committedVersionId]) +} + +model StoryPlan { + id String @id @default(cuid()) + novelId String + chapterId String? + parentId String? + sourceStateSnapshotId String? + level StoryPlanLevel + planRole StoryPlanRole? + phaseLabel String? + title String + objective String + participantsJson String? + revealsJson String? + riskNotesJson String? + mustAdvanceJson String? + mustPreserveJson String? + sourceIssueIdsJson String? + replannedFromPlanId String? + hookTarget String? + status String @default("draft") + externalRef String? + rawPlanJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + parent StoryPlan? @relation("StoryPlanHierarchy", fields: [parentId], references: [id], onDelete: SetNull) + children StoryPlan[] @relation("StoryPlanHierarchy") + sourceStateSnapshot StoryStateSnapshot? @relation(fields: [sourceStateSnapshotId], references: [id], onDelete: SetNull) + scenes ChapterPlanScene[] + replanRuns ReplanRun[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, level, createdAt]) + @@index([novelId, level, externalRef]) + @@index([novelId, level, chapterId]) + @@index([chapterId, createdAt]) + @@index([externalRef]) + @@index([sourceStateSnapshotId]) +} + +model ChapterPlanScene { + id String @id @default(cuid()) + planId String + sortOrder Int + title String + objective String? + conflict String? + reveal String? + emotionBeat String? + plan StoryPlan @relation(fields: [planId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([planId, sortOrder]) +} + +model ReplanRun { + id String @id @default(cuid()) + novelId String + chapterId String? + sourcePlanId String? + triggerType String + reason String + outputSummary String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter? @relation(fields: [chapterId], references: [id], onDelete: SetNull) + sourcePlan StoryPlan? @relation(fields: [sourcePlanId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + + @@index([novelId, createdAt]) + @@index([chapterId, createdAt]) +} + +model AuditReport { + id String @id @default(cuid()) + novelId String + chapterId String + auditType AuditType + overallScore Int? + summary String? + legacyScoreJson String? + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade) + issues AuditIssue[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, chapterId, auditType, createdAt]) + @@index([chapterId, createdAt]) +} + +model AuditIssue { + id String @id @default(cuid()) + reportId String + auditType AuditType + severity String + code String + description String + evidence String + fixSuggestion String + status AuditIssueStatus @default(open) + report AuditReport @relation(fields: [reportId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([reportId, status]) + @@index([auditType, severity]) +} + +model KnowledgeDocument { + id String @id @default(cuid()) + title String + fileName String + kind KnowledgeDocumentKind @default(user_upload) + sourceAnalysisId String? @unique + status KnowledgeDocumentStatus @default(enabled) + activeVersionId String? + activeVersionNumber Int @default(0) + latestIndexStatus KnowledgeIndexStatus @default(idle) + lastIndexedAt DateTime? + activeVersion KnowledgeDocumentVersion? @relation("KnowledgeDocumentActiveVersion", fields: [activeVersionId], references: [id], onDelete: SetNull) + versions KnowledgeDocumentVersion[] @relation("KnowledgeDocumentVersions") + bindings KnowledgeBinding[] + bookAnalyses BookAnalysis[] + publishedFromAnalyses BookAnalysis[] @relation("BookAnalysisPublishedDocument") + continuationNovels Novel[] @relation("NovelKnowledgeContinuation") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, updatedAt]) + @@index([kind, updatedAt]) + @@index([title]) +} + +model KnowledgeDocumentVersion { + id String @id @default(cuid()) + documentId String + versionNumber Int + content String + contentHash String + charCount Int + document KnowledgeDocument @relation("KnowledgeDocumentVersions", fields: [documentId], references: [id], onDelete: Cascade) + activeForDocuments KnowledgeDocument[] @relation("KnowledgeDocumentActiveVersion") + chapters DocumentChapter[] + bookAnalyses BookAnalysis[] + sourceCaches BookAnalysisSourceCache[] + createdAt DateTime @default(now()) + + @@unique([documentId, versionNumber]) + @@index([documentId, createdAt]) + @@index([contentHash]) +} + +model DocumentChapter { + id String @id @default(cuid()) + documentVersionId String + chapterIndex Int + title String + startOffset Int + endOffset Int + charCount Int + summary String? + splitter String @default("rule") + documentVersion KnowledgeDocumentVersion @relation(fields: [documentVersionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([documentVersionId, chapterIndex]) + @@index([documentVersionId, startOffset]) +} + +model KnowledgeBinding { + id String @id @default(cuid()) + targetType KnowledgeBindingTargetType + targetId String + documentId String + sourceAnalysisId String? + document KnowledgeDocument @relation(fields: [documentId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@unique([targetType, targetId, documentId]) + @@index([targetType, targetId]) + @@index([documentId]) + @@index([sourceAnalysisId]) +} + +model BookAnalysis { + id String @id @default(cuid()) + documentId String + documentVersionId String + title String + status BookAnalysisStatus @default(queued) + summary String? + provider String? + model String? + temperature Float? + maxTokens Int? + budgetTokens Int? + usedTokens Int? @default(0) + userFocusInstruction String? + sourceStartChapterIndex Int? + sourceEndChapterIndex Int? + sourceStartOffset Int? + sourceEndOffset Int? + sourceScopeLabel String? + progress Float @default(0) + pendingManualRecovery Boolean @default(false) + heartbeatAt DateTime? + currentStage String? + currentItemKey String? + currentItemLabel String? + cancelRequestedAt DateTime? + attemptCount Int @default(0) + maxAttempts Int @default(1) + lastError String? + lastRunAt DateTime? + publishedDocumentId String? + document KnowledgeDocument @relation(fields: [documentId], references: [id], onDelete: Cascade) + documentVersion KnowledgeDocumentVersion @relation(fields: [documentVersionId], references: [id], onDelete: NoAction) + publishedDocument KnowledgeDocument? @relation("BookAnalysisPublishedDocument", fields: [publishedDocumentId], references: [id], onDelete: SetNull) + continuationNovels Novel[] @relation("NovelContinuationBookAnalysis") + sections BookAnalysisSection[] + characters BookAnalysisCharacter[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([documentId, status]) + @@index([documentVersionId]) + @@index([status, updatedAt]) +} + +model BookAnalysisSourceCache { + id String @id @default(cuid()) + documentVersionId String + provider String + model String + temperature Float + notesMaxTokens Int + sourceScopeKey String @default("full") + segmentVersion Int @default(1) + segmentCount Int + notesJson String + documentVersion KnowledgeDocumentVersion @relation(fields: [documentVersionId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([documentVersionId, sourceScopeKey, provider, model, temperature, notesMaxTokens, segmentVersion]) + @@index([documentVersionId, updatedAt]) +} + +model BookAnalysisSection { + id String @id @default(cuid()) + analysisId String + sectionKey String + title String + status BookAnalysisSectionStatus @default(idle) + aiContent String? + editedContent String? + notes String? + focusInstruction String? + structuredDataJson String? + normalizationWarningsJson String? + evidenceJson String? + frozen Boolean @default(false) + sortOrder Int + analysis BookAnalysis @relation(fields: [analysisId], references: [id], onDelete: Cascade) + updatedAt DateTime @updatedAt + + @@unique([analysisId, sectionKey]) + @@index([analysisId, sortOrder]) +} + +model BookAnalysisCharacter { + id String @id @default(cuid()) + analysisId String + name String + role String + status String @default("candidate") + briefDescription String? + importance String? + occurringChaptersJson String? + lastGenerationError String? + generationDepth String @default("standard") + selectedDimensionsJson String? + profileJson String? + depthMetadataJson String? + profileSectionsJson String? + evidenceJson String? + sortOrder Int @default(0) + analysis BookAnalysis @relation(fields: [analysisId], references: [id], onDelete: Cascade) + arcs BookAnalysisCharacterArc[] + scenes BookAnalysisCharacterScene[] + appearance BookAnalysisCharacterAppearance? + appearanceSnapshots BookAnalysisCharacterAppearanceSnapshot[] + appearanceTerms BookAnalysisCharacterAppearanceTerm[] + imageTasks ImageGenerationTask[] + imageAssets ImageAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([analysisId, sortOrder]) + @@index([analysisId, name]) + @@index([analysisId, status]) +} + +model BookAnalysisCharacterAppearance { + id String @id @default(cuid()) + characterId String @unique + coveragePercent Int @default(0) + consolidatedAppearanceJson String? + variantPolicyJson String? + lastIndexedChapterIndex Int? + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + snapshots BookAnalysisCharacterAppearanceSnapshot[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([coveragePercent]) +} + +model BookAnalysisCharacterAppearanceSnapshot { + id String @id @default(cuid()) + appearanceId String + characterId String + chapterIndex Int + chapterTitle String? + appearanceJson String? + evidenceJson String? + summaryCaption String? + contextSceneRefsJson String? + manuallyEdited Boolean @default(false) + appearance BookAnalysisCharacterAppearance @relation(fields: [appearanceId], references: [id], onDelete: Cascade) + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + images BookAnalysisCharacterAppearanceImage[] + terms BookAnalysisCharacterAppearanceTerm[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([characterId, chapterIndex]) + @@index([appearanceId, chapterIndex]) + @@index([characterId, chapterIndex]) + @@index([chapterIndex]) +} + +model BookAnalysisCharacterAppearanceTerm { + id String @id @default(cuid()) + characterId String + snapshotId String + chapterIndex Int + text String + category String? + confidence Float? + stability String? + evidenceJson String? + status String @default("pending") + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + snapshot BookAnalysisCharacterAppearanceSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([snapshotId, text]) + @@index([characterId, status, updatedAt]) + @@index([snapshotId]) + @@index([chapterIndex]) +} + +model BookAnalysisCharacterAppearanceImage { + id String @id @default(cuid()) + snapshotId String + generationTaskId String? + imageAssetId String? + imagePromptJson String? + referenceAssetIdsJson String? + snapshot BookAnalysisCharacterAppearanceSnapshot @relation(fields: [snapshotId], references: [id], onDelete: Cascade) + generationTask ImageGenerationTask? @relation(fields: [generationTaskId], references: [id], onDelete: SetNull) + imageAsset ImageAsset? @relation(fields: [imageAssetId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([imageAssetId]) + @@index([generationTaskId]) + @@index([snapshotId]) +} + +model BookAnalysisCharacterArc { + id String @id @default(cuid()) + characterId String + chapterIndex Int? + stageLabel String + stateSnapshotJson String? + evidenceJson String? + sortOrder Int @default(0) + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([characterId, sortOrder]) + @@index([chapterIndex]) +} + +model BookAnalysisCharacterScene { + id String @id @default(cuid()) + characterId String + sceneLabel String + sceneType String? + performanceJson String? + evidenceJson String? + sortOrder Int @default(0) + character BookAnalysisCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([characterId, sortOrder]) +} + +model KnowledgeChunk { + id String @id @default(cuid()) + tenantId String @default("default") + ownerType RagOwnerType + ownerId String + novelId String? + worldId String? + title String? + chunkText String + chunkHash String + chunkOrder Int + tokenEstimate Int @default(0) + language String @default("zh") + metadataJson String? + facetKeys String? + chapterAnchor String? + embedProvider String + embedModel String + embedVersion Int @default(1) + indexedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([tenantId, ownerType, ownerId]) + @@index([tenantId, novelId]) + @@index([tenantId, worldId]) + @@index([facetKeys]) + @@index([chapterAnchor]) + @@index([chunkHash]) +} + +model RagIndexJob { + id String @id @default(cuid()) + tenantId String @default("default") + jobType RagJobType + ownerType RagOwnerType + ownerId String + status RagJobStatus @default(queued) + attempts Int @default(0) + maxAttempts Int @default(5) + runAfter DateTime @default(now()) + payloadJson String? + lastError String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, runAfter]) + @@index([tenantId, ownerType, ownerId]) +} + +model RagRetrievalTrace { + id String @id @default(cuid()) + tenantId String @default("default") + novelId String? + worldId String? + queryDigest String + queryPreview String? + scopeJson String? + candidateCounts String @default("{}") + hitsJson String @default("[]") + timingsJson String @default("{}") + fallbackTriggered Boolean @default(false) + rerankerUsed Boolean @default(false) + createdAt DateTime @default(now()) + + @@index([tenantId, createdAt]) + @@index([tenantId, novelId, createdAt]) + @@index([queryDigest]) +} + +model NovelSideEffectJob { + id String @id @default(cuid()) + novelId String? + jobType String + status String @default("pending") + idempotencyKey String @unique + payloadVersion Int @default(1) + payloadJson String + attempts Int @default(0) + maxAttempts Int @default(5) + runAfter DateTime @default(now()) + leaseOwner String? + leaseExpiresAt DateTime? + lastError String? + finishedAt DateTime? + novel Novel? @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([status, runAfter]) + @@index([novelId, status, updatedAt]) + @@index([leaseOwner, leaseExpiresAt]) + @@index([jobType, status, runAfter]) +} + +model TaskCenterArchive { + id String @id @default(cuid()) + taskKind String + taskId String + archivedAt DateTime @default(now()) + + @@unique([taskKind, taskId]) + @@index([taskKind, archivedAt]) +} + +model StoryTimelineEvent { + id String @id @default(cuid()) + novelId String + chapterId String? + chapterIndex Int? + eventOrder Int + storyDayIndex Int? + storyTimeLabel String? + title String + summary String + type String + status String + visibility String + source String + participantIdsJson String @default("[]") + locationId String? + factionIdsJson String @default("[]") + prerequisiteIdsJson String @default("[]") + consequenceIdsJson String @default("[]") + stateChangesJson String @default("[]") + eventKey String? + confidence Float @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, chapterIndex]) + @@index([novelId, eventOrder]) + @@index([novelId, status]) + @@index([novelId, eventKey]) +} + +model ChapterTimeAnchor { + id String @id @default(cuid()) + novelId String + chapterId String + chapterIndex Int + storyDayIndex Int? + timeLabel String + startsAfterIdsJson String @default("[]") + plannedEventIdsJson String @default("[]") + endedWithIdsJson String @default("[]") + previousHookIdsJson String @default("[]") + nextHookIdsJson String @default("[]") + forbiddenEventIdsJson String @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([novelId, chapterId]) + @@index([novelId, chapterIndex]) +} + +model TimelineHook { + id String @id @default(cuid()) + novelId String + createdInChapterId String + createdInChapterIndex Int + expectedResolveByChapterIndex Int? + resolveMode String @default("long_arc") + blocking Boolean @default(false) + resolvedInChapterId String? + resolvedInChapterIndex Int? + title String + description String + status String + priority String + relatedEventIdsJson String @default("[]") + participantIdsJson String @default("[]") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, status]) + @@index([novelId, resolveMode, blocking]) + @@index([novelId, createdInChapterIndex]) + @@index([novelId, expectedResolveByChapterIndex]) +} + +model TimelineConstraint { + id String @id @default(cuid()) + novelId String + chapterId String? + chapterIndex Int? + type String + severity String + description String + relatedEventIdsJson String @default("[]") + relatedHookIdsJson String @default("[]") + relatedCharacterIdsJson String @default("[]") + active Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([novelId, chapterIndex]) + @@index([novelId, active]) +} + +model TimelineCheckReport { + id String @id @default(cuid()) + novelId String + chapterId String + chapterIndex Int + status String + score Float + issuesJson String @default("[]") + createdAt DateTime @default(now()) + + @@index([novelId, chapterIndex]) + @@index([novelId, status]) +} + +model NovelFactEntry { + id String @id @default(cuid()) + novelId String + chapterOrder Int + text String + category String @default("completed") + source String @default("auto") + novel Novel @relation(fields: [novelId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + + @@index([novelId, chapterOrder]) + @@index([novelId, category]) +} + +// ============================================================ +// 短剧创作模块(Drama)—— 独立 bounded context +// 所有 Drama* 表自成一套,不与 Novel 建外键关系(仅软引用 sourceRef)。 +// 与小说模块的唯一接触点是 services/drama/source/NovelSourceAdapter(只读)。 +// 分类字段统一用 String + 注释(不用 enum),降低双 schema 维护成本。 +// ============================================================ + +model DramaProject { + id String @id @default(cuid()) + title String + source String @default("original") + sourceRef String? + sourceInput String? + track String? + theme String? + orientation String @default("vertical_paid") + targetEpisodes Int @default(80) + strategy String? + status String @default("draft") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sourceBundle DramaSourceBundle? + characters DramaCharacter[] + episodes DramaEpisode[] + facts DramaFact[] + characterLibraryItems DramaCharacterLibrary[] + storyboards DramaStoryboard[] + videoPrompts DramaVideoPrompt[] + batchJobs DramaBatchJob[] + + @@index([source]) + @@index([status]) +} + +model DramaSourceBundle { + id String @id @default(cuid()) + projectId String @unique + synopsis String? + beats String? + worldNotes String? + hardFacts String? + rawText String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) +} + +model DramaCharacter { + id String @id @default(cuid()) + projectId String + name String + archetype String? + persona String? + speechStyle String? + visualAnchor String? + voiceProfile String? + relations String? + sourceCharacterRef String? + // 角色形象图 JSON: { status:"idle"|"generating"|"done"|"error", url?:string, prompt?:string, generatedAt?:string } + portraitData String? + // 三视图 JSON: [{ view:"front"|"side"|"back", url?:string, status:string, ... }, ...] + threeViewData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model DramaEpisode { + id String @id @default(cuid()) + projectId String + order Int + title String + content String? @default("") + hookOpening String? + cliffhanger String? + hookType String? + isPaywall Boolean @default(false) + emotionNet Int? + beatSheet String? + sourceMap String? + durationSec Int? + status String @default("planned") + qualityFlags String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + storyboards DramaStoryboard[] + videoPrompts DramaVideoPrompt[] + batchJobs DramaBatchJob[] + + @@unique([projectId, order]) + @@index([projectId, status]) +} + +model DramaFact { + id String @id @default(cuid()) + projectId String + episodeOrder Int + text String + category String @default("completed") + source String @default("auto") + createdAt DateTime @default(now()) + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId, episodeOrder]) + @@index([projectId, category]) +} + +model DramaCharacterLibrary { + id String @id @default(cuid()) + projectId String? + name String + archetype String? + persona String? + speechStyle String? + visualAnchor String? + voiceProfile String? + relations String? + tags String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject? @relation(fields: [projectId], references: [id], onDelete: SetNull) + + @@index([projectId]) + @@index([name]) +} + +model DramaStoryboard { + id String @id @default(cuid()) + projectId String + episodeId String + version Int @default(1) + status String @default("draft") + summary String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode DramaEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + shots DramaShot[] + + @@index([projectId]) + @@index([episodeId]) +} + +model DramaShot { + id String @id @default(cuid()) + storyboardId String + order Int + shotSize String? + cameraMove String? + durationSec Int? + location String? + action String + dialogue String? + characterRefs String? + visualPrompt String? + keyframeData String? + dialogueAudioData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + storyboard DramaStoryboard @relation(fields: [storyboardId], references: [id], onDelete: Cascade) + + @@unique([storyboardId, order]) + @@index([storyboardId]) +} + +model DramaVideoPrompt { + id String @id @default(cuid()) + projectId String + episodeId String? + shotId String? + provider String @default("mock") + prompt String + negativePrompt String? + aspectRatio String @default("9:16") + durationSec Int? + status String @default("prompted") + version Int @default(1) + supersededById String? + providerTaskId String? + resultUrl String? + failureReason String? + providerResult String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode DramaEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) + + @@index([projectId]) + @@index([episodeId]) + @@index([projectId, shotId, version]) + @@index([provider, status]) +} + +model DramaBatchJob { + id String @id @default(cuid()) + projectId String + episodeId String? + type String + status String @default("pending") + progress String @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project DramaProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode DramaEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) + + @@index([projectId, createdAt]) + @@index([episodeId, status]) + @@index([type, status]) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Comic(AI 漫画)模块 +// 低耦合要点:所有外键对 Novel 零引用,sourceRef 为软引用(不建外键,保证可拆分) +// ───────────────────────────────────────────────────────────────────────────── + +model ComicProject { + id String @id @default(cuid()) + title String + sourceType String @default("original") + sourceRef String? + sourceInput String? + trackId String? + stylePreset String? + status String @default("draft") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sourceBundle ComicSourceBundle? + characters ComicCharacter[] + characterAssets ComicCharacterAsset[] + scenes ComicScene[] + episodes ComicEpisode[] + facts ComicFact[] + uploadAssets ComicUploadAsset[] + exportJobs ComicExportJob[] + batchJobs ComicBatchJob[] + + @@index([sourceType]) + @@index([status]) +} + +model ComicSourceBundle { + id String @id @default(cuid()) + projectId String @unique + bundleJson String + importedAt DateTime @default(now()) + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) +} + +model ComicCharacter { + id String @id @default(cuid()) + projectId String + name String + gender String @default("unknown") + persona String? + visualAnchor String? + sheetData String? + sourceCharacterRef String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + assets ComicCharacterAsset[] + + @@index([projectId]) +} + +model ComicCharacterAsset { + id String @id @default(cuid()) + characterId String + projectId String + assetType String + name String + description String? + imageData String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + character ComicCharacter @relation(fields: [characterId], references: [id], onDelete: Cascade) + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([characterId]) + @@index([projectId]) +} + +model ComicScene { + id String @id @default(cuid()) + projectId String + name String + sceneType String @default("interior") + bible String? + sheetData String? + sortOrder Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model ComicEpisode { + id String @id @default(cuid()) + projectId String + order Int + title String? + hookType String? + cliffhanger String? + isPaywalled Boolean @default(false) + outline String? + sourceText String? + status String @default("draft") + scriptConfig String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + panels ComicPanel[] + batchJobs ComicBatchJob[] + + @@unique([projectId, order]) + @@index([projectId, status]) +} + +model ComicPanel { + id String @id @default(cuid()) + episodeId String + order Int + panelType String? + action String + dialogues String? + characterRefs String? + sceneRef String? + visualPrompt String? + densityLevel String? + focus String? + layoutData String? + imageData String? + letteredData String? + motionData String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + episode ComicEpisode @relation(fields: [episodeId], references: [id], onDelete: Cascade) + + @@unique([episodeId, order]) + @@index([episodeId]) +} + +model ComicFact { + id String @id @default(cuid()) + projectId String + text String + category String @default("completed") + episodeOrder Int? + createdAt DateTime @default(now()) + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model ComicUploadAsset { + id String @id @default(cuid()) + projectId String + kind String + filePath String + refId String? + metadata String? + createdAt DateTime @default(now()) + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId, kind]) +} + +model ComicExportJob { + id String @id @default(cuid()) + projectId String + episodeId String? + format String + spec String? + status String @default("pending") + artifacts String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId, createdAt]) + @@index([format, status]) +} + +model ComicBatchJob { + id String @id @default(cuid()) + projectId String + episodeId String? + type String + status String @default("pending") + progress String @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + project ComicProject @relation(fields: [projectId], references: [id], onDelete: Cascade) + episode ComicEpisode? @relation(fields: [episodeId], references: [id], onDelete: SetNull) + + @@index([projectId, createdAt]) + @@index([episodeId, status]) + @@index([type, status]) +} diff --git a/android-app/app/src/main/assets/noderuntime/capi.so b/android-app/app/src/main/assets/noderuntime/capi.so new file mode 100644 index 0000000000..5514210d43 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/capi.so differ diff --git a/android-app/app/src/main/assets/noderuntime/legacy.so b/android-app/app/src/main/assets/noderuntime/legacy.so new file mode 100644 index 0000000000..39fb2b6445 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/legacy.so differ diff --git a/android-app/app/src/main/assets/noderuntime/libc++_shared.so b/android-app/app/src/main/assets/noderuntime/libc++_shared.so new file mode 100644 index 0000000000..6c115e75cf Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libc++_shared.so differ diff --git a/android-app/app/src/main/assets/noderuntime/libcares.so b/android-app/app/src/main/assets/noderuntime/libcares.so new file mode 100644 index 0000000000..6749cf0e38 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libcares.so differ diff --git a/android-app/app/src/main/assets/noderuntime/libcrypto.so.3 b/android-app/app/src/main/assets/noderuntime/libcrypto.so.3 new file mode 100644 index 0000000000..3d51cd599e Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libcrypto.so.3 differ diff --git a/android-app/app/src/main/assets/noderuntime/libffi.so b/android-app/app/src/main/assets/noderuntime/libffi.so new file mode 100644 index 0000000000..cba703d701 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libffi.so differ diff --git a/android-app/app/src/main/assets/noderuntime/libicudata.so.78 b/android-app/app/src/main/assets/noderuntime/libicudata.so.78 new file mode 100644 index 0000000000..c507d0011e Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libicudata.so.78 differ diff --git a/android-app/app/src/main/assets/noderuntime/libicui18n.so.78 b/android-app/app/src/main/assets/noderuntime/libicui18n.so.78 new file mode 100644 index 0000000000..2ad9143514 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libicui18n.so.78 differ diff --git a/android-app/app/src/main/assets/noderuntime/libicuio.so.78 b/android-app/app/src/main/assets/noderuntime/libicuio.so.78 new file mode 100644 index 0000000000..c8e2b77b0f Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libicuio.so.78 differ diff --git a/android-app/app/src/main/assets/noderuntime/libicutu.so.78 b/android-app/app/src/main/assets/noderuntime/libicutu.so.78 new file mode 100644 index 0000000000..a54367db1d Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libicutu.so.78 differ diff --git a/android-app/app/src/main/assets/noderuntime/libicuuc.so.78 b/android-app/app/src/main/assets/noderuntime/libicuuc.so.78 new file mode 100644 index 0000000000..ff9dfa25a8 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libicuuc.so.78 differ diff --git a/android-app/app/src/main/assets/noderuntime/libsqlite3.so b/android-app/app/src/main/assets/noderuntime/libsqlite3.so new file mode 100644 index 0000000000..988437cba8 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libsqlite3.so differ diff --git a/android-app/app/src/main/assets/noderuntime/libsqlite3.so.0 b/android-app/app/src/main/assets/noderuntime/libsqlite3.so.0 new file mode 100644 index 0000000000..988437cba8 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libsqlite3.so.0 differ diff --git a/android-app/app/src/main/assets/noderuntime/libssl.so.3 b/android-app/app/src/main/assets/noderuntime/libssl.so.3 new file mode 100644 index 0000000000..fca4688c67 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libssl.so.3 differ diff --git a/android-app/app/src/main/assets/noderuntime/libuv.so b/android-app/app/src/main/assets/noderuntime/libuv.so new file mode 100644 index 0000000000..9f8965928e Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libuv.so differ diff --git a/android-app/app/src/main/assets/noderuntime/libz.so.1 b/android-app/app/src/main/assets/noderuntime/libz.so.1 new file mode 100644 index 0000000000..98fe42e887 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/libz.so.1 differ diff --git a/android-app/app/src/main/assets/noderuntime/loader_attic.so b/android-app/app/src/main/assets/noderuntime/loader_attic.so new file mode 100644 index 0000000000..e0e58b8f73 Binary files /dev/null and b/android-app/app/src/main/assets/noderuntime/loader_attic.so differ diff --git a/android-app/app/src/main/assets/noderuntime/runtime.list b/android-app/app/src/main/assets/noderuntime/runtime.list new file mode 100644 index 0000000000..cef74d6e55 --- /dev/null +++ b/android-app/app/src/main/assets/noderuntime/runtime.list @@ -0,0 +1,17 @@ +capi.so +legacy.so +libc++_shared.so +libcares.so +libcrypto.so.3 +libffi.so +libicudata.so.78 +libicui18n.so.78 +libicuio.so.78 +libicutu.so.78 +libicuuc.so.78 +libsqlite3.so +libsqlite3.so.0 +libssl.so.3 +libuv.so +libz.so.1 +loader_attic.so diff --git a/android-app/app/src/main/java/com/ainovel/assistant/MainActivity.java b/android-app/app/src/main/java/com/ainovel/assistant/MainActivity.java new file mode 100644 index 0000000000..5506b19d97 --- /dev/null +++ b/android-app/app/src/main/java/com/ainovel/assistant/MainActivity.java @@ -0,0 +1,250 @@ +package com.ainovel.assistant; + +import android.Manifest; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.LinearLayout; + +import java.io.File; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.core.view.WindowInsetsControllerCompat; + +public class MainActivity extends AppCompatActivity { + + private static final int NOTIF_PERM_REQUEST = 1001; + public static final String ACTION_SERVER_READY = "com.ainovel.assistant.SERVER_READY"; + private WebView webView; + private LinearLayout loadingLayout; + private boolean frontendLoaded = false; + private final BroadcastReceiver serverReadyReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + loadFrontend(); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // 沉浸式:隐藏状态栏,前端内容占满全屏不被遮挡 + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + WindowInsetsControllerCompat insetsController = WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView()); + if (insetsController != null) { + insetsController.hide(WindowInsetsCompat.Type.statusBars()); + insetsController.setSystemBarsBehavior( + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } + + setContentView(R.layout.activity_main); + + webView = findViewById(R.id.webview); + loadingLayout = findViewById(R.id.loading_layout); + WebSettings settings = webView.getSettings(); + settings.setJavaScriptEnabled(true); + settings.setDomStorageEnabled(true); + settings.setAllowFileAccess(true); + // 禁止 HTTP 缓存:前端每次随 APK 更新,旧缓存会导致显示旧版本/旧功能 + settings.setCacheMode(WebSettings.LOAD_NO_CACHE); + // 允许 file:// 页面访问 http://127.0.0.1:3000 的后端 API(跨域) + settings.setAllowUniversalAccessFromFileURLs(true); + settings.setAllowFileAccessFromFileURLs(true); + settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); + // WebView 下载(前端导出 a[download] / blob)→ 写入公共 Downloads/AINovel, + // Android 10+ 通过 MediaStore 免权限写入,用户文件管理器直接可见 + webView.setDownloadListener((url, userAgent, contentDisposition, mimetype, contentLength) -> { + try { + String fileName = parseDownloadFileName(contentDisposition, url); + if (url.startsWith("blob:")) { + saveBlobDownload(url, fileName, mimetype); + } else { + downloadToPublicStorage(url, userAgent, fileName, mimetype); + } + } catch (Exception e) { + android.util.Log.e("AINOVEL", "download failed: " + e.getMessage()); + } + }); + webView.setWebViewClient(new WebViewClient() { + @Override + public void onPageFinished(android.webkit.WebView view, String url) { + super.onPageFinished(view, url); + // 注入安卓 App 标志:前端据此强制桌面布局(本 App 已固定横屏, + // 不使用宽度断点判断移动端,避免低分辨率手机横屏误入移动布局) + view.evaluateJavascript("window.__ANDROID_APP__ = true;", null); + } + }); + + // 直接启动 Node 服务(前台服务在 Android 12+ 即使无通知权限也能运行, + // 只是通知不可见;不阻塞在权限请求上,避免用户未授权导致服务永不启动) + startNodeService(); + + // 注册 server 就绪广播:node 服务监听 3000 后才加载前端,避免白屏/请求失败 + IntentFilter filter = new IntentFilter(ACTION_SERVER_READY); + if (Build.VERSION.SDK_INT >= 33) { + registerReceiver(serverReadyReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED); + } else { + registerReceiver(serverReadyReceiver, filter); + } + + // 兜底:若 60 秒内广播未到达(如 node 已运行但日志未匹配),也尝试加载前端 + webView.postDelayed(() -> loadFrontend(), 60_000); + } + + private void loadFrontend() { + if (frontendLoaded) { + return; + } + frontendLoaded = true; + runOnUiThread(() -> { + // 清掉 WebView 历史缓存(旧前端可能残留,导致版本/功能不是最新) + webView.clearCache(true); + // 前端静态资源解压到 files/www,用 file:// 加载(不依赖后端存活); + // 后端 API 通过 http://127.0.0.1:3000 访问 + File wwwDir = new File(getFilesDir(), "www"); + File indexHtml = new File(wwwDir, "index.html"); + if (indexHtml.exists()) { + webView.loadUrl("file://" + indexHtml.getAbsolutePath()); + } else { + // 兜底:若前端未就绪,直接连后端(后端会返回 API 响应/404) + webView.loadUrl("http://127.0.0.1:3000/"); + } + // 隐藏 Loading,显示 WebView + webView.setVisibility(View.VISIBLE); + if (loadingLayout != null) { + loadingLayout.setVisibility(View.GONE); + } + }); + } + + private void startNodeService() { + try { + startForegroundService(new Intent(this, NodeService.class)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == NOTIF_PERM_REQUEST) { + startNodeService(); + } + } + + @Override + public void onBackPressed() { + if (webView.canGoBack()) { + webView.goBack(); + } else { + super.onBackPressed(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + try { + unregisterReceiver(serverReadyReceiver); + } catch (Exception ignored) { + } + } + + // ---- 下载辅助(导出到公共 Downloads/AINovel)---- + + private static String parseDownloadFileName(String contentDisposition, String url) { + if (contentDisposition != null) { + java.util.regex.Matcher m = java.util.regex.Pattern.compile("filename\\*?=(?:UTF-8'')?\"?([^;\"]+)\"?").matcher(contentDisposition); + if (m.find()) { + try { + return java.net.URLDecoder.decode(m.group(1).trim(), "UTF-8"); + } catch (Exception ignored) { + return m.group(1).trim(); + } + } + } + String path = url.contains("?") ? url.substring(0, url.indexOf("?")) : url; + String name = path.substring(path.lastIndexOf("/") + 1); + return name.isEmpty() ? "download.txt" : name; + } + + /** 常规 URL 下载:流式写入公共 Downloads/AINovel(MediaStore,免权限) */ + private void downloadToPublicStorage(String url, String userAgent, String fileName, String mimetype) { + new Thread(() -> { + try { + java.net.HttpURLConnection conn = (java.net.HttpURLConnection) new java.net.URL(url).openConnection(); + conn.setRequestProperty("User-Agent", userAgent != null ? userAgent : "Mozilla/5.0"); + conn.setConnectTimeout(15000); + conn.setReadTimeout(30000); + java.io.InputStream in = conn.getInputStream(); + writeToPublicDownloads(in, fileName, mimetype); + in.close(); + conn.disconnect(); + } catch (Exception e) { + android.util.Log.e("AINOVEL", "url download failed: " + e.getMessage()); + } + }).start(); + } + + /** blob URL 下载:evaluateJavascript 取回 base64,解码写入公共目录 */ + private void saveBlobDownload(String blobUrl, String fileName, String mimetype) { + webView.evaluateJavascript( + "(async () => { const r = await fetch('" + blobUrl + "'); const b = await r.arrayBuffer(); " + + "let bytes = new Uint8Array(b); let bin = ''; const chunk = 0x8000; " + + "for (let i = 0; i < bytes.length; i += chunk) { bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); } " + + "return btoa(bin); })()", + value -> { + if (value == null || value.equals("null")) return; + try { + String base64 = value.replaceAll("^\"|\"$", ""); + byte[] data = android.util.Base64.decode(base64, android.util.Base64.DEFAULT); + writeToPublicDownloads(new java.io.ByteArrayInputStream(data), fileName, mimetype); + } catch (Exception e) { + android.util.Log.e("AINOVEL", "blob download failed: " + e.getMessage()); + } + }); + } + + /** 写入公共 Downloads/AINovel 目录(Android 10+ MediaStore 免权限) */ + private void writeToPublicDownloads(java.io.InputStream in, String fileName, String mimetype) { + try { + String type = mimetype != null ? mimetype : "application/octet-stream"; + android.content.ContentValues values = new android.content.ContentValues(); + values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); + values.put(MediaStore.Downloads.MIME_TYPE, type); + values.put(MediaStore.Downloads.RELATIVE_PATH, "Download/AINovel"); + android.net.Uri collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); + android.net.Uri item = getContentResolver().insert(collection, values); + if (item == null) { + android.util.Log.e("AINOVEL", "MediaStore insert failed for " + fileName); + return; + } + java.io.OutputStream out = getContentResolver().openOutputStream(item); + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) { + out.write(buf, 0, n); + } + out.flush(); + out.close(); + android.util.Log.i("AINOVEL", "downloaded to Download/AINovel/" + fileName); + } catch (Exception e) { + android.util.Log.e("AINOVEL", "writeToPublicDownloads failed: " + e.getMessage()); + } + } +} diff --git a/android-app/app/src/main/java/com/ainovel/assistant/NodeBridge.java b/android-app/app/src/main/java/com/ainovel/assistant/NodeBridge.java new file mode 100644 index 0000000000..927d73930b --- /dev/null +++ b/android-app/app/src/main/java/com/ainovel/assistant/NodeBridge.java @@ -0,0 +1,36 @@ +package com.ainovel.assistant; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +/** + * 封装 nodejs-mobile 的 JNI 入口。 + * nodejs-mobile gradle plugin 提供 io.nodejs.mobile.Node 类, + * 其静态方法 Node.start(String scriptPath) 在后台线程运行 Node 源码。 + * 通过反射调用,避免编译期对未加载本地库的硬依赖。 + */ +public final class NodeBridge { + + private static final Logger LOG = Logger.getLogger(NodeBridge.class.getName()); + private static final AtomicBoolean started = new AtomicBoolean(false); + private static final String NODE_CLASS = "io.nodejs.mobile.Node"; + + private NodeBridge() {} + + public static void start(String scriptPath) { + if (started.compareAndSet(false, true)) { + try { + Class nodeClass = Class.forName(NODE_CLASS); + java.lang.reflect.Method m = nodeClass.getMethod("start", String.class); + m.invoke(null, scriptPath); + } catch (Throwable t) { + LOG.severe("Failed to start Node runtime: " + t.getMessage()); + started.set(false); + } + } + } + + public static boolean isStarted() { + return started.get(); + } +} diff --git a/android-app/app/src/main/java/com/ainovel/assistant/NodeService.java b/android-app/app/src/main/java/com/ainovel/assistant/NodeService.java new file mode 100644 index 0000000000..051cd1f551 --- /dev/null +++ b/android-app/app/src/main/java/com/ainovel/assistant/NodeService.java @@ -0,0 +1,315 @@ +package com.ainovel.assistant; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Intent; +import android.os.Build; +import android.app.Service; +import android.os.IBinder; +import android.content.res.AssetManager; +import android.content.res.AssetFileDescriptor; + +import androidx.core.app.NotificationCompat; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * 前台服务:把 assets/nodejs 解压到 filesDir,用内嵌的 Termux Node 二进制 + * (lib/arm64/node) 启动后端,常驻通知保活。 + */ +public class NodeService extends Service { + + private static final String CHANNEL_ID = "ainovel_node_channel"; + private static final int NOTIF_ID = 1001; + + private Process nodeProcess; + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + startForeground(NOTIF_ID, buildNotification()); + startNode(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return START_STICKY; + } + + private void startNode() { + if (nodeProcess != null) return; + // 解压 + 启动放后台线程,避免阻塞 Service 创建导致 ANR + new Thread(new Runnable() { + @Override + public void run() { + try { + android.util.Log.e("AINOVEL", "startNode: extracting assets..."); + File nodeDir = new File(getFilesDir(), "nodejs"); + File extractFlag = new File(nodeDir, ".extracted"); + if (!extractFlag.exists()) { + extractZip(getAssets(), "nodejs/node-project.zip", nodeDir); + extractFlag.createNewFile(); + } + android.util.Log.e("AINOVEL", "startNode: assets extracted to " + nodeDir.getAbsolutePath()); + + // 前端静态资源(解压到 files/www,WebView 用 file:// 加载) + File wwwDir = new File(getFilesDir(), "www"); + File wwwFlag = new File(wwwDir, ".extracted"); + if (!wwwFlag.exists()) { + extractZip(getAssets(), "www.zip", wwwDir); + wwwFlag.createNewFile(); + } + + File dataDir = new File(getFilesDir(), "data"); + dataDir.mkdirs(); + + // 强制把 libnodebin.so 从 APK 提取到 nativeLibraryDir(该分区可执行) + try { + System.loadLibrary("nodebin"); + } catch (Throwable t) { + android.util.Log.w("AINOVEL", "System.loadLibrary(nodebin) note: " + t.getMessage()); + } + File nativeLibDir = new File(getApplicationInfo().nativeLibraryDir); + File nodeBin = new File(nativeLibDir, "libnodebin.so"); + if (!nodeBin.exists()) { + throw new IOException("embedded node binary not found at " + nodeBin.getAbsolutePath()); + } + android.util.Log.e("AINOVEL", "startNode: node bin=" + nodeBin.getAbsolutePath()); + + // 解压 node 运行库到 filesDir/bin(库加载不受 noexec 限制) + File binDir = new File(getFilesDir(), "bin"); + extractRuntime(getAssets(), binDir); + android.util.Log.e("AINOVEL", "startNode: runtime at " + binDir.getAbsolutePath()); + + // 启动前用 better-sqlite3 直接执行 Prisma SQL 迁移建表(安卓/web 模式下 server 不自动建表) + java.util.Map dbEnv = new java.util.HashMap<>(); + dbEnv.put("LD_LIBRARY_PATH", binDir.getAbsolutePath() + ":" + System.getenv("LD_LIBRARY_PATH")); + dbEnv.put("HOME", nodeDir.getAbsolutePath()); + dbEnv.put("OPENSSL_CONF", "/dev/null"); + dbEnv.put("AI_NOVEL_APP_DATA_DIR", dataDir.getAbsolutePath()); + try { + File initDb = new File(nodeDir, "init-db.cjs"); + ArrayList dbCmd = new ArrayList<>(); + dbCmd.add(nodeBin.getAbsolutePath()); + dbCmd.add(initDb.getAbsolutePath()); + ProcessBuilder dbpb = new ProcessBuilder(dbCmd); + dbpb.directory(nodeDir); + dbpb.environment().putAll(dbEnv); + dbpb.redirectErrorStream(true); + dbpb.redirectInput(ProcessBuilder.Redirect.from(new java.io.File("/dev/null"))); + Process dbp = dbpb.start(); + java.io.InputStream dbis = dbp.getInputStream(); + java.io.BufferedReader dbr = new java.io.BufferedReader(new java.io.InputStreamReader(dbis)); + String dbl; + while ((dbl = dbr.readLine()) != null) { + android.util.Log.i("NODEJS-MOBILE", "[init-db] " + dbl); + } + int dbc = dbp.waitFor(); + android.util.Log.e("AINOVEL", "init-db exited with code " + dbc); + } catch (Exception dbe) { + android.util.Log.e("AINOVEL", "init-db failed (continuing): " + dbe.getMessage()); + } + + // 构建启动命令 + ArrayList cmd = new ArrayList<>(); + cmd.add(nodeBin.getAbsolutePath()); + cmd.add("app.js"); + // 环境 + java.util.Map env = new java.util.HashMap<>(); + env.put("NODE_PATH", nodeDir.getAbsolutePath()); + env.put("AI_NOVEL_RUNTIME", "web"); + env.put("AI_NOVEL_DATABASE_MODE", "sqlite"); + env.put("AI_NOVEL_APP_DATA_DIR", dataDir.getAbsolutePath()); + env.put("DATABASE_URL", "file:" + nodeDir.getAbsolutePath() + "/dev.db"); + // 允许 file:// 页面(WebView 从本地加载,Origin 为 null)跨域访问后端 API + env.put("CORS_ORIGIN", "null"); + env.put("PORT", "3000"); + env.put("HOST", "127.0.0.1"); + env.put("OPENSSL_CONF", "/dev/null"); + env.put("LD_LIBRARY_PATH", binDir.getAbsolutePath() + ":" + System.getenv("LD_LIBRARY_PATH")); + env.put("HOME", nodeDir.getAbsolutePath()); + + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.directory(nodeDir); + pb.environment().putAll(env); + pb.redirectErrorStream(true); + nodeProcess = pb.start(); + android.util.Log.e("AINOVEL", "startNode: node process started"); + + // 读取 node 输出到 logcat + final InputStream is = nodeProcess.getInputStream(); + new Thread(new Runnable() { + @Override + public void run() { + boolean notified = false; + try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { + String line; + while ((line = br.readLine()) != null) { + android.util.Log.i("NODEJS-MOBILE", line); + // 检测 server 监听就绪(日志含端口/监听字样),通知前端加载 + if (!notified && isServerReadyLine(line)) { + notified = true; + sendServerReadyBroadcast(); + } + } + } catch (IOException e) { + android.util.Log.e("NODEJS-MOBILE", "node stdout reader ended: " + e.getMessage()); + } + } + }).start(); + + int code = nodeProcess.waitFor(); + android.util.Log.e("AINOVEL", "node process exited with code " + code); + nodeProcess = null; + } catch (Exception e) { + android.util.Log.e("AINOVEL", "startNode FAILED", e); + writeDebug("START_FAIL: " + android.util.Log.getStackTraceString(e)); + } + } + }).start(); + } + + private void writeDebug(String msg) { + try { + File f = new File(getExternalFilesDir(null), "debug.txt"); + java.io.FileWriter fw = new java.io.FileWriter(f, true); + fw.write(msg + "\n"); + fw.close(); + } catch (Exception ignore) {} + } + + /** 流式解压 assets 中的单个 zip 到 destDir(避免对大目录逐文件 AssetManager.open) */ + private void extractZip(AssetManager am, String assetPath, File destDir) throws IOException { + destDir.mkdirs(); + android.content.res.AssetFileDescriptor afd = am.openFd(assetPath); + try { + java.io.FileInputStream fis = new java.io.FileInputStream(afd.getFileDescriptor()); + long skip = afd.getStartOffset(); + while (skip > 0) { + long n = fis.skip(skip); + if (n <= 0) break; + skip -= n; + } + java.io.BufferedInputStream bis = new java.io.BufferedInputStream(fis); + ZipInputStream zis = new ZipInputStream(bis); + ZipEntry entry; + int count = 0; + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + File outFile = new File(destDir, name); + if (entry.isDirectory()) { + outFile.mkdirs(); + } else { + outFile.getParentFile().mkdirs(); + try (OutputStream os = new java.io.FileOutputStream(outFile)) { + byte[] buf = new byte[65536]; + int n; + while ((n = zis.read(buf)) > 0) os.write(buf, 0, n); + } + } + zis.closeEntry(); + count++; + if (count % 5000 == 0) { + android.util.Log.e("AINOVEL", "unzipped " + count + " entries"); + } + } + zis.close(); + android.util.Log.e("AINOVEL", "unzip done: " + count + " entries -> " + destDir.getAbsolutePath()); + } finally { + afd.close(); + } + } + + /** 解压 node 运行时(assets/noderuntime/*)到 binDir,并设可执行权限 */ + private void extractRuntime(AssetManager am, File binDir) throws IOException { + binDir.mkdirs(); + ArrayList files = readAssetLines(am, "noderuntime/runtime.list"); + for (String f : files) { + InputStream in = am.open("noderuntime/" + f); + File out = new File(binDir, f); + copyFile(in, out); + if (f.equals("libnodebin.so")) { + out.setExecutable(true); + } + } + android.util.Log.e("AINOVEL", "runtime extracted to " + binDir.getAbsolutePath()); + } + + private ArrayList readAssetLines(AssetManager am, String assetPath) throws IOException { + ArrayList lines = new ArrayList<>(); + InputStream in = am.open(assetPath); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty()) lines.add(line); + } + br.close(); + return lines; + } + + private void copyFile(InputStream in, File out) throws IOException { + out.getParentFile().mkdirs(); + try (OutputStream os = new java.io.FileOutputStream(out)) { + byte[] buf = new byte[8192]; + int n; + while ((n = in.read(buf)) > 0) os.write(buf, 0, n); + } + in.close(); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel ch = new NotificationChannel( + CHANNEL_ID, "小说创作引擎", NotificationManager.IMPORTANCE_LOW); + ch.setDescription("本地 AI 创作服务运行中"); + getSystemService(NotificationManager.class).createNotificationChannel(ch); + } + } + + private Notification buildNotification() { + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("AI 小说创作引擎") + .setContentText("本地服务运行中(localhost:3000)") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setOngoing(true) + .build(); + } + + /** 判断 node 输出行是否为 server 监听就绪日志 */ + private boolean isServerReadyLine(String line) { + if (line == null) return false; + String l = line.toLowerCase(); + return (l.contains("listening") && l.contains("3000")) + || l.contains("listening on http://localhost:3000"); + } + + /** 通知 MainActivity:server 已就绪,可以加载前端 */ + private void sendServerReadyBroadcast() { + try { + Intent intent = new Intent(MainActivity.ACTION_SERVER_READY); + intent.setPackage(getPackageName()); + sendBroadcast(intent); + android.util.Log.e("AINOVEL", "SERVER_READY broadcast sent"); + } catch (Exception e) { + android.util.Log.e("AINOVEL", "broadcast failed: " + e.getMessage()); + } + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } +} diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libIex-3_4.so b/android-app/app/src/main/jniLibs/arm64-v8a/libIex-3_4.so new file mode 100644 index 0000000000..ac4eddcece Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libIex-3_4.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libIlmThread-3_4.so b/android-app/app/src/main/jniLibs/arm64-v8a/libIlmThread-3_4.so new file mode 100644 index 0000000000..50f2a4fbb0 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libIlmThread-3_4.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libImath-3_2.so b/android-app/app/src/main/jniLibs/arm64-v8a/libImath-3_2.so new file mode 100644 index 0000000000..2640659345 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libImath-3_2.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXR-3_4.so b/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXR-3_4.so new file mode 100644 index 0000000000..f8eea53c32 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXR-3_4.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXRCore-3_4.so b/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXRCore-3_4.so new file mode 100644 index 0000000000..f50b931176 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXRCore-3_4.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXRUtil-3_4.so b/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXRUtil-3_4.so new file mode 100644 index 0000000000..3fdabc7de7 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libOpenEXRUtil-3_4.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libc++_shared.so b/android-app/app/src/main/jniLibs/arm64-v8a/libc++_shared.so new file mode 100644 index 0000000000..6c115e75cf Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libc++_shared.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcairo-gobject.so b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo-gobject.so new file mode 100644 index 0000000000..8475541029 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo-gobject.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcairo-gobject.so.2 b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo-gobject.so.2 new file mode 100644 index 0000000000..8475541029 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo-gobject.so.2 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcairo.so b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo.so new file mode 100644 index 0000000000..96234238e1 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcairo.so.2 b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo.so.2 new file mode 100644 index 0000000000..96234238e1 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcairo.so.2 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcares.so b/android-app/app/src/main/jniLibs/arm64-v8a/libcares.so new file mode 100644 index 0000000000..6749cf0e38 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcares.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcfitsio.so b/android-app/app/src/main/jniLibs/arm64-v8a/libcfitsio.so new file mode 100644 index 0000000000..62324223c0 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcfitsio.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcfitsio.so.10 b/android-app/app/src/main/jniLibs/arm64-v8a/libcfitsio.so.10 new file mode 100644 index 0000000000..62324223c0 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcfitsio.so.10 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libcgif.so b/android-app/app/src/main/jniLibs/arm64-v8a/libcgif.so new file mode 100644 index 0000000000..56b4364436 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libcgif.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libexif.so b/android-app/app/src/main/jniLibs/arm64-v8a/libexif.so new file mode 100644 index 0000000000..a94486b453 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libexif.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libexpat.so b/android-app/app/src/main/jniLibs/arm64-v8a/libexpat.so new file mode 100644 index 0000000000..fcf887e09d Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libexpat.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libexpat.so.1 b/android-app/app/src/main/jniLibs/arm64-v8a/libexpat.so.1 new file mode 100644 index 0000000000..fcf887e09d Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libexpat.so.1 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libfftw3.so b/android-app/app/src/main/jniLibs/arm64-v8a/libfftw3.so new file mode 100644 index 0000000000..c3cb0d8361 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libfftw3.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libfontconfig.so b/android-app/app/src/main/jniLibs/arm64-v8a/libfontconfig.so new file mode 100644 index 0000000000..4a8148740c Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libfontconfig.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libgio-2.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libgio-2.0.so.0 new file mode 100644 index 0000000000..6c1a72d905 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libgio-2.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libglib-2.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libglib-2.0.so.0 new file mode 100644 index 0000000000..da6116db1a Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libglib-2.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libgmodule-2.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libgmodule-2.0.so.0 new file mode 100644 index 0000000000..e8a2e92e62 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libgmodule-2.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libgobject-2.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libgobject-2.0.so.0 new file mode 100644 index 0000000000..ab0769323c Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libgobject-2.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libimagequant.so b/android-app/app/src/main/jniLibs/arm64-v8a/libimagequant.so new file mode 100644 index 0000000000..7d6333bc2f Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libimagequant.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libjpeg.so b/android-app/app/src/main/jniLibs/arm64-v8a/libjpeg.so new file mode 100644 index 0000000000..b65a041fba Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libjpeg.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libjpeg.so.8 b/android-app/app/src/main/jniLibs/arm64-v8a/libjpeg.so.8 new file mode 100644 index 0000000000..b65a041fba Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libjpeg.so.8 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/liblcms2.so b/android-app/app/src/main/jniLibs/arm64-v8a/liblcms2.so new file mode 100644 index 0000000000..65661976d8 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/liblcms2.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libnodebin.so b/android-app/app/src/main/jniLibs/arm64-v8a/libnodebin.so new file mode 100644 index 0000000000..61239893be Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libnodebin.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libopenjp2.so b/android-app/app/src/main/jniLibs/arm64-v8a/libopenjp2.so new file mode 100644 index 0000000000..903e791d9f Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libopenjp2.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libopenjph.so b/android-app/app/src/main/jniLibs/arm64-v8a/libopenjph.so new file mode 100644 index 0000000000..2d3adacdc9 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libopenjph.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libpango-1.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libpango-1.0.so.0 new file mode 100644 index 0000000000..80690c690f Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libpango-1.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libpangocairo-1.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libpangocairo-1.0.so.0 new file mode 100644 index 0000000000..c9b004c734 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libpangocairo-1.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libpangoft2-1.0.so.0 b/android-app/app/src/main/jniLibs/arm64-v8a/libpangoft2-1.0.so.0 new file mode 100644 index 0000000000..f6e8fdde97 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libpangoft2-1.0.so.0 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libpng16.so b/android-app/app/src/main/jniLibs/arm64-v8a/libpng16.so new file mode 100644 index 0000000000..9d1c9dd344 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libpng16.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/librsvg-2.so b/android-app/app/src/main/jniLibs/arm64-v8a/librsvg-2.so new file mode 100644 index 0000000000..39435c899a Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/librsvg-2.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libsharpyuv.so b/android-app/app/src/main/jniLibs/arm64-v8a/libsharpyuv.so new file mode 100644 index 0000000000..7c211c6f2b Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libsharpyuv.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libtiff.so b/android-app/app/src/main/jniLibs/arm64-v8a/libtiff.so new file mode 100644 index 0000000000..947d622bf8 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libtiff.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libuv.so b/android-app/app/src/main/jniLibs/arm64-v8a/libuv.so new file mode 100644 index 0000000000..9f8965928e Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libuv.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libvips-cpp.so b/android-app/app/src/main/jniLibs/arm64-v8a/libvips-cpp.so new file mode 100644 index 0000000000..06a7a88162 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libvips-cpp.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libvips-cpp.so.42 b/android-app/app/src/main/jniLibs/arm64-v8a/libvips-cpp.so.42 new file mode 100644 index 0000000000..06a7a88162 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libvips-cpp.so.42 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libvips.so b/android-app/app/src/main/jniLibs/arm64-v8a/libvips.so new file mode 100644 index 0000000000..c23b233f3f Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libvips.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libvips.so.42 b/android-app/app/src/main/jniLibs/arm64-v8a/libvips.so.42 new file mode 100644 index 0000000000..c23b233f3f Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libvips.so.42 differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libwebp.so b/android-app/app/src/main/jniLibs/arm64-v8a/libwebp.so new file mode 100644 index 0000000000..5fa506f790 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libwebp.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libwebpdemux.so b/android-app/app/src/main/jniLibs/arm64-v8a/libwebpdemux.so new file mode 100644 index 0000000000..36a11db1e5 Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libwebpdemux.so differ diff --git a/android-app/app/src/main/jniLibs/arm64-v8a/libwebpmux.so b/android-app/app/src/main/jniLibs/arm64-v8a/libwebpmux.so new file mode 100644 index 0000000000..059773576e Binary files /dev/null and b/android-app/app/src/main/jniLibs/arm64-v8a/libwebpmux.so differ diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..6f41a27a30 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..204f0fdf59 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..f1db564cab Binary files /dev/null and b/android-app/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..d9ca54ec03 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..0ed79e2579 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..44922fa395 Binary files /dev/null and b/android-app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..82c931b68e --- /dev/null +++ b/android-app/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + AI 小说创作 + diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml new file mode 100644 index 0000000000..6ce361ceea --- /dev/null +++ b/android-app/app/src/main/res/values/themes.xml @@ -0,0 +1,6 @@ + + + + diff --git a/android-app/build.gradle b/android-app/build.gradle new file mode 100644 index 0000000000..5dded59812 --- /dev/null +++ b/android-app/build.gradle @@ -0,0 +1,9 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.5.2' + } +} diff --git a/android-app/gradle.properties b/android-app/gradle.properties new file mode 100644 index 0000000000..bc2c358f0c --- /dev/null +++ b/android-app/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=true +android.nativeBuild.fortify=false diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..09523c0e54 --- /dev/null +++ b/android-app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android-app/gradlew.bat b/android-app/gradlew.bat new file mode 100644 index 0000000000..78f4bbd8a5 --- /dev/null +++ b/android-app/gradlew.bat @@ -0,0 +1,5 @@ +@rem gradlew.bat - 使用本地 gradle 8.9 +@echo off +set DIR=%~dp0 +set GRADLE_HOME=C:\Android\gradle-8.9 +"%GRADLE_HOME%\bin\gradle.bat" %* diff --git a/android-app/settings.gradle b/android-app/settings.gradle new file mode 100644 index 0000000000..6390007e76 --- /dev/null +++ b/android-app/settings.gradle @@ -0,0 +1,20 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url 'https://maven.google.com' } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + maven { url 'https://maven.google.com' } + } +} + +rootProject.name = "AINovelAssistant" +include ':app' diff --git a/android/.env.android b/android/.env.android new file mode 100644 index 0000000000..19f53459f8 --- /dev/null +++ b/android/.env.android @@ -0,0 +1,13 @@ +# 安卓端环境变量覆盖(参考配置,NodeService.java 已注入等效默认值) +# 离线优先:不配置 QDRANT_URL 即禁用 RAG +# 漫画工作台:使用 WASM sharp(@img/sharp-wasm32 + @emnapi/runtime),无需原生库 + +AI_NOVEL_RUNTIME=web +AI_NOVEL_DATABASE_MODE=sqlite +AI_NOVEL_DISABLE_RAG=1 + +# 如需后台常驻 + LAN 访问(同一 WiFi 下 PC 调试)可取消下一行注释 +# ALLOW_LAN=1 + +# 数据落盘目录(Termux 下可在 .bashrc 预置;nodejs-mobile 下由 App 注入) +# AI_NOVEL_APP_DATA_DIR=/data/data/com.ainovel.app/files/data diff --git a/android/build-android-assets.sh b/android/build-android-assets.sh new file mode 100644 index 0000000000..89c3ddbc2b --- /dev/null +++ b/android/build-android-assets.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# ============================================================================ +# AI-Novel-Writing-Assistant — 安卓端资产构建脚本 +# +# 作用:从源码生成安卓资产,**每次构建 APK 前都必须运行**(不是只跑一次): +# 1) android-app/app/src/main/assets/nodejs/bundle.cjs (server 打包产物) +# 2) android-app/app/src/main/assets/www.zip (前端静态资源) +# 3) android-app/app/src/main/assets/nodejs/node-project.zip (node 运行环境,固定资产) +# 并注入 NDK 编译的 bionic sharp.node(漫画工作台原生渲染)。 +# +# ⚠️ 重要:www.zip 必须每次重新生成!NodeService 运行时解压的是 www.zip +# (不是 assets/www/ 目录)。若只同步 www/ 目录而不重新打 www.zip, +# 打包进 APK 的仍是旧前端(版本号/功能不更新)。前端代码有任何改动, +# 都必须重跑本脚本再构建 APK。 +# +# 用法(MSYS/Git-Bash 或 Linux/macOS 均可): +# bash android/build-android-assets.sh +# cd android-app && gradle clean assembleDebug +# +# 注意: +# - esbuild 必须用项目内 node_modules/.pnpm/esbuild@0.27.3(pnpm exec 可能解析到别的版本) +# - Python 用 PY 环境变量覆盖(Windows 下写死 Python311 全路径) +# - bundle.cjs 只 external sharp/better-sqlite3/@prisma/client(原生/重型模块), +# express/cors/helmet 等必须打进包内(否则安卓运行时 require 404) +# - www.zip / node-project.zip 条目不能带 www/ 或 nodejs/ 前缀 +# (NodeService 解压到 files/www、files/nodejs,多一层前缀会路径错位) +# ============================================================================ +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +NODEJS_ASSETS="$PROJECT_ROOT/android-app/app/src/main/assets/nodejs" +WWW_DIR="$PROJECT_ROOT/android-app/app/src/main/assets/www" +ASSETS_ROOT="$PROJECT_ROOT/android-app/app/src/main/assets" + +# Python:Windows 下裸 python3 是商店桩,必须显式全路径;可用 PY 覆盖 +PY="${PY:-C:/Users/Administrator/AppData/Local/Programs/Python/Python311/python.exe}" +if ! "$PY" --version >/dev/null 2>&1; then + PY="$(command -v python3 || command -v python)" +fi + +to_win() { + if command -v cygpath >/dev/null 2>&1; then cygpath -w "$1"; else echo "$1"; fi +} + +# esbuild:锁定项目内版本(pnpm exec 会解析到错误版本) +ESBUILD="" +for cand in "$PROJECT_ROOT"/node_modules/.pnpm/esbuild@*/node_modules/esbuild/bin/esbuild; do + [ -f "$cand" ] && ESBUILD="$cand" && break +done +if [ -z "$ESBUILD" ]; then + echo "[android-assets] ERROR: 未找到项目内 esbuild,请先 pnpm install" >&2 + exit 1 +fi + +echo "[android-assets] PROJECT_ROOT = $PROJECT_ROOT" + +# --------------------------------------------------------------------------- +# 0) 构建 @ai-novel/shared(workspace 包,tsc 编译 dist;bundle 需要其 exports) +# --------------------------------------------------------------------------- +echo "[android-assets] 0/3 构建 @ai-novel/shared ..." +if [ -d "$PROJECT_ROOT/shared" ]; then + (cd "$PROJECT_ROOT/shared" && pnpm build >/tmp/shared_build.log 2>&1) \ + && echo " shared build OK" \ + || { echo "[android-assets] WARN: shared 构建失败,继续(可能已有 dist)" >&2; } +else + echo "[android-assets] WARN: shared 目录不存在,跳过" >&2 +fi + +# --------------------------------------------------------------------------- +# 1) bundle.cjs:esbuild 打包 server/src/app.ts(server 全部代码) +# 只 external 原生/重型模块;express/cors/helmet/morgan 等打进包内 +# --------------------------------------------------------------------------- +echo "[android-assets] 1/3 构建 bundle.cjs ..." +ESB_WIN="$(to_win "$ESBUILD")" +SRC_WIN="$(to_win "$PROJECT_ROOT/server/src/app.ts")" +OUT_WIN="$(to_win "$NODEJS_ASSETS/bundle.cjs")" +"$ESB_WIN" "$SRC_WIN" \ + --bundle --platform=node --format=cjs \ + --outfile="$OUT_WIN" \ + --external:sharp --external:better-sqlite3 --external:@prisma/client \ + --log-level=warning +echo " bundle.cjs $(stat -c%s "$NODEJS_ASSETS/bundle.cjs" 2>/dev/null || wc -c < "$NODEJS_ASSETS/bundle.cjs") bytes" + +# --------------------------------------------------------------------------- +# 2) 前端构建 + www.zip:vite 构建(相对 base + 安卓 API 地址)→ www/ → 打包 +# 注意:必须 AI_NOVEL_CLIENT_BASE=relative(file:// 加载需要相对路径), +# VITE_API_BASE_URL 指向 127.0.0.1:3000(安卓上 localhost 解析异常) +# --------------------------------------------------------------------------- +echo "[android-assets] 2/3 构建前端 www.zip ..." +if [ -d "$PROJECT_ROOT/client/node_modules" ]; then + (cd "$PROJECT_ROOT/client" && AI_NOVEL_CLIENT_BASE=relative VITE_API_BASE_URL="http://127.0.0.1:3000/api" npx vite build >/tmp/vite_android_build.log 2>&1) \ + && echo " vite build OK" \ + || { echo "[android-assets] WARN: vite build 失败,使用现有 www/ 目录" >&2; } + # vite 输出到 dist/,同步到 www/ + if [ -d "$PROJECT_ROOT/client/dist" ]; then + rm -rf "$WWW_DIR" + cp -r "$PROJECT_ROOT/client/dist" "$WWW_DIR" + fi +else + echo "[android-assets] WARN: client/node_modules 不存在,跳过前端构建(使用现有 www/)" >&2 +fi +WWW_ZIP_WIN="$(to_win "$ASSETS_ROOT/www.zip")" +rm -f "$ASSETS_ROOT/www.zip" +(cd "$WWW_DIR" && "$PY" - "$WWW_ZIP_WIN" <<'PYEOF' +import sys, os, zipfile +out = os.path.abspath(sys.argv[1]) +z = zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) +count = 0 +for root, _, files in os.walk("."): + for f in files: + p = os.path.join(root, f) + if os.path.abspath(p) == out: + continue # 跳过输出文件自身,避免无限自包含 + arc = p[2:] if p.startswith("./") else p # 去 ./ 前缀 + z.write(p, arc) + count += 1 +z.close() +print(f" www.zip {count} files -> {os.path.getsize(out)} bytes") +PYEOF +) + +# --------------------------------------------------------------------------- +# 3) node-project.zip:固定资产(依赖锁定),不重新打包。 +# 依赖树包含安卓版原生库(better-sqlite3 ELF + WASM sharp 运行时), +# Windows 开发机无法凭空生成(pnpm 会装 Windows 版原生库导致真机 +# bad ELF magic)。zip 缺失时从 GitHub Release 下载(见 README)。 +# --------------------------------------------------------------------------- +if [ -f "$NODEJS_ASSETS/node-project.zip" ]; then + echo "[android-assets] 3/3 node-project.zip 已存在(固定资产,跳过重建)" + echo " $(stat -c%s "$NODEJS_ASSETS/node-project.zip" 2>/dev/null || wc -c < "$NODEJS_ASSETS/node-project.zip") bytes" +else + echo "[android-assets] 3/3 WARN: node-project.zip 缺失!" >&2 + echo " 请从 GitHub Release 下载(README 有说明),或从发布版 APK 提取。" >&2 +fi + +# 注:漫画工作台 sharp 使用 WASM 版(@img/sharp-wasm32 + @emnapi/runtime), +# 不注入 bionic sharp.node——原生版依赖 libvips 全家(libstdc++ ABI 在真机 +# 不兼容,dlopen 失败);WASM 版免原生库、真机验证渲染正常。 + +echo "[android-assets] 完成。现在可构建 APK:" +echo " cd android-app && gradle clean assembleDebug" diff --git a/android/fetch-termux-libs.py b/android/fetch-termux-libs.py new file mode 100644 index 0000000000..b24ae8067c --- /dev/null +++ b/android/fetch-termux-libs.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +"""从 Termux 仓库批量下载缺失的 .so 并放入 assets/noderuntime。 + +用法: python fetch-termux-libs.py +""" +import lzma +import os +import re +import shutil +import subprocess +import tarfile +import urllib.request + +BASE = "https://packages.termux.dev/apt/termux-main" +DEST = "C:/Users/Administrator/AI-Novel-Writing-Assistant/android-app/app/src/main/assets/noderuntime" + +# 缺失库 -> 可能来源包 +NEEDED = { + "libiconv.so": "libiconv", + "liblzma.so.5": "liblzma", + "libpcre2-8.so": "libpcre2", + "libxml2.so.16": "libxml2", + "libzstd.so.1": "libzstd", + "libharfbuzz.so": "libharfbuzz", + "libpixman-1.so": "libpixman", + "libxcb.so": "libxcb", + "libxcb-render.so": "libxcb", + "libxcb-shm.so": "libxcb", + "libgdk_pixbuf-2.0.so.0": "libgdk-pixbuf", + "libstdc++.so": "libc++", + "libbz2.so.1.0": "libbz2", + "libpng.so": "libpng", + "libjpeg.so": "libjpeg", + "libwebp.so": "libwebp", +} + +# 下载 Packages 索引 +print("downloading Packages index...") +urllib.request.urlretrieve(f"{BASE}/dists/stable/main/binary-aarch64/Packages.gz", "/tmp/pkgs_full.gz") +with open("/tmp/pkgs_full.gz", "rb") as f: + import gzip + pkgs_text = gzip.decompress(f.read()).decode("utf-8", errors="ignore") + +# 解析 Package -> Filename +pkg_file = {} +current = None +for line in pkgs_text.splitlines(): + if line.startswith("Package: "): + current = line[len("Package: "):].strip() + elif line.startswith("Filename: ") and current: + pkg_file[current] = line[len("Filename: "):].strip() + current = None + +# 对每个缺失库,找到其真实来源包(从库文件反查) +# Termux 的库文件名与包名对应:libiconv.so -> libiconv 包等 +def find_package_for_lib(libname): + # libxcb-render/libxcb-shm 在 libxcb 包;libstdc++ 在 libc++ 包 + pkg_guess = NEEDED.get(libname) + return pkg_guess + +found = {} +for lib, pkg in NEEDED.items(): + fn = pkg_file.get(pkg) + if fn: + found[lib] = fn + print(f" {lib} <- {pkg}: {fn}") + else: + print(f" {lib} <- {pkg}: NOT FOUND in index") + +# 下载并解压 +for lib, fn in found.items(): + url = f"{BASE}/{fn}" + deb = f"/tmp/{os.path.basename(fn)}" + try: + urllib.request.urlretrieve(url, deb) + with open(deb, "rb") as f: + data = f.read() + pos = 8 + extracted = [] + while pos < len(data): + name = data[pos:pos+16].decode().strip() + size = int(data[pos+48:pos+58].decode().strip()) + content = data[pos+60:pos+60+size] + if "data.tar" in name: + tar_data = lzma.decompress(content) if name.endswith("xz") else content + tf = tarfile.open(fileobj=__import__("io").BytesIO(tar_data)) + for m in tf.getmembers(): + if m.isfile() and (m.name.endswith(".so") or ".so." in m.name): + base = os.path.basename(m.name) + # 匹配目标库(含无后缀/带版本) + want_base = lib.split(".")[0] + if base.startswith(want_base) or lib.startswith(base.split(".")[0]): + data_out = tf.extractfile(m).read() + out = os.path.join(DEST, base) + open(out, "wb").write(data_out) + extracted.append(base) + pos += 60 + size + (size % 2) + print(f" {lib}: extracted {extracted}") + except Exception as e: + print(f" {lib}: FAILED {e}") + +# 更新 runtime.list +rl = os.path.join(DEST, "runtime.list") +existing = set(open(rl).read().split()) if os.path.exists(rl) else set() +for f in os.listdir(DEST): + if f.endswith(".so") or ".so." in f: + existing.add(f) +open(rl, "w").write("\n".join(sorted(existing)) + "\n") +print(f"runtime.list: {len(existing)} entries") diff --git a/android/fetch-termux-libs2.py b/android/fetch-termux-libs2.py new file mode 100644 index 0000000000..61408ab913 --- /dev/null +++ b/android/fetch-termux-libs2.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""从 Termux 仓库自动补齐缺失的 .so:按库文件名反查包并下载解压。""" +import gzip +import io +import lzma +import os +import tarfile +import urllib.request + +BASE = "https://packages.termux.dev/apt/termux-main" +DEST = "C:/Users/Administrator/AI-Novel-Writing-Assistant/android-app/app/src/main/assets/noderuntime" + +# 目标缺失库(libname -> 候选包名) +TARGETS = { + "libpcre2-8.so": ["libpcre2-8", "libpcre2"], + "libzstd.so.1": ["libzstd", "zstd"], + "libharfbuzz.so": ["libharfbuzz", "harfbuzz"], + "libfribidi.so": ["libfribidi", "fribidi"], + "libcurl.so": ["libcurl", "curl"], + "libgdk_pixbuf-2.0.so.0": ["libgdk-pixbuf", "gdk-pixbuf"], + "libX11.so": ["libx11", "x11"], + "libXau.so": ["libxau", "xau"], + "libXdmcp.so": ["libxdmcp", "xdmcp"], + "libXext.so": ["libxext", "xext"], + "libXrender.so": ["libxrender", "xrender"], + "libandroid-shmem.so": ["libandroid-shmem", "android-shmem"], + "libstdc++.so": ["libc++", "libstdc++"], +} + +print("downloading Packages index...") +urllib.request.urlretrieve(f"{BASE}/dists/stable/main/binary-aarch64/Packages.gz", "/tmp/pkgs2.gz") +with open("/tmp/pkgs2.gz", "rb") as f: + pkgs_text = gzip.decompress(f.read()).decode("utf-8", errors="ignore") + +# 解析所有包块 +blocks = {} +current = None +for line in pkgs_text.splitlines(): + if line.startswith("Package: "): + current = line[len("Package: "):].strip() + blocks[current] = {} + elif current and ":" in line: + k, v = line.split(":", 1) + blocks[current][k.strip()] = v.strip() + +# 反查:包含目标库文件的包(看 Installed-Size 或依赖,最可靠是直接尝试候选包名) +def find_filename(libname, candidates): + for c in candidates: + if c in blocks: + fn = blocks[c].get("Filename") + if fn: + return fn + # 模糊匹配包名包含候选关键词 + key = candidates[0].replace("lib", "").replace("-", "") + for pkg, info in blocks.items(): + if key in pkg.lower(): + fn = info.get("Filename") + if fn and fn.endswith("_aarch64.deb"): + return fn + return None + +ok = 0 +fail = [] +for lib, cands in TARGETS.items(): + fn = find_filename(lib, cands) + if not fn: + fail.append(lib) + print(f" {lib}: NO PACKAGE FOUND") + continue + url = f"{BASE}/{fn}" + deb = f"/tmp/tl_{os.path.basename(fn)}" + try: + urllib.request.urlretrieve(url, deb) + with open(deb, "rb") as f: + data = f.read() + pos = 8 + got = [] + while pos < len(data): + name = data[pos:pos+16].decode().strip() + size = int(data[pos+48:pos+58].decode().strip()) + content = data[pos+60:pos+60+size] + if "data.tar" in name: + tar_data = lzma.decompress(content) if name.endswith("xz") else content + tf = tarfile.open(fileobj=io.BytesIO(tar_data)) + for m in tf.getmembers(): + if m.isfile() and (m.name.endswith(".so") or ".so." in m.name): + base = os.path.basename(m.name) + # 接受目标库名匹配(前缀或包含) + want = lib.split(".so")[0] + if base.startswith(want) or want.startswith(base.split(".so")[0]): + open(os.path.join(DEST, base), "wb").write(tf.extractfile(m).read()) + got.append(base) + pos += 60 + size + (size % 2) + print(f" {lib} ({os.path.basename(fn)}): {got}") + ok += 1 + except Exception as e: + fail.append(lib) + print(f" {lib}: FAIL {e}") + +print(f"\n完成: {ok} 成功, {len(fail)} 失败: {fail}") diff --git a/android/prune-node-project.py b/android/prune-node-project.py new file mode 100644 index 0000000000..b06e8a99a2 --- /dev/null +++ b/android/prune-node-project.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""裁剪 node-project.zip:只保留安卓运行时必需的依赖。 + +bundle.cjs 已内联所有纯 JS 依赖(express/langchain/@aws-sdk 等), +node_modules 运行时只需 sharp、better-sqlite3、@prisma/client 三棵依赖树。 +本脚本从完整 zip 裁剪出精简 zip,压缩后 <100MB 可直接提交进仓库。 + +用法: python prune-node-project.py +""" +import sys +import zipfile + +KEEP_PREFIXES = ( + # sharp 及其运行时依赖 + "node_modules/sharp/", + "node_modules/@img/colour/", + "node_modules/@img/sharp-wasm32/", + "node_modules/detect-libc/", + "node_modules/semver/", + "node_modules/color/", + "node_modules/color-string/", + "node_modules/color-convert/", + "node_modules/color-name/", + "node_modules/simple-swizzle/", + "node_modules/is-arrayish/", + # better-sqlite3 及其运行时依赖 + "node_modules/better-sqlite3/", + "node_modules/bindings/", + "node_modules/file-uri-to-path/", + "node_modules/prebuild-install/", + "node_modules/rc/", + "node_modules/tar-fs/", + "node_modules/tar-stream/", + "node_modules/bl/", + "node_modules/end-of-stream/", + "node_modules/once/", + "node_modules/wrappy/", + "node_modules/readable-stream/", + "node_modules/string_decoder/", + "node_modules/safe-buffer/", + "node_modules/ieee754/", + "node_modules/base64-js/", + "node_modules/buffer/", + "node_modules/util-deprecate/", + "node_modules/inherits/", + "node_modules/mkdirp-classic/", + "node_modules/b4a/", + "node_modules/queue-tick/", + "node_modules/streamx/", + "node_modules/fs-constants/", + "node_modules/chownr/", + "node_modules/minimist/", + "node_modules/strip-json-comments/", + "node_modules/deep-extend/", + "node_modules/ini/", + "node_modules/simple-concat/", + "node_modules/simple-get/", + "node_modules/decompress-response/", + "node_modules/mimic-response/", + "node_modules/expand-template/", + "node_modules/github-from-package/", + "node_modules/node-abi/", + "node_modules/napi-build-utils/", + "node_modules/tunnel-agent/", + "node_modules/safe-buffer/", + # @prisma/client 运行时 + "node_modules/@prisma/client/", + "node_modules/@prisma/client-runtime-utils/", + "node_modules/.prisma/", + "node_modules/@prisma/adapter-better-sqlite3/", + "node_modules/@prisma/adapter-libsql/", + "node_modules/@prisma/better-sqlite3/", + "node_modules/@prisma/driver-adapter-utils/", + "node_modules/@prisma/prisma-schema-wasm/", + # 顶层业务入口 + "app.js", + "init-db.cjs", + "bundle.cjs", + "package.json", + "schema.prisma", + "prisma-schema.sqlite.prisma", + "migrations.sqlite/", +) + + +def prune(src: str, dst: str) -> None: + zin = zipfile.ZipFile(src) + zout = zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED, compresslevel=9) + kept = 0 + dropped = 0 + for info in zin.infolist(): + if info.filename.startswith(KEEP_PREFIXES): + zout.writestr(info, zin.read(info.filename)) + kept += 1 + else: + dropped += 1 + zout.close() + import os + print(f"pruned: kept {kept}, dropped {dropped}") + print(f"output: {os.path.getsize(dst) / 1024 / 1024:.1f} MB") + + +if __name__ == "__main__": + prune(sys.argv[1], sys.argv[2]) diff --git a/android/run-on-android.sh b/android/run-on-android.sh new file mode 100644 index 0000000000..4c9cb5b2b8 --- /dev/null +++ b/android/run-on-android.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# ============================================================================ +# AI-Novel-Writing-Assistant — 手机端启动脚本(Termux / nodejs-mobile 通用) +# 作用:在安卓手机的 Node 运行时里启动后端 Express 服务, +# 前端 WebView 只需加载 http://localhost:3000 即可。 +# +# 用法: +# 1) Termux 方案: +# pkg install nodejs +# cd AI-Novel-Writing-Assistant +# bash android/run-on-android.sh +# 2) nodejs-mobile 方案(APK 内置 Node 后由 Java 调用本脚本亦可) +# +# 说明: +# - 后端默认 SQLite(schema.sqlite.prisma),无需 Postgres +# - RAG/Qdrant 默认禁用(不配置 QDRANT_URL 即离线可用) +# - 漫画工坊:安卓端通过 NDK 交叉编译的 bionic `sharp.node` + Termux libvips 启用 +# (WASM 回退会因 emscripten pthreads 死锁不可用,glibc 预编译包 bionic 无法加载)。 +# 见 android/sharp-android/README.md。 +# - RAG/Qdrant 默认禁用(不配置 QDRANT_URL 即离线可用) +# ============================================================================ + +set -euo pipefail + +# --- 根目录(脚本位于 android/ 下,上级即项目根) --- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +# --- 安卓专属数据目录(避免写进项目源码目录,便于备份/清理) --- +if [ -z "${AI_NOVEL_APP_DATA_DIR:-}" ]; then + if [ -n "${HOME:-}" ]; then + export AI_NOVEL_APP_DATA_DIR="$HOME/ai-novel-data" + fi +fi +mkdir -p "${AI_NOVEL_APP_DATA_DIR}" + +# --- 加载安卓环境变量覆盖(若存在) --- +if [ -f "$SCRIPT_DIR/.env.android" ]; then + set -a + . "$SCRIPT_DIR/.env.android" + set +a +fi + +# --- 关键环境变量(可被 .env.android 覆盖) --- +export NODE_ENV="${NODE_ENV:-production}" +export AI_NOVEL_RUNTIME="${AI_NOVEL_RUNTIME:-web}" +export PORT="${PORT:-3000}" +export HOST="${HOST:-localhost}" +export AI_NOVEL_DATABASE_MODE="${AI_NOVEL_DATABASE_MODE:-sqlite}" +export DATABASE_URL="${DATABASE_URL:-file:${AI_NOVEL_APP_DATA_DIR}/ai_novel.db}" + +# 禁用 PC 专属/安卓不可用的能力(RAG 默认关闭,可配 QDRANT_URL 开启) +export AI_NOVEL_DISABLE_RAG="${AI_NOVEL_DISABLE_RAG:-1}" + +# --- 确保 Prisma SQLite client 已生成(首次运行) --- +echo "[android] 生成 Prisma SQLite client(首次稍慢)..." +( cd server && pnpm prisma generate --config prisma.config.ts ) || \ + ( cd server && npx prisma generate --config prisma.config.ts ) + +# --- 应用 SQLite 迁移 --- +echo "[android] 应用数据库迁移..." +( cd server && pnpm exec prisma migrate deploy --config prisma.config.ts ) || \ + echo "[android] 警告:迁移跳过(可能已存在)" + +# --- 启动后端 --- +echo "[android] 启动后端 -> http://localhost:${PORT}" +echo "[android] 数据目录: ${AI_NOVEL_APP_DATA_DIR}" +exec node server/dist/app.js diff --git a/android/sharp-android/README.md b/android/sharp-android/README.md new file mode 100644 index 0000000000..7f69c4ac0b --- /dev/null +++ b/android/sharp-android/README.md @@ -0,0 +1,56 @@ +# 安卓端 sharp(漫画工作台)原生移植说明 + +漫画工作台依赖 `sharp`(基于 libvips)。安卓 APK 内嵌的 node 运行时是 JNI 嵌入版, +无法使用 glibc 预编译包,WASM 回退(`@img/sharp-wasm32`)又会因 emscripten pthreads +死锁卡死事件循环。唯一可行路径是 **NDK 交叉编译 bionic `sharp.node`**,链接 Termux 的 +bionic libvips。 + +## 已验证结论 + +| 方案 | 结果 | +|------|------| +| `@img/sharp-wasm32`(WASM 回退) | ❌ 嵌入 JNI node 加载即死锁,事件循环被同步卡死 | +| `@img/sharp-linux-arm64`(glibc 预编译) | ❌ NEEDED `libc.so.6`/`ld-linux-aarch64.so.1`,bionic 无法加载 | +| **NDK 编译 bionic `sharp.node` + Termux libvips** | ✅ 真机实测渲染成功(`SHARP_OK bytes=96`) | + +## 产物(均已提交到仓库) + +- `android/sharp-android/sharp.node` — 编译好的 AArch64 bionic `sharp.node`(444KB) +- `android/sharp-android/compile-sharp.sh` — 用 NDK clang 编译 `sharp.node` 的脚本 +- `android/sharp-android/pkg-config.py` — Termux libvips 的 pkg-config 包装器 +- `android/sharp-android/assemble-libs.py` — 收集运行时 `.so` 闭包到 `jniLibs_arm64/` +- `android/sharp-android/inject-sharp.sh` — 将 `sharp.node` 注入 `node-project.zip` + +## 运行时依赖(已在 APK 内) + +- `android-app/app/src/main/jniLibs/arm64-v8a/` 下新增 45 个 Termux bionic `.so` + (libvips 及其传递依赖闭包:glib/gio/gobject/cairo/pango/rsvg/OpenEXR/fontconfig/…) +- 这些 `.so` 在 Android linker 的 app lib 目录内互相解析;node 自身依赖 + (`libz.so.1`/`libcares`/`libuv`/`libicu` 等)由 `NodeService` 的 `binDir` 经 + `LD_LIBRARY_PATH` 提供,**不要**把 node 自身 `.so` 放进 `jniLibs`(会冲突导致 + `libnodebin.so` 加载失败)。 + +## 重新构建流程 + +`node-project.zip` / `bundle.cjs` / `www.zip` / `noderuntime/` 已用 git LFS 入库 +(见仓库根 `.gitattributes`),fork 后 clone 即自带,无需重新生成。若需重建 +`node-project.zip`(例如改了 server 依赖),方法如下: + +1. 准备真实扁平的 `node_modules`(pnpm install 后解软链),放回 + `android-app/app/src/main/assets/nodejs/`(含手写 `app.js`/`init-db.cjs` + esbuild 产物 `bundle.cjs`) +2. 将该目录打包为 `node-project.zip` +3. 注入 bionic sharp.node(本目录的 `inject-sharp.sh`,已做 MSYS 路径兼容) + +## 仅重编译 sharp 的流程(sharp.node 损坏或需升级时) + +```bash +# 1) 准备 Termux libvips + sharp 官方 C++ 头文件(一次性,见 compile-sharp.sh 顶部注释) +# 2) 编译 sharp.node +bash android/sharp-android/compile-sharp.sh +# 3) 注入 node-project.zip(LFS 已入库,checkout 后即存在) +bash android/sharp-android/inject-sharp.sh +# 4) 重建 APK +cd android-app && gradle clean assembleDebug +``` + +> 注:`inject-sharp.sh` 依赖已存在的 `node-project.zip` 作为输入(LFS 已入库)。 diff --git a/android/sharp-android/assemble-libs.py b/android/sharp-android/assemble-libs.py new file mode 100644 index 0000000000..72ef974efc --- /dev/null +++ b/android/sharp-android/assemble-libs.py @@ -0,0 +1,64 @@ +import os, zipfile + +# DT_NEEDED from sharp.node (observed) +needed = [ + "libvips-cpp.so.42","libvips.so.42","libglib-2.0.so.0","libgio-2.0.so.0", + "libgobject-2.0.so.0","libgmodule-2.0.so.0","libz.so.1","libexpat.so.1", + "libfftw3.so","libcfitsio.so.10","libimagequant.so","libcgif.so","libexif.so", + "libjpeg.so.8","libpng16.so","libwebp.so","libsharpyuv.so","libwebpmux.so", + "libwebpdemux.so","libpangocairo-1.0.so.0","libpango-1.0.so.0","libfontconfig.so", + "libcairo.so.2","libpangoft2-1.0.so.0","libtiff.so","librsvg-2.so","libcairo-gobject.so.2", + "liblcms2.so","libOpenEXR-3_4.so","libOpenEXRUtil-3_4.so","libOpenEXRCore-3_4.so", + "libIex-3_4.so","libIlmThread-3_4.so","libImath-3_2.so","libopenjph.so","libopenjp2.so", + "libc++_shared.so", +] +LIBDIR="C:/Android/vips/termux/lib" +NDK="C:/Android/sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/windows-x86_64" +OUT="C:/Android/vips/jniLibs_arm64" +os.makedirs(OUT, exist_ok=True) + +# Build a name->candidate map from LIBDIR (match by base name without version) +import re +def base(name): + # strip version suffix like .42 or .0 or -3_4 etc -> match libX.so* + m = re.match(r"^(lib[^.]+\.so)", name) + return m.group(1) if m else name + +avail = {} +for f in os.listdir(LIBDIR): + if f.endswith('.so') or '.so.' in f: + avail.setdefault(base(f), []).append(f) + +missing=[] +copied=0 +for n in needed: + b=base(n) + cands = avail.get(b, []) + # prefer exact match, else first + pick = n if n in cands else (cands[0] if cands else None) + if not pick: + missing.append(n); continue + src=os.path.join(LIBDIR,pick) + dst=os.path.join(OUT, pick) + if not os.path.exists(dst): + import shutil + shutil.copy(src,dst) + copied+=1 + # also copy the unversioned symlink name (libX.so) if exists, for linker + unv = b # libX.so + if unv not in os.listdir(OUT): + for c in cands: + if c==unv: + import shutil; shutil.copy(os.path.join(LIBDIR,c), os.path.join(OUT,unv)) + +# libc++_shared.so from NDK +ndk_lib=os.path.join(NDK,"sysroot","usr","lib","aarch64-linux-android","libc++_shared.so") +if os.path.exists(ndk_lib): + import shutil + shutil.copy(ndk_lib, os.path.join(OUT,"libc++_shared.so")) + print("copied libc++_shared.so from NDK") +else: + print("NDK libc++_shared.so NOT FOUND at", ndk_lib) + +print(f"copied {copied} libs, missing {len(missing)}: {missing}") +print("OUT dir:", OUT) diff --git a/android/sharp-android/compile-sharp.sh b/android/sharp-android/compile-sharp.sh new file mode 100644 index 0000000000..aa57134ad5 --- /dev/null +++ b/android/sharp-android/compile-sharp.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# 编译 sharp 的 .node(bionic,链接 Termux libvips),输出到 C:/Android/vips/sharp.node +set -e +NDK=C:/Android/sdk/ndk/26.3.11579264 +TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/windows-x86_64 +API=24 +CC=$TOOLCHAIN/bin/aarch64-linux-android$API-clang.cmd +CXX=$TOOLCHAIN/bin/aarch64-linux-android$API-clang++.cmd + +SRC=C:/Android/vips/sharp-src/package/src +OUT=C:/Android/vips/sharp.node +INCLUDE_DIRS="-IC:/Android/vips/shdev/package/include -IC:/Android/vips/termux/include -IC:/Android/vips/termux/include/glib-2.0 -IC:/Android/vips/termux/lib/glib-2.0/include" +NODE_INC="-IC:/Android/vips/node-v22.22.3/include/node" +NAA_INC="-IC:/Android/vips/node-addon-api/package" +LIBDIR=C:/Android/vips/termux/lib + +LIBS="-lvips-cpp -lvips -lglib-2.0 -lgio-2.0 -lgobject-2.0 -lgmodule-2.0 -lz -lexpat -lfftw3 -lcfitsio -limagequant -lcgif -lexif -ljpeg -lpng16 -lwebp -lsharpyuv -lwebpmux -lwebpdemux -lpangocairo-1.0 -lpango-1.0 -lfontconfig -lcairo -lpangoft2-1.0 -ltiff -lrsvg-2 -lcairo-gobject -llcms2 -lOpenEXR-3_4 -lOpenEXRUtil-3_4 -lOpenEXRCore-3_4 -lIex-3_4 -lIlmThread-3_4 -lImath-3_2 -lopenjph -lopenjp2" + +DEFINES="-DNAPI_VERSION=9 -DNODE_API_SWALLOW_UNTHROWABLE_EXCEPTIONS -DSHARP_USE_GLOBAL_LIBVIPS -DG_DISABLE_ASSERT -DG_DISABLE_CAST_CHECKS -DG_DISABLE_CHECKS -DNODE_ADDON_API_DISABLE_DEPRECATED -DVIPS_CPLUSPLUS_API=" + +echo "=== 编译 .o ===" +OBJS="" +for f in common metadata stats operations pipeline utilities sharp; do + echo " compiling $f.cc" + $CXX $INCLUDE_DIRS $NODE_INC $NAA_INC $DEFINES \ + -std=c++17 -fexceptions -fPIC -O2 -Wall \ + -c $SRC/$f.cc -o C:/Android/vips/$f.o + OBJS="$OBJS C:/Android/vips/$f.o" +done + +echo "=== 链接 sharp.node ===" +$CXX -shared -o $OUT $OBJS \ + -L$LIBDIR -Wl,--no-as-needed $LIBS \ + -Wl,--disable-new-dtags -Wl,-z,nodelete \ + -fPIC -std=c++17 -fexceptions +echo "=== 完成: $OUT ===" +ls -la $OUT diff --git a/android/sharp-android/inject-sharp.sh b/android/sharp-android/inject-sharp.sh new file mode 100644 index 0000000000..c5a682f969 --- /dev/null +++ b/android/sharp-android/inject-sharp.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# 将 NDK 交叉编译的 bionic sharp.node 注入 node-project.zip, +# 替换原 @img/sharp-wasm32/sharp.node(之前仅为 WASM 回退 shim)。 +# +# 前置: +# - C:/Android/vips/sharp.node 已编译(见 compile-sharp.sh) +# - android-app/app/src/main/assets/nodejs/node-project.zip 已存在 +# +# 用法:bash android/sharp-android/inject-sharp.sh +set -euo pipefail + +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ZIP="$PROJECT_ROOT/android-app/app/src/main/assets/nodejs/node-project.zip" +SRC_NODE="$PROJECT_ROOT/android/sharp-android/sharp.node" +TARGET="node_modules/@img/sharp-wasm32/sharp.node" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +if [ ! -f "$SRC_NODE" ]; then + echo "ERROR: $SRC_NODE 不存在,先运行 compile-sharp.sh 生成 bionic sharp.node" >&2 + exit 1 +fi +if [ ! -f "$ZIP" ]; then + echo "ERROR: $ZIP 不存在" >&2 + exit 1 +fi + +PY="${PY:-C:/Users/Administrator/AppData/Local/Programs/Python/Python311/python.exe}" + +# MSYS:把路径转成 Windows 原生格式,原生 python.exe 才能打开 +if command -v cygpath >/dev/null 2>&1; then + ZIP="$(cygpath -w "$ZIP")" + SRC_NODE="$(cygpath -w "$SRC_NODE")" +fi + +"$PY" - "$ZIP" "$SRC_NODE" "$TARGET" <<'PYEOF' +import sys, zipfile, shutil, os +zip_path, src_node, target = sys.argv[1], sys.argv[2], sys.argv[3] +tmp = os.environ.get('TMPDIR') or os.path.join(os.path.dirname(zip_path), '.inject_tmp') +os.makedirs(tmp, exist_ok=True) +new_zip = os.path.join(tmp, 'np_inject.zip') +data = open(src_node,'rb').read() +zin = zipfile.ZipFile(zip_path,'r') +zout = zipfile.ZipFile(new_zip,'w',zipfile.ZIP_DEFLATED) +replaced = False +for it in zin.infolist(): + if it.filename == target: + zout.writestr(it.filename, data) + replaced = True + print('replaced', target, len(data)) + else: + zout.writestr(it, zin.read(it.filename)) +if not replaced: + # 目标条目不存在则追加(首次注入) + zout.writestr(target, data) + print('appended', target, len(data)) +zin.close(); zout.close() +shutil.move(new_zip, zip_path) +print('updated', zip_path, os.path.getsize(zip_path)) +PYEOF + +echo "[inject-sharp] done: $TARGET <- $SRC_NODE" diff --git a/android/sharp-android/pkg-config.py b/android/sharp-android/pkg-config.py new file mode 100644 index 0000000000..21519fafdc --- /dev/null +++ b/android/sharp-android/pkg-config.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +import sys, os, re + +PC_DIRS = [ + os.environ.get("PKG_CONFIG_PATH", "").split(os.pathsep)[0], + r"C:\Android\vips\termux\lib\pkgconfig", + r"C:/Android/vips/termux/lib/pkgconfig", + r"C:\Android\vips\usr\lib\pkgconfig", +] +PC_DIRS = [d for d in PC_DIRS if d] +SYSROOT = r"C:\Android\sdk\ndk\26.3.11579264\toolchains\llvm\prebuilt\windows-x86_64\sysroot" + +def find_pc(name): + # name may be pkg or pkg.pc + candidates = [name, name + ".pc"] + for d in PC_DIRS: + for c in candidates: + p = os.path.join(d, c) + if os.path.exists(p): + return p + return None + +def parse(pc): + data = {} + with open(pc, encoding="utf-8", errors="ignore") as f: + for line in f: + line = line.split("#")[0].strip() + if not line: + continue + # .pc files mix `key=value` (prefix/includedir) and `key: value` (Name/Cflags/Libs) + if ":" in line and not line.startswith(":"): + k, v = line.split(":", 1) + elif "=" in line: + k, v = line.split("=", 1) + else: + continue + k = k.strip() + v = v.strip() + if k and not k.startswith((" ", "\t")): + data[k] = v + return data + +def subst(val, data): + # simple ${var} substitution + for _ in range(5): + val = re.sub(r"\$\{([\w]+)\}", lambda mm: data.get(mm.group(1), ""), val) + return val + +def resolve(name): + pc = find_pc(name) + if not pc: + return "", "", "" + data = parse(pc) + libs = subst(data.get("Libs", ""), data) + cflags = subst(data.get("Cflags", ""), data) + # inline requires (strip version constraints like "glib-2.0 >= 2.52") + req = data.get("Requires", "") + " " + data.get("Requires.private", "") + req = re.sub(r">\s*=\s*[\d.]+", "", req) # drop >= version + req = re.sub(r"<\s*=\s*[\d.]+", "", req) + req = re.sub(r"[,\s]+", " ", req).strip() + return libs, cflags, req + +def main(): + args = sys.argv[1:] + if "--version" in args: + print("0.29.2-fake") + return + if "--exists" in args: + name = args[-1] + sys.exit(0 if find_pc(name) else 1) + if "--modversion" in args: + name = args[-1] + pc = find_pc(name) + if pc: + d = parse(pc) + print(d.get("Version", "")) + return + if "--cflags" in args: + name = args[-1] + _, cflags, req = resolve(name) + out = cflags + for r in req.split(): + if r: + _, rc, _ = resolve(r) + out += " " + rc + print(out) + return + if "--libs" in args: + name = args[-1] + libs, _, req = resolve(name) + out = libs + for r in req.split(): + if r: + rl, _, _ = resolve(r) + out += " " + rl + print(out) + return + if "--static" in args: + # crude: return libs + cflags + name = args[-1] + libs, cflags, req = resolve(name) + out = cflags + " " + libs + for r in req.split(): + if r: + rl, rc, _ = resolve(r) + out += " " + rc + " " + rl + print(out) + return + # default: print both + name = args[-1] + libs, cflags, req = resolve(name) + out = cflags + " " + libs + for r in req.split(): + if r: + rl, rc, _ = resolve(r) + out += " " + rc + " " + rl + print(out) + +if __name__ == "__main__": + main() diff --git a/android/sharp-android/sharp.node b/android/sharp-android/sharp.node new file mode 100644 index 0000000000..5e43dd4e9d Binary files /dev/null and b/android/sharp-android/sharp.node differ diff --git a/client/src/components/layout/AppLayout.tsx b/client/src/components/layout/AppLayout.tsx index 04c48df22b..b6beb62c2f 100644 --- a/client/src/components/layout/AppLayout.tsx +++ b/client/src/components/layout/AppLayout.tsx @@ -48,8 +48,12 @@ export default function AppLayout() { }, [location.pathname]); const isNovelWorkspace = Boolean(workspaceRoute?.novelId); + // AI 自动导演创建向导:移动端不走站点壳(无底部导航/开书按钮), + // 避免横竖屏切换时同一页面外壳完全不同 + const isMobileBarePage = location.pathname === "/novels/auto-director"; const useMobileNovelWorkspaceLayout = isMobileViewport && isNovelWorkspace; - const useMobileSiteLayout = isMobileViewport && !isNovelWorkspace; + const useMobileSiteLayout = isMobileViewport && !isNovelWorkspace && !isMobileBarePage; + const useMobileBareLayout = isMobileViewport && isMobileBarePage; const useMobileFullWidthContent = useMemo( () => shouldUseAutoDirectorMobileFullWidthContent(location.pathname), [location.pathname], @@ -91,13 +95,32 @@ export default function AppLayout() { ); } + if (useMobileBareLayout) { + return ( + + +
+ + + + }> + + + +
+
+
+ ); + } + if (useMobileNovelWorkspaceLayout) { return (
- + {/* AI 创作实况入口:放右下角,避免与 header 右上角的创作工具菜单重叠 */} + }> diff --git a/client/src/components/layout/Sidebar.tsx b/client/src/components/layout/Sidebar.tsx index 3fa7751963..e564a296d4 100644 --- a/client/src/components/layout/Sidebar.tsx +++ b/client/src/components/layout/Sidebar.tsx @@ -55,7 +55,7 @@ const navGroups: NavGroup[] = [ { to: "/", label: "首页", icon: House }, { to: "/help", label: "创作向导", icon: CircleHelp }, { to: "/novels", label: "小说列表", icon: BookOpenText }, - { to: "/drama", label: "短剧工作台", icon: MonitorPlay, disabled: true }, + { to: "/drama", label: "短剧工作台", icon: MonitorPlay }, { to: "/comic", label: "漫画工作台", icon: SquareStack }, { to: "/creative-hub", label: "创作中枢", icon: LayoutDashboard }, { to: "/book-analysis", label: "拆书", icon: ScanSearch }, diff --git a/client/src/components/layout/mobile/MobileSiteShell.tsx b/client/src/components/layout/mobile/MobileSiteShell.tsx index d215e989f2..562a0f9e4b 100644 --- a/client/src/components/layout/mobile/MobileSiteShell.tsx +++ b/client/src/components/layout/mobile/MobileSiteShell.tsx @@ -159,7 +159,7 @@ export default function MobileSiteShell({ children }: MobileSiteShellProps) {
{primaryNavItems.map((item) => { const Icon = primaryIcons[item.key as MobilePrimaryNavKey]; - const isActive = item.key === "more" ? activeGroup === "more" || moreOpen : activeGroup === item.key; + const isActive = item.key === "more" ? moreOpen : !moreOpen && activeGroup === item.key; return ( +
+
AI 自动导演创建
+
+ 从一个起始想法开始,AI 完成整本规划准备后,再由你选择正文生产方式。 +
+
+

{novelTitle}

+

{statusText}

+
diff --git a/docs/releases/release-notes.md b/docs/releases/release-notes.md index 407f930eaf..0f2c0d8f41 100644 --- a/docs/releases/release-notes.md +++ b/docs/releases/release-notes.md @@ -58,6 +58,24 @@ - 开篇会更快进入压力、异常或冲突,主角拥有明确目标和主动行动;中段持续提供题材匹配的反转与回报,结尾给出清楚落点。 - 全篇检查会识别慢热说明、空泛抒情、推进不足和不适合手机阅读的段落,并在不覆盖人工修改的前提下完成一次必要修整。 +### 2026-08-03(安卓版:fork 可直接构建 + 漫画渲染稳定化 + 0.4.7.2) + +- **v0.4.7.2**:解锁短剧工作台桌面端入口(此前误标"即将推出",功能实际可用);横竖屏按方向切换布局——竖屏为手机布局(底部导航)、横屏为桌面布局(左侧导航),默认竖屏可自由旋转。 +- 安卓端构建资产全部入库(node 运行库 / 依赖包 / 数据库迁移),fork 仓库后按 README 两步即可构建出可运行的 APK,不再需要从发布包提取资产。 +- 漫画工作台渲染改用 WASM sharp(@img/sharp-wasm32),免原生库依赖,真机渲染稳定;APK 体积进一步降至约 163MB。 +- 漫画模块数据库迁移补全(角色性别、场景表、分镜引用列),新安装可直接建库使用。 +- 修复首次启动模型配置弹窗无法录入自定义厂商的问题;修复安卓端网络连接失败的构建参数。 +- WebView 导出功能:小说/资料导出文件写入公共下载目录 `Download/AINovel`,文件管理器直接可见。 +- 手机端统一为桌面布局(横屏固定),操作入口与电脑端一致。 + +### 2026-08-02(安卓本地运行版首个可运行版本,漫画工作台已原生启用) + +- 新增安卓手机本地运行版本:完整的创作工作台可以在手机本地运行,不依赖电脑或云端(模型调用需联网,用户自配模型服务)。 +- 手机端顶部和底部导航的分类与命名和电脑端保持一致:资产、系统、运行记录等入口名称统一。 +- 手机端启动不再白屏等待:启动时显示加载提示,本地服务就绪后自动进入工作台。 +- 手机端顶部状态栏不再遮挡内容,切换横竖屏不会重启工作台,App 图标与电脑端一致。 +- 漫画工作台已在手机端启用:通过 NDK 交叉编译的 bionic `sharp.node` + Termux libvips 原生渲染,替代在嵌入运行时死锁的 WASM 回退,手机端可本地生成分镜图像。 + ### 2026-07-30(创作工作室与短篇完整成稿) - 新增以“先说想法”为起点的创作工作室。AI 会先理解想写的体验,推荐更适合短篇还是长篇,并给出两个差异清楚的方向;确认一个方向即可开始。 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7185f4036b..ea4304c7d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11159,7 +11159,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.3 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.13.6(debug@4.4.3)) + retry-axios: 2.6.0(axios@1.13.6) tough-cookie: 4.1.4 transitivePeerDependencies: - supports-color @@ -12861,7 +12861,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - retry-axios@2.6.0(axios@1.13.6(debug@4.4.3)): + retry-axios@2.6.0(axios@1.13.6): dependencies: axios: 1.13.6(debug@4.4.3) diff --git a/server/src/app.ts b/server/src/app.ts index 6efa2e60c0..50cf9dd9ac 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -141,6 +141,7 @@ export function createApp() { app.use("/api/novel-workflows", novelWorkflowsRouter); app.use("/api/novels", novelExportRouter); app.use("/api/drama", dramaRouter); + // 漫画工坊:安卓端使用 NDK 交叉编译的 bionic libvips + sharp 原生 .node(在 node-project.zip 内) app.use("/api/comic", comicRouter); app.use("/api/worlds", worldRouter); app.use("/api/rag", ragRouter); diff --git a/server/src/prisma/migrations.sqlite/20260803100000_comic_character_gender/migration.sql b/server/src/prisma/migrations.sqlite/20260803100000_comic_character_gender/migration.sql new file mode 100644 index 0000000000..5d2b356530 --- /dev/null +++ b/server/src/prisma/migrations.sqlite/20260803100000_comic_character_gender/migration.sql @@ -0,0 +1,4 @@ +-- 补充 ComicCharacter.gender 列(20260402 的 character_gender_fields 迁移早于 +-- 20260612 add_comic_module 建表,漏掉了该列;此处对齐 schema.prisma) +ALTER TABLE "ComicCharacter" +ADD COLUMN "gender" TEXT NOT NULL DEFAULT 'unknown'; diff --git a/server/src/prisma/migrations.sqlite/20260803110000_comic_missing_tables/migration.sql b/server/src/prisma/migrations.sqlite/20260803110000_comic_missing_tables/migration.sql new file mode 100644 index 0000000000..8f7ede6ab6 --- /dev/null +++ b/server/src/prisma/migrations.sqlite/20260803110000_comic_missing_tables/migration.sql @@ -0,0 +1,35 @@ +-- 补充 ComicCharacterAsset / ComicScene 建表(20260612 add_comic_module 迁移 +-- 漏建这两张表;schema.prisma 已有定义,此处对齐,修复迁移建库环境报 +-- "table does not exist") +CREATE TABLE "ComicCharacterAsset" ( + "id" TEXT NOT NULL PRIMARY KEY, + "characterId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "assetType" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "imageData" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicCharacterAsset_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "ComicCharacter" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "ComicCharacterAsset_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "ComicCharacterAsset_characterId_idx" ON "ComicCharacterAsset"("characterId"); +CREATE INDEX "ComicCharacterAsset_projectId_idx" ON "ComicCharacterAsset"("projectId"); + +CREATE TABLE "ComicScene" ( + "id" TEXT NOT NULL PRIMARY KEY, + "projectId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "sceneType" TEXT NOT NULL DEFAULT 'interior', + "bible" TEXT, + "sheetData" TEXT, + "sortOrder" INTEGER NOT NULL DEFAULT 0, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ComicScene_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "ComicProject" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE INDEX "ComicScene_projectId_idx" ON "ComicScene"("projectId"); diff --git a/server/src/prisma/migrations.sqlite/20260803120000_comic_panel_sceneref/migration.sql b/server/src/prisma/migrations.sqlite/20260803120000_comic_panel_sceneref/migration.sql new file mode 100644 index 0000000000..18361958a8 --- /dev/null +++ b/server/src/prisma/migrations.sqlite/20260803120000_comic_panel_sceneref/migration.sql @@ -0,0 +1,4 @@ +-- 补充 ComicPanel.sceneRef 列(schema.prisma 已定义,但 add_comic_module 建表 +-- 时未包含,后续也无迁移补齐;此处对齐 schema,修复迁移建库环境报 +-- "column does not exist") +ALTER TABLE "ComicPanel" ADD COLUMN "sceneRef" TEXT;