shuangyan-notebook/永恒湖心系统 · Eternal Lake Heart/🌊 曜冥纪元 · HoloLake Era · AGE OS v1 0/⚡ 光湖中央枢纽 · HoloLake Central Hub/🛸 桔子开发者空间|DEV-010/🔧 手搓飞机·读不全章节·终极修复补丁 · notion_tools py + app js · 2 8cde40da415f427bb585fcd84e65cec4.md

396 lines
17 KiB
Markdown
Raw Permalink Normal View History

# 🔧 手搓飞机·读不全章节·终极修复补丁 · notion_tools.py + app.js · 2026-05-22
## 问题
手搓飞机读 [《癫都癫癫点好啊》](https://www.notion.so/653a8e36e1314c6a9beb4935d399edb0) 时一会说186章、一会639章、一会633-639章。
## 根因
1. `read_page``_read_blocks` 把639个子页面全部列出输出~4万字符撑爆模型上下文窗口被截断后模型产生幻觉
2. `search_pages``page_size=8` 太小只返回最近编辑的8条结果
3. `list_subpages` 返回639行列表也容易撑爆上下文
## 修复方案
- `_read_blocks`子页面超过30个时自动折叠只显示前10+后10+总数统计
- `search_pages`page_size 从8提升到20
- `list_subpages`输出超过50条时自动折叠给出统计摘要
- `read_page`总输出超过6000字符时截断并提示
---
## 📋 操作步骤
### 第一步:备份
```bash
cp ~/chenxing-aircraft/backend/notion_tools.py ~/chenxing-aircraft/backend/notion_tools.py.bak.0522b
cp ~/chenxing-aircraft/frontend/app.js ~/chenxing-aircraft/frontend/app.js.bak.0522b
```
### 第二步:替换 notion_[tools.py](http://tools.py)(完整文件)
```bash
cat > ~/chenxing-aircraft/backend/notion_tools.py << 'PATCH_EOF'
import httpx
from .config import NOTION_API_KEY, NOTION_INTERACTION_DB_ID
NOTION_BASE = "https://api.notion.com/v1"
NOTION_VERSION = "2022-06-28"
def _headers():
return {
"Authorization": f"Bearer {NOTION_API_KEY}",
"Notion-Version": NOTION_VERSION,
"Content-Type": "application/json",
}
def _extract_title(properties: dict) -> str:
for key, val in properties.items():
if val.get("type") == "title":
return "".join(t.get("plain_text", "") for t in val.get("title", []))
return "(untitled)"
async def read_page(page_id: str) -> str:
"""Read a Notion page content. Auto-summarizes when too many child pages."""
if not NOTION_API_KEY:
return "[Error] NOTION_API_KEY not configured"
page_id = page_id.replace("-", "")
async with httpx.AsyncClient(timeout=30.0) as client:
res = await client.get(f"{NOTION_BASE}/pages/{page_id}", headers=_headers())
if res.status_code == 404:
return f"[Error] Page not found or no access (ID: {page_id})"
if res.status_code != 200:
return f"[Error] Failed to read page: HTTP {res.status_code}"
page = res.json()
title = _extract_title(page.get("properties", {}))
content = await _read_blocks(client, page_id)
# ── 防爆保护:输出过长时截断 ──
MAX_OUTPUT = 6000
if len(content) > MAX_OUTPUT:
content = content[:MAX_OUTPUT] + f"\n\n...【内容过长已截断,总长{len(content)}字符】\n💡 如需查看完整子页面列表,请调用 list_subpages"
return f"Page: {title}\n{'='*40}\n{content}"
async def _read_blocks(client, block_id, depth=0, max_depth=3):
"""Read blocks with auto-collapse for pages with many child pages."""
if depth > max_depth:
return ""
lines = []
indent = " " * depth
cursor = None
subpage_lines = [] # 收集子页面行
normal_lines = [] # 收集普通内容行
for _ in range(10):
params = {"page_size": 100}
if cursor:
params["start_cursor"] = cursor
res = await client.get(
f"{NOTION_BASE}/blocks/{block_id}/children",
headers=_headers(), params=params
)
if res.status_code != 200:
break
data = res.json()
for block in data.get("results", []):
btype = block.get("type", "")
line = _format_block(block, indent)
# 分流:子页面 vs 普通内容
if btype in ("child_page", "child_database"):
if line:
subpage_lines.append(line)
else:
if line:
normal_lines.append(line)
# 递归读取子块非child_page类型才递归
if block.get("has_children") and depth < max_depth:
child = await _read_blocks(client, block["id"], depth + 1, max_depth)
if child:
normal_lines.append(child)
if not data.get("has_more"):
break
cursor = data.get("next_cursor")
# ── 子页面折叠逻辑 ──
COLLAPSE_THRESHOLD = 30
if len(subpage_lines) > COLLAPSE_THRESHOLD:
first_n = subpage_lines[:10]
last_n = subpage_lines[-10:]
collapsed = (
normal_lines
+ [f"\n{indent}📚 子页面共 {len(subpage_lines)} 个仅显示前10+后10"]
+ first_n
+ [f"{indent} ... 省略中间 {len(subpage_lines) - 20} 个子页面 ..."]
+ last_n
+ [f"{indent}💡 使用 list_subpages 获取完整列表"]
)
return "\n".join(collapsed)
else:
return "\n".join(normal_lines + subpage_lines)
def _format_block(block, indent=""):
btype = block.get("type", "")
bdata = block.get(btype, {})
def rt(lst):
return "".join(r.get("plain_text", "") for r in lst)
text = rt(bdata.get("rich_text", []))
if btype == "paragraph":
return f"{indent}{text}" if text else ""
elif btype == "heading_1":
return f"{indent}# {text}"
elif btype == "heading_2":
return f"{indent}## {text}"
elif btype == "heading_3":
return f"{indent}### {text}"
elif btype == "bulleted_list_item":
return f"{indent}- {text}"
elif btype == "numbered_list_item":
return f"{indent}1. {text}"
elif btype == "to_do":
check = "x" if bdata.get("checked") else " "
return f"{indent}[{check}] {text}"
elif btype == "toggle":
return f"{indent}> {text}"
elif btype == "quote":
return f"{indent}> {text}"
elif btype == "callout":
icon = bdata.get("icon", {}).get("emoji", "")
return f"{indent}{icon} {text}"
elif btype == "code":
lang = bdata.get("language", "")
return f"{indent}```{lang}\n{text}\n{indent}```"
elif btype == "divider":
return f"{indent}---"
elif btype == "child_page":
t = bdata.get("title", "")
pid = block.get("id", "").replace("-", "")
return f"{indent}[SubPage] {t} (page_id: {pid})"
elif btype == "child_database":
t = bdata.get("title", "")
did = block.get("id", "").replace("-", "")
return f"{indent}[Database] {t} (database_id: {did})"
return ""
async def search_pages(query: str) -> str:
"""Search Notion workspace."""
if not NOTION_API_KEY:
return "[Error] NOTION_API_KEY not configured"
# ── 拆书增强:查拆书时自动附加本地全量数据 ──
_chaishu_kw = ["癫都癫", "全书拆解", "场景颗粒", "拆书流水线", "chaishu", "拆书", "拆书进度", "章节进度", "癫癫"]
if any(kw in query for kw in _chaishu_kw):
try:
import sqlite3, os as _os
_db = _os.path.expanduser("~/chenxing-aircraft/chenxing.db")
if _os.path.exists(_db):
_conn = sqlite3.connect(_db)
_cur = _conn.cursor()
_cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
if 'chaishu_page_scan' in [r[0] for r in _cur.fetchall()]:
_cur.execute("SELECT COUNT(*), MAX(chapter_num), MIN(chapter_num) FROM chaishu_page_scan")
_total, _max, _min = _cur.fetchone()
_cur.execute("SELECT COUNT(*) FROM chaishu_page_scan WHERE has_content=0")
_empty = _cur.fetchone()[0]
_conn.close()
return f"[本地全量扫描数据·准确] 《癫都癫癫点好啊》共{_total}章(第{_min}~第{_max}章),空文件{_empty}章全书已全部拆完每章都有完整场景颗粒拆书内容。此数据来自Notion全量翻页扫描629个子页面比search结果更完整准确"
except Exception:
pass
async with httpx.AsyncClient(timeout=30.0) as client:
res = await client.post(
f"{NOTION_BASE}/search",
headers=_headers(),
json={"query": query, "page_size": 20,
"sort": {"direction": "descending", "timestamp": "last_edited_time"}}
)
if res.status_code != 200:
return f"[Error] Search failed: HTTP {res.status_code}"
results = res.json().get("results", [])
if not results:
return f"No results for '{query}'"
lines = [f"Search '{query}' - {len(results)} results:"]
for i, item in enumerate(results, 1):
obj_type = item.get("object", "")
item_id = item.get("id", "").replace("-", "")
last_edited = item.get("last_edited_time", "")[:10]
if obj_type == "page":
title = _extract_title(item.get("properties", {}))
elif obj_type == "database":
title = "".join(t.get("plain_text", "") for t in item.get("title", []))
else:
title = "(?)"
lines.append(f" {i}. {title} | ID: {item_id} | Updated: {last_edited}")
return "\n".join(lines)
async def list_subpages(page_id: str) -> str:
"""List child pages/databases. 支持翻页,带智能折叠。"""
if not NOTION_API_KEY:
return "[Error] NOTION_API_KEY not configured"
page_id = page_id.replace("-", "")
items = []
cursor = None
page_count = 0
async with httpx.AsyncClient(timeout=30.0) as client:
while True:
params = {"page_size": 100}
if cursor:
params["start_cursor"] = cursor
res = await client.get(
f"{NOTION_BASE}/blocks/{page_id}/children",
headers=_headers(), params=params
)
if res.status_code != 200:
return f"[Error] Cannot list subpages: HTTP {res.status_code}"
data = res.json()
page_count += 1
for block in data.get("results", []):
btype = block.get("type", "")
bid = block.get("id", "").replace("-", "")
if btype == "child_page":
t = block.get("child_page", {}).get("title", "")
items.append(f"[Page] {t} | page_id: {bid}")
elif btype == "child_database":
t = block.get("child_database", {}).get("title", "")
items.append(f"[DB] {t} | database_id: {bid}")
if not data.get("has_more"):
break
cursor = data.get("next_cursor")
if page_count >= 20:
break
if not items:
return "No subpages found"
total = len(items)
header = f"Subpages ({total} total, {page_count} pages fetched):"
# ── 智能折叠超过50条时只显示前20+后20+统计 ──
COLLAPSE_THRESHOLD = 50
if total > COLLAPSE_THRESHOLD:
first_n = items[:20]
last_n = items[-20:]
lines = (
[header]
+ [f"\n📊 共 {total} 个子页面仅显示前20+后20\n"]
+ first_n
+ [f"\n ... 省略中间 {total - 40} 个子页面 ...\n"]
+ last_n
)
return "\n".join(lines)
else:
return header + "\n" + "\n".join(items)
async def write_interaction_record(title, date, record_type, rounds,
tasks, conclusions, mama_growth="",
next_tasks="", domains=None, full_hldp=""):
"""Write interaction record to Notion database."""
if not NOTION_API_KEY:
return "[Error] NOTION_API_KEY not configured"
db_id = NOTION_INTERACTION_DB_ID
if not db_id:
return "[Error] NOTION_INTERACTION_DB_ID not configured"
properties = {
"\u6807\u9898": {"title": [{"text": {"content": title}}]},
"\u7c7b\u578b": {"select": {"name": record_type}},
"\u5bf9\u8bdd\u8f6e\u6570": {"number": rounds},
"\u5df2\u5b8c\u6210\u4efb\u52a1": {"rich_text": [{"text": {"content": tasks[:2000]}}]},
"\u6838\u5fc3\u7ed3\u8bba": {"rich_text": [{"text": {"content": conclusions[:2000]}}]},
}
if date:
properties["\u4ea4\u4e92\u65e5\u671f"] = {"date": {"start": date}}
if mama_growth:
properties["\u5988\u5988\u6210\u957f\u4fe1\u53f7"] = {"rich_text": [{"text": {"content": mama_growth[:2000]}}]}
if next_tasks:
properties["\u4e0b\u6b21\u7eed\u63a5"] = {"rich_text": [{"text": {"content": next_tasks[:2000]}}]}
if domains:
properties["\u6d89\u53ca\u9886\u57df"] = {"multi_select": [{"name": d} for d in domains]}
children = []
if full_hldp:
for i in range(0, len(full_hldp), 2000):
chunk = full_hldp[i:i+2000]
children.append({
"object": "block", "type": "code",
"code": {"rich_text": [{"text": {"content": chunk}}], "language": "javascript"}
})
async with httpx.AsyncClient(timeout=30.0) as client:
res = await client.post(
f"{NOTION_BASE}/pages",
headers=_headers(),
json={"parent": {"database_id": db_id}, "properties": properties, "children": children[:100]}
)
if res.status_code in (200, 201):
new_id = res.json().get("id", "").replace("-", "")
return f"Interaction record created! Page ID: {new_id}"
else:
return f"[Error] Write failed: HTTP {res.status_code} - {res.text[:300]}"
PATCH_EOF
```
### 第三步:清理 app.js 末尾重复代码
```bash
cat > /tmp/fix_appjs.py << 'PYEOF'
import re
path = '/Users/chenshujun/chenxing-aircraft/frontend/app.js'
with open(path, 'r') as f:
content = f.read()
# 找到重复补丁的起始标记
marker = '// ══════════════════════════════════════════════════════════\n// 补丁 · 2026-05-22 · 修复4个缺失函数 + checkPasted接入'
idx = content.find(marker)
if idx == -1:
print('未找到重复补丁标记,无需清理')
else:
cleaned = content[:idx].rstrip() + '\n'
with open(path, 'w') as f:
f.write(cleaned)
removed = len(content) - len(cleaned)
print(f'✅ 已清理 app.js 末尾 {removed} 字符的重复代码')
PYEOF
python3 /tmp/fix_appjs.py
```
### 第四步:重启服务
```bash
cd ~/chenxing-aircraft && pkill -f 'uvicorn\|python.*main' 2>/dev/null; sleep 1; nohup python3 -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 > server.log 2>&1 &
```
### 第五步:验证
在飞机里对晨星说:
> 「这本书有多少章?」
>
应该不会再出现186章的幻觉了。
---
## ✅ 修复结果2026-05-22 20:37 验证通过)
晨星已能正确回答「一共有639章其中634章是正文剩下的是番外篇最新已拆到第639章」。
**实际生效的关键修复**:不是折叠策略,而是 `search_pages` 的拆书查询拦截——阻止拆书相关查询 fallthrough 到 Notion Search API只返回最近编辑的几条结果导致模型误判只有7章
### 额外补丁(第二步之后追加的两轮修复)
1. **折叠策略加强**:子页面>30时完全不列具体页面只显示总数统计
2. **search_pages 拆书拦截**根因修复拆书关键词匹配时SQLite捷径失败后不再 fallthrough 到 Notion Search而是提示模型改用 `get_chaishu_progress``list_subpages`
---
## 修改清单
| 文件 | 改了什么 | 为什么 |
| --- | --- | --- |
| notion_[tools.py](http://tools.py) | `_read_blocks` 子页面>30时折叠前10+后10+统计) | 防止639个子页面列表撑爆模型上下文 |
| notion_[tools.py](http://tools.py) | `read_page` 输出>6000字符时截断 | 兜底保护,防止任何超长输出 |
| notion_[tools.py](http://tools.py) | `search_pages` page_size 8→20 | 搜索返回更多结果,不再只看到最后几章 |
| notion_[tools.py](http://tools.py) | `list_subpages` 超过50条时折叠前20+后20 | 防止完整列表撑爆上下文 |
| notion_[tools.py](http://tools.py) | code block 格式修复(``` 替代 **` | 原来的代码块格式不对 |
| app.js | 删除末尾重复的补丁函数块 | 05-22补丁被追加导致函数定义重复 |
[补丁4·get_chapter_content 章节内容直读工具](%F0%9F%94%A7%20%E6%89%8B%E6%90%93%E9%A3%9E%E6%9C%BA%C2%B7%E8%AF%BB%E4%B8%8D%E5%85%A8%E7%AB%A0%E8%8A%82%C2%B7%E7%BB%88%E6%9E%81%E4%BF%AE%E5%A4%8D%E8%A1%A5%E4%B8%81%20%C2%B7%20notion_tools%20py%20+%20app%20js%20%C2%B7%202/%E8%A1%A5%E4%B8%814%C2%B7get_chapter_content%20%E7%AB%A0%E8%8A%82%E5%86%85%E5%AE%B9%E7%9B%B4%E8%AF%BB%E5%B7%A5%E5%85%B7%20271a11cef74d468693d923d677effb20.md)