- docs/PLAN.md: GXDE 小埃同学 v0.2 方案(传送门原生 drop、暂存区、内容工厂、待办、Chat、k9) - README.md: 项目介绍 - LICENSE: GPL-3.0-or-later
20 KiB
GXDE 小埃同学 · 设计方案
项目:GXDE AI See(小埃同学) 版本:v0.2(方案评审稿) 日期:2026-10-30 仓库:https://git.spark-app.store/shenmo7192/gxde-ai-see 状态:方案评审中,确认后开始编码
1. 一句话
拖一下,世界就到你手边。
小埃同学是 GXDE 桌面的常驻 AI 助手。它把「拖拽」变成一个贯穿桌面的动作: 拖到传送门,选一个窗口,内容就落进那个窗口的输入框; 拖到内容工厂,按文件类型分词、提链接、翻译、OCR、加待办、问小埃; 拖进暂存区,东西就先放着,随时再拖出去。
2. 产品组成
一个常驻进程(gxde-ai-see),四个模块 + 一个聊天客户端:
| 模块 | 说明 | 入口 |
|---|---|---|
| 传送门 Portal | 拖拽暂存面板 + 原生 drop 到目标窗口 | 拖拽开始时自动出现 |
| 内容工厂 Factory | 按内容类型提供动作 | 拖动时出现在面板下方 |
| 待办 Todo | 可选置顶、可拖动、不进任务栏的小窗口 | 面板菜单 / 内容工厂 |
| 小埃同学 Chat | DTK2 AI 聊天(DeepSeek / 自定义 API / MCP) | 独立 desktop 入口、传送门里的「问问小埃」 |
三者共享一套存储与 UI 组件;聊天是独立窗口,传送门是常驻面板。
┌─────────────────────────────┐
拖拽开始 ──────▶ │ 传送门面板(右下角,常驻) │
│ ┌─────────┐ ┌──────────┐ │
│ │ 传送门 │ │ 暂存区 │ │
│ │ │ │ 列表 │ │
│ └─────────┘ └──────────┘ │
│ ┌─────────────────────────┐│
│ │ 🔧 内容工厂(按类型出现) ││
│ └─────────────────────────┘│
└──────────────┬──────────────┘
│
┌──────────────┴──────────────┐
│ 合成器 portal(爆炸平铺) │
│ 原生 drop 到目标窗口输入框 │
└─────────────────────────────┘
3. 设计原则
- 原生优先:能用 Wayland/X11 原生协议完成的,不自己模拟。 传送门的落点用原生 DnD,不是剪贴板 + Ctrl+V。
- 不抢焦点、不打扰:面板不进入 dock,不抢键盘焦点,不打断用户正在做的事。
- 不监听剪贴板:暂存区不是剪贴板管理器,只收用户主动拖入的内容。
- 可降级:没有 GXDE portal 合成器时功能降级但程序可用,不崩溃、不反复重启。
- 数据可恢复:暂存区、待办、对话都持久化;文件消失自动清理。
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,只在合成器里做三件事:
-
portal 模式下的 present_windows:缩略图照常飞入,但
- 不安装 wlroots 的 pointer/keyboard/touch grab(否则会顶掉拖拽自己的 grab);
- 不锁 cursor,保留拖拽图标;
- 每帧按 cursor 更新 hover 高亮(复用现有的 105% 放大 + 亮度插值)。
-
一个 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()让拖拽图标继续跟随。
-
状态收尾:
- 关闭 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-shellOverlay 层,锚定右下,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 反复拉起。
- 首次弹一次 toast(dconf/配置记
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. 待确认
- 「置顶」的语义:v1 面板用 layer-shell,天然不抢焦点、不进 dock、位置精确,
但 layer-shell 无法做到「被普通窗口盖住」。可选:
- A(推荐 v1):layer-shell;「取消置顶」= 被窗口遮挡时自动隐藏;
- B:普通窗口 + 合成器窗口 hints(真正的 kept_above 开关,改动更大);
- C:layer-shell + 合成器扩展「普通层」。
- 传送门原生 drop 方案(§4.3)是否认可?这是整个方案的地基。
- 内容工厂的动作集合与「朝有空间方向展开」的交互是否 OK?
- Chat 初版范围(§9.1)是否合适?MCP / 压缩是否要放进初版?
- 分词先用 ICU 是否可接受?
- 合成器落地顺序:先 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.hgxde-file-manager/gxde-desktop-panel:hover / dodge 动画gxde-control-center/src/frame/notify:删除动画dtk2widget-qt6:DBlurEffectWidget / DApplicationgxde-k9:保活- DSH(
deepseek-harness):对话界面与 MCP 参考