Files

1609 lines
74 KiB
PHP
Raw Permalink Normal View History

2026-06-14 14:00:24 +08:00
<?php
require_once __DIR__ . '/cong.php';
require_once __DIR__ . '/admin/bootstrap.php';
require_once __DIR__ . '/lib/Cloak/CountryAllowlist.php';
2026-06-14 14:00:24 +08:00
require_once __DIR__ . '/admin/ConfigWriter.php';
require_once __DIR__ . '/lib/Cloak/DomainRepository.php';
require_once __DIR__ . '/lib/CloakAdSourceGuard.php';
require_once __DIR__ . '/lib/Cloak/CloudflareConfig.php';
require_once __DIR__ . '/lib/Cloak/DomainProvisioningService.php';
session_start();
$filename = basename(__FILE__);
$current_name = pathinfo($filename, PATHINFO_FILENAME);
// 登出
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "logout") {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
echo("<script>window.location='{$current_name}';</script>");
exit();
}
// 重置临时链接参数
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delUrlArgs") {
$check_name = strip_tags($_REQUEST['check_name']);
$dir = dirname(__FILE__) . '/args';
$files = glob($dir . '/' . $check_name . "_*.txt");
foreach ($files as $file) {
if (preg_match('/_(\d{10})\.txt$/', $file, $m)) {
unlink($file);
}
}
header('Location: ' . $protocol . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] . '?action=edit_config&name=' . urlencode($check_name));
exit;
}
// 确定当前配置上下文(供表单回显用)
2026-06-14 18:30:06 +08:00
$isNewConfigDraft = false;
$configCopySource = '';
$copyFrom = dash_sanitize_config_name($_REQUEST['copy_from'] ?? '');
2026-06-14 14:00:24 +08:00
if (!empty($_REQUEST["name"])) {
$check_name = trim($_REQUEST["name"]);
$config_name = str_replace(".php", "", $check_name);
$config_file = "check_config/" . $config_name . "_config.php";
if (!file_exists($config_file)) {
2026-06-14 18:30:06 +08:00
$isNewConfigDraft = true;
$sourceFile = ($copyFrom !== '') ? "check_config/{$copyFrom}_config.php" : '';
if ($copyFrom !== '' && is_file($sourceFile)) {
$config_file = $sourceFile;
$configCopySource = $copyFrom;
if ($check_name === 'new_config' || $check_name === '') {
$check_name = dash_suggest_copy_config_name($copyFrom);
}
} else {
$config_file = "check_config/index_config.php";
}
2026-06-14 14:00:24 +08:00
}
} else {
$check_name = "index";
2026-06-14 18:30:06 +08:00
$config_file = "check_config/index_config.php";
2026-06-14 14:00:24 +08:00
}
include($config_file);
// 密码处理
if (!empty($_REQUEST['password'])) {
$_SESSION['password'] = $_REQUEST['password'];
$_SESSION["myaction"] = $_REQUEST['myaction'];
}
// 设置测试访问状态(写入完整 Session 判定缓存,便于测试广告落地链接直达真实页)
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "setuvi") {
require_once __DIR__ . '/lib/Cloak/VisitorRepository.php';
if (!empty($_REQUEST["res"]) && $_REQUEST["res"] == "true") {
cloak_session_mark_real_visitor();
}
if (!empty($_REQUEST["res"]) && $_REQUEST["res"] == "false") {
cloak_session_clear_real_visitor();
}
echo("<script>window.location='{$current_name}?action=edit_config&name={$check_name}';</script>");
exit();
}
// 保存配置
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "submit") {
$check_name = trim($_REQUEST["check_name"]);
$methods = trim($_REQUEST["methods"]);
$country_on = in_array(strtoupper(trim($_REQUEST['country_on'] ?? '')), ['ON']) ? 'ON' : 'OFF';
$country_raw = $country_on === 'ON' ? (string) ($_REQUEST['country'] ?? '') : '';
$country = $country_on === 'ON' ? CountryAllowlist::formatForConfig($country_raw) : '';
2026-06-14 14:00:24 +08:00
$blacklist_group_id = (int)($_REQUEST["blacklist_group_id"] ?? 0);
$whitelist_group_id = (int)($_REQUEST["whitelist_group_id"] ?? 0);
try {
$pdoDash = new PDO(
'mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4',
DB_USERNAME, DB_PASSWORD,
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$chk = $pdoDash->prepare('SELECT 1 FROM ip_groups WHERE id = ? AND type = ? LIMIT 1');
if ($blacklist_group_id > 0) {
$chk->execute([$blacklist_group_id, 'black']);
if (!$chk->fetchColumn()) $blacklist_group_id = 0;
}
if ($whitelist_group_id > 0) {
$chk->execute([$whitelist_group_id, 'white']);
if (!$chk->fetchColumn()) $whitelist_group_id = 0;
}
} catch (Throwable $e) {
$blacklist_group_id = $whitelist_group_id = 0;
}
$DB_fp = $_REQUEST["DB_fp"];
$DB_zp = trim($_REQUEST["DB_zp"]);
$redirect_method = trim($_REQUEST["redirect_method"]);
$show_content = trim($_REQUEST["show_content"]);
$costm_ip_score = trim($_REQUEST["costm_ip_score"]);
$write_log = trim($_REQUEST["write_log"]);
$zh_on = trim($_REQUEST["zh_on"]);
$os_on = trim($_REQUEST["os_on"]);
$mobile_on = trim($_REQUEST["mobile_on"]);
$v_referer = trim($_REQUEST["v_referer"]);
$is_virtual = trim($_REQUEST["is_virtual"]);
$auto_black = trim($_REQUEST["auto_black"]);
$auto_black_times = trim($_REQUEST["auto_black_times"]);
$white_params = trim($_REQUEST["white_params"]);
$keep_params = trim($_REQUEST["keep_params"]);
$iphone_model = $_REQUEST["iphone_model"];
$risk_number = $_REQUEST["risk_number"];
$risk_enhanced = in_array(strtoupper(trim($_REQUEST["risk_enhanced"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$url_args_timeout = $_REQUEST["url_args_timeout"];
$debug_mode = in_array(strtoupper(trim($_REQUEST["debug_mode"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_fb = in_array(strtoupper(trim($_REQUEST["ad_fb"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_google = in_array(strtoupper(trim($_REQUEST["ad_google"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_tiktok = in_array(strtoupper(trim($_REQUEST["ad_tiktok"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$ad_strict = in_array(strtoupper(trim($_REQUEST["ad_strict"] ?? '')), ['ON']) ? 'ON' : 'OFF';
$config_name_submit = str_replace('.php', '', $check_name);
$existing_spm_id = '';
$existing_cfg = 'check_config/' . $config_name_submit . '_config.php';
if (file_exists($existing_cfg)) {
$existing_raw = file_get_contents($existing_cfg);
if (preg_match("/define\('CLOAK_AD_SPM_ID',\s*'([^']*)'\)/", $existing_raw, $spmM)) {
$existing_spm_id = $spmM[1];
}
}
$ad_spm_id = CloakAdSourceGuard::isValidSpmId($existing_spm_id) ? $existing_spm_id : '';
if (($ad_fb === 'ON' || $ad_google === 'ON' || $ad_tiktok === 'ON') && $ad_spm_id === '') {
$ad_spm_id = CloakAdSourceGuard::generateSpmId();
}
if (strtoupper($auto_black) === 'ON' && $blacklist_group_id <= 0) {
echo "<script>alert('开启「自动黑名单」时,必须先选择「黑名单 IP 组」。');history.back();</script>";
exit();
}
if ($country_on === 'ON' && $country === '') {
echo "<script>alert('已开启国家条件,请填写允许的国家代码(如 US,GB,CA)。');history.back();</script>";
exit();
}
2026-06-14 14:00:24 +08:00
if (!empty($methods)) {
ConfigWriter::save($check_name, [
'methods' => $methods,
'blacklist_group_id' => $blacklist_group_id,
'whitelist_group_id' => $whitelist_group_id,
'country' => $country,
'show_content' => $show_content,
'DB_fp' => $DB_fp,
'costm_ip_score' => $costm_ip_score,
'auto_black' => $auto_black,
'is_virtual' => $is_virtual,
'auto_black_times' => $auto_black_times,
'white_params' => $white_params,
'iphone_model' => $iphone_model,
'write_log' => $write_log,
'zh_on' => $zh_on,
'os_on' => $os_on,
'keep_params' => $keep_params,
'redirect_method' => $redirect_method,
'DB_zp' => $DB_zp,
'mobile_on' => $mobile_on,
'v_referer' => $v_referer,
'risk_number' => $risk_number,
'risk_enhanced' => $risk_enhanced,
'url_args_timeout' => $url_args_timeout,
'debug_mode' => $debug_mode,
'ad_fb' => $ad_fb,
'ad_google' => $ad_google,
'ad_tiktok' => $ad_tiktok,
'ad_strict' => $ad_strict,
'ad_spm_id' => $ad_spm_id,
]);
try {
$pdoBind = DomainRepository::pdo();
DomainRepository::ensureTable($pdoBind);
2026-07-06 10:34:34 +08:00
$siteDomainRaw = $_REQUEST['site_domain_id'] ?? '0';
if ($siteDomainRaw === 'random') {
$existingId = DomainRepository::domainIdForConfig($pdoBind, $check_name);
if ($existingId > 0) {
$siteDomainId = $existingId;
} else {
$siteDomainId = DomainRepository::pickRandomDomainId($pdoBind);
}
} else {
$siteDomainId = (int) $siteDomainRaw;
}
2026-06-14 14:00:24 +08:00
if ($siteDomainId > 0) {
DomainRepository::bindConfigToDomain($pdoBind, $check_name, $siteDomainId);
} else {
DomainRepository::clearConfigBinding($pdoBind, $check_name);
}
} catch (Throwable $e) {
}
echo("<script>alert('保存成功!');window.location='{$current_name}?action=edit_config&name={$check_name}';</script>");
exit();
}
}
function write_file($file_name, $text_str) {
$f = fopen($file_name, "w");
rewind($f);
fwrite($f, $text_str);
fclose($f);
}
function onoff_radio($name, $val, $label_on='开启', $label_off='关闭') {
$on = ($val == 'ON') ? 'active' : '';
$off = ($val != 'ON') ? 'active' : '';
return '<div class="btn-group btn-group-toggle" data-toggle="buttons">
2026-07-06 10:34:34 +08:00
<label class="btn btn-sm btn-outline-primary ' . $on . '">
2026-06-14 14:00:24 +08:00
<input type="radio" name="' . $name . '" value="ON" ' . ($val=='ON'?'checked':'') . '> ' . $label_on . '
</label>
2026-07-06 10:34:34 +08:00
<label class="btn btn-sm btn-outline-primary ' . $off . '">
2026-06-14 14:00:24 +08:00
<input type="radio" name="' . $name . '" value="OFF" ' . ($val!='ON'?'checked':'') . '> ' . $label_off . '
</label>
</div>';
}
function dash_truncate_attr($text, $maxLen = 52) {
$text = (string) $text;
$esc = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
if (mb_strlen($text, 'UTF-8') <= $maxLen) {
return $esc;
}
return '<span class="cell-truncate" title="' . $esc . '">' . htmlspecialchars(mb_substr($text, 0, $maxLen, 'UTF-8'), ENT_QUOTES, 'UTF-8') . '…</span>';
}
if (strlen($text) <= $maxLen) {
return $esc;
}
return '<span class="cell-truncate" title="' . $esc . '">' . htmlspecialchars(substr($text, 0, $maxLen), ENT_QUOTES, 'UTF-8') . '…</span>';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IPCLOAKAPI 多功能管理后台面板 v8.2.1</title>
<link rel="stylesheet" href="./dist/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="./dist/jquery-2.2.4.min.js"></script>
<script src="./dist/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
<style>
body { background:#f0f2f5; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif; }
.topbar { background:linear-gradient(135deg,#16baaa,#0e8d81); color:#fff; padding:14px 24px; display:flex; align-items:center; justify-content:space-between; box-shadow:0 2px 8px rgba(0,0,0,.15); }
.topbar a { color:rgba(255,255,255,.85); text-decoration:none; font-size:.9rem; }
.topbar a:hover { color:#fff; }
.topbar h1 { margin:0; font-size:1.25rem; font-weight:600; }
.page-body { max-width:1100px; margin:32px auto; padding:0 16px; }
2026-07-06 10:34:34 +08:00
.page-body.page-body--list { width:90%; max-width:none; margin-left:auto; margin-right:auto; }
2026-06-14 14:00:24 +08:00
.status-bar { display:flex; gap:12px; margin-bottom:20px; flex-wrap:wrap; }
.status-badge { display:inline-flex; align-items:center; gap:6px; padding:6px 14px; border-radius:20px; font-size:.82rem; font-weight:500; }
.status-ok { background:#d4edda; color:#155724; }
.status-err { background:#f8d7da; color:#721c24; }
.card-header-custom { background:#fff; border-bottom:1px solid #e9ecef; padding:14px 20px; display:flex; align-items:center; justify-content:space-between; border-radius:8px 8px 0 0; }
.card-header-custom h5 { margin:0; font-size:1rem; font-weight:600; }
.config-table th { background:#f8f9fa; font-weight:600; }
.config-table td, .config-table th { vertical-align:middle !important; padding:12px 16px !important; }
.config-table tr:hover { background:#f0f7ff; }
.url-text { font-size:.78rem; color:#6c757d; }
.ad-link-row { display:flex; align-items:flex-start; gap:6px; margin-bottom:6px; }
.ad-link-row:last-child { margin-bottom:0; }
.ad-link-label { font-size:.72rem; color:#495057; white-space:nowrap; min-width:58px; padding-top:2px; }
.btn-copy-ad { font-size:.72rem; padding:2px 8px; flex-shrink:0; }
.ad-link-hint { font-size:.68rem; color:#adb5bd; margin-top:4px; }
.section-heading { background:#e8f4fd; color:#1565c0; font-weight:600; padding:8px 14px; border-radius:4px; margin:6px 0 2px; font-size:.85rem; letter-spacing:.3px; }
.form-label-cell { width:36%; font-size:.88rem; color:#444; padding:10px 14px !important; }
.form-val-cell { padding:8px 14px !important; }
.form-section { border:1px solid #dee2e6; border-radius:6px; margin-bottom:8px; overflow:hidden; }
.form-section .section-toggle { background:#f8f9fa; padding:10px 16px; cursor:pointer; display:flex; align-items:center; justify-content:space-between; font-weight:600; font-size:.9rem; border:none; width:100%; text-align:left; }
.form-section .section-toggle:hover { background:#e9ecef; }
.form-section .section-toggle .arrow { transition:transform .2s; }
.form-section .section-toggle.collapsed .arrow { transform:rotate(-90deg); }
.back-link { display:inline-flex; align-items:center; gap:6px; color:#1565c0; font-size:.9rem; margin-bottom:16px; text-decoration:none; }
.back-link:hover { text-decoration:underline; }
.sticky-actions { position:sticky; bottom:0; background:#fff; border-top:1px solid #dee2e6; padding:12px 20px; display:flex; gap:10px; align-items:center; z-index:100; border-radius:0 0 8px 8px; }
.login-wrap { min-height:100vh; display:flex; align-items:center; justify-content:center; }
.login-card { background:#fff; border-radius:10px; box-shadow:0 4px 20px rgba(0,0,0,.1); padding:40px; width:340px; }
.login-card h2 { text-align:center; color:#16baaa; margin-bottom:24px; font-size:1.4rem; }
.list-page-title { margin-bottom: 4px; }
.list-page-title h2 { margin: 0 0 6px; font-size: 1.2rem; font-weight: 600; color: #2c3e50; }
.list-page-title p { margin: 0; font-size: .85rem; color: #8c98a4; }
.card-shell { border-radius: 10px; overflow: hidden; border: 1px solid #e8ecf1; box-shadow: 0 2px 12px rgba(15,23,42,.04); }
.card-header-custom .header-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.card-header-custom .header-actions .btn-teal { background: #16baaa; color: #fff; border: none; }
.card-header-custom .header-actions .btn-teal:hover { background: #0e8d81; color: #fff; }
.list-table-wrap { overflow-x: auto; }
.config-table thead th {
position: sticky;
top: 0;
z-index: 5;
background: linear-gradient(180deg, #f4f7fb 0%, #eef2f7 100%) !important;
border-bottom: 2px solid #dde4ec !important;
font-size: .82rem;
letter-spacing: .02em;
text-transform: none;
white-space: nowrap;
}
.config-table tbody tr:nth-child(even) { background: #fafbfc; }
.config-table tbody tr:hover { background: #eef6ff !important; }
.config-table tbody tr { transition: background-color .15s ease; }
.config-table tbody td { border-color: #eef1f5 !important; font-size: .86rem; }
.list-table thead th {
background: #f4f7fb;
font-weight: 600;
font-size: .82rem;
border-bottom: 2px solid #dde4ec !important;
padding: 10px 14px !important;
white-space: nowrap;
}
.list-table tbody tr:nth-child(even) { background: #fafbfc; }
.list-table tbody tr:hover { background: #eef6ff; }
.list-table tbody td { vertical-align: middle !important; padding: 11px 14px !important; border-color: #eef1f5 !important; font-size: .84rem; }
.cell-truncate { display: inline-block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; cursor: default; }
.url-text-truncate { font-size: .78rem; color: #6c757d; word-break: break-all; }
.action-btn-group { display: flex; flex-wrap: wrap; gap: 6px; }
.action-btn-group .btn { border-radius: 6px; }
.domain-host-badge { display: inline-block; padding: 2px 8px; background: #e8f4fd; color: #1565c0; border-radius: 4px; font-size: .78rem; font-family: 'SF Mono', Consolas, monospace; }
.config-name-cell strong { color: #2c3e50; font-size: .92rem; }
.empty-hint { padding: 28px 20px; text-align: center; color: #8c98a4; font-size: .88rem; background: #fafbfc; border-radius: 0 0 10px 10px; }
2026-07-06 10:34:34 +08:00
.list-toolbar { display:flex; flex-wrap:wrap; align-items:center; gap:10px; padding:12px 16px; border-bottom:1px solid #e9ecef; background:#fafbfc; }
.list-toolbar .search-form { display:flex; align-items:center; gap:8px; flex:1; min-width:220px; }
.list-toolbar .search-form input[type="search"] { max-width:280px; }
.list-pagination { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:10px; padding:12px 16px; border-top:1px solid #e9ecef; background:#fff; font-size:.84rem; color:#6c757d; }
.list-pagination .pagination { margin:0; }
.config-table .col-check { width:36px; text-align:center; }
/* 表单开关:选中实心蓝,未选中蓝框蓝字 */
.btn-group-toggle .btn.btn-outline-primary,
.btn-group-toggle .btn.btn-outline-secondary {
color: #1565c0;
border-color: #64b5f6;
background: #fff;
font-weight: 500;
min-width: 52px;
}
.btn-group-toggle .btn.btn-outline-primary:hover,
.btn-group-toggle .btn.btn-outline-secondary:hover {
color: #0d47a1;
background: #e3f2fd;
border-color: #42a5f5;
}
.btn-group-toggle .btn.btn-outline-primary.active,
.btn-group-toggle .btn.btn-outline-secondary.active,
.btn-group-toggle .btn.btn-outline-primary.active:hover,
.btn-group-toggle .btn.btn-outline-secondary.active:hover {
color: #fff !important;
background: linear-gradient(180deg, #2196f3 0%, #1565c0 100%) !important;
border-color: #1565c0 !important;
box-shadow: 0 1px 4px rgba(21, 101, 192, .35);
}
.btn-group-toggle .btn.btn-outline-primary:not(.active):focus,
.btn-group-toggle .btn.btn-outline-secondary:not(.active):focus {
box-shadow: 0 0 0 .15rem rgba(33, 150, 243, .25);
}
2026-06-14 14:00:24 +08:00
</style>
</head>
<body>
<?php if (empty($_SESSION['password'])): ?>
<!-- ===================== 登录页 ===================== -->
<div class="login-wrap">
<div class="login-card">
<h2>IPCLOAKAPI 多功能管理后台面板 v8.2.1</h2>
<form method="post" action="<?php echo $current_name; ?>">
<div class="form-group">
<label>登录密码</label>
<input type="password" name="password" class="form-control" placeholder="请输入密码" autofocus>
</div>
<input type="hidden" name="myaction" value="dolist">
<button type="submit" class="btn btn-block" style="background:#16baaa;color:#fff;margin-top:8px;">进入后台</button>
</form>
</div>
</div>
<?php exit(); endif; ?>
<?php
// 密码验证
if ($_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
echo("<script>alert('密码错误!');window.location='{$current_name}';</script>");
exit();
}
// 站点域名管理
$cloakDomainMsg = '';
$cloakDomainErr = '';
$cloakCfEnabled = CloudflareConfig::isEnabled();
2026-07-06 10:34:34 +08:00
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'add_domain') {
$hostText = trim((string) ($_POST['new_hostnames'] ?? $_POST['new_hostname'] ?? ''));
if ($hostText !== '') {
try {
$pdoDom = DomainRepository::pdo();
$dnsMode = $_POST['dns_mode'] ?? 'auto';
if (!in_array($dnsMode, ['auto', 'manual'], true)) {
$dnsMode = 'auto';
}
if ($dnsMode === 'auto' && !$cloakCfEnabled) {
$cloakDomainErr = '自动模式需要先在 cong.php 配置 Cloudflare API Token 与 Account ID,或改选手动模式。';
2026-06-14 14:00:24 +08:00
} else {
2026-07-06 10:34:34 +08:00
$res = DomainProvisioningService::provisionBatch($pdoDom, $hostText, $dnsMode);
if ($res['ok']) {
$addedNames = array_map(static function ($row) {
return $row['hostname'] ?? '';
}, $res['added'] ?? []);
$addedNames = array_filter($addedNames);
$cloakDomainMsg = '已成功添加 ' . count($addedNames) . ' 个域名:'
. htmlspecialchars(implode('、', $addedNames));
if (($res['dns_mode'] ?? '') === 'auto' && $cloakCfEnabled) {
$cloakDomainMsg .= '。请到注册商修改 Nameserver 后,在列表中点击「检测/刷新」。';
} elseif (($res['dns_mode'] ?? '') === 'manual') {
$cloakDomainMsg .= '(手动解析模式,无需 Cloudflare 检测)。';
} else {
$cloakDomainMsg .= ',请在编辑配置时关联此域名。';
}
if (!empty($res['failed'])) {
$failParts = [];
foreach ($res['failed'] as $f) {
$failParts[] = ($f['hostname'] ?? '?') . '' . ($f['error'] ?? '失败') . '';
}
$cloakDomainErr = count($res['failed']) . ' 个域名添加失败:'
. htmlspecialchars(implode('', $failParts));
}
} else {
$cloakDomainErr = $res['error'] ?? '添加失败';
if (!empty($res['failed'])) {
$failParts = [];
foreach ($res['failed'] as $f) {
$failParts[] = ($f['hostname'] ?? '?') . '' . ($f['error'] ?? '失败') . '';
}
$cloakDomainErr .= '' . htmlspecialchars(implode('', $failParts));
}
}
2026-06-14 14:00:24 +08:00
}
2026-07-06 10:34:34 +08:00
} catch (Throwable $e) {
$cloakDomainErr = $e->getMessage();
2026-06-14 14:00:24 +08:00
}
2026-07-06 10:34:34 +08:00
} else {
$cloakDomainErr = '请至少输入一个域名';
2026-06-14 14:00:24 +08:00
}
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'check_domain') {
$domainId = (int) ($_POST['domain_id'] ?? $_REQUEST['domain_id'] ?? 0);
$jsonOut = isset($_REQUEST['format']) && $_REQUEST['format'] === 'json';
try {
$res = DomainProvisioningService::checkAndProvision(DomainRepository::pdo(), $domainId);
if ($jsonOut) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode($res, JSON_UNESCAPED_UNICODE);
exit;
}
if ($res['ok']) {
$cloakDomainMsg = $res['message'] ?? '检测完成';
} else {
$cloakDomainErr = $res['message'] ?? '检测失败';
}
} catch (Throwable $e) {
if ($jsonOut) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok' => false, 'message' => $e->getMessage()], JSON_UNESCAPED_UNICODE);
exit;
}
$cloakDomainErr = $e->getMessage();
}
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'delete_domain' && !empty($_REQUEST['domain_id'])) {
try {
$res = DomainProvisioningService::deleteWithCloudflare(DomainRepository::pdo(), (int) $_REQUEST['domain_id']);
if ($res['ok']) {
$cloakDomainMsg = $res['message'] ?? '域名已删除。';
if (!empty($res['warning'])) {
$cloakDomainErr = $res['warning'];
}
} else {
$cloakDomainErr = $res['message'] ?? '删除失败';
}
} catch (Throwable $e) {
$cloakDomainErr = $e->getMessage();
}
}
// 系统状态检测(列表页用)
function dash_check_mysql() {
try {
$pdo = new PDO('mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4', DB_USERNAME, DB_PASSWORD, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
return true;
} catch (Throwable $e) { return false; }
}
function dash_check_redis() {
if (!extension_loaded('redis') || strtoupper(REDIS_ENABLED) !== 'ON') return null;
try {
$r = new Redis();
if (!@$r->connect(REDIS_HOST, (int)REDIS_PORT, 0.5)) return false;
$r->ping();
return true;
} catch (Throwable $e) { return false; }
}
function dash_sanitize_config_name($name)
{
$name = trim(str_replace('.php', '', (string) $name));
if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
return '';
}
return $name;
}
function dash_suggest_copy_config_name($source)
{
$source = dash_sanitize_config_name($source);
if ($source === '') {
return 'new_config';
}
$base = $source . '_copy';
$candidate = $base;
$suffix = 2;
while (file_exists(__DIR__ . "/check_config/{$candidate}_config.php")) {
$candidate = $base . $suffix;
$suffix++;
}
return $candidate;
}
2026-06-14 14:00:24 +08:00
function dash_cfg_get($raw, $name)
{
if (preg_match("/define\('" . preg_quote($name, '/') . "',\s*'([^']*)'\)/", $raw, $m)) {
return $m[1];
}
return '';
}
function dash_build_ad_links($pf, $protocol, $host)
{
$path = __DIR__ . "/check_config/{$pf}_config.php";
try {
$pdoDashHost = DomainRepository::pdo();
DomainRepository::ensureTable($pdoDashHost);
$bound = DomainRepository::primaryHostnameForConfig($pdoDashHost, $pf);
if ($bound !== '') {
$host = $bound;
}
} catch (Throwable $e) {
}
$host = DomainRepository::normalizeHostname($host);
if ($host === '') {
$host = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
}
$base = rtrim($protocol . $host, '/') . '/' . (($pf != 'index') ? $pf : '');
if (!file_exists($path)) {
return [['label' => '落地链接', 'url' => $base]];
}
$raw = file_get_contents($path);
$enabled = [];
if (dash_cfg_get($raw, 'CLOAK_AD_FB') === 'ON') {
$enabled[] = CloakAdSourceGuard::PLATFORM_FB;
}
if (dash_cfg_get($raw, 'CLOAK_AD_GOOGLE') === 'ON') {
$enabled[] = CloakAdSourceGuard::PLATFORM_GOOGLE;
}
if (dash_cfg_get($raw, 'CLOAK_AD_TIKTOK') === 'ON') {
$enabled[] = CloakAdSourceGuard::PLATFORM_TIKTOK;
}
if (empty($enabled)) {
return [['label' => '落地链接', 'url' => $base]];
}
2026-06-27 23:58:54 +08:00
$spmId = dash_cfg_get($raw, 'CLOAK_AD_SPM_ID');
$fpUrls = [];
if (preg_match("/define\('DB_FP',\s*(array\s*\(.*?\))\s*\)/s", $raw, $fpMatch)) {
@eval('$fpUrls = ' . $fpMatch[1] . ';');
}
$query = CloakAdSourceGuard::buildAdLinkQuery($enabled, $spmId, $fpUrls);
2026-06-14 14:00:24 +08:00
$url = $base . ($query !== '' ? '?' . $query : '');
return [['label' => '落地链接', 'url' => $url]];
}
2026-07-06 10:34:34 +08:00
function dash_build_config_index()
{
$items = [];
$dir = __DIR__ . '/check_config';
$dh = @opendir($dir);
if (!$dh) {
return $items;
}
while (false !== ($f = readdir($dh))) {
if ($f === '.' || $f === '..') {
continue;
}
$path = $dir . '/' . $f;
if (is_dir($path) || $f === 'index.php' || $f === 'a.txt') {
continue;
}
if (substr($f, -11) !== '_config.php') {
continue;
}
$name = str_replace('_config.php', '', $f);
if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
continue;
}
$entryPhp = __DIR__ . '/' . $name . '.php';
if (!file_exists($entryPhp)) {
copy(__DIR__ . '/tem_link.php', $entryPhp);
}
$items[] = [
'name' => $name,
'mtime' => (int) @filemtime($path),
'path' => $path,
];
}
closedir($dh);
usort($items, function ($a, $b) {
if ($a['mtime'] === $b['mtime']) {
return strcmp($a['name'], $b['name']);
}
return $b['mtime'] <=> $a['mtime'];
});
return $items;
}
function dash_paginate_configs(array $all, $q, $page, $perPage)
{
$q = trim((string) $q);
$page = max(1, (int) $page);
$perPage = max(1, (int) $perPage);
if ($q !== '') {
$qLower = function_exists('mb_strtolower') ? mb_strtolower($q, 'UTF-8') : strtolower($q);
$all = array_values(array_filter($all, function ($row) use ($qLower) {
$name = (string) ($row['name'] ?? '');
$hay = function_exists('mb_strtolower') ? mb_strtolower($name, 'UTF-8') : strtolower($name);
return strpos($hay, $qLower) !== false;
}));
}
$total = count($all);
$totalPages = $total > 0 ? (int) ceil($total / $perPage) : 1;
if ($page > $totalPages) {
$page = $totalPages;
}
$offset = ($page - 1) * $perPage;
return [
'items' => array_slice($all, $offset, $perPage),
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'total_pages' => $totalPages,
];
}
function dash_delete_config($name)
{
$name = dash_sanitize_config_name($name);
if ($name === '' || $name === 'index') {
return false;
}
try {
DomainRepository::clearConfigBinding(DomainRepository::pdo(), $name);
} catch (Throwable $e) {
}
@unlink(__DIR__ . "/check_config/{$name}_config.php");
@unlink(__DIR__ . "/{$name}.php");
return true;
}
function dash_list_query_params($q, $page)
{
$params = [];
if (trim((string) $q) !== '') {
$params['q'] = trim((string) $q);
}
if ((int) $page > 1) {
$params['page'] = (int) $page;
}
return $params;
}
function dash_list_url($currentName, $q, $page)
{
$params = dash_list_query_params($q, $page);
$qs = $params !== [] ? '?' . http_build_query($params) : '';
return $currentName . $qs;
}
// 配置列表索引(按文件修改时间倒序)
$configListAll = dash_build_config_index();
$listSearchQ = trim((string) ($_GET['q'] ?? ''));
$listPage = max(1, (int) ($_GET['page'] ?? 1));
$listPerPage = 10;
$configPage = dash_paginate_configs($configListAll, $listSearchQ, $listPage, $listPerPage);
$products_file_ary = array_map(function ($row) {
return $row['name'];
}, $configListAll);
// 删除 / 批量删除配置
if (!empty($_REQUEST["action"]) && $_REQUEST["action"] == "delete_config" && !empty($_REQUEST["name"])) {
$name = trim($_REQUEST["name"]);
if ($name == "index") {
echo("<script>alert('请勿删除默认配置!');window.location='{$current_name}';</script>");
exit();
}
dash_delete_config($name);
echo("<script>alert('删除成功!');window.location='{$current_name}';</script>");
exit();
}
if (!empty($_REQUEST['action']) && $_REQUEST['action'] === 'batch_delete_config') {
$names = $_REQUEST['names'] ?? [];
if (!is_array($names)) {
$names = [$names];
2026-06-14 14:00:24 +08:00
}
2026-07-06 10:34:34 +08:00
$deleted = 0;
$seen = [];
foreach ($names as $rawName) {
$name = dash_sanitize_config_name($rawName);
if ($name === '' || $name === 'index' || isset($seen[$name])) {
continue;
}
$seen[$name] = true;
if (dash_delete_config($name)) {
$deleted++;
}
}
$redirect = dash_list_url($current_name, $_POST['q'] ?? '', 1);
$msg = $deleted > 0 ? "已删除 {$deleted} 个配置" : '未删除任何配置';
echo("<script>alert('" . addslashes($msg) . "');window.location='" . addslashes($redirect) . "';</script>");
exit();
2026-06-14 14:00:24 +08:00
}
// 加载 IP 组(编辑页用)
$blackGroups = $whiteGroups = [];
try {
$pdoDash = new PDO('mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4', DB_USERNAME, DB_PASSWORD, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);
$stb = $pdoDash->prepare("SELECT id, name FROM ip_groups WHERE type = 'black' ORDER BY id ASC");
$stb->execute(); $blackGroups = $stb->fetchAll();
$stw = $pdoDash->prepare("SELECT id, name FROM ip_groups WHERE type = 'white' ORDER BY id ASC");
$stw->execute(); $whiteGroups = $stw->fetchAll();
} catch (Throwable $e) {}
$currentBlacklistGroupId = defined('BLACKLIST_GROUP_ID') ? (int)BLACKLIST_GROUP_ID : 0;
$currentWhitelistGroupId = defined('WHITELIST_GROUP_ID') ? (int)WHITELIST_GROUP_ID : 0;
$action = $_REQUEST['action'] ?? '';
$isEdit = in_array($action, ['edit_config', 'submit'], true);
$cloakSiteDomains = [];
2026-07-06 10:34:34 +08:00
$domainConfigMap = [];
2026-06-14 14:00:24 +08:00
$selectedDomainId = 0;
$dashboardDefaultHost = DomainRepository::normalizeHostname($_SERVER['HTTP_HOST'] ?? '');
try {
$pdoDash = $pdoDash ?? DomainRepository::pdo();
DomainRepository::ensureTable($pdoDash);
$cloakSiteDomains = DomainRepository::listAll($pdoDash);
2026-07-06 10:34:34 +08:00
$domainConfigMap = DomainRepository::groupConfigsByDomainId($pdoDash);
2026-06-14 14:00:24 +08:00
if ($isEdit) {
$selectedDomainId = DomainRepository::domainIdForConfig($pdoDash, $check_name);
}
} catch (Throwable $e) {
$cloakSiteDomains = [];
2026-07-06 10:34:34 +08:00
$domainConfigMap = [];
2026-06-14 14:00:24 +08:00
}
// 视图判断
$_d = function($name, $default) { return defined($name) ? constant($name) : $default; };
?>
<!-- ===================== 顶栏 ===================== -->
<div class="topbar">
<h1>IPCLOAKAPI 多功能管理后台面板 v8.2.1</h1>
<div style="display:flex;gap:16px;align-items:center;">
<?php if ($isEdit): ?>
<a href="<?php echo $current_name; ?>">← 返回列表</a>
<?php endif; ?>
<a href="<?php echo $current_name; ?>?action=logout">退出登录</a>
</div>
</div>
2026-07-06 10:34:34 +08:00
<div class="page-body<?php echo $isEdit ? '' : ' page-body--list'; ?>">
2026-06-14 14:00:24 +08:00
<?php if (!$isEdit): ?>
<!-- ===================== 列表视图 ===================== -->
<div class="list-page-title">
<h2>配置管理</h2>
<p>管理 Cloak 配置、广告落地链接与站点域名</p>
</div>
<!-- 系统状态 -->
<div class="status-bar">
<?php
$mysqlOk = dash_check_mysql();
$redisOk = dash_check_redis();
echo '<span class="status-badge ' . ($mysqlOk ? 'status-ok' : 'status-err') . '">● MySQL ' . ($mysqlOk ? '已连接' : '连接失败') . '</span>';
if ($redisOk === null) {
echo '<span class="status-badge" style="background:#fff3cd;color:#856404;">● Redis 未启用</span>';
} else {
echo '<span class="status-badge ' . ($redisOk ? 'status-ok' : 'status-err') . '">● Redis ' . ($redisOk ? '已连接' : '连接失败') . '</span>';
}
?>
2026-07-06 10:34:34 +08:00
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 配置数:<?php echo (int) $configPage['total']; ?><?php if ($listSearchQ !== ''): ?>(搜索「<?php echo htmlspecialchars($listSearchQ); ?>」)<?php endif; ?></span>
2026-06-14 14:00:24 +08:00
<span class="status-badge" style="background:#e2e3e5;color:#383d41;">● 已登记域名:<?php echo count($cloakSiteDomains); ?></span>
</div>
<?php if ($cloakDomainMsg): ?>
<div class="alert alert-success"><?php echo $cloakDomainMsg; ?></div>
<?php endif; ?>
<?php if ($cloakDomainErr): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($cloakDomainErr); ?></div>
<?php endif; ?>
<!-- 配置列表卡片 -->
<div class="card shadow-sm card-shell">
<div class="card-header-custom">
<h5>配置列表</h5>
<div class="header-actions">
<a href="/table" target="_blank" class="btn btn-sm btn-outline-secondary">查看日志</a>
<a href="/log_analytics.php" target="_blank" class="btn btn-sm btn-outline-secondary">数据分析</a>
2026-07-06 10:34:34 +08:00
<button type="submit" form="batchDeleteForm" class="btn btn-sm btn-outline-danger" id="batchDeleteBtn" onclick="return dashBatchDeleteConfirm();">批量删除</button>
2026-06-14 14:00:24 +08:00
<a href="<?php echo $current_name; ?>?action=edit_config&name=new_config" class="btn btn-sm btn-teal">+ 新增配置</a>
</div>
</div>
2026-07-06 10:34:34 +08:00
<div class="list-toolbar">
<form method="get" action="<?php echo htmlspecialchars($current_name); ?>" class="search-form mb-0">
<input type="search" name="q" class="form-control form-control-sm" placeholder="搜索配置名称…" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
<button type="submit" class="btn btn-sm btn-outline-primary">搜索</button>
<?php if ($listSearchQ !== ''): ?>
<a href="<?php echo htmlspecialchars($current_name); ?>" class="btn btn-sm btn-outline-secondary">清除</a>
<?php endif; ?>
</form>
</div>
<form method="post" action="<?php echo htmlspecialchars($current_name); ?>" id="batchDeleteForm">
<input type="hidden" name="action" value="batch_delete_config">
<input type="hidden" name="q" value="<?php echo htmlspecialchars($listSearchQ, ENT_QUOTES); ?>">
2026-06-14 14:00:24 +08:00
<div class="list-table-wrap">
<table class="table config-table mb-0">
<thead>
<tr>
2026-07-06 10:34:34 +08:00
<th class="col-check"><input type="checkbox" id="checkAllConfigs" title="全选"></th>
<th style="width:20%">配置名称</th>
<th style="width:12%">绑定域名</th>
2026-06-14 14:00:24 +08:00
<th>广告链接(广告后台填入的链接)</th>
<th>真实页链接(客户到达的链接)</th>
2026-07-06 10:34:34 +08:00
<th style="width:20%">操作</th>
2026-06-14 14:00:24 +08:00
</tr>
</thead>
<tbody>
2026-07-06 10:34:34 +08:00
<?php if (empty($configPage['items'])): ?>
<tr><td colspan="6" class="empty-hint">暂无匹配的配置<?php if ($listSearchQ !== ''): ?>,请调整搜索关键词<?php endif; ?></td></tr>
<?php endif; ?>
<?php foreach ($configPage['items'] as $cfgRow):
$pf = $cfgRow['name'];
2026-06-14 14:00:24 +08:00
$pcfg = "check_config/{$pf}_config.php";
$pfp = [];
if (file_exists($pcfg)) {
// 简单读取 DB_FP 和 DB_ZP 值用于展示
$raw = file_get_contents($pcfg);
if (preg_match("/define\('DB_FP',\s*(array\s*\(.*?\))\s*\)/s", $raw, $m)) {
@eval('$pfp = ' . $m[1] . ';');
}
preg_match("/define\('DB_ZP',\s*'([^']*)'\)/", $raw, $zpM);
$pzp = $zpM[1] ?? '';
}
?>
<tr>
2026-07-06 10:34:34 +08:00
<td class="col-check">
<?php if ($pf !== 'index'): ?>
<input type="checkbox" class="config-row-check" name="names[]" value="<?php echo htmlspecialchars($pf, ENT_QUOTES); ?>">
<?php endif; ?>
</td>
2026-06-14 14:00:24 +08:00
<td class="config-name-cell">
<strong><?php echo htmlspecialchars($pf); ?></strong>
<?php if ($pf === 'index'): ?><span class="badge badge-secondary ml-1" style="font-size:.7rem;">默认</span><?php endif; ?>
</td>
<td style="font-size:.82rem;">
<?php
$pfHost = '';
try {
$pfHost = DomainRepository::primaryHostnameForConfig($pdoDash ?? DomainRepository::pdo(), $pf);
} catch (Throwable $e) {
}
if ($pfHost !== '') {
echo '<span class="domain-host-badge" title="' . htmlspecialchars($pfHost, ENT_QUOTES) . '">' . htmlspecialchars($pfHost) . '</span>';
} else {
echo '<span class="text-muted cell-truncate" title="' . htmlspecialchars($dashboardDefaultHost, ENT_QUOTES) . '">' . htmlspecialchars($dashboardDefaultHost) . '<br><small>(默认后台域)</small></span>';
}
?>
</td>
<td>
<?php
$adLinks = dash_build_ad_links($pf, $protocol, $_SERVER['HTTP_HOST']);
foreach ($adLinks as $link):
?>
<div class="ad-link-row">
<span class="ad-link-label"><?php echo htmlspecialchars($link['label']); ?></span>
<div class="url-text flex-grow-1 url-text-truncate"><?php echo dash_truncate_attr($link['url'], 56); ?></div>
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy-ad" data-copy="<?php echo htmlspecialchars($link['url'], ENT_QUOTES); ?>">复制</button>
</div>
<?php endforeach; ?>
</td>
<td>
<?php if (!empty($pfp)) {
$first = is_array($pfp) ? $pfp[0] : $pfp;
echo '<div class="url-text url-text-truncate">' . dash_truncate_attr($first, 48) . '</div>';
if (is_array($pfp) && count($pfp) > 1) {
echo '<div class="url-text text-muted">+' . (count($pfp)-1) . ' 个链接</div>';
}
} else { ?>
<span class="text-muted" style="font-size:.82rem;">—</span>
<?php } ?>
</td>
<td>
<div class="action-btn-group">
<a href="<?php echo $current_name . '?action=edit_config&name=' . urlencode($pf); ?>" class="btn btn-sm btn-outline-primary">编辑</a>
<a href="<?php echo $current_name . '?action=edit_config&name=new_config&amp;copy_from=' . urlencode($pf); ?>" class="btn btn-sm btn-outline-info">复制</a>
2026-06-14 14:00:24 +08:00
<a href="/<?php echo $pf; ?>.php" target="_blank" class="btn btn-sm btn-outline-secondary">查看</a>
<?php if ($pf !== 'index'): ?>
<button class="btn btn-sm btn-outline-danger" onclick="if(confirm('确定删除配置「<?php echo htmlspecialchars($pf); ?>」?')) location='<?php echo $current_name . '?action=delete_config&name=' . $pf; ?>'">删除</button>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
2026-07-06 10:34:34 +08:00
<?php if ($configPage['total_pages'] > 1 || $configPage['total'] > 0): ?>
<div class="list-pagination">
<span>第 <?php echo (int) $configPage['page']; ?> / <?php echo (int) $configPage['total_pages']; ?> 页,共 <?php echo (int) $configPage['total']; ?> 条</span>
<?php if ($configPage['total_pages'] > 1): ?>
<nav aria-label="配置分页">
<ul class="pagination pagination-sm">
<?php
$curPage = (int) $configPage['page'];
$maxPage = (int) $configPage['total_pages'];
$prevPage = max(1, $curPage - 1);
$nextPage = min($maxPage, $curPage + 1);
?>
<li class="page-item<?php echo $curPage <= 1 ? ' disabled' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $prevPage)); ?>">上一页</a>
</li>
<?php for ($p = 1; $p <= $maxPage; $p++): ?>
<li class="page-item<?php echo $p === $curPage ? ' active' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $p)); ?>"><?php echo $p; ?></a>
</li>
<?php endfor; ?>
<li class="page-item<?php echo $curPage >= $maxPage ? ' disabled' : ''; ?>">
<a class="page-link" href="<?php echo htmlspecialchars(dash_list_url($current_name, $listSearchQ, $nextPage)); ?>">下一页</a>
</li>
</ul>
</nav>
<?php endif; ?>
</div>
<?php endif; ?>
</form>
2026-06-14 14:00:24 +08:00
</div>
<!-- 站点域名 -->
<div class="card shadow-sm card-shell mb-4" style="margin-top: 16px;">
<div class="card-header-custom">
<h5>站点域名</h5>
</div>
<div style="padding:16px 20px;">
<?php if ($cloakCfEnabled): ?>
<div class="alert alert-info mb-3" style="font-size:.88rem;">
2026-07-06 10:34:34 +08:00
<strong>Cloudflare 已启用:</strong>选择「自动」时将创建 Zone 并配置 DNS/SSL;选择「手动」时仅本地登记(您已自行配置 DNS)。
源站仍需在宝塔添加站点并监听 <code><?php echo htmlspecialchars(CloudflareConfig::serverIp() ?: 'SERVER_IP'); ?></code>。
2026-06-14 14:00:24 +08:00
</div>
<?php else: ?>
<div class="alert alert-warning mb-3" style="font-size:.88rem;">
2026-07-06 10:34:34 +08:00
<strong>提示:</strong>自动模式需先在 <code>cong.php</code> 配置 Cloudflare API Token 与 Account ID。若已自行完成 DNS 解析,请选择「手动」模式。
使用前请先在宝塔添加站点并开启 SSL。
2026-06-14 14:00:24 +08:00
</div>
<?php endif; ?>
2026-07-06 10:34:34 +08:00
<form method="post" action="<?php echo $current_name; ?>" class="mb-3">
2026-06-14 14:00:24 +08:00
<input type="hidden" name="action" value="add_domain">
2026-07-06 10:34:34 +08:00
<div class="form-group mb-2">
<label class="mb-1"><strong>域名列表</strong> <small class="text-muted">(每行一个,也支持逗号分隔)</small></label>
<textarea name="new_hostnames" class="form-control form-control-sm" rows="5" style="max-width:520px;font-family:monospace;" placeholder="shop.example.com&#10;api.example.com&#10;landing.example.com" required></textarea>
</div>
<div class="form-group mb-2">
<label class="d-block mb-1"><strong>DNS 配置方式</strong></label>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary active">
<input type="radio" name="dns_mode" value="auto" checked autocomplete="off"> 自动
</label>
<label class="btn btn-sm btn-outline-primary">
<input type="radio" name="dns_mode" value="manual" autocomplete="off"> 手动
</label>
</div>
<small class="text-muted d-block mt-1">
自动:通过 Cloudflare 创建 Zone 并配置 DNS/SSL(默认)。
手动:仅登记域名,不调用 Cloudflare,无需检测/刷新。
</small>
</div>
<button type="submit" class="btn btn-sm" style="background:#16baaa;color:#fff;">批量添加</button>
2026-06-14 14:00:24 +08:00
</form>
<?php if (empty($cloakSiteDomains)): ?>
<div class="empty-hint">尚未添加域名。未添加时,广告链接将使用当前后台访问域名:<code><?php echo htmlspecialchars($dashboardDefaultHost); ?></code></div>
<?php else: ?>
<div class="list-table-wrap">
<table class="table table-sm mb-0 list-table">
<thead><tr>
<th>域名</th>
<th>关联配置</th>
<?php if ($cloakCfEnabled): ?><th>Cloudflare</th><th>Nameserver</th><?php endif; ?>
<th style="width:140px;">操作</th>
</tr></thead>
<tbody>
<?php foreach ($cloakSiteDomains as $dom):
$cfStatus = $dom['cf_status'] ?? 'local';
$cfLabel = CloudflareConfig::statusLabel($cfStatus);
$badgeCls = 'secondary';
if ($cfStatus === 'pending_ns') { $badgeCls = 'warning'; }
2026-07-06 10:34:34 +08:00
elseif ($cfStatus === 'manual') { $badgeCls = 'secondary'; }
2026-06-14 14:00:24 +08:00
elseif ($cfStatus === 'provisioning') { $badgeCls = 'info'; }
elseif ($cfStatus === 'ready') { $badgeCls = 'success'; }
elseif ($cfStatus === 'error') { $badgeCls = 'danger'; }
$nsList = CloudflareConfig::parseNameservers($dom['cf_nameservers'] ?? '');
$delConfirm = '确定删除该域名?';
if ($cloakCfEnabled && !empty($dom['cf_zone_id'])) {
$delConfirm .= '将同时删除 Cloudflare Zone(不可恢复)。';
}
?>
<tr>
<td><span class="domain-host-badge" title="<?php echo htmlspecialchars($dom['hostname'], ENT_QUOTES); ?>"><?php echo htmlspecialchars($dom['hostname']); ?></span></td>
<td><?php
2026-07-06 10:34:34 +08:00
$boundConfigs = $domainConfigMap[(int) $dom['id']] ?? [];
if ($boundConfigs === []) {
2026-06-14 14:00:24 +08:00
echo '<span class="text-muted">未关联</span>';
2026-07-06 10:34:34 +08:00
} else {
foreach ($boundConfigs as $cfgName) {
echo '<span class="badge badge-light border mr-1">' . htmlspecialchars($cfgName) . '</span>';
}
2026-06-14 14:00:24 +08:00
}
?></td>
<?php if ($cloakCfEnabled): ?>
<td>
<span class="badge badge-<?php echo $badgeCls; ?>"><?php echo htmlspecialchars($cfLabel); ?></span>
<?php if ($cfStatus === 'error' && !empty($dom['cf_error'])): ?>
<br><small class="text-danger"><?php echo htmlspecialchars($dom['cf_error']); ?></small>
<?php endif; ?>
</td>
<td style="font-size:.8rem;max-width:220px;">
<?php if ($nsList): ?>
<?php foreach ($nsList as $ns): ?><div><span class="cell-truncate domain-host-badge" style="display:block;max-width:200px;" title="<?php echo htmlspecialchars($ns, ENT_QUOTES); ?>"><?php echo htmlspecialchars($ns); ?></span></div><?php endforeach; ?>
<?php else: ?>
<span class="text-muted">—</span>
<?php endif; ?>
</td>
<?php endif; ?>
<td class="text-nowrap">
<div class="action-btn-group">
<?php if ($cloakCfEnabled && in_array($cfStatus, ['pending_ns', 'provisioning', 'ready', 'error'], true)): ?>
<form method="post" action="<?php echo $current_name; ?>" class="d-inline">
<input type="hidden" name="action" value="check_domain">
<input type="hidden" name="domain_id" value="<?php echo (int)$dom['id']; ?>">
<button type="submit" class="btn btn-sm btn-outline-primary">检测/刷新</button>
</form>
<?php endif; ?>
<a href="<?php echo $current_name; ?>?action=delete_domain&amp;domain_id=<?php echo (int)$dom['id']; ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('<?php echo htmlspecialchars($delConfirm, ENT_QUOTES); ?>');">删除</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<?php else: ?>
<!-- ===================== 编辑视图 ===================== -->
<a href="<?php echo $current_name; ?>" class="back-link">← 返回配置列表</a>
<div class="card shadow-sm" style="border-radius:8px;overflow:hidden;">
<div class="card-header-custom">
<h5><?php echo $isNewConfigDraft ? '新增配置' : '编辑配置'; ?><?php echo htmlspecialchars($check_name); ?><?php if ($isNewConfigDraft): ?> <span class="badge badge-warning" style="font-size:.75rem;vertical-align:middle;">未保存</span><?php endif; ?><?php if ($isNewConfigDraft && $configCopySource !== ''): ?> <span class="badge badge-info" style="font-size:.75rem;vertical-align:middle;">从 <?php echo htmlspecialchars($configCopySource); ?> 复制</span><?php endif; ?></h5>
2026-06-14 14:00:24 +08:00
<div>
<?php if (empty($_SESSION["check_result"]) || $_SESSION["check_result"] != "true"): ?>
<a href="<?php echo $current_name; ?>?action=setuvi&res=true&name=<?php echo $check_name; ?>" class="btn btn-sm btn-outline-success mr-1">设置当前浏览器为真实访客</a>
<?php else: ?>
<a href="<?php echo $current_name; ?>?action=setuvi&res=false&name=<?php echo $check_name; ?>" class="btn btn-sm btn-outline-warning mr-1">清除访客状态</a>
<?php endif; ?>
</div>
</div>
<form action="<?php echo $current_name; ?>?action=submit" method="post" id="configForm">
<div style="padding:16px;">
<!-- ===== 基本设置 ===== -->
<div class="form-section">
<button type="button" class="section-toggle" data-target="sec-basic">
基本设置 <span class="arrow">▼</span>
</button>
<div id="sec-basic">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell"><strong>配置名称</strong></td>
<td class="form-val-cell">
<input type="text" name="check_name" class="form-control form-control-sm" value="<?php echo htmlspecialchars($check_name); ?>" style="max-width:300px;">
<small class="text-muted"><?php echo ($isNewConfigDraft && $configCopySource !== '') ? '已按「' . htmlspecialchars($configCopySource) . '」预填,请确认配置名称后保存' : '示例:f123、d258'; ?></small>
2026-06-14 14:00:24 +08:00
</td>
</tr>
<tr>
<td class="form-label-cell">
<strong>关联域名</strong><br>
<small class="text-muted">请先在上方「站点域名」中添加并在宝塔创建该站点与 SSL</small>
</td>
<td class="form-val-cell">
<select name="site_domain_id" class="form-control form-control-sm" style="max-width:360px;">
<option value="0"<?php echo $selectedDomainId === 0 ? ' selected' : ''; ?>>
不指定(广告链接使用当前后台域名:<?php echo htmlspecialchars($dashboardDefaultHost); ?>
</option>
2026-07-06 10:34:34 +08:00
<?php if (count($cloakSiteDomains) >= 2 && ($isNewConfigDraft || $selectedDomainId === 0)): ?>
<option value="random">随机域名(从全部站点域名中随机分配)</option>
<?php endif; ?>
<?php foreach ($cloakSiteDomains as $dom):
$boundOnDom = $domainConfigMap[(int) $dom['id']] ?? [];
$boundCount = count($boundOnDom);
?>
2026-06-14 14:00:24 +08:00
<option value="<?php echo (int) $dom['id']; ?>"<?php echo $selectedDomainId === (int) $dom['id'] ? ' selected' : ''; ?>>
<?php echo htmlspecialchars($dom['hostname']); ?>
2026-07-06 10:34:34 +08:00
<?php if ($boundCount > 0): ?>
<?php echo $boundCount; ?> 个配置已关联)
2026-06-14 14:00:24 +08:00
<?php endif; ?>
</option>
<?php endforeach; ?>
</select>
<?php if (empty($cloakSiteDomains)): ?>
<small class="text-warning d-block mt-1">尚未添加任何域名,保存后广告链接将使用 <?php echo htmlspecialchars($dashboardDefaultHost); ?></small>
<?php endif; ?>
</td>
</tr>
<tr>
<td class="form-label-cell">
是否开启 DEBUG 模式<br>
<small class="text-muted">开启后访问落地链接将显示格式化调试页(判定结果、理由、代码行、访客信息),不会跳转真实页/安全页;生产环境请关闭</small>
</td>
<td class="form-val-cell"><?php echo onoff_radio('debug_mode', $_d('CLOAK_DEBUG_MODE','OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">屏蔽模式</td>
<td class="form-val-cell">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-sm btn-outline-primary <?php if(SHOW_SITE_MODE_SWITCH == 'ip_check') echo 'active'; ?>">
<input type="radio" name="methods" value="ip_check" <?php if(SHOW_SITE_MODE_SWITCH == 'ip_check') echo 'checked'; ?>> 正常屏蔽模式
</label>
<label class="btn btn-sm btn-outline-primary <?php if(SHOW_SITE_MODE_SWITCH == 'fp') echo 'active'; ?>">
<input type="radio" name="methods" value="fp" <?php if(SHOW_SITE_MODE_SWITCH == 'fp') echo 'checked'; ?>> 真实页模式
</label>
<label class="btn btn-sm btn-outline-primary <?php if(SHOW_SITE_MODE_SWITCH == 'zp') echo 'active'; ?>">
<input type="radio" name="methods" value="zp" <?php if(SHOW_SITE_MODE_SWITCH == 'zp') echo 'checked'; ?>> 安全页模式
</label>
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">
国家条件<br>
<small class="text-muted">开启后仅允许填写的国家访问真实页;关闭则不做国家拦截。</small>
</td>
<td class="form-val-cell">
<?php
$countryCodesVal = defined('SHOW_SITE_COUNTRY')
? CountryAllowlist::formatForConfig((string) SHOW_SITE_COUNTRY)
: '';
$countryOnVal = $countryCodesVal !== '' ? 'ON' : 'OFF';
echo onoff_radio('country_on', $countryOnVal, '开启', '关闭');
?>
</td>
</tr>
<tr id="countryCodesRow"<?php echo $countryOnVal === 'ON' ? '' : ' style="display:none;"'; ?>>
<td class="form-label-cell">允许的国家代码(多个使用英文逗号隔开)</td>
<td class="form-val-cell">
<input type="text" name="country" class="form-control form-control-sm" value="<?php echo htmlspecialchars($countryCodesVal); ?>" placeholder="如:US,GB,CA(小写会自动转大写;支持中英文逗号、分号)" style="max-width:360px;">
</td>
</tr>
2026-06-14 14:00:24 +08:00
<tr>
<td class="form-label-cell">广告策略 ID</td>
<td class="form-val-cell"><input type="text" name="costm_ip_score" class="form-control form-control-sm" value="<?php echo COSTM_IP_SCORE; ?>" style="max-width:360px;"></td>
</tr>
<tr>
<td class="form-label-cell">安全页跳转方式</td>
<td class="form-val-cell">
<select name="redirect_method" class="form-control form-control-sm" style="max-width:320px;">
<option value="curl" <?php if(CLOAK_REDIRECT_METHOD=='curl') echo 'selected'; ?>>抓取显示(链接不变)</option>
<option value="302" <?php if(CLOAK_REDIRECT_METHOD=='302') echo 'selected'; ?>>302 跳转(域名改变)</option>
<option value="js" <?php if(CLOAK_REDIRECT_METHOD=='js') echo 'selected'; ?>>JS 跳转</option>
</select>
</td>
</tr>
<tr>
<td class="form-label-cell">安全页链接(留空显示空白页)</td>
<td class="form-val-cell"><input type="text" name="DB_zp" class="form-control form-control-sm" value="<?php echo DB_ZP; ?>" placeholder="https://..." style="max-width:420px;"></td>
</tr>
<tr>
<td class="form-label-cell">真实页跳转方式</td>
<td class="form-val-cell">
<select name="show_content" class="form-control form-control-sm" style="max-width:320px;">
<option value="curl" <?php if(CLOAK_SHOW_CONTENT=='curl') echo 'selected'; ?>>抓取显示(链接不变)</option>
<option value="302" <?php if(CLOAK_SHOW_CONTENT=='302') echo 'selected'; ?>>302 跳转(域名改变)</option>
<option value="js" <?php if(CLOAK_SHOW_CONTENT=='js') echo 'selected'; ?>>JS 跳转</option>
</select>
</td>
</tr>
<tr>
<td class="form-label-cell">真实页链接地址</td>
<td class="form-val-cell" id="db_fp">
<?php
if (DB_FP) {
foreach (DB_FP as $idx => $link) {
if ($idx == 0) {
echo '<input type="text" name="DB_fp[]" class="form-control form-control-sm mb-1" value="' . htmlspecialchars($link) . '" style="max-width:420px;" data-idx="1"> <a href="javascript:void(0);" id="addNewLinkBtn" class="btn btn-sm btn-outline-secondary">+ 新增</a>';
} else {
echo '<br><input type="text" name="DB_fp[]" class="form-control form-control-sm mt-1" value="' . htmlspecialchars($link) . '" style="max-width:420px;" data-idx="' . ($idx+1) . '"> <a href="javascript:void(0);" class="removeLinkBtn btn btn-sm btn-outline-danger">删除</a>';
}
}
} else {
echo '<input type="text" name="DB_fp[]" class="form-control form-control-sm" value="" style="max-width:420px;" data-idx="1"> <a href="javascript:void(0);" id="addNewLinkBtn" class="btn btn-sm btn-outline-secondary">+ 新增</a>';
}
?>
</td>
</tr>
</table>
</div>
</div>
<!-- ===== IP 与名单 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-ip">
IP黑白单管理 <span class="arrow">▼</span>
</button>
<div id="sec-ip" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">白名单 IP 组(允许访问真实页)</td>
<td class="form-val-cell">
<select name="whitelist_group_id" class="form-control form-control-sm" style="max-width:320px;">
<option value="0" <?php if($currentWhitelistGroupId===0) echo 'selected'; ?>>无</option>
<?php foreach($whiteGroups as $wg): ?>
<option value="<?php echo (int)$wg['id']; ?>" <?php if($currentWhitelistGroupId===(int)$wg['id']) echo 'selected'; ?>><?php echo htmlspecialchars($wg['name']); ?> (ID <?php echo $wg['id']; ?>)</option>
<?php endforeach; ?>
</select>
<small><a href="/admin_ip_manager.php?type=white" target="_blank">管理白名单</a></small>
</td>
</tr>
<tr>
<td class="form-label-cell">黑名单 IP 组(命中则拦截)</td>
<td class="form-val-cell">
<select name="blacklist_group_id" class="form-control form-control-sm" style="max-width:320px;">
<option value="0" <?php if($currentBlacklistGroupId===0) echo 'selected'; ?>>无</option>
<?php foreach($blackGroups as $bg): ?>
<option value="<?php echo (int)$bg['id']; ?>" <?php if($currentBlacklistGroupId===(int)$bg['id']) echo 'selected'; ?>><?php echo htmlspecialchars($bg['name']); ?> (ID <?php echo $bg['id']; ?>)</option>
<?php endforeach; ?>
</select>
<small><a href="/admin_ip_manager.php?type=black" target="_blank">管理黑名单</a></small>
</td>
</tr>
<tr>
<td class="form-label-cell">自动黑名单(达到次数后自动加入黑名单组)</td>
<td class="form-val-cell">
<div class="d-flex align-items-center gap-2" style="gap:10px;">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
2026-07-06 10:34:34 +08:00
<label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK=='ON') echo 'active'; ?>">
2026-06-14 14:00:24 +08:00
<input type="radio" name="auto_black" id="auto_black_on" value="ON" <?php if(AUTO_BLACK=='ON') echo 'checked'; ?>> 开启
</label>
2026-07-06 10:34:34 +08:00
<label class="btn btn-sm btn-outline-primary <?php if(AUTO_BLACK!='ON') echo 'active'; ?>">
2026-06-14 14:00:24 +08:00
<input type="radio" name="auto_black" id="auto_black_off" value="OFF" <?php if(AUTO_BLACK!='ON') echo 'checked'; ?>> 关闭
</label>
</div>
<span class="ml-2">触发次数:</span>
<input type="number" name="auto_black_times" value="<?php echo AUTO_BLACK_TIMES; ?>" class="form-control form-control-sm" style="width:80px;" min="1">
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">白名单链接参数(携带此参数直接放行,格式:param=value</td>
<td class="form-val-cell"><input type="text" name="white_params" class="form-control form-control-sm" value="<?php echo htmlspecialchars(WHITE_PARAMS); ?>" style="max-width:280px;" placeholder="如:token=abc123"></td>
</tr>
</table>
</div>
</div>
<!-- ===== 设备与语言 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-device">
设备与语言过滤 <span class="arrow">▼</span>
</button>
<div id="sec-device" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">屏蔽无 Referer 来源(防爬虫)</td>
<td class="form-val-cell"><?php echo onoff_radio('v_referer', CLOAK_V_REFERER); ?></td>
</tr>
<tr>
<td class="form-label-cell">仅允许移动端访问</td>
<td class="form-val-cell"><?php echo onoff_radio('mobile_on', CLOAK_MOBILE_ON); ?></td>
</tr>
<tr>
<td class="form-label-cell">屏蔽中文浏览器</td>
<td class="form-val-cell"><?php echo onoff_radio('zh_on', CLOAK_ZH_ON); ?></td>
</tr>
<tr>
<td class="form-label-cell">仅允许 iOS / Android 系统</td>
<td class="form-val-cell"><?php echo onoff_radio('os_on', CLOAK_OS_ON); ?></td>
</tr>
<tr>
<td class="form-label-cell">保留跳转链接参数</td>
<td class="form-val-cell"><?php echo onoff_radio('keep_params', KEEP_PARAMS); ?></td>
</tr>
<tr>
<td class="form-label-cell">屏蔽 iPhone 型号</td>
<td class="form-val-cell">
<select name="iphone_model" class="form-control form-control-sm" style="max-width:260px;">
<option value="0" <?php if(IPHONE_MODEL=='0') echo 'selected'; ?>>不屏蔽</option>
<option value="8" <?php if(IPHONE_MODEL=='8') echo 'selected'; ?>>屏蔽 iPhone 8 及以下</option>
<option value="10" <?php if(IPHONE_MODEL=='10') echo 'selected'; ?>>屏蔽 iPhone X 及以下</option>
<option value="11" <?php if(IPHONE_MODEL=='11') echo 'selected'; ?>>屏蔽 iPhone 11 及以下</option>
<option value="12" <?php if(IPHONE_MODEL=='12') echo 'selected'; ?>>屏蔽 iPhone 12 及以下</option>
</select>
</td>
</tr>
<tr>
<td class="form-label-cell">
虚拟设备检测<br>
<small class="text-muted">简单:全平台自动化/爬虫;标准:+环境一致性(移动无触摸等);严格:+移动GPU校验与传感器(约180ms</small>
</td>
<td class="form-val-cell">
<select name="is_virtual" class="form-control form-control-sm" style="max-width:260px;">
<option value="0" <?php if(IS_VIRTUAL=='0') echo 'selected'; ?>>不检测</option>
<option value="1" <?php if(IS_VIRTUAL=='1') echo 'selected'; ?>>简单</option>
<option value="2" <?php if(IS_VIRTUAL=='2') echo 'selected'; ?>>标准</option>
<option value="3" <?php if(IS_VIRTUAL=='3') echo 'selected'; ?>>严格</option>
</select>
</td>
</tr>
</table>
</div>
</div>
<!-- ===== 广告来源限制 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-ad-source">
广告来源限制 <span class="arrow">▼</span>
</button>
<div id="sec-ad-source" style="display:none;">
<table class="table table-sm mb-0">
<tr>
<td class="form-label-cell">Facebook</td>
<td class="form-val-cell"><?php echo onoff_radio('ad_fb', $_d('CLOAK_AD_FB', 'OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">Google</td>
<td class="form-val-cell"><?php echo onoff_radio('ad_google', $_d('CLOAK_AD_GOOGLE', 'OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">TikTok</td>
<td class="form-val-cell"><?php echo onoff_radio('ad_tiktok', $_d('CLOAK_AD_TIKTOK', 'OFF')); ?></td>
</tr>
<tr>
<td class="form-label-cell">严格模式</td>
<td class="form-val-cell">
<?php echo onoff_radio('ad_strict', $_d('CLOAK_AD_STRICT', 'OFF')); ?>
<div class="text-muted mt-1" style="font-size:.78rem;">开启后,未通过广告来源验证的访问将直接跳转安全页;关闭时仅追加「未确定流量来源」并继续后续判定,日志行显示为黄色。</div>
</td>
</tr>
<tr>
<td class="form-label-cell">强验证参数 ad_spm_id</td>
<td class="form-val-cell">
<?php $spmShow = $_d('CLOAK_AD_SPM_ID', ''); ?>
<?php if ($spmShow !== ''): ?>
<code><?php echo htmlspecialchars($spmShow); ?></code>
<div class="text-muted mt-1" style="font-size:.78rem;">保存配置且开启任一广告平台时自动生成,写入广告链接用于强验证。</div>
<?php else: ?>
<span class="text-muted">保存并开启任一广告平台后自动生成</span>
<?php endif; ?>
</td>
</tr>
</table>
</div>
</div>
<!-- ===== 高级设置 ===== -->
<div class="form-section">
<button type="button" class="section-toggle collapsed" data-target="sec-advanced">
高级设置 <span class="arrow">▼</span>
</button>
<div id="sec-advanced" style="display:none;">
<table class="table table-sm mb-0">
<tr>
2026-06-14 18:30:06 +08:00
<td class="form-label-cell">指纹风险系数(0=关闭,最大90</td>
<td class="form-val-cell">
<input type="number" name="risk_number" class="form-control form-control-sm" value="<?php echo CLOAK_RISK_NUMBER; ?>" style="width:100px;" min="0" max="90">
<div class="text-muted mt-2" style="font-size:.78rem;line-height:1.55;">
数值越高,拦截越严格。访客风险分达到或超过此值时跳转安全页,低于则进入真实页。
设为 <strong>0</strong> 表示关闭二次风控。
</div>
<details class="mt-2" style="font-size:.78rem;line-height:1.55;">
<summary style="cursor:pointer;color:#0c5460;font-weight:600;">推荐设置(与下方「加强判断」配合)</summary>
<div class="text-muted mt-2">
<p class="mb-2">请根据「加强判断」开关选择对应数值,两种模式<strong>不宜使用同一分数</strong>。</p>
<table class="table table-sm table-bordered mb-2" style="font-size:.76rem;background:#fff;">
<thead class="thead-light">
<tr><th>加强判断</th><th>建议分数</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><strong>OFF</strong></td>
<td><strong>68</strong></td>
<td>默认推荐,兼顾流量与防护,误拦较少</td>
</tr>
<tr>
<td><strong>ON</strong></td>
<td><strong>52</strong></td>
<td>识别更严,适合需要更高防护的场景</td>
</tr>
</tbody>
</table>
<p class="mb-1"><strong>微调:</strong></p>
<ul class="mb-0 pl-3">
<li>真实用户被误拦 → 分数 <strong>+3 ~ +5</strong></li>
<li>可疑访问漏过 → 分数 <strong>3 ~ 5</strong></li>
<li>可在「访问日志 → 显示更多 → 判定详情」查看每条记录的风险分与拦截原因</li>
</ul>
</div>
</details>
</td>
2026-06-14 14:00:24 +08:00
</tr>
<tr>
<td class="form-label-cell">加强判断</td>
<td class="form-val-cell">
<?php echo onoff_radio('risk_enhanced', $_d('CLOAK_RISK_ENHANCED', 'OFF')); ?>
2026-06-14 18:30:06 +08:00
<div class="text-muted mt-1" style="font-size:.78rem;line-height:1.55;">
开启后识别更精细,可拦截更多自动化访问;老旧设备或弱网环境下可能受影响。<br>
建议配合上方风险系数:OFF 用 <strong>68</strong>ON 用 <strong>52</strong>。
</div>
2026-06-14 14:00:24 +08:00
</td>
</tr>
<tr>
<td class="form-label-cell">
临时链接参数有效时长(分钟,0=关闭)<br>
<small class="text-muted">通过广告来源限制规则(ad_spm_id / clid / Referer / UA 等)后开始计时,有效时长内放行;到期重新判断。未开启广告平台时仍按 clid/ad_spm_id 触发。</small>
</td>
<td class="form-val-cell">
<div class="d-flex align-items-center" style="gap:8px;">
<input type="number" name="url_args_timeout" class="form-control form-control-sm" value="<?php echo CLOAK_URL_ARGS_TIMEOUT; ?>" style="width:90px;" min="0"> 分钟
<button type="button" class="btn btn-sm btn-outline-warning" onclick="delUrlArgs()">重置计时</button>
</div>
</td>
</tr>
<tr>
<td class="form-label-cell">记录访客日志</td>
<td class="form-val-cell">
<div class="btn-group btn-group-toggle" data-toggle="buttons">
2026-07-06 10:34:34 +08:00
<label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG=='1') echo 'active'; ?>">
2026-06-14 14:00:24 +08:00
<input type="radio" name="write_log" value="1" <?php if(WRITE_LOG=='1') echo 'checked'; ?>> 开启
</label>
2026-07-06 10:34:34 +08:00
<label class="btn btn-sm btn-outline-primary <?php if(WRITE_LOG!='1') echo 'active'; ?>">
2026-06-14 14:00:24 +08:00
<input type="radio" name="write_log" value="0" <?php if(WRITE_LOG!='1') echo 'checked'; ?>> 关闭
</label>
</div>
</td>
</tr>
</table>
</div>
</div>
</div><!-- /padding -->
<!-- 底部操作栏 -->
<div class="sticky-actions">
<button type="submit" class="btn btn-primary">保存配置</button>
<a href="<?php echo $current_name . '?action=edit_config&name=' . $check_name; ?>" class="btn btn-outline-secondary">重置</a>
<a href="/table" target="_blank" class="btn btn-outline-info">查看日志</a>
<a href="/log_analytics.php" target="_blank" class="btn btn-outline-info">数据分析</a>
<a href="/share_log.php" target="_blank" class="btn btn-outline-info">分享日志</a>
</div>
</form>
</div>
<?php endif; ?>
</div><!-- /page-body -->
<script>
$(function() {
// 手风琴展开/折叠
$('.section-toggle').on('click', function() {
var target = '#' + $(this).data('target');
$(this).toggleClass('collapsed');
$(target).slideToggle(180);
});
function syncCountryCodesRow() {
var on = $('input[name="country_on"]:checked').val() === 'ON';
$('#countryCodesRow').toggle(on);
if (!on) {
$('input[name="country"]').val('');
}
}
$(document).on('change', 'input[name="country_on"]', syncCountryCodesRow);
syncCountryCodesRow();
2026-06-14 14:00:24 +08:00
// 动态添加链接
var linkIdx = 1;
$(document).on('click', '#addNewLinkBtn', function() {
linkIdx++;
var html = '<br><input type="text" name="DB_fp[]" class="form-control form-control-sm mt-1" value="" style="max-width:420px;" data-idx="' + linkIdx + '"> <a href="javascript:void(0);" class="removeLinkBtn btn btn-sm btn-outline-danger">删除</a>';
$(this).after(html);
});
$(document).on('click', '.removeLinkBtn', function() {
$(this).prev('br').remove();
$(this).prev('input').remove();
$(this).remove();
});
// 自动黑名单校验
$('#configForm').on('submit', function() {
var on = $('#auto_black_on').is(':checked');
var gid = parseInt($('select[name=blacklist_group_id]').val(), 10) || 0;
if (on && gid <= 0) {
alert('开启「自动黑名单」时,必须先选择「黑名单 IP 组」。');
return false;
}
return true;
});
// 广告链接复制
$(document).on('click', '.btn-copy-ad', function() {
var text = $(this).attr('data-copy') || '';
if (!text) return;
var btn = this;
var done = function() {
var orig = btn.textContent;
btn.textContent = '已复制';
setTimeout(function() { btn.textContent = orig; }, 1200);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done).catch(function() {
window.prompt('请手动复制链接:', text);
});
} else {
window.prompt('请手动复制链接:', text);
}
});
2026-07-06 10:34:34 +08:00
$('#checkAllConfigs').on('change', function() {
$('.config-row-check').prop('checked', this.checked);
});
$(document).on('change', '.config-row-check', function() {
var all = $('.config-row-check');
var checked = all.filter(':checked').length;
$('#checkAllConfigs').prop('checked', all.length > 0 && checked === all.length);
});
2026-06-14 14:00:24 +08:00
});
2026-07-06 10:34:34 +08:00
function dashBatchDeleteConfirm() {
var checked = document.querySelectorAll('.config-row-check:checked');
if (checked.length === 0) {
alert('请至少选择一个配置');
return false;
}
return confirm('确定删除选中的 ' + checked.length + ' 个配置?此操作不可恢复。');
}
2026-06-14 14:00:24 +08:00
function delUrlArgs() {
if (!confirm('确认重置临时链接参数计时?')) return;
window.location.href = '<?php echo $current_name; ?>?action=delUrlArgs&check_name=<?php echo urlencode($check_name); ?>';
}
</script>
</body>
</html>