From 020453fa093b58708b11d081cd4fd1b8adb75f42 Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Tue, 23 Jun 2026 00:58:45 +0000 Subject: [PATCH] optimize guko fork installer and backup tools --- docker-compose.example.yml | 5 +- scripts/guko-install.sh | 108 ++++++++++++++++++++++++++++++++++--- telegram-bot/bot.py | 68 +++++++++++++++++++++++ 3 files changed, 174 insertions(+), 7 deletions(-) diff --git a/docker-compose.example.yml b/docker-compose.example.yml index c37d343..33b0fc2 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,7 +1,10 @@ name: guko services: guko-bot: - image: ghcr.io/shuijiao1/guko:latest + image: guko:local + build: + context: . + dockerfile: telegram-bot/Dockerfile container_name: guko-bot restart: unless-stopped env_file: .env diff --git a/scripts/guko-install.sh b/scripts/guko-install.sh index be8461e..46d7945 100755 --- a/scripts/guko-install.sh +++ b/scripts/guko-install.sh @@ -6,6 +6,9 @@ INSTALL_DIR="${GUKO_INSTALL_DIR:-/opt/guko}" COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml" ENV_FILE="${INSTALL_DIR}/.env" 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() { if [ "$(id -u)" -ne 0 ]; then @@ -23,6 +26,50 @@ install_docker() { systemctl enable --now docker >/dev/null 2>&1 || true } +render_compose() { + cat > "${COMPOSE_FILE}" < 下载 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() { local url="$1" out="$2" 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" chmod 700 "${INSTALL_DIR}/keys" - fetch "${BASE_URL}/docker-compose.example.yml" "${COMPOSE_FILE}" + fetch_source + render_compose if [ ! -f "${ENV_FILE}" ]; then fetch "${BASE_URL}/.env.example" "${ENV_FILE}" sed -i \ -e 's/^BOT_TOKEN=.*/BOT_TOKEN=replace-me/' \ - -e 's/^ALLOWED_USERS=.*/ALLOWED_USERS=123456789/' \ - -e 's/^ADMIN_USERS=.*/ADMIN_USERS=123456789/' \ + -e "s/^ALLOWED_USERS=.*/ALLOWED_USERS=${DEFAULT_TG_ID}/" \ + -e "s/^ADMIN_USERS=.*/ADMIN_USERS=${DEFAULT_TG_ID}/" \ "${ENV_FILE}" fi @@ -60,7 +108,11 @@ GUKO 一键部署脚本 bash <(curl -fsSL https://mjjtop.com/guko) restart 重启 bash <(curl -fsSL https://mjjtop.com/guko) logs 看日志 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} 首次安装后必须编辑:${ENV_FILE} @@ -75,6 +127,33 @@ compose() { 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}" case "$cmd" in install|init) @@ -96,7 +175,7 @@ MSG up|start) need_root [ -f "${COMPOSE_FILE}" ] || { echo "未安装,先执行 install" >&2; exit 1; } - compose pull + compose build --pull compose up -d compose ps ;; @@ -111,10 +190,27 @@ MSG ;; update) need_root - compose pull + install_docker + init_files + compose build --pull compose up -d compose ps ;; + pull) + need_root + init_files + ;; + backup) + need_root + backup_data + ;; + doctor) + need_root + doctor + ;; + repo) + echo "https://mjjtop.com/guko-repo" + ;; logs) need_root compose logs -f --tail=200 diff --git a/telegram-bot/bot.py b/telegram-bot/bot.py index 2eeef95..5224553 100755 --- a/telegram-bot/bot.py +++ b/telegram-bot/bot.py @@ -217,6 +217,58 @@ def next_manual_id(servers): 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 ( + '📦 GUKO 配置概览\n\n' + f'服务器:{total} 台\n' + f'密钥登录:{key} 台\n' + f'密码登录:{password} 台\n' + f'未完整配置:{skipped} 台\n' + f'清单:{safe(str(SERVERS_JSON))}\n' + f'密钥目录:{safe(str(KEYS_DIR))}\n' + f'结果目录:{safe(str(RESULTS_DIR))}' + ) + def redact_inventory(inv: dict): def clean_server(s): 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='已导出脱敏配置(密码已隐藏)。') +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): if not await admin_guard(update): return 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('testall', '批量测试 SSH'), BotCommand('exportconfig', '导出脱敏配置'), + BotCommand('backup', '导出完整备份'), + BotCommand('summary', '配置概览'), BotCommand('info', '查看单台操作面板:/info 名字/IP/ID'), BotCommand('jobs', '查看后台任务'), BotCommand('history', '查看测试历史:/history 服务器'), @@ -3925,6 +3991,8 @@ def main(): app.add_handler(CommandHandler('testssh', testssh_cmd)) app.add_handler(CommandHandler('testall', testall_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('jobs', jobs_cmd)) app.add_handler(CommandHandler('history', history_cmd))