目錄

重點速讀 2 分鐘|18 分鐘深讀
  1. 這篇先做什麼,不做什麼
  2. 為什麼第一篇先用 Telegram
  3. 最小架構:Telegram 到 Hermes 到隔離主機
  4. 準備環境與 Bot token
  5. 建立 Hermes 專案資料夾
  6. 貼上最小版 Hermes 程式
  7. 第一次啟動與測試
  8. 安全底線:不要把聊天軟體接成 root shell
  9. 下一篇要接 Claude 和 Codex 時,這層要怎麼升級
  10. 大家都在問

這篇先做一個新手能跑起來的最小版本:Telegram 收訊、Hermes 驗證身分、只允許白名單命令、把結果回傳手機。先不要讓 AI 直接碰任意 shell;等入口、紀錄和安全邊界都穩了,下一篇再接 Claude 規劃與 Codex 執行。

  • 第一篇目標不是「完整自動化」,而是先做出安全入口。
  • 使用 Telegram Bot API 的 getUpdates long polling,不需要公開 webhook、HTTPS 或反向代理。
  • Hermes 在這裡是控制層:驗證使用者、解析指令、建立紀錄、只放行安全命令。
  • 本篇範例只支援 /ping/whoami/status/run date/run uptime/run pwd
  • 先跑通這個版本,再把 Claude、ChatGPT Codex、多入口與完整隔離主機拆到後續文章。
  1. 這篇先做什麼,不做什麼
  2. 為什麼第一篇先用 Telegram
  3. 最小架構:Telegram 到 Hermes 到隔離主機
  4. 準備環境與 Bot token
  5. 建立 Hermes 專案資料夾
  6. 貼上最小版 Hermes 程式
  7. 第一次啟動與測試
  8. 安全底線:不要把聊天軟體接成 root shell
  9. 下一篇要接 Claude 和 Codex 時,這層要怎麼升級

這篇先做什麼,不做什麼

這個系列最後會走向「完整龍蝦機器人」:外部入口可以發任務,Hermes 負責調度,Claude 負責規劃與審查,ChatGPT Codex 負責在隔離主機裡讀檔、改檔、跑測試、回報結果。

但第一篇不要一開始就把所有東西接滿。新手最容易出事的地方,不是程式碼寫不出來,而是太早把聊天訊息接到高權限 shell,最後任何一句話都可能變成主機命令。

所以這篇只完成一件事:讓 Telegram 成為一個「受控入口」。你會得到一個 Hermes 小程式,它可以收 Telegram 訊息、檢查是不是允許的使用者、把每次執行寫入 jobs/ 紀錄,並只允許幾個安全命令。

這篇會做 這篇先不做
Telegram Bot 收訊 公開 webhook 與 HTTPS 部署
allowlist 身分驗證 多人 RBAC 權限系統
白名單命令 任意 shell / root 權限
任務紀錄與回報 真正呼叫 Claude / Codex
單機最小可跑版 Docker、VM、Kubernetes、systemd 深入部署

如果你只想知道能不能跑,答案是:可以。照著做完,手機傳 /run date,主機會回傳目前時間;傳 /status,主機會回傳任務紀錄數量。

為什麼第一篇先用 Telegram

Telegram 適合當第一篇入口,原因很務實:建立 Bot 快、手機好測、Bot API 是普通 HTTP 介面,而且官方同時支援 getUpdates 和 webhook。getUpdates 是 long polling,程式主動去 Telegram 伺服器拿更新;webhook 則需要 Telegram 對你的 HTTPS URL 發 POST。

新手第一版用 long polling 比較舒服,因為你不需要公開主機,也不需要先處理憑證、反向代理、防火牆和網域。缺點是正式多人服務不適合只靠這種單行程輪詢;但拿來建立最小可用架構,剛好。

如果你未來會把這台隔離主機接到交易所通知、行情或後台提醒,第一階段請只做「讀取與提醒」,不要一開始開自動下單。測試帳號可以從 OKXBybit 開始理解 API、KYC、商品型態與費率;但所在地限制、槓桿、下架規則和帳號安全,都要以官方最新資訊為準。本文不教自動交易,先把入口隔離做好。

最小架構:Telegram 到 Hermes 到隔離主機

這篇的流程只有五步:

  1. 你在 Telegram 傳訊息給 Bot。
  2. Hermes 用 getUpdates 收到訊息。
  3. Hermes 檢查 Telegram user id 是否在 allowlist。
  4. Hermes 只接受白名單裡的命令,例如 dateuptimepwd
  5. Hermes 把 stdout、stderr、exit code 和時間寫成 job 紀錄,再把摘要傳回 Telegram。
Telegram 指令進入 Hermes 閘門的流程圖,包含 allowlist、白名單命令、job 紀錄與回覆
Telegram 指令進入 Hermes 閘門的流程圖,包含 allowlist、白名單命令、job 紀錄與回覆

這裡的 Hermes 不是神祕框架,而是一個你自己控制的中介層。它的價值在於:外部入口不直接碰主機權限,AI 也不直接拿到所有密鑰。之後要接 Claude 或 Codex,也是接到 Hermes 後面,不是讓 Telegram 直接呼叫高權限工具。

準備環境與 Bot token

你需要:

  • 一台可以長時間開著的 macOS 或 Linux 主機。
  • Python 3。
  • Telegram 帳號。
  • 一個新的 Telegram Bot token。

先在 Telegram 找 @BotFather

  1. /newbot
  2. 依照提示取 Bot 名稱與 username。
  3. BotFather 會回傳一串 token。
  4. 不要把 token 貼到群組、文章、GitHub 或截圖裡。

接著在主機建立工作資料夾。

mkdir -p hermes-bot/jobs hermes-bot/logs
cd hermes-bot
python3 -m venv .venv
source .venv/bin/activate

建立 .env

touch .env
chmod 600 .env

先放 token,ALLOWED_TELEGRAM_USERS 可以先留空,等一下用 /whoami 查自己的 user id。

TELEGRAM_BOT_TOKEN=把_BotFather_給你的_token_貼在這裡
ALLOWED_TELEGRAM_USERS=

建立 Hermes 專案資料夾

最小版目錄長這樣:

hermes-bot/
  hermes.py
  .env
  jobs/
  logs/

jobs/ 會放每次任務的 JSON 紀錄。logs/ 先保留給之後常駐服務使用。這兩個資料夾看起來很普通,但它們是後續升級成真正任務佇列、審批流程、Codex 執行紀錄的起點。

這篇的白名單命令如下:

Telegram 指令 主機實際執行 用途 風險
/ping 不執行 shell 確認 Bot 活著
/whoami 不執行 shell 回傳你的 Telegram user id
/status 不執行 shell 查看 jobs 數量
/run date date 測試主機時間
/run uptime uptime 測試主機負載
/run pwd pwd 確認執行位置

注意,這裡沒有 /run 任意內容。這是故意的。

貼上最小版 Hermes 程式

hermes-bot/ 裡建立 hermes.py,貼上下面這份程式。

#!/usr/bin/env python3
import datetime as dt
import json
import os
import pathlib
import subprocess
import time
import urllib.parse
import urllib.request

ROOT = pathlib.Path(__file__).resolve().parent
JOBS_DIR = ROOT / "jobs"
ENV_FILE = ROOT / ".env"

SAFE_COMMANDS = {
    "date": ["date"],
    "uptime": ["uptime"],
    "pwd": ["pwd"],
}


def load_env():
    if not ENV_FILE.exists():
        return
    for raw_line in ENV_FILE.read_text(encoding="utf-8").splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key, value = line.split("=", 1)
        value = value.strip().strip('"').strip("'")
        os.environ.setdefault(key.strip(), value)


def require_config():
    token = os.environ.get("TELEGRAM_BOT_TOKEN", "").strip()
    if not token:
        raise SystemExit("Missing TELEGRAM_BOT_TOKEN in .env")

    raw_users = os.environ.get("ALLOWED_TELEGRAM_USERS", "")
    allowed_users = {
        int(item.strip())
        for item in raw_users.split(",")
        if item.strip().isdigit()
    }
    return token, allowed_users


def telegram_api(token, method, payload):
    url = f"https://api.telegram.org/bot{token}/{method}"
    data = urllib.parse.urlencode(payload).encode("utf-8")
    req = urllib.request.Request(url, data=data)
    with urllib.request.urlopen(req, timeout=40) as response:
        body = response.read().decode("utf-8")
    result = json.loads(body)
    if not result.get("ok"):
        raise RuntimeError(result)
    return result["result"]


def send_message(token, chat_id, text):
    if len(text) > 3500:
        text = text[:3500] + "\n\n[輸出太長,已截斷]"
    telegram_api(token, "sendMessage", {
        "chat_id": chat_id,
        "text": text,
        "disable_web_page_preview": "true",
    })


def write_job(command_name, command, completed):
    JOBS_DIR.mkdir(exist_ok=True)
    job_id = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    job_path = JOBS_DIR / f"{job_id}-{command_name}.json"
    job_path.write_text(json.dumps({
        "jobId": job_id,
        "commandName": command_name,
        "command": command,
        "completedAt": dt.datetime.now(dt.timezone.utc).isoformat(),
        **completed,
    }, ensure_ascii=False, indent=2), encoding="utf-8")
    return job_id


def run_safe_command(command_name):
    command = SAFE_COMMANDS[command_name]
    started = time.time()
    try:
        proc = subprocess.run(
            command,
            cwd=ROOT,
            text=True,
            capture_output=True,
            timeout=10,
            check=False,
        )
        completed = {
            "exitCode": proc.returncode,
            "stdout": proc.stdout.strip(),
            "stderr": proc.stderr.strip(),
            "durationSeconds": round(time.time() - started, 3),
        }
    except subprocess.TimeoutExpired as exc:
        completed = {
            "exitCode": None,
            "stdout": exc.stdout or "",
            "stderr": "Command timed out after 10 seconds",
            "durationSeconds": round(time.time() - started, 3),
        }
    job_id = write_job(command_name, command, completed)
    return job_id, completed


def help_text():
    return (
        "Hermes ready.\n\n"
        "可用指令:\n"
        "/whoami - 顯示你的 Telegram user id\n"
        "/ping - 測試連線\n"
        "/status - 查看任務紀錄\n"
        "/run date - 顯示主機時間\n"
        "/run uptime - 顯示主機負載\n"
        "/run pwd - 顯示執行目錄"
    )


def handle_message(token, allowed_users, message):
    chat_id = message["chat"]["id"]
    user = message.get("from", {})
    user_id = user.get("id")
    text = (message.get("text") or "").strip()

    if text == "/whoami":
        send_message(token, chat_id, f"你的 Telegram user id 是:{user_id}")
        return

    if not allowed_users:
        send_message(token, chat_id, "尚未設定 ALLOWED_TELEGRAM_USERS;目前只回覆 /whoami。")
        return

    if user_id not in allowed_users:
        send_message(token, chat_id, "拒絕:這個 Telegram user id 不在 allowlist。")
        return

    if text in ("/start", "/help"):
        send_message(token, chat_id, help_text())
        return

    if text == "/ping":
        send_message(token, chat_id, "pong")
        return

    if text == "/status":
        count = len(list(JOBS_DIR.glob("*.json"))) if JOBS_DIR.exists() else 0
        send_message(token, chat_id, f"Hermes online. jobs={count}")
        return

    if text.startswith("/run "):
        command_name = text.removeprefix("/run ").strip()
        if command_name not in SAFE_COMMANDS:
            send_message(token, chat_id, f"拒絕:{command_name} 不在 SAFE_COMMANDS 白名單。")
            return
        job_id, completed = run_safe_command(command_name)
        send_message(token, chat_id, (
            f"job={job_id}\n"
            f"command={command_name}\n"
            f"exit={completed['exitCode']}\n\n"
            f"stdout:\n{completed['stdout'] or '(empty)'}\n\n"
            f"stderr:\n{completed['stderr'] or '(empty)'}"
        ))
        return

    send_message(token, chat_id, help_text())


def main():
    load_env()
    token, allowed_users = require_config()
    offset = None
    print("Hermes Telegram loop started. Press Ctrl+C to stop.")

    while True:
        updates = telegram_api(token, "getUpdates", {
            "timeout": 25,
            "offset": offset or "",
            "allowed_updates": json.dumps(["message"]),
        })
        for update in updates:
            offset = update["update_id"] + 1
            message = update.get("message")
            if message:
                handle_message(token, allowed_users, message)


if __name__ == "__main__":
    main()

第一次啟動與測試

先啟動 Hermes:

python3 hermes.py

Telegram 打開你剛建立的 Bot,傳:

/whoami

Hermes 會回傳你的 Telegram user id。把它填回 .env

TELEGRAM_BOT_TOKEN=你的_token
ALLOWED_TELEGRAM_USERS=123456789

停止程式後重跑:

python3 hermes.py

接著在 Telegram 依序測:

/ping
/status
/run date
/run uptime
/run pwd

如果成功,你會看到主機回傳時間、負載與目前目錄;jobs/ 裡也會出現 JSON 紀錄。這代表最小入口已經完成:Telegram 可以下指令,Hermes 可以驗證、執行白名單命令、寫紀錄、回報結果。

安全底線:不要把聊天軟體接成 root shell

這篇最重要的不是程式碼,而是邊界。Telegram、LINE、Slack、Discord 都只是入口;入口不應該直接等於主機權限。

風險 錯誤做法 第一篇採用的做法
假使用者或陌生人傳訊息 所有人都能執行 用 Telegram user id allowlist
任意命令注入 /run rm -rf ... 也丟給 shell 只允許 SAFE_COMMANDS
token 外洩 token 寫進文章、截圖、repo token 只放 .env,檔案權限 600
無法追蹤 執行完就消失 每次 /run 寫入 jobs/*.json
權限過大 用 root 跑 Bot 用普通使用者跑,後續再做更細隔離

這也是為什麼本篇沒有做「輸入什麼就執行什麼」。如果你要讓 AI 主機長期運作,白名單、審批、紀錄和可撤銷權限,比一開始看起來帥氣的全自動更重要。

下一篇要接 Claude 和 Codex 時,這層要怎麼升級

第一篇完成後,你已經有入口和任務紀錄。下一篇才適合把角色拆清楚:

  • Claude:負責理解需求、拆任務、寫計畫、審查結果。
  • ChatGPT Codex:負責在隔離工作區讀檔、改檔、跑測試、產出回報。
  • Hermes:負責驗證、排隊、審批、紀錄、回傳,並決定哪些任務可以交給 Claude 或 Codex。

升級時,不要讓 Telegram 直接呼叫 Codex,也不要讓 Claude 直接拿到所有 shell 權限。比較穩的升級方式是新增幾個高階任務:

未來指令 Hermes 應該做的事 是否需要人工確認
/plan 寫一篇文章 交給 Claude 產生計畫 低風險可不需要
/codex status 查看 Codex 工作區狀態 通常不需要
/codex run <任務> 建立 Codex 任務,而不是直接跑 shell 需要
/approve <job> 放行已審核任務 必須
/cancel <job> 中止排隊或執行中的任務 視任務而定

到這裡就可以收工。第一篇的成果不是一個華麗機器人,而是一個乾淨、可解釋、可延伸的入口。這種骨架做好,後面要接 Claude、Codex、LINE、Slack、Web Console 或 API,都會穩很多。

延伸閱讀

大家都在問

脆上快聊,群裡慢慢拆。

脆友問第一篇為什麼不用 webhook啊

脆上怎麼說看脆 因為 webhook 需要公開 HTTPS URL、憑證、路由和重送處理。新手第一版用 getUpdates long polling 比較少環境問題,也不需要把隔離主機直接暴露到網路上。

剛入坑群友問這個 Hermes 可以直接拿來跑 Codex 嗎?

老群友答 不建議直接改成任意 Codex shell。比較安全的方式是讓 Hermes 建立任務檔,人工或下一層 runner 再決定是否交給 Codex。危險任務要有 /approve

剛入坑群友問Telegram user id 算安全登入嗎?

老群友答 它只是一層基本 allowlist,不等於完整登入系統。正式環境還要加上密鑰輪替、審批、速率限制、平台簽章或 webhook secret、操作日誌與權限分級。

剛入坑群友問可以把 Bot 加到群組嗎?

老群友答 第一版不建議。先用私人對話測通,等你有群組權限規則、指令前綴、審批流程和日誌後,再考慮放進群組。

脆友問所以為什麼不用現成 Bot framework啊

脆上怎麼說看脆 可以用,但第一篇刻意用 Python 標準函式庫,讓讀者看懂 Bot API、輪詢、allowlist、白名單命令和任務紀錄。理解骨架後,再換 framework 會更穩。

資料查證:2026-06-13。本文參考 Telegram Bot API 官方文件、Python venv 官方文件、OpenAI Codex 官方產品與開發者文件、Anthropic Claude 產品資料;實際 API、產品功能、平台限制與安全建議仍以官方最新資訊為準。

延伸閱讀

隨機文章