@walkhi/code-relax 0.1.0-beta.1 → 0.1.0-beta.11
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 +53 -64
- package/dist/bin/codex-remote.mjs +90 -71
- package/dist/bin/self-relay-admin.mjs +8 -0
- package/dist/bin/self-relay-demo.mjs +1 -1
- package/dist/bin/self-relay-server.mjs +5 -2
- package/dist/shared/app-server-events.cjs +64 -24
- package/dist/shared/file-mentions.cjs +35 -0
- package/dist/shared/p2p-data-channel.cjs +50 -0
- package/dist/skills/code-relax/SKILL.md +1 -1
- package/dist/src/desktop-launcher.mjs +16 -21
- package/dist/src/file-upload-session.mjs +124 -0
- package/dist/src/lan-socket-server.mjs +59 -10
- package/dist/src/managed-app-server.mjs +36 -14
- package/dist/src/message-images.mjs +0 -18
- package/dist/src/onboarding.mjs +6 -8
- package/dist/src/platform/README.md +1 -1
- package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
- package/dist/src/platform/windows/background-process.mjs +7 -2
- package/dist/src/platform/windows/codex-runtime.mjs +117 -0
- package/dist/src/platform/windows/desktop-host.mjs +17 -3
- package/dist/src/platform/windows/desktop-info.ps1 +2 -4
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
- package/dist/src/platform/windows/launch-worker.mjs +42 -0
- package/dist/src/platform/windows/resident-startup.ps1 +58 -18
- package/dist/src/platform/windows/service-host.mjs +4 -16
- package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
- package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
- package/dist/src/runtime-discovery.mjs +43 -0
- package/dist/src/self-relay/admin-cloud.mjs +101 -0
- package/dist/src/self-relay/admin-page.mjs +71 -0
- package/dist/src/self-relay/admin-server.mjs +61 -0
- package/dist/src/self-relay/admin-state.mjs +122 -0
- package/dist/src/self-relay/client.mjs +2 -0
- package/dist/src/self-relay/connector.mjs +107 -20
- package/dist/src/self-relay/demo.mjs +4 -5
- package/dist/src/self-relay/lifecycle.mjs +58 -70
- package/dist/src/self-relay/p2p-probe.mjs +123 -83
- package/dist/src/self-relay/server.mjs +224 -29
- package/dist/src/server.mjs +616 -1299
- package/dist/src/service-doctor.mjs +14 -26
- package/dist/src/service-install.mjs +1 -4
- package/dist/src/service-lifecycle.mjs +12 -12
- package/dist/src/shared-app-server.mjs +206 -46
- package/dist/src/shared-catalog.mjs +5 -25
- package/dist/src/shared-recovery.mjs +11 -9
- package/dist/src/shared-thread-stream.mjs +1 -1
- package/dist/src/skill-install.mjs +0 -13
- package/package.json +16 -18
- package/tools/postinstall.mjs +140 -3
- package/dist/src/app-server-client.mjs +0 -468
- package/dist/src/app-server-tasks.mjs +0 -355
- package/dist/src/debug-ui.mjs +0 -131
- package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
- package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
- package/dist/src/platform/windows/desktop-tools.mjs +0 -48
- package/dist/src/platform/windows/launch-worker.ps1 +0 -13
- package/dist/src/thread-catalog.mjs +0 -42
- package/dist/tools/find-desktop-pipe.mjs +0 -10
- package/dist/web/approval-ui.js +0 -188
- package/dist/web/capabilities.js +0 -57
- package/dist/web/chat-transport.js +0 -196
- package/dist/web/chat.css +0 -736
- package/dist/web/chat.js +0 -5149
- package/dist/web/client-platform.js +0 -11
- package/dist/web/harmony-platform.js +0 -51
- package/dist/web/highlight-powershell.min.js +0 -39
- package/dist/web/highlight.min.js +0 -1262
- package/dist/web/history-cache.js +0 -146
- package/dist/web/index.html +0 -222
- package/dist/web/lan-session.js +0 -122
- package/dist/web/p2p-diagnostics.js +0 -28
- package/dist/web/p2p-probe.js +0 -83
- package/dist/web/resources.json +0 -1
- package/dist/web/self-relay-session.js +0 -119
- package/dist/web/server-requests.js +0 -67
- package/dist/web/timeline-sync.js +0 -67
- package/dist/web/vendor/README.md +0 -14
- package/dist/web/vendor/highlight-powershell.min.js +0 -39
- package/dist/web/vendor/highlight.LICENSE.txt +0 -29
- package/dist/web/vendor/highlight.min.js +0 -1262
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { readRecord, writeRecord } from '../../service-lifecycle.mjs';
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
const requiredMethods = ['thread/start', 'thread/resume', 'thread/list', 'thread/turns/list',
|
|
11
|
+
'thread/items/list', 'turn/start', 'turn/steer', 'turn/interrupt', 'model/list', 'project/list',
|
|
12
|
+
'thread/settings/update', 'configRequirements/read', 'thread/queue/list', 'thread/queue/add',
|
|
13
|
+
'thread/queue/delete', 'thread/queue/start', 'thread/read', 'thread/unsubscribe', 'config/read', 'account/rateLimits/read'];
|
|
14
|
+
const fingerprint = file => { const s = fs.statSync(file); return `${s.size}:${s.mtimeMs}`; };
|
|
15
|
+
|
|
16
|
+
// Resolve native binaries, never execute a shell shim from a background task.
|
|
17
|
+
export function discoverCodex(environment = process.env) {
|
|
18
|
+
const found = new Map();
|
|
19
|
+
const add = (file, source) => {
|
|
20
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) return;
|
|
21
|
+
const executable = fs.realpathSync(file);
|
|
22
|
+
if (!found.has(executable.toLowerCase()) || source === 'desktop') found.set(executable.toLowerCase(), { executable, source });
|
|
23
|
+
};
|
|
24
|
+
if (environment.CODE_RELAX_CODEX_PATH) {
|
|
25
|
+
const file = environment.CODE_RELAX_CODEX_PATH;
|
|
26
|
+
if (!path.isAbsolute(file) || path.basename(file).toLowerCase() !== 'codex.exe') throw new Error('CODE_RELAX_CODEX_PATH 必须是 codex.exe 的绝对路径。');
|
|
27
|
+
add(file, 'explicit');
|
|
28
|
+
if (!found.size) throw new Error('CODE_RELAX_CODEX_PATH 指定的程序不存在。');
|
|
29
|
+
return [...found.values()];
|
|
30
|
+
}
|
|
31
|
+
const local = environment.LOCALAPPDATA || path.join(os.homedir(), 'AppData/Local');
|
|
32
|
+
const home = environment.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
33
|
+
const directories = [...String(environment.PATH || environment.Path || '').split(';').filter(Boolean),
|
|
34
|
+
environment.CODEX_INSTALL_DIR, path.join(local, 'Programs/OpenAI/Codex/bin'),
|
|
35
|
+
path.join(home, 'packages/standalone/current/bin')].filter(Boolean);
|
|
36
|
+
for (const directory of directories) {
|
|
37
|
+
add(path.join(directory, 'codex.exe'), 'cli');
|
|
38
|
+
const packageFile = path.join(directory, 'node_modules/@openai/codex/package.json');
|
|
39
|
+
if (!fs.existsSync(packageFile)) continue;
|
|
40
|
+
const root = path.dirname(fs.realpathSync(packageFile));
|
|
41
|
+
let vendor = path.join(root, 'vendor');
|
|
42
|
+
try { vendor = path.join(path.dirname(createRequire(packageFile).resolve('@openai/codex-win32-x64/package.json')), 'vendor'); }
|
|
43
|
+
catch (error) { if (error.code !== 'MODULE_NOT_FOUND') throw error; }
|
|
44
|
+
add(path.join(vendor, 'x86_64-pc-windows-msvc/bin/codex.exe'), 'npm');
|
|
45
|
+
}
|
|
46
|
+
const desktopRoot = path.join(local, 'OpenAI/Codex/bin');
|
|
47
|
+
add(path.join(desktopRoot, 'codex.exe'), 'desktop');
|
|
48
|
+
if (fs.existsSync(desktopRoot)) for (const entry of fs.readdirSync(desktopRoot, { withFileTypes: true })) {
|
|
49
|
+
if (entry.isDirectory()) add(path.join(desktopRoot, entry.name, 'codex.exe'), 'desktop');
|
|
50
|
+
}
|
|
51
|
+
return [...found.values()];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function compareVersions(a, b) {
|
|
55
|
+
const parse = v => v.split(/[.-]/).map(part => /^\d+$/.test(part) ? Number(part) : part);
|
|
56
|
+
const aa = parse(a), bb = parse(b);
|
|
57
|
+
for (let i = 0; i < Math.max(aa.length, bb.length); i++) {
|
|
58
|
+
if (aa[i] === bb[i]) continue;
|
|
59
|
+
if (aa[i] === undefined) return 1;
|
|
60
|
+
if (bb[i] === undefined) return -1;
|
|
61
|
+
return typeof aa[i] === 'number' && typeof bb[i] === 'number' ? aa[i] - bb[i] : String(aa[i]).localeCompare(String(bb[i]));
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function verifyProtocol(executable, stateRoot) {
|
|
67
|
+
const directory = fs.mkdtempSync(path.join(stateRoot, 'cli-schema-'));
|
|
68
|
+
try {
|
|
69
|
+
await exec(executable, ['app-server', 'generate-json-schema', '--experimental', '--out', directory],
|
|
70
|
+
{ windowsHide: true, timeout: 30000, maxBuffer: 4 * 1024 * 1024 });
|
|
71
|
+
const request = fs.readFileSync(path.join(directory, 'ClientRequest.json'), 'utf8');
|
|
72
|
+
const missing = requiredMethods.filter(method => !request.includes(`"${method}"`));
|
|
73
|
+
const turns = fs.readFileSync(path.join(directory, 'v2/ThreadTurnsListParams.json'), 'utf8');
|
|
74
|
+
if (!turns.includes('"itemsView"') || !turns.includes('"summary"')) missing.push('turn summary pagination');
|
|
75
|
+
if (missing.length) throw new Error(`缺少协议能力:${missing.join(', ')}`);
|
|
76
|
+
} finally {
|
|
77
|
+
const remove = dir => {
|
|
78
|
+
for (const item of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
79
|
+
const file = path.join(dir, item.name);
|
|
80
|
+
if (item.isDirectory()) remove(file); else fs.unlinkSync(file);
|
|
81
|
+
}
|
|
82
|
+
fs.rmdirSync(dir);
|
|
83
|
+
};
|
|
84
|
+
remove(directory);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function resolveCodexRuntime(stateRoot, { refresh = false, environment = process.env } = {}) {
|
|
89
|
+
fs.mkdirSync(stateRoot, { recursive: true });
|
|
90
|
+
const recordFile = path.join(stateRoot, 'codex-runtime.json');
|
|
91
|
+
const saved = readRecord(recordFile);
|
|
92
|
+
environment = { ...environment,
|
|
93
|
+
CODEX_HOME: environment.CODEX_HOME || saved?.codexHome || path.join(os.homedir(), '.codex'),
|
|
94
|
+
CODE_RELAX_CODEX_PATH: environment.CODE_RELAX_CODEX_PATH || (saved?.source === 'explicit' ? saved.executable : '') };
|
|
95
|
+
if (!refresh && saved?.executable && fs.existsSync(saved.executable)
|
|
96
|
+
&& saved.fingerprint === fingerprint(saved.executable)) return saved;
|
|
97
|
+
const candidates = [], rejected = [];
|
|
98
|
+
for (const candidate of discoverCodex(environment)) {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await exec(candidate.executable, ['--version'], { windowsHide: true, timeout: 10000 });
|
|
101
|
+
const version = /^codex-cli (\d+\.\d+\.\d+(?:-[\w.]+)?)/m.exec(stdout)?.[1];
|
|
102
|
+
if (!version) throw new Error('无法识别版本');
|
|
103
|
+
candidates.push({ ...candidate, version });
|
|
104
|
+
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
105
|
+
}
|
|
106
|
+
candidates.sort((a, b) => Number(b.source === 'desktop') - Number(a.source === 'desktop') || compareVersions(b.version, a.version));
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
try {
|
|
109
|
+
await verifyProtocol(candidate.executable, stateRoot);
|
|
110
|
+
const result = { ...candidate, fingerprint: fingerprint(candidate.executable),
|
|
111
|
+
codexHome: path.resolve(environment.CODEX_HOME || path.join(os.homedir(), '.codex')) };
|
|
112
|
+
writeRecord(recordFile, result);
|
|
113
|
+
return result;
|
|
114
|
+
} catch (error) { rejected.push(`${candidate.executable}: ${error.message}`); }
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`未找到兼容的 Codex CLI,请安装或更新 Codex CLI 后执行 relax setup。${rejected.length ? '\n' + rejected.join('\n') : ''}`);
|
|
117
|
+
}
|
|
@@ -17,15 +17,29 @@ export const desktopHost = {
|
|
|
17
17
|
sharedServiceUrl,
|
|
18
18
|
start: startWindowsProcess,
|
|
19
19
|
closeDesktop: executable => runScript('stop-desktop.ps1', ['-Executable', executable]),
|
|
20
|
-
stop: pid => process.kill(pid),
|
|
20
|
+
stop: pid => process.kill(pid),
|
|
21
|
+
async prepareKernelRestart() {
|
|
22
|
+
const info = await this.inspect();
|
|
23
|
+
if (!info.running || !await this.sharedServiceUrl()) return null;
|
|
24
|
+
await this.closeDesktop(info.desktop);
|
|
25
|
+
return info.desktop;
|
|
26
|
+
},
|
|
27
|
+
async resumeAfterKernelRestart(executable, environment) {
|
|
28
|
+
if (!executable) return;
|
|
29
|
+
await this.start(executable, [], environment, []);
|
|
30
|
+
const deadline = Date.now() + 30000;
|
|
31
|
+
while (!await this.sharedServiceUrl()) {
|
|
32
|
+
if (Date.now() >= deadline) throw new Error('Codex 内核已就绪,但 Desktop 未完成重新接入。');
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
34
|
+
}
|
|
35
|
+
},
|
|
21
36
|
ready: async url => { try { return (await fetch(url, { signal: AbortSignal.timeout(1000) })).ok; } catch { return false; } },
|
|
22
37
|
};
|
|
23
38
|
export const resolveStateRoot = stateRoot => runScript('resolve-state-root.ps1', ['-StateRoot', stateRoot]);
|
|
24
39
|
|
|
25
|
-
export async function
|
|
40
|
+
export async function removeDesktopShortcut({ serviceRoot, nodeExecutable }, destination) {
|
|
26
41
|
const entry = path.join(serviceRoot, 'dist/bin/codex-remote.mjs');
|
|
27
42
|
const args = ['-NodePath', nodeExecutable, '-Entry', entry];
|
|
28
|
-
if (remove) args.push('-Remove');
|
|
29
43
|
if (destination) args.push('-Destination', destination);
|
|
30
44
|
return JSON.parse(await runScript('desktop-shortcuts.ps1', args));
|
|
31
45
|
}
|
|
@@ -8,9 +8,7 @@ if ($ProcessId -gt 0) {
|
|
|
8
8
|
exit
|
|
9
9
|
}
|
|
10
10
|
$package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
|
|
11
|
-
if (-not $package) {
|
|
11
|
+
if (-not $package) { @{ desktop=$null; running=$false } | ConvertTo-Json -Compress; exit }
|
|
12
12
|
$desktop = Join-Path $package.InstallLocation 'app/ChatGPT.exe'
|
|
13
13
|
$running = @(Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $desktop })
|
|
14
|
-
|
|
15
|
-
$cli = Get-ChildItem -LiteralPath $binaryRoot -Filter codex.exe -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
16
|
-
@{ desktop=$desktop; cli=$cli.FullName; running=($running.Count -gt 0) } | ConvertTo-Json -Compress
|
|
14
|
+
@{ desktop=$desktop; running=($running.Count -gt 0) } | ConvertTo-Json -Compress
|
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
param([string]$NodePath, [string]$Entry, [
|
|
1
|
+
param([string]$NodePath, [string]$Entry, [string]$Destination)
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
3
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
4
|
$shell = New-Object -ComObject WScript.Shell
|
|
5
5
|
if (-not $Destination) { $Destination = $shell.SpecialFolders.Item('Desktop') }
|
|
6
|
-
|
|
7
|
-
$
|
|
6
|
+
# Only identify and remove the retired shortcut; never create a new launcher.
|
|
7
|
+
$launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
|
|
8
|
+
$items = @(@{Name='Code Relax - Codex 内核'; Command='shared-start'})
|
|
8
9
|
$results = @()
|
|
9
10
|
foreach ($item in $items) {
|
|
10
11
|
$target = Join-Path $Destination ($item.Name + '.lnk')
|
|
11
12
|
$arguments = '-NoProfile -NoExit -ExecutionPolicy Bypass -File "' + $launcher + '" -NodePath "' + $NodePath + '" -Entry "' + $Entry + '" -Action ' + $item.Command
|
|
12
13
|
if (Test-Path -LiteralPath $target) {
|
|
13
14
|
$existing = $shell.CreateShortcut($target)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if ($Remove) {
|
|
17
|
-
if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target }
|
|
18
|
-
} else {
|
|
19
|
-
$link = $shell.CreateShortcut($target)
|
|
20
|
-
$link.TargetPath = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
|
|
21
|
-
$link.Arguments = $arguments
|
|
22
|
-
$link.WorkingDirectory = Split-Path $Entry -Parent
|
|
23
|
-
$link.Description = $item.Name
|
|
24
|
-
$link.Save()
|
|
15
|
+
$powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
|
|
16
|
+
if ($existing.Arguments -ne $arguments -or $existing.TargetPath -ne $powershell) { continue }
|
|
25
17
|
}
|
|
18
|
+
if (Test-Path -LiteralPath $target) { Remove-Item -LiteralPath $target }
|
|
26
19
|
$results += $target
|
|
27
20
|
}
|
|
28
21
|
ConvertTo-Json -InputObject $results -Compress
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
const [requestPath, readyPath, instance, diagnosticPath] = process.argv.slice(2);
|
|
5
|
+
let result = { instance, brokerPid: process.pid };
|
|
6
|
+
|
|
7
|
+
function diagnostic(message) {
|
|
8
|
+
if (!diagnosticPath) return;
|
|
9
|
+
fs.appendFileSync(diagnosticPath, `${new Date().toISOString()} ${message}\n`, 'utf8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const spec = JSON.parse(fs.readFileSync(requestPath, 'utf8').replace(/^\uFEFF/u, ''));
|
|
14
|
+
diagnostic('request loaded');
|
|
15
|
+
if (spec.instance !== instance) throw new Error('Windows process broker request identity mismatch.');
|
|
16
|
+
fs.rmSync(requestPath, { force: true });
|
|
17
|
+
const descriptors = (spec.logs || []).map(file => fs.openSync(file, 'a'));
|
|
18
|
+
try {
|
|
19
|
+
const child = spawn(spec.executable, spec.args || [], {
|
|
20
|
+
windowsHide: true,
|
|
21
|
+
env: spec.environment,
|
|
22
|
+
stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'],
|
|
23
|
+
});
|
|
24
|
+
await new Promise((resolve, reject) => {
|
|
25
|
+
child.once('spawn', resolve);
|
|
26
|
+
child.once('error', reject);
|
|
27
|
+
});
|
|
28
|
+
// Keep this WMI-owned broker alive with the target so Codex tools inherit
|
|
29
|
+
// its hidden console instead of creating a visible console per command.
|
|
30
|
+
result.pid = child.pid;
|
|
31
|
+
diagnostic(`target started (PID ${child.pid})`);
|
|
32
|
+
} finally {
|
|
33
|
+
descriptors.forEach(descriptor => fs.closeSync(descriptor));
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
result.error = error?.stack || error?.message || String(error);
|
|
37
|
+
try { diagnostic(`failed: ${result.error}`); } catch {}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const temporary = `${readyPath}.tmp`;
|
|
41
|
+
fs.writeFileSync(temporary, JSON.stringify(result), 'utf8');
|
|
42
|
+
fs.renameSync(temporary, readyPath);
|
|
@@ -1,27 +1,67 @@
|
|
|
1
1
|
param([string]$NodePath, [string]$Entry, [ValidateSet('enable','disable','status')][string]$Action='status')
|
|
2
2
|
$ErrorActionPreference = 'Stop'
|
|
3
3
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
4
|
+
|
|
5
|
+
$taskName = 'Code Relax Bridge'
|
|
6
|
+
$taskSource = '@walkhi/code-relax'
|
|
7
|
+
$taskPath = '\' + $taskName
|
|
8
|
+
$arguments = '"' + $Entry + '" start --json'
|
|
9
|
+
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
10
|
+
|
|
11
|
+
$scheduler = New-Object -ComObject 'Schedule.Service'
|
|
12
|
+
$scheduler.Connect()
|
|
13
|
+
$root = $scheduler.GetFolder('\')
|
|
14
|
+
$registered = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
|
|
15
|
+
if ($registered.Count -gt 0 -and $registered[0].Definition.RegistrationInfo.Source -ne $taskSource) {
|
|
16
|
+
throw 'Scheduled task is not owned by this installation.'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Remove only the legacy shortcut created by this package. A Startup LNK that
|
|
20
|
+
# launches hidden PowerShell with ExecutionPolicy Bypass triggers AV heuristics.
|
|
4
21
|
$shell = New-Object -ComObject WScript.Shell
|
|
5
|
-
$
|
|
6
|
-
$
|
|
7
|
-
$
|
|
8
|
-
$
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
throw 'Startup shortcut is not owned by this installation.'
|
|
22
|
+
$legacyPath = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
|
|
23
|
+
$legacyLauncher = Join-Path $PSScriptRoot 'resident-start.ps1'
|
|
24
|
+
$legacyExists = Test-Path -LiteralPath $legacyPath
|
|
25
|
+
if ($Action -ne 'status' -and $legacyExists) {
|
|
26
|
+
$legacy = $shell.CreateShortcut($legacyPath)
|
|
27
|
+
if (-not $legacy.Arguments.Contains('"' + $legacyLauncher + '"')) {
|
|
28
|
+
throw 'Legacy startup shortcut is not owned by this installation.'
|
|
13
29
|
}
|
|
14
30
|
}
|
|
31
|
+
|
|
15
32
|
if ($Action -eq 'enable') {
|
|
16
33
|
if (-not (Test-Path -LiteralPath $NodePath) -or -not (Test-Path -LiteralPath $Entry)) { throw 'Installed runtime is missing.' }
|
|
17
|
-
$
|
|
18
|
-
$
|
|
19
|
-
$
|
|
20
|
-
$
|
|
21
|
-
$
|
|
22
|
-
$
|
|
23
|
-
$
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
$definition = $scheduler.NewTask(0)
|
|
35
|
+
$definition.RegistrationInfo.Description = 'Start the Code Relax Bridge when this user signs in.'
|
|
36
|
+
$definition.RegistrationInfo.Source = $taskSource
|
|
37
|
+
$definition.Principal.UserId = $currentUser
|
|
38
|
+
$definition.Principal.LogonType = 3 # TASK_LOGON_INTERACTIVE_TOKEN
|
|
39
|
+
$definition.Principal.RunLevel = 0 # TASK_RUNLEVEL_LUA
|
|
40
|
+
$definition.Settings.Enabled = $true
|
|
41
|
+
$definition.Settings.Hidden = $true
|
|
42
|
+
$definition.Settings.StartWhenAvailable = $true
|
|
43
|
+
$definition.Settings.DisallowStartIfOnBatteries = $false
|
|
44
|
+
$definition.Settings.StopIfGoingOnBatteries = $false
|
|
45
|
+
$definition.Settings.ExecutionTimeLimit = 'PT5M'
|
|
46
|
+
$definition.Settings.MultipleInstances = 2 # TASK_INSTANCES_IGNORE_NEW
|
|
47
|
+
$trigger = $definition.Triggers.Create(9) # TASK_TRIGGER_LOGON
|
|
48
|
+
$trigger.UserId = $currentUser
|
|
49
|
+
$trigger.Enabled = $true
|
|
50
|
+
$exec = $definition.Actions.Create(0) # TASK_ACTION_EXEC
|
|
51
|
+
$exec.Path = $NodePath
|
|
52
|
+
$exec.Arguments = $arguments
|
|
53
|
+
$exec.WorkingDirectory = $env:USERPROFILE
|
|
54
|
+
[void]$root.RegisterTaskDefinition($taskName, $definition, 6, $null, $null, 3, $null) # TASK_CREATE_OR_UPDATE
|
|
55
|
+
} elseif ($Action -eq 'disable' -and $registered.Count -gt 0) {
|
|
56
|
+
$root.DeleteTask($taskName, 0)
|
|
26
57
|
}
|
|
27
|
-
|
|
58
|
+
if ($Action -ne 'status' -and $legacyExists) { Remove-Item -LiteralPath $legacyPath }
|
|
59
|
+
|
|
60
|
+
$active = @($root.GetTasks(1) | Where-Object { $_.Name -eq $taskName } | Select-Object -First 1)
|
|
61
|
+
$taskEnabled = $active.Count -gt 0 -and $active[0].Enabled
|
|
62
|
+
@{
|
|
63
|
+
enabled=($taskEnabled -or ($Action -eq 'status' -and $legacyExists))
|
|
64
|
+
path=$(if ($taskEnabled) { 'Task Scheduler:' + $taskPath } elseif ($legacyExists) { $legacyPath } else { 'Task Scheduler:' + $taskPath })
|
|
65
|
+
scope='current-user-logon'
|
|
66
|
+
mechanism=$(if ($taskEnabled) { 'task-scheduler' } elseif ($legacyExists) { 'legacy-startup-shortcut' } else { 'task-scheduler' })
|
|
67
|
+
} | ConvertTo-Json -Compress
|
|
@@ -20,24 +20,12 @@ async function detect(script) {
|
|
|
20
20
|
export const sharedServiceUrl = () => detect('get-shared-service-url.ps1');
|
|
21
21
|
export const lanAddress = async () => (await lanNetworks())[0]?.address || '';
|
|
22
22
|
export { lanNetworks };
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const target = path.join(stateRoot, name);
|
|
27
|
-
const old = path.join(bridgeRoot, '../.codex/bridge', name);
|
|
28
|
-
if (!fs.existsSync(target) && fs.existsSync(old)) fs.copyFileSync(old, target, fs.constants.COPYFILE_EXCL);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export async function desktopEnvironment(context, sharedUrl) {
|
|
32
|
-
const environment = { ...process.env };
|
|
33
|
-
environment.CODEX_THREAD_ID ||= environment.CODEX_SESSION_ID || context?.sourceThreadId;
|
|
34
|
-
if (sharedUrl) environment.CODEX_REMOTE_SHARED_WS_URL = sharedUrl;
|
|
35
|
-
else delete environment.CODEX_REMOTE_SHARED_WS_URL;
|
|
36
|
-
const installation = readRecord(path.join(runtimePaths(environment).serviceRoot, 'installation.json'));
|
|
23
|
+
export async function bridgeEnvironment() {
|
|
24
|
+
const environment = { ...process.env };
|
|
25
|
+
const installation = readRecord(path.join(runtimePaths(environment).serviceRoot, 'installation.json'));
|
|
37
26
|
const executable = installation?.nodeExecutable;
|
|
38
27
|
if (!executable || !fs.existsSync(executable)) throw new Error('安装时选用的 Node 不存在,请重新安装 Code Relax。');
|
|
39
28
|
environment.CODEX_MCP_NODE_PATH = executable;
|
|
40
|
-
environment.
|
|
41
|
-
if (sharedUrl) delete environment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
29
|
+
delete environment.CODEX_APP_TOOLS_PIPE_PATH;
|
|
42
30
|
return { executable, environment };
|
|
43
31
|
}
|
|
@@ -10,30 +10,40 @@ try {
|
|
|
10
10
|
Add-Type -Path (Join-Path $PSScriptRoot 'IsolatedProcess.cs')
|
|
11
11
|
$launchRoot = [IsolatedProcess]::ResolvePath($launchRoot)
|
|
12
12
|
$ready = Join-Path $launchRoot ($instance + '.json')
|
|
13
|
+
$request = Join-Path $launchRoot ($instance + '.request.json')
|
|
14
|
+
$diagnostic = Join-Path $launchRoot ($instance + '.broker.log')
|
|
13
15
|
$spec | Add-Member -NotePropertyName instance -NotePropertyValue $instance
|
|
14
|
-
$spec | Add-Member -NotePropertyName
|
|
15
|
-
$
|
|
16
|
-
$
|
|
16
|
+
$spec | Add-Member -NotePropertyName diagnostic -NotePropertyValue $diagnostic
|
|
17
|
+
$requestTemporary = $request + '.tmp'
|
|
18
|
+
[IO.File]::WriteAllText($requestTemporary, ($spec | ConvertTo-Json -Compress -Depth 10), (New-Object Text.UTF8Encoding($false)))
|
|
19
|
+
[IO.File]::Move($requestTemporary, $request)
|
|
17
20
|
$startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{
|
|
18
|
-
ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
|
|
21
|
+
ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
|
|
19
22
|
}
|
|
20
|
-
$
|
|
21
|
-
$command = '"' + $
|
|
23
|
+
$worker = Join-Path $PSScriptRoot 'launch-worker.mjs'
|
|
24
|
+
$command = '"' + [string]$spec.brokerExecutable + '" "' + $worker + '" "' + $request + '" "' + $ready + '" "' + $instance + '" "' + $diagnostic + '"'
|
|
22
25
|
$broker = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{
|
|
23
26
|
CommandLine=$command; CurrentDirectory=$env:USERPROFILE; ProcessStartupInformation=$startup
|
|
24
27
|
}
|
|
25
28
|
if ($broker.ReturnValue -ne 0) { throw ('Windows process broker failed: ' + $broker.ReturnValue) }
|
|
26
|
-
$deadline = [DateTime]::UtcNow.AddSeconds(
|
|
29
|
+
$deadline = [DateTime]::UtcNow.AddSeconds(30)
|
|
27
30
|
while (-not (Test-Path -LiteralPath $ready)) {
|
|
28
|
-
if ([DateTime]::UtcNow -gt $deadline) {
|
|
31
|
+
if ([DateTime]::UtcNow -gt $deadline) {
|
|
32
|
+
$detail = if (Test-Path -LiteralPath $diagnostic) { (Get-Content -LiteralPath $diagnostic -Raw -Encoding UTF8).Trim() } else { 'broker did not create a diagnostic log' }
|
|
33
|
+
throw ('Windows process broker handoff timed out (broker PID ' + $broker.ProcessId + '): ' + $detail)
|
|
34
|
+
}
|
|
29
35
|
Start-Sleep -Milliseconds 50
|
|
30
36
|
}
|
|
31
37
|
$result = Get-Content -LiteralPath $ready -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
32
38
|
Remove-Item -LiteralPath $ready
|
|
33
39
|
if ($result.instance -ne $instance -or $result.brokerPid -ne $broker.ProcessId) { throw 'Windows process broker identity mismatch.' }
|
|
34
40
|
if ($result.error) { throw [string]$result.error }
|
|
41
|
+
if (Test-Path -LiteralPath $diagnostic) { Remove-Item -LiteralPath $diagnostic -Force }
|
|
35
42
|
[Console]::WriteLine([int]$result.pid)
|
|
36
43
|
} catch {
|
|
37
44
|
[Console]::Error.WriteLine($_.Exception.Message)
|
|
38
45
|
exit 1
|
|
46
|
+
} finally {
|
|
47
|
+
if ($request -and (Test-Path -LiteralPath $request)) { Remove-Item -LiteralPath $request -Force }
|
|
48
|
+
if ($requestTemporary -and (Test-Path -LiteralPath $requestTemporary)) { Remove-Item -LiteralPath $requestTemporary -Force }
|
|
39
49
|
}
|
|
@@ -5,15 +5,18 @@ $package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
|
|
|
5
5
|
if (-not $package -or $Executable -ne (Join-Path $package.InstallLocation 'app/ChatGPT.exe')) {
|
|
6
6
|
throw 'Desktop executable identity changed; recovery stopped.'
|
|
7
7
|
}
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
$
|
|
8
|
+
# Never kill another user's Desktop or an app-server process tree. The independent
|
|
9
|
+
# installer/recovery worker must survive and relaunch only in this interactive session.
|
|
10
|
+
$currentSessionId = (Get-Process -Id $PID -ErrorAction Stop).SessionId
|
|
11
|
+
$workers = @(Get-CimInstance Win32_Process -Filter "Name='ChatGPT.exe'" | Where-Object {
|
|
12
|
+
$_.ExecutablePath -eq $Executable -and $_.SessionId -eq $currentSessionId
|
|
13
|
+
})
|
|
11
14
|
foreach ($worker in $workers) {
|
|
12
15
|
$actual = Get-CimInstance Win32_Process -Filter "ProcessId=$($worker.ProcessId)"
|
|
13
16
|
if ($actual -and $actual.ExecutablePath -eq $worker.ExecutablePath -and $actual.CreationDate -eq $worker.CreationDate) {
|
|
14
17
|
Stop-Process -Id $worker.ProcessId -Force -ErrorAction Stop
|
|
15
18
|
}
|
|
16
19
|
}
|
|
17
|
-
if (Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $Executable }) {
|
|
20
|
+
if (Get-Process ChatGPT -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $Executable -and $_.SessionId -eq $currentSessionId }) {
|
|
18
21
|
throw 'Desktop did not exit; recovery stopped.'
|
|
19
22
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { discoverCodex, resolveCodexRuntime } from './platform/windows/codex-runtime.mjs';
|
|
3
|
+
|
|
4
|
+
// Advisory discovery is independent of execution health. Failure here must never
|
|
5
|
+
// disable an already running app-server or cause an automatic process restart.
|
|
6
|
+
export class RuntimeDiscovery {
|
|
7
|
+
constructor({ stateRoot, host, url, interval = 60000 }) {
|
|
8
|
+
Object.assign(this, { stateRoot, host, url, interval });
|
|
9
|
+
this.state = {};
|
|
10
|
+
}
|
|
11
|
+
start() { this.stopped = false; void this.tick(); }
|
|
12
|
+
stop() { this.stopped = true; clearTimeout(this.timer); }
|
|
13
|
+
async sample() {
|
|
14
|
+
const errors = [];
|
|
15
|
+
try {
|
|
16
|
+
const candidates = discoverCodex();
|
|
17
|
+
const signature = JSON.stringify(candidates.map(candidate => {
|
|
18
|
+
const stat = fs.statSync(candidate.executable);
|
|
19
|
+
return [candidate.executable, stat.size, stat.mtimeMs];
|
|
20
|
+
}));
|
|
21
|
+
if (signature !== this.signature) {
|
|
22
|
+
await resolveCodexRuntime(this.stateRoot, { refresh: true });
|
|
23
|
+
this.signature = signature;
|
|
24
|
+
}
|
|
25
|
+
} catch (error) { errors.push(error.message); }
|
|
26
|
+
try {
|
|
27
|
+
const desktop = await this.host.inspect();
|
|
28
|
+
const attached = desktop.running && await this.host.sharedServiceUrl() === this.url;
|
|
29
|
+
this.state = { desktop: { state: !desktop.running ? 'stopped' : attached ? 'managed' : 'ordinary',
|
|
30
|
+
installed: Boolean(desktop.desktop), attachedToManagedServer: Boolean(attached) },
|
|
31
|
+
desktopRestartRecommended: Boolean(desktop.running && !attached), checkedAt: new Date().toISOString() };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
errors.push(error.message);
|
|
34
|
+
this.state = { checkedAt: new Date().toISOString() };
|
|
35
|
+
}
|
|
36
|
+
if (errors.length) this.state.discoveryError = errors.join('\n');
|
|
37
|
+
return this.state;
|
|
38
|
+
}
|
|
39
|
+
async tick() {
|
|
40
|
+
await this.sample();
|
|
41
|
+
if (!this.stopped) { this.timer = setTimeout(() => this.tick(), this.interval); this.timer.unref?.(); }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import tencentcloud from 'tencentcloud-sdk-nodejs-common';
|
|
3
|
+
|
|
4
|
+
const INSTANCE_ID = 'lhins-byrs7n8o';
|
|
5
|
+
const REGION = 'ap-beijing';
|
|
6
|
+
const { CommonClient } = tencentcloud;
|
|
7
|
+
const METRICS = [
|
|
8
|
+
['LighthouseOutratio', 'outRatioPercent', 10],
|
|
9
|
+
['LighthouseOuttraffic', 'outMbps', 10],
|
|
10
|
+
['LighthouseIntraffic', 'inMbps', 10],
|
|
11
|
+
['CpuUsage', 'cpuPercent', 10],
|
|
12
|
+
['MemUsage', 'memoryPercent', 10],
|
|
13
|
+
['DiskUsage', 'diskPercent', 60],
|
|
14
|
+
['CpuLoad1', 'cpuLoad1', 10],
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function latestPoint(result) {
|
|
18
|
+
const point = result?.DataPoints?.[0];
|
|
19
|
+
if (!point?.Values?.length || !point?.Timestamps?.length) return null;
|
|
20
|
+
const index = point.Values.length - 1;
|
|
21
|
+
const value = Number(point.Values[index]);
|
|
22
|
+
const timestamp = Number(point.Timestamps[index]);
|
|
23
|
+
return Number.isFinite(value) && Number.isFinite(timestamp)
|
|
24
|
+
? { value, sampledAt: new Date(timestamp * 1000).toISOString() } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function startAdminCloud({ credentialFile = process.env.CREDENTIALS_DIRECTORY
|
|
28
|
+
? `${process.env.CREDENTIALS_DIRECTORY}/tencent-monitor.json` : null,
|
|
29
|
+
metricIntervalMs = 60000, trafficIntervalMs = 300000 } = {}) {
|
|
30
|
+
if (!credentialFile) return { read: () => ({ configured: false }), close() {} };
|
|
31
|
+
const { secretId, secretKey } = JSON.parse(fs.readFileSync(credentialFile, 'utf8'));
|
|
32
|
+
if (!secretId || !secretKey) throw new Error('Tencent monitor credentials are incomplete');
|
|
33
|
+
const credential = { secretId, secretKey };
|
|
34
|
+
const profile = { httpProfile: { reqTimeout: 8 } };
|
|
35
|
+
const monitor = new CommonClient('monitor.tencentcloudapi.com', '2018-07-24',
|
|
36
|
+
{ credential, region: REGION, profile });
|
|
37
|
+
const lighthouse = new CommonClient('lighthouse.tencentcloudapi.com', '2020-03-24',
|
|
38
|
+
{ credential, region: REGION, profile });
|
|
39
|
+
let closed = false;
|
|
40
|
+
let metricBusy = false;
|
|
41
|
+
let trafficBusy = false;
|
|
42
|
+
const current = { configured: true, metricAvailable: false, trafficAvailable: false,
|
|
43
|
+
outRatioPercent: null, outMbps: null, inMbps: null, metricSampledAt: null,
|
|
44
|
+
cpuPercent: null, memoryPercent: null, diskPercent: null, cpuLoad1: null,
|
|
45
|
+
trafficUsedBytes: null, trafficTotalBytes: null, trafficRemainingBytes: null,
|
|
46
|
+
trafficStartAt: null, trafficEndAt: null, trafficSampledAt: null };
|
|
47
|
+
|
|
48
|
+
async function sampleMetrics() {
|
|
49
|
+
if (metricBusy || closed) return;
|
|
50
|
+
metricBusy = true;
|
|
51
|
+
const end = new Date();
|
|
52
|
+
const base = { Namespace: 'QCE/LIGHTHOUSE',
|
|
53
|
+
StartTime: new Date(end.getTime() - 180000).toISOString(), EndTime: end.toISOString(),
|
|
54
|
+
Instances: [{ Dimensions: [{ Name: 'InstanceId', Value: INSTANCE_ID }] }] };
|
|
55
|
+
try {
|
|
56
|
+
const values = await Promise.allSettled(METRICS.map(([MetricName, , Period]) =>
|
|
57
|
+
monitor.request('GetMonitorData', { ...base, MetricName, Period })));
|
|
58
|
+
if (closed) return;
|
|
59
|
+
const samples = values.map(result => result.status === 'fulfilled' ? latestPoint(result.value) : null);
|
|
60
|
+
for (let index = 0; index < METRICS.length; index++) {
|
|
61
|
+
const [, field] = METRICS[index];
|
|
62
|
+
const value = samples[index]?.value ?? null;
|
|
63
|
+
current[field] = value !== null && (field === 'outMbps' || field === 'inMbps') ? value * 8 : value;
|
|
64
|
+
}
|
|
65
|
+
current.metricSampledAt = samples.reduce((latest, sample) => sample?.sampledAt > latest
|
|
66
|
+
? sample.sampledAt : latest, '') || null;
|
|
67
|
+
current.metricAvailable = Boolean(current.metricSampledAt);
|
|
68
|
+
} catch {
|
|
69
|
+
current.metricAvailable = false;
|
|
70
|
+
} finally { metricBusy = false; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function sampleTraffic() {
|
|
74
|
+
if (trafficBusy || closed) return;
|
|
75
|
+
trafficBusy = true;
|
|
76
|
+
try {
|
|
77
|
+
const data = await lighthouse.request('DescribeInstancesTrafficPackages', { InstanceIds: [INSTANCE_ID] });
|
|
78
|
+
if (closed) return;
|
|
79
|
+
const packages = data?.InstanceTrafficPackageSet?.find(item => item.InstanceId === INSTANCE_ID)?.TrafficPackageSet || [];
|
|
80
|
+
const active = packages.find(item => Date.parse(item.StartTime) <= Date.now()
|
|
81
|
+
&& Date.parse(item.EndTime) >= Date.now()) || packages[0];
|
|
82
|
+
current.trafficUsedBytes = active?.TrafficUsed ?? null;
|
|
83
|
+
current.trafficTotalBytes = active?.TrafficPackageTotal ?? null;
|
|
84
|
+
current.trafficRemainingBytes = active?.TrafficPackageRemaining ?? null;
|
|
85
|
+
current.trafficStartAt = active?.StartTime ?? null;
|
|
86
|
+
current.trafficEndAt = active?.EndTime ?? null;
|
|
87
|
+
current.trafficSampledAt = active ? new Date().toISOString() : null;
|
|
88
|
+
current.trafficAvailable = Boolean(active);
|
|
89
|
+
} catch {
|
|
90
|
+
current.trafficAvailable = false;
|
|
91
|
+
} finally { trafficBusy = false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
void sampleMetrics();
|
|
95
|
+
void sampleTraffic();
|
|
96
|
+
const metricTimer = setInterval(sampleMetrics, metricIntervalMs);
|
|
97
|
+
const trafficTimer = setInterval(sampleTraffic, trafficIntervalMs);
|
|
98
|
+
metricTimer.unref();
|
|
99
|
+
trafficTimer.unref();
|
|
100
|
+
return { read: () => ({ ...current }), close() { closed = true; clearInterval(metricTimer); clearInterval(trafficTimer); } };
|
|
101
|
+
}
|