@walkhi/code-relax 0.1.0-beta.68 → 0.1.0-beta.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/manage-relay-admin.mjs +20 -20
- package/dist/bin/migrate-relay-db.mjs +109 -109
- package/dist/bin/publish-harmony-release.mjs +46 -46
- package/dist/bin/self-relay-admin.mjs +8 -8
- package/dist/bin/self-relay-demo.mjs +33 -33
- package/dist/bin/self-relay-server.mjs +66 -66
- package/dist/bin/set-relay-admin-password.mjs +11 -11
- package/dist/build-platform.json +1 -1
- package/dist/get-lan-networks.ps1 +40 -40
- package/dist/shared/app-server-events.cjs +511 -511
- package/dist/shared/p2p-data-channel.cjs +72 -72
- package/dist/shared/p2p-diagnostics.cjs +28 -28
- package/dist/shared/p2p-ice-policy.cjs +22 -22
- package/dist/shared/plan-pricing.cjs +32 -32
- package/dist/shared/question-attention.cjs +69 -69
- package/dist/shared/release-version.cjs +31 -31
- package/dist/src/archived-threads.mjs +22 -22
- package/dist/src/background-process.mjs +18 -18
- package/dist/src/code-browser.mjs +100 -100
- package/dist/src/desktop-launcher.mjs +62 -62
- package/dist/src/dots-task-files.mjs +52 -52
- package/dist/src/execution-health.mjs +26 -26
- package/dist/src/lan-socket-server.mjs +1 -1
- package/dist/src/message-images.mjs +9 -9
- package/dist/src/plan-pricing.mjs +47 -47
- package/dist/src/platform/codex-runtime.mjs +3 -3
- package/dist/src/platform/credential-store.mjs +19 -19
- package/dist/src/platform/desktop-host.mjs +3 -3
- package/dist/src/platform/desktop-permission-mode.mjs +17 -17
- package/dist/src/platform/desktop-project-registration.mjs +3 -3
- package/dist/src/platform/file-opener.mjs +9 -9
- package/dist/src/platform/linux/README.md +3 -3
- package/dist/src/platform/linux/codex-runtime.mjs +56 -56
- package/dist/src/platform/linux/desktop-installation.mjs +36 -36
- package/dist/src/platform/linux/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/linux/network-info.mjs +11 -11
- package/dist/src/platform/linux/project-locations.mjs +17 -17
- package/dist/src/platform/linux/resident-startup.mjs +52 -52
- package/dist/src/platform/linux/retained-runtime.mjs +76 -76
- package/dist/src/platform/linux/service-host.mjs +18 -18
- package/dist/src/platform/macos/README.md +3 -3
- package/dist/src/platform/macos/desktop-project-registration.mjs +11 -11
- package/dist/src/platform/macos/network-info.mjs +8 -8
- package/dist/src/platform/macos/project-locations.mjs +8 -8
- package/dist/src/platform/macos/resident-startup.mjs +84 -84
- package/dist/src/platform/macos/retained-runtime.mjs +127 -127
- package/dist/src/platform/macos/service-host.mjs +18 -18
- package/dist/src/platform/macos/system.mjs +29 -29
- package/dist/src/platform/network-info.mjs +3 -3
- package/dist/src/platform/path-identity.mjs +5 -5
- package/dist/src/platform/posix/network-candidates.mjs +23 -23
- package/dist/src/platform/posix/runtime-paths.mjs +25 -25
- package/dist/src/platform/power-inhibitor.mjs +22 -22
- package/dist/src/platform/project-locations.mjs +3 -3
- package/dist/src/platform/runtime-paths.mjs +8 -8
- package/dist/src/platform/select.mjs +5 -5
- package/dist/src/platform/service-host.mjs +3 -3
- package/dist/src/platform/user-environment.mjs +3 -3
- package/dist/src/platform/windows/IsolatedProcess.cs +79 -79
- package/dist/src/platform/windows/background-process.mjs +30 -30
- package/dist/src/platform/windows/desktop-helper.mjs +179 -179
- package/dist/src/platform/windows/desktop-runtime.ps1 +10 -10
- package/dist/src/platform/windows/desktop-shortcuts.ps1 +7 -7
- package/dist/src/platform/windows/keep-awake.ps1 +20 -20
- package/dist/src/platform/windows/network-info.mjs +34 -34
- package/dist/src/platform/windows/resident-start.js +20 -20
- package/dist/src/platform/windows/resident-start.ps1 +7 -7
- package/dist/src/platform/windows/resident-startup.ps1 +83 -83
- package/dist/src/platform/windows/resolve-state-root.ps1 +14 -14
- package/dist/src/platform/windows/runtime-paths.mjs +20 -20
- package/dist/src/platform/windows/rustdesk-video-probe.mjs +167 -167
- package/dist/src/platform/windows/start-hidden-console.ps1 +58 -58
- package/dist/src/platform/windows/stop-desktop.ps1 +22 -22
- package/dist/src/platform/windows/user-environment.ps1 +41 -41
- package/dist/src/project-create.mjs +121 -121
- package/dist/src/project-management.mjs +1 -1
- package/dist/src/project-remove.mjs +55 -55
- package/dist/src/project-update.mjs +59 -59
- package/dist/src/relay-config-import.mjs +45 -45
- package/dist/src/self-relay/account-connections.mjs +52 -52
- package/dist/src/self-relay/admin-auth.mjs +59 -59
- package/dist/src/self-relay/admin-cloud.mjs +101 -101
- package/dist/src/self-relay/admin-login-page.mjs +7 -7
- package/dist/src/self-relay/admin-server.mjs +105 -105
- package/dist/src/self-relay/admin-state.mjs +127 -127
- package/dist/src/self-relay/admin-workstations-page.mjs +95 -95
- package/dist/src/self-relay/admin-workstations.mjs +143 -143
- package/dist/src/self-relay/android-releases.mjs +61 -61
- package/dist/src/self-relay/client.mjs +61 -61
- package/dist/src/self-relay/connection-reporting.mjs +38 -38
- package/dist/src/self-relay/connector.mjs +13 -2
- package/dist/src/self-relay/debug-allowlist.mjs +33 -33
- package/dist/src/self-relay/demo.mjs +6 -6
- package/dist/src/self-relay/device-info.mjs +27 -27
- package/dist/src/self-relay/device-store.mjs +92 -92
- package/dist/src/self-relay/lan-authorizations.mjs +71 -71
- package/dist/src/self-relay/lifecycle.mjs +278 -278
- package/dist/src/self-relay/long-pairing.mjs +40 -40
- package/dist/src/self-relay/mysql-state.mjs +118 -118
- package/dist/src/self-relay/phone-connections.mjs +55 -55
- package/dist/src/self-relay/server.mjs +14 -3
- package/dist/src/self-relay/usage-daily.mjs +55 -55
- package/dist/src/self-relay/windows-route-helper.mjs +75 -75
- package/dist/src/self-relay/wire.mjs +36 -36
- package/dist/src/server.mjs +40 -18
- package/dist/src/service-control.mjs +82 -82
- package/dist/src/service-health.mjs +33 -33
- package/dist/src/service-power.mjs +58 -58
- package/dist/src/service-restart.mjs +81 -81
- package/dist/src/service-update-worker.mjs +42 -42
- package/dist/src/service-update.mjs +80 -80
- package/dist/src/shared-recovery.mjs +152 -152
- package/dist/src/skill-install.mjs +51 -51
- package/dist/src/thread-title-generation.mjs +150 -150
- package/dist/src/turn-model-history.mjs +47 -47
- package/dist/tools/rtc-route-helper.ps1 +99 -99
- package/dist/tools/start-rtc-route-helper.ps1 +15 -15
- package/dist/web/vendor/markdown-it.LICENSE.txt +22 -22
- package/dist/web/vendor/markdown-it.umd.min.js +13 -13
- package/package.json +4 -4
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
using System;
|
|
2
|
-
using System.ComponentModel;
|
|
3
|
-
using System.Runtime.InteropServices;
|
|
4
|
-
using System.Text;
|
|
5
|
-
|
|
6
|
-
// Only the three explicitly opened standard handles cross the process boundary.
|
|
7
|
-
// In particular, inherited app-server sockets must not reach background workers.
|
|
8
|
-
public static class IsolatedProcess {
|
|
9
|
-
[StructLayout(LayoutKind.Sequential)] struct Security { public int length; public IntPtr descriptor; public int inherit; }
|
|
10
|
-
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct Startup {
|
|
11
|
-
public int cb; public string reserved, desktop, title;
|
|
12
|
-
public int x,y,xSize,ySize,xCount,yCount,fill,flags; public short show,reservedSize;
|
|
13
|
-
public IntPtr reservedBytes,input,output,error;
|
|
14
|
-
}
|
|
15
|
-
[StructLayout(LayoutKind.Sequential)] struct StartupEx { public Startup startup; public IntPtr attributes; }
|
|
16
|
-
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo { public IntPtr process,thread; public int pid,tid; }
|
|
17
|
-
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateFileW(string name,uint access,uint share,ref Security security,uint disposition,uint flags,IntPtr template);
|
|
18
|
-
[DllImport("kernel32.dll", SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr list,int count,int flags,ref IntPtr size);
|
|
19
|
-
[DllImport("kernel32.dll", SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr list,uint flags,IntPtr attribute,IntPtr value,IntPtr size,IntPtr previous,IntPtr returned);
|
|
20
|
-
[DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
|
|
21
|
-
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool CreateProcessW(string application,StringBuilder command,IntPtr processSecurity,IntPtr threadSecurity,bool inherit,uint flags,IntPtr environment,string cwd,ref StartupEx startup,out ProcessInfo info);
|
|
22
|
-
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
23
|
-
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern uint GetFinalPathNameByHandleW(IntPtr handle,StringBuilder path,uint length,uint flags);
|
|
24
|
-
public static string ResolvePath(string path) {
|
|
25
|
-
var security=new Security {length=Marshal.SizeOf(typeof(Security))};
|
|
26
|
-
var handle=CreateFileW(path,0,7,ref security,3,0x02000000,IntPtr.Zero);
|
|
27
|
-
if(handle==new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
28
|
-
try {
|
|
29
|
-
var result=new StringBuilder(32768);
|
|
30
|
-
uint length=GetFinalPathNameByHandleW(handle,result,(uint)result.Capacity,0);
|
|
31
|
-
if(length==0 || length>=result.Capacity) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
32
|
-
var value=result.ToString();
|
|
33
|
-
return value.StartsWith(@"\\?\UNC\") ? @"\\"+value.Substring(8) : value.StartsWith(@"\\?\") ? value.Substring(4) : value;
|
|
34
|
-
} finally { CloseHandle(handle); }
|
|
35
|
-
}
|
|
36
|
-
static string Quote(string value) {
|
|
37
|
-
var result=new StringBuilder("\""); int slashes=0;
|
|
38
|
-
foreach(char c in value) {
|
|
39
|
-
if(c=='\\') { slashes++; continue; }
|
|
40
|
-
result.Append('\\', c=='"' ? slashes*2+1 : slashes); slashes=0; result.Append(c);
|
|
41
|
-
}
|
|
42
|
-
result.Append('\\',slashes*2); return result.Append('"').ToString();
|
|
43
|
-
}
|
|
44
|
-
static IntPtr File(string file,bool input) {
|
|
45
|
-
var sa=new Security { length=Marshal.SizeOf(typeof(Security)), inherit=1 };
|
|
46
|
-
// FILE_APPEND_DATA avoids truncating diagnostic history on recovery.
|
|
47
|
-
var handle=CreateFileW(file,input?0x80000000u:4u,7,ref sa,input?3u:4u,0x80,IntPtr.Zero);
|
|
48
|
-
if(handle==new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
49
|
-
return handle;
|
|
50
|
-
}
|
|
51
|
-
public static int Start(string executable,string[] arguments,string stdout,string stderr,string[] environment) {
|
|
52
|
-
IntPtr input=IntPtr.Zero,output=IntPtr.Zero,error=IntPtr.Zero,list=IntPtr.Zero,handles=IntPtr.Zero,environmentBlock=IntPtr.Zero;
|
|
53
|
-
bool initialized=false;
|
|
54
|
-
try {
|
|
55
|
-
input=File("NUL",true); output=File(String.IsNullOrEmpty(stdout)?"NUL":stdout,false); error=File(String.IsNullOrEmpty(stderr)?"NUL":stderr,false);
|
|
56
|
-
IntPtr size=IntPtr.Zero; InitializeProcThreadAttributeList(IntPtr.Zero,1,0,ref size);
|
|
57
|
-
list=Marshal.AllocHGlobal(size);
|
|
58
|
-
if(!InitializeProcThreadAttributeList(list,1,0,ref size)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
59
|
-
initialized=true; handles=Marshal.AllocHGlobal(IntPtr.Size*3);
|
|
60
|
-
Marshal.Copy(new IntPtr[]{input,output,error},0,handles,3);
|
|
61
|
-
if(!UpdateProcThreadAttribute(list,0,new IntPtr(0x20002),handles,new IntPtr(IntPtr.Size*3),IntPtr.Zero,IntPtr.Zero)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
62
|
-
var startup=new StartupEx(); startup.startup.cb=Marshal.SizeOf(typeof(StartupEx));
|
|
63
|
-
startup.startup.flags=0x101; startup.startup.show=0;
|
|
64
|
-
startup.startup.input=input; startup.startup.output=output; startup.startup.error=error; startup.attributes=list;
|
|
65
|
-
var command=new StringBuilder(Quote(executable)); foreach(var argument in arguments) command.Append(' ').Append(Quote(argument));
|
|
66
|
-
Array.Sort(environment,StringComparer.OrdinalIgnoreCase);
|
|
67
|
-
var environmentBytes=Encoding.Unicode.GetBytes(String.Join("\0",environment)+"\0\0");
|
|
68
|
-
environmentBlock=Marshal.AllocHGlobal(environmentBytes.Length); Marshal.Copy(environmentBytes,0,environmentBlock,environmentBytes.Length);
|
|
69
|
-
ProcessInfo info;
|
|
70
|
-
if(!CreateProcessW(executable,command,IntPtr.Zero,IntPtr.Zero,true,0x80410,environmentBlock,null,ref startup,out info)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
71
|
-
CloseHandle(info.thread); CloseHandle(info.process); return info.pid;
|
|
72
|
-
} finally {
|
|
73
|
-
if(initialized) DeleteProcThreadAttributeList(list);
|
|
74
|
-
if(list!=IntPtr.Zero) Marshal.FreeHGlobal(list); if(handles!=IntPtr.Zero) Marshal.FreeHGlobal(handles);
|
|
75
|
-
if(environmentBlock!=IntPtr.Zero) Marshal.FreeHGlobal(environmentBlock);
|
|
76
|
-
if(input!=IntPtr.Zero) CloseHandle(input); if(output!=IntPtr.Zero) CloseHandle(output); if(error!=IntPtr.Zero) CloseHandle(error);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
1
|
+
using System;
|
|
2
|
+
using System.ComponentModel;
|
|
3
|
+
using System.Runtime.InteropServices;
|
|
4
|
+
using System.Text;
|
|
5
|
+
|
|
6
|
+
// Only the three explicitly opened standard handles cross the process boundary.
|
|
7
|
+
// In particular, inherited app-server sockets must not reach background workers.
|
|
8
|
+
public static class IsolatedProcess {
|
|
9
|
+
[StructLayout(LayoutKind.Sequential)] struct Security { public int length; public IntPtr descriptor; public int inherit; }
|
|
10
|
+
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] struct Startup {
|
|
11
|
+
public int cb; public string reserved, desktop, title;
|
|
12
|
+
public int x,y,xSize,ySize,xCount,yCount,fill,flags; public short show,reservedSize;
|
|
13
|
+
public IntPtr reservedBytes,input,output,error;
|
|
14
|
+
}
|
|
15
|
+
[StructLayout(LayoutKind.Sequential)] struct StartupEx { public Startup startup; public IntPtr attributes; }
|
|
16
|
+
[StructLayout(LayoutKind.Sequential)] struct ProcessInfo { public IntPtr process,thread; public int pid,tid; }
|
|
17
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern IntPtr CreateFileW(string name,uint access,uint share,ref Security security,uint disposition,uint flags,IntPtr template);
|
|
18
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool InitializeProcThreadAttributeList(IntPtr list,int count,int flags,ref IntPtr size);
|
|
19
|
+
[DllImport("kernel32.dll", SetLastError=true)] static extern bool UpdateProcThreadAttribute(IntPtr list,uint flags,IntPtr attribute,IntPtr value,IntPtr size,IntPtr previous,IntPtr returned);
|
|
20
|
+
[DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list);
|
|
21
|
+
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] static extern bool CreateProcessW(string application,StringBuilder command,IntPtr processSecurity,IntPtr threadSecurity,bool inherit,uint flags,IntPtr environment,string cwd,ref StartupEx startup,out ProcessInfo info);
|
|
22
|
+
[DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr handle);
|
|
23
|
+
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] static extern uint GetFinalPathNameByHandleW(IntPtr handle,StringBuilder path,uint length,uint flags);
|
|
24
|
+
public static string ResolvePath(string path) {
|
|
25
|
+
var security=new Security {length=Marshal.SizeOf(typeof(Security))};
|
|
26
|
+
var handle=CreateFileW(path,0,7,ref security,3,0x02000000,IntPtr.Zero);
|
|
27
|
+
if(handle==new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
28
|
+
try {
|
|
29
|
+
var result=new StringBuilder(32768);
|
|
30
|
+
uint length=GetFinalPathNameByHandleW(handle,result,(uint)result.Capacity,0);
|
|
31
|
+
if(length==0 || length>=result.Capacity) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
32
|
+
var value=result.ToString();
|
|
33
|
+
return value.StartsWith(@"\\?\UNC\") ? @"\\"+value.Substring(8) : value.StartsWith(@"\\?\") ? value.Substring(4) : value;
|
|
34
|
+
} finally { CloseHandle(handle); }
|
|
35
|
+
}
|
|
36
|
+
static string Quote(string value) {
|
|
37
|
+
var result=new StringBuilder("\""); int slashes=0;
|
|
38
|
+
foreach(char c in value) {
|
|
39
|
+
if(c=='\\') { slashes++; continue; }
|
|
40
|
+
result.Append('\\', c=='"' ? slashes*2+1 : slashes); slashes=0; result.Append(c);
|
|
41
|
+
}
|
|
42
|
+
result.Append('\\',slashes*2); return result.Append('"').ToString();
|
|
43
|
+
}
|
|
44
|
+
static IntPtr File(string file,bool input) {
|
|
45
|
+
var sa=new Security { length=Marshal.SizeOf(typeof(Security)), inherit=1 };
|
|
46
|
+
// FILE_APPEND_DATA avoids truncating diagnostic history on recovery.
|
|
47
|
+
var handle=CreateFileW(file,input?0x80000000u:4u,7,ref sa,input?3u:4u,0x80,IntPtr.Zero);
|
|
48
|
+
if(handle==new IntPtr(-1)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
49
|
+
return handle;
|
|
50
|
+
}
|
|
51
|
+
public static int Start(string executable,string[] arguments,string stdout,string stderr,string[] environment) {
|
|
52
|
+
IntPtr input=IntPtr.Zero,output=IntPtr.Zero,error=IntPtr.Zero,list=IntPtr.Zero,handles=IntPtr.Zero,environmentBlock=IntPtr.Zero;
|
|
53
|
+
bool initialized=false;
|
|
54
|
+
try {
|
|
55
|
+
input=File("NUL",true); output=File(String.IsNullOrEmpty(stdout)?"NUL":stdout,false); error=File(String.IsNullOrEmpty(stderr)?"NUL":stderr,false);
|
|
56
|
+
IntPtr size=IntPtr.Zero; InitializeProcThreadAttributeList(IntPtr.Zero,1,0,ref size);
|
|
57
|
+
list=Marshal.AllocHGlobal(size);
|
|
58
|
+
if(!InitializeProcThreadAttributeList(list,1,0,ref size)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
59
|
+
initialized=true; handles=Marshal.AllocHGlobal(IntPtr.Size*3);
|
|
60
|
+
Marshal.Copy(new IntPtr[]{input,output,error},0,handles,3);
|
|
61
|
+
if(!UpdateProcThreadAttribute(list,0,new IntPtr(0x20002),handles,new IntPtr(IntPtr.Size*3),IntPtr.Zero,IntPtr.Zero)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
62
|
+
var startup=new StartupEx(); startup.startup.cb=Marshal.SizeOf(typeof(StartupEx));
|
|
63
|
+
startup.startup.flags=0x101; startup.startup.show=0;
|
|
64
|
+
startup.startup.input=input; startup.startup.output=output; startup.startup.error=error; startup.attributes=list;
|
|
65
|
+
var command=new StringBuilder(Quote(executable)); foreach(var argument in arguments) command.Append(' ').Append(Quote(argument));
|
|
66
|
+
Array.Sort(environment,StringComparer.OrdinalIgnoreCase);
|
|
67
|
+
var environmentBytes=Encoding.Unicode.GetBytes(String.Join("\0",environment)+"\0\0");
|
|
68
|
+
environmentBlock=Marshal.AllocHGlobal(environmentBytes.Length); Marshal.Copy(environmentBytes,0,environmentBlock,environmentBytes.Length);
|
|
69
|
+
ProcessInfo info;
|
|
70
|
+
if(!CreateProcessW(executable,command,IntPtr.Zero,IntPtr.Zero,true,0x80410,environmentBlock,null,ref startup,out info)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
71
|
+
CloseHandle(info.thread); CloseHandle(info.process); return info.pid;
|
|
72
|
+
} finally {
|
|
73
|
+
if(initialized) DeleteProcThreadAttributeList(list);
|
|
74
|
+
if(list!=IntPtr.Zero) Marshal.FreeHGlobal(list); if(handles!=IntPtr.Zero) Marshal.FreeHGlobal(handles);
|
|
75
|
+
if(environmentBlock!=IntPtr.Zero) Marshal.FreeHGlobal(environmentBlock);
|
|
76
|
+
if(input!=IntPtr.Zero) CloseHandle(input); if(output!=IntPtr.Zero) CloseHandle(output); if(error!=IntPtr.Zero) CloseHandle(error);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
export function startWindowsProcess(executable, args, environment, logs = []) {
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
8
|
-
const helperEnvironment = {};
|
|
9
|
-
for (const name of ['SystemRoot', 'windir', 'LOCALAPPDATA', 'USERPROFILE', 'TEMP', 'TMP', 'PSModulePath']) {
|
|
10
|
-
const key = Object.keys(process.env).find(item => item.toLowerCase() === name.toLowerCase());
|
|
11
|
-
if (key) helperEnvironment[key] = process.env[key];
|
|
12
|
-
}
|
|
13
|
-
const helper = spawn(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
14
|
-
fileURLToPath(new URL('./start-hidden-console.ps1', import.meta.url))], {
|
|
15
|
-
windowsHide: true, env: helperEnvironment, stdio: ['pipe', 'pipe', 'pipe'],
|
|
16
|
-
});
|
|
17
|
-
let output = '', error = '';
|
|
18
|
-
helper.stdout.setEncoding('utf8').on('data', chunk => { output += chunk; });
|
|
19
|
-
helper.stderr.setEncoding('utf8').on('data', chunk => { error += chunk; });
|
|
20
|
-
helper.once('error', reject); helper.stdin.on('error', reject);
|
|
21
|
-
helper.once('exit', code => {
|
|
22
|
-
helper.stdout.destroy(); helper.stderr.destroy(); helper.stdin.destroy();
|
|
23
|
-
if (code !== 0) return reject(new Error(error.trim() || `后台启动失败 (${code})`));
|
|
24
|
-
const pid = Number(output.trim());
|
|
25
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) return reject(new Error('后台启动未返回有效 PID'));
|
|
26
|
-
resolve(pid);
|
|
27
|
-
});
|
|
28
|
-
helper.stdin.end(JSON.stringify({ brokerExecutable: process.execPath, executable, args, logs, environment }));
|
|
29
|
-
});
|
|
30
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function startWindowsProcess(executable, args, environment, logs = []) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
8
|
+
const helperEnvironment = {};
|
|
9
|
+
for (const name of ['SystemRoot', 'windir', 'LOCALAPPDATA', 'USERPROFILE', 'TEMP', 'TMP', 'PSModulePath']) {
|
|
10
|
+
const key = Object.keys(process.env).find(item => item.toLowerCase() === name.toLowerCase());
|
|
11
|
+
if (key) helperEnvironment[key] = process.env[key];
|
|
12
|
+
}
|
|
13
|
+
const helper = spawn(powershell, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File',
|
|
14
|
+
fileURLToPath(new URL('./start-hidden-console.ps1', import.meta.url))], {
|
|
15
|
+
windowsHide: true, env: helperEnvironment, stdio: ['pipe', 'pipe', 'pipe'],
|
|
16
|
+
});
|
|
17
|
+
let output = '', error = '';
|
|
18
|
+
helper.stdout.setEncoding('utf8').on('data', chunk => { output += chunk; });
|
|
19
|
+
helper.stderr.setEncoding('utf8').on('data', chunk => { error += chunk; });
|
|
20
|
+
helper.once('error', reject); helper.stdin.on('error', reject);
|
|
21
|
+
helper.once('exit', code => {
|
|
22
|
+
helper.stdout.destroy(); helper.stderr.destroy(); helper.stdin.destroy();
|
|
23
|
+
if (code !== 0) return reject(new Error(error.trim() || `后台启动失败 (${code})`));
|
|
24
|
+
const pid = Number(output.trim());
|
|
25
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return reject(new Error('后台启动未返回有效 PID'));
|
|
26
|
+
resolve(pid);
|
|
27
|
+
});
|
|
28
|
+
helper.stdin.end(JSON.stringify({ brokerExecutable: process.execPath, executable, args, logs, environment }));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
import net from 'node:net';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { execFile } from 'node:child_process';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const exec = promisify(execFile);
|
|
10
|
-
const binary = fileURLToPath(new URL('../../../native/desktop-helper/CodeRelaxDesktop.exe', import.meta.url));
|
|
11
|
-
const powershell = path.join(process.env.SystemRoot || 'C:/Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
12
|
-
const quote = value => `'${String(value).replaceAll("'", "''")}'`;
|
|
13
|
-
const encoded = script => Buffer.from(script, 'utf16le').toString('base64');
|
|
14
|
-
const maxFrameBytes = 6 * 1024 * 1024;
|
|
15
|
-
|
|
16
|
-
export async function desktopHelperStatus() {
|
|
17
|
-
if (process.platform !== 'win32' || !fs.existsSync(binary)) return { installed: false, running: false, state: 'unavailable' };
|
|
18
|
-
const { stdout } = await exec(binary, ['--status'], { windowsHide: true, encoding: 'utf8', timeout: 5000 });
|
|
19
|
-
const result = JSON.parse(stdout.trim());
|
|
20
|
-
if (result.error) throw new Error(result.error);
|
|
21
|
-
return result;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function elevateHelper(argument) {
|
|
25
|
-
const script = `$ErrorActionPreference = 'Stop'; try { $child = Start-Process -FilePath ${quote(binary)} -ArgumentList ${quote(argument)} -Verb RunAs -PassThru -Wait; exit $child.ExitCode } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }`;
|
|
26
|
-
try { await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded(script)], { windowsHide: true, timeout: 180000 }); }
|
|
27
|
-
catch (error) { throw new Error(`桌面操作辅助服务配置未完成,需要在工作站确认 UAC:${error.stderr?.trim() || error.message}`); }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function removeDesktopHelper() {
|
|
31
|
-
const current = await desktopHelperStatus();
|
|
32
|
-
if (!current.installed) return { installed: false, removed: false };
|
|
33
|
-
await elevateHelper('--remove');
|
|
34
|
-
const result = await desktopHelperStatus();
|
|
35
|
-
if (result.installed) throw new Error('桌面操作辅助服务尚未移除。');
|
|
36
|
-
return { ...result, removed: true };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function installDesktopHelper() {
|
|
40
|
-
if (!fs.existsSync(binary)) throw new Error('缺少原生 桌面操作辅助组件,请先构建或安装完整 npm 包。');
|
|
41
|
-
const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded('[Console]::OutputEncoding = New-Object Text.UTF8Encoding; [Security.Principal.WindowsIdentity]::GetCurrent().User.Value')], { windowsHide: true, encoding: 'utf8' });
|
|
42
|
-
const sid = stdout.trim();
|
|
43
|
-
if (!/^S-1-5-21-(?:\d+-){3}\d+$/.test(sid)) throw new Error('无法获取当前 Windows 用户 SID。');
|
|
44
|
-
const current = await desktopHelperStatus();
|
|
45
|
-
const hash = target => createHash('sha256').update(fs.readFileSync(target)).digest('hex');
|
|
46
|
-
if (current.running && current.userSid === sid && hash(binary) === hash(current.path)) return { ...current, changed: false };
|
|
47
|
-
// Only this installer is elevated. Node and the workstation remain user-level.
|
|
48
|
-
await elevateHelper(`--install ${sid}`);
|
|
49
|
-
const installed = await desktopHelperStatus();
|
|
50
|
-
if (!installed.running || installed.userSid !== sid) throw new Error('桌面操作辅助服务尚未就绪。');
|
|
51
|
-
return { ...installed, changed: true };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
class PipeSession {
|
|
55
|
-
constructor() {
|
|
56
|
-
this.socket = net.createConnection('\\\\.\\pipe\\CodeRelax.Desktop.v1');
|
|
57
|
-
this.socket.setEncoding('utf8');
|
|
58
|
-
this.buffer = '';
|
|
59
|
-
this.waiter = null;
|
|
60
|
-
this.closed = false;
|
|
61
|
-
this.socket.on('data', data => {
|
|
62
|
-
this.buffer += data;
|
|
63
|
-
if (Buffer.byteLength(this.buffer) > maxFrameBytes) { this.fail(new Error('桌面画面超过大小限制')); return; }
|
|
64
|
-
const end = this.buffer.indexOf('\n');
|
|
65
|
-
if (end < 0) return;
|
|
66
|
-
try {
|
|
67
|
-
const result = JSON.parse(this.buffer.slice(0, end));
|
|
68
|
-
this.buffer = this.buffer.slice(end + 1);
|
|
69
|
-
if (!this.waiter) throw new Error('收到未请求的 桌面数据');
|
|
70
|
-
const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer);
|
|
71
|
-
const complete = () => result.error
|
|
72
|
-
? waiter.reject(Object.assign(new Error(result.error), { code: 'desktop_native_error' })) : waiter.resolve(result);
|
|
73
|
-
if (result.state === 'pending' || result.continuing === true) complete();
|
|
74
|
-
else this.socket.write('ACK\n', error => {
|
|
75
|
-
if (error) console.warn(JSON.stringify({ event: 'desktop-response-ack', code: error.code || 'write_failed' }));
|
|
76
|
-
// The parsed result remains authoritative even if cleanup disconnects.
|
|
77
|
-
complete();
|
|
78
|
-
});
|
|
79
|
-
} catch (error) { this.fail(error); }
|
|
80
|
-
});
|
|
81
|
-
this.socket.on('error', error => this.fail(error));
|
|
82
|
-
this.socket.on('close', () => { this.closed = true; this.fail(new Error('桌面操作辅助连接已关闭,请重新获取画面')); });
|
|
83
|
-
}
|
|
84
|
-
fail(error) {
|
|
85
|
-
if (this.waiter) { const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer); waiter.reject(error); }
|
|
86
|
-
this.socket.destroy();
|
|
87
|
-
}
|
|
88
|
-
request(command) {
|
|
89
|
-
if (this.closed || this.socket.destroyed || this.waiter) return Promise.reject(new Error('桌面操作辅助连接不可用'));
|
|
90
|
-
return new Promise((resolve, reject) => {
|
|
91
|
-
this.waiter = { resolve, reject, timer: setTimeout(() => this.fail(new Error('桌面操作辅助请求超时;点击不会自动重发,请刷新画面核对')), 10000) };
|
|
92
|
-
this.socket.write(`${command}\n`);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
close() { this.fail(new Error('桌面画面已释放')); }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Frame and input share one worker pipe. Serialize them without replaying input.
|
|
99
|
-
export class DesktopHelper {
|
|
100
|
-
constructor() { this.active = null; this.reading = false; this.queue = Promise.resolve(); this.version = 0; }
|
|
101
|
-
serial(operation) {
|
|
102
|
-
const version = this.version;
|
|
103
|
-
const result = this.queue.then(() => {
|
|
104
|
-
if (version !== this.version) throw Object.assign(new Error('桌面操作已取消,请刷新'), { code: 'desktop_session_invalid' });
|
|
105
|
-
return operation();
|
|
106
|
-
});
|
|
107
|
-
this.queue = result.catch(() => {});
|
|
108
|
-
return result;
|
|
109
|
-
}
|
|
110
|
-
clear(current = this.active) {
|
|
111
|
-
if (!current) return;
|
|
112
|
-
clearTimeout(current.timer); current.session.close();
|
|
113
|
-
if (this.active === current) this.active = null;
|
|
114
|
-
}
|
|
115
|
-
async frame({ preview = false } = {}) {
|
|
116
|
-
if (this.reading) throw new Error('正在获取桌面画面,请稍候');
|
|
117
|
-
this.reading = true;
|
|
118
|
-
try { return await this.serial(async () => {
|
|
119
|
-
if (!this.active || this.active.preview !== preview) {
|
|
120
|
-
this.clear();
|
|
121
|
-
const version = this.version;
|
|
122
|
-
const status = await desktopHelperStatus();
|
|
123
|
-
if (version !== this.version) throw new Error('桌面操作已取消');
|
|
124
|
-
if (!status.running) return { state: 'unavailable', message: '工作站桌面服务尚未就绪' };
|
|
125
|
-
this.active = { session: new PipeSession(), preview };
|
|
126
|
-
}
|
|
127
|
-
const current = this.active;
|
|
128
|
-
try {
|
|
129
|
-
clearTimeout(current.timer);
|
|
130
|
-
const frame = await current.session.request(current.frame ? 'FRAME' : preview ? 'PREVIEW' : 'FRAME');
|
|
131
|
-
if (this.active !== current) throw new Error('桌面画面已释放');
|
|
132
|
-
if (frame.state !== 'pending') { this.clear(current); return frame; }
|
|
133
|
-
if (!/^[a-f0-9]{32}$/.test(frame.frameId) || !/^[a-f0-9]{32}$/.test(frame.sessionId) || !Number.isSafeInteger(frame.width) || !Number.isSafeInteger(frame.height) ||
|
|
134
|
-
!['image/png', 'image/jpeg'].includes(frame.mimeType) || !frame.image?.startsWith('data:' + frame.mimeType + ';base64,') ||
|
|
135
|
-
frame.width < 1 || frame.height < 1 || frame.preview !== preview) throw new Error('桌面画面格式无效');
|
|
136
|
-
const image = Buffer.from(frame.image.slice(('data:' + frame.mimeType + ';base64,').length), 'base64');
|
|
137
|
-
delete frame.image;
|
|
138
|
-
current.sessionId = frame.sessionId;
|
|
139
|
-
frame.imagePath = '/api/desktop/frames/' + frame.frameId;
|
|
140
|
-
frame.size = image.length;
|
|
141
|
-
current.frame = frame; current.image = image; current.expires = Date.now() + 30000;
|
|
142
|
-
current.timer = setTimeout(() => this.release(undefined, current.sessionId), 30000);
|
|
143
|
-
return frame;
|
|
144
|
-
} catch (error) { this.clear(current); throw error; }
|
|
145
|
-
}); } finally { this.reading = false; }
|
|
146
|
-
}
|
|
147
|
-
input(sessionId, command) {
|
|
148
|
-
return this.serial(async () => {
|
|
149
|
-
const current = this.active;
|
|
150
|
-
if (!current?.frame || sessionId !== current.sessionId || Date.now() >= current.expires)
|
|
151
|
-
throw Object.assign(new Error('请刷新桌面后操作'), { code: 'desktop_session_invalid' });
|
|
152
|
-
try {
|
|
153
|
-
const result = await current.session.request(command);
|
|
154
|
-
if (result.continuing !== true) this.clear(current);
|
|
155
|
-
return result;
|
|
156
|
-
} catch (error) { this.clear(current); throw error; }
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
move({ sessionId, dx, dy }) {
|
|
160
|
-
if (!Number.isSafeInteger(dx) || !Number.isSafeInteger(dy) || Math.abs(dx) > 512 || Math.abs(dy) > 512 || (!dx && !dy))
|
|
161
|
-
throw new Error('无效的相对鼠标位移');
|
|
162
|
-
return this.input(sessionId, 'MOVE ' + sessionId + ' ' + dx + ' ' + dy);
|
|
163
|
-
}
|
|
164
|
-
click({ sessionId, button }) {
|
|
165
|
-
if (!['left', 'right'].includes(button)) throw new Error('无效的鼠标按键');
|
|
166
|
-
return this.input(sessionId, 'BUTTON ' + sessionId + ' ' + button);
|
|
167
|
-
}
|
|
168
|
-
image(frameId) {
|
|
169
|
-
const current = this.active;
|
|
170
|
-
if (!current?.frame || current.frame.frameId !== frameId || Date.now() >= current.expires)
|
|
171
|
-
throw new Error('桌面画面已释放或过期,请重新获取');
|
|
172
|
-
return current.image;
|
|
173
|
-
}
|
|
174
|
-
release(frameId, sessionId) {
|
|
175
|
-
if (frameId && this.active?.frame?.frameId !== frameId) return;
|
|
176
|
-
if (sessionId && this.active?.sessionId !== sessionId) return;
|
|
177
|
-
this.version++; this.clear();
|
|
178
|
-
}
|
|
179
|
-
}
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
const binary = fileURLToPath(new URL('../../../native/desktop-helper/CodeRelaxDesktop.exe', import.meta.url));
|
|
11
|
+
const powershell = path.join(process.env.SystemRoot || 'C:/Windows', 'System32/WindowsPowerShell/v1.0/powershell.exe');
|
|
12
|
+
const quote = value => `'${String(value).replaceAll("'", "''")}'`;
|
|
13
|
+
const encoded = script => Buffer.from(script, 'utf16le').toString('base64');
|
|
14
|
+
const maxFrameBytes = 6 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export async function desktopHelperStatus() {
|
|
17
|
+
if (process.platform !== 'win32' || !fs.existsSync(binary)) return { installed: false, running: false, state: 'unavailable' };
|
|
18
|
+
const { stdout } = await exec(binary, ['--status'], { windowsHide: true, encoding: 'utf8', timeout: 5000 });
|
|
19
|
+
const result = JSON.parse(stdout.trim());
|
|
20
|
+
if (result.error) throw new Error(result.error);
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function elevateHelper(argument) {
|
|
25
|
+
const script = `$ErrorActionPreference = 'Stop'; try { $child = Start-Process -FilePath ${quote(binary)} -ArgumentList ${quote(argument)} -Verb RunAs -PassThru -Wait; exit $child.ExitCode } catch { [Console]::Error.WriteLine($_.Exception.Message); exit 1 }`;
|
|
26
|
+
try { await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded(script)], { windowsHide: true, timeout: 180000 }); }
|
|
27
|
+
catch (error) { throw new Error(`桌面操作辅助服务配置未完成,需要在工作站确认 UAC:${error.stderr?.trim() || error.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function removeDesktopHelper() {
|
|
31
|
+
const current = await desktopHelperStatus();
|
|
32
|
+
if (!current.installed) return { installed: false, removed: false };
|
|
33
|
+
await elevateHelper('--remove');
|
|
34
|
+
const result = await desktopHelperStatus();
|
|
35
|
+
if (result.installed) throw new Error('桌面操作辅助服务尚未移除。');
|
|
36
|
+
return { ...result, removed: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function installDesktopHelper() {
|
|
40
|
+
if (!fs.existsSync(binary)) throw new Error('缺少原生 桌面操作辅助组件,请先构建或安装完整 npm 包。');
|
|
41
|
+
const { stdout } = await exec(powershell, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded('[Console]::OutputEncoding = New-Object Text.UTF8Encoding; [Security.Principal.WindowsIdentity]::GetCurrent().User.Value')], { windowsHide: true, encoding: 'utf8' });
|
|
42
|
+
const sid = stdout.trim();
|
|
43
|
+
if (!/^S-1-5-21-(?:\d+-){3}\d+$/.test(sid)) throw new Error('无法获取当前 Windows 用户 SID。');
|
|
44
|
+
const current = await desktopHelperStatus();
|
|
45
|
+
const hash = target => createHash('sha256').update(fs.readFileSync(target)).digest('hex');
|
|
46
|
+
if (current.running && current.userSid === sid && hash(binary) === hash(current.path)) return { ...current, changed: false };
|
|
47
|
+
// Only this installer is elevated. Node and the workstation remain user-level.
|
|
48
|
+
await elevateHelper(`--install ${sid}`);
|
|
49
|
+
const installed = await desktopHelperStatus();
|
|
50
|
+
if (!installed.running || installed.userSid !== sid) throw new Error('桌面操作辅助服务尚未就绪。');
|
|
51
|
+
return { ...installed, changed: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class PipeSession {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.socket = net.createConnection('\\\\.\\pipe\\CodeRelax.Desktop.v1');
|
|
57
|
+
this.socket.setEncoding('utf8');
|
|
58
|
+
this.buffer = '';
|
|
59
|
+
this.waiter = null;
|
|
60
|
+
this.closed = false;
|
|
61
|
+
this.socket.on('data', data => {
|
|
62
|
+
this.buffer += data;
|
|
63
|
+
if (Buffer.byteLength(this.buffer) > maxFrameBytes) { this.fail(new Error('桌面画面超过大小限制')); return; }
|
|
64
|
+
const end = this.buffer.indexOf('\n');
|
|
65
|
+
if (end < 0) return;
|
|
66
|
+
try {
|
|
67
|
+
const result = JSON.parse(this.buffer.slice(0, end));
|
|
68
|
+
this.buffer = this.buffer.slice(end + 1);
|
|
69
|
+
if (!this.waiter) throw new Error('收到未请求的 桌面数据');
|
|
70
|
+
const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer);
|
|
71
|
+
const complete = () => result.error
|
|
72
|
+
? waiter.reject(Object.assign(new Error(result.error), { code: 'desktop_native_error' })) : waiter.resolve(result);
|
|
73
|
+
if (result.state === 'pending' || result.continuing === true) complete();
|
|
74
|
+
else this.socket.write('ACK\n', error => {
|
|
75
|
+
if (error) console.warn(JSON.stringify({ event: 'desktop-response-ack', code: error.code || 'write_failed' }));
|
|
76
|
+
// The parsed result remains authoritative even if cleanup disconnects.
|
|
77
|
+
complete();
|
|
78
|
+
});
|
|
79
|
+
} catch (error) { this.fail(error); }
|
|
80
|
+
});
|
|
81
|
+
this.socket.on('error', error => this.fail(error));
|
|
82
|
+
this.socket.on('close', () => { this.closed = true; this.fail(new Error('桌面操作辅助连接已关闭,请重新获取画面')); });
|
|
83
|
+
}
|
|
84
|
+
fail(error) {
|
|
85
|
+
if (this.waiter) { const waiter = this.waiter; this.waiter = null; clearTimeout(waiter.timer); waiter.reject(error); }
|
|
86
|
+
this.socket.destroy();
|
|
87
|
+
}
|
|
88
|
+
request(command) {
|
|
89
|
+
if (this.closed || this.socket.destroyed || this.waiter) return Promise.reject(new Error('桌面操作辅助连接不可用'));
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
this.waiter = { resolve, reject, timer: setTimeout(() => this.fail(new Error('桌面操作辅助请求超时;点击不会自动重发,请刷新画面核对')), 10000) };
|
|
92
|
+
this.socket.write(`${command}\n`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
close() { this.fail(new Error('桌面画面已释放')); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Frame and input share one worker pipe. Serialize them without replaying input.
|
|
99
|
+
export class DesktopHelper {
|
|
100
|
+
constructor() { this.active = null; this.reading = false; this.queue = Promise.resolve(); this.version = 0; }
|
|
101
|
+
serial(operation) {
|
|
102
|
+
const version = this.version;
|
|
103
|
+
const result = this.queue.then(() => {
|
|
104
|
+
if (version !== this.version) throw Object.assign(new Error('桌面操作已取消,请刷新'), { code: 'desktop_session_invalid' });
|
|
105
|
+
return operation();
|
|
106
|
+
});
|
|
107
|
+
this.queue = result.catch(() => {});
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
clear(current = this.active) {
|
|
111
|
+
if (!current) return;
|
|
112
|
+
clearTimeout(current.timer); current.session.close();
|
|
113
|
+
if (this.active === current) this.active = null;
|
|
114
|
+
}
|
|
115
|
+
async frame({ preview = false } = {}) {
|
|
116
|
+
if (this.reading) throw new Error('正在获取桌面画面,请稍候');
|
|
117
|
+
this.reading = true;
|
|
118
|
+
try { return await this.serial(async () => {
|
|
119
|
+
if (!this.active || this.active.preview !== preview) {
|
|
120
|
+
this.clear();
|
|
121
|
+
const version = this.version;
|
|
122
|
+
const status = await desktopHelperStatus();
|
|
123
|
+
if (version !== this.version) throw new Error('桌面操作已取消');
|
|
124
|
+
if (!status.running) return { state: 'unavailable', message: '工作站桌面服务尚未就绪' };
|
|
125
|
+
this.active = { session: new PipeSession(), preview };
|
|
126
|
+
}
|
|
127
|
+
const current = this.active;
|
|
128
|
+
try {
|
|
129
|
+
clearTimeout(current.timer);
|
|
130
|
+
const frame = await current.session.request(current.frame ? 'FRAME' : preview ? 'PREVIEW' : 'FRAME');
|
|
131
|
+
if (this.active !== current) throw new Error('桌面画面已释放');
|
|
132
|
+
if (frame.state !== 'pending') { this.clear(current); return frame; }
|
|
133
|
+
if (!/^[a-f0-9]{32}$/.test(frame.frameId) || !/^[a-f0-9]{32}$/.test(frame.sessionId) || !Number.isSafeInteger(frame.width) || !Number.isSafeInteger(frame.height) ||
|
|
134
|
+
!['image/png', 'image/jpeg'].includes(frame.mimeType) || !frame.image?.startsWith('data:' + frame.mimeType + ';base64,') ||
|
|
135
|
+
frame.width < 1 || frame.height < 1 || frame.preview !== preview) throw new Error('桌面画面格式无效');
|
|
136
|
+
const image = Buffer.from(frame.image.slice(('data:' + frame.mimeType + ';base64,').length), 'base64');
|
|
137
|
+
delete frame.image;
|
|
138
|
+
current.sessionId = frame.sessionId;
|
|
139
|
+
frame.imagePath = '/api/desktop/frames/' + frame.frameId;
|
|
140
|
+
frame.size = image.length;
|
|
141
|
+
current.frame = frame; current.image = image; current.expires = Date.now() + 30000;
|
|
142
|
+
current.timer = setTimeout(() => this.release(undefined, current.sessionId), 30000);
|
|
143
|
+
return frame;
|
|
144
|
+
} catch (error) { this.clear(current); throw error; }
|
|
145
|
+
}); } finally { this.reading = false; }
|
|
146
|
+
}
|
|
147
|
+
input(sessionId, command) {
|
|
148
|
+
return this.serial(async () => {
|
|
149
|
+
const current = this.active;
|
|
150
|
+
if (!current?.frame || sessionId !== current.sessionId || Date.now() >= current.expires)
|
|
151
|
+
throw Object.assign(new Error('请刷新桌面后操作'), { code: 'desktop_session_invalid' });
|
|
152
|
+
try {
|
|
153
|
+
const result = await current.session.request(command);
|
|
154
|
+
if (result.continuing !== true) this.clear(current);
|
|
155
|
+
return result;
|
|
156
|
+
} catch (error) { this.clear(current); throw error; }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
move({ sessionId, dx, dy }) {
|
|
160
|
+
if (!Number.isSafeInteger(dx) || !Number.isSafeInteger(dy) || Math.abs(dx) > 512 || Math.abs(dy) > 512 || (!dx && !dy))
|
|
161
|
+
throw new Error('无效的相对鼠标位移');
|
|
162
|
+
return this.input(sessionId, 'MOVE ' + sessionId + ' ' + dx + ' ' + dy);
|
|
163
|
+
}
|
|
164
|
+
click({ sessionId, button }) {
|
|
165
|
+
if (!['left', 'right'].includes(button)) throw new Error('无效的鼠标按键');
|
|
166
|
+
return this.input(sessionId, 'BUTTON ' + sessionId + ' ' + button);
|
|
167
|
+
}
|
|
168
|
+
image(frameId) {
|
|
169
|
+
const current = this.active;
|
|
170
|
+
if (!current?.frame || current.frame.frameId !== frameId || Date.now() >= current.expires)
|
|
171
|
+
throw new Error('桌面画面已释放或过期,请重新获取');
|
|
172
|
+
return current.image;
|
|
173
|
+
}
|
|
174
|
+
release(frameId, sessionId) {
|
|
175
|
+
if (frameId && this.active?.frame?.frameId !== frameId) return;
|
|
176
|
+
if (sessionId && this.active?.sessionId !== sessionId) return;
|
|
177
|
+
this.version++; this.clear();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
$ErrorActionPreference = 'Stop'
|
|
2
|
-
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
3
|
-
$package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
|
|
4
|
-
if (-not $package) { 'null'; exit }
|
|
5
|
-
$resources = Join-Path $package.InstallLocation 'app\resources'
|
|
6
|
-
@{
|
|
7
|
-
package = $package.PackageFullName
|
|
8
|
-
executable = Join-Path $resources 'codex.exe'
|
|
9
|
-
codeModeHost = Join-Path $resources 'codex-code-mode-host.exe'
|
|
10
|
-
} | ConvertTo-Json -Compress
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
|
|
3
|
+
$package = Get-AppxPackage OpenAI.Codex | Select-Object -First 1
|
|
4
|
+
if (-not $package) { 'null'; exit }
|
|
5
|
+
$resources = Join-Path $package.InstallLocation 'app\resources'
|
|
6
|
+
@{
|
|
7
|
+
package = $package.PackageFullName
|
|
8
|
+
executable = Join-Path $resources 'codex.exe'
|
|
9
|
+
codeModeHost = Join-Path $resources 'codex-code-mode-host.exe'
|
|
10
|
+
} | ConvertTo-Json -Compress
|