docs(07): research phase domain - login auth system

This commit is contained in:
2026-03-17 17:08:30 +08:00
parent 58487b0fa3
commit 1d1f3309b2
@@ -0,0 +1,474 @@
# Phase 7: 登录认证体系 - Research
**Researched:** 2026-03-17
**Domain:** Playwright 浏览器自动化、知乎登录认证、Cookie 管理
**Confidence:** HIGH
## Summary
本阶段实现知乎登录认证体系,支持二维码扫码和手动 Cookie 两种认证方式。核心技术基于 Playwright 浏览器自动化,参考 MediaCrawler 项目的成熟实现模式。
关键实现点:
1. **二维码登录流程**:访问登录页 → Canvas 截图获取二维码 → 保存图片 → 轮询检测 `z_c0` Cookie → 超时退出
2. **Cookie 验证**:通过 `GET /api/v4/me` 接口检测登录状态
3. **Cookie 持久化**:存储到 `~/.kb_env` 文件,格式为 `ZHIHU_COOKIE="name1=value1; name2=value2"`
**Primary recommendation:** 扩展现有 `PlaywrightBrowser` 类添加二维码登录能力,复用 `KBSettings` 的 `zhihu_cookie` 字段,新增 `login` CLI 子命令。
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
#### 二维码显示方式
- **显示方式**: 保存为图片文件,作为标准 step 输出(让 AI 代理能够获取并展示)
- **存储位置**: 中间目录(与现有中间文件存放位置一致)
- **等待超时**: 60 秒
- **超时行为**: 轮询检测,超时退出(不自动重试)
#### Cookie 存储策略
- **存储位置**: `~/.kb_env` 文件(与现有配置文件一致)
- **输入格式**: 支持 JSON 导入(Chrome Cookie 导出插件格式),内部存储为字符串
- **存储格式**: 原始 Cookie 字符串,例如 `ZHIHU_COOKIE="name1=value1; name2=value2"`
- **加密**: 不加密,用户需自行保护 `~/.kb_env` 文件权限
#### 过期检测机制
- **检测时机**: 每次使用前检测 Cookie 有效性
- **检测方法**: 请求知乎用户信息接口,检查返回状态
- **过期处理**: 自动触发扫码重新登录
#### 命令行交互流程
- **登录命令**: `python3 kb-workflow/kb_workflow.py login`
- **默认模式**: 扫码模式(弹出二维码让用户扫码)
- **手动设置**: `--import-cookie cookies.json` 从文件导入 JSON 格式 Cookie
- **状态查看**: `login --status` 检查当前 Cookie 是否有效
### Claude's Discretion
- 二维码图片的具体文件名格式
- 用户信息接口的具体 URL 和返回格式
- JSON Cookie 解析的具体实现
- 错误提示信息的具体文案
### Deferred Ideas (OUT OF SCOPE)
None - discussion stayed within phase scope
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|-----------------|
| AUTH-01 | 用户可通过二维码扫码登录知乎 | Playwright Canvas 截图 + `z_c0` Cookie 轮询检测 |
| AUTH-02 | 用户可手动设置 Cookie 进行认证 | JSON 解析 + `_add_cookies()` 方法复用 |
| AUTH-03 | Cookie 自动持久化到本地文件,下次自动复用 | `KBSettings.zhihu_cookie` + `~/.kb_env` 写入 |
| AUTH-04 | Cookie 过期时自动检测并提示重新登录 | `/api/v4/me` 接口验证 + 状态码检测 |
</phase_requirements>
## Standard Stack
### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| playwright | 已安装 | 浏览器自动化 | 现有项目已使用,支持 Cookie、Canvas 操作 |
| pydantic | v2 | Schema 验证 | 现有项目使用,类型安全 |
| pydantic-settings | 已安装 | 配置管理 | 现有 KBSettings 使用 |
| tenacity | 需安装 | 重试/轮询逻辑 | 登录状态轮询的标准库 |
### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| rich | 可选 | 终端输出美化 | CLI 交互提示 |
| httpx | 可选 | HTTP 请求 | Cookie 验证接口调用(或复用 Playwright) |
### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| playwright | selenium | Playwright 更现代,项目已使用 |
| playwright | requests + 手动逆向 | 免维护,无需逆向签名算法 |
| tenacity | 自实现轮询 | tenacity 提供声明式重试,更可靠 |
**Installation:**
```bash
pip install tenacity
```
## Architecture Patterns
### Recommended Project Structure
```
kb_workflow/
├── auth/ # 新增:认证模块
│ ├── __init__.py
│ ├── zhihu_login.py # 知乎登录核心逻辑
│ └── cookie_manager.py # Cookie 持久化管理
├── metadata/
│ └── zhihu_extractor.py # 现有:PlaywrightBrowser 类(可扩展)
├── pipeline/
│ └── config.py # 现有:KBSettings(已有 zhihu_cookie)
└── kb_workflow.py # 现有:CLI 入口(添加 login 子命令)
```
### Pattern 1: 二维码登录流程
**What:** 使用 Playwright 访问知乎登录页,从 Canvas 元素截图获取二维码,轮询检测登录状态
**When to use:** 默认登录模式
**Example:**
```python
# Source: MediaCrawler media_platform/zhihu/login.py
import asyncio
from tenacity import retry, stop_after_attempt, wait_fixed
class ZhihuLogin:
LOGIN_URL = "https://www.zhihu.com/signin"
CANVAS_SELECTOR = "canvas.SignIn-qrcode"
@retry(stop=stop_after_attempt(60), wait=wait_fixed(1))
async def check_login_state(self) -> bool:
"""轮询检测登录状态 - 检查 z_c0 Cookie"""
cookies = await self.context.cookies()
for cookie in cookies:
if cookie["name"] == "z_c0":
return True
return False
async def login_by_qrcode(self):
"""二维码登录主流程"""
# 1. 访问登录页
await self.page.goto(self.LOGIN_URL)
# 2. 从 Canvas 截图获取二维码
canvas = await self.page.wait_for_selector(self.CANVAS_SELECTOR)
screenshot = await canvas.screenshot()
qr_base64 = base64.b64encode(screenshot).decode()
# 3. 保存二维码图片
qrcode_path = Path(f"~/work/intermediate/zhihu_qrcode_{timestamp}.png")
qrcode_path.write_bytes(screenshot)
# 4. 轮询等待扫码(60秒超时)
try:
logged_in = await self.check_login_state()
if logged_in:
# 5. 提取并保存 Cookie
cookies = await self.context.cookies()
cookie_str = self._format_cookies(cookies)
return cookie_str
except RetryError:
raise TimeoutError("扫码超时,请重试")
```
### Pattern 2: Cookie 验证
**What:** 通过知乎 API 检测 Cookie 是否有效
**When to use:** 每次使用 Cookie 前、`login --status` 命令
**Example:**
```python
# Source: 知乎 API 实测
VALIDATION_URL = "https://www.zhihu.com/api/v4/me"
async def validate_cookie(self, cookie_str: str) -> dict:
"""验证 Cookie 有效性"""
# 方式1:使用 Playwright context(推荐,与登录流程一致)
self.browser.start(cookie=cookie_str)
response = await self.page.request.get(self.VALIDATION_URL)
if response.status == 200:
user_info = await response.json()
return {
"valid": True,
"username": user_info.get("name", ""),
"user_id": user_info.get("id", "")
}
elif response.status == 401:
return {"valid": False, "reason": "Cookie 已过期"}
else:
return {"valid": False, "reason": f"验证失败: {response.status}"}
```
### Pattern 3: JSON Cookie 导入
**What:** 解析 Chrome Cookie Editor 导出的 JSON 格式
**When to use:** `--import-cookie` 参数
**Example:**
```python
# Chrome Cookie Editor JSON 格式:
# [{"name": "z_c0", "value": "xxx", "domain": ".zhihu.com"}, ...]
def parse_json_cookies(json_path: str) -> str:
"""从 JSON 文件解析 Cookie 为字符串格式"""
import json
with open(json_path, 'r') as f:
cookies = json.load(f)
# 过滤知乎域名,转换为 name=value 格式
zhihu_cookies = [
f"{c['name']}={c['value']}"
for c in cookies
if 'zhihu.com' in c.get('domain', '')
]
return '; '.join(zhihu_cookies)
```
### Pattern 4: Cookie 持久化
**What:** 将 Cookie 写入 `~/.kb_env` 文件
**When to use:** 登录成功后、导入 Cookie 后
**Example:**
```python
# 复用现有 KBSettings 的 zhihu_cookie 字段
# 存储格式: ZHIHU_COOKIE="name1=value1; name2=value2"
def save_cookie_to_env(cookie_str: str):
"""保存 Cookie 到 ~/.kb_env"""
env_path = Path.home() / ".kb_env"
# 读取现有内容
lines = []
if env_path.exists():
lines = env_path.read_text().splitlines()
# 更新或添加 ZHIHU_COOKIE 行
cookie_line = f'ZHIHU_COOKIE="{cookie_str}"'
found = False
for i, line in enumerate(lines):
if line.startswith("ZHIHU_COOKIE="):
lines[i] = cookie_line
found = True
break
if not found:
lines.append(cookie_line)
# 写回文件
env_path.write_text('\n'.join(lines) + '\n')
# 清除配置缓存
get_settings.cache_clear()
```
### Anti-Patterns to Avoid
- **手动逆向 x-zse-96 签名**: 维护成本高,算法经常变化。使用二维码登录完全避免此问题。
- **在终端直接显示二维码 ASCII**: 兼容性差,AI 代理无法读取。保存为图片文件是更可靠的方案。
- **硬编码超时逻辑**: 使用 tenacity 的 `@retry` 装饰器更清晰、可测试。
- **忽略 Cookie 域名过滤**: 导入时必须过滤 `.zhihu.com` 域名,否则会导入无关 Cookie。
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| 轮询重试逻辑 | while loop + sleep | tenacity @retry | 声明式、可配置、有异常处理 |
| Cookie 字符串解析 | 手动 split(';') | 现有 `_add_cookies()` 方法 | 已处理边界情况 |
| 配置文件读写 | 手动文件操作 | pydantic-settings | 自动类型转换、环境变量支持 |
| Canvas 截图 | 手动像素提取 | Playwright element.screenshot() | 内置优化,支持多种格式 |
**Key insight:** 现有代码库已有 Playwright 封装和配置管理,优先复用而非新建。
## Common Pitfalls
### Pitfall 1: Canvas 选择器变化
**What goes wrong:** 知乎更新前端后,Canvas 选择器 `canvas.SignIn-qrcode` 失效
**Why it happens:** 前端类名可能随版本变化
**How to avoid:**
1. 使用多个备选选择器
2. 添加选择器存在性检测
3. 失败时给出明确错误提示
**Warning signs:** `wait_for_selector` 超时
```python
CANVAS_SELECTORS = [
"canvas.SignIn-qrcode",
"canvas[class*='qrcode']",
"canvas" # 最宽松的备选
]
async def find_qrcode_canvas(self):
for selector in self.CANVAS_SELECTORS:
try:
return await self.page.wait_for_selector(selector, timeout=5000)
except TimeoutError:
continue
raise RuntimeError("无法找到二维码 Canvas 元素")
```
### Pitfall 2: z_c0 Cookie 延迟出现
**What goes wrong:** 用户扫码后,`z_c0` Cookie 不是立即出现
**Why it happens:** 服务端验证需要时间
**How to avoid:** 使用 tenacity 的 1 秒间隔轮询,60 次重试(60 秒总超时)
**Warning signs:** 扫码成功但检测失败
### Pitfall 3: Cookie 格式不一致
**What goes wrong:** 不同来源的 Cookie 格式不同(字符串 vs JSON vs 字典)
**Why it happens:** Chrome 插件、手动复制、Playwright 导出格式各异
**How to avoid:**
1. 统一内部格式为字符串 `name1=value1; name2=value2`
2. 在边界层做格式转换
3. 提供格式自动检测
**Warning signs:** Cookie 设置后仍显示未登录
### Pitfall 4: ~/.kb_env 文件权限
**What goes wrong:** Cookie 明文存储可能被其他用户读取
**Why it happens:** 文件创建时使用默认 umask
**How to avoid:**
1. 创建文件时设置 600 权限
2. 提示用户检查权限
```python
env_path = Path.home() / ".kb_env"
env_path.touch(mode=0o600, exist_ok=True)
```
## Code Examples
### CLI 子命令扩展
```python
# Source: 现有 kb_workflow.py 模式
# 在 main() 函数中添加 login 子命令
def add_login_subparser(subparsers):
"""添加 login 子命令"""
login_parser = subparsers.add_parser('login', help='知乎登录认证')
login_parser.add_argument('--import-cookie', '-i', type=str,
help='从 JSON 文件导入 Cookie')
login_parser.add_argument('--status', action='store_true',
help='检查当前 Cookie 状态')
return login_parser
# 在 main() 中:
if args.command == 'login':
if args.status:
result = check_cookie_status()
elif args.import_cookie:
result = import_cookie_from_file(args.import_cookie)
else:
result = login_by_qrcode()
print(json.dumps(result, ensure_ascii=False, indent=2))
```
### 扩展 PlaywrightBrowser
```python
# Source: 现有 zhihu_extractor.py PlaywrightBrowser 类
# 添加二维码登录相关方法
class PlaywrightBrowser:
# 现有方法保持不变...
async def get_qrcode_screenshot(self) -> bytes:
"""获取登录二维码截图"""
canvas = await self.page.wait_for_selector("canvas.SignIn-qrcode")
return await canvas.screenshot()
async def check_login_cookie(self) -> bool:
"""检测是否已登录(检查 z_c0 Cookie)"""
cookies = await self.context.cookies()
return any(c["name"] == "z_c0" for c in cookies)
async def get_all_cookies_str(self) -> str:
"""获取所有 Cookie 为字符串格式"""
cookies = await self.context.cookies()
return '; '.join(f"{c['name']}={c['value']}" for c in cookies)
```
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| 逆向 x-zse-96 签名 | 二维码扫码登录 | 2024+ | 免维护,无需逆向 |
| 终端 ASCII 二维码 | 图片文件保存 | 本项目决定 | AI 代理可读取 |
| 手动 Cookie 管理 | 自动持久化 | 本项目决定 | 用户体验提升 |
**Deprecated/outdated:**
- 纯 API 请求(无浏览器):需要维护签名算法,知乎经常更新
- 第三方打码服务:付费且不稳定,二维码登录免费可靠
## Open Questions
1. **二维码图片文件名格式**
- What we know: 存储在中间目录,包含时间戳
- What's unclear: 是否需要包含随机数避免冲突
- Recommendation: 使用 `zhihu_qrcode_{timestamp}.png` 格式,同一秒内多次登录概率低
2. **验证接口的精确返回格式**
- What we know: `/api/v4/me` 返回用户信息
- What's unclear: 具体字段名称(name vs username)
- Recommendation: 实现时先打印完整响应,根据实际返回调整字段名
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | pytest |
| Config file | pytest.ini |
| Quick run command | `pytest tests/test_auth/ -x -v` |
| Full suite command | `pytest tests/ -v` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| AUTH-01 | 二维码扫码登录 | unit + integration | `pytest tests/test_auth/test_qrcode_login.py -x` | Wave 0 |
| AUTH-02 | 手动设置 Cookie | unit | `pytest tests/test_auth/test_cookie_import.py -x` | Wave 0 |
| AUTH-03 | Cookie 持久化 | unit | `pytest tests/test_auth/test_cookie_persistence.py -x` | Wave 0 |
| AUTH-04 | Cookie 过期检测 | unit | `pytest tests/test_auth/test_cookie_validation.py -x` | Wave 0 |
### Sampling Rate
- **Per task commit:** `pytest tests/test_auth/ -x`
- **Per wave merge:** `pytest tests/ -v`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `tests/test_auth/__init__.py` — 测试模块初始化
- [ ] `tests/test_auth/test_qrcode_login.py` — AUTH-01 二维码登录测试
- [ ] `tests/test_auth/test_cookie_import.py` — AUTH-02 Cookie 导入测试
- [ ] `tests/test_auth/test_cookie_persistence.py` — AUTH-03 Cookie 持久化测试
- [ ] `tests/test_auth/test_cookie_validation.py` — AUTH-04 Cookie 验证测试
- [ ] `tests/test_auth/conftest.py` — 共享 fixtures (mock browser, mock env file)
**测试策略:**
- 使用 `unittest.mock` 模拟 Playwright browser 和 context
- 使用 `tmp_path` fixture 创建临时 `~/.kb_env` 文件
- Cookie 验证测试使用 mock HTTP 响应
## Sources
### Primary (HIGH confidence)
- **MediaCrawler 源码** - `media_platform/zhihu/login.py`, `tools/crawler_util.py`
- 二维码登录完整实现
- Cookie 格式转换函数
- **现有项目代码** - `kb_workflow/metadata/zhihu_extractor.py`
- PlaywrightBrowser 类实现
- Cookie 设置方法 `_add_cookies()`
- **现有项目代码** - `kb_workflow/pipeline/config.py`
- KBSettings 配置类
- zhihu_cookie 字段定义
### Secondary (MEDIUM confidence)
- 知乎 API 实测 - `/api/v4/me` 端点行为
- Chrome Cookie Editor JSON 格式规范
### Tertiary (LOW confidence)
- 网络博客关于知乎登录流程的描述(需实现时验证)
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH - 现有项目已使用 Playwright 和 pydantic-settings
- Architecture: HIGH - MediaCrawler 提供了成熟的参考实现
- Pitfalls: MEDIUM - 部分边界情况需要在实现时验证
**Research date:** 2026-03-17
**Valid until:** 2026-04-17 (知乎 API 可能变化,选择器可能更新)