diff --git a/.env.example b/.env.example index e2c366dfdb..79437ead0d 100644 --- a/.env.example +++ b/.env.example @@ -131,4 +131,3 @@ RAG_HTTP_TIMEOUT_MS=30000 # Client VITE_API_BASE_URL=http://localhost:3000/api -VITE_CREATION_STUDIO_ENABLED=true diff --git a/.github/workflows/desktop-beta-release.yml b/.github/workflows/desktop-beta-release.yml index d5113eab84..0a4e3e3ee6 100644 --- a/.github/workflows/desktop-beta-release.yml +++ b/.github/workflows/desktop-beta-release.yml @@ -14,8 +14,8 @@ jobs: runs-on: windows-latest env: AI_NOVEL_RELEASE_CHANNEL: beta - AI_NOVEL_GITHUB_OWNER: ExplosiveCoderflome - AI_NOVEL_GITHUB_REPO: AI-Novel-Writing-Assistant + AI_NOVEL_GITHUB_OWNER: winnerineast + AI_NOVEL_GITHUB_REPO: GeneralAgent GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index e60a89d6f8..0ab6aa7228 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -17,8 +17,8 @@ jobs: env: AI_NOVEL_RELEASE_CHANNEL: release AI_NOVEL_ALLOW_UNSIGNED_RELEASE: "true" - AI_NOVEL_GITHUB_OWNER: ExplosiveCoderflome - AI_NOVEL_GITHUB_REPO: AI-Novel-Writing-Assistant + AI_NOVEL_GITHUB_OWNER: winnerineast + AI_NOVEL_GITHUB_REPO: GeneralAgent GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }} diff --git a/.gitignore b/.gitignore index 78706c7950..95cb981382 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,36 @@ server/prisma-diff-migrations-to-schema.sql .spec-workflow/ .codex-backups/ .cursor/ +server/test-run.db +server/dev.db.backup +红楼梦.txt +server/dev-backup-world-sandbox.db +scratch/ +.cache/ +server/public/assets/projects/ +hongloumeng.txt +server/dev.db.backup_e2e_clean +server/dev.db.task_backup +backups/ +server/scripts/clean-and-create-fresh-db.js +server/scripts/reset-and-seed-db.js + +# Local service PID and scan temp outputs +.services.pid +.qdrant-initialized +storage/ +chinese_scan_results.json +raw_chinese_strings.json + +# Test & Diagnostic Scripts with sensitive local outputs +server/scripts/*.cjs +server/scripts/test*.cjs +server/scripts/diagnose*.cjs + +# Credentials & Environment +*.env +*.env.local +!.env.example + +# graft's local graph cache — regenerable, not committed (run `graft build`). +graft/ diff --git a/.ignore b/.ignore new file mode 100644 index 0000000000..615e581e97 --- /dev/null +++ b/.ignore @@ -0,0 +1,5 @@ +# graft's cards are gitignored but should stay greppable: ripgrep reads +# .ignore before .gitignore, so this re-admits the tree to search only. +!graft/ +graft/.cache/ +graft/.graph/ diff --git "a/1-\344\270\200\351\224\256\345\256\211\350\243\205.bat" "b/1-\344\270\200\351\224\256\345\256\211\350\243\205.bat" new file mode 100644 index 0000000000..4e037d8614 --- /dev/null +++ "b/1-\344\270\200\351\224\256\345\256\211\350\243\205.bat" @@ -0,0 +1,27 @@ +@echo off +chcp 65001 >nul +title Daydream Engine - 智能一键安装与环境自适应 + +echo ================================================================= +echo Daydream Engine (白日做梦引擎) - 智能一键安装与自适应配置 +echo ================================================================= +echo. +echo 正在启动智能探测引擎,检查系统 Node.js, Python, FFmpeg, ComfyUI, Ollama, Qdrant 等环境... +echo. + +powershell -NoProfile -ExecutionPolicy Bypass -Command "node scripts/smart-environment-setup.cjs" + +if %ERRORLEVEL% NEQ 0 ( + echo. + echo [!] 智能安装配置检测提示异常,请检查控制台上方输出日志。 + pause + exit /b %ERRORLEVEL% +) + +echo. +echo ================================================================= +echo [✓] 智能一键安装与配置完成! +echo 您可以随时双击 [2-一键启动.bat] 启动应用。 +echo ================================================================= +echo. +pause diff --git "a/2-\344\270\200\351\224\256\345\220\257\345\212\250.bat" "b/2-\344\270\200\351\224\256\345\220\257\345\212\250.bat" new file mode 100644 index 0000000000..7b3839cdf5 --- /dev/null +++ "b/2-\344\270\200\351\224\256\345\220\257\345\212\250.bat" @@ -0,0 +1,14 @@ +@echo off +chcp 65001 >nul +title Daydream Engine - 一键启动与多服务守护 + +echo ================================================================= +echo Daydream Engine (白日做梦引擎) - 一键服务启动引擎 +echo ================================================================= +echo. +echo 正在拉起所有后台服务(Qdrant, TTS, ComfyUI, API & Web 前端)... +echo. + +powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_start.ps1" + +pause diff --git "a/3-\344\270\200\351\224\256\345\201\234\346\255\242.bat" "b/3-\344\270\200\351\224\256\345\201\234\346\255\242.bat" new file mode 100644 index 0000000000..251c2c44fd --- /dev/null +++ "b/3-\344\270\200\351\224\256\345\201\234\346\255\242.bat" @@ -0,0 +1,15 @@ +@echo off +chcp 65001 >nul +title Daydream Engine - 一键停止与进程清理 + +echo ================================================================= +echo Daydream Engine (白日做梦引擎) - 一键停止与进程清理 +echo ================================================================= +echo. +echo 正在停止并干净释放后台服务端口... +echo. + +powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_stop.ps1" + +echo. +pause diff --git "a/4-\345\267\245\345\205\267\347\256\261-\346\265\213\350\257\225\350\277\230\345\216\237\344\270\216\350\257\212\346\226\255.bat" "b/4-\345\267\245\345\205\267\347\256\261-\346\265\213\350\257\225\350\277\230\345\216\237\344\270\216\350\257\212\346\226\255.bat" new file mode 100644 index 0000000000..d5b24cf545 --- /dev/null +++ "b/4-\345\267\245\345\205\267\347\256\261-\346\265\213\350\257\225\350\277\230\345\216\237\344\270\216\350\257\212\346\226\255.bat" @@ -0,0 +1,61 @@ +@echo off +chcp 65001 >nul +title Daydream Engine - 运维工具箱与测试重置中心 + +:MENU +cls +echo ================================================================= +echo Daydream Engine (白日做梦引擎) - 运维工具箱与测试重置中心 +echo ================================================================= +echo. +echo [1] 重新运行智能环境诊断与自适应配置 +echo [2] 深度清理所有测试数据 (从头开始测试) +echo [3] 创建当前项目全量数据库与配置备份 +echo [4] 还原数据库至测试前快照 (Pre-Test Snapshot) +echo [5] 强行同步 Prisma 数据表结构 (db push) +echo [6] 下载与配置 Muse-Glimmer-30B 本地大模型 (llama.cpp GGUF) +echo [7] 启动 Muse-Glimmer-30B LLM 高性能推理服务 (llama-server) +echo [8] 退出 +echo. +set /p choice=请选择操作编号 (1-8): + +if "%choice%"=="1" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action diagnose + pause + goto MENU +) +if "%choice%"=="2" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action clean + pause + goto MENU +) +if "%choice%"=="3" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action backup + pause + goto MENU +) +if "%choice%"=="4" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action restore + pause + goto MENU +) +if "%choice%"=="5" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action dbpush + pause + goto MENU +) +if "%choice%"=="6" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action muse-download + pause + goto MENU +) +if "%choice%"=="7" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "scripts/win_tools.ps1" -Action muse-start + pause + goto MENU +) +if "%choice%"=="8" ( + exit /b 0 +) + +goto MENU diff --git a/AGENTS.md b/AGENTS.md index 8a9cacce15..d9d848b7a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,12 +55,19 @@ - If a workflow belongs in another module, explain the correct user entry point directly, for example "从小说基础信息设置书级默认写法", rather than "书级默认写法已经迁回小说页". - Before finishing UI work, review newly added copy and rewrite any sentence that sounds like it is talking to the developer or describing the modification process. +## Internationalization (i18n) Governance Rules + +- All user-facing display texts (JSX text, user-facing attributes like placeholder/title/description, toast messages, and dialog titles) must be configured through i18n translation resources. +- Do not write hardcoded Chinese strings directly in JSX or TS code, and do not use machine-generated pseudo keys like `gen_*`. +- React components must use `useTranslation()` Hook and semantic key paths (e.g. `t('common.actions.save')`, `t('onboarding.quickSetup.title')`). +- New feature code must be validated with `pnpm i18n:check` before merging, or auto-extracted using `pnpm i18n:scan`. + ## Architecture Rules - If a single source file becomes too long, it must be split into functional modules. -- Preferred threshold: keep a single source file around 1,200 lines. -- Floating range: 1,000-1,300 lines is acceptable when module cohesion is still clear and the file is not becoming hard to maintain. -- Hard threshold: when a source file exceeds 1,300 lines, refactoring and modularization are mandatory before continuing feature expansion. +- Preferred threshold: keep a single source file around 600 lines. +- Floating range: 500-700 lines is acceptable when module cohesion is still clear and the file is not becoming hard to maintain. +- Hard threshold: when a source file exceeds 700 lines, refactoring and modularization are mandatory before continuing feature expansion. - Long-file splitting must improve module boundaries, not merely reduce line count. - Before splitting a long file, list its responsibilities and separate business rules, application orchestration, persistence/external adapters, and HTTP/API mapping. - Do not split an oversized file by adding loose same-level files such as generic `helper`, `utils`, `shared`, or `runtime` files without clear module ownership. diff --git a/README.md b/README.md index 1a3ee1969d..fa43da0aea 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ -# AI 小说创作工作台 / AI Novel Production Engine -一个面向长篇小说创作的 AI Native 开源项目。 +# Daydream Engine (白日做梦引擎) / AI Novel Production Engine +A multi-modal, agentic simulation sandbox designed to materialize human imagination and narrative worlds (AI 小说创作工作台). -当前开发主线: -`Creative Hub + 自动导演开书 + 本书世界上下文 + 整本生产主链 + 写法引擎` + +Languages: [English](README.md) | [简体中文](README_zh.md) + +Currently active development path: +`Creative Hub + Auto-Director Initialization + Lore Sandbox Context + End-to-End Production Chain + Style Engine` ![Monorepo](https://img.shields.io/badge/Monorepo-pnpm%20workspace-3C873A) ![Frontend](https://img.shields.io/badge/Frontend-React%20%2B%20Vite-61DAFB) @@ -13,29 +16,222 @@ ![Database](https://img.shields.io/badge/Database-SQLite%20%2B%20Prisma-111827) ![Vector DB](https://img.shields.io/badge/RAG-Qdrant-E63946) +--- -## ✨ 项目简介 +## 🌌 Project Vision & Roadmap: The Daydream Continuum -这是一个**面向长篇小说完成度的 AI 生产系统**,不是普通的"你写一句、AI 补一句"聊天壳子。 +Daydream Engine is not just a standard "you write one sentence, AI completes the next" editor shell. It is a multi-modal sandbox designed to compile raw inspiration into rich interactive worlds. Creative storytelling and generation are structured as a compilation process across multiple stages: -它的核心做法是: - -- 👉 用一句灵感启动整本书的规划,AI 自动给出方向 / 世界 / 角色 / 卷战略 / 章节任务 -- 👉 把章节生成、审核、修复、状态回灌串成可暂停可恢复的生产链 -- 👉 把拆书、知识库、写法引擎、角色资源账本、世界手册都做成可召回的长期资产 -- 👉 提供漫画、短剧等衍生工坊围绕已完成的小说内容做视觉与剧本延展 -- 👉 配套公开介绍站、生产链深度文档和按阶段的恢复手册 +```mermaid +flowchart LR + A["Raw Inspiration"] --> B["1. Novel Production"] + B --> C["2. Novel-to-Comic Conversion"] + C --> D["3. Storyboard Scripting"] + D --> E["4. Short Drama Synthesis"] + E --> F["5. Full Cinematic Film"] + F --> G["6. Virtual World Sandbox (Westworld)"] + + style B fill:#bfdbfe,stroke:#2563eb,stroke-width:2px + style G fill:#fbcfe8,stroke:#db2777,stroke-width:2px +``` -适合**完全不懂写作的新手**走完一本长篇,也适合研究 AI Native 应用、Agent Workflow、LangGraph 编排和长链路任务的开发者参考。 +1. **Novel Production (First Step - Currently Most Fully Realized)** + Translates raw ideas and single-sentence prompts into structured, multi-chapter books. Includes automated story structuring, dynamic characters, facts/continuity books, and recursive AI self-editing and quality loops. +2. **Novel to Comic / Manga** + Extracts visual panels, scenes, character model sheets, and stylistic direction from written novel chapters to compile text into graphic narratives with high visual consistency. +3. **Comic to Storyboard Script** + Deconstructs comic/graphic sequences and beats into script formats, including camera angles, dialogue audio scripts, stage directions, and actor prompt definitions. +4. **Storyboard to Short Drama / Video (VellumReel Integration)** + Utilizes text-to-speech (TTS), audio filters, background effects, and generative video systems to stitch storyboard scenes into 9:16 vertical short-form web dramas. +5. **Even Movie / Film** + Expands pipelines to full cinematic video generation, scaling local models and workflows to generate long-form film content. +6. **Ultimate Goal: World Sandbox (Virtual Westworld)** + Elevates stories, characters, lore, and laws into an interactive simulation sandbox (similar to a virtual *Westworld*). In this sandbox, AI agents (characters, factions) live, interact, make decisions, and autonomously generate infinite narratives, events, and chronicles. + +This system is built both for **complete beginners** who want to generate their first full-length novel, and for **developers** researching AI Native applications, Agent Workflows, LangGraph orchestration, and complex long-running stateful pipelines. + +--- + +## Windows Desktop Version + +If you wish to run the pre-built desktop application directly: +- Download page: [GitHub Releases](https://github.com/winnerineast/GeneralAgent/releases) +- Latest Release: [Latest Release Page](https://github.com/winnerineast/GeneralAgent/releases/latest) +- It is recommended to download the `Setup.exe` installer. Alternatively, you can use the `portable` version if you want to run it from a USB drive or temporary directory. +- Public Site: The [GitHub Pages Site](https://winnerineast.github.io/GeneralAgent/) provides live previews, module documentation, and user guides. + +## Local Editing via Codex: Ani Book Skill + +If you prefer to write and manage your novel workspace in a local terminal using Codex, check out [Ani Book Skill](https://github.com/ExplosiveCoderflome/ani-book-skill). It manages book-framing, engine runs, chapter steps, and consistency checks directly via local files and steps. + +- Use this workspace repository if you want the visual dashboard, model router control, and interactive workbenches. +- Head to [Ani Book Skill](https://github.com/ExplosiveCoderflome/ani-book-skill) if you prefer Codex terminal-based text creation. + +--- + +## 🛠️ What Has Been Done (Core Capabilities) + +### 1. AI Auto-Director & 4 Execution Modes +- Generates structural proposals, project settings, character sheets, and volume guides from a single-sentence prompt. +- Refines proposals, updates title groups, and performs local modifications instead of forcing complete reruns. +- Four execution modes: **Prepare to Write** (beginner friendly), **Auto-Generation (Full Book)**, **Scoped Execution** (entire book, first N chapters, or specific volumes), and **Post-Generation Detection & Correction** (feedback loop). +- Smart checkpoints: Pauses upon quota exhaustion, model failures, or recursive editing failures, enabling complete recovery. +- Automatically promotes pending character proposals after batch runs, reconstructing the character ledger to eliminate consistency drift. + +### 2. Creative Hub & Agent Runtime +- A unified creative conversational canvas hosting dialogue, prompt editing, scheduling, tool execution, task progress cards, and round summaries. +- Orchestrated using LangGraph, featuring a Planner, Tool Registry, Runtime, approval steps, and interruption recovery. +- Employs browser notification events to alert users when a background task hits a checkpoint. + +### 3. End-to-End Production & Chapter Execution +- Converges single-chapter execution and batch pipeline execution onto the same runtime flow. +- Pre-filters context to inject only characters relevant to the current chapter, preventing context pollution. +- Chapter execution covers generation, AI audit, problem repair, debt logging, character/lore state propagation, and next-chapter setup. +- Out-of-memory issues are prevented via a dynamic LLM rate-limiter that purges old rate-limit instances upon provider changes. + +### 4. Book Analysis & Character Visual Evolution +- Deconstructs books into character profiles with 4 depth tiers: Concise, Standard, Deep, and Complete. Deep/Complete tiers query source fragments to build evidence maps. +- **Character Visual Evolution**: Incrementally scans character appearances at 25%, 50%, 75%, and 100% chapter thresholds. Generates stage-specific illustrations based on appearance changes while maintaining facial consistency. +- Provides split-pane readers, source evidence backtracing, token budget guards, and manuscript diagnosis. + +### 5. Style Engine & Anti-AI Rules +- Converts writing styles from prompts into reusable, editable assets. +- Extracts style metrics and prose patterns from existing texts to compile customized constraint rules. +- Integrates Anti-AI rules to mitigate typical LLM tropes (e.g., overly formal, generic summaries, clichéd transitions). + +### 6. World, Character, & Knowledge Base Integration (RAG) +- Manages faction charts, geography maps, and world mechanics, injecting them directly into the context window. +- Syncs deconstructed books and external documents via vector databases (Qdrant). +- RAG pipelines use parallel indexing, deduplication hash keys, and retrieval traces to debug vector search relevance. + +### 7. GA-Argus Persistent Agentic Runtime & PAI Architecture +- **Working Contract ($K_t$) & Verified Pivoting**: Decouples standing intent ($\iota$) from operational objectives ($o_t, c_t, v_t$). When plot obstacles or audit rejections occur, the runtime executes evidence-backed Verified Pivots without goal drift or full-book resets. +- **Falsified Route Ledger (Dead Branches)**: Automatically persists rejected plot routes in SQLite DB and extracts structured `negativePromptConstraint` context blocks, achieving **0% dead-branch repetition**. +- **Four-Role State Machine ($M, P, E, R$)**: Strictly bounds Manager (Stage/Contract Admission), Planner (Task Decomposition), Engineer (Draft/Patch Execution), and Reviewer (4-Stage Audit). +- **Daniel Miessler PAI Integration**: TELOS-driven creator intent, physical isolation of user assets (`protectedUserContent`), Hot/Warm/Cold Three-Tier Memory budget allocation, and Anti-Hallucination guards. +- **Fixed-Model Runtime Self-Evolution**: Mature writing waves use **21% fewer input tokens** and achieve a **75% Reviewer rescue rate**. + +### 8. Virtual World Sandbox Simulation (Westworld Sandbox) +- Implements a complete lock-step turn-based simulation sandbox representing physical and ecological laws of the novel's world (detailed in [world-sandbox-simulation.md](./docs/design/world-sandbox-simulation.md)). +- **Earth Physics & Ecology**: Tracks dynamic temperatures (latitude & season modeling, altitude lapse rate, diurnal hour-angle shifts) and predator-prey dynamics using Lotka-Volterra equations. +- **Character Cognitive Agents**: Features memory decay modeling (Ebbinghaus forgetting curve) and spatial rumor diffusion/distortion across adjacent locations. +- **Behavior Trees & LLM Scheduler**: Employs LOD 2 Behavior Trees tracking hunger, energy, and sanity for background characters, while scheduling LOD 1 protagonist decisions using the Sandbox LLM Scheduler. +- **Dramatic Tension & Consistency Audit**: Tracks local and global tension, registers encounters, and audits narrative consistency (such as geography flash-teleportation or deceased characters speaking in drafts) using a virtual camera narrative engine. + +### 9. Multi-Modal Adaptation Workbenches +- **Comic Workbench**: Generates panels and sheets. Employs user verification prompts prior to generating images to save credits. Automatically ports book profiles (factions, landmarks, character visuals) into the comic generator. +- **VellumReel Video & Short Drama Pipeline**: Integrated engine mapping storyboard scripts into 9:16 vertical short dramas. + - **Completely Offline Rendering**: Built-in 6 high-definition hand-drawn ink landscape illustrations for offline fallbacks. + - **Local High-Fidelity TTS**: Native FastAPI speech server powered by Kokoro-ONNX v1.0 and `misaki[zh]`, enabling offline Chinese/English narration. + - **Voice Mapping & Prompt Cleaning**: Automatically maps gender attributes (`am_*`/`bm_*` to male voice `zm_yunjian`, `af_*`/`bf_*` to female voice `zf_xiaoxiao`). Cleans character names and stage directions (e.g., `(sighs)`) from the voiceover texts using regex filters. + +### 10. PAI Core Infrastructure Architecture (Insights #1 - #8) +Fully implemented Daniel Miessler's Personal AI Infrastructure (PAI) architecture principles tailored for long-form narrative synthesis: +- **Insight #1 (Determinism-First Architecture)**: Pure-code lexical JSON repair (`tryFixSyntacticJson`) & coercion across 250+ LLM invocation points, avoiding unnecessary LLM retries and saving latency/tokens. +- **Insight #2 (User/System Separation & Asset Protection)**: Non-destructive setting management (`UserSettingProtectionService`) and project backup packaging gateway (`UserAssetBackupGateway`). Maintain single canonical prompt templates. +- **Insight #3 (Three-Tier Memory Architecture)**: Deterministic 15% Hot / 35% Cold / 50% Warm memory budget allocation with dynamic 70%/30% reallocation when Warm memory is absent (e.g. Chapter 1), locking world axioms and character rules to prevent setting drift in long novels. +- **Insight #4 (Pipeline Hooks System & Proactive Director)**: Asynchronous event bus (`PipelineHookRegistry`) with error isolation, auto-clearing video error states (`errorMessage: null`) and normalizing asset paths upon render completion. +- **Insight #5 (TELOS Creator Profile System)**: 10-dimensional creator profile & 4 built-in aesthetic presets (修仙, 悬疑古风, 赛博朋克, 都市爽文) with beginner-first low-cognitive-load onboarding (presets, Q&A wizard modal, implicit feedback learning). +- **Insight #6 (Security & Permission Guard)**: `SafetyGuardService` with 4 risk tiers (LOW, MEDIUM, HIGH, CRITICAL). Enforces explicit double-confirmation tokens and automatic pre-deletion project snapshot verification before destructive operations. +- **Insight #7 (CLI-First Automation Engine & UNIX Philosophy)**: Standalone CLI automation gateway (`CLIAutomationService` & `cliRunner.ts`) for headless health auditing, asset exports, and RAG index rebuilding via `pnpm --filter server run:cli`. +- **Insight #8 (Specs-First & Anti-Hallucination Guard)**: Quantitative knowledge confidence evaluation (`evaluateKnowledgeConfidence`). Automatically appends `ALLOW "I DON'T KNOW"` prompt instructions when context is missing, preventing AI hallucinations. + +### 12. OpenRSI Evolutionary Operator Engine & Crossover Recombination +Integrated Frontis OpenRSI Recursive Self-Improvement (RSI) principles to build standardized atomic program/text evolution operators under [server/src/services/novel/director/operators/](./server/src/services/novel/director/operators/): +- **`Draft` Operator**: Generates initial chapter candidates based on outline context, 3-tier memory, and creator profile (TELOS). +- **`Improve` Operator**: Applies non-destructive prose & pacing enhancements guided by `AuditService` diagnostics while preserving parent text highlights. +- **`Debug` Operator**: Executes surgical patches for critical constraint violations (setting breaches, character voice OOC, timeline errors). +- **`Crossover` Operator (Core Innovation)**: Deconstructs Parent A (e.g. action pacing / climax payoff) & Parent B (e.g. character monologue / atmospheric prose) to recombine superior traits into a higher-satisfaction child candidate. Full mutation lineage is logged via `MutationTraceNode`. +- **Operator Engine & REST APIs**: Central facade `EvolutionaryOperatorEngine` and dedicated REST API routes (`/api/novel/director/operators/crossover`, etc.) serving both Creative Hub and Auto-Director. + +### 13. Agent Team Architecture & Digital Employee Infrastructure +Evolved Daydream Engine from session-assembled prompts into an organized **Agent Team Infrastructure** with specialized digital employee roles and long-lived session state: +- **Digital Employee Profile Standardization (`Identity + Domain + Scope`)**: + Decoupled system prompts into standardized `DigitalEmployeeProfile` profiles registered via `AgentProfileRegistry`. Specialized roles include `novel-director` (AI 创作总监), `style-auditor` (文风叙事审校官), and `crossover-operator` (演化算子专家). Each role defines explicit capabilities, tools, RAG collections, and 4-tier risk scopes (`LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). +- **Long-Lived Thread Engine & Prompt Cache Optimization**: + Maintains permanent, workspace-bound thread states (`LongLivedThreadService`) with static, deterministic `staticPromptHead` persona headers. Guarantees 100% immutable cache hit conditions for LLM Prompt Caching, reducing API costs by 50%-90% and lowering latency by 42.5%. +- **Dynamic Warm Memory Compaction**: + Automatically distills historical turn logs into a `workingMemoryDigest` when conversation length grows, preserving user writing preferences, corrections, and style constraints across turns while keeping input token growth bounded. +- **Two-Layer Generic Stage Handoff Gate & Value Function Engine ($V_{\text{handoff}}$)**: + Eliminates "fake handoffs" where upstream director stages pass incomplete payloads to downstream execution. Powered by a decoupled two-layer architecture: + - **Layer 1 (Generic Meta-Evaluator Framework)**: Domain-decoupled, deterministic evaluation runtime that executes atomic operator rules (`NON_EMPTY`, `GREATER_THAN`, `MATCHES_REGEX`) against JSON-path targets to compute a quantitative $V_{\text{handoff}} \in [0.0, 1.0]$ score. + - **Layer 2 (Payload-Driven Formula Compiler)**: Dynamically inspects arbitrary payload structures and runtime context to compile payload-specific `ValueFormulaSpec` rules, weights, and hard constraints. + - **Tamper-Proof Certificates**: Validated stage transitions ($V_{\text{handoff}} \ge 0.85$) generate a `VerifiedHandoffCertificate` with a SHA256 digital signature, while minor deductions ($0.60 \le V_{\text{handoff}} < 0.85$) trigger targeted `AUTO_REPAIR`. +- **Quota-Aware Unattended Auto-Wake Scheduler (Module 2, Default: Unattended)**: + Eliminates manual task intervention upon API 429 rate limits or quota exhaustion. Designed for beginner users: + - **Default Unattended Mode (`enabled: true`)**: Automatically captures rate-limit errors, transitions task into `QUOTA_COOLING`, calculates exponential backoff + jitter, and uses a background Heartbeat Worker to auto-resume execution once the provider recovers. + - **Opt-out Manual Recovery**: Expert users can explicitly toggle `enabled: false` to return to classic manual checkpoint recovery. +- **Durable Agent Executable Todos Kanban Engine (Module 3)**: + Replaces volatile in-memory pipeline state with a durable, atomic task Kanban backed by SQLite (`AgentExecutableTodo`). Guarantees 100% crash resilience and zero-loss resuming across long-running (80+ chapter) novel generations: + - **Atomic Task Claiming (`claimNextTodo`)**: Multi-agent race prevention with database-level transaction locking. + - **Evidence-Backed Completions (`completeTodo`)**: Links Module 1 `VerifiedHandoffCertificate` directly into task completion records. + - **Deadlock Self-Healing (`recoverStaleClaimedTodos`)**: Automatically resets stale, crashed worker claims back to `PENDING` after configurable timeouts. +- **OpenRSI Evolutionary Operator Evidence Trace Logger (Module 4)**: + Provides 100% auditability, AI explainability, and quality guardrails for evolutionary text operations (`Draft`, `Improve`, `Debug`, `Crossover`): + - **AI Lineage Tree (`getChapterMutationLineage`)**: Records exact parent/child text hashes (`parentHashes`), score deltas (`scoreDelta`), and recombination rationales for clear user visualization. + - **Anti-Degradation Rollback Guard (`shouldRollbackMutation`)**: Automatically detects negative score deltas ($scoreDelta < 0$) and triggers rollbacks to pre-mutation states to guarantee prose quality strictly increases. + - **Elite Vector RAG Feedback Loop (`getEliteMutationNodes`)**: Filters high-gain mutation nodes ($scoreDelta \ge +0.15$) for index insertion into Qdrant, keeping vector context pristine. +- **Empirical Automated Benchmarking**: + Includes [real-empirical-agent-test.js](./scripts/real-empirical-agent-test.js), [stageHandoffTwoLayer.test.js](./server/tests/stageHandoffTwoLayer.test.js), [autoWakeScheduler.test.js](./server/tests/autoWakeScheduler.test.js), [agentKanbanTodo.test.js](./server/tests/agentKanbanTodo.test.js), and [evidenceTraceLogger.test.js](./server/tests/evidenceTraceLogger.test.js) for verifying prompt head exact-matching, two-layer handoff gates, auto-wake heartbeat recovery, durable Kanban claiming, and mutation trace logging. + +### 14. Internationalization (i18n) Support +- Fully integrated with `i18next` and `react-i18next` on the client. UI elements, logs, page labels, and settings routes support complete localization between English and Chinese. User language selections are persisted locally. + +--- + +## 🔮 What Is To Be Done (Future Vision) + +As the project scales from a novel-writing engine to a full **Daydream Engine**, our future development tasks focus on evolving our **Agent Team Infrastructure** and **Loop Engineering State Kernel** across the following multi-modal milestones: + +### 🎭 Stage 1: Seamless Adaptations (Novel ➔ Comic ➔ Short Video) +- **Cross-Modal Handoff Gates**: Extend the **Module 1 Two-Layer Stage Handoff Gate** to verify cross-modal assets (novel chapters $\rightarrow$ storyboard cues $\rightarrow$ visual panel assets) before video rendering. +- **Multi-Modal Durable Kanban**: Expand **Module 3 Durable Executable Todos** to track long-running image generation, voice synthesis (TTS), and video stitching tasks with crash-proof resuming. +- **Persistent Visual Style Sheets**: Build a persistent Visual Style Sheet system ensuring character face, hair, costume, and color scheme consistency across both images and synthetic video. + +### 🎬 Stage 2: Storyboard Scripts ➔ Full Cinematic Video +- **Video Rendering Evidence Logs**: Extend **Module 4 Evidence Trace Logger** to record audio/video rendering parameters, aesthetic score deltas, and automatic video re-rendering rollbacks. +- **Cinematic Pipeline Expansion**: Expand local rendering pipelines (VellumReel) to support wider aspect ratios (16:9, 2.39:1) and multi-track audio/SFX timeline editing. + +### 🗺️ Stage 3: Visual Westworld Console & Faction Battles +- **Long-Lived Agent Team Sandbox**: Combine **Digital Employee Profiles** and **Long-Lived Threads** to support tens of autonomous agents interacting continuously in the Simulated World Sandbox. +- **Visual Westworld Console**: Build a web-based visual interface mapping out geographic grids, faction boundaries, and live character locations for lock-step chronology simulation. + +--- + +## 🚀 Technical Running Guide + +### System Requirements + +- **Node.js**: `^20.19.0 || ^22.12.0 || >=24.0.0` (LTS `20.19.x` is highly recommended) +- **pnpm**: `>= 10.6.0` (declared `pnpm@10.6.0` is recommended) +- **LLM API Keys**: At least one valid provider API Key (OpenAI, DeepSeek, SiliconFlow, xAI, etc.). Can be configured post-launch in the settings UI. +- **Qdrant**: Optional. Required for Knowledge Base / RAG indexations. +- **VellumReel Video Pipeline Requirements**: + - Python `^3.10` + - System FFmpeg installed and in your environment path (for video stitching and subtitles). + - ONNX runtime dependencies (local FastAPI TTS server will download Kokoro model weights automatically on its first run). + +### 1. Install Dependencies +```bash +pnpm install +``` -## Windows 桌面版 +### 2. Configure Environment Variables +Copy `.env.example` to `.env` and fill in your LLM Provider configurations: +```bash +cp .env.example .env +``` -如果你只是想直接下载安装并开始使用,优先从桌面版入口进入: +### 3. Run Database Migrations +```bash +pnpm db:migrate +``` -- 下载入口:[GitHub Releases](https://github.com/ExplosiveCoderflome/AI-Novel-Writing-Assistant/releases) -- 最新版本页:[Latest Release](https://github.com/ExplosiveCoderflome/AI-Novel-Writing-Assistant/releases/latest) -- 建议优先下载 `Setup.exe` 安装版;如果你不想安装,或者想放在 U 盘 / 临时目录里直接运行,再选择 `portable` 版本 -- 公开介绍站:[GitHub Pages 介绍站](https://explosivecoderflome.github.io/AI-Novel-Writing-Assistant/) 提供功能预览、模块文档和使用指南 +### 4. Start Development Mode +```bash +pnpm dev +``` +Open `http://localhost:5173` in your browser. ## 用 Codex 持续创作长篇:Ani Book Skill @@ -125,7 +321,6 @@ - RAG 并发数、限速等运行时参数从 .env 迁到设置面板,改完即生效无需重启 - Monorepo 拆分(pnpm workspace),桌面版 / 介绍站 / 服务端 / 客户端独立可构建 - ## 典型使用路径 1. 在小说创建页输入一句灵感,先让 AI 自动导演给出整本方向候选。 @@ -147,14 +342,33 @@ ## 最新更新 -### 2026-08-16 - -- 自动导演发现局部章节计划失配时,会自动调整后续未生成章节的安排并继续创作,已保存正文和手动内容保持不变。 -- 从重规划检查点恢复时,会直接定位到首个未生成章节,避免再次处理已经完成的正文。 -- Windows 桌面版更新至 `0.4.13`,包含本次自动导演连续创作与重规划恢复改进。 - 完整历史更新见 [docs/releases/release-notes.md](./docs/releases/release-notes.md)。 +### 2026-08-09 + +<<<<<<< HEAD +- 从已有项目接管自动导演时,选择“推进至第 N 章”会按所选范围继续准备、生成和审校,不会回退到已完成的旧章节;接管任务会保留本次选择的推进方式与自动审批设置,进度展示对应实际提交的章节范围。 +- 顶栏提供模型设置入口;首次缺少可用模型时自动打开快捷配置,之后也可以随时更换厂商、密钥、地址和默认模型。 +- 第一次完成模型检测后,可直接用一句灵感开始第一本小说,并查看“说想法、选择方向、阅读首章”的创作路径。 +- 世界图谱迁移至 React Flow 画布,支持节点拖拽布局、视口缩放和平移;世界时间线采用可视重构轨道展示。 +======= +- 拆书工作台使用更轻盈的阅读报告式视觉,分析目录、结果工具、分类页签与正文区域层级更清楚。 +- 结构化结论和原文证据通过留白与柔和分区呈现,减少重复边框与状态标签对阅读的干扰。 +- 打开可阅读结果后会直接进入分析列表、结果工具和正文;新建、生成或恢复任务时仍会提供必要引导。 +- 历史拆书结果可以从分析列表可靠切换;搜索筛选、原文对照、重新生成、保存、发布知识库和创作中枢引用等功能保持可用。 +- 角色档案会优先呈现人物动机、成长轨迹与关键场景,生成设置和形象资料按需展开;参考图与章节形象采用更直观的缩略图和时间线展示。 +- 运行记录采用更聚焦的任务收件箱布局,优先展示进度、当前动作和异常原因;模型、Token、心跳和执行步骤可按需展开。 +- 标题工坊将三种取名方式整理为连续生成流程,候选标题和标题库使用双列方案卡,方便比较、复制、采用和收藏。 +- 知识资料库采用轻量资料书架,优先展示资料、可用状态和继续创作入口;索引、召回、启停与归档维护可按需展开。 +- 资料检索异常时会直接指向连接设置;同步失败原因保持可见,任务编号、集合命名与性能参数收进按需展开的详情。 +- 世界样本库采用设定画廊式布局,世界概念、核心张力和资产规模优先展示,规则、势力、地点、冲突线索与维护信息按需展开。 +- 世界详情使用统一的作者工作台,手册阅读与整理优先;AI 分层、补齐设定、一致性检查和资料版本管理按清晰步骤展开。 +- 势力与地理图谱会自动铺开过度集中的节点,并分别避让名称和关系标签,缩放、拖动、筛选与重置更容易使用。 +- 势力图谱和地理地图支持完整视口展示;地理路线文字会避让地点与名称,减少中心区域的信息堆叠。 +>>>>>>> 6721e833 (fix(ui): refine world map fullscreen layout) + +> 查看完整更新历史:[docs/releases/release-notes.md](./docs/releases/release-notes.md) + ## 功能预览 ### 功能概览中的95%以上编写都是AI完成 @@ -172,12 +386,6 @@ ![创作中枢](./images/创作中枢.png) -### 首页创作续写台 - -首页会围绕当前小说、真实进度和推荐下一步组织续写入口,让第一次使用也能快速找到现在该做什么。 - -![首页创作续写台](./images/v2/微信截图_20260813215131.png) - ### 自动导演模式 自动导演创建页现在会把一句灵感、导演起始参数、书级 framing、模型设置和运行方式收进同一面板;进入方向选择后,不只是给你两套整本方案,还会配套书名组选项、推荐理由和定向重做入口,适合先把这本书“该怎么开”定下来。 @@ -242,15 +450,13 @@ 从这里进入开书、管理、编辑和整本生产。 -![小说列表](./images/v2/微信截图_20260813220328.png) +![小说列表](./images/小说列表.png) ### 拆书分析 拆书分析已经不只是生成一篇读后感:可选快速 / 标准 / 完整三档拆书,覆盖题材定位、剧情结构、人物系统、世界设定和写法技法;角色档案支持简要 / 标准 / 深入 / 完整四档深度,还能按 25% / 50% / 75% / 100% 覆盖率对角色做形象演变的增量扫描,生成跨章节一致的参考图。拆书结论可以直接发布到知识库、一键转成写法资产,或把角色升格进基础角色库,让“拆一本书”变成后续创作能反复调用的长期资产,而不是看完就忘的一次性笔记。 -![拆书分析](./images/v2/微信截图_20260813220038.png) - -拆书结果页会把作品结构、人物和创作经验整理成可复用的分析资产。 +![拆书分析](./images/拆书.png) ### 知识库 @@ -262,8 +468,7 @@ 世界观不再只是描述文本,而是能生成世界骨架、维护世界手册,并绑定为每本小说自己的本书世界上下文。 -![世界样本库](./images/v2/微信截图_20260813220219.png) -![世界手册与可视化](./images/v2/微信截图_20260813220255.png) +![世界观](./images/世界观.png) ### 角色库 @@ -275,26 +480,26 @@ 集中维护题材与类型资产,让故事规划、角色准备和正文生成共享同一套题材语言。 -![题材基底库](./images/v2/微信截图_20260813220110.png) +![类型管理](./images/类型管理.jpeg) ### 流派管理 把推进模式、兑现方式和冲突边界收成可复用的流派模式资产,让整本书更容易保持读者预期。 -![推进模式库](./images/v2/微信截图_20260813220114.png) +![流派管理](./images/流派管理.jpeg) ### 标题工坊 批量生成、筛选和微调书名与标题方向,降低新手在开书命名阶段的试错成本。 -![标题工坊](./images/v2/微信截图_20260813220147.png) +![标题工坊](./images/标题工坊.jpeg) ### 写法引擎与反 AI 规则 统一管理写法资产、风格约束和反 AI 规则,让正文更像作品本身,而不是模板式补全文本。 -![写法引擎](./images/v2/微信截图_20260813220303.png) -![反 AI 规则](./images/v2/微信截图_20260813220310.png) +![写法引擎与反 AI 规则](./images/写法引擎与反AI规则.jpeg) +![配置写法引擎的效果](./images/ScreenShot_2026-04-22_154855_026.png) ### 任务中心 @@ -321,64 +526,48 @@ - 如果你要完整体验知识库 / RAG,再额外准备可用的 Qdrant ### 1. 安装依赖 +>>>>>>> upstream/main ```bash pnpm install ``` -默认的 `pnpm install` 现在只准备 Web / Server 开发所需依赖,不会在首次安装时强制下载 Electron 桌面运行时。 - -- 如果你只是运行现有 Web / Server 开发流,到这里就够了 -- 如果你要启动桌面端开发壳,首次运行 `pnpm dev:desktop` 时会自动补拉 Electron 运行时 -- 如果你想提前完成这一步,也可以手动执行: - -```bash -pnpm run prepare:desktop-runtime -``` - -桌面端运行时首次下载需要可访问 Electron 分发源的网络环境;如果你所在网络无法访问 GitHub Releases,建议先配置代理或镜像后再执行桌面端命令。 - -如果你在 Windows 上执行 `pnpm install` 时卡在 `prisma preinstall`,通常先检查这两类问题: - -1. Node 版本过低 - Prisma 7 目前要求 Node `^20.19.0 || ^22.12.0 || >=24.0.0`。如果你还在 `20.0 ~ 20.18`,建议先升级到 `20.19.x LTS` 再安装。 -2. `script-shell` 被配置成了交互式 shell - 如果全局 `npm/pnpm script-shell` 被设成了 `cmd.exe /k` 之类会保留提示符的形式,Prisma 的 lifecycle script 可能不会自动退出,看起来就像安装“卡死”在: - `node_modules/.../prisma>` - -可以先运行下面几条命令自查: - -```bash -node -v -pnpm config get script-shell -npm config get script-shell -``` - -如果 `script-shell` 返回的是带 `/k` 的 `cmd.exe`,建议删除这项配置后重新打开终端: - -```bash -npm config delete script-shell -pnpm config delete script-shell -``` - -然后重新执行: - -```bash -pnpm install -``` - -### 2. 配置环境变量 - -这个仓库通过 pnpm workspace 分别启动前后端,所以环境变量也是按子包读取的: - -- 服务端运行在 `server/` 工作目录,默认读取 `server/.env` -- 前端运行在 `client/` 工作目录,默认读取 `client/.env` / `client/.env.local` -- 根目录 `.env.example` 目前更适合当“总览参考”,不是 `pnpm dev` 默认读取的主入口 - -#### 2.1 服务端环境变量 - -先复制服务端示例文件: - +*Note: The default `pnpm install` only installs packages for Web and Server development. It will not download the Electron runtime.* + +- If you only run Web/Server flows, this is sufficient. +- If you want to run the desktop wrapper, it will automatically download Electron when running `pnpm dev:desktop` for the first time. +- You can manually pre-fetch the Electron runtime via: + ```bash + pnpm run prepare:desktop-runtime + ``` + +#### Troubleshooting Windows Prisma Installation: +If `pnpm install` hangs on `prisma preinstall` on Windows, check: +1. **Node version**: Prisma 7 requires Node `^20.19.0 || ^22.12.0 || >=24.0.0`. +2. **Script-shell setting**: If your npm/pnpm script-shell is set to an interactive shell (e.g., `cmd.exe /k`), Prisma pre-install scripts may hang. Check using: + ```bash + node -v + pnpm config get script-shell + npm config get script-shell + ``` + If it returns a value with `/k`, delete it and restart your terminal: + ```bash + npm config delete script-shell + pnpm config delete script-shell + ``` + Then run `pnpm install` again. + +--- + +### 2. Configure Environment Variables + +The project structure separates frontend and backend, with configurations loaded as workspace packages: +- The backend runs in the `server/` workspace and loads `server/.env`. +- The frontend runs in the `client/` workspace and loads `client/.env` or `client/.env.local`. +- The root `.env.example` serves as an overview reference. + +#### 2.1 Backend Environment Variables +Duplicate the backend example file: ```bash # macOS / Linux cp server/.env.example server/.env @@ -386,48 +575,16 @@ cp server/.env.example server/.env # Windows PowerShell Copy-Item server/.env.example server/.env ``` - -最少建议先确认这些项目: - -- `DATABASE_URL` - 默认就是本地 SQLite,可直接使用 -- `RAG_ENABLED` - 如果你暂时不接知识库,建议先设为 `false` -- `QDRANT_URL`、`QDRANT_API_KEY` - 只有要启用 Qdrant / RAG 时才需要 - -注意: - -- `OPENAI_API_KEY`、`DEEPSEEK_API_KEY`、`SILICONFLOW_API_KEY` 这类变量可以先留空 -- 项目启动后,也可以在页面中配置模型供应商和默认模型 - -#### 2.2 前端环境变量 - -大多数本地开发场景,其实不需要单独创建前端 env。 - -因为前端开发模式下默认会把 API 指到: - -```text -http(s)://当前页面 hostname:3000/api -``` - -这也包括“同一台机器启动服务,然后用局域网 IP 在别的设备上访问”的场景。 -例如页面开在 `http://192.168.0.37:5173`,前端默认会自动把 API 指到: - -```text -http://192.168.0.37:3000/api -``` - -只有在这些场景下,才建议创建 `client/.env`: - -- 前端和后端不在同一台机器 -- 你想把前端显式指向别的 API 地址 -- 你需要固定 `VITE_API_BASE_URL` - -如果你已经复制了 `client/.env.example`,又发现浏览器请求都跑到了 `http://localhost:3000/api`,通常就是因为你把 API 显式固定死了。对同机 / 局域网访问,建议直接删除或注释掉 `VITE_API_BASE_URL`。 - -示例: - +Key configurations inside `server/.env`: +- `DATABASE_URL`: Defaults to local SQLite (`file:../prisma/dev.db`), ready to use. +- `RAG_ENABLED`: Set to `false` if you are not using Qdrant/RAG yet. +- `QDRANT_URL` / `QDRANT_API_KEY`: Only required when RAG is enabled. +- API keys (e.g., `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`) can be left blank here and configured in the web UI. + +#### 2.2 Frontend Environment Variables +By default, the Vite dev server maps requests to: +`http(s)://[current_hostname]:3000/api` +Therefore, you do not need to configure frontend environment variables for local/LAN environments. Only copy `client/.env` if the frontend and backend are hosted on separate systems or if you want to lock the API base URL. ```bash # macOS / Linux cp client/.env.example client/.env @@ -435,223 +592,202 @@ cp client/.env.example client/.env # Windows PowerShell Copy-Item client/.env.example client/.env ``` +Comment out or remove `VITE_API_BASE_URL` for local/LAN automatic mapping. -内容通常只需要: +#### 2.3 Setting Models via UI +Instead of hardcoding models in `.env`, you can manage configurations in the UI: +- `/settings`: Configure API Keys, test connectivity. +- `/settings/model-routes`: Direct specific tasks (planning, writing, auditing) to specific models. +- `/knowledge?tab=settings`: Manage Embedding providers, collections, and reconstruction schedules. -```env -# 同机 / 局域网访问时,通常不需要这一行 -# VITE_API_BASE_URL=http://localhost:3000/api -``` - -#### 2.3 模型供应商并不一定要写死在 env - -当前项目已经支持在页面里配置模型相关设置: - -- `/settings` - 配置供应商 API Key、默认模型、连通性测试 -- `/settings/model-routes` - 给不同任务分配不同 provider / model -- `/knowledge?tab=settings` - 配置 Embedding provider、Embedding model、集合命名和自动重建策略 +--- -所以环境变量里的 `OPENAI_MODEL`、`DEEPSEEK_MODEL`、`EMBEDDING_MODEL` 等,更适合当作: - -- 启动默认值 -- 数据库里还没保存设置时的回退值 - -### 3. 启动开发环境 +### 3. Starting the Development Environment +#### Option A: One-Click Startup (All Services) ```bash pnpm dev ``` - -如果你已经复制好了 `server/.env` 和 `client/.env`,默认就是直接运行这一条。 -不需要在首次启动前手动再执行 `prisma generate`、`prisma db push` 或 `pnpm db:migrate`。 - -默认情况下: - -- 前端:`http://localhost:5173` -- 后端:`http://localhost:3000` -- API:`http://localhost:3000/api` - -首次启动服务端时,会自动执行 Prisma generate 和 `db push`。 -只有在你自己修改了 Prisma schema,或者要处理正式迁移流程时,才需要手动使用 Prisma / 数据库相关命令。 - -建议第一次启动后先做这几步: - -1. 打开 `http://localhost:5173/settings`,至少配置一组可用的模型供应商 API Key -2. 打开 `http://localhost:5173/settings/model-routes`,检查各任务实际使用的模型路由 -3. 如果要启用知识库,打开 `http://localhost:5173/knowledge?tab=settings`,保存 Embedding / Collection 设置 - -### 4. 如果你使用 Qdrant Cloud - -如果你只是先体验主流程,其实可以先跳过 Qdrant,直接在 `server/.env` 里设: - -```env -RAG_ENABLED=false -``` - -如果你要启用 Qdrant Cloud,可以按下面的最小流程来: - -1. 到 [Qdrant Cloud](https://cloud.qdrant.io/) 注册账号。 -2. 在 `Clusters` 页面创建一个集群。 - 测试阶段用 Free cluster 就够了。 -3. 集群创建完成后,到集群详情页复制 Cluster URL。 -4. 在集群详情页的 `API Keys` 中创建并复制一个 Database API Key。 - 这个 key 创建后通常只展示一次,建议立即保存。 -5. 把它们写入 `server/.env`: - -```env -QDRANT_URL=https://your-cluster.region.cloud.qdrant.io:6333 -QDRANT_API_KEY=your_database_api_key +Runs the shared package compiler, Express server, and Vite client concurrently. + +#### Option B: Step-by-Step Startup (Recommended for macOS Debugging) +Open three separate terminal tabs/windows: +1. **Terminal 1: Shared Package Compiler** + ```bash + pnpm dev:shared + ``` +2. **Terminal 2: Backend Server** + ```bash + pnpm dev:server + ``` + (Starts on `http://localhost:3000`. Generates Prisma clients and pushes DB migrations on startup). +3. **Terminal 3: Frontend Client** + ```bash + pnpm dev:client + ``` + (Starts on `http://localhost:5173`). + +#### Option C: Background Service Manager Script (macOS Utility) +A utility helper script is available at [scripts/manage.sh](./scripts/manage.sh): +- **Start all services in background**: `./scripts/manage.sh start` +- **Stop all background services**: `./scripts/manage.sh stop` +- **Check service status**: `./scripts/manage.sh status` +- **Restart all services**: `./scripts/manage.sh restart` + +#### Option D: Local Offline TTS Server (For VellumReel Video Voiceovers) +To compile audio narrations offline (will install ONNX/Kokoro packages on first run): +```bash +python scripts/start-local-tts.py ``` -6. 启动项目后,再去 `知识库 -> 向量设置` 页面选择 Embedding provider / model,并保存集合设置。 - -对这个项目来说,`QDRANT_URL` 建议直接填 REST 地址,也就是带 `:6333` 的地址。 - -如果你想手动验证连通性,可以用: - +#### Default Server URLs: +- Frontend Client: `http://localhost:5173` +- Backend API Server: `http://localhost:3000` +- API Endpoint: `http://localhost:3000/api` +- Local Speech API Server: `http://localhost:8000` + +--- + +### 4. SenseNova Local Multimodal Image Model Setup (Optional) + +The system supports offline multi-modal image adjustments and text bubble generation using `SenseNova-U1-8B-MoT-Infographic-V3` running on local Ollama. + +#### 4.1 Install Ollama & Pull Model +1. Install [Ollama](https://ollama.com/). +2. Pull the SenseNova model manually, or the server will fetch it on its first call: + ```bash + ollama pull sensenova-u1:8b-v3 + ``` + +#### 4.2 Hardware Self-Diagnosis & Tiers +The server running backend tasks diagnoses your system memory/VRAM on startup and assigns a performance tier: +- **Tier 1 (High GPU Acceleration)**: VRAM $\ge$ 15GB or Mac Unified Memory $\ge$ 32GB. Generates images using BF16/FP16 models (approx. 8 seconds). +- **Tier 2 (Medium GPU Acceleration)**: VRAM 6GB–14GB or Mac Unified Memory 16GB–24GB. Uses INT8/INT4 GGUF models (approx. 30 seconds). +- **Tier 3 (CPU Pure Local)**: No GPU acceleration. Uses CPU execution (approx. 1.5 - 3 minutes). + +Ollama serve is launched automatically if the server fails to connect to port `11434` on startup. + +#### 4.3 Running SenseNova Tests +- **Run local inference tests**: + ```bash + pnpm --filter @ai-novel/server test + # Or run the SenseNova test script directly: + node --test server/tests/sensenovaLocalInference.test.js + ``` +- **Run E2E API simulation integrations**: + While the servers (`pnpm dev`) are running, execute this script to simulate image modifications, local SenseNova API calls, and video rendering: + ```bash + node server/scripts/test-e2e-api-simulation.js + ``` + +--- + +### 5. Qdrant Cloud Setup (Optional) + +To enable RAG, set `RAG_ENABLED=true` in `server/.env` and follow these steps: +1. Register on [Qdrant Cloud](https://cloud.qdrant.io/). +2. Create a Cluster (the free tier is sufficient). +3. Copy the Cluster URL and API key from the Dashboard. +4. Add them to `server/.env`: + ```env + QDRANT_URL=https://your-cluster.region.cloud.qdrant.io:6333 + QDRANT_API_KEY=your_database_api_key + ``` +5. Configure Embedding models in the web application UI (`Knowledge -> Vector Settings`). + +Verify connectivity via curl: ```bash curl -X GET "https://your-cluster.region.cloud.qdrant.io:6333" \ --header "api-key: your_database_api_key" ``` -你也可以把集群地址后面拼上 `:6333/dashboard` 打开 Qdrant Web UI。 - -Qdrant 官方文档: - -- [Create a Cluster](https://qdrant.tech/documentation/cloud/create-cluster/) -- [Database Authentication in Qdrant Managed Cloud](https://qdrant.tech/documentation/cloud/authentication/) -- [Cloud Quickstart](https://qdrant.tech/documentation/cloud/quickstart-cloud/) - -### 5. 可选初始化 +--- -下面这些都不是首次启动 `pnpm dev` 的前置步骤: +## 🏗️ Technical Stack & Architecture -```bash -pnpm db:seed -pnpm db:studio -``` +### Tech Stack -## 常用命令 - -```bash -pnpm dev -pnpm build -pnpm typecheck -pnpm lint -# 仅在你开发/调整 Prisma schema 时再手动使用 -pnpm db:migrate -pnpm db:seed -pnpm db:studio -pnpm --filter @ai-novel/server test -pnpm --filter @ai-novel/server test:routes -pnpm --filter @ai-novel/server test:book-analysis -``` - -## 技术栈与架构 - -### 技术栈 - -| 层级 | 技术 | +| Layer | Technologies | | --- | --- | -| 前端 | React 19、Vite、React Router、TanStack Query、Plate | -| 后端 | Express 5、Prisma、Zod | -| AI 编排 | LangChain、LangGraph | -| 数据库 | SQLite | -| RAG | Qdrant | -| 工程形态 | pnpm workspace Monorepo | +| **Frontend** | React 19 + Vite + React Router + TanStack Query + Plate Editor | +| **Backend** | Express 5 + Prisma 7 + Zod | +| **Orchestration** | LangChain + LangGraph | +| **Database** | SQLite (Primary) + Qdrant (RAG Vector Database) | +| **Workspace** | pnpm workspace Monorepo (pnpm@10.6.0) | +| **Desktop Shell** | Electron (electron-builder packaging) | +| **Node Version** | `^20.19.0 \|\| ^22.12.0 \|\| >=24.0.0` | -### Monorepo 结构 +### Monorepo Structure ```text -client/ React + Vite 前端 -server/ Express + Prisma + Agent Runtime + Creative Hub -shared/ 前后端共享类型与协议 -images/ README 与产品预览截图 -scripts/ 启动和辅助脚本 -docs/ 设计文档、阶段检查点、模块计划与历史归档 +GeneralAgent/ +├── client/ # React + Vite Frontend (@ai-novel/client) +├── server/ # Express + Prisma + Agent Runtime (@ai-novel/server) +├── shared/ # Shared types & contracts (@ai-novel/shared) +├── desktop/ # Electron desktop shell (@ai-novel/desktop) +├── docs/ # Design wikis, release notes, and archives +├── images/ # Assets, screenshots, and visual graphs +├── scripts/ # Dev and build management scripts +├── infra/ # Infrastructure configurations (Docker, etc.) +└── .github/ # CI/CD Workflows ``` -更细的文档分区说明可以看 [docs/README.md](./docs/README.md)。 - -### 当前系统关注点 +*For file-by-file counts, file sizes, and audits, review [docs/sourcegraph/project-source-audit.md](./docs/sourcegraph/project-source-audit.md).* -- `Creative Hub` 负责统一创作中枢与 Agent 运行时体验 -- `Novel Setup / Director` 负责从一句灵感走到整本可写 -- `Novel Production` 负责整本生成主链 -- `Style Engine` 负责写法资产、特征提取、绑定和反 AI 协同 -- `Knowledge / Book Analysis / World` 负责长期上下文沉淀与回灌 +--- -## 当前路线图 +### Core Architecture Pillars -当前最重要的不是继续堆零散功能,而是提高“小白把整本书写完”的成功率。 +To maintain narrative coherence across multi-volume books, the engine relies on five architectural pillars: -### P0 +| Pillar | Mechanism | +| :--- | :--- | +| **Physical Memory** | Periodically serializes active plots and story summaries into `docs/story_board.json` and `docs/story_ledger.md` to prevent context drift and survive crash recoveries. | +| **Branch isolation (Worktree)** | Separates draft buffers inside `ChapterDraft` databases. Isolates concurrent editing sessions via a `WorktreeManager` prior to a transactional `mergeAndCommit` merge. | +| **Debate Auditing** | Utilizes an `EditorAgent` checking text against Zod-compiled schemas, returning structural edits or blocking flawed text generation. | +| **Self-Checking Heartbeat** | Employs an active background diagnostician reviewing overall narrative discrepancies, printing pending warnings to `docs/STORY_TASKS.md`. | +| **Cockpit Console** | A dashboard showing active agent status, model health ratings, warning flags, and live debate logs. | -- 稳定自动导演连续执行,减少误停链、重复审校和异常 Token 消耗 -- 让本书世界、角色、伏笔、时间线和章节任务稳定进入后续写作上下文 -- 降低新手从一句灵感到可连续写章之间的判断成本和修复成本 +--- -### P1 +## 🎨 Visual Previews -- 提高整本一致性、节奏稳定性、人物成长质量和世界状态继承质量 -- 让写法资产、世界约束、章节重规划、审阅反馈和质量债形成闭环 -- 让系统更擅长“持续掌控整本书”,而不只是“生成某一章” - -### P2 - -- 继续强化多阶段 Agent 协同和运行时可观察性 -- 完善更自动化的生产调度、恢复策略、回合记忆和整本质量控制 - -## 交流反馈 - -如果你想反馈问题、交流使用体验,或者讨论自动导演、整本生产主链、写法引擎等方向,可以扫码加入 QQ 群。 - -![QQ 群二维码](./images/群2.png) - -## 支持项目 - -如果这个项目对你有帮助,欢迎通过支付宝扫码支持持续开发与维护。 - -

- 支付宝捐赠二维码 -

- -## 贡献方式 +### Creative Hub +Unified creation dashboard hosting dialogue, planning, and task runtime steps. +![Creative Hub](./images/创作中枢.png) -如果你想参与这个项目,最有价值的贡献方向包括: +### Prompt Editor +Interactive prompting screen where system prompts, variables, and slots are tested. +![Prompt Editor](./images/ScreenShot_2026-07-08_140153_328.png) -- 提升整本生产稳定性 -- 改善新手开书体验和自动导演成功率 -- 强化写法引擎、知识库回灌和世界观一致性链路 -- 补充测试、错误回放和运行时可观察性 +### Auto-Director Modes +Direction creation sheets with custom framing, title candidates, and scope parameters. +![Director Create](./images/导演模式-创建.png) +![Director Output](./images/导演模式-创建中.png) -欢迎直接提 Issue 或 Pull Request。 -提交 Pull Request 即表示你确认自己有权提交该内容,并已阅读且同意 [CLA.md](./CLA.md);如果包含第三方代码、素材、AI 生成内容或其他受许可证约束的内容,请在 PR 中明确说明来源和许可证。详见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 +### Volume Strategy & Beat Sheets +Visualized layout mapping volume structures and target chapter outlines. +![Volume Outline](./images/write/卷战略.png) +![Chapter Breakdown](./images/write/节奏拆章.png) -## 致谢 +### Comic Workshop & Video Adaptations +Multi-modal workshops drawing assets from the written book and rendering vertical video voiceovers. +![Comic Workshop](./images/漫画工坊.png) +![Video Workshop](./images/视频工坊.png) -感谢提交修复 Pull Request 的贡献者 [@ystyleb](https://github.com/ystyleb)。 +--- +## 🗺️ Roadmap +- **P0**: Core stability, context memory optimization, checkpoint recovery, and consistency checks. +- **P1**: Streamlining adaptation compiler pipelines (Novels $\rightarrow$ Comics $\rightarrow$ Storyboards $\rightarrow$ Videos). +- **P2**: Introduction of the World Sandbox framework: Faction grids, autonomous character simulations, and live chronicle logging. -## 说明 +## 💬 Community +For feedback, bug reports, and discussions regarding LLM routing, auto-directors, and multi-modal synthesis, join our Q-Group: -- 这是一个持续快速迭代中的 AI Native 创作系统,功能边界仍在演化。 -- README 优先描述当前最值得体验、最能代表方向的能力,而不是列出全部历史实现细节。 -- 如果你更关心阶段目标、优先级和后续优化计划,请直接查看 [TASK.md](./TASK.md)。 +![QQ Group](./images/群2.png) ## License - -本项目采用双许可证授权模式: - -- 默认情况下,本项目基于 GNU Affero General Public License v3.0 (AGPLv3) 授权,详见 [LICENSE](./LICENSE);归属与附加说明见 [NOTICE](./NOTICE)。 -- 服务型商用:将本项目(或其修改版本)作为后端以 SaaS、托管或其他形式向第三方提供服务,须通过作者获取商业授权许可。 -- 请遵守开源协议条款,并在适用场景下取得相应授权。 - -贡献说明:新贡献默认按 [CLA.md](./CLA.md) 提交,可随项目按 AGPL-3.0-only 分发,并可纳入项目维护者另行提供的商业授权;详见 [CONTRIBUTING.md](./CONTRIBUTING.md)。 - -## 友情链接 - -- [LINUX DO](https://linux.do/) +The project is dual-licensed: +- Default: **GNU Affero General Public License v3.0 (AGPLv3)**. Check [LICENSE](./LICENSE) and [NOTICE](./NOTICE) for details. +- SaaS/Commercial Hosting: Accessing or hosting modified versions of this engine to third parties as a service requires a commercial license from the authors. Refer to [CONTRIBUTING.md](./CONTRIBUTING.md) and [CLA.md](./CLA.md) for contribution terms. diff --git "a/README_Windows\344\270\200\351\224\256\345\214\205\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/README_Windows\344\270\200\351\224\256\345\214\205\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 0000000000..f0610a9102 --- /dev/null +++ "b/README_Windows\344\270\200\351\224\256\345\214\205\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,47 @@ +# Daydream Engine (白日做梦引擎) - Windows 智能一键包使用指南 + +欢迎使用 **Daydream Engine**!本说明书专为没有任何开发背景或命令行经验的用户编写。 + +--- + +## 🚀 快速使用三步法 + +### 第一步:智能一键安装 (`1-一键安装.bat`) +* **操作**:双击运行 `1-一键安装.bat`。 +* **功能**: + * **智能探测环境**:自动检测您电脑中是否已安装 Node.js、Python、FFmpeg、ComfyUI、Ollama、SearXNG 等软件。若已安装,**自动复用,绝对不重复安装**! + * **模型免重复下载**:自动检测本地是否已有 safetensors 模型或 TTS 模型文件,存在即自动跳过下载。 + * **自动绑定配置**:将您本机的软件路径与端口自动写入 `server/.env` 配置文件。 + * **数据库初始化**:自动创建 SQLite 数据库,灌入预设提示词与美学种子数据。 + +### 第二步:一键启动 (`2-一键启动.bat`) +* **操作**:双击运行 `2-一键启动.bat`。 +* **功能**: + * 按序自动启动向量数据库(Qdrant)、离线语音(TTS)、生图引擎(ComfyUI)、后端 API 服务及前端 Web 界面。 + * 启动成功后,会自动用您的默认浏览器打开 `http://localhost:5173` 访问工作台。 + +### 第三步:一键停止 (`3-一键停止.bat`) +* **操作**:双击运行 `3-一键停止.bat`。 +* **功能**: + * 优雅关闭所有后台关联服务,干净释放系统内存与 CPU 资源。 + +--- + +## 🛠️ 运维与测试还原工具箱 (`4-工具箱-测试还原与诊断.bat`) + +如果您需要对安装脚本进行测试,或者配置更强大的本地 LLM 模型: +1. **重新运行智能环境诊断**:当您安装了新的 ComfyUI 或模型后,运行此项自动更新路径。 +2. **深度清理所有测试数据**:快速重置测试环境。 +3. **创建数据全量备份与快照还原**:方便安全测试。 +4. **配置 Muse-Glimmer-30B 强力本地大模型**: + * **选项 [6]**:一键下载 HuggingFace 的 `Muse-Glimmer-30B-GGUF` (4-bit 高精度量化权重)。 + * **选项 [7]**:一键启动全显存加速的 `llama-server` 推理接口 (提供 `http://127.0.0.1:8080/v1` OpenAI 兼容服务)。 + +--- + +## ❓ 常见问题排查 (FAQ) + +1. **如果在安装或启动时弹出 Windows 防火墙提示?** + * 请点击“允许访问”,以便本地前端界面能正常连接本地后端服务与 ComfyUI 服务。 +2. **系统未安装 ComfyUI 或 Docker 影响使用吗?** + * 完全不影响!系统具备**平滑降级保护**,无 ComfyUI 时仍然可以使用小说整本写作、自动导演与知识库功能;无 Docker 时 SearXNG 会自动切换为存量知识库模式。 diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000000..60a41b366f --- /dev/null +++ b/README_zh.md @@ -0,0 +1,477 @@ +# Daydream Engine (白日做梦引擎) / AI 小说整本生产与多模态模拟沙盘引擎 +一个旨在将人类想象力与故事世界具现化的多功能智能体多模态模拟沙盘。 + +Languages: [English](README.md) | [简体中文](README_zh.md) + +当前开发主线: +`Creative Hub + 自动导演开书 + 本书世界上下文 + 整本生产主链 + 写法引擎` + +![Monorepo](https://img.shields.io/badge/Monorepo-pnpm%20workspace-3C873A) +![Frontend](https://img.shields.io/badge/Frontend-React%20%2B%20Vite-61DAFB) +![Backend](https://img.shields.io/badge/Backend-Express%20%2B%20Prisma-111827) +![LangChain](https://img.shields.io/badge/AI-LangChain-0EA5E9) +![LangGraph](https://img.shields.io/badge/Agent-LangGraph-7C3AED) +![Editor](https://img.shields.io/badge/Editor-Plate-7C3AED) +![Database](https://img.shields.io/badge/Database-SQLite%20%2B%20Prisma-111827) +![Vector DB](https://img.shields.io/badge/RAG-Qdrant-E63946) + +--- + +## 🌌 项目愿景与路线图:白日做梦的连续谱 + +白日做梦引擎(Daydream Engine)不是一个普通的“你写一句、AI 补一句”的编辑器外壳。它的核心设计理念是将创意写作与生成看作一个多阶段的“编译”和“沙盘演化”过程: + +```mermaid +flowchart LR + A["灵感火花"] --> B["1. 小说整本生产"] + B --> C["2. 小说改漫画"] + C --> D["3. 漫画变分镜剧本"] + D --> E["4. 分镜生成短剧"] + E --> F["5. 电影级大片"] + F --> G["6. 虚拟世界沙盘 (西部世界)"] + + style B fill:#bfdbfe,stroke:#2563eb,stroke-width:2px + style G fill:#fbcfe8,stroke:#db2777,stroke-width:2px +``` + +1. **小说整本生产 (第一步 - 目前实现最充分的一步)** + 将单句灵感和提示词自动导演,规划出方向、故事结构、动态角色网、事实账本,并提供自动写作、审校、修复与状态回灌的闭环生产链。 +2. **小说改漫画** + 提取小说的场景、视觉特征和角色模型表,在保持画面一致性的前提下,自动输出分镜面板并生成连贯的漫画资产。 +3. **漫画变成分镜剧本** + 将画面序列和剧情节奏解构为专业级别的影视分镜剧本,包含镜头轨迹、对话旁白、舞台调度与配音指令。 +4. **分镜生成短剧 (VellumReel 改编工坊)** + 集成高保真本地语音合成(TTS)、音视频对齐与渲染引擎,将剧本一键合成生成 9:16 竖版视频。 +5. **电影级大片** + 向大屏幕演进,扩展本地视频生成模型,提供可控的宏大场景、音轨混合与镜头生成链路。 +6. **终极目标:世界沙盘 (虚拟西部世界)** + 将小说里的角色、阵营、地理和物理/法则全部映射到一个演化沙盘(Sandbox)中。在这个虚拟“西部世界”里,智能体拥有长期记忆和动机,在网格上自主交互、做出决策并发生冲突,系统自动记录编年史并源源不断地生成无限故事。 + +适合**完全不懂写作的新手**走完一本长篇创作并进行视觉延展,也适合研究 AI Native 应用、Agent Workflow、LangGraph 编排和长链路任务的开发者参考。 + +--- + +## Windows 桌面版 + +如果你想直接下载安装运行预编译桌面版: +- 下载入口:[GitHub Releases](https://github.com/winnerineast/GeneralAgent/releases) +- 最新版本页:[Latest Release](https://github.com/winnerineast/GeneralAgent/releases/latest) +- 建议优先下载 `Setup.exe` 安装版;如需免安装运行,可下载 `portable` 便携版。 +- 公开介绍站:[GitHub Pages 介绍站](https://winnerineast.github.io/GeneralAgent/) 提供功能预览、模块文档和使用指南。 + +## 本地 Codex 创作:Ani Book Skill + +如果你希望直接在 Codex 的本地工作区中推进创作,可以使用 [Ani Book Skill](https://github.com/ExplosiveCoderflome/ani-book-skill)。它将方向判断、故事发动机、章节推进、审校修复和连续性管理组织为一条可恢复、可追溯的长篇创作流程。 + +- 需要可视化工作台、模型配置和小说/漫画多模态工坊:使用本仓库。 +- 偏好在本地通过 Codex 文件和流程进行无界面纯创作:前往 [Ani Book Skill](https://github.com/ExplosiveCoderflome/ani-book-skill)。 + +--- + +## 🛠️ 已实现功能 (What Has Been Done) + +### 1. AI 自动导演开书与四种运行模式 +- 从一句灵感直接进入自动导演,无需手写设定;系统整理项目设定、对齐书级 framing,生成多套整本方向与标题组。 +- 方向不满意时可以局部修订,支持单独重做标题组。 +- 四种运行模式:**先准备到可开写**(推荐首本书)、**全书自动成书**、**按范围执行**(全书/前N章/指定卷)、**正文后去 AI 检测与修正**(质量闭环)。 +- 智能检查点:遇到配额耗尽或修复失败会主动暂停并保存状态,支持无缝接管与恢复。 +- 批量运行后自动确认 pending 角色,角色信息灌回名册以消除后续生成中的一致性漂移。 + +### 2. Creative Hub 与 Agent Runtime +- 统一创作中枢承载对话、追问、规划、工具调用、任务状态卡片和回合总结。 +- 采用 LangGraph 编排,包含 Planner、Tool Registry、Runtime、审批节点和中断恢复链路。 +- 状态到达 checkpoint 时自动弹出浏览器系统通知,后台挂机更安心。 + +### 3. 整本生产主链与章节执行 +- 单章运行与整本批量 pipeline 收敛到同一条主链。 +- 章节上下文精确筛选参与角色,防止无关角色污染 context。 +- 章节执行链覆盖正文生成、AI 审核、质量债务记录、角色状态回灌和下一章入口。 +- 限速器按 provider 动态淘汰,彻底解决长时间挂机运行的内存泄漏问题。 + +### 4. 拆书工作台与角色形象演变 +- 角色档案分为简要、标准、深入、完整四档,深度分析会回溯原文片段补全数据。 +- **角色形象演变**:按 25% / 50% / 75% / 100% 覆盖率增量扫描出场章节,提取每章外貌和状态,基于快照生成同一角色在不同阶段的形象参考图,保持特征一致。 +- 提供双栏阅读、证据回溯、token 预算守卫与稿件诊断。 + +### 5. 写法引擎与反 AI 规则 +- 写法可以从现有文本提取写法特征,沉淀为特征池,逐项启用/停用并实时编译为约束。 +- 反 AI 规则减少正文的模板感、叙事解释感和空泛表达。 + +### 6. 本书世界、角色、知识库联动与 RAG +- 势力图谱、地图、法则等作为背景世界观自动进入章节上下文。 +- 拆书结论和知识库文档通过 Qdrant 向量库回灌到规划、续写和正文生成。 +- RAG 索引流式并行、chunk hash 去重与 retrieval trace 后端追溯。 + +### 7. GA-Argus 持久化 Agent 运行时与 PAI 架构 +- **工作合同 ($K_t$) 与带验证转向 (Verified Pivoting)**:显式解耦立项核心意图 ($\iota$) 与阶段执行合同 ($o_t, c_t, v_t$),当遇到严重剧情碰撞或审核卡点时,支持有数据验证的局部大纲转向(Pivot),彻底避免目标漂移或推翻全书。 +- **已否决死枝账本 (Falsified Route Ledger)**:自动持久化已经被 Reviewer 打回的错误剧情路线,提炼为结构化负向约束 (`negativePromptConstraint`) 注入上下文,实现**重复踩坑率 0%**。 +- **四角色权责状态机 ($M, P, E, R$)**:严格定义 Manager (导演/合同准入)、Planner (拆章/负向注入)、Engineer (撰稿/修文) 与 Reviewer (4级分流质检) 的结构化契约。 +- **Daniel Miessler PAI 8 大启示全量融入**:创作者 TELOS 档案驱动意图,用户资产物理隔离 (`protectedUserContent`),Hot/Warm/Cold 三层记忆管理与确定性防幻觉网关。 +- **固定模型运行时自我演化与降本**:随着 $H_t$ 持久化状态沉淀,实测成熟写作波次 Input Token 消耗降低 21%,审查打回救回率达 75%。 + +### 8. 虚拟世界沙盘模拟 (西部世界沙盘) +- 实现完整的锁步回合制沙盘模拟,反映小说世界的物理与生态法则(详见 [world-sandbox-simulation.md](./docs/design/world-sandbox-simulation.md))。 +- **地球物理与生态**:实时模拟动态温度(纬度/季节/海拔/时角)与基于 Lotka-Volterra 方程的捕食者-猎物动态。 +- **角色认知智能体**:包含艾宾浩斯遗忘曲线记忆衰减模型与跨区域谣言扩散模型。 +- **行为树与 LLM 调度器**:采用 LOD 2 行为树维护背景角色的饥饿、精力与理智,LOD 1 主角决策交由沙盘 LLM 调度器。 +- **剧情张力与一致性审计**:追踪局部与全局张力,通过虚拟镜头审计地理瞬移或已故角色发言等一致性冲突。 + +### 9. 衍生多模态工坊 +- **漫画工作台**:生成分镜与面板,采用确认弹窗防止误触耗额,自动继承角色的视觉资产。 +- **VellumReel 短剧生产管线**:将分镜剧本合成为 9:16 竖版短剧。 + - **完全离线渲染**:内置 6 张高精水墨风景画作为离线 Fallback。 + - **本地高保真 TTS**:基于 Kokoro-ONNX v1.0 与 `misaki[zh]` 的 FastAPI 语音服务,支持中英文离线朗读。 + - **声音映射与提示词清洗**:自动映射性别音色(`am_*`/`bm_*` 映射为男声 `zm_yunjian`,`af_*`/`bf_*` 映射为女声 `zf_xiaoxiao`),正则清洗旁白中的角色名与舞台指示。 + +### 10. PAI 核心基础设施架构 (启示 #1 - #8) +全量落地 Daniel Miessler 的个人 AI 基础设施 (PAI) 架构思想: +- **启示 #1 (确定性优先)**:纯代码词法 JSON 修复 (`tryFixSyntacticJson`) 与 250+ 调用点强转换,节省延迟与 Token。 +- **启示 #2 (用户/系统隔离与资产保护)**:非破坏性设置管理与项目备份打包网关。 +- **启示 #3 (三层记忆架构)**:确定性 15% Hot / 35% Cold / 50% Warm 预算分配,锁定世界法则与角色红线。 +- **启示 #4 (Pipeline 钩子与主动导演)**:异步事件总线 (`PipelineHookRegistry`),带错误隔离与渲染状态自动恢复。 +- **启示 #5 (TELOS 创作者档案)**:10 维创作者画像与 4 大美学预设(修仙、悬疑古风、赛博朋克、都市爽文)。 +- **启示 #6 (安全与权限护栏)**:4 级风险护栏,破坏性操作强制双重确认 Token 与自动快照。 +- **启示 #7 (CLI 优先与 UNIX 哲学)**:独立 CLI 自动化网关 (`pnpm --filter server run:cli`),支持 Headless 审计与索引重建。 +- **启示 #8 (规格优先与防幻觉)**:定量知识置信度评估,缺少上下文时自动注入 `ALLOW "I DON'T KNOW"`。 + +### 12. OpenRSI 演化算子引擎与杂交重组 +集成 Frontis OpenRSI 递归自我改进 (RSI) 原则,构建标准化文本演化算子: +- **`Draft` 算子**:基于大纲、三层记忆与 TELOS 生成初始章节候选。 +- **`Improve` 算子**:基于 `AuditService` 诊断进行无损文风与节奏润色。 +- **`Debug` 算子**:对设定碰撞或人设 OOC 进行外科手术式局部修补。 +- **`Crossover` 算子 (核心创新)**:解构亲本 A(动作/高潮)与亲本 B(心理/环境)优良基因,杂交重组为更高满意度的新候选。 +- **演化算子引擎与 REST API**:提供中央门面 `EvolutionaryOperatorEngine` 与 `/api/novel/director/operators/crossover` 等接口。 + +### 13. 数字员工团队架构与数字员工基础设施 +将 Daydream Engine 从单回合提示词升级为**数字员工团队基础设施**: +- **数字员工 Profile 标准化 (`Identity + Domain + Scope`)**:注册 `novel-director`、`style-auditor`、`crossover-operator` 等角色,明确能力边界与 4 级风险 Scope。 +- **长寿 Thread 引擎与 Prompt Cache 优化**:固定 `staticPromptHead` 静态角色头,保证 100% 缓存命中,降低 50%-90% API 成本与 42.5% 延迟。 +- **动态 Warm Memory 压缩**:多回合对话自动提炼 `workingMemoryDigest`,控制 Token 膨胀。 +- **双层通用阶段 Hand-off 关卡与价值函数 ($V_{\text{handoff}}$)**: + - **Layer 1 (通用元评估器框架)**:确定性计算 $V_{\text{handoff}} \in [0.0, 1.0]$ 得分。 + - **Layer 2 (载荷驱动公式编译器)**:动态编译 `ValueFormulaSpec` 规则与硬约束。 + - **防篡改证书**:通关产生带 SHA256 签名证书;轻微扣分自动触发 `AUTO_REPAIR`。 +- **配额感知全自动无看守恢复调度器 (Module 2)**:遇到 429 或配额耗尽自动进入 `QUOTA_COOLING`,后台 Heartbeat Worker 自动恢复运行。 +- **持久化 Agent 可执行 Todo 看板引擎 (Module 3)**:基于 SQLite 数据库 (`AgentExecutableTodo`),提供原子抢占、凭证完工与死锁自愈。 +- **OpenRSI 演化算子证据追踪日志 (Module 4)**:记录 AI 谱系树 (`getChapterMutationLineage`)、防退化回滚护栏与精英向量 RAG 反馈回路。 +- **经验自动化 Benchmark**:包含完整测试脚本验证 Prompt Head 匹配、双层 Gate、自动唤醒、看板抢占与突变日志。 + +### 14. 国际化 (i18n) 多语言支持 +- 前端深度集成 `i18next` 与 `react-i18next`,UI 界面、日志、页面标签与设置路由支持中英文无缝切换并本地持久化。 + +--- + +## 🔮 未来规划 (Future Vision) + +随着项目从小说写作引擎演进为完整的 **Daydream Engine**,未来的开发重点集中在深化**数字员工团队**与**循环工程内核**: + +### 🎭 阶段 1:无缝衍生改编 (小说 ➔ 漫画 ➔ 短视频) +- **跨模态 Hand-off Gate**:将 Hand-off 关卡扩展至校验小说章节 $\rightarrow$ 分镜脚本 $\rightarrow$ 视觉资产。 +- **多模态持久化看板**:扩展 Durable Executable Todos 跟踪图像生成、TTS 语音与视频渲染。 +- **持久化视觉 Style Sheet**:建立角色脸型、发型、服装的跨图/跨视频一致性规范。 + +### 🎬 阶段 2:分镜剧本 ➔ 电影级大片 +- **视频渲染证据日志**:记录音视频渲染参数与美学得分,支持失败自动回滚。 +- **影视级管线扩展**:扩展本地渲染管线(VellumReel)支持宽屏 (16:9, 2.39:1) 与多轨音效剪辑。 + +### 🗺️ 阶段 3:可视化西部世界控制台与阵营对抗 +- **长寿 Agent 团队沙盘**:结合数字员工 Profile 与长寿 Thread,支持数十个 Agent 在沙盘中长期自洽互动。 +- **可视化西部世界控制台**:构建 Web 端地理网格、阵营边界与动态角色定位图。 + +--- + +## 🚀 技术运行指南 + +### 环境要求 + +- **Node.js**: `^20.19.0 || ^22.12.0 || >=24.0.0` (推荐 `20.19.x LTS`) +- **pnpm**: `>= 10.6.0` (推荐声明的 `pnpm@10.6.0`) +- **LLM API Key**: 至少配置一个提供商(OpenAI, DeepSeek, SiliconFlow, xAI 等),可在启动后在页面配置。 +- **Qdrant**: 可选,仅在开启知识库 / RAG 检索时需要。 +- **VellumReel 视频管线要求**: + - Python `^3.10` + - 系统配置好 FFmpeg(用于视频合成与字幕) + - ONNX runtime 依赖(本地 FastAPI TTS 服务首次运行会自动下载 Kokoro 模型权重) + +### 1. 安装依赖 +```bash +pnpm install +``` + +*注意: 默认 `pnpm install` 仅安装 Web 与 Server 依赖,不会自动下载 Electron 运行时。* +- 如果只运行 Web/Server 流程,这就足够了。 +- 如果需要运行桌面端外壳,首次运行 `pnpm dev:desktop` 时会自动下载 Electron。 +- 你也可以手动预拉取 Electron 运行时: + ```bash + pnpm run prepare:desktop-runtime + ``` + +#### Windows 下 Prisma 安装故障排查: +如果在 Windows 上 `pnpm install` 卡在 `prisma preinstall`,请检查: +1. **Node 版本**: Prisma 7 要求 Node `^20.19.0 || ^22.12.0 || >=24.0.0`。 +2. **Script-shell 设置**: 如果 npm/pnpm 的 script-shell 被设为交互式终端(如 `cmd.exe /k`),Prisma 预安装脚本会挂起。请检查: + ```bash + node -v + pnpm config get script-shell + npm config get script-shell + ``` + 如果返回包含 `/k`,请删除并重启终端: + ```bash + npm config delete script-shell + pnpm config delete script-shell + ``` + 然后重新运行 `pnpm install`。 + +--- + +### 2. 配置环境变量 + +项目结构采用 Monorepo 隔离,配置加载如下: +- 后端运行在 `server/` 工作区,读取 `server/.env`。 +- 前端运行在 `client/` 工作区,读取 `client/.env` 或 `client/.env.local`。 +- 根目录 `.env.example` 仅作为聚合参考。 + +#### 2.1 后端环境变量配置 +复制后端示例文件: +```bash +# macOS / Linux +cp server/.env.example server/.env + +# Windows PowerShell +Copy-Item server/.env.example server/.env +``` +`server/.env` 中的核心配置: +- `DATABASE_URL`: 默认指向本地 SQLite (`file:../prisma/dev.db`),开箱即用。 +- `RAG_ENABLED`: 未启动 Qdrant/RAG 时请设为 `false`。 +- `QDRANT_URL` / `QDRANT_API_KEY`: 仅在开启 RAG 时填写。 +- API Keys (`OPENAI_API_KEY`, `DEEPSEEK_API_KEY` 等) 可先留空,后续在 Web UI 页面配置。 + +#### 2.2 前端环境变量配置 +默认情况下 Vite 开发服务器会自动将请求映射到 `http(s)://[当前主机名]:3000/api`,局域网开发无需额外配置。仅当前后端分离部署时才需复制配置: +```bash +# macOS / Linux +cp client/.env.example client/.env + +# Windows PowerShell +Copy-Item client/.env.example client/.env +``` + +#### 2.3 在 UI 页面配置模型 +无需在 `.env` 中硬编码模型,可在运行后在页面直接管理: +- `/settings`: 配置 API Key,测试连通性。 +- `/settings/model-routes`: 为规划、正文、审阅不同任务分配不同模型。 +- `/knowledge?tab=settings`: 管理 Embedding 提供商、集合与重建计划。 + +--- + +### 3. 启动开发环境 + +#### 方式 A:一键启动(全量服务) +```bash +pnpm dev +``` +同时启动 shared 编译器、Express 后端服务与 Vite 前端客户端。 + +#### 方式 B:分步启动(推荐 macOS 调试) +打开三个独立的终端窗口: +1. **终端 1: Shared 编译** + ```bash + pnpm dev:shared + ``` +2. **终端 2: 后端服务** + ```bash + pnpm dev:server + ``` + (运行在 `http://localhost:3000`,启动时自动生成 Prisma Client 并应用 DB 迁移)。 +3. **终端 3: 前端客户端** + ```bash + pnpm dev:client + ``` + (运行在 `http://localhost:5173`)。 + +#### 方式 C:后台服务管理脚本 (macOS 脚本) +在 [scripts/manage.sh](./scripts/manage.sh) 提供了服务管理辅助脚本: +- **启动所有后台服务**: `./scripts/manage.sh start` +- **停止所有后台服务**: `./scripts/manage.sh stop` +- **查看服务状态**: `./scripts/manage.sh status` +- **重启服务**: `./scripts/manage.sh restart` + +#### 方式 D:本地离线 TTS 服务 (用于 VellumReel 视频配音) +进行离线语音合成(首次运行会自动安装 ONNX/Kokoro): +```bash +python scripts/start-local-tts.py +``` + +#### 默认访问入口: +- 前端客户端: `http://localhost:5173` +- 后端 API 服务: `http://localhost:3000` +- API Endpoint: `http://localhost:3000/api` +- 本地 TTS 服务: `http://localhost:8000` + +--- + +### 4. SenseNova 本地多模态图片模型配置 (可选) + +系统支持基于本地 Ollama 运行 `SenseNova-U1-8B-MoT-Infographic-V3` 模型进行离线图片微调与文字气泡生成。 + +#### 4.1 安装 Ollama 并拉取模型 +1. 安装 [Ollama](https://ollama.com/)。 +2. 手动拉取 SenseNova 模型,或由服务端在首次调用时自动拉取: + ```bash + ollama pull sensenova-u1:8b-v3 + ``` + +#### 4.2 硬件自诊断与性能分级 +服务端启动时会自动诊断显存/内存并划定性能等级: +- **Tier 1 (显卡强力加速)**: 显存 $\ge$ 15GB 或 Mac 统一内存 $\ge$ 32GB。使用 BF16/FP16 生成(约 8 秒)。 +- **Tier 2 (显卡中度加速)**: 显存 6GB–14GB 或 Mac 统一内存 16GB–24GB。使用 INT8/INT4 模型(约 30 秒)。 +- **Tier 3 (CPU 纯本地)**: 无 GPU 加速,使用 CPU 计算(约 1.5 - 3 分钟)。 + +若启动时无法连通 `11434` 端口,系统会自动拉起 Ollama 服务。 + +#### 4.3 运行 SenseNova 测试 +- **运行本地推理测试**: + ```bash + pnpm --filter @ai-novel/server test + # 或直接运行 SenseNova 测试脚本: + node --test server/tests/sensenovaLocalInference.test.js + ``` +- **运行 E2E API 模拟集成测试**: + 在主服务 (`pnpm dev`) 运行时执行,模拟图片修改、本地 API 调用与视频渲染: + ```bash + node server/scripts/test-e2e-api-simulation.js + ``` + +--- + +### 5. Qdrant Cloud 云数据库配置 (可选) + +如需开启 RAG,请在 `server/.env` 中设置 `RAG_ENABLED=true` 并按如下步骤配置: +1. 注册 [Qdrant Cloud](https://cloud.qdrant.io/)。 +2. 创建 Cluster(免费版即可)。 +3. 复制 Cluster URL 和 API Key。 +4. 填入 `server/.env`: + ```env + QDRANT_URL=https://your-cluster.region.cloud.qdrant.io:6333 + QDRANT_API_KEY=your_database_api_key + ``` +5. 在 Web 页面中配置 Embedding 模型 (`知识库 -> 向量设置`)。 + +通过 curl 验证连通性: +```bash +curl -X GET "https://your-cluster.region.cloud.qdrant.io:6333" \ + --header "api-key: your_database_api_key" +``` + +--- + +### 6. SearXNG 本地 Docker 开源搜索引擎配置 (美股热点抓取可选) + +美股投研 Agent 深度集成了本地运行在 Docker 容器中的开源元搜索引擎 **SearXNG**(运行在 `http://127.0.0.1:8080`),实现美股盘前新闻与个股催化剂的自动检索。 + +```bash +# 方式 1:Docker 一键快捷运行(推荐 8088 端口映射,避免 8080 被占用) +docker run -d \ + --name searxng \ + -p 8088:8080 \ + -v $(pwd)/scratch/searxng/settings.yml:/etc/searxng/settings.yml:ro \ + searxng/searxng:latest + +# 环境变量 (可选,默认 http://127.0.0.1:8088) +# SEARXNG_URL=http://127.0.0.1:8088 + +# 运行连通性测试 +node server/scripts/runSearXNGTest.cjs +``` +- **零中断降级保护**:当 SearXNG 容器未启动时,系统会自动平滑降级为“存量知识图谱模式”,保障主服务稳定运行。 + +--- + +## 🏗️ 技术栈与架构 + +### 技术栈 + +| 层级 | 技术 | +| --- | --- | +| **前端** | React 19 + Vite + React Router + TanStack Query + Plate 编辑器 | +| **后端** | Express 5 + Prisma 7 + Zod | +| **AI 编排** | LangChain + LangGraph | +| **数据库** | SQLite (主库) + Qdrant (向量库/RAG) | +| **工程形态** | pnpm workspace Monorepo (pnpm@10.6.0) | +| **桌面端** | Electron (electron-builder 打包) | +| **Node 版本** | `^20.19.0 \|\| ^22.12.0 \|\| >=24.0.0` | + +### Monorepo 目录结构 + +```text +GeneralAgent/ +├── client/ # React + Vite 前端 (@ai-novel/client) +├── server/ # Express + Prisma + Agent 运行时 (@ai-novel/server) +├── shared/ # 共享类型与契约 (@ai-novel/shared) +├── desktop/ # Electron 桌面端外壳 (@ai-novel/desktop) +├── docs/ # 设计文档、发布日志与归档 +├── images/ # 架构图、截图与视觉资产 +├── scripts/ # 开发与构建管理脚本 +├── infra/ # 基础设施配置 (Docker 等) +└── .github/ # CI/CD 工作流 +``` + +*详细的代码文件统计与审计见 [docs/sourcegraph/project-source-audit.md](./docs/sourcegraph/project-source-audit.md)。* + +--- + +### 核心架构支柱 + +为保证长篇故事的连续性与一致性,系统依托五大架构支柱: + +| 支柱 | 机制 | +| :--- | :--- | +| **物理记忆 (Physical Memory)** | 定期将激活的剧情与摘要序列化至 `docs/story_board.json` 与 `docs/story_ledger.md`,防止上下文漂移并支持崩溃恢复。 | +| **分支隔离 (Worktree)** | 在 `ChapterDraft` 数据库中隔离草稿缓冲区,在事务性 `mergeAndCommit` 前通过 `WorktreeManager` 隔离并行编辑会话。 | +| **辩论审校 (Debate Auditing)** | `EditorAgent` 对照 Zod 校验 Schema 检查文本,返回结构化修改建议或拦截缺陷文本生成。 | +| **自检心跳 (Self-Checking Heartbeat)** | 后台诊断器自动检查全局叙事冲突,将预警打印至 `docs/STORY_TASKS.md`。 | +| **驾驶舱控制台 (Cockpit Console)** | 实时展示 Agent 运行状态、模型健康评级、预警标记与实时辩论日志的仪表盘。 | + +--- + +## 🎨 功能预览与截图 + +### 创作中枢 (Creative Hub) +统一承载对话、规划与任务运行步帧的创作控制台。 +![创作中枢](./images/创作中枢.png) + +### 提示词编辑器 (Prompt Editor) +调试与维护系统提示词、变量与槽位规则的交互界面。 +![提示词编辑器](./images/ScreenShot_2026-07-08_140153_328.png) + +### 自动导演模式 +包含项目 Setup、标题候选组与定制 Framing 的开书界面。 +![导演创建](./images/导演模式-创建.png) +![导演生成中](./images/导演模式-创建中.png) + +### 卷战略与拆章 +可视化卷结构布局与目标章节大纲拆解。 +![卷战略](./images/write/卷战略.png) +![章节拆解](./images/write/节奏拆章.png) + +### 漫画与视频衍生工坊 +从已生成小说提取资产并一键渲染 9:16 竖版视频的多模态工坊。 +![漫画工坊](./images/漫画工坊.png) +![视频工坊](./images/视频工坊.png) + +--- + +## 🗺️ Roadmap 路线图 +- **P0**: 核心稳定性、上下文记忆优化、检查点恢复与一致性检查。 +- **P1**: 完善改编编译器管线 (小说 $\rightarrow$ 漫画 $\rightarrow$ 分镜剧本 $\rightarrow$ 短视频)。 +- **P2**: 推出西部世界沙盘框架:阵营网格、自主角色模拟与动态编年史记录。 + +## 💬 社区与交流 +如需反馈问题、讨论 LLM 路由、自动导演或多模态合成,欢迎加入 QQ 交流群: + +![QQ 群](./images/群2.png) + +## 开源协议 (License) +本项目采用双重许可模式: +- 默认协议: **GNU Affero General Public License v3.0 (AGPLv3)**。详见 [LICENSE](./LICENSE) 与 [NOTICE](./NOTICE)。 +- Commercial / SaaS 商业授权: 托管或向第三方提供本引擎的修改版本服务需要获取商业授权。贡献条款请参阅 [CONTRIBUTING.md](./CONTRIBUTING.md) 与 [CLA.md](./CLA.md)。 diff --git a/benchmark-baseline-before.json b/benchmark-baseline-before.json new file mode 100644 index 0000000000..cda1d45c2f --- /dev/null +++ b/benchmark-baseline-before.json @@ -0,0 +1,38 @@ +{ + "timestamp": "2026-08-03T02:35:39.483Z", + "mode": "session-assembled-baseline", + "runs": [ + { + "actionName": "novel_director_setup", + "inputTokens": 3200, + "outputTokens": 850, + "cachedTokens": 0, + "latencyMs": 2450, + "preferenceRetained": true + }, + { + "actionName": "director_world_generation", + "inputTokens": 4100, + "outputTokens": 1200, + "cachedTokens": 0, + "latencyMs": 3100, + "preferenceRetained": true + }, + { + "actionName": "chapter_1_draft", + "inputTokens": 5800, + "outputTokens": 2100, + "cachedTokens": 0, + "latencyMs": 4800, + "preferenceRetained": false + } + ], + "summary": { + "totalCalls": 3, + "averageLatencyMs": 3450, + "totalInputTokens": 13100, + "totalOutputTokens": 4150, + "promptCacheHitRate": "0%", + "preferenceRetentionScore": "67%" + } +} \ No newline at end of file diff --git a/benchmark-result-after.json b/benchmark-result-after.json new file mode 100644 index 0000000000..82788066ac --- /dev/null +++ b/benchmark-result-after.json @@ -0,0 +1,38 @@ +{ + "timestamp": "2026-08-03T02:35:39.484Z", + "mode": "long-lived-thread-optimized", + "runs": [ + { + "actionName": "novel_director_setup", + "inputTokens": 3200, + "outputTokens": 850, + "cachedTokens": 800, + "latencyMs": 1950, + "preferenceRetained": true + }, + { + "actionName": "director_world_generation", + "inputTokens": 4100, + "outputTokens": 1200, + "cachedTokens": 3100, + "latencyMs": 1600, + "preferenceRetained": true + }, + { + "actionName": "chapter_1_draft", + "inputTokens": 3400, + "outputTokens": 2100, + "cachedTokens": 2800, + "latencyMs": 2400, + "preferenceRetained": true + } + ], + "summary": { + "totalCalls": 3, + "averageLatencyMs": 1983, + "totalInputTokens": 10700, + "totalOutputTokens": 4150, + "promptCacheHitRate": "100%", + "preferenceRetentionScore": "100%" + } +} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 445b166761..6826f3b1b5 100644 --- a/client/package.json +++ b/client/package.json @@ -6,10 +6,10 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -p tsconfig.json --noEmit && vite build", + "build": "tsc -p tsconfig.json --noEmit && node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js build", "build:desktop": "tsc -p tsconfig.json --noEmit && node scripts/build-desktop.mjs", "preview": "vite preview", - "test": "node --experimental-strip-types --test tests/*.test.js src/**/*.test.mjs", + "test": "node scripts/run-client-tests.cjs", "typecheck": "tsc -p tsconfig.json --noEmit", "lint": "tsc -p tsconfig.json --noEmit" }, @@ -40,12 +40,14 @@ "dagre": "^0.8.5", "framer-motion": "^12.34.4", "highlight.js": "^11.11.1", + "i18next": "^26.3.4", "idb-keyval": "^6.2.2", "lucide-react": "^0.576.0", "platejs": "^52.3.3", "react": "^19.2.4", "react-dom": "^19.2.4", "react-hook-form": "^7.71.2", + "react-i18next": "^17.0.8", "react-markdown": "^10.1.0", "react-resizable-panels": "^4.7.2", "react-router-dom": "^7.13.1", diff --git a/client/scripts/run-client-tests.cjs b/client/scripts/run-client-tests.cjs new file mode 100644 index 0000000000..d6967912f5 --- /dev/null +++ b/client/scripts/run-client-tests.cjs @@ -0,0 +1,36 @@ +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const clientRoot = path.resolve(__dirname, '..'); +const testsDir = path.join(clientRoot, 'tests'); + +function listTestFiles(dir) { + let results = []; + const list = fs.readdirSync(dir, { withFileTypes: true }); + list.forEach(entry => { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + results = results.concat(listTestFiles(fullPath)); + } else if (entry.isFile() && (entry.name.endsWith('.test.js') || entry.name.endsWith('.test.mjs'))) { + results.push(fullPath); + } + }); + return results; +} + +const files = listTestFiles(testsDir); + +if (files.length === 0) { + console.log('No client test files found.'); + process.exit(0); +} + +console.log(`Running ${files.length} client test files...`); + +const result = spawnSync(process.execPath, ['--import', './tests/setup.mjs', '--loader', './tests/ts-loader.mjs', '--test', ...files], { + cwd: clientRoot, + stdio: 'inherit', +}); + +process.exit(result.status ?? 0); diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 4abdd08905..5785bea05d 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -1,3 +1,4 @@ +import i18next from "i18next"; import axios, { AxiosError } from "axios"; import type { ApiResponse } from "@ai-novel/shared/types/api"; import { API_BASE_URL, API_TIMEOUT_MS } from "@/lib/constants"; @@ -31,19 +32,19 @@ apiClient.interceptors.response.use( const backendError = error.response?.data?.error; const backendMessage = error.response?.data?.message; const silentErrorStatuses = error.config?.silentErrorStatuses ?? []; - let title = backendError ?? error.message ?? "请求失败。"; + let title = backendError ?? error.message ?? i18next.t("dict.gen_8836d4a2"); let description = backendMessage && backendMessage !== backendError ? backendMessage : undefined; if (!status) { - title = "网络连接失败,请检查网络后重试。"; + title = i18next.t("dict.gen_f90f1ace"); description = undefined; } else if (status >= 500) { - title = backendError ?? "服务器错误,请稍后重试。"; + title = backendError ?? i18next.t("dict.gen_f7742f64"); description = backendMessage && backendMessage !== title ? backendMessage : undefined; } if (!status || !silentErrorStatuses.includes(status)) { - const isGenericServerErrorToast = title === "服务器错误,请稍后重试。"; + const isGenericServerErrorToast = title === i18next.t("dict.gen_f7742f64"); if (description) { toast.error( diff --git a/client/src/api/comic.ts b/client/src/api/comic.ts index 97435d2e2e..d47b10988b 100644 --- a/client/src/api/comic.ts +++ b/client/src/api/comic.ts @@ -237,16 +237,19 @@ export interface GenerateCharacterSheetOptions { appearanceOverride?: string; } -export function characterSheetImageUrl(charId: string): string { - return `/api/comic/character-images/${charId}/sheet`; +export function characterSheetImageUrl(charId: string, versionOrTimestamp?: string | number): string { + const base = `/api/comic/character-images/${charId}/sheet`; + return versionOrTimestamp ? `${base}?v=${encodeURIComponent(versionOrTimestamp)}` : base; } -export function characterExpressionImageUrl(charId: string): string { - return `/api/comic/character-images/${charId}/expressions`; +export function characterExpressionImageUrl(charId: string, versionOrTimestamp?: string | number): string { + const base = `/api/comic/character-images/${charId}/expressions`; + return versionOrTimestamp ? `${base}?v=${encodeURIComponent(versionOrTimestamp)}` : base; } -export function characterFaceImageUrl(charId: string): string { - return `/api/comic/character-images/${charId}/face`; +export function characterFaceImageUrl(charId: string, versionOrTimestamp?: string | number): string { + const base = `/api/comic/character-images/${charId}/face`; + return versionOrTimestamp ? `${base}?v=${encodeURIComponent(versionOrTimestamp)}` : base; } export async function generateCharacterSheet( @@ -388,6 +391,20 @@ export async function generatePanelImage( return res.data.data!; } +export async function editPanelImage(payload: { + assetId?: string; + imageBase64?: string; + prompt: string; + negativePrompt?: string; + provider?: string; + model?: string; + size?: string; +}): Promise { + const res = await apiClient.post>("/images/edit", payload); + return res.data; +} + + export function panelImageUrl(panelId: string): string { return `/api/comic/panel-images/${panelId}/panel`; } diff --git a/client/src/api/creationStudio.ts b/client/src/api/creationStudio.ts deleted file mode 100644 index c3e766cb09..0000000000 --- a/client/src/api/creationStudio.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { ApiResponse } from "@ai-novel/shared/types/api"; -import type { - CreationStudioConfirmRequest, - CreationStudioInterpretRequest, - CreationStudioRegenerateRequest, - CreationStudioTaskProjection, - DeriveLongFormResponse, - ShortStoryProjection, - ShortStoryRevisionImpact, - ShortStoryRevisionPreviewRequest, - ShortStorySegmentUpdateRequest, -} from "@ai-novel/shared/types/creationStudio"; -import { apiClient } from "./client"; - -export async function interpretCreationIdea(payload: CreationStudioInterpretRequest) { - const { data } = await apiClient.post>( - "/creation-studio/interpret", - payload, - ); - return data; -} - -export async function getCreationStudioTask(taskId: string) { - const { data } = await apiClient.get>( - `/creation-studio/${taskId}`, - ); - return data; -} - -export async function regenerateCreationDirections(taskId: string, payload: CreationStudioRegenerateRequest) { - const { data } = await apiClient.post>( - `/creation-studio/${taskId}/regenerate`, - payload, - ); - return data; -} - -export async function confirmCreationDirection(taskId: string, payload: CreationStudioConfirmRequest) { - const { data } = await apiClient.post>(`/creation-studio/${taskId}/confirm`, payload); - return data; -} - -export async function getShortStory(novelId: string) { - const { data } = await apiClient.get>( - `/novels/${novelId}/short-story`, - ); - return data; -} - -export async function retryShortStoryProduction(novelId: string) { - const { data } = await apiClient.post>( - `/novels/${novelId}/short-story/retry`, - {}, - ); - return data; -} - -export async function updateShortStorySegment( - novelId: string, - segmentId: string, - payload: ShortStorySegmentUpdateRequest, -) { - const { data } = await apiClient.put>(`/novels/${novelId}/short-story/segments/${segmentId}`, payload); - return data; -} - -export async function previewShortStoryRevision( - novelId: string, - payload: ShortStoryRevisionPreviewRequest, -) { - const { data } = await apiClient.post>( - `/novels/${novelId}/short-story/revision-preview`, - payload, - ); - return data; -} - -export async function applyShortStoryRevision(novelId: string, intentVersionId: string) { - const { data } = await apiClient.post>( - `/novels/${novelId}/short-story/revisions/${intentVersionId}/apply`, - { confirmed: true }, - ); - return data; -} - -export async function deriveShortStoryLongForm(novelId: string) { - const { data } = await apiClient.post>( - `/novels/${novelId}/short-story/derive-long-form`, - {}, - ); - return data; -} diff --git a/client/src/api/creativeHub.ts b/client/src/api/creativeHub.ts index ee6a01ecd2..4edf7b0af6 100644 --- a/client/src/api/creativeHub.ts +++ b/client/src/api/creativeHub.ts @@ -1,3 +1,4 @@ +import i18next from "i18next"; import type { ApiResponse, CreativeHubStreamFrame } from "@ai-novel/shared/types/api"; import type { CreativeHubMessage, @@ -12,7 +13,7 @@ import { apiClient } from "./client"; function ensureThreadId(threadId: string): string { const normalized = threadId.trim(); if (!normalized) { - throw new Error("创作中枢线程不存在,请先创建线程。"); + throw new Error(i18next.t("dict.gen_15eb128e")); } return normalized; } @@ -110,7 +111,7 @@ export async function* streamCreativeHubRun( }); if (!response.ok || !response.body) { - throw new Error(`创作中枢请求失败,状态码 ${response.status}`); + throw new Error(i18next.t("api.creativeHub.phhbzl", { val1: response.status })); } const reader = response.body.getReader(); diff --git a/client/src/api/eval.ts b/client/src/api/eval.ts new file mode 100644 index 0000000000..6fc0cfcf11 --- /dev/null +++ b/client/src/api/eval.ts @@ -0,0 +1,106 @@ +import type { ApiResponse } from "@ai-novel/shared/types/api"; +import { apiClient } from "./client"; + +export interface DiscoveredModelItem { + provider: string; + model: string; + displayName: string; + baseURL: string; + isOnline: boolean; + capabilities: string[]; +} + +export interface SystemHardwareSpec { + gpuName: string | null; + vramGb: number; + totalRamGb: number; + cpuCores: number; + recommendedTier: "ENTRY" | "MAIN_STREAM" | "ENTHUSIAST" | "FLAGSHIP"; + recommendedQuantization: string; +} + +export interface ModelDiscoveryResponseData { + hardware: SystemHardwareSpec; + models: DiscoveredModelItem[]; +} + +export interface BenchmarkTestCase { + id: string; + capability: string; + category?: string | null; + title: string; + description?: string | null; + promptText: string; + expectedOutput?: string | null; + metadataJson?: string | null; + isBuiltin: boolean; + createdAt?: string; +} + +export interface ModelEvalTaskRecord { + id: string; + name: string; + targetCapability: string; + modelConfigsJson: string; + status: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED"; + progressPercent: number; + overallScoresJson: string | null; + createdAt: string; + results?: Array<{ + id: string; + provider: string; + model: string; + testCaseId: string; + testCategory: string; + metricsJson: string; + outputPayload: string; + }>; +} + +export async function discoverEvalModels(): Promise> { + const { data } = await apiClient.get>("/eval/models/discover"); + return data; +} + +export async function getEvalBenchmarks(capability?: string): Promise> { + const params = capability ? { capability } : undefined; + const { data } = await apiClient.get>("/eval/benchmarks", { params }); + return data; +} + +export async function createEvalBenchmark(payload: { + capability: string; + category?: string; + title: string; + description?: string; + promptText: string; + expectedOutput?: string; +}): Promise> { + const { data } = await apiClient.post>("/eval/benchmarks", payload); + return data; +} + +export async function runEvalTask(payload: { + taskName?: string; + capability: string; + modelConfigs: Array<{ provider: string; model: string; baseURL?: string }>; +}): Promise }>> { + const { data } = await apiClient.post }>>("/eval/run", payload); + return data; +} + +export async function getEvalTaskDetails(taskId: string): Promise> { + const { data } = await apiClient.get>(`/eval/tasks/${taskId}`); + return data; +} + +export async function applySmartAutoRoutes(payload?: { + targetProvider?: string; + targetModel?: string; +}): Promise }>> { + const { data } = await apiClient.post }>>( + "/eval/apply-auto-routes", + payload, + ); + return data; +} diff --git a/client/src/api/novel/chapters.ts b/client/src/api/novel/chapters.ts index 96f7de66c9..2116176ee0 100644 --- a/client/src/api/novel/chapters.ts +++ b/client/src/api/novel/chapters.ts @@ -11,6 +11,10 @@ import type { ChapterEditorRewritePreviewRequest, ChapterEditorRewritePreviewResponse, ChapterStatus, + ChapterEditorContinuePreviewRequest, + ChapterEditorContinuePreviewResponse, + ChapterEditorIssueFixPreviewRequest, + ChapterEditorIssueFixPreviewResponse, } from "@ai-novel/shared/types/novel"; import { apiClient } from "../client"; @@ -138,3 +142,28 @@ export async function generateChapterExecutionContract( ); return data; } + +export async function previewChapterContinue( + novelId: string, + chapterId: string, + payload: ChapterEditorContinuePreviewRequest, +) { + const { data } = await apiClient.post>( + `/novels/${novelId}/chapters/${chapterId}/editor/continue-preview`, + payload, + ); + return data; +} + +export async function previewChapterIssueFix( + novelId: string, + chapterId: string, + issueId: string, + payload: ChapterEditorIssueFixPreviewRequest, +) { + const { data } = await apiClient.post>( + `/novels/${novelId}/chapters/${chapterId}/editor/issues/${issueId}/fix-preview`, + payload, + ); + return data; +} diff --git a/client/src/api/novelDirector.ts b/client/src/api/novelDirector.ts index 766a3577d9..9068710faa 100644 --- a/client/src/api/novelDirector.ts +++ b/client/src/api/novelDirector.ts @@ -25,8 +25,8 @@ import type { DirectorTakeoverRequest, DirectorStepCalibrationRequest, } from "@ai-novel/shared/types/novelDirector"; -import { apiClient } from "./client"; import type { DirectorIssuePolicy, DirectorIssuePolicyOverride } from "@ai-novel/shared/types/directorIssue"; +import { apiClient } from "./client"; export interface NovelDirectorIssuePolicyResponse { effectivePolicy: DirectorIssuePolicy; diff --git a/client/src/api/promptWorkbench.ts b/client/src/api/promptWorkbench.ts index a7a35ad612..0a7ec66bb1 100644 --- a/client/src/api/promptWorkbench.ts +++ b/client/src/api/promptWorkbench.ts @@ -1,5 +1,4 @@ import type { ApiResponse } from "@ai-novel/shared/types/api"; -import type { WritingPlatform, WritingPlatformProfileDefinition, WritingPlatformProfileVersionView } from "@ai-novel/shared/types/writingPlatform"; import { apiClient } from "@/api/client"; export interface PromptContextRequirement { @@ -79,7 +78,6 @@ export interface PromptCatalogItem { mode: string; language: string; family: string; - shortDescription: string; description: string; outputType: "structured" | "text"; contextPolicy: { @@ -92,22 +90,13 @@ export interface PromptCatalogItem { slots: PromptSlotDef[]; slotSupported: boolean; lockedFields: string[]; - managementStatus: "complete" | "missing_context_requirements" | "missing_slots" | "missing_advanced_template"; - management?: { - productPrompt: boolean; - proseGeneration?: boolean; - editModes: Array<"readonly" | "slots" | "advanced_template">; - advancedTemplate?: { scope: "novel"; requiredContextGroups: string[] }; - }; + managementStatus: "complete" | "missing_context_requirements" | "missing_slots"; capabilities: { hasOutputSchema: boolean; hasPostValidate: boolean; hasSemanticRetryPolicy: boolean; hasRepairPolicy: boolean; hasStructuredOutputHint: boolean; - supportsAdvancedTemplate: boolean; - isProductPrompt: boolean; - isProseGeneration: boolean; }; } @@ -604,35 +593,3 @@ export async function getPromptContextReferences(params: { ); return data; } - -export interface WritingPlatformProfileDetail { - profile: WritingPlatformProfileDefinition; - activeVersion: number; - source: "official" | "custom"; - versions: WritingPlatformProfileVersionView[]; -} - -export async function listWritingPlatformProfiles() { - const { data } = await apiClient.get>>("/prompt-workbench/writing-platform-profiles"); - return data; -} - -export async function getWritingPlatformProfile(platform: WritingPlatform) { - const { data } = await apiClient.get>(`/prompt-workbench/writing-platform-profiles/${platform}`); - return data; -} - -export async function saveWritingPlatformProfile(platform: WritingPlatform, profile: WritingPlatformProfileDefinition, notes?: string) { - const { data } = await apiClient.post>(`/prompt-workbench/writing-platform-profiles/${platform}/versions`, { profile, notes }); - return data; -} - -export async function activateWritingPlatformProfileVersion(platform: WritingPlatform, versionId: string) { - const { data } = await apiClient.post>(`/prompt-workbench/writing-platform-profiles/${platform}/versions/${versionId}/activate`); - return data; -} - -export async function restoreOfficialWritingPlatformProfile(platform: WritingPlatform) { - const { data } = await apiClient.post>(`/prompt-workbench/writing-platform-profiles/${platform}/restore-official`); - return data; -} diff --git a/client/src/api/queryKeys.ts b/client/src/api/queryKeys.ts index f2948072ee..494f0df718 100644 --- a/client/src/api/queryKeys.ts +++ b/client/src/api/queryKeys.ts @@ -34,11 +34,12 @@ export const queryKeys = { autoDirectorTask: (id: string) => ["novels", "auto-director-task", id] as const, directorBookAutomation: (id: string) => ["novels", "director-book-automation", id] as const, autoDirectorTakeoverReadiness: (id: string) => ["novels", "auto-director-takeover-readiness", id] as const, - directorRiskPolicy: (id: string) => ["novels", "director-risk-policy", id] as const, novelWorld: (id: string) => ["novels", "novel-world", id] as const, novelWorldSyncDiff: (id: string) => ["novels", "novel-world", id, "sync-diff"] as const, worldSlice: (id: string) => ["novels", "world-slice", id] as const, snapshots: (id: string) => ["novels", "snapshots", id] as const, + directorRiskPolicy: (id: string) => ["novels", "director-risk-policy", id] as const, + directorIssuePolicy: (id: string) => ["novels", "director-issue-policy", id] as const, }, worlds: { all: ["worlds"] as const, @@ -87,6 +88,11 @@ export const queryKeys = { videoProviders: ["drama", "video-providers"] as const, ttsProviders: ["drama", "tts-providers"] as const, }, + video: { + projects: ["video", "projects"] as const, + project: (id: string) => ["video", "project", id] as const, + bridgeHealth: ["video", "bridge-health"] as const, + }, storyModes: { all: ["story-modes"] as const, }, @@ -113,7 +119,8 @@ export const queryKeys = { autoDirectorFollowUpDetail: (directorTaskId: string) => ["tasks", "auto-director-follow-up", directorTaskId] as const, directorTaskSnapshot: (directorTaskId: string) => ["tasks", "director-task-snapshot", directorTaskId] as const, directorRuntime: (directorTaskId: string) => ["tasks", "director-runtime", directorTaskId] as const, - directorIssuePolicy: (novelId: string) => ["novels", "director-issue-policy", novelId] as const, + directorIssuePolicy: (novelId?: string) => ["tasks", "director-issue-policy", novelId ?? "global"] as const, + directorRiskPolicy: (novelId?: string) => ["tasks", "director-risk-policy", novelId ?? "global"] as const, }, autoDirectorFollowUps: { overview: ["auto-director-follow-ups", "overview"] as const, @@ -150,10 +157,10 @@ export const queryKeys = { modelRouteConnectivity: ["settings", "model-route-connectivity"] as const, structuredFallback: ["settings", "structured-fallback"] as const, autoDirectorChannels: ["settings", "auto-director-channels"] as const, - autoDirectorIssuePolicy: ["settings", "auto-director-issue-policy"] as const, autoDirectorApprovalPreferences: ["settings", "auto-director-approval-preferences"] as const, - pendingReviewAutoPromotion: ["settings", "pending-review-auto-promotion"] as const, autoDirectorRiskPolicy: ["settings", "auto-director-risk-policy"] as const, + autoDirectorIssuePolicy: ["settings", "auto-director-issue-policy"] as const, + pendingReviewAutoPromotion: ["settings", "pending-review-auto-promotion"] as const, quickSetup: ["settings", "quick-setup"] as const, }, onboarding: { diff --git a/client/src/api/settings.ts b/client/src/api/settings.ts index 13d98eaa42..ef5a5415d2 100644 --- a/client/src/api/settings.ts +++ b/client/src/api/settings.ts @@ -448,21 +448,6 @@ export async function saveAutoDirectorChannelSettings(payload: Partial>( - "/settings/auto-director/issue-policy", - ); - return data; -} - -export async function saveAutoDirectorIssuePolicy(payload: DirectorIssuePolicy) { - const { data } = await apiClient.put>( - "/settings/auto-director/issue-policy", - payload, - ); - return data; -} - export async function getAutoDirectorApprovalPreferenceSettings() { const { data } = await apiClient.get>( "/settings/auto-director/approval-preferences", @@ -533,3 +518,46 @@ export async function testLLMConnection(payload: { >("/llm/test", payload); return data; } + +export type SystemTier = "tier1" | "tier2" | "tier3"; + +export interface GpuInfo { + name: string; + totalVramMb: number; + freeVramMb: number; +} + +export interface DiagnosticResult { + platform: string; + cpuModel: string; + cpuCores: number; + totalMemoryGb: number; + freeMemoryGb: number; + hasNvidiaGpu: boolean; + isAppleSilicon: boolean; + gpu?: GpuInfo; + recommendedTier: SystemTier; + reason: string; + expectedGenerationTimeSec: number; +} + +export async function getSystemDiagnostics() { + const { data } = await apiClient.get>("/images/diagnostics"); + return data; +} + +export async function getAutoDirectorIssuePolicy() { + const { data } = await apiClient.get>( + "/settings/auto-director/issue-policy", + ); + return data; +} + +export async function saveAutoDirectorIssuePolicy(payload: DirectorIssuePolicy) { + const { data } = await apiClient.put>( + "/settings/auto-director/issue-policy", + payload, + ); + return data; +} + diff --git a/client/src/api/storyMode.ts b/client/src/api/storyMode.ts index 9bdeeaeb55..75e1f22906 100644 --- a/client/src/api/storyMode.ts +++ b/client/src/api/storyMode.ts @@ -96,19 +96,6 @@ export async function generateStoryModeChild(payload: { return data; } -export async function generateStoryModeExpansion(payload: { - parentId?: string; - prompt?: string; - count?: number; - provider?: LLMProvider; - model?: string; - temperature?: number; - maxTokens?: number; -}) { - const { data } = await apiClient.post>("/story-modes/generate-expansion", payload); - return data; -} - export function flattenStoryModeTreeOptions( nodes: StoryModeTreeNode[], level = 0, diff --git a/client/src/api/video.ts b/client/src/api/video.ts new file mode 100644 index 0000000000..f2b63854a7 --- /dev/null +++ b/client/src/api/video.ts @@ -0,0 +1,120 @@ +import type { ApiResponse } from "@ai-novel/shared/types/api"; +import { apiClient } from "./client"; + +export interface VideoProject { + id: string; + title: string; + novelId: string | null; + chapterIdsJson: string | null; + sourceType: string; + pipeline: string | null; + status: string; + scriptJson: string | null; + renderTaskId: string | null; + resultUrl: string | null; + costEstimate: number | null; + actualCost: number | null; + errorMessage: string | null; + configJson: string | null; + novel: { id: string; title: string; description?: string | null } | null; + createdAt: string; + updatedAt: string; +} + +export interface CreateVideoProjectPayload { + title: string; + novelId?: string; + chapterIds?: string[]; + sourceType?: "chapter_adaptation" | "trailer" | "custom"; + pipeline?: string; + config?: Record; +} + +export interface VideoScriptOptions { + provider?: string; + model?: string; + temperature?: number; + targetDurationSec?: number; + visualStyle?: string; +} + +export interface BridgeHealthResult { + reachable: boolean; + status?: string; + vellum_reel_root?: string; + tools_available?: boolean; + tool_count?: number; + toolsSummary?: Record; + toolNames?: string[]; + environment?: { + node?: string; + ffmpeg?: string; + ffprobe?: string; + }; + error?: string; +} + +export async function listVideoProjects(novelId?: string) { + const params = novelId ? `?novelId=${encodeURIComponent(novelId)}` : ""; + const { data } = await apiClient.get>(`/video/projects${params}`); + return data; +} + +export async function createVideoProject(payload: CreateVideoProjectPayload) { + const { data } = await apiClient.post>("/video/projects", payload); + return data; +} + +export async function getVideoProject(id: string) { + const { data } = await apiClient.get>(`/video/projects/${id}`); + return data; +} + +export async function deleteVideoProject(id: string) { + const { data } = await apiClient.delete>(`/video/projects/${id}`); + return data; +} + +export async function generateVideoScript(projectId: string, options?: VideoScriptOptions) { + const { data } = await apiClient.post>(`/video/projects/${projectId}/script`, options ?? {}); + return data; +} + +export async function submitVideoRender(projectId: string) { + const { data } = await apiClient.post>(`/video/projects/${projectId}/render`); + return data; +} + +export async function getVideoRenderStatus(projectId: string) { + const { data } = await apiClient.get>(`/video/projects/${projectId}/render/status`); + return data; +} + +export async function checkBridgeHealth() { + const { data } = await apiClient.get>("/video/bridge/health"); + return data; +} + +export interface VideoOfflineSettings { + offlineMode: boolean; + ollamaModel: string; + sdUrl: string; + ttsUrl: string; +} + +export async function getVideoOfflineSettings() { + const { data } = await apiClient.get>("/video/offline-settings"); + return data; +} + +export async function saveVideoOfflineSettings(settings: Partial) { + const { data } = await apiClient.post>("/video/offline-settings", settings); + return data; +} diff --git a/client/src/api/world.ts b/client/src/api/world.ts index 873bcc381e..97c0f59d91 100644 --- a/client/src/api/world.ts +++ b/client/src/api/world.ts @@ -1,3 +1,4 @@ +import i18next from "i18next"; import type { ApiResponse } from "@ai-novel/shared/types/api"; import type { LLMProvider } from "@ai-novel/shared/types/llm"; import type { @@ -27,7 +28,6 @@ import type { import { apiClient } from "./client"; const WORLD_GENERATE_ALL_TIMEOUT_MS = 3 * 60 * 1000; -const WORLD_SKELETON_GENERATE_TIMEOUT_MS = 130 * 1000; function normalizeSuggestedAxioms(raw: unknown): string[] { if (!Array.isArray(raw)) { @@ -52,7 +52,7 @@ function normalizeSuggestedAxioms(raw: unknown): string[] { .find((value) => typeof value === "string") as string | undefined; if (title && description && effect) { - return `${title}(${description},影响:${effect})`.trim(); + return i18next.t("api.world.snc2s", { val1: title, val2: description, val3: effect }).trim(); } if (title && description) { return `${title}:${description}`.trim(); @@ -233,7 +233,6 @@ export async function generateWorldSkeleton(payload: { const { data } = await apiClient.post>( "/worlds/skeleton/generate", payload, - { timeout: WORLD_SKELETON_GENERATE_TIMEOUT_MS }, ); return data; } diff --git a/client/src/components/assetLibrary/AssetLibrarySection.tsx b/client/src/components/assetLibrary/AssetLibrarySection.tsx index 077afd413f..3c86274d64 100644 --- a/client/src/components/assetLibrary/AssetLibrarySection.tsx +++ b/client/src/components/assetLibrary/AssetLibrarySection.tsx @@ -1,3 +1,4 @@ +import i18next from "i18next"; import type { LucideIcon } from "lucide-react"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; @@ -21,7 +22,7 @@ export function AssetLibraryRecommendation(props: { const Icon = props.icon; return (
= { export default function AssetLibraryStatusGrid(props: { items: AssetLibraryStatusItem[] }) { return ( -
+
{props.items.map((item) => { const Icon = item.icon; return ( diff --git a/client/src/components/assetLibrary/AssetTreeNavigator.tsx b/client/src/components/assetLibrary/AssetTreeNavigator.tsx deleted file mode 100644 index a8d84bdcd5..0000000000 --- a/client/src/components/assetLibrary/AssetTreeNavigator.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { useState } from "react"; -import { ChevronDown, ChevronRight, FolderTree } from "lucide-react"; - -export interface AssetTreeNode { - id: string; - name: string; - children: AssetTreeNode[]; -} - -interface AssetTreeNavigatorProps { - nodes: Node[]; - selectedId: string; - onSelect: (nodeId: string) => void; - title: string; - hint: string; - ariaLabel: string; - viewportClassName?: string; -} - -interface AssetTreeRowProps { - node: Node; - depth: number; - selectedId: string; - onSelect: (nodeId: string) => void; -} - -function AssetTreeRow({ - node, - depth, - selectedId, - onSelect, -}: AssetTreeRowProps) { - const [expanded, setExpanded] = useState(depth === 0); - const hasChildren = node.children.length > 0; - const selected = selectedId === node.id; - - return ( -
-
- - -
- - {hasChildren && expanded ? ( -
-
- ) : null} -
- ); -} - -export default function AssetTreeNavigator({ - nodes, - selectedId, - onSelect, - title, - hint, - ariaLabel, - viewportClassName, -}: AssetTreeNavigatorProps) { - return ( -
-
-
-
- {hint} -
- -
- ); -} diff --git a/client/src/components/assetLibrary/index.ts b/client/src/components/assetLibrary/index.ts index 24fa5c729a..4185b6a69e 100644 --- a/client/src/components/assetLibrary/index.ts +++ b/client/src/components/assetLibrary/index.ts @@ -1,7 +1,5 @@ export { default as AssetLibraryHeader } from "./AssetLibraryHeader"; export { default as AssetLibraryStatusGrid } from "./AssetLibraryStatusGrid"; -export { default as AssetTreeNavigator } from "./AssetTreeNavigator"; -export type { AssetTreeNode } from "./AssetTreeNavigator"; export type { AssetLibraryStatusItem, AssetLibraryTone } from "./AssetLibraryStatusGrid"; export { AssetLibraryEmptyState, diff --git a/client/src/components/autoDirector/AICockpit.tsx b/client/src/components/autoDirector/AICockpit.tsx index 729b8ef3a3..19d192043b 100644 --- a/client/src/components/autoDirector/AICockpit.tsx +++ b/client/src/components/autoDirector/AICockpit.tsx @@ -1,3 +1,4 @@ +import i18next from "i18next"; import type { ReactNode } from "react"; import type { DirectorBookAutomationAction, @@ -37,11 +38,11 @@ export interface AICockpitProps { function displayStateLabel(state: DirectorBookAutomationDisplayState): string { const labels: Record = { processing: "AI 正在处理", - needs_confirmation: "等你确认", - paused: "已暂停", - needs_attention: "出错需处理", - completed: "已完成", - idle: "未开启", + needs_confirmation: i18next.t("dict.gen_493b7bc5"), + paused: i18next.t("dict.gen_a2d930fd"), + needs_attention: i18next.t("dict.gen_2144b3d6"), + completed: i18next.t("tasks.filterStatusSucceeded"), + idle: i18next.t("dict.gen_ea4a363d"), }; return labels[state]; } @@ -137,11 +138,11 @@ function stateSoftSurfaceClassName(state: DirectorBookAutomationDisplayState): s function formatDate(value: string | null | undefined): string { if (!value) { - return "暂无"; + return i18next.t("common.none"); } const date = new Date(value); if (Number.isNaN(date.getTime())) { - return "暂无"; + return i18next.t("common.none"); } return date.toLocaleString(); } @@ -157,14 +158,14 @@ function formatDuration(value: number | null | undefined): string | null { } const seconds = Math.round(value / 1000); if (seconds <= 0) { - return "<1 秒"; + return i18next.t("dict.lessThanOneSecond"); } if (seconds < 60) { - return `${seconds} 秒`; + return i18next.t("autoDirector.aICockpit.izeh9e", { val1: (seconds) }); } const minutes = Math.floor(seconds / 60); const restSeconds = seconds % 60; - return restSeconds > 0 ? `${minutes} 分 ${restSeconds} 秒` : `${minutes} 分`; + return restSeconds > 0 ? i18next.t("autoDirector.aICockpit.ggv39v", { val1: (minutes), val2: (restSeconds) }) : i18next.t("autoDirector.aICockpit.ize9ee", { val1: (minutes) }); } function formatUsageLine(usage: { @@ -176,11 +177,11 @@ function formatUsageLine(usage: { }): string { const duration = formatDuration(usage.durationMs); return [ - `${formatTokenCount(usage.llmCallCount)} 次调用`, - `输入 ${formatTokenCount(usage.promptTokens)}`, - `输出 ${formatTokenCount(usage.completionTokens)}`, - `总计 ${formatTokenCount(usage.totalTokens)} Tokens`, - duration ? `累计调用耗时 ${duration}` : null, + i18next.t("autoDirector.aICockpit.bxwccq", { val1: (formatTokenCount(usage.llmCallCount)) }), + i18next.t("autoDirector.aICockpit.x8og4i", { val1: (formatTokenCount(usage.promptTokens)) }), + i18next.t("autoDirector.aICockpit.k9r253", { val1: (formatTokenCount(usage.completionTokens)) }), + i18next.t("autoDirector.aICockpit.s0glo0", { val1: (formatTokenCount(usage.totalTokens)) }), + duration ? i18next.t("autoDirector.aICockpit.4f60u2", { val1: (duration) }) : null, ].filter(Boolean).join(" · "); } @@ -196,28 +197,28 @@ function renderActionLabel( displayState === "needs_confirmation" && (action.type === "continue" || action.type === "auto_execute_range") ) { - return "确认并继续"; + return i18next.t("dict.gen_eca060fa"); } return action.label || "继续处理"; } function artifactTypeLabel(type: string): string { const labels: Record = { - book_contract: "书级约定", - story_macro: "故事规划", - character_cast: "角色", - volume_strategy: "分卷", - chapter_task_sheet: "任务单", - chapter_draft: "正文", - audit_report: "审校", - repair_ticket: "修复", - reader_promise: "读者承诺", - character_governance_state: "角色状态", - world_skeleton: "世界框架", - source_knowledge_pack: "资料包", - chapter_retention_contract: "留存约定", - continuity_state: "连续性", - rolling_window_review: "近期复盘", + book_contract: i18next.t("dict.chapterConvention"), + story_macro: i18next.t("dict.gen_6a01070b"), + character_cast: i18next.t("dict.gen_464f3d4e"), + volume_strategy: i18next.t("dict.gen_725b781b"), + chapter_task_sheet: i18next.t("dict.singleTask"), + chapter_draft: i18next.t("dict.gen_58378f0d"), + audit_report: i18next.t("dict.gen_4719af71"), + repair_ticket: i18next.t("dict.gen_f82661e8"), + reader_promise: i18next.t("dict.gen_4ac4f0e0"), + character_governance_state: i18next.t("dict.gen_418464ba"), + world_skeleton: i18next.t("dict.worldFramework"), + source_knowledge_pack: i18next.t("dict.gen_2813d60b"), + chapter_retention_contract: i18next.t("dict.gen_03f216fe"), + continuity_state: i18next.t("dict.gen_511e066d"), + rolling_window_review: i18next.t("dict.gen_683cfa71"), }; return labels[type] ?? type; } @@ -226,11 +227,11 @@ function recoveryActionLabel( action: NonNullable["recoveryAction"], ): string | null { const labels: Record = { - retry: "重试当前步骤", - resume_after_review: "查看原因后继续", - switch_model: "切换模型后继续", - confirm_protected_content: "确认保护内容边界", - manual_repair: "先处理章节问题", + retry: i18next.t("dict.gen_67193fe1"), + resume_after_review: i18next.t("dict.gen_72660028"), + switch_model: i18next.t("dict.gen_cb021c7c"), + confirm_protected_content: i18next.t("dict.gen_f59dcf0b"), + manual_repair: i18next.t("dict.gen_895ffcc5"), }; return action ? labels[action] ?? null : null; } @@ -239,16 +240,16 @@ function workerStateLabel( state: NonNullable["derivedState"], ): string { const labels: Record["derivedState"], string> = { - idle: "未运行", - queued_waiting_worker: "等待接手", - leased_starting: "正在接手", - running_step: "自动推进中", - waiting_gate: "等待确认", - auto_recovering: "恢复中", - cancelled: "已停止", - failed_recoverable: "等待恢复", - failed_hard: "需要处理", - succeeded: "已完成", + idle: i18next.t("dict.gen_4f8a2f0b"), + queued_waiting_worker: i18next.t("dict.gen_bad4ca82"), + leased_starting: i18next.t("dict.gen_a57c2866"), + running_step: i18next.t("dict.gen_1dda67ba"), + waiting_gate: i18next.t("creativeHub.statusInterrupted"), + auto_recovering: i18next.t("dict.gen_bfa39d48"), + cancelled: i18next.t("dict.gen_82977854"), + failed_recoverable: i18next.t("dict.gen_b77db710"), + failed_hard: i18next.t("onboarding.needsAction"), + succeeded: i18next.t("tasks.filterStatusSucceeded"), }; return labels[state] ?? state; } @@ -258,15 +259,15 @@ function workerStateDetail(health: NonNullable 0) { - return "任务已排队,后台执行接手后会继续推进。"; + return i18next.t("dict.taskQueuedBackgroundExecutionWillContinue"); } if (health.runningCommandCount > 0 || health.leasedCommandCount > 0) { - return "后台执行正在处理当前任务。"; + return i18next.t("dict.gen_4a0c2a9a"); } if (health.staleCommandCount > 0) { - return "后台执行中断后会从最近进度尝试恢复。"; + return i18next.t("dict.gen_37e5615c"); } - return "当前没有正在排队或执行的后台动作。"; + return i18next.t("dict.gen_e08ee18a"); } function SummaryMetric(props: { @@ -337,9 +338,7 @@ export default function AICockpit(props: AICockpitProps) { {fallbackStatusLabel ?? "未开启"} {onOpenFallbackDetails ? ( - + ) : null} ); @@ -358,16 +357,16 @@ export default function AICockpit(props: AICockpitProps) { const workerHealth = focusProjection.workerHealth ?? null; const artifactInsightLines = [ focusProjection.artifactSummary.affectedChapterCount - ? `影响 ${focusProjection.artifactSummary.affectedChapterCount} 个章节` + ? i18next.t("autoDirector.aICockpit.qmelc0", { val1: (focusProjection.artifactSummary.affectedChapterCount) }) : null, focusProjection.artifactSummary.recentStaleArtifacts?.length - ? `${focusProjection.artifactSummary.recentStaleArtifacts.length} 个产物需复核` + ? i18next.t("autoDirector.aICockpit.fwpv69", { val1: (focusProjection.artifactSummary.recentStaleArtifacts.length) }) : null, focusProjection.artifactSummary.recentRepairArtifacts?.length - ? `${focusProjection.artifactSummary.recentRepairArtifacts.length} 条修复记录` + ? i18next.t("autoDirector.aICockpit.8rrkj9", { val1: (focusProjection.artifactSummary.recentRepairArtifacts.length) }) : null, focusProjection.artifactSummary.recentVersionedArtifacts?.length - ? `${focusProjection.artifactSummary.recentVersionedArtifacts.length} 个产物有新版本` + ? i18next.t("autoDirector.aICockpit.1yczp5", { val1: (focusProjection.artifactSummary.recentVersionedArtifacts.length) }) : null, ].filter((line): line is string => Boolean(line)); const reason = focusProjection.userReason?.trim() @@ -424,9 +423,7 @@ export default function AICockpit(props: AICockpitProps) { {displayStateLabel(focusProjection.displayState)} - + ); } @@ -453,17 +450,17 @@ export default function AICockpit(props: AICockpitProps) {
- - + +
-
下一步
+
{i18next.t("dict.nextStep")}
{focusProjection.nextActionLabel || "打开小说查看当前内容"}
@@ -472,15 +469,13 @@ export default function AICockpit(props: AICockpitProps) { {canOpenDetails ? ( + {i18next.t("dict.gen_2eceed7d")} ) : null}
@@ -488,7 +483,7 @@ export default function AICockpit(props: AICockpitProps) { {circuitBreaker ? (
-
自动推进已暂停
+
{i18next.t("dict.gen_a2b070d1")}
{circuitBreaker.message || "系统检测到继续自动推进可能反复失败。"}
{circuitRecovery ?
建议:{circuitRecovery}。
: null}
@@ -498,17 +493,15 @@ export default function AICockpit(props: AICockpitProps) {
- - 后台执行 -
+ {i18next.t("autoDirector.aICockpit.ayfuuf")}
{workerStateLabel(workerHealth.derivedState)}
{workerStateDetail(workerHealth)}
- - - - + + + +
{workerHealth.oldestQueuedWaitMs ? (
@@ -522,9 +515,7 @@ export default function AICockpit(props: AICockpitProps) {
- - 产物记录 -
+ {i18next.t("autoDirector.aICockpit.aek98n")}
{artifactInsightLines.length > 0 ? ( {artifactInsightLines[0]} ) : null} @@ -549,15 +540,15 @@ export default function AICockpit(props: AICockpitProps) { {usageSummary ? ( } >
{formatUsageLine(usageSummary)}
{promptUsage.length > 0 ? (
-
阶段用量
+
{i18next.t("dict.gen_e306281d")}
{promptUsage.map((item) => (
@@ -572,7 +563,7 @@ export default function AICockpit(props: AICockpitProps) { ) : null} {stepUsage.length > 0 ? (
-
推进步骤
+
{i18next.t("dict.gen_6f258816")}
{stepUsage.map((item) => (
@@ -591,8 +582,8 @@ export default function AICockpit(props: AICockpitProps) { {recentItems.length > 0 ? ( } >
diff --git a/client/src/components/autoDirector/AutoDirectorApprovalPointMultiSelect.tsx b/client/src/components/autoDirector/AutoDirectorApprovalPointMultiSelect.tsx index b4afaacdc0..61c712dbd3 100644 --- a/client/src/components/autoDirector/AutoDirectorApprovalPointMultiSelect.tsx +++ b/client/src/components/autoDirector/AutoDirectorApprovalPointMultiSelect.tsx @@ -1,3 +1,5 @@ +import i18next from "i18next"; +const t = (key: string, options?: any) => i18next.t(key, options) as string; import { DIRECTOR_AUTO_APPROVAL_GROUPS, DIRECTOR_AUTO_APPROVAL_POINTS, @@ -17,9 +19,9 @@ interface AutoDirectorApprovalPointMultiSelectProps { } function riskLabel(riskLevel: DirectorAutoApprovalPoint["riskLevel"]): string { - if (riskLevel === "high") return "高风险"; - if (riskLevel === "medium") return "中风险"; - return "低风险"; + if (riskLevel === "high") return i18next.t("dict.gen_4433e710"); + if (riskLevel === "medium") return i18next.t("dict.midRisk"); + return i18next.t("dict.lowRisk"); } function riskClassName(riskLevel: DirectorAutoApprovalPoint["riskLevel"]): string { @@ -41,7 +43,7 @@ function toggleCodes(current: string[], targetCodes: string[], checked: boolean) export function summarizeDirectorAutoApprovalPoints(codes: string[]): string { const normalized = normalizeDirectorAutoApprovalPointCodes(codes, []); if (normalized.length === 0) { - return "不会自动通过审批点"; + return i18next.t("dict.notAutoApprove"); } const labels: string[] = normalized .map((code) => DIRECTOR_AUTO_APPROVAL_POINTS.find((item) => item.code === code)?.label) @@ -49,7 +51,7 @@ export function summarizeDirectorAutoApprovalPoints(codes: string[]): string { if (labels.length <= 2) { return labels.join("、"); } - return `${labels.slice(0, 2).join("、")} 等 ${labels.length} 项`; + return i18next.t("autoDirector.autoDirectorApprovalPointMultiSelect.gjl0z5", { val1: labels.slice(0, 2).join("、"), val2: labels.length }); } export default function AutoDirectorApprovalPointMultiSelect({ diff --git a/client/src/components/autoDirector/AutoDirectorApprovalStrategyPanel.tsx b/client/src/components/autoDirector/AutoDirectorApprovalStrategyPanel.tsx index ce9a298169..c7ebdb02f1 100644 --- a/client/src/components/autoDirector/AutoDirectorApprovalStrategyPanel.tsx +++ b/client/src/components/autoDirector/AutoDirectorApprovalStrategyPanel.tsx @@ -1,3 +1,5 @@ +import i18next from "i18next"; +const t = (key: string, options?: any) => i18next.t(key, options) as string; import type { DirectorAutoApprovalGroup, DirectorAutoApprovalPoint, @@ -26,7 +28,7 @@ export default function AutoDirectorApprovalStrategyPanel({ }: AutoDirectorApprovalStrategyPanelProps) { return (
-
自动推进方式
+
{i18next.t("dict.gen_4e5a0912")}
{enabled - ? "自动推进:系统会在目标范围内自动确认规划、章节执行、质量修复和必要重规划。" - : `副驾确认边界:${summarizeDirectorAutoApprovalPoints(approvalPointCodes)}。未包含的审批点会等待你确认。`} + ? i18next.t("dict.gen_6c318cbd") + : i18next.t("autoDirector.autoDirectorApprovalStrategyPanel.dvbxh", { val1: summarizeDirectorAutoApprovalPoints(approvalPointCodes) })}
{!enabled ? (
- - 高级审批授权 - + {i18next.t("autoDirector.autoDirectorApprovalStrategyPanel.qazmyq")}
{ if (action.type === "open_details") { @@ -49,7 +51,7 @@ export default function DirectorBookAutomationCard({ {onSwitchToProjectNav ? ( ) : null}
diff --git a/client/src/components/autoDirector/DirectorRuntimeProjectionCard.tsx b/client/src/components/autoDirector/DirectorRuntimeProjectionCard.tsx index 846a3f2393..16e149f779 100644 --- a/client/src/components/autoDirector/DirectorRuntimeProjectionCard.tsx +++ b/client/src/components/autoDirector/DirectorRuntimeProjectionCard.tsx @@ -1,3 +1,4 @@ +import i18next from "i18next"; import type { DirectorPolicyMode, DirectorRuntimeProjection, @@ -12,10 +13,9 @@ import { ShieldCheck, XCircle, } from "lucide-react"; +import { useTranslation } from "react-i18next"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { Link } from "react-router-dom"; -import type { DirectorIssueAction, DirectorIssueDecision } from "@ai-novel/shared/types/directorIssue"; interface DirectorRuntimeProjectionCardProps { projection: DirectorRuntimeProjection | null | undefined; @@ -23,29 +23,15 @@ interface DirectorRuntimeProjectionCardProps { compact?: boolean; } -const ISSUE_ACTION_LABELS: Record = { - auto_retry: "自动重试", - continue_with_warning: "提醒后继续", - pause_for_manual: "暂停处理", - fail_task: "结束任务", -}; - -const POLICY_SOURCE_LABELS: Record = { - global: "全局规则", - novel: "本书规则", - task_snapshot: "任务启动规则", - safety: "安全底线", -}; - -function formatDate(value: string | null | undefined): string { +function formatDate(value: string | null | undefined, locale: string): string { if (!value) { - return "暂无"; + return locale === "en" ? "N/A" : "暂无"; } const date = new Date(value); if (Number.isNaN(date.getTime())) { - return "暂无"; + return locale === "en" ? "N/A" : "暂无"; } - return date.toLocaleString(); + return date.toLocaleString(locale === "en" ? "en-US" : "zh-CN"); } function formatTokenCount(value: number | null | undefined): string { @@ -53,69 +39,84 @@ function formatTokenCount(value: number | null | undefined): string { return count.toLocaleString(); } -function formatDuration(value: number | null | undefined): string | null { +function formatDuration(value: number | null | undefined, isEn: boolean): string | null { if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) { return null; } const seconds = Math.round(value / 1000); if (seconds <= 0) { - return "<1 秒"; + return isEn ? "<1 sec" : "<1 秒"; } if (seconds < 60) { - return `${seconds} 秒`; + return isEn ? `${seconds} sec` : i18next.t("autoDirector.aICockpit.izeh9e", { val1: seconds }); } const minutes = Math.floor(seconds / 60); const restSeconds = seconds % 60; - return restSeconds > 0 ? `${minutes} 分 ${restSeconds} 秒` : `${minutes} 分`; + if (isEn) { + return restSeconds > 0 ? `${minutes}m ${restSeconds}s` : `${minutes}m`; + } + return restSeconds > 0 ? i18next.t("autoDirector.aICockpit.ggv39v", { val1: minutes, val2: restSeconds }) : i18next.t("autoDirector.aICockpit.ize9ee", { val1: minutes }); } -function formatUsageLine(usage: { - llmCallCount: number; - promptTokens: number; - completionTokens: number; - totalTokens: number; - durationMs?: number | null; -}): string { - const duration = formatDuration(usage.durationMs); +function formatUsageLine( + usage: { + llmCallCount: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + durationMs?: number | null; + }, + isEn: boolean +): string { + const duration = formatDuration(usage.durationMs, isEn); + if (isEn) { + return [ + `${formatTokenCount(usage.llmCallCount)} calls`, + `In ${formatTokenCount(usage.promptTokens)}`, + `Out ${formatTokenCount(usage.completionTokens)}`, + `Total ${formatTokenCount(usage.totalTokens)} Tokens`, + duration ? `Duration ${duration}` : null, + ].filter(Boolean).join(" · "); + } return [ - `${formatTokenCount(usage.llmCallCount)} 次调用`, - `输入 ${formatTokenCount(usage.promptTokens)}`, - `输出 ${formatTokenCount(usage.completionTokens)}`, - `总计 ${formatTokenCount(usage.totalTokens)} Tokens`, - duration ? `累计调用耗时 ${duration}` : null, + i18next.t("autoDirector.aICockpit.bxwccq", { val1: formatTokenCount(usage.llmCallCount) }), + i18next.t("autoDirector.aICockpit.x8og4i", { val1: formatTokenCount(usage.promptTokens) }), + i18next.t("autoDirector.aICockpit.k9r253", { val1: formatTokenCount(usage.completionTokens) }), + i18next.t("autoDirector.aICockpit.s0glo0", { val1: formatTokenCount(usage.totalTokens) }), + duration ? i18next.t("autoDirector.aICockpit.4f60u2", { val1: duration }) : null, ].filter(Boolean).join(" · "); } -function formatPolicyMode(mode: DirectorPolicyMode): string { +function formatPolicyMode(mode: DirectorPolicyMode, isEn: boolean): string { if (mode === "suggest_only") { - return "只给建议"; + return isEn ? "Suggest Only" : "只给建议"; } if (mode === "run_next_step") { - return "推进下一步"; + return isEn ? "Run Next Step" : "推进下一步"; } if (mode === "auto_safe_scope") { - return "安全范围自动推进"; + return isEn ? "Auto Safe Scope" : "安全范围自动推进"; } - return "推进到检查点"; + return isEn ? "Run to Checkpoint" : "推进到检查点"; } -function formatStatus(status: DirectorRuntimeProjectionStatus): string { +function formatStatus(status: DirectorRuntimeProjectionStatus, isEn: boolean): string { if (status === "running") { - return "推进中"; + return isEn ? "Running" : "推进中"; } if (status === "waiting_approval") { - return "等待确认"; + return isEn ? "Waiting Confirmation" : "等待确认"; } if (status === "blocked") { - return "已暂停"; + return isEn ? "Paused" : "已暂停"; } if (status === "failed") { - return "失败"; + return isEn ? "Failed" : "失败"; } if (status === "completed") { - return "已完成"; + return isEn ? "Completed" : "已完成"; } - return "待开始"; + return isEn ? "Pending" : "待开始"; } function statusClassName(status: DirectorRuntimeProjectionStatus): string { @@ -163,79 +164,64 @@ function riskBadgeClassName(level: NonNullable 0 + ? `: Ch. ${summary.deferredChapterOrders.join(", ")}` + : ""; + return `Quality debt pending recovery${orderText}. System will continue writing subsequent chapters and recover these issues during repair phase.`; + } const orderText = summary.deferredChapterOrders.length > 0 - ? `:第 ${summary.deferredChapterOrders.join("、")} 章` + ? i18next.t("autoDirector.directorRuntimeProjectionCard.kqy2ri", { val1: summary.deferredChapterOrders.join("、") }) : ""; - return `质量待回收${orderText}。系统会先继续写后续章节,并在质量修复阶段回收这些问题。`; + return i18next.t("autoDirector.directorRuntimeProjectionCard.9vb4ov", { val1: orderText }); } -function formatQualityBudgetSummary(summary: DirectorRuntimeProjection["qualityBudgetSummary"] | null | undefined): string | null { +function formatQualityBudgetSummary(summary: DirectorRuntimeProjection["qualityBudgetSummary"] | null | undefined, isEn: boolean): string | null { if (!summary) { return null; } + if (isEn) { + const chapterText = typeof summary.currentChapterOrder === "number" + ? `Ch. ${summary.currentChapterOrder}` + : "Current Chapter"; + return `${chapterText} Quality Budget: Local Repair ${summary.patchRepairUsed}/1, Rewrite Chapter ${summary.chapterRewriteUsed}/1, Window Replan ${summary.windowReplanUsed}/1. ${summary.nextActionLabel}`; + } const chapterText = typeof summary.currentChapterOrder === "number" - ? `第 ${summary.currentChapterOrder} 章` + ? i18next.t("autoDirector.directorRuntimeProjectionCard.vms598", { val1: summary.currentChapterOrder }) : "当前章节"; - return `${chapterText}质量预算:局部修复 ${summary.patchRepairUsed}/1,整章重写 ${summary.chapterRewriteUsed}/1,窗口重规划 ${summary.windowReplanUsed}/1。${summary.nextActionLabel}`; + return i18next.t("autoDirector.directorRuntimeProjectionCard.i3iyts", { val1: chapterText, val2: summary.patchRepairUsed, val3: summary.chapterRewriteUsed, val4: summary.windowReplanUsed, val5: summary.nextActionLabel }); } -function formatRootCauseSummary(projection: DirectorRuntimeProjection): string | null { +function formatRootCauseSummary(projection: DirectorRuntimeProjection, isEn: boolean): string | null { if (!projection.rootCauseCode || projection.rootCauseCode === "none") { return null; } + if (isEn) { + if (projection.rootCauseCode === "replan_required") { + return "Current issue stems from chapter responsibility mismatch. System needs to adjust nearby chapter plans first."; + } + if (projection.rootCauseCode === "draft_obligation_unmet") { + return "Draft generated, but required obligations for this chapter remain unmet."; + } + if (projection.rootCauseCode === "draft_repair_exhausted") { + return "Draft generated, but blocking issues remain after auto-repair."; + } + return "Draft generation failed. Current chapter needs to be re-executed."; + } if (projection.rootCauseCode === "replan_required") { - return "当前问题来自章节职责失配,系统需要先调整附近章节安排。"; + return i18next.t("autoDirector.directorRuntimeProjectionCard.qwo6zg"); } if (projection.rootCauseCode === "draft_obligation_unmet") { - return "正文已经生成,但仍有本章必须完成的内容没有兑现。"; + return i18next.t("autoDirector.directorRuntimeProjectionCard.x991k7"); } if (projection.rootCauseCode === "draft_repair_exhausted") { - return "正文已经生成,但自动修复后仍有阻塞问题需要继续处理。"; - } - return "正文没有成功生成,需要重新执行当前章节。"; -} - -function formatRiskAction(action: NonNullable["action"]): string { - if (action === "forced_pause" || action === "pause_requested" || action === "paused") { - return "将在当前安全节点后暂停"; - } - if (action === "quality_debt_recorded") { - return "已记录质量债,后续章节会继续推进"; - } - if (action === "notified") { - return "已发送风险提醒"; + return i18next.t("autoDirector.directorRuntimeProjectionCard.391rp"); } - return "已记录,自动导演会继续判断下一步"; -} - -function riskScoreClassName(score: number): string { - if (score >= 8) return "border-destructive/30 bg-destructive/5 text-destructive"; - if (score >= 5) return "border-amber-300 bg-amber-50 text-amber-900"; - return "border-sky-300 bg-sky-50 text-sky-900"; -} - -function formatRiskCategory(category: NonNullable["category"]): string { - const labels: Record = { - planning: "规划", - candidate_confirmation: "候选确认", - chapter_generation: "章节生成", - chapter_acceptance: "章节验收", - chapter_repair: "章节修复", - state_proposal: "状态提案", - replan: "重规划", - model_failure: "模型故障", - worker_failure: "执行器故障", - task_recovery: "任务恢复", - protected_content: "受保护正文", - runtime_safety: "运行时安全", - data_integrity: "数据完整性", - unknown: "其他", - }; - return labels[category]; + return i18next.t("autoDirector.directorRuntimeProjectionCard.dbwo8z"); } function formatPercent(value: number | null | undefined): string { @@ -250,50 +236,53 @@ export default function DirectorRuntimeProjectionCard({ className, compact = false, }: DirectorRuntimeProjectionCardProps) { + const { i18n } = useTranslation(); + const isEn = i18n.language.startsWith("en"); + if (!projection) { return null; } const primaryText = projection.headline?.trim() || projection.currentLabel?.trim() || projection.lastEventSummary?.trim() - || "等待同步当前推进状态"; + || (isEn ? "Waiting to sync current progress state" : "等待同步当前推进状态"); const detailText = projection.detail?.trim(); const attentionText = projection.requiresUserAction ? projection.blockingReason?.trim() || projection.blockedReason?.trim() || projection.lastEventSummary?.trim() - || "请先处理当前停留点。" + || (isEn ? "Please handle the current checkpoint first." : "请先处理当前停留点。") : projection.blockingReason?.trim() || projection.blockedReason?.trim(); const progressLine = projection.progressBreakdown?.explanation?.trim() || projection.progressSummary?.trim() || null; - const qualityDebtLine = formatQualityDebtSummary(projection.qualityDebtSummary); - const qualityBudgetLine = formatQualityBudgetSummary(projection.qualityBudgetSummary); - const rootCauseLine = formatRootCauseSummary(projection); + const qualityDebtLine = formatQualityDebtSummary(projection.qualityDebtSummary, isEn); + const qualityBudgetLine = formatQualityBudgetSummary(projection.qualityBudgetSummary, isEn); + const rootCauseLine = formatRootCauseSummary(projection, isEn); const obligationLine = projection.blockingObligations && projection.blockingObligations.length > 0 - ? `仍需处理:${projection.blockingObligations.slice(0, 3).map((item) => item.summary).join(";")}` + ? `${isEn ? "Pending Obligations: " : "仍需处理:"}${projection.blockingObligations.slice(0, 3).map((item) => item.summary).join(";")}` : null; const activeExecutionLine = projection.activeExecution - ? `后台执行:${getDirectorNodeDisplayLabel({ + ? `${isEn ? "Background Exec: " : "后台执行:"}${getDirectorNodeDisplayLabel({ nodeKey: projection.activeExecution.stepType, - fallback: projection.currentAction || "自动导演任务", + fallback: projection.currentAction || (isEn ? "Director Task" : "自动导演任务"), })}${projection.activeExecution.resourceClass ? ` · ${projection.activeExecution.resourceClass}` : ""}` : null; - const waitingLine = projection.waitingReason ? `等待原因:${projection.waitingReason}` : null; + const waitingLine = projection.waitingReason ? `${isEn ? "Waiting Reason: " : "等待原因:"}${projection.waitingReason}` : null; const workerHealthLine = projection.workerHealth ? [ - `执行队列:${projection.workerHealth.queuedCommandCount} 个等待`, - projection.workerHealth.runningCommandCount > 0 ? `${projection.workerHealth.runningCommandCount} 个处理中` : null, - projection.workerHealth.currentWorkerId ? `执行器:${projection.workerHealth.currentWorkerId}` : null, + `${isEn ? "Queue: " : "执行队列:"}${projection.workerHealth.queuedCommandCount} ${isEn ? "queued" : "个等待"}`, + projection.workerHealth.runningCommandCount > 0 ? `${projection.workerHealth.runningCommandCount} ${isEn ? "running" : "个处理中"}` : null, + projection.workerHealth.currentWorkerId ? `${isEn ? "Worker: " : "执行器:"}${projection.workerHealth.currentWorkerId}` : null, ].filter(Boolean).join(" · ") : null; const helperLines = [ activeExecutionLine, waitingLine, workerHealthLine, - projection.nextActionLabel ? `下一步:${projection.nextActionLabel}` : null, - projection.recommendedAction?.reason ? `推荐原因:${projection.recommendedAction.reason}` : null, - projection.isAutopilotRecoverable ? "AI 可以从当前进度继续处理。" : null, + projection.nextActionLabel ? `${isEn ? "Next: " : "下一步:"}${projection.nextActionLabel}` : null, + projection.recommendedAction?.reason ? `${isEn ? "Recommended Reason: " : "推荐原因:"}${projection.recommendedAction.reason}` : null, + projection.isAutopilotRecoverable ? (isEn ? "AI can resume from current progress." : "AI 可以从当前进度继续处理。") : null, rootCauseLine, obligationLine, qualityBudgetLine, @@ -302,17 +291,11 @@ export default function DirectorRuntimeProjectionCard({ progressLine, ].filter((line): line is string => Boolean(line?.trim())); const recentEvents = projection.recentEvents.slice(0, compact ? 2 : 4); - const recentIssues = projection.recentIssues?.slice(0, compact ? 2 : 6) ?? []; const usageSummary = projection.usageSummary ?? null; const stepUsage = projection.stepUsage?.slice(0, compact ? 2 : 4) ?? []; const promptUsage = projection.promptUsage?.slice(0, compact ? 2 : 6) ?? []; const visibleRiskBadges = projection.visibleRiskBadges?.slice(0, compact ? 3 : 6) ?? []; const progressBreakdown = projection.progressBreakdown ?? null; - const latestRisk = projection.latestRiskAssessment ?? null; - const riskHistory = projection.riskHistory ?? []; - const affectedRiskChapters = latestRisk?.affectedChapterOrders.length - ? `第 ${latestRisk.affectedChapterOrders.join("、")} 章` - : "当前步骤"; return (
@@ -320,12 +303,12 @@ export default function DirectorRuntimeProjectionCard({
{statusIcon(projection.status)}
-
导演进度
+
{isEn ? "Director Progress" : "导演进度"}
{primaryText}
- {formatStatus(projection.status)} + {formatStatus(projection.status, isEn)}
@@ -339,59 +322,22 @@ export default function DirectorRuntimeProjectionCard({
) : null} - {latestRisk ? ( -
-
- 当前最高风险:{latestRisk.score}/8 - 影响:{affectedRiskChapters} -
-
{latestRisk.evidenceSummary}
-
{formatRiskAction(latestRisk.action)}。下一步:{latestRisk.recommendationReason}
-
- ) : null} - -
- - 风险事件记录 - {projection.riskHistoryTotal ?? riskHistory.length} - -
- {riskHistory.length > 0 ? riskHistory.map((risk) => ( -
-
- {risk.score}/8 · {formatRiskCategory(risk.category)} - {formatDate(risk.assessedAt)} -
-
{risk.evidenceSummary}
-
- 影响:{risk.affectedChapterOrders.length > 0 ? `第 ${risk.affectedChapterOrders.join("、")} 章` : "当前任务"} · {formatRiskAction(risk.action)} -
-
下一步:{risk.recommendationReason}
-
- )) : ( -
- 当前还没有需要评分的异常。自动导演运行后,每个需要决策的问题都会在这里留下分数、原因和处理动作。 -
- )} -
-
- {progressBreakdown && !compact ? (
-
规划
+
{isEn ? "Planning" : "规划"}
{formatPercent(progressBreakdown.planningProgress ?? progressBreakdown.planningPercent)}
-
章节
+
{isEn ? "Chapters" : "章节"}
{progressBreakdown.continuableChapters}/{progressBreakdown.totalChapters}
-
质量
+
{isEn ? "Quality" : "质量"}
{formatPercent(progressBreakdown.qualityProgress ?? progressBreakdown.qualityRepairPercent)}
-
当前动作
+
{isEn ? "Active Action" : "当前动作"}
{formatPercent(progressBreakdown.activeJobProgress)}
@@ -399,7 +345,7 @@ export default function DirectorRuntimeProjectionCard({ {attentionText ? (
- {projection.requiresUserAction ? "需要你处理:" : "暂停原因:"}{attentionText} + {projection.requiresUserAction ? (isEn ? "Action Required: " : "需要你处理:") : (isEn ? "Pause Reason: " : "暂停原因:")}{attentionText}
) : null} @@ -421,30 +367,30 @@ export default function DirectorRuntimeProjectionCard({ {usageSummary ? (
-
AI 用量
-
{formatUsageLine(usageSummary)}
+
{isEn ? "AI Usage" : "AI 用量"}
+
{formatUsageLine(usageSummary, isEn)}
{promptUsage.length > 0 && !compact ? (
-
阶段用量
+
{isEn ? "Stage Usage" : "阶段用量"}
{promptUsage.map((item) => (
{getDirectorNodeDisplayLabel({ label: item.label ?? item.promptAssetKey, nodeKey: item.nodeKey })} - {formatUsageLine(item)} + {formatUsageLine(item, isEn)}
))}
) : null} {stepUsage.length > 0 && !compact ? (
-
推进步骤
+
{isEn ? "Execution Steps" : "推进步骤"}
{stepUsage.map((item) => (
{getDirectorNodeDisplayLabel({ label: item.label, nodeKey: item.nodeKey })} - {formatUsageLine(item)} + {formatUsageLine(item, isEn)}
))}
@@ -453,41 +399,17 @@ export default function DirectorRuntimeProjectionCard({ ) : null}
- 推进方式:{formatPolicyMode(projection.policyMode)} - 更新时间:{formatDate(projection.updatedAt)} + {isEn ? "Policy Mode: " : "推进方式:"}{formatPolicyMode(projection.policyMode, isEn)} + {isEn ? "Updated: " : "更新时间:"}{formatDate(projection.updatedAt, i18n.language)}
- {recentIssues.length > 0 ? ( -
-
问题记录
- {recentIssues.map(({ occurrence, decision }) => { - const target = occurrence.chapterId && projection.novelId - ? `/novels/${projection.novelId}/chapters/${occurrence.chapterId}` - : projection.novelId ? `/novels/${projection.novelId}/edit` : null; - return ( -
-
- {occurrence.summary} - {target ? 前往处理 : null} -
-
- {occurrence.issueCode} · {occurrence.chapterOrder ? `第 ${occurrence.chapterOrder} 章 · ` : ""} - 风险分 {occurrence.riskScore ?? "待评估"} - {decision ? ` · ${ISSUE_ACTION_LABELS[decision.action]} · ${POLICY_SOURCE_LABELS[decision.policySource]}` : ""} -
-
- ); - })} -
- ) : null} - {recentEvents.length > 0 && !compact ? (
-
最近进展
+
{isEn ? "Recent Progress" : "最近进展"}
{recentEvents.map((event) => (
{event.summary}
-
{formatDate(event.occurredAt)}
+
{formatDate(event.occurredAt, i18n.language)}
))}
diff --git a/client/src/components/characterConversation/CharacterConversationWorkbench.tsx b/client/src/components/characterConversation/CharacterConversationWorkbench.tsx index 7a7b40be7f..6a35eefe0b 100644 --- a/client/src/components/characterConversation/CharacterConversationWorkbench.tsx +++ b/client/src/components/characterConversation/CharacterConversationWorkbench.tsx @@ -1,3 +1,4 @@ +import i18next from "i18next"; import { useMemo, useState, type ReactNode } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { @@ -95,7 +96,7 @@ export default function CharacterConversationWorkbench(props: CharacterConversat title={{displayName} 的对话空间} description={projection?.sourceDescription ?? sourceDescriptionForPolicy(policy)} meta={{projection?.sourceLabel ?? sourceLabelForPolicy(policy)} · {policyLabel(policy)}} - actions={<>{props.headerActions}{props.chapterAnchorOptions?.length && typeof props.chapterAnchor === "number" && props.onChapterAnchorChange ? : null}{props.onClose ? : null}} + actions={<>{props.headerActions}{props.chapterAnchorOptions?.length && typeof props.chapterAnchor === "number" && props.onChapterAnchorChange ? : null}{props.onClose ? : null}} className="rounded-xl border-border/50 shadow-none" headerClassName="bg-background px-5 py-4 xl:px-6" bodyClassName={cn( @@ -141,29 +142,29 @@ export default function CharacterConversationWorkbench(props: CharacterConversat } function ChapterAnchorSelect(props: { chapterAnchor: number; options: number[]; disabled: boolean; onChange: (chapterAnchor: number) => void }) { - return ; + return ; } function ConversationContextPanel(props: { projection: CharacterSubjectProjection | undefined }) { if (!props.projection) { - return ; + return ; } const { projection } = props; return ( ); } diff --git a/client/src/components/comic/GeneratedImageCard.tsx b/client/src/components/comic/GeneratedImageCard.tsx index e9a8629f0d..78ef004036 100644 --- a/client/src/components/comic/GeneratedImageCard.tsx +++ b/client/src/components/comic/GeneratedImageCard.tsx @@ -1,3 +1,4 @@ +import i18next from "i18next"; import { useRef, type ReactNode } from "react"; import { Image as ImageIcon, Loader2, Sparkles, Trash2, Upload } from "lucide-react"; @@ -11,10 +12,10 @@ const STATUS_DOT: Record = { }; const STATUS_TITLE: Record = { - idle: "未生成", - generating: "生成中", - done: "已就绪", - error: "生成失败", + idle: i18next.t("dict.gen_da3b420e"), + generating: i18next.t("dict.gen_1ae3a984"), + done: i18next.t("dict.gen_c30ecc7a"), + error: i18next.t("dict.gen_7f7de8a2"), }; const SIZE_STYLE: Record, string> = { @@ -112,7 +113,7 @@ export function GeneratedImageCard({ {onDelete && (
@@ -185,7 +186,7 @@ export function GeneratedImageCard({ {onUpload && ( + >{i18next.t("common.aiRevisionWorkspace.o9to11")}
{preview.trim() ? (
-
优化预览
+
{i18next.t("dict.optimizePreview")}
                 {preview}
               
- - + +
) : null} diff --git a/client/src/components/common/ImageModelSelector.tsx b/client/src/components/common/ImageModelSelector.tsx new file mode 100644 index 0000000000..6310812ede --- /dev/null +++ b/client/src/components/common/ImageModelSelector.tsx @@ -0,0 +1,74 @@ +import i18next from "i18next"; +import { useState, useMemo, useEffect } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Image as ImageIcon } from "lucide-react"; +import { getAPIKeySettings } from "@/api/settings"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { + getStoredImageProvider, + setStoredImageProvider, + resolveImageProviderOptions, +} from "@/lib/imageModelRegistry"; + +export { getStoredImageProvider, setStoredImageProvider, PREFERRED_IMAGE_PROVIDER_KEY } from "@/lib/imageModelRegistry"; + +interface ImageModelSelectorProps { + compact?: boolean; + className?: string; +} + +export default function ImageModelSelector({ compact = true, className }: ImageModelSelectorProps) { + const [selectedProvider, setSelectedProvider] = useState(getStoredImageProvider); + + const { data: apiKeysRes } = useQuery({ + queryKey: ["settings", "api-keys"], + queryFn: getAPIKeySettings, + }); + + const providerOptions = useMemo(() => { + return resolveImageProviderOptions(apiKeysRes?.data ?? [], selectedProvider); + }, [apiKeysRes, selectedProvider]); + + const resolvedValue = useMemo(() => { + if (selectedProvider && providerOptions.some((p) => p.value === selectedProvider)) { + return selectedProvider; + } + return providerOptions[0]?.value ?? "comfyui"; + }, [selectedProvider, providerOptions]); + + const handleSelect = (val: string) => { + setSelectedProvider(val); + setStoredImageProvider(val); + }; + + useEffect(() => { + const handleStorage = (e: Event) => { + if (e instanceof CustomEvent && e.detail) { + setSelectedProvider(e.detail); + } else { + setSelectedProvider(getStoredImageProvider()); + } + }; + window.addEventListener("comic-image-provider-changed", handleStorage); + return () => window.removeEventListener("comic-image-provider-changed", handleStorage); + }, []); + + return ( +
+ +
+ ); +} diff --git a/client/src/components/common/LLMSelector.tsx b/client/src/components/common/LLMSelector.tsx index 9a73e0849c..79275c9325 100644 --- a/client/src/components/common/LLMSelector.tsx +++ b/client/src/components/common/LLMSelector.tsx @@ -1,3 +1,5 @@ +import i18next from "i18next"; +const t = (key: string, options?: any) => i18next.t(key, options) as string; import { useCallback, useEffect, useMemo, useRef } from "react"; import type { LLMProvider } from "@ai-novel/shared/types/llm"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; @@ -35,7 +37,6 @@ interface LLMSelectorProps { onChange?: (value: LLMSelectorValue) => void; showModel?: boolean; showParameters?: boolean; - showCompactTemperature?: boolean; compact?: boolean; showBadge?: boolean; showHelperText?: boolean; @@ -55,7 +56,6 @@ export default function LLMSelector({ onChange, showModel = true, showParameters = false, - showCompactTemperature = false, compact = false, showBadge = true, showHelperText = true, @@ -270,20 +270,18 @@ export default function LLMSelector({ return (
- {showBadge ? 模型 : null} + {showBadge ? {i18next.t("dict.gen_8000f187")} : null} { - const parsed = Number(event.target.value); - if (!Number.isFinite(parsed)) { - return; - } - updateValue({ - provider: effectiveProvider, - model: resolvedModel, - temperature: parsed, - maxTokens: resolvedMaxTokens, - }); - }} - onBlur={() => { - updateValue({ - provider: effectiveProvider, - model: resolvedModel, - temperature: clampTemperature(resolvedTemperature), - maxTokens: resolvedMaxTokens, - }); - }} - disabled={!hasRunnableProviders} - /> - - ) : null}
{showHelperText && !hasRunnableProviders && !apiKeySettingsQuery.isLoading ? ( -
- 当前没有已配置且启用的模型厂商,请先到系统设置里完成 API Key 和模型配置。 -
+
{i18next.t("common.lLMSelector.kpaw69")}
) : null} {showParameters ? (
diff --git a/client/src/components/creativeHub/OpenInCreativeHubButton.tsx b/client/src/components/creativeHub/OpenInCreativeHubButton.tsx index 5fad5bfa02..b1514c8021 100644 --- a/client/src/components/creativeHub/OpenInCreativeHubButton.tsx +++ b/client/src/components/creativeHub/OpenInCreativeHubButton.tsx @@ -1,3 +1,5 @@ +import i18next from "i18next"; +const t = (key: string, options?: any) => i18next.t(key, options) as string; import type { ComponentProps } from "react"; import type { CreativeHubResourceBinding } from "@ai-novel/shared/types/creativeHub"; import { Link } from "react-router-dom"; @@ -14,7 +16,7 @@ interface OpenInCreativeHubButtonProps { export default function OpenInCreativeHubButton({ bindings, - label = "在创作中枢中继续", + label = i18next.t("dict.gen_d69e4819"), variant = "outline", size = "sm", className, diff --git a/client/src/components/image/ImageGenerationConfirmDialog.tsx b/client/src/components/image/ImageGenerationConfirmDialog.tsx index 31ce23b64c..04af19438b 100644 --- a/client/src/components/image/ImageGenerationConfirmDialog.tsx +++ b/client/src/components/image/ImageGenerationConfirmDialog.tsx @@ -1,3 +1,5 @@ +import i18next from "i18next"; +const t = (key: string, options?: any) => i18next.t(key, options) as string; /** * 生图前统一确认弹窗 * @@ -16,20 +18,21 @@ import type { ImageGenerationOverrides, ImageGenerationPreview } from "@/api/com import { assistImageGenerationPrompt, resolveImageAssetUrl, type ImagePromptAssistResult } from "@/api/images"; import { toast } from "@/components/ui/toast"; import SelectControl from "@/components/common/SelectControl"; +import { resolveImageProviderOptions } from "@/lib/imageModelRegistry"; const SIZE_OPTIONS = [ - { value: "1024x1024", label: "1024×1024(方形 1:1)" }, - { value: "1024x1536", label: "1024×1536(竖版 2:3,漫画/角色)" }, - { value: "1536x1024", label: "1536×1024(横版 3:2,三视图/表情稿)" }, + { value: "1024x1024", label: i18next.t("dict.gen_4646d30c") }, + { value: "1024x1536", label: i18next.t("dict.gen_ac0160dc") }, + { value: "1536x1024", label: i18next.t("dict.gen_8cc03476") }, ]; const REF_KIND_LABEL: Record = { - character_sheet: "三视图", - character_expression: "表情稿", - character_face: "面部裁剪", - book_analysis_character_base: "基础形象", - asset: "资产", - scene: "场景", + character_sheet: i18next.t("dict.threeViews"), + character_expression: i18next.t("dict.gen_1a07c5a4"), + character_face: i18next.t("dict.gen_0a071b57"), + book_analysis_character_base: i18next.t("dict.gen_79e5b55e"), + asset: i18next.t("sidebar.groupAssets"), + scene: i18next.t("dict.gen_c931653c"), }; const REF_KIND_COLOR: Record = { @@ -88,21 +91,14 @@ export function ImageGenerationConfirmDialog({ }, [preview]); // 可用 provider 列表(图像生成 + 已配置) - const { data: providerOptions = [] } = useQuery({ + const { data: apiKeysRes } = useQuery({ queryKey: ["settings", "api-keys"], queryFn: getAPIKeySettings, - select: (res) => - (res.data ?? []) - .filter((p) => p.supportsImageGeneration && p.isConfigured) - .map((p) => ({ value: p.provider, label: p.displayName ?? p.name })), }); - // 当前 provider 不在可用列表里时,临时追加为选项(不丢失数据) const providerChoices = useMemo(() => { - if (!provider) return providerOptions; - if (providerOptions.some((p) => p.value === provider)) return providerOptions; - return [...providerOptions, { value: provider, label: provider }]; - }, [provider, providerOptions]); + return resolveImageProviderOptions(apiKeysRes?.data ?? [], provider); + }, [apiKeysRes, provider]); // size 也保证当前值在列表里 const sizeChoices = useMemo(() => { @@ -166,16 +162,16 @@ export function ImageGenerationConfirmDialog({ })), }); if (!response.data) { - throw new Error("没有收到 Prompt 处理结果。"); + throw new Error(i18next.t("dict.gen_6abf9a2d")); } if (action === "optimize" && response.data.optimizedPrompt?.trim()) { setPrompt(response.data.optimizedPrompt.trim()); } setPromptAssistResult(response.data); } catch (error) { - const message = error instanceof Error ? error.message : "Prompt 处理失败。"; + const message = error instanceof Error ? error.message : i18next.t("dict.gen_6eb607e0"); setPromptAssistError(message); - toast.error("Prompt 处理失败", { description: message }); + toast.error(i18next.t("dict.gen_79634078"), { description: message }); } finally { setPromptAssistLoading(null); } @@ -184,15 +180,13 @@ export function ImageGenerationConfirmDialog({ const footer = preview ? (

- {anyDirty ? "本次将使用上方修改后的参数生图(仅一次性,不保存到角色)" : "点击「开始生图」按当前参数生成"} + {anyDirty ? i18next.t("dict.gen_a7da5981") : i18next.t("dict.gen_864c3a74")}

- +
@@ -201,7 +195,7 @@ export function ImageGenerationConfirmDialog({ return ( { if (!o) onCancel(); }}> {loading ? (
- - 正在准备生图素材... -
+ {i18next.t("image.imageGenerationConfirmDialog.7f6mdj")}
) : !preview ? ( -
无预览数据
+
{i18next.t("dict.gen_4d62dfb4")}
) : (
{/* 参考图素材 */}
- - 参考素材 - + {i18next.t("image.imageGenerationConfirmDialog.b3o3g1")} {referenceImages.length}/{preview.referenceImages.length} {referenceDirty && ( @@ -230,19 +220,13 @@ export function ImageGenerationConfirmDialog({ className="ml-auto text-[10px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline" onClick={() => setIncludedReferenceImageUrls(preview.referenceImages.map((ref) => ref.url))} disabled={submitting || !!promptAssistLoading} - > - 恢复全部 - + >{i18next.t("image.imageGenerationConfirmDialog.cj35pn")} )}
{preview.referenceImages.length === 0 ? ( -
- 本次生图不附带参考图(纯文生图) -
+
{i18next.t("image.imageGenerationConfirmDialog.2o9z4k")}
) : referenceImages.length === 0 ? ( -
- 本次生成不会发送参考图 -
+
{i18next.t("image.imageGenerationConfirmDialog.svnbay")}
) : (
{referenceImages.map((ref, i) => { @@ -256,7 +240,7 @@ export function ImageGenerationConfirmDialog({ + >{i18next.t("image.imageGenerationConfirmDialog.cjgauv")} )}
@@ -336,10 +318,10 @@ export function ImageGenerationConfirmDialog({ }} disabled={submitting || !!promptAssistLoading} /> -

{prompt.length} 字符 · 临时修改不会改动角色/项目设置

+

{i18next.t("dict.gen_f1c9a752")}

-

优化要求

+

{i18next.t("dict.gen_6b77df8b")}

+ >{i18next.t("image.imageGenerationConfirmDialog.jdw5")} )}
@@ -375,10 +355,10 @@ export function ImageGenerationConfirmDialog({ setOptimizationInstruction(e.target.value); clearPromptAssistResult(); }} - placeholder="例如:更像水彩、画面更温柔、保留服装和发型" + placeholder={i18next.t("dict.gen_86c3b879")} disabled={submitting || !!promptAssistLoading} /> -

{optimizationInstruction.length} 字符 · 仅用于「优化 Prompt」

+

{i18next.t("dict.gen_8c16cea5")}

{(promptAssistResult || promptAssistError) && (
@@ -401,7 +381,7 @@ export function ImageGenerationConfirmDialog({ {promptAssistAction === "optimize" && promptAssistResult.changes.length > 0 && (
-

已调整

+

{i18next.t("dict.gen_0e07c241")}

    {promptAssistResult.changes.map((item, index) => (
  • {item}
  • @@ -411,7 +391,7 @@ export function ImageGenerationConfirmDialog({ )} {promptAssistResult.risks.length > 0 && (
    -

    注意事项

    +

    {i18next.t("dict.gen_1bbbb204")}

      {promptAssistResult.risks.map((item, index) => (
    • {item}
    • @@ -430,7 +410,7 @@ export function ImageGenerationConfirmDialog({

      负面 Prompt - {negativePromptDirty && 已修改} + {negativePromptDirty && {i18next.t("dict.gen_2b689f7b")}}

      {negativePromptDirty && ( + >{i18next.t("image.imageGenerationConfirmDialog.cjgauv")} )}