From fecda7cce34bd8d959710bdb1babee9601f5d500 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Apr 2026 16:08:00 +0000 Subject: [PATCH] fix(web): make bot-link usable on public IP with HTTP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "复制专属链接" button silently failed on public-IP HTTP deployments because navigator.clipboard requires a secure context. Now the link is always revealed in a modal with a read-only input (select-all on focus), so users can copy manually even when clipboard APIs and execCommand both fail. The dialog still tries to auto-copy when possible. Also adds a publicUrl config option that overrides window.location.origin for link generation (useful behind reverse proxies / with custom domains), exposed via GET /api/config/public-url, and a trustProxy flag so Express honors X-Forwarded-* when fronted by nginx/Caddy/Cloudflare. https://claude.ai/code/session_019FSX3S3UUcKEYWanYmoqUv --- src/data/config.ts | 12 +- src/index.ts | 5 +- src/web/server.ts | 10 ++ web/src/components/Navbar.vue | 205 ++++++++++++++++++++++++++++++---- 4 files changed, 206 insertions(+), 26 deletions(-) diff --git a/src/data/config.ts b/src/data/config.ts index c1a67a5..12bf70f 100755 --- a/src/data/config.ts +++ b/src/data/config.ts @@ -13,7 +13,15 @@ export interface BotConfig { adminGroups: number[]; autoReturnDelay: number; autoPauseOnEmpty: boolean; - idleTimeoutMinutes: number; + idleTimeoutMinutes: number; + // Public base URL used when generating share links (e.g. the bot专属链接). + // Leave empty to use the browser's current origin. Example: + // "https://music.example.com" or "http://1.2.3.4:3000" + publicUrl: string; + // When true, Express trusts X-Forwarded-* headers from a reverse proxy + // (nginx/Caddy/Cloudflare). Required for correct protocol/host detection + // behind HTTPS-terminating proxies. + trustProxy: boolean; } export function getDefaultConfig(): BotConfig { @@ -30,6 +38,8 @@ export function getDefaultConfig(): BotConfig { autoReturnDelay: 300, autoPauseOnEmpty: true, idleTimeoutMinutes: 0, + publicUrl: "", + trustProxy: false, }; } diff --git a/src/index.ts b/src/index.ts index 2f75fd6..9a056a9 100755 --- a/src/index.ts +++ b/src/index.ts @@ -79,7 +79,10 @@ async function main() { await webServer.start(); logger.info({ webPort: config.webPort }, "TSMusicBot started"); - logger.info(`WebUI: http://localhost:${config.webPort}`); + const publicUrl = (config.publicUrl ?? "").trim().replace(/\/+$/, ""); + logger.info( + `WebUI: ${publicUrl || `http://localhost:${config.webPort}`}` + ); const shutdown = () => { logger.info("Shutting down..."); diff --git a/src/web/server.ts b/src/web/server.ts index cb6d85d..90b7dec 100755 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -38,8 +38,18 @@ export function createWebServer(options: WebServerOptions): WebServer { const server = http.createServer(app); const logger = options.logger.child({ component: "web" }); + if (options.config.trustProxy) { + // Honor X-Forwarded-* from a reverse proxy (nginx/Caddy/Cloudflare). + app.set("trust proxy", true); + } + app.use(express.json()); + app.get("/api/config/public-url", (_req, res) => { + const raw = (options.config.publicUrl ?? "").trim(); + res.json({ publicUrl: raw ? raw.replace(/\/+$/, "") : null }); + }); + app.use( "/api/bot", createBotRouter(options.botManager, options.config, options.configPath, logger) diff --git a/web/src/components/Navbar.vue b/web/src/components/Navbar.vue index d6b577b..c095d5d 100644 --- a/web/src/components/Navbar.vue +++ b/web/src/components/Navbar.vue @@ -59,10 +59,30 @@ + +