// ==UserScript==
// @name B站字幕获取、AI分析及广告跳过工具
// @namespace http://tampermonkey.net/
// @version 1.5.0
// @description 自动提取B站视频字幕,支持AI生成的CC字幕,通过AI总结+广告识别,自动跳过广告。支持热门评论舆论分析。
// @author LiuMashiro
// @license MIT
// @match *://www.bilibili.com/video/*
// @match *://www.bilibili.com/list/watchlater*
// @match *://www.bilibili.com/bangumi/play/ep*
// @match *://www.bilibili.com/bangumi/play/ss*
// @grant GM_addStyle
// @grant GM_setClipboard
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect api.deepseek.com
// @connect open.bigmodel.cn
// @connect ark.cn-beijing.volces.com
// @connect api.openai.com
// @connect api.anthropic.com
// @connect generativelanguage.googleapis.com
// @connect raw.githubusercontent.com
// @connect scriptcat.org
// @connect *
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ===================== 1. 全局配置 =====================
const SCRIPT_VERSION = '1.5.0';
const GITHUB_REPO_URL = 'https://github.com/LiuMashiro/Bilibili-Subtitle-Extraction-AI-Summary-Ad-Skipping/tree/main';
const GREASYFORK_URL = 'https://greasyfork.org/zh-CN/scripts/579482-b%E7%AB%99%E5%AD%97%E5%B9%95%E8%8E%B7%E5%8F%96-ai%E5%88%86%E6%9E%90%E5%8F%8A%E5%B9%BF%E5%91%8A%E8%B7%B3%E8%BF%87%E5%B7%A5%E5%85%B7';
const SCRIPTCAT_URL = 'https://scriptcat.org/zh-CN/script-show-page/6728';
const CHANGELOG_RAW_URL = 'https://raw.githubusercontent.com/LiuMashiro/Bilibili-Subtitle-Extraction-AI-Summary-Ad-Skipping/main/CHANGELOG.md';
const API_PLATFORMS = {
'deepseek': {
name: 'DeepSeek (性价比高)',
url: 'https://api.deepseek.com/v1/chat/completions',
models: ['deepseek-v4-flash', 'deepseek-v4-pro', '自定义'],
link: 'https://platform.deepseek.com/'
},
'zlm': {
name: '智谱ZLM (提供免费模型)',
url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
models: ['GLM-4.7-Flash (免费)', 'GLM-5.2', 'GLM-5.1', 'GLM-5', 'GLM-5-Turbo', 'GLM-4.7', 'GLM-4.7-FlashX', 'GLM-4.6', 'GLM-4.5-Air', 'GLM-4.5-AirX', 'GLM-4-Long', 'GLM-4-FlashX-250414', 'GLM-4-Flash-250414', '自定义'],
link: 'https://bigmodel.cn/'
},
'doubao': {
name: '火山方舟 (豆包)',
url: 'https://ark.cn-beijing.volces.com/api/v3/chat/completions',
models: ['doubao-seed-2-0-lite-260428', 'doubao-seed-2-0-mini-260428', 'doubao-seed-2-0-pro-260215', '自定义'],
link: 'https://www.volcengine.com/product/ark'
},
'chatgpt': {
name: 'ChatGPT',
url: 'https://api.openai.com/v1/chat/completions',
models: ['gpt-5.5', 'gpt-5.5-pro', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.4-nano', 'gpt-5.4-pro', '自定义'],
link: 'https://platform.openai.com/'
},
'claude': {
name: 'Claude',
url: 'https://api.anthropic.com/v1/messages',
models: ['claude-sonnet-4-6', 'claude-opus-4-8', 'claude-fable-5', 'claude-mythos-5', 'claude-haiku-4-5-20251001', '自定义'],
link: 'https://console.anthropic.com/'
},
'gemini': {
name: 'Gemini',
url: 'https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent',
models: ['gemini-3.1-pro-preview', 'gemini-3.5-flash', 'gemini-3-flash-preview', 'gemini-3.1-flash-lite', 'gemini-3.1-flash-lite-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro', '自定义'],
link: 'https://aistudio.google.com/'
},
'custom': {
name: '自定义',
url: '',
models: ['自定义'],
link: ''
}
};
const TAB_OPTIONS = {
'preview': '浏览',
'ai': 'AI分析',
'text': '文本'
};
const DETAIL_LEVELS = {
'very_detailed': '非常详细',
'detailed': '详细',
'concise': '简洁',
'minimal': '极简'
};
// ===================== 2. 设置读取 =====================
let bse_platform = GM_getValue('bse_platform', 'deepseek');
const _oldGlobalKey = GM_getValue('bse_api_key', '');
if (_oldGlobalKey && !GM_getValue('bse_api_key_' + bse_platform, '')) {
GM_setValue('bse_api_key_' + bse_platform, _oldGlobalKey);
}
let API_KEY = GM_getValue('bse_api_key_' + bse_platform, '') || _oldGlobalKey;
let API_URL = GM_getValue('bse_api_url', API_PLATFORMS['deepseek'].url);
let bse_model = GM_getValue('bse_model', 'deepseek-v4-flash');
let autoGenSummary = GM_getValue('bse_auto_summary', false);
let autoOpenPanel = GM_getValue('bse_auto_open_panel', true);
let autoOpenTab = GM_getValue('bse_auto_open_tab', 'preview');
let enableOpinionAnalysis = GM_getValue('bse_opinion_analysis', true);
let bse_detail_level = GM_getValue('bse_detail_level', 'concise');
let bse_auto_skip_ad = GM_getValue('bse_auto_skip_ad', true);
// ===================== 3. 常量与提示词 =====================
const AD_BRAND_LIST = ["转转", "追觅", "神奇小鹿", "妙界", "拼多多", "加速器", "得物", "萌牙家"];
const AD_MARK_COLOR = 'rgba(255, 193, 7, 0.6)';
function getAISummaryPrompt() {
let summaryWord, overviewWord, listWord;
switch (bse_detail_level) {
case 'very_detailed':
summaryWord = '非常详细';
overviewWord = '全面'; listWord = '详细地分点列出核心结论、关键信息和具体细节(包含论述过程和支撑论据)'; break;
case 'detailed':
summaryWord = '详细';
overviewWord = '详细'; listWord = '详细地分点列出核心结论和关键信息'; break;
case 'minimal':
summaryWord = '极简';
overviewWord = '极简'; listWord = '极简地分点列出核心要点(剔除一切修饰性废话)'; break;
default:
summaryWord = '简洁';
overviewWord = '简明'; listWord = '精简地分点列出核心结论和关键信息(剔除修饰性废话)'; break;
}
return `注意:请不要在总结中提及视频中的任何广告植入、商业推广等内容,只聚焦核心内容。
已知以下品牌均属于广告范畴(包含但不限于):${AD_BRAND_LIST.join('、')}。
字幕包含时间戳([MM:SS.ms]),但在总结内容中请严格剔除时间戳,只保留通顺的文字。字幕为智能识别,可能包含错误。
请根据字幕内容,生成一份【${summaryWord}】的视频总结:
1. ${overviewWord}概括视频核心主题和整体概述。
2. ${listWord}。
最多使用"###"三个井号。
正确的例子:
## 视频总结
### 核心主题
示例内容。
### 核心结论与关键信息
- **示例内容**:
- 示例内容。
如果提供了热门评论数据,在"核心结论与关键信息"之后,使用分割线"---"隔开,输出舆论分析:
## 舆论分析
- 提炼评论区的1-N个主要观点方向(不一定非要是多个,根据情况决定),简明概括每个方向的核心立场,标注每个观点方向的情感倾向(正面/负面/中性/混合)和大约占比。
- 如有高赞代表性观点,可简要引用(无需标注用户名)
- 一句话概括评论区整体氛围
如果没有提供评论数据,则跳过此部分,不输出"---"和"### 舆论分析"。
识别中间插入的广告。在全文的最后末尾列出"广告时间"部分,支持以下两种格式:
格式A(同一行):广告时间[MM:SS - MM:SS]
格式B(分行,标题后换行):
### 广告时间
[MM:SS - MM:SS]
规则:
- 如果视频中没有广告,请严格回复:广告时间[无]
- 如果有多段中间插入的广告,取最长的一段。
- <5s的广告时间忽略不计。
- 只包含分钟和秒,禁止任何其他多余文字、符号或标点。
- "-"左右包含空格
- 超长视频允许分钟数值大于60,如[70:00 - 75:00]。禁止小时位。禁止分秒毫秒位。
使用清晰的Markdown格式进行排版,包括正确的分级标题、列表缩进等。`;
}
// ===================== 4. 安全策略 =====================
let trustedPolicy = null;
if (window.trustedTypes && window.trustedTypes.createPolicy) {
try { trustedPolicy = window.trustedTypes.createPolicy('bsePolicy', { createHTML: s => s });
} catch (e) {}
}
function safeSetInnerHTML(el, html) {
if (trustedPolicy) el.innerHTML = trustedPolicy.createHTML(html);
else el.innerHTML = html;
}
// ===================== 5. 样式 =====================
GM_addStyle(`
:root {
--bse-primary: #00AEEC;
--bse-primary-hover: #0098ce;
--bse-bg-glass: rgba(255,255,255,0.98);
--bse-bg-card: #f8fafc;
--bse-border: #e2e8f0;
--bse-text: #0f172a;
--bse-text-dim: #64748b;
--bse-text-muted: #94a3b8;
--bse-shadow: 0 12px 40px -10px rgba(0,0,0,0.12), 0 4px 16px -4px rgba(0,0,0,0.06);
--bse-radius-lg: 20px;
--bse-radius-md: 14px;
--bse-radius-sm: 10px;
--bse-warning: #ffc107;
--bse-warning-bg: #fff3cd;
--bse-warning-border: #ffeeba;
--bse-warning-text: #856404;
--bse-ad-bg: #fffbeb;
--bse-ad-border: #fbbf24;
--bse-ad-text: #92400e;
--bse-ad-button: #f59e0b;
--bse-ad-button-hover: #FF8C00;
}
* { font-family: -apple-system,BlinkMacSystemFont,"Microsoft YaHei",sans-serif !important;
}
.bse-container { position:fixed; z-index:100000; right:24px; top:80px;
}
.bse-trigger-btn { width:52px; height:52px; border-radius:16px; background:var(--bse-primary); border:none; cursor:pointer; box-shadow:0 8px 24px rgba(0,174,236,0.3);
display:flex; align-items:center; justify-content:center; transition:all 0.25s cubic-bezier(0.4,0,0.2,1); position:relative; }
.bse-trigger-btn:hover { transform:translateY(-2px) scale(1.04);
box-shadow:0 12px 32px rgba(0,174,236,0.4); }
.bse-trigger-btn:active { transform:translateY(0) scale(0.98);
}
.bse-trigger-btn svg { width:24px; height:24px; fill:white; transition:transform 0.3s ease;
}
.bse-trigger-btn:hover svg { transform:scale(1.1);
}
.bse-status-dot { position:absolute; top:-2px; right:-2px; width:12px; height:12px; border-radius:50%; border:2px solid white; transition:background 0.3s, transform 0.3s; display:none; }
.bse-status-dot.state-yellow { display:block; background:#f59e0b; transform:scale(1.1); }
.bse-status-dot.state-green { display:block; background:#10b981; transform:scale(1.1); }
@keyframes bse-pulse { 0%,100%{opacity:1; transform:scale(1)}50%{opacity:0.4;
transform:scale(1.2)} }
@keyframes bse-spin { to{transform:rotate(360deg)} }
@keyframes bse-fadein { from{opacity:0;transform:translateY(-10px) scale(0.98)}to{opacity:1;transform:none} }
@keyframes bse-fadeout { from{opacity:1;transform:none}to{opacity:0;transform:translateY(-10px) scale(0.98)} }
@keyframes bse-slideup { from{opacity:0;transform:translateY(-12px)}to{opacity:1;transform:none} }
@keyframes bse-shake { 0%,100%{transform:translateX(0)}10%,30%,50%,70%,90%{transform:translateX(-2px)}20%,40%,60%,80%{transform:translateX(2px)} }
.bse-panel { position:absolute;
top:66px; right:0;
width:430px;
max-height:min(calc(100vh - 120px), 66vh);
background:var(--bse-bg-glass); backdrop-filter:blur(24px); border-radius:var(--bse-radius-lg); box-shadow:var(--bse-shadow); border:1px solid rgba(255,255,255,0.4); display:none; flex-direction:column; overflow:hidden; animation:bse-fadein 0.25s cubic-bezier(0.16,1,0.3,1);
}
.bse-panel.show { display:flex;
}
.bse-panel.hiding { animation:bse-fadeout 0.2s ease forwards;
}
.bse-header { padding:18px 22px 14px; border-bottom:1px solid var(--bse-border); display:flex; align-items:center; justify-content:space-between; flex-shrink:0;
}
.bse-title { font-size:16px; font-weight:700; color:var(--bse-text); margin:0; display:flex; align-items:center; gap:8px; flex-wrap:wrap;
}
.bse-platform-tag { display:inline-block; padding:3px 8px; background:rgba(0,174,236,0.1); color:var(--bse-primary); font-size:11px; font-weight:700; border-radius:6px;
}
.bse-subtitle-info { font-size:13px; color:var(--bse-text-dim); margin-top:4px; font-weight:500; transition:color 0.3s;
}
.bse-ad-hint { font-size:12px; color:var(--bse-warning-text); margin-top:2px; font-weight:500; display:flex; align-items:center; gap:4px; flex-wrap:wrap;
}
.bse-header-actions { display:flex; align-items:center; gap:8px; flex-shrink:0;
}
.bse-icon-btn { width:34px; height:34px; border-radius:var(--bse-radius-sm); background:var(--bse-bg-card); border:1px solid var(--bse-border); cursor:pointer; display:flex; align-items:center;
justify-content:center; color:var(--bse-text-dim); transition:all 0.2s; text-decoration:none; }
.bse-icon-btn:hover { background:#e2e8f0; color:var(--bse-text); transform:scale(1.05);
}
.bse-icon-btn:active { transform:scale(0.95);
}
.bse-icon-btn svg { width:18px; height:18px; fill:currentColor; transition:transform 0.4s ease;
}
.bse-icon-btn.spinning svg { animation:bse-spin 0.8s linear infinite;
}
.bse-icon-btn.settings-btn:hover svg { transform:rotate(90deg);
}
.bse-update-badge { display:inline-flex; align-items:center; gap:4px; padding:2px 8px; background:linear-gradient(135deg, #ef4444, #dc2626); color:white; font-size:11px;
font-weight:700; border-radius:8px; cursor:pointer; text-decoration:none; transition:all 0.2s; margin-left:4px; vertical-align:middle; white-space:nowrap; }
.bse-update-badge:hover { transform:scale(1.05);
box-shadow:0 2px 8px rgba(220,38,38,0.4); color:white; text-decoration:none; }
.bse-ext-links { display:flex; gap:8px; justify-content:center; align-items:center; flex-wrap: wrap;
margin-bottom:14px; }
.bse-ext-link { display:inline-flex; align-items:center; gap:5px; padding:5px 12px; border-radius:8px; text-decoration:none; font-size:12px; font-weight:500;
transition:all 0.2s; color:var(--bse-text-dim); background:var(--bse-bg-card); border:1px solid var(--bse-border); }
.bse-ext-link:hover { color:var(--bse-text); border-color:#cbd5e1; transform:translateY(-1px);
box-shadow:0 2px 6px rgba(0,0,0,0.06); text-decoration:none; }
.bse-ext-link svg { width:14px; height:14px; fill:currentColor; flex-shrink:0;
}
.bse-api-warning { background:var(--bse-warning-bg); border:1px solid var(--bse-warning-border); border-radius:var(--bse-radius-md); padding:12px 16px; margin:16px 22px 0;
display:flex; align-items:center; gap:10px; animation:bse-shake 0.5s ease; }
.bse-api-warning-icon { font-size:18px;
}
.bse-api-warning-text { flex:1; font-size:13px; color:var(--bse-warning-text); font-weight:600;
}
.bse-api-warning-btn { background:var(--bse-warning); color:white; border:none; border-radius:var(--bse-radius-sm); padding:6px 12px; font-size:12px; font-weight:600; cursor:pointer;
transition:all 0.2s; }
.bse-api-warning-btn:hover { background:#e0a800; transform:translateY(-1px);
}
.bse-api-warning-btn:active { transform:translateY(0);
}
.bse-source-section { border-bottom:1px solid var(--bse-border); flex-shrink:0;
}
.bse-source-header { display:flex; align-items:center; justify-content:space-between; padding:12px 22px; cursor:pointer; user-select:none; transition:background 0.2s;
}
.bse-source-header:hover { background:rgba(0,0,0,0.02);
}
.bse-source-label { font-size:13px; font-weight:600; color:var(--bse-text-dim);
}
.bse-source-arrow { width:20px; height:20px; display:flex; align-items:center; justify-content:center; transition:transform 0.3s cubic-bezier(0.4,0,0.2,1); color:var(--bse-text-dim);
}
.bse-source-arrow svg { width:16px; height:16px; fill:currentColor;
}
.bse-source-arrow.collapsed { transform:rotate(-90deg);
}
.bse-source-body { padding:0 22px 14px; display:flex; flex-wrap:wrap; gap:8px; animation:bse-slideup 0.3s ease;
}
.bse-source-body.hidden { display:none;
}
.bse-subtitle-option { padding:6px 14px; background:white; border:1px solid var(--bse-border); border-radius:20px; color:var(--bse-text); font-size:13px; font-weight:500;
cursor:pointer; transition:all 0.25s cubic-bezier(0.4,0,0.2,1); display:flex; align-items:center; gap:6px; position:relative; overflow:hidden; }
.bse-subtitle-option::before { content:'';
position:absolute; top:0; left:0; width:0; height:100%; background:var(--bse-primary); opacity:0.1; transition:width 0.3s ease;
}
.bse-subtitle-option:hover { border-color:#cbd5e1; transform:translateY(-1px); box-shadow:0 2px 8px rgba(0,0,0,0.06);
}
.bse-subtitle-option:hover::before { width:100%;
}
.bse-subtitle-option:active { transform:translateY(0);
}
.bse-subtitle-option.active { background:var(--bse-primary); border-color:var(--bse-primary); color:white; transform:scale(1.02); box-shadow:0 4px 12px rgba(0,174,236,0.25);
}
.bse-subtitle-option.active::before { display:none;
}
.bse-tag { font-size:10px; font-weight:700; padding:2px 6px; border-radius:6px; transition:all 0.2s;
}
.bse-subtitle-option:not(.active) .bse-tag.ai { background:rgba(0,174,236,0.1); color:var(--bse-primary);
}
.bse-subtitle-option:not(.active) .bse-tag.cc { background:rgba(16,185,129,0.1); color:#10b981;
}
.bse-subtitle-option.active .bse-tag { background:rgba(255,255,255,0.2); color:white;
}
.bse-tabs { display:flex; padding:5px; background:var(--bse-bg-card); border-radius:var(--bse-radius-md); margin:16px 22px 4px; gap:4px; flex-shrink:0;
}
.bse-tabs.hidden { display:none;
}
.bse-tab { flex:1; padding:8px 0; border:none; background:transparent; color:var(--bse-text-dim); font-size:13.5px; font-weight:600; cursor:pointer; border-radius:var(--bse-radius-sm);
transition:all 0.25s cubic-bezier(0.4,0,0.2,1); text-align:center; position:relative; overflow:hidden; }
.bse-tab::before { content:''; position:absolute; bottom:0; left:50%;
width:0; height:2px; background:var(--bse-primary); transition:all 0.3s ease; transform:translateX(-50%); }
.bse-tab:hover:not(.active) { color:var(--bse-text); background:rgba(255,255,255,0.5);
}
.bse-tab:hover:not(.active)::before { width:60%;
}
.bse-tab.active { background:white; color:var(--bse-primary); box-shadow:0 2px 8px rgba(0,0,0,0.06); transform:translateY(-1px);
}
.bse-tab.active::before { width:80%;
}
.bse-content { flex:1; min-height:0; overflow-y:auto; padding:14px 22px 20px;
}
.bse-content::-webkit-scrollbar { width:6px;
}
.bse-content::-webkit-scrollbar-thumb { background:#cbd5e1; border-radius:4px; transition:background 0.2s;
}
.bse-content::-webkit-scrollbar-thumb:hover { background:#94a3b8;
}
.bse-checkbox-label { display:flex; align-items:center; gap:8px; font-size:14px; font-weight:500; color:var(--bse-text); cursor:pointer; user-select:none; transition:color 0.2s;
}
.bse-checkbox-label:hover { color:var(--bse-primary);
}
.bse-checkbox-label input[type="checkbox"] { width:16px; height:16px; accent-color:#7dd3fc; cursor:pointer; margin:0; flex-shrink:0; transition:transform 0.2s;
}
.bse-checkbox-label input[type="checkbox"]:hover { transform:scale(1.1);
}
.bse-text-controls { display:flex; align-items:center; justify-content:space-between; margin-bottom:14px; padding:10px 14px; background:white; border-radius:var(--bse-radius-sm);
border:1px solid var(--bse-border); transition:box-shadow 0.2s; }
.bse-text-controls:hover { box-shadow:0 2px 8px rgba(0,0,0,0.04);
}
.bse-text-area { width:100%; min-height:280px; background:white; border:1px solid var(--bse-border); border-radius:var(--bse-radius-md); padding:16px; color:var(--bse-text); font-size:14px;
line-height:1.7; resize:vertical; box-sizing:border-box; transition:all 0.2s; }
.bse-text-area:focus { outline:none; border-color:var(--bse-primary);
box-shadow:0 0 0 3px rgba(0,174,236,0.1); transform:translateY(-1px); }
.bse-loading, .bse-empty { display:flex; flex-direction:column; align-items:center;
justify-content:center; padding:60px 20px; color:var(--bse-text-dim); font-size:15px; font-weight:500; gap:16px; animation:bse-slideup 0.3s ease;
}
.bse-spinner { width:32px; height:32px; border:3px solid rgba(0,174,236,0.15); border-top-color:var(--bse-primary); border-radius:50%;
animation:bse-spin 0.8s linear infinite; }
.bse-stats { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:16px;
}
.bse-stat-item { background:white; border:1px solid var(--bse-border); border-radius:var(--bse-radius-md); padding:14px; text-align:center; transition:all 0.2s;
}
.bse-stat-item:hover { transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.06);
}
.bse-stat-label { font-size:12px; font-weight:600; color:var(--bse-text-dim); margin-bottom:6px;
}
.bse-stat-value { font-size:20px; font-weight:800; color:var(--bse-text); transition:color 0.2s;
}
.bse-stat-item:hover .bse-stat-value { color:var(--bse-primary);
}
.bse-subtitle-item { padding:14px 16px; margin-bottom:10px; background:white; border-radius:var(--bse-radius-md); border:1px solid var(--bse-border); cursor:pointer;
transition:all 0.25s cubic-bezier(0.4,0,0.2,1); display:flex; flex-direction:column; gap:6px; position:relative; overflow:hidden; }
.bse-subtitle-item::before { content:''; position:absolute;
left:0; top:0; width:3px; height:0; background:var(--bse-primary); transition:height 0.3s ease; }
.bse-subtitle-item:hover { border-color:#cbd5e1;
box-shadow:0 4px 12px rgba(0,0,0,0.04); transform:translateY(-1px); }
.bse-subtitle-item:hover::before { height:100%;
}
.bse-subtitle-item:active { transform:translateY(0);
}
.bse-ts { font-size:12px; color:var(--bse-primary); font-family:monospace; font-weight:700; background:rgba(0,174,236,0.06); align-self:flex-start; padding:2px 6px; border-radius:4px;
transition:all 0.2s; }
.bse-subtitle-item:hover .bse-ts { background:var(--bse-primary); color:white;
}
.bse-st { font-size:14.5px; color:var(--bse-text); line-height:1.6;
}
.bse-ai-big-btn { width:100%; padding:14px; background:var(--bse-primary); color:white; border:none; border-radius:var(--bse-radius-md); font-size:15px; font-weight:600; cursor:pointer; margin-bottom:16px;
display:flex; align-items:center; justify-content:center; gap:8px; transition:all 0.25s cubic-bezier(0.4,0,0.2,1); box-shadow:0 4px 16px rgba(0,174,236,0.25); position:relative; overflow:hidden;
}
.bse-ai-big-btn::before { content:''; position:absolute; top:0; left:-100%; width:100%; height:100%; background:linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition:left 0.5s ease; }
.bse-ai-big-btn:hover:not(:disabled) { background:var(--bse-primary-hover); transform:translateY(-2px); box-shadow:0 8px 24px rgba(0,174,236,0.35);
}
.bse-ai-big-btn:hover:not(:disabled)::before { left:100%;
}
.bse-ai-big-btn:active:not(:disabled) { transform:translateY(0);
}
.bse-ai-big-btn:disabled { opacity:0.5; cursor:not-allowed;
}
.bse-retry-btn { position:absolute; top:16px; right:16px; width:32px; height:32px; background:#f1f5f9; border:none; border-radius:8px; cursor:pointer; display:flex;
align-items:center; justify-content:center; color:var(--bse-text-dim); z-index:10; transition:background 0.2s, color 0.2s; animation:bse-slideup 0.3s ease;
}
.bse-retry-btn:hover { background:var(--bse-primary); color:white;
}
.bse-retry-btn svg { width:16px; height:16px; fill:currentColor; transition:transform 0.4s ease;
}
.bse-retry-btn:hover svg { transform:rotate(180deg) scale(1.1);
}
.bse-ai-result { background:white; border-radius:var(--bse-radius-md); padding:24px; margin-bottom:16px; border:1px solid var(--bse-border); color:var(--bse-text); line-height:1.8; font-size:15px;
transition:box-shadow 0.2s; animation:bse-slideup 0.3s ease; }
.bse-ai-result:hover { box-shadow:0 4px 12px rgba(0,0,0,0.04);
}
.bse-markdown h1 { font-size:20px; font-weight:800; margin:24px 0 12px; padding-bottom:10px; border-bottom:1px solid var(--bse-border);
}
.bse-markdown h2 { font-size:18px; font-weight:700; margin:20px 0 10px;
}
.bse-markdown h3 { font-size:16px; font-weight:700; color:var(--bse-primary); margin:18px 0 8px;
}
.bse-markdown p { margin-bottom:14px; font-size:15px; color:#334155;
}
.bse-markdown ul,.bse-markdown ol { margin:10px 0 16px; padding-left:24px;
}
.bse-markdown ul { list-style-type:disc;
}
.bse-markdown li { margin-bottom:8px; font-size:15px; color:#334155; line-height:1.7;
}
.bse-markdown strong { color:var(--bse-text); font-weight:700;
}
.bse-markdown code { background:#f1f5f9; color:var(--bse-primary); padding:2px 6px; border-radius:4px; font-size:13.5px;
}
.bse-markdown blockquote { border-left:4px solid var(--bse-primary); margin:14px 0; padding:10px 16px; background:#f0f9ff;
border-radius:0 var(--bse-radius-sm) var(--bse-radius-sm) 0; color:var(--bse-text-dim); }
.bse-markdown hr { border:none; height:1px; background:var(--bse-border);
margin:20px 0; }
.bse-sp-box { border-radius:24px; padding:16px 20px; margin-bottom:16px; display:flex; flex-direction:column; gap:10px;
animation:bse-slideup 0.3s ease; }
.bse-sp-box.status-found { background:var(--bse-ad-bg); border:2px solid var(--bse-ad-border);
box-shadow:0 4px 16px rgba(251,191,36,0.15); }
.bse-sp-box.status-none { background:linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
border:1px solid #22c55e; box-shadow:0 4px 12px rgba(34,197,94,0.1); }
.bse-sp-box.status-err { background:linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
border:1px solid #ef4444; box-shadow:0 4px 12px rgba(239,68,68,0.1); }
.bse-sp-header { display:flex; align-items:center; gap:10px;
flex-wrap:wrap; }
.bse-sp-icon { width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:bold; flex-shrink:0;
}
.status-found .bse-sp-icon { background:#FF8C00; color:white; box-shadow:0 2px 8px rgba(217,119,6,0.4);
}
.status-none .bse-sp-icon { background:#22c55e; color:white; box-shadow:0 2px 8px rgba(34,197,94,0.3);
}
.status-err .bse-sp-icon { background:#ef4444; color:white; box-shadow:0 2px 8px rgba(239,68,68,0.3);
}
.bse-sp-title { font-size:14px; font-weight:700; flex:1;
}
.status-found .bse-sp-title { color:var(--bse-ad-text);
}
.status-none .bse-sp-title { color:#166534;
}
.status-err .bse-sp-title { color:#991b1b;
}
.bse-sp-badge { background:white; border:1px solid var(--bse-ad-border); border-radius:10px; padding:6px 12px; font-family:monospace; font-size:13px; font-weight:700;
color:var(--bse-ad-text); box-shadow:0 2px 4px rgba(0,0,0,0.05); }
.bse-sp-action-row { display:flex; align-items:center; gap:10px; margin-left:34px;
}
.bse-sp-action-row .bse-sp-badge { flex:1;
}
.bse-sp-skip { background:var(--bse-ad-button); color:white; border:none; border-radius:10px;
padding:8px 16px; font-size:13px; font-weight:600; cursor:pointer; transition:all 0.25s cubic-bezier(0.4,0,0.2,1); box-shadow:0 2px 8px rgba(245,158,11,0.3); flex-shrink:0;
}
.bse-sp-skip:hover { background:var(--bse-ad-button-hover); transform:translateY(-2px) scale(1.02); box-shadow:0 4px 12px rgba(245,158,11,0.4);
}
.bse-sp-skip:active { transform:translateY(0) scale(0.98);
}
.bse-sp-hint { font-size:12px; color:#b45309; margin-left:34px;
}
.bse-followup-section { margin-top:4px; background:white; border:1px solid var(--bse-border); border-radius:var(--bse-radius-md); padding:16px; transition:box-shadow 0.2s;
animation:bse-slideup 0.3s ease; }
.bse-followup-section:hover { box-shadow:0 4px 12px rgba(0,0,0,0.04);
}
.bse-followup-label { font-size:13px; font-weight:700; color:var(--bse-primary); margin-bottom:10px; display:flex; align-items:center; gap:6px;
}
.bse-followup-input { width:100%; background:#f8fafc; border:1px solid var(--bse-border); border-radius:var(--bse-radius-sm); padding:12px 14px; color:var(--bse-text); font-size:14px;
margin-bottom:12px; resize:none; height:72px; box-sizing:border-box; transition:all 0.2s; }
.bse-followup-input:focus { outline:none; border-color:var(--bse-primary); background:white; transform:translateY(-1px);
box-shadow:0 0 0 3px rgba(0,174,236,0.1); }
.bse-followup-btn { width:100%; padding:12px; background:var(--bse-primary); color:white; border:none;
border-radius:var(--bse-radius-sm); font-size:14px; font-weight:600; cursor:pointer; transition:all 0.2s; }
.bse-followup-btn:hover:not(:disabled) { background:var(--bse-primary-hover); transform:translateY(-1px);
}
.bse-followup-btn:disabled { opacity:0.5; cursor:not-allowed;
}
.bse-qa-item { margin-top:16px; padding-top:16px; border-top:1px solid var(--bse-border); animation:bse-slideup 0.3s ease;
}
.bse-qa-q { font-size:14px; font-weight:700; color:var(--bse-text); margin-bottom:10px; background:#f1f5f9; padding:10px 14px; border-radius:var(--bse-radius-sm); transition:background 0.2s;
}
.bse-qa-q:hover { background:#e2e8f0;
}
.bse-qa-a { font-size:14.5px; color:var(--bse-text); line-height:1.7; padding:0 4px;
}
.bse-settings-group { margin-bottom:8px;
}
.bse-settings-group + .bse-settings-group { margin-top:28px; padding-top:24px; border-top:1px solid var(--bse-border);
}
.bse-settings-group-title { font-size:15px; font-weight:800; color:var(--bse-text); margin-bottom:16px; display:flex; align-items:center; gap:8px;
}
.bse-settings-group-title-dot { width:7px; height:7px; border-radius:50%; background:var(--bse-primary); flex-shrink:0;
}
.bse-settings-subgroup { margin-bottom:22px;
}
.bse-settings-subgroup:last-child { margin-bottom:0;
}
.bse-settings-subgroup-title { font-size:13.5px; font-weight:700; color:var(--bse-text); margin-bottom:14px; letter-spacing:0.2px;
}
.bse-settings-subgroup + .bse-settings-subgroup { padding-top:18px; border-top:1px solid var(--bse-border);
}
.bse-settings-block { margin-bottom:16px;
}
.bse-settings-block:last-child { margin-bottom:0;
}
.bse-settings-block-label { display:block; font-size:13px; font-weight:600; color:var(--bse-text-dim); margin-bottom:8px;
}
.bse-settings-input { width:100%; padding:12px 14px; background:#f8fafc; border:1px solid var(--bse-border); border-radius:var(--bse-radius-sm); font-size:14px; color:var(--bse-text);
box-sizing:border-box; transition:all 0.2s; }
.bse-settings-input:focus { outline:none; border-color:var(--bse-primary); background:white;
box-shadow:0 0 0 3px rgba(0,174,236,0.1); transform:translateY(-1px); }
.bse-settings-check-row { display:flex; align-items:flex-start; gap:10px; cursor:pointer;
user-select:none; transition:opacity 0.2s; }
.bse-settings-check-row:hover { opacity:0.9;
}
.bse-settings-check-row input[type="checkbox"] { width:16px; height:16px; margin-top:3px; accent-color:#7dd3fc; cursor:pointer; flex-shrink:0; transition:transform 0.2s;
}
.bse-settings-check-row input[type="checkbox"]:hover { transform:scale(1.1);
}
.bse-settings-check-text { display:flex; flex-direction:column; gap:4px;
}
.bse-settings-check-title { font-size:14px; font-weight:500; color:var(--bse-text); line-height:1.4;
}
.bse-settings-check-desc { font-size:12px; color:var(--bse-text-muted); line-height:1.5;
}
.bse-author-info { margin-top:28px; padding-top:24px; border-top:1px solid var(--bse-border); text-align:center;
}
.bse-author-text { font-size:13px; color:var(--bse-text-muted);
}
.bse-author-link { color:var(--bse-primary); text-decoration:none; font-weight:400; transition:color 0.2s;
}
.bse-author-link:hover { color:var(--bse-primary-hover); text-decoration:underline;
}
.bse-footer { padding:16px 22px; border-top:1px solid var(--bse-border); display:flex; gap:12px; flex-shrink:0; flex-direction:column;
}
.bse-btn { flex:1; min-width:0; padding:12px 8px; border:none; border-radius:var(--bse-radius-md); font-size:13.5px; font-weight:600; cursor:pointer; display:flex; align-items:center; justify-content:center;
gap:6px; white-space:nowrap; transition:all 0.25s cubic-bezier(0.4,0,0.2,1); position:relative; overflow:hidden; }
.bse-btn::before { content:''; position:absolute; top:0; left:-100%;
width:100%; height:100%; background:linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); transition:left 0.5s ease; }
.bse-btn:hover:not(:disabled)::before { left:100%;
}
.bse-btn-primary { background:var(--bse-primary); color:white;
}
.bse-btn-primary:hover:not(:disabled) { background:var(--bse-primary-hover); transform:translateY(-1px); box-shadow:0 4px 12px rgba(0,174,236,0.2);
}
.bse-btn-primary:active:not(:disabled) { transform:translateY(0);
}
.bse-btn-primary:disabled { opacity:0.5; cursor:not-allowed;
}
.bse-btn-secondary { background:white; color:var(--bse-text); border:1px solid var(--bse-border);
}
.bse-btn-secondary:hover:not(:disabled) { background:#f8fafc; border-color:#cbd5e1; transform:translateY(-1px); box-shadow:0 2px 8px rgba(0,0,0,0.04);
}
.bse-btn-secondary:active:not(:disabled) { transform:translateY(0);
}
.bse-btn-secondary:disabled { opacity:0.5; cursor:not-allowed;
}
.bse-toast { position:fixed; bottom:80px; left:50%; transform:translateX(-50%) translateY(16px) scale(0.95); background:rgba(15,23,42,0.95); color:white; padding:12px 24px;
border-radius:12px; font-size:14px; font-weight:500; opacity:0; transition:opacity 0.25s ease, transform 0.25s cubic-bezier(0.16,1,0.3,1); z-index:100001; pointer-events:none; white-space:nowrap;
}
.bse-toast.show { opacity:1; transform:translateX(-50%) translateY(0) scale(1);
}
.bse-toast.success { background:rgba(16,185,129,0.95);
}
.bse-toast.error { background:rgba(239,68,68,0.95);
}
.bse-toast.warning { background:rgba(255,193,7,0.95); }
`);
// ===================== 6. 全局状态 =====================
let allSubtitles = [];
let currentSubtitleData = null;
let selectedSubtitleId = null;
let panelVisible = false;
let currentTab = 'preview';
let isLoading = false;
let showTimestamps = true;
let showRawAIText = false;
let sourceCollapsed = true;
let currentVideoKey = null;
let currentAid = null;
let hotComments = [];
let aiSummaryCache = {};
let aiConversationHistory = [];
let adSegments = [];
let hasJumpedAds = {};
let adSkipInterval = null;
let progressMarkObserver = null;
let isGeneratingAI = false;
let autoGenerateTimer = null;
let currentGenerationId = 0;
let progressMarkInitialized = false;
let lastAdCheckResult = null;
let latestVersion = null;
let hasUpdate = false;
let updateLinkUrl = null;
let _documentClickHandler = null;
// ===================== 7. 日志工具 =====================
function log(...args) { console.log('[BSE]', ...args);
}
function _ts() {
const n = new Date();
return `${String(n.getHours()).padStart(2, '0')}:${String(n.getMinutes()).padStart(2, '0')}:${String(n.getSeconds()).padStart(2, '0')}.${String(n.getMilliseconds()).padStart(3, '0')}`;
}
function logAPI(action, data) { console.log(`[BSE-API] [${_ts()}] ${action}`, data !== undefined ? data : '');
}
// ===================== 8. 缓存管理 =====================
function loadCache() {
const raw = GM_getValue('aiSummaryCache', {});
const result = {};
for (const key of Object.keys(raw)) {
const val = raw[key];
if (typeof val === 'string') result[key] = { summary: val, qa: [] };
else if (val && typeof val === 'object' && typeof val.summary === 'string') result[key] = { summary: val.summary, qa: Array.isArray(val.qa) ?
val.qa : [] };
}
return result;
}
function getCachedSummary(videoKey) {
const entry = aiSummaryCache[videoKey];
if (!entry) return null;
return typeof entry === 'string' ? entry : entry.summary || null;
}
function getCachedQA(videoKey) {
const entry = aiSummaryCache[videoKey];
if (!entry || typeof entry === 'string') return [];
return Array.isArray(entry.qa) ? entry.qa : [];
}
function setCachedSummary(videoKey, summary) {
const existing = aiSummaryCache[videoKey];
const qa = (existing && Array.isArray(existing.qa)) ? existing.qa : [];
aiSummaryCache[videoKey] = { summary, qa };
GM_setValue('aiSummaryCache', aiSummaryCache);
}
function appendCachedQA(videoKey, q, a) {
const entry = aiSummaryCache[videoKey];
if (!entry) return;
if (!Array.isArray(entry.qa)) entry.qa = [];
entry.qa.push({ q, a });
GM_setValue('aiSummaryCache', aiSummaryCache);
}
// ===================== 9. 通用工具 =====================
function formatTime(s) { const m = Math.floor(s / 60), sec = Math.floor(s % 60);
return `${m}:${sec.toString().padStart(2, '0')}`; }
function formatTimeWithMs(s) { const m = Math.floor(s / 60), sec = Math.floor(s % 60), ms = Math.floor((s % 1) * 100);
return `${m}:${sec.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}`; }
function formatTimeForSRT(s) { const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60); const sec = Math.floor(s % 60);
const ms = Math.floor((s % 1) * 1000); return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`;
}
function parseAdTime(str) { str = str.trim(); const m = str.match(/^(\d+):(\d{2})$/); return m ?
parseInt(m[1]) * 60 + parseInt(m[2]) : null; }
function escapeHtml(t) { if (!t) return '';
return t.replace(/&/g, '&').replace(//g, '>'); }
function formatCommentsForAI() {
if (!hotComments.length) return '';
return hotComments.map(c => `${c.content.length > 200 ? c.content.slice(0, 200) + '...' : c.content} ${c.like}`).join('\n');
}
function showToast(msg, type = '') {
let el = document.querySelector('.bse-toast');
if (!el) { el = document.createElement('div'); el.className = 'bse-toast'; document.body.appendChild(el);
}
el.textContent = msg;
el.className = 'bse-toast' + (type ? ' ' + type : '');
void el.offsetWidth; el.classList.add('show');
clearTimeout(el._t);
el._t = setTimeout(() => el.classList.remove('show'), 2500);
}
function seekToTime(sec) { const v = document.querySelector('video');
if (v) { v.currentTime = sec; showToast(`跳转到 ${formatTime(sec)}`, 'success'); } }
function setLoadingState(loading) { isLoading = loading;
document.querySelector('#bse-refresh-btn')?.classList.toggle('spinning', loading); }
function getVideoTitle() { const h1 = document.querySelector('h1.video-title'); if (!h1) return '';
return h1.dataset.title || h1.getAttribute('title') || h1.textContent.trim(); }
function getVideoDescription() { const descElement = document.querySelector('.desc-info-text');
if (!descElement) return ''; return descElement.textContent.trim(); }
function getVideoTags() { const tagElements = document.querySelectorAll('.tag-link .tag-name');
if (!tagElements || tagElements.length === 0) return []; return Array.from(tagElements).map(tag => tag.textContent.trim());
}
function compareVersions(v1, v2) {
const p1 = v1.split('.').map(Number), p2 = v2.split('.').map(Number);
for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
const a = p1[i] || 0, b = p2[i] || 0;
if (a > b) return 1; if (a < b) return -1;
}
return 0;
}
let scriptcatCheckResult = null;
let githubCheckResult = null;
let scriptcatCheckDone = false;
let githubCheckDone = false;
function resolveUpdateAfterChecks() {
if (!scriptcatCheckDone || !githubCheckDone) return;
let chosen = null;
if (githubCheckResult) chosen = { source: 'Github', version: githubCheckResult.version, url: GITHUB_REPO_URL };
else if (scriptcatCheckResult) chosen = { source: 'ScriptCat', version: scriptcatCheckResult.version, url: SCRIPTCAT_URL };
if (!chosen) { log('更新检测: 两个来源均未检测成功'); return; }
if (compareVersions(chosen.version, SCRIPT_VERSION) > 0) {
latestVersion = chosen.version;
updateLinkUrl = chosen.url;
hasUpdate = true;
log(`发现新版本(${chosen.source}):`, latestVersion);
showUpdateBadgeInPanel();
} else {
log(`当前已是最新版本(${chosen.source}):`, SCRIPT_VERSION);
}
}
function checkForUpdates() {
checkForUpdatesScriptCat();
checkForUpdatesGithub();
}
function checkForUpdatesScriptCat() {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://scriptcat.org/zh-CN/script-show-page/6728/version',
timeout: 8000,
onload: function(response) {
if (response.status === 200) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(response.responseText, 'text/html');
const labelXpath = "//*[normalize-space(text())='最新版本']";
const labelNode = doc.evaluate(labelXpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if (labelNode) {
const itemContainer = labelNode.closest('div, li, section');
if (itemContainer) {
const versionMatch = itemContainer.textContent.match(/\d+\.\d+\.\d+/);
if (versionMatch) {
scriptcatCheckResult = { version: versionMatch[0] };
}
}
}
} catch(e) { log('ScriptCat更新检测解析异常:', e); }
}
scriptcatCheckDone = true;
resolveUpdateAfterChecks();
},
onerror: function () { scriptcatCheckDone = true; resolveUpdateAfterChecks(); },
ontimeout: function () { scriptcatCheckDone = true; resolveUpdateAfterChecks(); }
});
}
function checkForUpdatesGithub() {
GM_xmlhttpRequest({
method: 'GET',
url: CHANGELOG_RAW_URL,
timeout: 8000,
onload: function (response) {
if (response.status === 200) {
const match = response.responseText.match(/##\s*\[([^\]]+)\]/);
if (match && match[1]) {
githubCheckResult = { version: match[1].trim() };
}
}
githubCheckDone = true;
resolveUpdateAfterChecks();
},
onerror: function () { log('更新检测: 网络请求失败'); githubCheckDone = true; resolveUpdateAfterChecks(); },
ontimeout: function () { log('更新检测: 请求超时'); githubCheckDone = true; resolveUpdateAfterChecks(); }
});
}
function showUpdateBadgeInPanel() {
const hint = document.getElementById('bse-ad-hint');
if (hint && !hint.querySelector('.bse-update-badge')) {
const badge = document.createElement('a');
badge.href = updateLinkUrl || SCRIPTCAT_URL;
badge.target = '_blank';
badge.className = 'bse-update-badge';
badge.textContent = '新版本 v' + latestVersion;
hint.appendChild(badge);
}
}
// ===================== 10. 进度条广告标记 =====================
function waitForElement(selector, callback) { const element = document.querySelector(selector);
if (element) callback(element); else setTimeout(() => waitForElement(selector, callback), 100); }
function createProgressMark(video, progressArea) {
const existingMark = document.getElementById('bse-ad-progress-mark');
if (existingMark) existingMark.remove();
if (!adSegments || adSegments.length === 0) return;
const mark = document.createElement('div'); mark.id = 'bse-ad-progress-mark';
mark.style.cssText = `position:absolute;height:100%;background:${AD_MARK_COLOR};z-index:1;pointer-events:none;border-radius:2px;`;
progressArea.appendChild(mark);
function updateMarkPosition() { const duration = video.duration; if (!duration || duration < adSegments[0].end) return;
mark.style.left = `${(adSegments[0].start / duration) * 100}%`; mark.style.width = `${(adSegments[0].end / duration) * 100 - (adSegments[0].start / duration) * 100}%`;
}
updateMarkPosition(); video.addEventListener('durationchange', updateMarkPosition); video.addEventListener('loadedmetadata', updateMarkPosition);
}
function initProgressMark() {
if (progressMarkInitialized) return; progressMarkInitialized = true;
waitForElement('.bpx-player-video-wrap video', (video) => {
waitForElement('.bpx-player-progress-area', (progressArea) => {
createProgressMark(video, progressArea);
if (progressMarkObserver) progressMarkObserver.disconnect();
progressMarkObserver = new MutationObserver(() => { const newVideo = document.querySelector('.bpx-player-video-wrap video'); if (newVideo && newVideo !== video) { progressMarkInitialized = false; initProgressMark(); progressMarkObserver.disconnect(); } });
progressMarkObserver.observe(document.body, { childList: true, subtree: true });
});
});
}
// ===================== 11. Markdown 渲染 =====================
function processInline(text) { return text.replace(/\*\*(.+?)\*\*/g, '$1').replace(/\*(.+?)\*/g, '$1').replace(/`([^`]+)`/g, '$1');
}
function markdownToHtml(md) {
if (!md) return '';
md = md.replace(/\r\n/g, '\n'); const lines = md.split('\n');
let out = [], stack = [], inCode = false, code = [];
for (const line of lines) {
if (line.trim().startsWith('```')) { if (inCode) { out.push('
' + escapeHtml(code.join('\n')) + '');
code = []; inCode = false; } else inCode = true; continue;
}
if (inCode) { code.push(line); continue;
}
const indent = line.match(/^[ \t]*/)[0].replace(/\t/g, ' ').length;
const t = line.trim(); if (!t) continue;
const ul = t.match(/^[-*][ \t]+(.*)$/), ol = t.match(/^\d+\.[ \t]+(.*)$/);
if (ul || ol) {
const type = ul ?
'ul' : 'ol', cnt = processInline(ul ? ul[1] : ol[1]);
if (!stack.length) { stack.push({ type, indent }); out.push(`<${type}>`);
} else {
const top = stack[stack.length - 1];
if (indent > top.indent) { stack.push({ type, indent }); out.push(`<${type}>`);
}
else if (indent < top.indent) { while (stack.length && stack[stack.length - 1].indent > indent) out.push(`${stack.pop().type}>`);
if (!stack.length || stack[stack.length - 1].indent < indent) { stack.push({ type, indent }); out.push(`<${type}>`);
} }
else if (top.type !== type) { out.push(`${stack.pop().type}>`);
stack.push({ type, indent }); out.push(`<${type}>`); }
} out.push(`${processInline(bq[1])}`); continue; } out.push(`
${processInline(t)}
`); } while (stack.length) out.push(`${stack.pop().type}>`); return out.join('\n'); } // ===================== 12. 广告解析与跳过 ===================== function extractAdSegments(rawSummary) { const text = rawSummary.replace(/\*/g, '').replace(/`/g, '').replace(/#/g, ' '); const timeRe = /广告时间[\s\S]{0,80}?\[(\d+:\d{2})\s*[-–—~至]\s*(\d+:\d{2})\]/g; const timeMatches = [...text.matchAll(timeRe)]; if (timeMatches.length > 0) { const last = timeMatches[timeMatches.length - 1]; const start = parseAdTime(last[1]), end = parseAdTime(last[2]); if (start !== null && end !== null && end > start) return { type: 'has_ad', segments: [{ start, end, startStr: last[1], endStr: last[2] }] }; } const noRe = /广告时间[\s\S]{0,80}?\[\s*无[^\]]*\]/g; if ([...text.matchAll(noRe)].length > 0) return { type: 'none', segments: [] }; return { type: 'error', segments: [] }; } function stripAdLine(summary) { const lines = summary.split('\n'); let cutIndex = lines.length; for (let i = 0; i < lines.length; i++) { if (lines[i].replace(/[#\s*`]/g, '').includes('广告时间')) { cutIndex = i; while (cutIndex > 0 && /^[#\s]/.test(lines[cutIndex - 1]) && lines[cutIndex - 1].trim() === '') cutIndex--; break; } } return lines.slice(0, cutIndex).join('\n').trim(); } function initAdSkipMonitor() { if (adSkipInterval) clearInterval(adSkipInterval); adSkipInterval = setInterval(() => { if (!bse_auto_skip_ad) return; if (!adSegments?.length) return; const video = document.querySelector('video'); if (!video || video.readyState === 0) return; const ct = video.currentTime; adSegments.forEach((ad, i) => { if (ct >= ad.start && ct < ad.end - 0.3) { video.currentTime = ad.end; const key = `${currentVideoKey}-${i}`; if (Date.now() - (hasJumpedAds[key] || 0) > 3000) { showToast('✓ 已自动跳过广告', 'success'); hasJumpedAds[key] = Date.now(); } } }); }, 1000); } // ===================== 13. B站 API ===================== async function fetchBilibiliSubtitles() { const url = window.location.href; const bvid = (url.match(/(BV[\w]+)/) || [])[1]; const page = parseInt((url.match(/[?&]p=(\d+)/) || [, 1])[1]); if (!bvid) return []; try { const vr = await fetch(`https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, { credentials: 'include' }); const vd = await vr.json(); if (vd.code !== 0 || !vd.data) return []; const aid = vd.data.aid, pages = vd.data.pages || []; let cid = vd.data.cid; if (pages.length >= page) cid = pages[page - 1].cid; currentAid = aid; const pr = await fetch(`https://api.bilibili.com/x/player/wbi/v2?aid=${aid}&cid=${cid}`, { credentials: 'include' }); const pd = await pr.json(); if (pd.code !== 0 || !pd.data?.subtitle?.subtitles) return []; return pd.data.subtitle.subtitles.filter(s => s.lan === 'zh-CN' || s.lan === 'zh' || s.lan.startsWith('ai-zh')).map((s, i) => ({ id: s.id || i, lan: s.lan, lan_doc: s.lan_doc, subtitle_url: s.subtitle_url, isAI: s.lan.startsWith('ai-'), body: null })); } catch (e) { return []; } } async function fetchSubtitleContent(url) { try { if (url.startsWith('//')) url = 'https:' + url; const r = await fetch(url); const d = await r.json(); return d.body || []; } catch (e) { return []; } } async function fetchHotComments() { let aid = currentAid; if (!aid) { try { aid = unsafeWindow.__INITIAL_STATE__?.aid; } catch {} } if (!aid) return []; try { const r = await fetch(`https://api.bilibili.com/x/v2/reply/main?type=1&oid=${aid}&mode=3&next=0&ps=30`, { credentials: 'include' }); const d = await r.json(); if (d.code !== 0 || !d.data?.replies) return []; return d.data.replies.map(r => ({ content: r.content.message, like: r.like })); } catch (e) { return []; } } // ===================== 14. AI API 调用 ===================== async function callAPIStream(messages, onChunk) { let isGemini = API_URL.includes('generativelanguage.googleapis.com'); let isClaude = API_URL.includes('anthropic.com'); let actualModel = bse_model.replace(' (免费)', ''); let fetchUrl = API_URL; let headers = { 'Content-Type': 'application/json' }; let bodyData = {}; if (isClaude) { headers['x-api-key'] = API_KEY; headers['anthropic-version'] = '2023-06-01'; headers['Accept'] = 'text/event-stream'; bodyData = { model: actualModel, max_tokens: 8192, stream: true, messages: messages }; } else if (isGemini) { fetchUrl = fetchUrl.replace('{model_name}', actualModel); if (fetchUrl.includes(':generateContent')) fetchUrl = fetchUrl.replace(':generateContent', ':streamGenerateContent'); fetchUrl += (fetchUrl.includes('?') ? '&' : '?') + `key=${API_KEY}&alt=sse`; bodyData = { contents: messages.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] })) }; } else { headers['Authorization'] = `Bearer ${API_KEY}`; headers['Accept'] = 'text/event-stream'; bodyData = { model: actualModel, messages: messages, stream: true }; } const startTime = Date.now(); const resp = await fetch(fetchUrl, { method: 'POST', headers, body: JSON.stringify(bodyData) }); if (!resp.ok) { if (resp.status === 429) throw new Error('HTTP 429 (请求频率过高,请稍后再试或更换限额更大的模型)'); throw new Error(`HTTP ${resp.status}`); } if (!resp.body) throw new Error('不支持流式响应'); const reader = resp.body.getReader(), dec = new TextDecoder('utf-8'); let buf = '', full = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buf += dec.decode(value, { stream: true }); const lines = buf.split(/\r?\n/); buf = lines.pop() || ''; for (const line of lines) { const t = line.trim(); if (!t || t.startsWith(':')) continue; if (isClaude && t.startsWith('event:')) continue; if (t.startsWith('data:')) { const ds = t.slice(5).trim(); if (!isClaude && ds === '[DONE]') return full; try { const d = JSON.parse(ds); let chunk = ''; if (isGemini) chunk = d.candidates?.[0]?.content?.parts?.[0]?.text || ''; else if (isClaude) { if (d.type === 'content_block_delta') chunk = d.delta?.text || ''; else if (d.type === 'message_stop') return full; } else chunk = d.choices?.[0]?.delta?.content || ''; if (chunk) { full += chunk; onChunk(full); } } catch {} } } } return full; } function callAPINoStream(messages) { return new Promise((resolve, reject) => { let isGemini = API_URL.includes('generativelanguage.googleapis.com'); let isClaude = API_URL.includes('anthropic.com'); let actualModel = bse_model.replace(' (免费)', ''); let fetchUrl = API_URL; let headers = { 'Content-Type': 'application/json' }; let bodyData = {}; if (isClaude) { headers['x-api-key'] = API_KEY; headers['anthropic-version'] = '2023-06-01'; bodyData = { model: actualModel, max_tokens: 8192, messages: messages }; } else if (isGemini) { fetchUrl = fetchUrl.replace('{model_name}', actualModel); fetchUrl += (fetchUrl.includes('?') ? '&' : '?') + `key=${API_KEY}`; bodyData = { contents: messages.map(m => ({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] })) }; } else { headers['Authorization'] = `Bearer ${API_KEY}`; bodyData = { model: actualModel, messages: messages }; } GM_xmlhttpRequest({ method: 'POST', url: fetchUrl, headers, data: JSON.stringify(bodyData), timeout: 60000, onload(r) { if (r.status === 429) return reject(new Error('HTTP 429 (请求频率过高,请稍后再试或更换限额更大的模型)')); try { const d = JSON.parse(r.responseText); if (d.error) return reject(new Error(d.error.message || JSON.stringify(d.error))); let result; if (isClaude) result = d.content?.[0]?.text; else if (isGemini) result = d.candidates?.[0]?.content?.parts?.[0]?.text; else result = d.choices?.[0]?.message?.content; if (!result) return reject(new Error('API返回异常')); resolve(result); } catch (e) { reject(new Error('解析失败')); } }, onerror() { reject(new Error('网络错误')); }, ontimeout() { reject(new Error('请求超时')); } }); }); } async function generateAISummaryStream(subtitleText, streamEl) { let contextInfo = ''; const videoTitle = getVideoTitle(); const videoDesc = getVideoDescription(); const videoTags = getVideoTags(); if (videoTitle) contextInfo += `视频标题:${videoTitle}\n`; if (videoDesc) contextInfo += `视频简介:${videoDesc}\n`; if (videoTags.length > 0) contextInfo += `视频标签:${videoTags.join(', ')}\n`; if (contextInfo) contextInfo += '\n'; const commentsText = (enableOpinionAnalysis && hotComments.length > 0) ? formatCommentsForAI() : ''; if (commentsText) contextInfo += `===== 热门评论(按热度排序)=====\n${commentsText}\n\n`; const messages = [{ role: 'user', content: `${getAISummaryPrompt()}\n\n${contextInfo}${subtitleText}` }]; let summary = await callAPIStream(messages, text => { safeSetInnerHTML(streamEl, markdownToHtml(text)); streamEl.scrollTop = streamEl.scrollHeight; }); let adCheck = extractAdSegments(summary); lastAdCheckResult = adCheck; if (adCheck.type === 'error') { safeSetInnerHTML(streamEl, markdownToHtml(summary) + '