// ==UserScript== // @name Nuist南信大公告更新检测 // @namespace https://bulletin.nuist.edu.cn/ // @version 3.3 // @description 每小时检查 NUIST 最新公告,并在配置AI API KEY的情况下总结最新公告 // @author QianYu // @crontab * once * * * // @grant GM.setValue // @grant GM.getValue // @grant GM.registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_notification // @connect bulletin.nuist.edu.cn // @connect generativelanguage.googleapis.com // @connect dashscope.aliyuncs.com // @connect models.inference.ai.azure.com // @connect api.deepseek.com // ==/UserScript== (function() { 'use strict'; // 生成 AI 总结 async function generateAISummary(newsTitles) { const provider = await GM.getValue('aiProvider', 'gemini'); const summaryLanguage = await GM.getValue('summaryLanguage', 'zh-CN'); // 获取对应提供商的 API Key const apiKey = await GM.getValue(`${provider}ApiKey`, ''); console.log('AI 提供商:', provider); console.log('API Key 状态:', apiKey ? '已配置' : '未配置'); if (!apiKey) { // 如果没有配置 API,返回默认总结(带层级标题) const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); return `今日更新 ${newsTitles.length} 条公告\n${numberedList}`; } const prompt = summaryLanguage === 'zh-CN' ? `请用一句话75个字以内总结以下${newsTitles.length}条南京信息工程大学公告的主要内容:\n${newsTitles.join('\n')}` : `Please summarize the following ${newsTitles.length} NUIST announcements in one sentence (in ${summaryLanguage}):\n${newsTitles.join('\n')}`; try { // 根据不同提供商调用对应的 API switch (provider) { case 'gemini': return await callGeminiAPI(apiKey, prompt, newsTitles); case 'qwen': return await callQwenAPI(apiKey, prompt, newsTitles); case 'github': return await callGitHubModelsAPI(apiKey, prompt, newsTitles); case 'deepseek': return await callDeepSeekAPI(apiKey, prompt, newsTitles); default: console.error('未知的 AI 提供商:', provider); const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); return `今日更新 ${newsTitles.length} 条公告\n${numberedList}`; } } catch (error) { console.error('AI 总结生成失败:', error); const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); return `今日更新 ${newsTitles.length} 条公告\n${numberedList}`; } } // Gemini API 调用 async function callGeminiAPI(apiKey, prompt, newsTitles) { const model = await GM.getValue('geminiModel', 'gemini-2.0-flash-exp'); console.log('使用 Gemini 模型:', model); return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, headers: {'Content-Type': 'application/json'}, data: JSON.stringify({ contents: [{parts: [{text: prompt}]}] }), onload: function(response) { console.log('Gemini API 响应:', response.status); try { const result = JSON.parse(response.responseText); if (result.candidates?.[0]?.content?.parts?.[0]?.text) { resolve(result.candidates[0].content.parts[0].text.trim()); } else { const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } } catch (error) { console.error('Gemini 解析错误:', error); const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } }, onerror: function(error) { console.error('Gemini API 错误:', error); const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); }, timeout: 15000 }); }); } // 通用 OpenAI 格式 API 调用(GitHub Models, DeepSeek) async function callOpenAIFormatAPI(apiKey, prompt, newsTitles, baseURL, modelName) { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: `${baseURL}/chat/completions`, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: modelName, messages: [{role: 'user', content: prompt}], max_tokens: 150 }), onload: function(response) { console.log(`${modelName} API 响应:`, response.status); try { const result = JSON.parse(response.responseText); if (result.choices?.[0]?.message?.content) { resolve(result.choices[0].message.content.trim()); } else { const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } } catch (error) { console.error(`${modelName} 解析错误:`, error); const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } }, onerror: function(error) { console.error(`${modelName} API 错误:`, error); const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); }, timeout: 15000 }); }); } // Qwen API 调用 async function callQwenAPI(apiKey, prompt, newsTitles) { const model = await GM.getValue('qwenModel', 'qwen-plus'); console.log('使用 Qwen 模型:', model); return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, data: JSON.stringify({ model: model, input: {messages: [{role: 'user', content: prompt}]}, parameters: {max_tokens: 150} }), onload: function(response) { console.log('Qwen API 响应:', response.status); try { const result = JSON.parse(response.responseText); if (result.output?.text) { resolve(result.output.text.trim()); } else { const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } } catch (error) { console.error('Qwen 解析错误:', error); const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } }, onerror: function(error) { console.error('Qwen API 错误:', error); const numberedList = newsTitles.map((t, i) => `${i + 1}. ${t}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); }, timeout: 15000 }); }); } // GitHub Models API 调用 async function callGitHubModelsAPI(apiKey, prompt, newsTitles) { const model = await GM.getValue('githubModel', 'gpt-4o-mini'); console.log('使用 GitHub Models:', model); return await callOpenAIFormatAPI(apiKey, prompt, newsTitles, 'https://models.inference.ai.azure.com', model); } // DeepSeek API 调用 async function callDeepSeekAPI(apiKey, prompt, newsTitles) { const model = await GM.getValue('deepseekModel', 'deepseek-chat'); console.log('使用 DeepSeek 模型:', model); return await callOpenAIFormatAPI(apiKey, prompt, newsTitles, 'https://api.deepseek.com', model); } async function checkForChanges() { GM_xmlhttpRequest({ method: 'GET', url: 'https://bulletin.nuist.edu.cn/', onload: async function(response) { const pageContent = response.responseText; const parser = new DOMParser(); const doc = parser.parseFromString(pageContent, 'text/html'); const currentZdtbTitles = []; const zdtbItems = doc.querySelectorAll('.zdtb a'); zdtbItems.forEach(item => { const title = item.textContent.trim(); currentZdtbTitles.push(title); }); const previousZdtbTitles = await GM.getValue('zdtbTitles', []); let changeDetected = false; if (JSON.stringify(currentZdtbTitles) !== JSON.stringify(previousZdtbTitles)) { changeDetected = true; console.log('置顶标题发生变化!'); } else { console.log('置顶标题无变化。'); } const newsTitles = []; const newsUrls = []; const newsItems = doc.querySelectorAll('.news_list.clearfix .news'); // 只获取带有 zxtb (最新) 标签的公告 newsItems.forEach((item) => { if (item.querySelector('.zxtb img')) { const titleElement = item.querySelector('.news_title .btt a'); const title = titleElement.textContent.trim(); const url = titleElement.href; newsTitles.push(title); newsUrls.push(url); } }); console.log('最新公告标题:', newsTitles); if (newsTitles.length > 0) { // 生成 AI 总结 const summary = await generateAISummary(newsTitles); const notificationTimeout = await GM.getValue('notificationTimeout', 10000); GM_notification({ title: changeDetected ? 'NUIST Bulletin Update <有变化>' : 'NUIST Bulletin Update', text: summary, timeout: notificationTimeout, image: 'https://www.nuist.edu.cn/_upload/tpl/00/43/67/template67/images/logo.png', onclick: () => { window.open('https://bulletin.nuist.edu.cn/', '_blank'); } }); console.log('通知已发送,AI总结:', summary); } else { console.log('没有最新公告'); } await GM.setValue('zdtbTitles', currentZdtbTitles); } }); } checkForChanges(); GM.registerMenuCommand('手动检查公告变化', checkForChanges); // AI 提供商配置 GM.registerMenuCommand('选择 AI 提供商', async () => { const providers = { 'gemini': 'Google Gemini', 'qwen': '通义千问 (Qwen)', 'github': 'GitHub Models', 'deepseek': 'DeepSeek' }; const currentProvider = await GM.getValue('aiProvider', 'gemini'); const providerList = Object.entries(providers).map(([key, name], i) => `${i + 1}. ${name}${key === currentProvider ? ' (当前)' : ''}` ).join('\n'); const choice = prompt(`选择 AI 提供商:\n\n${providerList}\n\n当前: ${providers[currentProvider]}`); if (choice !== null && choice.trim()) { const choiceNum = parseInt(choice.trim()); const providerKeys = Object.keys(providers); if (choiceNum >= 1 && choiceNum <= providerKeys.length) { const selectedProvider = providerKeys[choiceNum - 1]; await GM.setValue('aiProvider', selectedProvider); alert(`已切换到: ${providers[selectedProvider]}\n\n请记得配置对应的 API Key!`); console.log('已切换 AI 提供商:', selectedProvider); } } }); // 配置 API Key GM.registerMenuCommand('配置 API Key', async () => { const provider = await GM.getValue('aiProvider', 'gemini'); const providerNames = { 'gemini': 'Google Gemini', 'qwen': '通义千问 (Qwen)', 'github': 'GitHub Models', 'deepseek': 'DeepSeek' }; const apiKeyLinks = { 'gemini': 'https://aistudio.google.com/app/apikey', 'qwen': 'https://dashscope.console.aliyun.com/apiKey', 'github': 'https://github.com/settings/tokens', 'deepseek': 'https://platform.deepseek.com/api_keys' }; const currentKey = await GM.getValue(`${provider}ApiKey`, ''); const maskedKey = currentKey ? `${currentKey.substring(0, 8)}...${currentKey.substring(currentKey.length - 4)}` : '未配置'; const apiKey = prompt( `配置 ${providerNames[provider]} API Key\n\n` + `当前: ${maskedKey}\n\n` + `获取地址: ${apiKeyLinks[provider]}\n\n` + `留空则使用默认总结格式`, '' ); if (apiKey !== null) { await GM.setValue(`${provider}ApiKey`, apiKey.trim()); alert(apiKey.trim() ? `${providerNames[provider]} API Key 已保存!` : `API Key 已清空,将使用默认总结` ); } }); GM.registerMenuCommand('设置总结语言', async () => { const currentLang = await GM.getValue('summaryLanguage', 'zh-CN'); const language = prompt('请输入总结语言(例如:zh-CN 中文,en-US 英文,ja-JP 日文):', currentLang); if (language !== null && language.trim()) { await GM.setValue('summaryLanguage', language.trim()); alert(`总结语言已设置为:${language.trim()}`); } }); // 选择模型(根据当前提供商) GM.registerMenuCommand('选择模型版本', async () => { const provider = await GM.getValue('aiProvider', 'gemini'); const modelOptions = { 'gemini': ['gemini-2.0-flash-exp', 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-2.0-flash-thinking-exp-1219'], 'qwen': ['qwen-plus', 'qwen-turbo', 'qwen-max'], 'github': ['gpt-4o', 'gpt-4o-mini', 'Phi-3.5-mini-instruct', 'Llama-3.2-90B-Vision-Instruct'], 'deepseek': ['deepseek-chat', 'deepseek-reasoner'] }; const providerNames = { 'gemini': 'Gemini', 'qwen': 'Qwen', 'github': 'GitHub Models', 'deepseek': 'DeepSeek' }; const models = modelOptions[provider] || []; const currentModel = await GM.getValue(`${provider}Model`, models[0]); const modelList = models.map((m, i) => `${i + 1}. ${m}${m === currentModel ? ' (当前)' : ''}`).join('\n'); const choice = prompt( `选择 ${providerNames[provider]} 模型(输入序号或自定义模型名):\n\n${modelList}\n\n当前: ${currentModel}` ); if (choice !== null && choice.trim()) { const choiceNum = parseInt(choice.trim()); let selectedModel; if (choiceNum >= 1 && choiceNum <= models.length) { selectedModel = models[choiceNum - 1]; } else { selectedModel = choice.trim(); } await GM.setValue(`${provider}Model`, selectedModel); alert(`已设置 ${providerNames[provider]} 模型为: ${selectedModel}`); console.log('已更改模型为:', selectedModel); } }); GM.registerMenuCommand('设置通知持续时间', async () => { const currentTimeout = await GM.getValue('notificationTimeout', 10000); const currentSeconds = currentTimeout / 1000; const input = prompt( `设置通知显示时长(秒):\n\n` + `当前: ${currentSeconds} 秒\n\n` + `建议值:\n` + `• 5-10 秒 - 快速浏览\n` + `• 15-30 秒 - 仔细阅读\n` + `• 0 - 不自动关闭(需手动关闭)\n\n` + `注意:某些浏览器可能会限制最大时长`, currentSeconds ); if (input !== null) { const seconds = parseFloat(input.trim()); if (!isNaN(seconds) && seconds >= 0) { const timeout = seconds === 0 ? 0 : seconds * 1000; await GM.setValue('notificationTimeout', timeout); const displayText = seconds === 0 ? '不自动关闭' : `${seconds} 秒`; alert(`通知持续时间已设置为: ${displayText}`); } else { alert('请输入有效的数字(0 或正数)'); } } }); })();