我日常涉及 Hugo 博客发布、客户端打包、Nginx 运维等多种重复性脚本。每次都要 SSH 连服务器并执行命令,操作链过长,也不方便,特别是在身边没有电脑的情况。所以我就想构建一个通用的执行引擎,通过小程序远程触发,且具备零前端修改的扩展能力。
系统架构设计
为了实现“明天增加脚本,小程序不发版”的目标,采用了“配置驱动” 模式。即“配置在云端,指令在指尖”。通过将业务逻辑(脚本路径与名称)完全从前端小程序中解耦,实现一套代码支持无限扩展的运维能力。
核心流程
- 后端 (Go):维护一个脚本配置列表(数据库或配置文件)。
- 前端 (小程序):启动时请求后端接口,拉取可用脚本列表。
- 触发:使用时选择脚本名称,点击执行。
- 鉴权:后端校验小程序 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的需求:
- 在服务器编写脚本:restart_nginx.sh。
- 更新后端配置:在 tasks 数组中追加一条记录 {ID: “nginx-reload”, Name: “重启 Nginx”}。
- 生效:重新打开小程序,列表已自动出现“重启 Nginx”,点击即可运行。
该方案优势
- 极简维护:小程序前端代码一生只需写一次,后续所有改动均在服务器端完成。
- 绝对安全:前端无法传递自定义 shell 命令,只能选择后端允许的 ID,杜绝注入攻击。基于微信生态,非本人无法通过鉴权。
- 轻量化:不记录日志、不反馈复杂结果,仅作为“远程开关”,响应极快。
前端示例代码:
- 操作界面
<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>
- 操作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' });
}
}
});
}
})
- 操作界面样式
.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;
}
想想就😄,当我在下班路上碰到服务器内存告警的时候,在手机上轻轻一点我的服务,问题就解决了,回家再慢慢查找原因。