Files
dsh_shenxian/dsh-server-docs/scripts/docs-sync-check.sh
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

116 lines
5.1 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# docs-sync-check.sh — 对账「本机文档镜像」与「服务器文档库」
#
# 用法:
# bash scripts/docs-sync-check.sh # 报告双端差异
# DOCS_REMOTE=bt-server bash scripts/docs-sync-check.sh
#
# 可覆盖的环境变量:
# DOCS_LOCAL_DIR 本机镜像目录(默认 D:/github/dsh_shenxian/dsh-server-docs)
# DOCS_REMOTE ssh 别名或 host(默认 bt-server)
# DOCS_REMOTE_DIR 服务器文档目录(默认 /opt/dsh/docs)
#
# 退出码:0 = 完全一致;1 = 存在差异;2 = 环境/连接错误
set -uo pipefail
LOCAL_DIR="${DOCS_LOCAL_DIR:-D:/github/dsh_shenxian/dsh-server-docs}"
REMOTE="${DOCS_REMOTE:-bt-server}"
REMOTE_DIR="${DOCS_REMOTE_DIR:-/opt/dsh/docs}"
[ -d "$LOCAL_DIR" ] || { echo "ERROR: 本机目录不存在: $LOCAL_DIR" >&2; exit 2; }
tmp_l="$(mktemp)"; tmp_r="$(mktemp)"
trap 'rm -f "$tmp_l" "$tmp_r" 2>/dev/null || true' EXIT
# ── 本机哈希:优先「一次 Python 算完」───────────────────────────────
# 历史坑(2026-09-12 实测):Git Bash 下 `find | while read` **逐文件 spawn** `md5sum` / `cut`
# (132 文件 ≈ 264 次进程启动)→ 全量对账 **2 分 15 秒**,慢到被调用方(handoff-guard 信息模式)当成"挂死"。
# Python 一次遍历 → 秒级。探测顺序:$DSH_PY → python3 → python → 本机兜底绝对路径。
PY="${DSH_PY:-}"
if [ -z "$PY" ]; then
for c in python3 python; do
if command -v "$c" >/dev/null 2>&1; then PY="$c"; break; fi
done
fi
if [ -z "$PY" ] && [ -x "E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" ]; then
PY="E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe"
fi
hash_local() {
if [ -n "$PY" ]; then
( cd "$LOCAL_DIR" && "$PY" -c '
import os, sys, hashlib
try:
sys.stdout.reconfigure(encoding="utf-8", newline="\n")
except Exception:
pass
SKIPF = (".git/", "交接单/.doing-", "交接单/.exec-lock")
SKIPD = (".git", "交接单/.doing-", "交接单/.exec-lock")
rows = []
for root, dirs, files in os.walk("."):
# ⚠️ 不能用 os.path.relpath 拼 rel —— Windows 上它会把「末尾带点」的名字规范化掉
# (实测:`INDEX.md.bak-….` 变成无点的名字 → open 失败 → 该文件被静默漏掉 → 制造假差异)。
r = root.replace(os.sep, "/")
pfx = "" if r in (".", "") else (r[2:] if r.startswith("./") else r)
dirs[:] = [d for d in dirs if not (((pfx + "/") if pfx else "") + d).startswith(SKIPD)]
for fn in files:
rel = (pfx + "/" + fn) if pfx else fn
if fn == ".DS_Store" or any(rel.startswith(p) for p in SKIPF):
continue
if os.path.islink(rel):
continue # 与 find -type f 同口径
try:
with open(rel, "rb") as fh:
h = hashlib.md5(fh.read()).hexdigest()
except Exception:
# Windows 会把「末尾带点 / 保留名」等非常规文件名规范化掉 → open 直接 ENOENT。
# 用 \\?\ 前缀绕过,**必须拼未规范化的绝对路径**(os.path.abspath 会 strip 末尾点 → 前缀失效)。
try:
with open("\\\\?\\" + os.getcwd() + "\\" + rel.replace("/", "\\"), "rb") as fh:
h = hashlib.md5(fh.read()).hexdigest()
except Exception:
continue
rows.append((rel, h))
rows.sort(key=lambda r: r[0].encode("utf-8")) # 与 LC_ALL=C sort 同口径
for rel, h in rows:
print(rel + "\t" + h)
' )
else
echo "WARN: 未找到 python,回退逐文件 md5sum(会慢 ~2 分钟)" >&2
( cd "$LOCAL_DIR" && find . -type f ! -path './.git/*' ! -name '.DS_Store' \
! -path './交接单/.doing-*' ! -path './交接单/.exec-lock*' | LC_ALL=C sort | while read -r f; do
printf '%s\t%s\n' "${f#./}" "$(md5sum "$f" | cut -d' ' -f1)"
done )
fi
}
hash_remote() {
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" \
"cd '$REMOTE_DIR' 2>/dev/null && find . -type f ! -path './.git/*' ! -name '.DS_Store' ! -path './交接单/.doing-*' ! -path './交接单/.exec-lock*' | LC_ALL=C sort | while read -r f; do printf '%s\t%s\n' \"\${f#./}\" \"\$(md5sum \"\$f\" | cut -d' ' -f1)\"; done" \
2>/dev/null
}
hash_local > "$tmp_l"
hash_remote > "$tmp_r"
if [ ! -s "$tmp_r" ]; then
echo "ERROR: 无法读取服务器目录 $REMOTE:$REMOTE_DIR(ssh 失败或目录不存在)" >&2
exit 2
fi
awk -F'\t' '
NR==FNR { l[$1]=$2; next }
{ r[$1]=$2 }
END {
same=0
for (k in l) {
if (k in r) { if (l[k]==r[k]) same++; else { diff++; print "⚠️ 内容不一致 " k } }
else { only_l++; print "⬆️ 仅本地(待推送) " k }
}
for (k in r) if (!(k in l)) { only_r++; print "⬇️ 仅服务器(待拉取) " k }
printf "\n—— 汇总 ——\n 一致: %d\n 内容不一致: %d\n 仅本地: %d\n 仅服务器: %d\n 本地文件总数: %d / 服务器文件总数: %d\n", same, diff+0, only_l+0, only_r+0, length(l), length(r)
if ((diff+0)+(only_l+0)+(only_r+0) > 0) { print "\n结果: 存在差异 ❌"; exit 1 }
print "\n结果: 双端一致 ✅"
}
' "$tmp_l" "$tmp_r"