很喜欢Pi Coding Agent的设计理念,简洁至上,需要什么定制即可。

Profile隔离

由于自己的工作可能经常在开发和安全来回切换,这实际上是两种角色,如果使用一个配置,那么其实又会让Pi有一定的笨重感,所以做配置隔离是一件有必要且必须的事。

配置同步

这里选用Git作为配置同步的工具,方便管理不用的版本。

设计思路

通过profile.lock来锚定当前应该使用的配置,然后通过alias或者直接定义function劫持pi调用,通过预设PI_CODING_AGENT_DIR环境变量进行配置切换。

对于登录的凭据,应该需要共享,否则每个配置文件都需要单独登录的话,那就过于复杂了,我们只需要package、settings等隔离即可;这里考虑通过链接将一个固定位置的同步到各隔离的配置文件中。

完整实现

整个配置仓库包含了对应的script,仓库应该克隆在~/.pi/profiles下。

Windows

首先对于Windows来说,都26年了,PowerShell才是主力的Shell,所以使用PowerShell来实现:

# Pi Coding Agent profile helpers for PowerShell.
 
$script:PiProfilesRoot = Split-Path -Parent $PSScriptRoot
$script:PiProfileLock = Join-Path $script:PiProfilesRoot 'profile.lock'
$script:PiSharedRoot = Join-Path $script:PiProfilesRoot '.shared'
$script:PiSharedFiles = @('auth.json', 'models-store.json')
 
function Assert-PiProfileName {
    param([Parameter(Mandatory)][string]$Name)
 
    if ($Name -notmatch '^[A-Za-z0-9][A-Za-z0-9_-]*$') {
        throw "Invalid profile name '$Name'. Use letters, numbers, underscores, or hyphens."
    }
}
 
function Get-PiProfilePath {
    param([Parameter(Mandatory)][string]$Name)
 
    Assert-PiProfileName -Name $Name
    Join-Path $script:PiProfilesRoot "profile_$Name"
}
 
function Connect-PiSharedFiles {
    param([Parameter(Mandatory)][string]$ProfilePath)
 
    New-Item -ItemType Directory -Path $script:PiSharedRoot -Force | Out-Null
    $legacyRoot = Join-Path ([Environment]::GetFolderPath('UserProfile')) '.pi\agent'
 
    foreach ($fileName in $script:PiSharedFiles) {
        $sharedPath = Join-Path $script:PiSharedRoot $fileName
        $profileFile = Join-Path $ProfilePath $fileName
        if (-not (Test-Path -LiteralPath $sharedPath -PathType Leaf)) {
            $legacyFile = Join-Path $legacyRoot $fileName
            if (Test-Path -LiteralPath $legacyFile -PathType Leaf) {
                Copy-Item -LiteralPath $legacyFile -Destination $sharedPath
            } elseif (Test-Path -LiteralPath $profileFile -PathType Leaf) {
                Copy-Item -LiteralPath $profileFile -Destination $sharedPath
            } else {
                Set-Content -LiteralPath $sharedPath -Value '{}' -NoNewline -Encoding utf8
            }
        }
 
        if (Test-Path -LiteralPath $profileFile -PathType Leaf) {
            $sharedHash = (Get-FileHash -LiteralPath $sharedPath -Algorithm SHA256).Hash
            $profileHash = (Get-FileHash -LiteralPath $profileFile -Algorithm SHA256).Hash
            if ($sharedHash -ne $profileHash) {
                throw "'$profileFile' differs from the shared $fileName. Reconcile it with '$sharedPath' before retrying."
            }
            Remove-Item -LiteralPath $profileFile -Force
        }
        New-Item -ItemType HardLink -Path $profileFile -Target $sharedPath -ErrorAction Stop | Out-Null
    }
}
 
function Sync-PiProfiles {
    [CmdletBinding()]
    param()
 
    if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
        throw 'Git is required but was not found in PATH.'
    }
 
    $gitDirectory = Join-Path $script:PiProfilesRoot '.git'
    if (-not (Test-Path -LiteralPath $gitDirectory)) {
        throw "'$script:PiProfilesRoot' is not a cloned Git repository. Clone the profile repository there first."
    }
 
    & git -C $script:PiProfilesRoot rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' *> $null
    if ($LASTEXITCODE -ne 0) { throw 'The current branch has no upstream. Configure it with git push --set-upstream first.' }
 
    & git -C $script:PiProfilesRoot add --all
    if ($LASTEXITCODE -ne 0) { throw 'Unable to stage Pi profile changes.' }
    & git -C $script:PiProfilesRoot diff --cached --quiet
    if ($LASTEXITCODE -eq 1) {
        & git -C $script:PiProfilesRoot commit -m 'chore: sync Pi profiles'
        if ($LASTEXITCODE -ne 0) { throw 'Unable to commit Pi profile changes.' }
    } elseif ($LASTEXITCODE -ne 0) {
        throw 'Unable to inspect staged Pi profile changes.'
    }
 
    & git -C $script:PiProfilesRoot fetch
    if ($LASTEXITCODE -ne 0) { throw 'Unable to fetch the Pi profile repository.' }
 
    $counts = @(& git -C $script:PiProfilesRoot rev-list --left-right --count 'HEAD...@{upstream}')
    if ($LASTEXITCODE -ne 0) { throw 'Unable to compare the local and upstream branches.' }
    $parts = ($counts -join ' ').Trim() -split '\s+'
    $ahead = [int]$parts[0]
    $behind = [int]$parts[1]
 
    if ($ahead -gt 0 -and $behind -gt 0) {
        & git -C $script:PiProfilesRoot pull --rebase
        if ($LASTEXITCODE -ne 0) { throw 'Unable to rebase local Pi profile changes. Resolve the Git conflict, then retry.' }
        & git -C $script:PiProfilesRoot push
        if ($LASTEXITCODE -ne 0) { throw 'Unable to push rebased Pi profiles.' }
        return
    }
    if ($ahead -gt 0) {
        & git -C $script:PiProfilesRoot push
        if ($LASTEXITCODE -ne 0) { throw 'Unable to push Pi profiles.' }
        return
    }
    if ($behind -gt 0) {
        & git -C $script:PiProfilesRoot pull --ff-only
        if ($LASTEXITCODE -ne 0) { throw 'Unable to pull Pi profiles.' }
        return
    }
    Write-Output 'Pi profiles are already synchronized.'
}
 
function New-PiProfile {
    [CmdletBinding()]
    param([Parameter(Mandatory, Position = 0)][string]$Name)
 
    $path = Get-PiProfilePath -Name $Name
    if (Test-Path -LiteralPath $path) {
        throw "Pi profile '$Name' already exists."
    }
    New-Item -ItemType Directory -Path $path -ErrorAction Stop | Out-Null
    try {
        New-Item -ItemType File -Path (Join-Path $path '.gitkeep') -ErrorAction Stop | Out-Null
        Connect-PiSharedFiles -ProfilePath $path
    } catch {
        Remove-Item -LiteralPath $path -Recurse -Force
        throw
    }
    Get-Item -LiteralPath $path
}
 
function Remove-PiProfile {
    [CmdletBinding()]
    param([Parameter(Mandatory, Position = 0)][string]$Name)
 
    $path = Get-PiProfilePath -Name $Name
    if (-not (Test-Path -LiteralPath $path -PathType Container)) {
        throw "Pi profile '$Name' does not exist."
    }
    if (-not $PSCmdlet.ShouldContinue("Delete '$path' and all profile-specific configuration?", "Remove Pi profile '$Name'")) {
        return
    }
    Remove-Item -LiteralPath $path -Recurse -Force
    if ((Get-PiProfile) -eq $Name) {
        Remove-Item -LiteralPath $script:PiProfileLock -Force -ErrorAction SilentlyContinue
        Remove-Item Env:\PI_CODING_AGENT_DIR -ErrorAction SilentlyContinue
    }
}
 
function Set-PiProfile {
    [CmdletBinding()]
    param([Parameter(Mandatory, Position = 0)][string]$Name)
 
    $path = Get-PiProfilePath -Name $Name
    if (-not (Test-Path -LiteralPath $path -PathType Container)) {
        throw "Pi profile '$Name' does not exist. Create it with New-PiProfile first."
    }
    Connect-PiSharedFiles -ProfilePath $path
    $resolved = (Resolve-Path -LiteralPath $path).Path
    Set-Content -LiteralPath $script:PiProfileLock -Value $Name -NoNewline -Encoding utf8
    $env:PI_CODING_AGENT_DIR = $resolved
    Write-Output "Default Pi profile set to '$Name' ($resolved)."
}
 
function Get-PiProfile {
    [CmdletBinding()]
    param()
 
    if (-not (Test-Path -LiteralPath $script:PiProfileLock -PathType Leaf)) { return $null }
    $name = (Get-Content -LiteralPath $script:PiProfileLock -Raw).Trim()
    Assert-PiProfileName -Name $name
    $name
}
 
function Import-PiDefaultProfile {
    if (-not (Test-Path -LiteralPath $script:PiProfileLock -PathType Leaf)) { return $null }
 
    $name = (Get-Content -LiteralPath $script:PiProfileLock -Raw).Trim()
    try {
        $path = Get-PiProfilePath -Name $name
    } catch {
        Write-Warning "Ignoring invalid profile.lock: $($_.Exception.Message)"
        return $null
    }
    if (-not (Test-Path -LiteralPath $path -PathType Container)) {
        Write-Warning "Ignoring profile.lock: Pi profile '$name' does not exist."
        return $null
    }
    (Resolve-Path -LiteralPath $path).Path
}
 
function Get-PiExecutable {
    foreach ($candidate in @('pi.exe', 'pi.cmd', 'pi.bat')) {
        $command = Get-Command $candidate -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
        if ($command) { return $command.Source }
    }
    throw 'Pi Coding Agent was not found in PATH.'
}
 
function pi {
    $arguments = @($args)
    $executable = Get-PiExecutable
    if ($arguments.Count -ge 2 -and $arguments[0] -eq '--profile') {
        $profilePath = Get-PiProfilePath -Name ([string]$arguments[1])
        if (-not (Test-Path -LiteralPath $profilePath -PathType Container)) {
            throw "Pi profile '$($arguments[1])' does not exist."
        }
        Connect-PiSharedFiles -ProfilePath $profilePath
        if ($arguments.Count -gt 2) {
            $arguments = @($arguments[2..($arguments.Count - 1)])
        } else {
            $arguments = @()
        }
        $previous = $env:PI_CODING_AGENT_DIR
        try {
            $env:PI_CODING_AGENT_DIR = (Resolve-Path -LiteralPath $profilePath).Path
            & $executable @arguments
        } finally {
            $env:PI_CODING_AGENT_DIR = $previous
        }
        return
    }
    $defaultPath = Import-PiDefaultProfile
    if (-not $defaultPath) {
        & $executable @arguments
        return
    }
    Connect-PiSharedFiles -ProfilePath $defaultPath
    $previous = $env:PI_CODING_AGENT_DIR
    try {
        $env:PI_CODING_AGENT_DIR = $defaultPath
        & $executable @arguments
    } finally {
        $env:PI_CODING_AGENT_DIR = $previous
    }
}
 
$defaultPiProfile = Import-PiDefaultProfile
if ($defaultPiProfile) { $env:PI_CODING_AGENT_DIR = $defaultPiProfile }
Remove-Variable defaultPiProfile -ErrorAction SilentlyContinue
 

macOS/Linux

使用shell来实现:

#!/usr/bin/env sh
# Pi Coding Agent profile helpers for POSIX-compatible shells.
 
_pi_profiles_script=$0
if [ -n "${BASH_SOURCE:-}" ]; then
    _pi_profiles_script=${BASH_SOURCE[0]}
elif [ -n "${ZSH_VERSION:-}" ]; then
    eval '_pi_profiles_script=${(%):-%x}'
fi
PI_PROFILES_ROOT=$(CDPATH= cd -- "$(dirname -- "$_pi_profiles_script")/.." && pwd)
export PI_PROFILES_ROOT
PI_PROFILE_LOCK=$PI_PROFILES_ROOT/profile.lock
export PI_PROFILE_LOCK
PI_SHARED_ROOT=$PI_PROFILES_ROOT/.shared
export PI_SHARED_ROOT
unset _pi_profiles_script
 
_pi_validate_name() {
    case ${1-} in
        ''|*[!A-Za-z0-9_-]*|[-_]* )
            printf '%s\n' "Invalid profile name '${1-}'. Use letters, numbers, underscores, or hyphens." >&2
            return 2
            ;;
    esac
}
 
_pi_profile_path() {
    _pi_validate_name "$1" || return
    printf '%s/profile_%s\n' "$PI_PROFILES_ROOT" "$1"
}
 
_pi_connect_shared() {
    _pi_target_profile=$1
    mkdir -p -- "$PI_SHARED_ROOT" || return
    for _pi_file in auth.json models-store.json; do
        _pi_shared=$PI_SHARED_ROOT/$_pi_file
        _pi_target=$_pi_target_profile/$_pi_file
        if [ ! -f "$_pi_shared" ]; then
            if [ -f "$HOME/.pi/agent/$_pi_file" ]; then
                cp -- "$HOME/.pi/agent/$_pi_file" "$_pi_shared" || return
            elif [ -f "$_pi_target" ]; then
                cp -- "$_pi_target" "$_pi_shared" || return
            else
                printf '{}' >"$_pi_shared" || return
            fi
        fi
        if [ -f "$_pi_target" ] || [ -L "$_pi_target" ]; then
            if ! cmp -s -- "$_pi_shared" "$_pi_target"; then
                printf '%s\n' "'$_pi_target' differs from the shared $_pi_file. Reconcile it with '$_pi_shared' before retrying." >&2
                return 2
            fi
            rm -f -- "$_pi_target" || return
        fi
        if [ "${OS:-}" = 'Windows_NT' ]; then
            ln -- "$_pi_shared" "$_pi_target" || return
        else
            ln -s -- "../.shared/$_pi_file" "$_pi_target" || return
        fi
    done
}
 
sync-pi() {
    command -v git >/dev/null 2>&1 || {
        printf '%s\n' 'Git is required but was not found in PATH.' >&2
        return 127
    }
 
    if [ ! -d "$PI_PROFILES_ROOT/.git" ]; then
        printf '%s\n' "'$PI_PROFILES_ROOT' is not a cloned Git repository. Clone the profile repository there first." >&2
        return 2
    fi
 
    if ! git -C "$PI_PROFILES_ROOT" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' >/dev/null 2>&1; then
        printf '%s\n' 'The current branch has no upstream. Configure it with git push --set-upstream first.' >&2
        return 2
    fi
    git -C "$PI_PROFILES_ROOT" add --all || return
    if ! git -C "$PI_PROFILES_ROOT" diff --cached --quiet; then
        git -C "$PI_PROFILES_ROOT" commit -m 'chore: sync Pi profiles' || return
    fi
    git -C "$PI_PROFILES_ROOT" fetch || return
    _pi_counts=$(git -C "$PI_PROFILES_ROOT" rev-list --left-right --count 'HEAD...@{upstream}') || return
    _pi_ahead=$(printf '%s\n' "$_pi_counts" | cut -f 1)
    _pi_behind=$(printf '%s\n' "$_pi_counts" | cut -f 2)
    case $_pi_ahead:$_pi_behind in
        *[!0-9:]*|:*|*:)
            printf '%s\n' "Unable to parse Git synchronization state: $_pi_counts" >&2
            return 2
            ;;
    esac
 
    if [ "$_pi_ahead" -gt 0 ] && [ "$_pi_behind" -gt 0 ]; then
        git -C "$PI_PROFILES_ROOT" pull --rebase || {
            printf '%s\n' 'Unable to rebase local Pi profile changes. Resolve the Git conflict, then retry.' >&2
            return 2
        }
        git -C "$PI_PROFILES_ROOT" push
    elif [ "$_pi_ahead" -gt 0 ]; then
        git -C "$PI_PROFILES_ROOT" push
    elif [ "$_pi_behind" -gt 0 ]; then
        git -C "$PI_PROFILES_ROOT" pull --ff-only
    else
        printf '%s\n' 'Pi profiles are already synchronized.'
    fi
}
 
new-pi() {
    [ "$#" -eq 1 ] || { printf '%s\n' 'Usage: new-pi NAME' >&2; return 2; }
    _pi_path=$(_pi_profile_path "$1") || return
    if [ -e "$_pi_path" ]; then
        printf '%s\n' "Pi profile '$1' already exists." >&2
        return 2
    fi
    mkdir -- "$_pi_path" || return
    : >"$_pi_path/.gitkeep" || {
        rmdir -- "$_pi_path"
        return 1
    }
    if ! _pi_connect_shared "$_pi_path"; then
        rm -rf -- "$_pi_path"
        return 1
    fi
    printf '%s\n' "$_pi_path"
}
 
rm-pi() {
    [ "$#" -eq 1 ] || { printf '%s\n' 'Usage: rm-pi NAME' >&2; return 2; }
    _pi_path=$(_pi_profile_path "$1") || return
    if [ ! -d "$_pi_path" ]; then
        printf '%s\n' "Pi profile '$1' does not exist." >&2
        return 2
    fi
    printf "Delete '%s' and all profile-specific configuration? [y/N] " "$_pi_path" >&2
    IFS= read -r _pi_answer
    case $_pi_answer in y|Y|yes|YES) ;; *) printf '%s\n' 'Cancelled.'; return 1 ;; esac
    rm -rf -- "$_pi_path" || return
    if [ "$(get-pi 2>/dev/null)" = "$1" ]; then
        rm -f -- "$PI_PROFILE_LOCK"
        unset PI_CODING_AGENT_DIR
    fi
}
 
set-pi() {
    [ "$#" -eq 1 ] || { printf '%s\n' 'Usage: set-pi NAME' >&2; return 2; }
    _pi_path=$(_pi_profile_path "$1") || return
    if [ ! -d "$_pi_path" ]; then
        printf '%s\n' "Pi profile '$1' does not exist. Create it with new-pi first." >&2
        return 2
    fi
    _pi_connect_shared "$_pi_path" || return
    PI_CODING_AGENT_DIR=$(CDPATH= cd -- "$_pi_path" && pwd)
    export PI_CODING_AGENT_DIR
    printf '%s' "$1" >"$PI_PROFILE_LOCK" || return
    printf '%s\n' "Default Pi profile set to '$1' ($PI_CODING_AGENT_DIR)."
}
 
get-pi() {
    [ "$#" -eq 0 ] || { printf '%s\n' 'Usage: get-pi' >&2; return 2; }
    [ -f "$PI_PROFILE_LOCK" ] || return 1
    IFS= read -r _pi_name <"$PI_PROFILE_LOCK" || [ -n "${_pi_name:-}" ] || return 1
    _pi_validate_name "$_pi_name" || return
    printf '%s\n' "$_pi_name"
}
 
_pi_load_default() {
    [ -f "$PI_PROFILE_LOCK" ] || return 1
    IFS= read -r _pi_name <"$PI_PROFILE_LOCK" || [ -n "${_pi_name:-}" ] || return 1
    _pi_path=$(_pi_profile_path "$_pi_name") || {
        printf '%s\n' 'Ignoring invalid profile.lock.' >&2
        return 1
    }
    if [ ! -d "$_pi_path" ]; then
        printf '%s\n' "Ignoring profile.lock: Pi profile '$_pi_name' does not exist." >&2
        return 1
    fi
    PI_CODING_AGENT_DIR=$(CDPATH= cd -- "$_pi_path" && pwd)
    export PI_CODING_AGENT_DIR
}
 
pi() {
    if [ "${1-}" = '--profile' ]; then
        [ "$#" -ge 2 ] || { printf '%s\n' 'Usage: pi --profile NAME [ARGS...]' >&2; return 2; }
        _pi_path=$(_pi_profile_path "$2") || return
        if [ ! -d "$_pi_path" ]; then
            printf '%s\n' "Pi profile '$2' does not exist." >&2
            return 2
        fi
        _pi_connect_shared "$_pi_path" || return
        shift 2
        PI_CODING_AGENT_DIR=$_pi_path command pi "$@"
        return
    fi
    if _pi_load_default; then
        _pi_connect_shared "$PI_CODING_AGENT_DIR" || return
        PI_CODING_AGENT_DIR=$PI_CODING_AGENT_DIR command pi "$@"
    else
        command pi "$@"
    fi
}
 
_pi_load_default >/dev/null 2>&1 || :
 

自定义模型供应商

由于/login无法直接添加API-Server,因此需要我们自己对其进行配置,在对应的profile_{NAME}下创建文件models.json,然后填入Provider即可,这里给一个例子:

{
  "providers": {
    "yuan-xiao-cong": {
      "baseUrl": "http://ai.enterprice.in/api/v1",
      "apiKey": "$AI_API_KEY",
      "api": "openai-completions",
      "models": [
        {
          "id": "qwen3-omni-instruct",
          "name": "Qwen3 Omni",
          "reasoning": false,
          "input": ["text", "image"],
          "contextWindow": 32768
        },
        {
          "id": "deepseek-v4-flash",
          "name": "DeepSeek V4 Flash",
          "reasoning": true,
          "input": ["text"],
          "contextWindow": 1048576,
          "thinkingLevelMap": {
            "minimal": null,
            "low": null,
            "medium": null,
            "high": "high",
            "xhigh": "max",
            "max": "max"
          },
          "compat": {
            "thinkingFormat": "chat-template",
            "chatTemplateKwargs": {
              "thinking": {
                "$var": "thinking.enabled"
              },
              "reasoning_effort": {
                "$var": "thinking.effort",
                "omitWhenOff": true
              }
            }
          }
        }
      ]
    }
  }
}

注意在环境变量中设置对应的KEY,不应该把API KEY直接放入文件中。