docs(09): research phase 9 domain

This commit is contained in:
2026-03-17 22:37:38 +08:00
parent b3c7386098
commit 45ba9c973f
@@ -0,0 +1,360 @@
# Phase 9: Pipeline 整合 - Research
**Researched:** 2026-03-17
**Domain:** Pipeline Integration / Zhihu Crawler Integration
**Confidence:** HIGH
## Summary
Phase 9 需要将 Phase 7-8 实现的 ZhihuCrawler 和 ZhihuLogin 整合进现有 Pipeline 架构,替换原有的 ZhihuPlaywrightExtractor。核心工作是修改 ZhihuAdapter 的内部实现,同时保持公共接口不变,确保对 DownloadProcessor 无感知变化。
**Primary recommendation:** 采用"内部替换"策略 - 修改 ZhihuAdapter 内部调用 ZhihuCrawler,保持接口稳定,评论追加到正文,Cookie 过期自动触发扫码登录。
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
#### 整合方式
- **策略**: 修改现有 ZhihuAdapter 内部实现,切换到使用 ZhihuCrawler
- **接口保持**: 保持 ZhihuAdapter 的公共接口不变,对 DownloadProcessor 无感知变化
- **实现变更**: fetch_metadata() 和 fetch_content() 内部调用 ZhihuCrawler 而非 ZhihuPlaywrightExtractor
#### 评论数据处理
- **默认行为**: 评论爬取默认开启
- **存储方式**: 评论追加到正文末尾(Markdown 格式)
- **数量上限**: 20 条热门评论(使用 hot 排序,API 默认限制)
- **嵌套处理**: 不展开回复的回复,只爬第一层评论(继承自 Phase 8)
#### Cookie 过期处理
- **策略**: 自动触发扫码登录
- **流程**: 检测到 Cookie 失效时,调用 ZhihuLogin 触发扫码登录,用户扫码后继续执行
- **用户体验**: 用户会看到二维码提示,扫码后流程自动继续
#### 失败降级策略
- **策略**: 直接报错,不降级
- **不降级**: 新爬虫失败时不尝试旧版 ZhihuPlaywrightExtractor
- **错误处理**: 抛出异常,由上层 Pipeline 统一处理错误
### Claude's Discretion
- 评论追加到正文的具体 Markdown 格式(标题、分隔线等)
- 二维码显示的具体实现方式(终端/图片文件)
- 错误提示信息的具体文案
### Deferred Ideas (OUT OF SCOPE)
None — discussion stayed within phase scope
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| PIPE-01 | 替换现有 ZhihuProcessor,使用新的爬虫实现 | ZhihuAdapter 内部调用 ZhihuCrawler,保持接口不变 |
| PIPE-02 | 保持与现有 Pipeline 架构的兼容性 | PlatformAdapter 接口不变,DownloadProcessor 无需修改 |
| PIPE-03 | 支持断点续传和幂等执行 | WorkflowStateManager + is_step_completed 检查机制 |
</phase_requirements>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| pydantic | v2 | 数据验证和模型定义 | 项目已使用,CrawlResult/AnswerData/ArticleData 等模型 |
| playwright | sync_api | 浏览器自动化 | ZhihuCrawler 已使用,支持代理和 Cookie |
| tenacity | ^8.0 | 重试机制 | ZhihuCrawler 使用 Retrying 对象 |
| asyncio | stdlib | 异步执行 | PlatformAdapter 接口使用 async/await |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| kb_workflow.crawler.ZhihuCrawler | internal | 问答/专栏/评论爬取 | fetch_metadata/fetch_content 调用 |
| kb_workflow.auth.ZhihuLogin | internal | QR 码登录和 Cookie 管理 | Cookie 过期时触发 |
| kb_workflow.pipeline.errors.AuthError | internal | 认证错误处理 | Cookie 失效时抛出 |
### Key Classes
| Class | Location | Purpose |
|-------|----------|---------|
| ZhihuCrawler | `kb_workflow/crawler/zhihu_crawler.py` | 主爬虫,crawl_answer/crawl_article/crawl_comments |
| ZhihuLogin | `kb_workflow/auth/zhihu_login.py` | login_by_qrcode(),Cookie 持久化 |
| ZhihuAdapter | `kb_workflow/pipeline/platforms/zhihu.py` | **待修改** - PlatformAdapter 实现 |
| DownloadProcessor | `kb_workflow/pipeline/processors/download.py` | 调用 Adapter,无需修改 |
## Architecture Patterns
### Current ZhihuAdapter Implementation
```python
# 当前实现:使用 ZhihuPlaywrightExtractor (DOM 解析)
class ZhihuAdapter(PlatformAdapter):
def _get_extractor(self):
return ZhihuPlaywrightExtractor(headless=self.headless, cookie=self.cookie)
async def fetch_metadata(self, content_id, content_type, source_url=None):
result = await asyncio.to_thread(self._extract_with_playwright, url)
# 返回 metadata dict
async def fetch_content(self, content_id, content_type, source_url=None):
metadata = await self.fetch_metadata(...)
return self._format_with_frontmatter(metadata, content_markdown, content_type)
```
### Target Implementation Pattern
```python
# 目标实现:使用 ZhihuCrawler (API 拦截)
class ZhihuAdapter(PlatformAdapter):
def __init__(self, ...):
self._crawler = None
def _get_crawler(self) -> ZhihuCrawler:
if self._crawler is None:
self._crawler = ZhihuCrawler(headless=self.headless, timeout=self.timeout)
return self._crawler
async def fetch_metadata(self, content_id, content_type, source_url=None):
crawler = self._get_crawler()
if content_type == "answer":
result = await asyncio.to_thread(crawler.crawl_answer, url, include_comments=True)
elif content_type == "zhuanlan":
result = await asyncio.to_thread(crawler.crawl_article, url)
# 转换 CrawlResult -> metadata dict
async def fetch_content(self, content_id, content_type, source_url=None):
# 调用 fetch_metadata,格式化为 Markdown(含评论)
```
### Cookie Expiration Handling Pattern
```python
# Cookie 过期处理流程
async def fetch_metadata(self, content_id, content_type, source_url=None):
try:
result = self._crawl_with_retry(url)
if result.error and self._is_cookie_expired(result):
# 触发扫码登录
login_result = self._trigger_qrcode_login()
if login_result.success:
# 刷新配置并重试
get_settings.cache_clear()
result = self._crawl_with_retry(url)
return self._convert_to_metadata(result)
except Exception as e:
raise AuthError(step_name="zhihu_metadata", ...)
```
### Comment Append Format (Claude's Discretion)
```markdown
## 正文内容
[原回答/文章内容]
---
## 热门评论
1. **@用户A**: 评论内容...
2. **@用户B**: 评论内容...
3. **@用户C**: 评论内容...
```
### Anti-Patterns to Avoid
- **不要修改 PlatformAdapter 接口**: DownloadProcessor 依赖接口稳定
- **不要在 ZhihuCrawler 中添加 Pipeline 逻辑**: 保持爬虫职责单一
- **不要同步阻塞调用爬虫**: 使用 asyncio.to_thread 包装
- **不要忽略 Cookie 过期**: 必须触发扫码登录流程
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| HTML to Markdown | 自定义转换器 | MarkdownConverter (已存在) | zhihu_extractor.py 已有 |
| Cookie 解析 | 手动解析 cookie 字符串 | PlaywrightBrowser._add_cookies() | 已实现,处理格式正确 |
| 重试逻辑 | @retry 装饰器 | tenacity Retrying 对象 | 支持可配置超时,Phase 8 已用 |
| API 签名 | 逆向 x-zse-96 | 浏览器自动处理 | route.fetch() 自动包含签名 |
**Key insight:** ZhihuCrawler 已完整实现问答/专栏/评论爬取,只需要正确调用和结果转换。
## Common Pitfalls
### Pitfall 1: CrawlResult 到 Metadata Dict 转换不完整
**What goes wrong:** ZhihuCrawler 返回 CrawlResult(AnswerData/ArticleData),但 DownloadProcessor 期望特定格式的 metadata dict
**Why it happens:** 数据模型不完全匹配
**How to avoid:** 实现 _convert_crawl_result_to_metadata() 转换函数,确保所有字段正确映射
**Warning signs:** DownloadProcessor 提取 title/author 时返回空字符串
### Pitfall 2: 评论未正确追加到正文
**What goes wrong:** fetch_content() 返回的内容不包含评论
**Why it happens:** include_comments=True 未正确传递或评论格式化逻辑遗漏
**How to avoid:** fetch_content() 必须调用 fetch_metadata(include_comments=True),格式化时追加评论
**Warning signs:** 输出 Markdown 文件无评论部分
### Pitfall 3: Cookie 过期检测不准确
**What goes wrong:** 误判 Cookie 有效/无效,导致不必要的登录或无限重试
**Why it happens:** 仅依赖 HTTP 状态码,未检查响应内容
**How to avoid:** 检查 API 响应中的 error 字段和 401/403 状态码
**Warning signs:** 登录循环或认证错误未触发
### Pitfall 4: asyncio.to_thread 调用错误
**What goes wrong:** ZhihuCrawler 方法调用参数传递不正确
**Why it happens:** to_thread 需要函数引用和参数分开传递
**How to avoid:** `await asyncio.to_thread(crawler.crawl_answer, url, include_comments=True)`
**Warning signs:** TypeError: 'CrawlResult' object is not callable
## Code Examples
### ZhihuCrawler Usage (from existing code)
```python
# Source: kb_workflow/crawler/zhihu_crawler.py
crawler = ZhihuCrawler(headless=True, timeout=30)
# 爬取回答(含评论)
result = crawler.crawl_answer(
url="https://www.zhihu.com/question/123/answer/456",
include_comments=True
)
# result = CrawlResult(success=True, content_type="answer", data=AnswerData, comments=[...])
# 爬取专栏文章
result = crawler.crawl_article(url="https://zhuanlan.zhihu.com/p/123")
# result = CrawlResult(success=True, content_type="article", data=ArticleData)
```
### ZhihuLogin QR Code Flow (from existing code)
```python
# Source: kb_workflow/auth/zhihu_login.py
login = ZhihuLogin(intermediate_path=str(settings.intermediate_path), headless=False)
result = login.login_by_qrcode()
if result.success:
# 保存 Cookie 到配置文件
cookie_path = Path.home() / ".kb_env"
# 更新 KB_ZHIHU_COOKIE 环境变量
# 刷新 settings 缓存
get_settings.cache_clear()
```
### Metadata Conversion Pattern
```python
def _convert_answer_to_metadata(self, result: CrawlResult) -> Dict[str, Any]:
"""Convert CrawlResult[AnswerData] to metadata dict."""
answer: AnswerData = result.data
return {
"type": "answer",
"title": answer.question_title,
"author": answer.metadata.author.name if answer.metadata.author else "",
"content": answer.content,
"content_markdown": self._html_to_markdown(answer.content),
"publish_time": self._format_timestamp(answer.metadata.created_time),
"url": f"https://www.zhihu.com/question/{answer.question_id}/answer/{answer.id}",
"question_id": str(answer.question_id),
"answer_id": str(answer.id),
"vote_up": answer.metadata.voteup_count,
"comment_count": answer.metadata.comment_count,
}
```
### Comment Formatting Pattern
```python
def _format_comments_section(self, comments: List[CommentData]) -> str:
"""Format comments as Markdown section."""
if not comments:
return ""
lines = ["\n\n---\n\n## 热门评论\n"]
for i, comment in enumerate(comments[:20], 1): # 最多 20 条
author = f"@{comment.author.name}" if comment.author else "匿名"
lines.append(f"{i}. **{author}**: {comment.content}")
return "\n".join(lines)
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| ZhihuPlaywrightExtractor (DOM 解析) | ZhihuCrawler (API 拦截) | Phase 8 | 更可靠的数据获取,支持评论 |
| 手动 Cookie 管理 | ZhihuLogin QR 码登录 | Phase 7 | 自动化登录流程 |
| 无评论支持 | 评论爬取 + 追加到正文 | Phase 8 | 更完整的内容存档 |
**Deprecated/outdated:**
- ZhihuPlaywrightExtractor 直接调用: 将被 ZhihuCrawler 替代
- 手动设置 KB_ZHIHU_COOKIE: 通过 login 命令自动管理
## Open Questions
1. **评论 Markdown 格式细节**
- What we know: 用户决策评论追加到正文,分隔线 + 热门评论标题
- What's unclear: 具体的分隔线样式、评论编号格式
- Recommendation: 使用 `---` 分隔线,`1. **@用户**: 内容` 格式
2. **Cookie 过期检测的精确条件**
- What we know: API 返回 401 或特定错误信息
- What's unclear: 具体的错误码和响应结构
- Recommendation: 检查 response.status == 401 或 JSON 中的 error 字段
3. **ZhihuLogin 在 headless 模式下的二维码展示**
- What we know: QR 码保存到文件,路径返回给用户
- What's unclear: 是否需要终端 ASCII 输出
- Recommendation: 默认保存文件并打印路径,可选终端输出
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | pytest |
| Config file | pytest.ini |
| Quick run command | `pytest tests/test_platforms/test_zhihu.py -x -v` |
| Full suite command | `pytest tests/ -v` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| PIPE-01 | ZhihuAdapter 使用 ZhihuCrawler | unit | `pytest tests/test_platforms/test_zhihu.py -x` | ✅ 需修改 |
| PIPE-02 | DownloadProcessor 兼容性 | integration | `pytest tests/test_processors/test_download.py -x` | ✅ 已存在 |
| PIPE-03 | 断点续传/幂等执行 | unit | `pytest tests/test_pipeline/test_idempotency.py -x` | ✅ 已存在 |
### Sampling Rate
- **Per task commit:** `pytest tests/test_platforms/test_zhihu.py -x -v`
- **Per wave merge:** `pytest tests/test_platforms/ tests/test_processors/test_download.py -v`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `tests/test_platforms/test_zhihu.py` — 更新测试使用 ZhihuCrawler mock
- [ ] 新增测试:Cookie 过期自动触发登录流程
- [ ] 新增测试:评论追加到正文格式验证
- [ ] 新增测试:CrawlResult 到 metadata 转换正确性
*(现有测试基础设施完善,主要是更新和新增 ZhihuAdapter 相关测试)*
## Sources
### Primary (HIGH confidence)
- `kb_workflow/crawler/zhihu_crawler.py` - ZhihuCrawler 实现
- `kb_workflow/crawler/models.py` - CrawlResult, AnswerData, ArticleData, CommentData
- `kb_workflow/auth/zhihu_login.py` - ZhihuLogin QR 码登录
- `kb_workflow/pipeline/platforms/zhihu.py` - ZhihuAdapter (待修改)
- `kb_workflow/pipeline/processors/download.py` - DownloadProcessor (无需修改)
### Secondary (MEDIUM confidence)
- `tests/test_platforms/test_zhihu.py` - 现有 ZhihuAdapter 测试
- `tests/test_crawler/` - ZhihuCrawler 测试
### Tertiary (LOW confidence)
- None - 所有信息均来自项目代码
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - 项目已有完整实现,只需整合
- Architecture: HIGH - PlatformAdapter 接口稳定,模式清晰
- Pitfalls: HIGH - 基于现有代码分析,问题可预见
**Research date:** 2026-03-17
**Valid until:** 30 days - 项目内部代码,稳定性高