# src — Python 文件汇总

> **生成时间**：2026-08-05 02:34:28 UTC  
> **根目录**：`C:\Users\s84336076\IDEProjects\recruiter-navi\src`

## 路径：`recruiter_navigator\__init__.py`

```python
"""Recruiter Tool Navigator — 51 个招聘工具的统一导航入口。"""

__version__ = "0.1.0"

```

---

## 路径：`recruiter_navigator\cli\__init__.py`

```python


```

---

## 路径：`recruiter_navigator\cli\app.py`

```python
"""Typer 主应用 — recruiter 命令行入口。"""

from __future__ import annotations

import typer
from rich.console import Console

from .init import init_command
from .list import list_command
from .use import use_command
from .web import web_command

console = Console()

app = typer.Typer(
    name="recruiter",
    help="Recruiter Tool Navigator — 47 个招聘工具的统一导航入口",
    add_completion=False,
    rich_markup_mode="rich",
)


@app.command("init")
def init_cmd(
    force: bool = typer.Option(False, "--force", "-f", help="覆盖已存在的配置文件"),
    output: str | None = typer.Option(None, "--output", "-o", help="输出目录（默认当前目录）"),
) -> None:
    """初始化本地配置文件，将内置 registry.yaml 复制到当前目录供自定义编辑。"""
    init_command(force=force, output=output)


@app.command("list")
def list_cmd(
    category: str | None = typer.Option(
        None, "--category", "-c", help="按分类筛选: search|resume|outreach|data|infra"
    ),
    keyword: str | None = typer.Option(
        None, "--keyword", "-k", help="关键词搜索"
    ),
    config: str | None = typer.Option(
        None, "--config", help="自定义 registry.yaml 路径"
    ),
) -> None:
    """列出所有可用的招聘工具。"""
    list_command(category=category, keyword=keyword, config=config)


@app.command("use")
def use_cmd(
    tool_name: str = typer.Argument(..., help="工具名称"),
    args: list[str] | None = typer.Argument(None, help="传递给工具的参数"),
) -> None:
    """启动指定的招聘工具。"""
    use_command(tool_name=tool_name, args=args)


@app.command("web")
def web_cmd(
    port: int = typer.Option(8080, "--port", "-p", help="Web 服务端口"),
    open_browser: bool = typer.Option(False, "--open", "-o", help="启动后自动打开浏览器"),
    config: str | None = typer.Option(
        None, "--config", help="自定义 registry.yaml 路径"
    ),
) -> None:
    """启动 Web 可视化面板。"""
    web_command(port=port, open_browser=open_browser, config=config)


if __name__ == "__main__":
    app()

```

---

## 路径：`recruiter_navigator\cli\init.py`

```python
"""recruiter init — 初始化本地配置文件。"""

from __future__ import annotations

import shutil
from pathlib import Path

from rich.console import Console
from rich.panel import Panel
from rich.syntax import Syntax

console = Console()

# 默认本地配置文件名
LOCAL_REGISTRY_FILENAME = "registry.yaml"


def init_command(
    force: bool = False,
    output: str | None = None,
) -> None:
    """将内置 registry.yaml 复制到当前目录，供自定义编辑。"""
    # 内置 registry.yaml 路径
    builtin_path = Path(__file__).resolve().parent.parent / "data" / "registry.yaml"

    if not builtin_path.exists():
        console.print("[red]错误: 找不到内置 registry.yaml[/red]")
        raise SystemExit(1)

    # 目标路径
    dest_dir = Path(output).resolve() if output else Path.cwd()
    dest_path = dest_dir / LOCAL_REGISTRY_FILENAME

    # 检查目标文件是否已存在
    if dest_path.exists() and not force:
        console.print(
            f"[yellow]文件已存在: {dest_path}[/yellow]\n"
            f"使用 [bold]recruiter init --force[/bold] 覆盖，"
            f"或手动编辑现有文件。"
        )
        raise SystemExit(1)

    # 确保目标目录存在
    dest_dir.mkdir(parents=True, exist_ok=True)

    # 复制文件
    shutil.copy2(builtin_path, dest_path)

    # 输出结果
    console.print()
    console.print(Panel(
        f"[green]✓ 已初始化本地配置文件[/green]\n\n"
        f"  路径: [bold cyan]{dest_path}[/bold cyan]\n\n"
        f"  下一步:\n"
        f"  1. 编辑 {LOCAL_REGISTRY_FILENAME} 添加/修改工具\n"
        f"  2. 运行 [bold]recruiter web --config {dest_path}[/bold] 启动 Web 面板\n"
        f"  3. 运行 [bold]recruiter list --config {dest_path}[/bold] 查看工具列表",
        title="🎉 初始化完成",
        border_style="green",
    ))
    console.print()

    # 显示文件前几行预览
    preview_lines = dest_path.read_text(encoding="utf-8").splitlines()[:8]
    preview = "\n".join(preview_lines)
    syntax = Syntax(preview, "yaml", theme="monokai", line_numbers=True)
    console.print(Panel(syntax, title=f"{LOCAL_REGISTRY_FILENAME} 预览", border_style="dim"))

```

---

## 路径：`recruiter_navigator\cli\list.py`

```python
"""recruiter list — 列出所有工具。"""

from __future__ import annotations

import sys

import typer
from rich.console import Console
from rich.table import Table

from ..core.models import Category
from ..core.registry import get_all_tools, get_tools_by_category, search_tools

# Windows 终端兼容：强制 UTF-8 输出
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]

console = Console()

# 分类显示名映射
_CATEGORY_LABELS: dict[Category, str] = {
    Category.SEARCH: "人才发现",
    Category.RESUME: "简历处理",
    Category.OUTREACH: "候选人触达",
    Category.DATA: "数据管理",
    Category.INFRA: "基础设施",
}


def list_command(
    category: str | None = None,
    keyword: str | None = None,
    config: str | None = None,
) -> None:
    """列出所有可用的招聘工具。"""
    # 如果指定了自定义配置
    if config:
        from pathlib import Path
        from ..core.registry import set_registry_path
        config_path = Path(config).resolve()
        if not config_path.exists():
            console.print(f"[red]错误: 配置文件不存在: {config_path}[/red]")
            raise SystemExit(1)
        set_registry_path(config_path)
        console.print(f"[cyan]使用自定义配置: {config_path}[/cyan]\n")
    # 按分类筛选
    if category:
        try:
            cat = Category(category)
        except ValueError:
            console.print(f"[red]无效分类: {category}[/red]")
            console.print("可选分类: search, resume, outreach, data, infra")
            raise typer.Exit(code=1)
        tools = get_tools_by_category(cat)
    # 按关键词搜索
    elif keyword:
        tools = search_tools(keyword)
        if not tools:
            console.print(f"[yellow]未找到与 '{keyword}' 相关的工具[/yellow]")
            return
    # 全部列出
    else:
        tools = get_all_tools()

    if not tools:
        console.print("[yellow]没有找到匹配的工具[/yellow]")
        return

    # 构建表格
    table = Table(
        title="Recruiter Tool Navigator — 工具列表",
        show_lines=True,
        header_style="bold magenta",
    )
    table.add_column("#", style="dim", width=4, justify="right")
    table.add_column("工具名", style="cyan", min_width=20)
    table.add_column("分类", style="green", min_width=12)
    table.add_column("模式", style="yellow", width=10)
    table.add_column("描述", style="white", min_width=30)

    for idx, tool in enumerate(tools, 1):
        cat_label = _CATEGORY_LABELS.get(tool.category, tool.category.value)
        mode_label = {
            "cli": "CLI",
            "web": "Web",
            "interactive": "交互",
        }.get(tool.launch.mode.value, tool.launch.mode.value)
        table.add_row(
            str(idx),
            tool.name,
            cat_label,
            mode_label,
            tool.description,
        )

    console.print(table)
    console.print(f"\n共 [bold]{len(tools)}[/bold] 个工具")

```

---

## 路径：`recruiter_navigator\cli\use.py`

```python
"""recruiter use — 一键启动工具。"""

from __future__ import annotations

import sys
from typing import Optional

import typer
from rich.console import Console

from ..core.launcher import launch_tool
from ..core.registry import get_tool

# Windows 终端兼容：强制 UTF-8 输出
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]

console = Console()


def use_command(
    tool_name: str = typer.Argument(..., help="工具名称"),
    args: Optional[list[str]] = typer.Argument(None, help="传递给工具的参数"),
) -> None:
    """启动指定的招聘工具。"""
    tool = get_tool(tool_name)
    if tool is None:
        console.print(f"[red]未找到工具: {tool_name}[/red]")
        console.print("使用 [bold]recruiter list[/bold] 查看所有可用工具")
        raise typer.Exit(code=1)

    console.print(f"[green]正在启动: {tool.display_name}[/green]")
    console.print(f"  描述: {tool.description}")
    console.print(f"  模式: {tool.launch.mode.value}")
    console.print()

    launch_tool(tool, args or [])

```

---

## 路径：`recruiter_navigator\cli\web.py`

```python
"""recruiter web — 启动 Web 可视化面板。"""

from __future__ import annotations

import sys

import typer
from rich.console import Console

# Windows 终端兼容：强制 UTF-8 输出
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]

console = Console()


def web_command(
    port: int = 8080,
    open_browser: bool = False,
    config: str | None = None,
) -> None:
    """启动 Web 可视化面板。

    Args:
        port: Web 服务端口。
        open_browser: 启动后自动打开浏览器。
        config: 自定义 registry.yaml 路径，不指定则使用内置默认配置。
    """
    import uvicorn

    # 如果指定了自定义配置，设置到 registry 模块
    if config:
        from recruiter_navigator.core.registry import set_registry_path
        config_path = __import__("pathlib").Path(config).resolve()
        if not config_path.exists():
            console.print(f"[red]错误: 配置文件不存在: {config_path}[/red]")
            raise SystemExit(1)
        set_registry_path(config_path)
        console.print(f"[cyan]使用自定义配置: {config_path}[/cyan]")

    if open_browser:
        import webbrowser
        webbrowser.open(f"http://localhost:{port}")

    console.print(f"[green]正在启动 Web 面板: http://localhost:{port}[/green]")
    uvicorn.run(
        "recruiter_navigator.web.app:create_app",
        factory=True,
        host="127.0.0.1",
        port=port,
    )

```

---

## 路径：`recruiter_navigator\core\__init__.py`

```python


```

---

## 路径：`recruiter_navigator\core\launcher.py`

```python
"""统一启动逻辑 — 根据工具配置启动 CLI / Web / 交互式工具。"""

from __future__ import annotations

import subprocess
import sys
import webbrowser

# Windows 终端兼容：强制 UTF-8 输出
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")  # type: ignore[attr-defined]

from .models import LaunchMode, Tool


def get_launch_commands(tool: Tool) -> dict[str, str]:
    """
    生成工具的终端命令指引，供 Web 页面展示。

    返回包含安装命令和运行命令的字典。
    """
    install_cmd = f"pip install {tool.name}"
    run_cmd = tool.launch.command

    if tool.launch.mode == LaunchMode.WEB and tool.launch.web_port:
        run_cmd = f"{tool.launch.command}  # 启动后访问 http://localhost:{tool.launch.web_port}"

    return {
        "install": install_cmd,
        "run": run_cmd,
        "mode": tool.launch.mode.value,
        "web_port": tool.launch.web_port,
    }


def launch_tool(tool: Tool, extra_args: list[str] | None = None) -> None:
    """
    根据工具的启动模式执行启动（CLI 模式专用）。

    - cli: 在当前终端执行命令
    - web: 启动服务并打开浏览器
    - interactive: 进入交互会话
    """
    args = extra_args or []

    if tool.launch.mode == LaunchMode.CLI:
        _launch_cli(tool.launch.command, args)
    elif tool.launch.mode == LaunchMode.WEB:
        _launch_web(tool.launch.command, tool.launch.web_port, args)
    elif tool.launch.mode == LaunchMode.INTERACTIVE:
        _launch_interactive(tool.launch.command, args)


def _launch_cli(command: str, args: list[str]) -> None:
    """在当前终端执行 CLI 命令。"""
    cmd = [command] + args
    try:
        subprocess.run(cmd, check=False)
    except FileNotFoundError:
        print(f"[错误] 找不到命令 '{command}'，请确认该工具已安装。")
        print(f"  安装方式: pip install {command}")
        sys.exit(1)


def _launch_web(command: str, port: int | None, args: list[str]) -> None:
    """启动 Web 服务并打开浏览器。"""
    cmd = [command] + args
    if port:
        url = f"http://localhost:{port}"
        print(f"[信息] 正在启动 Web 服务: {url}")
        webbrowser.open(url)
    try:
        subprocess.run(cmd, check=False)
    except FileNotFoundError:
        print(f"[错误] 找不到命令 '{command}'，请确认该工具已安装。")
        print(f"  安装方式: pip install {command}")
        sys.exit(1)


def _launch_interactive(command: str, args: list[str]) -> None:
    """启动交互式会话。"""
    cmd = [command] + args
    try:
        subprocess.run(cmd, check=False)
    except FileNotFoundError:
        print(f"[错误] 找不到命令 '{command}'，请确认该工具已安装。")
        print(f"  安装方式: pip install {command}")
        sys.exit(1)

```

---

## 路径：`recruiter_navigator\core\models.py`

```python
"""Pydantic 数据模型 — 工具注册表的核心数据结构。"""

from __future__ import annotations

from enum import Enum

from pydantic import BaseModel


# ── 枚举 ──────────────────────────────────────────────

class Category(str, Enum):
    """工具分类。"""
    SEARCH = "search"
    RESUME = "resume"
    OUTREACH = "outreach"
    DATA = "data"
    INFRA = "infra"


class LaunchMode(str, Enum):
    """启动模式。"""
    CLI = "cli"
    WEB = "web"
    INTERACTIVE = "interactive"


# ── 文档中心子模型 ────────────────────────────────────

class QuickStart(BaseModel):
    """新手指引。"""
    install: str
    command_example: str
    expected_output: str


class Workflow(BaseModel):
    """工作流上下游推荐。"""
    prev_tools: list[str] = []
    next_tools: list[str] = []


class ToolDocs(BaseModel):
    """工具文档中心配置（前端驱动）。"""
    readme: str = ""
    use_cases: list[str] = []
    workflow: Workflow = Workflow()
    quick_start: QuickStart | None = None
    tags: list[str] = []


# ── 核心模型 ──────────────────────────────────────────

class LaunchConfig(BaseModel):
    """启动配置。"""
    mode: LaunchMode
    command: str
    web_port: int | None = None


class Tool(BaseModel):
    """单个工具的完整元数据。"""
    name: str
    display_name: str
    description: str
    category: Category
    pypi_url: str
    launch: LaunchConfig
    docs: ToolDocs = ToolDocs()


class ToolSummary(BaseModel):
    """工具摘要（用于列表/卡片展示）。"""
    name: str
    display_name: str
    description: str
    category: Category
    launch_mode: LaunchMode
    tags: list[str] = []

    @classmethod
    def from_tool(cls, tool: Tool) -> ToolSummary:
        return cls(
            name=tool.name,
            display_name=tool.display_name,
            description=tool.description,
            category=tool.category,
            launch_mode=tool.launch.mode,
            tags=tool.docs.tags,
        )


class Registry(BaseModel):
    """工具注册表。"""
    tools: list[Tool] = []

```

---

## 路径：`recruiter_navigator\core\registry.py`

```python
"""注册表加载器 — 从 registry.yaml 加载工具元数据。"""

from __future__ import annotations

from functools import lru_cache
from pathlib import Path

import yaml

from .models import Category, Registry, Tool, ToolSummary

_DATA_DIR = Path(__file__).resolve().parent.parent / "data"
_REGISTRY_PATH = _DATA_DIR / "registry.yaml"

# 全局自定义配置路径（由 CLI --config 参数设置）
_custom_registry_path: Path | None = None


def set_registry_path(path: str | Path | None) -> None:
    """设置自定义 registry.yaml 路径。

    设置后，所有后续的注册表加载将使用该路径。
    传入 None 则恢复使用内置默认路径。
    """
    global _custom_registry_path
    _custom_registry_path = Path(path).resolve() if path else None
    # 清除缓存，确保下次加载使用新路径
    _load_raw.cache_clear()


def get_registry_path() -> Path:
    """获取当前生效的 registry.yaml 路径。"""
    if _custom_registry_path is not None:
        return _custom_registry_path
    return _REGISTRY_PATH


@lru_cache(maxsize=1)
def _load_raw() -> dict:
    """加载 YAML 原始数据（带缓存）。"""
    path = get_registry_path()
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)


def load_registry() -> Registry:
    """加载并解析完整的工具注册表。"""
    raw = _load_raw()
    return Registry.model_validate(raw)


def get_all_tools() -> list[Tool]:
    """获取所有工具。"""
    return load_registry().tools


def get_tool(name: str) -> Tool | None:
    """按名称获取单个工具。"""
    for tool in get_all_tools():
        if tool.name == name:
            return tool
    return None


def get_tools_by_category(category: Category) -> list[Tool]:
    """按分类筛选工具。"""
    return [t for t in get_all_tools() if t.category == category]


def search_tools(keyword: str) -> list[Tool]:
    """按关键词搜索工具（匹配名称、显示名、描述、标签）。"""
    kw = keyword.lower()
    results: list[Tool] = []
    for t in get_all_tools():
        searchable = " ".join(
            [t.name, t.display_name, t.description] + t.docs.tags + t.docs.use_cases
        ).lower()
        if kw in searchable:
            results.append(t)
    return results


def get_summaries() -> list[ToolSummary]:
    """获取所有工具的摘要信息（用于列表/卡片展示）。"""
    return [ToolSummary.from_tool(t) for t in get_all_tools()]


def get_categories() -> dict[Category, int]:
    """获取各分类的工具数量。"""
    counts: dict[Category, int] = {}
    for t in get_all_tools():
        counts[t.category] = counts.get(t.category, 0) + 1
    return counts

```

---

## 路径：`recruiter_navigator\data\__init__.py`

```python


```

---

## 路径：`recruiter_navigator\web\__init__.py`

```python


```

---

## 路径：`recruiter_navigator\web\app.py`

```python
"""FastAPI Web 应用。"""

from __future__ import annotations

from pathlib import Path

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from .routes.detail import router as detail_router
from .routes.home import router as home_router
from .routes.index import router as index_router

_TEMPLATE_DIR = Path(__file__).resolve().parent / "templates"
_STATIC_DIR = Path(__file__).resolve().parent / "static"


def create_app() -> FastAPI:
    """创建并配置 FastAPI 应用。"""
    app = FastAPI(
        title="Recruiter Tool Navigator",
        description="47 个招聘工具的统一导航入口",
        version="0.1.0",
    )

    # 模板引擎
    templates = Jinja2Templates(directory=str(_TEMPLATE_DIR))
    app.state.templates = templates

    # 静态文件 - 必须在路由注册之前挂载
    if _STATIC_DIR.exists():
        app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")

    # 注册路由
    app.include_router(home_router)
    app.include_router(index_router)
    app.include_router(detail_router)

    return app

```

---

## 路径：`recruiter_navigator\web\routes\__init__.py`

```python


```

---

## 路径：`recruiter_navigator\web\routes\detail.py`

```python
"""工具详情 API — 返回单个工具的完整 JSON + 终端命令指引。"""

from __future__ import annotations

from fastapi import APIRouter, HTTPException

from ...core.launcher import get_launch_commands
from ...core.registry import get_all_tools, get_tool

router = APIRouter(prefix="/api")


@router.get("/tools")
async def list_tools():
    """获取所有工具的摘要列表。"""
    tools = get_all_tools()
    return [
        {
            "name": t.name,
            "display_name": t.display_name,
            "description": t.description,
            "category": t.category.value,
            "launch_mode": t.launch.mode.value,
            "tags": t.docs.tags,
        }
        for t in tools
    ]


@router.get("/tool/{tool_name}")
async def get_tool_detail(tool_name: str):
    """获取单个工具的完整详情。"""
    tool = get_tool(tool_name)
    if tool is None:
        raise HTTPException(status_code=404, detail=f"工具 '{tool_name}' 不存在")
    data = tool.model_dump()
    # 附加终端命令指引
    data["launch_commands"] = get_launch_commands(tool)
    return data


@router.get("/launch-guide/{tool_name}")
async def launch_guide(tool_name: str):
    """获取工具的终端启动指引（不直接执行，仅返回命令）。"""
    tool = get_tool(tool_name)
    if tool is None:
        raise HTTPException(status_code=404, detail=f"工具 '{tool_name}' 不存在")
    return get_launch_commands(tool)

```

---

## 路径：`recruiter_navigator\web\routes\home.py`

```python
"""Home 主页路由 — 招聘流程信息图。"""

from __future__ import annotations

import json

from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse

from ...core.models import Category
from ...core.registry import get_all_tools

router = APIRouter()

# 阶段定义：编号、名称、英文标识、颜色、图标
_STAGES = [
    {"id": "prepare", "label": "职位准备", "color": "#3B82F6", "icon": "📋", "order": 1},
    {"id": "search", "label": "人才搜索", "color": "#10B981", "icon": "🔍", "order": 2},
    {"id": "resume", "label": "简历处理", "color": "#8B5CF6", "icon": "📄", "order": 3},
    {"id": "outreach", "label": "候选人触达", "color": "#F59E0B", "icon": "✉️", "order": 4},
    {"id": "data", "label": "数据分析", "color": "#06B6D4", "icon": "📊", "order": 5},
    {"id": "infra", "label": "基础设施", "color": "#6B7280", "icon": "🔧", "order": 6},
]

# 搜索阶段子分组
_SEARCH_SUBGROUPS = {
    "linkedin": {"label": "LinkedIn 生态", "tools": ["search-talent", "linkedin-horse", "linkedin-lion", "linkedin-cat"]},
    "maimai": {"label": "脉脉生态", "tools": ["maimai-cat", "mm-friend", "graph-cli-maimai"]},
    "liepin": {"label": "猎聘生态", "tools": ["liepin-cat"]},
    "github": {"label": "GitHub 生态", "tools": ["github-cat", "github-bird", "github-reachout"]},
    "academic": {"label": "学术/专利", "tools": ["paper-talent"]},
}

# 简历阶段子分组
_RESUME_SUBGROUPS = {
    "parse": {"label": "解析", "tools": ["power-doc", "textag", "resume-oneclick"]},
    "evaluate": {"label": "评估", "tools": ["talent-lens", "profile-claw"]},
    "profile": {"label": "画像", "tools": ["talent-align"]},
    "filter": {"label": "筛选", "tools": ["resume-flow"]},
    "aux": {"label": "辅助", "tools": ["resume-story", "chinese-finder", "chinese-name-detector", "cn-detect", "cvbuilder-cli"]},
}

# 触达阶段子分组
_OUTREACH_SUBGROUPS = {
    "email": {"label": "邮件生成", "tools": ["cold-msg"]},
    "message": {"label": "消息发送", "tools": ["recruiter-msg"]},
    "followup": {"label": "跟进", "tools": ["replyme"]},
    "allinone": {"label": "一站式", "tools": ["talent-whale"]},
}

# 数据阶段子分组
_DATA_SUBGROUPS = {
    "process": {"label": "数据处理", "tools": ["excel-toolbox", "excel-merger", "excel-text-classifier"]},
    "visual": {"label": "可视化", "tools": ["excel-vis", "orgchart-swan"]},
    "insight": {"label": "洞察", "tools": ["talent-birdview", "jobdeer", "patent-analyzer"]},
}

# 基础设施子分组
_INFRA_SUBGROUPS = {
    "llm": {"label": "LLM", "tools": ["llmdog", "batch-swallow"]},
    "browser": {"label": "浏览器", "tools": ["browser-dog"]},
    "agent": {"label": "Agent", "tools": ["sting-agent", "loop-agent-cli", "taskwatch"]},
    "api": {"label": "API", "tools": ["wsapi-sdk", "recruiter-cat"]},
    "util": {"label": "工具", "tools": ["page-extract", "larkfunc"]},
}

# 典型招聘场景
_SCENARIOS = [
    {
        "id": "linkedin-standard",
        "title": "LinkedIn 标准招聘流程",
        "desc": "通用岗位，在 LinkedIn 上批量搜索和触达候选人",
        "chain": ["kwtree", "search-talent", "linkedin-horse", "resume-oneclick", "talent-lens", "cold-msg", "replyme"],
    },
    {
        "id": "maimai-ai",
        "title": "脉脉 AI 自动化招聘",
        "desc": "国内互联网岗位，在脉脉上自动化招聘",
        "chain": ["maimai-cat", "mm-friend", "graph-cli-maimai", "cold-msg", "replyme"],
    },
    {
        "id": "github-reachout",
        "title": "GitHub 开源人才定向招聘",
        "desc": "技术岗位，从开源社区定向挖掘开发者",
        "chain": ["github-cat", "github-bird", "github-reachout", "cold-msg", "replyme"],
    },
    {
        "id": "academic-talent",
        "title": "学术/专利人才挖掘",
        "desc": "AI/算法/研究岗位，从学术圈挖掘高端人才",
        "chain": ["patent-analyzer", "paper-talent", "maimai-cat", "mm-friend", "cold-msg"],
    },
    {
        "id": "resume-batch",
        "title": "简历批量处理与评估",
        "desc": "已有大量简历需批量处理和评估",
        "chain": ["power-doc", "resume-oneclick", "talent-align", "talent-lens", "talent-birdview"],
    },
    {
        "id": "multi-channel",
        "title": "一站式多渠道触达",
        "desc": "同时通过邮件和 LinkedIn 多渠道触达候选人",
        "chain": ["resume-oneclick", "cold-msg", "talent-whale", "replyme"],
    },
    {
        "id": "agent-auto",
        "title": "Agent 自动化全流程",
        "desc": "无人值守自动化执行长期招聘任务",
        "chain": ["sting-agent", "loop-agent-cli", "graph-cli-maimai", "cold-msg"],
    },
    {
        "id": "data-analysis",
        "title": "招聘数据可视化分析",
        "desc": "对招聘数据进行全局分析和可视化展示",
        "chain": ["search-talent", "excel-toolbox", "excel-merger", "excel-vis", "orgchart-swan"],
    },
]


@router.get("/home", response_class=HTMLResponse)
async def home_page(request: Request):
    """招聘流程信息图主页。"""
    tools = get_all_tools()

    # 构建工具数据（含 workflow 信息）
    tools_data = []
    for t in tools:
        tools_data.append({
            "name": t.name,
            "display_name": t.display_name,
            "description": t.description,
            "category": t.category.value,
            "launch_mode": t.launch.mode.value,
            "web_port": t.launch.web_port,
            "pypi_url": t.pypi_url,
            "tags": t.docs.tags,
            "use_cases": t.docs.use_cases,
            "prev_tools": t.docs.workflow.prev_tools,
            "next_tools": t.docs.workflow.next_tools,
            "quick_start": t.docs.quick_start.model_dump() if t.docs.quick_start else None,
        })

    tools_json = json.dumps(tools_data, ensure_ascii=False)
    stages_json = json.dumps(_STAGES, ensure_ascii=False)
    scenarios_json = json.dumps(_SCENARIOS, ensure_ascii=False)

    # 子分组
    subgroups = {
        "search": _SEARCH_SUBGROUPS,
        "resume": _RESUME_SUBGROUPS,
        "outreach": _OUTREACH_SUBGROUPS,
        "data": _DATA_SUBGROUPS,
        "infra": _INFRA_SUBGROUPS,
    }
    subgroups_json = json.dumps(subgroups, ensure_ascii=False)

    return request.app.state.templates.TemplateResponse(
        request,
        "home.html",
        {
            "tools_json": tools_json,
            "stages_json": stages_json,
            "scenarios_json": scenarios_json,
            "subgroups_json": subgroups_json,
        },
    )


@router.get("/api/workflow-graph")
async def workflow_graph():
    """返回完整的 Workflow 拓扑数据，供前端绘制流程图。"""
    tools = get_all_tools()
    nodes = []
    edges = []

    for t in tools:
        nodes.append({
            "id": t.name,
            "label": t.display_name,
            "category": t.category.value,
        })
        for next_tool in t.docs.workflow.next_tools:
            edges.append({
                "source": t.name,
                "target": next_tool,
            })

    return {"nodes": nodes, "edges": edges}

```

---

## 路径：`recruiter_navigator\web\routes\index.py`

```python
"""首页路由 — 工具画廊。"""

from __future__ import annotations

import json

from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse

from ...core.models import Category
from ...core.registry import get_all_tools, get_categories

router = APIRouter()

# 分类显示名映射
_CATEGORY_LABELS = {
    Category.SEARCH: "🔍 人才发现",
    Category.RESUME: "📄 简历处理",
    Category.OUTREACH: "✉️ 候选人触达",
    Category.DATA: "📊 数据管理",
    Category.INFRA: "🔧 基础设施",
}


@router.get("/", response_class=HTMLResponse)
async def index(request: Request):
    """工具画廊首页。"""
    tools = get_all_tools()
    categories = get_categories()

    # 将工具数据序列化为 JSON 供 Alpine.js 使用
    tools_json = json.dumps(
        [
            {
                "name": t.name,
                "display_name": t.display_name,
                "description": t.description,
                "category": t.category.value,
                "launch_mode": t.launch.mode.value,
                "tags": t.docs.tags,
            }
            for t in tools
        ],
        ensure_ascii=False,
    )

    return request.app.state.templates.TemplateResponse(
        request,
        "index.html",
        {
            "tools": tools,
            "categories": categories,
            "cat_labels": {k.value: v for k, v in _CATEGORY_LABELS.items()},
            "tools_json": tools_json,
        },
    )

```

---