Skip to content

Commit ab1b699

Browse files
committed
feat(acp): 添加 Agent Client Protocol 支持
1 parent f48aa42 commit ab1b699

19 files changed

Lines changed: 2199 additions & 49 deletions

.claude/settings.local.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@
6565
"Bash(pnpm approve-builds:*)",
6666
"Bash(git reset:*)",
6767
"Bash(cloc:*)",
68-
"Bash(grep:*)"
68+
"Bash(grep:*)",
69+
"WebFetch(domain:zed.dev)",
70+
"WebFetch(domain:agentclientprotocol.com)",
71+
"WebFetch(domain:www.npmjs.com)",
72+
"WebFetch(domain:deepwiki.com)"
6973
],
7074
"deny": [],
7175
"ask": [],

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"@vscode/ripgrep": "^1.17.0"
115115
},
116116
"dependencies": {
117+
"@agentclientprotocol/sdk": "^0.12.0",
117118
"@inkjs/ui": "^2.0.0",
118119
"@modelcontextprotocol/sdk": "^1.17.4",
119120
"ahooks": "^3.9.5",

pnpm-lock.yaml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/acp/AcpFileSystemService.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
/**
2+
* ACP 文件系统服务适配器
3+
*
4+
* 将文件操作转发给 IDE(ACP Client)执行。
5+
* 当 IDE 声明支持 fs 能力时,可以使用此服务替代本地文件操作。
6+
*/
7+
8+
import type {
9+
AgentSideConnection,
10+
FileSystemCapability,
11+
} from '@agentclientprotocol/sdk';
12+
import { createLogger, LogCategory } from '../logging/Logger.js';
13+
import {
14+
type FileStat,
15+
type FileSystemService,
16+
LocalFileSystemService,
17+
} from '../services/FileSystemService.js';
18+
19+
const logger = createLogger(LogCategory.AGENT);
20+
21+
/**
22+
* ACP 文件系统服务
23+
*
24+
* 将文件操作转发给 IDE 执行。
25+
* 如果 IDE 不支持某个操作,则回退到本地文件系统。
26+
*/
27+
export class AcpFileSystemService implements FileSystemService {
28+
constructor(
29+
private readonly connection: AgentSideConnection,
30+
private readonly sessionId: string,
31+
private readonly capabilities: FileSystemCapability,
32+
private readonly fallback: FileSystemService = new LocalFileSystemService()
33+
) {}
34+
35+
/**
36+
* 读取文本文件
37+
*
38+
* 如果 IDE 支持 readTextFile,则通过 ACP 协议读取;
39+
* 否则回退到本地文件系统。
40+
*/
41+
async readTextFile(filePath: string): Promise<string> {
42+
if (!this.capabilities.readTextFile) {
43+
logger.debug(`[AcpFileSystem] readTextFile fallback: ${filePath}`);
44+
return this.fallback.readTextFile(filePath);
45+
}
46+
47+
try {
48+
logger.debug(`[AcpFileSystem] readTextFile via ACP: ${filePath}`);
49+
const response = await this.connection.readTextFile({
50+
path: filePath,
51+
sessionId: this.sessionId,
52+
});
53+
return response.content;
54+
} catch (error) {
55+
logger.warn(`[AcpFileSystem] readTextFile ACP failed, fallback: ${error}`);
56+
return this.fallback.readTextFile(filePath);
57+
}
58+
}
59+
60+
/**
61+
* 写入文本文件
62+
*
63+
* 如果 IDE 支持 writeTextFile,则通过 ACP 协议写入;
64+
* 否则回退到本地文件系统。
65+
*/
66+
async writeTextFile(filePath: string, content: string): Promise<void> {
67+
if (!this.capabilities.writeTextFile) {
68+
logger.debug(`[AcpFileSystem] writeTextFile fallback: ${filePath}`);
69+
return this.fallback.writeTextFile(filePath, content);
70+
}
71+
72+
try {
73+
logger.debug(`[AcpFileSystem] writeTextFile via ACP: ${filePath}`);
74+
await this.connection.writeTextFile({
75+
path: filePath,
76+
content,
77+
sessionId: this.sessionId,
78+
});
79+
} catch (error) {
80+
logger.warn(`[AcpFileSystem] writeTextFile ACP failed, fallback: ${error}`);
81+
return this.fallback.writeTextFile(filePath, content);
82+
}
83+
}
84+
85+
/**
86+
* 检查文件是否存在
87+
*
88+
* 策略:优先信任 ACP,宁可误判"存在"也不要误判"不存在"
89+
*
90+
* 1. 如果 IDE 支持 readTextFile,通过 ACP 判断:
91+
* - 读取成功 → 存在
92+
* - 错误明确是"not found/enoent" → 不存在
93+
* - 其他错误(权限、二进制、超时等)→ 假设存在
94+
* (让后续操作揭示真正问题,而非提前终止)
95+
*
96+
* 2. 如果 IDE 不支持 readTextFile,fallback 到本地
97+
*/
98+
async exists(filePath: string): Promise<boolean> {
99+
// 如果 IDE 不支持文件读取,fallback 到本地
100+
if (!this.capabilities.readTextFile) {
101+
logger.debug(`[AcpFileSystem] exists fallback to local: ${filePath}`);
102+
return this.fallback.exists(filePath);
103+
}
104+
105+
// 通过 ACP 检查
106+
try {
107+
await this.connection.readTextFile({
108+
path: filePath,
109+
sessionId: this.sessionId,
110+
});
111+
logger.debug(`[AcpFileSystem] exists(${filePath}): true (ACP read success)`);
112+
return true;
113+
} catch (error) {
114+
const errorMsg = String(error).toLowerCase();
115+
116+
// 只有明确的"不存在"错误才返回 false
117+
const notFoundPatterns = [
118+
'not found',
119+
'no such file',
120+
'enoent',
121+
'does not exist',
122+
'file not found',
123+
'path not found',
124+
];
125+
126+
const isNotFound = notFoundPatterns.some((pattern) => errorMsg.includes(pattern));
127+
128+
if (isNotFound) {
129+
logger.debug(`[AcpFileSystem] exists(${filePath}): false (ACP: not found)`);
130+
return false;
131+
}
132+
133+
// 其他错误(权限、二进制、超时等)假设文件存在
134+
// 让后续操作揭示真正问题,而非在这里提前终止
135+
// 使用 warn 级别记录,便于诊断
136+
const errorType = categorizeError(errorMsg);
137+
logger.warn(
138+
`[AcpFileSystem] exists(${filePath}): assuming exists due to ${errorType} error`,
139+
{ error: String(error), errorType, filePath }
140+
);
141+
return true;
142+
}
143+
}
144+
145+
/**
146+
* 读取二进制文件
147+
*
148+
* ACP 协议目前只支持文本文件读取,二进制文件回退到本地。
149+
*/
150+
async readBinaryFile(filePath: string): Promise<Buffer> {
151+
// ACP 协议暂不支持二进制读取,使用本地
152+
logger.debug(`[AcpFileSystem] readBinaryFile fallback: ${filePath}`);
153+
return this.fallback.readBinaryFile(filePath);
154+
}
155+
156+
/**
157+
* 获取文件统计信息
158+
*
159+
* ACP 协议暂不支持 stat 操作,回退到本地。
160+
*/
161+
async stat(filePath: string): Promise<FileStat | null> {
162+
// ACP 协议暂无 stat 方法,使用本地
163+
logger.debug(`[AcpFileSystem] stat fallback: ${filePath}`);
164+
return this.fallback.stat(filePath);
165+
}
166+
167+
/**
168+
* 创建目录
169+
*
170+
* ACP 协议暂不支持 mkdir 操作,回退到本地。
171+
* 注:writeTextFile 通常会自动创建父目录。
172+
*/
173+
async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise<void> {
174+
// ACP 协议暂无 mkdir 方法,使用本地
175+
logger.debug(`[AcpFileSystem] mkdir fallback: ${dirPath}`);
176+
return this.fallback.mkdir(dirPath, options);
177+
}
178+
179+
/**
180+
* 获取 IDE 支持的文件系统能力
181+
*/
182+
getCapabilities(): FileSystemCapability {
183+
return this.capabilities;
184+
}
185+
186+
/**
187+
* 检查是否支持读取文件
188+
*/
189+
canReadTextFile(): boolean {
190+
return this.capabilities.readTextFile ?? false;
191+
}
192+
193+
/**
194+
* 检查是否支持写入文件
195+
*/
196+
canWriteTextFile(): boolean {
197+
return this.capabilities.writeTextFile ?? false;
198+
}
199+
}
200+
201+
/**
202+
* 创建 ACP 文件系统服务
203+
*
204+
* 根据 ACP 客户端能力创建适当的文件系统服务实例。
205+
*
206+
* @param connection - ACP 连接
207+
* @param sessionId - 会话 ID
208+
* @param capabilities - IDE 声明的文件系统能力(可选)
209+
* @returns 文件系统服务实例
210+
*/
211+
export function createAcpFileSystemService(
212+
connection: AgentSideConnection,
213+
sessionId: string,
214+
capabilities?: FileSystemCapability
215+
): FileSystemService {
216+
if (capabilities && (capabilities.readTextFile || capabilities.writeTextFile)) {
217+
logger.debug(
218+
`[AcpFileSystem] Using ACP file system service (read: ${capabilities.readTextFile}, write: ${capabilities.writeTextFile})`
219+
);
220+
return new AcpFileSystemService(connection, sessionId, capabilities);
221+
}
222+
223+
logger.debug('[AcpFileSystem] Using local file system service (no IDE capabilities)');
224+
return new LocalFileSystemService();
225+
}
226+
227+
/**
228+
* 对错误信息进行分类,便于诊断
229+
*/
230+
function categorizeError(errorMsg: string): string {
231+
if (errorMsg.includes('permission') || errorMsg.includes('access denied')) {
232+
return 'permission';
233+
}
234+
if (errorMsg.includes('timeout') || errorMsg.includes('timed out')) {
235+
return 'timeout';
236+
}
237+
if (errorMsg.includes('binary') || errorMsg.includes('encoding')) {
238+
return 'binary';
239+
}
240+
if (errorMsg.includes('too large') || errorMsg.includes('size')) {
241+
return 'size';
242+
}
243+
if (errorMsg.includes('connection') || errorMsg.includes('network')) {
244+
return 'network';
245+
}
246+
return 'unknown';
247+
}

0 commit comments

Comments
 (0)