控制台所有请求共用的 request() 无条件带上 Content-Type: application/json,
包括无正文的 DELETE /api/v1/instances/:id 与 POST test-connection。Fastify
的 JSON 解析器见到"声明了 JSON 却是空正文"会直接回 400 HTTP_400,请求在
进入路由之前就被拒——所以"删除节点"看起来点了没反应,日志里三次真实尝试
全部是 400,而路由本身完好。
复现(对 8788 实测):
- DELETE + Content-Type: json + 空正文 → 400 HTTP_400(旧行为,坏)
- DELETE 不带 Content-Type → 401 CONSOLE_AUTH_REQUIRED(到达路由)
- DELETE + Content-Type: json + {} → 401(能解析,故非路由问题)
- GET + Content-Type: json + 空 → 401(Fastify 对 GET 不解析正文)
修复:Content-Type 只在确实有正文时才发送。删除的意图本来就在头部
(If-Match / X-Preparation-Id / X-Confirmation-Token)里,无正文属正常形状。
同源的 POST test-connection 也一并修好。iOS 客户端本就用 route.body != nil
判断,与修复后的契约一致——Web 侧是唯一的例外。
回归测试:
- web:断言 DELETE 只发 Accept、不发 Content-Type
- api:断言真实形状到达路由并完成删除(202 且节点转 404),
同时断言"声称 JSON 却空正文"仍须显式 400、节点必须留存
验证:147 文件 / 1371 测试全绿;lint 干净;typecheck 干净;dist 已重建且
8788 正在服务的 bundle 与磁盘一致(md5 相同)。
Multi SimAdmin
一个用于聚合管理多个 SimAdmin 实例的轻量本地运维面板。
iOS Native 应用
apps/ios 提供不依赖 8788 控制台的原生 iPhone 应用,当前版本 0.2.2,构建号 10,运行在 iOS 16 及以上,产物为未签名 IPA,供 TrollStore 或自有签名流程安装。iOS 开发在 ios-native-ipa 分支进行。
底部为节点、消息、设置三个入口:
- 节点:运行概览、标签多选筛选、标签批量归类、搜索、批量选择、实例增删改,卡片直连实例详情。
- 实例详情:设备、SIM、蜂窝网络、系统资源、CPU、连接状态六个模块,敏感值默认脱敏,显示后离开页面即隐藏。
- 消息:跨实例短信读取、按实例与方向筛选、搜索、发送、删除。
- 设置:主题外观(跟随系统 / 浅色 / 深色)、应用信息、本地数据与日志中心、隐私与数据、数据清理。
实例信息与密码保存在应用沙盒本地文件,会话 Cookie 只存在于进程内存,不使用 Keychain,不上传任何数据。应用只访问用户添加的节点地址,上游路径为固定白名单。
按产品决策移除的能力:全局访问密码、定时与后台调度、iOS 远程推送、自动备份与自动清理、控制台自更新,以及依赖常驻进程的应用内通知中心(日志中心因此收进设置,不再单独占一个底部入口)。
构建与自检:
./scripts/build-ios-ipa.sh # 产出 build/ipa/MultiSimAdmin.ipa 与 .sha256
cd apps/ios && swift test # 48 个用例
文档:现行说明 docs/ios-native.md,发布记录 0.2.2、0.2.1 与 0.2.0。
功能
- 只需要在
config.json配置多个 SimAdmin 地址。 - Animal Island / 动森风格 Dashboard:暖米色背景、棕色文字、Ribbon 飘带标题、NookPhone 应用格、胶囊按钮与波点卡片。
- UI 内直接新增、编辑、删除设备地址,配置实时写入本地
config.json,并提供返回首页入口。 - 实例舰队列表、状态筛选、搜索、当前设备摘要。
- 支持无密码实例状态读取;有密码实例可通过本地服务代登录并保持会话。
- 统一代理 SimAdmin API,内置 API 工作台,便于读取/调试设备、SIM、网络、短信、eSIM、OTA 等接口。
- 保留原始 SimAdmin 页面 iframe 嵌入;若目标站禁止 iframe,可一键在原站打开。
- 融合 SimAdminHub 的管理思路:跨节点短信、通知、自动化与跨域健康自然融入 Fleet 节点总览体系,无需部署或接入外部 Hub。
- 敏感配置留在本地
config.json,仓库只提交config.example.json。
一键安装(macOS / Linux)
curl -fsSL https://gitea.chickliu.fun/Hermes/multi-simadmin/raw/branch/main/scripts/install.sh | sh
脚本会从 Gitea 下载源码,固定使用 pnpm 11.13.0 安装依赖并构建 Web,然后启动:
- LAN Gateway:
0.0.0.0:8788 - Control-plane API:
127.0.0.1:8790 - Canary Gateway:
8789保持关闭
安装完成后按终端输出访问 http://<本机局域网 IP>:8788/fleet。首次密码可在当前管理台直接设置。Gateway 默认监听所有网络接口,因此必须确保主机仅接入可信内网或已通过防火墙限制 8788 的来源;HTTP 部署不得直接暴露到公网,公网开放必须由前置代理提供 HTTPS 和访问控制。
秘密存储按平台自动选择:macOS 使用 Keychain,Linux 使用数据目录下 0600 权限的
secrets.json文件;也可用MULTI_SIMADMIN_SECRET_BACKEND=macos-keychain|secret-file显式指定。脚本不会覆盖已有源码目录或数据库,也不会占用已被其他进程监听的 8788/8790 端口。
服务管理
下载脚本后可执行完整生命周期命令:
curl -fsSL https://gitea.chickliu.fun/Hermes/multi-simadmin/raw/branch/main/scripts/install.sh -o /tmp/multi-simadmin-install.sh
sh /tmp/multi-simadmin-install.sh status
sh /tmp/multi-simadmin-install.sh restart
sh /tmp/multi-simadmin-install.sh stop
sh /tmp/multi-simadmin-install.sh start
sh /tmp/multi-simadmin-install.sh uninstall
uninstall 默认只移除程序源码,保留数据库、Gateway token 和日志。默认安装位置 macOS 为 ~/Library/Application Support/multi-simadmin,Linux 为 ~/.local/share/multi-simadmin(遵循 XDG),均可通过 MULTI_SIMADMIN_HOME 覆盖;额外 LAN Host 可通过 MULTI_SIMADMIN_ALLOWED_HOSTS(逗号分隔)配置。
Linux systemd(可选,但推荐)
sh install.sh unit 会生成 ~/.config/systemd/user/ 下的 API 与 Gateway 用户单元(Restart=on-failure、开机自启、密钥经 0600 的 service-env 注入)。随后:
systemctl --user enable --now multi-simadmin-api.service multi-simadmin-gateway.service
单元内声明了 MULTI_SIMADMIN_SYSTEMD_UNIT,因此 Web 控制台的“在线更新”在 Linux 上会把重启交给 systemd 完成,形成自更新闭环。由 systemd 托管后请用 systemctl 管理进程,不要再用本脚本 start/stop,避免双进程争抢端口。
Fleet 节点总览与跨域健康
/fleet 读取本机 Control Plane 中的节点状态、资源快照与设备号码,形成节点总览。侧栏在实例总数 / 在线 / 需处理之外,还聚合跨域健康指标:
- 短信异常:标记哪些节点的短信接口不可用,点击直达
/fleet/messages。 - 通知异常:汇总跨节点通知队列的 pending + failed 数量,点击直达
/fleet/notifications。
节点卡片直接进入对应实例详情,查看 CPU、内存、温度、SIM 号码、平台与版本。
跨节点短信中心
/fleet/messages 融合官方 SimAdminHub 的短信中心形态:它通过本地 Control Plane 按节点读取 SimAdmin 的 /api/sms/list,再跨设备聚合搜索、展示与发送。
- 跨设备记录:汇总各节点最新短信,支持按号码、内容或节点名称搜索,并分页加载。
- 会话视图:按号码分组为会话,节点栏 / 会话栏 / 消息栏三栏工作流,默认打开最新会话。
- 发送短信:从可用节点下拉选择目标设备,提交到
/api/v1/fleet/messages/send后由 Control Plane 转发到对应 SimAdmin。 - 批量删除:可在短信记录中勾选选择并确认删除,Control Plane 通过严格白名单的
/api/sms/batch-delete按节点批量清理,未成功项会以逐条失败原因返回。 - 安全边界:短信正文与号码只在本地页面展示;批量删除请求最多 500 条,且不接收外部 Hub 地址、Webhook 或任何可注入字段。
跨节点通知中心
/fleet/notifications 融合官方 SimAdminHub 的通知中心能力:它按本地节点聚合阅读 SimAdmin 的通知接口,再跨设备汇总转发通道、转发规则、转发日志与失败重试队列。
- 设备可用性:自动登录后读取
/api/notifications/config、/api/notifications/logs与/api/notifications/queue,无法读取的实例单独标记。 - 转发通道:统计通道总数、启用数以及常见的 webhook、Bark、PushPlus、企业微信、钉钉、飞书、Telegram、邮件等类型。
- 转发日志:展示成功、失败、免打扰、未匹配、无可用通道与最近记录。
- 失败重试:展示待发送、定时、重试中、发送中和失败的队列摘要,并支持整队列重试、单条重试或删除积压项。
- 安全边界:只返回经过白名单清洗的计数与最近条目,不暴露通知原文、webhook/token/secret 或模板配置。
旧版开发入口
旧 server/ 与 public/ 仅用于历史兼容和开发验证,不是当前生产安装入口:
cp config.example.json config.json
corepack pnpm install
corepack pnpm start
仓库统一使用 pnpm(
pnpm-lock.yaml)作为唯一 lockfile;陈旧的package-lock.json已移除。
配置
编辑 config.json:
{
"server": { "host": "127.0.0.1", "port": 8788 },
"instances": [
{
"id": "simadmin-open",
"name": "无密码 SimAdmin",
"url": "http://192.168.68.1:3000",
"description": "密码保护关闭或尚未配置密码的实例",
"auth": { "mode": "none" },
"tags": ["open"]
},
{
"id": "simadmin-password",
"name": "有密码 SimAdmin",
"url": "http://192.168.68.2:3000",
"description": "服务端代登录并保存会话 Cookie;密码只保存在本地 config.json",
"auth": {
"mode": "password",
"password": "CHANGE_ME"
},
"tags": ["password"]
}
]
}
字段说明:
id:唯一标识,只能包含字母、数字、下划线、短横线、点号。name:页面显示名称。url:SimAdmin 原始访问地址。description:可选描述。auth.mode:none表示无密码;password表示由本地聚合服务代登录。auth.password:可选。本地保存后可自动刷新会话;不要提交真实密码。
为什么只接受 IP 地址?
聚合服务会代理设备管理 API。为避免 DNS 重绑定将已校验主机名切换到本机或元数据地址,设备 URL 必须使用明确的 IPv4/IPv6 字面地址;局域网 IP(如 192.168.x.x)可正常使用。
常用命令
npm test
npm start
架构与阶段边界
第一阶段采用无构建链的可组合架构:
server/index.js是唯一 bootstrap,负责路径、监听和优雅关闭;server/app.js的buildApp()无导入副作用,可通过 Fastifyinject()测试。server/config/提供 immutable snapshot、串行事务和 temp + atomic rename;模板只用于首次加载,提交始终写实际config.json。server/clients/在配置成功提交后 reconcile;server/status/与server/proxy/是独立边界。public/domain、infrastructure、state、router放置 Node 可测试模块,app.js暂作 DOM composition/controller。styles.css依次导入 tokens、base、components、responsive。
验证命令:
npm test
find server public test -name '*.js' -exec node --check {} \;
npm audit --omit=dev --audit-level=high
git diff --check
第二阶段安全加固后,服务仅允许监听 loopback;显式配置 0.0.0.0、LAN 地址或非 loopback 主机将拒绝启动。实例目标必须为允许的 IP 字面地址,拒绝凭据、loopback、unspecified、link-local、metadata、multicast 及主机名,以避免 DNS 重绑定。生产代理使用精确路径/方法白名单,写操作要求短期一次性确认令牌。
注意
- 当前项目是本地管理面板,不修改 SimAdmin 本体。
config.json可能包含真实设备地址和密码,已在.gitignore中排除。- 部分写操作接口(短信发送、网络模式、频段/小区锁定等)会改变设备状态,执行前请确认目标实例。