Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
64828404fb | ||
![]() |
7681dc6350 | ||
![]() |
08c2a2cb5a | ||
![]() |
f0b420fe2e | ||
![]() |
3bfb96b091 | ||
![]() |
29ddfe41f5 | ||
![]() |
5d124c7efb | ||
![]() |
40de47640c | ||
![]() |
ea02338102 | ||
![]() |
816dcd4481 | ||
![]() |
ced3bda8bb | ||
![]() |
59be2d2173 | ||
![]() |
b1dbd46db4 | ||
![]() |
f25dec4c87 | ||
![]() |
27c1d42596 | ||
![]() |
58f625baf4 | ||
![]() |
4bad940aff | ||
![]() |
d4f743663b | ||
![]() |
4c0a6de1e9 | ||
![]() |
c359714bc5 | ||
![]() |
ffb42bd131 | ||
![]() |
9ab29259b1 | ||
![]() |
9ad8773b4d | ||
![]() |
a34a1886de | ||
![]() |
f4f18afb7a | ||
![]() |
fcb01e83b2 | ||
![]() |
cfba180603 | ||
![]() |
aad6fd78d5 | ||
![]() |
19fde43f8b | ||
![]() |
9510a4cb67 | ||
![]() |
337a3b6831 | ||
![]() |
91b97df99a | ||
![]() |
a1910be29c | ||
![]() |
3157b935e0 | ||
![]() |
eaa588698a | ||
![]() |
5e436ccaf1 | ||
![]() |
0b4824975a | ||
![]() |
ae4b346084 | ||
![]() |
d95f7034f8 | ||
![]() |
3ed09818ef | ||
![]() |
5f34a64fde | ||
![]() |
96b161f431 | ||
![]() |
01299b2fa2 | ||
![]() |
827b94f75f | ||
![]() |
acfcdfbb16 | ||
![]() |
453f41ff19 | ||
![]() |
e2a01325f4 | ||
![]() |
2581caa9e3 | ||
![]() |
8e0c152fd8 | ||
![]() |
3751a52cd6 | ||
![]() |
32555d45b1 | ||
![]() |
ab2323f421 | ||
![]() |
139191065b | ||
![]() |
d70a7488fc | ||
![]() |
6c4a052bf6 | ||
![]() |
5ac69de59a | ||
![]() |
90233f006a | ||
![]() |
40e4623413 | ||
![]() |
dc511fabc4 | ||
![]() |
30b799f710 | ||
![]() |
b82215ca16 | ||
![]() |
654f6c7fd6 | ||
![]() |
919370c56c | ||
![]() |
e7f4572768 | ||
![]() |
dba788a1dd | ||
![]() |
6c7c568a48 | ||
![]() |
88c687b66d | ||
![]() |
4a58fbd955 | ||
![]() |
d694d9537d | ||
![]() |
c5883bb25b | ||
![]() |
f04e08ee3e | ||
![]() |
6d2f1c7b64 | ||
![]() |
fe697059b4 | ||
![]() |
2d894844b0 | ||
![]() |
ca885af394 | ||
![]() |
4d9c5bf003 | ||
![]() |
2ccddbe4b6 | ||
![]() |
843ab85215 | ||
![]() |
ed1f3dc378 | ||
![]() |
acbf075280 | ||
![]() |
59ef02033a | ||
![]() |
d57e6f41ed | ||
![]() |
0c1d6919ec | ||
![]() |
f788990705 | ||
![]() |
139fed40d8 | ||
![]() |
6d5f985dfe | ||
![]() |
686acdd7c5 | ||
![]() |
0fe365e3d9 | ||
![]() |
e0dcd66453 | ||
![]() |
c3f914e7e5 | ||
![]() |
d45a9b84bb | ||
![]() |
efb5e94d4e | ||
![]() |
e378421c72 | ||
![]() |
9d57006d16 | ||
![]() |
e4cd09d1dc | ||
![]() |
60fbc354d0 | ||
![]() |
dbfceb9550 | ||
![]() |
e0b3f6e7a7 | ||
![]() |
96247a29d4 | ||
![]() |
e53ab2ff44 | ||
![]() |
714043c9ec | ||
![]() |
550c552643 | ||
![]() |
707b9c4b9e | ||
![]() |
2592e201ed | ||
![]() |
1ea549323d | ||
![]() |
3ef47b200c | ||
![]() |
0d21a6a2a2 | ||
![]() |
5face0839a | ||
![]() |
eae089b0e9 | ||
![]() |
4f80fc9e27 | ||
![]() |
ad3167627b | ||
![]() |
44088af063 | ||
![]() |
09c591b24b | ||
![]() |
7f0ed6142f | ||
![]() |
a5279253a9 | ||
![]() |
3f49841ab2 | ||
![]() |
9df5cec23a | ||
![]() |
9fed5fc844 | ||
![]() |
6e028d377a | ||
![]() |
1f246e3966 | ||
![]() |
357cc473c7 | ||
![]() |
bd74b068e3 | ||
![]() |
0b7307ac3a | ||
![]() |
c7aa312463 | ||
![]() |
fb2a8ecd51 | ||
![]() |
ed66824931 | ||
![]() |
9abda2f6d1 | ||
![]() |
22bb48c274 | ||
![]() |
1d9ac090f3 | ||
![]() |
3961475421 | ||
![]() |
989f9b8a37 | ||
![]() |
3c9db3dc9f | ||
![]() |
c8e99a1dc4 | ||
![]() |
7101f1c7a1 | ||
![]() |
179da0bbf6 | ||
![]() |
b00392b9f5 | ||
![]() |
71c677a193 | ||
![]() |
e42f36ee72 | ||
![]() |
32ee5aa78f | ||
![]() |
d2ceb6e35e | ||
![]() |
19c6e208a6 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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/`
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
28
.github/workflows/webpack.yml
vendored
Normal 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
23
.vscode/launch.json
vendored
Normal 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}"
|
||||
}
|
||||
]
|
||||
}
|
93
README.md
93
README.md
@ -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="Featured|HelloGitHub" 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 @@
|
||||
|
||||

|
||||
|
||||
## 捐赠
|
||||
|
||||
👉👉👉[点击去捐赠](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
|
||||
[](https://starchart.cc/luckjiawei/frpc-desktop)
|
||||
## Star History
|
||||
|
||||
[](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
11
baidu_urls.txt
Normal 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
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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: `导入失败,无法识别文件`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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')
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -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
49
electron/json/extract.py
Normal 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()
|
46968
electron/json/frp-releases.json
Normal file
46968
electron/json/frp-releases.json
Normal file
File diff suppressed because it is too large
Load Diff
440
electron/json/frp_all_sha256_checksums.json
Normal file
440
electron/json/frp_all_sha256_checksums.json
Normal 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"
|
||||
}
|
@ -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}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
12
electron/utils/desensitize.ts
Normal file
12
electron/utils/desensitize.ts
Normal 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
19
electron/utils/file.ts
Normal 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
30
electron/utils/log.ts
Normal 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}`);
|
||||
};
|
20
package.json
20
package.json
@ -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
BIN
screenshots/gratuity.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 196 KiB |
BIN
screenshots/mp_qr.jpg
Normal file
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
BIN
screenshots/wechat-qr.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 272 KiB |
9
src/components/IconifyIcon/index.ts
Normal file
9
src/components/IconifyIcon/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import iconifyIconOffline from "./src/iconifyIconOffline";
|
||||
import iconifyIconOnline from "./src/iconifyIconOnline";
|
||||
|
||||
/** 本地图标组件 */
|
||||
const IconifyIconOffline = iconifyIconOffline;
|
||||
/** 在线图标组件 */
|
||||
const IconifyIconOnline = iconifyIconOnline;
|
||||
|
||||
export {IconifyIconOffline, IconifyIconOnline};
|
30
src/components/IconifyIcon/src/iconifyIconOffline.ts
Normal file
30
src/components/IconifyIcon/src/iconifyIconOffline.ts
Normal 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: () => []
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
30
src/components/IconifyIcon/src/iconifyIconOnline.ts
Normal file
30
src/components/IconifyIcon/src/iconifyIconOnline.ts
Normal 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: () => []
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
79
src/components/IconifyIcon/src/offlineIcon.ts
Normal file
79
src/components/IconifyIcon/src/offlineIcon.ts
Normal 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
21
src/intro/index.ts
Normal 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;
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
24
src/main.ts
24
src/main.ts
@ -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"}, "*");
|
||||
});
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
9
src/types/global.d.ts
vendored
9
src/types/global.d.ts
vendored
@ -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;
|
||||
}
|
@ -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
@ -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>-->
|
||||
<!-- <!– <div class="!mb-[10px]">–>-->
|
||||
<!-- <!– <el-radio-group v-model="currMirror">–>-->
|
||||
<!-- <!– <el-radio-button v-for="m in mirrors" :label="m" />–>-->
|
||||
<!-- <!– </el-radio-group>–>-->
|
||||
<!-- <!– </div>–>-->
|
||||
<!-- </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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
14
src/views/proxy/commonIp.json
Normal file
14
src/views/proxy/commonIp.json
Normal 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
107
types/global.d.ts
vendored
Normal 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 {};
|
Loading…
Reference in New Issue
Block a user