Files
openclaw-config/scripts/new-agent.sh
T
openclaw-314 83439ee623 feat(agent): 建立 agent 创建规范与数据库自动化
- scripts/new-agent.sh:建库/建号/授权/写 .env/创建 agent/渲染中文模板/自检,幂等且默认拒绝覆盖
- scripts/agent-templates/:5 个中文模板(IDENTITY/SOUL/AGENTS/MEMORY/USER)
- skills/agent-provisioning/:main 创建 agent 的唯一入口与红线(description 覆盖中英触发说法)
- workspace/:修复悬空 gitlink,将 9 个配置文档纳入版本控制(含新增创建 agent 与数据库红线)
- docs/:设计文档(含附录 A 实测发现)、实现计划(含 sudo 事故修正)
- .gitignore:workspace 白名单,避免数据/技能/媒体进入版本库
2026-09-16 11:18:16 +08:00

207 lines
9.2 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 → 渲染中文模板 → 自检
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]"; }
# —— ⓪ 参数解析(参数不足直接给用法,不当作未知参数)——
[[ $# -ge 2 ]] || { usage; exit 2; }
AGENT_ID="$1"; AGENT_NAME="$2"; shift 2
DRY_RUN=0; RESET_PASSWORD=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
--reset-password) RESET_PASSWORD=1 ;;
*) echo "未知参数: $arg" >&2; usage; exit 2 ;;
esac
done
# —— ① 入参校验 ——
[[ -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; }
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"; 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 保持不变)"
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
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"
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"
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
EOF
[[ "$SELFCHECK" == "ok" ]] || { echo "⚠️ 自检失败:请检查授权"; exit 1; }