// ==UserScript==
// @name Clash Connection Monitor
// @namespace http://tampermonkey.net/
// @version 0.2
// @description 优雅地显示当前网页的Clash连接信息
// @author Your name
// @match *://*/*
// @grant GM_xmlhttpRequest
// @icon https://cdn.jsdelivr.net/gh/Dreamacro/clash/docs/logo.png
// ==/UserScript==
(function () {
("use strict");
// Clash API 配置
const CLASH_API = {
BASE_URL: "http://127.0.0.1:7890",
SECRET: "修改为你的密码或者置空",
};
// 在 CLASH_API 配置后添加
const PREFERENCES = {
position: 'right', // 'left' 或 'right'
};
// 动态样式
const style = document.createElement("style");
style.textContent = `
#clash-monitor {
position: fixed;
bottom: 20px;
${PREFERENCES.position}: 20px;
z-index: 10000;
display: flex;
flex-direction: column-reverse;
align-items: ${PREFERENCES.position === 'right' ? 'flex-end' : 'flex-start'};
gap: 8px;
}
#status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #95a5a6;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
#status-dot:hover {
transform: scale(1.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
#connection-details {
max-height: 0;
opacity: 0;
overflow: hidden;
padding: 0;
border-radius: 8px;
font-size: 12px;
line-height: 1.5;
pointer-events: none;
white-space: nowrap;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: all 0.3s ease;
}
#clash-monitor:hover #connection-details {
max-height: 200px;
opacity: 1;
padding: 10px;
pointer-events: auto;
}
.detail-item {
display: flex;
justify-content: space-between;
gap: 16px;
margin: 4px 0;
}
.detail-item span {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
#status-dot:active {
transform: scale(0.9);
}
`;
document.head.appendChild(style);
// 创建监视器元素
const monitor = document.createElement("div");
monitor.id = "clash-monitor";
monitor.innerHTML = `
<div id="status-dot"></div>
<div id="connection-details">
<code>
<div class="detail-item">
<span>链路:</span>
<span id="proxy-chain">-</span>
</div>
<div class="detail-item">
<span>规则:</span>
<span id="rule-match">-</span>
</div>
<div class="detail-item">
<span>上传:</span>
<span id="upload-speed">-</span>
</div>
<div class="detail-item">
<span>下载:</span>
<span id="download-speed">-</span>
</div>
</code>
</div>
`;
document.body.appendChild(monitor);
// 格式化字节数
function formatBytes(bytes) {
if (bytes === 0 || !bytes) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
// 更新连接信息
function updateConnectionInfo() {
GM_xmlhttpRequest({
method: "GET",
url: `${CLASH_API.BASE_URL}/connections`,
headers: {
Authorization: `Bearer ${CLASH_API.SECRET}`,
},
onload: function (response) {
try {
const data = JSON.parse(response.responseText);
const connections = data.connections;
const currentHost = window.location.hostname;
const currentConn = connections.find(
(conn) => conn.metadata.host === currentHost
);
const statusDot = document.getElementById("status-dot");
const proxyChain = document.getElementById("proxy-chain");
const ruleMatch = document.getElementById("rule-match");
const uploadSpeed = document.getElementById("upload-speed");
const downloadSpeed = document.getElementById("download-speed");
if (currentConn) {
const isProxy = !currentConn.chains.includes("DIRECT");
statusDot.style.backgroundColor = isProxy
? "#3498db" // 代理:蓝色
: "#2ecc71"; // 直连:绿色
proxyChain.textContent = currentConn.chains.join(" → ");
//ruleMatch.textContent = currentConn.rule || "未知";
ruleMatch.textContent = currentConn.rulePayload ? currentConn.rulePayload + " → " + currentConn.rule : currentConn.rule;
uploadSpeed.textContent = formatBytes(currentConn.upload);
downloadSpeed.textContent = formatBytes(currentConn.download);
} else {
statusDot.style.backgroundColor = "#95a5a6"; // 断开:灰色
proxyChain.textContent = "-";
ruleMatch.textContent = "-";
uploadSpeed.textContent = "-";
downloadSpeed.textContent = "-";
}
} catch (error) {
console.error("解析连接信息失败:", error);
}
},
onerror: function () {
console.error("Clash API 请求失败,请检查网络或 API 配置。");
},
});
}
// 初始化并设置定时更新
updateConnectionInfo();
setInterval(updateConnectionInfo, 5000);
// 监听页面可见性变化
document.addEventListener("visibilitychange", () => {
if (!document.hidden) updateConnectionInfo();
});
// 在 CLASH_API 配置后添加检测函数
function getBackgroundColor(element) {
let bg = window.getComputedStyle(element).backgroundColor;
while (bg === "rgba(0, 0, 0, 0)" && element.parentElement) {
element = element.parentElement;
bg = window.getComputedStyle(element).backgroundColor;
}
return bg === "rgba(0, 0, 0, 0)" ? "rgb(255, 255, 255)" : bg;
}
function isLightColor(r, g, b) {
// 使用 YIQ 算法计算亮度
return (r * 299 + g * 147 + b * 114) / 1000 > 128;
}
function getRGBValues(color) {
const match = color.match(/\d+/g);
return match ? match.map(Number) : [255, 255, 255];
}
function updateTheme() {
const bgColor = getBackgroundColor(document.body);
const [r, g, b] = getRGBValues(bgColor);
const isLight = isLightColor(r, g, b);
const details = document.getElementById('connection-details');
if (!details) return;
if (isLight) {
details.style.background = 'rgba(255, 255, 255, 0.98)';
details.style.color = '#2c3e50';
details.style.borderColor = 'rgba(0, 0, 0, 0.15)';
details.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.15)';
} else {
details.style.background = 'rgba(28, 28, 30, 0.98)';
details.style.color = '#ffffff';
details.style.borderColor = 'rgba(255, 255, 255, 0.2)';
details.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.4)';
}
}
// 在 document.body.appendChild(monitor) 后添加:
updateTheme();
// 添加 MutationObserver 监听背景色变化
const observer = new MutationObserver(updateTheme);
observer.observe(document.body, {
attributes: true,
childList: true,
subtree: true
});
// 监听页面主题变化
window.matchMedia('(prefers-color-scheme: dark)').addListener(updateTheme);
// 添加点击切换位置的功能
document.getElementById('status-dot').addEventListener('dblclick', function(e) {
e.preventDefault();
PREFERENCES.position = PREFERENCES.position === 'right' ? 'left' : 'right';
const monitor = document.getElementById('clash-monitor');
monitor.style[PREFERENCES.position === 'right' ? 'left' : 'right'] = '';
monitor.style[PREFERENCES.position] = '20px';
monitor.style.alignItems = PREFERENCES.position === 'right' ? 'flex-end' : 'flex-start';
// 保存设置到 localStorage
localStorage.setItem('clash-monitor-position', PREFERENCES.position);
});
// 在初始化时读取保存的位置设置
const savedPosition = localStorage.getItem('clash-monitor-position');
if (savedPosition) {
PREFERENCES.position = savedPosition;
}
})();