pi-web 的 HTTP API 手册:47 个路由、接入姿势与 DIY 配方

AI 速览2026/9/16 11:34更新

作者通读并实测了 pi-web 0.8.11 的源码,整理出 47 个 HTTP 路由的调用手册,目的是绕过界面直接用脚本调度对话。核心结论是只有四个端点撑起自动化:列会话、查运行中、读单会话状态、向会话发 prompt,其余都是配套。接入需过 Host 白名单和密码两道闸,脚本用 Basic 认证且不带 Origin 即可;注意会话列表有 30 秒缓存、events 端点会冷启动会话、models-config 明文返回 API Key、登录无速率限制,因此不宜暴露到公网。

我把 pi-web 的源码扒了一遍,逐个端点读过并实测,整理成这份手册。目的不是给 pi-web 提 PR,而是让我自己以后能绕过它的界面直接调——比如写脚本问"现在有多少对话在跑"、把答案扔回某个对话继续跑、或者拿会话状态做自己的看板。

以 pi-web 0.8.11 为准。凡标「实测」的是我真发过请求验证过的;标「读源码」的是没在真实数据上跑写操作、只按代码推的。这份东西会过期,pi-web 更新很快(写这篇时 npm 上已到 0.9.1)。

一、它怎么进得来#

pi-web 是本地工具,默认只监听 127.0.0.1:30141。可以用 --hostname 改绑到别的地址(比如容器网桥,或 0.0.0.0 走局域网)——改了之后 127.0.0.1:30141 就连不上了,得用你实际绑定的那个地址访问。

请求进门要过两道闸,顺序是先信源、后密码(proxy.ts):

规则不通过的后果
Host 白名单localhost*.localhost任意 IP 字面量直接放行;其余必须精确等于 PI_WEB_HOSTNAMEPI_WEB_ALLOWED_HOSTS 里的值403 {"error":"Untrusted API request"}
Origin / Sec-Fetch-Site只在请求带 OriginSec-Fetch-Site 时才检查cross-site 直接拒;带 Origin 就必须与 Host 同源403
密码见下页面请求 302/login;API 请求 401 {"error":"Authentication required"}

密码那层有两种凭证,任选其一即可

  1. Authorization: Basic base64("pi:<密码>") —— 用户名固定是 pi,脚本走这条。

  2. Cookie: pi-web-auth=<值> —— 浏览器登录后拿到,有效期 7 天。

免鉴权的路径只有三个/login/api/auth/web/login/api/auth/web/logout。其余全部要密码,包括 /api/auth/providers 这种看起来无关紧要的。

API 未授权时返回的 401 故意不带 WWW-Authenticate(源码注释说是为了避免 Chrome 弹出原生 Basic 对话框并扣住 fetch 响应)——所以浏览器不会弹框,只会看到 401。

二、外部脚本接入的正确姿势#

前提是知道启动 pi-web 时设的 PI_WEB_PASSWORD(它就是普通环境变量,怎么存取决于你怎么起服务;别把它写进任何会被别人读到的地方)。然后记住四条

  1. Host 要对得上:连 IP 字面量随便连;用域名访问时该域名必须在 PI_WEB_ALLOWED_HOSTS 里。浏览器改不了 Host,但 curl -H 'Host: ...' 能——别乱改。

  2. 别带 Origin,也别带 Sec-Fetch-Site。带了就要和 Host 同源,否则 403。Node 的 fetchaxioscurl 默认都不带,直接用就对了——这就是"裸脚本天然免过第二闸"的原因。

  3. 写操作带 Content-Type: application/json,并且是合法 JSON。部分端点会强校验(不合法给 415),也有几个端点忘了 catch 非法 JSON,会返回 500 而不是 400。

  4. SSE 用 curl -N,反代还要关缓冲。

最省事的写法:

# 自己先 export 一下这两个变量,全文示例都用它们
export BASE=http://127.0.0.1:30141
export PW="<启动 pi-web 时设的 PI_WEB_PASSWORD>"

curl -s -u "pi:$PW" "$BASE/api/sessions" | head -c 300

Node 侧同理:

const r = await fetch(`${BASE}/api/sessions`, {
  headers: { Authorization: 'Basic ' + Buffer.from(`pi:${process.env.PI_WEB_PASSWORD}`).toString('base64') },

不用先换 cookie:脚本每次都做 Basic Auth 就够了。cookie 的唯一价值是让浏览器免输密码。

三、心智模型:47 个路由,7 组#

先把最容易混的四个区分清楚:

你想知道该调谁说明
历史上有哪些对话GET /api/sessions读磁盘,返回全部会话 + runningSessionIds。默认走 30 秒内存缓存
现在谁在跑(最省)GET /api/agent/running只读进程内注册表,不碰磁盘、不产生 agent 命令,适合 1–3 秒轮询
某个对话现在什么状态GET /api/sessions/[id]/state轻量;和 POST /api/agent/[id] {"type":"get_state"} 返回同一份结构
怎么给它发消息POST /api/agent/[id]{"type":"prompt","message":...}这是「把答案抛回对话」的唯一入口

七组的分工:

路由文件干什么
会话 sessions8读历史、看状态、重命名、删除、导出
对话 agent5新建会话、发命令、订阅实时流
鉴权 auth6provider 登录、API Key、网页登录
模型与资源7模型列表、models.json 读写、插件装卸、工具设置
文件与 Git8浏览/读写/上传文件、文件索引、Git 变更、worktree
技能与推送9skills 搜索/装/更新、Web Push、版本检查、home
子代理与信任4subagent profile CRUD、项目信任

四、端点速查表#

47 个路由文件、65 个方法导出(一个文件里 GET/POST/PATCH/DELETE 会有多个),下面按方法拆开列出。这些端点全部需要认证,故不再单列。加粗的是最常用的。

方法路径作用
GET/api/sessions列出全部会话(磁盘 + 运行中),带 runningSessionIds
GET/api/sessions/[id]单个会话详情:树、上下文、用量统计、活跃时长
PATCH/api/sessions/[id]重命名(body {name}
DELETE/api/sessions/[id]删除会话文件(不可撤销)
GET/api/sessions/[id]/state运行时状态(在跑吗、上下文占用多少)
GET/api/sessions/[id]/context分页取上下文消息(向上翻页用)
GET/api/sessions/[id]/export导出成自包含单文件 HTML
POST/api/sessions/[id]/auto-name用模型生成会话标题(会真调一次模型)
GET/api/sessions/[id]/entries/[entryId]/thinking懒加载某个 thinking 块
GET/api/sessions/[id]/entries/[entryId]/tool-result-image懒加载工具结果里的图片
POST/api/agent/new新建会话并下发第一条消息
POST/api/agent/[id]给已存在的会话发命令(prompt / abort / set_model / fork …)
GET/api/agent/[id]只读状态快照,不启动会话
GET/api/agent/[id]/eventsSSE 实时事件流,会冷启动会话
GET/api/agent/running正在跑的会话 id 快照
GET/api/agent/[id]/bash-output读 bash 长输出的临时文件
GET/api/auth/providers可登录 provider 清单(OAuth / API Key)
GET/api/auth/login/[provider]启动 OAuth 登录(SSE 推交互步骤)
POST/api/auth/login/[provider]回填授权码(body {token,code}
POST/api/auth/logout/[provider]删 OAuth 凭据
POST/api/auth/api-key/[provider]存 API Key(body {apiKey}
DELETE/api/auth/api-key/[provider]删 API Key
POST/api/auth/web/login用密码换 7 天 cookie
POST/api/auth/web/logout清 cookie
GET/api/models按 cwd 列可用模型 + thinking 档位
GET/api/models-config明文models.json
PUT/api/models-config全量覆盖 models.json
GET/api/models-config/catalog从 models.dev 搜模型元数据与价格
POST/api/models-config/discover问上游「有哪些模型」
POST/api/models-config/test发一条真实请求测模型通不通
GET/api/plugins列插件包与加载出的资源
POST/api/plugins装/卸/更新/启停插件包
GET/api/tools/settings是否 Windows / PowerShell 工具开关
PUT/api/tools/settings切 PowerShell 工具(仅 Windows
GET/api/files/[...path]列目录/读文件/下载/预览/监听(?type=
POST/api/files/[...path]上传 / 上传冲突预检
GET/api/file-index文件模糊搜索(@ 补全用)
GET/api/cwd/browse列子目录(目录选择器)
POST/api/cwd/validate校验目录并授权
POST/api/default-cwd建并返回 ~/pi-cwd-YYYYMMDD
GET/api/git/status变更文件 + 增删行数
GET/api/git/diff单文件 unified diff
GET/api/worktrees列 worktree
POST/api/worktrees新建 worktree
DELETE/api/worktrees删除 worktree(body 传参)
GET/api/skills列某 cwd 下可用的全部 skill
PATCH/api/skills切 skill 的 disable-model-invocation
POST/api/skills/check检查 skill 有无新版本
POST/api/skills/install装 skill(底层 npx skills add
POST/api/skills/search搜 skills 市场
POST/api/skills/update更新已装 skill
GET/api/push/config取 Web Push 公钥
POST/api/push/subscribe登记浏览器推送订阅
GET/api/app-update查 pi-web 自身有无新版本
GET/api/home服务端的 home 目录
GET/api/subagents/[id]子代理运行元信息
POST/api/subagents/[id]steer / abort 子代理
GET/api/subagents/profiles列全部子代理 profile
PUT/api/subagents/profiles新建/覆盖 profile
PATCH/api/subagents/profiles切 profile 的 enabled
DELETE/api/subagents/profiles删 profile(body 传参)
GET/api/subagents/settings内置子代理扩展开关(本版本写死 false
PUT/api/subagents/settings写那个开关
GET/api/project-trust查某目录是否需要/已获得信任
POST/api/project-trust信任某项目(会销毁该 cwd 的运行时会话)

五、读会话:sessions 组#

列表与字段#

curl -s -u "pi:$PW" "$BASE/api/sessions"

返回 { sessions, runningSessionIds, completionNotificationSuppressedSessionIds }。它的规模可以很大——在会话多的实例上,几百条会话、几十个 cwd 目录是常态。

每个 session 的关键字段:

  • id / path / cwd —— 会话 id、jsonl 绝对路径、工作目录("哪个文件夹"就是这个)

  • name? —— 改过名才有这个字段

  • created / modified —— 列表按 modified 降序

  • messageCount —— 累计消息数,含已被压缩掉的历史

  • firstMessage —— 第一条 user 消息,没有就是 "(no messages)"

  • projectRoot? / projectKey? / branch? / isWorktree? —— 由服务端调 git 补出来的项目信息(这也是列表有缓存的原因)

  • parentSessionId? / relation? —— fork 或 subagent 关系

  • transient? —— 只存在于内存、jsonl 还没落盘

force=1 会绕过缓存:实测这个参数要花约 10 秒(要重扫全部会话文件,还带 git 调用),平时别加。

单个会话#

curl -s -u "pi:$PW" "$BASE/api/sessions/$ID?tail=20&deferThinking&deferMedia"

返回 { sessionId, filePath, info, leafId, tree, context, stats, totalActiveMs, toolNames? }

  • tail 默认 50、上限 1000

  • deferThinking / deferMedia 把 thinking 块和图片换成懒加载(正文分别走那两条 entries/... 路由)

  • contextentryIdsmessages 一一对应;model / thinkingLevel 是沿 parentId 往上找最近的变更,也就是当前生效值

  • 注意 info.messageCount大于 context.messages.length(前者是全部 entries 的累计,含被压缩掉的历史)

状态:判断"在不在干活"#

curl -s -u "pi:$PW" "$BASE/api/sessions/$ID/state"

在跑 → {"running":true,"state":{...}};不在跑但磁盘上有 → {"running":false}(200,不是 404)。

state 里真正有用的:

  • isStreaming / isPromptRunning / isBashRunning / isCompacting

  • contextUsage: {percent, contextWindow, tokens} —— "上下文快满了"就看这个,实测常见形态是 {"percent":28.47,"contextWindow":262144,"tokens":74639}

  • queuedMessagespendingMessageCountextensionStatuses

一个坑state.messageCount 在当前实现里硬编码为 0,别拿它计数(计数用 stats.totalMessages)。

这条路由不会重置空闲计时器——所以轮询它不会把会话"叫醒",可以放心用。

其他#

  • GET .../context?tail=&before=&leafId=:向上翻页。hasMore:false 表示爬到顶了。

  • GET .../export:导出自包含单文件 HTML,实测最小会话也要 2 秒出 270KB。这个端点做了件很细的事——导出模板里有三个递归函数,深线性会话会爆栈,所以它用"必须恰好命中 1 次"的严格替换把递归改成迭代。

  • PATCH .../{id} body {"name":"..."}:重命名,追加一条 session_info,不落 SQL。注意它只认磁盘上的会话,不认还没落盘的 transient 会话。

  • DELETE .../{id}:删 jsonl + 关运行时会话。它会顺手把同目录下指向它的子会话重挂到它的父会话上——别的目录下的子会话不会被重挂,这是真实边界。

  • POST .../auto-name:会真调一次模型生成标题;会话没在跑会先把它拉起来

六、接管对话:agent 组(DIY 的核心)#

新建一个会话并下发任务#

curl -s -u "pi:$PW" -X POST "$BASE/api/agent/new" \
  -H 'Content-Type: application/json' \
  -d '{"cwd":"/path/to/your/project","type":"prompt","message":"先读 README 再总结",
       "provider":"deepseek","modelId":"deepseek-v4-flash","thinkingLevel":"low"}'
  • cwd 必填(只检查存在),type 实际必填("ensure_session" 只建会话不下发消息)

  • provider + modelId 必须成对,给一半会 500

  • toolNames 只接受内置白名单 bash|read|edit|write|grep|find|ls|powershell;传 [] 会走 chat-only 分支

  • 返回 {success:true, sessionId, data, model, thinkingLevel}

实现上的小细节:它用 __new__<uuid> 当临时键,而没用时间戳——因为同键的并发调用会被合并成同一个会话,毫秒级会撞车。

给已存在的会话发命令#

curl -s -u "pi:$PW" -X POST "$BASE/api/agent/$SID" \
  -H 'Content-Type: application/json' -d '{"type":"prompt","message":"用方案 A,继续"}'

这是把答案抛回对话的入口。 type 支持的完整命令表:

type关键字段说明
promptmessageimages?streamingBehavior?下发消息,受理即返回,不等跑完
steer / follow_upmessageimages?流式过程中的引导 / 排队
abort中止
get_state取状态快照
get_last_assistant_text取最后一段助手回复
set_modelprovidermodelId换模型
set_toolstoolNames换工具(会话在跑时不允许,会重建会话)
set_thinking_levellevel换思考档位
set_session_namename改名
compact / abort_compaction压缩上下文
fork / cloneentryId?分支
navigate_treeentryIdsummarize?切分支
bash / abort_bashcommandexcludeFromContext?直接跑 shell
clear_queueget_toolsget_commandsreload杂项

要点:

  • prompt 立刻返回 null,只代表"受理了"。真正的输出要走 SSE。

  • 会话不在内存里但磁盘上有时,这个接口会冷启动它再发命令——也就是能"复活"旧对话。

  • 会话正忙时 prompt 会被拒(Cannot send a prompt while a shell command is running)。

  • 图片最多 10 张、每张 ≤10MB base64。

实时流#

timeout 5 curl -s -N -u "pi:$PW" "$BASE/api/agent/$SID/events"
  • 全是 data: 行,没有 event:,类型在 JSON 的 type 字段里

  • 固定先发 {"type":"connected","sessionId":...},就绪前到达的事件会先缓冲再回放

  • 30 秒一次心跳注释帧

  • turn_start/turn_end 被丢弃,agent_end 被裁剪成只剩 type,message_update 去掉了累积快照字段

  • 重要副作用:这条路由会冷启动会话。打开一个旧会话的 SSE 等于把它唤醒。

  • 会话文件找不到时返回的是 404 纯文本 Session not found,不是 JSON

浏览器里 EventSource 不能带自定义头,所以浏览器侧只能靠 cookie。

最便宜的全局轮询#

curl -s -u "pi:$PW" "$BASE/api/agent/running"
# {"runningSessionIds":["<sessionId>"],"completionNotificationSuppressedSessionIds":[]}

判据是进程内的:alive && (有 pending prompt || streaming || compacting || bash 在跑)

代价要记住:它只知道"进程活着的会话"。pi-web 重启后这些 id 全空——因为重启后没有任何会话被唤醒,磁盘上的历史会话一个都不在里面。

七、文件、Git 与工作目录#

files 的路径语义(最容易踩)#

/api/files/[...path] 的 segments 拼起来就是去掉开头斜杠的绝对路径

curl -s -u "pi:$PW" "$BASE/api/files/path/to/your/project/lib?type=list"
#                              ^^^^^^^^^^^^^^^^^^^^ 这里对应 /path/to/your/project/lib

所以 /api/files/lib 指的是根目录下的 /lib,不是"相对某个 cwd"。

type 取值:list(默认)/ read / download / meta / preview / watch(SSE 监听文件变化)。

  • 文本读取上限 256KB,图片 10MB,超限 413

  • 图片/音频/PDF 走原始字节流,不是 JSON

  • .svg 额外加了 CSP(防同源脚本执行)

  • watch 监听的是父目录并按路径过滤,还会比较 mtime/ctime/ino/size 丢弃纯读取产生的事件,避免自己刷新自己

访问控制是"允许根"白名单:所有历史会话的 cwdprojectRoot,加上 ~/pi-cwd-YYYYMMDD,加上运行时被授权的目录。不在里面 → 403 Access denied。路径检查做两次:先词法,再对 realpath 检查一次——软链接躲不过第二次

上传:POST ?type=upload,字段名固定 files,单文件 25MB、合计 100MB;撞名用 conflict=error|overwrite|skip 控制;有部分失败时返回 207

工作目录#

  • POST /api/cwd/validate body {"cwd":"..."} —— 校验目录,并且把这个目录加进允许根(这就是"授权"这一步本身)。返回 projectRoot / projectKey

  • POST /api/default-cwd —— 建 ~/pi-cwd-YYYYMMDD 并返回。日期取的是 UTC,所以跨时区的深夜会拿到"昨天/明天"的目录名。

  • GET /api/file-index?cwd=&q= —— 模糊搜索。排序在完整列表上做、截断在匹配之后,所以仓库再大也能搜到深处的文件。git 仓库走 git ls-files,非 git 退化为 BFS。

Git#

curl -s -u "pi:$PW" "$BASE/api/git/status?cwd=/path/to/your/project"
curl -s -u "pi:$PW" "$BASE/api/git/diff?cwd=...&path=<绝对路径>"
  • status 对非 git 目录不是错误:返回 isGitRepository:false + 空数组(200)。只看落在 cwd 子树内的变更。

  • diff 对"没变更的文件"返回 {"supported":false}(200),不是错误。未跟踪文件的 patch 是服务端合成的。

worktree#

GET /api/worktrees?cwd= 会解析项目 + 列出 worktree;POST/DELETE 建删。删除时如果工作区脏,会返回 409 + dirty:true,让你确认是否强制删。

八、模型、插件、技能#

模型列表#

curl -s -u "pi:$PW" "$BASE/api/models?cwd=/path/to/your/project"

返回 models / modelList / defaultModel / thinkingLevels 等。cwd 必须在允许根里,问 /etc 会 403。5 分钟缓存,加载失败不报 500,而是返回空列表 + modelError

models.json 读写#

curl -s -u "pi:$PW" "$BASE/api/models-config"          # 读
curl -s -u "pi:$PW" -X PUT -H 'content-type: application/json' \
     --data @/tmp/models.json "$BASE/api/models-config"  # 覆盖写

两条要命的

  1. GETapiKey 明文返回,不脱敏。这不是理论——它真的会把配置里每个 provider 的 key 裸着回给你,所以别把这个响应贴到任何地方。

  2. PUT整文件替换,不是合并。正确姿势永远是 GET → 改 → PUT 回去,否则没带上来的 provider 直接没了。写盘是原子写 + 权限 0600。

配套三条:catalog 从 models.dev 搜模型给价格建议(1 小时缓存,上游挂了回退旧值);discover 拿 baseUrl + key 问上游有哪些模型;test 发一条真实请求测通不通——test 不改你的真实配置,它会在临时目录写一份只含该模型的 models.json 再 boot 一次。

插件#

GET /api/plugins?cwd= 列包与资源、POST 装卸更新。disable/enable 只改 settings.jsoninstall/update真的联网拉 npm/git。

技能#

GET /api/skills?cwd= 列的是"这个 cwd 下实际可用的全部 skill"(复用 SDK 加载器,不是扫目录)。

PATCH /api/skills 就是那条"关 skill"操作的 HTTP 化,body {filePath, disableModelInvocation}——按键存在与否精准改行,不会重写整个 YAML,正文里出现的同名字样不受影响。

install / update 底层都是 npx skills add ... --agent pi,超时 60 秒,不经过 shell。search 先试 HTTP API,失败回落 npx skills find

实测一个反直觉的现状check/update 这两个接口实际可用数可能是 0。它们要求 skill 的锁文件里同时有 sourceType=githubskillPathversionHash 这几项,而很多 skill 是通过 well-known 方式或符号链接装进来的,不满足条件——实测一批 42 个 skill 里只有 1 个带 install 元信息,而它偏偏 canCheckForUpdates:false。用之前先用 GET /api/skills 看一眼有没有带 install 的。

九、鉴权、推送与子代理#

登录#

GET /api/auth/providers 列出可登录的 provider(实测常见形态:7 个左右 OAuth provider,loggedIn 多为 false)。

OAuth 是两段式,不是轮询

EventSource GET /api/auth/login/<provider>
   ← SSE {"type":"auth", url, instructions, token}
   浏览器打开 url 授权
   → POST /api/auth/login/<provider> {token, code}
   ← SSE {"type":"success"} 或 {"type":"error"}

token 是进程内存注册表里的键,Next.js 重启即失效。而且 POST 返回 ok 只代表"回填成功",不代表登录成功——成功信号只从 SSE 的 success 来。

POST/DELETE /api/auth/api-key/[provider] 存删 API Key,两个方向都是类型安全的(OAuth 凭据不会被误删,反之亦然),类型不匹配给 409。

Web Push#

GET /api/push/config 给 VAPID 公钥(首次调用会现场生成密钥对并落盘到 ~/.pi/agent/web-push.json,权限 600,私钥永不出服务端)。POST /api/push/subscribe 登记订阅,按 endpoint 去重 upsert,所以前端每次加载都能放心重发。

子代理#

GET /api/subagents/profiles?cwd= 列全部 profile(含被遮蔽的),PUT/PATCH/DELETE 管理(DELETE 也要带 body)。tools 字段会被白名单过滤,只留 7 个内置工具名,其余静默丢弃

一个值得知道的现状GET/PUT /api/subagents/settings 那个"内置子代理扩展"开关,在 0.8.11写死了 return false,测试里还明确断言"即使文件写了 true 也返回 false"。所以内置子代理扩展当前处于关闭状态,/api/subagents/[id] 基本只能查到历史 run(或者直接 404)。

项目信任#

GET /api/project-trust?cwd= 查某个目录是否需要信任。它读的是 ~/.pi/agent/trust.json,结构是"绝对路径 → true/false/null",查找是就近祖先语义——给父目录记 true 会连带信任所有子目录。

POST 信任某项目时有副作用:写 trust.json 之后会销毁该 cwd 上所有运行中的会话,让它们下次以"已信任"重新加载资源。它还要求该 cwd 上没有 busy 的会话,否则 409。

十、DIY 配方#

1. 一眼看全部对话 + 谁在跑#

# 注意 `. as $s` 那一行不能省:jq 里 index() 的参数是在管道左侧的输入上求值,
# 直接写 index(.id) 会去 runningSessionIds 自己身上找 .id,恒为 null
curl -s -u "pi:$PW" "$BASE/api/sessions" \
  | jq -r '. as $d | $d.sessions | sort_by(.modified) | reverse | .[:15][]
           | . as $s
           | "\(if ($d.runningSessionIds | index($s.id)) then "▶ 跑" else "  停" end)  \($s.cwd)  \($s.messageCount)条  \($s.firstMessage[:30] | gsub("\n";" "))"'

输出长这样:

▶ 跑  /path/to/project-a  106条  先读一下 README 再总结这个项目
  停  /path/to/project-b   79条  帮我查一下这个报错是怎么来的

2. 粗判"哪些对话在等我"#

纯结构化信号,不用 AI:最后一条是 user 消息,就说明这个对话在等你。会话 jsonl 里 messages 有 role,取最后一条判断即可。

遍历 ~/.pi/agent/sessions/**/*.jsonl,读最后一条 message 的 role:
  role == "user"        → 在等你
  assistant / toolResult → 跑完了,或还在干活

这条判据不需要模型、不会猜错,但只能给出"可能",不能给出"完成/未完成"——因为 pi 的会话记录里根本没有结构化的完成状态

3. 把答案抛回某个对话#

SID=<你的会话 id>
curl -s -u "pi:$PW" -X POST "$BASE/api/agent/$SID" \
  -H 'Content-Type: application/json' \
  -d '{"type":"prompt","message":"按方案 B 来,改完跑测试"}'

发完想盯结果就接 SSE:

curl -s -N -u "pi:$PW" "$BASE/api/agent/$SID/events" | grep --line-buffered '"type":"agent_end"'

4. 新开一个无人值守的会话#

curl -s -u "pi:$PW" -X POST "$BASE/api/agent/new" \
  -H 'Content-Type: application/json' \
  -d '{"cwd":"/path/to/your/project","type":"prompt","message":"跑一遍测试并报告失败项"}'

5. 把会话导出给别人看#

curl -s -u "pi:$PW" -OJ "$BASE/api/sessions/$SID/export"   # 得到自包含 HTML

单文件、带会话树和搜索,可以直接发给别人。

十一、坑与风险(写博客时顺手记下的)#

功能性的

  1. GET /api/sessions 默认 30 秒缓存;要最新得 force=1,但那个要花约 10 秒。

  2. state.messageCount 恒为 0,别用它。计数用 stats.totalMessages

  3. GET /api/agent/[id]/events 会冷启动会话——别拿它当"只订阅不打扰"的用法。

  4. 如果你用 --hostname 把它绑到了非回环地址(比如容器网桥),那 127.0.0.1:30141 就连不上,得用实际绑定的地址。

  5. 所有缓存(会话列表、模型列表、版本检查、登录 token、允许根)都在内存里,重启即清。别当持久状态用。

  6. PUT /api/models-config 是整文件覆盖,必须先 GET 后 PUT。

  7. 三个接口在"JSON 非法"时行为不一致:auth/loginauth/api-key 给 500,auth/web/login 给 400。

  8. POST /api/cwd/validatePOST /api/worktreesPOST /api/project-trust 都有副作用(授权目录 / 建目录 / 销毁会话),不是纯读。

安全面的(我自己看完最在意的三条)

  1. GET /api/models-config 明文返回所有 apiKey。任何能过认证的人(或任何拿到密码的脚本)都能一次性读走全部上游 key。

  2. GET /api/cwd/browse 没有允许根校验(它只过 Host + 密码那一层)。实测 ?path=/etc 能正常列出宿主机任意可读目录的目录名——拿不到文件内容(内容受 /api/files 白名单管),但足以摸清机器结构。

  3. 登录接口没有速率限制/api/auth/web/login 只做恒定时间比较,可以无限次试密码;cookie 也没设 Secure;登出只清客户端 cookie、不吊销旧 cookie(服务端无状态),想强吊销只能改密码。

结论很简单:这东西是本地高权限工具,不要裸露到公网。要走外网就套 HTTPS 反代 + 额外限速,或者干脆只对着 VPN 开。

结语#

47 个路由里,真正撑起"把我的对话调度起来"这件事的只有四个:GET /api/sessions(有哪些对话)、GET /api/agent/running(谁在跑)、GET /api/sessions/[id]/state(它什么状态)、POST /api/agent/[id](把话递回去)。

剩下的都是配套。摸清这四个,再往上搭自己的面板就是拼装了——反而"这个对话到底完成没有"这个判断,pi-web 这条链路上给不了你,因为 jsonl 里从来没有这个字段。那得靠别的手段。

讨论 0

还没有人说话。

    登录登录后可以参与讨论