我有個冷門需求,用了另類方法解決,覺得蠻有趣,寫篇筆記分享。

之前提過我家的路由器對外是用非固定 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 可完全自治,但走到這一步又違背力求簡單的初衷),就當因為莫名堅持自找麻煩吧,呵。

Post a comment