This repository has been archived on 2026-08-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
kb-skills/.planning/phases/02-platform-skills/02-RESEARCH.md
T

478 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 2: Platform Skills - Research
**Researched:** 2026-03-13
**Domain:** Python / B站知乎API集成 / 内容获取管道
**Confidence:** HIGH
## Summary
Phase 2 需要集成B站和知乎平台的内容获取能力,形成从链接到原始内容的完整流程。项目已有完整的平台API实现(kb-bilibili-metadata、kb-zhihu-metadata),以及链接解析器(kb-link-parser)。Phase 1 已建立了核心管道基础设施(schemas.py、errors.py、config.py、retry.py)。
**Primary recommendation:** 复用现有平台提取器实现,通过统一 LinkParser 类整合链接解析,使用 Phase 1 的 StepInput/StepOutput 模式包装现有功能,确保幂等检查和网络重试机制。
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
#### 链接解析策略
- **解析器结构**: 统一 LinkParser 类 — 一个模块处理所有平台,通过正则/模式匹配区分
- **支持 URL 类型**:
- B站视频: bilibili.com/video/BVxxx, b23.tv/xxx(短链解析)
- B站专栏: bilibili.com/read/cvxxx
- 知乎问答: zhihu.com/question/xxx/answer/xxx
- 知乎专栏: zhuanlan.zhihu.com/p/xxx
- **ID 提取**: BV号 + AV号(同时支持,自动转换)
- **短链支持**: 支持 b23.tv 短链解析,获取真实 URL
- **错误处理**: 无法识别的 URL 返回 None + 错误信息(不抛异常)
#### 元数据输出格式
- **B站视频字段**: 标题、作者、时长、发布时间、描述、封面、链接、ID(BV号)
- **知乎字段**: 标题、作者、发布时间、内容摘要、链接、ID
- **文件命名**: 平台_ID.json(如 bilibili_BV1xx.json, zhihu_12345.json)
- **存储位置**: 内容目录下(与内容文件同目录)
#### 内容输出格式
- **Markdown 结构**: YAML Frontmatter + 正文内容
- **Frontmatter 字段**: title, author, source_url, platform, content_type, fetched_at
- **图片处理**: 保留原始链接(不下载到本地)
- **格式保留**: 基础格式(标题、段落、列表、代码块)
- **不保留**: 表格、数学公式、LaTeX(Phase 2 不处理)
#### 幂等检查机制
- **检查时机**: 链接解析后 — 解析成功后立即检查目录是否存在
- **判断依据**: 检查输出文件是否存在(元数据 JSON + 内容 Markdown)
- **重复处理**: 询问用户 — 发现已存在文件时询问是否重新下载
### Claude's Discretion
- LinkParser 的正则表达式具体实现
- 文件名清理规则(特殊字符处理)
- 网络请求超时时间和重试次数(复用 Phase 1 的 retry 机制)
### Deferred Ideas (OUT OF SCOPE)
- B站/知乎用户主页跟踪 — 未来功能
- 批量链接处理 — 当前只处理单个链接
- 进度显示 — v2 考虑
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| PLAT-01 | B站视频处理 - 支持 B站视频的元数据提取、字幕下载、语音识别、深度分析 | 复用 kb-bilibili-metadata/bilibili_extractor.py 的 BilibiliExtractor 类,支持视频元数据和专栏提取 |
| PLAT-02 | B站专栏处理 - 支持 B站专栏的文字提取和深度分析 | 复用 kb-bilibili-content/bilibili_content.py 的 extract_article() 方法 |
| PLAT-03 | 知乎问答/专栏处理 - 支持知乎问答和专栏的文字提取和深度分析 | 复用 kb-zhihu-metadata/zhihu_extractor.py 的 ZhihuPlaywrightExtractor 类 |
</phase_requirements>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| pydantic | v2 | Schema 验证 | Phase 1 已采用,类型安全,自动验证 |
| pydantic-settings | v2 | 配置管理 | Phase 1 已采用,统一从 ~/.kb_env 加载 |
| tenacity | 8.x | 重试机制 | Phase 1 已采用,指数退避,装饰器模式 |
| httpx | 0.27+ | HTTP 客户端 | 现有代码已使用,支持异步,比 requests 更现代 |
| aiohttp | 3.9+ | 异步 HTTP | 现有代码已使用,用于 B站 API 调用 |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| playwright | 1.40+ | 浏览器自动化 | 知乎内容提取、B站专栏全文获取 |
| yt-dlp | latest | 视频下载 | B站视频下载(替代 bilibili-dl) |
| requests | 2.31+ | 同步 HTTP | 短链解析、简单 API 调用 |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| playwright | selenium | Playwright 更现代,API 更友好,异步支持更好 |
| httpx | requests | httpx 支持异步,更适合高并发场景;requests 更简单稳定 |
**Installation:**
```bash
pip install pydantic pydantic-settings tenacity httpx aiohttp playwright yt-dlp requests
playwright install chromium # Playwright 浏览器安装
```
## Architecture Patterns
### Recommended Project Structure
```
kb_pipeline/
├── __init__.py # 导出公共 API
├── schemas.py # StepInput/StepOutput/WorkflowState(Phase 1 已完成)
├── errors.py # KBError 体系(Phase 1 已完成)
├── config.py # KBSettings 配置(Phase 1 已完成)
├── retry.py # retry_on_network_error(Phase 1 已完成)
├── state.py # WorkflowStateManager(Phase 1 已完成)
├── link_parser.py # 统一链接解析器(Phase 2 新增)
├── platforms/ # 平台适配器(Phase 2 新增)
│ ├── __init__.py
│ ├── base.py # 平台适配器基类
│ ├── bilibili.py # B站适配器
│ └── zhihu.py # 知乎适配器
└── content_fetcher.py # 内容获取协调器(Phase 2 新增)
```
### Pattern 1: 平台适配器模式
**What:** 为每个平台创建统一的适配器接口,封装元数据提取和内容获取逻辑
**When to use:** 需要支持多个内容平台时
**Example:**
```python
# Source: 项目设计模式
from abc import ABC, abstractmethod
from typing import Dict, Any, Optional
from kb_pipeline.schemas import StepInput, StepOutput
class PlatformAdapter(ABC):
"""平台适配器基类"""
@abstractmethod
def get_content_type(self, url: str) -> str:
"""识别内容类型 (video/article/answer/zhuanlan)"""
pass
@abstractmethod
async def fetch_metadata(self, content_id: str, content_type: str) -> Dict[str, Any]:
"""获取元数据"""
pass
@abstractmethod
async def fetch_content(self, content_id: str, content_type: str) -> str:
"""获取正文内容(Markdown格式)"""
pass
@abstractmethod
def get_output_filename(self, content_id: str, content_type: str) -> str:
"""生成输出文件名"""
pass
```
### Pattern 2: 链接解析统一入口
**What:** 统一 LinkParser 类处理所有平台的 URL 解析
**When to use:** 任何需要从 URL 提取平台和内容 ID 的场景
**Example:**
```python
# Source: 基于现有 kb-link-parser/link-parser.py 重构
import re
from typing import Optional, Dict, Any
from dataclasses import dataclass
@dataclass
class ParsedLink:
"""解析后的链接信息"""
platform: str # "bilibili" | "zhihu"
content_type: str # "video" | "article" | "answer" | "zhuanlan"
content_id: str # BV号、CV号、知乎ID等
original_url: str # 原始 URL
resolved_url: str # 解析后的完整 URL(短链展开后)
class LinkParser:
"""统一链接解析器"""
# B站正则模式
BILIBILI_PATTERNS = {
"video": re.compile(r'bilibili\.com/video/(BV[a-zA-Z0-9]+)'),
"article": re.compile(r'bilibili\.com/read/cv(\d+)'),
"short": re.compile(r'b23\.tv/([a-zA-Z0-9]+)'),
}
# 知乎正则模式
ZHIHU_PATTERNS = {
"answer": re.compile(r'zhihu\.com/question/\d+/answer/(\d+)'),
"zhuanlan": re.compile(r'zhuanlan\.zhihu\.com/p/(\d+)'),
"zvideo": re.compile(r'zhihu\.com/zvideo/(\d+)'),
}
def parse(self, url: str) -> Optional[ParsedLink]:
"""解析 URL,返回结构化信息或 None"""
# 1. 先处理短链
resolved = self._resolve_short_link(url)
# 2. 匹配平台模式
for platform, patterns in [("bilibili", self.BILIBILI_PATTERNS),
("zhihu", self.ZHIHU_PATTERNS)]:
for content_type, pattern in patterns.items():
match = pattern.search(resolved)
if match:
return ParsedLink(
platform=platform,
content_type=content_type,
content_id=match.group(1),
original_url=url,
resolved_url=resolved
)
return None # 无法识别,返回 None(不抛异常)
def _resolve_short_link(self, url: str) -> str:
"""解析短链"""
# 复用现有 link_parser_shortlink.py 的逻辑
if "b23.tv" in url:
try:
import requests
resp = requests.head(url, allow_redirects=True, timeout=10)
return resp.url
except Exception:
pass
return url
```
### Pattern 3: 幂等检查机制
**What:** 在执行前检查输出文件是否已存在
**When to use:** 避免重复下载和处理
**Example:**
```python
# Source: CONTEXT.md 决策
from pathlib import Path
from typing import Tuple
class IdempotencyChecker:
"""幂等检查器"""
def check_exists(self, output_dir: Path, content_id: str) -> Tuple[bool, list[Path]]:
"""检查文件是否已存在
Returns:
(exists, files): 是否存在,存在的文件列表
"""
metadata_file = output_dir / f"{content_id}_metadata.json"
content_file = output_dir / f"{content_id}_content.md"
existing = []
if metadata_file.exists():
existing.append(metadata_file)
if content_file.exists():
existing.append(content_file)
return len(existing) > 0, existing
def prompt_redownload(self, existing_files: list[Path]) -> bool:
"""询问用户是否重新下载
Returns:
True 表示重新下载,False 表示跳过
"""
file_list = "\n".join(f" - {f.name}" for f in existing_files)
print(f"内容已存在:\n{file_list}")
response = input("是否重新下载?[y/N]: ").strip().lower()
return response == 'y'
```
### Anti-Patterns to Avoid
- **每个平台单独的 CLI 入口:** 不要为每个平台创建独立的命令行工具,应统一到工作流管道
- **硬编码文件路径:** 使用 KBSettings 配置,不要硬编码输出目录
- **忽略网络错误:** 必须使用 retry_on_network_error 装饰器处理网络请求
- **同步阻塞调用:** 元数据获取和内容下载应使用 async/await
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| BV/AV号转换 | 自己实现算法 | bilibili-api 库的转换函数 | 算法复杂,易出错 |
| 短链解析 | HTTP 重定向处理 | requests.head(allow_redirects=True) | 自动处理重定向链 |
| HTML 转 Markdown | 正则替换 | markdown_converter.py 已有实现 | 处理各种 HTML 边界情况 |
| 浏览器自动化 | selenium 脚本 | Playwright + 现有提取器 | 现有代码已完整实现 |
**Key insight:** 现有代码库已有完整的平台 API 实现,Phase 2 的核心工作是**整合**而非**重写**。
## Common Pitfalls
### Pitfall 1: B站 API 403 错误
**What goes wrong:** 直接调用 B站 API 返回 403 Forbidden
**Why it happens:** B站有反爬机制,需要正确的 User-Agent 和 Referer
**How to avoid:** 使用现有 BilibiliExtractor 的 DEFAULT_HEADERS
**Warning signs:** API 返回 403,或返回空数据
### Pitfall 2: 知乎需要登录
**What goes wrong:** 知乎内容无法获取,返回登录页面
**Why it happens:** 知乎对未登录用户限制内容访问
**How to avoid:** 从 KBSettings 获取 zhihu_cookie,传递给 Playwright 浏览器
**Warning signs:** 内容为空,页面标题包含"登录"
### Pitfall 3: 短链解析超时
**What goes wrong:** b23.tv 短链解析卡住或超时
**Why it happens:** 网络问题或短链服务不可用
**How to avoid:** 设置合理的 timeout(10秒),失败时返回原始 URL
**Warning signs:** 请求超时,网络异常
### Pitfall 4: 文件名包含特殊字符
**What goes wrong:** 标题包含 / : * ? " < > | 等字符导致文件创建失败
**Why it happens:** 操作系统文件系统限制
**How to avoid:** 实现文件名清理函数,移除或替换非法字符
**Warning signs:** FileNotFoundError,PermissionError
### Pitfall 5: 幂等检查遗漏
**What goes wrong:** 重复执行产生重复文件或覆盖已有内容
**Why it happens:** 忘记在执行前检查文件是否存在
**How to avoid:** 在 LinkParser 解析成功后立即检查,遵循 CONTEXT.md 的决策
**Warning signs:** workflow_state.json 中步骤状态与实际文件不一致
## Code Examples
### B站视频元数据获取
```python
# Source: kb-bilibili-metadata/bilibili_extractor.py
from kb_bilibili_metadata.bilibili_extractor import BilibiliExtractor
async def fetch_bilibili_video(bvid: str) -> dict:
"""获取 B站视频元数据"""
extractor = BilibiliExtractor(headless=True, timeout=30)
result = extractor.extract_video_metadata(bvid)
# 提取核心字段
return {
"title": result["metadata"]["title"],
"author": result["metadata"]["author"],
"duration": result["metadata"]["duration"],
"pubtime": result["metadata"]["pubtime"],
"description": result["description"],
"stats": result["stats"],
"bvid": result["metadata"]["bvid"],
"url": result["metadata"]["url"],
}
```
### 知乎内容提取
```python
# Source: kb-zhihu-metadata/zhihu_extractor.py
from kb_zhihu_metadata.zhihu_extractor import ZhihuPlaywrightExtractor
from kb_pipeline.config import get_settings
async def fetch_zhihu_content(url: str) -> dict:
"""获取知乎内容"""
settings = get_settings()
cookie = settings.zhihu_cookie.get_secret_value()
extractor = ZhihuPlaywrightExtractor(
headless=True,
timeout=30,
cookie=cookie if cookie else None
)
result = extractor.extract(url)
if not result:
return None
return {
"type": result["type"],
"title": result["title"],
"author": result["author"],
"content": result["content_markdown"],
"publish_time": result["publish_time"],
"url": url,
"content_id": result["content_id"],
}
```
### 内容输出格式
```python
# Source: CONTEXT.md 决策
from datetime import datetime
import yaml
def format_content_markdown(metadata: dict, content: str) -> str:
"""生成标准 Markdown 格式(YAML Frontmatter + 正文)"""
frontmatter = {
"title": metadata["title"],
"author": metadata["author"],
"source_url": metadata["url"],
"platform": metadata["platform"],
"content_type": metadata["content_type"],
"fetched_at": datetime.now().isoformat(),
}
return f"---\n{yaml.dump(frontmatter, allow_unicode=True)}---\n\n{content}"
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| 各平台独立脚本 | 统一管道模型 | Phase 1 | 可复用基础设施,统一错误处理 |
| 手动处理短链 | 自动解析 + 回退 | 现有 | 提升用户体验,减少手动操作 |
| 同步阻塞请求 | 异步并发 | 现有 | 提升性能,支持并发处理 |
**Deprecated/outdated:**
- bilibili-dl: 已被 yt-dlp 替代,yt-dlp 支持更多平台且维护活跃
- selenium: 已被 Playwright 替代,Playwright API 更现代
## Open Questions
1. **Playwright 浏览器安装**
- What we know: Playwright 需要单独安装浏览器(`playwright install chromium`)
- What's unclear: 是否需要在首次运行时自动检测并提示安装
- Recommendation: 在 README 中说明安装步骤,运行时检测并给出友好提示
2. **Cookie 过期处理**
- What we know: 知乎需要 Cookie 才能获取完整内容
- What's unclear: Cookie 过期时的错误提示和刷新机制
- Recommendation: 检测登录页面特征,返回 AuthError 并提示更新 Cookie
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | pytest 8.x |
| Config file | pytest.ini |
| Quick run command | `pytest tests/test_pipeline/test_link_parser.py -x -v` |
| Full suite command | `pytest tests/ -v --cov=kb_pipeline` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| PLAT-01 | B站视频元数据提取 | unit | `pytest tests/test_platforms/test_bilibili.py::test_video_metadata -x` | Wave 0 |
| PLAT-01 | B站视频内容下载 | integration | `pytest tests/test_platforms/test_bilibili.py::test_video_content -x` | Wave 0 |
| PLAT-02 | B站专栏内容提取 | unit | `pytest tests/test_platforms/test_bilibili.py::test_article_content -x` | Wave 0 |
| PLAT-03 | 知乎问答内容提取 | unit | `pytest tests/test_platforms/test_zhihu.py::test_answer_content -x` | Wave 0 |
| PLAT-03 | 知乎专栏内容提取 | unit | `pytest tests/test_platforms/test_zhihu.py::test_zhuanlan_content -x` | Wave 0 |
| PLAT-01/02/03 | 链接解析 | unit | `pytest tests/test_pipeline/test_link_parser.py -x` | Wave 0 |
| PLAT-01/02/03 | 幂等检查 | unit | `pytest tests/test_pipeline/test_idempotency.py -x` | Wave 0 |
### Sampling Rate
- **Per task commit:** `pytest tests/test_pipeline/test_link_parser.py tests/test_platforms/ -x`
- **Per wave merge:** `pytest tests/ -v --cov=kb_pipeline --cov-report=term-missing`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `tests/test_pipeline/test_link_parser.py` — LinkParser 单元测试
- [ ] `tests/test_pipeline/test_idempotency.py` — 幂等检查测试
- [ ] `tests/test_platforms/__init__.py` — 测试模块初始化
- [ ] `tests/test_platforms/conftest.py` — 平台测试共享 fixtures
- [ ] `tests/test_platforms/test_bilibili.py` — B站适配器测试
- [ ] `tests/test_platforms/test_zhihu.py` — 知乎适配器测试
- [ ] Mock fixtures: B站/知乎 API 响应数据
## Sources
### Primary (HIGH confidence)
- 现有代码: `kb-bilibili-metadata/bilibili_extractor.py` - B站 API 调用实现
- 现有代码: `kb-zhihu-metadata/zhihu_extractor.py` - 知乎 Playwright 提取器
- 现有代码: `kb-link-parser/link-parser.py` - 链接解析器
- 现有代码: `kb_pipeline/schemas.py` - Phase 1 Schema 定义
### Secondary (MEDIUM confidence)
- Phase 1 实现: `kb_pipeline/errors.py`, `kb_pipeline/config.py`, `kb_pipeline/retry.py`
- 现有代码: `kb_workflow/kb_workflow.py` - 子技能映射和内容类型判断
### Tertiary (LOW confidence)
- N/A - 本阶段研究基于现有代码库,无外部来源
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - 基于 Phase 1 已采用的技术栈和现有代码
- Architecture: HIGH - 现有代码已有成熟实现,主要是整合工作
- Pitfalls: HIGH - 基于现有代码中已处理的问题和 CONTEXT.md 决策
**Research date:** 2026-03-13
**Valid until:** 30 days - Python 和相关库相对稳定
---
*Research complete. Ready for planning phase.*