fix(wechat): 公众号新版页面抓不到正文,改从 cgiDataNew 里读
CI / backend (push) Canceled after 0s

用户发来 https://mp.weixin.qq.com/s/ZEOsi4R4JcmZ_mhS6qpcww,存进库的正文只有 67 个字符
("原文链接 + 一个句号")。复盘发现公众号换了渲染方式:新版页面把整篇塞进
window.cgiDataNew 这个 JS 对象由前端渲染,页面里根本没有 id="js_content";
三种 UA 拿到的字节数完全一样,也不是 UA 的问题。

- 从 JS 字符串字面量里读正文(content_noencode / desc),按 \xNN、\uNNNN 反转义,
  话题标签是双重转义过的 HTML,先 unescape 再走 DOM。
- 配图字段排在文字之后,所以前后各找 2500 字符合图;剩下没用过的 mmbiz 图按
  12 张上限补在末尾(图文型文章靠就近配对会漏)。
- 解析顺序:旧 DOM → 新版 JS 数据 → 通用抽取,一层层退。

实测两篇:2373 字符 + 12 张图、2772 字符 + 13 张图,作者也都取到了。

同时补上用户提的两件:

- 知识能删了:DELETE /knowledge/{id}?confirm=true,软删 + 真删检索切片,
  只有作者或 owner/admin 能删;列表卡片(悬停出现)和阅读页动作行各一个入口。
  删完详情也回 7001,否则收藏的链接还能打开一篇已删除的知识。
- 分类下面有标签清单了:GET /knowledge/tags 按标签数"有多少条知识用了它",
  侧栏渲染成 #标签 + 条数,点一下筛选、再点取消。
This commit is contained in:
Steven
2026-11-06 02:08:36 +08:00
parent 626f83fb9c
commit 690779c5fb
10 changed files with 720 additions and 15 deletions
+115
View File
@@ -423,3 +423,118 @@ async def test_list_pages_through_every_document(client: AsyncClient) -> None:
assert cursor is None
assert sorted(seen) == titles
assert len(seen) == len(set(seen)), "翻页翻出了重复条目"
async def test_delete_removes_from_list_and_search(client: AsyncClient) -> None:
"""删掉的知识不能还留在列表和检索里——切片要一起删。"""
owner = await login(client)
ws = await create_workspace(client, owner, "删除知识")
note = unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge",
json={
"title": "回滚前先停灰度",
"body_md": "灰度流量上涨后错误率飙升,第一步先停灰度。",
"tag_names": ["发布", "回滚"],
},
headers=owner["headers"],
)
)
unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}/status",
json={"action": "publish", "version": note["version"]},
headers=owner["headers"],
)
)
# 不带 confirm 不给删,防手滑
resp = await client.delete(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}", headers=owner["headers"]
)
assert resp.json()["code"] == 1002
unwrap(
await client.delete(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}",
params={"confirm": "true"},
headers=owner["headers"],
)
)
page = unwrap(
await client.get(f"{V1}/workspaces/{ws['id']}/knowledge", headers=owner["headers"])
)
assert page["items"] == []
hits = unwrap(
await client.get(
f"{V1}/workspaces/{ws['id']}/knowledge/search",
params={"q": "灰度"},
headers=owner["headers"],
)
)
assert hits["items"] == []
tags = unwrap(
await client.get(f"{V1}/workspaces/{ws['id']}/knowledge/tags", headers=owner["headers"])
)
assert tags["items"] == [], "删掉的知识不该再给标签贡献条数"
# 删完再从收藏的链接点进来:要当它不存在,不能还渲染出一篇"删掉了的知识"
detail = await client.get(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}", headers=owner["headers"]
)
assert detail.json()["code"] == 7001
again = await client.delete(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}",
params={"confirm": "true"},
headers=owner["headers"],
)
assert again.json()["message"] == "已经删过了", "重复删不该报错,回一句就够"
async def test_only_author_or_admin_can_delete(client: AsyncClient) -> None:
owner = await login(client)
ws = await create_workspace(client, owner, "谁能删")
member = await _member(client, owner, ws)
note = unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge",
json={"title": "别人的笔记", "body_md": "随手记的"},
headers=owner["headers"],
)
)
resp = await client.delete(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}",
params={"confirm": "true"},
headers=member["headers"],
)
assert resp.json()["code"] == 3004
# 作者自己能删
unwrap(
await client.delete(
f"{V1}/workspaces/{ws['id']}/knowledge/{note['id']}",
params={"confirm": "true"},
headers=owner["headers"],
)
)
async def test_tags_endpoint_counts_documents(client: AsyncClient) -> None:
owner = await login(client)
ws = await create_workspace(client, owner, "标签统计")
for title, tags in (("甲", ["发布", "回滚"]), ("乙", ["发布"])):
unwrap(
await client.post(
f"{V1}/workspaces/{ws['id']}/knowledge",
json={"title": title, "tag_names": tags},
headers=owner["headers"],
)
)
got = unwrap(
await client.get(f"{V1}/workspaces/{ws['id']}/knowledge/tags", headers=owner["headers"])
)
counts = {row["name"]: row["count"] for row in got["items"]}
assert counts == {"发布": 2, "回滚": 1}