# GXDE 小埃同学 · 设计方案 > 项目:GXDE AI See(小埃同学) > 版本:v0.2(方案评审稿) > 日期:2026-10-30 > 仓库: > 状态:**方案评审中,确认后开始编码** --- ## 1. 一句话 **拖一下,世界就到你手边。** 小埃同学是 GXDE 桌面的常驻 AI 助手。它把「拖拽」变成一个贯穿桌面的动作: 拖到**传送门**,选一个窗口,内容就落进那个窗口的输入框; 拖到**内容工厂**,按文件类型分词、提链接、翻译、OCR、加待办、问小埃; 拖进**暂存区**,东西就先放着,随时再拖出去。 --- ## 2. 产品组成 一个常驻进程(`gxde-ai-see`),四个模块 + 一个聊天客户端: | 模块 | 说明 | 入口 | | --- | --- | --- | | **传送门 Portal** | 拖拽暂存面板 + 原生 drop 到目标窗口 | 拖拽开始时自动出现 | | **内容工厂 Factory** | 按内容类型提供动作 | 拖动时出现在面板下方 | | **待办 Todo** | 可选置顶、可拖动、不进任务栏的小窗口 | 面板菜单 / 内容工厂 | | **小埃同学 Chat** | DTK2 AI 聊天(DeepSeek / 自定义 API / MCP) | 独立 desktop 入口、传送门里的「问问小埃」 | 三者共享一套存储与 UI 组件;聊天是独立窗口,传送门是常驻面板。 ``` ┌─────────────────────────────┐ 拖拽开始 ──────▶ │ 传送门面板(右下角,常驻) │ │ ┌─────────┐ ┌──────────┐ │ │ │ 传送门 │ │ 暂存区 │ │ │ │ │ │ 列表 │ │ │ └─────────┘ └──────────┘ │ │ ┌─────────────────────────┐│ │ │ 🔧 内容工厂(按类型出现) ││ │ └─────────────────────────┘│ └──────────────┬──────────────┘ │ ┌──────────────┴──────────────┐ │ 合成器 portal(爆炸平铺) │ │ 原生 drop 到目标窗口输入框 │ └─────────────────────────────┘ ``` --- ## 3. 设计原则 1. **原生优先**:能用 Wayland/X11 原生协议完成的,不自己模拟。 传送门的落点用**原生 DnD**,不是剪贴板 + Ctrl+V。 2. **不抢焦点、不打扰**:面板不进入 dock,不抢键盘焦点,不打断用户正在做的事。 3. **不监听剪贴板**:暂存区不是剪贴板管理器,只收用户主动拖入的内容。 4. **可降级**:没有 GXDE portal 合成器时功能降级但程序可用,不崩溃、不反复重启。 5. **数据可恢复**:暂存区、待办、对话都持久化;文件消失自动清理。 --- ## 4. 传送门(核心) ### 4.1 用户流程 ``` 用户拖起文件/文字/图片 └─▶ 面板在右下角淡入(暂存区为空时才需要弹;非空时本来就常显) ├─▶ 松手在「暂存区」:加入暂存区列表 ├─▶ 松手在「内容工厂」:展开动作,松手在某个动作上执行 └─▶ 悬停「传送门」约 150ms:进入爆炸平铺(portal 模式) └─▶ 鼠标移到某个窗口缩略图上 └─▶ 松手:内容**原生 drop** 进该窗口的输入框 / 拖放区 └─▶ 关闭平铺、恢复窗口状态、激活目标窗口 ``` ### 4.2 为什么不用剪贴板 + Ctrl+V 剪贴板只能粘贴到**当前焦点**控件,无法命中「鼠标下的那个输入框」; 而且会污染用户剪贴板、需要还原、需要处理图片/文件多 MIME。 原生 drop 由目标 App 自己决定在哪个控件、以什么形式接收——这正是用户 说的「把文件 drop 到输入框本身就是粘贴的结果」。 ### 4.3 原生 drop 的实现(关键设计) Wayland 的拖拽目标由合成器内部的 `wlr_drag->focus` 决定。目标 App 只有在 收到 `wl_data_device.enter` 后才会 `accept`,松手时才收到 `drop`。 我们**不修改 wlroots**,只在合成器里做三件事: 1. **portal 模式下的 present_windows**:缩略图照常飞入,但 - 不安装 wlroots 的 pointer/keyboard/touch grab(否则会顶掉拖拽自己的 grab); - 不锁 cursor,保留拖拽图标; - 每帧按 cursor 更新 hover 高亮(复用现有的 105% 放大 + 亮度插值)。 2. **一个 wlcom 层的 `seat_pointer_grab`**:`cursor_feed_motion()` 会先询问它, 返回 true 就吞掉事件。在这个 grab 里: - 根据 cursor 位置找到 hover 的缩略图; - 把光标坐标从缩略图矩形线性映射回窗口本地坐标: `sx = (px - thumb.x) * view.w / thumb.w`,`sy` 同理; - 目标变了就调 `wlr_seat_pointer_notify_enter(seat, view->surface, sx, sy)`, 否则调 `wlr_seat_pointer_notify_motion(seat, time, sx, sy)`; - 这样目标 App 会像普通 DnD 一样收到 `enter/motion`,并在光标位置判断 「这里是不是一个可接收的输入框」,回 `accept`。 - 松手时调 `wlr_seat_pointer_notify_button(..., RELEASED)` 后返回 true: `wlr_drag` 的 `drag_handle_pointer_button` 看到 `focus_client && accepted`, 就向真实窗口发 `drop` —— **原生 drop 完成**。 - 同时调用 `selection_handle_cursor_move()` 让拖拽图标继续跟随。 3. **状态收尾**: - 关闭 present_windows(反向动画); - 恢复窗口状态(portal 不移动真实窗口,只隐藏缩略图,所以恢复很轻); - 激活目标窗口,让用户看到粘贴结果; - `cursor_rebase()` 恢复指针焦点。 关键前提(已核对代码): - present_windows 用 `ky_scene_node_set_enabled(false)` 隐藏真实 view 的 scene node, **surface 仍然是 mapped 的**,client 并不知道自己不可见,收 drop 完全正常。 - `drag_set_focus()` 会为目标 client 创建 offer 并发送 `enter`; client 的 `accept` 在 hover 期间就完成,松手时不存在时序竞争。 - `drag_handle_pointer_button` 在 `focus_client && accepted` 时才会真正 drop, 否则 drag 自然取消——目标不接受时不会误操作。 ### 4.4 合成器改动(flake-wm + gxde-wlcom) 两个仓库同源(flake-wm 用 CMake,gxde-wlcom 用 meson),代码相同、构建文件不同。 | 文件 | 改动 | | --- | --- | | `src/vendor/dkwin/presentwindows/wlcom_presentwindows.c` | 增加 portal 模式:不装 grab、不锁 cursor、每帧高亮;导出 `present_windows_begin_portal()` / `end_portal()` / `portal_item_rect()` / `portal_hovered_uuid()` | | `src/vendor/gxde/portal/portal.c` | 新模块:D-Bus、状态机、grab、坐标映射、收尾 | | `src/vendor/gxde/portal/portal_dbus.c` | `org.gxde.Portal` 接口 | | `src/input/selection.c` | 暴露当前 drag 指针(或 portal 模块直接监听 seat 的 start_drag/destroy) | | `src/view/view_p.h` | 导出 portal 需要的 present_windows 函数 | | `CMakeLists.txt` / `meson.build` | 加 `src/vendor/gxde/portal` 目录 | D-Bus 接口草案(会话总线,合成器持有): ``` interface org.gxde.Portal BeginPortal() -> b # 打开 portal;没有可平铺窗口返回 false CancelPortal() ListWindows() -> a(ssiiii) # uuid, title, app_id, x, y, w, h(调试 / 降级用) signal DragStarted(ay mimes, s source_app_id) signal DragEnded() signal PortalDropped(s uuid, b accepted) ``` ### 4.5 边界与异常 | 情况 | 行为 | | --- | --- | | 没有已打开窗口 | `BeginPortal()` 返回 false,面板提示「当前无已打开的窗口」,拖拽不中断 | | 目标窗口不接受 drop | `accept` 为 false,drag 自然取消,面板提示「该窗口不接受拖放」 | | 拖到缩略图之间的空隙 | 无目标,drag 取消 | | 拖回面板/暂存区 | 按正常 drop 处理(暂存区接住) | | 按 Esc | drag 取消,portal 关闭 | | 目标窗口是拖拽源自己 | 允许(等同拖回原窗口) | | 最小化/全屏窗口 | v1 只平铺当前工作区可见窗口;全屏窗口先退出全屏再平铺,结束后恢复 | | 多显示器 | v1 在面板所在输出上平铺;后续支持跨屏 | | 触摸拖拽 | v1 只做鼠标,触摸后续补 | ### 4.6 降级:X11 / 非 GXDE 合成器 面板在传送门旁边扩展一块**窗口图标网格**(按空间自动选左/右/上/下): - 图标来源:Wayland 走 `org.gxde.Portal.ListWindows`,X11 走 EWMH `_NET_CLIENT_LIST`。 - hover 图标 → 最小化其他窗口,激活/提升该窗口;用户把内容拖到已显示的窗口上松开 → X11 的 XDND 是真实 drop,直接生效。 - 拖出传送门区域 → 恢复原窗口状态。 - 完全不支持 portal 的 Wayland 合成器:退化为「复制到剪贴板 + 提示」。 - Windows 后续复用同一套图标网格 + `SetForegroundWindow`。 ### 4.7 数据流(谁拿到 drop 数据) 传输全程由源 App 和目标 App 通过 Wayland DnD 完成,**小埃同学不碰数据**: - 从别的 App 拖入:源 App → 目标 App 直连;暂存区只保存 URI/文本/图片副本。 - 从暂存区拖出:小埃同学作为 source 提供 `text/uri-list` / `text/plain` / `image/png`; 目标 App 收到 drop。 因此传送门不需要读取别人的 data source,也不需要剪贴板,隐私面最小。 --- ## 5. 暂存区 ### 5.1 条目 | 类型 | 存储 | 说明 | | --- | --- | --- | | 文件 | URI | 保留原路径,可拖出到文件管理器/编辑器 | | 文字 | 原文 | 展示做截断,完整保留 | | 图片 | 缓存 PNG | 落盘到 `~/.cache/gxde-ai-see/images/`,不依赖源文件 | ### 5.2 行为 - 只有**主动松手在暂存区**才添加;路过不算。不监听剪贴板。 - 去重:同类型同内容(路径 / 文本 hash / 图片 hash)只留一条,重复拖入置顶并高亮。 - 上限:默认 200 条,超出丢最旧,可配置。 - 文件消失:`QFileSystemWatcher` + 2s 轮询双保险,消失则播放删除动画并移除。 - 持久化:`~/.local/share/gxde-ai-see/staging.json`,原子写(QSaveFile)。 - 暂存区非空 → 面板常显;为空 → 只在拖拽时出现。 - 从暂存区跃迁后条目保留(除非用户删除或文件消失)。 ### 5.3 列表交互 - 每行:复选框、缩略图/类型图标、名称、关闭按钮。 - 选中任意复选框 → 列表上方出现「全选 / 反选 / 删除选中」;取消全不选后收起。 - 拖动行排序(带动画),拖出到别的 App(`QDrag`),拖到传送门(原生 drop)。 --- ## 6. 面板 UI 与动画 ### 6.1 窗口形态 - Wayland:`layer-shell` Overlay 层,锚定右下,`keyboard_interactivity = none`, 不抢焦点、不进 dock、可精确定位。 - X11:无边框工具窗口 + `_NET_WM_STATE_SKIP_TASKBAR` + 右下定位。 - 尺寸:宽 500;高度随内容,列表上限 `屏高 / 3`,超出滚动;顶部手柄可拖高; 可「固定大小」。 - 位置:默认右下(输入法面板附近),可拖边框移动,可恢复默认,可固定。 ### 6.2 弹窗时机与「打扰」控制 拖拽在很多场景都会发生,无脑弹窗会很烦。控制策略: - **MIME 过滤**:只对 `text/uri-list`、`text/plain`、`image/*` 响应; 拖窗口、拖滚动条、拖 Dock 图标这类没有这些 MIME 的不会触发。 - **120ms 延迟**:拖拽开始后延迟再显示,快速点击式拖拽不闪面板。 - **忽略自身拖拽**:从暂存区拖出时不重复弹面板。 - **不抢焦点**:layer-shell + `keyboard none`,不会打断正在编辑的窗口。 - **不遮挡**:面板固定在右下角,尺寸克制;可配置「仅暂存区非空时常显」。 - **可关闭自动弹出**:设置里可关闭「拖拽时自动出现」,只保留常驻面板。 - **悬停延迟**:传送门需要悬停约 150ms 才展开,路过不会炸开全屏。 - **多显示器**:只在光标所在输出弹出,不在所有屏幕同时出现。 ### 6.3 动画 | 场景 | 参考 | 效果 | | --- | --- | --- | | 面板出现/消失 | qimpanel | 上移 12px + 淡入,160ms OutCubic | | 行 hover | desktop-panel hover box | 背景淡入 + 图标 1.04 + 关闭按钮淡入,120ms | | 删除 | control-center `NotifyModel::showRemoveAnim` | 右移淡出 160ms,再折叠 + 后续行上移 180ms | | 拖动排序 | desktop-panel dodge | 其余行 150ms 平移到新槽位,InOutCubic | | 内容工厂展开 | — | 高度展开 + 图标错峰淡入 180ms | | 传送门悬停 | — | 呼吸光晕 + 边框高亮 | | 爆炸平铺 | present_windows | OutQuint 300ms,缩略图从真实位置飞入 | | 复选框 | — | 勾选描边 120ms | ### 6.4 右键菜单 固定位置 / 恢复默认位置 / 固定大小 / 恢复默认大小 / 置顶 / 全屏时隐藏 / 拖拽时自动出现 / 开机自启与保活 / 清空暂存区 / 设置 / 关于。 > 「置顶」的现实取舍见 §13 待确认 1。 --- ## 7. 内容工厂 拖动时若类型受支持,面板下方出现「🔧 内容工厂」框;把内容拖到框上,框朝**有空间的 方向**(左/右/上/下)展开动作面板;每个动作 hover 有缩放/光晕动画;松手在哪项执行哪项。 ### 7.1 动作矩阵 | 类型 | 动作 | | --- | --- | | 文本 / 文本类 Office | 打开、**分词**、**提取链接**、翻译成…、朗读、添加到待办、问问小埃、复制 | | 图片 | 打开、去除背景(占位)、OCR、添加到待办、问问小埃、**提取主色**、**生成描述** | | 音频 | 打开、人声提取(占位)、添加到待办、问问小埃、**转文字**、**时长/码率信息** | | 其他 | 打开、添加到待办、问问小埃、复制路径 | ### 7.2 能力来源 - **分词**:ICU `BreakIterator`(zh)先做;效果不够再换内置词典。 - **提取链接**:正则抽 `http(s)://`、`www.`、邮箱;弹框列出、单条/全部复制。 - **打开**:`QDesktopServices::openUrl`。 - **翻译 / 生成描述**:走小埃同学的模型(设置里选模型)。 - **朗读**:优先 GXDE Daemon TTS,回退 `spd-say`。 - **OCR**:优先 GXDE Daemon,未安装则禁用并提示。 - **待办**:§8。 - **问问小埃**:把内容作为附件塞进当前(或新建)对话的输入框。 --- ## 8. 待办 - 独立小窗口:可选置顶、可拖动、不进任务栏、位置大小持久化。 - 列表项:复选框、文本、删除;支持拖动排序。 - 数据存 `~/.local/share/gxde-ai-see/todo.json`。 - 入口:面板右键菜单、内容工厂「添加到待办」、桌面文件。 --- ## 9. 小埃同学 Chat ### 9.1 初版范围 - **API**:DeepSeek 官方预设 + 自定义 `base_url / model / key`; 首次启动引导开通 DeepSeek 并填 Key(可跳过,稍后在设置里填)。 - **对话**:多对话管理(新建/重命名/删除/置顶)、流式输出、Markdown 渲染、 代码块高亮/复制。 - **编辑**:修改用户消息、重试上一条、从任意消息 fork 新对话。 - **上下文**:token 估算;手动压缩;超阈值自动压缩(让模型总结旧消息); 可设上下文长度。 - **MCP**:stdio server 配置、`tools/list`、`tools/call`,以 function calling 暴露给模型。 - **设置**:模型参数、系统提示、代理、MCP、数据目录、关于。 - **独立入口**:`gxde-ai-see.desktop`,图标由 `Pictures/同步区/erotica/image_7.png` 处理成多尺寸 hicolor 图标。 - **关于**:版本、许可、仓库、致谢。 ### 9.2 技术 - Qt6 Widgets + DTK2;HTTP 用 `QNetworkAccessManager`(SSE 流式);SQLite 存对话。 - MCP 用 `QProcess` + JSON-RPC 2.0(stdio),协议与 DSH 对齐。 - UI 参考 DSH Web:左侧会话栏、消息流、底部输入框、设置。 - 后续:skill、DSH 定制发行、让聊天修改自身配置(安装 skill 等)。 --- ## 10. 后台与 gxde-k9 - 安装 `~/.local/share/GXDE/gxde-k9/slimy/gxde-ai-see.slimy`, 由 k9 每 5s 做 PID 检测并拉起 `gxde-ai-see --daemon`。 - 设置项「开机自启动与后台保活」= 创建/删除该 `.slimy` 文件。 - **环境不支持**(没有 `org.gxde.Portal`、X11 且无降级后端等): - 首次弹一次 toast(dconf/配置记 `warned-unsupported`); - 进程**不退出**,进入低资源空闲(不轮询、不建窗口),避免 k9 反复拉起。 --- ## 11. 工程结构 ``` gxde-ai-see/ ├── CMakeLists.txt ├── src/ │ ├── main.cpp │ ├── app.{h,cpp} # 单实例、生命周期、后台模式 │ ├── config.{h,cpp} # 配置 │ ├── staging/ # 暂存区模型/存储/缩略图 │ ├── ui/ # 面板、列表、委托、内容工厂、待办、聊天 │ ├── portal/ # org.gxde.Portal 客户端 + 降级后端 │ ├── chat/ # DeepSeek 客户端、MCP、会话存储 │ └── actions/ # 分词、链接提取、TTS、OCR 适配 ├── data/ # desktop、图标、gschema ├── debian/ ├── docs/PLAN.md └── README.md ``` 合成器侧:`flake-wm` 和 `gxde-wlcom` 各加 `src/vendor/gxde/portal/`。 --- ## 12. 里程碑 | 阶段 | 内容 | 可验证 | | --- | --- | --- | | **M0** | 技术验证:portal grab + 重定向 drop + present_windows portal 模式 | 嵌套合成器里拖一个文件进编辑器输入框 | | **M1** | App 骨架:构建/单实例/配置/面板/暂存区/持久化/动画 | 当前会话可用的暂存区 | | **M2** | 合成器 portal 模块(两个仓库)+ 原生 drop 全流程 | 传送门端到端 | | **M3** | 内容工厂(本地动作 + AI 动作)+ 待办 | 分词/提链接/翻译/待办 | | **M4** | 小埃同学 Chat(API/流式/多对话/编辑/重试/fork/上下文) | 可日常对话 | | **M5** | MCP、自动压缩、skill 预留 | 接 MCP server | | **M6** | 打包、k9、i18n、关于、图标、文档 | deb 可安装 | --- ## 13. 待确认 1. **「置顶」的语义**:v1 面板用 layer-shell,天然不抢焦点、不进 dock、位置精确, 但 layer-shell 无法做到「被普通窗口盖住」。可选: - A(推荐 v1):layer-shell;「取消置顶」= 被窗口遮挡时自动隐藏; - B:普通窗口 + 合成器窗口 hints(真正的 kept_above 开关,改动更大); - C:layer-shell + 合成器扩展「普通层」。 2. **传送门原生 drop 方案**(§4.3)是否认可?这是整个方案的地基。 3. **内容工厂**的动作集合与「朝有空间方向展开」的交互是否 OK? 4. **Chat 初版范围**(§9.1)是否合适?MCP / 压缩是否要放进初版? 5. **分词**先用 ICU 是否可接受? 6. **合成器落地顺序**:先 flake-wm 再移植 gxde-wlcom,还是两个仓库同步改? --- ## 14. 风险 | 风险 | 影响 | 对策 | | --- | --- | --- | | client 对不可见 surface 忽略 DnD | portal 失效 | M0 先验证;不行则降级为「激活窗口 + 粘贴」 | | 坐标映射误差(缩放/分数缩放) | drop 落点偏 | 用 item 实际 rect 与 view geometry 计算,加 clamp | | 合成器 grab 与拖拽 grab 冲突 | 拖拽中断 | portal 模式不装 wlroots grab,只用 wlcom 层 grab | | 目标 App 不支持 DnD | 没反应 | 提示「该窗口不接受拖放」 | | 大图/大文本 | 内存 | 图片落盘、文本截断展示、上限 200 | | k9 反复拉起 | 资源浪费 | 不支持环境不退出,低资源空闲 | | X11 行为差异 | 降级 | 图标网格 + EWMH | --- ## 15. 参考 - `flake-wm` / `gxde-wlcom`:`src/vendor/dkwin/presentwindows/`、 `src/input/selection.c`、`src/input/cursor.c`、`src/view/view_p.h` - `gxde-file-manager/gxde-desktop-panel`:hover / dodge 动画 - `gxde-control-center/src/frame/notify`:删除动画 - `dtk2widget-qt6`:DBlurEffectWidget / DApplication - `gxde-k9`:保活 - DSH(`deepseek-harness`):对话界面与 MCP 参考