#!/usr/bin/env bash # ============================================================================= # db-conn.sh —— agent 访问自己专属 MySQL 库的唯一入口(统一 helper) # # 【用途】 # agent 需要查/写自己的库(库名 = 自己的 agent id)时,一律通过本脚本。 # 不要自己拼 mysql 命令,不要用 root,不要用 docker exec 直连。 # # 【为什么需要它】 # 口令存放在 ~/.openclaw/.env,键名是中文的 DB_PASSWORD_<中文名>(便于人阅读)。 # 实测确认:中文键名**无法**作为环境变量注入 agent 的执行环境,也无法交给 # `openclaw secrets store --kind env` 托管(其名称语法限制为纯大写 ASCII)。 # 依据见 ~/.openclaw/docs/agent-创建规范与自动化-设计.md 附录 A 第 8 条。 # 因此由本脚本按 agent id 反查中文名 → 读 .env → 代入容器内的 docker MySQL。 # # 【用法】 # db-conn.sh 交互式 mysql(已切到自己的库) # db-conn.sh --sql "select 1" 执行一条 SQL,结果打到 stdout # db-conn.sh --print 只打印等价命令(口令用占位符,可安全复制) # db-conn.sh --help # # 【安全约定】 # 1. 口令只通过环境变量 MYSQL_PWD 传给容器内的 mysql 客户端(客户端原生支持), # 绝不出现在 argv/命令行,也绝不打印到 stdout/stderr,不落盘。 # 2. 传参写成 `docker exec -e MYSQL_PWD`(**不带 =值**):docker 会从本脚本进程 # 的环境里转发该变量。若写成 `-e MYSQL_PWD="$值"`,口令会进入 docker 进程的 # argv,`ps -eo args` 可见(已实测),故不采用。 # 3. 账号固定 agent_、库固定 ;脚本不提供切换账号/库的开关。 # 4. --print 用占位符替换口令,输出可安全粘贴进聊天记录/工单。 # 5. 本脚本不读取、也不使用任何 root 凭据。 # # 【退出码】 # 0 成功 # 2 用法/环境错误(参数缺失或非法、agent 不存在、中文名取不到、口令缺失) # 其他 docker 或 mysql 的原始退出码 # ============================================================================= set -euo pipefail # —— 绝对路径与固定 PATH:不依赖登录 shell / rc 文件,cron 与 agent exec 都能跑 —— export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" OPENCLAW_ROOT="${OPENCLAW_ROOT:-/home/yangxuan/.openclaw}" ENV_FILE="$OPENCLAW_ROOT/.env" CONFIG_FILE="$OPENCLAW_ROOT/openclaw.json" MYSQL_CONTAINER="mysql" usage() { cat <<'USAGE' 用法: db-conn.sh 交互式连接自己的库 db-conn.sh --sql "" 执行一条 SQL 并输出结果 db-conn.sh --print 只打印等价命令(口令以占位符显示) db-conn.sh --help 即 openclaw.json 中 agents.entries 的键(如 main、juaner、sql)。 账号固定为 agent_,库固定为 ;口令自动从 ~/.openclaw/.env 按中文键名 DB_PASSWORD_<中文名> 读取,不会出现在命令行里。 USAGE } # —— 打印用:把 SQL 包成可直接复制的 shell 字面量(优先单引号,含单引号时退回 %q)—— quote_sql() { local s="$1" if [[ "$s" != *"'"* ]]; then printf "'%s'" "$s" else printf '%q' "$s" fi } # —— ⓪ 参数解析 —— AGENT_ID="" SQL="" PRINT_ONLY=0 while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage; exit 0 ;; --print) PRINT_ONLY=1; shift ;; --sql) [[ $# -ge 2 ]] || { echo "错误:--sql 后面缺少 SQL 语句" >&2; usage >&2; exit 2; } SQL="$2"; shift 2 ;; --sql=*) SQL="${1#--sql=}"; shift ;; --*) echo "错误:未知参数 $1" >&2; usage >&2; exit 2 ;; *) if [[ -z "$AGENT_ID" ]]; then AGENT_ID="$1"; shift else echo "错误:多余的参数 $1(只接受一个 )" >&2; usage >&2; exit 2 fi ;; esac done [[ -n "$AGENT_ID" ]] || { echo "错误:缺少 " >&2; usage >&2; exit 2; } [[ "$AGENT_ID" =~ ^[a-z][a-z0-9-]{0,23}$ ]] || { echo "错误:agent_id 非法(只允许小写字母开头、[a-z0-9-]、最长 24 字符):$AGENT_ID" >&2 exit 2 } DB_USER="agent_$AGENT_ID" DB_NAME="$AGENT_ID" # —— ① 环境依赖 —— command -v python3 >/dev/null 2>&1 || { echo "错误:找不到 python3(本脚本用它解析 $CONFIG_FILE)" >&2; exit 2; } command -v docker >/dev/null 2>&1 || { echo "错误:找不到 docker 命令" >&2; exit 2; } [[ -f "$CONFIG_FILE" ]] || { echo "错误:找不到配置文件 $CONFIG_FILE" >&2; exit 2; } [[ -f "$ENV_FILE" ]] || { echo "错误:找不到口令文件 $ENV_FILE" >&2; exit 2; } # —— ② agent_id → 中文名(agents.entries..name;取不到即报错)—— AGENT_NAME="" NAME_RC=0 AGENT_NAME="$(python3 - "$CONFIG_FILE" "$AGENT_ID" <<'PY' import json, sys cfg_path, agent_id = sys.argv[1], sys.argv[2] try: with open(cfg_path, encoding="utf-8") as fh: cfg = json.load(fh) except Exception as exc: sys.stderr.write("读取 %s 失败:%s\n" % (cfg_path, exc)) sys.exit(3) entries = ((cfg.get("agents") or {}).get("entries")) or {} entry = entries.get(agent_id) if not isinstance(entry, dict): sys.exit(4) # 主键是 name;个别条目若只有 identity.name,则兜底取它,避免 helper 对拓扑差异敏感 name = str(entry.get("name") or "").strip() if not name and isinstance(entry.get("identity"), dict): name = str(entry["identity"].get("name") or "").strip() if not name: sys.exit(5) sys.stdout.write(name) PY )" || NAME_RC=$? case "$NAME_RC" in 0) ;; 4) echo "错误:agent '$AGENT_ID' 不在 $CONFIG_FILE 的 agents.entries 中" >&2; exit 2 ;; 5) echo "错误:agent '$AGENT_ID' 在配置里没有 name(中文名),无法定位其口令键" >&2; exit 2 ;; *) echo "错误:解析 $CONFIG_FILE 失败(python3 退出码 $NAME_RC)" >&2; exit 2 ;; esac # —— ③ 中文名 → 口令(.env 内键名 DB_PASSWORD_<中文名>)—— PWD_KEY="DB_PASSWORD_$AGENT_NAME" PWD_VALUE="" PWD_VALUE="$( awk -v k="$PWD_KEY" 'index($0, k "=") == 1 { print substr($0, length(k) + 2); exit }' "$ENV_FILE" )" # 去掉行尾 CR 与成对引号(.env 可能被编辑器写坏) PWD_VALUE="${PWD_VALUE%$'\r'}" case "$PWD_VALUE" in \"*\") PWD_VALUE="${PWD_VALUE:1:${#PWD_VALUE}-2}" ;; \'*\') PWD_VALUE="${PWD_VALUE:1:${#PWD_VALUE}-2}" ;; esac if [[ -z "$PWD_VALUE" ]]; then echo "错误:$ENV_FILE 里没有 $PWD_KEY(agent '$AGENT_ID'/中文名 '$AGENT_NAME')" >&2 echo " 该 agent 可能尚未纳入 —— 请联系杨轩用 new-agent.sh 补建/纳入后重试" >&2 exit 2 fi # —— ④ --print:只打印等价命令,口令用占位符(绝不打印真实值)—— if [[ "$PRINT_ONLY" == "1" ]]; then if [[ -n "$SQL" ]]; then printf "MYSQL_PWD='<从 .env 读取:%s>' docker exec -e MYSQL_PWD %s mysql -u%s -N -B -e %s %s\n" \ "$PWD_KEY" "$MYSQL_CONTAINER" "$DB_USER" "$(quote_sql "$SQL")" "$DB_NAME" else # 交互式:本地有 TTY 才能 -it(与下面实际执行时的判定保持一致) if [[ -t 0 && -t 1 ]]; then TTY_FLAGS="-it"; else TTY_FLAGS="-i"; fi printf "MYSQL_PWD='<从 .env 读取:%s>' docker exec %s -e MYSQL_PWD %s mysql -u%s %s\n" \ "$PWD_KEY" "$TTY_FLAGS" "$MYSQL_CONTAINER" "$DB_USER" "$DB_NAME" fi exit 0 fi # —— ⑤ 连接:口令经 MYSQL_PWD 环境变量转发(不进 argv、不进 stdout)—— if [[ -n "$SQL" ]]; then MYSQL_PWD="$PWD_VALUE" docker exec -e MYSQL_PWD "$MYSQL_CONTAINER" \ mysql -u"$DB_USER" -N -B -e "$SQL" "$DB_NAME" else if [[ -t 0 && -t 1 ]]; then TTY_FLAGS="-it"; else TTY_FLAGS="-i"; fi echo "→ 已连接:库 $DB_NAME,账号 $DB_USER(输入 exit 或按 Ctrl-D 退出)" >&2 MYSQL_PWD="$PWD_VALUE" docker exec $TTY_FLAGS -e MYSQL_PWD "$MYSQL_CONTAINER" \ mysql -u"$DB_USER" "$DB_NAME" fi