├── .gitignore ├── Interceptor.test.json ├── LICENSE ├── README.func.md ├── README.md ├── README.zh.md ├── extension-zips.js ├── media ├── diagram.png └── operation.gif ├── package.json ├── packages ├── code-editor │ ├── .babelrc │ ├── babel.config.js │ ├── examples │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ ├── packages │ │ ├── index.js │ │ ├── index.vue │ │ └── useEditor.js │ ├── public │ │ └── index.html │ └── vue.config.js ├── compatibility │ ├── package.json │ ├── src │ │ ├── index.ts │ │ └── types.ts │ ├── tsconfig.json │ └── types │ │ ├── index.d.ts │ │ └── types.d.ts ├── json-editor │ ├── .babelrc │ ├── babel.config.js │ ├── examples │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ ├── packages │ │ ├── index.js │ │ └── index.vue │ ├── public │ │ └── index.html │ └── vue.config.js ├── proxy-lib │ ├── package.json │ ├── src │ │ ├── common.ts │ │ ├── createFetch.ts │ │ ├── createXHR.ts │ │ ├── index.ts │ │ ├── overrideFunc.ts │ │ ├── redirectFetch.ts │ │ ├── redirectUrlFunc.ts │ │ ├── redirectXHR.ts │ │ └── types.ts │ ├── test │ │ ├── base.test.html │ │ ├── base.test.interval.html │ │ ├── redirect.html │ │ └── test.xhr.post.get.html │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── types │ │ ├── common.d.ts │ │ ├── createFetch.d.ts │ │ ├── createXHR.d.ts │ │ ├── index.d.ts │ │ ├── overrideFunc.d.ts │ │ ├── redirectFetch.d.ts │ │ ├── redirectUrlFunc.d.ts │ │ ├── redirectXHR.d.ts │ │ └── types.d.ts │ └── vite.config.ts ├── shared-utils │ ├── package.json │ ├── src │ │ ├── action.ts │ │ ├── consts.ts │ │ ├── env.ts │ │ ├── index.ts │ │ ├── notice.ts │ │ └── storage.ts │ ├── tsconfig.json │ └── types │ │ ├── action.d.ts │ │ ├── consts.d.ts │ │ ├── env.d.ts │ │ ├── index.d.ts │ │ ├── notice.d.ts │ │ └── storage.d.ts ├── shell-chrome │ ├── icons │ │ ├── 128.png │ │ ├── 128g.png │ │ ├── 48.png │ │ └── 48g.png │ ├── manifest.json │ ├── package.json │ ├── src │ │ ├── consts.ts │ │ ├── content.ts │ │ ├── document.ts │ │ └── service-worker │ │ │ ├── badge.ts │ │ │ ├── event.ts │ │ │ ├── index.ts │ │ │ ├── init.ts │ │ │ ├── notice.ts │ │ │ └── panel.ts │ ├── tsconfig.json │ └── webpack.config.js └── vue-panels │ ├── .vscode │ └── settings.json │ ├── LICENSE │ ├── README.md │ ├── babel.config.js │ ├── jsconfig.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ ├── src │ ├── App.vue │ ├── common │ │ ├── element-plugin.js │ │ ├── index.js │ │ ├── notice.js │ │ └── store.js │ ├── index.scss │ ├── lang │ │ ├── en.js │ │ ├── fr.js │ │ ├── index.js │ │ ├── ireland.js │ │ ├── ja.js │ │ ├── ko.js │ │ ├── ru.js │ │ ├── zh_CN.js │ │ └── zh_TW.js │ ├── main.js │ └── views │ │ ├── index.vue │ │ ├── interceptor │ │ ├── jsonEdit.vue │ │ ├── modal.vue │ │ ├── table.vue │ │ └── tag.vue │ │ └── redirector │ │ ├── modal.vue │ │ └── table.vue │ └── vue.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── release.js └── zip └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | # .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | lib 25 | build 26 | *.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gj 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.func.md: -------------------------------------------------------------------------------- 1 | ## 函数式响应 2 | 3 | **函数式响应**面向特殊需求。本质上也是通过代码片段方式注入到浏览器中,相比固定格式它可以更灵活,支持 Promise。你甚至可以直接在函数体内使用**XHR/Fetch**去发起独立请求。但需要注意的是,所有逻辑只能写在**setup**函数中。 4 | 5 | ![diagram](/media/diagram.png) 6 | 7 | ### 参数定义 8 | 9 | #### req 10 | 11 | 请求参数 12 | 13 | ```ts 14 | type Req = { 15 | url: string; 16 | method: string; 17 | body?: any; 18 | }; 19 | ``` 20 | 21 | #### res 22 | 23 | 响应参数 24 | 25 | ```ts 26 | type Res = { 27 | status: string; // 系统响应状态码 28 | customStatus: string; // 自定义状态码 29 | response: any; 30 | }; 31 | ``` 32 | 33 | #### next 34 | 35 | ```ts 36 | type Next = { 37 | override?: string; 38 | status?: string | number; 39 | }; 40 | ``` 41 | 42 | ### 示例一 43 | 44 | ```js 45 | function setup(req, res, next) { 46 | const RoleType = { 47 | ADMIN: 1, 48 | NORMAL: 2, 49 | }; 50 | 51 | let override; 52 | // 需要区分XHR于Fetch请求对应不同数据格式的response 53 | const json = JSON.parse(res.response); 54 | if (json.user.roleType === RoleType.NORMAL) { 55 | json.user.roleType = RoleType.ADMIN; 56 | override = json; 57 | } 58 | 59 | next({ override }); 60 | } 61 | ``` 62 | 63 | ### 示例二 64 | 65 | ```js 66 | async function setup(req, res, next) { 67 | let override; 68 | if (res.status !== 200) { 69 | // 独立接口请求 70 | const res = await fetch("https://v1.hitokoto.cn/"); 71 | const json = await res.json(); 72 | override = { 73 | code: 100, 74 | data: json, 75 | }; 76 | // 日志输出 77 | console.log("override:", override); 78 | } else override = res.response; 79 | 80 | next({ 81 | // 如果不传override,则返回"" 82 | override, 83 | // 如果不传status,则返回customStatus 84 | status: 200, 85 | }); 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Ajax Proxy

4 | 5 |
6 | 7 |

A browser plugin based on Chromium kernel · Tools for Developers · For the modification of web-side response

8 | 9 |

10 | 11 | GitHub 12 | 13 | 14 | chrome web store 15 | 16 | 17 | chrome rating 18 | 19 | 20 | 21 | edge addons 22 | 23 | 24 | edge users 25 | 26 |

27 | 28 |
29 | 30 | 31 | English | [中文](README.zh.md) 32 | 33 | 34 |
35 | 36 | ## When to use 37 | 38 | - When actual data fails to meet expected results, mocking data is needed. 39 | - In development or production stages, verification of exceptional scenarios or edge cases is necessary. 40 | - The frequent changes in interface data hinder the development process. 41 | - When a certain interface returns a 404 error. 42 | 43 | ## Installation 44 | 45 | [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/ajax-proxy/iladajdkobpmadjfpeginhngnneaoefi) 46 | 47 | [Google Chrome](https://chrome.google.com/webstore/detail/ajax-proxy/jbikjaejnjfbloojafllmdiknfndgljo) 48 | 49 | ## Examples 50 | 51 | Video: [https://www.youtube.com/watch?v=F\_\_7LXBqnvQ&list=PLniy0-3-8-V1ZhsmG6\_\_HdOJBAschGWSt](https://www.youtube.com/watch?v=F__7LXBqnvQ&list=PLniy0-3-8-V1ZhsmG6__HdOJBAschGWSt) 52 | 53 | 54 | 55 | ![operation.gif](media/operation.gif) 56 | 57 | ![zhihu](https://github.com/g0ngjie/ajax-proxy/wiki/images/zhihu-ajaxproxy.png) 58 | 59 | ## FAQ 60 | 61 | 1. Data interception does not work 62 | - You can switch between `interceptor` and `redirector` to solve the Ajax referencing problem 63 | ![issues_checked](https://github.com/g0ngjie/ajax-proxy/wiki/images/issues_checked.png) 64 | - You can select the `Network` section in Developer Tools and disable caching by checking ☑️ 65 | ![issues_disabled_cache](https://github.com/g0ngjie/ajax-proxy/wiki/images/issues_disabled_cache.png) 66 | 2. [Function-based response explanation](README.func.md) 67 | 68 | ## Monorepo 69 | 70 | | Package | Description | 71 | | ------------------------------------------------- | ---------------------------------------- | 72 | | [@proxy/compatibility](./packages/compatibility/) | Old Data Compatibility Library | 73 | | [@proxy/lib](./packages/proxy-lib/) | Manipulating the Ajax Core Logic Library | 74 | | [@proxy/shared-utils](./packages/shared-utils/) | Public Class Libraries | 75 | | [@proxy/shell-chrome](./packages/shell-chrome/) | Browser Extension Library | 76 | | [@proxy/vue-panels](./packages/vue-panels/) | Application Operator Panel | 77 | 78 | ## Use of source code 79 | 80 | 1. Download the corresponding version of [Source code](https://github.com/g0ngjie/ajax-proxy/releases) and unzip it 81 | 2. Open `Developer mode` in your browser 82 | 3. Then just load the unpacked folder 83 | 84 | ## Testing 85 | 86 | You can test it directly in [Swagger](https://petstore.swagger.io/) 87 | 88 | ## ⭐ Stargazers 89 | 90 | Thanks for your support! 91 | 92 | [![Stargazers for ajax-proxy](https://reporoster.com/stars/g0ngjie/ajax-proxy)](https://github.com/g0ngjie/ajax-proxy/stargazers) 93 | 94 | ## License 95 | 96 | Ajax Proxy is [MIT licensed](LICENSE). 97 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

Ajax Proxy

4 | 5 |
6 | 7 |

一款基于Chromium内核的浏览器插件 · 面向开发者的工具 · 用于Web端接口数据的修改

8 | 9 |

10 | 11 | GitHub 12 | 13 | 14 | chrome web store 15 | 16 | 17 | chrome rating 18 | 19 | 20 | 21 | edge addons 22 | 23 | 24 | edge users 25 | 26 |

27 | 28 |
29 | 30 | 31 | [English](README.md) | 中文 32 | 33 | 34 |
35 | 36 | ## 适用场景 37 | 38 | - 当实际数据无法达到预期结果时,需要进行 Mock 数据处理。 39 | - 在开发或生产阶段,需要验证异常场景或临界值。 40 | - 开发阶段数据频繁变更,导致页面无法正常联调时。 41 | - 某个接口返回 404 错误时。 42 | 43 | 44 | 45 | ## 安装 46 | 47 | [Edge 版本](https://microsoftedge.microsoft.com/addons/detail/ajax-proxy/iladajdkobpmadjfpeginhngnneaoefi) 48 | 49 | [Chrome 版本](https://chrome.google.com/webstore/detail/ajax-proxy/jbikjaejnjfbloojafllmdiknfndgljo) 50 | 51 | ## 效果展示 52 | 53 | 视频: [https://www.bilibili.com/video/BV1KB4y1j7Gm](https://www.bilibili.com/video/BV1KB4y1j7Gm) 54 | 55 | 56 | 57 | ![operation.gif](media/operation.gif) 58 | 59 | ![zhihu](https://github.com/g0ngjie/ajax-proxy/wiki/images/zhihu-ajaxproxy.png) 60 | 61 | ## 视频介绍 62 | 63 | - [请求方式拦截介绍](https://www.bilibili.com/video/BV1eW4y1H76x/?vd_source=47f2c439d1dcdfef3c5f144bf04b0c01) 64 | - [状态码规则与正则的使用介绍](https://www.bilibili.com/video/BV1LV4y1V7e2/?vd_source=47f2c439d1dcdfef3c5f144bf04b0c01) 65 | 66 | ## 常见问题 67 | 68 | 1. 数据拦截不起作用 69 | - 方法 1: 可以通过切换 `interceptor` 和 `redirector` 来刷新 Ajax 引用问题 70 | ![issues_checked](https://github.com/g0ngjie/ajax-proxy/wiki/images/issues_checked.png) 71 | - 方法 2: 可以在开发者工具中的`网络(network)`里面,通过 ☑️ 禁用缓存 72 | ![issues_disabled_cache](https://github.com/g0ngjie/ajax-proxy/wiki/images/issues_disabled_cache.png) 73 | 2. [函数方式响应说明](README.func.md) 74 | 75 | ## Monorepo 76 | 77 | | Package | Description | 78 | | ------------------------------------------------- | -------------------- | 79 | | [@proxy/compatibility](./packages/compatibility/) | 老数据兼容库 | 80 | | [@proxy/lib](./packages/proxy-lib/) | 操作 Ajax 核心逻辑库 | 81 | | [@proxy/shared-utils](./packages/shared-utils/) | 通用类库 | 82 | | [@proxy/shell-chrome](./packages/shell-chrome/) | 浏览器扩展库 | 83 | | [@proxy/vue-panels](./packages/vue-panels/) | 应用操作面板 | 84 | 85 | ## 源码使用方式 86 | 87 | 1. 下载对应版本的 [Source code](https://github.com/g0ngjie/ajax-proxy/releases) 解压 88 | 2. 浏览器打开 `开发者模式` 89 | 3. 加载解压后的文件夹 90 | 91 | ## 测试用例 92 | 93 | 下载 [Interceptor.test.json](https://github.com/g0ngjie/ajax-proxy/blob/master/Interceptor.test.json) 94 | 95 | 分别使用在 [掘金](https://juejin.cn/) 首页、[百度翻译](https://fanyi.baidu.com/) 两个网站测试 96 | 97 | 1. 掘金: 直接在首页查看效果; 98 | 2. 百度翻译: 随便翻译点内容即可; 99 | 3. 也可以直接在[Swagger](https://petstore.swagger.io/)中测试 100 | 101 | ## ⭐ Stargazers 102 | 103 | 感谢支持! 104 | 105 | [![Stargazers for ajax-proxy](https://reporoster.com/stars/g0ngjie/ajax-proxy)](https://github.com/g0ngjie/ajax-proxy/stargazers) 106 | 107 | ## License 108 | 109 | Ajax Proxy is [MIT licensed](LICENSE). 110 | -------------------------------------------------------------------------------- /extension-zips.js: -------------------------------------------------------------------------------- 1 | // require modules 2 | const fs = require('fs') 3 | const path = require('path') 4 | const archiver = require('archiver') 5 | const IS_CI = !!(process.env.CIRCLECI || process.env.GITHUB_ACTIONS) 6 | const ProgressBar = require('progress') 7 | const readDirGlob = require('readdir-glob') 8 | const PKG = require("./package.json") 9 | 10 | const INCLUDE_GLOBS = [ 11 | 'icons/**', 12 | 'panels/**', 13 | 'content.js', 14 | 'document.js', 15 | 'manifest.json', 16 | 'service_worker.js', 17 | ] 18 | // SKIP_GLOBS makes glob searches more efficient 19 | const SKIP_DIR_GLOBS = ['node_modules', 'src'] 20 | 21 | function bytesToSize(bytes) { 22 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'] 23 | if (bytes === 0) return '0 Byte' 24 | const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))) 25 | return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i] 26 | } 27 | 28 | (async () => { 29 | await writeZip(`ajax-proxy-${PKG.version}.zip`, 'shell-chrome/build') 30 | 31 | async function writeZip(fileName, packageDir) { 32 | // create a file to stream archive data to. 33 | const output = fs.createWriteStream(path.join(__dirname, 'zip', fileName)) 34 | const archive = archiver('zip', { 35 | zlib: { level: 9 }, // Sets the compression level. 36 | }) 37 | 38 | if (!IS_CI) { 39 | const status = { 40 | total: 0, 41 | cFile: '...', 42 | cSize: '0 Bytes', 43 | tBytes: 0, 44 | tSize: '0 Bytes', 45 | } 46 | 47 | async function parseFileStats() { 48 | return new Promise((resolve, reject) => { 49 | const globber = readDirGlob(path.join('packages', packageDir), 50 | { pattern: INCLUDE_GLOBS, skip: SKIP_DIR_GLOBS, mark: true, stat: true }) 51 | globber.on('match', match => { 52 | if (!match.stat.isDirectory()) status.total++ 53 | }) 54 | globber.on('error', err => { 55 | reject(err) 56 | }) 57 | globber.on('end', () => { 58 | resolve() 59 | }) 60 | }) 61 | } 62 | await parseFileStats().catch(err => { 63 | console.error(err) 64 | process.exit(1) 65 | }) 66 | 67 | const bar = new ProgressBar(`${fileName} @ :tSize [:bar] :current/:total :percent +:cFile@:cSize`, { 68 | width: 18, 69 | incomplete: ' ', 70 | total: status.total, 71 | }) 72 | bar.tick(0, status) 73 | 74 | archive.on('entry', (entry) => { 75 | if (!entry.stats.isDirectory()) { 76 | const n = entry.name 77 | status.written++ 78 | status.cFile = n.length > 14 79 | ? '...' + n.slice(n.length - 11) 80 | : n 81 | status.cSize = bytesToSize(entry.stats.size) 82 | status.tBytes += entry.stats.size 83 | status.tSize = bytesToSize(status.tBytes) 84 | bar.tick(1, status) 85 | } 86 | }) 87 | } 88 | 89 | const end = new Promise((resolve) => { 90 | // listen for all archive data to be written 91 | // 'close' event is fired only when a file descriptor is involved 92 | output.on('close', () => { 93 | if (archive.pointer() < 1000) { 94 | console.warn(`Zip file (${fileName}) is only ${archive.pointer()} bytes`) 95 | } 96 | resolve() 97 | }) 98 | }) 99 | 100 | // This event is fired when the data source is drained no matter what was the data source. 101 | // It is not part of this library but rather from the NodeJS Stream API. 102 | // @see: https://nodejs.org/api/stream.html#stream_event_end 103 | output.on('end', function () { 104 | 'nothing' 105 | }) 106 | 107 | // good practice to catch warnings (ie stat failures and other non-blocking errors) 108 | archive.on('warning', function (err) { 109 | if (err.code !== 'ENOENT') { 110 | // throw error 111 | console.error(err) 112 | process.exit(1) 113 | } 114 | }) 115 | 116 | // good practice to catch this error explicitly 117 | archive.on('error', function (err) { 118 | console.error(err) 119 | process.exit(1) 120 | }) 121 | 122 | // pipe archive data to the file 123 | archive.pipe(output) 124 | 125 | INCLUDE_GLOBS.forEach(glob => { 126 | // append files from a glob pattern 127 | archive.glob(glob, { cwd: path.join('packages', packageDir), skip: SKIP_DIR_GLOBS }) 128 | }) 129 | 130 | // finalize the archive (ie we are done appending files but streams have to finish yet) 131 | // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand 132 | archive.finalize() 133 | 134 | await end 135 | } 136 | })() 137 | -------------------------------------------------------------------------------- /media/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/media/diagram.png -------------------------------------------------------------------------------- /media/operation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/media/operation.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ajax-proxy", 3 | "private": true, 4 | "description": "Modify your Ajax response to test", 5 | "version": "2.2.10", 6 | "scripts": { 7 | "dev": "pnpm -C ./packages/vue-panels serve", 8 | "watch": "run-p watch:lib watch:chrome", 9 | "watch:lib": "pnpm -C ./packages/proxy-lib watch", 10 | "watch:chrome": "pnpm -C ./packages/shell-chrome watch", 11 | "build": "run-s build:all pkg", 12 | "build:all": "pnpm -F \"./packages/**\" build", 13 | "build:lib": "pnpm -C ./packages/proxy-lib build", 14 | "build:utils": "pnpm -C ./packages/shared-utils build", 15 | "build:chrome": "pnpm -C ./packages/shell-chrome build", 16 | "build:comp": "pnpm -C ./packages/compatibility build", 17 | "build:panels": "pnpm -C ./packages/vue-panels build", 18 | "clean:build": "rm -rf ./packages/proxy-lib/lib ./packages/shell-chrome/build ./packages/shared-utils/lib", 19 | "release": "node release.js", 20 | "pkg": "cp -r ./packages/vue-panels/dist ./packages/shell-chrome/build/panels", 21 | "zip": "node ./extension-zips.js", 22 | "preinstall": "npx only-allow pnpm" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/g0ngjie/ajax-proxy.git" 27 | }, 28 | "engines": { 29 | "node": ">=16.16.0" 30 | }, 31 | "keywords": [], 32 | "author": { 33 | "name": "Gj", 34 | "email": "gongjie0422@163.com" 35 | }, 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/g0ngjie/ajax-proxy/issues" 39 | }, 40 | "homepage": "https://github.com/g0ngjie/ajax-proxy#readme", 41 | "devDependencies": { 42 | "archiver": "^5.3.1", 43 | "inquirer": "6.2.0", 44 | "npm-run-all": "^4.1.5", 45 | "progress": "^2.0.3", 46 | "readdir-glob": "^1.1.2", 47 | "semver": "^7.3.7", 48 | "typescript": "^4.6.4" 49 | } 50 | } -------------------------------------------------------------------------------- /packages/code-editor/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "component", 5 | { 6 | // 组件库的名字,需要和 package.json 里的 name 相同; 7 | "libraryName": "@proxy/code-editor", 8 | // 存放组件的文件夹,如果不想配置此项,默认文件夹的名字为 lib; 9 | "libDir": "lib" 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/code-editor/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | } 4 | -------------------------------------------------------------------------------- /packages/code-editor/examples/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 37 | -------------------------------------------------------------------------------- /packages/code-editor/examples/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | render: h => h(App), 6 | }).$mount('#app') 7 | -------------------------------------------------------------------------------- /packages/code-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/code-editor", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A code editor of vue", 6 | "main": "lib/index.common.js", 7 | "files": [ 8 | "lib" 9 | ], 10 | "scripts": { 11 | "build": "vue-cli-service build --target lib --name index --dest lib packages/index.js", 12 | "serve": "vue-cli-service serve" 13 | }, 14 | "dependencies": { 15 | "ace-builds": "^1.13.1", 16 | "core-js": "^3.6.5", 17 | "vue": "2.6.10" 18 | }, 19 | "eslintConfig": { 20 | "root": true, 21 | "env": { 22 | "node": true 23 | }, 24 | "extends": [ 25 | "plugin:vue/essential", 26 | "eslint:recommended" 27 | ], 28 | "parserOptions": { 29 | "parser": "babel-eslint" 30 | }, 31 | "rules": {} 32 | }, 33 | "browserslist": [ 34 | "> 1%", 35 | "last 2 versions", 36 | "not dead" 37 | ], 38 | "devDependencies": { 39 | "@vue/cli-plugin-babel": "~4.5.0", 40 | "@vue/cli-service": "~4.5.0", 41 | "babel-eslint": "^10.1.0", 42 | "eslint": "^6.7.2", 43 | "eslint-plugin-vue": "^6.2.2", 44 | "vue-template-compiler": "2.6.10" 45 | } 46 | } -------------------------------------------------------------------------------- /packages/code-editor/packages/index.js: -------------------------------------------------------------------------------- 1 | import VueCodeEditor from "./index.vue"; 2 | 3 | // 动态引入src目录下的所有index.js 4 | function install(Vue) { 5 | Vue.component(VueCodeEditor.name, VueCodeEditor); 6 | } 7 | 8 | if (typeof window !== "undefined" && window.Vue) { 9 | install(window.Vue); 10 | } 11 | 12 | export default { install }; 13 | export { VueCodeEditor } -------------------------------------------------------------------------------- /packages/code-editor/packages/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/code-editor/packages/useEditor.js: -------------------------------------------------------------------------------- 1 | import ace from "ace-builds"; 2 | import "ace-builds/webpack-resolver"; // 在 webpack 环境中使用必须要导入 3 | // 根据自己的需求按需引入 4 | import "ace-builds/src-noconflict/ext-language_tools"; 5 | import "ace-builds/src-noconflict/theme-monokai"; // 主题 6 | import "ace-builds/src-noconflict/mode-javascript"; // 语言模式 7 | import "ace-builds/src-noconflict/snippets/javascript"; //代码提示 8 | 9 | // https://ace.c9.io/#nav=howto 10 | // 初始化 11 | export function useInit(container, type) { 12 | // 初始化 13 | const target = ace.edit(container, { 14 | maxLines: 20, // 最大行数,超过会自动出现滚动条 15 | minLines: 20, // 最小行数,还未到最大行数时,编辑器会自动伸缩大小 16 | fontSize: 14, // 编辑器内字体大小 17 | theme: "ace/theme/monokai", // 主题 18 | mode: "ace/mode/javascript", // 默认设置的语言模式 19 | tabSize: 4, // 制表符设置为 4 个空格大小 20 | }); 21 | target.setOptions({ 22 | enableSnippets: true, 23 | enableLiveAutocompletion: true, 24 | enableBasicAutocompletion: true, 25 | }); 26 | 27 | // 自定义提示 28 | customCompletions(target, type) 29 | 30 | return target; 31 | } 32 | 33 | // 自定义提示 34 | function customCompletions(target, type = 'interceptor') { 35 | if (type === 'interceptor') 36 | target.completers.push({ 37 | getCompletions: function (state, session, pos, prefix, callback) { 38 | if (prefix.length === 0) { 39 | callback(null, []); 40 | return; 41 | } 42 | callback(null, [ 43 | { meta: 'AjaxProxy::Ctx.req', caption: 'req.url: string', value: 'req.url', score: 100 }, 44 | { meta: 'AjaxProxy::Ctx.req', caption: 'req.method: string', value: 'req.method', score: 100 }, 45 | { meta: 'AjaxProxy::Ctx.req', caption: 'req.body?: any', value: 'req.body', score: 100 }, 46 | { meta: 'AjaxProxy::Ctx.res', caption: 'res.status: string', value: 'res.status', score: 100 }, 47 | { meta: 'AjaxProxy::Ctx.res', caption: 'res.customStatus: string', value: 'res.customStatus', score: 100 }, 48 | { meta: 'AjaxProxy::Ctx.res', caption: 'res.response: any', value: 'res.response', score: 100 }, 49 | { 50 | meta: 'AjaxProxy::Next', 51 | caption: 'next({ override?: string, status?: string | number })', 52 | value: 'next({ override: "", status: "" });', 53 | score: 100 54 | }, 55 | ]); 56 | }, 57 | }); 58 | else 59 | target.completers.push({ 60 | getCompletions: function (state, session, pos, prefix, callback) { 61 | if (prefix.length === 0) { 62 | callback(null, []); 63 | return; 64 | } 65 | callback(null, [ 66 | { meta: 'AjaxProxy::Ctx.req', caption: 'req.url: string', value: 'req.url', score: 100 }, 67 | { meta: 'AjaxProxy::Ctx.req', caption: 'req.method: string', value: 'req.method', score: 100 }, 68 | { 69 | meta: 'AjaxProxy::Next', 70 | caption: 'next({ url: string, headers?: { [key: string]: string } })', 71 | value: 'next({ url: req.url });', 72 | score: 100 73 | }, 74 | ]); 75 | }, 76 | }); 77 | } 78 | 79 | // 默认内容 80 | export function getDefaultContent(type = "interceptor") { 81 | let defaultContent = type === 'interceptor' ? 82 | ` 83 | /** @see https://github.com/g0ngjie/ajax-proxy/blob/master/README.func.md */ 84 | function setup(req, res, next) { 85 | // TODO... 86 | // type Next = { override?: string, status?: string | number } 87 | next({ override: "", status: "" }); 88 | } 89 | ` : 90 | ` 91 | /** @see https://github.com/g0ngjie/ajax-proxy/blob/master/README.func.md */ 92 | function setup( 93 | req, /** req: { url: string, method: string }*/ 94 | next /**{ url: string, headers?: { [key: string]: string } }*/ 95 | ) { 96 | // TODO... 97 | next({ url: "" }); 98 | } 99 | ` 100 | return defaultContent 101 | } 102 | -------------------------------------------------------------------------------- /packages/code-editor/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/code-editor/vue.config.js: -------------------------------------------------------------------------------- 1 | 2 | const isProduction = process.env.NODE_ENV === "production"; 3 | 4 | module.exports = { 5 | publicPath: './', 6 | runtimeCompiler: true, 7 | // 修改 src 为 examples 8 | pages: { 9 | index: { 10 | entry: "examples/main.js", 11 | template: "public/index.html", 12 | filename: "index.html", 13 | }, 14 | }, 15 | productionSourceMap: false, 16 | configureWebpack: (config) => { 17 | if (isProduction) { 18 | config.module.rules.push({ 19 | test: /\.mjs$/, 20 | include: /node_modules/, 21 | type: "javascript/auto" 22 | }); 23 | // 取消webpack警告的性能提示 24 | config.performance = { 25 | hints: "warning", 26 | //入口起点的最大体积 27 | maxEntrypointSize: 50000000, 28 | //生成文件的最大体积 29 | maxAssetSize: 30000000, 30 | }; 31 | } 32 | }, 33 | } -------------------------------------------------------------------------------- /packages/compatibility/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/compatibility", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Ajax Proxy 新老数据兼容库", 6 | "main": "lib/index.js", 7 | "types": "types/index.d.ts", 8 | "scripts": { 9 | "watch": "tsc -w", 10 | "build": "rm -rf lib types && tsc" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^18.0.1" 14 | }, 15 | "keywords": [], 16 | "author": { 17 | "name": "Gj", 18 | "email": "gongjie0422@163.com" 19 | }, 20 | "license": "MIT", 21 | "dependencies": { 22 | "@proxy/lib": "workspace:^1.0.0" 23 | } 24 | } -------------------------------------------------------------------------------- /packages/compatibility/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MappingOldKeys, 3 | NewGLobalStateStruct, 4 | NewUploadStruct, 5 | OldGLobalStateStruct, 6 | OldInterceptorStruct, 7 | OldRedirectorStruct, 8 | OldUploadStruct 9 | } from "./types"; 10 | 11 | // 判断是否为老GlobalState 数据 12 | function isOldGlobalState(x: any): x is OldGLobalStateStruct { 13 | return x && x.hasOwnProperty("globalSwitchOn") 14 | } 15 | 16 | // 判断是否为老上传数据结构 17 | function isOldUploadState(x: any): x is OldUploadStruct { 18 | return x && 19 | ( 20 | x.hasOwnProperty("proxy_routes") || 21 | x.hasOwnProperty("redirect") || 22 | x.hasOwnProperty("lang") 23 | ) 24 | } 25 | 26 | // 判断是否为老 拦截列表 27 | function isOldInterceptor(x: any): x is OldInterceptorStruct[] { 28 | if (!x) return false 29 | if (!Array.isArray(x)) return false 30 | for (let i = 0; i < x.length; i++) { 31 | const target = x[i]; 32 | if (!target.hasOwnProperty("match")) return false 33 | } 34 | return true 35 | } 36 | 37 | // 判断是否为老 重定向列表 38 | function isOldRedirector(x: any): x is OldRedirectorStruct[] { 39 | if (!x) return false 40 | if (!Array.isArray(x)) return false 41 | for (let i = 0; i < x.length; i++) { 42 | const target = x[i]; 43 | if (!target.hasOwnProperty("redirect")) return false 44 | } 45 | return true 46 | } 47 | 48 | // 转义拦截列表 49 | function interceptorConversion(target: any) { 50 | if (isOldInterceptor(target)) { 51 | const newInterceptor: NewGLobalStateStruct['interceptor_matching_content'] = target.map(interceptor => { 52 | const { 53 | id = "", switchOn = false, filterType = "normal", method = "ANY", remark = "", 54 | match = "", tagId = "", statusCode = "200", override = "", hit = 0 55 | } = interceptor 56 | return { 57 | id, switch_on: switchOn, filter_type: filterType, method, remark, 58 | match_url: match, tagId, status_code: statusCode, override, hit 59 | } 60 | }) 61 | return newInterceptor 62 | } 63 | return target 64 | } 65 | 66 | // 转义重定向列表 67 | function redirectorConversion(target: any) { 68 | if (isOldRedirector(target)) { 69 | const newInterceptor: NewGLobalStateStruct['redirector_matching_content'] = target.map(interceptor => { 70 | const { 71 | id = "", switchOn = false, filterType = "normal", method = "ANY", remark = "", 72 | domain = "", redirect = "", headers = [], whitelist = [] 73 | } = interceptor 74 | return { 75 | id, switch_on: switchOn, filter_type: filterType, method, remark, 76 | domain, redirect_url: redirect, headers, ignores: whitelist 77 | } 78 | }) 79 | return newInterceptor 80 | } 81 | return target 82 | } 83 | 84 | /** 85 | * 数据转换 86 | * content-script 使用, 页面加载时 87 | * 由于 v2.1.0 版本 项目重构,数据属性有变动。 88 | * 需要将老数据预处理为新数据结构体 89 | */ 90 | export function onLoadForDataConversion(target: T) { 91 | if (isOldGlobalState(target)) { 92 | // 需要转换数据格式 93 | const { 94 | globalSwitchOn, 95 | mode, 96 | proxy_routes, 97 | redirect 98 | } = target 99 | 100 | // 转义 拦截列表 101 | const interceptors = interceptorConversion(proxy_routes) 102 | // 转义 重定向列表 103 | const redirectors = redirectorConversion(redirect) 104 | const newData: NewGLobalStateStruct = { 105 | global_on: globalSwitchOn, 106 | mode, 107 | interceptor_matching_content: interceptors, 108 | redirector_matching_content: redirectors 109 | } 110 | // 旧数据对应key,用于上游做旧数据清理使用 111 | const changeKeywords: MappingOldKeys = ["globalSwitchOn", "mode", "proxy_routes", "redirect"] 112 | return { 113 | changed: true, 114 | data: newData, 115 | changeKeywords 116 | } 117 | } 118 | 119 | // 新数据结构,无需转换 120 | return { 121 | changed: false, 122 | data: target, 123 | changeKeywords: [] 124 | } 125 | } 126 | 127 | /** 128 | * 数据转换 129 | * panels 使用, 数据上传时 130 | * 由于 v2.1.0 版本 项目重构,数据属性有变动。 131 | * 需要将老数据预处理为新数据结构体 132 | * 原数据格式: { lang, proxy_routes, tags, mode, redirect } 133 | * 转义为: { language, mode, tags, interceptors, redirectors } 134 | */ 135 | export function onUploadForDataConversion(target: OldUploadStruct | NewUploadStruct) { 136 | if (isOldUploadState(target)) { 137 | const { 138 | lang = 'en', proxy_routes = [], redirect = [], mode = 'interceptor', tags = [] 139 | } = target 140 | 141 | // 转义 拦截列表 142 | const interceptors = interceptorConversion(proxy_routes) 143 | // 转义 重定向列表 144 | const redirectors = redirectorConversion(redirect) 145 | const newData: NewUploadStruct = { 146 | language: lang, 147 | mode, 148 | tags, 149 | interceptors, 150 | redirectors 151 | } 152 | return newData 153 | } 154 | // 新数据结构,无需转换 155 | return target 156 | } 157 | -------------------------------------------------------------------------------- /packages/compatibility/src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IFilterType, 3 | IGlobalState, 4 | IMatchInterceptorContent, 5 | IMatchRedirectContent, 6 | IMode, 7 | IRedirectHeader, 8 | IRequestMethod 9 | } from "@proxy/lib/types/types"; 10 | 11 | /** 12 | * 标签结构体 13 | * 新老数据没变化 14 | * {id: "633f895c49960dd6iuyz3s3t", name: "asdf", used: false} 15 | * */ 16 | type TagStruct = { 17 | id: string 18 | name: string 19 | used: boolean 20 | } 21 | 22 | type OldCommonStruct = { 23 | id: string 24 | /**是否需要匹配 */ 25 | switchOn: boolean 26 | /**匹配规则 */ 27 | filterType: IFilterType 28 | /**请求协议 */ 29 | method?: IRequestMethod 30 | /**备注 */ 31 | remark?: string; 32 | } 33 | 34 | /**老数据- 拦截列表 */ 35 | export type OldInterceptorStruct = { 36 | /**匹配目标URL */ 37 | match: string 38 | /**标签id */ 39 | tagId?: TagStruct['id'] 40 | /**状态码 */ 41 | statusCode?: string; 42 | /**命中率 */ 43 | hit?: number; 44 | /**需要覆盖的内容 */ 45 | override?: string; 46 | } & OldCommonStruct 47 | 48 | /**老数据- 重定向列表 */ 49 | export type OldRedirectorStruct = { 50 | /**域名 */ 51 | domain: string 52 | /**重定向地址 */ 53 | redirect: string 54 | /**请求头 */ 55 | headers?: IRedirectHeader[]; 56 | /**忽略名单 */ 57 | whitelist?: string[]; 58 | } & OldCommonStruct 59 | 60 | /** 61 | * 老数据 GlobalState 结构体 62 | */ 63 | export type OldGLobalStateStruct = { 64 | /**全局开关 */ 65 | globalSwitchOn: boolean 66 | /**模式 */ 67 | mode: IMode 68 | /**拦截规则列表 */ 69 | proxy_routes?: OldInterceptorStruct[] 70 | /**重定向规则列表 */ 71 | redirect?: OldRedirectorStruct[] 72 | } 73 | 74 | /** 75 | * 旧数据 key 值映射 76 | */ 77 | export type MappingOldKeys = (keyof OldGLobalStateStruct)[] 78 | 79 | /** 80 | * 新GlobalState数据结构体 81 | */ 82 | export type NewGLobalStateStruct = IGlobalState & { 83 | /**拦截规则列表 */ 84 | interceptor_matching_content?: ( 85 | IMatchInterceptorContent & { 86 | id: string 87 | tagId?: TagStruct['id'] 88 | } 89 | )[] 90 | redirector_matching_content?: ( 91 | IMatchRedirectContent & { 92 | id: string 93 | } 94 | )[] 95 | } 96 | 97 | /** 98 | * 上传 - 老数据格式 99 | */ 100 | export type OldUploadStruct = { 101 | /**语言 */ 102 | lang: string 103 | /**拦截规则列表 */ 104 | proxy_routes?: OldInterceptorStruct[] 105 | /**重定向规则列表 */ 106 | redirect?: OldRedirectorStruct[] 107 | /**模式 */ 108 | mode: IMode 109 | /**标签 */ 110 | tags?: TagStruct[] 111 | } 112 | 113 | /** 114 | * 上传 - 新数据格式 115 | */ 116 | export type NewUploadStruct = { 117 | language: string 118 | mode: IMode 119 | tags: TagStruct[] 120 | interceptors?: ( 121 | IMatchInterceptorContent & { 122 | id: string 123 | tagId?: TagStruct['id'] 124 | } 125 | )[] 126 | redirectors?: ( 127 | IMatchRedirectContent & { 128 | id: string 129 | } 130 | )[] 131 | } -------------------------------------------------------------------------------- /packages/compatibility/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "types": [ 11 | "node" 12 | ], 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "sourceMap": true, 16 | "preserveWatchOutput": true, 17 | // Strict 18 | "noImplicitAny": false, 19 | "noImplicitThis": true, 20 | "alwaysStrict": true, 21 | "strictBindCallApply": true, 22 | "strictFunctionTypes": true, 23 | "declaration": true, 24 | "declarationDir": "types", 25 | "outDir": "lib" 26 | }, 27 | "include": [ 28 | "src/**/*", 29 | ], 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } -------------------------------------------------------------------------------- /packages/compatibility/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { MappingOldKeys, NewGLobalStateStruct, NewUploadStruct, OldGLobalStateStruct, OldUploadStruct } from "./types"; 2 | /** 3 | * 数据转换 4 | * content-script 使用, 页面加载时 5 | * 由于 v2.1.0 版本 项目重构,数据属性有变动。 6 | * 需要将老数据预处理为新数据结构体 7 | */ 8 | export declare function onLoadForDataConversion(target: T): { 11 | changed: boolean; 12 | data: NewGLobalStateStruct; 13 | changeKeywords: MappingOldKeys; 14 | } | { 15 | changed: boolean; 16 | data: T; 17 | changeKeywords: never[]; 18 | }; 19 | /** 20 | * 数据转换 21 | * panels 使用, 数据上传时 22 | * 由于 v2.1.0 版本 项目重构,数据属性有变动。 23 | * 需要将老数据预处理为新数据结构体 24 | * 原数据格式: { lang, proxy_routes, tags, mode, redirect } 25 | * 转义为: { language, mode, tags, interceptors, redirectors } 26 | */ 27 | export declare function onUploadForDataConversion(target: OldUploadStruct | NewUploadStruct): NewUploadStruct; 28 | -------------------------------------------------------------------------------- /packages/compatibility/types/types.d.ts: -------------------------------------------------------------------------------- 1 | import { IFilterType, IGlobalState, IMatchInterceptorContent, IMatchRedirectContent, IMode, IRedirectHeader, IRequestMethod } from "@proxy/lib/types/types"; 2 | /** 3 | * 标签结构体 4 | * 新老数据没变化 5 | * {id: "633f895c49960dd6iuyz3s3t", name: "asdf", used: false} 6 | * */ 7 | declare type TagStruct = { 8 | id: string; 9 | name: string; 10 | used: boolean; 11 | }; 12 | declare type OldCommonStruct = { 13 | id: string; 14 | /**是否需要匹配 */ 15 | switchOn: boolean; 16 | /**匹配规则 */ 17 | filterType: IFilterType; 18 | /**请求协议 */ 19 | method?: IRequestMethod; 20 | /**备注 */ 21 | remark?: string; 22 | }; 23 | /**老数据- 拦截列表 */ 24 | export declare type OldInterceptorStruct = { 25 | /**匹配目标URL */ 26 | match: string; 27 | /**标签id */ 28 | tagId?: TagStruct['id']; 29 | /**状态码 */ 30 | statusCode?: string; 31 | /**命中率 */ 32 | hit?: number; 33 | /**需要覆盖的内容 */ 34 | override?: string; 35 | } & OldCommonStruct; 36 | /**老数据- 重定向列表 */ 37 | export declare type OldRedirectorStruct = { 38 | /**域名 */ 39 | domain: string; 40 | /**重定向地址 */ 41 | redirect: string; 42 | /**请求头 */ 43 | headers?: IRedirectHeader[]; 44 | /**忽略名单 */ 45 | whitelist?: string[]; 46 | } & OldCommonStruct; 47 | /** 48 | * 老数据 GlobalState 结构体 49 | */ 50 | export declare type OldGLobalStateStruct = { 51 | /**全局开关 */ 52 | globalSwitchOn: boolean; 53 | /**模式 */ 54 | mode: IMode; 55 | /**拦截规则列表 */ 56 | proxy_routes?: OldInterceptorStruct[]; 57 | /**重定向规则列表 */ 58 | redirect?: OldRedirectorStruct[]; 59 | }; 60 | /** 61 | * 旧数据 key 值映射 62 | */ 63 | export declare type MappingOldKeys = (keyof OldGLobalStateStruct)[]; 64 | /** 65 | * 新GlobalState数据结构体 66 | */ 67 | export declare type NewGLobalStateStruct = IGlobalState & { 68 | /**拦截规则列表 */ 69 | interceptor_matching_content?: (IMatchInterceptorContent & { 70 | id: string; 71 | tagId?: TagStruct['id']; 72 | })[]; 73 | redirector_matching_content?: (IMatchRedirectContent & { 74 | id: string; 75 | })[]; 76 | }; 77 | /** 78 | * 上传 - 老数据格式 79 | */ 80 | export declare type OldUploadStruct = { 81 | /**语言 */ 82 | lang: string; 83 | /**拦截规则列表 */ 84 | proxy_routes?: OldInterceptorStruct[]; 85 | /**重定向规则列表 */ 86 | redirect?: OldRedirectorStruct[]; 87 | /**模式 */ 88 | mode: IMode; 89 | /**标签 */ 90 | tags?: TagStruct[]; 91 | }; 92 | /** 93 | * 上传 - 新数据格式 94 | */ 95 | export declare type NewUploadStruct = { 96 | language: string; 97 | mode: IMode; 98 | tags: TagStruct[]; 99 | interceptors?: (IMatchInterceptorContent & { 100 | id: string; 101 | tagId?: TagStruct['id']; 102 | })[]; 103 | redirectors?: (IMatchRedirectContent & { 104 | id: string; 105 | })[]; 106 | }; 107 | export {}; 108 | -------------------------------------------------------------------------------- /packages/json-editor/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "component", 5 | { 6 | // 组件库的名字,需要和 package.json 里的 name 相同; 7 | "libraryName": "@proxy/json-editor", 8 | // 存放组件的文件夹,如果不想配置此项,默认文件夹的名字为 lib; 9 | "libDir": "lib" 10 | } 11 | ] 12 | ] 13 | } -------------------------------------------------------------------------------- /packages/json-editor/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@vue/cli-plugin-babel/preset'] 3 | } 4 | -------------------------------------------------------------------------------- /packages/json-editor/examples/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 49 | -------------------------------------------------------------------------------- /packages/json-editor/examples/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | render: h => h(App), 6 | }).$mount('#app') 7 | -------------------------------------------------------------------------------- /packages/json-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/json-editor", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A json editor of vue", 6 | "main": "lib/index.common.js", 7 | "files": [ 8 | "lib" 9 | ], 10 | "scripts": { 11 | "build": "vue-cli-service build --target lib --name index --dest lib packages/index.js", 12 | "serve": "vue-cli-service serve" 13 | }, 14 | "dependencies": { 15 | "core-js": "^3.6.5", 16 | "jsoneditor": "^9.9.2", 17 | "vue": "^2.6.11" 18 | }, 19 | "eslintConfig": { 20 | "root": true, 21 | "env": { 22 | "node": true 23 | }, 24 | "extends": [ 25 | "plugin:vue/essential", 26 | "eslint:recommended" 27 | ], 28 | "parserOptions": { 29 | "parser": "babel-eslint" 30 | }, 31 | "rules": {} 32 | }, 33 | "browserslist": [ 34 | "> 1%", 35 | "last 2 versions", 36 | "not dead" 37 | ], 38 | "devDependencies": { 39 | "@vue/cli-plugin-babel": "~4.5.0", 40 | "@vue/cli-service": "~4.5.0", 41 | "babel-eslint": "^10.1.0", 42 | "eslint": "^6.7.2", 43 | "eslint-plugin-vue": "^6.2.2", 44 | "vue-template-compiler": "2.6.11" 45 | } 46 | } -------------------------------------------------------------------------------- /packages/json-editor/packages/index.js: -------------------------------------------------------------------------------- 1 | import VueJsonEditor from "./index.vue"; 2 | 3 | // 动态引入src目录下的所有index.js 4 | function install(Vue) { 5 | Vue.component(VueJsonEditor.name, VueJsonEditor); 6 | } 7 | 8 | if (typeof window !== "undefined" && window.Vue) { 9 | install(window.Vue); 10 | } 11 | 12 | export default { install }; 13 | export { VueJsonEditor } -------------------------------------------------------------------------------- /packages/json-editor/packages/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 107 | 108 | 147 | -------------------------------------------------------------------------------- /packages/json-editor/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | 12 | 13 | 14 | 18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/json-editor/vue.config.js: -------------------------------------------------------------------------------- 1 | 2 | const isProduction = process.env.NODE_ENV === "production"; 3 | 4 | module.exports = { 5 | publicPath: './', 6 | runtimeCompiler: true, 7 | // 修改 src 为 examples 8 | pages: { 9 | index: { 10 | entry: "examples/main.js", 11 | template: "public/index.html", 12 | filename: "index.html", 13 | }, 14 | }, 15 | productionSourceMap: false, 16 | configureWebpack: (config) => { 17 | if (isProduction) { 18 | config.module.rules.push({ 19 | test: /\.mjs$/, 20 | include: /node_modules/, 21 | type: "javascript/auto" 22 | }); 23 | // 取消webpack警告的性能提示 24 | config.performance = { 25 | hints: "warning", 26 | //入口起点的最大体积 27 | maxEntrypointSize: 50000000, 28 | //生成文件的最大体积 29 | maxAssetSize: 30000000, 30 | }; 31 | } 32 | }, 33 | } -------------------------------------------------------------------------------- /packages/proxy-lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/lib", 3 | "version": "1.0.0", 4 | "description": "ajax interceptor lib", 5 | "main": "lib/index.umd.js", 6 | "module": "lib/index.esm.js", 7 | "typings": "types/index.d.ts", 8 | "scripts": { 9 | "watch": "run-p watch:ts watch:vite", 10 | "build": "rm -rf types lib && tsc && vite build", 11 | "watch:ts": "tsc --watch", 12 | "watch:vite": "vite build --watch" 13 | }, 14 | "files": [ 15 | "lib", 16 | "types" 17 | ], 18 | "keywords": [], 19 | "author": { 20 | "name": "Gj", 21 | "email": "gongjie0422@163.com" 22 | }, 23 | "license": "MIT", 24 | "devDependencies": { 25 | "vite": "^2.9.13" 26 | }, 27 | "dependencies": { 28 | "@proxy/shared-utils": "workspace:^1.0.0" 29 | } 30 | } -------------------------------------------------------------------------------- /packages/proxy-lib/src/common.ts: -------------------------------------------------------------------------------- 1 | import { IMatchInterceptorContent, IMatchRedirectContent } from "./types"; 2 | import { NoticeTo } from "@proxy/shared-utils"; 3 | 4 | // match_url规则匹配 5 | export function maybeMatching( 6 | url: string, 7 | match: string, 8 | type: IMatchInterceptorContent['filter_type'] = "normal" 9 | ) { 10 | let matched = false; 11 | switch (type) { 12 | // 普通匹配规则 13 | case "normal": 14 | matched = url.includes(match); 15 | break; 16 | // 正则匹配规则 17 | case "regex": 18 | try { 19 | url.match(new RegExp(match, "i")) && (matched = true); 20 | } catch (error) { } 21 | break; 22 | } 23 | return matched; 24 | } 25 | 26 | /** 27 | * 规则过滤 28 | * 域名判断 29 | * 规则类型判断 30 | * 忽略列表判断 31 | */ 32 | // 再根据规则匹配m 33 | export function matchIgnoresAndRule( 34 | url: string, 35 | match: string, 36 | type: IMatchRedirectContent['filter_type'] = "normal", 37 | ignores: IMatchRedirectContent['ignores'] 38 | ) { 39 | let matched = false; 40 | // 先判断是否白名单过滤 41 | const findOne = ignores?.find((target) => url.includes(target)); 42 | // 白名单过滤掉 43 | if (findOne) return matched; 44 | matched = maybeMatching(url, match, type); 45 | return matched; 46 | } 47 | 48 | function isURLObject(x: unknown): x is URL { 49 | return x && 50 | (x as any).hasOwnProperty('host') && 51 | (x as any).hasOwnProperty('hash') 52 | } 53 | 54 | /**格式化URL 返回 string */ 55 | export function fmtURLToString(url: string | URL) { 56 | if (isURLObject(url)) return url.toString() 57 | else return url 58 | } 59 | 60 | /**获取重定向URL */ 61 | export function finalRedirectUrl( 62 | url: string, 63 | match: string, 64 | redirect_url: string, 65 | type: IMatchRedirectContent['filter_type'] = "normal" 66 | ) { 67 | let finalUrl = url; 68 | switch (type) { 69 | // 普通替换 70 | case "normal": 71 | finalUrl = url.replace(match, redirect_url); 72 | break; 73 | // 正则替换 74 | case "regex": 75 | finalUrl = url.replace(new RegExp(match, "i"), redirect_url); 76 | break; 77 | } 78 | return finalUrl; 79 | } 80 | 81 | // 通知到 content 命中统计 82 | export function notice(url: string, match_url: string, method: string) { 83 | window.dispatchEvent( 84 | new CustomEvent(NoticeTo.CONTENT, { 85 | // 注意: 这里一般只用到 match_url 和 method 86 | // url为真实请求地址,使用的话 会导致 hit无法正确匹配, 87 | // 例如 fanyi.baidu.com 接口参数在url上面,match_url 为原始 url 88 | detail: { url, match_url, method }, 89 | }) 90 | ); 91 | } 92 | 93 | export const warn = (...args: any[]) => console.warn(...args) -------------------------------------------------------------------------------- /packages/proxy-lib/src/createFetch.ts: -------------------------------------------------------------------------------- 1 | import { maybeMatching, notice } from "./common"; 2 | import { execSetup, getCtx } from "./overrideFunc"; 3 | import { OverrideType, RefGlobalState } from "./types"; 4 | 5 | // 共享状态 6 | let globalState: RefGlobalState 7 | // fetch 副本 8 | export const OriginFetch = window.fetch.bind(window) 9 | // 初始化共享状态 10 | export const initInterceptorFetchState = (state: RefGlobalState) => globalState = state 11 | 12 | function CustomFetch(input: RequestInfo | URL, init?: RequestInit): Promise { 13 | let fetchMethod: string | undefined | "ANY" = "ANY" 14 | if (init) { 15 | fetchMethod = init.method?.toUpperCase() || "ANY" 16 | } 17 | return OriginFetch(input, init).then(async (response: Response) => { 18 | let txt: string | undefined; 19 | let status = response.status 20 | let statusText = response.statusText 21 | let _overrideType: OverrideType = "json" 22 | for (let i = 0; i < globalState.value.interceptor_matching_content.length; i++) { 23 | const target = globalState.value.interceptor_matching_content[i]; 24 | const { 25 | switch_on = true, 26 | match_url, 27 | override = "", 28 | filter_type, 29 | method, 30 | status_code = "200", 31 | override_type = "json", 32 | override_func = "" 33 | } = target 34 | // 是否需要匹配 35 | if (switch_on && match_url) { 36 | // 判断是否存在协议匹配 37 | if (method && ![fetchMethod, "ANY"].includes(method.toUpperCase())) continue 38 | // 规则匹配 39 | const matched = maybeMatching(response.url, match_url, filter_type); 40 | if (!matched) continue // 退出当前循环 41 | _overrideType = override_type 42 | if (override_type === "function") { 43 | const ctx = getCtx( 44 | response.url, 45 | init?.method?.toUpperCase() || "GET", 46 | response.status, 47 | status_code, 48 | init?.body, 49 | response 50 | ) 51 | const payload = await execSetup(ctx, override_func) 52 | if (payload.override) 53 | txt = typeof payload.override === "string" ? payload.override : JSON.stringify(payload.override); 54 | status = +payload.status! 55 | statusText = payload.status + "" 56 | } else { 57 | // 修改响应 58 | txt = typeof override === "string" ? override : JSON.stringify(override); 59 | // 修改状态码 60 | status = +status_code 61 | statusText = status_code 62 | } 63 | // 通知 64 | notice(response.url, match_url, fetchMethod || "") 65 | } 66 | } 67 | 68 | // 返回原始响应 69 | if (!globalState.value.global_on || (!txt && _overrideType !== 'function')) return response 70 | 71 | const stream = new ReadableStream({ 72 | start(controller) { 73 | controller.enqueue(new TextEncoder().encode(txt)); 74 | controller.close(); 75 | }, 76 | }); 77 | const newResponse = new Response(stream, { 78 | headers: response.headers, 79 | status: status, 80 | statusText: statusText, 81 | }); 82 | const proxy = new Proxy(newResponse, { 83 | get: function (target, prop) { 84 | const checkKeys = ['ok', 'redirected', 'type', 'url', 'useFinalURL', 'body', 'bodyUsed']; 85 | if (checkKeys.includes(prop as string)) { 86 | return Reflect.get(target, prop); 87 | } 88 | return Reflect.get(target, prop); 89 | }, 90 | }); 91 | 92 | for (let key in proxy) { 93 | // 获取proxy key 对应实例 94 | const target = Reflect.get(proxy, key); 95 | // 判断实例是否为 Response 实例 96 | if (typeof target === "function") { 97 | // 将新的 Response 实例绑定到 proxy 实例上 98 | Reflect.set(proxy, key, target.bind(newResponse)); 99 | } 100 | } 101 | return proxy; 102 | }); 103 | } 104 | 105 | export default CustomFetch -------------------------------------------------------------------------------- /packages/proxy-lib/src/createXHR.ts: -------------------------------------------------------------------------------- 1 | import { maybeMatching, notice } from "./common"; 2 | import { execSetup, getCtx } from "./overrideFunc"; 3 | import { RefGlobalState } from "./types"; 4 | 5 | // 共享状态 6 | let globalState: RefGlobalState 7 | // XMLHttpRequest 副本 8 | export const OriginXHR = window.XMLHttpRequest; 9 | // 初始化共享状态 10 | export const initInterceptorXHRState = (state: RefGlobalState) => globalState = state 11 | 12 | class CustomXHR extends XMLHttpRequest { 13 | // 响应内容 14 | responseText!: string; 15 | // XHR 响应 16 | response: any; 17 | status!: number; 18 | statusText: string = ""; 19 | // 请求协议 20 | method = 'GET' 21 | // 请求Body 22 | body?: Document | XMLHttpRequestBodyInit | null 23 | // 消息锁 24 | private message_once_lock: boolean = false; 25 | 26 | constructor() { 27 | super(); 28 | // 初始化原始XHR实例 29 | // 将XHR属性赋值给Custom 30 | // 重写 response & responseText 31 | this.watchAndOverride() 32 | // 拦截open,获取请求协议 33 | this.getMethod() 34 | } 35 | 36 | // 获取请求协议 37 | private getMethod() { 38 | const { open, send } = this 39 | this.open = ( 40 | method: string, 41 | url: string | URL, 42 | async?: boolean, 43 | username?: string | null, 44 | password?: string | null, 45 | ) => { 46 | // 获取当前请求协议 47 | this.method = (method || 'ANY').toUpperCase() 48 | open.apply(this, [method, url, async !== undefined ? async : true, username, password]) 49 | } 50 | this.send = (body?: Document | XMLHttpRequestBodyInit | null) => { 51 | this.body = body 52 | send.call(this, body) 53 | } 54 | } 55 | 56 | // 规则匹配,修改响应内容 57 | private async maybeNeedModifyRes(origin_xhr_response: any) { 58 | for (let i = 0; i < globalState.value.interceptor_matching_content.length; i++) { 59 | const target = globalState.value.interceptor_matching_content[i]; 60 | const { 61 | switch_on = true, 62 | match_url, 63 | override = "", 64 | filter_type, 65 | method, 66 | status_code = "200", 67 | override_type = "json", 68 | override_func = "" 69 | } = target 70 | // 是否需要匹配 71 | if (switch_on && match_url) { 72 | // 判断是否存在协议匹配 73 | if (method && ![this.method, "ANY"].includes(method.toUpperCase())) continue 74 | // 规则匹配 75 | const matched = maybeMatching(this.responseURL, match_url, filter_type); 76 | if (!matched) continue // 退出当前循环 77 | if (override_type === "function") { 78 | const ctx = getCtx(this.responseURL, this.method, this.status, status_code, this.body, origin_xhr_response) 79 | const payload = await execSetup(ctx, override_func) 80 | if (payload.override) { 81 | const _override = typeof payload.override === "string" ? payload.override : JSON.stringify(payload.override); 82 | this.responseText = _override; 83 | this.response = _override; 84 | } 85 | this.status = +payload.status! 86 | this.statusText = payload.status + "" 87 | } else { 88 | // 修改响应 89 | this.responseText = override; 90 | this.response = override; 91 | // 修改状态码 92 | this.status = +status_code 93 | this.statusText = status_code 94 | } 95 | // 通知 96 | if (!this.message_once_lock) { 97 | notice(this.responseURL, match_url, this.method); 98 | this.message_once_lock = true; 99 | } 100 | } 101 | } 102 | } 103 | 104 | // 属性重写 105 | private overrideAttr(attr: keyof XMLHttpRequest, xhr: XMLHttpRequest) { 106 | // 重写属性 107 | // @ts-ignore 108 | if (typeof xhr[attr] === "function") this[attr] = xhr[attr].bind(xhr); 109 | else if (['responseText', 'response', 'status', 'statusText'].includes(attr)) 110 | // responseText和response 属性只读 111 | // 缓存在对应 自定义 _[attr] 上 112 | Object.defineProperty(this, attr, { 113 | get: () => 114 | // @ts-ignore 115 | this[`_${attr}`] == undefined ? xhr[attr] : this[`_${attr}`], 116 | // @ts-ignore 117 | set: (val) => (this[`_${attr}`] = val), 118 | enumerable: true, 119 | }); 120 | else 121 | Object.defineProperty(this, attr, { 122 | get: () => xhr[attr], 123 | // @ts-ignore 124 | set: (val) => (xhr[attr] = val), 125 | enumerable: true, 126 | }); 127 | } 128 | 129 | // 拦截监听 130 | private watchAndOverride() { 131 | // 获取原始XHR 132 | const xhr = new OriginXHR(); 133 | for (let attr in xhr) { 134 | if (attr === "onreadystatechange") { 135 | xhr.onreadystatechange = async (...args) => { 136 | // 开启拦截 137 | if (this.readyState == 4) await this.maybeNeedModifyRes(xhr.response); 138 | this.onreadystatechange && this.onreadystatechange.apply(this, args); 139 | }; 140 | continue; 141 | } 142 | // else if (attr === "onload") { 143 | // xhr.onload = async (...args) => { 144 | // // 开启拦截 145 | // await this.maybeNeedModifyRes(xhr.response); 146 | // this.onload && this.onload.apply(this, args); 147 | // }; 148 | // this.onload = null; 149 | // continue; 150 | // } 151 | // 其他属性重写 152 | this.overrideAttr(attr as keyof XMLHttpRequest, xhr) 153 | } 154 | } 155 | } 156 | 157 | export default CustomXHR -------------------------------------------------------------------------------- /packages/proxy-lib/src/index.ts: -------------------------------------------------------------------------------- 1 | import { IGlobalState, IMatchInterceptorContent, IMatchRedirectContent, IMode, IRequestMethod, RefGlobalState } from "./types"; 2 | import CreateXHR, { initInterceptorXHRState, OriginXHR } from "./createXHR"; 3 | import CreateFetch, { initInterceptorFetchState, OriginFetch } from "./createFetch"; 4 | import RedirectXHR, { initRedirectXHRState } from "./redirectXHR"; 5 | import RedirectFetch, { initRedirectFetchState } from "./redirectFetch"; 6 | import { warn } from "./common"; 7 | 8 | // 初始化共享状态 9 | const globalState: RefGlobalState = { 10 | value: { 11 | // 全局状态开关 12 | global_on: false, 13 | // 模式 14 | mode: 'interceptor', 15 | // 拦截匹配内容 16 | interceptor_matching_content: [], 17 | // 重定向匹配 18 | redirector_matching_content: [] 19 | } 20 | } 21 | 22 | 23 | function isIGlobalState(x: any): x is IGlobalState { 24 | return x && 25 | x.hasOwnProperty("global_on") && 26 | x.hasOwnProperty("mode") 27 | } 28 | 29 | function isMode(x: any): x is IMode { 30 | return typeof x === 'string' && ["interceptor", "redirector"].includes(x) 31 | } 32 | 33 | function isArray(x: any): x is any[] { 34 | return x && Array.isArray(x) 35 | } 36 | 37 | function isInterceptors(x: any): x is IMatchInterceptorContent[] { 38 | for (let i = 0; i < x.length; i++) { 39 | const target = x[i]; 40 | const bool = target.hasOwnProperty("match_url") && target.hasOwnProperty("switch_on") 41 | if (!bool) { 42 | return false 43 | } 44 | } 45 | return true 46 | } 47 | 48 | function isRedirectors(x: any): x is IMatchRedirectContent[] { 49 | for (let i = 0; i < x.length; i++) { 50 | const target = x[i]; 51 | const bool = target.hasOwnProperty("domain") && 52 | target.hasOwnProperty("switch_on") && 53 | target.hasOwnProperty("redirect_url") 54 | if (!bool) { 55 | return false 56 | } 57 | } 58 | return true 59 | } 60 | 61 | // 初始化状态 62 | function initState() { 63 | // 初始化共享状态 64 | initInterceptorXHRState(globalState); 65 | initInterceptorFetchState(globalState); 66 | initRedirectXHRState(globalState); 67 | initRedirectFetchState(globalState); 68 | } 69 | 70 | // 实例挂载 71 | function mountInstance() { 72 | const { global_on = true, mode } = globalState.value 73 | // 每次挂载时,需要预先重置一下引用 74 | window.XMLHttpRequest = OriginXHR 75 | window.fetch = OriginFetch 76 | if (global_on) { 77 | if (mode === 'interceptor') { 78 | // 挂载拦截器 79 | window.XMLHttpRequest = CreateXHR 80 | window.fetch = CreateFetch 81 | } else if (mode === "redirector") { 82 | // 挂载重定向 83 | window.XMLHttpRequest = RedirectXHR 84 | window.fetch = RedirectFetch 85 | } 86 | } 87 | } 88 | 89 | /**全局开关 */ 90 | function update(global_switch_on: T): void 91 | /**修改模式 */ 92 | function update(mode: T): void 93 | /**修改拦截器 */ 94 | function update(interceptors: T): void 95 | /**修改重定向 */ 96 | function update(redirectors: T): void 97 | /**修改全部属性 */ 98 | function update(state: T): void 99 | function update(target: unknow) { 100 | // 全局开关 101 | if (typeof target === "boolean") { 102 | globalState.value.global_on = target 103 | // 更新一波实例 104 | mountInstance() 105 | } 106 | // 修改模式 107 | else if (isMode(target)) { 108 | globalState.value.mode = target 109 | // 需要更新一下实例 110 | mountInstance() 111 | } 112 | // 数组类型: 拦截列表、重定向列表 113 | else if (isArray(target)) { 114 | if (target.length > 0) { 115 | // 修改拦截器 116 | if (isInterceptors(target)) globalState.value.interceptor_matching_content = target 117 | // 修改重定向 118 | else if (isRedirectors(target)) globalState.value.redirector_matching_content = target 119 | } else { 120 | // 判断当前模式 121 | // 清空拦截列表 122 | if (globalState.value.mode === 'interceptor') globalState.value.interceptor_matching_content = [] 123 | // 清空重定向列表 124 | else globalState.value.redirector_matching_content = [] 125 | } 126 | } 127 | // 设置全部属性 128 | // 默认初始化时使用 129 | else if (isIGlobalState(target)) { 130 | // 替换全部 131 | globalState.value = target 132 | // 重新挂载实例 133 | mountInstance() 134 | } else warn("unknow type") 135 | } 136 | 137 | initState() 138 | 139 | export default { 140 | update, 141 | } 142 | 143 | export type { 144 | IRequestMethod, 145 | IMatchInterceptorContent, 146 | IMatchRedirectContent, 147 | } -------------------------------------------------------------------------------- /packages/proxy-lib/src/overrideFunc.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | const errLog = function (...args: any[]) { 4 | console.log("%c[AjaxProxy][error]: ", "color: #ff4d4f", ...args) 5 | } 6 | 7 | type Ctx = { 8 | req: { 9 | url: string 10 | method: string 11 | body?: any 12 | } 13 | res: { 14 | status: string 15 | customStatus: string 16 | response: any 17 | } 18 | } 19 | 20 | type Next = { 21 | override?: string 22 | status?: string | number 23 | } 24 | 25 | function isNext(x: any): x is Next { 26 | return !!x && (x.override || x.status) 27 | } 28 | 29 | export function getCtx( 30 | url: string, 31 | method: string, 32 | status: string | number, 33 | customStatus: string, 34 | body?: any, 35 | response?: any 36 | ): Ctx { 37 | return { 38 | req: { url, method, body }, 39 | res: { status: status.toString(), customStatus, response }, 40 | } 41 | } 42 | 43 | function overrideNext(next: T, customStatus: string): T { 44 | if (isNext(next)) { 45 | // 默认status 46 | if (!next.status) next.status = customStatus 47 | // 默认响应值 48 | if (!next.override) next.override = "" 49 | return next 50 | } 51 | return { override: "", status: customStatus } as T 52 | } 53 | 54 | export function execSetup(ctx: Ctx, funcText: string): Promise { 55 | return new Promise(resolve => { 56 | try { 57 | const source = ';(' + funcText + ')' 58 | const execFunc = window.eval(source) 59 | const type = typeof execFunc 60 | if (type === "function") { 61 | if (!funcText.includes('next(')) { 62 | errLog("The structure of 'next' is incorrect [code 1]") 63 | return resolve({ override: "", status: ctx.res.customStatus }) 64 | } 65 | execFunc(ctx.req, ctx.res, (next: Next) => { 66 | const overrideData = overrideNext(next, ctx.res.customStatus) 67 | return resolve(overrideData) 68 | }) 69 | } else { 70 | errLog("Please enter a correct 'function' [code 2]") 71 | return resolve({ override: "", status: ctx.res.customStatus }) 72 | } 73 | } catch (error) { 74 | errLog(error) 75 | return resolve({ override: "", status: ctx.res.customStatus }) 76 | } 77 | }) 78 | } -------------------------------------------------------------------------------- /packages/proxy-lib/src/redirectFetch.ts: -------------------------------------------------------------------------------- 1 | import { finalRedirectUrl, matchIgnoresAndRule } from "./common"; 2 | import { execSetup } from "./redirectUrlFunc"; 3 | import { RefGlobalState } from "./types"; 4 | 5 | // 共享状态 6 | let globalState: RefGlobalState 7 | const OriginFetch = window.fetch.bind(window) 8 | // 初始化共享状态 9 | export const initRedirectFetchState = (state: RefGlobalState) => globalState = state 10 | 11 | export default async function CustomFetch(input: RequestInfo | URL, init?: RequestInit): Promise { 12 | let fetchMethod: string | undefined | "ANY" = "ANY" 13 | let customInit: RequestInit = init || {} 14 | if (init) { 15 | fetchMethod = init.method?.toUpperCase() || "ANY" 16 | } 17 | 18 | for (let i = 0; i < globalState.value.redirector_matching_content.length; i++) { 19 | const { 20 | switch_on = true, 21 | domain = "", 22 | method = "ANY", 23 | filter_type, 24 | redirect_url = "", 25 | headers = [], 26 | ignores = [], 27 | redirect_type = "text", 28 | redirect_func = "" 29 | } = globalState.value.redirector_matching_content[i]; 30 | if (switch_on) { 31 | // 判断是否存在协议匹配 32 | if (method && ![fetchMethod, "ANY"].includes(method.toUpperCase())) break 33 | if (redirect_type === "function") { 34 | const payload = await execSetup({ url: input.toString(), method: fetchMethod }, redirect_func) 35 | input = payload.url 36 | 37 | if (init?.headers) { 38 | // 初始化 RequestInit 39 | customInit = init 40 | const initHeaders = new Headers(init.headers) 41 | if (payload.headers) { 42 | for (const key in payload.headers) { 43 | if (Object.prototype.hasOwnProperty.call(payload.headers, key)) { 44 | const value = payload.headers[key]; 45 | if (key && value) initHeaders.set(key, value) 46 | } 47 | } 48 | } 49 | customInit.headers = initHeaders 50 | } else { 51 | const newHeaders: HeadersInit = payload.headers ? Object.keys(payload.headers).map(key => [key, payload.headers![key]]) : [] 52 | customInit = { 53 | headers: newHeaders 54 | } 55 | } 56 | 57 | // 值取当前命中的第一个,后续再命中的忽略 58 | break; 59 | } else if (matchIgnoresAndRule(input.toString(), domain, filter_type, ignores)) { 60 | input = finalRedirectUrl(input.toString(), domain, redirect_url, filter_type) 61 | 62 | if (init?.headers) { 63 | // 初始化 RequestInit 64 | customInit = init 65 | const initHeaders = new Headers(init.headers) 66 | headers.forEach(header => { 67 | initHeaders.set(header.key, header.value) 68 | }) 69 | customInit.headers = initHeaders 70 | } else { 71 | const newHeaders: HeadersInit = headers.map(header => [header.key, header.value]) 72 | customInit = { 73 | headers: newHeaders 74 | } 75 | } 76 | 77 | // 值取当前命中的第一个,后续再命中的忽略 78 | break; 79 | } 80 | } 81 | } 82 | return OriginFetch.call(CustomFetch, input, customInit) 83 | } 84 | -------------------------------------------------------------------------------- /packages/proxy-lib/src/redirectUrlFunc.ts: -------------------------------------------------------------------------------- 1 | import { IRedirectHeader } from './types'; 2 | 3 | 4 | const errLog = function (...args: any[]) { 5 | console.log("%c[AjaxProxy][error]: ", "color: #ff4d4f", ...args) 6 | } 7 | 8 | type Req = { 9 | url: string 10 | method: string 11 | } 12 | 13 | type Next = { 14 | url: string, 15 | headers?: { [key: IRedirectHeader['key']]: IRedirectHeader['value'] } 16 | } 17 | 18 | function isNext(x: any): x is Next { 19 | if (!!x) return true 20 | if (x.url && !x.headers) return true 21 | return x.headers && Object.prototype.toString.call(x.headers) == '[object Object]' 22 | } 23 | 24 | export function execSetup(req: Req, funcText: string): Promise { 25 | return new Promise(resolve => { 26 | try { 27 | const source = ';(' + funcText + ')' 28 | const execFunc = window.eval(source) 29 | const type = typeof execFunc 30 | if (type === "function") { 31 | if (!funcText.includes('next(')) { 32 | errLog("The structure of 'next' is incorrect [code 1]") 33 | // original 34 | return resolve({ url: req.url }) 35 | } 36 | execFunc(req, (next: Next) => { 37 | // custom 38 | if (isNext(next)) { 39 | return resolve({ url: next.url, headers: next.headers }) 40 | } 41 | }) 42 | return resolve({ url: req.url }) 43 | } 44 | errLog("Please enter a correct 'function' [code 2]") 45 | return resolve({ url: req.url }) 46 | } catch (error) { 47 | errLog(error) 48 | return resolve({ url: req.url }) 49 | } 50 | }) 51 | } -------------------------------------------------------------------------------- /packages/proxy-lib/src/redirectXHR.ts: -------------------------------------------------------------------------------- 1 | import { finalRedirectUrl, fmtURLToString, matchIgnoresAndRule } from "./common"; 2 | import { execSetup } from "./redirectUrlFunc"; 3 | import { RefGlobalState } from "./types"; 4 | 5 | // 状态 6 | let globalState: RefGlobalState 7 | // 初始化共享状态 8 | export const initRedirectXHRState = (state: RefGlobalState) => globalState = state 9 | 10 | export default class CustomRedirectXHR extends XMLHttpRequest { 11 | 12 | // 请求协议 13 | method = 'ANY' 14 | 15 | constructor() { 16 | super(); 17 | this.watchAndRedirect() 18 | } 19 | 20 | private watchAndRedirect() { 21 | const { open } = this 22 | const origin_XHR_open = open 23 | const origin_XHR_setRequestHeader = this.setRequestHeader 24 | const origin_XHR_send = this.send 25 | this.open = async (method: string, 26 | url: string | URL, 27 | async?: boolean, 28 | username?: string | null, 29 | password?: string | null, 30 | ) => { 31 | this.method = (method || "ANY").toUpperCase() 32 | for (let i = 0; i < globalState.value.redirector_matching_content.length; i++) { 33 | const { 34 | switch_on = true, 35 | domain = "", 36 | method = "ANY", 37 | filter_type, 38 | redirect_url = "", 39 | headers = [], 40 | ignores = [], 41 | redirect_type = "text", 42 | redirect_func = "" 43 | } = globalState.value.redirector_matching_content[i]; 44 | if (switch_on) { 45 | // 判断是否存在协议匹配 46 | if (method && ![this.method, "ANY"].includes(method.toUpperCase())) return 47 | // 规则判断 48 | const currentUrl = fmtURLToString(url) 49 | if (redirect_type === "function") { 50 | const payload = await execSetup({ url: currentUrl, method: this.method }, redirect_func) 51 | url = payload.url 52 | this.send = (body?: Document | XMLHttpRequestBodyInit | null) => { 53 | if (payload.headers) { 54 | for (const key in payload.headers) { 55 | if (Object.prototype.hasOwnProperty.call(payload.headers, key)) { 56 | const value = payload.headers[key]; 57 | if (key && value) 58 | this.setRequestHeader(key, value) 59 | } 60 | } 61 | } 62 | origin_XHR_send.call(this, body) 63 | } 64 | break; 65 | } else if (matchIgnoresAndRule(currentUrl, domain, filter_type, ignores)) { 66 | url = finalRedirectUrl(currentUrl, domain, redirect_url, filter_type) 67 | 68 | // 获取 自定义 header 映射关系 69 | const cacheHeaderMap: { [key: string]: string } = {} 70 | headers.forEach(header => (cacheHeaderMap[header.key] = header.value)) 71 | 72 | this.setRequestHeader = (name: string, value: string) => { 73 | // 判断是否存在需要修改的header 74 | // 如果存在,则不修改,让send里面重新设置 75 | if (!cacheHeaderMap[name]) origin_XHR_setRequestHeader.apply(this, [name, value]) 76 | } 77 | this.send = (body?: Document | XMLHttpRequestBodyInit | null) => { 78 | for (let k = 0; k < headers.length; k++) { 79 | const header = headers[k]; 80 | // 移除掉 映射关系 81 | // Reflect.deleteProperty(cacheHeaderMap, header.key) 82 | delete cacheHeaderMap[header.key] 83 | this.setRequestHeader(header.key, header.value) 84 | } 85 | origin_XHR_send.call(this, body) 86 | } 87 | // 值取当前命中的第一个,后续再命中的忽略 88 | break; 89 | } 90 | 91 | } 92 | } 93 | origin_XHR_open.apply(this, [method, url, async !== undefined ? async : true, username, password]) 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /packages/proxy-lib/src/types.ts: -------------------------------------------------------------------------------- 1 | /**请求协议 */ 2 | export type IRequestMethod = "ANY" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; 3 | /**规则 */ 4 | export type IFilterType = "normal" | "regex"; 5 | /**模式 */ 6 | export type IMode = "interceptor" | "redirector" 7 | /**全局状态结构体 */ 8 | export type RefGlobalState = { value: T } 9 | /**响应式类型 */ 10 | export type OverrideType = 'json' | 'function' 11 | /**重定向类型 */ 12 | export type RedirectType = 'text' | 'function' 13 | 14 | type CommonContent = { 15 | /**是否需要匹配 */ 16 | switch_on: boolean; 17 | /**匹配规则 */ 18 | filter_type?: IFilterType 19 | /**请求协议 */ 20 | method?: IRequestMethod 21 | /**备注 */ 22 | remark?: string; 23 | } 24 | 25 | /**拦截器对象 */ 26 | export type IMatchInterceptorContent = { 27 | /**匹配目标URL */ 28 | match_url: string; 29 | /**需要覆盖的内容 */ 30 | override?: string; 31 | /**命中率 */ 32 | hit?: number; 33 | /**状态码 */ 34 | status_code?: string 35 | /**响应式类型 */ 36 | override_type?: OverrideType 37 | /**函数响应 */ 38 | override_func?: string 39 | } & CommonContent 40 | 41 | /**重定向头部结构体 */ 42 | export type IRedirectHeader = { 43 | key: string 44 | value: string 45 | description?: string 46 | } 47 | 48 | /**重定向对象结构体 */ 49 | export type IMatchRedirectContent = { 50 | /**域名 */ 51 | domain: string 52 | /**重定向地址 */ 53 | redirect_url: string 54 | /**请求头 */ 55 | headers?: IRedirectHeader[] 56 | /**忽略名单 */ 57 | ignores?: string[] 58 | /**重定向类型 */ 59 | redirect_type?: RedirectType 60 | /**函数响应 */ 61 | redirect_func?: string 62 | } & CommonContent 63 | 64 | export type IGlobalState = { 65 | /**全局开关 */ 66 | global_on: boolean; 67 | /**模式 */ 68 | mode: IMode; 69 | /**拦截规则列表 */ 70 | interceptor_matching_content: IMatchInterceptorContent[]; 71 | /**重定向规则列表 */ 72 | redirector_matching_content: IMatchRedirectContent[]; 73 | } 74 | -------------------------------------------------------------------------------- /packages/proxy-lib/test/base.test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /packages/proxy-lib/test/base.test.interval.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /packages/proxy-lib/test/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /packages/proxy-lib/test/test.xhr.post.get.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /packages/proxy-lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "useDefineForClassFields": true, 5 | "module": "esnext", 6 | "forceConsistentCasingInFileNames": true, 7 | "moduleResolution": "node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "sourceMap": false, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "esModuleInterop": true, 14 | "lib": ["esnext", "dom"], 15 | "skipLibCheck": true, 16 | "declaration": true, 17 | "declarationDir": "types", 18 | "outDir": "types", 19 | "emitDeclarationOnly": true, 20 | }, 21 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 22 | "references": [{ "path": "./tsconfig.node.json" }] 23 | } 24 | -------------------------------------------------------------------------------- /packages/proxy-lib/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "strict": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "moduleResolution": "node" 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/common.d.ts: -------------------------------------------------------------------------------- 1 | import { IMatchInterceptorContent, IMatchRedirectContent } from "./types"; 2 | export declare function maybeMatching(url: string, match: string, type?: IMatchInterceptorContent['filter_type']): boolean; 3 | /** 4 | * 规则过滤 5 | * 域名判断 6 | * 规则类型判断 7 | * 忽略列表判断 8 | */ 9 | export declare function matchIgnoresAndRule(url: string, match: string, type: import("./types").IFilterType | undefined, ignores: IMatchRedirectContent['ignores']): boolean; 10 | /**格式化URL 返回 string */ 11 | export declare function fmtURLToString(url: string | URL): string; 12 | /**获取重定向URL */ 13 | export declare function finalRedirectUrl(url: string, match: string, redirect_url: string, type?: IMatchRedirectContent['filter_type']): string; 14 | export declare function notice(url: string, match_url: string, method: string): void; 15 | export declare const warn: (...args: any[]) => void; 16 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/createFetch.d.ts: -------------------------------------------------------------------------------- 1 | import { RefGlobalState } from "./types"; 2 | export declare const OriginFetch: ((input: URL | RequestInfo, init?: RequestInit | undefined) => Promise) & typeof fetch; 3 | export declare const initInterceptorFetchState: (state: RefGlobalState) => RefGlobalState; 4 | declare function CustomFetch(input: RequestInfo | URL, init?: RequestInit): Promise; 5 | export default CustomFetch; 6 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/createXHR.d.ts: -------------------------------------------------------------------------------- 1 | import { RefGlobalState } from "./types"; 2 | export declare const OriginXHR: { 3 | new (): XMLHttpRequest; 4 | prototype: XMLHttpRequest; 5 | readonly DONE: number; 6 | readonly HEADERS_RECEIVED: number; 7 | readonly LOADING: number; 8 | readonly OPENED: number; 9 | readonly UNSENT: number; 10 | }; 11 | export declare const initInterceptorXHRState: (state: RefGlobalState) => RefGlobalState; 12 | declare class CustomXHR extends XMLHttpRequest { 13 | responseText: string; 14 | response: any; 15 | status: number; 16 | statusText: string; 17 | method: string; 18 | body?: Document | XMLHttpRequestBodyInit | null; 19 | private message_once_lock; 20 | constructor(); 21 | private getMethod; 22 | private maybeNeedModifyRes; 23 | private overrideAttr; 24 | private watchAndOverride; 25 | } 26 | export default CustomXHR; 27 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { IGlobalState, IMatchInterceptorContent, IMatchRedirectContent, IMode, IRequestMethod } from "./types"; 2 | /**全局开关 */ 3 | declare function update(global_switch_on: T): void; 4 | /**修改模式 */ 5 | declare function update(mode: T): void; 6 | /**修改拦截器 */ 7 | declare function update(interceptors: T): void; 8 | /**修改重定向 */ 9 | declare function update(redirectors: T): void; 10 | /**修改全部属性 */ 11 | declare function update(state: T): void; 12 | declare const _default: { 13 | update: typeof update; 14 | }; 15 | export default _default; 16 | export type { IRequestMethod, IMatchInterceptorContent, IMatchRedirectContent, }; 17 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/overrideFunc.d.ts: -------------------------------------------------------------------------------- 1 | declare type Ctx = { 2 | req: { 3 | url: string; 4 | method: string; 5 | body?: any; 6 | }; 7 | res: { 8 | status: string; 9 | customStatus: string; 10 | response: any; 11 | }; 12 | }; 13 | declare type Next = { 14 | override?: string; 15 | status?: string | number; 16 | }; 17 | export declare function getCtx(url: string, method: string, status: string | number, customStatus: string, body?: any, response?: any): Ctx; 18 | export declare function execSetup(ctx: Ctx, funcText: string): Promise; 19 | export {}; 20 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/redirectFetch.d.ts: -------------------------------------------------------------------------------- 1 | import { RefGlobalState } from "./types"; 2 | export declare const initRedirectFetchState: (state: RefGlobalState) => RefGlobalState; 3 | export default function CustomFetch(input: RequestInfo | URL, init?: RequestInit): Promise; 4 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/redirectUrlFunc.d.ts: -------------------------------------------------------------------------------- 1 | import { IRedirectHeader } from './types'; 2 | declare type Req = { 3 | url: string; 4 | method: string; 5 | }; 6 | declare type Next = { 7 | url: string; 8 | headers?: { 9 | [key: IRedirectHeader['key']]: IRedirectHeader['value']; 10 | }; 11 | }; 12 | export declare function execSetup(req: Req, funcText: string): Promise; 13 | export {}; 14 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/redirectXHR.d.ts: -------------------------------------------------------------------------------- 1 | import { RefGlobalState } from "./types"; 2 | export declare const initRedirectXHRState: (state: RefGlobalState) => RefGlobalState; 3 | export default class CustomRedirectXHR extends XMLHttpRequest { 4 | method: string; 5 | constructor(); 6 | private watchAndRedirect; 7 | } 8 | -------------------------------------------------------------------------------- /packages/proxy-lib/types/types.d.ts: -------------------------------------------------------------------------------- 1 | /**请求协议 */ 2 | export declare type IRequestMethod = "ANY" | "GET" | "POST" | "PUT" | "DELETE" | "PATCH"; 3 | /**规则 */ 4 | export declare type IFilterType = "normal" | "regex"; 5 | /**模式 */ 6 | export declare type IMode = "interceptor" | "redirector"; 7 | /**全局状态结构体 */ 8 | export declare type RefGlobalState = { 9 | value: T; 10 | }; 11 | /**响应式类型 */ 12 | export declare type OverrideType = 'json' | 'function'; 13 | /**重定向类型 */ 14 | export declare type RedirectType = 'text' | 'function'; 15 | declare type CommonContent = { 16 | /**是否需要匹配 */ 17 | switch_on: boolean; 18 | /**匹配规则 */ 19 | filter_type?: IFilterType; 20 | /**请求协议 */ 21 | method?: IRequestMethod; 22 | /**备注 */ 23 | remark?: string; 24 | }; 25 | /**拦截器对象 */ 26 | export declare type IMatchInterceptorContent = { 27 | /**匹配目标URL */ 28 | match_url: string; 29 | /**需要覆盖的内容 */ 30 | override?: string; 31 | /**命中率 */ 32 | hit?: number; 33 | /**状态码 */ 34 | status_code?: string; 35 | /**响应式类型 */ 36 | override_type?: OverrideType; 37 | /**函数响应 */ 38 | override_func?: string; 39 | } & CommonContent; 40 | /**重定向头部结构体 */ 41 | export declare type IRedirectHeader = { 42 | key: string; 43 | value: string; 44 | description?: string; 45 | }; 46 | /**重定向对象结构体 */ 47 | export declare type IMatchRedirectContent = { 48 | /**域名 */ 49 | domain: string; 50 | /**重定向地址 */ 51 | redirect_url: string; 52 | /**请求头 */ 53 | headers?: IRedirectHeader[]; 54 | /**忽略名单 */ 55 | ignores?: string[]; 56 | /**重定向类型 */ 57 | redirect_type?: RedirectType; 58 | /**函数响应 */ 59 | redirect_func?: string; 60 | } & CommonContent; 61 | export declare type IGlobalState = { 62 | /**全局开关 */ 63 | global_on: boolean; 64 | /**模式 */ 65 | mode: IMode; 66 | /**拦截规则列表 */ 67 | interceptor_matching_content: IMatchInterceptorContent[]; 68 | /**重定向规则列表 */ 69 | redirector_matching_content: IMatchRedirectContent[]; 70 | }; 71 | export {}; 72 | -------------------------------------------------------------------------------- /packages/proxy-lib/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: resolve(__dirname, 'src/index.ts'), 9 | name: 'lib', 10 | }, 11 | rollupOptions: { 12 | output: [ 13 | { 14 | format: 'umd', 15 | entryFileNames: '[name].umd.js', 16 | sourcemap: true, 17 | dir: resolve(__dirname, 'lib'), 18 | }, 19 | { 20 | format: 'esm', 21 | entryFileNames: '[name].esm.js', 22 | sourcemap: true, 23 | dir: resolve(__dirname, 'lib'), 24 | } 25 | ] 26 | } 27 | }, 28 | plugins: [] 29 | }) 30 | -------------------------------------------------------------------------------- /packages/shared-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/shared-utils", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "lib/index.js", 7 | "types": "types/index.d.ts", 8 | "devDependencies": { 9 | "@types/chrome": "^0.0.197", 10 | "@types/node": "^18.0.1" 11 | }, 12 | "scripts": { 13 | "watch": "tsc -w", 14 | "build": "rm -rf lib types && tsc" 15 | }, 16 | "keywords": [], 17 | "author": { 18 | "name": "Gj", 19 | "email": "gongjie0422@163.com" 20 | }, 21 | "license": "MIT" 22 | } -------------------------------------------------------------------------------- /packages/shared-utils/src/action.ts: -------------------------------------------------------------------------------- 1 | import { useAction } from "./env"; 2 | 3 | /**设置徽章 */ 4 | export function setBadge(count: number | undefined) { 5 | if (useAction) { 6 | chrome.action.setBadgeBackgroundColor({ color: "#006d75" }); 7 | if (count) { 8 | chrome.action.setBadgeText({ text: `+${count}` }); 9 | } else { 10 | chrome.action.setBadgeText({ text: "" }); 11 | } 12 | } 13 | } 14 | 15 | /**Tab栏图片启用禁用状态 */ 16 | export function setTabIcon(enable: boolean) { 17 | if (useAction) { 18 | chrome.action.setIcon({ 19 | path: enable ? "/icons/128.png" : "/icons/128g.png", 20 | }); 21 | } 22 | } -------------------------------------------------------------------------------- /packages/shared-utils/src/consts.ts: -------------------------------------------------------------------------------- 1 | /**storage enums */ 2 | export enum StorageKey { 3 | LANGUAGE = 'ajax-proxy:storage:language', 4 | /**全局开关 */ 5 | GLOBAL_SWITCH = 'ajax-proxy:storage:global-switch', 6 | /**拦截列表 */ 7 | INTERCEPT_LIST = 'ajax-proxy:storage:intercept-list', 8 | /**重定向列表 */ 9 | REDIRECT_LIST = 'ajax-proxy:storage:redirect-list', 10 | /**模式 */ 11 | MODE = 'ajax-proxy:storage:mode', 12 | /**标签 */ 13 | TAGS = 'ajax-proxy:storage:tags', 14 | } 15 | 16 | /**通知-去向 */ 17 | export enum NoticeTo { 18 | /**通知 content */ 19 | CONTENT = 'ajax-proxy:notice:to:content', 20 | /**通知 panels */ 21 | PANELS = 'ajax-proxy:notice:to:panels', 22 | /**通知 document */ 23 | DOCUMENT = 'ajax-proxy:notice:to:document', 24 | /**通知 service-worker */ 25 | SERVICE_WORKER = 'ajax-proxy:notice:to:service-worker' 26 | } 27 | 28 | /**通知-来自 */ 29 | export enum NoticeFrom { 30 | /**来自 content */ 31 | CONTENT = 'ajax-proxy:notice:from:content', 32 | /**来自 panels */ 33 | PANELS = 'ajax-proxy:notice:from:panels', 34 | /**来自 service-worker */ 35 | SERVICE_WORKER = 'ajax-proxy:notice:from:service-worker', 36 | } 37 | 38 | /**通知Key */ 39 | export enum NoticeKey { 40 | /**全局开关 */ 41 | GLOBAL_SWITCH = 'ajax-proxy:notice:global-switch', 42 | /**拦截数据列表 */ 43 | INTERCEPT_LIST = 'ajax-proxy:notice:intercept-list', 44 | /**重定向列表 */ 45 | REDIRECT_LIST = 'ajax-proxy:notice:redirect-list', 46 | /**获取当前title */ 47 | GET_CURRENT_TITLE = "ajax-proxy:notice:get-current-title", 48 | /**徽章状态 */ 49 | BADGE_STATUS = 'ajax-proxy:notice:badge-status', 50 | /**命中率 */ 51 | HIT_RATE = 'ajax-proxy:notice:hit-rate', 52 | /**模式 */ 53 | MODE = 'ajax-proxy:notice:mode', 54 | } 55 | -------------------------------------------------------------------------------- /packages/shared-utils/src/env.ts: -------------------------------------------------------------------------------- 1 | 2 | export const useAction = typeof chrome !== "undefined" && typeof chrome.action !== "undefined"; 3 | 4 | export const useRuntime = typeof chrome !== "undefined" && typeof chrome.runtime !== "undefined"; 5 | 6 | export const useStorage = typeof chrome !== 'undefined' && typeof chrome.storage !== 'undefined' 7 | -------------------------------------------------------------------------------- /packages/shared-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consts' 2 | export * from './env' 3 | export * from './storage' 4 | export * from './notice' 5 | export * from './action' -------------------------------------------------------------------------------- /packages/shared-utils/src/notice.ts: -------------------------------------------------------------------------------- 1 | import { useRuntime } from "./env"; 2 | import { NoticeFrom, NoticeTo, NoticeKey } from "./consts"; 3 | 4 | /** 5 | * 通知 content -> document 6 | */ 7 | export function noticeDocumentByContent(key: NoticeKey | string, value) { 8 | window.postMessage({ 9 | from: NoticeFrom.CONTENT, 10 | to: NoticeTo.DOCUMENT, 11 | key, 12 | value, 13 | }); 14 | } 15 | 16 | /** 17 | * 通知 content -> service-worker 18 | * @param key 19 | * @param value 20 | */ 21 | export function noticeServiceWorkerByContent(key: NoticeKey | string, value) { 22 | if (useRuntime) { 23 | chrome.runtime.sendMessage({ 24 | from: NoticeFrom.CONTENT, 25 | to: NoticeTo.SERVICE_WORKER, 26 | key, 27 | value, 28 | }).catch(err => { 29 | // 离线状态 或 网页未加载成功情况下,content_script 未加载,导致没有接收方 30 | // 会导致:Error: Could not establish connection. Receiving end does not exist. 31 | }); 32 | } 33 | } 34 | 35 | /** 36 | * 通知 panels -> service-worker 37 | */ 38 | export function noticeServiceWorkerByPanels(key, value) { 39 | if (useRuntime) { 40 | chrome.runtime.sendMessage(chrome.runtime.id, { 41 | from: NoticeFrom.PANELS, 42 | to: NoticeTo.SERVICE_WORKER, 43 | key, value 44 | }).catch(err => { 45 | // 离线状态 或 网页未加载成功情况下,content_script 未加载,导致没有接收方 46 | // 会导致:Error: Could not establish connection. Receiving end does not exist. 47 | }); 48 | } 49 | } 50 | 51 | /** 52 | * 通知 service-worker -> panels 53 | */ 54 | export function noticePanelsByServiceWorker(key: NoticeKey, value?: any) { 55 | if (useRuntime) { 56 | chrome.runtime.sendMessage({ 57 | from: NoticeFrom.SERVICE_WORKER, 58 | to: NoticeTo.PANELS, 59 | key, value 60 | }).catch(err => { }) 61 | } 62 | } 63 | 64 | /** service-worker 长链接监听 */ 65 | export function onConnectByServiceWorker( 66 | onConnectFn: (port: chrome.runtime.Port) => void, 67 | onDisconnectFn: () => void, 68 | ) { 69 | // 长链接 70 | // 好处是可以实现无刷新更新拦截器代理 71 | // 弊端是每一个新的tab页都会更新current_port,旧的长链会注销 72 | chrome.runtime.onConnect.addListener((port) => { 73 | onConnectFn(port) 74 | // 监听长链接 被断开 75 | port.onDisconnect.addListener(() => onDisconnectFn()) 76 | }) 77 | } 78 | 79 | 80 | /** 81 | * 通知 service-worker -> content 82 | */ 83 | export function noticeContentByServiceWorker(port: chrome.runtime.Port | undefined, key: NoticeKey, value) { 84 | if (port) { 85 | try { 86 | port.postMessage({ 87 | from: NoticeFrom.SERVICE_WORKER, 88 | to: NoticeTo.CONTENT, 89 | key, value 90 | }) 91 | } catch (error) { 92 | // catch err 93 | // error: Error: Attempting to use a disconnected port object 94 | // 当被操作页被关闭掉,而在操作面板上继续操作时,此时port通信断开,报异常 95 | // 一般不会走到这里,上有已经做 onDisconnect 监听 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * 当前活动页签发生改变 102 | */ 103 | export function onCurrentTabChanged(callback: (tab: chrome.tabs.Tab) => void) { 104 | chrome.tabs.onActivated.addListener(activeInfo => { 105 | chrome.tabs.get(activeInfo.tabId) 106 | .then(getTab => { 107 | // 判断是否为一个正常的页签 108 | if (getTab.url?.startsWith("http") || getTab.url?.startsWith("https")) callback(getTab) 109 | // title 置空 110 | else callback({ ...getTab, title: "" }) 111 | }) 112 | .catch(err => { }) 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /packages/shared-utils/src/storage.ts: -------------------------------------------------------------------------------- 1 | import { StorageKey } from './consts' 2 | import { useStorage } from './env' 3 | 4 | // chrome.storage.sync.set 大于8,192字节的数据时,会报错 -> QUOTA_BYTES_PER_ITEM quota exceeded 5 | // chrome.storage.local.set可以包含5242880 6 | 7 | let storageData 8 | 9 | export function initStorage(): Promise { 10 | return new Promise((resolve) => { 11 | if (useStorage) { 12 | chrome.storage.local.get(null, result => { 13 | storageData = result 14 | resolve() 15 | }) 16 | } else { 17 | storageData = {} 18 | resolve() 19 | } 20 | }) 21 | } 22 | 23 | export function getStorage(key: string, defaultValue: any = null) { 24 | checkStorage() 25 | if (useStorage) { 26 | return getDefaultValue(storageData[key], defaultValue) 27 | } else { 28 | try { 29 | return getDefaultValue(JSON.parse(localStorage.getItem(key) as any), defaultValue) 30 | } catch (e) { } 31 | } 32 | } 33 | 34 | /**不走缓存获取数据 */ 35 | export function getRealStorage(key: StorageKey, defaultValue: any = null) { 36 | if (useStorage) { 37 | return new Promise(resolve => { 38 | chrome.storage.local.get(key, result => { 39 | if (result.hasOwnProperty(key)) resolve(getDefaultValue(result[key], defaultValue)) 40 | else resolve(defaultValue); 41 | }) 42 | }) 43 | } else { 44 | try { 45 | const result = getDefaultValue(JSON.parse(localStorage.getItem(key) as any), defaultValue) 46 | return Promise.resolve(result) 47 | } catch (e) { 48 | return Promise.resolve(null) 49 | } 50 | } 51 | } 52 | 53 | export function setStorage(key: string, val: any) { 54 | checkStorage() 55 | if (useStorage) { 56 | storageData[key] = val 57 | chrome.storage.local.set({ [key]: val }) 58 | } else { 59 | try { 60 | localStorage.setItem(key, JSON.stringify(val)) 61 | } catch (e) { } 62 | } 63 | } 64 | 65 | export function removeStorage(keys: string | string[]) { 66 | checkStorage() 67 | if (useStorage) { 68 | if (Array.isArray(keys)) keys.forEach(target => delete storageData[target]) 69 | else delete storageData[keys] 70 | chrome.storage.local.remove(keys) 71 | } else { 72 | try { 73 | if (Array.isArray(keys)) keys.forEach(target => localStorage.removeItem(target)) 74 | else localStorage.removeItem(keys) 75 | } catch (e) { } 76 | } 77 | } 78 | 79 | export function clearStorage() { 80 | checkStorage() 81 | if (useStorage) { 82 | storageData = {} 83 | chrome.storage.local.clear() 84 | } else { 85 | try { 86 | localStorage.clear() 87 | } catch (e) { } 88 | } 89 | } 90 | 91 | function checkStorage() { 92 | if (!storageData) { 93 | throw new Error('Storage wasn\'t initialized with \'init()\'') 94 | } 95 | } 96 | 97 | function getDefaultValue(value, defaultValue) { 98 | if (value == null) { 99 | return defaultValue 100 | } 101 | return value 102 | } 103 | 104 | /**获取全部数据 */ 105 | export function getStorageAll(): Promise<{ [key: string]: any }> { 106 | if (useStorage) { 107 | return new Promise(resolve => { 108 | chrome.storage.local.get(null, result => { 109 | resolve(result) 110 | }) 111 | }) 112 | } else { 113 | if (JSON.stringify(localStorage) === '{}') return Promise.resolve({}) 114 | const data = Object.keys(localStorage).reduce(function (obj, str) { 115 | try { 116 | obj[str] = JSON.parse(localStorage.getItem(str) as any) 117 | } catch (e) { 118 | obj[str] = localStorage.getItem(str) 119 | } 120 | return obj 121 | }, {}); 122 | return Promise.resolve(data) 123 | } 124 | } -------------------------------------------------------------------------------- /packages/shared-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "types": [ 11 | "node", 12 | "chrome" 13 | ], 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "sourceMap": true, 17 | "preserveWatchOutput": true, 18 | // Strict 19 | "noImplicitAny": false, 20 | "noImplicitThis": true, 21 | "alwaysStrict": true, 22 | "strictBindCallApply": true, 23 | "strictFunctionTypes": true, 24 | "declaration": true, 25 | "declarationDir": "types", 26 | "outDir": "lib" 27 | }, 28 | "include": [ 29 | "src/**/*", 30 | ], 31 | "exclude": [ 32 | "node_modules" 33 | ] 34 | } -------------------------------------------------------------------------------- /packages/shared-utils/types/action.d.ts: -------------------------------------------------------------------------------- 1 | /**设置徽章 */ 2 | export declare function setBadge(count: number | undefined): void; 3 | /**Tab栏图片启用禁用状态 */ 4 | export declare function setTabIcon(enable: boolean): void; 5 | -------------------------------------------------------------------------------- /packages/shared-utils/types/consts.d.ts: -------------------------------------------------------------------------------- 1 | /**storage enums */ 2 | export declare enum StorageKey { 3 | LANGUAGE = "ajax-proxy:storage:language", 4 | /**全局开关 */ 5 | GLOBAL_SWITCH = "ajax-proxy:storage:global-switch", 6 | /**拦截列表 */ 7 | INTERCEPT_LIST = "ajax-proxy:storage:intercept-list", 8 | /**重定向列表 */ 9 | REDIRECT_LIST = "ajax-proxy:storage:redirect-list", 10 | /**模式 */ 11 | MODE = "ajax-proxy:storage:mode", 12 | /**标签 */ 13 | TAGS = "ajax-proxy:storage:tags" 14 | } 15 | /**通知-去向 */ 16 | export declare enum NoticeTo { 17 | /**通知 content */ 18 | CONTENT = "ajax-proxy:notice:to:content", 19 | /**通知 panels */ 20 | PANELS = "ajax-proxy:notice:to:panels", 21 | /**通知 document */ 22 | DOCUMENT = "ajax-proxy:notice:to:document", 23 | /**通知 service-worker */ 24 | SERVICE_WORKER = "ajax-proxy:notice:to:service-worker" 25 | } 26 | /**通知-来自 */ 27 | export declare enum NoticeFrom { 28 | /**来自 content */ 29 | CONTENT = "ajax-proxy:notice:from:content", 30 | /**来自 panels */ 31 | PANELS = "ajax-proxy:notice:from:panels", 32 | /**来自 service-worker */ 33 | SERVICE_WORKER = "ajax-proxy:notice:from:service-worker" 34 | } 35 | /**通知Key */ 36 | export declare enum NoticeKey { 37 | /**全局开关 */ 38 | GLOBAL_SWITCH = "ajax-proxy:notice:global-switch", 39 | /**拦截数据列表 */ 40 | INTERCEPT_LIST = "ajax-proxy:notice:intercept-list", 41 | /**重定向列表 */ 42 | REDIRECT_LIST = "ajax-proxy:notice:redirect-list", 43 | /**获取当前title */ 44 | GET_CURRENT_TITLE = "ajax-proxy:notice:get-current-title", 45 | /**徽章状态 */ 46 | BADGE_STATUS = "ajax-proxy:notice:badge-status", 47 | /**命中率 */ 48 | HIT_RATE = "ajax-proxy:notice:hit-rate", 49 | /**模式 */ 50 | MODE = "ajax-proxy:notice:mode" 51 | } 52 | -------------------------------------------------------------------------------- /packages/shared-utils/types/env.d.ts: -------------------------------------------------------------------------------- 1 | export declare const useAction: boolean; 2 | export declare const useRuntime: boolean; 3 | export declare const useStorage: boolean; 4 | -------------------------------------------------------------------------------- /packages/shared-utils/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './consts'; 2 | export * from './env'; 3 | export * from './storage'; 4 | export * from './notice'; 5 | export * from './action'; 6 | -------------------------------------------------------------------------------- /packages/shared-utils/types/notice.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { NoticeKey } from "./consts"; 3 | /** 4 | * 通知 content -> document 5 | */ 6 | export declare function noticeDocumentByContent(key: NoticeKey | string, value: any): void; 7 | /** 8 | * 通知 content -> service-worker 9 | * @param key 10 | * @param value 11 | */ 12 | export declare function noticeServiceWorkerByContent(key: NoticeKey | string, value: any): void; 13 | /** 14 | * 通知 panels -> service-worker 15 | */ 16 | export declare function noticeServiceWorkerByPanels(key: any, value: any): void; 17 | /** 18 | * 通知 service-worker -> panels 19 | */ 20 | export declare function noticePanelsByServiceWorker(key: NoticeKey, value?: any): void; 21 | /** service-worker 长链接监听 */ 22 | export declare function onConnectByServiceWorker(onConnectFn: (port: chrome.runtime.Port) => void, onDisconnectFn: () => void): void; 23 | /** 24 | * 通知 service-worker -> content 25 | */ 26 | export declare function noticeContentByServiceWorker(port: chrome.runtime.Port | undefined, key: NoticeKey, value: any): void; 27 | /** 28 | * 当前活动页签发生改变 29 | */ 30 | export declare function onCurrentTabChanged(callback: (tab: chrome.tabs.Tab) => void): void; 31 | -------------------------------------------------------------------------------- /packages/shared-utils/types/storage.d.ts: -------------------------------------------------------------------------------- 1 | import { StorageKey } from './consts'; 2 | export declare function initStorage(): Promise; 3 | export declare function getStorage(key: string, defaultValue?: any): any; 4 | /**不走缓存获取数据 */ 5 | export declare function getRealStorage(key: StorageKey, defaultValue?: any): Promise; 6 | export declare function setStorage(key: string, val: any): void; 7 | export declare function removeStorage(keys: string | string[]): void; 8 | export declare function clearStorage(): void; 9 | /**获取全部数据 */ 10 | export declare function getStorageAll(): Promise<{ 11 | [key: string]: any; 12 | }>; 13 | -------------------------------------------------------------------------------- /packages/shell-chrome/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/packages/shell-chrome/icons/128.png -------------------------------------------------------------------------------- /packages/shell-chrome/icons/128g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/packages/shell-chrome/icons/128g.png -------------------------------------------------------------------------------- /packages/shell-chrome/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/packages/shell-chrome/icons/48.png -------------------------------------------------------------------------------- /packages/shell-chrome/icons/48g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/packages/shell-chrome/icons/48g.png -------------------------------------------------------------------------------- /packages/shell-chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Ajax Proxy", 4 | "version": "2.2.10", 5 | "description": "Modify your Ajax response to test", 6 | "author": "Gj", 7 | "icons": { 8 | "128": "icons/128.png" 9 | }, 10 | "permissions": [ 11 | "storage", 12 | "notifications", 13 | "tabs" 14 | ], 15 | "action": { 16 | "default_icon": "icons/48g.png" 17 | }, 18 | "background": { 19 | "service_worker": "service_worker.js" 20 | }, 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "" 25 | ], 26 | "js": [ 27 | "content.js" 28 | ], 29 | "run_at": "document_start", 30 | "all_frames": true 31 | } 32 | ], 33 | "commands": { 34 | "open_panel": { 35 | "suggested_key": { 36 | "default": "Shift+Alt+Q", 37 | "windows": "Shift+Alt+Q", 38 | "mac": "Shift+Alt+Q", 39 | "linux": "Shift+Alt+Q" 40 | }, 41 | "description": "Open panel" 42 | }, 43 | "close_panel": { 44 | "suggested_key": { 45 | "default": "Shift+Alt+C", 46 | "windows": "Shift+Alt+C", 47 | "mac": "Shift+Alt+C", 48 | "linux": "Shift+Alt+C" 49 | }, 50 | "description": "Close panel" 51 | }, 52 | "full_screen": { 53 | "suggested_key": { 54 | "default": "Shift+Alt+W", 55 | "windows": "Shift+Alt+W", 56 | "mac": "Shift+Alt+W", 57 | "linux": "Shift+Alt+W" 58 | }, 59 | "description": "Full screen" 60 | }, 61 | "resize_window": { 62 | "suggested_key": { 63 | "default": "Shift+Alt+S", 64 | "windows": "Shift+Alt+S", 65 | "mac": "Shift+Alt+S", 66 | "linux": "Shift+Alt+S" 67 | }, 68 | "description": "Resize screen" 69 | } 70 | }, 71 | "web_accessible_resources": [ 72 | { 73 | "resources": [ 74 | "document.js" 75 | ], 76 | "matches": [ 77 | "" 78 | ] 79 | } 80 | ] 81 | } -------------------------------------------------------------------------------- /packages/shell-chrome/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/shell-chrome", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rm -rf build && webpack --config webpack.config.js" 8 | }, 9 | "keywords": [], 10 | "author": { 11 | "name": "Gj", 12 | "email": "gongjie0422@163.com" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "@proxy/compatibility": "workspace:^1.0.0", 17 | "@proxy/lib": "workspace:^1.0.0", 18 | "@proxy/shared-utils": "workspace:^1.0.0" 19 | }, 20 | "devDependencies": { 21 | "@types/chrome": "^0.0.197", 22 | "copy-webpack-plugin": "^11.0.0", 23 | "ts-loader": "^9.4.1", 24 | "webpack": "^5.74.0", 25 | "webpack-cli": "^4.10.0" 26 | } 27 | } -------------------------------------------------------------------------------- /packages/shell-chrome/src/consts.ts: -------------------------------------------------------------------------------- 1 | 2 | /**长链接通信名称 */ 3 | export const CONNECT_NAME = 'ajax-proxy:connect:custom:name' 4 | 5 | /** 6 | * 首次设置document.title 7 | * 该枚举只在 content 和 service-worker 中使用 8 | */ 9 | export const INIT_CURRENT_TITLE = "ajax-proxy:notice:init-current-title" 10 | 11 | /** 12 | * 通知更新 IGlobalState 结构数据 13 | * content 首次加载 @proxy/lib 14 | */ 15 | export const NOTICE_KEY_REFRESH_GLOBAL_STATE = "ajax-proxy:notice:refresh:global-state" -------------------------------------------------------------------------------- /packages/shell-chrome/src/content.ts: -------------------------------------------------------------------------------- 1 | // console.log("Ajax proxy content.js") 2 | 3 | import { 4 | initStorage, 5 | NoticeTo, 6 | NoticeFrom, 7 | NoticeKey, 8 | StorageKey, 9 | noticeDocumentByContent, 10 | noticeServiceWorkerByContent, 11 | getStorageAll, 12 | setStorage, 13 | getRealStorage, 14 | removeStorage, 15 | } from "@proxy/shared-utils"; 16 | import { CONNECT_NAME, INIT_CURRENT_TITLE, NOTICE_KEY_REFRESH_GLOBAL_STATE } from "./consts"; 17 | import { onLoadForDataConversion } from "@proxy/compatibility"; 18 | 19 | // 在页面上插入代码 20 | const script = document.createElement("script"); 21 | script.setAttribute("type", "text/javascript"); 22 | script.setAttribute("src", chrome.runtime.getURL("document.js")); 23 | document.documentElement.appendChild(script); 24 | 25 | initStorage().then(() => { 26 | // 发送当前tab页 title 27 | noticeServiceWorkerByContent(INIT_CURRENT_TITLE, window.document.title) 28 | 29 | // document.js 资源加载 30 | script.addEventListener("load", async () => { 31 | // 获取 全局开关、模式、拦截列表、重定向列表 32 | const data = await getStorageAll(); 33 | // 新老数据转换 34 | const { changed, data: getData, changeKeywords } = onLoadForDataConversion(data) 35 | // 如果有老数据变更新数据,则需要在这里 setStorage 36 | if (changed) { 37 | setStorage(StorageKey.GLOBAL_SWITCH, getData.global_on) 38 | setStorage(StorageKey.MODE, getData.mode) 39 | setStorage(StorageKey.INTERCEPT_LIST, getData.interceptor_matching_content) 40 | setStorage(StorageKey.REDIRECT_LIST, getData.redirector_matching_content) 41 | // 需要清理对应旧数据,不然始终会进到当前判断条件中 42 | removeStorage(changeKeywords) 43 | } 44 | const getGlobalSwtich = getData[StorageKey.GLOBAL_SWITCH] || false 45 | if (getGlobalSwtich) noticeDocumentByContent(NOTICE_KEY_REFRESH_GLOBAL_STATE, getData) 46 | }); 47 | 48 | // 长链接通信接收 service-worker -> document 49 | const port = chrome.runtime.connect({ name: CONNECT_NAME }); 50 | // 接收service-worker 传来的信息,转发给 document.js 51 | port.onMessage.addListener(function (msg) { 52 | if (msg.from === NoticeFrom.SERVICE_WORKER && msg.to === NoticeTo.CONTENT) { 53 | const { GLOBAL_SWITCH, INTERCEPT_LIST, REDIRECT_LIST, MODE } = NoticeKey 54 | if ([GLOBAL_SWITCH, INTERCEPT_LIST, REDIRECT_LIST, MODE].includes(msg.key)) { 55 | // 注意:如果是全局开关开启的话,需要预先通知更新 mode模式 56 | if ( 57 | // 全局开关 58 | msg.key === GLOBAL_SWITCH && 59 | // 开启状态下 60 | msg.value 61 | ) { 62 | // content-script 所有 getStorage 都必须访问真实实例,不能走缓存 63 | getRealStorage(StorageKey.MODE, 'interceptor').then(getMode => { 64 | // 通知 @proxy/lib 先更新 mode 65 | // 如果不更新,lib里始终都是 拦截模式 66 | noticeDocumentByContent(MODE, getMode) 67 | }) 68 | } 69 | noticeDocumentByContent(msg.key, msg.value) 70 | } 71 | } 72 | }); 73 | 74 | // 接收lib 传来的信息 转发给 service-worker 75 | // 没有from 属性 76 | window.addEventListener( 77 | NoticeTo.CONTENT, 78 | function (event) { 79 | const customEvent = event as CustomEvent 80 | // 通知徽章上命中率需要变更 81 | noticeServiceWorkerByContent(NoticeKey.BADGE_STATUS, customEvent.detail) 82 | }, 83 | false 84 | ); 85 | 86 | }) 87 | 88 | 89 | -------------------------------------------------------------------------------- /packages/shell-chrome/src/document.ts: -------------------------------------------------------------------------------- 1 | // console.log("Ajax proxy document.js"); 2 | 3 | import lib from "@proxy/lib"; 4 | import { NoticeFrom, NoticeTo, NoticeKey, StorageKey } from "@proxy/shared-utils"; 5 | import { NOTICE_KEY_REFRESH_GLOBAL_STATE } from "./consts"; 6 | 7 | window.addEventListener( 8 | "message", 9 | function (event) { 10 | const data = event.data; 11 | let globalSwitch = false 12 | if (data.from === NoticeFrom.CONTENT && data.to === NoticeTo.DOCUMENT) { 13 | switch (data.key) { 14 | // 更新全部数据 15 | // content 首次加载时 16 | case NOTICE_KEY_REFRESH_GLOBAL_STATE: 17 | const { 18 | GLOBAL_SWITCH, 19 | MODE, 20 | INTERCEPT_LIST, 21 | REDIRECT_LIST, 22 | } = StorageKey 23 | // 获取 映射 24 | // 设置默认值 25 | const { 26 | [GLOBAL_SWITCH]: global_on = false, 27 | [MODE]: mode = 'interceptor', 28 | [INTERCEPT_LIST]: interceptor_matching_content = [], 29 | [REDIRECT_LIST]: redirector_matching_content = [], 30 | } = data.value || {} 31 | lib.update({ global_on, mode, interceptor_matching_content, redirector_matching_content }) 32 | break; 33 | // 全局开关 34 | case NoticeKey.GLOBAL_SWITCH: 35 | globalSwitch = data.value; 36 | lib.update(globalSwitch) 37 | break; 38 | // 模式切换 39 | case NoticeKey.MODE: 40 | lib.update(data.value) 41 | break; 42 | // 拦截器列表 43 | case NoticeKey.INTERCEPT_LIST: 44 | lib.update(data.value) 45 | break; 46 | // 重定向列表 47 | case NoticeKey.REDIRECT_LIST: 48 | lib.update(data.value) 49 | break; 50 | } 51 | } 52 | }, 53 | false 54 | ); 55 | -------------------------------------------------------------------------------- /packages/shell-chrome/src/service-worker/badge.ts: -------------------------------------------------------------------------------- 1 | 2 | // 和徽章相关的函数 3 | 4 | import { IRequestMethod } from "@proxy/lib"; 5 | import { NoticeKey, StorageKey, setStorage, getRealStorage, noticePanelsByServiceWorker } from "@proxy/shared-utils"; 6 | import { chromeNativeNotice } from "./notice"; 7 | 8 | // 同步 命中率 9 | async function syncRoutesAsHit(routes, match_url, method) { 10 | const list = routes || []; 11 | // 总命中率 12 | let counter = 0; 13 | for (let i = 0; i < list.length; i++) { 14 | const target = list[i]; 15 | if (target.switch_on) { 16 | // target 可能没有 method 属性时,设置默认值 17 | const targetMethod = target.method || "ANY" 18 | // 如果match_url 和method 匹配 则叠加当前命中率 19 | if ( 20 | target.match_url === match_url && 21 | (targetMethod === "ANY" || targetMethod === method) 22 | ) { 23 | const totalHit = target.hit ? target.hit + 1 : 1; 24 | target.hit = totalHit; 25 | counter += totalHit; 26 | 27 | // 这里开始统计某个接口命中次数是否存在过多 28 | const LIMIT = 100; 29 | // 命中次数 太多,通知一下 30 | let tooHigh = totalHit === LIMIT; 31 | // 每隔20次提醒一下 32 | if (!tooHigh && totalHit > LIMIT) { 33 | tooHigh = (totalHit - LIMIT) % 20 === 0; 34 | } 35 | if (tooHigh) { 36 | const message = [target.match_url, target.remark || ""].join("\n"); 37 | const lang = await getRealStorage(StorageKey.LANGUAGE, "en"); 38 | const i18n = { 39 | en: "Too many interceptions", 40 | zh: "拦截次数过多", 41 | }; 42 | chromeNativeNotice({ 43 | title: i18n[lang] || i18n.en, 44 | message, 45 | }); 46 | } 47 | } else if (target.hit) { 48 | // 汇总其他match_url上 hit 49 | counter += target.hit; 50 | } 51 | } 52 | } 53 | // 更新本地拦截列表 54 | setStorage(StorageKey.INTERCEPT_LIST, list); 55 | return counter; 56 | } 57 | 58 | /**当前命中数据的结构体 */ 59 | type BadgeHit = { 60 | match_url: string 61 | method: IRequestMethod 62 | } 63 | // badge 右下角小徽章设置 64 | export async function chromeBadge(data?: BadgeHit) { 65 | const { match_url, method } = data || {} 66 | const globalSwitchOn = await getRealStorage(StorageKey.GLOBAL_SWITCH, false); 67 | if (!globalSwitchOn) { 68 | chrome.action.setBadgeText({ text: "" }); 69 | return; 70 | } 71 | // 判断模式 72 | const mode = await getRealStorage(StorageKey.MODE, 'interceptor'); 73 | // 如果是重定向 74 | if (mode === "redirector") { 75 | chrome.action.setBadgeBackgroundColor({ color: "#006d75" }); 76 | chrome.action.setBadgeText({ text: "R" }); 77 | return; 78 | } 79 | // 拦截器模式颜色 80 | chrome.action.setBadgeBackgroundColor({ color: "#F56C6C" }); 81 | const interceptList = await getRealStorage(StorageKey.INTERCEPT_LIST, []); 82 | // 如果没有需要拦截的数据时,设置默认值 83 | if (interceptList.length === 0) { 84 | chrome.action.setBadgeText({ text: "" }); 85 | return; 86 | } 87 | 88 | const counter = await syncRoutesAsHit(interceptList, match_url, method) 89 | // 当计算完成,且 参数存在时证明 hit 属性已经做过叠加,需要通知到 panels变更列表 hit 数据 90 | if (match_url && method) { 91 | // 通知 panels 当前 match_url & method 的条件下已经命中,hit 属性已经变更 需要更新table 列表 92 | noticePanelsByServiceWorker(NoticeKey.HIT_RATE) 93 | } 94 | if (counter) chrome.action.setBadgeText({ text: `+${counter}` }); 95 | else chrome.action.setBadgeText({ text: "" }); 96 | } -------------------------------------------------------------------------------- /packages/shell-chrome/src/service-worker/event.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPanel, 3 | closePanel, 4 | fullScreenPanel, 5 | resizeWindow, 6 | } from "./panel"; 7 | 8 | // 监听图标点击事件 9 | function onIconClickListener() { 10 | chrome.action.onClicked.addListener(function (tab) { 11 | createPanel(); 12 | }); 13 | } 14 | 15 | /** 16 | * 监听快捷键 17 | * 在manifest.json中定义 commands 18 | */ 19 | function onCommandListener() { 20 | // commands 21 | chrome.commands.onCommand.addListener(function (command) { 22 | switch (command) { 23 | case "open_panel": 24 | createPanel(); 25 | break; 26 | case "close_panel": 27 | closePanel(); 28 | break; 29 | case "full_screen": 30 | fullScreenPanel(); 31 | break; 32 | case "resize_window": 33 | resizeWindow(); 34 | break; 35 | default: 36 | break; 37 | } 38 | }); 39 | } 40 | 41 | /**注册全部监听列表 */ 42 | export function injectEventListener() { 43 | // 监听图标点击事件 44 | onIconClickListener() 45 | // 监听快捷键 46 | onCommandListener() 47 | } -------------------------------------------------------------------------------- /packages/shell-chrome/src/service-worker/index.ts: -------------------------------------------------------------------------------- 1 | 2 | // console.log("Ajax proxy service_worker.js") 3 | 4 | import { 5 | NoticeFrom, 6 | NoticeTo, 7 | NoticeKey, 8 | initStorage, 9 | noticePanelsByServiceWorker, 10 | } from "@proxy/shared-utils"; 11 | import { injectEventListener } from "./event"; 12 | import { noticeContent, useCurrentTitle } from "./notice"; 13 | import { initDefaultSth } from "./init"; 14 | import { chromeBadge } from "./badge"; 15 | import { INIT_CURRENT_TITLE } from "../consts"; 16 | 17 | initStorage().then(() => { 18 | // 接收content 和 panels 传来的信息 19 | chrome.runtime.onMessage.addListener((msg) => { 20 | 21 | if (msg.to === NoticeTo.SERVICE_WORKER) { 22 | const { key, value } = msg 23 | // 徽章 24 | if ([NoticeFrom.CONTENT, NoticeFrom.PANELS].includes(msg.from)) { 25 | // 当 panels 清空某个命中统计时 26 | // 重新统计一下全局命中率 27 | if (key === NoticeKey.BADGE_STATUS) chromeBadge(value) 28 | } 29 | 30 | if (msg.from === NoticeFrom.PANELS) { 31 | // 判断启用状态 32 | if (key === NoticeKey.GLOBAL_SWITCH) { 33 | chrome.action.setIcon({ 34 | path: value ? "icons/128.png" : "icons/128g.png", 35 | }); 36 | // 更新一下图标状态 37 | chromeBadge() 38 | noticeContent(key, value) 39 | } 40 | // 模式切换 41 | if (key === NoticeKey.MODE) { 42 | chromeBadge(); 43 | noticeContent(key, value) 44 | } 45 | // 拦截器列表 46 | if (key === NoticeKey.INTERCEPT_LIST) { 47 | chromeBadge(); 48 | noticeContent(key, value) 49 | } 50 | // 重定向列表 51 | if (key === NoticeKey.REDIRECT_LIST) { 52 | noticeContent(key, value) 53 | } 54 | // 获取当前document.title [被动] 55 | // panels -> service-worker -> connect.port.sender.tab.title -> panels 56 | if (key === NoticeKey.GET_CURRENT_TITLE) { 57 | const title = useCurrentTitle() 58 | noticePanelsByServiceWorker(NoticeKey.GET_CURRENT_TITLE, title) 59 | } 60 | } else if (msg.from === NoticeFrom.CONTENT) { 61 | // content -> service-worker -> panels 62 | // 页面加载初始化当前title [主动] 63 | if (key === INIT_CURRENT_TITLE) { 64 | // 接收content转发给panels 65 | noticePanelsByServiceWorker(NoticeKey.GET_CURRENT_TITLE, value) 66 | } 67 | } 68 | } 69 | }); 70 | 71 | // 设置默认项 72 | initDefaultSth() 73 | 74 | // 注册其他监听列表 75 | injectEventListener() 76 | }) 77 | -------------------------------------------------------------------------------- /packages/shell-chrome/src/service-worker/init.ts: -------------------------------------------------------------------------------- 1 | import { getRealStorage, StorageKey } from "@proxy/shared-utils"; 2 | import { chromeBadge } from "./badge"; 3 | 4 | /**设置默认项 */ 5 | export async function initDefaultSth() { 6 | // 设置默认icon 7 | const bool = await getRealStorage(StorageKey.GLOBAL_SWITCH, false) 8 | if (bool) chrome.action.setIcon({ path: "icons/128.png" }); 9 | else chrome.action.setIcon({ path: "icons/128g.png" }); 10 | 11 | // 初始化徽章状态 12 | chromeBadge() 13 | } -------------------------------------------------------------------------------- /packages/shell-chrome/src/service-worker/notice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NoticeKey, 3 | onConnectByServiceWorker, 4 | noticePanelsByServiceWorker, 5 | noticeContentByServiceWorker, 6 | onCurrentTabChanged 7 | } from "@proxy/shared-utils"; 8 | import { CONNECT_NAME } from "../consts"; 9 | import { createPanel } from "./panel"; 10 | 11 | /** 12 | * 可以定义多对多,但考虑此工具无需多个链接。 13 | * 多对多方式: 14 | * current_port = {} 15 | * ... 16 | * current_port[tabId].postMessage() 17 | */ 18 | let current_port: chrome.runtime.Port | undefined = undefined; 19 | 20 | // 长链接 21 | // 好处是可以实现无刷新更新拦截器代理 22 | // 弊端是每一个新的tab页都会更新current_port,旧的长链会注销 23 | onConnectByServiceWorker((port) => { 24 | if (port.name === CONNECT_NAME) { 25 | current_port = port 26 | } 27 | }, () => { 28 | // 长链接断开 29 | current_port = undefined 30 | // 通知 panels 清空 title 31 | noticePanelsByServiceWorker(NoticeKey.GET_CURRENT_TITLE, "") 32 | }) 33 | 34 | // 当页签发生改变时,通知panels 变更title 35 | onCurrentTabChanged((tab) => { 36 | noticePanelsByServiceWorker(NoticeKey.GET_CURRENT_TITLE, tab.title) 37 | }) 38 | 39 | /** 40 | * 通知 content 41 | */ 42 | export const noticeContent = (key, value) => noticeContentByServiceWorker(current_port, key, value) 43 | 44 | /**获取 port里 title */ 45 | export function useCurrentTitle() { 46 | return current_port?.sender?.tab?.title 47 | } 48 | 49 | /** 50 | * 系统通知 51 | * 当命中率过高时 52 | */ 53 | export function chromeNativeNotice({ title, message }) { 54 | const notification = new Notification(title, { 55 | icon: "icons/128.png", 56 | body: message, 57 | }); 58 | notification.onclick = function () { 59 | createPanel(); 60 | notification.close(); 61 | }; 62 | } -------------------------------------------------------------------------------- /packages/shell-chrome/src/service-worker/panel.ts: -------------------------------------------------------------------------------- 1 | 2 | let current_window_id: number | undefined 3 | 4 | /**获取所有windowId */ 5 | async function getAllWindowIds(): Promise<(number | undefined)[]> { 6 | return new Promise((resolve) => { 7 | chrome.windows.getAll(function (targets) { 8 | const ids = targets.map((item) => item.id); 9 | resolve(ids); 10 | }); 11 | }); 12 | } 13 | 14 | /**创建视图 */ 15 | export async function createPanel() { 16 | const _createFunc = function () { 17 | // https://developer.chrome.com/docs/extensions/reference/windows/ 18 | chrome.windows.create( 19 | { 20 | url: "panels/index.html", 21 | type: "popup", 22 | width: 1300, 23 | height: 750, 24 | top: 30, 25 | left: 150, 26 | }, 27 | function (target) { 28 | // bugfix: id undefined 问题 29 | if (target) current_window_id = target.id 30 | } 31 | ); 32 | }; 33 | if (!current_window_id) { 34 | _createFunc(); 35 | } else { 36 | // 获取所有窗口id,判断cacheId是否存在 37 | // 如果已经存在则置前 38 | const ids = await getAllWindowIds(); 39 | const exist = ids.some((item) => item === current_window_id); 40 | if (exist) { 41 | chrome.windows.update(current_window_id, { focused: true }); 42 | return; 43 | } 44 | // 不存在,则重新创建,刷新cacheId 45 | _createFunc(); 46 | } 47 | } 48 | 49 | /**关闭视图 */ 50 | export async function closePanel() { 51 | if (current_window_id) { 52 | chrome.windows.remove(current_window_id); 53 | current_window_id = undefined 54 | } 55 | } 56 | 57 | /**全屏 */ 58 | export async function fullScreenPanel() { 59 | if (current_window_id) { 60 | chrome.windows.getCurrent(function (current) { 61 | if (current.id && current.id === current_window_id) { 62 | switch (current.state) { 63 | case "fullscreen": 64 | chrome.windows.update(current.id, { state: "normal" }); 65 | break; 66 | default: 67 | chrome.windows.update(current.id, { state: "fullscreen" }); 68 | break; 69 | } 70 | } 71 | }); 72 | } 73 | } 74 | 75 | /**修改 panels 窗口大小 */ 76 | export async function resizeWindow() { 77 | if (current_window_id) { 78 | chrome.windows.getCurrent(function (current) { 79 | // normal", "minimized", "maximized", or "fullscreen" 80 | const conf = { 81 | normal: "maximized", 82 | maximized: "fullscreen", 83 | fullscreen: "normal", 84 | }; 85 | if (current.id && current.state && current.id === current_window_id) 86 | chrome.windows.update(current.id, { state: conf[current.state] }); 87 | }); 88 | } 89 | } -------------------------------------------------------------------------------- /packages/shell-chrome/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "types": [ 11 | "chrome" 12 | ], 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "sourceMap": true, 16 | "preserveWatchOutput": true, 17 | // Strict 18 | "noImplicitAny": false, 19 | "noImplicitThis": true, 20 | "alwaysStrict": true, 21 | "strictBindCallApply": true, 22 | "strictFunctionTypes": true 23 | }, 24 | "include": [ 25 | "src/**/*", 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } -------------------------------------------------------------------------------- /packages/shell-chrome/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | 4 | const staticFiles = ['manifest.json', 'icons/*'].map(file => { 5 | return { 6 | from: file, 7 | to: '.' 8 | } 9 | }); 10 | 11 | module.exports = { 12 | mode: 'production', 13 | entry: { 14 | content: resolve(__dirname, "src/content.ts"), 15 | document: resolve(__dirname, "src/document.ts"), 16 | service_worker: resolve(__dirname, "src/service-worker/index.ts"), 17 | }, 18 | output: { 19 | path: resolve(__dirname, 'build'), 20 | filename: '[name].js', 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.ts$/, 26 | use: 'ts-loader', 27 | exclude: /node_modules/ 28 | } 29 | ] 30 | }, 31 | resolve: { 32 | // 需要打包的文件后缀 33 | extensions: [".ts", ".js"] 34 | }, 35 | plugins: [ 36 | new CopyPlugin( 37 | { 38 | patterns: staticFiles, 39 | } 40 | ), 41 | ], 42 | // devtool: process.env.NODE_ENV !== 'production' 43 | // ? 'inline-source-map' 44 | // : false, 45 | } 46 | -------------------------------------------------------------------------------- /packages/vue-panels/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.displayLanguage": "zh", 3 | "i18n-ally.dirStructure": "auto", 4 | "i18n-ally.disablePathParsing": false, 5 | "i18n-ally.localesPaths": [ 6 | "src/lang" 7 | ], 8 | "i18n-ally.enabledParsers": [ 9 | "js" 10 | ], 11 | "i18n-ally.annotationInPlace": false, 12 | "i18n-ally.autoDetection": true, 13 | "i18n-ally.keystyle": "nested" 14 | } -------------------------------------------------------------------------------- /packages/vue-panels/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 g0ngjie 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 | -------------------------------------------------------------------------------- /packages/vue-panels/README.md: -------------------------------------------------------------------------------- 1 | # ajax-intercepter-views 2 | 3 | ![examples](https://github.com/g0ngjie/ajax-proxy/wiki/images/zhihu-ajaxproxy.png) 4 | 5 | ### 插件地址 6 | [Github](https://github.com/g0ngjie/ajax-proxy) 7 | ### 使用示例 8 | 9 | > 视频使用示例 10 | 11 | [BiliBili](https://www.bilibili.com/video/BV1rf4y1p749/) -------------------------------------------------------------------------------- /packages/vue-panels/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ], 5 | plugins: [ 6 | [ 7 | "component", 8 | { 9 | "libraryName": "element-ui", 10 | "styleLibraryName": "theme-chalk" 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/vue-panels/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/vue-panels/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@proxy/vue-panels", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "watch": "vue-cli-service build --watch --no-clean --mode production" 9 | }, 10 | "dependencies": { 11 | "@alrale/common-lib": "^1.0.49", 12 | "@proxy/code-editor": "workspace:^1.0.0", 13 | "@proxy/compatibility": "workspace:^1.0.0", 14 | "@proxy/json-editor": "workspace:^1.0.0", 15 | "@proxy/shared-utils": "workspace:^1.0.0", 16 | "core-js": "^3.6.5", 17 | "element-ui": "~2.15.1", 18 | "export-from-json": "^1.7.0", 19 | "vue": "^2.6.11", 20 | "vue-i18n": "^8.24.3" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "~4.5.0", 24 | "@vue/cli-plugin-router": "~4.5.0", 25 | "@vue/cli-plugin-vuex": "~4.5.0", 26 | "@vue/cli-service": "~4.5.0", 27 | "babel-plugin-component": "^1.1.1", 28 | "sass": "~1.26.5", 29 | "sass-loader": "~8.0.2", 30 | "vue-template-compiler": "^2.6.11" 31 | }, 32 | "browserslist": [ 33 | "> 1%", 34 | "last 2 versions", 35 | "not dead" 36 | ] 37 | } -------------------------------------------------------------------------------- /packages/vue-panels/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/packages/vue-panels/public/favicon.ico -------------------------------------------------------------------------------- /packages/vue-panels/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Ajax Proxy 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/vue-panels/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 12 | 13 | 23 | 29 | -------------------------------------------------------------------------------- /packages/vue-panels/src/common/element-plugin.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import { 3 | Dialog, 4 | Input, 5 | Switch, 6 | Select, 7 | Option, 8 | Button, 9 | RadioGroup, 10 | RadioButton, 11 | Table, 12 | TableColumn, 13 | Tooltip, 14 | Form, 15 | FormItem, 16 | Upload, 17 | MessageBox, 18 | Message, 19 | Alert, 20 | Drawer, 21 | Tag, 22 | Row, 23 | Col, 24 | Tabs, 25 | TabPane, 26 | } from "element-ui"; 27 | 28 | Vue.use(Dialog); 29 | Vue.use(Input); 30 | Vue.use(Switch); 31 | Vue.use(Select); 32 | Vue.use(Option); 33 | Vue.use(Button); 34 | Vue.use(RadioGroup); 35 | Vue.use(RadioButton); 36 | Vue.use(Table); 37 | Vue.use(TableColumn); 38 | Vue.use(Tooltip); 39 | Vue.use(Form); 40 | Vue.use(FormItem); 41 | Vue.use(Upload); 42 | Vue.use(Alert); 43 | Vue.use(Drawer); 44 | Vue.use(Tag); 45 | Vue.use(Row); 46 | Vue.use(Col); 47 | Vue.use(Tabs); 48 | Vue.use(TabPane); 49 | 50 | Vue.prototype.$msgbox = MessageBox; 51 | Vue.prototype.$alert = MessageBox.alert; 52 | Vue.prototype.$confirm = MessageBox.confirm; 53 | Vue.prototype.$prompt = MessageBox.prompt; 54 | Vue.prototype.$message = Message; 55 | -------------------------------------------------------------------------------- /packages/vue-panels/src/common/index.js: -------------------------------------------------------------------------------- 1 | import { MessageBox } from "element-ui"; 2 | 3 | /** 4 | * 确认弹窗 5 | * @returns 6 | */ 7 | export async function confirmFunc({ 8 | message = "", 9 | title = "", 10 | confirmText, 11 | cancelText, 12 | type, 13 | }) { 14 | const res = await MessageBox.confirm(message, title, { 15 | confirmButtonText: confirmText, 16 | cancelButtonText: cancelText, 17 | type, 18 | }).catch(() => { 19 | return { 20 | ok: false, 21 | }; 22 | }); 23 | return { 24 | ok: res === "confirm", 25 | }; 26 | } 27 | 28 | /**提交内容 */ 29 | export async function promptFunc({ 30 | message = "", 31 | title = "", 32 | inputValue = "", 33 | }) { 34 | const { action, value } = await MessageBox.prompt(message, title, { 35 | inputValue, 36 | }).catch(() => { 37 | return { 38 | ok: false, 39 | }; 40 | }); 41 | return { 42 | ok: action === "confirm", 43 | data: value, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /packages/vue-panels/src/common/notice.js: -------------------------------------------------------------------------------- 1 | 2 | import { noticeServiceWorkerByPanels, NoticeKey } from "@proxy/shared-utils"; 3 | 4 | /**通知 */ 5 | export const useNotice = { 6 | /**全局开关 */ 7 | globalSwitchOn: (value) => noticeServiceWorkerByPanels(NoticeKey.GLOBAL_SWITCH, value), 8 | /**同步拦截路由列表 */ 9 | changeIntercepts: (routes) => noticeServiceWorkerByPanels(NoticeKey.INTERCEPT_LIST, routes), 10 | /**通知徽章上命中率需要变更 */ 11 | changeBadge: () => noticeServiceWorkerByPanels(NoticeKey.BADGE_STATUS, null), 12 | /**同步给 @proxy/lib 模式 */ 13 | changeMode: (value) => noticeServiceWorkerByPanels(NoticeKey.MODE, value), 14 | /**同步给 @proxy/lib 重定向列表 */ 15 | changeRedirects: (value) => noticeServiceWorkerByPanels(NoticeKey.REDIRECT_LIST, value), 16 | /**获取当前title */ 17 | getCurrentTitle: () => noticeServiceWorkerByPanels(NoticeKey.GET_CURRENT_TITLE), 18 | } -------------------------------------------------------------------------------- /packages/vue-panels/src/common/store.js: -------------------------------------------------------------------------------- 1 | 2 | import { setStorage, getStorage, getRealStorage, StorageKey, getStorageAll } from "@proxy/shared-utils"; 3 | 4 | /**获取所有 */ 5 | export async function getStoreAll() { 6 | const getData = await getStorageAll() 7 | const { LANGUAGE, GLOBAL_SWITCH, MODE, TAGS, INTERCEPT_LIST, REDIRECT_LIST } = StorageKey 8 | const { 9 | [LANGUAGE]: language = 'en', 10 | [GLOBAL_SWITCH]: globalSwitch = false, 11 | [MODE]: mode = 'interceptor', 12 | [TAGS]: tags = [], 13 | [INTERCEPT_LIST]: interceptors = [], 14 | [REDIRECT_LIST]: redirectors = [], 15 | } = getData || {} 16 | 17 | return { 18 | ok: true, 19 | data: { language, globalSwitch, mode, tags, interceptors, redirectors } 20 | } 21 | } 22 | 23 | /**多语言设置 */ 24 | export const useLang = { 25 | get() { 26 | return getStorage(StorageKey.LANGUAGE, 'en') 27 | }, 28 | set(value) { 29 | setStorage(StorageKey.LANGUAGE, value) 30 | } 31 | } 32 | 33 | /**同步全局开关 */ 34 | export const useGLobalSwitch = { 35 | get() { 36 | return getStorage(StorageKey.GLOBAL_SWITCH, false) 37 | }, 38 | set(value) { 39 | setStorage(StorageKey.GLOBAL_SWITCH, value) 40 | } 41 | } 42 | 43 | /**拦截路由列表 */ 44 | export const useInterceptorRoutes = { 45 | // 注意,panels 与 service-worker 使用的storage 是不同的实例 46 | // 命中率更新 是在 service-worker 里面直接进行数据存储的 47 | // 所以这里面不能用 getStorage,因为这里面会走缓存,导致无法拿到最新的数据 48 | getReal() { 49 | return getRealStorage(StorageKey.INTERCEPT_LIST, []) 50 | }, 51 | set(value) { 52 | setStorage(StorageKey.INTERCEPT_LIST, value) 53 | } 54 | } 55 | 56 | /**Tag列表 */ 57 | export const useTags = { 58 | get() { 59 | return getStorage(StorageKey.TAGS, []) 60 | }, 61 | set(value) { 62 | setStorage(StorageKey.TAGS, value) 63 | } 64 | } 65 | 66 | /**模式状态 */ 67 | export const useMode = { 68 | get() { 69 | return getStorage(StorageKey.MODE, "interceptor") 70 | }, 71 | set(value) { 72 | setStorage(StorageKey.MODE, value) 73 | } 74 | } 75 | 76 | /**重定向列表 */ 77 | export const useRedirects = { 78 | get() { 79 | return getStorage(StorageKey.REDIRECT_LIST, []) 80 | }, 81 | set(value) { 82 | setStorage(StorageKey.REDIRECT_LIST, value) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/vue-panels/src/index.scss: -------------------------------------------------------------------------------- 1 | /* 改变主题色变量 */ 2 | $--color-primary: teal; 3 | 4 | /* 改变 icon 字体路径变量,必需 */ 5 | $--font-path: '~element-ui/lib/theme-chalk/fonts'; 6 | 7 | @import "~element-ui/packages/theme-chalk/src/index"; -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | interceptor: "Interceptor", 3 | redirector: "Redirector", 4 | redirect: "Redirect", 5 | create: "Create", 6 | edit: "Edit", 7 | del: "Delete", 8 | copy: "Copy", 9 | path: 'Match', 10 | matchPath: 'Match', 11 | normal: "Normal", 12 | regex: "Regex", 13 | responseData: 'ResponseText', 14 | responseFunc: 'Function', 15 | remark: 'Remark', 16 | tag: 'Tag', 17 | statusCode: 'StatusCode', 18 | domain: "Domain", 19 | updateRequestHeaders: "Request header modificatio", 20 | option: "Option", 21 | append: "Append", 22 | describe: "Description", 23 | exclusionList: "Exclusion lis", 24 | noEmpty: "Cannot be empty", 25 | confirm: "OK", 26 | cancel: "Cancel", 27 | searchTxt: "Search", 28 | reset: "Reset", 29 | status: 'Status', 30 | matchType: "Match type", 31 | hit: "Hit", 32 | handle: 'Options', 33 | restore: "Restore", 34 | backup: "Backup", 35 | msg: { 36 | pathNotEmpty: 'match is required', 37 | responseDataNotEmpty: 'response text is required', 38 | domainNotEmpty: 'Domain name information cannot be empty', 39 | redirectNotEmpty: 'The redirection address cannot be empty', 40 | keyNotEmpty: 'key cannot be empty', 41 | valueNotEmpty: 'value cannot be empty', 42 | jsonFormatError: 'JSON format error', 43 | confirmDeletion: 'Are you sure you want to delete?', 44 | noDataToDownload: 'There is no data to download', 45 | overrideData: 'If the upload is successful, the original file will be overwritten', 46 | readJsonErr: 'Read exception. The file may not be a JSON', 47 | dataErr: 'Data exception', 48 | formatErr: 'Format error', 49 | importEmpty: 'You imported an empty list', 50 | existsEmptyString: 'Empty string exists', 51 | }, 52 | placeholder: { 53 | select: 'Please select', 54 | input: 'Please input', 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/fr.js: -------------------------------------------------------------------------------- 1 | // 法文 2 | export default { 3 | interceptor: "Intercepteur", 4 | redirector: "Redirection", 5 | redirect: "Redirection", 6 | create: "Nouveau", 7 | edit: "Édition", 8 | del: "Supprimer", 9 | copy: "Copier", 10 | path: 'Le chemin', 11 | matchPath: 'Correspondance', 12 | normal: "Fréquent", 13 | regex: "Regular", 14 | responseData: 'Données de réponse', 15 | responseFunc: 'Sur mesure', 16 | remark: 'Remarques', 17 | tag: 'Étiquettes', 18 | statusCode: "Code d'état", 19 | domain: "Nom de domaine", 20 | updateRequestHeaders: "Modification de l'en - tête de la demande", 21 | option: "Options", 22 | append: "Ajouter", 23 | describe: "Description", 24 | exclusionList: "Liste d'exclusion", 25 | noEmpty: "Ne peut pas être vide", 26 | confirm: "C'est sûr.", 27 | cancel: "Annulation", 28 | searchTxt: "Recherche", 29 | reset: "Réinitialiser", 30 | status: 'Statut', 31 | matchType: "Type de correspondance", 32 | hit: "Hit", 33 | handle: 'Fonctionnement', 34 | restore: "Reprise", 35 | backup: "Sauvegarde", 36 | msg: { 37 | pathNotEmpty: 'Le chemin de requête ne peut pas être vide', 38 | responseDataNotEmpty: 'Les données de réponse ne peuvent pas être vides', 39 | domainNotEmpty: 'Les informations sur le nom de domaine ne peuvent pas être vides', 40 | redirectNotEmpty: "L'adresse de redirection ne peut pas être vide", 41 | keyNotEmpty: "La valeur de la clé ne peut pas être vide", 42 | valueNotEmpty: 'La valeur ne peut pas être vide', 43 | jsonFormatError: 'Erreur de format json', 44 | confirmDeletion: 'Confirmer la suppression?', 45 | noDataToDownload: 'Aucune donnée à télécharger', 46 | overrideData: 'Le fichier original sera écrasé si le téléchargement réussit', 47 | readJsonErr: 'Exception de lecture, le fichier peut ne pas être un json', 48 | dataErr: 'Données anormales', 49 | formatErr: 'Mauvais format', 50 | importEmpty: 'Vous avez importé une liste vide', 51 | existsEmptyString: 'Présence d\'une chaîne vide', 52 | }, 53 | placeholder: { 54 | select: 'Veuillez sélectionner', 55 | input: 'Veuillez entrer', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | import ElementLocale from 'element-ui/lib/locale' 5 | import enLocale from 'element-ui/lib/locale/lang/en' 6 | import zhLocale from 'element-ui/lib/locale/lang/zh-CN' 7 | import twLocale from 'element-ui/lib/locale/lang/zh-TW' 8 | // 日本语 9 | import jaLocale from 'element-ui/lib/locale/lang/ja' 10 | // 法语 11 | import frLocale from 'element-ui/lib/locale/lang/fr' 12 | // 韩语 13 | import koLocale from 'element-ui/lib/locale/lang/ko' 14 | // 俄语 15 | import ruLocale from 'element-ui/lib/locale/lang/ru-RU' 16 | 17 | import langZh from "./zh_CN.js" 18 | import langEN from "./en.js" 19 | import langTW from "./zh_TW.js" 20 | import langJA from "./ja.js" 21 | import langFR from "./fr.js" 22 | import langKO from "./ko.js" 23 | import langRU from "./ru.js" 24 | // 爱尔兰 25 | import langIE from "./ireland.js" 26 | 27 | Vue.use(VueI18n) 28 | 29 | const i18n = new VueI18n({ 30 | locale: 'en', 31 | messages: { 32 | 'zh': { ...langZh, ...zhLocale }, 33 | 'en': { ...langEN, ...enLocale }, 34 | 'tw': { ...langTW, ...twLocale }, 35 | 'ja': { ...langJA, ...jaLocale }, 36 | 'fr': { ...langFR, ...frLocale }, 37 | 'ko': { ...langKO, ...koLocale }, 38 | 'ru': { ...langRU, ...ruLocale }, 39 | 'ie': { ...langIE, ...enLocale } 40 | } 41 | }) 42 | ElementLocale.i18n((key, value) => i18n.t(key, value)) 43 | 44 | export default i18n 45 | 46 | export const Langs = [ 47 | { value: 'en', label: 'English' }, 48 | { value: 'zh', label: '简体中文' }, 49 | { value: 'tw', label: '繁體中文' }, 50 | { value: 'ja', label: '日本語' }, 51 | { value: 'fr', label: 'Français' }, 52 | { value: 'ko', label: '한국어' }, 53 | { value: 'ru', label: 'Русский' }, 54 | { value: 'ie', label: 'Gaeilge' } 55 | ] -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/ireland.js: -------------------------------------------------------------------------------- 1 | // 爱尔兰 2 | export default { 3 | interceptor: "Interceptor", 4 | redirector: "athdhírigh", 5 | redirect: "athdhírigh", 6 | create: "tógáil nua", 7 | edit: "eagar", 8 | del: "Scrios", 9 | copy: "cóipeáil", 10 | path: 'ruta', 11 | matchPath: 'Path matching', 12 | normal: "gnáth", 13 | regex: "rialta", 14 | responseData: 'Sonraí freagra', 15 | responseFunc: 'feidhm', 16 | remark: 'ráitis', 17 | tag: 'label', 18 | statusCode: 'Cód stádais', 19 | domain: "domain name", 20 | updateRequestHeaders: "Request header modification", 21 | option: "rogha", 22 | append: "Cuir leis", 23 | describe: "tuairisc", 24 | exclusionList: "Liosta eisiamh", 25 | noEmpty: "Ní féidir a bheith folamh", 26 | confirm: "cinneadh", 27 | cancel: "Cealaigh", 28 | searchTxt: "cuardaigh", 29 | reset: "Athshocraigh", 30 | status: 'Stádas', 31 | matchType: "Cineál comhoiriúnach", 32 | hit: "Cruthaigh", 33 | handle: 'oibríocht', 34 | restore: "athshlánú", 35 | backup: "cúltaca", 36 | msg: { 37 | pathNotEmpty: 'Ní féidir conair an iarratais a bheith folamh', 38 | responseDataNotEmpty: 'Ní féidir sonraí freagra a bheith folamh', 39 | domainNotEmpty: 'Ní féidir eolas faoi ainm an fhearainn a bheith folamh', 40 | redirectNotEmpty: 'Ní féidir seoladh athdhírithe a bheith folamh', 41 | keyNotEmpty: 'Ní féidir an luach eochrach a bheith folamh', 42 | valueNotEmpty: 'Ní féidir luach a bheith folamh', 43 | jsonFormatError: 'Earráid fhormáide JSON', 44 | confirmDeletion: 'deimhnigh scriosadh?', 45 | noDataToDownload: 'Gan sonraí le híosluchtú', 46 | overrideData: 'Má tá an t- uasluchtú rathúil, sárófar an bunchomhad', 47 | readJsonErr: 'Reading exception. The file may not be a JSON', 48 | dataErr: 'Eisceachta sonraí', 49 | formatErr: 'Earráid fhormáide', 50 | importEmpty: 'Iompórtáil tú liosta folamh', 51 | existsEmptyString: 'Tá teaghrán folamh ann', 52 | }, 53 | placeholder: { 54 | select: 'Roghnaigh le do thoil', 55 | input: 'Iontráil le do thoil', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/ja.js: -------------------------------------------------------------------------------- 1 | // 日语 2 | export default { 3 | interceptor: "ブロッキング", 4 | redirector: "リダイレクト", 5 | redirect: "リダイレクト", 6 | create: "新規作成", 7 | edit: "編集", 8 | del: "削除", 9 | copy: "コピー", 10 | path: 'パス', 11 | matchPath: 'パスマッチング', 12 | normal: "通常", 13 | regex: "正則", 14 | responseData: 'レスポンスデータ', 15 | responseFunc: 'ビスポーク', 16 | remark: 'コメント', 17 | tag: 'タブ', 18 | statusCode: 'じょうたいコード', 19 | domain: "ドメイン名", 20 | updateRequestHeaders: "要求ヘッダの変更", 21 | option: "オプション", 22 | append: "追加", 23 | describe: "説明", 24 | exclusionList: "除外リスト", 25 | noEmpty: "空にできません", 26 | confirm: "を選択します。", 27 | cancel: "キャンセル", 28 | searchTxt: "検索", 29 | reset: "リセット", 30 | status: '状態', 31 | matchType: "照合タイプ", 32 | hit: "ヒット", 33 | handle: '操作', 34 | restore: "リカバリ", 35 | backup: "バックアップ", 36 | msg: { 37 | pathNotEmpty: '要求パスは空にできません', 38 | responseDataNotEmpty: '応答データを空にすることはできません', 39 | domainNotEmpty: 'ドメイン名情報は空にできません', 40 | redirectNotEmpty: 'リダイレクトアドレスは空にできません', 41 | keyNotEmpty: 'key値は空にできません', 42 | valueNotEmpty: 'value値は空にできません', 43 | jsonFormatError: 'JSON形式エラー', 44 | confirmDeletion: '削除の確認?', 45 | noDataToDownload: 'ダウンロードできるデータがありません', 46 | overrideData: 'アップロードに成功すると、元のファイルが上書きされます。', 47 | readJsonErr: '読み込み異常、ファイルがJSONでない可能性があります', 48 | dataErr: 'データ異常', 49 | formatErr: 'フォーマットエラー', 50 | importEmpty: '空のリストをインポートしました', 51 | existsEmptyString: '空の文字列が存在します', 52 | }, 53 | placeholder: { 54 | select: '選択してください', 55 | input: '入力してください', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/ko.js: -------------------------------------------------------------------------------- 1 | // 韩语 2 | export default { 3 | interceptor: "차단기", 4 | redirector: "방향을 바꾸다", 5 | redirect: "방향을 바꾸다", 6 | create: "신설", 7 | edit: "편집자", 8 | del: "삭제", 9 | copy: "복제하다", 10 | path: '경로', 11 | matchPath: '경로 일치', 12 | normal: "여간하다", 13 | regex: "정칙", 14 | responseData: '응답 데이터', 15 | responseFunc: '사용자 정의', 16 | remark: '비고', 17 | tag: '라벨', 18 | statusCode: '상태 코드', 19 | domain: "도메인 이름", 20 | updateRequestHeaders: "헤더 수정 요청", 21 | option: "옵션", 22 | append: "덧붙이다", 23 | describe: "묘사", 24 | exclusionList: "명단을 제외하다", 25 | noEmpty: "공백일 수 없습니다.", 26 | confirm: "확정하다", 27 | cancel: "취소", 28 | searchTxt: "수색하다", 29 | reset: "재설정", 30 | status: '컨디션', 31 | matchType: "일치 유형", 32 | hit: "명중하다", 33 | handle: '조작하다', 34 | restore: "되살리다", 35 | backup: "백업", 36 | msg: { 37 | pathNotEmpty: '요청 경로가 비어 있을 수 없습니다', 38 | responseDataNotEmpty: '응답 데이터는 비워둘 수 없습니다.', 39 | domainNotEmpty: '도메인 이름은 비워 둘 수 없습니다.', 40 | redirectNotEmpty: '주소 리디렉션은 비워 둘 수 없습니다.', 41 | keyNotEmpty: '키 값은 비워둘 수 없습니다.', 42 | valueNotEmpty: 'value 값은 비워 둘 수 없습니다.', 43 | jsonFormatError: 'JSON 형식 오류', 44 | confirmDeletion: '삭제 확인?', 45 | noDataToDownload: '다운로드할 데이터가 없습니다.', 46 | overrideData: '업로드에 성공한 원본 파일을 덮어씁니다', 47 | readJsonErr: '읽기 예외, 파일이 JSON이 아닐 수 있음', 48 | dataErr: '데이터 예외', 49 | formatErr: '형식 오류', 50 | importEmpty: '빈 목록을 가져왔습니다', 51 | existsEmptyString: '빈 문자열 있음', 52 | }, 53 | placeholder: { 54 | select: '선택하십시오.', 55 | input: '입력하십시오.', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/ru.js: -------------------------------------------------------------------------------- 1 | // 俄语 2 | export default { 3 | interceptor: "перехватчик", 4 | redirector: "Перенаправление", 5 | redirect: "Перенаправление", 6 | create: "Добавить", 7 | edit: "редактор", 8 | del: "Удалить", 9 | copy: "копировать", 10 | path: 'путь', 11 | matchPath: 'согласование путей', 12 | normal: "обычный", 13 | regex: "регулярный", 14 | responseData: 'данные ответа', 15 | responseFunc: 'функция', 16 | remark: 'Примечание', 17 | tag: 'метка', 18 | statusCode: 'код состояния', 19 | domain: "доменное имя", 20 | updateRequestHeaders: "заголовок запроса", 21 | option: "Параметры", 22 | append: "Добавить", 23 | describe: "описание", 24 | exclusionList: "исключить список", 25 | noEmpty: "не может быть пустым", 26 | confirm: "определение", 27 | cancel: "отмена", 28 | searchTxt: "поиск", 29 | reset: "сбросить", 30 | status: 'состояние', 31 | matchType: "Тип согласования", 32 | hit: "попадание", 33 | handle: 'операция', 34 | restore: "восстановить", 35 | backup: "резервная копия", 36 | msg: { 37 | pathNotEmpty: 'путь запроса не может быть пустым', 38 | responseDataNotEmpty: 'данные ответа не могут быть пустыми', 39 | domainNotEmpty: 'Информация о домене не может быть пустой', 40 | redirectNotEmpty: 'адрес перенаправления не может быть пустым', 41 | keyNotEmpty: 'значение key не может быть пустым', 42 | valueNotEmpty: 'значение value не может быть пустым', 43 | jsonFormatError: 'ошибка формата JSON', 44 | confirmDeletion: 'Подтверждение удаления?', 45 | noDataToDownload: 'Нет данных, которые можно загрузить', 46 | overrideData: 'Файл будет перезаписан', 47 | readJsonErr: 'Ошибка чтения файла может быть не JSON', 48 | dataErr: 'аномалия данных', 49 | formatErr: 'ошибка формата', 50 | importEmpty: 'вы импортировали пустой список', 51 | existsEmptyString: 'Наличие пустых строк', 52 | }, 53 | placeholder: { 54 | select: 'Выберите', 55 | input: 'Введите пожалуйста', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/zh_CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | interceptor: "拦截器", 3 | redirector: "重定向", 4 | redirect: "重定向", 5 | create: "新增", 6 | edit: "编辑", 7 | del: "删除", 8 | copy: "复制", 9 | path: '路径', 10 | matchPath: '路径匹配', 11 | normal: "普通", 12 | regex: "正则", 13 | responseData: '响应数据', 14 | responseFunc: '函数式', 15 | remark: '备注', 16 | tag: '标签', 17 | statusCode: '状态码', 18 | domain: "域名", 19 | updateRequestHeaders: "请求头修改", 20 | option: "选项", 21 | append: "添加", 22 | describe: "描述", 23 | exclusionList: "排除名单", 24 | noEmpty: "不能为空", 25 | confirm: "确 定", 26 | cancel: "取 消", 27 | searchTxt: "搜索", 28 | reset: "重置", 29 | status: '状态', 30 | matchType: "匹配类型", 31 | hit: "命中", 32 | handle: '操作', 33 | restore: "恢复", 34 | backup: "备份", 35 | msg: { 36 | pathNotEmpty: '请求路径不能为空', 37 | responseDataNotEmpty: '响应数据不能为空', 38 | domainNotEmpty: '域名信息不能为空', 39 | redirectNotEmpty: '重定向地址不能为空', 40 | keyNotEmpty: 'key值不能为空', 41 | valueNotEmpty: 'value值不能为空', 42 | jsonFormatError: 'JSON格式错误', 43 | confirmDeletion: '确认删除?', 44 | noDataToDownload: '没有数据可以下载', 45 | overrideData: '上传成功原文件会被覆盖', 46 | readJsonErr: '读取异常,文件可能不是一个JSON', 47 | dataErr: '数据异常', 48 | formatErr: '格式错误', 49 | importEmpty: '你导入了一个空列表', 50 | existsEmptyString: '存在空字符串', 51 | }, 52 | placeholder: { 53 | select: '请选择', 54 | input: '请输入', 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/vue-panels/src/lang/zh_TW.js: -------------------------------------------------------------------------------- 1 | // 台湾 2 | export default { 3 | interceptor: "攔截器", 4 | redirector: "重定向", 5 | redirect: "重定向", 6 | create: "新增", 7 | edit: "編輯", 8 | del: "删除", 9 | copy: "複製", 10 | path: '路徑', 11 | matchPath: '路徑匹配', 12 | normal: "普通", 13 | regex: "正則", 14 | responseData: '響應數據', 15 | responseFunc: '函數式', 16 | remark: '備註', 17 | tag: '標籤', 18 | statusCode: '狀態碼', 19 | domain: "域名", 20 | updateRequestHeaders: "請求頭修改", 21 | option: "選項", 22 | append: "添加", 23 | describe: "描述", 24 | exclusionList: "排除名單", 25 | noEmpty: "不能為空", 26 | confirm: "確 定", 27 | cancel: "取 消", 28 | searchTxt: "蒐索", 29 | reset: "重置", 30 | status: '狀態', 31 | matchType: "匹配類型", 32 | hit: "命中", 33 | handle: '操作', 34 | restore: "恢復", 35 | backup: "備份", 36 | msg: { 37 | pathNotEmpty: '請求路徑不能為空', 38 | responseDataNotEmpty: '響應數據不能為空', 39 | domainNotEmpty: '域名資訊不能為空', 40 | redirectNotEmpty: '重定向地址不能為空', 41 | keyNotEmpty: 'key值不能為空', 42 | valueNotEmpty: 'value值不能為空', 43 | jsonFormatError: 'JSON格式錯誤', 44 | confirmDeletion: '確認删除?', 45 | noDataToDownload: '沒有數據可以下載', 46 | overrideData: '上傳成功原檔案會被覆蓋', 47 | readJsonErr: '讀取异常,檔案可能不是一個JSON', 48 | dataErr: '數據异常', 49 | formatErr: '格式錯誤', 50 | importEmpty: '你導入了一個空清單', 51 | existsEmptyString: '存在空字符串', 52 | }, 53 | placeholder: { 54 | select: '請選擇', 55 | input: '請輸入', 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /packages/vue-panels/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | import '@/common/element-plugin' 5 | import i18n from "@/lang/index"; 6 | 7 | import { initStorage } from "@proxy/shared-utils"; 8 | 9 | Vue.prototype.$ELEMENT = { size: 'mini' }; 10 | import "./index.scss" 11 | 12 | Vue.config.productionTip = false 13 | 14 | initStorage().then(() => { 15 | new Vue({ 16 | i18n, 17 | render: h => h(App) 18 | }).$mount('#app') 19 | }) 20 | -------------------------------------------------------------------------------- /packages/vue-panels/src/views/index.vue: -------------------------------------------------------------------------------- 1 | 91 | 267 | 324 | -------------------------------------------------------------------------------- /packages/vue-panels/src/views/interceptor/jsonEdit.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 65 | 98 | -------------------------------------------------------------------------------- /packages/vue-panels/src/views/interceptor/modal.vue: -------------------------------------------------------------------------------- 1 | 151 | 152 | 238 | -------------------------------------------------------------------------------- /packages/vue-panels/src/views/interceptor/table.vue: -------------------------------------------------------------------------------- 1 | 111 | 112 | 295 | 296 | 304 | -------------------------------------------------------------------------------- /packages/vue-panels/src/views/interceptor/tag.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 103 | 104 | 131 | -------------------------------------------------------------------------------- /packages/vue-panels/src/views/redirector/table.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 146 | 147 | 152 | -------------------------------------------------------------------------------- /packages/vue-panels/vue.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | // npm install compression-webpack-plugin@5.0.0 -S -D 3 | // const CompressionWebpackPlugin = require("compression-webpack-plugin"); 4 | const isProduction = process.env.NODE_ENV === "production"; 5 | 6 | module.exports = { 7 | devServer: { 8 | host: "0.0.0.0", 9 | port: 8082, 10 | https: false, 11 | hot: true, // 开启热模块加载 12 | disableHostCheck: true, // 启用 host 访问 13 | // proxy: {}, 14 | }, 15 | publicPath: process.env.NODE_ENV === "production" ? "./" : "/", 16 | productionSourceMap: false, 17 | configureWebpack: (config) => { 18 | // 开启gzip压缩 19 | if (isProduction) { 20 | // config.plugins.push( 21 | // new CompressionWebpackPlugin({ 22 | // algorithm: "gzip", 23 | // test: /\.js$|\.html$|\.json$|\.css/, 24 | // threshold: 10240, 25 | // minRatio: 0.8, 26 | // }) 27 | // ); 28 | // 开启分离js 29 | config.optimization = { 30 | runtimeChunk: "single", 31 | splitChunks: { 32 | chunks: "all", 33 | maxInitialRequests: Infinity, 34 | minSize: 20000, 35 | cacheGroups: { 36 | vendor: { 37 | test: /[\\/]node_modules[\\/]/, 38 | name(module) { 39 | // get the name. E.g. node_modules/packageName/not/this/part.js 40 | // or node_modules/packageName 41 | const packageName = module.context.match( 42 | /[\\/]node_modules[\\/](.*?)([\\/]|$)/ 43 | )[1]; 44 | // npm package names are URL-safe, but some servers don't like @ symbols 45 | return `npm.${packageName.replace("@", "")}`; 46 | }, 47 | }, 48 | }, 49 | }, 50 | }; 51 | // 取消webpack警告的性能提示 52 | config.performance = { 53 | hints: "warning", 54 | //入口起点的最大体积 55 | maxEntrypointSize: 50000000, 56 | //生成文件的最大体积 57 | maxAssetSize: 30000000, 58 | //只给出 js 文件的性能提示 59 | assetFilter: function(assetFilename) { 60 | return assetFilename.endsWith(".js"); 61 | }, 62 | }; 63 | } 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # all packages in subdirs of packages/ and components/ 3 | - "packages/**" 4 | # exclude packages that are inside test directories 5 | - "!**/test/**" 6 | -------------------------------------------------------------------------------- /release.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const inquirer = require('inquirer') 3 | const semver = require('semver') 4 | const pkg = require('./package.json') 5 | const manifest = require('./packages/shell-chrome/manifest.json') 6 | 7 | const curVersion = pkg.version 8 | 9 | ;(async () => { 10 | const { newVersion } = await inquirer.prompt([{ 11 | type: 'input', 12 | name: 'newVersion', 13 | message: `Please provide a version (current: ${curVersion}):`, 14 | }]) 15 | 16 | if (!semver.valid(newVersion)) { 17 | console.error(`Invalid version: ${newVersion}`) 18 | process.exit(1) 19 | } 20 | 21 | if (semver.lt(newVersion, curVersion)) { 22 | console.error(`New version (${newVersion}) cannot be lower than current version (${curVersion}).`) 23 | process.exit(1) 24 | } 25 | 26 | const { yes } = await inquirer.prompt([{ 27 | name: 'yes', 28 | message: `Release ${newVersion}?`, 29 | type: 'confirm', 30 | }]) 31 | 32 | if (yes) { 33 | pkg.version = newVersion 34 | manifest.version = newVersion 35 | fs.writeFileSync('./package.json', JSON.stringify(pkg, null, 2)) 36 | fs.writeFileSync('./packages/shell-chrome/manifest.json', JSON.stringify(manifest, null, 2)) 37 | } else { 38 | process.exit(1) 39 | } 40 | })() 41 | -------------------------------------------------------------------------------- /zip/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/g0ngjie/ajax-proxy/d4c4edb6382dab15a0db0eb9cf6454666cc21684/zip/.gitkeep --------------------------------------------------------------------------------