仰望星辰工作室

feat(api): 支持 1.81 与 1.83+ 外部 API 行为切换

Y yang1145 提交于 2026-09-30 19:54 · 6a9d087 ·父提交 f354519
feat(api): 支持 1.81 与 1.83+ 外部 API 行为切换

1.83+ 在外部主机 API 上收紧的行为约束会直接拒掉仍在用 1.81 对接模块的老客户,
传输包络两版一致,差异只在行为层,故加全局开关 MN_config.api_compat 放宽:
跨节点归属校验、kt 站点名按 max(id)+1、zjmode 缺参清零、xf 真值解停、鉴权失败老文案。
老版恒真的「主机已存在」表达式与 tz 资产级联删除属 bug/新能力,刻意不还原。

1.81 老库在线升到 1.85 缺这列时只是切不了模式,不会报错,故增量 SQL 用裸 ALTER
以适配在线更新执行器按分号逐句的语义。
8 个文件变更 +130 -20 13172193298@163.com
•MPHX/function.php +7 -0
•admin/api/setting.php +8 -0
•api/api.php +46 -17
•docs/api/external.md +23 -0
•install/install.api.php +9 -0
•install/install.sql +3 -2
•templates/default/admin/set.php +29 -1
•update/update_v185_api_compat.sql +5 -0
变更内容
diff --git a/MPHX/function.php b/MPHX/function.php
index 46f0a44..c7bd1e2 100644
--- a/MPHX/function.php
+++ b/MPHX/function.php
@@ -286,6 +286,13 @@ function mnbt_log($user = '系统', $type = '系统日志', $content = '内容',
 	return logjl($user, $type, $content, $status, $db);
 }
 
+function mnbt_api_compat_mode()
+{
+	global $conf;
+	// api_compat='1' 时对外主机 API 还原 1.81 老协议行为,供未升级对接模块的客户过渡使用
+	return isset($conf['api_compat']) && (string)$conf['api_compat'] === '1';
+}
+
 function send_post($url, $post_data) {
   if(!is_array($post_data)) $post_data=[];
   $postdata = http_build_query($post_data);
diff --git a/admin/api/setting.php b/admin/api/setting.php
index 88c3188..269e6f6 100644
--- a/admin/api/setting.php
+++ b/admin/api/setting.php
@@ -19,6 +19,14 @@ if($egn=='setapi') {
 	if($DB->query_prepare($sql,[$apikey,$apiqk,$lin,$win,$siteid]))json_exit('修改成功'); else json_exit('修改失败'.$DB->error());
 	return;
 }
+if($egn=='setapicompat') {
+	// 对外 API 协议模式开关:0=新版协议(1.83+ 严格),1=兼容 1.81 对接模块
+	$apicompat = (daddslashes($_POST['api_compat'] ?? '0') === '1') ? '1' : '0';
+	logjl($user,'API设置','对外API协议模式切换为'.($apicompat==='1'?'兼容1.81':'新版1.83+'),'修改成功',$DB);
+	$sql="update `MN_config` set `api_compat` =? where `id`=?";
+	if($DB->query_prepare($sql,[$apicompat,$siteid]))json_exit('修改成功'); else json_exit('修改失败'.$DB->error());
+	return;
+}
 if($egn=='setkzmb') {
 	$name=daddslashes($_POST['name']);
 	$ftp=daddslashes($_POST['ftp']);
diff --git a/api/api.php b/api/api.php
index 1722018..12548c4 100644
--- a/api/api.php
+++ b/api/api.php
@@ -37,6 +37,11 @@ $et_zj=$DB->get_row_prepare("SELECT * FROM MN_zj WHERE user=? limit 1", [$user])
 if($cert=='' || $cert['qk']=='false')api_json_exit(100, '错误!该宝塔不存在或该宝塔已经被关闭');
 $adyjm=$cert['ktmy'].$cert['qmk'];$mdjm=md5($adyjm);
 if($keye!=$mdjm){
+    if(mnbt_api_compat_mode()){
+        // 1.81 兼容:老对接模块按 msg 文案分支,沿用原文案与不带 key 前缀的日志
+        mnbt_log('外部API','API鉴权','API-'.$bh.' '.$user.' 宝塔调用密钥错误','鉴权失败',$DB);
+        api_json_exit(100, '错误!您所传输的宝塔调用密钥与该宝塔的调用密钥不匹配!');
+    }
     // 不回显正确密钥,避免密钥泄露
     mnbt_log('外部API','API鉴权','API-'.$bh.' '.$user.' 宝塔调用密钥错误(发送:'.substr($keye,0,8).',btdh='.$bh.')','鉴权失败',$DB);
     api_json_exit(100, '调用密钥不匹配!');
@@ -47,8 +52,9 @@ $btkeye=$cert['btmy'];
 // 归属校验:除 kt(新建主机)和 cfif(连接验证)外,所有动作必须校验主机属于该节点,防止跨节点越权操作
 if($gn!='kt' && $gn!='cfif'){
     if(empty($et_zj))api_json_exit(100, '不存在主机用户名');
+    // 1.81 兼容模式仅放宽跨节点校验(老对接模块传的 username 可能与节点代号对不上);空主机检查两种模式都保留
     // ssbt/btdh 为 varchar 节点代号,双端强转字符串严格比较,避免 '01' 与 '1' 被宽松比较误判
-    if((string)$et_zj['ssbt'] !== (string)$bh)api_json_exit(100, '主机不属于该节点');
+    if(!mnbt_api_compat_mode() && (string)$et_zj['ssbt'] !== (string)$bh)api_json_exit(100, '主机不属于该节点');
 }
 
 if($gn=='cfif'){
@@ -60,7 +66,7 @@ if($gn=='cfif'){
     $webdx=json_encode(array('max'=>daddslashes($_POST['webdx'] ?? 0),'dq'=>0));
     $sqldx=json_encode(array('max'=>daddslashes($_POST['sqldx'] ?? 0),'dq'=>0));
     $ymbds=daddslashes($_POST['ymbds'] ?? 0);
-    // 直接真值判断:已存在则报错
+    // 直接真值判断:已存在则报错。1.81 原表达式 ($et_zj!='' || $et_zj!=false) 恒真是 bug 而非兼容需求,兼容模式刻意不还原
     if($et_zj){
         api_lifecycle_log('API开通主机','开通'.$user.'失败:主机已存在','开通失败');
         api_json_exit(100, '错误!该主机已经存在,请重新开通!');
@@ -96,11 +102,18 @@ if($gn=='cfif'){
         api_lifecycle_log('API开通主机','开通'.$user.'失败:账号重复','开通失败');
         api_json_exit(100, '错误!该账号已存在!请更换账号!');
     }
-    // max(id)+1 并发下可能重复导致插入失败,失败时重新取号重试几次
     $hskr=mt_rand(4,10);
     $rqsj=md5($date.$user);
     $wjler=substr($rqsj, $hskr , 3);
-    $btserw='mnbt.'.mt_rand(1,999).$wjler;
+    if(mnbt_api_compat_mode()){
+        // 1.81 兼容:站点名按 max(id)+1 序列生成,老对接模块据此猜测站点名
+        $rowe=$DB->get_row_prepare("SELECT id FROM MN_zj WHERE 1 order by id desc limit 1");
+        $id=($rowe['id'] ?? 0)+1;
+        $btserw='mnbt.'.$id.mt_rand(1,999).$wjler;
+    }else{
+        // 站点名不含 id,配合下方取号重试,避免并发下 max(id)+1 重复
+        $btserw='mnbt.'.mt_rand(1,999).$wjler;
+    }
     $mrwww=$cert['btos']=='1' ? $conf['hxi'].'/'.$btserw : $conf['hxo'].'/'.$btserw;
     $r_data = $api->webkt($user,$pass,$btserw,'主机','true','true',$phpVersion,$mrwww);
     $cjqk=$r_data['siteStatus'] ?? false;
@@ -120,13 +133,20 @@ if($gn=='cfif'){
         foreach(($r_datn['data'] ?? []) as $val){ if($val['name']===$user){ $aedfs=$val['id']; break; } }
         foreach(($r_datp['data'] ?? []) as $val){ if($val['name']===$user){ $sqlfs=$val['id']; break; } }
         // 写入主机记录:max(id)+1 在并发下可能重复导致插入失败,失败时重新取号重试几次
+        $insert_sql = "INSERT INTO `MN_zj` (`id`, `ssbt`, `user`, `pass`, `sqluser`, `sqlpass`, `data`, `datae`, `qk`, `btid`, `sqldz`, `ftpid`, `ymbds`, `hxa`, `hxb`, `hxc`, `hxd`, `llmax`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
+        $insert_args = [$bh, $user, $pass, $user, $pass, $date, $datae, 'true', $zdide, $btserw, $aedfs, $ymbds, $webdx, $sqldx, '2', $sqlfs, $flowratemax];
         $insert_ok = false;
-        for($tryi = 0; $tryi < 3; $tryi++){
-            $rowe=$DB->get_row_prepare("SELECT id FROM MN_zj WHERE 1 order by id desc limit 1");
-            $id=($rowe['id'] ?? 0)+1;
-            if($DB->query_prepare("INSERT INTO `MN_zj` (`id`, `ssbt`, `user`, `pass`, `sqluser`, `sqlpass`, `data`, `datae`, `qk`, `btid`, `sqldz`, `ftpid`, `ymbds`, `hxa`, `hxb`, `hxc`, `hxd`, `llmax`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", [$id, $bh, $user, $pass, $user, $pass, $date, $datae, 'true', $zdide, $btserw, $aedfs, $ymbds, $webdx, $sqldx, '2', $sqlfs, $flowratemax])){
-                $insert_ok = true;
-                break;
+        if(mnbt_api_compat_mode()){
+            // 1.81 兼容:沿用建站前取的号单次插入,保证站点名与 id 序列一致
+            $insert_ok = (bool)$DB->query_prepare($insert_sql, array_merge([$id], $insert_args));
+        }else{
+            for($tryi = 0; $tryi < 3; $tryi++){
+                $rowe=$DB->get_row_prepare("SELECT id FROM MN_zj WHERE 1 order by id desc limit 1");
+                $id=($rowe['id'] ?? 0)+1;
+                if($DB->query_prepare($insert_sql, array_merge([$id], $insert_args))){
+                    $insert_ok = true;
+                    break;
+                }
             }
         }
         if($insert_ok){
@@ -162,7 +182,9 @@ if($gn=='cfif'){
     $api = new bt_api($btipe,$btkeye);
     $r_data = $api->setdqsj($et_zj['btid'],$x_dq_date);
     // qk 为字符串 'true'/'false',需显式比较;续费后到期日在未来且主机处于暂停状态(qk=='false',含到期暂停)时自动解停
-    if(strtotime($date)-strtotime($x_dq_date)<0 && $x_dq_date!='0000-00-00' && $et_zj['qk']=='false'){
+    // 1.81 兼容:老版对 $et_zj['qk'] 做真值判断('false' 字符串亦为真,故总会解停),按老版原样还原
+    $qk_pf = mnbt_api_compat_mode() ? $et_zj['qk'] : $et_zj['qk']=='false';
+    if(strtotime($date)-strtotime($x_dq_date)<0 && $x_dq_date!='0000-00-00' && $qk_pf){
         $api->siteqt($et_zj['btid'],$et_zj['sqldz'],true);
         $api->setftpzt($et_zj['ftpid'],$et_zj['user'],'1');
     }
@@ -219,15 +241,22 @@ if($gn=='cfif'){
 }elseif($gn == 'zjmode'){
     $zjdata=$DB->get_row_prepare("SELECT * FROM MN_zj WHERE user=?", [$user]);
     if($zjdata == null) api_json_exit(100, '不存在主机用户名');
-    // zjmode 使用单独查询,同样需要校验主机归属节点
-    if((string)$zjdata['ssbt'] !== (string)$bh)api_json_exit(100, '主机不属于该节点');
+    // zjmode 使用单独查询,同样需要校验主机归属节点;1.81 兼容模式仅放宽跨节点校验
+    if(!mnbt_api_compat_mode() && (string)$zjdata['ssbt'] !== (string)$bh)api_json_exit(100, '主机不属于该节点');
     $hxa_array = json_decode($zjdata['hxa'],true);
     $hxb_array = json_decode($zjdata['hxb'],true);
     $llmax_array = json_decode($zjdata['llmax'],true);
-    // 有传参才覆盖对应配额,三项相互独立,避免缺参导致配额被清零
-    if(isset($_POST['websize']) && $_POST['websize']!=='') $hxa_array['max'] = $_POST['websize'];
-    if(isset($_POST['sqlsize']) && $_POST['sqlsize']!=='') $hxb_array['max'] = $_POST['sqlsize'];
-    if(isset($_POST['ll']) && $_POST['ll']!=='') $llmax_array['max'] = $_POST['ll'];
+    if(mnbt_api_compat_mode()){
+        // 1.81 兼容:无条件覆盖三项配额,老对接模块依赖缺参即清零的语义
+        $hxa_array['max'] = $_POST['websize'] ?? null;
+        $hxb_array['max'] = $_POST['sqlsize'] ?? null;
+        $llmax_array['max'] = $_POST['ll'] ?? null;
+    }else{
+        // 有传参才覆盖对应配额,三项相互独立,避免缺参导致配额被清零
+        if(isset($_POST['websize']) && $_POST['websize']!=='') $hxa_array['max'] = $_POST['websize'];
+        if(isset($_POST['sqlsize']) && $_POST['sqlsize']!=='') $hxb_array['max'] = $_POST['sqlsize'];
+        if(isset($_POST['ll']) && $_POST['ll']!=='') $llmax_array['max'] = $_POST['ll'];
+    }
     if($DB->query_prepare("UPDATE `MN_zj` SET `hxa` = ?, `hxb` = ?, `llmax` = ? WHERE `user` = ?", [json_encode($hxa_array), json_encode($hxb_array), json_encode($llmax_array), $user])) api_json_exit(200, '主机修改成功');
     api_json_exit(100, '主机修改失败我也不知道什么问题,请联系开发者');
 }elseif($gn == 'start'){
diff --git a/docs/api/external.md b/docs/api/external.md
index df28356..a9e32f4 100644
--- a/docs/api/external.md
+++ b/docs/api/external.md
@@ -69,6 +69,29 @@ mn_bh=1&mn_key=YOUR_API_KEY&mn_keye=MD5_OF_BT_KEY&mn_vs=15&username=testuser&pas
 - 自动检测节点 PHP 版本(优先使用 `MN_bt.mrbts_php`,否则取最新已安装版本)
 - 自动触发 `host.created` 钩子(插件可监听)
 
+### 协议兼容开关(`MN_config.api_compat`)
+
+V1.83+ 在不变的传输包络(POST 表单 + `{success,code,msg}` 响应)之上加入了若干**行为约束**,仍在使用 1.81 版对接模块(`wp_seller_plugin`、魔方财务 `mnbthost` server module)的客户升级 MNBT 后可能被拒。为此提供开关:
+
+- 取值:`0` = 新版协议(1.83+ 严格,默认);`1` = 兼容 1.81 对接模块
+- 后台切换:系统设置 → API 设置 → 「对外 API 协议模式」
+- 代码判断:`api/api.php` 内统一走 `mnbt_api_compat_mode()`(`MPHX/function.php`)读取该配置
+
+两种模式的行为差异(仅 `api/api.php`):
+
+| 差异点 | 新版协议(api_compat=0) | 兼容 1.81(api_compat=1) |
+|--------|--------------------------|---------------------------|
+| 跨节点归属校验(除 `kt`/`cfif` 外,含 `zjmode`) | `MN_zj.ssbt` 与 `mn_bh` 严格比较,不一致返回 `主机不属于该节点` | 跳过该校验(老模块传的 `username` 可能与节点代号对不上) |
+| 空主机检查 | `不存在主机用户名` | 保留不变(1.81 靠弱类型访问空行是 bug,不还原) |
+| `kt` 站点名 | `mnbt.<随机>.<后缀>`,写库失败重取号重试 | `mnbt.<max(id)+1><随机>.<后缀>`,单次取号插入(老模块按 id 序列猜站点名) |
+| `zjmode` 配额 | 仅在传入 `websize`/`sqlsize`/`ll` 非空时覆盖 | 无条件覆盖,缺参即清零(老模块依赖该语义) |
+| `xf` 续费自动解停 | 仅当 `qk=='false'`(真暂停)才解停 | 对 `qk` 做真值判断(1.81 原行为:总会解停) |
+| 宝塔调用密钥校验失败 | `调用密钥不匹配!`,日志带 key 前缀 | `错误!您所传输的宝塔调用密钥与该宝塔的调用密钥不匹配!`,日志不带前缀(老模块按 msg 文案分支) |
+| `kt` 主机存在判断 | `if($et_zj)` | 同左,刻意不还原 1.81 的恒真表达式(那是 bug 不是兼容需求) |
+| `tz` 删除主机 | 级联删除 `MN_plugin_hosting_asset` | 同左,保留不动 |
+
+> **提示:** 兼容模式只是过渡期方案,会放宽跨节点归属等安全约束。建议尽快将对接模块升级到适配 1.83+ 协议的版本,然后切回「新版协议」。
+
 ---
 
 ## 4.2 MNBT 节点 API(api/node.php)
diff --git a/install/install.api.php b/install/install.api.php
index 053cc94..23903db 100644
--- a/install/install.api.php
+++ b/install/install.api.php
@@ -108,6 +108,8 @@ function mnbt_upgrade_columns($actual_config, $actual_zj, $actual_bt, $actual_dp
             'home_footer' => "ALTER TABLE `{$actual_config}` ADD `home_footer` text NOT NULL",
             'home_show_notice' => "ALTER TABLE `{$actual_config}` ADD `home_show_notice` varchar(10) NOT NULL DEFAULT 'true'",
             'home_show_plans' => "ALTER TABLE `{$actual_config}` ADD `home_show_plans` varchar(10) NOT NULL DEFAULT 'true'",
+            // V1.85 对外 API 协议模式开关(0=1.83+ 严格,1=1.81 兼容)
+            'api_compat' => "ALTER TABLE `{$actual_config}` ADD `api_compat` varchar(10) NOT NULL DEFAULT '0'",
         ),
         $actual_zj => array(
             'backup' => "ALTER TABLE `{$actual_zj}` ADD `backup` VARCHAR(50) NOT NULL DEFAULT '{\"max\":\"3\",\"dq\":0}'",
@@ -313,6 +315,13 @@ switch ($action) {
                 $result['need_upgrade'] = true;
                 $result['missing_columns'][] = 'MN_config.home_enable';
             }
+
+            // 检查 V1.85 新增字段(对外 API 协议模式开关)
+            $col_api_compat = DB::get_row("SHOW COLUMNS FROM `MN_config` LIKE 'api_compat'");
+            if (!$col_api_compat) {
+                $result['need_upgrade'] = true;
+                $result['missing_columns'][] = 'MN_config.api_compat';
+            }
         }
 
         echo Res(1, '升级检测完成', $result);
diff --git a/install/install.sql b/install/install.sql
index 3538e6c..6a6f33f 100644
--- a/install/install.sql
+++ b/install/install.sql
@@ -38,10 +38,11 @@ CREATE TABLE `MN_config` (
   `home_footer` text NOT NULL,                         -- V1.84: 主页底部版权(空则回退系统版权)
   `home_show_notice` varchar(10) NOT NULL DEFAULT 'true', -- V1.84: 显示公告区
   `home_show_plans` varchar(10) NOT NULL DEFAULT 'true',  -- V1.84: 显示套餐区
+  `api_compat` varchar(10) NOT NULL DEFAULT '0',          -- V1.85: 对外 API 协议模式(0=1.83+ 严格,1=1.81 兼容)
   PRIMARY KEY (`id`)
 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
-INSERT INTO `MN_config`(`id`, `user`, `pwd`, `gg`, `name`, `yzm`,`yzme`,`wzqk`,`auther`,`kzmbqk`, `apiqk`,`api`,`qqh`,`date`,`hxw`, `hxe`, `hxr`, `hxt`, `hxy`, `hxu`,`hxi`,`hxo`,`hxp`,`hxa`, `hxs`,`hxd`, `pay_methods`, `home_enable`, `home_theme`, `home_theme_settings`, `home_title`, `home_hero`, `home_primary`, `home_logo`, `home_favicon`, `home_footer`, `home_show_notice`, `home_show_plans`) VALUES
-('1', 'admin', '123456', '', '', 'true', 'false', '', '', 'true', '', '', '', '', 'mnftp', '', '', '', '', '56', '/www/wwwroot', 'D:/wwwroot', "<a href='./'>Copyright ©梦奈云 2023</a>", '', '', '', '', 'true', '', '', '', '', '#4f46e5', '', '', '', 'true', 'true');
+INSERT INTO `MN_config`(`id`, `user`, `pwd`, `gg`, `name`, `yzm`,`yzme`,`wzqk`,`auther`,`kzmbqk`, `apiqk`,`api`,`qqh`,`date`,`hxw`, `hxe`, `hxr`, `hxt`, `hxy`, `hxu`,`hxi`,`hxo`,`hxp`,`hxa`, `hxs`,`hxd`, `pay_methods`, `home_enable`, `home_theme`, `home_theme_settings`, `home_title`, `home_hero`, `home_primary`, `home_logo`, `home_favicon`, `home_footer`, `home_show_notice`, `home_show_plans`, `api_compat`) VALUES
+('1', 'admin', '123456', '', '', 'true', 'false', '', '', 'true', '', '', '', '', 'mnftp', '', '', '', '', '56', '/www/wwwroot', 'D:/wwwroot', "<a href='./'>Copyright ©梦奈云 2023</a>", '', '', '', '', 'true', '', '', '', '', '#4f46e5', '', '', '', 'true', 'true', '0');
 
 
 
diff --git a/templates/default/admin/set.php b/templates/default/admin/set.php
index 6456f18..95dc3c7 100644
--- a/templates/default/admin/set.php
+++ b/templates/default/admin/set.php
@@ -93,14 +93,42 @@ if ($set == 'wz') {
         </div>
       </div>
     </div>
+    <div class="mn-set-field">
+      <label for="apicompat">对外 API 协议模式</label>
+      <select class="form-control" id="apicompat" name="apicompat" size="1">
+        <option value="0" <?php if ((string)($conf['api_compat'] ?? '0') !== '1') echo 'selected'; ?>>新版协议(1.83+,默认)</option>
+        <option value="1" <?php if ((string)($conf['api_compat'] ?? '0') === '1') echo 'selected'; ?>>兼容 1.81 对接模块</option>
+      </select>
+      <small>兼容模式仅还原 1.81 老协议的行为约束(跨节点校验、站点名规则等),供未升级对接模块的老客户过渡使用。</small>
+    </div>
     <div class="mn-set-actions">
       <button class="btn btn-primary btn-block" type="button" onclick="setapi()"><i class="mdi mdi-content-save-outline"></i> 保存修改</button>
+      <button class="btn btn-outline-secondary btn-block" type="button" onclick="setapicompat()"><i class="mdi mdi-swap-vertical"></i> 保存协议模式</button>
     </div>
     <div class="mn-set-note">
-      <b>注意:</b>建站目录请勿随意修改,已开通主机可能受影响。API 密钥修改后,监控 URL 与外部对接均需同步更新。默认 PHP 版本需在宝塔软件商店中已安装。
+      <b>注意:</b>建站目录请勿随意修改,已开通主机可能受影响。API 密钥修改后,监控 URL 与外部对接均需同步更新。默认 PHP 版本需在宝塔软件商店中已安装。兼容模式只是过渡期方案,建议尽快升级对接模块(WordPress 销售插件 / 魔方财务 server module)后切回新版协议。
     </div>
   </div>
 </div>
+<script>
+// 协议模式独立提交:xtset.js 的 setapi() 不感知该字段,为避免公共 JS 扩散改动单独走 setapicompat
+function setapicompat() {
+  msloading('正在修改中!请稍后...','text-info','text-info');
+  let data = {};
+  data['gn'] = 'setapicompat';
+  data['api_compat'] = document.getElementById('apicompat').value;
+  $.post('./ajax.php', data, function (date) {
+    var jsoe = JSON.parse(date);
+    var qk = jsoe.code;
+    if (qk == '修改成功') {
+      msalert(1, '协议模式已保存!', 2000);
+    } else {
+      msalert(4, qk, 2000);
+    }
+    msloadingde();
+  });
+}
+</script>
 
 <?php } elseif ($set == 'kzmb') { ?>
 <div class="mn-set-card">
diff --git a/update/update_v185_api_compat.sql b/update/update_v185_api_compat.sql
new file mode 100644
index 0000000..116e646
--- /dev/null
+++ b/update/update_v185_api_compat.sql
@@ -0,0 +1,5 @@
+-- MNBT V1.85 对外 API 协议兼容开关增量 SQL
+-- 为 MN_config 追加 api_compat 字段(0=1.83+ 严格协议,1=兼容 1.81 对接模块)
+-- 注意:在线更新的执行器按分号逐句跑且忽略单句报错,所以这里用裸 ALTER,
+-- 不要写成 DELIMITER/存储过程(那样会被拆碎),列已存在时报错会被安全跳过。
+ALTER TABLE `MN_config` ADD `api_compat` varchar(10) NOT NULL DEFAULT '0';