背景 链接到标题
Pi Coding Agent 是一个开源的 AI 编程助手,69K+ Stars,MIT 协议。它的架构分三层:
- @earendil-works/pi-ai — 统一多 Provider LLM API
- @earendil-works/pi-agent-core — Agent 运行时(Tool calling、状态管理)
- @earendil-works/pi-coding-agent — CLI + SDK
Pi Web 是社区开发的 Web UI,1.1K Stars。它通过 Pi SDK 直接创建 AgentSession,通过 SSE 推送流式事件到浏览器,不跑 CLI 子进程。
同类项目还有 jmfederico/pi-web(支持远程 Machine/Fleet)和 pi-gui(Electron 桌面版)。
部署 链接到标题
环境要求:Node.js 22+。
git clone https://github.com/agegr/pi-web.git
cd pi-web
npm install
npm run dev
默认运行在 http://localhost:30141。
配置 DeepSeek 模型 链接到标题
Pi 内置了 DeepSeek Provider,模型定义在 deepseek.models.ts,包含 deepseek-v4-flash 和 deepseek-v4-pro。
认证 链接到标题
在 ~/.pi/agent/auth.json 中写入 API Key:
{
"deepseek": {
"type": "api_key",
"key": "sk-你的key"
}
}
Pi 也支持环境变量 DEEPSEEK_API_KEY,但 auth.json 更持久。
设为默认模型 链接到标题
~/.pi/agent/settings.json:
{
"defaultProvider": "deepseek",
"defaultModel": "deepseek-v4-flash"
}
重启后新建 Session 自动使用 DeepSeek V4 Flash。
自定义扩展 链接到标题
Pi 的扩展机制允许注册自定义 Tool,LLM 可以在对话中直接调用。扩展是 TypeScript 文件,通过 pi.registerTool() 注册。
扩展目录结构 链接到标题
项目级扩展放在 .pi/extensions/ 下,Pi SDK 的 DefaultResourceLoader 会根据 cwd 自动发现。
项目根目录/
└── .pi/
└── extensions/
└── my-extension/
├── index.ts # 入口,注册 Tool
├── helper.ts # 辅助模块
└── package.json # 可选,npm 依赖
注册 Tool 示例 链接到标题
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { Type } from "typebox";
export default function (pi: ExtensionAPI) {
pi.registerTool({
name: "my_tool",
label: "我的工具",
description: "工具描述,LLM 据此决定何时调用",
parameters: Type.Object({
query: Type.String({ description: "搜索关键词" }),
topK: Type.Optional(Type.Number({ default: 5 })),
}),
async execute(_toolCallId, params, signal) {
const result = await fetch("http://api.example.com/search", {
signal, // 支持取消
});
return {
content: [{ type: "text", text: JSON.stringify(result) }],
details: {},
};
},
});
}
关键点:
parameters用typebox定义 Schema,LLM 据此生成参数execute接收signal参数,支持 Abort- 返回
{ content, details },content给 LLM 阅读 description用中文,LLM 能理解
实战:封装 Windmill API 链接到标题
现有 bash 脚本通过 curl 调用 Windmill 异步 Job:
# bin/qdrant-search
JOB_ID=$(curl -s -X POST "$WINDMILL_URL/api/w/default/jobs/run/p/$SCRIPT" ...)
for i in $(seq 1 60); do
RESULT=$(curl -s "$WINDMILL_URL/api/w/default/jobs/completed/get/$JOB_ID" ...)
...
sleep 1
done
改为 TypeScript 扩展后,用 fetch 替代 curl,直接内联调用:
async function runWindmillScript(
script: string,
params: Record<string, unknown>,
signal?: AbortSignal
): Promise<unknown> {
const token = getWindmillToken();
// 提交 Job
const resp = await fetch(`${WINDMILL_URL}/api/w/default/jobs/run/p/${script}`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify(params),
});
const jobId = (await resp.text()).trim();
// 轮询结果
for (let i = 0; i < 60; i++) {
if (signal?.aborted) throw new Error("Cancelled");
const poll = await fetch(`${WINDMILL_URL}/api/w/default/jobs/completed/get/${jobId}`, {
headers: { Authorization: `Bearer ${token}` },
});
if (poll.status === 404) { // Job 未完成
await new Promise(r => setTimeout(r, 1000));
continue;
}
const data = await poll.json();
if (data?.success === true) return data.result;
if (data?.success === false) throw new Error(data.result?.error?.message);
}
throw new Error("Timeout");
}
踩坑记录:
- Windmill 提交 Job 返回裸 UUID,不是 JSON 字符串。用
resp.text()而非resp.json() - Job 未完成时返回 404,不是
{ success: false }。需要把 404 当作"继续等待"而非错误 - cwd 匹配问题:项目级
.pi/extensions/只在cwd为项目根时自动发现。如果 Web UI 的默认工作目录不是项目根,需要把扩展路径配到全局settings.json的extensions字段,或修改 Web UI 的默认 cwd
完整示例 链接到标题
注册了 4 个教材搜索工具:
| Tool 名 | 功能 | 参数 |
|---|---|---|
qdrant_search |
语义搜索(向量检索) | query, topK?, bookName? |
es_search |
全文检索(关键词精确匹配) | query, topK?, bookName? |
pageindex_content |
获取指定页码文本 | bookId, pages |
pageindex_structure |
获取章节树结构 | bookId |
工作原理 链接到标题
浏览器 UI (React)
↕ HTTP + SSE
Next.js API Routes
↕ Pi SDK (createAgentSession)
AgentSession (Tool calling, 状态管理)
↕ fetch()
Windmill Server / 外部 API
Pi Web 不跑 CLI 子进程,直接通过 @earendil-works/pi-coding-agent 的 createAgentSession 创建 Session,通过 session.subscribe() 接收流式事件,通过 SSE 推送到浏览器。