KPanel 桌面图标工作区

September 20, 2026 · View on GitHub

  • 状态:已实现,待 L3 上线审批
  • 适用范围:宽屏桌面图标布局与框选、右侧小插件显隐、已安装应用与网站入口显隐、站点别名、URL/文件快捷方式及外部文件拖入上传
  • 业务真源:静态桌面入口、应用市场运行时清单、Agent 网站清单与文件系统
  • 持久化:入口与布局保存在 Panel 本地有界工作区;外部拖入通过文件管理写入 Agent 真实文件,桌面移除不删除真实资源

1. 目标与非目标

管理员可以在宽屏桌面框选或追加选择图标、整体拖动、交换位置或自动排列;刷新、重新登录和 Panel 重启后 恢复布局。已安装应用和网站可以仅从桌面隐藏并再次恢复;自定义快捷方式支持名称、描述、绝对 HTTP(S) URL 和可选本地图标的创建、编辑与删除。文件管理中的普通文件和目录可以通过菜单、批量 操作或拖放创建桌面引用;打开后进入对应文件管理位置,多个不同目标可在独立窗口中并行使用。

从 KPanel 文件管理添加的文件和目录入口只是指向 Agent 文件管理真实路径的快捷方式,不复制、不移动、 不上传目标。从 Windows、macOS 或 Linux 文件管理器拖入 KPanel 桌面的外部项目属于上传:文件内容通过 现有 Panel → Agent 文件流写入真实宿主机目录,再为成功的顶层文件或目录创建桌面入口。零配置默认目录 为 /home/KPanel Desktop,不存在时首次上传自动创建;管理员可在传输状态中切换到任意已经存在、能由 KPanel 文件管理访问的规范绝对目录。该位置是当前浏览器的轻量偏好,不进入 workspace,也不复制文件 真源。桌面编组是可选的展示容器,不提供文件系统文件夹、像素级自由重叠、 多人独立桌面、跨主机 workspace 同步或远程图标抓取;已配对 KPanel 间的显式文件复制采用独立协议, 见 cross-kpanel-file-transfer.md。不改变经典模式、应用安装状态、网站配置、Nginx、Docker 或 kejilion.sh 产物。桌面图标落点吸附现有网格;超过单页容量时扩展纵向可滚动工作区,不隐藏、 截断或重叠入口。编组只保存成员稳定键,不能映射为 Linux 目录或成为文件真源。

2. 真源与删除语义

桌面合并四类入口:

  1. desktopApps 提供固定系统入口,稳定键为 nav:<route>
  2. 应用市场 inventory 提供已安装应用、状态、名称、URL 和市场图标,稳定键为 app:<id>
  3. sites list 提供已启用网站、域名、URL 和站点图标,稳定键为 site:<32 lowercase hex>
  4. 工作区保存 URL、文件和目录快捷方式,布局稳定键为 shortcut:<32 lowercase hex>;文件路径 仍以 Agent 文件系统为唯一真源。

应用与网站先按既有规则去重,再应用工作区的显隐、站点别名和位置。工作区不保存应用或网站的 URL、图标、安装状态等快照;真源变化后桌面刷新应采用最新值。暂时未发现的来源不主动删除其位置, 来源重新出现后仍可恢复。

“从桌面移除”只把 app:<id>site:<id> 加入 hiddenEntryKeys,绝不调用应用卸载、网站删除、 容器、Nginx 或宿主机写入接口;固定 nav: 入口和 shortcut: 不可加入隐藏集合。管理窗口负责恢复 隐藏入口。删除快捷方式只从工作区移除对应 shortcut 及 shortcut:<id> 位置,metadata 提交后由 服务端回收其无引用图标;文件或目录快捷方式使用“从桌面移除”,绝不删除、移动或修改真实目标。 右侧内置小插件只把稳定的 widget:<id> 加入 hiddenWidgetKeys,隐藏不会删除插件状态或 widgetPositions;重新显示时优先恢复原位置,若该位置已被占用则由网格引擎重新吸附到可用位置。

3. Workspace v5

GET /api/v1/desktop/workspace 的响应结构如下;iconVersioniconURL 仅在快捷方式已有合法图标时 返回,warning 仅在存储不可用时返回:

{
  "schemaVersion": 5,
  "resourceVersion": "sha256:<64 lowercase hex>",
  "available": true,
  "groups": [],
  "hiddenEntryKeys": ["app:thirdparty-example"],
  "hiddenWidgetKeys": ["widget:services"],
  "positions": {
    "nav:/overview": { "x": 0, "y": 0 },
    "shortcut:0123456789abcdef0123456789abcdef": { "x": 0.25, "y": 0.5 }
  },
  "widgetPositions": {
    "widget:clock": { "x": 0.82, "y": 0 },
    "widget:monitor": { "x": 0.82, "y": 0.32 }
  },
  "labels": {
    "site:0123456789abcdef0123456789abcdef": "我的网站"
  },
  "shortcuts": [
    {
      "id": "0123456789abcdef0123456789abcdef",
      "name": "管理入口",
      "description": "自定义说明",
      "targetType": "url",
      "url": "https://example.com/path",
      "iconVersion": "<64 lowercase hex>",
      "iconURL": "/api/v1/desktop/shortcuts/0123456789abcdef0123456789abcdef/icon?v=<version>",
      "createdAt": "2026-08-14T00:00:00Z",
      "updatedAt": "2026-08-14T00:00:00Z"
    },
    {
      "id": "fedcba9876543210fedcba9876543210",
      "name": "nginx.conf",
      "description": "",
      "targetType": "file",
      "path": "/etc/nginx/nginx.conf",
      "createdAt": "2026-08-14T00:00:00Z",
      "updatedAt": "2026-08-14T00:00:00Z"
    }
  ]
}

全量 PUT /api/v1/desktop/workspace 请求只包含可写业务字段:

{
  "expectedResourceVersion": "sha256:<64 lowercase hex>",
  "groups": [],
  "hiddenEntryKeys": [],
  "hiddenWidgetKeys": [],
  "positions": {
    "nav:/overview": { "x": 0, "y": 0 }
  },
  "widgetPositions": {
    "widget:clock": { "x": 0.82, "y": 0 }
  },
  "labels": {},
  "shortcuts": [
    {
      "id": "0123456789abcdef0123456789abcdef",
      "name": "管理入口",
      "description": "自定义说明",
      "targetType": "url",
      "url": "https://example.com/path"
    }
  ]
}

positions 仅保存宽屏工作区内的逻辑坐标:x ∈ [0,1]y ∈ [0, MaxPositions]。横坐标在当前 可用宽度内归一化;纵坐标以当前可视区的纵向可移动距离为归一化单位,不足一个网格步长时取一个 步长。整数和小数共同表示连续的跨页偏移,y > 1 的位置位于首屏以下,因此不受单页网格容量限制。 widgetPositions 使用同样的归一化坐标;插件尺寸由前端注册表声明,布局引擎按矩形占用多个基础网格 单元。服务端拒绝 NaN、Infinity、越界值及超过位置总量上限的请求。

positions 允许 nav:app:site:、已存在的 shortcut:group:widgetPositions 只接受稳定的 widget: 键;labels 只接受 site: 键,且只 改变桌面显示名,不修改网站真源。resourceVersion 由规范化 metadata 计算,不包含图标内容;快捷 方式时间戳由服务端维护,不由客户端提交。请求采用严格 JSON,未知字段、重复或非法稳定键、悬空 shortcut 位置均拒绝。

targetType 必须是 urlfiledirectory。URL 目标只带 url;文件和目录目标只带 path, 且路径必须是最长 4096 字节的规范绝对 POSIX 路径。服务端不在 workspace PUT 时读取目标路径,避免 每次布局保存产生文件系统 I/O;文件管理在实际打开时重新查询目标并显示已删除、无权限或类型变化。

项目v5 上限
自定义快捷方式64 个
隐藏入口512 个
隐藏小插件512 个
图标 + 分组 + 插件位置512 个
分组 / 分组成员合计32 个 / 512 个
工作区编码后大小256 KiB
名称 / 描述48 / 160 个 Unicode 字符
URL2048 字节
文件或目录路径4096 字节
单张图标 / 图标总量256 KiB / 16 MiB
图标边长 / 总像素单边最大 1024 px / 最大 100 万像素

Workspace v1 在读取时将既有快捷方式迁移为 targetType:"url",Workspace v2 在读取时补充空的 widgetPositions,v1/v2/v3 补充空 groups,v4 组在缺少格位时按既有成员顺序派生,下一次成功 PUT 后原子写为 v5; 迁移不访问 URL 或宿主机文件。现有 kpanel:desktop-site-names:v1 只做一次迁移:将仍有效且服务端 尚无值的站点别名合并进 labels,全量 PUT 成功后才删除旧 localStorage 键;冲突或写入失败时保留 旧值,避免丢失或重复覆盖。

3.1 可选桌面编组

默认 groups: [],升级不自动编组,组外图标继续散放。每组为 { id, name, members, columns, collapsed, rows?, slots? }:ID 为唯一 32 位小写十六进制,名称 1–48 字符, 列数 2–4,成员为有序且跨组唯一的 nav: / app: / site: / shortcut: 稳定键。 不允许嵌套组或小插件,shortcut 成员必须存在;暂时隐藏/离线的应用和网站保留成员关系。 组锚点保存为 positions["group:<id>"],不覆盖成员原散放坐标。slots 为成员稳定键到 0–511 逻辑格位的映射,格位跨行按 columns 编号,同组不得重复,键必须是成员;缺少的成员赋予最早空格。 显式空格不自动压紧,隐藏/暂离线成员占位保留。rows 字段仅保留旧 schema 兼容,不再影响高度; 前端读取时按 0 派生且不触发写入,后续分组变更保存为 0。高度随最高占用格位自动增减,不预留末尾空行。 窄屏将逻辑行折行显示,不覆盖宽屏格位。

桌面空白右键可新建空组,多选工具条和图标右键可“编为一组”,均直接以本地化默认名称创建,不弹配置框;拖到散放图标附近连续停留 450ms 显示“松开创建分组”,放下后才保存,快速经过、离开或 Esc 不成组。图标可拖入、拖出; 拖入空格保留其他空位,单图标在组内拖到占用格时交换,跨组加入时将被占格成员移至后续空格。 菜单加入优先填空位;Ctrl/Command+方向键提供逐格移动。拖动标题空白区移动整组;单击组名原地编辑,Enter/失焦保存,Esc 取消,空名保留原名,输入法选字不提交,失败保留草稿供重试。更多菜单仅含重命名和解散。标题按钮 收起/展开,收起条按格位顺序显示前 3 个可见成员的缩略图,窄屏减少缩略图但保留名称与操作。 分组底色与文字跟随应用浅深主题。展开分组的外框、碰撞与拖放命中占用同一整数网格矩形, 分组外框对齐桌面网格,组内使用独立紧凑网格:四列图标外框占四列,默认宽 375px,与时间组件同宽;左右内边距 16px、列间距 8px,图标 52px、文字 14px,成员格高 78px,绘制与拖放命中共用格位尺寸。标题高 36px,高度向桌面整行对齐,多余空间均匀分配到行间与上下留白;两行高 196px、三行高 296px。组内不强求与外部图标逐行对齐。邻接外部图标/插件/展开组的水平间距为 5px、垂直为 4px。展开组落点吸附桌面网格,旧锚点仅派生对齐、不在加载时回写。展开与收起分组拖动中均连续跟随指针,成员同步平移;移动阶段只限制工作区边界,不运行落点吸附或碰撞回退、不保存。松手后才一次判定落点,碰撞时恢复原位置、不推开其他项;取消拖动不写入。收起保留 56px 紧凑高度及松手后的像素边缘吸附。主动留出的格位不压缩。桌面固定每逻辑行 4 个,旧组读取时保留格位并按四列显示,后续操作按四列保存;窄屏仍折行适配。不设置预留行数、列数或最大图标数量;已有存储容量边界不变。 最后一个成员拖出或移至其他组后自动移除空组,仍可撤销;主动新建的空组不受影响,隐藏成员不视为移出。 解散只恢复散放入口,不删除任何真实资源。 每次放下或确认只提交一次 workspace;保存失败恢复已确认状态,冲突沿用已有 CAS 提示与重载。 最近一次编组操作可撤销,后续 workspace 变更后撤销失效,防止覆盖新状态。 散放和组内图标始终使用同一 transform 坐标系,成组/解散/撤销时用 280ms 缓出归位;拖拽时无过渡、保持跟手。 底板成组时轻微缩放淡入,解散时约 200ms 淡出;退场底板立即 inert 且不截获点击。点击创建/解散后立即展示过渡,失败恢复布局与原选择并持续显示错误;减少动画偏好禁用过渡。 收起成员不可键盘聚焦、框选或全选;窄屏派生布局不覆盖宽屏锚点。

PUT 显式 groups: [] 表示解散全部组;旧缓存客户端省略 groups 时保留已有组和组锚点, 仅移除已删除快捷方式的成员引用。旧 v4 页面省略 slots 时保留已有格位/行数;原样透传当前 slots 却改变成员时,只清理被移除成员的格位;纯排序将新的成员顺序映射到原占用格位,保留空格。 显式修改的非法格位仍被拒绝。新客户端总是提交格位;v5 不增加独立 API、权限或 Agent 写入。

4. API、权限与并发

当前接口为:

GET    /api/v1/desktop/workspace
PUT    /api/v1/desktop/workspace
GET    /api/v1/desktop/shortcuts/{id}/icon[?v=<64 lowercase hex>]
PUT    /api/v1/desktop/shortcuts/{id}/icon
DELETE /api/v1/desktop/shortcuts/{id}/icon
GET    /api/v1/files/entry?path=<absolute-posix-path>
POST   /api/v1/files/entries
POST   /api/v1/files/upload?path=<directory>&name=<file>&overwrite=false
POST   /api/v1/files/actions
  • 全部接口要求有效 Session;PUT/DELETE 还要求同源 Origin 和 CSRF。错误方法、路径、RawPath 与未允许 的查询参数必须被拒绝。
  • workspace PUT 是全量替换,必须携带当前 expectedResourceVersion;版本不匹配返回 409。前端将 连续写入串行化,每次基于最新已确认快照提交;冲突时重载服务端胜者并向用户报告失败,不自动重放。
  • 图标 PUT 的 body 是原始二进制,不是 JSON 或 multipart;Content-Type 只能是 image/pngimage/jpegimage/webp。DELETE 无请求体。图标写入与 workspace resourceVersion 相互独立, 图标端点当前不使用 expectedResourceVersion
  • 创建或编辑带图快捷方式是两阶段操作:先全量 PUT metadata,再按需 DELETE/PUT 图标并重新 GET workspace。图标阶段失败时,已提交的快捷方式或 metadata 保留,编辑窗显示错误并允许重试;不得 宣告整个操作成功,也不得把该边界描述为跨文件原子事务。
  • 自定义 URL 前后端均解析校验,只允许无 userinfo 的绝对 http:/https: URL;拒绝相对地址、 控制字符、无 host、非法端口及 javascript:data:file:blob: 等协议。localhost 与 私网地址可作为管理员浏览器的跳转目标,但 Panel/Agent 不访问目标,不形成 SSRF。
  • URL 入口继续复用跳转确认窗并使用安全的新窗口打开策略。workspace 和图标变更均写审计;审计只 记录入口 ID、类型、字节数或变化计数,不记录完整 URL、查询参数、描述、本地路径和文件名。
  • 文件路径前后端都只接受规范绝对 POSIX 路径,继续复用文件管理既有根目录约束、保护路径、符号链接 与权限判定。files/entry 只返回单个现有条目的元数据;files/entries 要求同源 CSRF,一次接受 1–64 个 不重复规范路径,只返回可访问条目并把不可用路径单独列出。两者都不读取文件内容;打开文件后才按文件管理 既有预览规则读取,目录只列出当前目录。快捷方式保存/打开不会把路径拼入 Shell,也不会新增路径审计。
  • 外部拖入不新增上传协议:每个普通文件继续使用 raw application/octet-stream 的既有 upload 端点, 目录继续使用固定枚举的 mkdir action。Panel 校验 Session、同源 Origin、CSRF、Content-Type 和 512 MiB 请求体上限后流式转发给 Unix Socket Agent;浏览器取消会中止未完成请求。上传审计沿用 文件管理既有 file.upload 记录,不把浏览器本地路径写入请求或日志。

5. 布局与交互

.desktop__icons$ 的一页可视区域按容器实际宽高和现有单元尺寸计算网格,不使用窗口坐标猜测。图标和 插件共用该工作区;图标占 1 \times 1 基础单元,插件按注册的 $columns × rows 占用矩形区域,任何可见对象 都不能重叠。 工作区允许纵向滚动并按相同网格步长连续扩展;水平坐标始终夹取在当前页内,纵向坐标可落到后续 可视区。单个图标拖到已占网格时交换位置;选中组整体移动时保留相对网格形状,目标冲突则吸附到 最近的完整空位,未选中图标不得被批量移动挤乱。所有可见键在整个滚动工作区必须对应唯一显示位置,超过单页容量 的入口继续排列到下一页,绝不能回退到 (0,0)、重叠或变为不可访问。

最多 512 个图标和插件拥有可持久化位置,并分配至全局首个空位。自动排列在此上限内按当前入口顺序逐页 执行 column-major:先在本页按列从上到下、再从左到右填满唯一网格位,然后进入下一页。可见入口 超过 512 个时,超出部分进入相互不重叠的临时区域,仍可打开、隐藏或删除,但不能拖动、键盘移动或 参与自动整理;自动整理应停止并提示限制,不发 PUT、不报告成功。滚动偏移不属于 workspace,不得 误写为图标位置。

  • window.innerWidth > 760:读取并保存唯一一套宽屏 positionsx 映射到当前页可用宽度,y 映射到连续纵向页面。视口变化后重新计算每页行列与唯一占位,但不得让溢出键重叠。

  • window.innerWidth <= 760:始终按入口顺序逐页派生紧凑滚动布局,不读取为独立位置档位,也不把 派生结果回写。拖拽、键盘移动和自动排列在此宽度禁用;返回宽屏后恢复原宽屏位置。

  • 插件在宽于 900px 的工作区中显示;管理弹窗可以按卡片组件单独显隐,窄屏隐藏插件但不清除 widgetPositions。插件的非交互主体区域均可拖拽,按钮、输入等交互控件保留自身操作;插件获得焦点后 使用 Ctrl/Command + Arrow 移动。新增插件必须使用稳定的 widget:<id> 键,并在注册表中声明组件、 管理图标、翻译标题、基础网格宽高和默认落点。

鼠标按下时只记录拖拽候选,不得立即调用 Pointer Capture。移动距离达到 6 px 后才进入拖拽并获取 pointer capture;阈值内的普通 clickdblclick 必须继续以子按钮为事件目标,分别完成选择和打开。 成功拖拽后短时抑制 click/dblclick,避免放下即打开。pointercancellostpointercapture、第二 指针和窗口失焦取消本次预览,不提交位置。拖动到可视区上下边缘时应有界自动滚动,并用工作区坐标 计算跨页落点。

宽屏鼠标在桌面空白处移动至少 4 px 后显示框选区域,只选择与框相交的可见图标;按住 Ctrl/Command 或 Shift 可在已有选择上追加,普通点击单个图标恢复单选。选中两个及以上入口时显示 轻量批量栏,支持整体拖动、取消选择和一次确认“从桌面移除”。固定 nav: 入口只能选择与移动; 批量移除时应用和网站写入 hiddenEntryKeys,快捷方式删除对应 metadata 与位置,整个动作只发一次 workspace PUT。确认文案必须明确不会卸载应用或删除网站、文件、目录;失败时保持选择,不报告成功。 Ctrl/Command+A 选择全部图标,Delete/Backspace 打开同一批量确认;Escape 按层级取消框选、菜单 或当前选择。框选只在宽屏鼠标启用,不改变触摸、手写笔或紧凑布局的既有手势。

触摸与手写笔保留单击打开和长按菜单;宽屏下移动达到 12 px 后直接拖动,无需额外整理模式。 键盘聚焦图标后使用 Ctrl/Command + Arrow 移动或交换,Enter/Space 打开,ContextMenu 或 Shift+F10 打开菜单;位置变化通过 aria-live 宣告。可见焦点、上下文菜单与减少动画偏好不得退化。

文件管理提供三条一致入口:单项右键“添加到桌面”、多选工具栏批量添加、把已选普通文件或目录拖到 桌面空白区。同一 KPanel 内的快捷方式拖放采用同页面内存令牌,只信任当前文件管理产生的数据;拖到另一个文件窗口执行文件 传输而不是创建入口,拖到其他组件或任务栏不执行操作。添加是单次 workspace PUT,重复目标不重复创建,超过 64 个快捷方式时整批失败且不部分 写入。拖放只创建引用,不改变源文件位置;落点按现有网格吸附,批量目标从落点起顺序排列。

跨 KPanel 拖放另用版本化、非凭据的 DataTransfer 描述符。目标 KPanel 必须通过已有 Cluster v2 配对身份向来源重新鉴权并拉取内容;一次最多处理 64 个顶层项目,每项独立原子提交。复制到目标桌面后 批量创建入口;拖到目标文件管理窗口的空白区或目录项时只复制到对应目录。跨面板永远是复制,不改变来源。 桌面上的 file / directory 快捷方式也可作为来源:宽屏鼠标以原生 drag 传递当前路径、类型和 resourceVersion,桌面悬停或键盘聚焦时用轻量角标说明可跨面板复制。元数据由单次有界批量请求预取, 缺失、无权限或类型变化的入口不进入描述符;混合框选只传可用文件/目录并明确提示跳过数量。应用、网站、 URL 和固定系统入口不生成文件描述符。同一桌面落下只移动整组选中图标,不复制真实文件。 完整规则见 cross-kpanel-file-transfer.md

操作系统外部拖入与上述内部令牌严格分流。外部 DataTransfer 中的普通文件和目录先在浏览器内有界 枚举,保留相对目录结构和空目录;浏览器绝对本地路径不会读取、提交或显示。松开后展示轻量传输浮层, 包括保存位置、当前文件、总进度、取消和完成/部分失败状态;落点只使用桌面网格坐标。上传最多两路并发, 单文件沿用文件管理的 512 MiB 服务端上限,单次拖入额外限制为 500 个文件、256 个目录、32 层、 2 GiB 总量。项目同名时生成 名称 (1) 等副本名,不覆盖现有真实文件;目录中的单个文件失败时保留 已经落盘的内容,显示部分失败,不报告整批成功。成功上传的文件/目录是真实 Agent 文件系统资源,桌面 图标仍只是入口;“从桌面移除”绝不删除真实内容,删除必须进入文件管理并复用回收站流程。

拖放方向保持单义:外部项目拖到桌面是“上传并创建入口”;外部项目拖到已打开的文件窗口沿用文件管理 上传到当前目录;文件管理内部项目拖到桌面是“仅创建入口”;文件管理项目拖到另一个文件窗口的空白区 或目录项默认移动,按住 Ctrl/Option 时复制。相同目录、源目录本身及其后代属于无效目标;同名目标不 覆盖,批量操作允许部分成功并展示结果。窗口间文件剪贴板共享同一内存状态,为键盘用户提供复制/剪切 后在另一窗口粘贴的等价路径。跨 KPanel 文件管理拖放无论修饰键如何都只执行复制;反向复制需要反向配对授权。 桌面或文件窗口中的单个普通文件拖到 KPanel 窗口外时,Chromium 桌面端通过同源流式下载向 Windows Explorer 或 macOS Finder 提供原文件;单个目录或批量选择实时流式打包为一个 ZIP 后以同样方式传出。 该附加载荷不改变桌面组拖动、窗口内复制或跨 KPanel 描述符;不支持该协议的浏览器继续保持内部拖放语义。 应用、网站、URL 或固定系统入口拖入文件窗口仍不生成 .desktop/.url,也不复制真实目标。桌面文件/ 目录快捷方式拖入同一 KPanel 的文件窗口不执行操作;拖入另一个已配对 KPanel 的文件窗口则按跨面板规则 复制真实目标。窗口间复制和上传传输可取消当前请求;移动开始后不提供主动 取消,确保客户端取得每个成功项目的新路径并同步桌面引用。Agent 仍以同目录 临时文件、fsync 和原子重命名为提交边界,已成功提交的早先项目不会因后续取消而回滚或被删除。

文件窗口的复制/移动沿用 Agent copy/move 动作并携带源 resourceVersion,服务端逐项拒绝状态已变化 的来源。操作结果返回每个成功项目的原路径和目标路径;移动或重命名成功后,Panel 以 workspace CAS 同步更新指向该项目及其后代的桌面文件/目录快捷方式,保留名称、图标位置和其他元数据。部分失败只更新 成功映射;workspace 冲突刷新后重试一次,仍失败时明确提示“真实文件已移动、快捷方式未同步”,不得 把文件操作报告为失败或静默删除快捷方式。所有打开相同源目录或目标目录的文件窗口收到轻量刷新通知; 已经打开被移动目录或其后代的其他窗口会跟随到新路径。

文件快捷方式打开父目录并选中/预览该文件,目录快捷方式直接打开目标目录。每个不同目标使用独立 文件管理窗口;再次打开完全相同目标时恢复并聚焦已有窗口,避免重复窗口。真实目标被删除、重命名、 权限收回或类型改变时,保留快捷方式并在打开时明确报错,管理员可从桌面移除后重新添加;不得静默 改指其他路径。URL 快捷方式仍可编辑,文件和目录名称来自创建时快照,本期通过重新添加完成改名。

6. 图片与持久化安全

服务端先用 http.MaxBytesReader 限制原始请求体,再同时校验声明 MIME、魔数、实际解码格式、边长和 像素数;PNG、JPEG、WebP 以外的 SVG、GIF、HTML、伪 MIME、坏图和像素炸弹全部拒绝。服务端忽略 客户端文件名和扩展名,不提供远程图标下载。

工作区保存在 ${DataDir}/desktop-workspace/workspace.json,图标保存在同目录 icons/<id>.icon; 目录权限为 0700,文件权限为 0600。图标文件名由已校验 shortcut ID 决定,iconVersion 是内容 SHA-256 的 64 位十六进制值。图标 GET 要求 Session,返回 ETagnosniff;带匹配 ?v= 时使用 private immutable 缓存,否则使用 private no-cache。

workspace 全量 PUT 通过同目录临时文件、fsync 和原子重命名提交,metadata 重命名是提交点;成功后 再尽力回收无引用图标。图标 PUT 单独原子替换 <id>.icon,图标 DELETE 是独立删除操作。启动和每次 metadata 提交后清理遗留临时文件与无引用 .icon 文件,但不删除其他任意文件。并发图标写入没有 metadata 乐观锁,最终内容以最后完成的成功原子替换为准,不能影响 workspace metadata。

缺少 workspace 文件时创建空 v5 配置。文件损坏、未知 schema、超限或非法结构时保留现场,并以 available:falsewarning:"desktop_workspace_unavailable" 返回空工作区;桌面继续使用真实来源和 默认布局,后续 workspace 与图标写入返回 503,不得覆盖损坏文件。核心登录、应用、网站和经典模式 不得受影响。

代码回滚可恢复上一稳定提交;仅支持 v4 或更早的旧版本会把 v5 视为不可用并保留独立工作区,不会静默 覆盖,重新升级后可继续读取。发布前如需兼容旧二进制,应先停写并按版本门禁确认;如需 回退数据,应停写并成对备份/恢复 workspace.jsonicons/,再校验权限、JSON、快捷方式与图标 对应关系,不能只恢复一侧。

7. 资源与兼容门禁

桌面首次加载只新增一次 workspace GET,并与 inventory/sites 并行;应用、网站、文件和目录入口不得 各自新增 metadata 请求。只有用户打开文件快捷方式时才新增一次 files/entry 校验。自定义图标按 实际显示懒加载并使用版本缓存。拖拽预览与纵向滚动期间不发网络或磁盘请求,只在放下、自动排列、 显隐或编辑确认后提交。最大数据集下需验证 256 KiB metadata、 512 个图标/插件位置、64 个快捷方式和 16 MiB 图标配额不会造成对象重叠、明显主线程卡顿、无界内存增长或 磁盘写放大。

支持当前项目浏览器基线的 Pointer Events、crypto.randomUUID 或兼容降级 ID 生成;服务端仍对所有 ID 做最终校验。功能保持独立 schema 和目录,避免旧版本整体写回其他状态文件时丢失新字段。候选需完成 L3 门禁并获得上线授权后,才能合并 main、打标签、发布镜像或部署生产环境。

8. L2 验收矩阵

维度必须验证
业务真源与删除隐藏应用/网站后 inventory、网站配置、容器、Nginx 和真实数据均不变;管理窗口可恢复;来源名称、URL、图标变化后刷新采用新真源;固定入口不可删除
Workspace 契约GET/全量 PUT 字段与类型一致;仅接受 nav:app:site:shortcut: 位置键;shortcut 位置必须有对应记录;labels 仅接受 site:x ∈ [0,1]y ∈ [0,MaxPositions],NaN/Infinity/越界及各容量上限校验生效
自定义快捷方式URL 无图回退、创建、编辑、描述、跳转确认、删除、64 个总上限;删除 metadata 后图标回收;图标阶段失败时 metadata 状态明确、编辑窗不报假成功且可重试
文件与目录快捷方式文件管理单项/多选/当前目录添加;拖到桌面空白区创建且不移动真实目标;同一路径去重、整批限额失败;文件打开父目录并预览、目录直接打开;不同目录独立窗口、同一目录聚焦复用;删除/改名/权限/类型变化明确失败;从桌面移除不改变文件系统
外部桌面上传Windows/macOS/Linux 外部文件与目录可递归上传,保留空目录和相对结构;默认目录首次自动创建、自定义现有目录生效;同名保留两份;单文件/文件数/目录数/深度/总量上限有效;最多两路并发;进度、取消、部分失败和完成状态准确;仅成功顶层项目创建入口;从桌面移除不删除上传内容
路径与拖放安全仅规范绝对 POSIX 路径;拒绝控制字符、反斜杠、空/点/父级组件、内部临时名前缀和超长路径;保护路径、只读目录和符号链接沿用文件管理规则;内部令牌与外部 Files 数据不混用,本地绝对路径不上传;窗口间移动/复制拒绝相同位置、源目录及其后代并携带资源版本;Panel/Agent 不执行 Shell,取消和失败不留下临时文件
URL 安全危险协议、userinfo、控制字符、相对 URL、无 host、非法端口和超长 URL 被拒绝;私网 URL 仅由浏览器确认后打开,服务端零出站请求
图片安全raw binary PUT 成功;multipart、SVG、HTML、GIF、伪 MIME、坏图、像素炸弹、超 1024 边长、超 100 万像素、超 256 KiB 和总配额超限均拒绝;未登录读取失败;ETag/304 与版本缓存正确
权限与审计未登录 GET/PUT/DELETE 被拒绝;缺 Origin 或 CSRF 的写入被拒绝;错误方法、RawPath、查询参数、Content-Type 和未知 JSON 字段返回明确 4xx;workspace 与图标变更有审计且不泄漏完整 URL/描述/路径/文件名
并发两标签基于同一 workspace 版本全量 PUT 只有一方成功、另一方 409 并重载远端胜者;快速连续保存按确认顺序串行,乱序响应不回滚;并发图标替换保持单个完整文件且不改 metadata 版本
鼠标与键盘真实 Chromium/Firefox 验证阈值前不 capture,普通 click/dblclick 仍命中子按钮;空白拖动 4 px 后框选,反向框选与 Ctrl/Command/Shift 追加正确,取消恢复原选择;选中组整体拖动保持形状且只写一次,冲突吸附最近完整空位;批量移除固定入口不变、应用/网站仅隐藏、快捷方式仅删入口且单次 PUT;Ctrl/Command+A、Delete/Backspace、Escape、Ctrl/Command+方向键、Enter/Space、菜单键和焦点/播报正确
触摸与手写笔单击打开、长按菜单、宽屏直接拖动、12 px 阈值、多指、pointercancel、失焦与滚动边界不误开或误保存;窄屏不允许重排
响应式与容量至少覆盖 1440、1280、761、760、390、320 px 和 80%、125%、200% 缩放;单页容量、单页容量+1、64 shortcuts+固定/动态入口均逐页唯一且可滚动访问;第 513 个入口进入独立临时区且可打开,不能重排,自动整理不 PUT、不报成功;<=760 只派生紧凑布局且不发位置 PUT,1440 → 390 → 1440 恢复原宽屏位置;图标不侵入任务栏或安全区
失败与损坏磁盘满、只读目录、原子替换失败、请求取消和进程重启不报告假成功;metadata 已提交而图标失败时可辨识和重试;损坏/未知 schema 保留现场、返回 unavailable 并只读降级
存储与回滚空目录初始化、权限、工作区/图标配额、孤儿回收、成对备份恢复;新版本写入后回滚旧二进制再升级,workspace 和图标仍可恢复
性能与体验最大数据集、连续拖拽、图标懒加载/缓存、深浅色、中英文、减少动画、加载/空/失败/冲突状态和零新增控制台错误
工程门禁相关 Go 单测与 race、Web store/API/布局/组件测试及构建通过;make verify-changemake verify-l2、Linux amd64/arm64 构建和真实浏览器验收形成证据

本地 mock 预览只证明布局和交互观感,不能替代 Session/CSRF、真实落盘、并发冲突、Linux 重启、 失败注入和回滚兼容证据。上线前必须另行冻结候选,并按项目 L3 发布流程重新验收。