Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ llm_profiles.json
aitext.lock
scripts/aitext.lock

# ── Tauri 桌面壳(仅本机打包用,勿提交;协作者 clone 后无此目录,不影响 npm run dev) ──
# ── Tauri 桌面壳(仅本机打包用,勿提交) ──
frontend/src-tauri/

# ── 仅本机 Windows 打包链路(协作者只需 npm run dev + 后端) ──
Expand All @@ -150,3 +150,12 @@ mcps/
*.rs.bk
*.orig
*.rej

# ════════════════════════════════════════════
# 本地临时文件和目录(自定义,不入库)
# ════════════════════════════════════════════
%USERPROFILE%/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if literal %USERPROFILE% directories exist that justify this pattern

fd -t d '^%USERPROFILE%$' --max-depth 3

Repository: shenminglinyi/PlotPilot

Length of output: 49


🏁 Script executed:

# Read .gitignore around lines 157-159 to see context and comments
sed -n '150,165p' .gitignore

Repository: shenminglinyi/PlotPilot

Length of output: 275


🌐 Web query:

Does git expand environment variables in .gitignore patterns like %USERPROFILE%

💡 Result:

No, Git does not expand environment variables like %USERPROFILE% in .gitignore patterns. .gitignore patterns are treated as literal static strings using fnmatch-style glob matching without shell expansion or variable substitution. Official Git documentation for gitignore(5) describes the PATTERN FORMAT exhaustively, listing supported syntax like wildcards (*, ?), character ranges, for recursive directories, negation (!), escapes (), and comments (#), but makes no mention of environment variable expansion. Patterns follow fnmatch(3) rules with Git-specific extensions for paths, confirming literal interpretation. Stack Overflow discussions confirm this: users attempting constructs like .$(echo %username%) or conditional env-based ignores are told patterns are "completely static" with no shell/language expansion. For user-specific ignores (e.g., Windows username files), solutions involve per-repo .git/info/exclude or local core.excludesfile with hardcoded paths, not variables in patterns. Even core.excludesfile paths support ~ expansion in git-config (e.g., ~/foo expands to $HOME/foo), but patterns within these files remain literal. No evidence of %VAR% or $VAR expansion inside patterns across Git versions up to 2.53 (2025). Workarounds for dynamic ignores: - Generate .gitignore via scripts/Makefile before Git operations. - Use repo-local .git/info/exclude for uncommitted user-specific rules. - Global core.excludesfile = %USERPROFILE%.gitignore (expands in config value, not pattern). To test: echo "%USERPROFILE%/temp" > .gitignore; touch "$HOME/temp.txt"; git status --ignored --others (temp.txt appears unless path literally matches %USERPROFILE%).

Citations:


Environment variable patterns won't work in .gitignore.

Git does not expand environment variables in .gitignore patterns; they are treated as literal static strings. These patterns will only match directories literally named %USERPROFILE%/, not the actual user profile path.

Based on the comment here, you intend to ignore local temporary directories. Use concrete path patterns instead, or consider using .git/info/exclude for local-only ignore rules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 157, The .gitignore entry "%USERPROFILE%/" is incorrect
because git treats it as a literal path; replace this pattern with concrete
ignore patterns for the actual local-temp directories you want ignored (for
example use names like tmp/, temp/, build/, .cache/ or any project-specific temp
folder names), or move a custom local-only rule into .git/info/exclude; update
the file to remove "%USERPROFILE%/" and add the explicit directory patterns (or
add guidance in repo docs about adding OS-specific paths to .git/info/exclude)
so the intended local profile/temp files are actually ignored.

.history/
frontend/%USERPROFILE%/
test_llm.py
.gitignore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

CRITICAL: Remove .gitignore from ignore patterns.

Adding .gitignore to the ignore list prevents Git from tracking changes to the .gitignore file itself. This means future updates to .gitignore cannot be committed or pushed, effectively breaking version control for this critical configuration file.

🚨 Proposed fix
 frontend/%USERPROFILE%/
 test_llm.py
-.gitignore
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 161, Remove the literal ".gitignore" entry from the
.gitignore file so Git can track and commit changes to that file; open the
.gitignore, locate the line containing ".gitignore" (exact string) and delete
it, then save and commit the updated .gitignore ensuring no other rules
inadvertently exclude the file (e.g., negate patterns or parent ignores) so the
file remains tracked.

6 changes: 0 additions & 6 deletions frontend/src/api/novel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ export const novelApi = {
title: string
author: string
target_chapters: number
premise?: string
genre?: string
world_preset?: string
/** V1 体量档:与 target_chapters 二选一由后端解析 */
length_tier?: 'short' | 'standard' | 'epic' | null
target_words_per_chapter?: number | null
}) => apiClient.post<NovelDTO>('/novels', data) as Promise<NovelDTO>,

/**
Expand Down
8 changes: 8 additions & 0 deletions frontend/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ export default defineConfig({
timeout: 0,
// 不要重写路径
rewrite: (path) => path,
// 🔥 关键:跟随重定向(让代理处理,而非浏览器)
followRedirects: true,
// 🔥 添加:错误处理
onProxyRes: (proxyRes, req, res) => {
if (proxyRes.statusCode === 307 || proxyRes.statusCode === 308) {
console.log('⚠️ Redirect detected:', proxyRes.headers.location);
}
}
Comment on lines +30 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Vite dev server proxy followRedirects onProxyRes behavior http-proxy

💡 Result:

In Vite's dev server proxy (powered by http-proxy), the followRedirects option controls whether the proxy automatically follows HTTP redirects (3xx responses) from the target server. Default is false, meaning redirects are passed through to the client as-is. To enable automatic following of redirects, pass followRedirects: true in the proxy options via vite.config.js: export default defineConfig({ server: { proxy: { '/api': { target: 'http://example.com', followRedirects: true, // other options... } } } }) Regarding onProxyRes behavior: This event fires for every response from the target, including redirects (e.g., 301/302). If followRedirects: true, the proxy internally follows the redirect (using the follow-redirects dependency), so onProxyRes receives the final response after all redirects. If false (default), onProxyRes receives the initial redirect response, which you can inspect/modify (e.g., rewrite Location header) or handle manually. To customize onProxyRes, use the configure function to access the proxy instance: '/api': { target: 'http://example.com', configure: (proxy) => { proxy.on('proxyRes', (proxyRes, req, res) => { if (proxyRes.statusCode >= 300 && proxyRes.statusCode < 400) { // Handle redirect: e.g., rewrite Location const location = proxyRes.headers.location; proxyRes.headers.location = location.replace('old', 'new'); } }); } } Note: Setting selfHandleResponse: true requires manually ending the response in onProxyRes handlers. Redirects are not followed automatically unless followRedirects: true. Use options like hostRewrite, autoRewrite, or protocolRewrite for automatic Location header rewriting on redirects without following them.

Citations:


Remove the redirect logging in onProxyRes—it will never execute with followRedirects: true.

When followRedirects: true is set, the proxy internally follows 307/308 redirects before onProxyRes fires. The callback receives the final response status, not the intermediate redirect status. The condition if (proxyRes.statusCode === 307 || proxyRes.statusCode === 308) will never be true and the logging is dead code.

Either remove the logging or set followRedirects: false and handle redirects manually in onProxyRes if redirect tracking is genuinely needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/vite.config.ts` around lines 30 - 37, Remove the dead redirect
logging in the Vite proxy config: delete the conditional that checks
proxyRes.statusCode === 307 || 308 and the console.log inside onProxyRes (or set
followRedirects to false if you intend to handle redirects manually); update the
onProxyRes handler to only contain live error/response handling logic and ensure
followRedirects remains true if you want automatic redirect following.

},
},
},
Expand Down
16 changes: 10 additions & 6 deletions infrastructure/ai/providers/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ def __init__(self, settings: Settings):
if settings.base_url:
client_kwargs["base_url"] = settings.base_url

self._http_client = httpx.AsyncClient(
timeout=httpx.Timeout(settings.timeout_seconds),
trust_env=False,
)
client_kwargs["http_client"] = self._http_client
self.async_client = AsyncOpenAI(**client_kwargs)

async def generate(
Expand Down Expand Up @@ -85,8 +80,16 @@ async def _generate_via_chat(self, prompt: Prompt, config: GenerationConfig) ->
"""Chat Completions API 非流式生成"""
messages = self._build_messages(prompt)
request_kwargs = self._build_chat_request_kwargs(messages, config)
print(f"[LLM] 准备调用 Chat API,模型: {request_kwargs.get('model')},max_tokens: {request_kwargs.get('max_tokens')}")
try:
response = await self.async_client.chat.completions.create(**request_kwargs)
print(f"[LLM] 收到响应,status: success")
except Exception as e:
print(f"[LLM] 请求异常: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
raise
Comment on lines +83 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace print() debug statements with proper logging.

Using print() for debugging is inappropriate for production code. These statements bypass the logging configuration and will clutter stdout. Use logger.debug() or logger.info() instead.

Also, line 86 has an f-string without placeholders (flagged by static analysis).

🛠️ Proposed fix
-        print(f"[LLM] 准备调用 Chat API,模型: {request_kwargs.get('model')},max_tokens: {request_kwargs.get('max_tokens')}")
+        logger.debug("Calling Chat API, model: %s, max_tokens: %s", request_kwargs.get('model'), request_kwargs.get('max_tokens'))
         try:
             response = await self.async_client.chat.completions.create(**request_kwargs)
-            print(f"[LLM] 收到响应,status: success")
+            logger.debug("Chat API response received successfully")
         except Exception as e:
-            print(f"[LLM] 请求异常: {type(e).__name__}: {e}")
-            import traceback
-            traceback.print_exc()
+            logger.error("Chat API request failed: %s: %s", type(e).__name__, e, exc_info=True)
             raise
🧰 Tools
🪛 Ruff (0.15.10)

[warning] 83-83: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


[warning] 83-83: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)


[error] 86-86: f-string without any placeholders

Remove extraneous f prefix

(F541)


[warning] 86-86: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/openai_provider.py` around lines 83 - 91, Replace
the three print() calls in the chat call block with proper logger calls: change
the pre-call debug print to logger.debug(...) including model and max_tokens
from request_kwargs, change the success print to logger.info(...) and remove the
unnecessary f-string prefix (no placeholders), and replace the exception
handling print/traceback with logger.exception(...) so the exception and
stacktrace are captured by the logging system before re-raising; target the
async_client.chat.completions.create call and the surrounding try/except in the
same function to locate the edits.


response = await self.async_client.chat.completions.create(**request_kwargs)
content = self._extract_text_from_response(response)

if not content:
Expand All @@ -99,6 +102,7 @@ async def _generate_via_chat(self, prompt: Prompt, config: GenerationConfig) ->

input_tokens = response.usage.prompt_tokens if response.usage else 0
output_tokens = response.usage.completion_tokens if response.usage else 0
print(f"[LLM] 生成成功,内容长度: {len(content)},输入tokens: {input_tokens},输出tokens: {output_tokens}")
return GenerationResult(
content=content,
token_usage=TokenUsage(input_tokens=input_tokens, output_tokens=output_tokens),
Expand Down
64 changes: 30 additions & 34 deletions infrastructure/persistence/database/sqlite_novel_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def __init__(self, db: DatabaseConnection):
self.db = db

def save(self, novel: Novel) -> None:
"""保存小说"""
"""保存小说(带写锁重试机制)"""
sql = """
INSERT INTO novels (
id, title, slug, author, target_chapters, premise,
Expand Down Expand Up @@ -84,7 +84,6 @@ def save(self, novel: Novel) -> None:
ladr = 1 if getattr(novel, "last_audit_drift_alert", False) else 0
lano = 1 if getattr(novel, "last_audit_narrative_ok", True) else 0
laat = getattr(novel, "last_audit_at", None)
# 新增字段
lavs = 1 if getattr(novel, "last_audit_vector_stored", False) else 0
lafs = 1 if getattr(novel, "last_audit_foreshadow_stored", False) else 0
late = 1 if getattr(novel, "last_audit_triples_extracted", False) else 0
Expand All @@ -94,38 +93,35 @@ def save(self, novel: Novel) -> None:
lai_json = json.dumps(lai) if lai else None
twpc = getattr(novel, "target_words_per_chapter", 2500)

self.db.execute(sql, (
novel_id,
novel.title,
slug,
author,
novel.target_chapters,
premise,
autopilot_status,
auto_approve_mode,
current_stage,
current_act,
current_chapter_in_act,
max_auto_chapters,
current_auto_chapters,
last_chapter_tension,
consecutive_error_count,
current_beat_index,
lacn,
lasim,
ladr,
lano,
laat,
lavs,
lafs,
late,
laqs_json,
lai_json,
twpc,
now,
now
))
self.db.get_connection().commit()
params = (
novel_id, novel.title, slug, author, novel.target_chapters, premise,
autopilot_status, auto_approve_mode, current_stage, current_act, current_chapter_in_act,
max_auto_chapters, current_auto_chapters, last_chapter_tension,
consecutive_error_count, current_beat_index,
lacn, lasim, ladr, lano, laat,
lavs, lafs, late, laqs_json, lai_json, twpc,
now, now
)

import time
import sqlite3
import random

max_retries = 15
base_delay = 1.0

for attempt in range(max_retries):
try:
self.db.execute(sql, params)
self.db.get_connection().commit()
return
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < max_retries - 1:
wait = base_delay * (2 ** attempt) + random.uniform(0, 1.0)
logger.warning(f"数据库写锁冲突,{wait:.2f}秒后重试 (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
continue
raise
Comment on lines +106 to +124

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Exponential backoff grows unbounded and inconsistent with connection timeout.

The retry logic uses base_delay * (2 ** attempt) which grows exponentially:

  • Attempt 5: ~32s wait
  • Attempt 10: ~1024s wait
  • Attempt 14: ~16384s wait

This is inconsistent with the 30-second busy_timeout configured at the connection level (per connection.py:336-342). After SQLite's built-in busy handler gives up at 30s, waiting 16000+ seconds before retrying is counterproductive.

Consider capping the maximum delay or reducing max_retries to align with the connection timeout:

🛠️ Proposed fix with capped delay
         max_retries = 15
         base_delay = 1.0
+        max_delay = 30.0  # Cap to match busy_timeout

         for attempt in range(max_retries):
             try:
                 self.db.execute(sql, params)
                 self.db.get_connection().commit()
                 return
             except sqlite3.OperationalError as e:
                 if "database is locked" in str(e) and attempt < max_retries - 1:
-                    wait = base_delay * (2 ** attempt) + random.uniform(0, 1.0)
+                    wait = min(base_delay * (2 ** attempt), max_delay) + random.uniform(0, 1.0)
                     logger.warning(f"数据库写锁冲突,{wait:.2f}秒后重试 (attempt {attempt + 1}/{max_retries})")
                     time.sleep(wait)
                     continue
                 raise
🧰 Tools
🪛 Ruff (0.15.10)

[error] 120-120: Standard pseudo-random generators are not suitable for cryptographic purposes

(S311)


[warning] 121-121: String contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/persistence/database/sqlite_novel_repository.py` around lines
106 - 124, The exponential backoff in sqlite_novel_repository.py (variables
max_retries and base_delay inside the for loop that calls self.db.execute and
self.db.get_connection().commit()) can grow far beyond the SQLite connection
busy_timeout; modify the retry logic to cap the exponential delay (e.g., compute
wait = min(base_delay * (2 ** attempt) + jitter, max_delay)) and set max_delay
to a value consistent with the connection busy_timeout (≈30s), or reduce
max_retries accordingly so retries occur within the busy_handler window; ensure
the "database is locked" branch uses the capped wait and keeps the same
logger.warning and time.sleep semantics.


async def async_save(self, novel: Novel) -> None:
"""异步保存小说(守护进程使用)"""
Expand Down
164 changes: 146 additions & 18 deletions interfaces/api/v1/blueprint/continuous_planning_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,35 @@
from application.paths import get_db_path
from interfaces.api.dependencies import get_database

import os
from infrastructure.ai.providers.openai_provider import OpenAIProvider
from infrastructure.ai.config.settings import Settings


from infrastructure.persistence.database.connection import get_database


router = APIRouter(prefix="/api/v1/planning", tags=["continuous-planning"])


# ==================== DTOs ====================


from pydantic import BaseModel, Field




class StructurePreference(BaseModel):
"""结构偏好"""
parts: int = Field(3, ge=1, le=10)
volumes_per_part: int = Field(3, ge=1, le=10)
acts_per_volume: int = Field(3, ge=1, le=10)

# class GenerateMacroPlanRequest(BaseModel):
# target_chapters: int
# structure: StructurePreference = Field(default_factory=StructurePreference)


class MacroPlanRequest(BaseModel):
"""宏观规划请求"""
Expand Down Expand Up @@ -62,28 +79,93 @@ class ContinuePlanningRequest(BaseModel):

# ==================== 依赖注入 ====================

# def get_service() -> ContinuousPlanningService:
# """获取规划服务"""
# db_path = get_db_path()
# story_node_repo = StoryNodeRepository(db_path)
# chapter_element_repo = ChapterElementRepository(db_path)

# # 获取 LLM 服务
# import os
# from infrastructure.ai.providers.anthropic_provider import AnthropicProvider
# from infrastructure.ai.config.settings import Settings

# llm_service = None
# api_key = os.getenv("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_AUTH_TOKEN")
# if api_key:
# settings = Settings(
# api_key=api_key.strip(),
# base_url=os.getenv("ANTHROPIC_BASE_URL")
# )
# try:
# llm_service = AnthropicProvider(settings)
# except Exception:
# pass

# from application.world.services.bible_service import BibleService
# from interfaces.api.dependencies import get_bible_repository

# bible_service = BibleService(get_bible_repository())

# return ContinuousPlanningService(
# story_node_repo,
# chapter_element_repo,
# llm_service,
# bible_service,
# chapter_repository=SqliteChapterRepository(get_database()),
# )

def get_service() -> ContinuousPlanningService:
"""获取规划服务"""
"""获取规划服务(优先使用 OpenAI 兼容配置)"""
db_path = get_db_path()
story_node_repo = StoryNodeRepository(db_path)
chapter_element_repo = ChapterElementRepository(db_path)

# 获取 LLM 服务
import os
from infrastructure.ai.providers.anthropic_provider import AnthropicProvider
from infrastructure.ai.config.settings import Settings

llm_service = None
api_key = os.getenv("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_AUTH_TOKEN")
if api_key:
settings = Settings(
api_key=api_key.strip(),
base_url=os.getenv("ANTHROPIC_BASE_URL")
)

# ---------- 1. 优先尝试 OpenAI 兼容配置(NVIDIA NIM 代理) ----------
openai_api_key = os.getenv('OPENAI_API_KEY')
openai_base_url = os.getenv('OPENAI_BASE_URL')

if openai_api_key and openai_base_url:
try:
llm_service = AnthropicProvider(settings)
except Exception:
pass
from infrastructure.ai.providers.openai_provider import OpenAIProvider
settings = Settings()
settings.api_key = openai_api_key.strip()
settings.base_url = openai_base_url.strip()
settings.default_model = os.getenv('DEFAULT_LLM_MODEL_NAME', 'qwen/qwen3.5-397b-a17b')
settings.use_legacy_chat_completions = True # 关键:使用 /v1/chat/completions
settings.timeout_seconds = 300.0
# ✅ 直接使用 OpenAIProvider,不需要包装成 LLMService
llm_service = OpenAIProvider(settings=settings)
print(f"✅ 使用 OpenAIProvider 初始化 LLM 服务: {settings.default_model} @ {settings.base_url}")
except Exception as e:
print(f"⚠️ OpenAIProvider 初始化失败: {e}")

# ---------- 2. 降级:尝试 Anthropic ----------
if llm_service is None:
api_key = os.getenv("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_AUTH_TOKEN")
if api_key:
try:
from infrastructure.ai.providers.anthropic_provider import AnthropicProvider
settings = Settings(
api_key=api_key.strip(),
base_url=os.getenv("ANTHROPIC_BASE_URL")
)
llm_service = AnthropicProvider(settings=settings)
print("✅ 使用 AnthropicProvider 初始化 LLM 服务")
except Exception:
pass
Comment on lines +159 to +160

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't silently swallow exceptions in Anthropic fallback.

The bare except Exception: pass hides initialization failures, making debugging difficult. At minimum, log the error:

🛠️ Proposed fix
             except Exception:
-                pass
+                logger.warning("AnthropicProvider initialization failed", exc_info=True)

Note: You'll need to add import logging and logger = logging.getLogger(__name__) at the top of the file.

🧰 Tools
🪛 Ruff (0.15.10)

[error] 159-160: try-except-pass detected, consider logging the exception

(S110)


[warning] 159-159: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interfaces/api/v1/blueprint/continuous_planning_routes.py` around lines 159 -
160, Replace the bare "except Exception: pass" in the Anthropic fallback block
with proper logging: add "import logging" and "logger =
logging.getLogger(__name__)" at the top of the file, then catch the exception
and call logger.exception(...) or logger.error(..., exc_info=True) inside that
except so the initialization error is recorded (locate the bare except in the
Anthropic fallback handling in continuous_planning_routes.py).


# ---------- 3. 检查是否成功 ----------
if llm_service is None:
raise RuntimeError(
"无法初始化任何 LLM 服务。请检查环境变量:\n"
" - OpenAI: OPENAI_API_KEY, OPENAI_BASE_URL\n"
" - Anthropic: ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN"
)

from application.world.services.bible_service import BibleService
from interfaces.api.dependencies import get_bible_repository
Expand All @@ -93,25 +175,69 @@ def get_service() -> ContinuousPlanningService:
return ContinuousPlanningService(
story_node_repo,
chapter_element_repo,
llm_service,
llm_service, # 这里直接传入 OpenAIProvider 实例
bible_service,
chapter_repository=SqliteChapterRepository(get_database()),
)


# ==================== 宏观规划 API ====================

@router.post("/novels/{novel_id}/macro/generate", status_code=202)
# async def generate_macro_plan(
# novel_id: str,
# request: MacroPlanRequest,
# background_tasks: BackgroundTasks,
# service: ContinuousPlanningService = Depends(get_service)
# ):
# #生成宏观规划

# #生成部-卷-幕结构框架,不保存,返回供用户编辑

# try:
# print(f"[DEBUG] 路由层: 收到请求 novel_id={novel_id}, request={request}")
# service.initialize_macro_plan_task(novel_id)

# async def _generate_task():
# try:
# result = await service.generate_macro_plan(
# novel_id=novel_id,
# target_chapters=request.target_chapters,
# structure_preference=request.structure.dict()
# )
# service.store_macro_plan_result(novel_id, result)
# except Exception as e:
# import traceback
# print(f"[ERROR] 生成宏观规划失败:")
# print(traceback.format_exc())
# service.store_macro_plan_error(novel_id, str(e))
# service._update_macro_progress(
# novel_id,
# status="failed",
# message=f"结构规划生成失败: {e}",
# )

# background_tasks.add_task(_generate_task)
# return {
# "success": True,
# "task_started": True,
# "novel_id": novel_id,
# }
# except Exception as e:
# import traceback
# print(f"[ERROR] 生成宏观规划失败:")
# print(traceback.format_exc())
# raise HTTPException(status_code=500, detail=f"生成宏观规划失败: {str(e)}")

async def generate_macro_plan(
novel_id: str,
request: MacroPlanRequest,
background_tasks: BackgroundTasks,
service: ContinuousPlanningService = Depends(get_service)
):
"""生成宏观规划
#生成宏观规划

生成部-卷-幕结构框架,不保存,返回供用户编辑
"""
#生成部-卷-幕结构框架,不保存,返回供用户编辑

try:
print(f"[DEBUG] 路由层: 收到请求 novel_id={novel_id}, request={request}")
service.initialize_macro_plan_task(novel_id)
Expand Down Expand Up @@ -148,6 +274,8 @@ async def _generate_task():
raise HTTPException(status_code=500, detail=f"生成宏观规划失败: {str(e)}")




@router.get("/novels/{novel_id}/macro/progress")
async def get_macro_plan_generation_progress(novel_id: str):
"""获取精密结构规划的实时进度。"""
Expand Down
Loading
Loading