Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 71 additions & 35 deletions scripts/ocr-review.sh
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
#!/bin/bash
# ocr 评审包装脚本(zcode-advisor)
#
# 平台适配(Windows / Git Bash 与新机,2026-10-08):
# ① 公共 git 目录定位(worktree 下 .git 是文件,写死会 ENOTDIR)② GNU timeout 优先
# ③ python 优先于 WindowsApps 的 python3 壳 ④ Windows npm 全局路径补 PATH
# ⑤ timeout 退出码 124 计入「被强制终止」判定。
# 这几项是**脚本级能力**:必须落在模板里,否则下次跑 ocr-sync-reviewer.sh 时
# 会被整文件覆盖掉(standard 模式 = 模板内容直写)。
#
# 设计(与 ~/.dsh/AGENTS.md 一致):
# - 评审工具**不进 CI**:模型网关是内网地址(192.168.50.139),GitHub runner 够不着;
# 拦截靠 CI 的确定性检查,评审只作为 advisory 关卡。
# - 也**不定时跑**:只在每个 PR 合并前调用一次。
#
# 模型降级链(provider|model,与 ~/.opencodereview/config.json 的 custom_providers 对应):
# ⚠️ 链的**唯一真源**是 ~/.dsh/templates/ocr-review/chain-public(运行时直接读它)。
# 本文件里那段 CHAIN 数组只是读不到链文件时的**兜底快照**,别在这里找当前顺序——
# 它必然滞后(改链不必改脚本,也就没人会记得同步它)。
# 改链:编辑 chain-public 再跑 bash ~/.dsh/bin/ocr-sync-reviewer.sh 刷快照。
# 这里不再逐条列举模型名与顺序:链外置的全部意义就是「枚举会过期」,再抄一份
# 只会制造第二处会撒谎的注释(2026-10-06 实测:注释写着主棒 deepseek,运行时首跳
# 其实是 chain-public 的 minimax-m3)。各模型的可用性/耗时/成本结论写在链文件里,
# 那才是它们生效的地方。
# 【2026-10-04】隐私铁律暂缓(用户拍板):私有仓也可用本链(私有副本用 ocr-review.sh.private
# 变体,链走 chain-private,同样允许 cloud 条目)。
# ⚠️ kimi-k3 不支持工具调用(三次探针无 tool_calls)→ 不能当 ocr 模型,勿加回。
# 模型降级链:**唯一真源是 ~/.dsh/templates/ocr-review/chain-public**(本脚本下方的
# CHAIN=(...) 只是读不到链文件时的兜底快照,由 ocr-sync-reviewer.sh 生成,勿手改)。
# 此处不再逐条列举模型名与顺序——枚举必然过期(2026-10-06 实测:本注释曾写"主棒
# deepseek",而运行时首跳早已是链文件里的另一个模型)。要改链请改链文件。
# 【2026-10-04】隐私铁律暂缓(用户拍板):私有仓也可用云端条目(私有副本用
# ocr-review.sh.private 变体,链走 chain-private,同样允许 cloud 条目)。
# ⚠️ 同站 kimi-k3 不支持工具调用(2026-10-04 三次探针无 tool_calls)→ 不能当 ocr 模型,勿加回。
# ⚠️ lucky / lucky-gem 需在 config.json 配 user_agent + headers.User-Agent,否则被
# Cloudflare 按 UA 拦截返回 403(不是服务失效)。
# 用法:
# ./scripts/ocr-review.sh # 默认基准 origin/dev(不存在则回退 main)
# ./scripts/ocr-review.sh v1.8.8 # 对比 tag / 分支
Expand All @@ -28,6 +32,13 @@ set -uo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT" || exit 1

# 公共 git 目录:worktree 下 .git 是文件不是目录,写死会失败(AGENTS.md §11 第 11 条)。
GIT_COMMON_DIR="$(git rev-parse --git-common-dir 2>/dev/null || echo .git)"
case "$GIT_COMMON_DIR" in
/*|?:*) ;; # 已是绝对路径(worktree 常见)
*) GIT_COMMON_DIR="$REPO_ROOT/$GIT_COMMON_DIR" ;;
esac
Comment on lines +36 to +40

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · high
在 Windows 环境下,$REPO_ROOT 与路径拼接时,可能存在反斜杠转义问题。例如,$REPO_ROOT/subdir 在 Windows 中应为 $REPO_ROOT\subdir。建议在拼接路径时使用 cygpath 工具或显式替换 / 为 `) 以确保兼容性。


# ── 并发锁(实测教训)──
# pre-push hook 每次推送都会起一个评审,而完整评审要 5–15 分钟。
# 连续推送(或 tag + main 分开推)会让多个评审同时跑:既争抢上游配额,
Expand All @@ -38,7 +49,7 @@ cd "$REPO_ROOT" || exit 1
# 锁内含 pid + 起始时间;发现锁时:
# - 持锁进程仍活着 → 提示并退出(不重复评审)
# - 持锁进程已死(崩溃/被 kill)→ 视为陈旧锁,接管
LOCK_DIR="$REPO_ROOT/.git/ocr-review.lock"
LOCK_DIR="$GIT_COMMON_DIR/ocr-review.lock"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · high
在 Windows 环境下,LOCK_DIR 路径构造中使用 / 作为分隔符可能导致路径解析失败。建议使用 cygpath 工具将路径转换为 Windows 格式,或者使用反斜杠 `) 进行拼接。

LOCK_INFO="$LOCK_DIR/info"
# 只有**自己拿到锁**才允许在退出时删锁。OCR_REVIEW_FORCE=1 整段跳过、根本不取锁,
# 若无条件删就会把另一个正在跑的评审的锁连根拔掉——而并行评审正是 FORCE 的用途,
Expand All @@ -55,7 +66,7 @@ if [ -z "${OCR_REVIEW_FORCE:-}" ]; then
[ -f "$LOCK_INFO" ] && HOLD_PID="$(sed -n 's/^pid=//p' "$LOCK_INFO" 2>/dev/null)"
if [ -n "$HOLD_PID" ] && kill -0 "$HOLD_PID" 2>/dev/null; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · high
在 Windows Git Bash 或 MSYS 环境下,kill -0 命令可能不可靠,因为它依赖于 Unix 系统的进程管理机制。建议使用 tasklist 或 ps 命令来检查进程存活状态,以确保跨平台兼容性。

echo "[ocr-review] 已有评审在进行(pid ${HOLD_PID}),本次跳过以免重复消耗。"
echo "[ocr-review] 已有报告:$REPO_ROOT/.git/ocr-review-last.txt"
echo "[ocr-review] 已有报告:$GIT_COMMON_DIR/ocr-review-last.txt"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 Windows 环境下,GIT_COMMON_DIR 变量拼接的路径可能包含空格或特殊字符,导致路径解析错误。建议使用引号包裹路径,并确保路径分隔符正确。例如,可以使用 "$GIT_COMMON_DIR/ocr-review-last.txt" 来确保路径被正确解析。

echo "[ocr-review] 如需强制并行,设 OCR_REVIEW_FORCE=1"
exit 0
fi
Expand All @@ -81,7 +92,10 @@ if ! command -v ocr >/dev/null 2>&1; then
fi
# 再按版本号倒序扫(字典序会把 v10 排在 v20 之前,可能命中陈旧的全局安装)
if ! command -v ocr >/dev/null 2>&1; then
for candidate in $(ls -1d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -Vr) /usr/local/bin /opt/homebrew/bin; do
for candidate in \
$(ls -1d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -Vr) \
/usr/local/bin /opt/homebrew/bin \
"${APPDATA:-$HOME/AppData/Roaming}/npm" "$HOME/AppData/Roaming/npm"; do
if [ -x "$candidate/ocr" ]; then
PATH="$candidate:$PATH"
break
Expand All @@ -97,6 +111,19 @@ fi

AGENT_MODEL_FILE="$HOME/.opencodereview/agent-model"

# python 解释器:Windows 上 python3 常是 WindowsApps 应用执行别名(跑起来只会弹应用商店),
# 故优先真实安装的 python;但内嵌脚本用了 f-string,**只接受 Python 3** —— 若 python 指向
# Python 2 就跳过它继续找 python3,不能靠"命令存在"就认定可用。
PY=""
for _cand in python python3; do
_p="$(command -v "$_cand" 2>/dev/null || true)"
[ -n "$_p" ] || continue
if "$_p" -c 'import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)' 2>/dev/null; then
PY="$_p"; break
fi
done
[ -n "$PY" ] || { echo "错误:找不到可用的 Python 3 解释器(渲染评审结果需要)" >&2; exit 1; }

# 校验 ref 是否真实存在(避免把非法参数带进 ocr)
require_ref() {
local ref="$1"
Expand Down Expand Up @@ -144,17 +171,22 @@ AGENT_MODEL="$(tr -d '[:space:]' < "$AGENT_MODEL_FILE" 2>/dev/null || true)"
CHAIN_FILE="${OCR_REVIEW_CHAIN_FILE:-$HOME/.dsh/templates/ocr-review/chain-public}"
CHAIN_SCOPE="public" # public 副本:cloud 条目合法,但仍以链文件为唯一真源
CHAIN=(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 CHAIN 数组中,虽然注释明确指出该数组是兜底快照,但新增的模型列表(如 space-bunny、lucky-gem 等)没有对应的详细说明或链接到外部文档。建议在注释中补充说明这些模型的来源、适用场景或注意事项,以便于维护者快速理解和调整。例如,可以参考旧注释中的格式,说明是否支持工具调用、是否有特殊配置要求等。

"nas-hy4|deepseek-v4.1-flash"
"runanytime|z-ai/glm-5.2"
"nas-octopus|glm-5.3-flash"
"nas-hy4|hy3"
"nas-hy4|hy4-preview-f"
"nas-hy4|space-bunny"
"lucky-gem|gemini-3.6-flash"
"x666|ministral-14b-latest"
Comment on lines +174 to 176

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 CHAIN 数组中,新增的条目 "nas-hy4|space-bunny" 和 "lucky|stealth/space-bunny-alpha" 可能存在重复或混淆。建议确认 space-bunny 是否与 stealth/space-bunny-alpha 完全不同,或者是否应合并或删除其中一个以避免冗余。同时,确保所有条目在实际使用时都符合预期的模型名称和配置。

"runanytime-astra|gpt-6-astra"
"runanytime|z-ai/glm-5.3"
"lucky|stealth/space-bunny-alpha"
"lucky|step-5-preview"
"daigua|gpt-6-sol"
"nas-hy4|gemini-3.8-flash"
"nas-octopus|gemini-3.7-flash"
"nas-hy4|deepseek-v4.1-flash"
"nas-hy4|deepseek-v4.1-flash-sg"
Comment on lines +182 to +183

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 CHAIN 数组中,新增的条目 "nas-hy4|deepseek-v4.1-flash-sg" 与现有的 "nas-hy4|deepseek-v4.1-flash" 非常相似,仅在末尾多了 -sg 后缀。建议明确说明这两个条目之间的区别,或者确认是否应合并或删除其中一个以避免混淆。

"nas-octopus|deepseek-v4-flash-0731"
"runanytime|z-ai/glm-5.2"
"runanytime|z-ai/glm-5.3"
"nas-hy4|glm-5.3-flash"
"nas-hy4|hy3"
"nas-hy4|hy4-preview-f"
)
# 运行时优先读链文件:读不到 / 解析出 0 条 → 保留上面的快照(脚本被单独拷走也跑得起来)。
_chain_found=()
Expand Down Expand Up @@ -215,16 +247,9 @@ OUT_JSON="$(mktemp "${TMPDIR_OCR%/}/ocr-review-XXXXXX" 2>/dev/null)" || {
}
[ -n "$OUT_JSON" ] || { echo "错误:mktemp 未返回路径" >&2; exit 1; }
ATTEMPT_LOG=""
# 注意:这个 trap 会覆盖前面的锁清理 trap,所以 rm -rf "$LOCK_DIR" 必须在这里再做一次,
# 否则锁目录永不消失,之后每次运行都误报"陈旧锁"(虽然能接管,但看着像坏了)。
# OCR_REVIEW_RAW_OUT(可选,由调用方提供,例如 pre-push hook):把 ocr 的**原始 JSON**
# 另存一份。它是权威产物(下面的渲染表由它生成),而 OUT_JSON 只是 mktemp 临时文件、
# 退出时被本函数删掉 —— 此前评审成功后原始 JSON 一并消失,事后无法复核或重渲染。
#
# 两道守卫,缺一不可:
# - `$CHOSEN` 非空 = 确有一跳产出完整评审。**不能**只看 OUT_JSON 非空:链全失败时
# OUT_JSON 里留着最后一跳的 partial/无效输出,照拷会把上一份好结论覆盖成半成品。
# - 命中锁而提前退出的进程根本走不到这里(锁检查在 OUT_JSON 创建之前)。
# 注意:这个 trap 会覆盖前面的锁清理 trap,所以锁回收必须在这里再做一次。
# 但只在**自己持锁**时回收(LOCK_ACQUIRED)——FORCE=1 根本没取锁,无条件删会把
# 并行评审的锁拆掉。
cleanup() {
if [ -n "${OCR_REVIEW_RAW_OUT:-}" ] && [ -n "${CHOSEN:-}" ]; then
Comment on lines 253 to 254

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 cleanup 函数中,虽然增加了对 LOCK_ACQUIRED 的检查,但未明确说明在 LOCK_ACQUIRED 未设置的情况下,是否会导致锁目录无法被正确回收。建议在 cleanup 函数中增加日志输出,以便在调试时明确知道锁回收的状态。例如,可以在 LOCK_ACQUIRED 未设置时输出一条警告日志,提示锁回收被跳过。

# 这里不再检查 OUT_JSON 非空:$CHOSEN 只在 valid_output 通过后赋值,而 valid_output
Expand Down Expand Up @@ -289,7 +314,7 @@ trap cleanup EXIT INT TERM
# 误拒完整结果、触发无谓重跑。覆盖完整性以 status 为准。
valid_output() {
[ -s "$OUT_JSON" ] || return 1
python3 - "$OUT_JSON" <<'PYCHECK' >/dev/null 2>&1
"$PY" - "$OUT_JSON" <<'PYCHECK' >/dev/null 2>&1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · medium
在 valid_output 函数中,使用 "$PY" 执行 Python 代码块时,可能存在 Python 版本兼容性问题。建议明确指定 Python 版本(如 python3),并确保 encoding="utf-8" 在所有 Python 版本下都能正确处理。

import json, sys
try:
with open(sys.argv[1], encoding="utf-8") as fh:
Expand Down Expand Up @@ -323,6 +348,17 @@ run_with_deadline() {
local secs="$1"; shift
case "$secs" in ''|*[!0-9]*) "$@"; return $? ;; esac
[ "$secs" -gt 0 ] || { "$@"; return $?; }
# 新机(Windows Git Bash / 装了 coreutils 的机器)有 GNU timeout,优先用它:
# 语义清晰(超时退出码 124),且 MSYS 会一并带走子进程树。
#
# ⚠️ 必须用 `timeout --version` 探测而不是 `command -v timeout`:Windows 自带
# C:/Windows/System32/timeout.exe 是同名异物(等 N 秒的延时命令,不包装别的命令),
# 命中它会把 `timeout 900 ocr review …` 当成非法参数静默跑偏。
Comment on lines +354 to +356

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · low
在 run_with_deadline 函数中,新增的 GNU timeout 检测逻辑使用了 timeout --version 命令来判断工具是否可用。然而,Windows 自带的 timeout 命令(位于 C:/Windows/System32/timeout.exe)可能会误导检测结果,因为它不支持 --version 参数。虽然代码中已经考虑了这一点,但建议进一步明确说明该逻辑的限制条件,并考虑添加更详细的错误处理或日志输出,以便在检测失败时提供更明确的反馈。

# GNU 版有 --version,Windows 版没有(实测其退出码与输出均不同)。
if timeout --version >/dev/null 2>&1; then
timeout "$secs" "$@"
return $?
fi
Comment on lines +351 to +361

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maintainability · medium
在 run_with_deadline 函数中,虽然优先使用了 GNU timeout 命令,但缺少对 Windows 环境下 timeout 命令的明确兼容性验证。虽然代码中已经处理了 GNU timeout 的优先级,但建议在 Windows 环境下进一步验证 timeout 命令的可用性和行为一致性,以确保超时退出码(如 124)在所有平台上都能正确处理。

Comment on lines +358 to +361

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
在 run_with_deadline 函数中,新增的 GNU timeout 检测逻辑虽然提高了 Windows 环境下的兼容性,但未处理非 GNU timeout 工具的情况。如果系统中既没有 GNU timeout 也没有其他超时工具,脚本可能会进入无限等待或意外行为。建议淽加更健壮的超时处理机制,例如回退到原有的 shell 实现或使用其他可靠的超时工具。

set -m
"$@" &
local cmd_pid=$!
Expand Down Expand Up @@ -391,7 +427,7 @@ for entry in "${CHAIN[@]}"; do
break
fi
case "$HOP_RC" in
137|143) echo " ⚠ 本跳到 ${HOP_LIMIT}s 上限被强制终止(退出码 ${HOP_RC}),降级到下一个" ;;
124|137|143) echo " ⚠ 本跳到 ${HOP_LIMIT}s 上限被强制终止(退出码 ${HOP_RC}),降级到下一个" ;;
Comment on lines 429 to +430

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · medium
在处理超时退出码时,124(GNU timeout)与 137/143(进程组信号)的逻辑在 Windows 环境下可能不一致。建议明确区分不同平台的超时退出码处理逻辑,并确保在所有平台上都能正确识别和处理。

esac
# 保留该次失败输出尾部,供全失败时排查(此前一律丢进 /dev/null)
if [ -n "$ATTEMPT_LOG" ]; then
Expand Down Expand Up @@ -426,7 +462,7 @@ fi
# { status, llm, message, summary, tool_calls, comments[], groups[], session_id, manifest, retry_report }
# comments[] 元素键:path, content, suggestion_code, existing_code, start_line, end_line, thinking, category, severity
# 渲染失败必须让脚本非零退出(set -e 未开启,故显式传递)。
if ! python3 - "$OUT_JSON" "$CHOSEN" <<'PY'
if ! "$PY" - "$OUT_JSON" "$CHOSEN" <<'PY'
import json, sys

path, chosen = sys.argv[1], sys.argv[2]
Expand Down
Loading