// ==UserScript== // @name 公文自动办理(一体化整合版)- 新增文件下载功能 // @namespace http://tampermonkey.net/ // @version 3.8 // @description 一体化脚本:1. 匹配dzgw域名执行公文自动分办/会办;2. 匹配znwg域名执行ACCESS_TOKEN每分钟同步并显示同步窗口 3.修改责任人员表决方式 4.增加会办文件下载功能 // @author Your Name // @match http://dzgw.zhbg.kjt.gx.gov/* // @match http://znwg.zhbgzt.gx.gov/gxbgtwgzs_zgt/aiQA/index // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addValueChangeListener // @grant GM_addStyle // @connect localhost // @connect znwg.zhbgzt.gx.gov // @connect sxqp.zhbgzt.gx.gov // @connect *.zhbgzt.gx.gov // @run-at context-menu // ==/UserScript== (function() { 'use strict'; const currentUrl = window.location.href; // ============================================== // 分支1:匹配公文办理域名(dzgw.zhbg.kjt.gx.gov),执行公文自动分办/会办逻辑 // ============================================== if (currentUrl.startsWith('http://dzgw.zhbg.kjt.gx.gov/')) { // 全局停止标志 window.stopAutoProcess = false; let dialogTip = null; /** * 获取格式化的当前时间 [YYYY-MM-DD HH:mm:ss] * @returns {string} 格式化的时间字符串 */ function getFormattedTime() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `[${year}-${month}-${day} ${hours}:${minutes}:${seconds}]`; } /** * 等待函数(返回Promise) * @param {number} ms - 等待毫秒数 * @returns {Promise} */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 通用POST请求函数(强化错误处理+超时+日志,统一复用) * @param {string} url - 请求地址 * @param {object} params - 请求参数对象 * @param {boolean} isCmdParams - 是否需要拼接cmdParams(默认否) * @param {array} cmdParams - 命令参数数组(默认空) * @returns {Promise} 响应数据 */ async function postRequest(url, params, isCmdParams = false, cmdParams = []) { console.log(`${getFormattedTime()} 发起POST请求:${url},参数:`, params); return new Promise((resolve, reject) => { let sendData = 'params=' + JSON.stringify(params) + '&controlself=true'; // 如需拼接cmdParams(会办/分办专用) if (isCmdParams) { sendData = `params=${encodeURIComponent(JSON.stringify(params))}&cmdParams=${encodeURIComponent(JSON.stringify(cmdParams))}`; } GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json, text/javascript, */*; q=0.01' }, data: sendData, timeout: 30000, // 30秒超时 onload: function(response) { try { if (response.status < 200 || response.status >= 300) { reject(`请求失败,状态码:${response.status},响应:${response.responseText.substring(0, 500)}`); return; } const res = JSON.parse(response.responseText); // 业务状态码校验 if (res.status && res.status.code !== "1") { reject(`业务处理失败:${res.status.text || '未知错误'}`); return; } resolve(res); } catch (e) { reject(`解析响应失败:${e.message},原始响应:${response.responseText.substring(0, 500)}`); } }, onerror: function(err) { const errMsg = `网络请求失败:${err.statusText || '未知网络错误'},URL:${url}`; console.error(`${getFormattedTime()} ${errMsg}`); reject(errMsg); }, ontimeout: function() { const errMsg = `请求超时(30秒),URL:${url}`; console.error(`${getFormattedTime()} ${errMsg}`); reject(errMsg); } }); }); } /** * 提取纯文本(去除HTML标签和特殊字符,办文详情专用) * @param {string} htmlStr - HTML字符串 * @returns {string} 纯文本 */ function extractTextByRegex(htmlStr) { if (!htmlStr) return ''; let text = htmlStr.replace(/<[^>]+>/g, ''); text = text.replace(/ /g, ' ') .replace(/</g, '<') .replace(/>/g, '>') .replace(/&/g, '&'); return text.replace(/\s+/g, ' ').trim(); } /** * 保留字符串中的数字部分(用于提取文号) * @param {string} str - 原始字符串 * @returns {string} 仅包含数字的字符串 */ function keepOnlyNumbers(str) { if (!str) return ''; return str.replace(/\D/g, ''); } /** * 获取文件树数据 * @param {Object} dataItem - 会办文件项 * @param {Object} data0 - 第一步获取的数据 * @returns {Promise} 文件树数据 */ async function getFileTreeData(dataItem, data0, wdtypeData) { // 从源对象中提取需要的参数(替换?.为&&) let requestParams =null; let url = ""; if (dataItem.filetype === "会签") { requestParams = { guid: (dataItem && dataItem.pviguid) || '', wdguid: (data0 && data0.custom.wdguid) || '', outboxguid: (dataItem && dataItem.outboxguid) || '', hqdocguid: (dataItem && dataItem.pviguid) || '', hqfenbandetailguid: (dataItem && dataItem.rowguid) || '' }; url = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/handleframeaction/getFileTree?isCommondto=true&action2rest=true'; } else { requestParams = { inboxGuid: dataItem.inboxguid, outboxGuid: dataItem.outboxguid, rowGuid: dataItem.rowguid, fileType: dataItem.filetype }; url = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/handleframehbaction/getFileTree?isCommondto=true&action2rest=true'; } try { const response = await postRequest( url, requestParams, false ); return response; } catch (error) { console.error('获取文件树数据出错:', error); throw error; } } /** * 提取所有下载任务(不再直接下载,而是返回任务列表) * @param {Object} sourceData - 源数据 * @returns {Array<{url: string, name: string}>} 下载任务列表 */ function extractAllUrls(sourceData) { const downloadTasks = []; // 移除可选链?.,替换为ES5兼容的&&判断 if (!sourceData || !sourceData.fileList || !Array.isArray(sourceData.fileList)) { console.warn('文件列表为空或格式错误'); return downloadTasks; } sourceData.fileList.forEach(item => { if (Array.isArray(item.files)) { item.files.forEach(file => { // 校验文件url有效性(移除?.) if (!file || typeof file.url !== 'string' || file.url.trim() === '') return; // 构建下载链接(保留原有逻辑,仅用于提取attachGuid)和文件名 const fullUrl = file.url; const fileName = `${keepOnlyNumbers(sourceData.banwennumber || sourceData.shouwenfilenumber)}_${file.title}`; // 添加到下载任务队列 downloadTasks.push({ url: fullUrl, name: fileName }); }); } }); return downloadTasks; } /** * 异步下载文件(使用GM_xmlhttpRequest绕过CORS限制) * @param {string} fullUrl 包含attachGuid的链接(用于提取fileId) * @param {string} fileName 自定义文件名 * @returns {Promise} */ async function downloadFileByFetch(fullUrl, fileName) { return new Promise((resolve, reject) => { try { // 1. 从fullUrl中提取attachGuid作为fileId(核心:和参考代码的guid对应) const reg = /attachGuid=([^&]+)/; const match = fullUrl.match(reg); const fileId = match ? match[1] : ''; if (!fileId) { throw new Error(`未从链接中提取到有效的fileId:${fullUrl}`); } // 2. 使用GM_xmlhttpRequest代替原生XMLHttpRequest,绕过CORS限制 GM_xmlhttpRequest({ method: 'POST', url: 'http://sxqp.zhbgzt.gx.gov/api-management/busi/fileService/downloadFile', headers: { "sxqp-header-user-info": "%7B%22operId%22%3A%22ui996q7o520ugvvtrgb48u%22%2C+%22operName%22%3A%22%E8%A9%B9%E5%BB%BA%22%2C+%22deptId%22%3A%225td76vu09xke4494x0rx82%22%2C+%22deptName%22%3A%22%E6%88%90%E6%9E%9C%E8%BD%AC%E5%8C%96%E4%B8%8E%E5%8C%BA%E5%9F%9F%E5%88%9B%E6%96%B0%E5%A4%84%22%2C+%22tenantId%22%3A%22%22%7D", "Content-type": "application/json;charset=utf-8", "Cookie": "BGZHTBGZUULSESSIONZHONGTAI=feb34a41-3d8a-4afa-8a16-0a5ef8e6e300" }, data: JSON.stringify({ fileId: fileId }), responseType: 'blob', timeout: 60000, // 60秒超时 onload: function(response) { if (response.status === 200) { // 从响应头提取文件名 let respFileName = ''; const disposition = response.responseHeaders.match(/content-disposition:\s*(.*)/i); if (disposition && disposition[1]) { const fileNameMatch = disposition[1].match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/); if (fileNameMatch && fileNameMatch[1]) { respFileName = fileNameMatch[1].replace(/['"]/g, ''); respFileName = decodeURIComponent(respFileName); } } // 优先使用自定义文件名,无则用响应头的,最后兜底 const finalFileName = fileName || respFileName || `文件_${fileId}`; // 创建a标签下载Blob const a = document.createElement('a'); a.download = finalFileName; // 处理Blob响应 let blob; if (response.response instanceof Blob) { blob = response.response; } else { // 如果response.response不是Blob,尝试创建 blob = new Blob([response.response], { type: response.responseHeaders.match(/content-type:\s*(.*)/i)?.[1] || 'application/octet-stream' }); } a.href = URL.createObjectURL(blob); a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(a.href); console.log(`✅ 触发下载:${finalFileName},fileId:${fileId}`); resolve(); } else { const errorMsg = `下载失败,状态码:${response.status}(${response.statusText})`; console.error(`❌ 下载${fileName}出错:`, errorMsg); alert(`文件${fileName}下载失败:${errorMsg}`); reject(new Error(errorMsg)); } }, onerror: function(err) { const errorMsg = `网络请求错误:${err.statusText || '未知网络错误'}`; console.error(`❌ 下载${fileName}出错:`, errorMsg); alert(`文件${fileName}下载失败:${errorMsg}`); reject(new Error(errorMsg)); }, ontimeout: function() { const errorMsg = '请求超时(60秒)'; console.error(`❌ 下载${fileName}出错:`, errorMsg); alert(`文件${fileName}下载失败:${errorMsg}`); reject(new Error(errorMsg)); } }); } catch (error) { console.error(`❌ 下载${fileName}出错:`, error); alert(`文件${fileName}下载失败,请稍后重试`); reject(error); } }); } /** * 处理下载队列(逐个执行,间隔5秒) * @param {Array<{url: string, name: string}>} tasks 下载任务列表 */ async function processDownloadQueue(tasks) { console.log(`开始处理下载队列,共 ${tasks.length} 个文件`); for (const task of tasks) { try { await downloadFileByFetch(task.url, task.name); // 每个文件下载后间隔5秒(改为异步延迟,不阻塞主线程) await new Promise(resolve => setTimeout(resolve, 5000)); } catch (error) { // 单个文件失败不中断队列,仍等待5秒继续下一个 console.error(`下载任务失败:${task.name}`, error); await new Promise(resolve => setTimeout(resolve, 5000)); } } console.log('🎉 所有下载任务执行完成'); } /** * 调用系统自带Stream接口获取大模型响应(核心修改函数:补充完整请求头,优化结果解析) * @param {string} prompt - 提示词 * @param {number} timeout - 超时时间(毫秒) * @returns {Promise} 大模型返回的目标JSON结果(包含tips和responsiblePerson字段) */ function callSystemStreamModel(prompt, timeout) { // 系统模型接口地址 const apiUrl = 'http://znwg.zhbgzt.gx.gov/gxbgtwgzs_zgt/prod-api/chat/stream'; // 请求载荷 const requestData = { "prompt": prompt, "wengao": "", "model": "dify_wenda", "fileIds": [], "zhishiku": "", "sessionId": "" }; return new Promise((resolve, reject) => { let fullAnswer = ''; // 拼接流式返回的完整answer内容 let lastProcessedIndex = 0; // 记录上一次处理到的响应位置,避免重复解析 let test = GM_getValue("ACCESS_TOKEN", ""); GM_xmlhttpRequest({ method: 'POST', url: apiUrl, // 关键修改:替换为监听得到的全局ACCESS_TOKEN变量 headers: { 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6', 'Authorization': 'Bearer ' + GM_getValue("ACCESS_TOKEN", ""), // 已替换为监听获取的变量 'Connection': 'keep-alive', 'Content-Type': 'application/json', 'Host': 'znwg.zhbgzt.gx.gov', 'Referer': 'http://znwg.zhbgzt.gx.gov/gxbgtwgzs_zgt/aiQA/index', 'User-Agent': 'Mozilla/5.0 (X11; Linux aarch64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.251 Safari/537.36', 'accept': 'text/event-stream' }, data: JSON.stringify(requestData), // 序列化请求载荷 timeout: timeout, // 超时时间 responseType: 'text', // 文本类型响应 // 流式响应处理:仅拼接answer内容,不做业务解析(按要求不在这里处理结果) onprogress: function(response) { try { // 原逻辑保留(此处用户脚本中应已存在,保持原样) } catch (e) { console.warn(`${getFormattedTime()} 流式响应拼接临时异常:${e.message}`); // 临时解析错误不终止,继续监听后续数据 } }, // 请求完成(所有流式数据接收完毕):在此处统一处理结果解析(核心修改部分) onload: function(response) { try { // 1. 先判断请求状态是否正常 if (response.status < 200 || response.status >= 300) { reject(`模型请求失败,状态码:${response.status},响应:${fullAnswer.substring(0, 500)}`); return; } if (!response.responseText) return; // 按行分割,过滤并解析EventStream数据 const lines = response.responseText.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { // 过滤非data开头的行 if (!line.startsWith('data:')) continue; // 去除"data: "前缀,解析JSON const jsonStr = line.substring(5).trim(); const dataObj = JSON.parse(jsonStr); // 仅拼接answer字段到完整响应,不做其他业务处理 if (dataObj.answer !== undefined && dataObj.answer !== null) { fullAnswer += dataObj.answer; } } // 2. 从完整拼接的响应中提取Markdown JSON代码块内容 // 正则匹配 ```json 开头和 ``` 结尾的内容,捕获中间的JSON字符串 const jsonCodeBlockRegex = /```json\s*([\s\S]*?)\s*```/; const matchResult = fullAnswer.match(jsonCodeBlockRegex); // 3. 校验是否匹配到JSON内容 if (!matchResult || !matchResult[1]) { reject("未在大模型响应中提取到有效的JSON格式内容"); return; } // 4. 提取并清理JSON字符串(去除首尾空白字符) const targetJsonStr = matchResult[1].trim(); // 5. 解析JSON字符串为目标对象 const finalResult = JSON.parse(targetJsonStr); // 6. 验证目标字段是否存在(可选,增强鲁棒性) if (!finalResult.hasOwnProperty('tips') || !finalResult.hasOwnProperty('responsiblePerson')) { reject("解析后的JSON缺少必填字段(tips/responsiblePerson)"); return; } // 7. 返回最终目标JSON对象 resolve(finalResult); } catch (e) { // 捕获所有解析异常并返回详细信息 reject(`解析大模型完整响应失败:${e.message},原始响应:${fullAnswer.substring(0, 500)}`); } }, // 网络错误 onerror: function(err) { const errMsg = `调用系统模型网络失败:${err.statusText || '未知网络错误'},已拼接响应:${fullAnswer.substring(0, 500)}`; console.error(`${getFormattedTime()} ${errMsg}`); reject(errMsg); }, // 超时错误 ontimeout: function() { const errMsg = `调用系统模型超时(${timeout/1000}秒),已拼接响应:${fullAnswer.substring(0, 500)}`; console.error(`${getFormattedTime()} ${errMsg}`); reject(errMsg); } }); }); } // ==================== 公共SMS发送流程(复用) ==================== /** * 第一步:获取公文列表数据(SMS发送专用) * @returns {Promise} 公文列表数据 */ async function getDocumentList() { const url = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/zaibanthisyearlistaction/getDataGridData?type=1&isCommondto=true&action2rest=true'; const params = { "daterange": "", "banwentype": "", "number": "", "title": "", "urgencydegree": "", "secretlevel": "", "processingstatus": "", "filetype": "", "categoryvalue": "", "pageIndex": 0, "pageSize": 10, "fromdatestr": "", "todatestr": "", "tabname": "todoarchivelist", "currentType": "来文时间", "laiwennumber": "", "laiwenouname": "" }; try { const result = await postRequest(url, params); if (!result.custom || !result.custom.data || result.custom.data.length === 0) { throw new Error("未获取到公文数据"); } console.log("第一步:成功获取公文列表数据", result); return result; } catch (error) { console.error("第一步:获取公文列表失败", error); throw error; } } /** * 第二步:发送SMS消息 * @param {object} docData - 单个公文数据对象 * @param {string} smsContent - 短信内容 * @returns {Promise} 发送结果 */ async function sendSmsMessage(docData, smsContent) { const url = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/handleframeaction/sendNextStep?isCommondto=true&action2rest=true'; const params = { "wdguid": docData.wdrowguid, "handlestate": docData.handlestate || "4", "buttonname": "1", "wdtype": docData.wdtype || "24", "user": docData.userguid, "type": 4, "smsContent": smsContent, "isSendSms": "1", "guid": docData.rowguid, "handleguid": docData.handleguid, "msg": "发送完成", "selecttab": "" }; try { const result = await postRequest(url, params); console.log("第二步:成功发送SMS消息", result); return result; } catch (error) { console.error("第二步:发送SMS消息失败", error); throw error; } } /** * 第四步:保存审核意见 * @param {object} docData - 单个公文数据对象 * @returns {Promise} 保存结果 */ async function saveAuditOpinion(docData) { const url = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/zzfileviewaction/saveShenheOpinion?isCommondto=true&action2rest=true'; const params = { "wdguid": docData.wdrowguid, "handleguid": docData.handleguid, "opinion": "退回重办", "huiqiantype": "退回重办", "wdtype": docData.wdtype || "24", "tag": "hg" }; try { const result = await postRequest(url, params); console.log("第四步:成功保存审核意见", result); return result; } catch (error) { console.error("第四步:保存审核意见失败", error); throw error; } } /** * 主函数:执行完整的短信发送流程 * @param {string} smsContent - 要发送的短信内容 * @returns {Promise} 最终执行结果 */ async function executeSmsSendProcess(smsContent) { try { console.log(`开始执行短信发送流程,内容:${smsContent.substring(0, 100)}`); const docListResult = await getDocumentList(); const firstDoc = docListResult?.custom?.data?.find(item => { return item.title && item.title.trim() === "测试"; }); if (!firstDoc) { const msg = "未找到标题为「测试」的公文,短信发送跳过"; console.log(msg); console.warn(msg); return { success: false, message: msg }; } await sendSmsMessage(firstDoc, smsContent); await getDocumentList(); await saveAuditOpinion(firstDoc); const successMsg = "✅ 短信发送流程执行成功"; console.log(successMsg); return { success: true, message: successMsg, docData: firstDoc }; } catch (error) { const errMsg = `❌ 短信发送流程执行失败:${error.message}`; console.log(errMsg); console.error(errMsg, error); return { success: false, message: errMsg, docData: null }; } } // ==================== 第一部分:办文自动分办逻辑 ==================== /** * 获取处室人员列表(办文分办专用) * @returns {Promise} 人员列表 */ async function getFBUserList() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceoutboxsendlistaction/getFBUserList?isCommondto=true&action2rest=true', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, timeout: 15000, onload: function(response) { try { const res = JSON.parse(response.responseText); const userList = res.custom?.data || []; resolve(userList); } catch (e) { const errMsg = `读取人员信息失败:${e.message}`; console.log(errMsg); console.error(errMsg); resolve([]); } }, onerror: function(err) { const errMsg = `请求人员信息接口失败:${err.statusText || '未知错误'}`; console.log(errMsg); console.error(errMsg); resolve([]); }, ontimeout: function() { const errMsg = '请求人员信息接口超时(15秒)'; console.log(errMsg); console.error(errMsg); resolve([]); } }); }); } /** * 获取办文列表(办文分办专用) * @returns {Promise} 过滤后的办文列表 */ async function getInboxList() { const params = { "title": "", "bianhao": "", "dispatchdepartment": "", "lwdate": "", "urgencydegree": "", "laiwennumber": "", "processingstatus": "", "senddatefrom": "", "senddateto": "", "pageIndex": 0, "pageSize": 10, "tabtype": "0" }; return new Promise((resolve) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceinboxdaichuliaction/getDataGridData?type=1&isCommondto=true&action2rest=true&controlself=true', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, data: 'params=' + JSON.stringify(params), timeout: 15000, onload: function(response) { try { const res = JSON.parse(response.responseText); const inboxList = res.custom?.data || []; resolve(inboxList); } catch (e) { const errMsg = `解析办文列表失败:${e.message}`; console.log(errMsg); console.error(errMsg); resolve([]); } }, onerror: function(err) { const errMsg = `请求办文列表接口失败:${err.statusText || '未知错误'}`; console.log(errMsg); console.error(errMsg); resolve([]); }, ontimeout: function() { const errMsg = '请求办文列表接口超时(15秒)'; console.log(errMsg); console.error(errMsg); resolve([]); } }); }); } /** * 获取办文docguid(办文分办专用) * @param {string} inboxguid - 办文收件箱GUID * @returns {Promise} docguid */ async function getDocGuid(inboxguid) { const params = { "guid": inboxguid }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/gxharceinboxdetailaction/getDataBean?isCommondto=true&action2rest=true', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, data: 'params=' + JSON.stringify(params), timeout: 30000, onload: function(response) { try { const res = JSON.parse(response.responseText); const docguid = res.custom?.arceInbox?.docguid; if (!docguid) { reject('未获取到docguid'); return; } resolve(docguid); } catch (e) { reject('解析docguid失败:' + e.message); } }, onerror: function(err) { reject('请求docguid接口失败:' + err.statusText); }, ontimeout: function() { reject('请求docguid接口超时(30秒)'); } }); }); } /** * 获取办文详情(办文分办专用) * @param {string} docguid - 办文GUID * @returns {Promise} 办文详情 */ async function getFileDetail(docguid) { const params = { "guid": docguid, "archivefiletypeguid": "" }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/wd25form2action/getDataBean?isCommondto=true&action2rest=true', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, data: 'params=' + JSON.stringify(params), timeout: 15000, onload: function(response) { try { const res = JSON.parse(response.responseText); const detail = { title: res.custom?.record?.title || '', ldps: res.custom?.ldps || '', niban: res.custom?.niban || '', hg: res.custom?.hg || '', wjzy: res.custom?.wjzy || '' }; resolve(detail); } catch (e) { reject('解析办文详情失败:' + e.message); } }, onerror: function(err) { reject('请求办文详情接口失败:' + err.statusText); }, ontimeout: function() { reject('请求办文详情接口超时(15秒)'); } }); }); } /** * 获取办文负责同志及办理提示(办文分办专用)- 修改为:3次生成+少数服从多数择优 * @param {object} fileDetail - 办文详情 * @returns {Promise} 负责同志及办理提示 */ async function getResponsiblePersonAndTips(fileDetail) { // 1. 构造原始生成prompt(与原逻辑一致) const basePrompt = ` 主:${fileDetail.title} 作为广西科技厅处长,根据来文信息确定处室内负责办理的同志并生成办理提示: 1. 来文标题:${fileDetail.title} 2. 厅领导对来文办理的批示:${extractTextByRegex(fileDetail.ldps)} 3. 办公室为来文办理的拟办意见:${extractTextByRegex(fileDetail.niban)} 4. 办公室主任审核拟办的意见:${extractTextByRegex(fileDetail.hg)} 5. 来文内容摘要:${extractTextByRegex(fileDetail.wjzy)} 6. 处室内同志分工: - 张斌:负责高新区、产业基地、科技成果转化(不含中试、概念验证平台)、科技成果转化政策、科技成果转化三年行动、科创广西试验区等工作; - 钟家安:负责服务业、科技企业孵化体系、大学科技园、技术转移体系、技术市场、技术合同登记、技术交易奖励、科技服务业、科技类社会组织,社会科技奖理工作等; - 陈美芝:负责科技金融、科创贷、科创担、科技保险、桂惠通、金融投资、企业创新积分制、项目立项管理、经费预算、火炬统计、保密工作等; - 唐欢:负责科技奖励、科技成果登记、科技成果转化平台(包括中试平台、中试基地、概念验证中心等)、科技成果评价、知识产权创造(包括对接市局监管局)、创新飞地、项目过程管理(验收、科研诚信)、绩效考核、科技宣传、内务工作 7.结果输出要求: - 输出格式为JSON:{"responsiblePerson":"姓名/无法确定","tips":"办理提示内容"} - responsiblePerson只能是上述四名同志之一。根据分工选择最合适的同志,无法确定则返回"无法确定"; - 办理提示要包含厅领导批示(没有则不包含)和办理期限(没有则不包含),可为空; `.trim(); try { // 2. 循环调用3次大模型,收集3个有效结果 const opinionList = []; const requiredCount = 3; console.log(`${getFormattedTime()} 开始调用${requiredCount}次大模型生成办文意见...`); while (opinionList.length < requiredCount) { if (window.stopAutoProcess) throw new Error("用户终止了自动办理流程,停止收集意见"); try { const singleResult = await callSystemStreamModel(basePrompt, 120000); // 验证结果有效性(字段完整、格式合规) if (singleResult && singleResult.hasOwnProperty('responsiblePerson') && singleResult.hasOwnProperty('tips')) { opinionList.push(singleResult); console.log(`${getFormattedTime()} 已收集${opinionList.length}/${requiredCount}个办文意见`); } else { console.warn(`${getFormattedTime()} 收集到无效意见,跳过并重试`); } } catch (e) { console.warn(`${getFormattedTime()} 单次模型调用失败,重试:${e.message}`); await sleep(1000); // 重试前短暂等待,避免频繁请求 } } // 3. 构造比较择优prompt,要求按少数服从多数选择最优结果 const comparePrompt = ` 现有${requiredCount}个办文办理意见结果,需要你按照「少数服从多数」原则选择最优结果: 1. 统计「responsiblePerson」字段的出现频次,选择频次超过半数的同志(若无法确定则返回"无法确定"); 2. 统计「tips」字段的核心内容(办理要求、期限、批示要点)出现频次,整合最优、最符合多数意见的办理提示; 3. 最优结果需严格遵循原有输出格式,不得新增字段、改变数据类型; 4. 若所有结果完全一致,直接返回该结果即可。 以下是${requiredCount}个办文意见: ${opinionList.map((item, index) => `意见${index+1}:${JSON.stringify(item)}`).join('\n')} 输出要求: 仅返回JSON格式,要用markdown的代码块形式,无需额外说明,格式如下: {"responsiblePerson":"姓名/无法确定","tips":"办理提示内容"} `.trim(); // 4. 调用大模型进行比较,获取最优结果 console.log(`${getFormattedTime()} 调用大模型对${requiredCount}个意见进行比较择优...`); const optimalResult = await callSystemStreamModel(comparePrompt, 120000); console.log(`${getFormattedTime()} 办文意见择优完成,最优结果:`, optimalResult); // 5. 返回最优结果 return optimalResult; } catch (e) { console.error(`${getFormattedTime()} 生成并择优办文意见失败:`, e); throw new Error(`获取负责同志及提示失败:${e.message}`); } } /** * 办文分办操作(办文分办专用) * @param {object} fileItem - 办文项 * @param {string} username - 负责同志姓名 * @param {string} tips - 办理提示 * @returns {Promise} 分办结果 */ async function fenbanFile(fileItem, username, tips) { const userList = await getFBUserList(); const user = userList.find(item => item.text === username); if (!user) { throw new Error(`未找到${username}的人员ID`); } const userId = user.id; const { inboxguid, rowguid, filetype } = fileItem; const params = { "inboxguid": inboxguid, "users": userId, "filetype": filetype, "fenbanreason": tips + "。" }; const cmdParams = [inboxguid, rowguid, userId, filetype, tips]; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceinboxdaichuliaction/doFenban?isCommondto=true&action2rest=true', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, data: `params=${JSON.stringify(params)}&cmdParams=${JSON.stringify(cmdParams)}`, timeout: 15000, onload: function(response) { try { const res = JSON.parse(response.responseText); if (res && res.custom && res.custom.msg === "分办成功!") { resolve(true); } else { const errorMsg = res?.custom?.msg || "分办接口返回未知结果"; reject(`分办失败:${errorMsg}`); } } catch (e) { reject('解析分办结果失败:' + e.message); } }, onerror: function(err) { reject('分办请求失败:' + err.statusText); }, ontimeout: function() { reject('分办请求超时(15秒)'); } }); }); } /** * 单个办文处理(办文分办专用) * @param {object} fileItem - 办文项 */ async function processSingleBanWenFile(fileItem) { const { inboxguid, title } = fileItem; try { dialogTip.updateTip(`\n=== 处理办文:${title} ===`); console.log("步骤1/6:获取docguid..."); const docguid = await getDocGuid(inboxguid); console.log("步骤2/6:获取办文详情..."); const fileDetail = await getFileDetail(docguid); console.log("步骤3/6:生成并择优办理提示..."); const result = await getResponsiblePersonAndTips(fileDetail); const { responsiblePerson, tips } = result; dialogTip.updateTip(`负责同志:${responsiblePerson}`); dialogTip.updateTip(`办理提示:${tips || '无'}`); console.log("等待30秒后执行分办..."); await sleep(30000); if (window.stopAutoProcess) return; if (responsiblePerson !== '无法确定') { await fenbanFile(fileItem, responsiblePerson, tips); dialogTip.updateTip(`办文《${title}》分办成功!`); await executeSmsSendProcess(`豆包报告:《${title}》分办成功!负责同志:${responsiblePerson}`); await sleep(3000); await executeSmsSendProcess(`豆包报告:《${title}》分办成功!负责同志:${responsiblePerson}`); } else { dialogTip.updateTip(`办文《${title}》无法确定负责同志,跳过分办`); await executeSmsSendProcess(`豆包报告:《${title}》无法确定负责同志,跳过分办`); } } catch (error) { dialogTip.updateTip(`办文《${title}》处理失败:${error.message}`); console.error(`处理办文${title}失败:`, error); } } /** * 办文分办核心流程 */ async function processBanWenFiles() { dialogTip.updateTip("\n==================== 开始处理办文分办 ===================="); const userList = await getFBUserList(); if (userList.length === 0) { console.log("未读取到处室人员信息,办文分办功能受限"); return; } const inboxList = await getInboxList(); if (inboxList.length === 0) { console.log("当前无待处理办文,跳过办文分办"); return; } for (const fileItem of inboxList) { if (window.stopAutoProcess) break; await processSingleBanWenFile(fileItem); } console.log("==================== 办文分办处理完成 ===================="); } // ==================== 第二部分:会办自动分办逻辑 ==================== /** * 初始化主办意见缓存(会办分办专用) */ function initZhubanCache() { if (!localStorage.getItem("zhuban")) { localStorage.setItem("zhuban", JSON.stringify({})); } } /** * 通过本地代理提取所有文件的摘要 * @param {string} fileCode - 文件码(用于匹配文件名前缀) * @returns {Promise} 合并后的摘要 */ async function extractAllFiles(fileCode) { return new Promise((resolve, reject) => { try { const requestData = { command: 'extractAllFiles', fileCode: fileCode }; GM_xmlhttpRequest({ method: 'POST', url: 'http://localhost:8765/extractAllFiles', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(requestData), timeout: 60000, // 60秒超时 onload: function(response) { try { if (response.status === 200) { const result = JSON.parse(response.responseText); if (result.success) { console.log(`✅ 成功提取 ${result.file_count || 0} 个文件的摘要`); resolve(result.summary || ''); } else { console.warn(`⚠️ 提取文件摘要失败: ${result.message || '未知错误'}`); resolve(''); // 失败时返回空字符串,不中断流程 } } else { console.warn(`⚠️ 代理服务器返回错误状态: ${response.status}`); resolve(''); // 失败时返回空字符串,不中断流程 } } catch (e) { console.error(`❌ 解析代理响应失败: ${e.message}`); resolve(''); // 失败时返回空字符串,不中断流程 } }, onerror: function(err) { const errorMsg = `代理服务器请求失败: ${err.statusText || '网络错误'}`; console.warn(`⚠️ ${errorMsg},请确保代理服务已启动`); resolve(''); // 失败时返回空字符串,不中断流程 }, ontimeout: function() { console.warn('⚠️ 代理服务器请求超时(60秒)'); resolve(''); // 失败时返回空字符串,不中断流程 } }); } catch (error) { console.error(`❌ extractAllFiles 异常: ${error.message}`); resolve(''); // 异常时返回空字符串,不中断流程 } }); } /** * 下载主办意见数据(会办分办专用)- 新增文件下载功能 * @param {object} dataItem - 会办文件项 * @returns {Promise} 主办意见数据 */ async function downloadZhubanData(dataItem) { let zhaiyao = ""; // 第一步:获取docguid const data0 = await postRequest( 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/handleframehbaction/getDataBean?isCommondto=true&action2rest=true&myScript=ShowZhuban', { inboxGuid: dataItem.inboxguid, outboxGuid: dataItem.outboxguid, rowGuid: dataItem.rowguid, fileType: dataItem.filetype }, false ); const docguid = data0.custom.docguid; // 第二步:获取wdtype const wdtypeData = await postRequest( 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/handleframeaction/getDataBean?isCommondto=true&action2rest=true&myScript=ShowZhuban', { guid: docguid, count: "1" }, false ); const wdtype = wdtypeData.custom.wdtype; // 第三步:下载分办意见 let zhubanUrl; if (wdtype == 24) { zhubanUrl = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/wd24form1action/getDataBean?isCommondto=true&action2rest=true&myScript=ShowZhuban'; } else { zhubanUrl = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/wd25form2action/getDataBean?isCommondto=true&action2rest=true&myScript=ShowZhuban'; } let fenbanData = null; if (data0.custom.fenwenbaopibase) { fenbanData = await postRequest( zhubanUrl, { guid: data0.custom.fenwenbaopibase.rowguid, archivefiletypeguid: data0.custom.fenwenbaopibase.archivefiletypeguid}, false ); } // 第三步:下载主办意见 const zhubanData = await postRequest( zhubanUrl, { guid: docguid, archivefiletypeguid: "" }, false ); // ============ 新增:获取文件树数据并下载相关文件 ============ try { console.log("开始获取文件树数据..."); // 获取文件树数据 const treeData = await getFileTreeData(dataItem, data0, wdtypeData); // 提取下载任务(补充treeData.custom的&&判断) const downloadTasks = extractAllUrls(treeData && treeData.custom ? treeData.custom : {}); // 执行下载队列(间隔5秒) if (downloadTasks.length > 0) { console.log(`找到 ${downloadTasks.length} 个文件需要下载`); dialogTip.updateTip(`开始下载 ${downloadTasks.length} 个附件文件`); await processDownloadQueue(downloadTasks); dialogTip.updateTip(`附件文件下载完成`); // 等待1分钟,等文件下载完。 await sleep(60000); zhaiyao = await extractAllFiles(keepOnlyNumbers(treeData.custom.banwennumber || treeData.custom.shouwenfilenumber)); } else { console.log('⚠️ 该元素无下载任务'); dialogTip.updateTip('无附件文件需要下载'); } } catch (err) { console.error('下载文件时出错:', err); dialogTip.updateTip(`文件下载失败:${err.message}`); } return {fenbanData, zhubanData, zhaiyao}; } /** * 获取会办办理意见(会办分办专用)- 修改为:3次生成+少数服从多数择优 * @param {string} fileTitle - 会办文件标题 * @param {string} niban - 主办拟办意见 * @param {string} hg - 主办核稿意见 * @returns {Promise} 办理意见结果 */ async function getHandleOpinion(fileTitle, fenban,fenbanhg, wjzy, niban, hg, zhaiyao) { // 1. 构造原始生成prompt(与原逻辑一致) const basePrompt = ` 会:${fileTitle || '无'} 作为广西科技厅成果处处长,根据来文的标题、摘要、厅办公室和主办处室的意见,所有文件摘要,以及处内同志分工,研究提出文件的会办意见,会办意见以JSON格式返回。 # 来文信息 - 来文标题:${fileTitle || '无'} - 文件摘要:${wjzy || '无'} - 厅办公室拟办意见:${fenban || '无'} - 厅办公室核稿意见:${fenbanhg || '无'} - 主办处室拟办意见:${niban || '无'} - 主办处室核稿意见:${hg || '无'} # 处内同志分工 - 张斌:负责高新区、产业基地、科技成果转化、科技成果转化政策、科技成果转化三年行动、科创广西试验区等工作;牵头负责处内工作总结、工作计划、火炬统计等工作。 - 钟家安:负责科技企业孵化体系、大学科技园、技术转移体系、技术市场、科技服务业、科技类社会组织,社会科技奖管理工作。 - 陈美芝:负责科技金融,包括科创贷、科创担、科技保险、企业创新积分制等;负责牵头开展项目立项下达、指南编制、经费管理、预算管理、保密工作;负责牵头处内政府采购工作。 - 唐欢:负责科技奖励、科技成果登记、科技成果转化平台(包括中试基地、概念验证中心)、科技成果评价、知识产权创造、创新飞地、绩效考核、科技宣传、数据资产、固定资产、其他内务工作。负责牵头开展项目过程管理,包括月度项目管理情况整理、项目验收、科研诚信处理、项目经费追退工作。 # 会办意见返回要求: - 格式为JSON:{"responsiblePerson":["姓名","姓名"],"tips":"会办意见文本"} - 其中"responsiblePerson"为会办意见涉及到的所有同志的姓名数组。 - 其中"tips"为会办意见文本 - responsiblePerson和tips表述要一致;特别是tips提到"处内同志""全处同志“等表示全部人员时,responsiblePerson要包含所有人员。 # 常见会办意见示例: - 请处内同志阅。 - 请处内同志阅遵。 - 请处内同志阅办。 - 请XXX同志阅办。 - 请XXX、XXX等同志阅办。 - 请XXX同志牵头,处内同志配合阅办。 - 请XXX同志牵头,XXX、XXX同志配合阅办。 - 请处内同志阅办。XX月XX日前完成。 # 办理要求 - 第一步确定负责的同志。首先根据来文标题、摘要、厅办公室和主办处室的意见确定和成果处相关的工作内容,在确定负责办理的同志时,要按照从宽的原则,尽量避免遗漏。 - 需要安排人员参加会议时,要先研究是需要1名同志参会还是多名同志参会。如果是1人参会,只安排1名涉及工作最多的同志参会。 - 办理涉及多名同志时,可以确定一名牵头同志,也可以不确定牵头同志;牵头的人员最多只有一名,配合同志可以时一名或多名,如果是全处同志配合,可以不需要提具体同志,直接说处内同志配合(这是responsiblePerson要返回所有同志)。在处内分工中明确是牵头负责的工作,在办理意见中一般写某某同志牵头办理。 - 如果主办处室意见里有办理时间要求的,办理意见里要提出办理时间要求,并和主办意见要求一致。但主办意见里类似"逾期未反馈视为无意见"这样的表述,不要包含在办理意见里。主办处室意见中没有办理时间要求的,不要提出办理时间要求。 - 如果主办处室意见里有提供材料、反馈意见等,在办理意见里可以转述,但不要增加内容,不要根据处内分工去进行任务分解,要保持办理意见的简洁。 - 办理意见中不要说明分工的理由(非常重要) # 其他要求: - 涉及处内总结的工作,全处同志需要配合。 - 全处同志都有项目管理的职责,涉及项目管理相关办法征求意见等文件,请全处同志阅提。 - 涉重金属环境安全隐患排查整治工作第五批任务清单涉及成果处任务是设立关键金属产业基金。 - 办公室或主办处室要求各处室、各直属单位阅悉或阅办的会议纪要、住房申购、印发文件等,一般情况要让全处同志阅悉或阅办。 - ** 注意 ** 有些文件主办处室先后多次送各处室会办,因此要以主办处室最近的办理意见为准,忽略之前的办理意见。 # 所有文件摘要 ${zhaiyao || '无'} `.trim(); try { // 2. 循环调用3次大模型,收集3个有效结果 const opinionList = []; const requiredCount =3; console.log(`${getFormattedTime()} 开始调用${requiredCount}次大模型生成会办意见...`); while (opinionList.length < requiredCount) { if (window.stopAutoProcess) throw new Error("用户终止了自动办理流程,停止收集意见"); try { const singleResult = await callSystemStreamModel(basePrompt, 1800000); // 验证结果有效性(字段完整、格式合规) if (singleResult && Array.isArray(singleResult.responsiblePerson) && singleResult.hasOwnProperty('tips')) { opinionList.push(singleResult); console.log(`${getFormattedTime()} 已收集${opinionList.length}/${requiredCount}个会办意见`); } else { console.warn(`${getFormattedTime()} 收集到无效意见,跳过并重试`); } } catch (e) { console.warn(`${getFormattedTime()} 单次模型调用失败,重试:${e.message}`); await sleep(1000); // 重试前短暂等待,避免频繁请求 } } // 3. 构造比较择优prompt,要求按少数服从多数选择最优结果 const comparePrompt = ` 现有${requiredCount}个会办办理意见结果,需要你按照「少数服从多数」原则选择最优结果: 1. 若有一半以上结果的「responsiblePerson」一致(不考虑人员排序),选择「responsiblePerson」一致的结果中「tips」最详细的一个作为最终结果。 2. 否则统计「responsiblePerson」数组中每个同志的出现次数(和顺序无关),出现次数超过总数一半的作为「responsiblePerson」;如果没有人出现次数超过一半,选择所有出现的同志「responsiblePerson」。 3. 根据上一步产生的负责的同志名单,参考各个「tips」内容(办理时间、办理要求、负责范围、执行要点等),形成办理的tips;tips和responsiblePerson关于人员的描述应该一致,不能出现冲突。 4. 最后结果需严格遵循原有输出格式,responsiblePerson为数组类型,tips为字符串类型,不得修改; 以下是${requiredCount}个会办意见: ${opinionList.map((item, index) => `意见${index+1}:${JSON.stringify(item)}`).join('\n')} 输出要求: 仅返回JSON格式,要用markdown的代码块形式,无需额外说明,格式如下: {"responsiblePerson":["姓名","姓名"],"tips":"办理提示内容"} `.trim(); // 4. 调用大模型进行比较,获取最优结果 console.log(`${getFormattedTime()} 调用大模型对${requiredCount}个意见进行比较择优...`); const optimalResult = await callSystemStreamModel(comparePrompt, 1800000); console.log(`${getFormattedTime()} 会办意见择优完成,最优结果:`, optimalResult); // 5. 返回最优结果 return optimalResult; } catch (e) { console.error(`${getFormattedTime()} 生成并择优会办意见失败:`, e); // 与原逻辑一致,返回默认空结果 return { tips: '', responsiblePerson: [] }; } } /** * 设置会办办理意见(会办分办专用) * @param {object} dataItem - 会办文件项 * @param {string} tips - 办理意见文本 * @returns {Promise} 保存结果 */ async function setHandleOpinion(dataItem, pviguid, tips) { const params = { wdGuid: pviguid, docGuid: pviguid, rowGuid: dataItem.rowguid, outboxGuid: dataItem.outboxguid, fileType: dataItem.filetype, opinion: tips + "。" }; return await postRequest( 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/wd25formhbaction/saveOpinion?isCommondto=true&action2rest=true', params, false ); } /** * 获取会办处室人员(会办分办专用) * @param {string} docguid - 文件GUID * @returns {Promise} 人员列表 */ async function getDeptUsers(docguid) { const data = await postRequest( 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceoutboxsendlistaction/getFBUserList?isCommondto=true&action2rest=true', { guid: docguid }, false ); return data.custom.data || []; } /** * 会办人员分办(会办分办专用) * @param {object} dataItem - 会办文件项 * @param {array} userIds - 人员ID数组 * @returns {Promise} 分办结果 */ async function assignPersons(dataItem, userIds) { const params = {}; const cmdParams = [ dataItem.inboxguid, dataItem.rowguid, userIds, dataItem.filetype, "", "0" ]; const assignUrl = 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceinboxdaichuliaction/doChenneifenban?isCommondto=true&action2rest=true'; return await postRequest(assignUrl, params, true, cmdParams); } /** * 会办分办核心流程 */ async function processHuiBanFiles() { initZhubanCache(); console.log("\n==================== 开始处理会办分办 ===================="); // 获取会办文件列表 const huibanData = await postRequest( 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceinboxdaichuliaction/getDataGridDataHuiBan?type=1&isCommondto=true&action2rest=true&controlself=true&my=true', { "title": "", "bianhao": "", "fawendz": "", "fawenserialnumber": "", "urgencydegree": "", "senddate": "", "processingstatus": "", "senddeptname": "", "fullfiletype": "", "senddatefrom": "", "senddateto": "", "pageIndex": 0, "pageSize": 100, "tabtype": "0" }, false ); const huibanList = huibanData.custom.data || []; if (huibanList.length === 0) { console.log("当前无待处理会办文件,跳过分办"); console.log("==================== 会办分办处理完成 ===================="); return; } // 遍历处理会办文件 for (let i = 0; i < huibanList.length && !window.stopAutoProcess; i++) { const item = huibanList[i]; const fileTitle = item.title || "无标题"; const fileStatus = item.huibanfenbanstatustext || "未知状态"; // 跳过已分办文件 if (fileStatus.indexOf("未分办") === -1) { continue; } dialogTip.updateTip(`\n=== 处理第 ${i+1}/${huibanList.length} 条会办文件 ===`); dialogTip.updateTip(`文件标题:${fileTitle}`); // 下载主办意见 try { console.log("下载主办意见数据..."); const Data = await downloadZhubanData(item); const fenban = Data.fenbanData?.custom?.niban || "无"; const fenbanhg = Data.fenbanData?.custom?.hg || "无"; const niban = Data.zhubanData.custom?.niban || "无"; const hg = extractTextByRegex(Data.zhubanData.custom?.hg || "无"); const wjzy = Data.fenbanData?.custom?.wjzy || Data.zhubanData.custom?.wjzy ||"无"; dialogTip.updateTip(`文件摘要:${wjzy}`); dialogTip.updateTip(`办公室拟办意见:${fenban}`); dialogTip.updateTip(`办公室核稿意见:${fenbanhg}`); dialogTip.updateTip(`主办处室拟办意见:${niban}`); dialogTip.updateTip(`主办处室核稿意见:${hg}`); // 生成并择优办理意见 console.log("生成并择优办理意见..."); const opinionResult = await getHandleOpinion(fileTitle, fenban,fenbanhg, wjzy, niban, hg, Data.zhaiyao); if (opinionResult.responsiblePerson.length === 0) { console.log("生成办理意见失败,跳过该文件"); continue; } dialogTip.updateTip(`办理意见:${opinionResult.tips}`); dialogTip.updateTip(`涉及同志:${opinionResult.responsiblePerson.join('、')}`); // 等待30秒 dialogTip.updateTip("等待30秒后执行分办..."); await sleep(30000); if (window.stopAutoProcess) break; // 提交办理意见 const result= await setHandleOpinion(item, Data.zhubanData.custom?.record.rowguid, opinionResult.tips); // 匹配人员ID console.log("匹配负责同志ID..."); const deptUsers = await getDeptUsers(item.docguid || ""); const userIds = []; for (const name of opinionResult.responsiblePerson) { const user = deptUsers.find(u => u.text === name); if (user) { userIds.push(user.id); } else { console.log(`未找到${name}的人员信息`); } } if (userIds.length === 0) { console.log("未匹配到人员ID,跳过分办"); continue; } // 执行分办 console.log("执行人员分办..."); await assignPersons(item, userIds); console.log(`会办文件《${fileTitle}》分办成功!`); await executeSmsSendProcess(`豆包报告:《${fileTitle}》会办分办成功!\n办理意见:${opinionResult.tips}\n涉及同志:${opinionResult.responsiblePerson.join('、')}`); await sleep(3000); } catch (e) { console.log(`会办文件《${fileTitle}》处理失败:${e.message}`); console.error(e); } } console.log("==================== 会办分办处理完成 ===================="); } /** * 10分钟倒数计时(修复后台标签页计时不准问题:采用时间戳对比法) * @param {object} dialogTip - 对话框对象 * @returns {Promise} true=倒数完成,false=被停止 */ async function countdown10Minutes(dialogTip) { const totalMs = 600000; // 10分钟总毫秒数 const startTime = Date.now(); // 记录倒计时开始的时间戳 dialogTip.progressContainer.style.display = 'block'; dialogTip.progressBar.style.width = '100%'; dialogTip.progressBar.style.background = '#28a745'; return new Promise((resolve) => { const timer = setInterval(() => { // 优先判断是否被用户停止 if (window.stopAutoProcess) { clearInterval(timer); dialogTip.progressContainer.style.display = 'none'; resolve(false); return; } // 核心修改:计算真实剩余时间(基于时间戳差值) const elapsedMs = Date.now() - startTime; // 已流逝的真实时间 let remainingMs = totalMs - elapsedMs; // 判断是否倒计时完成 if (remainingMs <= 0) { clearInterval(timer); dialogTip.progressContainer.style.display = 'none'; resolve(true); return; } // 更新进度条和剩余时间显示 const progressPercent = (remainingMs / totalMs) * 100; dialogTip.progressBar.style.width = `${progressPercent}%`; // 格式化剩余时间(分:秒) const remainingMin = Math.floor(remainingMs / 60000); const remainingSec = Math.floor((remainingMs % 60000) / 1000); const minStr = remainingMin.toString().padStart(2, '0'); const secStr = remainingSec.toString().padStart(2, '0'); dialogTip.progressText.textContent = `剩余:${minStr}:${secStr}`; // 更新进度条颜色(和原有逻辑一致) if (remainingMs < totalMs / 2) { dialogTip.progressBar.style.background = '#ffc107'; } if (remainingMs < totalMs / 4) { dialogTip.progressBar.style.background = '#dc3545'; } }, 100); // 定时器仍保留100ms间隔(仅用于更新UI,不影响计时准确性) // 初始状态更新(和原有逻辑一致) const initialMin = Math.floor(totalMs / 60000); const initialSec = Math.floor((totalMs % 60000) / 1000); const minStr = initialMin.toString().padStart(2, '0'); const secStr = initialSec.toString().padStart(2, '0'); dialogTip.progressText.textContent = `剩余:${minStr}:${secStr}`; }); } /** * 判断当前时间是否在 00:30 - 07:00 之间 * @returns {boolean} 符合时间段返回true,否则返回false */ function isInLateNightPeriod() { const now = new Date(); const hours = now.getHours(); const minutes = now.getMinutes(); // 情况1:0点30分及以后(00:30 <= 时间 < 01:00) if (hours === 0 && minutes >= 30) { return true; } // 情况2:1点到6点整(01:00 <= 时间 < 07:00) if (hours >= 1 && hours < 7) { return true; } // 其他时间段返回false return false; } /** * 计算当前时间到早上7:00的剩余毫秒数 * @returns {number} 剩余毫秒数 */ function getMillisecondsTo7AM() { const now = new Date(); const target = new Date(); // 设置目标时间为当天7:00:00 target.setHours(7, 0, 0, 0); // 计算时间差(毫秒) return target.getTime() - now.getTime(); } /** * 倒计时到早上7:00(保留进度条和停止判断) * @param {object} dialogTip - 对话框对象 * @returns {Promise} true=倒计时完成,false=被用户停止 */ async function countdownTo7AM(dialogTip) { const totalMs = getMillisecondsTo7AM(); if (totalMs <= 0) { return true; // 已过7点,直接返回完成 } const startTime = Date.now(); dialogTip.progressContainer.style.display = 'block'; dialogTip.progressBar.style.width = '100%'; dialogTip.progressBar.style.background = '#28a745'; return new Promise((resolve) => { const timer = setInterval(() => { // 优先判断用户停止指令 if (window.stopAutoProcess) { clearInterval(timer); dialogTip.progressContainer.style.display = 'none'; resolve(false); return; } // 计算真实剩余时间 const elapsedMs = Date.now() - startTime; let remainingMs = totalMs - elapsedMs; // 倒计时完成 if (remainingMs <= 0) { clearInterval(timer); dialogTip.progressContainer.style.display = 'none'; resolve(true); return; } // 更新进度条 const progressPercent = (remainingMs / totalMs) * 100; dialogTip.progressBar.style.width = `${progressPercent}%`; // 格式化剩余时间(时:分:秒) const remainingHour = Math.floor(remainingMs / 3600000); const remainingMin = Math.floor((remainingMs % 3600000) / 60000); const remainingSec = Math.floor((remainingMs % 60000) / 1000); const hourStr = remainingHour.toString().padStart(2, '0'); const minStr = remainingMin.toString().padStart(2, '0'); const secStr = remainingSec.toString().padStart(2, '0'); dialogTip.progressText.textContent = `暂停至7:00,剩余:${hourStr}:${minStr}:${secStr}`; // 进度条颜色渐变 if (remainingMs < totalMs / 2) { dialogTip.progressBar.style.background = '#ffc107'; } if (remainingMs < totalMs / 4) { dialogTip.progressBar.style.background = '#dc3545'; } }, 100); // 初始化显示 const initialHour = Math.floor(totalMs / 3600000); const initialMin = Math.floor((totalMs % 3600000) / 60000); const initialSec = Math.floor((totalMs % 60000) / 1000); const hourStr = initialHour.toString().padStart(2, '0'); const minStr = initialMin.toString().padStart(2, '0'); const secStr = initialSec.toString().padStart(2, '0'); dialogTip.progressText.textContent = `暂停至7:00,剩余:${hourStr}:${minStr}:${secStr}`; }); } /** * 创建会办风格信息显示窗口(核心复用) * @returns {object} 对话框对象 */ function createDialog() { // 移除旧窗口 const oldMask = document.querySelector('div[style*="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 9998;"]'); const oldDialog = document.querySelector('div[style*="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; height: 450px; background: white; z-index: 9999;"]'); if (oldMask) oldMask.remove(); if (oldDialog) oldDialog.remove(); // 创建遮罩层 const mask = document.createElement('div'); mask.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); z-index: 9998; `; // 创建对话框容器 const dialog = document.createElement('div'); dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; height: 450px; background: white; border: 2px solid #333; border-radius: 8px; padding: 20px; z-index: 9999; box-shadow: 0 0 20px rgba(0,0,0,0.5); font-family: "Microsoft Yahei", Arial, sans-serif; display: flex; flex-direction: column; `; // 标题 const title = document.createElement('h3'); title.textContent = "公文自动办理进程"; title.style.margin = "0 0 10px 0"; title.style.textAlign = "center"; // 日志文本域 const textArea = document.createElement('textarea'); textArea.style.cssText = ` flex: 1; width: 100%; padding: 10px; resize: none; font-size: 14px; line-height: 1.5; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 10px; `; textArea.value = `${getFormattedTime()} === 公文自动办理脚本启动 ===`; textArea.readOnly = true; // 进度条容器 const progressContainer = document.createElement('div'); progressContainer.style.cssText = ` width: 100%; height: 30px; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 10px; overflow: hidden; display: none; position: relative; `; // 进度条 const progressBar = document.createElement('div'); progressBar.style.cssText = ` width: 100%; height: 100%; background: #28a745; transition: width 100ms linear; `; // 进度文本 const progressText = document.createElement('div'); progressText.style.cssText = ` position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; font-weight: bold; pointer-events: none; font-size: 14px; `; // 组装进度条 progressContainer.appendChild(progressBar); progressContainer.appendChild(progressText); // 停止按钮 const stopBtn = document.createElement('button'); stopBtn.textContent = "结束自动办理"; stopBtn.style.cssText = ` padding: 10px 20px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; margin: 0 auto; display: block; `; stopBtn.onmouseover = function() { this.style.background = "#c82333"; }; stopBtn.onmouseout = function() { this.style.background = "#dc3545"; }; stopBtn.onclick = async () => { window.stopAutoProcess = true; textArea.value += `\n\n${getFormattedTime()} - === 用户手动终止自动办理 ===`; await sleep(1000); mask.remove(); dialog.remove(); }; // 组装对话框 dialog.appendChild(title); dialog.appendChild(textArea); dialog.appendChild(progressContainer); dialog.appendChild(stopBtn); document.body.appendChild(mask); document.body.appendChild(dialog); // 返回对话框对象 return { updateTip: (text) => { textArea.value += `\n${getFormattedTime()} - ${text}`; textArea.scrollTop = textArea.scrollHeight; }, dialog, mask, progressContainer, progressBar, progressText }; } // ==================== 主流程:办文→会办→智能等待(区分时间段) ==================== async function mainProcess() { dialogTip = createDialog(); console.log("初始化完成,即将启动公文自动办理流程..."); await executeSmsSendProcess("豆包报告:开始自动办理公文!"); await sleep(3000); // 外层循环 while (!window.stopAutoProcess) { try { // 第一步:处理办文分办 await processBanWenFiles(); // 第二步:处理会办分办 await processHuiBanFiles(); // 第三步:智能等待(区分时间段) if (window.stopAutoProcess) break; // 判断当前是否在凌晨00:30-07:00时间段 if (isInLateNightPeriod()) { console.log("\n=== 本轮办理完成,当前为00:30-07:00,将暂停至早上7:00 ==="); const countdownResult = await countdownTo7AM(dialogTip); if (!countdownResult) { console.log("等待过程中被用户终止"); break; } } else { console.log("\n=== 本轮办理完成,将等待10分钟后进入下一轮 ==="); const countdownResult = await countdown10Minutes(dialogTip); if (!countdownResult) { console.log("等待过程中被用户终止"); break; } } console.log("\n=== 等待完成,启动下一轮公文办理 ==="); } catch (e) { console.log(`本轮办理出现异常:${e.message},将等待10分钟后重试`); console.error("主流程异常:", e); // 异常后仍按时间段判断等待方式 if (!window.stopAutoProcess) { if (isInLateNightPeriod()) { await countdownTo7AM(dialogTip); } else { await countdown10Minutes(dialogTip); } } } } // 流程终止清理 dialogTip.updateTip("公文自动办理流程已终止"); await sleep(1000); dialogTip.mask.remove(); dialogTip.dialog.remove(); } // 启动确认 if (confirm("是否启动公文自动办理脚本?\n流程:先分办公文→再会办公文→智能等待(00:30-07:00暂停至7点,其余时间暂停10分钟)")) { mainProcess(); } } // ============================================== // 分支2:匹配TOKEN同步域名(znwg.zhbgzt.gx.gov),执行ACCESS_TOKEN同步逻辑 // ============================================== else if (currentUrl === 'http://znwg.zhbgzt.gx.gov/gxbgtwgzs_zgt/aiQA/index') { // 同步窗口相关变量 let syncWindow = null; let syncInterval = null; let syncTimer = null; /** * 创建令牌同步窗口 */ function createSyncWindow() { // 移除旧窗口 const oldMask = document.querySelector('#syncTokenMask'); const oldWindow = document.querySelector('#syncTokenWindow'); if (oldMask) oldMask.remove(); if (oldWindow) oldWindow.remove(); // 创建遮罩层 const mask = document.createElement('div'); mask.id = 'syncTokenMask'; mask.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.3); z-index: 9998; `; // 创建窗口容器 const windowDiv = document.createElement('div'); windowDiv.id = 'syncTokenWindow'; windowDiv.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; height: 300px; background: white; border: 2px solid #007bff; border-radius: 10px; padding: 20px; z-index: 9999; box-shadow: 0 0 20px rgba(0,123,255,0.3); font-family: "Microsoft Yahei", Arial, sans-serif; display: flex; flex-direction: column; `; // 标题 const title = document.createElement('h3'); title.textContent = "ACCESS_TOKEN 同步状态"; title.style.cssText = ` margin: 0 0 15px 0; text-align: center; color: #007bff; border-bottom: 2px solid #007bff; padding-bottom: 10px; `; // 状态显示区域 const statusArea = document.createElement('div'); statusArea.id = 'syncStatusArea'; statusArea.style.cssText = ` flex: 1; padding: 15px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 15px; overflow-y: auto; font-size: 14px; line-height: 1.6; `; // 初始状态 const currentTime = new Date().toLocaleTimeString(); statusArea.innerHTML = `
令牌同步服务已启动
开始时间: ${currentTime}
同步间隔: 每10分钟同步一次
当前状态: 等待首次同步...
上次同步: 无
下次同步: 计算中...
`; // 令牌预览区域 const tokenPreview = document.createElement('div'); tokenPreview.id = 'tokenPreview'; tokenPreview.style.cssText = ` margin-top: 10px; padding: 10px; background: #e9ecef; border-radius: 4px; font-family: monospace; font-size: 12px; word-break: break-all; max-height: 60px; overflow-y: auto; `; tokenPreview.textContent = "当前令牌: 等待同步..."; statusArea.appendChild(tokenPreview); // 按钮容器 const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; justify-content: space-between; gap: 10px; `; // 立即同步按钮 const syncNowBtn = document.createElement('button'); syncNowBtn.textContent = "立即同步"; syncNowBtn.style.cssText = ` flex: 1; padding: 12px; background: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; `; syncNowBtn.onmouseover = () => syncNowBtn.style.background = "#218838"; syncNowBtn.onmouseout = () => syncNowBtn.style.background = "#28a745"; // 退出按钮 const exitBtn = document.createElement('button'); exitBtn.textContent = "退出同步"; exitBtn.style.cssText = ` flex: 1; padding: 12px; background: #dc3545; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; `; exitBtn.onmouseover = () => exitBtn.style.background = "#c82333"; exitBtn.onmouseout = () => exitBtn.style.background = "#dc3545"; // 组装窗口 buttonContainer.appendChild(syncNowBtn); buttonContainer.appendChild(exitBtn); windowDiv.appendChild(title); windowDiv.appendChild(statusArea); windowDiv.appendChild(buttonContainer); document.body.appendChild(mask); document.body.appendChild(windowDiv); // 按钮事件 syncNowBtn.onclick = () => { updateStatus("正在执行手动同步..."); syncAccessToken(true); // true表示是手动触发 }; exitBtn.onclick = () => { exitSync(); }; return { window: windowDiv, mask: mask, updateStatus: (text) => { const statusDiv = document.createElement('div'); statusDiv.textContent = `${new Date().toLocaleTimeString()}: ${text}`; statusDiv.style.marginTop = '5px'; statusDiv.style.color = '#495057'; statusArea.appendChild(statusDiv); statusArea.scrollTop = statusArea.scrollHeight; }, updateLastSync: (time, token) => { const lastSyncInfo = document.getElementById('lastSyncInfo'); const tokenPreview = document.getElementById('tokenPreview'); if (lastSyncInfo) { lastSyncInfo.textContent = `上次同步: ${time}`; } if (tokenPreview) { const preview = token.substring(0, 30) + (token.length > 30 ? '...' : ''); tokenPreview.textContent = `当前令牌: ${preview}`; tokenPreview.title = `完整令牌: ${token}`; } }, updateNextSync: (time) => { const nextSyncInfo = document.getElementById('nextSyncInfo'); if (nextSyncInfo) { nextSyncInfo.textContent = `下次同步: ${time}`; } } }; } /** * 更新窗口状态 */ function updateStatus(text) { if (syncWindow) { syncWindow.updateStatus(text); } } /** * 更新同步时间信息 */ function updateSyncInfo() { const now = new Date(); const lastSyncTime = now.toLocaleTimeString(); const nextSyncTime = new Date(now.getTime() + 600000).toLocaleTimeString(); // 10分钟后 if (syncWindow) { syncWindow.updateLastSync(lastSyncTime, GM_getValue("ACCESS_TOKEN", "")); syncWindow.updateNextSync(nextSyncTime); } } /** * 退出同步功能 */ function exitSync() { // 清除定时器 if (syncTimer) { clearInterval(syncTimer); syncTimer = null; } // 更新状态 updateStatus("已停止令牌同步服务"); // 1秒后移除窗口 setTimeout(() => { if (syncWindow && syncWindow.mask) { syncWindow.mask.remove(); } if (syncWindow && syncWindow.window) { syncWindow.window.remove(); } syncWindow = null; }, 1000); } /** * 核心函数:获取localStorage的ACCESS_TOKEN并同步到油猴存储 */ function syncAccessToken(isManual = false) { try { // 1. 从目标域名的localStorage中获取ACCESS_TOKEN const localToken = localStorage.getItem("ACCESS_TOKEN"); // 2. 判断令牌是否存在(避免存储null/空值) if (localToken) { // 3. 使用油猴API GM_setValue存储令牌,键名为"ACCESS_TOKEN" const oldToken = GM_getValue("ACCESS_TOKEN", ""); GM_setValue("ACCESS_TOKEN", localToken); const status = isManual ? "[手动同步成功]" : "[自动同步成功]"; const logMsg = `${status} 时间:${new Date().toLocaleString()},令牌已更新`; console.log(logMsg); updateStatus(logMsg); // 更新同步信息 updateSyncInfo(); // 如果令牌有变化,显示更详细的信息 if (oldToken !== localToken) { updateStatus(`令牌有变化,已更新最新版本`); } } else { const status = isManual ? "[手动同步失败]" : "[自动同步失败]"; const logMsg = `${status} 时间:${new Date().toLocaleString()},未从localStorage获取到ACCESS_TOKEN`; console.warn(logMsg); updateStatus(logMsg); } } catch (error) { const status = isManual ? "[手动同步异常]" : "[自动同步异常]"; const logMsg = `${status} 时间:${new Date().toLocaleString()},错误信息:${error.message}`; console.error(logMsg); updateStatus(logMsg); } } // 主初始化函数 function initTokenSync() { // 1. 创建同步窗口 syncWindow = createSyncWindow(); // 2. 脚本初始化时立即执行一次同步(无需等待1分钟) syncAccessToken(); updateStatus("首次同步完成"); updateSyncInfo(); // 3. 设置定时器,每10分钟(600000毫秒)执行一次同步操作 syncTimer = setInterval(() => { syncAccessToken(); }, 600000); // 4. 更新状态 updateStatus("定时同步服务已启动,每10分钟同步一次"); // 5. 添加样式 GM_addStyle(` #syncTokenWindow { animation: fadeIn 0.3s ease-out; } @keyframes fadeIn { from { opacity: 0; transform: translate(-50%, -55%); } to { opacity: 1; transform: translate(-50%, -50%); } } #syncStatusArea::-webkit-scrollbar { width: 6px; } #syncStatusArea::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 3px; } #syncStatusArea::-webkit-scrollbar-thumb { background: #007bff; border-radius: 3px; } #syncStatusArea::-webkit-scrollbar-thumb:hover { background: #0056b3; } `); } // 启动令牌同步 initTokenSync(); } })();