diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index b2704eb..df66e44 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -113,11 +113,11 @@ Plans:
2. 爬虫的 navigator.webdriver 属性对知乎不可见
3. Playwright 特有的 JS 属性(__playwright, pwInspect 等)被移除
4. 现有功能(登录、Cookie 持久化、内容爬取)保持正常工作
-**Plans**: TBD
+**Plans**: 2 plans
Plans:
-- [ ] 11-01: 指纹伪装脚本实现 — 隐藏 webdriver、移除 Playwright 特有属性、模拟 Chrome 特性
-- [ ] 11-02: ZhihuCrawler 集成与验证 — 注入伪装脚本,验证爬取功能正常
+- [ ] 11-01-PLAN.md — 创建 StealthConfig 模块和单元测试
+- [ ] 11-02-PLAN.md — 集成 stealth 到 ZhihuCrawler 并验证
---
@@ -138,7 +138,7 @@ Phases execute in numeric order: 1 → 2 → ... → 10 → 11
| 8. 内容爬取实现 | v1.2 | 3/3 | Complete | 2026-03-17 |
| 9. Pipeline 整合 | v1.2 | 1/1 | Complete | 2026-03-17 |
| 10. Proxy Pool | v1.3 | 2/2 | Complete | 2026-03-18 |
-| 11. Stealth Fingerprinting | v1.4 | 0/2 | Not started | - |
+| 11. Stealth Fingerprinting | v1.4 | 0/2 | Ready to execute | - |
---
diff --git a/.planning/phases/11-stealth-fingerprinting/11-01-PLAN.md b/.planning/phases/11-stealth-fingerprinting/11-01-PLAN.md
new file mode 100644
index 0000000..fe7b1a0
--- /dev/null
+++ b/.planning/phases/11-stealth-fingerprinting/11-01-PLAN.md
@@ -0,0 +1,536 @@
+---
+phase: 11-stealth-fingerprinting
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - kb_workflow/crawler/stealth/__init__.py
+ - kb_workflow/crawler/stealth/stealth_config.py
+ - tests/test_crawler/conftest.py
+ - tests/test_crawler/test_stealth.py
+autonomous: true
+requirements:
+ - STEALTH-01
+ - STEALTH-02
+ - STEALTH-03
+ - STEALTH-04
+must_haves:
+ truths:
+ - "navigator.webdriver 属性被隐藏(返回 undefined/false)"
+ - "Playwright 特有属性(__playwright, __pw_manual, __PW_inspect)被移除"
+ - "window.chrome 对象被模拟为正常 Chrome 浏览器结构"
+ - "User-Agent 不包含 HeadlessChrome 字符串"
+ artifacts:
+ - path: "kb_workflow/crawler/stealth/__init__.py"
+ provides: "Stealth 模块入口,导出 StealthConfig 和 apply_stealth"
+ min_lines: 10
+ - path: "kb_workflow/crawler/stealth/stealth_config.py"
+ provides: "StealthConfig Pydantic 模型和 apply_stealth_async 函数"
+ min_lines: 50
+ - path: "tests/test_crawler/test_stealth.py"
+ provides: "Stealth 功能单元测试"
+ min_lines: 100
+ key_links:
+ - from: "kb_workflow/crawler/stealth/stealth_config.py"
+ to: "playwright_stealth.Stealth"
+ via: "from playwright_stealth import Stealth"
+ pattern: "from playwright_stealth"
+---
+
+
+创建浏览器指纹伪装模块,使用 playwright-stealth 库隐藏 Playwright 自动化特征。
+
+Purpose: 为 ZhihuCrawler 提供可复用的指纹伪装能力,绕过知乎的浏览器指纹检测
+Output: kb_workflow/crawler/stealth/ 模块 + 单元测试
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/11-stealth-fingerprinting/11-RESEARCH.md
+@.planning/phases/11-stealth-fingerprinting/11-VALIDATION.md
+
+
+
+
+From kb_workflow/crawler/config.py:
+```python
+class CrawlerConfig(BaseModel):
+ proxy: Optional[ProxyConfig] = Field(None, description="Proxy configuration")
+ proxy_required: bool = Field(default=False)
+ min_delay: float = Field(default=1.0)
+ max_delay: float = Field(default=3.0)
+```
+
+From kb_workflow/crawler/zhihu_crawler.py:
+```python
+class ZhihuCrawler:
+ def __init__(
+ self,
+ headless: bool = True,
+ timeout: int = 30,
+ config: Optional[CrawlerConfig] = None,
+ proxy_pool: Optional[ProxyPool] = None
+ ):
+ ...
+
+ def _start_browser_with_proxy(self, cookie: str) -> None:
+ """Start browser with proxy configuration."""
+ self._playwright = sync_playwright().start()
+ self._browser = self._playwright.chromium.launch(headless=self.headless)
+ context_options = {
+ 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ ...
+ self._context = self._browser.new_context(**context_options)
+```
+
+From playwright_stealth (external library):
+```python
+from playwright_stealth import Stealth
+
+stealth = Stealth(
+ navigator_webdriver=True,
+ navigator_user_agent=True,
+ navigator_plugins=True,
+ # ... other options
+)
+stealth.apply_stealth_async(context) # for async
+stealth.apply_stealth_sync(context) # for sync
+```
+
+
+
+
+
+
+ Task 1: 创建 StealthConfig 模块
+ kb_workflow/crawler/stealth/__init__.py, kb_workflow/crawler/stealth/stealth_config.py
+
+ - Test 1: StealthConfig 默认值正确(stealth_enabled=True, navigator_webdriver=True 等)
+ - Test 2: apply_stealth_sync 在 mock context 上正确调用 playwright_stealth
+ - Test 3: get_default_stealth_config() 返回正确的默认配置
+
+
+ 1. 创建 `kb_workflow/crawler/stealth/` 目录
+
+ 2. 创建 `kb_workflow/crawler/stealth/__init__.py`:
+ - 导出 StealthConfig 类
+ - 导出 apply_stealth_sync, apply_stealth_async 函数
+ - 导出 get_default_stealth_config 便捷函数
+
+ 3. 创建 `kb_workflow/crawler/stealth/stealth_config.py`:
+
+ ```python
+ """Stealth configuration for browser fingerprint masking.
+
+ Uses playwright-stealth library to hide Playwright automation signatures.
+ """
+
+ from typing import Optional
+ from playwright.sync_api import BrowserContext
+ from playwright.async_api import BrowserContext as AsyncBrowserContext
+ from pydantic import BaseModel, Field
+ from playwright_stealth import Stealth
+
+
+ class StealthConfig(BaseModel):
+ """Configuration for browser stealth settings.
+
+ All options map to playwright-stealth evasion scripts.
+ Default values optimized for Zhihu anti-detection.
+ """
+
+ stealth_enabled: bool = Field(
+ default=True,
+ description="Enable stealth mode (set False to disable)"
+ )
+
+ # Navigator evasion options
+ navigator_webdriver: bool = Field(
+ default=True,
+ description="Hide navigator.webdriver property (STEALTH-01)"
+ )
+ navigator_user_agent: bool = Field(
+ default=True,
+ description="Replace HeadlessChrome in User-Agent (STEALTH-04)"
+ )
+ navigator_plugins: bool = Field(
+ default=True,
+ description="Mock navigator.plugins array"
+ )
+ navigator_languages: bool = Field(
+ default=True,
+ description="Set navigator.languages"
+ )
+ navigator_platform: bool = Field(
+ default=True,
+ description="Set navigator.platform"
+ )
+ navigator_vendor: bool = Field(
+ default=True,
+ description="Set navigator.vendor"
+ )
+ navigator_permissions: bool = Field(
+ default=True,
+ description="Override navigator.permissions"
+ )
+
+ # Chrome object evasion (STEALTH-03)
+ chrome_app: bool = Field(
+ default=True,
+ description="Mock chrome.app object"
+ )
+ chrome_csi: bool = Field(
+ default=True,
+ description="Mock chrome.csi function"
+ )
+ chrome_load_times: bool = Field(
+ default=True,
+ description="Mock chrome.loadTimes function"
+ )
+
+ # WebGL and rendering
+ webgl_vendor: bool = Field(
+ default=True,
+ description="Override WebGL vendor/renderer strings"
+ )
+
+ # Other evasion options
+ hairline: bool = Field(
+ default=True,
+ description="Fix hairline detection"
+ )
+ iframe_content_window: bool = Field(
+ default=True,
+ description="Fix iframe contentWindow"
+ )
+ media_codecs: bool = Field(
+ default=True,
+ description="Mock media codecs"
+ )
+ error_prototype: bool = Field(
+ default=True,
+ description="Fix error prototype chain"
+ )
+ sec_ch_ua: bool = Field(
+ default=True,
+ description="Set sec-ch-ua headers"
+ )
+
+ def create_stealth(self) -> Stealth:
+ """Create playwright-stealth Stealth instance from config.
+
+ Returns:
+ Configured Stealth instance
+ """
+ return Stealth(
+ navigator_webdriver=self.navigator_webdriver,
+ navigator_user_agent=self.navigator_user_agent,
+ navigator_plugins=self.navigator_plugins,
+ navigator_languages=self.navigator_languages,
+ navigator_platform=self.navigator_platform,
+ navigator_vendor=self.navigator_vendor,
+ navigator_permissions=self.navigator_permissions,
+ chrome_app=self.chrome_app,
+ chrome_csi=self.chrome_csi,
+ chrome_load_times=self.chrome_load_times,
+ webgl_vendor=self.webgl_vendor,
+ hairline=self.hairline,
+ iframe_content_window=self.iframe_content_window,
+ media_codecs=self.media_codecs,
+ error_prototype=self.error_prototype,
+ sec_ch_ua=self.sec_ch_ua,
+ )
+
+
+ def apply_stealth_sync(
+ context: BrowserContext,
+ config: Optional[StealthConfig] = None
+ ) -> None:
+ """Apply stealth settings to browser context (synchronous).
+
+ Args:
+ context: Playwright BrowserContext to apply stealth to
+ config: StealthConfig instance (uses defaults if not provided)
+ """
+ if config is None:
+ config = StealthConfig()
+
+ if not config.stealth_enabled:
+ return
+
+ stealth = config.create_stealth()
+ stealth.apply_stealth_sync(context)
+
+
+ async def apply_stealth_async(
+ context: AsyncBrowserContext,
+ config: Optional[StealthConfig] = None
+ ) -> None:
+ """Apply stealth settings to browser context (asynchronous).
+
+ Args:
+ context: Playwright async BrowserContext to apply stealth to
+ config: StealthConfig instance (uses defaults if not provided)
+ """
+ if config is None:
+ config = StealthConfig()
+
+ if not config.stealth_enabled:
+ return
+
+ stealth = config.create_stealth()
+ await stealth.apply_stealth_async(context)
+
+
+ def get_default_stealth_config() -> StealthConfig:
+ """Get default StealthConfig for Zhihu crawling.
+
+ Returns:
+ StealthConfig with all evasion options enabled
+ """
+ return StealthConfig()
+ ```
+
+ 4. 创建 `kb_workflow/crawler/stealth/__init__.py`:
+ ```python
+ """Stealth module for browser fingerprint masking."""
+
+ from .stealth_config import (
+ StealthConfig,
+ apply_stealth_sync,
+ apply_stealth_async,
+ get_default_stealth_config,
+ )
+
+ __all__ = [
+ "StealthConfig",
+ "apply_stealth_sync",
+ "apply_stealth_async",
+ "get_default_stealth_config",
+ ]
+ ```
+
+
+ pytest tests/test_crawler/test_stealth.py::test_stealth_config_defaults -x
+
+
+ - StealthConfig 类创建完成,包含所有 playwright-stealth 选项
+ - apply_stealth_sync/apply_stealth_async 函数可用
+ - 单元测试通过
+
+
+
+
+ Task 2: 创建 Stealth 功能单元测试
+ tests/test_crawler/test_stealth.py, tests/test_crawler/conftest.py
+
+ - Test 1: test_stealth_config_defaults - 默认配置正确
+ - Test 2: test_stealth_config_disabled - stealth_enabled=False 时不应用伪装
+ - Test 3: test_apply_stealth_sync_calls_library - apply_stealth_sync 正确调用 playwright_stealth
+ - Test 4: test_get_default_stealth_config - 返回默认配置实例
+ - Test 5: test_stealth_options_mapping - 配置选项正确映射到 Stealth 类
+
+
+ 1. 在 `tests/test_crawler/conftest.py` 中添加 stealth_config fixture:
+
+ ```python
+ @pytest.fixture
+ def stealth_config():
+ """Default StealthConfig for testing."""
+ from kb_workflow.crawler.stealth import StealthConfig
+ return StealthConfig()
+
+
+ @pytest.fixture
+ def mock_browser_context():
+ """Mock Playwright BrowserContext for stealth testing."""
+ from unittest.mock import MagicMock
+ context = MagicMock()
+ context.add_init_script = MagicMock()
+ return context
+ ```
+
+ 2. 创建 `tests/test_crawler/test_stealth.py`:
+
+ ```python
+ """Tests for stealth fingerprint masking module."""
+
+ import pytest
+ from unittest.mock import MagicMock, patch, call
+
+
+ class TestStealthConfig:
+ """Tests for StealthConfig class."""
+
+ def test_stealth_config_defaults(self):
+ """Test that default StealthConfig has all options enabled."""
+ from kb_workflow.crawler.stealth import StealthConfig
+
+ config = StealthConfig()
+
+ assert config.stealth_enabled is True
+ # STEALTH-01: navigator.webdriver hiding
+ assert config.navigator_webdriver is True
+ # STEALTH-04: User-Agent modification
+ assert config.navigator_user_agent is True
+ # STEALTH-03: Chrome properties
+ assert config.chrome_app is True
+ assert config.chrome_csi is True
+ assert config.chrome_load_times is True
+ # Other important options
+ assert config.navigator_plugins is True
+ assert config.webgl_vendor is True
+
+ def test_stealth_config_disabled(self):
+ """Test that stealth can be disabled."""
+ from kb_workflow.crawler.stealth import StealthConfig
+
+ config = StealthConfig(stealth_enabled=False)
+ assert config.stealth_enabled is False
+
+ def test_create_stealth_returns_stealth_instance(self):
+ """Test that create_stealth() returns configured Stealth."""
+ from kb_workflow.crawler.stealth import StealthConfig
+ from playwright_stealth import Stealth
+
+ config = StealthConfig()
+ stealth = config.create_stealth()
+
+ assert isinstance(stealth, Stealth)
+
+ def test_stealth_options_mapping(self):
+ """Test that StealthConfig options map correctly to Stealth."""
+ from kb_workflow.crawler.stealth import StealthConfig
+
+ config = StealthConfig(
+ navigator_webdriver=True,
+ navigator_user_agent=False, # Disable one option
+ chrome_app=True,
+ )
+
+ stealth = config.create_stealth()
+
+ # Verify the Stealth object was created with correct options
+ # The Stealth class stores these as attributes
+ assert stealth.navigator_webdriver is True
+ assert stealth.navigator_user_agent is False
+
+
+ class TestApplyStealth:
+ """Tests for apply_stealth functions."""
+
+ def test_apply_stealth_sync_calls_library(self, mock_browser_context):
+ """Test that apply_stealth_sync calls playwright-stealth correctly."""
+ from kb_workflow.crawler.stealth import apply_stealth_sync, StealthConfig
+
+ config = StealthConfig()
+
+ with patch('kb_workflow.crawler.stealth.stealth_config.Stealth') as mock_stealth_class:
+ mock_stealth_instance = MagicMock()
+ mock_stealth_class.return_value = mock_stealth_instance
+
+ apply_stealth_sync(mock_browser_context, config)
+
+ # Verify Stealth was created with config options
+ mock_stealth_class.assert_called_once()
+ # Verify apply_stealth_sync was called on the Stealth instance
+ mock_stealth_instance.apply_stealth_sync.assert_called_once_with(
+ mock_browser_context
+ )
+
+ def test_apply_stealth_sync_disabled_skips_stealth(self, mock_browser_context):
+ """Test that stealth is skipped when disabled."""
+ from kb_workflow.crawler.stealth import apply_stealth_sync, StealthConfig
+
+ config = StealthConfig(stealth_enabled=False)
+
+ with patch('kb_workflow.crawler.stealth.stealth_config.Stealth') as mock_stealth_class:
+ apply_stealth_sync(mock_browser_context, config)
+
+ # Stealth should not be created when disabled
+ mock_stealth_class.assert_not_called()
+
+ def test_apply_stealth_sync_default_config(self, mock_browser_context):
+ """Test that apply_stealth_sync works with default config."""
+ from kb_workflow.crawler.stealth import apply_stealth_sync
+
+ with patch('kb_workflow.crawler.stealth.stealth_config.Stealth') as mock_stealth_class:
+ mock_stealth_instance = MagicMock()
+ mock_stealth_class.return_value = mock_stealth_instance
+
+ # Should work without explicit config
+ apply_stealth_sync(mock_browser_context)
+
+ mock_stealth_class.assert_called_once()
+
+
+ class TestGetDefaultStealthConfig:
+ """Tests for get_default_stealth_config function."""
+
+ def test_get_default_stealth_config(self):
+ """Test that get_default_stealth_config returns StealthConfig."""
+ from kb_workflow.crawler.stealth import get_default_stealth_config
+
+ config = get_default_stealth_config()
+
+ assert config.stealth_enabled is True
+ assert isinstance(config, type(config)) # Is a StealthConfig
+
+ def test_get_default_stealth_config_is_fresh_instance(self):
+ """Test that each call returns a new instance."""
+ from kb_workflow.crawler.stealth import get_default_stealth_config
+
+ config1 = get_default_stealth_config()
+ config2 = get_default_stealth_config()
+
+ # Should be equal but not the same object
+ assert config1 == config2
+ assert config1 is not config2
+
+
+ # Wave 0 completion marker
+ WAVE_0_COMPLETE = True
+ ```
+
+
+ pytest tests/test_crawler/test_stealth.py -x -v
+
+
+ - test_stealth.py 创建完成,包含 5 个测试用例
+ - conftest.py 添加了 stealth_config 和 mock_browser_context fixtures
+ - 所有测试通过
+
+
+
+
+
+
+**Per-task verification:**
+- Task 1: `pytest tests/test_crawler/test_stealth.py::TestStealthConfig::test_stealth_config_defaults -x`
+- Task 2: `pytest tests/test_crawler/test_stealth.py -x -v`
+
+**Wave verification:**
+- All 5 tests in test_stealth.py pass
+- No regression in existing tests: `pytest tests/test_crawler/ -x`
+
+
+
+- [ ] kb_workflow/crawler/stealth/ 模块创建完成
+- [ ] StealthConfig 类包含所有 playwright-stealth 选项
+- [ ] apply_stealth_sync/apply_stealth_async 函数可用
+- [ ] 5 个单元测试全部通过
+- [ ] 无现有测试回归
+
+
+
diff --git a/.planning/phases/11-stealth-fingerprinting/11-02-PLAN.md b/.planning/phases/11-stealth-fingerprinting/11-02-PLAN.md
new file mode 100644
index 0000000..560d33c
--- /dev/null
+++ b/.planning/phases/11-stealth-fingerprinting/11-02-PLAN.md
@@ -0,0 +1,506 @@
+---
+phase: 11-stealth-fingerprinting
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 11-01
+files_modified:
+ - kb_workflow/crawler/zhihu_crawler.py
+ - tests/test_crawler/test_zhihu_crawler_stealth.py
+autonomous: true
+requirements:
+ - VERIFY-01
+ - VERIFY-02
+must_haves:
+ truths:
+ - "ZhihuCrawler 访问问答页面时不再被 40362 错误拦截"
+ - "现有功能(登录、Cookie 持久化、内容爬取)保持正常工作"
+ - "stealth_enabled 参数控制是否启用指纹伪装"
+ artifacts:
+ - path: "kb_workflow/crawler/zhihu_crawler.py"
+ provides: "ZhihuCrawler 集成 stealth 功能"
+ contains: "stealth_enabled"
+ - path: "tests/test_crawler/test_zhihu_crawler_stealth.py"
+ provides: "集成测试验证 stealth 功能"
+ min_lines: 50
+ key_links:
+ - from: "kb_workflow/crawler/zhihu_crawler.py"
+ to: "kb_workflow/crawler/stealth"
+ via: "from .stealth import apply_stealth_sync, StealthConfig"
+ pattern: "from \\.stealth import"
+ - from: "ZhihuCrawler._start_browser_with_proxy"
+ to: "apply_stealth_sync"
+ via: "调用 apply_stealth_sync(self._context, self._stealth_config)"
+ pattern: "apply_stealth_sync"
+---
+
+
+将指纹伪装模块集成到 ZhihuCrawler,并验证爬虫能正常绕过知乎反爬检测。
+
+Purpose: 让 ZhihuCrawler 在启动浏览器时自动应用指纹伪装,绕过 40362 错误
+Output: 修改后的 ZhihuCrawler + 集成测试
+
+
+
+@/home/starmen1265/.claude/get-shit-done/workflows/execute-plan.md
+@/home/starmen1265/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/11-stealth-fingerprinting/11-RESEARCH.md
+@.planning/phases/11-stealth-fingerprinting/11-VALIDATION.md
+
+
+@.planning/phases/11-stealth-fingerprinting/11-01-PLAN.md
+
+
+
+
+From kb_workflow/crawler/stealth (Plan 11-01):
+```python
+class StealthConfig(BaseModel):
+ stealth_enabled: bool = True
+ navigator_webdriver: bool = True
+ navigator_user_agent: bool = True
+ # ... other options
+
+def apply_stealth_sync(
+ context: BrowserContext,
+ config: Optional[StealthConfig] = None
+) -> None:
+ """Apply stealth settings to browser context (synchronous)."""
+
+def get_default_stealth_config() -> StealthConfig:
+ """Get default StealthConfig for Zhihu crawling."""
+```
+
+From kb_workflow/crawler/zhihu_crawler.py (current implementation):
+```python
+class ZhihuCrawler:
+ def __init__(
+ self,
+ headless: bool = True,
+ timeout: int = 30,
+ config: Optional[CrawlerConfig] = None,
+ proxy_pool: Optional[ProxyPool] = None
+ ):
+ self.headless = headless
+ self.timeout = timeout
+ self.config = config or get_crawler_config()
+ self.proxy_pool = proxy_pool
+ self._current_proxy: Optional[ProxyEntry] = None
+ # Internal browser management
+ self._playwright = None
+ self._browser = None
+ self._context = None
+ self._page = None
+
+ def _start_browser_with_proxy(self, cookie: str) -> None:
+ """Start browser with proxy configuration."""
+ self.config.validate_proxy()
+ self._playwright = sync_playwright().start()
+ self._browser = self._playwright.chromium.launch(headless=self.headless)
+ context_options = {
+ 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ # ... proxy configuration ...
+ context_options["service_workers"] = "block"
+ self._context = self._browser.new_context(**context_options)
+ if cookie:
+ self._add_cookies(cookie)
+ self._page = self._context.new_page()
+ self._page.set_default_timeout(self.timeout * 1000)
+```
+
+From tests/test_crawler/conftest.py:
+```python
+@pytest.fixture
+def mock_page():
+ """Mock Playwright page object."""
+ page = MagicMock()
+ page.route = MagicMock()
+ page.goto = MagicMock()
+ return page
+
+@pytest.fixture
+def sample_answer_api_response():
+ """Sample answer API response data."""
+ return {
+ "id": 123456789,
+ "type": "answer",
+ # ...
+ }
+```
+
+
+
+
+
+
+ Task 1: 集成 stealth 到 ZhihuCrawler
+ kb_workflow/crawler/zhihu_crawler.py
+
+ - Test 1: ZhihuCrawler 初始化时 stealth_enabled 默认为 True
+ - Test 2: ZhihuCrawler 初始化时可禁用 stealth(stealth_enabled=False)
+ - Test 3: _start_browser_with_proxy 在创建 context 后调用 apply_stealth_sync
+ - Test 4: stealth_enabled=False 时不调用 apply_stealth_sync
+ - Test 5: launch 时添加 --disable-blink-features=AutomationControlled 参数
+
+
+ 修改 `kb_workflow/crawler/zhihu_crawler.py`,集成 stealth 功能:
+
+ 1. 添加导入(在文件顶部导入区域):
+ ```python
+ from .stealth import apply_stealth_sync, StealthConfig
+ ```
+
+ 2. 修改 `__init__` 方法,添加 stealth 参数:
+ ```python
+ def __init__(
+ self,
+ headless: bool = True,
+ timeout: int = 30,
+ config: Optional[CrawlerConfig] = None,
+ proxy_pool: Optional[ProxyPool] = None,
+ stealth_config: Optional[StealthConfig] = None # 新增参数
+ ):
+ """Initialize ZhihuCrawler.
+
+ Args:
+ headless: Whether to run browser in headless mode
+ timeout: Request timeout in seconds
+ config: Optional CrawlerConfig instance
+ proxy_pool: Optional ProxyPool instance for multi-proxy rotation
+ stealth_config: Optional StealthConfig instance (uses defaults if not provided)
+ """
+ self.headless = headless
+ self.timeout = timeout
+ self.config = config or get_crawler_config()
+ self.proxy_pool = proxy_pool
+ self._stealth_config = stealth_config # 存储配置
+ self._current_proxy: Optional[ProxyEntry] = None
+ # ... rest of initialization
+ ```
+
+ 3. 修改 `_start_browser_with_proxy` 方法:
+ ```python
+ def _start_browser_with_proxy(self, cookie: str) -> None:
+ """Start browser with proxy configuration.
+
+ Validates proxy if required and applies to browser context.
+ Applies stealth fingerprint masking after context creation.
+
+ Args:
+ cookie: Cookie string for authentication
+
+ Raises:
+ ValueError: If proxy_required=True but no proxy is configured
+ """
+ # Validate proxy configuration
+ self.config.validate_proxy()
+
+ # Build launch args for stealth (关键:添加 --disable-blink-features)
+ launch_args = []
+ if self._stealth_config is None or self._stealth_config.stealth_enabled:
+ launch_args.append('--disable-blink-features=AutomationControlled')
+
+ self._playwright = sync_playwright().start()
+ self._browser = self._playwright.chromium.launch(
+ headless=self.headless,
+ args=launch_args if launch_args else None
+ )
+
+ # Build context options
+ context_options = {
+ 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+
+ # Prioritize proxy pool over single proxy config
+ if self.proxy_pool and self.proxy_pool.has_proxies():
+ proxy_entry = self.proxy_pool.get_next_proxy()
+ if proxy_entry:
+ proxy_config = self._parse_proxy_url(proxy_entry.url)
+ context_options["proxy"] = proxy_config
+ self._current_proxy = proxy_entry
+ elif self.config.proxy:
+ context_options["proxy"] = self.config.proxy.to_playwright_format()
+
+ # Block Service Worker to prevent interference with API interception
+ context_options["service_workers"] = "block"
+
+ self._context = self._browser.new_context(**context_options)
+
+ # 关键改动:应用 stealth 指纹伪装
+ apply_stealth_sync(self._context, self._stealth_config)
+
+ # Add cookies if provided
+ if cookie:
+ self._add_cookies(cookie)
+
+ self._page = self._context.new_page()
+ self._page.set_default_timeout(self.timeout * 1000)
+ ```
+
+ 关键点:
+ - 在 `launch()` 时添加 `--disable-blink-features=AutomationControlled` 参数
+ - 在 `new_context()` 后、添加 cookies 前调用 `apply_stealth_sync()`
+ - 保持向后兼容:stealth_config 为 None 时使用默认配置(启用 stealth)
+
+
+ pytest tests/test_crawler/test_zhihu_crawler_stealth.py -x -v
+
+
+ - ZhihuCrawler 添加 stealth_config 参数
+ - _start_browser_with_proxy 调用 apply_stealth_sync
+ - launch 时添加 --disable-blink-features 参数
+ - 所有集成测试通过
+
+
+
+
+ Task 2: 创建 ZhihuCrawler Stealth 集成测试
+ tests/test_crawler/test_zhihu_crawler_stealth.py
+
+ - Test 1: test_crawler_has_stealth_config_param - ZhihuCrawler 接受 stealth_config 参数
+ - Test 2: test_crawler_default_stealth_enabled - 默认启用 stealth
+ - Test 3: test_crawler_can_disable_stealth - 可以禁用 stealth
+ - Test 4: test_start_browser_applies_stealth - _start_browser_with_proxy 调用 apply_stealth_sync
+ - Test 5: test_launch_args_include_disable_blink_features - launch 参数包含 --disable-blink-features
+
+
+ 创建 `tests/test_crawler/test_zhihu_crawler_stealth.py`:
+
+ ```python
+ """Integration tests for ZhihuCrawler stealth functionality.
+
+ Tests that stealth fingerprint masking is properly integrated
+ into ZhihuCrawler's browser initialization.
+ """
+
+ import pytest
+ from unittest.mock import MagicMock, patch, call
+
+
+ class TestZhihuCrawlerStealthIntegration:
+ """Tests for stealth integration in ZhihuCrawler."""
+
+ def test_crawler_has_stealth_config_param(self):
+ """Test that ZhihuCrawler accepts stealth_config parameter."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+ from kb_workflow.crawler.stealth import StealthConfig
+
+ config = StealthConfig(stealth_enabled=False)
+ crawler = ZhihuCrawler(stealth_config=config)
+
+ assert crawler._stealth_config == config
+
+ def test_crawler_default_stealth_enabled(self):
+ """Test that stealth is enabled by default (None = use defaults)."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+
+ crawler = ZhihuCrawler()
+
+ # None means use default stealth config (enabled)
+ assert crawler._stealth_config is None
+
+ def test_crawler_can_disable_stealth(self):
+ """Test that stealth can be explicitly disabled."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+ from kb_workflow.crawler.stealth import StealthConfig
+
+ disabled_config = StealthConfig(stealth_enabled=False)
+ crawler = ZhihuCrawler(stealth_config=disabled_config)
+
+ assert crawler._stealth_config.stealth_enabled is False
+
+ @patch('kb_workflow.crawler.zhihu_crawler.sync_playwright')
+ @patch('kb_workflow.crawler.zhihu_crawler.apply_stealth_sync')
+ def test_start_browser_applies_stealth(
+ self,
+ mock_apply_stealth,
+ mock_sync_playwright,
+ ):
+ """Test that _start_browser_with_proxy calls apply_stealth_sync."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+
+ # Setup mocks
+ mock_playwright = MagicMock()
+ mock_browser = MagicMock()
+ mock_context = MagicMock()
+ mock_sync_playwright.return_value.start.return_value = mock_playwright
+ mock_playwright.chromium.launch.return_value = mock_browser
+ mock_browser.new_context.return_value = mock_context
+
+ crawler = ZhihuCrawler()
+ crawler._start_browser_with_proxy(cookie="test_cookie")
+
+ # Verify apply_stealth_sync was called
+ mock_apply_stealth.assert_called_once()
+ call_args = mock_apply_stealth.call_args
+ assert call_args[0][0] == mock_context # First arg is context
+
+ @patch('kb_workflow.crawler.zhihu_crawler.sync_playwright')
+ @patch('kb_workflow.crawler.zhihu_crawler.apply_stealth_sync')
+ def test_start_browser_launch_args_include_disable_blink_features(
+ self,
+ mock_apply_stealth,
+ mock_sync_playwright,
+ ):
+ """Test that launch includes --disable-blink-features=AutomationControlled."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+
+ # Setup mocks
+ mock_playwright = MagicMock()
+ mock_browser = MagicMock()
+ mock_context = MagicMock()
+ mock_sync_playwright.return_value.start.return_value = mock_playwright
+ mock_playwright.chromium.launch.return_value = mock_browser
+ mock_browser.new_context.return_value = mock_context
+
+ crawler = ZhihuCrawler()
+ crawler._start_browser_with_proxy(cookie="test_cookie")
+
+ # Verify launch was called with correct args
+ launch_call = mock_playwright.chromium.launch
+ launch_call.assert_called_once()
+ call_kwargs = launch_call.call_args[1]
+
+ assert 'args' in call_kwargs
+ assert '--disable-blink-features=AutomationControlled' in call_kwargs['args']
+
+ @patch('kb_workflow.crawler.zhihu_crawler.sync_playwright')
+ @patch('kb_workflow.crawler.zhihu_crawler.apply_stealth_sync')
+ def test_start_browser_stealth_disabled_no_blink_features_arg(
+ self,
+ mock_apply_stealth,
+ mock_sync_playwright,
+ ):
+ """Test that --disable-blink-features is not added when stealth disabled."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+ from kb_workflow.crawler.stealth import StealthConfig
+
+ # Setup mocks
+ mock_playwright = MagicMock()
+ mock_browser = MagicMock()
+ mock_context = MagicMock()
+ mock_sync_playwright.return_value.start.return_value = mock_playwright
+ mock_playwright.chromium.launch.return_value = mock_browser
+ mock_browser.new_context.return_value = mock_context
+
+ disabled_config = StealthConfig(stealth_enabled=False)
+ crawler = ZhihuCrawler(stealth_config=disabled_config)
+ crawler._start_browser_with_proxy(cookie="test_cookie")
+
+ # Verify launch was called without args or with empty/None args
+ launch_call = mock_playwright.chromium.launch
+ call_kwargs = launch_call.call_args[1]
+
+ # When stealth disabled, args should be None or not contain the flag
+ if 'args' in call_kwargs and call_kwargs['args']:
+ assert '--disable-blink-features=AutomationControlled' not in call_kwargs['args']
+
+ # apply_stealth_sync should still be called but with disabled config
+ mock_apply_stealth.assert_called_once()
+
+
+ class TestZhihuCrawlerBackwardCompatibility:
+ """Tests for backward compatibility after stealth integration."""
+
+ def test_crawler_works_without_stealth_config(self):
+ """Test that ZhihuCrawler works without explicit stealth_config."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+
+ # Should not raise any errors
+ crawler = ZhihuCrawler(
+ headless=True,
+ timeout=30
+ )
+ assert crawler is not None
+
+ def test_crawler_accepts_all_existing_params(self):
+ """Test that all existing parameters still work."""
+ from kb_workflow.crawler.zhihu_crawler import ZhihuCrawler
+ from kb_workflow.crawler.config import CrawlerConfig
+
+ config = CrawlerConfig()
+ crawler = ZhihuCrawler(
+ headless=False,
+ timeout=60,
+ config=config
+ )
+
+ assert crawler.headless is False
+ assert crawler.timeout == 60
+ assert crawler.config == config
+
+
+ # Wave 0 completion marker
+ WAVE_0_COMPLETE = True
+ ```
+
+
+ pytest tests/test_crawler/test_zhihu_crawler_stealth.py -x -v
+
+
+ - test_zhihu_crawler_stealth.py 创建完成
+ - 8 个测试用例覆盖 stealth 集成和向后兼容性
+ - 所有测试通过
+
+
+
+
+ Task 3: 回归测试验证
+ N/A
+
+ 运行完整的 crawler 测试套件,确保 stealth 集成没有破坏现有功能:
+
+ 1. 运行所有 crawler 测试:
+ ```bash
+ pytest tests/test_crawler/ -v
+ ```
+
+ 2. 检查测试输出,确保:
+ - 所有现有测试通过
+ - 没有新增的 deprecation warnings
+ - 没有导入错误
+
+ 3. 如果有测试失败,分析原因并修复
+
+
+ pytest tests/test_crawler/ -v
+
+
+ - 所有 crawler 测试通过(包括新测试和现有测试)
+ - VERIFY-02 要求满足(现有功能保持正常工作)
+
+
+
+
+
+
+**Per-task verification:**
+- Task 1: `pytest tests/test_crawler/test_zhihu_crawler_stealth.py::TestZhihuCrawlerStealthIntegration -x -v`
+- Task 2: `pytest tests/test_crawler/test_zhihu_crawler_stealth.py -x -v`
+- Task 3: `pytest tests/test_crawler/ -v`
+
+**Wave verification:**
+- All 8 new tests pass
+- All existing tests pass (regression check)
+- Total test count should increase by 8
+
+
+
+- [ ] ZhihuCrawler 集成 stealth_config 参数
+- [ ] _start_browser_with_proxy 调用 apply_stealth_sync
+- [ ] launch 包含 --disable-blink-features=AutomationControlled 参数
+- [ ] 8 个集成测试全部通过
+- [ ] 所有现有测试通过(无回归)
+
+
+