# ai-review workflow 的 `contains('/ai-review')` 守卫被 AIBot 自评论中的文件路径误触

**Date**: 2026-05-15
**Tags**: gitea-actions, workflow-bug, self-trigger-loop, substring-matching
**Related**: PR #380 / #171 / #259

## 症状

PR #380 显示有 **8 条 AI Review 评论**，但实际只有 **4 个 commit**。最初以为是 review 反复指控同一问题，深查后发现 4 条是"幽灵评论"——同一 commit 的 head SHA 上跑了多轮 review，且大多 `diff 0 bytes`。

## 触发链（自触发循环）

```
1. PR 推新 commit → pull_request synchronize → ai-review.yml 跑
                                              ↓
2. runner 把 review 评论写回 Gitea PR（评论 body 引用文件路径
   `scripts/ops/ai-review-runner.sh` 和 `.gitea/workflows/ai-review.yml`）
                                              ↓
3. Gitea Actions: issue_comment created 事件触发 ai-review.yml
                                              ↓
4. workflow `if` 守卫: `contains(github.event.comment.body, '/ai-review')`
   ↓ contains 是子串匹配，路径里有 `/ai-review` 子串 → 守卫通过！
                                              ↓
5. runner 又跑一次 → 又写一条评论 → 又触发 issue_comment → goto 2
```

被以下两个机制压平，没真的无限循环：

- runner 幂等短路 `CURRENT_DIFF_HASH == PREV_DIFF_HASH`（但 `!= issue_comment` 时不生效）
- race condition + LLM 自己识别为"沿用上轮"返回零增量 finding，状态稳态后不再变化

## 根因

`contains()` 是纯子串匹配。`/ai-review` 是 `/ai-review-runner` / `/ai-review.yml` / `/ai-review-state-b64` 的子串。

意图：让用户在 PR 评论里敲 `/ai-review` 手动触发 review。
落地：任何提到 `ai-review-*` 路径的评论都被触发。

## 修法（防御深度）

```yaml
if: |
  ... ||
  (github.event_name == 'issue_comment' &&
   github.event.issue.pull_request != null &&
   github.event.comment.user.login != 'AIBot' &&         # 排除 bot 自己
   startsWith(github.event.comment.body, '/ai-review'))  # 命令必须在首字符
```

两层防御：
1. **`startsWith` 替代 `contains`**：slash 命令本来就该锚定在首字符（人敲命令是 `/ai-review` 开头，不是嵌在路径中间）
2. **排除 AIBot 评论**：防御深度——未来加新 bot 不会重蹈，也防别的子串巧合（如 PR description 里有 `/ai-review` 段引用）

## 教训

### 1. 子串匹配 + 自反馈循环 = 隐性 bug

Workflow / chatbot 的命令解析用 `contains()` 是常见坑——只要 bot 自己的输出有可能包含命令字符串，就形成循环。规则：**所有 bot 命令解析都用 prefix/regex anchor，不要 contains。** 同时**bot 触发的事件不应触发同一 bot**（user.login != bot）。

### 2. Gitea Actions `if` 函数行为跟 GitHub Actions 一致

- `contains(haystack, needle)` = 纯子串
- `startsWith` / `endsWith` = 锚定到边界
- 没有 regex 函数；需要正则得拆 step + grep

### 3. 用 actions/runs?event=issue_comment 排查"幽灵"评论

诊断套路（可复用）：
1. 数 PR 评论里 bot 输出有几条
2. 比对 `actions/runs` 里同 workflow 的 run 数
3. 不匹配 → 必有"非 pull_request 触发"的 run，查 `event=issue_comment` / `workflow_dispatch`
4. 找到触发评论 → 看是否 bot 自己

## 影响范围

- 本 workflow：之前每条 AI review 评论都会触发 1-2 次幽灵 review（约 2x 成本）
- `weekly-retro.yml`：也有 `issue_comment` 触发器，应同样审查（暂未发现 weekly-retro bot 在自己评论中出现 `/weekly-retro` 之类的子串）

## 验证

修复后开新 PR 观察：每 commit 应只产生 1 条 ai-review 评论，没有 issue_comment-event 的幽灵 run。
