// ==UserScript== // @name bililive-go Live Room Sender // @namespace https://github.com/bililive-go/bililive-go // @version 0.2.1 // @description Send the current live room to bililive-go with dedupe, remote endpoint support and dynamic port detection // @author bililive-go // @license GPL-3.0-only // @match *://live.bilibili.com/* // @match *://live.douyin.com/* // @match *://v.douyin.com/* // @match *://www.douyu.com/* // @match *://www.huya.com/* // @match *://live.kuaishou.com/* // @match *://www.yy.com/* // @match *://live.acfun.cn/* // @match *://www.lang.live/* // @match *://fm.missevan.com/* // @match *://www.openrec.tv/* // @match *://weibo.com/* // @match *://live.weibo.com/* // @match *://www.xiaohongshu.com/* // @match *://xhslink.com/* // @match *://www.yizhibo.com/* // @match *://www.hongdoufm.com/* // @match *://live.kilakila.cn/* // @match *://www.zhanqi.tv/* // @match *://cc.163.com/* // @match *://www.twitch.tv/* // @match *://egame.qq.com/* // @match *://www.huajiao.com/* // @match *://play.sooplive.com/* // @match http://127.0.0.1:*/* // @match http://localhost:*/* // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_addStyle // @connect 127.0.0.1 // @connect localhost // @connect * // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/569635/bililive-go%20Live%20Room%20Sender.user.js // @updateURL https://update.greasyfork.icu/scripts/569635/bililive-go%20Live%20Room%20Sender.meta.js // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) { return; } var STORAGE_KEYS = { manualEndpoint: 'bgo_manual_endpoint', lastEndpoint: 'bgo_last_endpoint', recentPorts: 'bgo_recent_ports', extraHeaders: 'bgo_extra_headers', }; var LOCAL_HOSTS = { '127.0.0.1': true, 'localhost': true, }; var LOCAL_ALIAS_HOSTS = { 'live.weibo.com': 'weibo.com', }; var DEFAULT_LOCAL_ENDPOINTS = [ 'http://127.0.0.1:8080', 'http://localhost:8080', ]; var FLOATING_LAYOUT = { dragThreshold: 4, viewportMargin: 12, panelGap: 12, }; var state = { endpoint: '', room: null, roomUrl: getCurrentRoomUrl(), opened: false, refreshing: false, hasChecked: false, lastCheckSignature: '', }; var ui = { launcher: null, launcherStatus: null, panel: null, pageValue: null, endpointValue: null, statusValue: null, primaryButton: null, refreshButton: null, settingsButton: null, closeButton: null, guideButton: null, guideModal: null, guideModalClose: null, guideModalBackdrop: null, }; var dragState = { active: false, moved: false, ignoreClick: false, pointerId: null, offsetX: 0, offsetY: 0, }; registerMenuCommands(); if (isLocalPage()) { autoBindCurrentOrigin(); return; } injectStyles(); createUI(); updatePageUrlDisplay(); watchLocationChange(); refreshPanelState(true); function registerMenuCommands() { if (typeof GM_registerMenuCommand !== 'function') { return; } GM_registerMenuCommand('设置 bililive 地址', function () { promptAndSaveEndpoint().then(function (changed) { if (changed) { refreshPanelState(true); } }); }); GM_registerMenuCommand('设置额外请求头', function () { promptAndSaveExtraHeaders().then(function (changed) { if (changed) { refreshPanelState(true); } }); }); GM_registerMenuCommand('清除 bililive 地址', function () { GM_setValue(STORAGE_KEYS.manualEndpoint, ''); GM_setValue(STORAGE_KEYS.lastEndpoint, ''); refreshPanelState(true); }); GM_registerMenuCommand('清除额外请求头', function () { GM_setValue(STORAGE_KEYS.extraHeaders, '{}'); refreshPanelState(true); }); } function isLocalPage() { return !!LOCAL_HOSTS[window.location.hostname]; } function autoBindCurrentOrigin() { probeEndpoint(window.location.origin).then(function (ok) { if (!ok) { return; } rememberEndpoint(window.location.origin); console.info('[bililive-go] 已自动绑定本地服务地址:', window.location.origin); }); } function injectStyles() { GM_addStyle([ '#bgo-tm-launcher {', ' position: fixed;', ' right: 18px;', ' bottom: 116px;', ' z-index: 2147483646;', ' display: inline-flex;', ' align-items: center;', ' gap: 8px;', ' padding: 10px 11px 10px 14px;', ' border: 1px solid rgba(148, 163, 184, 0.24);', ' border-radius: 999px;', ' background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(241, 245, 249, 0.9));', ' color: #0f172a;', ' font-size: 12px;', ' font-weight: 700;', ' letter-spacing: 0.02em;', ' cursor: grab;', ' user-select: none;', ' touch-action: none;', ' box-shadow: 0 20px 38px rgba(15, 23, 42, 0.18);', ' backdrop-filter: blur(18px);', ' box-shadow: 0 20px 38px rgba(15, 23, 42, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.72);', ' transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;', '}', '.bgo-tm-launcher-brand {', ' white-space: nowrap;', '}', '.bgo-tm-launcher-status {', ' display: inline-flex;', ' align-items: center;', ' justify-content: center;', ' min-width: 54px;', ' padding: 6px 10px;', ' border-radius: 999px;', ' background: rgba(15, 23, 42, 0.08);', ' color: #334155;', ' font-size: 11px;', ' font-weight: 800;', ' letter-spacing: 0.01em;', ' transition: background 160ms ease, color 160ms ease;', '}', '#bgo-tm-launcher[data-state="loading"] .bgo-tm-launcher-status,', '#bgo-tm-launcher[data-state="sending"] .bgo-tm-launcher-status {', ' background: rgba(59, 130, 246, 0.12);', ' color: #1d4ed8;', '}', '#bgo-tm-launcher[data-state="ready"] .bgo-tm-launcher-status {', ' background: rgba(20, 184, 166, 0.14);', ' color: #0f766e;', '}', '#bgo-tm-launcher[data-state="added"] .bgo-tm-launcher-status {', ' background: rgba(22, 163, 74, 0.14);', ' color: #166534;', '}', '#bgo-tm-launcher[data-state="offline"] .bgo-tm-launcher-status,', '#bgo-tm-launcher[data-state="unsupported"] .bgo-tm-launcher-status {', ' background: rgba(148, 163, 184, 0.18);', ' color: #475569;', '}', '#bgo-tm-launcher[data-state="error"] .bgo-tm-launcher-status {', ' background: rgba(239, 68, 68, 0.14);', ' color: #b91c1c;', '}', '#bgo-tm-launcher:hover {', ' transform: translateY(-2px);', ' box-shadow: 0 24px 46px rgba(15, 23, 42, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.82);', ' border-color: rgba(20, 184, 166, 0.28);', ' background: #ffffff;', '}', '#bgo-tm-launcher[data-dragging="true"] {', ' cursor: grabbing;', ' transition: none;', '}', '#bgo-tm-launcher[data-dragging="true"]:hover {', ' transform: none;', '}', '#bgo-tm-launcher::before {', ' content: "";', ' width: 9px;', ' height: 9px;', ' border-radius: 999px;', ' background: radial-gradient(circle at 30% 30%, #99f6e4, #14b8a6 65%, #0f766e 100%);', ' box-shadow: 0 0 18px rgba(20, 184, 166, 0.46);', '}', '#bgo-tm-panel {', ' position: fixed;', ' right: 18px;', ' bottom: 168px;', ' width: min(380px, calc(100vw - 24px));', ' z-index: 2147483646;', ' border-radius: 24px;', ' overflow: hidden;', ' background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.96));', ' color: #0f172a;', ' box-shadow: 0 30px 70px rgba(15, 23, 42, 0.26);', ' border: 1px solid rgba(148, 163, 184, 0.22);', ' display: none;', ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;', ' backdrop-filter: blur(22px);', ' max-height: min(760px, calc(100vh - 28px));', '}', '#bgo-tm-panel::before {', ' content: "";', ' position: absolute;', ' inset: 0 0 auto 0;', ' height: 164px;', ' background: radial-gradient(circle at top right, rgba(45, 212, 191, 0.24), transparent 54%), linear-gradient(135deg, rgba(15, 23, 42, 0.08), transparent 72%);', ' pointer-events: none;', '}', '#bgo-tm-panel.bgo-open {', ' display: block;', '}', '#bgo-tm-panel * {', ' box-sizing: border-box;', '}', '.bgo-tm-header {', ' display: flex;', ' align-items: flex-start;', ' justify-content: space-between;', ' gap: 12px;', ' padding: 18px 18px 10px;', ' position: relative;', ' color: #0f172a;', '}', '.bgo-tm-header-main {', ' min-width: 0;', '}', '.bgo-tm-kicker {', ' display: inline-flex;', ' align-items: center;', ' gap: 6px;', ' padding: 4px 9px;', ' border-radius: 999px;', ' background: rgba(15, 23, 42, 0.06);', ' color: #0f766e;', ' font-size: 11px;', ' font-weight: 700;', ' letter-spacing: 0.06em;', ' text-transform: uppercase;', '}', '.bgo-tm-kicker::before {', ' content: "";', ' width: 7px;', ' height: 7px;', ' border-radius: 999px;', ' background: #14b8a6;', ' box-shadow: 0 0 14px rgba(20, 184, 166, 0.38);', '}', '.bgo-tm-title {', ' margin-top: 10px;', ' font-size: 20px;', ' line-height: 1.1;', ' font-weight: 800;', ' letter-spacing: -0.02em;', '}', '.bgo-tm-subtitle {', ' margin-top: 6px;', ' color: #475569;', ' font-size: 12px;', ' line-height: 1.5;', '}', '.bgo-tm-actions {', ' display: flex;', ' gap: 8px;', ' flex-wrap: wrap;', ' justify-content: flex-end;', ' flex-shrink: 0;', '}', '.bgo-tm-icon-btn {', ' min-width: 52px;', ' border: 1px solid rgba(148, 163, 184, 0.28);', ' background: rgba(255, 255, 255, 0.8);', ' color: #334155;', ' border-radius: 12px;', ' padding: 7px 10px;', ' cursor: pointer;', ' font-size: 12px;', ' font-weight: 600;', ' transition: background 160ms ease, border-color 160ms ease, transform 160ms ease;', '}', '.bgo-tm-icon-btn:hover {', ' background: #ffffff;', ' border-color: rgba(20, 184, 166, 0.34);', ' transform: translateY(-1px);', '}', '.bgo-tm-body {', ' position: relative;', ' padding: 8px 18px 18px;', ' overflow: auto;', '}', '.bgo-tm-row {', ' margin-bottom: 10px;', ' padding: 12px 14px;', ' border-radius: 16px;', ' background: rgba(255, 255, 255, 0.82);', ' border: 1px solid rgba(226, 232, 240, 0.92);', ' box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.58);', '}', '.bgo-tm-label {', ' display: inline-block;', ' margin-bottom: 6px;', ' color: #64748b;', ' font-size: 12px;', ' font-weight: 700;', ' letter-spacing: 0.02em;', '}', '.bgo-tm-value {', ' font-size: 13px;', ' line-height: 1.65;', ' color: #0f172a;', ' word-break: break-all;', '}', '.bgo-tm-status {', ' min-height: 56px;', ' padding: 12px 14px;', ' border-radius: 16px;', ' background: linear-gradient(135deg, rgba(15, 118, 110, 0.1), rgba(45, 212, 191, 0.08));', ' border: 1px solid rgba(20, 184, 166, 0.18);', ' box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.55);', '}', '.bgo-tm-help-trigger {', ' display: inline-flex;', ' align-items: center;', ' justify-content: center;', ' gap: 10px;', ' padding: 12px 14px;', ' border-radius: 18px;', ' border: 1px solid rgba(148, 163, 184, 0.24);', ' background: linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.9));', ' color: #0f172a;', ' font-size: 13px;', ' font-weight: 700;', ' cursor: pointer;', ' box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);', ' transition: transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;', '}', '.bgo-tm-help-trigger:hover {', ' transform: translateY(-1px);', ' border-color: rgba(20, 184, 166, 0.3);', ' box-shadow: 0 12px 26px rgba(15, 23, 42, 0.08);', ' background: #ffffff;', '}', '.bgo-tm-help-trigger::before {', ' content: "";', ' width: 10px;', ' height: 10px;', ' border-radius: 999px;', ' background: linear-gradient(135deg, #0f766e, #2dd4bf);', ' box-shadow: 0 0 16px rgba(20, 184, 166, 0.3);', '}', '.bgo-tm-guide-list {', ' margin: 0;', ' padding-left: 18px;', ' color: #475569;', ' font-size: 12px;', ' line-height: 1.7;', '}', '.bgo-tm-guide-list li + li {', ' margin-top: 6px;', '}', '.bgo-tm-modal {', ' position: fixed;', ' inset: 0;', ' z-index: 2147483647;', ' display: none;', '}', '.bgo-tm-modal.bgo-visible {', ' display: block;', '}', '.bgo-tm-modal-backdrop {', ' position: absolute;', ' inset: 0;', ' background: rgba(15, 23, 42, 0.46);', ' backdrop-filter: blur(6px);', '}', '.bgo-tm-modal-card {', ' position: absolute;', ' top: 50%;', ' left: 50%;', ' width: min(420px, calc(100vw - 28px));', ' max-height: min(560px, calc(100vh - 32px));', ' overflow: auto;', ' transform: translate(-50%, -50%);', ' border-radius: 24px;', ' background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.96));', ' border: 1px solid rgba(203, 213, 225, 0.74);', ' box-shadow: 0 34px 80px rgba(15, 23, 42, 0.34);', ' padding: 18px;', '}', '.bgo-tm-modal-head {', ' display: flex;', ' align-items: center;', ' justify-content: space-between;', ' gap: 12px;', ' margin-bottom: 12px;', '}', '.bgo-tm-modal-title {', ' font-size: 18px;', ' font-weight: 800;', ' color: #0f172a;', ' letter-spacing: -0.02em;', '}', '.bgo-tm-modal-close {', ' border: 1px solid rgba(148, 163, 184, 0.28);', ' background: rgba(255, 255, 255, 0.82);', ' color: #334155;', ' border-radius: 12px;', ' padding: 8px 12px;', ' font-size: 12px;', ' font-weight: 700;', ' cursor: pointer;', '}', '.bgo-tm-modal-copy {', ' padding: 14px 14px 12px;', ' border-radius: 18px;', ' background: linear-gradient(180deg, rgba(248, 250, 252, 0.96), rgba(241, 245, 249, 0.88));', ' border: 1px solid rgba(203, 213, 225, 0.72);', '}', '.bgo-tm-modal-copy .bgo-tm-guide-list {', ' color: #334155;', ' font-size: 13px;', '}', '.bgo-tm-buttons {', ' display: flex;', ' gap: 10px;', ' margin-top: 16px;', '}', '.bgo-tm-primary, .bgo-tm-secondary {', ' flex: 1;', ' border-radius: 16px;', ' padding: 12px 14px;', ' font-size: 13px;', ' font-weight: 700;', ' cursor: pointer;', ' transition: transform 160ms ease, box-shadow 160ms ease, border-color 160ms ease, background 160ms ease;', '}', '.bgo-tm-primary {', ' border: 0;', ' background: linear-gradient(135deg, #0f766e, #14b8a6);', ' color: #f8fafc;', ' box-shadow: 0 14px 30px rgba(15, 118, 110, 0.24);', '}', '.bgo-tm-primary:hover:not([disabled]) {', ' transform: translateY(-1px);', ' box-shadow: 0 18px 34px rgba(15, 118, 110, 0.28);', '}', '.bgo-tm-primary[disabled] {', ' cursor: not-allowed;', ' background: linear-gradient(135deg, #cbd5e1, #e2e8f0);', ' color: #475569;', ' box-shadow: none;', '}', '.bgo-tm-secondary {', ' border: 1px solid rgba(148, 163, 184, 0.28);', ' background: rgba(255, 255, 255, 0.84);', ' color: #0f172a;', '}', '.bgo-tm-secondary:hover {', ' transform: translateY(-1px);', ' border-color: rgba(20, 184, 166, 0.3);', ' background: #ffffff;', '}', '@media (max-width: 480px) {', ' #bgo-tm-launcher {', ' right: 12px;', ' bottom: 92px;', ' padding-right: 10px;', ' }', ' #bgo-tm-panel {', ' right: 12px;', ' bottom: 142px;', ' width: calc(100vw - 24px);', ' }', ' .bgo-tm-buttons {', ' flex-direction: column;', ' }', ' .bgo-tm-actions {', ' width: 100%;', ' justify-content: flex-start;', ' }', '}', ].join('\n')); } function createUI() { ui.launcher = document.createElement('button'); ui.launcher.id = 'bgo-tm-launcher'; ui.launcher.type = 'button'; ui.launcher.setAttribute('data-state', 'loading'); ui.launcher.innerHTML = [ 'bililive', '检测中', ].join(''); ui.launcher.addEventListener('click', handleLauncherClick); ui.launcherStatus = ui.launcher.querySelector('[data-role="launcher-status"]'); ui.panel = document.createElement('section'); ui.panel.id = 'bgo-tm-panel'; ui.panel.innerHTML = [ '
', '
', '
Live Bridge
', '
bililive-go
', '
一键把当前直播页发送到 bililive-go,新建监控更快也更直接。
', '
', '
', ' ', ' ', ' ', '
', '
', '
', '
', ' 当前页面', '
', '
', '
', ' bililive 地址', '
未检测
', '
', '
', ' 状态', '
点击面板后开始检测
', '
', '
', ' ', ' ', '
', '
', '
', '
', ' ', '
', ].join(''); ui.pageValue = ui.panel.querySelector('[data-role="page"]'); ui.endpointValue = ui.panel.querySelector('[data-role="endpoint"]'); ui.statusValue = ui.panel.querySelector('[data-role="status"]'); ui.primaryButton = ui.panel.querySelector('[data-role="primary"]'); ui.refreshButton = ui.panel.querySelector('[data-role="refresh"]'); ui.settingsButton = ui.panel.querySelector('[data-role="settings"]'); ui.closeButton = ui.panel.querySelector('[data-role="close"]'); ui.guideButton = ui.panel.querySelector('[data-role="guide-open"]'); ui.guideModal = ui.panel.querySelector('[data-role="guide-modal"]'); ui.guideModalClose = ui.panel.querySelector('[data-role="guide-close"]'); ui.guideModalBackdrop = ui.panel.querySelector('[data-role="guide-backdrop"]'); ui.primaryButton.addEventListener('click', handlePrimaryAction); ui.refreshButton.addEventListener('click', function () { refreshPanelState(true); }); ui.settingsButton.addEventListener('click', function () { promptAndSaveEndpoint().then(function (changed) { if (changed) { refreshPanelState(true); } }); }); ui.closeButton.addEventListener('click', closePanel); ui.guideButton.addEventListener('click', openGuideModal); ui.guideModalClose.addEventListener('click', closeGuideModal); ui.guideModalBackdrop.addEventListener('click', closeGuideModal); document.body.appendChild(ui.launcher); document.body.appendChild(ui.panel); bindLauncherDrag(); } function bindLauncherDrag() { if (!ui.launcher) { return; } ui.launcher.addEventListener('pointerdown', handleLauncherPointerDown); window.addEventListener('pointermove', handleLauncherPointerMove); window.addEventListener('pointerup', handleLauncherPointerEnd); window.addEventListener('pointercancel', handleLauncherPointerEnd); window.addEventListener('resize', handleViewportChange); } function handleLauncherClick(event) { if (dragState.ignoreClick) { dragState.ignoreClick = false; event.preventDefault(); return; } togglePanel(); } function handleLauncherPointerDown(event) { if (!ui.launcher) { return; } if (event.pointerType === 'mouse' && event.button !== 0) { return; } var rect = ui.launcher.getBoundingClientRect(); dragState.active = true; dragState.moved = false; dragState.pointerId = event.pointerId; dragState.offsetX = event.clientX - rect.left; dragState.offsetY = event.clientY - rect.top; ui.launcher.setAttribute('data-dragging', 'true'); if (typeof ui.launcher.setPointerCapture === 'function') { try { ui.launcher.setPointerCapture(event.pointerId); } catch (error) { } } } function handleLauncherPointerMove(event) { if (!dragState.active || dragState.pointerId !== event.pointerId || !ui.launcher) { return; } var currentRect = ui.launcher.getBoundingClientRect(); var position = clampLauncherPosition( event.clientX - dragState.offsetX, event.clientY - dragState.offsetY ); if (!dragState.moved) { dragState.moved = Math.abs(position.left - currentRect.left) >= FLOATING_LAYOUT.dragThreshold || Math.abs(position.top - currentRect.top) >= FLOATING_LAYOUT.dragThreshold; if (!dragState.moved) { return; } } applyLauncherPosition(position.left, position.top); if (state.opened) { updatePanelPosition(); } } function handleLauncherPointerEnd(event) { if (!dragState.active || dragState.pointerId !== event.pointerId) { return; } dragState.ignoreClick = dragState.moved; dragState.active = false; dragState.moved = false; dragState.pointerId = null; if (!ui.launcher) { return; } ui.launcher.removeAttribute('data-dragging'); if (typeof ui.launcher.releasePointerCapture === 'function') { try { ui.launcher.releasePointerCapture(event.pointerId); } catch (error) { } } } function handleViewportChange() { if (hasCustomLauncherPosition()) { var position = clampLauncherPosition(getCustomLauncherLeft(), getCustomLauncherTop()); applyLauncherPosition(position.left, position.top); } if (state.opened) { updatePanelPosition(); } } function hasCustomLauncherPosition() { return !!(ui.launcher && ui.launcher.style.left && ui.launcher.style.top); } function getCustomLauncherLeft() { if (!ui.launcher) { return 0; } return parseFloat(ui.launcher.style.left) || 0; } function getCustomLauncherTop() { if (!ui.launcher) { return 0; } return parseFloat(ui.launcher.style.top) || 0; } function applyLauncherPosition(left, top) { if (!ui.launcher) { return; } var position = clampLauncherPosition(left, top); ui.launcher.style.left = position.left + 'px'; ui.launcher.style.top = position.top + 'px'; ui.launcher.style.right = 'auto'; ui.launcher.style.bottom = 'auto'; } function clampLauncherPosition(left, top) { var launcherWidth = ui.launcher ? ui.launcher.offsetWidth : 0; var launcherHeight = ui.launcher ? ui.launcher.offsetHeight : 0; var maxLeft = Math.max(FLOATING_LAYOUT.viewportMargin, window.innerWidth - launcherWidth - FLOATING_LAYOUT.viewportMargin); var maxTop = Math.max(FLOATING_LAYOUT.viewportMargin, window.innerHeight - launcherHeight - FLOATING_LAYOUT.viewportMargin); return { left: clamp(left, FLOATING_LAYOUT.viewportMargin, maxLeft), top: clamp(top, FLOATING_LAYOUT.viewportMargin, maxTop), }; } function updatePanelPosition() { if (!ui.launcher || !ui.panel) { return; } var launcherRect = ui.launcher.getBoundingClientRect(); var panelRect = ui.panel.getBoundingClientRect(); var maxLeft = Math.max(FLOATING_LAYOUT.viewportMargin, window.innerWidth - panelRect.width - FLOATING_LAYOUT.viewportMargin); var maxTop = Math.max(FLOATING_LAYOUT.viewportMargin, window.innerHeight - panelRect.height - FLOATING_LAYOUT.viewportMargin); var left = clamp(launcherRect.right - panelRect.width, FLOATING_LAYOUT.viewportMargin, maxLeft); var top = launcherRect.top - panelRect.height - FLOATING_LAYOUT.panelGap; if (top < FLOATING_LAYOUT.viewportMargin) { top = launcherRect.bottom + FLOATING_LAYOUT.panelGap; } ui.panel.style.left = left + 'px'; ui.panel.style.top = clamp(top, FLOATING_LAYOUT.viewportMargin, maxTop) + 'px'; ui.panel.style.right = 'auto'; ui.panel.style.bottom = 'auto'; } function clamp(value, min, max) { if (value < min) { return min; } if (value > max) { return max; } return value; } function togglePanel() { if (state.opened) { closePanel(); return; } openPanel(); } function openPanel() { state.opened = true; ui.panel.classList.add('bgo-open'); updatePanelPosition(); refreshPanelState(false); } function closePanel() { state.opened = false; ui.panel.classList.remove('bgo-open'); closeGuideModal(); } function openGuideModal() { if (!ui.guideModal) { return; } ui.guideModal.classList.add('bgo-visible'); } function closeGuideModal() { if (!ui.guideModal) { return; } ui.guideModal.classList.remove('bgo-visible'); } function updatePageUrlDisplay() { state.roomUrl = getCurrentRoomUrl(); if (ui.pageValue) { ui.pageValue.textContent = state.roomUrl; } } function getCurrentRoomUrl() { return window.location.href.split('#')[0]; } function watchLocationChange() { var lastUrl = state.roomUrl; window.setInterval(function () { var nextUrl = getCurrentRoomUrl(); if (nextUrl === lastUrl) { return; } lastUrl = nextUrl; state.room = null; state.endpoint = ''; state.hasChecked = false; state.lastCheckSignature = ''; updatePageUrlDisplay(); setEndpointText('待重新检测'); setStatusText('页面已变化,请重新检测当前直播间'); setPrimaryState('发送到 bililive', true); setLauncherState('loading', '检测中'); refreshPanelState(true); }, 1000); } function buildCheckSignature() { return JSON.stringify({ roomUrl: getCurrentRoomUrl(), manualEndpoint: normalizeEndpoint(GM_getValue(STORAGE_KEYS.manualEndpoint, '')), lastEndpoint: normalizeEndpoint(GM_getValue(STORAGE_KEYS.lastEndpoint, '')), extraHeaders: String(GM_getValue(STORAGE_KEYS.extraHeaders, '{}') || '{}'), }); } function canReusePanelState() { if (!state.hasChecked) { return false; } return state.lastCheckSignature === buildCheckSignature(); } async function refreshPanelState(force) { if (state.refreshing) { return; } if (force !== true && canReusePanelState()) { return; } state.refreshing = true; state.room = null; setPrimaryState('检测中', true); setStatusText('正在连接 bililive-go...'); setEndpointText('检测中'); setLauncherState('loading', '检测中'); try { var endpoint = await resolveEndpoint(); if (!endpoint) { state.endpoint = ''; setEndpointText('未连接'); setStatusText('未找到可用的 bililive-go。请先在菜单中设置地址。远程和 Docker 部署建议填写完整 URL。'); setPrimaryState('未连接 bililive', true); setLauncherState('offline', '未连接'); return; } state.endpoint = endpoint; setEndpointText(endpoint); var room = await resolveCurrentRoom(endpoint); if (!room) { state.room = null; setStatusText('当前页面不是 bililive-go 支持的直播间地址,或页面本身不是直播间。'); setPrimaryState('当前页面不支持', true); setLauncherState('unsupported', '不支持'); return; } state.room = room; var tasks = await getTasks(endpoint); if (isAlreadyAdded(tasks, room)) { setStatusText('当前直播间已存在于 bililive-go 监控列表。'); setPrimaryState('已添加过', true); setLauncherState('added', '已添加'); return; } setStatusText('已识别平台:' + room.platform + ',可以发送到 bililive-go。'); setPrimaryState('发送到 bililive', false); setLauncherState('ready', '可发送'); } catch (error) { setStatusText('检测失败:' + error.message); setPrimaryState('发送失败', true); setLauncherState('error', '失败'); } finally { state.hasChecked = true; state.lastCheckSignature = buildCheckSignature(); state.refreshing = false; } } async function handlePrimaryAction() { if (!state.endpoint || !state.room) { return; } setPrimaryState('发送中', true); setStatusText('正在向 bililive-go 添加直播间...'); setLauncherState('sending', '发送中'); try { var latestTasks = await getTasks(state.endpoint); if (isAlreadyAdded(latestTasks, state.room)) { setStatusText('当前直播间已经在 bililive-go 监控列表里,无需重复添加。'); setPrimaryState('已添加过', true); setLauncherState('added', '已添加'); return; } await addTask(state.endpoint, state.room.canonical_url); setStatusText('添加成功,当前直播间已进入 bililive-go 监控列表。'); setPrimaryState('已添加过', true); setLauncherState('added', '已添加'); } catch (error) { setStatusText('发送失败:' + error.message); setPrimaryState('发送失败', true); setLauncherState('error', '失败'); } } function setEndpointText(value) { ui.endpointValue.textContent = value; } function setStatusText(value) { ui.statusValue.textContent = value; } function setPrimaryState(text, disabled) { ui.primaryButton.textContent = text; ui.primaryButton.disabled = disabled; } function setLauncherState(stateKey, text) { if (!ui.launcher) { return; } ui.launcher.setAttribute('data-state', stateKey || 'idle'); if (ui.launcherStatus) { ui.launcherStatus.textContent = text || ''; } } async function promptAndSaveEndpoint() { var currentValue = GM_getValue(STORAGE_KEYS.manualEndpoint, '') || GM_getValue(STORAGE_KEYS.lastEndpoint, '') || ''; var input = window.prompt('请输入 bililive-go 地址,例如 http://127.0.0.1:8080 或 https://example.com/bgo', currentValue); if (input === null) { return false; } input = normalizeEndpoint(input); if (!input) { window.alert('地址不能为空'); return false; } var ok = await probeEndpoint(input); if (!ok) { window.alert('该地址无法连接到 bililive-go,请确认地址、端口、路径和网络连通性是否正确'); return false; } GM_setValue(STORAGE_KEYS.manualEndpoint, input); rememberEndpoint(input); return true; } async function promptAndSaveExtraHeaders() { var currentValue = GM_getValue(STORAGE_KEYS.extraHeaders, '{}'); var input = window.prompt('请输入额外请求头 JSON,例如 {"Authorization":"Bearer xxx"}', currentValue); if (input === null) { return false; } input = String(input).trim(); if (!input) { GM_setValue(STORAGE_KEYS.extraHeaders, '{}'); return true; } try { var parsed = JSON.parse(input); if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { throw new Error('请求头必须是 JSON 对象'); } GM_setValue(STORAGE_KEYS.extraHeaders, JSON.stringify(parsed)); return true; } catch (error) { window.alert('额外请求头格式错误:' + error.message); return false; } } async function resolveEndpoint() { var candidates = collectEndpointCandidates(); for (var i = 0; i < candidates.length; i += 1) { var candidate = candidates[i]; var ok = await probeEndpoint(candidate); if (!ok) { continue; } rememberEndpoint(candidate); return candidate; } return ''; } function collectEndpointCandidates() { var values = []; pushCandidate(values, GM_getValue(STORAGE_KEYS.manualEndpoint, '')); pushCandidate(values, GM_getValue(STORAGE_KEYS.lastEndpoint, '')); if (isLocalPage()) { pushCandidate(values, window.location.origin); } var recentPorts = GM_getValue(STORAGE_KEYS.recentPorts, []); if (Array.isArray(recentPorts)) { for (var i = 0; i < recentPorts.length; i += 1) { var port = String(recentPorts[i]).trim(); if (!port) { continue; } pushCandidate(values, 'http://127.0.0.1:' + port); pushCandidate(values, 'http://localhost:' + port); } } for (var j = 0; j < DEFAULT_LOCAL_ENDPOINTS.length; j += 1) { pushCandidate(values, DEFAULT_LOCAL_ENDPOINTS[j]); } return values; } function pushCandidate(list, value) { value = normalizeEndpoint(value); if (!value) { return; } if (list.indexOf(value) !== -1) { return; } list.push(value); } function normalizeEndpoint(value) { if (!value) { return ''; } value = String(value).trim(); if (!value) { return ''; } if (!/^https?:\/\//i.test(value)) { value = 'http://' + value; } try { var parsed = new URL(value); var pathname = parsed.pathname || '/'; pathname = pathname.replace(/\/+$/, ''); if (!pathname || pathname === '/') { return parsed.origin; } return parsed.origin + pathname; } catch (error) { return ''; } } function rememberEndpoint(endpoint) { endpoint = normalizeEndpoint(endpoint); if (!endpoint) { return; } GM_setValue(STORAGE_KEYS.lastEndpoint, endpoint); try { var port = new URL(endpoint).port; if (!port) { return; } var recentPorts = GM_getValue(STORAGE_KEYS.recentPorts, []); if (!Array.isArray(recentPorts)) { recentPorts = []; } recentPorts = recentPorts.filter(function (item) { return String(item) !== port; }); recentPorts.unshift(port); GM_setValue(STORAGE_KEYS.recentPorts, recentPorts.slice(0, 6)); } catch (error) { console.warn('[bililive-go] 记录端口失败:', error); } } async function probeEndpoint(endpoint) { try { var response = await requestJSON('GET', joinUrl(endpoint, '/osrp/v1/info')); return response.status >= 200 && response.status < 300 && response.data && response.data.success === true; } catch (error) { return false; } } async function resolveCurrentRoom(endpoint) { var inputUrls = buildResolveUrlCandidates(); var lastError = null; for (var i = 0; i < inputUrls.length; i += 1) { try { var response = await requestJSON('POST', joinUrl(endpoint, '/osrp/v1/resolve'), { url: inputUrls[i], }); return getSuccessData(response); } catch (error) { lastError = error; } } if (lastError) { console.warn('[bililive-go] 解析当前页面失败:', lastError.message); } return null; } function buildResolveUrlCandidates() { var currentUrl = getCurrentRoomUrl(); var values = [currentUrl]; try { var parsed = new URL(currentUrl); var aliasHost = LOCAL_ALIAS_HOSTS[parsed.hostname]; if (aliasHost) { parsed.hostname = aliasHost; values.push(parsed.toString()); } } catch (error) { console.warn('[bililive-go] 当前页面 URL 解析失败:', error); } return values.filter(function (value, index, list) { return list.indexOf(value) === index; }); } function normalizeCompareUrl(input) { if (!input) { return ''; } try { var parsed = new URL(String(input).trim()); var aliasHost = LOCAL_ALIAS_HOSTS[parsed.hostname]; if (aliasHost) { parsed.hostname = aliasHost; } parsed.hash = ''; var trackingParams = { spm_id_from: true, visit_id: true, session_id: true, live_from: true, from: true, from_id: true, share_from: true, share_source: true, share_medium: true, share_plat: true, share_token: true, source: true, refer_from: true, utm_source: true, utm_medium: true, utm_campaign: true, utm_content: true, utm_term: true, launch_id: true, broadcast_type: true, is_room_feed: true, }; Object.keys(trackingParams).forEach(function (key) { if (parsed.searchParams.has(key)) { parsed.searchParams.delete(key); } }); var pathname = parsed.pathname || '/'; if (pathname.length > 1) { pathname = pathname.replace(/\/+$/, ''); } parsed.pathname = pathname || '/'; var queryPairs = []; parsed.searchParams.forEach(function (value, key) { queryPairs.push([key, value]); }); queryPairs.sort(function (a, b) { if (a[0] === b[0]) { return a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0; } return a[0] < b[0] ? -1 : 1; }); parsed.search = ''; for (var i = 0; i < queryPairs.length; i += 1) { parsed.searchParams.append(queryPairs[i][0], queryPairs[i][1]); } return parsed.toString(); } catch (error) { return String(input).trim(); } } function collectRoomUrlCandidates(room) { var candidates = []; function addCandidate(value) { if (!value) { return; } var normalized = normalizeCompareUrl(value); if (!normalized) { return; } if (candidates.indexOf(normalized) !== -1) { return; } candidates.push(normalized); } addCandidate(getCurrentRoomUrl()); var resolveCandidates = buildResolveUrlCandidates(); for (var i = 0; i < resolveCandidates.length; i += 1) { addCandidate(resolveCandidates[i]); } if (room) { addCandidate(room.canonical_url); } return candidates; } async function getTasks(endpoint) { var response = await requestJSON('GET', joinUrl(endpoint, '/osrp/v1/tasks')); var data = getSuccessData(response); if (!data || !Array.isArray(data.tasks)) { return []; } return data.tasks; } function isAlreadyAdded(tasks, room) { if (!Array.isArray(tasks) || !room) { return false; } var roomUrlCandidates = collectRoomUrlCandidates(room); for (var i = 0; i < tasks.length; i += 1) { var task = tasks[i]; if (!task) { continue; } if (task.id && room.stream_id && task.id === room.stream_id) { return true; } if (task.stream_id && room.stream_id && task.stream_id === room.stream_id) { return true; } var taskUrl = normalizeCompareUrl(task.url); if (taskUrl && roomUrlCandidates.indexOf(taskUrl) !== -1) { return true; } } return false; } async function addTask(endpoint, url) { var response = await requestJSON('POST', joinUrl(endpoint, '/osrp/v1/tasks'), { url: url, auto_start: true, }); getSuccessData(response); } function joinUrl(base, path) { return String(base).replace(/\/+$/, '') + '/' + String(path).replace(/^\/+/, ''); } function getExtraHeaders() { var rawValue = GM_getValue(STORAGE_KEYS.extraHeaders, '{}'); if (!rawValue) { return {}; } try { var parsed = typeof rawValue === 'string' ? JSON.parse(rawValue) : rawValue; if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') { return {}; } var headers = {}; Object.keys(parsed).forEach(function (key) { var headerName = String(key).trim(); if (!headerName) { return; } var headerValue = parsed[key]; if (headerValue === undefined || headerValue === null) { return; } headers[headerName] = String(headerValue); }); return headers; } catch (error) { console.warn('[bililive-go] 额外请求头解析失败:', error); return {}; } } function getSuccessData(response) { if (!response) { throw new Error('响应为空'); } if (response.status < 200 || response.status >= 300) { throw new Error(extractErrorMessage(response) || '请求失败,HTTP ' + response.status); } if (!response.data || response.data.success !== true) { throw new Error(extractErrorMessage(response) || '接口返回失败'); } return response.data.data; } function extractErrorMessage(response) { if (!response || !response.data) { return ''; } if (response.data.error && response.data.error.message) { return response.data.error.message; } if (typeof response.data.message === 'string') { return response.data.message; } return ''; } function requestJSON(method, url, body) { return new Promise(function (resolve, reject) { var headers = getExtraHeaders(); if (body) { headers['Content-Type'] = 'application/json'; } GM_xmlhttpRequest({ method: method, url: url, timeout: 4000, headers: Object.keys(headers).length > 0 ? headers : undefined, data: body ? JSON.stringify(body) : undefined, onload: function (response) { var data = null; try { data = response.responseText ? JSON.parse(response.responseText) : null; } catch (error) { reject(new Error('返回了非 JSON 数据')); return; } resolve({ status: response.status, data: data, }); }, onerror: function () { reject(new Error('网络请求失败')); }, ontimeout: function () { reject(new Error('请求超时')); }, }); }); } })();