我日常涉及 Hugo 博客发布、客户端打包、Nginx 运维等多种重复性脚本。每次都要 SSH 连服务器并执行命令,操作链过长,也不方便,特别是在身边没有电脑的情况。所以我就想构建一个通用的执行引擎,通过小程序远程触发,且具备零前端修改的扩展能力。

系统架构设计

为了实现“明天增加脚本,小程序不发版”的目标,采用了“配置驱动” 模式。即“配置在云端,指令在指尖”。通过将业务逻辑(脚本路径与名称)完全从前端小程序中解耦,实现一套代码支持无限扩展的运维能力。

核心流程

  1. 后端 (Go):维护一个脚本配置列表(数据库或配置文件)。
  2. 前端 (小程序):启动时请求后端接口,拉取可用脚本列表。
  3. 触发:使用时选择脚本名称,点击执行。
  4. 鉴权:后端校验小程序 OpenID,仅允许本人指令生效。

技术实现

系统分为三层,确保安全性与扩展性的统一:

  • 配置层 (Go Config):在服务器端定义脚本的 ID、名称和实际路径。
  • 鉴权层 (WeChat Auth):利用微信小程序 OpenID 建立强一致性的身份白名单。
  • 展示层 (Mini Program UI):动态拉取后端配置,仅负责“展示列表”与“触发指令”。

技术实现方案

A. 后端:动态脚本引擎 (Go)

后端不再硬编码脚本路径,而是定义一个结构体:

// 脚本任务定义
type ScriptTask struct {
    ID      string `json:"id"`      // 前端传递的任务标识
    Name    string `json:"name"`    // 小程序界面显示的文字
    Command string `json:"-"`       // 实际执行的脚本路径 (对前端保密)
}

// 示例配置(可存放在 JSON 文件或数据库中)
var tasks = []ScriptTask{
    {ID: "hugo-post", Name: "发布 Hugo 文章", Command: "/scripts/deploy_hugo.sh"},
    {ID: "build-client", Name: "构建客户端", Command: "/scripts/build_mole_go.sh"},
    {ID: "nginx-restart", Name: "重启 Nginx 服务", Command: "systemctl restart nginx"},
}

对外接口定义:

  • API 1 (/list): 返回 []ScriptTask(过滤掉 Path 字段)。
  • API 2 (/execute): 接收 id 和 code (小程序 OpenID/Token)。

其中在调用/execute执行接口时,通过微信登录流程获取 code 换取 openid,后端建立白名单。实现OpenID 物理隔离,前端只传递 ID(如 nginx-restart),不传递任何实际的 shell 指令,防止命令注入攻击。实现指令脱敏。非常危险的脚本,如执行删除操作,不对外提供接口,手动在服务器操作。

这是执行鉴权示例代码:

const AdminOpenID = "YOUR_WECHAT_OPENID"

func ExecuteHandler(w http.ResponseWriter, r *http.Request) {
    userOpenID := getOpenID(r) // 从鉴权 Token 中解析
    if userOpenID != AdminOpenID {
        http.Error(w, "权限不足", http.StatusForbidden)
        return
    }
    // 执行对应 ID 的脚本
}

B. 前端:动态 UI 渲染 (小程序)

小程序端保持高度抽象,只负责展示,小程序端不包含任何具体的脚本逻辑,界面完全由后端接口驱动。例如:

  • Data 结构:scripts: []
  • 交互逻辑:onLoad 时请求 /list,将结果渲染为 picker (选择器) 或列表。点击按钮时,发送当前选中的 id 给后端。

当扩展新的功能时,例如明天增加了重启Nginx的需求:

  1. 在服务器编写脚本:restart_nginx.sh。
  2. 更新后端配置:在 tasks 数组中追加一条记录 {ID: “nginx-reload”, Name: “重启 Nginx”}。
  3. 生效:重新打开小程序,列表已自动出现“重启 Nginx”,点击即可运行。

该方案优势

  • 极简维护:小程序前端代码一生只需写一次,后续所有改动均在服务器端完成。
  • 绝对安全:前端无法传递自定义 shell 命令,只能选择后端允许的 ID,杜绝注入攻击。基于微信生态,非本人无法通过鉴权。
  • 轻量化:不记录日志、不反馈复杂结果,仅作为“远程开关”,响应极快。

前端示例代码:

  1. 操作界面
<view class="page" data-weui-theme="light">
  <view class="weui-form">
    <view class="weui-form__text-area">
      <h2 class="weui-form__title">指令中心</h2>
      <view class="weui-form__desc">仅限管理员操作,请谨慎触发本地脚本</view>
    </view>

    <view class="weui-form__control-area">
      <view class="weui-cells__group weui-cells__group_form">
        <view class="weui-cells weui-cells_after-title">
          
          <!-- 脚本选择器 -->
          <picker bindchange="bindScriptChange" value="{{index}}" range="{{scriptList}}" range-key="name">
            <view class="weui-cell weui-cell_active weui-cell_select weui-cell_select-after">
              <view class="weui-cell__hd">
                <label class="weui-label">待执行脚本</label>
              </view>
              <view class="weui-cell__bd">
                <view class="weui-select">{{scriptList[index].name || '点击选择脚本'}}</view>
              </view>
            </view>
          </picker>

          <!-- 身份确认展示(只读) -->
          <view class="weui-cell">
            <view class="weui-cell__hd"><label class="weui-label">当前身份</label></view>
            <view class="weui-cell__bd">
              <text class="weui-badge" style="background-color: #07C160;">管理员 (Authenticated)</text>
            </view>
          </view>
        </view>
      </view>
    </view>

    <!-- 操作区 -->
    <view class="weui-form__tips-area">
      <view class="weui-form__tips">所选脚本将在服务器后台静默执行</view>
    </view>
    <view class="weui-form__opr-area">
      <button class="weui-btn weui-btn_primary" bindtap="executeCommand">立即执行指令</button>
    </view>
  </view>

  <!-- 页脚说明 -->
  <view class="weui-footer weui-footer_fixed-bottom">
    <view class="weui-footer__text">Copyright © 2026 Eagle 豆子实验室</view>
  </view>
</view>
  1. 操作JS逻辑
   Page({
        data: {
            // 脚本列表:实际开发时通过 onLoad 调用后端接口获取
            scriptList: [
            { id: 'hugo-post', name: '发布 Hugo 文章' },
            { id: 'build-client', name: '构建客户端' },
            { id: 'nginx-restart', name: '重启 Nginx 服务' }
            ],
            index: 0, // 当前选择的索引
        },

        onLoad: function() {
            // TODO: 从后端接口拉取最新的脚本配置列表
            // this.fetchScriptConfig();
        },

        // 选择脚本
        bindScriptChange: function(e) {
            this.setData({
            index: e.detail.value
            });
        },

        // 执行指令
        executeCommand: function() {
            const selectedTask = this.data.scriptList[this.data.index];
            
            wx.showModal({
            title: '确认执行',
            content: '确定要运行「${selectedTask.name}」吗?',
            confirmText: '执行',
            confirmColor: '#FA5151',
            success: (res) => {
                if (res.confirm) {
                console.log('正在调用接口执行 ID:', selectedTask.id);
                // TODO: 发送 OpenID 和 TaskID 到后端
                wx.showToast({ title: '指令已发出', icon: 'success' });
                }
            }
            });
        }
        })
  1. 操作界面样式
.page {
  height: 100%;
  background-color: #ededed;
}

.weui-label {
  width: 105px;
  word-wrap: break-word;
  word-break: break-all;
}

.weui-badge {
  display: inline-block;
  padding: 0.15em 0.4em;
  min-width: 8px;
  border-radius: 18px;
  color: #fff;
  line-height: 1.2;
  text-align: center;
  font-size: 10px;
  vertical-align: middle;
}

想想就😄,当我在下班路上碰到服务器内存告警的时候,在手机上轻轻一点我的服务,问题就解决了,回家再慢慢查找原因。