Files
openclaw-config/scripts/new-agent.sh
T
openclaw-314 31c9958741 feat(agent-db): 引入 db-conn.sh 统一数据库入口,各 agent 改用专用账号
- 新增 scripts/db-conn.sh:按 agent id 反查中文名,读 .env 后经 MYSQL_PWD 转发给 docker exec,口令不进 argv/stdout,--print 只输出占位符
- new-agent.sh 新增 --adopt 模式(为既有 agent 补建库/账号),与 --reset-password 互斥
- AGENTS.md.tpl 与 11 个 workspace-*/AGENTS.md 改用 db-conn.sh,禁用 root 与 docker exec 直连
- mysql-schema-sync 技能改用 MYSQL_PWD_SQL 环境变量,不再硬编码密码
- 设计文档补附录 A 第 8 条:中文键名无法注入 agent 环境的三条实测依据
2026-09-16 11:30:55 +08:00

230 lines
11 KiB
Bash
Executable File
Raw 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.
#!/usr/bin/env bash
# 新建 OpenClaw agent:建库 → 建号 → 授权 → 写 .env → 创建 agent → 渲染中文模板 → 自检
# 另含 --reset-password(重置既有账号密码)与 --adopt(为既有 agent 补建缺失的库/账号)两个模式
set -euo pipefail
ENV_FILE="$HOME/.openclaw/.env"
TEMPLATE_DIR="$HOME/.openclaw/scripts/agent-templates"
OPENCLAW_ROOT="$HOME/.openclaw"
LOG_DIR="$OPENCLAW_ROOT/workspace/db"
LOG_FILE="$LOG_DIR/agent-provision.log"
MYSQL_CONTAINER="mysql"
# 渲染的模板清单(全部中文,.tpl 内含 {{AGENT_ID}} / {{AGENT_NAME}} / {{CREATED_AT}} 占位符)
TEMPLATES=(IDENTITY SOUL AGENTS MEMORY USER)
TEMPLATE_LIST="$(printf '%s.md ' "${TEMPLATES[@]}")"
usage() { echo "用法: new-agent.sh <id> <中文名> [--dry-run] [--reset-password] [--adopt]"; }
# —— ⓪ 参数解析(参数不足直接给用法,不当作未知参数)——
[[ $# -ge 2 ]] || { usage; exit 2; }
AGENT_ID="$1"; AGENT_NAME="$2"; shift 2
DRY_RUN=0; RESET_PASSWORD=0; ADOPT=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--reset-password) RESET_PASSWORD=1 ;;
--adopt) ADOPT=1 ;;
*) echo "未知参数: $arg" >&2; usage; exit 2 ;;
esac
done
[[ $((RESET_PASSWORD + ADOPT)) -le 1 ]] || { echo "--reset-password 与 --adopt 互斥,不能同时使用" >&2; exit 2; }
# —— ① 入参校验 ——
[[ -n "$AGENT_ID" && -n "$AGENT_NAME" ]] || { usage; exit 2; }
[[ "$AGENT_ID" =~ ^[a-z][a-z0-9-]{0,23}$ ]] || { echo "id 非法:只允许小写字母开头、[a-z0-9-]、最长 24 字符"; exit 2; }
[[ "$AGENT_NAME" =~ ^[^[:space:]]{2,20}$ ]] || { echo "中文名非法:2–20 个字符且不含空白"; exit 2; }
WORKSPACE="$OPENCLAW_ROOT/workspace-$AGENT_ID"
DB_USER="agent_$AGENT_ID"
PASSWORD_VAR="DB_PASSWORD_$AGENT_NAME"
# —— root 凭据:从 .env 读,绝不硬编码 ——
# 注:bk02 现网 .env 的 root 密码键名为 MYSQL_PWD(无 MYSQL_PWD_ROOT),
# 故按 MYSQL_PWD_ROOT → MYSQL_PWD 顺序回退读取,脚本内不出现任何明文口令。
# 管道末尾的 `|| true` 不可省略:grep 无匹配时返回 1,配合 set -o pipefail
# 会让赋值语句失败并触发 set -e,脚本将无声退出。
read_env_var() {
grep -E "^$1=" "$ENV_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '"'"'"'' || true
}
MYSQL_ROOT_PWD="$(read_env_var MYSQL_PWD_ROOT)"
if [[ -z "$MYSQL_ROOT_PWD" ]]; then
MYSQL_ROOT_PWD="$(read_env_var MYSQL_PWD)"
fi
[[ -n "$MYSQL_ROOT_PWD" ]] || { echo "无法从 $ENV_FILE 读取 MYSQL_PWD_ROOT(或回退键 MYSQL_PWD"; exit 1; }
mysql_root() { docker exec -e MYSQL_PWD="$MYSQL_ROOT_PWD" "$MYSQL_CONTAINER" mysql -uroot -N -B -e "$1"; }
# —— ② 冲突检查 ——
DB_EXISTS="$(mysql_root "select count(*) from information_schema.schemata where schema_name='$AGENT_ID';")"
USER_EXISTS="$(mysql_root "select count(*) from mysql.user where user='$DB_USER';")"
AGENT_EXISTS="$(python3 -c "
import json
d=json.load(open('$OPENCLAW_ROOT/openclaw.json'))
print(1 if '$AGENT_ID' in (d.get('agents') or {}).get('entries',{}) else 0)
")"
WS_EXISTS=$([[ -d "$WORKSPACE" ]] && echo 1 || echo 0)
if [[ "$RESET_PASSWORD" == "1" ]]; then
[[ "$USER_EXISTS" == "1" ]] || { echo "账号 $DB_USER 不存在,无法重置密码"; exit 1; }
elif [[ "$ADOPT" == "1" ]]; then
# 纳入模式:只补「库/账号缺失」的存量 agent,跳过 创建 agent / 模板渲染 / set-identity。
# 库或账号已存在时仍拒绝 —— 保持与 create 模式一致的幂等语义,避免覆盖既有数据。
[[ "$DB_EXISTS" == "0" ]] || { echo "❌ 库 $AGENT_ID 已存在 —— --adopt 只补建缺失的库/账号;库已存在请改用 new-agent.sh --reset-password 或手工授权"; exit 1; }
[[ "$USER_EXISTS" == "0" ]] || { echo "❌ 账号 $DB_USER 已存在 —— --adopt 拒绝;如需重置密码请用 --reset-password"; exit 1; }
[[ "$AGENT_EXISTS" == "1" || "$WS_EXISTS" == "1" ]] || { echo "❌ agent 条目与目录 $WORKSPACE 都不存在 —— 这属于新建场景,请去掉 --adopt"; exit 1; }
else
[[ "$DB_EXISTS" == "0" ]] || { echo "❌ 库 $AGENT_ID 已存在 —— 已存在请使用 --reset-password,或改用 new-agent.sh 管理"; exit 1; }
[[ "$USER_EXISTS" == "0" ]] || { echo "❌ 账号 $DB_USER 已存在 —— 请使用 --reset-password"; exit 1; }
[[ "$AGENT_EXISTS" == "0" ]] || { echo "❌ agent 条目 $AGENT_ID 已存在"; exit 1; }
[[ "$WS_EXISTS" == "0" ]] || { echo "❌ 目录 $WORKSPACE 已存在"; exit 1; }
fi
# —— ③ 生成密码 ——
NEW_PWD="$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-32)"
# —— ④ SQLdry-run 只打印)——
SQL_CREATE_DB="CREATE DATABASE \`$AGENT_ID\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"
SQL_CREATE_USER="CREATE USER '$DB_USER'@'%' IDENTIFIED BY '$NEW_PWD';"
SQL_ALTER_USER="ALTER USER '$DB_USER'@'%' IDENTIFIED BY '$NEW_PWD';"
SQL_GRANT="GRANT ALL PRIVILEGES ON \`$AGENT_ID\`.* TO '$DB_USER'@'%';"
if [[ "$RESET_PASSWORD" == "1" ]]; then MODE="reset-password"
elif [[ "$ADOPT" == "1" ]]; then MODE="adopt"
else MODE="create"; fi
if [[ "$DRY_RUN" == "1" ]]; then
echo "[dry-run] 模式:$MODE"
echo "[dry-run] 将执行:"
if [[ "$MODE" == "reset-password" ]]; then
if [[ "$DB_EXISTS" == "0" ]]; then
echo " $SQL_CREATE_DB # 账号在、库缺失,补建"
fi
echo " $SQL_ALTER_USER # 替代 CREATE USER"
else
echo " $SQL_CREATE_DB"
echo " $SQL_CREATE_USER"
fi
echo " $SQL_GRANT"
echo "[dry-run] 将写入 $ENV_FILE$PASSWORD_VAR=<32位随机>"
if [[ "$MODE" == "reset-password" ]]; then
echo "[dry-run] 将跳过:openclaw agents add / 模板渲染 / set-identityagent 与 workspace 保持不变)"
elif [[ "$MODE" == "adopt" ]]; then
echo "[dry-run] 将跳过:openclaw agents add / 模板渲染 / set-identityagent 条目已存在=$AGENT_EXISTS,目录已存在=$WS_EXISTS,不覆盖既有内容)"
else
echo "[dry-run] 将创建 workspace$WORKSPACE"
echo "[dry-run] 将执行:openclaw agents add $AGENT_ID --workspace $WORKSPACE --non-interactive"
echo "[dry-run] 将渲染 ${#TEMPLATES[@]} 个中文模板到 $WORKSPACE$TEMPLATE_LIST"
echo "[dry-run] 将删除英文骨架:$WORKSPACE/BOOTSTRAP.md(身份已由 IDENTITY.md 确定,不再走起名仪式)"
echo "[dry-run] 将删除:$WORKSPACE/.git(与存量 12 个 workspace 保持一致,避免嵌套仓库污染 ~/.openclaw"
fi
exit 0
fi
if [[ "$MODE" == "reset-password" ]]; then
# 重置模式:库通常已存在,不重复执行 CREATE DATABASE —— 否则 MySQL 会抛
# ERROR 1007 干扰输出(即便 `|| true` 吞掉退出码,错误仍会打在 stderr 上)。
# 仅在「账号在、库丢失」的异常状态下补建。
if [[ "$DB_EXISTS" == "0" ]]; then
echo "⚠️ 库 $AGENT_ID 不存在(账号存在但库缺失),补建中"
mysql_root "$SQL_CREATE_DB" >/dev/null
fi
mysql_root "$SQL_ALTER_USER" >/dev/null
else
# create 与 adopt 共用:建库 + 建号(adopt 已在上面的检查中确认两者都不存在)
mysql_root "$SQL_CREATE_DB" >/dev/null
mysql_root "$SQL_CREATE_USER" >/dev/null
fi
mysql_root "$SQL_GRANT" >/dev/null
# —— ⑤ 写 .env(同键覆盖,不重复追加)——
if grep -q "^${PASSWORD_VAR}=" "$ENV_FILE"; then
python3 - "$ENV_FILE" "$PASSWORD_VAR" "$NEW_PWD" <<'PY'
import sys
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
lines = open(path, encoding='utf-8').read().splitlines()
out = [f'{key}={val}' if l.startswith(key + '=') else l for l in lines]
open(path, 'w', encoding='utf-8').write('\n'.join(out) + '\n')
PY
else
printf '%s=%s\n' "$PASSWORD_VAR" "$NEW_PWD" >> "$ENV_FILE"
fi
chmod 600 "$ENV_FILE"
if [[ "$MODE" == "reset-password" ]]; then
# —— 重置模式:跳过 ⑥ 创建 agent / ⑦ 模板渲染 / ⑧ set-identity ——
# agent 条目、workspace 与模板都已存在,重复执行只会失败或覆盖既有内容。
echo "ℹ️ 重置模式:跳过 创建 agent / 模板渲染 / set-identity"
elif [[ "$MODE" == "adopt" ]]; then
# —— 纳入模式:同样跳过 ⑥⑦⑧ ——
# agent 条目与 workspace(含既有 AGENTS.md 等文档)都已存在,
# 本模式只负责「建库 → 建账号 → 授权 → 写 .env」,绝不触碰既有 workspace 内容。
echo "ℹ️ 纳入模式(adopt):跳过 创建 agent / 模板渲染 / set-identity(不覆盖既有 workspace 内容)"
else
# —— ⑥ 创建 agent ——
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; nvm use 26 >/dev/null 2>&1
openclaw agents add "$AGENT_ID" --workspace "$WORKSPACE" --non-interactive >/dev/null
# —— ⑦ 渲染中文模板(覆盖 openclaw 生成的英文骨架)——
# 用 python3 逐字替换而非 sed:中文名校验允许 `/` 与 `&`
# 它们在 sed 的 s/// 里分别是分隔符与"整个匹配"引用,会造成渲染失败或静默错替换。
CREATED_AT="$(date '+%Y-%m-%d')"
python3 - "$TEMPLATE_DIR" "$WORKSPACE" "$AGENT_ID" "$AGENT_NAME" "$CREATED_AT" "${TEMPLATES[*]}" <<'PY'
import sys, pathlib
tpl_dir, ws, aid, aname, created, names = sys.argv[1:7]
repl = {'{{AGENT_ID}}': aid, '{{AGENT_NAME}}': aname, '{{CREATED_AT}}': created}
for name in names.split():
src = pathlib.Path(tpl_dir, name + '.md.tpl').read_text(encoding='utf-8')
for key, val in repl.items():
src = src.replace(key, val)
pathlib.Path(ws, name + '.md').write_text(src, encoding='utf-8')
PY
mkdir -p "$WORKSPACE/memory"
# openclaw agents add 会初始化一个 git 仓库,但存量 12 个 workspace 全都没有 →
# 删掉它,避免嵌套仓库污染父仓库 ~/.openclaw 的提交。
rm -rf "$WORKSPACE/.git"
# BOOTSTRAP.md 会让 agent 首次对话走"起名仪式"(要求用户给它起名),
# 而中文名在创建时就已确定并写入 IDENTITY.md → 删除,避免语义矛盾。
rm -f "$WORKSPACE/BOOTSTRAP.md"
# —— ⑧ 中文名同步进配置 ——
# --from-identity 只解析 `- Name: xxx` 这类英文标签,中文模板必然解析失败,
# 因此实际上总是走 --name 兜底分支。
openclaw agents set-identity --agent "$AGENT_ID" --from-identity >/dev/null 2>&1 || \
openclaw agents set-identity --agent "$AGENT_ID" --name "$AGENT_NAME" >/dev/null
fi
# —— ⑨ 自检:证明授权真的生效 ——
if docker exec -e MYSQL_PWD="$NEW_PWD" "$MYSQL_CONTAINER" mysql -u"$DB_USER" -N -B -e "SELECT 1;" "$AGENT_ID" >/dev/null 2>&1; then
SELFCHECK="ok"
else
SELFCHECK="FAILED"
fi
mkdir -p "$LOG_DIR"
printf '[%s] id=%s name=%s user=%s mode=%s selfcheck=%s dry_run=%s\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" "$AGENT_ID" "$AGENT_NAME" "$DB_USER" "$MODE" "$SELFCHECK" "$DRY_RUN" >> "$LOG_FILE"
if [[ "$MODE" == "reset-password" ]]; then
echo ""
echo "✅ 已重置 agent 密码:$AGENT_NAME$AGENT_ID"
elif [[ "$MODE" == "adopt" ]]; then
echo ""
echo "✅ 已为既有 agent 补建专属库与专用账号:$AGENT_NAME$AGENT_ID"
else
echo ""
echo "✅ 已创建 agent$AGENT_NAME$AGENT_ID"
fi
cat <<EOF
MODE=$MODE
DATABASE=$AGENT_ID
USER=$DB_USER
PASSWORD_ENV=$PASSWORD_VAR
WORKSPACE=$WORKSPACE
自检=$SELFCHECK
连接:$OPENCLAW_ROOT/scripts/db-conn.sh $AGENT_ID (交互式;或加 --sql "<SQL>" 执行单条 SQL;口令自动从 .env 读取,不进命令行)
EOF
[[ "$SELFCHECK" == "ok" ]] || { echo "⚠️ 自检失败:请检查授权"; exit 1; }