广告概率: ${result.finalProbability}%
${result.start && result.end ? `广告时间: ${escapeHtml(result.start)} - ${escapeHtml(result.end)}
` : ''}分析说明: ${escapeHtml(result.note) || '无'}
概率调整: ${escapeHtml(result.adjustmentNote) || '无'}
// ==UserScript== // @name Bilibili Video Ad Skipper // @namespace http://tampermonkey.net/ // @homepageURL https://github.com/StarsWhere/Bilibili-Video-Ad-Skipper // @version 2.0 // @description 本工具利用人工智能(AI)分析哔哩哔哩(Bilibili)的弹幕和评论,能够基于概率识别视频中的广告片段,并实现自动跳过。它结合了概率机制与评论分析,从而提高了广告检测的精准度。 // @author StarsWhere // @license MIT // @match https://www.bilibili.com/video/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect api.bilibili.com // @connect comment.bilibili.com // @connect api.openai.com // @connect api.deepseek.com // @connect generativelanguage.googleapis.com // @connect api.anthropic.com // @connect * // @icon https://raw.githubusercontent.com/StarsWhere/Bilibili-Video-Ad-Skipper/main/png/icon.png // @downloadURL none // ==/UserScript== (function () { 'use strict'; // --- CONSTANTS (常量定义) --- const settingsIconBase64 = 'https://raw.githubusercontent.com/StarsWhere/Bilibili-Video-Ad-Skipper/main/png/icon.png' const API_PROVIDERS = { openai: { defaultUrl: 'https://api.openai.com/v1', needsUrl: false, models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'] }, deepseek: { defaultUrl: 'https://api.deepseek.com/v1', needsUrl: false, models: ['deepseek-chat', 'deepseek-coder'] }, gemini: { defaultUrl: 'https://generativelanguage.googleapis.com/v1beta', needsUrl: false, models: ['gemini-pro', 'gemini-pro-vision'] }, anthropic: { defaultUrl: 'https://api.anthropic.com/v1', needsUrl: false, models: ['claude-3-5-sonnet-20240620', 'claude-3-haiku-20240307'] }, custom: { defaultUrl: '', needsUrl: true, models: [] // 用户可以手动输入 } }; const DEFAULT_SETTINGS = { theme: 'light', firstTimeUse: true, floatingPosition: { x: 50, y: 50 }, apiProvider: 'openai', baseUrl: '', apiKey: '', model: '', enableR1Params: false, useLegacyOpenAIFormat: false, defaultSkip: true, probabilityThreshold: 70, durationPenalty: 5, minAdDuration: 30, maxAdDuration: 300, maxDanmakuCount: 500, minDanmakuForFullAnalysis: 10, enableWhitelist: true, whitelistRegex: false, whitelist: [ '分', '秒', ':', '.', '空降', '指路', '感谢', '君', '跳过', '广告', '快进', '坐标', '时间', '分钟', '开始', '结束', '进度', '节点', '推广', '赞助', '商务', '合作', '链接', '购买', '优惠', '折扣' ], enableBlacklist: true, blacklistRegex: false, blacklist: ['正片', '省流', '总结', '回顾', '分享'], // 更改: 最新的默认提示词 agentPrompt: `### Agent Prompt (提示词) **角色 (Role):** 你是一个智能agent,专门分析Bilibili视频的弹幕以检测其中包含的商业广告(硬广)时间段。 **任务 (Task):** 你收到的内容包含两部分: 1. 经过整理后的弹幕文本,格式为 \`MM:SS\` 或 \`HH:MM:SS\` 2. 视频的第一条评论内容及其状态(是否为置顶评论) 你的核心任务是根据这些信息,判断视频是否含有广告,确定广告的时间段,并给出广告概率评估。 **工作流程与逻辑 (Workflow & Logic):** **识别广告标记弹幕**: - 寻找"时间跳转"或"广告提示"类弹幕。 - 常见模式:\`X分Y秒\`, \`X:Y\`, \`X.Y\`, \`感谢XX君\`, \`空降坐标\`, \`指路牌\`, \`xx秒后\`等。 - 注意:忽略含有"正片"、"省流"的弹幕,这些通常指向正常内容, 弹幕不会存在商业推广内容,你只是需要评估是否有类似\`路标\`的弹幕存在即可 3. **广告概率评估标准**: - **90-100%**: 多条弹幕指向同一时间点。 - **70-89%**: 复数弹幕指向同一时间点,模式明确,即使评论无广告信息。 - **50-69%**: 存在弹幕指向时间点,但模式相对明确。 - **30-49%**: 弹幕证据较弱,但存在一些可疑指向。 - **10-29%**: 非常微弱的证据。 - **0-9%**: 基本无广告证据。 4. **时间确定**: - **广告结束时间**: 弹幕指向的目标时间点。 - **广告开始时间**: 指向该时间的最早弹幕的发送时间戳。 5. **处理无广告情况**: - 如果弹幕中的数字都是描述性的,且没有明确的时间跳转指示。 **输出格式 (Output Format):** 统一返回以下JSON格式: { "probability": 数字(0-100, 表示广告存在的概率), "start": "开始时间(格式: MM:SS 或 HH:MM:SS, 如果没有则为null)", "end": "结束时间(格式: MM:SS 或 HH:MM:SS, 如果没有则为null)", "note": "分析说明" } **注意事项**: - probability: 0-100的整数,表示广告概率百分比。 - start/end: 当probability >= 30时必须提供,否则可为null。 - note: 必须详细说明判断依据。 - 输出必须是纯JSON,不包含任何其他文本或markdown标记。 **最终指令 (Final Instruction):** 你的输出**必须且只能是**一个纯粹的、格式正确的JSON对象。**绝对禁止**包含任何JSON之外的文本。` }; // --- STYLES (样式定义) --- const injectStyles = () => { const styleId = 'bili-ai-skipper-styles'; if (document.getElementById(styleId)) return; const style = document.createElement('style'); style.id = styleId; style.textContent = ` :root { --primary-color: #00AEEC; --primary-hover: #0096D6; --danger-color: #FF6B6B; --danger-hover: #FF5252; --success-color: #4CAF50; --warning-color: #FF9800; --text-primary: #333; --text-secondary: #666; --bg-primary: #fff; --bg-secondary: #f5f5f5; --border-color: #ddd; --shadow: 0 2px 8px rgba(0,0,0,0.1); --shadow-lg: 0 4px 16px rgba(0,0,0,0.15); } .dark-theme, .bili-ai-skipper-settings-backdrop.dark-theme, .bili-ai-skipper-first-time-modal.dark-theme { --text-primary: #e0e0e0; --text-secondary: #b0b0b0; --bg-primary: #2a2a2a; --bg-secondary: #1e1e1e; --border-color: #404040; --shadow: 0 2px 8px rgba(0,0,0,0.3); --shadow-lg: 0 4px 16px rgba(0,0,0,0.4); } /* 圆形悬浮按钮 */ .bili-ai-skipper-floating-btn { position: fixed; width: 50px; height: 50px; border-radius: 50%; background: var(--bg-primary); border: 2px solid var(--primary-color); box-shadow: var(--shadow-lg); cursor: pointer; z-index: 10000; display: flex; align-items: center; justify-content: center; opacity: 0.7; transition: all 0.3s ease; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } .bili-ai-skipper-floating-btn:hover { opacity: 1; transform: scale(1.1); } .bili-ai-skipper-floating-btn img { width: 24px; height: 24px; } /* Toast 消息 */ .bili-ai-skipper-toast { position: fixed; top: 20px; right: 20px; background: var(--bg-primary); color: var(--text-primary); padding: 12px 20px; border-radius: 8px; box-shadow: var(--shadow-lg); z-index: 10001; font-size: 14px; border-left: 4px solid var(--primary-color); max-width: 300px; word-wrap: break-word; animation: slideInRight 0.3s ease; } @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } /* 设置界面 */ .bili-ai-skipper-settings-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10002; display: flex; align-items: center; justify-content: center; animation: fadeIn 0.2s ease; } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } .bili-ai-skipper-settings-modal { background: var(--bg-primary); color: var(--text-primary); border-radius: 12px; width: 90%; max-width: 900px; height: 800px; display: flex; flex-direction: column; box-shadow: var(--shadow-lg); animation: slideInDown 0.3s ease; overflow: hidden; } .bili-ai-skipper-settings-modal.dark-theme { background: var(--bg-primary); color: var(--text-primary); } @keyframes slideInDown { from { transform: translateY(-50px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .bili-ai-skipper-settings-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid var(--border-color); background: var(--bg-secondary); flex-shrink: 0; } .bili-ai-skipper-settings-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--text-primary); } .bili-ai-skipper-settings-close { background: none; border: none; font-size: 24px; cursor: pointer; color: var(--text-secondary); padding: 0; width: 30px; height: 30px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; } .bili-ai-skipper-settings-close:hover { background: var(--danger-color); color: white; } .bili-ai-skipper-settings-body { padding: 0; flex-grow: 1; overflow-y: auto; } .bili-ai-skipper-settings-tabs { display: flex; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); flex-shrink: 0; } .bili-ai-skipper-settings-tab { flex: 1; padding: 15px 20px; border: none; background: none; color: var(--text-secondary); cursor: pointer; transition: all 0.2s ease; font-size: 14px; font-weight: 500; } .bili-ai-skipper-settings-tab.active { color: var(--primary-color); background: var(--bg-primary); border-bottom: 2px solid var(--primary-color); } .bili-ai-skipper-settings-tab:hover:not(.active) { color: var(--text-primary); background: var(--bg-primary); } .bili-ai-skipper-tab-content { display: none; padding: 20px; } .bili-ai-skipper-tab-content.active { display: block; } .bili-ai-skipper-settings-section { margin-bottom: 25px; } .bili-ai-skipper-settings-section:last-child { margin-bottom: 0; } .bili-ai-skipper-settings-section h3 { margin: 0 0 15px 0; font-size: 16px; font-weight: 600; color: var(--text-primary); border-bottom: 1px solid var(--border-color); padding-bottom: 8px; } .bili-ai-skipper-settings-group { margin-bottom: 15px; } .bili-ai-skipper-settings-group-inline { display: flex; gap: 15px; margin-bottom: 15px; } .bili-ai-skipper-settings-group-inline > div { flex: 1; } .bili-ai-skipper-settings-label { display: block; margin-bottom: 5px; font-weight: 500; color: var(--text-primary); font-size: 14px; } .bili-ai-skipper-settings-input, .bili-ai-skipper-settings-select, .bili-ai-skipper-list-input input[type="text"] { width: 100%; padding: 10px 12px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); font-size: 14px; transition: all 0.2s ease; box-sizing: border-box; } .bili-ai-skipper-settings-input:focus, .bili-ai-skipper-settings-select:focus, .bili-ai-skipper-list-input input[type="text"]:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(0, 174, 236, 0.2); } .bili-ai-skipper-settings-textarea { width: 100%; min-height: 440px; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); color: var(--text-primary); font-size: 14px; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; line-height: 1.5; resize: vertical; transition: all 0.2s ease; box-sizing: border-box; } .bili-ai-skipper-settings-textarea:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(0, 174, 236, 0.2); } .bili-ai-skipper-settings-checkbox { display: flex; align-items: center; margin-bottom: 10px; } .bili-ai-skipper-settings-checkbox input[type="checkbox"] { margin-right: 8px; transform: scale(1.1); } .bili-ai-skipper-settings-checkbox label { cursor: pointer; font-size: 14px; color: var(--text-primary); } .bili-ai-skipper-settings-footer { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-top: 1px solid var(--border-color); background: var(--bg-secondary); flex-shrink: 0; } .bili-ai-skipper-theme-toggle { display: flex; gap: 10px; } .bili-ai-skipper-theme-btn { width: 40px; height: 40px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--bg-primary); cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; font-size: 18px; } .bili-ai-skipper-theme-btn:hover { border-color: var(--primary-color); transform: scale(1.05); } .bili-ai-skipper-settings-actions { display: flex; gap: 10px; } .bili-ai-skipper-settings-btn-primary, .bili-ai-skipper-settings-btn-secondary, .bili-ai-skipper-list-add-btn { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s ease; } .bili-ai-skipper-settings-btn-primary, .bili-ai-skipper-list-add-btn { background: var(--primary-color); color: white; } .bili-ai-skipper-settings-btn-primary:hover, .bili-ai-skipper-list-add-btn:hover { background: var(--primary-hover); transform: translateY(-1px); } .bili-ai-skipper-settings-btn-secondary { background: var(--bg-primary); color: var(--text-primary); border: 1px solid var(--border-color); } .bili-ai-skipper-settings-btn-secondary:hover { background: var(--bg-secondary); } /* 列表管理 (白名单/黑名单) */ .bili-ai-skipper-list-container { margin-top: 10px; } .bili-ai-skipper-list-input { display: flex; margin-bottom: 10px; } .bili-ai-skipper-list-input input[type="text"] { flex-grow: 1; margin-right: 10px; } .bili-ai-skipper-list-add-btn { padding: 0 15px; height: auto; line-height: normal; } .bili-ai-skipper-list-items { max-height: 150px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 4px; padding: 5px; background: var(--bg-primary); } .bili-ai-skipper-list-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 5px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); } .bili-ai-skipper-list-item:last-child { border-bottom: none; } .bili-ai-skipper-list-item span { flex-grow: 1; word-break: break-all; margin-right: 10px; } .bili-ai-skipper-list-remove-btn { background: none; border: none; color: var(--danger-color); cursor: pointer; font-size: 18px; padding: 0 5px; flex-shrink: 0; } .bili-ai-skipper-list-remove-btn:hover { color: var(--danger-hover); } /* 结果弹窗 */ .bili-ai-skipper-result-popup { position: fixed; bottom: 20px; right: 20px; width: 350px; background: var(--bg-primary); color: var(--text-primary); border-radius: 12px; box-shadow: var(--shadow-lg); z-index: 10001; overflow: hidden; animation: slideInUp 0.3s ease; border: 1px solid var(--border-color); } .bili-ai-skipper-result-popup.dark-theme { background: var(--bg-primary); color: var(--text-primary); border-color: var(--border-color); } @keyframes slideInUp { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } .bili-ai-skipper-result-popup .header { background: var(--primary-color); color: white; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; cursor: move; } .bili-ai-skipper-result-popup .title { font-weight: 600; font-size: 14px; } .bili-ai-skipper-result-popup .close-btn { background: none; border: none; color: white; font-size: 20px; cursor: pointer; padding: 0; width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; line-height: 1; } .bili-ai-skipper-result-popup .close-btn:hover { background: rgba(255, 255, 255, 0.2); } .bili-ai-skipper-result-popup .content { padding: 15px; font-size: 13px; line-height: 1.6; color: var(--text-primary); } .bili-ai-skipper-result-popup .content p { margin: 0 0 10px 0; } .bili-ai-skipper-result-popup .content p strong { color: var(--text-primary); } .bili-ai-skipper-result-popup .footer { padding: 10px 15px; border-top: 1px solid var(--border-color); background: var(--bg-secondary); } .bili-ai-skipper-result-popup .footer label { display: flex; align-items: center; font-size: 13px; color: var(--text-secondary); cursor: pointer; } .bili-ai-skipper-result-popup .footer input[type="checkbox"] { margin-right: 8px; transform: scale(1.1); } .bili-ai-skipper-result-popup .raw-response { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 4px; padding: 10px; font-family: monospace; font-size: 11px; white-space: pre-wrap; word-break: break-all; max-height: 150px; overflow-y: auto; margin-top: 8px; color: var(--text-secondary); } .bili-ai-skipper-result-popup details { margin-top: 10px; } .bili-ai-skipper-result-popup summary { cursor: pointer; font-size: 12px; color: var(--text-secondary); margin-bottom: 5px; font-weight: 500; } .bili-ai-skipper-result-popup summary:hover { color: var(--text-primary); } .bili-ai-skipper-result-popup.error .header { background-color: var(--danger-color); } /* 模型下拉框 */ .bili-ai-skipper-model-container { position: relative; } .bili-ai-skipper-model-dropdown { position: absolute; top: 100%; left: 0; right: 0; background: var(--bg-primary); border: 1px solid var(--border-color); border-top: none; border-radius: 0 0 6px 6px; max-height: 200px; overflow-y: auto; z-index: 1000; box-shadow: var(--shadow); } .bili-ai-skipper-model-option { padding: 10px 12px; cursor: pointer; transition: background 0.2s ease; font-size: 14px; color: var(--text-primary); } .bili-ai-skipper-model-option:hover { background: var(--bg-secondary); } /* 首次使用模态框 */ .bili-ai-skipper-first-time-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); z-index: 10003; display: flex; align-items: center; justify-content: center; } .bili-ai-skipper-first-time-content { background: var(--bg-primary); color: var(--text-primary); border-radius: 12px; padding: 30px; max-width: 500px; width: 90%; text-align: center; box-shadow: var(--shadow-lg); } .bili-ai-skipper-first-time-modal.dark-theme .bili-ai-skipper-first-time-content { background: var(--bg-primary); color: var(--text-primary); } .bili-ai-skipper-first-time-title { font-size: 24px; font-weight: 600; margin-bottom: 20px; color: var(--primary-color); } .bili-ai-skipper-first-time-description { font-size: 16px; line-height: 1.6; margin-bottom: 25px; color: var(--text-primary); text-align: left; } .bili-ai-skipper-first-time-description strong { color: var(--text-primary); } .bili-ai-skipper-first-time-input { width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 14px; margin-bottom: 20px; background: var(--bg-primary); color: var(--text-primary); box-sizing: border-box; } .bili-ai-skipper-first-time-actions { text-align: center; } .bili-ai-skipper-first-time-btn { background: var(--primary-color); color: white; border: none; padding: 12px 30px; border-radius: 6px; font-size: 16px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .bili-ai-skipper-first-time-btn:disabled { background: var(--text-secondary); cursor: not-allowed; } .bili-ai-skipper-first-time-btn:not(:disabled):hover { background: var(--primary-hover); transform: translateY(-1px); } /* 自定义OpenAI选项组 */ #custom-openai-options-group .bili-ai-skipper-settings-checkbox { margin-left: 10px; margin-top: 10px; } #custom-openai-options-group .bili-ai-skipper-settings-checkbox:first-child { margin-top: 15px; } `; document.head.appendChild(style); }; // --- UTILITY FUNCTIONS (工具函数) --- const showToast = (message, duration = 3000) => { const settings = GM_getValue('ai_settings', DEFAULT_SETTINGS); const toast = document.createElement('div'); toast.className = 'bili-ai-skipper-toast'; if (settings.theme === 'dark') { toast.classList.add('dark-theme'); } toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), duration); }; const makeDraggable = (element, handle) => { let isDragging = false; let currentX, currentY, initialX, initialY, xOffset = 0, yOffset = 0; const dragStart = (e) => { if (e.type === "touchstart") { initialX = e.touches[0].clientX - xOffset; initialY = e.touches[0].clientY - yOffset; } else { initialX = e.clientX - xOffset; initialY = e.clientY - yOffset; } if (e.target === handle) { isDragging = true; } }; const dragEnd = () => { initialX = currentX; initialY = currentY; isDragging = false; }; const drag = (e) => { if (isDragging) { e.preventDefault(); if (e.type === "touchmove") { currentX = e.touches[0].clientX - initialX; currentY = e.touches[0].clientY - initialY; } else { currentX = e.clientX - initialX; currentY = e.clientY - initialY; } xOffset = currentX; yOffset = currentY; element.style.transform = `translate(${currentX}px, ${currentY}px)`; } }; handle.addEventListener("mousedown", dragStart); document.addEventListener("mousemove", drag); document.addEventListener("mouseup", dragEnd); }; const timeStringToSeconds = (timeStr) => { if (!timeStr) return 0; const parts = String(timeStr).split(':').map(Number); if (parts.length === 2) { return parts[0] * 60 + parts[1]; } else if (parts.length === 3) { return parts[0] * 3600 + parts[1] * 60 + parts[2]; } return parseInt(timeStr) || 0; }; const secondsToTimeString = (seconds) => { seconds = Math.floor(seconds); const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; const pad = (num) => String(num).padStart(2, '0'); if (h > 0) { return `${pad(h)}:${pad(m)}:${pad(s)}`; } return `${pad(m)}:${pad(s)}`; }; // --- API FUNCTIONS (API 函数) --- const getVideoInfo = (bvid) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/web-interface/view?bvid=${bvid}`, onload: response => { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data.data.cid); } else { reject(new Error('获取视频信息失败')); } } catch (error) { reject(error); } }, onerror: () => reject(new Error('网络请求失败')) }); }); }; const getDanmakuXml = (cid) => { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/v1/dm/list.so?oid=${cid}`, onload: response => resolve(response.responseText), onerror: () => reject(new Error('获取弹幕失败')) }); }); }; const getTopComment = () => { return new Promise((resolve) => { setTimeout(() => { try { const firstReplyItem = document.querySelector('.reply-list .root-reply-container'); if (!firstReplyItem) { resolve({ text: '', status: '不存在置顶评论' }); return; } const commentContentElement = firstReplyItem.querySelector('.reply-content .reply-con'); const commentText = commentContentElement ? commentContentElement.textContent.trim() : ''; const isPinned = firstReplyItem.querySelector('.reply-tag .top-badge'); if (isPinned) { if (commentText) { resolve({ text: commentText, status: '存在置顶评论,内容如下:' }); } else { resolve({ text: '', status: '存在置顶评论,但未能成功获取其内容。' }); } } else { if (commentText) { resolve({ text: commentText, status: '不存在置顶评论,首条评论内容为:' }); } else { resolve({ text: '', status: '不存在置顶评论' }); } } } catch (error) { console.error("获取评论失败:", error); resolve({ text: '', status: '获取评论时发生错误。' }); } }, 2000); }); }; const parseAndFilterDanmaku = (xmlString) => { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, 'text/xml'); const danmakus = Array.from(xmlDoc.querySelectorAll('d')); if (danmakus.length === 0) return null; const settings = GM_getValue('ai_settings', DEFAULT_SETTINGS); let filteredDanmakus = danmakus.map(d => { const attr = d.getAttribute('p').split(','); return { time: parseFloat(attr[0]), text: d.textContent.trim() }; }).filter(d => d.text.length > 0); if (settings.enableBlacklist && settings.blacklist.length > 0) { filteredDanmakus = filteredDanmakus.filter(d => { return !settings.blacklist.some(pattern => { if (settings.blacklistRegex) { try { return new RegExp(pattern, 'i').test(d.text); } catch (e) { return d.text.toLowerCase().includes(pattern.toLowerCase()); } } else { return d.text.toLowerCase().includes(pattern.toLowerCase()); } }); }); } if (settings.enableWhitelist && settings.whitelist.length > 0) { filteredDanmakus = filteredDanmakus.filter(d => { return settings.whitelist.some(pattern => { if (settings.whitelistRegex) { try { return new RegExp(pattern, 'i').test(d.text); } catch (e) { return d.text.toLowerCase().includes(pattern.toLowerCase()); } } else { return d.text.toLowerCase().includes(pattern.toLowerCase()); } }); }); } if (filteredDanmakus.length < settings.minDanmakuForFullAnalysis) { const simplePatterns = ['广告', '推广', '商品', '购买', '链接', '淘宝', '京东']; const hasAdKeywords = filteredDanmakus.some(d => simplePatterns.some(pattern => d.text.includes(pattern)) ); if (!hasAdKeywords) { showToast('过滤后有效弹幕过少且无明显广告标识, 跳过分析', 3000); return null; } } if (filteredDanmakus.length > settings.maxDanmakuCount) { for (let i = filteredDanmakus.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [filteredDanmakus[i], filteredDanmakus[j]] = [filteredDanmakus[j], filteredDanmakus[i]]; } filteredDanmakus = filteredDanmakus.slice(0, settings.maxDanmakuCount); } return filteredDanmakus .sort((a, b) => a.time - b.time) .map(d => `${secondsToTimeString(d.time)} ${d.text}`) .join('\n'); }; const callAI = async (danmakuText, topCommentString) => { const settings = GM_getValue('ai_settings', DEFAULT_SETTINGS); if (!settings.apiKey) { throw new Error('请先配置API密钥'); } const provider = API_PROVIDERS[settings.apiProvider]; const baseUrl = settings.baseUrl || provider.defaultUrl; const userMessage = `弹幕内容:\n${danmakuText}\n\n评论区情况:\n${topCommentString || '无'}`; let requestBody, headers, url; headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${settings.apiKey}` }; url = `${baseUrl}/chat/completions`; requestBody = { model: settings.model, messages: [ { role: 'system', content: settings.agentPrompt }, { role: 'user', content: userMessage } ], temperature: 0.3 }; if (settings.apiProvider === 'gemini') { url = `${baseUrl}/models/${settings.model}:generateContent?key=${settings.apiKey}`; headers = { 'Content-Type': 'application/json' }; requestBody = { contents: [{ parts: [{ text: `${settings.agentPrompt}\n\n${userMessage}` }] }] }; } else if (settings.apiProvider === 'anthropic') { url = `${baseUrl}/messages`; headers = { 'Content-Type': 'application/json', 'x-api-key': settings.apiKey, 'anthropic-version': '2023-06-01' }; requestBody = { model: settings.model, max_tokens: 1024, messages: [ { role: 'user', content: `${settings.agentPrompt}\n\n${userMessage}` } ] }; } else if (settings.apiProvider === 'custom') { if (settings.useLegacyOpenAIFormat) { showToast("传统OpenAI API格式的自定义逻辑尚未完全实现。", 5000); } if (settings.enableR1Params) { showToast("R1模型参数的自定义逻辑尚未完全实现。", 5000); } } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: headers, data: JSON.stringify(requestBody), onload: response => { try { const data = JSON.parse(response.responseText); let content; if (settings.apiProvider === 'gemini') { content = data.candidates?.[0]?.content?.parts?.[0]?.text; } else if (settings.apiProvider === 'anthropic') { content = data.content?.[0]?.text; } else { content = data.choices?.[0]?.message?.content; } if (!content) { console.error('AI响应中未找到有效内容:', data); throw new Error('AI响应格式错误或无有效内容'); } let jsonStr = content.trim(); if (jsonStr.startsWith('```json')) { jsonStr = jsonStr.replace(/^```json\s*\n?/, '').replace(/\n?```$/, ''); } else if (jsonStr.startsWith('```')) { jsonStr = jsonStr.replace(/^```\s*\n?/, '').replace(/\n?```$/, ''); } if (jsonStr.startsWith('`') && jsonStr.endsWith('`')) { jsonStr = jsonStr.slice(1, -1); } try { const result = JSON.parse(jsonStr); resolve(result); } catch (parseError) { console.error('JSON解析失败:', parseError, '原始响应:', content); throw new Error(`解析AI响应失败: ${parseError.message}. 原始响应: ${content.substring(0, 200)}...`); } } catch (error) { reject(error); } }, onerror: () => reject(new Error('AI API请求失败')) }); }); }; const calculateFinalProbability = (aiResult, settings) => { let finalProbability = aiResult.probability || 0; let adjustmentNote = ''; if (aiResult.start && aiResult.end) { const startSeconds = timeStringToSeconds(aiResult.start); const endSeconds = timeStringToSeconds(aiResult.end); const duration = endSeconds - startSeconds; if (duration < settings.minAdDuration) { const penalty = Math.min(30, (settings.minAdDuration - duration) * 2); finalProbability = Math.max(0, finalProbability - penalty); adjustmentNote += `时长过短惩罚: -${penalty}%; `; } if (duration > settings.maxAdDuration) { const penalty = Math.min(40, (duration - settings.maxAdDuration) * settings.durationPenalty); finalProbability = Math.max(0, finalProbability - penalty); adjustmentNote += `时长过长惩罚: -${penalty}%; `; } } return { ...aiResult, finalProbability: Math.round(finalProbability), adjustmentNote: adjustmentNote || '无调整' }; }; const showResultPopup = (result, danmakuSentToAI, commentStringSentToAI) => { const settings = GM_getValue('ai_settings', DEFAULT_SETTINGS); const popup = document.createElement('div'); popup.className = 'bili-ai-skipper-result-popup'; if (settings.theme === 'dark') { popup.classList.add('dark-theme'); } const escapeHtml = (unsafe) => { if (typeof unsafe !== 'string') { unsafe = String(unsafe || ''); } const tempDiv = document.createElement('div'); tempDiv.textContent = unsafe; return tempDiv.innerHTML; }; const formattedDanmakuAndComment = `【评论区情况】\n${commentStringSentToAI || '无'}\n\n【发送给AI的弹幕列表】\n${danmakuSentToAI || '无'}`; popup.innerHTML = `
广告概率: ${result.finalProbability}%
${result.start && result.end ? `广告时间: ${escapeHtml(result.start)} - ${escapeHtml(result.end)}
` : ''}分析说明: ${escapeHtml(result.note) || '无'}
概率调整: ${escapeHtml(result.adjustmentNote) || '无'}
错误信息: