|
@@ -6,8 +6,8 @@ Webnovel Dashboard - FastAPI 主应用
|
|
|
|
|
|
|
|
import asyncio
|
|
import asyncio
|
|
|
import json
|
|
import json
|
|
|
-import os
|
|
|
|
|
import sqlite3
|
|
import sqlite3
|
|
|
|
|
+from contextlib import asynccontextmanager, closing
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from typing import Optional
|
|
from typing import Optional
|
|
|
|
|
|
|
@@ -48,7 +48,17 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
if project_root:
|
|
if project_root:
|
|
|
_project_root = Path(project_root).resolve()
|
|
_project_root = Path(project_root).resolve()
|
|
|
|
|
|
|
|
- app = FastAPI(title="Webnovel Dashboard", version="0.1.0")
|
|
|
|
|
|
|
+ @asynccontextmanager
|
|
|
|
|
+ async def _lifespan(_: FastAPI):
|
|
|
|
|
+ webnovel = _webnovel_dir()
|
|
|
|
|
+ if webnovel.is_dir():
|
|
|
|
|
+ _watcher.start(webnovel, asyncio.get_running_loop())
|
|
|
|
|
+ try:
|
|
|
|
|
+ yield
|
|
|
|
|
+ finally:
|
|
|
|
|
+ _watcher.stop()
|
|
|
|
|
+
|
|
|
|
|
+ app = FastAPI(title="Webnovel Dashboard", version="0.1.0", lifespan=_lifespan)
|
|
|
|
|
|
|
|
app.add_middleware(
|
|
app.add_middleware(
|
|
|
CORSMiddleware,
|
|
CORSMiddleware,
|
|
@@ -57,17 +67,6 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
allow_headers=["*"],
|
|
allow_headers=["*"],
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # --- 生命周期 ---
|
|
|
|
|
- @app.on_event("startup")
|
|
|
|
|
- async def _startup():
|
|
|
|
|
- webnovel = _webnovel_dir()
|
|
|
|
|
- if webnovel.is_dir():
|
|
|
|
|
- _watcher.start(webnovel, asyncio.get_event_loop())
|
|
|
|
|
-
|
|
|
|
|
- @app.on_event("shutdown")
|
|
|
|
|
- async def _shutdown():
|
|
|
|
|
- _watcher.stop()
|
|
|
|
|
-
|
|
|
|
|
# ===========================================================
|
|
# ===========================================================
|
|
|
# API:项目元信息
|
|
# API:项目元信息
|
|
|
# ===========================================================
|
|
# ===========================================================
|
|
@@ -98,8 +97,7 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
include_archived: bool = False,
|
|
include_archived: bool = False,
|
|
|
):
|
|
):
|
|
|
"""列出所有实体(可按类型过滤)。"""
|
|
"""列出所有实体(可按类型过滤)。"""
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
q = "SELECT * FROM entities"
|
|
q = "SELECT * FROM entities"
|
|
|
params: list = []
|
|
params: list = []
|
|
|
clauses: list[str] = []
|
|
clauses: list[str] = []
|
|
@@ -113,24 +111,18 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
q += " ORDER BY last_appearance DESC"
|
|
q += " ORDER BY last_appearance DESC"
|
|
|
rows = conn.execute(q, params).fetchall()
|
|
rows = conn.execute(q, params).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/entities/{entity_id}")
|
|
@app.get("/api/entities/{entity_id}")
|
|
|
def get_entity(entity_id: str):
|
|
def get_entity(entity_id: str):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
row = conn.execute("SELECT * FROM entities WHERE id = ?", (entity_id,)).fetchone()
|
|
row = conn.execute("SELECT * FROM entities WHERE id = ?", (entity_id,)).fetchone()
|
|
|
if not row:
|
|
if not row:
|
|
|
raise HTTPException(404, "实体不存在")
|
|
raise HTTPException(404, "实体不存在")
|
|
|
return dict(row)
|
|
return dict(row)
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/relationships")
|
|
@app.get("/api/relationships")
|
|
|
def list_relationships(entity: Optional[str] = None, limit: int = 200):
|
|
def list_relationships(entity: Optional[str] = None, limit: int = 200):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
if entity:
|
|
if entity:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
"SELECT * FROM relationships WHERE from_entity = ? OR to_entity = ? ORDER BY chapter DESC LIMIT ?",
|
|
"SELECT * FROM relationships WHERE from_entity = ? OR to_entity = ? ORDER BY chapter DESC LIMIT ?",
|
|
@@ -142,8 +134,6 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
(limit,),
|
|
(limit,),
|
|
|
).fetchall()
|
|
).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/relationship-events")
|
|
@app.get("/api/relationship-events")
|
|
|
def list_relationship_events(
|
|
def list_relationship_events(
|
|
@@ -152,8 +142,7 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
to_chapter: Optional[int] = None,
|
|
to_chapter: Optional[int] = None,
|
|
|
limit: int = 200,
|
|
limit: int = 200,
|
|
|
):
|
|
):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
q = "SELECT * FROM relationship_events"
|
|
q = "SELECT * FROM relationship_events"
|
|
|
params: list = []
|
|
params: list = []
|
|
|
clauses: list[str] = []
|
|
clauses: list[str] = []
|
|
@@ -172,22 +161,16 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
params.append(limit)
|
|
params.append(limit)
|
|
|
rows = conn.execute(q, params).fetchall()
|
|
rows = conn.execute(q, params).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/chapters")
|
|
@app.get("/api/chapters")
|
|
|
def list_chapters():
|
|
def list_chapters():
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
rows = conn.execute("SELECT * FROM chapters ORDER BY chapter ASC").fetchall()
|
|
rows = conn.execute("SELECT * FROM chapters ORDER BY chapter ASC").fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/scenes")
|
|
@app.get("/api/scenes")
|
|
|
def list_scenes(chapter: Optional[int] = None, limit: int = 500):
|
|
def list_scenes(chapter: Optional[int] = None, limit: int = 500):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
if chapter is not None:
|
|
if chapter is not None:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
"SELECT * FROM scenes WHERE chapter = ? ORDER BY scene_index ASC", (chapter,)
|
|
"SELECT * FROM scenes WHERE chapter = ? ORDER BY scene_index ASC", (chapter,)
|
|
@@ -197,35 +180,26 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
"SELECT * FROM scenes ORDER BY chapter ASC, scene_index ASC LIMIT ?", (limit,)
|
|
"SELECT * FROM scenes ORDER BY chapter ASC, scene_index ASC LIMIT ?", (limit,)
|
|
|
).fetchall()
|
|
).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/reading-power")
|
|
@app.get("/api/reading-power")
|
|
|
def list_reading_power(limit: int = 50):
|
|
def list_reading_power(limit: int = 50):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
"SELECT * FROM chapter_reading_power ORDER BY chapter DESC LIMIT ?", (limit,)
|
|
"SELECT * FROM chapter_reading_power ORDER BY chapter DESC LIMIT ?", (limit,)
|
|
|
).fetchall()
|
|
).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/review-metrics")
|
|
@app.get("/api/review-metrics")
|
|
|
def list_review_metrics(limit: int = 20):
|
|
def list_review_metrics(limit: int = 20):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
"SELECT * FROM review_metrics ORDER BY end_chapter DESC LIMIT ?", (limit,)
|
|
"SELECT * FROM review_metrics ORDER BY end_chapter DESC LIMIT ?", (limit,)
|
|
|
).fetchall()
|
|
).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/state-changes")
|
|
@app.get("/api/state-changes")
|
|
|
def list_state_changes(entity: Optional[str] = None, limit: int = 100):
|
|
def list_state_changes(entity: Optional[str] = None, limit: int = 100):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
if entity:
|
|
if entity:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
"SELECT * FROM state_changes WHERE entity_id = ? ORDER BY chapter DESC LIMIT ?",
|
|
"SELECT * FROM state_changes WHERE entity_id = ? ORDER BY chapter DESC LIMIT ?",
|
|
@@ -236,13 +210,10 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
"SELECT * FROM state_changes ORDER BY chapter DESC LIMIT ?", (limit,)
|
|
"SELECT * FROM state_changes ORDER BY chapter DESC LIMIT ?", (limit,)
|
|
|
).fetchall()
|
|
).fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
@app.get("/api/aliases")
|
|
@app.get("/api/aliases")
|
|
|
def list_aliases(entity: Optional[str] = None):
|
|
def list_aliases(entity: Optional[str] = None):
|
|
|
- conn = _get_db()
|
|
|
|
|
- try:
|
|
|
|
|
|
|
+ with closing(_get_db()) as conn:
|
|
|
if entity:
|
|
if entity:
|
|
|
rows = conn.execute(
|
|
rows = conn.execute(
|
|
|
"SELECT * FROM aliases WHERE entity_id = ?", (entity,)
|
|
"SELECT * FROM aliases WHERE entity_id = ?", (entity,)
|
|
@@ -250,8 +221,6 @@ def create_app(project_root: str | Path | None = None) -> FastAPI:
|
|
|
else:
|
|
else:
|
|
|
rows = conn.execute("SELECT * FROM aliases").fetchall()
|
|
rows = conn.execute("SELECT * FROM aliases").fetchall()
|
|
|
return [dict(r) for r in rows]
|
|
return [dict(r) for r in rows]
|
|
|
- finally:
|
|
|
|
|
- conn.close()
|
|
|
|
|
|
|
|
|
|
# ===========================================================
|
|
# ===========================================================
|
|
|
# API:文档浏览(正文/大纲/设定集 —— 只读)
|
|
# API:文档浏览(正文/大纲/设定集 —— 只读)
|