build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
117 lines
5.3 KiB
Bash
117 lines
5.3 KiB
Bash
#!/usr/bin/env bash
|
||
# docs-sync-check.sh — 对账「本机文档镜像」与「服务器文档库」
|
||
#
|
||
# 用法:
|
||
# bash 07-scripts/docs-sync-check.sh # 报告双端差异
|
||
# DOCS_REMOTE=bt-server bash 07-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/", "tmp/", "05-交接单/.doing-", "05-交接单/.exec-lock", "05-交接单/.locks/")
|
||
SKIPD = (".git", "tmp", "05-交接单/.doing-", "05-交接单/.exec-lock", "05-交接单/.locks")
|
||
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 './tmp/*' ! -path '*/.locks/*' \
|
||
! -path './05-交接单/.doing-*' ! -path './05-交接单/.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 './tmp/*' ! -path '*/.locks/*' ! -path './05-交接单/.doing-*' ! -path './05-交接单/.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"
|