Phần 1 dừng lại ở thời điểm NemoClaw cuối cùng đã chạy ổn định trên GreenNode AgentBase: image đã được build từ repo của NVIDIA, wrapper health check cho port 8080 đã được thêm vào, runtime đã ở trạng thái ACTIVE, và một mô hình 27B đang trả lời phía sau endpoint có TLS. Như vậy, câu hỏi “liệu nó có chạy được ở đây không?” đã có lời giải. Nhưng vẫn còn một câu hỏi khác chưa được trả lời: “người dùng thực sự sẽ nói chuyện với nó bằng cách nào?”.

Phần 2 bắt đầu đúng từ điểm đó và bổ sung lớp giao tiếp còn thiếu: một runtime Telegram bridge riêng, nhận tin nhắn từ Telegram, gọi trực tiếp API MaaS của GreenNode, rồi gửi câu trả lời trở lại khung chat. Đồng thời, phần này cũng ghi lại vì sao bridge bắt buộc phải tách thành một runtime độc lập, proxy outbound và cơ chế webhook đã thay đổi thiết kế ra sao, và những lỗi mới nào chỉ xuất hiện khi bạn đặt một bot thực sự phía trước NemoClaw.

Kết quả thực tế: @NemoClaw_tungvt6bot đã có thể trả lời tin nhắn Telegram, sử dụng qwen/qwen3-5-27b thông qua GreenNode MaaS.

Chúng ta sẽ xây gì

Một runtime Telegram bridge riêng — một ứng dụng Python FastAPI nhẹ có nhiệm vụ:

  • Nhận webhook update từ Telegram tại POST /invocations.
  • Gọi trực tiếp API LLM của GreenNode MaaS.
  • Gửi câu trả lời ngược lại Telegram qua sendMessage.

Runtime này chạy độc lập dưới tên nemoclaw-telegram-bridge trên AgentBase, tách hoàn toàn khỏi runtime backend của NemoClaw.

Kiến trúc

Người dùng
└─► Máy chủ Telegram
    └─► POST /invocations (webhook, inbound — luôn hoạt động)
        └─► Runtime nemoclaw-telegram-bridge
            └─► API LLM GreenNode MaaS (qwen/qwen3-5-27b)
                └─► api.telegram.org/sendMessage (outbound — Python httpx xử lý được)

Vì sao phải tách riêng bridge runtime thay vì nhét Telegram vào thẳng nemoclaw-v2? Runtime Node.js của NemoClaw trên container flavor 2x4 không thể kết nối outbound tới api.telegram.org một cách ổn định, vì GreenNode định tuyến toàn bộ traffic outbound qua một transparent proxy tại 10.200.0.1:3128. Proxy này làm HTTP client undici của Node.js lỗi với UND_ERR_CONNECT_TIMEOUT. Trong khi đó, thư viện httpx của Python xử lý proxy này một cách tự nhiên, nên bridge được viết bằng Python.

Bridge cũng dùng cơ chế webhook thay vì polling. Telegram sẽ chủ động đẩy update vào runtime, nên việc nhận tin nhắn inbound vẫn hoạt động kể cả khi outbound bị giới hạn trong lúc gọi ra ngoài.

Những lỗi đã gặp

Đây là các lỗi thực tế xuất hiện trong quá trình triển khai. Toàn bộ cách xử lý đã được đưa thẳng vào các bước bên dưới.

#Vấn đềTriệu chứngCách xử lý
1Outbound bị chặn trên flavor 2x4Bot không trả lời, không có lỗi rõ ràngDùng Python httpx thay vì Node.js, và dùng webhook thay vì polling.
2Gọi nemoclaw-v2 qua AGENTBASE_INVOCATIONS_URL trả về 404Bot trả lời “Sorry, something went wrong”Bỏ mô hình proxy trung gian và gọi trực tiếp API MaaS LLM từ bridge.
3Trường password của Angular không nhận native setter hoặc keyboard inputNút SAVE vẫn bị mờ; deploy thất bạiChỉ set password bằng document.execCommand('insertText', false, pwd).
4Phải bỏ tick Use agent base registry credentialsFailed to pull image dù robot credentials đúngBỏ chọn ô đó để GreenNode không tự thay bằng credentials nội bộ của họ.
5API key LLM bị cắt ngắn trong UI của runtime401 Unauthorized từ MaaS APILấy full key từ hộp thoại API Keys bằng cách đọc trực tiếp từ DOM.
6extra_body trong httpx thuần bị gửi nguyên như một JSON keyModel từ chối hoặc bỏ qua requestĐặt chat_template_kwargs ở top-level của JSON body.
7Telegram bot token bị bake thẳng vào Docker imageRủi ro lộ bí mậtChỉ truyền token qua biến môi trường của GreenNode: TELEGRAM_BOT_TOKEN.
8Thinking token của Qwen3 làm response phình toPhản hồi chứa khối <think>...</think>Thêm "chat_template_kwargs": {"enable_thinking": false} vào body request gửi LLM.

Lỗi 1: Outbound bị chặn trên flavor 2x4

Container flavor 2x4 của GreenNode định tuyến traffic outbound qua transparent proxy tại 10.200.0.1:3128. HTTP client mặc định của Node.js là undici không xử lý proxy này ổn, nên các kết nối outbound tới api.telegram.org bị timeout với lỗi UND_ERR_CONNECT_TIMEOUT. Trong khi đó, httpx của Python tự nhận proxy, nên bridge được viết bằng Python.

Mô hình webhook cũng giúp ích vì Telegram chủ động gửi update vào runtime. Bridge không cần tự polling Telegram chỉ để nhận tin nhắn mới.

Lỗi 2: Mô hình proxy qua AGENTBASE_INVOCATIONS_URL trả về 404

Cách nghĩ đầu tiên rất tự nhiên là cho bridge đi qua nemoclaw-v2: nhận message từ Telegram, gọi invocations URL của NemoClaw, rồi trả câu trả lời về. Nhưng cách này không hoạt động ở đây. Biến môi trường AGENTBASE_INVOCATIONS_URL mà GreenNode inject vào runtime trỏ tới một định dạng URL đã trả về 404 trong mô hình gọi chéo runtime này.

Cách xử lý là làm cho bridge hoàn toàn tự chủ và gọi trực tiếp MaaS API của GreenNode tại https://maas-llm-aiplatform-hcm.api.vngcloud.vn/v1/chat/completions.

Lỗi 3: Trường password của Angular

Form Edit Runtime của GreenNode dùng Angular reactive forms. Trường password không chấp nhận cả việc mô phỏng gõ bàn phím lẫn native JavaScript value setter kèm input event, vì state nội bộ của Angular không ghi nhận các thay đổi đó.

Cách duy nhất hoạt động ổn định là:

const pwInput = document.querySelectorAll('input[type=password]')[0];
pwInput.focus();
pwInput.select();
document.execCommand('insertText', false, 'YOUR_PASSWORD');

execCommand('insertText') kích hoạt cơ chế change detection của Angular thông qua luồng native text input của trình duyệt, ở tầng thấp hơn so với dispatchEvent tự tạo.

Lỗi 4: Phải bỏ chọn Use Agent Base Registry Credentials

Trong form Edit có hai checkbox:

  • Image authentication — bắt buộc phải bật.
  • Use agent base registry credentials — bắt buộc phải tắt.

Nếu ô thứ hai được bật, GreenNode sẽ tự thay robot credentials bạn nhập bằng credentials nội bộ của họ cho vCR registry. Những credentials mặc định này không có quyền vào private repository, nên pull image sẽ fail.

Lỗi 5: API key bị cắt ngắn trong UI

Form biến môi trường của runtime có thể hiển thị API key bị cắt ngắn về mặt thị giác, dù giá trị thực tế dài hơn. Trong quá trình triển khai, giá trị nhìn thấy từ form edit chỉ có 80 ký tự, trong khi key thật dài 96 ký tự. Phần bị thiếu đó khiến toàn bộ request tới MaaS fail với lỗi 401 Unauthorized.

Hãy lấy full key từ trang API Keys và đọc nó từ DOM trong hộp thoại “View API Key”:

Array.from(document.querySelectorAll('*'))
  .filter(el => el.children.length === 0 && el.textContent.trim().startsWith('vn-'))
  .map(el => el.textContent.trim())[0];

Lỗi 6: extra_body trong raw httpx khác với LangChain

ChatOpenAI của LangChain xử lý extra_body theo cách đặc biệt và merge nó vào request body. Còn httpx thuần thì không. Nếu gửi extra_body nguyên xi, API có thể bỏ qua hoặc từ chối request.

Với request httpx trực tiếp, hãy đặt các field ở top-level:

payload = {
  "model": LLM_MODEL,
  "messages": messages,
  "chat_template_kwargs": {"enable_thinking": False}
}

Lỗi 7: Bảo mật bot token

Telegram bot token tuyệt đối không được bake vào Docker image. Một khi image đã được push lên registry, layer của nó có thể bị inspect và secret có thể bị trích xuất. Chỉ nên truyền token qua biến môi trường của GreenNode là TELEGRAM_BOT_TOKEN.

Lỗi 8: Thinking token của Qwen3

Các model Qwen3 có chế độ “thinking”, tạo ra khối <think>...</think> trước câu trả lời cuối cùng. Điều này làm tăng độ trễ và để lộ reasoning-style output ra phía người dùng. Hãy tắt nó bằng:

"chat_template_kwargs": {"enable_thinking": False}

Yêu cầu kỹ thuật cần chuẩn bị

  • Part 1 đã hoàn tất với nemoclaw-v2 ở trạng thái ACTIVE, dù bridge vẫn có thể chạy độc lập.
  • Một Telegram bot token lấy từ @BotFather.
  • Một API key GreenNode MaaS đầy đủ 96 ký tự.
  • Một tài khoản robot vCR có quyền push vào repository.
  • Docker Desktop đang chạy trên máy local.

Hướng dẫn tích hợp từng bước

Không muốn tự viết bridge từ đầu? Võ Trọng Thư, Head of AI Lab của chúng tôi, đã public sẵn một bridge hoàn chỉnh, có thể deploy ngay, tại github.com/votrongthu/NemoClaw_ThuVT. Chỉ cần clone repo, điền token của bạn vào là xong, không cần tự viết code. 
Repo này đã có sẵn mọi thứ cần dùng:

  • main.py, Dockerfile và file .env.example đã cấu hình sẵn cho GreenNode MaaS
  • Typing indicator, history theo từng chat, lệnh /start và /reset
  • Danh sách allowlist TELEGRAM_ALLOWED_IDS và cơ chế chia đoạn dài

Nếu cách đó phù hợp với bạn, có thể bỏ qua Bước 1 và đi thẳng tới Bước 2.

Bước 1: Tạo ứng dụng bridge

Tạo một thư mục mới cho source của bridge:

telegram-bot-src/
├── main.py
├── requirements.txt
└── Dockerfile

requirements.txt

fastapi>=0.115.0
uvicorn>=0.34.0
httpx>=0.28.0

Dockerfile

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

Khác với backend NemoClaw, image này không cần wrapper riêng vì FastAPI chạy với uvicorn đã tự serve trên port 8080.

main.py

import os, asyncio, logging, httpx
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request, Response
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
TELEGRAM_API = f"https://api.telegram.org/bot{BOT_TOKEN}"
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://maas-llm-aiplatform-hcm.api.vngcloud.vn/v1")
LLM_API_KEY = os.environ.get("LLM_API_KEY", "")
LLM_MODEL = os.environ.get("LLM_MODEL", "qwen/qwen3-5-27b")
SYSTEM_PROMPT = os.environ.get(
    "SYSTEM_PROMPT",
    "You are NemoClaw, an intelligent AI assistant. Be helpful, concise, and friendly.",
)
_conversations: dict[int, list] = {}
MAX_HISTORY = 20
@asynccontextmanager
async def lifespan(app: FastAPI):
    logger.info("NemoClaw Telegram bridge starting...")
    logger.info(f"LLM: {LLM_MODEL} @ {LLM_BASE_URL}")
    yield
    logger.info("Shutting down.")
app = FastAPI(lifespan=lifespan)
@app.get("/health")
async def health():
    return {"status": "ok"}
async def call_llm(chat_id: int, user_message: str) -> str:
    history = _conversations.setdefault(chat_id, [])
    history.append({"role": "user", "content": user_message})
    if len(history) > MAX_HISTORY:
        _conversations[chat_id] = history[-MAX_HISTORY:]
        history = _conversations[chat_id]
    messages = [{"role": "system", "content": SYSTEM_PROMPT}] + history
    payload = {
        "model": LLM_MODEL,
        "messages": messages,
        "max_tokens": 2000,
        "temperature": 0.7,
        "chat_template_kwargs": {"enable_thinking": False},
    }
    async with httpx.AsyncClient(timeout=55.0) as client:
        resp = await client.post(
            f"{LLM_BASE_URL}/chat/completions",
            headers={
                "Authorization": f"Bearer {LLM_API_KEY}",
                "Content-Type": "application/json",
            },
            json=payload,
        )
        logger.info(f"LLM response: HTTP {resp.status_code}")
        if resp.status_code != 200:
            logger.error(f"LLM error body: {resp.text[:500]}")
            resp.raise_for_status()
        data = resp.json()
        assistant_message = data["choices"][0]["message"]["content"]
        history.append({"role": "assistant", "content": assistant_message})
        return assistant_message
async def send_message(chat_id: int, text: str):
    for parse_mode in ["Markdown", None]:
        try:
            chunks = [text[i:i + 4096] for i in range(0, len(text), 4096)]
            async with httpx.AsyncClient(timeout=15.0) as client:
                markdown_failed = False
                for chunk in chunks:
                    payload: dict = {"chat_id": chat_id, "text": chunk}
                    if parse_mode:
                        payload["parse_mode"] = parse_mode
                    resp = await client.post(f"{TELEGRAM_API}/sendMessage", json=payload)
                    if resp.status_code == 400 and parse_mode:
                        markdown_failed = True
                        break
                if not markdown_failed:
                    return
        except Exception as e:
            logger.error(f"send_message error (parse_mode={parse_mode}): {e}")
            return
async def process_message(chat_id: int, user_id: int, text: str):
    try:
        reply = await call_llm(chat_id, text)
    except Exception as e:
        logger.error(f"LLM error for chat {chat_id}: {type(e).__name__}: {e}")
        reply = "Sorry, something went wrong. Please try again."
    if reply:
        await send_message(chat_id, reply)
@app.post("/invocations")
async def webhook(request: Request):
    try:
        update = await request.json()
    except Exception:
        return Response(status_code=200)
    message = update.get("message") or update.get("edited_message")
    if not message:
        return {"ok": True}
    text = message.get("text", "").strip()
    chat_id = message.get("chat", {}).get("id")
    user_id = message.get("from", {}).get("id")
    if not text or not chat_id:
        return {"ok": True}
    if text == "/start":
        await send_message(chat_id, "Hi! I'm NemoClaw, your AI assistant. How can I help you?")
        return {"ok": True}
    if text == "/clear":
        _conversations.pop(chat_id, None)
        await send_message(chat_id, "Conversation history cleared.")
        return {"ok": True}
    asyncio.create_task(process_message(chat_id, user_id, text))
    return {"ok": True}

Các quyết định thiết kế chính

  • POST /invocations trả về HTTP 200 ngay lập tức và xử lý LLM ở background vì Telegram sẽ retry nếu không nhận phản hồi trong khoảng 5 giây.
  • History hội thoại được giữ trong bộ nhớ theo từng chat_id, nên sẽ mất khi container restart.
  • /start và /clear được xử lý mà không cần gọi LLM.

Bước 2: Build và push image của bridge

# Chạy trong thư mục telegram-bot-src
docker build -t vcr.vngcloud.vn/YOUR-ORG/telegram-bot:v1 .
docker push vcr.vngcloud.vn/YOUR-ORG/telegram-bot:v1

Image này build rất nhanh vì chỉ gồm Python và ba dependency nhỏ. Luôn tăng tag image sau mỗi lần push.

Bước 3: Deploy bridge runtime

Vào GreenNode AgentBase → Deploy a new Agent → Custom Agent.

Cấu hình runtime

TrườngGiá trị
Agent runtime namenemoclaw-telegram-bridge
Image URLvcr.vngcloud.vn/YOUR-ORG/telegram-bot:v1
Flavorruntime-s2-general-2x4
Min/Max replicas1 / 1

Image authentication

  • Bật Image authentication.
  • Giữ Use agent base registry credentials ở trạng thái tắt.
  • Username: tên robot account trên vCR.
  • Password: mật khẩu robot account trên vCR.

Nếu cần set password từ browser console vì lỗi của Angular form, dùng đoạn sau:

const pw = document.querySelectorAll('input[type=password]')[0];
pw.focus();
pw.select();
document.execCommand('insertText', false, 'YOUR_PASSWORD');

Biến môi trường

KeyGiá trịGhi chú
TELEGRAM_BOT_TOKENYOUR_BOT_TOKENLấy từ BotFather. Tuyệt đối không bake vào image.
LLM_BASE_URLhttps://maas-llm-aiplatform-hcm.api.vngcloud.vn/v1Endpoint MaaS của GreenNode.
LLM_API_KEYYOUR_FULL_96_CHAR_KEYDùng full value từ trang API Keys.
LLM_MODELqwen/qwen3-5-27bHoặc một model MaaS khác đang khả dụng.

Bấm SAVE → Confirm. Chờ runtime chuyển sang ACTIVE.

Bước 4: Đăng ký Telegram webhook

Khi bridge runtime đã ACTIVE, copy endpoint URL từ trang chi tiết runtime, ví dụ:

https://endpoint-XXXX.agentbase-runtime.aiplatform.vngcloud.vn

Đăng ký URL đó làm Telegram webhook:

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \
  --data-urlencode "url=https://endpoint-XXXX.agentbase-runtime.aiplatform.vngcloud.vn/invocations"

Kết quả kỳ vọng:

{"ok": true, "result": true, "description": "Webhook was set"}

Kiểm tra lại đăng ký webhook:

curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getWebhookInfo"

Trường url phải hiển thị endpoint của runtime cộng thêm /invocations, và pending_update_count nên là 0 nếu update đang được xử lý đúng.

Bước 5: Test bot

Mở Telegram và gửi một tin nhắn cho bot. Hành vi kỳ vọng:

  • Bot hiển thị typing indicator khá nhanh.
  • LLM trả lời trong khoảng 5–30 giây, tuỳ theo tải model.
  • Phản hồi quay lại đúng khung chat.

Các lệnh được hỗ trợ:

  • /start — gửi lời chào mà không gọi LLM.
  • /clear — xoá history hội thoại của chat hiện tại.

Trong log runtime của GreenNode, bạn nên thấy các dòng như:

INFO:main:LLM request: POST .../chat/completions model=qwen/qwen3-5-27b msgs=2
INFO:main:LLM response: HTTP 200
INFO: ... - "POST /invocations HTTP/1.1" 200 OK

Nếu log báo HTTP 401 thì API key sai hoặc bị cắt. Nếu hoàn toàn không có log /invocations, webhook nhiều khả năng chưa đăng ký đúng.

Bước 6: Cập nhật bridge

Khi main.py thay đổi:

docker build -t vcr.vngcloud.vn/YOUR-ORG/telegram-bot:v2 .
docker push vcr.vngcloud.vn/YOUR-ORG/telegram-bot:v2

Sau đó trên GreenNode:

  • Bấm Edit trên runtime nemoclaw-telegram-bridge.
  • Cập nhật Image URL sang :v2.
  • Nhập lại password vCR.
  • Đảm bảo Use agent base registry credentials vẫn đang tắt.
  • Bấm SAVE → Confirm.

URL webhook sẽ không đổi, nên không cần đăng ký lại.

Xử lý sự cố

Bot không trả lời / không có log /invocations

Nguyên nhân: Webhook chưa được đăng ký hoặc đang trỏ sai URL.

Cách xử lý: Chạy getWebhookInfo, kiểm tra URL đã đăng ký, rồi gọi lại setWebhook với endpoint đúng và thêm hậu tố /invocations.

401 Unauthorized trong log LLM

Nguyên nhân: LLM_API_KEY sai hoặc bị cắt ngắn.

Cách xử lý: Lấy full key 96 ký tự bằng cách đọc DOM trong hộp thoại API Keys.

Failed to pull image khi deploy

Nguyên nhân A: Ô Use agent base registry credentials đang bật.

Nguyên nhân B: Password vCR không được Angular ghi nhận.

Cách xử lý: Bỏ chọn checkbox đó và dùng execCommand('insertText') nếu cần.

Bot trả lời ngay bằng “Sorry, something went wrong”

Nguyên nhân: Lệnh gọi LLM đang bị lỗi.

Cách xử lý: Kiểm tra log runtime để xem HTTP status và error body. Các nguyên nhân phổ biến:

  • 401 — API key sai.
  • 404 — LLM_BASE_URL sai.
  • 422 hoặc 400 — request body sai định dạng, thường do đặt chat_template_kwargs bên trong extra_body.

Response chứa khối <think>...</think>

Nguyên nhân: Chế độ thinking của Qwen3 chưa bị tắt.

Cách xử lý: Đảm bảo "chat_template_kwargs": {"enable_thinking": False} nằm ở top-level của request body.

Tóm tắt các runtime đang chạy

RuntimeImageMục đích
nemoclaw-v2nemoclaw:v16Backend NemoClaw cho OpenClaw agent và sandbox execution.
nemoclaw-telegram-bridgetelegram-bot:v5Bridge webhook Telegram nối vào GreenNode MaaS.
telegram-bot(DO NOT TOUCH)Một bridge OpenClaw tách riêng.
openclaw-agent(DO NOT TOUCH)Một agent runtime tách riêng.

Runtime nemoclaw-telegram-bridge hoàn toàn độc lập. Nó không gọi sang nemoclaw-v2 và cũng không cần runtime đó phải chạy.

Telegram bridge được thêm vào ngày 2026-06-12. Runtime nemoclaw-telegram-bridge, Version 10.