Files

353 lines
8.9 KiB
Bash
Executable File
Raw Permalink 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.
#!/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