主题
文件操作服务
本文引用的源码文件
本文引用的文件
目录
引言
本文件操作服务由 Chrome 扩展端与独立 Agent 进程组成。扩展端通过本地 HTTP/WebSocket 调用 Agent 提供的文件系统接口,实现路径受限的文件读取、写入、列表、统计、重命名、移动、删除、上传、下载、预览和回收站恢复等操作。Agent 侧的安全机制以“白名单 + 硬阻止 + 符号链接校验”为核心,回收站提供软删除、跨设备迁移、过期清理和元数据持久化;批量下载通过 ZIP 打包,流式上传/下载避免大文件内存膨胀。
项目结构
与文件操作直接相关的代码主要分布在以下位置:
- Agent HTTP 路由与文件操作实现:
agent/src/server.js - 路径白名单、符号链接防护、命令分级安全策略:
agent/src/security.js - 回收站模块(移动、恢复、清理、定时任务):
agent/src/trash.js - 扩展端侧边栏文件操作封装:
src/side_panel/workspace-manager.js - 文件系统 API 集成测试:
agent/test/integration/api.integration.test.js
graph TB Client["Chrome 扩展<br/>side_panel"] --> Server["Agent HTTP 服务器<br/>server.js"] Server --> Security["路径与安全检查<br/>security.js"] Server --> Trash["回收站模块<br/>trash.js"] Server --> FS["Node 文件系统<br/>fs / fs/promises"] Server --> Logger["日志记录<br/>logger.js"] Server --> Search["搜索工具<br/>search.js"]
图表来源
- agent/src/server.js#L1-L20
- agent/src/server.js#L417-L443
- agent/src/security.js#L1-L10
- agent/src/trash.js#L1-L10
章节来源
- agent/src/server.js#L1-L60
- agent/src/security.js#L1-L30
- agent/src/trash.js#L1-L40
- src/side_panel/workspace-manager.js#L569-L616
核心组件
- HTTP 路由层:统一处理认证、国际化、CORS、请求体解析、文件大小限制、日志记录和错误响应。
- 路径安全检查层:负责路径格式规整、工作目录放行、白名单前缀匹配、符号链接
realpath校验、敏感路径硬阻止。 - 回收站模块:负责将删除目标移动到
.ai-helper-agent/.trash,记录元数据,支持恢复、过期清理和跨设备回退复制。 - 文件操作端点:提供
/api/fs/read|write|list|stat|mkdir|rename|move|delete|upload-stream|download*|preview-xlsx等能力。 - 扩展端封装:提供
deleteFs、renameFs、createDir、moveFs、getFileInfo等调用入口。
章节来源
- agent/src/server.js#L417-L443
- agent/src/server.js#L751-L862
- agent/src/server.js#L1037-L1559
- agent/src/security.js#L102-L229
- agent/src/trash.js#L160-L251
- src/side_panel/workspace-manager.js#L569-L616
架构总览
文件操作请求的典型流程如下:
sequenceDiagram
participant UI as "扩展端界面"
participant WM as "workspace-manager.js"
participant S as "server.js"
participant SEC as "security.js"
participant TR as "trash.js"
participant FS as "Node 文件系统"
UI->>WM : 调用 deleteFs(path)
WM->>S : POST /api/fs/delete { path }
S->>SEC : checkPath(path)
SEC-->>S : allowed/resolved/reason
alt 允许访问
S->>FS : stat(path)
FS-->>S : 文件信息
S->>TR : moveToTrash(resolved)
TR->>FS : rename/cp/rm
TR-->>S : trashId/isDir
S-->>WM : success + trashId
else 拒绝访问
S-->>WM : 403 + reason
end
图表来源
- src/side_panel/workspace-manager.js#L569-L583
- agent/src/server.js#L1145-L1161
- agent/src/security.js#L109-L229
- agent/src/trash.js#L166-L202
详细组件分析
路径白名单验证与目录遍历防护
路径检查的核心逻辑位于 checkPath:
- 输入为空或非字符串时直接拒绝。
- 展开
~到用户主目录。 - 使用
normalizePathFormat处理 Windows 盘符、MSYS 路径、反斜杠和大写盘符。 - 先做绝对路径解析与工作目录合并。
- 执行硬阻止检查,禁止访问 Agent 配置、配对记录、MCP 配置、日志、回收站、技能状态等敏感路径。
- 对记忆存储目录和工作目录做特殊放行。
- 对
allowedPaths做前缀匹配。 - 使用
realpath解析符号链接,并对真实路径再次执行硬阻止和前缀检查。 - 最终返回
allowed、resolved或拒绝原因。
flowchart TD
Start(["进入 checkPath"]) --> ValidateInput["校验 path 是否为非空字符串"]
ValidateInput --> ExpandTilde["展开 ~ 到用户主目录"]
ExpandTilde --> Normalize["normalizePathFormat 规整路径"]
Normalize --> Resolve["resolve + normalize 得到规范化路径"]
Resolve --> HardBlock["硬阻止检查"]
HardBlock --> MemoryWorkdir{"是否命中记忆目录或工作目录?"}
MemoryWorkdir --> |是| RealpathCheck["realpath 解析符号链接"]
MemoryWorkdir --> |否| AllowedPrefix["allowedPaths 前缀匹配"]
AllowedPrefix --> RealpathCheck
RealpathCheck --> HardBlockReal["对 realPath 再次硬阻止"]
HardBlockReal --> AllowedPrefixReal["对 realPath 再次前缀匹配"]
AllowedPrefixReal --> Result["返回 allowed/resolved/reason"]
图表来源
章节来源
符号链接检测与权限控制
- 符号链接绕过防护:所有可访问路径都经过
realpath解析,确保实际落盘位置仍在白名单内且未命中硬阻止清单。 - 工作目录永远放行,但会通过
realpath防止通过工作目录内的符号链接逃逸到 Agent 系统目录。 - 脚本文件写入后强制剥离执行权限:当文件扩展名为常见脚本类型或内容包含 shebang 时,设置权限为
0o644,降低直接执行风险。 - 流式上传的脚本文件同样会执行权限剥离。
- 文件统计接口返回
mode、uid、gid、isSymbolicLink等元信息,便于上层判断文件属性。
classDiagram
class Security {
+checkPath(path, tFn)
+checkCommand(command, force, tFn)
+normalizePathFormat(pathStr, platformOverride)
+isHardBlocked(normalizedPath, platformOverride)
}
class Server {
+handleRequest(req, res)
+parseBody(req)
+parseMultipartBody(req)
+getMimeType(filePath)
}
class Trash {
+moveToTrash(sourcePath, tFn)
+restoreFromTrash(trashId, tFn)
+listTrash()
+startPeriodicCleanup()
+stopPeriodicCleanup()
}
Server --> Security : "路径安全检查"
Server --> Trash : "删除转回收站"
图表来源
- agent/src/security.js#L760-L761
- agent/src/server.js#L1052-L1087
- agent/src/server.js#L849-L853
- agent/src/server.js#L1113-L1142
- agent/src/trash.js#L166-L202
章节来源
- agent/src/security.js#L148-L160
- agent/src/security.js#L180-L229
- agent/src/server.js#L1075-L1083
- agent/src/server.js#L849-L853
- agent/src/server.js#L1113-L1142
文件读写与编码转换
- 文本读取:
/api/fs/read要求路径允许、存在且不是目录,按utf-8读取并返回 content、size、path。 - 文本写入:
/api/fs/write支持utf-8和base64两种编码;base64会解码为 Buffer 再写入,用于二进制文件。 - 二进制文件支持:通过
encoding: 'base64'写入二进制内容;/api/fs/download返回 base64 内容和 MIME 类型。 - 流式上传:
/api/fs/upload-stream接收原始二进制 body,按Content-Length和累计字节数双重限制,写入完成后剥离脚本执行权限。 - 流式下载:
/api/fs/download-stream单文件使用createReadStream直接管道输出,避免整个文件加载到内存;目录则先打包为 ZIP 再分块发送。 - MIME 类型推断:根据扩展名映射常见文本、图片、音视频、Office、压缩包类型。
flowchart TD
ReadReq["POST /api/fs/read"] --> CheckPath["checkPath 校验"]
CheckPath --> Exists["检查是否存在"]
Exists --> IsFile{"是否文件?"}
IsFile --> |否| ErrorDir["返回 400 路径是目录"]
IsFile --> |是| SizeCheck["检查大小限制"]
SizeCheck --> ReadUtf8["utf-8 读取"]
ReadUtf8 --> ReadResp["返回 content/size/path"]
WriteReq["POST /api/fs/write"] --> CheckPathW["checkPath 校验"]
CheckPathW --> Encoding{"encoding 是 base64?"}
Encoding --> |是| DecodeBase64["Buffer.from(base64)"]
Encoding --> |否| Utf8Buf["Buffer.from(utf-8)"]
DecodeBase64 --> WriteBinary["writeFile(Buffer)"]
Utf8Buf --> WriteText["writeFile(text)"]
WriteBinary --> ScriptChmod["脚本文件 chmod 0o644"]
WriteText --> ScriptChmod
ScriptChmod --> WriteResp["返回 size/path"]
图表来源
章节来源
批量操作与原子性保证
- 重命名:
/api/fs/rename仅修改文件名,不跨目录;校验新名称不含路径分隔符,Windows 下过滤非法字符;目标已存在返回 409;目标路径也经过checkPath校验。 - 移动:
/api/fs/move要求源存在、目标目录存在;计算目标完整路径后再次校验;目标已存在返回 409;底层使用rename。 - 删除:
/api/fs/delete不直接物理删除,而是调用moveToTrash,返回trashId。 - 原子性与事务回滚:当前实现没有显式的事务管理器或回滚机制。重命名和移动依赖文件系统原语;删除依赖回收站模块。若中间步骤失败,可能产生部分状态,例如回收站元数据写入前文件已移动,或移动失败时清理半成品目标。文档应明确:该服务不承诺跨多个操作的 ACID 事务。
flowchart TD
RenameReq["POST /api/fs/rename"] --> ValidateName["校验 newName 无分隔符/非法字符"]
ValidateName --> CheckOld["checkPath(oldPath)"]
CheckOld --> ExistsOld["检查旧路径存在"]
ExistsOld --> NewFullPath["拼接新路径"]
NewFullPath --> CheckNew["checkPath(newFullPath)"]
CheckNew --> ExistsNew{"目标已存在?"}
ExistsNew --> |是| Conflict409["返回 409 冲突"]
ExistsNew --> |否| DoRename["rename(old, new)"]
DoRename --> RenameResp["返回 newPath/newName"]
MoveReq["POST /api/fs/move"] --> CheckSrc["checkPath(src)"]
CheckSrc --> CheckDest["checkPath(destDir)"]
CheckDest --> SrcExists["检查源存在"]
SrcExists --> DestIsDir["检查目标为目录"]
DestIsDir --> CalcDest["join(destDir, basename(src))"]
CalcDest --> CheckDestPath["checkPath(destPath)"]
CheckDestPath --> DestExists{"目标已存在?"}
DestExists --> |是| Conflict409Move["返回 409 冲突"]
DestExists --> |否| DoMove["rename(src, destPath)"]
DoMove --> MoveResp["返回 newPath"]
图表来源
章节来源
回收站功能:暂存、恢复、清理与持久化
- 移动至回收站:
moveToTrash确保回收站目录存在,清理过期条目,统计文件或目录大小,优先使用同文件系统rename,跨设备时回退为cp+ 删除源,并记录元数据到metadata.json。 - 恢复:
restoreFromTrash查找条目,检查回收站文件是否存在,检查原位置是否被占用,成功后移回原路径并删除元数据。 - 列表:
listTrash返回有效条目。 - 定期清理:每 6 小时清理超过 7 天的条目,递归删除目录或单个文件,更新元数据。
- 语言支持:通过
setTrashLang注入请求语言,内部翻译函数优先使用传入的tFn。
flowchart TD
DeleteAPI["POST /api/fs/delete"] --> CheckPathDel["checkPath"]
CheckPathDel --> StatDel["stat 获取 isDir/size"]
StatDel --> MoveTrash["moveToTrash"]
MoveTrash --> EnsureTrash["ensureTrashDir"]
EnsureTrash --> CleanExpired["cleanExpiredTrash"]
CleanExpired --> GetSize["getDirSize 或 s.size"]
GetSize --> CrossDevice["movePathCrossDevice"]
CrossDevice --> SaveMeta["loadMetadata + saveMetadata"]
SaveMeta --> ReturnTrash["返回 trashId/isDir"]
RestoreAPI["恢复回收站"] --> LoadMeta["loadMetadata"]
LoadMeta --> FindEntry["按 trashId 查找"]
FindEntry --> ExistsTrash{"回收站文件存在?"}
ExistsTrash --> |否| NotFound["返回 entryNotFound/fileNotFound"]
ExistsTrash --> |是| OriginalOccupied{"原位置被占用?"}
OriginalOccupied --> |是| Occupied["返回 originalPathOccupied"]
OriginalOccupied --> |否| MoveBack["movePathCrossDevice 回原路径"]
MoveBack --> RemoveMeta["删除元数据"]
RemoveMeta --> RestoreOk["返回 restoredPath"]
图表来源
- agent/src/server.js#L1145-L1161
- agent/src/trash.js#L44-L74
- agent/src/trash.js#L100-L158
- agent/src/trash.js#L166-L251
章节来源
- agent/src/trash.js#L44-L74
- agent/src/trash.js#L100-L158
- agent/src/trash.js#L166-L251
- agent/src/server.js#L1145-L1161
文件监控、变更通知、版本管理与冲突解决
- 文件监控:Agent 服务器导入 Node
watch,但当前文件操作路由中未见基于watch的变更事件分发逻辑;现有实现更偏向于按需读取和下载。 - 变更通知:未发现 WebSocket 或轮询机制用于向扩展端推送文件变更事件。
- 版本管理:未发现内置的版本快照、差异比较或 Git 集成;删除走回收站,恢复一次还原。
- 冲突解决:重命名和移动在目标已存在时返回 409;上传同名文件会追加序号;恢复回收站时若原位置被占用则拒绝恢复。
章节来源
- agent/src/server.js#L4-L5
- agent/src/server.js#L751-L790
- agent/src/server.js#L1467-L1512
- agent/src/server.js#L1514-L1559
- agent/src/trash.js#L210-L240
依赖关系分析
server.js依赖security.js进行路径校验,依赖trash.js处理删除,依赖logger.js记录操作,依赖search.js提供文件与内容搜索。security.js依赖config.js获取工作目录和白名单,依赖i18n.js提供多语言错误信息。trash.js依赖i18n.js和sys-lang.js,并使用 Nodefs/promises完成文件操作。- 扩展端
workspace-manager.js通过agentRequest调用/api/fs/*接口。
graph LR Server["server.js"] --> Security["security.js"] Server --> Trash["trash.js"] Server --> Logger["logger.js"] Server --> Search["search.js"] Security --> Config["config.js"] Security --> I18n["i18n.js"] Trash --> I18n Trash --> SysLang["sys-lang.js"] Workspace["workspace-manager.js"] --> Server
图表来源
- agent/src/server.js#L1-L23
- agent/src/security.js#L1-L6
- agent/src/trash.js#L1-L10
- src/side_panel/workspace-manager.js#L569-L616
章节来源
- agent/src/server.js#L1-L23
- agent/src/security.js#L1-L6
- agent/src/trash.js#L1-L10
- src/side_panel/workspace-manager.js#L569-L616
性能与资源限制
- JSON Body 限制:
MAX_BODY_SIZE为 200MB,防止超大 JSON 请求。 - 流式上传限制:
MAX_UPLOAD_SIZE为 200MB,同时校验Content-Length和累计写入字节数,超出即销毁请求和写入流。 - 读取大小限制:
read、download使用config.fileMaxSize或默认 50MB,超过直接拒绝。 - 流式下载优化:单文件使用
createReadStream管道输出;ZIP 打包结果按 64KB 分块写入,配合setImmediate让出事件循环,避免阻塞。 - ZIP 打包:使用
archiver将目录或路径集合压缩为临时 ZIP,完成后转为 base64 或通过流返回。 - XLSX 预览:服务端使用 SheetJS 读取前若干行,裁剪空行列,避免前端引入重型库。
章节来源
- agent/src/server.js#L132-L137
- agent/src/server.js#L268-L303
- agent/src/server.js#L305-L383
- agent/src/server.js#L792-L862
- agent/src/server.js#L1267-L1385
- agent/src/server.js#L1387-L1442
故障排查指南
- 路径被拒绝:检查
checkPath返回的reason,确认路径是否在allowedPaths或工作目录下,且未被硬阻止清单拦截。 - 符号链接问题:如果路径指向符号链接,注意
realpath会解析到真实路径,真实路径仍需满足白名单。 - 删除后找不到文件:删除操作会返回
trashId,可通过回收站列表和恢复接口找回。 - 重命名/移动冲突:目标已存在时返回 409,需先处理冲突文件或改名。
- 上传失败:检查
Content-Length是否超过限制,以及脚本文件是否因权限剥离导致无法执行。 - 下载过大文件 OOM:优先使用
/api/fs/download-stream,避免 base64 膨胀和全量内存加载。 - 集成测试行为:读目录、列文件、缺失 path、删除后 stat 等行为有集成测试覆盖,可作为预期行为参考。
章节来源
- agent/src/security.js#L109-L229
- agent/src/server.js#L1037-L1050
- agent/src/server.js#L1467-L1512
- agent/src/server.js#L1514-L1559
- agent/test/integration/api.integration.test.js#L257-L343
安全最佳实践
- 输入验证:始终使用
checkPath校验路径,拒绝空值、相对路径逃逸、不在白名单的路径。 - 输出净化:下载文件名使用
basename和 URL 编码,避免路径穿越;ZIP 打包只包含合法路径。 - 资源限制:严格限制 JSON body、流式上传、读取和下载大小;脚本文件写入后剥离执行权限。
- 审计日志:所有文件操作通过
logFs、logSecurity、logError记录路径、大小、类型和错误信息。 - 最小权限:工作目录永远放行,但其他路径必须显式加入
allowedPaths;Agent 系统目录硬阻止。 - 防 DoS:重启、更新、关闭等管理接口有冷却时间和防重入保护。
- 跨平台兼容:路径格式化、大小写、分隔符、盘符处理均考虑 Windows/macOS/Linux 差异。
章节来源
- agent/src/server.js#L417-L443
- agent/src/server.js#L751-L862
- agent/src/server.js#L1037-L1559
- agent/src/security.js#L62-L100
- agent/src/security.js#L109-L229
结论
文件操作服务以 server.js 为入口,结合 security.js 的路径白名单与符号链接防护,以及 trash.js 的回收站机制,提供了安全的文件读取、写入、列表、统计、重命名、移动、删除、上传、下载和预览能力。其优势在于:
- 多层路径校验与硬阻止清单,防止越权访问。
- 回收站软删除与过期清理,降低误删风险。
- 流式上传/下载与 ZIP 打包,兼顾大文件与用户体验。
- 脚本文件权限剥离与审计日志,提升安全性与可观测性。
需要注意:
- 当前实现没有跨操作的 ACID 事务或自动回滚机制。
- 文件监控、变更通知、版本管理并非当前文件操作服务的核心能力。
- 批量操作应以“多次原子文件系统调用 + 业务层重试/冲突处理”的方式设计,而非依赖隐式事务。