diff --git a/danmu_api/ui/css/components.css.js b/danmu_api/ui/css/components.css.js
index f6ec486..debf2a8 100644
--- a/danmu_api/ui/css/components.css.js
+++ b/danmu_api/ui/css/components.css.js
@@ -712,4 +712,50 @@ export const componentsCssContent = /* css */ `
display: inline-block;
margin-left: 10px;
}
+/* Bilibili Cookie 编辑器样式 */
+.bili-cookie-editor {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.bili-cookie-status {
+ background: #f8f9fa;
+ padding: 12px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ border-left: 4px solid #667eea;
+}
+
+.bili-status-icon {
+ font-size: 18px;
+}
+
+.bili-status-text {
+ flex: 1;
+ font-weight: 500;
+}
+
+.bili-cookie-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.btn-sm {
+ padding: 6px 12px;
+ font-size: 13px;
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+ .bili-cookie-actions {
+ flex-direction: column;
+ }
+
+ .bili-cookie-actions .btn {
+ width: 100%;
+ }
+}
`;
diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js
index 215c80a..4718c45 100644
--- a/danmu_api/ui/js/systemsettings.js
+++ b/danmu_api/ui/js/systemsettings.js
@@ -527,10 +527,37 @@ function renderValueInput(item) {
} else {
// 文本输入
- // 如果值太长,使用textarea而不是input
- if (value && value.length > 50) {
- // 计算行数,每行约50个字符
- const rows = Math.min(Math.max(Math.ceil(value.length / 50), 3), 10); // 最少3行,最多10行
+ const currentKey = document.getElementById('env-key') ? document.getElementById('env-key').value : '';
+ const isBilibiliCookie = currentKey === 'BILIBILI_COOKIE';
+
+ if (isBilibiliCookie) {
+ // Bilibili Cookie 专用编辑界面
+ const rows = value && value.length > 50 ? Math.min(Math.max(Math.ceil(value.length / 50), 3), 8) : 3;
+ container.innerHTML = \`
+
+
+ 🔍
+ 检测中...
+
+
+
+
+
+
+
+
+
推荐使用扫码登录自动获取,或手动粘贴包含 SESSDATA 和 bili_jct 的完整 Cookie
+
+ \`;
+
+ // 自动检测 Cookie 状态
+ setTimeout(() => {
+ autoCheckBilibiliCookieStatus();
+ }, 100);
+ } else if (value && value.length > 50) {
+ const rows = Math.min(Math.max(Math.ceil(value.length / 50), 3), 10);
container.innerHTML = \`
@@ -1215,4 +1242,190 @@ function removeMapItem(button) {
item.remove();
}
}
+/* ========================================
+ Bilibili Cookie 扫码登录功能
+ ======================================== */
+let biliQRCheckInterval = null;
+let biliBiliQRKey = null;
+
+async function startBilibiliQRLogin() {
+ // 创建扫码登录模态框
+ if (!document.getElementById('bili-qr-modal')) {
+ const modalHTML = \`
+
+ \`;
+ document.body.insertAdjacentHTML('beforeend', modalHTML);
+ }
+
+ const modal = document.getElementById('bili-qr-modal');
+ const qrCode = document.getElementById('bili-qr-code');
+ const qrLoading = document.getElementById('bili-qr-loading');
+ const qrStatus = document.getElementById('bili-qr-status');
+
+ modal.classList.add('active');
+ qrCode.style.display = 'none';
+ qrCode.innerHTML = '';
+ qrLoading.style.display = 'block';
+ qrStatus.textContent = '正在生成二维码...';
+
+ if (biliQRCheckInterval) {
+ clearInterval(biliQRCheckInterval);
+ }
+
+ try {
+ const response = await fetch(buildApiUrl('/api/cookie/qr/generate', true), {
+ method: 'POST'
+ });
+
+ const result = await response.json();
+
+ if (result.success && result.data) {
+ biliBiliQRKey = result.data.qrcode_key;
+ const qrUrl = result.data.url;
+
+ qrCode.innerHTML = '
';
+ qrCode.style.display = 'block';
+ qrLoading.style.display = 'none';
+ qrStatus.textContent = '请使用 Bilibili APP 扫描';
+
+ startBiliQRCheck();
+ } else {
+ throw new Error(result.message || '生成二维码失败');
+ }
+ } catch (error) {
+ qrLoading.style.display = 'none';
+ qrStatus.textContent = '❌ ' + error.message;
+ }
+}
+
+function startBiliQRCheck() {
+ if (!biliBiliQRKey) return;
+
+ const qrStatus = document.getElementById('bili-qr-status');
+
+ biliQRCheckInterval = setInterval(async () => {
+ try {
+ const response = await fetch(buildApiUrl('/api/cookie/qr/check', true), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ qrcode_key: biliBiliQRKey })
+ });
+
+ const result = await response.json();
+
+ if (result.success && result.data) {
+ const code = result.data.code;
+
+ switch (code) {
+ case 86101:
+ qrStatus.textContent = '⏳ 等待扫码...';
+ break;
+ case 86090:
+ qrStatus.textContent = '📱 已扫码,请确认';
+ break;
+ case 86038:
+ qrStatus.textContent = '❌ 二维码已过期';
+ clearInterval(biliQRCheckInterval);
+ break;
+ case 0:
+ qrStatus.textContent = '✅ 登录成功!';
+ clearInterval(biliQRCheckInterval);
+
+ if (result.data.cookie) {
+ fillBilibiliCookie(result.data.cookie);
+ }
+
+ setTimeout(() => {
+ closeBiliQRModal();
+ }, 1000);
+ break;
+ }
+ }
+ } catch (error) {
+ console.error('检查扫码状态失败:', error);
+ }
+ }, 2000);
+}
+
+function fillBilibiliCookie(cookie) {
+ const textInput = document.getElementById('text-value');
+ if (textInput) {
+ textInput.value = cookie;
+ textInput.dispatchEvent(new Event('input', { bubbles: true }));
+
+ textInput.style.borderColor = 'var(--success-color, #28a745)';
+ setTimeout(() => {
+ textInput.style.borderColor = '';
+ // 填入后不立即检测,提示用户保存
+ showBilibiliCookieSaveHint();
+ }, 2000);
+ }
+}
+
+function closeBiliQRModal() {
+ const modal = document.getElementById('bili-qr-modal');
+ if (modal) {
+ modal.classList.remove('active');
+ }
+
+ if (biliQRCheckInterval) {
+ clearInterval(biliQRCheckInterval);
+ }
+}
+
+async function autoCheckBilibiliCookieStatus() {
+ const textInput = document.getElementById('text-value');
+ const statusEl = document.getElementById('bili-cookie-status');
+
+ if (!textInput || !statusEl) return;
+
+ const cookie = textInput.value.trim();
+
+ // 如果输入框为空,提示未配置
+ if (!cookie) {
+ statusEl.innerHTML = '⚠️未配置';
+ return;
+ }
+
+ statusEl.innerHTML = '🔍检测中...';
+
+ try {
+ const response = await fetch(buildApiUrl('/api/cookie/status', true));
+ const result = await response.json();
+
+ if (result.success && result.data && result.data.isValid) {
+ const expiresAt = result.data.expiresAt;
+ const now = Math.floor(Date.now() / 1000);
+ const daysLeft = Math.ceil((expiresAt - now) / (24 * 60 * 60));
+
+ statusEl.innerHTML = \`✅\${result.data.uname} (剩余 \${daysLeft} 天)\`;
+ } else {
+ // 检测到无效时,提示用户保存并重新部署
+ showBilibiliCookieSaveHint();
+ }
+ } catch (error) {
+ statusEl.innerHTML = '⚠️检测失败';
+ }
+}
+// 显示 Bilibili Cookie 保存提示
+function showBilibiliCookieSaveHint() {
+ const statusEl = document.getElementById('bili-cookie-status');
+ if (!statusEl) return;
+
+ statusEl.innerHTML = '💾请点击保存按钮,Vercel等平台需重新部署后生效';
+}
`;
diff --git a/danmu_api/utils/cookie-util.js b/danmu_api/utils/cookie-util.js
new file mode 100644
index 0000000..c99d5fa
--- /dev/null
+++ b/danmu_api/utils/cookie-util.js
@@ -0,0 +1,303 @@
+import { jsonResponse } from './http-util.js';
+import { log } from './log-util.js';
+import { Globals } from '../configs/globals.js';
+import { Envs } from '../configs/envs.js';
+
+// 存储二维码登录会话(内存存储)
+const qrLoginSessions = new Map();
+
+/**
+ * 保存Cookie到环境变量
+ */
+function saveCookieToGlobals(cookie) {
+ try {
+ if (Globals.envs) {
+ Globals.envs.bilibliCookie = cookie;
+ }
+
+ if (Envs.env) {
+ Envs.env.BILIBILI_COOKIE = cookie;
+ }
+
+ if (Envs.originalEnvVars && typeof Envs.originalEnvVars.set === 'function') {
+ Envs.originalEnvVars.set('BILIBILI_COOKIE', cookie);
+ }
+
+ log("info", `Cookie已保存,长度: ${cookie ? cookie.length : 0}`);
+ } catch (err) {
+ log("error", `保存Cookie失败: ${err.message}`);
+ }
+}
+
+/**
+ * 从多个位置获取Cookie
+ */
+function getCookieFromGlobals() {
+ if (Globals.envs && Globals.envs.bilibliCookie) {
+ return Globals.envs.bilibliCookie;
+ }
+ if (Envs.env && Envs.env.BILIBILI_COOKIE) {
+ return Envs.env.BILIBILI_COOKIE;
+ }
+ if (typeof process !== 'undefined' && process.env && process.env.BILIBILI_COOKIE) {
+ return process.env.BILIBILI_COOKIE;
+ }
+ return '';
+}
+
+/**
+ * 验证 Cookie 有效性
+ */
+async function verifyCookieValidity(cookie) {
+ if (!cookie) {
+ return { isValid: false, error: '缺少Cookie' };
+ }
+
+ if (!cookie.includes('SESSDATA') || !cookie.includes('bili_jct')) {
+ return { isValid: false, error: 'Cookie格式不完整' };
+ }
+
+ try {
+ const response = await fetch('https://api.bilibili.com/x/web-interface/nav', {
+ method: 'GET',
+ headers: {
+ 'Cookie': cookie,
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.code === 0 && data.data && data.data.isLogin) {
+ return {
+ isValid: true,
+ data: {
+ uname: data.data.uname,
+ mid: data.data.mid,
+ face: data.data.face
+ }
+ };
+ } else {
+ return { isValid: false, error: data.message || 'Cookie无效或已过期' };
+ }
+ } catch (error) {
+ return { isValid: false, error: '验证请求失败: ' + error.message };
+ }
+}
+
+/**
+ * 解析Cookie中的过期时间
+ */
+function parseExpiresFromCookie(cookie) {
+ try {
+ const sessdataMatch = cookie.match(/SESSDATA=([^;]+)/);
+ if (sessdataMatch) {
+ let sessdata = sessdataMatch[1];
+ try {
+ sessdata = decodeURIComponent(sessdata);
+ } catch (e) {}
+
+ const parts = sessdata.split(',');
+ if (parts.length >= 2) {
+ const timestamp = parseInt(parts[1], 10);
+ const now = Math.floor(Date.now() / 1000);
+ if (!isNaN(timestamp) && timestamp > now) {
+ return timestamp;
+ }
+ }
+ }
+ } catch (e) {}
+ return Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60; // 默认30天
+}
+
+/**
+ * 获取Cookie状态
+ */
+export async function handleCookieStatus() {
+ try {
+ const cookie = getCookieFromGlobals();
+
+ if (!cookie) {
+ return jsonResponse({
+ success: true,
+ data: { isValid: false }
+ });
+ }
+
+ const verifyResult = await verifyCookieValidity(cookie);
+
+ if (verifyResult.isValid) {
+ const expiresAt = parseExpiresFromCookie(cookie);
+
+ return jsonResponse({
+ success: true,
+ data: {
+ isValid: true,
+ uname: verifyResult.data.uname,
+ expiresAt: expiresAt
+ }
+ });
+ } else {
+ return jsonResponse({
+ success: true,
+ data: {
+ isValid: false,
+ error: verifyResult.error
+ }
+ });
+ }
+ } catch (error) {
+ return jsonResponse({ success: false, message: error.message }, 500);
+ }
+}
+
+/**
+ * 生成登录二维码
+ */
+export async function handleQRGenerate() {
+ try {
+ const response = await fetch('https://passport.bilibili.com/x/passport-login/web/qrcode/generate', {
+ method: 'GET',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Referer': 'https://www.bilibili.com'
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.code !== 0) {
+ return jsonResponse({
+ success: false,
+ message: data.message || '生成失败'
+ }, 400);
+ }
+
+ const qrcodeKey = data.data.qrcode_key;
+ const qrcodeUrl = data.data.url;
+
+ qrLoginSessions.set(qrcodeKey, {
+ createTime: Date.now(),
+ status: 'pending'
+ });
+
+ const now = Date.now();
+ for (const [key, session] of qrLoginSessions.entries()) {
+ if (now - session.createTime > 5 * 60 * 1000) {
+ qrLoginSessions.delete(key);
+ }
+ }
+
+ return jsonResponse({
+ success: true,
+ data: {
+ qrcode_key: qrcodeKey,
+ url: qrcodeUrl
+ }
+ });
+ } catch (error) {
+ return jsonResponse({
+ success: false,
+ message: error.message
+ }, 500);
+ }
+}
+
+/**
+ * 检查二维码扫描状态
+ */
+export async function handleQRCheck(request) {
+ try {
+ const body = await request.json();
+ const qrcodeKey = body.qrcodeKey || body.qrcode_key;
+
+ if (!qrcodeKey) {
+ return jsonResponse({ success: false, message: '缺少qrcodeKey参数' }, 400);
+ }
+
+ const response = await fetch(
+ `https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=${qrcodeKey}`,
+ {
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+ }
+ );
+
+ const data = await response.json();
+ let cookie = null;
+
+ if (data.data.code === 0) {
+ // 从URL参数提取Cookie
+ if (data.data.url) {
+ const url = new URL(data.data.url);
+ const params = new URLSearchParams(url.search);
+ const SESSDATA = decodeURIComponent(params.get('SESSDATA') || '');
+ const bili_jct = decodeURIComponent(params.get('bili_jct') || '');
+ const DedeUserID = decodeURIComponent(params.get('DedeUserID') || '');
+
+ if (SESSDATA) {
+ cookie = `SESSDATA=${SESSDATA}; bili_jct=${bili_jct}; DedeUserID=${DedeUserID}`;
+ }
+ }
+ }
+
+ const result = {
+ success: true,
+ data: {
+ code: data.data.code,
+ message: data.data.message || ''
+ }
+ };
+
+ if (cookie) {
+ result.data.cookie = cookie;
+ }
+
+ return jsonResponse(result);
+ } catch (error) {
+ return jsonResponse({ success: false, message: error.message }, 500);
+ }
+}
+
+/**
+ * 保存Cookie
+ */
+export async function handleCookieSave(request) {
+ try {
+ const body = await request.json();
+ const cookie = body.cookie || '';
+
+ if (!cookie) {
+ return jsonResponse({ success: false, message: '缺少cookie参数' }, 400);
+ }
+
+ if (!cookie.includes('SESSDATA') || !cookie.includes('bili_jct')) {
+ return jsonResponse({
+ success: false,
+ message: 'Cookie格式不正确'
+ }, 400);
+ }
+
+ const verifyResult = await verifyCookieValidity(cookie);
+
+ if (!verifyResult.isValid) {
+ return jsonResponse({
+ success: false,
+ message: 'Cookie验证失败: ' + (verifyResult.error || '无效')
+ }, 400);
+ }
+
+ saveCookieToGlobals(cookie);
+
+ return jsonResponse({
+ success: true,
+ data: {
+ uname: verifyResult.data.uname
+ },
+ message: 'Cookie保存成功'
+ });
+ } catch (error) {
+ return jsonResponse({ success: false, message: error.message }, 500);
+ }
+}
\ No newline at end of file
diff --git a/danmu_api/worker.js b/danmu_api/worker.js
index a49d507..a5c0a10 100644
--- a/danmu_api/worker.js
+++ b/danmu_api/worker.js
@@ -8,6 +8,12 @@ import { getBangumi, getComment, getCommentByUrl, getSegmentComment, matchAnime,
import { handleConfig, handleUI, handleLogs, handleClearLogs, handleDeploy, handleClearCache } from "./apis/system-api.js";
import { handleSetEnv, handleAddEnv, handleDelEnv } from "./apis/env-api.js";
import { Segment } from "./models/dandan-model.js"
+import {
+ handleCookieStatus,
+ handleQRGenerate,
+ handleQRCheck,
+ handleCookieSave
+} from "./utils/cookie-util.js";
let globals;
@@ -115,7 +121,8 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
// 智能处理API路径前缀,确保最终有一个正确的 /api/v2
if (path !== "/" && path !== "/api/logs" && !path.startsWith('/api/env')
- && !path.startsWith('/api/deploy') && !path.startsWith('/api/cache')) {
+ && !path.startsWith('/api/deploy') && !path.startsWith('/api/cache')
+ && !path.startsWith('/api/cookie') && !path.startsWith('/api/config')) {
log("info", `[Path Check] Starting path normalization for: "${path}"`);
const pathBeforeCleanup = path; // 保存清理前的路径检查是否修改
@@ -136,7 +143,8 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
// 2. 补全:如果路径缺少前缀(例如请求原始路径为 /search/anime),则补全
const pathBeforePrefixCheck = path;
if (!path.startsWith('/api/v2') && path !== '/' && !path.startsWith('/api/logs')
- && !path.startsWith('/api/env') && !path.startsWith('/api/env') && !path.startsWith('/api/cache')) {
+ && !path.startsWith('/api/env') && !path.startsWith('/api/cache')
+ && !path.startsWith('/api/cookie') && !path.startsWith('/api/config')) {
log("info", `[Path Check] Path is missing /api/v2 prefix. Adding...`);
path = '/api/v2' + path;
}
@@ -356,6 +364,28 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
return handleClearCache();
}
+ // ========== Cookie 管理 API ==========
+
+ // GET /api/cookie/status - 获取Cookie状态
+ if (path === "/api/cookie/status" && method === "GET") {
+ return handleCookieStatus();
+ }
+
+ // POST /api/cookie/qr/generate - 生成登录二维码
+ if (path === "/api/cookie/qr/generate" && method === "POST") {
+ return handleQRGenerate();
+ }
+
+ // POST /api/cookie/qr/check - 检查二维码扫描状态
+ if (path === "/api/cookie/qr/check" && method === "POST") {
+ return handleQRCheck(req);
+ }
+
+ // POST /api/cookie/save - 保存Cookie
+ if (path === "/api/cookie/save" && method === "POST") {
+ return handleCookieSave(req);
+ }
+
return jsonResponse({ message: "Not found" }, 404);
}