This commit is contained in:
2026-07-12 21:26:08 +08:00
commit 9dd41afd48
502 changed files with 129901 additions and 0 deletions
Executable
+722
View File
@@ -0,0 +1,722 @@
#!/bin/bash
# llm-wiki unified installer
set -euo pipefail
SKILL_NAME="llm-wiki"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_REGISTRY_SCRIPT="$SCRIPT_DIR/scripts/source-registry.sh"
ADAPTER_STATE_SCRIPT="$SCRIPT_DIR/scripts/adapter-state.sh"
PLATFORM="auto"
PLATFORM_EXPLICIT=0
DRY_RUN=0
TARGET_DIR=""
INSTALL_HOOKS=0
UNINSTALL_HOOKS=0
UPGRADE=0
WITH_OPTIONAL_ADAPTERS=0
# 这些项目都在运行时会被读取或链接:
# - 入口与说明文件:README / CLAUDE / AGENTS / CHANGELOG
# - 安装入口:install.sh / setup.sh
# - 实际执行内容:SKILL.md / scripts / templates / deps
# - 平台薄入口:platformsREADME、CLAUDE、AGENTS 都会引用)
MANAGED_ITEMS=(
"SKILL.md"
"README.md"
"CLAUDE.md"
"AGENTS.md"
"HERMES.md"
"CHANGELOG.md"
"install.sh"
"setup.sh"
"install.ps1"
"scripts"
"templates"
"deps"
"platforms"
)
DEP_SKILLS=()
list_companion_skill_sources() {
case "$1" in
claude)
printf '%s\n' "platforms/claude/companions/llm-wiki-upgrade"
;;
esac
}
# 微信工具 URL 从共享配置读取,与 adapter-state.sh 保持一致
source "$SCRIPT_DIR/scripts/shared-config.sh"
source "$SCRIPT_DIR/scripts/runtime-context.sh"
info() { printf '\033[36m[信息]\033[0m %s\n' "$1"; }
ok() { printf '\033[32m[完成]\033[0m %s\n' "$1"; }
warn() { printf '\033[33m[警告]\033[0m %s\n' "$1"; }
err() { printf '\033[31m[错误]\033[0m %s\n' "$1" >&2; }
usage() {
cat <<'EOF'
用法:
bash install.sh --platform <claude|codex|openclaw|hermes|auto> [--dry-run]
bash install.sh --platform claude --install-hooks
bash install.sh --install-hooks
bash install.sh --uninstall-hooks
bash install.sh --upgrade [--platform <claude|codex|openclaw|hermes|auto>]
bash install.sh --platform codex --with-optional-adapters
选项:
--platform 目标平台。默认 auto;只有检测到唯一平台时才会自动安装。
--dry-run 只打印安装计划,不写入文件。
--target-dir 指定技能目标目录(直接传最终的 llm-wiki 目录)。
--with-optional-adapters 显式启用网页 / X / YouTube / 公众号等可选提取器安装。
--install-hooks 注册 Claude Code 的 SessionStart hook。
--uninstall-hooks 移除 Claude Code 的 SessionStart hook。
--upgrade 拉取最新代码并更新已安装的 llm-wiki(保留 hook 配置)。
-h, --help 显示帮助。
EOF
}
hook_command_for_skill_dir() {
printf 'bash %s/scripts/hook-session-start.sh\n' "$1"
}
require_jq() {
if ! command -v jq >/dev/null 2>&1; then
err "注册或移除 hook 需要 jq"
exit 1
fi
}
register_claude_session_hook() {
local skill_dir="$1"
local settings_dir settings_path backup_path hook_command tmp_file
[ -d "$skill_dir" ] || {
err "未找到已安装的 llm-wiki$skill_dir"
exit 1
}
require_jq
settings_dir="$HOME/.claude"
settings_path="$settings_dir/settings.json"
backup_path="$settings_dir/settings.json.bak.llm-wiki"
hook_command="$(hook_command_for_skill_dir "$skill_dir")"
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] register SessionStart hook: %s\n' "$hook_command"
return 0
fi
mkdir -p "$settings_dir"
[ -f "$settings_path" ] || printf '{}\n' > "$settings_path"
cp "$settings_path" "$backup_path"
if jq -e --arg cmd "$hook_command" '[ (.hooks.SessionStart // [])[]? | (.hooks // [])[]? | .command ] | index($cmd) != null' "$settings_path" > /dev/null; then
ok "Claude Code SessionStart hook 已存在,跳过"
return 0
fi
tmp_file="$(mktemp)"
jq --arg cmd "$hook_command" '
.hooks = (.hooks // {}) |
.hooks.SessionStart = ((.hooks.SessionStart // []) + [{"hooks":[{"type":"command","command":$cmd}]}])
' "$settings_path" > "$tmp_file"
mv "$tmp_file" "$settings_path"
ok "Claude Code SessionStart hook 已注册"
}
uninstall_claude_session_hook() {
local skill_dir="$1"
local settings_dir settings_path backup_path hook_command tmp_file
require_jq
settings_dir="$HOME/.claude"
settings_path="$settings_dir/settings.json"
backup_path="$settings_dir/settings.json.bak.llm-wiki"
hook_command="$(hook_command_for_skill_dir "$skill_dir")"
if [ ! -f "$settings_path" ]; then
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] uninstall SessionStart hook: %s\n' "$hook_command"
return 0
fi
ok "未找到 Claude Code settings.json,跳过 hook 移除"
return 0
fi
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] uninstall SessionStart hook: %s\n' "$hook_command"
return 0
fi
cp "$settings_path" "$backup_path"
tmp_file="$(mktemp)"
jq --arg cmd "$hook_command" '
.hooks = (.hooks // {}) |
.hooks.SessionStart = (
(.hooks.SessionStart // [])
| map(.hooks = ((.hooks // []) | map(select(.command != $cmd))))
| map(select((.hooks // []) | length > 0))
) |
if ((.hooks.SessionStart // []) | length) == 0 then del(.hooks.SessionStart) else . end |
if (.hooks | length) == 0 then del(.hooks) else . end
' "$settings_path" > "$tmp_file"
mv "$tmp_file" "$settings_path"
ok "Claude Code SessionStart hook 已移除"
}
load_dependency_skills() {
local dep
DEP_SKILLS=()
while IFS= read -r dep; do
[ -n "$dep" ] && DEP_SKILLS+=("$dep")
done < <(bash "$SOURCE_REGISTRY_SCRIPT" unique-dependencies bundled)
}
join_source_labels() {
local category="$1"
bash "$SOURCE_REGISTRY_SCRIPT" list-by-category "$category" \
| awk -F '\t' '
BEGIN { separator = "" }
NF {
printf "%s%s", separator, $2
separator = "、"
}
END {
if (separator == "") {
printf "-"
}
printf "\n"
}
'
}
run_cmd() {
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] %s\n' "$*"
return 0
fi
"$@"
}
copy_item() {
local source_path="$1"
local target_path="$2"
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] copy %s -> %s\n' "$source_path" "$target_path"
return 0
fi
rm -rf "$target_path"
cp -R "$source_path" "$target_path"
}
detect_available_platforms() {
local found=()
if [ -d "$HOME/.claude" ] || [ -d "$HOME/.claude/skills" ]; then
found+=("claude")
fi
if [ -d "$HOME/.codex" ] || [ -d "$HOME/.codex/skills" ] || [ -d "$HOME/.Codex" ] || [ -d "$HOME/.Codex/skills" ]; then
found+=("codex")
fi
if [ -d "$HOME/.openclaw" ] || [ -d "$HOME/.openclaw/skills" ]; then
found+=("openclaw")
fi
if [ -d "$HOME/.hermes" ] || [ -d "$HOME/.hermes/skills" ]; then
found+=("hermes")
fi
printf '%s\n' "${found[@]}"
}
install_dependency_skills() {
local skill_root="$1"
local dep dep_target dep_source
for dep in "${DEP_SKILLS[@]}"; do
dep_source="$SCRIPT_DIR/deps/$dep"
dep_target="$skill_root/$dep"
if [ ! -d "$dep_source" ]; then
warn "$depdeps/ 中未找到源文件,跳过"
continue
fi
copy_item "$dep_source" "$dep_target"
ok "$dep 已准备到 $dep_target"
done
}
install_companion_skills() {
local platform="$1"
local skill_root="$2"
local skill_rel skill_source skill_target skill_name
while IFS= read -r skill_rel; do
[ -n "$skill_rel" ] || continue
skill_source="$SCRIPT_DIR/$skill_rel"
skill_name="$(basename "$skill_rel")"
skill_target="$skill_root/$skill_name"
if [ ! -d "$skill_source" ]; then
warn "$skill_name:仓库中未找到源文件,跳过"
continue
fi
copy_item "$skill_source" "$skill_target"
ok "$skill_name 已安装到 $skill_target"
done < <(list_companion_skill_sources "$platform")
}
install_bundle() {
local target_dir="$1"
local item source_path target_path
for item in "${MANAGED_ITEMS[@]}"; do
source_path="$SCRIPT_DIR/$item"
target_path="$target_dir/$item"
if [ ! -e "$source_path" ]; then
warn "$item:安装源文件缺失,跳过"
continue
fi
if [ "$source_path" = "$target_path" ] && [ -e "$target_path" ]; then
continue
fi
copy_item "$source_path" "$target_path"
done
# 安装后校验:确保清单文件都已就位(Windows install.ps1 尤其重要)
if [ "$DRY_RUN" -ne 1 ]; then
for item in "${MANAGED_ITEMS[@]}"; do
source_path="$SCRIPT_DIR/$item"
target_path="$target_dir/$item"
if [ -e "$source_path" ] && [ ! -e "$target_path" ]; then
err "$item:已列入安装清单但未出现在目标目录,拷贝可能失败"
exit 1
fi
done
ok "已校验全部 ${#MANAGED_ITEMS[@]} 项安装清单文件"
fi
}
install_node_deps() {
local skill_root="$1"
local baoyu_dir="$skill_root/baoyu-url-to-markdown/scripts"
if [ ! -d "$baoyu_dir" ] || [ ! -f "$baoyu_dir/package.json" ]; then
return 0
fi
if [ -d "$baoyu_dir/node_modules" ]; then
ok "baoyu-url-to-markdown 的 Node 依赖已存在"
return 0
fi
info "安装 baoyu-url-to-markdown 的 Node 依赖..."
if [ "$DRY_RUN" -eq 1 ]; then
if command -v bun >/dev/null 2>&1; then
printf '[dry-run] (cd %s && bun install)\n' "$baoyu_dir"
elif command -v npm >/dev/null 2>&1; then
printf '[dry-run] (cd %s && npm install)\n' "$baoyu_dir"
else
printf '[dry-run] 未找到 bun 或 npm,无法安装 Node 依赖\n'
fi
return 0
fi
if command -v bun >/dev/null 2>&1; then
(cd "$baoyu_dir" && bun install) || warn "bun install 失败,跳过(可手动粘贴文本作为替代)"
elif command -v npm >/dev/null 2>&1; then
(cd "$baoyu_dir" && npm install) || warn "npm install 失败,跳过(可手动粘贴文本作为替代)"
else
warn "未找到 bun 或 npm,无法安装 Node 依赖"
echo " 推荐安装 buncurl -fsSL https://bun.sh/install | bash"
return 0
fi
[ -d "$baoyu_dir/node_modules" ] && ok "baoyu-url-to-markdown 的 Node 依赖安装完成"
}
install_uv_tools() {
if ! command -v uv >/dev/null 2>&1; then
warn "未找到 uv,跳过 wechat-article-to-markdown 安装"
echo " 安装 uvcurl -LsSf https://astral.sh/uv/install.sh | sh"
return 0
fi
if command -v wechat-article-to-markdown >/dev/null 2>&1; then
ok "wechat-article-to-markdown 已安装"
return 0
fi
info "安装 wechat-article-to-markdown..."
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] uv tool install %s\n' "${WECHAT_TOOL_URL}"
return 0
fi
uv tool install "${WECHAT_TOOL_URL}" \
|| warn "wechat-article-to-markdown 安装失败(可手动安装:uv tool install ${WECHAT_TOOL_URL}"
if command -v wechat-article-to-markdown >/dev/null 2>&1; then
ok "wechat-article-to-markdown 安装完成"
fi
}
bootstrap_optional_adapters() {
local skill_root="$1"
if [ "$WITH_OPTIONAL_ADAPTERS" -ne 1 ]; then
return 0
fi
load_dependency_skills
install_dependency_skills "$skill_root"
install_node_deps "$skill_root"
install_uv_tools
}
check_environment() {
echo ""
echo "================================"
echo " 环境检查"
echo "================================"
echo ""
if command -v uv >/dev/null 2>&1; then
ok "uv 已安装(可安装 wechat-article-to-markdown,并运行 youtube-transcript"
else
warn "未找到 uv。wechat-article-to-markdown 和 youtube-transcript 需要 uv"
echo " 可用 Homebrew 安装:brew install uv"
fi
if command -v wechat-article-to-markdown >/dev/null 2>&1; then
ok "wechat-article-to-markdown 已可用"
else
warn "未找到 wechat-article-to-markdown。无法自动提取微信公众号"
echo " 可手动安装:uv tool install ${WECHAT_TOOL_URL}"
fi
if command -v lsof >/dev/null 2>&1 && lsof -i :9222 -sTCP:LISTEN >/dev/null 2>&1; then
ok "Chrome 调试端口 9222 已监听(可复用已登录会话)"
else
info "未检测到 Chrome 调试端口 9222。baoyu-url-to-markdown 仍可自动拉起临时浏览器"
echo " 如需复用已登录会话,再执行:open -na \"Google Chrome\" --args --remote-debugging-port=9222"
fi
echo ""
echo "提示:即使部分外挂不可用,PDF / 本地文件 / 纯文本仍可直接进入主线。"
}
print_source_boundary() {
local core_sources optional_sources manual_sources
core_sources="$(join_source_labels core_builtin)"
optional_sources="$(join_source_labels optional_adapter)"
manual_sources="$(join_source_labels manual_only)"
echo ""
echo "================================"
echo " 来源边界"
echo "================================"
echo ""
echo "核心主线:$core_sources"
echo "可选外挂:$optional_sources"
echo "手动入口:$manual_sources"
}
print_adapter_states() {
local output
echo ""
echo "================================"
echo " 外挂状态"
echo "================================"
echo ""
output="$(
bash "$ADAPTER_STATE_SCRIPT" --skill-root "$SKILL_ROOT" --layout-mode installed_skill summary-human 2>&1
)" || {
warn "无法生成外挂状态摘要"
printf '%s\n' "$output"
return 0
}
printf '%s\n' "$output"
}
print_optional_adapter_hint() {
local command
command="bash install.sh"
if [ "$UPGRADE" -eq 1 ]; then
command="$command --upgrade"
fi
command="$command --platform ${PLATFORM}"
if [ -n "$TARGET_DIR" ]; then
command="$command --target-dir ${TARGET_DIR}"
fi
command="$command --with-optional-adapters"
echo ""
echo "提示:当前只准备了知识库核心主线。"
echo "如需网页 / X / 微信公众号 / YouTube / 知乎自动提取,再运行:"
echo " $command"
}
print_claude_upgrade_hint() {
local platform="$1"
if [ "$platform" != "claude" ]; then
return 0
fi
echo ""
echo "提示:Claude Code 安装完成后,还可以直接用 /llm-wiki-upgrade 更新核心主线。"
}
while [ $# -gt 0 ]; do
case "$1" in
--platform)
[ $# -ge 2 ] || { err "--platform 需要一个值"; usage; exit 1; }
PLATFORM="$2"
PLATFORM_EXPLICIT=1
shift 2
;;
--dry-run)
DRY_RUN=1
shift
;;
--target-dir)
[ $# -ge 2 ] || { err "--target-dir 需要一个值"; usage; exit 1; }
TARGET_DIR="$2"
shift 2
;;
--with-optional-adapters)
WITH_OPTIONAL_ADAPTERS=1
shift
;;
--install-hooks)
INSTALL_HOOKS=1
shift
;;
--uninstall-hooks)
UNINSTALL_HOOKS=1
shift
;;
--upgrade)
UPGRADE=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
err "未知参数:$1"
usage
exit 1
;;
esac
done
if [ "$INSTALL_HOOKS" -eq 1 ] && [ "$UNINSTALL_HOOKS" -eq 1 ]; then
err "--install-hooks 和 --uninstall-hooks 不能同时使用"
exit 1
fi
if [ "$UPGRADE" -eq 1 ]; then
if [ -n "$TARGET_DIR" ] && [ "$PLATFORM" = "auto" ]; then
err "自定义目标目录升级时请显式传入 --platform"
exit 1
fi
if [ "$PLATFORM" = "auto" ]; then
detected_platforms=()
for p in claude codex openclaw hermes; do
skill_root_candidate="$(resolve_platform_skill_root "$p")"
[ -d "$skill_root_candidate/$SKILL_NAME" ] && detected_platforms+=("$p")
done
if [ "${#detected_platforms[@]}" -eq 0 ]; then
err "没有检测到已安装的 llm-wiki,请先运行安装"
exit 1
fi
if [ "${#detected_platforms[@]}" -gt 1 ]; then
err "检测到多个已安装平台:${detected_platforms[*]}。升级时请显式传入 --platform"
exit 1
fi
PLATFORM="${detected_platforms[0]}"
UPGRADE_PLATFORMS=("${detected_platforms[@]}")
else
UPGRADE_PLATFORMS=("$PLATFORM")
fi
echo ""
echo "================================"
echo " llm-wiki 升级"
echo "================================"
echo ""
if [ -d "$SCRIPT_DIR/.git" ]; then
info "从远程拉取最新代码..."
if [ "$DRY_RUN" -eq 1 ]; then
printf '[dry-run] git -C %s pull\n' "$SCRIPT_DIR"
else
git -C "$SCRIPT_DIR" pull || {
err "git pull 失败,请检查网络或手动拉取后重试"
exit 1
}
fi
ok "代码已拉取到最新"
else
warn "当前目录不是 git 仓库,跳过 git pull"
fi
upgrade_failures=0
for upgrade_platform in "${UPGRADE_PLATFORMS[@]}"; do
upgrade_root="$(resolve_platform_skill_root "$upgrade_platform")"
if [ -n "$TARGET_DIR" ]; then
upgrade_target="$TARGET_DIR"
upgrade_root="$(dirname "$upgrade_target")"
else
upgrade_target="$upgrade_root/$SKILL_NAME"
fi
echo ""
info "更新 $upgrade_platform 的 llm-wiki..."
echo " 目标目录:$upgrade_target"
if [ ! -d "$upgrade_target" ]; then
err "$upgrade_platform 尚未安装 llm-wiki$upgrade_target"
upgrade_failures=$((upgrade_failures + 1))
continue
fi
run_cmd mkdir -p "$upgrade_target"
install_bundle "$upgrade_target"
install_companion_skills "$upgrade_platform" "$upgrade_root"
bootstrap_optional_adapters "$upgrade_root"
ok "$upgrade_platform 的 llm-wiki 已更新"
done
if [ "$upgrade_failures" -gt 0 ]; then
echo ""
err "llm-wiki 升级失败,请先确认目标目录存在且已完成安装"
exit 1
fi
print_source_boundary
if [ "$WITH_OPTIONAL_ADAPTERS" -eq 1 ]; then
check_environment
else
print_optional_adapter_hint
fi
print_claude_upgrade_hint "$PLATFORM"
echo ""
ok "llm-wiki 升级完成"
exit 0
fi
if [ "$PLATFORM" = "auto" ] && { [ "$INSTALL_HOOKS" -eq 1 ] || [ "$UNINSTALL_HOOKS" -eq 1 ]; }; then
PLATFORM="claude"
fi
if [ "$PLATFORM" = "auto" ]; then
detected_platforms=()
while IFS= read -r platform_name; do
[ -n "$platform_name" ] && detected_platforms+=("$platform_name")
done < <(detect_available_platforms)
if [ "${#detected_platforms[@]}" -eq 1 ]; then
PLATFORM="${detected_platforms[0]}"
elif [ "${#detected_platforms[@]}" -eq 0 ]; then
err "没有检测到受支持的平台目录。请显式传入 --platform claude|codex|openclaw|hermes"
exit 1
else
err "检测到多个可用平台:${detected_platforms[*]}。请显式传入 --platform"
exit 1
fi
fi
SKILL_ROOT="$(resolve_platform_skill_root "$PLATFORM")"
if { [ "$INSTALL_HOOKS" -eq 1 ] || [ "$UNINSTALL_HOOKS" -eq 1 ]; } && [ "$PLATFORM" != "claude" ]; then
err "只有 Claude Code 支持 SessionStart hook"
exit 1
fi
if [ -n "$TARGET_DIR" ]; then
TARGET_SKILL_DIR="$TARGET_DIR"
SKILL_ROOT="$(dirname "$TARGET_SKILL_DIR")"
else
TARGET_SKILL_DIR="$SKILL_ROOT/$SKILL_NAME"
fi
if [ "$UNINSTALL_HOOKS" -eq 1 ]; then
uninstall_claude_session_hook "$TARGET_SKILL_DIR"
echo ""
ok "llm-wiki hook 已移除"
exit 0
fi
if [ "$INSTALL_HOOKS" -eq 1 ] && [ "$PLATFORM_EXPLICIT" -eq 0 ] && [ -z "$TARGET_DIR" ]; then
register_claude_session_hook "$TARGET_SKILL_DIR"
echo ""
ok "llm-wiki hook 已准备完成"
exit 0
fi
echo ""
echo "================================"
echo " llm-wiki 安装"
echo "================================"
echo ""
echo "平台:$PLATFORM"
echo "技能根目录:$SKILL_ROOT"
echo "目标目录:$TARGET_SKILL_DIR"
run_cmd mkdir -p "$SKILL_ROOT"
run_cmd mkdir -p "$TARGET_SKILL_DIR"
install_bundle "$TARGET_SKILL_DIR"
install_companion_skills "$PLATFORM" "$SKILL_ROOT"
bootstrap_optional_adapters "$SKILL_ROOT"
print_source_boundary
if [ "$WITH_OPTIONAL_ADAPTERS" -eq 1 ]; then
check_environment
print_adapter_states
else
print_optional_adapter_hint
fi
print_claude_upgrade_hint "$PLATFORM"
if [ "$INSTALL_HOOKS" -eq 1 ]; then
register_claude_session_hook "$TARGET_SKILL_DIR"
fi
echo ""
ok "llm-wiki 已准备完成"