#!/usr/bin/env bash # # memory-tencentdb-ctl.sh — memory_tencentdb (TDAI) 服务统一管理脚本 # # 两种运行模式: # # 默认:standalone 模式 # Gateway 以独立 HTTP 服务方式运行,完全不触碰 ~/.hermes/ # 日志目录 : $TDAI_DATA_DIR/logs/ # 配置文件 : $TDAI_DATA_DIR/tdai-gateway.json (llm / embedding / tcvdb) # Gateway 端: 127.0.0.1:8420 # # --hermes 模式(需显式传 --hermes 或设 MEMORY_TENCENTDB_MODE=hermes) # 额外做 hermes 集成 —— 路径约定沿用 install_hermes_tdai_gateway.sh: # 日志目录 : ~/.hermes/logs/memory_tencentdb/ # env 片段文件 : ~/.hermes/env.d/memory-tencentdb-llm.sh (config llm 会写入) # hermes 主配置 : ~/.hermes/config.yaml (enable-hermes-memory 会修改) # 目的:hermes 的 supervisor 可通过 os.environ.copy() 把 LLM 凭据继承给 # 它自己托管起来的 Gateway 子进程;也方便 hermes 端排障。 # # 命令: # start | stop | restart | status | logs | health # config llm --api-key --base-url --model # config embedding --provider

--api-key --base-url --model --dimensions # [--proxy-url ] # config vdb --url --username --api-key --database [--ca-pem ] # config show # enable-hermes-memory # 仅 --hermes 模式:把 hermes config.yaml 的 memory.provider # # 置为 memory_tencentdb # # 绝大多数子命令支持 --dry-run;写入操作均使用临时文件 + rename 原子替换, # 生成的敏感文件权限设为 0600。 set -euo pipefail # ============================================================ # 常量 / 路径 # ============================================================ SCRIPT_NAME="memory-tencentdb-ctl" USER_HOME="${HOME:-$(eval echo "~$(whoami)")}" # memory-tencentdb 统一根目录(所有 tdai 相关数据/代码默认收纳在此) # 可通过环境变量 MEMORY_TENCENTDB_ROOT 覆盖 MEMORY_TENCENTDB_ROOT="${MEMORY_TENCENTDB_ROOT:-$USER_HOME/.memory-tencentdb}" TDAI_INSTALL_DIR="${TDAI_INSTALL_DIR:-$MEMORY_TENCENTDB_ROOT/tdai-memory-openclaw-plugin}" TDAI_DATA_DIR="${TDAI_DATA_DIR:-$MEMORY_TENCENTDB_ROOT/memory-tdai}" GATEWAY_CFG="$TDAI_DATA_DIR/tdai-gateway.json" # 旧路径(仅做提示,不自动迁移;迁移由 install_hermes_memory_tencentdb.sh 负责) _LEGACY_INSTALL_DIR="$USER_HOME/tdai-memory-openclaw-plugin" _LEGACY_DATA_DIR="$USER_HOME/memory-tdai" if [ -z "${TDAI_INSTALL_DIR_EXPLICIT:-}" ] && [ ! -e "$TDAI_INSTALL_DIR" ] && [ -e "$_LEGACY_INSTALL_DIR" ]; then printf '[%s] WARN: legacy install dir detected at %s; new default is %s. Run install_hermes_memory_tencentdb.sh to migrate, or `export TDAI_INSTALL_DIR=%s` to keep old location.\n' \ "$SCRIPT_NAME" "$_LEGACY_INSTALL_DIR" "$TDAI_INSTALL_DIR" "$_LEGACY_INSTALL_DIR" >&2 fi if [ -z "${TDAI_DATA_DIR_EXPLICIT:-}" ] && [ ! -e "$TDAI_DATA_DIR" ] && [ -e "$_LEGACY_DATA_DIR" ]; then printf '[%s] WARN: legacy data dir detected at %s; new default is %s. Run install_hermes_memory_tencentdb.sh to migrate, or `export TDAI_DATA_DIR=%s` to keep old location.\n' \ "$SCRIPT_NAME" "$_LEGACY_DATA_DIR" "$TDAI_DATA_DIR" "$_LEGACY_DATA_DIR" >&2 fi # hermes 路径仅在 --hermes 模式下使用;此处保留定义以便 helper 复用。 HERMES_HOME="${HERMES_HOME:-$USER_HOME/.hermes}" HERMES_CONFIG="$HERMES_HOME/config.yaml" HERMES_ENV_DIR="$HERMES_HOME/env.d" GATEWAY_HOST="${MEMORY_TENCENTDB_GATEWAY_HOST:-127.0.0.1}" GATEWAY_PORT="${MEMORY_TENCENTDB_GATEWAY_PORT:-8420}" # 运行模式:standalone(默认)| hermes MODE="${MEMORY_TENCENTDB_MODE:-standalone}" # 这些会在 _apply_mode_paths 中根据 MODE 实际赋值 HERMES_LOG_DIR="" PID_FILE="" STDOUT_LOG="" STDERR_LOG="" DRY_RUN=0 # ============================================================ # 通用 helpers # ============================================================ log() { printf '[%s] %s\n' "$SCRIPT_NAME" "$*"; } warn() { printf '[%s:warn] %s\n' "$SCRIPT_NAME" "$*" >&2; } die() { printf '[%s:error] %s\n' "$SCRIPT_NAME" "$*" >&2; exit "${2:-1}"; } need_cmd() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" 127 } # 安全 shell 引用,避免 api_key 等特殊字符破坏 source shell_quote() { printf '%s' "$1" | sed -e "s/'/'\\\\''/g" -e "1s/^/'/" -e "\$s/\$/'/" } # 原子写文件:write_file write_file_atomic() { local path="$1" mode="$2" local dir; dir="$(dirname "$path")" mkdir -p "$dir" if [[ $DRY_RUN -eq 1 ]]; then log "[dry-run] would write $path (mode=$mode):" sed 's/^/ /' return 0 fi local tmp; tmp="$(mktemp "$dir/.${SCRIPT_NAME}.XXXXXX")" cat > "$tmp" chmod "$mode" "$tmp" mv -f "$tmp" "$path" log "wrote $path (mode=$mode)" } # 端口上的监听 PID(优先 lsof,兜底 ss) listening_pids() { local port="$1" if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null || true elif command -v ss >/dev/null 2>&1; then ss -ltnpH "sport = :$port" 2>/dev/null \ | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | sort -u fi } # 健康检查(不依赖 curl,用 python3) health_check() { local timeout="${1:-3}" python3 - "$GATEWAY_HOST" "$GATEWAY_PORT" "$timeout" <<'PYEOF' 2>/dev/null import json, sys, urllib.request host, port, timeout = sys.argv[1], int(sys.argv[2]), float(sys.argv[3]) url = f"http://{host}:{port}/health" try: with urllib.request.urlopen(url, timeout=timeout) as r: body = r.read().decode("utf-8", "replace") print(body) sys.exit(0) except Exception as e: print(f"health check failed: {e}", file=sys.stderr) sys.exit(1) PYEOF } # 根据 MODE 解析日志 / PID 目录。必须在解析完 --hermes 后、任何 ensure_paths # / 启动逻辑之前调用。 _apply_mode_paths() { case "$MODE" in standalone) HERMES_LOG_DIR="${MEMORY_TENCENTDB_LOG_DIR:-$TDAI_DATA_DIR/logs}" ;; hermes) HERMES_LOG_DIR="${MEMORY_TENCENTDB_LOG_DIR:-$HERMES_HOME/logs/memory_tencentdb}" ;; *) die "invalid MODE: $MODE (expected standalone | hermes)" 1 ;; esac PID_FILE="$HERMES_LOG_DIR/gateway.pid" STDOUT_LOG="$HERMES_LOG_DIR/gateway.stdout.log" STDERR_LOG="$HERMES_LOG_DIR/gateway.stderr.log" } # 仅在 --hermes 模式下执行的守卫。非 hermes 模式调用 hermes 专属命令会直接退出。 require_hermes_mode() { [[ "$MODE" == "hermes" ]] || die \ "'$1' 仅在 --hermes 模式下可用;请追加 --hermes 或设 MEMORY_TENCENTDB_MODE=hermes" 1 } ensure_paths() { mkdir -p "$HERMES_LOG_DIR" "$TDAI_DATA_DIR" [[ "$MODE" == "hermes" ]] && mkdir -p "$HERMES_ENV_DIR" return 0 } # 在需要 source 用户 env 的命令里调用。standalone 模式下只读取顶层 /etc/profile.d/ # 的系统级配置(保持和 install 脚本兼容),不 source ~/.hermes/env.d/*。 source_user_envs() { # 系统级:install_hermes_tdai_gateway.sh 写入的 /etc/profile.d/memory-tencentdb-env.sh # 里只有 Gateway 自身需要的变量(port/host/cmd/llm env),两种模式都可安全 source。 if [[ -r /etc/profile.d/memory-tencentdb-env.sh ]]; then # shellcheck disable=SC1091 source /etc/profile.d/memory-tencentdb-env.sh fi if [[ "$MODE" == "hermes" ]]; then if [[ -r /etc/profile.d/hermes-env.sh ]]; then # shellcheck disable=SC1091 source /etc/profile.d/hermes-env.sh fi # 用户级 env.d,优先级更高 if [[ -d "$HERMES_ENV_DIR" ]]; then local f for f in "$HERMES_ENV_DIR"/*.sh; do [[ -r "$f" ]] || continue # shellcheck disable=SC1090 source "$f" done fi fi } # ============================================================ # 启动命令构造 # # 优先级: # 1. MEMORY_TENCENTDB_GATEWAY_CMD(install 脚本写入的环境变量) # 2. 本地 tsx: cd $TDAI_INSTALL_DIR && npx tsx src/gateway/server.ts # ============================================================ resolve_gateway_cmd() { if [[ -n "${MEMORY_TENCENTDB_GATEWAY_CMD:-}" ]]; then printf '%s' "$MEMORY_TENCENTDB_GATEWAY_CMD" return 0 fi local entry="$TDAI_INSTALL_DIR/src/gateway/server.ts" [[ -f "$entry" ]] || die "Gateway entry not found: $entry (是否已执行 install_hermes_tdai_gateway.sh?)" # 与 install 脚本相同风格:sh -c 'cd ... && exec npx tsx ...' printf "sh -c 'cd %s && exec npx tsx src/gateway/server.ts'" "$TDAI_INSTALL_DIR" } # ============================================================ # 子命令:start / stop / restart / status / logs / health # ============================================================ cmd_start() { ensure_paths source_user_envs local pids; pids="$(listening_pids "$GATEWAY_PORT")" if [[ -n "$pids" ]]; then warn "Gateway 已在 :$GATEWAY_PORT 运行 (pid=$pids)" return 0 fi need_cmd node need_cmd npx local gw_cmd; gw_cmd="$(resolve_gateway_cmd)" log "starting gateway: $gw_cmd" log "stdout -> $STDOUT_LOG" log "stderr -> $STDERR_LOG" if [[ $DRY_RUN -eq 1 ]]; then log "[dry-run] skip spawn" return 0 fi # setsid 让 Gateway 脱离当前 shell 的进程组;nohup 兜底保证终端断开不挂 # eval 是必要的:gw_cmd 是 "sh -c '...'" 这种带引号结构 if command -v setsid >/dev/null 2>&1; then eval "setsid nohup $gw_cmd >>\"$STDOUT_LOG\" 2>>\"$STDERR_LOG\" >\"$STDOUT_LOG\" 2>>\"$STDERR_LOG\" "$PID_FILE" log "spawned pid=$bg_pid (shell wrapper)" # 等待端口监听起来 / 健康检查 local i for i in $(seq 1 30); do sleep 0.5 if [[ -n "$(listening_pids "$GATEWAY_PORT")" ]]; then if health_check 2 >/dev/null 2>&1; then log "gateway healthy on http://$GATEWAY_HOST:$GATEWAY_PORT" return 0 fi fi done warn "gateway 未在 15s 内通过健康检查,请查看 $STDERR_LOG" return 1 } cmd_stop() { local pids; pids="$(listening_pids "$GATEWAY_PORT")" if [[ -z "$pids" ]]; then log "no gateway listening on :$GATEWAY_PORT" # 同时清理可能残留的 shell wrapper if [[ -f "$PID_FILE" ]]; then local wpid; wpid="$(cat "$PID_FILE" 2>/dev/null || true)" [[ -n "$wpid" ]] && kill -0 "$wpid" 2>/dev/null && kill -TERM "$wpid" 2>/dev/null || true rm -f "$PID_FILE" fi return 0 fi log "sending SIGTERM to: $pids" [[ $DRY_RUN -eq 1 ]] && { log "[dry-run] skip kill"; return 0; } # shellcheck disable=SC2086 kill -TERM $pids 2>/dev/null || true local i for i in $(seq 1 10); do sleep 0.5 pids="$(listening_pids "$GATEWAY_PORT")" [[ -z "$pids" ]] && break done if [[ -n "$pids" ]]; then warn "SIGTERM 未生效,发送 SIGKILL: $pids" # shellcheck disable=SC2086 kill -KILL $pids 2>/dev/null || true fi rm -f "$PID_FILE" log "gateway stopped" } cmd_restart() { cmd_stop || true sleep 0.5 cmd_start } cmd_status() { local pids; pids="$(listening_pids "$GATEWAY_PORT")" echo "== memory_tencentdb Gateway ==" echo " mode : $MODE" echo " host:port : $GATEWAY_HOST:$GATEWAY_PORT" echo " install : $TDAI_INSTALL_DIR" echo " data dir : $TDAI_DATA_DIR" echo " log dir : $HERMES_LOG_DIR" echo " config : $GATEWAY_CFG $([[ -f $GATEWAY_CFG ]] && echo '[exists]' || echo '[missing]')" if [[ "$MODE" == "hermes" ]]; then echo " hermes cfg: $HERMES_CONFIG $([[ -f $HERMES_CONFIG ]] && echo '[exists]' || echo '[missing]')" fi if [[ -n "$pids" ]]; then echo " state : RUNNING (pid=$pids)" if health_check 2 >/dev/null 2>&1; then echo " health : OK" else echo " health : UNHEALTHY" fi else echo " state : STOPPED" fi if [[ "$MODE" == "hermes" ]]; then echo echo "== hermes memory provider ==" if [[ -f "$HERMES_CONFIG" ]]; then local prov prov="$(sed -n '/^memory:/,/^[a-zA-Z]/p' "$HERMES_CONFIG" \ | sed -n 's/^[[:space:]]*provider:[[:space:]]*//p' | head -n1)" echo " memory.provider = ${prov:-}" else echo " (hermes config 不存在)" fi echo echo "== env files ==" if [[ -d "$HERMES_ENV_DIR" ]]; then ls -l "$HERMES_ENV_DIR"/*.sh 2>/dev/null || echo " (none)" else echo " $HERMES_ENV_DIR not found" fi fi } cmd_logs() { local which="${1:-all}" lines="${2:-200}" case "$which" in out|stdout) tail -n "$lines" -f "$STDOUT_LOG" ;; err|stderr) tail -n "$lines" -f "$STDERR_LOG" ;; all|*) log "tail $STDOUT_LOG & $STDERR_LOG (Ctrl-C 退出)" tail -n "$lines" -f "$STDOUT_LOG" "$STDERR_LOG" ;; esac } cmd_health() { if health_check 3; then log "gateway healthy" else die "gateway unhealthy" 1 fi } # ============================================================ # 子命令:config # # 写入两处(与 sync_tdai_llm.sh 一致): # - $HERMES_ENV_DIR/memory-tencentdb-

.sh 仅 llm 段需要(通过环境变量暴露) # - $GATEWAY_CFG (JSON) 三段 llm / embedding / tcvdb 都合并写入 # ============================================================ # ---- JSON 合并 helper ---- # 用法:merge_gateway_json "
" <<<"$json_fragment" # section: llm / embedding / tcvdb merge_gateway_json() { local section="$1" ensure_paths if [[ $DRY_RUN -eq 1 ]]; then log "[dry-run] would merge '$section' into $GATEWAY_CFG" sed 's/^/ /' return 0 fi need_cmd python3 local fragment; fragment="$(cat)" SECTION="$section" FRAGMENT="$fragment" CFG="$GATEWAY_CFG" \ python3 - <<'PYEOF' import json, os, tempfile section = os.environ["SECTION"] fragment = json.loads(os.environ["FRAGMENT"]) path = os.environ["CFG"] cfg = {} if os.path.isfile(path): try: with open(path, "r", encoding="utf-8") as f: cfg = json.load(f) or {} except Exception: cfg = {} # memory.* 段嵌套在 "memory" 下(gateway config.ts 的 loadGatewayConfig 约定), # 但 llm 是顶层段(见 src/gateway/config.ts 第 79 行 obj(fileConfig,"llm"))。 if section == "llm": merged = cfg.get("llm") or {} merged.update(fragment) cfg["llm"] = merged else: mem = cfg.get("memory") or {} sub = mem.get(section) or {} sub.update(fragment) mem[section] = sub cfg["memory"] = mem d = os.path.dirname(path) or "." fd, tmp = tempfile.mkstemp(prefix=".tdai-gateway.", dir=d) os.close(fd) with open(tmp, "w", encoding="utf-8") as f: json.dump(cfg, f, indent=2, ensure_ascii=False) os.chmod(tmp, 0o600) os.replace(tmp, path) PYEOF log "merged '$section' into $GATEWAY_CFG (0600)" } # ---- config llm ---- cmd_config_llm() { local api_key="" base_url="" model="" restart=0 while [[ $# -gt 0 ]]; do case "$1" in --api-key) api_key="$2"; shift 2 ;; --base-url) base_url="$2"; shift 2 ;; --model) model="$2"; shift 2 ;; --restart) restart=1; shift ;; *) die "config llm: 未知参数 $1" 1 ;; esac done [[ -n "$api_key" ]] || die "--api-key 必填" [[ -n "$base_url" ]] || die "--base-url 必填" [[ -n "$model" ]] || die "--model 必填" case "$base_url" in http://*|https://*) ;; *) die "--base-url 必须以 http:// 或 https:// 开头: $base_url" 1 ;; esac log "configure LLM: model=$model base_url=$base_url api_key=<${#api_key} chars>" # 1) env 文件(仅 --hermes 模式):供 hermes 启动时 source, # hermes supervisor 再通过 os.environ.copy() 把凭据注入它自己托管的 Gateway 子进程。 # standalone 模式下 Gateway 直接读 tdai-gateway.json,无需 env 文件。 if [[ "$MODE" == "hermes" ]]; then local qk qu qm qk="$(shell_quote "$api_key")" qu="$(shell_quote "$base_url")" qm="$(shell_quote "$model")" write_file_atomic "$HERMES_ENV_DIR/memory-tencentdb-llm.sh" 600 < 'sqlite'" + (" (tcvdb creds purged)" if purge else " (tcvdb creds kept)") + "\n") PYEOF if [[ $purge -eq 1 ]]; then log "memory.storeBackend = sqlite (tcvdb creds purged)" else log "memory.storeBackend = sqlite (tcvdb creds kept; 加 --purge-creds 可清除)" fi [[ $restart -eq 1 ]] && cmd_restart || log "tip: 追加 --restart 让回退立即生效" } # ---- config show ---- cmd_config_show() { echo "== $GATEWAY_CFG ==" if [[ -f "$GATEWAY_CFG" ]]; then # 自动脱敏 apiKey python3 - "$GATEWAY_CFG" <<'PYEOF' import json, sys cfg = json.load(open(sys.argv[1], "r", encoding="utf-8")) def redact(d): if isinstance(d, dict): for k, v in d.items(): if isinstance(v, (dict, list)): redact(v) elif k.lower() in ("apikey","api_key","password","token") and isinstance(v, str) and v: d[k] = f"" elif isinstance(d, list): for x in d: redact(x) redact(cfg) print(json.dumps(cfg, indent=2, ensure_ascii=False)) PYEOF else echo "(not found)" fi echo if [[ "$MODE" == "hermes" ]]; then echo "== env files ==" if [[ -d "$HERMES_ENV_DIR" ]]; then local f for f in "$HERMES_ENV_DIR"/memory-tencentdb-*.sh; do [[ -r "$f" ]] || continue echo "--- $f ---" # 脱敏 API key 值 sed -E "s/(API_KEY=)'([^']{0,4})[^']*'/\1'\2'/g" "$f" done fi else echo "(standalone 模式:不写 env.d;如需 hermes 集成请追加 --hermes)" fi } # ============================================================ # 子命令:enable-hermes-memory (仅 --hermes 模式) # 把 $HERMES_CONFIG 的 memory.provider 设置为 memory_tencentdb。 # 写入策略(按优先级,自动降级): # 1) ruamel.yaml round-trip:完整保留注释、键序、引号、缩进风格 # 2) 最小化原位行编辑:只改 provider 行,缩进直接拷贝既有兄弟键 # 3) memory 段不存在 → 在末尾追加最小段 # ============================================================ cmd_enable_hermes_memory() { require_hermes_mode "enable-hermes-memory" [[ -f "$HERMES_CONFIG" ]] || die "hermes 配置不存在: $HERMES_CONFIG" [[ $# -eq 0 ]] || die "enable-hermes-memory: 不支持额外参数: $*" 1 if [[ $DRY_RUN -eq 1 ]]; then log "[dry-run] would set memory.provider=memory_tencentdb in $HERMES_CONFIG" return 0 fi need_cmd python3 # 写入策略(按优先级,自动降级): # 1. ruamel.yaml round-trip:完整保留注释、键序、引号、缩进风格 # 2. 原位行编辑:只改 memory.provider 那一行;缩进直接拷贝同段兄弟键的前缀 # 3. memory 段不存在 → 在文件末尾追加最小段(此时无既有格式可破坏) python3 - "$HERMES_CONFIG" <<'PYEOF' import os, re, sys, tempfile path = sys.argv[1] TARGET = "memory_tencentdb" def _atomic_write(text: str) -> None: d = os.path.dirname(path) or "." fd, tmp = tempfile.mkstemp(prefix=".hermes-config.", dir=d) os.close(fd) try: with open(tmp, "w", encoding="utf-8") as f: f.write(text) os.replace(tmp, path) except Exception: try: os.unlink(tmp) except OSError: pass raise def try_ruamel() -> bool: """首选:用 ruamel.yaml round-trip 改写,完整保留注释/键序/缩进/引号。""" try: from ruamel.yaml import YAML # type: ignore except Exception: return False yaml = YAML(typ="rt") yaml.preserve_quotes = True # 不强制 default indent;ruamel 会沿用文档原有缩进风格 with open(path, "r", encoding="utf-8") as f: data = yaml.load(f) if data is None: # 空文件:构造最小 mapping from ruamel.yaml.comments import CommentedMap data = CommentedMap() if "memory" not in data or not hasattr(data.get("memory"), "__setitem__"): from ruamel.yaml.comments import CommentedMap data["memory"] = CommentedMap() data["memory"]["provider"] = TARGET import io buf = io.StringIO() yaml.dump(data, buf) _atomic_write(buf.getvalue()) print(f"updated {path} (ruamel.yaml round-trip)") return True def fallback_inline_edit() -> None: """兜底:纯文本最小化原位编辑,永不重写整个文件结构。 规则: - 找到顶级 `memory:` 段及其 block(直到下一个顶级键) - 若 block 内已有 `^(\\s+)provider\\s*:` 行 → 用相同前缀替换该行 (缩进直接复用现有 provider 行,零猜测) - 否则在 block 内"拓印"任意已存在兄弟键的缩进,紧跟 `memory:` 之后插入一行 - 若 block 完全为空(仅 memory: 一行)→ 在 `memory:` 后插入一行, 缩进 copy 自同文件中其他顶级 mapping 的子键缩进;找不到再退化为 2 - 若全文没有 `memory:` 顶级段 → 在文件末尾追加最小段 """ with open(path, "r", encoding="utf-8") as f: lines = f.readlines() top_key_re = re.compile(r"^[A-Za-z_][\w\-]*\s*:") memory_start_re = re.compile(r"^memory\s*:\s*(#.*)?$") sibling_key_re = re.compile(r"^(\s+)[A-Za-z_][\w\-]*\s*:") provider_line_re = re.compile(r"^(\s+)provider(\s*):(\s*)(.*)$") def infer_indent_from_doc() -> str: """从文档中其他顶级 mapping 的首个子键提取缩进字符串。""" in_top = False for ln in lines: if top_key_re.match(ln): in_top = True continue if in_top: if not ln.strip() or ln.lstrip().startswith("#"): continue m = sibling_key_re.match(ln) if m: return m.group(1) if top_key_re.match(ln): in_top = True continue in_top = False return " " # 极端情况:整个文件只有 memory: 自己 # 定位 memory: 顶级段 mem_idx = -1 for idx, ln in enumerate(lines): if memory_start_re.match(ln): mem_idx = idx break if mem_idx == -1: # 全文无 memory 段:直接追加(不存在格式破坏问题) indent_str = infer_indent_from_doc() if lines and not lines[-1].endswith("\n"): lines.append("\n") lines.append("memory:\n") lines.append(f"{indent_str}provider: {TARGET}\n") _atomic_write("".join(lines)) print(f"updated {path} (appended new memory section)") return # 圈定 block 范围:[mem_idx+1, end) end = len(lines) for j in range(mem_idx + 1, len(lines)): if top_key_re.match(lines[j]): end = j break block = lines[mem_idx + 1:end] # 1) 若已有 provider 行,用相同前缀原位替换该行 for k, b in enumerate(block): m = provider_line_re.match(b) if m: indent = m.group(1) sp_before = m.group(2) sp_after = m.group(3) or " " # 保留行尾注释(# 后内容) tail = m.group(4) comment = "" ci = tail.find("#") if ci >= 0: # 简单处理:value 是 # 之前的部分(去除右侧空格),后续保留注释 comment = " " + tail[ci:].rstrip("\n") new_line = f"{indent}provider{sp_before}:{sp_after}{TARGET}{comment}\n" lines[mem_idx + 1 + k] = new_line _atomic_write("".join(lines)) print(f"updated {path} (replaced provider line in-place)") return # 2) 无 provider 行:拓印同 block 内其他兄弟键的缩进 sibling_indent = None for b in block: if not b.strip() or b.lstrip().startswith("#"): continue m = sibling_key_re.match(b) if m: sibling_indent = m.group(1) break if sibling_indent is None: # block 内无任何兄弟键(如刚刚新建/只有注释)→ 从文档其它段推断 sibling_indent = infer_indent_from_doc() insert_at = mem_idx + 1 new_line = f"{sibling_indent}provider: {TARGET}\n" lines.insert(insert_at, new_line) _atomic_write("".join(lines)) print(f"updated {path} (inserted provider line)") if not try_ruamel(): sys.stderr.write( "[memory-tencentdb-ctl:info] ruamel.yaml 未安装,使用最小化原位编辑回退方案 " "(pip install ruamel.yaml 可获得最佳保真度)\n" ) fallback_inline_edit() PYEOF log "hermes memory.provider = memory_tencentdb" } # ============================================================ # 命令分发 # ============================================================ usage() { cat <<'USAGE' memory-tencentdb-ctl.sh — memory_tencentdb (TDAI) Gateway 管理脚本 运行模式: standalone (默认) Gateway 独立运行;日志落在 $TDAI_DATA_DIR/logs/;不触碰 ~/.hermes --hermes 额外做 hermes 集成:日志落 ~/.hermes/logs/memory_tencentdb/, config llm 同步写 ~/.hermes/env.d/memory-tencentdb-llm.sh, 并开放 enable-hermes-memory 子命令 (也可通过 MEMORY_TENCENTDB_MODE=hermes 全局生效) 常用: memory-tencentdb-ctl start 启动 Gateway memory-tencentdb-ctl stop 停止 Gateway memory-tencentdb-ctl restart 重启 Gateway memory-tencentdb-ctl status 查看状态 memory-tencentdb-ctl health 健康检查 (/health) memory-tencentdb-ctl logs [out|err|all] [N=200] 滚动查看日志 配置 (默认只写 $TDAI_DATA_DIR/tdai-gateway.json,即 ~/.memory-tencentdb/memory-tdai/tdai-gateway.json; --hermes 时 LLM 额外写 env.d): memory-tencentdb-ctl config llm --api-key K --base-url U --model M [--restart] memory-tencentdb-ctl config embedding --provider P --api-key K --base-url U \ --model M --dimensions D [--proxy-url U] [--restart] memory-tencentdb-ctl config embedding --provider none # 关闭 embedding memory-tencentdb-ctl config vdb --url U --api-key K --database D \ [--username root] [--alias A] [--ca-pem /path] \ [--embedding-model bge-large-zh] [--no-set-backend] [--restart] memory-tencentdb-ctl config vdb-off [--purge-creds] [--restart] # 切回本地 sqlite 存储;默认保留 tcvdb 凭据 # (仅改 storeBackend),加 --purge-creds 才清除凭据 memory-tencentdb-ctl config show # 打印配置(apiKey 已脱敏) Hermes 集成 (需 --hermes): memory-tencentdb-ctl --hermes enable-hermes-memory 设置 ~/.hermes/config.yaml 的 memory.provider; 优先 ruamel.yaml round-trip(保留注释/格式), 未安装时降级为最小化原位行编辑(缩进自动从既有键拓印)。 全局选项: --hermes / --standalone 切换运行模式(默认 standalone) --dry-run 预演所有写操作,不真正落盘 -h, --help 显示本帮助 关键环境变量: MEMORY_TENCENTDB_MODE standalone | hermes (等价于 --hermes / --standalone) MEMORY_TENCENTDB_GATEWAY_HOST / _PORT Gateway 监听地址 (default 127.0.0.1:8420) MEMORY_TENCENTDB_GATEWAY_CMD 自定义启动命令(否则走 $TDAI_INSTALL_DIR) MEMORY_TENCENTDB_LOG_DIR 覆盖日志目录 MEMORY_TENCENTDB_ROOT 统一根目录(默认 ~/.memory-tencentdb) TDAI_INSTALL_DIR / TDAI_DATA_DIR 插件源码 / 数据目录 (默认分别位于 $MEMORY_TENCENTDB_ROOT 之下) USAGE } # 拆掉全局 flag ARGS=() while [[ $# -gt 0 ]]; do case "$1" in --dry-run) DRY_RUN=1; shift ;; --hermes) MODE="hermes"; shift ;; --standalone) MODE="standalone"; shift ;; -h|--help) usage; exit 0 ;; *) ARGS+=("$1"); shift ;; esac done set -- "${ARGS[@]:-}" # 根据 MODE 初始化日志 / PID 路径 _apply_mode_paths [[ $# -ge 1 ]] || { usage; exit 1; } SUB="$1"; shift || true case "$SUB" in start) cmd_start "$@" ;; stop) cmd_stop "$@" ;; restart) cmd_restart "$@" ;; status) cmd_status "$@" ;; health) cmd_health "$@" ;; logs) cmd_logs "$@" ;; config) [[ $# -ge 1 ]] || die "config 需要子命令: llm | embedding | vdb | vdb-off | show" 1 SECTION="$1"; shift || true case "$SECTION" in llm) cmd_config_llm "$@" ;; embedding) cmd_config_embedding "$@" ;; vdb) cmd_config_vdb "$@" ;; vdb-off) cmd_config_vdb_off "$@" ;; show) cmd_config_show "$@" ;; *) die "未知 config 子命令: $SECTION" 1 ;; esac ;; enable-hermes-memory) cmd_enable_hermes_memory "$@" ;; *) usage; die "未知命令: $SUB" 1 ;; esac