@qpjoy/electron-tunnel 0.1.2 → 0.1.3
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/dist/admin/AdminServer.js +24 -0
- package/dist/admin/admin-ui.js +70 -4
- package/dist/db/TunnelDatabase.d.ts +2 -0
- package/dist/db/TunnelDatabase.js +31 -0
- package/dist/ipc/registerTunnelIpc.js +15 -0
- package/dist/mihomo/MihomoManager.d.ts +3 -1
- package/dist/mihomo/MihomoManager.js +19 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -209,6 +209,23 @@ class AdminServer {
|
|
|
209
209
|
sendJson(res, 200, subscription);
|
|
210
210
|
};
|
|
211
211
|
}
|
|
212
|
+
const editSubscriptionMatch = pathname.match(/^\/api\/subscriptions\/(\d+)$/);
|
|
213
|
+
if ((method === 'PATCH' || method === 'PUT') && editSubscriptionMatch) {
|
|
214
|
+
return async (_req, res, body) => {
|
|
215
|
+
const input = body;
|
|
216
|
+
const subscription = await this.manager.editSubscription({
|
|
217
|
+
...input,
|
|
218
|
+
id: Number(editSubscriptionMatch[1])
|
|
219
|
+
});
|
|
220
|
+
if (subscription.active) {
|
|
221
|
+
await this.applyRuntimeConfigChange();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
await this.notifySettingsChange();
|
|
225
|
+
}
|
|
226
|
+
sendJson(res, 200, subscription);
|
|
227
|
+
};
|
|
228
|
+
}
|
|
212
229
|
const deleteSubscriptionMatch = pathname.match(/^\/api\/subscriptions\/(\d+)$/);
|
|
213
230
|
if (method === 'DELETE' && deleteSubscriptionMatch) {
|
|
214
231
|
return async (_req, res) => {
|
|
@@ -241,6 +258,13 @@ class AdminServer {
|
|
|
241
258
|
sendJson(res, 200, rules);
|
|
242
259
|
};
|
|
243
260
|
}
|
|
261
|
+
if (method === 'DELETE' && presetMatch) {
|
|
262
|
+
return async (_req, res) => {
|
|
263
|
+
const count = this.manager.removePreset(presetMatch[1]);
|
|
264
|
+
await this.applyRuntimeConfigChange();
|
|
265
|
+
sendJson(res, 200, { ok: true, count });
|
|
266
|
+
};
|
|
267
|
+
}
|
|
244
268
|
return null;
|
|
245
269
|
}
|
|
246
270
|
}
|
package/dist/admin/admin-ui.js
CHANGED
|
@@ -57,7 +57,7 @@ function navMarkup() {
|
|
|
57
57
|
function presetMarkup() {
|
|
58
58
|
return Object.keys(defaults_1.DOMAIN_PRESETS).map((id) => {
|
|
59
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>';
|
|
60
|
+
return '<button class="btn outline" data-preset="' + id + '" data-preset-button="' + id + '">' + icon(preset.icon) + '<span>' + preset.label + '</span></button>';
|
|
61
61
|
}).join('');
|
|
62
62
|
}
|
|
63
63
|
function adminHtml() {
|
|
@@ -105,6 +105,7 @@ function adminHtml() {
|
|
|
105
105
|
.field-short{width:150px}
|
|
106
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
107
|
.btn.outline{background:#fff;color:#1976d2}
|
|
108
|
+
.btn.active{background:#dce9ff;color:#0f62d0}
|
|
108
109
|
.btn.negative{background:#fff;color:#c10015;border-color:#c10015}
|
|
109
110
|
.icon-button{width:46px;height:46px;border:0;border-radius:50%;background:transparent;color:#101418;display:grid;place-items:center}
|
|
110
111
|
.icon-button.outline{border:1px solid #1976d2;color:#1976d2;background:#fff}
|
|
@@ -124,6 +125,12 @@ function adminHtml() {
|
|
|
124
125
|
.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
126
|
.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
127
|
.toast.negative{background:#c10015}
|
|
128
|
+
.modal-backdrop{position:fixed;inset:0;z-index:15;display:grid;place-items:center;background:rgba(15,23,42,.28);padding:24px}
|
|
129
|
+
.modal-card{width:min(720px,100%);border:1px solid #dfe4ea;border-radius:8px;background:#fff;padding:18px;box-shadow:0 18px 48px rgba(16,24,40,.22)}
|
|
130
|
+
.modal-title{margin:0 0 14px;font-size:22px;font-weight:800}
|
|
131
|
+
.modal-form{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
132
|
+
.modal-form .wide{grid-column:1 / -1}
|
|
133
|
+
.modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:16px}
|
|
127
134
|
.empty{color:#697386;padding:10px 0}
|
|
128
135
|
[hidden]{display:none!important}
|
|
129
136
|
@media (max-width:860px){
|
|
@@ -166,6 +173,7 @@ function adminHtml() {
|
|
|
166
173
|
</div>
|
|
167
174
|
|
|
168
175
|
<div id="toast" class="toast" hidden></div>
|
|
176
|
+
<div id="modal" class="modal-backdrop" hidden></div>
|
|
169
177
|
|
|
170
178
|
<script>
|
|
171
179
|
var token = window.localStorage.getItem('qpjoyTunnelAdminToken') || '';
|
|
@@ -218,14 +226,16 @@ function adminHtml() {
|
|
|
218
226
|
toast.timer = window.setTimeout(function () { el.hidden = true; }, 1800);
|
|
219
227
|
}
|
|
220
228
|
async function run(action, message) {
|
|
221
|
-
if (busy) return;
|
|
229
|
+
if (busy) return false;
|
|
222
230
|
setBusy(true);
|
|
223
231
|
try {
|
|
224
232
|
await action();
|
|
225
233
|
await refresh();
|
|
226
234
|
if (message) toast(message, false);
|
|
235
|
+
return true;
|
|
227
236
|
} catch (error) {
|
|
228
237
|
toast(error instanceof Error ? error.message : String(error), true);
|
|
238
|
+
return false;
|
|
229
239
|
} finally {
|
|
230
240
|
setBusy(false);
|
|
231
241
|
}
|
|
@@ -267,6 +277,12 @@ function adminHtml() {
|
|
|
267
277
|
function status() {
|
|
268
278
|
return snapshot ? snapshot.status : null;
|
|
269
279
|
}
|
|
280
|
+
function presetActive(preset) {
|
|
281
|
+
return !!((snapshot && snapshot.rules) || []).some(function (rule) { return rule.source === 'preset:' + preset; });
|
|
282
|
+
}
|
|
283
|
+
function subscriptionById(id) {
|
|
284
|
+
return ((snapshot && snapshot.subscriptions) || []).find(function (sub) { return sub.id === Number(id); });
|
|
285
|
+
}
|
|
270
286
|
function renderFrame() {
|
|
271
287
|
var s = status();
|
|
272
288
|
byId('pageTitle').textContent = pageTitles[currentPage] || '首页';
|
|
@@ -332,6 +348,7 @@ function adminHtml() {
|
|
|
332
348
|
var cards = items.length ? items.map(function (sub) {
|
|
333
349
|
return '<article class="subscription-card ' + (sub.active ? 'active' : '') + '">' +
|
|
334
350
|
'<div class="card-head"><span>${icon('subscriptions')}</span><div class="card-title">' + escapeHtml(sub.name) + '</div><div class="spacer"></div>' +
|
|
351
|
+
'<button class="icon-button" data-edit-sub="' + sub.id + '" title="编辑">${icon('save')}</button>' +
|
|
335
352
|
'<button class="icon-button" data-refresh-sub="' + sub.id + '" title="刷新">${icon('refresh')}</button>' +
|
|
336
353
|
'<button class="icon-button" data-delete-sub="' + sub.id + '" title="删除">${icon('delete')}</button></div>' +
|
|
337
354
|
'<div class="muted ellipsis">' + escapeHtml(redactedUrl(sub.url)) + '</div>' +
|
|
@@ -402,6 +419,51 @@ function adminHtml() {
|
|
|
402
419
|
else if (currentPage === 'logs') body.innerHTML = renderLogs();
|
|
403
420
|
else body.innerHTML = renderHome();
|
|
404
421
|
bindPageEvents();
|
|
422
|
+
document.querySelectorAll('[data-preset-button]').forEach(function (button) {
|
|
423
|
+
var preset = button.dataset.presetButton;
|
|
424
|
+
var active = presetActive(preset);
|
|
425
|
+
button.classList.toggle('active', active);
|
|
426
|
+
button.title = active ? '再次点击移除这一组白名单' : '点击加入这一组白名单';
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
function closeModal() {
|
|
430
|
+
var modal = byId('modal');
|
|
431
|
+
modal.hidden = true;
|
|
432
|
+
modal.innerHTML = '';
|
|
433
|
+
}
|
|
434
|
+
function openEditSubscriptionModal(id) {
|
|
435
|
+
var sub = subscriptionById(id);
|
|
436
|
+
if (!sub) return;
|
|
437
|
+
var modal = byId('modal');
|
|
438
|
+
modal.innerHTML = '<div class="modal-card">' +
|
|
439
|
+
'<h2 class="modal-title">编辑订阅</h2>' +
|
|
440
|
+
'<div class="modal-form">' +
|
|
441
|
+
'<input id="editSubUrl" class="field wide" placeholder="订阅文件链接" value="' + escapeHtml(sub.url) + '">' +
|
|
442
|
+
'<input id="editSubName" class="field" placeholder="名称" value="' + escapeHtml(sub.name) + '">' +
|
|
443
|
+
'<input id="editSubUser" class="field" placeholder="用户" value="' + escapeHtml(sub.username || '') + '">' +
|
|
444
|
+
'<input id="editSubPass" class="field wide" type="password" placeholder="密码" value="' + escapeHtml(sub.password || '') + '">' +
|
|
445
|
+
'</div>' +
|
|
446
|
+
'<div class="modal-actions">' +
|
|
447
|
+
'<button id="cancelEditSub" class="btn outline">取消</button>' +
|
|
448
|
+
'<button id="saveEditSub" class="btn">${icon('save')}<span>保存</span></button>' +
|
|
449
|
+
'</div>' +
|
|
450
|
+
'</div>';
|
|
451
|
+
modal.hidden = false;
|
|
452
|
+
byId('cancelEditSub').onclick = closeModal;
|
|
453
|
+
byId('saveEditSub').onclick = async function () {
|
|
454
|
+
var saved = await run(function () {
|
|
455
|
+
return api('/api/subscriptions/' + sub.id, {
|
|
456
|
+
method: 'PATCH',
|
|
457
|
+
body: JSON.stringify({
|
|
458
|
+
name: byId('editSubName').value,
|
|
459
|
+
url: byId('editSubUrl').value,
|
|
460
|
+
username: byId('editSubUser').value,
|
|
461
|
+
password: byId('editSubPass').value
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
}, '订阅已保存');
|
|
465
|
+
if (saved) closeModal();
|
|
466
|
+
};
|
|
405
467
|
}
|
|
406
468
|
function bindPageEvents() {
|
|
407
469
|
var modeSelect = byId('modeSelect');
|
|
@@ -453,12 +515,16 @@ function adminHtml() {
|
|
|
453
515
|
document.body.onclick = function (event) {
|
|
454
516
|
var target = event.target;
|
|
455
517
|
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]');
|
|
518
|
+
var element = target.closest('[data-preset],[data-active-sub],[data-refresh-sub],[data-delete-sub],[data-edit-sub],[data-rule-remove],[data-test-url]');
|
|
457
519
|
if (!element) return;
|
|
458
|
-
if (element.dataset.preset)
|
|
520
|
+
if (element.dataset.preset) {
|
|
521
|
+
var active = presetActive(element.dataset.preset);
|
|
522
|
+
run(function () { return api('/api/presets/' + element.dataset.preset, { method: active ? 'DELETE' : 'POST' }); }, active ? '白名单集合已移除' : '白名单集合已加入');
|
|
523
|
+
}
|
|
459
524
|
if (element.dataset.activeSub) run(function () { return api('/api/subscriptions/' + element.dataset.activeSub + '/active', { method: 'POST' }); }, '订阅已启用');
|
|
460
525
|
if (element.dataset.refreshSub) run(function () { return api('/api/subscriptions/' + element.dataset.refreshSub + '/update', { method: 'POST' }); }, '订阅已更新');
|
|
461
526
|
if (element.dataset.deleteSub) run(function () { return api('/api/subscriptions/' + element.dataset.deleteSub, { method: 'DELETE' }); }, '订阅已删除');
|
|
527
|
+
if (element.dataset.editSub) openEditSubscriptionModal(Number(element.dataset.editSub));
|
|
462
528
|
if (element.dataset.ruleRemove) run(function () { return api('/api/rules/' + element.dataset.ruleRemove, { method: 'DELETE' }); }, '规则已删除');
|
|
463
529
|
if (element.dataset.testUrl) {
|
|
464
530
|
var testInput = byId('testUrl');
|
|
@@ -11,12 +11,14 @@ export declare class TunnelDatabase {
|
|
|
11
11
|
getSubscription(id: number): SubscriptionRecord | null;
|
|
12
12
|
getActiveSubscription(): SubscriptionRecord | null;
|
|
13
13
|
createSubscription(input: SubscriptionInput): SubscriptionRecord;
|
|
14
|
+
updateSubscription(id: number, input: SubscriptionInput): SubscriptionRecord;
|
|
14
15
|
deleteSubscription(id: number): void;
|
|
15
16
|
setActiveSubscription(id: number): SubscriptionRecord;
|
|
16
17
|
updateSubscriptionContent(id: number, content: string, localPath: string): SubscriptionRecord;
|
|
17
18
|
listRules(): DomainRule[];
|
|
18
19
|
upsertRule(kind: DomainRuleKind, domain: string, source?: string): DomainRule;
|
|
19
20
|
removeRule(id: number): void;
|
|
21
|
+
removeRulesBySource(source: string): number;
|
|
20
22
|
addEvent(level: EventRecord['level'], message: string): void;
|
|
21
23
|
listEvents(limit?: number): EventRecord[];
|
|
22
24
|
private ensureDefaultSettings;
|
|
@@ -174,6 +174,33 @@ class TunnelDatabase {
|
|
|
174
174
|
}
|
|
175
175
|
return this.getSubscription(created.id) ?? created;
|
|
176
176
|
}
|
|
177
|
+
updateSubscription(id, input) {
|
|
178
|
+
const current = this.getSubscription(id);
|
|
179
|
+
if (!current) {
|
|
180
|
+
throw new Error(`subscription not found: ${id}`);
|
|
181
|
+
}
|
|
182
|
+
this.db.prepare(`
|
|
183
|
+
UPDATE subscriptions
|
|
184
|
+
SET name = @name,
|
|
185
|
+
url = @url,
|
|
186
|
+
username = @username,
|
|
187
|
+
password = @password,
|
|
188
|
+
updated_at = @updatedAt
|
|
189
|
+
WHERE id = @id
|
|
190
|
+
`).run({
|
|
191
|
+
id,
|
|
192
|
+
name: input.name,
|
|
193
|
+
url: input.url,
|
|
194
|
+
username: input.username ?? '',
|
|
195
|
+
password: input.password ?? '',
|
|
196
|
+
updatedAt: nowIso()
|
|
197
|
+
});
|
|
198
|
+
const updated = this.getSubscription(id);
|
|
199
|
+
if (!updated) {
|
|
200
|
+
throw new Error(`subscription not found after update: ${id}`);
|
|
201
|
+
}
|
|
202
|
+
return updated;
|
|
203
|
+
}
|
|
177
204
|
deleteSubscription(id) {
|
|
178
205
|
this.db.prepare('DELETE FROM subscriptions WHERE id = ?').run(id);
|
|
179
206
|
const active = this.getActiveSubscription();
|
|
@@ -245,6 +272,10 @@ class TunnelDatabase {
|
|
|
245
272
|
removeRule(id) {
|
|
246
273
|
this.db.prepare('DELETE FROM domain_rules WHERE id = ?').run(id);
|
|
247
274
|
}
|
|
275
|
+
removeRulesBySource(source) {
|
|
276
|
+
const result = this.db.prepare('DELETE FROM domain_rules WHERE source = ?').run(source);
|
|
277
|
+
return Number(result.changes);
|
|
278
|
+
}
|
|
248
279
|
addEvent(level, message) {
|
|
249
280
|
this.db.prepare(`
|
|
250
281
|
INSERT INTO events (level, message, created_at)
|
|
@@ -20,6 +20,16 @@ function registerTunnelIpc(ipcMain, manager, options) {
|
|
|
20
20
|
}
|
|
21
21
|
return subscription;
|
|
22
22
|
});
|
|
23
|
+
ipcMain.handle('tunnel:edit-subscription', async (_event, input) => {
|
|
24
|
+
const subscription = await manager.editSubscription(input);
|
|
25
|
+
if (subscription.active) {
|
|
26
|
+
await runtimeChanged(manager, options);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
await changed(options);
|
|
30
|
+
}
|
|
31
|
+
return subscription;
|
|
32
|
+
});
|
|
23
33
|
ipcMain.handle('tunnel:delete-subscription', async (_event, id) => {
|
|
24
34
|
manager.deleteSubscription(id);
|
|
25
35
|
await runtimeChanged(manager, options);
|
|
@@ -95,4 +105,9 @@ function registerTunnelIpc(ipcMain, manager, options) {
|
|
|
95
105
|
await runtimeChanged(manager, options);
|
|
96
106
|
return rules;
|
|
97
107
|
});
|
|
108
|
+
ipcMain.handle('tunnel:remove-preset', async (_event, preset) => {
|
|
109
|
+
const count = manager.removePreset(preset);
|
|
110
|
+
await runtimeChanged(manager, options);
|
|
111
|
+
return count;
|
|
112
|
+
});
|
|
98
113
|
}
|
|
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
|
|
2
2
|
import { TunnelDatabase } from '../db/TunnelDatabase';
|
|
3
3
|
import { type DomainPresetId } from '../defaults';
|
|
4
4
|
import { MihomoApi } from './MihomoApi';
|
|
5
|
-
import type { DomainRule, RuntimeMode, SubscriptionInput, SubscriptionRecord, TunnelManagerOptions, TunnelSnapshot, TunnelPorts, TunnelStatus } from '../types';
|
|
5
|
+
import type { DomainRule, RuntimeMode, SubscriptionInput, SubscriptionUpdateInput, SubscriptionRecord, TunnelManagerOptions, TunnelSnapshot, TunnelPorts, TunnelStatus } from '../types';
|
|
6
6
|
interface ManagerPaths {
|
|
7
7
|
root: string;
|
|
8
8
|
db: string;
|
|
@@ -32,6 +32,7 @@ export declare class MihomoManager extends EventEmitter {
|
|
|
32
32
|
listRules(): DomainRule[];
|
|
33
33
|
listEvents(): import("../types").EventRecord[];
|
|
34
34
|
createSubscription(input: SubscriptionInput): Promise<SubscriptionRecord>;
|
|
35
|
+
editSubscription(input: SubscriptionUpdateInput): Promise<SubscriptionRecord>;
|
|
35
36
|
private localSubscriptionPath;
|
|
36
37
|
private fetchSubscriptionContent;
|
|
37
38
|
deleteSubscription(id: number): void;
|
|
@@ -46,6 +47,7 @@ export declare class MihomoManager extends EventEmitter {
|
|
|
46
47
|
updateActiveSubscription(): Promise<SubscriptionRecord>;
|
|
47
48
|
addDomainRule(kind: 'allow' | 'block', domain: string, source?: string): DomainRule;
|
|
48
49
|
addPreset(preset: DomainPresetId): DomainRule[];
|
|
50
|
+
removePreset(preset: DomainPresetId): number;
|
|
49
51
|
removeDomainRule(id: number): void;
|
|
50
52
|
renderConfig(): string;
|
|
51
53
|
private runExclusive;
|
|
@@ -210,6 +210,20 @@ class MihomoManager extends events_1.EventEmitter {
|
|
|
210
210
|
throw error;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
async editSubscription(input) {
|
|
214
|
+
const current = this.db.getSubscription(input.id);
|
|
215
|
+
if (!current) {
|
|
216
|
+
throw new Error(`subscription not found: ${input.id}`);
|
|
217
|
+
}
|
|
218
|
+
const normalized = normalizeSubscriptionInput(input);
|
|
219
|
+
const content = await this.fetchSubscriptionContent(normalized);
|
|
220
|
+
const subscription = this.db.updateSubscription(input.id, normalized);
|
|
221
|
+
const localPath = this.localSubscriptionPath(subscription.id);
|
|
222
|
+
(0, fs_1.writeFileSync)(localPath, content, 'utf8');
|
|
223
|
+
const updated = this.db.updateSubscriptionContent(subscription.id, content, localPath);
|
|
224
|
+
this.log('info', `Subscription edited: ${updated.name}`);
|
|
225
|
+
return updated;
|
|
226
|
+
}
|
|
213
227
|
localSubscriptionPath(id) {
|
|
214
228
|
return (0, path_1.join)(this.paths.profiles, `subscription-${id}.yaml`);
|
|
215
229
|
}
|
|
@@ -313,6 +327,11 @@ class MihomoManager extends events_1.EventEmitter {
|
|
|
313
327
|
this.log('info', `Preset allowlist added: ${preset}`);
|
|
314
328
|
return rules;
|
|
315
329
|
}
|
|
330
|
+
removePreset(preset) {
|
|
331
|
+
const changes = this.db.removeRulesBySource(`preset:${preset}`);
|
|
332
|
+
this.log('info', `Preset allowlist removed: ${preset} (${changes} rules)`);
|
|
333
|
+
return changes;
|
|
334
|
+
}
|
|
316
335
|
removeDomainRule(id) {
|
|
317
336
|
this.db.removeRule(id);
|
|
318
337
|
this.log('info', `Domain rule removed: ${id}`);
|
package/dist/types.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export interface SubscriptionInput {
|
|
|
24
24
|
username?: string;
|
|
25
25
|
password?: string;
|
|
26
26
|
}
|
|
27
|
+
export interface SubscriptionUpdateInput extends SubscriptionInput {
|
|
28
|
+
id: number;
|
|
29
|
+
}
|
|
27
30
|
export interface SubscriptionRecord extends Required<SubscriptionInput> {
|
|
28
31
|
id: number;
|
|
29
32
|
localPath: string | null;
|