- sing-box 单进程 bind 数据网卡 - mixed HTTP+SOCKS + 中文静态面板 - install.sh 支持 curl | sudo bash - 512MB 友好 MemoryMax 默认 96MB
216 lines
9.3 KiB
HTML
216 lines
9.3 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>数据出口代理 · 控制台</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0f1419; --card: #1a2332; --text: #e7ecf3; --muted: #8b9bb4;
|
||
--accent: #3d8bfd; --ok: #3dd68c; --warn: #f5a524; --bad: #f31260; --border: #2a3548;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||
background: var(--bg); color: var(--text); min-height: 100vh; }
|
||
header { padding: 16px 20px; border-bottom: 1px solid var(--border);
|
||
display: flex; flex-wrap: wrap; gap: 12px; align-items: center; justify-content: space-between; }
|
||
h1 { font-size: 1.15rem; margin: 0; font-weight: 600; }
|
||
.muted { color: var(--muted); font-size: 0.85rem; }
|
||
main { padding: 16px; max-width: 960px; margin: 0 auto; display: grid; gap: 14px; }
|
||
.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; }
|
||
.row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
||
label { font-size: 0.85rem; color: var(--muted); }
|
||
input, button { background: #0d121a; color: var(--text); border: 1px solid var(--border);
|
||
border-radius: 8px; padding: 8px 10px; font-size: 0.95rem; }
|
||
input { min-width: 160px; }
|
||
button { cursor: pointer; background: var(--accent); border: none; font-weight: 600; }
|
||
button.secondary { background: #2a3548; }
|
||
button.danger { background: var(--bad); }
|
||
.pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.8rem; background: #243044; }
|
||
.pill.ok { background: rgba(61,214,140,.15); color: var(--ok); }
|
||
.pill.bad { background: rgba(243,18,96,.15); color: var(--bad); }
|
||
.pill.warn { background: rgba(245,165,36,.15); color: var(--warn); }
|
||
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
|
||
th, td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--border); }
|
||
th { color: var(--muted); font-weight: 500; }
|
||
pre { margin: 0; white-space: pre-wrap; word-break: break-all; font-size: 0.8rem;
|
||
color: var(--muted); max-height: 220px; overflow: auto; }
|
||
.banner { background: rgba(61,139,253,.12); border: 1px solid rgba(61,139,253,.35);
|
||
border-radius: 10px; padding: 10px 12px; font-size: 0.9rem; line-height: 1.5; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div>
|
||
<h1>数据出口代理 <span class="muted">cellular-proxy</span></h1>
|
||
<div class="muted">走代理 = 走运营商流量 · 单进程轻量</div>
|
||
</div>
|
||
<div class="row">
|
||
<span id="connState" class="pill warn">未连接</span>
|
||
<button class="secondary" onclick="refreshAll()">刷新</button>
|
||
</div>
|
||
</header>
|
||
<main>
|
||
<section class="banner">
|
||
<strong>行为说明:</strong>系统默认仍可走 WiFi。
|
||
只要客户端使用本机 HTTP/SOCKS 代理,出口一律绑定<strong>数据网卡</strong>(流量)。
|
||
无需配置域名分流。
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="row" style="margin-bottom:10px">
|
||
<div>
|
||
<label>API 地址</label><br/>
|
||
<input id="apiBase" placeholder="http://127.0.0.1:9090" />
|
||
</div>
|
||
<div>
|
||
<label>密钥 secret</label><br/>
|
||
<input id="secret" type="password" placeholder="PANEL_SECRET" />
|
||
</div>
|
||
<div style="align-self:end">
|
||
<button onclick="saveAuth(); refreshAll()">保存并连接</button>
|
||
</div>
|
||
</div>
|
||
<div class="muted">密钥仅存本浏览器 localStorage。</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="muted">版本</div>
|
||
<div id="verInfo">—</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="row" style="justify-content:space-between;margin-bottom:8px">
|
||
<strong>出站</strong>
|
||
<span class="muted">应只有 cellular(数据网)</span>
|
||
</div>
|
||
<div id="proxies">加载中…</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<div class="row" style="justify-content:space-between;margin-bottom:8px">
|
||
<strong>当前连接</strong>
|
||
<button class="secondary" onclick="loadConnections()">刷新</button>
|
||
</div>
|
||
<div style="overflow:auto">
|
||
<table>
|
||
<thead><tr><th>目标</th><th>出站链</th><th>上传</th><th>下载</th><th></th></tr></thead>
|
||
<tbody id="connBody"></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<strong>怎么用</strong>
|
||
<pre>
|
||
1. 代理端口默认 7890(HTTP + SOCKS5 同一端口)
|
||
2. 设备设置系统/应用代理 → Debian 的 LAN-IP:7890
|
||
3. 验证:直连 ifconfig.me 与 走代理 ifconfig.me 公网 IP 应不同
|
||
4. 改网卡:改 settings.conf 里 CELLULAR_IFACE 后 sudo cpxy generate
|
||
5. 低内存:MEMORY_MAX_MB=64~96,LOG_LEVEL=warn
|
||
</pre>
|
||
</section>
|
||
|
||
<section class="card">
|
||
<strong>API 调试</strong>
|
||
<pre id="raw">—</pre>
|
||
</section>
|
||
</main>
|
||
<script>
|
||
const $ = (id) => document.getElementById(id);
|
||
function defaultBase() { return location.origin || "http://127.0.0.1:9090"; }
|
||
function loadAuth() {
|
||
$("apiBase").value = localStorage.getItem("cpxy_api") || defaultBase();
|
||
$("secret").value = localStorage.getItem("cpxy_secret") || "";
|
||
}
|
||
function saveAuth() {
|
||
localStorage.setItem("cpxy_api", $("apiBase").value.trim().replace(/\/$/, ""));
|
||
localStorage.setItem("cpxy_secret", $("secret").value);
|
||
}
|
||
function base() { return ($("apiBase").value || defaultBase()).replace(/\/$/, ""); }
|
||
function headers() {
|
||
const h = { "Content-Type": "application/json" };
|
||
const s = $("secret").value;
|
||
if (s) h["Authorization"] = "Bearer " + s;
|
||
return h;
|
||
}
|
||
async function api(path, opt = {}) {
|
||
const r = await fetch(base() + path, { ...opt, headers: { ...headers(), ...(opt.headers || {}) } });
|
||
if (!r.ok) throw new Error(path + " " + r.status + " " + (await r.text()));
|
||
const t = await r.text();
|
||
try { return t ? JSON.parse(t) : {}; } catch { return t; }
|
||
}
|
||
function fmt(n) {
|
||
if (n == null) return "—";
|
||
const u = ["B", "KB", "MB", "GB"];
|
||
let i = 0, x = Number(n);
|
||
while (x >= 1024 && i < u.length - 1) { x /= 1024; i++; }
|
||
return x.toFixed(i ? 1 : 0) + " " + u[i];
|
||
}
|
||
async function refreshAll() {
|
||
try {
|
||
const ver = await api("/version");
|
||
$("verInfo").textContent = ver.version || JSON.stringify(ver);
|
||
$("connState").textContent = "已连接";
|
||
$("connState").className = "pill ok";
|
||
$("raw").textContent = JSON.stringify(ver, null, 2);
|
||
await loadProxies();
|
||
await loadConnections();
|
||
} catch (e) {
|
||
$("connState").textContent = "失败: " + e.message;
|
||
$("connState").className = "pill bad";
|
||
$("proxies").textContent = "无法连接 API。请确认服务已启动,并填写正确 secret。";
|
||
}
|
||
}
|
||
async function loadProxies() {
|
||
const data = await api("/proxies");
|
||
const proxies = data.proxies || {};
|
||
const names = Object.keys(proxies).filter(k => ["cellular", "DIRECT", "REJECT"].includes(k) || proxies[k].type === "Direct");
|
||
const keys = names.length ? names : Object.keys(proxies).slice(0, 15);
|
||
let html = "<table><thead><tr><th>名称</th><th>类型</th><th>测速</th></tr></thead><tbody>";
|
||
for (const name of keys) {
|
||
const p = proxies[name] || {};
|
||
html += `<tr><td>${name}</td><td>${p.type || ""}</td>
|
||
<td><button class="secondary" onclick="delay('${name}')">测速</button> <span id="d-${name}"></span></td></tr>`;
|
||
}
|
||
html += "</tbody></table>";
|
||
$("proxies").innerHTML = html;
|
||
}
|
||
async function delay(name) {
|
||
try {
|
||
const r = await api("/proxies/" + encodeURIComponent(name) + "/delay?timeout=5000&url=" +
|
||
encodeURIComponent("https://www.gstatic.com/generate_204"));
|
||
$("d-" + name).textContent = (r.delay != null ? r.delay + "ms" : JSON.stringify(r));
|
||
} catch (e) { $("d-" + name).textContent = e.message; }
|
||
}
|
||
async function loadConnections() {
|
||
try {
|
||
const data = await api("/connections");
|
||
const conns = data.connections || [];
|
||
const body = $("connBody");
|
||
body.innerHTML = "";
|
||
conns.slice(0, 50).forEach(c => {
|
||
const tr = document.createElement("tr");
|
||
const host = (c.metadata && (c.metadata.host || c.metadata.destinationIP)) || "—";
|
||
const port = c.metadata && c.metadata.destinationPort ? ":" + c.metadata.destinationPort : "";
|
||
tr.innerHTML = `<td>${host}${port}</td><td>${c.chains ? c.chains.join("→") : "—"}</td>
|
||
<td>${fmt(c.upload)}</td><td>${fmt(c.download)}</td>
|
||
<td><button class="danger" onclick="closeConn('${c.id}')">断开</button></td>`;
|
||
body.appendChild(tr);
|
||
});
|
||
if (!conns.length) body.innerHTML = "<tr><td colspan=5 class=muted>暂无连接</td></tr>";
|
||
} catch (e) {
|
||
$("connBody").innerHTML = "<tr><td colspan=5>" + e.message + "</td></tr>";
|
||
}
|
||
}
|
||
async function closeConn(id) {
|
||
await api("/connections/" + id, { method: "DELETE" });
|
||
loadConnections();
|
||
}
|
||
loadAuth();
|
||
refreshAll();
|
||
setInterval(() => { loadConnections().catch(() => {}); }, 5000);
|
||
</script>
|
||
</body>
|
||
</html>
|