OA系统公文一键下载解决方案

一、工作背景

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

本例中,通过开发人员工具可以看到标题字段对应的 idgoa_doc_receivefile_title,因此脚本可以通过 #goa_doc_receivefile_title 对其进行定位。如果换成其他 OA,“标题”字段的 HTML 结构很可能不同

这时需要使用 Edge 浏览器的“开发人员工具”检查收文单中的标题字段。打开收文单后,按 F12 进入开发人员工具,使用“元素”工具定位“标题”对应的内容,查看这个字段在网页 HTML 中的具体结构,例如 idnameclass 或其他能够唯一定位该字段的属性。然后根据实际找到的结构,对脚本中的 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 系统通过技术措施明确限制上述操作,请遵循单位规定,不应通过本工具规避相关管理要求。

订阅
提醒
0 评论
内嵌讨论
查看全部讨论