docs: add desktop pet feature design

Adds the full documentation set for the planned desktop pet feature under
docs/features/desktop-pet: README, PRD, user stories, logic design, UI
design, and technical design. The pet is scoped as a lightweight always-on
entry point for quick drafting, drag-and-drop staging, and task status at a
glance; it does not introduce a second Ask/Execute channel, task execution
path, or independent task-state store. Design only, no implementation yet.
This commit is contained in:
mesalogo
2026-09-03 23:22:33 +08:00
parent 4e60a4ca61
commit 374968d50b
6 changed files with 1208 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
# 桌面宠物
桌面宠物是 GoodBuddy 桌面端一个可选的常驻悬浮入口,用于随时发起提问、暂存文件或文字,
并直观查看当前任务状态。桌面宠物不建立新的对话、任务或执行体系:它只是现有 Conversation、
Task 和 Runtime 能力的一个轻量触发面和状态镜面。
## 核心边界
- 桌面宠物**不**新增 Ask/Execute 执行通道,也不在自身窗口内直接展示模型回复。快速发起
和拖放暂存的内容始终交给主窗口现有输入区确认后发送,从不自动发送或自动执行。
- 桌面宠物**不**持续读取屏幕内容、剪贴板或全局选中文本。
- 桌面宠物显示的任务状态、通知和历史记录均来自现有 Task/Conversation 数据,不建立第二套
存储或索引。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、范围、功能需求和产品验收 |
| [User Stories](./user-stories.md) | 首次启用、正常使用、失败恢复、兼容和边界场景 |
| [功能逻辑](./logic-design.md) | 不变量、状态维度、决策表和失败结果 |
| [UI 设计](./ui-design.md) | 窗口交互、设置入口、状态呈现、无障碍和响应式行为 |
| [技术设计](./technical-design.md) | 窗口与进程边界、IPC 边界、持久化、生命周期和验证策略 |
功能尚未实施,因此不创建 `progress.md`;实施开始后按 [`docs/features/AGENTS.md`](../AGENTS.md)
的证据要求补充。
## 术语
- **桌面宠物窗口**:应用启用该功能后创建的常驻、无边框、可置顶悬浮窗口。
- **快速发起**:点击宠物打开的小型输入框,用于输入文字或附加单个文件,提交后转入主窗口。
- **暂存**:快速发起或拖放产生的文字和附件在提交前的中间状态,不代表已发送或已执行。
- **状态镜面**:宠物窗口对当前任务状态(等待处理、执行中、已完成、失败、空闲)的展示,
数据权威来自 Task 与 Conversation,不是独立状态源。
- **安静模式**:抑制宠物完成/失败气泡提示的设置,可访问的状态查询不受影响。
## 相关功能
- [Task 与 Job 文档集](../task-and-job/README.md):任务状态、状态优先级和 Task Center
导航的权威定义,宠物的状态镜面必须与其保持一致,不重新定义任务生命周期。
- [助手工作栏](../assistant-workbar/README.md):主窗口执行空间和输入区的权威定义,宠物的
快速发起和拖放暂存最终落地到该输入区,不复制其行为。
- [直连模型 Agent 能力](../direct-model-agent/README.md)、[远程主机与远程执行](../remote-host/README.md):
Ask/Execute 授权边界的权威定义,宠物不扩大或绕过该边界。
- [文档处理](../document-processing/README.md):拖放文件解析和格式支持的权威定义,宠物拖放
暂存复用同一处理管线,不重新定义支持类型。
- 根 [`UI-DESIGN.md`](../../../UI-DESIGN.md):全局视觉、通知视口、Switch/Checkbox 语义和
无障碍规则的权威定义。
- [`docs/development/release-runbook.md`](../../development/release-runbook.md):跨平台打包
矩阵和交付物的权威定义。
+203
View File
@@ -0,0 +1,203 @@
# 桌面宠物功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| User Stories | [桌面宠物 User Stories](./user-stories.md) |
| 任务模型 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
本文负责桌面宠物自身的状态、不变量和决策规则,不重新定义 Task、Conversation、Ask/
Execute 或附件的既有语义;这些语义以各自权威文档为准。
## 1. 核心不变量
### INV-1 宠物不是第二条执行通道
快速起草和拖放暂存只产生主窗口会话输入区的草稿数据(文本、附件引用),不产生消息、
不产生 Task、不触发模型调用。发送、模式选择和 Runtime 选择继续完全由主窗口既有流程
拥有。
### INV-2 状态镜面是既有状态的投影
宠物状态镜面的取值只能从当前 Task 集合的既有状态推导,不允许引入宠物专属的任务状态
字段或独立于 Task Center 的状态存储。宠物状态与 Task Center 展示的状态出现分歧即视为
缺陷。
### INV-3 总开关是唯一的存在性开关
“启用桌面宠物”关闭时,宠物窗口、气泡和托盘条目文案必须整体消失;除该开关外,任何
子设置(外观、位置、安静模式)的变化都不得单独创建或销毁宠物窗口。
### INV-4 暂时隐藏不影响启用状态
“暂时隐藏”只影响窗口可见性,不修改总开关持久化值,也不清除位置、外观或安静模式设置。
### INV-5 提示与原生通知互斥
同一个任务终态事件(完成或失败)只能产生宠物气泡或现有原生系统通知中的一种,不产生
两种同时可见的独立提示。
### INV-6 位置始终落在可用显示器内
宠物窗口的持久化位置在应用于当前显示器拓扑之前必须被校验;不能得出宠物窗口完全位于
任何当前可见显示器之外的最终坐标。
### INV-7 拖放与起草共享暂存规则
拖放产生的文本和文件必须满足与快速起草附件相同的类型、大小、数量限制;两者不允许存在
两套互不一致的限制值。
## 2. 状态维度
### 2.1 总开关与可见性
```text
disabled -> enabled_visible
enabled_visible <-> enabled_hidden (暂时隐藏 / 重新显示)
enabled_visible|enabled_hidden -> disabled (关闭总开关)
```
`disabled` 是唯一没有宠物窗口存在的状态;`enabled_hidden` 窗口不可见但设置全部保留。
### 2.2 快速起草弹出框
```text
closed -> drafting -> submitted | discarded
drafting -> closed (Escape / 点击外部 / 关闭按钮)
```
`submitted` 是终态转换动作,不是持续状态:提交后弹出框立即回到 `closed`,草稿数据已经
转交给主窗口。
### 2.3 拖放交互
```text
idle -> drag_over -> dropped_accepted | dropped_rejected | drag_cancelled
```
`drag_over` 只影响视觉反馈,不产生任何持久数据;只有 `dropped_accepted` 才产生暂存
数据。`dropped_rejected` 用于文件夹或全部文件超限的场景,不产生暂存也不打开主窗口。
### 2.4 状态镜面
```text
unavailable | idle | completed_recent | running | waiting_approval | failed_recent
```
取值由当前活跃与最近终态 Task 集合计算得出,属于派生状态,不单独持久化。
### 2.5 提示气泡
```text
none -> pending -> shown -> dismissed
```
`pending` 表示事件已发生但由于主窗口可见或安静模式而被抑制,不进入 `shown`。
## 3. 状态镜面优先级
宠物在任意时刻只呈现一个状态镜面取值。存在多个活跃 Task 时按以下优先级取最高项:
| 优先级 | 状态 | 条件 |
| --- | --- | --- |
| 1 | `unavailable` | 用于确认状态的 Runtime 或网络当前不可达 |
| 2 | `waiting_approval` | 至少一个 Task 处于等待批准 |
| 3 | `failed_recent` | 至少一个 Task 在最近的“最近失败”窗口内失败,且用户尚未查看 |
| 4 | `running` | 至少一个 Task 处于执行中 |
| 5 | `completed_recent` | 至少一个 Task 在最近的“最近完成”窗口内完成,且用户尚未查看 |
| 6 | `idle` | 以上条件都不满足 |
“最近失败”“最近完成”窗口的具体时长和“已查看”判定属于技术选择,由
[技术设计](./technical-design.md)基于既有 Task 已读/未读语义确定,不在本文固定具体
数值。
## 4. 提示归并决策表
| 事件 | 主窗口状态 | 安静模式 | 结果 |
| --- | --- | --- | --- |
| 任务完成 | 不可见或未聚焦 | 关闭 | 显示宠物气泡;不触发原生通知 |
| 任务完成 | 不可见或未聚焦 | 开启 | 不显示气泡;不触发原生通知;仅更新状态镜面 |
| 任务完成 | 可见并聚焦 | 任意 | 不显示气泡;反馈通过主窗口既有机制呈现 |
| 任务失败 | 不可见或未聚焦 | 关闭 | 显示宠物气泡并保持可见直到用户查看或关闭 |
| 任务失败 | 不可见或未聚焦 | 开启 | 不显示气泡;仅更新状态镜面为 `failed_recent` |
| 任务失败 | 可见并聚焦 | 任意 | 不显示气泡;反馈通过主窗口既有机制呈现 |
“主窗口可见并聚焦”与现有 [`desktop-notification.ts`](../../../src/main/desktop-notification.ts)
判定原生通知是否触发所使用的条件保持一致,避免宠物和原生通知各自使用不同的可见性
判定导致同一事件在两处呈现或同时缺失。
## 5. 快速起草与拖放的目标会话决策表
| 场景 | 目标会话 |
| --- | --- |
| 主窗口存在当前活跃会话,用户未在弹出框内更改目标 | 该活跃会话 |
| 用户在弹出框内选择“新建对话” | 提交时新建并激活一个会话 |
| 主窗口不存在任何会话 | 提交时新建并激活一个会话 |
| 目标会话在提交前被用户在主窗口中删除 | 视为“不存在任何会话”,回退到新建会话 |
拖放没有目标切换界面,始终采用与快速起草相同的默认目标会话规则。
## 6. 附件与文本限制
快速起草和拖放暂存复用现有聊天附件规则,不定义新的限制数值:
- 单条草稿的附件数量上限、单个附件大小上限、支持的文本与图片类型,与主窗口聊天附件
的现有限制一致。
- 拖放同时包含多个文件时,逐个按上述规则校验;不合规文件被拒绝,不影响合规文件继续
暂存。
- 拖放文件夹被整体拒绝,不做递归展开或部分接受。
## 7. 位置与显示器校验
1. 加载持久化位置后,检查该坐标是否位于当前任一可见显示器的可用区域内。
2. 位于范围内:直接使用。
3. 不在范围内(例如记忆的显示器已断开):改用主显示器上一个默认可见位置,并在下一次
用户拖动宠物后覆盖持久化位置。
4. 开启“贴边”时,拖动释放位置与最近边缘的距离小于吸附阈值即吸附到该边缘;阈值和默认
位置的具体数值属于技术选择。
## 8. 失败与恢复
| 场景 | 行为 |
| --- | --- |
| 主窗口进程尚未创建或已被销毁 | 提交快速起草或拖放时先创建/恢复主窗口,再写入草稿 |
| 写入草稿时主窗口窗口对象不可用 | 提交失败,弹出框保留草稿内容供用户重试,不静默丢弃 |
| 状态查询所需的 Runtime/网络不可达 | 状态镜面显示 `unavailable`,不回退显示 `idle` |
| 显示器拓扑变化导致位置越界 | 按第 7 节回退到默认可见位置,不隐藏窗口 |
| 用户在气泡展示期间关闭总开关 | 气泡与宠物窗口一并立即消失 |
| 应用启动时总开关为开启但窗口创建失败 | 记录失败原因,不阻塞主窗口正常启动,托盘菜单提供重试入口 |
## 9. 需求追踪
| 需求 | 主要逻辑 |
| --- | --- |
| `FR-1` | `INV-3`、总开关与可见性状态 |
| `FR-2` | 位置与显示器校验、`INV-6` |
| `FR-3` | `INV-1`、快速起草状态、目标会话决策表 |
| `FR-4` | `INV-1`、`INV-7`、拖放交互状态、附件与文本限制 |
| `FR-5` | `INV-2`、状态镜面优先级 |
| `FR-6` | `INV-5`、提示归并决策表 |
| `FR-7` | 提示归并决策表中安静模式列 |
| `FR-8` | 位置与显示器校验之外的技术选择,逻辑仅约束持久化立即生效 |
| `FR-9` | `INV-4`、总开关与可见性状态 |
| `FR-10` | 快速起草状态中的键盘转换,具体按键映射见 UI 设计 |
| `FR-11` | `INV-1`、`INV-2`、`INV-7` |
## 10. 逻辑完整性评估
本设计覆盖了启用/关闭、暂时隐藏、快速起草、拖放(含超限和文件夹)、状态镜面优先级、
提示与原生通知的归并、位置越界恢复、以及主窗口不可用时的失败路径。
未在本文固定、留给技术设计选择的实现细节:
- “最近完成/最近失败”窗口的具体时长和“已查看”判定的确切事件来源。
- 贴边吸附阈值和默认回退位置的具体像素值。
- 状态查询的具体轮询或事件驱动机制。
这些是技术选择而非未解决的产品决策:本文的优先级和决策表已经足以在任何取值下保持
行为一致。
+158
View File
@@ -0,0 +1,158 @@
# 桌面宠物 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 适用产品 | GoodBuddy Desktop |
| 关联文档 | [功能逻辑](./logic-design.md)、[UI 设计](./ui-design.md)、[技术设计](./technical-design.md) |
## 1. 背景
GoodBuddy 当前只能通过主窗口发起提问、查看任务和管理执行空间。用户想要快速提问、丢一个
文件进去,或者只是看一眼任务有没有跑完时,仍必须先唤出并聚焦整个主窗口。这在多任务、多
显示器场景下打断当前工作流。
桌面宠物提供一个常驻、体积小、不抢焦点的悬浮入口,承担三类高频、轻量的动作:快速起草
一个问题、拖放文件或文字暂存、以及查看当前任务状态。它不是新的执行通道,只是现有
Conversation、Task 和 Runtime 能力上的一层轻量触发面和状态镜面。
## 2. 产品目标
1. 用户无需先打开或聚焦主窗口即可起草一条消息或暂存一个文件。
2. 用户可以在不打开 Task Center 的情况下,一眼看到当前是否有任务在跑、等待处理或失败。
3. 桌面宠物默认不产生新的干扰:不主动弹窗抢焦点,不重复现有原生通知。
4. 桌面宠物的开关、位置、外观和提示强度完全由用户控制,且可随时完全关闭并恢复到没有该
功能之前的界面状态。
5. 桌面宠物不新增或绕过 Ask/Execute 授权边界,也不建立第二条消息发送或任务执行路径。
## 3. 功能范围
### 3.1 包含
- 设置中的“桌面宠物”一级分类,包含总开关、外观、位置和提示设置。
- 常驻悬浮窗口:可拖动、跨显示器记忆位置、贴边、置顶开关。
- 点击宠物打开的“快速起草”弹出框:输入文字、附加至多一个本机文件,提交后转入主窗口
对应会话的输入区草稿,不自动发送。
- 拖放文字或文件到宠物窗口,效果与快速起草的附件路径一致。
- 状态镜面:显示当前活跃任务的汇总状态,点击后打开 Task Center 中对应任务。
- 完成/失败气泡提示,及独立于原生通知的安静模式开关。
- 从有限的内置角色/外观中选择,调整尺寸、透明度和动画强度。
- 全局显示/隐藏开关,托盘菜单入口,键盘可达的等效操作路径。
### 3.2 不包含
- 在宠物窗口内直接展示模型回复、执行工具调用或建立第二条 Ask/Execute 通道。
- 未经用户确认自动发送消息或自动执行任务。
- 持续读取屏幕内容、剪贴板或系统级选中文本。
- 用户上传任意自定义头像图片、养成机制、货币或每日签到。
- 多个宠物实例、宠物之间互动或联网社交功能。
- 管理员远程强制启用、配置或监控用户的桌面宠物。
- 替代 Task Center、助手工作栏或现有通知系统的权威数据来源。
## 4. 功能需求
### FR-1 总开关与默认状态
- 设置新增“桌面宠物”一级分类,包含“启用桌面宠物”总开关。
- 全新安装和现有用户升级后默认关闭,不在未经用户操作时出现在桌面上。
- 关闭总开关立即隐藏宠物窗口,保留用户此前的位置、外观和提示设置以便重新开启。
### FR-2 常驻悬浮窗口
- 启用后创建一个无边框、默认置顶、不出现在任务栏的小型窗口。
- 用户可拖动到任意位置;位置按显示器分别记忆,显示器断开重连后回退到最近的可用显示器。
- 提供“贴边”选项,拖动到屏幕边缘附近时自动吸附。
- 提供独立的“始终置顶”开关;关闭后宠物仍可见,但可被其他窗口遮挡。
- 宠物窗口不接受系统级最大化、最小化或调整大小操作。
### FR-3 快速起草
- 点击宠物打开一个紧邻宠物的小型弹出框,包含文本输入和“附加文件”按钮。
- 文本输入支持多行、粘贴图片;附加文件复用现有附件类型、数量和大小限制。
- 弹出框显示将要进入的目标:当前活跃会话,或“新建对话”。用户可在弹出框内切换目标,
但不能在弹出框内选择模型、Runtime 或 Ask/Execute 模式。
- 提交后主窗口被显示或聚焦,目标会话被激活,文字和附件作为草稿填入该会话的输入区,
输入焦点落在该输入区。提交动作本身不发送消息、不创建任务。
- 取消或关闭弹出框放弃当前草稿,不影响主窗口任何已有草稿。
### FR-4 拖放暂存
- 用户可以把本机文件或选中文字拖到宠物窗口上。
- 拖入时宠物给出可放置的视觉反馈;释放后遵循与快速起草提交相同的行为:显示主窗口、
激活目标会话、把内容放入该会话输入区草稿。
- 拖放文件的类型、数量、大小限制与现有聊天附件限制一致;超出限制时明确提示被拒绝的
文件,其余合法文件仍正常暂存。
- 拖放不支持文件夹;文件夹被拖入时提示不支持,不递归展开。
### FR-5 状态镜面
- 宠物在空闲、执行中、等待处理、已完成、失败之间切换外观,反映当前所有活跃 Task 的
汇总状态;多个状态并存时按“等待处理 > 失败 > 执行中 > 已完成 > 空闲”的优先级显示。
- 点击状态镜面打开或聚焦主窗口的 Task Center,并定位到触发当前状态的任务。
- Runtime 或网络不可用导致任务状态无法确认时,状态镜面显示“状态不可用”而不是回退成
“空闲”。
### FR-6 完成与失败提示
- 任务从执行中或等待处理转为已完成或失败,且主窗口不可见或未聚焦时,宠物旁显示一条
气泡,说明任务标题和结果。
- 气泡在用户不操作时于一段时间后自动消失;失败气泡保持可见直到用户查看或手动关闭。
- 点击气泡等价于点击状态镜面,打开对应任务详情。
- 同一事件只能触发桌面宠物气泡或现有原生系统通知中的一种,不同时出现两条提示;具体
归并规则由[功能逻辑](./logic-design.md)定义。
### FR-7 安静模式
- 独立于总开关的“安静模式”开关,开启后不再显示完成/失败气泡。
- 安静模式不影响状态镜面本身的外观切换,也不影响原生系统通知的现有行为。
### FR-8 外观与个性化
- 提供数量有限的内置角色供选择,不支持上传自定义图片。
- 提供尺寸、透明度和动画强度调整;调整立即生效并持久化。
- 提供“恢复默认外观”操作。
### FR-9 显示、隐藏与退出
- 提供独立于总开关的“暂时隐藏”动作:隐藏宠物窗口但保留启用状态,可通过托盘菜单或
设置重新显示。
- 托盘菜单新增“显示/隐藏桌面宠物”入口,随当前可见性变化文案。
- 应用退出时宠物窗口一并关闭,不残留独立进程。
### FR-10 键盘可达性
- 宠物窗口本身可通过系统级窗口切换(如 Alt+Tab)取得焦点,取得焦点后可用键盘打开快速
起草弹出框、在状态镜面和提示之间移动,并可用 Escape 关闭弹出框。
- 所有交互提供可访问名称和状态说明,不仅依赖动画或颜色表达状态。
### FR-11 与现有能力的边界
- 快速起草和拖放暂存最终使用主窗口现有的会话、附件、模型和 Ask/Execute 选择逻辑,不
新增第二套发送或授权路径。
- 状态镜面和气泡的任务状态数据来自现有 Task 与 Conversation 记录,不建立独立存储、不
与 Task Center 的状态定义产生分歧。
- 桌面宠物不改变现有全局快捷键、托盘菜单既有条目或主窗口默认行为。
## 5. 产品验收
- 全新安装和已升级用户默认看不到桌面宠物;显式启用后立即出现在预期位置。
- 关闭总开关后桌面上不再有任何宠物窗口或残留提示,重新开启后恢复此前的位置和外观。
- 通过快速起草或拖放提交的内容,在主窗口对应会话的输入区中可见为未发送草稿,用户需要
自行确认发送;关闭 GoodBuddy 或取消草稿不会产生任何任务或消息记录。
- 状态镜面呈现的状态与 Task Center 中对应任务的状态一致;两者出现分歧即视为缺陷。
- 同一任务完成或失败事件不会同时产生宠物气泡和原生系统通知。
- 安静模式开启时不出现气泡,但状态镜面外观仍随任务状态变化。
- 键盘用户无需鼠标即可打开快速起草、查看状态并关闭弹出框。
- 中英文文案、深浅主题、多显示器和 DPI 缩放下宠物窗口位置和内容均正确显示。
## 6. 明确限制
- 桌面宠物不提供在其自身窗口内完成整段对话或查看历史消息的能力;查看完整回复必须进入
主窗口。
- 桌面宠物不支持多实例、多角色同屏或角色之间的模拟互动。
- 透明背景效果依赖操作系统合成器;在不支持透明合成的 Linux 桌面环境下,宠物窗口可能
退化为不透明的小型窗口,具体行为见[技术设计](./technical-design.md)。
@@ -0,0 +1,335 @@
# 桌面宠物技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| UI 设计 | [UI 设计](./ui-design.md) |
## 1. 进程与窗口边界
```text
Main
-> createMainWindow() (既有,src/main/window.ts)
-> createPetWindow() (新增)
-> 独立 BrowserWindow:无边框、透明、置顶、不入任务栏
-> 独立 preload:src/preload/pet.ts(新增,窄 API)
-> 加载独立渲染入口:src/renderer/pet.html + src/renderer/src/pet/
Renderer(主窗口,既有)
-> window.goodbuddy(既有 DesktopApi,不变)
Renderer(宠物窗口,新增)
-> window.goodbuddyPet(新增窄 API,与既有 window.goodbuddy 完全隔离)
```
宠物窗口是与主窗口平级的第二个 `BrowserWindow`,不是主窗口的子窗口、不是 `WebContentsView`
叠加层。理由:
- 主窗口现有 `setWindowOpenHandler` 拒绝一切子窗口(`src/main/window.ts`),沿用该策略
比引入例外更简单。
- 宠物窗口需要独立于主窗口的显示/隐藏、位置和置顶状态,作为子窗口会继承父窗口的显示
状态,不满足“主窗口隐藏时宠物仍可见”的产品要求。
不复用主窗口的 preload 和渲染入口:主窗口 preload 暴露的 `DesktopApi` 面积很大(对话、
知识库、设备共享等)。给宠物窗口加载同一 preload 会让一个新增的、长期运行、暴露给
远程可控网页内容风险更低但攻击面更广的窗口拥有不必要的能力。新增窄 preload 遵循仓库
现有的“只暴露显式方法”约束。
## 2. 模块职责
| 模块 | 位置 | 职责 |
| --- | --- | --- |
| `PetWindowService` | `src/main/pet/pet-window-service.ts`(新增) | 创建/销毁宠物窗口、位置持久化与越界校验、置顶与贴边、显示/隐藏 |
| `PetSettingsStore` | `src/main/pet/pet-settings-store.ts`(新增) | 总开关、外观、位置、安静模式的持久化,遵循既有 settings-file-utils 模式 |
| `PetStatusProjector` | `src/main/pet/pet-status-projector.ts`(新增) | 从既有 Task 查询结果计算状态镜面取值,不引入新状态源 |
| `PetDraftBridge` | `src/main/pet/pet-draft-bridge.ts`(新增) | 把宠物提交的文本/附件转交主窗口渲染进程,驱动会话激活与草稿写入 |
| `PetNotificationCoordinator` | `src/main/pet/pet-notification-coordinator.ts`(新增) | 与既有 `desktop-notification.ts` 协商同一事件只走一条提示路径 |
| 既有 `context-manager.ts` | 不变 | 宠物附件校验复用其 `selectFiles`/校验规则,不新增第二套限制 |
| 既有 `assistant-database.ts` / Task 查询 | 不变 | 状态镜面查询的唯一数据来源 |
`PetStatusProjector` 只读取既有 Task 查询结果并按
[功能逻辑第 3 节](./logic-design.md#3-状态镜面优先级)的优先级归并,不写入、不缓存跨进程
重启的状态;重启后状态镜面从当前 Task 集合重新计算。
## 3. 宠物窗口创建
```ts
// src/main/pet/pet-window-service.ts(示意)
function createPetWindow(): BrowserWindow {
return new BrowserWindow({
width: PET_SIZE.width,
height: PET_SIZE.height,
show: false,
frame: false,
transparent: true,
resizable: false,
skipTaskbar: true,
alwaysOnTop: true, // 由“始终置顶”设置决定实际值
hasShadow: false,
backgroundColor: '#00000000',
webPreferences: {
preload: join(currentDirectory, '../preload/pet.cjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
})
}
```
- `transparent: true` 依赖操作系统合成器。按 PRD 6 节明确限制,在不支持透明合成的
Linux 桌面环境(例如未运行合成器的部分窗口管理器)下,Electron/Chromium 会把窗口
渲染为不透明;`PetWindowService` 不检测合成器能力,只在渲染进程内把背景色设置为
当前主题的画布色作为不透明回退,保证内容仍然可读,不尝试探测或声明支持特定合成器。
- `alwaysOnTop` 的实际值由“始终置顶”Switch 驱动,通过 `window.setAlwaysOnTop(bool)`
在设置变更时更新,不需要重建窗口。
- 复用既有 `resolveWindowIcon`/任务栏排除逻辑作为参考,但宠物窗口 `skipTaskbar: true`
固定为真,不提供用户设置。
- 宠物窗口不设置 `parent`,与主窗口是同级独立窗口;应用单实例锁和 `second-instance`
处理(`src/main/index.ts`)不需要改动,因为宠物窗口的存在与否由 `PetWindowService`
内部状态决定,而不是第二个 Electron 实例。
## 4. 拖放实现
Electron 渲染进程原生支持 HTML5 拖放;宠物渲染进程监听 `dragenter`/`dragover`/`drop`
即可获得视觉反馈和释放事件,不需要 Main 参与拖放事件本身。文件路径解析复用既有
`webUtils.getPathForFile`(已用于 `src/preload/index.ts` 的知识库拖放导入):
```ts
// src/preload/pet.ts(示意)
import { contextBridge, ipcRenderer, webUtils } from 'electron'
contextBridge.exposeInMainWorld('goodbuddyPet', {
submitDraft: (input: PetDraftSubmission) =>
ipcRenderer.invoke(petIpcChannels.submitDraft, input),
resolveDroppedPaths: (files: File[]) =>
files.map((file) => webUtils.getPathForFile(file)).filter(Boolean),
// ...状态订阅、设置读写等方法
})
```
拖入的文本(非文件)通过 `dataTransfer.getData('text/plain')` 直接读取,随普通文本
字段一起提交,不经过路径解析。
宠物渲染进程把解析出的路径和文本打包为 `PetDraftSubmission`,通过 IPC 交给 Main;Main
侧的路径校验、类型判断、大小限制和读取,全部复用既有 `ContextManager` 的规则(`src/main/context-manager.ts`
中的 `supportedExtensions`、`supportedImageExtensions`、`supportedDocumentExtensions`、
`maximumContextBytes`、`maximumAttachmentsPerMessage`),不新增校验逻辑。`ContextManager`
目前的公开入口是围绕原生对话框(`selectFiles`)设计的;实现时需要新增一个接受任意
canonical 路径数组的方法(例如 `stageFromPaths`),内部复用现有扩展名/大小/文档解析
分支,而不是复制一份判断逻辑。文件夹路径(`fs.stat` 判定为目录)在该方法入口即被拒绝,
不递归遍历。
## 5. 快速起草提交与主窗口联动
```text
宠物渲染进程 submitDraft(input)
-> Main: PetDraftBridge.handleSubmit(input)
-> ContextManager.stageFromPaths(input.filePaths) (复用既有校验)
-> 若主窗口不存在:createMainWindow() + loadMainWindow()(复用既有函数)
-> showWindow(mainWindow)(既有函数)
-> mainWindow.webContents.send(petIpcChannels.draftReceived, {
targetConversationId, text, stagedContextIds
})
-> 主窗口渲染进程(App.tsx 新增一个事件监听,与既有 conversation:new 监听模式一致)
-> 若 targetConversationId 无效或已删除:回退为新建会话(既有新建会话路径)
-> 激活目标会话、把 text 写入该会话输入框状态、把 stagedContextIds 追加到该
会话的附件状态(既有 App.tsx 附件状态更新路径)
-> 聚焦输入框
```
- 草稿只在主窗口渲染进程内变成“输入框状态”和“待发送附件列表”,与用户手动打字/手动
点击附件按钮产生的状态是同一份 React state,不建立并行的草稿存储。这直接满足
`INV-1`:宠物不产生消息、不产生 Task。
- `stagedContextIds` 复用现有 `context:*` IPC 产生的 Context ID;主窗口把这些 ID 加入
附件列表的方式,与用户通过“附加文件”按钮选择文件后的状态更新完全一致。
- 若提交发生时主窗口曾被销毁(用户从未打开过,或者提前研究表明当前架构不销毁主窗口
只隐藏),`PetDraftBridge` 调用既有 `createMainWindow`/`loadMainWindow`,等待
`ready-to-show` 完成后再发送 `draftReceived`,避免竞态。
## 6. IPC 边界
新增独立 IPC 通道命名空间 `pet:*`,与既有 `ipcChannels` 并列定义在
`src/shared/ipc-channels.ts`:
```text
pet:get-snapshot (设置 + 当前状态镜面取值 + 可见性)
pet:update-settings (总开关、外观、贴边、置顶、安静模式)
pet:submit-draft (快速起草/拖放提交)
pet:dismiss-bubble (关闭完成/失败气泡)
pet:open-task (点击状态镜面/气泡,打开 Task Center 对应任务)
pet:toggle-visibility (托盘“隐藏/显示桌面宠物”)
```
- 所有输入使用 `src/shared/pet-contracts.ts`(新增)中的 Zod schema 校验,与既有约定
一致。
- `trusted-ipc-sender.ts` 的 `assertTrustedSender` 按窗口实例参数化使用;宠物窗口的
IPC handler 校验 `event.sender === petWindow.webContents`,不能用主窗口的 trusted
sender 校验函数验证宠物窗口的请求,也不能反过来。
- 宠物 preload 不暴露任何既有 `DesktopApi` 方法;需要的能力(提交草稿、读取/写入宠物
设置、订阅状态镜面)都通过 `pet:*` 通道单独暴露为 `window.goodbuddyPet` 的具体方法。
- Main 侧广播状态镜面变化时,只向宠物窗口发送派生后的枚举值(第 3 节的六种状态之一)
和最小必要的任务标题/ID,不发送完整 Task 或 Conversation 对象。
## 7. 持久化
新增设置文件,遵循既有 `settings-file-utils.ts` 的原子写入、版本校验和损坏隔离模式,
不复用 `ApplicationSettingsStore` 或 `ShortcutSettingsStore` 的 schema:
```text
pet-settings.json
{
version: 1,
enabled: boolean,
quietMode: boolean,
alwaysOnTop: boolean,
snapToEdge: boolean,
appearance: { character: string, size: 'small'|'medium'|'large',
opacity: number, motion: 'off'|'reduced'|'full' },
position: { displayId: number, x: number, y: number } | null
}
```
- `position` 为 `null` 表示尚未被用户拖动过,使用默认位置计算规则(第 8 节)。
- `displayId` 使用 Electron `screen.getAllDisplays()` 返回的 `id`;加载时按第 8 节校验,
不假设跨重启保持稳定的显示器 id 一定仍然存在。
- 独立 schema 版本号(从 1 开始),与主设置版本号互不影响;未来变更只需要迁移这一个
文件。
- 是否需要独立数据库表:不需要。宠物没有需要查询、筛选或跨设备同步的历史数据,一个
JSON 文件足够,符合仓库 KISS/YAGNI 约束。
## 8. 位置与多显示器校验
```ts
function resolvePetPosition(
saved: PetPosition | null,
displays: Display[]
): { x: number; y: number } {
if (saved) {
const display = displays.find((d) => d.id === saved.displayId)
if (display && pointWithinWorkArea(saved, display.workArea)) {
return saved
}
}
const primary = screen.getPrimaryDisplay()
return defaultPositionWithin(primary.workArea) // 例如右下角,留出安全边距
}
```
- 该函数在宠物窗口创建时以及 `display-added`/`display-removed`/`display-metrics-changed`
事件触发时调用;越界立即调用 `window.setPosition` 校正,不等待用户下一次拖动。
- 用户拖动结束(`moved` 事件)时把当前 `getBounds()` 和 `screen.getDisplayNearestPoint`
得到的 `displayId` 写入 `PetSettingsStore`,去抖后原子写入,避免拖动过程中高频写盘。
- 贴边吸附在渲染进程拖动过程中做视觉预判(不移动实际窗口,只显示吸附提示),在
`moved` 事件的最终位置上由 Main 一次性计算是否落入吸附阈值并调用 `setPosition`
吸附到边缘,阈值和默认位置的具体像素值作为实现细节调优,不在此固定。
## 9. 状态镜面查询
`PetStatusProjector` 不新增事件源,而是订阅既有 Task 变化通知(主窗口渲染进程已经
用于更新 Task Center 和会话内任务卡片的同一套 Main → Renderer 事件)在 Main 侧的
对应版本,或者在 Main 侧直接查询 `assistant-database.ts` 暴露的 Task 读取方法。具体
选择两者之一属于实现阶段的技术选择,约束条件是:
- 不引入宠物专属的任务状态表或缓存;查询/订阅的结果必须与 Task Center 在同一时刻
展示的状态一致。
- 查询开销必须是有界的(只统计活跃与最近终态 Task 的数量和状态,不拉取消息正文)。
- Runtime/网络不可达导致查询失败时返回 `unavailable`,不静默返回 `idle`。
“最近完成”“最近失败”窗口时长和“已查看”判定:复用 Task 已有的已读/查看时间戳字段
(若既有模型已提供);若当前模型没有这类字段,实现时以“该 Task 终态发生后,用户是否
已经点击过对应的宠物气泡或状态镜面,或已经在 Task Center 中打开过该 Task”作为已查看
判定,窗口时长建议与现有原生通知的展示逻辑保持同量级(分钟级),具体数值在实现时
根据既有 Task 事件粒度确定。
## 10. 提示与原生通知协商
`PetNotificationCoordinator` 与 `desktop-notification.ts` 的 `showDesktopNotificationWhenUnfocused`
共享同一个“主窗口是否可见并聚焦”判定:
```ts
function shouldSuppressBecauseMainWindowVisible(mainWindow: BrowserWindow): boolean {
return !mainWindow.isDestroyed() && mainWindow.isFocused()
}
```
- 现有 `showDesktopNotificationWhenUnfocused` 已经使用等价条件(`window.isFocused()`)
决定是否弹出原生通知;`PetNotificationCoordinator` 复用同一判定函数(如需要,提取为
共享工具),保证两者的“可见性”定义永远一致,不出现原生通知判定为可见但宠物判定为
不可见(或反之)导致同一事件同时出现两条提示或完全没有提示的情况。
- 实现层面:调用触发通知的代码路径在决定“原生通知 vs 宠物气泡”时二选一,而不是
两者分别独立判断后都可能触发;例如由现有触发点先判断安静模式和宠物启用状态,
为真则调用 `PetNotificationCoordinator`,否则调用现有 `showDesktopNotificationWhenUnfocused`。
- 安静模式开启时,两者都不触发;状态镜面仍照常更新(状态镜面更新走独立的
`pet:get-snapshot`/推送通道,不经过通知协商)。
## 11. 与既有模块集成
- 快速起草和拖放最终只调用主窗口渲染进程已有的“设置输入框文本”“追加附件”“激活
会话”“创建新会话”状态更新函数,不新建与 Ask/Execute、模型选择、Runtime 选择相关
的任何路径。
- 点击状态镜面/气泡打开 Task Center 复用现有 Task Center 导航(既有 IPC/路由,与托盘
“新建对话”发送 `conversation:new` 事件属于同一类模式:Main 发送一个事件,主窗口
渲染进程消费并完成导航)。
- 托盘菜单新增项复用现有 `Menu.buildFromTemplate` 结构(`src/main/index.ts` 的
`buildTray`),插入一个受当前可见性状态驱动的条目,不重写整个菜单构建函数。
- 全局快捷键(`src/shared/shortcut.ts`、`shortcut-settings-service.ts`)不受影响,
桌面宠物不注册新的全局快捷键,因为激活方式是点击窗口本身或系统级窗口切换,不需要
额外抢占全局按键空间。
## 12. 生命周期
- 应用启动读取 `PetSettingsStore`;`enabled: true` 时在既有服务和 IPC 注册完成后创建
宠物窗口(与主窗口创建顺序一致,不阻塞主窗口 `ready-to-show`)。
- 宠物窗口创建失败(例如极端情况下操作系统拒绝创建透明窗口):记录原因,不阻塞主窗口
启动,托盘“显示桌面宠物”下次点击时重试创建。
- 应用退出(`before-quit`/既有 `isQuitting` 标记)时,宠物窗口与主窗口一并销毁,不需要
独立的关闭确认或延迟清理,因为宠物窗口没有正在进行的网络连接或未保存状态:草稿已经
转交主窗口的那一刻起就属于主窗口生命周期。
- “暂时隐藏”只调用 `window.hide()`,不销毁窗口实例,重新显示直接 `showWindow` 复用
既有实例,避免频繁创建/销毁透明窗口带来的闪烁。
## 13. 打包
- 宠物渲染入口(HTML、内置角色素材)加入 `electron.vite.config.ts` 的 renderer 构建
输入(现有配置已有 `input` 多入口结构,参考现有主渲染入口的声明方式新增一项)。
- 内置角色素材(数量有限的静态图片/精灵图)作为打包资源加入 `package.json` 的 `files`/
`extraResources`,不通过运行时网络下载,符合仓库“不新增远程资源请求”的字体/资源
约束的同一精神。
- 新增 preload 产物(`pet.cjs`)需要加入 `electron.vite.config.ts` 的 main/preload 构建
`entryFileNames` 规则,与现有主 preload 产物并列。
- 六个标准打包目标(Windows/macOS/Linux × x64/arm64)均需验证宠物窗口可创建、透明或
回退不透明背景符合第 3 节预期;LoongArch 预览目标不单独验证,遵循仓库现有策略。
## 14. 验证策略
### 14.1 自动化
- `PetSettingsStore` 的默认值、原子写入、损坏隔离和版本校验(沿用
`settings-file-utils.ts` 现有测试模式)。
- `resolvePetPosition` 在显示器存在/不存在/坐标越界三种输入下的返回值。
- `PetStatusProjector` 按第 3 节优先级表在各种 Task 组合输入下的输出。
- `PetNotificationCoordinator` 与 `desktop-notification.ts` 共享判定函数在“可见并聚焦”
“不可见”“安静模式开启”组合下只触发一条路径或都不触发。
- `ContextManager.stageFromPaths`(新增方法)对合法文件、超限文件、不支持类型、文件夹
路径的分支覆盖,复用现有 `context-manager` 测试基础设施。
- `pet:*` IPC handler 的 trusted sender、schema 校验和错误码。
- 快速起草提交后主窗口渲染进程状态更新的单元测试:目标会话激活、文本写入、附件追加、
草稿被丢弃时不产生 Task 或消息记录。
### 14.2 真实环境
- Windows、macOS、Linux 至少各一次验证宠物窗口创建、拖动、贴边吸附、透明或回退背景的
实际渲染效果。
- 真实拖放一个合法文件、一个超限文件、一个文件夹到宠物窗口,确认 UI 设计第 4.3 节的
反馈和最终主窗口状态符合预期。
- 真实触发一次任务完成和一次任务失败,在“主窗口隐藏”“主窗口可见”“安静模式开启”
三种组合下确认气泡和原生通知的出现符合第 10 节的互斥规则。
- 多显示器插拔场景下确认位置回退逻辑(断开记忆显示器后宠物仍可见)。
- 打包后的应用在六个标准目标上完成启用、拖动、快速起草、退出流程的一次冒烟验证。
设计文档完成不代表功能实施或上述验证通过。
+207
View File
@@ -0,0 +1,207 @@
# 桌面宠物 UI 设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
本文定义设置中的“桌面宠物”页面和独立宠物窗口的界面。所有颜色、间距、字体、通知、
Switch/Checkbox、焦点和响应式规则以全局设计系统为准;宠物窗口是应用内唯一不使用标准
`PageShell` 的持久界面,其例外范围在第 4 节明确限定。
## 1. 设置页面信息架构
设置中心新增“桌面宠物”一级分类,使用 `standard` PageShell,不使用页签,按以下顺序
纵向排列:
```text
桌面宠物
在桌面上保留一个轻量入口,随时起草消息、拖入文件或查看任务状态
启用桌面宠物 [Switch 关]
关闭后不在桌面上显示任何宠物窗口或提示
—— 以下设置仅在启用后可编辑,保持可见但降低强调度 ——
外观
角色 [分段选择:三到四个内置角色缩略图]
尺寸 [滑块 小 · 中 · 大]
透明度 [滑块]
动画强度 [滑块 关闭 · 轻量 · 完整]
[恢复默认外观]
位置与窗口
始终置顶 [Switch 开]
贴边吸附 [Switch 开]
提示
安静模式(不显示完成 / 失败气泡) [Switch 关]
```
- 总开关是本页唯一决定宠物窗口存在与否的控件,符合 `INV-3`;其余控件在总开关关闭时
保持可见但不可交互,说明文字保持“关闭后不在桌面上显示任何宠物窗口或提示”。
- 二元持久状态一律使用共享 Switch;角色选择使用共享 `SegmentedControl`(3-4 个内置
角色,符合数量上限);尺寸、透明度和动画强度使用标准滑块控件并显示当前取值。
- “恢复默认外观”只重置本区域三项外观设置,不改变总开关、位置或安静模式,与
[功能逻辑](./logic-design.md) 中“恢复默认外观”的范围一致。
- 页面不提供“立即显示 / 隐藏”按钮:暂时隐藏的入口固定放在托盘菜单,避免在设置页和
托盘中出现两个含义相近但状态不同步的控件。
## 2. 首次启用与关闭反馈
- 开启总开关后宠物窗口在默认位置出现,不产生任何应用通知或原生通知,因为这是用户刚
完成的显式操作,结果已经可见。
- 关闭总开关后宠物窗口和任何气泡立即消失;设置页本身不产生额外通知。
- 外观、位置相关 Switch 和滑块的变更立即生效并持久化,失败时通过应用通知报告,不静默
丢弃用户输入。
## 3. 托盘入口
- 托盘菜单在“新建对话”与“设置”之间新增一项,文案随当前可见性变化:
- 已启用且可见:“隐藏桌面宠物”。
- 已启用但隐藏:“显示桌面宠物”。
- 未启用:“显示桌面宠物”,点击后等同于在设置中开启总开关并显示窗口。
- 该菜单项不复制设置页的开关语义,只切换可见性;未启用状态下点击才会连带开启总开关,
行为需要在可访问名称或菜单说明中明确,避免用户误以为它只是显示/隐藏。
## 4. 宠物窗口
宠物窗口是无边框、透明背景、不可缩放的持久悬浮窗口,不使用 `PageShell`、不属于标准
一级页面,因此以下规则专属于该窗口:
```text
┌───────────┐
│ 角色 │ ← 状态镜面:角色外观随任务状态切换
│ (idle) │
└───────────┘
```
- 窗口本身没有标题栏、边框或系统窗口控件;关闭宠物只能通过设置总开关、托盘“隐藏桌面
宠物”或退出应用完成,窗口自身不提供关闭按钮,避免与“暂时隐藏”“关闭功能”两种含义
混淆。
- 窗口整体可拖动;拖动过程中显示当前吸附意图(开启贴边时,接近边缘会有轻微吸附反馈)。
- 窗口右键菜单提供“快速起草”“暂时隐藏”“打开设置”“退出 GoodBuddy”,与托盘菜单保持
同一套文案。
### 4.1 状态镜面外观
宠物角色在以下六种取值间切换,取值定义见[功能逻辑](./logic-design.md) 第 3 节:
| 状态 | 视觉表达 | 可访问名称 |
| --- | --- | --- |
| `unavailable` | 角色显示为暗淡的问号叠加 | “任务状态当前不可用” |
| `idle` | 角色静止或轻微待机动作 | “没有进行中的任务” |
| `running` | 角色显示持续的工作动作 | “有任务正在执行” |
| `waiting_approval` | 角色举牌或举手动作,带醒目但非警示色的强调 | “有任务等待你的确认” |
| `completed_recent` | 角色短暂展示完成动作后回到待机 | “有任务刚刚完成” |
| `failed_recent` | 角色显示明显区别于完成的姿态,并叠加警示色边缘 | “有任务失败,需要查看” |
- 状态表达必须同时使用动作/姿态和可访问名称两种信号,不能只依赖颜色区分
`completed_recent` 与 `failed_recent`。
- `prefers-reduced-motion` 或用户将动画强度设为“关闭”时,状态切换只改变静态姿态和颜色
强调,不播放位移或缩放动画。
- 点击状态镜面区域(角色本体)触发 US-D4:显示并聚焦主窗口,Task Center 定位到触发
当前状态的任务;`idle` 和 `unavailable` 状态下点击等同于显示并聚焦主窗口,不做定位。
### 4.2 快速起草弹出框
点击宠物打开的弹出框锚定在宠物窗口旁(自动避开屏幕边缘),结构如下:
```text
发送到:[当前会话名称 v]
[多行文本输入,占位文案“输入消息,或拖入文件…”]
[📎 附加文件] [已附加:contract.docx ×]
[取消] [提交]
```
- “发送到”是一个富信息单选菜单,选项为当前活跃会话与“新建对话”,遵循全局设计系统
第 6.11 节的单选选择器规则;默认选中当前活跃会话。
- 文本输入支持多行、`Ctrl+V` 粘贴图片和文字,与主窗口聊天输入框的粘贴行为一致。
- 附加文件按钮打开原生文件选择;已附加文件以行内标签形式展示,可单独移除。
- 拒绝的文件(超限或不支持)在附件区下方以就地错误文字展示,不使用应用级通知,因为
这是与当前操作直接绑定的字段级反馈。
- “提交”把文本和附件转交目标会话的输入区草稿,随后关闭弹出框;提交过程中禁用重复
点击并显示进行中状态,成功后不额外弹出通知(主窗口的显现和草稿本身就是反馈)。
- “取消”或点击弹出框外部或按 Escape 关闭弹出框并丢弃草稿,焦点返回宠物窗口。
- 弹出框打开时焦点进入文本输入;Tab 顺序为文本输入 → 附加文件 → 已附加文件的移除
按钮 → 取消 → 提交;关闭后焦点回到宠物窗口本体。
### 4.3 拖放反馈
- 文件或文字拖入宠物窗口范围时,窗口整体出现可放置的高亮边框和轻微放大反馈。
- 释放后立即按第 4.2 节“提交”的相同路径处理,不额外弹出确认框;这保持“拖动=起草并
发送到会话输入区(而非立即发送消息)”的一致语义。
- 拖入文件夹或全部文件超限时,宠物短暂显示拒绝反馈(边框变为警示色并伴随简短文字
提示,例如“不支持文件夹”),不打开主窗口、不产生暂存。
- 部分文件超限、部分合法时,合法文件正常处理,超限文件的名称通过一条会在数秒后自动
消失的就地提示列出,不使用应用级通知(拖放是即时操作,反馈应贴近操作发生的位置)。
### 4.4 完成 / 失败气泡
```text
┌─────────────────────────────┐
│ ✓ 已完成 │
│ 合同风险检查 │
│ [×] │
└─────────────────────────────┘
▲
(角色)
```
- 气泡锚定在宠物窗口旁,方向随宠物在屏幕上的位置自动避开边缘。
- 完成气泡在约 4.5 秒后自动消失,与全局应用通知的成功/信息时长一致;失败气泡保持可见
直到用户点击查看或点击关闭按钮。
- 气泡本体可点击,等同于点击状态镜面:显示并聚焦主窗口并定位到对应任务。
- 关闭按钮 `[×]` 有独立可访问名称(“关闭提示”),不与气泡主体的点击目标冲突:点击
关闭按钮只关闭气泡,不导航到主窗口。
- 气泡使用 `status`/polite 实时区域播报完成,失败气泡使用 `alert`/assertive,与全局
设计系统的通知语义保持一致,即使气泡本身不是应用通知视口的一部分。
## 5. 与应用通知视口的关系
- 宠物气泡是应用通知视口的窗口外等价物,只在主窗口不可见/未聚焦且安静模式关闭时出现;
同一事件不会同时进入应用通知视口和宠物气泡,具体归并规则见
[功能逻辑](./logic-design.md) 第 4 节。
- 快速起草和拖放产生的字段级错误(附件超限、不支持类型)始终就地展示在宠物窗口或弹出
框内,不进入应用通知视口,保持“就地错误”与“全局通知”分工一致。
## 6. 无障碍
- 状态镜面、气泡和弹出框内的所有交互元素都有持久可访问名称,不依赖占位文案代替标签。
- 宠物窗口可通过系统级窗口切换获得焦点;获得焦点后:
- 预期的激活键(例如 Enter 或 Space)打开快速起草弹出框。
- 弹出框内 Tab 顺序遵循第 4.2 节。
- Escape 关闭当前打开的弹出框或气泡,焦点回到宠物窗口本体。
- 状态镜面的状态变化通过 `status`/polite 实时区域播报简短说明,避免因动画本身无法被
屏幕阅读器感知而丢失信息;不对每次动画帧重复播报。
- 焦点环、危险色使用和触控目标尺寸遵循全局设计系统第 11 节的通用要求。
## 7. 响应式与多显示器
- 宠物窗口尺寸不随主窗口大小变化;用户通过设置中的“尺寸”滑块单独控制。
- 宠物窗口位置按显示器分别记忆;显示器不可用时的回退规则见
[功能逻辑](./logic-design.md) 第 7 节,界面表现为宠物出现在主显示器的默认可见位置,
不出现在屏幕之外或不可见状态。
- 弹出框和气泡的锚定方向根据宠物当前位置与屏幕边缘的距离自动选择展开方向(上/下/左/
右),确保始终完整可见,不被裁切到屏幕外。
## 8. 深浅主题
- 弹出框、气泡和设置页控件复用全局语义令牌,不新增页面专属颜色。
- 宠物角色素材本身分别提供适配浅色和深色桌面背景的版本,避免透明背景下角色轮廓在深色
桌面壁纸上难以辨认;具体资源交付方式属于技术设计范畴。
## 9. 固定词汇
使用“桌面宠物”“启用桌面宠物”“暂时隐藏”“快速起草”“安静模式”“状态镜面”,定义见
[README](./README.md) 术语表。不使用“挂件”“桌宠助手”或其他未在术语表中定义的别名,
保持中文文案统一。
+255
View File
@@ -0,0 +1,255 @@
# 桌面宠物 User Stories
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [桌面宠物 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
## 角色
- **日常用户**:在多任务、多显示器场景下使用 GoodBuddy 处理日常工作。
- **首次体验用户**:刚升级或安装 GoodBuddy,尚未接触过桌面宠物功能。
- **键盘用户**:主要通过键盘操作,可能同时使用屏幕阅读器。
- **多显示器用户**:使用两个或更多显示器,可能随时插拔外接显示器。
## US-A 首次启用
### US-A1 默认不出现
**Given** 用户全新安装或刚升级 GoodBuddy
**When** 用户尚未进入“桌面宠物”设置
**Then** 桌面上不出现任何宠物窗口,托盘菜单显示“显示桌面宠物”而非“隐藏”
### US-A2 首次启用
**Given** 用户在设置中打开“启用桌面宠物”总开关
**When** 开关切换为开启
**Then** 宠物窗口在默认位置出现,使用默认外观,且不产生任何应用级或原生通知
### US-A3 关闭后完全消失
**Given** 桌面宠物已启用并可见
**When** 用户关闭“启用桌面宠物”总开关
**Then** 宠物窗口立即消失,未完成的气泡提示一并消失,托盘菜单条目恢复为“显示桌面宠物”,
且用户之前设置的位置、外观和提示偏好被保留供下次开启时使用
## US-B 快速起草
### US-B1 起草文字并转入主窗口
**Given** 桌面宠物已启用,主窗口当前隐藏
**When** 用户点击宠物并输入一段文字后点击提交
**Then** 主窗口被显示并聚焦,目标会话的输入区出现刚输入的文字作为草稿,输入焦点落在
该输入区,且没有任何消息被发送、没有任何任务被创建
### US-B2 起草时附加文件
**Given** 快速起草弹出框已打开
**When** 用户点击“附加文件”并选择一个受支持类型和大小内的文件
**Then** 该文件在弹出框内显示为待提交附件;提交后该附件出现在主窗口对应会话输入区的
待发送附件列表中
### US-B3 附加不受支持的文件
**Given** 快速起草弹出框已打开
**When** 用户选择一个超出大小限制或类型不受支持的文件
**Then** 弹出框就地显示明确的拒绝原因,不添加该文件,且不影响已经添加的其他文字或附件
### US-B4 取消起草
**Given** 用户已在快速起草弹出框中输入文字或添加附件
**When** 用户点击取消或按 Escape 关闭弹出框
**Then** 弹出框关闭且草稿被丢弃,主窗口任何会话的现有草稿不受影响,且不创建任何草稿或
任务记录
### US-B5 选择目标会话
**Given** 主窗口当前存在多个会话,且用户在快速起草弹出框中看到默认目标会话
**When** 用户在弹出框内切换目标为“新建对话”
**Then** 提交后主窗口创建并激活一个新会话,草稿内容进入该新会话的输入区,不影响原有
会话的草稿
## US-C 拖放暂存
### US-C1 拖放单个文件
**Given** 桌面宠物已启用并可见
**When** 用户从文件管理器拖动一个受支持类型的文件到宠物窗口上并释放
**Then** 宠物在拖入过程中显示可放置的视觉反馈,释放后主窗口被显示并聚焦,该文件出现在
目标会话输入区的待发送附件列表中
### US-C2 拖放多个文件,部分超限
**Given** 用户同时拖动数量或大小超出限制的多个文件到宠物窗口
**When** 用户释放拖放
**Then** 合法文件正常进入目标会话的待发送附件列表,超限或不支持的文件被单独列出拒绝
原因,不阻止合法文件被暂存
### US-C3 拖放文件夹
**Given** 用户拖动一个文件夹到宠物窗口
**When** 用户释放拖放
**Then** 宠物提示不支持文件夹,不递归展开文件夹内容,也不产生任何暂存
### US-C4 拖放选中文字
**Given** 用户在其他应用中选中一段文字并拖动到宠物窗口
**When** 用户释放拖放
**Then** 该文字进入目标会话输入区的草稿文本,行为与快速起草输入文字一致
## US-D 状态镜面
### US-D1 查看空闲状态
**Given** 当前没有任何活跃 Task
**When** 用户查看宠物外观
**Then** 宠物显示空闲状态,且可访问名称明确说明当前没有进行中的任务
### US-D2 查看执行中状态
**Given** 有一个 Task 处于执行中
**When** 用户查看宠物外观
**Then** 宠物切换为执行中状态,且该状态与 Task Center 中该任务的状态一致
### US-D3 多任务状态优先级
**Given** 同时存在一个等待处理的任务和一个执行中的任务
**When** 用户查看宠物外观
**Then** 宠物按照“等待处理优先于执行中”的规则显示等待处理状态
### US-D4 点击状态镜面打开任务详情
**Given** 宠物当前显示执行中或失败状态
**When** 用户点击宠物的状态镜面区域
**Then** 主窗口被显示并聚焦,Task Center 定位到触发当前状态的具体任务
### US-D5 状态不可用
**Given** 用于确认任务状态的 Runtime 或网络当前不可用
**When** 用户查看宠物外观
**Then** 宠物显示“状态不可用”,不回退显示为空闲或执行中
## US-E 完成与失败提示
### US-E1 主窗口不可见时收到完成提示
**Given** 主窗口当前隐藏或未聚焦,安静模式未开启
**When** 一个任务从执行中变为已完成
**Then** 宠物旁出现说明任务标题和结果的气泡,且该事件不重复触发原生系统通知
### US-E2 失败提示保持可见
**Given** 主窗口当前隐藏,安静模式未开启
**When** 一个任务失败
**Then** 宠物旁出现失败气泡并保持可见,直到用户点击查看或手动关闭,不自动消失
### US-E3 主窗口可见时不重复提示
**Given** 主窗口当前可见并聚焦
**When** 一个任务完成或失败
**Then** 不出现宠物气泡;反馈通过主窗口现有机制呈现,避免同一事件重复出现在两个位置
### US-E4 开启安静模式
**Given** 用户在设置中开启“安静模式”
**When** 之后任意任务完成或失败,且主窗口不可见
**Then** 不出现任何宠物气泡,但状态镜面外观仍随任务状态正常变化
## US-F 位置、外观与多显示器
### US-F1 记忆位置
**Given** 用户把宠物拖动到某个显示器的特定位置
**When** 用户重启 GoodBuddy
**Then** 宠物出现在同一显示器的同一位置
### US-F2 显示器断开后回退
**Given** 宠物位置记忆在一个已插入的外接显示器上
**When** 该外接显示器被移除后重新启动 GoodBuddy
**Then** 宠物出现在当前可用的主显示器上一个可见、不越界的位置,而不是不可见或位于屏幕
之外
### US-F3 贴边吸附
**Given** 用户开启“贴边”选项
**When** 用户把宠物拖动到接近屏幕边缘的位置并释放
**Then** 宠物自动吸附到屏幕边缘,且吸附后仍可被再次拖动离开
### US-F4 调整外观
**Given** 用户在设置中调整宠物尺寸、透明度或动画强度
**When** 用户改变任意一项取值
**Then** 宠物外观立即按新取值更新,且设置持久化,重启后保持
### US-F5 恢复默认外观
**Given** 用户已经修改过角色、尺寸、透明度或动画强度
**When** 用户点击“恢复默认外观”
**Then** 上述设置全部恢复为默认值,宠物位置和总开关状态不受影响
## US-G 暂时隐藏与托盘
### US-G1 暂时隐藏
**Given** 桌面宠物已启用并可见
**When** 用户从托盘菜单选择“隐藏桌面宠物”
**Then** 宠物窗口消失,总开关状态和其他设置不变,托盘菜单条目变为“显示桌面宠物”
### US-G2 重新显示
**Given** 桌面宠物已启用但当前隐藏
**When** 用户从托盘菜单选择“显示桌面宠物”
**Then** 宠物窗口在此前记忆的位置重新出现
### US-G3 应用退出
**Given** 桌面宠物已启用并可见
**When** 用户退出 GoodBuddy
**Then** 宠物窗口一并关闭,不留下任何独立运行的进程或残留窗口
## US-H 键盘与无障碍
### US-H1 键盘打开快速起草
**Given** 桌面宠物窗口通过系统级窗口切换取得焦点
**When** 键盘用户按下预期的激活键
**Then** 快速起草弹出框打开,焦点进入输入框
### US-H2 键盘关闭弹出框
**Given** 快速起草弹出框已打开且有键盘焦点
**When** 用户按下 Escape
**Then** 弹出框关闭,焦点回到宠物窗口本身
### US-H3 屏幕阅读器状态说明
**Given** 用户使用屏幕阅读器
**When** 焦点移动到宠物的状态镜面
**Then** 屏幕阅读器播报当前状态的文字说明,不仅依赖颜色或动画表达状态
## US-I 兼容与边界
### US-I1 升级用户保持现状
**Given** 已在使用 GoodBuddy 且从未接触过桌面宠物功能的用户升级到包含该功能的版本
**When** 用户正常启动应用
**Then** 桌面宠物保持关闭,界面和行为与升级前一致
### US-I2 不支持透明合成的环境
**Given** 用户的操作系统或桌面环境不支持窗口透明合成
**When** 用户启用桌面宠物
**Then** 宠物窗口退化为不透明的小型窗口而不是渲染异常或无法显示,具体规则见
[技术设计](./technical-design.md)
### US-I3 拖放到非目标区域
**Given** 用户尝试把文件拖到宠物窗口之外的桌面区域
**When** 用户释放拖放
**Then** GoodBuddy 不接收该拖放,不产生任何暂存或错误提示