Skip to content

Feature/db write retry 保存小说(带写锁重试机制)#123

Closed
LuoFengXiaoXiao wants to merge 7 commits into
shenminglinyi:masterfrom
LuoFengXiaoXiao:feature/db-write-retry
Closed

Feature/db write retry 保存小说(带写锁重试机制)#123
LuoFengXiaoXiao wants to merge 7 commits into
shenminglinyi:masterfrom
LuoFengXiaoXiao:feature/db-write-retry

Conversation

@LuoFengXiaoXiao

@LuoFengXiaoXiao LuoFengXiaoXiao commented Apr 19, 2026

Copy link
Copy Markdown

变更类型

sqlite_novel_repository.py.save


变更说明


架构影响

  • 涉及层级:infrastructure(删除不适用项)
  • 是否新增数据库表/字段:否(如是,请附 migration 说明)
  • 是否修改现有 API 契约(路径/字段/类型变更): 否

测试

# 后端单测(必填,粘贴你实际跑的命令和结果摘要)
pytest tests/unit/... -q

# 前端构建(如改了前端必填)
cd frontend && npm run build
  • 新增/修改的逻辑有对应单测
  • 本地后端启动正常(python -m uvicorn ...
  • 本地前端启动正常(npm run dev

风险说明

  • 潜在风险:无
  • 回滚方式:

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved backend stability by adding automatic retry handling for database locking issues.
  • API Changes

    • Simplified the novel creation endpoint to accept only essential information: title, author, ID, and chapter count.
  • Chores

    • Updated development server proxy configuration for improved request handling.
    • Adjusted CORS settings for local development environments.

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces multiple configuration and logic enhancements across the stack: simplifies the frontend createNovel API contract by removing optional fields, adds database write-lock retry logic with exponential backoff, implements LLM provider fallback (OpenAI → Anthropic), enables proxy redirect-following with logging, tightens CORS restrictions, and updates build/git artifacts handling.

Changes

Cohort / File(s) Summary
Build & Environment
.gitignore
Removed collaborator notes; added ignore patterns for %USERPROFILE%/, .history/, test_llm.py, and .gitignore itself.
Frontend API
frontend/src/api/novel.ts
Removed optional fields (premise, genre, world_preset, length_tier, target_words_per_chapter) from createNovel payload, leaving only novel_id, title, author, target_chapters.
Frontend Configuration
frontend/vite.config.ts
Enabled proxy redirect-following (followRedirects: true) and added response hook to log HTTP 307/308 redirects with Location headers.
LLM Provider
infrastructure/ai/providers/openai_provider.py
Removed custom httpx.AsyncClient management; added try/except debug logging around Chat Completions calls with exception details and token counts.
Database Persistence
infrastructure/persistence/database/sqlite_novel_repository.py
Added retry mechanism with exponential backoff + jitter for sqlite3.OperationalError on database lock; updated docstring.
API Routes & Services
interfaces/api/v1/blueprint/continuous_planning_routes.py
Refactored get_service() to initialize LLM provider preferring OpenAI with fallback to Anthropic; raises RuntimeError if neither is available; added console logging.
API Router Config
interfaces/api/v1/core/novels.py
Added redirect_slashes=False parameter to /novels router configuration.
App Configuration
interfaces/main.py
Restricted CORS to localhost development origins (ports 3000, 5173) instead of allowing all origins; added expose_headers=["*"].

Sequence Diagrams

sequenceDiagram
    participant Client
    participant get_service as get_service()
    participant OpenAI as OpenAIProvider
    participant Anthropic as AnthropicProvider
    
    Client->>get_service: Initialize LLM service
    get_service->>OpenAI: Read OPENAI_API_KEY & OPENAI_BASE_URL
    alt OpenAI initialization succeeds
        OpenAI-->>get_service: Provider ready
        get_service-->>Client: OpenAI provider assigned
    else OpenAI unavailable/fails
        get_service->>Anthropic: Read ANTHROPIC_API_KEY/AUTH_TOKEN
        alt Anthropic initialization succeeds
            Anthropic-->>get_service: Provider ready
            get_service-->>Client: Anthropic provider assigned
        else Both fail
            get_service-->>Client: RuntimeError with env var requirements
        end
    end
Loading
sequenceDiagram
    participant App
    participant sqlite_novel_repository as Repository.save()
    participant SQLite as SQLite DB
    
    App->>sqlite_novel_repository: save(novel)
    sqlite_novel_repository->>sqlite_novel_repository: Build params (once)
    loop For each retry attempt
        sqlite_novel_repository->>SQLite: execute(...)
        alt Success
            SQLite-->>sqlite_novel_repository: OK
            sqlite_novel_repository-->>App: Return
        else OperationalError: "database is locked"
            sqlite_novel_repository->>sqlite_novel_repository: Calculate backoff<br/>(base × 2^attempt + jitter)
            sqlite_novel_repository->>sqlite_novel_repository: Wait (exponential backoff)
            sqlite_novel_repository->>sqlite_novel_repository: Log warning
            Note over sqlite_novel_repository: Retry next iteration
        else Other error
            sqlite_novel_repository-->>App: Re-raise exception
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #67: Modifies frontend/src/api/novel.ts createNovel in opposite direction—adds target_words_per_chapter and other optional fields that this PR removes.
  • PR #56: Modifies infrastructure/ai/providers/openai_provider.py with different request/error handling and fallback patterns.

Suggested reviewers

  • shenminglinyi

Poem

🐰 Hops through retry loops with flair,
Fallback LLMs float through the air,
Lock-free dances, backoff ballet,
Simpler APIs brighten the day!
Config tweaks, CORS locked tight,
This PR makes all things right!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete. It lacks proper change type selection, detailed change explanation, and concrete testing results. The 'change type' section only lists a file name instead of checking appropriate boxes, and testing evidence is missing. Complete the description by: (1) checking the appropriate change type box (feat/fix/refactor/perf/docs/chore); (2) providing 1-3 sentences explaining what was changed and why; (3) adding actual test command outputs; (4) completing the testing checklist with evidence.
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title directly matches the main change: adding a retry mechanism for database write locks in the save function, as shown in sqlite_novel_repository.py.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
infrastructure/ai/providers/openai_provider.py (1)

38-47: ⚠️ Potential issue | 🟠 Major

Pass custom httpx client with trust_env=False to match other providers.

The OpenAI SDK's AsyncOpenAI constructor accepts an http_client parameter for custom httpx configuration. AnthropicProvider and GeminiProvider both explicitly pass httpx.AsyncClient(trust_env=False) to prevent system proxy interference that causes TLS errors (as documented in the AnthropicProvider comment). OpenAIProvider currently omits this, relying on the SDK's default httpx client with trust_env=True, which will route requests through system HTTP(S)_PROXY and risks connection failures in proxy environments.

Add:

client_kwargs["http_client"] = httpx.AsyncClient(
    timeout=httpx.Timeout(settings.timeout_seconds),
    trust_env=False
)
🤖 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 38 - 47, The
OpenAIProvider currently constructs client_kwargs and instantiates AsyncOpenAI
without supplying a custom httpx client, which leaves trust_env=True and can
cause proxy/TLS issues; update the construction of client_kwargs (the dict
passed to AsyncOpenAI in the OpenAIProvider) to include an "http_client" entry
that is an instance of httpx.AsyncClient created with
timeout=httpx.Timeout(settings.timeout_seconds) and trust_env=False so the
AsyncOpenAI(... client_kwargs) call uses the custom client and matches
AnthropicProvider/GeminiProvider behavior.
🧹 Nitpick comments (9)
interfaces/api/v1/core/novels.py (1)

20-20: Minor formatting: missing space after comma.

✨ Proposed fix
-router = APIRouter(prefix="/novels",redirect_slashes=False, tags=["novels"])
+router = APIRouter(prefix="/novels", redirect_slashes=False, tags=["novels"])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interfaces/api/v1/core/novels.py` at line 20, The APIRouter instantiation
line has a formatting issue: add a space after the comma between
prefix="/novels" and redirect_slashes=False so the call to APIRouter(...) reads
with proper spacing; update the router = APIRouter(...) expression (the router
symbol and APIRouter call) to include that space.
infrastructure/persistence/database/sqlite_novel_repository.py (1)

106-108: Move imports to module level.

Importing time, sqlite3, and random inside the method adds overhead on each call and deviates from Python conventions. These are standard library modules and should be imported at the top of the file.

♻️ Proposed fix

At the top of the file (around line 4):

import time
import random

Then remove lines 106-108 from inside the method. Note: sqlite3 is already implicitly available via the connection module, but if you need to catch sqlite3.OperationalError, add import sqlite3 at module level.

🤖 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 - 108, The inline imports of time, sqlite3, and random inside the method
should be moved to the module level to avoid per-call overhead and follow Python
conventions: add "import time" and "import random" at the top of the file (and
"import sqlite3" at module level only if you need to reference sqlite3
exceptions like sqlite3.OperationalError) and remove the corresponding import
statements currently inside the method in sqlite_novel_repository.py so only
top-level imports remain; ensure any exception handling or references in
functions (e.g., where sqlite3.OperationalError is used) still work after the
move.
interfaces/api/v1/blueprint/continuous_planning_routes.py (5)

135-142: Prefer constructing Settings with all values upfront.

Mutating a dataclass after construction is not idiomatic and bypasses __post_init__ validation. Use the constructor to set all fields:

♻️ Proposed fix
-            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
+            settings = Settings(
+                api_key=openai_api_key.strip(),
+                base_url=openai_base_url.strip(),
+                default_model=os.getenv('DEFAULT_LLM_MODEL_NAME', 'qwen/qwen3.5-397b-a17b'),
+                use_legacy_chat_completions=True,
+                timeout_seconds=300.0,
+            )
🤖 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 135 -
142, The code mutates a Settings instance after construction; instead
instantiate Settings with all fields set to ensure __post_init__ runs and
validations apply. Replace the current multi-line mutation with a single
constructor call to Settings(...) passing api_key=openai_api_key.strip(),
base_url=openai_base_url.strip(),
default_model=os.getenv('DEFAULT_LLM_MODEL_NAME', 'qwen/qwen3.5-397b-a17b'),
use_legacy_chat_completions=True, timeout_seconds=300.0 and then pass that
Settings instance to OpenAIProvider(settings=...). Locate the Settings creation
and llm_service = OpenAIProvider(...) in continuous_planning_routes (the block
where settings is built) and update there.

37-41: Remove duplicate imports of BaseModel and Field.

These are already imported at line 8.

♻️ Proposed fix
-
-from pydantic import BaseModel, Field
-
-
-
🤖 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 37 -
41, Remove the duplicate import statement "from pydantic import BaseModel,
Field" that was reintroduced at lines 37-41; keep the original import already
declared earlier (line ~8) and delete this redundant import so only one
"BaseModel, Field" import remains in the module.

49-52: Remove large blocks of commented-out code.

Multiple sections of commented-out code (~100 lines total) add noise and reduce maintainability. If this code is no longer needed, remove it. If it's for reference, consider documenting the alternative approach in comments or an ADR instead.

Also applies to: 82-116, 186-230, 277-278

🤖 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 49 -
52, Remove the large commented-out blocks (e.g., the commented
GenerateMacroPlanRequest class and other commented sections around the indicated
ranges) from interfaces/api/v1/blueprint/continuous_planning_routes.py to reduce
noise; if the code is meant to be preserved for reference, extract it into a
short ADR or add a concise explanatory comment linking to the ADR instead of
leaving multiline commented code in the file, and ensure any needed type names
(like GenerateMacroPlanRequest, StructurePreference) are either restored as real
definitions or omitted so there are no dead comments left in the module.

24-29: Remove duplicate import of get_database.

get_database is imported twice: at line 22 (from interfaces.api.dependencies) and at line 29 (from infrastructure.persistence.database.connection). The import at line 29 shadows the first one.

♻️ Proposed fix
 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
🤖 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 24 -
29, The file imports get_database twice (once from interfaces.api.dependencies
and again from infrastructure.persistence.database.connection), which causes the
latter to shadow the former; remove the duplicate import by deleting the
redundant "from infrastructure.persistence.database.connection import
get_database" line and ensure all references in continuous_planning_routes.py
use the single intended get_database import (verify usages in route
handlers/methods that call get_database).

143-145: Replace print() statements with proper logging.

Using print() bypasses the logging configuration. Use a logger for consistency with the rest of the codebase.

Also applies to: 158-158

🤖 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 143 -
145, Replace the print() calls inside the OpenAIProvider initialization
try/except with the module logger so messages go through the app logging system:
in the try block that references settings.default_model and settings.base_url,
call logger.info(...) (or logger.debug as appropriate) instead of print for the
success message, and in the except block call logger.error(..., exc_info=True)
to include the exception details; also change the other print() at the later
occurrence (around the same OpenAIProvider init) to use the same logger level
for consistency.
infrastructure/ai/providers/openai_provider.py (1)

105-105: Debug print statement should use logger.

♻️ Proposed fix
-        print(f"[LLM] 生成成功,内容长度: {len(content)},输入tokens: {input_tokens},输出tokens: {output_tokens}")
+        logger.debug("Generation successful, content_length: %d, input_tokens: %d, output_tokens: %d", len(content), input_tokens, output_tokens)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/openai_provider.py` at line 105, Replace the raw
debug print call that logs generation metrics with the module logger (e.g.,
logger.info or logger.debug) so logs go through the configured logging system;
change the print(f"[LLM] 生成成功,内容长度: {len(content)},输入tokens:
{input_tokens},输出tokens: {output_tokens}") to logger.info(...) or
logger.debug(...) using the same formatted message and the existing logger in
openai_provider.py (referencing variables content, input_tokens, output_tokens)
so it integrates with normal log handling.
interfaces/main.py (1)

20-21: Remove dead commented-out code.

This commented-out environment variable line serves no purpose and adds noise. If it's needed for debugging, consider using a proper environment variable check instead.

🧹 Proposed fix
-#os.environ['DISABLE_AUTO_DAEMON'] = '1'
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interfaces/main.py` around lines 20 - 21, Remove the dead commented-out line
"#os.environ['DISABLE_AUTO_DAEMON'] = '1'" from the module to eliminate noise;
if you need to toggle DISABLE_AUTO_DAEMON for debugging, implement a proper
runtime check that reads os.environ (e.g., use os.getenv or a config loader)
rather than leaving commented-out code in the source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- 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.
- 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.

In `@frontend/vite.config.ts`:
- Around line 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.

In `@infrastructure/ai/providers/openai_provider.py`:
- Around line 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.

In `@infrastructure/persistence/database/sqlite_novel_repository.py`:
- Around line 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.

In `@interfaces/api/v1/blueprint/continuous_planning_routes.py`:
- Around line 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).

---

Outside diff comments:
In `@infrastructure/ai/providers/openai_provider.py`:
- Around line 38-47: The OpenAIProvider currently constructs client_kwargs and
instantiates AsyncOpenAI without supplying a custom httpx client, which leaves
trust_env=True and can cause proxy/TLS issues; update the construction of
client_kwargs (the dict passed to AsyncOpenAI in the OpenAIProvider) to include
an "http_client" entry that is an instance of httpx.AsyncClient created with
timeout=httpx.Timeout(settings.timeout_seconds) and trust_env=False so the
AsyncOpenAI(... client_kwargs) call uses the custom client and matches
AnthropicProvider/GeminiProvider behavior.

---

Nitpick comments:
In `@infrastructure/ai/providers/openai_provider.py`:
- Line 105: Replace the raw debug print call that logs generation metrics with
the module logger (e.g., logger.info or logger.debug) so logs go through the
configured logging system; change the print(f"[LLM] 生成成功,内容长度:
{len(content)},输入tokens: {input_tokens},输出tokens: {output_tokens}") to
logger.info(...) or logger.debug(...) using the same formatted message and the
existing logger in openai_provider.py (referencing variables content,
input_tokens, output_tokens) so it integrates with normal log handling.

In `@infrastructure/persistence/database/sqlite_novel_repository.py`:
- Around line 106-108: The inline imports of time, sqlite3, and random inside
the method should be moved to the module level to avoid per-call overhead and
follow Python conventions: add "import time" and "import random" at the top of
the file (and "import sqlite3" at module level only if you need to reference
sqlite3 exceptions like sqlite3.OperationalError) and remove the corresponding
import statements currently inside the method in sqlite_novel_repository.py so
only top-level imports remain; ensure any exception handling or references in
functions (e.g., where sqlite3.OperationalError is used) still work after the
move.

In `@interfaces/api/v1/blueprint/continuous_planning_routes.py`:
- Around line 135-142: The code mutates a Settings instance after construction;
instead instantiate Settings with all fields set to ensure __post_init__ runs
and validations apply. Replace the current multi-line mutation with a single
constructor call to Settings(...) passing api_key=openai_api_key.strip(),
base_url=openai_base_url.strip(),
default_model=os.getenv('DEFAULT_LLM_MODEL_NAME', 'qwen/qwen3.5-397b-a17b'),
use_legacy_chat_completions=True, timeout_seconds=300.0 and then pass that
Settings instance to OpenAIProvider(settings=...). Locate the Settings creation
and llm_service = OpenAIProvider(...) in continuous_planning_routes (the block
where settings is built) and update there.
- Around line 37-41: Remove the duplicate import statement "from pydantic import
BaseModel, Field" that was reintroduced at lines 37-41; keep the original import
already declared earlier (line ~8) and delete this redundant import so only one
"BaseModel, Field" import remains in the module.
- Around line 49-52: Remove the large commented-out blocks (e.g., the commented
GenerateMacroPlanRequest class and other commented sections around the indicated
ranges) from interfaces/api/v1/blueprint/continuous_planning_routes.py to reduce
noise; if the code is meant to be preserved for reference, extract it into a
short ADR or add a concise explanatory comment linking to the ADR instead of
leaving multiline commented code in the file, and ensure any needed type names
(like GenerateMacroPlanRequest, StructurePreference) are either restored as real
definitions or omitted so there are no dead comments left in the module.
- Around line 24-29: The file imports get_database twice (once from
interfaces.api.dependencies and again from
infrastructure.persistence.database.connection), which causes the latter to
shadow the former; remove the duplicate import by deleting the redundant "from
infrastructure.persistence.database.connection import get_database" line and
ensure all references in continuous_planning_routes.py use the single intended
get_database import (verify usages in route handlers/methods that call
get_database).
- Around line 143-145: Replace the print() calls inside the OpenAIProvider
initialization try/except with the module logger so messages go through the app
logging system: in the try block that references settings.default_model and
settings.base_url, call logger.info(...) (or logger.debug as appropriate)
instead of print for the success message, and in the except block call
logger.error(..., exc_info=True) to include the exception details; also change
the other print() at the later occurrence (around the same OpenAIProvider init)
to use the same logger level for consistency.

In `@interfaces/api/v1/core/novels.py`:
- Line 20: The APIRouter instantiation line has a formatting issue: add a space
after the comma between prefix="/novels" and redirect_slashes=False so the call
to APIRouter(...) reads with proper spacing; update the router = APIRouter(...)
expression (the router symbol and APIRouter call) to include that space.

In `@interfaces/main.py`:
- Around line 20-21: Remove the dead commented-out line
"#os.environ['DISABLE_AUTO_DAEMON'] = '1'" from the module to eliminate noise;
if you need to toggle DISABLE_AUTO_DAEMON for debugging, implement a proper
runtime check that reads os.environ (e.g., use os.getenv or a config loader)
rather than leaving commented-out code in the source.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aa4f9f8b-49d8-4ee6-a2a6-b03f081adb64

📥 Commits

Reviewing files that changed from the base of the PR and between 7d3268e and f204544.

📒 Files selected for processing (8)
  • .gitignore
  • frontend/src/api/novel.ts
  • frontend/vite.config.ts
  • infrastructure/ai/providers/openai_provider.py
  • infrastructure/persistence/database/sqlite_novel_repository.py
  • interfaces/api/v1/blueprint/continuous_planning_routes.py
  • interfaces/api/v1/core/novels.py
  • interfaces/main.py
💤 Files with no reviewable changes (1)
  • frontend/src/api/novel.ts

Comment thread .gitignore
# ════════════════════════════════════════════
# 本地临时文件和目录(自定义,不入库)
# ════════════════════════════════════════════
%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.

Comment thread .gitignore
.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.

Comment thread frontend/vite.config.ts
Comment on lines +30 to +37
// 🔥 关键:跟随重定向(让代理处理,而非浏览器)
followRedirects: true,
// 🔥 添加:错误处理
onProxyRes: (proxyRes, req, res) => {
if (proxyRes.statusCode === 307 || proxyRes.statusCode === 308) {
console.log('⚠️ Redirect detected:', proxyRes.headers.location);
}
}

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.

Comment on lines +83 to +91
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

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.

Comment on lines +106 to +124
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

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.

Comment on lines +159 to +160
except Exception:
pass

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).

@shenminglinyi

Copy link
Copy Markdown
Owner

感谢 @LuoFengXiaoXiao 的贡献!

这个 DB 写锁重试机制的思路很有价值,但注意到 PR 描述中提到「运行起来有点儿怪」,同时代码中保留了 print 调试语句,部分 API 参数变更(如移除 premisegenre 等字段)可能影响现有前端调用。

为了保持主分支稳定,暂时先关闭此 PR。建议:

  1. 移除调试用的 print 语句,使用 logging 模块
  2. 确认重试机制在并发场景下的稳定性
  3. 如需变更 API 契约,建议单独讨论

您的贡献已被记录在核心贡献者名单中,后续完善后欢迎重新提交。再次感谢!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants