用 Telegram 當終端機跑 CLI
| | | 4 | |
我有個冷門需求,用了另類方法解決,覺得蠻有趣,寫篇筆記分享。
之前提過我家的路由器對外是用非固定 IP (理由是它會定期更換,不易被人鎖定,比固定 IP 來得安全),但因為要開 VPN 從外連回來,所以需要知道對外 IP,華碩路由器有免費 DDNS 服務,但之前有陣子不穩定被搞到有點不爽,最後我的解法是設定排程定期查詢對外 IP 自動更新 DNS,簡單解決問題。
但偶爾遇到網路異常,或在兩次排程間 IP 異動 DNS 還來不及更新,就只能用 LINE 敲小閃光,若剛好沒課在家先確認家裡網路是否不通,若通就幫我連 ifconfig.io 回報最新對外 IP。
最近想把這段確認查詢交給機器人處理,任務預先可知且固定,沒必要串 AI 模型花 Token,而為了提高效率最好能像 CLI 一樣打幾個字下指令給參數就收工,別浪費打字或說一堆話交辦。想了想,若想隨時隨地可用,可用 Telegram 或 LINE,形式等於用聊天視窗當終端機跑 CLI 工具,很怪,但是我想要的功能。
LINE 機器人需要有公開網站介面跑 Webhook,加上需要綁 Office Account、新增 Message API,設定較麻煩;這回我打算試寫 Telegram 機器人接收訊息並回應,程式在內網執行即可,不需找外部伺服器或打洞接受外部連線,省事又安全。
話說,Telegram 接受訊息的機器人超級簡單好寫,以 C# 為例,dotnet package add Telegram.Bot,用 var bot = new TelegramBotClient(token); 建立 Bot 個體,bot.OnMessage += async (msg, type) => { ... await bot.SendMessage(chatId: msg.Chat.Id, text: reply, cancellationToken: cts.Token); 指定回應邏輯,接著用 var me = await bot.GetMe(); 啟動,這樣就可以了。
至於實作 POSIX 標準 CLI 介面,我借用了微軟官方的 System.CommandLine 程式庫,省去自己處理彈性化參數組合的功夫,主體程式如下:
using System.CommandLine;
using TelegramCliBot;
using Telegram.Bot;
DotNetEnv.Env.Load();
// 確保 stdout 在非 TTY 環境(如 Docker)下不緩衝,讓 docker compose logs 能即時看到輸出
Console.OutputEncoding = System.Text.Encoding.UTF8;
var stdout = new System.IO.StreamWriter(Console.OpenStandardOutput()) { AutoFlush = true };
Console.SetOut(stdout);
var token = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN") ??
throw new InvalidOperationException("請設定環境變數 TELEGRAM_BOT_TOKEN");
var bot = new TelegramBotClient(token);
using var cts = new CancellationTokenSource();
var cmdModule = new CommandModule();
bot.OnMessage += async (msg, type) =>
{
if (msg.Text is null) return;
Console.WriteLine($"[{msg.Chat.Id}] {msg.Chat.FirstName}: {msg.Text}");
// 移除 Telegram 指令前置 '/'(/echo hello → echo hello)
var text = msg.Text.TrimStart();
if (text.StartsWith('/')) text = text[1..];
string reply;
try
{
reply = cmdModule.ParseAndInvoke(text);
if (string.IsNullOrWhiteSpace(reply)) reply = "(無輸出)";
}
catch (Exception ex)
{
reply = $"❌ 錯誤:{ex.Message}";
}
await bot.SendMessage(chatId: msg.Chat.Id, text: reply, cancellationToken: cts.Token);
};
bot.OnError += async (ex, _) =>
{
Console.WriteLine($"發生錯誤:{ex.Message}");
await Task.CompletedTask;
};
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel();
var me = await bot.GetMe();
Console.WriteLine($"機器人 @{me.Username} 已啟動,等待指令中…");
Console.WriteLine("傳送 -h 或 --help 查看可用指令。");
try { await Task.Delay(Timeout.Infinite, cts.Token); } catch (TaskCanceledException) { }
Console.WriteLine("機器人已停止。");
如下圖,指令參數用起來跟 CLI 一模一樣,常用 CMD、bash 的人應該會喜歡。

而命令的部分,我讓每個指令自成一個物件,只需寫很少的 Code 即可完成。

using System.CommandLine;
namespace TelegramCliBot.Commands;
public class WhatsMyIpCommand : Command
{
static HttpClient httpClient = new ();
public WhatsMyIpCommand() : base("ip", "顯示寬頻對外之公共 IP 位址")
{
this.SetAction(pr =>
{
try
{
string ip = httpClient.GetStringAsync("https://icanhazip.com").Result;
pr.InvocationConfiguration.Output.WriteLine($"對外 IP 位址 = {ip}");
}
catch (Exception ex)
{
pr.InvocationConfiguration.Output.WriteLine($"無法取得 IP 位址:{ex.Message}");
}
});
}
}
總之,我自己用得很滿意,哈! 要是剛好你也喜歡這種另類玩法,範例專案已上傳到 Github。
Describes building a Telegram bot that works like a CLI, using System.CommandLine and C# to execute predefined commands (e.g., checking public IP) without AI, enabling simple, reliable automation entirely within a private network.
Comments
# by 名無し
寫支小程式或 PowerShell 排程透過 Cloudflare 的 API 更新 A 記錄不是比較快?
# by Jeffrey
to 名無し, 已有類似機制(可參考文中取對外 IP 文章連結),本文是機制失效時排查用。
# by yoyo
請問此use case 如果在路由器設定tailscale會比較簡單嗎?
# by Jeffrey
to yoyo, 會,tailscale 很多人推。純粹是我不想透過第三方服務連 VPN 而已 (自架 Headscale 可完全自治,但走到這一步又違背力求簡單的初衷),就當因為莫名堅持自找麻煩吧,呵。