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/07-login-auth/07-RESEARCH.md
T

17 KiB
Raw Blame History

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 秒
  • 超时行为: 轮询检测,超时退出(不自动重试)
  • 存储位置: ~/.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:

pip install tenacity

Architecture Patterns

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:

# 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("扫码超时,请重试")

What: 通过知乎 API 检测 Cookie 是否有效

When to use: 每次使用 Cookie 前、login --status 命令

Example:

# 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}"}

What: 解析 Chrome Cookie Editor 导出的 JSON 格式

When to use: --import-cookie 参数

Example:

# 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)

What: 将 Cookie 写入 ~/.kb_env 文件

When to use: 登录成功后、导入 Cookie 后

Example:

# 复用现有 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 超时

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 元素")

What goes wrong: 用户扫码后,z_c0 Cookie 不是立即出现 Why it happens: 服务端验证需要时间 How to avoid: 使用 tenacity 的 1 秒间隔轮询,60 次重试(60 秒总超时)

Warning signs: 扫码成功但检测失败

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. 提示用户检查权限
env_path = Path.home() / ".kb_env"
env_path.touch(mode=0o600, exist_ok=True)

Code Examples

CLI 子命令扩展

# 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

# 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 可能变化,选择器可能更新)