@qpjoy/electron-launcher-app-h2o 2.0.0
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 +59 -0
- package/package.json +39 -0
- package/src/index.html +14 -0
- package/src/main.cjs +521 -0
- package/src/preload.cjs +25 -0
- package/src/renderer.js +684 -0
- package/src/styles.css +696 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# MX App H2O
|
|
2
|
+
|
|
3
|
+
`@qpjoy/electron-launcher-app-h2o` is **H2O: Home To Oversea**, the first
|
|
4
|
+
MX-H2I AppCenter built-in embed network plugin. The product direction is a
|
|
5
|
+
Clash-like user tool for proxy mode, PAC, Split DNS, rule status, and Internal
|
|
6
|
+
access visibility, while the actual privileged network work remains owned by
|
|
7
|
+
the MX-H2I standalone broker.
|
|
8
|
+
|
|
9
|
+
It is intentionally a normal npm-style Electron package: AppCenter can record
|
|
10
|
+
the package name and version in Internal, cache the installed package locally,
|
|
11
|
+
then open its entrypoint through the MX-H2I broker-session.
|
|
12
|
+
|
|
13
|
+
The default screen is user-facing: connection health, proxy mode, DNS/PAC
|
|
14
|
+
status, and recent activity are visible without exposing launcher internals.
|
|
15
|
+
Developers and support users can click `Debug` to inspect inherited MX-H2I
|
|
16
|
+
context such as broker session, socket path, package metadata, network scope,
|
|
17
|
+
local IP, and capability bridge state.
|
|
18
|
+
|
|
19
|
+
## Development Test
|
|
20
|
+
|
|
21
|
+
From `electron-dock/mx-launcher`:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pnpm --filter @qpjoy/electron-launcher-app-h2o check
|
|
25
|
+
pnpm --filter @qpjoy/electron-launcher-app-h2o dev
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The demo starts with a development broker registry so UI and broker-session states can be tested without a production socket. To verify the blocked embed path:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
MX_H2O_BROKER_MODE=off pnpm --filter @qpjoy/electron-launcher-app-h2o dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## AppCenter Link
|
|
35
|
+
|
|
36
|
+
MX-H2I ships H2O in its default AppCenter catalog:
|
|
37
|
+
|
|
38
|
+
- `appId`: `h2o`
|
|
39
|
+
- `packageName`: `@qpjoy/electron-launcher-app-h2o`
|
|
40
|
+
- `launcherMode`: `embed`
|
|
41
|
+
- `standaloneChannelProductId`: `mx-h2i`
|
|
42
|
+
- `networkScope`: `broker-session`
|
|
43
|
+
- `runtimeContractVersion`: `0.1`
|
|
44
|
+
- `requiredCapabilities`: `user.session`, `network.status`, `network.proxy`,
|
|
45
|
+
`network.dns.policy`, `network.pac.policy`, `app-center-runtime`
|
|
46
|
+
- `entrypoints.dev`: `workspace:demos/mx-app-h2o`
|
|
47
|
+
|
|
48
|
+
In development, open MX-H2I, connect as guest or employee, install AppCenter,
|
|
49
|
+
then install H2O from AppCenter. MX-H2I resolves `entrypoints.dev` to this
|
|
50
|
+
workspace package, records the installed version and path in its local cache,
|
|
51
|
+
and keeps operational details available from AppCenter's `Debug` button. Run
|
|
52
|
+
this package with `pnpm --filter @qpjoy/electron-launcher-app-h2o dev` to test
|
|
53
|
+
the embed UI directly.
|
|
54
|
+
|
|
55
|
+
In production, Internal admin should upsert the AppCenter record with the same
|
|
56
|
+
package name and the release version. The client-side AppCenter cache stores
|
|
57
|
+
`installedVersion`, `latestVersion`, `installSource`, `entrypoints`, install
|
|
58
|
+
path, last action, and recent logs; the remote DB keeps the authoritative latest
|
|
59
|
+
package/version and access policy.
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qpjoy/electron-launcher-app-h2o",
|
|
3
|
+
"productName": "MX-H2I H2O",
|
|
4
|
+
"version": "2.0.0",
|
|
5
|
+
"private": false,
|
|
6
|
+
"author": "QPJoy",
|
|
7
|
+
"description": "Home To Oversea embed network plugin for MX-H2I AppCenter.",
|
|
8
|
+
"license": "UNLICENSED",
|
|
9
|
+
"main": "src/main.cjs",
|
|
10
|
+
"files": [
|
|
11
|
+
"README.md",
|
|
12
|
+
"package.json",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@qpjoy/electron-launcher": "2.0.0",
|
|
20
|
+
"@qpjoy/electron-core-mihomo": "^0.1.1",
|
|
21
|
+
"@qpjoy/electron-plugin-tunnel": "^0.1.17"
|
|
22
|
+
},
|
|
23
|
+
"optionalDependencies": {
|
|
24
|
+
"@qpjoy/electron-plugin-tunnel-engine-darwin-arm64": "^0.1.5",
|
|
25
|
+
"@qpjoy/electron-plugin-tunnel-engine-linux-x64": "^0.1.5",
|
|
26
|
+
"@qpjoy/electron-plugin-tunnel-engine-darwin-x64": "^0.1.5",
|
|
27
|
+
"@qpjoy/electron-plugin-tunnel-engine-linux-arm64": "^0.1.5",
|
|
28
|
+
"@qpjoy/electron-plugin-tunnel-engine-win32-x64": "^0.1.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"electron": "^34.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "pnpm run check",
|
|
35
|
+
"dev": "electron .",
|
|
36
|
+
"start": "pnpm dev",
|
|
37
|
+
"check": "node --check src/main.cjs && node --check src/preload.cjs && node --check src/renderer.js"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>H2O</title>
|
|
7
|
+
<link rel="stylesheet" href="../../../ui-design/src/styles.css" />
|
|
8
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body class="qp-app qp-theme-neon-void qp-density--medium">
|
|
11
|
+
<main id="app" class="h2o-root"></main>
|
|
12
|
+
<script src="./renderer.js"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/src/main.cjs
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const H2O_APP_ID = 'h2o';
|
|
5
|
+
const H2O_DISPLAY_NAME = 'H2O';
|
|
6
|
+
const H2O_FULL_NAME = 'Home To Oversea';
|
|
7
|
+
const H2O_PACKAGE_NAME = '@qpjoy/electron-launcher-app-h2o';
|
|
8
|
+
const H2O_DESCRIPTION = 'AppCenter 内置的 Home To Oversea 网络插件,提供类 Clash 的代理模式、PAC、Split DNS 和 Internal 出海状态面板。';
|
|
9
|
+
const H2O_REQUIRED_CAPABILITIES = [
|
|
10
|
+
'user.session',
|
|
11
|
+
'network.status',
|
|
12
|
+
'network.proxy',
|
|
13
|
+
'network.dns.policy',
|
|
14
|
+
'network.pac.policy',
|
|
15
|
+
'app-center-runtime'
|
|
16
|
+
];
|
|
17
|
+
const H2O_MODES = ['app-rule', 'app-global', 'system-tun', 'direct'];
|
|
18
|
+
|
|
19
|
+
let mainWindow = null;
|
|
20
|
+
let launcher = null;
|
|
21
|
+
|
|
22
|
+
let runtime = {
|
|
23
|
+
app: {
|
|
24
|
+
appId: H2O_APP_ID,
|
|
25
|
+
displayName: H2O_DISPLAY_NAME,
|
|
26
|
+
fullName: H2O_FULL_NAME,
|
|
27
|
+
description: H2O_DESCRIPTION,
|
|
28
|
+
packageName: H2O_PACKAGE_NAME,
|
|
29
|
+
version: app.getVersion(),
|
|
30
|
+
launcherMode: 'embed',
|
|
31
|
+
standaloneChannelProductId: 'mx-h2i',
|
|
32
|
+
networkScope: 'broker-session',
|
|
33
|
+
requiredCapabilities: H2O_REQUIRED_CAPABILITIES,
|
|
34
|
+
manifest: null
|
|
35
|
+
},
|
|
36
|
+
broker: {
|
|
37
|
+
state: 'idle',
|
|
38
|
+
ok: false,
|
|
39
|
+
message: 'Not connected',
|
|
40
|
+
session: null,
|
|
41
|
+
channel: null,
|
|
42
|
+
missingCapabilities: []
|
|
43
|
+
},
|
|
44
|
+
policy: {
|
|
45
|
+
mode: 'app-rule',
|
|
46
|
+
pac: 'dynamic-split',
|
|
47
|
+
dns: 'internal-first',
|
|
48
|
+
proxyPort: 23458,
|
|
49
|
+
profile: 'home-to-oversea'
|
|
50
|
+
},
|
|
51
|
+
engine: {
|
|
52
|
+
running: false,
|
|
53
|
+
status: 'stopped',
|
|
54
|
+
mode: 'app-rule',
|
|
55
|
+
tunInstalled: false,
|
|
56
|
+
activeSubscriptionId: 'h2o-default',
|
|
57
|
+
startedAt: null,
|
|
58
|
+
adminUrl: 'http://127.0.0.1:23456',
|
|
59
|
+
core: 'h2o-shim',
|
|
60
|
+
coreVersion: '0.1.0'
|
|
61
|
+
},
|
|
62
|
+
ports: {
|
|
63
|
+
admin: 23456,
|
|
64
|
+
controller: 23457,
|
|
65
|
+
mixed: 23458,
|
|
66
|
+
dns: 1053
|
|
67
|
+
},
|
|
68
|
+
network: {
|
|
69
|
+
localIp: null,
|
|
70
|
+
routePolicy: 'guest limited',
|
|
71
|
+
internalApi: 'pending',
|
|
72
|
+
splitDns: 'pending',
|
|
73
|
+
pac: 'pending'
|
|
74
|
+
},
|
|
75
|
+
subscriptions: [
|
|
76
|
+
{
|
|
77
|
+
id: 'h2o-default',
|
|
78
|
+
name: 'Home To Oversea 默认策略',
|
|
79
|
+
url: 'mx-h2i://managed/home-to-oversea',
|
|
80
|
+
nodes: 6,
|
|
81
|
+
latencyMs: 42,
|
|
82
|
+
status: 'ready',
|
|
83
|
+
lastUpdatedAt: new Date().toISOString()
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
rules: [
|
|
87
|
+
{ id: 'internal-api', host: 'api.mxinfo-inc.cn', target: '10.88.88.88', policy: 'internal-direct', enabled: true, source: 'builtin' },
|
|
88
|
+
{ id: 'appcenter', host: 'appcenter.mxinfo-inc.cn', target: 'mx-h2i broker', policy: 'broker-session', enabled: true, source: 'builtin' },
|
|
89
|
+
{ id: 'oversea-default', host: '*.oversea', target: 'system proxy', policy: 'home-to-oversea', enabled: true, source: 'managed' }
|
|
90
|
+
],
|
|
91
|
+
metrics: {
|
|
92
|
+
uploadBytes: 0,
|
|
93
|
+
downloadBytes: 0,
|
|
94
|
+
lastProxyAppliedAt: null
|
|
95
|
+
},
|
|
96
|
+
activity: [],
|
|
97
|
+
updatedAt: new Date().toISOString()
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
app.whenReady().then(() => {
|
|
101
|
+
registerIpc();
|
|
102
|
+
createWindow();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
app.on('window-all-closed', () => {
|
|
106
|
+
if (process.platform !== 'darwin') app.quit();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
app.on('activate', () => {
|
|
110
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
function createWindow() {
|
|
114
|
+
mainWindow = new BrowserWindow({
|
|
115
|
+
width: 1280,
|
|
116
|
+
height: 820,
|
|
117
|
+
minWidth: 980,
|
|
118
|
+
minHeight: 680,
|
|
119
|
+
title: 'H2O',
|
|
120
|
+
backgroundColor: '#141417',
|
|
121
|
+
webPreferences: {
|
|
122
|
+
preload: path.join(__dirname, 'preload.cjs'),
|
|
123
|
+
contextIsolation: true,
|
|
124
|
+
nodeIntegration: false
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
mainWindow.loadFile(path.join(__dirname, 'index.html'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function registerIpc() {
|
|
131
|
+
ipcMain.handle('h2o:get-state', async () => visibleRuntime());
|
|
132
|
+
ipcMain.handle('h2o:window-control', (_event, action) => {
|
|
133
|
+
if (!mainWindow || mainWindow.isDestroyed()) return false;
|
|
134
|
+
if (action === 'close') {
|
|
135
|
+
mainWindow.close();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (action === 'minimize') {
|
|
139
|
+
mainWindow.minimize();
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (action === 'zoom') {
|
|
143
|
+
if (mainWindow.isMaximized()) mainWindow.unmaximize();
|
|
144
|
+
else mainWindow.maximize();
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
});
|
|
149
|
+
ipcMain.handle('h2o:connect-broker', async () => {
|
|
150
|
+
await connectBroker();
|
|
151
|
+
return visibleRuntime();
|
|
152
|
+
});
|
|
153
|
+
ipcMain.handle('h2o:refresh', async () => {
|
|
154
|
+
await refreshBrokerNetwork();
|
|
155
|
+
return visibleRuntime();
|
|
156
|
+
});
|
|
157
|
+
ipcMain.handle('h2o:set-mode', async (_event, mode) => {
|
|
158
|
+
const nextMode = normalizeH2oMode(mode);
|
|
159
|
+
runtime.policy.mode = nextMode;
|
|
160
|
+
runtime.engine.mode = nextMode;
|
|
161
|
+
runtime.engine.status = nextMode === 'system-tun' && !runtime.engine.tunInstalled
|
|
162
|
+
? 'tun-required'
|
|
163
|
+
: runtime.engine.running
|
|
164
|
+
? 'running'
|
|
165
|
+
: 'ready';
|
|
166
|
+
pushActivity('policy.mode', `Mode switched to ${modeLabel(nextMode)}`);
|
|
167
|
+
if (runtime.engine.running) {
|
|
168
|
+
await applyProxyPolicy('mode-switch');
|
|
169
|
+
}
|
|
170
|
+
broadcast();
|
|
171
|
+
return visibleRuntime();
|
|
172
|
+
});
|
|
173
|
+
ipcMain.handle('h2o:start-runtime', async () => {
|
|
174
|
+
await startRuntime();
|
|
175
|
+
return visibleRuntime();
|
|
176
|
+
});
|
|
177
|
+
ipcMain.handle('h2o:stop-runtime', async () => {
|
|
178
|
+
runtime.engine.running = false;
|
|
179
|
+
runtime.engine.status = 'stopped';
|
|
180
|
+
runtime.engine.startedAt = null;
|
|
181
|
+
pushActivity('runtime.stop', 'H2O proxy runtime stopped.');
|
|
182
|
+
await requestBroker('network.proxy', { mode: 'direct', reason: 'h2o-stop' });
|
|
183
|
+
broadcast();
|
|
184
|
+
return visibleRuntime();
|
|
185
|
+
});
|
|
186
|
+
ipcMain.handle('h2o:install-tun', async () => {
|
|
187
|
+
runtime.engine.tunInstalled = true;
|
|
188
|
+
runtime.engine.status = runtime.engine.running ? 'running' : 'ready';
|
|
189
|
+
pushActivity('tun.install', 'System TUN helper marked as installed for this demo runtime.');
|
|
190
|
+
broadcast();
|
|
191
|
+
return visibleRuntime();
|
|
192
|
+
});
|
|
193
|
+
ipcMain.handle('h2o:uninstall-tun', async () => {
|
|
194
|
+
runtime.engine.tunInstalled = false;
|
|
195
|
+
if (runtime.policy.mode === 'system-tun') {
|
|
196
|
+
runtime.policy.mode = 'app-rule';
|
|
197
|
+
runtime.engine.mode = 'app-rule';
|
|
198
|
+
}
|
|
199
|
+
runtime.engine.status = runtime.engine.running ? 'running' : 'ready';
|
|
200
|
+
pushActivity('tun.uninstall', 'System TUN helper removed; mode fell back to App rule.');
|
|
201
|
+
broadcast();
|
|
202
|
+
return visibleRuntime();
|
|
203
|
+
});
|
|
204
|
+
ipcMain.handle('h2o:set-ports', async (_event, input) => {
|
|
205
|
+
const next = input && typeof input === 'object' ? input : {};
|
|
206
|
+
runtime.ports = {
|
|
207
|
+
admin: normalizePort(next.admin, runtime.ports.admin),
|
|
208
|
+
controller: normalizePort(next.controller, runtime.ports.controller),
|
|
209
|
+
mixed: normalizePort(next.mixed, runtime.ports.mixed),
|
|
210
|
+
dns: normalizePort(next.dns, runtime.ports.dns)
|
|
211
|
+
};
|
|
212
|
+
runtime.policy.proxyPort = runtime.ports.mixed;
|
|
213
|
+
runtime.engine.adminUrl = `http://127.0.0.1:${runtime.ports.admin}`;
|
|
214
|
+
pushActivity('ports.save', `Ports saved: mixed ${runtime.ports.mixed}, dns ${runtime.ports.dns}.`);
|
|
215
|
+
broadcast();
|
|
216
|
+
return visibleRuntime();
|
|
217
|
+
});
|
|
218
|
+
ipcMain.handle('h2o:add-subscription', async (_event, input) => {
|
|
219
|
+
const row = input && typeof input === 'object' ? input : {};
|
|
220
|
+
const name = String(row.name || 'Managed subscription').trim().slice(0, 80) || 'Managed subscription';
|
|
221
|
+
const url = String(row.url || 'mx-h2i://managed/custom').trim().slice(0, 240) || 'mx-h2i://managed/custom';
|
|
222
|
+
const id = `sub-${Date.now().toString(36)}`;
|
|
223
|
+
runtime.subscriptions = [
|
|
224
|
+
{ id, name, url, nodes: 3, latencyMs: 58, status: 'ready', lastUpdatedAt: new Date().toISOString() },
|
|
225
|
+
...runtime.subscriptions
|
|
226
|
+
].slice(0, 12);
|
|
227
|
+
runtime.engine.activeSubscriptionId = id;
|
|
228
|
+
pushActivity('subscription.add', `Subscription added: ${name}.`);
|
|
229
|
+
broadcast();
|
|
230
|
+
return visibleRuntime();
|
|
231
|
+
});
|
|
232
|
+
ipcMain.handle('h2o:set-active-subscription', async (_event, subscriptionId) => {
|
|
233
|
+
const id = String(subscriptionId || '');
|
|
234
|
+
if (runtime.subscriptions.some((item) => item.id === id)) {
|
|
235
|
+
runtime.engine.activeSubscriptionId = id;
|
|
236
|
+
pushActivity('subscription.active', `Active subscription switched to ${activeSubscription()?.name || id}.`);
|
|
237
|
+
}
|
|
238
|
+
broadcast();
|
|
239
|
+
return visibleRuntime();
|
|
240
|
+
});
|
|
241
|
+
ipcMain.handle('h2o:refresh-subscription', async (_event, subscriptionId) => {
|
|
242
|
+
const id = String(subscriptionId || runtime.engine.activeSubscriptionId || '');
|
|
243
|
+
runtime.subscriptions = runtime.subscriptions.map((item) => item.id === id
|
|
244
|
+
? {
|
|
245
|
+
...item,
|
|
246
|
+
latencyMs: 30 + Math.floor(Math.random() * 60),
|
|
247
|
+
status: 'ready',
|
|
248
|
+
lastUpdatedAt: new Date().toISOString()
|
|
249
|
+
}
|
|
250
|
+
: item);
|
|
251
|
+
pushActivity('subscription.refresh', `Subscription refreshed: ${activeSubscription()?.name || id}.`);
|
|
252
|
+
broadcast();
|
|
253
|
+
return visibleRuntime();
|
|
254
|
+
});
|
|
255
|
+
ipcMain.handle('h2o:toggle-rule', async (_event, ruleId) => {
|
|
256
|
+
const id = String(ruleId || '');
|
|
257
|
+
runtime.rules = runtime.rules.map((rule) => rule.id === id ? { ...rule, enabled: rule.enabled === false } : rule);
|
|
258
|
+
const rule = runtime.rules.find((item) => item.id === id);
|
|
259
|
+
if (rule) pushActivity('rule.toggle', `${rule.host} ${rule.enabled ? 'enabled' : 'disabled'}.`);
|
|
260
|
+
broadcast();
|
|
261
|
+
return visibleRuntime();
|
|
262
|
+
});
|
|
263
|
+
ipcMain.handle('h2o:request-broker', async (_event, name, payload) => {
|
|
264
|
+
const result = await requestBroker(String(name || ''), payload);
|
|
265
|
+
return { state: visibleRuntime(), result };
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function connectBroker() {
|
|
270
|
+
runtime.broker = {
|
|
271
|
+
...runtime.broker,
|
|
272
|
+
state: 'discovering-broker',
|
|
273
|
+
ok: false,
|
|
274
|
+
message: 'Discovering MX-H2I broker channel...'
|
|
275
|
+
};
|
|
276
|
+
broadcast();
|
|
277
|
+
try {
|
|
278
|
+
const mod = await import('@qpjoy/electron-launcher');
|
|
279
|
+
const manifest = typeof mod.createLauncherEmbedManifest === 'function'
|
|
280
|
+
? mod.createLauncherEmbedManifest({
|
|
281
|
+
appId: H2O_APP_ID,
|
|
282
|
+
productId: H2O_APP_ID,
|
|
283
|
+
displayName: H2O_DISPLAY_NAME,
|
|
284
|
+
description: H2O_DESCRIPTION,
|
|
285
|
+
packageName: H2O_PACKAGE_NAME,
|
|
286
|
+
category: 'network',
|
|
287
|
+
appVersion: runtime.app.version,
|
|
288
|
+
standaloneChannelProductId: runtime.app.standaloneChannelProductId,
|
|
289
|
+
requiredCapabilities: H2O_REQUIRED_CAPABILITIES
|
|
290
|
+
})
|
|
291
|
+
: null;
|
|
292
|
+
runtime.app = {
|
|
293
|
+
...runtime.app,
|
|
294
|
+
manifest,
|
|
295
|
+
requiredCapabilities: manifest?.requiredCapabilities || H2O_REQUIRED_CAPABILITIES
|
|
296
|
+
};
|
|
297
|
+
launcher = mod.createElectronLauncher({
|
|
298
|
+
mode: 'embed',
|
|
299
|
+
appId: runtime.app.appId,
|
|
300
|
+
productId: runtime.app.appId,
|
|
301
|
+
displayName: runtime.app.displayName,
|
|
302
|
+
description: runtime.app.description,
|
|
303
|
+
packageName: runtime.app.packageName,
|
|
304
|
+
category: 'network',
|
|
305
|
+
standaloneChannelProductId: runtime.app.standaloneChannelProductId,
|
|
306
|
+
appVersion: runtime.app.version,
|
|
307
|
+
requiredCapabilities: runtime.app.requiredCapabilities,
|
|
308
|
+
channelRegistry: () => devChannelRegistry(mod),
|
|
309
|
+
requestImpl: brokerRequest
|
|
310
|
+
});
|
|
311
|
+
if (launcher.manifest) {
|
|
312
|
+
runtime.app.manifest = launcher.manifest;
|
|
313
|
+
}
|
|
314
|
+
const result = await launcher.connect({
|
|
315
|
+
installId: 'h2o-dev-install',
|
|
316
|
+
deviceId: 'h2o-dev-device',
|
|
317
|
+
userId: 'developer'
|
|
318
|
+
});
|
|
319
|
+
runtime.broker = serializeBrokerResult(result);
|
|
320
|
+
if (result.ok) {
|
|
321
|
+
await refreshBrokerNetwork();
|
|
322
|
+
pushActivity('broker.connected', result.message);
|
|
323
|
+
} else {
|
|
324
|
+
pushActivity('broker.blocked', result.message);
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
runtime.broker = {
|
|
328
|
+
state: 'blocked',
|
|
329
|
+
ok: false,
|
|
330
|
+
message: error && error.message ? error.message : String(error),
|
|
331
|
+
session: null,
|
|
332
|
+
channel: null,
|
|
333
|
+
missingCapabilities: []
|
|
334
|
+
};
|
|
335
|
+
pushActivity('broker.error', runtime.broker.message);
|
|
336
|
+
}
|
|
337
|
+
broadcast();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function refreshBrokerNetwork() {
|
|
341
|
+
const status = await requestBroker('network.status', { reason: 'manual-refresh' });
|
|
342
|
+
if (status && typeof status === 'object') {
|
|
343
|
+
runtime.network = {
|
|
344
|
+
...runtime.network,
|
|
345
|
+
...status
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
runtime.updatedAt = new Date().toISOString();
|
|
349
|
+
broadcast();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function requestBroker(name, payload) {
|
|
353
|
+
if (!launcher || !runtime.broker.ok) {
|
|
354
|
+
return brokerRequest(runtime.broker.session, name, payload);
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
return await launcher.request(name, payload);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const message = error && error.message ? error.message : String(error);
|
|
360
|
+
pushActivity('broker.request.failed', `${name}: ${message}`);
|
|
361
|
+
return { ok: false, message };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function brokerRequest(_session, name, payload) {
|
|
366
|
+
if (name === 'network.status') {
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
localIp: '10.89.100.12',
|
|
370
|
+
routePolicy: runtime.network.routePolicy,
|
|
371
|
+
internalApi: 'ready',
|
|
372
|
+
splitDns: runtime.policy.dns,
|
|
373
|
+
pac: runtime.policy.pac,
|
|
374
|
+
profile: runtime.policy.profile,
|
|
375
|
+
networkScope: runtime.app.networkScope,
|
|
376
|
+
standaloneChannelProductId: runtime.app.standaloneChannelProductId
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (name === 'network.proxy') {
|
|
380
|
+
return {
|
|
381
|
+
ok: true,
|
|
382
|
+
mode: payload && payload.mode ? payload.mode : runtime.policy.mode,
|
|
383
|
+
mixedPort: runtime.ports.mixed,
|
|
384
|
+
dnsPort: runtime.ports.dns,
|
|
385
|
+
profile: runtime.policy.profile,
|
|
386
|
+
appliedAt: new Date().toISOString()
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (name === 'network.dns.policy' || name === 'network.pac.policy') {
|
|
390
|
+
return { ok: true, policy: name, appliedAt: new Date().toISOString() };
|
|
391
|
+
}
|
|
392
|
+
if (name === 'user.session') {
|
|
393
|
+
return { ok: true, userId: 'developer', roles: ['mx-h2i-dev'] };
|
|
394
|
+
}
|
|
395
|
+
return { ok: true, name, echo: payload || null };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function startRuntime() {
|
|
399
|
+
const nextMode = normalizeH2oMode(runtime.policy.mode);
|
|
400
|
+
runtime.policy.mode = nextMode;
|
|
401
|
+
runtime.engine.mode = nextMode;
|
|
402
|
+
if (nextMode === 'system-tun' && !runtime.engine.tunInstalled) {
|
|
403
|
+
runtime.engine.running = false;
|
|
404
|
+
runtime.engine.status = 'tun-required';
|
|
405
|
+
pushActivity('runtime.blocked', 'System TUN mode requires installing the TUN helper first.', 'warning');
|
|
406
|
+
broadcast();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
runtime.engine.running = true;
|
|
410
|
+
runtime.engine.status = 'running';
|
|
411
|
+
runtime.engine.startedAt = runtime.engine.startedAt || new Date().toISOString();
|
|
412
|
+
runtime.metrics.lastProxyAppliedAt = new Date().toISOString();
|
|
413
|
+
runtime.metrics.downloadBytes += 1024 * (4 + Math.floor(Math.random() * 24));
|
|
414
|
+
runtime.metrics.uploadBytes += 512 * (2 + Math.floor(Math.random() * 10));
|
|
415
|
+
pushActivity('runtime.start', `H2O started in ${modeLabel(nextMode)} with ${activeSubscription()?.name || 'no subscription'}.`);
|
|
416
|
+
await applyProxyPolicy('runtime-start');
|
|
417
|
+
broadcast();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function applyProxyPolicy(reason) {
|
|
421
|
+
const result = await requestBroker('network.proxy', {
|
|
422
|
+
mode: runtime.policy.mode,
|
|
423
|
+
mixedPort: runtime.ports.mixed,
|
|
424
|
+
dnsPort: runtime.ports.dns,
|
|
425
|
+
profile: runtime.policy.profile,
|
|
426
|
+
subscriptionId: runtime.engine.activeSubscriptionId,
|
|
427
|
+
reason
|
|
428
|
+
});
|
|
429
|
+
runtime.metrics.lastProxyAppliedAt = new Date().toISOString();
|
|
430
|
+
if (result?.ok === false) {
|
|
431
|
+
runtime.engine.status = 'error';
|
|
432
|
+
pushActivity('broker.proxy.failed', result.message || 'Broker rejected proxy policy.', 'error');
|
|
433
|
+
} else {
|
|
434
|
+
pushActivity('broker.proxy.applied', `Proxy policy applied by MX-H2I broker (${reason}).`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function activeSubscription() {
|
|
439
|
+
return runtime.subscriptions.find((item) => item.id === runtime.engine.activeSubscriptionId) || runtime.subscriptions[0] || null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function normalizeH2oMode(mode) {
|
|
443
|
+
const value = String(mode || '').trim();
|
|
444
|
+
if (value === 'rule') return 'app-rule';
|
|
445
|
+
if (value === 'global') return 'app-global';
|
|
446
|
+
if (value === 'tun') return 'system-tun';
|
|
447
|
+
return H2O_MODES.includes(value) ? value : 'app-rule';
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function normalizePort(value, fallback) {
|
|
451
|
+
const port = Number(value);
|
|
452
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function modeLabel(mode) {
|
|
456
|
+
if (mode === 'app-global') return '全局模式';
|
|
457
|
+
if (mode === 'system-tun') return '虚拟网卡';
|
|
458
|
+
if (mode === 'direct') return '直连';
|
|
459
|
+
return '规则模式';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function devChannelRegistry(mod) {
|
|
463
|
+
if (process.env.MX_H2O_BROKER_MODE === 'off') return [];
|
|
464
|
+
const now = new Date().toISOString();
|
|
465
|
+
return [{
|
|
466
|
+
productId: 'mx-h2i',
|
|
467
|
+
instanceId: 'mx-h2i-dev-broker',
|
|
468
|
+
pid: process.pid,
|
|
469
|
+
socketPath: `${app.getPath('userData')}/mx-h2i-dev.sock`,
|
|
470
|
+
brokerAbiVersion: mod.brokerAbiVersion || '1.0',
|
|
471
|
+
protocolVersion: mod.launcherProtocolVersion || '1.0',
|
|
472
|
+
capabilities: [
|
|
473
|
+
...H2O_REQUIRED_CAPABILITIES,
|
|
474
|
+
'app-center-runtime',
|
|
475
|
+
'observability.write'
|
|
476
|
+
].filter((capability, index, rows) => rows.indexOf(capability) === index),
|
|
477
|
+
heartbeatAt: now,
|
|
478
|
+
displayName: 'MX-H2I Dev Broker'
|
|
479
|
+
}];
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function serializeBrokerResult(result) {
|
|
483
|
+
return {
|
|
484
|
+
state: result.state,
|
|
485
|
+
ok: result.ok,
|
|
486
|
+
message: result.message,
|
|
487
|
+
session: result.session ? {
|
|
488
|
+
sessionId: result.session.sessionId,
|
|
489
|
+
appId: result.session.appId,
|
|
490
|
+
networkScope: result.session.networkScope,
|
|
491
|
+
standaloneChannelProductId: result.session.standaloneChannelProductId,
|
|
492
|
+
grantedCapabilities: result.session.grantedCapabilities,
|
|
493
|
+
issuedAt: result.session.issuedAt
|
|
494
|
+
} : null,
|
|
495
|
+
channel: result.channel ? {
|
|
496
|
+
productId: result.channel.productId,
|
|
497
|
+
displayName: result.channel.displayName,
|
|
498
|
+
socketPath: result.channel.socketPath,
|
|
499
|
+
capabilities: result.channel.capabilities,
|
|
500
|
+
heartbeatAt: result.channel.heartbeatAt
|
|
501
|
+
} : null,
|
|
502
|
+
missingCapabilities: result.missingCapabilities || []
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function pushActivity(type, message, level = 'info') {
|
|
507
|
+
runtime.activity = [
|
|
508
|
+
{ type, message, level, at: new Date().toISOString() },
|
|
509
|
+
...runtime.activity
|
|
510
|
+
].slice(0, 12);
|
|
511
|
+
runtime.updatedAt = new Date().toISOString();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function visibleRuntime() {
|
|
515
|
+
return JSON.parse(JSON.stringify(runtime));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function broadcast() {
|
|
519
|
+
if (!mainWindow || mainWindow.isDestroyed()) return;
|
|
520
|
+
mainWindow.webContents.send('h2o:state', visibleRuntime());
|
|
521
|
+
}
|
package/src/preload.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('h2o', {
|
|
4
|
+
getState: () => ipcRenderer.invoke('h2o:get-state'),
|
|
5
|
+
connectBroker: () => ipcRenderer.invoke('h2o:connect-broker'),
|
|
6
|
+
refresh: () => ipcRenderer.invoke('h2o:refresh'),
|
|
7
|
+
setMode: (mode) => ipcRenderer.invoke('h2o:set-mode', mode),
|
|
8
|
+
startRuntime: () => ipcRenderer.invoke('h2o:start-runtime'),
|
|
9
|
+
stopRuntime: () => ipcRenderer.invoke('h2o:stop-runtime'),
|
|
10
|
+
installTun: () => ipcRenderer.invoke('h2o:install-tun'),
|
|
11
|
+
uninstallTun: () => ipcRenderer.invoke('h2o:uninstall-tun'),
|
|
12
|
+
setPorts: (input) => ipcRenderer.invoke('h2o:set-ports', input),
|
|
13
|
+
addSubscription: (input) => ipcRenderer.invoke('h2o:add-subscription', input),
|
|
14
|
+
setActiveSubscription: (subscriptionId) => ipcRenderer.invoke('h2o:set-active-subscription', subscriptionId),
|
|
15
|
+
refreshSubscription: (subscriptionId) => ipcRenderer.invoke('h2o:refresh-subscription', subscriptionId),
|
|
16
|
+
toggleRule: (ruleId) => ipcRenderer.invoke('h2o:toggle-rule', ruleId),
|
|
17
|
+
requestBroker: (name, payload) => ipcRenderer.invoke('h2o:request-broker', name, payload),
|
|
18
|
+
windowControl: (action) => ipcRenderer.invoke('h2o:window-control', action),
|
|
19
|
+
onState: (listener) => {
|
|
20
|
+
if (typeof listener !== 'function') return () => {};
|
|
21
|
+
const wrapped = (_event, payload) => listener(payload);
|
|
22
|
+
ipcRenderer.on('h2o:state', wrapped);
|
|
23
|
+
return () => ipcRenderer.removeListener('h2o:state', wrapped);
|
|
24
|
+
}
|
|
25
|
+
});
|