Kiến trúc

aipager là một chương trình asyncio chạy nền trong một tiến trình duy nhất. Nó nối ba thế giới: chat Telegram của bạn, các tiến trình Claude Code bạn chạy trong dtach, và hệ thống hook của Claude Code. Mọi thứ đi qua một event loop: không có worker pool, không có cơ sở dữ liệu, không có hàng đợi giữa các tiến trình.

Mã nguồn trên GitHub ↗Trang này là bản dịch của trang tiếng Anh; nếu hai bên khác nhau, trang tiếng Anh là đúng. Tiếng Anh

aipager là một chương trình asyncio chạy nền trong một tiến trình duy nhất. Nó nối ba thế giới: chat Telegram của bạn, các tiến trình Claude Code bạn chạy trong dtach, và hệ thống hook của Claude Code. Mọi thứ đi qua một event loop: không có worker pool, không có cơ sở dữ liệu, không có hàng đợi giữa các tiến trình.

Sơ đồ các phần#

Loading diagram…

Mọi thứ trong sơ đồ đều chạy trên máy tính này, trừ Telegram. aipager còn nói chuyện với vài máy chủ khác: tunnel Cloudflare của Mini App (và GitHub, một lần, để lấy cloudflared), telegram.org để lấy script của Mini App, việc kiểm tra phiên bản khi bạn yêu cầu cập nhật, việc tải mô hình nhận dạng giọng nói khi bạn dùng tin nhắn thoại lần đầu, và địa chỉ nhận báo cáo sự cố khi bạn gửi một báo cáo. Tất cả được liệt kê ở Mô hình bảo mật → kết nối mạng.

Cách tiến trình được tổ chức#

Một lần gọi asyncio.run trong aipager/cli/daemon.py chạy toàn bộ. Bên trong đó:

  • TelegramBot (aipager/bot/): giữ một Application của python-telegram-bot. Nhận cập nhật từ Telegram, chuyển chúng tới các phần xử lý tin nhắn, nút bấm và tin nhắn thoại, và giữ cho thẻ làm việc, thanh trạng thái được ghim và các câu trả lời luôn được cập nhật.
  • HookReceiver (aipager/dtach/hook_receiver.py): mở một socket datagram Unix ở $XDG_RUNTIME_DIR/aipager.sock, đọc JSON mà aipager-hook và aipager-statusline gửi tới, rồi phân việc theo hook_event_name của nó (hoặc type, với tin nhắn của chính aipager; xem sự kiện hook).
  • SessionMonitor (aipager/session_monitor.py): thức dậy mỗi 2 giây để quét /tmp/claude-dtach-*.sock, đối chiếu các phiên tìm thấy với danh sách phiên, và chạy các bộ canh chừng: một thẻ làm việc ngừng chuyển động, một tin nhắn Claude Code không bao giờ nhận, một yêu cầu cấp quyền không có tin gì trong 5 phút mà cuộc trò chuyện đã lưu cho thấy đã xong (AIPAGER_INTERACTIVE_TIMEOUT), một việc mà lúc kết thúc bị bỏ lỡ.
  • SessionRegistry (aipager/state.py): các đối tượng TrackedSession theo tên. Được lưu vào ~/.claude/aipager-sessions.json (một cách nguyên tử) khi tắt và mỗi khi có thay đổi cần giữ qua lần khởi động lại.
  • Máy chủ Mini App (aipager/miniapp/server.py): một ứng dụng aiohttp ở 127.0.0.1 trên cổng MINIAPP_PORT (mặc định 8765), được đưa ra cho Telegram qua một tunnel Cloudflare được quản lý trong lúc nó bật (mặc định là bật). Cung cấp bảng điều khiển /app.

Tất cả những phần này chạy như các task trên cùng một loop. Việc nào có thể làm loop bị chặn thì chạy trong các thread riêng: chuyển tin nhắn thoại thành chữ, tạo và gửi báo cáo sự cố, kiểm tra cập nhật và lên kế hoạch khởi động lại, tải script của Mini App và cloudflared, và kiểm tra đăng nhập lúc khởi động.

Trình tự khởi động#

Từ aipager/cli/daemon.py:

  1. Chuyển cấu hình cũ (config.env, team.yaml) sang aipager.yaml, một lần, và cất các file cũ đi.
  2. Từ chối khởi động nếu một aipager khác đang trả lời trên socket, sau đó giữ khóa của aipager (~/.local/share/aipager/daemon.lock).
  3. Kiểm tra token của bot với Telegram (getMe).
  4. Sửa lại các file của chính Claude Code: thêm hook aipager và dòng trạng thái còn thiếu vào ~/.claude/settings.json, và trả lời các câu hỏi ở lần chạy đầu mà một người dùng Telegram không thể trả lời (xem bảo mật).
  5. SessionRegistry.load(): đọc ~/.claude/aipager-sessions.json. Nếu file không có hoặc không đọc được, danh sách bắt đầu trống.
  6. Xác định địa chỉ của Mini App (URL riêng của bạn, hoặc Tailscale), rồi bot.start(): bắt đầu nhận cập nhật từ Telegram.
  7. HookReceiver.start(): xóa file socket cũ còn sót, bind, lắng nghe.
  8. bot.recover_sessions(): đưa trở lại thẻ và yêu cầu của các phiên đang làm việc hoặc đang chờ lúc aipager dừng (xem lệnh → câu trả lời khi việc kết thúc).
  9. SessionMonitor.start(): bắt đầu vòng quét mỗi 2 giây.
  10. Máy chủ Mini App khởi động, rồi tới tunnel (khi chưa đặt URL riêng), và các nút 📱 App được đăng lên.
  11. aipager chờ tín hiệu: SIGINT hoặc SIGTERM làm nó dừng, SIGUSR1 tải lại các chat và quy tắc (nhóm → tải lại không cần khởi động lại).

Trình tự tắt#

  1. SIGINT hoặc SIGTERM đặt sự kiện stop.
  2. Một lệnh /update đang chạy được cho thêm chút thời gian, rồi trình cài đặt của nó bị dừng (xem lệnh → cập nhật).
  3. registry.save().
  4. Tunnel dừng, rồi tới máy chủ Mini App.
  5. session_monitor.stop(), hook_receiver.stop() (đóng socket và xóa file của nó).
  6. bot.stop(): hủy các hiệu ứng chuyển động và dừng Application.
  7. registry.save() lần nữa, cho một thẻ mà việc gửi vẫn chưa xong.

Việc tự khởi động lại từ Telegram dựa vào việc toàn bộ trình tự này chạy xong trước khi bản thay thế bind vào socket. Những gì bạn thấy lúc đó được mô tả ở lệnh của bot → khởi động lại.

Vị trí file và socket#

Đường dẫnDùng để làm gìAi ghi
$XDG_RUNTIME_DIR/aipager.sockSocket datagram Unix cho các sự kiện hook (/tmp/aipager.sock khi không có $XDG_RUNTIME_DIR; $AIPAGER_SOCKET_PATH được ưu tiên hơn cả hai)aipager (bind)
aipager-reply-<id>.sock (cạnh socket điều khiển)Một socket trả lời ngắn hạn cho mỗi yêu cầu cấp quyền mà hook đang chờaipager-hook
aipager-flood-mute.json, aipager-flood-backoff.json (cạnh socket điều khiển)Trạng thái giới hạn tốc độ hiện tại của Telegram, cho aipager status và doctoraipager
aipager-modelswitch-<name>.json (cạnh socket điều khiển)Đánh dấu một lần đổi mô hình do aipager gõ, trong 30 giây (hook)aipager (ghi), aipager-hook (nhận)
/tmp/claude-dtach-<name>.sockSocket điều khiển của dtach cho mỗi phiêndtach
/tmp/claude-status-<name>.jsonDữ liệu dòng trạng thái của mỗi phiênaipager-statusline
/tmp/claude-notes-<name>/Một ghi chú quyền cho mỗi tin nhắn Telegram chưa được nhận, và một dấu turn-open mà hook giữ trong lúc một việc đang chạy (một tin nhắn nhập vào việc đó chỉ có thể thu hẹp quy tắc của nó)aipager (ghi chú, ghi), aipager-hook (dùng ghi chú; ghi turn-open, và cả hai cùng xóa nó)
/tmp/claude-policy-<name>.jsonBản chụp quyền chuẩn cho việc đang chạyaipager-hook (ghi lúc nhận tin nhắn), được bước kiểm tra PreToolUse đọc
/tmp/claude-policy-.floor-<uid>.jsonMức an toàn tối thiểu: danh sách có sẵn cộng với phần safety: của bạn, cho các đường dự phòng của hookaipager
/tmp/claude-reply-<name>.txtTin nhắn bạn đã trả lời, khi bạn trả lời một tin nhắn cũ hơnaipager
/tmp/aipager-files/<chat>/File và tin nhắn thoại gửi từ Telegram, mỗi chat một thư mục riêng (0700, file 0600)aipager
/tmp/aipager.logĐầu ra của việc tự khởi động lại khi không có dịch vụaipager
~/.claude/aipager-sessions.jsonTrạng thái lâu dài của danh sách phiênaipager
~/.claude/aipager-audit.jsonlLog Cho phép / Từ chối / câu trả lờiaipager (chỉ ghi thêm)
~/.claude/aipager-flood-state.jsonLịch sử giới hạn tốc độ của Telegram cho từng chat, được giữ qua các lần khởi động lại (xử lý sự cố)aipager
~/.claude/aipager-tunnel.json, aipager-cloudflared.logTunnel Mini App mà aipager giữ: tiến trình, các cổng cục bộ và một mã băm của địa chỉ (không bao giờ là chính địa chỉ), để aipager kế tiếp tiếp quản nó sau một lần khởi động lại có kế hoạch (quyền 600); các cảnh báo của cloudflaredaipager; tiến trình canh gác của tunnel được giữ lại và aipager uninstall cũng xóa bản ghi này
~/.claude/aipager-pending-users.jsonNhững người đã nhắn bot trong một nhóm mà bot không phục vụ, hoặc không phải thành viên của một nhóm mà bot phục vụ, để aipager config thêm họ (quyền 0600, mỗi lần ghi thì thay cả file, 200 người mới nhất trong 30 ngày qua)aipager (ghi), aipager config (đọc)
~/aipager-users/<label>/Thư mục gốc mặc định của các thành viên (quyền 0700), nơi các thành viên user và read_only bắt đầu phiên của mình; aipager uninstall giữ lại chúngaipager (tạo ra ở lần /new đầu tiên hoặc lần “Phiên mới” đầu tiên trong Mini App của một thành viên)
~/.claude/settings.jsonCấu hình hook của Claude Codeaipager config / aipager setup; mỗi lần khởi động, aipager thêm hook hoặc dòng trạng thái aipager còn thiếu và đặt skipDangerousModePermissionPrompt (bảo mật)
~/.claude/settings.json.bak.*Bản sao lưu trước mỗi lần ghi lạiaipager config, aipager setup, aipager uninstall
~/.claude.jsonFile của chính Claude Code: aipager đánh dấu thư mục nơi một phiên bắt đầu là đáng tin ngay trước khi phiên bắt đầu (kể cả thư mục home, không bao giờ / hay một thư mục phía trên thư mục home), để câu hỏi về độ tin cậy của Claude Code không chặn một phiên bắt đầu từ Telegram. Lúc aipager khởi động, không có gì được đánh dấu. Không bao giờ bị ghi đè khi không đọc được trọn vẹn hoặc một Claude đang chạy đã thay đổi nó trong lúc đóaipager
~/.claude/.credentials.json → .credentials.json.staleFile đăng nhập của Claude Code, được cất sang một bên khi một phiên bắt đầu với CLAUDE_CODE_OAUTH_TOKEN được đặt mà file chỉ chứa một lần đăng nhập đã hết hạn hoặc trống (nếu không, file sẽ được ưu tiên hơn token); muốn hoàn tác thì chuyển nó về chỗ cũaipager
~/.config/aipager/aipager.yamlToken bot, các chat, thành viên + vai trò, cài đặt Mini Appaipager config (quyền 600); aipager chuyển mục của một nhóm khi Telegram nâng cấp nó thành supergroup, và giữ một bản aipager.yaml.bak.<time> khi chuyển cài đặt Mini App cũ vào đó
~/.config/aipager/policy.yamlQuy tắc theo vai trò + phần bổ sung cho mức an toànbạn (kiểm tra bằng aipager policy validate); một bản mẫu có chú thích được ghi một lần khi chuyển một bản cài cũ sang
~/.config/aipager/preferences.jsonCác lựa chọn /settings của từng chat và từng phiênaipager
~/.config/aipager/daemon.envThông tin đăng nhập Claude cho các phiên được khởi độngbạn / aipager service install / aipager doctor --fix (quyền 600)
~/.config/aipager/config.env, team.yaml → *.bak.<time>, *.retired.<time>Cấu hình của một bản cài cũ: được sao chép, rồi đổi tên cất đi khi aipager.yaml đã chứa nóaipager, aipager config, aipager setup
~/.config/aipager/keyboard.jsonTùy chỉnh bàn phím (không bắt buộc)bạn
~/.local/share/aipager/sessions/Một thư mục cho mỗi phiên với SESSION.md của nó (ai được nói chuyện với phiên, và các quy tắc), được tạo lại mỗi lần khởi động phiênaipager
~/.local/share/aipager/webapp-sdk/, cloudflared/Script Mini App của Telegram và chương trình cloudflared đã được xác minhaipager
~/.local/share/aipager/daemon.lock, update.lock, update-restart.jsonMỗi lúc chỉ một aipager; mỗi lúc chỉ một lần cập nhật; lần cập nhật cần thông báo sau khi khởi động lạiaipager
~/.local/share/aipager/install.json, running.jsonaipager bắt đầu ở đây lần đầu khi nào; bây giờ nó có đang chạy không (để phân biệt crash với dừng bình thường)aipager
~/.local/state/aipager/reports.jsonBáo cáo sự cố: dấu vân tay lỗi đã ghi nhận, số lần, và trạng thái của lời đề nghị (quyền 600); xem báo cáo sự cốaipager
~/.local/state/aipager/report-sends.jsonCác báo cáo sự cố đã gửi hôm nay (giới hạn mỗi ngày)aipager, aipager report
~/.local/state/aipager/languages.jsonTrạng thái ngôn ngữ (quyền 600): ai đã dùng aipager trước khi có ngôn ngữ, ngôn ngữ Telegram gần nhất thấy được của từng người, lời đề nghị đổi ngôn ngữ (chỉ một lần) và dòng chào đầu tiên đã được gửi chưa, và mã băm của mỗi mô tả bot mà aipager đã đặt; xem Ngôn ngữaipager
~/.config/systemd/user/aipager.service (Linux), ~/Library/LaunchAgents/com.aipager.daemon.plist (macOS), và một bản .bak.<time> của file bị lần cài đặt thay thế (trên Linux chỉ khi nó khác)Dịch vụ chạy nềnaipager service install
~/Library/Logs/aipager.log (macOS)Log của dịch vụ launchd, được aipager logs đọclaunchd, cho dịch vụ

Ngoài những file này, aipager tạo một thư mục phiên mới khi bạn yêu cầu (“Thư mục mới” trong /new hoặc trong Mini App), và các trình cài đặt cùng mô hình giọng nói ghi file riêng của chúng khi bạn cập nhật hoặc dùng tin nhắn thoại. Tất cả đều nằm trong thư mục home của bạn, trong /tmp, hoặc cạnh socket điều khiển. aipager không bao giờ nâng quyền; xem bảo mật.

Bản chạy tách biệt (để thử nghiệm)#

Đặt AIPAGER_INSTANCE_DIR thành một thư mục tuyệt đối thuộc về bạn, và mọi file dùng chung ở trên sẽ chuyển vào đó: socket điều khiển (cùng các socket trả lời, file tín hiệu giới hạn tốc độ và dấu đổi mô hình nằm cạnh nó), các socket claude-dtach-*, các file claude-policy-*, claude-notes-*, claude-status-* và claude-reply-*, file mức an toàn tối thiểu, thư mục tải về aipager-files và log tự khởi động lại. Các phiên mà bản chạy này khởi động sẽ thừa hưởng cài đặt, nên hook của chúng chỉ nói chuyện với bản chạy đó, và các quy tắc an toàn bảo vệ các file đã chuyển theo cùng cách. Bản chạy này không đọc .env của một bản sao mã nguồn.

Các file trong thư mục home của bạn không chuyển theo, nên hãy trỏ cả HOME tới một thư mục riêng. aipager start từ chối chạy khi thư mục bản chạy được đặt mà HOME lại là thư mục home thật của bạn, khi thư mục đó không phải đường dẫn tuyệt đối tới một thư mục thuộc về bạn, hoặc khi nó quá dài để làm đường dẫn socket.

/update và /restart không được tách biệt: chúng tác động lên gói aipager đã cài và dịch vụ aipager.service, nên đừng bao giờ gửi chúng tới một bản chạy thử nghiệm.

AIPAGER_TELEGRAM_API_BASE (ví dụ http://127.0.0.1:41234) gửi mọi yêu cầu Bot API và mọi lượt tải file tới địa chỉ đó thay vì https://api.telegram.org. Địa chỉ đó phải là máy tính này (localhost hoặc một địa chỉ loopback), trừ khi đặt AIPAGER_TELEGRAM_API_ALLOW_REMOTE=1, để một lỗi gõ nhầm không thể gửi token của bot đi nơi khác.

Cả hai cài đặt tồn tại để các test end-to-end có thể chạy một aipager thứ hai với một Telegram giả trên máy, bên cạnh aipager thật của bạn. Khi không đặt cái nào, mọi đường dẫn và địa chỉ đều như trong bảng trên.

Vì sao dùng dtach#

dtach cho mỗi phiên Claude Code một PTY thật mà không gắn nó vào một terminal phải luôn mở. aipager bắt đầu một phiên bằng dtach -n và gõ vào nó bằng dtach -p (tin nhắn, Escape, các phím để trả lời một hộp thoại); nó không bao giờ đọc màn hình, vì hook và cuộc trò chuyện đã lưu cho biết chuyện gì xảy ra. Bạn có thể kết nối vào để dùng trực tiếp từ bất kỳ shell nào bằng aipager session <name>. Kết nối đó đi theo phiên qua một lần khởi động lại từ Telegram (/mode sang hoặc từ Chế độ Toàn quyền, /restart, “Khởi động lại”): khi socket của phiên biến mất, nó chờ tối đa 15 giây cho cùng socket đó quay lại rồi tự kết nối lại. Một phiên đã thực sự kết thúc sẽ trả bạn về shell sau khi chờ (Ctrl-C để thôi chờ sớm hơn). Kết nối chạy với phím tách của dtach bị tắt, nên Ctrl-\ đi tới Claude; để thoát, đóng terminal hoặc ô tmux, và phiên vẫn tiếp tục chạy.

Kết quả: Claude Code chạy như thể bạn đang gõ trong terminal, nhưng terminal đó có thể đến rồi đi mà không làm phiền phiên đang chạy. aipager tìm ra các phiên bằng cách quét /tmp/claude-dtach-*.sock ở mỗi vòng quét 2 giây. Một phiên được tìm thấy thuộc về chat mà tên của nó được tạo cho, còn một phiên bắt đầu trong terminal thì, trên bản cài có cả chat riêng lẫn nhóm, thuộc về chat riêng của chủ sở hữu.

Xem thêm#