llm-api-gateway-cli 1.0.0
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/.env.example +10 -0
- package/README.md +1127 -0
- package/cli-agent.js +666 -0
- package/cli-anthropic.js +236 -0
- package/cli-claude-code.js +317 -0
- package/cli-openai.js +212 -0
- package/completions/_llm-api-gateway-cli +65 -0
- package/completions/llm-api-gateway-cli.bash +64 -0
- package/completions/llm-api-gateway-cli.fish +43 -0
- package/images/chat.png +0 -0
- package/images/settings.png +0 -0
- package/images/task.png +0 -0
- package/lib/agent.js +607 -0
- package/lib/commands.js +468 -0
- package/lib/common.js +196 -0
- package/lib/config.js +70 -0
- package/lib/configcmd.js +230 -0
- package/lib/hub.js +1494 -0
- package/lib/jsonstore.js +49 -0
- package/lib/mcp.js +375 -0
- package/lib/memory.js +109 -0
- package/lib/plandoc.js +178 -0
- package/lib/pricing.js +52 -0
- package/lib/runner.js +234 -0
- package/lib/runstore.js +96 -0
- package/lib/secrets.js +198 -0
- package/lib/sessionstore.js +269 -0
- package/lib/settings.js +517 -0
- package/lib/tasksession.js +594 -0
- package/lib/taskstore.js +740 -0
- package/lib/tools.js +927 -0
- package/package.json +55 -0
- package/public/app.js +1055 -0
- package/public/index.html +167 -0
- package/public/manual.css +215 -0
- package/public/manual.html +381 -0
- package/public/manual.js +186 -0
- package/public/models.js +121 -0
- package/public/render.js +250 -0
- package/public/styles.css +955 -0
- package/public/task-slash.js +493 -0
- package/public/task.css +739 -0
- package/public/task.html +220 -0
- package/public/task.js +3127 -0
- package/public/theme.js +91 -0
- package/public/tint.js +261 -0
- package/scripts/install.ps1 +537 -0
- package/scripts/install.sh +510 -0
- package/server.js +14 -0
- package/task-server.js +15 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
llm-api-gateway-cli 一键安装脚本(Windows PowerShell 5.1 / PowerShell 7 通用)。
|
|
4
|
+
|
|
5
|
+
.NOTES
|
|
6
|
+
本文件必须保持 **UTF-8 带 BOM**:Windows PowerShell 5.1 读无 BOM 的 UTF-8 会按 ANSI(中文
|
|
7
|
+
系统上是 GBK)解,结果不只是中文乱码 —— 某些字节序列会被解成引号/反引号,直接把脚本变成
|
|
8
|
+
语法错误。不少编辑器与写文件工具保存时会丢掉 BOM,**改完请复查首三字节是不是 EF BB BF**:
|
|
9
|
+
|
|
10
|
+
[System.IO.File]::ReadAllBytes('scripts\install.ps1')[0..2] -join ',' # 应为 239,187,191
|
|
11
|
+
|
|
12
|
+
行尾统一 LF(仓库根有 .gitattributes 兜底,Windows 上手工改仍可能变 CRLF)。
|
|
13
|
+
|
|
14
|
+
.DESCRIPTION
|
|
15
|
+
从 GitHub Release 下载 `npm pack` 产物(llm-api-gateway-cli-<版本>.tgz),
|
|
16
|
+
用 `npm install -g --prefix <目录>` 装到用户私有目录,并把 bin 目录接进用户 PATH。
|
|
17
|
+
不做逐平台编译产物:本项目除 openai / @anthropic-ai/sdk 外零运行时依赖,
|
|
18
|
+
一份 tarball 同时供 curl 安装与 `npm i -g` 两条路线使用,避免两套东西漂移。
|
|
19
|
+
|
|
20
|
+
为什么默认装到 $HOME\.llm-api-gateway:Windows 的全局 npm 目录(%APPDATA%\npm)
|
|
21
|
+
与 Program Files 都可能需要管理员权限或被杀软盯着,装到用户家目录既不需要提权,
|
|
22
|
+
卸载也只是删一个目录。这对「装到无写权限的全局目录」这条风险是直接对策。
|
|
23
|
+
|
|
24
|
+
.PARAMETER Version
|
|
25
|
+
安装指定版本,例如 v1.0.0 或 1.0.0;不传则查最新 Release。
|
|
26
|
+
|
|
27
|
+
.PARAMETER Prefix
|
|
28
|
+
安装目录,默认 $HOME\.llm-api-gateway。
|
|
29
|
+
|
|
30
|
+
.PARAMETER Base
|
|
31
|
+
所有下载地址的前缀,用于换国内镜像(默认 GitHub 仓库地址)。
|
|
32
|
+
|
|
33
|
+
.PARAMETER NoModifyPath
|
|
34
|
+
不改用户 PATH,只打印需要手工加的一项。
|
|
35
|
+
|
|
36
|
+
.PARAMETER DryRun
|
|
37
|
+
演练:只打印将要执行的步骤,不下载、不安装、不改 PATH。
|
|
38
|
+
|
|
39
|
+
.PARAMETER Force
|
|
40
|
+
已是目标版本时也重装一遍(默认直接跳过)。
|
|
41
|
+
|
|
42
|
+
.PARAMETER Help
|
|
43
|
+
显示帮助。
|
|
44
|
+
|
|
45
|
+
.EXAMPLE
|
|
46
|
+
irm https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/v1.0.0/scripts/install.ps1 | iex
|
|
47
|
+
|
|
48
|
+
.EXAMPLE
|
|
49
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install.ps1 -Version v1.0.0 -DryRun
|
|
50
|
+
|
|
51
|
+
.EXAMPLE
|
|
52
|
+
# 管道进来时没法直接传参,用 scriptblock 形式传
|
|
53
|
+
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/boonya-hrgk/llm-api-gateway-cli/v1.0.0/scripts/install.ps1))) -Help
|
|
54
|
+
|
|
55
|
+
.NOTES
|
|
56
|
+
环境变量(同名参数优先于环境变量):
|
|
57
|
+
LLM_GATEWAY_INSTALL_BASE 所有下载地址的前缀(默认 GitHub 仓库地址)
|
|
58
|
+
LLM_GATEWAY_INSTALL_VERSION 等价 -Version
|
|
59
|
+
LLM_GATEWAY_INSTALL_DIR 等价 -Prefix(另外兼容文档里写的 PREFIX)
|
|
60
|
+
LLM_GATEWAY_INSTALL_API 查询 latest 用的 API 前缀(默认 https://api.github.com)
|
|
61
|
+
LLM_GATEWAY_INSTALL_DRY_RUN=1 等价 -DryRun
|
|
62
|
+
GITHUB_ACTIONS=true 在 CI 里把 bin 目录写进 $env:GITHUB_PATH,而不是改用户 PATH
|
|
63
|
+
#>
|
|
64
|
+
param(
|
|
65
|
+
[string]$Version = '',
|
|
66
|
+
[string]$Prefix = '',
|
|
67
|
+
[string]$Base = '',
|
|
68
|
+
[switch]$NoModifyPath,
|
|
69
|
+
[switch]$DryRun,
|
|
70
|
+
[switch]$Force,
|
|
71
|
+
[switch]$Help
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# ── 常量 ─────────────────────────────────────────────────────────────────────
|
|
75
|
+
# TGZ 名必须与 package.json 的 name/version 对齐:`npm pack` 产出 <name>-<version>.tgz,
|
|
76
|
+
# 而安装器是按 tag 拼文件名的,两边一旦不一致就是 404(CI 里也钉了这条一致性)。
|
|
77
|
+
$AppName = 'llm-api-gateway-cli'
|
|
78
|
+
$MainCmd = 'gateway-agent'
|
|
79
|
+
$DefaultRepoSlug = 'boonya-hrgk/llm-api-gateway-cli'
|
|
80
|
+
$DefaultBase = "https://github.com/$DefaultRepoSlug"
|
|
81
|
+
$DefaultApi = 'https://api.github.com'
|
|
82
|
+
$MinNodeMajor = 18
|
|
83
|
+
# 装 5.1 也认这个 UA:GitHub API 对没有 UA 的请求直接 403,而 PS5.1 的默认 UA
|
|
84
|
+
# 在某些代理/网关下会被改写,显式带上最省事。
|
|
85
|
+
$UserAgent = "$AppName-installer"
|
|
86
|
+
|
|
87
|
+
# ── 输出 ─────────────────────────────────────────────────────────────────────
|
|
88
|
+
# 提示符统一用 ASCII(==> / [ok] / [!] / [错误]):Windows 控制台在 GBK 代码页下
|
|
89
|
+
# 显示 ✓ ✗ 这类符号会变成问号,日志重定向后更难认。中文本身靠文件 BOM 保证不乱码。
|
|
90
|
+
function Write-Info { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Cyan }
|
|
91
|
+
function Write-Ok { param([string]$Message) Write-Host " [ok] $Message" -ForegroundColor Green }
|
|
92
|
+
function Write-Warn { param([string]$Message) Write-Host " [!] $Message" -ForegroundColor Yellow }
|
|
93
|
+
function Fail { param([string]$Message) throw $Message }
|
|
94
|
+
|
|
95
|
+
function Show-Usage {
|
|
96
|
+
@"
|
|
97
|
+
llm-api-gateway-cli 一键安装脚本(Windows / PowerShell 5.1 与 7)
|
|
98
|
+
|
|
99
|
+
用法:
|
|
100
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File install.ps1 [选项]
|
|
101
|
+
irm <base>/scripts/install.ps1 | iex
|
|
102
|
+
& ([scriptblock]::Create((irm <base>/scripts/install.ps1))) [选项] # 管道形式想传参就用这个
|
|
103
|
+
|
|
104
|
+
选项:
|
|
105
|
+
-Version <版本> 安装指定版本,例如 v1.0.0 或 1.0.0(默认查最新 Release)
|
|
106
|
+
-Prefix <目录> 安装目录(默认 $HOME\.llm-api-gateway,不需要管理员权限)
|
|
107
|
+
-Base <URL> 所有下载地址的前缀,用于换镜像
|
|
108
|
+
-NoModifyPath 不改用户 PATH,只打印需要手工加的一项
|
|
109
|
+
-DryRun 演练:只打印将要执行的步骤,不下载、不安装、不改 PATH
|
|
110
|
+
-Force 已是目标版本时也重装一遍(默认直接跳过)
|
|
111
|
+
-Help 显示本帮助
|
|
112
|
+
|
|
113
|
+
环境变量:
|
|
114
|
+
LLM_GATEWAY_INSTALL_BASE 下载前缀(默认 $DefaultBase)
|
|
115
|
+
LLM_GATEWAY_INSTALL_VERSION 等价 -Version
|
|
116
|
+
LLM_GATEWAY_INSTALL_DIR 等价 -Prefix(兼容 PREFIX)
|
|
117
|
+
LLM_GATEWAY_INSTALL_API latest 查询前缀(默认 $DefaultApi)
|
|
118
|
+
LLM_GATEWAY_INSTALL_DRY_RUN=1 等价 -DryRun
|
|
119
|
+
GITHUB_ACTIONS=true 把 bin 目录写进 `$env:GITHUB_PATH
|
|
120
|
+
|
|
121
|
+
装完怎么用:
|
|
122
|
+
gateway-task # 起服务(没有密钥也能起),在页面「设置」里填密钥
|
|
123
|
+
$MainCmd config set key sk-xxx # 或者命令行填:写 credentials.json,不进 config.json
|
|
124
|
+
`$env:GATEWAY_KEY = 'sk-xxx' # 或者环境变量 / SK / .env(优先级更高)
|
|
125
|
+
$MainCmd --help
|
|
126
|
+
$MainCmd -p "用一句话介绍你自己"
|
|
127
|
+
"@ | Write-Host
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ── 全局状态 ─────────────────────────────────────────────────────────────────
|
|
131
|
+
$script:TempDir = ''
|
|
132
|
+
# windows 上 npm 把 bin 垫片直接放在 prefix 根目录,包体在 <prefix>\node_modules;
|
|
133
|
+
# unix 上才是 <prefix>/bin 与 <prefix>/lib/node_modules —— 判定错了就会出现
|
|
134
|
+
# 「装完了但 PATH 指到一个空目录」。
|
|
135
|
+
$script:BinDir = ''
|
|
136
|
+
$script:PkgJson = ''
|
|
137
|
+
|
|
138
|
+
function Get-PlatformFlags {
|
|
139
|
+
$onWindows = $true
|
|
140
|
+
if ($PSVersionTable.PSVersion.Major -ge 6) { $onWindows = [bool]$IsWindows }
|
|
141
|
+
return $onWindows
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function Invoke-Setup {
|
|
145
|
+
if ($Help) { Show-Usage; return }
|
|
146
|
+
|
|
147
|
+
if ($PSVersionTable.PSVersion.Major -lt 5) {
|
|
148
|
+
Fail "需要 PowerShell 5.1 或更高版本(当前 $($PSVersionTable.PSVersion))"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# 5.1 默认可能还在 TLS 1.0:老系统上不补这一句,连 GitHub 会以「基础连接已关闭」失败,
|
|
152
|
+
# 用户只会以为是网络问题。PS7 里这句是空操作,留着无害。
|
|
153
|
+
try {
|
|
154
|
+
$sp = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
155
|
+
[Net.ServicePointManager]::SecurityProtocol = $sp
|
|
156
|
+
} catch { }
|
|
157
|
+
|
|
158
|
+
# 5.1 的进度条在下载大文件时会拖慢一个数量级;用完还原,别改用户会话的状态
|
|
159
|
+
$oldProgress = $ProgressPreference
|
|
160
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
161
|
+
try {
|
|
162
|
+
# ── 参数与环境变量合并 ───────────────────────────────────────────────────
|
|
163
|
+
if (-not $Base) {
|
|
164
|
+
if ($env:LLM_GATEWAY_INSTALL_BASE) { $Base = $env:LLM_GATEWAY_INSTALL_BASE } else { $Base = $DefaultBase }
|
|
165
|
+
}
|
|
166
|
+
if (-not $Version -and $env:LLM_GATEWAY_INSTALL_VERSION) { $Version = $env:LLM_GATEWAY_INSTALL_VERSION }
|
|
167
|
+
if (-not $Prefix) {
|
|
168
|
+
if ($env:LLM_GATEWAY_INSTALL_DIR) { $Prefix = $env:LLM_GATEWAY_INSTALL_DIR }
|
|
169
|
+
elseif ($env:PREFIX) { $Prefix = $env:PREFIX }
|
|
170
|
+
else { $Prefix = Join-Path $HOME '.llm-api-gateway' }
|
|
171
|
+
}
|
|
172
|
+
$api = $DefaultApi
|
|
173
|
+
if ($env:LLM_GATEWAY_INSTALL_API) { $api = $env:LLM_GATEWAY_INSTALL_API }
|
|
174
|
+
if ($env:LLM_GATEWAY_INSTALL_DRY_RUN -eq '1') { $DryRun = $true }
|
|
175
|
+
|
|
176
|
+
# 结尾斜杠会让拼出来的 URL 变 //,某些镜像/签名校验会因此 403
|
|
177
|
+
$Base = $Base.TrimEnd('/')
|
|
178
|
+
$api = $api.TrimEnd('/')
|
|
179
|
+
|
|
180
|
+
# 展开 ~ 与相对路径:后面要做 PATH 幂等比较,带 ~ 的字符串永远比不出相等
|
|
181
|
+
$Prefix = $Prefix.TrimEnd('\', '/')
|
|
182
|
+
if ($Prefix -eq '~') { $Prefix = $HOME }
|
|
183
|
+
elseif ($Prefix.StartsWith('~/') -or $Prefix.StartsWith('~\')) { $Prefix = Join-Path $HOME $Prefix.Substring(2) }
|
|
184
|
+
if (-not [System.IO.Path]::IsPathRooted($Prefix)) { $Prefix = Join-Path (Get-Location).ProviderPath $Prefix }
|
|
185
|
+
$Prefix = [System.IO.Path]::GetFullPath($Prefix)
|
|
186
|
+
|
|
187
|
+
$onWindows = Get-PlatformFlags
|
|
188
|
+
if ($onWindows) {
|
|
189
|
+
$script:BinDir = $Prefix
|
|
190
|
+
$script:PkgJson = Join-Path (Join-Path $Prefix 'node_modules') (Join-Path $AppName 'package.json')
|
|
191
|
+
} else {
|
|
192
|
+
$script:BinDir = Join-Path $Prefix 'bin'
|
|
193
|
+
$script:PkgJson = Join-Path (Join-Path (Join-Path $Prefix 'lib') 'node_modules') (Join-Path $AppName 'package.json')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# 从下载前缀里反推仓库 slug:fork / 自建镜像的人不用再手改脚本里的常量
|
|
197
|
+
$repoSlug = $DefaultRepoSlug
|
|
198
|
+
if ($Base -match '^https://github\.com/([^/]+/[^/]+)') { $repoSlug = $Matches[1] }
|
|
199
|
+
|
|
200
|
+
if ($DryRun) { Write-Info '演练模式(-DryRun):只打印将要做的事,不下载、不安装、不改 PATH' }
|
|
201
|
+
Write-Info "安装目录:$Prefix"
|
|
202
|
+
Write-Info "下载前缀:$Base"
|
|
203
|
+
Write-Info "bin 垫片目录:$($script:BinDir)"
|
|
204
|
+
|
|
205
|
+
# ── 前置检查:Node >= 18 ─────────────────────────────────────────────────
|
|
206
|
+
$node = Get-Command node -ErrorAction SilentlyContinue
|
|
207
|
+
if (-not $node) {
|
|
208
|
+
Fail @"
|
|
209
|
+
找不到 node。本工具需要 Node.js $MinNodeMajor 或更高版本;请先自行安装后重跑:
|
|
210
|
+
· 官方安装包 https://nodejs.org/zh-cn/download
|
|
211
|
+
· 版本管理 nvm-windows (https://github.com/coreybutler/nvm-windows) / fnm / scoop / winget
|
|
212
|
+
安装器不代装 Node —— 替你改系统级运行时是越界行为,装错版本更难排查。
|
|
213
|
+
"@
|
|
214
|
+
}
|
|
215
|
+
$nodeRaw = (& node -v 2>$null | Select-Object -First 1)
|
|
216
|
+
if ($LASTEXITCODE -ne 0 -or -not $nodeRaw) {
|
|
217
|
+
Fail "node 存在但执行失败(node -v 退出码 $LASTEXITCODE),请检查 Node 安装是否完整"
|
|
218
|
+
}
|
|
219
|
+
if ($nodeRaw -notmatch '^v?(\d+)\.') {
|
|
220
|
+
Fail "无法识别 node 版本(node -v 输出:'$nodeRaw')"
|
|
221
|
+
}
|
|
222
|
+
$nodeMajor = [int]$Matches[1]
|
|
223
|
+
if ($nodeMajor -lt $MinNodeMajor) {
|
|
224
|
+
Fail @"
|
|
225
|
+
Node.js 版本过低:当前 $nodeRaw,需要 v$MinNodeMajor 及以上。
|
|
226
|
+
升级办法:nvm install $MinNodeMajor(然后 nvm use $MinNodeMajor),或用 winget upgrade OpenJS.NodeJS
|
|
227
|
+
"@
|
|
228
|
+
}
|
|
229
|
+
Write-Ok "Node.js $nodeRaw"
|
|
230
|
+
|
|
231
|
+
$npmCmd = Get-Command npm.cmd -ErrorAction SilentlyContinue
|
|
232
|
+
if (-not $npmCmd) { $npmCmd = Get-Command npm -ErrorAction SilentlyContinue }
|
|
233
|
+
if (-not $npmCmd) {
|
|
234
|
+
Fail ' 找不到 npm。它随 Node.js 一起分发,请检查安装是否完整(node -v 正常但 npm 缺失,常见于手工拼装的环境变量)'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
# ── 版本解析 ─────────────────────────────────────────────────────────────
|
|
238
|
+
if (-not $Version) {
|
|
239
|
+
if ($DryRun) {
|
|
240
|
+
# 演练模式不发网络请求:没网时也能确认参数解析与路径规划是否正确
|
|
241
|
+
$Version = '<latest>'
|
|
242
|
+
Write-Info '[-DryRun] 会查询最新 Release 解析版本(此处不联网,用占位符继续)'
|
|
243
|
+
} else {
|
|
244
|
+
$Version = Resolve-LatestVersion -Api $api -RepoSlug $repoSlug -Base $Base
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
$version = $Version.TrimStart('v', 'V').Trim()
|
|
248
|
+
|
|
249
|
+
if ($version -eq '<latest>') {
|
|
250
|
+
$tag = 'v<latest>'
|
|
251
|
+
$tgz = "$AppName-<version>.tgz"
|
|
252
|
+
} else {
|
|
253
|
+
$tag = "v$version"
|
|
254
|
+
$tgz = "$AppName-$version.tgz"
|
|
255
|
+
Write-Info "使用版本:$tag"
|
|
256
|
+
}
|
|
257
|
+
$downloadUrl = "$Base/releases/download/$tag/$tgz"
|
|
258
|
+
|
|
259
|
+
# ── 幂等:已是目标版本就直接退出,连 PATH 都不碰 ─────────────────────────
|
|
260
|
+
$have = Get-InstalledVersion
|
|
261
|
+
if ($version -ne '<latest>') {
|
|
262
|
+
if ($have -eq $version -and -not $Force) {
|
|
263
|
+
Write-Ok "$AppName $version 已经装在 $Prefix,无需重装"
|
|
264
|
+
Write-Info '想强制重装:加 -Force;想升级到别的版本:-Version vX.Y.Z'
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
if ($have) { Write-Info "已装版本 $have,将覆盖安装为 $version" }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if ($DryRun) {
|
|
271
|
+
Write-Info "[-DryRun] 将下载:$downloadUrl"
|
|
272
|
+
Write-Info "[-DryRun] 将执行:npm install -g --prefix `"$Prefix`" <临时目录>\$tgz"
|
|
273
|
+
Register-Path -OnWindows $onWindows
|
|
274
|
+
Write-Info '[-DryRun] 到此为止,没有写任何文件'
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ── 版本存在性预检 ───────────────────────────────────────────────────────
|
|
279
|
+
# 拿不到 200 却在后面报「下载失败」,用户会以为是网络问题,实际是版本号写错了 ——
|
|
280
|
+
# 这两种情况的处理办法完全不同,所以先分开探测。
|
|
281
|
+
if ($version -ne '<latest>') {
|
|
282
|
+
$code = Get-HttpStatus -Url $downloadUrl
|
|
283
|
+
if ($code -eq 404) {
|
|
284
|
+
Fail @"
|
|
285
|
+
Release $tag 里没有 $tgz(HTTP 404)。
|
|
286
|
+
可能是版本号写错,或该 tag 还没走完发布流水线;-Version 指定的版本必须已经打过 tag。
|
|
287
|
+
核对可用版本:$Base/releases
|
|
288
|
+
"@
|
|
289
|
+
} elseif ($code -ne 200 -and $code -ne 302) {
|
|
290
|
+
Write-Warn "无法预先校验 $downloadUrl(HTTP $code),继续尝试下载"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# ── 下载 ─────────────────────────────────────────────────────────────────
|
|
295
|
+
# 临时目录集中放系统 temp,装完/失败一律删掉:不留半成品是硬要求
|
|
296
|
+
$script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("$AppName-install-" + [Guid]::NewGuid().ToString('N'))
|
|
297
|
+
New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null
|
|
298
|
+
$tgzPath = Join-Path $script:TempDir $tgz
|
|
299
|
+
|
|
300
|
+
Write-Info "下载 $downloadUrl"
|
|
301
|
+
try {
|
|
302
|
+
Invoke-WebRequest -Uri $downloadUrl -OutFile $tgzPath -UseBasicParsing -UserAgent $UserAgent -TimeoutSec 300
|
|
303
|
+
} catch {
|
|
304
|
+
Fail @"
|
|
305
|
+
下载失败:$downloadUrl
|
|
306
|
+
$($_.Exception.Message)
|
|
307
|
+
排查顺序:
|
|
308
|
+
1) 确认版本存在:$Base/releases
|
|
309
|
+
2) 国内网络换镜像:`$env:LLM_GATEWAY_INSTALL_BASE = 'https://your-mirror.example.com'
|
|
310
|
+
3) 代理环境确认 `$env:HTTPS_PROXY 已设置
|
|
311
|
+
"@
|
|
312
|
+
}
|
|
313
|
+
if (-not (Test-Path $tgzPath)) { Fail "下载后没有找到文件:$tgzPath" }
|
|
314
|
+
Write-Ok "已下载到 $tgzPath"
|
|
315
|
+
|
|
316
|
+
# ── 安装 ─────────────────────────────────────────────────────────────────
|
|
317
|
+
Write-Info "安装到 $Prefix"
|
|
318
|
+
# --prefix 指到用户家目录下的私有目录:全局 npm 目录在多数系统上需要管理员权限,
|
|
319
|
+
# 安装器不该悄悄要求提权,也不该往系统目录里写东西。
|
|
320
|
+
& $npmCmd.Source install -g --prefix $Prefix $tgzPath
|
|
321
|
+
if ($LASTEXITCODE -ne 0) {
|
|
322
|
+
Fail @"
|
|
323
|
+
npm 安装失败(临时文件已清理)。
|
|
324
|
+
手工复核:npm install -g --prefix "$Prefix" $tgz
|
|
325
|
+
常见原因:安装目录不可写、npm 镜像源不通(npm config get registry)、磁盘空间不足
|
|
326
|
+
"@
|
|
327
|
+
}
|
|
328
|
+
Write-Ok "已安装 $AppName $version → $Prefix"
|
|
329
|
+
|
|
330
|
+
# 垫片缺失说明安装没真正落地(例如 npm 把 bin 放到别处),早说比让用户敲命令没反应好
|
|
331
|
+
$shim = Join-Path $script:BinDir $(if ($onWindows) { "$MainCmd.cmd" } else { $MainCmd })
|
|
332
|
+
if (-not (Test-Path $shim)) { Write-Warn "没有找到 $shim,$MainCmd 可能不在预期位置" }
|
|
333
|
+
|
|
334
|
+
Register-Path -OnWindows $onWindows
|
|
335
|
+
|
|
336
|
+
Write-Host ''
|
|
337
|
+
Write-Ok '安装完成'
|
|
338
|
+
Write-Host @"
|
|
339
|
+
|
|
340
|
+
接下来:
|
|
341
|
+
1) 配密钥(不用手写 .env;三种给法任选):
|
|
342
|
+
起服务后在页面「设置」里填 —— 存 `$HOME\.llm-api-gateway-cli\credentials.json(仅本用户可读,不进 config.json)
|
|
343
|
+
或命令行:$MainCmd config set key sk-xxx
|
|
344
|
+
或环境变量:`$env:GATEWAY_KEY = 'sk-xxx' # 也认 SK / OPENAI_API_KEY / ANTHROPIC_API_KEY;也可以放 .env
|
|
345
|
+
没配之前 Web 服务照样能起(横幅会写「密钥 未配置」),只是发消息会被提示去配。
|
|
346
|
+
2) 自检(新开的终端里):
|
|
347
|
+
$MainCmd --help
|
|
348
|
+
$MainCmd -p "用一句话介绍你自己" # 命令行 agent 没有界面,缺密钥会当场报错并列出三种给法
|
|
349
|
+
3) 起本地 Web UI / 任务页(同一个服务、同一个端口):
|
|
350
|
+
gateway-web 或 gateway-task,然后打开 http://127.0.0.1:3100/task
|
|
351
|
+
4) 想改模型 / 模式 / 历史预算:
|
|
352
|
+
$MainCmd config list # 带「来源」列,一眼看出被哪个环境变量盖住了
|
|
353
|
+
|
|
354
|
+
卸载:Remove-Item -Recurse -Force "$Prefix",并删掉用户 PATH 里的那一项。
|
|
355
|
+
"@
|
|
356
|
+
} finally {
|
|
357
|
+
$ProgressPreference = $oldProgress
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
# 解析最新 tag;先用 GitHub API(文档 5.2 第 3 步),失败再退回镜像重定向
|
|
362
|
+
function Resolve-LatestVersion {
|
|
363
|
+
param([string]$Api, [string]$RepoSlug, [string]$Base)
|
|
364
|
+
|
|
365
|
+
$apiUrl = "$Api/repos/$RepoSlug/releases/latest"
|
|
366
|
+
Write-Info "未指定 -Version,查询最新 Release($apiUrl)"
|
|
367
|
+
try {
|
|
368
|
+
$resp = Invoke-RestMethod -Uri $apiUrl -UseBasicParsing -UserAgent $UserAgent -TimeoutSec 20
|
|
369
|
+
if ($resp -and $resp.tag_name) { return [string]$resp.tag_name }
|
|
370
|
+
} catch {
|
|
371
|
+
Write-Warn "GitHub API 查询失败:$($_.Exception.Message)"
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
# 国内直连 api.github.com 常被墙:GitHub 的 /releases/latest 是 302 到 /releases/tag/<tag>,
|
|
375
|
+
# 镜像只要保留这个重定向就还能解析出最新版本。
|
|
376
|
+
try {
|
|
377
|
+
$r = Invoke-WebRequest -Uri "$Base/releases/latest" -UseBasicParsing -UserAgent $UserAgent -TimeoutSec 20
|
|
378
|
+
$finalUri = $null
|
|
379
|
+
# 5.1 用 ResponseUri,7 里换成 RequestMessage.RequestUri:两个属性都探一下,
|
|
380
|
+
# 探不到就当作没有(没有 Set-StrictMode,取不存在的属性只会得到 $null)
|
|
381
|
+
try { $finalUri = $r.BaseResponse.ResponseUri } catch { }
|
|
382
|
+
if (-not $finalUri) { try { $finalUri = $r.BaseResponse.RequestMessage.RequestUri } catch { } }
|
|
383
|
+
if ("$finalUri" -match '/tag/(v?[0-9][^/?#]*)$') { return $Matches[1] }
|
|
384
|
+
} catch {
|
|
385
|
+
Write-Warn "镜像重定向解析失败:$($_.Exception.Message)"
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
Fail @"
|
|
389
|
+
无法解析最新版本(GitHub API 与镜像重定向都没成功)。
|
|
390
|
+
请显式指定版本重试:-Version v1.0.0
|
|
391
|
+
国内网络可换镜像:`$env:LLM_GATEWAY_INSTALL_BASE = '<你的镜像>/llm-api-gateway-cli'
|
|
392
|
+
"@
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
# 直接读全局包里的 package.json,而不是跑 `gateway-agent --version`:
|
|
396
|
+
# 后者在 PATH 里可能有另一个同名命令,那个版本不代表本次要装的前缀。
|
|
397
|
+
function Get-InstalledVersion {
|
|
398
|
+
if (-not (Test-Path $script:PkgJson)) { return '' }
|
|
399
|
+
try {
|
|
400
|
+
$json = Get-Content -Raw -Path $script:PkgJson -Encoding UTF8 | ConvertFrom-Json
|
|
401
|
+
if ($json.version) { return [string]$json.version }
|
|
402
|
+
} catch { }
|
|
403
|
+
return ''
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function Get-HttpStatus {
|
|
407
|
+
param([string]$Url)
|
|
408
|
+
try {
|
|
409
|
+
$null = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -UserAgent $UserAgent -TimeoutSec 20
|
|
410
|
+
return 200
|
|
411
|
+
} catch {
|
|
412
|
+
# 有的镜像/服务器不认 HEAD(405)或直接断开(0):交给调用方决定是否继续尝试下载
|
|
413
|
+
if ($_.Exception.Response) {
|
|
414
|
+
try { return [int]$_.Exception.Response.StatusCode } catch { return 0 }
|
|
415
|
+
}
|
|
416
|
+
return 0
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# ── PATH 注入 ────────────────────────────────────────────────────────────────
|
|
421
|
+
function Register-Path {
|
|
422
|
+
param([bool]$OnWindows)
|
|
423
|
+
|
|
424
|
+
$binDir = $script:BinDir
|
|
425
|
+
$manual = " 把 $binDir 加进 PATH"
|
|
426
|
+
|
|
427
|
+
if ($env:GITHUB_ACTIONS -eq 'true') {
|
|
428
|
+
# CI 里改用户 PATH 是白费力气:runner 的每个 step 都是新进程,
|
|
429
|
+
# 只有 $env:GITHUB_PATH 会被 GitHub 注入到后续 step 的 PATH 里。
|
|
430
|
+
if ($env:GITHUB_PATH) {
|
|
431
|
+
# 同一个 job 里重跑安装(升级 / -Force)不该把 PATH 越加越长
|
|
432
|
+
$already = $false
|
|
433
|
+
if (Test-Path $env:GITHUB_PATH) {
|
|
434
|
+
$already = @(Get-Content -Path $env:GITHUB_PATH -ErrorAction SilentlyContinue) -contains $binDir
|
|
435
|
+
}
|
|
436
|
+
if ($already) {
|
|
437
|
+
Write-Ok "`$env:GITHUB_PATH 里已有 $binDir,跳过 PATH 注入"
|
|
438
|
+
} elseif ($DryRun) {
|
|
439
|
+
Write-Info "[-DryRun] 会把 $binDir 追加到 `$env:GITHUB_PATH($env:GITHUB_PATH)"
|
|
440
|
+
} else {
|
|
441
|
+
# 用 .NET 追加:PS5.1 的 Add-Content -Encoding UTF8 会写 BOM,
|
|
442
|
+
# 文件首行的 BOM 会让 GitHub 认不出第一个路径
|
|
443
|
+
[System.IO.File]::AppendAllText($env:GITHUB_PATH, $binDir + [Environment]::NewLine)
|
|
444
|
+
Write-Ok "已把 $binDir 追加到 `$env:GITHUB_PATH"
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
Write-Warn '检测到 GITHUB_ACTIONS=true 但没有 $env:GITHUB_PATH,跳过 PATH 注入'
|
|
448
|
+
}
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if ($NoModifyPath) {
|
|
453
|
+
Write-Info '-NoModifyPath:未改 PATH。需要时手工处理:'
|
|
454
|
+
Write-Host $manual
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (-not $OnWindows) {
|
|
459
|
+
Write-Warn "当前不是 Windows,本脚本不改 *nix 的 shell 启动文件。请手工加一行:"
|
|
460
|
+
Write-Host " export PATH=`"$binDir`":`$PATH"
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
# 直接读写注册表原文而不是 [Environment]::GetEnvironmentVariable('Path','User'):
|
|
465
|
+
# 后者会把 %VAR% 展开,回写时若变成字面量就等于悄悄改坏了用户的 PATH。
|
|
466
|
+
# 同理只在原文后面追加一项、不重建整个值,其它项与原有 kind 都不动。
|
|
467
|
+
$rawUserPath = ''
|
|
468
|
+
$userPathItem = Get-ItemProperty -Path 'HKCU:\Environment' -Name 'Path' -ErrorAction SilentlyContinue
|
|
469
|
+
if ($userPathItem -and $userPathItem.PSObject.Properties['Path']) { $rawUserPath = [string]$userPathItem.Path }
|
|
470
|
+
|
|
471
|
+
$entries = @()
|
|
472
|
+
if ($rawUserPath) { $entries = $rawUserPath -split ';' | Where-Object { $_ -ne '' } }
|
|
473
|
+
$normalized = $entries | ForEach-Object { $_.Trim().TrimEnd('\', '/') }
|
|
474
|
+
if ($normalized -contains $binDir.TrimEnd('\', '/')) {
|
|
475
|
+
Write-Ok "用户 PATH 里已有 $binDir,跳过 PATH 注入"
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
# 机器级 PATH 里已有就不重复加(只读检查,不动机器级配置)
|
|
479
|
+
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
|
480
|
+
if ($machinePath) {
|
|
481
|
+
$machineNorm = $machinePath -split ';' | Where-Object { $_ -ne '' } | ForEach-Object { $_.Trim().TrimEnd('\', '/') }
|
|
482
|
+
if ($machineNorm -contains $binDir.TrimEnd('\', '/')) {
|
|
483
|
+
Write-Ok "系统 PATH 里已有 $binDir,跳过 PATH 注入"
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if ($DryRun) {
|
|
489
|
+
Write-Info "[-DryRun] 会向用户 PATH(HKCU:\Environment 的 Path)追加:$binDir"
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (-not (Test-Path 'HKCU:\Environment')) { New-Item -Path 'HKCU:\Environment' -Force | Out-Null }
|
|
494
|
+
if ($rawUserPath) {
|
|
495
|
+
$newUserPath = $rawUserPath.TrimEnd(';') + ';' + $binDir
|
|
496
|
+
} else {
|
|
497
|
+
$newUserPath = $binDir
|
|
498
|
+
}
|
|
499
|
+
# 两个分支都**显式**要 ExpandString,不依赖 Set-ItemProperty 回写时的 kind 默认行为:
|
|
500
|
+
# 用户的 User PATH 绝大多数是 REG_EXPAND_SZ,一旦被降级成 REG_SZ,里面的
|
|
501
|
+
# %USERPROFILE%\... 之类就不再展开 —— 等于安装器悄悄改坏了别人的 PATH,
|
|
502
|
+
# 事后极难归因。原本是 REG_SZ 的 PATH 变成 REG_EXPAND_SZ 无害且更好:
|
|
503
|
+
# %VAR% 会正常展开,未定义的变量 Windows 原样保留。
|
|
504
|
+
New-ItemProperty -Path 'HKCU:\Environment' -Name 'Path' -Value $newUserPath -PropertyType ExpandString -Force | Out-Null
|
|
505
|
+
# 当前会话也生效,省得用户为了跑一句 --help 还得先重开终端
|
|
506
|
+
$env:Path = "$binDir;$env:Path"
|
|
507
|
+
Write-Ok "已把 $binDir 追加到用户 PATH"
|
|
508
|
+
Write-Info '新开的终端才会自动带上;当前这个会话已经临时生效'
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# ── 入口 ─────────────────────────────────────────────────────────────────────
|
|
512
|
+
# 先执行、再在 catch 里落错误,最后统一收尾:这样 finally 一定跑(临时文件一定删),
|
|
513
|
+
# 退出码也不会被 finally 吞掉。
|
|
514
|
+
#
|
|
515
|
+
# 只有以文件方式运行(-File / powershell .\install.ps1)时才 exit:
|
|
516
|
+
# 文档推荐的 `irm ... | iex` 是把这段脚本跑在用户**当前会话**里,
|
|
517
|
+
# 在那里 exit 会直接把用户自己的 PowerShell 窗口关掉 —— 装不上是小事,
|
|
518
|
+
# 关掉用户窗口(可能还挂着别的活)是大事。
|
|
519
|
+
# 管道模式下没有退出码可传,所以至少把失败明确打出来。
|
|
520
|
+
$exitCode = 0
|
|
521
|
+
try {
|
|
522
|
+
Invoke-Setup
|
|
523
|
+
} catch {
|
|
524
|
+
Write-Host " [错误] $($_.Exception.Message)" -ForegroundColor Red
|
|
525
|
+
$exitCode = 1
|
|
526
|
+
} finally {
|
|
527
|
+
if ($script:TempDir -and (Test-Path $script:TempDir)) {
|
|
528
|
+
Remove-Item -Recurse -Force $script:TempDir -ErrorAction SilentlyContinue
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
$runFromFile = [bool]$MyInvocation.MyCommand.Path
|
|
533
|
+
if ($runFromFile) {
|
|
534
|
+
exit $exitCode
|
|
535
|
+
} elseif ($exitCode -ne 0) {
|
|
536
|
+
Write-Host ' [错误] 安装未完成(已保留你当前的会话,不执行 exit)' -ForegroundColor Red
|
|
537
|
+
}
|