diff --git a/docs/02-requirements.md b/docs/02-requirements.md index b69770f..7c9d82a 100644 --- a/docs/02-requirements.md +++ b/docs/02-requirements.md @@ -23,6 +23,7 @@ | BR-019 | Chub 默认将本地 `config.json` 保存到正在执行的 `chub.exe` 同级目录;首次使用新位置时安全迁移旧 `%APPDATA%\\chub\\config.json`,新位置既有配置永不被旧配置覆盖。 | P1 | | BR-020 | 用户可为无认证代理维护必填、唯一的代理名称;新建实例默认使用实际 `chub.exe` 同级 `user_data_dirs\\<稳定实例 ID>`,实例列表在用户数据目录与调试端口之间显示代理名称。 | P1 | | BR-021 | 设置中的 Chrome 与 Edge 可执行文件路径必须支持 Windows 原生 `.exe` 文件选择;标准安装路径搜索必须在对应字段旁可感知地报告搜索、取消、确认当前路径、更新路径或失败,且不丢失已有输入。 | P1 | +| BR-022 | 删除实例确认框默认只删除 Chub 实例配置;用户可显式、默认未勾选地选择同时删除该实例的 User Data Dir。数据删除必须在后台重新确认目标目录未被 Chub 或外部浏览器占用,并通过绝对路径、非卷根及非链接边界校验;失败时保留实例配置和目录。 | P1 | ## 验收标准 @@ -53,6 +54,7 @@ - 图标创建失败、Explorer 重启、菜单关闭和应用退出不会阻塞 UI 帧,也不会导致浏览器启动、关闭、接管或按进程名枚举。退出时必须移除图标;外部 Chrome/Edge 永远不因通知区域命令被停止。 - 默认配置路径必须由实际可执行文件路径解析,而非当前工作目录;迁移只在新位置不存在且旧配置存在、可读取、可校验时执行。迁移使用现有原子写入并保留旧配置恢复备份;目标目录不可写或旧配置畸形时,不创建半成品配置、不静默回退或混合两个配置来源,并给出可恢复诊断。 - 设置页的 Chrome/Edge“选择”必须打开仅允许现有 `.exe` 的 Windows 原生文件选择器;取消、选择失败或不合规结果必须保留原输入。搜索标准安装路径时,结果即使与当前路径相同,也必须在同一字段旁明确表示“已确认当前路径可用”;搜索、取消、成功和失败不得依赖页面下方的全局反馈。 +- 删除实例确认框默认不删除 User Data Dir。只有用户勾选“同时删除 User Data Dir 中的数据”并再次点击具体的“删除实例及数据”后,才允许删除该目录;对运行中、启动/停止中、外部关联、外部占用或未知占用实例,数据删除选项禁用并说明需先停止/刷新。后台须在实际删除前重新检查 Chub registry 与外部占用,拒绝相对路径、卷根、普通文件和符号链接/重解析点。数据删除成功后才移除实例配置;检查、占用或删除失败时实例配置和目录均保留,并给出可恢复提示。 ## 不做 diff --git a/docs/04-architecture.md b/docs/04-architecture.md index d6425c9..ac2909b 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -51,6 +51,7 @@ UI/CLI 不直接操作 `exec.Cmd`、Win32 handle 或数据库。Application 层 - 通知区域:Windows platform adapter 在独立原生消息循环中管理一个图标、Tooltip 和原生菜单,向 `cmd/chub` 发送仅含“显示主窗口 / 刷新状态 / 退出应用”的 typed command。应用 coordinator 决定如何显示 Gio 窗口、发起既有刷新或执行退出清理;adapter 不持有浏览器进程句柄,不调用浏览器启动/停止接口。图标状态仅来自非敏感实例计数;Explorer 重启后重新注册,应用结束前删除图标。首版不拦截 Gio 的窗口关闭消息,也不隐藏主窗口。 - 配置定位与迁移:`config.OpenDefault()` 先用 `os.Executable()` 的目录解析目标 `config.json`,不依赖工作目录。目标文件已存在时直接使用它;目标不存在时仅尝试读取旧 `%APPDATA%\\chub\\config.json`,通过现有 decode/normalize 校验后以 Store 原子写入目标,并把旧文件保留为只读恢复备份。迁移失败不会写入目标、不会删除旧文件、不会回退到 AppData 继续运行;应用层只显示可恢复的启动诊断,不显示配置内容。CLI 与 GUI 共用该入口,避免出现不同配置来源。 - 浏览器可执行文件选择:`internal/platform/files.ExecutablePicker` 仅负责通过 Windows 原生 `OpenFileDialog` 返回已存在的绝对 `.exe` 路径;Gio shell 通过后台 adapter 调用并以字段和请求代次接收结果,不在 UI frame 中打开对话框或执行系统 I/O。Chrome 与 Edge 各自保存选择中状态和字段级反馈;取消、失败和不合规结果不改写 editor。标准路径搜索沿用独立可取消任务,但成功结果(包括与当前输入相同的确认结果)必须显示在发起字段旁,避免被页面下方的无关状态区遮蔽。 +- Delete instance data:实例删除默认只移除本地配置。用户在确认弹层显式勾选后,Gio shell 以实例 ID、目录快照和请求代次向后台 `InstanceDataDeleter` 发起不可取消的数据删除;执行期间弹层保留并禁用重复/取消操作。`cmd/chub` adapter 在 `RemoveAll` 前重新检查 Chub registry 和 Windows 外部 profile 占用,任一检查失败或发现占用均拒绝;`internal/platform/files.InstanceDataRemover` 只接受绝对、清理后的非卷根目录,并拒绝普通文件、符号链接或重解析点。删除成功后 shell 才删除实例行并持久化配置;任何检查或文件删除失败都保留实例行、配置和目录。该路径不枚举、关闭或接管浏览器进程。 Chrome 和 Edge 共用大部分 Chromium 参数,但 executable 默认路径、进程名称和安装方式不同,通过 `BrowserDefinition` 封装差异。CDP 端点只绑定 `127.0.0.1`;端口号和 `/json/version` 是诊断数据,WebSocket URL 不进入持久化、事件或普通 UI 文案。`/json/list` 仅用于只读页面目标信息;URL 在 DTO 边界移除 userinfo、query 和 fragment。 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 617cd1f..c7562b9 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -56,6 +56,7 @@ | T-403 | 便携配置路径与安全 legacy 迁移 | T-202,T-309 | DONE | | T-404 | 代理名称、独立默认目录与实例列表代理列 | T-304,T-306,T-309,T-403 | DONE | | T-405 | 设置浏览器 executable 选择与可感知搜索反馈 | T-205,T-206,T-207 | DONE | +| T-406 | 实例删除确认中的 User Data Dir 显式删除 | T-301,T-303,T-305 | DOING | ## Backlog diff --git a/docs/07-user-stories.md b/docs/07-user-stories.md index be45475..7becc65 100644 --- a/docs/07-user-stories.md +++ b/docs/07-user-stories.md @@ -18,6 +18,7 @@ | US-014 | 用户复制或升级 Chub 后,实例配置与程序一同保存在可执行文件目录;首次升级会安全带入旧 AppData 配置且不丢失已有新配置。 | P1 | | US-015 | 用户可以用易读名称维护代理,并在实例列表快速确认每个实例使用的代理;新建实例会得到程序目录内独立的默认 User Data Dir。 | P1 | | US-016 | 用户可以从设置页选择 Chrome 或 Edge 的本机可执行文件,或搜索标准安装位置,并立即知道当前路径是否已确认或被更新。 | P1 | +| US-017 | 用户删除实例时默认只移除配置;在浏览器已停止且明确勾选后,可以同时删除该实例的 User Data Dir。 | P1 | ## 验收场景 @@ -72,3 +73,7 @@ CLI/API 返回稳定错误码和结构化数据;未知 PID、身份不匹配 ### US-016 浏览器 executable 选择与搜索反馈 给定用户位于设置页,点击 Chrome 或 Edge 路径旁的“选择”后,Chub 在后台打开 Windows 原生文件选择器,并只接受已有的绝对 `.exe` 文件。成功选择只更新对应输入框并在字段旁说明已选择;取消、错误或不合规路径保留原输入并给出可恢复说明。点击“搜索”后,按钮在任务进行期间显示“取消”;搜索发现标准安装路径时,即使结果与当前值相同,也在同一字段旁显示“已确认当前路径可用”。切换到实例页再返回后,输入、选择/搜索状态和字段反馈保持,不执行全盘扫描或任何浏览器进程操作。 + +### US-017 删除实例及 User Data Dir + +给定用户点击实例删除图标,确认框默认只说明删除 Chub 配置,User Data Dir 保留。用户可以勾选默认未选中的“同时删除 User Data Dir 中的数据”;框内显示对象名称、目录路径和不可恢复提示,确认按钮变为“删除实例及数据”。仅当实例显示为“已退出”或“启动失败”时该选项可用;运行、启动/停止、外部关联、外部占用和未知占用状态明确提示先停止并刷新。确认后 Chub 在后台再次检查该目录的 Chub registry 与外部浏览器占用,并校验目录边界;删除成功才从列表和配置移除实例。用户取消、占用、目录边界、权限或 I/O 失败时,实例配置和 User Data Dir 都保持,用户可关闭浏览器、检查权限后重试。 diff --git a/docs/08-interaction-checklist.md b/docs/08-interaction-checklist.md index 186082a..0dc60ba 100644 --- a/docs/08-interaction-checklist.md +++ b/docs/08-interaction-checklist.md @@ -18,6 +18,7 @@ | IX-014 | 配置启动与迁移 | exe 同级配置解析、legacy 配置迁移和可恢复失败反馈 | 新配置、合法 legacy、目标优先、畸形 legacy、目标目录不可写、重复启动 | P1 | | IX-015 | 代理名称、默认目录与列表列 | 代理名称/地址保存、下拉回填、实例默认目录、代理名称展示 | 新建、编辑、重名/重复地址、无代理、长名称、窄窗口、目录权限失败 | P1 | | IX-016 | 设置浏览器 executable 路径 | Chrome/Edge 选择本机 `.exe`、搜索标准安装位置与取消 | 选择中、用户取消、选择失败、非法结果、搜索中、取消、路径更新、确认当前路径、未找到、切换页面 | P1 | +| IX-017 | 实例删除及数据删除 | 删除配置确认、显式勾选 User Data Dir、后台占用复核和文件删除 | 默认配置删除、已勾选、运行/外部禁用、删除中、用户取消、占用、路径拒绝、权限/I/O 失败、成功、迟到结果 | P1 | ## 通用规则 @@ -40,3 +41,4 @@ - 通知区域图标是 Windows 原生的辅助入口,不替代页面内可键盘访问的实例操作。图标默认项为“显示 Chub”,双击同效;右键菜单顺序固定为显示、刷新、分隔线、退出。刷新复用页面刷新命令并在进行中禁用重复项;退出是应用级命令,不能伪装为关闭单个浏览器。Tooltip 使用文字汇总,不含实例名称或敏感字段。菜单关闭不改变状态;图标失效或 Explorer 重启后后台重建,不抢焦点。首版不提供“关闭时隐藏到通知区域”。 - 配置迁移不是用户可编辑表单:应用启动时在 platform 层完成,GUI frame 不读取或写入文件。成功迁移仅以不打扰操作的文本说明来源已更新;失败时保留实例列表输入与当前可见页面,给出“程序目录中的 config.json 无法使用;请检查目录权限或恢复旧配置”的可恢复提示,不能显示完整配置、代理地址或用户路径。无需 HTML 原型,因为不新增可操作控件;需以真实 Windows 文件权限和 CLI/GUI 启动验证。 - 浏览器 executable 路径的“选择”和“搜索”是字段级异步操作:选择中禁止同一字段重复打开,搜索中按钮显示“取消”。每个结果都紧邻其 Chrome/Edge 路径字段显示,不能只写到需滚动才能看见的页面底部。用户取消、系统错误、空结果或不为绝对 `.exe` 的结果均保留原输入;搜索命中当前值时显示明确确认而不是静默完成。两个字段、页面切换和迟到结果彼此隔离;键盘与触控均可触发“选择”和“搜索/取消”。本次是既有控件的局部行为修复,不新增信息架构或视觉模式,因此无需改动 HTML 原型;须以真实 Windows 文件对话框 smoke 验收。 +- 实例删除确认框默认焦点在“取消”,Escape/关闭与取消同效。默认“删除实例”只删除配置,并明确 User Data Dir 保留;“同时删除 User Data Dir 中的数据”是默认未选中的业务复选框,不与行选择混淆。勾选后显示完整目录和不可恢复警告,确认按钮使用具体“删除实例及数据”文案;仅“已退出”/“启动失败”可勾选,其他状态禁用且说明原因。数据删除中弹层保持、背景不可用且不提供伪取消;结果通过实例 ID、目录快照和请求代次过滤。成功后焦点回到可预测的相邻实例操作;检查、占用、边界、权限或 I/O 失败时保留当前实例、勾选和弹层,并说明目录与配置均未删除、可先停止浏览器或检查权限后重试。 diff --git a/docs/api.md b/docs/api.md index 9ca744f..0f528ef 100644 --- a/docs/api.md +++ b/docs/api.md @@ -59,6 +59,18 @@ type ExecutablePicker interface { Windows 实现只返回绝对 `.exe` 路径;用户取消返回可识别的取消错误。UI 在后台调用该契约,并以 `PathField` 和请求代次丢弃过期结果。选择成功才改写对应 Chrome/Edge 字段;同字段的搜索/选择状态和文字反馈必须独立保存,不能依赖页面底部的全局消息。 +实例数据删除分成 UI 授权、占用复核和文件删除三个边界: + +```go +type InstanceDataDeleter func(context.Context, InstanceRow) error + +type InstanceDataRemover interface { + RemoveInstanceData(context.Context, string) error +} +``` + +`InstanceDataDeleter` 仅由删除确认框中默认未勾选的“同时删除 User Data Dir 中的数据”触发;它必须先复核 Chub registry 和外部 profile 占用,再调用 `InstanceDataRemover`。后者只接受绝对、非卷根且非链接的目录,目录不存在可视为已清除;它不根据进程名扫描、停止或接管浏览器。任何错误都使 UI 保留实例配置;只有数据删除成功才发布实例配置删除。 + Windows 通知区域使用仅限 platform 的内部 DTO;Gio shell 不直接调用 Shell API: ```go diff --git a/docs/current-state.md b/docs/current-state.md index 9cff98c..0c9fdde 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -3,7 +3,7 @@ ## 快照 - 日期:2026-07-27 -- 阶段:Phase 4 CDP 只读观察与后台入口(T-301 至 T-310、T-201 至 T-208、T-401、T-403 至 T-405 已完成;T-402 已规划) +- 阶段:Phase 4 CDP 只读观察与后台入口(T-301 至 T-310、T-201 至 T-208、T-401、T-403 至 T-405 已完成;T-402 已规划;T-406 进行中) - 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、loopback CDP 端口分配与端点校验、受限 CDP 目标读取、exe 同级 JSON 配置与 legacy 安全迁移、优雅关闭、Job Object、真实 Chrome/Edge smoke、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-208、T-301 至 T-310、T-401、T-403 至 T-405 已完成 - UI:Gio 双页 Shell 使用左侧“实例/设置”导航;实例页以等宽“新建实例 / 刷新实例状态”命令区开始,窄内容区自动堆叠。列表固定显示实例名称、浏览器类型、用户数据目录、代理名称、调试端口、状态和带边框的操作列;代理列只由稳定 `ProxyID` 解析当前名称(无代理时显示“无代理”),长名称省略且不挤压操作列。端口列在未启动/已退出时显示“建议 {端口}”、启动中显示“准备 {端口}”、托管运行显示“实际 {端口}”、外部关联显示“外部 {端口}”,避免将持久首选端口误作监听事实。新建表单从设置的起始端口推荐最低未冲突端口,并从实际 exe 同级 `user_data_dirs\\<实例 ID>` 预填一个独立、可编辑的 User Data Dir;ID 先分配,UI 不做目录 I/O,无法取得 exe 根目录时保留手动输入入口。编辑停止实例可修改首选端口;启动后台优先该端口并避开其他实例的首选/实际端口,成功回退后才保存新的首选值。操作顺序固定为启动/停止(或重新检测)、编辑、删除,删除与相邻操作保持更大间距以降低误点。主操作在已退出时启动,在 Chub 托管运行或调试不可用时显示停止,在启动/停止中禁用,在外部关联、外部占用或未知占用时仅重新检测;优雅停止通过后台 adapter 验证 registry 的 PID/profile 身份并等待其释放,绝不按进程名关闭或接管外部 Chrome/Edge。刷新通过后台回调只检查已保存实例,以配置快照丢弃编辑或删除后的过期结果;它结合 Chub registry、指定 profile 的外部占用证据和 loopback CDP 端点更新状态,但不扫描、接管或关闭其他 Chrome/Edge。双击、Enter 或编辑图标打开实例编辑弹层,支持名称、浏览器类型、User Data Dir、启动 URL、首选端口、以“名称 · 地址”显示的代理选择和只读实际端口;保存保留未公开启动选项,Escape 对脏表单先请求确认,活跃/外部关联实例锁定身份约束字段。设置页使用完整代理地址选择器、代理名称与地址输入及保存/删除操作;选择会回填名称和地址,保存按稳定 `proxyId` 新增或更新并校验名称/地址唯一性,删除未引用代理前必须确认,仍被实例引用的代理会被拒绝。启动通过异步回调接到 Windows 浏览器启动器:未占用目录从已保存的起始端口(空值默认 9666)选择 loopback CDP 端口,并在启动时按已选代理 ID 解析最新的 `--proxy-server` 参数;同目录外部浏览器只有在 `DevToolsActivePort` 与 CDP 端点可验证时才显示“外部已关联”,且不会接管其生命周期。删除使用确认弹层,只删除 Chub 实例配置而不删除 User Data Dir。新建、编辑和删除实例会保存到本地配置;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;设置页 Chrome/Edge 路径已接入后台 Windows 原生 `.exe` 选择器,选择和搜索结果始终显示在对应字段旁;命中已填路径会明确确认而非静默完成。新建实例表单使用 Label、Windows 原生目录选择器和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter - T-307:本应用会话启动的受管 Chrome/Edge 根进程由单实例后台 `Wait` 监控;意外退出按 ID、启动代次和 PID 验证后立即变为“已退出”,清空运行时 PID/端口,并以可关闭、可合并的提示告知用户。提示关闭后焦点回到启动操作;Chub 请求停止、外部实例、过期事件和应用关闭取消监控均不提示。 @@ -16,7 +16,8 @@ - T-403:默认配置已改为实际 `chub.exe` 同级 `config.json`,不受当前工作目录影响。目标缺失时会校验并原子复制旧 AppData 配置,旧文件保持恢复副本;目标优先且不会自动合并、切回 AppData 或迁移浏览器 User Data Dir。CLI 与 GUI 共用 `OpenDefault()`;配置不可用时 CLI 返回无敏感细节的稳定错误,GUI 显示恢复提示。 - T-404:已完成代理名称/地址同次显式保存、名称与端点唯一性校验、选择回填、列表代理名称列以及独立默认 profile 目录。新建实例先分配稳定 ID,再由实际 exe 目录生成 `user_data_dirs\\<实例 ID>`;代理名称按 ProxyID 推导而不复制到实例配置,UI 不进行文件 I/O。已通过单元测试、竞态检测、vet、无控制台 GUI 打包和 Windows smoke。 - T-405:已完成 Windows `OpenFileDialog` executable picker、Chrome/Edge 后台结果通道与字段级状态反馈。搜索同路径时明确确认当前路径可用;选择取消或错误保留输入,选择成功只更新目标字段。已通过单元测试、竞态检测、vet、无控制台打包,以及真实原生对话框取消/预选 Chrome `.exe` 返回 smoke。 -- blocker:无;下一任务为 T-402。 +- T-406:进行中。将实例删除确认框扩展为默认仅删除配置、可显式勾选删除 User Data Dir;文件删除将在后台复核 Chub/external profile 占用和目录边界,失败时保留配置与目录。 +- blocker:无;当前任务为 T-406,随后恢复 T-402。 ## 当前目录 diff --git a/docs/tasks/T-406.md b/docs/tasks/T-406.md new file mode 100644 index 0000000..7b26f36 --- /dev/null +++ b/docs/tasks/T-406.md @@ -0,0 +1,34 @@ +--- +id: T-406 +title: 实例删除确认中的 User Data Dir 显式删除 +phase: 4 +deps: [T-301, T-303, T-305] +status: DOING +created: 2026-07-27 +owner: codex +--- + +## 需求与背景 + +当前实例删除确认只移除 Chub 配置,明确保留 User Data Dir。用户需要在同一确认框中选择“删除实例和数据”,以便在不再需要某个隔离环境时一并删除其浏览器 profile 数据。 + +## 方案与边界 + +- 确认框保留“取消 / 删除实例”安全默认路径;新增默认未勾选的“同时删除 User Data Dir 中的数据”业务复选框。勾选后展示目标目录和不可恢复警告,确认按钮改为“删除实例及数据”。取消、Escape 和窗口关闭均不改变实例或数据。 +- 只有状态为“已退出”或“启动失败”的实例可勾选数据删除;运行、启动/停止、外部关联、外部占用和未知占用时选项禁用,用户仍可仅删除配置。该 UI 状态只是预防性提示,不是删除授权依据。 +- 确认数据删除后,Gio 在后台调用 `InstanceDataDeleter`,以实例 ID、User Data Dir 和请求代次接收结果。删除过程中确认框保持且禁用重复/取消;成功后才删除实例行、持久化配置并恢复列表焦点。失败保持实例、目录、确认框和勾选,以可恢复说明提示停止浏览器或检查权限。 +- `cmd/chub` adapter 在文件删除前重新检查 Chub registry 与 Windows 外部 profile 占用;不确定、检查失败或占用均拒绝。`internal/platform/files.InstanceDataRemover` 只处理绝对、清理后的非卷根真实目录,拒绝普通文件、符号链接或重解析点;目录不存在可视作已清除。不得按浏览器进程名枚举、关闭或接管任何进程。 +- 此次扩展了不可逆确认状态,先更新 `docs/ui/browser-manager.html` 交互原型和 `docs/ui/README.md`。原型仅模拟选择和结果,绝不删除本机文件。 + +## 验收要点 + +- 自动化:新增安全目录删除(成功、相对路径、卷根、普通文件、链接、取消上下文)测试;新增占用复核、UI 默认配置删除、勾选后的后台成功/失败/过期结果及运行态禁用测试。`go test ./...`、`go test -race ./cmd/chub ./internal/ui ./internal/platform/files`、`go vet ./...`、`cmd /c build.bat` 通过。 +- Windows smoke:已退出实例打开删除确认时复选框默认未选,勾选后可见实际目录与不可恢复警告,确认按钮文案改变;删除测试 profile 后其目录和配置均消失。运行中/外部占用实例只能删除配置。故意占用目录、权限拒绝或无效路径时,确认框保留且配置/目录都不变。 +- 交互与无障碍:确认框背景不可交互、默认焦点为取消、Escape 取消;进入删除中状态不显示不可兑现的取消;错误以文字说明影响范围和恢复路径,不显示原始系统错误。窄窗口、200% 缩放、长路径和键盘 Tab/Space/Enter 仍可完成或取消。 + +## 执行记录 + +- 状态:DOING +- 变更:已完成需求、架构、API、用户故事、交互清单、原型验收场景与安全边界定义;待更新原型并提交文档后实现。 +- 验证:待执行。 +- 阻塞:无。 diff --git a/docs/ui/README.md b/docs/ui/README.md index 7044de6..6274e1a 100644 --- a/docs/ui/README.md +++ b/docs/ui/README.md @@ -18,6 +18,7 @@ 8. 点击“设置”:检查四个路径的 Label、选择/搜索按钮;Chrome/Edge 的“选择”必须打开 Windows 原生 `.exe` 文件选择器,取消后保留输入。搜索期间按钮显示“取消”,再次点击取消当前搜索;搜索命中当前路径时在同一字段旁显示已确认反馈,切换页面后状态保持。 9. 双击“运行中”或“外部已关联”实例,点击“查看标签页”:检查读取中、目标列表、只读边界文案与 Escape/关闭后回到入口。未启动或没有实际端口的实例应禁用入口。 10. 设置代理:检查代理名称和代理地址均有 Label;选择项会回填两项,保存修改后实例列表仅显示最新代理名称。新建实例应默认填入程序目录内不同的 `user_data_dirs\\<实例 ID>` 子目录;窄窗口下代理列不能挤压操作按钮。 +11. 点击实例删除:默认确认只删除 Chub 配置。对于已退出实例,勾选“同时删除 User Data Dir 中的数据”后确认按钮改为“删除实例及数据”,并显示目录和不可恢复警告;运行或外部占用实例必须禁用该选项。取消、占用或失败时实例与目录保留。 ## 当前原型假设 @@ -27,6 +28,7 @@ - 设置作为独立顶级页面,采用显式提交;取消只恢复本次未保存的表单修改。 - 路径搜索不进行默认全盘扫描:浏览器搜索标准安装路径,目录搜索验证当前输入;Chrome/Edge 路径通过 Windows 原生文件选择器选择既有 `.exe`,新建实例的 User Data Dir 已接入 Windows 原生目录选择器。 - 外部实例显示占用证据,但不提供接管或强制关闭入口。 +- 实例删除默认只移除配置;数据删除是明确、默认未选中的附加选择,且只能对已退出/启动失败实例发起。实际删除前仍复核占用和目录边界;原型只模拟结果,不执行文件删除。 - “标签页(CDP 页面目标)”只读取已验证 loopback CDP 端点的展示 DTO;不会显示 WebSocket 调试地址,不提供任何页面或浏览器控制。它不是物理窗口管理视图。 - 原型中的“确认”只模拟事件反馈,不启动真实浏览器或改变文件。 @@ -41,5 +43,6 @@ | 路径选择与搜索 | 选择和搜索均通过后台 adapter、独立请求 ID、取消函数与结果通道;迟到结果按字段/任务 ID 丢弃,反馈紧邻发起字段 | | 详情抽屉 | 自定义辅助面板,关闭后焦点返回实例行 | | 确认对话框 | 模态层限制背景交互,Escape 取消,危险操作不默认聚焦 | +| 实例数据删除 | 持久的业务勾选、实例 ID/目录快照/请求代次;后台复核后才允许删除并在成功后持久化配置 | | CDP 页面目标 | 异步只读 DTO、ID/端口/请求快照防过期、Escape 关闭后返回入口焦点 | | Toast/状态区 | 非模态状态反馈;异步结果通过统一事件触发重绘 | diff --git a/docs/ui/browser-manager.html b/docs/ui/browser-manager.html index 1ff0025..8c36397 100644 --- a/docs/ui/browser-manager.html +++ b/docs/ui/browser-manager.html @@ -298,6 +298,7 @@

关闭浏览器实例?

将请求浏览器正常退出。未保存的网页内容由浏览器自行处理。

+

删除实例?

编辑实例

修改下次启动使用的保存配置。

用于在实例列表中识别此浏览器环境。
浏览器类型
必须是绝对路径;同一目录不能被多个实例同时使用。
仅支持 http/https;留空时启动浏览器默认页。
下次启动优先使用;与其他 Chub 实例重复时会提示修改。
选择已保存的无认证代理;修改后下次启动生效。
仅在已验证的运行或外部关联状态显示;此处只读。

标签页(CDP 页面目标)

只读查看经验证的本机 CDP 页面目标。

不会显示 WebSocket 地址,也不会导航、刷新、关闭页面或执行脚本。
正在读取已验证的本地 CDP 页面目标…

放弃未保存的更改?

可以先保存配置,或放弃本次编辑并关闭对话框。

@@ -324,6 +325,8 @@ let unexpectedExitIDs = []; let unexpectedExitFocusID = ''; let tabsRequest = 0; + let pendingInstanceDeleteID = ''; + let instanceDeleteRunning = false; const $ = (id) => document.getElementById(id); const statusClass = (s) => s; const proxyLabel = (id) => proxies.find(p => p.id === id)?.server || '无代理'; @@ -347,7 +350,7 @@ tr.addEventListener('click', e => { if (!e.target.closest('button')) selectRow(x.id, body); }); tr.addEventListener('dblclick', e => { if (!e.target.closest('button')) openEdit(x.id); }); tr.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); openEdit(x.id); } else if (e.key === ' ') { e.preventDefault(); selectRow(x.id, body); } }); - tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectRow(x.id, body); if (b.dataset.action === 'edit') openEdit(x.id); else if (b.dataset.action === 'delete') showToast(`原型演示:将确认删除“${x.name}”的 Chub 配置,不会删除 User Data Dir。`); else if (x.status === 'running') { x.status = 'stopping'; x.statusText = '停止中'; renderRows(); setTimeout(() => { x.status = 'exited'; x.statusText = '已退出'; x.pid = '—'; x.port = 0; renderRows(); showToast(`“${x.name}”已优雅退出。`); }, 700); } else openConfirm(x.id, 'start'); })); + tr.querySelectorAll('button').forEach(b => b.addEventListener('click', e => { e.stopPropagation(); selectRow(x.id, body); if (b.dataset.action === 'edit') openEdit(x.id); else if (b.dataset.action === 'delete') openInstanceDelete(x.id); else if (x.status === 'running') { x.status = 'stopping'; x.statusText = '停止中'; renderRows(); setTimeout(() => { x.status = 'exited'; x.statusText = '已退出'; x.pid = '—'; x.port = 0; renderRows(); showToast(`“${x.name}”已优雅退出。`); }, 700); } else openConfirm(x.id, 'start'); })); body.appendChild(tr); }); $('emptyState').style.display = rows.length ? 'none' : 'block'; $('resultCount').textContent = `显示 ${rows.length} 个实例`; @@ -401,6 +404,14 @@ function showDrawer() { $('drawer').classList.add('open'); $('backdrop').classList.add('open'); $('drawer').setAttribute('aria-hidden', 'false'); setTimeout(() => $('drawer').querySelector('button, input, select')?.focus(), 60); } function hideDrawer() { $('drawer').classList.remove('open'); $('backdrop').classList.remove('open'); $('drawer').setAttribute('aria-hidden', 'true'); } function openConfirm(id, action) { const x = instances.find(i => i.id === id); modalAction = action; $('dialogTitle').textContent = action === 'restart' ? '重启浏览器实例?' : action === 'start' ? '启动浏览器实例?' : '关闭浏览器实例?'; $('dialogText').textContent = action === 'restart' ? `将先请求“${x.name}”正常退出,再使用相同配置重新启动。` : action === 'start' ? `将使用已保存配置启动“${x.name}”。` : `将请求“${x.name}”正常退出。未保存的网页内容由浏览器自行处理。`; $('dialogWarning').hidden = action !== false; $('dialogConfirm').textContent = action === false ? '优雅关闭' : action === 'restart' ? '确认重启' : '启动实例'; $('dialogConfirm').className = action === false ? 'danger-button' : 'primary'; $('confirmDialog').showModal(); } + function canDeleteInstanceData(x) { return ['exited', 'failed'].includes(x.status); } + function updateInstanceDeleteChoice() { const x = instances.find(i => i.id === pendingInstanceDeleteID), checked = $('deleteInstanceData').checked && !$('deleteInstanceData').disabled; $('instanceDeleteWarning').hidden = !checked; $('instanceDeleteWarning').textContent = checked && x ? `将永久删除:${x.dir}。此操作无法撤销;Chub 仍会在执行前重新检查浏览器占用。` : ''; $('instanceDeleteConfirm').textContent = checked ? '删除实例及数据' : '删除实例'; } + function openInstanceDelete(id) { const x = instances.find(i => i.id === id); if (!x) return; pendingInstanceDeleteID = id; instanceDeleteRunning = false; const allowed = canDeleteInstanceData(x); $('instanceDeleteText').textContent = `确定删除“${x.name}”的 Chub 实例配置吗?默认会保留 User Data Dir。`; $('deleteInstanceData').checked = false; $('deleteInstanceData').disabled = !allowed; $('instanceDeleteUnavailable').hidden = allowed; $('instanceDeleteUnavailable').textContent = allowed ? '' : '检测到浏览器正在运行、启动/停止或占用;请先停止并刷新状态后再删除数据。当前仍可只删除实例配置。'; $('instanceDeleteCancel').disabled = false; $('instanceDeleteConfirm').disabled = false; updateInstanceDeleteChoice(); $('instanceDeleteDialog').showModal(); setTimeout(() => $('instanceDeleteCancel').focus(), 0); } + function closeInstanceDelete() { if (instanceDeleteRunning) return; $('instanceDeleteDialog').close(); pendingInstanceDeleteID = ''; } + $('deleteInstanceData').onchange = updateInstanceDeleteChoice; + $('instanceDeleteCancel').onclick = closeInstanceDelete; + $('instanceDeleteDialog').addEventListener('cancel', e => { e.preventDefault(); closeInstanceDelete(); }); + $('instanceDeleteConfirm').onclick = () => { const x = instances.find(i => i.id === pendingInstanceDeleteID), withData = $('deleteInstanceData').checked && !$('deleteInstanceData').disabled; if (!x || instanceDeleteRunning) return; if (!withData) { instances.splice(instances.indexOf(x), 1); $('instanceDeleteDialog').close(); pendingInstanceDeleteID = ''; renderRows(); showToast(`已删除“${x.name}”的实例配置;User Data Dir 未被删除。`); return; } instanceDeleteRunning = true; $('instanceDeleteCancel').disabled = true; $('instanceDeleteConfirm').disabled = true; $('instanceDeleteConfirm').textContent = '正在删除数据…'; setTimeout(() => { const current = instances.find(i => i.id === pendingInstanceDeleteID); if (current) instances.splice(instances.indexOf(current), 1); $('instanceDeleteDialog').close(); pendingInstanceDeleteID = ''; instanceDeleteRunning = false; renderRows(); showToast(`原型演示:已删除“${x.name}”的实例配置及 User Data Dir 数据。`); }, 700); }; function showToast(message) { const toast = $('toast'); toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 3600); } function hasOtherModal() { return [...document.querySelectorAll('dialog[open]')].some(dialog => dialog.id !== 'unexpectedExitDialog'); } function queueUnexpectedExit(id) {