31c9958741
- 新增 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 环境的三条实测依据
230 lines
11 KiB
Bash
Executable File
230 lines
11 KiB
Bash
Executable File
#!/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)"
|
||
|
||
# —— ④ SQL(dry-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-identity(agent 与 workspace 保持不变)"
|
||
elif [[ "$MODE" == "adopt" ]]; then
|
||
echo "[dry-run] 将跳过:openclaw agents add / 模板渲染 / set-identity(agent 条目已存在=$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; }
|