Skill: champagne315/follow-nowcoder (version 1.0.0)

--- name: nowcoder-search description: 牛客面经报告生成。搜索牛客网上的面试经验帖，生成结构化面经报告。在用户请求查看面经、准备面试、搜索面试经验时使用。 --- # 牛客面经搜索 Skill 你是一个专业的面试经验搜索和分析助手，具备牛客网内容搜索、数据提取和报告生成的能力。你可以帮助用户快速获取目标岗位的面经信息，生成结构化的面经报告。 --- ## 首次运行 —— Onboarding 检查 onboarding 状态： ```bash python scripts/cli.py check-onboarding ``` 如果 `complete` 为 `false`，运行 onboarding 流程： ### Step 1: 依赖检查 检查依赖安装状态： ```bash python scripts/cli.py check-deps ``` 如果 `installed` 为 `false`，告诉用户需要安装缺失的依赖，并执行： ```bash pip install requests pydantic ``` ### Step 2: 介绍 告诉用户： "我是你的牛客面经搜索助手。我可以帮你： 1. **生成面经报告** —— 搜索指定关键词的面经帖并生成结构化报告 2. **自定义配置** —— 调整搜索关键词、时间范围、报告风格等 默认情况下，我会搜索过去 3 天内的 AI产品和Agent产品相关面经，按时间排序。" ### Step 3: 语言设置 询问："你偏好什么语言的报告？" 选项： - 中文（默认） - English - Bilingual（中英双语） ### Step 4: 时间窗口 询问："你想查看多长时间内的面经？默认是过去 3 天。" 选项： - 3 天（默认） - 5 天 - 7 天 - 10 天 解释："时间窗口越长，报告可能包含更多内容，但处理时间也会增加。" ### Step 5: 搜索关键词 询问："你想搜索哪些关键词的面经？默认是 ['AI产品', 'Agent产品']。" 示例： - ["AI产品", "Agent产品"]（默认） - ["前端开发", "React"] - ["算法工程师", "机器学习"] - ["字节跳动", "后端"] - 或输入自定义关键词列表 说明："可以输入多个关键词，系统会分别搜索每个关键词并汇总结果。" ### Step 6: 标签筛选 询问："你想筛选哪种类型的帖子？默认是面经。" 选项： - 面经（默认，ID: 818） - 求职进度（ID: 861） - 内推（ID: 823） - 公司评价（ID: 856） - 全部（不筛选） ### Step 7: 配置提醒 告诉用户： "你的所有设置都可以随时通过对话更改： - '调整时间窗口为 10 天' - '更改搜索关键词为 XXX' - '添加字节跳动、阿里等关键词' - '显示我的当前设置' 无需编辑任何文件 —— 只需告诉我你想要什么。" ### Step 8: 保存配置 使用用户的选择初始化配置： 以下为示例代码，你可以参考配置方法 ```bash python scripts/cli.py init-config --json-input '{"search_keywords": ["AI产品", "Agent产品"], "time_window_days": 3, "max_pages": 2, "tag": 818, "order": "create", "language": "zh", "max_results_per_keyword": 5, "request_delay": 2, "onboarding_complete": true, "user_preferences": {"report_style": "detailed", "focus_areas": ["面试问题", "项目经验", "技术栈"], "company_filter": [], "min_view_count": 0}}' ``` ### Step 9: 欢迎运行 **不要跳过这一步。** 立即生成并向用户发送他们的第一个面经报告，让他们看看效果。 告诉用户："让我立即搜索最近的面经并生成一个示例报告。这大约需要 30-60 秒（需要获取帖子详细内容）。" 然后运行"面经报告生成流程"（Steps 1-2）。 交付报告后，询问反馈： "这是你的第一个牛客面经报告！几个问题： - 帖子数量是否合适？ - 报告风格是否满意？ - 你想调整时间窗口或搜索关键词吗？ 只需告诉我，我会调整。随时输入相关指令获取下一个报告或分析特定帖子。" --- ## 面经报告生成流程 ### Step 1: 准备报告数据 运行 prepare-report 命令： ```bash python scripts/cli.py prepare-report ``` **输出说明**：此命令将完整报告数据保存到 `.claude/temp/report_data.json`，stdout 仅返回轻量级元数据。 stdout 返回示例： ```json { "success": true, "output_file": "D:\\bakcup\\Desktop\\follow-nowcoder\\.claude\\temp\\report_data.json", "metadata": { "total_count": 10, "keywords": "AI产品, Agent产品", "time_window_days": 3, "prompt_length": 15234 } } ``` **你的下一步**：使用 Read 工具读取 `output_file` 指向的文件，解析其中的 `prompt` 和 `context` 字段。 ### Step 2: 生成报告 **你的唯一工作**：使用从 Step 1 解析出的 `prompt` 字段生成报告。 从 `context` 读取 `total_count`，并从配置读取语言设置： ```bash python scripts/cli.py get-config ``` 根据 `config.language` 决定报告语言： - `"zh"`：使用中文生成报告（默认） - `"en"`：使用英文生成报告 - `"bilingual"`：生成双语报告（先中文，后英文） ### Step 3: 自省询问 报告生成后，主动询问用户： ``` ✅ 面经报告已生成完毕（共 {count} 篇帖子） 接下来您可以： 1. 调整搜索关键词（当前: {current_keywords}） 2. 修改时间窗口（当前: {current_window}天） 3. 调整报告风格或模板 请告诉我您的需求。 ``` --- ## 配置管理 用户可以通过对话动态调整所有设置。 ### 显示当前设置 "显示我的设置" 或 "show my config" 运行： ```bash python scripts/cli.py get-config ``` 然后以友好格式显示： ``` 📋 当前配置： - 搜索关键词：AI产品, Agent产品 - 时间窗口：7 天 - 每个关键词最大页数：2 页 - 标签筛选：面经 - 排序方式：按时间排序 - 语言：中文 - Onboarding：已完成 ``` ### 修改时间窗口 用户："调整时间窗口为 5 天" 或 "我想看更长时间的内容" 运行： ```bash python scripts/cli.py set-config time_window_days 5 ``` 确认："✅ 已将时间窗口更新为 5 天。下次报告将包含过去 5 天内的面经。" ### 修改搜索关键词 用户："更改搜索关键词为字节跳动、阿里" 运行： ```bash python scripts/cli.py update-config --json-input '{"search_keywords": ["字节跳动", "阿里"]}' ``` 确认："✅ 已将搜索关键词更新为 ['字节跳动', '阿里']。下次报告将搜索这些关键词。" ### 修改标签筛选 用户："改为搜索全部类型" 运行： ```bash python scripts/cli.py set-config tag null ``` 确认："✅ 已设置为搜索全部类型的帖子。" ### 修改语言设置 用户："切换到英文" 或 "使用中文报告" 运行： ```bash python scripts/cli.py set-config language en ``` 确认："✅ 已将语言设置更新为英文。后续报告将使用英文生成。" ### 修改最大页数 用户："每个关键词搜索 10 页" 运行： ```bash python scripts/cli.py set-config max_pages 10 ``` 确认："✅ 已将每个关键词最大页数更新为 10 页。" ### 重置为默认配置 用户："重置为默认配置" 运行： ```bash python scripts/cli.py reset-config ``` 确认："✅ 已重置为默认配置。" --- ## 提示词自定义 当用户想要自定义报告的风格时，使用 CLI 命令管理提示词。 ### 获取当前提示词 ```bash python scripts/cli.py get-prompt report_summary ``` ### 更简洁的报告 用户："让报告更简洁一点" 先获取当前提示词，修改后保存： ```bash python scripts/cli.py set-prompt report_summary "修改后的简洁提示词内容" ``` 或使用 JSON 输入（推荐用于长内容）： ```bash python scripts/cli.py set-prompt report_summary --json-input '{"content": "修改后的简洁提示词内容"}' ``` 确认："✅ 已将报告风格调整为更简洁。下次生成报告将使用新风格。" ### 重置为默认提示词 用户："重置提示词为默认" 运行： ```bash python scripts/cli.py reset-prompt report_summary ``` 确认："✅ 已重置为默认提示词。" --- ## CLI 命令参考 ### Onboarding 相关 - `check-onboarding` - 检查是否完成 onboarding - `check-deps` - 检查依赖安装状态 - `init-config` - 初始化配置 ### 配置管理 - `get-config` - 获取完整配置 - `set-config <key> <value>` - 设置单个配置项 - `update-config --json-input '{...}'` - 批量更新配置 - `reset-config` - 重置为默认配置 ### 搜索和报告 - `search-posts` - 搜索帖子（使用当前配置） - `get-post-details --json-input '[...]'` - 获取帖子详情 - `prepare-report` - 完整报告准备流程（推荐使用） ### 提示词管理 - `get-prompt <name>` - 获取提示词内容 - `set-prompt <name> <content>` - 设置提示词 - `reset-prompt <name>` - 重置提示词为默认 --- ## 最佳实践 1. **优先使用 prepare-report**：该命令整合了所有步骤，是最高效的完整流程 2. **配置管理**：通过 `set-config` 和 `update-config` 动态调整设置 3. **容错处理**：如果命令失败，检查错误信息并提供替代方案 4. **主动询问**：每次完成任务后，主动询问是否需要调整或继续 5. **数据统计**：从 `prepare-report` 的输出中获取统计数据 --- ## 安全说明 **所有操作都通过封装好的 CLI 工具执行** - 使用 `scripts/cli.py` 或 `python -m nowcoder.cli` 调用 - 所有操作都有确定的输入输出格式 - 统一的 JSON 输出格式便于解析 - 内置错误处理和输入验证

path: config/default_config.json
content: {
  "search_keywords": ["AI产品", "Agent产品"],
  "time_window_days": 3,
  "max_pages": 2,
  "tag": 818,
  "order": "create",
  "language": "zh",
  "max_results_per_keyword": 5,
  "request_delay": 2,
  "onboarding_complete": false,
  "user_preferences": {
    "report_style": "detailed",
    "focus_areas": ["面试问题", "项目经验", "技术栈"],
    "company_filter": [],
    "min_view_count": 0
  }
}


path: prompts/report_summary.md
content: # 面经报告生成提示词

你是一个专业的面试经验分析助手。现在有一组从牛客网收集的面试经验帖子，请生成一份结构化的面经报告。

## 数据概览

- **搜索关键词**: {keywords}
- **时间范围**: 过去 {time_window_days} 天
- **帖子总数**: {total_count} 篇
- **数据来源**: 牛客网

## 帖子数据

{posts_data}

---

## 报告要求

请按照以下结构生成面经报告：

### 1. 整体概览
- 涉及的主要公司/岗位

### 2. 逐篇详细经验总结

每篇面经以以下格式输出：
- **帖子标题，作者，时间**
- **关键问题**  #直接列出5-10个面试官的核心问题
- **经验提取**  #你从面经中概括出的经验
- **原文链接**
---

## 输出格式

- 使用 Markdown 格式
- 结构清晰，层级分明
- 重点内容加粗或使用列表
- 引用原文时标注来源
- 语言风格：专业、实用、简洁

{language_instruction}


path: scripts/cli.py
content: #!/usr/bin/env python
"""NowCoder CLI 入口脚本

使用方法:
  python scripts/cli.py <command> [args...]
  python -m nowcoder.cli <command> [args...]
"""

import sys
from pathlib import Path

# 添加项目根目录到路径
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))

# 导入并运行 CLI
from src.cli import main

if __name__ == "__main__":
    main()


path: src/analyzer.py
content: """分析模块

提供数据准备和报告生成功能。
"""

from typing import Any, Dict, List, Optional
from datetime import datetime

from .search import SearchRecord, SearchResult, get_post_detail, FeedDetail, DiscussDetail
from .utils import load_prompt, format_timestamp, get_tag_name


def prepare_summary(
    search_results: Dict[str, SearchResult],
    config: Dict[str, Any],
    fetch_details: bool = True
) -> Dict[str, Any]:
    """准备面经报告数据

    自动加载提示词模板并填充数据。

    Args:
        search_results: 搜索结果字典（关键词 -> SearchResult）
        config: 用户配置
        fetch_details: 是否获取帖子详情（正文内容）

    Returns:
        包含上下文、提示词和指令的字典
    """
    # 加载提示词模板
    prompt_template = load_prompt("report_summary")

    # 统计总帖子数
    total_count = sum(result.size for result in search_results.values())

    # 构建帖子数据
    posts_data = []
    for keyword, result in search_results.items():
        posts_data.append(f"\\n### 关键词: {keyword}\n")
        posts_data.append(f"找到 {result.size} 篇帖子（共 {result.total} 篇）\n")

        for idx, record in enumerate(result.records, 1):
            # 获取帖子详情
            detail = None
            if fetch_details:
                try:
                    detail = get_post_detail(record)
                except Exception:
                    pass

            post_info = format_post_record(record, idx, detail)
            posts_data.append(post_info)

    posts_text = "\n".join(posts_data)

    # 准备上下文数据
    context = {
        "keywords": ", ".join(search_results.keys()),
        "time_window_days": config.get("time_window_days", 7),
        "total_count": total_count,
        "search_results": {
            k: {
                "size": v.size,
                "total": v.total,
                "records": [
                    {
                        "title": r.title,
                        "rc_type": r.rc_type,
                        "uuid": r.uuid,
                        "content_id": r.content_id,
                        "created_at": r.created_at,
                        "view_count": r.view_count,
                        "like_count": r.like_count,
                        "comment_count": r.comment_count,
                        "company": r.company,
                        "job_title": r.job_title,
                    }
                    for r in v.records
                ]
            }
            for k, v in search_results.items()
        }
    }

    # 根据语言设置生成指令
    language = config.get("language", "zh")
    language_instruction = get_language_instruction(language)

    # 填充提示词
    prompt = prompt_template.format(
        keywords=context["keywords"],
        time_window_days=context["time_window_days"],
        total_count=total_count,
        posts_data=posts_text,
        language_instruction=language_instruction
    )

    return {
        "context": context,
        "prompt": prompt,
        "instruction": "请使用上述提示词和数据生成面经报告"
    }


def format_post_record(record: SearchRecord, index: int, detail: Optional[FeedDetail | DiscussDetail] = None) -> str:
    """格式化单条帖子记录

    Args:
        record: 搜索记录
        index: 序号
        detail: 帖子详情（可选）

    Returns:
        格式化的文本
    """
    lines = [
        f"\n**{index}. {record.title}**",
        f"- 类型: {'Feed动态' if record.rc_type == 201 else '讨论帖'}",
        f"- 发布时间: {format_timestamp(record.created_at)}",
        f"- 浏览/点赞/评论: {record.view_count} / {record.like_count} / {record.comment_count}",
    ]

    if record.company or record.job_title:
        auth_info = f"- 认证: {record.company}"
        if record.job_title:
            auth_info += f" - {record.job_title}"
        lines.append(auth_info)

    # 添加链接
    if record.rc_type == 201:
        lines.append(f"- 链接: https://www.nowcoder.com/feed/main/detail/{record.uuid}")
    else:
        lines.append(f"- 链接: https://www.nowcoder.com/discuss/{record.content_id}")

    # 添加正文内容
    if detail:
        content = detail.content if hasattr(detail, 'content') else ""
        if content:
            # 截断过长的内容（最多 5000 字符）
            if len(content) > 5000:
                content = content[:5000] + "\n...[内容已截断]"
            lines.append(f"\n**正文内容**:\n{content}")

    return "\n".join(lines)


def get_language_instruction(language: str) -> str:
    """获取语言设置指令

    Args:
        language: 语言代码

    Returns:
        语言指令文本
    """
    if language == "zh":
        return "\n**语言要求**: 请使用中文生成报告"
    elif language == "en":
        return "\n**语言要求**: Please generate the report in English"
    elif language == "bilingual":
        return "\n**语言要求**: 请生成双语报告（先中文后英文）"
    else:
        return "\n**语言要求**: 请使用中文生成报告"


def get_top_posts(
    search_results: Dict[str, SearchResult],
    top_n: int = 10,
    by: str = "view_count"
) -> List[SearchRecord]:
    """获取热门帖子

    Args:
        search_results: 搜索结果
        top_n: 返回数量
        by: 排序依据（view_count, like_count, comment_count）

    Returns:
        排序后的帖子列表
    """
    all_records = []
    for result in search_results.values():
        all_records.extend(result.records)

    # 排序
    if by == "view_count":
        all_records.sort(key=lambda r: r.view_count, reverse=True)
    elif by == "like_count":
        all_records.sort(key=lambda r: r.like_count, reverse=True)
    elif by == "comment_count":
        all_records.sort(key=lambda r: r.comment_count, reverse=True)

    return all_records[:top_n]


def calculate_statistics(search_results: Dict[str, SearchResult]) -> Dict[str, Any]:
    """计算统计数据

    Args:
        search_results: 搜索结果

    Returns:
        统计数据字典
    """
    all_records = []
    for result in search_results.values():
        all_records.extend(result.records)

    if not all_records:
        return {
            "total_posts": 0,
            "avg_view_count": 0,
            "avg_like_count": 0,
            "avg_comment_count": 0,
            "top_viewed": None,
            "top_liked": None,
        }

    # 计算平均值
    avg_view = sum(r.view_count for r in all_records) / len(all_records)
    avg_like = sum(r.like_count for r in all_records) / len(all_records)
    avg_comment = sum(r.comment_count for r in all_records) / len(all_records)

    # 找出最热门的帖子
    top_viewed = max(all_records, key=lambda r: r.view_count)
    top_liked = max(all_records, key=lambda r: r.like_count)

    return {
        "total_posts": len(all_records),
        "avg_view_count": int(avg_view),
        "avg_like_count": int(avg_like),
        "avg_comment_count": int(avg_comment),
        "top_viewed": {
            "title": top_viewed.title,
            "view_count": top_viewed.view_count,
        },
        "top_liked": {
            "title": top_liked.title,
            "like_count": top_liked.like_count,
        },
    }


path: src/cli.py
content: """统一 CLI 工具

封装所有命令行操作，提供安全的接口给 Agent 使用。
"""

import argparse
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional

# 添加父目录到路径以导入模块
sys.path.insert(0, str(Path(__file__).parent.parent))

from src.utils import (
    load_config,
    save_config,
    get_default_config,
    load_prompt,
    save_prompt,
    reset_prompt,
    USER_CONFIG_PATH,
    USER_PROMPTS_DIR,
    USER_CONFIG_DIR,
)
from src.search import search_posts, get_post_detail, SearchResult
from src.analyzer import prepare_summary


# ==================== 辅助函数 ====================

def json_output(data: Any) -> str:
    """输出 JSON 格式结果"""
    # 在 Windows 上设置 UTF-8 编码输出
    if sys.platform == "win32":
        sys.stdout.reconfigure(encoding='utf-8')
    return json.dumps(data, ensure_ascii=False, indent=2)


def error_output(message: str) -> str:
    """输出错误信息"""
    return json_output({"error": message})


def print_json(data: Any) -> None:
    """安全地打印 JSON 输出"""
    # 在 Windows 上设置 UTF-8 编码输出
    if sys.platform == "win32":
        sys.stdout.reconfigure(encoding='utf-8')
    print(json.dumps(data, ensure_ascii=False, indent=2))


# ==================== Onboarding 命令 ====================

def cmd_check_onboarding(args) -> Dict[str, Any]:
    """检查 onboarding 状态"""
    try:
        config = load_config()
        complete = config.get("onboarding_complete", False)
        return {"complete": bool(complete)}
    except Exception as e:
        return {"error": str(e), "complete": False}


def cmd_check_deps(args) -> Dict[str, Any]:
    """检查依赖是否安装"""
    missing = []
    all_installed = True

    # 检查 requests
    try:
        import requests
    except ImportError:
        missing.append("requests")
        all_installed = False

    # 检查 pydantic
    try:
        import pydantic
    except ImportError:
        missing.append("pydantic")
        all_installed = False

    return {
        "installed": all_installed,
        "missing": missing,
    }


def cmd_init_config(args) -> Dict[str, Any]:
    """初始化配置

    支持从 JSON 输入或参数读取配置
    """
    try:
        # 优先从 json_input 读取
        if args.json_input:
            config = json.loads(args.json_input)
        # 否则使用默认配置
        else:
            config = get_default_config()
            config["onboarding_complete"] = True

        save_config(config)
        return {"success": True, "config": config}
    except Exception as e:
        return {"error": str(e), "success": False}


# ==================== 配置管理命令 ====================

def cmd_get_config(args) -> Dict[str, Any]:
    """获取完整配置"""
    try:
        config = load_config()
        return {"config": config}
    except Exception as e:
        return {"error": str(e)}


def cmd_set_config(args) -> Dict[str, Any]:
    """设置单个配置项

    用法: set-config <key> <value>
    """
    try:
        if len(args.args) < 2:
            return {"error": "需要提供 key 和 value 参数", "success": False}

        key = args.args[0]
        value_str = args.args[1]

        # 尝试解析 JSON 值
        try:
            value = json.loads(value_str)
        except json.JSONDecodeError:
            # 保持为字符串
            value = value_str

        config = load_config()
        config[key] = value
        save_config(config)

        return {"success": True, "config": config}
    except Exception as e:
        return {"error": str(e), "success": False}


def cmd_update_config(args) -> Dict[str, Any]:
    """批量更新配置

    用法: update-config --json-input '{"key": "value"}'
    """
    try:
        if not args.json_input:
            return {"error": "需要提供 --json-input 参数", "success": False}

        updates = json.loads(args.json_input)
        config = load_config()
        config.update(updates)
        save_config(config)

        return {"success": True, "config": config}
    except Exception as e:
        return {"error": str(e), "success": False}


def cmd_reset_config(args) -> Dict[str, Any]:
    """重置为默认配置"""
    try:
        config = get_default_config()
        config["onboarding_complete"] = True  # 保持 onboarding 状态
        save_config(config)
        return {"success": True, "config": config}
    except Exception as e:
        return {"error": str(e), "success": False}


# ==================== 搜索相关命令 ====================

def cmd_search_posts(args) -> Dict[str, Any]:
    """搜索帖子

    使用当前配置搜索
    """
    try:
        config = load_config()

        # 执行搜索
        search_results = search_posts(
            keywords=config['search_keywords'],
            max_pages=config['max_pages'],
            tag=config['tag'],
            order=config['order'],
            delay=config['request_delay'],
            max_results=config.get('max_results_per_keyword', 5),
            time_window_days=config.get('time_window_days', 3)
        )

        # 统计信息
        total_count = sum(result.size for result in search_results.values())
        total_found = sum(result.total for result in search_results.values())

        # 序列化结果
        serialized_results = {}
        for keyword, result in search_results.items():
            serialized_results[keyword] = {
                "size": result.size,
                "total": result.total,
                "total_page": result.total_page,
                "records": [
                    {
                        "title": r.title,
                        "rc_type": r.rc_type,
                        "uuid": r.uuid,
                        "content_id": r.content_id,
                        "created_at": r.created_at,
                        "view_count": r.view_count,
                        "like_count": r.like_count,
                        "comment_count": r.comment_count,
                        "company": r.company,
                        "job_title": r.job_title,
                    }
                    for r in result.records
                ]
            }

        return {
            "results": serialized_results,
            "stats": {
                "total_count": total_count,
                "total_found": total_found,
                "keywords_count": len(search_results)
            }
        }
    except Exception as e:
        return {"error": str(e), "results": {}, "stats": {"total_count": 0}}


def cmd_get_post_details(args) -> Dict[str, Any]:
    """获取帖子详情

    用法: get-post-details --json-input '[{"title": "...", "rc_type": 201, "uuid": "..."}]'
    """
    try:
        if not args.json_input:
            return {"error": "需要提供 --json-input 参数", "details": {}}

        records = json.loads(args.json_input)
        if not isinstance(records, list):
            return {"error": "json_input 必须是数组", "details": {}}

        details = {}
        for record in records:
            try:
                # 重建 SearchRecord 对象
                from src.search import SearchRecord
                search_record = SearchRecord(**record)

                detail = get_post_detail(search_record)
                if detail:
                    record_key = search_record.uuid or search_record.content_id
                    details[record_key] = {
                        "title": detail.title,
                        "content": detail.content[:5000] if len(detail.content) > 5000 else detail.content,
                        "truncated": len(detail.content) > 5000,
                    }
            except Exception:
                continue

        return {"details": details}
    except Exception as e:
        return {"error": str(e), "details": {}}


# ==================== 报告准备命令 ====================

def cmd_prepare_report(args) -> Dict[str, Any]:
    """完整报告准备流程

    整合：加载配置 -> 搜索帖子 -> 获取详情 -> 准备提示词 -> 保存到文件

    返回轻量级元数据，完整数据保存到文件供 Agent 读取
    """
    try:
        config = load_config()

        # 搜索帖子
        search_results = search_posts(
            keywords=config['search_keywords'],
            max_pages=config['max_pages'],
            tag=config['tag'],
            order=config['order'],
            delay=config['request_delay'],
            max_results=config.get('max_results_per_keyword', 5),
            time_window_days=config.get('time_window_days', 3)
        )

        # 准备报告数据
        result = prepare_summary(search_results, config, fetch_details=True)

        # 保存到临时文件（.claude/temp 目录）
        from pathlib import Path
        temp_dir = Path(".claude/temp")
        temp_dir.mkdir(parents=True, exist_ok=True)
        output_file = temp_dir / "report_data.json"

        # 确保 UTF-8 编码写入
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(result, f, ensure_ascii=False, indent=2)

        # 返回轻量级元数据，完整数据在文件中
        return {
            "success": True,
            "output_file": str(output_file.absolute()),
            "metadata": {
                "total_count": result.get("context", {}).get("total_count", 0),
                "keywords": result.get("context", {}).get("keywords", ""),
                "time_window_days": result.get("context", {}).get("time_window_days", 0),
                "prompt_length": len(result.get("prompt", "")),
            }
        }
    except Exception as e:
        return {"error": str(e), "success": False}


# ==================== 提示词管理命令 ====================

def cmd_get_prompt(args) -> Dict[str, Any]:
    """获取提示词内容

    用法: get-prompt <name>
    """
    try:
        if not args.args:
            return {"error": "需要提供提示词名称", "content": ""}

        name = args.args[0]
        content = load_prompt(name)
        return {"content": content}
    except FileNotFoundError:
        return {"error": f"提示词不存在: {args.args[0]}", "content": ""}
    except Exception as e:
        return {"error": str(e), "content": ""}


def cmd_set_prompt(args) -> Dict[str, Any]:
    """设置提示词

    用法: set-prompt <name> <content>
         或: set-prompt <name> --json-input '{"content": "..."}'
    """
    try:
        if not args.args:
            return {"error": "需要提供提示词名称", "success": False}

        name = args.args[0]

        # 优先从 json_input 读取内容
        if args.json_input:
            data = json.loads(args.json_input)
            content = data.get("content", "")
        # 否则从参数读取
        elif len(args.args) >= 2:
            content = args.args[1]
        else:
            return {"error": "需要提供提示词内容", "success": False}

        save_prompt(name, content)
        return {"success": True}
    except Exception as e:
        return {"error": str(e), "success": False}


def cmd_reset_prompt(args) -> Dict[str, Any]:
    """重置提示词为默认

    用法: reset-prompt <name>
    """
    try:
        if not args.args:
            return {"error": "需要提供提示词名称", "success": False}

        name = args.args[0]
        success = reset_prompt(name)
        return {"success": success}
    except Exception as e:
        return {"error": str(e), "success": False}


# ==================== 主函数 ====================

def main():
    parser = argparse.ArgumentParser(
        description="NowCoder 面经搜索 CLI 工具",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument("command", help="命令名称")
    parser.add_argument("args", nargs="*", help="命令参数")
    parser.add_argument("--json-input", help="JSON 输入参数")

    args = parser.parse_args()

    # 命令映射
    COMMANDS = {
        "check-onboarding": cmd_check_onboarding,
        "check-deps": cmd_check_deps,
        "init-config": cmd_init_config,
        "get-config": cmd_get_config,
        "set-config": cmd_set_config,
        "update-config": cmd_update_config,
        "reset-config": cmd_reset_config,
        "search-posts": cmd_search_posts,
        "get-post-details": cmd_get_post_details,
        "prepare-report": cmd_prepare_report,
        "get-prompt": cmd_get_prompt,
        "set-prompt": cmd_set_prompt,
        "reset-prompt": cmd_reset_prompt,
    }

    if args.command not in COMMANDS:
        print_json({"error": f"未知命令: {args.command}"})
        sys.exit(1)

    # 执行命令
    try:
        result = COMMANDS[args.command](args)
        print_json(result)
    except Exception as e:
        print_json({"error": str(e)})
        sys.exit(1)


if __name__ == "__main__":
    main()


path: src/search.py
content: """搜索模块

提供牛客网内容搜索和详情获取功能。
"""

import re
import time
import ssl
from typing import Dict, List, Optional
from dataclasses import dataclass

import requests
import urllib3
from pydantic import BaseModel, Field

# ==================== OpenSSL 3.0 兼容性修复 ====================
# Python 3.13 使用 OpenSSL 3.0，默认 SECLevel=2 过于严格
# 降低安全级别以兼容某些旧服务器

# 全局设置 SSL 安全级别
try:
    import os
    os.environ['OPENSSL_CONF'] = ''
except Exception:
    pass

# 禁用 SSL 验证警告（仅用于调试）
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 创建自定义 SSL Adapter
class SSLAdapter(requests.adapters.HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        # 创建兼容 OpenSSL 3.0 的 SSL 上下文
        context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
        # 降低安全级别
        try:
            context.set_ciphers('DEFAULT@SECLEVEL=0')
        except Exception:
            context.set_ciphers('ALL')
        # 不验证证书
        context.check_hostname = False
        context.verify_mode = ssl.CERT_NONE
        kwargs['ssl_context'] = context
        kwargs['assert_hostname'] = False
        return super().init_poolmanager(*args, **kwargs)

# 使用自定义 Session
session = requests.Session()
session.mount('https://', SSLAdapter())


# ==================== 数据模型 ====================


class SearchRecord(BaseModel):
    """搜索记录（搜索结果）"""

    title: str = Field(description="文章标题")
    rc_type: int = Field(
        description="内容类型：201=Feed（动态），207=讨论帖"
    )
    uuid: str = Field(default="", description="文章UUID（rc_type=201时使用）")
    content_id: str = Field(default="", description="内容ID（rc_type=207时使用）")
    created_at: int = Field(default=0, description="创建时间戳（毫秒）")
    edit_time: int = Field(default=0, description="修改时间戳（毫秒）")
    view_count: int = Field(default=0, description="浏览量")
    like_count: int = Field(default=0, description="点赞数")
    comment_count: int = Field(default=0, description="评论数")
    company: str = Field(default="", description="认证公司名称")
    job_title: str = Field(default="", description="认证岗位名称")


class FeedDetail(BaseModel):
    """Feed详情（动态类型）"""

    title: str = Field(description="文章标题")
    content: str = Field(description="文章完整内容")
    uuid: str = Field(default="", description="文章UUID")
    url: str = Field(default="", description="文章访问链接")


class DiscussDetail(BaseModel):
    """讨论帖详情（rcType=207）"""

    title: str = Field(description="帖子标题")
    content: str = Field(description="帖子完整内容")
    content_id: str = Field(default="", description="内容ID")
    url: str = Field(default="", description="帖子访问链接")


class SearchResult(BaseModel):
    """搜索结果"""

    current: int = Field(description="当前页码")
    size: int = Field(description="当页文章数量")
    total: int = Field(description="文章总数")
    total_page: int = Field(description="总页数")
    records: List[SearchRecord] = Field(description="搜索结果列表")


# ==================== API 配置 ====================

NOWCODER_API_URL = "https://gw-c.nowcoder.com/api/sparta/pc/search"
NOWCODER_DETAIL_URL = "https://www.nowcoder.com/feed/main/detail"
NOWCODER_DISCUSS_API_URL = "https://gw-c.nowcoder.com/api/sparta/detail/content-data/detail"
NOWCODER_DISCUSS_URL = "https://www.nowcoder.com/discuss"
REQUEST_TIMEOUT = 30

TAG_OPTIONS = {
    818: "面经",
    861: "求职进度",
    823: "内推",
    856: "公司评价",
}

VALID_TAG_IDS = list(TAG_OPTIONS.keys())
VALID_ORDER_VALUES = ["", "create"]


# ==================== 工具函数 ====================


def html_to_text(html: str) -> str:
    """将 HTML 内容转换为纯文本

    Args:
        html: HTML 格式的内容

    Returns:
        纯文本内容
    """
    if not html:
        return ""

    # 移除 script 和 style 标签及其内容
    html = re.sub(
        r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE
    )
    html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE)

    # 将常见的块级标签转换为换行
    html = re.sub(r"</?(p|div|br|h[1-6]|li|tr)[^>]*>", "\n", html, flags=re.IGNORECASE)

    # 移除所有其他 HTML 标签
    html = re.sub(r"<[^>]+>", "", html)

    # 处理 HTML 实体
    html = html.replace("&nbsp;", " ")
    html = html.replace("&amp;", "&")
    html = html.replace("&lt;", "<")
    html = html.replace("&gt;", ">")
    html = html.replace("&quot;", '"')
    html = html.replace("&#39;", "'")
    html = html.replace("\xa0", " ")

    # 合并多个连续换行为最多两个
    html = re.sub(r"\n\s*\n", "\n\n", html)

    # 去除首尾空白
    return html.strip()


def search_nowcoder_api(
    query: str,
    page: int = 1,
    tag: Optional[int] = None,
    order: str = "",
) -> dict:
    """调用牛客网搜索 API

    Args:
        query: 搜索关键词
        page: 页码，从1开始
        tag: 标签筛选 ID
        order: 排序方式

    Returns:
        API 响应数据
    """
    # 构建 tag 参数
    tag_list = []
    if tag and tag in TAG_OPTIONS:
        tag_list = [{"name": TAG_OPTIONS[tag], "id": tag, "count": None}]

    payload = {
        "type": "all",
        "query": query,
        "page": page,
        "tag": tag_list,
        "order": order,
        "gioParams": {
            "searchFrom_var": "顶部导航栏",
            "searchEnter_var": "主站",
        },
    }

    headers = {
        "Content-Type": "application/json; charset=UTF-8",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    }

    response = session.post(
        NOWCODER_API_URL, json=payload, headers=headers, timeout=REQUEST_TIMEOUT, verify=False
    )
    response.raise_for_status()
    return response.json()


def parse_search_response(response_data: dict, time_window_days: Optional[int] = None) -> SearchResult:
    """解析搜索响应数据

    Args:
        response_data: API 响应数据
        time_window_days: 时间窗口（天），用于过滤结果

    Returns:
        解析后的搜索结果
    """
    data = response_data.get("data", {})
    records_raw = data.get("records", [])

    records = []
    for record in records_raw:
        rc_type = record.get("rc_type", 0)
        record_data = record.get("data", {})

        # 跳过 data 不是 dict 的记录（如职位推荐 rc_type=213）
        if not isinstance(record_data, dict):
            continue

        user_brief = record_data.get("userBrief", {})
        frequency_data = record_data.get("frequencyData", {})

        # 提取认证信息
        identity_list = user_brief.get("identityList") or []
        company = ""
        job_title = ""
        if identity_list and len(identity_list) > 0:
            first_identity = identity_list[0]
            company = first_identity.get("companyName", "")
            job_title = first_identity.get("jobName", "")

        # 根据 rc_type 处理不同类型
        if rc_type == 201:
            # Feed类型
            moment_data = record_data.get("momentData", {})
            if not moment_data:
                continue
            records.append(
                SearchRecord(
                    title=moment_data.get("title", ""),
                    rc_type=rc_type,
                    uuid=moment_data.get("uuid", ""),
                    content_id="",
                    created_at=moment_data.get("createdAt", 0),
                    edit_time=moment_data.get("editTime", 0),
                    view_count=frequency_data.get("viewCnt", 0),
                    like_count=frequency_data.get("likeCnt", 0),
                    comment_count=frequency_data.get("commentCnt", 0),
                    company=company,
                    job_title=job_title,
                )
            )
        elif rc_type == 207:
            # 讨论帖类型
            content_data = record_data.get("contentData", {})
            if not content_data:
                continue
            records.append(
                SearchRecord(
                    title=content_data.get("title", ""),
                    rc_type=rc_type,
                    uuid="",
                    content_id=str(content_data.get("id", "")),
                    created_at=content_data.get("createTime", 0),
                    edit_time=content_data.get("editTime", 0),
                    view_count=frequency_data.get("viewCnt", 0),
                    like_count=frequency_data.get("likeCnt", 0),
                    comment_count=frequency_data.get("commentCnt", 0),
                    company=company,
                    job_title=job_title,
                )
            )

    # 应用时间窗口过滤
    if time_window_days and time_window_days > 0:
        from datetime import datetime, timedelta
        cutoff_time = int((datetime.now() - timedelta(days=time_window_days)).timestamp() * 1000)
        records = [r for r in records if r.created_at >= cutoff_time]

    return SearchResult(
        current=data.get("current", 1),
        size=data.get("size", 0),
        total=data.get("total", 0),
        total_page=data.get("totalPage", 0),
        records=records,
    )


def get_feed_detail_from_page(uuid: str) -> FeedDetail:
    """从网页获取Feed详情

    Args:
        uuid: 文章UUID

    Returns:
        Feed详情
    """
    if not uuid:
        raise ValueError("需要提供 uuid 参数")

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    }

    url = f"{NOWCODER_DETAIL_URL}/{uuid}"

    response = session.get(url, headers=headers, timeout=REQUEST_TIMEOUT, verify=False)
    response.raise_for_status()
    html = response.text

    # 检查内容是否存在
    if "内容不存在!" in html:
        raise ValueError(
            f"内容不存在! 请检查输入的uuid是否正确。"
            f"注意：此工具仅适用于Feed类型（动态），如果是讨论帖类型（rcType=207），请使用 get_discuss_details 工具。"
        )

    # 提取 title
    title_match = re.search(r'"title":"([^"]+)"', html)
    title = title_match.group(1) if title_match else ""

    # 提取正文
    content = ""

    # 直接从页面中的 feed-content-text 区块提取
    content_match = re.search(
        r'<div[^>]*class="[^"]*feed-content-text[^"]*"[^>]*>(.*?)</div>',
        html,
        flags=re.DOTALL | re.IGNORECASE,
    )
    if content_match:
        content_html = content_match.group(1)
        content = html_to_text(content_html)

    # 回退到 JSON 片段提取
    if not content:
        content_matches = re.findall(r'"content":"([^"]+)"', html)
        for match in content_matches:
            if len(match) > 100 and "..." not in match:
                content = match
                break
        if content:
            content = (
                content.replace("\\n", "\n")
                .replace("\\u002F", "/")
                .replace("\\t", "\t")
            )

    return FeedDetail(
        title=title,
        content=content,
        uuid=uuid,
        url=url,
    )


def get_discuss_detail_from_api(content_id: str) -> DiscussDetail:
    """从API获取讨论帖详情

    Args:
        content_id: 内容ID

    Returns:
        讨论帖详情
    """
    if not content_id:
        raise ValueError("需要提供 content_id 参数")

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    }

    api_url = f"{NOWCODER_DISCUSS_API_URL}/{content_id}"
    page_url = f"{NOWCODER_DISCUSS_URL}/{content_id}"

    response = requests.get(api_url, headers=headers, timeout=REQUEST_TIMEOUT)
    response.raise_for_status()
    data = response.json()

    # 检查API响应是否成功
    if not data.get("success"):
        raise ValueError(
            f"内容不存在或请求失败! 请检查输入的content_id是否正确。"
            f"注意：此工具仅适用于讨论帖类型（rcType=207）。"
        )

    content_data = data.get("data", {})

    # 将HTML内容转换为纯文本
    rich_text = content_data.get("richText", "") or content_data.get("content", "")
    plain_content = html_to_text(rich_text)

    return DiscussDetail(
        title=content_data.get("title", ""),
        content=plain_content,
        content_id=content_id,
        url=page_url,
    )


# ==================== 高级搜索函数 ====================


def search_posts(
    keywords: List[str],
    max_pages: int = 1,
    tag: Optional[int] = None,
    order: str = "create",
    delay: float = 2.0,
    max_results: int = 10,
    time_window_days: Optional[int] = None,
) -> Dict[str, SearchResult]:
    """搜索多个关键词的帖子

    Args:
        keywords: 关键词列表
        max_pages: 每个关键词最大获取页数
        tag: 标签筛选ID
        order: 排序方式
        delay: 请求延迟（秒）
        max_results: 每个关键词最大返回结果数，默认为10
        time_window_days: 时间窗口（天），用于过滤结果

    Returns:
        字典，键为关键词，值为搜索结果
    """
    results = {}

    # 校验参数
    if tag is not None and tag not in VALID_TAG_IDS:
        raise ValueError(f"无效的标签ID，可选值：{VALID_TAG_IDS}")
    if order not in VALID_ORDER_VALUES:
        raise ValueError(f"无效的排序方式，可选值：{VALID_ORDER_VALUES}")

    for idx, keyword in enumerate(keywords):
        try:
            # 添加延迟（第一个请求不需要）
            if idx > 0 and delay > 0:
                time.sleep(delay)

            # 先获取第一页
            first_response = search_nowcoder_api(keyword, page=1, tag=tag, order=order)
            if not first_response.get("success"):
                results[keyword] = SearchResult(
                    current=1, size=0, total=0, total_page=0, records=[]
                )
                continue

            first_result = parse_search_response(first_response, time_window_days)
            total_page = first_result.total_page

            # 收集所有记录
            all_records = list(first_result.records)
            seen_ids = set()
            for record in all_records:
                if record.uuid:
                    seen_ids.add(record.uuid)
                elif record.content_id:
                    seen_ids.add(record.content_id)

            # 如果已经达到 max_results，直接返回
            if len(all_records) >= max_results:
                results[keyword] = SearchResult(
                    current=1,
                    size=max_results,
                    total=first_result.total,
                    total_page=total_page,
                    records=all_records[:max_results],
                )
                continue

            # 确定需要获取的页数
            if max_pages <= 0:
                pages_to_fetch = total_page
            else:
                pages_to_fetch = min(max_pages, total_page)

            # 获取剩余页面，直到达到 max_results
            for page in range(2, pages_to_fetch + 1):
                try:
                    if delay > 0:
                        time.sleep(delay)

                    response_data = search_nowcoder_api(keyword, page=page, tag=tag, order=order)
                    if response_data.get("success"):
                        page_result = parse_search_response(response_data, time_window_days)
                        for record in page_result.records:
                            record_id = record.uuid if record.uuid else record.content_id
                            if record_id and record_id not in seen_ids:
                                seen_ids.add(record_id)
                                all_records.append(record)
                                # 达到 max_results 时停止
                                if len(all_records) >= max_results:
                                    break
                        if len(all_records) >= max_results:
                            break
                except Exception:
                    continue

            results[keyword] = SearchResult(
                current=1,
                size=min(len(all_records), max_results),
                total=first_result.total,
                total_page=total_page,
                records=all_records[:max_results],
            )

        except Exception as e:
            results[keyword] = SearchResult(
                current=1, size=0, total=0, total_page=0, records=[]
            )

    return results


def get_post_detail(record: SearchRecord) -> Optional[FeedDetail | DiscussDetail]:
    """获取帖子详情

    Args:
        record: 搜索记录

    Returns:
        帖子详情，失败返回 None
    """
    try:
        if record.rc_type == 201:
            return get_feed_detail_from_page(uuid=record.uuid)
        elif record.rc_type == 207:
            return get_discuss_detail_from_api(content_id=record.content_id)
    except Exception:
        return None

    return None


path: src/utils.py
content: """工具函数模块

提供配置管理和提示词管理功能。
"""

import json
import os
from pathlib import Path
from typing import Any, Dict, Optional


# 默认配置路径
DEFAULT_CONFIG_PATH = Path(__file__).parent.parent / "config" / "default_config.json"
USER_CONFIG_DIR = Path.home() / ".nowcoder-search"
USER_CONFIG_PATH = USER_CONFIG_DIR / "config.json"
USER_PROMPTS_DIR = USER_CONFIG_DIR / "prompts"
DEFAULT_PROMPTS_DIR = Path(__file__).parent.parent / "prompts"


def get_default_config() -> Dict[str, Any]:
    """获取默认配置

    Returns:
        默认配置字典
    """
    with open(DEFAULT_CONFIG_PATH, "r", encoding="utf-8") as f:
        return json.load(f)


def load_config() -> Dict[str, Any]:
    """加载用户配置，如果不存在则返回默认配置

    Returns:
        配置字典
    """
    # 如果用户配置存在，优先使用
    if USER_CONFIG_PATH.exists():
        try:
            with open(USER_CONFIG_PATH, "r", encoding="utf-8") as f:
                return json.load(f)
        except Exception:
            # 配置文件损坏，返回默认配置
            return get_default_config()

    # 否则返回默认配置
    return get_default_config()


def save_config(config: Dict[str, Any]) -> None:
    """保存配置到用户目录

    Args:
        config: 配置字典
    """
    # 确保目录存在
    USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True)

    # 保存配置
    with open(USER_CONFIG_PATH, "w", encoding="utf-8") as f:
        json.dump(config, f, ensure_ascii=False, indent=2)


def load_prompt(prompt_name: str) -> str:
    """加载提示词模板

    优先从用户目录加载，如果不存在则使用默认模板。

    Args:
        prompt_name: 提示词名称（不含扩展名），如 'report_summary'

    Returns:
        提示词内容
    """
    # 检查用户自定义提示词
    user_prompt_path = USER_PROMPTS_DIR / f"{prompt_name}.md"
    if user_prompt_path.exists():
        with open(user_prompt_path, "r", encoding="utf-8") as f:
            return f.read()

    # 使用默认提示词
    default_prompt_path = DEFAULT_PROMPTS_DIR / f"{prompt_name}.md"
    if default_prompt_path.exists():
        with open(default_prompt_path, "r", encoding="utf-8") as f:
            return f.read()

    raise FileNotFoundError(f"提示词模板不存在: {prompt_name}")


def save_prompt(prompt_name: str, content: str) -> None:
    """保存自定义提示词到用户目录

    Args:
        prompt_name: 提示词名称（不含扩展名）
        content: 提示词内容
    """
    # 确保目录存在
    USER_PROMPTS_DIR.mkdir(parents=True, exist_ok=True)

    # 保存提示词
    prompt_path = USER_PROMPTS_DIR / f"{prompt_name}.md"
    with open(prompt_path, "w", encoding="utf-8") as f:
        f.write(content)


def reset_prompt(prompt_name: str) -> bool:
    """重置提示词为默认版本

    Args:
        prompt_name: 提示词名称

    Returns:
        是否成功重置
    """
    user_prompt_path = USER_PROMPTS_DIR / f"{prompt_name}.md"
    if user_prompt_path.exists():
        user_prompt_path.unlink()
        return True
    return False


def format_timestamp(timestamp_ms: int) -> str:
    """格式化时间戳为可读字符串

    Args:
        timestamp_ms: 毫秒级时间戳

    Returns:
        格式化的时间字符串
    """
    from datetime import datetime

    if timestamp_ms == 0:
        return "未知"

    dt = datetime.fromtimestamp(timestamp_ms / 1000)
    return dt.strftime("%Y-%m-%d %H:%M")


def get_tag_name(tag_id: Optional[int]) -> str:
    """获取标签名称

    Args:
        tag_id: 标签ID

    Returns:
        标签名称
    """
    tag_map = {
        818: "面经",
        861: "求职进度",
        823: "内推",
        856: "公司评价",
        None: "全部"
    }
    return tag_map.get(tag_id, "未知")


def get_order_name(order: str) -> str:
    """获取排序方式名称

    Args:
        order: 排序方式

    Returns:
        排序方式名称
    """
    order_map = {
        "": "默认排序",
        "create": "按时间排序"
    }
    return order_map.get(order, "未知")


path: src/__init__.py
content: """NowCoder 面经搜索 Skill - 核心模块"""

__version__ = "0.1.0"


# 支持 python -m nowcoder.cli
if __name__ == "__main__":
    from .cli import main
    main()


path: README.md
content: # NowCoder 面经搜索工具

[![Python](https://img.shields.io/badge/Python-3.12+-blue.svg)](https://www.python.org/)
[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

一个强大的牛客网面试经验搜索和分析工具，作为 Claude Code Skill 运行，提供完整的自动化工作流。

## 核心功能

### 🎯 主要特性
- 🚀 **完整 Onboarding** - 首次使用引导配置
- 📝 **自动生成报告** - 结构化面经报告，包含面试题汇总、经验总结、趋势分析
- ⚙️ **配置管理** - 动态调整搜索关键词、时间窗口、报告风格
- 💡 **智能推荐** - 自动筛选高价值内容

## 快速开始

### 前置要求

- Python 3.12+
- [uv](https://github.com/astral-sh/uv)（推荐）或 pip

### 安装

#### 使用 uv 安装（推荐）

```bash
# 克隆项目
git clone https://github.com/Infinityay/nowcoder-mcp.git
cd nowcoder-mcp

# 安装依赖
uv sync
```

#### 使用 pip 安装

```bash
# 克隆项目
git clone https://github.com/Infinityay/nowcoder-mcp.git
cd nowcoder-mcp

# 创建虚拟环境
python -m venv .venv
source .venv/bin/activate  # Linux/macOS
# .venv\Scripts\activate  # Windows

# 安装依赖
pip install -r requirements.txt
```

### 使用方法

在 Claude Desktop 或其他支持 Skill 的 AI 助手中：

1. 将 `SKILL.md` 文件加载到对话中
2. 首次使用会自动运行 Onboarding 流程
3. 配置您的偏好设置
4. 自动生成第一个面经报告

详细说明请查看 [SKILL.md](SKILL.md)。

## 项目结构

```
nowcoder-search/
├── SKILL.md                    # Agent Skill 主文档
├── config/
│   └── default_config.json     # 默认配置
├── prompts/
│   └── report_summary.md       # 面经报告提示词模板
├── src/
│   ├── __init__.py
│   ├── utils.py                # 工具函数（配置、提示词管理）
│   ├── search.py               # 搜索功能模块
│   └── analyzer.py             # 分析和报告准备模块
└── README.md                   # 本文件
```

## 功能说明

### 1. 面经报告生成

根据配置的关键词、时间窗口等参数，自动搜索并生成结构化面经报告，包括：
- 整体概览
- 重点推荐（3-5篇）
- 技术面试题汇总
- 经验总结
- 趋势观察
- 行动建议

### 2. 配置管理

通过对话动态调整：
- 搜索关键词
- 时间窗口
- 标签筛选
- 报告语言（中文/英文/双语）
- 报告风格

## 配置参数

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `search_keywords` | 搜索关键词列表 | `["Java后端", "秋招"]` |
| `time_window_days` | 时间窗口（天） | `7` |
| `max_pages` | 每个关键词最大页数 | `5` |
| `tag` | 标签筛选ID | `818`（面经） |
| `order` | 排序方式 | `"create"`（按时间） |
| `language` | 报告语言 | `"zh"`（中文） |

### 标签筛选选项

| tag ID | 含义 |
|--------|------|
| `818` | 面经 |
| `861` | 求职进度 |
| `823` | 内推 |
| `856` | 公司评价 |
| `null` | 不筛选（全部） |

## 配置路径

- 用户配置: `~/.nowcoder-search/config.json`
- 用户提示词: `~/.nowcoder-search/prompts/`
- 报告输出: `~/.nowcoder-search/reports/`
- 默认配置: `config/default_config.json`
- 默认提示词: `prompts/`

## 性能考虑

- 搜索时添加 2 秒延迟，避免请求过快
- 支持多关键词并行搜索
- 支持分页获取，避免一次性加载过多数据

## 内容类型说明

牛客网的内容分为两种类型：

| 类型 | rc_type | 标识字段 | 详情页 URL |
|-----|---------|---------|-----------|
| **Feed 动态** | 201 | `uuid` | `nowcoder.com/feed/main/detail/{uuid}` |
| **讨论帖** | 207 | `content_id` | `nowcoder.com/discuss/{content_id}` |

## 常见使用场景

### 场景 1: 准备特定公司的面试

```
用户: "我下周要面试字节跳动的后端岗位，帮我看看面经"

助手会:
1. 检查配置中的关键词是否包含"字节跳动"
2. 如果不包含，临时搜索或更新配置
3. 搜索并生成报告
4. 筛选出字节跳动相关的帖子
5. 重点推荐和分析
```

### 场景 2: 快速浏览最新面经

```
用户: "给我看看最近的面经"

助手会:
1. 使用当前配置直接搜索
2. 生成报告
3. 按时间排序展示
4. 快速概览
```

### 场景 3: 针对性学习

```
用户: "我想重点准备算法题"

助手会:
1. 修改配置，添加"算法"关键词
2. 搜索并生成报告
3. 重点提取算法面试题
4. 提供练习建议
```

## 注意事项

- 首次使用会运行 Onboarding 流程
- 配置保存在 `~/.nowcoder-search/config.json`
- 自定义提示词保存在 `~/.nowcoder-search/prompts/`
- 报告可导出到 `~/.nowcoder-search/reports/`

## 技术栈

- **Python 3.12+**: 核心语言
- **requests**: HTTP 请求
- **pydantic**: 数据验证

## License

MIT License

## 贡献

欢迎提交 Issue 和 Pull Request！

---


path: requirements.txt
content: requests>=2.31.0
pydantic>=2.0.0


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