################################################################################ # 闪电API - Hermes Agent 一键安装脚本 (Windows PowerShell) # # 功能: 安装 Hermes Agent (NousResearch) + 配置 闪电API (Claude + GPT) # 官网: flashapi.top # 模型调用: ai.flashapi.top (国内直连) / cdn.flashapi.top (海外CDN) # # 用法 (PowerShell): irm https://sh.flashapi.top/hermes.ps1 | iex # # 兼容 Windows PowerShell 5.1 与 PowerShell 7+;不需要管理员权限,所有操作在用户目录下完成 # # 写入位置: $env:HERMES_HOME\config.yaml (默认 %LOCALAPPDATA%\hermes\config.yaml) # - providers.flashapi-claude (Anthropic Messages, base_url 不带 /v1) # - providers.flashapi-gpt (OpenAI Responses, base_url 带 /v1) # - model.default / model.provider (默认模型) # - agent.reasoning_effort (默认推理强度) # 只做文本级的段内增改,保留其余配置与注释;写前自动备份。与 hermes.sh 行为一致。 ################################################################################ & { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch {} try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {} ############################################################################## # 常量定义 ############################################################################## $OnWindows = ([Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT) $UserHome = $env:USERPROFILE if (-not $UserHome) { $UserHome = $HOME } $LocalAppData = $env:LOCALAPPDATA if (-not $LocalAppData) { $LocalAppData = Join-Path (Join-Path $UserHome 'AppData') 'Local' } $HermesInstallPs1 = 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1' $ProviderClaude = 'flashapi-claude' $ProviderGpt = 'flashapi-gpt' $ProviderClaudeName = '闪电API Claude' $ProviderGptName = '闪电API GPT' $DefaultClaudeModel = 'claude-opus-5-5' $DefaultGptModel = 'gpt-6.1-sol' $DefaultEffort = 'medium' # 可变状态放在哈希表里 (子函数可直接修改,且不污染用户会话) $S = @{ BaseDir = '' Dir = '' ConfigFile = '' Profile = '' DoClaude = $false DoGpt = $false FamilyLabel = '' MergeError = '' } $Line = '───────────────────────────────────────────────────────' ############################################################################## # 工具函数 ############################################################################## # 输出带颜色的一行: Say '文本' '颜色' '文本' '颜色' ... (颜色为空 = 默认色) function Write-Parts { param([object[]]$Parts, [bool]$NoNewline) if ($null -ne $Parts) { for ($i = 0; $i -lt $Parts.Count; $i += 2) { $text = [string]$Parts[$i] $color = '' if ($i + 1 -lt $Parts.Count) { $color = [string]$Parts[$i + 1] } if ($color) { Write-Host $text -NoNewline -ForegroundColor $color } else { Write-Host $text -NoNewline } } } if (-not $NoNewline) { Write-Host '' } } function Say { Write-Parts -Parts $args -NoNewline $false } function SayN { Write-Parts -Parts $args -NoNewline $true } function Read-Line { # 读取一行输入 (与 bash 的 read -r 一样去掉首尾空白);输入流结束时返回 $null $v = Read-Host if ($null -eq $v) { return $null } return ([string]$v).Trim() } function Read-YesNo { param([string]$Prompt) SayN " $Prompt" 'Yellow' $a = Read-Line return ($a -eq 'y' -or $a -eq 'Y') } function Print-Banner { Say '' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say ' ' '' ' ██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗' 'DarkYellow' Say ' ' '' ' ██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝' 'DarkYellow' Say ' ' '' ' ███████║█████╗ ██████╔╝██╔████╔██║█████╗ ███████╗' 'DarkYellow' Say ' ' '' ' ██╔══██║██╔══╝ ██╔══██╗██║╚██╔╝██║██╔══╝ ╚════██║' 'DarkYellow' Say ' ' '' ' ██║ ██║███████╗██║ ██║██║ ╚═╝ ██║███████╗███████║' 'DarkYellow' Say ' ' '' ' ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝' 'DarkYellow' Say ' ' '' ' ⚡ 闪电API × Hermes Agent ⚡' 'Yellow' Say ' ' '' ' ⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡' 'Yellow' Say '' Say " $Line" 'DarkGray' Say ' ' '' '⚡ 闪电API' 'White' ' ' '' '|' 'DarkGray' ' ' '' '官网' 'Cyan' ' ' '' 'flashapi.top' 'Yellow' Say ' ' '' 'Hermes Agent 一键安装配置' 'White' ' ' '' '|' 'DarkGray' ' ' '' '让顶尖模型触手可及' 'Cyan' Say " $Line" 'DarkGray' Say ' ' '' '国内直连:' 'DarkGray' ' ' '' 'ai.flashapi.top' 'Green' ' ' '' '海外CDN:' 'DarkGray' ' ' '' 'cdn.flashapi.top' 'Green' Say " $Line" 'DarkGray' Say '' } function Print-Status { param([string]$Type, [string]$Message) switch ($Type) { 'ok' { Say ' ' '' '[⚡]' 'Green' " $Message" '' } 'info' { Say ' ' '' '[*]' 'Cyan' " $Message" '' } 'warn' { Say ' ' '' '[!]' 'Yellow' " $Message" '' } 'error' { Say ' ' '' '[x]' 'Red' " $Message" '' } default { Say " $Message" } } } # 读取密码输入 (星号遮盖) function Read-Secret { param([string]$Prompt) SayN $Prompt 'White' $sec = Read-Host -AsSecureString if ($null -eq $sec -or $sec.Length -eq 0) { return '' } $plain = '' $bstr = [IntPtr]::Zero try { $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } catch { $plain = (New-Object System.Management.Automation.PSCredential('x', $sec)).GetNetworkCredential().Password } finally { if ($bstr -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } } if ($null -eq $plain) { return '' } return $plain.Trim() } function Mask-Key { param([string]$Value) if (-not $Value) { return '' } if ($Value.Length -le 12) { return $Value } return $Value.Substring(0, 8) + '...' + $Value.Substring($Value.Length - 4) } function Backup-IfExists { param([string]$File) if (Test-Path -LiteralPath $File -PathType Leaf) { $backup = $File + '.bak.' + (Get-Date -Format 'yyyyMMddHHmmss') Copy-Item -LiteralPath $File -Destination $backup -Force Print-Status 'info' "已备份现有文件: $backup" } } function Ensure-Dir { param([string]$Dir) if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { New-Item -ItemType Directory -Path $Dir -Force | Out-Null } } # 写文件: UTF-8 无 BOM,LF 换行 function Write-Utf8File { param([string]$Path, [string]$Text) [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) } function Add-SessionPath { param([string]$Dir) if (-not (Test-Path -LiteralPath $Dir -PathType Container)) { return } $sep = [IO.Path]::PathSeparator $parts = @($env:PATH -split [regex]::Escape([string]$sep)) if ($parts -notcontains $Dir) { $env:PATH = $Dir + $sep + $env:PATH } } # YAML 双引号字符串 function ConvertTo-YamlDq { param([string]$Value) return '"' + $Value.Replace('\', '\\').Replace('"', '\"') + '"' } ############################################################################## # Hermes 目录 (与 hermes_constants.get_hermes_home 一致) # $env:HERMES_HOME > %LOCALAPPDATA%\hermes # 未设置 HERMES_HOME 且存在 active_profile (非 default) 时,写入 profiles\ ############################################################################## function Resolve-HermesHome { $S.Profile = '' if ($env:HERMES_HOME) { $S.BaseDir = $env:HERMES_HOME $S.Dir = $S.BaseDir } else { $S.BaseDir = Join-Path $LocalAppData 'hermes' $S.Dir = $S.BaseDir $ap = Join-Path $S.BaseDir 'active_profile' if (Test-Path -LiteralPath $ap -PathType Leaf) { $name = '' try { $name = ([string](Get-Content -LiteralPath $ap -TotalCount 1)).Trim() } catch { $name = '' } if ($name -and $name -ne 'default') { $pdir = Join-Path (Join-Path $S.BaseDir 'profiles') $name if (Test-Path -LiteralPath $pdir -PathType Container) { $S.Profile = $name $S.Dir = $pdir } } } } $S.ConfigFile = Join-Path $S.Dir 'config.yaml' } ############################################################################## # 功能 1: 安装 Hermes Agent ############################################################################## function Get-HermesExe { $c = Get-Command 'hermes' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $c) { return $c.Path } return $null } function Check-HermesInstalled { $exe = Get-HermesExe if (-not $exe) { return $null } $ErrorActionPreference = 'Continue' $out = $null try { $out = & $exe --version 2>$null | Select-Object -First 1 } catch { $out = $null } if ($out) { return ([string]$out).Trim() } return 'unknown' } function Refresh-HermesPath { Add-SessionPath (Join-Path $S.BaseDir 'bin') if ($OnWindows) { try { $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if ($userPath) { foreach ($p in ($userPath -split ';')) { if ($p) { Add-SessionPath $p } } } } catch {} } } function Install-Hermes { Say '' Print-Status 'info' '⚡ 正在安装 Hermes Agent ...' Say '' $ver = Check-HermesInstalled if ($ver) { Print-Status 'ok' "Hermes Agent 已安装 ($ver)" Say '' if (-not (Read-YesNo '是否重新安装/更新? (y/N): ')) { Print-Status 'info' '跳过安装'; return $true } } Print-Status 'info' '调用官方安装器 (install.ps1 -NonInteractive,自动准备 uv / Python / Git,无需 pip) ...' Print-Status 'info' '首次安装需要下载依赖,可能需要几分钟,请耐心等待' Say '' # 下载到临时文件 (带 BOM 保存,避免 5.1 按 ANSI 解析),在独立的 PowerShell 进程中运行, # 安装器内部的 exit 不会关闭当前窗口 $tmp = Join-Path ([IO.Path]::GetTempPath()) ('hermes-install-' + [guid]::NewGuid().ToString('N') + '.ps1') try { $resp = Invoke-WebRequest -Uri $HermesInstallPs1 -UseBasicParsing -TimeoutSec 60 [IO.File]::WriteAllText($tmp, [string]$resp.Content, (New-Object Text.UTF8Encoding $true)) } catch { Print-Status 'error' "无法下载官方安装器: $HermesInstallPs1" Print-Status 'info' '请检查网络 (需要能访问 raw.githubusercontent.com 与 github.com)' return $false } $code = 1 try { $psExe = (Get-Process -Id $PID).Path if (-not $psExe) { $psExe = 'powershell.exe' } & $psExe -NoProfile -ExecutionPolicy Bypass -File $tmp -NonInteractive | Out-Host $code = $LASTEXITCODE } catch { Print-Status 'error' "运行安装器出错: $($_.Exception.Message)" $code = 1 } finally { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } Say '' Refresh-HermesPath if ($code -ne 0) { Print-Status 'error' "官方安装器执行失败 (退出码 $code)" Print-Status 'info' '可稍后重试,或手动执行:' Say " irm $HermesInstallPs1 | iex" 'Cyan' return $false } $ver = Check-HermesInstalled if ($ver) { Print-Status 'ok' "⚡ Hermes Agent 安装成功 ($ver)" } else { Print-Status 'warn' '安装已完成,但未在 PATH 中找到 hermes' Print-Status 'info' '请重新打开终端后再试' } return $true } ############################################################################## # YAML 文本工具 (与 hermes.sh 中的 awk 实现逐行等价) ############################################################################## function Get-Indent { param([string]$s) return ($s.Length - $s.TrimStart(' ').Length) } function Get-Unquoted { param([string]$v) $v = $v.Trim(" `t".ToCharArray()) if ($v.Length -ge 2 -and (($v.StartsWith('"') -and $v.EndsWith('"')) -or ($v.StartsWith("'") -and $v.EndsWith("'")))) { $v = $v.Substring(1, $v.Length - 2) } return $v } function Get-YamlKey { param([string]$s) if ($s.StartsWith('#') -or $s.StartsWith('-')) { return '' } $m = [regex]::Match($s, ':([ \t]|$)') if (-not $m.Success) { return '' } return (Get-Unquoted $s.Substring(0, $m.Index)) } function Get-YamlRest { param([string]$s) $m = [regex]::Match($s, ':([ \t]|$)') if (-not $m.Success) { return '' } $r = $s.Substring($m.Index + 1).Trim(" `t".ToCharArray()) if ($r.StartsWith('#')) { $r = '' } $r = [regex]::Replace($r, '[ \t]+#.*$', '') return $r } function Test-TopLine { param([string]$s) return ($s -cmatch '^[^ \t#-]') } function Test-BlankLine { param([string]$s) return ($s -cmatch '^[ \t]*$') } function Test-CommentLine { param([string]$s) return ($s -cmatch '^[ \t]*#') } function Test-EmptyFlow { param([string]$r) return ($r -eq '' -or $r -ceq '{}' -or $r -ceq 'null' -or $r -ceq '~' -or $r -ceq 'Null' -or $r -ceq 'NULL') } function Get-Pad { param([int]$n) return (' ' * $n) } # 读取 YAML 文件为行数组 (去 BOM、去 CR,末尾换行不产生空行) function Read-YamlLines { param([string]$Path) $list = New-Object System.Collections.Generic.List[string] if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return ,$list } $text = [IO.File]::ReadAllText($Path, (New-Object Text.UTF8Encoding $false)) if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) } if ($text.Length -eq 0) { return ,$list } $parts = $text.Split("`n") $n = $parts.Length if ($text.EndsWith("`n")) { $n = $n - 1 } for ($i = 0; $i -lt $n; $i++) { $l = $parts[$i] if ($l.EndsWith("`r")) { $l = $l.Substring(0, $l.Length - 1) } $list.Add($l) } return ,$list } # 读取 providers.. (取最后一个 providers: 段) function Read-ProviderField { param([string]$Name, [string]$Field) $lines = Read-YamlLines $S.ConfigFile $insec = $false; $ci = 0; $inent = $false; $ei = 0; $val = '' foreach ($line in $lines) { if (Test-TopLine $line) { $k = Get-YamlKey $line if ($k -ne '') { $insec = ($k -eq 'providers'); $ci = 0; $inent = $false } if ($insec) { $val = '' } continue } if (-not $insec) { continue } if ((Test-BlankLine $line) -or (Test-CommentLine $line)) { continue } $i = Get-Indent $line if ($ci -eq 0) { $ci = $i } if ($i -eq $ci) { $inent = ((Get-YamlKey $line.Substring($ci)) -eq $Name); $ei = 0; continue } if ($inent -and $i -gt $ci) { if ($ei -eq 0) { $ei = $i } if ($i -eq $ei -and (Get-YamlKey $line.Substring($ei)) -eq $Field) { $v = $line.Substring($ei) $m = [regex]::Match($v, ':([ \t]|$)') $v = $v.Substring($m.Index + 1) $v = [regex]::Replace($v, '[ \t]+#.*$', '') $val = Get-Unquoted $v } } } return $val } # 读取 model. function Read-ModelField { param([string]$Field) $lines = Read-YamlLines $S.ConfigFile $insec = $false; $ci = 0; $val = '' foreach ($line in $lines) { if (Test-TopLine $line) { $k = Get-YamlKey $line if ($k -ne '') { $insec = ($k -eq 'model'); $ci = 0; if ($insec) { $val = '' } } continue } if (-not $insec) { continue } if ((Test-BlankLine $line) -or (Test-CommentLine $line)) { continue } $i = Get-Indent $line if ($ci -eq 0) { $ci = $i } if ($i -eq $ci -and (Get-YamlKey $line.Substring($ci)) -eq $Field) { $v = $line.Substring($ci) $m = [regex]::Match($v, ':([ \t]|$)') $v = $v.Substring($m.Index + 1) $v = [regex]::Replace($v, '[ \t]+#.*$', '') $val = Get-Unquoted $v } } return $val } # 文本级合并 config.yaml (算法与 hermes.sh 的 yaml_merge 相同) # model: 设置 default / provider,移除 base_url/api_key/api_mode/key_env/api_key_env # providers: 删除同名 flashapi-* 条目后写入新条目,其余条目保留 # agent: 设置 reasoning_effort # 只处理最后一次出现的同名顶层段。返回合并后的文本;失败返回 $null 并设置 $S.MergeError function Merge-HermesYaml { param( [System.Collections.Generic.List[string]]$Lines, [string]$ModelDefault, [string]$ModelProvider, [string]$Effort, [string[]]$ProviderBlock, [string[]]$RemoveKeys ) $managed = @('model', 'providers', 'agent') $drop = @('base_url', 'api_key', 'api_mode', 'key_env', 'api_key_env') $cnt = @{ model = 0; providers = 0; agent = 0 } $hdr = @{ model = -1; providers = -1; agent = -1 } $inl = @{ model = ''; providers = ''; agent = '' } $ci = @{ model = 0; providers = 0; agent = 0 } $fdef = $false; $fprov = $false; $feff = $false # ---------- 第 1 遍 ---------- $top = '' for ($n = 0; $n -lt $Lines.Count; $n++) { $line = $Lines[$n] if (Test-TopLine $line) { $k = Get-YamlKey $line if ($k -ne '') { $top = $k if ($managed -contains $k) { $cnt[$k] = $cnt[$k] + 1; $hdr[$k] = $n; $inl[$k] = Get-YamlRest $line; $ci[$k] = 0 if ($k -eq 'model') { $fdef = $false; $fprov = $false } if ($k -eq 'agent') { $feff = $false } } } continue } if (-not ($managed -contains $top)) { continue } if ((Test-BlankLine $line) -or (Test-CommentLine $line)) { continue } if ($ci[$top] -eq 0) { $ci[$top] = Get-Indent $line } if ((Get-Indent $line) -eq $ci[$top]) { $ck = Get-YamlKey $line.Substring($ci[$top]) if ($top -eq 'model' -and ($ck -eq 'default' -or $ck -eq 'model')) { $fdef = $true } if ($top -eq 'model' -and $ck -eq 'provider') { $fprov = $true } if ($top -eq 'agent' -and $ck -eq 'reasoning_effort') { $feff = $true } } } foreach ($k in @('providers', 'agent')) { if ($cnt[$k] -gt 0 -and -not (Test-EmptyFlow $inl[$k])) { $S.MergeError = $k; return $null } } if ($cnt['model'] -gt 0 -and ($inl['model'] -cmatch '^[{\[]') -and -not (Test-EmptyFlow $inl['model'])) { $S.MergeError = 'model'; return $null } # ---------- 第 2 遍 ---------- $out = New-Object System.Collections.Generic.List[string] $sec = ''; $c = 2 $skipping = $false; $skipind = 0 $buf = New-Object System.Collections.Generic.List[string] $doneDef = $false; $doneProv = $false; $doneEff = $false for ($n = 0; $n -lt $Lines.Count; $n++) { $line = $Lines[$n] if ((Test-TopLine $line) -and (Get-YamlKey $line) -ne '') { if ($skipping) { $skipping = $false; foreach ($b in $buf) { $out.Add($b) }; $buf.Clear() } $k = Get-YamlKey $line $sec = '' if (($managed -contains $k) -and $n -eq $hdr[$k]) { $sec = $k } $c = 2 if ($sec -ne '' -and $ci[$sec] -gt 0) { $c = $ci[$sec] } if ($sec -eq 'model') { $doneDef = $false; $doneProv = $false if (-not (Test-EmptyFlow $inl['model'])) { $out.Add('model:') $out.Add((Get-Pad 2) + 'default: ' + $ModelDefault) $out.Add((Get-Pad 2) + 'provider: ' + $ModelProvider) $doneDef = $true; $doneProv = $true continue } $out.Add('model:') if (-not $fdef) { $out.Add((Get-Pad $c) + 'default: ' + $ModelDefault); $doneDef = $true } if (-not $fprov) { $out.Add((Get-Pad $c) + 'provider: ' + $ModelProvider); $doneProv = $true } continue } if ($sec -eq 'providers') { $out.Add('providers:') foreach ($p in $ProviderBlock) { if ($p -ne '') { $out.Add((Get-Pad $c) + $p) } } continue } if ($sec -eq 'agent') { $doneEff = $false $out.Add('agent:') if (-not $feff) { $out.Add((Get-Pad $c) + 'reasoning_effort: ' + $Effort); $doneEff = $true } continue } $out.Add($line) continue } if ($skipping) { if (Test-BlankLine $line) { $buf.Add($line); continue } $li = Get-Indent $line if ($li -gt $skipind -or ($li -eq $skipind -and $line.Substring($skipind).StartsWith('-'))) { $buf.Clear(); continue } $skipping = $false; foreach ($b in $buf) { $out.Add($b) }; $buf.Clear() } if ($sec -ne '' -and -not (Test-CommentLine $line) -and -not (Test-BlankLine $line) -and (Get-Indent $line) -eq $c) { $ck = Get-YamlKey $line.Substring($c) if ($sec -eq 'model') { if ($ck -eq 'default' -or $ck -eq 'model') { if (-not $doneDef) { $out.Add((Get-Pad $c) + 'default: ' + $ModelDefault); $doneDef = $true } $skipping = $true; $skipind = $c; $buf.Clear(); continue } if ($ck -eq 'provider') { if (-not $doneProv) { $out.Add((Get-Pad $c) + 'provider: ' + $ModelProvider); $doneProv = $true } $skipping = $true; $skipind = $c; $buf.Clear(); continue } if ($drop -contains $ck) { $skipping = $true; $skipind = $c; $buf.Clear(); continue } } elseif ($sec -eq 'providers') { if ($RemoveKeys -contains $ck) { $skipping = $true; $skipind = $c; $buf.Clear(); continue } } elseif ($sec -eq 'agent') { if ($ck -eq 'reasoning_effort') { if (-not $doneEff) { $out.Add((Get-Pad $c) + 'reasoning_effort: ' + $Effort); $doneEff = $true } $skipping = $true; $skipind = $c; $buf.Clear(); continue } } } $out.Add($line) } if ($skipping) { foreach ($b in $buf) { $out.Add($b) } } if ($cnt['model'] -eq 0) { if ($Lines.Count -gt 0) { $out.Add('') } $out.Add('model:') $out.Add(' default: ' + $ModelDefault) $out.Add(' provider: ' + $ModelProvider) } if ($cnt['providers'] -eq 0) { $out.Add(''); $out.Add('providers:') foreach ($p in $ProviderBlock) { if ($p -ne '') { $out.Add(' ' + $p) } } } if ($cnt['agent'] -eq 0) { $out.Add(''); $out.Add('agent:'); $out.Add(' reasoning_effort: ' + $Effort) } return (($out -join "`n") + "`n") } ############################################################################## # 功能 2: 配置 API ############################################################################## function Get-ContextLength { param([string]$Id) $big = @('claude-opus-5-5', 'claude-sonnet-5-5', 'claude-fable-5-1', 'claude-fable-5', 'claude-opus-5', 'claude-sonnet-5', 'claude-opus-4-8') $bigGpt = @('gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-astra', 'gpt-6-luna') if ($big -contains $Id) { return 1000000 } if ($Id.StartsWith('claude-')) { return 200000 } if ($bigGpt -contains $Id) { return 1050000 } return 272000 } # 用 /v1/models 验证 Key;返回 @{ Code = 状态码(0=网络错误); Ids = 模型ID数组 } function Fetch-Models { param([string]$BaseUrl, [string]$ApiKey) $result = @{ Code = 0; Ids = @() } try { $r = Invoke-WebRequest -Uri ($BaseUrl + '/v1/models') -Headers @{ Authorization = "Bearer $ApiKey" } -Method Get -UseBasicParsing -TimeoutSec 15 $result.Code = [int]$r.StatusCode $ids = New-Object System.Collections.Generic.List[string] try { $j = [string]$r.Content | ConvertFrom-Json $data = $null if ($null -ne $j -and $null -ne $j.PSObject.Properties['data']) { $data = $j.data } foreach ($m in @($data)) { if ($null -ne $m -and $null -ne $m.PSObject.Properties['id'] -and $m.id) { $ids.Add([string]$m.id) } } } catch {} $result.Ids = $ids.ToArray() } catch { $ex = $_.Exception $resp = $null if ($null -ne $ex -and $null -ne $ex.PSObject.Properties['Response']) { $resp = $ex.Response } if ($null -ne $resp) { try { $result.Code = [int]$resp.StatusCode } catch { $result.Code = 0 } } } return $result } function Select-FamilyIds { param([string]$Family, [string[]]$Ids) $out = New-Object System.Collections.Generic.List[string] foreach ($id in $Ids) { if (-not $id) { continue } if ($Family -eq 'claude') { if (-not $id.StartsWith('claude-')) { continue } } else { if (-not $id.StartsWith('gpt-')) { continue } if ($id.Contains('compact')) { continue } } if (-not $out.Contains($id)) { $out.Add($id) } } return ,$out.ToArray() } # 验证一个 Key;返回 @{ Ok = 是否继续; Ids = 家族模型ID数组 } function Validate-Key { param([string]$Family, [string]$BaseUrl, [string]$ApiKey) $label = 'Claude' if ($Family -eq 'gpt') { $label = 'GPT' } Print-Status 'info' "正在验证 $label 分组 API Key ..." $r = Fetch-Models $BaseUrl $ApiKey $ids = @() if ($r.Code -eq 200) { $ids = Select-FamilyIds $Family $r.Ids if ($ids.Count -gt 0) { Print-Status 'ok' "⚡ $label Key 验证通过,获取到 $($ids.Count) 个 $label 模型" } else { Print-Status 'warn' "Key 有效,但模型列表里没有 $label 模型 —— 这个 Key 可能不是 $label 分组" Print-Status 'info' '请到 flashapi.top 确认该令牌所属分组,将只写入默认模型' } } elseif ($r.Code -eq 401) { Print-Status 'error' "$label API Key 无效 (401 Unauthorized)" if (-not (Read-YesNo '验证未通过,是否仍要保存? (y/N): ')) { Print-Status 'info' '已取消配置'; return @{ Ok = $false; Ids = @() } } } elseif ($r.Code -eq 0) { Print-Status 'warn' "无法连接到 $BaseUrl (网络错误)" if (-not (Read-YesNo '验证未通过,是否仍要保存? (y/N): ')) { Print-Status 'info' '已取消配置'; return @{ Ok = $false; Ids = @() } } } else { Print-Status 'warn' "API 返回状态码: $($r.Code) (可能仍可用),将只写入默认模型" } return @{ Ok = $true; Ids = $ids } } # 生成一个 provider 条目 (相对缩进的行数组) function New-ProviderEntry { param([string]$Key, [string]$Name, [string]$Url, [string]$Mode, [string]$ApiKey, [string]$Default, [string[]]$Ids) $list = New-Object System.Collections.Generic.List[string] $list.Add($Default) foreach ($id in $Ids) { if ($id -and -not $list.Contains($id)) { $list.Add($id) } } $lines = New-Object System.Collections.Generic.List[string] $lines.Add("${Key}:") $lines.Add(' name: ' + (ConvertTo-YamlDq $Name)) $lines.Add(' base_url: ' + (ConvertTo-YamlDq $Url)) $lines.Add(' api_mode: ' + $Mode) $lines.Add(' api_key: ' + (ConvertTo-YamlDq $ApiKey)) $lines.Add(' default_model: ' + (ConvertTo-YamlDq $Default)) $lines.Add(' models:') foreach ($id in $list) { $lines.Add(' ' + (ConvertTo-YamlDq $id) + ':') $lines.Add(' context_length: ' + (Get-ContextLength $id)) $lines.Add(' supports_vision: true') } return ,$lines.ToArray() } function Write-HermesConfig { param([string]$BaseUrl, [string]$ClaudeKey, [string[]]$ClaudeIds, [string]$GptKey, [string[]]$GptIds, [string]$DefaultProvider, [string]$DefaultModel) Ensure-Dir $S.Dir $block = New-Object System.Collections.Generic.List[string] $rm = New-Object System.Collections.Generic.List[string] if ($S.DoClaude) { foreach ($l in (New-ProviderEntry $ProviderClaude $ProviderClaudeName $BaseUrl 'anthropic_messages' $ClaudeKey $DefaultClaudeModel $ClaudeIds)) { $block.Add($l) } $rm.Add($ProviderClaude) } if ($S.DoGpt) { foreach ($l in (New-ProviderEntry $ProviderGpt $ProviderGptName ($BaseUrl + '/v1') 'codex_responses' $GptKey $DefaultGptModel $GptIds)) { $block.Add($l) } $rm.Add($ProviderGpt) } $lines = Read-YamlLines $S.ConfigFile $S.MergeError = '' $text = Merge-HermesYaml -Lines $lines -ModelDefault (ConvertTo-YamlDq $DefaultModel) -ModelProvider (ConvertTo-YamlDq $DefaultProvider) ` -Effort (ConvertTo-YamlDq $DefaultEffort) -ProviderBlock $block.ToArray() -RemoveKeys $rm.ToArray() if ($null -eq $text) { Print-Status 'error' 'config.yaml 中 model/providers/agent 段使用了单行 {...} 写法,无法无损合并' Print-Status 'info' '请把该段改成多行缩进写法后重试,原文件未被修改' return $false } Backup-IfExists $S.ConfigFile $tmp = $S.ConfigFile + '.tmp' Write-Utf8File $tmp $text Move-Item -LiteralPath $tmp -Destination $S.ConfigFile -Force return $true } function Choose-Families { Say ' ' '' '请选择要配置的模型家族:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' Claude + GPT ' '' '(回车默认)' 'DarkGray' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' 仅 Claude' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '3)' 'Cyan' ' 仅 GPT' '' Say '' SayN ' ' '' '请输入选项 [回车=1]: ' 'White' $choice = Read-Line switch ($choice) { { $_ -eq '' -or $_ -eq '1' } { $S.DoClaude = $true; $S.DoGpt = $true; $S.FamilyLabel = 'Claude + GPT'; break } '2' { $S.DoClaude = $true; $S.DoGpt = $false; $S.FamilyLabel = '仅 Claude'; break } '3' { $S.DoClaude = $false; $S.DoGpt = $true; $S.FamilyLabel = '仅 GPT'; break } default { Print-Status 'error' '无效选项,已取消'; return $false } } Say '' Print-Status 'ok' "已选择: $($S.FamilyLabel)" return $true } function Configure-Api { Say '' Print-Status 'info' '⚡ 配置 闪电API (Claude + GPT)' Say '' if ($S.Profile) { Print-Status 'info' "检测到当前 Hermes profile: $($S.Profile),将写入 $($S.ConfigFile)" } $existing = Read-ProviderField $ProviderClaude 'api_key' if ($existing) { Print-Status 'info' "当前 Claude Key: $(Mask-Key $existing)" } $existing = Read-ProviderField $ProviderGpt 'api_key' if ($existing) { Print-Status 'info' "当前 GPT Key: $(Mask-Key $existing)" } $existing = Read-ProviderField $ProviderClaude 'base_url' if (-not $existing) { $existing = Read-ProviderField $ProviderGpt 'base_url' } if ($existing) { Print-Status 'info' "当前 BASE URL: $existing" } Say '' $S.DoClaude = $false $S.DoGpt = $false $S.FamilyLabel = '' if (-not (Choose-Families)) { return $false } Say '' # ── 选择线路 ── Say ' ' '' '请选择 API 线路:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' ' '' '国内直连' 'Green' ' - https://ai.flashapi.top' '' Say ' ' '' '服务器位于香港,国内可直连,无需翻墙' 'DarkGray' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' ' '' '海外CDN' 'Green' ' - https://cdn.flashapi.top' '' Say ' ' '' '全球CDN加速,适合海外用户' 'DarkGray' Say '' SayN ' ' '' '请输入选项 (1/2): ' 'White' $choice = Read-Line $baseUrl = '' if ($choice -eq '1') { $baseUrl = 'https://ai.flashapi.top' } elseif ($choice -eq '2') { $baseUrl = 'https://cdn.flashapi.top' } else { Print-Status 'error' '无效选项,已取消'; return $false } Say '' Print-Status 'info' "已选择: $baseUrl" Say '' # ── 令牌分组提醒 ── Say ' ' '' '╔══════════════════════════════════════════════════════╗' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '⚡ 重要提示' 'White' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '请先在官网' 'DarkGray' ' ' '' 'flashapi.top' 'Yellow' ' ' '' '创建' 'DarkGray' ' ' '' '令牌分组' 'Green' ' ' '' '║' 'Yellow' Say ' ' '' '║' 'Yellow' ' ' '' '未创建令牌分组将无法获取模型列表!' 'DarkGray' ' ' '' '║' 'Yellow' Say ' ' '' '╚══════════════════════════════════════════════════════╝' 'Yellow' Say '' # ── 输入 API Key (密码遮盖) ── $claudeKey = '' $gptKey = '' if ($S.DoClaude) { $claudeKey = Read-Secret ' 请输入 Claude 分组 API Key: ' if (-not $claudeKey) { Print-Status 'error' 'API Key 不能为空,已取消'; return $false } } if ($S.DoGpt) { if ($S.DoClaude) { $gptKey = Read-Secret ' 请输入 GPT 分组 API Key (回车 = 与 Claude Key 相同): ' if (-not $gptKey) { $gptKey = $claudeKey } } else { $gptKey = Read-Secret ' 请输入 GPT 分组 API Key: ' } if (-not $gptKey) { Print-Status 'error' 'API Key 不能为空,已取消'; return $false } } Say '' # ── 验证 ── $claudeIds = @() $gptIds = @() if ($S.DoClaude) { $v = Validate-Key 'claude' $baseUrl $claudeKey if (-not $v.Ok) { return $false } $claudeIds = $v.Ids } if ($S.DoGpt) { $v = Validate-Key 'gpt' $baseUrl $gptKey if (-not $v.Ok) { return $false } $gptIds = $v.Ids } Say '' $defaultProvider = $ProviderGpt $defaultModel = $DefaultGptModel if ($S.DoClaude) { $defaultProvider = $ProviderClaude; $defaultModel = $DefaultClaudeModel } # ── 写入 Hermes 配置 ── if (-not (Write-HermesConfig $baseUrl $claudeKey $claudeIds $gptKey $gptIds $defaultProvider $defaultModel)) { return $false } Say '' if ($S.DoClaude) { Print-Status 'ok' "Claude Key 已保存: $(Mask-Key $claudeKey) (供应商 $ProviderClaude)" Print-Status 'ok' "Claude BASE URL: $baseUrl (anthropic_messages)" } if ($S.DoGpt) { Print-Status 'ok' "GPT Key 已保存: $(Mask-Key $gptKey) (供应商 $ProviderGpt)" Print-Status 'ok' "GPT BASE URL: $baseUrl/v1 (codex_responses / Responses API)" } Print-Status 'ok' "默认模型: $defaultModel (provider: $defaultProvider)" Print-Status 'ok' "推理强度: $DefaultEffort (agent.reasoning_effort)" Print-Status 'ok' "配置文件已写入: $($S.ConfigFile)" Say '' Say ' ' '' '使用方法:' 'White' Say ' ' '' '启动:' 'Cyan' ' hermes' '' Say ' ' '' '快速验证:' 'Cyan' ' hermes -z "Reply with exactly: OK"' '' if ($S.DoGpt) { Say ' ' '' '用 GPT:' 'Cyan' " hermes --provider $ProviderGpt -m $DefaultGptModel" '' } if ($S.DoClaude) { Say ' ' '' '用 Claude:' 'Cyan' " hermes --provider $ProviderClaude -m $DefaultClaudeModel" '' } if ($S.DoGpt -and $S.DoClaude) { Say ' ' '' '会话中切换:' 'Cyan' " /model $DefaultGptModel --provider $ProviderGpt " '' '(加 --global 设为默认)' 'DarkGray' } Say ' ' '' '选择默认模型:' 'Cyan' ' hermes model ' '' '(交互式选择供应商与模型)' 'DarkGray' Say ' ' '' '推理强度:' 'Cyan' ' 会话中 /reasoning high,或 hermes config set agent.reasoning_effort high' '' if (-not (Get-HermesExe)) { Say '' Print-Status 'warn' '尚未检测到 hermes 命令,请先执行菜单 1 安装 (配置已写好,安装后直接可用)' } if ($S.DoClaude) { Say '' Print-Status 'info' '提示: 若 Claude 请求报错 "Requests from the ... framework are not supported",' Say ' ' '' '说明该 Claude Key 所在分组仅限官方客户端;请到 flashapi.top 在允许第三方工具的分组下' 'DarkGray' Say ' ' '' '新建令牌,然后重新运行本脚本选择「2) 配置 闪电API」' 'DarkGray' } return $true } ############################################################################## # 状态检测与菜单 ############################################################################## function Show-StatusSummary { Say ' ' '' '当前状态:' 'White' $osLabel = 'windows' if (-not $OnWindows) { $osLabel = [string][Environment]::OSVersion.Platform } Say ' 系统环境: ' '' $osLabel 'Cyan' $ver = Check-HermesInstalled if ($ver) { Say ' Hermes: ' '' '已安装' 'Green' " ($ver)" '' } else { Say ' Hermes: ' '' '未安装' 'Red' } $key = Read-ProviderField $ProviderClaude 'api_key' if ($key) { Say ' Claude: ' '' '已配置' 'Green' " ($(Mask-Key $key))" '' } else { Say ' Claude: ' '' '未配置' 'Red' } $key = Read-ProviderField $ProviderGpt 'api_key' if ($key) { Say ' GPT: ' '' '已配置' 'Green' " ($(Mask-Key $key))" '' } else { Say ' GPT: ' '' '未配置' 'Red' } $url = Read-ProviderField $ProviderClaude 'base_url' if (-not $url) { $url = Read-ProviderField $ProviderGpt 'base_url' } if ($url) { Say ' API 线路: ' '' $url 'Green' } $model = Read-ModelField 'default' if ($model) { $prov = Read-ModelField 'provider' if (-not $prov) { $prov = 'auto' } Say ' 默认模型: ' '' $model 'Cyan' ' ' '' "($prov)" 'DarkGray' } if ($S.Profile) { Say ' Profile: ' '' $S.Profile 'Cyan' } Say ' Hermes 配置: ' '' $S.ConfigFile 'Cyan' Say '' } function Show-Menu { Say ' ' '' '请选择操作:' 'White' Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '1)' 'Cyan' ' 安装 Hermes Agent' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '2)' 'Cyan' ' 配置 闪电API (Claude + GPT)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '3)' 'Cyan' ' 一键全部安装 (安装 + 配置)' '' Say ' ' '' '⚡' 'Yellow' ' ' '' '0)' 'Cyan' ' 退出' '' Say '' SayN ' ' '' '请输入选项: ' 'White' } function Invoke-Safely { param([scriptblock]$Block) try { return (& $Block) } catch { Print-Status 'error' "出错: $($_.Exception.Message)"; return $false } } ############################################################################## # 主流程 ############################################################################## function Main { Resolve-HermesHome Refresh-HermesPath Print-Banner Show-StatusSummary while ($true) { Show-Menu $choice = Read-Line if ($null -eq $choice) { Say ''; return } switch ($choice) { '1' { Invoke-Safely { Install-Hermes } | Out-Null Say '' } '2' { Invoke-Safely { Configure-Api } | Out-Null Say '' } '3' { $installed = Invoke-Safely { Install-Hermes } if ($installed -eq $true) { Say '' Say " $Line" 'DarkGray' $configured = Invoke-Safely { Configure-Api } if ($configured -eq $true) { Say '' Say " $Line" 'DarkGray' Say '' Print-Status 'ok' '⚡ 全部完成! 请重新打开终端后运行: hermes' } } Say '' } '0' { Say '' Say ' ' '' '⚡' 'Yellow' ' ' '' '感谢使用闪电API,祝你Vibe Coding愉快!' 'White' Say ' ' '' '官网: flashapi.top' 'DarkGray' Say '' return } default { Print-Status 'error' '无效选项,请重新选择' Say '' } } } } Main }