很喜欢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直接放入文件中。