diff --git a/.github/workflows/proxy-dashboard-update.yml b/.github/workflows/proxy-dashboard-update.yml index acdaeae8..f9828522 100644 --- a/.github/workflows/proxy-dashboard-update.yml +++ b/.github/workflows/proxy-dashboard-update.yml @@ -2,9 +2,10 @@ # 🔺 Sovereign: TCS-0002∞ | Root: SYS-GLW-0001 # 📜 Copyright: 国作登字-2026-A-00037559 # ═══════════════════════════════════════════════ -# 📊 铸渊专线 · 每日仪表盘自动更新 +# 📊 铸渊专线 · 每6小时仪表盘自动更新 # -# 每天UTC 0:00 (北京时间08:00) 自动更新README仪表盘 +# 每6小时自动更新README仪表盘 (UTC 0/6/12/18) +# VPN系统代码变更时也自动触发 # 从SG服务器获取最新流量数据 # ═══════════════════════════════════════════════ @@ -12,8 +13,12 @@ name: '📊 铸渊专线 · 仪表盘' on: schedule: - - cron: '0 0 * * *' # 每天UTC 0:00 = 北京08:00 + - cron: '0 */6 * * *' # 每6小时自动更新 (UTC 0/6/12/18 = 北京 8/14/20/2) workflow_dispatch: # 手动触发 + push: + branches: [main] + paths: + - 'server/proxy/**' # VPN系统代码变更时自动触发 permissions: contents: write diff --git a/server/proxy/service/subscription-server-v3.js b/server/proxy/service/subscription-server-v3.js index e82e1a40..812c0bae 100644 --- a/server/proxy/service/subscription-server-v3.js +++ b/server/proxy/service/subscription-server-v3.js @@ -307,19 +307,33 @@ ${nodeNames} : ` - "♻️ 自动选择"\n${nodeNames}`) : nodeNames; - // Claude专线独立代理组: 始终显示为下拉菜单 (select类型) - // 无论是否有硅谷节点,都创建Claude专线组,让用户手动选择 - // 有SV节点时优先SV,无SV节点时使用现有节点 - const claudeProxies = svNode + // Claude专线独立代理组: 双组设计 — fallback自动切换 + select手动覆盖 + // 1. fallback组: 优先SV节点,SV不可用时自动回落到其他节点(解决"切不过去"问题) + // 2. select组: 用户可手动选择具体走哪个节点 + // 有SV节点时优先SV,无SV节点时自动使用现有节点 + const claudeFallbackProxies = svNode + ? ` - "${svNode.name}"\n${nodes.filter(n => n.region !== 'us-sv').map(n => ` - "${n.name}"`).join('\n')}` + : nodeNames; + + const claudeSelectProxies = svNode ? ` - "${svNode.name}"\n${nodes.length > 1 ? ' - "♻️ 自动选择"\n' : ''}${nodeNames}` : (nodes.length > 1 ? ` - "♻️ 自动选择"\n${nodeNames}` : nodeNames); - // Claude专线代理组YAML块 — 始终生成 (下拉选择·select类型) + // Claude专线代理组YAML块 — fallback类型自动切换 + select手动覆盖 const claudeGroupBlock = ` - name: "🇺🇸 Claude专线" + type: fallback + proxies: +${claudeFallbackProxies} + url: "https://claude.ai" + interval: 180 + timeout: 5 + + - name: "🇺🇸 Claude手动" type: select proxies: -${claudeProxies} + - "🇺🇸 Claude专线" +${claudeSelectProxies} `; return `# 光湖语言世界 · ${user.label} 的独立专线 — 冰朔开发维护 @@ -452,9 +466,10 @@ ${toolProxies} # ── 路由规则 ────────────────────────────── rules: - # Claude专线 (独立代理组·下拉选择·始终可选) - - DOMAIN-SUFFIX,claude.ai,🇺🇸 Claude专线 - - DOMAIN-SUFFIX,anthropic.com,🇺🇸 Claude专线 + # Claude专线 (fallback自动切换·优先硅谷·不可用时自动回落) + # 用户可在VPN客户端选择「Claude手动」切换到指定节点 + - DOMAIN-SUFFIX,claude.ai,🇺🇸 Claude手动 + - DOMAIN-SUFFIX,anthropic.com,🇺🇸 Claude手动 # AI服务 (其他AI) - DOMAIN-SUFFIX,openai.com,🤖 AI服务 @@ -778,6 +793,97 @@ mode: direct return; } + // ── V3 流量仪表盘 实时数据API: /dashboard-api/{token} ── + // 仪表盘页面通过此API获取最新数据,页面刷新时自动拉取 + const dashApiMatch = pathname.match(/^\/dashboard-api\/([a-f0-9]+)$/); + if (dashApiMatch && req.method === 'GET') { + const token = dashApiMatch[1]; + const user = userManager.findUserByToken(token); + + if (!user) { + res.writeHead(403, { 'Content-Type': 'application/json; charset=utf-8' }); + res.end(JSON.stringify({ error: true, code: 'FORBIDDEN', message: '认证失败' })); + return; + } + + const poolStatus = userManager.getPoolStatus(); + const userUsedGB = ((user.traffic.upload_bytes + user.traffic.download_bytes) / (1024 ** 3)).toFixed(2); + const nodes = buildVpnNodes(); + const poolUsedGB = (typeof poolStatus.pool_used_gb === 'number' ? poolStatus.pool_used_gb : 0).toFixed(1); + const poolPct = Math.min(typeof poolStatus.pool_percentage === 'number' ? poolStatus.pool_percentage : 0, 100).toFixed(1); + const svNodeApi = nodes.find(n => n.region === 'us-sv'); + + // 读取反向加速状态 + let boostStatusApi = '未检测'; + try { + const bs = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'reverse-boost-status.json'), 'utf8')); + boostStatusApi = bs.current?.bbr?.is_bbr ? '✅ BBR加速中' : '⚠️ 未加速'; + } catch { /* ignore */ } + + // 读取用户带宽共享状态 + let bwApiStatus = { active: false, status: '未加入', ip_authorized: false, auth_time: '' }; + try { + const bwPool = require('./bandwidth-pool-agent'); + const contribInfo = bwPool.isContributor(user.email); + if (contribInfo.is_contributor) { + bwApiStatus.active = contribInfo.status === 'active'; + bwApiStatus.ip_authorized = true; + bwApiStatus.status = bwApiStatus.active ? '🚀 加速已生效' : '⚠️ 已暂停'; + bwApiStatus.auth_time = contribInfo.authorized_at || ''; + } + } catch { /* ignore */ } + + // 读取带宽池状态 + let bwPoolApiInfo = { total_contributors: 0, active_contributors: 0, total_contributed_gb: 0, pool_status: 'idle' }; + try { + bwPoolApiInfo = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'bandwidth-pool-status.json'), 'utf8')); + } catch { /* ignore */ } + + // 读取今日流量 + let todayGBApi = '—'; + try { + const snap = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'daily-traffic-snapshot.json'), 'utf8')); + const today = new Date().toISOString().slice(0, 10); + if (snap.date === today && snap.per_user?.[user.email]) { + todayGBApi = snap.per_user[user.email].total_gb.toFixed(2); + } + } catch { /* ignore */ } + + res.writeHead(200, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-cache, no-store, must-revalidate' + }); + res.end(JSON.stringify({ + success: true, + accel: { + active: bwApiStatus.active, + ip_authorized: bwApiStatus.ip_authorized, + status: bwApiStatus.status, + auth_time: bwApiStatus.auth_time, + boost: boostStatusApi, + pool_active: bwPoolApiInfo.active_contributors || 0, + pool_total: bwPoolApiInfo.total_contributors || 0 + }, + traffic: { + today_gb: todayGBApi, + user_gb: userUsedGB, + pool_used_gb: poolUsedGB, + pool_total_gb: poolStatus.pool_total_gb, + pool_pct: poolPct + }, + nodes: nodes.map(n => ({ name: n.name, region: n.region, latency_ms: n.latency_ms || null })), + system: { + version: 'V3.0', + users_count: poolStatus.users_count, + smart_routing: nodes.length > 1 ? '✅ url-test' : '单节点', + claude_line: svNodeApi ? '✅ 硅谷节点在线' : '🔄 自动回落(新加坡)', + sv_available: !!svNodeApi + }, + updated_at: new Date().toISOString() + })); + return; + } + // ── V3 流量仪表盘: /dashboard/{token} ────── // 手机浏览器可查看 · 流量/节点/系统状态 const dashMatch = pathname.match(/^\/dashboard\/([a-f0-9]+)$/); @@ -887,7 +993,7 @@ mode: direct
IP授权${bwIpAuthorized ? '✅ 已授权' : '❌ 未授权'}
带宽加速${bwContribStatus}
网速增强${bwContribActive ? '✅ 已开启' : '⚠️ 未开启'}
-
反向加速${boostStatus}
+
反向加速${boostStatus}
加速池人数${bwPoolInfo.active_contributors}人在线 / ${bwPoolInfo.total_contributors}人
${bwContribAuthTime ? '
授权时间' + new Date(bwContribAuthTime).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + '
' : ''}
@@ -897,27 +1003,29 @@ mode: direct

📊 流量概览

-
今日用量${todayGB} GB
-
个人本月${userUsedGB} GB
-
流量池${poolUsedGB} / ${poolStatus.pool_total_gb} GB
+
今日用量${todayGB} GB
+
个人本月${userUsedGB} GB
+
流量池${poolUsedGB} / ${poolStatus.pool_total_gb} GB
-
${poolPct}%
+
${poolPct}%
重置日期每月1号
-

🔌 节点状态 (${nodes.length}个)

+

🔌 节点状态 (${nodes.length}个)

+
${nodes.map(n => `
${esc(n.name)}${n.latency_ms ? n.latency_ms + 'ms' : '在线'}
`).join('\n ')} - ${svNode ? '
🇺🇸 Claude专线可用 · 在VPN软件中选择「Claude专线」分组即可精准切换
' : ''} +
+
${svNode ? '
🇺🇸 Claude专线可用 · 硅谷节点优先 · 不可用时自动回落到其他节点
' : '
🇺🇸 Claude专线: 硅谷节点离线 · 已自动回落到新加坡节点 · 在「Claude手动」中可切换
'}

⚡ 系统状态

服务版本V3.0
-
在线用户${poolStatus.users_count}
-
智能选路${nodes.length > 1 ? '✅ url-test' : '单节点'}
-
Claude专线${svNode ? '✅ 硅谷节点在线' : '⚠️ 暂不可用'}
+
在线用户${poolStatus.users_count}
+
智能选路${nodes.length > 1 ? '✅ url-test' : '单节点'}
+
Claude专线${svNode ? '✅ 硅谷节点在线' : '🔄 自动回落(新加坡)'}
@@ -960,7 +1068,7 @@ mode: direct