├── .cursor └── rules │ ├── code.mdc │ └── project.mdc ├── .eslintignore ├── .eslintrc.js ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── banner.txt ├── docs ├── TODO.md └── translate.md ├── fuck-hot-compile.sh ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── ai │ └── deepseek.ts ├── analyzer │ ├── core-encoding │ │ ├── base64-analyzer │ │ │ └── base64-analyzer.ts │ │ ├── hex-encode-analyzer │ │ │ └── hex-encode-analyzer.ts │ │ └── url-encode-analyzer │ │ │ └── url-encode-analyzer.ts │ ├── encrypt │ │ ├── rsa │ │ │ ├── rsa-analyzer.ts │ │ │ └── rsa-context.ts │ │ └── sign │ │ │ ├── sign-analyzer.ts │ │ │ └── sign-context.ts │ ├── json-analyzer.ts │ ├── request-analyzer.ts │ ├── response-analyzer.ts │ └── stack │ │ └── stack-analyzer.ts ├── codec │ └── commons │ │ ├── aes-codec │ │ └── aes-codec.ts │ │ ├── base64-codec │ │ └── base64-codec.ts │ │ ├── des-codec │ │ └── des-codec.ts │ │ ├── gzip-codec │ │ └── gzip-codec.ts │ │ ├── hex-codec │ │ └── hex-codec.ts │ │ ├── protobuf-codec │ │ ├── base-parser.ts │ │ ├── core.ts │ │ ├── index.ts │ │ ├── protobuf-codec.ts │ │ ├── standard-parser.ts │ │ └── types.ts │ │ ├── rsa-codec │ │ └── rsa-codec.ts │ │ ├── url-encode-codec │ │ └── url-encode-codec.ts │ │ └── zip-codec │ │ └── zip-codec.ts ├── config │ └── ui │ │ ├── components │ │ ├── AboutTab.ts │ │ ├── Modal.ts │ │ ├── SettingsTab.ts │ │ └── XhrBreakpointsTab.ts │ │ ├── menu.ts │ │ ├── styles.ts │ │ ├── types.ts │ │ └── vueLoader.ts ├── context │ ├── auth-context.ts │ ├── body-context.ts │ ├── content-type.ts │ ├── context-location.ts │ ├── event-context.ts │ ├── fetch-context.ts │ ├── header-context.ts │ ├── header.ts │ ├── param-context.ts │ ├── param-type.ts │ ├── param.ts │ ├── request-context.ts │ ├── response-context.ts │ ├── url-context.ts │ └── xhr-context.ts ├── debuggers │ ├── debugger-tester.ts │ ├── debugger.ts │ ├── header-debugger.ts │ ├── id-generator.ts │ └── string-matcher.ts ├── hook │ ├── fetch │ │ ├── fetch-hook.ts │ │ ├── holder.ts │ │ ├── index.ts │ │ ├── logger │ │ │ ├── error-logger.ts │ │ │ ├── request-logger.ts │ │ │ └── response-logger.ts │ │ ├── request │ │ │ ├── parse-request-body.ts │ │ │ ├── parse-request-headers.ts │ │ │ ├── parse-request-info.ts │ │ │ ├── parse-request-object.ts │ │ │ ├── parse-request-url.ts │ │ │ └── update-url-context.ts │ │ └── response │ │ │ ├── parse-response-body.ts │ │ │ └── parse-response-headers.ts │ └── xhr │ │ ├── holder.ts │ │ ├── request │ │ ├── attribute │ │ │ └── add-on-abort-hook.ts │ │ └── method │ │ │ ├── add-abort-hook.ts │ │ │ ├── add-add-event-listener-hook.ts │ │ │ ├── add-open-hook.ts │ │ │ ├── add-override-mimetype-hook.ts │ │ │ ├── add-send-hook.ts │ │ │ └── add-set-request-header-hook.ts │ │ ├── response │ │ └── attribute │ │ │ ├── add-onreadystatechange-hook.ts │ │ │ ├── add-visit-response-header-hook.ts │ │ │ └── add-visit-response-property-hook.ts │ │ ├── xml-http-request-object-hook.ts │ │ └── xml-http-request-prototype-hook.ts ├── index.ts ├── init │ └── init.ts ├── logger │ ├── index.ts │ └── logger.ts ├── message-formatter │ ├── base-message.ts │ └── request │ │ └── method │ │ ├── open-message.ts │ │ └── send-message.ts ├── parser │ ├── array-buffer-body-parser.ts │ ├── blob-body-parser.ts │ ├── form-body-parser.ts │ ├── form-data-body-parser.ts │ ├── form-param-parser.ts │ ├── header-parser.ts │ ├── json-body-parser.ts │ ├── request-body-parser.ts │ ├── request-context-parser.ts │ ├── response-body-parser.ts │ ├── response-context-parser.ts │ ├── text-body-parser.ts │ ├── url-context-parser.ts │ ├── url-search-params-body-parser.ts │ └── xhr-context-parser.ts ├── storage │ ├── IStorage.ts │ ├── IndexedDBStorage.ts │ ├── StorageFactory.ts │ ├── TampermonkeyStorage.ts │ └── index.ts └── utils │ ├── code-util.ts │ ├── color-util.ts │ ├── id-generator.ts │ ├── log-util.ts │ ├── string-util.ts │ ├── unsafe-window.ts │ └── url-util.ts ├── test_data ├── intercetor-test.md ├── test-header.html ├── test-hook-xhr-hook.html ├── test-object-defineProperty.html └── test.html ├── tsconfig.json ├── userscript-headers.js ├── webpack.common.js ├── webpack.config.js ├── webpack.dev.js └── webpack.prod.js /.cursor/rules/code.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 代码规范 3 | globs: * 4 | alwaysApply: false 5 | --- 6 | - 你的代码应该尽量模块化,而不是把所有代码都堆在一个文件中 7 | - 当需要打印输出时,使用 src/logger 日志模块 8 | - 当需要存储时,使用 src/storage 存储模块 9 | - 当需要调用AI时,使用 src/ai 模块 10 | -------------------------------------------------------------------------------- /.cursor/rules/project.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 项目背景 3 | globs: * 4 | alwaysApply: false 5 | --- 6 | # 项目背景 7 | - 这是一个使用TypeScript开发的油猴扩展 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | *.config.js 4 | jest.config.js 5 | webpack.*.js 6 | .eslintrc.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: [ 5 | '@typescript-eslint', 6 | ], 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | rules: { 12 | '@typescript-eslint/no-unused-vars': ['warn', { 13 | 'argsIgnorePattern': '^_', 14 | 'varsIgnorePattern': '^_', 15 | 'caughtErrorsIgnorePattern': '^_' 16 | }], 17 | 'no-unused-vars': 'off' 18 | } 19 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | 7 | # Build output 8 | dist/ 9 | build/ 10 | *.tsbuildinfo 11 | 12 | # IDE and editor files 13 | .idea/ 14 | .vscode/ 15 | *.swp 16 | *.swo 17 | .DS_Store 18 | 19 | # Test coverage 20 | coverage/ 21 | 22 | # Environment variables 23 | .env 24 | .env.local 25 | .env.*.local 26 | 27 | # Logs 28 | logs/ 29 | *.log 30 | 31 | # Temporary files 32 | *.tmp 33 | *.temp 34 | 35 | # Logs 36 | logs 37 | *.log 38 | lerna-debug.log* 39 | .pnpm-debug.log* 40 | 41 | # Diagnostic reports (https://nodejs.org/api/report.html) 42 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 43 | 44 | # Runtime data 45 | pids 46 | *.pid 47 | *.seed 48 | *.pid.lock 49 | 50 | # Directory for instrumented libs generated by jscoverage/JSCover 51 | lib-cov 52 | 53 | # Coverage directory used by tools like istanbul 54 | coverage 55 | *.lcov 56 | 57 | # nyc test coverage 58 | .nyc_output 59 | 60 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 61 | .grunt 62 | 63 | # Bower dependency directory (https://bower.io/) 64 | bower_components 65 | 66 | # node-waf configuration 67 | .lock-wscript 68 | 69 | # Compiled binary addons (https://nodejs.org/api/addons.html) 70 | build/Release 71 | 72 | # Dependency directories 73 | jspm_packages/ 74 | 75 | # Snowpack dependency directory (https://snowpack.dev/) 76 | web_modules/ 77 | 78 | # Optional npm cache directory 79 | .npm 80 | 81 | # Optional eslint cache 82 | .eslintcache 83 | 84 | # Optional stylelint cache 85 | .stylelintcache 86 | 87 | # Microbundle cache 88 | .rpt2_cache/ 89 | .rts2_cache_cjs/ 90 | .rts2_cache_es/ 91 | .rts2_cache_umd/ 92 | 93 | # Optional REPL history 94 | .node_repl_history 95 | 96 | # Output of 'npm pack' 97 | *.tgz 98 | 99 | # Yarn Integrity file 100 | .yarn-integrity 101 | 102 | # dotenv environment variable files 103 | .env.development.local 104 | .env.test.local 105 | .env.production.local 106 | 107 | # parcel-bundler cache (https://parceljs.org/) 108 | .cache 109 | .parcel-cache 110 | 111 | # Next.js build output 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | .nuxt 117 | dist 118 | 119 | # Gatsby files 120 | .cache/ 121 | # Comment in the public line in if your project uses Gatsby and not Next.js 122 | # https://nextjs.org/blog/next-9-1#public-directory-support 123 | # public 124 | 125 | # vuepress build output 126 | .vuepress/dist 127 | 128 | # vuepress v2.x temp and cache directory 129 | .temp 130 | .cache 131 | 132 | # Docusaurus cache and generated files 133 | .docusaurus 134 | 135 | # Serverless directories 136 | .serverless/ 137 | 138 | # FuseBox cache 139 | .fusebox/ 140 | 141 | # DynamoDB Local files 142 | .dynamodb/ 143 | 144 | # TernJS port file 145 | .tern-port 146 | 147 | # Stores VSCode versions used for testing VSCode extensions 148 | .vscode-test 149 | 150 | # yarn v2 151 | .yarn/cache 152 | .yarn/unplugged 153 | .yarn/build-state.yml 154 | .yarn/install-state.gz 155 | .pnp.* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": true, 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "useTabs": false 8 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 CC11001100 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XHR Monitor Debugger Hook 2 | 3 | 一个用于监控和调试 XMLHttpRequest 请求的 TypeScript 库。 4 | 5 | ## 特性 6 | 7 | - 监控所有 XMLHttpRequest 请求 8 | - 支持请求 URL 过滤 9 | - 支持请求参数过滤 10 | - 支持请求头过滤 11 | - 提供完整的请求上下文信息 12 | - 支持动态配置更新 13 | - 使用 TypeScript 编写,提供完整的类型定义 14 | 15 | ## 安装 16 | 17 | ```bash 18 | npm install xhr-monitor-debugger-hook 19 | ``` 20 | 21 | ## 使用方法 22 | 23 | ### 基本用法 24 | 25 | ```typescript 26 | import { defaultDebugger } from 'xhr-monitor-debugger-hook'; 27 | 28 | // 启动调试器 29 | defaultDebugger.start(); 30 | 31 | // 发送请求 32 | const xhr = new XMLHttpRequest(); 33 | xhr.open('GET', 'https://api.example.com/data'); 34 | xhr.send(); 35 | 36 | // 停止调试器 37 | defaultDebugger.stop(); 38 | ``` 39 | 40 | ### 自定义配置 41 | 42 | ```typescript 43 | import { XhrDebugger, IXhrDebuggerConfig } from 'xhr-monitor-debugger-hook'; 44 | 45 | const config: IXhrDebuggerConfig = { 46 | enable: true, 47 | enableRequestUrlFilter: true, 48 | requestUrlFilterCondition: 'api.example.com', 49 | enableRequestParamFilter: false, 50 | requestParamFilterCondition: '', 51 | enableRequestHeaderFilter: false, 52 | requestHeaderFilterCondition: '' 53 | }; 54 | 55 | const debugger = new XhrDebugger(config); 56 | debugger.start(); 57 | ``` 58 | 59 | ### 更新配置 60 | 61 | ```typescript 62 | debugger.updateConfig({ 63 | enableRequestUrlFilter: true, 64 | requestUrlFilterCondition: 'api.example.com' 65 | }); 66 | ``` 67 | 68 | ### 获取请求上下文 69 | 70 | ```typescript 71 | const context = debugger.getContext(); 72 | console.log(context.toString()); 73 | ``` 74 | 75 | ## API 文档 76 | 77 | ### XhrDebugger 78 | 79 | 主要的调试器类。 80 | 81 | #### 方法 82 | 83 | - `start()`: 启动调试器 84 | - `stop()`: 停止调试器 85 | - `updateConfig(config: Partial)`: 更新配置 86 | - `getConfig()`: 获取当前配置 87 | - `getContext()`: 获取当前上下文 88 | 89 | ### IXhrDebuggerConfig 90 | 91 | 调试器配置接口。 92 | 93 | #### 属性 94 | 95 | - `enable`: 是否启用调试器 96 | - `enableRequestUrlFilter`: 是否启用请求 URL 过滤 97 | - `requestUrlFilterCondition`: 请求 URL 过滤条件 98 | - `enableRequestParamFilter`: 是否启用请求参数过滤 99 | - `requestParamFilterCondition`: 请求参数过滤条件 100 | - `enableRequestHeaderFilter`: 是否启用请求头过滤 101 | - `requestHeaderFilterCondition`: 请求头过滤条件 102 | 103 | ## 示例 104 | 105 | 查看 [example.ts](src/example.ts) 获取更多使用示例。 106 | 107 | ## 许可证 108 | 109 | MIT -------------------------------------------------------------------------------- /banner.txt: -------------------------------------------------------------------------------- 1 | 2 | ▗▄▄▄▖▗▖ ▗▖▗▄▄▖ ▗▄▄▄▖ ▗▄▄▖ ▗▄▄▖▗▄▄▖ ▗▄▄▄▖▗▄▄▖▗▄▄▄▖ 3 | █ ▝▚▞▘ ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▐▌ ▐▌ █ ▐▌ ▐▌ █ 4 | █ ▐▌ ▐▛▀▘ ▐▛▀▀▘ ▝▀▚▖▐▌ ▐▛▀▚▖ █ ▐▛▀▘ █ 5 | █ ▐▌ ▐▌ ▐▙▄▄▖▗▄▄▞▘▝▚▄▄▖▐▌ ▐▌▗▄█▄▖▐▌ █ 6 | 7 | 8 | 9 | ▗▖ ▗▖ ▗▄▄▖▗▄▄▄▖▗▄▄▖ ▗▄▄▖ ▗▄▄▖▗▄▄▖ ▗▄▄▄▖▗▄▄▖▗▄▄▄▖ 10 | ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▐▌▐▌ ▐▌ ▐▌ ▐▌ █ ▐▌ ▐▌ █ 11 | ▐▌ ▐▌ ▝▀▚▖▐▛▀▀▘▐▛▀▚▖ ▝▀▚▖▐▌ ▐▛▀▚▖ █ ▐▛▀▘ █ 12 | ▝▚▄▞▘▗▄▄▞▘▐▙▄▄▖▐▌ ▐▌▗▄▄▞▘▝▚▄▄▖▐▌ ▐▌▗▄█▄▖▐▌ █ 13 | 14 | 15 | 16 | ▗▄▄▄▖▗▄▄▄▖▗▖ ▗▖▗▄▄▖ ▗▖ ▗▄▖▗▄▄▄▖▗▄▄▄▖ 17 | █ ▐▌ ▐▛▚▞▜▌▐▌ ▐▌▐▌ ▐▌ ▐▌ █ ▐▌ 18 | █ ▐▛▀▀▘▐▌ ▐▌▐▛▀▘ ▐▌ ▐▛▀▜▌ █ ▐▛▀▀▘ 19 | █ ▐▙▄▄▖▐▌ ▐▌▐▌ ▐▙▄▄▖▐▌ ▐▌ █ ▐▙▄▄▖ -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | - 我们需要在网页中绘制一个操作界面。 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/translate.md: -------------------------------------------------------------------------------- 1 | # JavaScript to TypeScript 翻译进度跟踪 2 | 3 | ## 待翻译文件列表 4 | 5 | ### src 目录 (优先级最高) 6 | - [x] src/index.js -> src/index.ts 7 | - [x] src/ai/**/*.js -> src/ai/**/*.ts 8 | - [x] src/parser/**/*.js -> src/parser/**/*.ts 9 | - [x] src/context/**/*.js -> src/context/**/*.ts 10 | - [x] src/init/**/*.js -> src/init/**/*.ts 11 | - [x] src/config/**/*.js -> src/config/**/*.ts 12 | - [x] src/debuggers/**/*.js -> src/debuggers/**/*.ts 13 | - [x] src/message-formatter/**/*.js -> src/message-formatter/**/*.ts 14 | - [x] src/utils/**/*.js -> src/utils/**/*.ts 15 | - [x] src/analyzer/**/*.js -> src/analyzer/**/*.ts 16 | - [x] src/hook/**/*.js -> src/hook/**/*.ts 17 | - [x] src/codec/**/*.js -> src/codec/**/*.ts 18 | 19 | ### 根目录 20 | - [x] main.js -> main.ts 21 | - [x] userscript-headers.js -> userscript-headers.ts 22 | - [x] userscript-headers-dev.js -> userscript-headers-dev.ts 23 | - [x] webpack.common.js -> webpack.common.ts 24 | - [x] webpack.dev.js -> webpack.dev.ts 25 | - [x] webpack.prod.js -> webpack.prod.ts 26 | 27 | ## 翻译规则 28 | 1. 保持原有的目录结构 29 | 2. 保持原有的代码逻辑 30 | 3. 保持原有的注释 31 | 4. 确保TypeScript类型正确 32 | 5. 确保通过TypeScript编译器检查 33 | 6. 不要遗漏任何代码行 34 | 7. 不要自由发挥或添加新功能 35 | 36 | ## 已完成翻译的文件 37 | 38 | - src/index.ts 39 | - src/ai/deepseek.ts 40 | - src/analyzer/core-encoding/base64-analyzer/base64-analyzer.ts 41 | - src/analyzer/core-encoding/hex-encode-analyzer/hex-encode-analyzer.ts 42 | - src/analyzer/core-encoding/url-encode-analyzer/url-encode-analyzer.ts 43 | - src/analyzer/encrypt/rsa/rsa-analyzer.ts 44 | - src/analyzer/encrypt/rsa/rsa-context.ts 45 | - src/analyzer/encrypt/sign/sign-analyzer.ts 46 | - src/analyzer/encrypt/sign/sign-context.ts 47 | - src/analyzer/request-analyzer.ts 48 | - src/analyzer/response-analyzer.ts 49 | - src/analyzer/stack/stack-analyzer.ts 50 | - src/codec/commons/aes-codec/aes-codec.ts 51 | - src/codec/commons/base64-codec/base64-codec.ts 52 | - src/codec/commons/des-codec/des-codec.ts 53 | - src/codec/commons/gzip-codec/gzip-codec.ts 54 | - src/codec/commons/hex-codec/hex-codec.ts 55 | - src/codec/commons/protobuf-codec/protobuf-codec.ts 56 | - src/codec/commons/url-encode-codec/url-encode-codec.ts 57 | - src/config/ui/menu.ts 58 | - src/context/auth-context.ts 59 | - src/context/body-context.ts 60 | - src/context/content-type.ts 61 | - src/context/context-location.ts 62 | - src/context/event-context.ts 63 | - src/context/header.ts 64 | - src/context/header-context.ts 65 | - src/context/param.ts 66 | - src/context/param-context.ts 67 | - src/context/param-type.ts 68 | - src/context/request-context.ts 69 | - src/context/response-context.ts 70 | - src/context/url-context.ts 71 | - src/context/xhr-context.ts 72 | - src/debuggers/debugger-tester.ts 73 | - src/hook/xhr/request/attribute/add-on-abort-hook.ts 74 | - src/hook/xhr/request/method/add-abort-hook.ts 75 | - src/hook/xhr/request/method/add-add-event-listener-hook.ts 76 | - src/hook/xhr/request/method/add-override-mimetype-hook.ts 77 | - src/hook/xhr/request/method/add-send-hook.ts 78 | - src/hook/xhr/request/method/add-set-request-header-hook.ts 79 | - src/hook/xhr/response/attribute/add-onreadystatechange-hook.ts 80 | - src/hook/xhr/response/attribute/add-visit-response-header-hook.ts 81 | - src/hook/xhr/response/attribute/add-visit-response-property-hook.ts 82 | - src/hook/xhr/xml-http-request-object-hook.ts 83 | - src/parser/xhr-context-parser.ts 84 | - src/parser/request-context-parser.ts 85 | - src/parser/form-param-parser.ts 86 | - src/parser/form-body-parser.ts 87 | - src/parser/response-context-parser.ts 88 | - src/parser/response-body-parser.ts 89 | - src/parser/json-body-parser.ts 90 | - src/parser/text-body-parser.ts 91 | - src/parser/url-context-parser.ts 92 | - src/parser/header-parser.ts 93 | - src/parser/url-search-params-body-parser.ts 94 | - src/parser/blob-body-parser.ts 95 | - src/parser/form-data-body-parser.ts 96 | - src/parser/array-buffer-body-parser.ts 97 | 98 | ## 翻译过程中的改进 99 | 100 | 1. 添加了类型注解,提高了代码的类型安全性 101 | 2. 使用了现代ES6+语法,如`import/export` 102 | 3. 改进了代码结构和命名规范 103 | 4. 保留并优化了原有的注释 104 | 5. 添加了更详细的文档说明 105 | 6. 使用了TypeScript特有的功能,如枚举和接口 106 | 7. 优化了错误处理和类型检查 107 | 8. 移除了TODO注释并实现了相应功能 108 | 9. 添加了专门的类型定义文件 109 | 10. 使用了更具体的类型而不是`any` 110 | 11. 改进了事件处理和回调函数的类型安全性 111 | 12. 添加了`DebuggerTester`来替代`debugger`语句 112 | 113 | ## 翻译过程中遇到的问题 114 | 115 | 所有文件都已经成功翻译为TypeScript,并保持了原有的功能和语义。主要改进包括: 116 | 117 | 1. 添加了类型注解,提高了代码的类型安全性 118 | 2. 使用了现代ES6+语法,如`import/export` 119 | 3. 改进了代码结构和命名规范 120 | 4. 保留并优化了原有的注释 121 | 5. 添加了更详细的文档说明 122 | 6. 使用了TypeScript特有的功能,如枚举和接口 123 | 7. 优化了错误处理和类型检查 124 | 8. 移除了TODO注释并实现了相应功能 -------------------------------------------------------------------------------- /fuck-hot-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 自动安装依赖的热更新脚本 4 | # 使用方式:chmod +x ./fuck-hot-compile.sh && ./fuck-hot-compile.sh 5 | 6 | # 检测包管理器并安装依赖 7 | init_project() { 8 | if command -v yarn &> /dev/null; then 9 | echo "使用 yarn 安装依赖..." 10 | yarn install --frozen-lockfile 11 | elif command -v npm &> /dev/null; then 12 | echo "使用 npm 安装依赖..." 13 | npm ci 14 | else 15 | echo "错误:未检测到 yarn 或 npm,请先安装 Node.js" 16 | exit 1 17 | fi 18 | } 19 | 20 | # 获取构建命令 21 | detect_build_command() { 22 | if [ -f yarn.lock ]; then 23 | echo "yarn build" 24 | else 25 | echo "npm run build" 26 | fi 27 | } 28 | 29 | # ---------- 主流程 ---------- 30 | init_project 31 | build_command=$(detect_build_command) 32 | 33 | echo "启动热更新监听..." 34 | while true; do 35 | echo "[$(date +'%T')] 开始构建..." 36 | if $build_command; then 37 | echo "[$(date +'%T')] 构建成功 ✅" 38 | else 39 | echo "[$(date +'%T')] 构建失败 ❌,10秒后重试..." 40 | sleep 10 41 | fi 42 | sleep 1 43 | done -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | roots: ['/src'], 5 | testMatch: ['**/__tests__/**/*.test.ts'], 6 | collectCoverage: true, 7 | coverageDirectory: 'coverage', 8 | coverageReporters: ['text', 'lcov'], 9 | moduleFileExtensions: ['ts', 'js', 'json', 'node'] 10 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-xhr-hook", 3 | "version": "0.4", 4 | "description": "this is userscript's description", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "repository": "https://github.com/JSREI/js-xhr-hook", 8 | "namespace": "https://github.com/JSREI/js-xhr-hook", 9 | "document": "https://github.com/JSREI/js-xhr-hook", 10 | "scripts": { 11 | "build": "NODE_ENV=production webpack --config webpack.prod.js", 12 | "watch": "webpack --watch --config webpack.dev.js", 13 | "type-check": "tsc --noEmit", 14 | "type-check:watch": "tsc --noEmit --watch" 15 | }, 16 | "author": "CC11001100 ", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@types/node": "^20.11.16", 20 | "@types/pako": "^2.0.3", 21 | "@typescript-eslint/eslint-plugin": "^8.32.1", 22 | "@typescript-eslint/parser": "^8.32.1", 23 | "current-script-polyfill": "^1.0.0", 24 | "eslint": "^8.57.1", 25 | "ts-loader": "^9.5.1", 26 | "typescript": "^5.3.3", 27 | "webpack": "^5.88.2", 28 | "webpack-cli": "^5.1.4", 29 | "webpack-merge": "^5.9.0" 30 | }, 31 | "dependencies": { 32 | "@types/uuid": "^10.0.0", 33 | "pako": "^2.1.0", 34 | "uuid": "^11.1.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ai/deepseek.ts: -------------------------------------------------------------------------------- 1 | // 配置你的 API 信息 2 | const API_KEY = 'your_api_key_here'; // 替换为你的实际 API 密钥 3 | const API_URL = 'https://api.deepseek.com/v1/chat/completions'; // 确认实际 API 端点 4 | 5 | interface DeepSeekMessage { 6 | role: string; 7 | content: string; 8 | } 9 | 10 | interface DeepSeekRequestData { 11 | model: string; 12 | messages: DeepSeekMessage[]; 13 | max_tokens: number; 14 | } 15 | 16 | interface DeepSeekResponse { 17 | choices: Array<{ 18 | message: { 19 | content: string; 20 | }; 21 | }>; 22 | } 23 | 24 | interface GMXMLHttpRequestDetails { 25 | method: string; 26 | url: string; 27 | headers: Record; 28 | data: string; 29 | onload: (response: { status: number; responseText: string }) => void; 30 | onerror: (error: Error) => void; 31 | } 32 | 33 | declare function GM_xmlhttpRequest(details: GMXMLHttpRequestDetails): void; 34 | 35 | function callDeepSeekAPI(prompt: string): Promise { 36 | return new Promise((resolve, reject) => { 37 | const requestData: DeepSeekRequestData = { 38 | model: 'deepseek-chat', // 根据实际模型调整 39 | messages: [{ 40 | role: 'user', 41 | content: prompt 42 | }], 43 | max_tokens: 500 44 | }; 45 | 46 | GM_xmlhttpRequest({ 47 | method: 'POST', 48 | url: API_URL, 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | 'Authorization': `Bearer ${API_KEY}` 52 | }, 53 | data: JSON.stringify(requestData), 54 | onload: function (response) { 55 | if (response.status >= 200 && response.status < 300) { 56 | try { 57 | const data: DeepSeekResponse = JSON.parse(response.responseText); 58 | resolve(data.choices[0].message.content); 59 | } catch (_e) { 60 | reject('解析响应失败'); 61 | } 62 | } else { 63 | reject(`API 请求失败,状态码:${response.status}`); 64 | } 65 | }, 66 | onerror: function (error: Error) { 67 | reject(`请求错误:${error.message}`); 68 | } 69 | }); 70 | }); 71 | } 72 | 73 | export { callDeepSeekAPI }; -------------------------------------------------------------------------------- /src/analyzer/core-encoding/base64-analyzer/base64-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../../../context/request-context"; 2 | import { ResponseContext } from "../../../context/response-context"; 3 | import { Param } from "../../../context/param"; 4 | import { Base64Codec } from "../../../codec/commons/base64-codec/base64-codec"; 5 | 6 | /** 7 | * Base64编码分析器 8 | */ 9 | export class Base64Analyzer { 10 | /** 11 | * 分析请求上下文中的Base64编码 12 | * @param requestContext {RequestContext} 请求上下文 13 | */ 14 | static analyzeRequestContext(requestContext: RequestContext): void { 15 | if (!requestContext.bodyContext) { 16 | return; 17 | } 18 | 19 | const rawBody = requestContext.bodyContext.getRawBodyPlain(); 20 | if (!rawBody || !Base64Codec.isBase64(rawBody)) { 21 | return; 22 | } 23 | 24 | requestContext.bodyContext.isRawBodyBase64 = true; 25 | requestContext.bodyContext.rawBodyBase64Decode = Base64Codec.decode(rawBody); 26 | } 27 | 28 | /** 29 | * 分析响应上下文中的Base64编码 30 | * @param responseContext {ResponseContext} 响应上下文 31 | */ 32 | static analyzeResponseContext(responseContext: ResponseContext): void { 33 | if (!responseContext.bodyContext) { 34 | return; 35 | } 36 | 37 | const rawBody = responseContext.bodyContext.getRawBodyPlain(); 38 | if (!rawBody || !Base64Codec.isBase64(rawBody)) { 39 | return; 40 | } 41 | 42 | responseContext.bodyContext.isRawBodyBase64 = true; 43 | responseContext.bodyContext.rawBodyBase64Decode = Base64Codec.decode(rawBody); 44 | } 45 | 46 | /** 47 | * 分析参数中的Base64编码 48 | * @param param {Param} 参数 49 | */ 50 | static analyzeParam(param: Param): void { 51 | if (!param.value) { 52 | return; 53 | } 54 | 55 | const value = param.getValuePlain(); 56 | if (!value || !Base64Codec.isBase64(value)) { 57 | return; 58 | } 59 | 60 | param.valueBase64Decode = Base64Codec.decode(value); 61 | param.isValueBase64 = true; 62 | } 63 | } -------------------------------------------------------------------------------- /src/analyzer/core-encoding/hex-encode-analyzer/hex-encode-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from "../../../context/request-context"; 2 | import { ResponseContext } from "../../../context/response-context"; 3 | import { Param } from "../../../context/param"; 4 | import { HexCodec } from "../../../codec/commons/hex-codec/hex-codec"; 5 | 6 | /** 7 | * 十六进制编码分析器 8 | */ 9 | export class HexEncodeAnalyzer { 10 | 11 | /** 12 | * 分析请求上下文中的十六进制编码 13 | * @param requestContext {RequestContext} 请求上下文 14 | */ 15 | static analyzeRequestContext(requestContext: RequestContext): void { 16 | // 请求参数 17 | for (const param of requestContext.getParams()) { 18 | this.analyzeParam(param); 19 | } 20 | 21 | // 请求体 22 | const rawBody = requestContext.bodyContext.getRawBodyPlain(); 23 | if (!rawBody || !HexCodec.isHex(rawBody)) { 24 | return; 25 | } 26 | const decodedBody = HexCodec.decode(rawBody); 27 | if (decodedBody instanceof DataView) { 28 | requestContext.bodyContext.rawBodyHexDecode = new TextDecoder().decode(decodedBody); 29 | } else { 30 | requestContext.bodyContext.rawBodyHexDecode = decodedBody; 31 | } 32 | requestContext.bodyContext.isRawBodyHex = true; 33 | } 34 | 35 | /** 36 | * 分析响应上下文中的十六进制编码 37 | * @param responseContext {ResponseContext} 响应上下文 38 | */ 39 | static analyzeResponseContext(responseContext: ResponseContext): void { 40 | // 响应体 41 | const rawBody = responseContext.bodyContext.getRawBodyPlain(); 42 | if (!rawBody || !HexCodec.isHex(rawBody)) { 43 | return; 44 | } 45 | const decodedBody = HexCodec.decode(rawBody); 46 | if (decodedBody instanceof DataView) { 47 | responseContext.bodyContext.rawBodyHexDecode = new TextDecoder().decode(decodedBody); 48 | } else { 49 | responseContext.bodyContext.rawBodyHexDecode = decodedBody; 50 | } 51 | responseContext.bodyContext.isRawBodyHex = true; 52 | } 53 | 54 | /** 55 | * 分析param并设置相关字段 56 | * @param param {Param} 参数对象 57 | */ 58 | static analyzeParam(param: Param): void { 59 | const value = param.getValuePlain(); 60 | if (!value || !HexCodec.isHex(value)) { 61 | return; 62 | } 63 | const decodedValue = HexCodec.decode(value); 64 | if (decodedValue instanceof DataView) { 65 | param.valueHexDecode = new TextDecoder().decode(decodedValue); 66 | } else { 67 | param.valueHexDecode = decodedValue; 68 | } 69 | param.isValueHex = true; 70 | } 71 | } -------------------------------------------------------------------------------- /src/analyzer/core-encoding/url-encode-analyzer/url-encode-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { UrlEncodeCodec } from "../../../codec/commons/url-encode-codec/url-encode-codec"; 2 | import { RequestContext } from "../../../context/request-context"; 3 | import { ResponseContext } from "../../../context/response-context"; 4 | import { Param } from "../../../context/param"; 5 | 6 | /** 7 | * URL编码分析器 8 | */ 9 | export class UrlEncodeAnalyzer { 10 | 11 | /** 12 | * 分析请求上下文中的URL编码 13 | * @param requestContext {RequestContext} 请求上下文 14 | */ 15 | static analyzeRequestContext(requestContext: RequestContext): void { 16 | if (!requestContext.bodyContext) { 17 | return; 18 | } 19 | 20 | const rawBody = requestContext.bodyContext.getRawBodyPlain(); 21 | if (!rawBody || !UrlEncodeCodec.isEncode(rawBody)) { 22 | return; 23 | } 24 | 25 | requestContext.bodyContext.isRawBodyUrlEncode = true; 26 | requestContext.bodyContext.rawBodyUrlDecode = UrlEncodeCodec.decode(rawBody); 27 | } 28 | 29 | /** 30 | * 分析响应上下文中的URL编码 31 | * @param responseContext {ResponseContext} 响应上下文 32 | */ 33 | static analyzeResponseContext(responseContext: ResponseContext): void { 34 | if (!responseContext.bodyContext) { 35 | return; 36 | } 37 | 38 | const rawBody = responseContext.bodyContext.getRawBodyPlain(); 39 | if (!rawBody || !UrlEncodeCodec.isEncode(rawBody)) { 40 | return; 41 | } 42 | 43 | responseContext.bodyContext.isRawBodyUrlEncode = true; 44 | responseContext.bodyContext.rawBodyUrlDecode = UrlEncodeCodec.decode(rawBody); 45 | } 46 | 47 | /** 48 | * 分析参数中的URL编码 49 | * @param param {Param} 参数 50 | */ 51 | static analyzeParam(param: Param): void { 52 | if (!param.value || !UrlEncodeCodec.isEncode(param.value)) { 53 | return; 54 | } 55 | 56 | param.isUrlEncoded = true; 57 | param.urlDecodedValue = UrlEncodeCodec.decode(param.value); 58 | } 59 | } -------------------------------------------------------------------------------- /src/analyzer/encrypt/rsa/rsa-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../../../context/xhr-context"; 2 | import { RsaContext } from "./rsa-context"; 3 | 4 | /** 5 | * RSA加密分析器 6 | */ 7 | export class RsaAnalyzer { 8 | 9 | /** 10 | * 分析请求中的RSA加密 11 | * @param xhrContext {XhrContext} XHR上下文 12 | * @returns {RsaContext | null} RSA上下文,如果没有找到RSA加密则返回 null 13 | */ 14 | analyze(_xhrContext: XhrContext): RsaContext | null { 15 | return null; 16 | } 17 | } -------------------------------------------------------------------------------- /src/analyzer/encrypt/rsa/rsa-context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * RSA上下文 3 | */ 4 | class RsaContext { 5 | 6 | modulus: string | null; 7 | modulusJsonPath: string | null; 8 | exponent: string | null; 9 | exponentJsonPath: string | null; 10 | 11 | constructor() { 12 | this.modulus = null; 13 | this.modulusJsonPath = null; 14 | this.exponent = null; 15 | this.exponentJsonPath = null; 16 | } 17 | 18 | } 19 | 20 | export { 21 | RsaContext 22 | }; -------------------------------------------------------------------------------- /src/analyzer/encrypt/sign/sign-analyzer.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../../../context/xhr-context"; 2 | import { SignContext } from "./sign-context"; 3 | 4 | /** 5 | * 签名分析器 6 | */ 7 | export class SignAnalyzer { 8 | 9 | /** 10 | * 分析请求中的签名 11 | * @param xhrContext {XhrContext} XHR上下文 12 | * @returns {SignContext | null} 签名上下文,如果没有找到签名则返回 null 13 | */ 14 | analyze(xhrContext: XhrContext): SignContext | null { 15 | // 分析请求参数中的签名 16 | for (const param of xhrContext.requestContext.getParams()) { 17 | if (param.name && this.maybeSignName(param.name)) { 18 | const signContext = new SignContext(); 19 | signContext.name = param.name; 20 | signContext.value = param.value || ''; 21 | return signContext; 22 | } 23 | } 24 | 25 | // 分析请求头中的签名 26 | for (const header of xhrContext.requestContext.headerContext.getAll()) { 27 | if (header.name && this.maybeSignName(header.name)) { 28 | const signContext = new SignContext(); 29 | signContext.name = header.name; 30 | signContext.value = header.value || ''; 31 | return signContext; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | 38 | /** 39 | * 判断是否可能是签名名称 40 | * @param name {string} 名称 41 | * @returns {boolean} 是否可能是签名名称 42 | */ 43 | private maybeSignName(name: string): boolean { 44 | const lowerName = name.toLowerCase(); 45 | return lowerName.includes('sign') || 46 | lowerName.includes('signature') || 47 | lowerName.includes('token') || 48 | lowerName.includes('auth'); 49 | } 50 | } -------------------------------------------------------------------------------- /src/analyzer/encrypt/sign/sign-context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 签名上下文 3 | */ 4 | export class SignContext { 5 | /** 6 | * 签名参数名称 7 | */ 8 | name: string; 9 | 10 | /** 11 | * 签名参数值 12 | */ 13 | value: string; 14 | 15 | /** 16 | * 创建签名上下文 17 | * @param name {string} 签名参数名称 18 | * @param value {string} 签名参数值 19 | */ 20 | constructor(name: string = '', value: string = '') { 21 | this.name = name; 22 | this.value = value; 23 | } 24 | } -------------------------------------------------------------------------------- /src/analyzer/json-analyzer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON对象分析器 3 | */ 4 | export class JsonAnalyzer { 5 | 6 | /** 7 | * 深度遍历 JSON 对象 8 | * @param obj {Record} 要遍历的对象 9 | * @param callback {Function} 回调函数,接收 name、path 和 value 10 | * @param path {string} 当前路径(用于递归) 11 | */ 12 | deepTraverse( 13 | obj: Record, 14 | callback: (name: string | undefined, path: string, value: unknown) => void, 15 | path: string = '' 16 | ): void { 17 | if (typeof obj === 'object' && obj !== null) { 18 | for (const key in obj) { 19 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 20 | const newPath = path ? `${path}.${key}` : key; 21 | const value = obj[key]; 22 | if (typeof value === 'object' && value !== null) { 23 | this.deepTraverse(value as Record, callback, newPath); 24 | } else { 25 | callback(key, newPath, value); 26 | } 27 | } 28 | } 29 | } else { 30 | const name = path.split('.').pop(); 31 | callback(name, path, obj); 32 | } 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /src/analyzer/request-analyzer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 请求分析器 3 | */ 4 | class RequestAnalyzer { 5 | 6 | } 7 | 8 | export { 9 | RequestAnalyzer 10 | }; -------------------------------------------------------------------------------- /src/analyzer/response-analyzer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 响应分析器 3 | */ 4 | class ResponseAnalyzer { 5 | 6 | } 7 | 8 | export { 9 | ResponseAnalyzer 10 | }; -------------------------------------------------------------------------------- /src/analyzer/stack/stack-analyzer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 分析调用栈 3 | */ 4 | class StackAnalyzer { 5 | 6 | } 7 | 8 | export { 9 | StackAnalyzer 10 | }; -------------------------------------------------------------------------------- /src/codec/commons/aes-codec/aes-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AES编解码器 3 | */ 4 | class AesCodec { 5 | 6 | } 7 | 8 | export { 9 | AesCodec 10 | }; -------------------------------------------------------------------------------- /src/codec/commons/base64-codec/base64-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base64编解码器 3 | */ 4 | class Base64Codec { 5 | 6 | /** 7 | * 判断字符串是否是base64字符串 8 | * 9 | * @param str 10 | * @return {boolean} 11 | */ 12 | static isBase64(str: string): boolean { 13 | if (!str) { 14 | return false; 15 | } 16 | try { 17 | // 尝试解码 18 | const decoded = atob(str); // Base64解码 19 | // 重新编码,检查是否与原始字符串匹配 20 | const reencoded = btoa(decoded); 21 | // 去除可能的填充字符'='后比较 22 | return str.replace(/=+$/, '') === reencoded.replace(/=+$/, ''); 23 | } catch (_e) { 24 | // 如果解码失败,返回false 25 | return false; 26 | } 27 | } 28 | 29 | static encode(dataView: DataView): string { 30 | const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 31 | let result = ''; 32 | let i = 0; 33 | const byteLength = dataView.byteLength; 34 | 35 | while (i < byteLength) { 36 | // 每次读取 3 个字节 37 | const byte1 = dataView.getUint8(i++); 38 | const byte2 = i < byteLength ? dataView.getUint8(i++) : 0; 39 | const byte3 = i < byteLength ? dataView.getUint8(i++) : 0; 40 | 41 | // 将 3 个字节转换为 4 个 Base64 字符 42 | const char1 = base64Chars.charAt(byte1 >> 2); 43 | const char2 = base64Chars.charAt(((byte1 & 0x03) << 4) | (byte2 >> 4)); 44 | const char3 = base64Chars.charAt(((byte2 & 0x0f) << 2) | (byte3 >> 6)); 45 | const char4 = base64Chars.charAt(byte3 & 0x3f); 46 | 47 | result += char1 + char2 + char3 + char4; 48 | } 49 | 50 | // 处理填充 51 | const paddingLength = byteLength % 3; 52 | if (paddingLength === 1) { 53 | result = result.slice(0, -2) + '=='; 54 | } else if (paddingLength === 2) { 55 | result = result.slice(0, -1) + '='; 56 | } 57 | 58 | return result; 59 | } 60 | 61 | static decode(input: string): string { 62 | const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 63 | const base64Lookup = new Uint8Array(256); 64 | for (let i = 0; i < base64Chars.length; i++) { 65 | base64Lookup[base64Chars.charCodeAt(i)] = i; 66 | } 67 | 68 | input = input.replace(/=+$/, ''); 69 | let result = ''; 70 | let i = 0; 71 | 72 | while (i < input.length) { 73 | const char1 = input.charAt(i++); 74 | const char2 = input.charAt(i++); 75 | const char3 = input.charAt(i++); 76 | const char4 = input.charAt(i++); 77 | 78 | const byte1 = base64Lookup[char1.charCodeAt(0)]; 79 | const byte2 = base64Lookup[char2.charCodeAt(0)]; 80 | const byte3 = base64Lookup[char3.charCodeAt(0)]; 81 | const byte4 = base64Lookup[char4.charCodeAt(0)]; 82 | 83 | const code1 = (byte1 << 2) | (byte2 >> 4); 84 | const code2 = ((byte2 & 0x0f) << 4) | (byte3 >> 2); 85 | const code3 = ((byte3 & 0x03) << 6) | byte4; 86 | 87 | result += String.fromCharCode(code1); 88 | if (char3 !== '=') result += String.fromCharCode(code2); 89 | if (char4 !== '=') result += String.fromCharCode(code3); 90 | } 91 | 92 | return result; 93 | } 94 | 95 | } 96 | 97 | export { 98 | Base64Codec 99 | }; -------------------------------------------------------------------------------- /src/codec/commons/des-codec/des-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * DES编解码器 3 | */ 4 | class DesCodec { 5 | 6 | } 7 | 8 | export { 9 | DesCodec 10 | }; -------------------------------------------------------------------------------- /src/codec/commons/gzip-codec/gzip-codec.ts: -------------------------------------------------------------------------------- 1 | import pako from 'pako'; 2 | 3 | /** 4 | * Gzip 编解码器 5 | */ 6 | export class GzipCodec { 7 | 8 | /** 9 | * 检查数据是否是 gzip 压缩的 10 | * @param data {Uint8Array} 要检查的数据 11 | * @returns {boolean} 是否是 gzip 压缩的数据 12 | */ 13 | static isGzipCompressed(data: Uint8Array): boolean { 14 | // Gzip 魔数是 1f 8b 15 | return data.length > 2 && data[0] === 0x1f && data[1] === 0x8b; 16 | } 17 | 18 | /** 19 | * 解码 gzip 压缩的数据 20 | * @param data {Uint8Array} 要解码的数据 21 | * @returns {Uint8Array} 解码后的数据 22 | */ 23 | static decode(data: Uint8Array): Uint8Array { 24 | try { 25 | return pako.inflate(data); 26 | } catch (e) { 27 | console.error('Error decompressing gzip data:', e); 28 | return data; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/codec/commons/hex-codec/hex-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * HexCodec 类提供了编码和解码十六进制字符串的工具。 3 | */ 4 | class HexCodec { 5 | 6 | /** 7 | * 将十六进制字符串解码为 DataView 对象 8 | * @param hexString {string} 十六进制字符串 9 | * @return {DataView} 返回 DataView 对象 10 | */ 11 | static decode(hexString: string): DataView { 12 | // 移除可能的空格或其他分隔符 13 | hexString = hexString.replace(/\s/g, ''); 14 | 15 | // 检查十六进制字符串的长度是否为偶数 16 | if (hexString.length % 2 !== 0) { 17 | throw new Error('Invalid hex string: length must be even.'); 18 | } 19 | 20 | // 将十六进制字符串转换为字节数组 21 | const byteLength = hexString.length / 2; 22 | const buffer = new ArrayBuffer(byteLength); 23 | const dataView = new DataView(buffer); 24 | 25 | for (let i = 0; i < byteLength; i++) { 26 | const byteHex = hexString.substr(i * 2, 2); 27 | const byteValue = parseInt(byteHex, 16); 28 | if (isNaN(byteValue)) { 29 | throw new Error(`Invalid hex string: invalid byte at position ${i * 2}.`); 30 | } 31 | dataView.setUint8(i, byteValue); 32 | } 33 | 34 | return dataView; 35 | } 36 | 37 | /** 38 | * 检查字符串是否是有效的十六进制字符串。 39 | * @param {string} string - 要检查的字符串。 40 | * @return {boolean} - 如果字符串是有效的十六进制字符串,则返回 true,否则返回 false。 41 | */ 42 | static isHex(string: string | null): boolean { 43 | if (!string) { 44 | return false; 45 | } 46 | 47 | // 移除所有空格 48 | string = string.replace(/\s/g, ''); 49 | 50 | // 检查长度是否为偶数(如果表示字节) 51 | if (string.length % 2 !== 0) { 52 | return false; 53 | } 54 | 55 | // 检查所有字符是否都是有效的十六进制字符 56 | const hexRegex = /^[0-9a-fA-F]+$/; 57 | return hexRegex.test(string); 58 | } 59 | 60 | /** 61 | * 十六进制编码 62 | * @param value 要编码的字符串 63 | * @returns 编码后的字符串 64 | */ 65 | static encode(value: string | null): string | null { 66 | if (!value) { 67 | return null; 68 | } 69 | try { 70 | const bytes = new TextEncoder().encode(value); 71 | let result = ''; 72 | for (let i = 0; i < bytes.length; i++) { 73 | result += bytes[i].toString(16).padStart(2, '0'); 74 | } 75 | return result; 76 | } catch (_e) { 77 | return null; 78 | } 79 | } 80 | 81 | } 82 | 83 | export { 84 | HexCodec 85 | }; -------------------------------------------------------------------------------- /src/codec/commons/protobuf-codec/base-parser.ts: -------------------------------------------------------------------------------- 1 | import { MessageType, HandlerFunction, MessageField, HandlerInput, isFileReader, FileReader } from './types'; 2 | 3 | export abstract class Parser { 4 | types: Record; 5 | nativeTypes: Record; 6 | defaultIndent: string; 7 | compactMaxLineLength: number; 8 | compactMaxLength: number; 9 | bytesPerLine: number; 10 | errorsProduced: Error[]; 11 | defaultHandler: string; 12 | defaultHandlers: Record; 13 | 14 | constructor() { 15 | const emptyFields: Record = Object.create(null); 16 | const defaultMessageType: MessageType = { 17 | compact: false, 18 | fields: emptyFields 19 | }; 20 | 21 | this.types = { 22 | message: defaultMessageType 23 | }; 24 | 25 | const defaultHandler = ((input: HandlerInput, gtype?: string, endgroup?: [number | null] | null): string => { 26 | if (typeof input === 'string') { 27 | return this.dim(input); 28 | } 29 | if (input instanceof Uint8Array) { 30 | return this.parseMessage({ read: () => input }, 'message', null); 31 | } 32 | if (isFileReader(input)) { 33 | return this.parseMessage(input as FileReader, gtype || 'message', endgroup || null); 34 | } 35 | throw new Error('Invalid input type'); 36 | }) as HandlerFunction; 37 | 38 | this.nativeTypes = { 39 | message: [defaultHandler, 2] 40 | }; 41 | 42 | this.defaultIndent = " ".repeat(4); 43 | this.compactMaxLineLength = 35; 44 | this.compactMaxLength = 70; 45 | this.bytesPerLine = 24; 46 | this.errorsProduced = []; 47 | this.defaultHandler = "message"; 48 | this.defaultHandlers = { 49 | '0': "varint", 50 | '1': "64bit", 51 | '2': "chunk", 52 | '3': "startgroup", 53 | '4': "endgroup", 54 | '5': "32bit", 55 | }; 56 | } 57 | 58 | indent(text: string, indent: string = this.defaultIndent): string { 59 | const lines = text.split("\n").map(line => line.length ? indent + line : line); 60 | return lines.join("\n"); 61 | } 62 | 63 | toDisplayCompactly(type: string, lines: string[]): boolean { 64 | try { 65 | return this.types[type]?.compact ?? false; 66 | } catch { 67 | return false; 68 | } 69 | } 70 | 71 | abstract dim(text: string): string; 72 | abstract parseMessage(file: FileReader, gtype: string, endgroup: [number | null] | null): string; 73 | } -------------------------------------------------------------------------------- /src/codec/commons/protobuf-codec/core.ts: -------------------------------------------------------------------------------- 1 | import { FileReaderLike } from './types'; 2 | 3 | export class Core { 4 | static readVarint(file: FileReaderLike): number | null { 5 | let result = 0; 6 | let pos = 0; 7 | 8 | while (true) { 9 | const b = file.read(1); 10 | if (!b.length) { 11 | if (pos === 0) return null; 12 | throw new Error("Unexpected EOF"); 13 | } 14 | result |= (b[0] & 0x7F) << pos; 15 | pos += 7; 16 | if (!(b[0] & 0x80)) { 17 | if (b[0] === 0 && pos !== 7) throw new Error("Invalid varint"); 18 | return result; 19 | } 20 | } 21 | } 22 | 23 | static readIdentifier(file: FileReaderLike): [number | null, number | null] { 24 | const id = Core.readVarint(file); 25 | if (id === null) return [null, null]; 26 | return [id >> 3, id & 0x07]; 27 | } 28 | 29 | static readValue(file: FileReaderLike, wireType: number): Uint8Array | number | boolean | null { 30 | if (wireType === 0) { 31 | return Core.readVarint(file); 32 | } else if (wireType === 1) { 33 | const c = file.read(8); 34 | if (!c.length) return null; 35 | if (c.length !== 8) throw new Error("Invalid 64-bit value"); 36 | return c; 37 | } else if (wireType === 2) { 38 | const length = Core.readVarint(file); 39 | if (length === null) return null; 40 | const c = file.read(length); 41 | if (c.length !== length) throw new Error("Invalid chunk length"); 42 | return c; 43 | } else if (wireType === 3 || wireType === 4) { 44 | return wireType === 3; 45 | } else if (wireType === 5) { 46 | const c = file.read(4); 47 | if (!c.length) return null; 48 | if (c.length !== 4) throw new Error("Invalid 32-bit value"); 49 | return c; 50 | } else { 51 | throw new Error(`Unknown wire type ${wireType}`); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/codec/commons/protobuf-codec/index.ts: -------------------------------------------------------------------------------- 1 | import { StandardParser } from './standard-parser'; 2 | import { FileReaderLike } from './types'; 3 | 4 | export * from './types'; 5 | export * from './core'; 6 | export * from './base-parser'; 7 | export * from './standard-parser'; 8 | 9 | export class ProtoBufCodec { 10 | /** 11 | * 解码ProtoBuf数据 12 | * @param file 类文件对象,包含buffer和read方法 13 | * @returns 解码后的数据 14 | */ 15 | decode(file: FileReaderLike & { buffer: Uint8Array }): unknown { 16 | const parser = new StandardParser(); 17 | const result = parser.parseMessage(file, 'message', null); 18 | console.log(result); 19 | return result; 20 | } 21 | } -------------------------------------------------------------------------------- /src/codec/commons/protobuf-codec/standard-parser.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from './base-parser'; 2 | import { createMethodWrapper, BaseParserMethod } from './types'; 3 | 4 | export class StandardParser extends Parser { 5 | messageCompactMaxLines: number; 6 | packedCompactMaxLines: number; 7 | dumpPrefix: string; 8 | dumpIndex: number; 9 | wireTypesNotMatching: boolean; 10 | groupsObserved: boolean; 11 | 12 | constructor() { 13 | super(); 14 | 15 | this.types = { 16 | message: { 17 | compact: false, 18 | fields: {} 19 | } 20 | }; 21 | 22 | this.messageCompactMaxLines = 4; 23 | this.packedCompactMaxLines = 20; 24 | 25 | this.dumpPrefix = "dump."; 26 | this.dumpIndex = 0; 27 | 28 | this.wireTypesNotMatching = false; 29 | this.groupsObserved = false; 30 | 31 | const typesToRegister: Record = { 32 | '0': ["varint", "sint32", "sint64", "int32", "int64", "uint32", "uint64", "enum", "bool"], 33 | '1': ["64bit", "sfixed64", "fixed64", "double"], 34 | '2': ["chunk", "bytes", "string", "message", "packed", "dump"], 35 | '5': ["32bit", "sfixed32", "fixed32", "float"], 36 | }; 37 | 38 | Object.entries(typesToRegister).forEach(([wireTypeStr, types]) => { 39 | const wireType = Number(wireTypeStr); 40 | types.forEach((type: string) => { 41 | const methodName = `parse${type.charAt(0).toUpperCase() + type.slice(1)}` as keyof StandardParser; 42 | const method = this[methodName]; 43 | if (typeof method === 'function') { 44 | this.nativeTypes[type] = [createMethodWrapper(method as BaseParserMethod, this), wireType]; 45 | } 46 | }); 47 | }); 48 | } 49 | 50 | parseVarint(value: number): string { 51 | return value.toString(); 52 | } 53 | 54 | parseSint32(value: number): string { 55 | return ((value >> 1) ^ -(value & 1)).toString(); 56 | } 57 | 58 | parseSint64(value: number): string { 59 | return ((value >> 1) ^ -(value & 1)).toString(); 60 | } 61 | 62 | parseInt32(value: number): string { 63 | return value.toString(); 64 | } 65 | 66 | parseInt64(value: number): string { 67 | return value.toString(); 68 | } 69 | 70 | parseUint32(value: number): string { 71 | return value.toString(); 72 | } 73 | 74 | parseUint64(value: number): string { 75 | return value.toString(); 76 | } 77 | 78 | parseEnum(value: number): string { 79 | return value.toString(); 80 | } 81 | 82 | parseBool(value: number): string { 83 | return value ? 'true' : 'false'; 84 | } 85 | 86 | dim(text: string): string { 87 | return text; 88 | } 89 | 90 | parseMessage(file: { read: (bytes: number) => Uint8Array }, gtype: string, endgroup: [number | null] | null): string { 91 | // 实现消息解析逻辑 92 | return ''; 93 | } 94 | } -------------------------------------------------------------------------------- /src/codec/commons/protobuf-codec/types.ts: -------------------------------------------------------------------------------- 1 | // 基础类型定义 2 | export type FileReaderLike = { read: (bytes: number) => Uint8Array }; 3 | 4 | export interface FileReader { 5 | read: (bytes: number) => Uint8Array; 6 | length?: number; 7 | } 8 | 9 | export interface MessageField { 10 | type: string; 11 | name: string; 12 | } 13 | 14 | export interface MessageType { 15 | compact: boolean; 16 | fields: Record; 17 | } 18 | 19 | // 解析器方法类型定义 20 | export type HandlerInput = number | string | Uint8Array | FileReader | FileReaderLike; 21 | 22 | export interface BaseParserMethod { 23 | (value: number): string; 24 | } 25 | 26 | export interface HandlerFunction extends BaseParserMethod { 27 | (input: HandlerInput, gtype?: string, endgroup?: [number | null] | null): string; 28 | } 29 | 30 | // 类型检查辅助函数 31 | export function isFileReader(value: unknown): value is FileReader | FileReaderLike { 32 | return typeof value === 'object' && value !== null && 'read' in value && typeof (value as any).read === 'function'; 33 | } 34 | 35 | // 类型转换辅助函数 36 | export function createMethodWrapper(method: BaseParserMethod, context: any): HandlerFunction { 37 | const wrapper = function(input: HandlerInput, gtype?: string, endgroup?: [number | null] | null): string { 38 | if (typeof input === 'number') { 39 | return method.call(context, input); 40 | } 41 | throw new Error('Invalid input type'); 42 | }; 43 | return wrapper as HandlerFunction; 44 | } -------------------------------------------------------------------------------- /src/codec/commons/rsa-codec/rsa-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * RSA编解码器 3 | */ 4 | class RsaCodec { 5 | 6 | } 7 | 8 | export { 9 | RsaCodec 10 | }; -------------------------------------------------------------------------------- /src/codec/commons/url-encode-codec/url-encode-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * URL编解码器 3 | */ 4 | class UrlEncodeCodec { 5 | 6 | /** 7 | * 判断字符串是否已被 URL 编码 8 | * @param value {string} 9 | * @return {boolean} 10 | */ 11 | static isEncode(value: string): boolean { 12 | if (!value) { 13 | return false; 14 | } 15 | // URL 编码的字符串通常包含 % 符号 16 | return typeof value === 'string' && /%[0-9A-Fa-f]{2}/.test(value); 17 | } 18 | 19 | /** 20 | * 对字符串进行 URL 编码 21 | * @param value {string} 22 | * @return {string} 23 | */ 24 | static encode(value: string): string { 25 | if (typeof value !== 'string') { 26 | throw new TypeError('Input must be a string'); 27 | } 28 | return encodeURIComponent(value); 29 | } 30 | 31 | /** 32 | * 对 URL 编码的字符串进行解码 33 | * @param value {string} 34 | * @return {string} 35 | */ 36 | static decode(value: string): string { 37 | if (typeof value !== 'string') { 38 | throw new TypeError('Input must be a string'); 39 | } 40 | return decodeURIComponent(value); 41 | } 42 | } 43 | 44 | export { 45 | UrlEncodeCodec 46 | }; -------------------------------------------------------------------------------- /src/codec/commons/zip-codec/zip-codec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ZIP编解码器 3 | */ 4 | class ZipCodec { 5 | 6 | } 7 | 8 | export { 9 | ZipCodec 10 | }; -------------------------------------------------------------------------------- /src/config/ui/components/AboutTab.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 关于标签页组件 3 | */ 4 | 5 | // 使用declare语句声明一个全局变量,在构建过程中会被替换为实际版本号 6 | declare const __VERSION__: string; 7 | 8 | /** 9 | * 获取应用版本号 10 | * 尝试从几个不同来源获取版本号 11 | * 如果无法获取则返回"unknown" 12 | */ 13 | function getVersion(): string { 14 | try { 15 | // 优先使用通过webpack定义的全局版本变量 16 | if (typeof __VERSION__ !== 'undefined') { 17 | return __VERSION__; 18 | } 19 | 20 | // 如果未能获取到版本号,则返回unknown 21 | return 'unknown'; 22 | } catch (e) { 23 | // 出错时返回unknown 24 | return 'unknown'; 25 | } 26 | } 27 | 28 | /** 29 | * 关于标签页模板 30 | */ 31 | export const aboutTemplate = ` 32 |
33 |

关于

34 |
35 |

JS-XHR-HOOK

36 |

一个用于监控和调试XHR请求的工具

37 |

版本: ${getVersion()}

38 |

39 | GitHub仓库 40 |

41 |

42 | 问题反馈 43 |

44 |
45 |
46 | `; -------------------------------------------------------------------------------- /src/config/ui/components/Modal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模态窗口组件 3 | */ 4 | import { TabItem } from '../types'; 5 | import { xhrBreakpointsTemplate } from './XhrBreakpointsTab'; 6 | import { settingsTemplate } from './SettingsTab'; 7 | import { aboutTemplate } from './AboutTab'; 8 | 9 | /** 10 | * 标签页配置 11 | */ 12 | export const tabItems: TabItem[] = [ 13 | { id: 'xhr-breakpoints', name: 'XHR断点' }, 14 | { id: 'settings', name: '设置' }, 15 | { id: 'about', name: '关于' } 16 | ]; 17 | 18 | /** 19 | * 获取标签页模板内容 20 | * @param tabId 标签页ID 21 | * @returns 对应的模板字符串 22 | */ 23 | export function getTabTemplate(tabId: string): string { 24 | switch (tabId) { 25 | case 'xhr-breakpoints': 26 | return xhrBreakpointsTemplate; 27 | case 'settings': 28 | return settingsTemplate; 29 | case 'about': 30 | return aboutTemplate; 31 | default: 32 | return '
未知标签页
'; 33 | } 34 | } 35 | 36 | /** 37 | * 模态窗口模板 38 | */ 39 | export const modalTemplate = ` 40 |
41 |
42 |
43 |

JS-XHR-HOOK 配置

44 | 45 |
46 | 47 |
48 | 56 |
57 | 58 |
59 |
60 | ${xhrBreakpointsTemplate} 61 |
62 | 63 |
64 | ${settingsTemplate} 65 |
66 | 67 |
68 | ${aboutTemplate} 69 |
70 |
71 |
72 |
73 | `; 74 | 75 | /** 76 | * 模态窗口方法 77 | */ 78 | export const modalMethods = { 79 | /** 80 | * 关闭模态窗口 81 | */ 82 | closeModal(this: { showModal: boolean }): void { 83 | this.showModal = false; 84 | }, 85 | 86 | /** 87 | * 切换标签页 88 | * @param tabId 标签页ID 89 | */ 90 | switchTab(this: { activeTab: string }, tabId: string): void { 91 | this.activeTab = tabId; 92 | } 93 | }; -------------------------------------------------------------------------------- /src/config/ui/components/SettingsTab.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 设置标签页组件 3 | */ 4 | import { Settings } from '../types'; 5 | 6 | /** 7 | * 设置标签页模板 8 | */ 9 | export const settingsTemplate = ` 10 |
11 |

全局设置

12 | 13 |
14 |
15 | 19 |
20 | 21 |
22 | 26 |
27 | 28 |
29 | 33 |
34 | 35 |
36 | 40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 | `; 48 | 49 | /** 50 | * 设置标签页方法 51 | */ 52 | export const settingsMethods = { 53 | /** 54 | * 保存设置 55 | */ 56 | saveSettings(this: { settings: Settings }): void { 57 | // 这里可以添加保存设置到存储的逻辑 58 | alert('设置已保存!'); 59 | 60 | // 实际项目中应该与存储模块集成 61 | // import storage from '../../../storage'; 62 | // await storage.set('settings', this.settings); 63 | } 64 | }; 65 | 66 | /** 67 | * 默认设置数据 68 | */ 69 | export const defaultSettings: Settings = { 70 | darkMode: false, 71 | autoCapture: true, 72 | maxHistorySize: 100, 73 | notifyOnCapture: true 74 | }; -------------------------------------------------------------------------------- /src/config/ui/menu.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 注册油猴脚本菜单和配置界面 3 | * 这是界面模块的主入口文件 4 | */ 5 | import { AppState } from './types'; 6 | import { addModalStyles } from './styles'; 7 | import { loadVue, Vue } from './vueLoader'; 8 | import { modalTemplate, modalMethods, tabItems } from './components/Modal'; 9 | import { xhrBreakpointsMethods, defaultXhrBreakpoints } from './components/XhrBreakpointsTab'; 10 | import { settingsMethods, defaultSettings } from './components/SettingsTab'; 11 | import { getUnsafeWindow } from '../../utils/unsafe-window'; 12 | import Logger from '../../logger/logger'; 13 | 14 | // 为Window类型扩展,添加我们的自定义属性 15 | interface ExtendedWindow extends Window { 16 | __jsreiModalState?: any; 17 | } 18 | 19 | /** 20 | * 注册油猴脚本菜单 21 | */ 22 | function registerMenu(): void { 23 | Logger.info('注册油猴脚本菜单'); 24 | GM_registerMenuCommand( 25 | "打开配置界面", 26 | function () { 27 | showConfigModal(); 28 | } 29 | ); 30 | } 31 | 32 | /** 33 | * 显示配置模态窗口 34 | */ 35 | function showConfigModal(): void { 36 | // 获取真实的页面窗口对象 37 | const realWindow = getUnsafeWindow() as ExtendedWindow; 38 | 39 | // 避免重复加载 40 | if (document.getElementById('jsrei-modal-container')) { 41 | if (realWindow.__jsreiModalState) { 42 | Logger.debug('模态窗口已存在,设置显示状态'); 43 | realWindow.__jsreiModalState.showModal = true; 44 | } 45 | return; 46 | } 47 | 48 | Logger.info('创建配置模态窗口'); 49 | 50 | // 创建一个新的DOM元素作为Vue实例的挂载点 51 | const modalContainer = document.createElement('div'); 52 | modalContainer.id = 'jsrei-modal-container'; 53 | document.body.appendChild(modalContainer); 54 | 55 | // 加载Vue.js 56 | Logger.debug('开始加载Vue.js'); 57 | loadVue((vue) => createModalApp(vue, realWindow)); 58 | } 59 | 60 | /** 61 | * 创建模态窗口Vue应用 62 | * @param vue 隔离的Vue实例 63 | * @param realWindow 真实的窗口对象 64 | */ 65 | function createModalApp(vue: Vue, realWindow: ExtendedWindow): void { 66 | Logger.debug('Vue.js加载成功,创建应用'); 67 | const { createApp } = vue; 68 | 69 | // 添加模态窗口样式 70 | addModalStyles(); 71 | 72 | // 合并所有方法 73 | const methods = { 74 | ...modalMethods, 75 | ...xhrBreakpointsMethods, 76 | ...settingsMethods 77 | }; 78 | 79 | // 创建Vue应用 80 | Logger.debug('初始化Vue应用数据'); 81 | const app = createApp({ 82 | data(): AppState { 83 | return { 84 | showModal: true, 85 | activeTab: 'xhr-breakpoints', 86 | tabs: tabItems, 87 | xhrBreakpoints: defaultXhrBreakpoints, 88 | settings: defaultSettings 89 | }; 90 | }, 91 | methods, 92 | template: modalTemplate 93 | }); 94 | 95 | // 挂载Vue实例 96 | Logger.info('挂载Vue应用到DOM'); 97 | app.mount('#jsrei-modal-container'); 98 | 99 | // 保存对状态的引用,便于外部访问 100 | try { 101 | if (app && '_instance' in app && app._instance && 'data' in app._instance) { 102 | realWindow.__jsreiModalState = app._instance.data; 103 | Logger.debug('已保存Vue实例状态到全局对象'); 104 | } 105 | } catch (e) { 106 | Logger.error('无法访问Vue实例内部状态:', e); 107 | } 108 | } 109 | 110 | /** 111 | * 显示配置界面 112 | * @deprecated 使用showConfigModal代替 113 | */ 114 | function show(): void { 115 | Logger.warn('使用已废弃的show()方法,请改用showConfigModal()'); 116 | showConfigModal(); 117 | } 118 | 119 | export { 120 | registerMenu, 121 | showConfigModal, 122 | show 123 | }; -------------------------------------------------------------------------------- /src/config/ui/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * UI组件类型定义 3 | */ 4 | 5 | // Vue相关类型 6 | export interface VueAppOptions { 7 | data?: () => any; 8 | methods?: Record; 9 | template?: string; 10 | components?: Record; 11 | setup?: (props: any) => any; 12 | } 13 | 14 | export interface Vue { 15 | createApp: (options: VueAppOptions) => { 16 | mount: (selector: string) => void; 17 | _instance?: { data: any }; 18 | }; 19 | } 20 | 21 | // 应用状态类型 22 | export interface AppState { 23 | showModal: boolean; 24 | activeTab: string; 25 | tabs: TabItem[]; 26 | xhrBreakpoints: XhrBreakpoint[]; 27 | settings: Settings; 28 | } 29 | 30 | // 标签页定义 31 | export interface TabItem { 32 | id: string; 33 | name: string; 34 | } 35 | 36 | // XHR断点定义 37 | export interface XhrBreakpoint { 38 | id: number; 39 | url: string; 40 | enabled: boolean; 41 | condition?: string; 42 | } 43 | 44 | // 设置项定义 45 | export interface Settings { 46 | darkMode: boolean; 47 | autoCapture: boolean; 48 | maxHistorySize: number; 49 | notifyOnCapture: boolean; 50 | } 51 | 52 | // 油猴API类型声明 53 | declare global { 54 | function GM_registerMenuCommand( 55 | name: string, 56 | callback: () => void, 57 | accessKey?: string 58 | ): number; 59 | 60 | function GM_xmlhttpRequest(details: { 61 | method: string; 62 | url: string; 63 | onload: (response: { responseText: string }) => void; 64 | onerror: (error: Error) => void; 65 | }): void; 66 | 67 | const Vue: Vue; 68 | } -------------------------------------------------------------------------------- /src/config/ui/vueLoader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Vue加载模块 3 | */ 4 | import { getUnsafeWindow } from '../../utils/unsafe-window'; 5 | import Logger from '../../logger/logger'; 6 | 7 | /** 8 | * Vue实例类型定义 9 | */ 10 | export interface Vue { 11 | createApp: (options: any) => { 12 | mount: (selector: string) => void; 13 | _instance?: { data: any }; 14 | }; 15 | } 16 | 17 | // 为Window类型扩展,添加我们的自定义属性 18 | interface ExtendedWindow extends Window { 19 | __JSREI_Vue?: Vue; 20 | } 21 | 22 | // 用于存储隔离的Vue实例 23 | let isolatedVue: Vue | null = null; 24 | 25 | /** 26 | * 使用油猴API加载Vue,并隔离于页面的其他Vue实例 27 | * @param callback 加载成功后的回调函数 28 | */ 29 | export function loadVue(callback: (vue: Vue) => void): void { 30 | // 如果已经加载过,直接返回缓存的Vue实例 31 | if (isolatedVue) { 32 | Logger.debug('使用缓存的Vue实例'); 33 | callback(isolatedVue); 34 | return; 35 | } 36 | 37 | Logger.info('开始从CDN加载Vue.js'); 38 | GM_xmlhttpRequest({ 39 | method: 'GET', 40 | url: 'https://unpkg.com/vue@3/dist/vue.global.js', 41 | onload: function (response) { 42 | try { 43 | Logger.debug('Vue.js下载成功,开始初始化隔离环境'); 44 | // 获取真实的页面窗口对象 45 | const realWindow = getUnsafeWindow() as ExtendedWindow; 46 | 47 | // 使用IIFE隔离Vue实例,避免全局污染 48 | const scriptContent = ` 49 | (function() { 50 | ${response.responseText} 51 | // 将Vue实例存储在自定义变量中而不是全局window对象上 52 | window.__JSREI_Vue = Vue; 53 | // 防止Vue暴露到全局作用域 54 | Vue = undefined; 55 | })(); 56 | `; 57 | 58 | // 创建并执行脚本 59 | const script = document.createElement('script'); 60 | script.textContent = scriptContent; 61 | document.head.appendChild(script); 62 | 63 | // 脚本执行后获取隔离的Vue实例 64 | script.onload = () => { 65 | Logger.debug('Vue.js脚本执行完毕,获取隔离实例'); 66 | // 使用realWindow获取隔离的Vue实例 67 | const vueInstance = realWindow.__JSREI_Vue; 68 | 69 | // 确保Vue实例正确加载 70 | if (vueInstance && typeof vueInstance.createApp === 'function') { 71 | Logger.info('Vue.js实例加载成功'); 72 | isolatedVue = vueInstance; 73 | // 清除临时全局变量 74 | delete realWindow.__JSREI_Vue; 75 | // 返回隔离的Vue实例,此时isolatedVue必定非空 76 | callback(isolatedVue!); 77 | } else { 78 | Logger.error('Vue实例加载失败或格式不正确'); 79 | } 80 | }; 81 | } catch (error) { 82 | Logger.error('初始化隔离Vue实例失败:', error); 83 | } 84 | }, 85 | onerror: function (error) { 86 | Logger.error('加载Vue.js失败:', error); 87 | }, 88 | }); 89 | } -------------------------------------------------------------------------------- /src/context/auth-context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 认证上下文,如果请求中有携带认证的话,会把响应的值设置上 3 | */ 4 | class AuthContext { 5 | 6 | username: string | null; 7 | password: string | null; 8 | 9 | /** 10 | * 构造函数 11 | * @param username {string | null} 用户名 12 | * @param password {string | null} 密码 13 | */ 14 | constructor(username: string | null = null, password: string | null = null) { 15 | this.username = username; 16 | this.password = password; 17 | } 18 | 19 | /** 20 | * 判断认证上下文是否为空 21 | * @return {boolean} 如果用户名和密码都为空则返回true,否则返回false 22 | */ 23 | isEmpty(): boolean { 24 | return !this.username && !this.password; 25 | } 26 | 27 | } 28 | 29 | export { 30 | AuthContext 31 | }; -------------------------------------------------------------------------------- /src/context/body-context.ts: -------------------------------------------------------------------------------- 1 | import { ContextLocation } from "./context-location"; 2 | import { ContentType } from "./content-type"; 3 | import { Param } from "./param"; 4 | import { ParamType } from "./param-type"; 5 | import { ParamContext } from './param-context'; 6 | 7 | interface JsonData { 8 | [key: string]: unknown; 9 | } 10 | 11 | /** 12 | * 表示一个请求体或者响应体 13 | */ 14 | export class BodyContext { 15 | /** 16 | * 原始数据 17 | */ 18 | rawBody: string | Blob | ArrayBuffer | FormData | URLSearchParams | null = null; 19 | 20 | /** 21 | * 原始数据的文本形式 22 | */ 23 | rawBodyText: string | null = null; 24 | 25 | /** 26 | * 上下文位置(请求/响应) 27 | */ 28 | location: symbol = ContextLocation.REQUEST; 29 | 30 | /** 31 | * 内容类型 32 | */ 33 | contentType: typeof ContentType[keyof typeof ContentType] = ContentType.PLAINTEXT; 34 | 35 | /** 36 | * 参数上下文 37 | */ 38 | paramContext: ParamContext | null = null; 39 | 40 | /** 41 | * JSON数据 42 | */ 43 | jsonData: JsonData | null = null; 44 | 45 | /** 46 | * XML内容 47 | */ 48 | xmlContent: Document | null = null; 49 | 50 | /** 51 | * 文本内容 52 | */ 53 | textContent: string | null = null; 54 | 55 | /** 56 | * Blob数据 57 | */ 58 | blobData: Blob | null = null; 59 | 60 | /** 61 | * ArrayBuffer数据 62 | */ 63 | arrayBufferData: ArrayBuffer | null = null; 64 | 65 | /** 66 | * 对象形式的数据 67 | */ 68 | object: Record = {}; 69 | 70 | // 如果是请求体的话,则在此处携带请求体里的参数 71 | params: Param[] = []; 72 | 73 | // 会分析一下rawBody是否是url编码了 74 | isRawBodyUrlEncode: boolean = false; 75 | // 如果是被url编码的话则会尝试对其进行解码 76 | rawBodyUrlDecode: string | null = null; 77 | 78 | // 会分析一下rawBody是否是使用hex编码了 79 | isRawBodyHex: boolean = false; 80 | // 如果是的话,则会尝试解码一下 81 | rawBodyHexDecode: string | null = null; 82 | 83 | // 会分析一下rawBody是否是使用base64编码了 84 | isRawBodyBase64: boolean = false; 85 | // 如果是的话,则会尝试解码一下 86 | rawBodyBase64Decode: string | null = null; 87 | 88 | constructor() { 89 | this.location = ContextLocation.UNKNOWN; 90 | this.rawBody = null; 91 | this.rawBodyText = null; 92 | this.contentType = ContentType.UNKNOWN; 93 | this.params = []; 94 | this.object = {}; 95 | this.isRawBodyUrlEncode = false; 96 | this.rawBodyUrlDecode = null; 97 | this.isRawBodyHex = false; 98 | this.rawBodyHexDecode = null; 99 | this.isRawBodyBase64 = false; 100 | this.rawBodyBase64Decode = null; 101 | } 102 | 103 | /** 104 | * 获取原始请求体的纯文本内容 105 | * @returns 解码后的请求体内容 106 | */ 107 | getRawBodyPlain(): string | null { 108 | if (this.isRawBodyBase64) { 109 | return this.rawBodyBase64Decode; 110 | } 111 | 112 | if (this.isRawBodyHex) { 113 | return this.rawBodyHexDecode; 114 | } 115 | 116 | if (this.isRawBodyUrlEncode) { 117 | return this.rawBodyUrlDecode; 118 | } 119 | 120 | if (typeof this.rawBody === 'string') { 121 | return this.rawBody; 122 | } 123 | 124 | if (this.rawBody instanceof URLSearchParams) { 125 | return this.rawBody.toString(); 126 | } 127 | 128 | if (this.textContent) { 129 | return this.textContent; 130 | } 131 | 132 | return null; 133 | } 134 | 135 | /** 136 | * 添加参数 137 | * @param name 参数名 138 | * @param value 参数值 139 | */ 140 | addParam(name: string, value: string): void { 141 | const param = new Param(); 142 | param.name = name; 143 | param.value = value; 144 | param.paramType = ParamType.FORM; 145 | param.paramLocation = this.location; 146 | this.params.push(param); 147 | } 148 | } -------------------------------------------------------------------------------- /src/context/content-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 表示内容的类型 3 | */ 4 | export const ContentType = { 5 | // 未知 6 | UNKNOWN: { value: "UNKNOWN", description: "未知类型" }, 7 | 8 | // 纯文本字符串 9 | PLAINTEXT: { value: "PLAINTEXT", description: "纯文本" }, 10 | 11 | // JSON字符串 12 | JSON: { value: "JSON", description: "JSON" }, 13 | 14 | // XML格式 15 | XML: { value: "XML", description: "XML" }, 16 | 17 | // 二进制 18 | BINARY: { value: "BINARY", description: "二进制" }, 19 | 20 | // 表单 21 | FORM: { value: "FORM", description: "表单" }, 22 | 23 | // Blob 24 | BLOB: { value: "BLOB", description: "Blob" }, 25 | 26 | // ArrayBuffer 27 | ARRAYBUFFER: { value: "ARRAYBUFFER", description: "ArrayBuffer" } 28 | } as const; -------------------------------------------------------------------------------- /src/context/context-location.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 用于表示是在请求头还是在响应头里 3 | */ 4 | export const ContextLocation = { 5 | // 未知 6 | UNKNOWN: Symbol('UNKNOWN'), 7 | 8 | // 在请求中 9 | REQUEST: Symbol('REQUEST'), 10 | 11 | // 在响应中 12 | RESPONSE: Symbol('RESPONSE') 13 | } as const; -------------------------------------------------------------------------------- /src/context/event-context.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 事件上下文类 3 | * 用于管理和存储事件相关的信息 4 | */ 5 | class EventContext { 6 | // 存储事件的数组 7 | events: Event[]; 8 | 9 | constructor() { 10 | this.events = []; 11 | } 12 | 13 | /** 14 | * 添加一个事件到事件上下文中 15 | * @param event {Event} 要添加的事件对象 16 | */ 17 | addEvent(event: Event): void { 18 | // TODO 2025-01-11 00:18:17 同名的时候覆盖? 19 | this.events.push(event); 20 | } 21 | } 22 | 23 | export { 24 | EventContext 25 | }; -------------------------------------------------------------------------------- /src/context/fetch-context.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from './request-context'; 2 | import { ResponseContext } from './response-context'; 3 | 4 | /** 5 | * FetchContext 类 6 | * 用于表示一个 Fetch API 请求的上下文。 7 | * 该类封装了请求和响应的上下文信息,方便对 Fetch 请求进行管理和操作。 8 | * 9 | * 参考文档: 10 | * https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API 11 | */ 12 | class FetchContext { 13 | // 唯一请求标识 14 | id: string; 15 | 16 | // 请求上下文 17 | requestContext: RequestContext; 18 | 19 | // 响应上下文 20 | responseContext: ResponseContext; 21 | 22 | // 原始请求 23 | originalRequest: Request | null = null; 24 | 25 | // 原始响应 26 | originalResponse: Response | null = null; 27 | 28 | /** 29 | * 构造函数 30 | * 初始化 FetchContext 实例,创建请求上下文和响应上下文的实例。 31 | */ 32 | constructor() { 33 | // 初始id为空字符串,会在FetchHook中被设置 34 | this.id = ''; 35 | 36 | // 初始化请求上下文 37 | this.requestContext = new RequestContext(); 38 | // 初始化响应上下文 39 | this.responseContext = new ResponseContext(); 40 | } 41 | } 42 | 43 | export { 44 | FetchContext 45 | }; -------------------------------------------------------------------------------- /src/context/header-context.ts: -------------------------------------------------------------------------------- 1 | import { ContextLocation } from './context-location'; 2 | import { Header } from './header'; 3 | 4 | /** 5 | * 表示一个 HTTP 头部上下文 6 | */ 7 | export class HeaderContext { 8 | /** 9 | * 存储所有的头部信息 10 | */ 11 | protected headers: Header[]; 12 | 13 | // ContextLocation,用于表明是请求头还是响应头的上下文 14 | location: symbol; 15 | 16 | constructor() { 17 | this.location = ContextLocation.UNKNOWN; 18 | this.headers = []; 19 | } 20 | 21 | /** 22 | * 添加一个头部 23 | * @param name 头部名称 24 | * @param value 头部值 25 | */ 26 | add(name: string, value: string): void { 27 | const header = new Header(); 28 | header.name = name.toLowerCase(); 29 | header.value = value; 30 | header.location = this.location; 31 | header.isCustom = false; 32 | this.headers.push(header); 33 | } 34 | 35 | /** 36 | * 根据名称获取头部 37 | * @param name 头部名称 38 | * @returns 头部对象,如果不存在则返回 null 39 | */ 40 | getByName(name: string): Header | null { 41 | name = name.toLowerCase(); 42 | for (const header of this.headers) { 43 | if (header.name?.toLowerCase() === name) { 44 | return header; 45 | } 46 | } 47 | return null; 48 | } 49 | 50 | /** 51 | * 获取所有头部 52 | * @returns 所有头部的数组 53 | */ 54 | getAll(): Header[] { 55 | return this.headers; 56 | } 57 | 58 | /** 59 | * 清空所有头部 60 | */ 61 | clear(): void { 62 | this.headers = []; 63 | } 64 | } -------------------------------------------------------------------------------- /src/context/header.ts: -------------------------------------------------------------------------------- 1 | import { ContextLocation } from './context-location'; 2 | 3 | /** 4 | * 用于表示一个请求头或响应头 5 | */ 6 | export class Header { 7 | // HeaderLocation类型,用于表示这个头所处的位置 8 | location: symbol; 9 | 10 | // 请求头的名称 11 | name: string | null; 12 | 13 | // 请求头的值 14 | value: string | null; 15 | 16 | // 此请求头或响应头是否是自定义的 17 | isCustom: boolean | null; 18 | 19 | constructor() { 20 | this.location = ContextLocation.UNKNOWN; 21 | this.name = null; 22 | this.value = null; 23 | this.isCustom = null; 24 | } 25 | } -------------------------------------------------------------------------------- /src/context/param-context.ts: -------------------------------------------------------------------------------- 1 | import { Param } from './param'; 2 | 3 | /** 4 | * 参数上下文,用于管理一组参数 5 | */ 6 | export class ParamContext { 7 | /** 8 | * 参数列表 9 | */ 10 | private params: Param[] = []; 11 | 12 | /** 13 | * 添加参数 14 | * @param param 参数对象 15 | */ 16 | add(param: Param): void { 17 | this.params.push(param); 18 | } 19 | 20 | /** 21 | * 获取所有参数 22 | * @returns {Param[]} 参数列表 23 | */ 24 | getParams(): Param[] { 25 | return this.params; 26 | } 27 | 28 | /** 29 | * 根据参数名获取参数 30 | * @param name 参数名 31 | * @returns {Param | undefined} 参数对象,如果不存在则返回undefined 32 | */ 33 | getParamByName(name: string): Param | undefined { 34 | return this.params.find(param => param.name === name); 35 | } 36 | 37 | /** 38 | * 获取参数数量 39 | * @returns {number} 参数数量 40 | */ 41 | size(): number { 42 | return this.params.length; 43 | } 44 | 45 | /** 46 | * 清空所有参数 47 | */ 48 | clear(): void { 49 | this.params = []; 50 | } 51 | } -------------------------------------------------------------------------------- /src/context/param-type.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 表示参数的类型 3 | */ 4 | export const ParamType = { 5 | // 未知,暂不支持的参数类型 6 | UNKNOWN: "UNKNOWN", 7 | 8 | // 表单参数 9 | FORM: "FORM", 10 | 11 | // 从query string中提取出来的参数 12 | QUERY_STRING: "QUERY_STRING", 13 | 14 | // JSON类型的参数 15 | JSON: "JSON", 16 | 17 | // XML参数 18 | XML: "XML", 19 | 20 | // URL参数 21 | URL: "URL" 22 | } as const; -------------------------------------------------------------------------------- /src/context/param.ts: -------------------------------------------------------------------------------- 1 | import { ParamType } from './param-type'; 2 | import { ContextLocation } from './context-location'; 3 | 4 | /** 5 | * 表示一个参数 6 | */ 7 | export class Param { 8 | /** 9 | * 参数名称 10 | */ 11 | name: string | null = null; 12 | 13 | /** 14 | * 参数值 15 | */ 16 | value: string | null = null; 17 | 18 | /** 19 | * 参数类型 20 | */ 21 | paramType: keyof typeof ParamType = "UNKNOWN"; 22 | 23 | /** 24 | * 参数位置(请求/响应) 25 | */ 26 | paramLocation: symbol = ContextLocation.UNKNOWN; 27 | 28 | /** 29 | * 参数是否经过URL编码 30 | */ 31 | isUrlEncoded: boolean = false; 32 | 33 | /** 34 | * URL解码后的值 35 | */ 36 | urlDecodedValue: string | null = null; 37 | 38 | /** 39 | * 参数是否经过Base64编码 40 | */ 41 | isBase64Encoded: boolean = false; 42 | 43 | /** 44 | * Base64解码后的值 45 | */ 46 | base64DecodedValue: string | null = null; 47 | 48 | /** 49 | * 参数是否经过十六进制编码 50 | */ 51 | isHexEncoded: boolean = false; 52 | 53 | /** 54 | * 十六进制解码后的值 55 | */ 56 | hexDecodedValue: string | null = null; 57 | 58 | /** 59 | * 参数是否为十六进制值 60 | */ 61 | isValueHex: boolean = false; 62 | 63 | /** 64 | * 十六进制值解码后的结果 65 | */ 66 | valueHexDecode: string | null = null; 67 | 68 | /** 69 | * 参数是否被Base64编码 70 | */ 71 | isValueBase64: boolean = false; 72 | 73 | /** 74 | * Base64解码后的值 75 | */ 76 | valueBase64Decode: string | null = null; 77 | 78 | constructor() { 79 | // 表示参数的类型,参数是从哪里提取出来的 80 | this.paramType = "UNKNOWN"; 81 | 82 | // 参数的位置 83 | this.paramLocation = ContextLocation.UNKNOWN; 84 | 85 | // 参数名称 86 | this.name = null; 87 | 88 | // 参数的值 89 | this.value = null; 90 | 91 | // 会分析一下value是否是url编码了 92 | this.isUrlEncoded = false; 93 | // 如果是被url编码的话则会尝试对其进行解码 94 | this.urlDecodedValue = null; 95 | 96 | // 会分析一下value是否是使用hex编码了 97 | this.isHexEncoded = false; 98 | // 如果是的话,则会尝试解码一下 99 | this.hexDecodedValue = null; 100 | 101 | // 会分析一下value是否是使用base64编码了 102 | this.isBase64Encoded = false; 103 | // 如果是的话,则会尝试解码一下 104 | this.base64DecodedValue = null; 105 | 106 | // 会分析一下value是否是十六进制值 107 | this.isValueHex = false; 108 | // 如果是的话,则会尝试解码一下 109 | this.valueHexDecode = null; 110 | 111 | // 会分析一下value是否是使用base64编码了 112 | this.isValueBase64 = false; 113 | // 如果是的话,则会尝试解码一下 114 | this.valueBase64Decode = null; 115 | } 116 | 117 | /** 118 | * 尝试获取一个最干净最还原的value 119 | */ 120 | getValuePlain(): string | null { 121 | if (this.isBase64Encoded) { 122 | return this.base64DecodedValue; 123 | } 124 | 125 | if (this.isHexEncoded) { 126 | return this.hexDecodedValue; 127 | } 128 | 129 | if (this.isUrlEncoded && this.urlDecodedValue) { 130 | return this.urlDecodedValue; 131 | } 132 | 133 | if (this.isValueHex) { 134 | return this.valueHexDecode; 135 | } 136 | 137 | if (this.isValueBase64 && this.valueBase64Decode) { 138 | return this.valueBase64Decode; 139 | } 140 | 141 | return this.value; 142 | } 143 | } -------------------------------------------------------------------------------- /src/context/request-context.ts: -------------------------------------------------------------------------------- 1 | import { HeaderContext } from './header-context'; 2 | import { UrlContext } from './url-context'; 3 | import { BodyContext } from './body-context'; 4 | import { AuthContext } from './auth-context'; 5 | import { EventContext } from './event-context'; 6 | import { Header } from './header'; 7 | import { Param } from './param'; 8 | import { ParamContext } from './param-context'; 9 | 10 | /** 11 | * 表示一个请求上下文 12 | */ 13 | class RequestContext { 14 | method: string | null; 15 | 16 | // 请求的当前状态 17 | // https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readyState 18 | readyState: number; 19 | 20 | isAsync: boolean; 21 | isAbortted: boolean; 22 | 23 | // 认证相关的上下文 24 | authContext: AuthContext; 25 | 26 | // 事件上下文 27 | eventContext: EventContext; 28 | 29 | urlContext: UrlContext; 30 | 31 | // 请求携带的请求体 32 | bodyContext: BodyContext; 33 | 34 | // 请求的请求头上下文 35 | headerContext: HeaderContext; 36 | 37 | // 请求参数上下文 38 | paramContext: ParamContext; 39 | 40 | // 覆盖的MIME类型 41 | overrideMimeType: string | null; 42 | 43 | constructor() { 44 | this.method = null; 45 | this.readyState = XMLHttpRequest.UNSENT; 46 | this.isAsync = true; 47 | this.isAbortted = false; 48 | this.authContext = new AuthContext(); 49 | this.eventContext = new EventContext(); 50 | this.urlContext = new UrlContext(); 51 | this.bodyContext = new BodyContext(); 52 | this.headerContext = new HeaderContext(); 53 | this.paramContext = new ParamContext(); 54 | this.overrideMimeType = null; 55 | } 56 | 57 | /** 58 | * 获取请求中所有的上下文 59 | * @return {Param[]} 所有参数的数组 60 | */ 61 | getParams(): Param[] { 62 | const params: Param[] = []; 63 | params.push(...this.urlContext.params); 64 | params.push(...this.bodyContext.params); 65 | return params; 66 | } 67 | 68 | /** 69 | * 获取Content-Type请求头 70 | * @return {Header|null} Content-Type请求头对象,如果不存在则返回null 71 | */ 72 | getContentTypeHeader(): Header | null { 73 | return this.headerContext.getByName("content-type"); 74 | } 75 | 76 | /** 77 | * 判断请求体是否是json 78 | * @return {boolean} 如果是json则返回true,否则返回false 79 | */ 80 | isJson(): boolean { 81 | const header = this.getContentTypeHeader(); 82 | if (!header) { 83 | return false; 84 | } 85 | return header.value?.toLowerCase().includes("application/json") || false; 86 | } 87 | 88 | /** 89 | * 判断请求体是否是表单 90 | * @return {boolean} 如果是表单则返回true,否则返回false 91 | */ 92 | isForm(): boolean { 93 | const header = this.getContentTypeHeader(); 94 | if (!header) { 95 | return false; 96 | } 97 | const contentType = header.value?.toLowerCase(); 98 | // TODO 2025-01-11 11:09:47 是否还有其它类型 99 | return contentType?.includes("application/x-www-form-urlencoded;") || false; 100 | } 101 | 102 | /** 103 | * 判断请求是否结束 104 | * @return {boolean} 如果请求结束则返回true,否则返回false 105 | */ 106 | isRequestDone(): boolean { 107 | return this.readyState === XMLHttpRequest.DONE; 108 | } 109 | } 110 | 111 | export { 112 | RequestContext 113 | }; -------------------------------------------------------------------------------- /src/context/response-context.ts: -------------------------------------------------------------------------------- 1 | import { UrlContext } from './url-context'; 2 | import { BodyContext } from './body-context'; 3 | import { HeaderContext } from './header-context'; 4 | 5 | /** 6 | * 表示一个响应上下文 7 | */ 8 | export class ResponseContext { 9 | /** 10 | * 响应状态码 11 | */ 12 | statusCode: number = 0; 13 | 14 | /** 15 | * URL上下文 16 | */ 17 | urlContext: UrlContext = new UrlContext(); 18 | 19 | /** 20 | * 头部上下文 21 | */ 22 | headerContext: HeaderContext = new HeaderContext(); 23 | 24 | /** 25 | * 响应体上下文 26 | */ 27 | bodyContext: BodyContext = new BodyContext(); 28 | 29 | /** 30 | * 响应类型 31 | */ 32 | responseType: XMLHttpRequestResponseType = ''; 33 | 34 | /** 35 | * 判断响应是否是JSON 36 | */ 37 | isJson(): boolean { 38 | const contentType = this.headerContext.getByName('content-type'); 39 | return contentType?.value?.toLowerCase().includes('application/json') || false; 40 | } 41 | } -------------------------------------------------------------------------------- /src/context/url-context.ts: -------------------------------------------------------------------------------- 1 | import { Param } from './param'; 2 | 3 | /** 4 | * UrlContext 类 5 | * 用于表示与 URL 相关的上下文信息。 6 | * 该类封装了 URL 的各个组成部分,方便对 URL 进行解析和操作。 7 | */ 8 | class UrlContext { 9 | /** 10 | * 原始 URL 11 | */ 12 | rawUrl: string | null; 13 | 14 | /** 15 | * 域名 16 | */ 17 | domain: string | null; 18 | 19 | /** 20 | * 端口 21 | */ 22 | port: number | null; 23 | 24 | /** 25 | * 协议 26 | */ 27 | protocol: string | null; 28 | 29 | /** 30 | * 查询字符串 31 | */ 32 | queryString: string | null; 33 | 34 | /** 35 | * 请求路径 36 | */ 37 | requestPath: string | null; 38 | 39 | /** 40 | * URL 参数 41 | */ 42 | params: Param[]; 43 | 44 | /** 45 | * 构造函数 46 | * 初始化 UrlContext 实例,设置 URL 相关的属性。 47 | */ 48 | constructor() { 49 | this.rawUrl = null; 50 | this.domain = null; 51 | this.port = null; 52 | this.protocol = null; 53 | this.queryString = null; 54 | this.requestPath = null; 55 | this.params = []; 56 | } 57 | 58 | /** 59 | * 设置 URL 60 | * @param url URL 字符串 61 | */ 62 | setUrl(url: string): void { 63 | this.rawUrl = url; 64 | try { 65 | const urlObj = new URL(url); 66 | this.domain = urlObj.hostname; 67 | this.port = urlObj.port ? parseInt(urlObj.port) : null; 68 | this.protocol = urlObj.protocol.slice(0, -1); // 移除末尾的 ':' 69 | this.queryString = urlObj.search.slice(1); // 移除开头的 '?' 70 | this.requestPath = urlObj.pathname; 71 | 72 | // 解析查询参数 73 | this.params = []; 74 | urlObj.searchParams.forEach((value, name) => { 75 | const param = new Param(); 76 | param.name = name; 77 | param.value = value; 78 | this.params.push(param); 79 | }); 80 | } catch (e) { 81 | console.error('Failed to parse URL:', e); 82 | } 83 | } 84 | 85 | /** 86 | * 清空 URL 信息 87 | */ 88 | clear(): void { 89 | this.rawUrl = null; 90 | this.domain = null; 91 | this.port = null; 92 | this.protocol = null; 93 | this.queryString = null; 94 | this.requestPath = null; 95 | this.params = []; 96 | } 97 | 98 | /** 99 | * 获取完整的 URL 100 | */ 101 | getFullUrl(): string | null { 102 | return this.rawUrl; 103 | } 104 | } 105 | 106 | export { 107 | UrlContext 108 | }; -------------------------------------------------------------------------------- /src/context/xhr-context.ts: -------------------------------------------------------------------------------- 1 | import { RequestContext } from './request-context'; 2 | import { ResponseContext } from './response-context'; 3 | 4 | /** 5 | * XhrContext 类 6 | * 用于表示一个 XMLHttpRequest (XHR) 请求的上下文。 7 | * 该类封装了请求和响应的上下文信息,方便对 XHR 请求进行管理和操作。 8 | * 9 | * 参考文档: 10 | * https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest 11 | */ 12 | class XhrContext { 13 | // 请求上下文 14 | requestContext: RequestContext; 15 | 16 | // 响应上下文 17 | responseContext: ResponseContext; 18 | 19 | /** 20 | * 构造函数 21 | * 初始化 XhrContext 实例,创建请求上下文和响应上下文的实例。 22 | */ 23 | constructor() { 24 | // 初始化请求上下文 25 | this.requestContext = new RequestContext(); 26 | // 初始化响应上下文 27 | this.responseContext = new ResponseContext(); 28 | } 29 | } 30 | 31 | export { 32 | XhrContext 33 | }; -------------------------------------------------------------------------------- /src/debuggers/debugger-tester.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../context/xhr-context"; 2 | import Logger from "../logger/logger"; 3 | 4 | /** 5 | * 调试器测试类 6 | */ 7 | export class DebuggerTester { 8 | /** 9 | * 测试调试器 10 | * @param xhrContext XHR上下文对象 11 | */ 12 | static test(xhrContext: XhrContext): void { 13 | // 在此处添加测试逻辑 14 | Logger.group('调试器测试', () => { 15 | Logger.info('测试上下文:', xhrContext); 16 | Logger.debug('URL:', xhrContext.requestContext.urlContext.rawUrl); 17 | Logger.debug('方法:', xhrContext.requestContext.method); 18 | 19 | // 请求头 20 | const requestHeaders = xhrContext.requestContext.headerContext.getAll(); 21 | if (requestHeaders && requestHeaders.length > 0) { 22 | Logger.debug('请求头:'); 23 | Logger.dir(requestHeaders); 24 | } 25 | 26 | // 响应头 27 | const responseHeaders = xhrContext.responseContext.headerContext.getAll(); 28 | if (responseHeaders && responseHeaders.length > 0) { 29 | Logger.debug('响应头:'); 30 | Logger.dir(responseHeaders); 31 | } 32 | 33 | // 请求体 34 | const requestBody = xhrContext.requestContext.bodyContext.getRawBodyPlain(); 35 | if (requestBody) { 36 | Logger.debug('请求体:'); 37 | Logger.info(requestBody); 38 | } 39 | 40 | // 响应体 41 | const responseBody = xhrContext.responseContext.bodyContext.getRawBodyPlain(); 42 | if (responseBody) { 43 | Logger.debug('响应体:'); 44 | Logger.info(responseBody); 45 | } 46 | }); 47 | } 48 | } -------------------------------------------------------------------------------- /src/debuggers/debugger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 调试器配置接口 3 | */ 4 | export interface DebuggerConfig { 5 | enable?: boolean; 6 | enableRequestUrlFilter?: boolean; 7 | requestUrlCondition?: string | null; 8 | requestParamNameCondition?: string | null; 9 | requestParamValueCondition?: string | null; 10 | setRequestHeaderNameCondition?: string | null; 11 | setRequestHeaderValueCondition?: string | null; 12 | requestBodyCondition?: string | null; 13 | getResponseHeaderNameCondition?: string | null; 14 | getResponseHeaderValueCondition?: string | null; 15 | responseBodyCondition?: string | null; 16 | enableDebuggerBeforeRequestSend?: boolean; 17 | enableDebuggerAfterResponseReceive?: boolean; 18 | actionDebuggerEnable?: { 19 | open?: boolean; 20 | setRequestHeader?: boolean; 21 | send?: boolean; 22 | responseCallback?: boolean; 23 | visitResponseAttribute?: boolean; 24 | }; 25 | } 26 | 27 | /** 28 | * 调试器基类 29 | */ 30 | export class Debugger { 31 | /** 32 | * 匹配器类型 33 | */ 34 | matcherType: string; 35 | 36 | /** 37 | * 调试器配置 38 | */ 39 | config: DebuggerConfig; 40 | 41 | constructor(config?: DebuggerConfig) { 42 | this.matcherType = ""; 43 | this.config = config || {}; 44 | } 45 | } -------------------------------------------------------------------------------- /src/debuggers/header-debugger.ts: -------------------------------------------------------------------------------- 1 | import { StringMatcher } from './string-matcher'; 2 | 3 | export const headerDebuggerTypeRequest = 'request'; 4 | export const headerDebuggerTypeResponse = 'response'; 5 | 6 | export class HeaderDebugger { 7 | type: string; 8 | onSetHeaderName: StringMatcher | null; 9 | onGetHeaderName: StringMatcher | null; 10 | onHeaderValueMatch: StringMatcher; 11 | 12 | constructor() { 13 | this.type = headerDebuggerTypeRequest; 14 | this.onSetHeaderName = null; 15 | this.onGetHeaderName = null; 16 | this.onHeaderValueMatch = new StringMatcher(); 17 | } 18 | } -------------------------------------------------------------------------------- /src/debuggers/id-generator.ts: -------------------------------------------------------------------------------- 1 | export class IDGenerator { 2 | private static counter = 0; 3 | 4 | static generate(): string { 5 | return `xhr_${++this.counter}`; 6 | } 7 | } -------------------------------------------------------------------------------- /src/debuggers/string-matcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 字符串匹配器 3 | */ 4 | export class StringMatcher { 5 | /** 6 | * 匹配类型 7 | */ 8 | type: string; 9 | 10 | /** 11 | * 匹配内容 12 | */ 13 | payload: string; 14 | 15 | constructor() { 16 | this.type = ""; 17 | this.payload = ""; 18 | } 19 | 20 | /** 21 | * 匹配字符串 22 | * @param s 要匹配的字符串 23 | * @returns {boolean} 是否匹配 24 | */ 25 | match(_s: string): boolean { 26 | // TODO: 实现匹配逻辑 27 | return false; 28 | } 29 | } -------------------------------------------------------------------------------- /src/hook/fetch/holder.ts: -------------------------------------------------------------------------------- 1 | import { getUnsafeWindow } from "../../utils/unsafe-window"; 2 | 3 | // 保存原始的 fetch 函数 4 | export const ancestorFetchHolder: typeof fetch = ((getUnsafeWindow() as Window & typeof globalThis).fetch); -------------------------------------------------------------------------------- /src/hook/fetch/index.ts: -------------------------------------------------------------------------------- 1 | import { FetchHook } from './fetch-hook'; 2 | 3 | /** 4 | * 为fetch API添加hook 5 | * @returns {void} 6 | */ 7 | export function addFetchHook(): void { 8 | new FetchHook().addHook(); 9 | } 10 | 11 | export { 12 | FetchHook 13 | }; -------------------------------------------------------------------------------- /src/hook/fetch/logger/error-logger.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import Logger from '../../../logger'; 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | /** 6 | * 错误类型枚举 7 | */ 8 | export enum FetchErrorType { 9 | REQUEST_PARSING = 'REQUEST_PARSING', 10 | REQUEST_HEADERS_PARSING = 'REQUEST_HEADERS_PARSING', 11 | REQUEST_BODY_PARSING = 'REQUEST_BODY_PARSING', 12 | REQUEST_OBJECT_PARSING = 'REQUEST_OBJECT_PARSING', 13 | URL_PARSING = 'URL_PARSING', 14 | RESPONSE_PARSING = 'RESPONSE_PARSING', 15 | RESPONSE_HEADERS_PARSING = 'RESPONSE_HEADERS_PARSING', 16 | RESPONSE_BODY_PARSING = 'RESPONSE_BODY_PARSING', 17 | EXECUTION = 'EXECUTION', 18 | UNKNOWN = 'UNKNOWN' 19 | } 20 | 21 | /** 22 | * 错误上下文接口 23 | */ 24 | export interface ErrorContext { 25 | url?: string; 26 | method?: string; 27 | requestId: string; 28 | timestamp: number; 29 | errorType: FetchErrorType; 30 | additionalInfo?: Record; 31 | } 32 | 33 | /** 34 | * 创建错误上下文 35 | * @param fetchContext fetch上下文 36 | * @param errorType 错误类型 37 | * @param additionalInfo 附加信息 38 | * @returns 错误上下文 39 | */ 40 | export function createErrorContext( 41 | fetchContext: FetchContext | null, 42 | errorType: FetchErrorType, 43 | additionalInfo?: Record 44 | ): ErrorContext { 45 | const context: ErrorContext = { 46 | timestamp: Date.now(), 47 | errorType, 48 | requestId: fetchContext?.id || uuidv4() // 使用fetchContext的id或者生成新ID 49 | }; 50 | 51 | if (fetchContext) { 52 | context.url = fetchContext.requestContext.urlContext.rawUrl || undefined; 53 | context.method = fetchContext.requestContext.method || undefined; 54 | } 55 | 56 | if (additionalInfo) { 57 | context.additionalInfo = additionalInfo; 58 | } 59 | 60 | return context; 61 | } 62 | 63 | /** 64 | * 格式化错误上下文为日志消息 65 | * @param context 错误上下文 66 | * @returns 格式化的日志消息 67 | */ 68 | function formatErrorContext(context: ErrorContext): string { 69 | const parts = [ 70 | `错误类型: ${context.errorType}`, 71 | ]; 72 | 73 | if (context.requestId) { 74 | parts.push(`请求ID: ${context.requestId}`); 75 | } 76 | 77 | if (context.url) { 78 | parts.push(`URL: ${context.url}`); 79 | } 80 | 81 | if (context.method) { 82 | parts.push(`方法: ${context.method}`); 83 | } 84 | 85 | return parts.join(' | '); 86 | } 87 | 88 | /** 89 | * 记录详细的错误信息 90 | * @param message 错误消息 91 | * @param error 错误对象 92 | * @param context 错误上下文 93 | */ 94 | export function logFetchError( 95 | message: string, 96 | error: Error | unknown, 97 | context: ErrorContext 98 | ): void { 99 | const formattedContext = formatErrorContext(context); 100 | const errorInfo = error instanceof Error 101 | ? { name: error.name, message: error.message, stack: error.stack } 102 | : error; 103 | 104 | Logger.group(`Fetch错误: ${message}`, () => { 105 | Logger.error(`${formattedContext}`); 106 | 107 | if (context.additionalInfo) { 108 | Logger.error('附加信息:', context.additionalInfo); 109 | } 110 | 111 | Logger.error('错误详情:', errorInfo); 112 | 113 | // 如果有堆栈,单独记录,提高可读性 114 | if (error instanceof Error && error.stack) { 115 | Logger.error('堆栈跟踪:'); 116 | const stackLines = error.stack.split('\n'); 117 | stackLines.forEach(line => Logger.error(' ', line)); 118 | } 119 | }); 120 | } -------------------------------------------------------------------------------- /src/hook/fetch/logger/request-logger.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import Logger from '../../../logger'; 3 | 4 | /** 5 | * 打印请求信息 6 | * @param fetchContext fetch上下文 7 | */ 8 | export function logRequest(fetchContext: FetchContext): void { 9 | Logger.info( 10 | `[Fetch Request] ${fetchContext.requestContext.method} ${fetchContext.requestContext.urlContext.rawUrl}` 11 | ); 12 | 13 | // 打印请求头 14 | const headers = fetchContext.requestContext.headerContext.getAll(); 15 | if (headers.length > 0) { 16 | Logger.group('Headers', () => { 17 | headers.forEach(header => { 18 | Logger.info(`${header.name}: ${header.value}`); 19 | }); 20 | }); 21 | } 22 | 23 | // 打印请求体 24 | if (fetchContext.requestContext.bodyContext?.rawBody) { 25 | Logger.info('Body:', fetchContext.requestContext.bodyContext.rawBody); 26 | } 27 | } -------------------------------------------------------------------------------- /src/hook/fetch/logger/response-logger.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import Logger from '../../../logger'; 3 | 4 | /** 5 | * 打印响应信息 6 | * @param fetchContext fetch上下文 7 | */ 8 | export function logResponse(fetchContext: FetchContext): void { 9 | Logger.info( 10 | `[Fetch Response] ${fetchContext.responseContext.statusCode} for ${fetchContext.requestContext.urlContext.rawUrl}` 11 | ); 12 | 13 | // 打印响应头 14 | const headers = fetchContext.responseContext.headerContext.getAll(); 15 | if (headers.length > 0) { 16 | Logger.group('Headers', () => { 17 | headers.forEach(header => { 18 | Logger.info(`${header.name}: ${header.value}`); 19 | }); 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /src/hook/fetch/request/parse-request-body.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import { RequestBodyParser } from '../../../parser/request-body-parser'; 3 | import Logger from '../../../logger'; 4 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 5 | 6 | /** 7 | * 处理标准请求体 8 | * @param bodyParser 请求体解析器 9 | * @param body 请求体 10 | * @returns 处理后的请求体 11 | */ 12 | export function processStandardBody(bodyParser: RequestBodyParser, body: BodyInit): any { 13 | return body; 14 | } 15 | 16 | /** 17 | * 处理ReadableStream类型的请求体 18 | * 由于ReadableStream不能直接解析,需要特殊处理 19 | * @param bodyParser 请求体解析器 20 | * @param body ReadableStream请求体 21 | * @returns 处理后的请求体(简化为空字符串) 22 | */ 23 | export function processReadableStreamBody(bodyParser: RequestBodyParser, body: ReadableStream): any { 24 | Logger.warn('ReadableStream请求体类型不能直接解析'); 25 | return ''; // 简化处理,使用空字符串作为占位符 26 | } 27 | 28 | /** 29 | * 根据请求体类型选择合适的处理函数 30 | * @param body 请求体 31 | * @returns 处理函数 32 | */ 33 | export function selectBodyProcessor(body: BodyInit): (bodyParser: RequestBodyParser, body: any) => any { 34 | if (body instanceof ReadableStream) { 35 | return processReadableStreamBody; 36 | } 37 | return processStandardBody; 38 | } 39 | 40 | /** 41 | * 解析请求体 42 | * @param fetchContext fetch上下文 43 | * @param body 请求体 44 | */ 45 | export function parseRequestBody(fetchContext: FetchContext, body: BodyInit): void { 46 | try { 47 | const bodyParser = new RequestBodyParser(); 48 | 49 | // 根据请求体类型选择处理函数 50 | const processor = selectBodyProcessor(body); 51 | 52 | // 处理请求体 53 | const processedBody = processor(bodyParser, body); 54 | 55 | // 解析处理后的请求体 56 | fetchContext.requestContext.bodyContext = bodyParser.parse(processedBody); 57 | } catch (error) { 58 | const bodyType = body instanceof ReadableStream ? 'ReadableStream' : 59 | body instanceof Blob ? 'Blob' : 60 | body instanceof ArrayBuffer ? 'ArrayBuffer' : 61 | body instanceof FormData ? 'FormData' : 62 | body instanceof URLSearchParams ? 'URLSearchParams' : 63 | typeof body === 'string' ? 'string' : 'unknown'; 64 | 65 | const errorContext = createErrorContext( 66 | fetchContext, 67 | FetchErrorType.REQUEST_BODY_PARSING, 68 | { 69 | bodyType: bodyType, 70 | method: fetchContext.requestContext.method, 71 | url: fetchContext.requestContext.urlContext.rawUrl, 72 | bodyPreview: typeof body === 'string' ? 73 | (body.length > 100 ? body.substring(0, 100) + '...' : body) : 74 | null 75 | } 76 | ); 77 | 78 | logFetchError('解析请求体失败', error, errorContext); 79 | } 80 | } -------------------------------------------------------------------------------- /src/hook/fetch/request/parse-request-headers.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import Logger from '../../../logger'; 3 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 4 | 5 | /** 6 | * 解析Headers类型的请求头 7 | * @param fetchContext fetch上下文 8 | * @param headers Headers对象 9 | */ 10 | export function parseHeadersObject(fetchContext: FetchContext, headers: Headers): void { 11 | try { 12 | headers.forEach((value, key) => { 13 | fetchContext.requestContext.headerContext.add(key, value); 14 | }); 15 | } catch (error) { 16 | const errorContext = createErrorContext( 17 | fetchContext, 18 | FetchErrorType.REQUEST_HEADERS_PARSING, 19 | { 20 | headerType: 'Headers', 21 | headersSize: headers ? countHeaders(headers) : 0 22 | } 23 | ); 24 | 25 | logFetchError('解析Headers对象请求头失败', error, errorContext); 26 | } 27 | } 28 | 29 | /** 30 | * 解析数组类型的请求头 31 | * @param fetchContext fetch上下文 32 | * @param headers 请求头数组,形如[[key, value], ...] 33 | */ 34 | export function parseHeadersArray(fetchContext: FetchContext, headers: string[][]): void { 35 | try { 36 | for (const [key, value] of headers) { 37 | fetchContext.requestContext.headerContext.add(key, value); 38 | } 39 | } catch (error) { 40 | const errorContext = createErrorContext( 41 | fetchContext, 42 | FetchErrorType.REQUEST_HEADERS_PARSING, 43 | { 44 | headerType: 'Array', 45 | headersSize: headers ? headers.length : 0, 46 | headersPreview: headers ? JSON.stringify(headers.slice(0, 3)) : null 47 | } 48 | ); 49 | 50 | logFetchError('解析数组类型请求头失败', error, errorContext); 51 | } 52 | } 53 | 54 | /** 55 | * 解析对象类型的请求头 56 | * @param fetchContext fetch上下文 57 | * @param headers 请求头对象,形如{key: value, ...} 58 | */ 59 | export function parseHeadersRecord(fetchContext: FetchContext, headers: Record): void { 60 | try { 61 | for (const key in headers) { 62 | if (Object.prototype.hasOwnProperty.call(headers, key)) { 63 | fetchContext.requestContext.headerContext.add(key, headers[key]); 64 | } 65 | } 66 | } catch (error) { 67 | const errorContext = createErrorContext( 68 | fetchContext, 69 | FetchErrorType.REQUEST_HEADERS_PARSING, 70 | { 71 | headerType: 'Object', 72 | headersSize: headers ? Object.keys(headers).length : 0, 73 | headerKeys: headers ? Object.keys(headers).slice(0, 5) : null 74 | } 75 | ); 76 | 77 | logFetchError('解析对象类型请求头失败', error, errorContext); 78 | } 79 | } 80 | 81 | /** 82 | * 计算Headers对象中的头部数量 83 | * @param headers Headers对象 84 | * @returns 头部数量 85 | */ 86 | function countHeaders(headers: Headers): number { 87 | let count = 0; 88 | headers.forEach(() => count++); 89 | return count; 90 | } 91 | 92 | /** 93 | * 解析请求头 94 | * @param fetchContext fetch上下文 95 | * @param headers 请求头对象 96 | */ 97 | export function parseRequestHeaders(fetchContext: FetchContext, headers: HeadersInit): void { 98 | try { 99 | if (headers instanceof Headers) { 100 | parseHeadersObject(fetchContext, headers); 101 | } else if (Array.isArray(headers)) { 102 | parseHeadersArray(fetchContext, headers); 103 | } else if (typeof headers === 'object') { 104 | parseHeadersRecord(fetchContext, headers); 105 | } 106 | } catch (error) { 107 | const errorContext = createErrorContext( 108 | fetchContext, 109 | FetchErrorType.REQUEST_HEADERS_PARSING, 110 | { 111 | headerType: headers instanceof Headers ? 'Headers' : 112 | Array.isArray(headers) ? 'Array' : 113 | typeof headers === 'object' ? 'Object' : 'Unknown' 114 | } 115 | ); 116 | 117 | logFetchError('解析请求头失败', error, errorContext); 118 | } 119 | } -------------------------------------------------------------------------------- /src/hook/fetch/request/parse-request-info.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import Logger from '../../../logger'; 3 | import { parseRequestObject } from './parse-request-object'; 4 | import { parseRequestUrl } from './parse-request-url'; 5 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 6 | 7 | /** 8 | * 解析请求信息 9 | * 10 | * 根据输入类型(Request对象或URL字符串)选择相应的解析函数 11 | * 12 | * @param fetchContext fetch上下文 13 | * @param input 请求资源(Request对象或URL字符串) 14 | * @param init 请求配置 15 | */ 16 | export function parseRequestInfo(fetchContext: FetchContext, input: RequestInfo | URL, init?: RequestInit): void { 17 | try { 18 | // 根据输入类型选择不同的解析函数 19 | if (input instanceof Request) { 20 | // 如果input是Request对象 21 | parseRequestObject(fetchContext, input); 22 | } else { 23 | // 如果input是URL字符串 24 | parseRequestUrl(fetchContext, input, init); 25 | } 26 | } catch (error) { 27 | // 创建详细的错误上下文 28 | const errorContext = createErrorContext( 29 | fetchContext, 30 | FetchErrorType.REQUEST_PARSING, 31 | { 32 | inputType: input instanceof Request ? 'Request' : 'URL', 33 | hasInit: !!init 34 | } 35 | ); 36 | 37 | // 使用增强的错误日志记录 38 | logFetchError('解析请求信息失败', error, errorContext); 39 | } 40 | } -------------------------------------------------------------------------------- /src/hook/fetch/request/parse-request-object.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import { RequestBodyParser } from '../../../parser/request-body-parser'; 3 | import Logger from '../../../logger'; 4 | import { updateUrlContext } from './update-url-context'; 5 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 6 | 7 | /** 8 | * 解析请求方法 9 | * @param fetchContext fetch上下文 10 | * @param request Request对象 11 | */ 12 | export function parseRequestMethod(fetchContext: FetchContext, request: Request): void { 13 | fetchContext.requestContext.method = request.method; 14 | } 15 | 16 | /** 17 | * 解析Request对象中的请求头 18 | * @param fetchContext fetch上下文 19 | * @param request Request对象 20 | */ 21 | export function parseRequestObjectHeaders(fetchContext: FetchContext, request: Request): void { 22 | request.headers.forEach((value, key) => { 23 | fetchContext.requestContext.headerContext.add(key, value); 24 | }); 25 | } 26 | 27 | /** 28 | * 异步解析Request对象中的请求体 29 | * @param fetchContext fetch上下文 30 | * @param request Request对象 31 | */ 32 | export function parseRequestObjectBody(fetchContext: FetchContext, request: Request): void { 33 | // 只有特定方法才解析请求体 34 | if (!['POST', 'PUT', 'PATCH'].includes(request.method)) { 35 | return; 36 | } 37 | 38 | // 克隆请求以便可以多次读取 39 | request.clone().text().then(body => { 40 | try { 41 | if (body) { 42 | const bodyParser = new RequestBodyParser(); 43 | fetchContext.requestContext.bodyContext = bodyParser.parse(body); 44 | } 45 | } catch (error) { 46 | const errorContext = createErrorContext( 47 | fetchContext, 48 | FetchErrorType.REQUEST_BODY_PARSING, 49 | { 50 | method: request.method, 51 | url: request.url, 52 | bodyLength: body ? body.length : 0, 53 | bodyPreview: body ? body.substring(0, 100) + (body.length > 100 ? '...' : '') : '' 54 | } 55 | ); 56 | 57 | logFetchError('解析Request对象请求体失败', error, errorContext); 58 | } 59 | }).catch(error => { 60 | const errorContext = createErrorContext( 61 | fetchContext, 62 | FetchErrorType.REQUEST_BODY_PARSING, 63 | { 64 | method: request.method, 65 | url: request.url, 66 | error: 'Failed to read request body' 67 | } 68 | ); 69 | 70 | logFetchError('读取Request对象请求体失败', error, errorContext); 71 | }); 72 | } 73 | 74 | /** 75 | * 解析Request对象 76 | * @param fetchContext fetch上下文 77 | * @param request Request对象 78 | */ 79 | export function parseRequestObject(fetchContext: FetchContext, request: Request): void { 80 | try { 81 | // 保存原始请求 82 | fetchContext.originalRequest = request.clone(); 83 | 84 | // 解析URL 85 | const url = request.url; 86 | updateUrlContext(fetchContext, url); 87 | 88 | // 解析请求方法 89 | parseRequestMethod(fetchContext, request); 90 | 91 | // 解析请求头 92 | parseRequestObjectHeaders(fetchContext, request); 93 | 94 | // 解析请求体 95 | parseRequestObjectBody(fetchContext, request); 96 | } catch (error) { 97 | const errorContext = createErrorContext( 98 | fetchContext, 99 | FetchErrorType.REQUEST_OBJECT_PARSING, 100 | { 101 | url: request.url, 102 | method: request.method, 103 | hasBody: !!request.body 104 | } 105 | ); 106 | 107 | logFetchError('解析Request对象失败', error, errorContext); 108 | } 109 | } -------------------------------------------------------------------------------- /src/hook/fetch/request/parse-request-url.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import { formatToUrl } from '../../../utils/url-util'; 3 | import Logger from '../../../logger'; 4 | import { updateUrlContext } from './update-url-context'; 5 | import { parseRequestHeaders } from './parse-request-headers'; 6 | import { parseRequestBody } from './parse-request-body'; 7 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 8 | 9 | /** 10 | * 解析请求方法 11 | * @param fetchContext fetch上下文 12 | * @param method 请求方法 13 | */ 14 | export function parseMethodFromInit(fetchContext: FetchContext, method?: string): void { 15 | fetchContext.requestContext.method = (method || 'GET').toUpperCase(); 16 | } 17 | 18 | /** 19 | * 解析URL字符串格式的请求 20 | * @param fetchContext fetch上下文 21 | * @param urlInfo URL字符串或URL对象 22 | * @param init 请求配置 23 | */ 24 | export function parseRequestUrl(fetchContext: FetchContext, urlInfo: string | URL, init?: RequestInit): void { 25 | try { 26 | // 格式化并解析URL 27 | const url = formatToUrl(urlInfo.toString()); 28 | updateUrlContext(fetchContext, url); 29 | 30 | // 解析请求方法 31 | parseMethodFromInit(fetchContext, init?.method); 32 | 33 | // 解析请求头 34 | if (init?.headers) { 35 | parseRequestHeaders(fetchContext, init.headers); 36 | } 37 | 38 | // 解析请求体(如果有) 39 | const method = fetchContext.requestContext.method; 40 | if (init?.body && method && ['POST', 'PUT', 'PATCH'].includes(method)) { 41 | parseRequestBody(fetchContext, init.body); 42 | } 43 | } catch (error) { 44 | const errorContext = createErrorContext( 45 | fetchContext, 46 | FetchErrorType.URL_PARSING, 47 | { 48 | urlInfo: typeof urlInfo === 'string' ? urlInfo : urlInfo.toString(), 49 | hasInit: !!init, 50 | initMethod: init?.method, 51 | hasBody: !!init?.body 52 | } 53 | ); 54 | 55 | logFetchError('解析URL请求失败', error, errorContext); 56 | } 57 | } -------------------------------------------------------------------------------- /src/hook/fetch/request/update-url-context.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import { Param } from '../../../context/param'; 3 | import { ParamType } from '../../../context/param-type'; 4 | import { ContextLocation } from '../../../context/context-location'; 5 | import Logger from '../../../logger'; 6 | 7 | /** 8 | * 更新URL上下文 9 | * @param fetchContext fetch上下文 10 | * @param url URL字符串 11 | */ 12 | export function updateUrlContext(fetchContext: FetchContext, url: string): void { 13 | try { 14 | const parsedUrl = new URL(url); 15 | fetchContext.requestContext.urlContext.rawUrl = url; 16 | fetchContext.requestContext.urlContext.domain = parsedUrl.hostname; 17 | fetchContext.requestContext.urlContext.port = parsedUrl.port ? 18 | parseInt(parsedUrl.port) : 19 | (parsedUrl.protocol === 'https:' ? 443 : 80); 20 | fetchContext.requestContext.urlContext.protocol = parsedUrl.protocol.replace(':', ''); 21 | fetchContext.requestContext.urlContext.queryString = parsedUrl.search; 22 | fetchContext.requestContext.urlContext.requestPath = parsedUrl.pathname; 23 | 24 | // 处理查询参数 25 | parsedUrl.searchParams.forEach((value, key) => { 26 | const param = new Param(); 27 | param.name = key; 28 | param.value = value; 29 | param.paramType = "URL"; 30 | param.paramLocation = ContextLocation.REQUEST; 31 | fetchContext.requestContext.urlContext.params.push(param); 32 | }); 33 | } catch (error) { 34 | Logger.error('Error parsing URL:', error); 35 | } 36 | } -------------------------------------------------------------------------------- /src/hook/fetch/response/parse-response-body.ts: -------------------------------------------------------------------------------- 1 | import { RequestBodyParser } from '../../../parser/request-body-parser'; 2 | import { BodyContext } from '../../../context/body-context'; 3 | import Logger from '../../../logger'; 4 | import { FetchContext } from '../../../context/fetch-context'; 5 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 6 | 7 | /** 8 | * 解析响应体 9 | * @param response 响应对象 10 | * @param fetchContext fetch上下文,用于记录错误日志 11 | * @returns 解析后的响应体上下文 12 | */ 13 | export async function parseResponseBody(response: Response, fetchContext?: FetchContext): Promise { 14 | // 根据响应类型选择不同的解析方式 15 | const contentType = response.headers.get('content-type') || ''; 16 | 17 | try { 18 | if (contentType.includes('application/json')) { 19 | const text = await response.clone().text(); 20 | const bodyParser = new RequestBodyParser(); 21 | return bodyParser.parse(text); 22 | } else if (contentType.includes('text/')) { 23 | const text = await response.clone().text(); 24 | const bodyParser = new RequestBodyParser(); 25 | return bodyParser.parse(text); 26 | } else if (contentType.includes('multipart/form-data')) { 27 | // 处理表单数据 28 | const formData = await response.clone().formData(); 29 | const bodyParser = new RequestBodyParser(); 30 | return bodyParser.parse(formData); 31 | } else { 32 | // 处理二进制数据 33 | const blob = await response.clone().blob(); 34 | const bodyParser = new RequestBodyParser(); 35 | return bodyParser.parse(blob); 36 | } 37 | } catch (error) { 38 | const errorContext = createErrorContext( 39 | fetchContext || null, 40 | FetchErrorType.RESPONSE_BODY_PARSING, 41 | { 42 | url: fetchContext?.requestContext.urlContext.rawUrl, 43 | method: fetchContext?.requestContext.method, 44 | status: response.status, 45 | statusText: response.statusText, 46 | contentType: contentType, 47 | contentLength: response.headers.get('content-length'), 48 | responseType: response.type 49 | } 50 | ); 51 | 52 | logFetchError('解析响应体失败', error, errorContext); 53 | return null; 54 | } 55 | } -------------------------------------------------------------------------------- /src/hook/fetch/response/parse-response-headers.ts: -------------------------------------------------------------------------------- 1 | import { FetchContext } from '../../../context/fetch-context'; 2 | import Logger from '../../../logger'; 3 | import { FetchErrorType, createErrorContext, logFetchError } from '../logger/error-logger'; 4 | 5 | /** 6 | * 计算Headers对象中的头部数量 7 | * @param headers Headers对象 8 | * @returns 头部数量 9 | */ 10 | function countHeaders(headers: Headers): number { 11 | let count = 0; 12 | headers.forEach(() => count++); 13 | return count; 14 | } 15 | 16 | /** 17 | * 解析响应头信息 18 | * @param fetchContext fetch上下文 19 | * @param response 响应对象 20 | */ 21 | export function parseResponseHeaders(fetchContext: FetchContext, response: Response): void { 22 | try { 23 | response.headers.forEach((value, key) => { 24 | fetchContext.responseContext.headerContext.add(key, value); 25 | }); 26 | } catch (error) { 27 | const errorContext = createErrorContext( 28 | fetchContext, 29 | FetchErrorType.RESPONSE_HEADERS_PARSING, 30 | { 31 | url: fetchContext.requestContext.urlContext.rawUrl, 32 | method: fetchContext.requestContext.method, 33 | status: response.status, 34 | statusText: response.statusText, 35 | headersSize: countHeaders(response.headers) 36 | } 37 | ); 38 | 39 | logFetchError('解析响应头失败', error, errorContext); 40 | } 41 | } -------------------------------------------------------------------------------- /src/hook/xhr/holder.ts: -------------------------------------------------------------------------------- 1 | import { getUnsafeWindow } from "../../utils/unsafe-window"; 2 | 3 | // 保存原始的 XMLHttpRequest 对象 4 | export const ancestorXMLHttpRequestHolder: typeof XMLHttpRequest = ((getUnsafeWindow() as Window & typeof globalThis).XMLHttpRequest); -------------------------------------------------------------------------------- /src/hook/xhr/request/attribute/add-on-abort-hook.ts: -------------------------------------------------------------------------------- 1 | import { DebuggerTester } from "../../../../debuggers/debugger-tester"; 2 | import { XhrContext } from "../../../../context/xhr-context"; 3 | 4 | /** 5 | * onabort事件回调 6 | * 7 | * https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort_event 8 | * 9 | * @param xhrObject {XMLHttpRequest} 10 | * @param xhrContext {XhrContext} 11 | * @param eventCallbackFunction {((this: XMLHttpRequest, ev: ProgressEvent) => any) | null} 12 | * @returns {(this: XMLHttpRequest, ev: ProgressEvent) => any} 13 | */ 14 | export function addOnabortHook( 15 | xhrObject: XMLHttpRequest, 16 | xhrContext: XhrContext, 17 | eventCallbackFunction: ((this: XMLHttpRequest, ev: ProgressEvent) => any) | null = null 18 | ): (this: XMLHttpRequest, ev: ProgressEvent) => any { 19 | return function(this: XMLHttpRequest, ev: ProgressEvent): any { 20 | // 检查上下文是否符合条件断点 21 | DebuggerTester.test(xhrContext); 22 | 23 | // 跟进去下面这个函数就是处理响应体的代码逻辑了 24 | if (eventCallbackFunction) { 25 | return eventCallbackFunction.call(this, ev); 26 | } else { 27 | return null; 28 | } 29 | }; 30 | } 31 | 32 | /** 33 | * 收集信息 34 | * @param xhrObject {XMLHttpRequest} 35 | * @param xhrContext {XhrContext} 36 | */ 37 | function _collectInformation(_xhrObject: XMLHttpRequest, _xhrContext: XhrContext): void { 38 | // 暂时为空实现 39 | } 40 | 41 | /** 42 | * 添加 onabort 钩子 43 | * @param xhrObject {XMLHttpRequest} XHR对象 44 | * @param xhrContext {XhrContext} XHR上下文 45 | */ 46 | export function addOnAbortHook(xhrObject: XMLHttpRequest, xhrContext: XhrContext): void { 47 | const originalOnAbort = xhrObject.onabort; 48 | 49 | xhrObject.onabort = function(this: XMLHttpRequest, ev: ProgressEvent): void { 50 | // 调用原始的 onabort 处理函数 51 | if (typeof originalOnAbort === 'function') { 52 | originalOnAbort.call(this, ev); 53 | } 54 | }; 55 | } -------------------------------------------------------------------------------- /src/hook/xhr/request/method/add-abort-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../../../../context/xhr-context"; 2 | 3 | /** 4 | * 添加 abort 方法钩子 5 | * @param xhrObject {XMLHttpRequest} XHR对象 6 | * @param _xhrContext {XhrContext} XHR上下文 7 | */ 8 | export function addAbortHook(xhrObject: XMLHttpRequest, _xhrContext: XhrContext): void { 9 | const originalAbort = xhrObject.abort; 10 | 11 | xhrObject.abort = new Proxy(originalAbort, { 12 | apply(target: () => void, _thisArg: unknown, _args: []): void { 13 | target.call(xhrObject); 14 | } 15 | }); 16 | } -------------------------------------------------------------------------------- /src/hook/xhr/request/method/add-add-event-listener-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../../../../context/xhr-context"; 2 | import { ResponseContextParser } from "../../../../parser/response-context-parser"; 3 | 4 | /** 5 | * 添加 addEventListener 方法钩子 6 | * @param xhrObject {XMLHttpRequest} XHR对象 7 | * @param xhrContext {XhrContext} XHR上下文 8 | */ 9 | export function addAddEventListenerHook(xhrObject: XMLHttpRequest, xhrContext: XhrContext): void { 10 | const originalAddEventListener = xhrObject.addEventListener; 11 | 12 | xhrObject.addEventListener = new Proxy(originalAddEventListener, { 13 | apply(target: XMLHttpRequest['addEventListener'], thisArg: unknown, [eventName, eventFunction, options]: [string, EventListenerOrEventListenerObject, (boolean | AddEventListenerOptions)?]): void { 14 | // 如果是 readystatechange 事件,则需要特殊处理 15 | if (eventName === 'readystatechange') { 16 | const wrappedFunction = function(this: XMLHttpRequest, ev: Event): void { 17 | if (this.readyState === XMLHttpRequest.DONE) { 18 | // 解析响应上下文 19 | xhrContext.responseContext = new ResponseContextParser().parse(xhrObject); 20 | } 21 | 22 | // 调用原始的事件处理函数 23 | if (typeof eventFunction === 'function') { 24 | eventFunction.call(this, ev); 25 | } else if (typeof (eventFunction as EventListenerObject).handleEvent === 'function') { 26 | (eventFunction as EventListenerObject).handleEvent.call(this, ev); 27 | } 28 | }; 29 | target.call(xhrObject, eventName, wrappedFunction, options); 30 | } else { 31 | target.call(xhrObject, eventName, eventFunction, options); 32 | } 33 | } 34 | }); 35 | } -------------------------------------------------------------------------------- /src/hook/xhr/request/method/add-open-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from '../../../../context/xhr-context'; 2 | import { XhrContextParser } from '../../../../parser/xhr-context-parser'; 3 | import { formatToUrl } from '../../../../utils/url-util'; 4 | import { OpenMessage } from '../../../../message-formatter/request/method/open-message'; 5 | import { AuthContext } from '../../../../context/auth-context'; 6 | 7 | type OpenMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE' | 'CONNECT' | 'PATCH'; 8 | type OpenFunction = (method: string, url: string, async?: boolean, username?: string | null, password?: string | null) => void; 9 | 10 | /** 11 | * 为open添加代理,以便在访问的时候能够拦截得到 12 | * 13 | * https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/open 14 | * 15 | * @param xhrObject {XMLHttpRequest} 16 | * @param xhrContext {XhrContext} 17 | * @returns {void} 18 | */ 19 | export function addOpenHook(xhrObject: XMLHttpRequest, xhrContext: XhrContext): void { 20 | const originalOpen = xhrObject.open; 21 | 22 | xhrObject.open = new Proxy(originalOpen, { 23 | apply(target: OpenFunction, thisArg: unknown, [method, url, isAsync = true, username, password]: [string, string, boolean?, string?, string?]): void { 24 | collectInformation(xhrObject, xhrContext, [method, url, isAsync, username, password]); 25 | target.call(xhrObject, method, url, isAsync, username, password); 26 | } 27 | }); 28 | } 29 | 30 | /** 31 | * 收集请求上的信息 32 | * 33 | * @param xhrObject 34 | * @param xhrContext 35 | * @param argArray 36 | */ 37 | function collectInformation(xhrObject: XMLHttpRequest, xhrContext: XhrContext, [method, url, isAsync = true, username, password]: [string, string, boolean?, string?, string?]): void { 38 | try { 39 | const formattedUrl = formatToUrl(url); 40 | new XhrContextParser().updateWithUrl(xhrContext, formattedUrl); 41 | const requestContext = xhrContext.requestContext; 42 | requestContext.isAsync = isAsync; 43 | requestContext.method = method.toUpperCase() as OpenMethod; 44 | // console.log("open " + url); 45 | 46 | if (username || password) { 47 | requestContext.authContext = new AuthContext(username, password); 48 | } 49 | 50 | OpenMessage.print(xhrContext); 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | } -------------------------------------------------------------------------------- /src/hook/xhr/request/method/add-override-mimetype-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../../../../context/xhr-context"; 2 | 3 | /** 4 | * 添加 overrideMimeType 方法钩子 5 | * @param xhrObject {XMLHttpRequest} XHR对象 6 | * @param xhrContext {XhrContext} XHR上下文 7 | */ 8 | export function addOverrideMimeTypeHook(xhrObject: XMLHttpRequest, xhrContext: XhrContext): void { 9 | const originalOverrideMimeType = xhrObject.overrideMimeType; 10 | 11 | xhrObject.overrideMimeType = new Proxy(originalOverrideMimeType, { 12 | apply(target: (mimeType: string) => void, thisArg: unknown, [mimeType]: [string]): void { 13 | xhrContext.requestContext.overrideMimeType = mimeType; 14 | target.call(xhrObject, mimeType); 15 | } 16 | }); 17 | } -------------------------------------------------------------------------------- /src/hook/xhr/request/method/add-send-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from '../../../../context/xhr-context'; 2 | import { RequestBodyParser } from "../../../../parser/request-body-parser"; 3 | 4 | /** 5 | * 为send方法生成代理对象并返回 6 | * 7 | * https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/send 8 | * 9 | * @param xhrObject {XMLHttpRequest} 10 | * @param xhrContext {XhrContext} 11 | * @returns {void} 12 | */ 13 | export function addSendHook(xhrObject: XMLHttpRequest, xhrContext: XhrContext): void { 14 | const originalSend = xhrObject.send; 15 | 16 | xhrObject.send = new Proxy(originalSend, { 17 | apply(target: (body?: Document | XMLHttpRequestBodyInit | null) => void, thisArg: unknown, [body]: [Document | XMLHttpRequestBodyInit | null | undefined]): void { 18 | if (body) { 19 | const requestBodyParser = new RequestBodyParser(); 20 | xhrContext.requestContext.bodyContext = requestBodyParser.parse(body); 21 | } 22 | target.call(xhrObject, body); 23 | } 24 | }); 25 | } -------------------------------------------------------------------------------- /src/hook/xhr/request/method/add-set-request-header-hook.ts: -------------------------------------------------------------------------------- 1 | import { Header } from "../../../../context/header"; 2 | import { ContextLocation } from "../../../../context/context-location"; 3 | import { HeaderParser } from "../../../../parser/header-parser"; 4 | import { DebuggerTester } from "../../../../debuggers/debugger-tester"; 5 | import { getUserCodeLocation } from "../../../../utils/code-util"; 6 | import { XhrContext } from "../../../../context/xhr-context"; 7 | 8 | /** 9 | * 设置请求头的时候拦截一下 10 | * 11 | * https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/setRequestHeader 12 | * 13 | * @param xhrObject {XMLHttpRequest} 14 | * @param xhrContext {XhrContext} 15 | * @returns {(name: string, value: string) => void} 16 | */ 17 | export function addSetRequestHeaderHook( 18 | xhrObject: XMLHttpRequest, 19 | xhrContext: XhrContext 20 | ): (name: string, value: string) => void { 21 | const originalSetRequestHeader = xhrObject.setRequestHeader.bind(xhrObject); 22 | 23 | return function setRequestHeaderHook(name: string, value: string): void { 24 | try { 25 | // 收集设置的请求头信息 26 | collectHeaderInformation(xhrContext, name, value); 27 | 28 | // 测试断点是否命中 29 | DebuggerTester.test(xhrContext); 30 | 31 | // 调用原始的setRequestHeader方法 32 | originalSetRequestHeader(name, value); 33 | } catch (error) { 34 | console.error('Error in setRequestHeaderHook:', error); 35 | throw error; 36 | } 37 | }; 38 | } 39 | 40 | /** 41 | * 采集请求头信息 42 | * 43 | * @param xhrContext {XhrContext} 44 | * @param headerName {string} 45 | * @param headerValue {string} 46 | */ 47 | function collectHeaderInformation( 48 | xhrContext: XhrContext, 49 | headerName: string, 50 | headerValue: string 51 | ): void { 52 | try { 53 | const header = new Header(); 54 | header.name = headerName; 55 | header.value = headerValue; 56 | header.location = ContextLocation.REQUEST; 57 | header.isCustom = !new HeaderParser().isStandardHeader(headerName); 58 | 59 | xhrContext.requestContext.headerContext.add(headerName, headerValue); 60 | 61 | // 记录自定义请求头的设置位置 62 | if (header.isCustom) { 63 | const userCodeLocation = getUserCodeLocation(); 64 | console.log(`设置了自定义请求头: ${headerName}:${headerValue}, 代码位置: ${userCodeLocation}`); 65 | } 66 | } catch (error) { 67 | console.error('Error collecting header information:', error); 68 | throw error; 69 | } 70 | } -------------------------------------------------------------------------------- /src/hook/xhr/response/attribute/add-onreadystatechange-hook.ts: -------------------------------------------------------------------------------- 1 | import { ResponseContextParser } from "../../../../parser/response-context-parser"; 2 | import { XhrContext } from "../../../../context/xhr-context"; 3 | import { RsaAnalyzer } from "../../../../analyzer/encrypt/rsa/rsa-analyzer"; 4 | import { UrlEncodeAnalyzer } from "../../../../analyzer/core-encoding/url-encode-analyzer/url-encode-analyzer"; 5 | import { Base64Analyzer } from "../../../../analyzer/core-encoding/base64-analyzer/base64-analyzer"; 6 | import { HexEncodeAnalyzer } from "../../../../analyzer/core-encoding/hex-encode-analyzer/hex-encode-analyzer"; 7 | import { DebuggerTester } from "../../../../debuggers/debugger-tester"; 8 | 9 | type ReadyStateChangeHandler = ((this: XMLHttpRequest, ev: Event) => unknown) | null; 10 | 11 | /** 12 | * 在设置 onreadystatechange 的时候替换为自己的函数 13 | * 14 | * https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readystatechange_event 15 | * 16 | * @param xhrObject {XMLHttpRequest} 17 | * @param xhrContext {XhrContext} 18 | * @param callbackFunction {ReadyStateChangeHandler} 19 | * @returns {ReadyStateChangeHandler} 20 | */ 21 | export function addOnreadystatechangeHook(xhrObject: XMLHttpRequest, xhrContext: XhrContext, callbackFunction: ReadyStateChangeHandler): ReadyStateChangeHandler { 22 | // https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readystatechange_event 23 | return function(this: XMLHttpRequest, ev: Event): unknown { 24 | try { 25 | xhrContext.requestContext.readyState = xhrObject.readyState; 26 | if (xhrContext.requestContext.isRequestDone()) { 27 | xhrContext.responseContext = new ResponseContextParser().parse(xhrObject); 28 | 29 | // 分析响应中的各种编码 30 | UrlEncodeAnalyzer.analyzeResponseContext(xhrContext.responseContext); 31 | HexEncodeAnalyzer.analyzeResponseContext(xhrContext.responseContext); 32 | Base64Analyzer.analyzeResponseContext(xhrContext.responseContext); 33 | 34 | // 响应体是整个编码的: 35 | // https://jzsc.mohurd.gov.cn/data/company 36 | 37 | const rsaContext = new RsaAnalyzer().analyze(xhrContext); 38 | if (rsaContext) { 39 | // 测试断点 40 | DebuggerTester.test(xhrContext); 41 | } 42 | } 43 | } catch (e) { 44 | console.error(e); 45 | } 46 | 47 | // 跟进去下面这个函数就是处理响应体的代码逻辑了 48 | if (callbackFunction) { 49 | return callbackFunction.call(this, ev); 50 | } 51 | return undefined; 52 | }; 53 | } -------------------------------------------------------------------------------- /src/hook/xhr/response/attribute/add-visit-response-header-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from '../../../../context/xhr-context'; 2 | import { Header } from '../../../../context/header'; 3 | import { ContextLocation } from '../../../../context/context-location'; 4 | 5 | type ResponseHeaderMethod = 'getAllResponseHeaders' | 'getResponseHeader'; 6 | 7 | /** 8 | * 拦截响应头的访问 9 | * 10 | * @param xhr XMLHttpRequest实例 11 | * @param context XHR上下文 12 | * @param method 访问方法名称 13 | */ 14 | export function addVisitResponseHeaderHook( 15 | xhr: XMLHttpRequest, 16 | context: XhrContext, 17 | method: ResponseHeaderMethod 18 | ): typeof xhr.getAllResponseHeaders | typeof xhr.getResponseHeader { 19 | if (method === 'getAllResponseHeaders') { 20 | return function getAllResponseHeadersHook(): string { 21 | const headers = xhr.getAllResponseHeaders(); 22 | if (headers) { 23 | // 解析所有响应头 24 | const headerLines = headers.trim().split(/[\r\n]+/); 25 | headerLines.forEach(line => { 26 | const parts = line.split(': '); 27 | const name = parts.shift(); 28 | const value = parts.join(': '); 29 | if (name && value) { 30 | const header = new Header(); 31 | header.name = name; 32 | header.value = value; 33 | header.location = ContextLocation.RESPONSE; 34 | context.responseContext.headerContext.add(name, value); 35 | } 36 | }); 37 | } 38 | return headers; 39 | }; 40 | } else { 41 | return function getResponseHeaderHook(headerName: string): string | null { 42 | const value = xhr.getResponseHeader(headerName); 43 | if (value) { 44 | context.responseContext.headerContext.add(headerName, value); 45 | } 46 | return value; 47 | }; 48 | } 49 | } -------------------------------------------------------------------------------- /src/hook/xhr/response/attribute/add-visit-response-property-hook.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from '../../../../context/xhr-context'; 2 | 3 | type ResponseProperty = 'response' | 'responseText' | 'responseType' | 'responseURL' | 'responseXML'; 4 | 5 | /** 6 | * 拦截响应属性的访问 7 | * 8 | * @param xhr XMLHttpRequest实例 9 | * @param context XHR上下文 10 | * @param property 访问的属性名 11 | * @returns {unknown} 属性值 12 | */ 13 | export function addVisitResponsePropertyHook( 14 | xhr: XMLHttpRequest, 15 | context: XhrContext, 16 | property: ResponseProperty 17 | ): unknown { 18 | const value = xhr[property]; 19 | 20 | // 只在请求完成时记录响应内容 21 | if (xhr.readyState === XMLHttpRequest.DONE) { 22 | try { 23 | switch (property) { 24 | case 'response': 25 | case 'responseText': 26 | if (typeof value === 'string') { 27 | context.responseContext.bodyContext.rawBody = value; 28 | } else if (value instanceof ArrayBuffer) { 29 | context.responseContext.bodyContext.rawBody = new TextDecoder().decode(value); 30 | } else if (value !== null && typeof value === 'object') { 31 | context.responseContext.bodyContext.rawBody = JSON.stringify(value); 32 | } 33 | break; 34 | 35 | case 'responseType': 36 | context.responseContext.responseType = value as XMLHttpRequestResponseType; 37 | break; 38 | 39 | case 'responseURL': 40 | if (typeof value === 'string') { 41 | context.responseContext.urlContext.rawUrl = value; 42 | } 43 | break; 44 | 45 | case 'responseXML': 46 | if (value instanceof Document) { 47 | context.responseContext.bodyContext.rawBody = new XMLSerializer().serializeToString(value); 48 | } 49 | break; 50 | } 51 | } catch (error) { 52 | console.error(`Error processing ${property}:`, error); 53 | } 54 | } 55 | 56 | return value; 57 | } -------------------------------------------------------------------------------- /src/hook/xhr/xml-http-request-prototype-hook.ts: -------------------------------------------------------------------------------- 1 | import { ancestorXMLHttpRequestHolder } from './holder'; 2 | import { XMLHttpRequestObjectHook } from './xml-http-request-object-hook'; 3 | import { getUnsafeWindow } from '../../utils/unsafe-window'; 4 | 5 | interface UnsafeWindow extends Window { 6 | XMLHttpRequest: typeof XMLHttpRequest; 7 | } 8 | 9 | /** 10 | * Hook原型的方法 11 | */ 12 | export class XMLHttpRequestPrototypeHook { 13 | 14 | addHook(): void { 15 | let xMLHttpRequestHolder = ancestorXMLHttpRequestHolder; 16 | let cachedProxyXHR: typeof XMLHttpRequest | null = null; 17 | 18 | Object.defineProperty(getUnsafeWindow() as UnsafeWindow, 'XMLHttpRequest', { 19 | get: () => { 20 | if (!cachedProxyXHR) { 21 | cachedProxyXHR = new Proxy(xMLHttpRequestHolder, { 22 | // new XMLHttpRequest()的时候给替换掉返回的对象 23 | construct(_target: typeof XMLHttpRequest, _argArray: unknown[]): XMLHttpRequest { 24 | const xhrObject = new xMLHttpRequestHolder(); 25 | return new XMLHttpRequestObjectHook(xhrObject).addHook(); 26 | } 27 | }); 28 | } 29 | return cachedProxyXHR; 30 | }, 31 | set: (newValue: typeof XMLHttpRequest) => { 32 | // 缓存失效 33 | cachedProxyXHR = null; 34 | // 设置为新的值,可能会存在多层嵌套的情况 35 | xMLHttpRequestHolder = newValue; 36 | }, 37 | configurable: true 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { init } from './init/init'; 2 | import { Debugger, DebuggerConfig } from './debuggers/debugger'; 3 | import { initLogger } from './logger'; 4 | import Logger from './logger/logger'; 5 | 6 | // 初始化日志系统 7 | initLogger(); 8 | 9 | // 默认的配置 10 | const defaultConfig: DebuggerConfig = { 11 | // 是否启用调试器 12 | enable: true, 13 | 14 | // 是否启用请求 URL 过滤 15 | enableRequestUrlFilter: true, 16 | 17 | // 请求 URL 条件 18 | requestUrlCondition: null, 19 | 20 | // 请求参数名条件 21 | requestParamNameCondition: null, 22 | 23 | // 请求参数值条件 24 | requestParamValueCondition: null, 25 | 26 | // 请求头名称条件 27 | setRequestHeaderNameCondition: null, 28 | 29 | // 请求头值条件 30 | setRequestHeaderValueCondition: null, 31 | 32 | // 请求体条件 33 | requestBodyCondition: null, 34 | 35 | // 响应头名称条件 36 | getResponseHeaderNameCondition: null, 37 | 38 | // 响应头值条件 39 | getResponseHeaderValueCondition: null, 40 | 41 | // 响应体条件 42 | responseBodyCondition: null, 43 | 44 | // 是否在请求发送前进入断点 45 | enableDebuggerBeforeRequestSend: true, 46 | 47 | // 是否在请求发送后进入断点 48 | enableDebuggerAfterResponseReceive: true, 49 | 50 | // 各种操作的断点配置 51 | actionDebuggerEnable: { 52 | open: true, 53 | setRequestHeader: true, 54 | send: true, 55 | responseCallback: true, 56 | visitResponseAttribute: false 57 | } 58 | }; 59 | 60 | // 创建默认的调试器实例 61 | export const defaultDebugger = new Debugger(defaultConfig); 62 | 63 | // 导出所有需要的类和函数 64 | export { Debugger, DebuggerConfig } from './debuggers/debugger'; 65 | export { XhrContext } from './context/xhr-context'; 66 | export { FetchContext } from './context/fetch-context'; 67 | export { XMLHttpRequestPrototypeHook } from './hook/xhr/xml-http-request-prototype-hook'; 68 | export { XMLHttpRequestObjectHook } from './hook/xhr/xml-http-request-object-hook'; 69 | export { FetchHook, addFetchHook } from './hook/fetch'; 70 | export { IDGenerator } from './debuggers/id-generator'; 71 | export { Logger } from './logger'; 72 | 73 | // 初始化 74 | (async () => { 75 | try { 76 | Logger.info('正在初始化 JS-XHR-HOOK...'); 77 | init(); 78 | Logger.info('JS-XHR-HOOK 初始化完成'); 79 | } catch (error) { 80 | Logger.error('JS-XHR-HOOK 初始化失败:', error); 81 | } 82 | })(); -------------------------------------------------------------------------------- /src/init/init.ts: -------------------------------------------------------------------------------- 1 | import { XMLHttpRequestPrototypeHook } from '../hook/xhr/xml-http-request-prototype-hook'; 2 | import { registerMenu } from '../config/ui/menu'; 3 | import { addFetchHook } from '../hook/fetch'; 4 | import Logger from '../logger'; 5 | 6 | /** 7 | * 初始化资源 8 | */ 9 | export function init(): void { 10 | // 注册菜单 11 | registerMenu(); 12 | 13 | try { 14 | // 添加 XHR Hook 15 | new XMLHttpRequestPrototypeHook().addHook(); 16 | Logger.info('XHR Hook 已启用'); 17 | 18 | // 添加 Fetch Hook 19 | addFetchHook(); 20 | Logger.info('Fetch Hook 已启用'); 21 | } catch (error) { 22 | Logger.error('添加Hook失败:', error); 23 | } 24 | } -------------------------------------------------------------------------------- /src/logger/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 日志模块入口文件 3 | * 提供日志配置和常用导出 4 | */ 5 | import Logger, { LogLevel, LoggerConfig } from './logger'; 6 | 7 | /** 8 | * 初始化项目日志记录器 9 | * 可以在这里根据环境变量或其他配置来确定日志级别 10 | */ 11 | export function initLogger(): void { 12 | // 判断是否为开发环境 13 | const isDevelopment = process.env.NODE_ENV === 'development'; 14 | 15 | // 开发环境使用DEBUG级别,生产环境使用INFO级别 16 | const config: Partial = { 17 | level: isDevelopment ? LogLevel.DEBUG : LogLevel.INFO, 18 | // 可以在此处添加其他配置项 19 | }; 20 | 21 | Logger.configure(config); 22 | 23 | // 输出初始化日志 24 | Logger.info(`日志系统初始化完成,当前级别: ${LogLevel[config.level || LogLevel.INFO]}`); 25 | } 26 | 27 | // 用法示例: 28 | /* 29 | // 基本使用 30 | Logger.debug('这是一条调试信息'); // 仅在调试级别或以下显示 31 | Logger.info('这是一条信息'); 32 | Logger.warn('这是一条警告'); 33 | Logger.error('这是一条错误', new Error('发生错误')); 34 | 35 | // 对象输出 36 | const data = { user: 'admin', id: 123 }; 37 | Logger.dir(data, '用户数据'); 38 | 39 | // 分组输出 40 | Logger.group('API调用', () => { 41 | Logger.info('请求URL:', '/api/data'); 42 | Logger.info('请求方法:', 'GET'); 43 | Logger.info('响应状态:', 200); 44 | }); 45 | 46 | // 性能计时 47 | Logger.time('数据处理'); 48 | // ... 执行一些操作 49 | Logger.timeEnd('数据处理'); 50 | 51 | // 临时修改配置 52 | const originalConfig = Logger.getConfig(); 53 | Logger.configure({ level: LogLevel.DEBUG }); 54 | // ... 执行需要详细日志的操作 55 | Logger.configure(originalConfig); // 恢复原配置 56 | */ 57 | 58 | // 导出日志工具 59 | export { Logger, LogLevel, LoggerConfig }; 60 | export default Logger; -------------------------------------------------------------------------------- /src/message-formatter/base-message.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 消息格式化器基类 3 | */ 4 | export class BaseMessage { 5 | // 基类暂时为空,子类会实现具体的格式化逻辑 6 | } -------------------------------------------------------------------------------- /src/message-formatter/request/method/open-message.ts: -------------------------------------------------------------------------------- 1 | import { BaseMessage } from "../../base-message"; 2 | import { getUserCodeLocation } from "../../../utils/code-util"; 3 | import { XhrContext } from "../../../context/xhr-context"; 4 | 5 | /** 6 | * 打开连接消息格式化器 7 | */ 8 | export class OpenMessage extends BaseMessage { 9 | /** 10 | * 打印打开连接的信息 11 | * @param xhrContext {XhrContext} XHR上下文 12 | */ 13 | static print(xhrContext: XhrContext): void { 14 | console.log("打开了连接: " + xhrContext.requestContext.urlContext.rawUrl + ", 代码位置: " + getUserCodeLocation()); 15 | } 16 | } -------------------------------------------------------------------------------- /src/message-formatter/request/method/send-message.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../../../context/xhr-context"; 2 | import { BaseMessage } from "../../base-message"; 3 | 4 | /** 5 | * 发送请求消息格式化器 6 | */ 7 | export class SendMessage extends BaseMessage { 8 | /** 9 | * 当发送请求的时候,把请求上下文中的内容打印一下 10 | * @param xhrContext {XhrContext} XHR上下文 11 | */ 12 | static print(xhrContext: XhrContext): void { 13 | const requestContext = xhrContext.requestContext; 14 | const responseContext = xhrContext.responseContext; 15 | 16 | console.groupCollapsed("%c📤 Request Context", "color: #4CAF50; font-size: 14px; font-weight: bold;"); 17 | console.groupCollapsed("%c🌐 URL Context", "color: #2196F3; font-size: 12px; font-weight: bold;"); 18 | console.table({ 19 | "Raw URL": requestContext.urlContext.rawUrl, 20 | "Domain": requestContext.urlContext.domain, 21 | "Port": requestContext.urlContext.port, 22 | "Protocol": requestContext.urlContext.protocol, 23 | "Query String": requestContext.urlContext.queryString, 24 | "Request Path": requestContext.urlContext.requestPath, 25 | }); 26 | console.groupEnd(); 27 | 28 | console.groupCollapsed("%c📝 Headers", "color: #FF9800; font-size: 12px; font-weight: bold;"); 29 | console.table(requestContext.headerContext.getAll().map(header => ({ 30 | "Name": header.name, 31 | "Value": header.value, 32 | "Location": header.location.toString(), 33 | "Is Custom": header.isCustom 34 | }))); 35 | console.groupEnd(); 36 | 37 | console.groupCollapsed("%c📦 Body Context", "color: #9C27B0; font-size: 12px; font-weight: bold;"); 38 | console.table({ 39 | "Location": requestContext.bodyContext.location.toString(), 40 | "Content Type": requestContext.bodyContext.contentType.description, 41 | "Raw Body": requestContext.bodyContext.rawBody, 42 | "Raw Body Text": requestContext.bodyContext.rawBodyText, 43 | "Is URL Encoded": requestContext.bodyContext.isRawBodyUrlEncode, 44 | "Is Hex Encoded": requestContext.bodyContext.isRawBodyHex, 45 | "Is Base64 Encoded": requestContext.bodyContext.isRawBodyBase64, 46 | }); 47 | console.groupEnd(); 48 | 49 | console.groupCollapsed("%c🔐 Auth Context", "color: #F44336; font-size: 12px; font-weight: bold;"); 50 | console.table({ 51 | "Username": requestContext.authContext.username, 52 | "Password": requestContext.authContext.password 53 | }); 54 | console.groupEnd(); 55 | 56 | console.groupCollapsed("%c📥 Response Context", "color: #4CAF50; font-size: 14px; font-weight: bold;"); 57 | console.table({ 58 | "Status Code": responseContext.statusCode, 59 | "Is JSON": responseContext.isJson(), 60 | }); 61 | console.groupEnd(); 62 | 63 | console.groupEnd(); 64 | } 65 | } -------------------------------------------------------------------------------- /src/parser/array-buffer-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from '../context/body-context'; 2 | import { ContentType } from '../context/content-type'; 3 | import { ContextLocation } from '../context/context-location'; 4 | 5 | /** 6 | * ArrayBuffer数据解析器 7 | * 8 | * 用于解析二进制ArrayBuffer类型的请求体数据,将其转换为标准的BodyContext对象。 9 | * 该解析器主要用于处理原始二进制数据,比如WebSocket传输、文件读取等场景。 10 | * 解析后的结果会保存原始的ArrayBuffer对象,以便后续访问或进一步处理。 11 | * 12 | * @example 13 | * // 使用示例 14 | * const parser = new ArrayBufferBodyParser(); 15 | * const buffer = new ArrayBuffer(8); 16 | * const bodyContext = parser.parse(buffer); 17 | * console.log(bodyContext.arrayBufferData.byteLength); // 输出: 8 18 | */ 19 | export class ArrayBufferBodyParser { 20 | /** 21 | * 解析ArrayBuffer数据 22 | * 23 | * 将输入的ArrayBuffer对象解析为BodyContext对象,保存原始的二进制数据。 24 | * 该方法会设置适当的位置、内容类型,并原样保存ArrayBuffer数据。 25 | * 26 | * @param data {ArrayBuffer} ArrayBuffer对象 - 需要解析的二进制缓冲区数据 27 | * @returns {BodyContext} 解析后的上下文 - 包含ArrayBuffer数据的请求体上下文 28 | * 29 | * @example 30 | * // 基本使用 31 | * const buffer = new ArrayBuffer(16); 32 | * const bodyContext = parser.parse(buffer); 33 | * 34 | * @example 35 | * // 与TypedArray一起使用 36 | * const buffer = new ArrayBuffer(4); 37 | * const view = new Uint8Array(buffer); 38 | * view.set([1, 2, 3, 4]); 39 | * const bodyContext = parser.parse(buffer); 40 | * 41 | * @example 42 | * // 从Blob转换并解析 43 | * const blob = new Blob(['binary data']); 44 | * blob.arrayBuffer().then(buffer => { 45 | * const bodyContext = parser.parse(buffer); 46 | * // 可以处理bodyContext... 47 | * }); 48 | * 49 | * @example 50 | * // 从网络请求获取并解析二进制数据 51 | * fetch('https://example.com/binary-data') 52 | * .then(response => response.arrayBuffer()) 53 | * .then(buffer => { 54 | * const bodyContext = parser.parse(buffer); 55 | * // 处理获取到的二进制数据 56 | * }); 57 | */ 58 | parse(data: ArrayBuffer): BodyContext { 59 | const bodyContext = new BodyContext(); 60 | bodyContext.rawBody = data; 61 | bodyContext.arrayBufferData = data; 62 | bodyContext.contentType = ContentType.BINARY; 63 | bodyContext.location = ContextLocation.REQUEST; 64 | return bodyContext; 65 | } 66 | } -------------------------------------------------------------------------------- /src/parser/blob-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from '../context/body-context'; 2 | import { ContentType } from '../context/content-type'; 3 | import { ContextLocation } from '../context/context-location'; 4 | 5 | /** 6 | * Blob数据解析器 7 | * 8 | * 用于解析二进制Blob类型的请求体数据,将其转换为标准的BodyContext对象。 9 | * 该解析器主要用于处理二进制文件上传、图片传输等场景下的Blob数据。 10 | * 解析后的结果会将contentType设置为BINARY,并保存原始的Blob对象以便后续访问。 11 | * 12 | * @example 13 | * // 使用示例 14 | * const parser = new BlobBodyParser(); 15 | * const blob = new Blob(['Hello World'], {type: 'text/plain'}); 16 | * const bodyContext = parser.parse(blob); 17 | * console.log(bodyContext.contentType); // 输出: BINARY 18 | * console.log(bodyContext.blobData); // 输出: Blob对象 19 | */ 20 | export class BlobBodyParser { 21 | /** 22 | * 解析Blob数据 23 | * 24 | * 将输入的Blob对象解析为BodyContext对象,设置适当的位置、内容类型,并保存原始Blob数据。 25 | * 该方法不会尝试读取或转换Blob的内容,仅将其封装到BodyContext中,以便后续处理。 26 | * 27 | * @param data {Blob} Blob对象 - 需要解析的二进制Blob数据 28 | * @returns {BodyContext} 解析后的上下文 - 包含Blob数据的请求体上下文 29 | * 30 | * @example 31 | * // 基本使用 32 | * const fileBlob = new Blob([fileData], {type: 'application/pdf'}); 33 | * const bodyContext = parser.parse(fileBlob); 34 | * 35 | * @example 36 | * // 解析图片Blob 37 | * // 假设从canvas获取了一个图片blob 38 | * const canvas = document.createElement('canvas'); 39 | * canvas.toBlob((blob) => { 40 | * if (blob) { 41 | * const bodyContext = parser.parse(blob); 42 | * console.log(bodyContext.blobData.type); // 输出: "image/png" 43 | * } 44 | * }); 45 | * 46 | * @example 47 | * // 与FormData结合使用 48 | * // 在实际应用中,可能从FormData中提取Blob进行解析 49 | * const formData = new FormData(); 50 | * formData.append('file', new Blob(['file content'])); 51 | * // 提取并解析 52 | * const fileBlob = formData.get('file') as Blob; 53 | * const bodyContext = parser.parse(fileBlob); 54 | */ 55 | parse(data: Blob): BodyContext { 56 | const bodyContext = new BodyContext(); 57 | bodyContext.rawBody = data; 58 | bodyContext.blobData = data; 59 | bodyContext.contentType = ContentType.BINARY; 60 | bodyContext.location = ContextLocation.REQUEST; 61 | return bodyContext; 62 | } 63 | } -------------------------------------------------------------------------------- /src/parser/form-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { FormParamParser } from "./form-param-parser"; 2 | import { BodyContext } from "../context/body-context"; 3 | import { ContextLocation } from "../context/context-location"; 4 | import { ContentType } from "../context/content-type"; 5 | 6 | /** 7 | * 表单请求体解析器 8 | * 9 | * 用于解析application/x-www-form-urlencoded格式的表单数据。 10 | * 表单数据通常是由键值对组成,以&符号分隔,如"key1=value1&key2=value2"格式。 11 | * 该解析器将表单字符串转换为结构化的BodyContext对象,并提取其中的参数。 12 | * 13 | * @example 14 | * // 使用示例 15 | * const parser = new FormBodyParser(); 16 | * const formString = 'username=admin&password=123456'; 17 | * const bodyContext = parser.parse(formString); 18 | * console.log(bodyContext.params); // 输出解析后的参数列表 19 | */ 20 | export class FormBodyParser { 21 | /** 22 | * 解析表单字符串 23 | * 24 | * 将URL编码的表单字符串解析为BodyContext对象,提取其中的参数信息。 25 | * 内部使用FormParamParser来解析表单参数,并将结果存储在BodyContext的params属性中。 26 | * 27 | * @param formString {string} 表单字符串 - URL编码的表单数据,如"name=value&key=123" 28 | * @return {BodyContext} 请求体上下文 - 包含解析后的表单数据和元信息 29 | * 30 | * @example 31 | * // 基本表单解析示例 32 | * const formString = 'username=john&age=25&active=true'; 33 | * const bodyContext = parse(formString); 34 | * 35 | * // 输出结果示例 36 | * // bodyContext.rawBody = 'username=john&age=25&active=true' 37 | * // bodyContext.contentType = ContentType.FORM 38 | * // bodyContext.location = ContextLocation.REQUEST 39 | * // bodyContext.params = [ 40 | * // {name: "username", value: "john"}, 41 | * // {name: "age", value: "25"}, 42 | * // {name: "active", value: "true"} 43 | * // ] 44 | * 45 | * @example 46 | * // 特殊字符编码示例 47 | * const formString = 'search=hello%20world&filter=age%3E20'; 48 | * const bodyContext = parse(formString); 49 | * // bodyContext.params中的参数会被URL解码 50 | * // [{name: "search", value: "hello world"}, {name: "filter", value: "age>20"}] 51 | */ 52 | parse(formString: string): BodyContext { 53 | const bodyContext = new BodyContext(); 54 | bodyContext.location = ContextLocation.REQUEST; 55 | bodyContext.rawBody = formString; 56 | bodyContext.contentType = ContentType.FORM; 57 | bodyContext.params = new FormParamParser().parse(formString); 58 | return bodyContext; 59 | } 60 | } -------------------------------------------------------------------------------- /src/parser/form-data-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from '../context/body-context'; 2 | import { ContentType } from '../context/content-type'; 3 | import { ContextLocation } from '../context/context-location'; 4 | 5 | /** 6 | * FormData 类型的请求体解析器 7 | * 8 | * 用于解析FormData类型的请求体数据,将其转换为标准的BodyContext对象。 9 | * 该解析器主要用于处理表单提交、文件上传等含有FormData的请求。 10 | * 解析过程中会遍历FormData中的所有字段,将其转换为参数列表。 11 | * 12 | * @example 13 | * // 使用示例 14 | * const parser = new FormDataBodyParser(); 15 | * const formData = new FormData(); 16 | * formData.append('username', '张三'); 17 | * formData.append('file', new File(['文件内容'], 'test.txt')); 18 | * const bodyContext = parser.parse(formData); 19 | * console.log(bodyContext.contentType); // 输出: FORM 20 | * console.log(bodyContext.params); // 输出参数数组 21 | */ 22 | export class FormDataBodyParser { 23 | /** 24 | * 解析 FormData 类型的请求体 25 | * 26 | * 将FormData对象解析为BodyContext对象,遍历其中的字段并转换为参数。 27 | * 对于普通字段,会直接使用其字符串值;对于File类型的字段,则使用文件名作为值。 28 | * 解析结果的contentType会被设置为FORM,位置设置为REQUEST。 29 | * 30 | * @param formData {FormData} FormData 对象 - 需要解析的表单数据 31 | * @returns {BodyContext} 解析后的请求体上下文 - 包含所有表单字段和文件信息 32 | * 33 | * @example 34 | * // 基本使用 35 | * const formData = new FormData(); 36 | * formData.append('name', '张三'); 37 | * formData.append('age', '25'); 38 | * const bodyContext = parser.parse(formData); 39 | * 40 | * @example 41 | * // 包含文件的表单 42 | * const formData = new FormData(); 43 | * formData.append('username', 'admin'); 44 | * formData.append('avatar', new File(['图片数据'], 'avatar.png')); 45 | * const bodyContext = parser.parse(formData); 46 | * // bodyContext.params 将包含两个参数: 47 | * // {name: 'username', value: 'admin'} 48 | * // {name: 'avatar', value: 'avatar.png'} 49 | * 50 | * @example 51 | * // 与DOM表单元素结合使用 52 | * const form = document.querySelector('form'); 53 | * const formData = new FormData(form); 54 | * const bodyContext = parser.parse(formData); 55 | * // bodyContext.params 将包含表单中所有字段 56 | */ 57 | parse(formData: FormData): BodyContext { 58 | const bodyContext = new BodyContext(); 59 | bodyContext.rawBody = formData; 60 | bodyContext.contentType = ContentType.FORM; 61 | bodyContext.location = ContextLocation.REQUEST; 62 | 63 | formData.forEach((value, name) => { 64 | bodyContext.addParam(name, value instanceof File ? value.name : String(value)); 65 | }); 66 | 67 | return bodyContext; 68 | } 69 | } -------------------------------------------------------------------------------- /src/parser/form-param-parser.ts: -------------------------------------------------------------------------------- 1 | import { Param } from "../context/param"; 2 | 3 | /** 4 | * 表单参数解析器 5 | * 6 | * 用于解析URL编码的表单字符串中的参数。 7 | * 表单字符串通常遵循"name1=value1&name2=value2"的格式, 8 | * 该解析器负责将其分割为单独的键值对并进行URL解码。 9 | * 10 | * @example 11 | * // 使用示例 12 | * const parser = new FormParamParser(); 13 | * const params = parser.parse('username=admin&password=123456'); 14 | * console.log(params); // 输出: [{name: "username", value: "admin"}, {name: "password", value: "123456"}] 15 | */ 16 | export class FormParamParser { 17 | 18 | /** 19 | * 解析表单字符串 20 | * 21 | * 将表单字符串拆分为键值对,并创建对应的Param对象数组。 22 | * 该方法会自动进行URL解码,处理百分号编码的特殊字符。 23 | * 如果某个参数只有键没有值,值会被设置为空字符串。 24 | * 25 | * @param formString {string} 表单字符串 - 要解析的表单格式字符串,如"key1=value1&key2=value2" 26 | * @returns {Param[]} 参数数组 - 解析后的Param对象数组 27 | * 28 | * @example 29 | * // 基本解析示例 30 | * const formStr = 'id=123&name=John&active=true'; 31 | * const params = parse(formStr); 32 | * // 返回结果: 33 | * // [ 34 | * // {name: "id", value: "123"}, 35 | * // {name: "name", value: "John"}, 36 | * // {name: "active", value: "true"} 37 | * // ] 38 | * 39 | * @example 40 | * // 处理特殊字符示例 41 | * const formStr = 'query=hello%20world&tags=javascript%2Chtml'; 42 | * const params = parse(formStr); 43 | * // 返回结果: 44 | * // [ 45 | * // {name: "query", value: "hello world"}, 46 | * // {name: "tags", value: "javascript,html"} 47 | * // ] 48 | * 49 | * @example 50 | * // 处理空值示例 51 | * const formStr = 'key1=value1&key2=&key3'; 52 | * const params = parse(formStr); 53 | * // 返回结果: 54 | * // [ 55 | * // {name: "key1", value: "value1"}, 56 | * // {name: "key2", value: ""}, 57 | * // {name: "key3", value: ""} 58 | * // ] 59 | */ 60 | parse(formString: string): Param[] { 61 | const params: Param[] = []; 62 | const pairs = formString.split('&'); 63 | 64 | for (const pair of pairs) { 65 | const [name = '', value = ''] = pair.split('='); 66 | if (name) { 67 | const param = new Param(); 68 | param.name = decodeURIComponent(name); 69 | param.value = value ? decodeURIComponent(value) : ''; 70 | params.push(param); 71 | } 72 | } 73 | 74 | return params; 75 | } 76 | } -------------------------------------------------------------------------------- /src/parser/header-parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 请求头解析器 3 | * 4 | * 用于解析和判断HTTP请求头的类型和标准性。 5 | * 根据RFC标准规范定义的HTTP标准请求头列表,判断一个请求头是否为标准HTTP请求头。 6 | * 7 | * @example 8 | * // 使用示例 9 | * const parser = new HeaderParser(); 10 | * const isStandard = parser.isStandardHeader('Content-Type'); // 返回 true 11 | * const isCustom = parser.isStandardHeader('X-Custom-Header'); // 返回 false 12 | */ 13 | export class HeaderParser { 14 | /** 15 | * 标准HTTP请求头列表 16 | * 17 | * 包含RFC规范中定义的所有标准HTTP请求头名称(小写形式)。 18 | * 参考: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers 19 | */ 20 | private static readonly STANDARD_HEADERS = new Set([ 21 | 'accept', 22 | 'accept-charset', 23 | 'accept-encoding', 24 | 'accept-language', 25 | 'authorization', 26 | 'cache-control', 27 | 'connection', 28 | 'content-length', 29 | 'content-type', 30 | 'cookie', 31 | 'date', 32 | 'expect', 33 | 'from', 34 | 'host', 35 | 'if-match', 36 | 'if-modified-since', 37 | 'if-none-match', 38 | 'if-range', 39 | 'if-unmodified-since', 40 | 'max-forwards', 41 | 'pragma', 42 | 'proxy-authorization', 43 | 'range', 44 | 'referer', 45 | 'te', 46 | 'upgrade', 47 | 'user-agent', 48 | 'via', 49 | 'warning' 50 | ]); 51 | 52 | /** 53 | * 判断是否为标准HTTP请求头 54 | * 55 | * 检查给定的请求头名称是否在标准HTTP请求头列表中。 56 | * 该方法会将输入的请求头名称转换为小写形式进行比较。 57 | * 58 | * @param headerName {string} 请求头名称 - 要检查的HTTP请求头名称,大小写不敏感 59 | * @returns {boolean} 是否为标准请求头 - 如果是标准HTTP请求头则返回true,否则返回false 60 | * 61 | * @example 62 | * // 标准请求头判断示例 63 | * isStandardHeader('Content-Type'); // 返回 true 64 | * isStandardHeader('X-Custom-Header'); // 返回 false 65 | */ 66 | isStandardHeader(headerName: string): boolean { 67 | return HeaderParser.STANDARD_HEADERS.has(headerName.toLowerCase()); 68 | } 69 | } -------------------------------------------------------------------------------- /src/parser/json-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from "../context/body-context"; 2 | import { ContentType } from "../context/content-type"; 3 | import { ContextLocation } from "../context/context-location"; 4 | import Logger from "../logger"; 5 | 6 | /** 7 | * JSON请求体解析器 8 | * 9 | * 用于解析JSON格式的请求体或响应体数据,将其转换为结构化的BodyContext对象。 10 | * 该解析器负责从原始JSON字符串中提取数据,并设置合适的内容类型和上下文位置。 11 | * 如果解析过程中出现错误,将捕获异常并记录错误,保证解析过程不会中断。 12 | * 13 | * @example 14 | * // 使用示例 15 | * const parser = new JsonBodyParser(); 16 | * const jsonData = '{"username": "test", "age": 25}'; 17 | * const bodyContext = parser.parse(jsonData); 18 | * console.log(bodyContext.jsonData); // 输出: {username: "test", age: 25} 19 | */ 20 | export class JsonBodyParser { 21 | /** 22 | * 解析JSON字符串 23 | * 24 | * 将JSON格式的字符串解析为JavaScript对象,并封装在BodyContext对象中。 25 | * 设置合适的内容类型(ContentType.JSON)和上下文位置(默认为REQUEST)。 26 | * 27 | * @param jsonString {string} JSON字符串 - 要解析的JSON格式字符串,如'{"name":"value"}' 28 | * @return {BodyContext} 请求体上下文 - 包含解析后的JSON数据和相关元数据的上下文对象 29 | * 30 | * @example 31 | * // JSON解析示例 32 | * const jsonString = '{"id": 123, "items": ["apple", "orange"]}'; 33 | * const result = parse(jsonString); 34 | * 35 | * // 结果示例 36 | * // result.rawBody = '{"id": 123, "items": ["apple", "orange"]}' 37 | * // result.contentType = ContentType.JSON 38 | * // result.location = ContextLocation.REQUEST 39 | * // result.jsonData = {id: 123, items: ["apple", "orange"]} 40 | * 41 | * @throws {Error} 如果JSON字符串格式不正确,内部会捕获异常并记录错误,返回jsonData为null的BodyContext 42 | */ 43 | parse(jsonString: string): BodyContext { 44 | const bodyContext = new BodyContext(); 45 | bodyContext.rawBody = jsonString; 46 | bodyContext.contentType = ContentType.JSON; 47 | bodyContext.location = ContextLocation.REQUEST; 48 | try { 49 | bodyContext.jsonData = JSON.parse(jsonString); 50 | } catch (error) { 51 | Logger.error('Error parsing JSON:', error); 52 | bodyContext.jsonData = null; 53 | } 54 | return bodyContext; 55 | } 56 | } -------------------------------------------------------------------------------- /src/parser/request-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from "../context/body-context"; 2 | import { TextBodyParser } from "./text-body-parser"; 3 | import { BlobBodyParser } from "./blob-body-parser"; 4 | import { ArrayBufferBodyParser } from "./array-buffer-body-parser"; 5 | import { FormDataBodyParser } from "./form-data-body-parser"; 6 | import { UrlSearchParamsBodyParser } from "./url-search-params-body-parser"; 7 | import { GzipCodec } from "../codec/commons/gzip-codec/gzip-codec"; 8 | import { ProtoBufCodec } from "../codec/commons/protobuf-codec/protobuf-codec"; 9 | 10 | /** 11 | * 请求体解析器 12 | * 13 | * 这是一个通用的请求体解析器,能够处理多种不同类型的请求体数据。 14 | * 根据输入数据的类型,自动选择合适的子解析器进行处理,包括: 15 | * - 字符串:使用TextBodyParser 16 | * - Blob:使用BlobBodyParser 17 | * - ArrayBuffer:使用ArrayBufferBodyParser 18 | * - FormData:使用FormDataBodyParser 19 | * - URLSearchParams:使用UrlSearchParamsBodyParser 20 | * - Uint8Array:尝试进行Gzip解压和ProtoBuf解码 21 | * 22 | * 该解析器是XHR监控系统中处理请求体数据的核心组件。 23 | * 24 | * @example 25 | * // 使用示例 26 | * const parser = new RequestBodyParser(); 27 | * const jsonData = JSON.stringify({name: 'test', id: 123}); 28 | * const bodyContext = parser.parse(jsonData); 29 | * console.log(bodyContext.rawBody); // 输出原始请求体内容 30 | */ 31 | export class RequestBodyParser { 32 | 33 | /** 34 | * 解析请求体 35 | * 36 | * 根据输入数据的类型,使用不同的子解析器来处理请求体数据。 37 | * 支持多种数据类型:string, Blob, ArrayBuffer, FormData, URLSearchParams, Uint8Array等。 38 | * 对于Uint8Array类型,会尝试进行Gzip解压缩和ProtoBuf解码。 39 | * 如果输入为null或undefined,则返回空的BodyContext对象。 40 | * 41 | * @param body {Document | XMLHttpRequestBodyInit | null} 请求体数据 - 要解析的请求体,可以是多种类型 42 | * @returns {BodyContext} 请求体上下文 - 包含解析后的请求体数据和元数据 43 | * 44 | * @example 45 | * // 解析JSON字符串示例 46 | * const jsonBody = '{"id": 123, "name": "产品名称"}'; 47 | * const bodyContext = parse(jsonBody); 48 | * // bodyContext.rawBody = '{"id": 123, "name": "产品名称"}' 49 | * // 类型会被TextBodyParser设置为PLAINTEXT或JSON(根据内容判断) 50 | * 51 | * @example 52 | * // 解析表单数据示例 53 | * const formData = new FormData(); 54 | * formData.append('username', 'admin'); 55 | * formData.append('file', new File(['文件内容'], 'test.txt')); 56 | * const bodyContext = parse(formData); 57 | * // bodyContext会包含FormData的解析结果 58 | * 59 | * @example 60 | * // 解析URL参数示例 61 | * const params = new URLSearchParams('id=123&action=create'); 62 | * const bodyContext = parse(params); 63 | * // bodyContext会包含URL参数的解析结果 64 | * 65 | * @example 66 | * // 解析二进制数据示例 67 | * const binaryData = new Uint8Array([...]); 68 | * const bodyContext = parse(binaryData); 69 | * // 如果是Gzip压缩的数据,会先解压 70 | * // 如果是ProtoBuf格式,会尝试解码 71 | * // 否则会转换为文本并解析 72 | */ 73 | parse(body: Document | XMLHttpRequestBodyInit | null): BodyContext { 74 | if (!body) { 75 | return new BodyContext(); 76 | } 77 | 78 | if (typeof body === "string") { 79 | return new TextBodyParser().parse(body); 80 | } 81 | 82 | if (body instanceof Blob) { 83 | return new BlobBodyParser().parse(body); 84 | } 85 | 86 | if (body instanceof ArrayBuffer) { 87 | return new ArrayBufferBodyParser().parse(body); 88 | } 89 | 90 | if (body instanceof FormData) { 91 | return new FormDataBodyParser().parse(body); 92 | } 93 | 94 | if (body instanceof URLSearchParams) { 95 | return new UrlSearchParamsBodyParser().parse(body); 96 | } 97 | 98 | if (body instanceof Uint8Array) { 99 | let decodeData = body; 100 | 101 | // 尝试进行Gzip解压缩 102 | if (GzipCodec.isGzipCompressed(decodeData)) { 103 | decodeData = GzipCodec.decode(decodeData); 104 | } 105 | 106 | // 创建用于ProtoBuf解码的文件对象 107 | const file = { 108 | buffer: decodeData, 109 | position: 0, 110 | read: function (bytes: number): Uint8Array { 111 | const chunk = this.buffer.slice(this.position, this.position + bytes); 112 | this.position += bytes; 113 | return chunk; 114 | }, 115 | }; 116 | 117 | // 尝试ProtoBuf解码 118 | const protoBufResult = new ProtoBufCodec().decode(file); 119 | if (protoBufResult) { 120 | const bodyContext = new BodyContext(); 121 | bodyContext.rawBody = JSON.stringify(protoBufResult); 122 | return bodyContext; 123 | } 124 | 125 | // 若无法以ProtoBuf解码,则尝试转为UTF-8文本 126 | const text = new TextDecoder('utf-8').decode(decodeData); 127 | return new TextBodyParser().parse(text); 128 | } 129 | 130 | // 若无法识别类型,返回空的上下文对象 131 | return new BodyContext(); 132 | } 133 | } -------------------------------------------------------------------------------- /src/parser/request-context-parser.ts: -------------------------------------------------------------------------------- 1 | import { UrlContextParser } from "./url-context-parser"; 2 | import { RequestContext } from "../context/request-context"; 3 | 4 | /** 5 | * 请求上下文解析器 6 | * 7 | * 用于解析和管理HTTP请求的上下文信息。 8 | * 该解析器负责从URL中提取信息并更新请求上下文,是构建完整请求信息的重要环节。 9 | * 通过委托给UrlContextParser来实现URL的具体解析功能。 10 | * 11 | * @example 12 | * // 使用示例 13 | * const parser = new RequestContextParser(); 14 | * const requestContext = new RequestContext(); 15 | * parser.updateWithUrl(requestContext, 'https://api.example.com/data?id=123'); 16 | * // requestContext.urlContext会被更新,包含URL各部分的信息 17 | */ 18 | export class RequestContextParser { 19 | /** 20 | * 使用URL更新请求上下文 21 | * 22 | * 解析提供的URL并更新请求上下文中的URL相关信息。 23 | * 该方法会创建一个新的UrlContextParser实例来解析URL, 24 | * 并将解析结果设置到请求上下文的urlContext属性中。 25 | * 26 | * @param requestContext {RequestContext} 请求上下文 - 要更新的请求上下文对象 27 | * @param url {string} URL字符串 - 要解析的URL,如"https://api.example.com/users?id=123" 28 | * 29 | * @example 30 | * // URL更新示例 31 | * const requestContext = new RequestContext(); 32 | * updateWithUrl(requestContext, 'https://api.example.com/products?category=electronics&sort=price'); 33 | * 34 | * // 执行后,requestContext.urlContext将包含以下信息 35 | * // domain: "api.example.com" 36 | * // protocol: "https" 37 | * // requestPath: "/products" 38 | * // queryString: "?category=electronics&sort=price" 39 | * // params: [ 40 | * // {name: "category", value: "electronics", paramType: ParamType.URL, paramLocation: ContextLocation.REQUEST}, 41 | * // {name: "sort", value: "price", paramType: ParamType.URL, paramLocation: ContextLocation.REQUEST} 42 | * // ] 43 | */ 44 | updateWithUrl(requestContext: RequestContext, url: string): void { 45 | requestContext.urlContext = new UrlContextParser().parse(url); 46 | } 47 | } -------------------------------------------------------------------------------- /src/parser/response-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from "../context/body-context"; 2 | import { ContentType } from "../context/content-type"; 3 | import { ContextLocation } from "../context/context-location"; 4 | 5 | /** 6 | * 响应体解析器 7 | * 8 | * 用于解析XMLHttpRequest的响应体数据,根据不同的响应类型采用不同的处理策略。 9 | * 该解析器能处理多种响应类型,包括文本、JSON、Blob、ArrayBuffer和XML文档等。 10 | * 解析结果会被封装在BodyContext对象中,便于统一处理和分析。 11 | * 12 | * @example 13 | * // 使用示例 14 | * const parser = new ResponseBodyParser(); 15 | * const xhr = new XMLHttpRequest(); 16 | * // ... 发送请求并接收响应 ... 17 | * const bodyContext = parser.parse(xhr); 18 | * console.log(bodyContext.contentType); // 输出响应的内容类型 19 | * console.log(bodyContext.rawBody); // 输出原始响应体 20 | */ 21 | export class ResponseBodyParser { 22 | 23 | /** 24 | * 解析响应体 25 | * 26 | * 根据XMLHttpRequest对象的responseType属性,解析响应体数据并创建相应的BodyContext对象。 27 | * 不同的响应类型会使用不同的处理方式: 28 | * - text: 直接使用responseText 29 | * - json: 将response对象转为JSON字符串 30 | * - blob: 使用Blob对象 31 | * - arraybuffer: 使用ArrayBuffer对象 32 | * - document: 将XML文档序列化为字符串 33 | * 34 | * @param xhrObject {XMLHttpRequest} XHR对象 - 包含响应数据的XMLHttpRequest对象 35 | * @returns {BodyContext} 响应体上下文 - 包含解析后的响应体数据和元数据 36 | * 37 | * @example 38 | * // 文本响应示例 39 | * const xhr = new XMLHttpRequest(); 40 | * xhr.open('GET', 'https://api.example.com/data', false); 41 | * xhr.send(); 42 | * const bodyContext = parse(xhr); 43 | * // 如果响应是文本类型 44 | * // bodyContext.contentType = ContentType.PLAINTEXT 45 | * // bodyContext.rawBody = "响应文本内容" 46 | * // bodyContext.location = ContextLocation.RESPONSE 47 | * 48 | * @example 49 | * // JSON响应示例 50 | * const xhr = new XMLHttpRequest(); 51 | * xhr.responseType = 'json'; 52 | * xhr.open('GET', 'https://api.example.com/data.json', false); 53 | * xhr.send(); 54 | * const bodyContext = parse(xhr); 55 | * // bodyContext.contentType = ContentType.JSON 56 | * // bodyContext.rawBody = '{"id":123,"name":"示例"}' 57 | * 58 | * @example 59 | * // 二进制响应示例 60 | * const xhr = new XMLHttpRequest(); 61 | * xhr.responseType = 'arraybuffer'; 62 | * xhr.open('GET', 'https://api.example.com/image.png', false); 63 | * xhr.send(); 64 | * const bodyContext = parse(xhr); 65 | * // bodyContext.contentType = ContentType.ARRAYBUFFER 66 | * // bodyContext.rawBody = [ArrayBuffer对象] 67 | */ 68 | parse(xhrObject: XMLHttpRequest): BodyContext { 69 | const bodyContext = new BodyContext(); 70 | bodyContext.location = ContextLocation.RESPONSE; 71 | 72 | // 根据响应类型设置内容 73 | switch (xhrObject.responseType) { 74 | case 'text': 75 | case '': 76 | bodyContext.contentType = ContentType.PLAINTEXT; 77 | bodyContext.rawBody = xhrObject.responseText; 78 | break; 79 | 80 | case 'json': 81 | bodyContext.contentType = ContentType.JSON; 82 | bodyContext.rawBody = JSON.stringify(xhrObject.response); 83 | break; 84 | 85 | case 'blob': 86 | bodyContext.contentType = ContentType.BLOB; 87 | bodyContext.rawBody = xhrObject.response; 88 | break; 89 | 90 | case 'arraybuffer': 91 | bodyContext.contentType = ContentType.ARRAYBUFFER; 92 | bodyContext.rawBody = xhrObject.response; 93 | break; 94 | 95 | case 'document': 96 | bodyContext.contentType = ContentType.XML; 97 | if (xhrObject.responseXML) { 98 | bodyContext.xmlContent = xhrObject.responseXML; 99 | bodyContext.rawBody = new XMLSerializer().serializeToString(xhrObject.responseXML); 100 | } 101 | break; 102 | 103 | default: 104 | bodyContext.contentType = ContentType.UNKNOWN; 105 | if (typeof xhrObject.response === 'string' || 106 | xhrObject.response instanceof Blob || 107 | xhrObject.response instanceof ArrayBuffer || 108 | xhrObject.response instanceof FormData || 109 | xhrObject.response instanceof URLSearchParams) { 110 | bodyContext.rawBody = xhrObject.response; 111 | } else { 112 | bodyContext.rawBody = null; 113 | } 114 | break; 115 | } 116 | 117 | return bodyContext; 118 | } 119 | } -------------------------------------------------------------------------------- /src/parser/response-context-parser.ts: -------------------------------------------------------------------------------- 1 | import { ResponseContext } from "../context/response-context"; 2 | import { ResponseBodyParser } from "./response-body-parser"; 3 | 4 | /** 5 | * 响应上下文解析器 6 | * 7 | * 用于从XMLHttpRequest对象中提取响应信息,并构建结构化的ResponseContext对象。 8 | * 该解析器是XHR监控系统中处理响应数据的重要组件,负责收集响应状态码、响应类型和响应体等信息。 9 | * 内部使用ResponseBodyParser来解析具体的响应体内容。 10 | * 11 | * @example 12 | * // 使用示例 13 | * const parser = new ResponseContextParser(); 14 | * const xhr = new XMLHttpRequest(); 15 | * // ... 发送请求并接收响应 ... 16 | * const responseContext = parser.parse(xhr); 17 | * console.log(responseContext.statusCode); // 输出: 200 18 | */ 19 | export class ResponseContextParser { 20 | 21 | /** 22 | * 解析响应上下文 23 | * 24 | * 从XMLHttpRequest对象中提取响应相关信息,包括状态码、响应类型和响应体。 25 | * 该方法会创建一个新的ResponseContext对象,填充从XHR对象获取的数据。 26 | * 响应体的解析会委托给ResponseBodyParser进行处理。 27 | * 28 | * @param xhrObject {XMLHttpRequest} XHR对象 - 已完成请求的XMLHttpRequest对象 29 | * @returns {ResponseContext} 响应上下文 - 包含解析后的响应信息 30 | * 31 | * @example 32 | * // 基本解析示例 33 | * const xhr = new XMLHttpRequest(); 34 | * xhr.open('GET', 'https://api.example.com/data', false); 35 | * xhr.send(); 36 | * // 假设xhr请求已完成并收到响应 37 | * const responseContext = parse(xhr); 38 | * 39 | * // 解析结果将包含: 40 | * // responseContext.statusCode = 200 (如果请求成功) 41 | * // responseContext.responseType = "" (默认) 或 "json", "text" 等 42 | * // responseContext.bodyContext 将包含解析后的响应体 43 | * 44 | * @example 45 | * // 访问解析后的响应体示例 46 | * const responseContext = parse(xhr); 47 | * if (responseContext.bodyContext.contentType === ContentType.JSON) { 48 | * console.log(responseContext.bodyContext.jsonData); 49 | * } else if (responseContext.statusCode === 404) { 50 | * console.log("资源未找到"); 51 | * } 52 | */ 53 | parse(xhrObject: XMLHttpRequest): ResponseContext { 54 | const responseContext = new ResponseContext(); 55 | 56 | // 设置响应状态码 57 | responseContext.statusCode = xhrObject.status; 58 | 59 | // 设置响应类型 60 | responseContext.responseType = xhrObject.responseType; 61 | 62 | // 解析响应体 63 | responseContext.bodyContext = new ResponseBodyParser().parse(xhrObject); 64 | 65 | return responseContext; 66 | } 67 | } -------------------------------------------------------------------------------- /src/parser/text-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from "../context/body-context"; 2 | import { ContextLocation } from "../context/context-location"; 3 | import { ContentType } from "../context/content-type"; 4 | 5 | /** 6 | * 文本请求体解析器 7 | * 8 | * 用于解析纯文本格式的请求体数据,将其转换为标准的BodyContext对象。 9 | * 该解析器主要用于处理Content-Type为text/plain或没有指定Content-Type的请求体。 10 | * 解析后的结果会将contentType设置为PLAINTEXT,并不会尝试进一步解析文本内容。 11 | * 12 | * @example 13 | * // 使用示例 14 | * const parser = new TextBodyParser(); 15 | * const bodyContext = parser.parse("Hello World"); 16 | * console.log(bodyContext.contentType); // 输出: PLAINTEXT 17 | * console.log(bodyContext.rawBody); // 输出: "Hello World" 18 | */ 19 | export class TextBodyParser { 20 | /** 21 | * 解析文本请求体 22 | * 23 | * 将输入的文本字符串解析为BodyContext对象,设置适当的位置、内容类型和原始内容。 24 | * 该方法不会对文本内容进行任何转换或特殊处理,仅创建一个包含原始文本的上下文对象。 25 | * 26 | * @param text {string} 文本内容 - 需要解析的原始文本字符串 27 | * @return {BodyContext} 请求体上下文 - 包含解析后的文本内容信息 28 | * 29 | * @example 30 | * // 基本使用 31 | * const bodyContext = parser.parse("用户名=张三&密码=123456"); 32 | * 33 | * @example 34 | * // 解析空文本 35 | * const emptyContext = parser.parse(""); 36 | * console.log(emptyContext.rawBody); // 输出: "" 37 | * 38 | * @example 39 | * // 解析包含特殊字符的文本 40 | * const specialContext = parser.parse(""); 41 | * // 文本会被原样保存,不会进行任何转义或处理 42 | */ 43 | parse(text: string): BodyContext { 44 | const bodyContext = new BodyContext(); 45 | bodyContext.location = ContextLocation.REQUEST; 46 | bodyContext.rawBody = text; 47 | bodyContext.contentType = ContentType.PLAINTEXT; 48 | bodyContext.params = []; 49 | return bodyContext; 50 | } 51 | } -------------------------------------------------------------------------------- /src/parser/url-context-parser.ts: -------------------------------------------------------------------------------- 1 | import { UrlContext } from "../context/url-context"; 2 | import { Param } from "../context/param"; 3 | import { ParamType } from "../context/param-type"; 4 | import { ContextLocation } from "../context/context-location"; 5 | import Logger from "../logger"; 6 | 7 | /** 8 | * URL上下文解析器 9 | * 10 | * 负责解析URL字符串,提取其中的各个组成部分,如域名、协议、路径、查询参数等。 11 | * 解析结果会被封装到UrlContext对象中,便于后续处理和分析。 12 | * 该解析器是处理HTTP请求URL的基础组件,为监控和调试提供重要信息。 13 | * 14 | * @example 15 | * // 使用示例 16 | * const parser = new UrlContextParser(); 17 | * const urlContext = parser.parse('https://api.example.com/users?id=123&role=admin'); 18 | * console.log(urlContext.domain); // 输出: api.example.com 19 | * console.log(urlContext.protocol); // 输出: https 20 | * console.log(urlContext.params); // 输出: [{name: "id", value: "123"}, {name: "role", value: "admin"}] 21 | */ 22 | export class UrlContextParser { 23 | /** 24 | * 解析URL并返回一个包含提取组件的UrlContext对象 25 | * 26 | * 将URL字符串分解为各个组成部分:域名、端口、协议、路径、查询参数等, 27 | * 并将这些信息存储在UrlContext对象中。如果传入无效URL,会返回一个空的UrlContext对象。 28 | * 对于查询参数,每个参数都会被解析成Param对象并存储在params数组中。 29 | * 30 | * @param {string} url - 需要解析的URL字符串,如"https://example.com/path?query=value" 31 | * @return {UrlContext} - 包含解析结果的UrlContext对象,其中包含域名、端口、协议、路径、查询参数等信息 32 | * 33 | * @example 34 | * // 解析完整URL示例 35 | * const url = 'https://api.example.com:8443/products/123?format=json&version=2'; 36 | * const context = parse(url); 37 | * 38 | * // 输出结果示例: 39 | * // context.rawUrl = 'https://api.example.com:8443/products/123?format=json&version=2' 40 | * // context.domain = 'api.example.com' 41 | * // context.port = 8443 42 | * // context.protocol = 'https' 43 | * // context.requestPath = '/products/123' 44 | * // context.queryString = '?format=json&version=2' 45 | * // context.params = [ 46 | * // {name: "format", value: "json", paramType: ParamType.URL, paramLocation: ContextLocation.REQUEST}, 47 | * // {name: "version", value: "2", paramType: ParamType.URL, paramLocation: ContextLocation.REQUEST} 48 | * // ] 49 | * 50 | * @example 51 | * // 解析简单URL示例 52 | * const url = 'https://example.com'; 53 | * const context = parse(url); 54 | * // context.domain = 'example.com' 55 | * // context.port = 443 56 | * // context.protocol = 'https' 57 | * // context.requestPath = '/' 58 | * // context.params = [] 59 | * 60 | * @throws {Error} 如果URL格式无效,可能会抛出异常。函数内部会尝试用try-catch捕获这些异常。 61 | */ 62 | parse(url: string): UrlContext { 63 | const urlContext = new UrlContext(); 64 | 65 | // 有可能是非法的空的字符串,比如响应失败之类的 66 | if (!url) { 67 | return urlContext; 68 | } 69 | 70 | try { 71 | // 使用URL类解析URL 72 | const parsedUrl = new URL(url); 73 | 74 | urlContext.rawUrl = url; 75 | 76 | // 提取并设置域名 77 | urlContext.domain = parsedUrl.hostname; 78 | 79 | // 提取并设置端口 80 | urlContext.port = parsedUrl.port ? parseInt(parsedUrl.port) : (parsedUrl.protocol === 'https:' ? 443 : 80); 81 | 82 | // 提取并设置协议 83 | urlContext.protocol = parsedUrl.protocol.replace(':', ''); 84 | 85 | // 提取并设置查询字符串 86 | urlContext.queryString = parsedUrl.search; 87 | 88 | // 提取并设置请求路径 89 | urlContext.requestPath = parsedUrl.pathname; 90 | 91 | // 提取并设置参数 92 | urlContext.params = []; 93 | parsedUrl.searchParams.forEach((value, key) => { 94 | const param = new Param(); 95 | param.paramType = ParamType.URL; // 参数类型为 URL 96 | param.paramLocation = ContextLocation.REQUEST; // 参数位置为 REQUEST 97 | param.name = key; // 参数名称 98 | param.value = value; // 参数值 99 | urlContext.params.push(param); 100 | }); 101 | } catch (error) { 102 | Logger.error('解析URL出错:', error); 103 | // 发生错误时返回空的上下文对象 104 | } 105 | 106 | return urlContext; 107 | } 108 | } -------------------------------------------------------------------------------- /src/parser/url-search-params-body-parser.ts: -------------------------------------------------------------------------------- 1 | import { BodyContext } from '../context/body-context'; 2 | import { ContentType } from '../context/content-type'; 3 | import { ContextLocation } from '../context/context-location'; 4 | import { ParamContext } from '../context/param-context'; 5 | import { Param } from '../context/param'; 6 | import { ParamType } from '../context/param-type'; 7 | 8 | /** 9 | * URLSearchParams数据解析器 10 | * 11 | * 用于解析URLSearchParams类型的请求体数据,将其转换为标准的BodyContext对象。 12 | * 该解析器主要用于处理URL查询参数、以及使用application/x-www-form-urlencoded格式的表单提交。 13 | * 解析过程中会遍历URLSearchParams中的所有键值对,将其转换为参数列表。 14 | * 15 | * @example 16 | * // 使用示例 17 | * const parser = new UrlSearchParamsBodyParser(); 18 | * const params = new URLSearchParams('name=张三&age=25'); 19 | * const bodyContext = parser.parse(params); 20 | * console.log(bodyContext.contentType); // 输出: FORM 21 | * console.log(bodyContext.paramContext.getAll().length); // 输出: 2 22 | */ 23 | export class UrlSearchParamsBodyParser { 24 | /** 25 | * 解析URLSearchParams数据 26 | * 27 | * 将URLSearchParams对象解析为BodyContext对象,遍历其中的所有键值对并转换为参数。 28 | * 每个键值对会创建一个Param对象,设置其名称、值、类型和位置。 29 | * 解析结果的contentType会被设置为FORM,位置设置为REQUEST。 30 | * 31 | * @param data {URLSearchParams} URLSearchParams对象 - 需要解析的URL参数 32 | * @returns {BodyContext} 解析后的上下文 - 包含所有URL参数的请求体上下文 33 | * 34 | * @example 35 | * // 基本使用 36 | * const params = new URLSearchParams(); 37 | * params.append('q', '搜索关键词'); 38 | * params.append('page', '1'); 39 | * const bodyContext = parser.parse(params); 40 | * 41 | * @example 42 | * // 从URL中提取并解析参数 43 | * const url = new URL('https://example.com/search?q=关键词&page=1'); 44 | * const bodyContext = parser.parse(url.searchParams); 45 | * 46 | * @example 47 | * // 从字符串创建并解析 48 | * const paramString = 'user=admin&token=abc123'; 49 | * const params = new URLSearchParams(paramString); 50 | * const bodyContext = parser.parse(params); 51 | * 52 | * @example 53 | * // 处理重复的参数名 54 | * const params = new URLSearchParams(); 55 | * params.append('tag', '技术'); 56 | * params.append('tag', '编程'); 57 | * const bodyContext = parser.parse(params); 58 | * // 注意:URLSearchParams允许同名参数,但在解析时后面的值会覆盖前面的值 59 | */ 60 | parse(data: URLSearchParams): BodyContext { 61 | const bodyContext = new BodyContext(); 62 | const paramContext = new ParamContext(); 63 | 64 | // 遍历URLSearchParams中的所有参数 65 | data.forEach((value, key) => { 66 | const param = new Param(); 67 | param.name = key; 68 | param.value = value; 69 | param.paramType = ParamType.FORM; 70 | param.paramLocation = ContextLocation.REQUEST; 71 | paramContext.add(param); 72 | }); 73 | 74 | bodyContext.rawBody = data; 75 | bodyContext.paramContext = paramContext; 76 | bodyContext.contentType = ContentType.FORM; 77 | bodyContext.location = ContextLocation.REQUEST; 78 | return bodyContext; 79 | } 80 | } -------------------------------------------------------------------------------- /src/parser/xhr-context-parser.ts: -------------------------------------------------------------------------------- 1 | import { XhrContext } from "../context/xhr-context"; 2 | import { RequestContextParser } from "./request-context-parser"; 3 | 4 | /** 5 | * XHR上下文解析器 6 | * 7 | * 用于处理和解析XMLHttpRequest(XHR)的上下文信息。 8 | * 该解析器主要负责更新XHR上下文中的URL相关信息,通过委托给RequestContextParser来完成实际的URL解析工作。 9 | * 这是XHR拦截和监控系统的重要组成部分,用于提取和组织请求的详细信息。 10 | * 11 | * @example 12 | * // 使用示例 13 | * const parser = new XhrContextParser(); 14 | * const xhrContext = new XhrContext(); 15 | * parser.updateWithUrl(xhrContext, 'https://api.example.com/users?id=123'); 16 | * // xhrContext.requestContext.urlContext 会被更新,包含URL的各个组成部分 17 | */ 18 | export class XhrContextParser { 19 | /** 20 | * 使用URL更新XHR上下文 21 | * 22 | * 通过提供的URL更新XHR上下文中的请求信息。 23 | * 该方法会解析URL并提取域名、路径、查询参数等信息,并将其存储在XHR上下文的请求上下文中。 24 | * 内部委托给RequestContextParser的updateWithUrl方法来完成实际的解析工作。 25 | * 26 | * @param xhrContext {XhrContext} XHR上下文 - 要更新的XMLHttpRequest上下文对象 27 | * @param url {string} URL字符串 - 要解析的完整URL,如"https://example.com/api?key=value" 28 | * 29 | * @example 30 | * // 更新上下文示例 31 | * const xhrContext = new XhrContext(); 32 | * updateWithUrl(xhrContext, 'https://api.example.com/data?sort=desc&page=1'); 33 | * 34 | * // 更新后的xhrContext.requestContext.urlContext可能包含 35 | * // domain: "api.example.com" 36 | * // protocol: "https" 37 | * // requestPath: "/data" 38 | * // queryString: "sort=desc&page=1" 39 | * // params: [{name: "sort", value: "desc"}, {name: "page", value: "1"}] 40 | */ 41 | updateWithUrl(xhrContext: XhrContext, url: string): void { 42 | new RequestContextParser().updateWithUrl(xhrContext.requestContext, url); 43 | } 44 | } -------------------------------------------------------------------------------- /src/storage/IStorage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储接口定义 3 | * 所有存储实现必须遵循此接口 4 | * 5 | * 该接口提供了统一的异步存储操作方法,包括:获取、设置、删除和清空数据 6 | * 7 | * 基本使用示例: 8 | * ```typescript 9 | * // 假设storage是IStorage的实现实例 10 | * // 存储简单数据 11 | * await storage.set('username', '张三'); 12 | * 13 | * // 存储复杂数据 14 | * await storage.set('userInfo', { 15 | * id: 1001, 16 | * name: '张三', 17 | * age: 28, 18 | * roles: ['admin', 'editor'] 19 | * }); 20 | * 21 | * // 获取数据并指定类型 22 | * const username = await storage.get('username'); 23 | * const userInfo = await storage.get<{id: number, name: string, age: number, roles: string[]}>('userInfo'); 24 | * 25 | * // 删除数据 26 | * await storage.remove('username'); 27 | * 28 | * // 清空所有数据 29 | * await storage.clear(); 30 | * ``` 31 | */ 32 | export interface IStorage { 33 | /** 34 | * 获取存储的值 35 | * @param key 键名 36 | * @returns 返回Promise,解析为存储的值或undefined(如果不存在) 37 | * 38 | * @example 39 | * // 获取简单类型 40 | * const name = await storage.get('name'); 41 | * 42 | * // 获取对象类型 43 | * const config = await storage.get<{theme: string, fontSize: number}>('config'); 44 | */ 45 | get(key: string): Promise; 46 | 47 | /** 48 | * 设置存储的值 49 | * @param key 键名 50 | * @param value 要存储的值(支持各种类型,包括对象、数组等) 51 | * @returns 返回Promise,表示操作完成 52 | * 53 | * @example 54 | * // 存储字符串 55 | * await storage.set('name', '李四'); 56 | * 57 | * // 存储对象 58 | * await storage.set('settings', {darkMode: true, fontSize: 16}); 59 | */ 60 | set(key: string, value: T): Promise; 61 | 62 | /** 63 | * 删除某个键值对 64 | * @param key 要删除的键名 65 | * @returns 返回Promise,表示操作完成 66 | * 67 | * @example 68 | * await storage.remove('tempData'); 69 | */ 70 | remove(key: string): Promise; 71 | 72 | /** 73 | * 清空所有存储数据 74 | * @returns 返回Promise,表示操作完成 75 | * 76 | * @example 77 | * // 慎用!这将删除所有存储的数据 78 | * await storage.clear(); 79 | */ 80 | clear(): Promise; 81 | } -------------------------------------------------------------------------------- /src/storage/StorageFactory.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from './IStorage'; 2 | import { TampermonkeyStorage } from './TampermonkeyStorage'; 3 | import { IndexedDBStorage } from './IndexedDBStorage'; 4 | 5 | /** 6 | * 存储类型 7 | * - tampermonkey: 使用油猴脚本API存储 8 | * - indexeddb: 使用浏览器的IndexedDB存储 9 | */ 10 | export type StorageType = 'tampermonkey' | 'indexeddb'; 11 | 12 | /** 13 | * 存储工厂类 14 | * 用于创建不同类型的存储实例 15 | * 16 | * 设计意图: 17 | * - 统一的接口:所有存储实现都遵循IStorage接口 18 | * - 灵活选择:可以根据运行环境选择合适的存储方式 19 | * - 易于扩展:可以方便地添加新的存储实现 20 | * 21 | * 使用示例: 22 | * ```typescript 23 | * import { StorageFactory, StorageType } from './storage'; 24 | * 25 | * // 创建油猴存储 26 | * const tmStorage = StorageFactory.createStorage('tampermonkey'); 27 | * await tmStorage.set('siteConfig', { autoPagination: true }); 28 | * 29 | * // 创建IndexedDB存储 30 | * const dbStorage = StorageFactory.createStorage('indexeddb'); 31 | * await dbStorage.set('downloadHistory', [ 32 | * { id: 1, filename: '文档1.pdf', date: '2023-06-15' }, 33 | * { id: 2, filename: '文档2.pdf', date: '2023-06-16' } 34 | * ]); 35 | * 36 | * // 在配置中选择存储方式 37 | * function initializeStorage(config: {storageType: StorageType}) { 38 | * const storage = StorageFactory.createStorage(config.storageType); 39 | * return storage; 40 | * } 41 | * 42 | * // 应用中使用统一接口,不关心底层实现 43 | * async function saveUserData(storage: IStorage, userData: any) { 44 | * await storage.set('userData', userData); 45 | * console.log('用户数据已保存'); 46 | * } 47 | * ``` 48 | */ 49 | export class StorageFactory { 50 | /** 51 | * 创建一个存储实例 52 | * @param type 存储类型,默认为tampermonkey 53 | * @returns 存储实例,实现了IStorage接口 54 | * 55 | * @example 56 | * // 油猴环境使用 57 | * const storage = StorageFactory.createStorage('tampermonkey'); 58 | * 59 | * // 普通网页环境使用 60 | * const storage = StorageFactory.createStorage('indexeddb'); 61 | * 62 | * // 根据环境自动选择 63 | * const storage = StorageFactory.createStorage( 64 | * typeof GM_getValue !== 'undefined' ? 'tampermonkey' : 'indexeddb' 65 | * ); 66 | */ 67 | static createStorage(type: StorageType = 'tampermonkey'): IStorage { 68 | switch (type.toLowerCase() as StorageType) { 69 | case 'tampermonkey': 70 | return new TampermonkeyStorage(); 71 | case 'indexeddb': 72 | return new IndexedDBStorage(); 73 | default: 74 | throw new Error(`不支持的存储类型: ${type}`); 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * 默认存储实例,使用油猴存储 81 | * 可以直接导入使用,无需创建实例 82 | * 83 | * @example 84 | * import storage from './storage'; 85 | * 86 | * // 直接使用默认实例 87 | * await storage.set('key', 'value'); 88 | * const value = await storage.get('key'); 89 | */ 90 | const defaultStorage = StorageFactory.createStorage('tampermonkey'); 91 | 92 | // 导出默认存储实例 93 | export default defaultStorage; -------------------------------------------------------------------------------- /src/storage/TampermonkeyStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorage } from './IStorage'; 2 | 3 | /** 4 | * 油猴脚本API类型定义 5 | * 这些API仅在油猴脚本环境中可用 6 | * 详细文档: https://www.tampermonkey.net/documentation.php 7 | */ 8 | declare function GM_getValue(key: string, defaultValue?: T): T; 9 | declare function GM_setValue(key: string, value: T): void; 10 | declare function GM_deleteValue(key: string): void; 11 | declare function GM_listValues(): string[]; 12 | 13 | /** 14 | * 油猴脚本存储实现 15 | * 使用GM_*系列API进行数据存储 16 | * 17 | * 适用场景: 18 | * - 在油猴脚本(Tampermonkey/Greasemonkey)环境中运行的代码 19 | * - 需要持久化存储数据 20 | * - 需要在不同页面间共享数据 21 | * 22 | * 存储限制: 23 | * - 受油猴脚本实现限制,一般支持存储JSON可序列化的数据 24 | * - 存储容量取决于浏览器实现,一般不会特别大 25 | * 26 | * 使用示例: 27 | * ```typescript 28 | * import { TampermonkeyStorage } from './storage'; 29 | * 30 | * // 创建油猴存储实例 31 | * const tmStorage = new TampermonkeyStorage(); 32 | * 33 | * // 存储数据 34 | * await tmStorage.set('配置', { 35 | * 主题: '暗色', 36 | * 字体大小: 16, 37 | * 自动保存: true, 38 | * 最近使用: ['工具1', '工具2', '工具3'] 39 | * }); 40 | * 41 | * // 读取数据 42 | * const config = await tmStorage.get<{ 43 | * 主题: string, 44 | * 字体大小: number, 45 | * 自动保存: boolean, 46 | * 最近使用: string[] 47 | * }>('配置'); 48 | * 49 | * console.log(`当前主题: ${config?.主题}`); 50 | * console.log(`字体大小: ${config?.字体大小}px`); 51 | * 52 | * // 删除数据 53 | * await tmStorage.remove('临时数据'); 54 | * ``` 55 | */ 56 | export class TampermonkeyStorage implements IStorage { 57 | /** 58 | * 获取存储的值 59 | * @param key 键名 60 | * @returns 返回Promise,解析为存储的值或undefined(如果不存在) 61 | * 62 | * 注意:由于GM_getValue是同步API,这里包装成异步是为了统一接口 63 | * 64 | * @example 65 | * const username = await storage.get('username'); 66 | * if (username) { 67 | * console.log(`当前用户: ${username}`); 68 | * } 69 | */ 70 | async get(key: string): Promise { 71 | return await GM_getValue(key); 72 | } 73 | 74 | /** 75 | * 设置存储的值 76 | * @param key 键名 77 | * @param value 要存储的值(任何可被JSON序列化的值) 78 | * @returns 返回Promise,表示操作完成 79 | * 80 | * @example 81 | * // 存储用户偏好设置 82 | * await storage.set('userPreferences', { 83 | * language: 'zh-CN', 84 | * notifications: true, 85 | * theme: 'dark' 86 | * }); 87 | */ 88 | async set(key: string, value: T): Promise { 89 | await GM_setValue(key, value); 90 | } 91 | 92 | /** 93 | * 删除某个键值对 94 | * @param key 要删除的键名 95 | * @returns 返回Promise,表示操作完成 96 | * 97 | * @example 98 | * // 删除登录状态 99 | * await storage.remove('loginStatus'); 100 | */ 101 | async remove(key: string): Promise { 102 | await GM_deleteValue(key); 103 | } 104 | 105 | /** 106 | * 清空所有存储数据 107 | * 这会删除油猴脚本存储的所有数据,请谨慎使用! 108 | * @returns 返回Promise,表示操作完成 109 | * 110 | * @example 111 | * // 重置所有设置 112 | * await storage.clear(); 113 | * console.log('所有设置已重置'); 114 | */ 115 | async clear(): Promise { 116 | const keys = await GM_listValues(); 117 | for (const key of keys) { 118 | await this.remove(key); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/storage/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储模块 3 | * 4 | * 该模块提供了一套统一的存储接口和多种存储实现,支持: 5 | * - 油猴脚本存储 (TampermonkeyStorage) 6 | * - IndexedDB存储 (IndexedDBStorage) 7 | * 8 | * 基本使用: 9 | * ```typescript 10 | * // 方式1: 使用默认存储实例(油猴存储) 11 | * import storage from '../storage'; 12 | * 13 | * await storage.set('key', 'value'); 14 | * const value = await storage.get('key'); 15 | * 16 | * // 方式2: 使用特定存储类型 17 | * import { StorageFactory } from '../storage'; 18 | * 19 | * // 创建IndexedDB存储 20 | * const dbStorage = StorageFactory.createStorage('indexeddb'); 21 | * await dbStorage.set('cacheData', { id: 1, name: '缓存数据' }); 22 | * 23 | * // 方式3: 直接创建存储实例 24 | * import { IndexedDBStorage } from '../storage'; 25 | * 26 | * const customDB = new IndexedDBStorage('customDB', 'customStore'); 27 | * await customDB.set('appState', { isLoggedIn: true, lastAccess: new Date() }); 28 | * ``` 29 | */ 30 | 31 | // 导出接口 32 | export { IStorage } from './IStorage'; 33 | 34 | // 导出存储实现 35 | export { TampermonkeyStorage } from './TampermonkeyStorage'; 36 | export { IndexedDBStorage } from './IndexedDBStorage'; 37 | 38 | // 导出工厂和类型 39 | export { StorageFactory, StorageType } from './StorageFactory'; 40 | 41 | // 导出默认存储实例 42 | import defaultStorage from './StorageFactory'; 43 | export default defaultStorage; -------------------------------------------------------------------------------- /src/utils/code-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成一个随机的函数名。 3 | * 4 | * @param length - 函数名的长度,默认为 8。 5 | * @return 返回生成的随机函数名。 6 | */ 7 | export function generateRandomFunctionName(length: number = 8): string { 8 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 9 | let result = ''; 10 | for (let i = 0; i < length; i++) { 11 | result += chars.charAt(Math.floor(Math.random() * chars.length)); 12 | } 13 | return result; 14 | } 15 | 16 | /** 17 | * 获取函数的方法体代码。 18 | * 19 | * @param fn - 要提取方法体的函数。 20 | * @return 返回函数的方法体代码。 21 | * @throws {TypeError} - 如果传入的参数不是函数,则抛出 TypeError。 22 | */ 23 | export function getFunctionBody(fn: Function): string { 24 | if (typeof fn !== 'function') { 25 | throw new TypeError('Expected a function'); 26 | } 27 | 28 | // 获取函数的完整代码 29 | const fullCode = fn.toString(); 30 | 31 | // 提取方法体的代码 32 | const bodyStart = fullCode.indexOf('{') + 1; // 找到方法体的开始位置 33 | const bodyEnd = fullCode.lastIndexOf('}'); // 找到方法体的结束位置 34 | 35 | // 提取并返回方法体的代码 36 | return fullCode.slice(bodyStart, bodyEnd).trim(); 37 | } 38 | 39 | /** 40 | * 获取函数的参数名。 41 | * 42 | * @param fn - 要提取参数名的函数。 43 | * @return 返回函数的参数名数组。如果没有参数,则返回空数组。 44 | * @throws {TypeError} - 如果传入的参数不是函数,则抛出 TypeError。 45 | */ 46 | export function getParameterNames(fn: Function): string[] { 47 | if (typeof fn !== 'function') { 48 | throw new TypeError('Expected a function'); 49 | } 50 | 51 | // 将函数转换为字符串 52 | const fnStr = fn.toString(); 53 | 54 | // 提取参数部分 55 | const paramPattern = /\(([^)]*)\)/; 56 | const match = fnStr.match(paramPattern); 57 | 58 | if (!match || !match[1]) { 59 | return []; // 如果没有参数,返回空数组 60 | } 61 | 62 | // 提取参数名称 63 | const paramNames = match[1] 64 | .split(',') 65 | .map(param => param.trim()) 66 | .filter(param => param); // 过滤掉空字符串 67 | 68 | return paramNames; 69 | } 70 | 71 | const tampermonkeyChromeExtensionId = "dhdgffkkebhmkfjojejmpbldmpobfkfo"; 72 | 73 | /** 74 | * 获取用户代码的位置,用户代码的定义就是调用栈里从用户的代码进入到插件代码的第一行代码 75 | * @returns {string | null} 用户代码位置 76 | */ 77 | export function getUserCodeLocation(): string | null { 78 | try { 79 | // 把调用栈一个栈帧一个栈帧的弹掉 80 | const stack = new Error().stack?.split("\n") || []; 81 | let index = stack.length - 1; 82 | while (index >= 0) { 83 | const frame = stack[index]; 84 | if (frame.includes(tampermonkeyChromeExtensionId) && index < stack.length) { 85 | return stack[index + 1].trim(); 86 | } else { 87 | index--; 88 | } 89 | } 90 | return null; 91 | } catch (e) { 92 | console.error('Error getting code location:', e); 93 | return null; 94 | } 95 | } -------------------------------------------------------------------------------- /src/utils/color-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成一个随机的 RGB 颜色值。 3 | * 4 | * @return {string} - 返回一个表示 RGB 颜色的字符串,格式为 `rgb(r, g, b)`,其中 `r`、`g`、`b` 分别是 0 到 255 之间的随机整数。 5 | */ 6 | function getRandomRGBColor(): string { 7 | const r: number = Math.floor(Math.random() * 256); // 红色分量 (0-255) 8 | const g: number = Math.floor(Math.random() * 256); // 绿色分量 (0-255) 9 | const b: number = Math.floor(Math.random() * 256); // 蓝色分量 (0-255) 10 | return `rgb(${r}, ${g}, ${b})`; 11 | } 12 | 13 | export { 14 | getRandomRGBColor 15 | }; -------------------------------------------------------------------------------- /src/utils/id-generator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ID生成工具 3 | * 提供多种ID生成方式 4 | */ 5 | 6 | /** 7 | * 用于生成有序的自增ID 8 | */ 9 | export class IDGenerator { 10 | private idPrefix: string; 11 | private next: number; 12 | 13 | /** 14 | * 构造函数 15 | * @param idPrefix 可以指定一个可选的 ID 前缀,如果指定的话生成的每个 ID 都有相同的前缀,未指定的话则 ID 无前缀只是一个自增的数字 16 | */ 17 | constructor(idPrefix = "") { 18 | this.idPrefix = idPrefix; 19 | this.next = 1; 20 | } 21 | 22 | /** 23 | * 返回下一个自增ID 24 | * @returns {string | number} 自增ID,格式为 "前缀-00000001" 或纯数字 25 | */ 26 | nextID(): string | number { 27 | const next = this.next; 28 | this.next++; 29 | if (this.idPrefix) { 30 | return `${this.idPrefix}-${next.toString().padStart(8, "0")}`; 31 | } else { 32 | return next; 33 | } 34 | } 35 | } 36 | 37 | /** 38 | * 生成一个随机 ID,格式类似于 UUID。 39 | * 40 | * @returns {string} - 返回一个随机生成的 ID,格式为 "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"。 41 | */ 42 | export function randomId(): string { 43 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c: string): string { 44 | // 生成一个 0 到 15 的随机整数 45 | const r: number = Math.random() * 16 | 0; 46 | // 如果字符是 'x',则直接使用随机数;如果是 'y',则根据规则生成特定值 47 | const v: number = c === 'x' ? r : (r & 0x3 | 0x8); 48 | // 将结果转换为十六进制字符串 49 | return v.toString(16); 50 | }); 51 | } -------------------------------------------------------------------------------- /src/utils/log-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 生成一个格式化字符串数组,用于 `console.log` 的多样式输出。 3 | * 4 | * 该函数接受一个包含消息和样式的数组,生成一个格式化字符串,用于将消息和样式一一对应。 5 | * 例如,输入 `["Hello", "color: red", "World", "color: blue"]`,输出 `"%c%s%c%s"`。 6 | * 7 | * @param {Array} messageAndStyleArray - 包含消息和样式的数组。消息和样式交替出现,例如 `["消息1", "样式1", "消息2", "样式2"]`。 8 | * @return {string} - 返回一个格式化字符串,例如 `"%c%s%c%s"`,用于 `console.log` 的多样式输出。 9 | */ 10 | function genFormatArray(messageAndStyleArray: string[]): string { 11 | const formatArray: string[] = []; 12 | // 遍历数组,每两个元素(消息和样式)生成一个 "%c%s" 13 | for (let i = 0, end = messageAndStyleArray.length / 2; i < end; i++) { 14 | formatArray.push("%c%s"); 15 | } 16 | // 将数组拼接成一个字符串 17 | return formatArray.join(""); 18 | } 19 | 20 | export { 21 | genFormatArray 22 | }; -------------------------------------------------------------------------------- /src/utils/string-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 将字符串重复指定次数并拼接成一个新字符串。 3 | * 4 | * @param {string} s - 需要重复的字符串。 5 | * @param {number} times - 重复的次数。 6 | * @return {string} - 返回重复拼接后的字符串。 7 | */ 8 | function repeat(s: string, times: number): string { 9 | const msgs: string[] = []; 10 | for (let i = 0; i < times; i++) { 11 | msgs.push(s); 12 | } 13 | return msgs.join(""); 14 | } 15 | 16 | /** 17 | * 将字符串填充到指定长度。如果字符串长度不足,则在末尾填充空格。 18 | * 19 | * @param {string} s - 需要填充的字符串。 20 | * @param {number} length - 目标长度。 21 | * @return {string} - 返回填充后的字符串。如果字符串长度已经大于或等于目标长度,则返回原字符串。 22 | */ 23 | function fillToLength(s: string, length: number): string { 24 | if (s.length >= length) { 25 | return s; 26 | } else { 27 | return s + repeat(" ", length - s.length); 28 | } 29 | } 30 | 31 | export { 32 | repeat, 33 | fillToLength, 34 | }; -------------------------------------------------------------------------------- /src/utils/unsafe-window.ts: -------------------------------------------------------------------------------- 1 | declare const unsafeWindow: Window; 2 | 3 | /** 4 | * 获取 `unsafeWindow` 对象,用于在油猴脚本中访问或修改全局 `window` 对象。 5 | * 6 | * 油猴脚本默认运行在一个沙箱环境中,无法直接修改全局 `window` 对象。 7 | * 通过 `unsafeWindow` 可以绕过沙箱机制,直接访问或修改全局 `window` 对象。 8 | * 使用此方法封装 `unsafeWindow` 的调用,便于后续追踪哪些地方使用了 `unsafeWindow`。 9 | * 10 | * @see https://wiki.greasespot.net/UnsafeWindow - 油猴脚本中 `unsafeWindow` 的官方文档。 11 | * @returns {Window} - 返回全局的 `unsafeWindow` 对象。 12 | */ 13 | export function getUnsafeWindow(): Window { 14 | // 在油猴脚本中,通常可以通过 unsafeWindow 访问原始的窗口对象 15 | // 但在其他环境中,我们直接返回 window 对象 16 | return (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window); 17 | } -------------------------------------------------------------------------------- /src/utils/url-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取 URL 的基础路径(包括协议、域名和路径部分,但不包括文件名)。 3 | * 4 | * @param urlString - 完整的 URL 字符串,例如 "https://example.com/path/to/page.html"。 5 | * @return 返回 URL 的基础路径,例如 "https://example.com/path/to/"。 6 | */ 7 | export function urlBasePath(urlString: string): string { 8 | const url = new URL(urlString); 9 | // 获取基础路径(包括协议、域名和路径部分,但不包括文件名) 10 | const basePath = `${url.origin}${url.pathname.substring(0, url.pathname.lastIndexOf("/") + 1)}`; 11 | // console.log(basePath); // 输出: https://example.com/path/to/ 12 | return basePath; 13 | } 14 | 15 | /** 16 | * 将URL格式化为字符串。支持多种格式: 17 | * 1. URL对象 18 | * 2. 完整的URL(以 "http://" 或 "https://" 开头) 19 | * 3. CDN URL(以 "//" 开头) 20 | * 4. 相对路径(以 "./" 开头) 21 | * 5. 省略域名的路径(以 "/" 开头) 22 | * 6. 其他相对路径 23 | * 24 | * @param url - URL对象、字符串或其他值 25 | * @returns 格式化后的URL字符串 26 | */ 27 | export function formatToUrl(url: string | URL | unknown): string { 28 | // 如果是URL对象,直接转换为字符串 29 | if (url instanceof URL) { 30 | return url.toString(); 31 | } 32 | 33 | // 强制将输入转换为字符串 34 | const srcString = String(url); 35 | 36 | // 如果已经是完整的URL,直接返回 37 | if (srcString.startsWith("http://") || srcString.startsWith("https://")) { 38 | return srcString; 39 | } 40 | 41 | // 处理CDN URL(以"//"开头) 42 | if (srcString.startsWith("//")) { 43 | return "https:" + srcString; 44 | } 45 | 46 | // 处理相对路径(以"./"开头) 47 | if (srcString.startsWith("./")) { 48 | return urlBasePath(window.location.href) + srcString.substring(2); 49 | } 50 | 51 | // 处理省略域名的路径(以"/"开头) 52 | if (srcString.startsWith("/")) { 53 | return window.location.origin + srcString; 54 | } 55 | 56 | // 处理其他相对路径 57 | return window.location.origin + "/" + srcString; 58 | } -------------------------------------------------------------------------------- /test_data/intercetor-test.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 拦截器设置请求参数测试通过的网站: 5 | https://sou.zhaopin.com/?jl=530&kw=kkk 6 | 7 | 8 | -------------------------------------------------------------------------------- /test_data/test-header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 测试请求头的debugger效果 6 | 7 | 8 | 9 | 10 | 11 | 23 | 29 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /test_data/test-hook-xhr-hook.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 40 | 41 | -------------------------------------------------------------------------------- /test_data/test-object-defineProperty.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 | 10 | 11 | 42 | 43 | -------------------------------------------------------------------------------- /test_data/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 43 | 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "lib": ["es6", "dom"], 6 | "allowJs": true, 7 | "outDir": "dist", 8 | "rootDir": "src", 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "skipLibCheck": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "dist"] 20 | } -------------------------------------------------------------------------------- /userscript-headers.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name ${name} 3 | // @namespace ${repository} 4 | // @version ${version} 5 | // @description ${description} 6 | // @document ${document} 7 | // @author ${author} 8 | // @match *://*/* 9 | // @run-at document-start 10 | // ==/UserScript== -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const webpackPackageJson = require('./package.json'); 4 | const fs = require('fs'); 5 | 6 | module.exports = { 7 | entry: { 8 | index: './src/index.ts' 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, 'dist'), 12 | }, 13 | resolve: { 14 | extensions: ['.ts', '.js'] 15 | }, 16 | optimization: { 17 | minimize: false, 18 | concatenateModules: false 19 | }, 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | __VERSION__: JSON.stringify(webpackPackageJson.version || '0.4') 23 | }), 24 | new webpack.BannerPlugin({ 25 | entryOnly: true, 26 | raw: true, 27 | banner: () => { 28 | let userscriptHeaders = fs.readFileSync('./userscript-headers.js').toString('utf-8'); 29 | userscriptHeaders = userscriptHeaders.replaceAll('${name}', webpackPackageJson['name'] || ''); 30 | userscriptHeaders = userscriptHeaders.replaceAll('${namespace}', webpackPackageJson['namespace'] || ''); 31 | userscriptHeaders = userscriptHeaders.replaceAll('${version}', webpackPackageJson['version'] || ''); 32 | userscriptHeaders = userscriptHeaders.replaceAll('${description}', webpackPackageJson['description'] || ''); 33 | userscriptHeaders = userscriptHeaders.replaceAll('${document}', webpackPackageJson['document'] || ''); 34 | userscriptHeaders = userscriptHeaders.replaceAll('${author}', webpackPackageJson['author'] || ''); 35 | userscriptHeaders = userscriptHeaders.replaceAll('${repository}', webpackPackageJson['repository'] || ''); 36 | 37 | const bannerFilePath = './banner.txt'; 38 | if (fs.existsSync(bannerFilePath)) { 39 | let banner = fs.readFileSync(bannerFilePath).toString('utf-8'); 40 | banner = banner.replaceAll('${name}', webpackPackageJson['name'] || ''); 41 | banner = banner.replaceAll('${namespace}', webpackPackageJson['namespace'] || ''); 42 | banner = banner.replaceAll('${version}', webpackPackageJson['version'] || ''); 43 | banner = banner.replaceAll('${description}', webpackPackageJson['description'] || ''); 44 | banner = banner.replaceAll('${document}', webpackPackageJson['document'] || ''); 45 | banner = banner.replaceAll('${author}', webpackPackageJson['author'] || ''); 46 | banner = banner.replaceAll('${repository}', webpackPackageJson['repository'] || ''); 47 | userscriptHeaders += '\n' + banner.split('\n').join('\n// ') + '\n'; 48 | } 49 | 50 | return userscriptHeaders; 51 | } 52 | }), 53 | ], 54 | module: { 55 | rules: [ 56 | { 57 | test: /\.ts$/, 58 | use: 'ts-loader', 59 | exclude: /node_modules/ 60 | }, 61 | { 62 | test: /\.css$/, 63 | use: ['style-loader', 'css-loader'] 64 | }, 65 | { 66 | test: /\.(png|svg|jpg|gif)$/, 67 | use: ['file-loader'] 68 | }, 69 | { 70 | test: /\.(woff|woff2|eot|ttf|otf)$/, 71 | use: ['file-loader'] 72 | } 73 | ] 74 | } 75 | }; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const { version } = require('./package.json'); 5 | 6 | module.exports = { 7 | mode: 'production', 8 | entry: './src/index.ts', 9 | output: { 10 | filename: 'xhr-monitor-debugger-hook.user.js', 11 | path: path.resolve(__dirname, 'dist'), 12 | library: { 13 | name: 'xhrMonitorDebuggerHook', 14 | type: 'umd', 15 | export: 'default' 16 | } 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.tsx?$/, 22 | use: 'ts-loader', 23 | exclude: /node_modules/ 24 | } 25 | ] 26 | }, 27 | resolve: { 28 | extensions: ['.tsx', '.ts', '.js'] 29 | }, 30 | optimization: { 31 | minimize: true, 32 | minimizer: [ 33 | new TerserPlugin({ 34 | terserOptions: { 35 | format: { 36 | comments: /@userscript/i 37 | } 38 | }, 39 | extractComments: false 40 | }) 41 | ] 42 | }, 43 | plugins: [ 44 | new webpack.BannerPlugin({ 45 | banner: `// ==UserScript== 46 | // @name XHR Monitor Debugger Hook 47 | // @namespace https://github.com/JSREI/js-xhr-monitor-debugger-hook 48 | // @version ${version} 49 | // @description A powerful XHR request monitor and debugger hook 50 | // @author JSREI 51 | // @match *://*/* 52 | // @grant none 53 | // @license MIT 54 | // ==/UserScript==`, 55 | raw: true 56 | }) 57 | ] 58 | }; -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const common = require("./webpack.common.js"); 2 | const {merge} = require("webpack-merge"); 3 | const webpack = require("webpack"); 4 | const path = require("path"); 5 | 6 | module.exports = env => { 7 | return merge(common, { 8 | mode: "none", 9 | //开启这个可以在开发环境中调试代码 10 | devtool: "source-map", 11 | devServer: { 12 | static: false, 13 | allowedHosts: "all", 14 | compress: false, 15 | port: 10086, 16 | hot: true 17 | }, 18 | plugins: [ 19 | //这两个插件用于开发环境时,修改保存代码之后页面自动刷新 20 | new webpack.HotModuleReplacementPlugin() 21 | ] 22 | }); 23 | } -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const common = require("./webpack.common.js"); 2 | const {merge} = require("webpack-merge"); 3 | 4 | module.exports = merge(common, { 5 | //开启这个可以在生产环境中调试代码 6 | devtool: "source-map", 7 | // 两个原因: 8 | // 1. 在油猴商店上架的脚本不允许混淆和压缩 9 | // 2. 不混淆不压缩保留注释能够稍微增加一点用户的信任度 10 | mode: "none", 11 | output: { 12 | filename: 'js-xhr-hook.user.js', 13 | library: { 14 | name: 'xhrMonitorDebuggerHook', 15 | type: 'umd', 16 | umdNamedDefine: true 17 | } 18 | }, 19 | optimization: { 20 | minimize: false, 21 | concatenateModules: false, 22 | moduleIds: 'named', 23 | chunkIds: 'named' 24 | }, 25 | externals: { 26 | } 27 | }); --------------------------------------------------------------------------------