函数/New-GPT图像.ps1
|
function New-GPT图像 { <# .SYNOPSIS 调用 gpt-image 模型生成图像。 .DESCRIPTION 通过 OpenAI 兼容接口生成图像,自动处理 b64_json / url 两种返回形式。 密钥、基础地址与模型以 DPAPI 加密记住。 有参考图时走 images/edits 端点,否则走 images/generations。 .PARAMETER 提示词 图像描述。与 -提示文件 二选一。 .PARAMETER 提示文件 提示词文本文件路径。与 -提示词 二选一。 .PARAMETER 密钥 交互式输入密钥开关。指定此开关时会提示输入。 .PARAMETER 密钥值 API 令牌明文。未指定时查找已记住的值。 .PARAMETER 基础地址 Base URL。未指定时查找已记住的值。 .PARAMETER 模型 模型名。未指定时查找已记住的值。 .PARAMETER 尺寸 自定义 WxH 像素(auto 为默认)。需满足:最长边 ≤3840、宽高均为 16 的倍数、长宽比 ≤3:1、总像素 655360~8294400。默认 auto。 .PARAMETER 质量 low | medium | high | xhigh | max | auto。默认 auto。 .PARAMETER 输出路径 输出文件路径。默认时间戳 PNG。 .PARAMETER 超时秒数 超时时间,默认 500。 .PARAMETER 参考图 参考图像路径或 URL(可多张)。官方限制(OpenAI images/edits,本函数不做本地拦截): GPT 图像模型最多 16 张,每张为小于 50MB 的 png / webp / jpg;dall-e-2 仅支持单张 ≤4MB 方形 png。 同时指定 -蒙版 时,蒙版仅应用于第一张参考图。 .PARAMETER 蒙版 蒙版(mask)图像路径,用于对参考图做局部重绘(inpainting),仅在有参考图时生效; 提供多张参考图时,蒙版仅应用于第一张。 语义:透明(alpha=0)区域 = 要重绘的地方;不透明区域 = 保持原样不动。 要求:1) 必须是 PNG;2) 宽高与参考图像素级一致(不会自动缩放对齐);3) RGBA 带 alpha 通道。 提示词只描述透明区域中想要的内容,其余部分模型严格保留参考图原样。 .EXAMPLE New-GPT图像 -提示词 "水彩柴犬" -密钥 'sk-xxx' -基础地址 'https://open.cherryin.net/v1' .EXAMPLE New-GPT图像 -提示词 "这里改成一顶红色圣诞帽" -参考图 .\立绘.png -蒙版 .\mask.png #> [CmdletBinding(DefaultParameterSetName = '提示词')] param( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = '提示词')] [ValidateNotNullOrEmpty()] [string]$提示词, [Parameter(Mandatory = $true, ParameterSetName = '提示文件')] [ValidateNotNullOrEmpty()] [string]$提示文件, [Parameter()] [switch]$密钥, [Parameter()][string]$密钥值, [Parameter()][string]$基础地址, [Parameter()][string]$模型, [Parameter()] [string]$尺寸 = 'auto', [Parameter()] [string]$质量 = 'auto', [Parameter()] [string]$输出路径 = ".\gpt-image_$(Get-Date -Format 'yyyyMMdd_HHmmss').png", [Parameter()][int]$超时秒数 = 500, [Parameter()] [ValidateNotNullOrEmpty()] [string[]]$参考图, [Parameter()][string]$蒙版 = '' ) $配置路径 = Join-Path $env:LOCALAPPDATA 'Image-generation-cli\AI图像生成-GPT配置.xml' # 提示词 if ($PSCmdlet.ParameterSetName -eq '提示文件') { $提示词 = Read-提示文件 -路径 $提示文件 } # 凭据 if ($密钥.IsPresent -and -not $密钥值) { $安全密钥 = Read-Host -Prompt '请输入 API 密钥' -AsSecureString $密钥值 = [System.Net.NetworkCredential]::new([string]::Empty, $安全密钥).Password } $记住的配置 = Import-记住的配置 -配置路径 $配置路径 $凭据 = Resolve-配置凭据 -参数密钥 $密钥值 -参数基础地址 $基础地址 -参数模型 $模型 ` -记住的配置 $记住的配置 -配置路径 $配置路径 # 基础地址自动补全 /v1 if ($凭据.基础地址 -notmatch '/v\d+/?$') { $凭据.基础地址 = $凭据.基础地址.TrimEnd('/') + '/v1' } # 参考图(支持本地路径和 URL) $参考图数据列表 = @() if ($参考图) { foreach ($单张 in $参考图) { $参考图数据列表 += Get-图像数据 -来源 $单张 } } # 蒙版(支持本地路径和 URL) $蒙版数据 = $null if ($蒙版) { $蒙版数据 = Get-图像数据 -来源 $蒙版 } # 构造请求 if ($参考图数据列表.Count -gt 0) { Add-Type -AssemblyName System.Net.Http $端点 = "$($凭据.基础地址.TrimEnd('/'))/images/edits" Write-Host "正在调用 $($凭据.模型) 生成图像 ..." -ForegroundColor Cyan # 连接中断无限重试:大尺寸/高质量多参考图生成耗时长, # 客户端默认 300 秒超时或中转链路易在等待期间断开连接("A task was canceled" # 且无响应体)。仅"无服务端响应体"的传输层中断才重试(退避等待,最长 30 秒); # 成功响应或明确的 HTTP 错误不重试。 # 重试不产生额外费用:每次调用生成一个幂等键贯穿所有尝试,OpenAI 兼容 API # 收到相同 Idempotency-Key 时会复用首次结果而非重新生成,避免重复计费。 $幂等键 = [guid]::NewGuid().ToString() $尝试 = 0 $表单 = $null $客户端 = $null $响应 = $null while ($true) { $尝试++ if ($尝试 -gt 1) { $等待秒数 = [Math]::Min($尝试, 30) Write-Host "连接中断,正在等待 $等待秒数 秒后第 $尝试 次重试(幂等键相同,不会重复计费)..." -ForegroundColor Yellow Start-Sleep -Seconds $等待秒数 $表单.Dispose() $客户端.Dispose() } $表单 = [System.Net.Http.MultipartFormDataContent]::new() $表单.Add([System.Net.Http.StringContent]::new($凭据.模型), 'model') $表单.Add([System.Net.Http.StringContent]::new($提示词), 'prompt') $表单.Add([System.Net.Http.StringContent]::new('1'), 'n') $表单.Add([System.Net.Http.StringContent]::new('low'), 'moderation') if ($尺寸 -ne 'auto') { $表单.Add([System.Net.Http.StringContent]::new($尺寸), 'size') } if ($质量 -ne 'auto') { $表单.Add([System.Net.Http.StringContent]::new($质量), 'quality') } if ($蒙版数据) { $mc = [System.Net.Http.ByteArrayContent]::new($蒙版数据.字节) $mc.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream') $表单.Add($mc, 'mask', $蒙版数据.文件名) } $字段名 = $(if ($参考图数据列表.Count -gt 1) { 'image[]' } else { 'image' }) foreach ($d in $参考图数据列表) { $ic = [System.Net.Http.ByteArrayContent]::new($d.字节) $ic.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream') $表单.Add($ic, $字段名, $d.文件名) } $客户端 = [System.Net.Http.HttpClient]::new() $客户端.Timeout = [TimeSpan]::FromSeconds($超时秒数) $客户端.DefaultRequestHeaders.Authorization = [System.Net.Http.Headers.AuthenticationHeaderValue]::new('Bearer', $凭据.密钥明文) # 幂等键:让上游把本次调用的多次重试视为同一请求,防止重复计费 if ($客户端.DefaultRequestHeaders.Contains('Idempotency-Key')) { $客户端.DefaultRequestHeaders.Remove('Idempotency-Key') } $客户端.DefaultRequestHeaders.Add('Idempotency-Key', $幂等键) try { $响应消息 = $客户端.PostAsync($端点, $表单).GetAwaiter().GetResult() $响应文本 = $响应消息.Content.ReadAsStringAsync().GetAwaiter().GetResult() if (-not $响应消息.IsSuccessStatusCode) { $状态码 = [int]$响应消息.StatusCode throw "HTTP $状态码:$响应文本" } $响应 = $响应文本 | ConvertFrom-Json break # 成功,跳出重试循环 } catch [System.Management.Automation.MethodInvocationException] { # 服务端报错并关闭连接时,HttpClient 常抛 TaskCanceledException("A task was canceled"), # 真实的 HTTP 错误藏在内部 WebException 中——逐层展开提取,而不是只报最外层消息 $详情 = Get-NetException详情 -Exception $_.Exception # 有服务端响应体(形如 "HTTP 4xx: {...}" 或含 error JSON)视为明确错误,直接抛出 if ($详情 -match '^HTTP \d{3}[::]' -or $详情 -match '"error"|"code"') { throw "API 请求失败:$详情" } # 纯粹的传输层中断:无响应体,判定为可重试(无限重试直到成功或明确错误) Write-Warning "连接被中断(无服务端响应,可能是生成超时或网络抖动):$详情" continue } catch { throw "API 请求失败:$($_.Exception.Message)" } } if ($表单) { $表单.Dispose() } if ($客户端) { $客户端.Dispose() } } else { $端点 = "$($凭据.基础地址.TrimEnd('/'))/images/generations" $请求头 = @{ 'Authorization' = "Bearer $($凭据.密钥明文)"; 'Content-Type' = 'application/json' } $请求体 = @{ model = $凭据.模型; prompt = $提示词; n = 1; moderation = 'low' } if ($尺寸 -ne 'auto') { $请求体['size'] = $尺寸 } if ($质量 -ne 'auto') { $请求体['quality'] = $质量 } $请求体 = $请求体 | ConvertTo-Json -Depth 5 Write-Host "正在调用 $($凭据.模型) 生成图像 ..." -ForegroundColor Cyan try { $响应 = Invoke-RestMethod -Uri $端点 -Method Post -Headers $请求头 ` -Body ([System.Text.Encoding]::UTF8.GetBytes($请求体)) -TimeoutSec $超时秒数 } catch { $错误详情 = $_.Exception.Message $错误详对象 = $_.ErrorDetails if ($错误详对象 -and $错误详对象.PSObject.Properties['Message'] -and $错误详对象.Message) { $错误详情 = $错误详对象.Message } throw "API 请求失败:$错误详情" } } # 记住凭据 if ($凭据.密钥来源 -ne '已记住' -or $凭据.基础地址来源 -ne '已记住' -or $凭据.模型来源 -ne '已记住') { try { Save-配置 -安全密钥 $凭据.安全密钥对象 -保存基础地址 $凭据.基础地址 -保存模型 $凭据.模型 -配置路径 $配置路径 Write-Host "密钥、基础地址与模型已记住:$配置路径" -ForegroundColor DarkGray } catch { Write-Warning "配置保存失败:$($_.Exception.Message)" } } # 解析响应 $数据属性 = $响应.PSObject.Properties['data'] if (-not $数据属性 -or -not $数据属性.Value -or $数据属性.Value.Count -eq 0) { throw "API 未返回图像数据:$($响应 | ConvertTo-Json -Depth 10 -Compress)" } $图像 = $数据属性.Value[0] $改写 = $图像.PSObject.Properties['revised_prompt'] if ($改写 -and $改写.Value) { Write-Host "改写后的提示词: $($改写.Value)" -ForegroundColor DarkGray } $解析后输出 = Resolve-输出路径 -输出路径 $输出路径 $b64属性 = $图像.PSObject.Properties['b64_json'] if ($b64属性 -and $b64属性.Value) { [System.IO.File]::WriteAllBytes($解析后输出, [Convert]::FromBase64String($b64属性.Value)) } else { $url属性 = $图像.PSObject.Properties['url'] if ($url属性 -and $url属性.Value) { Invoke-WebRequest -Uri $url属性.Value -OutFile $解析后输出 -TimeoutSec $超时秒数 } else { throw "响应中既没有 b64_json 也没有 url:$($图像 | ConvertTo-Json -Depth 5 -Compress)" } } Write-Host "图像已保存: $解析后输出" -ForegroundColor Green return $解析后输出 } |