396 lines
17 KiB
Markdown
396 lines
17 KiB
Markdown
|
|
# 🔧 手搓飞机·读不全章节·终极修复补丁 · 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)
|