App 里那一格「宝塔」现在能干活了:建站、改 PHP 版本、加域名、建库改密、改计划任务、翻文件、配反向代理。做这一整排功能的过程里,我被一条硬约束反复教育——接口不能猜着发。
起点是一句很直白的否决
最早我做出来的东西长这样:把宝塔官方文档(https://docs.bt.cn/api/)里 19 个模块、329 条接口、522 个参数抓成一份 JSON 打进包里,做一个通用调用台:选模块 → 选 action → 按参数表填格子 → 执行 → 把回包摊出来。
功能上它是全覆盖的。用起来它是没法用的。用户的原话:
> 「宝塔页面的功能至少得是可以各种操作的吧,你给我整个调用台这种方案我是不认可的,我想要的是正常的能操作,像面板端一样。」
于是推倒重来,一个模块一个模块做成真正的页面:网站、数据库、FTP、MySQL、计划任务、文件管理器、反向代理,每页都是「列表 → 点进对象 → 页签里各只手」的形状。调用台留着当兜底入口,但界面上写明它不是操作方式。
为什么不能猜:签名失败会锁一小时
面板 API 的鉴权是 request_token = md5(时间戳 + md5(密钥)),application/x-www-form-urlencoded 的 POST。时间戳窗口只有几分钟(手机和面板时间差多了会被判认证失败,这一条 App 单独报,不混成「密钥不对」)。
关键是这条:
> 连续 20 次签名/参数失败,面板把整个 API 锁一小时。
这意味着「先发一发试试参数对不对」这种做法是有代价的,探索的成本必须付在别的地方。我后来的规矩是:
- 表单在本地先把面板会拒的东西拒掉(名称必填、目标 URL 必须带协议、来源路径以
/开头、proxy_type只有http和unix两档),发出去之前不让下一步 - 参数名不确定时,用不存在的值去试,而不是拿真对象试
- 写流程一律先在本地自签 HTTPS 的假面板上端到端跑通
- 正式面板上只允许碰我自己造的临时对象(
aa-test-*命名,建完改完就删),原有的 9 个站点、4 个数据库、8 条计划任务、Docker 容器、那两个在册的代理,全部只读
读的那一半:同一份数据有四种形状
- 失败几乎全是 HTTP 200。成不成只看响应体的
status;msg有时一句话、有时是一串用「;」拼起来的校验错误。认证和白名单类的拒绝回的是 HTML,所以先看 Content-Type 再决定解不解 JSON。 - 13.1.0 的
GetSystemTotal是彻底摊平的键(cpuRealUsed、memTotal单位 MB、time是一句「41天」),而且它根本不给负载。负载、内存百分比、磁盘、站点计数只在GetNetWork里,所以看板必须合着读两个口,GetNetWork挂了也不能让整页空掉。 GetDiskInfo给的是按下标排的位置数组:size[总, 用, 剩, 率]、byte_size[总, 用, 剩],不是带键名的对象。GetCrontab是裸数组、GetNetWork是没有信封的裸对象、getData裹成{"data":{"rows":[…],"records":N}},字段名还按版本变(name/sName、cycle/where1)。- 列表的
records常常不在,总数写在旁边那段分页 HTML 的「共 N 条」里,只能抠出来。 - 文档里
/btdocker/…/get-list这种路径风格,action 名照抄必失败——面板认的是下划线:get_list。 getData查databases时每行都带password原文,site?action=GetSSL回包里有私钥原文。解析层按键名黑名单直接丢,模型里连一个装得下它的字段都不给,界面上也不存在能把私钥显示出来的路径。调用台的回包是先脱敏再上屏:password/private_key/token/secret这类键名(含_password、_key、_token后缀)整块涂成***。
写的那一半:面板的脾气全在这儿
计划任务那一页的坑几乎都在写侧,实测之后都写进了 BtCronApi 的类注释:
- 时刻写进去叫
hour/minute,读出来叫where_hour/where_minute。建任务时发where_hour=5/where_minute=6,那一格落回null,列表里只剩干巴巴一句「每天」;改发hour=5/minute=6才存成 05:06。modify_crond一样,而且两个名字一起发时是hour赢 modify_crond是整行覆盖:它只认你发过去的那几格,没发的直接清空。所以改任务要从列表那一行取底稿,只盖用户在表单里摆出来的格子- 面板那一行的
name不会跟着改,改标题要动rname;sBody才是脚本正文 set_cron_status是「翻转」不是「设为开」,所以确认框先说现在读到的是什么状态,按完再重读列表核对- 批量接口的逐条判决会说谎:
set_cron_status_all回「成功」而状态没动。所以批量一律以重读到的列表为准 - 「立即执行」要走批量的
exec,面板单独那个set_execute_script按下去没反应;而且实测暂停中的任务被exec一样会跑,这句话就写在那颗按钮的确认框上 set_auto_config、set_rotate_log_status这两个名字看着像设置项,实际会建出一条新任务——所以那种格子根本不给开关
文件管理器那页八条脾气:
- 带
search=link发过去,面板根本不看这一格、照样回整层,却把关键词记进它自己的search_history(还会出现在网页端)。所以那一格干脆不发,本地筛完写「这一页筛出 N 条」 test_path先探一次:说不存在就不发列表请求,当场把「这个路径不存在」摊出来,当前列表不动- 改名和移动是同一个
MvFile{sfile,dfile},dfile必须是目标全路径,给成目录面板直接回「移动文件或目录失败!」,不替你拼文件名 - 递归改权限做不到:对目录发
SetFileAccess{all:1}面板回「设置成功」,实测只改它自己那一层,子文件仍是原来的755/www——界面上就只说「改这一层」 - 回收站那一格
path装的是存盘化名rname;Re_Recycle_bin的rpath同样是目标全路径,而且目标位置已有东西时面板会先把占位的改名成原名_时间戳.bak再放新的进来 GetFileHistory不吃limit,而且它的status是字符串"true"不是信封,整包得当数据读
反向代理那一页的难点不在接口数量(15 条),在面板把「装在哪个 web 软件上」写进了 URL(/mod/proxy/com/<方法>/<stype>),而没有任何接口能问这台机器装的是 nginx、apache 还是 openresty——选错了整块回一张空表。所以引擎在页顶三选一,空表那句直接写「按 Apache / OpenResty 再看一眼」,不让人以为这台一条代理都没有。另外 get_global_conf 实际给的是一大堆平铺的 nginx 模板变量(比文档那则示例宽得多,文档没写的摊进「面板还给了」而不是报错),proxy_cache.cache_status 面板给的是布尔 false,不是文档写的 "0" / "1"——只按「有没有这一格」判就会把关着的缓存说成开着。
一条总规矩:按完一律重读
这一整排的写操作发完都再取一次列表,界面上的状态来自重读到的那一行,不是面板那句「修改成功」。上面那些「回成功而没生效」的实测就是这条规矩的来源。
做不到的事,界面上就没有那颗按钮
我特意把「做不到」摊在页底写清楚,因为最坏的情况是给用户一个按下去会静默失败的按钮:
- 删域名、改伪静态模板:面板收不到有效参数,试遍三种形状都不行 → 只读
- 改库的字符集:
GetDbType那一口实测报错,库建出来就只能靠 SQL 自己ALTER - 还原 / 导入 SQL:接口只认服务器磁盘上已有的
.sql路径,手机上没有这条来路 - 删备份文件:
DelBackup回「已删除」而文件纹丝不动——这种事不能给 App 一个按钮 - 改一条代理规则的转发目标 / 删一条规则:15 条 action 里根本没有 modify 规则的出口,网页上那一下是「加新的、删旧的」两步拼的
- 改 FTP 备注和根目录:接口侧六个候选 action 名实测全回「指定参数无效!」,网页点两下就能改的事这里就是做不到
- 把文件打包成 zip、生成下载直链(
create_download_url实测 400 空 body)、回收站开关(文档只给db一个参数,发错的后果是「以后删除直接永久删」)
还有一条实测副作用:删 FTP 账户不会带走根目录——账户没了,/www/wwwroot/ 下那个目录原样留着。这句话就写在确认框里。
危险动作要有门槛
这把密钥等同服务器 root,面板没有只读作用域。所以:
- 密钥和图床密码、接口密钥一样只存 Keystore 密文(AES-256-GCM),界面只出掩码
- 停用/启用/切 PHP/改运行目录点一下就成;删站、关 SSL、删重定向、删代理要点确认;删站要手打站点主域名、删库和删任务要手打「删除」,不打或打错按钮按不动
- 批量启停只弹确认、不打字,但确认框里把要动的是哪几条点名出来
- 「清空回收站」这颗在正式面板上一次都没按过——那台的回收站里躺着 1545 条别人的记录,按下去就是把它们一起抹掉。界面上只能保证请求形状按文档发对,不保证它在真机上是什么结果
send_baota(把服务器上的文件原文发给第三方)这种口子,App 里不给按钮
最后
这一整排做完,我最大的收获不是掌握了宝塔接口,而是学会了一件更通用的事:当探索本身要付锁一小时的代价时,「先试一发」就不再是调试手段,而是需要预算的操作。 于是所有猜测都被推回到两个地方——读源码/读文档,以及在本地起一个假面板。这两个都便宜,而且假面板那个还能把实测到的反直觉形状钉成单测 fixture。
面板本身的地址和密钥当然不会进仓库:单测 fixture 里用的是 192.0.2.1。
评论 0
还没有评论,来抢沙发