// ==UserScript== // @name 抖音推荐影响器 (Smart Feed Assistant) // @namespace https://github.com/baianjo/Douyin-Smart-Feed-Assistant // @version 2.1.0 // @description 通过AI智能分析内容,优化你的信息流体验 // @author Baianjo // @match *://www.douyin.com/* // @connect api.moonshot.cn // @connect api.deepseek.com // @connect dashscope.aliyuncs.com // @connect dashscope-intl.aliyuncs.com // @connect open.bigmodel.cn // @connect * // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant unsafeWindow // @run-at document-end // @homepageURL https://github.com/baianjo/Douyin-Smart-Feed-Assistant // @supportURL mailto:1987892914@qq.com // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/553873/%E6%8A%96%E9%9F%B3%E6%8E%A8%E8%8D%90%E5%BD%B1%E5%93%8D%E5%99%A8%20%28Smart%20Feed%20Assistant%29.user.js // @updateURL https://update.greasyfork.icu/scripts/553873/%E6%8A%96%E9%9F%B3%E6%8E%A8%E8%8D%90%E5%BD%B1%E5%93%8D%E5%99%A8%20%28Smart%20Feed%20Assistant%29.meta.js // ==/UserScript== /* * ============================================================ * 🔧 开发者注意事项 (DEVELOPER NOTES) * ============================================================ * * 【需要定期更新的部分】 * * 1. DOM选择器 (约95-120行) * - 抖音更新后最容易失效的部分 * - 如果无法提取标题/作者/标签,请用F12检查新的类名 * - 添加新选择器到数组开头作为优先方案 * * 2. 快捷键映射 (约540行) * - 当前:Z=点赞, R=不感兴趣, X=评论, ArrowDown=下一个 * - 如果抖音修改快捷键,请在pressKey函数中更新 * * 【代码结构说明】 * - CONFIG模块: 所有配置项和默认值 * - Utils模块: 通用工具函数 * - VideoExtractor模块: DOM操作和内容提取 * - AIService模块: AI API调用和判断逻辑 * - UI模块: 用户界面创建和交互 * - Controller模块: 主控制流程 * * 【常见问题排查】 * - 无法提取视频信息 → 检查DOM选择器 * - API调用失败 → 检查网络和API Key * - 操作无效 → 检查快捷键是否变化 * - 视频不切换 → 检查滚动逻辑和判断条件 */ (function() { 'use strict'; // ==================== 配置管理模块 ==================== const CONFIG = { // 默认配置 defaults: { // API设置 apiKey: '', customEndpoint: '', // 自定义API地址(支持第三方转发) customModel: '', // 自定义模型名称 apiProvider: 'deepseek', // 记住用户选择的模板 selectedTemplate: '', // 空字符串表示"自定义规则" // 提示词 promptLike: '我希望看到积极向上、有教育意义、展示美好事物的内容。', promptNeutral: '普通的娱乐内容、日常生活记录,不特别推荐也不反对。', promptDislike: '低俗、暴力、虚假信息、过度营销的内容应该被过滤。', // 行为控制 minDelay: 1, maxDelay: 3, runDuration: 15, // 高级选项 skipProbability: 8, watchBeforeLike: [2, 4], maxRetries: 3, // UI状态 panelMinimized: true, panelPosition: { x: window.innerWidth - 80, y: 100 } }, /* * ⚠️ 重要:DOM选择器配置 * 这是最容易失效的部分,抖音每次更新可能都需要调整 * * 调试技巧: * 1. 打开F12开发者工具 * 2. 点击左上角的"选择元素"图标 * 3. 鼠标悬停在视频标题/作者/标签上 * 4. 查看右侧高亮的HTML结构 * 5. 复制类名或结构特征 * 6. 添加到下面的数组中(优先级从上到下) */ selectors: { // 视频标题 title: [ 'div[class*="pQBVl"] span span span', // 当前主方案 (2025-10) '#slidelist [data-e2e="feed-item"] div[style*="lineClamp"]', '.video-info-detail span', '[data-e2e="feed-title"]' ], // 作者名称 author: [ '[data-e2e="feed-author-name"]', '.author-name', 'a[class*="author"]', '[class*="AuthorName"]' ], // 标签(话题) tags: [ 'a[href*="/search/"]', '.tag-link', '[class*="hashtag"]', 'a[class*="SLdJu"]' // 当前发现的标签类名 ], }, // ⚠️ 开发者维护区域:API 提供商统一配置 // // 📌 requestParams 参数说明: // - 填写具体值(如 temperature: 0.3)→ 发送到 API // - 注释掉或删除该行 → 不发送,使用 API 默认值 // - stream: false 是必填项(禁用流式输出) // // 🔧 关于 vendorSpecific(厂商特定参数): // • 仅在预设厂商配置中使用(如 GLM 的 thinking 禁用) // • ⚠️ 切勿在所有配置中统一添加!原因: // - 多数 OpenAI 兼容 API 会严格验证参数 // - 遇到未知字段会返回 400/422 错误 // - 只有明确支持的厂商才能使用特定参数 // • 自定义 API 暂不应添加 vendorSpecific apiProviders: { deepseek: { name: 'DeepSeek(推荐:最便宜)', endpoint: 'https://api.deepseek.com/v1/chat/completions', defaultModel: 'deepseek-chat', models: [ { value: 'deepseek-chat', label: 'deepseek-chat (V3.2推荐)' } ], requestParams: { temperature: 0.3, // 可选:删除此行则使用 API 默认值 max_tokens: 500, // 可选:删除此行则使用 API 默认值 stream: false // 必填:禁用流式输出 } }, kimi: { name: 'Kimi / 月之暗面', endpoint: 'https://api.moonshot.cn/v1/chat/completions', defaultModel: 'kimi-k2-0905-preview', models: [ { value: 'kimi-k2-0905-preview', label: 'kimi-k2-0905-preview' } ], requestParams: { temperature: 0.3, max_tokens: 500, stream: false } }, qwen: { name: 'Qwen / 通义千问', endpoint: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', defaultModel: 'qwen-flash', models: [ { value: 'qwen-max', label: 'qwen-max(最强)' }, { value: 'qwen-plus', label: 'qwen-plus(推荐)' }, { value: 'qwen-flash', label: 'qwen-flash(快速)' } ], requestParams: { temperature: 0.3, max_tokens: 500, stream: false } }, glm: { name: 'GLM / 智谱AI', endpoint: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', defaultModel: 'glm-4.6', models: [ { value: 'glm-4.6', label: 'glm-4.6' }, { value: 'glm-4-flash', label: 'glm-4-flash(免费)' } ], requestParams: { temperature: 0.3, max_tokens: 500, stream: false, // ⚠️ GLM 专属:禁用思考模式(否则会超时) vendorSpecific: { thinking: { type: 'disabled' } } } }, gemini: { name: 'Gemini / Google AI Studio', endpoint: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', defaultModel: 'gemini-2.5-flash', models: [ { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' }, { value: 'gemini-2.5-pro', label: 'gemini-2.5-pro' }, ], requestParams: { stream: false, vendorSpecific: { "extra_body": { // Gemini 要求的字段名 "google": { "thinking_config": { "thinking_budget": 128, "include_thoughts": false } } } } } } }, // ✅ 简化:从统一配置中获取默认模型 getDefaultModel: (provider) => { return CONFIG.apiProviders[provider]?.defaultModel || ''; }, // 预设模板 templates: { '小学内容引导': { like: '对小学生趣味生动的STEM科普、历史故事,启发学习兴趣与好奇心;展现中国普通劳动者的奉献,或适合小学生同情的感人的家庭、师生、同学、社会百态、家国情谊、乡土情结;分享小升初的经验、名校风光、为什么学习等合理焦虑的正面话题;培养自律、诚信、爱护家人、尊重他人的品格;展现自然风光、创意手工、小学生健康运动,培养审美与动手能力;学习良好的价值观和金钱观。', neutral: '不含强烈价值观输出的日常生活记录、社会新闻、萌宠、美食、旅行片段;非低俗的唱歌、乐器弹奏等才艺表演;非暴力、非上瘾的益智类或创意类游戏短视频;死板/不够通俗/不够引人入胜的知识。', dislike: '无意义的玩梗、降智恶搞;炫富攀比、宣扬过度消费;展现不尊重长辈、师长,恶意捉弄他人,或传播负面情绪的内容;易上瘾的长时间游戏直播/录播;包含、低俗、性暗示的内容。' }, '中学内容引导': { like: '系统讲解科学、技术、历史、商业等领域知识,构建知识体系;专业技能(如编程、设计、摄影)的学习实践过程;对时事与社会现象有理有据的逻辑分析,提供多元视角,培养独立思考;顶尖学府生活、职业规划与个人成长经验;高质量纪录片,展现自然与文化厚重,培养人文关怀与社会责任感。', neutral: '不含强烈价值观输出的日常生活Vlog、美食探店、旅行记录;不含攻击性的普通新闻资讯;非专业、纯娱乐性质的才艺表演。', dislike: '纯粹玩梗、逻辑缺失的抽象内容;无节制宣扬消费主义、炫富;传播负面情绪、制造性别对立或社会矛盾;包含性暗示、观感不适的舞蹈、低俗笑话。' }, '效率与知识': { like: '商业分析、科技前沿、技能学习、效率工具、深度思考类内容。有价值、有启发。', neutral: '新闻资讯、行业动态等信息类内容。', dislike: '娱乐八卦、情感鸡汤、无意义的搞笑视频、标题党。' }, '新闻与时事': { like: '严肃新闻、社会事件、政策解读、国际局势、经济分析等客观理性的内容。', neutral: '地方新闻、社区故事等区域性内容。', dislike: '未经证实的传言、情绪化煽动、极端观点。' }, '健康生活': { like: '健身运动、营养饮食、心理健康、医学科普、户外活动等促进身心健康的内容。', neutral: '美食探店、旅游vlog等生活方式内容。', dislike: '伪科学养生、极端减肥、危险运动、不健康的生活方式。' }, '艺术审美': { like: '绘画、音乐、舞蹈、摄影、设计、建筑等艺术创作和欣赏内容。有美感、有深度。', neutral: '普通的才艺展示、手工DIY等创意内容。', dislike: '低俗模仿、审美庸俗、抄袭作品。' }, '美女审美': { like: '高颜值、身材姣好的年轻女性为绝对主角的视频。tag可能是舞蹈、御姐、黑丝、cos、女友、擦边、泳装、穿搭等。', neutral: '女性的展示内容,或无法分辨是什么视频类型。视频未完全满足like标准中的成品质量和视觉聚焦要求,但只要可能和女性相关即可,即使需要猜测。tag可能是表情管理、瑜伽、美颜等。这类视频标题往往是无意义的话甚至几乎无标题,如「心很贵 一定要装最美的东西/你想我了吗」', dislike: '严格排除所有非上述定义的视频。包括但不限于:纯风景、新闻、时政、科普、教育、影视剪辑、动漫、游戏、美食、萌宠、Vlog、生活记录、剧情短剧、手工、绘画等。' }, '帅哥审美': { 'like': '高颜值、身材姣好的年轻男性为绝对主角的视频。tag可能是舞蹈、型男、西装、肌肉、腹肌、cos、男友、男友视角、擦边、泳裤、穿搭、男神等。', 'neutral': '男性的展示内容,或无法分辨是什么视频类型。视频未完全满足like标准中的成品质量和视觉聚焦要求,但只要可能和男性相关即可,即使需要猜测。tag可能是表情管理、健身、运动、美颜等。这类视频标题往往是无意义的话甚至几乎无标题,如「今天的心情... / 猜我在想什么」', 'dislike': '严格排除所有非上述定义的视频。包括但不限于:纯风景、新闻、时政、科普、教育、影视剪辑、动漫、游戏、美食、萌宠、Vlog、生活记录、剧情短剧、手工、绘画等。' } } }; /** * 🔧 配置加载函数(带深度验证) * * 验证策略: * 1. 类型检查(number/string/boolean/object/array) * 2. 数值有效性(NaN/Infinity检查) * 3. 范围限制(min/max边界) * 4. 嵌套对象完整性(panelPosition、watchBeforeLike) */ const loadConfig = () => { try { const saved = GM_getValue('config', null); // 情况1:无存储数据 → 直接返回默认值(深拷贝) if (!saved || typeof saved !== 'object') { console.log('[智能助手] 📋 使用默认配置'); return JSON.parse(JSON.stringify(CONFIG.defaults)); } // 情况2:有存储数据 → 合并并验证 const merged = { ...CONFIG.defaults, ...saved }; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 📌 数值字段验证(关键参数) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const numberFields = [ { key: 'minDelay', min: 1, max: 60 }, { key: 'maxDelay', min: 1, max: 60 }, { key: 'runDuration', min: 1, max: 180 }, { key: 'skipProbability', min: 0, max: 100 }, { key: 'maxRetries', min: 1, max: 10 } ]; numberFields.forEach(({ key, min, max }) => { const val = merged[key]; // 检查:是否为数字、是否有效、是否在范围内 if ( typeof val !== 'number' || isNaN(val) || !isFinite(val) || // 排除Infinity val < min || val > max ) { console.warn(`[智能助手] ⚠️ 配置项 ${key} 无效 (${val}),使用默认值 (${CONFIG.defaults[key]})`); merged[key] = CONFIG.defaults[key]; } }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 📌 字符串字段验证 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const stringFields = ['apiKey', 'customEndpoint', 'customModel', 'apiProvider', 'selectedTemplate', 'promptLike', 'promptNeutral', 'promptDislike']; stringFields.forEach(key => { if (typeof merged[key] !== 'string') { console.warn(`[智能助手] ⚠️ 配置项 ${key} 类型错误,重置为默认值`); merged[key] = CONFIG.defaults[key]; } }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 📌 布尔字段验证 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ const boolFields = ['panelMinimized']; boolFields.forEach(key => { if (typeof merged[key] !== 'boolean') { console.warn(`[智能助手] ⚠️ 配置项 ${key} 类型错误,重置为默认值`); merged[key] = CONFIG.defaults[key]; } }); // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 📌 复杂对象验证:panelPosition // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ if ( !merged.panelPosition || typeof merged.panelPosition !== 'object' || typeof merged.panelPosition.x !== 'number' || typeof merged.panelPosition.y !== 'number' || isNaN(merged.panelPosition.x) || isNaN(merged.panelPosition.y) || !isFinite(merged.panelPosition.x) || !isFinite(merged.panelPosition.y) ) { console.warn('[智能助手] ⚠️ panelPosition 数据无效,重置为默认值'); merged.panelPosition = { x: CONFIG.defaults.panelPosition.x, y: CONFIG.defaults.panelPosition.y }; } else { // 🆕 额外检查:位置是否在屏幕范围内 const maxX = window.innerWidth - 60; const maxY = window.innerHeight - 60; if (merged.panelPosition.x < 0 || merged.panelPosition.x > maxX) { console.warn('[智能助手] ⚠️ panelPosition.x 超出范围,自动修正'); merged.panelPosition.x = Math.max(0, Math.min(maxX, merged.panelPosition.x)); } if (merged.panelPosition.y < 0 || merged.panelPosition.y > maxY) { console.warn('[智能助手] ⚠️ panelPosition.y 超出范围,自动修正'); merged.panelPosition.y = Math.max(0, Math.min(maxY, merged.panelPosition.y)); } } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 📌 复杂对象验证:watchBeforeLike // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ if ( !Array.isArray(merged.watchBeforeLike) || merged.watchBeforeLike.length !== 2 || typeof merged.watchBeforeLike[0] !== 'number' || typeof merged.watchBeforeLike[1] !== 'number' || isNaN(merged.watchBeforeLike[0]) || isNaN(merged.watchBeforeLike[1]) || merged.watchBeforeLike[0] < 0 || merged.watchBeforeLike[1] > 30 || merged.watchBeforeLike[0] > merged.watchBeforeLike[1] // 🆕 逻辑检查:min不能大于max ) { console.warn('[智能助手] ⚠️ watchBeforeLike 数据无效,重置为默认值'); merged.watchBeforeLike = [...CONFIG.defaults.watchBeforeLike]; } // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 📌 特殊逻辑验证 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 检查:minDelay 不能大于 maxDelay if (merged.minDelay > merged.maxDelay) { console.warn('[智能助手] ⚠️ minDelay > maxDelay,自动交换'); [merged.minDelay, merged.maxDelay] = [merged.maxDelay, merged.minDelay]; } // 检查:apiProvider 是否有效 const validProviders = [...Object.keys(CONFIG.apiProviders), 'custom']; if (!validProviders.includes(merged.apiProvider)) { console.warn(`[智能助手] ⚠️ apiProvider 无效 (${merged.apiProvider}),重置为 deepseek`); merged.apiProvider = 'deepseek'; } console.log('[智能助手] ✅ 配置加载并验证完成'); return merged; } catch (e) { // 🆕 错误处理:解析失败时返回默认值 console.error('[智能助手] ❌ 配置加载失败:', e); alert('⚠️ 配置数据损坏,已重置为默认值\n\n如果问题持续,请清除浏览器扩展数据后重试'); // 清除损坏的配置 try { GM_deleteValue('config'); } catch (delErr) { console.error('[智能助手] 无法删除损坏的配置:', delErr); } return JSON.parse(JSON.stringify(CONFIG.defaults)); } }; const saveConfig = async (config) => { try { // 🆕 保存前验证 console.log('[智能助手] 📝 准备保存配置:', { 位置: config.panelPosition, 最小化: config.panelMinimized }); // 🆕 验证位置数据有效性 if (config.panelPosition) { if (isNaN(config.panelPosition.x) || isNaN(config.panelPosition.y)) { console.error('[智能助手] ❌ 位置数据无效:', config.panelPosition); alert('⚠️ 检测到无效的位置数据(NaN),已取消保存'); return; } } // 同步写入 GM_setValue('config', config); // 延迟确保写入完成 await new Promise(resolve => setTimeout(resolve, 100)); // 🆕 延长到100ms // 🆕 验证写入成功 const saved = GM_getValue('config', null); if (saved && saved.panelPosition) { const match = saved.panelPosition.x === config.panelPosition.x && saved.panelPosition.y === config.panelPosition.y; console.log('[智能助手] ✅ 保存验证:', { 写入位置: config.panelPosition, 读取位置: saved.panelPosition, 匹配状态: match ? '✓ 成功' : '✗ 失败' }); if (!match) { console.error('[智能助手] ❌ 保存验证失败!写入的值和读取的值不一致'); } } else { console.error('[智能助手] ❌ 保存验证失败,读取到空数据'); } } catch (e) { console.error('[智能助手] ❌ GM_setValue 失败:', e); alert('⚠️ 配置保存失败!\n' + e.message); } }; // ==================== 工具函数 ==================== const Utils = { // 随机延迟(模拟人类行为) randomDelay: (min, max) => { return new Promise(resolve => { const delay = (Math.random() * (max - min) + min) * 1000; setTimeout(resolve, delay); }); }, // 查找元素(支持多套备用选择器) findElement: (selectors, root = document) => { for (const selector of selectors) { try { const el = root.querySelector(selector); if (el) return el; } catch (e) { console.warn(`[智能助手] 选择器失败: ${selector}`, e); } } return null; }, // 查找所有元素 findElements: (selectors, root = document) => { for (const selector of selectors) { try { const els = root.querySelectorAll(selector); if (els.length > 0) return Array.from(els); } catch (e) { console.warn(`[智能助手] 选择器失败: ${selector}`, e); } } return []; }, /* * 模拟键盘快捷键 * * 抖音网页版快捷键(可能随版本变化): * - Z: 点赞/取消点赞 * - X: 打开/关闭评论区 * - R: 标记"不感兴趣" * - ArrowDown/↓: 下一个视频 * - ArrowUp/↑: 上一个视频 * - Space: 播放/暂停 * * 如果抖音修改了快捷键,请在这里更新 */ pressKey: (key) => { const keyMap = { 'z': { key: 'z', code: 'KeyZ', keyCode: 90 }, 'x': { key: 'x', code: 'KeyX', keyCode: 88 }, 'r': { key: 'r', code: 'KeyR', keyCode: 82 }, 'ArrowDown': { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 }, 'ArrowUp': { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 }, 'Space': { key: ' ', code: 'Space', keyCode: 32 } }; const config = keyMap[key] || { key: key, code: key, keyCode: key.charCodeAt(0) }; const event = new KeyboardEvent('keydown', { key: config.key, code: config.code, keyCode: config.keyCode, bubbles: true, cancelable: true }); document.dispatchEvent(event); }, // 等待元素出现 waitForElement: (selectors, timeout = 5000) => { return new Promise((resolve) => { const startTime = Date.now(); const timer = setInterval(() => { const el = Utils.findElement(selectors); if (el || Date.now() - startTime > timeout) { clearInterval(timer); resolve(el); } }, 100); }); }, // 提取文本 extractText: (element) => { if (!element) return ''; return element.innerText || element.textContent || ''; }, // 格式化时间 formatTime: (seconds) => { const m = Math.floor(seconds / 60); const s = Math.floor(seconds % 60); return `${m}:${s.toString().padStart(2, '0')}`; } }; // ==================== DOM操作模块 ==================== const VideoExtractor = { // 🆕 通过视口中心定位当前视频容器 getCurrentFeedItem: () => { const centerX = window.innerWidth / 2; const centerY = window.innerHeight / 2; const centerEl = document.elementFromPoint(centerX, centerY); if (!centerEl) { UI.log('⚠️ 无法定位中心元素', 'warning'); return null; } // 向上查找 feed-item 容器 const feedItem = centerEl.closest('[data-e2e="feed-item"]'); if (!feedItem) { UI.log('⚠️ 未找到 feed-item 容器', 'warning'); } return feedItem; }, // 🆕 获取完整标题(改进版:不主动点击展开) getFullTitle: (container) => { if (!container) return ''; // 提取标题(优先级从高到低) const titleSelectors = [ 'div[class*="pQBVl"]', // 🆕 改为选择整个容器,而不是内部 span 'div[data-e2e="video-desc"]', '.video-info-detail', '[data-e2e="feed-title"]' ]; for (const selector of titleSelectors) { const el = container.querySelector(selector); if (el) { // 🆕 获取所有文本节点(包括被折叠的部分) let text = el.innerText || el.textContent || ''; // 过滤掉标签部分(# 开头的内容) const lines = text.split('\n'); let cleanText = ''; for (const line of lines) { if (line.trim().startsWith('#')) break; // 遇到标签就停止 cleanText += line + ' '; } cleanText = cleanText.trim(); // 移除"展开"按钮文本 cleanText = cleanText.replace(/展开$/, '').trim(); if (cleanText.length > 2) { return cleanText; } } } return ''; }, // 获取当前视频信息 getCurrentVideoInfo: async (config) => { // 🆕 增加初始等待,确保 DOM 稳定 await new Promise(r => setTimeout(r, 500)); // 🆕 智能重试机制(最多 4 次) let feedItem = null; const maxAttempts = 7; // ← 可配置重试次数 const retryDelayMs = 250; // ← 可配置重试间隔(毫秒) for (let attempt = 0; attempt < maxAttempts; attempt++) { feedItem = VideoExtractor.getCurrentFeedItem(); if (feedItem) { // 额外验证:确保元素在视口内 const rect = feedItem.getBoundingClientRect(); const isInView = rect.top < window.innerHeight && rect.bottom > 0; if (isInView) { if (attempt > 0) { UI.log(`✅ 重试成功(第 ${attempt + 1} 次)`, 'success'); } break; // 成功找到,退出循环 } else { UI.log(`⚠️ 找到元素但不在视口 (y: ${rect.top.toFixed(0)}),等待 ${retryDelayMs}ms 后重试`, 'warning'); feedItem = null; } } else { UI.log(`⚠️ 未找到 feed-item(尝试 ${attempt + 1}/${maxAttempts}),等待 ${retryDelayMs}ms 后重试`, 'warning'); } // 等待后重试(最后一次不等) if (attempt < maxAttempts - 1) { await new Promise(r => setTimeout(r, retryDelayMs)); } } if (!feedItem) { return null; } const info = { title: '', author: '', tags: [], url: window.location.href, isLive: false }; // 检测是否为直播 info.isLive = !!( feedItem.querySelector('[data-e2e="feed-live"]') || feedItem.querySelector('.live-icon') || feedItem.querySelector('a[data-e2e="live-slider"]') ); if (info.isLive) { UI.log('🔴 检测到直播,跳过信息提取', 'info'); return info; } // 提取标题(可能需要展开) info.title = VideoExtractor.getFullTitle(feedItem); // 如果标题太短,等待一下再试 if (info.title.length < 3) { await Utils.randomDelay(0.5, 0.5); info.title = VideoExtractor.getFullTitle(feedItem); } // 提取作者 const authorSelectors = [ '[data-e2e="feed-author-name"]', '.author-name', 'a[class*="author"]', '[class*="AuthorName"]' ]; for (const selector of authorSelectors) { const el = feedItem.querySelector(selector); if (el) { info.author = Utils.extractText(el).trim(); break; } } // 提取标签(只取前3个,避免混入其他视频) const tagEls = feedItem.querySelectorAll('a[href*="/search/"]'); info.tags = Array.from(tagEls) .slice(0, 3) .map(el => Utils.extractText(el).trim()) .filter(t => t.startsWith('#')); UI.log(`📺 标题: ${info.title.substring(0, 40)}${info.title.length > 40 ? '...' : ''}`, 'success'); if (info.author) UI.log(`👤 作者: ${info.author}`, 'info'); if (info.tags.length > 0) UI.log(`🏷️ 标签: ${info.tags.join(', ')}`, 'info'); return info; }, // 构建内容档案 buildDossier: (info) => { const parts = []; if (info.author) parts.push(`作者:${info.author}`); if (info.title) parts.push(`标题:${info.title}`); if (info.tags.length > 0) parts.push(`标签:${info.tags.join(', ')}`); return parts.join('。'); }, // 执行操作(简化版,不再需要回滚) executeAction: async (action, config) => { const [minWatch, maxWatch] = config.watchBeforeLike; const watchTime = Math.random() * (maxWatch - minWatch) + minWatch; UI.log(`⏱️ 观看 ${watchTime.toFixed(1)} 秒...`, 'info'); await Utils.randomDelay(minWatch, maxWatch); switch (action) { case 'like': UI.log('👍 执行: 点赞', 'success'); Utils.pressKey('z'); await Utils.randomDelay(1, 1.5); break; case 'dislike': UI.log('👎 执行: 不感兴趣', 'warning'); Utils.pressKey('r'); await Utils.randomDelay(0.5, 1.5); return; // 不感兴趣会自动跳转,不需要手动下滚 case 'neutral': UI.log('➡️ 执行: 忽略', 'info'); break; } // 下滚到下一个视频 UI.log('⬇️ 切换到下一个视频...', 'info'); Utils.pressKey('ArrowDown'); await Utils.randomDelay(1, 2.5); } }; // ==================== AI交互模块 ==================== const AIService = { /* * 调用AI API * * 支持多种API格式: * 1. 标准OpenAI格式(OpenAI, DeepSeek, Kimi等) * 2. 自定义endpoint(第三方转发服务) */ callAPI: (messages, config) => { return new Promise((resolve, reject) => { let endpoint = ''; // ✅ 修复:只有选择"自定义"时才使用 customEndpoint if (config.apiProvider === 'custom' && config.customEndpoint) { // ← 加上提供商判断 // 用户填写的自定义地址(简单处理) endpoint = config.customEndpoint.replace(/\/+$/, ''); // 如果用户只填了基础地址(如 https://api.example.com 或 https://api.example.com/v1) if (!endpoint.includes('/chat/completions')) { // 智能补全 if (/\/v\d+$/.test(endpoint)) { // 情况 1: 已有版本号 /v1, /v4 等 endpoint += '/chat/completions'; } else { // 情况 2: 无版本号或其他路径,统一加 /v1/chat/completions endpoint += '/v1/chat/completions'; } } } else { // 使用预设厂商的完整端点 const provider = CONFIG.apiProviders[config.apiProvider]; if (!provider) { reject(new Error('未知的 API 提供商')); return; } endpoint = provider.endpoint; } // ✅ 构建请求头(OpenAI 兼容格式) const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}` }; // ✅ 构建请求体基础部分(防止提供商间模型混用) let modelName; if (config.apiProvider === 'custom') { // 自定义模式:直接使用用户输入的模型名 modelName = config.customModel || 'gpt-3.5-turbo'; } else { // 预设模式:检查保存的模型是否在当前提供商的列表中 const provider = CONFIG.apiProviders[config.apiProvider]; const validModels = provider?.models?.map(m => m.value) || []; if (config.customModel && validModels.includes(config.customModel)) { modelName = config.customModel; } else { // 如果保存的模型不匹配,使用当前提供商的默认模型 modelName = CONFIG.getDefaultModel(config.apiProvider); } } const baseBody = { model: modelName, messages: messages }; // ✅ 合并厂商特定的请求参数(temperature、max_tokens、stream、vendorSpecific 等) const provider = CONFIG.apiProviders[config.apiProvider]; let body; if (provider?.requestParams) { const params = { ...provider.requestParams }; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // 🔧 vendorSpecific 自动展开机制 // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // // 作用:将厂商特定参数从容器中提取,放到请求体根级别 // // 示例转换: // 输入 requestParams: // { // temperature: 0.3, // vendorSpecific: { // thinking: { type: 'disabled' }, // custom_param: true // } // } // // 输出 HTTP 请求体: // { // "model": "glm-4.6", // "messages": [...], // "temperature": 0.3, ← 标准参数保留 // "thinking": { "type": "disabled" }, ← 从 vendorSpecific 展开 // "custom_param": true ← 从 vendorSpecific 展开 // } // // 为什么这样设计? // • 避免配置文件混乱(清晰区分标准参数和特殊参数) // • 防止参数冲突(不同厂商的特殊参数互不干扰) // // ⚠️ 注意事项: // • vendorSpecific 中的参数会覆盖同名的外层参数 // • 仅在预设厂商配置中使用,自定义 API 不支持 // • 如果参数未生效,检查日志中的"完整请求体 JSON" // // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ if (params.vendorSpecific && typeof params.vendorSpecific === 'object') { // 提取特殊参数 const vendorFields = params.vendorSpecific; // 从 params 中删除容器(避免发送 vendorSpecific 字段本身) delete params.vendorSpecific; // 合并:基础内容 + 标准参数 + 厂商特殊参数 body = { ...baseBody, // model, messages ...params, // temperature, stream 等 ...vendorFields // thinking, custom_param 等 }; // 🆕 更详细的调试日志 UI.log(`🔧 检测到 vendorSpecific 参数`, 'info', 'debug'); UI.log(`📦 容器内容: ${JSON.stringify(vendorFields)}`, 'info', 'debug'); UI.log(`✅ 已自动展开到请求体根级别`, 'success', 'debug'); } else { // 没有特殊参数,直接合并 body = { ...baseBody, ...params }; } } else { // 🆕 自定义 API:使用最小化请求体(第 818 行开始的逻辑) body = { ...baseBody, temperature: 0.3, max_tokens: 500, stream: false // ⚠️ 不添加 vendorSpecific! // 原因:不知道用户的 API 支持什么参数,保守策略 }; // 🆕 检测疑似推理模型,发出警告 const modelName = body.model.toLowerCase(); if (modelName.includes('reason') || modelName.includes('think') || modelName.includes('r1') || modelName.includes('o1')) { UI.log('⚠️⚠️⚠️ 警告:检测到疑似推理模型!', 'warning'); UI.log(`📛 模型名称: ${body.model}`, 'warning'); UI.log('💡 推理模型可能导致解析失败,强烈建议切换到标准对话模型', 'warning'); UI.log('✅ 推荐模型: deepseek-chat, gpt-4o-mini, claude-3.5-sonnet 等', 'info'); } } UI.log(`📡 请求地址: ${endpoint}`, 'info', 'debug'); UI.log(`🤖 使用模型: ${body.model}`, 'info', 'debug'); UI.log(`⚙️ 参数: temperature=${body.temperature}, max_tokens=${body.max_tokens}, stream=${body.stream}`, 'info', 'debug'); UI.log('──────── 📡 请求详情 ────────', 'info', 'debug'); UI.log(`🌐 完整 URL: ${endpoint}`, 'info', 'debug'); UI.log(`🔑 Authorization: Bearer ${config.apiKey.substring(0, 15)}...`, 'info', 'debug'); UI.log(`📦 请求体关键字段:`, 'info', 'debug'); UI.log(` • model: ${body.model}`, 'info', 'debug'); UI.log(` • temperature: ${body.temperature}`, 'info', 'debug'); UI.log(` • max_tokens: ${body.max_tokens}`, 'info', 'debug'); UI.log(` • stream: ${body.stream}`, 'info', 'debug'); if (body.thinking) { UI.log(` • thinking: ${JSON.stringify(body.thinking)}`, 'warning', 'debug'); } UI.log(`📄 完整请求体 JSON (前 800 字符):`, 'info', 'debug'); UI.log(JSON.stringify(body, null, 2).substring(0, 800), 'info', 'debug'); UI.log('────────────────────────────', 'info', 'debug'); // 🆕 添加等待提示 UI.log('⏳ 正在发送请求...', 'info', 'debug'); // 🆕 等待动画(每2秒输出一次) let waitCount = 0; const waitTimer = setInterval(() => { waitCount++; UI.log(`⏳ 等待服务器响应... (${waitCount * 2}秒)`, 'info', 'debug'); }, 2000); GM_xmlhttpRequest({ method: 'POST', url: endpoint, headers: headers, data: JSON.stringify(body), timeout: 30000, onload: (response) => { clearInterval(waitTimer); // 🆕 清除等待动画 UI.log('✅ 收到响应', 'success'); UI.log('──────── 📥 响应详情 ────────', 'info', 'debug'); UI.log(`📊 状态码: ${response.status} ${response.statusText}`, 'info', 'debug'); UI.log(`📄 响应体前 1000 字符:`, 'info', 'debug'); UI.log(response.responseText.substring(0, 1000), 'info', 'debug'); UI.log('────────────────────────────', 'info', 'debug'); try { if (response.status !== 200) { UI.log(`❌ HTTP ${response.status}: ${response.statusText}`, 'error'); reject(new Error(`HTTP ${response.status}: ${response.responseText.substring(0, 200)}`)); return; } const data = JSON.parse(response.responseText); let content = ''; // 🆕 改进:处理标准格式 + 推理模型的特殊格式 if (data.choices && data.choices[0] && data.choices[0].message) { const msg = data.choices[0].message; content = msg.content || ''; // 标准字段 // 🆕 检测推理模型的特殊响应 if (!content && msg.reasoning_content) { UI.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'error'); UI.log('❌ 检测到推理模型的响应格式!', 'error'); UI.log('', 'error'); UI.log('📋 详细信息:', 'error'); UI.log(` • API 返回了 reasoning_content 而非 content`, 'error'); UI.log(` • 这表明你使用了带推理功能的模型`, 'error'); UI.log(` • 当前模型: ${body.model}`, 'error'); UI.log('', 'error'); UI.log('✅ 解决方案:', 'info'); UI.log(' 1. 如使用自定义API,请切换到标准对话模型', 'info'); UI.log(' 推荐: deepseek-chat, gpt-4o-mini, claude-3.5-sonnet', 'info'); UI.log(' 2. 或在"基础设置"中选择预设厂商(已优化)', 'info'); UI.log('', 'error'); UI.log('💡 为什么会这样?', 'info'); UI.log(' 推理模型(如 deepseek-reasoner)会先思考再回答,', 'info'); UI.log(' 其思考过程存储在 reasoning_content 中,', 'info'); UI.log(' 而本脚本需要直接的回答(存储在 content 中)。', 'info'); UI.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', 'error'); throw new Error( '推理模型响应格式不兼容\n\n' + '请切换到标准对话模型,或使用预设厂商配置。\n' + '详细信息请查看运行日志。' ); } } else if (data.message && data.message.content) { content = data.message.content; } else { UI.log(`⚠️ 未知响应格式: ${JSON.stringify(data).substring(0, 300)}`, 'error'); throw new Error('API 返回了不支持的格式,请检查模型是否正确'); } if (!content) { // 🆕 更详细的空内容错误提示 const rawSnippet = response.responseText.substring(0, 500); let errorMsg = 'API 返回空内容'; // 二次检测(防止某些边缘情况) if (rawSnippet.includes('reasoning') || rawSnippet.includes('thinking')) { errorMsg += '\n\n可能使用了推理模型,请切换到标准对话模型'; } throw new Error(errorMsg + '\n\n原始响应片段:\n' + rawSnippet); } UI.log('✅ AI 响应成功', 'success'); resolve(content); } catch (e) { UI.log(`💥 解析失败: ${e.message}`, 'error'); reject(new Error(`${e.message}\n原始响应: ${response.responseText.substring(0, 500)}`)); } }, onerror: (error) => { clearInterval(waitTimer); // 🆕 清除等待动画 const msg = `🌐 网络错误 - ${error.statusText || error.error || '连接失败'}`; UI.log(msg, 'error'); reject(new Error(msg)); }, ontimeout: () => { clearInterval(waitTimer); // 🆕 清除等待动画 UI.log('⏱️ 请求超时(30秒)', 'error'); reject(new Error('请求超时,可能是网络问题或模型响应过慢')); } }); }); }, // 测试API连接 testAPI: async (config) => { UI.log('════════════════════════════', 'info'); UI.log('🧪 开始测试 API 连接', 'info'); UI.log('════════════════════════════', 'info'); // 🆕 显示当前配置快照 UI.log(`📌 配置快照:`, 'info', 'debug'); UI.log(` • API 提供商: ${config.apiProvider}`, 'info', 'debug'); UI.log(` • API Key 前缀: ${config.apiKey.substring(0, 12)}...`, 'info', 'debug'); UI.log(` • 自定义端点: ${config.customEndpoint || '(空 - 使用预设)'}`, 'info'); UI.log(` • 自定义模型: ${config.customModel || '(空 - 使用预设)'}`, 'info'); UI.log('', 'info'); const testMessages = [ { role: 'user', content: '请回复"连接成功"' } ]; try { const response = await AIService.callAPI(testMessages, config); UI.log('════════════════════════════', 'success'); UI.log('✅ API 测试成功!', 'success'); UI.log(`📨 AI 响应内容: ${response.substring(0, 100)}`, 'success'); UI.log('════════════════════════════', 'success'); return { success: true, message: response }; } catch (e) { UI.log('════════════════════════════', 'error'); UI.log('❌ API 测试失败!', 'error'); UI.log(`📛 错误消息: ${e.message}`, 'error'); UI.log('💡 请检查上方的请求/响应详情', 'warning'); UI.log('════════════════════════════', 'error'); return { success: false, message: e.message }; } }, // 单次判定模式(推荐) judgeSingle: async (dossier, config) => { const prompt = `你是一个内容分类助手。现在给出三种规则:「 【点赞规则】 ${config.promptLike} 【忽略规则】 ${config.promptNeutral} 【不感兴趣规则】 ${config.promptDislike} 」 请根据以上规则判断下述视频内容:「 【视频内容】 ${dossier} 」 **重要提示**:标签可能包含干扰或对不上该视频标题的信息。 请直接回答以下JSON格式,不要有任何其他内容: {"action": "like/neutral/dislike", "reason": "简短理由"}`; const messages = [{ role: 'user', content: prompt }]; const response = await AIService.callAPI(messages, config); // 解析JSON const jsonMatch = response.match(/\{[^}]+\}/); if (jsonMatch) { return JSON.parse(jsonMatch[0]); } // 降级解析 if (response.includes('like') || response.includes('点赞')) { return { action: 'like', reason: response }; } if (response.includes('dislike') || response.includes('不感兴趣')) { return { action: 'dislike', reason: response }; } return { action: 'neutral', reason: response }; }, // 主判定入口 judge: async (dossier, config) => { return await AIService.judgeSingle(dossier, config); } }; // ==================== UI模块 ==================== const UI = { panel: null, floatingButton: null, create: () => { // 添加样式 GM_addStyle(` /* 悬浮按钮 - 水晶风格 */ .smart-feed-float-btn { position: fixed; width: 60px; height: 60px; border-radius: 50%; background: linear-gradient(135deg, rgba(139, 162, 251, 0.85) 0%, rgba(185, 163, 251, 0.85) 100%); backdrop-filter: blur(10px); box-shadow: 0 8px 32px rgba(139, 162, 251, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4); border: 1px solid rgba(255, 255, 255, 0.2); cursor: move; z-index: 999999; display: flex; align-items: center; justify-content: center; font-size: 24px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); user-select: none; } .smart-feed-float-btn:hover { transform: scale(1.05); box-shadow: 0 12px 40px rgba(139, 162, 251, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.5); } .smart-feed-float-btn.running { background: linear-gradient(135deg, rgba(99, 230, 190, 0.85) 0%, rgba(56, 178, 172, 0.85) 100%); animation: pulse-glow 2s infinite; } @keyframes pulse-glow { 0%, 100% { box-shadow: 0 8px 32px rgba(99, 230, 190, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.4); } 50% { box-shadow: 0 12px 48px rgba(99, 230, 190, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.5); } } /* 主面板 - 毛玻璃效果 */ .smart-feed-panel { position: fixed; width: 420px; max-height: 80vh; background: rgba(255, 255, 255, 0.5); backdrop-filter: blur(20px) saturate(180%); -webkit-backdrop-filter: blur(20px) saturate(180%); border-radius: 20px; box-shadow: 0 20px 60px rgba(100, 100, 150, 0.15), 0 0 0 1px rgba(255, 255, 255, 0.3); z-index: 999998; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #1f2937; overflow: hidden; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } /* 顶部标题栏 - 水晶风格 + 集成开始按钮 */ .smart-feed-header { padding: 16px 20px; background: linear-gradient(135deg, rgba(139, 162, 251, 0.65) 0%, rgba(185, 163, 251, 0.65) 100%); backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255, 255, 255, 0.2); display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; } .smart-feed-title { font-size: 16px; font-weight: 700; color: rgba(255, 255, 255, 0.95); display: flex; align-items: center; gap: 8px; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } /* 顶部按钮组 */ .smart-feed-header-actions { display: flex; gap: 8px; align-items: center; } /* 开始运行按钮(在顶部) */ .smart-feed-start-btn { padding: 8px 16px; border-radius: 10px; border: none; background: rgba(255, 255, 255, 0.9); color: #10b981; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2); } .smart-feed-start-btn:hover { background: rgba(255, 255, 255, 1); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); } .smart-feed-start-btn.running { background: rgba(239, 68, 68, 0.9); color: white; } .smart-feed-start-btn.running:hover { background: rgba(239, 68, 68, 1); } .smart-feed-close { width: 32px; height: 32px; border-radius: 50%; background: rgba(255, 255, 255, 0.25); backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.95); font-size: 20px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; justify-content: center; } .smart-feed-close:hover { background: rgba(255, 255, 255, 0.35); transform: rotate(90deg); } .smart-feed-body { max-height: calc(80vh - 70px); overflow-y: auto; padding: 20px; background: rgba(255, 255, 255, 0.5); } .smart-feed-body::-webkit-scrollbar { width: 6px; } .smart-feed-body::-webkit-scrollbar-thumb { background: rgba(139, 162, 251, 0.3); border-radius: 3px; } .smart-feed-body::-webkit-scrollbar-thumb:hover { background: rgba(139, 162, 251, 0.5); } /* 标签页 */ .smart-feed-tabs { display: flex; gap: 8px; margin-bottom: 20px; background: rgba(241, 245, 249, 0.6); backdrop-filter: blur(10px); padding: 4px; border-radius: 12px; } .smart-feed-tab { flex: 1; padding: 10px; border: none; background: transparent; color: #64748b; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; } .smart-feed-tab:hover { color: #475569; background: rgba(255, 255, 255, 0.5); } .smart-feed-tab.active { background: rgba(255, 255, 255, 0.9); color: rgba(139, 162, 251, 1); box-shadow: 0 2px 8px rgba(139, 162, 251, 0.15); } /* 表单元素 */ .smart-feed-section { margin-bottom: 20px; } .smart-feed-label { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-size: 14px; font-weight: 600; color: #374151; } .smart-feed-help { cursor: help; width: 18px; height: 18px; border-radius: 50%; background: rgba(139, 162, 251, 0.2); color: rgba(139, 162, 251, 1); display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold; transition: all 0.2s; } .smart-feed-help:hover { background: rgba(139, 162, 251, 0.9); color: white; transform: scale(1.1); } .smart-feed-input, .smart-feed-textarea, .smart-feed-select { width: 100%; padding: 12px; border: 2px solid rgba(229, 231, 235, 0.8); border-radius: 10px; background: rgba(255, 255, 255, 0.8); backdrop-filter: blur(10px); color: #1f2937; font-size: 14px; transition: all 0.2s; box-sizing: border-box; } .smart-feed-input:focus, .smart-feed-textarea:focus, .smart-feed-select:focus { outline: none; border-color: rgba(139, 162, 251, 0.8); background: rgba(255, 255, 255, 0.95); box-shadow: 0 0 0 3px rgba(139, 162, 251, 0.1); } .smart-feed-textarea { min-height: 80px; resize: vertical; font-family: inherit; } .smart-feed-button { width: 100%; padding: 14px; border: none; border-radius: 12px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 10px; } .smart-feed-button-primary { background: linear-gradient(135deg, rgba(16, 185, 129, 0.9) 0%, rgba(5, 150, 105, 0.9) 100%); color: white; box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2); } .smart-feed-button-primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3); } .smart-feed-button-stop { background: linear-gradient(135deg, rgba(239, 68, 68, 0.9) 0%, rgba(220, 38, 38, 0.9) 100%); color: white; } .smart-feed-button-secondary { background: rgba(243, 244, 246, 0.8); backdrop-filter: blur(10px); color: #374151; } .smart-feed-button-secondary:hover { background: rgba(229, 231, 235, 0.9); } /* 日志 */ .smart-feed-log { background: rgba(248, 250, 252, 0.8); backdrop-filter: blur(10px); border: 1px solid rgba(226, 232, 240, 0.8); border-radius: 10px; padding: 15px; max-height: 300px; overflow-y: auto; font-size: 12px; font-family: 'Courier New', monospace; } /* 🆕 在这里添加以下新样式(约第352行) */ /* 可折叠日志容器 */ .collapsible-log { position: relative; display: inline-block; width: 100%; } /* 预览文本(默认显示) */ .collapsible-log .log-preview { display: inline; color: inherit; } /* 完整文本(默认隐藏) */ .collapsible-log .log-full { display: none; margin-top: 8px; padding: 10px; background: rgba(241, 245, 249, 0.9); border-radius: 6px; border: 1px solid rgba(226, 232, 240, 0.6); font-size: 11px; line-height: 1.6; overflow-x: auto; white-space: pre-wrap; word-break: break-all; } /* 展开按钮 */ .collapsible-log .expand-btn { margin-left: 8px; padding: 2px 8px; border: none; background: rgba(139, 162, 251, 0.15); color: rgba(139, 162, 251, 1); border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.2s; vertical-align: middle; } .collapsible-log .expand-btn:hover { background: rgba(139, 162, 251, 0.25); transform: translateY(-1px); } /* 展开状态 */ .collapsible-log.expanded .log-preview { display: none; } .collapsible-log.expanded .log-full { display: block; } .collapsible-log.expanded .expand-btn { background: rgba(239, 68, 68, 0.15); color: rgba(239, 68, 68, 1); } .collapsible-log.expanded .expand-btn::before { content: '收起 '; } .collapsible-log:not(.expanded) .expand-btn::before { content: '展开 '; } .smart-feed-log-item { margin-bottom: 8px; padding: 6px 0; border-bottom: 1px solid rgba(226, 232, 240, 0.5); display: flex; gap: 10px; } .smart-feed-log-time { color: #94a3b8; flex-shrink: 0; } .smart-feed-log-text { flex: 1; } /* 其他 */ .smart-feed-range-group { display: flex; gap: 10px; align-items: center; } .smart-feed-range-input { flex: 1; } .smart-feed-checkbox-group { display: flex; align-items: center; gap: 10px; padding: 12px; background: rgba(248, 250, 252, 0.8); backdrop-filter: blur(10px); border-radius: 10px; } .smart-feed-checkbox { width: 20px; height: 20px; cursor: pointer; } .smart-feed-info-box { background: rgba(254, 243, 199, 0.8); backdrop-filter: blur(10px); border-left: 4px solid rgba(245, 158, 11, 0.8); padding: 12px; border-radius: 8px; font-size: 13px; color: #92400e; margin-bottom: 15px; } .smart-feed-link { color: rgba(139, 162, 251, 1); text-decoration: none; font-weight: 600; } .smart-feed-link:hover { text-decoration: underline; } /* 统计卡片 */ .smart-feed-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-bottom: 20px; } .smart-feed-stat-card { background: linear-gradient(135deg, rgba(240, 249, 255, 0.8) 0%, rgba(224, 242, 254, 0.8) 100%); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; text-align: center; border: 1px solid rgba(186, 230, 253, 0.3); } .smart-feed-stat-value { font-size: 24px; font-weight: 700; color: #0284c7; } .smart-feed-stat-label { font-size: 12px; color: #64748b; margin-top: 5px; } /* 性能优化:启用 GPU 加速 */ .smart-feed-panel, .smart-feed-float-btn, .smart-feed-button { will-change: transform; transform: translateZ(0); } /* 可折叠帮助框 */ .collapsible-help-box .help-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; user-select: none; } .collapsible-help-box .help-toggle-btn { padding: 4px 12px; border: none; background: rgba(139, 162, 251, 0.2); color: rgba(139, 162, 251, 1); border-radius: 6px; cursor: pointer; font-size: 12px; font-weight: 600; transition: all 0.2s; } .collapsible-help-box .help-toggle-btn:hover { background: rgba(139, 162, 251, 0.3); transform: translateY(-1px); } .collapsible-help-box .help-content { display: none; margin-top: 12px; padding-top: 12px; } .collapsible-help-box.expanded .help-content { display: block; } .collapsible-help-box.expanded .help-toggle-btn { background: rgba(239, 68, 68, 0.2); color: rgba(239, 68, 68, 1); } `); const config = loadConfig(); // 🆕 详细调试日志 console.log('[智能助手] 🔧 初始化 - 完整配置:', config); console.log('[智能助手] 📍 panelPosition 原始值:', config.panelPosition); console.log('[智能助手] 📍 panelPosition 类型检查:', { 是对象: typeof config.panelPosition === 'object', x类型: typeof config.panelPosition?.x, y类型: typeof config.panelPosition?.y, x值: config.panelPosition?.x, y值: config.panelPosition?.y }); // 创建悬浮按钮 UI.floatingButton = document.createElement('div'); UI.floatingButton.className = 'smart-feed-float-btn'; UI.floatingButton.innerHTML = '🤖'; // 🆕 更严格的位置解析 let savedX, savedY, useDefault = false; if (config.panelPosition && typeof config.panelPosition.x === 'number' && typeof config.panelPosition.y === 'number' && !isNaN(config.panelPosition.x) && !isNaN(config.panelPosition.y)) { savedX = config.panelPosition.x; savedY = config.panelPosition.y; console.log('[智能助手] ✅ 使用保存的位置:', savedX, savedY); } else { savedX = window.innerWidth - 80; savedY = 100; useDefault = true; console.log('[智能助手] ⚠️ 使用默认位置(原因: panelPosition无效):', savedX, savedY); console.log('[智能助手] 💡 判断依据:', { 存在性: !!config.panelPosition, x是数字: typeof config.panelPosition?.x === 'number', y是数字: typeof config.panelPosition?.y === 'number', x非NaN: !isNaN(config.panelPosition?.x), y非NaN: !isNaN(config.panelPosition?.y) }); } // 🆕 确保值在合理范围内 savedX = Math.max(0, Math.min(window.innerWidth - 60, savedX)); savedY = Math.max(0, Math.min(window.innerHeight - 60, savedY)); // 🆕 显式设置style(确保没有transform干扰) UI.floatingButton.style.left = savedX + 'px'; UI.floatingButton.style.top = savedY + 'px'; UI.floatingButton.style.transform = 'none'; // 🆕 强制移除transform UI.floatingButton.title = '点击打开智能助手'; console.log('[智能助手] 🎯 按钮最终位置:', { left: UI.floatingButton.style.left, top: UI.floatingButton.style.top, 使用默认值: useDefault }); // 创建主面板(默认隐藏) UI.panel = document.createElement('div'); UI.panel.className = 'smart-feed-panel'; UI.panel.style.display = config.panelMinimized ? 'none' : 'block'; // 🆕 面板位置跟随按钮 const panelLeft = Math.max(10, savedX - 360); const panelTop = Math.max(10, savedY); UI.panel.style.left = panelLeft + 'px'; UI.panel.style.top = panelTop + 'px'; console.log('[智能助手] 面板初始位置:', panelLeft, panelTop); // 🆕 调试日志 UI.panel.innerHTML = `
| 步骤 1 |
获取 API Key(注册即可) • 推荐新手选 DeepSeek • 无论何种平台,注册后在控制台点"创建 API Key"(确保有余额,1元足矣。初次注册可能会送),复制那串英文 • 或选 智谱GLM(有长期免费模型,但其控制台稍显复杂) |
| 步骤 2 |
填写配置 • 在下方"API 提供商"选你刚注册的平台 • 把复制的 Key 粘贴到"API Key"输入框 • 点击"🧪 测试连接"按钮(看到绿色成功提示就对了) |
| 步骤 3 |
设置偏好 • 新手直接选"预设模板"(如"青少年内容引导") • 或者在三个规则框里描述你想看/不想看什么 • 滚动到底部点"💾 保存当前配置" |
https://api.example.com/v1gpt-4o-mini)deepseek-reasoner、o1 等会导致解析失败
CONFIG.apiProviders[厂商].models 数组