一、工作背景
OA系统已经部署在互联网访问环境中,工作人员通过浏览器即可完成公文收发、审批和查阅等工作。但却以“信息安全”、“保密管理”等理由,对公文正文设置各种使用限制,不提供下载、不允许打印,只能在特定的在线阅读页面中查看。安全管理本身当然是必要的,但安全管理的重点应该是明确哪些文件可以下载、谁可以下载、下载后如何管理。
如果一个系统已经允许用户在互联网环境下登录,并且经过身份认证后可以正常查看某份公文,那么真正需要解决的安全问题,应当更多地依靠身份认证、权限控制、访问控制、日志审计、数据加密以及终端安全管理等手段来保障,而不是一味限制正常的办公操作。
而且,更有意思的是,部分OA系统虽然在页面上没有提供下载按钮,但公文正文实际上已经被系统加载到浏览器的在线阅读组件中,并以OFD等标准电子文档格式提供给阅读器使用。因此,与其让工作人员反复截图、打印后再扫描,或者为了离线查看而采用各种低效的方式,不如在具备正常访问和阅读权限的前提下,提供一种更加规范、方便的本地保存方式。
本文提供了一种方式为OA公文增加本地保存能力,并根据收文单中的公文标题自动命名下载文件,从而实现公文的离线阅读和打印,前提仍然是使用者本身已经具有相应的文件使用权限。
二、适用范围
本文方法主要适用于使用FlexOffice / WebOffice公文在线阅读方式的 OA 系统。
适用于以下场景:
- 使用Edge浏览器访问 OA 系统;
- OA系统中的公文正文通过网页在线阅读;
- 公文正文实际以OFD格式提供;
- 系统页面中存在公文正文的在线阅读功能,但没有提供方便的本地下载入口;
- 需要将公文正文下载到本地,用于离线阅读、保存或打印。
其他浏览器环境和OA系统,只要是FlexOffice / WebOffice模式,那么基础环境设置方式一般都类同,同时简单改动代码中监控地址部分即可实现。
三、部署流程
(一)安装公文阅读软件
由于OA系统中的部分公文正文实际采用OFD格式,下载后的文件并不是普通的Word文档,因此需要安装支持 OFD 格式的阅读软件。建议安装“数科OFD”或“金山WPS”。
安装完成后,可以直接使用该软件打开下载后的.ofd文件,实现:
- 离线阅读
- 打印
- 页面查看
- 内容留存
(二)安装浏览器扩展环境
两种方式任选其一(离线环境是给不能使用互联网的设备提供的方法)。
1.在线安装方式:通过浏览器应用商店安装篡改猴。
2.离线安装方式:
本方法使用 Edge 浏览器进行操作。
- 首先,点击此处下载插件并保存(插件适用Edge和Chrome浏览器);
- 然后,在目标设备的Edge 浏览器地址栏输入
edge://extensions/,按回车进入扩展管理页面,并在页面中打开开发人员模式选项; - 最后,将准备好的
tampermonkey.crx文件直接拖拽到 Edge 页面内进行添加扩展。
完成后,浏览器中就安装好了篡改猴(Tampermonkey)。
(三)配置篡改猴权限
在浏览器右上角的扩展菜单中找到“篡改猴”。
建议先将篡改猴固定到浏览器工具栏,方便后续使用。
右键点击“篡改猴” → “管理扩展”
进入篡改猴的扩展管理页面。在页面中找到相关权限设置,并打开:
允许用户脚本和允许访问文件URL
以此授权插件获取URL地址来判断是否运行脚本。
(四)安装“公文一键下载”脚本
点击浏览器右上角的:篡改猴
然后选择:添加新脚本
在进入脚本编辑页面后,将编辑器中原有的内容全部删除,然后将下面“公文一键下载”脚本文本完整复制并粘贴到编辑器中。
粘贴完成后按Ctrl + S保存脚本,然后正常访问OA系统即可出现下载按钮实现一键下载公文。如图:

“公文一键下载”脚本:
// ==UserScript==
// @name 公文一键下载YY
// @version 2.1.0
// @description 自动获取收文单标题,并在 WebOffice 阅读器中一键下载 OFD
// @match http://192.171.40.3:8600/*
// @match http://192.171.40.3:8095/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect 192.171.40.3
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
console.log('[公文下载] 脚本启动');
console.log('[公文下载] 当前地址:', location.href);
// =====================================================
// 配置
// =====================================================
const TITLE_KEY =
'current_gongwen_title';
// =====================================================
// 清理文件名
// =====================================================
function cleanFileName(title) {
if (!title) {
return '';
}
title =
title
.replace(
/^收文[::]\s*/i,
''
)
.replace(
/[\\/:*?"<>|]/g,
'_'
)
.replace(
/\s+/g,
' '
)
.trim();
// 删除已经存在的扩展名
title =
title.replace(
/\.(ofd|doc|docx|pdf)$/i,
''
);
// Windows 文件名不能以空格或点结尾
title =
title.replace(
/[. ]+$/g,
''
);
return title;
}
// =====================================================
// 从 8600 收文单寻找标题
// =====================================================
function findReceiveTitle() {
// -------------------------------------------------
// 第一优先级:
// #goa_doc_receivefile_title
// -------------------------------------------------
const textarea =
document.querySelector(
'#goa_doc_receivefile_title'
);
if (textarea) {
let title =
textarea.value ||
textarea.textContent ||
'';
title =
cleanFileName(title);
if (title) {
return title;
}
}
// -------------------------------------------------
// 第二优先级:
// data-uniquefieldname
// -------------------------------------------------
const display =
document.querySelector(
'[data-uniquefieldname="goa_doc_receivefile_title"]'
);
if (display) {
const pre =
display.querySelector(
'pre'
);
let title =
pre
? pre.textContent
: display.textContent;
title =
cleanFileName(title);
if (title) {
return title;
}
}
return '';
}
// =====================================================
// 保存标题
//
// 使用 GM_setValue,因为:
//
// 8600
// 和
// 8095
//
// 是不同端口,localStorage 无法跨端口共享。
// =====================================================
function saveTitle(title) {
if (!title) {
return;
}
title =
cleanFileName(title);
if (!title) {
return;
}
GM_setValue(
TITLE_KEY,
title
);
console.log(
'[公文下载] 已保存公文标题:',
title
);
}
// =====================================================
// 读取标题
// =====================================================
async function getSavedTitle() {
try {
const title =
await GM_getValue(
TITLE_KEY,
''
);
if (title) {
return cleanFileName(
title
);
}
} catch (e) {
console.error(
'[公文下载] 读取标题失败:',
e
);
}
return '';
}
// =====================================================
// 8600 页面
//
// 负责找标题 → 保存标题
//
// 不显示按钮
// =====================================================
function init8600() {
console.log(
'[公文下载] 进入 8600 收文单页面'
);
let lastTitle = '';
function checkTitle() {
const title =
findReceiveTitle();
if (
title &&
title !== lastTitle
) {
lastTitle =
title;
saveTitle(
title
);
console.log(
'[公文下载] 当前公文标题:',
title
);
}
}
// 立即检查
checkTitle();
// -------------------------------------------------
// 页面动态加载
// -------------------------------------------------
const observer =
new MutationObserver(
function () {
checkTitle();
}
);
observer.observe(
document.documentElement,
{
childList: true,
subtree: true
}
);
// -------------------------------------------------
// 定时检查
// -------------------------------------------------
setInterval(
checkTitle,
1000
);
}
// =====================================================
// 获取 8095 OFD 地址
// =====================================================
function getFileUrl() {
try {
const url =
new URL(
location.href
);
const file =
url.searchParams.get(
'file'
);
if (!file) {
return null;
}
console.log(
'[公文下载] 找到 file:',
file
);
return file;
} catch (e) {
console.error(
'[公文下载] 获取 file 失败:',
e
);
}
return null;
}
// =====================================================
// 创建下载按钮
// =====================================================
function createDownloadButton(
fileUrl
) {
// 防止重复
if (
document.getElementById(
'ofd-download-button'
)
) {
return;
}
const button =
document.createElement(
'button'
);
button.id =
'ofd-download-button';
button.textContent =
'⬇ 下载公文';
button.style.cssText = `
position: fixed;
top: 40px;
right: 20px;
z-index: 2147483647;
padding: 10px 18px;
background: #1677ff;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
box-shadow: 0 3px 12px rgba(0,0,0,.25);
`;
// =================================================
// 点击下载
// =================================================
button.addEventListener(
'click',
async function () {
console.log(
'[公文下载] 开始下载'
);
button.textContent =
'正在下载...';
button.style.background =
'#fa8c16';
// -----------------------------------------
// 获取公文标题
// -----------------------------------------
let title =
await getSavedTitle();
if (!title) {
title =
'公文正文';
console.warn(
'[公文下载] 没有找到保存的标题'
);
}
const fileName =
title + '.ofd';
console.log(
'[公文下载] 最终文件名:',
fileName
);
// -----------------------------------------
// 请求 OFD
// -----------------------------------------
GM_xmlhttpRequest({
method: 'GET',
url: fileUrl,
responseType: 'blob',
// -------------------------------------
// 成功
// -------------------------------------
onload:
function (response) {
console.log(
'[公文下载] HTTP状态:',
response.status
);
if (
response.status < 200 ||
response.status >= 300
) {
alert(
'下载失败\nHTTP状态:' +
response.status
);
button.textContent =
'⬇ 下载公文';
button.style.background =
'#1677ff';
return;
}
const blob =
response.response;
// ---------------------------------
// 检查 OFD 文件头
// ---------------------------------
const reader =
new FileReader();
reader.onload =
function () {
const bytes =
new Uint8Array(
reader.result
);
console.log(
'[公文下载] 文件头:',
Array.from(
bytes.slice(
0,
8
)
)
);
/*
* OFD 实际上是 ZIP 容器。
*
* 正常文件开头:
*
* 50 4B 03 04
*/
if (
bytes[0] !== 0x50 ||
bytes[1] !== 0x4B
) {
alert(
'下载到的文件不是正常的 OFD 文件。'
);
button.textContent =
'⬇ 下载公文';
button.style.background =
'#1677ff';
return;
}
// ---------------------------------
// 浏览器下载
// ---------------------------------
const blobUrl =
URL.createObjectURL(
blob
);
const a =
document.createElement(
'a'
);
a.href =
blobUrl;
a.download =
fileName;
document.body.appendChild(
a
);
a.click();
a.remove();
setTimeout(
function () {
URL.revokeObjectURL(
blobUrl
);
},
10000
);
// ---------------------------------
// 完成
// ---------------------------------
button.textContent =
'✓ 下载完成';
button.style.background =
'#52c41a';
setTimeout(
function () {
button.textContent =
'⬇ 下载公文';
button.style.background =
'#1677ff';
},
2000
);
};
reader.readAsArrayBuffer(
blob
);
},
// -------------------------------------
// 失败
// -------------------------------------
onerror:
function (error) {
console.error(
'[公文下载] 请求失败:',
error
);
alert(
'下载请求失败,请查看浏览器控制台。'
);
button.textContent =
'⬇ 下载公文';
button.style.background =
'#1677ff';
}
});
}
);
// =================================================
// 添加按钮
// =================================================
document.body.appendChild(
button
);
console.log(
'[公文下载] 下载按钮已显示'
);
}
// =====================================================
// 8095 Reader
// =====================================================
function init8095() {
console.log(
'[公文下载] 进入 8095 Reader'
);
const fileUrl =
getFileUrl();
if (!fileUrl) {
console.log(
'[公文下载] 当前页面没有 file 参数'
);
return;
}
console.log(
'[公文下载] OFD 地址已找到'
);
function tryCreateButton() {
if (!document.body) {
return false;
}
createDownloadButton(
fileUrl
);
return true;
}
// 页面已经有 body
if (
tryCreateButton()
) {
return;
}
// body 还没出现
const observer =
new MutationObserver(
function () {
if (
tryCreateButton()
) {
observer.disconnect();
}
}
);
observer.observe(
document.documentElement,
{
childList: true,
subtree: true
}
);
}
// =====================================================
// 根据端口判断页面
// =====================================================
if (
location.port === '8600'
) {
init8600();
}
else if (
location.port === '8095'
) {
init8095();
}
})();
四、脚本说明
本文提供的“公文一键下载”脚本,是根据“我”目前使用的OA系统编写的,可以完美适配。对于其他OA系统来说,是一个通用的模板,但不是可以直接适用于所有OA系统的脚本。
如果其他系统工作方式比较类似,通常只需要重点检查以下几个地方:
(一)修改 OA 网站地址
脚本:
// @match http://192.171.40.3:8600/* // @match http://192.171.40.3:8095/*
这里表示脚本在哪些网页上运行。
其中:
8600是当前 OA 收文单页面;8095是 WebOffice 公文阅读页面。
如果其他 OA 使用的是不同的 IP、域名或端口,需要将这里修改成实际地址。
(二)修改文件服务器地址
脚本:
// @connect 192.171.40.3
这一项与公文实际下载地址有关。
本例中,OA 页面和公文文件都位于 192.171.40.3 服务器,因此使用这个地址。
如果其他 OA 的公文文件存放在另外的服务器,例如:
OA: 192.168.1.10 文件: 192.168.1.20
那么就需要根据实际的公文下载地址修改为:
// @connect 192.168.1.10 // @connect 192.168.1.20
(三)确认“标题”字段
标题字段用于文件下载时候的命名。
本脚本会先打开 OA 的收文单,从收文单中的“标题”字段获取公文名称,然后将这个名称作为下载后的 OFD 文件名。
当前 OA 中,标题字段对应:goa_doc_receivefile_title
本例中,通过开发人员工具可以看到标题字段对应的 id 为 goa_doc_receivefile_title,因此脚本可以通过 #goa_doc_receivefile_title 对其进行定位。如果换成其他 OA,“标题”字段的 HTML 结构很可能不同。
这时需要使用 Edge 浏览器的“开发人员工具”检查收文单中的标题字段。打开收文单后,按 F12 进入开发人员工具,使用“元素”工具定位“标题”对应的内容,查看这个字段在网页 HTML 中的具体结构,例如 id、name、class 或其他能够唯一定位该字段的属性。然后根据实际找到的结构,对脚本中的 findReceiveTitle() 函数进行相应修改,使脚本能够准确找到标题内容。
需要注意的是,标题获取失败并不会影响公文正文的下载。 如果没有成功获取标题,脚本会使用默认文件名“公文正文.ofd”进行保存。
(四)确认公文实际下载地址
1.本文中的OA分析
很多 OA 打开公文时,浏览器地址栏显示的是 WebOffice 阅读器页面的地址,而不是公文文件本身的下载地址。
例如本文使用的 OA,打开公文正文后,浏览器访问的是类似下面的地址:http://192.171.40.3:8095/reader?file=http%3A%2F%2F192.171.40.3%3A8600%2Fflexbase%2Fattachment%2Fdl%3Ffid%3D402a91…
这里需要注意,file 是 WebOffice 阅读器地址中的一个参数。
而这个 file 参数的内容,经过 URL 解码以后,才是真正的公文文件地址:http://192.171.40.3:8600/flexbase/attachment/dl?fid=……&chksum=……&hash=……
也就是说,整个过程实际上是:
打开 WebOffice 阅读器→ 读取当前页面 URL→ 找到 file 参数→ 得到 file 参数里面的地址→ 请求这个地址→ 获得真正的 OFD 文件
脚本中对应的代码就是:
const url = new URL(location.href);
const file = url.searchParams.get('file');
这里的作用并不是直接下载文件,而是从 WebOffice 阅读器地址中取出 file 参数的内容。
取得这个地址以后,脚本再通过:GM_xmlhttpRequest()向真正的公文文件地址发起请求,最终获得 OFD 文件。
2.如果用于其他 OA
如果其他 OA 的 WebOffice 阅读器也是通过类似:?file=xxxx的方式提供公文文件,那么可以采用类似的方法获取下载地址。
如果打开正文后,地址栏中没有 file 参数,也不要认为公文无法下载。这只说明该 OA 获取正文文件的方式不同。
这时可以使用 Edge 浏览器的开发人员工具进行分析:
- 打开公文正文;
- 按 F12 打开开发人员工具;
- 进入 “网络(Network)”;
- 重新打开公文或者刷新页面;
- 查看页面加载过程中产生的请求;
- 找到实际返回 OFD 文件,或者返回公文文件地址的请求。
需要找到的是最终能够获取 OFD 文件的地址或接口,而不是单纯的 WebOffice 阅读器页面地址。
简单来说:浏览器地址栏里的 WebOffice 地址只是“阅读页面”,真正需要下载的是它背后的 OFD 文件。不同 OA 获取这个文件的方式可能不同,因此这一部分通常需要根据实际系统进行适配。
另外,如果文件地址需要登录状态、Cookie 或其他认证信息才能访问,也需要根据实际 OA 的认证方式进行处理。
(五)确认 OA 和 WebOffice 的页面关系
脚本目前按照两个页面进行处理:
8600→ 收文单→ 获取公文标题→ 8095→ WebOffice 阅读器→ 获取 OFD 地址→ 显示“下载公文”按钮
所以代码最后有这样的判断,用以实现判断当前浏览器打开的是哪个端口,然后决定执行哪一部分功能。
if (location.port === '8600') {
init8600();
}
else if (location.port === '8095') {
init8095();
}
如果其他 OA 使用的端口不同,或者 OA 和 WebOffice 根本不是这样的两个页面,就需要根据实际情况进行修改。
(六)小结
因此,如果想把本文的脚本用于其他 OA,并不是简单地修改一个 IP 地址就可以。
建议按照下面的顺序检查:
OA 地址 → 文件服务器 → 收文单标题字段 → WebOffice 实际文件地址 → ⑤OA 与 WebOffice 的页面关系
其中,前两项通常比较容易修改,后三项则需要根据具体 OA 的页面结构进行分析。
如果其他 OA 同样采用“收文单 + WebOffice + OFD”的工作方式,那么这套思路仍然具有参考价值,只需要针对具体系统进行适配即可。
免责声明
本文及所提供的“公文一键下载”脚本仅用于技术研究、学习以及合法的办公场景。
本脚本的设计前提是:使用者已经通过正常方式登录 OA 系统,并且对相关公文具有相应的查看、使用、下载、打印或保存权限。
本脚本不会提供账号破解、权限提升、绕过身份认证或突破服务器访问控制等功能。脚本仅对用户当前已经能够正常访问的页面进行处理,并将页面中已经提供的公文文件地址获取到本地。
需要特别注意:能够在线查看公文,并不意味着一定具有下载、复制、打印或者对外传播的权限。 实际使用时,应当遵守所在单位的信息安全、保密管理、档案管理以及 OA 系统的相关规定。
对于涉及国家秘密、工作秘密、商业秘密、个人信息或者其他受保护信息的文件,在未确认具有相应权限和保存条件的情况下,请勿使用本工具进行下载、复制、存储或传播。现行《中华人民共和国保守国家秘密法》对国家秘密的复制、记录、存储以及涉密信息系统的管理均有明确要求。
使用者应自行确认其使用行为具有合法性和相应授权。因使用本脚本造成的任何文件泄露、违规传播、违反单位管理制度或其他法律责任,由实际使用者自行承担。本文作者不对使用者未经授权的使用行为承担责任。
如果所在单位明确禁止下载、复制、打印相关公文,或者 OA 系统通过技术措施明确限制上述操作,请遵循单位规定,不应通过本工具规避相关管理要求。