函数/New-Gemini图像.ps1

function New-Gemini图像 {
    <#
    .SYNOPSIS
        调用 Gemini 图像模型生成图像。
    .DESCRIPTION
        通过 Gemini generateContent 接口生成图像,自动从响应 parts 提取 base64 图像。
        密钥、基础地址与模型以 DPAPI 加密记住。网页搜索与图片搜索默认开启。
    .PARAMETER 提示词
        图像描述。与 -提示文件 二选一。
    .PARAMETER 提示文件
        提示词文本文件路径。与 -提示词 二选一。
    .PARAMETER 密钥
        API 令牌。未指定时查找已记住的值。
    .PARAMETER 基础地址
        Base URL。未指定时查找已记住的值。
    .PARAMETER 模型
        模型名。未指定时查找已记住的值。
    .PARAMETER 输出路径
        输出文件路径。默认时间戳 PNG。
    .PARAMETER 超时秒数
        超时时间,默认 300。
    .PARAMETER 参考图
        参考图像路径(可多张)。
    .PARAMETER 宽高比
        1:1 | 16:9 | 9:16 等。默认不指定。
    .PARAMETER 分辨率
        1K | 2K | 4K。默认不指定。
    .EXAMPLE
        New-Gemini图像 -提示词 "水彩柴犬" -密钥 'sk-xxx' -基础地址 'https://ssvip.dmxapi.com/v1' -模型 'gemini-3.1-flash-image-preview'
    #>

    [CmdletBinding(DefaultParameterSetName = '提示词')]
    param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = '提示词')]
        [ValidateNotNullOrEmpty()]
        [string]$提示词,

        [Parameter(Mandatory = $true, ParameterSetName = '提示文件')]
        [ValidateNotNullOrEmpty()]
        [string]$提示文件,

        [Parameter()][string]$密钥,
        [Parameter()][string]$基础地址,
        [Parameter()][string]$模型,

        [Parameter()]
        [string]$输出路径 = ".\gemini_$(Get-Date -Format 'yyyyMMdd_HHmmss').png",

        [Parameter()][int]$超时秒数 = 300,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$参考图,

        [Parameter()][string]$宽高比 = '',
        [Parameter()][string]$分辨率 = ''
    )

    $配置路径 = Join-Path $env:LOCALAPPDATA 'Image-generation-cli\AI图像生成-Gemini配置.xml'

    # 提示词
    if ($PSCmdlet.ParameterSetName -eq '提示文件') {
        $提示词 = Read-提示文件 -路径 $提示文件
    }

    # 凭据
    $记住的配置 = Import-记住的配置 -配置路径 $配置路径
    $凭据 = Resolve-配置凭据 -参数密钥 $密钥 -参数基础地址 $基础地址 -参数模型 $模型 `
        -记住的配置 $记住的配置 -配置路径 $配置路径

    # 参考图
    $参考图路径列表 = @()
    if ($参考图) {
        foreach ($单张 in $参考图) {
            if (-not (Test-Path -LiteralPath $单张 -PathType Leaf)) { throw "参考图不存在:$单张" }
            $参考图路径列表 += $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($单张)
        }
    }

    # 宽高比
    $有效宽高比 = @('1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '21:9', '5:4', '4:5')
    if ($宽高比 -and $宽高比 -notin $有效宽高比) {
        throw "宽高比无效:$宽高比。有效值:$($有效宽高比 -join '、')"
    }

    # 分辨率
    if ($分辨率 -and $分辨率 -notin @('1K', '2K', '4K')) {
        throw "分辨率无效:$分辨率。有效值:1K、2K、4K"
    }

    # 构造 contents
    $部件列表 = [System.Collections.Generic.List[object]]::new()
    $部件列表.Add(@{ text = $提示词 })
    foreach ($p in $参考图路径列表) {
        $部件列表.Add(@{
                inline_data = @{
                    mime_type = Get-Mime类型 -路径 $p
                    data      = [Convert]::ToBase64String([System.IO.File]::ReadAllBytes($p))
                }
            })
    }

    # generationConfig
    $生成配置 = @{ responseModalities = @('TEXT', 'IMAGE') }
    if ($宽高比 -or $分辨率) {
        $图像配置 = @{}
        if ($宽高比) { $图像配置['aspectRatio'] = $宽高比 }
        if ($分辨率) { $图像配置['imageSize'] = $分辨率 }
        $生成配置['imageConfig'] = $图像配置
    }

    # 请求体
    $请求体 = @{
        contents         = @(@{ parts = $部件列表 })
        generationConfig = $生成配置
        safetySettings   = @(
            @{ category = 'HARM_CATEGORY_HARASSMENT';        threshold = 'BLOCK_NONE' },
            @{ category = 'HARM_CATEGORY_HATE_SPEECH';       threshold = 'BLOCK_NONE' },
            @{ category = 'HARM_CATEGORY_SEXUALLY_EXPLICIT'; threshold = 'BLOCK_NONE' },
            @{ category = 'HARM_CATEGORY_DANGEROUS_CONTENT'; threshold = 'BLOCK_NONE' }
        )
        tools            = @(
            @{ google_search = @{ search_types = @{ web_search = @{ }; image_search = @{ } } } }
        )
    } | ConvertTo-Json -Depth 10

    $端点 = "{0}/models/{1}:generateContent" -f $凭据.基础地址.TrimEnd('/'), $凭据.模型
    $请求头 = @{ 'Authorization' = "Bearer $($凭据.密钥明文)"; 'Content-Type' = 'application/json' }

    Write-Host "正在调用 $($凭据.模型) 生成图像 ..." -ForegroundColor Cyan

    try {
        $响应 = Invoke-RestMethod -Uri $端点 -Method Post -Headers $请求头 `
            -Body ([System.Text.Encoding]::UTF8.GetBytes($请求体)) -TimeoutSec $超时秒数
    }
    catch {
        $错误详情 = $_.ErrorDetails.Message
        if (-not $错误详情) { $错误详情 = $_.Exception.Message }
        throw "API 请求失败:$错误详情"
    }

    # 记住凭据
    if ($凭据.密钥来源 -ne '已记住' -or $凭据.基础地址来源 -ne '已记住' -or $凭据.模型来源 -ne '已记住') {
        try {
            Save-配置 -安全密钥 $凭据.安全密钥对象 -保存基础地址 $凭据.基础地址 -保存模型 $凭据.模型 -配置路径 $配置路径
            Write-Host "密钥、基础地址与模型已记住:$配置路径" -ForegroundColor DarkGray
        }
        catch { Write-Warning "配置保存失败:$($_.Exception.Message)" }
    }

    # 解析响应
    if (-not $响应.candidates -or $响应.candidates.Count -eq 0) {
        throw "API 未返回候选内容。"
    }
    $候选 = $响应.candidates[0]

    # 安全拦截检查
    $结束原因属性 = $候选.PSObject.Properties['finishReason']
    $结束原因 = if ($结束原因属性 -and $结束原因属性.Value) { $结束原因属性.Value } else { '' }
    $结束消息属性 = $候选.PSObject.Properties['finishMessage']
    $结束消息 = if ($结束消息属性 -and $结束消息属性.Value) { ": $($结束消息属性.Value)" } else { '' }

    if ($结束原因 -eq 'IMAGE_SAFETY') {
        throw "生成的图像被安全过滤器拦截(IMAGE_SAFETY)$结束消息。请修改提示词后重试。"
    }
    if ($结束原因 -eq 'SAFETY') {
        throw "提示词被安全过滤器拦截(SAFETY)$结束消息。请修改提示词后重试。"
    }

    $内容属性 = $候选.PSObject.Properties['content']
    if (-not $内容属性 -or -not $内容属性.Value) {
        if ($结束原因) { throw "模型未生成任何内容(finishReason=$结束原因)$结束消息。请修改提示词后重试。" }
        throw "响应中没有 content 字段。"
    }
    $部件属性 = $内容属性.Value.PSObject.Properties['parts']
    if (-not $部件属性 -or -not $部件属性.Value) {
        if ($结束原因) { throw "模型完成了推理但未生成图像(finishReason=$结束原因)$结束消息。请修改提示词后重试。" }
        throw "响应中没有 content.parts 字段。"
    }
    $部件 = $部件属性.Value

    $图像部件 = $null
    foreach ($单部件 in $部件) {
        if ($单部件.PSObject.Properties['inlineData'] -and $单部件.inlineData) { $图像部件 = $单部件.inlineData; break }
        if ($单部件.PSObject.Properties['inline_data'] -and $单部件.inline_data) { $图像部件 = $单部件.inline_data; break }
    }
    if (-not $图像部件) {
        throw "响应中没有图像数据:$($响应 | ConvertTo-Json -Depth 10 -Compress)"
    }

    foreach ($单部件 in $部件) {
        if ($单部件.PSObject.Properties['text'] -and $单部件.text) {
            Write-Host "模型说明: $($单部件.text)" -ForegroundColor DarkGray
        }
    }

    $解析后输出 = Resolve-输出路径 -输出路径 $输出路径
    [System.IO.File]::WriteAllBytes($解析后输出, [Convert]::FromBase64String($图像部件.data))
    Write-Host "图像已保存: $解析后输出" -ForegroundColor Green
    return $解析后输出
}