// ==UserScript== // @name 办文自动分办脚本 // @namespace http://tampermonkey.net/ // @version 1.3 // @description 自动获取办文列表,生成办理提示并分办给负责同志(右键触发版,修复退出后对话框消失+日志时间戳+流程卡死问题) // @author Your Name // @match http://dzgw.zhbg.kjt.gx.gov/* // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @connect localhost // @run-at context-menu // ==/UserScript== (function() { 'use strict'; /** * 获取格式化的当前时间 [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 - 请求参数对象 * @returns {Promise} 响应数据 */ async function postRequest(url, params) { console.log(`${getFormattedTime()} 发起POST请求:${url},参数:`, params); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: url, headers: { 'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'application/json, text/javascript, */*; q=0.01' }, data: 'params=' + JSON.stringify(params) + '&controlself=true', timeout: 30000, // 延长超时到30秒 onload: function(response) { try { console.log(`${getFormattedTime()} 请求响应:${url},状态码:${response.status}`); if (response.status < 200 || response.status >= 300) { reject(`请求失败,状态码:${response.status},响应:${response.responseText.substring(0, 500)}`); return; } let res; try { res = JSON.parse(response.responseText); } catch (parseErr) { reject(`解析响应失败:${parseErr.message},原始响应(前500字符):${response.responseText.substring(0, 500)}`); return; } if (res.status && res.status.code !== "1") { reject(`业务处理失败:${res.status.text || '未知错误'},响应状态:${JSON.stringify(res.status)}`); return; } resolve(res); } catch (e) { reject(`处理响应失败:${e.message},原始响应(前500字符):${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); } }); }); } /** * 第一步:获取公文列表数据 * @returns {Promise} 公文列表数据 */ async function getDocumentList() { // 修复URL换行问题 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) { // 修复URL 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) { // 修复URL换行问题 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 { updateDialogInfo(`开始执行短信发送流程,内容:${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 = "未找到标题为「测试」的公文,短信发送跳过"; updateDialogInfo(msg); console.warn(msg); return { success: false, message: msg }; } await sendSmsMessage(firstDoc, smsContent); await getDocumentList(); await saveAuditOpinion(firstDoc); const successMsg = "✅ 短信发送流程执行成功"; updateDialogInfo(successMsg); console.log(successMsg); return { success: true, message: successMsg, docData: firstDoc }; } catch (error) { const errMsg = `❌ 短信发送流程执行失败:${error.message}`; updateDialogInfo(errMsg); console.error(errMsg, error); return { success: false, message: errMsg, docData: null }; } } /** * 提取纯文本(去除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(); } // ========== 状态持久化(解决退出后状态丢失) ========== function getGlobalState() { return { isStopped: GM_getValue('isStopped', false), processedFailedIds: GM_getValue('processedFailedIds', []), userList: GM_getValue('userList', []) }; } function saveGlobalState(key, value) { GM_setValue(key, value); } function resetGlobalState() { GM_setValue('isStopped', false); GM_setValue('processedFailedIds', []); } // ========== 对话框创建(右键触发专用,强化容错) ========== function createProcessDialog() { // 强制清理旧对话框(右键重新触发时避免残留) const oldDialog = document.getElementById('processDialog'); if (oldDialog) oldDialog.remove(); // 创建新对话框 const dialog = document.createElement('div'); dialog.id = 'processDialog'; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 600px; height: 400px; background: white; border: 2px solid #333; padding: 20px; z-index: 99999; border-radius: 8px; box-shadow: 0 0 20px rgba(0,0,0,0.3); display: block; `; // 信息文本框 const infoBox = document.createElement('textarea'); infoBox.id = 'processInfo'; infoBox.style.cssText = ` width: 100%; height: 80%; resize: none; padding: 10px; font-size: 14px; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 10px; `; infoBox.readOnly = true; // 初始化内容添加时间戳 infoBox.value = `${getFormattedTime()} === 办文自动分办系统(右键触发版)=== ${getFormattedTime()} 初始化完成,流程已启动... `; // 结束按钮 const stopBtn = document.createElement('button'); stopBtn.innerText = '结束自动办理'; stopBtn.style.cssText = ` padding: 8px 20px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin-right: 10px; `; stopBtn.onclick = () => { saveGlobalState('isStopped', true); dialog.style.display = 'none'; resetGlobalState(); alert('已停止自动办理流程,状态已重置'); }; // 重新触发按钮(适配右键逻辑) const retriggerBtn = document.createElement('button'); retriggerBtn.innerText = '重新右键触发'; retriggerBtn.style.cssText = ` padding: 8px 20px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; `; retriggerBtn.onclick = () => { dialog.style.display = 'none'; resetGlobalState(); alert('请右键页面重新触发脚本以重启流程'); }; const btnBox = document.createElement('div'); btnBox.appendChild(stopBtn); btnBox.appendChild(retriggerBtn); dialog.appendChild(infoBox); dialog.appendChild(btnBox); document.body.appendChild(dialog); return dialog; } // 更新对话框信息(强化容错:丢失则重建+每行时间戳) function updateDialogInfo(content) { if (!content || content.trim() === '') return; // 处理多行内容,每行都添加时间戳 const lines = content.split('\n').filter(line => line.trim() !== ''); const contentWithTime = lines.map(line => `${getFormattedTime()} ${line.trim()}`).join('\n'); const infoBox = document.getElementById('processInfo'); if (infoBox) { // 避免重复空行 const currentValue = infoBox.value.trim(); infoBox.value = currentValue ? `${currentValue}\n${contentWithTime}` : contentWithTime; infoBox.scrollTop = infoBox.scrollHeight; } else { // 对话框丢失时重建(延迟确保DOM加载) createProcessDialog(); setTimeout(() => updateDialogInfo(content), 100); } } // ========== 核心业务逻辑(强化错误处理) ========== async function getFBUserList() { return new Promise((resolve) => { // 改为resolve避免终止流程 GM_xmlhttpRequest({ method: 'POST', // 修复URL换行问题 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 || []; saveGlobalState('userList', userList); resolve(userList); } catch (e) { const errMsg = `读取人员信息失败:${e.message}`; updateDialogInfo(errMsg); console.error(errMsg); resolve([]); // 返回空数组不终止流程 } }, onerror: function(err) { const errMsg = `请求人员信息接口失败:${err.statusText || '未知错误'}`; updateDialogInfo(errMsg); console.error(errMsg); resolve([]); }, ontimeout: function() { const errMsg = '请求人员信息接口超时(15秒)'; updateDialogInfo(errMsg); console.error(errMsg); resolve([]); } }); }); } async function getInboxList() { const { processedFailedIds } = getGlobalState(); const params = { "title":"", "bianhao":"", "dispatchdepartment":"", "lwdate":"", "urgencydegree":"", "laiwennumber":"", "processingstatus":"", "senddatefrom":"", "senddateto":"", "pageIndex":0, "pageSize":10, "tabtype":"0" }; return new Promise((resolve) => { // 改为resolve避免终止流程 GM_xmlhttpRequest({ method: 'POST', // 修复URL换行问题 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 || []; const filterList = inboxList.filter(item => !processedFailedIds.includes(item.inboxguid)); resolve(filterList); } catch (e) { const errMsg = `解析办文列表失败:${e.message}`; updateDialogInfo(errMsg); console.error(errMsg); resolve([]); // 返回空数组 } }, onerror: function(err) { const errMsg = `请求办文列表接口失败:${err.statusText || '未知错误'}`; updateDialogInfo(errMsg); console.error(errMsg); resolve([]); }, ontimeout: function() { const errMsg = '请求办文列表接口超时(15秒)'; updateDialogInfo(errMsg); console.error(errMsg); resolve([]); } }); }); } async function getDocGuid(inboxguid) { const params = { "guid": inboxguid }; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', // 修复URL换行问题 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秒)'); } }); }); } 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秒)'); } }); }); } async function getResponsiblePersonAndTips(fileDetail) { const prompt = ` 请根据以下办文信息确定处室负责同志并生成办理提示: 1. 办文标题:${fileDetail.title} 2. 厅领导批示:${extractTextByRegex(fileDetail.ldps)} 3. 办公室拟办意见:${extractTextByRegex(fileDetail.niban)} 4. 办公室主任审核意见:${extractTextByRegex(fileDetail.hg)} 5. 文件摘要:${extractTextByRegex(fileDetail.wjzy)} 处室同志分工: - 张斌:负责高新区、产业基地、科技成果转化(不含中试、概念验证平台)、科技成果转化政策、科技成果转化三年行动、科创广西试验区等工作; - 钟家安:负责科技企业孵化体系、大学科技园、技术转移体系、技术市场、技术合同登记、技术交易奖励、科技服务业、科技类社会组织,社会科技奖理工作等; - 陈美芝:负责科技金融、科创贷、科创担、科技保险、桂惠通、金融投资、企业创新积分制、项目立项管理、经费预算、火炬统计、保密工作等; - 唐欢:负责科技奖励、科技成果登记、科技成果转化平台(包括中试平台、中试基地、概念验证中心等)、科技成果评价、知识产权创造(包括对接市局监管局)、创新飞地、项目过程管理(验收、科研诚信)、绩效考核、科技宣传、内务工作。 输出要求: - 负责同志只能是上述四人之一,根据分工选择最合适的同志,无法确定则返回"无法确定"; - 办理提示需包含厅领导批示(无则不包含)和办理期限(无则不包含),可为空; - 输出格式为JSON:{"responsiblePerson":"姓名/无法确定","tips":"办理提示内容"} `; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: 'http://localhost:8000/api/get_tips', headers: { 'Content-type': 'application/json' }, data: JSON.stringify({ prompt: prompt }), timeout: 120000, onload: function(response) { try { const res = JSON.parse(response.responseText); resolve(res); } catch (e) { reject('解析大模型返回结果失败:' + e.message); } }, onerror: function(err) { reject('调用本地代理失败:' + err.statusText); }, ontimeout: function() { reject('调用本地代理超时(30秒)'); } }); }); } async function fenbanFile(inboxguid, rowguid, filetype, username, tips) { const { userList } = getGlobalState(); const user = userList.find(item => item.text === username); if (!user) { throw new Error(`未找到${username}的人员ID`); } const userId = user.id; const params = { "inboxguid": inboxguid, "users": userId, "filetype": filetype, "fenbanreason": tips + "R" }; const cmdParams = [inboxguid, rowguid, userId, filetype, tips]; const postData = `params=${JSON.stringify(params)}&cmdParams=${JSON.stringify(cmdParams)}`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', // 修复URL换行问题 url: 'http://dzgw.zhbg.kjt.gx.gov/bgtoa/rest/arceinboxdaichuliaction/doFenban?isCommondto=true&action2rest=true', headers: { 'Content-type': 'application/x-www-form-urlencoded' }, data: postData, 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秒)'); } }); }); } // 强化等待函数(防泄漏+异常处理) function wait(seconds, stopFlag) { return new Promise((resolve) => { let timer = 0; const interval = setInterval(() => { try { timer++; if (stopFlag?.() || timer >= seconds) { clearInterval(interval); resolve(); } } catch (e) { console.error('Wait函数检查停止状态失败:', e); clearInterval(interval); resolve(); } }, 1000); }); } async function processSingleFile(fileItem) { const { inboxguid, title, filetype, rowguid } = fileItem; const { processedFailedIds } = getGlobalState(); try { updateDialogInfo(`\n正在处理办文:${title}\n步骤1/6:获取docguid...`); const docguid = await getDocGuid(inboxguid); updateDialogInfo(`正在处理办文:${title}\n步骤2/6:获取办文详情...`); const fileDetail = await getFileDetail(docguid); updateDialogInfo(`正在处理办文:${title}\n步骤3/6:生成办理提示...`); const result = await getResponsiblePersonAndTips(fileDetail); const { responsiblePerson, tips } = result; const tipContent = ` === 办文处理提示 === 文件标题:${title} 负责同志:${responsiblePerson} 办理提示:${tips || '无'} 将等待30秒后继续处理,可点击"结束自动办理"按钮退出。 `; updateDialogInfo(tipContent); await wait(30, () => getGlobalState().isStopped); if (getGlobalState().isStopped) return; if (responsiblePerson !== '无法确定') { await fenbanFile(inboxguid, rowguid, filetype, responsiblePerson, tips); updateDialogInfo(`办文${title}分办成功!负责同志:${responsiblePerson}`); await executeSmsSendProcess(`豆包报告:《${title}》分办成功!负责同志:${responsiblePerson}`); await sleep(3000); await executeSmsSendProcess(`豆包报告:${title}分办成功!负责同志:${responsiblePerson}`); } else { const newFailedIds = [...processedFailedIds, inboxguid]; saveGlobalState('processedFailedIds', newFailedIds); updateDialogInfo(`办文${title}无法确定负责同志,已标记为不重复处理`); await executeSmsSendProcess(`豆包报告:${title}无法确定负责同志,已标记为不重复处理`); } } catch (error) { try { const newFailedIds = [...processedFailedIds, inboxguid]; saveGlobalState('processedFailedIds', newFailedIds); updateDialogInfo(`办文${title}处理失败:${error.message}\n已标记为不重复处理`); } catch (stateErr) { updateDialogInfo(`办文${title}处理失败:${error.message},且更新失败状态失败:${stateErr.message}`); } console.error(`处理办文${title}失败:`, error); } } // ========== 主流程(右键触发入口,强化容错) ========== async function mainProcess() { // 启动前强制重置状态(避免右键重复触发的残留问题) resetGlobalState(); // 强制创建对话框(右键触发时立即显示) createProcessDialog(); updateDialogInfo('初始化中,正在读取处室人员信息...'); try { await executeSmsSendProcess(`豆包报告:开始自动处理公文分办(右键触发版)!`); await sleep(3000); await executeSmsSendProcess(`豆包报告:开始自动处理公文分办(右键触发版)!`); // 读取处室人员信息(失败不终止) const userList = await getFBUserList(); if (userList.length === 0) { updateDialogInfo('未读取到处室人员信息,将继续尝试读取办文列表,但分办功能可能无法使用'); } // 主循环(强化错误处理,单次失败不终止) while (!getGlobalState().isStopped) { try { updateDialogInfo('正在读取办文列表...'); const inboxList = await getInboxList(); if (inboxList.length === 0) { updateDialogInfo('当前无待处理办文,将等待10分钟后重试...'); await wait(600, () => getGlobalState().isStopped); continue; } for (const fileItem of inboxList) { if (getGlobalState().isStopped) break; // 单个文件处理失败不影响其他文件 try { await processSingleFile(fileItem); } catch (fileError) { updateDialogInfo(`处理单个办文失败:${fileError.message},继续处理下一个`); console.error('处理单个办文失败:', fileError); } } if (!getGlobalState().isStopped) { updateDialogInfo('本轮办文处理完成,将等待10分钟后重新读取...'); await wait(600, () => getGlobalState().isStopped); } } catch (loopError) { updateDialogInfo(`读取办文列表或处理本轮数据失败:${loopError.message},将等待10分钟后重试...`); console.error('主循环单次执行失败:', loopError); // 即使出错,也等待10分钟后重试,不终止循环 if (!getGlobalState().isStopped) { await wait(600, () => getGlobalState().isStopped); } } } } catch (error) { updateDialogInfo(`流程异常终止:${error.message}`); console.error('主流程异常:', error); resetGlobalState(); } } // ========== 右键触发核心逻辑(无load事件) ========== (async function initByContextMenu() { // 右键触发时先弹出确认框 if (confirm('确认启动办文自动分办流程(右键触发版)吗?')) { // 立即执行主流程 await mainProcess(); } else { alert('已取消启动,如需重新触发请右键页面选择本脚本'); } })(); })();