# 从零制作 Skill，并用真实项目跑通（上）

学习 Skill 最省力的办法，不是先背完概念，而是亲手走一遍完整流程。今天我们就从一个空文件夹开始，做出能被 Agent 调用的 Skill。

先看一个真实场景：接手陌生项目时，最费劲的往往不是写代码，而是在几十层目录里“考古”——它到底做什么？入口在哪？哪些文件值得先读？

为了把流程讲清楚，我选择制作一个叫 **X-ray** 的示例 Skill：进入陌生仓库后调用它，AI 会先扫描，再挑关键文件阅读，最后生成一份能直接打开的 `xray-report.html`。

**X-ray 只是这次的练习题，不是标准答案。** 你真正要学的是“描述任务 → 准备脚本和资料 → 安装 → 调用 → 验收”这套方法。以后完全可以把它换成代码审查、周报整理、接口文档生成，或者任何你经常重复做的事情。

![上集主线：制作 X-ray、装进 Agent 工具，并在真实仓库完成第一次本地闭环](images/01-episode-guide.png)<!-- display-width:520 -->

这一集会稍长，因为我们要一次完成两个动作：**从零制作 X-ray，再把它装进 CodeBuddy，在一个真实仓库里跑通第一次闭环。**

- **预计用时**：35～50 分钟；代码可以直接复制，不要求提前会 Python。
- **成功标志**：测试目录里出现 `xray-data.json` 和 `xray-report.html`，浏览器能打开一份完整的项目分析报告。
- **今天不做**：不发布 GitHub，不碰 ClawHub，也不运行测试仓库自己的项目代码。

不用先把 Skill 的所有原理背下来。今天的目标很朴素：像小时候抄一遍代码那样，把一条完整路线亲手走通，手感自然就来了。

## 1. 先准备四样东西

第一次出现的工具，我们都只讲“马上要用的那一点”。已经装好的，直接跳过。

- **[CodeBuddy IDE](https://www.codebuddy.cn/ide/)**：一款带 Agent 能力的 AI 编程工具，本集用它加载并调用 X-ray。Windows 10+、macOS 和 Linux 都有安装包；[官方安装说明在这里](https://www.codebuddy.cn/docs/ide/Getting-Started/Installation)。能打开一个本地文件夹并进入 Agent 对话，就算准备完成。
- **[Python 3](https://www.python.org/downloads/)**：负责运行扫描脚本。在 PowerShell 执行 `python --version`，能看到 `Python 3.x` 就不用重装；如果系统只认 `py --version`，后文把 `python` 换成 `py` 即可。
- **[Git](https://git-scm.com/downloads)**：负责把测试项目克隆到电脑。执行 `git --version`，能看到版本号就算过关。
- **[VS Code](https://code.visualstudio.com/Download)**：选装。素材里的部分代码编辑画面来自 VS Code 风格界面；只用 CodeBuddy 也能完成，不需要为了截图长得一样再装一套编辑器。

公司电脑如果没有安装权限，先走组织的软件准入流程。教程可以等等，安全制度不适合“曲线救国”。

## 2. Skill 到底是什么？

说白了，Skill 是一套保存下来的做事方法。

普通提示词像临时口头交代；Skill 更像 Agent 的“岗位说明书 + 工具箱”。下次换一个项目，不用重新解释几十行要求，只要调用 `/x-ray`，Agent 就知道应该先扫描、再读证据、最后交付报告。

OpenAI 的[可复用 Skill 教程](https://learn.chatgpt.com/codex/use-cases/reusable-codex-skills)采用同样的分层思路。不同工具的安装目录和唤起方式会不同，但下面四个位置很好记：

![一个 Skill 的四个位置：说明、脚本、分析规则与报告模板](images/02-skill-anatomy-diagram.png)<!-- display-width:520 -->

- `SKILL.md`：告诉 Agent 什么时候上班、按什么流程工作、哪些事不能做。
- `scripts/`：负责数文件、识别配置等确定性工作。
- `references/`：保存分析规则，帮助 Agent 区分事实和推断。
- `assets/`：保存最终报告会用到的模板。

X-ray 的设计原则只有一句：**代码负责测量，Agent 负责理解，HTML 负责展示。**

## 3. 搭好 X-ray 的空骨架

这一步的作用，是先把 Skill 的四类内容分开放好，后面不会把说明、脚本和模板搅成一锅粥。

先在桌面创建一个名为 `x-ray` 的文件夹。你可以使用 CodeBuddy、VS Code，或者直接用系统文件管理器。

![先创建一个名为 x-ray 的空文件夹](images/03-create-skill-folder.png)<!-- display-width:600 -->

接着在里面创建三个文件夹和一个文件：

```text
x-ray/
├─ assets/
├─ references/
├─ scripts/
└─ SKILL.md
```

也可以在 PowerShell 中一次完成：

```powershell
New-Item -ItemType Directory -Force x-ray
Set-Location x-ray
New-Item -ItemType Directory -Force assets, references, scripts
New-Item -ItemType File -Force SKILL.md
```

![X-ray 的基础目录包含 assets、references、scripts 和 SKILL.md](images/04-skill-folder-structure.png)<!-- display-width:600 -->

**看到什么算成功：** 左侧文件树和截图一样，有三个文件夹与 `SKILL.md`。现在它还不会干活，但办公室已经装修完了。

## 4. 写 SKILL.md：先把岗位说清楚

`SKILL.md` 相当于写给 Agent 的岗位说明书：它决定什么时候触发、按什么顺序工作，以及哪些事绝对不能做。

把下面内容完整写入 `SKILL.md`：

```markdown
---
name: x-ray
description: 扫描一个陌生的软件项目，快速识别项目类型、技术栈、架构、文件结构、复杂度和学习价值。适用于用户希望快速理解、探索、检查或接手一个陌生代码库的场景。
---

# X-ray

分析当前软件项目，并生成一份简洁、可视化的项目概览。

## 目标

调用 X-ray 时：

1. 扫描项目目录结构。
2. 判断项目是什么类型。
3. 识别主要技术栈。
4. 找出重要目录和关键文件。
5. 推断项目的整体架构。
6. 评估项目复杂度。
7. 判断这个项目适合哪些方向的人学习。
8. 推荐理解项目的阅读顺序。
9. 最终生成一个独立可打开的 HTML 可视化报告。

## 安全规则

默认把项目视为只读。

不要：

- 修改项目源代码；
- 安装依赖；
- 执行项目代码；
- 读取或暴露 `.env` 等文件中的敏感信息；
- 执行未知脚本。

优先使用静态分析和文件读取来理解项目。
```

![在 SKILL.md 中写清目标、流程与只读安全边界](images/05-write-skill-md.png)<!-- display-width:600 -->

这里最重要的不是写得多，而是三件事写清楚：**什么时候触发、要交付什么、不能碰什么。** 陌生仓库里的脚本不能因为名字亲切就随手运行，`.env` 也不是项目欢迎词。

## 5. 写第一个脚本：先学会“数”

大模型擅长理解，但不适合自己数几百个文件。这一步先把“数文件、认语言”交给脚本，得到可靠的一手数据。

在 `scripts` 目录创建 `scan_repo.py`。

![在 scripts 目录创建 scan_repo.py](images/06-create-scan-script.png)<!-- display-width:600 -->

接下来正文直接提供可复制的完整代码。为了不让同一段内容占两遍版面，长代码截图不再重复插入；我们只保留创建文件、执行命令和结果验收的真实画面。清晰代码原图仍完整保存在本集图片集中。

复制下面的完整代码：

```python
from pathlib import Path
from collections import Counter
import json
import sys

IGNORE_DIRS = {
    ".git",
    "node_modules",
    ".next",
    "dist",
    "build",
    "__pycache__",
    ".venv",
    "venv",
}

LANGUAGE_MAP = {
    ".py": "Python",
    ".js": "JavaScript",
    ".jsx": "JavaScript",
    ".ts": "TypeScript",
    ".tsx": "TypeScript",
    ".java": "Java",
    ".go": "Go",
    ".rs": "Rust",
    ".rb": "Ruby",
    ".php": "PHP",
    ".cs": "C#",
    ".cpp": "C++",
    ".c": "C",
    ".swift": "Swift",
    ".kt": "Kotlin",
    ".vue": "Vue",
    ".svelte": "Svelte",
}


def should_ignore(path: Path) -> bool:
    return any(part in IGNORE_DIRS for part in path.parts)


def scan_repository(root: Path):
    files = []
    directories = set()
    languages = Counter()

    for path in root.rglob("*"):
        if should_ignore(path.relative_to(root)):
            continue

        if path.is_dir():
            directories.add(str(path.relative_to(root)))
            continue

        files.append(str(path.relative_to(root)))

        language = LANGUAGE_MAP.get(path.suffix.lower())
        if language:
            languages[language] += 1

    return {
        "project_name": root.name,
        "total_files": len(files),
        "total_directories": len(directories),
        "languages": dict(languages.most_common()),
        "files": files,
    }


def main():
    target = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
    result = scan_repository(target)

    print(json.dumps(
        result,
        ensure_ascii=False,
        indent=2
    ))


if __name__ == "__main__":
    main()
```

它做的事情很朴素：跳过缓存和依赖目录，统计文件、目录、语言，再把文件清单交出来。模型不用自己掰手指数 174 个文件，我们已经给它配了计算器。

在 `x-ray` 根目录打开终端，执行和截图一致的命令：

```powershell
python scripts/scan_repo.py .
```

![运行 python scripts/scan_repo.py . 测试扫描脚本](images/07-run-scan-repo.png)<!-- display-width:600 -->

正常情况下，终端会返回一段 JSON：

![扫描脚本返回项目名、文件数、目录数与语言](images/08-scan-repo-result.png)<!-- display-width:600 -->

**第一次小成功：** 能看到 `project_name`、`total_files`、`total_directories`、`languages` 和 `files`，说明扫描脚本已经能工作。

## 6. 再写一个脚本：识别技术栈

只知道“有多少文件”还不够。这个脚本会根据真实配置文件识别技术栈，让 Agent 有证据再下结论。

继续在 `scripts` 中创建 `detect_stack.py`。

![继续创建用于识别技术栈的 detect_stack.py](images/09-create-detect-stack.png)<!-- display-width:600 -->

复制下面代码：

```python
from pathlib import Path
import json
import sys


def load_json(path: Path):
    try:
        return json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}


def detect_stack(root: Path):
    detected = []

    package_json = root / "package.json"

    if package_json.exists():
        detected.append("Node.js")
        data = load_json(package_json)
        dependencies = {}
        dependencies.update(data.get("dependencies", {}))
        dependencies.update(data.get("devDependencies", {}))

        rules = {
            "react": "React",
            "next": "Next.js",
            "vue": "Vue",
            "nuxt": "Nuxt",
            "svelte": "Svelte",
            "@sveltejs/kit": "SvelteKit",
            "express": "Express",
            "fastify": "Fastify",
            "nestjs": "NestJS",
            "@nestjs/core": "NestJS",
            "tailwindcss": "Tailwind CSS",
            "prisma": "Prisma",
            "@prisma/client": "Prisma",
            "drizzle-orm": "Drizzle ORM",
            "typescript": "TypeScript",
            "vite": "Vite",
            "webpack": "Webpack",
            "vitest": "Vitest",
            "jest": "Jest",
        }

        for package_name, technology in rules.items():
            if package_name in dependencies:
                detected.append(technology)

    if (root / "requirements.txt").exists():
        detected.append("Python")

    if (root / "pyproject.toml").exists():
        detected.append("Python")

    if (root / "Cargo.toml").exists():
        detected.append("Rust")

    if (root / "go.mod").exists():
        detected.append("Go")

    if (root / "pom.xml").exists():
        detected.append("Java / Maven")

    if (root / "build.gradle").exists():
        detected.append("Java / Gradle")

    if (root / "Dockerfile").exists():
        detected.append("Docker")

    if (root / "docker-compose.yml").exists():
        detected.append("Docker Compose")

    if (root / "docker-compose.yaml").exists():
        detected.append("Docker Compose")

    detected = list(dict.fromkeys(detected))

    return {
        "technology_stack": detected
    }


def main():
    target = Path(
        sys.argv[1] if len(sys.argv) > 1 else "."
    ).resolve()

    result = detect_stack(target)

    print(json.dumps(
        result,
        ensure_ascii=False,
        indent=2
    ))


if __name__ == "__main__":
    main()
```

还是在同一个终端执行：

```powershell
python scripts/detect_stack.py .
```

![运行 python scripts/detect_stack.py . 查看识别结果](images/10-run-detect-stack.png)<!-- display-width:600 -->

截图里得到的是空数组 `[]`，这不是翻车。我们此刻扫描的是 X-ray 自己，它的根目录没有 `package.json`、`requirements.txt` 等证据。**没证据就不乱猜，恰恰是好习惯。**

## 7. 把两份结果装进同一个 JSON

前两个脚本各管一摊，这一步给它们加一个统一入口。以后 Agent 只需要运行一次，就能拿到一份结构化数据。

创建 `scripts/run_xray.py`。

![创建 run_xray.py 统一生成扫描数据](images/11-create-run-xray.png)<!-- display-width:600 -->

写入：

```python
from pathlib import Path
import json
import sys

from scan_repo import scan_repository
from detect_stack import detect_stack


def run_xray(root: Path):
    repo_data = scan_repository(root)
    stack_data = detect_stack(root)

    result = {
        "project": repo_data,
        "stack": stack_data,
    }

    return result


def main():
    target = Path(
        sys.argv[1] if len(sys.argv) > 1 else "."
    ).resolve()

    result = run_xray(target)

    output_path = Path("xray-data.json")

    output_path.write_text(
        json.dumps(
            result,
            ensure_ascii=False,
            indent=2
        ),
        encoding="utf-8"
    )

    print(f"X-ray 扫描完成")
    print(f"目标项目：{target}")
    print(f"数据文件：{output_path.resolve()}")


if __name__ == "__main__":
    main()
```

这个文件是统一入口。它把仓库扫描和技术栈识别合并，写成 `xray-data.json`，方便 Agent 后面继续处理。

## 8. 补三件事：关键文件、复杂度、HTML 模板

做到这里，最小骨架已经有了：说明书、扫描脚本、技术栈识别和统一入口。代码看累了可以先歇一下；你不需要立刻理解每一行 Python，只要知道每个文件负责什么、运行后应该看到什么。

接下来三个组件会把“能扫描”升级成“能给出有用报告”：找关键文件、计算可解释的复杂度、把结果放进 HTML。它们不是为了把 Python 课偷偷塞进来，而是给 Agent 提供可复用的测量工具。

先创建 `scripts/find_key_files.py`：

![创建 find_key_files.py 寻找高信息密度文件](images/12-create-find-key-files.png)<!-- display-width:600 -->

```python
from pathlib import Path
import json
import sys

KEY_FILE_RULES = {
    "README.md": "项目说明",
    "README": "项目说明",
    "package.json": "Node.js 项目配置",
    "pyproject.toml": "Python 项目配置",
    "requirements.txt": "Python 依赖",
    "Cargo.toml": "Rust 项目配置",
    "go.mod": "Go 项目配置",
    "Dockerfile": "容器部署",
    "docker-compose.yml": "容器编排",
    "docker-compose.yaml": "容器编排",
    "next.config.js": "Next.js 配置",
    "next.config.mjs": "Next.js 配置",
    "next.config.ts": "Next.js 配置",
    "vite.config.js": "Vite 配置",
    "vite.config.ts": "Vite 配置",
    "tsconfig.json": "TypeScript 配置",
    "prisma/schema.prisma": "数据库模型",
    "src/main.py": "Python 程序入口",
    "main.py": "Python 程序入口",
    "src/main.ts": "程序入口",
    "src/main.tsx": "前端程序入口",
    "src/app/layout.tsx": "Next.js 根布局",
    "src/app/page.tsx": "Next.js 首页",
    "middleware.ts": "中间件",
    "middleware.js": "中间件",
}


def find_key_files(root: Path):
    key_files = []

    for relative_path, reason in KEY_FILE_RULES.items():
        path = root / relative_path

        if path.exists():
            key_files.append({
                "path": relative_path,
                "reason": reason
            })

    return {
        "key_files": key_files
    }


def main():
    target = Path(
        sys.argv[1] if len(sys.argv) > 1 else "."
    ).resolve()

    result = find_key_files(target)

    print(json.dumps(
        result,
        ensure_ascii=False,
        indent=2
    ))


if __name__ == "__main__":
    main()
```

它不让模型凭感觉选文件，而是先把 README、配置、入口等高信息密度位置列出来。

再创建 `scripts/complexity.py`：

![创建 complexity.py 计算可解释的复杂度分数](images/13-create-complexity.png)<!-- display-width:600 -->

```python
from pathlib import Path
import json
import sys


def clamp(value, min_value=0, max_value=10):
    return max(min_value, min(max_value, value))


def calculate_complexity(project_data, stack_data, key_files_data):
    total_files = project_data.get("total_files", 0)
    total_directories = project_data.get("total_directories", 0)

    languages = project_data.get("languages", {})
    technology_stack = stack_data.get("technology_stack", [])
    key_files = key_files_data.get("key_files", [])

    size_score = min(total_files / 100, 10)
    structure_score = min(total_directories / 20, 10)
    stack_score = min(len(technology_stack) * 1.2, 10)
    language_score = min(len(languages) * 2, 10)
    key_file_score = min(len(key_files), 10)

    final_score = (
        size_score * 0.30
        + structure_score * 0.20
        + stack_score * 0.25
        + language_score * 0.10
        + key_file_score * 0.15
    )

    final_score = round(clamp(final_score), 1)

    if final_score < 3:
        level = "简单"
    elif final_score < 5:
        level = "中等"
    elif final_score < 7:
        level = "偏复杂"
    elif final_score < 9:
        level = "复杂"
    else:
        level = "非常复杂"

    return {
        "score": final_score,
        "level": level,
        "dimensions": {
            "size": round(size_score, 1),
            "structure": round(structure_score, 1),
            "technology_stack": round(stack_score, 1),
            "languages": round(language_score, 1),
            "key_files": round(key_file_score, 1),
        }
    }


def main():
    if len(sys.argv) < 2:
        print("用法：python complexity.py xray-data.json")
        return

    data_path = Path(sys.argv[1])
    data = json.loads(data_path.read_text(encoding="utf-8"))

    result = calculate_complexity(
        data["project"],
        data["stack"],
        data["key_files"]
    )

    print(json.dumps(
        result,
        ensure_ascii=False,
        indent=2
    ))


if __name__ == "__main__":
    main()
```

复杂度不是圣旨，只是一个可以解释、可以调整的起点。至少它比“模型看了一眼，觉得大概 8 分”靠谱。

然后在 `assets/report-template.html` 写一个最小模板：

```html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>X-ray Report</title>
</head>
<body>
    <h1>🔍 X-ray</h1>
    <h2>{{PROJECT_NAME}}</h2>
    <p>文件数量：{{TOTAL_FILES}}</p>
    <p>目录数量：{{TOTAL_DIRECTORIES}}</p>
    <p>复杂度：{{COMPLEXITY_SCORE}} / 10</p>
    <h2>技术栈</h2>
    <div>{{TECH_STACK}}</div>
    <h2>关键文件</h2>
    <div>{{KEY_FILES}}</div>
</body>
</html>
```

文件树里应该能看到模板：

![在 assets 中创建 report-template.html](images/14-create-report-template.png)<!-- display-width:600 -->

最后创建 `scripts/render_report.py`：

![创建 render_report.py 把结构化数据填入模板](images/15-create-render-report.png)<!-- display-width:600 -->

```python
from pathlib import Path
import json
import sys


def render_report(data_path):
    data = json.loads(
        Path(data_path).read_text(encoding="utf-8")
    )

    script_dir = Path(__file__).resolve().parent
    template_path = (
        script_dir.parent
        / "assets"
        / "report-template.html"
    )

    template = template_path.read_text(
        encoding="utf-8"
    )

    project = data["project"]
    stack = data["stack"]
    complexity = data["complexity"]
    key_files = data["key_files"]

    tech_stack = ", ".join(
        stack["technology_stack"]
    )

    key_files_text = ""

    for item in key_files["key_files"]:
        key_files_text += (
            f"<p>{item['path']} —— "
            f"{item['reason']}</p>"
        )

    template = template.replace(
        "{{PROJECT_NAME}}",
        project["project_name"]
    )

    template = template.replace(
        "{{TOTAL_FILES}}",
        str(project["total_files"])
    )

    template = template.replace(
        "{{TOTAL_DIRECTORIES}}",
        str(project["total_directories"])
    )

    template = template.replace(
        "{{COMPLEXITY_SCORE}}",
        str(complexity["score"])
    )

    template = template.replace(
        "{{TECH_STACK}}",
        tech_stack
    )

    template = template.replace(
        "{{KEY_FILES}}",
        key_files_text
    )

    output_path = (
        Path(data_path).parent
        / "xray-report.html"
    )

    output_path.write_text(
        template,
        encoding="utf-8"
    )

    print("✅ X-ray 报告生成完成")
    print(output_path)


if __name__ == "__main__":
    render_report(sys.argv[1])
```

先别单独运行它。当前 `run_xray.py` 只稳定生成 `project` 和 `stack`，而这个渲染脚本还需要 `key_files` 与 `complexity`。本集的真实路线是让 Agent 调用这些组件、抽样阅读源码，再生成完整 HTML；后面再把它们串成纯脚本流水线也来得及。

## 9. 先运行统一扫描入口

在交给 Agent 之前，我们先自己运行一次统一入口。这样如果出错，也能立刻判断是脚本问题，还是后面的 Agent 调用问题。

在 `x-ray` 根目录执行：

```powershell
python scripts\run_xray.py .
```

![运行 python scripts/run_xray.py . 生成 xray-data.json](images/16-run-xray-command.png)<!-- display-width:600 -->

你应该看到“X-ray 扫描完成”、目标项目路径和数据文件路径，同时目录里多出 `xray-data.json`。

![xray-data.json 保存脚本采集的一手事实](images/17-xray-data-result.png)<!-- display-width:600 -->

**到这里，事实采集层已经成功。** JSON 只负责记录它真正看见的东西，不负责把未知内容脑补成“高并发微服务宇宙”。

## 10. 给 Agent 一份分析规则

扫描数据只告诉 Agent“看到了什么”，分析规则负责告诉它“应该怎样解释、什么时候必须承认不确定”。

在 `references` 中创建 `analysis-rules.md`。

![在 references 中创建 analysis-rules.md](images/18-create-analysis-rules.png)<!-- display-width:600 -->

写入：

```markdown
# X-ray 分析规则

先读取 `xray-data.json`，所有分析必须基于扫描得到的一手数据。

## project.total_files
用于判断项目规模。

## project.total_directories
用于判断项目的目录规模和组织程度。

## project.languages
用于判断项目主要使用的开发语言。

## project.files
用于了解项目文件结构，并选择值得进一步阅读的关键文件。

## stack.technology_stack
用于判断已经识别出的技术栈。

## 分析原则

在 `xray-data.json` 的基础上，再读取 README、配置文件和关键源码，
进一步判断：

- 这个项目是什么
- 项目类型
- 整体架构
- 关键模块
- 项目复杂度
- 适合哪些方向的人学习
- 推荐阅读顺序

所有结论必须优先依据 `xray-data.json` 和实际源码。

`references` 只负责说明“如何解释扫描数据”，不能覆盖或虚构项目的一手事实。

如果证据不足，明确标记为“不确定”，不要猜测。
```

这份文件的作用不是给答案，而是约束“怎么得出答案”。事实来自项目，规则只负责提醒 Agent 别演得太投入。

## 11. 把完整流程补回 SKILL.md

前面的文件各自已经能工作，但 Agent 还不知道如何把它们串起来。这一步把完整工作流补回 `SKILL.md`，相当于给它一张执行路线图。

在 `SKILL.md` 的安全规则后继续加入：

```markdown
## 工作流程

当用户调用 X-ray 分析一个陌生项目时：

1. 使用 `scripts/run_xray.py` 扫描当前项目。
2. 读取扫描生成的 `xray-data.json`。
3. 读取 `references/analysis-rules.md`，按照其中的规则解释扫描数据。
4. 根据 `xray-data.json` 中的文件列表，进一步读取：
   - README
   - 项目配置文件
   - 程序入口
   - 关键源码文件
5. 基于真实扫描数据和实际源码，分析：
   - 这个项目是什么
   - 项目类型
   - 技术栈
   - 文件结构
   - 整体架构
   - 项目复杂度
   - 适合哪些方向的人学习
   - 推荐阅读顺序
6. 最终生成一个 HTML 可视化项目报告。

## 原则

- 优先依据 `xray-data.json` 和实际源码。
- `references` 只提供分析方法，不提供项目事实。
- 不确定的信息明确标记为“不确定”。
- 不修改被分析项目的源代码。
- 不安装依赖。
- 不执行未知项目代码。
```

现在 X-ray 的“大脑说明书、测量工具、判断规则和报告模板”都齐了。下一步不再盯着代码看，我们直接把它扔进真实项目。

## 12. 选一个你想分析的项目

这里不要求你必须使用 AIFriends：

- **手边有自己的项目**：直接使用自己的公开项目，或确认允许发送给当前模型服务的项目。
- **暂时没有合适项目**：跟着截图使用 AIFriends，最容易对照结果。

不要把未经允许的公司仓库或敏感代码交给云端 Agent。示例可以换，数据边界不能靠勇气突破。

为了和截图一致，可以在桌面新建 `测试X-ray` 文件夹。

![新建测试X-ray文件夹，准备第一次真实项目验收](images/19-create-local-test-folder.png)<!-- display-width:600 -->

进入这个文件夹，执行和截图一致的 SSH 命令：

```powershell
git clone git@github.com:ppshux/AIFriends.git
```

![克隆本集使用的公开测试仓库；截图采用 Git SSH 地址](images/20-clone-aifriends.png)<!-- display-width:600 -->

如果你还没有配置 GitHub SSH Key，就用官方仓库页面提供的 HTTPS 地址：

```powershell
git clone https://github.com/ppshuX/AIFriends.git
```

本次截图使用 AIFriends，只是为了演示和方便对照；它不是教程的主角，也不是制作 Skill 的必选项。

两条命令只选一条。看到 `Cloning into 'AIFriends'...` 并顺利结束，就说明测试项目已经准备好；我们不会安装它的依赖，也不会运行它。

## 13. 用 CodeBuddy 打开测试目录

这一步是让 CodeBuddy 把测试项目当作当前工作目录。这样调用 `/x-ray` 时，它才知道应该分析谁。

用 CodeBuddy 打开刚刚的 `测试X-ray` 文件夹。

![用 CodeBuddy 打开测试文件夹](images/21-open-codebuddy.png)<!-- display-width:600 -->

第一次使用 CodeBuddy 的读者，回看第 1 节的[官方下载入口](https://www.codebuddy.cn/ide/)即可。这里的成功标志只有一个：左侧能看到刚克隆的项目文件，右侧能打开 Agent 对话。

## 14. 把本地 X-ray 装进 CodeBuddy

Skill 文件夹做好了，不等于 Agent 已经能看见它。这一步要把 X-ray 放进 CodeBuddy 实际读取的 Skill 目录。

我们直接把真实实验里的话原样交给 Agent：

```text
把我桌面的 x-ray 放到你的skill目录里
```

![让 CodeBuddy 把桌面的 x-ray 放入本地 Skill 目录](images/22-install-local-prompt.png)<!-- display-width:600 -->

CodeBuddy 会找到桌面的 `x-ray`，再把它放进自己的 Skill 目录。Windows 上通常是：

```text
C:\Users\你的用户名\.codebuddy\skills\x-ray
```

![CodeBuddy 完成本地 Skill 安装并提示重启](images/23-install-local-result.png)<!-- display-width:600 -->

截图里的关键信息是安装目录与“需要重启/重载”。完成后彻底重启 CodeBuddy，让它重新扫描 Skills。

## 15. 调用 /x-ray，生成第一份真实报告

前面都是搭积木，现在才是真正的验收：如果 `/x-ray` 能被识别，并为测试项目生成报告，这个 Skill 才算跑通。

重启后，在对话框输入 `/x`。列表中能看到 `/x-ray`，说明安装成功。

![重启后输入斜杠加 x，列表中已经出现 x-ray](images/24-skill-visible.png)<!-- display-width:600 -->

选中它，直接回车，不需要额外写提示词：

```text
/x-ray
```

![直接执行 /x-ray，不需要另外编写提示词](images/25-invoke-xray.png)<!-- display-width:600 -->

CodeBuddy 会先运行扫描脚本，再读取 JSON、分析规则、README、配置与必要源码。

![CodeBuddy 开始运行 X-ray 并读取扫描脚本](images/26-xray-running.png)<!-- display-width:600 -->

等它完成后，测试目录里应该出现：

```text
xray-data.json
xray-report.html
```

打开 HTML，先看项目结构与关键技术组件：

![第一份报告展示项目结构与技术组件](images/27-aifriends-report-code.png)<!-- display-width:600 -->

继续往下看复杂度、适合的学习方向和推荐阅读顺序：

![第一份报告给出复杂度、适合方向和推荐阅读顺序](images/28-aifriends-report-path.png)<!-- display-width:600 -->

最后回到报告顶部，确认项目名和关键指标：

![第一份 X-ray 项目解剖报告已经生成](images/29-aifriends-report-overview.png)<!-- display-width:600 -->

**上集通关：** 第一份真实项目报告已经生成。此时我们不只是“写了几个文件”，而是亲手完成了“制作 Skill → 本地安装 → 真实项目调用 → 交付 HTML”的第一次闭环。

## 16. 三个最容易卡住的地方

- **终端提示找不到 Python**：依次试 `python --version` 与 `py --version`，后续统一使用能显示 Python 3 的那个命令。
- **重启后没有 `/x-ray`**：检查 `%USERPROFILE%\.codebuddy\skills\x-ray\SKILL.md` 是否存在，并确认 `SKILL.md` 文件名没有变成 `SKILL.md.txt`。
- **只有 JSON，没有 HTML**：JSON 只代表脚本层完成；继续等待 Agent 抽样阅读与生成报告，并查看对话里是否出现文件读取或写入失败。

还有一个容易被忽略的边界：X-ray **不修改目标项目源码**，但会在测试目录写出 `xray-data.json` 和 `xray-report.html`。如果用于公司仓库，还要先确认代码能否发送给当前模型服务，并检查报告里是否包含内部路径、仓库名或业务信息。

## 17. 收尾：我们真正做成了什么

![上集通关：X-ray 已在真实仓库中生成第一份项目解剖报告](images/30-episode-summary.png)<!-- display-width:520 -->

这一集完成了三件事：

1. 用 `SKILL.md + scripts + references + assets` 做出 X-ray；
2. 把它装进 CodeBuddy，并成功唤起 `/x-ray`；
3. 用一个真实仓库生成第一份可以打开的项目解剖报告。

这时的 X-ray 已经不是一段聊天记录，而是一套可以重复调用的工作方法。

更重要的是，你已经掌握了制作 Skill 的通用套路。哪怕你对“项目 X 光机”没有兴趣，也可以保留目录结构和验证方法，把里面的任务换成自己的需求。

下一集只增加一个变量：**把它发布到 GitHub，再从仓库重新安装，并换一个项目验证“换个地方还能用”。**
