Files
2026-07-12 21:26:08 +08:00

353 lines
8.9 KiB
Bash
Executable File

#!/bin/bash
# llm-wiki 缓存脚本
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/shared-config.sh"
usage() {
cat <<'EOF'
用法:
bash scripts/cache.sh check <file>
bash scripts/cache.sh update <file> <source_page>
bash scripts/cache.sh invalidate <file>
EOF
}
require_file() {
local file_path="$1"
[ -n "$file_path" ] || {
usage
exit 1
}
[ -f "$file_path" ] || {
echo "文件不存在:$file_path" >&2
exit 1
}
}
find_wiki_root() {
local file_path="$1"
local dir parent
dir="$(cd "$(dirname "$file_path")" && pwd)"
while true; do
if [ -f "$dir/.wiki-cache.json" ] || [ -f "$dir/.wiki-schema.md" ]; then
printf '%s\n' "$dir"
return 0
fi
parent="$(dirname "$dir")"
[ "$parent" = "$dir" ] && return 1
dir="$parent"
done
}
cache_file_path() {
printf '%s/.wiki-cache.json\n' "$1"
}
ensure_cache_file() {
local cache_file="$1"
if [ ! -f "$cache_file" ]; then
cat > "$cache_file" <<'EOF'
{
"version": 1,
"entries": {}
}
EOF
fi
}
relative_path() {
require_python_cmd
"$PYTHON_CMD" - "$1" "$2" <<'PY'
import os
import sys
print(os.path.relpath(os.path.realpath(sys.argv[2]), os.path.realpath(sys.argv[1])))
PY
}
normalized_source_page() {
local wiki_root="$1"
local source_page="$2"
if [ -z "$source_page" ]; then
printf '%s\n' ""
return 0
fi
case "$source_page" in
/*)
require_python_cmd
"$PYTHON_CMD" - "$wiki_root" "$source_page" <<'PY'
import os
import sys
wiki_root = os.path.realpath(sys.argv[1])
source_page = os.path.realpath(sys.argv[2])
try:
common = os.path.commonpath([wiki_root, source_page])
except ValueError:
common = ""
if common == wiki_root:
print(os.path.relpath(source_page, wiki_root))
else:
print(sys.argv[2])
PY
;;
*)
printf '%s\n' "$source_page"
;;
esac
}
file_hash() {
require_python_cmd
"$PYTHON_CMD" - "$1" "$2" <<'PY'
import hashlib
import pathlib
import sys
relative_path = sys.argv[1].encode("utf-8")
file_path = pathlib.Path(sys.argv[2])
content = file_path.read_bytes()
digest = hashlib.sha256(relative_path + b"\0" + content).hexdigest()
print(f"sha256:{digest}")
PY
}
cache_check() {
local file_path="$1"
local wiki_root cache_file relative_path_value current_hash result
require_file "$file_path"
wiki_root="$(find_wiki_root "$file_path")" || {
echo "未找到知识库根目录:$file_path" >&2
exit 1
}
cache_file="$(cache_file_path "$wiki_root")"
if [ ! -f "$cache_file" ]; then
printf 'MISS\n'
return 0
fi
require_python_cmd
relative_path_value="$(relative_path "$wiki_root" "$file_path")"
current_hash="$(file_hash "$relative_path_value" "$file_path")"
result="$(
"$PYTHON_CMD" - "$cache_file" "$wiki_root" "$relative_path_value" "$current_hash" <<'PY'
import hashlib
import json
import os
import pathlib
import sys
cache_file, wiki_root, relative_path, current_hash = sys.argv[1:5]
with open(cache_file, "r", encoding="utf-8") as fh:
data = json.load(fh)
entry = data.get("entries", {}).get(relative_path)
# 无 cache entry → 尝试自愈(exact filename stem match + source_path 验证)
if not entry:
raw_stem = pathlib.Path(relative_path).stem
sources_dir = os.path.join(wiki_root, "wiki", "sources")
if os.path.isdir(sources_dir):
for f in os.listdir(sources_dir):
if pathlib.Path(f).stem == raw_stem and f.endswith(".md"):
source_page = os.path.join("wiki", "sources", f)
source_abs = os.path.join(wiki_root, source_page)
# 验证 source 页面的 source_path frontmatter 是否指向当前 raw 文件
source_path_match = False
try:
with open(source_abs, "r", encoding="utf-8") as sf:
in_frontmatter = False
for line in sf:
stripped = line.strip()
if stripped == "---":
if in_frontmatter:
break # end of frontmatter
in_frontmatter = True
continue
if in_frontmatter and stripped.startswith("source_path:"):
fm_value = stripped.split(":", 1)[1].strip()
# 匹配相对路径的末尾部分
if relative_path.endswith(fm_value) or fm_value.endswith(relative_path) or fm_value == relative_path:
source_path_match = True
break
except (OSError, UnicodeDecodeError):
pass
if not source_path_match:
# stem 匹配但 source_path 不一致 → 不信任,需要验证
print("MISS:repaired_needs_verify")
raise SystemExit(0)
# stem + source_path 都匹配 → 安全自愈
timestamp = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
entries = data.setdefault("entries", {})
entries[relative_path] = {
"hash": current_hash,
"ingested_at": timestamp,
"source_page": source_page,
}
tmp_file = cache_file + ".tmp"
with open(tmp_file, "w", encoding="utf-8") as fh2:
json.dump(data, fh2, ensure_ascii=False, indent=2)
fh2.write("\n")
os.replace(tmp_file, cache_file)
print("HIT(repaired)")
raise SystemExit(0)
print("MISS:no_entry")
raise SystemExit(0)
if entry.get("hash") != current_hash:
print("MISS:hash_changed")
raise SystemExit(0)
source_page = entry.get("source_page")
if not source_page:
print("MISS:no_entry")
raise SystemExit(0)
source_path = source_page
if not os.path.isabs(source_path):
source_path = os.path.join(wiki_root, source_path)
if not os.path.isfile(source_path):
print("MISS:no_source")
else:
print("HIT")
PY
)"
printf '%s\n' "$result"
}
cache_update() {
local file_path="$1"
local source_page="$2"
local wiki_root cache_file relative_path_value current_hash normalized_source timestamp
require_file "$file_path"
wiki_root="$(find_wiki_root "$file_path")" || {
echo "未找到知识库根目录:$file_path" >&2
exit 1
}
cache_file="$(cache_file_path "$wiki_root")"
ensure_cache_file "$cache_file"
require_python_cmd
relative_path_value="$(relative_path "$wiki_root" "$file_path")"
current_hash="$(file_hash "$relative_path_value" "$file_path")"
normalized_source="$(normalized_source_page "$wiki_root" "$source_page")"
timestamp="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
"$PYTHON_CMD" - "$cache_file" "$relative_path_value" "$current_hash" "$timestamp" "$normalized_source" <<'PY'
import json
import os
import sys
cache_file, relative_path, file_hash_value, timestamp, source_page = sys.argv[1:6]
with open(cache_file, "r", encoding="utf-8") as fh:
data = json.load(fh)
entries = data.setdefault("entries", {})
entries[relative_path] = {
"hash": file_hash_value,
"ingested_at": timestamp,
"source_page": source_page,
}
tmp_file = cache_file + ".tmp"
with open(tmp_file, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
fh.write("\n")
os.replace(tmp_file, cache_file)
PY
printf 'UPDATED\n'
}
cache_invalidate() {
local file_path="$1"
local wiki_root cache_file relative_path_value
# 不调用 require_file:文件可能已被删除(级联删除场景)
# 直接通过路径查找缓存条目
wiki_root="$(find_wiki_root "$file_path")" || {
echo "未找到知识库根目录:$file_path" >&2
exit 1
}
cache_file="$(cache_file_path "$wiki_root")"
if [ ! -f "$cache_file" ]; then
printf 'INVALIDATED\n'
return 0
fi
require_python_cmd
relative_path_value="$(relative_path "$wiki_root" "$file_path")"
"$PYTHON_CMD" - "$cache_file" "$relative_path_value" <<'PY'
import json
import os
import sys
cache_file, relative_path = sys.argv[1:3]
with open(cache_file, "r", encoding="utf-8") as fh:
data = json.load(fh)
data.setdefault("entries", {}).pop(relative_path, None)
tmp_file = cache_file + ".tmp"
with open(tmp_file, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
fh.write("\n")
os.replace(tmp_file, cache_file)
PY
printf 'INVALIDATED\n'
}
command_name="${1:-}"
case "$command_name" in
check)
[ "$#" -eq 2 ] || { usage; exit 1; }
cache_check "$2"
;;
update)
[ "$#" -eq 3 ] || { usage; exit 1; }
cache_update "$2" "$3"
;;
invalidate)
[ "$#" -eq 2 ] || { usage; exit 1; }
cache_invalidate "$2"
;;
*)
usage
exit 1
;;
esac