Skip to content

文件操作服务 ​

本文引用的源码文件

本文引用的文件

目录 ​

  1. 引言
  2. 项目结构
  3. 核心组件
  4. 架构总览
  5. 详细组件分析
  6. 依赖关系分析
  7. 性能与资源限制
  8. 故障排查指南
  9. 安全最佳实践
  10. 结论

引言 ​

本文件操作服务由 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"]

图表来源

章节来源

核心组件 ​

  • 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 等调用入口。

章节来源

架构总览 ​

文件操作请求的典型流程如下:

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

图表来源

详细组件分析 ​

路径白名单验证与目录遍历防护 ​

路径检查的核心逻辑位于 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 : "删除转回收站"

图表来源

章节来源

文件读写与编码转换 ​

  • 文本读取:/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 服务器导入 Node watch,但当前文件操作路由中未见基于 watch 的变更事件分发逻辑;现有实现更偏向于按需读取和下载。
  • 变更通知:未发现 WebSocket 或轮询机制用于向扩展端推送文件变更事件。
  • 版本管理:未发现内置的版本快照、差异比较或 Git 集成;删除走回收站,恢复一次还原。
  • 冲突解决:重命名和移动在目标已存在时返回 409;上传同名文件会追加序号;恢复回收站时若原位置被占用则拒绝恢复。

章节来源

依赖关系分析 ​

  • server.js 依赖 security.js 进行路径校验,依赖 trash.js 处理删除,依赖 logger.js 记录操作,依赖 search.js 提供文件与内容搜索。
  • security.js 依赖 config.js 获取工作目录和白名单,依赖 i18n.js 提供多语言错误信息。
  • trash.js 依赖 i18n.js 和 sys-lang.js,并使用 Node fs/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

图表来源

章节来源

性能与资源限制 ​

  • 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 读取前若干行,裁剪空行列,避免前端引入重型库。

章节来源

故障排查指南 ​

  • 路径被拒绝:检查 checkPath 返回的 reason,确认路径是否在 allowedPaths 或工作目录下,且未被硬阻止清单拦截。
  • 符号链接问题:如果路径指向符号链接,注意 realpath 会解析到真实路径,真实路径仍需满足白名单。
  • 删除后找不到文件:删除操作会返回 trashId,可通过回收站列表和恢复接口找回。
  • 重命名/移动冲突:目标已存在时返回 409,需先处理冲突文件或改名。
  • 上传失败:检查 Content-Length 是否超过限制,以及脚本文件是否因权限剥离导致无法执行。
  • 下载过大文件 OOM:优先使用 /api/fs/download-stream,避免 base64 膨胀和全量内存加载。
  • 集成测试行为:读目录、列文件、缺失 path、删除后 stat 等行为有集成测试覆盖,可作为预期行为参考。

章节来源

安全最佳实践 ​

  • 输入验证:始终使用 checkPath 校验路径,拒绝空值、相对路径逃逸、不在白名单的路径。
  • 输出净化:下载文件名使用 basename 和 URL 编码,避免路径穿越;ZIP 打包只包含合法路径。
  • 资源限制:严格限制 JSON body、流式上传、读取和下载大小;脚本文件写入后剥离执行权限。
  • 审计日志:所有文件操作通过 logFs、logSecurity、logError 记录路径、大小、类型和错误信息。
  • 最小权限:工作目录永远放行,但其他路径必须显式加入 allowedPaths;Agent 系统目录硬阻止。
  • 防 DoS:重启、更新、关闭等管理接口有冷却时间和防重入保护。
  • 跨平台兼容:路径格式化、大小写、分隔符、盘符处理均考虑 Windows/macOS/Linux 差异。

章节来源

结论 ​

文件操作服务以 server.js 为入口,结合 security.js 的路径白名单与符号链接防护,以及 trash.js 的回收站机制,提供了安全的文件读取、写入、列表、统计、重命名、移动、删除、上传、下载和预览能力。其优势在于:

  • 多层路径校验与硬阻止清单,防止越权访问。
  • 回收站软删除与过期清理,降低误删风险。
  • 流式上传/下载与 ZIP 打包,兼顾大文件与用户体验。
  • 脚本文件权限剥离与审计日志,提升安全性与可观测性。

需要注意:

  • 当前实现没有跨操作的 ACID 事务或自动回滚机制。
  • 文件监控、变更通知、版本管理并非当前文件操作服务的核心能力。
  • 批量操作应以“多次原子文件系统调用 + 业务层重试/冲突处理”的方式设计,而非依赖隐式事务。