Skill: zhanghaiyu0511/gitlab-code-review (version 1.2.0)

--- name: gitlab-code-review description: GitLab 定时代码审查，自动获取新提交并生成 Review 报告。触发场景：(1) 用户说"配置 GitLab code review"、"设置代码审查"、"帮我监控 GitLab 提交"； (2) 用户提到 GitLab code review 或需要定时审查代码；(3) Heartbeat 执行定时检查时。 --- # GitLab Code Review 定时获取 GitLab 新提交，自动生成代码审查报告并推送通知。 ## 用户配置流程 当用户说「配置 GitLab code review」或类似意图时： ### 1. 引导用户输入配置（或复用现有配置） **首先检查现有配置**：读取 `workspace/.env` 文件，如果已存在 GitLab 相关配置，直接复用，无需重新询问。 如果配置不完整，逐个询问： ``` 我需要几个配置信息： 1. GitLab URL 是什么？（例如：https://gitlab.example.com） 2. 项目路径是什么？（group/project 格式） 3. 分支名称是什么？（默认 main） 4. Personal Access Token 是什么？ 需要的权限：read_api 获取方式：GitLab → 用户设置 → Access Tokens ``` ### 2. 创建/更新配置文件 收集完信息后，创建或更新 `workspace/.env` 文件： ``` GITLAB_URL=<用户输入的URL> GITLAB_TOKEN=<用户输入的Token> GITLAB_PROJECT=<用户输入的项目路径> GITLAB_BRANCH=<用户输入的分支> ``` ### 3. 创建 Cron 定时任务 **重要：使用 OpenClaw cron 命令创建定时任务** ```bash openclaw cron add \ --name "GitLab Code Review" \ --cron "0 * * * *" \ --stagger 5m \ --channel <当前channel> \ --to <当前用户ID> \ --message "执行 GitLab Code Review 定时任务：运行 python3 skills/gitlab-code-review/scripts/fetch_commits.py 获取新提交，检查 memory/pending_review_*.json。**有新提交**才进行 code review 并推送报告；**无新提交则静默结束，不推送任何消息，不调用模型审查**" ``` **参数说明**： - `--cron "0 * * * *"` - 每小时整点执行 - `--stagger 5m` - 5 分钟随机偏移，避免整点拥堵 - `--channel` - 当前对话的 channel（如 feishu） - `--to` - 当前用户的 ID **检查是否已存在**：先运行 `openclaw cron list`，如果已存在同名任务，跳过创建。 ### 4. 简化 HEARTBEAT.md 更新 `HEARTBEAT.md`，说明定时任务已配置为 cron： ```markdown # HEARTBEAT.md # Heartbeat 任务清单 GitLab Code Review 已配置为独立的 cron 定时任务（每小时整点执行），无需在 heartbeat 中处理。 --- **注意**： - 定时任务通过 `openclaw cron list` 查看 ``` ### 5. 执行首次审查 配置完成后，**立即执行首次审查**： 1. 运行 `python3 skills/gitlab-code-review/scripts/fetch_commits.py` 获取待审查提交 2. 检查 `memory/pending_review_*.json` 是否存在 3. 如果有待处理文件： - 按文件名排序（时间正序） - 逐个进行深度 code review - 生成完整报告 `memory/code_review_<short_id>.md` - 使用 message 工具推送到当前用户： - 发送简化版报告文本 - 发送完整报告文件（.md 附件） - 删除已处理的 pending 文件 4. 如果无新提交：静默结束，不推送任何消息 ### 6. 确认完成 ``` ✅ 配置完成！ - 已创建 .env 文件 - 已创建 cron 定时任务（每小时整点执行） - 已完成首次审查并推送报告 查看定时任务：openclaw cron list ``` ## 定时执行逻辑 Cron 任务触发时，agent 会执行以下步骤： 1. **检查配置**：读取 `workspace/.env` 2. **获取提交**：执行 `python3 skills/gitlab-code-review/scripts/fetch_commits.py` 3. **检查结果**：查看 `memory/pending_review_*.json` 4. **有新提交**： - 读取 pending 文件内容 - 进行深度 code review - 生成报告 `memory/code_review_<short_id>.md` - 推送简化版报告和完整报告文件到当前对话用户 - 删除 pending 文件 5. **无新提交**： - **静默结束，不推送任何消息** - **不调用模型审查代码** - 直接返回，不做任何输出 ## 推送方式 使用 message 工具推送两条消息： **1. 简化版报告文本**： ```json { "action": "send", "channel": "feishu", "target": "当前用户ID", "message": "简化版报告内容" } ``` **2. 完整报告文件**： ```json { "action": "send", "channel": "feishu", "target": "当前用户ID", "media": "memory/code_review_<short_id>.md", "filename": "code_review_<short_id>.md", "caption": "📋 完整的 Code Review 报告" } ``` ## 脚本说明 ### scripts/fetch_commits.py 获取 GitLab 未审查的提交。 **执行方式**： ```bash python3 skills/gitlab-code-review/scripts/fetch_commits.py ``` **首次运行**：只获取最近 2 个提交 **后续运行**：获取上次审查之后的所有新提交（最多 50 个） **依赖**： - 配置文件：`workspace/.env` - Python 包：`requests`, `python-dotenv` **输出**： - `memory/pending_review_<short_id>.json` - 待审查提交数据 - `memory/gitlab_review_state.json` - 审查状态记录 ## Code Review 维度 审查提交时，关注以下维度： ### 🔒 安全性 - SQL 注入、XSS、CSRF 漏洞 - 敏感信息泄露（密码、Token 硬编码） - 权限校验缺失 ### ⚡ 性能 - N+1 查询问题 - 循环内数据库操作 - 大数据集内存问题 - 缓存策略合理性 ### 📦 代码质量 - 命名规范与可读性 - 注释完整性 - 错误处理与边界检查 - 重复代码 ### 🧪 可测试性 - 依赖注入 - 单一职责 - 测试覆盖 ## 报告格式 完整报告保存到 `memory/code_review_<short_id>.md`。 简化版推送格式： ```markdown 📋 GitLab Code Review - 新提交审查 提交: <short_id> 标题: <title> 作者: <author> 时间: <time> 变更: <N> 个文件 ⚠️ 发现 <严重程度> 问题 🔴 主要问题: 1. <问题描述> 📄 <文件路径>:<行号范围> 💡 改进建议: 1. <建议内容> 📊 状态: <状态说明> 🔗 查看详情: <commit_url> ``` ## 故障排查 ### Skill 未识别 确保 skill 安装在正确位置： - ClawHub 安装：[REDACTED_SECRET] - 手动安装：解压到 `workspace/skills/` ### 脚本执行失败 ```bash # 检查依赖 pip3 install requests python-dotenv # 测试连接 python3 skills/gitlab-code-review/scripts/fetch_commits.py ``` ### Token 权限不足 确保 Token 有 `read_api` 权限： - GitLab → 用户设置 → Access Tokens - 勾选 `read_api` - 创建新 Token ### 无新提交被检测 检查 `memory/gitlab_review_state.json` 中的 `last_reviewed_commit_id`： ```bash cat memory/gitlab_review_state.json ``` 如果需要重新检测，删除该文件后重新运行脚本。 ### 定时任务未执行 检查 cron 任务状态： ```bash openclaw cron list ``` 手动触发测试： ```bash openclaw cron run <job-id> ```

path: INSTALL.md
content: # GitLab Code Review Skill 安装指南

自动监控 GitLab 提交，定时生成代码审查报告。

## 安装方式

### 方式一：通过 ClawHub CLI 安装（推荐）

```bash
# 1. 安装 ClawHub CLI
npm i -g clawhub

# 2. 登录（首次使用）
clawhub login

# 3. 安装 Skill
clawhub install gitlab-code-review
```

### 方式二：网页下载手动安装

如果 ClawHub CLI 安装不成功，可以通过网页下载：

1. 访问 https://clawhub.ai/zhanghaiyu0511/gitlab-code-review
2. 点击 **Download ZIP** 下载
3. 解压到 OpenClaw workspace 的 skills 目录：

```bash
# 解压到 workspace/skills/ 目录
cd ~/.openclaw/workspace/skills
unzip ~/Downloads/gitlab-code-review.zip
```

## 配置步骤

安装完成后，在 OpenClaw 中说：

> 「配置 GitLab code review」

然后回答以下问题：

| 问题 | 示例 |
|------|------|
| GitLab URL | `https://gitlab.example.com` |
| 项目路径 | `group/project` |
| 分支名称 | `main` 或 `dev` |
| Personal Access Token | `glpat-xxxxxxxx` |

**获取 Token**：
1. 打开 GitLab → 用户设置 → Access Tokens
2. 勾选 `read_api` 权限
3. 创建并复制 Token

配置完成后，整点将自动检查新提交并推送审查报告。

**注意**：首次运行只获取最近 2 个提交进行审查，之后每次获取新提交。

## 使用说明

### 自动执行

每小时整点自动检查新提交：
- 有新提交 → 生成审查报告 → 推送通知
- 无新提交 → 静默（不发送消息）

### 手动触发

```bash
python ~/.openclaw/workspace/skills/gitlab-code-review/scripts/fetch_commits.py
```

或在 OpenClaw 中说：
> 「检查 GitLab 是否有新提交」

## 配置文件

配置保存在 `workspace/.env`：

```
GITLAB_URL=https://gitlab.example.com
GITLAB_TOKEN=glpat-xxxxxxxx
GITLAB_PROJECT=group/project
GITLAB_BRANCH=main
```

**注意**：`.env` 文件包含敏感信息，已自动加入 `.gitignore`。

## 故障排查

### 提示缺少配置

```
Error: Missing config: GITLAB_TOKEN, GITLAB_PROJECT
```

**解决**：重新运行配置流程，或手动检查 `workspace/.env` 文件。

### 脚本执行失败

```bash
# 检查依赖
pip3 install requests python-dotenv

# 测试连接
python ~/.openclaw/workspace/skills/gitlab-code-review/scripts/fetch_commits.py
```

### Token 权限不足

确保 Token 有 `read_api` 权限。

### Token 已失效

如果看到 `Token was revoked` 错误，需要重新生成 Token。

## 文件说明

| 文件 | 说明 |
|------|------|
| `workspace/.env` | GitLab 配置（敏感） |
| `workspace/HEARTBEAT.md` | 定时任务配置 |
| `memory/gitlab_review_state.json` | 审查状态记录 |
| `memory/pending_review_*.json` | 待审查提交 |
| `memory/code_review_*.md` | 审查报告 |

## 更新 Skill

```bash
clawhub update gitlab-code-review
```

或重新从网页下载最新版本。

## 链接

- Skill 主页: https://clawhub.ai/zhanghaiyu0511/gitlab-code-review
- OpenClaw 文档: https://docs.openclaw.ai
- ClawHub 社区: https://clawhub.com


path: _meta.json
content: ownerId: kn7a96a359r6gthz049n2dv469829f5y
slug: gitlab-code-review
version: 1.1.0
publishedAt: 1772680922301

path: scripts/fetch_commits.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GitLab Code Review - Fetch Commits

Get unreviewed commits and save to pending files.
"""

import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')

import requests
import json
import urllib.parse
import os
from pathlib import Path
from datetime import datetime

# Clear any cached environment variables
for key in ["GITLAB_URL", "GITLAB_TOKEN", "GITLAB_PROJECT", "GITLAB_BRANCH"]:
    os.environ.pop(key, None)

# Load .env file
# Script: workspace/skills/gitlab-code-review/scripts/fetch_commits.py
# .env: workspace/.env
WORKSPACE_DIR = Path(__file__).parent.parent.parent.parent  # Go up 4 levels to workspace
ENV_FILE = WORKSPACE_DIR / ".env"

try:
    from dotenv import load_dotenv
    load_dotenv(ENV_FILE, override=True)
except ImportError:
    if ENV_FILE.exists():
        with open(ENV_FILE, "r") as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    key, value = line.split("=", 1)
                    os.environ[key.strip()] = value.strip()

# Read config from environment
GITLAB_URL = os.getenv("GITLAB_URL", "https://gitlab.example.com")
GITLAB_TOKEN = os.getenv("GITLAB_TOKEN", "")
PROJECT_PATH = os.getenv("GITLAB_PROJECT", "")
BRANCH = os.getenv("GITLAB_BRANCH", "main")

OUTPUT_DIR = WORKSPACE_DIR / "memory"
STATE_FILE = OUTPUT_DIR / "gitlab_review_state.json"


def validate_config():
    """Validate configuration"""
    missing = []
    if not GITLAB_TOKEN:
        missing.append("GITLAB_TOKEN")
    if not PROJECT_PATH:
        missing.append("GITLAB_PROJECT")

    if missing:
        print(f"Error: Missing config: {', '.join(missing)}")
        print(f"Please configure in {ENV_FILE}")
        return False
    return True


def load_state():
    """Load state"""
    if STATE_FILE.exists():
        with open(STATE_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"last_check": None, "last_reviewed_commit_id": None}


def save_state(state):
    """Save state"""
    state["last_check"] = datetime.now().isoformat()
    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    with open(STATE_FILE, "w", encoding="utf-8") as f:
        json.dump(state, f, ensure_ascii=False, indent=2)


def get_project_id(headers):
    """Get project ID"""
    encoded_path = urllib.parse.quote(PROJECT_PATH, safe="")
    url = f"{GITLAB_URL}/api/v4/projects/{encoded_path}"
    response = requests.get(url, headers=headers, timeout=10, proxies={"http": None, "https": None})
    data = response.json()
    if "id" not in data:
        print(f"Error: {data.get('error', 'Unknown error')}")
        print(f"Response: {response.text[:200]}")
        return None
    return data["id"]


def get_commits(project_id, headers, limit=50):
    """Get recent commits"""
    url = f"{GITLAB_URL}/api/v4/projects/{project_id}/repository/commits"
    response = requests.get(
        url, headers=headers, params={"per_page": limit, "ref_name": BRANCH}, timeout=10, proxies={"http": None, "https": None}
    )
    return response.json()


def get_commit_diff(project_id, commit_id, headers):
    """Get commit diff"""
    url = f"{GITLAB_URL}/api/v4/projects/{project_id}/repository/commits/{commit_id}/diff"
    response = requests.get(url, headers=headers, timeout=10, proxies={"http": None, "https": None})
    return response.json()


def prepare_commit_data(project_id, commit, headers):
    """Prepare commit data for review"""
    diffs = get_commit_diff(project_id, commit["id"], headers)

    review_data = {
        "project": PROJECT_PATH,
        "project_id": project_id,
        "branch": BRANCH,
        "commit": {
            "id": commit["id"],
            "short_id": commit["short_id"],
            "title": commit["title"],
            "author": commit["author_name"],
            "time": commit["created_at"],
            "message": commit["message"],
            "url": commit["web_url"],
        },
        "changes": [],
    }

    for diff in diffs:
        review_data["changes"].append(
            {
                "file": diff["new_path"],
                "new_file": diff.get("new_file", False),
                "deleted_file": diff.get("deleted_file", False),
                "renamed_file": diff.get("renamed_file", False),
                "diff": diff["diff"],
            }
        )

    return review_data


def main():
    if not validate_config():
        return

    headers = {"Private-Token": GITLAB_TOKEN}

    state = load_state()
    last_reviewed_id = state.get("last_reviewed_commit_id")

    print(f"GitLab URL: {GITLAB_URL}")
    print(f"Project: {PROJECT_PATH}")
    print(f"Branch: {BRANCH}")
    print(f"Last reviewed: {last_reviewed_id}")
    print()

    project_id = get_project_id(headers)
    if not project_id:
        return

    print(f"Project ID: {project_id}")

    commits = get_commits(project_id, headers)

    if not commits:
        print("No commits found")
        return

    print(f"Found {len(commits)} commits in branch")

    new_commits = []
    for commit in commits:
        if commit["id"] == last_reviewed_id:
            break
        new_commits.append(commit)

    # First run: only get the latest 2 commits
    if last_reviewed_id is None and len(new_commits) > 2:
        new_commits = new_commits[:2]
        print("(First run: only fetching latest 2 commits)")

    if not new_commits:
        print("No new commits to review")
        return

    new_commits.reverse()
    print(f"Found {len(new_commits)} new commit(s) to review")

    for i, commit in enumerate(new_commits, 1):
        print(f"Processing {i}/{len(new_commits)}: {commit['short_id']}")

        commit_data = prepare_commit_data(project_id, commit, headers)

        output_file = OUTPUT_DIR / f"pending_review_{commit['short_id']}.json"
        with open(output_file, "w", encoding="utf-8") as f:
            json.dump(commit_data, f, ensure_ascii=False, indent=2)

        print(f"  Saved: {commit['short_id']}")

    state["last_reviewed_commit_id"] = commits[0]["id"]
    save_state(state)

    print(f"\nDone: {len(new_commits)} commit(s) prepared for review")
    print(f"Updated last reviewed to: {commits[0]['short_id']}")


if __name__ == "__main__":
    main()


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