// ==UserScript== // @name B站视频字幕 AI 总结助手 // @namespace https://scriptcat.org/ // @version 1.0.0 // @description B站视频字幕 AI 总结,支持字幕刷新、模块高亮、当前播放器字幕识别、浮球拖拽、面板调宽、多 API 配置、自定义 Prompt、关闭思考、超时设置、字幕分段、时间跳转、总结缓存。 // @author ScriptCat Agent // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/list/* // @match https://www.bilibili.com/bangumi/play/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @connect * // @license GPL-3.0 // ==/UserScript== (function () { 'use strict'; const STORE_KEYS = { apiBase: 'bili_ai_summary_api_base', apiKey: 'bili_ai_summary_api_key', apiModel: 'bili_ai_summary_api_model', apiProfiles: 'bili_ai_summary_api_profiles', activeApiProfileId: 'bili_ai_summary_active_api_profile_id', summaryPrompt: 'bili_ai_summary_custom_prompt', disableThinking: 'bili_ai_summary_disable_thinking', requestTimeout: 'bili_ai_summary_request_timeout', floatPos: 'bili_ai_summary_float_pos', panelWidth: 'bili_ai_summary_panel_width' }; const DEFAULT_API_BASE = 'https://api.deepseek.com/v1'; const DEFAULT_MODEL = 'deepseek-v4-flash'; const DEFAULT_FLOAT_POS = { right: 24, top: 180 }; const DEFAULT_PANEL_WIDTH = 420; const MIN_PANEL_WIDTH = 320; const MAX_PANEL_WIDTH = 720; const MAX_PANEL_HEIGHT = 560; const ABS_MIN_PANEL_HEIGHT = 120; const DEFAULT_REQUEST_TIMEOUT = 120000; const DEFAULT_SUMMARY_PROMPT = `请你根据 B 站视频字幕生成中文 Markdown 总结。 要求: 1. 必须按字幕中的时间顺序分段总结。 2. 每个主要小节标题必须以时间开头,例如:## [03:20] 主题内容。 3. 每个时间段下面写 2-4 条要点。 4. 保留关键观点、重要细节、结论和转折。 5. 不要编造字幕中没有的信息。 6. 不要输出无关寒暄。 7. 所有时间必须保留为 [mm:ss] 或 [hh:mm:ss] 格式。`; let floatBtn = null; let panel = null; let resizeHandle = null; let isPanelOpen = false; let latestSubtitleText = ''; let latestSummaryText = ''; let latestDisplayText = ''; let currentModule = 'subtitle'; // subtitle / summary let isGenerating = false; let livePanelWidth = null; function $(selector, root = document) { return root.querySelector(selector); } function clamp(num, min, max) { return Math.max(min, Math.min(max, num)); } function escapeHTML(str) { return String(str || '').replace(/[&<>"']/g, s => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[s])); } function toast(message, duration = 2200) { const el = document.createElement('div'); el.textContent = message; el.style.cssText = ` position: fixed; z-index: 2147483647; left: 50%; top: 24px; transform: translateX(-50%); padding: 9px 14px; border-radius: 999px; background: rgba(0,0,0,.78); color: #fff; font-size: 13px; pointer-events: none; box-shadow: 0 6px 20px rgba(0,0,0,.2); `; document.body.appendChild(el); setTimeout(() => { el.style.opacity = '0'; el.style.transition = 'opacity .25s'; setTimeout(() => el.remove(), 300); }, duration); } /*********************** * API / 配置 ***********************/ function createProfileId() { return `profile_${Date.now()}_${Math.random().toString(16).slice(2)}`; } function getApiProfiles() { let profiles = GM_getValue(STORE_KEYS.apiProfiles, null); if (!Array.isArray(profiles) || !profiles.length) { const oldBase = GM_getValue(STORE_KEYS.apiBase, DEFAULT_API_BASE); const oldKey = GM_getValue(STORE_KEYS.apiKey, ''); const oldModel = GM_getValue(STORE_KEYS.apiModel, DEFAULT_MODEL); profiles = [{ id: createProfileId(), name: '默认配置', apiBase: oldBase || DEFAULT_API_BASE, apiKey: oldKey || '', apiModel: oldModel || DEFAULT_MODEL }]; GM_setValue(STORE_KEYS.apiProfiles, profiles); GM_setValue(STORE_KEYS.activeApiProfileId, profiles[0].id); } return profiles; } function saveApiProfiles(profiles) { GM_setValue(STORE_KEYS.apiProfiles, profiles); } function getActiveApiProfileId() { const profiles = getApiProfiles(); const savedId = GM_getValue(STORE_KEYS.activeApiProfileId, ''); if (profiles.some(p => p.id === savedId)) return savedId; const firstId = profiles[0]?.id || ''; if (firstId) GM_setValue(STORE_KEYS.activeApiProfileId, firstId); return firstId; } function setActiveApiProfileId(id) { GM_setValue(STORE_KEYS.activeApiProfileId, id); } function getActiveApiProfile() { const profiles = getApiProfiles(); const activeId = getActiveApiProfileId(); return profiles.find(p => p.id === activeId) || profiles[0]; } function getApiBase() { const profile = getActiveApiProfile(); return String(profile?.apiBase || DEFAULT_API_BASE).replace(/\/+$/, ''); } function getApiKey() { const profile = getActiveApiProfile(); return profile?.apiKey || ''; } function getApiModel() { const profile = getActiveApiProfile(); return profile?.apiModel || DEFAULT_MODEL; } function getSummaryPrompt() { return GM_getValue(STORE_KEYS.summaryPrompt, DEFAULT_SUMMARY_PROMPT) || DEFAULT_SUMMARY_PROMPT; } function setSummaryPrompt(prompt) { GM_setValue(STORE_KEYS.summaryPrompt, prompt || DEFAULT_SUMMARY_PROMPT); } function getDisableThinking() { return GM_getValue(STORE_KEYS.disableThinking, true); } function setDisableThinking(value) { GM_setValue(STORE_KEYS.disableThinking, !!value); } function getRequestTimeout() { const saved = Number(GM_getValue(STORE_KEYS.requestTimeout, DEFAULT_REQUEST_TIMEOUT)); return clamp(saved || DEFAULT_REQUEST_TIMEOUT, 10000, 600000); } function setRequestTimeout(ms) { const value = clamp(Number(ms) || DEFAULT_REQUEST_TIMEOUT, 10000, 600000); GM_setValue(STORE_KEYS.requestTimeout, value); return value; } function getPanelWidth() { const saved = Number(GM_getValue(STORE_KEYS.panelWidth, DEFAULT_PANEL_WIDTH)); return clamp(saved || DEFAULT_PANEL_WIDTH, MIN_PANEL_WIDTH, MAX_PANEL_WIDTH); } function setPanelWidth(width) { const w = clamp(width, MIN_PANEL_WIDTH, MAX_PANEL_WIDTH); GM_setValue(STORE_KEYS.panelWidth, w); return w; } function getFloatPos() { const saved = GM_getValue(STORE_KEYS.floatPos, null); if (saved && typeof saved === 'object') { return { right: typeof saved.right === 'number' ? saved.right : DEFAULT_FLOAT_POS.right, top: typeof saved.top === 'number' ? saved.top : DEFAULT_FLOAT_POS.top }; } return { ...DEFAULT_FLOAT_POS }; } function saveFloatPos(pos) { GM_setValue(STORE_KEYS.floatPos, pos); } function isVideoPage() { return /bilibili\.com\/video\/|bilibili\.com\/bangumi\/play\/|bilibili\.com\/list\//.test(location.href); } function formatTime(seconds) { seconds = Number(seconds) || 0; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } function timeToSeconds(time) { const parts = String(time || '').split(':').map(Number); if (parts.length === 2) return parts[0] * 60 + parts[1]; if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2]; return 0; } function markdownToHTML(text) { let html = escapeHTML(text || ''); html = html.replace(/^### (.*)$/gm, '
$1');
html = html.replace(/\[([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\]/g, (_, time) => {
return `[${time}]`;
});
html = html.replace(/^\s*[-*]\s+(.*)$/gm, ''); html = `
${html}
`; html = html.replace(/\s*( \s*( 检测到视频切换,已清空旧字幕和旧总结。 面板会默认显示当前视频的「字幕分段」。 点击「AI总结」只会首次生成;已有总结时只显示缓存。 只有点击「重新生成」才会重新请求 AI。)/g, '$1');
html = html.replace(/(<\/ul>)\s*<\/p>/g, '$1');
return html;
}
/***********************
* 样式
***********************/
function injectStyle() {
if ($('#bili-ai-summary-style')) return;
const style = document.createElement('style');
style.id = 'bili-ai-summary-style';
style.textContent = `
#bili-ai-summary-float {
position: fixed;
z-index: 2147483646;
width: 48px;
height: 48px;
border-radius: 999px;
border: none;
outline: none;
cursor: grab;
background: linear-gradient(135deg, #00a1d6, #7c4dff);
color: #fff;
font-size: 13px;
font-weight: 700;
box-shadow: 0 8px 24px rgba(0,0,0,.22);
user-select: none;
display: flex;
align-items: center;
justify-content: center;
}
#bili-ai-summary-float:active {
cursor: grabbing;
}
#bili-ai-summary-panel {
position: fixed;
z-index: 2147483645;
width: 420px;
max-height: 560px;
background: rgba(255,255,255,.98);
color: #18191c;
border: 1px solid rgba(0,0,0,.08);
border-radius: 14px;
box-shadow: 0 16px 44px rgba(0,0,0,.22);
overflow: hidden;
display: none;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
}
html[data-theme="dark"] #bili-ai-summary-panel,
body.dark #bili-ai-summary-panel {
background: rgba(32,33,36,.98);
color: #e3e5e7;
border-color: rgba(255,255,255,.1);
}
#bili-ai-summary-panel.bili-ai-open {
display: flex;
flex-direction: column;
}
.bili-ai-resize-handle {
position: absolute;
left: -22px;
top: 0;
width: 22px;
height: 100%;
cursor: ew-resize;
z-index: 3;
background: linear-gradient(to right, rgba(0,161,214,.05), rgba(0,161,214,.22));
}
.bili-ai-resize-handle::after {
content: "";
position: absolute;
right: 5px;
top: 16px;
bottom: 16px;
width: 3px;
border-radius: 99px;
background: rgba(0,161,214,.85);
}
.bili-ai-header {
flex: 0 0 auto;
padding: 12px 14px 10px;
border-bottom: 1px solid rgba(0,0,0,.08);
display: flex;
align-items: center;
gap: 8px;
}
html[data-theme="dark"] .bili-ai-header,
body.dark .bili-ai-header {
border-bottom-color: rgba(255,255,255,.1);
}
.bili-ai-title {
font-size: 15px;
font-weight: 700;
}
.bili-ai-subtitle {
font-size: 12px;
color: #9499a0;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.bili-ai-mini-btn {
border: 1px solid rgba(0,0,0,.12);
background: #fff;
color: #18191c;
border-radius: 8px;
padding: 5px 8px;
font-size: 12px;
cursor: pointer;
white-space: nowrap;
}
.bili-ai-mini-btn:hover {
background: #f2f3f5;
}
#bili-ai-refresh-subtitle-btn {
padding: 5px 7px;
}
html[data-theme="dark"] .bili-ai-mini-btn,
body.dark .bili-ai-mini-btn {
background: #2f3033;
color: #e3e5e7;
border-color: rgba(255,255,255,.12);
}
.bili-ai-content {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
padding: 14px;
font-size: 14px;
line-height: 1.75;
}
.bili-ai-content h1,
.bili-ai-content h2,
.bili-ai-content h3 {
margin: 12px 0 8px;
line-height: 1.4;
}
.bili-ai-content h1 { font-size: 18px; }
.bili-ai-content h2 { font-size: 16px; }
.bili-ai-content h3 { font-size: 15px; }
.bili-ai-content p { margin: 8px 0; }
.bili-ai-content ul { padding-left: 20px; margin: 8px 0; }
.bili-ai-content li { margin: 4px 0; }
.bili-ai-content code {
background: rgba(0,0,0,.06);
padding: 1px 4px;
border-radius: 4px;
}
.bili-ai-time {
display: inline-block;
padding: 1px 7px;
margin-right: 4px;
border-radius: 999px;
background: rgba(0,161,214,.12);
color: #00a1d6;
font-size: 12px;
font-weight: 700;
cursor: pointer;
user-select: none;
white-space: nowrap;
}
.bili-ai-time:hover {
background: rgba(0,161,214,.22);
color: #008ac0;
}
.bili-ai-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
color: #61666d;
padding: 26px 8px;
text-align: center;
}
.bili-ai-spinner {
width: 26px;
height: 26px;
border: 3px solid rgba(0,161,214,.18);
border-top-color: #00a1d6;
border-radius: 50%;
animation: biliAiSpin .8s linear infinite;
}
@keyframes biliAiSpin {
to { transform: rotate(360deg); }
}
.bili-ai-footer {
flex: 0 0 auto;
position: sticky;
bottom: 0;
padding: 10px;
background: rgba(255,255,255,.98);
border-top: 1px solid rgba(0,0,0,.08);
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
html[data-theme="dark"] .bili-ai-footer,
body.dark .bili-ai-footer {
background: rgba(32,33,36,.98);
border-top-color: rgba(255,255,255,.1);
}
.bili-ai-footer button {
border: none;
border-radius: 9px;
padding: 8px 4px;
font-size: 12px;
cursor: pointer;
background: #f1f2f3;
color: #18191c;
}
.bili-ai-footer button:hover {
background: #e3e5e7;
}
.bili-ai-footer button.primary {
background: #00a1d6;
color: #fff;
}
.bili-ai-footer button.primary:hover {
background: #0092c2;
}
.bili-ai-footer button.bili-ai-tab-btn {
font-weight: 700;
}
.bili-ai-footer button.bili-ai-tab-btn.primary {
background: #00a1d6;
color: #fff;
box-shadow: 0 0 0 2px rgba(0,161,214,.16);
}
html[data-theme="dark"] .bili-ai-footer button,
body.dark .bili-ai-footer button {
background: #3a3b3f;
color: #e3e5e7;
}
html[data-theme="dark"] .bili-ai-footer button.primary,
body.dark .bili-ai-footer button.primary {
background: #00a1d6;
color: #fff;
}
.bili-ai-config {
display: grid;
gap: 10px;
}
.bili-ai-field label {
display: block;
font-size: 12px;
color: #9499a0;
margin-bottom: 4px;
}
.bili-ai-field input,
.bili-ai-field select,
.bili-ai-field textarea {
width: 100%;
box-sizing: border-box;
border: 1px solid rgba(0,0,0,.12);
border-radius: 8px;
padding: 8px 9px;
outline: none;
font-size: 13px;
background: #fff;
color: #18191c;
}
.bili-ai-field textarea {
min-height: 150px;
resize: vertical;
line-height: 1.5;
font-family: inherit;
}
html[data-theme="dark"] .bili-ai-field input,
body.dark .bili-ai-field input,
html[data-theme="dark"] .bili-ai-field select,
body.dark .bili-ai-field select,
html[data-theme="dark"] .bili-ai-field textarea,
body.dark .bili-ai-field textarea {
background: #2f3033;
color: #e3e5e7;
border-color: rgba(255,255,255,.12);
}
.bili-ai-config-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.bili-ai-config-actions button {
border: none;
border-radius: 8px;
padding: 8px 10px;
font-size: 13px;
cursor: pointer;
background: #f1f2f3;
}
.bili-ai-config-actions button.primary {
background: #00a1d6;
color: #fff;
}
.bili-ai-error {
color: #f85a54;
white-space: pre-wrap;
}
.bili-ai-muted {
color: #9499a0;
font-size: 13px;
}
`;
document.documentElement.appendChild(style);
}
/***********************
* UI
***********************/
function createFloatButton() {
if (floatBtn) return;
floatBtn = document.createElement('button');
floatBtn.id = 'bili-ai-summary-float';
floatBtn.textContent = 'AI';
floatBtn.title = 'B站 AI 字幕总结';
const pos = getFloatPos();
floatBtn.style.right = `${pos.right}px`;
floatBtn.style.top = `${pos.top}px`;
document.body.appendChild(floatBtn);
let dragging = false;
let moved = false;
let startX = 0;
let startY = 0;
let startRight = 0;
let startTop = 0;
floatBtn.addEventListener('mousedown', e => {
if (e.button !== 0) return;
dragging = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
const rect = floatBtn.getBoundingClientRect();
startRight = window.innerWidth - rect.right;
startTop = rect.top;
document.body.style.userSelect = 'none';
e.preventDefault();
});
document.addEventListener('mousemove', e => {
if (!dragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true;
const rect = floatBtn.getBoundingClientRect();
const width = rect.width || 48;
const height = rect.height || 48;
let nextRight = startRight - dx;
let nextTop = startTop + dy;
nextRight = clamp(nextRight, 8, window.innerWidth - width - 8);
nextTop = clamp(nextTop, 8, window.innerHeight - height - 8);
floatBtn.style.right = `${nextRight}px`;
floatBtn.style.top = `${nextTop}px`;
saveFloatPos({
right: nextRight,
top: nextTop
});
if (isPanelOpen) updatePopoverPlacement();
});
document.addEventListener('mouseup', () => {
if (!dragging) return;
dragging = false;
document.body.style.userSelect = '';
setTimeout(() => {
moved = false;
}, 0);
});
floatBtn.addEventListener('click', e => {
if (moved) return;
if (isPanelOpen) {
closePanel();
} else {
openPanel();
}
e.preventDefault();
});
}
function createPanel() {
if (panel) return;
panel = document.createElement('div');
panel.id = 'bili-ai-summary-panel';
resizeHandle = document.createElement('div');
resizeHandle.className = 'bili-ai-resize-handle';
const header = document.createElement('div');
header.className = 'bili-ai-header';
header.innerHTML = `