@walkhi/code-relax 0.1.0-beta.30 → 0.1.0-beta.32
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 +95 -89
- package/dist/bin/cleanup-accounts.mjs +63 -63
- package/dist/bin/codex-remote.mjs +374 -367
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-android-release.mjs +63 -58
- package/dist/bin/publish-harmony-release.mjs +46 -0
- package/dist/bin/release-upload-server.mjs +174 -0
- 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/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/get-shared-service-url.ps1 +6 -6
- package/dist/shared/app-server-events.cjs +422 -412
- package/dist/shared/code-browser.cjs +159 -159
- package/dist/shared/file-changes.cjs +31 -31
- package/dist/shared/file-citations.cjs +117 -117
- package/dist/shared/file-mentions.cjs +92 -92
- package/dist/shared/message-delivery.cjs +54 -54
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-file-channel.cjs +78 -74
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -0
- package/dist/shared/server-requests.cjs +219 -219
- package/dist/shared/timeline-sync.cjs +173 -173
- package/dist/skills/code-relax/SKILL.md +22 -22
- package/dist/src/archived-threads.mjs +22 -0
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/bounded-process-log.mjs +63 -63
- package/dist/src/celsius-client.mjs +107 -107
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +46 -46
- package/dist/src/dots-approval-timeline.mjs +99 -99
- package/dist/src/dots-avatar.mjs +42 -42
- package/dist/src/dots-chat-projection.mjs +50 -50
- package/dist/src/dots-chat-server.mjs +461 -461
- package/dist/src/dots-message-images.mjs +319 -319
- package/dist/src/dots-profile.mjs +77 -77
- package/dist/src/dots-room-activity.mjs +94 -94
- package/dist/src/dots-room-client.mjs +135 -135
- package/dist/src/dots-room-projection.mjs +94 -94
- package/dist/src/dots-schedules.mjs +66 -66
- package/dist/src/dots-summary-tasks.mjs +32 -32
- package/dist/src/dots-task-catalog.mjs +430 -430
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/file-transfer-range.mjs +22 -22
- package/dist/src/file-upload-session.mjs +130 -130
- package/dist/src/hosted-history-app-server.mjs +103 -103
- package/dist/src/hosted-history-cache.mjs +113 -113
- package/dist/src/lan-socket-server.mjs +265 -263
- package/dist/src/managed-app-server.mjs +244 -244
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/message-upload-files.mjs +74 -74
- package/dist/src/onboarding.mjs +12 -12
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/README.md +12 -12
- package/dist/src/platform/codex-protocol.mjs +46 -46
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +3 -3
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/desktop-project-store.mjs +208 -149
- package/dist/src/platform/file-opener.mjs +9 -0
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -0
- package/dist/src/platform/linux/credential-store.mjs +28 -0
- package/dist/src/platform/linux/desktop-host.mjs +83 -0
- package/dist/src/platform/linux/desktop-installation.mjs +36 -0
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -0
- package/dist/src/platform/linux/network-info.mjs +11 -0
- package/dist/src/platform/linux/process.mjs +42 -0
- package/dist/src/platform/linux/project-locations.mjs +17 -0
- package/dist/src/platform/linux/resident-startup.mjs +52 -0
- package/dist/src/platform/linux/retained-runtime.mjs +76 -0
- package/dist/src/platform/linux/service-host.mjs +18 -0
- package/dist/src/platform/linux/system.mjs +49 -0
- package/dist/src/platform/linux/user-environment.mjs +7 -0
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/codex-runtime.mjs +125 -125
- package/dist/src/platform/macos/credential-store.mjs +10 -10
- package/dist/src/platform/macos/desktop-host.mjs +82 -82
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -10
- package/dist/src/platform/macos/network-info.mjs +8 -28
- 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/macos/user-environment.mjs +7 -7
- package/dist/src/platform/native-package.mjs +58 -46
- 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 -0
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- 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/activate-desktop.ps1 +48 -48
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/codex-runtime.mjs +215 -215
- package/dist/src/platform/windows/credential-store.mjs +27 -27
- package/dist/src/platform/windows/credential-store.ps1 +49 -49
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-host.mjs +58 -58
- package/dist/src/platform/windows/desktop-info.ps1 +14 -14
- package/dist/src/platform/windows/desktop-project-registration.mjs +4 -4
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +21 -21
- package/dist/src/platform/windows/dots-profile.mjs +49 -49
- package/dist/src/platform/windows/launch-worker.mjs +41 -41
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/project-locations.mjs +16 -16
- 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/service-host.mjs +31 -31
- package/dist/src/platform/windows/start-hidden-console.ps1 +49 -49
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.mjs +50 -50
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-archive.mjs +30 -0
- package/dist/src/project-create.mjs +121 -119
- package/dist/src/project-management.mjs +59 -69
- package/dist/src/project-remove.mjs +55 -0
- package/dist/src/project-update.mjs +56 -0
- package/dist/src/runtime-discovery.mjs +47 -47
- package/dist/src/self-relay/account-cleanup.mjs +30 -30
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-page.mjs +80 -80
- package/dist/src/self-relay/admin-server.mjs +74 -74
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-style.mjs +14 -14
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connector.mjs +439 -438
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +37 -37
- package/dist/src/self-relay/device-info.mjs +27 -0
- package/dist/src/self-relay/device-store.mjs +92 -84
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +183 -183
- package/dist/src/self-relay/file-wire.mjs +13 -13
- package/dist/src/self-relay/lan-authorizations.mjs +71 -42
- package/dist/src/self-relay/lifecycle.mjs +264 -250
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -105
- package/dist/src/self-relay/p2p-probe.mjs +159 -159
- package/dist/src/self-relay/privacy-page.mjs +26 -26
- package/dist/src/self-relay/public-releases.mjs +52 -0
- package/dist/src/self-relay/rtc-file-bridge.mjs +41 -41
- package/dist/src/self-relay/server.mjs +1006 -932
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/usage-meter.mjs +141 -141
- 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 +1861 -1778
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-doctor.mjs +110 -110
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-install.mjs +91 -91
- package/dist/src/service-lifecycle.mjs +138 -138
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/shared-app-server.mjs +615 -611
- package/dist/src/shared-catalog.mjs +132 -132
- package/dist/src/shared-recovery.mjs +146 -146
- package/dist/src/shared-thread-stream.mjs +164 -164
- 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/src/usage-reset.mjs +17 -17
- package/dist/src/workstation-downloads.mjs +120 -120
- 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 +7 -6
- package/tools/postinstall.mjs +127 -127
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import http from 'node:http';
|
|
6
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { Transform, Readable } from 'node:stream';
|
|
9
|
+
import { execFile } from 'node:child_process';
|
|
10
|
+
import { promisify } from 'node:util';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { verifyReleaseIdentity, createUploadSigner } from '../relay-admin/release-oidc.mjs';
|
|
13
|
+
import { androidReleaseStorage } from '../relay-admin/android-release-storage.mjs';
|
|
14
|
+
import { readAndroidCatalog } from '../src/self-relay/android-releases.mjs';
|
|
15
|
+
|
|
16
|
+
import { readHarmonyConfig, validateHarmonyMetadata, checkHarmonyVersion, verifyHarmony, activateHarmony } from '../relay-admin/harmony-release.mjs';
|
|
17
|
+
|
|
18
|
+
const execute = promisify(execFile);
|
|
19
|
+
const config = JSON.parse(fs.readFileSync('/etc/codex-relay/android-releases.json', 'utf8'));
|
|
20
|
+
const credential = JSON.parse(fs.readFileSync('/etc/codex-relay/tencent-monitor.json', 'utf8'));
|
|
21
|
+
const storage = androidReleaseStorage('/etc/codex-relay/android-releases.json', '/etc/codex-relay/tencent-monitor.json');
|
|
22
|
+
const signUpload = createUploadSigner(config, { secretId: credential.secretId, secretKey: credential.secretKey });
|
|
23
|
+
const stateDir = '/var/lib/codex-relay/release-publisher';
|
|
24
|
+
fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
25
|
+
const cosCall = (method, args) => new Promise((resolve, reject) => storage.cos[method](args, (e, r) => e ? reject(e) : resolve(r)));
|
|
26
|
+
const object = key => ({ Bucket: config.bucket, Region: config.region, Key: key });
|
|
27
|
+
const identityKey = p => `${p.repository_id}/${p.run_id}/${p.run_attempt}/${p.sha}`;
|
|
28
|
+
const filename = id => path.join(stateDir, id + '.json');
|
|
29
|
+
function save(row) {
|
|
30
|
+
const temp = filename(row.intentId) + '.' + randomUUID();
|
|
31
|
+
fs.writeFileSync(temp, JSON.stringify(row), { mode: 0o600 }); fs.renameSync(temp, filename(row.intentId));
|
|
32
|
+
}
|
|
33
|
+
const read = id => fs.existsSync(filename(id)) ? JSON.parse(fs.readFileSync(filename(id))) : null;
|
|
34
|
+
const publicState = row => ({ ok: true, intentId: row.intentId, status: row.status, result: row.result, error: row.error });
|
|
35
|
+
async function readJson(req) {
|
|
36
|
+
const chunks = []; let size = 0;
|
|
37
|
+
for await (const chunk of req) { size += chunk.length; if (size > 16384) throw Error('Request too large'); chunks.push(chunk); }
|
|
38
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
|
|
39
|
+
}
|
|
40
|
+
function metadataFrom(data, claims, target) {
|
|
41
|
+
const { packageName, versionCode, versionName, size, sha256, contentMd5, certificateSha256, notes,
|
|
42
|
+
buildCommit, artifactId, runId, sourceCommit, verifyOnly } = data;
|
|
43
|
+
if (!Number.isSafeInteger(versionCode) || versionCode < 1 || versionCode > 2100000000
|
|
44
|
+
|| !/^[a-f0-9]{64}$/.test(sha256)
|
|
45
|
+
|| !Number.isSafeInteger(size) || size < 1 || size > 200 * 1024 * 1024
|
|
46
|
+
|| typeof notes !== 'string' || notes.length > 2000 || !/^[A-Za-z0-9+/]{22}==$/.test(contentMd5)
|
|
47
|
+
|| !/^\d+$/.test(artifactId) || runId !== claims.run_id || sourceCommit !== claims.sha
|
|
48
|
+
|| typeof verifyOnly !== 'boolean' || !/^[a-f0-9]{40}$/.test(buildCommit)) throw Error('Invalid metadata');
|
|
49
|
+
if (target === 'hap') validateHarmonyMetadata(data, readHarmonyConfig());
|
|
50
|
+
else if (packageName !== 'com.walkhi.codexremote' || !/^\d+\.\d+\.\d+\.\d+$/.test(versionName)
|
|
51
|
+
|| !versionName.endsWith('.' + versionCode) || certificateSha256 !== config.certificateSha256) throw Error('Invalid Android identity');
|
|
52
|
+
return { target, ...(target === 'hap' ? { files: data.files } : {}), packageName, versionCode, versionName, size, sha256, contentMd5, certificateSha256, notes,
|
|
53
|
+
buildCommit, artifactId, runId, sourceCommit, verifyOnly };
|
|
54
|
+
}
|
|
55
|
+
let busy = false;
|
|
56
|
+
const server = http.createServer(async (req, res) => {
|
|
57
|
+
const reply = (code, data) => { if (!res.destroyed) { res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(data)); } };
|
|
58
|
+
let claims;
|
|
59
|
+
try { claims = await verifyReleaseIdentity((req.headers.authorization || '').replace(/^Bearer /, '')); }
|
|
60
|
+
catch { reply(401, { ok: false, error: 'OIDC_REJECTED' }); return; }
|
|
61
|
+
const target = /^\/code-relax\/publish\/(android|hap)\/intents(?:\/|$)/.exec(req.url || '')?.[1];
|
|
62
|
+
if (!target) { reply(404, { ok: false, error: 'NOT_FOUND' }); return; }
|
|
63
|
+
const prefix = `/code-relax/publish/${target}/intents`;
|
|
64
|
+
try {
|
|
65
|
+
if (req.method === 'POST' && req.url === prefix) {
|
|
66
|
+
let metadata;
|
|
67
|
+
try { metadata = metadataFrom(await readJson(req), claims, target); } catch { reply(400, { ok: false, error: 'INVALID_RELEASE' }); return; }
|
|
68
|
+
if (!metadata.verifyOnly && target === 'hap') {
|
|
69
|
+
try { checkHarmonyVersion(metadata); } catch { reply(409, { ok: false, error: 'VERSION_CONFLICT' }); return; }
|
|
70
|
+
}
|
|
71
|
+
if (!metadata.verifyOnly && target === 'android') {
|
|
72
|
+
const catalog = readAndroidCatalog(config.catalogFile);
|
|
73
|
+
const previous = catalog.releases.find(x => x.versionCode === metadata.versionCode);
|
|
74
|
+
if (catalog.latestVersionCode > metadata.versionCode || (previous && previous.sha256 !== metadata.sha256)) {
|
|
75
|
+
reply(409, { ok: false, error: 'VERSION_CONFLICT' }); return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const identity = identityKey(claims);
|
|
79
|
+
const intentId = createHash('sha256').update(JSON.stringify({ identity, metadata })).digest('hex');
|
|
80
|
+
let row = read(intentId);
|
|
81
|
+
if (row && row.status !== 'prepared') { reply(200, publicState(row)); return; }
|
|
82
|
+
const key = metadata.verifyOnly ? `code-relax/android/ci-verification/${claims.run_id}/${intentId}.apk`
|
|
83
|
+
: `code-relax/android/${metadata.versionCode}/Code-Relax-${metadata.sha256.slice(0, 16)}.apk`;
|
|
84
|
+
const keys = target === 'hap' ? metadata.files.map(f => metadata.verifyOnly
|
|
85
|
+
? `code-relax/harmony/ci-verification/${claims.run_id}/${intentId}/${f.name}`
|
|
86
|
+
: `code-relax/harmony/${metadata.versionCode}/${f.name}`) : [key];
|
|
87
|
+
row ??= { intentId, identity, metadata, key, keys, status: 'prepared' };
|
|
88
|
+
save(row);
|
|
89
|
+
const grant = target === 'hap' ? { uploads: await Promise.all(metadata.files.map(async (f, i) => ({ name: f.name,
|
|
90
|
+
...await signUpload(keys[i], f, i === 1 ? 'application/json' : i === 2 ? 'image/png' : 'application/octet-stream') }))) }
|
|
91
|
+
: await signUpload(key, metadata);
|
|
92
|
+
reply(200, { ...publicState(row), ...grant }); return;
|
|
93
|
+
}
|
|
94
|
+
const match = new RegExp(`^${prefix}/([a-f0-9]{64})(/complete)?$`).exec(req.url || '');
|
|
95
|
+
if (!match || !((!match[2] && req.method === 'GET') || (match[2] && req.method === 'POST'))) {
|
|
96
|
+
reply(404, { ok: false, error: 'NOT_FOUND' }); return;
|
|
97
|
+
}
|
|
98
|
+
const row = read(match[1]);
|
|
99
|
+
if (!row || row.identity !== identityKey(claims) || (row.metadata.target || 'android') !== target) { reply(404, { ok: false, error: 'INTENT_NOT_FOUND' }); return; }
|
|
100
|
+
if (match[2] && row.status === 'prepared') {
|
|
101
|
+
if (busy) { reply(409, { ok: false, error: 'PUBLICATION_BUSY' }); return; }
|
|
102
|
+
busy = true; row.status = 'validating'; save(row);
|
|
103
|
+
void complete(row).finally(() => { busy = false; });
|
|
104
|
+
}
|
|
105
|
+
reply(row.status === 'validating' ? 202 : 200, publicState(row));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(JSON.stringify({ event: 'release-api-failed', code: /^[A-Za-z.]+$/.test(error.code || '') ? error.code : 'io-or-provider' }));
|
|
108
|
+
reply(503, { ok: false, error: 'RELEASE_UNAVAILABLE' });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
async function complete(row) {
|
|
112
|
+
const { metadata, key } = row;
|
|
113
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'code-relax-release-'));
|
|
114
|
+
const apk = path.join(dir, 'release.apk'), json = path.join(dir, 'release.json');
|
|
115
|
+
const hap = metadata.target === 'hap';
|
|
116
|
+
const files = hap ? metadata.files : [{ ...metadata, name: 'release.apk' }];
|
|
117
|
+
let stage = 'download';
|
|
118
|
+
try {
|
|
119
|
+
for (const [i, file] of files.entries()) {
|
|
120
|
+
const ticket = await cosCall('getObjectUrl', { ...object(row.keys?.[i] || key), Sign: true, Method: 'GET', Protocol: 'https:', Expires: 300 });
|
|
121
|
+
const response = await fetch(ticket.Url, { signal: AbortSignal.timeout(120000) });
|
|
122
|
+
if (!response.ok || Number(response.headers.get('content-length')) !== file.size) throw Error();
|
|
123
|
+
let size = 0; const hash = createHash('sha256'), md5 = createHash('md5');
|
|
124
|
+
await pipeline(Readable.fromWeb(response.body), new Transform({ transform(chunk, encoding, next) {
|
|
125
|
+
size += chunk.length; if (size > file.size) return next(Error('Oversize'));
|
|
126
|
+
hash.update(chunk); md5.update(chunk); next(null, chunk);
|
|
127
|
+
} }), fs.createWriteStream(path.join(dir, file.name), { flags: 'wx', mode: 0o600 }));
|
|
128
|
+
if (size !== file.size || hash.digest('hex') !== file.sha256 || md5.digest('base64') !== file.contentMd5) throw Error();
|
|
129
|
+
const unsigned = new URL(ticket.Url); unsigned.search = '';
|
|
130
|
+
if (!hap && (await fetch(unsigned, { method: 'HEAD', signal: AbortSignal.timeout(15000) })).status !== 403) throw Error();
|
|
131
|
+
}
|
|
132
|
+
if (hap) {
|
|
133
|
+
stage = 'harmony-signature';
|
|
134
|
+
await verifyHarmony(dir, metadata, readHarmonyConfig());
|
|
135
|
+
row.result = { sha256: metadata.sha256, versionCode: metadata.versionCode, size: metadata.size };
|
|
136
|
+
if (metadata.verifyOnly) row.status = 'verified';
|
|
137
|
+
else { stage = 'harmony-publish'; row.result = await activateHarmony(metadata, storage, row.keys); row.status = 'published'; }
|
|
138
|
+
save(row); return;
|
|
139
|
+
}
|
|
140
|
+
stage = 'signature';
|
|
141
|
+
const { stdout: signature } = await execute('/usr/bin/apksigner', ['verify', '--print-certs', apk], {
|
|
142
|
+
timeout: 30000, env: { ...process.env, JAVA_TOOL_OPTIONS: '-Xmx256m' } });
|
|
143
|
+
const certificates = [...signature.matchAll(/^Signer #\d+ certificate SHA-256 digest: ([a-f0-9]{64})$/gm)];
|
|
144
|
+
if (certificates.length !== 1 || certificates[0][1] !== metadata.certificateSha256) throw Error();
|
|
145
|
+
stage = 'identity';
|
|
146
|
+
const { stdout: badging } = await execute('/usr/bin/aapt', ['dump', 'badging', apk], { timeout: 30000 });
|
|
147
|
+
const identity = /^package: name='([^']+)' versionCode='(\d+)' versionName='([^']+)'/m.exec(badging);
|
|
148
|
+
if (!identity || identity[1] !== metadata.packageName || Number(identity[2]) !== metadata.versionCode
|
|
149
|
+
|| identity[3] !== metadata.versionName || /^application-debuggable/m.test(badging)) throw Error();
|
|
150
|
+
row.result = { sha256: metadata.sha256, versionCode: metadata.versionCode, size: metadata.size };
|
|
151
|
+
if (metadata.verifyOnly) row.status = 'verified';
|
|
152
|
+
else {
|
|
153
|
+
stage = 'publish';
|
|
154
|
+
fs.writeFileSync(json, JSON.stringify(metadata), { mode: 0o600 });
|
|
155
|
+
const { stdout } = await execute(process.execPath, [fileURLToPath(new URL('./publish-android-release.mjs', import.meta.url)), apk, json, '--existing-object'], { timeout: 240000 });
|
|
156
|
+
row.result = JSON.parse(stdout.trim()); row.status = 'published';
|
|
157
|
+
}
|
|
158
|
+
save(row);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
row.status = 'failed'; row.error = `RELEASE_${stage.toUpperCase()}_FAILED`; save(row);
|
|
161
|
+
console.error(JSON.stringify({ event: 'release-validation-failed', intentId: row.intentId, stage, code: /^[A-Za-z0-9_.-]+$/.test(error.code || '') ? error.code : 'validation' }));
|
|
162
|
+
} finally {
|
|
163
|
+
for (const file of [...files.map(f => path.join(dir, f.name)), json]) fs.rmSync(file, { force: true }); fs.rmdirSync(dir);
|
|
164
|
+
// Verification objects remain private evidence; the deployment identity has no DeleteObject grant.
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// A restart never blindly launches an interrupted publisher a second time.
|
|
168
|
+
for (const name of fs.readdirSync(stateDir).filter(x => /^[a-f0-9]{64}\.json$/.test(x))) {
|
|
169
|
+
const row = read(name.slice(0, -5));
|
|
170
|
+
if (row.status === 'validating') { row.status = 'failed'; row.error = 'INTERRUPTED_CHECK_LATEST'; save(row); }
|
|
171
|
+
}
|
|
172
|
+
server.requestTimeout = 15000;
|
|
173
|
+
server.listen(45852, '127.0.0.1');
|
|
174
|
+
process.on('SIGTERM', () => { server.close(); const timer = setInterval(() => { if (!busy) { clearInterval(timer); process.exit(0); } }, 250); });
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { startAdminServer } from '../src/self-relay/admin-server.mjs';
|
|
2
|
-
|
|
3
|
-
const port = Number(process.env.RELAY_ADMIN_PORT || 45851);
|
|
4
|
-
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error('Invalid RELAY_ADMIN_PORT');
|
|
5
|
-
const server = await startAdminServer({ port });
|
|
6
|
-
console.log(JSON.stringify({ service: 'codex-relay-admin', port: server.port }));
|
|
7
|
-
await new Promise(resolve => { process.once('SIGTERM', resolve); process.once('SIGINT', resolve); });
|
|
8
|
-
await server.close();
|
|
1
|
+
import { startAdminServer } from '../src/self-relay/admin-server.mjs';
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.RELAY_ADMIN_PORT || 45851);
|
|
4
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error('Invalid RELAY_ADMIN_PORT');
|
|
5
|
+
const server = await startAdminServer({ port });
|
|
6
|
+
console.log(JSON.stringify({ service: 'codex-relay-admin', port: server.port }));
|
|
7
|
+
await new Promise(resolve => { process.once('SIGTERM', resolve); process.once('SIGINT', resolve); });
|
|
8
|
+
await server.close();
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { startRelayDemo } from '../src/self-relay/demo.mjs';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
-
|
|
6
|
-
const mode = 'lan-shared';
|
|
7
|
-
let remoteRelay;
|
|
8
|
-
if (process.env.RELAY_URL) {
|
|
9
|
-
let hostKey;
|
|
10
|
-
const sshIndex = process.argv.indexOf('--ssh-host');
|
|
11
|
-
if (sshIndex !== -1) {
|
|
12
|
-
const host = process.argv[sshIndex + 1];
|
|
13
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(host || '')) throw new Error('Invalid SSH alias');
|
|
14
|
-
const result = spawnSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', host,
|
|
15
|
-
'sudo -n cat /etc/codex-relay/host.key'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
16
|
-
if (result.error || result.status !== 0) throw new Error('Cannot read relay credential through SSH');
|
|
17
|
-
hostKey = result.stdout.trim();
|
|
18
|
-
} else hostKey = fs.readFileSync(process.env.RELAY_HOST_KEY_FILE, 'utf8').trim();
|
|
19
|
-
remoteRelay = { url: process.env.RELAY_URL, hostKey };
|
|
20
|
-
}
|
|
21
|
-
const demo = await startRelayDemo({ mode, remoteRelay });
|
|
22
|
-
let client;
|
|
23
|
-
try {
|
|
24
|
-
client = demo.connect(); await client.ready;
|
|
25
|
-
const health = await client.request('/api/health');
|
|
26
|
-
if (health.status !== 200 || health.body.transportMode !== mode) throw new Error('Relay validation failed');
|
|
27
|
-
console.log(JSON.stringify({ ok: true, simulated: true, mode, relay: demo.relay.url,
|
|
28
|
-
bridge: demo.bridgeUrl, message: '中继 → 连接器 → 模拟桌面服务已连通;未连接 Codex,未执行任务。' }, null, 2));
|
|
29
|
-
if (!process.argv.includes('--check')) {
|
|
30
|
-
console.log('验证连接保持运行。Ctrl+C 停止全部模拟进程内服务。');
|
|
31
|
-
await new Promise(resolve => { process.once('SIGINT', resolve); process.once('SIGTERM', resolve); });
|
|
32
|
-
}
|
|
33
|
-
} finally { client?.close(); await demo.close(); }
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startRelayDemo } from '../src/self-relay/demo.mjs';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
const mode = 'lan-shared';
|
|
7
|
+
let remoteRelay;
|
|
8
|
+
if (process.env.RELAY_URL) {
|
|
9
|
+
let hostKey;
|
|
10
|
+
const sshIndex = process.argv.indexOf('--ssh-host');
|
|
11
|
+
if (sshIndex !== -1) {
|
|
12
|
+
const host = process.argv[sshIndex + 1];
|
|
13
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(host || '')) throw new Error('Invalid SSH alias');
|
|
14
|
+
const result = spawnSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', host,
|
|
15
|
+
'sudo -n cat /etc/codex-relay/host.key'], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
16
|
+
if (result.error || result.status !== 0) throw new Error('Cannot read relay credential through SSH');
|
|
17
|
+
hostKey = result.stdout.trim();
|
|
18
|
+
} else hostKey = fs.readFileSync(process.env.RELAY_HOST_KEY_FILE, 'utf8').trim();
|
|
19
|
+
remoteRelay = { url: process.env.RELAY_URL, hostKey };
|
|
20
|
+
}
|
|
21
|
+
const demo = await startRelayDemo({ mode, remoteRelay });
|
|
22
|
+
let client;
|
|
23
|
+
try {
|
|
24
|
+
client = demo.connect(); await client.ready;
|
|
25
|
+
const health = await client.request('/api/health');
|
|
26
|
+
if (health.status !== 200 || health.body.transportMode !== mode) throw new Error('Relay validation failed');
|
|
27
|
+
console.log(JSON.stringify({ ok: true, simulated: true, mode, relay: demo.relay.url,
|
|
28
|
+
bridge: demo.bridgeUrl, message: '中继 → 连接器 → 模拟桌面服务已连通;未连接 Codex,未执行任务。' }, null, 2));
|
|
29
|
+
if (!process.argv.includes('--check')) {
|
|
30
|
+
console.log('验证连接保持运行。Ctrl+C 停止全部模拟进程内服务。');
|
|
31
|
+
await new Promise(resolve => { process.once('SIGINT', resolve); process.once('SIGTERM', resolve); });
|
|
32
|
+
}
|
|
33
|
+
} finally { client?.close(); await demo.close(); }
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import serviceEndpoints from '../shared/service-endpoints.cjs';
|
|
4
|
-
import { deviceStore, validId } from '../src/self-relay/device-store.mjs';
|
|
5
|
-
import { openRelayMysql } from '../src/self-relay/mysql-state.mjs';
|
|
6
|
-
import { startSelfRelay } from '../src/self-relay/server.mjs';
|
|
7
|
-
import { updateDebugAllowlist } from '../src/self-relay/debug-allowlist.mjs';
|
|
8
|
-
|
|
9
|
-
const keyFile = process.env.RELAY_HOST_KEY_FILE;
|
|
10
|
-
if (!keyFile) throw new Error('RELAY_HOST_KEY_FILE is required');
|
|
11
|
-
const hostKey = fs.readFileSync(keyFile, 'utf8').trim();
|
|
12
|
-
const port = Number(process.env.RELAY_PORT || 45850);
|
|
13
|
-
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('Invalid RELAY_PORT');
|
|
14
|
-
const stateFile = process.env.RELAY_STATE_FILE;
|
|
15
|
-
const mysqlConfigFile = process.env.RELAY_MYSQL_CONFIG_FILE;
|
|
16
|
-
const accountConfigFile = process.env.RELAY_ACCOUNT_CONFIG_FILE;
|
|
17
|
-
const longPairingFile = process.env.RELAY_LONG_PAIRING_FILE || '/var/lib/codex-relay/long-pairing.json';
|
|
18
|
-
if (!stateFile && !mysqlConfigFile) throw new Error('RELAY_STATE_FILE or RELAY_MYSQL_CONFIG_FILE is required');
|
|
19
|
-
const debugAllowlistFile = process.env.RELAY_DEBUG_ALLOWLIST_FILE || '/etc/codex-relay/debug-allowlist.json';
|
|
20
|
-
const adminStateFile = process.env.RELAY_ADMIN_STATE_FILE || '/var/lib/codex-relay/admin.json';
|
|
21
|
-
const maxRelaySessions = Number(process.env.RELAY_MAX_RELAY_SESSIONS || 8);
|
|
22
|
-
if (!Number.isSafeInteger(maxRelaySessions) || maxRelaySessions < 1 || maxRelaySessions > 256) throw new Error('Invalid RELAY_MAX_RELAY_SESSIONS');
|
|
23
|
-
const debugCommand = process.argv.find(value => ['--debug-list', '--debug-allow', '--debug-deny'].includes(value));
|
|
24
|
-
if (debugCommand) {
|
|
25
|
-
const database = mysqlConfigFile ? await openRelayMysql(mysqlConfigFile, maxRelaySessions) : null;
|
|
26
|
-
const records = (database?.devices || deviceStore(stateFile)).entries();
|
|
27
|
-
if (debugCommand === '--debug-list') {
|
|
28
|
-
console.log(JSON.stringify(records.map(record => ({
|
|
29
|
-
deviceId: record.id, status: record.status, clientCount: record.clients.length,
|
|
30
|
-
})), null, 2));
|
|
31
|
-
} else {
|
|
32
|
-
const index = process.argv.indexOf(debugCommand), deviceId = process.argv[index + 1];
|
|
33
|
-
if (!validId(deviceId)) throw new Error('Invalid device ID');
|
|
34
|
-
const record = records.find(record => record.id === deviceId && record.status === 'active');
|
|
35
|
-
if (!record && debugCommand === '--debug-allow') throw new Error('Active paired device not found');
|
|
36
|
-
updateDebugAllowlist(debugAllowlistFile, deviceId, debugCommand === '--debug-allow');
|
|
37
|
-
console.log('Debug allowlist updated; restart relay to apply.');
|
|
38
|
-
}
|
|
39
|
-
if (database) await database.close();
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
if (process.argv.includes('--init-state')) { if (mysqlConfigFile) throw new Error('Use migrations for MySQL mode'); deviceStore(stateFile, { initialize: true }); console.log('Device store initialized'); process.exit(0); }
|
|
43
|
-
const revokeIndex = process.argv.indexOf('--revoke-host');
|
|
44
|
-
if (revokeIndex >= 0) {
|
|
45
|
-
// Run only with the service stopped: this is an offline administrator operation.
|
|
46
|
-
if (!process.argv.includes('--offline')) throw new Error('Stop the relay service and supply --offline');
|
|
47
|
-
const id = process.argv[revokeIndex + 1];
|
|
48
|
-
if (!validId(id)) throw new Error('Invalid device ID');
|
|
49
|
-
const database = mysqlConfigFile ? await openRelayMysql(mysqlConfigFile, maxRelaySessions) : null;
|
|
50
|
-
const store = database?.devices || deviceStore(stateFile), record = store.entries().find(item => item.id === id);
|
|
51
|
-
if (!record) throw new Error('Device not found');
|
|
52
|
-
await store.save({ ...record, status: 'revoked', clients: [], revision: record.revision + 1, updatedAt: new Date().toISOString() });
|
|
53
|
-
if (database) await database.close();
|
|
54
|
-
console.log('Computer and phone authorization revoked'); process.exit(0);
|
|
55
|
-
}
|
|
56
|
-
const relay = await startSelfRelay({ port, hostKey, stateFile, mysqlConfigFile, accountConfigFile, debugAllowlistFile, adminStateFile, maxRelaySessions,
|
|
57
|
-
longPairingFile,
|
|
58
|
-
androidReleaseConfigFile: process.env.RELAY_ANDROID_RELEASE_CONFIG_FILE,
|
|
59
|
-
androidReleaseCredentialFile: process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/tencent-monitor.json` : undefined,
|
|
60
|
-
usageDiagnosticKeyFile: process.env.RELAY_USAGE_DIAGNOSTIC_KEY_FILE,
|
|
61
|
-
publicRelayUrl: process.env.RELAY_PUBLIC_URL || serviceEndpoints.relayUrl, appDownloadUrl: process.env.RELAY_APP_DOWNLOAD_URL || serviceEndpoints.installUrl,
|
|
62
|
-
log: entry => console.log(JSON.stringify({ time: new Date().toISOString(), ...entry })) });
|
|
63
|
-
console.log(JSON.stringify({ service: 'codex-self-relay', listen: relay.url, maxConnections: 128, maxRelaySessions,
|
|
64
|
-
maxFrameMiB: 8, encryption: 'TLS must be supplied by reverse proxy; no end-to-end encryption' }));
|
|
65
|
-
await new Promise(resolve => { process.once('SIGTERM', resolve); process.once('SIGINT', resolve); });
|
|
66
|
-
await relay.close();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import serviceEndpoints from '../shared/service-endpoints.cjs';
|
|
4
|
+
import { deviceStore, validId } from '../src/self-relay/device-store.mjs';
|
|
5
|
+
import { openRelayMysql } from '../src/self-relay/mysql-state.mjs';
|
|
6
|
+
import { startSelfRelay } from '../src/self-relay/server.mjs';
|
|
7
|
+
import { updateDebugAllowlist } from '../src/self-relay/debug-allowlist.mjs';
|
|
8
|
+
|
|
9
|
+
const keyFile = process.env.RELAY_HOST_KEY_FILE;
|
|
10
|
+
if (!keyFile) throw new Error('RELAY_HOST_KEY_FILE is required');
|
|
11
|
+
const hostKey = fs.readFileSync(keyFile, 'utf8').trim();
|
|
12
|
+
const port = Number(process.env.RELAY_PORT || 45850);
|
|
13
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error('Invalid RELAY_PORT');
|
|
14
|
+
const stateFile = process.env.RELAY_STATE_FILE;
|
|
15
|
+
const mysqlConfigFile = process.env.RELAY_MYSQL_CONFIG_FILE;
|
|
16
|
+
const accountConfigFile = process.env.RELAY_ACCOUNT_CONFIG_FILE;
|
|
17
|
+
const longPairingFile = process.env.RELAY_LONG_PAIRING_FILE || '/var/lib/codex-relay/long-pairing.json';
|
|
18
|
+
if (!stateFile && !mysqlConfigFile) throw new Error('RELAY_STATE_FILE or RELAY_MYSQL_CONFIG_FILE is required');
|
|
19
|
+
const debugAllowlistFile = process.env.RELAY_DEBUG_ALLOWLIST_FILE || '/etc/codex-relay/debug-allowlist.json';
|
|
20
|
+
const adminStateFile = process.env.RELAY_ADMIN_STATE_FILE || '/var/lib/codex-relay/admin.json';
|
|
21
|
+
const maxRelaySessions = Number(process.env.RELAY_MAX_RELAY_SESSIONS || 8);
|
|
22
|
+
if (!Number.isSafeInteger(maxRelaySessions) || maxRelaySessions < 1 || maxRelaySessions > 256) throw new Error('Invalid RELAY_MAX_RELAY_SESSIONS');
|
|
23
|
+
const debugCommand = process.argv.find(value => ['--debug-list', '--debug-allow', '--debug-deny'].includes(value));
|
|
24
|
+
if (debugCommand) {
|
|
25
|
+
const database = mysqlConfigFile ? await openRelayMysql(mysqlConfigFile, maxRelaySessions) : null;
|
|
26
|
+
const records = (database?.devices || deviceStore(stateFile)).entries();
|
|
27
|
+
if (debugCommand === '--debug-list') {
|
|
28
|
+
console.log(JSON.stringify(records.map(record => ({
|
|
29
|
+
deviceId: record.id, status: record.status, clientCount: record.clients.length,
|
|
30
|
+
})), null, 2));
|
|
31
|
+
} else {
|
|
32
|
+
const index = process.argv.indexOf(debugCommand), deviceId = process.argv[index + 1];
|
|
33
|
+
if (!validId(deviceId)) throw new Error('Invalid device ID');
|
|
34
|
+
const record = records.find(record => record.id === deviceId && record.status === 'active');
|
|
35
|
+
if (!record && debugCommand === '--debug-allow') throw new Error('Active paired device not found');
|
|
36
|
+
updateDebugAllowlist(debugAllowlistFile, deviceId, debugCommand === '--debug-allow');
|
|
37
|
+
console.log('Debug allowlist updated; restart relay to apply.');
|
|
38
|
+
}
|
|
39
|
+
if (database) await database.close();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
if (process.argv.includes('--init-state')) { if (mysqlConfigFile) throw new Error('Use migrations for MySQL mode'); deviceStore(stateFile, { initialize: true }); console.log('Device store initialized'); process.exit(0); }
|
|
43
|
+
const revokeIndex = process.argv.indexOf('--revoke-host');
|
|
44
|
+
if (revokeIndex >= 0) {
|
|
45
|
+
// Run only with the service stopped: this is an offline administrator operation.
|
|
46
|
+
if (!process.argv.includes('--offline')) throw new Error('Stop the relay service and supply --offline');
|
|
47
|
+
const id = process.argv[revokeIndex + 1];
|
|
48
|
+
if (!validId(id)) throw new Error('Invalid device ID');
|
|
49
|
+
const database = mysqlConfigFile ? await openRelayMysql(mysqlConfigFile, maxRelaySessions) : null;
|
|
50
|
+
const store = database?.devices || deviceStore(stateFile), record = store.entries().find(item => item.id === id);
|
|
51
|
+
if (!record) throw new Error('Device not found');
|
|
52
|
+
await store.save({ ...record, status: 'revoked', clients: [], revision: record.revision + 1, updatedAt: new Date().toISOString() });
|
|
53
|
+
if (database) await database.close();
|
|
54
|
+
console.log('Computer and phone authorization revoked'); process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
const relay = await startSelfRelay({ port, hostKey, stateFile, mysqlConfigFile, accountConfigFile, debugAllowlistFile, adminStateFile, maxRelaySessions,
|
|
57
|
+
longPairingFile,
|
|
58
|
+
androidReleaseConfigFile: process.env.RELAY_ANDROID_RELEASE_CONFIG_FILE,
|
|
59
|
+
androidReleaseCredentialFile: process.env.CREDENTIALS_DIRECTORY ? `${process.env.CREDENTIALS_DIRECTORY}/tencent-monitor.json` : undefined,
|
|
60
|
+
usageDiagnosticKeyFile: process.env.RELAY_USAGE_DIAGNOSTIC_KEY_FILE,
|
|
61
|
+
publicRelayUrl: process.env.RELAY_PUBLIC_URL || serviceEndpoints.relayUrl, appDownloadUrl: process.env.RELAY_APP_DOWNLOAD_URL || serviceEndpoints.installUrl,
|
|
62
|
+
log: entry => console.log(JSON.stringify({ time: new Date().toISOString(), ...entry })) });
|
|
63
|
+
console.log(JSON.stringify({ service: 'codex-self-relay', listen: relay.url, maxConnections: 128, maxRelaySessions,
|
|
64
|
+
maxFrameMiB: 8, encryption: 'TLS must be supplied by reverse proxy; no end-to-end encryption' }));
|
|
65
|
+
await new Promise(resolve => { process.once('SIGTERM', resolve); process.once('SIGINT', resolve); });
|
|
66
|
+
await relay.close();
|
package/dist/build-platform.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"layout":"npm-platform-packages","version":"0.1.0-beta.
|
|
1
|
+
{"layout":"npm-platform-packages","version":"0.1.0-beta.32"}
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
2
|
-
|
|
3
|
-
$profiles = @{}
|
|
4
|
-
Get-NetConnectionProfile -ErrorAction SilentlyContinue | ForEach-Object {
|
|
5
|
-
$profiles[[int]$_.InterfaceIndex] = [string]$_.Name
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
$networks = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() |
|
|
9
|
-
Where-Object {
|
|
10
|
-
$_.OperationalStatus -eq 'Up' -and
|
|
11
|
-
$_.NetworkInterfaceType -in @('Wireless80211', 'Ethernet') -and
|
|
12
|
-
$_.GetIPProperties().GatewayAddresses.Count -gt 0
|
|
13
|
-
} | ForEach-Object {
|
|
14
|
-
$adapter = $_
|
|
15
|
-
$properties = $adapter.GetIPProperties()
|
|
16
|
-
$interfaceIndex = $properties.GetIPv4Properties().Index
|
|
17
|
-
$networkType = if ($adapter.NetworkInterfaceType -eq 'Wireless80211') { 'wifi' } else { 'ethernet' }
|
|
18
|
-
$networkName = $profiles[$interfaceIndex]
|
|
19
|
-
if ($networkType -eq 'wifi') {
|
|
20
|
-
$wlan = (& netsh wlan show interfaces interface="$($adapter.Name)" 2>$null) -join "`n"
|
|
21
|
-
$ssid = [regex]::Match($wlan, '(?m)^\s*SSID\s*:\s*(.*?)\s*$')
|
|
22
|
-
if ($ssid.Success -and -not [string]::IsNullOrWhiteSpace($ssid.Groups[1].Value)) {
|
|
23
|
-
$networkName = $ssid.Groups[1].Value.Trim()
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if ([string]::IsNullOrWhiteSpace($networkName)) { $networkName = $adapter.Name }
|
|
27
|
-
$properties.UnicastAddresses | Where-Object {
|
|
28
|
-
$_.Address.AddressFamily -eq 'InterNetwork' -and
|
|
29
|
-
$_.Address.IPAddressToString -match '^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.)'
|
|
30
|
-
} | ForEach-Object {
|
|
31
|
-
[PSCustomObject]@{
|
|
32
|
-
address = $_.Address.IPAddressToString
|
|
33
|
-
networkType = $networkType
|
|
34
|
-
networkName = $networkName
|
|
35
|
-
interfaceName = [string]$adapter.Name
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
ConvertTo-Json -InputObject @($networks) -Compress
|
|
1
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
2
|
+
|
|
3
|
+
$profiles = @{}
|
|
4
|
+
Get-NetConnectionProfile -ErrorAction SilentlyContinue | ForEach-Object {
|
|
5
|
+
$profiles[[int]$_.InterfaceIndex] = [string]$_.Name
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
$networks = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() |
|
|
9
|
+
Where-Object {
|
|
10
|
+
$_.OperationalStatus -eq 'Up' -and
|
|
11
|
+
$_.NetworkInterfaceType -in @('Wireless80211', 'Ethernet') -and
|
|
12
|
+
$_.GetIPProperties().GatewayAddresses.Count -gt 0
|
|
13
|
+
} | ForEach-Object {
|
|
14
|
+
$adapter = $_
|
|
15
|
+
$properties = $adapter.GetIPProperties()
|
|
16
|
+
$interfaceIndex = $properties.GetIPv4Properties().Index
|
|
17
|
+
$networkType = if ($adapter.NetworkInterfaceType -eq 'Wireless80211') { 'wifi' } else { 'ethernet' }
|
|
18
|
+
$networkName = $profiles[$interfaceIndex]
|
|
19
|
+
if ($networkType -eq 'wifi') {
|
|
20
|
+
$wlan = (& netsh wlan show interfaces interface="$($adapter.Name)" 2>$null) -join "`n"
|
|
21
|
+
$ssid = [regex]::Match($wlan, '(?m)^\s*SSID\s*:\s*(.*?)\s*$')
|
|
22
|
+
if ($ssid.Success -and -not [string]::IsNullOrWhiteSpace($ssid.Groups[1].Value)) {
|
|
23
|
+
$networkName = $ssid.Groups[1].Value.Trim()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if ([string]::IsNullOrWhiteSpace($networkName)) { $networkName = $adapter.Name }
|
|
27
|
+
$properties.UnicastAddresses | Where-Object {
|
|
28
|
+
$_.Address.AddressFamily -eq 'InterNetwork' -and
|
|
29
|
+
$_.Address.IPAddressToString -match '^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.)'
|
|
30
|
+
} | ForEach-Object {
|
|
31
|
+
[PSCustomObject]@{
|
|
32
|
+
address = $_.Address.IPAddressToString
|
|
33
|
+
networkType = $networkType
|
|
34
|
+
networkName = $networkName
|
|
35
|
+
interfaceName = [string]$adapter.Name
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ConvertTo-Json -InputObject @($networks) -Compress
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Detect the Desktop connection, not merely a listening standalone server.
|
|
2
|
-
$connections = @(Get-NetTCPConnection -RemotePort 45839 -State Established -ErrorAction SilentlyContinue | Where-Object {
|
|
3
|
-
$_.RemoteAddress -eq '127.0.0.1' -and
|
|
4
|
-
(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName -eq 'ChatGPT'
|
|
5
|
-
})
|
|
6
|
-
if ($connections.Count) { 'ws://127.0.0.1:45839' }
|
|
1
|
+
# Detect the Desktop connection, not merely a listening standalone server.
|
|
2
|
+
$connections = @(Get-NetTCPConnection -RemotePort 45839 -State Established -ErrorAction SilentlyContinue | Where-Object {
|
|
3
|
+
$_.RemoteAddress -eq '127.0.0.1' -and
|
|
4
|
+
(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName -eq 'ChatGPT'
|
|
5
|
+
})
|
|
6
|
+
if ($connections.Count) { 'ws://127.0.0.1:45839' }
|