// ==UserScript== // @name Nuist南信大公告更新检测 // @namespace https://bulletin.nuist.edu.cn/ // @version 3.0 // @description 每日检查 NUIST 公告页的变更,并返回前 10 个非置顶标题,并手动执行和通知变更 // @author QianYu // @crontab * 1-23 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 // ==/UserScript== (function() { 'use strict'; async function generateAISummary(newsTitles, modelOverride = null) { const apiKey = await GM.getValue('geminiApiKey', ''); const summaryLanguage = await GM.getValue('summaryLanguage', 'zh-CN'); const defaultModel = await GM.getValue('geminiModel', 'gemini-2.5-flash'); const model = modelOverride || defaultModel; console.log('API Key 状态:', apiKey ? '已配置' : '未配置'); console.log('使用模型:', model); if (!apiKey) { const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); return `今日更新 ${newsTitles.length} 条公告\n${numberedList}`; } try { const prompt = summaryLanguage === 'zh-CN' ? `请用一句话总结以下${newsTitles.length}条南京信息工程大学公告的主要内容:\n${newsTitles.join('\n')}` : `Please summarize the following ${newsTitles.length} NUIST announcements in one sentence (in ${summaryLanguage}):\n${newsTitles.join('\n')}`; return new Promise((resolve, reject) => { 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); console.log('Gemini API 响应内容:', response.responseText); try { const result = JSON.parse(response.responseText); if (result.candidates && result.candidates[0]?.content?.parts[0]?.text) { const summary = result.candidates[0].content.parts[0].text.trim(); resolve(summary); } else { console.error('Gemini 返回格式异常:', result); const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } } catch (error) { console.error('解析 Gemini 响应失败:', error, response.responseText); const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); } }, onerror: function(error) { console.error('调用 Gemini API 失败:', error); const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); resolve(`今日更新 ${newsTitles.length} 条公告\n${numberedList}`); }, timeout: 15000 }); }); } catch (error) { console.error('AI 总结生成失败:', error); const numberedList = newsTitles.map((title, index) => `${index + 1}. ${title}`).join('\n'); return `今日更新 ${newsTitles.length} 条公告\n${numberedList}`; } } 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'); 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) { const summary = await generateAISummary(newsTitles); GM_notification({ title: changeDetected ? 'NUIST Bulletin Update <有变化>' : 'NUIST Bulletin Update', text: summary, timeout: 0, 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); GM.registerMenuCommand('配置 Gemini API Key', async () => { const apiKey = prompt('请输入您的 Gemini API Key(留空则使用默认总结):', await GM.getValue('geminiApiKey', '')); if (apiKey !== null) { await GM.setValue('geminiApiKey', apiKey.trim()); alert(apiKey.trim() ? '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('选择 Gemini 模型', async () => { const models = [ 'gemini-2.5-flash', 'gemini-2.5-pro', ]; const currentModel = await GM.getValue('geminiModel', 'gemini-2.0-flash-exp'); const modelList = models.map((m, i) => `${i + 1}. ${m}${m === currentModel ? ' (当前)' : ''}`).join('\n'); const choice = prompt(`选择 Gemini 模型(输入序号或自定义模型名):\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('geminiModel', selectedModel); alert(`已设置模型为: ${selectedModel}`); console.log('已更改模型为:', selectedModel); } }); })();