用户发来 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:
@@ -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}
|
||||
|
||||
@@ -43,6 +43,28 @@ BLOCKED_HTML = """
|
||||
<html><body><div class="weui-msg"><p>环境异常</p><p>完成验证后即可继续访问</p></div></body></html>
|
||||
"""
|
||||
|
||||
# 新版公众号页面:没有 #js_content,正文全在 window.cgiDataNew 里;
|
||||
# 换行是 \x0a,话题标签是双重转义过的 HTML 片段,图片字段排在文字前面。
|
||||
# 正文抽成常量:desc 与 content_noencode 同文(真实页面也是这样,后者更权威),
|
||||
# 写成一行会超行宽,所以拆开拼。
|
||||
_WECHAT_DATA_BODY = (
|
||||
r"食材: 蒜瓣500g、小米辣60g\x0a\x0a步骤:\x0a1\u20e3蒜瓣打碎,"
|
||||
r"放在冷水里泡10分钟(泡一下蒜不会发苦)\x0a2\u20e3油烧热,转小火把蒜泥炒香"
|
||||
r"\x0a\x0a注意\x26lt;a class=\x26quot;wx_topic_link\x26quot;\x26gt;"
|
||||
r"#调料\x26lt;/a\x26gt;"
|
||||
)
|
||||
WECHAT_DATA_HTML = (
|
||||
"<html><head>\n"
|
||||
'<meta property="og:title" content="自制万能蒜蓉酱" />\n'
|
||||
"</head><body>\n<script>\n window.cgiDataNew = {\n"
|
||||
" nick_name: '破冰记的随笔',\n item_list: [{\n"
|
||||
" cdn_url: 'http://mmbiz.qpic.cn/mmbiz_jpg/xyz/640?wx_fmt=jpeg',\n"
|
||||
" title: '自制万能蒜蓉酱',\n"
|
||||
" desc: '" + _WECHAT_DATA_BODY + "',\n"
|
||||
" content_noencode: '" + _WECHAT_DATA_BODY + "'\n"
|
||||
" }]\n };\n</script>\n</body></html>\n"
|
||||
)
|
||||
|
||||
|
||||
def test_extract_url_only_when_message_is_a_link() -> None:
|
||||
assert article_service.extract_url("https://mp.weixin.qq.com/s/abc") == (
|
||||
@@ -71,6 +93,24 @@ def test_wechat_article_parses_title_body_and_images() -> None:
|
||||
assert "扫码关注" not in got.body_md
|
||||
|
||||
|
||||
def test_new_wechat_page_reads_js_data() -> None:
|
||||
"""新版页面没有 #js_content:正文在 cgiDataNew 的 JS 字符串里,得能读出来。"""
|
||||
got = article_service.parse_article(WECHAT_DATA_HTML, "https://mp.weixin.qq.com/s/abc")
|
||||
|
||||
assert got.title == "自制万能蒜蓉酱"
|
||||
assert got.author == "破冰记的随笔"
|
||||
assert "蒜瓣打碎" in got.body_md
|
||||
# \x0a 要还原成换行,否则整篇挤成一行
|
||||
assert "\n" in got.body_md
|
||||
# 双重转义的话题标签还原成文字,不能留下 <a 这种碎片
|
||||
assert "#调料" in got.body_md
|
||||
assert "<" not in got.body_md
|
||||
# 配图要跟着正文进来(正文里换成本地地址之前,先能取到这张图)
|
||||
assert "mmbiz.qpic.cn/mmbiz_jpg/xyz/640" in got.body_md
|
||||
# desc 与 content_noencode 是同一段文字,不能重复两遍
|
||||
assert got.body_md.count("蒜瓣打碎") == 1
|
||||
|
||||
|
||||
def test_blocked_wechat_page_is_reported() -> None:
|
||||
with pytest.raises(article_service.ArticleError):
|
||||
article_service.parse_article(BLOCKED_HTML, "https://mp.weixin.qq.com/s/abc")
|
||||
|
||||
Reference in New Issue
Block a user