@walkhi/code-relax 0.1.0-beta.66 → 0.1.0-beta.68
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/bin/codex-remote.mjs +4 -2
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-harmony-release.mjs +46 -46
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/bin/set-relay-admin-password.mjs +11 -11
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/shared/app-server-events.cjs +511 -511
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-ice-policy.cjs +22 -22
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -69
- package/dist/shared/release-version.cjs +31 -31
- package/dist/src/archived-threads.mjs +22 -22
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +62 -62
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +19 -19
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-permission-mode.mjs +17 -17
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/file-opener.mjs +9 -9
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -56
- package/dist/src/platform/linux/desktop-installation.mjs +36 -36
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/linux/network-info.mjs +11 -11
- package/dist/src/platform/linux/project-locations.mjs +17 -17
- package/dist/src/platform/linux/resident-startup.mjs +52 -52
- package/dist/src/platform/linux/retained-runtime.mjs +76 -76
- package/dist/src/platform/linux/service-host.mjs +18 -18
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/macos/network-info.mjs +8 -8
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -23
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/power-inhibitor.mjs +22 -22
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
- package/dist/src/platform/windows/keep-awake.ps1 +20 -20
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-create.mjs +121 -121
- package/dist/src/project-management.mjs +1 -1
- package/dist/src/project-remove.mjs +55 -55
- package/dist/src/project-update.mjs +59 -59
- package/dist/src/relay-config-import.mjs +45 -45
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-auth.mjs +59 -59
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-login-page.mjs +7 -7
- package/dist/src/self-relay/admin-server.mjs +105 -105
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
- package/dist/src/self-relay/admin-workstations.mjs +143 -143
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connection-reporting.mjs +38 -38
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +6 -6
- package/dist/src/self-relay/device-info.mjs +27 -27
- package/dist/src/self-relay/device-store.mjs +92 -92
- package/dist/src/self-relay/lan-authorizations.mjs +71 -71
- package/dist/src/self-relay/lifecycle.mjs +278 -278
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -118
- package/dist/src/self-relay/phone-connections.mjs +55 -55
- package/dist/src/self-relay/server.mjs +1066 -1066
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +1947 -1947
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-power.mjs +58 -58
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/service-update-worker.mjs +42 -42
- package/dist/src/service-update.mjs +80 -80
- package/dist/src/shared-recovery.mjs +152 -152
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +4 -4
- package/tools/postinstall.mjs +1 -1
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
|
|
3
|
-
const prefix = '/code-relax/android/';
|
|
4
|
-
const webOrigin = 'https://www.coderelax.cn';
|
|
5
|
-
|
|
6
|
-
export function readAndroidCatalog(file) {
|
|
7
|
-
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
8
|
-
if (!Array.isArray(data.releases) || !data.releases.length) throw new Error('No Android releases');
|
|
9
|
-
const versions = new Set();
|
|
10
|
-
for (const item of data.releases) {
|
|
11
|
-
if (!Number.isSafeInteger(item.versionCode) || item.versionCode < 1 || item.versionCode > 2100000000
|
|
12
|
-
|| versions.has(item.versionCode) || typeof item.versionName !== 'string' || !/^\d+\.\d+\.\d+\.\d+$/.test(item.versionName)
|
|
13
|
-
|| !item.versionName.endsWith('.' + item.versionCode)
|
|
14
|
-
|| !Number.isSafeInteger(item.size) || item.size <= 0 || !/^[a-f0-9]{64}$/.test(item.sha256)
|
|
15
|
-
|| typeof item.objectKey !== 'string' || !/^code-relax\/android\/\d+\/[A-Za-z0-9._-]+\.apk$/.test(item.objectKey)
|
|
16
|
-
|| typeof item.notes !== 'string' || item.notes.length > 10000) throw new Error('Invalid Android release catalog');
|
|
17
|
-
versions.add(item.versionCode);
|
|
18
|
-
}
|
|
19
|
-
if (!versions.has(data.latestVersionCode)) throw new Error('Latest Android release is missing');
|
|
20
|
-
return data;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createAndroidReleaseHandler({ catalogFile, signDownload, log = () => {} }) {
|
|
24
|
-
return async function handle(request, response, target) {
|
|
25
|
-
if (!target.pathname.startsWith(prefix)) return false;
|
|
26
|
-
const headers = { 'Cache-Control': 'no-store', 'Content-Type': 'application/json; charset=utf-8',
|
|
27
|
-
'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer',
|
|
28
|
-
'Access-Control-Allow-Origin': webOrigin, Vary: 'Origin' };
|
|
29
|
-
const reply = (code, data) => { response.writeHead(code, headers); response.end(JSON.stringify(data)); };
|
|
30
|
-
if (request.method !== 'GET') { reply(405, { error: 'Method not allowed' }); return true; }
|
|
31
|
-
if (![prefix + 'latest.json', prefix + 'download'].includes(target.pathname)) {
|
|
32
|
-
reply(404, { error: 'Not found' }); return true;
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
if (!catalogFile || !signDownload) { reply(503, { error: 'Android release is not available' }); return true; }
|
|
36
|
-
const catalog = readAndroidCatalog(catalogFile);
|
|
37
|
-
const requested = target.searchParams.get('versionCode');
|
|
38
|
-
if (requested !== null && !/^[1-9]\d{0,9}$/.test(requested)) { reply(400, { error: 'Invalid version' }); return true; }
|
|
39
|
-
const code = target.pathname.endsWith('/latest.json') || requested === null ? catalog.latestVersionCode : Number(requested);
|
|
40
|
-
const release = catalog.releases.find(item => item.versionCode === code);
|
|
41
|
-
if (!release) { reply(404, { error: 'Release unavailable; check for updates again' }); return true; }
|
|
42
|
-
const { versionCode, versionName, size, sha256, notes } = release;
|
|
43
|
-
if (target.pathname.endsWith('/latest.json')) {
|
|
44
|
-
reply(200, { versionCode, versionName, size, sha256, notes }); return true;
|
|
45
|
-
}
|
|
46
|
-
// Sign only an explicitly published object, never a caller-supplied key or URL.
|
|
47
|
-
const ticket = await signDownload(release);
|
|
48
|
-
if (!ticket || new URL(ticket.url).protocol !== 'https:' || !Number.isSafeInteger(ticket.expiresAt) || ticket.expiresAt <= Date.now()) {
|
|
49
|
-
throw new Error('Invalid download ticket');
|
|
50
|
-
}
|
|
51
|
-
if (target.searchParams.get('redirect') === '1') {
|
|
52
|
-
response.writeHead(302, { ...headers, Location: ticket.url }); response.end();
|
|
53
|
-
} else reply(200, { versionCode, url: ticket.url, expiresAt: ticket.expiresAt });
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Provider errors may contain signed URLs or credentials. Never return or log them.
|
|
56
|
-
log({ event: 'android-release-error', type: error?.code === 'ENOENT' ? 'not-published' : 'configuration-or-storage' });
|
|
57
|
-
reply(503, { error: 'Android release is temporarily unavailable' });
|
|
58
|
-
}
|
|
59
|
-
return true;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
const prefix = '/code-relax/android/';
|
|
4
|
+
const webOrigin = 'https://www.coderelax.cn';
|
|
5
|
+
|
|
6
|
+
export function readAndroidCatalog(file) {
|
|
7
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
8
|
+
if (!Array.isArray(data.releases) || !data.releases.length) throw new Error('No Android releases');
|
|
9
|
+
const versions = new Set();
|
|
10
|
+
for (const item of data.releases) {
|
|
11
|
+
if (!Number.isSafeInteger(item.versionCode) || item.versionCode < 1 || item.versionCode > 2100000000
|
|
12
|
+
|| versions.has(item.versionCode) || typeof item.versionName !== 'string' || !/^\d+\.\d+\.\d+\.\d+$/.test(item.versionName)
|
|
13
|
+
|| !item.versionName.endsWith('.' + item.versionCode)
|
|
14
|
+
|| !Number.isSafeInteger(item.size) || item.size <= 0 || !/^[a-f0-9]{64}$/.test(item.sha256)
|
|
15
|
+
|| typeof item.objectKey !== 'string' || !/^code-relax\/android\/\d+\/[A-Za-z0-9._-]+\.apk$/.test(item.objectKey)
|
|
16
|
+
|| typeof item.notes !== 'string' || item.notes.length > 10000) throw new Error('Invalid Android release catalog');
|
|
17
|
+
versions.add(item.versionCode);
|
|
18
|
+
}
|
|
19
|
+
if (!versions.has(data.latestVersionCode)) throw new Error('Latest Android release is missing');
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createAndroidReleaseHandler({ catalogFile, signDownload, log = () => {} }) {
|
|
24
|
+
return async function handle(request, response, target) {
|
|
25
|
+
if (!target.pathname.startsWith(prefix)) return false;
|
|
26
|
+
const headers = { 'Cache-Control': 'no-store', 'Content-Type': 'application/json; charset=utf-8',
|
|
27
|
+
'X-Content-Type-Options': 'nosniff', 'Referrer-Policy': 'no-referrer',
|
|
28
|
+
'Access-Control-Allow-Origin': webOrigin, Vary: 'Origin' };
|
|
29
|
+
const reply = (code, data) => { response.writeHead(code, headers); response.end(JSON.stringify(data)); };
|
|
30
|
+
if (request.method !== 'GET') { reply(405, { error: 'Method not allowed' }); return true; }
|
|
31
|
+
if (![prefix + 'latest.json', prefix + 'download'].includes(target.pathname)) {
|
|
32
|
+
reply(404, { error: 'Not found' }); return true;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if (!catalogFile || !signDownload) { reply(503, { error: 'Android release is not available' }); return true; }
|
|
36
|
+
const catalog = readAndroidCatalog(catalogFile);
|
|
37
|
+
const requested = target.searchParams.get('versionCode');
|
|
38
|
+
if (requested !== null && !/^[1-9]\d{0,9}$/.test(requested)) { reply(400, { error: 'Invalid version' }); return true; }
|
|
39
|
+
const code = target.pathname.endsWith('/latest.json') || requested === null ? catalog.latestVersionCode : Number(requested);
|
|
40
|
+
const release = catalog.releases.find(item => item.versionCode === code);
|
|
41
|
+
if (!release) { reply(404, { error: 'Release unavailable; check for updates again' }); return true; }
|
|
42
|
+
const { versionCode, versionName, size, sha256, notes } = release;
|
|
43
|
+
if (target.pathname.endsWith('/latest.json')) {
|
|
44
|
+
reply(200, { versionCode, versionName, size, sha256, notes }); return true;
|
|
45
|
+
}
|
|
46
|
+
// Sign only an explicitly published object, never a caller-supplied key or URL.
|
|
47
|
+
const ticket = await signDownload(release);
|
|
48
|
+
if (!ticket || new URL(ticket.url).protocol !== 'https:' || !Number.isSafeInteger(ticket.expiresAt) || ticket.expiresAt <= Date.now()) {
|
|
49
|
+
throw new Error('Invalid download ticket');
|
|
50
|
+
}
|
|
51
|
+
if (target.searchParams.get('redirect') === '1') {
|
|
52
|
+
response.writeHead(302, { ...headers, Location: ticket.url }); response.end();
|
|
53
|
+
} else reply(200, { versionCode, url: ticket.url, expiresAt: ticket.expiresAt });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Provider errors may contain signed URLs or credentials. Never return or log them.
|
|
56
|
+
log({ event: 'android-release-error', type: error?.code === 'ENOENT' ? 'not-published' : 'configuration-or-storage' });
|
|
57
|
+
reply(503, { error: 'Android release is temporarily unavailable' });
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events';
|
|
2
|
-
import WebSocket from 'ws';
|
|
3
|
-
import { decode, relayAddress, send } from './wire.mjs';
|
|
4
|
-
|
|
5
|
-
// Headless mobile-side adapter for the local validation. No OpenAI credentials.
|
|
6
|
-
export function connectRelayClient({ relayUrl, room, code, credential, clientId, accountToken, observe = false }) {
|
|
7
|
-
relayAddress(relayUrl);
|
|
8
|
-
const events = new EventEmitter(), pending = new Map();
|
|
9
|
-
const socket = new WebSocket(relayUrl, { maxPayload: 64 * 1024 * 1024, perMessageDeflate: true });
|
|
10
|
-
let serial = 0, connected = false, pairedCredential = credential, pairedClientId = clientId;
|
|
11
|
-
const ready = new Promise((resolve, reject) => {
|
|
12
|
-
const timer = setTimeout(() => { reject(new Error('Relay connection timeout')); socket.terminate(); }, 7000);
|
|
13
|
-
socket.on('open', () => send(socket, observe ? { type: 'observe', room, credential, clientId, accountToken }
|
|
14
|
-
: code ? { type: 'pair', room, code, credential, clientId, accountToken } : { type: 'resume', room, credential, clientId, accountToken }));
|
|
15
|
-
socket.on('error', error => { reject(error); events.emit('diagnostic', error.message); });
|
|
16
|
-
socket.on('close', () => {
|
|
17
|
-
clearTimeout(timer); connected = false; reject(new Error('Relay connection closed'));
|
|
18
|
-
for (const request of pending.values()) { clearTimeout(request.timer); request.reject(new Error('连接断开,操作结果未确认,不自动重发')); }
|
|
19
|
-
pending.clear(); events.emit('disconnected');
|
|
20
|
-
});
|
|
21
|
-
socket.on('message', (bytes, binary) => {
|
|
22
|
-
try {
|
|
23
|
-
const message = decode(bytes, binary);
|
|
24
|
-
if (message.type === 'paired') { if (message.credential) pairedCredential = message.credential; if (message.clientId) pairedClientId = message.clientId; }
|
|
25
|
-
else if (message.type === 'observing') { clearTimeout(timer); connected = true; events.emit('observing', message); resolve({ room, credential: pairedCredential, clientId: pairedClientId }); }
|
|
26
|
-
else if (message.type === 'computer-info') events.emit('computer-info', { hostName: message.hostName });
|
|
27
|
-
else if (message.type === 'lan-candidates') events.emit('lan-candidates', message.candidates);
|
|
28
|
-
else if (message.type === 'official-computer') events.emit('official-computer', message.computer);
|
|
29
|
-
else if (message.type === 'official-computer-saved') events.emit('official-computer-saved', message.computer);
|
|
30
|
-
else if (message.type === 'announcement') events.emit('announcement', message.announcement);
|
|
31
|
-
else if (message.type === 'ready') { clearTimeout(timer); connected = true; resolve({ room, credential: pairedCredential, clientId: pairedClientId }); }
|
|
32
|
-
else if (message.type === 'data') {
|
|
33
|
-
const body = decode(message.payload, false), request = pending.get(body.id);
|
|
34
|
-
if (body.type === 'response' && request) { clearTimeout(request.timer); pending.delete(body.id); request.resolve(body); }
|
|
35
|
-
else events.emit('message', body);
|
|
36
|
-
} else throw new Error('Invalid relay message');
|
|
37
|
-
} catch (error) { reject(error); events.emit('diagnostic', error.message); socket.close(1008, 'Invalid response'); }
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
return {
|
|
41
|
-
ready, events,
|
|
42
|
-
request(path, { method = 'GET', body, timeoutMs = 5000 } = {}) {
|
|
43
|
-
if (!connected) return Promise.reject(new Error('Relay not ready'));
|
|
44
|
-
const id = ++serial;
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const timer = setTimeout(() => { pending.delete(id); reject(new Error('请求超时,结果未确认,不自动重发')); }, timeoutMs);
|
|
47
|
-
pending.set(id, { resolve, reject, timer });
|
|
48
|
-
if (!send(socket, { type: 'data', payload: JSON.stringify({ type: 'request', id, path, method, ...(body ? { body } : {}) }) })) {
|
|
49
|
-
clearTimeout(timer); pending.delete(id); reject(new Error('Relay not connected'));
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
},
|
|
53
|
-
send(message) { if (!connected) throw new Error('Relay not ready'); return send(socket, { type: 'data', payload: JSON.stringify(message) }); },
|
|
54
|
-
routeDirect() { if (!connected) throw new Error('Relay not ready'); return send(socket, { type: 'p2p-route', direct: 1 }); },
|
|
55
|
-
recordOfficialComputer(computer) {
|
|
56
|
-
if (!connected || !observe) throw new Error('Metadata channel not ready');
|
|
57
|
-
return send(socket, { type: 'official-computer', ...computer });
|
|
58
|
-
},
|
|
59
|
-
close() { socket.terminate(); },
|
|
60
|
-
};
|
|
61
|
-
}
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { decode, relayAddress, send } from './wire.mjs';
|
|
4
|
+
|
|
5
|
+
// Headless mobile-side adapter for the local validation. No OpenAI credentials.
|
|
6
|
+
export function connectRelayClient({ relayUrl, room, code, credential, clientId, accountToken, observe = false }) {
|
|
7
|
+
relayAddress(relayUrl);
|
|
8
|
+
const events = new EventEmitter(), pending = new Map();
|
|
9
|
+
const socket = new WebSocket(relayUrl, { maxPayload: 64 * 1024 * 1024, perMessageDeflate: true });
|
|
10
|
+
let serial = 0, connected = false, pairedCredential = credential, pairedClientId = clientId;
|
|
11
|
+
const ready = new Promise((resolve, reject) => {
|
|
12
|
+
const timer = setTimeout(() => { reject(new Error('Relay connection timeout')); socket.terminate(); }, 7000);
|
|
13
|
+
socket.on('open', () => send(socket, observe ? { type: 'observe', room, credential, clientId, accountToken }
|
|
14
|
+
: code ? { type: 'pair', room, code, credential, clientId, accountToken } : { type: 'resume', room, credential, clientId, accountToken }));
|
|
15
|
+
socket.on('error', error => { reject(error); events.emit('diagnostic', error.message); });
|
|
16
|
+
socket.on('close', () => {
|
|
17
|
+
clearTimeout(timer); connected = false; reject(new Error('Relay connection closed'));
|
|
18
|
+
for (const request of pending.values()) { clearTimeout(request.timer); request.reject(new Error('连接断开,操作结果未确认,不自动重发')); }
|
|
19
|
+
pending.clear(); events.emit('disconnected');
|
|
20
|
+
});
|
|
21
|
+
socket.on('message', (bytes, binary) => {
|
|
22
|
+
try {
|
|
23
|
+
const message = decode(bytes, binary);
|
|
24
|
+
if (message.type === 'paired') { if (message.credential) pairedCredential = message.credential; if (message.clientId) pairedClientId = message.clientId; }
|
|
25
|
+
else if (message.type === 'observing') { clearTimeout(timer); connected = true; events.emit('observing', message); resolve({ room, credential: pairedCredential, clientId: pairedClientId }); }
|
|
26
|
+
else if (message.type === 'computer-info') events.emit('computer-info', { hostName: message.hostName });
|
|
27
|
+
else if (message.type === 'lan-candidates') events.emit('lan-candidates', message.candidates);
|
|
28
|
+
else if (message.type === 'official-computer') events.emit('official-computer', message.computer);
|
|
29
|
+
else if (message.type === 'official-computer-saved') events.emit('official-computer-saved', message.computer);
|
|
30
|
+
else if (message.type === 'announcement') events.emit('announcement', message.announcement);
|
|
31
|
+
else if (message.type === 'ready') { clearTimeout(timer); connected = true; resolve({ room, credential: pairedCredential, clientId: pairedClientId }); }
|
|
32
|
+
else if (message.type === 'data') {
|
|
33
|
+
const body = decode(message.payload, false), request = pending.get(body.id);
|
|
34
|
+
if (body.type === 'response' && request) { clearTimeout(request.timer); pending.delete(body.id); request.resolve(body); }
|
|
35
|
+
else events.emit('message', body);
|
|
36
|
+
} else throw new Error('Invalid relay message');
|
|
37
|
+
} catch (error) { reject(error); events.emit('diagnostic', error.message); socket.close(1008, 'Invalid response'); }
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
ready, events,
|
|
42
|
+
request(path, { method = 'GET', body, timeoutMs = 5000 } = {}) {
|
|
43
|
+
if (!connected) return Promise.reject(new Error('Relay not ready'));
|
|
44
|
+
const id = ++serial;
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const timer = setTimeout(() => { pending.delete(id); reject(new Error('请求超时,结果未确认,不自动重发')); }, timeoutMs);
|
|
47
|
+
pending.set(id, { resolve, reject, timer });
|
|
48
|
+
if (!send(socket, { type: 'data', payload: JSON.stringify({ type: 'request', id, path, method, ...(body ? { body } : {}) }) })) {
|
|
49
|
+
clearTimeout(timer); pending.delete(id); reject(new Error('Relay not connected'));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
send(message) { if (!connected) throw new Error('Relay not ready'); return send(socket, { type: 'data', payload: JSON.stringify(message) }); },
|
|
54
|
+
routeDirect() { if (!connected) throw new Error('Relay not ready'); return send(socket, { type: 'p2p-route', direct: 1 }); },
|
|
55
|
+
recordOfficialComputer(computer) {
|
|
56
|
+
if (!connected || !observe) throw new Error('Metadata channel not ready');
|
|
57
|
+
return send(socket, { type: 'official-computer', ...computer });
|
|
58
|
+
},
|
|
59
|
+
close() { socket.terminate(); },
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
|
|
5
|
-
const reporters = new Map();
|
|
6
|
-
|
|
7
|
-
// Workstation observations only. The bounded in-memory outbox covers cloud
|
|
8
|
-
// reconnects, not process exit; no polling or message payloads are involved.
|
|
9
|
-
export function connectionReporting(stateRoot) {
|
|
10
|
-
let key = path.resolve(stateRoot);
|
|
11
|
-
if (process.platform === 'win32') key = key.toLowerCase();
|
|
12
|
-
if (reporters.has(key)) return reporters.get(key);
|
|
13
|
-
const active = new Map(), pending = new Map(), events = new EventEmitter();
|
|
14
|
-
function record(connection, state) {
|
|
15
|
-
const event = { ...connection, id: randomUUID(), state, at: new Date().toISOString() };
|
|
16
|
-
pending.set(event.id, event);
|
|
17
|
-
if (pending.size > 128) pending.delete(pending.keys().next().value);
|
|
18
|
-
events.emit('change', event);
|
|
19
|
-
}
|
|
20
|
-
const reporter = {
|
|
21
|
-
connected(connectionId, clientId, mode) {
|
|
22
|
-
if (active.has(connectionId)) return;
|
|
23
|
-
const connection = { connectionId, clientId, mode };
|
|
24
|
-
active.set(connectionId, connection); record(connection, 'connected');
|
|
25
|
-
},
|
|
26
|
-
disconnected(connectionId) {
|
|
27
|
-
const connection = active.get(connectionId);
|
|
28
|
-
if (!connection) return;
|
|
29
|
-
active.delete(connectionId); record(connection, 'disconnected');
|
|
30
|
-
},
|
|
31
|
-
snapshot: () => [...active.values()],
|
|
32
|
-
pending: () => [...pending.values()],
|
|
33
|
-
acknowledge: id => pending.delete(id),
|
|
34
|
-
watch(listener) { events.on('change', listener); return () => events.off('change', listener); },
|
|
35
|
-
};
|
|
36
|
-
reporters.set(key, reporter);
|
|
37
|
-
return reporter;
|
|
38
|
-
}
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const reporters = new Map();
|
|
6
|
+
|
|
7
|
+
// Workstation observations only. The bounded in-memory outbox covers cloud
|
|
8
|
+
// reconnects, not process exit; no polling or message payloads are involved.
|
|
9
|
+
export function connectionReporting(stateRoot) {
|
|
10
|
+
let key = path.resolve(stateRoot);
|
|
11
|
+
if (process.platform === 'win32') key = key.toLowerCase();
|
|
12
|
+
if (reporters.has(key)) return reporters.get(key);
|
|
13
|
+
const active = new Map(), pending = new Map(), events = new EventEmitter();
|
|
14
|
+
function record(connection, state) {
|
|
15
|
+
const event = { ...connection, id: randomUUID(), state, at: new Date().toISOString() };
|
|
16
|
+
pending.set(event.id, event);
|
|
17
|
+
if (pending.size > 128) pending.delete(pending.keys().next().value);
|
|
18
|
+
events.emit('change', event);
|
|
19
|
+
}
|
|
20
|
+
const reporter = {
|
|
21
|
+
connected(connectionId, clientId, mode) {
|
|
22
|
+
if (active.has(connectionId)) return;
|
|
23
|
+
const connection = { connectionId, clientId, mode };
|
|
24
|
+
active.set(connectionId, connection); record(connection, 'connected');
|
|
25
|
+
},
|
|
26
|
+
disconnected(connectionId) {
|
|
27
|
+
const connection = active.get(connectionId);
|
|
28
|
+
if (!connection) return;
|
|
29
|
+
active.delete(connectionId); record(connection, 'disconnected');
|
|
30
|
+
},
|
|
31
|
+
snapshot: () => [...active.values()],
|
|
32
|
+
pending: () => [...pending.values()],
|
|
33
|
+
acknowledge: id => pending.delete(id),
|
|
34
|
+
watch(listener) { events.on('change', listener); return () => events.off('change', listener); },
|
|
35
|
+
};
|
|
36
|
+
reporters.set(key, reporter);
|
|
37
|
+
return reporter;
|
|
38
|
+
}
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { validId } from './device-store.mjs';
|
|
5
|
-
|
|
6
|
-
export function readDebugAllowlist(file) {
|
|
7
|
-
if (!file || !fs.existsSync(file)) return { version: 1, devices: [] };
|
|
8
|
-
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
9
|
-
if (value.version !== 1 || !Array.isArray(value.devices) || value.devices.some(entry => !validId(entry.deviceId))) {
|
|
10
|
-
throw new Error('Invalid debug allowlist');
|
|
11
|
-
}
|
|
12
|
-
return value;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function debugAllowed(allowlist, deviceId, client) {
|
|
16
|
-
return Boolean(client && allowlist.devices.some(entry => entry.deviceId === deviceId));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Administrator-only file; never invoked by the relay wire protocol or Desktop CLI.
|
|
20
|
-
export function updateDebugAllowlist(file, deviceId, enabled) {
|
|
21
|
-
if (!validId(deviceId)) throw new Error('Invalid debug device');
|
|
22
|
-
const value = readDebugAllowlist(file);
|
|
23
|
-
const previous = fs.existsSync(file) ? fs.statSync(file) : null;
|
|
24
|
-
value.devices = value.devices.filter(entry => entry.deviceId !== deviceId);
|
|
25
|
-
if (enabled) value.devices.push({ deviceId });
|
|
26
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
28
|
-
try {
|
|
29
|
-
fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o640, flag: 'wx' });
|
|
30
|
-
if (previous && process.platform !== 'win32') fs.chownSync(temporary, previous.uid, previous.gid);
|
|
31
|
-
fs.renameSync(temporary, file);
|
|
32
|
-
} finally { fs.rmSync(temporary, { force: true }); }
|
|
33
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { validId } from './device-store.mjs';
|
|
5
|
+
|
|
6
|
+
export function readDebugAllowlist(file) {
|
|
7
|
+
if (!file || !fs.existsSync(file)) return { version: 1, devices: [] };
|
|
8
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
9
|
+
if (value.version !== 1 || !Array.isArray(value.devices) || value.devices.some(entry => !validId(entry.deviceId))) {
|
|
10
|
+
throw new Error('Invalid debug allowlist');
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function debugAllowed(allowlist, deviceId, client) {
|
|
16
|
+
return Boolean(client && allowlist.devices.some(entry => entry.deviceId === deviceId));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Administrator-only file; never invoked by the relay wire protocol or Desktop CLI.
|
|
20
|
+
export function updateDebugAllowlist(file, deviceId, enabled) {
|
|
21
|
+
if (!validId(deviceId)) throw new Error('Invalid debug device');
|
|
22
|
+
const value = readDebugAllowlist(file);
|
|
23
|
+
const previous = fs.existsSync(file) ? fs.statSync(file) : null;
|
|
24
|
+
value.devices = value.devices.filter(entry => entry.deviceId !== deviceId);
|
|
25
|
+
if (enabled) value.devices.push({ deviceId });
|
|
26
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
27
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
28
|
+
try {
|
|
29
|
+
fs.writeFileSync(temporary, JSON.stringify(value, null, 2) + '\n', { mode: 0o640, flag: 'wx' });
|
|
30
|
+
if (previous && process.platform !== 'win32') fs.chownSync(temporary, previous.uid, previous.gid);
|
|
31
|
+
fs.renameSync(temporary, file);
|
|
32
|
+
} finally { fs.rmSync(temporary, { force: true }); }
|
|
33
|
+
}
|
|
@@ -6,8 +6,8 @@ import { connectBridgeRelay } from './connector.mjs';
|
|
|
6
6
|
import { connectRelayClient } from './client.mjs';
|
|
7
7
|
import { secret } from './wire.mjs';
|
|
8
8
|
|
|
9
|
-
export async function startRelayDemo({ mode = 'lan-shared', pairingTtlMs, dispatch, remoteRelay, hostName = 'TEST-PC', lanCandidates = [], relayOptions = {} } = {}) {
|
|
10
|
-
if (mode !== 'lan-shared') throw new Error('Invalid demo mode');
|
|
9
|
+
export async function startRelayDemo({ mode = 'lan-shared', pairingTtlMs, dispatch, remoteRelay, hostName = 'TEST-PC', lanCandidates = [], relayOptions = {} } = {}) {
|
|
10
|
+
if (mode !== 'lan-shared') throw new Error('Invalid demo mode');
|
|
11
11
|
const token = secret(), calls = [];
|
|
12
12
|
const bridge = http.createServer((_request, response) => { response.writeHead(404); response.end(); });
|
|
13
13
|
const lan = attachLanSocket(bridge, { token, dispatch: async (request, reply) => {
|
|
@@ -17,15 +17,15 @@ export async function startRelayDemo({ mode = 'lan-shared', pairingTtlMs, dispat
|
|
|
17
17
|
if (request.url.split('?')[0] === '/api/threads/demo/events') {
|
|
18
18
|
reply.startEvents(); reply.event('update', { thread: { id: 'demo' }, simulated: true }); return;
|
|
19
19
|
}
|
|
20
|
-
if (request.url === '/api/demo/stop') return reply.json(200, { stopped: true, simulated: true });
|
|
21
|
-
reply.json(404, { error: '模拟桌面服务不执行真实任务' });
|
|
20
|
+
if (request.url === '/api/demo/stop') return reply.json(200, { stopped: true, simulated: true });
|
|
21
|
+
reply.json(404, { error: '模拟桌面服务不执行真实任务' });
|
|
22
22
|
} });
|
|
23
23
|
await new Promise(resolve => bridge.listen(0, '127.0.0.1', resolve));
|
|
24
|
-
const relay = remoteRelay ? { ...remoteRelay, async close() {} } : await startSelfRelay({ pairingTtlMs, ...relayOptions });
|
|
24
|
+
const relay = remoteRelay ? { ...remoteRelay, async close() {} } : await startSelfRelay({ pairingTtlMs, ...relayOptions });
|
|
25
25
|
const bridgeUrl = `ws://127.0.0.1:${bridge.address().port}/api/socket`;
|
|
26
26
|
const identity = remoteRelay ? { id: randomUUID(), secret: secret() } : undefined;
|
|
27
27
|
const clientId = randomUUID(), credential = secret();
|
|
28
|
-
const connector = connectBridgeRelay({ identity, relayUrl: relay.url, hostKey: relay.hostKey, bridgeUrl, bridgeToken: token, hostName, lanCandidates });
|
|
28
|
+
const connector = connectBridgeRelay({ identity, relayUrl: relay.url, hostKey: relay.hostKey, bridgeUrl, bridgeToken: token, hostName, lanCandidates });
|
|
29
29
|
let pairing;
|
|
30
30
|
try { pairing = await connector.ready; }
|
|
31
31
|
catch (error) { connector.close(); await relay.close(); lan.close(); await new Promise(resolve => bridge.close(resolve)); throw error; }
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
export function deviceInfo(value = {}) {
|
|
2
|
-
const info = {};
|
|
3
|
-
if (value.platform !== undefined && value.platform !== null) {
|
|
4
|
-
if (!['android', 'harmony', 'ios'].includes(value.platform)) throw new Error('Invalid mobile platform');
|
|
5
|
-
info.platform = value.platform;
|
|
6
|
-
}
|
|
7
|
-
if (value.deviceName !== undefined && value.deviceName !== null) {
|
|
8
|
-
if (typeof value.deviceName !== 'string' || value.deviceName.length > 80 || /[\u0000-\u001f\u007f]/.test(value.deviceName)) throw new Error('Invalid mobile device name');
|
|
9
|
-
if (value.deviceName.trim()) info.deviceName = value.deviceName.trim();
|
|
10
|
-
}
|
|
11
|
-
return info;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function connectionInfo(client, at, info = {}) {
|
|
15
|
-
const time = Date.parse(at);
|
|
16
|
-
if (!Number.isFinite(time)) throw new Error('Invalid mobile connection time');
|
|
17
|
-
const next = deviceInfo(info);
|
|
18
|
-
if (client.lastConnectedAt && Date.parse(client.lastConnectedAt) > time) return client;
|
|
19
|
-
return { ...client, ...next, lastConnectedAt: new Date(time).toISOString() };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function validConnectionInfo(client) {
|
|
23
|
-
try {
|
|
24
|
-
deviceInfo(client);
|
|
25
|
-
return client.lastConnectedAt == null || Number.isFinite(Date.parse(client.lastConnectedAt));
|
|
26
|
-
} catch { return false; }
|
|
27
|
-
}
|
|
1
|
+
export function deviceInfo(value = {}) {
|
|
2
|
+
const info = {};
|
|
3
|
+
if (value.platform !== undefined && value.platform !== null) {
|
|
4
|
+
if (!['android', 'harmony', 'ios'].includes(value.platform)) throw new Error('Invalid mobile platform');
|
|
5
|
+
info.platform = value.platform;
|
|
6
|
+
}
|
|
7
|
+
if (value.deviceName !== undefined && value.deviceName !== null) {
|
|
8
|
+
if (typeof value.deviceName !== 'string' || value.deviceName.length > 80 || /[\u0000-\u001f\u007f]/.test(value.deviceName)) throw new Error('Invalid mobile device name');
|
|
9
|
+
if (value.deviceName.trim()) info.deviceName = value.deviceName.trim();
|
|
10
|
+
}
|
|
11
|
+
return info;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function connectionInfo(client, at, info = {}) {
|
|
15
|
+
const time = Date.parse(at);
|
|
16
|
+
if (!Number.isFinite(time)) throw new Error('Invalid mobile connection time');
|
|
17
|
+
const next = deviceInfo(info);
|
|
18
|
+
if (client.lastConnectedAt && Date.parse(client.lastConnectedAt) > time) return client;
|
|
19
|
+
return { ...client, ...next, lastConnectedAt: new Date(time).toISOString() };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validConnectionInfo(client) {
|
|
23
|
+
try {
|
|
24
|
+
deviceInfo(client);
|
|
25
|
+
return client.lastConnectedAt == null || Number.isFinite(Date.parse(client.lastConnectedAt));
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
}
|