@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.
Files changed (80) hide show
  1. package/README.md +53 -64
  2. package/dist/bin/codex-remote.mjs +90 -71
  3. package/dist/bin/self-relay-admin.mjs +8 -0
  4. package/dist/bin/self-relay-demo.mjs +1 -1
  5. package/dist/bin/self-relay-server.mjs +5 -2
  6. package/dist/shared/app-server-events.cjs +64 -24
  7. package/dist/shared/file-mentions.cjs +35 -0
  8. package/dist/shared/p2p-data-channel.cjs +50 -0
  9. package/dist/skills/code-relax/SKILL.md +1 -1
  10. package/dist/src/desktop-launcher.mjs +16 -21
  11. package/dist/src/file-upload-session.mjs +124 -0
  12. package/dist/src/lan-socket-server.mjs +59 -10
  13. package/dist/src/managed-app-server.mjs +36 -14
  14. package/dist/src/message-images.mjs +0 -18
  15. package/dist/src/onboarding.mjs +6 -8
  16. package/dist/src/platform/README.md +1 -1
  17. package/dist/src/platform/windows/IsolatedProcess.cs +7 -3
  18. package/dist/src/platform/windows/background-process.mjs +7 -2
  19. package/dist/src/platform/windows/codex-runtime.mjs +117 -0
  20. package/dist/src/platform/windows/desktop-host.mjs +17 -3
  21. package/dist/src/platform/windows/desktop-info.ps1 +2 -4
  22. package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -14
  23. package/dist/src/platform/windows/launch-worker.mjs +42 -0
  24. package/dist/src/platform/windows/resident-startup.ps1 +58 -18
  25. package/dist/src/platform/windows/service-host.mjs +4 -16
  26. package/dist/src/platform/windows/start-hidden-console.ps1 +18 -8
  27. package/dist/src/platform/windows/stop-desktop.ps1 +7 -4
  28. package/dist/src/runtime-discovery.mjs +43 -0
  29. package/dist/src/self-relay/admin-cloud.mjs +101 -0
  30. package/dist/src/self-relay/admin-page.mjs +71 -0
  31. package/dist/src/self-relay/admin-server.mjs +61 -0
  32. package/dist/src/self-relay/admin-state.mjs +122 -0
  33. package/dist/src/self-relay/client.mjs +2 -0
  34. package/dist/src/self-relay/connector.mjs +107 -20
  35. package/dist/src/self-relay/demo.mjs +4 -5
  36. package/dist/src/self-relay/lifecycle.mjs +58 -70
  37. package/dist/src/self-relay/p2p-probe.mjs +123 -83
  38. package/dist/src/self-relay/server.mjs +224 -29
  39. package/dist/src/server.mjs +616 -1299
  40. package/dist/src/service-doctor.mjs +14 -26
  41. package/dist/src/service-install.mjs +1 -4
  42. package/dist/src/service-lifecycle.mjs +12 -12
  43. package/dist/src/shared-app-server.mjs +206 -46
  44. package/dist/src/shared-catalog.mjs +5 -25
  45. package/dist/src/shared-recovery.mjs +11 -9
  46. package/dist/src/shared-thread-stream.mjs +1 -1
  47. package/dist/src/skill-install.mjs +0 -13
  48. package/package.json +16 -18
  49. package/tools/postinstall.mjs +140 -3
  50. package/dist/src/app-server-client.mjs +0 -468
  51. package/dist/src/app-server-tasks.mjs +0 -355
  52. package/dist/src/debug-ui.mjs +0 -131
  53. package/dist/src/platform/windows/desktop-monitor.mjs +0 -34
  54. package/dist/src/platform/windows/desktop-shortcut-run.ps1 +0 -5
  55. package/dist/src/platform/windows/desktop-tools.mjs +0 -48
  56. package/dist/src/platform/windows/launch-worker.ps1 +0 -13
  57. package/dist/src/thread-catalog.mjs +0 -42
  58. package/dist/tools/find-desktop-pipe.mjs +0 -10
  59. package/dist/web/approval-ui.js +0 -188
  60. package/dist/web/capabilities.js +0 -57
  61. package/dist/web/chat-transport.js +0 -196
  62. package/dist/web/chat.css +0 -736
  63. package/dist/web/chat.js +0 -5149
  64. package/dist/web/client-platform.js +0 -11
  65. package/dist/web/harmony-platform.js +0 -51
  66. package/dist/web/highlight-powershell.min.js +0 -39
  67. package/dist/web/highlight.min.js +0 -1262
  68. package/dist/web/history-cache.js +0 -146
  69. package/dist/web/index.html +0 -222
  70. package/dist/web/lan-session.js +0 -122
  71. package/dist/web/p2p-diagnostics.js +0 -28
  72. package/dist/web/p2p-probe.js +0 -83
  73. package/dist/web/resources.json +0 -1
  74. package/dist/web/self-relay-session.js +0 -119
  75. package/dist/web/server-requests.js +0 -67
  76. package/dist/web/timeline-sync.js +0 -67
  77. package/dist/web/vendor/README.md +0 -14
  78. package/dist/web/vendor/highlight-powershell.min.js +0 -39
  79. package/dist/web/vendor/highlight.LICENSE.txt +0 -29
  80. 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 desktopShortcuts({ serviceRoot, nodeExecutable }, remove = false, destination) {
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) { throw '未找到 Codex Desktop,请先安装并登录。' }
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
- $binaryRoot = Join-Path $env:LOCALAPPDATA 'OpenAI/Codex/bin'
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, [switch]$Remove, [string]$Destination)
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
- $launcher = Join-Path $PSScriptRoot 'desktop-shortcut-run.ps1'
7
- $items = @(@{Name='Code Relax - 完整控制'; Command='shared-start'}, @{Name='Code Relax - 基础控制'; Command='desktop-start'})
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
- if ($existing.Arguments -ne $arguments) { throw ('快捷方式不是本安装创建的,未覆盖或删除:' + $target) }
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
- $target = Join-Path $shell.SpecialFolders.Item('Startup') 'Code Relax Bridge.lnk'
6
- $launcher = Join-Path $PSScriptRoot 'resident-start.ps1'
7
- $powershell = Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/powershell.exe'
8
- $exists = Test-Path -LiteralPath $target
9
- if ($exists) {
10
- $existing = $shell.CreateShortcut($target)
11
- if ($existing.TargetPath -ne $powershell -or -not $existing.Arguments.Contains('"' + $launcher + '"')) {
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
- $link = $shell.CreateShortcut($target)
18
- $link.TargetPath = $powershell
19
- $link.Arguments = '-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $launcher + '" -NodePath "' + $NodePath + '" -Entry "' + $Entry + '"'
20
- $link.WorkingDirectory = $env:USERPROFILE
21
- $link.WindowStyle = 7
22
- $link.Description = 'Code Relax resident computer connection'
23
- $link.Save()
24
- } elseif ($Action -eq 'disable' -and $exists) {
25
- Remove-Item -LiteralPath $target
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
- @{ enabled=(Test-Path -LiteralPath $target); path=$target; scope='current-user-logon' } | ConvertTo-Json -Compress
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 migrateState(stateRoot) {
24
- fs.mkdirSync(stateRoot, { recursive: true });
25
- for (const name of ['bridge-config.json', 'bridge-session.json']) {
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.CODEX_REMOTE_LEGACY_UPLOADS_ROOTS = JSON.stringify(installation.legacyUploadsRoots || []);
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 ready -NotePropertyValue $ready
15
- $environment = @(Get-ChildItem Env: | Where-Object { $_.Name -ne 'CODEX_REMOTE_PROCESS_SPEC' } | ForEach-Object { $_.Name + '=' + $_.Value })
16
- $environment += 'CODEX_REMOTE_PROCESS_SPEC=' + ($spec | ConvertTo-Json -Compress -Depth 10)
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; EnvironmentVariables=[string[]]$environment
21
+ ShowWindow=[uint16]0; CreateFlags=[uint32]0x1000010
19
22
  }
20
- $powershell = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
21
- $command = '"' + $powershell + '" -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + (Join-Path $PSScriptRoot 'launch-worker.ps1') + '"'
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(20)
29
+ $deadline = [DateTime]::UtcNow.AddSeconds(30)
27
30
  while (-not (Test-Path -LiteralPath $ready)) {
28
- if ([DateTime]::UtcNow -gt $deadline) { throw 'Windows process broker handoff timed out.' }
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
- # This action is only reached after explicit whole-workstation recovery confirmation.
9
- # Never kill an app-server process tree: the independent recovery worker must survive.
10
- $workers = @(Get-CimInstance Win32_Process -Filter "Name='ChatGPT.exe'" | Where-Object { $_.ExecutablePath -eq $Executable })
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
+ }