fix(web): make bot-link usable on public IP with HTTP

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
This commit is contained in:
Claude committed 2026-04-17 16:08:00 +00:00
1 parent b9e42d543c
commit fecda7cce3
4 files changed
+206 -26

No files matched your search

+11 -1
View File
@@ -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,
};
}
+4 -1
View File
@@ -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...");
+10
View File
@@ -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)