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 ConflicttừgetUpdatesnghĩ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ọigetUpdatesnào hoàn tất trong 120 s, và/readyzbá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_USERSlà 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ó
keybắt đầu bằngtelegram:<telegram user id>:. Nếu chưa có, tạo một session với keytelegram:<telegram user id>:<unix seconds>trên agent đã ánh xạ./newbắ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:
inputcủa run mangtelegram_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à offsetgetUpdateskế 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
typingvà 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
safegovà 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
429từ Telegram được xử lý bằngretry_aftercộ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 cancelgetUpdates (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 + 1internal/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(attributetelegram.update_id,telegram.chat_type,hango.telegram.outcome=run_created|duplicate|busy|unknown_sender|ignored|command). Traceinvoke_agentcủ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.createvớiactor_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(attributeoutcome), counterhango.telegram.api_calls(attributemethod,status), histogramhango.telegram.api_duration, histogramhango.telegram.edit_lag(thời gian từ event của run đến lần sửa hiển thị nó), counterhango.telegram.unknown_senders, counterhango.telegram.format_fallbacks(HTML bị từ chối, gửi lại dạng plain text), counterhango.telegram.poll_conflicts(409 từgetUpdates). - Log mang
telegram.update_id,run_idvà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ớihttptest.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à resolveHANGO_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ộnginternal/tool/builtin, thêminternal/telegram.internal/server/handlers_runs.go— gọirun.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 địnhhttps://api.telegram.org, được test vàmake obs-verifycủ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—/readyzgồ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
- Tách
run.Createra khỏihandlers_runs.govới các test handler hiện có vẫn xanh, rồi thêmcreate_test.gocho dedup theo update id. - Viết
api.govà test của nó:getUpdates,sendMessage,editMessageText,sendChatAction,setMyCommands, đường429 retry_after, việc lùi khi gặp409, 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. - 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. - 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. - Viết
format.govới test dạng bảng, rồistream.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ả. - 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.
- 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. - Á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.
- 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 theoupdate_idvì 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/telegramcù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, xemhango.telegram.api_callstheo status để tìm lỗi Telegram, vàhango.telegram.edit_lagkhi 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ộnginternal/tool/builtinnêu trongplan.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
429trì hoãn lời gọi kế tiếp theoretry_after, một409lùi lại mà không thoát, và/readyzfail 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.createvớidetail.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.updatevớ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 ro | Khả năng x Tác động | Dấu hiệu | Cá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ếm | Low x High | Lượ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 crash | High x Medium | Hai run có cùng telegram_update_id | Lookup dedup trước khi tạo run, được test bao phủ |
| Bộ chuyển Markdown tạo ra HTML mà Telegram từ chối | Medium x Low | hango.telegram.format_fallbacks tăng | Fallback 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 Telegram | Medium x Low | hango.telegram.api_calls{status="429"} tăng | Tôn trọng retry_after; tăng HANGO_TELEGRAM_EDIT_INTERVAL |
| Cả gia đình dùng chung quota của một tenant | High x Medium | Mứ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 request | Medium x High | Token hiện trong log | Client 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ó.