docs: define instance data deletion

This commit is contained in:
QiuSW
2026-07-27 22:59:10 +08:00
parent 03b23045a7
commit e400172d04
10 changed files with 75 additions and 3 deletions
+2
View File
@@ -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 与外部占用,拒绝相对路径、卷根、普通文件和符号链接/重解析点。数据删除成功后才移除实例配置;检查、占用或删除失败时实例配置和目录均保留,并给出可恢复提示。
## 不做
+1
View File
@@ -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。
+1
View File
@@ -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
+5
View File
@@ -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 都保持,用户可关闭浏览器、检查权限后重试。
+2
View File
@@ -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 失败时保留当前实例、勾选和弹层,并说明目录与配置均未删除、可先停止浏览器或检查权限后重试。
+12
View File
@@ -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
+3 -2
View File
@@ -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。
## 当前目录
+34
View File
@@ -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、用户故事、交互清单、原型验收场景与安全边界定义;待更新原型并提交文档后实现。
- 验证:待执行。
- 阻塞:无。
+3
View File
@@ -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/状态区 | 非模态状态反馈;异步结果通过统一事件触发重绘 |
+12 -1
View File
@@ -298,6 +298,7 @@
<div class="drawer-backdrop" id="backdrop"></div>
<aside class="drawer" id="drawer" aria-label="实例详情和配置" aria-hidden="true"></aside>
<dialog id="confirmDialog" aria-labelledby="dialogTitle"><div class="dialog-inner"><h2 id="dialogTitle">关闭浏览器实例?</h2><p id="dialogText">将请求浏览器正常退出。未保存的网页内容由浏览器自行处理。</p><div class="dialog-warning" id="dialogWarning" hidden>强制终止可能造成 profile 数据损坏。仅在浏览器无响应时使用。</div><div class="dialog-actions"><button class="secondary" id="dialogCancel">取消</button><button class="primary" id="dialogConfirm">优雅关闭</button></div></div></dialog>
<dialog id="instanceDeleteDialog" aria-labelledby="instanceDeleteTitle" aria-describedby="instanceDeleteText"><div class="dialog-inner"><h2 id="instanceDeleteTitle">删除实例?</h2><p id="instanceDeleteText"></p><label class="check-line"><input type="checkbox" id="deleteInstanceData"><span><b>同时删除 User Data Dir 中的数据</b><br><small class="help">默认不删除。目录中的浏览器数据删除后无法恢复。</small></span></label><div class="form-note" id="instanceDeleteUnavailable" hidden></div><div class="dialog-warning" id="instanceDeleteWarning" hidden></div><div class="dialog-actions"><button class="secondary" id="instanceDeleteCancel">取消</button><button class="danger-button" id="instanceDeleteConfirm">删除实例</button></div></div></dialog>
<dialog class="edit-dialog" id="editDialog" aria-labelledby="editTitle"><form class="dialog-inner edit-form" id="editForm"><div><h2 id="editTitle">编辑实例</h2><p id="editSubtitle">修改下次启动使用的保存配置。</p></div><div class="field"><label for="editName">实例名称 <span>*</span></label><input id="editName" required><small>用于在实例列表中识别此浏览器环境。</small></div><div class="field"><span>浏览器类型</span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="editKind" value="chrome" id="editChrome"> Chrome</label><label><input type="radio" name="editKind" value="edge" id="editEdge"> Edge</label></div></div><div class="field"><label for="editDataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="editDataDir" required><button type="button" class="secondary" id="chooseEditDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="editURL">启动 URL(可选)</label><input id="editURL" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="editPreferredPort">首选调试端口</label><input id="editPreferredPort" type="number" min="1024" max="65535" inputmode="numeric"><small>下次启动优先使用;与其他 Chub 实例重复时会提示修改。</small></div><div class="field"><label for="editProxy">代理</label><select id="editProxy"><option value="">无代理</option></select><small>选择已保存的无认证代理;修改后下次启动生效。</small></div><div class="field"><label for="editPort">实际调试端口</label><input class="readonly-value" id="editPort" readonly><small>仅在已验证的运行或外部关联状态显示;此处只读。</small></div><div class="form-note" id="editReadOnlyNote" hidden>该实例正在运行或为外部关联。为避免配置与运行身份不一致,浏览器类型、User Data Dir 和首选端口已锁定。</div><div class="dialog-actions"><button type="button" class="secondary" id="editTabs">查看标签页</button><button type="button" class="secondary" id="editCancel">取消</button><button type="submit" class="primary" id="editSave">保存更改</button></div></form></dialog>
<dialog class="tabs-dialog" id="tabsDialog" aria-labelledby="tabsTitle" aria-describedby="tabsMeta"><div class="dialog-inner"><div><h2 id="tabsTitle">标签页(CDP 页面目标)</h2><p id="tabsSubtitle">只读查看经验证的本机 CDP 页面目标。</p></div><div class="tabs-meta" id="tabsMeta">不会显示 WebSocket 地址,也不会导航、刷新、关闭页面或执行脚本。</div><div class="tabs-state" id="tabsState" role="status" aria-live="polite">正在读取已验证的本地 CDP 页面目标…</div><div class="tabs-target-list" id="tabsTargetList" hidden></div><div class="dialog-actions"><button class="primary" id="tabsClose">关闭</button></div></div></dialog>
<dialog id="discardDialog" aria-labelledby="discardTitle"><div class="dialog-inner"><h2 id="discardTitle">放弃未保存的更改?</h2><p>可以先保存配置,或放弃本次编辑并关闭对话框。</p><div class="dialog-actions"><button class="secondary" id="keepEditing">继续编辑</button><button class="secondary" id="discardChanges">不保存</button><button class="primary" id="saveChanges">保存</button></div></div></dialog>
@@ -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) {