diff --git a/.temp_to_pub/compress.py b/.temp_to_pub/compress.py index 11bfaab..72d372f 100644 --- a/.temp_to_pub/compress.py +++ b/.temp_to_pub/compress.py @@ -64,7 +64,7 @@ def compress_folder_to_7z_split(folder_path, output_file): except: subprocess.call(["7zz", "a", "-v95m", output_file, folder_path]) -easyspider_version = "0.6.3" +easyspider_version = "0.6.5" if __name__ == "__main__": diff --git a/ElectronJS/EasySpider_en/content-scripts/main.js b/ElectronJS/EasySpider_en/content-scripts/main.js index 412a90f..f67b430 100644 --- a/ElectronJS/EasySpider_en/content-scripts/main.js +++ b/ElectronJS/EasySpider_en/content-scripts/main.js @@ -18269,7 +18269,7 @@ staticRenderFns: [function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._ }, }; -var css_248z = "\n#wrapperToolkitIframe{\n position: absolute;\n top:0;\n}\n.toolcannotdrag{\n background-color: navy;\n width: 100%;\n text-align: center;\n font-size: 13px;\n height: 26px !important;\n padding-top: 8px !important;\n color: white;\n}\n"; +var css_248z = "\r\n#wrapperToolkitIframe{\r\n position: absolute;\r\n top:0;\r\n}\r\n.toolcannotdrag{\r\n background-color: navy;\r\n width: 100%;\r\n text-align: center;\r\n font-size: 13px;\r\n height: 26px !important;\r\n padding-top: 8px !important;\r\n color: white;\r\n}\r\n"; styleInject(css_248z); ({ diff --git a/ElectronJS/EasySpider_en/manifest.json b/ElectronJS/EasySpider_en/manifest.json index 4cea3c3..17e0f63 100644 --- a/ElectronJS/EasySpider_en/manifest.json +++ b/ElectronJS/EasySpider_en/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "description": "EasySpider's chrome extension", "author": "Naibo Wang", "action": { diff --git a/ElectronJS/EasySpider_zh/content-scripts/main.js b/ElectronJS/EasySpider_zh/content-scripts/main.js index 412a90f..f67b430 100644 --- a/ElectronJS/EasySpider_zh/content-scripts/main.js +++ b/ElectronJS/EasySpider_zh/content-scripts/main.js @@ -18269,7 +18269,7 @@ staticRenderFns: [function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._ }, }; -var css_248z = "\n#wrapperToolkitIframe{\n position: absolute;\n top:0;\n}\n.toolcannotdrag{\n background-color: navy;\n width: 100%;\n text-align: center;\n font-size: 13px;\n height: 26px !important;\n padding-top: 8px !important;\n color: white;\n}\n"; +var css_248z = "\r\n#wrapperToolkitIframe{\r\n position: absolute;\r\n top:0;\r\n}\r\n.toolcannotdrag{\r\n background-color: navy;\r\n width: 100%;\r\n text-align: center;\r\n font-size: 13px;\r\n height: 26px !important;\r\n padding-top: 8px !important;\r\n color: white;\r\n}\r\n"; styleInject(css_248z); ({ diff --git a/ElectronJS/EasySpider_zh/manifest.json b/ElectronJS/EasySpider_zh/manifest.json index 4cea3c3..17e0f63 100644 --- a/ElectronJS/EasySpider_zh/manifest.json +++ b/ElectronJS/EasySpider_zh/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "description": "EasySpider's chrome extension", "author": "Naibo Wang", "action": { diff --git a/ElectronJS/XPathHelper.crx b/ElectronJS/XPathHelper.crx deleted file mode 100644 index d24c7a2..0000000 Binary files a/ElectronJS/XPathHelper.crx and /dev/null differ diff --git a/ElectronJS/XPathHelper_MV3/LICENSE b/ElectronJS/XPathHelper_MV3/LICENSE new file mode 100644 index 0000000..27ce4e4 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one of + the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ElectronJS/XPathHelper_MV3/NOTICE.md b/ElectronJS/XPathHelper_MV3/NOTICE.md new file mode 100644 index 0000000..8914d4a --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/NOTICE.md @@ -0,0 +1,23 @@ +# XPath Helper for EasySpider + +This directory is a Manifest V3 compatibility port of the XPath Helper 2.0.2 +extension previously distributed in this repository as `XPathHelper.crx`. + +The original source files identify Google Inc. and `opensource@google.com` as +their author and are licensed under the Apache License, Version 2.0. The +original CRX bundled by EasySpider has SHA-256: + +`CA08441B51C00DAD6DDAF6713A6F4F613465253198B1FB376A3E57FEC1903EFD` + +EasySpider's 2026 compatibility changes are limited to: + +- replacing Manifest V2 with Manifest V3; +- replacing the background page with a service worker; +- replacing `chrome.browserAction` with `chrome.action`; +- updating the web-accessible resource declaration; +- loading the unpacked extension through WebDriver BiDi. + +The XPath bar, XPath evaluation, element highlighting, Ctrl+Shift+X shortcut, +and toolbar action behavior remain based on the original implementation. + +See `LICENSE` for the full Apache-2.0 terms. diff --git a/ElectronJS/XPathHelper_MV3/background.js b/ElectronJS/XPathHelper_MV3/background.js new file mode 100644 index 0000000..1768938 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/background.js @@ -0,0 +1,39 @@ +/** + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author opensource@google.com + * @license Apache License, Version 2.0. + * + * Modified by the EasySpider project in 2026 for Chrome Manifest V3: + * use an extension service worker and chrome.action. + */ + +'use strict'; + +function handleRequest(request, sender, cb) { + // Simply relay the request. This lets content.js talk to bar.js. + if (!sender.tab || sender.tab.id === undefined) { + return false; + } + chrome.tabs.sendMessage(sender.tab.id, request, cb); + return true; +} +chrome.runtime.onMessage.addListener(handleRequest); + +chrome.action.onClicked.addListener(function(tab) { + chrome.tabs.sendMessage(tab.id, {type: 'toggleBar'}); +}); diff --git a/ElectronJS/XPathHelper_MV3/bar.css b/ElectronJS/XPathHelper_MV3/bar.css new file mode 100644 index 0000000..7211699 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/bar.css @@ -0,0 +1,75 @@ +/** + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author opensource@google.com + * @license Apache License, Version 2.0. + */ + +*, +:before, +:after { + box-sizing: border-box; +} + +body { + background: rgba(0, 0, 0, 0.9); + display: flex; + font-family: sans-serif; + height: 120px; + margin: 0; + overflow: hidden; + padding: 12px; +} + +textarea { + background-color: transparent; + border: 1px solid rgba(255, 255, 255, 0.3); + color: #f0f0f0; + display: block; + font-family: monospace; + font-size: 13px; + margin: 0; + overflow-x: hidden; + padding: 2px; + resize: none; +} + +#query-box, #results-box { + display: flex; + flex-direction: column; +} + +#query-box { + padding-right: 12px; + width: 60%; +} + +#results-box { + width: 40%; +} + +.box-name { + color: #808080; + font-size: 12px; + padding-bottom: 2px; + text-transform: uppercase; +} + +#query, #results { + flex: 1; + width: 100%; +} diff --git a/ElectronJS/XPathHelper_MV3/bar.html b/ElectronJS/XPathHelper_MV3/bar.html new file mode 100644 index 0000000..3702968 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/bar.html @@ -0,0 +1,18 @@ + + + + + + + +
+
Query
+ +
+
+
Results ()
+ +
+ + + diff --git a/ElectronJS/XPathHelper_MV3/bar.js b/ElectronJS/XPathHelper_MV3/bar.js new file mode 100644 index 0000000..b1e2162 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/bar.js @@ -0,0 +1,90 @@ +/** + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author opensource@google.com + * @license Apache License, Version 2.0. + */ + +'use strict'; + +// Constants. +var MOVE_COOLDOWN_PERIOD_MS = 400; +var X_KEYCODE = 88; + +// Global variables. +var queryEl = document.getElementById('query'); +var resultsEl = document.getElementById('results'); +var nodeCountEl = document.getElementById('node-count'); + +var nodeCountText = document.createTextNode('0'); +nodeCountEl.appendChild(nodeCountText); + +// Used by handleMouseMove() to enforce a cooldown period on move. +var lastMoveTimeInMs = 0; + +var evaluateQuery = function() { + chrome.runtime.sendMessage({ + type: 'evaluate', + query: queryEl.value + }); +}; + +var handleRequest = function(request, sender, cb) { + // Note: Setting textarea's value and text node's nodeValue is XSS-safe. + if (request.type === 'update') { + if (request.query !== null) { + queryEl.value = request.query; + } + if (request.results !== null) { + resultsEl.value = request.results[0]; + nodeCountText.nodeValue = request.results[1]; + } + } +}; + +var handleMouseMove = function(e) { + if (e.shiftKey) { + // Only move bar if we aren't in the cooldown period. Note, the cooldown + // duration should take CSS transition time into consideration. + var timeInMs = new Date().getTime(); + if (timeInMs - lastMoveTimeInMs < MOVE_COOLDOWN_PERIOD_MS) { + return; + } + lastMoveTimeInMs = timeInMs; + // Tell content script to move iframe to a different part of the screen. + chrome.runtime.sendMessage({type: 'moveBar'}); + } +}; + +var handleKeyDown = function(e) { + var ctrlKey = e.ctrlKey || e.metaKey; + var shiftKey = e.shiftKey; + if (e.keyCode === X_KEYCODE && ctrlKey && shiftKey) { + chrome.runtime.sendMessage({type: 'hideBar'}); + } +}; + +queryEl.addEventListener('keyup', evaluateQuery); +queryEl.addEventListener('mouseup', evaluateQuery); + +// Add mousemove listener so we can detect Shift + mousemove inside iframe. +document.addEventListener('mousemove', handleMouseMove); +// Add keydown listener so we can detect Ctrl-Shift-X and tell the content +// script to hide iframe and steal focus. +document.addEventListener('keydown', handleKeyDown); + +chrome.runtime.onMessage.addListener(handleRequest); diff --git a/ElectronJS/XPathHelper_MV3/content.css b/ElectronJS/XPathHelper_MV3/content.css new file mode 100644 index 0000000..25dff43 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/content.css @@ -0,0 +1,53 @@ +/** + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author opensource@google.com + * @license Apache License, Version 2.0. + */ + +#xh-bar { + border-width: 0 !important; + position: fixed !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: 120px !important; /* bar.css body height */ + z-index: 2147483647 !important; + transform: translate3d(0, 0, 0) !important; + transition: transform 0.2s ease-out !important; +} + +#xh-bar.bottom { + top: auto !important; + bottom: 0 !important; +} + +#xh-bar.hidden { + transform: translate3d(0, -100%, 0) !important; +} + +#xh-bar.hidden.bottom { + transform: translate3d(0, 100%, 0) !important; +} + +.xh-highlight { + background-color: #fff59d !important; +} + +img.xh-highlight { + opacity: 0.5 !important; +} diff --git a/ElectronJS/XPathHelper_MV3/content.js b/ElectronJS/XPathHelper_MV3/content.js new file mode 100644 index 0000000..44fbd75 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/content.js @@ -0,0 +1,268 @@ +/** + * Copyright 2011 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @author opensource@google.com + * @license Apache License, Version 2.0. + */ + +'use strict'; + +// Extension namespace. +var xh = xh || {}; + +//////////////////////////////////////////////////////////// +// Generic helper functions and constants + +xh.SHIFT_KEYCODE = 16; +xh.X_KEYCODE = 88; + +xh.elementsShareFamily = function(primaryEl, siblingEl) { + var p = primaryEl, s = siblingEl; + return (p.tagName === s.tagName && + (!p.className || p.className === s.className) && + (!p.id || p.id === s.id)); +}; + +xh.getElementIndex = function(el) { + var index = 1; // XPath is one-indexed + var sib; + for (sib = el.previousSibling; sib; sib = sib.previousSibling) { + if (sib.nodeType === Node.ELEMENT_NODE && xh.elementsShareFamily(el, sib)) { + index++; + } + } + if (index > 1) { + return index; + } + for (sib = el.nextSibling; sib; sib = sib.nextSibling) { + if (sib.nodeType === Node.ELEMENT_NODE && xh.elementsShareFamily(el, sib)) { + return 1; + } + } + return 0; +}; + +xh.makeQueryForElement = function(el) { + var query = ''; + for (; el && el.nodeType === Node.ELEMENT_NODE; el = el.parentNode) { + var component = el.tagName.toLowerCase(); + var index = xh.getElementIndex(el); + if (el.id) { + component += '[@id=\'' + el.id + '\']'; + } else if (el.className) { + component += '[@class=\'' + el.className + '\']'; + } + if (index >= 1) { + component += '[' + index + ']'; + } + // If the last tag is an img, the user probably wants img/@src. + if (query === '' && el.tagName.toLowerCase() === 'img') { + component += '/@src'; + } + query = '/' + component + query; + } + return query; +}; + +xh.highlight = function(els) { + for (var i = 0, l = els.length; i < l; i++) { + els[i].classList.add('xh-highlight'); + } +}; + +xh.clearHighlights = function() { + var els = document.querySelectorAll('.xh-highlight'); + for (var i = 0, l = els.length; i < l; i++) { + els[i].classList.remove('xh-highlight'); + } +}; + +// Returns [values, nodeCount]. Highlights result nodes, if applicable. Assumes +// no nodes are currently highlighted. +xh.evaluateQuery = function(query) { + var xpathResult = null; + var str = ''; + var nodeCount = 0; + var toHighlight = []; + + try { + xpathResult = document.evaluate(query, document, null, + XPathResult.ANY_TYPE, null); + } catch (e) { + str = '[INVALID XPATH EXPRESSION]'; + nodeCount = 0; + } + + if (!xpathResult) { + return [str, nodeCount]; + } + + if (xpathResult.resultType === XPathResult.BOOLEAN_TYPE) { + str = xpathResult.booleanValue ? '1' : '0'; + nodeCount = 1; + } else if (xpathResult.resultType === XPathResult.NUMBER_TYPE) { + str = xpathResult.numberValue.toString(); + nodeCount = 1; + } else if (xpathResult.resultType === XPathResult.STRING_TYPE) { + str = xpathResult.stringValue; + nodeCount = 1; + } else if (xpathResult.resultType === + XPathResult.UNORDERED_NODE_ITERATOR_TYPE) { + for (var node = xpathResult.iterateNext(); node; + node = xpathResult.iterateNext()) { + if (node.nodeType === Node.ELEMENT_NODE) { + toHighlight.push(node); + } + if (str) { + str += '\n'; + } + str += node.textContent; + nodeCount++; + } + if (nodeCount === 0) { + str = '[NULL]'; + } + } else { + // Since we pass XPathResult.ANY_TYPE to document.evaluate(), we should + // never get back a result type not handled above. + str = '[INTERNAL ERROR]'; + nodeCount = 0; + } + + xh.highlight(toHighlight); + return [str, nodeCount]; +}; + +//////////////////////////////////////////////////////////// +// xh.Bar class definition + +xh.Bar = function() { + this.boundHandleRequest_ = this.handleRequest_.bind(this); + this.boundMouseMove_ = this.mouseMove_.bind(this); + this.boundKeyDown_ = this.keyDown_.bind(this); + + this.inDOM_ = false; + this.currEl_ = null; + + this.barFrame_ = document.createElement('iframe'); + this.barFrame_.src = chrome.runtime.getURL('bar.html'); + this.barFrame_.id = 'xh-bar'; + // Init to hidden so first showBar_() triggers fade-in. + this.barFrame_.classList.add('hidden'); + + document.addEventListener('keydown', this.boundKeyDown_); + chrome.runtime.onMessage.addListener(this.boundHandleRequest_); +}; + +xh.Bar.prototype.hidden_ = function() { + return this.barFrame_.classList.contains('hidden'); +}; + +xh.Bar.prototype.updateQueryAndBar_ = function(el) { + xh.clearHighlights(); + this.query_ = el ? xh.makeQueryForElement(el) : ''; + this.updateBar_(true); +}; + +xh.Bar.prototype.updateBar_ = function(updateQuery) { + var results = this.query_ ? xh.evaluateQuery(this.query_) : ['', 0]; + chrome.runtime.sendMessage({ + type: 'update', + query: updateQuery ? this.query_ : null, + results: results + }); +}; + +xh.Bar.prototype.showBar_ = function() { + var that = this; + function impl() { + that.barFrame_.classList.remove('hidden'); + document.addEventListener('mousemove', that.boundMouseMove_); + that.updateBar_(true); + } + if (!this.inDOM_) { + this.inDOM_ = true; + document.body.appendChild(this.barFrame_); + } + window.setTimeout(impl, 0); +}; + +xh.Bar.prototype.hideBar_ = function() { + var that = this; + function impl() { + that.barFrame_.classList.add('hidden'); + document.removeEventListener('mousemove', that.boundMouseMove_); + xh.clearHighlights(); + } + window.setTimeout(impl, 0); +}; + +xh.Bar.prototype.toggleBar_ = function() { + if (this.hidden_()) { + this.showBar_(); + } else { + this.hideBar_(); + } +}; + +xh.Bar.prototype.handleRequest_ = function(request, sender, cb) { + if (request.type === 'evaluate') { + xh.clearHighlights(); + this.query_ = request.query; + this.updateBar_(false); + } else if (request.type === 'moveBar') { + // Move iframe to a different part of the screen. + this.barFrame_.classList.toggle('bottom'); + } else if (request.type === 'hideBar') { + this.hideBar_(); + window.focus(); + } else if (request.type === 'toggleBar') { + this.toggleBar_(); + } +}; + +xh.Bar.prototype.mouseMove_ = function(e) { + if (this.currEl_ === e.toElement) { + return; + } + this.currEl_ = e.toElement; + if (e.shiftKey) { + this.updateQueryAndBar_(this.currEl_); + } +}; + +xh.Bar.prototype.keyDown_ = function(e) { + var ctrlKey = e.ctrlKey || e.metaKey; + var shiftKey = e.shiftKey; + if (e.keyCode === xh.X_KEYCODE && ctrlKey && shiftKey) { + this.toggleBar_(); + } + // If the user just pressed Shift and they're not holding Ctrl, update query. + // Note that we rely on the mousemove handler to have updated this.currEl_. + // Also, note that checking e.shiftKey wouldn't work here, since Shift is the + // key that triggered this event. + if (!this.hidden_() && !ctrlKey && e.keyCode === xh.SHIFT_KEYCODE) { + this.updateQueryAndBar_(this.currEl_); + } +}; + +//////////////////////////////////////////////////////////// +// Initialization code + +if (location.href.indexOf('acid3.acidtests.org') === -1) { + window.xhBarInstance = new xh.Bar(); +} diff --git a/ElectronJS/XPathHelper_MV3/manifest.json b/ElectronJS/XPathHelper_MV3/manifest.json new file mode 100644 index 0000000..3127470 --- /dev/null +++ b/ElectronJS/XPathHelper_MV3/manifest.json @@ -0,0 +1,49 @@ +{ + "manifest_version": 3, + "name": "XPath Helper for EasySpider", + "version": "3.0.0", + "description": "Extract, edit, and evaluate XPath queries with ease. Manifest V3 compatibility port for EasySpider.", + "author": "Original XPath Helper contributors; Manifest V3 compatibility port by the EasySpider project", + "background": { + "service_worker": "background.js" + }, + "action": { + "default_icon": { + "19": "static/icon_19x19.png", + "38": "static/icon_38x38.png" + }, + "default_title": "XPath Helper" + }, + "content_scripts": [ + { + "matches": [ + "" + ], + "css": [ + "content.css" + ], + "js": [ + "content.js" + ], + "run_at": "document_idle" + } + ], + "icons": { + "16": "static/icon_16x16.png", + "32": "static/icon_32x32.png", + "48": "static/icon_48x48.png", + "128": "static/icon_128x128.png" + }, + "web_accessible_resources": [ + { + "resources": [ + "bar.css", + "bar.html", + "bar.js" + ], + "matches": [ + "" + ] + } + ] +} diff --git a/ElectronJS/XPathHelper_MV3/static/icon_128x128.png b/ElectronJS/XPathHelper_MV3/static/icon_128x128.png new file mode 100644 index 0000000..90730f8 Binary files /dev/null and b/ElectronJS/XPathHelper_MV3/static/icon_128x128.png differ diff --git a/ElectronJS/XPathHelper_MV3/static/icon_16x16.png b/ElectronJS/XPathHelper_MV3/static/icon_16x16.png new file mode 100644 index 0000000..8f95bcc Binary files /dev/null and b/ElectronJS/XPathHelper_MV3/static/icon_16x16.png differ diff --git a/ElectronJS/XPathHelper_MV3/static/icon_19x19.png b/ElectronJS/XPathHelper_MV3/static/icon_19x19.png new file mode 100644 index 0000000..f598c96 Binary files /dev/null and b/ElectronJS/XPathHelper_MV3/static/icon_19x19.png differ diff --git a/ElectronJS/XPathHelper_MV3/static/icon_32x32.png b/ElectronJS/XPathHelper_MV3/static/icon_32x32.png new file mode 100644 index 0000000..9a77fcc Binary files /dev/null and b/ElectronJS/XPathHelper_MV3/static/icon_32x32.png differ diff --git a/ElectronJS/XPathHelper_MV3/static/icon_38x38.png b/ElectronJS/XPathHelper_MV3/static/icon_38x38.png new file mode 100644 index 0000000..ee284f7 Binary files /dev/null and b/ElectronJS/XPathHelper_MV3/static/icon_38x38.png differ diff --git a/ElectronJS/XPathHelper_MV3/static/icon_48x48.png b/ElectronJS/XPathHelper_MV3/static/icon_48x48.png new file mode 100644 index 0000000..3f6c8c4 Binary files /dev/null and b/ElectronJS/XPathHelper_MV3/static/icon_48x48.png differ diff --git a/ElectronJS/change_version.py b/ElectronJS/change_version.py index 487a676..3ec7c5b 100644 --- a/ElectronJS/change_version.py +++ b/ElectronJS/change_version.py @@ -1,90 +1,74 @@ +"""Update the version in all source and packaging metadata files. + +Run this script from any directory, for example:: + + python ElectronJS/change_version.py 0.6.5 +""" + +import argparse import json -import os import re -import sys - -# 读取JSON文件 +from pathlib import Path -def read_json_file(file_path): - with open(file_path, 'r', encoding='utf-8') as file: - data = json.load(file) - return data - -# 保存为JSON文件 +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") +ROOT = Path(__file__).resolve().parent -def save_json_file(data, file_path): - with open(file_path, 'w', encoding='utf-8') as file: - json.dump(data, file, indent=4, ensure_ascii=False) +def _read_text(path): + with path.open("r", encoding="utf-8", newline="") as file: + return file.read() -def update_file_version(file_path, new_version, key="当前版本/Current Version: v"): - with open(file_path, 'r', encoding='utf-8') as file: - lines = file.readlines() - with open(file_path, 'w', encoding='utf-8') as file: - for line in lines: - if key in line: - pattern = r'('+key+')\d+\.\d+\.\d+' - line = re.sub(pattern, r'\g<1>'+new_version, line) - file.write(line) +def _write_text(path, text): + with path.open("w", encoding="utf-8", newline="") as file: + file.write(text) -version = "0.6.3" +def update_text_file(path, prefix, version): + text = _read_text(path) + pattern = re.escape(prefix) + r"\d+\.\d+\.\d+" + updated, count = re.subn(pattern, prefix + version, text) + if count == 0: + raise ValueError(f"Version marker not found in {path}: {prefix}") + _write_text(path, updated) + + +def update_json_file(path, version, update_forge_app_version=False): + original = _read_text(path) + data = json.loads(original) + data["version"] = version + if isinstance(data.get("packages"), dict) and "" in data["packages"]: + data["packages"][""]["version"] = version + if update_forge_app_version: + data["config"]["forge"]["packagerConfig"]["appVersion"] = version + newline = "\r\n" if "\r\n" in original else "\n" + _write_text(path, json.dumps(data, indent=4, ensure_ascii=False) + newline) + + +def update_version(version): + update_text_file(ROOT.parent / ".temp_to_pub" / "compress.py", 'easyspider_version = "', version) + update_text_file(ROOT / "src" / "taskGrid" / "logic.js", '"version": "', version) + update_text_file(ROOT.parent / "ExecuteStage" / "easyspider_executestage.py", '"version": "', version) + update_text_file(ROOT / "src" / "index.html", "软件当前版本:v", version) + update_text_file(ROOT / "src" / "index.html", "Current Version: v", version) + + update_json_file(ROOT / "package.json", version, update_forge_app_version=True) + update_json_file(ROOT / "package-lock.json", version) + update_json_file(ROOT.parent / "Extension" / "manifest_v3" / "package.json", version) + update_json_file(ROOT.parent / "Extension" / "manifest_v3" / "package-lock.json", version) + update_json_file(ROOT.parent / "Extension" / "manifest_v3" / "src" / "manifest.json", version) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("version", help="new semantic version, for example 0.6.5") + args = parser.parse_args() + if not VERSION_RE.fullmatch(args.version): + parser.error("version must have the form MAJOR.MINOR.PATCH") + update_version(args.version) + print(f"Updated EasySpider version to {args.version}") -# py html js if __name__ == "__main__": - - file_path = "../.temp_to_pub/compress.py" - update_file_version(file_path, version, key='easyspider_version = "') - - file_path = "./src/taskGrid/logic.js" - update_file_version(file_path, version, key='"version": "') - - file_path = "../ExecuteStage/easyspider_executestage.py" - update_file_version(file_path, version, key='"version": "') - - # index.html - file_path = "./src/index.html" - update_file_version(file_path, version, key="软件当前版本:v") - update_file_version(file_path, version, key="Current Version: v") - - # package.json - file_path = "./package.json" - - # 读取JSON文件 - electron_config = read_json_file(file_path) - print(electron_config["version"]) - - # 修改数据 - electron_config["version"] = version - electron_config["config"]["forge"]["packagerConfig"]["appVersion"] = version - - # 保存为JSON文件 - save_json_file(electron_config, file_path) - - # 插件的package.json - file_path = "../Extension/manifest_v3/package.json" - - # 读取JSON文件 - electron_config = read_json_file(file_path) - print(electron_config["version"]) - - # 修改数据 - electron_config["version"] = version - - # 保存为JSON文件 - save_json_file(electron_config, file_path) - - file_path = "../Extension/manifest_v3/src/manifest.json" - - # 读取JSON文件 - electron_config = read_json_file(file_path) - print(electron_config["version"]) - - # 修改数据 - electron_config["version"] = version - - # 保存为JSON文件 - save_json_file(electron_config, file_path) + main() diff --git a/ElectronJS/clean_and_release_win64.cmd b/ElectronJS/clean_and_release_win64.cmd index 000a770..b7f803a 100644 --- a/ElectronJS/clean_and_release_win64.cmd +++ b/ElectronJS/clean_and_release_win64.cmd @@ -19,6 +19,7 @@ copy ..\ExecuteStage\requirements.txt ..\.temp_to_pub\EasySpider_windows_x64\Cod copy ..\ExecuteStage\Readme.md ..\.temp_to_pub\EasySpider_windows_x64\Code copy ..\ExecuteStage\myCode.py ..\.temp_to_pub\EasySpider_windows_x64 xcopy ..\ExecuteStage\undetected_chromedriver_ES ..\.temp_to_pub\EasySpider_windows_x64\Code\undetected_chromedriver_ES /E /I /Y +if exist ..\.temp_to_pub\EasySpider_windows_x64\Code\undetected_chromedriver_ES\__pycache__ rmdir /s /Q ..\.temp_to_pub\EasySpider_windows_x64\Code\undetected_chromedriver_ES\__pycache__ xcopy ..\ExecuteStage\.vscode ..\.temp_to_pub\EasySpider_windows_x64\Code\.vscode /E /I /Y rmdir /s /Q ..\.temp_to_pub\EasySpider_windows_x64\user_data rmdir /s /Q ..\.temp_to_pub\EasySpider_windows_x64\execution_instances @@ -26,5 +27,8 @@ rmdir /s /Q ..\.temp_to_pub\EasySpider_windows_x64\TempUserDataFolder mkdir ..\.temp_to_pub\EasySpider_windows_x64\execution_instances rmdir /s /Q ..\.temp_to_pub\EasySpider_windows_x64\Data mkdir ..\.temp_to_pub\EasySpider_windows_x64\Data -xcopy .\tasks\* ..\.temp_to_pub\EasySpider_windows_x64\tasks\ /E /I /Y +if exist ..\.temp_to_pub\EasySpider_windows_x64\tasks rmdir /s /Q ..\.temp_to_pub\EasySpider_windows_x64\tasks +mkdir ..\.temp_to_pub\EasySpider_windows_x64\tasks +powershell -NoProfile -Command "$repo = (Resolve-Path '..').Path; $stage = (Resolve-Path '..\.temp_to_pub\EasySpider_windows_x64\tasks').Path; $files = @(git -C $repo -c 'safe.directory=*' ls-files -- 'ElectronJS/tasks'); if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; foreach ($file in $files) { $relative = $file -replace '^ElectronJS/tasks[\\/]', ''; $destination = Join-Path $stage $relative; $null = New-Item -ItemType Directory -Force -Path (Split-Path $destination); Copy-Item -LiteralPath (Join-Path $repo $file) -Destination $destination -Force }" +if errorlevel 1 exit /b %errorlevel% diff --git a/ElectronJS/execute_linux64.sh b/ElectronJS/execute_linux64.sh index 8db4c4c..40637b9 100755 --- a/ElectronJS/execute_linux64.sh +++ b/ElectronJS/execute_linux64.sh @@ -1,3 +1,5 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -./EasySpider/resources/app/chrome_linux64/easyspider_executestage $1 $2 $3 $4 $5 $6 $7 $8 $9 +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/easyspider_executestage" "$@" diff --git a/ElectronJS/execute_macos.sh b/ElectronJS/execute_macos.sh index 71fd902..40637b9 100755 --- a/ElectronJS/execute_macos.sh +++ b/ElectronJS/execute_macos.sh @@ -1,5 +1,5 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -echo "Executing EasySpider on MacOS" - -./easyspider_executestage $1 $2 $3 $4 $5 $6 $7 $8 $9 +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/easyspider_executestage" "$@" diff --git a/ElectronJS/main.js b/ElectronJS/main.js index 77448ca..20a5904 100644 --- a/ElectronJS/main.js +++ b/ElectronJS/main.js @@ -1425,8 +1425,6 @@ async function runBrowser(lang = "en", user_data_folder = "", mobile = false) { // } else if (lang == "zh") { // options.addExtensions(path.join(__dirname, "EasySpider_zh.crx")); // } - // options.addExtensions(path.join(__dirname, "XPathHelper.crx")); - // 使用 BiDi WebExtension API const getWebExtensionInstance = require('./selenium-webdriver-bidi-webExtension/webExtension'); const ExtensionData = require('./selenium-webdriver-bidi-webExtension/extensionData'); @@ -1473,12 +1471,21 @@ async function runBrowser(lang = "en", user_data_folder = "", mobile = false) { let easySpiderPath; if (lang == "zh") { easySpiderPath = path.join(__dirname, "EasySpider_zh"); - await webExtension.install(ExtensionData.setPath(easySpiderPath)); } else { easySpiderPath = path.join(__dirname, "EasySpider_en"); - await webExtension.install(ExtensionData.setPath(easySpiderPath)); } + const easySpiderId = await webExtension.install( + ExtensionData.setPath(easySpiderPath) + ); + console.log(`EasySpider 扩展已安装,ID: ${easySpiderId}`); + // Chrome 151 不再接受旧的 Manifest V2 XPathHelper.crx; + // 使用 BiDi 安装解压后的 Manifest V3 兼容版本。 + const xpathHelperPath = path.join(__dirname, "XPathHelper_MV3"); + const xpathHelperId = await webExtension.install( + ExtensionData.setPath(xpathHelperPath) + ); + console.log(`XPath Helper MV3 已安装,ID: ${xpathHelperId}`); } catch (error) { console.error('安装扩展失败:', error); throw error; diff --git a/ElectronJS/package-lock.json b/ElectronJS/package-lock.json index 12112b9..547a948 100644 --- a/ElectronJS/package-lock.json +++ b/ElectronJS/package-lock.json @@ -1,12 +1,12 @@ { "name": "easy-spider", - "version": "0.6.3", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "easy-spider", - "version": "0.6.3", + "version": "0.6.5", "license": "AGPL-3.0", "dependencies": { "cors": "^2.8.5", @@ -28,7 +28,8 @@ "@electron-forge/maker-rpm": "^6.0.5", "@electron-forge/maker-squirrel": "^6.0.5", "@electron-forge/maker-zip": "^6.0.5", - "electron": "^27.1.3" + "electron": "^27.1.3", + "electron-rebuild": "^3.2.9" } }, "node_modules/@bazel/runfiles": { @@ -2142,6 +2143,36 @@ "url": "https://github.com/electron/electron-packager?sponsor=1" } }, + "node_modules/electron-rebuild": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/electron-rebuild/-/electron-rebuild-3.2.9.tgz", + "integrity": "sha512-FkEZNFViUem3P0RLYbZkUjC8LUFIK+wKq09GHoOITSJjfDAVQv964hwaNseTTWt58sITQX3/5fHNYcTefqaCWw==", + "deprecated": "Please use @electron/rebuild moving forward. There is no API change, just a package name change", + "dev": true, + "license": "MIT", + "dependencies": { + "@malept/cross-spawn-promise": "^2.0.0", + "chalk": "^4.0.0", + "debug": "^4.1.1", + "detect-libc": "^2.0.1", + "fs-extra": "^10.0.0", + "got": "^11.7.0", + "lzma-native": "^8.0.5", + "node-abi": "^3.0.0", + "node-api-version": "^0.1.4", + "node-gyp": "^9.0.0", + "ora": "^5.1.0", + "semver": "^7.3.5", + "tar": "^6.0.5", + "yargs": "^17.0.1" + }, + "bin": { + "electron-rebuild": "lib/src/cli.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, "node_modules/electron-squirrel-startup": { "version": "1.0.0", "license": "Apache-2.0", @@ -3678,6 +3709,47 @@ "node": ">=12" } }, + "node_modules/lzma-native": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/lzma-native/-/lzma-native-8.0.6.tgz", + "integrity": "sha512-09xfg67mkL2Lz20PrrDeNYZxzeW7ADtpYFbwSQh9U8+76RIzx5QsJBMy8qikv3hbUPfpy6hqwxt6FcGK81g9AA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^3.1.0", + "node-gyp-build": "^4.2.1", + "readable-stream": "^3.6.0" + }, + "bin": { + "lzmajs": "bin/lzmajs" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/lzma-native/node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lzma-native/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/make-fetch-happen": { "version": "11.1.1", "dev": true, diff --git a/ElectronJS/package.json b/ElectronJS/package.json index 724a9e5..ebcd709 100644 --- a/ElectronJS/package.json +++ b/ElectronJS/package.json @@ -1,7 +1,7 @@ { "name": "easy-spider", "productName": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "icon": "./favicon", "description": "NoCode Visual Web Crawler", "main": "main.js", @@ -27,7 +27,8 @@ "@electron-forge/maker-rpm": "^6.0.5", "@electron-forge/maker-squirrel": "^6.0.5", "@electron-forge/maker-zip": "^6.0.5", - "electron": "^27.1.3" + "electron": "^27.1.3", + "electron-rebuild": "^3.2.9" }, "repository": "https://github.com/NaiboWang/EasySpider", "dependencies": { @@ -68,7 +69,7 @@ ], "packagerConfig": { "icon": "./favicon", - "appVersion": "0.6.3", + "appVersion": "0.6.5", "name": "EasySpider", "executableName": "EasySpider", "appCopyright": "Naibo Wang (naibowang@foxmail.com)", @@ -81,4 +82,4 @@ "publishers": [] } } -} \ No newline at end of file +} diff --git a/ElectronJS/package_linux64.sh b/ElectronJS/package_linux64.sh index 30e265e..749450d 100755 --- a/ElectronJS/package_linux64.sh +++ b/ElectronJS/package_linux64.sh @@ -1,40 +1,81 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -# This script is used to build.md the package for Linux 64-bit. -rm -r out -cd ../Extension/manifest_v3 -node package.js -cd ../../ElectronJS -npm run package -mv out/EasySpider-linux-x64 out/EasySpider -rm -rf out/EasySpider/resources/app/chrome_win64 -rm -rf out/EasySpider/resources/app/chromedrivers -rm -rf out/EasySpider/resources/app/Data -rm -rf out/EasySpider/resources/app/.idea -rm -rf out/EasySpider/resources/app/tasks -rm -rf out/EasySpider/resources/app/execution_instances -rm -rf out/EasySpider/resources/app/user_data -rm -rf out/EasySpider/resources/app/TempUserDataFolder -rm -rf ../.temp_to_pub/EasySpider_Linux_x64/EasySpider -rm out/EasySpider/resources/app/vs_BuildTools.exe -mv out/EasySpider ../.temp_to_pub/EasySpider_Linux_x64/EasySpider -rm -rf ../.temp_to_pub/EasySpider_Linux_x64/Code -mkdir ../.temp_to_pub/EasySpider_Linux_x64/Code -# cp ../ExecuteStage/easyspider_executestage.py ../.temp_to_pub/EasySpider_Linux_x64/Code -# cp ../ExecuteStage/myChrome.py ../.temp_to_pub/EasySpider_Linux_x64/Code -# cp ../ExecuteStage/utils.py ../.temp_to_pub/EasySpider_Linux_x64/Code -cp ../ExecuteStage/*.py ../.temp_to_pub/EasySpider_Linux_x64/Code -cp ../ExecuteStage/requirements.txt ../.temp_to_pub/EasySpider_Linux_x64/Code -cp ../ExecuteStage/Readme.md ../.temp_to_pub/EasySpider_Linux_x64/Code -cp ../ExecuteStage/myCode.py ../.temp_to_pub/EasySpider_Linux_x64 -cp -Rf ../ExecuteStage/undetected_chromedriver_ES ../.temp_to_pub/EasySpider_Linux_x64/Code -cp -Rf ../ExecuteStage/.vscode ../.temp_to_pub/EasySpider_Linux_x64/Code -chmod 777 ../.temp_to_pub/EasySpider_Linux_x64/easy-spider.sh -rm -rf ../.temp_to_pub/EasySpider_Linux_x64/user_data -rm -rf ../.temp_to_pub/EasySpider_Linux_x64/execution_instances -mkdir ../.temp_to_pub/EasySpider_Linux_x64/execution_instances -rm -rf ../.temp_to_pub/EasySpider_Linux_x64/Data -mkdir ../.temp_to_pub/EasySpider_Linux_x64/Data -rm EasySpider_zh.crx -rm EasySpider_en.crx -cp -Rf ./tasks/* ../.temp_to_pub/EasySpider_Linux_x64/tasks +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" +STAGE_DIR="$REPO_ROOT/.temp_to_pub/EasySpider_Linux_x64" + +if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then + echo "This script must run on a native Linux x86_64 build host." >&2 + exit 1 +fi + +CHROME_DIR="$SCRIPT_DIR/chrome_linux64" +for required_file in chrome chromedriver_linux64; do + if [[ ! -f "$CHROME_DIR/$required_file" ]]; then + echo "Missing Linux x64 browser file: $CHROME_DIR/$required_file" >&2 + exit 1 + fi +done + +# main.js starts this launcher from chrome_linux64. Keep the launcher beside +# the execution-stage binary so it can resolve that binary relative to itself. +cp "$SCRIPT_DIR/execute_linux64.sh" "$CHROME_DIR/execute_linux64.sh" +chmod 755 "$CHROME_DIR/chrome" "$CHROME_DIR/chromedriver_linux64" \ + "$CHROME_DIR/execute_linux64.sh" + +rm -rf "$SCRIPT_DIR/out" +(cd "$REPO_ROOT/Extension/manifest_v3" && node package.js) +(cd "$SCRIPT_DIR" && npm run package -- --platform=linux --arch=x64) + +PACKAGED_APP="$SCRIPT_DIR/out/EasySpider-linux-x64" +if [[ ! -d "$PACKAGED_APP" ]]; then + echo "Electron Forge did not produce $PACKAGED_APP" >&2 + exit 1 +fi + +APP_RESOURCES="$PACKAGED_APP/resources/app" +rm -rf "$APP_RESOURCES/chrome_win32" "$APP_RESOURCES/chrome_win64" \ + "$APP_RESOURCES/chrome_mac64.app" \ + "$APP_RESOURCES/chromedrivers" "$APP_RESOURCES/Data" \ + "$APP_RESOURCES/.idea" "$APP_RESOURCES/tasks" \ + "$APP_RESOURCES/execution_instances" "$APP_RESOURCES/user_data" \ + "$APP_RESOURCES/TempUserDataFolder" +rm -f "$APP_RESOURCES/vs_BuildTools.exe" "$APP_RESOURCES/VS_BuildTools.exe" \ + "$APP_RESOURCES/chromedriver_mac64" + +mkdir -p "$STAGE_DIR" +rm -rf "$STAGE_DIR/EasySpider" "$STAGE_DIR/Code" "$STAGE_DIR/Data" \ + "$STAGE_DIR/execution_instances" "$STAGE_DIR/tasks" \ + "$STAGE_DIR/user_data" "$STAGE_DIR/TempUserDataFolder" +rm -f "$STAGE_DIR/config.json" "$STAGE_DIR/mysql_config.json" +mkdir -p "$STAGE_DIR/Code" "$STAGE_DIR/Data" "$STAGE_DIR/execution_instances" "$STAGE_DIR/tasks" +mv "$PACKAGED_APP" "$STAGE_DIR/EasySpider" + +cp "$REPO_ROOT/ExecuteStage"/*.py "$STAGE_DIR/Code/" +cp "$REPO_ROOT/ExecuteStage/requirements.txt" "$STAGE_DIR/Code/" +cp "$REPO_ROOT/ExecuteStage/Readme.md" "$STAGE_DIR/Code/" +cp "$REPO_ROOT/ExecuteStage/myCode.py" "$STAGE_DIR/" +cp -R "$REPO_ROOT/ExecuteStage/undetected_chromedriver_ES" "$STAGE_DIR/Code/" +cp -R "$REPO_ROOT/ExecuteStage/.vscode" "$STAGE_DIR/Code/" + +find "$STAGE_DIR" -type d -name __pycache__ -prune -exec rm -rf {} + +find "$STAGE_DIR" -type d -name .pytest_cache -prune -exec rm -rf {} + + +copy_tracked_tasks() { + while IFS= read -r -d '' task; do + relative="${task#tasks/}" + destination="$STAGE_DIR/tasks/$relative" + mkdir -p "$(dirname -- "$destination")" + cp "$SCRIPT_DIR/$task" "$destination" + done < <(git -c safe.directory='*' -C "$SCRIPT_DIR" ls-files -z -- tasks) +} +copy_tracked_tasks + +chmod 755 "$STAGE_DIR/easy-spider.sh" "$STAGE_DIR/first_time_run.sh" 2>/dev/null || true +chmod 755 "$STAGE_DIR/EasySpider/resources/app/chrome_linux64/chrome" \ + "$STAGE_DIR/EasySpider/resources/app/chrome_linux64/chromedriver_linux64" \ + "$STAGE_DIR/EasySpider/resources/app/chrome_linux64/easyspider_executestage" \ + "$STAGE_DIR/EasySpider/resources/app/chrome_linux64/execute_linux64.sh" \ + 2>/dev/null || true +echo "Linux x64 application staged at $STAGE_DIR" diff --git a/ElectronJS/package_macos.sh b/ElectronJS/package_macos.sh index 30c4f95..950fb33 100755 --- a/ElectronJS/package_macos.sh +++ b/ElectronJS/package_macos.sh @@ -1,37 +1,87 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -# This script is used to build.md the package for MacOS. -cd ../Extension/manifest_v3/ -node package.js -cd ../../ElectronJS -rm -rf out -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app -npm run make -unzip out/make/zip/darwin/*64/EasySpider-darwin* -d ../.temp_to_pub/EasySpider_MacOS/ -# 使用 adhoc 签名 -# 这样程序在 macOS 就会显示为“无法验证开发者,你应将其移动到废纸篓”而不是“此应用已损坏,无法打开”了。 -# 这样软件就可以直接在设置-隐私与安全性中批准打开,而不需要 `xattr -d` 强行清除属性 -# 这不是一个真正的签名,只是一个空签名,不能让软件通过门禁认证。 -codesign --force --deep --sign - ../.temp_to_pub/EasySpider_MacOS/EasySpider.app -# mv out/EasySpider-darwin-*64/EasySpider.app ../.temp_to_pub/EasySpider_MacOS/ -rm ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/VS_BuildTools.exe -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/chrome_win64 -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/chromedrivers -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/Data -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/.idea -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/tasks -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/execution_instances -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/user_data -rm -r ../.temp_to_pub/EasySpider_MacOS/EasySpider.app/Contents/Resources/app/TempUserDataFolder -rm -rf ../.temp_to_pub/EasySpider_MacOS/Code -mkdir ../.temp_to_pub/EasySpider_MacOS/Code -# cp ../ExecuteStage/easyspider_executestage.py ../.temp_to_pub/EasySpider_MacOS/Code -# cp ../ExecuteStage/myChrome.py ../.temp_to_pub/EasySpider_MacOS/Code -# cp ../ExecuteStage/utils.py ../.temp_to_pub/EasySpider_MacOS/Code -cp ../ExecuteStage/*.py ../.temp_to_pub/EasySpider_MacOS/Code -cp ../ExecuteStage/requirements.txt ../.temp_to_pub/EasySpider_MacOS/Code -cp ../ExecuteStage/Readme.md ../.temp_to_pub/EasySpider_MacOS/Code -cp ../ExecuteStage/myCode.py ../.temp_to_pub/EasySpider_MacOS -cp -Rf ../ExecuteStage/undetected_chromedriver_ES ../.temp_to_pub/EasySpider_MacOS/Code -cp -Rf ../ExecuteStage/.vscode ../.temp_to_pub/EasySpider_MacOS/Code -cp -Rf ./tasks/* "../.temp_to_pub/EasySpider_MacOS/Sample Tasks" +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" +STAGE_DIR="$REPO_ROOT/.temp_to_pub/EasySpider_MacOS" + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "This script must run on macOS." >&2 + exit 1 +fi + +HOST_ARCH="$(uname -m)" +case "$HOST_ARCH" in + arm64) ELECTRON_ARCH=arm64 ;; + x86_64) ELECTRON_ARCH=x64 ;; + *) echo "Unsupported macOS architecture: $HOST_ARCH" >&2; exit 1 ;; +esac + +(cd "$REPO_ROOT/Extension/manifest_v3" && node package.js) +rm -rf "$SCRIPT_DIR/out" "$STAGE_DIR/EasySpider.app" +(cd "$SCRIPT_DIR" && npm run make -- --platform=darwin --arch="$ELECTRON_ARCH") + +ZIP_PATH="$(find "$SCRIPT_DIR/out/make/zip/darwin/$ELECTRON_ARCH" -type f -name '*.zip' -print -quit 2>/dev/null || true)" +if [[ -z "$ZIP_PATH" || ! -f "$ZIP_PATH" ]]; then + echo "Electron Forge did not produce a macOS $ELECTRON_ARCH zip." >&2 + exit 1 +fi +mkdir -p "$STAGE_DIR" +unzip -q "$ZIP_PATH" -d "$STAGE_DIR" + +APP_PATH="$STAGE_DIR/EasySpider.app" +APP_RESOURCES="$APP_PATH/Contents/Resources/app" +if [[ ! -d "$APP_PATH" ]]; then + echo "Packaged application was not found at $APP_PATH" >&2 + exit 1 +fi + +rm -f "$APP_RESOURCES/vs_BuildTools.exe" "$APP_RESOURCES/VS_BuildTools.exe" +rm -rf "$APP_RESOURCES/chrome_win32" "$APP_RESOURCES/chrome_win64" \ + "$APP_RESOURCES/chrome_linux64" \ + "$APP_RESOURCES/chromedrivers" "$APP_RESOURCES/Data" \ + "$APP_RESOURCES/.idea" "$APP_RESOURCES/tasks" \ + "$APP_RESOURCES/execution_instances" "$APP_RESOURCES/user_data" \ + "$APP_RESOURCES/TempUserDataFolder" + +rm -rf "$STAGE_DIR/Code" "$STAGE_DIR/Sample Tasks" "$STAGE_DIR/Data" \ + "$STAGE_DIR/execution_instances" "$STAGE_DIR/user_data" \ + "$STAGE_DIR/TempUserDataFolder" +rm -f "$STAGE_DIR/config.json" "$STAGE_DIR/mysql_config.json" +mkdir -p "$STAGE_DIR/Code" "$STAGE_DIR/Sample Tasks" "$STAGE_DIR/Data" \ + "$STAGE_DIR/execution_instances" +cp "$REPO_ROOT/ExecuteStage"/*.py "$STAGE_DIR/Code/" +cp "$REPO_ROOT/ExecuteStage/requirements.txt" "$STAGE_DIR/Code/" +cp "$REPO_ROOT/ExecuteStage/Readme.md" "$STAGE_DIR/Code/" +cp "$REPO_ROOT/ExecuteStage/myCode.py" "$STAGE_DIR/" +cp -R "$REPO_ROOT/ExecuteStage/undetected_chromedriver_ES" "$STAGE_DIR/Code/" +cp -R "$REPO_ROOT/ExecuteStage/.vscode" "$STAGE_DIR/Code/" + +while IFS= read -r -d '' task; do + relative="${task#tasks/}" + destination="$STAGE_DIR/Sample Tasks/$relative" + mkdir -p "$(dirname -- "$destination")" + cp "$SCRIPT_DIR/$task" "$destination" +done < <(git -c safe.directory='*' -C "$SCRIPT_DIR" ls-files -z -- tasks) + +find "$STAGE_DIR" -type d -name __pycache__ -prune -exec rm -rf {} + +find "$STAGE_DIR" -type d -name .pytest_cache -prune -exec rm -rf {} + +chmod 755 "$STAGE_DIR/first_time_run.sh" "$STAGE_DIR/execute_macos.sh" 2>/dev/null || true +chmod 755 "$STAGE_DIR/easyspider_executestage" "$STAGE_DIR/easyspider_executestage_full" 2>/dev/null || true + +# Sign only after all files have been copied/deleted. Set CODESIGN_IDENTITY to +# a Developer ID identity for a distributable build; the default '-' is ad hoc. +CODESIGN_IDENTITY="${CODESIGN_IDENTITY:--}" +sign_path() { + local path="$1" + if [[ "$CODESIGN_IDENTITY" == "-" ]]; then + codesign --force --deep --sign - "$path" + else + codesign --force --deep --options runtime --timestamp --sign "$CODESIGN_IDENTITY" "$path" + fi +} +sign_path "$STAGE_DIR/easyspider_executestage" +sign_path "$STAGE_DIR/easyspider_executestage_full" +sign_path "$APP_PATH" +codesign --verify --deep --strict "$APP_PATH" +echo "macOS $HOST_ARCH application staged at $STAGE_DIR" diff --git a/ElectronJS/selenium-webdriver-bidi-webExtension/webExtension.js b/ElectronJS/selenium-webdriver-bidi-webExtension/webExtension.js index dcf8d96..11d07ff 100644 --- a/ElectronJS/selenium-webdriver-bidi-webExtension/webExtension.js +++ b/ElectronJS/selenium-webdriver-bidi-webExtension/webExtension.js @@ -61,6 +61,10 @@ class WebExtension { console.log('Type:', response.type) console.log('Result:', response.result) console.log('Full:', JSON.stringify(response, null, 2)) + + if (response.type === 'error') { + throw new Error(`${response.error}: ${response.message}`) + } return response.result.extension } diff --git a/ElectronJS/src/index.html b/ElectronJS/src/index.html index ac8ea94..c7d82b2 100644 --- a/ElectronJS/src/index.html +++ b/ElectronJS/src/index.html @@ -89,7 +89,7 @@ EasySpider is a completely free and ad-free open-source software. The developmen Browse official website to watch tutorials

-

Current Version: v0.6.3

+

Current Version: v0.6.5

Newest Version: {{newest_version}}

@@ -191,7 +191,7 @@ EasySpider is a completely free and ad-free open-source software. The developmen 点此访问官网查看文档/视频教程

-

软件当前版本:v0.6.3

+

软件当前版本:v0.6.5

官网最新版本:{{newest_version}}

diff --git a/ElectronJS/src/taskGrid/logic.js b/ElectronJS/src/taskGrid/logic.js index ddacfb2..32ca03b 100644 --- a/ElectronJS/src/taskGrid/logic.js +++ b/ElectronJS/src/taskGrid/logic.js @@ -491,7 +491,7 @@ if (mobile == "true") { } let serviceInfo = { - "version": "0.6.3" + "version": "0.6.5" }; function saveService(type) { @@ -625,7 +625,7 @@ function saveService(type) { "links": links, "create_time": $("#create_time").val(), "update_time": formatDateTime(new Date()), - "version": "0.6.3", + "version": "0.6.5", "saveThreshold": saveThreshold, // "cloudflare": cloudflare, "quitWaitTime": parseInt($("#quitWaitTime").val()), diff --git a/ExecuteStage/easyspider_executestage.py b/ExecuteStage/easyspider_executestage.py index 321aa0f..614e061 100644 --- a/ExecuteStage/easyspider_executestage.py +++ b/ExecuteStage/easyspider_executestage.py @@ -2310,7 +2310,7 @@ if __name__ == '__main__': "remote_control_key": "", # 如果开启远程控制,那么初始化时需要填写此 key,保证安全性 "keyboard": True, # 是否监听键盘输入 "pause_key": "p", # 暂停键 - "version": "0.6.3", + "version": "0.6.5", "docker_driver": "", "user_folder": "", } @@ -2326,8 +2326,6 @@ if __name__ == '__main__': os.mkdir(os.getcwd() + "/Data") if sys.platform == "darwin" and platform.architecture()[0] == "64bit": options.binary_location = "EasySpider.app/Contents/Resources/app/chrome_mac64.app/Contents/MacOS/Google Chrome" - options.add_extension( - "EasySpider.app/Contents/Resources/app/XPathHelper.crx") driver_path = "EasySpider.app/Contents/Resources/app/chromedriver_mac64" print(driver_path) if c.config_folder == "": @@ -2341,7 +2339,6 @@ if __name__ == '__main__': # 相对于下一条检查语句,这里去掉了 EasySpider 文件夹这一层 options.binary_location = "resources/app/chrome_linux64/chrome" driver_path = "resources/app/chrome_linux64/chromedriver_linux64" - options.add_extension("resources/app/XPathHelper.crx") elif os.path.exists(os.getcwd() + "/EasySpider/resources"): # 打包后的路径 print("Finding chromedriver in EasySpider", os.getcwd() + "/EasySpider") @@ -2350,17 +2347,14 @@ if __name__ == '__main__': os.getcwd(), "EasySpider/resources/app/chrome_win32/chrome.exe") # 指定chrome位置 driver_path = os.path.join( os.getcwd(), "EasySpider/resources/app/chrome_win32/chromedriver_win32.exe") - options.add_extension("EasySpider/resources/app/XPathHelper.crx") elif sys.platform == "win32" and platform.architecture()[0] == "64bit": options.binary_location = os.path.join( os.getcwd(), "EasySpider/resources/app/chrome_win64/chrome.exe") driver_path = os.path.join( os.getcwd(), "EasySpider/resources/app/chrome_win64/chromedriver_win64.exe") - options.add_extension("EasySpider/resources/app/XPathHelper.crx") elif sys.platform == "linux" and platform.architecture()[0] == "64bit": options.binary_location = "EasySpider/resources/app/chrome_linux64/chrome" driver_path = "EasySpider/resources/app/chrome_linux64/chromedriver_linux64" - options.add_extension("EasySpider/resources/app/XPathHelper.crx") else: print("Unsupported platform") sys.exit() @@ -2375,22 +2369,18 @@ if __name__ == '__main__': os.getcwd(), "EasySpider/resources/app/chrome_win32/chrome.exe") # 指定chrome位置 driver_path = os.path.join( os.getcwd(), "EasySpider/resources/app/chrome_win32/chromedriver_win32.exe") - options.add_extension("EasySpider/resources/app/XPathHelper.crx") elif sys.platform == "win32" and platform.architecture()[0] == "64bit": options.binary_location = "../ElectronJS/chrome_win64/chrome.exe" # 指定chrome位置 driver_path = "../ElectronJS/chrome_win64/chromedriver_win64.exe" - options.add_extension("../ElectronJS/XPathHelper.crx") elif sys.platform == "linux" and platform.architecture()[0] == "64bit": options.binary_location = "../ElectronJS/chrome_linux64/chrome" driver_path = "../ElectronJS/chrome_linux64/chromedriver_linux64" - options.add_extension("../ElectronJS/XPathHelper.crx") else: print("Unsupported platform for automatic detection. You need to specify chrome executable path, chromedriver path in code.") sys.exit() else: options.binary_location = "./chrome.exe" # 指定chrome位置 driver_path = "./chromedriver.exe" - options.add_extension("XPathHelper.crx") options.add_experimental_option( 'excludeSwitches', ['enable-automation']) # 以开发者模式 diff --git a/ExecuteStage/generateEXE_win64.cmd b/ExecuteStage/generateEXE_win64.cmd index 0f5c647..6222bf1 100644 --- a/ExecuteStage/generateEXE_win64.cmd +++ b/ExecuteStage/generateEXE_win64.cmd @@ -1,7 +1,27 @@ -rmdir /s /Q build -rmdir /s /Q dist -@REM --add-data "C:\Python311\Lib\site-packages\ddddocr\common.onnx;ddddocr" -@REM pyinstaller -F --icon=favicon.ico easyspider_executestage.py -pyinstaller -F --icon=favicon.ico --add-data "C:\Python311\Lib\site-packages\onnxruntime\capi\onnxruntime_providers_shared.dll;onnxruntime\capi" --add-data "C:\Python311\Lib\site-packages\ddddocr\common_old.onnx;ddddocr" easyspider_executestage.py -del ..\ElectronJS\chrome_win64\easyspider_executestage.exe -copy dist\easyspider_executestage.exe ..\ElectronJS\chrome_win64\easyspider_executestage.exe \ No newline at end of file +@echo off +setlocal + +if exist build rmdir /s /Q build +if exist dist rmdir /s /Q dist + +for /f "delims=" %%P in ('python -c "import onnxruntime, pathlib; print(pathlib.Path(onnxruntime.__file__).parent / 'capi' / 'onnxruntime_providers_shared.dll')"') do set "ONNX_DLL=%%P" +for /f "delims=" %%P in ('python -c "import ddddocr, pathlib; print(pathlib.Path(ddddocr.__file__).parent / 'common_old.onnx')"') do set "DDDDOCR_MODEL=%%P" + +if not exist "%ONNX_DLL%" ( + echo Could not find onnxruntime_providers_shared.dll in the active Python environment. + exit /b 1 +) +if not exist "%DDDDOCR_MODEL%" ( + echo Could not find ddddocr common_old.onnx in the active Python environment. + exit /b 1 +) + +pyinstaller -F --icon=favicon.ico --hidden-import selenium.webdriver.ie.webdriver --add-data "%ONNX_DLL%;onnxruntime\capi" --add-data "%DDDDOCR_MODEL%;ddddocr" easyspider_executestage.py +if errorlevel 1 exit /b %errorlevel% +if not exist dist\easyspider_executestage.exe exit /b 1 + +if not exist ..\ElectronJS\chrome_win64 mkdir ..\ElectronJS\chrome_win64 +if exist ..\ElectronJS\chrome_win64\easyspider_executestage.exe del /f /q ..\ElectronJS\chrome_win64\easyspider_executestage.exe +copy /Y dist\easyspider_executestage.exe ..\ElectronJS\chrome_win64\easyspider_executestage.exe +if errorlevel 1 exit /b %errorlevel% +echo Windows x64 execution stage packaged successfully. diff --git a/ExecuteStage/generateExecutable_Linux64.sh b/ExecuteStage/generateExecutable_Linux64.sh index 566426a..b9ef912 100755 --- a/ExecuteStage/generateExecutable_Linux64.sh +++ b/ExecuteStage/generateExecutable_Linux64.sh @@ -1,11 +1,47 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -rm -r build -rm -r dist -# 一定要先source一下,不然会出现找不到conda命令的错误!!! -source ~/miniconda3/etc/profile.d/conda.sh -conda activate easyspider -# Python一定要是3.11版本,不然会出现浏览器弹出崩溃的错误!!!原来使用的3.8,崩溃原因未知。 -pyinstaller -F --add-data "/home/naibo/miniconda3/envs/easyspider/lib/python3.11/site-packages/onnxruntime/capi/onnxruntime_pybind11_state.cpython-311-x86_64-linux-gnu.so:onnxruntime/capi" --add-data "/home/naibo/miniconda3/envs/easyspider/lib/python3.11/site-packages/ddddocr/common_old.onnx:ddddocr" easyspider_executestage.py -rm ../ElectronJS/chrome_linux64/easyspider_executestage -cp dist/easyspider_executestage ../ElectronJS/chrome_linux64/easyspider_executestage +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" +PYTHON_BIN="${PYTHON_BIN:-python3}" + +if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "x86_64" ]]; then + echo "This script must run on a native Linux x86_64 build host." >&2 + exit 1 +fi + +PYTHON_VERSION="$($PYTHON_BIN -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" +PYTHON_ARCH="$($PYTHON_BIN -c 'import platform; print(platform.machine())')" +if [[ "$PYTHON_VERSION" != "3.11" || "$PYTHON_ARCH" != "x86_64" ]]; then + echo "Python 3.11 x86_64 is required; found Python $PYTHON_VERSION $PYTHON_ARCH." >&2 + exit 1 +fi + +DDDDOCR_MODEL="$($PYTHON_BIN -c 'import ddddocr, pathlib; print(pathlib.Path(ddddocr.__file__).resolve().parent / "common_old.onnx")')" +ONNX_STATE="$($PYTHON_BIN -c 'import onnxruntime, pathlib; capi = pathlib.Path(onnxruntime.__file__).resolve().parent / "capi"; matches = sorted(capi.glob("onnxruntime_pybind11_state*.so")); print(matches[0] if matches else "")')" + +if [[ ! -f "$DDDDOCR_MODEL" ]]; then + echo "Could not find ddddocr/common_old.onnx in the active Python environment." >&2 + exit 1 +fi +if [[ ! -f "$ONNX_STATE" ]]; then + echo "Could not find onnxruntime_pybind11_state*.so in the active Python environment." >&2 + exit 1 +fi + +cd "$SCRIPT_DIR" +rm -rf build dist + +"$PYTHON_BIN" -m PyInstaller \ + --noconfirm \ + --clean \ + --onefile \ + --hidden-import selenium.webdriver.ie.webdriver \ + --add-binary "$ONNX_STATE:onnxruntime/capi" \ + --add-data "$DDDDOCR_MODEL:ddddocr" \ + easyspider_executestage.py + +TARGET_DIR="$REPO_ROOT/ElectronJS/chrome_linux64" +mkdir -p "$TARGET_DIR" +install -m 755 dist/easyspider_executestage "$TARGET_DIR/easyspider_executestage" +echo "Linux x64 execution stage packaged successfully." diff --git a/ExecuteStage/generateExecutable_Macos.sh b/ExecuteStage/generateExecutable_Macos.sh index a0a873d..c1fd440 100755 --- a/ExecuteStage/generateExecutable_Macos.sh +++ b/ExecuteStage/generateExecutable_Macos.sh @@ -1,22 +1,73 @@ -# 先打包一个不带ddddocr和pandas的版本,然后再打包一个带的版本,不带ddddocr和pandas的版本运行速度会快很多 -rm -r build -rm -r dist -pyinstaller -F --icon=favicon.ico easyspider_executestage.py --exclude-module ddddocr --exclude-module onnxruntime --exclude-module onnx --exclude-module onnxruntime_pybind11_state.so --exclude-module pillow --exclude-module pandas --exclude-module numpy --exclude-module scipy --exclude-module sklearn +#!/usr/bin/env bash +set -euo pipefail -rm ../.temp_to_pub/EasySpider_MacOS/easyspider_executestage -cp dist/easyspider_executestage ../.temp_to_pub/EasySpider_MacOS/easyspider_executestage -# mv dist/easyspider_executestage ../ElectronJS/easyspider_executestage +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" +PYTHON_BIN="${PYTHON_BIN:-python3}" -echo "With ddddocr and pandas" +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "This script must run on macOS." >&2 + exit 1 +fi -# # 打包带ddddocr和pandas的版本 -rm -r build -rm -r dist -# Get the site-packages path for ddddocr and onnxruntime -# 如果当前终端激活了 conda 环境,下方脚本应当可以正确的从 conda 环境安装的包中获得数据文件位置 -ddddocr_path=$(python3 -c "import ddddocr; print(ddddocr.__path__[0])") -onnxruntime_path=$(python3 -c "import onnxruntime; print(onnxruntime.__path__[0])") +HOST_ARCH="$(uname -m)" +PYTHON_VERSION="$($PYTHON_BIN -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" +PYTHON_ARCH="$($PYTHON_BIN -c 'import platform; print(platform.machine())')" +if [[ "$HOST_ARCH" != "arm64" && "$HOST_ARCH" != "x86_64" ]]; then + echo "Unsupported macOS architecture: $HOST_ARCH" >&2 + exit 1 +fi +if [[ "$PYTHON_VERSION" != "3.11" || "$PYTHON_ARCH" != "$HOST_ARCH" ]]; then + echo "Native Python 3.11 $HOST_ARCH is required; found Python $PYTHON_VERSION $PYTHON_ARCH." >&2 + exit 1 +fi -pyinstaller -F --icon=favicon.ico --add-data "$onnxruntime_path/capi/onnxruntime_pybind11_state.so:onnxruntime/capi" --add-data "$ddddocr_path/common_old.onnx:ddddocr" easyspider_executestage.py -rm ../.temp_to_pub/EasySpider_MacOS/easyspider_executestage_full -cp dist/easyspider_executestage ../.temp_to_pub/EasySpider_MacOS/easyspider_executestage_full \ No newline at end of file +STAGE_DIR="$REPO_ROOT/.temp_to_pub/EasySpider_MacOS" +mkdir -p "$STAGE_DIR" +cd "$SCRIPT_DIR" + +# The lightweight executor omits OCR and Pandas but retains Pillow, which is +# imported by the execution-stage program independently of OCR. +rm -rf build dist +"$PYTHON_BIN" -m PyInstaller \ + --noconfirm \ + --clean \ + --onefile \ + --target-arch "$HOST_ARCH" \ + --icon favicon.ico \ + --hidden-import selenium.webdriver.ie.webdriver \ + --exclude-module ddddocr \ + --exclude-module onnxruntime \ + --exclude-module onnx \ + --exclude-module pandas \ + --exclude-module numpy \ + --exclude-module scipy \ + --exclude-module sklearn \ + easyspider_executestage.py +install -m 755 dist/easyspider_executestage "$STAGE_DIR/easyspider_executestage" + +DDDDOCR_MODEL="$($PYTHON_BIN -c 'import ddddocr, pathlib; print(pathlib.Path(ddddocr.__file__).resolve().parent / "common_old.onnx")')" +ONNX_STATE="$($PYTHON_BIN -c 'import onnxruntime, pathlib; capi = pathlib.Path(onnxruntime.__file__).resolve().parent / "capi"; matches = sorted(capi.glob("onnxruntime_pybind11_state*.so")); print(matches[0] if matches else "")')" + +if [[ ! -f "$DDDDOCR_MODEL" ]]; then + echo "Could not find ddddocr/common_old.onnx in the active Python environment." >&2 + exit 1 +fi +if [[ ! -f "$ONNX_STATE" ]]; then + echo "Could not find onnxruntime_pybind11_state*.so in the active Python environment." >&2 + exit 1 +fi + +rm -rf build dist +"$PYTHON_BIN" -m PyInstaller \ + --noconfirm \ + --clean \ + --onefile \ + --target-arch "$HOST_ARCH" \ + --icon favicon.ico \ + --hidden-import selenium.webdriver.ie.webdriver \ + --add-binary "$ONNX_STATE:onnxruntime/capi" \ + --add-data "$DDDDOCR_MODEL:ddddocr" \ + easyspider_executestage.py +install -m 755 dist/easyspider_executestage "$STAGE_DIR/easyspider_executestage_full" +echo "macOS $HOST_ARCH execution stages packaged successfully." diff --git a/ExecuteStage/requirements.txt b/ExecuteStage/requirements.txt index 496b90f..8b1a1c5 100644 --- a/ExecuteStage/requirements.txt +++ b/ExecuteStage/requirements.txt @@ -1,14 +1,14 @@ commandline_config==2.2.3 -requests==2.32.3 -selenium==4.27.1 -pyinstaller==5.13.2 -Pillow==10.2.0 -xlsxwriter==3.2.0 -openpyxl==3.1.2 -pymysql==1.1.1 -lxml==5.3.0 -ddddocr==1.5.6 -pynput==1.7.6 -beautifulsoup4==4.12.2 -undetected-chromedriver==3.4.7 -pandas==2.2.3 +requests==2.34.2 +selenium==4.47.0 +pyinstaller==6.22.1 +Pillow==12.3.0 +xlsxwriter==3.2.9 +openpyxl==3.1.5 +pymysql==1.2.0 +lxml==6.1.1 +ddddocr==1.6.1 +pynput==1.8.2 +beautifulsoup4==4.15.0 +undetected-chromedriver==3.5.5 +pandas==3.0.5 diff --git a/Extension/manifest_v3/package-lock.json b/Extension/manifest_v3/package-lock.json index 7c14b2e..627da8b 100644 --- a/Extension/manifest_v3/package-lock.json +++ b/Extension/manifest_v3/package-lock.json @@ -1,12 +1,12 @@ { "name": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "license": "AGPL-3.0", "dependencies": { "crx": "^5.0.1", diff --git a/Extension/manifest_v3/package.json b/Extension/manifest_v3/package.json index 9c054ad..7e6e42a 100644 --- a/Extension/manifest_v3/package.json +++ b/Extension/manifest_v3/package.json @@ -1,6 +1,6 @@ { "name": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "type": "module", "scripts": { "build": "rollup -c", @@ -48,4 +48,4 @@ "rollup-plugin-zip": "^1.0.1", "typescript": "^4.4.3" } -} \ No newline at end of file +} diff --git a/Extension/manifest_v3/src/manifest.json b/Extension/manifest_v3/src/manifest.json index 9f0cc8a..5c0efb4 100644 --- a/Extension/manifest_v3/src/manifest.json +++ b/Extension/manifest_v3/src/manifest.json @@ -1,6 +1,6 @@ { "name": "EasySpider", - "version": "0.6.3", + "version": "0.6.5", "description": "EasySpider's chrome extension", "author": "Naibo Wang", "manifest_version": 3, @@ -57,4 +57,4 @@ "activeTab", "notifications" ] -} \ No newline at end of file +} diff --git a/scripts/smoke_python_dependencies.py b/scripts/smoke_python_dependencies.py new file mode 100644 index 0000000..fd4b9a3 --- /dev/null +++ b/scripts/smoke_python_dependencies.py @@ -0,0 +1,122 @@ +"""Small, side-effect-free smoke tests for EasySpider's Python dependencies.""" + +from __future__ import annotations + +import io +import json +from importlib import metadata + + +def package_version(distribution: str) -> str: + return metadata.version(distribution) + + +def main() -> None: + results: dict[str, object] = {} + + # Image: exercise both Pillow and OpenCV using an in-memory PNG. + import cv2 + import numpy as np + from PIL import Image, ImageDraw + + image = Image.new("RGB", (96, 32), "white") + ImageDraw.Draw(image).text((8, 8), "1234", fill="black") + png = io.BytesIO() + image.save(png, format="PNG") + png_bytes = png.getvalue() + decoded = cv2.imdecode(np.frombuffer(png_bytes, dtype=np.uint8), cv2.IMREAD_COLOR) + assert image.rotate(90, expand=True).size == (32, 96) + assert decoded is not None and decoded.shape[:2] == (32, 96) + results["image"] = { + "Pillow": package_version("Pillow"), + "opencv-python": package_version("opencv-python"), + "decoded_shape": list(decoded.shape), + } + + # OCR: initialize the bundled ONNX model and perform one inference. + import ddddocr + + ocr = ddddocr.DdddOcr(show_ad=False) + ocr_text = ocr.classification(png_bytes) + assert isinstance(ocr_text, str) + results["ocr"] = { + "ddddocr": package_version("ddddocr"), + "onnxruntime": package_version("onnxruntime"), + "result_type": type(ocr_text).__name__, + } + + # HTML/XML: parse HTML with BeautifulSoup and evaluate XML XPath with lxml. + from bs4 import BeautifulSoup + from lxml import etree + + soup = BeautifulSoup("

EasySpider

", "html.parser") + assert soup.select_one("h1").get_text(strip=True) == "EasySpider" + xml_root = etree.fromstring(b"ok") + assert xml_root.xpath("string(/root/item[@id='1'])") == "ok" + results["html_xml"] = { + "beautifulsoup4": package_version("beautifulsoup4"), + "lxml": package_version("lxml"), + } + + # Excel: write with XlsxWriter and read the same workbook with openpyxl. + import openpyxl + import xlsxwriter + + workbook_bytes = io.BytesIO() + workbook = xlsxwriter.Workbook(workbook_bytes, {"in_memory": True}) + sheet = workbook.add_worksheet("Smoke") + sheet.write(0, 0, "EasySpider") + sheet.write_number(0, 1, 65) + workbook.close() + workbook_bytes.seek(0) + loaded = openpyxl.load_workbook(workbook_bytes, read_only=True, data_only=True) + assert loaded["Smoke"]["A1"].value == "EasySpider" + assert loaded["Smoke"]["B1"].value == 65 + loaded.close() + results["excel"] = { + "xlsxwriter": package_version("xlsxwriter"), + "openpyxl": package_version("openpyxl"), + } + + # Pandas: exercise DataFrame creation, grouping, and aggregation. + import pandas as pd + + frame = pd.DataFrame({"group": ["a", "a", "b"], "value": [1, 2, 3]}) + totals = frame.groupby("group", sort=True)["value"].sum().to_dict() + assert totals == {"a": 3, "b": 3} + results["pandas"] = {"pandas": package_version("pandas"), "totals": totals} + + # Selenium: construct real Chrome options and an XPath locator without starting Chrome. + from selenium import webdriver + from selenium.webdriver.common.by import By + + options = webdriver.ChromeOptions() + options.add_argument("--headless=new") + locator = (By.XPATH, "//h1") + assert "--headless=new" in options.arguments and locator == ("xpath", "//h1") + results["selenium"] = { + "selenium": package_version("selenium"), + "arguments": options.arguments, + "locator": list(locator), + } + + # Keyboard: construct key representations without injecting input into the OS. + from pynput.keyboard import Key, KeyCode + + pause_key = KeyCode.from_char("p") + assert pause_key.char == "p" and Key.space.value is not None + results["keyboard"] = {"pynput": package_version("pynput"), "pause_key": pause_key.char} + + # MySQL client: exercise PyMySQL's query-string escaping without needing credentials. + import pymysql + from pymysql.converters import escape_string + + escaped = escape_string("O'Reilly") + assert escaped == "O\\'Reilly" + results["mysql_client"] = {"PyMySQL": package_version("PyMySQL"), "escaped": escaped} + + print(json.dumps(results, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/test_bundled_chrome.py b/scripts/test_bundled_chrome.py new file mode 100644 index 0000000..b4218ea --- /dev/null +++ b/scripts/test_bundled_chrome.py @@ -0,0 +1,66 @@ +"""Verify the Chrome/ChromeDriver bundled in a Windows EasySpider package.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from selenium import webdriver +from selenium.webdriver.chrome.service import Service +from selenium.webdriver.common.by import By + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "--stage", + type=Path, + default=Path(".temp_to_pub/EasySpider_windows_x64"), + help="root of the staged Windows package", + ) + parser.add_argument("--url", default="https://example.com/") + args = parser.parse_args() + + app = args.stage.resolve() / "EasySpider" / "resources" / "app" + chrome_binary = app / "chrome_win64" / "chrome.exe" + chrome_driver = app / "chrome_win64" / "chromedriver_win64.exe" + if not chrome_binary.is_file() or not chrome_driver.is_file(): + raise FileNotFoundError(f"bundled browser files are missing below {app}") + + options = webdriver.ChromeOptions() + options.binary_location = str(chrome_binary) + options.add_argument("--headless=new") + options.add_argument("--disable-gpu") + options.add_argument("--no-first-run") + options.add_argument("--no-default-browser-check") + options.add_argument("--ignore-certificate-errors") + + driver = webdriver.Chrome(service=Service(str(chrome_driver)), options=options) + try: + driver.set_page_load_timeout(30) + driver.get(args.url) + heading = driver.find_element(By.TAG_NAME, "h1").text.strip() + assert driver.title == "Example Domain", driver.title + assert heading == "Example Domain", heading + print( + json.dumps( + { + "url": driver.current_url, + "title": driver.title, + "heading": heading, + "browser_version": driver.capabilities.get("browserVersion"), + "platform": driver.capabilities.get("platformName"), + "chrome_binary": str(chrome_binary), + "chrome_driver": str(chrome_driver), + }, + ensure_ascii=False, + indent=2, + ) + ) + finally: + driver.quit() + + +if __name__ == "__main__": + main() diff --git a/scripts/test_xpath_helper_bidi.js b/scripts/test_xpath_helper_bidi.js new file mode 100644 index 0000000..af0248a --- /dev/null +++ b/scripts/test_xpath_helper_bidi.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +// Windows smoke test for the two unpacked extensions loaded by EasySpider. +// It uses the same Chrome, ChromeDriver, WebDriver BiDi helper and Chrome +// flags as ElectronJS/main.js, but serves a deterministic local test page. + +const http = require('http'); +const path = require('path'); + +const repoRoot = path.resolve(__dirname, '..'); +const electronRoot = process.env.EASYSPIDER_ELECTRON_ROOT + ? path.resolve(process.env.EASYSPIDER_ELECTRON_ROOT) + : path.join(repoRoot, 'ElectronJS'); +const seleniumRoot = path.join(electronRoot, 'node_modules', 'selenium-webdriver'); + +const {Builder, By, Key, until} = require(seleniumRoot); +const chrome = require(path.join(seleniumRoot, 'chrome')); +const getWebExtensionInstance = require( + path.join(electronRoot, 'selenium-webdriver-bidi-webExtension', 'webExtension') +); +const ExtensionData = require( + path.join(electronRoot, 'selenium-webdriver-bidi-webExtension', 'extensionData') +); + +const chromeBinary = path.join(electronRoot, 'chrome_win64', 'chrome.exe'); +const chromeDriver = path.join( + electronRoot, + 'chrome_win64', + 'chromedriver_win64.exe' +); +const easySpiderExtension = path.join(electronRoot, 'EasySpider_zh'); +const xpathHelperExtension = path.join(electronRoot, 'XPathHelper_MV3'); +const legacyXpathHelper = path.join(electronRoot, 'XPathHelper.crx'); + +function startTestServer() { + const html = ` + + XPath Helper BiDi Test + +

XPath Smoke Test

+
first
+
second
+ +`; + + const server = http.createServer((request, response) => { + response.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'}); + response.end(html); + }); + + return new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => resolve(server)); + }); +} + +function closeServer(server) { + return new Promise((resolve) => server.close(resolve)); +} + +async function main() { + const server = await startTestServer(); + const address = server.address(); + let driver; + + try { + const options = new chrome.Options(); + options.setChromeBinaryPath(chromeBinary); + options.set('webSocketUrl', true); + options.set('excludeSwitches', ['enable-automation']); + options.set('useAutomationExtension', false); + options.addArguments('--enable-unsafe-extension-debugging'); + options.addArguments('--remote-debugging-pipe'); + options.addArguments('--disable-blink-features=AutomationControlled'); + options.addArguments('--disable-infobars'); + options.addArguments('--disable-gpu'); + options.addArguments('--no-first-run'); + options.addArguments('--no-default-browser-check'); + if (process.env.HEADED !== '1') { + options.addArguments('--headless=new'); + } + + const serviceBuilder = new chrome.ServiceBuilder(chromeDriver); + driver = await new Builder() + .forBrowser('chrome') + .setChromeOptions(options) + .setChromeService(serviceBuilder) + .build(); + + const webExtension = await getWebExtensionInstance(driver); + let legacyArchiveError = null; + let legacyPathError = null; + if (process.env.TEST_LEGACY === '1') { + try { + await webExtension.install(ExtensionData.setArchivePath(legacyXpathHelper)); + } catch (error) { + legacyArchiveError = String(error && (error.stack || error.message || error)); + } + } + if (process.env.LEGACY_UNPACKED_PATH) { + try { + await webExtension.install( + ExtensionData.setPath(path.resolve(process.env.LEGACY_UNPACKED_PATH)) + ); + } catch (error) { + legacyPathError = String(error && (error.stack || error.message || error)); + } + } + + const easySpiderId = await webExtension.install( + ExtensionData.setPath(easySpiderExtension) + ); + const xpathHelperId = await webExtension.install( + ExtensionData.setPath(xpathHelperExtension) + ); + + await driver.get(`http://127.0.0.1:${address.port}/`); + await driver.wait(until.elementLocated(By.id('smoke-title')), 10000); + + await driver.actions() + .keyDown(Key.CONTROL) + .keyDown(Key.SHIFT) + .sendKeys('x') + .keyUp(Key.SHIFT) + .keyUp(Key.CONTROL) + .perform(); + + const xpathFrame = await driver.wait( + until.elementLocated(By.id('xh-bar')), + 10000 + ); + await driver.wait(async () => { + return !(await xpathFrame.getAttribute('class')).split(/\s+/).includes('hidden'); + }, 10000); + + await driver.switchTo().frame(xpathFrame); + const query = await driver.findElement(By.id('query')); + await query.clear(); + await query.sendKeys('//h1'); + + const nodeCount = await driver.findElement(By.id('node-count')); + await driver.wait(async () => (await nodeCount.getText()) === '1', 10000); + const nodeCountText = await nodeCount.getText(); + const resultText = await driver.findElement(By.id('results')).getAttribute('value'); + + await driver.switchTo().defaultContent(); + const highlighted = await driver.executeScript( + "return document.querySelectorAll('.xh-highlight').length" + ); + const title = await driver.getTitle(); + + if (highlighted !== 1) { + throw new Error(`Expected one highlighted XPath match, got ${highlighted}`); + } + + console.log(JSON.stringify({ + chromeBinary, + chromeDriver, + easySpiderId, + xpathHelperId, + title, + nodeCount: nodeCountText, + resultText, + highlighted, + legacyArchiveTested: process.env.TEST_LEGACY === '1', + legacyArchiveRejected: legacyArchiveError !== null, + legacyArchiveError, + legacyPathTested: Boolean(process.env.LEGACY_UNPACKED_PATH), + legacyPathRejected: legacyPathError !== null, + legacyPathError, + }, null, 2)); + } finally { + if (driver) { + await driver.quit(); + } + await closeServer(server); + } +} + +main().catch((error) => { + console.error(error && (error.stack || error)); + process.exitCode = 1; +});