Chuyển đến nội dung

Kênh Telegram

Tổng quan

Người dùng thật đầu tiên của hango là một gia đình: vợ, chị em, các con trai và bố mẹ của chủ dự án. Không ai trong số họ sẽ gọi một HTTP API, nên giai đoạn này đưa cho họ một ứng dụng chat mà họ đã có sẵn. Một bot Telegram nhận tin nhắn riêng, ánh xạ từng người dùng Telegram sang một người dùng hango, biến mỗi tin nhắn thành một run bình thường trên agent của người đó, và stream câu trả lời về bằng cách sửa một tin nhắn Telegram khi các token đến. Một tin nhắn từ kênh đi qua đúng các đường tạo run, quota, audit và telemetry giống POST /v1/sessions/{id}/runs. Nó là cửa trước thứ hai của cùng một harness, không phải một tính năng riêng. Web chat sẽ đến sau.

Giai đoạn dạy những gì mọi tích hợp kênh trong goclaw và OpenClaw đều phải giải quyết: ánh xạ danh tính từ một nền tảng không đáng tin, việc nền tảng giao tin at-least-once (ít nhất một lần), sự liên tục của session, và việc nhét một luồng token vào giới hạn tốc độ của nền tảng.

Yêu cầu

Chức năng

  • Long polling bằng getUpdates (timeout 50 s) tới Telegram Bot API. Không cần URL công khai, chứng chỉ TLS hay webhook, nên hango có thể chạy trên một máy ở nhà sau NAT. Chế độ webhook là việc làm tiếp theo.
  • Sức khỏe của việc polling, theo cách OpenClaw làm: một 409 Conflict từ getUpdates nghĩa là một tiến trình khác đang poll cùng bot, nên poller ghi log lỗi nêu rõ nguyên nhân, lùi 30 s và đếm sự kiện, thay vì tranh nhau với poller kia. Một watchdog đánh dấu bot là unhealthy khi không có lời gọi getUpdates nào hoàn tất trong 120 s, và /readyz báo điều đó.
  • Lúc khởi động bot đăng ký các lệnh của nó bằng setMyCommands, để Telegram hiện chúng trong menu.
  • Chỉ xử lý chat riêng. Update của group, channel và tin nhắn đã sửa được ack rồi bỏ qua, kèm một dòng log debug.
  • Ánh xạ danh tính là cấu hình tĩnh, vì tập người dùng chỉ vài người trong gia đình: HANGO_TELEGRAM_USERS là một JSON object từ Telegram user id sang {"user_id": "<hango user uuid>", "agent": "<agent slug>"}. Lúc khởi động mỗi mục được resolve ra tenant của người dùng và kiểm tra: người dùng tồn tại và đang active, còn agent slug tồn tại và hiển thị với người dùng đó theo quy tắc visibility của Phase 2. Một mục sai làm khởi động thất bại với thông báo nêu Telegram id.
  • Người gửi lạ nhận một câu trả lời cố định (“This bot is private. Ask the owner to add your Telegram id: N.”) tối đa một lần mỗi giờ cho mỗi người gửi, và không tạo run nào. Id người gửi được log và đếm, không bao giờ log nội dung tin nhắn của họ.
  • Sự liên tục của session: session hiện tại của một người dùng Telegram là session mới nhất có key bắt đầu bằng telegram:<telegram user id>:. Nếu chưa có, tạo một session với key telegram:<telegram user id>:<unix seconds> trên agent đã ánh xạ. /new bắt đầu một session mới theo cùng cách.
  • Một tin nhắn trả lời (reply) tin nhắn trước sẽ đưa text của tin nhắn đó vào input của run như ngữ cảnh trích dẫn (cắt còn 1000 ký tự), để câu “ý bạn là gì ở đoạn này?” hoạt động được.
  • Các lệnh: /new (session mới), /cancel (hủy run đang chạy trong session hiện tại qua đường cancel của Phase 6), /whoami (hiện tên người dùng hango đã ánh xạ và agent slug), /help. Mọi text khác bắt đầu bằng / nhận nội dung help.
  • Một tin nhắn text tạo run qua hàm tạo run dùng chung tách ra từ handler của Phase 5, với scope của người dùng đã ánh xạ. Vượt quota (Phase 13) thì trả lời bằng thông báo quota. Một session đã có run đang pending hoặc running thì trả lời “Still working on the last message. Send /cancel to stop it.” và không tạo gì.
  • Giao trùng là vô hại: input của run mang telegram_update_id, và việc tạo run cho một tin nhắn Telegram trước hết kiểm tra session xem đã có run nào với update id đó chưa. Nhờ vậy một lần crash giữa lúc tạo run và offset getUpdates kế tiếp không bao giờ chạy một tin nhắn hai lần.
  • Trả lời dạng streaming: gửi chat action typing và lặp lại mỗi 4 s cho đến khi có text đầu tiên, gửi một tin nhắn giữ chỗ (placeholder), subscribe vào các event của run qua broadcaster của Phase 6, và sửa placeholder bằng text đã tích lũy tối đa một lần mỗi 1.5 s cho mỗi chat. Lần sửa cuối xảy ra ở event kết thúc. Text quá 4096 ký tự được tiếp tục ở một tin nhắn mới, cắt tại ranh giới đoạn hoặc dòng.
  • Định dạng: đầu ra của model là Markdown, nên lần sửa cuối chuyển nó sang Telegram HTML (in đậm, in nghiêng, inline code, code block, link, và danh sách thành các dòng thường) với mọi <, > và & còn lại được escape. Các lần sửa trung gian giữ ở plain text, vì một cấu trúc Markdown dở dang không thể chuyển đổi an toàn. Nếu Telegram từ chối HTML (400 can't parse entities), cùng đoạn text được gửi lại dưới dạng plain text và việc fallback được đếm.
  • Một lần sửa thất bại (tin nhắn đã bị xóa, hoặc quá cũ để sửa) fallback sang gửi tin nhắn mới, như goclaw làm, nên một câu trả lời không bao giờ bị mất.
  • Một run thất bại sửa placeholder thành một dòng báo lỗi ngắn kèm run id. Một run bị hủy ghi “Stopped.” Các tool call hiện thành một dòng trạng thái (“Using web_fetch…”) mà lần sửa text kế tiếp sẽ thay thế.
  • Ảnh, voice note, sticker và tài liệu nhận câu trả lời “Text only for now.” và không tạo run. Phase 20 sẽ thêm chúng.

Phi chức năng

  • Một package mới, internal/telegram. Chỉ dùng thư viện chuẩn (net/http, encoding/json); không dùng Telegram SDK, vì năm method của Bot API được dùng (getUpdates, sendMessage, editMessageText, sendChatAction, setMyCommands) đều nhỏ và việc nhìn thấy JSON của chúng là một phần bài học.
  • Bot token là bí mật: không bao giờ log, không bao giờ nằm trong span attribute hay error. URL của Bot API chứa token, nên phần bọc lỗi của HTTP client cắt URL đi trước khi trả về.
  • Poller chạy dưới safego và dừng theo thứ tự drain của Phase 6 trước khi scheduler drain, nên không có run mới nào được tạo trong lúc shutdown.
  • Giới hạn tốc độ của Telegram (khoảng một tin nhắn mỗi giây mỗi chat, 30 mỗi giây mỗi bot) được tôn trọng bằng bộ throttle của việc sửa cộng một token bucket cho mỗi bot. Một 429 từ Telegram được xử lý bằng retry_after cộng một giây trên mọi method, và vòng lặp sửa bỏ qua các lần sửa cho đến lúc đó thay vì xếp hàng chúng.

Kiến trúc

// internal/telegram/bot.go
type Bot struct {
	api      *apiClient          // Bot API over net/http, token kept private
	users    map[int64]Mapping   // resolved at startup from HANGO_TELEGRAM_USERS
	runs     RunCreator          // the shared run-creation function, injected by internal/app
	events   EventSubscriber     // phase-6 broadcaster subscription, injected
	sessions SessionFinder       // newest telegram:<id>: session, or create one
}

type Mapping struct {
	TelegramUserID int64
	Scope          store.Scope // tenant, user and current role, rebuilt per message
	AgentID        uuid.UUID
}

func (b *Bot) Run(ctx context.Context) error // long-poll loop; returns on ctx cancel
getUpdates (offset) ──▶ for each update:
   private text? ──no──▶ ignore or fixed reply
   known sender? ──no──▶ "This bot is private…" (once per hour)
   command? ──▶ /new /cancel /whoami /help
   run already active in session? ──▶ "Still working…"
   run with this update_id exists? ──▶ skip (duplicate delivery)
   CreateRun(scope, session, input{text, telegram_update_id}) ──▶ quota, audit run.create, enqueue
   placeholder message ──▶ subscribe run events ──▶ throttled edits ──▶ final edit
next getUpdates with offset = last update_id + 1

internal/telegram import internal/store cho Scope và các lookup session, run, và không import gì từ internal/server. Việc tạo run chuyển từ internal/server/handlers_runs.go sang internal/run/create.go, để handler HTTP và bot cùng gọi một hàm và không thể lệch nhau về hành vi quota, dedup hay audit.

Instrumentation

  • Mỗi update có một span telegram.update (attribute telegram.update_id, telegram.chat_type, hango.telegram.outcome = run_created|duplicate|busy|unknown_sender|ignored|command). Trace invoke_agent của một run được tạo ra được liên kết với nó bằng span link, nên Tempo hiện được bước nhảy từ tin nhắn Telegram sang run.
  • Audit row của run là run.create với actor_kind=user, người dùng đã ánh xạ làm actor và detail.channel = "telegram". Không có audit action mới.
  • Metric: counter hango.telegram.updates (attribute outcome), counter hango.telegram.api_calls (attribute method, status), histogram hango.telegram.api_duration, histogram hango.telegram.edit_lag (thời gian từ event của run đến lần sửa hiển thị nó), counter hango.telegram.unknown_senders, counter hango.telegram.format_fallbacks (HTML bị từ chối, gửi lại dạng plain text), counter hango.telegram.poll_conflicts (409 từ getUpdates).
  • Log mang telegram.update_id, run_id và trace_id, không bao giờ mang nội dung tin nhắn hay token.

Các file liên quan

Tạo mới

  • internal/telegram/api.go, internal/telegram/api_test.go — client Bot API chạy với httptest.Server, token bị cắt khỏi error.
  • internal/telegram/bot.go, internal/telegram/bot_test.go — vòng lặp poll, định tuyến, các lệnh và dedup.
  • internal/telegram/stream.go, internal/telegram/stream_test.go — từ event của run đến các lần sửa có throttle, cắt ở 4096 ký tự, dòng trạng thái tool, fallback khi sửa thất bại.
  • internal/telegram/format.go, internal/telegram/format_test.go — Markdown sang Telegram HTML có escape, được test trên code block, nhấn mạnh lồng nhau, link có & trong URL và đầu vào không cân bằng.
  • internal/telegram/mapping.go, internal/telegram/mapping_test.go — phân tích và resolve HANGO_TELEGRAM_USERS.
  • internal/run/create.go, internal/run/create_test.go — việc tạo run tách ra từ handler của Phase 5, cộng thêm lookup dedup theo update id.
  • docs/learning/18-telegram-channel.md.

Sửa đổi

  • plan.md — trần package từ 18 lên 19 cộng internal/tool/builtin, thêm internal/telegram.
  • internal/server/handlers_runs.go — gọi run.Create.
  • internal/store/run.go, internal/store/pg/run.go — FindByInputKey(ctx, sessionID, key, value) cho phép kiểm tra dedup.
  • internal/config/config.go — HANGO_TELEGRAM_BOT_TOKEN, HANGO_TELEGRAM_USERS, HANGO_TELEGRAM_EDIT_INTERVAL (mặc định 1.5s), HANGO_TELEGRAM_API_URL (mặc định https://api.telegram.org, được test và make obs-verify của Phase 14 ghi đè). Bot chỉ khởi động khi token được đặt.
  • internal/app/app.go — dựng và khởi động bot, thêm nó vào thứ tự drain trước scheduler.
  • internal/telemetry/metrics_names.go — bảy tên metric.
  • internal/server/health.go — /readyz gồm cả watchdog của poller khi bot được bật.
  • .env.example, README.md, docs/learning/README.md, docs/system-architecture.md.

Các bước triển khai

  1. Tách run.Create ra khỏi handlers_runs.go với các test handler hiện có vẫn xanh, rồi thêm create_test.go cho dedup theo update id.
  2. Viết api.go và test của nó: getUpdates, sendMessage, editMessageText, sendChatAction, setMyCommands, đường 429 retry_after, việc lùi khi gặp 409, và một test chứng minh chuỗi lỗi của một lời gọi thất bại không chứa token.
  3. Viết mapping.go: phân tích JSON, resolve từng người dùng và agent lúc khởi động, thất bại kèm Telegram id khi một mục sai.
  4. Viết bot.go: vòng lặp poll với xử lý offset và watchdog, đăng ký lệnh, ngữ cảnh reply, bộ lọc chat riêng, throttle câu trả lời cho người gửi lạ, các lệnh, kiểm tra busy, kiểm tra dedup và tạo run. Test với một API server giả phát lại một chuỗi update theo kịch bản, gồm cả cùng một update hai lần.
  5. Viết format.go với test dạng bảng, rồi stream.go: lặp action typing, subscribe, gộp các text delta, sửa tối đa một lần mỗi khoảng, cắt text dài, hiện và thay các dòng trạng thái tool, kết thúc bằng lần sửa HTML cùng fallback plain text, và gửi tin nhắn mới khi một lần sửa thất bại. Test với broadcaster giả và đồng hồ giả.
  6. Nối config, khởi động app và thứ tự drain. Thêm span, span link tới trace của run, metric và các field log.
  7. Tạo một bot thật với BotFather, ánh xạ Telegram id của chính bạn, và chạy một cuộc hội thoại đầu-cuối với docker compose up. Ghi trace Tempo từ telegram.update đến run và generation Langfuse vào learning doc.
  8. Ánh xạ một thành viên gia đình, chạy cuộc hội thoại đầu tiên của họ cùng họ, và ghi lại điều gì làm họ bối rối. Phản hồi đó đi vào learning doc và vào phần việc tiếp theo là web chat.
  9. Cập nhật docs và viết learning doc.

Dàn ý tài liệu học

docs/learning/18-telegram-channel.md phải có:

  • Khái niệm: một kênh là một cửa trước. Nó biến một tin nhắn của nền tảng thành một run trong đúng tenant, user và session, và biến các event của run trở lại thành tin nhắn của nền tảng. Bản thân harness không đổi.
  • Các lựa chọn thiết kế: long polling thay vì webhook cho deploy ở nhà; ánh xạ danh tính tĩnh cho một tập người dùng nhỏ đáng tin; một session cho mỗi người dùng Telegram với /new; dedup theo update_id vì Telegram giao at-least-once; throttle việc sửa vì nền tảng giới hạn tốc độ.
  • Đánh đổi: ánh xạ tĩnh so với mã pairing (đơn giản và kiểm toán được trong config so với tự phục vụ); sửa tin nhắn so với một tin nhắn cho mỗi chunk (chat sạch so với ít lời gọi API hơn); các lần sửa trung gian dạng plain text với một lần sửa HTML cuối so với định dạng mọi lần sửa (không bao giờ vỡ giữa chừng so với định dạng đẹp hơn khi đang stream).
  • So sánh với goclaw: internal/channels/telegram cùng channel manager, pairing service và cấu hình tenant theo từng instance.
  • So sánh với OpenClaw: DM pairing cho người gửi lạ và mô hình tin cậy của nó, trong đó người gửi từ một kênh là đầu vào không đáng tin.
  • Cách debug tầng này: bắt đầu từ span telegram.update (attribute outcome), đi theo span link vào trace của run, xem hango.telegram.api_calls theo status để tìm lỗi Telegram, và hango.telegram.edit_lag khi câu trả lời có cảm giác chậm.
  • Phase 20 thêm gì: ảnh, tài liệu và voice note đi vào, file đi ra, mã pairing và nút approval. Group chat, chế độ webhook và web chat vẫn là việc làm tiếp theo.
  • Bài tập: gửi cùng một tin nhắn hai lần thật nhanh và xem câu trả lời busy; kill hango giữa chừng một run và xem run hoàn tất sau khi reclaim trong khi tin nhắn Telegram được hoàn thành; hạ khoảng sửa xuống cho đến khi Telegram trả 429.
  • Ngân sách thời gian: 15h triển khai + 2h viết learning doc.

Tiêu chí thành công

  • go build ./... && go vet ./... && go test ./... đều pass, và danh sách package chính xác là 19 package cộng internal/tool/builtin nêu trong plan.md.
  • Endpoint run qua HTTP và bot đều tạo run qua run.Create; các test handler hiện có của Phase 5 pass mà không phải sửa.
  • Phát lại cùng một update Telegram hai lần chỉ tạo một run (test).
  • Người gửi lạ nhận câu trả lời cố định tối đa một lần mỗi giờ và không tạo run (test).
  • Một tin nhắn trong lúc có run đang chạy nhận câu trả lời busy và không tạo run (test).
  • Một run vượt quota trả lời bằng thông báo quota và không tạo run (test).
  • Việc sửa xảy ra tối đa một lần mỗi khoảng, và một câu trả lời quá 4096 ký tự được cắt thành nhiều tin nhắn (test với đồng hồ giả).
  • Tin nhắn cuối là Telegram HTML chuyển từ Markdown; HTML bị Telegram từ chối được gửi lại dạng plain text; một lần sửa thất bại trở thành một tin nhắn mới (test).
  • Một 429 trì hoãn lời gọi kế tiếp theo retry_after, một 409 lùi lại mà không thoát, và /readyz fail khi không có lần poll nào hoàn tất trong 120 s (test).
  • Một tin nhắn trả lời tin nhắn trước đặt text của tin nhắn đó vào input của run (test).
  • Không dòng log, span attribute hay error trả về nào chứa bot token (test).
  • Một run tạo từ Telegram có audit row run.create với detail.channel = "telegram" và người dùng đã ánh xạ làm actor.
  • Một cuộc hội thoại thật qua bot của BotFather chạy được đầu-cuối, và trace Tempo liên kết telegram.update với run (kiểm tra thủ công, ghi trong learning doc).
  • Shutdown dừng poller trước khi scheduler drain (test khẳng định không run nào được tạo sau khi drain bắt đầu).

Đánh giá rủi ro

Rủi roKhả năng x Tác độngDấu hiệuCách xử lý đã quyết định trước
Tài khoản Telegram của một thành viên gia đình bị chiếmLow x HighLượng run bất thường từ một người dùng đã ánh xạQuota của Phase 13 chặn chi phí; xóa mục đó khỏi HANGO_TELEGRAM_USERS rồi restart; audit trail cho thấy mọi run cùng kênh của nó
Telegram giao một update hai lần sau một lần crashHigh x MediumHai run có cùng telegram_update_idLookup dedup trước khi tạo run, được test bao phủ
Bộ chuyển Markdown tạo ra HTML mà Telegram từ chốiMedium x Lowhango.telegram.format_fallbacks tăngFallback plain text đã gửi được câu trả lời; thêm đầu vào lỗi làm một test case
Việc sửa chạm giới hạn tốc độ của TelegramMedium x Lowhango.telegram.api_calls{status="429"} tăngTôn trọng retry_after; tăng HANGO_TELEGRAM_EDIT_INTERVAL
Cả gia đình dùng chung quota của một tenantHigh x MediumMức dùng của một người chặn tất cảTạm chấp nhận; quota theo từng user là việc làm tiếp theo nếu xảy ra
Bot token rò qua một error chứa URL của requestMedium x HighToken hiện trong logClient cắt URL khỏi error, được test bao phủ

Rollback

Bỏ đặt HANGO_TELEGRAM_BOT_TOKEN thì bot không khởi động. Xóa internal/telegram và phần nối dây của nó đưa binary về hành vi của Phase 13. run.Create ở lại, vì handler HTTP dùng nó.