@@ -0,0 +1,206 @@
#!/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) "
# —— ④ 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" ; 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 保持不变)"
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; }