@qpjoy/electron-tunnel 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -6
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/admin/AdminServer.d.ts +8 -1
- package/dist/admin/AdminServer.js +45 -160
- package/dist/admin/admin-ui.d.ts +1 -0
- package/dist/admin/admin-ui.js +492 -0
- package/dist/createElectronTunnel.js +3 -1
- package/dist/ipc/registerTunnelIpc.js +44 -16
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -26,6 +26,20 @@ app.on('before-quit', () => {
|
|
|
26
26
|
});
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
The package starts a browser admin backend by default. Keep mode switching,
|
|
30
|
+
subscription management, local ports, start/stop, and TUN install/uninstall in
|
|
31
|
+
that admin UI so the host Electron app does not need tunnel-specific screens:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
http://127.0.0.1:23456
|
|
35
|
+
admin/admin
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
When the admin changes runtime settings, the SDK reapplies the Electron session
|
|
39
|
+
proxy automatically. If the admin switches to virtual NIC mode and TUN is
|
|
40
|
+
installed, the package can request the required system privilege from the host
|
|
41
|
+
app process.
|
|
42
|
+
|
|
29
43
|
The package also installs a CLI:
|
|
30
44
|
|
|
31
45
|
```bash
|
|
@@ -45,9 +59,5 @@ extraResources: [
|
|
|
45
59
|
]
|
|
46
60
|
```
|
|
47
61
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```text
|
|
51
|
-
http://127.0.0.1:23456
|
|
52
|
-
admin/admin
|
|
53
|
-
```
|
|
62
|
+
This package redistributes third-party tunnel engine binaries. See
|
|
63
|
+
`THIRD_PARTY_NOTICES.md` before publishing apps that include those resources.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Third Party Notices
|
|
2
|
+
|
|
3
|
+
This npm package redistributes third-party software as bundled tunnel engine
|
|
4
|
+
resources. The resources are installed into the consuming Electron app at
|
|
5
|
+
runtime; end users do not need to install or configure the engine separately.
|
|
6
|
+
|
|
7
|
+
## MetaCubeX/mihomo
|
|
8
|
+
|
|
9
|
+
- Project: MetaCubeX/mihomo
|
|
10
|
+
- Source: https://github.com/MetaCubeX/mihomo
|
|
11
|
+
- Release used for bundled binaries: v1.19.24
|
|
12
|
+
- License: GNU General Public License version 3
|
|
13
|
+
- License text: https://raw.githubusercontent.com/MetaCubeX/mihomo/Meta/LICENSE
|
|
14
|
+
- Corresponding source: https://github.com/MetaCubeX/mihomo/tree/v1.19.24
|
|
15
|
+
|
|
16
|
+
Bundled binary resources:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
resources/engine/darwin-arm64/mihomo.gz
|
|
20
|
+
resources/engine/darwin-x64/mihomo.gz
|
|
21
|
+
resources/engine/linux-x64/mihomo.gz
|
|
22
|
+
resources/engine/linux-arm64/mihomo.gz
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The bundled binaries are unmodified release artifacts downloaded from the
|
|
26
|
+
upstream project. If you redistribute an Electron application that includes
|
|
27
|
+
these resources, preserve this notice and comply with the upstream GPL-3.0
|
|
28
|
+
license terms, including source-code availability requirements for the bundled
|
|
29
|
+
engine.
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { MihomoManager } from '../mihomo/MihomoManager';
|
|
2
|
+
interface AdminServerOptions {
|
|
3
|
+
afterSettingsChange?: () => Promise<void> | void;
|
|
4
|
+
}
|
|
2
5
|
export declare class AdminServer {
|
|
3
6
|
private readonly manager;
|
|
7
|
+
private readonly options;
|
|
4
8
|
private server;
|
|
5
|
-
constructor(manager: MihomoManager);
|
|
9
|
+
constructor(manager: MihomoManager, options?: AdminServerOptions);
|
|
6
10
|
start(): void;
|
|
7
11
|
stop(): void;
|
|
8
12
|
private handle;
|
|
13
|
+
private applyRuntimeConfigChange;
|
|
14
|
+
private notifySettingsChange;
|
|
9
15
|
private route;
|
|
10
16
|
}
|
|
17
|
+
export {};
|
|
@@ -4,7 +4,7 @@ exports.AdminServer = void 0;
|
|
|
4
4
|
const http_1 = require("http");
|
|
5
5
|
const url_1 = require("url");
|
|
6
6
|
const security_1 = require("../security");
|
|
7
|
-
const
|
|
7
|
+
const admin_ui_1 = require("./admin-ui");
|
|
8
8
|
const sessions = new Set();
|
|
9
9
|
function sendJson(res, status, data) {
|
|
10
10
|
const body = JSON.stringify(data);
|
|
@@ -21,151 +21,6 @@ function sendText(res, status, data, contentType = 'text/plain; charset=utf-8')
|
|
|
21
21
|
});
|
|
22
22
|
res.end(data);
|
|
23
23
|
}
|
|
24
|
-
function adminHtml() {
|
|
25
|
-
const presets = Object.keys(defaults_1.DOMAIN_PRESETS)
|
|
26
|
-
.map((id) => `<button data-preset="${id}">${id}</button>`)
|
|
27
|
-
.join('');
|
|
28
|
-
return `<!doctype html>
|
|
29
|
-
<html lang="zh-CN">
|
|
30
|
-
<head>
|
|
31
|
-
<meta charset="utf-8">
|
|
32
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
33
|
-
<title>QPJoy Tunnel Admin</title>
|
|
34
|
-
<style>
|
|
35
|
-
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;margin:0;background:#f4f6f8;color:#121417}
|
|
36
|
-
header{height:64px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;background:#fff;border-bottom:1px solid #dde1e6}
|
|
37
|
-
main{max-width:1160px;margin:0 auto;padding:24px;display:grid;gap:18px}
|
|
38
|
-
section{background:#fff;border:1px solid #dde1e6;border-radius:8px;padding:18px}
|
|
39
|
-
input,select,button{height:36px;border:1px solid #c9d1d9;border-radius:6px;padding:0 10px;font-size:14px}
|
|
40
|
-
button{background:#1264d8;color:#fff;border-color:#1264d8;cursor:pointer}
|
|
41
|
-
button.secondary{background:#fff;color:#202833}
|
|
42
|
-
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
|
|
43
|
-
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
|
|
44
|
-
.muted{color:#6b7280}
|
|
45
|
-
.card{border:1px solid #e3e7ec;border-radius:8px;padding:14px}
|
|
46
|
-
pre{white-space:pre-wrap;max-height:280px;overflow:auto;background:#101418;color:#dbeafe;border-radius:8px;padding:14px}
|
|
47
|
-
#login{max-width:360px;margin:12vh auto}
|
|
48
|
-
#app{display:none}
|
|
49
|
-
</style>
|
|
50
|
-
</head>
|
|
51
|
-
<body>
|
|
52
|
-
<section id="login">
|
|
53
|
-
<h2>QPJoy Tunnel</h2>
|
|
54
|
-
<div class="row"><input id="user" placeholder="admin" value="admin"><input id="pass" type="password" placeholder="password" value="admin"><button id="loginBtn">登录</button></div>
|
|
55
|
-
</section>
|
|
56
|
-
<div id="app">
|
|
57
|
-
<header><strong>QPJoy Tunnel Admin</strong><span id="status" class="muted"></span></header>
|
|
58
|
-
<main>
|
|
59
|
-
<section>
|
|
60
|
-
<h3>模式</h3>
|
|
61
|
-
<div class="row">
|
|
62
|
-
<select id="mode">
|
|
63
|
-
<option value="system-tun">虚拟网卡</option>
|
|
64
|
-
<option value="app-global">全局模式</option>
|
|
65
|
-
<option value="app-rule">App 模式</option>
|
|
66
|
-
</select>
|
|
67
|
-
<button id="saveMode">切换</button>
|
|
68
|
-
<button id="installTun" class="secondary">安装 TUN</button>
|
|
69
|
-
<button id="uninstallTun" class="secondary">卸载 TUN</button>
|
|
70
|
-
<button id="start">启动</button>
|
|
71
|
-
<button id="stop" class="secondary">停止</button>
|
|
72
|
-
</div>
|
|
73
|
-
</section>
|
|
74
|
-
<section>
|
|
75
|
-
<h3>高级设置</h3>
|
|
76
|
-
<div class="row">
|
|
77
|
-
<input id="corePath" placeholder="自动使用内置隧道引擎" style="flex:1;min-width:320px">
|
|
78
|
-
<button id="saveCorePath">保存引擎路径</button>
|
|
79
|
-
</div>
|
|
80
|
-
</section>
|
|
81
|
-
<section>
|
|
82
|
-
<h3>本地端口</h3>
|
|
83
|
-
<div class="row">
|
|
84
|
-
<input id="mixedPort" placeholder="23458">
|
|
85
|
-
<input id="dnsPort" placeholder="23459">
|
|
86
|
-
<button id="savePorts">保存端口</button>
|
|
87
|
-
</div>
|
|
88
|
-
</section>
|
|
89
|
-
<section>
|
|
90
|
-
<h3>订阅</h3>
|
|
91
|
-
<div class="row">
|
|
92
|
-
<input id="subName" placeholder="名称">
|
|
93
|
-
<input id="subUrl" placeholder="订阅文件链接" style="flex:1;min-width:320px">
|
|
94
|
-
<input id="subUser" placeholder="用户">
|
|
95
|
-
<input id="subPass" placeholder="密码" type="password">
|
|
96
|
-
<button id="addSub">新建</button>
|
|
97
|
-
<button id="updateSub" class="secondary">更新当前</button>
|
|
98
|
-
</div>
|
|
99
|
-
<div id="subs" class="grid"></div>
|
|
100
|
-
</section>
|
|
101
|
-
<section>
|
|
102
|
-
<h3>白名单 / 黑名单</h3>
|
|
103
|
-
<div class="row">
|
|
104
|
-
${presets}
|
|
105
|
-
<input id="ruleDomain" placeholder="example.com">
|
|
106
|
-
<select id="ruleKind"><option value="allow">白名单</option><option value="block">黑名单</option></select>
|
|
107
|
-
<button id="addRule">添加</button>
|
|
108
|
-
</div>
|
|
109
|
-
<div id="rules" class="grid"></div>
|
|
110
|
-
</section>
|
|
111
|
-
<section>
|
|
112
|
-
<h3>日志</h3>
|
|
113
|
-
<pre id="events"></pre>
|
|
114
|
-
</section>
|
|
115
|
-
</main>
|
|
116
|
-
</div>
|
|
117
|
-
<script>
|
|
118
|
-
let token = '';
|
|
119
|
-
async function api(path, options = {}) {
|
|
120
|
-
const res = await fetch(path, {
|
|
121
|
-
...options,
|
|
122
|
-
headers: { 'content-type': 'application/json', authorization: token ? 'Bearer ' + token : '', ...(options.headers || {}) }
|
|
123
|
-
});
|
|
124
|
-
if (!res.ok) throw new Error(await res.text());
|
|
125
|
-
return res.json();
|
|
126
|
-
}
|
|
127
|
-
async function refresh() {
|
|
128
|
-
const data = await api('/api/snapshot');
|
|
129
|
-
document.querySelector('#status').textContent = data.status.running ? '运行中' : '已停止';
|
|
130
|
-
document.querySelector('#mode').value = data.status.mode;
|
|
131
|
-
document.querySelector('#corePath').value = data.status.corePath || '';
|
|
132
|
-
document.querySelector('#mixedPort').value = data.status.ports.mixed;
|
|
133
|
-
document.querySelector('#dnsPort').value = data.status.ports.dns;
|
|
134
|
-
document.querySelector('#subs').innerHTML = data.subscriptions.map(s => '<div class="card"><strong>'+s.name+'</strong><p class="muted">'+s.url+'</p><p>'+(s.active?'当前':'')+' '+(s.lastUpdatedAt||'未更新')+'</p><button data-active="'+s.id+'">启用</button> <button class="secondary" data-refresh="'+s.id+'">刷新</button> <button class="secondary" data-sub-remove="'+s.id+'">删除</button></div>').join('');
|
|
135
|
-
document.querySelector('#rules').innerHTML = data.rules.map(r => '<div class="card"><strong>'+r.kind+'</strong> '+r.domain+'<p class="muted">'+r.source+'</p><button class="secondary" data-rule-remove="'+r.id+'">删除</button></div>').join('');
|
|
136
|
-
document.querySelector('#events').textContent = data.events.map(e => '['+e.level+'] '+e.createdAt+' '+e.message).join('\\n');
|
|
137
|
-
}
|
|
138
|
-
document.querySelector('#loginBtn').onclick = async () => {
|
|
139
|
-
const result = await api('/api/login', { method:'POST', body: JSON.stringify({ username:user.value, password:pass.value }) });
|
|
140
|
-
token = result.token;
|
|
141
|
-
login.style.display = 'none';
|
|
142
|
-
app.style.display = 'block';
|
|
143
|
-
refresh();
|
|
144
|
-
};
|
|
145
|
-
document.querySelector('#saveMode').onclick = async () => { await api('/api/mode', { method:'POST', body: JSON.stringify({ mode: mode.value }) }); refresh(); };
|
|
146
|
-
document.querySelector('#saveCorePath').onclick = async () => { await api('/api/core/path', { method:'POST', body: JSON.stringify({ corePath: corePath.value }) }); refresh(); };
|
|
147
|
-
document.querySelector('#savePorts').onclick = async () => { await api('/api/ports', { method:'POST', body: JSON.stringify({ mixed: Number(mixedPort.value), dns: Number(dnsPort.value) }) }); refresh(); };
|
|
148
|
-
document.querySelector('#installTun').onclick = async () => { await api('/api/tun/install', { method:'POST' }); refresh(); };
|
|
149
|
-
document.querySelector('#uninstallTun').onclick = async () => { await api('/api/tun/uninstall', { method:'POST' }); refresh(); };
|
|
150
|
-
document.querySelector('#start').onclick = async () => { await api('/api/core/start', { method:'POST' }); refresh(); };
|
|
151
|
-
document.querySelector('#stop').onclick = async () => { await api('/api/core/stop', { method:'POST' }); refresh(); };
|
|
152
|
-
document.querySelector('#updateSub').onclick = async () => { await api('/api/subscriptions/active/update', { method:'POST' }); refresh(); };
|
|
153
|
-
document.querySelector('#addSub').onclick = async () => { await api('/api/subscriptions', { method:'POST', body: JSON.stringify({ name: subName.value, url: subUrl.value, username: subUser.value, password: subPass.value }) }); refresh(); };
|
|
154
|
-
document.querySelector('#addRule').onclick = async () => { await api('/api/rules', { method:'POST', body: JSON.stringify({ kind: ruleKind.value, domain: ruleDomain.value }) }); refresh(); };
|
|
155
|
-
document.body.onclick = async (event) => {
|
|
156
|
-
const target = event.target;
|
|
157
|
-
if (!(target instanceof HTMLElement)) return;
|
|
158
|
-
if (target.dataset.preset) await api('/api/presets/'+target.dataset.preset, { method:'POST' });
|
|
159
|
-
if (target.dataset.active) await api('/api/subscriptions/'+target.dataset.active+'/active', { method:'POST' });
|
|
160
|
-
if (target.dataset.refresh) await api('/api/subscriptions/'+target.dataset.refresh+'/update', { method:'POST' });
|
|
161
|
-
if (target.dataset.subRemove) await api('/api/subscriptions/'+target.dataset.subRemove, { method:'DELETE' });
|
|
162
|
-
if (target.dataset.ruleRemove) await api('/api/rules/'+target.dataset.ruleRemove, { method:'DELETE' });
|
|
163
|
-
if (target.dataset.preset || target.dataset.active || target.dataset.refresh || target.dataset.subRemove || target.dataset.ruleRemove) refresh();
|
|
164
|
-
};
|
|
165
|
-
</script>
|
|
166
|
-
</body>
|
|
167
|
-
</html>`;
|
|
168
|
-
}
|
|
169
24
|
async function readBody(req) {
|
|
170
25
|
const chunks = [];
|
|
171
26
|
for await (const chunk of req) {
|
|
@@ -186,9 +41,11 @@ function isAuthed(req) {
|
|
|
186
41
|
}
|
|
187
42
|
class AdminServer {
|
|
188
43
|
manager;
|
|
44
|
+
options;
|
|
189
45
|
server = null;
|
|
190
|
-
constructor(manager) {
|
|
46
|
+
constructor(manager, options = {}) {
|
|
191
47
|
this.manager = manager;
|
|
48
|
+
this.options = options;
|
|
192
49
|
}
|
|
193
50
|
start() {
|
|
194
51
|
if (this.server) {
|
|
@@ -213,7 +70,7 @@ class AdminServer {
|
|
|
213
70
|
const method = req.method ?? 'GET';
|
|
214
71
|
const pathname = (0, url_1.parse)(req.url ?? '/', true).pathname ?? '/';
|
|
215
72
|
if (method === 'GET' && pathname === '/') {
|
|
216
|
-
sendText(res, 200, adminHtml(), 'text/html; charset=utf-8');
|
|
73
|
+
sendText(res, 200, (0, admin_ui_1.adminHtml)(), 'text/html; charset=utf-8');
|
|
217
74
|
return;
|
|
218
75
|
}
|
|
219
76
|
const body = await readBody(req);
|
|
@@ -240,6 +97,13 @@ class AdminServer {
|
|
|
240
97
|
}
|
|
241
98
|
await route(req, res, body);
|
|
242
99
|
}
|
|
100
|
+
async applyRuntimeConfigChange() {
|
|
101
|
+
await this.manager.applyRuntimeConfigChange();
|
|
102
|
+
await this.options.afterSettingsChange?.();
|
|
103
|
+
}
|
|
104
|
+
async notifySettingsChange() {
|
|
105
|
+
await this.options.afterSettingsChange?.();
|
|
106
|
+
}
|
|
243
107
|
route(method, pathname) {
|
|
244
108
|
if (method === 'GET' && pathname === '/api/snapshot') {
|
|
245
109
|
return async (_req, res) => sendJson(res, 200, await this.manager.snapshot());
|
|
@@ -249,7 +113,10 @@ class AdminServer {
|
|
|
249
113
|
const { mode } = body;
|
|
250
114
|
const changedMode = this.manager.setMode(mode);
|
|
251
115
|
if (changedMode) {
|
|
252
|
-
await this.
|
|
116
|
+
await this.applyRuntimeConfigChange();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
await this.notifySettingsChange();
|
|
253
120
|
}
|
|
254
121
|
sendJson(res, 200, this.manager.status());
|
|
255
122
|
};
|
|
@@ -258,51 +125,69 @@ class AdminServer {
|
|
|
258
125
|
return async (_req, res, body) => {
|
|
259
126
|
const { mixed, dns } = body;
|
|
260
127
|
await this.manager.setLocalPorts({ mixed, dns });
|
|
128
|
+
await this.notifySettingsChange();
|
|
261
129
|
sendJson(res, 200, this.manager.status());
|
|
262
130
|
};
|
|
263
131
|
}
|
|
264
132
|
if (method === 'POST' && pathname === '/api/tun/install') {
|
|
265
133
|
return async (_req, res) => {
|
|
266
134
|
this.manager.installTunFeature();
|
|
267
|
-
await this.
|
|
135
|
+
await this.applyRuntimeConfigChange();
|
|
268
136
|
sendJson(res, 200, this.manager.status());
|
|
269
137
|
};
|
|
270
138
|
}
|
|
271
139
|
if (method === 'POST' && pathname === '/api/tun/uninstall') {
|
|
272
140
|
return async (_req, res) => {
|
|
273
141
|
this.manager.uninstallTunFeature();
|
|
274
|
-
await this.
|
|
142
|
+
await this.applyRuntimeConfigChange();
|
|
275
143
|
sendJson(res, 200, this.manager.status());
|
|
276
144
|
};
|
|
277
145
|
}
|
|
278
146
|
if (method === 'POST' && pathname === '/api/core/start') {
|
|
279
147
|
return async (_req, res) => {
|
|
280
148
|
await this.manager.start();
|
|
149
|
+
await this.notifySettingsChange();
|
|
281
150
|
sendJson(res, 200, this.manager.status());
|
|
282
151
|
};
|
|
283
152
|
}
|
|
284
153
|
if (method === 'POST' && pathname === '/api/core/stop') {
|
|
285
154
|
return async (_req, res) => {
|
|
286
155
|
await this.manager.stop();
|
|
156
|
+
await this.notifySettingsChange();
|
|
157
|
+
sendJson(res, 200, this.manager.status());
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (method === 'POST' && pathname === '/api/core/restart') {
|
|
161
|
+
return async (_req, res) => {
|
|
162
|
+
await this.manager.restart();
|
|
163
|
+
await this.notifySettingsChange();
|
|
287
164
|
sendJson(res, 200, this.manager.status());
|
|
288
165
|
};
|
|
289
166
|
}
|
|
290
167
|
if (method === 'POST' && pathname === '/api/core/path') {
|
|
291
|
-
return (_req, res, body) => {
|
|
168
|
+
return async (_req, res, body) => {
|
|
292
169
|
const { corePath } = body;
|
|
293
170
|
this.manager.setCorePath(corePath);
|
|
171
|
+
await this.notifySettingsChange();
|
|
294
172
|
sendJson(res, 200, this.manager.status());
|
|
295
173
|
};
|
|
296
174
|
}
|
|
297
175
|
if (method === 'POST' && pathname === '/api/subscriptions') {
|
|
298
176
|
return async (_req, res, body) => {
|
|
299
|
-
|
|
177
|
+
const subscription = await this.manager.createSubscription(body);
|
|
178
|
+
if (subscription.active) {
|
|
179
|
+
await this.applyRuntimeConfigChange();
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
await this.notifySettingsChange();
|
|
183
|
+
}
|
|
184
|
+
sendJson(res, 200, subscription);
|
|
300
185
|
};
|
|
301
186
|
}
|
|
302
187
|
if (method === 'POST' && pathname === '/api/subscriptions/active/update') {
|
|
303
188
|
return async (_req, res) => {
|
|
304
189
|
const subscription = await this.manager.updateActiveSubscription();
|
|
305
|
-
await this.
|
|
190
|
+
await this.applyRuntimeConfigChange();
|
|
306
191
|
sendJson(res, 200, subscription);
|
|
307
192
|
};
|
|
308
193
|
}
|
|
@@ -310,7 +195,7 @@ class AdminServer {
|
|
|
310
195
|
if (method === 'POST' && activeMatch) {
|
|
311
196
|
return async (_req, res) => {
|
|
312
197
|
const subscription = this.manager.setActiveSubscription(Number(activeMatch[1]));
|
|
313
|
-
await this.
|
|
198
|
+
await this.applyRuntimeConfigChange();
|
|
314
199
|
sendJson(res, 200, subscription);
|
|
315
200
|
};
|
|
316
201
|
}
|
|
@@ -319,7 +204,7 @@ class AdminServer {
|
|
|
319
204
|
return async (_req, res) => {
|
|
320
205
|
const subscription = await this.manager.updateSubscription(Number(updateMatch[1]));
|
|
321
206
|
if (subscription.active) {
|
|
322
|
-
await this.
|
|
207
|
+
await this.applyRuntimeConfigChange();
|
|
323
208
|
}
|
|
324
209
|
sendJson(res, 200, subscription);
|
|
325
210
|
};
|
|
@@ -328,7 +213,7 @@ class AdminServer {
|
|
|
328
213
|
if (method === 'DELETE' && deleteSubscriptionMatch) {
|
|
329
214
|
return async (_req, res) => {
|
|
330
215
|
this.manager.deleteSubscription(Number(deleteSubscriptionMatch[1]));
|
|
331
|
-
await this.
|
|
216
|
+
await this.applyRuntimeConfigChange();
|
|
332
217
|
sendJson(res, 200, { ok: true });
|
|
333
218
|
};
|
|
334
219
|
}
|
|
@@ -336,7 +221,7 @@ class AdminServer {
|
|
|
336
221
|
return async (_req, res, body) => {
|
|
337
222
|
const { kind, domain } = body;
|
|
338
223
|
const rule = this.manager.addDomainRule(kind, domain);
|
|
339
|
-
await this.
|
|
224
|
+
await this.applyRuntimeConfigChange();
|
|
340
225
|
sendJson(res, 200, rule);
|
|
341
226
|
};
|
|
342
227
|
}
|
|
@@ -344,7 +229,7 @@ class AdminServer {
|
|
|
344
229
|
if (method === 'DELETE' && ruleDeleteMatch) {
|
|
345
230
|
return async (_req, res) => {
|
|
346
231
|
this.manager.removeDomainRule(Number(ruleDeleteMatch[1]));
|
|
347
|
-
await this.
|
|
232
|
+
await this.applyRuntimeConfigChange();
|
|
348
233
|
sendJson(res, 200, { ok: true });
|
|
349
234
|
};
|
|
350
235
|
}
|
|
@@ -352,7 +237,7 @@ class AdminServer {
|
|
|
352
237
|
if (method === 'POST' && presetMatch) {
|
|
353
238
|
return async (_req, res) => {
|
|
354
239
|
const rules = this.manager.addPreset(presetMatch[1]);
|
|
355
|
-
await this.
|
|
240
|
+
await this.applyRuntimeConfigChange();
|
|
356
241
|
sendJson(res, 200, rules);
|
|
357
242
|
};
|
|
358
243
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function adminHtml(): string;
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adminHtml = adminHtml;
|
|
4
|
+
const defaults_1 = require("../defaults");
|
|
5
|
+
const navItems = [
|
|
6
|
+
{ id: 'home', label: '首页', icon: 'dashboard' },
|
|
7
|
+
{ id: 'proxy', label: '代理', icon: 'proxy' },
|
|
8
|
+
{ id: 'subscriptions', label: '订阅', icon: 'subscriptions' },
|
|
9
|
+
{ id: 'rules', label: '规则', icon: 'rules' },
|
|
10
|
+
{ id: 'test', label: '测试', icon: 'test' },
|
|
11
|
+
{ id: 'logs', label: '日志', icon: 'logs' }
|
|
12
|
+
];
|
|
13
|
+
const presetLabels = {
|
|
14
|
+
google: { label: 'Google', icon: 'globe' },
|
|
15
|
+
youtube: { label: 'YouTube', icon: 'play' },
|
|
16
|
+
x: { label: 'X / Twitter', icon: 'at' },
|
|
17
|
+
telegram: { label: 'Telegram', icon: 'send' }
|
|
18
|
+
};
|
|
19
|
+
function icon(name) {
|
|
20
|
+
const common = 'viewBox="0 0 24 24" aria-hidden="true"';
|
|
21
|
+
const shapes = {
|
|
22
|
+
logo: '<svg ' + common + '><circle cx="12" cy="12" r="3.2"/><circle cx="12" cy="3.8" r="2"/><circle cx="12" cy="20.2" r="2"/><circle cx="3.8" cy="12" r="2"/><circle cx="20.2" cy="12" r="2"/><circle cx="6.2" cy="6.2" r="1.8"/><circle cx="17.8" cy="6.2" r="1.8"/><circle cx="6.2" cy="17.8" r="1.8"/><circle cx="17.8" cy="17.8" r="1.8"/><path d="M12 6v12M6 12h12M7.6 7.6l8.8 8.8M16.4 7.6l-8.8 8.8" fill="none" stroke="currentColor" stroke-width="1.9"/></svg>',
|
|
23
|
+
dashboard: '<svg ' + common + '><path d="M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z"/></svg>',
|
|
24
|
+
proxy: '<svg ' + common + '><path d="M10 4h4v5h-4zM4 15h5v5H4zM15 15h5v5h-5z"/><path d="M12 9v3M6.5 15v-3h11v3" fill="none" stroke="currentColor" stroke-width="2"/></svg>',
|
|
25
|
+
subscriptions: '<svg ' + common + '><path d="M5 4h14v6H5zM5 14h14v6H5z"/><circle cx="8" cy="7" r="1" fill="#fff"/><circle cx="8" cy="17" r="1" fill="#fff"/></svg>',
|
|
26
|
+
rules: '<svg ' + common + '><path d="M4 7h8M4 17h8" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/><path d="m16 6 2 2 3-4M16 18l4-4M20 18l-4-4" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
27
|
+
test: '<svg ' + common + '><path d="M7 10V7a5 5 0 0 1 10 0v3" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="M5 10h14v10H5z" fill="none" stroke="currentColor" stroke-width="2.2"/><circle cx="12" cy="15" r="1.5"/></svg>',
|
|
28
|
+
logs: '<svg ' + common + '><path d="M6 4h12v16H6z"/><path d="M9 8h6M9 12h6M9 16h4" stroke="#fff" stroke-width="1.6" stroke-linecap="round"/></svg>',
|
|
29
|
+
refresh: '<svg ' + common + '><path d="M20 6v5h-5M4 18v-5h5" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"/><path d="M18 9a7 7 0 0 0-12-3M6 15a7 7 0 0 0 12 3" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round"/></svg>',
|
|
30
|
+
restart: '<svg ' + common + '><path d="M12 5a7 7 0 1 1-6.2 3.8" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/><path d="M5 5v5h5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
31
|
+
play: '<svg ' + common + '><path d="M8 5v14l11-7z"/></svg>',
|
|
32
|
+
stop: '<svg ' + common + '><path d="M7 7h10v10H7z"/></svg>',
|
|
33
|
+
open: '<svg ' + common + '><path d="M5 5h7v2H7v10h10v-5h2v7H5z"/><path d="M14 4h6v6h-2V7.4l-7.3 7.3-1.4-1.4L16.6 6H14z"/></svg>',
|
|
34
|
+
download: '<svg ' + common + '><path d="M12 4v10M8 10l4 4 4-4M5 19h14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
35
|
+
save: '<svg ' + common + '><path d="M5 4h12l2 2v14H5z"/><path d="M8 4v6h8V4M8 17h8" stroke="#fff" stroke-width="1.8"/></svg>',
|
|
36
|
+
add: '<svg ' + common + '><path d="M12 5v14M5 12h14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg>',
|
|
37
|
+
delete: '<svg ' + common + '><path d="M6 7h12M9 7V5h6v2M8 10l1 9h6l1-9" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
38
|
+
shieldAdd: '<svg ' + common + '><path d="M12 3 5 6v5c0 5 3 8 7 10 4-2 7-5 7-10V6z" fill="currentColor"/><path d="M12 8v7M8.5 11.5h7" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
39
|
+
shieldRemove: '<svg ' + common + '><path d="M12 3 5 6v5c0 5 3 8 7 10 4-2 7-5 7-10V6z" fill="currentColor"/><path d="m7 7 10 10" stroke="#fff" stroke-width="2"/></svg>',
|
|
40
|
+
globe: '<svg ' + common + '><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2"/><path d="M4 12h16M12 4c2 2.4 3 5 3 8s-1 5.6-3 8M12 4c-2 2.4-3 5-3 8s1 5.6 3 8" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>',
|
|
41
|
+
at: '<svg ' + common + '><circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="2"/><path d="M16 8v5a3 3 0 0 0 6 0 10 10 0 1 0-4 8" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
|
|
42
|
+
send: '<svg ' + common + '><path d="M3 11 21 3l-7 18-3-7z"/></svg>',
|
|
43
|
+
check: '<svg ' + common + '><path d="m5 12 4 4L19 6" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
44
|
+
block: '<svg ' + common + '><circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="2.2"/><path d="m7 7 10 10" stroke="currentColor" stroke-width="2.2"/></svg>'
|
|
45
|
+
};
|
|
46
|
+
return shapes[name] ?? shapes.dashboard;
|
|
47
|
+
}
|
|
48
|
+
function iconButton(name, id, title, extraClass = '') {
|
|
49
|
+
return '<button id="' + id + '" class="icon-button ' + extraClass + '" title="' + title + '">' + icon(name) + '</button>';
|
|
50
|
+
}
|
|
51
|
+
function navMarkup() {
|
|
52
|
+
return navItems.map((item) => ('<button class="nav-item" data-page="' + item.id + '">' +
|
|
53
|
+
'<span class="nav-icon">' + icon(item.icon) + '</span>' +
|
|
54
|
+
'<span>' + item.label + '</span>' +
|
|
55
|
+
'</button>')).join('');
|
|
56
|
+
}
|
|
57
|
+
function presetMarkup() {
|
|
58
|
+
return Object.keys(defaults_1.DOMAIN_PRESETS).map((id) => {
|
|
59
|
+
const preset = presetLabels[id] ?? { label: id, icon: 'globe' };
|
|
60
|
+
return '<button class="btn outline" data-preset="' + id + '">' + icon(preset.icon) + '<span>' + preset.label + '</span></button>';
|
|
61
|
+
}).join('');
|
|
62
|
+
}
|
|
63
|
+
function adminHtml() {
|
|
64
|
+
return `<!doctype html>
|
|
65
|
+
<html lang="zh-CN">
|
|
66
|
+
<head>
|
|
67
|
+
<meta charset="utf-8">
|
|
68
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
69
|
+
<title>QPJoy Tunnel Admin</title>
|
|
70
|
+
<style>
|
|
71
|
+
:root{color:#101418;background:#f3f5f7;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
72
|
+
*{box-sizing:border-box}
|
|
73
|
+
body{margin:0;background:#f3f5f7;color:#101418}
|
|
74
|
+
button,input,select{font:inherit}
|
|
75
|
+
button{cursor:pointer}
|
|
76
|
+
button:disabled{cursor:wait;opacity:.62}
|
|
77
|
+
svg{width:22px;height:22px;display:block;fill:currentColor}
|
|
78
|
+
.login-page{min-height:100vh;display:grid;place-items:center;padding:24px}
|
|
79
|
+
.login-card{width:min(420px,100%);border:1px solid #dfe4ea;border-radius:8px;background:#fff;padding:24px}
|
|
80
|
+
.brand-lockup{height:72px;display:flex;align-items:center;gap:12px;padding:0 22px;font-size:24px;font-weight:800}
|
|
81
|
+
.brand-lockup svg{width:38px;height:38px}
|
|
82
|
+
.login-card .brand-lockup{height:auto;padding:0 0 20px}
|
|
83
|
+
.login-form{display:grid;gap:12px}
|
|
84
|
+
.app-shell{min-height:100vh;display:grid;grid-template-columns:220px 1fr}
|
|
85
|
+
.side-panel{background:#fbfcfd;border-right:1px solid #dfe4ea}
|
|
86
|
+
.nav-list{padding:6px 12px}
|
|
87
|
+
.nav-item{width:100%;height:54px;border:0;border-radius:8px;margin:6px 0;padding:0 18px;display:flex;align-items:center;gap:18px;background:transparent;color:#111827;font-size:16px;font-weight:800;text-align:left}
|
|
88
|
+
.nav-item.active{background:#dce9ff;color:#0f62d0}
|
|
89
|
+
.nav-icon{width:28px;display:grid;place-items:center}
|
|
90
|
+
.content-panel{padding:22px;min-width:0}
|
|
91
|
+
.toolbar-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
92
|
+
.page-toolbar{margin-bottom:18px}
|
|
93
|
+
.page-title{font-size:30px;line-height:1.2;font-weight:800}
|
|
94
|
+
.spacer{flex:1}
|
|
95
|
+
.content-stack{display:grid;gap:18px}
|
|
96
|
+
.section-surface,.metric-cell,.subscription-card,.rule-item{background:#fff;border:1px solid #dfe4ea;border-radius:8px}
|
|
97
|
+
.section-surface{padding:16px}
|
|
98
|
+
.status-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}
|
|
99
|
+
.metric-cell{min-height:86px;padding:12px}
|
|
100
|
+
.metric-label{color:#697386;font-size:13px;margin-bottom:8px}
|
|
101
|
+
.metric-value{font-size:20px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
102
|
+
.field{height:42px;border:1px solid #c9d4e2;border-radius:6px;background:#fff;color:#101418;padding:0 12px;min-width:0}
|
|
103
|
+
.field:focus{outline:2px solid #b8d6ff;border-color:#1578ff}
|
|
104
|
+
.field-grow{flex:1 1 320px}
|
|
105
|
+
.field-short{width:150px}
|
|
106
|
+
.btn{height:42px;border:1px solid #1578ff;border-radius:6px;background:#1976d2;color:#fff;padding:0 16px;display:inline-flex;align-items:center;justify-content:center;gap:9px;font-weight:800;white-space:nowrap}
|
|
107
|
+
.btn.outline{background:#fff;color:#1976d2}
|
|
108
|
+
.btn.negative{background:#fff;color:#c10015;border-color:#c10015}
|
|
109
|
+
.icon-button{width:46px;height:46px;border:0;border-radius:50%;background:transparent;color:#101418;display:grid;place-items:center}
|
|
110
|
+
.icon-button.outline{border:1px solid #1976d2;color:#1976d2;background:#fff}
|
|
111
|
+
.icon-button.primary{background:#1976d2;color:#fff;box-shadow:0 3px 8px rgba(16,24,40,.18)}
|
|
112
|
+
.chip{min-height:34px;border-radius:999px;padding:6px 16px;display:inline-flex;align-items:center;color:#fff;background:#8b929a;font-weight:700}
|
|
113
|
+
.chip.positive{background:#21ba45}
|
|
114
|
+
.subscription-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
|
|
115
|
+
.subscription-card{min-height:118px;padding:16px}
|
|
116
|
+
.subscription-card.active{border-color:#1578ff;box-shadow:inset 4px 0 0 #1578ff}
|
|
117
|
+
.card-head{display:flex;align-items:center;gap:8px;min-width:0}
|
|
118
|
+
.card-title{font-size:18px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
119
|
+
.muted{color:#6b7280}
|
|
120
|
+
.ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
121
|
+
.rule-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
|
|
122
|
+
.rule-item{min-height:64px;padding:10px;display:grid;grid-template-columns:34px minmax(0,1fr) 38px;gap:10px;align-items:center}
|
|
123
|
+
.rule-icon.allow{color:#21ba45}.rule-icon.block{color:#c10015}
|
|
124
|
+
.mono-log{height:calc(100vh - 150px);min-height:280px;overflow:auto;border-radius:8px;background:#101418;color:#dbeafe;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;line-height:1.5;padding:12px;white-space:pre-wrap}
|
|
125
|
+
.toast{position:fixed;right:18px;top:18px;z-index:20;max-width:min(520px,calc(100vw - 36px));border-radius:6px;padding:12px 16px;background:#21ba45;color:#fff;box-shadow:0 8px 24px rgba(16,24,40,.18);font-weight:700}
|
|
126
|
+
.toast.negative{background:#c10015}
|
|
127
|
+
.empty{color:#697386;padding:10px 0}
|
|
128
|
+
[hidden]{display:none!important}
|
|
129
|
+
@media (max-width:860px){
|
|
130
|
+
.app-shell{grid-template-columns:1fr}
|
|
131
|
+
.side-panel{position:static;border-right:0;border-bottom:1px solid #dfe4ea}
|
|
132
|
+
.brand-lockup{height:62px}
|
|
133
|
+
.nav-list{display:flex;overflow:auto;gap:8px;padding:0 12px 12px}
|
|
134
|
+
.nav-item{width:auto;min-width:108px;margin:0}
|
|
135
|
+
.content-panel{padding:18px}
|
|
136
|
+
}
|
|
137
|
+
</style>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<section id="login" class="login-page">
|
|
141
|
+
<div class="login-card">
|
|
142
|
+
<div class="brand-lockup">${icon('logo')}<span>QPJoy Tunnel</span></div>
|
|
143
|
+
<div class="login-form">
|
|
144
|
+
<input id="loginUser" class="field" placeholder="admin" value="admin">
|
|
145
|
+
<input id="loginPass" class="field" type="password" placeholder="password" value="admin">
|
|
146
|
+
<button id="loginBtn" class="btn">${icon('check')}<span>登录</span></button>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
|
|
151
|
+
<div id="app" class="app-shell" hidden>
|
|
152
|
+
<aside class="side-panel">
|
|
153
|
+
<div class="brand-lockup">${icon('logo')}<span>QPJoy Tunnel</span></div>
|
|
154
|
+
<nav class="nav-list">${navMarkup()}</nav>
|
|
155
|
+
</aside>
|
|
156
|
+
<main class="content-panel">
|
|
157
|
+
<div class="toolbar-row page-toolbar">
|
|
158
|
+
<div id="pageTitle" class="page-title">首页</div>
|
|
159
|
+
<div class="spacer"></div>
|
|
160
|
+
${iconButton('refresh', 'refreshBtn', '刷新')}
|
|
161
|
+
${iconButton('restart', 'restartBtn', '重载', 'outline')}
|
|
162
|
+
${iconButton('play', 'toggleCoreBtn', '启动', 'primary')}
|
|
163
|
+
</div>
|
|
164
|
+
<div id="pageBody" class="content-stack"></div>
|
|
165
|
+
</main>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div id="toast" class="toast" hidden></div>
|
|
169
|
+
|
|
170
|
+
<script>
|
|
171
|
+
var token = window.localStorage.getItem('qpjoyTunnelAdminToken') || '';
|
|
172
|
+
var currentPage = window.localStorage.getItem('qpjoyTunnelAdminPage') || 'home';
|
|
173
|
+
var snapshot = null;
|
|
174
|
+
var busy = false;
|
|
175
|
+
var modeLabels = { 'system-tun': '虚拟网卡', 'app-global': '全局模式', 'app-rule': 'App 模式' };
|
|
176
|
+
var pageTitles = { home: '首页', proxy: '代理', subscriptions: '订阅', rules: '规则', test: '测试', logs: '日志' };
|
|
177
|
+
|
|
178
|
+
function byId(id) { return document.getElementById(id); }
|
|
179
|
+
function escapeHtml(value) {
|
|
180
|
+
return String(value == null ? '' : value).replace(/[&<>"']/g, function (char) {
|
|
181
|
+
return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char];
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function errorMessage(text) {
|
|
185
|
+
try {
|
|
186
|
+
var data = JSON.parse(text);
|
|
187
|
+
return data.error || data.message || text;
|
|
188
|
+
} catch (_error) {
|
|
189
|
+
return text || '请求失败';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function api(path, options) {
|
|
193
|
+
var res = await fetch(path, {
|
|
194
|
+
method: options && options.method ? options.method : 'GET',
|
|
195
|
+
body: options && options.body,
|
|
196
|
+
headers: Object.assign({ 'content-type': 'application/json', authorization: token ? 'Bearer ' + token : '' }, options && options.headers ? options.headers : {})
|
|
197
|
+
});
|
|
198
|
+
var text = await res.text();
|
|
199
|
+
if (!res.ok) {
|
|
200
|
+
throw new Error(errorMessage(text));
|
|
201
|
+
}
|
|
202
|
+
return text ? JSON.parse(text) : {};
|
|
203
|
+
}
|
|
204
|
+
function setBusy(value) {
|
|
205
|
+
busy = value;
|
|
206
|
+
document.querySelectorAll('button,input,select').forEach(function (element) {
|
|
207
|
+
if (element.id !== 'loginUser' && element.id !== 'loginPass') {
|
|
208
|
+
element.disabled = value;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function toast(message, negative) {
|
|
213
|
+
var el = byId('toast');
|
|
214
|
+
el.textContent = message;
|
|
215
|
+
el.className = negative ? 'toast negative' : 'toast';
|
|
216
|
+
el.hidden = false;
|
|
217
|
+
window.clearTimeout(toast.timer);
|
|
218
|
+
toast.timer = window.setTimeout(function () { el.hidden = true; }, 1800);
|
|
219
|
+
}
|
|
220
|
+
async function run(action, message) {
|
|
221
|
+
if (busy) return;
|
|
222
|
+
setBusy(true);
|
|
223
|
+
try {
|
|
224
|
+
await action();
|
|
225
|
+
await refresh();
|
|
226
|
+
if (message) toast(message, false);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
toast(error instanceof Error ? error.message : String(error), true);
|
|
229
|
+
} finally {
|
|
230
|
+
setBusy(false);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function formatBytes(bytes) {
|
|
234
|
+
var value = Number(bytes || 0);
|
|
235
|
+
if (value < 1024) return value + ' B';
|
|
236
|
+
var units = ['KB', 'MB', 'GB', 'TB'];
|
|
237
|
+
var unit = -1;
|
|
238
|
+
do {
|
|
239
|
+
value = value / 1024;
|
|
240
|
+
unit += 1;
|
|
241
|
+
} while (value >= 1024 && unit < units.length - 1);
|
|
242
|
+
return value.toFixed(value >= 10 ? 1 : 2).replace(/\\.0$/, '') + ' ' + units[unit];
|
|
243
|
+
}
|
|
244
|
+
function relativeTime(value) {
|
|
245
|
+
if (!value) return '未更新';
|
|
246
|
+
var diff = Date.now() - new Date(value).getTime();
|
|
247
|
+
var minutes = Math.max(1, Math.round(diff / 60000));
|
|
248
|
+
if (minutes < 60) return minutes + ' 分钟前';
|
|
249
|
+
var hours = Math.round(minutes / 60);
|
|
250
|
+
if (hours < 24) return hours + ' 小时前';
|
|
251
|
+
return Math.round(hours / 24) + ' 天前';
|
|
252
|
+
}
|
|
253
|
+
function redactedUrl(url) {
|
|
254
|
+
return String(url || '').replace(/\\/\\/([^:@/]+):([^@/]+)@/, '//***:***@');
|
|
255
|
+
}
|
|
256
|
+
function metric(label, value) {
|
|
257
|
+
return '<div class="metric-cell"><div class="metric-label">' + escapeHtml(label) + '</div><div class="metric-value">' + escapeHtml(value) + '</div></div>';
|
|
258
|
+
}
|
|
259
|
+
function statusChip(text, positive) {
|
|
260
|
+
return '<span class="chip ' + (positive ? 'positive' : '') + '">' + escapeHtml(text) + '</span>';
|
|
261
|
+
}
|
|
262
|
+
function normalizeUrl(value) {
|
|
263
|
+
var raw = String(value || '').trim();
|
|
264
|
+
if (!raw) throw new Error('测试网址不能为空');
|
|
265
|
+
return /^https?:\\/\\//i.test(raw) ? raw : 'https://' + raw;
|
|
266
|
+
}
|
|
267
|
+
function status() {
|
|
268
|
+
return snapshot ? snapshot.status : null;
|
|
269
|
+
}
|
|
270
|
+
function renderFrame() {
|
|
271
|
+
var s = status();
|
|
272
|
+
byId('pageTitle').textContent = pageTitles[currentPage] || '首页';
|
|
273
|
+
document.querySelectorAll('.nav-item').forEach(function (item) {
|
|
274
|
+
item.classList.toggle('active', item.dataset.page === currentPage);
|
|
275
|
+
});
|
|
276
|
+
var toggle = byId('toggleCoreBtn');
|
|
277
|
+
toggle.innerHTML = s && s.running ? '${icon('stop')}' : '${icon('play')}';
|
|
278
|
+
toggle.title = s && s.running ? '停止' : '启动';
|
|
279
|
+
}
|
|
280
|
+
function renderHome() {
|
|
281
|
+
var s = status();
|
|
282
|
+
var traffic = snapshot ? snapshot.traffic : {};
|
|
283
|
+
return '<div class="status-strip">' +
|
|
284
|
+
metric('运行状态', s && s.running ? '运行中' : '已停止') +
|
|
285
|
+
metric('当前模式', modeLabels[s ? s.mode : 'app-rule'] || 'App 模式') +
|
|
286
|
+
metric('当前订阅', s && s.activeSubscription ? s.activeSubscription.name : '未选择') +
|
|
287
|
+
metric('连接数', traffic.connections || 0) +
|
|
288
|
+
'</div>' +
|
|
289
|
+
'<div class="status-strip">' +
|
|
290
|
+
metric('上传总量', formatBytes(traffic.uploadTotal)) +
|
|
291
|
+
metric('下载总量', formatBytes(traffic.downloadTotal)) +
|
|
292
|
+
metric('本地代理', ':' + (s ? s.ports.mixed : 23458)) +
|
|
293
|
+
metric('管理后台', ':' + (s ? s.ports.admin : 23456)) +
|
|
294
|
+
'</div>' +
|
|
295
|
+
'<section class="section-surface"><div class="toolbar-row">' +
|
|
296
|
+
'<button id="updateActiveSub" class="btn">${icon('download')}<span>更新当前订阅</span></button>' +
|
|
297
|
+
'<button id="openAdminTab" class="btn outline">${icon('open')}<span>浏览器后台</span></button>' +
|
|
298
|
+
statusChip('TUN ' + (s && s.tunInstalled ? '已安装' : '未安装'), !!(s && s.tunInstalled)) +
|
|
299
|
+
statusChip('流量 ' + (traffic.available ? '可读' : '未连接'), !!traffic.available) +
|
|
300
|
+
'</div></section>';
|
|
301
|
+
}
|
|
302
|
+
function renderProxy() {
|
|
303
|
+
var s = status();
|
|
304
|
+
return '<section class="section-surface"><div class="toolbar-row">' +
|
|
305
|
+
'<select id="modeSelect" class="field field-short" style="width:220px">' +
|
|
306
|
+
'<option value="app-rule">App 模式</option><option value="app-global">全局模式</option><option value="system-tun">虚拟网卡</option>' +
|
|
307
|
+
'</select>' +
|
|
308
|
+
'<button id="saveMode" class="btn">${icon('refresh')}<span>切换</span></button>' +
|
|
309
|
+
'<button id="installTun" class="btn outline">${icon('shieldAdd')}<span>安装 TUN</span></button>' +
|
|
310
|
+
'<button id="uninstallTun" class="btn negative">${icon('shieldRemove')}<span>卸载 TUN</span></button>' +
|
|
311
|
+
statusChip('TUN ' + (s && s.tunInstalled ? '已安装' : '未安装'), !!(s && s.tunInstalled)) +
|
|
312
|
+
'</div></section>' +
|
|
313
|
+
'<section class="section-surface"><div class="toolbar-row">' +
|
|
314
|
+
'<input id="corePath" class="field field-grow" placeholder="自动使用内置隧道引擎" value="' + escapeHtml(s && s.corePath ? s.corePath : '') + '">' +
|
|
315
|
+
'<button id="saveCorePath" class="btn">${icon('save')}<span>保存引擎路径</span></button>' +
|
|
316
|
+
'</div></section>' +
|
|
317
|
+
'<section class="section-surface"><div class="toolbar-row">' +
|
|
318
|
+
'<input id="mixedPort" class="field field-short" type="number" placeholder="本地代理端口" value="' + escapeHtml(s ? s.ports.mixed : 23458) + '">' +
|
|
319
|
+
'<input id="dnsPort" class="field field-short" type="number" placeholder="DNS 端口" value="' + escapeHtml(s ? s.ports.dns : 23459) + '">' +
|
|
320
|
+
'<button id="savePorts" class="btn">${icon('save')}<span>保存端口</span></button>' +
|
|
321
|
+
statusChip('推荐 23458 / 23459,避开 Clash 7890', false) +
|
|
322
|
+
'</div></section>' +
|
|
323
|
+
'<div class="status-strip">' +
|
|
324
|
+
metric('当前模式', modeLabels[s ? s.mode : 'app-rule'] || 'App 模式') +
|
|
325
|
+
metric('本地代理', ':' + (s ? s.ports.mixed : 23458)) +
|
|
326
|
+
metric('DNS', ':' + (s ? s.ports.dns : 23459)) +
|
|
327
|
+
metric('控制接口', ':' + (s ? s.ports.controller : 23457)) +
|
|
328
|
+
'</div>';
|
|
329
|
+
}
|
|
330
|
+
function renderSubscriptions() {
|
|
331
|
+
var items = snapshot ? snapshot.subscriptions : [];
|
|
332
|
+
var cards = items.length ? items.map(function (sub) {
|
|
333
|
+
return '<article class="subscription-card ' + (sub.active ? 'active' : '') + '">' +
|
|
334
|
+
'<div class="card-head"><span>${icon('subscriptions')}</span><div class="card-title">' + escapeHtml(sub.name) + '</div><div class="spacer"></div>' +
|
|
335
|
+
'<button class="icon-button" data-refresh-sub="' + sub.id + '" title="刷新">${icon('refresh')}</button>' +
|
|
336
|
+
'<button class="icon-button" data-delete-sub="' + sub.id + '" title="删除">${icon('delete')}</button></div>' +
|
|
337
|
+
'<div class="muted ellipsis">' + escapeHtml(redactedUrl(sub.url)) + '</div>' +
|
|
338
|
+
'<div class="toolbar-row" style="margin-top:14px"><span class="muted">' + escapeHtml(relativeTime(sub.lastUpdatedAt)) + '</span><div class="spacer"></div>' +
|
|
339
|
+
'<button class="btn outline" data-active-sub="' + sub.id + '">启用</button></div>' +
|
|
340
|
+
'</article>';
|
|
341
|
+
}).join('') : '<div class="empty">暂无订阅</div>';
|
|
342
|
+
return '<section class="section-surface"><div class="toolbar-row">' +
|
|
343
|
+
'<input id="subUrl" class="field field-grow" placeholder="订阅文件链接">' +
|
|
344
|
+
'<input id="subName" class="field field-short" placeholder="名称" style="width:180px">' +
|
|
345
|
+
'<input id="subUser" class="field field-short" placeholder="用户" style="width:140px">' +
|
|
346
|
+
'<input id="subPass" class="field field-short" type="password" placeholder="密码" style="width:140px">' +
|
|
347
|
+
'<button id="addSub" class="btn">${icon('add')}<span>新建</span></button>' +
|
|
348
|
+
'</div></section>' +
|
|
349
|
+
'<div class="subscription-grid">' + cards + '</div>';
|
|
350
|
+
}
|
|
351
|
+
function renderRules() {
|
|
352
|
+
var items = snapshot ? snapshot.rules : [];
|
|
353
|
+
var cards = items.length ? items.map(function (rule) {
|
|
354
|
+
var allow = rule.kind === 'allow';
|
|
355
|
+
return '<article class="rule-item">' +
|
|
356
|
+
'<span class="rule-icon ' + (allow ? 'allow' : 'block') + '">' + (allow ? '${icon('check')}' : '${icon('block')}') + '</span>' +
|
|
357
|
+
'<div><div class="ellipsis" style="font-weight:700">' + escapeHtml(rule.domain) + '</div><div class="muted ellipsis">' + escapeHtml(rule.source) + '</div></div>' +
|
|
358
|
+
'<button class="icon-button" data-rule-remove="' + rule.id + '" title="删除">${icon('delete')}</button>' +
|
|
359
|
+
'</article>';
|
|
360
|
+
}).join('') : '<div class="empty">暂无规则</div>';
|
|
361
|
+
return '<section class="section-surface"><div class="toolbar-row">' +
|
|
362
|
+
'${presetMarkup()}' +
|
|
363
|
+
'<div class="spacer"></div>' +
|
|
364
|
+
'<input id="ruleDomain" class="field" placeholder="example.com" style="width:220px">' +
|
|
365
|
+
'<select id="ruleKind" class="field" style="width:130px"><option value="allow">白名单</option><option value="block">黑名单</option></select>' +
|
|
366
|
+
'<button id="addRule" class="btn">${icon('add')}<span>添加</span></button>' +
|
|
367
|
+
'</div></section>' +
|
|
368
|
+
'<div class="rule-grid">' + cards + '</div>';
|
|
369
|
+
}
|
|
370
|
+
function renderTest() {
|
|
371
|
+
var s = status();
|
|
372
|
+
return '<section class="section-surface"><div class="toolbar-row">' +
|
|
373
|
+
'<input id="testUrl" class="field field-grow" value="https://www.google.com" placeholder="https://www.google.com">' +
|
|
374
|
+
'<button id="openTest" class="btn">${icon('open')}<span>打开测试窗口</span></button>' +
|
|
375
|
+
'</div></section>' +
|
|
376
|
+
'<section class="section-surface"><div class="toolbar-row">' +
|
|
377
|
+
'<button class="btn outline" data-test-url="https://www.google.com">${icon('globe')}<span>Google</span></button>' +
|
|
378
|
+
'<button class="btn outline" data-test-url="https://www.youtube.com">${icon('play')}<span>YouTube</span></button>' +
|
|
379
|
+
'<button class="btn outline" data-test-url="https://x.com">${icon('at')}<span>X</span></button>' +
|
|
380
|
+
'<button class="btn outline" data-test-url="https://web.telegram.org">${icon('send')}<span>Telegram</span></button>' +
|
|
381
|
+
'</div></section>' +
|
|
382
|
+
'<div class="status-strip">' +
|
|
383
|
+
metric('当前模式', modeLabels[s ? s.mode : 'app-rule'] || 'App 模式') +
|
|
384
|
+
metric('本地代理', ':' + (s ? s.ports.mixed : 23458)) +
|
|
385
|
+
metric('运行状态', s && s.running ? '运行中' : '已停止') +
|
|
386
|
+
'</div>';
|
|
387
|
+
}
|
|
388
|
+
function renderLogs() {
|
|
389
|
+
var events = snapshot ? snapshot.events : [];
|
|
390
|
+
var text = events.map(function (event) {
|
|
391
|
+
return '[' + event.level + '] ' + new Date(event.createdAt).toLocaleString() + ' ' + event.message;
|
|
392
|
+
}).join('\\n');
|
|
393
|
+
return '<section class="section-surface"><div class="mono-log">' + escapeHtml(text) + '</div></section>';
|
|
394
|
+
}
|
|
395
|
+
function renderPage() {
|
|
396
|
+
renderFrame();
|
|
397
|
+
var body = byId('pageBody');
|
|
398
|
+
if (currentPage === 'proxy') body.innerHTML = renderProxy();
|
|
399
|
+
else if (currentPage === 'subscriptions') body.innerHTML = renderSubscriptions();
|
|
400
|
+
else if (currentPage === 'rules') body.innerHTML = renderRules();
|
|
401
|
+
else if (currentPage === 'test') body.innerHTML = renderTest();
|
|
402
|
+
else if (currentPage === 'logs') body.innerHTML = renderLogs();
|
|
403
|
+
else body.innerHTML = renderHome();
|
|
404
|
+
bindPageEvents();
|
|
405
|
+
}
|
|
406
|
+
function bindPageEvents() {
|
|
407
|
+
var modeSelect = byId('modeSelect');
|
|
408
|
+
if (modeSelect && status()) modeSelect.value = status().mode;
|
|
409
|
+
var saveMode = byId('saveMode');
|
|
410
|
+
if (saveMode) saveMode.onclick = function () { run(function () { return api('/api/mode', { method: 'POST', body: JSON.stringify({ mode: byId('modeSelect').value }) }); }, '模式已切换'); };
|
|
411
|
+
var installTun = byId('installTun');
|
|
412
|
+
if (installTun) installTun.onclick = function () { run(function () { return api('/api/tun/install', { method: 'POST' }); }, 'TUN 已安装'); };
|
|
413
|
+
var uninstallTun = byId('uninstallTun');
|
|
414
|
+
if (uninstallTun) uninstallTun.onclick = function () { run(function () { return api('/api/tun/uninstall', { method: 'POST' }); }, 'TUN 已卸载'); };
|
|
415
|
+
var saveCorePath = byId('saveCorePath');
|
|
416
|
+
if (saveCorePath) saveCorePath.onclick = function () { run(function () { return api('/api/core/path', { method: 'POST', body: JSON.stringify({ corePath: byId('corePath').value }) }); }, '引擎路径已保存'); };
|
|
417
|
+
var savePorts = byId('savePorts');
|
|
418
|
+
if (savePorts) savePorts.onclick = function () { run(function () { return api('/api/ports', { method: 'POST', body: JSON.stringify({ mixed: Number(byId('mixedPort').value), dns: Number(byId('dnsPort').value) }) }); }, '本地端口已保存'); };
|
|
419
|
+
var updateActiveSub = byId('updateActiveSub');
|
|
420
|
+
if (updateActiveSub) updateActiveSub.onclick = function () { run(function () { return api('/api/subscriptions/active/update', { method: 'POST' }); }, '当前订阅已更新'); };
|
|
421
|
+
var openAdminTab = byId('openAdminTab');
|
|
422
|
+
if (openAdminTab) openAdminTab.onclick = function () { window.open(window.location.href, '_blank'); };
|
|
423
|
+
var addSub = byId('addSub');
|
|
424
|
+
if (addSub) addSub.onclick = function () {
|
|
425
|
+
run(function () {
|
|
426
|
+
return api('/api/subscriptions', { method: 'POST', body: JSON.stringify({ name: byId('subName').value, url: byId('subUrl').value, username: byId('subUser').value, password: byId('subPass').value }) });
|
|
427
|
+
}, '订阅已保存');
|
|
428
|
+
};
|
|
429
|
+
var addRule = byId('addRule');
|
|
430
|
+
if (addRule) addRule.onclick = function () {
|
|
431
|
+
run(function () { return api('/api/rules', { method: 'POST', body: JSON.stringify({ kind: byId('ruleKind').value, domain: byId('ruleDomain').value }) }); }, '规则已添加');
|
|
432
|
+
};
|
|
433
|
+
var openTest = byId('openTest');
|
|
434
|
+
if (openTest) openTest.onclick = function () { try { window.open(normalizeUrl(byId('testUrl').value), '_blank'); } catch (error) { toast(error.message, true); } };
|
|
435
|
+
}
|
|
436
|
+
async function refresh() {
|
|
437
|
+
snapshot = await api('/api/snapshot');
|
|
438
|
+
renderPage();
|
|
439
|
+
}
|
|
440
|
+
document.querySelectorAll('.nav-item').forEach(function (item) {
|
|
441
|
+
item.onclick = function () {
|
|
442
|
+
currentPage = item.dataset.page || 'home';
|
|
443
|
+
window.localStorage.setItem('qpjoyTunnelAdminPage', currentPage);
|
|
444
|
+
renderPage();
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
byId('refreshBtn').onclick = function () { run(function () { return refresh(); }); };
|
|
448
|
+
byId('restartBtn').onclick = function () { run(function () { return api('/api/core/restart', { method: 'POST' }); }, '隧道已重载'); };
|
|
449
|
+
byId('toggleCoreBtn').onclick = function () {
|
|
450
|
+
var running = status() && status().running;
|
|
451
|
+
run(function () { return api(running ? '/api/core/stop' : '/api/core/start', { method: 'POST' }); }, running ? '隧道已停止' : '隧道已启动');
|
|
452
|
+
};
|
|
453
|
+
document.body.onclick = function (event) {
|
|
454
|
+
var target = event.target;
|
|
455
|
+
if (!(target instanceof Element)) return;
|
|
456
|
+
var element = target.closest('[data-preset],[data-active-sub],[data-refresh-sub],[data-delete-sub],[data-rule-remove],[data-test-url]');
|
|
457
|
+
if (!element) return;
|
|
458
|
+
if (element.dataset.preset) run(function () { return api('/api/presets/' + element.dataset.preset, { method: 'POST' }); }, '白名单集合已加入');
|
|
459
|
+
if (element.dataset.activeSub) run(function () { return api('/api/subscriptions/' + element.dataset.activeSub + '/active', { method: 'POST' }); }, '订阅已启用');
|
|
460
|
+
if (element.dataset.refreshSub) run(function () { return api('/api/subscriptions/' + element.dataset.refreshSub + '/update', { method: 'POST' }); }, '订阅已更新');
|
|
461
|
+
if (element.dataset.deleteSub) run(function () { return api('/api/subscriptions/' + element.dataset.deleteSub, { method: 'DELETE' }); }, '订阅已删除');
|
|
462
|
+
if (element.dataset.ruleRemove) run(function () { return api('/api/rules/' + element.dataset.ruleRemove, { method: 'DELETE' }); }, '规则已删除');
|
|
463
|
+
if (element.dataset.testUrl) {
|
|
464
|
+
var testInput = byId('testUrl');
|
|
465
|
+
if (testInput) testInput.value = element.dataset.testUrl;
|
|
466
|
+
window.open(element.dataset.testUrl, '_blank');
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
byId('loginBtn').onclick = async function () {
|
|
470
|
+
await run(async function () {
|
|
471
|
+
var result = await api('/api/login', { method: 'POST', body: JSON.stringify({ username: byId('loginUser').value, password: byId('loginPass').value }) });
|
|
472
|
+
token = result.token;
|
|
473
|
+
window.localStorage.setItem('qpjoyTunnelAdminToken', token);
|
|
474
|
+
byId('login').hidden = true;
|
|
475
|
+
byId('app').hidden = false;
|
|
476
|
+
await refresh();
|
|
477
|
+
}, '已登录');
|
|
478
|
+
};
|
|
479
|
+
if (token) {
|
|
480
|
+
byId('login').hidden = true;
|
|
481
|
+
byId('app').hidden = false;
|
|
482
|
+
refresh().catch(function () {
|
|
483
|
+
token = '';
|
|
484
|
+
window.localStorage.removeItem('qpjoyTunnelAdminToken');
|
|
485
|
+
byId('login').hidden = false;
|
|
486
|
+
byId('app').hidden = true;
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
</script>
|
|
490
|
+
</body>
|
|
491
|
+
</html>`;
|
|
492
|
+
}
|
|
@@ -25,11 +25,13 @@ function createElectronTunnel(host, options = {}) {
|
|
|
25
25
|
userDataPath: options.userDataPath ?? host.app.getPath('userData'),
|
|
26
26
|
bundledEngineDir: options.bundledEngineDir ?? defaultBundledEngineDir()
|
|
27
27
|
});
|
|
28
|
-
const admin = new AdminServer_1.AdminServer(manager);
|
|
29
28
|
async function applyProxy() {
|
|
30
29
|
const status = manager.status();
|
|
31
30
|
await (0, electronProxy_1.applyElectronProxy)(host.session, status.mode, status.ports);
|
|
32
31
|
}
|
|
32
|
+
const admin = new AdminServer_1.AdminServer(manager, {
|
|
33
|
+
afterSettingsChange: applyProxy
|
|
34
|
+
});
|
|
33
35
|
if (options.startAdminServer !== false) {
|
|
34
36
|
admin.start();
|
|
35
37
|
}
|
|
@@ -4,67 +4,95 @@ exports.registerTunnelIpc = registerTunnelIpc;
|
|
|
4
4
|
async function changed(options) {
|
|
5
5
|
await options?.afterSettingsChange?.();
|
|
6
6
|
}
|
|
7
|
+
async function runtimeChanged(manager, options) {
|
|
8
|
+
await manager.applyRuntimeConfigChange();
|
|
9
|
+
await changed(options);
|
|
10
|
+
}
|
|
7
11
|
function registerTunnelIpc(ipcMain, manager, options) {
|
|
8
12
|
ipcMain.handle('tunnel:snapshot', () => manager.snapshot());
|
|
9
|
-
ipcMain.handle('tunnel:create-subscription', (_event, input) =>
|
|
13
|
+
ipcMain.handle('tunnel:create-subscription', async (_event, input) => {
|
|
14
|
+
const subscription = await manager.createSubscription(input);
|
|
15
|
+
if (subscription.active) {
|
|
16
|
+
await runtimeChanged(manager, options);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
await changed(options);
|
|
20
|
+
}
|
|
21
|
+
return subscription;
|
|
22
|
+
});
|
|
10
23
|
ipcMain.handle('tunnel:delete-subscription', async (_event, id) => {
|
|
11
24
|
manager.deleteSubscription(id);
|
|
12
|
-
await manager
|
|
25
|
+
await runtimeChanged(manager, options);
|
|
13
26
|
});
|
|
14
27
|
ipcMain.handle('tunnel:set-active-subscription', async (_event, id) => {
|
|
15
28
|
const subscription = manager.setActiveSubscription(id);
|
|
16
|
-
await manager
|
|
29
|
+
await runtimeChanged(manager, options);
|
|
17
30
|
return subscription;
|
|
18
31
|
});
|
|
19
32
|
ipcMain.handle('tunnel:update-subscription', async (_event, id) => {
|
|
20
33
|
const subscription = await manager.updateSubscription(id);
|
|
21
34
|
if (subscription.active) {
|
|
22
|
-
await manager
|
|
35
|
+
await runtimeChanged(manager, options);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await changed(options);
|
|
23
39
|
}
|
|
24
40
|
return subscription;
|
|
25
41
|
});
|
|
26
42
|
ipcMain.handle('tunnel:update-active-subscription', async () => {
|
|
27
43
|
const subscription = await manager.updateActiveSubscription();
|
|
28
|
-
await manager
|
|
44
|
+
await runtimeChanged(manager, options);
|
|
29
45
|
return subscription;
|
|
30
46
|
});
|
|
31
47
|
ipcMain.handle('tunnel:set-mode', async (_event, mode) => {
|
|
32
48
|
const changedMode = manager.setMode(mode);
|
|
33
49
|
if (changedMode) {
|
|
34
|
-
await manager
|
|
50
|
+
await runtimeChanged(manager, options);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await changed(options);
|
|
35
54
|
}
|
|
55
|
+
});
|
|
56
|
+
ipcMain.handle('tunnel:set-core-path', async (_event, corePath) => {
|
|
57
|
+
manager.setCorePath(corePath);
|
|
36
58
|
await changed(options);
|
|
37
59
|
});
|
|
38
|
-
ipcMain.handle('tunnel:set-core-path', (_event, corePath) => manager.setCorePath(corePath));
|
|
39
60
|
ipcMain.handle('tunnel:set-local-ports', async (_event, ports) => {
|
|
40
61
|
await manager.setLocalPorts(ports);
|
|
41
62
|
await changed(options);
|
|
42
63
|
});
|
|
43
64
|
ipcMain.handle('tunnel:install-tun', async () => {
|
|
44
65
|
manager.installTunFeature();
|
|
45
|
-
await manager
|
|
46
|
-
await changed(options);
|
|
66
|
+
await runtimeChanged(manager, options);
|
|
47
67
|
});
|
|
48
68
|
ipcMain.handle('tunnel:uninstall-tun', async () => {
|
|
49
69
|
manager.uninstallTunFeature();
|
|
50
|
-
await manager
|
|
70
|
+
await runtimeChanged(manager, options);
|
|
71
|
+
});
|
|
72
|
+
ipcMain.handle('tunnel:start', async () => {
|
|
73
|
+
await manager.start();
|
|
74
|
+
await changed(options);
|
|
75
|
+
});
|
|
76
|
+
ipcMain.handle('tunnel:stop', async () => {
|
|
77
|
+
await manager.stop();
|
|
78
|
+
await changed(options);
|
|
79
|
+
});
|
|
80
|
+
ipcMain.handle('tunnel:restart', async () => {
|
|
81
|
+
await manager.restart();
|
|
51
82
|
await changed(options);
|
|
52
83
|
});
|
|
53
|
-
ipcMain.handle('tunnel:start', () => manager.start());
|
|
54
|
-
ipcMain.handle('tunnel:stop', () => manager.stop());
|
|
55
|
-
ipcMain.handle('tunnel:restart', () => manager.restart());
|
|
56
84
|
ipcMain.handle('tunnel:add-rule', async (_event, input) => {
|
|
57
85
|
const rule = manager.addDomainRule(input.kind, input.domain);
|
|
58
|
-
await manager
|
|
86
|
+
await runtimeChanged(manager, options);
|
|
59
87
|
return rule;
|
|
60
88
|
});
|
|
61
89
|
ipcMain.handle('tunnel:remove-rule', async (_event, id) => {
|
|
62
90
|
manager.removeDomainRule(id);
|
|
63
|
-
await manager
|
|
91
|
+
await runtimeChanged(manager, options);
|
|
64
92
|
});
|
|
65
93
|
ipcMain.handle('tunnel:add-preset', async (_event, preset) => {
|
|
66
94
|
const rules = manager.addPreset(preset);
|
|
67
|
-
await manager
|
|
95
|
+
await runtimeChanged(manager, options);
|
|
68
96
|
return rules;
|
|
69
97
|
});
|
|
70
98
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qpjoy/electron-tunnel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable QPJoy tunnel runtime and CLI for Electron apps on macOS and Linux.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"qpjoy-tunnel": "dist/cli.js"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
|
+
"README.md",
|
|
22
|
+
"THIRD_PARTY_NOTICES.md",
|
|
21
23
|
"dist",
|
|
22
24
|
"resources/engine"
|
|
23
25
|
],
|