<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh">
	<id>https://md5.pw/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Musazu</id>
	<title>md5.pw - 用户贡献 [zh]</title>
	<link rel="self" type="application/atom+xml" href="https://md5.pw/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Musazu"/>
	<link rel="alternate" type="text/html" href="https://md5.pw/index.php?title=Special:%E7%94%A8%E6%88%B7%E8%B4%A1%E7%8C%AE/Musazu"/>
	<updated>2026-05-03T13:38:20Z</updated>
	<subtitle>用户贡献</subtitle>
	<generator>MediaWiki 1.43.5</generator>
	<entry>
		<id>https://md5.pw/index.php?title=%E5%AE%9E%E6%88%98%E7%AC%94%E8%AE%B0%EF%BC%9A%E6%90%AC%E7%93%A6%E5%B7%A5_Docker_%E9%83%A8%E7%BD%B2_.NET_8_%E4%B8%8E_BWH_API_%E8%87%AA%E5%8A%A8%E7%9B%91%E6%8E%A7%E6%B5%81%E6%B0%B4%E7%BA%BF&amp;diff=1937</id>
		<title>实战笔记：搬瓦工 Docker 部署 .NET 8 与 BWH API 自动监控流水线</title>
		<link rel="alternate" type="text/html" href="https://md5.pw/index.php?title=%E5%AE%9E%E6%88%98%E7%AC%94%E8%AE%B0%EF%BC%9A%E6%90%AC%E7%93%A6%E5%B7%A5_Docker_%E9%83%A8%E7%BD%B2_.NET_8_%E4%B8%8E_BWH_API_%E8%87%AA%E5%8A%A8%E7%9B%91%E6%8E%A7%E6%B5%81%E6%B0%B4%E7%BA%BF&amp;diff=1937"/>
		<updated>2026-04-15T12:29:33Z</updated>

		<summary type="html">&lt;p&gt;Musazu：​&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;最近社区里有不少帖子在讨论各种面板、测速，或者教大家写 Bash 脚本查 VPS 流量。&lt;br /&gt;
&lt;br /&gt;
其实对于开发者来说，买一台网络稳的机器（比如搬瓦工的 GIA），最大的用途还是跑自己的后端服务。但这年头，如果每次改完代码还要手动 FTP 传文件、在宿主机折腾各种运行时环境，不仅容易把系统弄脏，维护起来也挺繁琐。&lt;br /&gt;
&lt;br /&gt;
今天分享一套我日常在用的 DevOps 工作流：在 Linux 下用 &#039;&#039;&#039;Docker Compose&#039;&#039;&#039; 隔离运行 .NET 8 服务，搭配 &#039;&#039;&#039;GitHub Actions&#039;&#039;&#039; 做全自动 CI/CD。顺便用 C# 对接一下搬瓦工官方的 KiwiVM API，写个 Telegram 监控端。废话不多说，直接开始。&lt;br /&gt;
&lt;br /&gt;
== 01. 代码实现：C# 调用 KiwiVM API 与 TG 机器人 ==&lt;br /&gt;
&lt;br /&gt;
相比传统的 Bash 脚本定时任务，用 C# 的 &amp;lt;code&amp;gt;BackgroundService&amp;lt;/code&amp;gt; 做长连接监控更稳定。另外，很多人用脚本查 API 经常会算错流量，主要是没留意官方文档里的两个细节：&#039;&#039;&#039;搬瓦工的高端机房流量是有 &amp;lt;code&amp;gt;monthly_data_multiplier&amp;lt;/code&amp;gt;（流量乘数）的，而且 API 返回的日期是 UNIX 时间戳。&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
我们在 .NET 8 Worker 项目中引入 &amp;lt;code&amp;gt;Telegram.Bot&amp;lt;/code&amp;gt;，顺手把这两个容易踩坑的地方处理掉：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;csharp&amp;quot;&amp;gt;&lt;br /&gt;
using Telegram.Bot;&lt;br /&gt;
using Telegram.Bot.Types;&lt;br /&gt;
using System.Net.Http.Json;&lt;br /&gt;
&lt;br /&gt;
public class BwhMonitorWorker : BackgroundService&lt;br /&gt;
{&lt;br /&gt;
    private readonly TelegramBotClient _botClient = new(&amp;quot;你的_TG_BOT_TOKEN&amp;quot;);&lt;br /&gt;
    private readonly HttpClient _http = new();&lt;br /&gt;
    &lt;br /&gt;
    private const string API_HOST = &amp;quot;https://&amp;quot; + &amp;quot;api.64clouds.com&amp;quot;;&lt;br /&gt;
    private const string VEID = &amp;quot;你的VEID&amp;quot;;&lt;br /&gt;
    private const string API_KEY = &amp;quot;你的API_KEY&amp;quot;;&lt;br /&gt;
    private const long ADMIN_CHAT_ID = 123456789; // 你的 TG 账号 ID&lt;br /&gt;
&lt;br /&gt;
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)&lt;br /&gt;
    {&lt;br /&gt;
        _botClient.StartReceiving(HandleUpdateAsync, HandleErrorAsync, null, stoppingToken);&lt;br /&gt;
        while (!stoppingToken.IsCancellationRequested) await Task.Delay(1000, stoppingToken);&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct)&lt;br /&gt;
    {&lt;br /&gt;
        if (update.Message?.Text == null || update.Message.Chat.Id != ADMIN_CHAT_ID) return;&lt;br /&gt;
        &lt;br /&gt;
        var text = update.Message.Text;&lt;br /&gt;
        var chatId = update.Message.Chat.Id;&lt;br /&gt;
&lt;br /&gt;
        if (text == &amp;quot;/status&amp;quot;)&lt;br /&gt;
        {&lt;br /&gt;
            var url = $&amp;quot;{API_HOST}/v1/getServiceInfo?veid={VEID}&amp;amp;api_key={API_KEY}&amp;quot;;&lt;br /&gt;
            var res = await _http.GetFromJsonAsync&amp;lt;BwhInfo&amp;gt;(url, ct);&lt;br /&gt;
            &lt;br /&gt;
            if (res.error == 0)&lt;br /&gt;
            {&lt;br /&gt;
                // 细节 1：计算时必须带上官方规定的流量乘数 (monthly_data_multiplier)&lt;br /&gt;
                double multiplier = res.monthly_data_multiplier &amp;gt; 0 ? res.monthly_data_multiplier : 1;&lt;br /&gt;
                double usedGb = (res.data_counter * multiplier) / 1024.0 / 1024.0 / 1024.0;&lt;br /&gt;
                double limitGb = (res.plan_monthly_data * multiplier) / 1024.0 / 1024.0 / 1024.0;&lt;br /&gt;
                &lt;br /&gt;
                // 细节 2：UNIX 时间戳转换为本地直观时间&lt;br /&gt;
                DateTime resetDate = DateTimeOffset.FromUnixTimeSeconds(res.data_next_reset).LocalDateTime;&lt;br /&gt;
                &lt;br /&gt;
                await bot.SendTextMessageAsync(chatId,&lt;br /&gt;
                    $&amp;quot;🖥️ 主机名: {res.hostname}\n&amp;quot; +&lt;br /&gt;
                    $&amp;quot;📍 节点: {res.node_location}\n&amp;quot; +&lt;br /&gt;
                    $&amp;quot;📶 流量: {usedGb:F2}GB / {limitGb:F2}GB\n&amp;quot; +&lt;br /&gt;
                    $&amp;quot;📅 重置: {resetDate:yyyy-MM-dd HH:mm:ss}&amp;quot;,&lt;br /&gt;
                    cancellationToken: ct);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        else if (text == &amp;quot;/reboot&amp;quot;)&lt;br /&gt;
        {&lt;br /&gt;
            var url = $&amp;quot;{API_HOST}/v1/restart?veid={VEID}&amp;amp;api_key={API_KEY}&amp;quot;;&lt;br /&gt;
            var res = await _http.GetFromJsonAsync&amp;lt;BwhInfo&amp;gt;(url, ct);&lt;br /&gt;
            if(res.error == 0) {&lt;br /&gt;
                 await bot.SendTextMessageAsync(chatId, &amp;quot;🔄 硬件重启指令已发送，请稍候...&amp;quot;, cancellationToken: ct);&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, CancellationToken ct) =&amp;gt; Task.CompletedTask;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// 对应官方文档的 JSON 映射类&lt;br /&gt;
public class BwhInfo {&lt;br /&gt;
    public int error { get; set; }&lt;br /&gt;
    public string hostname { get; set; }&lt;br /&gt;
    public string node_location { get; set; }&lt;br /&gt;
    public long data_counter { get; set; }&lt;br /&gt;
    public long plan_monthly_data { get; set; }&lt;br /&gt;
    public double monthly_data_multiplier { get; set; }&lt;br /&gt;
    public long data_next_reset { get; set; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 02. Docker 容器化：告别环境污染 ==&lt;br /&gt;
&lt;br /&gt;
我们不需要在宿主机上装任何 .NET SDK 或运行时，直接把程序打包进 Docker。这样不仅系统干净，而且自带进程守护（挂了自动拉起），省去了配置 Systemd 的麻烦。&lt;br /&gt;
&lt;br /&gt;
在代码根目录下新建这两个文件：&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;1. &amp;lt;code&amp;gt;Dockerfile&amp;lt;/code&amp;gt;&#039;&#039;&#039;（极简运行时镜像）：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;docker&amp;quot;&amp;gt;&lt;br /&gt;
# 使用微软官方轻量级 ASP.NET 8 运行时镜像&lt;br /&gt;
FROM mcr.microsoft.com/dotnet/aspnet:8.0&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&lt;br /&gt;
# 拷贝编译好的文件到容器内&lt;br /&gt;
COPY . .&lt;br /&gt;
&lt;br /&gt;
# 启动程序 (替换为你的 DLL 名字)&lt;br /&gt;
ENTRYPOINT [&amp;quot;dotnet&amp;quot;, &amp;quot;MyBwhBot.dll&amp;quot;]&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;2. &amp;lt;code&amp;gt;docker-compose.yml&amp;lt;/code&amp;gt;&#039;&#039;&#039;：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
services:&lt;br /&gt;
  bwh-bot:&lt;br /&gt;
    build: .&lt;br /&gt;
    container_name: bwh-telegram-bot&lt;br /&gt;
    restart: always # 容器挂了自动重启&lt;br /&gt;
    environment:&lt;br /&gt;
      - TZ=Asia/Shanghai&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 03. GitHub Actions 全自动化 CI/CD ==&lt;br /&gt;
&lt;br /&gt;
服务和容器配置都准备好了，接下来的痛点是怎么自动化部署。这里我们直接用 GitHub Actions 搞定。&lt;br /&gt;
&lt;br /&gt;
只要你往 &amp;lt;code&amp;gt;main&amp;lt;/code&amp;gt; 分支推代码，GitHub 就会自动走完这个流程：装 .NET SDK -&amp;gt; 编译代码 -&amp;gt; 打包连同 Docker 文件一起 SCP 传到服务器 -&amp;gt; 最后通过 SSH 触发 &amp;lt;code&amp;gt;docker compose up&amp;lt;/code&amp;gt; 构建并重启。&lt;br /&gt;
&lt;br /&gt;
在代码仓库新建 &amp;lt;code&amp;gt;.github/workflows/deploy.yml&amp;lt;/code&amp;gt; 文件：&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;yaml&amp;quot;&amp;gt;&lt;br /&gt;
name: Docker Deploy to BWH&lt;br /&gt;
&lt;br /&gt;
on:&lt;br /&gt;
  push:&lt;br /&gt;
    branches: [ &amp;quot;main&amp;quot; ]&lt;br /&gt;
&lt;br /&gt;
jobs:&lt;br /&gt;
  build-and-deploy:&lt;br /&gt;
    runs-on: ubuntu-latest&lt;br /&gt;
    steps:&lt;br /&gt;
    - uses: actions/checkout@v4&lt;br /&gt;
    &lt;br /&gt;
    - name: 配置 .NET 8 编译环境&lt;br /&gt;
      uses: actions/setup-dotnet@v4&lt;br /&gt;
      with:&lt;br /&gt;
        dotnet-version: &#039;8.0.x&#039;&lt;br /&gt;
&lt;br /&gt;
    - name: 编译发布&lt;br /&gt;
      run: dotnet publish -c Release -o ./publish_out&lt;br /&gt;
&lt;br /&gt;
    - name: 准备 Docker 文件&lt;br /&gt;
      run: |&lt;br /&gt;
        cp Dockerfile ./publish_out/&lt;br /&gt;
        cp docker-compose.yml ./publish_out/&lt;br /&gt;
&lt;br /&gt;
    - name: SCP 传输文件到服务器&lt;br /&gt;
      uses: appleboy/scp-action@v0.1.7&lt;br /&gt;
      with:&lt;br /&gt;
        host: ${{ secrets.BWH_IP }}          # 你的 VPS IP&lt;br /&gt;
        username: root&lt;br /&gt;
        key: ${{ secrets.SSH_PRIVATE_KEY }}  # 私钥 (在 GitHub 后台配置)&lt;br /&gt;
        source: &amp;quot;./publish_out/*&amp;quot;&lt;br /&gt;
        target: &amp;quot;/opt/bwh-bot&amp;quot;&lt;br /&gt;
        strip_components: 1&lt;br /&gt;
&lt;br /&gt;
    - name: SSH 触发 Docker 重新构建&lt;br /&gt;
      uses: appleboy/ssh-action@v1.0.3&lt;br /&gt;
      with:&lt;br /&gt;
        host: ${{ secrets.BWH_IP }}&lt;br /&gt;
        username: root&lt;br /&gt;
        key: ${{ secrets.SSH_PRIVATE_KEY }}&lt;br /&gt;
        script: |&lt;br /&gt;
          cd /opt/bwh-bot&lt;br /&gt;
          docker compose down&lt;br /&gt;
          docker compose up -d --build&lt;br /&gt;
          docker image prune -f  # 清理旧的无用镜像&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&amp;lt;blockquote&amp;gt;&#039;&#039;&#039;提示&#039;&#039;&#039;：记得在 GitHub 仓库的 &amp;lt;code&amp;gt;Settings -&amp;gt; Secrets and variables -&amp;gt; Actions&amp;lt;/code&amp;gt; 里把 &amp;lt;code&amp;gt;BWH_IP&amp;lt;/code&amp;gt; 和 &amp;lt;code&amp;gt;SSH_PRIVATE_KEY&amp;lt;/code&amp;gt; 填好。&amp;lt;/blockquote&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== 总结 ==&lt;br /&gt;
&lt;br /&gt;
配置好之后，以后每次在本地更新完代码只要 push 一下，云端的服务就会自动完成重建和替换。&lt;br /&gt;
&lt;br /&gt;
这套流程其实对 VPS 的网络连通性有一定要求。如果服务器网络比较差，GitHub Action 在 SCP 传文件或者 SSH 连接时偶尔会超时报错。平时部署后端服务，建议尽量选网络稳一点的机房（类似 搬瓦工 CN2 GIA 高端线这种），文件传输基本秒达，CI/CD 流水线跑起来会顺畅很多。&lt;/div&gt;</summary>
		<author><name>Musazu</name></author>
	</entry>
	<entry>
		<id>https://md5.pw/index.php?title=%E5%AE%9E%E6%88%98%E7%AC%94%E8%AE%B0%EF%BC%9A%E6%90%AC%E7%93%A6%E5%B7%A5_Docker_%E9%83%A8%E7%BD%B2_.NET_8_%E4%B8%8E_BWH_API_%E8%87%AA%E5%8A%A8%E7%9B%91%E6%8E%A7%E6%B5%81%E6%B0%B4%E7%BA%BF&amp;diff=1936</id>
		<title>实战笔记：搬瓦工 Docker 部署 .NET 8 与 BWH API 自动监控流水线</title>
		<link rel="alternate" type="text/html" href="https://md5.pw/index.php?title=%E5%AE%9E%E6%88%98%E7%AC%94%E8%AE%B0%EF%BC%9A%E6%90%AC%E7%93%A6%E5%B7%A5_Docker_%E9%83%A8%E7%BD%B2_.NET_8_%E4%B8%8E_BWH_API_%E8%87%AA%E5%8A%A8%E7%9B%91%E6%8E%A7%E6%B5%81%E6%B0%B4%E7%BA%BF&amp;diff=1936"/>
		<updated>2026-04-15T12:25:07Z</updated>

		<summary type="html">&lt;p&gt;Musazu：​创建页面，内容为“最近社区里有不少帖子在讨论各种面板、测速，或者教大家写 Bash 脚本查 VPS 流量。  其实对于开发者来说，买一台网络稳的机器（比如搬瓦工的 GIA），最大的用途还是跑自己的后端服务。但这年头，如果每次改完代码还要手动 FTP 传文件、在宿主机折腾各种运行时环境，不仅容易把系统弄脏，维护起来也挺繁琐。  今天分享一套我日常在用的 DevOps 工…”&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;最近社区里有不少帖子在讨论各种面板、测速，或者教大家写 Bash 脚本查 VPS 流量。&lt;br /&gt;
&lt;br /&gt;
其实对于开发者来说，买一台网络稳的机器（比如搬瓦工的 GIA），最大的用途还是跑自己的后端服务。但这年头，如果每次改完代码还要手动 FTP 传文件、在宿主机折腾各种运行时环境，不仅容易把系统弄脏，维护起来也挺繁琐。&lt;br /&gt;
&lt;br /&gt;
今天分享一套我日常在用的 DevOps 工作流：在 Linux 下用 &#039;&#039;&#039;**Docker Compose**&#039;&#039;&#039; 隔离运行 .NET 8 服务，搭配 &#039;&#039;&#039;**GitHub Actions**&#039;&#039;&#039; 做全自动 CI/CD。顺便用 C# 对接一下搬瓦工官方的 KiwiVM API，写个 Telegram 监控端。废话不多说，直接开始。&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;## 01. 代码实现：C# 调用 KiwiVM API 与 TG 机器人&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
相比传统的 Bash 脚本定时任务，用 C# 的 `BackgroundService` 做长连接监控更稳定。另外，很多人用脚本查 API 经常会算错流量，主要是没留意官方文档里的两个细节：&#039;&#039;&#039;**搬瓦工的高端机房流量是有 `monthly_data_multiplier`（流量乘数）的，而且 API 返回的日期是 UNIX 时间戳。**&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
我们在 .NET 8 Worker 项目中引入 `Telegram.Bot`，顺手把这两个容易踩坑的地方处理掉：&lt;br /&gt;
&lt;br /&gt;
```csharp&lt;br /&gt;
&lt;br /&gt;
using Telegram.Bot;&lt;br /&gt;
&lt;br /&gt;
using Telegram.Bot.Types;&lt;br /&gt;
&lt;br /&gt;
using System.Net.Http.Json;&lt;br /&gt;
&lt;br /&gt;
public class BwhMonitorWorker : BackgroundService&lt;br /&gt;
&lt;br /&gt;
{&lt;br /&gt;
&lt;br /&gt;
    private readonly TelegramBotClient _botClient = new(&amp;quot;你的_TG_BOT_TOKEN&amp;quot;);&lt;br /&gt;
&lt;br /&gt;
    private readonly HttpClient _http = new();&lt;br /&gt;
&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
    private const string API_HOST = &amp;quot;https://&amp;quot; + &amp;quot;api.64clouds.com&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    private const string VEID = &amp;quot;你的VEID&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    private const string API_KEY = &amp;quot;你的API_KEY&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
    private const long ADMIN_CHAT_ID = 123456789; // 你的 TG 账号 ID，防止别人蹭用&lt;br /&gt;
&lt;br /&gt;
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)&lt;br /&gt;
&lt;br /&gt;
    {&lt;br /&gt;
&lt;br /&gt;
        _botClient.StartReceiving(HandleUpdateAsync, HandleErrorAsync, null, stoppingToken);&lt;br /&gt;
&lt;br /&gt;
        while (!stoppingToken.IsCancellationRequested) await Task.Delay(1000, stoppingToken);&lt;br /&gt;
&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private async Task HandleUpdateAsync(ITelegramBotClient bot, Update update, CancellationToken ct)&lt;br /&gt;
&lt;br /&gt;
    {&lt;br /&gt;
&lt;br /&gt;
        if (update.Message?.Text == null || update.Message.Chat.Id != ADMIN_CHAT_ID) return;&lt;br /&gt;
&lt;br /&gt;
       &lt;br /&gt;
&lt;br /&gt;
        var text = update.Message.Text;&lt;br /&gt;
&lt;br /&gt;
        var chatId = update.Message.Chat.Id;&lt;br /&gt;
&lt;br /&gt;
        if (text == &amp;quot;/status&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        {&lt;br /&gt;
&lt;br /&gt;
            var url = $&amp;quot;{API_HOST}/v1/getServiceInfo?veid={VEID}&amp;amp;api_key={API_KEY}&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var res = await _http.GetFromJsonAsync&amp;lt;BwhInfo&amp;gt;(url, ct);&lt;br /&gt;
&lt;br /&gt;
           &lt;br /&gt;
&lt;br /&gt;
            if (res.error == 0)&lt;br /&gt;
&lt;br /&gt;
            {&lt;br /&gt;
&lt;br /&gt;
                // 细节 1：计算时必须带上官方规定的流量乘数 (monthly_data_multiplier)&lt;br /&gt;
&lt;br /&gt;
                double multiplier = res.monthly_data_multiplier &amp;gt; 0 ? res.monthly_data_multiplier : 1;&lt;br /&gt;
&lt;br /&gt;
                double usedGb = (res.data_counter * multiplier) / 1024.0 / 1024.0 / 1024.0;&lt;br /&gt;
&lt;br /&gt;
                double limitGb = (res.plan_monthly_data * multiplier) / 1024.0 / 1024.0 / 1024.0;&lt;br /&gt;
&lt;br /&gt;
               &lt;br /&gt;
&lt;br /&gt;
                // 细节 2：UNIX 时间戳转换为本地直观时间&lt;br /&gt;
&lt;br /&gt;
                DateTime resetDate = DateTimeOffset.FromUnixTimeSeconds(res.data_next_reset).LocalDateTime;&lt;br /&gt;
&lt;br /&gt;
               &lt;br /&gt;
&lt;br /&gt;
                await bot.SendTextMessageAsync(chatId,&lt;br /&gt;
&lt;br /&gt;
                    $&amp;quot;🖥️ 主机名: {res.hostname}\n&amp;quot; +&lt;br /&gt;
&lt;br /&gt;
                    $&amp;quot;📍 节点: {res.node_location}\n&amp;quot; +&lt;br /&gt;
&lt;br /&gt;
                    $&amp;quot;📶 流量: {usedGb:F2}GB / {limitGb:F2}GB\n&amp;quot; +&lt;br /&gt;
&lt;br /&gt;
                    $&amp;quot;📅 重置: {resetDate:yyyy-MM-dd HH:mm:ss}&amp;quot;,&lt;br /&gt;
&lt;br /&gt;
                    cancellationToken: ct);&lt;br /&gt;
&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
        else if (text == &amp;quot;/reboot&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
        {&lt;br /&gt;
&lt;br /&gt;
            var url = $&amp;quot;{API_HOST}/v1/restart?veid={VEID}&amp;amp;api_key={API_KEY}&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
            var res = await _http.GetFromJsonAsync&amp;lt;BwhInfo&amp;gt;(url, ct);&lt;br /&gt;
&lt;br /&gt;
            if(res.error == 0) {&lt;br /&gt;
&lt;br /&gt;
                 await bot.SendTextMessageAsync(chatId, &amp;quot;🔄 硬件重启指令已发送，请稍候...&amp;quot;, cancellationToken: ct);&lt;br /&gt;
&lt;br /&gt;
            }&lt;br /&gt;
&lt;br /&gt;
        }&lt;br /&gt;
&lt;br /&gt;
    }&lt;br /&gt;
&lt;br /&gt;
    private Task HandleErrorAsync(ITelegramBotClient bot, Exception ex, CancellationToken ct) =&amp;gt; Task.CompletedTask;&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
// 对应官方文档的 JSON 映射类&lt;br /&gt;
&lt;br /&gt;
public class BwhInfo {&lt;br /&gt;
&lt;br /&gt;
    public int error { get; set; }&lt;br /&gt;
&lt;br /&gt;
    public string hostname { get; set; }&lt;br /&gt;
&lt;br /&gt;
    public string node_location { get; set; }&lt;br /&gt;
&lt;br /&gt;
    public long data_counter { get; set; }&lt;br /&gt;
&lt;br /&gt;
    public long plan_monthly_data { get; set; }&lt;br /&gt;
&lt;br /&gt;
    public double monthly_data_multiplier { get; set; }&lt;br /&gt;
&lt;br /&gt;
    public long data_next_reset { get; set; }&lt;br /&gt;
&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;## 02. Docker 容器化：告别环境污染&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
我们不需要在宿主机上装任何 .NET SDK 或运行时，直接把程序打包进 Docker。这样不仅系统干净，而且自带进程守护（挂了自动拉起），省去了配置 Systemd 的麻烦。&lt;br /&gt;
&lt;br /&gt;
在代码根目录下新建这两个文件：&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;**1. `Dockerfile`**&#039;&#039;&#039;（极简运行时镜像）：&lt;br /&gt;
&lt;br /&gt;
```dockerfile&lt;br /&gt;
&lt;br /&gt;
&amp;lt;nowiki&amp;gt;#&amp;lt;/nowiki&amp;gt; 使用微软官方轻量级 ASP.NET 8 运行时镜像&lt;br /&gt;
&lt;br /&gt;
FROM mcr.microsoft.com/dotnet/aspnet:8.0&lt;br /&gt;
&lt;br /&gt;
WORKDIR /app&lt;br /&gt;
&lt;br /&gt;
&amp;lt;nowiki&amp;gt;#&amp;lt;/nowiki&amp;gt; 拷贝编译好的文件到容器内&lt;br /&gt;
&lt;br /&gt;
COPY . .&lt;br /&gt;
&lt;br /&gt;
&amp;lt;nowiki&amp;gt;#&amp;lt;/nowiki&amp;gt; 启动程序 (替换为你的 DLL 名字)&lt;br /&gt;
&lt;br /&gt;
ENTRYPOINT [&amp;quot;dotnet&amp;quot;, &amp;quot;MyBwhBot.dll&amp;quot;]&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;**2. `docker-compose.yml`**&#039;&#039;&#039;：&lt;br /&gt;
&lt;br /&gt;
```yaml&lt;br /&gt;
&lt;br /&gt;
services:&lt;br /&gt;
&lt;br /&gt;
  bwh-bot:&lt;br /&gt;
&lt;br /&gt;
    build: .&lt;br /&gt;
&lt;br /&gt;
    container_name: bwh-telegram-bot&lt;br /&gt;
&lt;br /&gt;
    restart: always # 容器挂了自动重启&lt;br /&gt;
&lt;br /&gt;
    environment:&lt;br /&gt;
&lt;br /&gt;
      - TZ=Asia/Shanghai&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;## 03. GitHub Actions 全自动化 CI/CD&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
服务和容器配置都准备好了，接下来的痛点是怎么自动化部署。这里我们直接用 GitHub Actions 搞定。&lt;br /&gt;
&lt;br /&gt;
只要你往 `main` 分支推代码，GitHub 就会自动走完这个流程：装 .NET SDK -&amp;gt; 编译代码 -&amp;gt; 打包连同 Docker 文件一起 SCP 传到搬瓦工 -&amp;gt; 最后通过 SSH 触发 `docker compose up` 构建并重启。&lt;br /&gt;
&lt;br /&gt;
在代码仓库新建 `.github/workflows/deploy.yml` 文件，可以直接抄：&lt;br /&gt;
&lt;br /&gt;
```yaml&lt;br /&gt;
&lt;br /&gt;
name: Docker Deploy to BWH&lt;br /&gt;
&lt;br /&gt;
on:&lt;br /&gt;
&lt;br /&gt;
  push:&lt;br /&gt;
&lt;br /&gt;
    branches: [ &amp;quot;main&amp;quot; ]&lt;br /&gt;
&lt;br /&gt;
jobs:&lt;br /&gt;
&lt;br /&gt;
  build-and-deploy:&lt;br /&gt;
&lt;br /&gt;
    runs-on: ubuntu-latest&lt;br /&gt;
&lt;br /&gt;
    steps:&lt;br /&gt;
&lt;br /&gt;
    - uses: actions/checkout@v4&lt;br /&gt;
&lt;br /&gt;
   &lt;br /&gt;
&lt;br /&gt;
    - name: 配置 .NET 8 编译环境&lt;br /&gt;
&lt;br /&gt;
      uses: actions/setup-dotnet@v4&lt;br /&gt;
&lt;br /&gt;
      with:&lt;br /&gt;
&lt;br /&gt;
        dotnet-version: &#039;8.0.x&#039;&lt;br /&gt;
&lt;br /&gt;
    - name: 编译发布&lt;br /&gt;
&lt;br /&gt;
      run: dotnet publish -c Release -o ./publish_out&lt;br /&gt;
&lt;br /&gt;
    - name: 准备 Docker 文件&lt;br /&gt;
&lt;br /&gt;
      run: |&lt;br /&gt;
&lt;br /&gt;
        cp Dockerfile ./publish_out/&lt;br /&gt;
&lt;br /&gt;
        cp docker-compose.yml ./publish_out/&lt;br /&gt;
&lt;br /&gt;
    - name: SCP 传输文件到服务器&lt;br /&gt;
&lt;br /&gt;
      uses: appleboy/scp-action@v0.1.7&lt;br /&gt;
&lt;br /&gt;
      with:&lt;br /&gt;
&lt;br /&gt;
        host: $&amp;lt;nowiki&amp;gt;{{ secrets.BWH_IP }}&amp;lt;/nowiki&amp;gt;          # 你的 VPS IP&lt;br /&gt;
&lt;br /&gt;
        username: root&lt;br /&gt;
&lt;br /&gt;
        key: $&amp;lt;nowiki&amp;gt;{{ secrets.SSH_PRIVATE_KEY }}&amp;lt;/nowiki&amp;gt;  # 私钥 (在 GitHub 后台配置)&lt;br /&gt;
&lt;br /&gt;
        source: &amp;quot;./publish_out/*&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        target: &amp;quot;/opt/bwh-bot&amp;quot;&lt;br /&gt;
&lt;br /&gt;
        strip_components: 1&lt;br /&gt;
&lt;br /&gt;
    - name: SSH 触发 Docker 重新构建&lt;br /&gt;
&lt;br /&gt;
      uses: appleboy/ssh-action@v1.0.3&lt;br /&gt;
&lt;br /&gt;
      with:&lt;br /&gt;
&lt;br /&gt;
        host: $&amp;lt;nowiki&amp;gt;{{ secrets.BWH_IP }}&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        username: root&lt;br /&gt;
&lt;br /&gt;
        key: $&amp;lt;nowiki&amp;gt;{{ secrets.SSH_PRIVATE_KEY }}&amp;lt;/nowiki&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        script: |&lt;br /&gt;
&lt;br /&gt;
          cd /opt/bwh-bot&lt;br /&gt;
&lt;br /&gt;
          docker compose down&lt;br /&gt;
&lt;br /&gt;
          docker compose up -d --build&lt;br /&gt;
&lt;br /&gt;
          docker image prune -f  # 清理旧的无用镜像&lt;br /&gt;
&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
&amp;gt; &#039;&#039;&#039;**提示**&#039;&#039;&#039;：记得在 GitHub 仓库的 `Settings -&amp;gt; Secrets and variables -&amp;gt; Actions` 里把 `BWH_IP` 和 `SSH_PRIVATE_KEY` 填好。&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;## 总结&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
配置好之后，以后每次在本地更新完代码只要 push 一下，云端的服务就会自动完成重建和替换。&lt;br /&gt;
&lt;br /&gt;
这套流程其实对 VPS 的网络连通性有一定要求。如果服务器网络比较差，GitHub Action 在 SCP 传文件或者 SSH 连接时偶尔会超时报错。平时部署后端服务，建议尽量选网络稳一点的机房（类似 搬瓦工 CN2 GIA 高端线这种），文件传输基本秒达，CI/CD 流水线跑起来会顺畅很多。&lt;/div&gt;</summary>
		<author><name>Musazu</name></author>
	</entry>
</feed>