Sfoglia il codice sorgente

fix: resolve audit report findings - v5.1 schema compatibility

审查报告修复:

🔴 高优先级:
1. ContextPackBuilder v4.0/v5.1 schema 不兼容 [已修复]
   - entities.entity_id → entities.id
   - entity_aliases → aliases
   - entity_kv → current_json 字段
   - 更新版本标注: v4.0 → v5.1

2. Skill 文档与 v5.1 数据迁移不一致 [已修复]
   - webnovel-write/SKILL.md: 更新写入存储说明
   - system-data-flow.md: 更新核心变化说明 (init/resume)
   - 统一术语: aliases 表, index.db v5.1 schema

🟠 中优先级:
1. structured_index (v4.0) 并存 [已标记]
   - 添加 DEPRECATED 警告
   - 说明仅用于旧项目迁移

2. RAG API 重试机制 [已存在]
   - 确认 api_client.py 已有 max_retries=3
   - 指数退避: delay = base_delay * (2 ** attempt)
   - 处理 429/500/502/503/504 状态码

验证:
✓ context_pack_builder.py 使用 v5.1 schema
✓ 无遗留 entity_kv/entity_aliases SQL 查询
✓ 文档统一使用 v5.1 术语

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
lingfengQAQ 5 mesi fa
parent
commit
d9450c3e22

+ 42 - 42
.claude/scripts/context_pack_builder.py

@@ -1,9 +1,14 @@
 #!/usr/bin/env python3
 """
-Context Pack Builder v4.0
+Context Pack Builder v5.1
 
 为章节写作生成结构化上下文包,取代直接读取 state.json。
 
+v5.1 变更:
+- 使用 v5.1 index_manager schema (entities.id, aliases, current_json)
+- 移除对 entity_kv 表的依赖,改用 current_json 字段
+- 移除对 entity_aliases 表的依赖,改用 aliases 表
+
 输出 Schema:
 {
   "core": {
@@ -88,7 +93,7 @@ class ContextPackBuilder:
             "meta": {
                 "chapter": chapter_num,
                 "project_root": str(self.project_root),
-                "version": "5.0"
+                "version": "5.1"
             },
             "core": self._build_core(chapter_num),
             "scene": self._build_scene(chapter_num),
@@ -209,33 +214,28 @@ class ContextPackBuilder:
         protagonist_id = snapshot.get("entity_id", "")
         conn = self._conn_index()
         if protagonist_id and conn is not None:
+            # v5.1 schema: entities 表使用 id 字段,current_json 存储状态
             row = conn.execute(
-                "SELECT canonical_name FROM entities WHERE entity_id = ? LIMIT 1",
+                "SELECT canonical_name, current_json FROM entities WHERE id = ? LIMIT 1",
                 (protagonist_id,),
             ).fetchone()
-            if row and row["canonical_name"]:
-                snapshot["name"] = row["canonical_name"]
-
-            kv_rows = conn.execute(
-                "SELECT key, value FROM entity_kv WHERE entity_id = ?",
-                (protagonist_id,),
-            ).fetchall()
-
-            def _parse(v: str):
-                try:
-                    return json.loads(v)
-                except Exception:
-                    return v
-
-            kv = {r["key"]: _parse(r["value"]) for r in kv_rows} if kv_rows else {}
-            if isinstance(kv.get("realm"), str) and kv.get("realm"):
-                snapshot["realm"] = kv["realm"]
-            if kv.get("layer") is not None and kv.get("layer") != "":
-                snapshot["layer"] = kv["layer"]
-            if isinstance(kv.get("bottleneck"), str) and kv.get("bottleneck"):
-                snapshot["bottleneck"] = kv["bottleneck"]
-            if isinstance(kv.get("location"), str) and kv.get("location"):
-                snapshot["location"] = kv["location"]
+            if row:
+                if row["canonical_name"]:
+                    snapshot["name"] = row["canonical_name"]
+                # 从 current_json 解析状态
+                if row["current_json"]:
+                    try:
+                        current = json.loads(row["current_json"])
+                        if isinstance(current.get("realm"), str) and current.get("realm"):
+                            snapshot["realm"] = current["realm"]
+                        if current.get("layer") is not None and current.get("layer") != "":
+                            snapshot["layer"] = current["layer"]
+                        if isinstance(current.get("bottleneck"), str) and current.get("bottleneck"):
+                            snapshot["bottleneck"] = current["bottleneck"]
+                        if isinstance(current.get("location"), str) and current.get("location"):
+                            snapshot["location"] = current["location"]
+                    except (json.JSONDecodeError, TypeError):
+                        pass
 
         return snapshot
 
@@ -283,8 +283,9 @@ class ContextPackBuilder:
         if conn is None:
             return {"name": "未知地点", "desc": ""}
 
+        # v5.1 schema: 使用 aliases 表(替代 entity_aliases)
         rows = conn.execute(
-            "SELECT alias, entity_id FROM entity_aliases WHERE entity_type = ?",
+            "SELECT alias, entity_id FROM aliases WHERE entity_type = ?",
             ("地点",),
         ).fetchall()
         if not rows:
@@ -302,8 +303,9 @@ class ContextPackBuilder:
             if alias not in outline:
                 continue
 
+            # v5.1 schema: entities 表使用 id 字段
             e = conn.execute(
-                "SELECT canonical_name, desc FROM entities WHERE entity_id = ? LIMIT 1",
+                "SELECT canonical_name, desc FROM entities WHERE id = ? LIMIT 1",
                 (entity_id,),
             ).fetchone()
             return {
@@ -321,8 +323,9 @@ class ContextPackBuilder:
         if conn is None:
             return []
 
+        # v5.1 schema: 使用 aliases 表(替代 entity_aliases)
         rows = conn.execute(
-            "SELECT alias, entity_id FROM entity_aliases WHERE entity_type = ?",
+            "SELECT alias, entity_id FROM aliases WHERE entity_type = ?",
             ("角色",),
         ).fetchall()
         if not rows:
@@ -339,27 +342,24 @@ class ContextPackBuilder:
         if not matched_ids:
             return []
 
-        def _parse(v: str):
-            try:
-                return json.loads(v)
-            except Exception:
-                return v
-
         tier_order = {"核心": 0, "支线": 1, "装饰": 2, "": 3}
         matched: List[Dict[str, Any]] = []
         for entity_id in matched_ids:
+            # v5.1 schema: entities 表使用 id 字段,current_json 存储状态
             e = conn.execute(
-                "SELECT canonical_name, tier FROM entities WHERE entity_id = ? LIMIT 1",
+                "SELECT canonical_name, tier, current_json FROM entities WHERE id = ? LIMIT 1",
                 (entity_id,),
             ).fetchone()
             if not e:
                 continue
 
-            kv_rows = conn.execute(
-                "SELECT key, value FROM entity_kv WHERE entity_id = ?",
-                (entity_id,),
-            ).fetchall()
-            snapshot = {r["key"]: _parse(r["value"]) for r in kv_rows} if kv_rows else {}
+            # 从 current_json 解析快照
+            snapshot = {}
+            if e["current_json"]:
+                try:
+                    snapshot = json.loads(e["current_json"])
+                except (json.JSONDecodeError, TypeError):
+                    pass
 
             matched.append(
                 {
@@ -478,7 +478,7 @@ class ContextPackBuilder:
 
 
 def main():
-    parser = argparse.ArgumentParser(description="Context Pack Builder v4.0")
+    parser = argparse.ArgumentParser(description="Context Pack Builder v5.1")
     parser.add_argument("--chapter", type=int, required=True, help="章节编号")
     parser.add_argument("--project-root", metavar="PATH", help="项目根目录")
     parser.add_argument("--output", metavar="FILE", help="输出文件路径(默认输出到 stdout)")

+ 5 - 0
.claude/scripts/structured_index.py

@@ -2,6 +2,11 @@
 """
 结构化索引系统(Structured Index System)v4.0
 
+⚠️ DEPRECATED: 本模块已被 v5.1 index_manager 替代。
+   - v5.1 使用不同的 schema(entities.id, aliases, current_json)
+   - 本模块仅保留用于兼容旧项目迁移
+   - 新项目请使用 data_modules.index_manager
+
 目标:取代向量化检索,使用 SQLite 提供精确、快速的结构化查询
 
 v4.0 变更:

+ 3 - 2
.claude/skills/webnovel-init/references/system-data-flow.md

@@ -34,9 +34,10 @@ cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/system-data-flow.md"
     └── archive/            # 归档数据
 ```
 
-### v5.0 核心变化
+### v5.1 核心变化
 - **双 Agent 架构**: Context Agent (读) + Data Agent (写)
 - **无 XML 标签**: 纯正文写作,Data Agent AI 自动提取实体
-- **统一 schema**: entities_v3 + alias_index(一对多)
+- **SQLite 存储**: entities/aliases/state_changes 迁移到 index.db
+- **state.json 精简**: 保持 < 5KB,仅存 protagonist_state 和 plot_threads
 
 </instructions>

+ 3 - 2
.claude/skills/webnovel-resume/references/system-data-flow.md

@@ -34,9 +34,10 @@ cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-query/references/system-data-flow.md"
     └── archive/            # 归档数据
 ```
 
-### v5.0 核心变化
+### v5.1 核心变化
 - **双 Agent 架构**: Context Agent (读) + Data Agent (写)
 - **无 XML 标签**: 纯正文写作,Data Agent AI 自动提取实体
-- **统一 schema**: entities_v3 + alias_index(一对多)
+- **SQLite 存储**: entities/aliases/state_changes 迁移到 index.db
+- **state.json 精简**: 保持 < 5KB,仅存 protagonist_state 和 plot_threads
 
 </instructions>

+ 6 - 6
.claude/skills/webnovel-write/SKILL.md

@@ -43,13 +43,13 @@ allowed-tools: Read Write Edit Grep Bash Task
 
 **Agent 自动完成**:
 1. 读取本章大纲,分析需要什么信息
-2. 读取 state.json 获取主角状态(使用 entities_v3 格式)
-3. 调用 data_modules.index_manager 查询相关实体
+2. 读取 state.json 获取主角状态快照
+3. 调用 index.db (v5.1 schema) 查询相关实体和别名
 4. 调用 data_modules.rag_adapter 语义检索
 5. Grep 设定集搜索相关设定
 6. 评估伏笔紧急度
 7. 选择风格样本
-8. 组装上下文包 JSON
+8. 组装上下文包 JSON (v5.1)
 
 **输出**:上下文包 JSON,包含:
 - `core`: 大纲、主角快照、最近摘要
@@ -281,9 +281,9 @@ cat "${CLAUDE_PLUGIN_ROOT}/skills/webnovel-write/references/writing/typesetting.
    - 低置信度 (<0.5): 标记待人工确认
 
 3. **写入存储**
-   - 更新 state.json (实体 + 状态)
-   - 更新 index.db (索引)
-   - 注册新别名到 alias_index
+   - 更新 state.json (精简状态)
+   - 更新 index.db (v5.1 schema: entities/aliases/state_changes)
+   - 注册新别名到 aliases 表
 
 4. **AI 场景切片**
    - 按地点/时间/视角切分场景