基于 Pi,我居然自己搞了一个好用的 AI Agent 桌面工作台
åºäº Piï¼æå± ç¶èªå·±æäºä¸ä¸ªå¥½ç¨ç AI Agent æ¡é¢å·¥ä½å°
å 广¯æ± 2026-08-19 0 é 读7åéåå¨åé¢ï¼è¿ç¯æç« 䏿¯ä»ä¹åä¸è½¯æï¼å°±æ¯æä¸ªäººæè ¾çä¸ä¸ªè®°å½ãèµ·å å¾ç®åââå¸é¢ä¸ç AI ç¼ç¨å·¥å ·è¦ä¹æ¯äºç«¯é»çï¼è¦ä¹æ³æ¹ç¹ä¸è¥¿é½æ ä»ä¸æãæä»¥æå°±æ³ï¼è½ä¸è½åºäºä¸ä¸ªå¼æºç agent çæï¼èªå·±æä¸ä¸ªæ¬å°ä¼å ãçå¾è§æ¸å¾çç Agent å·¥ä½å°ï¼äºæ¯å°±æäºä¸é¢è¿ä¸ªä¸è¥¿ã
å®ç½ï¼tether-code.xyz
å 说ç»è®ºï¼å«éå¤é è½®å
å¾å¤äººä¸å¬"èªå·±å¼å Agent"ï¼ç¬¬ä¸ååºæ¯"é£ä¸å¾ä»æ¨¡åè°ç¨ãæ¶æ¯å¾ªç¯ãå·¥å ·åè®®ãæéç³»ç»ä¸è·¯æåï¼"ã
å
¶å®ä¸ç¨ãAgent é¢åè¿äºå¹´æ²æ·ä¸æ¥ä¸æ¹ä¸éç弿ºçæï¼æ¯å¦ Piï¼@earendil-works/pi-* é£å¥ npm å
ï¼ã宿 agent 循ç¯ã模ååè®®ãcoding-agent æ©å±ãRPCãTUI ç»ä»¶è¿äºé½å好äºã
æçæè·¯å¾ç®åï¼ç«å¨ Pi çè©èä¸ï¼æç²¾åè±å¨äº§åè¾¹çåä½éªä¸ï¼è䏿¯éæ°åæè½®åã
æç»ååºæ¥çä¸è¥¿å« Tetherââä¸ä¸ª Electron æ¡é¢åºç¨ï¼æ¨¡åè°ç¨ãå·¥ä½åºå·¥å ·ãç»ç«¯å½ä»¤ãæéæç¤ºãä¼è¯åå²ãdiff 审æ¥ï¼å ¨é¨æ¶æå¨ä¸ä¸ªæ¬å°å·¥ä½å°éãä¼è¯åé ç½®æ°æ®é½å¨ä½ æºå¨ä¸ï¼æ¨¡å请æ±ç´è¿ä½ é ç½®ç providerï¼ä¸é´æ²¡æä»»ä½ä¸è½¬æå¡ã
æ¶æé¿ä»ä¹æ ·
ä¸å¥è¯æ»ç»æ¶æï¼
React Rendererï¼ä¼è¯ / diff / 设置 UIï¼
â contextBridge / Electron IPC
â¼
Electron Mainï¼çªå£ãå·¥ä½åºãåæ®ãAgent è¿ç¨å®¿ä¸»ï¼
â JSON-RPC over stdio
â¼
tether-agent-coreï¼æéãæ²ç®±ãå·¥å
·ãcheckpointãMCPãä¼è¯ï¼
â
â¼
Pi çæï¼agent loop · 模ååè®® · coding-agent æ©å± · RPC · TUIï¼
è¿éæä¸ªå ³é®è®¾è®¡ï¼æ¸²æè¿ç¨æ²¡æä»»ä½ Node.js æéï¼æ¡é¢è½åå ¨é¨èµ°ç±»ååç IPC å¥çº¦ï¼Agent è·å¨ç¬ç«åè¿ç¨éï¼å´©æºäºä¼è¯æä»¶è¿å¨ç£çä¸ï¼å¯ä»¥ç»§ç»å½å¯¹è¯æ¢å¤ï¼ä½ç»ä¸éé»éæ¾æ²¡è·å®çå½ä»¤ã
ç±»ååç IPC å¥çº¦ï¼æ¯æ³è±¡ä¸éè¦
Electron éæå¸¸è§çå®å
¨ç¿»è½¦å°±æ¯ contextBridge æ´é²äºä¸ªå¤§èå
¨ç对象ãæè¿è¾¹åè¿æ¥ââå
å¨ src/shared/types.ts éå®ä¹å¥½æ´ä¸ª DesktopApi æ¥å£ï¼preload åªæ¯å®çä¸ä¸ªæºæ¢°å®ç°ï¼
// src/shared/types.tsï¼èéï¼
export interface DesktopApi {
workspace: {
choose(): Promise<string | null>;
recent(): Promise<WorkspaceItem[]>;
read(path: string, cwd?: string): Promise<{ path: string; content: string; binary: boolean }>;
open(path: string, cwd?: string): Promise<void>;
list(cwd?: string): Promise<string[]>;
restore(files: Array<{ path: string; content: string | null; mode?: number }>, cwd?: string): Promise<{ restored: string[] }>;
onChanged(listener: (root: string) => void): () => void;
};
agent: {
start(options: AgentStartOptions): Promise<AgentSnapshot>;
stop(): Promise<void>;
command<T = unknown>(type: string, data?: Record<string, unknown>): Promise<T>;
onEvent(listener: (event: AgentEvent) => void): () => void;
};
}
preload å±å°±æ¯çº¯ç¿»è¯ï¼ä¸æºä»»ä½é»è¾ï¼
// src/preload/index.tsï¼èéï¼
import { contextBridge, ipcRenderer } from "electron";
import type { AgentEvent, DesktopApi } from "../shared/types";
function subscribe<T>(channel: string, listener: (payload: T) => void): () => void {
const handler = (_event: Electron.IpcRendererEvent, payload: T) => listener(payload);
ipcRenderer.on(channel, handler);
return () => ipcRenderer.removeListener(channel, handler);
}
const api: DesktopApi = {
platform: process.platform,
workspace: {
choose: () => ipcRenderer.invoke("workspace:choose"),
recent: () => ipcRenderer.invoke("workspace:recent"),
read: (filePath, cwd) => ipcRenderer.invoke("workspace:read", filePath, cwd),
restore: (files, cwd) => ipcRenderer.invoke("workspace:restore", files, cwd),
onChanged: (listener) => subscribe<string>("workspace:changed", listener),
},
agent: {
start: (options) => ipcRenderer.invoke("agent:start", options),
stop: () => ipcRenderer.invoke("agent:stop"),
command: (type, data) => ipcRenderer.invoke("agent:command", type, data),
onEvent: (listener) => subscribe<AgentEvent>("agent:event", listener),
},
};
contextBridge.exposeInMainWorld("harness", api);
好å¤å¾ææ¾ï¼æ¸²æè¿ç¨æ³ç¢°æä»¶ç³»ç»ï¼DesktopApi éæ²¡è¿ä¸ªæ¹æ³ï¼ç¼è¯å¨ç´æ¥æ¥éãæ³ç»è¿ï¼contextIsolation å¼çï¼nodeIntegration å
³çï¼æ è·¯å¯èµ°ãå®å
¨è¾¹ç䏿¯é èªè§ï¼æ¯é ç±»åå®ä¹ã
Agent è¿ç¨å®¿ä¸»ï¼ä¸ä¸ªå¸¦è¶ æ¶ç JSON-RPC 客æ·ç«¯
主è¿ç¨éæåäºä¸ª AgentHostï¼è´è´£ spawn agent åè¿ç¨ãæè¡è§£æ JSON-RPCãå¤çè¶
æ¶ï¼
// src/main/agent-host.tsï¼èéï¼
const DEFAULT_RPC_TIMEOUT_MS = 45_000;
const LONG_RPC_TIMEOUT_MS = 30 * 60_000;
const LONG_RUNNING_REQUESTS = new Set([
"prompt", "steer", "abort", "get_entries",
"get_fork_messages", "get_messages", "get_session_stats", "fork", "compact",
]);
export class AgentHost {
private child?: ChildProcessWithoutNullStreams;
private pending = new Map<string, PendingRequest>();
async start(options: AgentStartOptions & { cwd: string }): Promise<AgentSnapshot> {
await this.stop();
const args = [
getTetherRpcEntryPath(),
"--mode", "rpc",
"--provider", options.provider,
"--permission", options.permission,
"--sandbox", options.sandbox,
];
if (options.network) args.push("--network");
// spawn åè¿ç¨ï¼å¼å§çå¬ stdout è¡
}
async snapshot(): Promise<AgentSnapshot> {
const [state, messages, models, thinkingLevels, stats, commands] = await Promise.all([
this.request("get_state"),
this.request("get_messages"),
this.request("get_available_models"),
this.request("get_available_thinking_levels"),
this.request("get_session_stats").catch(() => undefined),
this.request("get_commands").catch(() => ({ commands: [] })),
]);
return { state, messages, models, thinkingLevels, stats, skills: parseSkillCommands(commands.commands) };
}
}
æä¸ªç»èï¼promptãforkãcompact è¿ç±»è¯·æ±å¨è¾è·å åéï¼æ®éè¯·æ± 45 ç§è¶
æ¶ãæä»¥æäºä¸ªç½ååââé¿ä»»å¡ç¨ 30 åéè¶
æ¶ï¼å
¶ä½ä¿æçè¶
æ¶å¿«é失败ãå«å°çè¿ä¸ªï¼æ²¡æè¶
æ¶æ§å¶ç RPC 客æ·ç«¯ï¼æä¸ä¸ªè¯·æ±ï¼æ´ä¸ª UI é½å¾è·ç塿»ã
æé模åï¼plan / ask / auto / full
Agent è½åæä»¶ãè½è·å½ä»¤ï¼æéè¾¹çå¿ é¡»æ¾å¼ãå个模å¼ï¼
| æ¨¡å¼ | è¡ä¸º |
|---|---|
plan | åªè¯»åæåè§åï¼è¯æå½ä»¤è·å¨åªè¯»æ²ç®± |
ask | åæä½ãç½ç»è®¿é®ãè¾¹çå级ä¹åå è¯¢é® |
auto | 常è§å·¥ä½åºæä½èªå¨æ§è¡ï¼å级æ¶è¯¢é® |
full | æ¾å¼ä¿¡ä»»ç项ç®å ³éå·¥ä½åºæ²ç®± |
åºå±èµ° macOS Seatbelt åæ²ç®±ï¼Windows ä¾§æä¸ä¸ªå®éªæ§çæ²ç®±è¾ å©ï¼è¦åç¬å®è£ å¯ç¨ï¼ãæ²ç®±æ¯çºµæ·±é²å¾¡ï¼ä¸æ¯è®©ä½ ä¸çå½ä»¤ççç±ââå¨éçä»åºé该审çè¿å¾å®¡ã
è¡¥ä¸ checkpointï¼è®© /undo ççè½åæ»
AI æ¹ä»£ç ææçå°±æ¯"宿æä»¶æ¹åäº"ãæçæ¹æ¡æ¯ï¼æææä»¶åæ´é½èµ°è¡¥ä¸ï¼patchï¼å½¢å¼ï¼æ¯æ¬¡åå
¥æä¸ä¸ª checkpointï¼UI é䏿¡ /undo å°±è½æ¢å¤ä¸ä¸è½®çæ¹å¨ã
// src/renderer/conversation.tsï¼èéï¼
export interface RestoreFile {
path: string;
content: string | null; // null 表示å é¤
}
export function lastTurnRestoreFiles(entries: SessionEntryLike[]): RestoreFile[] {
// ä»ä¼è¯æ¡ç®éæ¾åºä¸ä¸è½®ææç checkpointï¼ç»è£
æå¯æ¢å¤æä»¶å表
}
export function dropLastTurn(messages: ChatMessage[]): ChatMessage[] {
// æ¤æä¸ä¸è½®çæ¸²ææ¶æ¯ï¼é
å workspace.restore ä¸èµ·ç¨
}
é
å主è¿ç¨ç workspace:restore IPCï¼UI ä¸ç¹äºæ¤éï¼ä¸»è¿ç¨ææä»¶ååç£çãè¿ä¸ªæºå¶çèµ·æ¥ä¸èµ·ç¼ï¼å®é
ç¨èµ·æ¥æå½ââå°¤å
¶å½ä½ 让 agent éæå®ä¸ä¸ªå¤§æä»¶è§å¾ä¸å¯¹çæ¶åã
ä¼è¯æ°æ®ï¼çº¯æ¬å°ãå¯ç»§ç»
æææ°æ®é½è½å¨ ~/.tether ä¸ï¼æ 饿µãæ ä¸è½¬ãå´©æºäºä¹ä¸æï¼ç£çä¸çä¼è¯æä»¶å¯ä»¥å½ä½å¯¹è¯ç»§ç»è¯»ï¼åªæ¯ä¸ä¼éé»éæ¾æ²¡æ§è¡å®çå½ä»¤ââå®å¯è®©ä½ æå¨éåï¼ä¹ä¸è¦åè£
ä¸åæ£å¸¸ã
è¿åäºå ä»¶å°äºï¼
- @ æä»¶æåï¼è¾å
¥æ¡é @ ä¸ä¸å°±è½æé¡¹ç®æä»¶å¼ç¨è¿æ¥ï¼æ«æèå´éå®å¨é¡¹ç®
.agents/skillsä¸.pi/skills - çæä¸æéï¼åå¤è¿å¨çææ¶ä½ å¯ä»¥ç»§ç»æåå车ï¼è¿éåï¼æå¤ 5 æ¡ï¼ï¼å½åè½®ç»ææé¡ºåºåï¼åæ¢çæä¼ä¿çéå
- ä¸è±åè¯ï¼i18n åäº locale 忢ï¼ç颿æ¡å®å ¨æ¬å°å
Skills / MCP / Hooksï¼ä¸æ¯ææ ·å
Pi çæç Skills ç±è¿è¡æ¶å è½½ï¼Tether ä¸å¦å loaderï¼SKILL.md 带 frontmatterï¼name + descriptionï¼ç¼ºé¡¹ä¸å è½½ï¼ãMCP å Hooks ä¹é½å¨ agent-core 鿥好äºï¼æ¡é¢å£³è¿è¾¹è´è´£æå®ä»¬å±ç¤ºåºæ¥ãæç¨æ·é
ç½®ä¼ ä¸å»ã
项ç®èªå·±ç skill 乿¯è¿ä¹æ²æ·çï¼æ¯å¦ UI æ°è´¨ç»ä¸èµ° .agents/skills/tether-uiï¼é¿ä»»å¡èµ° plan-then-actãinit-long-runãcontinue-long-run è¿ä¸ç»ãå agent åºç¨æå¤§çå¿å¾ä¹ä¸ï¼æå¢é约å®åæ skillï¼è䏿¯é è天记å½å£å£ç¸ä¼ ã
踩è¿çå / å¿å¾
- 嫿 Electron 彿µè§å¨ã渲æè¿ç¨å主è¿ç¨çè¾¹ç妿ä¸å¼å§ä¸åæ¸ æ¥ï¼å颿¯ä¸ªåè½é½å¨è·å®å ¨é ç½®ææã
- agent è¿ç¨å¿ é¡»ç¬ç«äº UI çå½å¨æãä¸å¼å§æä¹æ³è¿æ¾ä¸»è¿ç¨éï¼åæ¥åç°ä¸ä¸ªå´©æºç agent è½ææ´ä¸ªåºç¨å¸¦èµ°ï¼ç¬ç«åè¿ç¨ + ç£çä¼è¯æ¯å¯ä¸é è°±çç»åã
- è¶ æ¶æ¯ RPC ç第ä¸å ¬æ°ã没æç½ååå¼çè¶ æ¶ç®¡çï¼é¿ä»»å¡åç任塿··å¨ä¸èµ·ï¼è¿æ©æä¸å¤© UI å ¨å¡ã
- è¡¥ä¸ä¼äºæ´ä½åå ¥ã让 AI æ¯æ¬¡åæ´ä¸ªæä»¶å¤ªå±é©ï¼patch + checkpoint æè½è®©"æ¤é"æä¸ºå¯è½ã
代ç å¨ GitHubï¼github.com/tt-11-dd/teâ¦ï¼MIT åè®®ï¼æ¬¢è¿æ¥çã
æå
说å®è¯ï¼ä»"æ³æ¹ä¸ª AI å·¥å ·é½æ¹ä¸å¨"å°"èªå·±æäºä¸ä¸ªé¡ºæçå·¥ä½å°"ï¼ä¸é´æå¼é±ç䏿¯ä»£ç éï¼èæ¯æ³æ¸ æ¥åªäºä¸è¥¿è¯¥å¤ç¨çæãåªäºä¸è¥¿å¿ é¡»èªå·±åãAgent çæè¿å¨å¿«éæ¼è¿ï¼ç«å¨å·¨äººçè©èä¸ï¼æäº§åè¾¹çåç¨æ·ä½éªæç£¨å¥½ï¼å¯è½æ¯ä»ä¹é½ä»é¶åæ´æä»·å¼ã
妿è¿ç¯æç« å¯¹ä½ æå¯åï¼æè ä½ æ³èè Agent åºç¨æ¶æï¼è¯è®ºåºè§ã
å¦æä½ è§å¾æç¨ï¼æ¬¢è¿ Star / 转åï¼ä¹æ¬¢è¿å¨è¯è®ºåºäº¤æµä½ 踩è¿ç Agent å¼ååã
Aitishiku.com