@yeaft/webchat-agent 1.0.524 → 1.0.526
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/local-runtime/server/index.js +2 -0
- package/local-runtime/server/routes/installer-routes.js +19 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +148 -171
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/installers/install.ps1 +264 -0
- package/local-runtime/web/installers/install.sh +150 -0
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/service/linux.js +24 -11
- package/service/macos.js +23 -19
- package/service/windows.js +8 -7
- package/yeaft/engine.js +5 -0
|
Binary file
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Yeaft</title>
|
|
7
7
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%232c2c2c'/><path d='M16 22V13M16 13L10 8M16 13L22 8' stroke='white' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' fill='none'/><circle cx='10' cy='8' r='2.4' fill='white'/><circle cx='22' cy='8' r='2.4' fill='white'/><circle cx='16' cy='22' r='2.4' fill='white'/></svg>">
|
|
8
8
|
<link rel="stylesheet" href="vendor/katex/katex.min.css?v=b0d91614">
|
|
9
|
-
<link rel="stylesheet" href="style.bundle.css?v=
|
|
9
|
+
<link rel="stylesheet" href="style.bundle.css?v=7048bc52">
|
|
10
10
|
<script src="vendor.bundle.js?v=b4d2c175"></script>
|
|
11
11
|
<script defer src="jszip.min.js"></script>
|
|
12
12
|
<script defer src="docx-preview.min.js"></script>
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="app"></div>
|
|
20
|
-
<script type="module" src="app.bundle.js?v=
|
|
20
|
+
<script type="module" src="app.bundle.js?v=63b1cae8"></script>
|
|
21
21
|
</body>
|
|
22
22
|
</html>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Safe user-level installer for the Yeaft Agent.
|
|
2
|
+
[CmdletBinding()]
|
|
3
|
+
param(
|
|
4
|
+
[Parameter()][string]$Server,
|
|
5
|
+
[Parameter()][string]$Secret,
|
|
6
|
+
[switch]$Help
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
$ErrorActionPreference = 'Stop'
|
|
10
|
+
$NodeChannel = 24
|
|
11
|
+
$Registry = 'https://pkg.yeaft.com/'
|
|
12
|
+
$Package = '@yeaft/webchat-agent@latest'
|
|
13
|
+
$Prefix = $null
|
|
14
|
+
$NpmCli = $null
|
|
15
|
+
$OwnedPrefix = $false
|
|
16
|
+
$ServiceInstallAttempted = $false
|
|
17
|
+
$InstallFailed = $false
|
|
18
|
+
$OriginalPath = $env:PATH
|
|
19
|
+
$OriginalTls = [Net.ServicePointManager]::SecurityProtocol
|
|
20
|
+
$SavedEnvironment = @{}
|
|
21
|
+
foreach ($Key in @('AGENT_SECRET', 'SERVER_URL', 'AGENT_NAME', 'YEAFT_AGENT_INSTANCE', 'WORK_DIR', 'YEAFT_DIR', 'PM2_HOME')) {
|
|
22
|
+
$Item = Get-Item -LiteralPath "Env:$Key" -ErrorAction SilentlyContinue
|
|
23
|
+
$SavedEnvironment[$Key] = @{ Present = $null -ne $Item; Value = if ($Item) { $Item.Value } else { $null } }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function Show-Help {
|
|
27
|
+
@'
|
|
28
|
+
Usage: install.ps1 -Server <ws:// or wss:// URL> -Secret <agent secret>
|
|
29
|
+
|
|
30
|
+
Installs a new, independently named Yeaft Agent in
|
|
31
|
+
$HOME\.yeaft\installations\<hostname>-NNNN. It never elevates privileges,
|
|
32
|
+
changes global PATH or npm configuration, or modifies an existing Agent. Node
|
|
33
|
+
24 is downloaded from nodejs.org only when installed Node/npm cannot satisfy
|
|
34
|
+
Node >=22.5.0.
|
|
35
|
+
'@
|
|
36
|
+
}
|
|
37
|
+
function Stop-Install([string]$Message) { throw $Message }
|
|
38
|
+
function Test-Node([string]$NodePath) {
|
|
39
|
+
# No embedded double quotes: Windows PowerShell 5.1 strips them in native argv.
|
|
40
|
+
& $NodePath --no-warnings -e 'const v=process.versions.node.split(String.fromCharCode(46)).map(Number);if(v[0]<22||(v[0]===22&&v[1]<5))process.exit(1);require(String.fromCharCode(110,111,100,101,58,115,113,108,105,116,101))' *> $null
|
|
41
|
+
return $LASTEXITCODE -eq 0
|
|
42
|
+
}
|
|
43
|
+
function Quote-PowerShellLiteral([string]$Value) { return "'" + $Value.Replace("'", "''") + "'" }
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if ($Help) { Show-Help; return }
|
|
47
|
+
|
|
48
|
+
$ParsedServer = $null
|
|
49
|
+
$ServerValid = -not [string]::IsNullOrWhiteSpace($Server) -and
|
|
50
|
+
-not ($Server -match "[\r\n]") -and
|
|
51
|
+
[Uri]::TryCreate($Server, [UriKind]::Absolute, [ref]$ParsedServer) -and
|
|
52
|
+
($ParsedServer.Scheme -eq 'ws' -or $ParsedServer.Scheme -eq 'wss') -and
|
|
53
|
+
-not [string]::IsNullOrWhiteSpace($ParsedServer.Host) -and
|
|
54
|
+
[string]::IsNullOrEmpty($ParsedServer.UserInfo)
|
|
55
|
+
if (-not $ServerValid) { Stop-Install '-Server must be a valid ws:// or wss:// URL with a host' }
|
|
56
|
+
if ([string]::IsNullOrWhiteSpace($Secret)) { Stop-Install '-Secret must not be empty' }
|
|
57
|
+
if ($Secret -match "[\r\n]") { Stop-Install 'arguments must not contain line breaks' }
|
|
58
|
+
if (-not $env:APPDATA) { Stop-Install 'APPDATA is required to install the Windows user service' }
|
|
59
|
+
$UserHome = if ($env:USERPROFILE) { $env:USERPROFILE } elseif ($env:HOME) { $env:HOME } else { $HOME }
|
|
60
|
+
if (-not $UserHome) { Stop-Install 'HOME is required to allocate the Agent installation' }
|
|
61
|
+
if ($UserHome -match "[\r\n]" -or $env:APPDATA -match "[\r\n]") { Stop-Install 'arguments must not contain line breaks' }
|
|
62
|
+
|
|
63
|
+
$Node = $null
|
|
64
|
+
$Npm = $null
|
|
65
|
+
$DownloadedRuntime = $false
|
|
66
|
+
$NodeCommand = Get-Command node.exe -CommandType Application -ErrorAction SilentlyContinue
|
|
67
|
+
if (-not $NodeCommand) { $NodeCommand = Get-Command node -CommandType Application -ErrorAction SilentlyContinue }
|
|
68
|
+
if ($NodeCommand -and (Test-Node $NodeCommand.Source)) {
|
|
69
|
+
# A PATH npm.cmd may use a different adjacent node.exe (nvm/Volta/shims).
|
|
70
|
+
# Resolve Node's actual executable and require its own npm JS entry point.
|
|
71
|
+
$CandidateNode = (& $NodeCommand.Source --no-warnings -p 'process.execPath' 2>$null | Out-String).Trim()
|
|
72
|
+
if ($LASTEXITCODE -eq 0 -and [IO.Path]::IsPathRooted($CandidateNode)) {
|
|
73
|
+
$NpmCandidate = Join-Path (Split-Path -Parent $CandidateNode) 'node_modules\npm\bin\npm-cli.js'
|
|
74
|
+
if ((Test-Path -LiteralPath $NpmCandidate) -and (Test-Node $CandidateNode)) {
|
|
75
|
+
& $CandidateNode $NpmCandidate --version *> $null
|
|
76
|
+
if ($LASTEXITCODE -eq 0) { $Node = $CandidateNode; $NpmCli = $NpmCandidate }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
$Root = Join-Path $UserHome '.yeaft\installations'
|
|
82
|
+
$InstancesRoot = Join-Path $UserHome '.yeaft\instances'
|
|
83
|
+
$HostPart = ([Environment]::MachineName.ToLowerInvariant() -replace '[^a-z0-9._-]', '-').Trim('-')
|
|
84
|
+
if (-not $HostPart) { $HostPart = 'agent' }
|
|
85
|
+
New-Item -ItemType Directory -Force -Path $Root | Out-Null
|
|
86
|
+
New-Item -ItemType Directory -Force -Path $InstancesRoot | Out-Null
|
|
87
|
+
|
|
88
|
+
$Name = $null
|
|
89
|
+
for ($Attempt = 0; $Attempt -lt 100; $Attempt++) {
|
|
90
|
+
$Digits = '{0:D4}' -f (Get-Random -Minimum 0 -Maximum 10000)
|
|
91
|
+
$Candidate = "$HostPart-$Digits"
|
|
92
|
+
$CandidatePrefix = Join-Path $Root $Candidate
|
|
93
|
+
$CandidateData = Join-Path $InstancesRoot $Candidate
|
|
94
|
+
$CandidateConfig = Join-Path $env:APPDATA "yeaft-agent\instances\$Candidate"
|
|
95
|
+
$CandidateStartup = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\yeaft-agent-$Candidate.bat"
|
|
96
|
+
if ((Test-Path -LiteralPath $CandidatePrefix) -or (Test-Path -LiteralPath $CandidateData) -or
|
|
97
|
+
(Test-Path -LiteralPath $CandidateConfig) -or (Test-Path -LiteralPath $CandidateStartup)) { continue }
|
|
98
|
+
try {
|
|
99
|
+
New-Item -ItemType Directory -Path $CandidatePrefix -ErrorAction Stop | Out-Null
|
|
100
|
+
$Prefix = $CandidatePrefix
|
|
101
|
+
$OwnedPrefix = $true
|
|
102
|
+
Set-Content -LiteralPath (Join-Path $Prefix '.installer-owned') -Value $PID -NoNewline
|
|
103
|
+
$Name = $Candidate
|
|
104
|
+
break
|
|
105
|
+
} catch {
|
|
106
|
+
if (Test-Path -LiteralPath $CandidatePrefix) { continue }
|
|
107
|
+
throw
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (-not $Name) { Stop-Install 'could not allocate a unique Agent name' }
|
|
111
|
+
|
|
112
|
+
# The directory was atomically created by this process. Do not alter ACLs on
|
|
113
|
+
# pre-existing directories. Disable inheritance and grant only the current
|
|
114
|
+
# user, SYSTEM, and Administrators full control.
|
|
115
|
+
$CurrentUserAcl = $null
|
|
116
|
+
try { $CurrentUserAcl = '*' + [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value } catch {}
|
|
117
|
+
if (-not $CurrentUserAcl) { $CurrentUserAcl = $env:USERNAME }
|
|
118
|
+
if (-not $CurrentUserAcl) { Stop-Install 'could not identify the current Windows user' }
|
|
119
|
+
& icacls.exe $Prefix '/inheritance:r' '/grant:r' "${CurrentUserAcl}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' '*S-1-5-32-544:(OI)(CI)F' *> $null
|
|
120
|
+
if ($LASTEXITCODE -ne 0) { Stop-Install 'could not restrict installation directory permissions' }
|
|
121
|
+
|
|
122
|
+
if (-not $Node) {
|
|
123
|
+
$Architecture = $null
|
|
124
|
+
try { $Architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() } catch {}
|
|
125
|
+
if ([string]::IsNullOrWhiteSpace($Architecture)) {
|
|
126
|
+
$Architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
|
|
127
|
+
}
|
|
128
|
+
switch -Regex ($Architecture) {
|
|
129
|
+
'^(X64|AMD64|x86_64)$' { $NodeArch = 'x64'; break }
|
|
130
|
+
'^(Arm64|ARM64|aarch64)$' { $NodeArch = 'arm64'; break }
|
|
131
|
+
default { Stop-Install 'unsupported CPU architecture' }
|
|
132
|
+
}
|
|
133
|
+
$DownloadDir = Join-Path $Prefix '.download'
|
|
134
|
+
New-Item -ItemType Directory -Path $DownloadDir | Out-Null
|
|
135
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
136
|
+
$SumsPath = Join-Path $DownloadDir 'SHASUMS256.txt'
|
|
137
|
+
Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -Uri "https://nodejs.org/dist/latest-v$NodeChannel.x/SHASUMS256.txt" -OutFile $SumsPath
|
|
138
|
+
$Match = Get-Content -LiteralPath $SumsPath | Where-Object { $_ -match "^[0-9a-fA-F]{64}\s+node-v24\.[0-9.]+-win-$NodeArch\.zip$" } | Select-Object -First 1
|
|
139
|
+
if (-not $Match) { Stop-Install 'no supported Node.js archive was listed by nodejs.org' }
|
|
140
|
+
$Parts = $Match -split '\s+', 2
|
|
141
|
+
$Expected = $Parts[0].ToLowerInvariant()
|
|
142
|
+
$File = $Parts[1].Trim()
|
|
143
|
+
$Archive = Join-Path $DownloadDir $File
|
|
144
|
+
$VersionMatch = [regex]::Match($File, '^node-v([0-9.]+)-')
|
|
145
|
+
if (-not $VersionMatch.Success) { Stop-Install 'no supported Node.js archive was listed by nodejs.org' }
|
|
146
|
+
$NodeVersion = $VersionMatch.Groups[1].Value
|
|
147
|
+
Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -Uri "https://nodejs.org/dist/v$NodeVersion/$File" -OutFile $Archive
|
|
148
|
+
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Archive).Hash.ToLowerInvariant()
|
|
149
|
+
if ($Actual -ne $Expected) { Stop-Install 'Node.js checksum verification failed' }
|
|
150
|
+
$Extracted = Join-Path $DownloadDir 'extracted'
|
|
151
|
+
Expand-Archive -LiteralPath $Archive -DestinationPath $Extracted
|
|
152
|
+
$NodeFolder = Get-ChildItem -LiteralPath $Extracted -Directory | Select-Object -First 1
|
|
153
|
+
if (-not $NodeFolder) { Stop-Install 'could not unpack Node.js' }
|
|
154
|
+
$Runtime = Join-Path $Prefix 'runtime'
|
|
155
|
+
Move-Item -LiteralPath $NodeFolder.FullName -Destination $Runtime
|
|
156
|
+
Remove-Item -LiteralPath $DownloadDir -Recurse -Force
|
|
157
|
+
$Node = Join-Path $Runtime 'node.exe'
|
|
158
|
+
$Npm = Join-Path $Runtime 'npm.cmd'
|
|
159
|
+
$NpmCli = Join-Path $Runtime 'node_modules\npm\bin\npm-cli.js'
|
|
160
|
+
$DownloadedRuntime = $true
|
|
161
|
+
if (-not (Test-Node $Node)) { Stop-Install 'downloaded Node.js does not meet the minimum version' }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
$NodeDir = Split-Path -Parent $Node
|
|
165
|
+
$LocalBin = Join-Path $Prefix 'node_modules\.bin'
|
|
166
|
+
$env:PATH = "$NodeDir;$Prefix;$LocalBin;$OriginalPath"
|
|
167
|
+
& $Node $NpmCli --version *> $null
|
|
168
|
+
if ($LASTEXITCODE -ne 0) { Stop-Install 'the selected Node.js does not provide a working npm' }
|
|
169
|
+
& $Node $NpmCli --prefix $Prefix --global=false install $Package pm2 --registry=$Registry --no-audit --no-fund --loglevel=error
|
|
170
|
+
if ($LASTEXITCODE -ne 0) { Stop-Install 'npm could not install the Yeaft Agent' }
|
|
171
|
+
|
|
172
|
+
$Cli = Join-Path $Prefix 'node_modules\@yeaft\webchat-agent\cli.js'
|
|
173
|
+
$Pm2 = Join-Path $Prefix 'node_modules\pm2\bin\pm2'
|
|
174
|
+
if (-not (Test-Path -LiteralPath $Cli) -or -not (Test-Path -LiteralPath $Pm2)) {
|
|
175
|
+
Stop-Install 'the installed package did not provide the required CLI and PM2 runtime'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
$WorkDir = Join-Path $Prefix 'workspace'
|
|
179
|
+
$YeaftDir = Join-Path $Prefix 'data'
|
|
180
|
+
New-Item -ItemType Directory -Path $WorkDir | Out-Null
|
|
181
|
+
$Pm2Home = Join-Path $Prefix 'pm2'
|
|
182
|
+
$env:PM2_HOME = $Pm2Home
|
|
183
|
+
$env:SERVER_URL = $Server
|
|
184
|
+
$env:AGENT_SECRET = $Secret
|
|
185
|
+
$env:AGENT_NAME = $Name
|
|
186
|
+
$env:YEAFT_AGENT_INSTANCE = $Name
|
|
187
|
+
$env:WORK_DIR = $WorkDir
|
|
188
|
+
$env:YEAFT_DIR = $YeaftDir
|
|
189
|
+
$ServiceInstallAttempted = $true
|
|
190
|
+
& $Node $Cli install --name $Name --yeaft-dir $YeaftDir --work-dir $WorkDir *> $null
|
|
191
|
+
if ($LASTEXITCODE -ne 0) { Stop-Install 'the Agent service could not be installed' }
|
|
192
|
+
|
|
193
|
+
# The published Windows service writes a bare "pm2 resurrect" line. Repair
|
|
194
|
+
# only this newly allocated instance startup file, retaining its tray line.
|
|
195
|
+
$StartupPath = Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\Startup\yeaft-agent-$Name.bat"
|
|
196
|
+
if (-not (Test-Path -LiteralPath $StartupPath)) { Stop-Install 'the Agent startup file was not created' }
|
|
197
|
+
$StartupLines = @(Get-Content -LiteralPath $StartupPath)
|
|
198
|
+
$TrayLines = @($StartupLines | Where-Object { $_ -match '^\s*start\s+.*agent-tray\.ps1' })
|
|
199
|
+
$BatchNode = $Node.Replace('%', '%%')
|
|
200
|
+
$BatchPm2 = $Pm2.Replace('%', '%%')
|
|
201
|
+
$BatchPath = "$NodeDir;$Prefix;$LocalBin".Replace('%', '%%')
|
|
202
|
+
$BatchHome = $Pm2Home.Replace('%', '%%')
|
|
203
|
+
$StartupContent = @('@echo off', 'setlocal DisableDelayedExpansion', 'chcp 65001 >nul', "set `"PATH=$BatchPath;%PATH%`"", "set `"PM2_HOME=$BatchHome`"", "`"$BatchNode`" `"$BatchPm2`" resurrect") + $TrayLines
|
|
204
|
+
[IO.File]::WriteAllText($StartupPath, ($StartupContent -join "`r`n") + "`r`n", (New-Object Text.UTF8Encoding($false)))
|
|
205
|
+
|
|
206
|
+
$Manager = Join-Path $Prefix 'yeaft-agent.ps1'
|
|
207
|
+
$NodeLiteral = Quote-PowerShellLiteral $Node
|
|
208
|
+
$CliLiteral = Quote-PowerShellLiteral $Cli
|
|
209
|
+
$NameLiteral = Quote-PowerShellLiteral $Name
|
|
210
|
+
$YeaftLiteral = Quote-PowerShellLiteral $YeaftDir
|
|
211
|
+
$WorkLiteral = Quote-PowerShellLiteral $WorkDir
|
|
212
|
+
$PathLiteral = Quote-PowerShellLiteral "$NodeDir;$Prefix;$LocalBin"
|
|
213
|
+
$Pm2HomeLiteral = Quote-PowerShellLiteral $Pm2Home
|
|
214
|
+
Set-Content -LiteralPath $Manager -Encoding UTF8 -Value @"
|
|
215
|
+
# Run this file in a child PowerShell, preserving the caller's environment.
|
|
216
|
+
if (`$args -contains 'upgrade') { Write-Error 'Use this Agent instance in the Web UI to upgrade safely.'; exit 1 }
|
|
217
|
+
`$oldPath = `$env:PATH
|
|
218
|
+
`$oldPm2Home = `$env:PM2_HOME
|
|
219
|
+
try {
|
|
220
|
+
`$env:PATH = $PathLiteral + ';' + `$oldPath
|
|
221
|
+
`$env:PM2_HOME = $Pm2HomeLiteral
|
|
222
|
+
& $NodeLiteral $CliLiteral @args --name $NameLiteral --yeaft-dir $YeaftLiteral --work-dir $WorkLiteral
|
|
223
|
+
`$code = `$LASTEXITCODE
|
|
224
|
+
} finally {
|
|
225
|
+
`$env:PATH = `$oldPath
|
|
226
|
+
`$env:PM2_HOME = `$oldPm2Home
|
|
227
|
+
}
|
|
228
|
+
exit `$code
|
|
229
|
+
"@
|
|
230
|
+
Set-Content -LiteralPath (Join-Path $Prefix 'node-path') -Value $Node
|
|
231
|
+
Remove-Item -LiteralPath (Join-Path $Prefix '.installer-owned') -Force
|
|
232
|
+
New-Item -ItemType File -Path (Join-Path $Prefix '.complete') | Out-Null
|
|
233
|
+
Write-Host "`nYeaft Agent $Name was installed."
|
|
234
|
+
Set-Content -LiteralPath (Join-Path $Prefix 'management-command') -Value $Manager
|
|
235
|
+
Write-Host "Manage it with:`n powershell -NoProfile -ExecutionPolicy Bypass -File $(Quote-PowerShellLiteral $Manager) status"
|
|
236
|
+
} catch {
|
|
237
|
+
$InstallFailed = $true
|
|
238
|
+
if ($OwnedPrefix -and -not $ServiceInstallAttempted -and $Prefix -and
|
|
239
|
+
(Test-Path -LiteralPath (Join-Path $Prefix '.installer-owned'))) {
|
|
240
|
+
Remove-Item -LiteralPath $Prefix -Recurse -Force -ErrorAction SilentlyContinue
|
|
241
|
+
}
|
|
242
|
+
$SafeMessages = @(
|
|
243
|
+
'-Server must be a valid ws:// or wss:// URL with a host','-Secret must not be empty',
|
|
244
|
+
'arguments must not contain line breaks','APPDATA is required to install the Windows user service',
|
|
245
|
+
'HOME is required to allocate the Agent installation','unsupported CPU architecture',
|
|
246
|
+
'could not allocate a unique Agent name','could not identify the current Windows user',
|
|
247
|
+
'could not restrict installation directory permissions',
|
|
248
|
+
'no supported Node.js archive was listed by nodejs.org','Node.js checksum verification failed',
|
|
249
|
+
'could not unpack Node.js','downloaded Node.js does not meet the minimum version',
|
|
250
|
+
'npm could not install the Yeaft Agent','the selected Node.js does not provide a working npm',
|
|
251
|
+
'the installed package did not provide the required CLI and PM2 runtime',
|
|
252
|
+
'the Agent service could not be installed','the Agent startup file was not created'
|
|
253
|
+
)
|
|
254
|
+
$Message = if ($SafeMessages -contains $_.Exception.Message) { $_.Exception.Message } else { 'an unexpected operation failed; no existing Agent was changed' }
|
|
255
|
+
[Console]::Error.WriteLine("Yeaft Agent installation failed: $Message")
|
|
256
|
+
} finally {
|
|
257
|
+
$env:PATH = $OriginalPath
|
|
258
|
+
[Net.ServicePointManager]::SecurityProtocol = $OriginalTls
|
|
259
|
+
foreach ($Key in $SavedEnvironment.Keys) {
|
|
260
|
+
if ($SavedEnvironment[$Key].Present) { Set-Item -LiteralPath "Env:$Key" -Value $SavedEnvironment[$Key].Value }
|
|
261
|
+
else { Remove-Item -LiteralPath "Env:$Key" -ErrorAction SilentlyContinue }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if ($InstallFailed) { exit 1 }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# 用户级一键安装:只创建新实例,不覆盖系统 Node/npm 或已有 Agent。
|
|
3
|
+
set -eu
|
|
4
|
+
umask 077
|
|
5
|
+
|
|
6
|
+
NODE_CHANNEL=24
|
|
7
|
+
REGISTRY=https://pkg.yeaft.com/
|
|
8
|
+
PACKAGE=@yeaft/webchat-agent@latest
|
|
9
|
+
SERVER=
|
|
10
|
+
SECRET=
|
|
11
|
+
PREFIX=
|
|
12
|
+
SERVICE_ATTEMPTED=false
|
|
13
|
+
|
|
14
|
+
usage() {
|
|
15
|
+
cat <<'EOF'
|
|
16
|
+
Usage: install.sh --server <ws:// or wss:// URL> --secret <agent secret>
|
|
17
|
+
|
|
18
|
+
Installs a new Yeaft Agent in ~/.yeaft/installations/<hostname>-NNNN.
|
|
19
|
+
Requires a Linux systemd user session or macOS launchd login session.
|
|
20
|
+
Reuses Node >=22.5.0 with npm, or downloads a private Node 24 runtime.
|
|
21
|
+
No sudo, system PATH changes, or upgrades/restarts of existing Agents.
|
|
22
|
+
EOF
|
|
23
|
+
}
|
|
24
|
+
fail() { printf '%s\n' "Yeaft Agent installation failed: $1" >&2; exit 1; }
|
|
25
|
+
cleanup() {
|
|
26
|
+
# PREFIX is assigned only AFTER successful exclusive creation. Once service
|
|
27
|
+
# installation starts, retain its runtime/config/logs for recovery.
|
|
28
|
+
if [ -n "$PREFIX" ] && [ "$SERVICE_ATTEMPTED" = false ]; then rm -rf "$PREFIX"; fi
|
|
29
|
+
}
|
|
30
|
+
trap cleanup EXIT
|
|
31
|
+
trap 'exit 130' INT
|
|
32
|
+
trap 'exit 143' HUP TERM
|
|
33
|
+
quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"; }
|
|
34
|
+
|
|
35
|
+
while [ "$#" -gt 0 ]; do
|
|
36
|
+
case "$1" in
|
|
37
|
+
--server) [ "$#" -ge 2 ] || fail '--server requires a value'; SERVER=$2; shift 2 ;;
|
|
38
|
+
--secret) [ "$#" -ge 2 ] || fail '--secret requires a value'; SECRET=$2; shift 2 ;;
|
|
39
|
+
-h|--help) usage; exit 0 ;;
|
|
40
|
+
*) fail 'unknown argument (use --help)' ;;
|
|
41
|
+
esac
|
|
42
|
+
done
|
|
43
|
+
case "$SERVER" in ws://?*|wss://?*) ;; *) fail '--server must be a ws:// or wss:// URL' ;; esac
|
|
44
|
+
[ -n "$SECRET" ] || fail '--secret must not be empty'
|
|
45
|
+
CR=$(printf '\r')
|
|
46
|
+
case "$SERVER$SECRET$HOME" in *"
|
|
47
|
+
"*|*"$CR"*) fail 'arguments and home directory must not contain line breaks' ;; esac
|
|
48
|
+
|
|
49
|
+
OS=$(uname -s)
|
|
50
|
+
ARCH=$(uname -m)
|
|
51
|
+
case "$OS" in
|
|
52
|
+
Linux)
|
|
53
|
+
command -v systemctl >/dev/null 2>&1 || fail 'Linux requires systemctl and a systemd user session'
|
|
54
|
+
systemctl --user show-environment >/dev/null 2>&1 || fail 'Linux requires an available systemd user manager; log in with a systemd user session and retry'
|
|
55
|
+
PLATFORM=linux ;;
|
|
56
|
+
Darwin)
|
|
57
|
+
command -v launchctl >/dev/null 2>&1 || fail 'macOS requires launchd'
|
|
58
|
+
launchctl print "gui/$(id -u)" >/dev/null 2>&1 || fail 'macOS requires a graphical login session for launchd'
|
|
59
|
+
PLATFORM=darwin ;;
|
|
60
|
+
*) fail 'unsupported operating system (use PowerShell on Windows)' ;;
|
|
61
|
+
esac
|
|
62
|
+
case "$ARCH" in x86_64|amd64) NODE_ARCH=x64 ;; arm64|aarch64) NODE_ARCH=arm64 ;; *) fail 'supported CPU architectures are x64 and arm64' ;; esac
|
|
63
|
+
version_ok() {
|
|
64
|
+
"$1" -e 'const [a,b]=process.versions.node.split(".").map(Number);if(a<22||(a===22&&b<5))process.exit(1);require("node:sqlite")' >/dev/null 2>&1
|
|
65
|
+
}
|
|
66
|
+
NODE=
|
|
67
|
+
NPM=
|
|
68
|
+
if command -v node >/dev/null 2>&1; then
|
|
69
|
+
CANDIDATE_NODE=$(command -v node)
|
|
70
|
+
# Resolve relative PATH entries, but retain the bin directory that owns npm.
|
|
71
|
+
CANDIDATE_NODE=$(cd "$(dirname "$CANDIDATE_NODE")" && pwd)/node
|
|
72
|
+
CANDIDATE_NPM=$(dirname "$CANDIDATE_NODE")/npm
|
|
73
|
+
if [ -x "$CANDIDATE_NPM" ] && version_ok "$CANDIDATE_NODE" && PATH="$(dirname "$CANDIDATE_NODE"):$PATH" "$CANDIDATE_NPM" --version >/dev/null 2>&1; then
|
|
74
|
+
NODE=$CANDIDATE_NODE
|
|
75
|
+
NPM=$CANDIDATE_NPM
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
ROOT=$HOME/.yeaft/installations
|
|
80
|
+
HOST=$(hostname 2>/dev/null || printf agent)
|
|
81
|
+
HOST=$(printf '%s' "$HOST" | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed 's/[^a-z0-9._-]/-/g; s/^-*//; s/-*$//' | cut -c1-40)
|
|
82
|
+
[ -n "$HOST" ] || HOST=agent
|
|
83
|
+
mkdir -p "$ROOT"
|
|
84
|
+
tries=0
|
|
85
|
+
while [ "$tries" -lt 100 ]; do
|
|
86
|
+
tries=$((tries + 1))
|
|
87
|
+
DIGITS=$(od -An -N4 -tu4 /dev/urandom | awk '{printf "%04d", $1 % 10000}')
|
|
88
|
+
case "$DIGITS" in [0-9][0-9][0-9][0-9]) ;; *) fail 'could not generate an instance name' ;; esac
|
|
89
|
+
NAME=$HOST-$DIGITS
|
|
90
|
+
if [ -e "$HOME/.config/yeaft-agent/instances/$NAME" ] || [ -L "$HOME/.config/yeaft-agent/instances/$NAME" ] ||
|
|
91
|
+
[ -e "$HOME/.yeaft/instances/$NAME" ] || [ -L "$HOME/.yeaft/instances/$NAME" ] ||
|
|
92
|
+
[ -e "$HOME/.config/systemd/user/yeaft-agent@$NAME.service" ] || [ -L "$HOME/.config/systemd/user/yeaft-agent@$NAME.service" ] ||
|
|
93
|
+
[ -e "$HOME/Library/LaunchAgents/com.yeaft.agent.$NAME.plist" ] || [ -L "$HOME/Library/LaunchAgents/com.yeaft.agent.$NAME.plist" ]; then continue; fi
|
|
94
|
+
if mkdir "$ROOT/$NAME" 2>/dev/null; then PREFIX=$ROOT/$NAME; break; fi
|
|
95
|
+
done
|
|
96
|
+
[ -n "$PREFIX" ] || fail 'could not allocate a unique Agent name'
|
|
97
|
+
|
|
98
|
+
if [ -z "$NODE" ]; then
|
|
99
|
+
command -v curl >/dev/null 2>&1 || fail 'curl is required to download Node.js'
|
|
100
|
+
command -v tar >/dev/null 2>&1 || fail 'tar is required to unpack Node.js'
|
|
101
|
+
printf 'Downloading a private Node.js 24 runtime...\n'
|
|
102
|
+
STAGING=$PREFIX/.download
|
|
103
|
+
mkdir "$STAGING"
|
|
104
|
+
SUMS=$STAGING/SHASUMS256.txt
|
|
105
|
+
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$SUMS" "https://nodejs.org/dist/latest-v$NODE_CHANNEL.x/SHASUMS256.txt" || fail 'could not download Node.js checksums'
|
|
106
|
+
FILE=$(awk -v suffix="-$PLATFORM-$NODE_ARCH.tar.gz" '$2 ~ /^node-v24\.[0-9]+\.[0-9]+-/ && substr($2,length($2)-length(suffix)+1)==suffix {print $2; exit}' "$SUMS")
|
|
107
|
+
[ -n "$FILE" ] || fail 'no supported Node.js archive was listed by nodejs.org'
|
|
108
|
+
VERSION=${FILE#node-}; VERSION=${VERSION%%-*}
|
|
109
|
+
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 -o "$STAGING/$FILE" "https://nodejs.org/dist/$VERSION/$FILE" || fail 'could not download Node.js'
|
|
110
|
+
EXPECTED=$(awk -v f="$FILE" '$2==f {print $1; exit}' "$SUMS")
|
|
111
|
+
if command -v sha256sum >/dev/null 2>&1; then ACTUAL=$(sha256sum "$STAGING/$FILE" | awk '{print $1}');
|
|
112
|
+
elif command -v shasum >/dev/null 2>&1; then ACTUAL=$(shasum -a 256 "$STAGING/$FILE" | awk '{print $1}');
|
|
113
|
+
else fail 'sha256sum or shasum is required to verify Node.js'; fi
|
|
114
|
+
[ "${#EXPECTED}" -eq 64 ] && [ "$ACTUAL" = "$EXPECTED" ] || fail 'Node.js checksum verification failed'
|
|
115
|
+
mkdir "$PREFIX/runtime"
|
|
116
|
+
tar -xzf "$STAGING/$FILE" -C "$PREFIX/runtime" --strip-components=1 || fail 'could not unpack Node.js'
|
|
117
|
+
NODE=$PREFIX/runtime/bin/node
|
|
118
|
+
NPM=$PREFIX/runtime/bin/npm
|
|
119
|
+
version_ok "$NODE" || fail 'downloaded Node.js cannot run on this OS or does not meet the minimum version'
|
|
120
|
+
PATH="$(dirname "$NODE"):$PATH" "$NPM" --version >/dev/null 2>&1 || fail 'downloaded Node.js did not provide a working npm'
|
|
121
|
+
rm -rf "$STAGING"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
YEAFT_INSTALL_SERVER="$SERVER" "$NODE" -e 'try {const u=new URL(process.env.YEAFT_INSTALL_SERVER);if(!["ws:","wss:"].includes(u.protocol)||!u.hostname||u.username||u.password)process.exit(1)}catch{process.exit(1)}' >/dev/null 2>&1 || fail 'invalid server URL'
|
|
125
|
+
NODE_DIR=$(dirname "$NODE")
|
|
126
|
+
printf 'Installing the Yeaft Agent in %s...\n' "$PREFIX"
|
|
127
|
+
PATH="$NODE_DIR:$PREFIX/bin:$PATH" "$NPM" --prefix "$PREFIX" --global install "$PACKAGE" \
|
|
128
|
+
--registry="$REGISTRY" --no-audit --no-fund --loglevel=error || fail 'npm could not install the Yeaft Agent'
|
|
129
|
+
CLI_JS=$PREFIX/lib/node_modules/@yeaft/webchat-agent/cli.js
|
|
130
|
+
[ -f "$CLI_JS" ] || fail 'the installed package did not provide yeaft-agent'
|
|
131
|
+
|
|
132
|
+
WRAPPER=$PREFIX/yeaft-agent
|
|
133
|
+
{
|
|
134
|
+
printf '#!/bin/sh\nPATH=%s:"$PATH"; export PATH\n' "$(quote "$NODE_DIR:$PREFIX/bin")"
|
|
135
|
+
printf '%s\n' 'for arg in "$@"; do if [ "$arg" = upgrade ]; then echo "Use this Agent instance in the Web UI to upgrade safely; standalone CLI upgrade targets a global npm installation." >&2; exit 1; fi; done'
|
|
136
|
+
printf 'exec %s %s "$@" --name %s --yeaft-dir %s\n' "$(quote "$NODE")" "$(quote "$CLI_JS")" "$(quote "$NAME")" "$(quote "$PREFIX/data")"
|
|
137
|
+
} > "$WRAPPER"
|
|
138
|
+
chmod 700 "$WRAPPER"
|
|
139
|
+
printf '%s\n' "$WRAPPER" > "$PREFIX/management-command"
|
|
140
|
+
# Use explicit roots rather than inheriting another Agent's WORK_DIR/YEAFT_DIR.
|
|
141
|
+
# Secret is a script argument, but is not passed through a shell to the CLI.
|
|
142
|
+
mkdir "$PREFIX/workspace"
|
|
143
|
+
SERVICE_ATTEMPTED=true
|
|
144
|
+
AGENT_SECRET="$SECRET" PATH="$NODE_DIR:$PREFIX/bin:$PATH" "$NODE" "$CLI_JS" install --server "$SERVER" --name "$NAME" \
|
|
145
|
+
--yeaft-dir "$PREFIX/data" --work-dir "$PREFIX/workspace" > "$PREFIX/service-install.log" 2>&1 || fail "service installation failed; inspect the private log at $PREFIX/service-install.log"
|
|
146
|
+
: > "$PREFIX/.complete"
|
|
147
|
+
printf '\nYeaft Agent %s was installed.\nManage it with:\n %s status --name %s\n' "$NAME" "$(quote "$WRAPPER")" "$NAME"
|
|
148
|
+
if [ "$PLATFORM" = linux ]; then
|
|
149
|
+
printf 'To keep the user service running after logout, ask an administrator to enable linger for your account.\n'
|
|
150
|
+
fi
|