原生桌面壳(Tether Desktop)

apps/Tether.Desktop 是 Tether 的原生图形桌面宿主:采用基于 PhotinoX 的轻量单进程架构,直接内嵌同一套 vendored Web 客户端,与 tether 命令行共享主目录、配置与会话存储,并通过插件化桥接契约实现原生目录选择、macOS 磨砂外观与优雅关停。

概述

Tether Desktop 是 Tether 的自有原生桌面壳实现(跟踪 milestone v0.23,#371)。在架构上,它明确不复刻 Electron,而是采取高度轻量化的单进程微壳方案:

  • 单进程微壳:Web 服务(Kestrel)与 WebView 窗口运行于同一进程内。进程启动后立即在后台线程池调用 AppBoot.StartAsync,就绪后窗口先经回环交接页再换取会话 cookie 进入主界面(见下文「同站交接平滑导航」)。两者共享内存与凭据上下文,不引入第二个 origin,也不使用自定义 scheme 转发,直接复用既有的浏览器信任闸(Host / Sec-Fetch-Site / Origin)。
  • 同一套 vendored UI:保持 web/upstream 源码只读,不新增任何 web/patches。客户端内建的桌面平台分支(如 html[data-platform='darwin']、html[data-fullscreen]、ui-directory-picker-native 等)由桌面壳在页面 Head 首位注入的预加载脚本激活,实现零补丁生效。
  • 与 tether 共享数据与 Profile:桌面壳默认使用 ~/.tether 主目录与 local profile,会话历史(<home>/<profile>/sessions)、工作区列表、用户设置(settings)、凭据与已安装插件与命令行完全互通,不建立第二份独立的桌面 profile。
apps/Tether.Desktop(进程入口:主线程消息循环 + 日志 + AppBoot 后台启动)
  │ BundleRuntimeOptions.DesktopShell = () => new PhotinoXDesktopShellPlugin(app, dispatcher, splash)
  │ BootOptions.Patches += bundles/desktop.patch.yaml(启动 overlay)
  ▼
组合树(profile local 的 bundles 栈 + desktop overlay)
  ├ desktop-shell   [Provider, Tether.Desktop.PhotinoX]  提供 IDesktopShell / IDesktopWindow
  │     窗口宿主、启动页、导航来源闸、外链外开、原生目录对话框、
  │     原生通知、Web 消息传输、macOS 原生外观(objc runtime)
  ├ desktop-bridge  [Consumer, Tether.Desktop.Web]        inject: IDesktopShell, IWebServer
  │     IndexInject preload:data-platform / data-fullscreen /
  │     __DSH_DIRECTORY_PICKER__ / dshDesktop;请求-响应通道分发
  ├ web-client-vendored(patch modules:browse → native directory picker)
  └ 既有 web 行(web-server、web-frontend-dsh、session-controller …)
        ▲
        │ http://127.0.0.1:{port}/__tether/desktop-handoff#{token}
        │   → 页面脚本 location.replace('/?token=' + hash)
        │   → 303 ./ (Set-Cookie: SameSite=Strict)
        │   → http://127.0.0.1:{port}/ (带 cookie 进入工作区)
        │   → 同 origin RPC / SignalR
  PhotinoWindow(WKWebView / WebView2 / WebKitGTK)

安装与运行

源码运行

在仓库根目录下,通过 dotnet run 即可直接启动桌面壳:

# 默认使用 ~/.tether 与 local profile 启动
dotnet run --project apps/Tether.Desktop

# 使用预录模型回复进行测试交互
dotnet run --project apps/Tether.Desktop -- --replies apps/Tether.Cli/replies/text.json

# 查看支持的启动参数
dotnet run --project apps/Tether.Desktop -- --help

本地打包脚本

桌面壳打包遵循本地显式动作原则(无 CI 额度,不走 GitHub Actions),使用 scripts/package-desktop.sh 脚本构建 self-contained 独立包:

# 为当前本机系统打包(macOS 默认输出 Tether.app 与 DMG 镜像)
./scripts/package-desktop.sh

# 指定版本号与目标架构 RID(支持 osx-arm64, osx-x64, win-x64, linux-x64)
./scripts/package-desktop.sh --version 0.23.0 --rid osx-arm64

# 跳过 DMG 制作,仅生成 Tether.app
./scripts/package-desktop.sh --skip-dmg

打包产物输出至 artifacts/desktop/<rid>/ 目录(已加入 .gitignore)。

未签名说明与 Gatekeeper

本地打包产物为未签名开发包(代码签名与公证环境挂点留给 #380)。在 macOS 上分发或跨机拷贝未签名的 .app 时,系统 Gatekeeper 会添加隔离标记导致无法启动。运行前需在终端执行:

xattr -dr com.apple.quarantine artifacts/desktop/osx-arm64/Tether.app

跨平台运行依赖

  • macOS:macOS 12.0 (Monterey) 或更高版本,底层由系统 WKWebView 提供渲染。
  • Windows:Windows 10 或 Windows 11;依赖 Microsoft Edge WebView2 Evergreen Runtime(Windows 11 已内置;Windows 10 若缺失需自行安装)。若安装了 Velopack vpk 工具,打包脚本会生成 Setup.exe 安装包。
  • Linux:依赖 WebKitGTK 4.1(如 Debian/Ubuntu 系统中的 libwebkit2gtk-4.1-0)。若安装了 vpk 工具,打包脚本会生成 .AppImage。

与 tether 的关系

桌面壳与命令行版 tether(WebHost)是同等地位的宿主形态,两者深度协同:

  • 共享数据:两者使用完全相同的目录布局(<home>/profiles/local 与 <home>/workspaces),会话事件流(JSONL)、设置、模型服务商配置及动态安装的外部插件完全一致。
  • 同 home 并行:同一个 home 下可以同时运行多个宿主(桌面壳与 tether web,或两个 tether web),每个宿主功能完整。会话写入由每会话内核锁(session.lock)裁决,同一会话同一时刻只有一个宿主能写;<home>/storage 下的 domain 存储只在每次写入时短暂持有 .writer 跨进程写区间;工作区注册表的每次修改在 <home>/workspace/workspace-registry.lock 内串行执行,并基于磁盘上的最新状态计算,两个宿主的修改不会互相覆盖(#458)。另一宿主对工作区的修改不会实时推送,本宿主下一次读取工作区(刷新页面、新建会话等)时才会看到。
  • 随机端口(端口 0):tether web 默认监听固定端口 8384;桌面壳启动时默认配置 --port 0,由操作系统分配空闲回环端口,两者运行互不抢占端口。
  • 单实例控制与激活:
    • 桌面主实例启动时在 <home>/desktop.lock 上获取独占内核锁,锁文件在进程生命周期内保持打开且永不删除(保持稳定 inode);
    • 主实例创建本地私有通信通道:Unix 系统上创建 <home>/desktop.sock 套接字(若 home 路径过长超过 103 字节限制,确定性回退至 $TMPDIR/tether-desktop/<16-hex>.sock,目录 0700、套接字 0600);Windows 上创建 CurrentUserOnly 命名管道;
    • 启动第二个桌面实例时,因锁被持有而捕获 LocalPathLeaseException,随即向已建立的通信通道发送 activate 消息请求主实例将窗口置顶激活,然后自身以退出码 0 立即退出;若通信通道不可用,则弹出原生警报提示后以退出码 1 退出。

启动与退出

秒开启动页与引导

桌面壳要求极致的启动响应:

  1. 秒开启动页:主线程在调用 PhotinoApplication.Run 后,首帧立即展示内嵌资源启动页(tether-app://boot/),呈现与 Web 端一致的 TETHER 字标、底色与 spinner,文案提示“正在启动本地服务…”。
  2. 后台异步引导:AppBoot.StartAsync 在后台线程池并行执行,装配插件树并启动 Kestrel。
  3. 同站交接平滑导航:
    • 引导成功后,由 IBrowserLaunchAuthority 生成带 launch token 的 URL;
    • 桌面桥接行提供 IDesktopLaunchHandoff 服务,将导航目标转换为本地回环静态交接页(http://127.0.0.1:<port>/__tether/desktop-handoff#<token>),token 仅携带在 URL fragment 中(不会发送至服务端网络层);
    • 解决 WebKit 跨站扣留 SameSite=Strict Cookie 根因:WebKit 按「发起这次导航的文档所在站点」判定 SameSite,启动页 tether-app://boot/ 的 host 是 boot,与 127.0.0.1 不同站;因此从自定义 scheme 启动页发起的顶层导航在服务端返回 303 重定向到 ./ 时,WebKit 会扣留服务端下发的 SameSite=Strict 会话 Cookie(请求头 Sec-Fetch-Site 为 none,并非判定依据),从而引发 401 认证失败。通过先导航至同源回环交接页,由交接页脚本在同源上下文下发起 location.replace('/?token=' + hash),使得整个 token 兑换与主界面请求均处于 same-origin 环境,Strict Cookie 正常附带,平滑换取认证进入工作区;
    • 在此之前,启动页已通过预加载脚本设置好平台标识,避免页面先按普通 Web 渲染再跳变的闪烁现象。

失败卡与三按钮交互

若启动阶段遭遇配置错误、插件冲突或损坏的 patch(抛出 StartupException 或 BootRollbackException),主进程不崩溃退出,而是将启动页原地刷新为诊断失败卡片:

  • 展示错误诊断摘要与生成的全量诊断日志文件绝对路径;
  • 提供三个原生交互按钮:
    • 重试(retry):重置启动状态,重新调度后台 AppBoot.StartAsync 启动流程;
    • 打开日志目录(logs):调用系统 NativeDesktop.RevealAsync,直接在 Finder(macOS)或资源管理器(Windows)中定位并高亮日志目录;
    • 退出(quit):触发协调关停流程退出进程。

关闭即优雅退出

桌面壳遵循“关闭即退出”原则,不常驻系统托盘或后台(与 #370 决策一致):

  • 拦截与收拢:用户点击窗口关闭按钮、按快捷键 Cmd+Q、菜单退出或系统发送 SIGTERM/SIGHUP 信号时,宿主拦截原生关闭事件(设置 Cancel = true),交由 DesktopShutdownCoordinator 处理;
  • 任务中确认:关停协调器检查 IAgentRegistry。若检测到仍有 Agent turn 处于 Running 状态,触发原生确认对话框提示用户。用户选择取消则恢复运行;选择确认则继续关停;
  • 优雅释放:按顺序取消启动任务令牌、等待 BootHost.DisposeAsync() 完成最终 drain、释放会话排他锁、销毁插件作用域并关闭单实例监听通道,最后调用 PhotinoApplication.Shutdown() 退出;
  • 强制退出:若关停期间收到第二次中断信号(如重复按 Ctrl+C),强制以退出码 130 立即终止。

常驻后台运行方案

需要 7×24 小时常驻后台运行以承载 Webhook 触发、定时调度或外部 API 的场景,应直接使用无界面的 tether web --no-open 命令,而不是将 GUI 窗口最小化到托盘。

桥接契约 v1

桌面壳通过能力缝 Tether.Desktop(程序集)定义契约,由 Tether.Desktop.PhotinoX(Provider)实现,并通过 Tether.Desktop.Web(Consumer)向浏览器环境注入预加载脚本 desktop-preload.js。

特性提供与不提供对照

桥接特性 / 标记状态设计理由与实现说明
html[data-platform]提供在 <head> 顶部同步注入(darwin / win32 / linux)。早于任何模块执行,用于驱动 Web 客户端内建的无标题栏、侧栏避让等 CSS 布局。
html[data-fullscreen]提供进出全屏时通过 window.fullscreen 事件动态设置或移除。macOS 进入全屏后自动收起红绿灯避让间距。
__DSH_DIRECTORY_PICKER__提供暴露 { pick: () => Promise<string|null> },内部调用 directoryPicker.pick 通道。新会话与侧栏添加工作区由此呼出原生目录选择器。
dshDesktop提供挂载 { protocolVersion: 1 },供客户端识别当前处于桌面桥接环境。
dshDesktopBoot不提供客户端引导态无需宿主特殊标记,由单进程启动页及认证重定向直接完成。
__DSH_HOST_PATHS__不提供WKWebView 与 WebView2 均缺乏类似 Electron 的 webUtils.getPathForFile 原生文件路径提取 API。拖入文件继续走标准 Web 上传流,拖入文件夹按客户端既有提示拒绝。
dshDesktop.browser不提供侧栏内嵌浏览器(ui-sidebar-browser)需要独立上下文的 guest <webview>,v1 暂不实现该复杂隔离能力,该模块不纳入桌面 roster。
dshDesktop.updates不提供遵循 Tether 的既定设计原则:“永远不自动重启、不自动更新”,不提供应用内自动更新通道。
dshPlatform不提供上游针对 DeepSeek 官方账号平台的特化界面,Tether 不承载该业务,故不提供。
__DSH_LOCALE__不提供WebView 运行时的 navigator.languages 已由系统语言环境决定,与浏览器表现一致,无需额外注入。

Roster 替换

在启动 overlay bundles/desktop.patch.yaml 中,桌面环境通过补丁将 vendored client 默认的 ui-directory-picker-browse 替换为 ui-directory-picker-native:

- op: replace
  path: /roster/modules/ui-directory-picker-browse
  value:
    id: ui-directory-picker-native
    name: ui-directory-picker-native

ui-directory-picker-native 会优先检测 window.__DSH_DIRECTORY_PICKER__ 呼出系统原生目录选择器;若桥接缺失则安全回退至 Web 浏览选择器。因此该替换只在桌面 overlay 中执行,普通 Web 模式保持原有 browse 选择器不变。

Wire 协议

Web 前端与 C# 宿主之间的双向通信基于 Photino 内建的 window.external.sendMessage 与 window.external.receiveMessage 机制构建。

报文格式

  • 单条报文约束:报文编码为 UTF-8 JSON,硬性限制最大 64 KiB;超限或畸形报文静默丢弃并记录丢弃计数,绝不反弹攻击。
  • 请求报文(JS → C#):
    {
      "id": 1,
      "channel": "directoryPicker.pick",
      "payload": null
    }
    
    id 必须为正整数;channel 必须为非空通道名称;payload 为可选的任意 JSON 节点。
  • 成功响应(C# → JS):
    {
      "id": 1,
      "ok": true,
      "value": "/Users/example/Projects/repo"
    }
    
  • 错误响应(C# → JS):
    {
      "id": 1,
      "ok": false,
      "error": {
        "code": "bridge-timeout",
        "message": "Request timed out."
      }
    }
    
  • 服务端事件推送(C# → JS):
    {
      "event": "window.fullscreen",
      "value": true
    }
    

错误码表

桌面桥接层定义了稳定的错误码(DesktopBridgeErrorCodes),并在下发给前端前强制进行安全映射脱敏,绝不向前端暴露内部异常调用栈或敏感信息:

错误码对应常量说明
unknown-channelUnknownChannel请求的目标通道未注册。
invalid-requestInvalidRequest请求 JSON 格式错误或缺少必填字段(如提取到有效 ID 则回复此错误)。
duplicate-channelDuplicateChannel通道名称重复注册(仅服务端内部异常)。
handler-failedHandlerFailed通道处理器执行抛出未受控异常,内部异常记录至 LifecycleEvents.Error,前端仅收到脱敏提示。
bridge-timeoutTimeout请求超时。注意:因仓库级 ErrorCodeOwnership 门禁约束禁止复用通用的 timeout 码,桌面桥接超时专属错误码为 bridge-timeout。
rejectedRejected请求被安全来源策略或参数校验安全拒绝。

超时控制规则

  • 默认超时:除模态对话框通道外,所有通道请求的执行均绑定 30 秒 超时时限;超时未完成则取消任务并回复 bridge-timeout。
  • 模态交互豁免:directoryPicker.pick 属于用户系统级模态交互,操作时长取决于用户选择,因此豁免超时。
  • 并发合并:directoryPicker.pick 内部由 DirectoryPickerCoordinator 管理,同一窗口内并发触发的多个目录选择请求会自动合并,共用同一个挂起的系统对话框,避免重复弹出多个弹窗。

macOS 原生外观

在 macOS 平台上,Tether.Desktop.PhotinoX 通过纯托管 Objective-C runtime P/Invoke(NativeObjcRuntime 直接调用 /usr/lib/libobjc.A.dylib)实现原生外观定制,不修改底层原生库,天然兼容 darwin arm64 与 x64:

  1. 隐藏标题栏:
    • 窗口创建后追加 NSWindowStyleMaskFullSizeContentView 样式掩码;
    • 设置 titlebarAppearsTransparent = YES 并将 titleVisibility 设为 NSWindowTitleHidden;
    • 窗口全内容撑满至屏幕顶端,红绿灯按钮保留在左上角。
  2. 侧栏 Vibrancy 磨砂材质:
    • 将 NSWindow 的 backgroundColor 设为透明,isOpaque 设为 NO;
    • 通过 KVC 动态设置 WKWebView 的 drawsBackground = NO;
    • 在 contentView 底部安全插入撑满窗口的 NSVisualEffectView(材质为 NSVisualEffectMaterialSidebar,状态 Active,混合模式 BehindWindow);
    • 配合 vendored UI 既有的透明度样式链(html/body 透明、主内容区不透明底色、侧栏半透明),实现纯正的 macOS 原生侧栏磨砂模糊效果。
  3. 顶部 52px 拖拽带几何判定:
    • 由于 WKWebView 不支持 WebKit 的 -webkit-app-region: drag 私有 CSS 属性,预加载脚本在捕获阶段监听 mousedown 事件;
    • 当点击纵坐标小于 52px(顶部操作条高度),且目标元素不是交互控件(排除 button, a, input, select, textarea, [tabindex], [role='...'] 等)时,发起 window.drag 请求,调用底层 BeginWindowDrag 启动物理拖拽;
    • 双击同一拖拽带区域发起 window.zoom,触发窗口 performZoom: 缩放。
  4. 标准系统主菜单(Menu Bar):
    • 独立于外观定制安装标准的 macOS 系统主菜单:
      • Tether:About Tether、Hide Tether、Hide Others、Show All、Quit Tether;
      • Edit:Undo (Cmd+Z)、Redo (Cmd+Shift+Z)、Cut (Cmd+X)、Copy (Cmd+C)、Paste (Cmd+V)、Select All (Cmd+A);
      • Window:Minimize (Cmd+M)、Zoom、Close (Cmd+W);
    • 保证了 WKWebView 焦点下所有标准剪贴板、撤销及全选快捷键稳定可用。
  5. Quit 映射逻辑:
    • 主菜单的 Quit Tether 绑定选择器 terminate:;
    • 底层 applicationShouldTerminate: 收到系统事件后触发 PhotinoApplication.ShutdownRequested;
    • 宿主拦截该请求并向 Cocoa 返回 NSTerminateCancel (0),阻止系统立即强杀进程,平滑转入关停协调器的优雅清理流程,最终由托管代码调用 Shutdown() 退出。
  6. 主题跟随:
    • 预加载脚本通过 MutationObserver 监听 html[data-ds-theme-source](取值 light / dark / system);
    • 触发 theme.set 通道动态调整 NSWindow.appearance,使侧栏磨砂材质与系统深浅色外观严格同步。
  7. 全屏响应:
    • 监听窗口全屏事件并推送 window.fullscreen,动态切换 html[data-fullscreen='true'],UI 自动收起红绿灯避让占位。

安全边界

桌面壳作为本地运行的高权限 GUI 载体,实施了严格的安全防御策略:

  • 导航来源安全闸(NavigationGate):
    • 主窗口顶层导航仅放行三类目标:匹配当前 WebServer 的回环 Host origin(http://127.0.0.1:<port>,Host 与 Port 均精确一致)、启动页 scheme(tether-app://boot/)以及初始化空白页(about:blank);
    • 其余所有外部 http/https 导航请求(包括同 Host 但不同端口)均被拦截,并自动交由操作系统默认外部浏览器打开(BlockAndOpenExternal);
    • 危险 scheme(如 javascript:、file:、data:、blob: 等)直接阻断拦截,绝不在主窗口或外部打开。
  • 外链与弹出窗口外开:
    • 页面请求打开新窗口(target=_blank 或 window.open)时,拦截在当前 WebView 内的生成,仅在确认为 http/https 协议时调用外部默认浏览器打开。
  • Web 消息过滤与脱敏:
    • WebMessageRouter 严格核对 Web 消息来源的顶层 URI;仅当来源完全匹配内部受信任的 Host origin 时才交由桥接分发器处理,非受信消息静默丢弃并计数;
    • 消息正文超限(>64 KiB)直接丢弃;日志输出绝不打印消息内容或启动 token;
    • 通道处理器抛出的所有未受控异常均脱敏为 handler-failed,详细堆栈仅内部记录至生命周期错误事件。
  • 无新增业务 /api 路由与交接页安全:
    • 桌面壳不暴露任何新增的业务 HTTP REST 接口,所有桌面专属能力严格限制在进程内内存 IPC 消息通道内;
    • 启动交接路由(/__tether/desktop-handoff)为 Exact 匹配的静态无状态中转页,不包含任何业务数据或会话状态;只响应 GET/HEAD(其他方法返回 405);严格配置 Content-Security-Policy: default-src 'none'; script-src 'sha256-...'; style-src 'sha256-...'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'、Cache-Control: no-store、Referrer-Policy: no-referrer 与 X-Content-Type-Options: nosniff;启动 token 仅通过 URL fragment 经客户端校验后做同源导航,绝不回传服务端网络层且绝不写回 DOM。
  • WebView 数据目录权限与生效差异:
    • 数据目录默认创建于 <home>/desktop/webview,按 Tether.LocalStorage 的 owner-only(POSIX 0700)纪律收紧访问权限;
    • 生效情况说明:PhotinoX 的 SetUserDataFolder 仅在 Windows(WebView2) 平台上实际生效并隔离用户数据目录;而在 macOS(WKWebView) 与 Linux(WebKitGTK) 平台上,底层原生库使用系统默认的 WebView 存储区,该参数不改变其物理存储位置。

fresh-home 首启决策

在 #373 实现期间(commit 052973f0),团队解决并固化了全新空 home 目录下的首启行为:

  • 背景:早期发行模板 src/Tether.Bundles/profiles/local/profile.yaml 声明的是 bundles: ["cli"]。这导致安装版命令 tether(默认以 WebHost 运行且缺省 profile 为 local)以及桌面壳在全新的 ~/.tether 目录下首次启动时,因缺少 CliHost 交互环境而无法正常启动。
  • 决策与调整:
    1. 将发行的 local profile 模板调整为 bundles: ["web"],对齐产品级默认 Web 栈;
    2. 开发 CLI(apps/Tether.Cli)在未显式传递 --profile 或 --composition 时,缺省使用 cli profile;
    3. AppBoot 的缺省 profile 维持 local 不变;
    4. 桌面壳在全新的空目录下启动时,能够直接挂载 Web 栈平滑首启,无需用户进行任何前置配置。

已知限制与后续工作

当前 v0.23 桌面壳的已知限制与规划后续项如下:

  1. 无内嵌浏览器:不支持侧栏内嵌网页浏览(ui-sidebar-browser),因其需要独立的 guest WebView 隔离上下文;
  2. 无拖放真实路径:缺乏原生文件拖放真实路径提取能力(__DSH_HOST_PATHS__),文件拖放统一走 Web 上传;
  3. Windows / Linux 原生窗框:Windows 与 Linux 当前采用系统标准窗框,未定制无标题栏与自定义顶栏(data-windows-titlebar);
  4. 未签名与未公证(#380):当前本地打包产物为未签名开发包,正式证书签名与 Apple Notary 公证流程留待 #380 处理;
  5. 无自动更新:桌面壳不包含自动下载更新或后台静默重启机制;
  6. 红绿灯精确定位:当前使用系统默认的红绿灯 inset(Web 端的 52px 顶部条已完全避开),像素级微调对齐留待后续优化;
  7. Wasm Worker 依赖环境 PATH:打包应用内置的 PTC 代码运行工作进程(Tether.PtcRuntime.Worker)为自包含配置,能在剥离 dotnet 的环境下运行;但若用户可选启用 Wasm worker,该 worker 仍以 dotnet <dll> 形式启动,因此环境 PATH 中必须存在 dotnet 可执行程序;
  8. 跨平台交叉打包(Cross-RID)未验证:在 macOS 上构建 linux-x64 或 win-x64 属于交叉发布,由于 apps/Tether.Desktop 配置了 ErrorOnDuplicatePublishOutputFiles=false,工作进程的 apphost 架构选取可能存在交叉混淆风险,非本机 RID 的发布包尚未在对应真机上全面验证;
  9. macOS 原生通知依赖 .app Bundle:macOS 原生通知服务要求必须在打包后的 .app Bundle 内运行;非 Bundle 环境下(如裸 dll 启动)发送通知会自动回退至子进程后端(osascript);
  10. 通知点击跳转限制:上游 vendored Web UI 仅支持客户端内存级会话切换,缺少基于 URL 路由的深层链接能力(deep link),因此通知点击事件仅负责唤醒与置顶桌面窗口,不进行会话跳转;
  11. 跨宿主工作区变更不实时推送:同一 home 下并行的宿主共享工作区注册表,但另一宿主的修改不会推送到本宿主已打开的界面,要等本宿主下一次读取或修改工作区(刷新页面、新建会话等)时才会出现;上游 dsh 同样只保证单进程内的变更可见(#458)。

验收

完整的自动化测试命令、通过数据、测试镜像门禁、Web 源码门禁以及 macOS 手工验收清单与观测记录详见:

👉 v0.23 桌面版验收清单与证据

在 GitHub 上编辑此页