Skill: naivekid/zhihu-assistant-skill (version 0.1.1)

--- name: zhihu-assistant description: 知乎热榜抓取与回答草稿生成助手 - 自动抓取知乎热榜，使用 AI 生成优质回答草稿，推送到飞书审核 version: 1.0.0 author: naiveKid requires: - python3 - pip3 --- # 知乎助手 (Zhihu Assistant) 自动抓取知乎热榜、生成回答草稿并推送到飞书审核的 OpenClaw Skill。 ## 功能特性 - ⏰ **定时抓取**：每小时自动抓取知乎热榜前10条 - 🧠 **智能生成**：使用 Kimi AI 生成优质回答草稿 - 📋 **审核队列**：推送到飞书，人工确认后手动发布 - 📝 **记忆去重**：自动过滤已回答过的问题 - 📊 **操作日志**：完整记录所有操作 ## 安装 ```bash openclaw skills install zhihu-assistant ``` ## 配置 安装后需要配置以下参数： ### 1. 知乎 Cookie（必需） 从浏览器开发者工具复制知乎 Cookie： 1. 登录知乎网页版 2. 按 F12 打开开发者工具 → Network 3. 刷新页面，找到任意请求 4. 复制 Request Headers 中的 Cookie ```bash openclaw config set skills.zhihu-assistant.zhihu_cookie "your_zhihu_cookie_here" ``` ### 2. Kimi API Key（必需） 从 [Kimi 开放平台](https://platform.moonshot.cn/) 获取 API Key： ```bash openclaw config set skills.zhihu-assistant.kimi_api_key "your_api_key_here" ``` ### 3. 飞书用户 ID（可选） 用于接收推送通知： ```bash openclaw config set skills.zhihu-assistant.feishu_user_id "your_feishu_user_id" ``` ### 4. 其他配置（可选） ```bash # 每次抓取数量（默认10） openclaw config set skills.zhihu-assistant.fetch_limit 10 # 最小热度过滤，单位万（默认10） openclaw config set skills.zhihu-assistant.min_heat 10 ``` ## 使用 ### 快捷命令 ```bash # 抓取热榜并生成草稿 openclaw zhihu fetch --limit 10 # 查看统计信息 openclaw zhihu stats # 推送到飞书 openclaw zhihu notify # 查看操作日志 openclaw zhihu logs # 拒绝草稿 openclaw zhihu reject --id P20260301... ``` ### 详细说明 #### `openclaw zhihu fetch [--limit N]` 抓取知乎热榜并生成回答草稿。 参数： - `--limit`：抓取前 N 条热榜（默认 10，最大 50） #### `openclaw zhihu stats` 查看当前统计信息： - 已回答问题数 - 待审核数量 - 已批准数量 - 已发布数量 - 已拒绝数量 #### `openclaw zhihu notify` 获取待推送的审核项列表，用于推送到飞书。 #### `openclaw zhihu logs` 查看最近的操作日志（最近 20 条）。 #### `openclaw zhihu reject --id <ID>` 拒绝指定的待审核项。 ## 定时任务 安装后会自动创建以下定时任务： | 任务 | 时间 | 功能 | |------|------|------| | zhihu-fetch | 每小时 0 分 | 抓取热榜并生成草稿 | | zhihu-notify | 每小时 5 分 | 推送待审核项到飞书 | 查看定时任务： ```bash openclaw cron list ``` ## 飞书交互 ### 接收推送 系统每小时自动推送待审核项到飞书，格式如下： ``` 📋 知乎回答待审核 问题：为什么现在的年轻人都不爱结婚了？ 热度：1250万 链接：https://zhihu.com/question/... --- 回答草稿（800字）： ... --- 待审核ID：P20260301120000_123456 💡 操作方式： • 复制内容到知乎发布 • 回复 "查看 P20260301..." 查看完整草稿 ``` ### 查看完整草稿 在飞书中回复： ``` 查看 P20260301120000_123456 ``` ## 项目结构 ``` zhihu-assistant/ ├── main.py # 主程序入口 ├── modules/ # 功能模块 │ ├── __init__.py │ ├── zhihu_hot.py # 知乎热榜抓取 │ ├── memory_store.py # 记忆存储管理 │ ├── content_gen.py # 内容生成 │ └── feishu_notify.py # 飞书通知 ├── data/ # 数据存储 │ ├── answered.json # 已回答问题列表 │ └── pending.json # 待审核队列 └── logs/ # 日志文件 └── app.log # 应用日志 ``` ## 注意事项 1. **Cookie 有效期**：知乎 Cookie 会过期，通常 1-3 个月，过期后需要重新获取 2. **API 调用限制**：Kimi API 有速率限制（免费版 20 RPM），请勿频繁调用 3. **内容质量**：AI 生成的内容仅供参考，发布前请人工审核 4. **账号安全**：请勿将 Cookie 和 API Key 提交到代码仓库 5. **合规使用**：遵守知乎社区规范，不要发布违规内容 ## 故障排查 ### 抓取失败（401 错误） - 检查 Cookie 是否过期 - 重新从浏览器复制最新 Cookie ### API 调用失败（429 错误） - 遇到速率限制，系统会自动等待并重试 - 免费版限制 20 请求/分钟，如需更高额度请升级账户 ### 飞书推送失败 - 检查 OpenClaw 飞书配置 - 检查目标用户 ID 是否正确 ## 更新日志 ### v1.0.0 (2026-03-01) - 初始版本发布 - 支持知乎热榜抓取 - 支持 AI 生成回答草稿 - 支持飞书推送 - 支持审核队列管理 ## License MIT ## 作者 - GitHub: [@naiveKid](https://github.com/naiveKid)

path: README.md
content: # 知乎助手 - OpenClaw Skill

自动抓取知乎热榜、生成回答草稿并推送到飞书审核的 OpenClaw Skill。

## 功能特性

- ⏰ **定时抓取**：每小时自动抓取知乎热榜前10条
- 🧠 **智能生成**：使用 Kimi AI 生成优质回答草稿
- 📋 **审核队列**：推送到飞书，人工确认后手动发布
- 📝 **记忆去重**：自动过滤已回答过的问题
- 📊 **操作日志**：完整记录所有操作

## 安装

```bash
openclaw skills install zhihu-assistant
```

## 配置

安装后需要配置以下参数：

### 1. 知乎 Cookie（必需）

从浏览器开发者工具复制知乎 Cookie：

1. 登录知乎网页版
2. 按 F12 打开开发者工具 → Network
3. 刷新页面，找到任意请求
4. 复制 Request Headers 中的 Cookie

```bash
openclaw config set skills.zhihu-assistant.zhihu_cookie "your_zhihu_cookie_here"
```

### 2. Kimi API Key（必需）

从 [Kimi 开放平台](https://platform.moonshot.cn/) 获取 API Key：

```bash
openclaw config set skills.zhihu-assistant.kimi_api_key "your_api_key_here"
```

### 3. 飞书用户 ID（可选）

用于接收推送通知：

```bash
openclaw config set skills.zhihu-assistant.feishu_user_id "your_feishu_user_id"
```

### 4. 其他配置（可选）

```bash
# 每次抓取数量（默认10）
openclaw config set skills.zhihu-assistant.fetch_limit 10

# 最小热度过滤，单位万（默认10）
openclaw config set skills.zhihu-assistant.min_heat 10
```

## 使用

### 快捷命令

```bash
# 抓取热榜并生成草稿
openclaw zhihu fetch --limit 10

# 查看统计信息
openclaw zhihu stats

# 推送到飞书
openclaw zhihu notify

# 查看操作日志
openclaw zhihu logs

# 拒绝草稿
openclaw zhihu reject --id P20260301...
```

## 定时任务

安装后会自动创建以下定时任务：

| 任务 | 时间 | 功能 |
|------|------|------|
| zhihu-fetch | 每小时 0 分 | 抓取热榜并生成草稿 |
| zhihu-notify | 每小时 5 分 | 推送待审核项到飞书 |

## 注意事项

1. **Cookie 有效期**：知乎 Cookie 会过期，通常 1-3 个月，过期后需要重新获取
2. **API 调用限制**：Kimi API 有速率限制（免费版 20 RPM），请勿频繁调用
3. **内容质量**：AI 生成的内容仅供参考，发布前请人工审核
4. **账号安全**：请勿将 Cookie 和 API Key 提交到代码仓库
5. **合规使用**：遵守知乎社区规范，不要发布违规内容

## License

MIT

## 作者

- GitHub: [@naiveKid](https://github.com/naiveKid)


path: config.schema.json
content: {
  "name": "zhihu-assistant",
  "description": "知乎热榜抓取与回答草稿生成助手",
  "version": "1.0.0",
  "author": "naiveKid",
  "config": {
    "zhihu_cookie": {
      "type": "string",
      "description": "知乎登录 Cookie，从浏览器开发者工具复制",
      "required": true,
      "secret": true
    },
    "kimi_api_key": {
      "type": "string",
      "description": "Kimi API Key，从 https://platform.moonshot.cn/ 获取",
      "required": true,
      "secret": true
    },
    "doubao_api_key": {
      "type": "string",
      "description": "豆包 API Key（备用，与 Kimi 二选一）",
      "required": false,
      "secret": true
    },
    "feishu_user_id": {
      "type": "string",
      "description": "飞书用户 ID，用于接收推送通知",
      "required": false
    },
    "fetch_limit": {
      "type": "integer",
      "description": "每次抓取热榜数量",
      "default": 10,
      "minimum": 1,
      "maximum": 50
    },
    "min_heat": {
      "type": "number",
      "description": "最小热度过滤（万）",
      "default": 10,
      "minimum": 0
    }
  }
}


path: install.sh
content: #!/bin/bash
# 知乎助手 Skill 安装脚本

set -e

echo "=== 安装知乎助手 Skill ==="

# 获取 skill 目录
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE_DIR="$HOME/.openclaw/workspace/zhihu-assistant"

echo "1. 创建工作目录: $WORKSPACE_DIR"
mkdir -p "$WORKSPACE_DIR"
mkdir -p "$WORKSPACE_DIR/data"
mkdir -p "$WORKSPACE_DIR/logs"

# 复制核心文件
echo "2. 复制核心文件..."
cp -r "$SKILL_DIR"/*.py "$WORKSPACE_DIR/"
cp -r "$SKILL_DIR/modules" "$WORKSPACE_DIR/"

# 安装依赖
echo "3. 安装 Python 依赖..."
pip3 install -q pyyaml requests httpx openai 2>/dev/null || pip3 install --break-system-packages -q pyyaml requests httpx openai 2>/dev/null || {
    echo "   警告: pip 安装失败，请手动安装: pyyaml requests httpx openai"
}

# 创建启动脚本
echo "4. 创建快捷命令..."
mkdir -p "$HOME/.openclaw/skills/zhihu-assistant"

cat > "$HOME/.openclaw/skills/zhihu-assistant/zhihu" << 'EOF'
#!/bin/bash
# 知乎助手快捷命令

WORKSPACE_DIR="$HOME/.openclaw/workspace/zhihu-assistant"

# 检查工作目录是否存在
if [ ! -d "$WORKSPACE_DIR" ]; then
    echo "错误: 知乎助手未安装"
    exit 1
fi

cd "$WORKSPACE_DIR"

COMMAND=$1
shift

case $COMMAND in
    fetch)
        python3 main.py fetch "$@"
        ;;
    stats)
        python3 main.py stats
        ;;
    notify)
        python3 main.py notify
        ;;
    logs)
        python3 main.py logs
        ;;
    reject)
        python3 main.py reject "$@"
        ;;
    config)
        echo "配置文件路径: $WORKSPACE_DIR/config.yaml"
        echo ""
        echo "当前配置（从 OpenClaw 读取）:"
        openclaw config get skills.zhihu-assistant 2>/dev/null || echo "  暂无配置"
        ;;
    help|--help|-h|*)
        echo "知乎助手 - 热榜抓取与内容管理"
        echo ""
        echo "使用: openclaw zhihu <command> [options]"
        echo ""
        echo "Commands:"
        echo "  fetch [--limit N]     抓取热榜并生成草稿"
        echo "  stats                 查看统计信息"
        echo "  notify                获取待推送列表"
        echo "  logs                  查看操作日志"
        echo "  reject --id ID        拒绝草稿"
        echo "  config                查看配置"
        echo "  help                  显示帮助"
        echo ""
        echo "配置方法:"
        echo "  openclaw config set skills.zhihu-assistant.zhihu_cookie 'xxx'"
        echo "  openclaw config set skills.zhihu-assistant.kimi_api_key 'xxx'"
        echo "  openclaw config set skills.zhihu-assistant.feishu_user_id 'xxx'"
        ;;
esac
EOF

chmod +x "$HOME/.openclaw/skills/zhihu-assistant/zhihu"

# 创建 tools 目录的快捷方式
mkdir -p "$SKILL_DIR/tools"
ln -sf "$HOME/.openclaw/skills/zhihu-assistant/zhihu" "$SKILL_DIR/tools/zhihu" 2>/dev/null || true

echo ""
echo "=== 安装完成 ==="
echo ""
echo "工作目录: $WORKSPACE_DIR"
echo ""
echo "使用前请先配置:"
echo "  openclaw config set skills.zhihu-assistant.zhihu_cookie 'your_cookie'"
echo "  openclaw config set skills.zhihu-assistant.kimi_api_key 'your_api_key'"
echo "  openclaw config set skills.zhihu-assistant.feishu_user_id 'your_user_id'"
echo ""
echo "使用命令:"
echo "  openclaw zhihu fetch --limit 10    # 抓取热榜"
echo "  openclaw zhihu stats               # 查看统计"
echo "  openclaw zhihu notify              # 推送到飞书"
echo "  openclaw zhihu help                # 显示帮助"
echo ""


path: main.py
content: """
知乎助手 - 主程序入口

功能：
- 抓取知乎热榜并生成回答草稿
- 管理待审核队列
- 推送到飞书
- 记录操作日志

使用方法：
    # 抓取热榜
    python main.py fetch --limit 10
    
    # 推送到飞书
    python main.py notify
    
    # 查看统计
    python main.py stats
    
    # 查看日志
    python main.py logs
    
    # 拒绝草稿
    python main.py reject --id P20260228...
"""
import os
import sys
import yaml
import json
import logging
import argparse
import subprocess
from pathlib import Path
from datetime import datetime

# 添加模块路径
sys.path.insert(0, str(Path(__file__).parent / "modules"))

from zhihu_hot import ZhihuHotFetcher
from memory_store import MemoryStore
from content_gen import ContentGenerator
from feishu_notify import FeishuNotifier

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.StreamHandler(),
        logging.FileHandler('./logs/app.log', encoding='utf-8')
    ]
)
logger = logging.getLogger(__name__)


def get_openclaw_config(key: str, default=None):
    """从 OpenClaw 配置读取"""
    try:
        result = subprocess.run(
            ['openclaw', 'config', 'get', f'skills.zhihu-assistant.{key}'],
            capture_output=True, text=True, timeout=10
        )
        if result.returncode == 0:
            value = result.stdout.strip()
            # 处理 JSON 输出
            if value.startswith('"') and value.endswith('"'):
                return json.loads(value)
            return value
    except Exception as e:
        logger.debug(f"读取 OpenClaw 配置失败: {e}")
    return default


class ZhihuAssistant:
    """
    知乎助手主类
    
    整合所有功能模块，提供统一的接口。
    
    Attributes:
        config: 配置字典
        memory: 记忆存储管理器
        zhihu: 知乎热榜抓取器
        generator: 内容生成器
        notifier: 飞书通知器
    """
    
    def __init__(self, config_path: str = "config.yaml"):
        """
        初始化助手
        
        Args:
            config_path: 配置文件路径，默认 config.yaml
        """
        self.config = self._load_config(config_path)
        self.memory = MemoryStore(
            data_dir=self.config['storage']['data_dir'],
            log_dir=self.config['storage']['log_dir']
        )
        self.zhihu = None
        self.generator = None
        self.notifier = None
        
        self._init_components()
    
    def _load_config(self, config_path: str) -> dict:
        """
        加载配置文件，优先从 OpenClaw config 读取敏感信息
        
        Args:
            config_path: 配置文件路径
            
        Returns:
            配置字典
        """
        # 默认配置
        config = {
            'zhihu': {
                'cookie': '',
                'user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
                'hot_url': 'https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total',
                'question_prefix': 'https://www.zhihu.com/question/'
            },
            'kimi': {
                'api_key': '',
                'model': 'kimi-coding/k2p5',
                'base_url': 'https://api.moonshot.cn/v1'
            },
            'feishu': {
                'use_openclaw': True,
                'webhook_url': '',
                'target_user': ''
            },
            'storage': {
                'data_dir': './data',
                'log_dir': './logs',
                'answered_file': './data/answered.json',
                'pending_file': './data/pending.json',
                'log_file': './logs/operation.log'
            },
            'filter': {
                'skip_keywords': ['广告', '推广', '招聘'],
                'min_heat': 10
            }
        }
        
        # 尝试从本地配置文件加载（非敏感配置）
        try:
            if os.path.exists(config_path):
                with open(config_path, 'r', encoding='utf-8') as f:
                    local_config = yaml.safe_load(f)
                    if local_config:
                        # 合并配置
                        for section in local_config:
                            if section in config:
                                config[section].update(local_config[section])
        except Exception as e:
            logger.warning(f"加载本地配置失败: {e}")
        
        # 从 OpenClaw config 读取敏感信息（优先级更高）
        # 知乎 Cookie
        zhihu_cookie = get_openclaw_config('zhihu_cookie')
        if zhihu_cookie:
            config['zhihu']['cookie'] = zhihu_cookie
            logger.info("已从 OpenClaw config 加载知乎 Cookie")
        
        # API Key（支持多种命名）
        [REDACTED_SECRET]'kimi_api_key') or get_openclaw_config('doubao_api_key')
        if [REDACTED_SECRET]'kimi']['api_key'] = api_key
            logger.info("已从 OpenClaw config 加载 API Key")
        
        # 飞书用户 ID
        feishu_user = get_openclaw_config('feishu_user_id')
        if feishu_user:
            config['feishu']['target_user'] = feishu_user
            logger.info("已从 OpenClaw config 加载飞书用户 ID")
        
        # 其他配置
        fetch_limit = get_openclaw_config('fetch_limit')
        if fetch_limit:
            try:
                config['filter']['fetch_limit'] = int(fetch_limit)
            except:
                pass
        
        min_heat = get_openclaw_config('min_heat')
        if min_heat:
            try:
                config['filter']['min_heat'] = float(min_heat)
            except:
                pass
        
        # 如果仍然没有敏感配置，尝试从环境变量读取
        if not config['kimi']['api_key']:
            config['kimi']['api_key'] = os.environ.get('KIMI_API_KEY', '')
        
        return config
    
    def _init_components(self):
        """初始化各功能组件"""
        # 知乎抓取器
        cookie = self.config['zhihu'].get('cookie', '')
        self.zhihu = ZhihuHotFetcher(
            cookie=cookie,
            user_agent=self.config['zhihu'].get('user_agent'),
            test_mode=(not cookie or cookie == 'your_zhihu_cookie_here')
        )
        
        # 内容生成器
        self.generator = ContentGenerator(
            [REDACTED_SECRET]'kimi']['api_key'],
            model=self.config['kimi']['model'],
            base_url=self.config['kimi']['base_url']
        )
        
        # 飞书通知器
        self.notifier = FeishuNotifier(
            use_openclaw=self.config['feishu'].get('use_openclaw', True),
            webhook_url=self.config['feishu'].get('webhook_url'),
            target_user=self.config['feishu'].get('target_user')
        )
    
    def fetch_and_generate(self, limit: int = 10):
        """
        抓取热榜并生成回答草稿
        
        Args:
            limit: 抓取前 N 条热榜
            
        Returns:
            新增待审核项数量
        """
        logger.info("开始抓取知乎热榜...")
        
        # 检查是否测试模式
        cookie = self.config['zhihu'].get('cookie', '')
        is_test_mode = not cookie or cookie == 'your_zhihu_cookie_here'
        
        # 1. 抓取热榜
        hot_list = self.zhihu.fetch_hot_list(limit=limit)
        
        if not hot_list:
            logger.warning("未获取到热榜数据")
            return 0
        
        logger.info(f"获取到 {len(hot_list)} 条热榜问题")
        
        # 2. 过滤和生成
        new_count = 0
        for question in hot_list:
            question_id = question['id']
            
            # 检查是否已回答
            if self.memory.is_answered(question_id):
                logger.info(f"跳过已回答问题: {question['title']}")
                continue
            
            # 检查热度
            min_heat = self.config.get('filter', {}).get('min_heat', 0)
            if question.get('heat', 0) < min_heat:
                logger.info(f"跳过低热度问题: {question['title']}")
                continue
            
            # 检查关键词过滤
            skip_keywords = self.config.get('filter', {}).get('skip_keywords', [])
            if any(kw in question['title'] for kw in skip_keywords):
                logger.info(f"跳过含关键词问题: {question['title']}")
                continue
            
            logger.info(f"正在生成回答: {question['title']}")
            
            # 3. 生成回答草稿
            draft = self.generator.generate_answer(
                question_title=question['title'],
                question_detail=question.get('excerpt', ''),
                excerpt=question.get('excerpt', '')
            )
            
            if not draft:
                logger.error(f"生成回答失败: {question['title']}")
                continue
            
            # 4. 质量评估
            quality = self.generator.estimate_quality(draft)
            logger.info(f"质量评分: {quality['score']}/100")
            
            # 质量阈值
            min_score = 30 if is_test_mode else 60
            if quality['score'] < min_score:
                logger.warning(f"质量不达标，跳过: {question['title']}")
                continue
            
            # 5. 添加到待审核队列
            pending_id = self.memory.add_pending(question, draft)
            new_count += 1
            
            logger.info(f"已添加到待审核队列: {pending_id}")
        
        logger.info(f"本次新增 {new_count} 条待审核项")
        return new_count
    
    def notify_pending(self):
        """
        获取待推送的待审核项列表
        
        Returns:
            待推送项列表
        """
        pending_items = self.memory.get_pending('pending')
        
        if not pending_items:
            logger.info("没有待审核项")
            return []
        
        logger.info(f"找到 {len(pending_items)} 条待审核项")
        
        # 返回未推送的项
        notified_items = []
        for item in pending_items:
            if not item.get('feishu_message_id'):
                notified_items.append(item)
                logger.info(f"待推送: {item['title']} (ID: {item['id']})")
        
        return notified_items
    
    def get_stats(self):
        """
        获取统计信息
        
        Returns:
            统计字典
        """
        return self.memory.get_stats()
    
    def reject(self, pending_id: str):
        """
        拒绝待审核项
        
        Args:
            pending_id: 待审核项ID
            
        Returns:
            True 如果成功，False 否则
        """
        if self.memory.update_pending_status(pending_id, 'rejected'):
            logger.info(f"已拒绝: {pending_id}")
            return True
        return False


def main():
    """主函数 - 解析命令行参数并执行"""
    parser = argparse.ArgumentParser(
        description='知乎助手 - 热榜抓取与内容管理',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  # 抓取热榜并生成草稿
  python main.py fetch --limit 10
  
  # 推送到飞书
  python main.py notify
  
  # 查看统计
  python main.py stats
  
  # 查看日志
  python main.py logs
  
  # 拒绝草稿
  python main.py reject --id P20260228...
        """
    )
    
    parser.add_argument('action', choices=[
        'fetch',      # 抓取热榜并生成草稿
        'notify',     # 获取待推送列表
        'stats',      # 查看统计
        'reject',     # 拒绝草稿
        'logs'        # 查看操作日志
    ], help='执行的操作')
    
    parser.add_argument('--id', help='待审核项ID（用于 reject）')
    parser.add_argument('--limit', type=int, default=10, help='抓取数量（默认10）')
    
    args = parser.parse_args()
    
    assistant = ZhihuAssistant()
    
    if args.action == 'fetch':
        count = assistant.fetch_and_generate(limit=args.limit)
        print(f"新增 {count} 条待审核项")
    
    elif args.action == 'notify':
        items = assistant.notify_pending()
        if items:
            print(f"找到 {len(items)} 条待推送项:")
            for item in items:
                print(f"  - {item['title'][:40]}... (ID: {item['id']})")
        else:
            print("没有待推送项")
    
    elif args.action == 'stats':
        stats = assistant.get_stats()
        print("\n=== 统计信息 ===")
        print(f"已回答问题: {stats['answered_count']}")
        print(f"待审核: {stats['pending_count']}")
        print(f"已批准: {stats['approved_count']}")
        print(f"已发布: {stats['published_count']}")
        print(f"已拒绝: {stats['rejected_count']}")
    
    elif args.action == 'reject':
        if not args.id:
            print("错误: 请指定 --id 参数")
            sys.exit(1)
        if assistant.reject(args.id):
            print(f"已拒绝: {args.id}")
        else:
            print(f"拒绝失败: {args.id}")
            sys.exit(1)
    
    elif args.action == 'logs':
        logs = assistant.memory.get_logs(limit=20)
        print("\n=== 最近操作日志 ===")
        for log in logs:
            print(f"[{log['timestamp']}] {log['action']}: {log['detail']}")


if __name__ == '__main__':
    main()


path: modules/__init__.py
content: # __init__.py
# 模块包初始化


path: modules/content_gen.py
content: """
内容生成模块

功能：
- 调用豆包 API 生成知乎回答草稿
- 支持自定义提示词
- 质量评估
- API 失败时降级到模拟草稿

使用方法：
    from content_gen import ContentGenerator
    
    generator = ContentGenerator(
        [REDACTED_SECRET]',
        model='doubao-seed-2-0-pro-260215',
        base_url='https://ark.cn-beijing.volces.com/api/v3'
    )
    
    draft = generator.generate_answer(
        question_title='为什么现在的年轻人都不爱结婚了？',
        question_detail='详细描述...',
        word_count=800
    )
"""
import os
import re
import time
import requests
from typing import Optional
import logging

logger = logging.getLogger(__name__)


class ContentGenerator:
    """
    内容生成器
    
    使用豆包 API 生成知乎回答草稿。
    当 API 调用失败时，自动降级到模拟草稿。
    
    Attributes:
        api_key: 豆包 API Key
        model: 模型名称
        base_url: API 基础 URL
        system_prompt: 系统提示词
    """
    
    def __init__(self, api_key: str = None, model: str = "doubao-seed-2-0-pro-260215", 
                 base_url: str = "https://ark.cn-beijing.volces.com/api/v3"):
        """
        初始化内容生成器
        
        Args:
            api_key: 豆包 API Key，默认从环境变量读取
            model: 模型名称
            base_url: API 基础 URL
        """
        self.[REDACTED_SECRET] or os.environ.get('KIMI_API_KEY')
        self.model = model
        self.base_url = base_url
        self.api_url = f"{base_url}/chat/completions"
        
        # 系统提示词 - 定义回答风格和格式
        self.system_prompt = """你是一位专业的知乎答主，擅长用通俗易懂的语言解释复杂问题。

回答要求：
1. 字数控制在800字左右（700-900字）
2. 结构清晰，包含：
   - 开头：直接回应问题，给出核心观点（100字左右）
   - 主体：分点论述，用例子和数据支撑（500-600字）
   - 结尾：总结升华，引发思考（100字左右）
3. 语言风格：
   - 专业但亲和，避免过于学术化
   - 适当使用口语化表达，像真人写作
   - 避免AI常用套话（如"值得注意的是"、"我们需要认识到"）
4. 内容要求：
   - 有独特见解，不泛泛而谈
   - 可以适度幽默，增加可读性
   - 如果问题有争议，呈现多角度观点

记住：你的目标是写出能获得高赞的优质回答。"""

    def generate_answer(self, question_title: str, question_detail: str = "", 
                       excerpt: str = "", word_count: int = 800) -> Optional[str]:
        """
        生成知乎回答草稿
        
        Args:
            question_title: 问题标题
            question_detail: 问题详细描述
            excerpt: 问题摘要
            word_count: 目标字数
            
        Returns:
            生成的回答草稿，失败时返回模拟草稿
        """
        # 添加延迟以避免 RPM 限制 (Tier0: 20 RPM)
        time.sleep(3)
        
        max_retries = 3
        for attempt in range(max_retries):
            try:
                # 构建用户提示
                user_prompt = self._build_prompt(question_title, question_detail, excerpt, word_count)
                
                # 调用 API
                headers = {
                    'Authorization': f'Bearer {self.api_key}',
                    'Content-Type': 'application/json'
                }
                
                data = {
                    'model': self.model,
                    'messages': [
                        {'role': 'system', 'content': self.system_prompt},
                        {'role': 'user', 'content': user_prompt}
                    ],
                    'temperature': 0.7,
                    'max_tokens': 1500
                }
                
                response = requests.post(self.api_url, headers=headers, json=data, timeout=60)
                response.raise_for_status()
                
                result = response.json()
                answer = result['choices'][0]['message']['content'].strip()
                
                # 后处理
                answer = self._post_process(answer)
                
                logger.info(f"成功生成回答，字数: {len(answer)}")
                return answer
                
            except requests.exceptions.HTTPError as e:
                if e.response.status_code == 429:
                    wait_time = (attempt + 1) * 5  # 5, 10, 15 秒
                    logger.warning(f"遇到速率限制，等待 {wait_time} 秒后重试...")
                    time.sleep(wait_time)
                else:
                    logger.error(f"生成回答失败: {e}")
                    break
            except Exception as e:
                logger.error(f"生成回答失败: {e}")
                break
        
        # 失败时返回模拟草稿
        return self._generate_mock_answer(question_title, word_count)
    
    def _build_prompt(self, title: str, detail: str, excerpt: str, word_count: int) -> str:
        """
        构建用户提示
        
        Args:
            title: 问题标题
            detail: 问题详情
            excerpt: 问题摘要
            word_count: 目标字数
            
        Returns:
            完整的提示文本
        """
        prompt_parts = [f"问题：{title}"]
        
        if detail:
            # 限制详情长度，避免 token 超限
            detail_short = detail[:500] + "..." if len(detail) > 500 else detail
            prompt_parts.append(f"问题描述：{detail_short}")
        
        if excerpt and not detail:
            prompt_parts.append(f"问题摘要：{excerpt}")
        
        prompt_parts.append(f"\n请为这个问题写一段{word_count}字左右的优质回答。")
        
        return "\n".join(prompt_parts)
    
    def _post_process(self, text: str) -> str:
        """
        后处理生成的文本
        
        - 移除 AI 标识
        - 确保结尾有标点
        - 规范化空行
        
        Args:
            text: 原始文本
            
        Returns:
            处理后的文本
        """
        # 移除可能的模型标识
        text = re.sub(r'^\[?AI\]?\s*', '', text)
        
        # 确保结尾有适当的标点
        text = text.strip()
        if text and text[-1] not in '。！？.!?':
            text += '。'
        
        # 规范化空行（最多两个连续空行）
        text = re.sub(r'\n{3,}', '\n\n', text)
        
        return text
    
    def _generate_mock_answer(self, question_title: str, word_count: int = 800) -> str:
        """
        生成模拟回答（用于 API 失败时的降级）
        
        Args:
            question_title: 问题标题
            word_count: 目标字数
            
        Returns:
            模拟回答草稿
        """
        # 根据关键词匹配预设回答
        mock_answers = {
            "结婚": """这个问题确实戳中了很多年轻人的痛点。

先说结论：不是不想结，而是不敢轻易结。

现在的年轻人面临的压力和上一代人完全不同。房价、教育成本、职场竞争，每一项都是一座大山。结婚不再是两个人搭伙过日子那么简单，而是意味着要承担巨大的经济责任。

更重要的是，现在的年轻人更重视自我实现。我们不再接受"到了年龄就该结婚"这种逻辑，而是希望找到真正契合的伴侣。如果找不到，宁可单身也不愿意将就。

当然，这并不意味着年轻人排斥婚姻。恰恰相反，我们对婚姻的期待更高了——不再是找个人凑合过一辈子，而是希望找到能共同成长、互相成就的伴侣。

这种变化，与其说是"不爱结婚"，不如说是"更谨慎地对待婚姻"。这未必是坏事，毕竟离婚率居高不下，谨慎一点总比草率结婚再离婚要好。

你怎么看这个问题？""",
            
            "人工智能": """2024年确实是AI发展的分水岭。

从ChatGPT到各种AI工具的普及，我们见证了一场静悄悄的技术革命。但说"爆发式"可能有点夸张——AI的发展是渐进的，只是今年恰好到了大众感知的临界点。

真正值得关注的是几个趋势：

第一，AI正在从"玩具"变成"工具"。早期的AI更多是噱头，现在已经开始真正提升生产力。写代码、做设计、处理文档，AI都能帮上大忙。

第二，门槛在快速降低。不需要懂技术，普通人也能用上强大的AI能力。这种普惠性是技术扩散的关键。

第三，竞争格局在重塑。OpenAI不再是一家独大，Claude、Gemini、国内的各大模型都在快速迭代。这种竞争对消费者是好事。

当然，焦虑也是真实的。很多人担心被AI取代。我的看法是：AI确实会替代一些工作，但也会创造新的机会。关键是保持学习，让自己成为会使用AI的人，而不是被AI替代的人。

未来已来，只是分布不均。你准备好迎接AI时代了吗？"""
        }
        
        # 根据问题关键词匹配模拟回答
        for keyword, answer in mock_answers.items():
            if keyword in question_title:
                return answer
        
        # 默认回答
        return f"""这是一个很有意思的问题。

关于"{question_title}"，我的看法是：

首先，这个问题没有标准答案，不同人有不同的经历和观点。但我们可以从几个角度来分析。

从社会层面看，这种现象往往反映了更深层次的变化。可能是经济结构的调整，也可能是价值观的变迁。理解这些背景，有助于我们更好地把握问题的本质。

从个人层面看，每个人面临的具体情况不同。重要的是找到适合自己的方式，而不是盲目跟随潮流。

最后，保持开放的心态很重要。世界在不断变化，今天的答案可能不适用于明天。持续学习、保持思考，才能在这个快速变化的时代找到自己的位置。

你对这个问题有什么看法？欢迎在评论区分享你的观点。"""
    
    def estimate_quality(self, answer: str) -> dict:
        """
        评估回答质量（启发式评分）
        
        评分维度：
        - 字数（30分）
        - 段落结构（20分）
        - 列表使用（20分）
        - 例子使用（15分）
        - 数据使用（15分）
        - AI套话扣分（-5分/个）
        
        Args:
            answer: 回答文本
            
        Returns:
            质量评分字典
        """
        score = 0
        issues = []
        
        # 字数检查
        char_count = len(answer)
        if 700 <= char_count <= 900:
            score += 30
        elif 500 <= char_count < 700:
            score += 20
            issues.append("字数偏少")
        elif char_count > 900:
            score += 20
            issues.append("字数偏多")
        else:
            score += 10
            issues.append("字数不足")
        
        # 结构检查（是否有分段）
        paragraphs = [p for p in answer.split('\n') if p.strip()]
        if len(paragraphs) >= 3:
            score += 20
        else:
            score += 10
            issues.append("段落较少")
        
        # 检查是否有列表（分点论述）
        if re.search(r'[\d一二三四五六七八九十]+[、.．]', answer):
            score += 20
        
        # 检查是否有例子
        if re.search(r'(比如|例如|举个栗子|像|就像)', answer):
            score += 15
        
        # 检查是否有数据/数字
        if re.search(r'\d+%|\d+\.?\d+\s*(万|亿|个|人|次)', answer):
            score += 15
        
        # 检查AI套话
        ai_phrases = ['值得注意的是', '我们需要认识到', '不可否认的是', '总而言之']
        for phrase in ai_phrases:
            if phrase in answer:
                score -= 5
                issues.append(f"包含AI套话: {phrase}")
        
        return {
            'score': max(0, score),
            'char_count': char_count,
            'paragraph_count': len(paragraphs),
            'issues': issues,
            'is_acceptable': score >= 60
        }


path: modules/feishu_notify.py
content: """
飞书通知模块

功能：
- 格式化待审核消息
- 支持飞书推送（通过 OpenClaw 或 Webhook）

使用方法：
    from feishu_notify import FeishuNotifier
    
    notifier = FeishuNotifier(use_openclaw=True)
    notifier.send_pending_review(pending_item)
"""
import logging
from typing import Optional

logger = logging.getLogger(__name__)


class FeishuNotifier:
    """
    飞书通知器
    
    负责格式化并推送审核消息到飞书。
    支持两种模式：
    - OpenClaw 内置推送（推荐）
    - Webhook 推送（备用）
    
    Attributes:
        use_openclaw: 是否使用 OpenClaw 内置推送
        webhook_url: Webhook URL（备用模式）
        target_user: 目标用户/群组 ID
    """
    
    def __init__(self, use_openclaw: bool = True, webhook_url: str = None, target_user: str = None):
        """
        初始化通知器
        
        Args:
            use_openclaw: 是否使用 OpenClaw 内置推送
            webhook_url: Webhook URL（备用）
            target_user: 目标用户/群组 ID
        """
        self.use_openclaw = use_openclaw
        self.webhook_url = webhook_url
        self.target_user = target_user
    
    def format_pending_message(self, pending_item: dict) -> str:
        """
        格式化待审核消息
        
        Args:
            pending_item: 待审核项字典
            
        Returns:
            格式化后的消息文本
        """
        title = pending_item['title']
        heat = pending_item.get('heat', 0)
        url = pending_item['url']
        draft = pending_item['draft']
        pending_id = pending_item['id']
        
        # 截断草稿预览（前400字）
        draft_preview = draft[:400] + "..." if len(draft) > 400 else draft
        
        return f"""📋 **知乎回答待审核**

**问题：** {title}
**热度：** {heat}万  
**链接：** {url}

---

**回答草稿（{len(draft)}字）：**
{draft_preview}

---

**待审核ID：** `{pending_id}`

💡 **操作方式：**
• 复制内容到知乎发布
• 回复 "查看 {pending_id}" 查看完整草稿
"""
    
    def format_full_draft(self, pending_item: dict) -> str:
        """
        格式化完整草稿消息
        
        Args:
            pending_item: 待审核项字典
            
        Returns:
            包含完整草稿的消息文本
        """
        draft = pending_item['draft']
        
        return f"""📄 **完整草稿**

**问题：** {pending_item['title']}
**ID：** `{pending_item['id']}`

---

{draft}

---

💡 **操作：** 复制上方内容到知乎发布
"""


# 快捷函数
def format_pending_review(pending_item: dict) -> str:
    """
    格式化待审核消息的快捷函数
    
    Args:
        pending_item: 待审核项字典
        
    Returns:
        格式化后的消息文本
    """
    notifier = FeishuNotifier()
    return notifier.format_pending_message(pending_item)


def format_full_draft(pending_item: dict) -> str:
    """
    格式化完整草稿的快捷函数
    
    Args:
        pending_item: 待审核项字典
        
    Returns:
        包含完整草稿的消息文本
    """
    notifier = FeishuNotifier()
    return notifier.format_full_draft(pending_item)


path: modules/memory_store.py
content: """
记忆存储模块

功能：
- 管理已回答问题列表（去重）
- 管理待审核队列（增删改查）
- 记录操作日志
- 数据持久化到 JSON 文件

数据文件：
- data/answered.json: 已回答问题ID列表
- data/pending.json: 待审核队列
- logs/operation.log: 操作日志（JSON Lines 格式）

使用方法：
    from memory_store import MemoryStore
    
    memory = MemoryStore(data_dir='./data', log_dir='./logs')
    
    # 检查是否已回答
    if memory.is_answered('123456'):
        print('已回答过')
    
    # 添加到待审核队列
    pending_id = memory.add_pending(question, draft)
    
    # 更新状态
    memory.update_pending_status(pending_id, 'published')
"""
import json
import os
from typing import List, Dict, Optional
from datetime import datetime
from pathlib import Path
import logging

logger = logging.getLogger(__name__)


class MemoryStore:
    """
    记忆存储管理器
    
    负责管理所有数据的持久化存储，包括：
    - 已回答问题的去重
    - 待审核队列的管理
    - 操作日志的记录
    
    Attributes:
        data_dir: 数据文件存储目录
        log_dir: 日志文件存储目录
        answered_file: 已回答问题列表文件路径
        pending_file: 待审核队列文件路径
        log_file: 操作日志文件路径
    """
    
    def __init__(self, data_dir: str = "./data", log_dir: str = "./logs"):
        """
        初始化存储管理器
        
        Args:
            data_dir: 数据文件存储目录，默认 ./data
            log_dir: 日志文件存储目录，默认 ./logs
        """
        self.data_dir = Path(data_dir)
        self.log_dir = Path(log_dir)
        
        # 确保目录存在
        self.data_dir.mkdir(parents=True, exist_ok=True)
        self.log_dir.mkdir(parents=True, exist_ok=True)
        
        # 文件路径
        self.answered_file = self.data_dir / "answered.json"
        self.pending_file = self.data_dir / "pending.json"
        self.log_file = self.log_dir / "operation.log"
        
        # 内存缓存
        self._answered_cache: set = set()
        self._pending_cache: List[Dict] = []
        
        self._load_data()
    
    def _load_data(self):
        """从磁盘加载数据到内存缓存"""
        # 加载已回答问题
        if self.answered_file.exists():
            try:
                with open(self.answered_file, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    self._answered_cache = set(data.get('question_ids', []))
                logger.info(f"已加载 {len(self._answered_cache)} 条已回答问题记录")
            except Exception as e:
                logger.error(f"加载已回答问题失败: {e}")
                self._answered_cache = set()
        
        # 加载待审核队列
        if self.pending_file.exists():
            try:
                with open(self.pending_file, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    self._pending_cache = data.get('items', [])
                logger.info(f"已加载 {len(self._pending_cache)} 条待审核记录")
            except Exception as e:
                logger.error(f"加载待审核队列失败: {e}")
                self._pending_cache = []
    
    def _save_answered(self):
        """保存已回答问题列表到磁盘"""
        try:
            data = {
                'question_ids': list(self._answered_cache),
                'updated_at': datetime.now().isoformat()
            }
            with open(self.answered_file, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            logger.error(f"保存已回答问题失败: {e}")
    
    def _save_pending(self):
        """保存待审核队列到磁盘"""
        try:
            data = {
                'items': self._pending_cache,
                'updated_at': datetime.now().isoformat()
            }
            with open(self.pending_file, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
        except Exception as e:
            logger.error(f"保存待审核队列失败: {e}")
    
    # ========== 已回答问题管理 ==========
    
    def is_answered(self, question_id: str) -> bool:
        """
        检查问题是否已回答过
        
        Args:
            question_id: 问题ID
            
        Returns:
            True 如果已回答，False 否则
        """
        return question_id in self._answered_cache
    
    def add_answered(self, question_id: str, title: str = ""):
        """
        标记问题为已回答
        
        Args:
            question_id: 问题ID
            title: 问题标题（用于日志）
        """
        self._answered_cache.add(question_id)
        self._save_answered()
        self.log_operation("ANSWERED", f"问题已回答: {title} (ID: {question_id})")
        logger.info(f"标记问题为已回答: {question_id}")
    
    def get_answered_count(self) -> int:
        """获取已回答问题数量"""
        return len(self._answered_cache)
    
    # ========== 待审核队列管理 ==========
    
    def add_pending(self, question: Dict, draft: str) -> str:
        """
        添加待审核项
        
        Args:
            question: 问题信息字典，包含 id, title, url, heat 等
            draft: 生成的回答草稿
            
        Returns:
            pending_id: 待审核项唯一ID
        """
        # 生成唯一ID：P + 时间戳 + 问题ID
        pending_id = f"P{datetime.now().strftime('%Y%m%d%H%M%S')}_{question['id']}"
        
        item = {
            'id': pending_id,
            'question_id': question['id'],
            'title': question['title'],
            'url': question['url'],
            'heat': question.get('heat', 0),
            'draft': draft,
            'status': 'pending',  # pending / approved / rejected / published
            'created_at': datetime.now().isoformat(),
            'updated_at': datetime.now().isoformat(),
            'feishu_message_id': None  # 飞书消息ID
        }
        
        self._pending_cache.append(item)
        self._save_pending()
        
        self.log_operation("PENDING_ADD", f"添加待审核: {question['title']}")
        logger.info(f"添加待审核项: {pending_id}")
        
        return pending_id
    
    def get_pending(self, status: str = 'pending') -> List[Dict]:
        """
        获取待审核列表
        
        Args:
            status: 筛选状态（pending/approved/rejected/published/all）
            
        Returns:
            待审核项列表
        """
        if status == 'all':
            return self._pending_cache.copy()
        return [item for item in self._pending_cache if item['status'] == status]
    
    def get_pending_by_id(self, pending_id: str) -> Optional[Dict]:
        """
        通过ID获取待审核项
        
        Args:
            pending_id: 待审核项ID
            
        Returns:
            待审核项字典，不存在返回 None
        """
        for item in self._pending_cache:
            if item['id'] == pending_id:
                return item
        return None
    
    def update_pending_status(self, pending_id: str, status: str, feishu_msg_id: str = None) -> bool:
        """
        更新待审核项状态
        
        Args:
            pending_id: 待审核项ID
            status: 新状态（approved/rejected/published）
            feishu_msg_id: 飞书消息ID（可选）
            
        Returns:
            True 如果更新成功，False 否则
        """
        for item in self._pending_cache:
            if item['id'] == pending_id:
                item['status'] = status
                item['updated_at'] = datetime.now().isoformat()
                if feishu_msg_id:
                    item['feishu_message_id'] = feishu_msg_id
                
                self._save_pending()
                self.log_operation("PENDING_UPDATE", f"更新状态: {pending_id} -> {status}")
                logger.info(f"更新待审核项状态: {pending_id} -> {status}")
                return True
        
        logger.warning(f"待审核项不存在: {pending_id}")
        return False
    
    def remove_pending(self, pending_id: str) -> bool:
        """
        删除待审核项
        
        Args:
            pending_id: 待审核项ID
            
        Returns:
            True 如果删除成功，False 否则
        """
        for i, item in enumerate(self._pending_cache):
            if item['id'] == pending_id:
                self._pending_cache.pop(i)
                self._save_pending()
                self.log_operation("PENDING_REMOVE", f"删除: {pending_id}")
                return True
        return False
    
    # ========== 操作日志 ==========
    
    def log_operation(self, action: str, detail: str):
        """
        记录操作日志
        
        Args:
            action: 操作类型，如 ANSWERED, PENDING_ADD, PUBLISH 等
            detail: 详细信息
        """
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'action': action,
            'detail': detail
        }
        
        # 追加到日志文件（JSON Lines 格式）
        try:
            with open(self.log_file, 'a', encoding='utf-8') as f:
                f.write(json.dumps(log_entry, ensure_ascii=False) + '\n')
        except Exception as e:
            logger.error(f"写入日志失败: {e}")
    
    def get_logs(self, limit: int = 100, action_filter: str = None) -> List[Dict]:
        """
        获取操作日志
        
        Args:
            limit: 返回最近 N 条
            action_filter: 按操作类型筛选
            
        Returns:
            日志条目列表
        """
        logs = []
        
        if not self.log_file.exists():
            return logs
        
        try:
            with open(self.log_file, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if not line:
                        continue
                    try:
                        entry = json.loads(line)
                        if action_filter and entry.get('action') != action_filter:
                            continue
                        logs.append(entry)
                    except json.JSONDecodeError:
                        continue
        except Exception as e:
            logger.error(f"读取日志失败: {e}")
        
        # 返回最近的 N 条
        return logs[-limit:]
    
    # ========== 统计信息 ==========
    
    def get_stats(self) -> Dict:
        """
        获取统计信息
        
        Returns:
            统计字典，包含各状态数量
        """
        pending = self.get_pending('pending')
        approved = self.get_pending('approved')
        published = self.get_pending('published')
        rejected = self.get_pending('rejected')
        
        return {
            'answered_count': len(self._answered_cache),
            'pending_count': len(pending),
            'approved_count': len(approved),
            'published_count': len(published),
            'rejected_count': len(rejected),
            'total_drafts': len(self._pending_cache)
        }


if __name__ == '__main__':
    # 测试代码
    store = MemoryStore()
    
    # 测试添加待审核
    test_question = {
        'id': '123456',
        'title': '测试问题',
        'url': 'https://zhihu.com/question/123456',
        'heat': 100.5
    }
    pending_id = store.add_pending(test_question, "这是测试回答草稿")
    print(f"添加待审核项: {pending_id}")
    
    # 测试获取统计
    stats = store.get_stats()
    print(f"统计: {json.dumps(stats, ensure_ascii=False, indent=2)}")


path: modules/zhihu_hot.py
content: """
知乎热榜抓取模块

功能：
- 抓取知乎热榜前 N 条问题
- 解析问题标题、链接、热度等信息
- 支持测试模式（无 Cookie 时使用模拟数据）

使用方法：
    from zhihu_hot import ZhihuHotFetcher
    
    fetcher = ZhihuHotFetcher(cookie='your_cookie')
    hot_list = fetcher.fetch_hot_list(limit=10)
    
    for question in hot_list:
        print(f"[{question['heat']}万] {question['title']}")
"""
import requests
import json
import re
from typing import List, Dict, Optional
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


# 测试数据 - 用于未配置 Cookie 时的演示
TEST_HOT_LIST = [
    {
        "id": "1234567890",
        "title": "为什么现在的年轻人都不爱结婚了？",
        "url": "https://www.zhihu.com/question/1234567890",
        "heat": 1250.5,
        "heat_text": "1250万",
        "excerpt": "最近发现身边很多90后、00后都没有结婚打算...",
        "fetched_at": datetime.now().isoformat()
    },
    {
        "id": "1234567891",
        "title": "如何看待2024年人工智能的爆发式发展？",
        "url": "https://www.zhihu.com/question/1234567891",
        "heat": 980.2,
        "heat_text": "980万",
        "excerpt": "从ChatGPT到各种AI工具，人工智能似乎在一夜之间改变了我们的生活...",
        "fetched_at": datetime.now().isoformat()
    },
    {
        "id": "1234567892",
        "title": "长期熬夜对身体有哪些不可逆的伤害？",
        "url": "https://www.zhihu.com/question/1234567892",
        "heat": 756.8,
        "heat_text": "756万",
        "excerpt": "作为一个经常熬夜的程序员，想知道长期熬夜到底会对身体造成什么影响...",
        "fetched_at": datetime.now().isoformat()
    }
]


class ZhihuHotFetcher:
    """
    知乎热榜抓取器
    
    Attributes:
        cookie: 知乎登录 Cookie
        user_agent: 请求 User-Agent
        test_mode: 是否为测试模式（无有效 Cookie 时使用模拟数据）
        session: requests.Session 对象
    """
    
    def __init__(self, cookie: str = None, user_agent: str = None, test_mode: bool = False):
        """
        初始化抓取器
        
        Args:
            cookie: 知乎 Cookie 字符串
            user_agent: 请求 User-Agent，默认使用 Chrome
        """
        self.cookie = cookie or ''
        self.user_agent = user_agent or 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
        # 如果没有 Cookie 或 Cookie 是占位符，使用测试模式
        self.test_mode = test_mode or (not cookie or cookie == 'your_zhihu_cookie_here')
        self.session = requests.Session()
        self._setup_headers()
        
    def _setup_headers(self):
        """设置 HTTP 请求头"""
        headers = {
            'User-Agent': self.user_agent,
            'Accept': 'application/json, text/plain, */*',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Referer': 'https://www.zhihu.com/hot',
            'X-Requested-With': 'fetch'
        }
        # 只在非测试模式时添加 Cookie
        if self.cookie and not self.test_mode:
            headers['Cookie'] = self.cookie
        self.session.headers.update(headers)
    
    def fetch_hot_list(self, limit: int = 10) -> List[Dict]:
        """
        抓取知乎热榜
        
        Args:
            limit: 返回前 N 条热榜
            
        Returns:
            热榜问题列表，每项包含：
            - id: 问题ID
            - title: 问题标题
            - url: 问题链接
            - heat: 热度值（万）
            - excerpt: 问题摘要
            - fetched_at: 抓取时间
        """
        # 测试模式：返回模拟数据
        if self.test_mode:
            logger.info("使用测试模式（未配置知乎Cookie）")
            return TEST_HOT_LIST[:limit]
        
        try:
            url = "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total"
            params = {
                'limit': 50,  # 多取一些用于过滤
                'desktop': 'true'
            }
            
            response = self.session.get(url, params=params, timeout=30)
            response.raise_for_status()
            
            data = response.json()
            questions = []
            
            for item in data.get('data', [])[:limit]:
                target = item.get('target', {})
                question = self._parse_question(item, target)
                if question:
                    questions.append(question)
            
            logger.info(f"成功抓取 {len(questions)} 条热榜问题")
            return questions
            
        except requests.RequestException as e:
            logger.error(f"抓取热榜失败: {e}")
            # 真实抓取失败时，降级到测试数据
            logger.info("使用测试数据作为降级")
            return TEST_HOT_LIST[:limit]
        except json.JSONDecodeError as e:
            logger.error(f"解析JSON失败: {e}")
            return TEST_HOT_LIST[:limit]
    
    def _parse_question(self, item: Dict, target: Dict) -> Optional[Dict]:
        """
        解析单条热榜数据
        
        Args:
            item: API 返回的原始数据项
            target: 目标对象数据
            
        Returns:
            解析后的问题字典，解析失败返回 None
        """
        try:
            # 提取问题ID
            question_id = target.get('id') or target.get('question', {}).get('id')
            if not question_id:
                return None
            
            # 提取热度
            heat_text = item.get('detail_text', '0')
            heat = self._parse_heat(heat_text)
            
            # 提取标题
            title = target.get('title') or target.get('question', {}).get('title', '')
            
            # 提取摘要
            excerpt = target.get('excerpt', '')
            
            return {
                'id': str(question_id),
                'title': title,
                'url': f"https://www.zhihu.com/question/{question_id}",
                'heat': heat,
                'heat_text': heat_text,
                'excerpt': excerpt,
                'fetched_at': datetime.now().isoformat()
            }
            
        except Exception as e:
            logger.warning(f"解析问题数据失败: {e}")
            return None
    
    def _parse_heat(self, heat_text: str) -> float:
        """
        解析热度值，统一转换为万为单位
        
        Args:
            heat_text: 热度文本，如 "1250万" 或 "12500000"
            
        Returns:
            热度值（万）
        """
        try:
            heat_text = heat_text.strip()
            
            # 匹配 "1234 万热度" 或 "1234万"
            if '万' in heat_text:
                match = re.search(r'(\d+\.?\d*)', heat_text)
                if match:
                    return float(match.group(1))
            
            # 匹配纯数字
            match = re.search(r'(\d+)', heat_text)
            if match:
                num = int(match.group(1))
                # 如果数字很大，转换为万
                if num > 10000:
                    return round(num / 10000, 1)
                return num
                
            return 0.0
            
        except Exception:
            return 0.0


if __name__ == '__main__':
    # 测试代码
    fetcher = ZhihuHotFetcher(cookie='', user_agent='Test')
    hot_list = fetcher.fetch_hot_list(limit=3)
    for q in hot_list:
        print(f"[{q['heat']}万] {q['title']}")
        print(f"   {q['url']}")
        print()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)