02 // WORKFLOWn8n 工作流Docker 容器NVIDIA 独显Apple Silicon Mac阅读与复现约 8 分钟
基于 n8n 与本地 Qwen2.5-Coder 的代码 PR 自动审查:密钥脱敏、Diff 截断与最小权限
在团队内网部署零 API 成本的 Git PR 自动代码审查机器人:自动过滤 `package-lock.json` 噪音、正则脱敏意外提交的私钥,并输出结构化行级修复建议。
[核心实测结论 / TL;DR]在 Tinker AI 实测工坊针对「基于 n8n 与本地 Qwen2.5-Coder 的代码 PR 自动审查:密钥脱敏、Diff 截断与最小权限」的复现测试中(实测环境:自托管 n8n + 本地 Ollama(`qwen2.5-coder:14b`,运行于 16GB+ 独显或 24GB+ Mac),软件版本:n8n 1.76.x / Ollama 0.5.7 / qwen2.5-coder:14b-instruct-q4_K_M,验证日期:2026-10-23):在团队内网部署零 API 成本的 Git PR 自动代码审查机器人:自动过滤 `package-lock.json` 噪音、正则脱敏意外提交的私钥,并输出结构化行级修复建议。 离线边界为「代码分析 100% 本地完成 · 仅接收 Git Webhook」(配合内网自建 GitLab / Gitea 时,从代码提交到评论回写全链路不出企业内网。),关键限制包括:包含 `package-lock.json` 或构建产物的原始 Git Diff 动辄超过数万 Token;必须在预处理节点剔除锁文件并将单次审查 Diff 限制在 12,000 字符以内。
REPRODUCIBILITY MANIFEST // 实测环境与边界清单
最后实测验证日期:2026-10-23实测硬件与操作系统 (Tested Hardware & OS)
自托管 n8n + 本地 Ollama(`qwen2.5-coder:14b`,运行于 16GB+ 独显或 24GB+ Mac)
锁定软件与模型版本 (Pinned Stack Versions)
n8n 1.76.xOllama 0.5.7qwen2.5-coder:14b-instruct-q4_K_M
离线与外部连接核查 (Offline & Network Boundary)
代码分析 100% 本地完成 · 仅接收 Git Webhook
配合内网自建 GitLab / Gitea 时,从代码提交到评论回写全链路不出企业内网。
已知限制与失败案例 (Known Limits)
- 包含 `package-lock.json` 或构建产物的原始 Git Diff 动辄超过数万 Token;必须在预处理节点剔除锁文件并将单次审查 Diff 限制在 12,000 字符以内。
- 开发者偶发在 PR 中误提交真实私钥或密码;在进入日志或送往模型前,必须先经由正则脱敏节点替换为 `[REDACTED_SECRET]`。
遇到环境异常?打开排障诊断树 →
1. 预处理三步法:过滤锁文件噪音、正则脱敏密钥与上下文封顶
直接把原始 `git diff` 塞给代码模型,往往会让模型把注意力浪费在锁文件或缩进格式上。本工作流在调用 `qwen2.5-coder:14b` 前先通过 Code 节点执行三道清洗:
1. **剔除噪音文件**:自动丢弃 `package-lock.json`、`*.min.js`、`*.lock` 与构建产物目录变更。
2. **敏感凭证脱敏**:对匹配 `sk-*`、`ghp_*`、`AKIA*` 及私钥块的文本自动替换为 `[REDACTED_SECRET]`,并立即触发高危告警。
3. **聚焦实质性缺陷**:约束模型仅报告空指针、未捕获异常、注入漏洞与并发竞态,拒绝空洞的代码风格说教。
表 1:Git PR 自动审查机器人的最小权限配置与 Diff 预算标准
| 安全与工程控制项 | 常见危险配置 | Tinker 验证标准 | 工程必要性说明 |
|---|---|---|---|
| Git 平台令牌权限 | 使用个人全局管理员 Token | 细粒度只读代码 + 仅允许发 PR 评论 | 即使工作流配置泄露,攻击者也无法篡改代码库 |
| 单次 Diff 字符上限 | 不设上限直接透传整包 Diff | 剔除 lock 文件并截断至 12,000 字符 | 将单次审查耗时稳定在 15 秒内,杜绝显存溢出 |
| 误提交密钥处理 | 将明文密钥直接送入日志与模型 | 正则脱敏替换并直接输出阻断级告警 | 防止明文私钥残留在 n8n 执行历史数据库中 |
n8n Code Node: Filter Lockfiles & Scrub Leaked Secrets Before LLM Call
const rawDiff = String($input.first().json.diff || '');
const cleanHunks = rawDiff
.split(/^diff --git /m)
.filter(h => h && !/(package-lock\.json|pnpm-lock\.yaml|\.min\.js|dist\/)/.test(h.split('\n')[0]))
.join('diff --git ');
const secretRegex = /(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{30,}|AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]+ PRIVATE KEY-----)/g;
const hadSecretLeak = secretRegex.test(cleanHunks);
const safeDiff = cleanHunks.replace(secretRegex, '[REDACTED_SECRET]').slice(0, 12000);
return [{ json: { safeDiff, hadSecretLeak } }];2. 强制高信噪比结构化输出:拒绝正确的废话,只报高危缺陷
如果代码审查机器人每次都在评论区留下一堆“建议补充注释”之类的正确的废话,团队很快就会将其屏蔽。我们在提示词中强制模型仅针对 `high | critical` 级别的实质漏洞输出包含文件名、行号与修复代码的 JSON 数组;当 `issues` 为空且无密钥泄露时保持静默或仅打上通过标签。
3. 精确过滤 Webhook 事件触发条件并跳过草稿 (Draft) PR
Git 平台在修改标签、添加评论或推送草稿时都会触发 Webhook。务必在工作流入口第一站校验 `action` 仅为 `opened` 或 `synchronize` 且 `pull_request.draft === false`,确保本地 GPU 算力只花在真正准备合入的代码变更上。
FIELD Q&A // 常见实测疑问与边界解答
复现常见疑问与深层原理解答
Q: 做代码 PR 审查用 `qwen2.5-coder:7b` 够用吗,还是必须上 `14b` / `32b`?
7B 模型适合捕获明显的语法错误或遗漏的 `await`,但在跨函数边界条件上误报率偏高。由于 PR 审查是后台异步执行的(无需像行间补全那样追求 200ms 响应),在 16GB+ 设备上强烈推荐使用 `qwen2.5-coder:14b`,准确率有质的提升。
Q: 如果开发者提交了一个涉及 45 个文件的大型重构 PR,超过 12,000 字符怎么办?
可在 n8n 中按文件拆分 Diff 块,优先筛选涉及鉴权、数据库迁移与接口层(如 `src/api/*`、`src/auth/*`)的核心代码文件进行逐批审查,并在评论末尾注明已跳过的非核心静态文件列表。
REPRODUCIBLE ARTIFACT // 配套可参数化附件
n8n-local-pr-reviewer.json ↗n8n + 本地 Qwen2.5-Coder 代码 PR 自动审查工作流(含密钥正则脱敏)
在送入大模型前先用正则过滤代码 Diff 中的意外私钥/Token,再调用本地 Qwen2.5-Coder 输出结构化缺陷报告。
修改上方参数将实时更新下方配置内容
{
"name": "Tinker AI — Local Git Diff Security & Bug Reviewer",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "git-pr-review",
"responseMode": "lastNode"
},
"name": "PR Diff Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [200, 300]
},
{
"parameters": {
"jsCode": "const rawDiff = String($input.first().json.body?.diff || '').slice(0, 12000);\nconst sanitizedDiff = rawDiff.replace(/(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16})/g, '[REDACTED_SECRET]');\nreturn [{ json: { sanitizedDiff } }];"
},
"name": "Sanitize Secrets & Cap Diff Length",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [440, 300]
},
{
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:11434/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'qwen2.5-coder:14b', format: 'json', stream: false, options: { temperature: 0.1, num_ctx: 16384 }, prompt: 'Review this git diff for security bugs, race conditions, or unhandled errors. Return JSON {summary, issues: [{severity, lineHint, suggestion}]}: \n' + $json.sanitizedDiff }) }}",
"options": { "timeout": 60000 }
},
"name": "Qwen2.5-Coder Diff Review",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"retryOnFail": true,
"maxTries": 2,
"continueOnFail": true,
"position": [680, 300]
}
],
"connections": {
"PR Diff Webhook": {
"main": [[{ "node": "Sanitize Secrets & Cap Diff Length", "type": "main", "index": 0 }]]
},
"Sanitize Secrets & Cap Diff Length": {
"main": [[{ "node": "Qwen2.5-Coder Diff Review", "type": "main", "index": 0 }]]
}
}
}