<?php
error_reporting(0);
define('SYSTEM_ROOT', dirname(__FILE__).'/');
define('ROOT', dirname(SYSTEM_ROOT).'/');
define('TEMPLATE_ROOT', ROOT.'template/');
date_default_timezone_set('Asia/Shanghai');
$date = date("Y-m-d H:i:s");

include_once(ROOT."includes/autoloader.php");
Autoloader::register();
require ROOT.'config.php';

$DB = new \lib\PdoHelper($dbconfig);
$conf = [];
$conf_ = $DB->query("SELECT * FROM pre_config");
if($conf_){
    while($res = $conf_->fetch()){
        $conf[$res['k']] = $res['v'];
    }
}

$conf_frame = $conf;
$frame_ = $DB->query("SELECT * FROM pre_frame_conf");
if($frame_){
    while($res = $frame_->fetch()){
        if(!array_key_exists($res['k'], $conf)){
            $conf_frame[$res['k']] = $res['v'];
        }
    }
}

include_once(ROOT."includes/functions.php");
include_once(ROOT."includes/robot_fun.php");

if (is_file(ROOT . 'includes/node_link_helper.php')) {
    include_once ROOT . 'includes/node_link_helper.php';
}

if(function_exists("set_time_limit")){
    @set_time_limit(0);
}
if(function_exists("ignore_user_abort")){
    @ignore_user_abort(true);
}
@header('Content-Type: text/html; charset=UTF-8');

$_key = isset($_GET['key'])?daddslashes($_GET['key']):exit('参数不正确');
if($_key == '')exit("参数不能为空");
if($_key != $conf['cronkey'] && $_key != $conf['apikey'])exit("监控密钥不正确----".$_key."----".$conf['cronkey']);

$time = time();

// 单入口锁，避免多处触发导致重复拉取/重复通知
$lock_seconds = intval($conf['node_link_cron_lock_seconds'] ?? 300);
if($lock_seconds <= 0) $lock_seconds = 300;
$lock_until = intval($conf['time_node_link_cron'] ?? 0);
$remain = $lock_until - $time;
$stale_threshold = max($lock_seconds * 3, 120);
if($remain > 0){
    if($remain <= $stale_threshold){
        exit('不能多处监控，剩余'.$remain.'秒');
    }else{
        echo "⚠️ 检测到残留锁(剩余{$remain}秒)，已自动释放\n";
    }
}
$DB->exec("REPLACE INTO `pre_config` SET v='".($time + $lock_seconds)."',k='time_node_link_cron'");
register_shutdown_function(function() use ($DB) {
    $DB->exec("REPLACE INTO `pre_config` SET v='".time()."',k='time_node_link_cron'");
});

// ================================
// 对接节点：授权到期提醒（pre_node_link.expire_time）
// ================================
$notify_end_switch = isset($conf['notice_end']) ? intval($conf['notice_end']) : 1;
$notify_end_day = isset($conf['notify_end_day']) ? intval($conf['notify_end_day']) : 5;
$notify_interval_hours = isset($conf['end_time']) && $conf['end_time'] !== '' ? intval($conf['end_time']) : 8;
if($notify_interval_hours < 1){
    $notify_interval_hours = 8;
}
$notify_interval_sec = $notify_interval_hours * 3600;

$notice_frame = intval($conf['notice_frame'] ?? 0); // 复用现有通知渠道配置：1邮件/2短信/3微信
$node_auth_notify_switch = intval($conf['node_auth_notice_switch'] ?? $notify_end_switch);
$node_auth_notify_day = intval($conf['node_auth_notify_day'] ?? $notify_end_day);
if ($node_auth_notify_day <= 0) $node_auth_notify_day = $notify_end_day;

$node_expire_send = 0;
try{
    $rsNode = $DB->query("SELECT id,custom_name,remote_class_name,biz,expire_time FROM `pre_node_link` WHERE status='1' LIMIT 5000");
    if($rsNode){
        while($node = $rsNode->fetch(PDO::FETCH_ASSOC)){
            $node_id = intval($node['id'] ?? 0);
            $expire_time = $node['expire_time'] ?? '';
            if($node_id <= 0 || empty($expire_time)) continue;

            $ts = strtotime($expire_time);
            if($ts === false) continue;

            $is_expired = $ts <= $time;
            $days_left_ceil = (int)ceil(($ts - $time) / 86400);

            if(!$is_expired){
                if($node_auth_notify_switch !== 1) continue;
                if($node_auth_notify_day <= 0) continue;
                if($days_left_ceil > $node_auth_notify_day) continue;
            }

            $node_name = trim((string)($node['custom_name'] ?? ''));
            if($node_name === '') $node_name = trim((string)($node['remote_class_name'] ?? ''));
            if($node_name === '') $node_name = '节点#' . $node_id;

            $bizTitle = (strtolower(trim((string)($node['biz'] ?? 'cloud'))) === 'dg') ? '代挂' : '云端';

            $lastNotifyKey = 'node_link_last_notify_' . $node_id;
            $last_sent = intval($conf[$lastNotifyKey] ?? 0);
            $skip_notify = ($notify_interval_sec > 0 && $last_sent > 0 && ($time - $last_sent) < $notify_interval_sec);
            if($skip_notify) continue;

            $tpl = $conf['node_auth_end_notify_nr'] ?? '';
            if($tpl === ''){
                $tpl = '对接节点授权到期提醒：节点：[节点名称] 业务：[业务] 到期时间：[到期时间] 剩余：[剩余天数] 天';
            }
            $con = str_replace(
                array('[节点名称]','[业务]','[到期时间]','[剩余天数]'),
                array($node_name,$bizTitle,$expire_time,$is_expired ? 0 : $days_left_ceil),
                $tpl
            );

            $sent_ok = false;
            if($notice_frame === 1){
                if(!empty($conf['mail_recv'])){
                    $sent_ok = \lib\MessageSend::send($conf['mail_recv'], '对接节点授权到期提醒', '对接节点授权到期提醒', $con, $conf['sitename'] ?? '系统通知');
                }
            }elseif($notice_frame === 2){
                if(!empty($conf['sms_phone'])){
                    $ret = send_sms($conf['sms_phone'], $con);
                    $sent_ok = ($ret === true || (is_array($ret) && intval($ret['code'] ?? 0) == 1));
                }
            }elseif($notice_frame === 3){
                if(!empty($conf['wechat_appuid'])){
                    $ret = send_wx($con, '对接节点授权到期提醒', 1, [], [$conf['wechat_appuid']]);
                    $sent_ok = ($ret === true || (is_array($ret) && intval($ret['code'] ?? 0) == 1));
                }
            }

            if($sent_ok){
                $DB->exec("REPLACE INTO `pre_config` SET v='".time()."',k='".$lastNotifyKey."'");
                $conf[$lastNotifyKey] = time();
                $node_expire_send++;
            }
        }
    }
}catch(\Throwable $e){
    echo "对接节点授权到期提醒异常: ".$e->getMessage()."\n";
}
echo "对接节点 - 授权需提醒/已提醒: {$node_expire_send} 个\n";

// ================================
// 对接节点：在线/离线 + 配置拉取失败告警
// ================================
$node_total = 0;
$node_online = 0;
$node_offline = 0;
$node_expired = 0;
$node_online_census_sum = 0;

$node_link_sync_timeout_sec = intval($conf['node_link_sync_timeout_sec'] ?? 3600);
if($node_link_sync_timeout_sec <= 0) $node_link_sync_timeout_sec = 3600;
$node_link_pull_interval_sec = intval($conf['node_link_pull_interval_sec'] ?? 1800);
if($node_link_pull_interval_sec < 0) $node_link_pull_interval_sec = 0;

// 用已有“接口异常通知”配置做节流
$login_notify_time = intval($conf['notice_frame_time'] ?? 0);
if($login_notify_time <= 0) $login_notify_time = 5;
$login_notify_rate = intval($conf['notice_frame_rate'] ?? 0);
if($login_notify_rate <= 0) $login_notify_rate = 10;
$login_notify_rate_start = 3;

if(function_exists('nodeLinkGetAuthInfo') && function_exists('nodeLinkGetConfig')){
    try{
        $rsNode = $DB->query("SELECT id,custom_name,remote_class_name,biz,expire_time,last_sync_time,sync_online_census,api_error_check,app_id,app_secret,api_url,remote_node_id,sync_type,sync_cid,sync_protocol,status FROM `pre_node_link` WHERE status='1' LIMIT 5000");
        if($rsNode){
            while($node = $rsNode->fetch(PDO::FETCH_ASSOC)){
                $node_id = intval($node['id'] ?? 0);
                if($node_id <= 0) continue;
                $node_total++;

                $expire_ts = !empty($node['expire_time']) ? strtotime($node['expire_time']) : null;
                $last_sync_ts = !empty($node['last_sync_time']) ? strtotime($node['last_sync_time']) : null;

                $is_expired = ($expire_ts !== null && $expire_ts !== false) ? ($expire_ts < $time) : false;
                $is_stale = ($last_sync_ts === null || $last_sync_ts === false) ? true : (($time - $last_sync_ts) > $node_link_sync_timeout_sec);

                $online_state = (!$is_expired && !$is_stale);
                if($online_state){
                    $node_online++;
                    $node_online_census_sum += intval($node['sync_online_census'] ?? 0);
                    $streak_key = 'node_link_err_streak_' . $node_id;
                    if(intval($conf[$streak_key] ?? 0) > 0){
                        $DB->exec("REPLACE INTO `pre_config` SET v='0',k='".$streak_key."'");
                        $conf[$streak_key] = 0;
                    }
                    continue;
                }

                $node_offline++;
                if($is_expired) $node_expired++;

                $node_name = trim((string)($node['custom_name'] ?? ''));
                if($node_name === '') $node_name = trim((string)($node['remote_class_name'] ?? ''));
                if($node_name === '') $node_name = '节点#' . $node_id;
                $bizTitle = (strtolower(trim((string)($node['biz'] ?? 'cloud'))) === 'dg') ? '代挂' : '云端';

                $api_error_check = intval($node['api_error_check'] ?? 1);
                $pullKey = 'node_link_last_pull_' . $node_id;
                $last_pull = intval($conf[$pullKey] ?? 0);
                $should_pull = ($api_error_check === 1)
                    && ($online_state === false)
                    && ($node_link_pull_interval_sec === 0 || $last_pull === 0 || ($time - $last_pull) >= $node_link_pull_interval_sec);

                $streak_key = 'node_link_err_streak_' . $node_id;
                $streak = intval($conf[$streak_key] ?? 0);

                $pull_ok = false;
                $pull_err_msg = '';
                if($should_pull){
                    $authInfo = nodeLinkGetAuthInfo($node);
                    $cfg = nodeLinkGetConfig($node);

                    $authExpire = null;
                    $authOk = false;
                    if(is_array($authInfo) && isset($authInfo['expire_time'])){
                        $authExpire = $authInfo['expire_time'];
                        $authOk = true;
                    }elseif(is_array($authInfo) && isset($authInfo['error'])){
                        $pull_err_msg = (string)($authInfo['error'] ?? '');
                    }else{
                        $pull_err_msg = '获取授权信息失败';
                    }

                    $cfgOk = false;
                    $syncType = null;
                    $syncCid = null;
                    $syncProtocol = null;
                    if(is_array($cfg) && !isset($cfg['error'])){
                        $cfgOk = true;
                        $syncType = $cfg['type'] ?? null;
                        $syncCid = $cfg['cid'] ?? null;
                        $syncProtocol = $cfg['protocol'] ?? null;
                        if(is_array($syncProtocol)) $syncProtocol = json_encode($syncProtocol, JSON_UNESCAPED_UNICODE);
                    }elseif(is_array($cfg) && isset($cfg['error'])){
                        $pull_err_msg = (string)($cfg['error'] ?? '');
                    }else{
                        $pull_err_msg = $pull_err_msg !== '' ? $pull_err_msg : '获取配置失败';
                    }

                    if($authOk || $cfgOk){
                        $DB->exec(
                            "UPDATE pre_node_link SET
                                expire_time = COALESCE(?, expire_time),
                                sync_type = COALESCE(?, sync_type),
                                sync_cid = COALESCE(?, sync_cid),
                                sync_protocol = COALESCE(?, sync_protocol),
                                last_sync_time = NOW()
                             WHERE id = ?",
                            [
                                $authExpire !== '' ? $authExpire : null,
                                !empty($syncType) ? $syncType : null,
                                !empty($syncCid) ? $syncCid : null,
                                ($syncProtocol !== '' ? $syncProtocol : null),
                                $node_id
                            ]
                        );
                        $pull_ok = true;
                    }

                    $DB->exec("REPLACE INTO `pre_config` SET v='".time()."',k='".$pullKey."'");
                    $conf[$pullKey] = time();
                }

                $streak_new = $streak + 1;
                $DB->exec("REPLACE INTO `pre_config` SET v='".intval($streak_new)."',k='".$streak_key."'");
                $conf[$streak_key] = $streak_new;

                $should_notify = ($streak_new >= $login_notify_rate_start) && ((($streak_new - $login_notify_rate_start) % max(1,$login_notify_rate)) === 0);
                $lastNotifyKey = 'node_link_last_notify_' . $node_id;
                $last_notify = intval($conf[$lastNotifyKey] ?? 0);
                $cooldown_sec = max(60, $login_notify_time * 60);
                $cooldown_ok = ($last_notify <= 0 || ($time - $last_notify) >= $cooldown_sec);

                if($notice_frame > 0 && $should_notify && $cooldown_ok){
                    $expire_text = !empty($node['expire_time']) ? $node['expire_time'] : '未知';
                    $last_sync_text = !empty($node['last_sync_time']) ? $node['last_sync_time'] : '未知';
                    $reason = $is_expired ? '授权已过期' : ($is_stale ? '最近同步超时/可能离线' : '节点异常');
                    if($pull_ok !== true && $pull_err_msg !== ''){
                        $reason .= '（配置拉取失败：'.$pull_err_msg.'）';
                    }

                    $content = '对接节点异常提醒：'.$reason.
                        '\n节点：'.$node_name.'（ID:'.$node_id.'，'.$bizTitle.'）'.
                        '\n到期时间：'.$expire_text.
                        '\n最近同步：'.$last_sync_text.
                        '\n当前在线人数（缓存）：'.intval($node['sync_online_census'] ?? 0);

                    $sent_ok = false;
                    if($notice_frame === 1){
                        if(!empty($conf['mail_recv'])){
                            $sent_ok = \lib\MessageSend::send($conf['mail_recv'], '对接节点异常提醒', '对接节点异常提醒', $content, $conf['sitename'] ?? '系统通知');
                        }
                    }elseif($notice_frame === 2){
                        if(!empty($conf['sms_phone'])){
                            $ret = send_sms($conf['sms_phone'], $content);
                            $sent_ok = ($ret === true || (is_array($ret) && intval($ret['code'] ?? 0) == 1));
                        }
                    }elseif($notice_frame === 3){
                        if(!empty($conf['wechat_appuid'])){
                            $ret = send_wx($content, '对接节点异常提醒', 1, [], [$conf['wechat_appuid']]);
                            $sent_ok = ($ret === true || (is_array($ret) && intval($ret['code'] ?? 0) == 1));
                        }
                    }

                    if($sent_ok){
                        $DB->exec("REPLACE INTO `pre_config` SET v='".time()."',k='".$lastNotifyKey."'");
                        $conf[$lastNotifyKey] = time();
                    }
                }
            }
        }
    }catch(\Throwable $e){
        echo "对接节点监控异常: ".$e->getMessage()."\n";
    }
}else{
    echo "对接节点监控未加载 node_link_helper.php，跳过\n";
}

echo "📡 对接节点监控完成\n";
echo "总对接节点数: {$node_total}\n";
echo "对接节点在线: {$node_online}\n";
echo "对接节点离线/异常: {$node_offline}\n";
echo "对接节点在线人数（缓存统计）: {$node_online_census_sum}\n";
echo "对接节点已过期: {$node_expired}\n";

// ================================
// 对接节点：框架QQ状态监控（按 cityid=link_xxx 更新 login_zt）
// 说明：多线程执行，只做在线/离线同步 + 支持 dropline=1 的二次登录
// ================================
$node_frame_qq_online = 0;
$node_frame_qq_offline = 0;
$node_frame_qq_relogin_ok = 0;
$node_frame_links_processed = 0;
$node_frame_links_have_local_qq = 0;
$node_frame_links_frame_list_fail = 0;

$node_qq_expired_count = 0;
$node_qq_deleted_count = 0;
$node_qq_forced_offline_count = 0;

$qq_key = function($q) {
    if ($q === '' || $q === null || $q === false) return '';
    $s = trim((string)$q);
    if ($s === '' || $s === '0') return '';
    if (ctype_digit($s)) return (string)((int)$s);
    return $s;
};

// 获取所有对接节点
$all_links = [];
$rsLinks2 = $DB->query("SELECT id,biz,custom_name,remote_class_name,expire_time,api_error_check,status FROM `pre_node_link` WHERE status='1' ORDER BY sort ASC,id ASC LIMIT 5000");
if($rsLinks2){
    while($link = $rsLinks2->fetch(PDO::FETCH_ASSOC)){
        $all_links[] = $link;
    }
}

// 检查是否开启多线程
$is_multithread = (isset($conf['qzone_multithread']) && $conf['qzone_multithread'] == '1');
$thread_count = isset($conf['qzone_thread_count']) ? intval($conf['qzone_thread_count']) : 4;
if($thread_count < 2) $thread_count = 2;
if($thread_count > 16) $thread_count = 16;

if(count($all_links) > 0 && $is_multithread && function_exists('curl_multi_init')){
    echo "⚡ 启用多线程模式，当前节点数: ".count($all_links)."，线程数: {$thread_count}\n";
    
    // 批次处理节点
    $chunks = array_chunk($all_links, $thread_count);
    foreach($chunks as $chunk_index => $links_chunk){
        $mh = curl_multi_init();
        $ch_list = [];
        $link_mapping = [];
        
        foreach($links_chunk as $i => $link){
            $nodeLinkId = intval($link['id'] ?? 0);
            if($nodeLinkId <= 0) continue;
            
            $frame = nodeLinkGetLoginConfig($nodeLinkId);
            if(!$frame || (is_array($frame) && isset($frame['error']))){
                // 拉不到远程配置，本地标记离线
                $frameCityId = 'link_' . $nodeLinkId;
                $localRows = [];
                $rsQ1 = $DB->query("SELECT 'pre_qq' AS t, qq FROM pre_qq WHERE cityid='{$frameCityId}'");
                if($rsQ1){ while($r = $rsQ1->fetch(PDO::FETCH_ASSOC)){ $localRows[] = $r; } }
                $rsQ2 = $DB->query("SELECT 'pre_dg_qq' AS t, qq FROM pre_dg_qq WHERE cityid='{$frameCityId}'");
                if($rsQ2){ while($r = $rsQ2->fetch(PDO::FETCH_ASSOC)){ $localRows[] = $r; } }
                
                foreach($localRows as $r){
                    $t = $r['t'];
                    $qq = $r['qq'];
                    $DB->exec("UPDATE `{$t}` SET uptime=NOW(),login_zt='0' WHERE qq='{$qq}'");
                }
                $node_frame_qq_offline += count($localRows);
                $node_frame_links_processed++;
                continue;
            }

            // 构造请求URL
            $url = $frame['url'] ?? '';
            if(empty($url)) continue;
            
            if(strpos($url, '?') === false) $url .= '?'; else $url .= '&';
            $ft = strtolower(trim((string)($frame['type'] ?? '')));
            if($ft === 'myqq' || $ft === 'myqqa'){
                $url .= 'Action=GetQQList';
            }elseif($ft === 'qbot'){
                $url .= 'act=QQlist';
            }else{
                $url .= 'Action=GetQQList';
            }
            
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, 10);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_multi_add_handle($mh, $ch);
            
            $ch_list[$i] = $ch;
            $link_mapping[$i] = ['link' => $link, 'frame' => $frame];
        }
        
        if(empty($ch_list)){
            curl_multi_close($mh);
            continue;
        }

        // 执行多线程请求
        $active = null;
        do {
            $mrc = curl_multi_exec($mh, $active);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);
        
        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($mh) != -1) {
                do {
                    $mrc = curl_multi_exec($mh, $active);
                } while ($mrc == CURLM_CALL_MULTI_PERFORM);
            }
        }
        
        // 处理响应
        foreach($ch_list as $i => $ch){
            $content = curl_multi_getcontent($ch);
            curl_multi_remove_handle($mh, $ch);
            curl_close($ch);
            
            $link_data = $link_mapping[$i];
            $link = $link_data['link'];
            $frame = $link_data['frame'];
            $nodeLinkId = intval($link['id']);
            $frameCityId = 'link_' . $nodeLinkId;
            $node_frame_links_processed++;
            
            $localRows = [];
            $rsQ1 = $DB->query("SELECT 'pre_qq' AS t, qq,zid,login_zt,dropline,endtime_arr FROM pre_qq WHERE cityid='{$frameCityId}'");
            if($rsQ1){ while($r = $rsQ1->fetch(PDO::FETCH_ASSOC)){ $localRows[] = $r; } }
            $rsQ2 = $DB->query("SELECT 'pre_dg_qq' AS t, qq,zid,login_zt,dropline,endtime_arr FROM pre_dg_qq WHERE cityid='{$frameCityId}'");
            if($rsQ2){ while($r = $rsQ2->fetch(PDO::FETCH_ASSOC)){ $localRows[] = $r; } }
            
            if(empty($localRows)) continue;
            $node_frame_links_have_local_qq++;
            
            $resp = null;
            if($content) $resp = json_decode($content, true);
            
            if(!$resp || !isset($resp['retcode']) || intval($resp['retcode']) !== 200){
                $node_frame_links_frame_list_fail++;
                continue;
            }
            
            // 解析框架QQ列表
            $frame_qq_list = [];
            if(isset($resp['bot']['list'])){
                $online_uins = [];
                foreach($resp['bot']['online_list'] ?? [] as $r){
                    $k = $qq_key($r['bot'] ?? $r['uin'] ?? 0);
                    if($k !== '') $online_uins[$k] = 1;
                }
                foreach($resp['bot']['list'] as $row){
                    $qq = $qq_key($row['bot'] ?? $row['uin'] ?? 0);
                    if($qq === '') continue;
                    $online = ($row['online'] === true || $row['online'] === 'true' || ($row['status'] ?? null) == 1 || ($row['status'] ?? null) === '1')
                        ? 1 : (isset($online_uins[$qq]) ? 1 : 0);
                    $frame_qq_list[$qq] = $online;
                }
            }elseif(isset($resp['QQlist'])){
                foreach($resp['QQlist'] as $qq => $status){
                    $k = $qq_key($qq);
                    if($k === '') continue;
                    $frame_qq_list[$k] = $status ? 1 : 0;
                }
            }elseif(isset($resp['data'])){
                foreach($resp['data'] as $qq => $status){
                    $k = $qq_key($qq);
                    if($k === '') continue;
                    $frame_qq_list[$k] = $status ? 1 : 0;
                }
            }
            
            // 执行过期监控和二次登录
            $ft = isset($frame['type']) ? strtolower(trim((string)$frame['type'])) : '';
            $skipRelogin = in_array($ft, ['myqq','myqqa','dr'], true);
            $apipwd_f = $frame['apipwd'] ?? '';
            $is_free_node = ($apipwd_f === '0' || $apipwd_f === 0 || ($apipwd_f && $apipwd_f !== ''));
            
            $online_count = 0;
            $offline_count = 0;
            
            foreach($localRows as $row){
                $t = $row['t'];
                $qq = $row['qq'];
                $qqk = $qq_key($qq);
                if($qqk === '') continue;
                
                // 过期监控
                $end_arr_json = $row['endtime_arr'] ?? '';
                $is_qq_expired = false;
                if(!$is_free_node && function_exists('is_end') && is_end($end_arr_json, $frame)){
                    $is_qq_expired = true;
                    $node_qq_expired_count++;
                }
                
                if($is_qq_expired){
                    $qq_end_day = isset($conf_frame['qq_end_day']) ? intval($conf_frame['qq_end_day']) : 0;
                    $del_qq_type = isset($conf_frame['del_qq_type']) ? intval($conf_frame['del_qq_type']) : 0;
                    
                    $is_fully_expired_for_del = false;
                    $endtime_arr_dec = json_decode($end_arr_json, true);
                    if(is_array($endtime_arr_dec) && !empty($endtime_arr_dec)){
                        $is_fully_expired_for_del = true;
                        foreach($endtime_arr_dec as $k => $end_t){
                            $end_timestamp = strtotime($end_t);
                            if(time() > $end_timestamp){
                                $days = floor((time() - $end_timestamp) / 86400);
                                if($qq_end_day > 0 && $days < $qq_end_day){
                                    $is_fully_expired_for_del = false;
                                    break;
                                } elseif($qq_end_day == 0){
                                    $is_fully_expired_for_del = false;
                                    break;
                                }
                            }else{
                                $is_fully_expired_for_del = false;
                                break;
                            }
                        }
                    }
                    
                    if($is_fully_expired_for_del){
                        if($del_qq_type === 0){
                            if($t === 'pre_qq') set_qq_user($qq,0,1); else $DB->exec("DELETE FROM `{$t}` WHERE qq='{$qq}'");
                            deleteqq($frame, $qq);
                        }elseif($del_qq_type === 1){
                            if($t === 'pre_qq') set_qq_user($qq,0,1); else $DB->exec("DELETE FROM `{$t}` WHERE qq='{$qq}'");
                        }elseif($del_qq_type === 2){
                            deleteqq($frame, $qq);
                        }
                        $node_qq_deleted_count++;
                        echo "🗑️ 账号: {$qq} 已过期达到删除天数，执行删除清理 (类型:{$del_qq_type})\n";
                        continue;
                    }else{
                        if(intval($row['login_zt'] ?? 0) === 1){
                            outqq($frame, $qq);
                            $node_qq_forced_offline_count++;
                            echo "⚠️ 账号: {$qq} 已过期，执行强制踢下线\n";
                        }
                    }
                }
                
                $remoteOnline = isset($frame_qq_list[$qqk]) ? intval($frame_qq_list[$qqk]) : 0;
                $remoteExist = isset($frame_qq_list[$qqk]);
                $new_zt = ($remoteOnline === 1) ? 1 : 0;
                
                if($new_zt === 0 && $remoteExist && intval($row['dropline'] ?? 0) === 1 && !$skipRelogin && !$is_qq_expired){
                    echo "➡️ 正在尝试二次登录账号: {$qq} ... ";
                    $relogin_res = cache_login($frame, $qq);
                    $isLoginSuccess = (($relogin_res['retcode'] ?? -1) == 200) && (
                        (isset($relogin_res['bool']) && ($relogin_res['bool'] === true || $relogin_res['bool'] === 'true')) ||
                        (isset($relogin_res['msg']) && strpos((string)$relogin_res['msg'], '登录成功') !== false)
                    );
                    if($isLoginSuccess){
                        $new_zt = 1;
                        $node_frame_qq_relogin_ok++;
                        echo "✅ 重登请求已发送\n";
                    } else {
                        echo "❌ 重登失败: ".($relogin_res['msg'] ?? '接口响应异常')."\n";
                    }
                }
                
                $DB->exec("UPDATE `{$t}` SET uptime=NOW(),login_zt='{$new_zt}' WHERE qq='{$qq}'");
                if($new_zt === 1){
                    $online_count++;
                }else{
                    $offline_count++;
                }
            }
            
            $node_frame_qq_online += $online_count;
            $node_frame_qq_offline += $offline_count;
            
            $DB->exec("UPDATE pre_node_link SET sync_online_census = ? WHERE id = ?", [$online_count, $nodeLinkId]);
        }
        curl_multi_close($mh);
    }
}else{
    // 单线程模式处理
    foreach($all_links as $link){
        $node_frame_links_processed++;
        $nodeLinkId = intval($link['id'] ?? 0);
        if($nodeLinkId <= 0) continue;
        $frameCityId = 'link_' . $nodeLinkId;

        // 读取本地绑定了该 cityid=link_xxx 的 QQ（两表都查）
        $localRows = [];
        $rsQ1 = $DB->query("SELECT 'pre_qq' AS t, qq,zid,login_zt,dropline,endtime_arr FROM pre_qq WHERE cityid='{$frameCityId}'");
        if($rsQ1){
            while($r = $rsQ1->fetch(PDO::FETCH_ASSOC)){
                $localRows[] = $r;
            }
        }
        $rsQ2 = $DB->query("SELECT 'pre_dg_qq' AS t, qq,zid,login_zt,dropline,endtime_arr FROM pre_dg_qq WHERE cityid='{$frameCityId}'");
        if($rsQ2){
            while($r = $rsQ2->fetch(PDO::FETCH_ASSOC)){
                $localRows[] = $r;
            }
        }
        if(empty($localRows)) continue;
        $node_frame_links_have_local_qq++;

        $node_name = trim((string)($link['custom_name'] ?? ''));
        if($node_name === '') $node_name = trim((string)($link['remote_class_name'] ?? ''));
        if($node_name === '') $node_name = '节点#' . $nodeLinkId;

        // 取远程框架配置
        $frame = nodeLinkGetLoginConfig($nodeLinkId);
        if(!$frame || (is_array($frame) && isset($frame['error']))){
            // 拉不到远程配置时，不做二次登录，只把本地标记离线（更安全：反映已不可控）
            foreach($localRows as $r){
                $t = $r['t'];
                $qq = $r['qq'];
                $DB->exec("UPDATE `{$t}` SET uptime=NOW(),login_zt='0' WHERE qq='{$qq}'");
            }
            $countTotal = count($localRows);
            $node_frame_qq_offline += $countTotal;
            continue;
        }

        // 获取远程框架在线列表
        $getFrameResp = get_frame_qq($frame, 1, 5);
        if(!isset($getFrameResp['retcode']) || intval($getFrameResp['retcode']) !== 200){
            $node_frame_links_frame_list_fail++;
            continue;
        }

        $frame_qq_list = [];
        if(isset($getFrameResp['bot']['list'])){
            $online_uins = [];
            foreach($getFrameResp['bot']['online_list'] ?? [] as $r){
                $k = $qq_key($r['bot'] ?? $r['uin'] ?? 0);
                if($k !== '') $online_uins[$k] = 1;
            }
            foreach($getFrameResp['bot']['list'] as $row){
                $qq = $qq_key($row['bot'] ?? $row['uin'] ?? 0);
                if($qq === '') continue;
                $online = ($row['online'] === true || $row['online'] === 'true' || ($row['status'] ?? null) == 1 || ($row['status'] ?? null) === '1')
                    ? 1 : (isset($online_uins[$qq]) ? 1 : 0);
                $frame_qq_list[$qq] = $online;
            }
        }elseif(isset($getFrameResp['QQlist'])){
            foreach($getFrameResp['QQlist'] as $qq => $status){
                $k = $qq_key($qq);
                if($k === '') continue;
                $frame_qq_list[$k] = $status ? 1 : 0;
            }
        }elseif(isset($getFrameResp['data'])){
            foreach($getFrameResp['data'] as $qq => $status){
                $k = $qq_key($qq);
                if($k === '') continue;
                $frame_qq_list[$k] = $status ? 1 : 0;
            }
        }

        // 二次登录参数准备
        $ft = isset($frame['type']) ? strtolower(trim((string)$frame['type'])) : '';
        $skipRelogin = in_array($ft, ['myqq','myqqa','dr'], true);
        $apipwd_f = $frame['apipwd'] ?? '';
        $is_free_node = ($apipwd_f === '0' || $apipwd_f === 0 || ($apipwd_f && $apipwd_f !== ''));

        $online_count = 0;
        $offline_count = 0;

        foreach($localRows as $row){
            $t = $row['t'];
            $qq = $row['qq'];
            $qqk = $qq_key($qq);
            if($qqk === '') continue;
            
            // ================================
            // 增加对接节点QQ过期监控和清理逻辑
            // ================================
            $end_arr_json = $row['endtime_arr'] ?? '';
            $is_qq_expired = false;
            if(!$is_free_node && function_exists('is_end') && is_end($end_arr_json, $frame)){
                $is_qq_expired = true;
                $node_qq_expired_count++;
            }
            
            if($is_qq_expired){
                // 判断是否需要彻底删除或者强踢
                $qq_end_day = isset($conf_frame['qq_end_day']) ? intval($conf_frame['qq_end_day']) : 0;
                $del_qq_type = isset($conf_frame['del_qq_type']) ? intval($conf_frame['del_qq_type']) : 0;
                
                $is_fully_expired_for_del = false;
                $endtime_arr_dec = json_decode($end_arr_json, true);
                if(is_array($endtime_arr_dec) && !empty($endtime_arr_dec)){
                    $is_fully_expired_for_del = true;
                    foreach($endtime_arr_dec as $k => $end_t){
                        $end_timestamp = strtotime($end_t);
                        if(time() > $end_timestamp){
                            $days = floor((time() - $end_timestamp) / 86400);
                            if($qq_end_day > 0 && $days < $qq_end_day){
                                $is_fully_expired_for_del = false;
                                break;
                            } elseif($qq_end_day == 0){
                                // qq_end_day=0 表示不执行自动删除
                                $is_fully_expired_for_del = false;
                                break;
                            }
                        }else{
                            $is_fully_expired_for_del = false;
                            break;
                        }
                    }
                }
                
                if($is_fully_expired_for_del){
                    if($del_qq_type === 0){
                        if($t === 'pre_qq') set_qq_user($qq,0,1); else $DB->exec("DELETE FROM `{$t}` WHERE qq='{$qq}'");
                        deleteqq($frame, $qq);
                    }elseif($del_qq_type === 1){
                        if($t === 'pre_qq') set_qq_user($qq,0,1); else $DB->exec("DELETE FROM `{$t}` WHERE qq='{$qq}'");
                    }elseif($del_qq_type === 2){
                        deleteqq($frame, $qq);
                    }
                    $node_qq_deleted_count++;
                    echo "🗑️ 账号: {$qq} 已过期达到删除天数，执行删除清理 (类型:{$del_qq_type})\n";
                    continue; // 账号已清理，跳过后续状态更新
                }else{
                    // 仅到期但未达到删除天数，执行强制下线
                    if(intval($row['login_zt'] ?? 0) === 1){
                        outqq($frame, $qq);
                        $node_qq_forced_offline_count++;
                        echo "⚠️ 账号: {$qq} 已过期，执行强制踢下线\n";
                    }
                }
            }
            // ================================

            $remoteOnline = isset($frame_qq_list[$qqk]) ? intval($frame_qq_list[$qqk]) : 0;
            $remoteExist = isset($frame_qq_list[$qqk]);
            $new_zt = ($remoteOnline === 1) ? 1 : 0;

            // dropline=1：尝试二次登录（只要框架离线、列表中存在该QQ，且开启了自动重登）
            if($new_zt === 0 && $remoteExist && intval($row['dropline'] ?? 0) === 1 && !$skipRelogin && !$is_qq_expired){
                $end_arr_json = $row['endtime_arr'] ?? '';
                if(!$is_free_node && function_exists('is_end') && is_end($end_arr_json, $frame)){
                    echo "⏭️ 账号: {$qq} 已到期，跳过自动重登\n";
                }else{
                    echo "➡️ 正在尝试二次登录账号: {$qq} ... ";
                    $relogin_res = cache_login($frame, $qq);
                    $isLoginSuccess = (($relogin_res['retcode'] ?? -1) == 200) && (
                        (isset($relogin_res['bool']) && ($relogin_res['bool'] === true || $relogin_res['bool'] === 'true')) ||
                        (isset($relogin_res['msg']) && strpos((string)$relogin_res['msg'], '登录成功') !== false)
                    );
                    if($isLoginSuccess){
                        $new_zt = 1;
                        $node_frame_qq_relogin_ok++;
                        echo "✅ 重登请求已发送\n";
                    } else {
                        echo "❌ 重登失败: ".($relogin_res['msg'] ?? '接口响应异常')."\n";
                    }
                }
            }

            $DB->exec("UPDATE `{$t}` SET uptime=NOW(),login_zt='{$new_zt}' WHERE qq='{$qq}'");
            if($new_zt === 1){
                $online_count++;
            }else{
                $offline_count++;
            }
        }

        $node_frame_qq_online += $online_count;
        $node_frame_qq_offline += $offline_count;

        // 更新该对接节点缓存在线人数
        $DB->exec("UPDATE pre_node_link SET sync_online_census = ? WHERE id = ?", [$online_count, $nodeLinkId]);
    }
}

echo "\n对接节点框架QQ监控完成\n";
echo "对接节点框架在线QQ: {$node_frame_qq_online}\n";
echo "对接节点框架离线QQ: {$node_frame_qq_offline}\n";
echo "对接节点框架二次登录成功: {$node_frame_qq_relogin_ok}\n";
echo "对接节点已过期QQ: {$node_qq_expired_count}\n";
echo "对接节点过期强制下线: {$node_qq_forced_offline_count}\n";
echo "对接节点过期删除清理: {$node_qq_deleted_count}\n";
echo "框架监控：pre_node_link status=1 节点遍历数: {$node_frame_links_processed}\n";
echo "框架监控：本地有绑定QQ的节点数: {$node_frame_links_have_local_qq}\n";
echo "框架监控：拉取框架列表失败的节点数: {$node_frame_links_frame_list_fail}\n";

?>

