#!/usr/bin/env bash # kbtree 客户端:按分类树检索 HomeAgent 知识库。 # # 由 knowledge-base skill 调用(agent 读 SKILL.md 后按需调本脚本)。 # 也可人工使用: # kb_tree.sh # 整棵树 # kb_tree.sh -c tech -d 1 # tech 子树,只看一层 # kb_tree.sh -q goroutine -c tech # 在 tech 子树内检索 # kb_tree.sh -l # 列分类 # # token 读取顺序:环境变量 KB_TOKEN > 本目录 config.json > 报错。 # 故意不放命令行参数:token 会进 shell 历史与 ps 输出。 set -euo pipefail BASE="${KB_BASE:-http://127.0.0.1:9892}" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # 读 token # 帮助:-h/--help 必须在解析命令之前判,否则会被当成未知命令 for a in "$@"; do case "$a" in -h|--help) echo "用法:" echo " kb_tree.sh [tree|categories|counts|search] [选项]" echo "选项:" echo " -q 关键词 检索词(给了 q 就走 search)" echo " -c 分类 限定分类子树(前缀匹配),如 tech / tech/go" echo " -d 层数 树展开层数,1=只看第一层(懒加载)" echo " -i 0|1 是否带条目详情,默认 1" echo " -l 条数 search 的返回条数,默认 10" echo "环境变量: KB_TOKEN(也可放同目录 config/config.json)" exit 0 ;; esac done if [[ -z "${KB_TOKEN:-}" ]]; then CFG="$HERE/../config.json" if [[ -f "$CFG" ]]; then KB_TOKEN="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("token",""))' "$CFG" 2>/dev/null || true)" fi fi if [[ -z "${KB_TOKEN:-}" ]]; then cat >&2 <<'EOF' kbtree: 未找到访问令牌。 取 token 的办法(其一): 1. 若 homed 启动日志里有「token 未配置,本次随机生成」,那行日志附近有 token; 2. 若已配置过固定 token,从配置库读: sqlite3 /config.db "SELECT value FROM config_kbtree WHERE key='token'" 3. 直接设置:export KB_TOKEN= token 未配置时服务会在启动时随机生成,进程重启即失效。 EOF exit 2 fi auth=(-H "X-API-Key: ${KB_TOKEN}") cmd="${1:-tree}"; shift || true query=""; category=""; depth=""; items=""; limit="" while getopts "q:c:d:i:l:h" opt 2>/dev/null; do case "$opt" in q) query="$OPTARG" ;; c) category="$OPTARG" ;; d) depth="$OPTARG" ;; i) items="$OPTARG" ;; l) limit="$OPTARG" ;; h) echo "用法: kb_tree.sh [tree|categories|counts|search] -q 关键词 -c 分类 -d 层数 -i 0|1 -l 条数"; exit 0 ;; esac done # 预检:端口通不通。不通就直说"服务未上线", # 而不是让用户看 curl: (7) Connection refused 后自行猜测。 HOSTPORT="${BASE#http://}"; HOSTPORT="${HOSTPORT%%/*}" if ! (exec 3<>"/dev/tcp/${HOSTPORT%%:*}/${HOSTPORT##*:}") 2>/dev/null; then cat >&2 </config.db ".backup '/config.db.bak-\$(date +%Y%m%d-%H%M%S)'" install -m 0755 <新二进制> /usr/local/bin/homed # 勿用 cp:会写坏运行中进程的映像 systemctl restart homeagent journalctl -u homeagent -f | grep kbtree EOF exit 7 fi enc() { python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.argv[1]))' "$1"; } # kb_call 发请求并把错误翻译成人话。 # 不用 curl -f:它只吐 "curl: (22) 404",把服务端给的有用信息(404 会附现有分类) # 全丢了 —— 而那恰恰是排查时最需要的。 kb_call() { local url="$1" raw code raw="$(curl -sS -w $'\n%{http_code}' "${auth[@]}" "$url" 2>&1)" || { echo "kbtree: 请求失败: ${raw}" >&2; exit 8 } code="$(printf '%s' "$raw" | tail -n1)" body="$(printf '%s' "$raw" | sed '$d')" case "$code" in 2*) printf '%s' "$body" | python3 -m json.tool; return 0 ;; 401) echo "kbtree: 令牌无效(401)。检查 config/config.json 或 KB_TOKEN 是否与 config_kbtree 表一致。" >&2; exit 3 ;; 404) echo "kbtree: 分类不存在(404)。服务端返回的现有分类:" >&2 printf '%s' "$body" | KB_BODY="$body" python3 -c ' import json, os, sys try: d = json.loads(os.environ.get("KB_BODY","")) print(" " + ", ".join(d.get("categories", [])), file=sys.stderr) except Exception: print(" (无法解析响应体)", file=sys.stderr) ' exit 4 ;; *) echo "kbtree: HTTP $code" >&2; printf '%s\n' "$body" >&2; exit 5 ;; esac } case "$cmd" in tree|categories|counts|search) ;; *) echo "未知命令: $cmd" >&2; exit 2 ;; esac if [[ "$cmd" == "categories" ]]; then kb_call "$BASE/categories" exit 0 fi if [[ "$cmd" == "counts" ]]; then kb_call "$BASE/counts" exit 0 fi if [[ "$cmd" == "search" || -n "$query" ]]; then [[ -n "$query" ]] || { echo "search 需要 -q 关键词" >&2; exit 2; } url="$BASE/search?q=$(enc "$query")" [[ -n "$category" ]] && url="$url&category=$(enc "$category")" [[ -n "$limit" ]] && url="$url&limit=$limit" kb_call "$url" exit 0 fi # 默认:树 url="$BASE/tree" [[ -n "$category" ]] && url="$url?category=$(enc "$category")" q="" [[ -n "$depth" ]] && q="depth=$depth" [[ -n "$items" ]] && q="${q:+$q&}items=$items" [[ -n "$q" ]] && url="$url?$q" kb_call "$url"