diff --git a/.planning/phases/07-login-auth/07-RESEARCH.md b/.planning/phases/07-login-auth/07-RESEARCH.md new file mode 100644 index 0000000..9c1b9a7 --- /dev/null +++ b/.planning/phases/07-login-auth/07-RESEARCH.md @@ -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 (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 + + + +## 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` 接口验证 + 状态码检测 | + + +## 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 可能变化,选择器可能更新)