本机开发环境:Pi 网关、Pi-Switch 与 Incus 隔离容器

本文适用于需要在本机同时满足“模型统一路由”与“开发环境隔离”的场景。你会学到:本机 Arch + Incus + Pi + Pi-Switch 网关的整体拓扑、各组件职责与真实配置、以及从宿主机到容器内可复现的验证路径。

本文基于 2026-08-31 本机实测整理,敏感值已脱敏(API Key、真实域名以占位符展示),以本机 incus list / ~/.pi/agent/settings.json / ~/.pi-switch/config.json 实测为准。

适用场景

  • 你在裸机上跑 piopencode 等 AI 编码代理,担心依赖污染宿主机,或多个项目需要独立文件系统/网络
  • 模型 Key 分散在多个上游(OpenAI、DeepSeek、私有网关),需要在本地统一入口做模型名路由、故障转移与用量统计
  • 需要一套“宿主机可写、容器内也可写”的共享目录,且不踩 nobody 65534 / chown: Invalid argument 的坑

整体架构

flowchart LR
  subgraph Host["宿主机 Arch Linux 7.1.10-zen1"]
    PI["pi 0.84.4\n~/.pi/agent"]
    PS["Pi-Switch 网关\n127.0.0.1:43112\nproviderPrefix=pi-switch"]
    INCUS["Incus 7.3 / LXC 7.0.0\ndir 存储 / nftables"]
  end
  subgraph Container["Incus 容器 arch\n10.10.10.85"]
    CPI["pi 0.84.4\ndefaultProvider=pi-switch"]
  end
  UP["上游模型\nhttps://opencode.ai/zen/go/v1\nDeepSeek / Kimi / GLM"]
  PROJ["共享目录\n/home/shial/Project ↔ /home/arch/Project\nshift=true"]

  CPI -->|hostproxy 127.0.0.1:43112| PS --> UP
  PI -.->|宿主机直连| PS
  INCUS --- PROJ
  Container --- PROJ
组件职责本机形态
piAgent 运行时、会话/扩展/技能管理fnm + npm 全局安装 @earendil-works/pi-coding-agent,配置集中在 ~/.pi/agent/
Pi-SwitchProvider 管理 + 模型名网关(路由/转换/故障转移)本地 Rust 网关 0.0.0.0:43112,WebUI 127.0.0.1:43110providerPrefix=pi-switch 写入 ~/.pi/agent/models.json
Pi 网关协议转换与统一端点由 Pi-Switch 承担:openai-responses API、无状态按 profile/model 路由、SSE 透传、断路器
Incus非特权容器隔离3 容器(arch RUNNING,其余模板 STOPPED),shift=true 共享目录,hostproxy 透出网关端口到容器

数据流:容器内 pi --provider pi-switch --model oc/muse-spark-1.2-contributor127.0.0.1:43112(hostproxy)→ Pi-Switch 按模型名选 profile ochttps://opencode.ai/zen/go/v1/responses → 上游。

主机基座

本机为 Arch Linux(7.1.10-zen1-1-zen),Incus 7.3 + LXC 7.0.0storage: dirfirewall: nftablesapi_extensions_count: 541

Node 与 pi:

node -v          # v24.16.0(由 fnm 管理,路径 ~/.local/share/fnm)
pi --version     # 0.84.4

pi 通过 npm install -g --ignore-scripts @earendil-works/pi-coding-agent 安装,可执行文件在 ~/.local/share/fnm/node-versions/v24.16.0/installation/bin/pi,切 Node 版本后需在对应版本重装。

~/.pi/agent/ 核心布局(本机实测):

~/.pi/agent/
├── settings.json        # 全局设置(defaultProvider、theme、packages)
├── models.json          # 由 Pi-Switch 网关发布写入
├── sessions/            # 会话 JSONL,按工作目录分树
├── state/workspace-history
├── extensions/、skills/  # 本地扩展与技能
└── auth.json            # 凭证,勿提交

宿主机 ~/.pi/agent/settings.json 关键项(脱敏):

{
  "defaultProvider": "opencode-go",
  "defaultModel": "muse-spark-1.2-contributor",
  "defaultThinkingLevel": "xhigh",
  "theme": "front-end-delight",
  "packages": ["../../Project/pi-switch", "npm:pi-mcp-adapter", "..."]
}

宿主机 defaultProvideropencode-go,但已通过本地包 ../../Project/pi-switch 接入 Pi-Switch;容器内 ~/.pi/agent/settings.json 则直接 defaultProvider: pi-switch

Pi 网关(由 Pi-Switch 提供)

本机没有独立的 AxonHub 实例,Pi 网关即 Pi-Switch 的本地代理。这是本机与早期 ai/axonhub-* 文档(Docker + SQLite 8090 方案)的差异:AxonHub 仍可作为可选网关,但当前日常链路为 Pi-Switch。

Pi-Switch 本质是轻量 profile 切换器 + 模型名网关(Rust 核心 + TUI/CLI/WebUI 三端同源):

  • Provider 管理:CRUD、多上游 upstreams[]、模型暴露 expose 到 pi、Responses 透传/转换
  • 网关:独立进程,无状态按 profile/model 路由,SSE 流式、User-Agent 伪装、OpenAI ↔ Anthropic、Responses ↔ Chat Completions 互转、故障转移与断路器
  • 发布模型:显式 发布到 Pi 才会写入 ~/.pi/agent/models.json,非自动同步

本机 ~/.pi-switch/config.json(脱敏后节选):

{
  "current": "oc",
  "profiles": {
    "oc": {
      "api": "openai-responses",
      "baseUrl": "https://opencode.ai/zen/go/v1",
      "apiKey": "sk-***",
      "exposedModels": ["muse-spark-1.2-contributor", "mimo-v2.5", "hy3", "kimi-k2.5", "glm-5.2"],
      "models": ["deepseek-v4-flash", "mimo-v2.5", "...共 30+ 个"]
    }
  },
  "settings": {
    "providerPrefix": "pi-switch",
    "gatewayApi": "openai-responses",
    "proxy": { "host": "0.0.0.0", "port": 43112, "failover": ["oc"] },
    "web": { "host": "127.0.0.1", "port": 43110 }
  }
}

本机运行态:

pi-switch proxy status
# Proxy daemon is running (PID 526553)
# Listen: http://0.0.0.0:43112
# Failover: oc

pi-switch provider list
# * oc [p1]  api: openai-responses  baseUrl: https://opencode.ai/zen/go/v1
#   models: deepseek-v4-flash, mimo-v2.5, hy3, kimi-k2.5, glm-5.2, ...

pi-switch config show | head -n 20

参考:Pi-Switch GitHub 与本地 ~/Project/pi-switch/README.mdCONTEXT.md

Pi-Switch

Pi-Switch 在 pi 侧以本地包 ../../Project/pi-switch 形式安装,并通过 extensions/index.ts 注入会话归因头(x-conversation-id / x-opencode-session),用于网关侧的 requests.log 聚合与 WebUI 统计。

  • 配置入口pi-switch tui(推荐)或 pi-switch webui start --daemon(浏览器 http://127.0.0.1:43110
  • 诊断pi-switch doctor 检查 config.json / models.json / 结构完整性
  • 统计:每次代理请求追加写入 ~/.pi-switch/requests.log(JSONL),WebUI 按 today/last24h/last7d/custom 聚合四维度 token(input/output/cached/reasoning)与缓存命中率
  • 断路器failureThreshold: 3cooldownSeconds: 60,半开探测恢复

宿主机与容器协同:宿主机网关监听 0.0.0.0:43112,Incus 通过 hostproxy 设备将该端口透入容器,容器内 127.0.0.1:43112 即宿主机网关,无需额外网络打洞。

Incus 隔离环境

容器清单(2026-08-31 实测,脱敏)

+-----------------+---------+--------------------+------+-----------+-----------+
|      NAME       |  STATE  |        IPV4        | IPV6 |   TYPE    | SNAPSHOTS |
+-----------------+---------+--------------------+------+-----------+-----------+
| arch            | RUNNING | 10.10.10.85 (eth0) |      | CONTAINER | 0         |
| debian-template | STOPPED |                    |      | CONTAINER | 0         |
| ubuntu-template | STOPPED |                    |      | CONTAINER | 0         |
+-----------------+---------+--------------------+------+-----------+-----------+

incus infoserver_version: 7.3driver: lxc | qemu 7.0.0 | 11.1.0kernel: 7.1.10-zen1-1-zen

容器 arch 规格:limits.cpu: 8limits.memory: 16GiBsecurity.nesting: truesecurity.protection.shift: true

非特权映射与共享目录

容器为非特权,volatile.idmap.current

[{"Isuid":true,"Hostid":165536,"Nsid":0,"Maprange":65536},
 {"Isgid":true,"Hostid":165536,"Nsid":0,"Maprange":65536}]

容器内:

cat /proc/self/uid_map
#          0     165536      65536

含义:容器 UID 0 → 宿主机 165536,容器 1000 → 宿主机 166536,以此类推。此前排障沉淀见 Incus 非特权容器 UID/GID 映射与共享目录排障

本机当前采用 shift=true(Incus shiftfs)而非手动 chown 166536:175536

devices:
  Project:
    path: /home/arch/Project
    source: /home/shial/Project
    shift: "true"
    type: disk
  hostproxy:
    bind: container
    connect: tcp:127.0.0.1:43112
    listen: tcp:127.0.0.1:43112
    type: proxy

效果:

  • 宿主机 id shialuid=1000 gid=1000 groups=...175536(project_shared),目录 drwxr-xr-x shial:shial /home/shial/Project
  • 容器内 ls -ln /home/arch/Project 显示 1000:1000nobody 65534 不再出现,Invalid argument 不再触发
  • 宿主机已保留 project:x:166536:175536project_shared:x:175536:shial 供回退到手动对齐方案时使用;但 shift=true 下无需 sudo chown -R 166536:175536

流水:宿主机 /home/shial/Project --shiftfs--> 容器 /home/arch/Project,双向读写即时可见。

端到端工作流

以容器内发起一次 pi 请求为例(全程可复现):

# 1. 宿主机:确认网关
pi-switch proxy status
# Listen: http://0.0.0.0:43112

# 2. 宿主机:确认 Incus
incus list
incus config show arch | grep -A2 hostproxy

# 3. 容器内:确认影子映射
incus exec arch -- bash -c "cat /proc/self/uid_map; id"

# 4. 容器内:经网关发起请求(模型名含 profile 前缀)
incus exec arch -- bash -c "pi --provider pi-switch --model oc/muse-spark-1.2-contributor -p 'hello via pi-switch'"

# 5. 宿主机:查看聚合
tail -n 1 ~/.pi-switch/requests.log | python3 -m json.tool
# 含 conversationId、model、provider、promptTokens、costTotal

# 6. 宿主机:WebUI 统计
# 浏览器打开 http://127.0.0.1:43110 → Stats 页按 today 聚合

~/.pi-switch/requests.log 示例(脱敏,单行 JSONL):

{"conversationId":"01a05616-4078-7e11-9972-2f1af750b73b","model":"muse-spark-1.2-contributor","provider":"oc","promptTokens":125856,"completionTokens":107,"ok":true,"status":200}

排障与常见问题

现象排查
nobody / 65534容器内 cat /proc/self/uid_map,确认 0 165536 65536;若用手动方案则检查 Host UID = 165536 + Container UID 是否对齐,参考 排障记录
chown: Invalid argument同上,宿主机 UID 未落在映射范围;或 shift=false 时未 chown -R 166536:175536
宿主机改不动容器写入的文件id shial 是否含 project_sharedls -ld /home/shial/Project 是否 g+rwXg+s(手动方案)
pi-switch proxy status 未运行pi-switch proxy start --daemon,查 ~/.pi-switch/proxy.log
容器内 curl 127.0.0.1:43112 不通incus config show arch 是否含 hostproxy,宿主机 `ss -tlnp
模型 401 / 429pi-switch config showbaseUrl/apiKey 脱敏核对,requests.logstatus/error,触发断路器时等待 60s 冷却

验证与自查

  1. pi --version 输出 0.84.4pi list~/.pi/agent/settings.jsonpackages 一致
  2. pi-switch proxy status 显示 runningListen: http://0.0.0.0:43112
  3. incus listarch RUNNING 10.10.10.85incus exec arch -- cat /proc/self/uid_map0 165536 65536
  4. 宿主机 id shialproject_sharedls -ld /home/shial/Project 可写
  5. 容器内 ls -ln /home/arch/Project65534touch /home/arch/Project/.test && ls /home/shial/Project/.test 双向可见
  6. 容器内 pi --provider pi-switch --model oc/muse-spark-1.2-contributor -p "hello" 成功返回,并在 ~/.pi-switch/requests.log 留痕
  7. hugo --quiet --ignoreCache 构建零错误,本文在 http://localhost:1313/ops/dev-env-pi-gateway-incus/ 渲染正常,Mermaid 可见

参考