主题
原生目录选择器
本文引用的源码文件
本文引用的文件
目录
引言
本文件聚焦“原生目录选择器”能力:扩展端侧边栏的“切换工作目录”弹窗中,当连接的是本机 Agent 时,提供“浏览”按钮调用系统原生文件夹选择对话框;用户确认后,路径回填到输入框,再由后端接口设置 Agent 的工作目录。该能力横跨 Chrome 扩展(Manifest V3)与独立 Node.js Agent 进程,是本地工作区管理的关键入口之一。
项目结构定位
原生目录选择器涉及两个代码域:
- Agent 端:负责实际弹出系统原生目录选择框、解析平台命令、处理超时与降级。
- 扩展端:负责在 Side Panel 中渲染“切换工作目录”弹窗、判断是否允许使用原生浏览按钮、发起 HTTP 请求并处理返回结果。
graph TB subgraph "Chrome 扩展端" SP["Side Panel<br/>workspace-panel.js"] WM["Workspace Manager<br/>workspace-manager.js"] end subgraph "Agent 端" API["HTTP Server<br/>server.js"] DP["Directory Picker<br/>directory-picker.js"] OS["操作系统 GUI<br/>macOS / Windows / Linux"] end SP --> WM WM --> API API --> DP DP --> OS
图表来源
- src/side_panel/workspace-panel.js#L6070-L6132
- src/side_panel/workspace-manager.js#L318-L324
- agent/src/server.js#L987-L1008
- agent/src/directory-picker.js#L1-L135
章节来源
- src/side_panel/workspace-panel.js#L6070-L6132
- src/side_panel/workspace-manager.js#L318-L324
- agent/src/server.js#L987-L1008
- agent/src/directory-picker.js#L1-L135
核心组件
pickDirectoryNative:Agent 端对外暴露的原生目录选择函数,封装跨平台命令构造、子进程执行、超时保护与结果归一化。buildPickCommand:根据平台生成对应命令及参数,并在 Linux 上支持 zenity/kdialog 回退。/api/config/pick-directory:Agent HTTP 路由,负责来源 IP 校验、调用pickDirectoryNative、记录日志并返回统一结果对象。pickDirectory:扩展端 Workspace Manager 中的轻量封装,直接 POST 到 Agent 的/api/config/pick-directory。- “切换工作目录”弹窗:扩展端 UI 层,检测是否连接本机 Agent,决定是否显示“浏览”按钮,并根据返回结果填充路径或提示不可用。
章节来源
- agent/src/directory-picker.js#L15-L52
- agent/src/directory-picker.js#L104-L134
- agent/src/server.js#L987-L1008
- src/side_panel/workspace-manager.js#L318-L324
- src/side_panel/workspace-panel.js#L6070-L6132
架构总览
从用户点击“浏览”到最终路径落盘,整体流程如下:
sequenceDiagram
participant User as "用户"
participant Panel as "Side Panel<br/>workspace-panel.js"
participant Manager as "Workspace Manager<br/>workspace-manager.js"
participant Server as "Agent HTTP Server<br/>server.js"
participant Picker as "Directory Picker<br/>directory-picker.js"
participant OS as "系统 GUI"
User->>Panel : 打开“切换工作目录”弹窗
Panel->>Panel : 检测 Agent URL 是否为本机
Panel->>Manager : 点击“浏览”调用 pickDirectory()
Manager->>Server : POST /api/config/pick-directory
Server->>Server : 校验 remoteAddress 是否 loopback
Server->>Picker : pickDirectoryNative(title)
Picker->>OS : 调用 osascript / powershell / zenity / kdialog
OS-->>Picker : 输出选中路径或空输出
Picker-->>Server : {success, path, available}
Server-->>Manager : 返回相同结构体
Manager-->>Panel : 返回结果
Panel->>Panel : 根据 success/available/error 填充或提示
图表来源
- src/side_panel/workspace-panel.js#L6070-L6132
- src/side_panel/workspace-manager.js#L318-L324
- agent/src/server.js#L987-L1008
- agent/src/directory-picker.js#L104-L134
详细组件分析
Agent 端:原生目录选择实现
directory-picker.js 是原生目录选择器的核心实现,职责包括:
- 按平台构造命令:
- macOS:通过
osascript调用 AppleScript 的choose folder with prompt。 - Windows:通过 PowerShell 的
FolderBrowserDialog,并使用-EncodedCommand避免命令行注入。 - Linux:优先
zenity --file-selection --directory,失败后回退kdialog --getexistingdirectory。
- macOS:通过
- 安全处理标题:对传入 title 进行引号、反斜杠等字符清理,防止脚本注入。
- 子进程生命周期管理:
- 默认 120 秒悬挂超时,超时后 kill 子进程并返回
timeout错误。 - 捕获 spawn 失败(如 ENOENT),用于判定环境是否可用。
- 默认 120 秒悬挂超时,超时后 kill 子进程并返回
- 路径归一化:去除首尾空白与 POSIX 路径尾斜杠(根目录
/除外)。 - 统一返回结构:
- 成功选中:
{ success: true, path, available: true } - 用户取消:
{ success: true, path: null, available: true } - 无 GUI/工具不可用:
{ success: false, path: null, available: false } - 其他错误:
{ success: false, path: null, available: true, error }
- 成功选中:
flowchart TD
Start(["进入 pickDirectoryNative"]) --> CheckLinux["Linux 且无 DISPLAY/WAYLAND_DISPLAY?"]
CheckLinux --> |是| NoGui["返回 available:false"]
CheckLinux --> |否| BuildCmd["buildPickCommand(title, platform)"]
BuildCmd --> Attempts{"是否有 fallback?"}
Attempts --> |有| TryFallback["依次尝试主命令与回退命令"]
Attempts --> |无| TryMain["仅尝试主命令"]
TryMain --> RunOnce["runPickOnce(command, args, timeout)"]
TryFallback --> RunOnce
RunOnce --> SpawnError{"spawn 失败?"}
SpawnError --> |是| NextAttempt["尝试下一个候选命令"]
SpawnError --> |否| Timeout{"是否超时?"}
Timeout --> |是| ReturnTimeout["返回 success:false, error:'timeout'"]
Timeout --> |否| Normalize["normalizePickedPath(stdout)"]
Normalize --> HasPath{"path 非空?"}
HasPath --> |是| ReturnSuccess["返回 success:true, path, available:true"]
HasPath --> |否| ReturnCancel["返回 success:true, path:null, available:true"]
NextAttempt --> Attempts
NoGui --> End(["结束"])
ReturnSuccess --> End
ReturnCancel --> End
ReturnTimeout --> End
图表来源
- agent/src/directory-picker.js#L15-L52
- agent/src/directory-picker.js#L54-L92
- agent/src/directory-picker.js#L104-L134
章节来源
Agent 端:HTTP 路由与安全门控
/api/config/pick-directory 是扩展端调用的唯一入口,关键行为:
- 仅接受 POST 请求。
- 严格校验来源 IP:仅允许
127.0.0.1、::1、::ffff:127.0.0.1、localhost。 - 远端来源不返回 403/404,而是以
available:false降级返回,让扩展端隐藏“浏览”按钮。 - 调用
pickDirectoryNative,成功选中时记录系统日志。 - 异常路径记录错误日志并返回
error字段。
flowchart TD
Req["收到 POST /api/config/pick-directory"] --> GetIP["读取 remoteAddress"]
GetIP --> IsLocal{"是否 loopback?"}
IsLocal --> |否| Block["记录 security 日志<br/>返回 available:false"]
IsLocal --> |是| CallPicker["调用 pickDirectoryNative(title)"]
CallPicker --> PickOk{"result.success && result.path?"}
PickOk --> |是| LogPick["记录 directory_picked 日志"]
PickOk --> |否| SkipLog["跳过日志"]
LogPick --> Respond["返回 JSON 结果"]
SkipLog --> Respond
Block --> Respond
图表来源
章节来源
扩展端:UI 与调用链
扩展端通过 Side Panel 的“切换工作目录”弹窗触发原生目录选择:
- 弹窗初始化时解析 Agent URL 的 hostname,若为
localhost、127.0.0.1或[::1],则启用“浏览”按钮。 - 点击“浏览”后调用
workspace-manager.js的pickDirectory(),后者直接 POST 到/api/config/pick-directory。 - 根据返回结果:
success && path:将路径填入输入框并聚焦。success && !path:视为用户取消,静默处理。available === false:提示“浏览不可用”。- 其他错误:提示具体 error。
sequenceDiagram
participant UI as "Switch Workdir Dialog"
participant WM as "workspace-manager.pickDirectory"
participant Fetch as "fetch('/api/config/pick-directory')"
participant Resp as "响应处理"
UI->>WM : pickDirectory()
WM->>Fetch : POST JSON {}
Fetch-->>WM : JSON {success,path,available,error}
WM-->>UI : 返回结果
UI->>Resp : 根据 success/available/error 更新 UI
图表来源
章节来源
依赖关系分析
server.js依赖directory-picker.js,并通过 HTTP 路由暴露给扩展端。workspace-manager.js依赖 Agent HTTP 配置与 token,封装所有 Agent 相关请求。workspace-panel.js依赖workspace-manager.js,并负责 UI 交互与状态展示。- 测试用例覆盖命令构造、路径归一化、取消行为、无 GUI 降级、回退机制与超时保护。
graph LR WP["workspace-panel.js"] --> WM["workspace-manager.js"] WM --> SJS["server.js"] SJS --> DPK["directory-picker.js"] DPK --> OS["系统 GUI"]
图表来源
- src/side_panel/workspace-panel.js#L6070-L6132
- src/side_panel/workspace-manager.js#L318-L324
- agent/src/server.js#L57
- agent/src/server.js#L987-L1008
- agent/src/directory-picker.js#L1-L135
章节来源
- agent/src/server.js#L57
- agent/src/server.js#L987-L1008
- src/side_panel/workspace-manager.js#L318-L324
- src/side_panel/workspace-panel.js#L6070-L6132
- agent/src/directory-picker.js#L1-L135
性能与可靠性特征
- 超时保护:默认 120 秒,避免用户长时间不操作导致子进程常驻。
- 多候选命令:Linux 下 zenity 失败自动回退 kdialog,提升可用性。
- 无 GUI 快速降级:Linux 无 DISPLAY/WAYLAND_DISPLAY 时直接返回
available:false,减少无效报错。 - 标题安全清洗:防止通过 title 注入 shell 或脚本。
- 路径归一化:消除 POSIX 路径尾斜杠与换行,保证前端一致性。
- 安全门控:仅本机 loopback 可调用,远端请求以
available:false降级,避免误暴露。
[本节为通用特性说明,不直接分析具体代码片段]
故障排查指南
常见现象与定位建议:
- “浏览”按钮未显示:
- 检查 Agent URL 的 hostname 是否为
localhost、127.0.0.1或[::1]。 - 若连接远端 Agent,预期行为是隐藏浏览按钮。
- 检查 Agent URL 的 hostname 是否为
- 点击“浏览”提示不可用:
- macOS:确认
osascript可用。 - Windows:确认 PowerShell 与
System.Windows.Forms可用。 - Linux:确认存在 DISPLAY/WAYLAND_DISPLAY,且 zenity/kdialog 至少一个可用。
- macOS:确认
- 点击“浏览”后无响应:
- 检查是否触发 120 秒超时,查看返回
error:'timeout'。 - 检查 Agent 进程是否被终止或端口不可达。
- 检查是否触发 120 秒超时,查看返回
- 返回路径异常:
- 检查 stdout 是否包含换行或尾斜杠,
directory-picker.js已做归一化。 - 确认 Agent 端日志是否记录
directory_picked。
- 检查 stdout 是否包含换行或尾斜杠,
章节来源
- agent/src/directory-picker.js#L104-L134
- agent/test/unit/directory-picker.unit.test.js#L60-L114
- agent/src/server.js#L987-L1008
- src/side_panel/workspace-panel.js#L6070-L6132
结论
原生目录选择器通过 Agent 端跨平台命令封装与扩展端 UI 控制,实现了本机工作目录的安全、直观选择。其设计强调:
- 平台兼容:macOS、Windows、Linux 均有明确实现与回退策略。
- 安全边界:仅本机 loopback 可调用,远端请求降级。
- 用户体验:超时保护、取消静默、路径归一化、错误提示清晰。
- 可测试性:单元测试覆盖命令构造、路径解析、取消、无 GUI、回退与超时场景。
在实际使用中,若需新增平台或替换底层 GUI 工具,应优先修改 buildPickCommand 与 pickDirectoryNative,并确保测试用例同步覆盖新行为。
[本节为总结性内容,不直接分析具体代码片段]