从基于致远互联(Seeyon)封装的SuwellLightRead技术的PDF预览页面提取原始PDF文件的方法原理与实践

文章目录

实践实例

源码:

html 复制代码
<!DOCTYPE html>
<meta http-equiv="X-UA-Compatible" content="IE=EDGE" />

<script src="/seeyon/rest/global/v1/env" type="text/javascript"></script>
<link rel="stylesheet" theme-data type="text/css"   href="/seeyon/common/all-min.css?V=V9_0SP1_250518_2028260">
<!--[if lt IE 9]>
<link rel="stylesheet" theme-data href="/seeyon/portal/management/css/iconfont.css?V=V9_0SP1_250518_2028260"/>
<![endif]-->
<link rel="icon"   type="image/x-icon"  href="/seeyon/common/images/A8N/favicon.ico?V=V9_0SP1_250518_2028260" />
<!--[if (!IE)|(gte IE 9)]><!-->
<!-- <link rel="stylesheet" type="text/css"   href="/seeyon/skin/dist/common/special-notIE8.css?V=V9_0SP1_250518_2028260"> -->
<!--<![endif]-->

<script type="text/javascript">;var _ctpSkinUrl='/skin/dist/components/components_theme_default.css';</script>

<link rel="stylesheet" href="/seeyon/skin/dist/min/skinComponents-defaultTheme-min.css?V=V9_0SP1_250518_2028260">

<link rel="stylesheet" href="/seeyon/skin/xicon/xicon-min.css?V=V9_0SP1_250518_2028260">

<link rel="stylesheet" type="text/css"  href="/seeyon/common/css/ddPeoCardSelect2Animate.css?V=V9_0SP1_250518_2028260" />
<!--[if lt IE 9]>
<link rel="stylesheet" theme-data type="text/css"   href="/seeyon/common/ctpUi/dist/css/ctpUi4IE8.css?V=V9_0SP1_250518_2028260">
<![endif]-->
<!--[if (!IE)|(gte IE 9)]><!-->
<link rel="stylesheet" theme-data type="text/css"   href="/seeyon/common/ctpUi/dist/css/ctpUi.css?V=V9_0SP1_250518_2028260">
<!--<![endif]-->

<script type="text/javascript">
  var isSecretLevelEnable = false;
  var CSRFTOKEN = 'null';
  var APPID = '4095';
  var _ctxPath = '/seeyon', _ctxServer = 'http://*:80/seeyon';
  var _staticPath = _ctxPath;
  var _staticSuffix = '?V=V9_0SP1_250518_2028260';
  var _locale = 'zh_CN',_isDevelop = false,_sessionid = '',_isModalDialog = false;
  var _editionI18nSuffix = '';
  
  var _resourceCode = "";
  var seeyonProductId="14";
  
</script>

 <script type="text/javascript">
 var setFlag=false;
  if(typeof Set!="undefined"){
     var SysSet=Set;
     setFlag=true;
  }
 </script>

<script type="text/javascript" charset="UTF-8" src="/seeyon/common/js/polyfill.min.js?V=V9_0SP1_250518_2028260"></script>

<script type="text/javascript" src="/seeyon/common/all-min.js?V=V9_0SP1_250518_2028260"></script>
<script type="text/javascript" src="/seeyon/common/ctpUi/dist/js/ctpUi.min.js?V=V9_0SP1_250518_2028260"></script>

<script type="text/javascript" src="/seeyon/common/js/ui/calendar/calendar-language.js?V=V9_0SP1_250518_2028260"></script>

<script type="text/javascript" src="/seeyon/main.do?method=headerjs&login=*"></script>
<script type="text/javascript">

var addinMenus = new Array();

$.ctx._currentPathId = 'bulData_bulView';
$.ctx._pageSize = 20;

$.ctx._emailShow = true;
$.ctx.fillmaps = null;

$.releaseOnunload();
</script>

<script type="text/javascript" src="/seeyon/ajaxStub.js?v=*"></script>

<html>
<head>
  <meta charset="utf-8">
  <title>*</title>
  <link rel="stylesheet" type="text/css" href="/seeyon/skin/dist/modules/bulletin.css?V=V9_0SP1_250518_2028260" theme-data/>
  <script type="text/javascript" charset="UTF-8">if (window.skinStyleHandler) {window.skinStyleHandler({variables: window.themeCssVariables || {}});}</script>
  <script type="text/javascript" src="/seeyon/common/waterMark/js/waterMark.js?V=V9_0SP1_250518_2028260"></script>
  <script type="text/javascript" src="/seeyon/apps_res/bulletin/js/common.js?V=V9_0SP1_250518_2028260"></script>
  <script type="text/javascript" src="/seeyon/apps_res/bulletin/js/index.js?V=V9_0SP1_250518_2028260"></script>
  <script type="text/javascript" charset="UTF-8" src="/seeyon/common/office/js/baseOffice.js?V=V9_0SP1_250518_2028260"></script>
  <script type="text/javascript" src="/seeyon/apps_res/bulletin/js/bulView.js?V=V9_0SP1_250518_2028260"></script>
  <script type="text/javascript" src="/seeyon/apps_res/doc/js/docFavorite.js?V=V9_0SP1_250518_2028260"></script>
  
  <script type="text/javascript" src="/seeyon/common/ofd/js/baseOfd.js?V=V9_0SP1_250518_2028260"></script>
  
  <script type="text/javascript" charset="UTF-8" src="/seeyon/apps_res/uc/rongcloud/chat.js?V=V9_0SP1_250518_2028260"></script>
  <script>
<!--
var theToShowAttachments = new ArrayList();
var downloadURL = "/seeyon/fileUpload.do";
//-->
</script>
<div style="display:none;">
<iframe name="downloadFileFrame" id="downloadFileFrame" frameborder="0" width="0" height="0"></iframe>
</div>

  <script type="text/javascript">
        var _isOfficeTrans = true;
    var isGuest =false;
    var bulId = "*";
    var  isEnableWPSOnlineView = false;
    var  isEnableDcsOnlineView = false;
    //dengwei  OA-229275 龙芯-点聚阅读器-查看pdf正文未展示,另存为报错 start
    var ext5 = "";
    //dengwei  OA-229275 龙芯-点聚阅读器-查看pdf正文未展示,另存为报错 end
    //dengwei OA-229889 windows+wps:公告,,word转ofd的正文,点击打印,报js错误 start
    var cebType = "";
    //dengwei OA-229889 windows+wps:公告,,word转ofd的正文,点击打印,报js错误 end
    var bulStyle = parseInt("2");
    var dataFormat_Type = "Pdf";
    var bul_ext4 = false;
    var _spaceType = '';
    var _spaceId = '';
    var wi = "";
    var waterMarkBase64 = "";
    var editType = "4,0";
    var officecanPrint = "false";
    var officecanSaveLocal = "false";
    window.top.pageName = 'bul';
    var useWebOffice=false;
    // 互联互通打印依赖参数
    var printParamsObj = {
        dataFormat:'Pdf',
        isChangedPdf:'true',
        isForm:false,
        content:'*',
        createDate:'2025-01-13 17:54',
        userId:'',
        ext:'false',
        title:'*'
    };

    function initOfficeSucessCallback(res) {
      if (typeof (res) != "undefined" && res != null){
        if (res.code === 1){
          $.alert(res.msg);
        }
      }
    }
    window.onload = function () {
      if (typeof (OfficeAPI) != "undefined") {
        OfficeAPI.setOptions({
          fileName: "*",
          editType: editType,
          officecanPrint: officecanPrint,
          officecanSaveLocal: officecanSaveLocal
        });
      }
    }
    
    //屏蔽鼠标右键
    window.document.oncontextmenu = function () {
      return false;
    }
    //禁止用户选择并复制数据
    window.document.onselectstart = function () {
      return false;
    }
    
    //返回首页(集团和单位空间下的处于发布状态下的公告)
    function toIndex() {
      if (('2' == '2' || '2' == '3') && '30' == '30' && 'myCollect' != 'pigeonhole') {
        var url = '/seeyon/bulData.do?method=bulIndex';
        window.location.href = url + CsrfGuard.getUrlSurffix();
      }
    }
  </script>
  <style>
    #content {border: none;}
    
    #content:hover {border: none;}
    
    .set_color_gold {color: #fab715;}
    table#col-contentTable tr td span.browse_class span[data-role], table#col-contentTable tr td span.browse_class span.xdRichTextBox {
      min-height: 23px !important;;
      white-space: pre-wrap !important;;
      height: auto !important;;
    }
    table#col-contentTable tr td table.xdLayout tr td span.browse_class span[data-role], table#col-contentTable tr td table.xdLayout tr td span.browse_class span.xdRichTextBox {
      /* width: 95% !important; */
    }
    #htmlContentDiv ul,
    #htmlContentDiv ol{
      font-size:16px;
    }
  </style>
  <script type="text/javascript">
    //表单签章相关,hw.js中需要用到
    var hwVer = '6,7,0,514';
    var _ctxPath = '/seeyon', _ctxServer = 'http://*:80/seeyon';
    var htmOcxUserName = $.ctx.CurrentUser.name;
  </script>
  <SCRIPT language=javascript for=SignatureControl event=EventOnSign(DocumentId,SignSn,KeySn,Extparam,EventId,Ext1)>
    //作用:重新获取签章位置
    if (EventId = 4) {
      CalculatePosition();
      SignatureControl.EventResult = true;
    }
  </SCRIPT>
  
  <style>html, body, div, img {
    -webkit-user-drag: none;
    user-drag: none;
  }</style>
</head>
<body>
<input type="hidden" id="subject" name="subject" value="*">
<div id="2">
    <div class="container bulentin" id="container1">
        <div class="container_top">
            <div class="bulentinLOG">
                <span class="index_Logo" title="返回首页" onclick="toIndex();" style="cursor: pointer;">
                    <img src="/seeyon/skin/dist/images/cultural/bulletin/notice_log1.png">
                    <span class="bulentinLOG_Text">公告</span>
                </span>
            </div>
        </div>
        
        <div class="content_container" id="content_container1">
            <div class="container_auto" id="page_height1">
                <div class="all_content" style="" id = "page_height2">
                    <div class="container_discuss detail moduleTWO" id="page_height" style=";">
                        <div class="discuss_left" style=";">
                            <div class="left_list">
                                <div class="mainText_head" id="head_height">
                                </div>
                                <div class="mainText_body">
                                    <div>
                                        <div id="contentTD" style="width:100%;height:768px;padding-bottom: 6px;text-align: left;" valign="top" colspan="6">
                                            <div id="mainbodyDiv" style="height:100%">
                                                <div style='display:none'>
                                                    <input type='hidden' name='bodyType' id='bodyType' value='Pdf'>
                                                    <input type="hidden" name="bodyCreateDate" value="2025-01-13 17:54:20">
                                                </div>
                                                <input id="contentNameId" type="hidden" name="contentName" value="">
                                                <script type="text/javascript">var currUserName = "*"</script>
                                                <script>
                                                    userId='*';
                                                    officeOcxUploadMaxSize='51200';
                                                    realSize="";
                                                    fileType="pdf";
                                                    createDate="2025-01-13 17:54:20";
                                                    sessionId="*";
                                                    var ofdReader="null";
                                                </script>
                                                <table align="center" border="0" height="100%" cellspacing="0" cellpadding="0" class="body-detail-office">
                                                  <tr>
                                                     <td height="100%">
                                                        <div id="officeFrameDiv" style="height:100%;display:none">
                                                          <iframe src="" name="officeEditorFrame" id="officeEditorFrame" frameborder="0" width="100%" height="100%"></iframe>
                                                        </div>
                                                        <script type="text/javascript" src="/seeyon/common/js/polyfill.min.js?V=*"></script>
                                                        <script type="text/javascript" src="/seeyon/common/office/js/officeSDK.js?V=*"></script>
                                                        <script>
                                                            var webRoot="http://*:80/seeyon";
                                                            var fileId="*";
                                                            var createDate="2025-01-13 17:54:20";
                                                            var category = "";
                                                            var editType="0,0";
                                                            var ocxVer="6,0,2,312";
                                                            var originalFileId = "";
                                                            var needReadFile = "true";
                                                            var currUserName="*";
                                                            var lastUpdateTime="*";
                                                            var officeOcxUploadMax=51200;
                                                            var pdfVer="9,0,0,1666";
                                                            var officeFileRealSize="";
                                                            var hasAdvanceOcx=true;
                                                            var isRetainedTraces="false";
                                                            var userId="*";
                                                            var isLoadOfficeImmediate="true";
                                                            var sessionId="*";
                                                            var ofdReader="null";
                                                            var officecanPrint="false";
                                                            var officecanSaveLocal="false";
                                                            var saveLocalFileName="*";
                                                            v3x.loadLanguage("/common/office/js/i18n");
                                                        </script>
                                                     </td>
                                                  </tr>
                                                </table>
                                                <script>
                                                    var officeParams = {
                                                                webRoot: "http://*:80/seeyon",
                                                                fileId: "*",
                                                                fileType: "pdf",
                                                                createDate: "2025-01-13 17:54:20",
                                                                editType: "0,0",
                                                                originalFileId: "" ,
                                                                needReadFile: true,
                                                                currUserName: "*",
                                                                lastUpdateTime: "*",
                                                                officeOcxUploadMaxSize: 51200,
                                                                canPrint: "false",
                                                                officecanPrint: "false",
                                                                officecanSaveLocal: "false",
                                                                loadOfficeImmediate:"true",
                                                                fileName:"*"
                                                            };
                                                    officeParams.canPrint = "false";
                                                    officeParams.officecanPrint = "false";
                                                    officeParams.officecanSaveLocal = "false";
                                                    officeParams.canDownload = "false";
                                                    officeParams.isNeedCheckSignDownload = "true";
                                                    officeParams.fileName = '*';

                                                    var _officeSDK = new OfficeSDK(officeParams);
                                                    var editType = officeParams.editType;
                                                    var mode = 'VIEW';
                                                    if (editType === '1,0' || editType === '2,1') {
                                                       mode = 'EDIT';
                                                    }
                                                    _officeSDK.init({
                                                        webRoot: officeParams.webRoot,
                                                        domId: 'officeFrameDiv',
                                                        mode: mode,
                                                        fileType: 'pdf'
                                                    }, function (err, result) {
                                                     if (err != null) {
                                                      if ($ && $.alert) {
                                                        $.alert('当前配置,不支持操作pdf类型文件');
                                                      } else {
                                                        alert('当前配置,不支持操作pdf类型文件');
                                                      }
                                                      return;
                                                     } else {
                                                    var targetDom = document.querySelector("#officeFrameDiv");
                                                    targetDom.style.display = "block";
                                                    targetDom.style.overflow = "hidden";
                                                    _officeSDK.openFile({},);
                                                     }
                                                    });
                                                </script>
                                            </div>
                                            
                                            <script type="text/javascript">
                                              // pdf 控制参数
                                              officecanPrint = "false";
                                              officecanSaveLocal = "false";
                                              editType = "0,0";
                                              // office 控制参数
                                              if(typeof(OfficeAPI) != "undefined"){
                                                  OfficeAPI.setOptions({
                                                      officecanPrint : "false",
                                                      officecanSaveLocal : "false",
                                                      editType : editType
                                                  });
                                              }
                                            </script>
                                            
                                            <script type="text/javascript">
                                                // office 控制不显示花脸,保存本地时强制清除痕迹
                                                if(typeof(OfficeAPI) != "undefined"){
                                                    OfficeAPI.setOptions({
                                                        showHl : "false",
                                                        isRemoveTrace : "true"
                                                    });
                                                }
                                            </script>
                                            <style>
                                                .contentText p{
                                                    font-size:16px;
                                                    line-height: 1.6;
                                                    word-break: break-all !important;
                                                    word-wrap: break-word !important;
                                                }
                                                .contentText ul,.contentText ol {
                                                    padding-left: 40px;
                                                }
                                                .contentText ul li {
                                                  list-style: disc;
                                                  font-size: 16px;
                                                  word-break: break-word !important;
                                                  word-wrap: break-word !important;
                                                }
                                                .contentText ol li {
                                                  list-style: decimal;
                                                  font-size: 16px;
                                                  word-break: break-word !important;
                                                  word-wrap: break-word !important;
                                                }
                                                .mainText_body table{
                                                    width:100%;
                                                }
                                            </style>
                                        </div>
                                    </div>
                                </div>
                                <div class="mainText_foot" id="foot_height" style="width:100%">
                                    <div class="padding30" style="display: none;padding: 5px 0px 5px 0px" id="attachmentTRAttFile">
                                        <div class="atts-label" >
                                            <span class="left label_num">
                                                <em class="icon16 file_attachment_16"></em>&nbsp;(<span id="attachmentNumberDivAttFile"></span>)
                                            </span>
                                            <div id="attFileDomain" isGrid="true" class="comp" comp="type:'fileupload',attachmentTrId:'AttFile',canFavourite:false,applicationCategory:'8',canDeleteOriginalAtts:false" attsdata=''></div>
                                        </div>
                                    </div>
                                    
                                    <div class="padding30" style="display: none; padding: 5px 0px 5px 0px" id="attachment2TRDoc1">
                                        <div class="atts-label">
                                            <span class="left label_num">
                                                <em class="icon16 relation_file_16"></em>&nbsp;(<span id="attachment2NumberDivDoc1"></span>)
                                            </span>
                                            <div id="assDocDomain" isCrid="true" class="comp" comp="type:'assdoc',attachmentTrId:'Doc1',applicationCategory:'8',modids:8,canDeleteOriginalAtts:false" attsdata=''></div>
                                        </div>
                                    </div>
                                    
                                </div>
                            </div>
                        </div>
                        <div class="module_Two">
                        </div>
                        <div class="attrNotice overflow" id="foot2_height">
                            <div class="mainText_head_msg" style="height:100%">
                                <div class="attrMsg"><span>属性信息 :</span>
                                </div>
                                <div class="left Wopacity mainText_head_msgSpan ">
                                    <span  class="pubMsg">
                                        <em class="pubMsg_bar">公告标题 :</em>
                                        <span class="pubMsg_span" title="*">*</span>
                                    </span>
                                    
                                    <span  class="pubMsg">
                                        <em class="pubMsg_bar">发布范围 :</em>
                                        <span class="pubMsg_span" title="*">*</span>
                                    </span>
                                    
                                    <span  class="pubMsg">
                                        <em class="pubMsg_bar">发布时间 :</em>
                                        <span class="pubMsg_span">2025-01-13 08:42</span>
                                    </span>
                                    
                                    <span  class="pubMsg">
                                        <em class="pubMsg_bar">发布部门 :</em>
                                        <span class="pubMsg_span" title="*">*</span>
                                    </span>
                                    <span  class="pubMsg">
                                        <em class="pubMsg_bar">阅读量 :</em>
                                        <span class="pubMsg_span">*</span>
                                    </span>
                                </div>
                            </div>
                            
                            <div class="setBtn" id="noprint">
                                <span class="">
                                    <span class="head_title_collect hand">
                                        <span id="article_fav_more" onclick="javaScript:cancelFavorite_bul('*')">
                                            <em id = "article_fav" class="ico16 stored_16 " ></em>
                                            <span class="Wopacity collect" >取消收藏</span>
                                        </span>
                                    </span>
                                    
                                    <span class="head_title_print hand" onclick="showReadList1('*','查看阅读信息' ,'false' )">
                                        <em class="icon16 discuss_click_current_16"></em>
                                        <span class="Wopacity">查看阅读信息</span>
                                    </span>
                                    
                                    <span class="hand" id="viewOriginalContentA" onclick="popupContentWin('*','Pdf','2025-01-13 17:54:20.573','','false','*')">
                                        <span class="syIcon sy-call_template set_color_gold" style=""></span>
                                        <span class="Wopacity">查看原文档</span>
                                    </span>
                                </span>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <div class="to_top" id="back_to_top">
        <span class="scroll_bg">
            <em class="to_top_24" title="返回顶部"></em>
        </span>
    </div>
</div>
</body>
<script type="text/javascript">
  //页面大小改变的时候移动ISignatureHTML签章对象,让其到达正确的位置
</script>
</html>

技术原理

基于致远互联(Seeyon)V9.0SP1版本封装的SuwellLightRead技术,其PDF预览页面采用"前端SDK管控-后端鉴权分发-阅读器渲染"的三层架构设计,核心目标是在保障文件安全的前提下提供预览能力。原始PDF文件的提取方法,本质是通过解析该架构的接口交互逻辑,复用合法会话权限,绕过前端功能限制,直接获取后端分发的原始文件下载链路,具体技术原理如下:

SuwellLightRead技术的PDF预览原生架构

SuwellLightRead作为轻量级PDF阅读组件,其预览流程依赖前端与后端的紧密协作,核心交互链路具有明确的分层特征:

  • 前端初始化与配置层 :页面加载时,通过OfficeSDK实例化核心配置,传入webRoot(系统根路径)、fileId(文件唯一标识)、fileType=pdf等必要参数,同时设置功能权限开关(officecanPrint: "false"officecanSaveLocal: "false"),禁用下载、打印等敏感操作。初始化后通过_officeSDK.init()绑定DOM容器,指定预览模式为VIEW,完成前端渲染准备。
  • 后端鉴权与资源分发层 :调用_officeSDK.openFile()触发后端请求,前端通过fetchXHR/seeyon/rest/skResource/sk/file/info接口发送请求,携带seed(随机种子)、tko=SuwellLightReadHeart(组件标识)、tolen(动态鉴权令牌)等核心参数,以及permissionJson权限配置(原生配置中download:0明确禁用下载权限)。后端校验会话合法性、参数有效性后,返回文件预览所需的元信息与资源地址,而非原始文件流。
  • 阅读器渲染层:前端通过内置阅读器(集成PDF解析内核)加载后端返回的预览资源,将文件流转换为可视化页面,全程屏蔽原始文件的直接访问链路,仅提供只读预览能力。
原始PDF文件提取的核心技术逻辑

提取方法的核心是利用架构设计中的"前端功能限制与后端鉴权松耦合"特性,通过请求拦截、参数解析、会话复用三步实现原始文件获取:

  • 请求拦截与核心响应捕获 :通过重写window.fetchXMLHttpRequest.prototype.open方法,监听前端与后端的交互请求,筛选包含"url"关键字的响应数据(此类响应中隐含中间链路fileInfoUrl)。拦截过程中克隆响应流,避免影响原生预览功能,同时记录响应的原始内容、状态码等关键信息,无需二次请求即可获取核心数据。
  • 中间链路解析与鉴权参数复用 :从捕获的响应中解析fileInfoUrl,该URL是后端为阅读器提供的资源访问中间入口,包含fileIdseedtolen等完整鉴权参数。由于后端仅校验参数合法性与会话有效性,未强制绑定请求发起方(前端阅读器或自定义脚本),因此可直接复用该URL进行后续请求。
  • 会话权限复用与下载地址获取 :向fileInfoUrl发送请求时,通过credentials: 'include'参数携带当前用户的合法会话Cookie,复用预览页面的访问权限。后端校验通过后,返回包含download_url的完整文件信息,该地址指向/seeyon/rest/office/file/oa/download接口,携带动态鉴权参数,直接访问即可获取原始PDF文件流,实现提取目的。
关键技术依赖与约束条件

提取操作的可行性依赖特定技术环境与权限边界,核心约束如下:

  • 技术依赖 :依赖前端JavaScript的原生API重写能力,需确保_officeSDK实例已初始化(页面加载完成后执行脚本);依赖后端对会话Cookie的信任机制,未额外校验请求的来源上下文(如Referer、请求头特征)。
  • 权限边界约束 :提取操作必须基于合法用户会话,需先登录系统并拥有目标PDF的预览权限,无有效会话时,fileInfoUrldownload_url的请求均会被后端拒绝。
  • 动态参数约束seedtolen为一次性动态鉴权参数,由后端实时生成并与当前会话绑定,过期或伪造参数会导致鉴权失败,需从当前页面的实时请求中拦截获取。
  • 接口兼容性约束 :提取逻辑依赖/seeyon/rest/skResource/sk/file/info/seeyon/rest/office/file/oa/download接口的固定路径与响应格式,接口路径或参数结构变更需要更新对应目标定位特征。

通用方法:手动提取原始PDF地址的步骤

在 SuwellLightRead 技术封装的 PDF 预览页面中,尽管前端界面明确禁用下载功能(如 permissionJson"download":0),但原始 PDF 文件的访问地址仍可通过浏览器开发者工具手动提取。该过程无需执行任何脚本,仅依赖对网络请求的观察与分析,适用于所有已通过身份认证、具备合法会话的用户。

由于实际加载流程中,系统先发起 /VIEW/url 的 XHR 请求获取阅读器配置,再加载 /web-reader/reader 页面,因此以下方法按真实请求顺序组织,并将共用逻辑抽象为前置与后置步骤,以提升操作效率和清晰度。

前提条件
  • 用户已成功登录致远 OA 系统;
  • 已打开目标 PDF 文档的预览页面(通常为公告、公文或附件详情页);
  • 浏览器支持开发者工具(如 Chrome DevTools、Edge DevTools 等);
  • 预览页面使用的是 SuwellLightRead 阅读器(URL 中包含 /web-reader/reader 或调用 _officeSDK.openFile)。
前置步骤:捕获 fileInfo 接口地址

无论采用哪种路径,最终目标都是获取形如以下格式的 fileInfo 接口 URL

复制代码
http://*:80/seeyon/rest/skResource/sk/file/info?v=...&fileId=...&seed=...&tolen=...&m=s

该接口是后续提取下载地址的关键入口。可通过以下任一方式获得:

方式 A:从 XHR 请求响应中直接提取(推荐,优先发生)
  1. 打开浏览器开发者工具(F12),切换至 Network(网络) 面板,勾选 Preserve log

  2. 刷新预览页面,在请求列表中查找 XHR/Fetch 类型的请求,其 URL 为:

    复制代码
    http://*/seeyon/rest/skResource/VIEW/url
  3. 点击该请求,查看 Response 内容,定位字段:

    json 复制代码
    {
      "data": {
        "fileInfoUrl": "http://*:80/seeyon/rest/skResource/sk/file/info?..."
      }
    }
  4. 复制 fileInfoUrl 的完整值,即为目标 fileInfo 接口地址。

方式 B:从阅读器页面 URL 参数中解码提取(备选,稍后发生)
  1. 在 Network 面板中查找类型为 Document 的请求,其 URL 形如:

    复制代码
    http://*:81/web-reader/reader?file=...
  2. 复制整个 URL,提取 file= 后的参数值;

  3. 对该值执行 URL 解码 (例如在浏览器控制台运行):

    js 复制代码
    decodeURIComponent("http%3A%2F%2F172...")
    // 如果存在问题,可以尝试2次解码
    // decodeURIComponent(decodeURIComponent("http%3A%2F%2F172..."))
  4. 解码结果即为完整的 fileInfo 接口地址。

建议优先使用方式 A,因其发生在页面渲染前,响应结构清晰,且无需处理嵌套编码。

后置处理步骤:从 fileInfo 接口获取并下载原始 PDF

一旦获得有效的 fileInfo 接口地址,后续操作完全一致:

  1. 访问 fileInfo 接口

    在新浏览器标签页中粘贴并打开该 URL。由于当前会话 Cookie 有效,服务器将返回 JSON 响应。

  2. 提取 download_url

    在响应体中定位如下字段:

    json 复制代码
    {
      "file": {
        "download_url": "http://*:80/seeyon/rest/office/file/oa/download?_=...&fileId=...&seed=...&tolen=...&m=s"
      }
    }

    download_url 即为原始 PDF 文件的直接下载链接。

  3. 下载文件

    • download_url 在新标签页中打开;
    • 浏览器将自动触发 PDF 文件下载;
    • 若页面空白或报错,请检查:
      • 当前会话是否仍有效(重新登录可刷新 token);
      • URL 是否完整复制(尤其注意 &? 的完整性);
注意事项
  • 时效性fileInfoUrldownload_url 中的 seedtolen 参数通常仅在几分钟内有效,建议在页面加载完成后立即操作。
  • 会话绑定:所有请求必须在同一浏览器会话下完成,跨设备或清除 Cookie 将导致 403 错误。
  • 权限限制:该方法仅适用于用户本身拥有"阅读"权限的文档,无法绕过后端权限校验。
  • 双重编码 :若使用方式 B,务必进行 decodeURIComponent,否则参数解析错误将导致 400 响应。

自动化提取脚本实战

提取PDF脚本
js 复制代码
// 核心:捕获含"url"的请求响应 → 直接解析fileInfoUrl → 请求fileInfoUrl → 生成新页面打开的下载按钮
(async function() {
    // ========== 全局变量初始化 ==========
    let firstMatchedResponse = null; // 直接记录第一个含"url"请求的响应内容
    let fileInfoUrl = ''; // 解析出的fileInfoUrl
    let downloadUrl = ''; // 最终下载地址
    const FILTER_KEY = 'url'; // 仅匹配含"url"的请求
    let isFirstMatched = false; // 标记是否已捕获第一个匹配请求

    // ========== 第一步:监听请求并直接记录响应内容 ==========
    // 1. 拦截fetch请求(捕获响应内容)
    const originalFetch = window.fetch;
    window.fetch = async function(url, options) {
        const fetchUrl = typeof url === 'string' ? url : url.href;
        // 仅处理含"url"的请求,且只记录第一个
        if (fetchUrl.includes(FILTER_KEY) && !isFirstMatched) {
            console.log(`📤 捕获到第一个含"${FILTER_KEY}"的fetch请求:`, fetchUrl);
            try {
                // 执行原生fetch并获取响应
                const response = await originalFetch.apply(this, arguments);
                // 克隆响应(避免原响应被消费)
                const responseClone = response.clone();
                // 获取原始响应文本(兼容非JSON格式)
                const rawText = await responseClone.text();
                // 记录第一个匹配请求的完整响应信息
                firstMatchedResponse = {
                    url: fetchUrl,
                    status: response.status,
                    statusText: response.statusText,
                    rawResponse: rawText, // 直接记录响应内容,无需二次请求
                    isJson: false
                };
                // 尝试解析JSON(容错)
                try {
                    firstMatchedResponse.jsonResponse = JSON.parse(rawText);
                    firstMatchedResponse.isJson = true;
                } catch (e) {
                    firstMatchedResponse.jsonResponse = null;
                    firstMatchedResponse.parseError = '响应不是合法JSON格式';
                }
                isFirstMatched = true; // 标记已捕获第一个
                console.log('%c✅ 已记录第一个匹配请求的响应内容', 'font-size:14px; color:purple; font-weight:bold;');
                return response;
            } catch (err) {
                console.error(`❌ 第一个含"${FILTER_KEY}"的fetch请求失败:`, err);
                firstMatchedResponse = { url: fetchUrl, error: err.message };
                isFirstMatched = true;
            }
        }
        // 非匹配请求/已捕获第一个,直接执行原生fetch
        return originalFetch.apply(this, arguments);
    };

    // 2. 拦截XHR请求(捕获响应内容,兼容老式请求)
    const originalXhrOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        // 仅处理含"url"的请求,且只记录第一个
        if (url.includes(FILTER_KEY) && !isFirstMatched) {
            console.log(`📤 捕获到第一个含"${FILTER_KEY}"的XHR请求:`, url);
            // 监听XHR加载完成事件,记录响应
            this.addEventListener('load', () => {
                if (isFirstMatched) return; // 已记录第一个则跳过
                // 记录XHR响应内容
                firstMatchedResponse = {
                    url: url,
                    status: this.status,
                    statusText: this.statusText,
                    rawResponse: this.responseText, // 直接记录响应内容
                    isJson: false
                };
                // 尝试解析JSON
                try {
                    firstMatchedResponse.jsonResponse = JSON.parse(this.responseText);
                    firstMatchedResponse.isJson = true;
                } catch (e) {
                    firstMatchedResponse.jsonResponse = null;
                    firstMatchedResponse.parseError = '响应不是合法JSON格式';
                }
                isFirstMatched = true;
                console.log('%c✅ 已记录第一个匹配请求的响应内容', 'font-size:14px; color:purple; font-weight:bold;');
            });
            // 监听XHR错误
            this.addEventListener('error', () => {
                if (isFirstMatched) return;
                firstMatchedResponse = { url: url, error: 'XHR请求失败' };
                isFirstMatched = true;
            });
        }
        return originalXhrOpen.apply(this, arguments);
    };

    // ========== 第二步:执行openFile触发请求 ==========
    console.log('✅ 已启动请求监听(直接记录响应内容),执行openFile...');
    if (!window._officeSDK) {
        alert('❌ 错误:未找到_officeSDK实例,请确保OfficeSDK已初始化!');
        // 还原原生方法
        window.fetch = originalFetch;
        XMLHttpRequest.prototype.open = originalXhrOpen;
        return;
    }

    try {
        await window._officeSDK.openFile({},);
        console.log('✅ _officeSDK.openFile 执行完成');
    } catch (err) {
        alert(`❌ openFile执行失败:${err.message}`);
        window.fetch = originalFetch;
        XMLHttpRequest.prototype.open = originalXhrOpen;
        return;
    }

    // ========== 第三步:解析fileInfoUrl(直接用记录的响应,无需二次请求) ==========
    setTimeout(async () => {
        // 还原原生方法
        window.fetch = originalFetch;
        XMLHttpRequest.prototype.open = originalXhrOpen;

        // 1. 检查是否捕获到第一个匹配请求的响应
        if (!firstMatchedResponse) {
            alert('❌ 未捕获到任何包含"url"的请求响应!');
            return;
        }

        // 打印第一个匹配请求的响应详情(供核实)
        console.log('%c==================== 第一个匹配请求的响应详情 ====================', 'font-size:16px; color:purple; font-weight:bold;');
        console.log('请求URL:', firstMatchedResponse.url);
        console.log('响应状态码:', firstMatchedResponse.status);
        console.log('响应原始内容:', firstMatchedResponse.rawResponse);
        console.log('是否为JSON格式:', firstMatchedResponse.isJson);
        if (firstMatchedResponse.parseError) {
            console.log('JSON解析错误:', firstMatchedResponse.parseError);
        }
        console.log('%c================================================================', 'font-size:16px; color:purple; font-weight:bold;');

        // 2. 直接从响应内容解析fileInfoUrl(无需二次请求)
        if (!firstMatchedResponse.isJson || !firstMatchedResponse.jsonResponse) {
            alert('❌ 第一个匹配请求的响应不是JSON格式,无法解析fileInfoUrl!');
            return;
        }
        // 提取fileInfoUrl并打印(供核实)
        fileInfoUrl = firstMatchedResponse.jsonResponse.data?.fileInfoUrl || '';
        console.log('%c==================== 解析出的fileInfoUrl ====================', 'font-size:16px; color:orange; font-weight:bold;');
        console.log('fileInfoUrl字段内容:', fileInfoUrl || '未找到');
        console.log('%c============================================================', 'font-size:16px; color:orange; font-weight:bold;');
        
        if (!fileInfoUrl) {
            alert('❌ 从响应中未解析到fileInfoUrl字段!');
            return;
        }

        // ========== 第四步:请求fileInfoUrl(携带Cookie)并打印详情 ==========
        try {
            console.log('📡 开始请求fileInfoUrl(携带本地Cookie)...');
            // 请求fileInfoUrl(携带Cookie)
            const fileInfoResponse = await fetch(fileInfoUrl, {
                credentials: 'include', // 关键:携带Cookie
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                }
            });

            // 打印fileInfoUrl请求的完整信息
            console.log('%c==================== fileInfoUrl请求详情 ====================', 'font-size:16px; color:green; font-weight:bold;');
            console.log('请求的完整URL:', fileInfoUrl);
            // 解析URL参数
            try {
                const urlObj = new URL(fileInfoUrl);
                const params = Object.fromEntries(urlObj.searchParams.entries());
                console.log('URL参数列表:', params);
                console.log('参数数量:', Object.keys(params).length);
            } catch (e) {
                console.log('URL解析失败(非合法URL):', e.message);
            }
            console.log('响应状态码:', fileInfoResponse.status);
            console.log('响应状态文本:', fileInfoResponse.statusText);
            // 打印原始返回内容(兼容非JSON格式)
            const fileInfoRawText = await fileInfoResponse.text();
            console.log('响应原始内容:', fileInfoRawText);
            // 尝试解析JSON(容错)
            let fileInfoResult = null;
            try {
                fileInfoResult = JSON.parse(fileInfoRawText);
                console.log('响应JSON解析结果:', fileInfoResult);
            } catch (e) {
                console.log('JSON解析错误:', e.message);
            }
            console.log('%c================================================================', 'font-size:16px; color:green; font-weight:bold;');

            // 3. 提取download_url
            if (fileInfoResult && fileInfoResult.file?.download_url) {
                downloadUrl = fileInfoResult.file.download_url;
                console.log('%c🎉 最终下载地址:', 'font-size:16px; color:red; font-weight:bold;', downloadUrl);
            } else {
                alert('❌ 从fileInfoUrl响应中未解析到download_url!');
                return;
            }

            // ========== 第五步:生成下载按钮(核心修复:改为a标签,新页面打开) ==========
            // 移除页面已存在的同名按钮(避免重复)
            const oldBtn = document.querySelector('#pdfDownloadBtn');
            if (oldBtn) oldBtn.remove();

            // 创建a标签(模拟按钮样式,本质是跳转链接)
            const downloadLink = document.createElement('a');
            downloadLink.id = 'pdfDownloadBtn';
            downloadLink.innerText = '📥 打开下载地址'; // 文案改为更贴合的描述
            downloadLink.href = downloadUrl; // 设置跳转地址
            downloadLink.target = '_blank'; // 强制在新页面打开
            downloadLink.rel = 'noopener noreferrer'; // 安全优化
            // 按钮样式(和之前一致,保持醒目)
            downloadLink.style.cssText = `
                position: fixed;
                top: 20px;
                right: 20px;
                padding: 12px 24px;
                background: #4444ff;
                color: white;
                border: none;
                border-radius: 8px;
                font-size: 16px;
                font-weight: bold;
                cursor: pointer;
                z-index: 9999;
                box-shadow: 0 4px 8px rgba(0,0,0,0.3);
                transition: background 0.3s;
                text-decoration: none; // 去掉a标签默认下划线
                display: inline-block; // 适配padding样式
            `;
            // 悬浮效果
            downloadLink.onmouseover = () => {
                downloadLink.style.background = '#6666ff';
            };
            downloadLink.onmouseout = () => {
                downloadLink.style.background = '#4444ff';
            };
            // 添加到页面
            document.body.appendChild(downloadLink);

            // ========== 第六步:人性化弹窗提示 ==========
            alert(`🎉 操作成功!
1. 已获取到下载地址(控制台可查看);
2. 页面右上角【打开下载地址】按钮可在新页面打开地址;
3. 若新页面未弹出,请关闭浏览器弹窗拦截后重试;`)
        } catch (reqErr) {
            alert(`❌ 请求fileInfoUrl失败:${reqErr.message}`);
            console.error('%c❌ 请求错误详情:', 'font-size:14px; color:red; font-weight:bold;', reqErr);
        }

    }, 5000); // 5秒延迟确保请求完成并记录响应
})();
脚本使用方法

该脚本为通用化的PDF下载地址自动提取工具,基于浏览器前端环境运行,无需额外部署依赖,仅需在目标PDF预览页面执行即可完成下载地址提取,具体使用流程如下:

前置条件
  1. 环境要求:使用Chrome、Firefox、Edge等现代浏览器(IE浏览器不兼容ES6语法,无法运行);
  2. 会话有效性 :已登录目标系统,且能正常打开PDF预览页面(页面可显示PDF内容,证明_officeSDK实例已初始化);
  3. 权限基础:拥有该PDF的预览权限(无预览权限时脚本无法捕获有效请求)。
核心操作步骤
  1. 打开浏览器开发者工具控制台

    1. 进入PDF预览页面,确保页面已完全加载(PDF内容正常显示);
    2. 按下快捷键 F12Ctrl+Shift+I(Mac系统为 Cmd+Opt+I)打开开发者工具;
    3. 在开发者工具面板中,切换至 Console(控制台) 标签页,清空原有日志(可选,点击面板左上角清空按钮)。
  2. 复制并执行脚本

    1. 全选本脚本的所有代码内容,使用 Ctrl+C(Mac为 Cmd+C)复制;
    2. 将复制的脚本粘贴到Console面板的输入框中,按下回车键执行脚本;
    3. 脚本启动后,控制台会输出 ✅ 已启动请求监听(直接记录响应内容),执行openFile... 日志,表明脚本开始运行。
  3. 等待脚本自动完成解析

    1. 脚本内置5秒延迟(确保请求完全捕获),期间控制台会依次输出以下关键日志:
      • 📤 捕获到第一个含"url"的fetch/XHR请求:确认请求拦截成功;
      • ✅ 已记录第一个匹配请求的响应内容:确认响应数据已保存;
      • 紫色/橙色/绿色分段日志:分别展示请求响应详情、fileInfoUrl解析结果、download_url提取结果;
    2. 脚本执行完成后,会弹出提示框 🎉 操作成功!,表明下载地址已提取完成。
下载地址使用方式
  1. 使用页面下载按钮(推荐)

    脚本执行成功后,PDF预览页面右上角会生成固定悬浮按钮 📥 打开下载地址,点击该按钮会在新标签页打开原始PDF的下载地址,浏览器自动触发PDF下载或直接渲染原始文件。

  2. 手动复制下载地址(备用)

    1. 仅推荐当按钮未正常生成时,从Console控制台提取下载地址。
    2. 在控制台日志中找到红色加粗的 🎉 最终下载地址: 行,复制其后的URL字符串;
    3. 打开新标签页,粘贴该URL并回车,即可访问原始PDF下载地址。
注意事项

本文分享的方法与脚本,仅适用于个人学习、研究或获取有权限的文档。在使用过程中,需严格遵守相关法律法规和网站的用户协议,不得用于非法获取受版权保护的文档,避免侵权风险。

相关推荐
徐同保2 小时前
使用onlyoffice预览word、excel、ppt、pdf等,可以双击index.html看效果的demo示例
pdf
不吃香菜的猪18 小时前
使用@vue-office/pdf时,pdf展示不全
javascript·vue.js·pdf
余衫马18 小时前
在Win10下编译 Poppler
c++·windows·qt·pdf·poppler
开开心心_Every1 天前
手机端课程表管理工具:支持课程导入自定义
python·游戏·微信·django·pdf·excel·语音识别
2401_861412141 天前
python 从入门到精通 高清PDF 背记手册
开发语言·python·pdf
今天也不想动1 天前
PaddleOCR实现批量pdf文件或图像的文本识别
pdf·文本识别
开开心心_Every1 天前
视频无损压缩工具:大幅减小体积并保持画质
游戏·微信·pdf·excel·音视频·语音识别·tornado
进阶的猿猴1 天前
java中实现markdown转为pdf
java·pdf·markdown
开开心心_Every1 天前
安卓语音转文字工具:免费支持实时转换视频
python·游戏·微信·django·pdf·excel·语音识别