optimize guko fork installer and backup tools

This commit is contained in:
OpenClaw
2026-06-23 00:58:45 +00:00
parent 044e432d69
commit 020453fa09
3 changed files with 174 additions and 7 deletions

View File

@@ -1,7 +1,10 @@
name: guko name: guko
services: services:
guko-bot: guko-bot:
image: ghcr.io/shuijiao1/guko:latest image: guko:local
build:
context: .
dockerfile: telegram-bot/Dockerfile
container_name: guko-bot container_name: guko-bot
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env

View File

@@ -6,6 +6,9 @@ INSTALL_DIR="${GUKO_INSTALL_DIR:-/opt/guko}"
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml" COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"
ENV_FILE="${INSTALL_DIR}/.env" ENV_FILE="${INSTALL_DIR}/.env"
SERVERS_FILE="${INSTALL_DIR}/servers.json" SERVERS_FILE="${INSTALL_DIR}/servers.json"
IMAGE_NAME="${GUKO_IMAGE:-guko:local}"
DEFAULT_TG_ID="${GUKO_TG_ID:-165067365}"
BACKUP_DIR="${GUKO_BACKUP_DIR:-${INSTALL_DIR}/backups}"
need_root() { need_root() {
if [ "$(id -u)" -ne 0 ]; then if [ "$(id -u)" -ne 0 ]; then
@@ -23,6 +26,50 @@ install_docker() {
systemctl enable --now docker >/dev/null 2>&1 || true systemctl enable --now docker >/dev/null 2>&1 || true
} }
render_compose() {
cat > "${COMPOSE_FILE}" <<COMPOSE
name: guko
services:
guko-bot:
image: ${IMAGE_NAME}
build:
context: .
dockerfile: telegram-bot/Dockerfile
container_name: guko-bot
restart: unless-stopped
env_file: .env
volumes:
- ./servers.json:/data/servers.json
- ./keys:/data/keys
- ./media:/data/media
- ./results:/data/results
- ./history.json:/data/history.json
- ./tmp:/data/tmp
COMPOSE
}
fetch_source() {
echo "==> 下载 GUKO 源码"
local tmp
tmp="$(mktemp -d)"
fetch "${BASE_URL}/telegram-bot/Dockerfile" "${tmp}/Dockerfile.check"
rm -rf "${INSTALL_DIR}/telegram-bot" "${INSTALL_DIR}/optional" "${INSTALL_DIR}/scripts" \
"${INSTALL_DIR}/auth.py" "${INSTALL_DIR}/guko.py" "${INSTALL_DIR}/jiaoops.py" \
"${INSTALL_DIR}/Makefile" "${INSTALL_DIR}/README.md" "${INSTALL_DIR}/LICENSE"
fetch "https://mjjtop.com/admin/guko/archive/main.tar.gz" "${tmp}/guko.tar.gz"
tar -xzf "${tmp}/guko.tar.gz" -C "${tmp}"
local src
src="$(find "${tmp}" -maxdepth 1 -type d \( -name 'guko' -o -name 'guko-*' \) | head -1)"
if [ -z "${src}" ] || [ ! -d "${src}" ]; then
src="$(find "${tmp}" -maxdepth 1 -mindepth 1 -type d | head -1)"
fi
cp -a "${src}/auth.py" "${src}/guko.py" "${src}/jiaoops.py" "${src}/telegram-bot" "${INSTALL_DIR}/"
[ -d "${src}/optional" ] && cp -a "${src}/optional" "${INSTALL_DIR}/" || true
[ -d "${src}/scripts" ] && cp -a "${src}/scripts" "${INSTALL_DIR}/" || true
cp -a "${src}/README.md" "${src}/LICENSE" "${INSTALL_DIR}/" 2>/dev/null || true
rm -rf "${tmp}"
}
fetch() { fetch() {
local url="$1" out="$2" local url="$1" out="$2"
curl -fL --retry 3 --connect-timeout 10 "$url" -o "$out" curl -fL --retry 3 --connect-timeout 10 "$url" -o "$out"
@@ -33,14 +80,15 @@ init_files() {
mkdir -p "${INSTALL_DIR}" "${INSTALL_DIR}/keys" "${INSTALL_DIR}/media" "${INSTALL_DIR}/results" "${INSTALL_DIR}/tmp" mkdir -p "${INSTALL_DIR}" "${INSTALL_DIR}/keys" "${INSTALL_DIR}/media" "${INSTALL_DIR}/results" "${INSTALL_DIR}/tmp"
chmod 700 "${INSTALL_DIR}/keys" chmod 700 "${INSTALL_DIR}/keys"
fetch "${BASE_URL}/docker-compose.example.yml" "${COMPOSE_FILE}" fetch_source
render_compose
if [ ! -f "${ENV_FILE}" ]; then if [ ! -f "${ENV_FILE}" ]; then
fetch "${BASE_URL}/.env.example" "${ENV_FILE}" fetch "${BASE_URL}/.env.example" "${ENV_FILE}"
sed -i \ sed -i \
-e 's/^BOT_TOKEN=.*/BOT_TOKEN=replace-me/' \ -e 's/^BOT_TOKEN=.*/BOT_TOKEN=replace-me/' \
-e 's/^ALLOWED_USERS=.*/ALLOWED_USERS=123456789/' \ -e "s/^ALLOWED_USERS=.*/ALLOWED_USERS=${DEFAULT_TG_ID}/" \
-e 's/^ADMIN_USERS=.*/ADMIN_USERS=123456789/' \ -e "s/^ADMIN_USERS=.*/ADMIN_USERS=${DEFAULT_TG_ID}/" \
"${ENV_FILE}" "${ENV_FILE}"
fi fi
@@ -60,7 +108,11 @@ GUKO 一键部署脚本
bash <(curl -fsSL https://mjjtop.com/guko) restart 重启 bash <(curl -fsSL https://mjjtop.com/guko) restart 重启
bash <(curl -fsSL https://mjjtop.com/guko) logs 看日志 bash <(curl -fsSL https://mjjtop.com/guko) logs 看日志
bash <(curl -fsSL https://mjjtop.com/guko) status 状态 bash <(curl -fsSL https://mjjtop.com/guko) status 状态
bash <(curl -fsSL https://mjjtop.com/guko) update 更新镜像并重启 bash <(curl -fsSL https://mjjtop.com/guko) update 更新源码/重建镜像并重启
bash <(curl -fsSL https://mjjtop.com/guko) pull 只更新源码和 compose
bash <(curl -fsSL https://mjjtop.com/guko) backup 备份配置/密钥/历史
bash <(curl -fsSL https://mjjtop.com/guko) doctor 检查部署状态
bash <(curl -fsSL https://mjjtop.com/guko) repo 打印仓库地址
安装目录:${INSTALL_DIR} 安装目录:${INSTALL_DIR}
首次安装后必须编辑:${ENV_FILE} 首次安装后必须编辑:${ENV_FILE}
@@ -75,6 +127,33 @@ compose() {
docker compose -f "${COMPOSE_FILE}" "$@" docker compose -f "${COMPOSE_FILE}" "$@"
} }
backup_data() {
mkdir -p "${BACKUP_DIR}"
chmod 700 "${BACKUP_DIR}"
local out
out="${BACKUP_DIR}/guko-backup-$(date +%Y%m%d-%H%M%S).tar.gz"
tar -C "${INSTALL_DIR}" -czf "${out}" \
.env servers.json history.json keys media results tmp 2>/dev/null || true
chmod 600 "${out}"
echo "✅ 备份完成:${out}"
}
doctor() {
echo "== GUKO doctor =="
echo "install_dir=${INSTALL_DIR}"
echo "compose=${COMPOSE_FILE}"
command -v docker >/dev/null 2>&1 && docker --version || echo "docker=missing"
docker compose version 2>/dev/null || echo "docker compose=missing"
[ -f "${ENV_FILE}" ] && echo ".env=ok" || echo ".env=missing"
[ -f "${SERVERS_FILE}" ] && echo "servers.json=ok" || echo "servers.json=missing"
if [ -f "${ENV_FILE}" ]; then
grep -E '^(BOT_TOKEN|ALLOWED_USERS|ADMIN_USERS)=' "${ENV_FILE}" | sed -E 's/(BOT_TOKEN=).*/\1***/'
fi
if [ -f "${COMPOSE_FILE}" ] && command -v docker >/dev/null 2>&1; then
compose ps || true
fi
}
cmd="${1:-install}" cmd="${1:-install}"
case "$cmd" in case "$cmd" in
install|init) install|init)
@@ -96,7 +175,7 @@ MSG
up|start) up|start)
need_root need_root
[ -f "${COMPOSE_FILE}" ] || { echo "未安装,先执行 install" >&2; exit 1; } [ -f "${COMPOSE_FILE}" ] || { echo "未安装,先执行 install" >&2; exit 1; }
compose pull compose build --pull
compose up -d compose up -d
compose ps compose ps
;; ;;
@@ -111,10 +190,27 @@ MSG
;; ;;
update) update)
need_root need_root
compose pull install_docker
init_files
compose build --pull
compose up -d compose up -d
compose ps compose ps
;; ;;
pull)
need_root
init_files
;;
backup)
need_root
backup_data
;;
doctor)
need_root
doctor
;;
repo)
echo "https://mjjtop.com/guko-repo"
;;
logs) logs)
need_root need_root
compose logs -f --tail=200 compose logs -f --tail=200

View File

@@ -217,6 +217,58 @@ def next_manual_id(servers):
return n return n
def backup_bundle_path() -> Path:
ts = datetime.now().strftime('%Y%m%d-%H%M%S')
out = TMP_DIR / f'guko-backup-{ts}.tar.gz'
out.parent.mkdir(parents=True, exist_ok=True)
files = []
for path in (SERVERS_JSON, HISTORY_JSON):
if path.exists():
files.append(path)
for d in (KEYS_DIR, MEDIA_DIR, RESULTS_DIR):
if d.exists():
files.append(d)
import tarfile
with tarfile.open(out, 'w:gz') as tar:
for item in files:
arc = item.name if item.is_file() else item.name
tar.add(item, arcname=arc)
try:
os.chmod(out, 0o600)
except Exception:
pass
return out
def inventory_summary_text() -> str:
inv = load_inventory()
servers = inv.get('servers') or []
total = len(servers)
password = key = skipped = 0
for item in servers:
try:
cfg = resolve_ssh(item, inv)
if cfg.get('auth') == 'password':
password += 1
elif cfg.get('key'):
key += 1
else:
skipped += 1
except Exception:
skipped += 1
return (
'📦 <b>GUKO 配置概览</b>\n\n'
f'服务器:<b>{total}</b> 台\n'
f'密钥登录:{key}\n'
f'密码登录:{password}\n'
f'未完整配置:{skipped}\n'
f'清单:<code>{safe(str(SERVERS_JSON))}</code>\n'
f'密钥目录:<code>{safe(str(KEYS_DIR))}</code>\n'
f'结果目录:<code>{safe(str(RESULTS_DIR))}</code>'
)
def redact_inventory(inv: dict): def redact_inventory(inv: dict):
def clean_server(s): def clean_server(s):
out = json.loads(json.dumps(s, ensure_ascii=False)) out = json.loads(json.dumps(s, ensure_ascii=False))
@@ -3253,6 +3305,18 @@ async def export_config_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
await update.message.reply_document(document=f, filename='guko-servers-redacted.json', caption='已导出脱敏配置(密码已隐藏)。') await update.message.reply_document(document=f, filename='guko-servers-redacted.json', caption='已导出脱敏配置(密码已隐藏)。')
async def backup_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not await admin_guard(update): return
path = backup_bundle_path()
with path.open('rb') as f:
await update.message.reply_document(document=f, filename=path.name, caption='GUKO 完整备份servers/history/keys/media/results。请妥善保存。')
async def summary_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not await guard(update): return
await update.message.reply_text(inventory_summary_text(), parse_mode=ParseMode.HTML)
async def testall_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE): async def testall_cmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not await admin_guard(update): return if not await admin_guard(update): return
servers = [s for s in load_inventory().get('servers', []) if s.get('host')] servers = [s for s in load_inventory().get('servers', []) if s.get('host')]
@@ -3475,6 +3539,8 @@ async def post_init(app: Application):
BotCommand('testssh', '测试服务器 SSH'), BotCommand('testssh', '测试服务器 SSH'),
BotCommand('testall', '批量测试 SSH'), BotCommand('testall', '批量测试 SSH'),
BotCommand('exportconfig', '导出脱敏配置'), BotCommand('exportconfig', '导出脱敏配置'),
BotCommand('backup', '导出完整备份'),
BotCommand('summary', '配置概览'),
BotCommand('info', '查看单台操作面板:/info 名字/IP/ID'), BotCommand('info', '查看单台操作面板:/info 名字/IP/ID'),
BotCommand('jobs', '查看后台任务'), BotCommand('jobs', '查看后台任务'),
BotCommand('history', '查看测试历史:/history 服务器'), BotCommand('history', '查看测试历史:/history 服务器'),
@@ -3925,6 +3991,8 @@ def main():
app.add_handler(CommandHandler('testssh', testssh_cmd)) app.add_handler(CommandHandler('testssh', testssh_cmd))
app.add_handler(CommandHandler('testall', testall_cmd)) app.add_handler(CommandHandler('testall', testall_cmd))
app.add_handler(CommandHandler('exportconfig', export_config_cmd)) app.add_handler(CommandHandler('exportconfig', export_config_cmd))
app.add_handler(CommandHandler('backup', backup_cmd))
app.add_handler(CommandHandler('summary', summary_cmd))
app.add_handler(CommandHandler('info', info_cmd)) app.add_handler(CommandHandler('info', info_cmd))
app.add_handler(CommandHandler('jobs', jobs_cmd)) app.add_handler(CommandHandler('jobs', jobs_cmd))
app.add_handler(CommandHandler('history', history_cmd)) app.add_handler(CommandHandler('history', history_cmd))