Compare commits

...

141 Commits
v1.0.8 ... main

Author SHA1 Message Date
刘嘉伟
64828404fb
Merge pull request #76 from luckjiawei/luckjiawei-patch-1
Update README.md
2025-02-21 10:36:48 +08:00
刘嘉伟
7681dc6350
Update README.md 2025-02-21 10:36:05 +08:00
刘嘉伟
08c2a2cb5a
Merge pull request #68 from locolocoer/main
bugfix: httpstohttp path error in windows
2025-02-13 14:33:43 +08:00
Daxian_feng
f0b420fe2e
Update webpack.yml 2025-01-24 21:58:18 +08:00
locolocoer
3bfb96b091 bugfix:path error in windows 2025-01-24 15:29:15 +08:00
Daxian_feng
29ddfe41f5
Create webpack.yml 2025-01-24 14:28:31 +08:00
刘嘉伟
5d124c7efb 🍻 add trending badge 2025-01-15 23:32:04 +08:00
刘嘉伟
40de47640c 💸 increase donation records 2025-01-15 23:27:42 +08:00
刘嘉伟
ea02338102 🔖 v1.1.6 2025-01-09 14:38:17 +08:00
刘嘉伟
816dcd4481 💸 update donation list 2025-01-09 14:38:06 +08:00
刘嘉伟
ced3bda8bb 🎉 vscode config 2025-01-09 14:31:34 +08:00
刘嘉伟
59be2d2173 🐛 Add the keepTunnelOpen parameter 2025-01-09 12:25:21 +08:00
刘嘉伟
b1dbd46db4 🐛 ini improvement 2025-01-09 12:23:05 +08:00
刘嘉伟
f25dec4c87 xtcp keepTunnelOpen 2025-01-09 12:10:55 +08:00
刘嘉伟
27c1d42596 🔊 Improve the log 2025-01-09 11:40:12 +08:00
刘嘉伟
58f625baf4 Supports importing frp 2025-01-09 11:34:34 +08:00
刘嘉伟
4bad940aff 💄 Icon optimization 2025-01-09 11:34:15 +08:00
刘嘉伟
d4f743663b Supports importing frp 2025-01-09 11:33:48 +08:00
刘嘉伟
4c0a6de1e9 Add JSON extraction script and generate SHA256 checksums: Introduced a new script to extract release information from a JSON file and fetch corresponding SHA256 checksums from remote assets. The checksums are saved in a new JSON file for easy access and verification. Enhanced logging for better traceability during the extraction process. 2025-01-08 14:51:14 +08:00
刘嘉伟
c359714bc5 Add new 'link' icon to IconifyIcon and update proxy view: Introduced the 'link' icon in the IconifyIcon component and replaced the 'content-copy' icon with 'link' in the proxy view for improved user interface consistency. 2025-01-08 14:51:03 +08:00
刘嘉伟
ffb42bd131 🔊 Refactor logging across multiple modules: Updated logging statements to utilize a new structured logging utility for improved traceability and consistency. Enhanced error handling and added informative logs for database operations, configuration management, and proxy handling. Masked sensitive data in logs to improve security during configuration operations. 2025-01-08 14:17:05 +08:00
刘嘉伟
9ab29259b1 🔊 Refactor logging in GitHub API: Replaced deprecated logging with a new logging utility for improved traceability. Updated log statements to use structured logging for mirror URL requests and response status codes, enhancing clarity and consistency in logs. 2025-01-08 13:53:42 +08:00
刘嘉伟
9ad8773b4d 🐛 Mirror source improvement 2025-01-08 13:50:51 +08:00
刘嘉伟
a34a1886de 🐛 Fixing the issue where frpc still runs after exiting with command + q. 2025-01-08 13:39:43 +08:00
刘嘉伟
f4f18afb7a 🔊 Improve the log 2025-01-08 12:07:42 +08:00
刘嘉伟
fcb01e83b2 🔊 Improve the log 2025-01-08 12:05:15 +08:00
刘嘉伟
cfba180603 Improve the log 2025-01-08 12:01:05 +08:00
刘嘉伟
aad6fd78d5 🔊 Improve the log 2025-01-08 12:01:00 +08:00
刘嘉伟
19fde43f8b Improve logging and error handling in API modules: Refactored common, file, and logger APIs to utilize a new logging utility for better traceability. Enhanced error handling for URL opening and file dialog interactions, ensuring informative logs for success and failure cases. Introduced a utility to mask sensitive data in configuration logs, improving security during application initialization. 2025-01-08 11:22:09 +08:00
刘嘉伟
9510a4cb67 Refactor logging and application initialization: Introduced a new logging utility for structured logging across the application. Enhanced the main process to log application events and errors, improving traceability and debugging. Cleaned up the code by removing deprecated comments and unused imports, and ensured proper error handling during configuration retrieval and API initialization. 2025-01-08 10:45:54 +08:00
刘嘉伟
337a3b6831 Enhance GitHub API integration: Added support for dynamic mirror selection and improved version handling. Introduced local JSON fallback for release data and refactored version retrieval logic. Updated UI to reflect mirror changes. 2025-01-07 14:43:58 +08:00
刘嘉伟
91b97df99a 🚧 传输配置完善 2025-01-06 16:14:52 +08:00
刘嘉伟
a1910be29c 增加 https2http 插件 2025-01-05 21:17:56 +08:00
刘嘉伟
3157b935e0 Merge remote-tracking branch 'origin/develop' into develop1226 2024-12-26 18:01:19 +08:00
刘嘉伟
eaa588698a 🚧 htts2http插件应用 2024-12-26 18:00:59 +08:00
刘嘉伟
5e436ccaf1 🚧 htts2http插件应用 2024-12-26 17:46:15 +08:00
刘嘉伟
0b4824975a 增加打开数据目录按钮 2024-12-22 22:04:52 +08:00
刘嘉伟
ae4b346084
Merge pull request #52 from webb-z/main
🐛 修复生成ini配置文件自定义域名错误的问题
2024-12-21 10:32:29 +08:00
webb
d95f7034f8 customDomain map时 不添加" 2024-12-20 19:03:11 +08:00
刘嘉伟
3ed09818ef 💸 更新捐赠列表 2024-12-20 15:02:32 +08:00
刘嘉伟
5f34a64fde 💸 增加捐赠 2024-12-18 17:14:40 +08:00
刘嘉伟
96b161f431 💸 增加捐赠 2024-12-12 17:34:40 +08:00
刘嘉伟
01299b2fa2 💸 增加捐赠 2024-12-11 17:50:53 +08:00
刘嘉伟
827b94f75f 🌱 增加捐赠 2024-12-09 22:08:26 +08:00
刘嘉伟
acfcdfbb16 🚑 修复window下toml目录错误 2024-12-06 17:12:25 +08:00
刘嘉伟
453f41ff19 优化 2024-12-05 12:07:01 +08:00
刘嘉伟
e2a01325f4 🐛 修复显示 2024-12-05 10:56:19 +08:00
刘嘉伟
2581caa9e3 🔖 发布新版本 2024-12-04 17:08:20 +08:00
刘嘉伟
8e0c152fd8 🔖 发布新版本 2024-12-04 17:06:54 +08:00
刘嘉伟
3751a52cd6 Merge branch 'develop' 2024-12-04 17:06:30 +08:00
刘嘉伟
32555d45b1 📝 更新捐赠 2024-12-04 17:06:05 +08:00
刘嘉伟
ab2323f421 增加捐赠等 2024-12-04 17:01:43 +08:00
刘嘉伟
139191065b 手动刷新日志 2024-12-04 16:35:00 +08:00
刘嘉伟
d70a7488fc ⬆️ 升级依赖 2024-12-04 16:19:27 +08:00
刘嘉伟
6c4a052bf6 打开外部日志的功能 2024-12-04 16:19:17 +08:00
刘嘉伟
5ac69de59a 增加webPort修改功能 2024-12-02 17:49:04 +08:00
刘嘉伟
90233f006a 增加适配sudp 2024-12-02 14:53:21 +08:00
刘嘉伟
40e4623413 认领HelloGithub 2024-12-02 09:19:36 +08:00
刘嘉伟
dc511fabc4 修复github api 限流问题 2024-12-01 10:10:59 +08:00
刘嘉伟
30b799f710 🌱 增加捐赠 2024-11-28 16:53:59 +08:00
刘嘉伟
b82215ca16 增加捐赠 2024-11-12 14:53:14 +08:00
刘嘉伟
654f6c7fd6 🔖 发布v1.1.4 2024-11-08 16:45:42 +08:00
刘嘉伟
919370c56c xtcp模式下打洞失败回退 stcp 2024-11-08 16:17:21 +08:00
刘嘉伟
e7f4572768 xtcp模式下打洞失败回退 stcp 2024-11-08 16:10:54 +08:00
刘嘉伟
dba788a1dd 🐛 warning 模式下报错 2024-11-08 16:10:15 +08:00
刘嘉伟
6c7c568a48 增加捐赠 2024-11-08 09:17:20 +08:00
刘嘉伟
88c687b66d
baidu
1
2024-10-29 10:09:35 +08:00
刘嘉伟
4a58fbd955 增加捐赠 2024-10-16 14:07:54 +08:00
刘嘉伟
d694d9537d 增加捐赠 2024-10-16 14:07:06 +08:00
刘嘉伟
c5883bb25b 增加捐赠 2024-10-14 15:15:21 +08:00
刘嘉伟
f04e08ee3e 新版本 2024-10-14 12:10:04 +08:00
刘嘉伟
6d2f1c7b64 生成代理名称 2024-10-14 12:09:36 +08:00
刘嘉伟
fe697059b4 生成代理名称 2024-10-14 12:06:08 +08:00
刘嘉伟
2d894844b0 xtcp 支持 2024-10-14 11:53:19 +08:00
刘嘉伟
ca885af394 静默启动 2024-10-14 11:27:50 +08:00
刘嘉伟
4d9c5bf003 静默启动 2024-10-14 11:24:54 +08:00
刘嘉伟
2ccddbe4b6 复制访问地址 2024-10-14 11:08:19 +08:00
刘嘉伟
843ab85215 子域名和自定义域名可以二选一 2024-09-25 11:43:42 +08:00
刘嘉伟
ed1f3dc378 子域名和自定义域名可以二选一 2024-09-25 10:48:43 +08:00
刘嘉伟
acbf075280 🐛 启动报错 2024-09-25 10:48:09 +08:00
刘嘉伟
59ef02033a Merge branch 'refs/heads/develop'
# Conflicts:
#	src/views/proxy/index.vue
2024-09-25 10:26:18 +08:00
刘嘉伟
d57e6f41ed 💄 修复窗口大小 2024-09-25 10:25:28 +08:00
刘嘉伟
0c1d6919ec 💫 修改 logo动画 2024-09-25 10:24:51 +08:00
刘嘉伟
f788990705 🐛 启动报错和支持 http Basic、子域名 2024-09-25 10:21:29 +08:00
刘嘉伟
139fed40d8 🐛 修复下载报错 2024-09-25 10:20:37 +08:00
刘嘉伟
6d5f985dfe 优化 2024-09-15 10:47:48 +08:00
刘嘉伟
686acdd7c5 🐛 修复下载报错 2024-09-15 10:31:21 +08:00
刘嘉伟
0fe365e3d9
Update issue templates 2024-09-10 16:52:04 +08:00
刘嘉伟
e0dcd66453
Update issue templates 2024-09-10 16:50:54 +08:00
刘嘉伟
c3f914e7e5 🚑 发布 1.1.1 2024-09-09 20:13:49 +08:00
刘嘉伟
d45a9b84bb 🐛 修复启动报错 2024-09-08 21:24:55 +08:00
刘嘉伟
efb5e94d4e 📝 批量端口 2024-09-08 00:21:52 +08:00
刘嘉伟
e378421c72 📝 批量端口 2024-09-07 18:08:37 +08:00
刘嘉伟
9d57006d16 📝 批量端口 2024-09-07 17:27:03 +08:00
刘嘉伟
e4cd09d1dc ini批量端口 2024-09-07 17:24:51 +08:00
刘嘉伟
60fbc354d0 批量端口前端校验 2024-09-07 17:18:59 +08:00
刘嘉伟
dbfceb9550 toml 批量端口 2024-09-07 17:01:49 +08:00
刘嘉伟
e0b3f6e7a7 范围端口 2024-09-07 16:49:38 +08:00
刘嘉伟
96247a29d4 ip 提示 2024-09-07 16:18:14 +08:00
刘嘉伟
e53ab2ff44 禁用启用代理 2024-09-05 10:36:19 +08:00
刘嘉伟
714043c9ec 禁用启用代理 2024-09-05 10:30:47 +08:00
刘嘉伟
550c552643 禁用启用代理 2024-09-05 10:28:55 +08:00
刘嘉伟
707b9c4b9e tls修改 2024-09-04 13:08:16 +08:00
刘嘉伟
2592e201ed 🐛 TLS名称不正确 2024-09-04 09:24:39 +08:00
刘嘉伟
1ea549323d 🚧 代理列表开关 2024-09-02 23:26:55 +08:00
刘嘉伟
3ef47b200c 🐛 修复心跳相关只能设置为10的问题 2024-09-02 22:53:59 +08:00
刘嘉伟
0d21a6a2a2 赞助 2024-08-27 12:00:28 +08:00
刘嘉伟
5face0839a 赞助 2024-08-27 11:59:41 +08:00
刘嘉伟
eae089b0e9 打赏 2024-08-27 11:57:35 +08:00
刘嘉伟
4f80fc9e27 新版本 2024-08-26 10:51:12 +08:00
刘嘉伟
ad3167627b 引导 2024-08-24 14:41:39 +08:00
刘嘉伟
44088af063 礼花特效、重启修复 2024-08-24 14:04:03 +08:00
刘嘉伟
09c591b24b 💄 去除标题动画 2024-08-22 14:32:18 +08:00
刘嘉伟
7f0ed6142f 🐛 启动校验 2024-08-22 14:31:19 +08:00
刘嘉伟
a5279253a9 💄 统一提示 2024-08-22 14:28:06 +08:00
刘嘉伟
3f49841ab2 🍻 导入导出、清空配置 2024-08-22 14:19:19 +08:00
刘嘉伟
9df5cec23a 🚸 删除提示框 2024-08-22 14:08:39 +08:00
刘嘉伟
9fed5fc844 配置导出 2024-08-21 22:31:12 +08:00
刘嘉伟
6e028d377a 增加镜像源下载 2024-08-21 13:46:14 +08:00
刘嘉伟
1f246e3966 增加镜像源下载 2024-08-21 13:37:42 +08:00
刘嘉伟
357cc473c7 📝 二维码、TODO调整 2024-08-21 10:39:39 +08:00
刘嘉伟
bd74b068e3 📝 二维码、TODO调整 2024-08-21 10:02:37 +08:00
刘嘉伟
0b7307ac3a 📝 二维码、TODO调整 2024-08-21 10:01:15 +08:00
刘嘉伟
c7aa312463 新版本 2024-08-18 12:03:48 +08:00
刘嘉伟
fb2a8ecd51 新版本 2024-08-17 17:22:46 +08:00
刘嘉伟
ed66824931 支持stcp 2024-08-17 00:48:23 +08:00
刘嘉伟
9abda2f6d1 支持stcp 2024-08-17 00:43:23 +08:00
刘嘉伟
22bb48c274 💄 stcp界面优化 2024-08-17 00:34:23 +08:00
刘嘉伟
1d9ac090f3 支持toml配置文件的stcp 2024-08-17 00:20:47 +08:00
刘嘉伟
3961475421 🚧 类型统一 2024-08-16 23:59:40 +08:00
刘嘉伟
989f9b8a37 💄 首页优化 2024-08-16 23:39:51 +08:00
刘嘉伟
3c9db3dc9f 💄 优化配置页面显示 2024-08-16 23:04:58 +08:00
刘嘉伟
c8e99a1dc4 💄 增加多分辨率 2024-08-16 23:02:03 +08:00
刘嘉伟
7101f1c7a1 🐛 优化多分辨率下的显示 2024-08-16 23:01:52 +08:00
刘嘉伟
179da0bbf6 💄 修复日志滚动条 2024-08-16 22:19:39 +08:00
刘嘉伟
b00392b9f5 💄 增加标题动画、增加自定义域名提示 2024-08-16 18:19:34 +08:00
刘嘉伟
71c677a193
Merge pull request #18 from forestxieCode/feat/xsl20240812-perf-github
perf:下载版本,自动更新下拉选择数据, 以及删除版本,取消当前选择的
2024-08-16 17:11:15 +08:00
刘嘉伟
e42f36ee72 图标离线、优化页面 2024-08-16 17:08:37 +08:00
刘嘉伟
32ee5aa78f 图标离线、优化页面 2024-08-16 17:06:54 +08:00
刘嘉伟
d2ceb6e35e 图标离线、优化页面 2024-08-16 16:23:20 +08:00
forestxieCode
19c6e208a6 perf:下载版本,自动更新下拉选择数据, 以及删除版本,取消当前选择的 2024-08-12 23:15:18 +08:00
54 changed files with 52956 additions and 1673 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**🖥️ 操作系统**
**🔖 Frpc-Desktop版本号**
**🤔 问题描述**
**🖼️ 错误截图**
**🔊 日志**
> 日志目录:
* Window: `%APPDATA%\Frpc-Desktop\logs\`
* MacOS: `~/Library/Logs/Frpc-Desktop/logs/`
* Linux: `~/Library/Logs/Frpc-Desktop/`

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

28
.github/workflows/webpack.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: NodeJS with Webpack
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.8.0]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Build
run: |
npm install
npx run release

23
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node-terminal",
"name": "dev",
"request": "launch",
"command": "npm run dev",
"cwd": "${workspaceFolder}"
},
{
"type": "node-terminal",
"name": "build electron",
"request": "launch",
"command": "npm run build:electron",
"cwd": "${workspaceFolder}"
}
]
}

View File

@ -20,25 +20,45 @@
<br />
支持所有frp版本 / 开机自启 / 可视化配置 / 免费开源
</p>
<p><a href="https://jwinks.com/p/frp/#frp%E6%98%AF%E4%BB%80%E4%B9%88">使用教程</a></p>
<a href="https://trendshift.io/repositories/12489" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12489" alt="luckjiawei%2Ffrpc-desktop | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/b0dc116e9f2e4b8188da5a6d3e1bd8a4" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=b0dc116e9f2e4b8188da5a6d3e1bd8a4&claim_uid=8ZMOhz30mGJAHpa" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
## TODO
- [x] 开机自启动
- [x] 适配多用户 user & meta_token
- [x] 便携版
- [x] 增加udp代理类型
- [x] 支持快速分享frps
- [x] 增加快速选择本地端口
- [ ] 支持stcp代理类型
- [ ] 支持配的导出导入
- [ ] 优化配置
- [x] 支持stcp代理类型
- [x] 通过镜像站下载frp
- [x] 支持所有配置的导入导出
- [x] 一键清空所有配置
- [x] 支持导入识别frpc.toml
- [x] tcp、udp协议支持批量端口
- [ ] support multiple languages
## 常见问题
### Mac提示已损坏
执行命令:`sudo xattr -cr Frpc-Desktop.app`
## 里程碑
- 2025-01-09: 发布v1.1.6版本
- 2024-12-04: 发布v1.1.5版本 优化体验、支持修改webport、解决github限流问题、日志优化
- 2024-11-08: 发布v1.1.4版本 修复已知BUG
- 2024-10-14: 发布v1.1.3版本 支持xtcp协议、优化体验
- 2024-09-25: 发布v1.1.2版本 支持 http basic、子域名
- 2024-09-07: 发布v1.1.0版本 支持批量端口、支持单条代理开关控制
- 2024-08-24: 发布v1.0.9版本 支持镜像下载、导出导入配置
- 2024-08-17: 发布v1.0.8版本 支持stcp代理
- 2024-08-11: 发布v1.0.7版本
- 2024-08-09: 发布v1.0.6版本
- 2024-08-06: 发布v1.0.5版本
@ -49,10 +69,20 @@
- 2023-11-28: 发布v1.0版本
## 社区
微信扫描加入开源项目交流群 广告勿进!!!
<img src="screenshots/wechat-qr.jpg" alt="二维码" width="200">
广告勿进!!!
### TG
[https://t.me/+4kziSBL3LxVmYzVl](https://t.me/+4kziSBL3LxVmYzVl)
### 微信群
**~~微信扫描加入开源项目交流群~~ 微信群超过200人无法扫码进群 关注公众号进群**
<img src="screenshots/wechat-qr.png" alt="二维码" width="200"><img src="screenshots/mp_qr.jpg" alt="公众号二维码" width="200">
## 演示
@ -68,7 +98,47 @@
![about](https://github.com/luckjiawei/frpc-desktop/blob/main/screenshots/about.png?raw=true)
## 捐赠
👉👉👉[点击去捐赠](https://jwinks.com/donate/)👈👈👈
**捐赠名单**
| 🕰 时间 | 📡 平台 | 🤲 捐赠者 | 💰 金额 | ✉️ 捐赠留言 |
|------------|-------|---------------|---------|--------------------------|
| 2024-08-06 | 微信 | 三木 | 1 元 | 无 |
| 2024-08-25 | 微信 | 晚风 | 1 元 | 无 |
| 2024-08-27 | 微信 | x | 1 元 | 无 |
| 2024-10-09 | 微信 | 解脱 | 20 元 | 无 |
| 2024-10-09 | 微信 | KMDN | 20 元 | 无 |
| 2024-10-14 | 微信 | 121 | 5 元 | 无 |
| 2024-10-14 | 微信 | Different | 10 元 | 感谢您的开源 |
| 2024-10-16 | 微信 | 。 。 。 | 50 元 | 感谢开源的frp软件 |
| 2024-11-2 | 微信 | gesoft | 10 元 | 加油 |
| 2024-11-7 | 微信 | *进 | 10 元 | 谢谢,可见可得,省心省力 |
| 2024-11-8 | 微信 | **创 | 10 元 | 无 |
| 2024-11-20 | 微信 | 一東 | 20 元 | 请你喝咖啡 |
| 2024-11-20 | 微信 | KEVINSKH | 10 元 | 感谢开发方便快捷的图形化操作界面👍 |
| 2024-11-26 | 微信 | | 3 元 | 无 |
| 2024-11-26 | 微信 | Kaori | 1 元 | 谢谢大佬的项目要是能添加web控制页面就更好了 |
| 2024-12-03 | 微信 | 17¥ | 20 元 | 谢谢,很方便的软件 |
| 2024-12-03 | 微信 | Cr@k3r | 5 元 | 感谢你的工作 |
| 2024-12-09 | 微信 | Vince | 20 元 | 支持国人开发! |
| 2024-12-11 | 支付宝 | **萌 | 20 元 | 加油加油 |
| 2024-12-11 | 支付宝 | *石 | 20 元 | 无 |
| 2024-12-16 | 微信 | 铁汉柔情 | 1 元 | 加油支持国人 |
| 2024-12-16 | 微信 | 亚索🌪️ | 1 元 | 无 |
| 2024-12-17 | 微信 | ppp789 | 1.6 元 | 无 |
| 2024-12-17 | 支付宝 | *涛 | 10 元 | 无 |
| 2024-12-18 | 微信 | 觉远 | 6.66 元 | 开源不易 |
| 2024-12-19 | 微信 | 官方提醒 | 1 元 | 无 |
| 2024-12-19 | 微信 | 木~易 | 6.66 元 | 加油 |
| 2025-01-06 | 微信 | 如是 | 2 元 | 支持开源 |
| 2025-01-13 | 微信 | David Veith | 18.88 元 | 开源无价,么么哒 |
| 2025-01-14 | 微信 | Xterminal SSH | 199 元 | Xterminal SSH 客户端前来支援 |
## 贡献者
<a href="https://github.com/luckjiawei/frpc-desktop/graphs/contributors">
<img src="https://contrib.rocks/image?repo=luckjiawei/frpc-desktop" />
</a>
@ -77,14 +147,23 @@
[MIT](LICENSE)
## Stargazers over time
[![Stargazers over time](https://starchart.cc/luckjiawei/frpc-desktop.svg?variant=adaptive)](https://starchart.cc/luckjiawei/frpc-desktop)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=luckjiawei/frpc-desktop&type=Date)](https://star-history.com/#luckjiawei/frpc-desktop&Date)
<!-- MARKDOWN LINKS & IMAGES -->
[forks-shield]: https://img.shields.io/github/forks/luckjiawei/frpc-desktop.svg?style=for-the-badge
[forks-url]: https://github.com/luckjiawei/frpc-desktop/network/members
[stars-shield]: https://img.shields.io/github/stars/luckjiawei/frpc-desktop.svg?style=for-the-badge
[stars-url]: https://github.com/luckjiawei/frpc-desktop/stargazers
[issues-shield]: https://img.shields.io/github/issues/luckjiawei/frpc-desktop.svg?style=for-the-badge
[issues-url]: https://github.com/luckjiawei/frpc-desktop/issues
[license-shield]: https://img.shields.io/github/license/luckjiawei/frpc-desktop.svg?style=for-the-badge
[license-url]: https://github.com/luckjiawei/frpc-desktop/blob/master/LICENSE

11
baidu_urls.txt Normal file
View File

@ -0,0 +1,11 @@
https://jwinks.com/p/frpc-desktop113
https://jwinks.com/p/free-jsaqy
https://jwinks.com/p/frpc-desktop110
https://jwinks.com/p/frpc-desktop109
https://jwinks.com/p/nginx-proxy-manager
https://jwinks.com/p/frp
https://jwinks.com/p/acme
https://jwinks.com/archives
https://jwinks.com/search
https://jwinks.com/%E9%93%BE%E6%8E%A5
https://jwinks.com/donate

View File

@ -1,15 +1,27 @@
import {ipcMain, shell} from "electron";
import log from "electron-log";
import { app, ipcMain, shell } from "electron";
import { logError, logInfo, LogModule, logWarn } from "../utils/log";
export const initCommonApi = () => {
ipcMain.on("common.openUrl", async (event, args) => {
if (args) {
logInfo(LogModule.APP, `Attempting to open URL: ${args}`);
try {
await shell.openExternal(args);
logInfo(LogModule.APP, `Successfully opened URL: ${args}`);
} catch (error) {
logError(
LogModule.APP,
`Failed to open URL: ${args}. Error: ${error.message}`
);
}
} else {
logWarn(LogModule.APP, "No URL provided to open.");
}
});
// 打开链接
ipcMain.on("common.openUrl", async (event, args) => {
if (args) {
log.info(`打开链接:${args}`)
shell.openExternal(args).then(() => {
});
}
})
}
ipcMain.on("common.relaunch", () => {
logInfo(LogModule.APP, "Application is relaunching.");
app.relaunch();
app.quit();
});
};

View File

@ -1,52 +1,337 @@
import {app, ipcMain} from "electron";
import {getConfig, saveConfig} from "../storage/config";
import {listVersion} from "../storage/version";
import { app, dialog, ipcMain, shell } from "electron";
import { clearConfig, getConfig, saveConfig } from "../storage/config";
import { clearVersion, listVersion } from "../storage/version";
import { genIniConfig, genTomlConfig, stopFrpcProcess } from "./frpc";
import { clearProxy, insertProxy, listProxy } from "../storage/proxy";
import path from "path";
import fs from "fs";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const log = require('electron-log');
const toml = require("@iarna/toml");
const { v4: uuidv4 } = require("uuid");
export const initConfigApi = () => {
ipcMain.on("config.saveConfig", async (event, args) => {
saveConfig(args, (err, numberOfUpdated, upsert) => {
if (!err) {
const start = args.systemSelfStart || false;
log.info("开启自启状态", start)
app.setLoginItemSettings({
openAtLogin: start, //win
openAsHidden: start, //macOs
});
export const initConfigApi = win => {
ipcMain.on("config.saveConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to save configuration.");
saveConfig(args, (err, numberOfUpdated, upsert) => {
if (!err) {
const start = args.systemSelfStart || false;
logDebug(LogModule.APP, "Startup status set to: " + start);
app.setLoginItemSettings({
openAtLogin: start, //win
openAsHidden: start //macOs
});
logInfo(LogModule.APP, "Configuration saved successfully.");
} else {
logError(LogModule.APP, `Error saving configuration: ${err}`);
}
event.reply("Config.saveConfig.hook", {
err: err,
numberOfUpdated: numberOfUpdated,
upsert: upsert
});
});
});
ipcMain.on("config.getConfig", async (event, args) => {
logInfo(LogModule.APP, "Requesting configuration.");
getConfig((err, doc) => {
if (err) {
logError(LogModule.APP, `Error retrieving configuration: ${err}`);
}
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.versions", event => {
logInfo(LogModule.APP, "Requesting version information.");
listVersion((err, doc) => {
if (err) {
logError(LogModule.APP, `Error retrieving version information: ${err}`);
}
event.reply("Config.versions.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.hasConfig", event => {
logInfo(LogModule.APP, "Checking if configuration exists.");
getConfig((err, doc) => {
if (err) {
logError(LogModule.APP, `Error checking configuration: ${err}`);
}
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
});
ipcMain.on("config.exportConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to export configuration.");
const result = await dialog.showOpenDialog({
properties: ["openDirectory"]
});
const outputDirectory = result.filePaths[0];
if (!outputDirectory) {
logWarn(LogModule.APP, "Export canceled by user.");
return;
}
logInfo(
LogModule.APP,
`Exporting configuration to directory ${outputDirectory} with type: ${args}`
);
getConfig((err1, config) => {
if (!err1 && config) {
listProxy((err2, proxys) => {
if (!err2) {
let configContent = "";
if (args === "ini") {
configContent = genIniConfig(config, proxys);
} else if (args === "toml") {
configContent = genTomlConfig(config, proxys);
}
event.reply("Config.saveConfig.hook", {
err: err,
numberOfUpdated: numberOfUpdated,
upsert: upsert
});
const configPath = path.join(
outputDirectory,
`frpc-desktop.${args}`
);
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{ flag: "w" },
err => {
if (err) {
logError(
LogModule.APP,
`Error writing configuration file: ${err}`
);
event.reply("config.exportConfig.hook", {
data: "导出错误",
err: err
});
} else {
logInfo(
LogModule.APP,
"Configuration exported successfully."
);
event.reply("Config.exportConfig.hook", {
data: {
configPath: configPath
}
});
}
}
);
} else {
logError(LogModule.APP, `Error listing proxies: ${err2}`);
}
});
} else {
logError(LogModule.APP, `Error retrieving configuration: ${err1}`);
}
});
});
ipcMain.on("config.getConfig", async (event, args) => {
getConfig((err, doc) => {
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
const parseTomlConfig = (tomlPath: string) => {
logInfo(LogModule.APP, `Parsing TOML configuration from ${tomlPath}`);
const importConfigPath = tomlPath;
const tomlData = fs.readFileSync(importConfigPath, "utf-8");
logInfo(LogModule.APP, "Configuration content read successfully.");
const sourceConfig = toml.parse(tomlData);
// 解析config
const targetConfig: FrpConfig = {
currentVersion: null,
serverAddr: sourceConfig.serverAddr || "",
serverPort: sourceConfig.serverPort || "",
authMethod: sourceConfig?.user
? "multiuser"
: sourceConfig?.auth?.method || "",
authToken: sourceConfig?.auth?.token || "",
transportHeartbeatInterval:
sourceConfig?.transport?.heartbeatInterval || 30,
transportHeartbeatTimeout:
sourceConfig?.transport?.heartbeatTimeout || 90,
tlsConfigEnable: sourceConfig?.transport?.tls?.enable || false,
tlsConfigCertFile: sourceConfig?.transport?.tls?.certFile || "",
tlsConfigKeyFile: sourceConfig?.transport?.tls?.keyFile || "",
tlsConfigServerName: sourceConfig?.transport?.tls?.serverName || "",
tlsConfigTrustedCaFile: sourceConfig?.transport?.tls?.trustedCaFile || "",
logLevel: sourceConfig?.log?.level || "info",
logMaxDays: sourceConfig?.log?.maxDays || 3,
proxyConfigProxyUrl: sourceConfig?.transport?.proxyURL || "",
proxyConfigEnable: Boolean(sourceConfig?.transport?.proxyURL) || false,
user: sourceConfig?.user || "",
metaToken: sourceConfig?.metadatas?.token || "",
systemSelfStart: false,
systemStartupConnect: false,
systemSilentStartup: false,
webEnable: true,
webPort: sourceConfig?.webServer?.port || 57400,
transportProtocol: sourceConfig?.transport?.protocol || "tcp",
transportDialServerTimeout:
sourceConfig?.transport?.dialServerTimeout || 10,
transportDialServerKeepalive:
sourceConfig?.transport?.dialServerKeepalive || 70,
transportPoolCount: sourceConfig?.transport?.poolCount || 0,
transportTcpMux: sourceConfig?.transport?.tcpMux || true,
transportTcpMuxKeepaliveInterval:
sourceConfig?.transport?.tcpMuxKeepaliveInterval || 7200
};
let frpcProxys = [];
// 解析proxy
if (sourceConfig?.proxies && sourceConfig.proxies.length > 0) {
const frpcProxys1 = sourceConfig.proxies.map(m => {
const rm: Proxy = {
_id: uuidv4(),
name: m?.name,
type: m?.type,
localIp: m?.localIP || "",
localPort: m?.localPort || null,
remotePort: m?.remotePort || null,
customDomains: m?.customDomains || [],
subdomain: m.subdomain || "",
basicAuth: m.basicAuth || false,
httpUser: m.httpUser || "",
httpPassword: m.httpPassword || "",
// 以下为stcp参数
stcpModel: "visited",
serverName: "",
secretKey: m?.secretKey || "",
bindAddr: "",
bindPort: null,
status: m?.status || true,
fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500,
keepTunnelOpen: m?.keepTunnelOpen || false,
https2http: m?.https2http || false,
https2httpCaFile: m?.https2httpCaFile || "",
https2httpKeyFile: m?.https2httpKeyFile || ""
};
return rm;
});
frpcProxys = [...frpcProxys, ...frpcProxys1];
logInfo(LogModule.APP, "Parsed proxies from configuration.");
}
// 解析stcp的访问者
if (sourceConfig?.visitors && sourceConfig.visitors.length > 0) {
const frpcProxys2 = sourceConfig.visitors.map(m => {
const rm: Proxy = {
_id: uuidv4(),
name: m?.name,
type: m?.type,
localIp: "",
localPort: null,
remotePort: null,
customDomains: [],
subdomain: m.subdomain || "",
basicAuth: m.basicAuth || false,
httpUser: m.httpUser || "",
httpPassword: m.httpPassword || "",
// 以下为stcp参数
stcpModel: "visitors",
serverName: m?.serverName,
secretKey: m?.secretKey || "",
bindAddr: m?.bindAddr,
bindPort: m?.bindPort,
status: m?.status || true,
fallbackTo: m?.fallbackTo,
fallbackTimeoutMs: m?.fallbackTimeoutMs || 500,
keepTunnelOpen: m?.keepTunnelOpen || false,
https2http: m?.https2http || false,
https2httpCaFile: m?.https2httpCaFile || "",
https2httpKeyFile: m?.https2httpKeyFile || ""
};
return rm;
});
frpcProxys = [...frpcProxys, ...frpcProxys2];
logInfo(LogModule.APP, "Parsed visitors from configuration.");
}
if (targetConfig) {
clearConfig(() => {
logInfo(LogModule.APP, "Clearing existing configuration.");
saveConfig(targetConfig);
logInfo(LogModule.APP, "New configuration saved.");
});
}
if (frpcProxys && frpcProxys.length > 0) {
clearProxy(() => {
frpcProxys.forEach(f => {
insertProxy(f, err => {
if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err}`);
} else {
logInfo(LogModule.APP, `Inserted proxy: ${JSON.stringify(f)}`);
}
});
});
});
});
}
};
ipcMain.on("config.versions", event => {
listVersion((err, doc) => {
event.reply("Config.versions.hook", {
err: err,
data: doc
});
});
ipcMain.on("config.importConfig", async (event, args) => {
logInfo(LogModule.APP, "Attempting to import configuration.");
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "FrpcConfig Files", extensions: ["toml", "ini"] } // 允许选择的文件类型
]
});
if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
return;
} else {
const filePath = result.filePaths[0];
const fileExtension = path.extname(filePath); // 获取文件后缀名
logWarn(
LogModule.APP,
`Importing file ${filePath} with extension ${fileExtension}`
);
if (fileExtension === ".toml") {
parseTomlConfig(filePath);
event.reply("Config.importConfig.hook", {
success: true
});
} else {
logError(
LogModule.APP,
`Import failed, unsupported file format: ${fileExtension}`
);
event.reply("Config.importConfig.hook", {
success: false,
data: `导入失败,暂不支持 ${fileExtension} 格式文件`
});
}
}
});
ipcMain.on("config.hasConfig", event => {
getConfig((err, doc) => {
event.reply("Config.getConfig.hook", {
err: err,
data: doc
});
});
ipcMain.on("config.clearAll", async (event, args) => {
logInfo(LogModule.APP, "Clearing all configurations.");
stopFrpcProcess(() => {
clearConfig();
clearProxy();
clearVersion();
event.reply("Config.clearAll.hook", {});
logInfo(LogModule.APP, "All configurations cleared.");
});
});
ipcMain.on("config.openDataFolder", async (event, args) => {
const userDataPath = app.getPath("userData");
logInfo(LogModule.APP, "Attempting to open data folder.");
shell.openPath(userDataPath).then(errorMessage => {
if (errorMessage) {
logError(LogModule.APP, `Failed to open data folder: ${errorMessage}`);
event.reply("Config.openDataFolder.hook", false);
} else {
logInfo(LogModule.APP, "Data folder opened successfully.");
event.reply("Config.openDataFolder.hook", true);
}
});
});
};

View File

@ -1,13 +1,21 @@
import {dialog, ipcMain} from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
export const initFileApi = () => {
ipcMain.handle("file.selectFile", async (event, args) => {
const result = dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [
{name: 'Text Files', extensions: args},
]
})
return result;
logInfo(LogModule.APP, `Attempting to open file dialog with filters: ${JSON.stringify(args)}`);
try {
const result = dialog.showOpenDialogSync({
properties: ['openFile'],
filters: [
{ name: 'Text Files', extensions: args },
]
});
logInfo(LogModule.APP, `File dialog result: ${JSON.stringify(result)}`);
return result;
} catch (error) {
logError(LogModule.APP, `Error opening file dialog: ${error.message}`);
return null;
}
});
}

View File

@ -1,36 +1,40 @@
import {app, ipcMain, Notification} from "electron";
import {Config, getConfig} from "../storage/config";
import {listProxy, Proxy} from "../storage/proxy";
import {getVersionById} from "../storage/version";
import { app, ipcMain, Notification } from "electron";
import { getConfig } from "../storage/config";
import { listProxy } from "../storage/proxy";
import { getVersionById } from "../storage/version";
import treeKill from "tree-kill";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
const fs = require("fs");
const path = require("path");
const {exec, spawn} = require("child_process");
const log = require('electron-log');
const { exec, spawn } = require("child_process");
export let frpcProcess = null;
const runningCmd = {
commandPath: null,
configPath: null
commandPath: null,
configPath: null
};
let frpcStatusListener = null;
/**
*
* @param versionId ID
* @param callback
*/
const getFrpcVersionWorkerPath = (
versionId: string,
callback: (workerPath: string) => void
versionId: number,
callback: (workerPath: string) => void
) => {
getVersionById(versionId, (err2, version) => {
if (!err2) {
if (version) {
callback(version["frpcVersionPath"]);
}
}
});
getVersionById(versionId, (err2, version) => {
if (!err2) {
if (version) {
callback(version["frpcVersionPath"]);
}
}
});
};
const isRangePort = (m: Proxy) => {
return (
(m.type === "tcp" || m.type === "udp") &&
(String(m.localPort).indexOf("-") !== -1 ||
String(m.localPort).indexOf(",") !== -1)
);
};
/**
@ -38,174 +42,434 @@ const getFrpcVersionWorkerPath = (
* @param config
* @param proxys
*/
const genTomlConfig = (config: Config, proxys: Proxy[]) => {
const proxyToml = proxys.map(m => {
let toml = `
[[proxies]]
name = "${m.name}"
type = "${m.type}"
localIP = "${m.localIp}"
export const genTomlConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyToml = proxys.map(m => {
const rangePort = isRangePort(m);
config.tlsConfigKeyFile = config.tlsConfigKeyFile.replace(/\\/g, "\\\\");
config.tlsConfigCertFile = config.tlsConfigCertFile.replace(/\\/g, "\\\\");
config.tlsConfigTrustedCaFile = config.tlsConfigTrustedCaFile.replace(
/\\/g,
"\\\\"
);
let toml = `${
rangePort
? `{{- range $_, $v := parseNumberRangePair "${m.localPort}" "${m.remotePort}" }}`
: ""
}
[[${
(m.type === "stcp" || m.type === "xtcp" || m.type === "sudp") &&
m.stcpModel === "visitors"
? "visitors"
: "proxies"
}]]
${rangePort ? "" : `name = "${m.name}"`}
type = "${m.type}"\n`;
switch (m.type) {
case "tcp":
case "udp":
if (rangePort) {
toml += `name = "${m.name}-{{ $v.First }}"
localPort = {{ $v.First }}
remotePort = {{ $v.Second }}\n`;
} else {
toml += `localIP = "${m.localIp}"
localPort = ${m.localPort}
`;
switch (m.type) {
case "tcp":
case "udp":
toml += `remotePort = ${m.remotePort}`;
break;
case "http":
case "https":
toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]`;
break;
default:
break;
remotePort = ${m.remotePort}\n`;
}
break;
case "http":
case "https":
const customDomains = m.customDomains.filter(f1 => f1 !== "");
if (customDomains && customDomains.length > 0) {
toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]\n`;
}
if (m.subdomain) {
toml += `subdomain="${m.subdomain}"\n`;
}
if (m.basicAuth) {
toml += `httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"\n`;
}
if (m.https2http) {
toml += `[proxies.plugin]
type = "https2http"
localAddr = "${m.localIp}:${m.localPort}"
crtPath = "${m.https2httpCaFile}"
keyPath = "${m.https2httpKeyFile}"\n`;
} else {
toml += `localIP = "${m.localIp}"
localPort = ${m.localPort}\n`;
}
return toml;
});
const toml = `
serverAddr = "${config.serverAddr}"
break;
case "xtcp":
if (m.stcpModel === "visitors") {
toml += `keepTunnelOpen = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp":
if (m.stcpModel === "visitors") {
// 访问者
toml += `serverName = "${m.serverName}"
bindAddr = "${m.bindAddr}"
bindPort = ${m.bindPort}\n`;
if (m.fallbackTo) {
toml += `fallbackTo = "${m.fallbackTo}"
fallbackTimeoutMs = ${m.fallbackTimeoutMs || 500}\n`;
}
} else if (m.stcpModel === "visited") {
// 被访问者
toml += `localIP = "${m.localIp}"
localPort = ${m.localPort}\n`;
}
toml += `secretKey="${m.secretKey}"\n`;
break;
default:
break;
}
if (rangePort) {
toml += `{{- end }}\n`;
}
return toml;
});
const toml = `serverAddr = "${config.serverAddr}"
serverPort = ${config.serverPort}
${config.authMethod === 'token' ? `
auth.method = "token"
auth.token = "${config.authToken}"
` : ""}
${config.authMethod === 'multiuser' ? `
user = "${config.user}"
metadatas.token = "${config.metaToken}"
` : ""}
${config.transportHeartbeatInterval ? `
transport.heartbeatInterval = ${config.transportHeartbeatInterval}
` : ""}
${config.transportHeartbeatTimeout ? `
transport.heartbeatTimeout = ${config.transportHeartbeatTimeout}
` : ""}
${
config.authMethod === "token"
? `auth.method = "token"
auth.token = "${config.authToken}"`
: ""
}
${
config.authMethod === "multiuser"
? `user = "${config.user}"
metadatas.token = "${config.metaToken}"`
: ""
}
log.to = "frpc.log"
log.level = "${config.logLevel}"
log.maxDays = ${config.logMaxDays}
webServer.addr = "127.0.0.1"
webServer.port = 57400
transport.tls.enable = ${config.tlsConfigEnable}
${config.tlsConfigEnable ? `
transport.tls.certFile = "${config.tlsConfigCertFile}"
transport.tls.keyFile = "${config.tlsConfigKeyFile}"
transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"
transport.tls.serverName = "${config.tlsConfigServerName}"
` : ""}
${config.proxyConfigEnable ? `
transport.proxyURL = "${config.proxyConfigProxyUrl}"
` : ""}
${proxyToml.join("")}
`;
return toml;
webServer.port = ${config.webPort}
${
config.transportProtocol
? `transport.protocol = "${config.transportProtocol}"`
: ""
}
${
config.transportPoolCount
? `transport.poolCount = ${config.transportPoolCount}`
: ""
}
${
config.transportDialServerTimeout
? `transport.dialServerTimeout = ${config.transportDialServerTimeout}`
: ""
}
${
config.transportDialServerKeepalive
? `transport.dialServerKeepalive = ${config.transportDialServerKeepalive}`
: ""
}
${
config.transportHeartbeatInterval
? `transport.heartbeatInterval = ${config.transportHeartbeatInterval}`
: ""
}
${
config.transportHeartbeatTimeout
? `transport.heartbeatTimeout = ${config.transportHeartbeatTimeout}`
: ""
}
${config.transportTcpMux ? `transport.tcpMux = ${config.transportTcpMux}` : ""}
${
config.transportTcpMux && config.transportTcpMuxKeepaliveInterval
? `transport.tcpMuxKeepaliveInterval = ${config.transportTcpMuxKeepaliveInterval}`
: ""
}
transport.tls.enable = ${config.tlsConfigEnable}
${
config.tlsConfigEnable && config.tlsConfigCertFile
? `transport.tls.certFile = "${config.tlsConfigCertFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `transport.tls.keyFile = "${config.tlsConfigKeyFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? `transport.tls.trustedCaFile = "${config.tlsConfigTrustedCaFile}"`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `transport.tls.serverName = "${config.tlsConfigServerName}"`
: ""
}
${
config.proxyConfigEnable
? `transport.proxyURL = "${config.proxyConfigProxyUrl}"`
: ""
}
${proxyToml.join("")}`;
return toml;
};
/**
* ini配置
* @param config
* @param proxys
*/
const genIniConfig = (config: Config, proxys: Proxy[]) => {
const proxyIni = proxys.map(m => {
let ini = `
[${m.name}]
export const genIniConfig = (config: FrpConfig, proxys: Proxy[]) => {
const proxyIni = proxys.map(m => {
const rangePort = isRangePort(m);
let ini = `[${rangePort ? "range:" : ""}${m.name}]
type = "${m.type}"
`;
switch (m.type) {
case "tcp":
case "udp":
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}
`;
switch (m.type) {
case "tcp":
case "udp":
ini += `remote_port = ${m.remotePort}`;
break;
case "http":
case "https":
ini += `custom_domains=[${m.customDomains.map(m => `"${m}"`)}]`;
break;
default:
break;
remote_port = ${m.remotePort}\n`;
break;
case "http":
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}
custom_domains=[${m.customDomains.map(m => `${m}`)}]
subdomain="${m.subdomain}"\n`;
if (m.basicAuth) {
ini += `
httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"\n`;
}
break;
case "https":
ini += `
custom_domains=[${m.customDomains.map(m => `${m}`)}]
subdomain="${m.subdomain}"\n`;
if (m.basicAuth) {
ini += `
httpUser = "${m.httpUser}"
httpPassword = "${m.httpPassword}"\n`;
}
if (m.https2http) {
ini += `
plugin = https2http
plugin_local_addr = ${m.localIp}:${m.localPort}
plugin_crt_path = ${m.https2httpCaFile}
plugin_key_path = ${m.https2httpKeyFile}\n`;
} else {
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}\n`;
}
break;
case "xtcp":
if (m.stcpModel === "visitors") {
ini += `keep_tunnel_open = ${m.keepTunnelOpen}\n`;
}
case "stcp":
case "sudp":
if (m.stcpModel === "visitors") {
// 访问者
ini += `
role = visitor
server_name = "${m.serverName}"
bind_addr = "${m.bindAddr}"
bind_port = ${m.bindPort}\n`;
if (m.fallbackTo) {
ini += `
fallback_to = ${m.fallbackTo}
fallback_timeout_ms = ${m.fallbackTimeoutMs || 500}\n`;
}
} else if (m.stcpModel === "visited") {
// 被访问者
ini += `
local_ip = "${m.localIp}"
local_port = ${m.localPort}\n`;
}
ini += `
sk="${m.secretKey}"\n`;
break;
default:
break;
}
return ini;
});
const ini = `
return ini;
});
const ini = `
[common]
server_addr = ${config.serverAddr}
server_port = ${config.serverPort}
${config.authMethod === 'token' ? `
${
config.authMethod === "token"
? `
authentication_method = ${config.authMethod}
token = ${config.authToken}
` : ""}
${config.authMethod === 'multiuser' ? `
token = ${config.authToken}\n`
: ""
}
${
config.authMethod === "multiuser"
? `
user = ${config.user}
meta_token = ${config.metaToken}
` : ""}
meta_token = ${config.metaToken}\n`
: ""
}
${config.transportHeartbeatInterval ? `
${config.transportProtocol ? `protocol = ${config.transportProtocol}` : ""}
${config.transportPoolCount ? `pool_count = ${config.transportPoolCount}` : ""}
${
config.transportDialServerTimeout
? `dial_server_timeout = ${config.transportDialServerTimeout}`
: ""
}
${
config.transportDialServerKeepalive
? `dial_server_keepalive = ${config.transportDialServerKeepalive}`
: ""
}
${
config.transportHeartbeatInterval
? `
heartbeat_interval = ${config.transportHeartbeatInterval}
` : ""}
${config.transportHeartbeatTimeout ? `
`
: ""
}
${
config.transportHeartbeatTimeout
? `
heartbeat_timeout = ${config.transportHeartbeatTimeout}
` : ""}
`
: ""
}
${config.transportTcpMux ? `transport.tcp_mux = ${config.transportTcpMux}` : ""}
${
config.transportTcpMux && config.transportTcpMuxKeepaliveInterval
? `tcp_mux_keepalive_interval = ${config.transportTcpMuxKeepaliveInterval}`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigCertFile
? `
tls_cert_file = ${config.tlsConfigCertFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigKeyFile
? `
tls_key_file = ${config.tlsConfigKeyFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigTrustedCaFile
? `
tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile}\n`
: ""
}
${
config.tlsConfigEnable && config.tlsConfigServerName
? `
tls_server_name = ${config.tlsConfigServerName}\n`
: ""
}
${
config.proxyConfigEnable
? `
http_proxy = "${config.proxyConfigProxyUrl}"\n`
: ""
}
log_file = "frpc.log"
log_level = ${config.logLevel}
log_max_days = ${config.logMaxDays}
admin_addr = 127.0.0.1
admin_port = 57400
admin_port = ${config.webPort}
tls_enable = ${config.tlsConfigEnable}
${config.tlsConfigEnable ? `
tls_cert_file = ${config.tlsConfigCertFile}
tls_key_file = ${config.tlsConfigKeyFile}
tls_trusted_ca_file = ${config.tlsConfigTrustedCaFile}
tls_server_name = ${config.tlsConfigServerName}
` : ""}
${config.proxyConfigEnable ? `
http_proxy = "${config.proxyConfigProxyUrl}"
` : ""}
${proxyIni.join("")}
`
return ini;
}
`;
return ini;
};
/**
*
*/
export const generateConfig = (
config: Config,
callback: (configPath: string) => void
config: FrpConfig,
callback: (configPath: string) => void
) => {
listProxy((err3, proxys) => {
if (!err3) {
const {currentVersion} = config;
let filename = null;
let configContent = "";
if (currentVersion < 124395282) {
// 版本小于v0.52.0
filename = "frp.ini";
configContent = genIniConfig(config, proxys)
} else {
filename = "frp.toml";
configContent = genTomlConfig(config, proxys)
}
const configPath = path.join(app.getPath("userData"), filename)
log.info(`生成配置成功 配置路径:${configPath}`)
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{flag: "w"},
err => {
if (!err) {
callback(filename);
}
}
);
listProxy((err3, proxys) => {
if (err3) {
logError(LogModule.FRP_CLIENT, `Failed to list proxies: ${err3.message}`);
return;
}
const { currentVersion } = config;
let filename = null;
let configContent = "";
const filtered = proxys
.map(m => {
if (m.status == null || m.status == undefined) {
m.status = true;
}
});
return m;
})
.filter(f => f.status);
if (currentVersion < 124395282) {
// 版本小于v0.52.0
filename = "frp.ini";
configContent = genIniConfig(config, filtered);
logInfo(
LogModule.FRP_CLIENT,
`Using INI format for configuration: ${filename}`
);
} else {
filename = "frp.toml";
configContent = genTomlConfig(config, filtered);
logInfo(
LogModule.FRP_CLIENT,
`Using TOML format for configuration: ${filename}`
);
}
const configPath = path.join(app.getPath("userData"), filename);
logInfo(
LogModule.FRP_CLIENT,
`Writing configuration to file: ${configPath}`
);
fs.writeFile(
configPath, // 配置文件目录
configContent, // 配置文件内容
{ flag: "w" },
err => {
if (err) {
logError(
LogModule.FRP_CLIENT,
`Failed to write configuration file: ${err.message}`
);
} else {
logInfo(
LogModule.FRP_CLIENT,
`Configuration file written successfully: ${filename}`
);
callback(filename);
}
}
);
});
};
/**
@ -215,149 +479,244 @@ export const generateConfig = (
* @param configPath
*/
const startFrpcProcess = (commandPath: string, configPath: string) => {
log.info(`启动frpc 目录:${app.getPath("userData")} 命令:${commandPath}`,)
const command = `${commandPath} -c ${configPath}`;
frpcProcess = spawn(command, {
cwd: app.getPath("userData"),
shell: true
});
runningCmd.commandPath = commandPath;
runningCmd.configPath = configPath;
frpcProcess.stdout.on("data", data => {
log.debug(`启动输出:${data}`)
});
frpcProcess.stdout.on("error", data => {
log.error(`启动错误:${data}`)
stopFrpcProcess(() => {
})
});
frpcStatusListener = setInterval(() => {
const status = frpcProcessStatus()
log.debug(`监听frpc子进程状态${status}`)
if (!status) {
new Notification({
title: "Frpc Desktop",
body: "连接已断开,请前往日志查看原因"
}).show()
clearInterval(frpcStatusListener)
}
}, 3000)
logInfo(
LogModule.FRP_CLIENT,
`Starting frpc process. Directory: ${app.getPath(
"userData"
)}, Command: ${commandPath}`
);
const command = `${commandPath} -c ${configPath}`;
frpcProcess = spawn(command, {
cwd: app.getPath("userData"),
shell: true
});
runningCmd.commandPath = commandPath;
runningCmd.configPath = configPath;
frpcProcess.stdout.on("data", data => {
logDebug(LogModule.FRP_CLIENT, `Frpc process output: ${data}`);
});
frpcProcess.stdout.on("error", data => {
logError(LogModule.FRP_CLIENT, `Frpc process error: ${data}`);
stopFrpcProcess(() => {});
});
frpcStatusListener = setInterval(() => {
const status = frpcProcessStatus();
logDebug(
LogModule.FRP_CLIENT,
`Monitoring frpc process status: ${status}, Listener ID: ${frpcStatusListener}`
);
if (!status) {
new Notification({
title: "Frpc Desktop",
body: "Connection lost, please check the logs for details."
}).show();
logError(
LogModule.FRP_CLIENT,
"Frpc process status check failed. Connection lost."
);
clearInterval(frpcStatusListener);
}
}, 3000);
};
/**
* frpc配置
*/
export const reloadFrpcProcess = () => {
if (frpcProcess && !frpcProcess.killed) {
getConfig((err1, config) => {
if (!err1) {
if (config) {
generateConfig(config, configPath => {
const command = `${runningCmd.commandPath} reload -c ${configPath}`;
log.info(`重载配置:${command}`)
exec(command, {
cwd: app.getPath("userData"),
shell: true
});
});
if (frpcProcess && !frpcProcess.killed) {
logDebug(
LogModule.FRP_CLIENT,
"Attempting to reload frpc process configuration."
);
getConfig((err1, config) => {
if (!err1) {
if (config) {
generateConfig(config, configPath => {
const command = `${runningCmd.commandPath} reload -c ${configPath}`;
logInfo(
LogModule.FRP_CLIENT,
`Reloading configuration: ${command}`
);
exec(
command,
{
cwd: app.getPath("userData"),
shell: true
},
(error, stdout, stderr) => {
if (error) {
logError(
LogModule.FRP_CLIENT,
`Error reloading configuration: ${error.message}`
);
return;
}
}
});
}
logDebug(
LogModule.FRP_CLIENT,
`Configuration reload output: ${stdout}`
);
if (stderr) {
logWarn(
LogModule.FRP_CLIENT,
`Configuration reload warnings: ${stderr}`
);
}
}
);
});
} else {
logWarn(LogModule.FRP_CLIENT, "No configuration found to reload.");
}
} else {
logError(LogModule.FRP_CLIENT, `Error getting configuration: ${err1}`);
}
});
} else {
logDebug(
LogModule.FRP_CLIENT,
"frpc process is not running or has been killed."
);
}
};
/**
* frpc子进程
*/
export const stopFrpcProcess = (callback?: () => void) => {
if (frpcProcess) {
treeKill(frpcProcess.pid, (error: Error) => {
if (error) {
log.error(`关闭frpc子进程失败 pid${frpcProcess.pid} error${error}`)
callback()
} else {
log.info(`关闭frpc子进程成功`)
frpcProcess = null
clearInterval(frpcStatusListener)
callback()
}
})
} else {
callback()
}
}
if (frpcProcess) {
treeKill(frpcProcess.pid, (error: Error) => {
if (error) {
logError(
LogModule.FRP_CLIENT,
`Failed to stop frpc process with pid: ${frpcProcess.pid}. Error: ${error.message}`
);
callback();
} else {
logInfo(
LogModule.FRP_CLIENT,
`Successfully stopped frpc process with pid: ${frpcProcess.pid}.`
);
frpcProcess = null;
clearInterval(frpcStatusListener);
callback();
}
});
} else {
logWarn(
LogModule.FRP_CLIENT,
"Attempted to stop frpc process, but no process is running."
);
logWarn(LogModule.FRP_CLIENT, "No frpc process to stop.");
callback();
}
};
/**
* frpc子进程状态
*/
export const frpcProcessStatus = () => {
if (!frpcProcess) {
return false;
}
try {
// 发送信号给进程,如果进程存在,会正常返回
process.kill(frpcProcess.pid, 0);
return true;
} catch (error) {
// 进程不存在,抛出异常
return false;
}
}
if (!frpcProcess) {
logDebug(LogModule.FRP_CLIENT, "frpc process is not running.");
return false;
}
try {
// 发送信号给进程,如果进程存在,会正常返回
process.kill(frpcProcess.pid, 0);
logDebug(
LogModule.FRP_CLIENT,
`frpc process is running with pid: ${frpcProcess.pid}`
);
return true;
} catch (error) {
// 进程不存在,抛出异常
logError(
LogModule.FRP_CLIENT,
`frpc process not found. Error: ${error.message}`
);
return false;
}
};
/**
* frpc流程
* @param config
*/
export const startFrpWorkerProcess = async (config: Config) => {
getFrpcVersionWorkerPath(
config.currentVersion,
(frpcVersionPath: string) => {
if (frpcVersionPath) {
generateConfig(config, configPath => {
const platform = process.platform;
if (platform === 'win32') {
startFrpcProcess(
path.join(frpcVersionPath, "frpc.exe"),
configPath
);
} else {
startFrpcProcess(
path.join(frpcVersionPath, "frpc"),
configPath
);
}
});
}
export const startFrpWorkerProcess = async (config: FrpConfig) => {
logInfo(LogModule.FRP_CLIENT, "Starting frpc worker process...");
getFrpcVersionWorkerPath(config.currentVersion, (frpcVersionPath: string) => {
if (frpcVersionPath) {
logInfo(
LogModule.FRP_CLIENT,
`Found frpc version path: ${frpcVersionPath}`
);
generateConfig(config, configPath => {
const platform = process.platform;
if (platform === "win32") {
logInfo(LogModule.FRP_CLIENT, "Starting frpc on Windows.");
startFrpcProcess(path.join(frpcVersionPath, "frpc.exe"), configPath);
} else {
logInfo(
LogModule.FRP_CLIENT,
"Starting frpc on non-Windows platform."
);
startFrpcProcess(path.join(frpcVersionPath, "frpc"), configPath);
}
);
}
});
} else {
logError(LogModule.FRP_CLIENT, "frpc version path not found.");
}
});
};
export const initFrpcApi = () => {
ipcMain.handle("frpc.running", async (event, args) => {
return frpcProcessStatus()
});
ipcMain.handle("frpc.running", async (event, args) => {
logDebug(LogModule.FRP_CLIENT, "Checking if frpc process is running...");
return frpcProcessStatus();
});
ipcMain.on("frpc.start", async (event, args) => {
getConfig((err1, config) => {
if (!err1) {
if (config) {
startFrpWorkerProcess(config)
} else {
event.reply(
"Home.frpc.start.error.hook", "请先前往设置页面,修改配置后再启动"
);
}
}
});
});
ipcMain.on("frpc.stop", () => {
if (frpcProcess && !frpcProcess.killed) {
stopFrpcProcess(() => {
})
ipcMain.on("frpc.start", async (event, args) => {
logInfo(LogModule.FRP_CLIENT, "Received request to start frpc process.");
getConfig((err1, config) => {
if (!err1) {
if (!config) {
logWarn(
LogModule.FRP_CLIENT,
"Configuration not found. Prompting user to set configuration."
);
event.reply(
"Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动"
);
return;
}
if (!config.currentVersion) {
logWarn(
LogModule.FRP_CLIENT,
"Current version not set in configuration. Prompting user."
);
event.reply(
"Home.frpc.start.error.hook",
"请先前往设置页面,修改配置后再启动"
);
return;
}
startFrpWorkerProcess(config);
} else {
logError(LogModule.FRP_CLIENT, `Error getting configuration: ${err1}`);
}
});
});
ipcMain.on("frpc.stop", () => {
logInfo(LogModule.FRP_CLIENT, "Received request to stop frpc process.");
if (frpcProcess && !frpcProcess.killed) {
stopFrpcProcess(() => {});
} else {
logWarn(LogModule.FRP_CLIENT, "No frpc process to stop.");
}
});
};

View File

@ -1,241 +1,598 @@
import electron, {app, BrowserWindow, ipcMain, net, shell} from "electron";
import {deleteVersionById, insertVersion} from "../storage/version";
import electron, {
app,
dialog,
BrowserWindow,
ipcMain,
net,
shell
} from "electron";
import {
deleteVersionById,
getVersionById,
insertVersion,
listVersion
} from "../storage/version";
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const {download} = require("electron-dl");
const AdmZip = require('adm-zip');
const log = require('electron-log');
const { download } = require("electron-dl");
const AdmZip = require("adm-zip");
import frpReleasesJson from "../json/frp-releases.json";
import frpChecksums from "../json/frp_all_sha256_checksums.json";
import { logInfo, logError, LogModule, logDebug, logWarn } from "../utils/log";
import { calculateFileChecksum, formatBytes } from "../utils/file";
import { el } from "element-plus/es/locale";
const versionRelation = {
"win32_x64": ["window", "amd64"],
"win32_arm64": ["window", "arm64"],
"win32_ia32": ["window", "386"],
"darwin_arm64": ["darwin", "arm64"],
"darwin_x64": ["darwin", "amd64"],
"darwin_amd64": ["darwin", "amd64"],
"linux_x64": ["linux", "amd64"],
"linux_arm64": ["linux", "arm64"],
}
win32_x64: ["window", "amd64"],
win32_arm64: ["window", "arm64"],
win32_ia32: ["window", "386"],
darwin_arm64: ["darwin", "arm64"],
darwin_x64: ["darwin", "amd64"],
darwin_amd64: ["darwin", "amd64"],
linux_x64: ["linux", "amd64"],
linux_arm64: ["linux", "arm64"]
};
const platform = process.platform;
const arch = process.arch;
let currArch = `${platform}_${arch}`
const frpArch = versionRelation[currArch]
let currArch = `${platform}_${arch}`;
const frpArch = versionRelation[currArch];
const unTarGZ = (tarGzPath: string, targetPath: string) => {
const tar = require("tar");
const unzip = zlib.createGunzip();
log.debug(`开始解压tar.gz${tarGzPath} 目标目录:${targetPath}`);
const readStream = fs.createReadStream(tarGzPath);
if (!fs.existsSync(unzip)) {
fs.mkdirSync(targetPath, {recursive: true});
}
readStream.pipe(unzip).pipe(
tar.extract({
cwd: targetPath,
filter: filePath => path.basename(filePath) === "frpc"
const tar = require("tar");
const unzip = zlib.createGunzip();
logInfo(
LogModule.APP,
`Starting to extract tar.gz: ${tarGzPath} to ${targetPath}`
);
const readStream = fs.createReadStream(tarGzPath);
if (!fs.existsSync(unzip)) {
fs.mkdirSync(targetPath, { recursive: true, mode: 0o777 });
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
}
readStream.on("error", err => {
logError(LogModule.APP, `Error reading tar.gz file: ${err.message}`);
});
readStream
.pipe(unzip)
.on("error", err => {
logError(LogModule.APP, `Error during gunzip: ${err.message}`);
})
.pipe(
tar
.extract({
cwd: targetPath,
filter: filePath => path.basename(filePath) === "frpc"
})
);
const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz"));
log.debug(`解压完成 解压后目录:${frpcPath}`);
return frpcPath;
// .on("finish", () => {
// console.log("解压完成!");
// });
.on("error", err => {
logError(LogModule.APP, `Error extracting tar file: ${err.message}`);
})
)
.on("finish", () => {
const frpcPath = path.join("frp", path.basename(tarGzPath, ".tar.gz"));
logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
});
return path.join("frp", path.basename(tarGzPath, ".tar.gz"));
};
const unZip = (zipPath: string, targetPath: string) => {
if (!fs.existsSync(path.join(targetPath, path.basename(zipPath, ".zip")))) {
fs.mkdirSync(path.join(targetPath, path.basename(zipPath, ".zip")), {recursive: true});
}
log.debug(`开始解压zip${zipPath} 目标目录:${targetPath}`);
/**
* unzipper解压
*/
// fs.createReadStream(zipPath)
// .pipe(unzipper.Extract({ path: targetPath }))
// // 只解压frpc.exe
// // .pipe(unzipper.ParseOne('frpc'))
// // .pipe(fs.createWriteStream(path.join(targetPath, path.basename(zipPath, ".zip"), "frpc.exe")))
// .on('finish', () => {
// console.log('File extracted successfully.');
// })
// .on('error', (err) => {
// console.error('Error extracting file:', err);
// });
if (!fs.existsSync(path.join(targetPath, path.basename(zipPath, ".zip")))) {
fs.mkdirSync(path.join(targetPath, path.basename(zipPath, ".zip")), {
recursive: true
});
logInfo(LogModule.APP, `Created target directory: ${targetPath}`);
logInfo(
LogModule.APP,
`Created directory for zip extraction: ${path.basename(zipPath, ".zip")}`
);
}
const zip = new AdmZip(zipPath)
logDebug(
LogModule.APP,
`Starting to unzip: ${zipPath} to target directory: ${targetPath}`
);
logInfo(LogModule.APP, `Starting to extract zip file: ${zipPath}`);
const zip = new AdmZip(zipPath);
try {
zip.extractAllTo(targetPath, true); // 第二个参数为 true表示覆盖已存在的文件
const frpcPath = path.join("frp", path.basename(zipPath, ".zip"));
log.debug(`解压完成 解压后目录:${frpcPath}`);
return frpcPath
}
logInfo(
LogModule.APP,
`Extraction completed. Extracted directory: ${frpcPath}`
);
logDebug(
LogModule.APP,
`Unzip completed. Extracted directory: ${frpcPath}`
);
return frpcPath;
} catch (error) {
logError(LogModule.APP, `Error extracting zip file: ${error.message}`);
}
export const initGitHubApi = () => {
// 版本
let versions = [];
const getVersion = versionId => {
return versions.find(f => f.id === versionId);
};
const getAdaptiveAsset = versionId => {
const {assets} = getVersion(versionId);
const asset = assets.find(
f => {
// const a = versionRelation[currArch]
const a = frpArch
if (a) {
const flag = a.every(item => f.name.includes(item))
return flag;
}
return false;
}
);
if (asset) {
log.info(`找到对应版本 ${asset.name}`)
}
return asset;
};
/**
* github上的frp所有版本
*/
ipcMain.on("github.getFrpVersions", async event => {
const request = net.request({
method: "get",
url: "https://api.github.com/repos/fatedier/frp/releases?page=1&per_page=1000"
});
request.on("response", response => {
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
versions = JSON.parse(responseData.toString());
// const borderContent: Electron.WebContents =
// BrowserWindow.getFocusedWindow().webContents;
const downloadPath = path.join(app.getPath("userData"), "download");
log.info(`开始获取frp版本 当前架构:${currArch} 对应frp架构${frpArch}`)
const returnVersionsData = versions
.filter(f => getAdaptiveAsset(f.id))
.map(m => {
const asset = getAdaptiveAsset(m.id);
if (asset) {
const absPath = `${downloadPath}/${asset.name}`;
m.absPath = absPath;
m.download_completed = fs.existsSync(absPath);
}
return m;
});
// log.debug(`获取到frp版本${JSON.stringify(returnVersionsData)}`)
event.reply("Download.frpVersionHook", returnVersionsData);
});
});
request.end();
});
/**
*
*/
ipcMain.on("github.download", async (event, args) => {
const version = getVersion(args);
const asset = getAdaptiveAsset(args);
const {browser_download_url} = asset;
log.info(`开始下载frp url${browser_download_url} asset${asset.name}`)
// 数据目录
await download(BrowserWindow.getFocusedWindow(), browser_download_url, {
filename: `${asset.name}`,
directory: path.join(app.getPath("userData"), "download"),
onProgress: progress => {
event.reply("Download.frpVersionDownloadOnProgress", {
id: args,
progress: progress
});
},
onCompleted: () => {
log.info(`frp下载完成 url${browser_download_url} asset${asset.name}`)
const targetPath = path.resolve(path.join(app.getPath("userData"), "frp"));
const ext = path.extname(asset.name)
let frpcVersionPath = ""
if (ext === '.zip') {
frpcVersionPath = unZip(path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
),
targetPath)
} else if (ext === '.gz' && asset.name.includes(".tar.gz")) {
frpcVersionPath = unTarGZ(
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
),
targetPath
);
}
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
event.reply("Download.frpVersionDownloadOnCompleted", args);
version.download_completed = true;
}
});
}
});
});
/**
*
*/
ipcMain.on("github.deleteVersion", async (event, args) => {
const {absPath, id} = args;
if (fs.existsSync(absPath)) {
deleteVersionById(id, () => {
fs.unlinkSync(absPath)
})
}
event.reply("Download.deleteVersion.hook", {
err: null,
data: "删除成功"
});
})
/**
*
*/
ipcMain.on("github.getFrpcDesktopLastVersions", async event => {
const request = net.request({
method: "get",
url: "https://api.github.com/repos/luckjiawei/frpc-desktop/releases/latest"
});
request.on("response", response => {
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
versions = JSON.parse(responseData.toString());
// const borderContent: Electron.WebContents =
// BrowserWindow.getFocusedWindow().webContents;
// const downloadPath = path.join(app.getPath("userData"), "download");
// log.info(`开始获取frp版本 当前架构:${currArch} 对应frp架构${frpArch}`)
// const returnVersionsData = versions
// .filter(f => getAdaptiveAsset(f.id))
// .map(m => {
// const asset = getAdaptiveAsset(m.id);
// if (asset) {
// const absPath = `${downloadPath}/${asset.name}`;
// m.absPath = absPath;
// m.download_completed = fs.existsSync(absPath);
// }
// return m;
// });
// log.debug(`获取到frp版本${JSON.stringify(returnVersionsData)}`)
event.reply("github.getFrpcDesktopLastVersionsHook", versions);
});
});
request.end();
})
return null;
};
export const initGitHubApi = win => {
// 版本
let versions: FrpVersion[] = [];
const getVersionByGithubVersionId = versionId => {
logDebug(LogModule.APP, `Attempting to get version with ID: ${versionId}`);
const version = versions.find(f => f.id === versionId);
if (version) {
logInfo(
LogModule.APP,
`Version details ID:${version.id}, Name:${version.name}, Published At:${version.published_at}`
);
} else {
logWarn(LogModule.APP, `No version found for ID: ${versionId}`);
}
return version;
};
const getVersionByAssetName = (assetName: string) => {
logDebug(
LogModule.APP,
`Attempting to get version with asset name: ${assetName}`
);
const version = versions.find(f =>
f.assets.some(asset => asset.name === assetName)
);
if (version) {
logInfo(
LogModule.APP,
`Version details ID:${version.id}, Name:${version.name}, Published At:${version.published_at}`
);
} else {
logWarn(LogModule.APP, `No version found for asset name: ${assetName}`);
}
return version;
};
const getAdaptiveAsset = versionId => {
const { assets } = getVersionByGithubVersionId(versionId);
if (!assets || assets.length === 0) {
logWarn(LogModule.GITHUB, `No assets found for version ID: ${versionId}`);
return null;
}
const asset = assets.find(f => {
const a = frpArch;
if (a) {
const flag = a.every(item => f.name.includes(item));
if (flag) {
logInfo(
LogModule.GITHUB,
`Found matching asset: ${f.name} for version ID: ${versionId}`
);
}
return flag;
}
logWarn(
LogModule.GITHUB,
`No architecture match found for asset: ${f.name}`
);
return false;
});
if (!asset) {
logError(
LogModule.GITHUB,
`No adaptive asset found for version ID: ${versionId}`
);
}
return asset;
};
/**
* handle github api release json
* @param githubReleaseJsonStr jsonStr
* @returns versions
*/
const handleApiResponse = (githubReleaseJsonStr: string) => {
const downloadPath = path.join(app.getPath("userData"), "download");
const frpPath = path.join(app.getPath("userData"), "frp");
logInfo(LogModule.GITHUB, "Parsing GitHub release JSON response.");
versions = JSON.parse(githubReleaseJsonStr);
if (versions) {
logInfo(
LogModule.GITHUB,
"Successfully parsed versions from GitHub response."
);
const returnVersionsData = versions
.filter(f => getAdaptiveAsset(f.id))
.map(m => {
const asset = getAdaptiveAsset(m.id);
const download_count = m.assets.reduce(
(sum, item) => sum + item.download_count,
0
);
if (asset) {
const absPath = path.join(
frpPath,
asset.name.replace(/(\.tar\.gz|\.zip)$/, "")
);
m.absPath = absPath;
m.download_completed = fs.existsSync(absPath);
m.download_count = download_count;
m.size = formatBytes(asset.size);
logInfo(
LogModule.GITHUB,
`Asset found: ${asset.name}, download count: ${download_count}`
);
} else {
logWarn(LogModule.GITHUB, `No asset found for version ID: ${m.id}`);
}
return m;
});
logDebug(
LogModule.GITHUB,
`Retrieved FRP versions: ${JSON.stringify(returnVersionsData)}`
);
return returnVersionsData;
} else {
logError(
LogModule.GITHUB,
"Failed to parse versions: No versions found in response."
);
return [];
}
};
/**
* conventMirrorUrl
* @param mirror mirror
* @returns mirrorUrl
*/
const conventMirrorUrl = (mirror: string) => {
switch (mirror) {
case "github":
return {
api: "https://api.github.com",
asset: "https://github.com"
};
default:
return {
api: "https://api.github.com",
asset: "https://github.com"
};
}
};
/**
* github上的frp所有版本
*/
ipcMain.on("github.getFrpVersions", async (event, mirror: string) => {
const { api } = conventMirrorUrl(mirror);
const mirrorUrl = api;
logInfo(LogModule.GITHUB, `Requesting mirror URL: ${mirrorUrl}`);
const request = net.request({
method: "get",
url: `${mirrorUrl}/repos/fatedier/frp/releases?page=1&per_page=1000`
});
let githubReleaseJsonStr = null;
request.on("response", response => {
logInfo(
LogModule.GITHUB,
`Received response with status code: ${response.statusCode}`
);
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
if (response.statusCode === 200) {
githubReleaseJsonStr = responseData.toString();
logInfo(
LogModule.GITHUB,
"Successfully retrieved GitHub release data."
);
} else {
logWarn(
LogModule.GITHUB,
"Failed to retrieve data, using local JSON instead. Status code: " +
response.statusCode
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
}
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
});
request.on("error", error => {
logError(
LogModule.GITHUB,
"Error occurred while requesting GitHub releases: " + error
);
githubReleaseJsonStr = JSON.stringify(frpReleasesJson);
const versions = handleApiResponse(githubReleaseJsonStr);
event.reply("Download.frpVersionHook", versions);
});
request.end();
});
const decompressFrp = (frpFilename: string, compressedFilePath: string) => {
const targetPath = path.resolve(path.join(app.getPath("userData"), "frp"));
const ext = path.extname(frpFilename);
let frpcVersionPath = "";
try {
if (ext === ".zip") {
unZip(
// path.join(
// path.join(app.getPath("userData"), "download"),
// `${frpFilename}`
// ),
compressedFilePath,
targetPath
);
logInfo(LogModule.APP, `Unzipped file to path: ${frpcVersionPath}`);
frpcVersionPath = path.join("frp", path.basename(frpFilename, ".zip"));
} else if (ext === ".gz" && frpFilename.includes(".tar.gz")) {
unTarGZ(
// path.join(
// path.join(app.getPath("userData"), "download"),
// `${frpFilename}`
// ),
compressedFilePath,
targetPath
);
frpcVersionPath = path.join(
"frp",
path.basename(frpFilename, ".tar.gz")
);
logInfo(LogModule.APP, `Untarred file to path: ${frpcVersionPath}`);
}
} catch (error) {
logError(LogModule.APP, `Error during extraction: ${error.message}`);
}
return frpcVersionPath;
};
/**
*
*/
ipcMain.on("github.download", async (event, args) => {
const { versionId, mirror } = args;
const version = getVersionByGithubVersionId(versionId);
const asset = getAdaptiveAsset(versionId);
const { browser_download_url } = asset;
let url = browser_download_url.replace(
"https://github.com",
conventMirrorUrl(mirror).asset
);
logDebug(
LogModule.GITHUB,
`Starting download for versionId: ${versionId}, mirror: ${mirror}, download URL: ${url}`
);
await download(BrowserWindow.getFocusedWindow(), url, {
filename: `${asset.name}`,
directory: path.join(app.getPath("userData"), "download"),
onProgress: progress => {
event.reply("Download.frpVersionDownloadOnProgress", {
id: versionId,
progress: progress
});
logDebug(
LogModule.GITHUB,
`Download progress for versionId: ${versionId} is ${
progress.percent * 100
}%`
);
},
onCompleted: () => {
logInfo(
LogModule.GITHUB,
`Download completed for versionId: ${versionId}, asset: ${asset.name}`
);
const frpcVersionPath = decompressFrp(
asset.name,
path.join(
path.join(app.getPath("userData"), "download"),
`${asset.name}`
)
);
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
listVersion((err, doc) => {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.frpVersionDownloadOnCompleted", versionId);
version.download_completed = true;
logInfo(
LogModule.GITHUB,
`Version ${versionId} has been inserted successfully.`
);
});
} else {
logError(LogModule.GITHUB, `Error inserting version: ${err}`);
}
});
}
});
});
/**
*
*/
ipcMain.on("github.deleteVersion", async (event, args) => {
const { absPath, id } = args;
logDebug(
LogModule.GITHUB,
`Attempting to delete version with ID: ${id} and path: ${absPath}`
);
if (fs.existsSync(absPath)) {
// if (process.platform === 'darwin') {
// fs.unlinkSync(absPath.replace(/ /g, '\\ '));
// }else{
// fs.unlinkSync(absPath);
// }
fs.rmSync(absPath, { recursive: true, force: true });
deleteVersionById(id, () => {
logInfo(
LogModule.GITHUB,
`Successfully deleted version with ID: ${id}`
);
});
} else {
logWarn(
LogModule.GITHUB,
`Version with ID: ${id} not found at path: ${absPath}`
);
}
listVersion((err, doc) => {
if (err) {
logError(LogModule.GITHUB, `Error listing versions: ${err}`);
} else {
event.reply("Config.versions.hook", { err, data: doc });
event.reply("Download.deleteVersion.hook", {
err: null,
data: "删除成功"
});
}
});
});
/**
*
*/
ipcMain.on("github.getFrpcDesktopLastVersions", async event => {
logInfo(LogModule.GITHUB, "Requesting the latest version from GitHub.");
const request = net.request({
method: "get",
url: "https://api.github.com/repos/luckjiawei/frpc-desktop/releases/latest"
});
request.on("response", response => {
let responseData: Buffer = Buffer.alloc(0);
response.on("data", (data: Buffer) => {
responseData = Buffer.concat([responseData, data]);
});
response.on("end", () => {
try {
versions = JSON.parse(responseData.toString());
logInfo(
LogModule.GITHUB,
"Successfully retrieved the latest version."
);
event.reply("github.getFrpcDesktopLastVersionsHook", versions);
} catch (error) {
logError(
LogModule.GITHUB,
`Error parsing response data: ${error.message}`
);
}
});
});
request.on("error", error => {
logError(LogModule.GITHUB, `Request error: ${error.message}`);
});
request.end();
});
ipcMain.on(
"download.importFrpFile",
async (event, filePath: string, targetPath: string) => {
const result = await dialog.showOpenDialog(win, {
properties: ["openFile"],
filters: [
{ name: "Frp 文件", extensions: ["tar.gz", "zip"] } // 允许选择的文件类型,分开后缀以确保可以选择
]
});
if (result.canceled) {
logWarn(LogModule.APP, "Import canceled by user.");
logWarn(LogModule.GITHUB, "User canceled the file import operation.");
return;
} else {
const filePath = result.filePaths[0];
// const fileExtension = path.extname(filePath);
logInfo(LogModule.APP, `User selected file: ${filePath}`);
const checksum = calculateFileChecksum(filePath);
logInfo(LogModule.APP, `Calculated checksum for the file: ${checksum}`);
const frpName = frpChecksums[checksum];
if (frpName) {
logInfo(LogModule.APP, `FRP file name found: ${frpName}`);
if (frpArch.every(item => frpName.includes(item))) {
logInfo(
LogModule.APP,
`Architecture matches for FRP file: ${frpName}`
);
const version = getVersionByAssetName(frpName);
getVersionById(version.id, (err, existingVersion) => {
if (!err && existingVersion) {
logInfo(
LogModule.APP,
`Version already exists: ${JSON.stringify(existingVersion)}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,版本已存在`
});
return; // 终止后续执行
}
const frpcVersionPath = decompressFrp(frpName, filePath);
logInfo(
LogModule.APP,
`Successfully decompressed FRP file: ${frpName} to path: ${frpcVersionPath}`
);
version["frpcVersionPath"] = frpcVersionPath;
insertVersion(version, (err, document) => {
if (!err) {
listVersion((err, doc) => {
event.reply("Config.versions.hook", { err, data: doc });
version.download_completed = true;
event.reply("Download.importFrpFile.hook", {
success: true,
data: `导入成功`
});
});
} else {
logError(LogModule.APP, `Error inserting version: ${err}`);
event.reply("Download.importFrpFile.hook", {
success: true,
data: `导入失败,未知错误`
});
}
});
});
} else {
logWarn(
LogModule.APP,
`Architecture does not match for FRP file: ${frpName}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,所选 frp 架构与操作系统不符`
});
}
} else {
logWarn(
LogModule.APP,
`No matching FRP file name found for checksum: ${checksum}`
);
event.reply("Download.importFrpFile.hook", {
success: false,
data: `导入失败,无法识别文件`
});
}
}
}
);
};

View File

@ -1,5 +1,5 @@
import {ipcMain} from "electron";
import log from "electron-log";
import { logDebug, logError, logInfo, LogModule, logWarn } from "../utils/log";
const {exec, spawn} = require("child_process");
@ -15,19 +15,19 @@ export const initLocalApi = () => {
: 'netstat -an | grep LISTEN';
ipcMain.on("local.getLocalPorts", async (event, args) => {
log.info("开始获取本地端口")
logInfo(LogModule.APP, "Starting to retrieve local ports");
// 执行命令
exec(command, (error, stdout, stderr) => {
if (error) {
log.error(`getLocalPorts - error ${error.message}`)
logError(LogModule.APP, `getLocalPorts - error: ${error.message}`);
return;
}
if (stderr) {
log.error(`getLocalPorts - stderr ${stderr}`)
logWarn(LogModule.APP, `getLocalPorts - stderr: ${stderr}`);
return;
}
log.debug(`sc ${stdout}`)
logDebug(LogModule.APP, `Command output: ${stdout}`);
let ports = [];
if (stdout) {
if (process.platform === 'win32') {
@ -72,7 +72,6 @@ export const initLocalApi = () => {
}
return singe;
})
// .filter(f => f.indexOf('TCP') > 0 || f.indexOf('UDP') > 0)
} else if (process.platform === 'linux') {
ports = stdout.split('\n')

View File

@ -1,4 +1,5 @@
import { app, ipcMain } from "electron";
import { app, ipcMain, shell } from "electron";
import { logInfo, logError, LogModule } from "../utils/log";
const fs = require("fs");
const path = require("path");
@ -8,24 +9,45 @@ export const initLoggerApi = () => {
const readLogger = (callback: (content: string) => void) => {
fs.readFile(logPath, "utf-8", (error, data) => {
if (!error) {
logInfo(LogModule.APP, "Log file read successfully.");
callback(data);
} else {
logError(LogModule.APP, `Error reading log file: ${error.message}`);
}
});
};
ipcMain.on("logger.getLog", async (event, args) => {
logInfo(LogModule.APP, "Received request to get log.");
readLogger(content => {
event.reply("Logger.getLog.hook", content);
logInfo(LogModule.APP, "Log data sent to client.");
});
});
ipcMain.on("logger.update", (event, args) => {
logInfo(LogModule.APP, "Watching log file for changes.");
fs.watch(logPath, (eventType, filename) => {
if (eventType === "change") {
logInfo(LogModule.APP, "Log file changed, reading new content.");
readLogger(content => {
event.reply("Logger.update.hook", content);
logInfo(LogModule.APP, "Updated log data sent to client.");
});
}
});
});
ipcMain.on("logger.openLog", (event, args) => {
logInfo(LogModule.APP, "Attempting to open log file.");
shell.openPath(logPath).then((errorMessage) => {
if (errorMessage) {
logError(LogModule.APP, `Failed to open Logger: ${errorMessage}`);
event.reply("Logger.openLog.hook", false);
} else {
logInfo(LogModule.APP, "Logger opened successfully.");
event.reply("Logger.openLog.hook", true);
}
});
});
};

View File

@ -4,15 +4,20 @@ import {
getProxyById,
insertProxy,
listProxy,
updateProxyById
updateProxyById,
updateProxyStatus
} from "../storage/proxy";
import { reloadFrpcProcess } from "./frpc";
import { logError, logInfo, LogModule, logWarn } from "../utils/log";
export const initProxyApi = () => {
ipcMain.on("proxy.getProxys", async (event, args) => {
logInfo(LogModule.APP, "Requesting to get proxies.");
listProxy((err, documents) => {
if (err) {
logError(LogModule.APP, `Error retrieving proxies: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxies retrieved successfully.");
}
event.reply("Proxy.getProxys.hook", {
err: err,
data: documents
@ -22,9 +27,13 @@ export const initProxyApi = () => {
ipcMain.on("proxy.insertProxy", async (event, args) => {
delete args["_id"];
logInfo(LogModule.APP, "Inserting a new proxy.");
insertProxy(args, (err, documents) => {
if (!err) {
reloadFrpcProcess()
if (err) {
logError(LogModule.APP, `Error inserting proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy inserted successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.insertProxy.hook", {
err: err,
@ -34,9 +43,13 @@ export const initProxyApi = () => {
});
ipcMain.on("proxy.deleteProxyById", async (event, args) => {
logInfo(LogModule.APP, `Deleting proxy with ID: ${args._id}`);
deleteProxyById(args, (err, documents) => {
if (!err) {
reloadFrpcProcess()
if (err) {
logError(LogModule.APP, `Error deleting proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy deleted successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.deleteProxyById.hook", {
err: err,
@ -46,7 +59,13 @@ export const initProxyApi = () => {
});
ipcMain.on("proxy.getProxyById", async (event, args) => {
logInfo(LogModule.APP, `Requesting proxy with ID: ${args._id}`);
getProxyById(args, (err, documents) => {
if (err) {
logError(LogModule.APP, `Error retrieving proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy retrieved successfully.");
}
event.reply("Proxy.getProxyById.hook", {
err: err,
data: documents
@ -55,10 +74,17 @@ export const initProxyApi = () => {
});
ipcMain.on("proxy.updateProxy", async (event, args) => {
if (!args._id) return;
if (!args._id) {
logWarn(LogModule.APP, "No proxy ID provided for update.");
return;
}
logInfo(LogModule.APP, `Updating proxy with ID: ${args._id}`);
updateProxyById(args, (err, documents) => {
if (!err) {
reloadFrpcProcess()
if (err) {
logError(LogModule.APP, `Error updating proxy: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy updated successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.updateProxy.hook", {
err: err,
@ -66,4 +92,24 @@ export const initProxyApi = () => {
});
});
});
ipcMain.on("proxy.updateProxyStatus", async (event, args) => {
logInfo(LogModule.APP, `Updating status for proxy ID: ${args._id}`);
if (!args._id) {
logWarn(LogModule.APP, "No proxy ID provided for status update.");
return;
}
updateProxyStatus(args._id, args.status, (err, documents) => {
if (err) {
logError(LogModule.APP, `Error updating proxy status: ${err.message}`);
} else {
logInfo(LogModule.APP, "Proxy status updated successfully.");
reloadFrpcProcess();
}
event.reply("Proxy.updateProxyStatus.hook", {
err: err,
data: documents
});
});
});
};

View File

@ -56,10 +56,7 @@ export const initUpdaterApi = (win: BrowserWindow) => {
})
autoUpdater.on('update-downloaded', () => {
console.log('update-downloaded')
dialog.showMessageBox({
type: 'info',
title: '应用更新',

49
electron/json/extract.py Normal file
View File

@ -0,0 +1,49 @@
import json
import os
import requests
import logging
# Set up logging configuration
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def extract_frp_releases():
current_dir = os.path.dirname(os.path.abspath(__file__))
json_file_path = os.path.join(current_dir, 'frp-releases.json')
logging.info('Reading JSON file: %s', json_file_path)
with open(json_file_path, 'r', encoding='utf-8') as file:
data = json.load(file)
checksums = {}
for i in data:
for asset in i['assets']:
if asset['name'] == 'frp_sha256_checksums.txt':
logging.info('Found checksum file: %s', asset['browser_download_url'])
content = fetch_txt_content(asset['browser_download_url'])
if content:
lines = content.splitlines()
for line in lines:
if line.strip(): # Ensure the line is not empty
parts = line.split()
if len(parts) == 2:
checksums[parts[0]] = parts[1] # 反转映射关系
logging.debug('Added checksum: %s -> %s', parts[0], parts[1])
output_file_path = os.path.join(current_dir, 'frp_all_sha256_checksums.json')
logging.info('Writing checksums to file: %s', output_file_path)
with open(output_file_path, 'w', encoding='utf-8') as output_file:
json.dump(checksums, output_file, ensure_ascii=False, indent=4)
def fetch_txt_content(url):
logging.info('Fetching content from: %s', url)
response = requests.get(url)
if response.status_code == 200:
logging.info('Successfully fetched content')
return response.text
else:
logging.error('Failed to fetch content, status code: %d', response.status_code)
return None
if __name__ == "__main__":
extract_frp_releases()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,440 @@
{
"013179d5fd3a5c1a48cf8b2da99c8d48f0f95b96907ad77483deb4d8615578f2": "frp_0.61.1_android_arm64.tar.gz",
"403a0ee5e92f083a863d984b7af1e9d70ba2aaa28e87f42f1fe085adf76b8491": "frp_0.61.1_darwin_amd64.tar.gz",
"3e65f13a17a284bd6013e6bb6856bc2720074cea6094cc446c1f4c3932154c2d": "frp_0.61.1_darwin_arm64.tar.gz",
"cb5edf41be38bd47bd7f68210c34500f5dac74423d8084ed8d7e0f3bce050cf5": "frp_0.61.1_freebsd_amd64.tar.gz",
"bff260b68ca7b1461182a46c4f34e9709ba32764eed30a15dd94ac97f50a2c40": "frp_0.61.1_linux_amd64.tar.gz",
"8c7ebb07916e69d959d2a90f80900850c41b6dc1decdb87ccba4bc2f3574f855": "frp_0.61.1_linux_arm.tar.gz",
"af6366f2b43920ebfe6235dba6060770399ed1fb18601e5818552bd46a7621f8": "frp_0.61.1_linux_arm64.tar.gz",
"f956807c4af5cc0fa3b06f2365f342abcb4b8f780b6f948db5a9d8c02bb3e7ba": "frp_0.61.1_linux_arm_hf.tar.gz",
"e4b096fe86b09fa4b2c292b9c833dd9d3f322cbc4b67e8f73d40267f9451edc2": "frp_0.61.1_linux_loong64.tar.gz",
"8c0e0c1384a0e8601153f767392c23c5cd90e8112c522d4d0852c8b477909f8b": "frp_0.61.1_linux_mips.tar.gz",
"96d94c7676aad45b3cb1fb9bb7ee8cfc36ba4db9803fe77636d4bf711e79e0fb": "frp_0.61.1_linux_mips64.tar.gz",
"febb306ffbaa7a74e27fdd37919a342d148f27d33afc13a0ecbec73d68e5e204": "frp_0.61.1_linux_mips64le.tar.gz",
"539d8d91091410abbca80efae36f8a693a9132094f2340ff781b390c7eac5a50": "frp_0.61.1_linux_mipsle.tar.gz",
"7f75d6fc124e94da1703fa94cd9fec87bfe9c2a8c6c6c5bdda53266d5c0b82ca": "frp_0.61.1_linux_riscv64.tar.gz",
"e0094cd0baf03d5ff9ce9739199406871ad8788cf51e766f00ad3a9e7a836f3a": "frp_0.61.1_windows_amd64.zip",
"099914b6f66b1d983a9402e3a9a116bccc683c877f6dbfbbb5c0f171b584675f": "frp_0.61.1_windows_arm64.zip",
"6e4e4c3b73b4637658e8a19bd214efa67d24d18213d0470d8ab564b279935ab4": "frp_0.61.0_android_arm64.tar.gz",
"731bab6f15ac2dcfc63d9b3c2040ee68c465f8520c5369a8a6f37c467568280b": "frp_0.61.0_darwin_amd64.tar.gz",
"dafa23f2424ec1926f689a4d36d37e6e5b0050858755d2977a9e6171e1c8fb25": "frp_0.61.0_darwin_arm64.tar.gz",
"bc257408cc57d5982638f6aecda0ff2d3d46597b5b12d780c5135a8b72ea38c5": "frp_0.61.0_freebsd_amd64.tar.gz",
"720a9fe2a3299346572544909a78c023344c88bde13c55b921e298e8c5ded21f": "frp_0.61.0_linux_amd64.tar.gz",
"38b2d2f9a46b636dcdf4d656373de86f6c869da98a4e323bd9587989c1c06db0": "frp_0.61.0_linux_arm.tar.gz",
"8d54b8faae5df02268bd784f78a155494893c6eb00070a185022198c1997ec7f": "frp_0.61.0_linux_arm64.tar.gz",
"f151b5087870a72faa13c026336f3a6b97f0df2dcae3f3b122c43e604772cd23": "frp_0.61.0_linux_arm_hf.tar.gz",
"19928aeeca3806606958ea4843390cd427c05631e00afbd91b9a48855a18ba79": "frp_0.61.0_linux_loong64.tar.gz",
"601a8b19647db6258381d388861f4d64081fabae419788ae16bb8cd46390f2f7": "frp_0.61.0_linux_mips.tar.gz",
"fbbbab9339c87cfff52b4a7cedaaca0623921599fee10f845139ad7bdd6aa384": "frp_0.61.0_linux_mips64.tar.gz",
"b37c43ec11ae2ab50610eaf0251c9b3f694291d529afc757cd4645f7b8eb111a": "frp_0.61.0_linux_mips64le.tar.gz",
"4defa5cd6372d06188284f61fee90865222d8297a51005253dd6a5161ea538e4": "frp_0.61.0_linux_mipsle.tar.gz",
"0131890f58576631cd3dc8ea937ccd18f70f3162fb39840f56df8339f5799138": "frp_0.61.0_linux_riscv64.tar.gz",
"ab7ae0633df538b35d0490212eabfd6d3a0051e9e446ae0053c9f63921a8c46e": "frp_0.61.0_windows_amd64.zip",
"939f3f3c78be9221a9bc3a2380c1168fb5797ba66504d7188890bf93838ee6d5": "frp_0.61.0_windows_arm64.zip",
"4937324806d0c5aed7720d4e43f2a3fe1fb0e40d9b1b1c18c33417568d168c84": "frp_0.60.0_android_arm64.tar.gz",
"e0109b707168c98b95d2c0382637d0e8e83918d8fe0cdcb1794999f1750d33b9": "frp_0.60.0_darwin_amd64.tar.gz",
"e87da68fb5d3f9af6e7209b0c64cb6f22d3c9c3adfaad509c78c263ecaed7906": "frp_0.60.0_darwin_arm64.tar.gz",
"05222274b4577f71087e57273223a86e9dacfb6c53f9371c0c0e67f371aff146": "frp_0.60.0_freebsd_amd64.tar.gz",
"be5e957d20358d9998407204a03abc27d894f3cd61691f8d8ebe1e5f0eea4ca3": "frp_0.60.0_linux_amd64.tar.gz",
"e9d66ce99b14cadef5e850a6ca63b131486898ecaee580e6b90c5418ac4ccf72": "frp_0.60.0_linux_arm.tar.gz",
"bd52be08eaf872b07a48d9a3f46d7dd8f7e9340d76f12d316f39717f05d644e1": "frp_0.60.0_linux_arm64.tar.gz",
"75627af508bb31c37d13f6559b4acccdd6779304d56324ffc1cc737037055b40": "frp_0.60.0_linux_arm_hf.tar.gz",
"b58952173a2274759059ffa0d7ed3997f953852cddd4144bc3002611e23811a5": "frp_0.60.0_linux_loong64.tar.gz",
"9e3a587a164b24613fc587d3d17270d1b6623729b56135e6d63eebf56c41bb4c": "frp_0.60.0_linux_mips.tar.gz",
"2747e75da664dc9ca54baea63e499a36c0bd3ee34fc7950cec868e7502f7b192": "frp_0.60.0_linux_mips64.tar.gz",
"bfa63bb8f3d5f96033e5c58d41c8fb7bcdbbdbb6f790a77de5b84bf466f24529": "frp_0.60.0_linux_mips64le.tar.gz",
"ad7fc34df2e951a8de187ed7ae667dcb85bae68009f33a2a607da1b8a4caea1e": "frp_0.60.0_linux_mipsle.tar.gz",
"0f29c256bc20942ea5a19d8e6d3caacdd2f4d5134e632c50bfb384046666ae28": "frp_0.60.0_linux_riscv64.tar.gz",
"5243b70972af6416af85779b943945a80050b1733b31ab41fa94f3617f655492": "frp_0.60.0_windows_amd64.zip",
"1a41b2226a7478d3d86e275ca50f43e3587874973141bc43b552d258530f0443": "frp_0.60.0_windows_arm64.zip",
"5d3b92e28e0e293d87d32540bcc724a43ba2f4ebcc0549a1411d733e62f561cf": "frp_0.59.0_android_arm64.tar.gz",
"a784616e949fd9a00dc457e1b2e5fac871a9e420206f50200b5751f967af2d7d": "frp_0.59.0_darwin_amd64.tar.gz",
"54886307b97f60d698ddc0dd940514fc7653764580918f60a470a758eebf2323": "frp_0.59.0_darwin_arm64.tar.gz",
"bac63201e778139da931916328daf53ca575ab88f391c304b12550f04e63eb87": "frp_0.59.0_freebsd_amd64.tar.gz",
"54927eb5c07bd51850771ea55ca23338cdebdbe227d1acf7e2f0d530bd5e09c7": "frp_0.59.0_linux_amd64.tar.gz",
"2c958e363acedd08be939016ec12d17f66470e4da601b21247af1b31ea74e606": "frp_0.59.0_linux_arm.tar.gz",
"5b23e876071521d4f745f60fe09e81753cf84df22a0c7ab47d3692a09d758d01": "frp_0.59.0_linux_arm64.tar.gz",
"ca0a66dc4bd732f7c39116c9bce55a2b416898d3d1584b7eee1795a879779636": "frp_0.59.0_linux_arm_hf.tar.gz",
"a18840130e7056e9c8854ad1835d4b58b8df2c8151459a73dffdabe38b887b2b": "frp_0.59.0_linux_mips.tar.gz",
"e99827b49e17224b07c6c0af3876aa98771813aeead4d98d4e3c71a657c29211": "frp_0.59.0_linux_mips64.tar.gz",
"d32ae7a736082720b1672006783b5c17309287ee13bab552dddc99e17537bed7": "frp_0.59.0_linux_mips64le.tar.gz",
"6dec7136bc1ef26e14e5190d1c4809088f4173ffd140daf84d623bd180949eaa": "frp_0.59.0_linux_mipsle.tar.gz",
"e373ae9a57a286343e9e7fe1c4d1d4ff676680a0aea7041b75dde1ee24494158": "frp_0.59.0_linux_riscv64.tar.gz",
"a53405994ad03b245b36ab98513c8b7370bc0a36be128b057dc687354d6464da": "frp_0.59.0_windows_amd64.zip",
"f9978e5f98b22b1171b03925e73b2601aa485cdd4993dc7ef82557fa0d173ea6": "frp_0.59.0_windows_arm64.zip",
"c31e71a4b845e8ebc34bf5ddcc116a8230322101fff2418de8be9e46f1c6047e": "frp_0.58.1_android_arm64.tar.gz",
"e3a15197675afe2dda9ca7588d76d6597c30c7d55ed8fd2ae8c9d7425c8ce82e": "frp_0.58.1_darwin_amd64.tar.gz",
"be615731a6f34bf92c44765abe4a7df25d82226f255b1d1fda5f36a0ea083ab4": "frp_0.58.1_darwin_arm64.tar.gz",
"73576032ee89de285142e6d74f7bb6fb8cc5c373961e4c8910ded6aa13c3e88c": "frp_0.58.1_freebsd_amd64.tar.gz",
"5bd9f8860b580ed9c42eed1c99dfaa03b196d0f68007dca088f6c098d498430d": "frp_0.58.1_linux_amd64.tar.gz",
"44c59c70aaefe48dd9b0427c7e86a6a24c7e83fc31e38a7a2885dd1d53c05091": "frp_0.58.1_linux_arm.tar.gz",
"25a77f4d7f4c5efeeaa89ed65b951a19014e79baac1efcbd57f0598b3ba95fd7": "frp_0.58.1_linux_arm64.tar.gz",
"d1bbe041d05f48adebdd2a8b6754daf1062f66a8139a32589a10a31890348d49": "frp_0.58.1_linux_arm_hf.tar.gz",
"8a30318eeeb996364eefe40f8f946250fe709a1c487f795643c85a7b53cf86e5": "frp_0.58.1_linux_mips.tar.gz",
"ac445e3f75eca9bd33388ebe7c33d6124f5c3fa0c7a3419b5285dabe2e5415b9": "frp_0.58.1_linux_mips64.tar.gz",
"291d4fb93ccaf8c79087a21744a06716b9899b1bb71e9c2cb48c487adcee3bdb": "frp_0.58.1_linux_mips64le.tar.gz",
"db325305379dd39a82baf0910e5d01a426726231ff8775c2c6295232500d6bbb": "frp_0.58.1_linux_mipsle.tar.gz",
"710638bde211638e7cd096811f77b994cee0081cb0c9c80c7b48d92ae2452f28": "frp_0.58.1_linux_riscv64.tar.gz",
"7aa26c8c082c2fc7960b422811c1bbcbfae219b2a108d6604a645e8dc7476337": "frp_0.58.1_windows_amd64.zip",
"14cb8785617bdac2af87133061ec9a303824044bc8a64850db72e6b150bf0e71": "frp_0.58.1_windows_arm64.zip",
"d3845380ea2f5291f42c382d6d64afae694673d4a41b86be8d4c08e9f03c5b33": "frp_0.58.0_android_arm64.tar.gz",
"7ddf254bbce297e1ffbae0fd696e9a3a3414cdc8a2c270b00b59f45c19b8c5c5": "frp_0.58.0_darwin_amd64.tar.gz",
"0bd38fb57e46d13bd1fe7469a875a3123e298f4d0485feac8dd7a80d885f3676": "frp_0.58.0_darwin_arm64.tar.gz",
"a347a2e294986018591c43b029d85a6911c58a60df060fdb0de11b1d70bb916d": "frp_0.58.0_freebsd_amd64.tar.gz",
"09a6a170070ce02d7de6afa941d341a2caad0aa8e09fb840a64da4acba24d73a": "frp_0.58.0_linux_amd64.tar.gz",
"5e4b8b5ac36fe62f26ef37430aa56b11e562b046849796c51a61fd90251af18a": "frp_0.58.0_linux_arm.tar.gz",
"ceef6df44f3a64b4a2076dfbb0ac02a3750b48fa0ac568478190ef477d4f2ec9": "frp_0.58.0_linux_arm64.tar.gz",
"379ad22bf406186d8665e9be73b8b0600625abf7e8f6403fe3ef9d59e97c011c": "frp_0.58.0_linux_arm_hf.tar.gz",
"3ef58b278d97290540912692f053c1a5d48fc382a85a774f0b8b3ed060af067e": "frp_0.58.0_linux_mips.tar.gz",
"2b90360468e67cebb9a5e656585da0e9de84ee1a811ee9d02bb275b9e736bf7e": "frp_0.58.0_linux_mips64.tar.gz",
"3519353b094158f59f1f10a3bdbac473cf04bd09f572a0225c2ae5f545ee45bd": "frp_0.58.0_linux_mips64le.tar.gz",
"3ccccc5f76b3750137e9227ee904635266a0a434f4d6fae7d431382477372388": "frp_0.58.0_linux_mipsle.tar.gz",
"f024440f7987fc845814ecdf058f4363690a53850c241af5c7c01fba6632c78e": "frp_0.58.0_linux_riscv64.tar.gz",
"f827466f3f0e256350b66881721bd52753ef5243765e6c22347ec83a271d555f": "frp_0.58.0_windows_amd64.zip",
"8bb04979969040c4c495450bc7921b23054f2d7418f2d8378bd65ecbd8b381b2": "frp_0.58.0_windows_arm64.zip",
"e20d90b0670c637a65125f89467170efb3fc227a78f44ee585a6d3fb55b6a881": "frp_0.57.0_android_arm64.tar.gz",
"9a12912bfbf7dad0ebe5fb3b0229b318a8670d078137f2384f81c1aa87bc0fb0": "frp_0.57.0_darwin_amd64.tar.gz",
"b18153bc7a6d6627f402380a6e5ac01b631207df54d7fcc0d89a8f6f81521401": "frp_0.57.0_darwin_arm64.tar.gz",
"22df14e317c351bda4bfaf256c46b6ec281304135ea24c00bb2a71a5e14d4f22": "frp_0.57.0_freebsd_amd64.tar.gz",
"3fd70ccfab20e75b8517627ec58e30b33003a24ca4629ed42650ef1b98f17e7d": "frp_0.57.0_linux_amd64.tar.gz",
"3c9e03e28899ba18e42f51006f7d94192fbae009885fd91cfc75b354cffebf58": "frp_0.57.0_linux_arm.tar.gz",
"e2f75360702bcdc390997de7b2557f21a1f28d7ebd4d1ca74cf2e38849185bcb": "frp_0.57.0_linux_arm64.tar.gz",
"795defca4853f7cded6625d792eae33b45987856b961a82c8b6cc44a8d0b3bc7": "frp_0.57.0_linux_mips.tar.gz",
"a41466714ba9463978139a62d241893a034425235b61ecf2efd868857e1c83b5": "frp_0.57.0_linux_mips64.tar.gz",
"d5d2ee272caa314a731dcc59ed4474c9f34953c617e8c29fdd86ea8c017f2e91": "frp_0.57.0_linux_mips64le.tar.gz",
"2fae90ae2544f8b46582cfb7d46984d837b193601b35aa9d63c2f4f52007e32b": "frp_0.57.0_linux_mipsle.tar.gz",
"4e155fcf4f0c7e186ccd2be94a2e036bb62790c9bc00d9145a2999b5e3f38717": "frp_0.57.0_linux_riscv64.tar.gz",
"18b6a345f7d4fb9250b8d751a99f58a0a2daace02a1f7a4e7bb567237e681335": "frp_0.57.0_windows_amd64.zip",
"6fe6b708ab65d61293fb7f1669a3dceab6d8a7d06f9f9b93db68025873f51c44": "frp_0.57.0_windows_arm64.zip",
"ef44189d246b4a95e0eabbf1d6d86ba94002e6f2bb5eefca8e3e8b8292abc085": "frp_0.56.0_darwin_amd64.tar.gz",
"93afeb34c835796508383b70028216eb3d43b2bf63bb3f7493acd1ec533d588e": "frp_0.56.0_darwin_arm64.tar.gz",
"531be6e910202087c61e10e57e28eee9a079fee380b8a42432de55d570bb25cb": "frp_0.56.0_freebsd_amd64.tar.gz",
"084d3c601a9f5d100ad3be26d94b643f2843fa64dcc5f2f2057c612bf7f9d4f1": "frp_0.56.0_linux_amd64.tar.gz",
"cf5cc61f68d705860538b8d3e865ae026a7b27e4da8c1c1a3f50c5e7827cd097": "frp_0.56.0_linux_arm.tar.gz",
"8805d70a692b0c5e20271214af085ffc3d8ea2176ce5dbe06fd6e4de59d8206f": "frp_0.56.0_linux_arm64.tar.gz",
"03fce0574a2df7993efff8bf3d1e45250b08692081cff53dfd266745db772f27": "frp_0.56.0_linux_mips.tar.gz",
"db2975501126fc0f61097acdff7484655e5d37b01de8c509c2c5e0e88591fb42": "frp_0.56.0_linux_mips64.tar.gz",
"4cdca1cc3d298a5e6628ec40e174882e26039d953492eaef6c0d25cef065ace5": "frp_0.56.0_linux_mips64le.tar.gz",
"0679e059dfca6cd022caf808ffe2709207377463a31ccddee1bcb75c161b341c": "frp_0.56.0_linux_mipsle.tar.gz",
"fdbcc2a7d73552e690bc9ca7fccb69b9efdf10fc4d78f0f7c63b14a9129bb116": "frp_0.56.0_linux_riscv64.tar.gz",
"572872fec378f423b141faa205b44faa07bbf06f7272b0a6a3235c7992a69998": "frp_0.56.0_windows_amd64.zip",
"b1c9ee1dff229639c43c60e39a6023798b5c96ccd38df7e3edd41cfb6990c90a": "frp_0.56.0_windows_arm64.zip",
"efd2156b1477d88b8ce1d9428cdeb1689bd12cefb4b31ca81b70eb7d65e22e59": "frp_0.55.1_darwin_amd64.tar.gz",
"220583e20edd98369dbe929d215a387ceea937b0e0637f62558506b2a6c603a2": "frp_0.55.1_darwin_arm64.tar.gz",
"c20f8abf5e0933bfd88fa974ad3a005c72f494aafc021916927774ab0ce6ca46": "frp_0.55.1_freebsd_amd64.tar.gz",
"4d13675c330ca07d532f7a2ebc72fdc011487fe318f2ee645842a3fa4b23c966": "frp_0.55.1_linux_amd64.tar.gz",
"bc283cb6e280e5fd5089216c8362003235dcf371e9f99bbc14462a0ef05c0b53": "frp_0.55.1_linux_arm.tar.gz",
"f14655042086ef4653c0351a6464fb7d73473baf26e15a5f59c298bd3df23d1c": "frp_0.55.1_linux_arm64.tar.gz",
"550e7d04aa4d00fb81b1cd566c58b056a3da8bcfd05631e5f4edd673232b9062": "frp_0.55.1_linux_mips.tar.gz",
"8a0f1ef0b8723089613e2754d965ac9059eed027064bdd484f417fa6f5756d12": "frp_0.55.1_linux_mips64.tar.gz",
"21b32cdaf6e4c74a88a0b6c3c377a3d40a23f73c0313625fa63ba4a6542616fe": "frp_0.55.1_linux_mips64le.tar.gz",
"b09684adfae58733bc12cd0ee3cf1e20d6b888c3e5280cf9f9e7a6467cf87a71": "frp_0.55.1_linux_mipsle.tar.gz",
"52fabafac257ef8ca28e53cc4f210789cfd882946d0f9d2f9457d63f0344a602": "frp_0.55.1_linux_riscv64.tar.gz",
"eeb4247038f58d6b89bd5608782489eeaa7bcfb83d61b5475284ab612978b328": "frp_0.55.1_windows_amd64.zip",
"b48943e9641fde4b91e0032fa031599fdbe3f9cebdd8612cec9e3477aecf2866": "frp_0.55.1_windows_arm64.zip",
"b509e7d50b164aaa62b30efb189caf965615ce266d51c243e494bca14d2f2864": "frp_0.54.0_darwin_amd64.tar.gz",
"dd8057968d3560e9ecb42b2ed50b796ec09573d5263f689c8e0633a8b8a7127a": "frp_0.54.0_darwin_arm64.tar.gz",
"ed25f0c61c45c7f013f2f5ef9194cb2854805db9c692f656e2b30a6ad1681436": "frp_0.54.0_freebsd_amd64.tar.gz",
"13102618f84a2efa07a90733d9bae72e48b897c29f4df4b38bdacebb99517e52": "frp_0.54.0_linux_amd64.tar.gz",
"0ccc051693da612b7c4eed265598d3c8878019cb21e6ec9e3869f94b93e6ca80": "frp_0.54.0_linux_arm.tar.gz",
"a3f01a59bca7cb330bf680019595bbbf5f8167494fab4c46eaaf836fdc3a1902": "frp_0.54.0_linux_arm64.tar.gz",
"1a1a729fe607c59dae787bc5322efcf8cc5a9e87623c6d10e2a08531829bb9fb": "frp_0.54.0_linux_mips.tar.gz",
"24fccce2e9c6684480bfd8ac0e9ea3e36d4203922fa5a39ae9f63bc0542f68f5": "frp_0.54.0_linux_mips64.tar.gz",
"18ee2a78c352eeceb07d55ba572955af64b14282914fe77edf632baf4ce0f967": "frp_0.54.0_linux_mips64le.tar.gz",
"e73e6a2bc3fc1900fb2810bf53bed0471149fb07c60917027661d9d654c0f6e8": "frp_0.54.0_linux_mipsle.tar.gz",
"3961db6d3c5951da49b40cfdae22c8fd53ea87a2ff97245d8aadd4d4206c6fea": "frp_0.54.0_linux_riscv64.tar.gz",
"95f0d8c8f4781fc8e42b7d644024c647032e3f6cd0ffe425e8f7d5a46d601557": "frp_0.54.0_windows_amd64.zip",
"61b4d21b669ceb671b298a4ed4aa3c70b33d6e3e4281f7417336a76f684424ca": "frp_0.54.0_windows_arm64.zip",
"91b1b306c1a538dd6d60857a1da9019241034bcaf0cc19e0c07abfaa8f6a8f75": "frp_0.53.2_darwin_amd64.tar.gz",
"76d2a7bc7ceb5f542ed5be5208f68253261a36d1f4206fc4689296d9033a59a2": "frp_0.53.2_darwin_arm64.tar.gz",
"3f9462f9c7aad6fec22159529b1db7382acd7254605894fbc44c7a7c464e148b": "frp_0.53.2_freebsd_amd64.tar.gz",
"df7356db409cc406294211063bf387a8b590289370811b1d10d6fdd1023c3250": "frp_0.53.2_linux_amd64.tar.gz",
"0f7acf26d92d39a2e3965ee91bf60e7c331844a1d7e81078ede526cf0459eccd": "frp_0.53.2_linux_arm.tar.gz",
"e67faadd41e6236f2bd67d35c9dfd807ff2941027686632f6f4c339dea8ef263": "frp_0.53.2_linux_arm64.tar.gz",
"5b8d4fddcbe0c9e1e82bf8ca30b97bde3fff668741e49a260d6c13c55584bbc9": "frp_0.53.2_linux_mips.tar.gz",
"ec8938be2d1b535eeaf7ba803dae2b6fa1059c6106791d59d98600928dfcc057": "frp_0.53.2_linux_mips64.tar.gz",
"0950dbcd22a110b50c7636f2ff7ca73ee120568d375d75539546c6590cd75ce9": "frp_0.53.2_linux_mips64le.tar.gz",
"f2f9c488451676a58566f6daf2a8a1c85aea193abdc7d7241ef0e12675238bc9": "frp_0.53.2_linux_mipsle.tar.gz",
"351b90825fb48695f36208f0e6cfbbd53f9539306119b5ca0aeb949bd255066a": "frp_0.53.2_linux_riscv64.tar.gz",
"043cd981e81f756123ea4501569ad8d1fbb8166d1046b349ca423aa6ddc0ce31": "frp_0.53.2_windows_amd64.zip",
"26eb992318437fad2d122ef76cfb3086f1339201486a1cdec910fe1a457ac383": "frp_0.53.2_windows_arm64.zip",
"2c02d8f219e83bea4bb4c9ddf1222bdabc068f656992e967dc702e70a1aafd80": "frp_0.53.0_darwin_amd64.tar.gz",
"a148f12a5261ef3186322b08cf1b1907d987505ec5485adb290a350bb2083f63": "frp_0.53.0_darwin_arm64.tar.gz",
"8b0067e658dcbb21313ae8192aa7e1d364af8e96aeb7893ba7422ea0844e8bd5": "frp_0.53.0_freebsd_amd64.tar.gz",
"662d62af7744b9b639b3473bbdd2c4c70dfa5ac5fe1d058d13ce3cc7ea059500": "frp_0.53.0_linux_amd64.tar.gz",
"e33075389b77f94a816ac45bf1d0ce2b540fd98dafac9828602625088967762f": "frp_0.53.0_linux_arm.tar.gz",
"1d5b17f54911bc22816b0d72b32c258b259eb912d9d0484fdc949a315f5a5d42": "frp_0.53.0_linux_arm64.tar.gz",
"f0439788bbeda72664259defbc0edb12825cbf2928c922e06103b7b715bae88a": "frp_0.53.0_linux_mips.tar.gz",
"32665745aaf03d263a9ce87f0ea7a17eb3476328c25c1a1fcccd0925934f7313": "frp_0.53.0_linux_mips64.tar.gz",
"ad977caa79c00c082206f46f521b8f99a44a051425dbb69ec9da1a152aac6279": "frp_0.53.0_linux_mips64le.tar.gz",
"718c0f0820f65782bc19af479f2406c9654fc564b9999a0936581b4ed1d91bb2": "frp_0.53.0_linux_mipsle.tar.gz",
"0e8f1915a1e2b1b2d37b11e831e49fb5f5fc2a14eea086f7ea5a1e4112095728": "frp_0.53.0_linux_riscv64.tar.gz",
"dfd7bc3410c018dc8bcf897696ddfb10e7aaf5a584b8220ae3949ec87205ea4c": "frp_0.53.0_windows_amd64.zip",
"23bafd6bf4ac0e631b37bcdc68827f4b36f06c3dcf0bd754f5d0f9acb4606a3b": "frp_0.53.0_windows_arm64.zip",
"07a0651b2053508bab9370df884096effa653cb24cfd8c454c438b15971ece63": "frp_0.52.3_linux_riscv64.tar.gz",
"0bf96f473385bbeb64faad3caec3ad721187b328f2228820e49838e187da0e22": "frp_0.52.3_linux_mipsle.tar.gz",
"24395170dfc41544eceeb78529c8de5b57b65250c27a02e058cd013e6f66097f": "frp_0.52.3_windows_arm64.zip",
"25431755a121c12dab3c28fec18eaef027a73aa5e9780b33f6801e152e42ab36": "frp_0.52.3_freebsd_amd64.tar.gz",
"3fcf04657f8efd6c6418047bb8c219878c913c4bdc678a8c4bbc8a49d3a389d1": "frp_0.52.3_windows_amd64.zip",
"46b6b8e83ccbbbc2e639c852dae9a41e79f8523d444fe39f9d8f7cc5e7661081": "frp_0.52.3_linux_arm.tar.gz",
"5e041b19ba9ca6a5255679b353099946065edfdf951d807db2587fa8c95b1447": "frp_0.52.3_linux_amd64.tar.gz",
"8e05baa844d928b6239bd9f43cd3e065fc2af971930bc6344e2c899d7eea14db": "frp_0.52.3_darwin_arm64.tar.gz",
"9aab5a4936295d13f2602c8e087fd789a7910b3b3c9a47b9fb799ec99020192b": "frp_0.52.3_linux_mips64.tar.gz",
"a249c503a622599ba68330f323de22a457e058157cb8e38cd3e59581993c03d2": "frp_0.52.3_linux_arm64.tar.gz",
"b64b34521d1942f05b9224bb21d025af5c0ae99fa2e2dff635f26f91d91a6188": "frp_0.52.3_linux_mips.tar.gz",
"c3b011e15c03348592d4a2adcdb90994e7ed29a43f572945505a429c12645215": "frp_0.52.3_linux_mips64le.tar.gz",
"c992b9a8a53c53465f035d5e254ecc1a9455f260fd110fe1600d5da4a37df413": "frp_0.52.3_darwin_amd64.tar.gz",
"05e2ba6184dcebe6fa334c2a1d4534433e8ff9372636ff98eef96e414212903c": "frp_0.52.2_freebsd_amd64.tar.gz",
"11f2af35bdaa799a38a180a1b73083d68843cf731ecea118a33597a14289589e": "frp_0.52.2_windows_amd64.zip",
"5ad396bc221aefa47d1192d6df11193240891ea3a88d0f0b941e1cb2967e2a01": "frp_0.52.2_linux_arm64.tar.gz",
"6add94e2916fd776bc2fd62a01fa6fd282f040e2f05ba42962e823eac821ae81": "frp_0.52.2_linux_amd64.tar.gz",
"8c47d8f1ad960d0f0459bd0fae7bc33c9266943d04549145b969c9107c59703f": "frp_0.52.2_darwin_amd64.tar.gz",
"94169b8d725d30bb0ddf19db73d18b99544dcc52521507419eb7fb42823ea8ac": "frp_0.52.2_linux_arm.tar.gz",
"b09d38e5eba230a6bb04f144f5d32d26ce69f1424bbbb1058d43c712ff558679": "frp_0.52.2_linux_mips.tar.gz",
"bc886aea03ddb2d4201501904a25816ac962cd3fbe6bc7fab3ca05357069666d": "frp_0.52.2_linux_mips64.tar.gz",
"c32b3159a8aa089b08222987a32b9856c046c276898613c75eec62d370df7e01": "frp_0.52.2_linux_mipsle.tar.gz",
"c7b22ed0a87596cd839b555e4992d80691359e75409063b6dca2dda96e7da480": "frp_0.52.2_darwin_arm64.tar.gz",
"ce70a9a044271be4336d7376aa1d5c5f8de8497b1e284b083f6d2184d6f57042": "frp_0.52.2_windows_arm64.zip",
"dc3220af2b22469da26209d4b376858c11160127e83bce09f85cd0c27a44d5d0": "frp_0.52.2_linux_riscv64.tar.gz",
"f1985ce963979371360df27054ba07df4d4ee35338880bed83ef609a4648c420": "frp_0.52.2_linux_mips64le.tar.gz",
"076d9ce5c8644dbeb313e2d90349ad33d3b718b2701899480573266b3f6f0e6a": "frp_0.52.1_linux_mips64.tar.gz",
"136cc6be28c798b2493875f498b5956a876c24cdbd028773aa9194c8bd846442": "frp_0.52.1_linux_amd64.tar.gz",
"13f227bc915c43961e1f3831f155c6934e7d5a65434af3b29bf494b1d5d276b7": "frp_0.52.1_linux_mipsle.tar.gz",
"1b3c61129cf7b45ad41a6b297f4425b9e700cf6302c8969232c7587ae7e727d9": "frp_0.52.1_darwin_arm64.tar.gz",
"69c08bae93e16aaf57debbe2b10df6824f5dfef32ce21b5d57d750b0698999ee": "frp_0.52.1_freebsd_amd64.tar.gz",
"73f3e7037e5f06e8f6fc30aa47aabbc815b4173decdcab149c647126a4aa6370": "frp_0.52.1_linux_riscv64.tar.gz",
"a7626329b690c269d640555033e156a55cffb967f11556eb782ff130d0ad7982": "frp_0.52.1_linux_arm.tar.gz",
"aff5412e89e7164b5083909f2b5a81d8edaa644a3bb6ef696843a6ee0d129fc3": "frp_0.52.1_linux_arm64.tar.gz",
"b2cb915a6e66c99fcceceae07b08d28002c575a3bc2c6aa8ea88c9ae45294be3": "frp_0.52.1_windows_arm64.zip",
"b993db8bf609419a850d3233f97bf422de7e5e54576120c36de0ad703e541bf2": "frp_0.52.1_darwin_amd64.tar.gz",
"d7c2ffe601af16d168d881b88817df81e9bc8646e56643545bd9a11f01ebac6a": "frp_0.52.1_windows_amd64.zip",
"e61df02bd13c250267ded9f0db8ef0e0f3a3eea63efbb8d041190883b0cee0cb": "frp_0.52.1_linux_mips.tar.gz",
"f64a03af886034ad8380631ef1d65728175f5af79674af39c29978a86c181c7a": "frp_0.52.1_linux_mips64le.tar.gz",
"1411f74ca4f05e63963448b9d0c972e16cbf98ba81864e1c04de0492ebd0c6fa": "frp_0.52.0_linux_mipsle.tar.gz",
"14c37cbee05947b2c67fe8064c132652b363c8b0d72fa401ddaf93efdc9538e3": "frp_0.52.0_linux_mips.tar.gz",
"1a8d2c5bfe3a0367068cdf890b025258e5614c3fef308985c001500902692817": "frp_0.52.0_windows_arm64.zip",
"4669cb8c374ff0ec48c0f6d15a939c59390c2109645914dd52d4deca519c084d": "frp_0.52.0_linux_riscv64.tar.gz",
"4794997fffc632dd8d357e9d00ca616e9efb2741e0f0acd1599f90be6281b9e6": "frp_0.52.0_linux_arm.tar.gz",
"5953e84b6a1590568b6d77a0b75093552577aa61484aff41b3ad0fb35c68719f": "frp_0.52.0_windows_amd64.zip",
"80228ba9bd43db42713f682032c0d4c2faa07ecb01be848bb57f6d51f24fa138": "frp_0.52.0_linux_amd64.tar.gz",
"8a5b86e7ea67bd1355ca5b9ddda60ecfdfb7c0b13cf06af71c1e72e88371016d": "frp_0.52.0_linux_arm64.tar.gz",
"91f46654fd8eae9fcc5a7189c6629a7e4b8f49654d996bbb45432cb4a46ac8f7": "frp_0.52.0_linux_mips64le.tar.gz",
"ad61f4285ae98dd4b8bad622888e97bb290e2ca667cd9ad52ad2877cc2ec6807": "frp_0.52.0_darwin_arm64.tar.gz",
"c17291696d623106324b9bad894599325a90148d7d19970b9142a445b789b571": "frp_0.52.0_freebsd_amd64.tar.gz",
"c68f67a262cf61a81945326e0e0c9e2a3dce209c3125bb0f05a16921141f4231": "frp_0.52.0_darwin_amd64.tar.gz",
"d21b617081093f98de5fc1e57700d4a104df67c4965f3fb99dc2650aefbce86f": "frp_0.52.0_linux_mips64.tar.gz",
"0108697c36c88f6ae776f923064236f4e890f3c887a94e798222e5ba3c08c568": "frp_0.51.3_linux_mips64.tar.gz",
"081e0f8ba995218e30ad3c0fa7a12493f17dcbbbac73fdae4391fddf8af2f918": "frp_0.51.3_freebsd_amd64.tar.gz",
"2e1a85c3cfa7cbbcb8747f53de4d7c913cd8ace7475988d823ca0e30bdcfa44e": "frp_0.51.3_linux_arm64.tar.gz",
"30b14705cdfcc4fbc654b55863d110a99deaa92a1490561e8dfd84326f9a9e9c": "frp_0.51.3_linux_riscv64.tar.gz",
"3fabb19b2157709cb6baea755513f38b2d5674539b54f7853454c48c5a9f22bf": "frp_0.51.3_linux_amd64.tar.gz",
"4cafe6451efd64e50a28f2533055b1f68fc59426838214d20341acba515b0eb5": "frp_0.51.3_windows_arm64.zip",
"5fc4a7caff50594c717e7d8e5929d4cb3e1674d81fd345a29abadce0a86d22f3": "frp_0.51.3_linux_mips.tar.gz",
"62170484c4d450fa47d86ed8b1dd20659b22cd7bc5a36caab330f244d6ea4d97": "frp_0.51.3_linux_mipsle.tar.gz",
"6abdb7353ae5562e16d28e1da142f5f97bd51964359901aafd694b4638f85739": "frp_0.51.3_darwin_arm64.tar.gz",
"bf8ab462d70a288b7ff2e9dda8151d16340ec4758843a619a936b7541f52fe54": "frp_0.51.3_freebsd_386.tar.gz",
"c35dcc7b9549eacce4d5b34a07a3d102b0c631ef4b72682ce0472f65b8777d4a": "frp_0.51.3_darwin_amd64.tar.gz",
"cf873001de9c33445213818c5844992e1a3a02486bd3defce556b95e9b0f4af0": "frp_0.51.3_linux_386.tar.gz",
"d1d9b02741e5d8742853665aad6a36a74a977fb82108b894712008db8d170276": "frp_0.51.3_windows_386.zip",
"d6373caf2bb26e7956c976d7d9142a082a0c259525bac3d5bb2fcfcbbfa63bc6": "frp_0.51.3_windows_amd64.zip",
"f300f69fe05b47e3b3e571a1fd83c7c0f7d69667d50a78ccbaa551bda3078169": "frp_0.51.3_linux_arm.tar.gz",
"ffa8edd59c275f6c592835b11b1f00e7c83c7d1e91aa8d9f6d666d286e902017": "frp_0.51.3_linux_mips64le.tar.gz",
"0b938c1c8389829602f511b4d8ebbe8f6d2ae6fb4e5a88540b1699c922a63610": "frp_0.51.2_linux_arm.tar.gz",
"13ac5e018ec166c098c2d67635068ad1b18247aaf02a8537532f52b4fda2dd29": "frp_0.51.2_windows_arm64.zip",
"3ce4df319c7ea35f8cfa13d1e03a0309fc4f57aeaaa02d05fb9fd560443e67ba": "frp_0.51.2_linux_riscv64.tar.gz",
"81930048c93d8db07af024cd0355809248501dec0ce182a734d16e6bd48055a3": "frp_0.51.2_freebsd_amd64.tar.gz",
"895b5c7ece8b458dff80ed790fc1633675a05fc9c4bd994ac89cf8e9d83bd32b": "frp_0.51.2_freebsd_386.tar.gz",
"9774490a0a4f822960a8da99a214cec6e2320622c2c20cd6b713e0e52806031c": "frp_0.51.2_linux_mipsle.tar.gz",
"99196195845422f6ac5962782fa3676f34fff343e0fed0f354cb6600d894afd8": "frp_0.51.2_linux_amd64.tar.gz",
"a41b7612e1057aff1743cdd0c9cf2dddd07f7e4e0340d419f05c42612b118a02": "frp_0.51.2_darwin_arm64.tar.gz",
"b430c31a107a7c5e48899e3ee800f39aa50300d3d76f87bb7afb7ede58875cfe": "frp_0.51.2_linux_mips64.tar.gz",
"b68640e6866a22639186095138657c53b0bb6626ec0438b488d1a2ffdde23155": "frp_0.51.2_linux_386.tar.gz",
"b83a269ce5fb9ff099695165a5d3565646f6032579c4bc6925c63fe8100aee0f": "frp_0.51.2_linux_arm64.tar.gz",
"c35d5b705e2b321cf612bcdeb44ee27392d6a1202248e8ec30bf178adf00f9da": "frp_0.51.2_windows_amd64.zip",
"cc928db0c984d3a7e9822ebb7ac897ddb90f43848488a5c3261b5704085fa92a": "frp_0.51.2_linux_mips.tar.gz",
"d458887ece9050b08d1d58c2718110643b87f254981cda6c86f25dd5559e3867": "frp_0.51.2_darwin_amd64.tar.gz",
"df37d932eb846e608187b0aca6d182467ff24c548a044b9206a93913ec93c752": "frp_0.51.2_windows_386.zip",
"f56461c7a75839fa5ab3f8be2988f9f5d57c8121c4d7c31e17d2d3a7447d2a7d": "frp_0.51.2_linux_mips64le.tar.gz",
"030544b09aff990592772ae508a62396c5648a267a14e5f2fad08324c3d9eb9a": "frp_0.51.1_linux_mips.tar.gz",
"03dae058d9b192aab4e119e620c40253f7693bfae095820ddd0313403d207d82": "frp_0.51.1_freebsd_amd64.tar.gz",
"1837335417e0bfa4c1caf7ce94047e1ba8020983c246b25679dc5efced9dae75": "frp_0.51.1_darwin_arm64.tar.gz",
"18740144d6c91dea850c695590973733ababc0634ca18073d2faec296f572b07": "frp_0.51.1_linux_mipsle.tar.gz",
"2379c3dc7bf783334051c06aec97ffb50007c9d17572aae45500f07c764ab99a": "frp_0.51.1_windows_arm64.zip",
"291fa7918aa575802ced2fb77e45f33a3cf7fc4b5c27c4ac31a68b2506c50a30": "frp_0.51.1_linux_mips64le.tar.gz",
"2d07711a0e24e3da968ad69aeeb458854572788e7869d276fcfb1189c824f9ff": "frp_0.51.1_linux_arm64.tar.gz",
"429b1032624f2fa211d31521f1d7f3703c022e476f6e225325842500eb3a37c6": "frp_0.51.1_linux_riscv64.tar.gz",
"4ce2100f0e9907d9dc152f94f56bf33bc44d029b2f83efde32b586a57bf55809": "frp_0.51.1_freebsd_386.tar.gz",
"70f57deb3ce57eb890104fe14d6fe442a815e095122a9c2b584e34d3c54f5563": "frp_0.51.1_windows_amd64.zip",
"74df509decd6953a77543ae8febcdc05379bb2bd0614ad2fe53a4a6cfac86caf": "frp_0.51.1_linux_arm.tar.gz",
"9f27cec3b7e600c0223c0de06b65feafa9ed6bf82a8b1dfe338aef6b03bac097": "frp_0.51.1_linux_mips64.tar.gz",
"adbfe65938517a8024565569825526643eac2d3294f4524d12a2846611107e08": "frp_0.51.1_linux_amd64.tar.gz",
"b7a7814aedd230b66e11f3626aa505a2a701d6afc19bc8be2143955bfa3c1d6e": "frp_0.51.1_linux_386.tar.gz",
"e0b8976e986ef0ed0901560810a81cc80cf8c332e087edd35f50e9a5a88c79ae": "frp_0.51.1_darwin_amd64.tar.gz",
"fe1eaa0c7066ad45a8a13838d15a6a6535e69250ecc3ed8c48bfb480c8b87e5a": "frp_0.51.1_windows_386.zip",
"025bf967e37ce095f31bc45d886156d365a0e9dc7aa0e7f3bbc91bd1c9717145": "frp_0.51.0_windows_amd64.zip",
"26acab3487be8980460ef86f0fdc7a446cfdadab02a5a0b27dc760ecce15ffc2": "frp_0.51.0_linux_riscv64.tar.gz",
"26c48aa4fa4458ad29d0de364904e24be40424d4f6c37005c2c2d9c6e41e2b06": "frp_0.51.0_windows_386.zip",
"3f75d981d58670ce7e0e3f5ead2bd3359cdd1f33b96da726c62013567a884639": "frp_0.51.0_linux_mipsle.tar.gz",
"5feca5a4d601ed393a3cc04d8bf3c41194ef56af155c326cf1e7fdfd130ef17a": "frp_0.51.0_freebsd_386.tar.gz",
"6c9628cb8382894dc0a928df8fcea9dad9cb763ff161e31f94f816443c7419e0": "frp_0.51.0_linux_arm.tar.gz",
"7174a1328325da89ed6aabcf522131db9928222154e9607b0d5a2f7b2977ae93": "frp_0.51.0_linux_amd64.tar.gz",
"7402fc76816fd653bbe050a3f8a2dfd7c1363c980e2cc3dc369c60c3f0d502a7": "frp_0.51.0_windows_arm64.zip",
"7ebff99259931e26c3baf8dd78c1af671d73a6c91a1d6ec9107c0c225df76bf0": "frp_0.51.0_freebsd_amd64.tar.gz",
"886ac7c8c0e01bddcb808947f76a5f904572e337fa4023cce4bad71a7ae9ca1c": "frp_0.51.0_linux_mips64le.tar.gz",
"9b3e4c64089c3b78ea1f666f11551e4ae6a435fc0797e39ab4fb07fd633b400c": "frp_0.51.0_linux_mips.tar.gz",
"b4a40bfaca19d5b8570be95ea2839fa82c7814c561510c3e3807ce273ee7c7cf": "frp_0.51.0_linux_mips64.tar.gz",
"b7f2414b1d8be99157e5b25ea578938520c45d094534fffb2e515796559b9b29": "frp_0.51.0_darwin_amd64.tar.gz",
"b8a22f70d3451a7f4b8e1718da28ef02dfb38d37193bcbdc1df39eb52d0da40b": "frp_0.51.0_darwin_arm64.tar.gz",
"ca7baeb243b5c264847067f6e5619311223f1741f73d5371ff7fa90698ff5a3b": "frp_0.51.0_linux_arm64.tar.gz",
"e377afeb481b30d9979fcbf636df6b5c4f9449b44f6c3d21a768aa5cb8767cb6": "frp_0.51.0_linux_386.tar.gz",
"1084631215170fc83b2de13f156a3b0e2ea02f2a0955fc94d3c6c5015391922c": "frp_0.50.0_linux_riscv64.tar.gz",
"1cda556f00b20f5b575ba40f83d8a007a8fa3308ef502c62fb7510989c3b7b10": "frp_0.50.0_freebsd_amd64.tar.gz",
"33893a93b57e6509132b4d6ae29f3e8a1f4c105c21746f0f0f036df0cf8d1979": "frp_0.50.0_linux_mipsle.tar.gz",
"4e2b06bd978472dd092c166b43ec56ab22c1347710fd77616283d2c27ee9ae56": "frp_0.50.0_freebsd_386.tar.gz",
"4f2088aff3460c9bd278121de7781985734969399d408f0c9e3f794165e0a407": "frp_0.50.0_linux_386.tar.gz",
"7bb651eec86e0126af3bd515235901a64b5490115defa10972e703c05bc65345": "frp_0.50.0_windows_amd64.zip",
"7fac327360b72613dec67583e4b939b65af0b88b676660821647b161ec2173fd": "frp_0.50.0_linux_mips64le.tar.gz",
"94e608af6d6f96619de403bf3aed4db8ab602999e0335380279e0d8aca1c6040": "frp_0.50.0_windows_386.zip",
"a5496a0364e4e071aa6a1cbcfd519e35ac8dcb4eac9a24e6a22340c4d4cf1914": "frp_0.50.0_linux_arm.tar.gz",
"b2768608b33e964fc7067657f385ba15a69762b0a875db47981953d70dd36af7": "frp_0.50.0_linux_mips.tar.gz",
"c57526a8a0010b811b9bd367704125033fc71774f6a66dcfd4224ec5478e0490": "frp_0.50.0_darwin_arm64.tar.gz",
"d33d83e8b98ce5413603f71b1c0b38c1b5bbe1d1c826b7ada84a7543a6cc6ea6": "frp_0.50.0_linux_arm64.tar.gz",
"db80349f17c39f502a631afda7cf5b95b2a85cdcafa92359b9f4d0375772c440": "frp_0.50.0_linux_mips64.tar.gz",
"e2047b43e87456568a505b84c45f52e0d2ed146896ec1e3fceb72e818200f11f": "frp_0.50.0_linux_amd64.tar.gz",
"ee8cdc63c2993ce8ab2bf918a56169a815254cd5f5a9a57567a904ec5dbf0145": "frp_0.50.0_windows_arm64.zip",
"f4cb27fb222cdd87a30674270614adfd0aa8350034a8bdbc50fc1967c0f0cb66": "frp_0.50.0_darwin_amd64.tar.gz",
"09329200234dd56722e095ee5b0b3d31bf8d39f3bdacb4a473b9144a7e8e8b7d": "frp_0.49.0_windows_arm64.zip",
"183ee0c672409cdd8b421f31e2b81753a4713bee962e1edf97f1455cda97173d": "frp_0.49.0_linux_amd64.tar.gz",
"1ca8187c73c3c75ace29675193659f9d6ddff3e5ddf2131f49f156844ca7d778": "frp_0.49.0_darwin_amd64.tar.gz",
"429aab2804d7431f684c6d409342af57381dbcafc4b37c49606063be2f92d4a3": "frp_0.49.0_linux_arm64.tar.gz",
"5b4204056ae94aa8281218656a1b3566eaaea2ddf4874eccb4a9c23cf9bc0fd0": "frp_0.49.0_linux_arm.tar.gz",
"76c7a4f5e35f32b726c48fdd32e292f63c7b374ba019a28dc44b04140f03e6de": "frp_0.49.0_darwin_arm64.tar.gz",
"7b6c9cf91ad9d00385d47139ffc69c0c9d72270886dbdb4f71f599efaec2cb64": "frp_0.49.0_freebsd_386.tar.gz",
"9033c6def481bde4bf7f2361966ae0ea92dfda5763a167460dcf0e231a2d02b8": "frp_0.49.0_linux_386.tar.gz",
"94ac6a42a165d913b79a0dcfb2d55a686e81b776697580e113aecd8815607076": "frp_0.49.0_freebsd_amd64.tar.gz",
"a343c8f23ba35c943e1c9311df17eb12f84c682d2ba0e965e244a49759b65f28": "frp_0.49.0_linux_mips64le.tar.gz",
"b117ea60954ad0c8d4e92eb60ca8e748806978506c377d59b4f5bc5295c4e3d1": "frp_0.49.0_linux_mips64.tar.gz",
"c44853992b0d6d3f9f5c777038590ee6a5869dbeb6362dfa5537e9d730aa26f6": "frp_0.49.0_windows_386.zip",
"d3a481b40889bf4c6fd35b18941de04ddaa2316ad51977a5af7bdddf3650f808": "frp_0.49.0_linux_mipsle.tar.gz",
"daf162e5cc90599aab036b7bb4ed6d4c521b2f5732a6cb40b08a00e6714deaa3": "frp_0.49.0_linux_riscv64.tar.gz",
"f39f10c0867a52eb9e4d2adf0bfa821993c950feca35437e84d274fba00bc595": "frp_0.49.0_linux_mips.tar.gz",
"fc5c5c5ff93300cea3141ff55fbccccb07cd0017d4e9cd4bcd324563f88f53fd": "frp_0.49.0_windows_amd64.zip",
"042fa197c0f91b27404c086eabfb62dad3ffaaad7101046f518abf58ae42ee1b": "frp_0.48.0_linux_mips64.tar.gz",
"0cd33dcfe9a38441eda2c60675f05ab3c3875b1e54608583d50d0835c567a30e": "frp_0.48.0_linux_arm.tar.gz",
"1e5b997597bacce1d971b83416c2f8c9cde0cbd294e6b11d91a3939f9c6356a9": "frp_0.48.0_darwin_arm64.tar.gz",
"2ab7b66c09391d9d76bd7a4818e85fb3818a10a46c91a804b982d7d4c9fddce3": "frp_0.48.0_linux_arm64.tar.gz",
"41c75d72848375144e46b9b9fe56168f365ce4bee56280757dada6c92bb8abc0": "frp_0.48.0_linux_riscv64.tar.gz",
"5de51fda0577a049945e42f386df70a8e9eb2769af96bb6b7471cb5072605be0": "frp_0.48.0_windows_386.zip",
"7a9fd341e0deb467ba0ab4913852adc965a0df2ba38e18ec80ab7ef61a9e99e8": "frp_0.48.0_linux_mips64le.tar.gz",
"7dba4f6e942502f0eca2ec37206671734eeb87c40a29f16b96ce14045da9e833": "frp_0.48.0_windows_arm64.zip",
"8ad8905b9296f3c26632f3bfc66302bc082b62295f6bbbb5b78e31d1e6649f26": "frp_0.48.0_windows_amd64.zip",
"a792cd515589050d475a28b714276a2960ed7ef8e0e5baeea3d38301a775fbb4": "frp_0.48.0_linux_mips.tar.gz",
"acd9f040fc6fb2a595f20bfb4faa66d9244615a0feaf9d2e4b03a994ca126a32": "frp_0.48.0_linux_386.tar.gz",
"d48623a74a00577be0409d912f8197a110f13192eab99d3959ceb11496ed0903": "frp_0.48.0_linux_mipsle.tar.gz",
"db53bdef3b270e45fb9efc489af2948be7c7fa1e3a5cae9698f2832e628bcd3b": "frp_0.48.0_linux_amd64.tar.gz",
"dd781cfd710345cca2df4d306245298efb61dc447d8004dd5542c1b2083e39a7": "frp_0.48.0_freebsd_amd64.tar.gz",
"e2dd4933cc48caba288be96ba5b226c7edb5be940c0452d9bc7faa28ab66847f": "frp_0.48.0_darwin_amd64.tar.gz",
"fdc0bca8460360346991a0f13e25233c87805bdc0f055f221f9c57c33b3b60fa": "frp_0.48.0_freebsd_386.tar.gz",
"00ffd863c32645660a29db758db4ea89f7c3eb616b3488cceca55345d8a5d11d": "frp_0.47.0_linux_arm.tar.gz",
"04d9eaf4997d1407feca0324beedaca577c63fa900ef04e6a97de9e8e2391e34": "frp_0.47.0_freebsd_386.tar.gz",
"125f87d334addd8ec7dacaf2a321a9f1c9a8b31c8a673d2d02808162cd67f997": "frp_0.47.0_linux_riscv64.tar.gz",
"3b9f8b80f13f20194490851b076186124b67b9a7845b32e5e035ae4aed2e45dc": "frp_0.47.0_linux_mips64.tar.gz",
"41a3a760ab0e04271f8bee1fd80011ce8e93a8455f78919864bcb13200f758f5": "frp_0.47.0_windows_amd64.zip",
"5b7c15f9e14042a99c38515ddfa694f188f59d72bde10ce341d86cbf7f801b19": "frp_0.47.0_windows_386.zip",
"71a0f3137f02da4116ea2b7d134c38be86a1229cffb0b1dac4469b561ea35985": "frp_0.47.0_linux_386.tar.gz",
"9299c297f6c75c6aa2bbbb5de27172e367328b6f5bbb6f8d1c4ca73c4c4af415": "frp_0.47.0_darwin_amd64.tar.gz",
"95583f7a979910ff4e65a5d9802df699063472a67a1f9e6d6fd6c2fcff448a14": "frp_0.47.0_linux_mips64le.tar.gz",
"95c0695cdf0cd8d399cabdccdff93b25aa7deb97e950bd3702bbbaf9a2baf87a": "frp_0.47.0_linux_mips.tar.gz",
"c53b188ec3eb09f34484d2576f957e61522875c0e7a99e67722d41b2b57cdb4d": "frp_0.47.0_linux_mipsle.tar.gz",
"cfc766cc82568e40d7198493340283cc0f4f42de97463aef863170f7e773ff9c": "frp_0.47.0_darwin_arm64.tar.gz",
"d7a7a6085fa6a9f8de0ae2c221c1ef110b9afc2a0122a058482ef3974d031ac0": "frp_0.47.0_linux_amd64.tar.gz",
"ee2d0d800b14ac26b8aeae4365df031e0186d23be150308735a0be753ec2d3f9": "frp_0.47.0_freebsd_amd64.tar.gz",
"f1dc0436b7f9f3f5c5d404cf5fb4a7319ff1cc22a06a687672020af620693f70": "frp_0.47.0_linux_arm64.tar.gz",
"0476f68f4552ae460d72f0b6c2c9fd4b6fb8dfdbafdec62695f02996d7221f81": "frp_0.46.1_darwin_arm64.tar.gz",
"200244a2c1bc9e186f875c23d0b78c9ab59a88052f4f4132e5c28a70fdc356b6": "frp_0.46.1_windows_amd64.zip",
"4af6b42eb79a5290d1e24e534a0ec34521dc2d30ef60898abd092ddb2e1cd55c": "frp_0.46.1_linux_riscv64.tar.gz",
"54e364bf382cc987a962fa5db328ce8bc375bff74ff7b8afcaeb1905a295e027": "frp_0.46.1_windows_386.zip",
"5f1660b704a8b580082b81e14a41d2da9ff1edeebc59b885acb92f1ab1f46838": "frp_0.46.1_linux_mips64le.tar.gz",
"76e5d42d4d2971de51de652417cfe38461ef9e18672e1070a1138910c8448a2f": "frp_0.46.1_linux_arm64.tar.gz",
"7c6208a3f7131802f24ad7bf7f02c760bba5c17443bdf328598d0758865f80df": "frp_0.46.1_linux_amd64.tar.gz",
"7d299b5695b0076b24e93928bad255f76c8352b5002fd459ef63c0199251abe9": "frp_0.46.1_linux_mips64.tar.gz",
"9704b24b5a58144293f7c7715b095b1ebf43b90e501050dfb9477094e6dca41b": "frp_0.46.1_linux_386.tar.gz",
"a47d75d634790109eaa5768d4e5cb504988e3754dcfe458072ef0b46d9aea419": "frp_0.46.1_freebsd_amd64.tar.gz",
"b330c29f6ef91302c6a2b9a0f6e86c77b498d0babb60fe182440f1b97e0554cb": "frp_0.46.1_linux_mipsle.tar.gz",
"bbb1ab095f30e9ecf1b745579f6ecff80eff11fb712f2bc364a656fbec89f73b": "frp_0.46.1_linux_mips.tar.gz",
"fc465df713f8c9d63c9380aa9da72b6ef639fb44917aed390d9c4d08c475a20d": "frp_0.46.1_linux_arm.tar.gz",
"fdde1a3e82d043cdca44b13c45e7593b61707385b30e919c38615d02d53e4b36": "frp_0.46.1_freebsd_386.tar.gz",
"ff71979ea17d481194beba325a55f5d2a319175ebc6a80df535a202a43614f24": "frp_0.46.1_darwin_amd64.tar.gz",
"0ac137ea9061aea6b6e8e5fc228b1082e14d3e29cafe6103f542ac4ffd728843": "frp_0.46.0_linux_mips64.tar.gz",
"1f1eefdf6a9ade3923edcd716c56941f2755848a4bd97167aaa1ceebfed95194": "frp_0.46.0_freebsd_386.tar.gz",
"275b254a20dfda754d6aba28d335a392df74150d6945d2da20a7c5718dc2c001": "frp_0.46.0_darwin_arm64.tar.gz",
"30199cd67bbed08c65f86c2420f0967491cad2ec791c97936666bc930d65e73e": "frp_0.46.0_linux_mipsle.tar.gz",
"3cd7ec9209b973520d47d784a09a368bfb9e2bb195f3c543ae5311720249e315": "frp_0.46.0_linux_arm.tar.gz",
"41f1014ee2ee7ed0a6e989deb937af9a8c01f4974fc1ef541583065475511d65": "frp_0.46.0_linux_riscv64.tar.gz",
"53242fd2bad1e6b3039fdef38df6219710864d1c9e639208a2106326921d15fd": "frp_0.46.0_darwin_amd64.tar.gz",
"62044b03a7bccb7e8f8f4f691f34838cd1160a643c0bb06ca8489e78d2d65897": "frp_0.46.0_linux_arm64.tar.gz",
"6681551b9bb7311625be8f3a269c183b600e13966787a8b11a8f9e8595a3d66b": "frp_0.46.0_windows_386.zip",
"754d66a918d3550c83e670a458f66954eec0521d6e76a20dd0a865992ad1b55e": "frp_0.46.0_linux_amd64.tar.gz",
"a77d3fa9419c5dc12ebd94eb5b97be3cff2c12b00dbe3884adc9ffcedf73909e": "frp_0.46.0_freebsd_amd64.tar.gz",
"b9c79acc881c58b0185465a5ded032d6210637f860712f04ecb800b66453d125": "frp_0.46.0_windows_amd64.zip",
"c14d5be9b9d80a48354c04dd1c3f80167abae94a1854d2f5116e4e5a0da89b91": "frp_0.46.0_linux_mips.tar.gz",
"c87ffc18bfa386cf946156f91fb8649a0cdbcd762550a0b8ab1f4774cb608455": "frp_0.46.0_linux_386.tar.gz",
"cac2bc6fccb071789d7acc95f02470cfb935cfc9c7c6a1e6d91457e4ff11e8e1": "frp_0.46.0_linux_mips64le.tar.gz",
"1a527c78ae25fa3e393d70fbfcea5b928ca96a689d8e82477f1b0db0cfc51e76": "frp_0.45.0_windows_386.zip",
"40d5025cb0b0a6f26cc79fd23fc78ccdfa050bd7e80d694f2039ab98093f831d": "frp_0.45.0_windows_amd64.zip",
"4c90633d523f467384a424bbfce211f737becbc7c4ac637e10e6c91fda8a6a26": "frp_0.45.0_linux_mips64.tar.gz",
"4faed559dc80bc2bf43b6c3da60e19f86c42ab8ed2b19e3ff0d3f4e4cca6c50c": "frp_0.45.0_linux_386.tar.gz",
"5eb942ba9ed0d45d2ac1ea6ed02fbff802a69c408c8eb68155dd2fb7c6fabb0e": "frp_0.45.0_linux_mips.tar.gz",
"63035108f37cc80d6043c1fcac50f8e856791a4fb8bcef0e792d97c88d8e35c5": "frp_0.45.0_darwin_arm64.tar.gz",
"8b2aee9d9eabc6078ae8a4c718030be85a13464becdb99f97f635e75425eb63e": "frp_0.45.0_freebsd_386.tar.gz",
"8ecf30ac7c14f85da20c1761c6418979282bff12db4d82ade2f4a1a8037bdf6e": "frp_0.45.0_linux_riscv64.tar.gz",
"97b4d3555734cba2af59b72b960ce10891b584dcf8d9e3db9f4f099c0a64131d": "frp_0.45.0_linux_mips64le.tar.gz",
"987f353f6ea282e259738eeb90c20b70fe20e1a49aca498b02acc47200c082bd": "frp_0.45.0_linux_arm.tar.gz",
"a660a94c158cb280974447efd174d3525d806ac7235f6546abeb1a57660a1125": "frp_0.45.0_darwin_amd64.tar.gz",
"b9a1a2387b9b07ec6be9d28e5ed9639c1ea29d41a84bc3a62b39ab476459b1ff": "frp_0.45.0_linux_amd64.tar.gz",
"e2a6179880b852366edc395685fa0e82eec542e9c8a2c3483d30d5740941a0e0": "frp_0.45.0_linux_mipsle.tar.gz",
"e57919a0e3a63705ef452bb2a6bc440f7a6273a8205ed9ce2ccfd063ea9b2215": "frp_0.45.0_linux_arm64.tar.gz",
"f9c6ad68a9e3903d1689cd85e84f00aa892a9e98b368a9f062599da9d2cb4967": "frp_0.45.0_freebsd_amd64.tar.gz",
"0fd011fb817fa36fe8735e3d97df523970d9be4f56f0848840f737b63ba37fbf": "frp_0.44.0_freebsd_amd64.tar.gz",
"23705712274935b9b223412bf731ecd672dcc8b5d0c11a39372aacedaa6a66a4": "frp_0.44.0_windows_amd64.zip",
"3262dee2fa68eb8d9428d209b2e87c2293d007529898850874b19707088c416e": "frp_0.44.0_darwin_arm64.tar.gz",
"3c4e769a29f03bcc9e998adcd1281142abfb5ff1dd66da5a435830a1cff34217": "frp_0.44.0_darwin_amd64.tar.gz",
"3cc79f9fc44300aed80988b31845328b428c0999572eb7f1df949eccee0f518e": "frp_0.44.0_linux_mipsle.tar.gz",
"45f65dafd172f3a5e05eabf3d4efbb954c92a88851a027f79c19f61a10b78287": "frp_0.44.0_linux_amd64.tar.gz",
"4aed98c21ef4534951b6faeab4982376695ae1e10ca90aedd27a9bfcf6caea2e": "frp_0.44.0_windows_386.zip",
"5f3f60a71fa040a36be5de818e6f95c48e8a2ba368b700a079b593f0e281dbd8": "frp_0.44.0_linux_mips.tar.gz",
"5f7c9ad77e37a5921450c013b9792dac4ea5ef5d3114ea9276585f62e2318a79": "frp_0.44.0_freebsd_386.tar.gz",
"60ee29ebb3683135c815b4e9b6681c92a445ac3f40e9302a70b65fca68ff5116": "frp_0.44.0_linux_mips64le.tar.gz",
"7c55322bb55e4085ab950711f0c3406a25f95573f618ed347e8f542ecf93cb78": "frp_0.44.0_linux_mips64.tar.gz",
"ad151125bd46fb8abf11f2a4347c7c85e102bb0e6128c69962c8d6bf9a71fca6": "frp_0.44.0_linux_arm.tar.gz",
"ce18273ca20bd38c567b0355ca2c85575651b39249294969daa51e568077a872": "frp_0.44.0_linux_386.tar.gz",
"f5acd6dd3812f30ed6a2a2a864231563a962d4ff09c64d21be106db6f8806af8": "frp_0.44.0_linux_arm64.tar.gz",
"00c526bdfae8fe448b1810c1c06b2827efa1158b7e324aa69c23a57a8b29f603": "frp_0.43.0_linux_arm64.tar.gz",
"0d05e3ebd2490c026e1b8f6780d901eedde65562af02acf3bf80d729a2aae52b": "frp_0.43.0_windows_amd64.zip",
"1fe64b366408022e4d61c1e37f64e268f7e72f4d351425df36c35fb1cfc534fd": "frp_0.43.0_linux_mips64.tar.gz",
"3c582f611716c77db5e4f69823fc72572006608f63d9859dea598f0dfc74ed0b": "frp_0.43.0_darwin_amd64.tar.gz",
"4eecced7aa167279bda23afe2be0f3dd9b61080531fdbae5137bd257c334992a": "frp_0.43.0_linux_386.tar.gz",
"618b1a0d2bfebc9bc3e59b4c39e67082a445e5aeaaaa0fec9eded436dd64a2d4": "frp_0.43.0_darwin_arm64.tar.gz",
"6a3e20b001ab57b066a52394ba2d992ae6d93b22260b0969307966fad6214692": "frp_0.43.0_linux_mips.tar.gz",
"6bef9db4560b6c7da2def271f7bc5bf6988fafa3e654f8a2bfb589fd7d79b2db": "frp_0.43.0_linux_mips64le.tar.gz",
"7c1416256f7f3637e0dfed99988d08282ae0866784f1eecd53a3639e1a942867": "frp_0.43.0_freebsd_amd64.tar.gz",
"801a1ea2bf02b9ff657c34708918397bec61408bed216f6ed45889973ee09a01": "frp_0.43.0_linux_arm.tar.gz",
"98ab35f179091726b739c9fbb6643cc7328076bfbddd09732bb68b1cdf1b7435": "frp_0.43.0_freebsd_386.tar.gz",
"bb8734f2be2907a2923aedf43757d6ff85a7c66af789b8dbef34ddaf2194f05f": "frp_0.43.0_windows_386.zip",
"c14ccd69607c34707120e7c2d2df9b6c0a11c7f40e22f116d75838e2038edba3": "frp_0.43.0_linux_mipsle.tar.gz",
"d458d70dd88048d1fc898d5422ed570e912d3f3ef3ee5928871438a08514f725": "frp_0.43.0_linux_amd64.tar.gz",
"19ca9f2b318ea2efbe9f2b213c2edd68de54c7ed35dc3f291146c67374d8c57d": "frp_0.42.0_windows_amd64.zip",
"35386af9e43ed1948faa7037050573eda3299d4a11061734fce5f4be51c56dd3": "frp_0.42.0_windows_386.zip",
"4ef082c1788e972f016f00286a2054c82189cec3a1a3e2af8123240c2888b6ff": "frp_0.42.0_linux_386.tar.gz",
"5c4828f6e89b6f2479b671d3e7644b34b6968a6017cac402144c844b48dcc621": "frp_0.42.0_linux_amd64.tar.gz",
"7946d13b2498410bf9fb0cc32fee7ea44bde8be438eb1b1bc67c440a3671589d": "frp_0.42.0_freebsd_amd64.tar.gz",
"7ff954d3f9f0d655be5f250ca50e8b065ddb8b4d3a1da0a55f740cc03301c6f5": "frp_0.42.0_darwin_amd64.tar.gz",
"b53e3cba1a8a3ebaa1e7d04f647eee3aed3417740692e346dc460c813403475c": "frp_0.42.0_linux_mipsle.tar.gz",
"bf980fa58499e947581c6b89b100d55c1d417fdda6f7544422a4a6400248e20d": "frp_0.42.0_freebsd_386.tar.gz",
"c6f00c7458e7546b9339ce65805b2969abf55f95698f0b2f0904ed85f187b3fa": "frp_0.42.0_linux_mips64le.tar.gz",
"c842849be22802e6500167fc34fac869c584ad1f70b6c56dcc66d7391171d567": "frp_0.42.0_linux_arm.tar.gz",
"de3397d1084686a5ab9f82fae2aa65f417cef7d7c2cc12f7eb9da51c0a404de6": "frp_0.42.0_linux_mips.tar.gz",
"de6262f886175411573c98fe2d5838449b4fc2472a07748964159a468ed0ccdf": "frp_0.42.0_darwin_arm64.tar.gz",
"eb8ea449f14a20480c77d6501f8b682516fa4a9394dd15d2a49b6a957aa862a9": "frp_0.42.0_linux_mips64.tar.gz",
"f8b9c30d3cef82aebdf5dfce8ba7d6a4943a4b51ef64223b59c5241e3023d8e5": "frp_0.42.0_linux_arm64.tar.gz"
}

View File

@ -1,31 +1,43 @@
import {app, BrowserWindow, ipcMain, Menu, MenuItem, MenuItemConstructorOptions, shell, Tray} from "electron";
import {release} from "node:os";
import node_path, {join} from "node:path";
import {initGitHubApi} from "../api/github";
import {initConfigApi} from "../api/config";
import {initProxyApi} from "../api/proxy";
import {initFrpcApi, startFrpWorkerProcess, stopFrpcProcess} from "../api/frpc";
import {initLoggerApi} from "../api/logger";
import {initFileApi} from "../api/file";
import {getConfig} from "../storage/config";
import log from "electron-log";
import {initCommonApi} from "../api/common";
import {initLocalApi} from "../api/local";
// The built directory structure
//
// ├─┬ dist-electron
// │ ├─┬ main
// │ │ └── index.js > Electron-Main
// │ └─┬ preload
// │ └── index.js > Preload-Scripts
// ├─┬ dist
// │ └── index.html > Electron-Renderer
//
import {
app,
BrowserWindow,
ipcMain,
Menu,
MenuItem,
MenuItemConstructorOptions,
shell,
Tray
} from "electron";
import { release } from "node:os";
import node_path, { join } from "node:path";
import { initGitHubApi } from "../api/github";
import { initConfigApi } from "../api/config";
import { initProxyApi } from "../api/proxy";
import {
initFrpcApi,
startFrpWorkerProcess,
stopFrpcProcess
} from "../api/frpc";
import { initLoggerApi } from "../api/logger";
import { initFileApi } from "../api/file";
import { getConfig } from "../storage/config";
import { initCommonApi } from "../api/common";
import { initLocalApi } from "../api/local";
import { initLog, logError, logInfo, LogModule } from "../utils/log";
import { maskSensitiveData } from "../utils/desensitize";
process.env.DIST_ELECTRON = join(__dirname, "..");
process.env.DIST = join(process.env.DIST_ELECTRON, "../dist");
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST;
? join(process.env.DIST_ELECTRON, "../public")
: process.env.DIST;
let win: BrowserWindow | null = null;
let tray = null;
const preload = join(__dirname, "../preload/index.js");
const url = process.env.VITE_DEV_SERVER_URL;
const indexHtml = join(process.env.DIST, "index.html");
let isQuiting;
// Disable GPU Acceleration for Windows 7
if (release().startsWith("6.1")) app.disableHardwareAcceleration();
@ -34,193 +46,259 @@ if (release().startsWith("6.1")) app.disableHardwareAcceleration();
if (process.platform === "win32") app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
app.quit();
process.exit(0);
}
// Remove electron security warnings
// This warning only shows in development mode
// Read more on https://www.electronjs.org/docs/latest/tutorial/security
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
async function createWindow(config: FrpConfig) {
let show = true;
if (config) {
show = !config.systemSilentStartup;
}
win = new BrowserWindow({
title: "Frpc Desktop",
icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"),
width: 800,
height: 600,
minWidth: 800,
minHeight: 600,
maxWidth: 1280,
maxHeight: 960,
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false
},
show: show
});
if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
win.loadURL(url);
// Open devTool if the app is not packaged
win.webContents.openDevTools();
} else {
win.loadFile(indexHtml);
}
let win: BrowserWindow | null = null;
let tray = null;
// Here, you can also use other preload
const preload = join(__dirname, "../preload/index.js");
const url = process.env.VITE_DEV_SERVER_URL;
const indexHtml = join(process.env.DIST, "index.html");
let isQuiting;
log.transports.file.level = 'debug';
log.transports.console.level = "debug";
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
async function createWindow() {
win = new BrowserWindow({
title: "Frpc Desktop",
icon: join(process.env.VITE_PUBLIC, "logo/only/16x16.png"),
width: 800,
height: 600,
minWidth: 800,
minHeight: 600,
maxWidth: 800,
maxHeight: 600,
webPreferences: {
preload,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
nodeIntegration: true,
contextIsolation: false
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith("https:")) shell.openExternal(url);
return { action: "deny" };
});
// 隐藏菜单栏
const { Menu } = require("electron");
Menu.setApplicationMenu(null);
// hide menu for Mac
// if (process.platform !== "darwin") {
// app.dock.hide();
// }
win.on("minimize", function (event) {
event.preventDefault();
win.hide();
});
win.on("close", function (event) {
if (!isQuiting) {
event.preventDefault();
win.hide();
if (process.platform === "darwin") {
app.dock.hide();
}
}
return false;
});
}
export const createTray = (config: FrpConfig) => {
let menu: Array<MenuItemConstructorOptions | MenuItem> = [
{
label: "显示主窗口",
click: function () {
win.show();
if (process.platform === "darwin") {
app.dock.show();
}
});
if (process.env.VITE_DEV_SERVER_URL) {
// electron-vite-vue#298
win.loadURL(url);
// Open devTool if the app is not packaged
win.webContents.openDevTools();
} else {
win.loadFile(indexHtml);
}
},
{
label: "退出",
click: () => {
isQuiting = true;
stopFrpcProcess(() => {
app.quit();
});
}
}
];
tray = new Tray(
node_path.join(process.env.VITE_PUBLIC, "logo/only/16x16.png")
);
tray.setToolTip("Frpc Desktop");
const contextMenu = Menu.buildFromTemplate(menu);
tray.setContextMenu(contextMenu);
// 托盘双击打开
tray.on("double-click", () => {
win.show();
});
logInfo(LogModule.APP, `Tray created successfully.`);
};
app.whenReady().then(() => {
initLog();
logInfo(
LogModule.APP,
`Application started. Current system architecture: ${
process.arch
}, platform: ${process.platform}, version: ${app.getVersion()}.`
);
getConfig((err, config) => {
if (err) {
logError(LogModule.APP, `Failed to get config: ${err.message}`);
return;
}
// Test actively push message to the Electron-Renderer
win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
createWindow(config)
.then(r => {
logInfo(LogModule.APP, `Window created successfully.`);
createTray(config);
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({url}) => {
if (url.startsWith("https:")) shell.openExternal(url);
return {action: "deny"};
});
if (config) {
logInfo(
LogModule.APP,
`Config retrieved: ${JSON.stringify(
maskSensitiveData(config, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
// 隐藏菜单栏
const {Menu} = require("electron");
Menu.setApplicationMenu(null);
// hide menu for Mac
// if (process.platform !== "darwin") {
// app.dock.hide();
// }
win.on('minimize', function (event) {
event.preventDefault();
win.hide();
});
win.on('close', function (event) {
if (!isQuiting) {
event.preventDefault();
win.hide();
if (process.platform === "darwin") {
app.dock.hide();
}
if (config.systemStartupConnect) {
startFrpWorkerProcess(config);
}
}
return false;
});
// Initialize APIs
try {
initGitHubApi(win);
logInfo(LogModule.APP, `GitHub API initialized.`);
}
initConfigApi(win);
logInfo(LogModule.APP, `Config API initialized.`);
export const createTray = () => {
log.info(`当前环境 platform${process.platform} arch${process.arch} appData${app.getPath("userData")} version:${app.getVersion()}`)
let menu: Array<(MenuItemConstructorOptions) | (MenuItem)> = [
{
label: '显示主窗口', click: function () {
win.show();
if (process.platform === "darwin") {
app.dock.show();
}
}
},
{
label: '退出',
click: () => {
isQuiting = true;
stopFrpcProcess(() => {
app.quit();
})
}
initProxyApi();
logInfo(LogModule.APP, `Proxy API initialized.`);
initFrpcApi();
logInfo(LogModule.APP, `FRPC API initialized.`);
initLoggerApi();
logInfo(LogModule.APP, `Logger API initialized.`);
initFileApi();
logInfo(LogModule.APP, `File API initialized.`);
initCommonApi();
logInfo(LogModule.APP, `Common API initialized.`);
initLocalApi();
logInfo(LogModule.APP, `Local API initialized.`);
// initUpdaterApi(win);
logInfo(LogModule.APP, `Updater API initialization skipped.`);
} catch (error) {
logError(
LogModule.APP,
`Error during API initialization: ${error.message}`
);
}
];
tray = new Tray(node_path.join(process.env.VITE_PUBLIC, "logo/only/16x16.png"))
tray.setToolTip('Frpc Desktop')
const contextMenu = Menu.buildFromTemplate(menu)
tray.setContextMenu(contextMenu)
// 托盘双击打开
tray.on('double-click', () => {
win.show();
})
getConfig((err, config) => {
if (!err) {
if (config) {
if (config.systemStartupConnect) {
log.info(`已开启自动连接 正在自动连接服务器`)
startFrpWorkerProcess(config)
}
}
}
})
}
app.whenReady().then(() => {
createWindow().then(r => {
createTray()
// 初始化各个API
initGitHubApi();
initConfigApi();
initProxyApi();
initFrpcApi();
initLoggerApi();
initFileApi();
initCommonApi();
initLocalApi();
// initUpdaterApi(win);
})
})
.catch(error => {
logError(LogModule.APP, `Error creating window: ${error.message}`);
});
});
});
app.on("window-all-closed", () => {
win = null;
if (process.platform !== "darwin") {
stopFrpcProcess(() => {
app.quit();
})
}
logInfo(LogModule.APP, `All windows closed.`);
win = null;
if (process.platform !== "darwin") {
stopFrpcProcess(() => {
logInfo(LogModule.APP, `FRPC process stopped. Quitting application.`);
app.quit();
});
}
});
app.on("second-instance", () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
logInfo(LogModule.APP, `Second instance detected.`);
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.on("activate", () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
createWindow();
}
});
app.on('before-quit', () => {
log.info("before-quit")
isQuiting = true;
})
// New window example arg: new windows url
ipcMain.handle("open-win", (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false
}
logInfo(LogModule.APP, `Application activated.`);
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length) {
allWindows[0].focus();
} else {
getConfig((err, config) => {
if (err) {
logError(
LogModule.APP,
`Failed to get config on activate: ${err.message}`
);
return;
}
createWindow(config).then(r => {
logInfo(LogModule.APP, `Window created on activate.`);
});
});
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`);
} else {
childWindow.loadFile(indexHtml, {hash: arg});
}
}
});
app.on("before-quit", () => {
logInfo(LogModule.APP, `Application is about to quit.`);
stopFrpcProcess(() => {
isQuiting = true;
});
});
ipcMain.handle("open-win", (_, arg) => {
logInfo(LogModule.APP, `Opening new window with argument: ${arg}`);
const childWindow = new BrowserWindow({
webPreferences: {
preload,
nodeIntegration: true,
contextIsolation: false
}
});
if (process.env.VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${url}#${arg}`);
logInfo(LogModule.APP, `Child window loaded URL: ${url}#${arg}`);
} else {
childWindow.loadFile(indexHtml, { hash: arg });
logInfo(
LogModule.APP,
`Child window loaded file: ${indexHtml} with hash: ${arg}`
);
}
});

View File

@ -1,47 +1,53 @@
import Datastore from "nedb";
import path from "path";
import {app} from "electron";
import { app } from "electron";
const log = require('electron-log');
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
import { maskSensitiveData } from "../utils/desensitize";
const configDB = new Datastore({
autoload: true,
filename: path.join(app.getPath("userData"), "config.db")
autoload: true,
filename: path.join(app.getPath("userData"), "config.db")
});
export type Config = {
currentVersion: any;
serverAddr: string;
serverPort: number;
authMethod: string;
authToken: string;
logLevel: string;
logMaxDays: number;
tlsConfigEnable: boolean;
tlsConfigCertFile: string;
tlsConfigKeyFile: string;
tlsConfigTrustedCaFile: string;
tlsConfigServerName: string;
proxyConfigEnable: boolean;
proxyConfigProxyUrl: string;
systemSelfStart: boolean;
systemStartupConnect: boolean;
user: string;
metaToken: string;
transportHeartbeatInterval: number;
transportHeartbeatTimeout: number;
};
/**
*
*/
export const saveConfig = (
document: Config,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
document: FrpConfig,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
document["_id"] = "1";
log.debug(`保存日志 ${JSON.stringify(document)}`)
configDB.update({_id: "1"}, document, {upsert: true}, cb);
document["_id"] = "1";
logDebug(
LogModule.DB,
`Saving configuration to the database. ${JSON.stringify(
maskSensitiveData(document, [
"serverAddr",
"serverPort",
"authToken",
"user",
"metaToken"
])
)}`
);
configDB.update(
{ _id: "1" },
document,
{ upsert: true },
(err, numberOfUpdated, upsert) => {
if (err) {
logError(
LogModule.DB,
`Error saving configuration: ${err.message}`
);
} else {
logInfo(
LogModule.DB,
`Configuration saved successfully. Updated: ${numberOfUpdated}, Upsert: ${upsert}`
); // 添加成功日志
}
if (cb) cb(err, numberOfUpdated, upsert);
}
);
};
/**
@ -49,7 +55,36 @@ export const saveConfig = (
* @param cb
*/
export const getConfig = (
cb: (err: Error | null, document: Config) => void
cb: (err: Error | null, document: FrpConfig) => void
) => {
configDB.findOne({_id: "1"}, cb);
logInfo(LogModule.DB, "Retrieving configuration from the database."); // 添加信息日志
configDB.findOne({ _id: "1" }, (err, document) => {
if (err) {
logError(
LogModule.DB,
`Error retrieving configuration: ${err.message}`
); // 添加错误日志
} else {
logInfo(LogModule.DB, "Configuration retrieved successfully."); // 添加成功日志
}
cb(err, document);
});
};
export const clearConfig = (cb?: (err: Error | null, n: number) => void) => {
logInfo(LogModule.DB, "Clearing all configurations from the database."); // 添加信息日志
configDB.remove({}, { multi: true }, (err, n) => {
if (err) {
logError(
LogModule.DB,
`Error clearing configurations: ${err.message}`
); // 添加错误日志
} else {
logInfo(
LogModule.DB,
`Successfully cleared configurations. Number of documents removed: ${n}`
); // 添加成功日志
}
if (cb) cb(err, n);
});
};

View File

@ -1,77 +1,143 @@
import Datastore from "nedb";
import path from "path";
import { app } from "electron";
const log = require('electron-log');
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const proxyDB = new Datastore({
autoload: true,
filename: path.join(app.getPath("userData"), "proxy.db")
});
export type Proxy = {
_id: string;
name: string;
type: string;
localIp: string;
localPort: number;
remotePort: number;
customDomains: string[];
};
/**
*
* @param proxy
* @param cb
*/
export const insertProxy = (
proxy: Proxy,
cb?: (err: Error | null, document: Proxy) => void
) => {
log.debug(`新增代理:${JSON.stringify(proxy)}`);
proxyDB.insert(proxy, cb);
logInfo(LogModule.DB, `Inserting proxy: ${JSON.stringify(proxy)}`);
proxyDB.insert(proxy, (err, document) => {
if (err) {
logError(LogModule.DB, `Error inserting proxy: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy inserted successfully: ${JSON.stringify(document)}`
);
}
if (cb) cb(err, document);
});
};
/**
*
* @param _id
* @param cb
*/
export const deleteProxyById = (
_id: string,
cb?: (err: Error | null, n: number) => void
) => {
log.debug(`删除代理:${_id}`);
proxyDB.remove({ _id: _id }, cb);
logInfo(LogModule.DB, `Deleting proxy with ID: ${_id}`);
proxyDB.remove({ _id: _id }, (err, n) => {
if (err) {
logError(LogModule.DB, `Error deleting proxy: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy deleted successfully. Number of documents removed: ${n}`
);
}
if (cb) cb(err, n);
});
};
/**
*
*/
export const updateProxyById = (
proxy: Proxy,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
log.debug(`修改代理:${proxy}`);
proxyDB.update({ _id: proxy._id }, proxy, {}, cb);
logInfo(LogModule.DB, `Updating proxy: ${JSON.stringify(proxy)}`);
proxyDB.update(
{ _id: proxy._id },
proxy,
{},
(err, numberOfUpdated, upsert) => {
if (err) {
logError(LogModule.DB, `Error updating proxy: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy updated successfully. Updated: ${numberOfUpdated}, Upsert: ${upsert}`
);
}
if (cb) cb(err, numberOfUpdated, upsert);
}
);
};
/**
*
* @param cb
*/
export const listProxy = (
callback: (err: Error | null, documents: Proxy[]) => void
) => {
proxyDB.find({}, callback);
logInfo(LogModule.DB, `Listing all proxies`);
proxyDB.find({}, (err, documents) => {
if (err) {
logError(LogModule.DB, `Error listing proxies: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxies listed successfully. Count: ${documents.length}`
);
}
callback(err, documents);
});
};
/**
* id查询
* @param id
* @param callback
*/
export const getProxyById = (
id: string,
callback: (err: Error | null, document: Proxy) => void
) => {
proxyDB.findOne({ _id: id }, callback);
logInfo(LogModule.DB, `Getting proxy by ID: ${id}`);
proxyDB.findOne({ _id: id }, (err, document) => {
if (err) {
logError(LogModule.DB, `Error getting proxy by ID: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy retrieved successfully: ${JSON.stringify(document)}`
);
}
callback(err, document);
});
};
export const clearProxy = (cb?: (err: Error | null, n: number) => void) => {
logInfo(LogModule.DB, `Clearing all proxies`);
proxyDB.remove({}, { multi: true }, (err, n) => {
if (err) {
logError(LogModule.DB, `Error clearing proxies: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxies cleared successfully. Number of documents removed: ${n}`
);
}
if (cb) cb(err, n);
});
};
export const updateProxyStatus = (
id: string,
st: boolean,
cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void
) => {
logInfo(LogModule.DB, `Updating proxy status for ID: ${id} to ${st}`);
proxyDB.update(
{ _id: id },
{ $set: { status: st } },
{},
(err, numberOfUpdated, upsert) => {
if (err) {
logError(LogModule.DB, `Error updating proxy status: ${err.message}`);
} else {
logInfo(
LogModule.DB,
`Proxy status updated successfully. Updated: ${numberOfUpdated}, Upsert: ${upsert}`
);
}
if (cb) cb(err, numberOfUpdated, upsert);
}
);
};

View File

@ -1,44 +1,90 @@
import Datastore from "nedb";
import path from "path";
import {app} from "electron";
const log = require('electron-log');
import { app } from "electron";
import { logInfo, logError, LogModule, logDebug } from "../utils/log";
const versionDB = new Datastore({
autoload: true,
filename: path.join(app.getPath("userData"), "version.db")
autoload: true,
filename: path.join(app.getPath("userData"), "version.db")
});
/**
*
* @param proxy
* Insert version
* @param version
* @param cb
*/
export const insertVersion = (
version: any,
cb?: (err: Error | null, document: any) => void
version: FrpVersion,
cb?: (err: Error | null, document: any) => void
) => {
log.debug(`新增版本:${JSON.stringify(version)}`);
versionDB.insert(version, cb);
logInfo(LogModule.DB, `Inserting version: ${JSON.stringify(version)}`);
versionDB.insert(version, (err, document) => {
if (err) {
logError(LogModule.DB, `Error inserting version: ${err.message}`);
} else {
logInfo(LogModule.DB, `Version inserted successfully: ${JSON.stringify(document)}`);
}
if (cb) cb(err, document);
});
};
/**
*
* List versions
* @param cb
*/
export const listVersion = (
callback: (err: Error | null, documents: any[]) => void
callback: (err: Error | null, documents: FrpVersion[]) => void
) => {
versionDB.find({}, callback);
logInfo(LogModule.DB, "Listing all versions.");
versionDB.find({}, (err, documents) => {
if (err) {
logError(LogModule.DB, `Error listing versions: ${err.message}`);
} else {
logInfo(LogModule.DB, `Successfully listed versions: ${documents.length} found.`);
}
callback(err, documents);
});
};
export const getVersionById = (
id: string,
callback: (err: Error | null, document: any) => void
id: number,
callback: (err: Error | null, document: FrpVersion) => void
) => {
versionDB.findOne({id: id}, callback);
logInfo(LogModule.DB, `Retrieving version by ID: ${id}`);
versionDB.findOne({ id: id }, (err, document) => {
if (err) {
logError(LogModule.DB, `Error retrieving version by ID: ${err.message}`);
} else {
logInfo(LogModule.DB, `Version retrieved successfully: ${JSON.stringify(document)}`);
}
callback(err, document);
});
};
export const deleteVersionById = (id: string, callback: (err: Error | null, document: any) => void) => {
log.debug(`删除版本:${id}`);
versionDB.remove({id: id}, callback);
}
export const deleteVersionById = (
id: string,
callback: (err: Error | null, document: any) => void
) => {
logInfo(LogModule.DB, `Deleting version: ${id}`);
versionDB.remove({ id: id }, (err, document) => {
if (err) {
logError(LogModule.DB, `Error deleting version: ${err.message}`);
} else {
logInfo(LogModule.DB, `Version deleted successfully: ${id}`);
}
callback(err, document);
});
};
export const clearVersion = (cb?: (err: Error | null, n: number) => void) => {
logInfo(LogModule.DB, "Clearing all versions from the database.");
versionDB.remove({}, { multi: true }, (err, n) => {
if (err) {
logError(LogModule.DB, `Error clearing versions: ${err.message}`);
} else {
logInfo(LogModule.DB, `Successfully cleared versions. Number of documents removed: ${n}`);
}
if (cb) cb(err, n);
});
};

View File

@ -0,0 +1,12 @@
export const maskSensitiveData = (
obj: Record<string, any>,
keysToMask: string[]
) => {
const maskedObj = JSON.parse(JSON.stringify(obj));
keysToMask.forEach(key => {
if (maskedObj.hasOwnProperty(key)) {
maskedObj[key] = "***";
}
});
return maskedObj;
};

19
electron/utils/file.ts Normal file
View File

@ -0,0 +1,19 @@
import { createHash } from "crypto";
export const formatBytes = (bytes: number, decimals: number = 2): string => {
if (bytes === 0) return "0 Bytes";
const k = 1024; // 1 KB = 1024 Bytes
const dm = decimals < 0 ? 0 : decimals; // Ensure decimal places are not less than 0
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); // Calculate unit index
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i]; // Return formatted string
};
export const calculateFileChecksum = (filePath: string): string => {
const fs = require("fs");
const fileBuffer = fs.readFileSync(filePath);
const hash = createHash("sha256");
hash.update(fileBuffer);
return hash.digest("hex");
};

30
electron/utils/log.ts Normal file
View File

@ -0,0 +1,30 @@
import log from "electron-log";
// 定义模块枚举
export enum LogModule {
APP = "app",
FRP_CLIENT = "frpc client",
GITHUB = "github",
DB = "db"
}
export const initLog = () => {
log.transports.file.level = "debug";
log.transports.console.level = "debug";
};
// 自定义日志输出函数,记录到指定业务模块
export const logInfo = (module: LogModule, message: string) => {
log.info(`[${module}] ${message}`);
};
export const logError = (module: LogModule, message: string) => {
log.error(`[${module}] ${message}`);
};
export const logDebug = (module: LogModule, message: string) => {
log.debug(`[${module}] ${message}`);
};
export const logWarn = (module: LogModule, message: string) => {
log.warn(`[${module}] ${message}`);
};

View File

@ -1,17 +1,21 @@
{
"name": "Frpc-Desktop",
"version": "1.0.7",
"version": "1.1.6",
"main": "dist-electron/main/index.js",
"description": "FRP跨平台桌面客户端可视化配置轻松实现内网穿透",
"repository": "github:luckjiawei/frpc-desktop",
"author": "刘嘉伟 <8473136@qq.com>",
"license": "MIT",
"private": true,
"type": "commonjs",
"keywords": [
"frp",
"frpc",
"proxy",
"electron-app"
"electron-app",
"vue",
"vue3",
"vite"
],
"engines": {
"node": ">= 18"
@ -26,6 +30,7 @@
"build": "vue-tsc --noEmit && vite build",
"build:electron": "npm run build && electron-builder --mac --win --linux",
"build:electron:mac": "npm run build && electron-builder --mac",
"build:electron:mac:arm": "npm run build && electron-builder --mac --arm64",
"build:electron:win": "npm run build && electron-builder --win",
"build:electron:linux": "npm run build && electron-builder --linux",
"release": "npm run build && electron-builder --mac --win --linux -p always",
@ -36,12 +41,14 @@
"electron:generate-icons": "electron-icon-builder --input=./public/logo.png --output=build --flatten"
},
"devDependencies": {
"@iconify-icons/material-symbols": "^1.2.58",
"@iconify/vue": "^4.1.1",
"@types/nedb": "^1.8.16",
"@vitejs/plugin-vue": "^4.3.3",
"@vue/eslint-config-prettier": "^7.1.0",
"@vueuse/core": "^9.13.0",
"autoprefixer": "^10.4.15",
"canvas-confetti": "^1.9.0",
"cssnano": "^6.0.1",
"electron": "^26.6.10",
"electron-builder": "^24.13.3",
@ -63,17 +70,20 @@
"vite-plugin-electron-renderer": "^0.14.5",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vue-tsc": "^2.0.22",
"vue-tsc": "2.0.22",
"vue-types": "^5.1.1"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"adm-zip": "^0.5.14",
"animate.css": "^4.1.1",
"electron-dl": "^3.5.1",
"electron-dl": "3.5.1",
"electron-log": "^5.1.7",
"intro.js": "^8.0.0-beta.1",
"isbinaryfile": "4.0.10",
"js-base64": "^3.7.7",
"tar": "^6.2.0",
"unused-filename": "^4.0.1"
"unused-filename": "^4.0.1",
"uuid": "^10.0.0"
}
}

BIN
screenshots/gratuity.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

BIN
screenshots/mp_qr.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

BIN
screenshots/wechat-qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

View File

@ -0,0 +1,9 @@
import iconifyIconOffline from "./src/iconifyIconOffline";
import iconifyIconOnline from "./src/iconifyIconOnline";
/** 本地图标组件 */
const IconifyIconOffline = iconifyIconOffline;
/** 在线图标组件 */
const IconifyIconOnline = iconifyIconOnline;
export {IconifyIconOffline, IconifyIconOnline};

View File

@ -0,0 +1,30 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon, addIcon } from "@iconify/vue/dist/offline";
// Iconify Icon在Vue里本地使用用于内网环境https://docs.iconify.design/icon-components/vue/offline.html
export default defineComponent({
name: "IconifyIconOffline",
components: { IconifyIcon },
props: {
icon: {
default: null
}
},
render() {
if (typeof this.icon === "object") addIcon(this.icon, this.icon);
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: this.icon,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

View File

@ -0,0 +1,30 @@
import { h, defineComponent } from "vue";
import { Icon as IconifyIcon } from "@iconify/vue";
// Iconify Icon在Vue里在线使用用于外网环境
export default defineComponent({
name: "IconifyIconOnline",
components: { IconifyIcon },
props: {
icon: {
type: String,
default: ""
}
},
render() {
const attrs = this.$attrs;
return h(
IconifyIcon,
{
icon: `${this.icon}`,
style: attrs?.style
? Object.assign(attrs.style, { outline: "none" })
: { outline: "none" },
...attrs
},
{
default: () => []
}
);
}
});

View File

@ -0,0 +1,79 @@
import {addIcon} from "@iconify/vue/dist/offline";
import Cloud from "@iconify-icons/material-symbols/cloud";
import RocketLaunchRounded from "@iconify-icons/material-symbols/rocket-launch-rounded";
import Download from "@iconify-icons/material-symbols/download-2";
import Settings from "@iconify-icons/material-symbols/settings";
import FileCopySharp from "@iconify-icons/material-symbols/file-copy-sharp";
import InfoSharp from "@iconify-icons/material-symbols/info-sharp";
import refreshRounded from "@iconify-icons/material-symbols/refresh-rounded";
import MoreVert from "@iconify-icons/material-symbols/more-vert";
import Add from "@iconify-icons/material-symbols/add";
import BringYourOwnIpRounded from "@iconify-icons/material-symbols/bring-your-own-ip-rounded";
import DeleteRounded from "@iconify-icons/material-symbols/delete-rounded";
import CancelPresentation from "@iconify-icons/material-symbols/cancel-presentation";
import GestureSelect from "@iconify-icons/material-symbols/gesture-select";
import SaveRounded from "@iconify-icons/material-symbols/save-rounded";
import Info from "@iconify-icons/material-symbols/info";
import QuestionMark from "@iconify-icons/material-symbols/question-mark";
import CheckCircleRounded from "@iconify-icons/material-symbols/check-circle-rounded";
import Error from "@iconify-icons/material-symbols/error";
import ContentCopy from "@iconify-icons/material-symbols/content-copy";
import ContentPasteGo from "@iconify-icons/material-symbols/content-paste-go";
import Edit from "@iconify-icons/material-symbols/edit";
import CheckBox from "@iconify-icons/material-symbols/check-box";
import ExportNotesOutline from "@iconify-icons/material-symbols/export-notes-outline";
import uploadRounded from "@iconify-icons/material-symbols/upload-rounded";
import downloadRounded from "@iconify-icons/material-symbols/download-rounded";
import deviceReset from "@iconify-icons/material-symbols/device-reset";
import switchAccessOutlineRounded from "@iconify-icons/material-symbols/switch-access-outline-rounded";
import switchAccessRounded from "@iconify-icons/material-symbols/switch-access-rounded";
import chargerRounded from "@iconify-icons/material-symbols/charger-rounded";
import fileOpenRounded from "@iconify-icons/material-symbols/file-open-rounded";
import attachMoneyRounded from "@iconify-icons/material-symbols/attach-money-rounded";
import volunteerActivismSharp from "@iconify-icons/material-symbols/volunteer-activism-sharp";
import description from "@iconify-icons/material-symbols/description";
import folderRounded from "@iconify-icons/material-symbols/folder-rounded";
import link from "@iconify-icons/material-symbols/link";
import unarchive from "@iconify-icons/material-symbols/unarchive";
import fileSaveRounded from "@iconify-icons/material-symbols/file-save-rounded";
addIcon("cloud", Cloud);
addIcon("rocket-launch-rounded", RocketLaunchRounded);
addIcon("download", Download);
addIcon("settings", Settings);
addIcon("file-copy-sharp", FileCopySharp);
addIcon("info-sharp", InfoSharp);
addIcon("refresh-rounded", refreshRounded);
addIcon("more-vert", MoreVert);
addIcon("add", Add);
addIcon("bring-your-own-ip-rounded", BringYourOwnIpRounded);
addIcon("charger-rounded", chargerRounded);
addIcon("delete-rounded", DeleteRounded);
addIcon("cancel-presentation", CancelPresentation);
addIcon("gesture-select", GestureSelect);
addIcon("save-rounded", SaveRounded);
addIcon("info", Info);
addIcon("question-mark", QuestionMark);
addIcon("check-circle-rounded", CheckCircleRounded);
addIcon("error", Error);
addIcon("content-copy", ContentCopy);
addIcon("content-paste-go", ContentPasteGo);
addIcon("edit", Edit);
addIcon("check-box", CheckBox);
addIcon("export", ExportNotesOutline);
addIcon("uploadRounded", uploadRounded);
addIcon("downloadRounded", downloadRounded);
addIcon("deviceReset", deviceReset);
addIcon("switchAccessOutlineRounded", switchAccessOutlineRounded);
addIcon("switchAccessRounded", switchAccessRounded);
addIcon("file-open-rounded", fileOpenRounded);
addIcon("attach-money-rounded", attachMoneyRounded);
addIcon("volunteer-activism-sharp", volunteerActivismSharp);
addIcon("description", description);
addIcon("folder-rounded", folderRounded);
addIcon("link", link);
addIcon("unarchive", unarchive);
addIcon("file-save-rounded", fileSaveRounded);

21
src/intro/index.ts Normal file
View File

@ -0,0 +1,21 @@
import introJs from "intro.js";
import "intro.js/introjs.css";
const intro = introJs.tour();
// 更多配置参数请到官方文档查看
intro.setOptions({
nextLabel: "下一个", // 下一个按钮文字
prevLabel: "上一个", // 上一个按钮文字
// skipLabel: '跳过', // 跳过按钮文字
doneLabel: "🎉 立即体验", // 完成按钮文字
autoPosition: false,
tooltipPosition: "right",
exitOnOverlayClick: false,
// hidePrev: true, // 在第一步中是否隐藏上一个按钮
// hideNext: true, // 在最后一步中是否隐藏下一个按钮
// exitOnOverlayClick: false, // 点击叠加层时是否退出介绍
// showStepNumbers: false, // 是否显示红色圆圈的步骤编号
// showBullets: false // 是否显示面板指示点
});
export default intro;

View File

@ -1,5 +1,4 @@
<script setup lang="ts">
import { Icon } from "@iconify/vue";
import { computed, defineComponent } from "vue";
import router from "@/router";
@ -13,15 +12,16 @@ const currentRoute = computed(() => {
</script>
<template>
<div class="flex justify-between">
<div class="breadcrumb">
<Icon
<div class="breadcrumb flex justify-between items-center">
<!-- animate__animated animate__lightSpeedInLeft-->
<div class="flex items-center justify-center breadcrumb-left">
<IconifyIconOffline
class="inline-block mr-2"
:icon="currentRoute.meta['icon'] as string"
/>
<span>{{ currentRoute.meta["title"] }}</span>
</div>
<div class="right">
<div class="breadcrumb-right">
<slot></slot>
</div>
</div>

View File

@ -1,16 +1,47 @@
<script lang="ts" setup>
import {computed, defineComponent, onMounted, ref} from "vue";
import {Icon} from "@iconify/vue";
import { computed, defineComponent, onMounted, ref } from "vue";
import router from "@/router";
import {RouteRecordRaw} from "vue-router";
import pkg from '../../../package.json';
import { RouteRecordRaw } from "vue-router";
import pkg from "../../../package.json";
import Intro from "@/intro";
import "intro.js/introjs.css";
import confetti from "canvas-confetti/src/confetti.js";
defineComponent({
name: "AppMain"
});
const routes = ref<Array<RouteRecordRaw>>([]);
const guideSteps = ref({
Home: {
step: "1",
intro: "此功能用于控制frpc的连接状态您可以轻松地断开或重新连接。"
},
Proxy: {
step: "2",
intro:
"在这里,您可以方便地配置和管理代理设置。无论是添加、修改还是删除代理,您都可以轻松完成。"
},
Download: {
step: "3",
intro: "通过此功能您可以快速下载最新版本的frp。"
},
Config: {
step: "4",
intro:
"此功能允许您设置软件的各种配置选项,包括连接方式等。根据您的需求进行个性化设置,以优化使用体验。"
},
Logger: {
step: "5",
intro:
"在日志查看功能中您可以实时查看FRP连接的日志信息。这有助于您监控连接状态及时排查可能出现的问题。"
},
Version: {
step: "6",
intro:
"通过此功能您可以查看当前安装的Frpc-Desktop版本并检查是否有可用更新。"
}
});
const currentRoute = computed(() => {
return router.currentRoute.value;
});
@ -32,38 +63,83 @@ const handleOpenGitHubReleases = () => {
// ipcRenderer.send("github.openReleases")
router.push({
name: "About"
})
}
});
};
const handleCompleteGuide: () => boolean = () => {
//
confetti({
zIndex: 12002,
particleCount: 200,
spread: 70,
origin: { y: 0.6 }
});
localStorage.setItem("guide", new Date().getTime().toString());
return true; // boolean
};
onMounted(() => {
routes.value = router.options.routes[0].children?.filter(
f => !f.meta?.hidden
f => !f.meta?.hidden
) as Array<RouteRecordRaw>;
if (!localStorage.getItem("guide")) {
//
Intro.onBeforeExit(handleCompleteGuide).start();
}
});
</script>
<template>
<div class="left-menu-container drop-shadow-xl">
<div class="logo-container">
<img src="/logo/only/128x128.png" class="logo animate__animated animate__flip" alt="Logo"/>
<img
src="/logo/only/128x128.png"
class="logo animate__animated animate__bounceInLeft"
alt="Logo"
/>
</div>
<ul class="menu-container">
<!-- enter-active-class="animate__animated animate__bounceIn"-->
<!-- leave-active-class="animate__animated animate__fadeOut"-->
<li
class="menu animate__animated"
:class="currentRoute?.name === r.name ? 'menu-selected' : ''"
v-for="r in routes"
:key="r.name"
@click="handleMenuChange(r)"
:data-step="guideSteps[r.name]?.step"
:data-intro="guideSteps[r.name]?.intro"
:data-disable-interaction="true"
class="menu animate__animated"
:class="currentRoute?.name === r.name ? 'menu-selected' : ''"
v-for="r in routes"
:key="r.name"
@click="handleMenuChange(r)"
>
<Icon class="animate__animated" :icon="r?.meta?.icon as string"/>
<IconifyIconOffline
class="animate__animated"
:icon="r?.meta?.icon as string"
></IconifyIconOffline>
</li>
</ul>
<div class="version mb-2 animate__animated" @click="handleOpenGitHubReleases">
{{ pkg.version }}
<div class="menu-footer mb-2">
<!-- <div-->
<!-- class="menu animate__animated"-->
<!-- @click="handleOpenGitHubReleases"-->
<!-- :data-step="guideSteps.Version?.step"-->
<!-- :data-intro="guideSteps.Version?.intro"-->
<!-- data-position="top"-->
<!-- >-->
<!-- <IconifyIconOffline-->
<!-- class="animate__animated"-->
<!-- icon="attach-money-rounded"-->
<!-- ></IconifyIconOffline>-->
<!-- </div>-->
<div
class="version animate__animated"
@click="handleOpenGitHubReleases"
:data-step="guideSteps.Version?.step"
:data-intro="guideSteps.Version?.intro"
data-position="top"
>
{{ pkg.version }}
</div>
</div>
</div>
</template>

View File

@ -2,6 +2,7 @@
import { defineComponent } from "vue";
import AppMain from "./compoenets/AppMain.vue";
import LeftMenu from "./compoenets/LeftMenu.vue";
import "@/components/IconifyIcon/src/offlineIcon";
defineComponent({
name: "Layout"

View File

@ -1,14 +1,22 @@
import { createApp } from "vue";
import {createApp} from "vue";
import App from "./App.vue";
import router from "./router";
import "./styles/index.scss";
import 'animate.css';
import ElementPlus from "element-plus";
import {
IconifyIconOffline,
IconifyIconOnline,
} from "./components/IconifyIcon";
createApp(App)
.use(router)
.use(ElementPlus)
.mount("#app")
.$nextTick(() => {
postMessage({ payload: "removeLoading" }, "*");
});
const app = createApp(App);
app.component("IconifyIconOffline", IconifyIconOffline);
app.component("IconifyIconOnline", IconifyIconOnline);
app.use(router)
.use(ElementPlus)
.mount("#app")
.$nextTick(() => {
postMessage({payload: "removeLoading"}, "*");
});

View File

@ -14,7 +14,7 @@ const routes: RouteRecordRaw[] = [
name: "Home",
meta: {
title: "连接",
icon: "material-symbols:rocket-launch-rounded",
icon: "rocket-launch-rounded",
keepAlive: true
},
component: () => import("@/views/home/index.vue")
@ -24,7 +24,7 @@ const routes: RouteRecordRaw[] = [
name: "Proxy",
meta: {
title: "穿透列表",
icon: "material-symbols:cloud",
icon: "cloud",
keepAlive: true
},
component: () => import("@/views/proxy/index.vue")
@ -34,7 +34,7 @@ const routes: RouteRecordRaw[] = [
name: "Download",
meta: {
title: "版本下载",
icon: "material-symbols:download-2",
icon: "download",
keepAlive: true
},
component: () => import("@/views/download/index.vue")
@ -44,7 +44,7 @@ const routes: RouteRecordRaw[] = [
name: "Config",
meta: {
title: "系统配置",
icon: "material-symbols:settings",
icon: "settings",
keepAlive: true
},
component: () => import("@/views/config/index.vue")
@ -54,7 +54,7 @@ const routes: RouteRecordRaw[] = [
name: "Logger",
meta: {
title: "日志",
icon: "material-symbols:file-copy-sharp",
icon: "file-copy-sharp",
keepAlive: true,
hidden: false
},
@ -65,7 +65,7 @@ const routes: RouteRecordRaw[] = [
name: "About",
meta: {
title: "关于",
icon: "material-symbols:info-sharp",
icon: "info-sharp",
keepAlive: true,
hidden: true
},

View File

@ -1,8 +1,8 @@
@import "reset";
@import "layout";
@import "element";
@import "tailwind";
@import "scrollbar";
@use "reset";
@use "layout";
@use "element";
@use "tailwind";
@use "scrollbar";
/* 自定义全局 CssVar */
:root {
--pure-transition-duration: 0.016s;
@ -20,3 +20,20 @@
html {
}
.h2 {
color: #5a3daa;
font-size: 16px;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "微软雅黑", Arial, sans-serif;
font-weight: 700;
padding: 6px 10px 6px 15px;
border-left: 5px solid #5a3daa;
border-radius: 4px;
background-color: #eeebf6;
margin-bottom: 18px;
}
.left-border {
border-left: 5px solid #5a3daa;
}

View File

@ -21,24 +21,30 @@ $danger-color: #F56C6C;
}
.breadcrumb {
color: $primary-color;
font-size: 36px;
height: 50px;
margin-bottom: 15px;
svg {
vertical-align: top;
}
.breadcrumb-left {
color: $primary-color;
font-size: 36px;
height: 30px;
span {
vertical-align: top;
color: black;
font-size: 18px;
font-weight: bold;
transform: translateY(5px);
display: inline-block;
svg {
vertical-align: top;
}
span {
vertical-align: top;
color: black;
font-size: 18px;
font-weight: bold;
//transform: translateY(5px);
display: inline-block;
}
}
}
}
.left-menu-container {
@ -48,20 +54,28 @@ $danger-color: #F56C6C;
display: flex; /* 设置为 flexbox */
flex-direction: column; /* 纵向排列子元素 */
.menu-footer {
margin-top: auto;
}
.version {
height: 40px;
width: 60px;
display: flex;
justify-content: center;
align-items: center;
color: $primary-color;
text-align: center;
margin-top: auto;
font-weight: bold;
font-size: 14px;
cursor: pointer;
}
.version:hover {
animation: heartBeat 1s;
}
.menu-container {
.menu-container, .menu-footer {
.menu {
display: flex;
@ -91,7 +105,6 @@ $danger-color: #F56C6C;
}
}
}
@ -111,6 +124,10 @@ $danger-color: #F56C6C;
color: $primary-color;
}
.bg-primary {
background: $primary-color;
}
.danger-text {
color: $danger-color !important;
}

View File

@ -6,7 +6,7 @@
-webkit-border-radius: 15px;
}
::-webkit-scrollbar-track-piece {
background-color: #ffff;
background-color: #fff;
border-radius:15px;
-webkit-border-radius: 15px;
}

View File

@ -1,9 +0,0 @@
declare module 'element-plus/dist/locale/zh-cn.mjs' {
const zhLocale: any;
export default zhLocale;
}
declare module 'element-plus/dist/locale/en.mjs' {
const enLocale: any;
export default enLocale;
}

View File

@ -44,6 +44,22 @@ const handleOpenGitHub = () => {
ipcRenderer.send("common.openUrl", "https://github.com/luckjiawei/frpc-desktop")
}
/**
* 打开捐赠界面
*/
const handleOpenDonate = () => {
ipcRenderer.send("common.openUrl", "https://jwinks.com/donate")
}
/**
* 打开文档
*/
const handleOpenDoc = () => {
ipcRenderer.send("common.openUrl", "https://jwinks.com/p/frp")
}
/**
* 获取最后一个版本
*/
@ -103,8 +119,6 @@ defineComponent({
class="w-[95px] h-[95px] mt-[-50px] animate__animated animate__flip" alt="Logo"/>
<div class="mt-[8px] text-2xl">Frpc Desktop</div>
<div class="mt-[8px] text-neutral-400 flex items-center">
<!-- <span class="font-bold"> v{{ pkg.version }}</span>-->
<el-link
:class="!isLastVersion? 'line-through': ''"
class="ml-2 font-bold">v{{ pkg.version }}
@ -114,11 +128,8 @@ defineComponent({
class="ml-2 text-[#67C23A] font-bold"
type="success">v{{ latestVersionInfo.name }}
</el-link>
<!-- <span class="ml-2 text-[#67C23A] font-bold"-->
<!-- @click="handleOpenNewVersion"-->
<!-- v-if="!isLastVersion && latestVersionInfo">v{{ latestVersionInfo.name }}</span>-->
<Icon class="ml-1.5 cursor-pointer check-update" icon="material-symbols:refresh-rounded"
@click="handleGetLastVersion"></Icon>
<IconifyIconOffline class="ml-1.5 cursor-pointer check-update" icon="refresh-rounded"
@click="handleGetLastVersion"/>
</div>
<div class="mt-[8px] text-sm text-center">
<p>
@ -129,12 +140,20 @@ defineComponent({
</p>
</div>
<div class="mt-[12px]">
<el-button plain type="success" @click="handleOpenDoc">
<IconifyIconOffline class="cursor-pointer mr-2" icon="description"/>
使用教程
</el-button>
<el-button plain type="success" @click="handleOpenDonate">
<IconifyIconOffline class="cursor-pointer mr-2" icon="volunteer-activism-sharp"/>
捐赠名单
</el-button>
<el-button plain type="primary" @click="handleOpenGitHub">
<Icon class="cursor-pointer mr-2" icon="logos:github-icon"/>
仓库地址
</el-button>
<el-button type="danger" plain @click="handleOpenGitHubIssues">
<Icon class="cursor-pointer mr-2" icon="material-symbols:question-mark"/>
<IconifyIconOffline class="cursor-pointer mr-2" icon="question-mark"/>
反馈问题
</el-button>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,45 @@
<script lang="ts" setup>
import {defineComponent, onMounted, onUnmounted, ref} from "vue";
import {ipcRenderer} from "electron";
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import { ipcRenderer } from "electron";
import moment from "moment";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import {Icon} from "@iconify/vue";
import {ElMessage} from "element-plus";
import {useDebounceFn} from "@vueuse/core";
import { ElMessage, ElMessageBox } from "element-plus";
import { useDebounceFn } from "@vueuse/core";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
defineComponent({
name: "Download"
});
type Asset = {
name: string
}
type Version = {
id: string;
name: string;
published_at: string;
download_completed: boolean;
absPath: string;
assets: Asset[]
};
const versions = ref<Array<Version>>([]);
const versions = ref<Array<FrpVersion>>([]);
const loading = ref(1);
const downloadPercentage = ref(0);
const downloading = ref<Map<string, number>>(new Map<string, number>());
const downloading = ref<Map<number, number>>(new Map<number, number>());
const currMirror = ref("github");
const mirrors = ref<Array<GitHubMirror>>([
{
id: "github",
name: "github"
}
]);
/**
* 获取版本
*/
const handleLoadVersions = () => {
ipcRenderer.send("github.getFrpVersions");
ipcRenderer.send("github.getFrpVersions", currMirror.value);
};
/**
* 下载
* @param version
*/
const handleDownload = useDebounceFn((version: Version) => {
ipcRenderer.send("github.download", version.id);
const handleDownload = useDebounceFn((version: FrpVersion) => {
// console.log(version, currMirror.value);
ipcRenderer.send("github.download", {
versionId: version.id,
mirror: currMirror.value
});
downloading.value.set(version.id, 0);
}, 300);
@ -49,10 +47,21 @@ const handleDownload = useDebounceFn((version: Version) => {
* 删除下载
* @param version
*/
const handleDeleteVersion = useDebounceFn((version: Version) => {
ipcRenderer.send("github.deleteVersion", {
id: version.id,
absPath: version.absPath
const handleDeleteVersion = useDebounceFn((version: FrpVersion) => {
ElMessageBox.alert(
`确认要删除 <span class="text-primary font-bold">${version.name} </span> 吗?`,
"提示",
{
showCancelButton: true,
cancelButtonText: "取消",
dangerouslyUseHTMLString: true,
confirmButtonText: "删除"
}
).then(() => {
ipcRenderer.send("github.deleteVersion", {
id: version.id,
absPath: version.absPath
});
});
}, 300);
@ -60,30 +69,30 @@ const handleInitDownloadHook = () => {
ipcRenderer.on("Download.frpVersionHook", (event, args) => {
loading.value--;
versions.value = args.map(m => {
m.published_at = moment(m.published_at).format("YYYY-MM-DD HH:mm:ss")
return m as Version;
}) as Array<Version>;
console.log(versions, 'versions')
m.published_at = moment(m.published_at).format("YYYY-MM-DD");
return m as FrpVersion;
}) as Array<FrpVersion>;
console.log(versions, "versions");
});
//
ipcRenderer.on("Download.frpVersionDownloadOnProgress", (event, args) => {
const {id, progress} = args;
const { id, progress } = args;
downloading.value.set(
id,
Number(Number(progress.percent * 100).toFixed(2))
id,
Number(Number(progress.percent * 100).toFixed(2))
);
});
ipcRenderer.on("Download.frpVersionDownloadOnCompleted", (event, args) => {
downloading.value.delete(args);
const version: Version | undefined = versions.value.find(
f => f.id === args
const version: FrpVersion | undefined = versions.value.find(
f => f.id === args
);
if (version) {
version.download_completed = true;
}
});
ipcRenderer.on("Download.deleteVersion.hook", (event, args) => {
const {err, data} = args
const { err, data } = args;
if (!err) {
loading.value++;
ElMessage({
@ -92,8 +101,24 @@ const handleInitDownloadHook = () => {
});
handleLoadVersions();
}
});
ipcRenderer.on("Download.importFrpFile.hook", (event, args) => {
const { success, data } = args;
console.log(args);
})
// if (err) {
loading.value++;
ElMessage({
type: success ? "success" : "error",
message: data
});
handleLoadVersions();
// }
});
};
const handleMirrorChange = () => {
handleLoadVersions();
};
onMounted(() => {
@ -104,83 +129,175 @@ onMounted(() => {
// });
});
const handleImportFrp = () => {
ipcRenderer.send("download.importFrpFile");
};
onUnmounted(() => {
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnProgress");
ipcRenderer.removeAllListeners("Download.frpVersionDownloadOnCompleted");
ipcRenderer.removeAllListeners("Download.frpVersionHook");
ipcRenderer.removeAllListeners("Download.deleteVersion.hook");
ipcRenderer.removeAllListeners("Download.importFrpFile.hook");
});
</script>
<template>
<div class="main">
<breadcrumb/>
<!-- <breadcrumb> -->
<breadcrumb>
<div class="flex">
<div class="h-full flex items-center justify-center mr-4">
<span class="text-sm font-bold">下载源 </span>
<el-select
class="w-40"
v-model="currMirror"
@change="handleMirrorChange"
>
<el-option
v-for="m in mirrors"
:label="m.name"
:key="m.id"
:value="m.id"
/>
</el-select>
</div>
<el-button class="mr-2" type="primary" @click="handleImportFrp">
<IconifyIconOffline icon="unarchive" />
</el-button>
</div>
<!-- <div-->
<!-- class="cursor-pointer h-[36px] w-[36px] bg-[#5f3bb0] rounded text-white flex justify-center items-center"-->
<!-- @click="handleOpenInsert"-->
<!-- >-->
<!-- <IconifyIconOffline icon="add" />-->
<!-- </div>-->
</breadcrumb>
<!-- <breadcrumb>-->
<!-- <div class="flex items-center">-->
<!-- <el-checkbox>加速下载</el-checkbox>-->
<!-- </div>-->
<!-- </breadcrumb>-->
<div class="app-container-breadcrumb pr-2" v-loading="loading > 0">
<template v-if="versions && versions.length > 0">
<div
class="w-full bg-white mb-4 rounded p-4 drop-shadow-lg flex justify-between items-center"
v-for="version in versions"
:key="version.id"
>
<div class="left">
<div class="mb-2">
<el-tag>{{ version.name }}</el-tag>
<!-- <el-tag class="ml-2">原文件名{{ version.assets[0]?.name }}</el-tag>-->
</div>
<div class="text-sm">
发布时间<span class="text-gray-00">{{
// moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
version.published_at
}}</span>
</div>
</div>
<div class="right">
<div v-if="version.download_completed">
<el-button type="text">已下载</el-button>
<!-- <span-->
<!-- class="primary-text text-sm font-bold ml-2"-->
<!-- >已下载</span>-->
<div class="w-full">
<template v-if="versions && versions.length > 0">
<el-row :gutter="20">
<!-- <el-col :span="24">-->
<!-- <div class="h2 flex justify-between !mb-[10px]">-->
<!-- <div>镜像源</div>-->
<!-- </div>-->
<!-- &lt;!&ndash; <div class="!mb-[10px]">&ndash;&gt;-->
<!-- &lt;!&ndash; <el-radio-group v-model="currMirror">&ndash;&gt;-->
<!-- &lt;!&ndash; <el-radio-button v-for="m in mirrors" :label="m" />&ndash;&gt;-->
<!-- &lt;!&ndash; </el-radio-group>&ndash;&gt;-->
<!-- &lt;!&ndash; </div>&ndash;&gt;-->
<!-- </el-col>-->
<!-- <el-col :span="24">-->
<!-- <div class="h2 flex justify-between">-->
<!-- <div>版本选择</div>-->
<!-- </div>-->
<!-- </el-col>-->
<el-col
v-for="version in versions"
:key="version.id"
:lg="6"
:md="8"
:sm="12"
:xl="6"
:xs="12"
class="mb-[20px]"
>
<div
class="w-full download-card bg-white rounded p-4 drop-shadow flex justify-between items-center"
>
<div class="left">
<div class="mb-2 flex items-center justify-center">
<span class="font-bold text-primary mr-2">{{
version.name
}}</span>
<el-tag size="small"> {{ version.size }}</el-tag>
<!-- <el-tag class="ml-2">原文件名{{ version.assets[0]?.name }}</el-tag>-->
</div>
<div class="text-[12px]">
<span class="">下载数</span>
<span class="text-primary font-bold"
>{{
// moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
version.download_count
}}
</span>
</div>
<div class="text-[12px]">
发布时间<span class="text-primary font-bold">{{
// moment(version.published_at).format("YYYY-MM-DD HH:mm:ss")
version.published_at
}}</span>
</div>
</div>
<div class="right">
<div v-if="version.download_completed">
<!-- <span class="text-[12px] text-primary font-bold mr-2"-->
<!-- >已下载</span-->
<!-- >-->
<div>
<el-button type="text" size="small">
<IconifyIconOffline class="mr-1" icon="check-box" />
已下载
</el-button>
</div>
<div>
<el-button
type="text"
size="small"
class="danger-text"
@click="handleDeleteVersion(version)"
>
<IconifyIconOffline
class="mr-1"
icon="delete-rounded"
/>
</el-button>
</div>
<el-button type="text" class="danger-text" @click="handleDeleteVersion(version)">
<Icon class="mr-1" icon="material-symbols:delete"/>
删除
</el-button>
<!-- <div>-->
<!-- <Icon class="mr-1" icon="material-symbols:download-2"/>-->
<!-- <span-->
<!-- class="danger-text text-sm font-bold ml-2"-->
<!-- >删除下载</span>-->
<!-- </div>-->
<!-- <el-button type="text"></el-button>-->
</div>
</div>
<template v-else>
<div class="w-32" v-if="downloading.has(version.id)">
<el-progress
:percentage="downloading.get(version.id)"
:text-inside="false"
/>
<template v-else>
<div class="w-32" v-if="downloading.has(version.id)">
<el-progress
:percentage="downloading.get(version.id)"
:text-inside="false"
/>
</div>
<el-button
v-else
size="small"
type="text"
@click="handleDownload(version)"
>
<IconifyIconOffline class="mr-1" icon="download" />
下载
</el-button>
</template>
</div>
</div>
<el-button v-else size="small" type="primary" @click="handleDownload(version)">
<Icon class="mr-1" icon="material-symbols:download-2"/>
下载
</el-button>
</template>
</div>
</div>
</template>
<div
</el-col>
</el-row>
</template>
<div
v-else
class="w-full h-full bg-white rounded p-2 overflow-hidden drop-shadow-xl flex justify-center items-center"
>
<el-empty description="暂无可用版本"/>
>
<el-empty description="暂无可用版本" />
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.download-card {
border-left: 5px solid #5a3daa;
}
</style>

View File

@ -1,11 +1,10 @@
<script lang="ts" setup>
import {defineComponent, onMounted, onUnmounted, ref} from "vue";
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import {ipcRenderer} from "electron";
import {Icon} from "@iconify/vue";
import {ElMessageBox} from "element-plus";
import { ipcRenderer } from "electron";
import { ElMessageBox } from "element-plus";
import router from "@/router";
import {useDebounceFn} from "@vueuse/core";
import { useDebounceFn, useIntervalFn } from "@vueuse/core";
defineComponent({
name: "Home"
@ -30,16 +29,16 @@ const handleButtonClick = useDebounceFn(() => {
}, 300);
onMounted(() => {
setInterval(() => {
useIntervalFn(() => {
ipcRenderer.invoke("frpc.running").then(data => {
running.value = data;
console.log('进程状态', data)
console.log("进程状态", data);
});
}, 300);
}, 500);
ipcRenderer.on("Home.frpc.start.error.hook", (event, args) => {
if (args) {
ElMessageBox.alert(args, "启动失败", {
ElMessageBox.alert(args, "提示", {
showCancelButton: true,
cancelButtonText: "取消",
confirmButtonText: "去设置"
@ -59,46 +58,53 @@ onUnmounted(() => {
<template>
<div class="main">
<breadcrumb/>
<breadcrumb />
<div class="app-container-breadcrumb">
<div
class="w-full h-full bg-white p-4 rounded drop-shadow-lg overflow-y-auto flex justify-center items-center"
class="w-full h-full bg-white p-4 rounded drop-shadow-lg overflow-y-auto flex justify-center items-center"
>
<div class="flex">
<div
class="w-40 h-40 border-[#5A3DAA] text-[#5A3DAA] border-4 rounded-full flex justify-center items-center text-[100px] relative"
class="w-40 h-40 border-[#5A3DAA] text-[#5A3DAA] border-4 rounded-full flex justify-center items-center text-[100px] relative"
>
<transition name="fade">
<div
v-show="running"
class="z-0 rounded-full opacity-20 left-circle bg-[#5A3DAA] w-full h-full animation-rotate-1"
v-show="running"
class="z-0 rounded-full opacity-20 left-circle bg-[#5A3DAA] w-full h-full animation-rotate-1"
/>
</transition>
<transition name="fade">
<div
v-show="running"
class="z-0 rounded-full opacity-20 right-circle top-[10px] bg-[#5A3DAA] w-full h-full animation-rotate-2"
v-show="running"
class="z-0 rounded-full opacity-20 right-circle top-[10px] bg-[#5A3DAA] w-full h-full animation-rotate-2"
/>
</transition>
<transition name="fade">
<div
v-show="running"
class="z-0 rounded-full opacity-20 top-circle bg-[#5A3DAA] w-full h-full animation-rotate-3"
v-show="running"
class="z-0 rounded-full opacity-20 top-circle bg-[#5A3DAA] w-full h-full animation-rotate-3"
/>
</transition>
<div
class="bg-white z-10 w-full h-full bg-white absolute rounded-full flex justify-center items-center"
class="bg-white z-10 w-full h-full absolute rounded-full flex justify-center items-center"
>
<Icon icon="material-symbols:rocket-launch-rounded"/>
<IconifyIconOffline icon="rocket-launch-rounded" />
</div>
</div>
<div class="flex justify-center items-center">
<div class="pl-8 h-28 w-52 flex flex-col justify-between">
<transition name="fade">
<div class="font-bold text-2xl text-center">
<Icon v-if="running" class="text-[#7EC050] inline-block relative top-1"
icon="material-symbols:check-circle-rounded"/>
<Icon v-else class="text-[#E47470] inline-block relative top-1" icon="material-symbols:error"/>
<IconifyIconOffline
v-if="running"
class="text-[#7EC050] inline-block relative top-1"
icon="check-circle-rounded"
/>
<IconifyIconOffline
v-else
class="text-[#E47470] inline-block relative top-1"
icon="error"
/>
Frpc {{ running ? "已启动" : "已断开" }}
</div>
</transition>
@ -110,11 +116,16 @@ onUnmounted(() => {
<!-- >查看日志-->
<!-- </el-button>-->
<div class="w-full justify-center text-center">
<el-link v-if="running" type="primary" @click="$router.replace({ name: 'Logger' })">查看日志</el-link>
<el-link
v-if="running"
type="primary"
@click="$router.replace({ name: 'Logger' })"
>查看日志</el-link
>
</div>
<div
class="w-full h-8 bg-[#563EA4] rounded flex justify-center items-center text-white font-bold cursor-pointer"
@click="handleButtonClick"
class="w-full h-8 bg-[#563EA4] rounded flex justify-center items-center text-white font-bold cursor-pointer"
@click="handleButtonClick"
>
{{ running ? "断 开" : "启 动" }}
</div>

View File

@ -1,7 +1,10 @@
<script lang="ts" setup>
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import { createVNode, defineComponent, onMounted, onUnmounted, ref } from "vue";
import Breadcrumb from "@/layout/compoenets/Breadcrumb.vue";
import { ipcRenderer } from "electron";
import IconifyIconOffline from "@/components/IconifyIcon/src/iconifyIconOffline";
import { useDebounce, useDebounceFn } from "@vueuse/core";
import { ElMessage } from "element-plus";
defineComponent({
name: "Logger"
@ -29,7 +32,12 @@ const handleLog2Html = (logContent: string) => {
return logs.reverse().join("");
};
const refreshStatus = ref(false);
const logLoading = ref(true);
onMounted(() => {
console.log('logger mounted')
ipcRenderer.send("logger.getLog");
ipcRenderer.on("Logger.getLog.hook", (event, args) => {
// console.log("", args, args.indexOf("\n"));
@ -38,29 +46,76 @@ onMounted(() => {
if (args) {
loggerContent.value = handleLog2Html(args);
}
ipcRenderer.send("logger.update");
logLoading.value = false;
if (refreshStatus.value) {
//
ElMessage({
type: "success",
message: "刷新成功"
});
} else {
ipcRenderer.send("logger.update");
}
});
ipcRenderer.on("Logger.update.hook", (event, args) => {
console.log("logger update hook", 1);
if (args) {
loggerContent.value = handleLog2Html(args);
}
});
ipcRenderer.on("Logger.openLog.hook", (event, args) => {
if (args) {
ElMessage({
type: "success",
message: "打开日志成功"
});
}
});
});
const openLocalLog = useDebounceFn(() => {
ipcRenderer.send("logger.openLog");
}, 1000);
const refreshLog = useDebounceFn(() => {
// ElMessage({
// type: "warning",
// icon: "<IconifyIconOffline icon=\"file-open-rounded\" />",
// message: "..."
// });
refreshStatus.value = true;
logLoading.value = true;
ipcRenderer.send("logger.getLog");
}, 300);
onUnmounted(() => {
console.log('logger unmounted')
ipcRenderer.removeAllListeners("Logger.getLog.hook");
ipcRenderer.removeAllListeners("Logger.openLog.hook");
});
</script>
<template>
<div class="main">
<breadcrumb />
<div class="app-container-breadcrumb">
<breadcrumb>
<el-button plain type="primary" @click="refreshLog">
<IconifyIconOffline icon="refresh-rounded" />
</el-button>
<el-button plain type="primary" @click="openLocalLog">
<IconifyIconOffline icon="file-open-rounded" />
</el-button>
</breadcrumb>
<div class="app-container-breadcrumb" v-loading="logLoading">
<div
class="w-full h-full p-4 bg-[#2B2B2B] rounded drop-shadow-lg overflow-y-auto"
class="w-full h-full p-2 bg-[#2B2B2B] rounded drop-shadow-lg overflow-y-auto"
v-html="loggerContent"
></div>
</div>
</div>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
::-webkit-scrollbar-track-piece {
background-color: transparent;
}
</style>

View File

@ -0,0 +1,14 @@
[
{
"value": "127.0.0.1"
},
{
"value": "192.168.1.1"
},
{
"value": "192.168.0.1"
},
{
"value": "192.168.5.1"
}
]

File diff suppressed because it is too large Load Diff

107
types/global.d.ts vendored Normal file
View File

@ -0,0 +1,107 @@
declare module 'element-plus/dist/locale/zh-cn.mjs' {
const zhLocale: any;
export default zhLocale;
}
declare module 'element-plus/dist/locale/en.mjs' {
const enLocale: any;
export default enLocale;
}
declare global {
/**
*
*/
type Proxy = {
_id: string;
name: string;
type: string;
localIp: string;
localPort: any;
remotePort: string;
customDomains: string[];
stcpModel: string;
serverName: string;
secretKey: string;
bindAddr: string;
bindPort: number;
status: boolean;
subdomain: string;
basicAuth: boolean;
httpUser: string;
httpPassword: string;
fallbackTo: string;
fallbackTimeoutMs: number;
https2http: boolean;
https2httpCaFile: string;
https2httpKeyFile: string;
keepTunnelOpen: boolean;
};
/**
*
*/
type LocalPort = {
protocol: string;
ip: string;
port: number;
}
/**
*
*/
type FrpVersion = {
id: number;
name: string;
published_at: string;
download_completed: boolean;
size: string;
download_count: number;
absPath: string;
assets: Asset[]
};
/**
*
*/
type FrpConfig = {
currentVersion: number;
serverAddr: string;
serverPort: number;
authMethod: string;
authToken: string;
logLevel: string;
logMaxDays: number;
tlsConfigEnable: boolean;
tlsConfigCertFile: string;
tlsConfigKeyFile: string;
tlsConfigTrustedCaFile: string;
tlsConfigServerName: string;
proxyConfigEnable: boolean;
proxyConfigProxyUrl: string;
systemSelfStart: boolean;
systemStartupConnect: boolean;
systemSilentStartup: boolean;
user: string;
metaToken: string;
transportHeartbeatInterval: number;
transportHeartbeatTimeout: number;
webEnable: boolean;
webPort: number;
transportProtocol: string;
transportDialServerTimeout: number;
transportDialServerKeepalive: number;
transportPoolCount: number;
transportTcpMux: boolean;
transportTcpMuxKeepaliveInterval: number;
};
type GitHubMirror = {
id: string;
name: string;
}
}
export {};