DeepSeek Harness Tauri 桌面版 | Only 8mb installer, zero environment setup, preset plugins, Windows / macOS / Linux.
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
fix(ci): Linux ARM64 改用 Ubuntu 22.04 构建并新增 GLIBC 2.35 产物门禁 (#1129) * fix(ci): Linux ARM64 改用 Ubuntu 22.04 构建并新增 GLIBC 2.35 产物门禁 (#1124) * fix(ci): 修正 GLIBC 版本排序并让解包扫描失败时门禁失败 (#1124) | 3 天前 | |
chore: 归档过期文档并删除测试文档 | 18 天前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
fix(native): stop iPadOS window controls from covering the page in windowed mode (#1164) iPadOS 26 窗口模式下,左上角的窗口控件会盖住页面,侧栏按钮点不到。现在 ios.infoPlist 设 UIDesignRequiresCompatibility,让系统像对旧版应用那样在顶部 为控件留一条窄带,内容随之落到控件下方。代价是系统原生控件保持 iOS 26 之前 的外观。这是临时方案:Xcode 27 构建会忽略该键,后续应升级到 Expo SDK 58, 在 SceneDelegate 中返回 .minimal 窗口控件样式。 In windowed mode on iPadOS 26, the window controls in the top-left corner cover the page, so the sidebar toggle can't be tapped. Setting UIDesignRequiresCompatibility in ios.infoPlist makes the system reserve a thin band at the top for the controls, as it does for older apps, so content moves below them. The trade-off is that native system UI keeps the pre-iOS 26 look. This is a stopgap: Xcode 27 builds ignore the key, so the follow-up is upgrading to Expo SDK 58 and returning the .minimal windowing control style from its SceneDelegate. | 10 小时前 | |
fix(sidebar): let remote web and DSH Mobile collapse the sidebar on wide screens (#1161) dsh-tauri-ui's global stylesheet hid the official "Collapse sidebar" button because the desktop navbar has its own toggle. That stylesheet loads in every client, and remote web and DSH Mobile have no desktop navbar, so at 1024px and wider an open sidebar had no button to close it. Drop the rule. Inside the desktop app, dsh-tauri's sidebar tweaks already hide the same button by its label, only in the desktop iframe. On phones, mobile-ui now hides the button itself while its drawer owns the sidebar, because the drawer is forced open and the button can't close it. dsh-tauri-ui 的全局样式表因为桌面 navbar 自带开关,隐藏了官方的「收起侧边栏」 按钮。但这张表在所有客户端都会加载,远程网页与 DSH Mobile 没有桌面 navbar, 所以在 1024px 及以上宽度,侧栏展开后没有按钮可以收起。现删掉这条规则。桌面端 由 dsh-tauri 的 sidebar tweaks 按文案隐藏同一个按钮,且只在桌面 iframe 内生效。 手机上抽屉接管侧栏时侧栏被强制展开,该按钮收不起抽屉,改由 mobile-ui 自行隐藏。 | 16 小时前 | |
fix(pet): 修复 Codex 宠物拖动始终朝右 (#1140) * fix(pet): respect Codex drag direction over bubble animations * test(pet): align tooling contract and scope drag precedence to Codex | 2 天前 | |
feat: 隔离夜间构建品牌并更新 Tauri 应用名称与图标 (#965) * feat: isolate nightly branding and refresh Tauri identity * fix: preserve upgrades across the Tauri product rename | 12 天前 | |
fix(ci): Linux ARM64 改用 Ubuntu 22.04 构建并新增 GLIBC 2.35 产物门禁 (#1129) * fix(ci): Linux ARM64 改用 Ubuntu 22.04 构建并新增 GLIBC 2.35 产物门禁 (#1124) * fix(ci): 修正 GLIBC 版本排序并让解包扫描失败时门禁失败 (#1124) | 3 天前 | |
docs: add screenshot delivery spec and handle reference (#1069) | 6 天前 | |
feat(remote): SSH 与远端访问统一化为 dsh-tauri-remote(S1–S6) (#990) * refactor(remote): dsh-tauri-ssh → dsh-tauri-remote 统一化(新基线 S1) - 插件包、宿主路由、客户端生成物、桌面壳桥白名单、manifest/README/文档全部迁到 dsh-tauri-remote 与 /api/tauri/remote 命名空间 - 保留上游新基线的 h3 结构:host/server/index.ts + defineWebServer + h3 handler - 状态目录迁到 ~/.dsh/remote,附 ~/.dsh/ssh 旧目录迁移与用例 - 类型面对齐:Remote* 命名、transport 契约下沉(transport.ts + transport.ssh.ts) - 验证:pnpm typecheck、插件单测 632 通过、eslint 干净 * feat(remote): S2 网关内核 (#10-05) 出站与入站共用的唯一 HTTP 反向代理内核:上游 URL 抽象与双向转发、Host/Origin 改写与 DSH 原生 cookie 注入、认证门禁与来源信任判定、zstd/gzip 压缩协商、 内部来源标识注入、监听生命周期,以及面向消费者的进程内状态与事件面。 - cookie 注入:铸造只由内核执行,token provider 每次重取、按 authority 缓存, 仅对可重放请求在 401 时重铸一次;失败不缓存、不阻断转发 - 认证:4 种来源 × 3 种认证配置的判定矩阵,登录页/401/403 三种表现, PBKDF2 密码 + HMAC 会话票据(含签发与过期时间)+ 链接 Token + 失败限速 - 压缩:zstd → gzip → identity,体积阈值、不可压缩类型、上游已编码重压; SSE 逐块 flush 不聚合;Upgrade 只跳压缩与缓冲,改写与注入照旧 - 内部来源标识(x-dsh-remote-source / x-dsh-remote-session):先剥离再注入, 隧道入口恒按公网,会话标识不出现在日志、响应与出站请求头中 - 转发请求体必须自描述:按上游声明的分帧重建,杜绝裸尾部字节构成的请求走私 - 升级腿在 cookie 铸造窗口内也持有 liveness,客户端中途断开即归位,连接数不泄漏 - 入站/隧道腿剥离上游 set-cookie(Spec 未规定的额外加固,已用用例钉住) (cherry picked from commit 623c182c021c3174460397e8c3473f85ed564cce) * test(remote): S2 用例回到新基线(scriptedCtx 转发 logger + 迁移/transport 覆盖补回) * chore(remote): 补交重命名遗漏(壳侧桥 /api/tauri/remote 白名单 + 锁文件包名) * feat(remote): S3 出站改造 (#10-05) 远端机器连接改走网关内核:连接产物收敛为无凭据的回环入口 http://127.0.0.1:<gatewayPort>,每台已连接机器一个 outbound 网关实例 (上游=本次连接的 SSH 隧道监听口,随连接生灭,端口跨重连复用); 启动 token 以 provider 形式交给网关按需铸造,远端实例重启后重铸仍成功, 读取失败不阻断连接也不缓存;网关未就绪不得报 connected,网关/上游异常 退出转 reconnecting,端口全部占用直接 given-up;SSH 链路启用 zlib@openssh.com(置于 none 之前,协商不到静默降级);隧道层不再注入 cookie。 (cherry picked from commit b9331ee4c7188221e033ce7d560e0e880d8bb0df) * fix(remote): S3 出站网关的链路判死与状态守卫 (#10-05) - 网关 `upstream` 是逐请求错误通道(504 超时 / 502 不可达),不再一律拆除链路: 仅入口自身 stopped 或回环隧道口 ECONNREFUSED 才重连,其余记可读事件并保留 link - 补齐 `gateway.start` 之后的 generation 复查:断开不再被成功路径静默覆盖 - 内核 cookie 铸造失败事件接入事件环(S3 §8 的「记录一条可读事件」) - 端口耗尽改用类型化错误码,去掉对中文文案的脆弱耦合 - detach 原因统一脱敏;beginReconnect 合并 pending 回收 (cherry picked from commit cd0102cdd95c5aa158ca2a9d8d3215035fd1c16a) * feat(remote): S4 入站暴露 (#10-05) (cherry picked from commit 88d023d9f9f13659ee6f1aa55e6d1ffb189966ee) * feat(remote): S5 公网隧道与宿主端口发现 (#10-05 S5) - 新增 service/tunnel.ts 与 utils/cloudflared.ts:quick/具名两种模式、tunnel 网关入口、 进程生命周期与事件环、PATH→数据目录→官方源/ghfast.top 镜像的二进制解析与自检 - access.json 的 tunnel 段收敛为显式 schema(enabled/mode/token/hostname), GET /access 带出隧道运行期视图(完整分支含链接与二维码,脱敏分支去掉 token/链接/二维码) - 新增 POST|DELETE /api/desktop/dsh-tauri-remote/access/tunnel(loopbackOnly) - 本机上游端口优先取宿主 webserver.port,DSH_WEB_PORT 仅作回退,非桌面载体可用 - 脱敏分支保留凭据存在性(hasPassword/hasToken),仍不含任何凭据内容 - genapi 管道登记面板专用路由目录:pnpm genapi 不再把 /access* 写进两份生成客户端 - 补测试:tunnel 9 例、cloudflared 8 例、/access 端口耗尽 error 路径、宿主端口优先、tunnel 段解析 (cherry picked from commit 3115e88cdf88072884f327f1c0e11c4456a6cf96) * feat(remote): S6 入站访问面板与部署闭包修复 (#10-05 S6) - 面板新增「本机访问」标签页:监听地址/端口、认证与 scope、密码与链接 Token 管理、 完整/掩码链接与二维码、公网隧道(quick/具名)启停与事件尾 - client/service/access.ts 直连同源 /access* 路由并逐字段容错解析;store 新增 access 模块; 隧道启动期由既有 1.5s 轮询刷新,其余时刻靠操作回包更新 - shell 深链常量补 REMOTE_TAB_ACCESS,契约测试同步 tab 与 4 条 access 路由 - qrcode 改为构建期内联 + devDependency:S1/S4 引入的运行时依赖会让 build:plugins 的部署闭包校验在 pngjs/browser.js(browserify 产物)上失败,已复现并修复 - 测试:面板组件 6 例、数据层 4 例,路由表/apply/remote-section 契约用例同步 (cherry picked from commit 36e5d0729dcf86ab518f4a0eb6651e0e65dfa7bd) * test(remote): 入站暴露真实宿主 e2e 与出站 WebSocket 升级覆盖 (#10-05) - 新增 dsh-tauri-remote-access.e2e.ts:真实 dsh web 宿主(非桌面载体)上的 /access 契约—— 默认态、回环监听真转发 + 宿主 webserver 端口发现、链接 Token 轮换/作废 + 二维码、 隧道参数校验不落盘、非回环来源 403 - dsh-tauri-remote.e2e.ts:出站网关在升级请求上注入铸造出的会话 cookie 并透传 WebSocket, 回显一帧往返,堵上 S3 评审点名的「RPC 与对话流全跑在 WS 上却无升级用例」验证缺口 (cherry picked from commit 8a62a9a14b5a0092ed977150b84513d61debd02a) * style(remote): eslint --fix 收敛三元换行、类型导入与数字字面量大小写 (cherry picked from commit 65924d3c8a008f3e122fcee285b71dbfd8d2a9c3) * feat(remote): 面板实测修正——清除密码、隧道状态行常驻与真实浏览器走查 本机实测(真实 dsh web 宿主 + Chromium + 真实 cloudflared quick 隧道)发现并修掉三处: - 设了访问密码就无法清除:POST /access 的 password 支持 null(清除),面板补「清除密码」按钮 - cloudflared 启动日志把「公网隧道已就绪」挤出事件尾:状态行单独常驻,进程尾只留最近 4 条 - 监听地址下拉在 iframe 里的指针命中不稳:面板补 aria-label,用例改以 HTTP 路由布置前置状态 新增 test/e2e/plugins/dsh-tauri-remote-panel.e2e.ts:真实浏览器走查(面板渲染、回环链接、 带 Token 的链接与二维码、凭据开关回写、DSH_E2E_TUNNEL=1 时真实 quick 隧道启停),截图落 test/e2e/.artifacts/;access 用例收尾同时清认证与密码,共享宿主不再把凭据留给后续文件。 (cherry picked from commit bde7cd9913ba971fb9ef08844379a1c8a5934c6c) * fix(remote): 新基线上补齐面板来源门禁、前缀与生成配置 - 面板写入口恢复自建 loopbackOnly:宿主 guard 只看 socket 地址,而暴露/隧道请求都从回环网关转发进来, 仅靠 guard 会让公网来源直接改本机暴露配置(网关标识声明为公网的用例已覆盖) - 面板客户端与 e2e 的 API 前缀统一为 /api/tauri/remote,修掉面板 404 - genapi transform 增加 Remote 段,生成物与提交一致(pnpm genapi 幂等) - qrcode/@types/qrcode 进入 workspace catalog(新基线无 utils/types 条目) - 验证:pnpm typecheck、全量 unit 3690 通过、eslint 干净、插件 e2e 19 通过 * style(remote): 面板服务导入排序 * refactor(remote): 面板来源门禁改为入口中间件并补 L1 路由测试 - 按 plugin.host.md 的口径:共享安全检查统一在 server 入口 app.use,业务 handler 不再各自包一层 remoteSourceGuard 只拦写方法,读方法仍由 handler 返回脱敏状态 - 新增 server/panel.test.ts:真实注册 + HTTP 验证三条面板路径、方法集、回环/公网读写行为 - 验证:pnpm typecheck、插件单测 1031 通过、eslint 干净、插件 e2e 19 通过 * test(e2e): 入站暴露用例挂进共享宿主并在当前内核批次运行 - 共享宿主 DEFAULT_ALSO 增加 dsh-tauri-remote:两条新用例依赖插件真的被挂载 (CI 之前默认只挂 pet + DEFAULT_ALSO 列表,故 404 / 找不到「服务器」分区) - CI:legacy 内核(0.1.5-rc)排除这两条用例,与 extension-panel 同口径; 改由「Host and migrated plugin contracts」(当前内核)批次运行 - 提交 dsh-tauri-ui 生成的 Tailwind 产物:面板新增 utility 后产物漂移, CI 的 Verify generated Tailwind CSS is current 会拦 - 本地验证:当前内核批次 19 通过(1 跳过),全插件 project 除既有 dsh-tauri-ui 用例(当前内核下本就不适配)外全绿 * test(remote): 面板分区切换按 aria-selected 重试点击 CI 上 iframe 内的指针命中会偶发丢失(与监听地址下拉同一现象),一次性 click 会 30s 超时; 改为断言 aria-selected 并重试点击,仍以面板挂载为最终判据 * feat(remote): 隧道腿按公网主机名判同源,暴露默认带凭据,隧道启动显示阶段进度 - 网关:isCrossSiteRequest 接受 Origin/Referer 命中隧道公网主机名的请求 (Safari/旧浏览器不发 Sec-Fetch,此前一律 403,用户无法在隧道页面改密码); 入口新增 publicHost 提供者,跨站拦截记 source 事件,拒绝文案补可执行建议 - 暴露:开启暴露且完全没有凭据时自动生成 20 位随机强密码并开认证,明文只在响应里出现一次 - 隧道:启动前强制开认证(先做纯校验,参数不合法不改认证),响应可带一次性 generatedPassword - cloudflared:下载改流式并回报 check/download/extract/verify 进度与字节数; 隧道把进度写进状态与事件环(每 5% 或 400ms 节流),下载阶段带 url - 状态:链接一律生成二维码并标注来源(loopback/lan) - 验证:插件单测 1031 通过 * feat(remote): 「远程访问」面板重排——入口置顶、进度可见、一次性密码提示 - 面板:访问入口置顶(本机/局域网 + 公网两行,各带来源标签、地址、复制、二维码; 未监听或隧道未运行时给占位与「开启暴露 / 启动隧道」动作) - 面板:认证 → 监听 → 公网隧道 → 事件;监听区写明「转发到桌面端当前后端」 - 面板:自动生成密码用高亮 callout 展示一次(复制 / 知道了),轮询刷新不覆盖 - 面板:隧道启动按阶段显示进度文字,下载阶段带百分比进度条与镜像地址 - 文案:分区与标签改「远程访问 / 连接到 SSH 服务器 / 同步到远端」,分区 order 置顶(-1); Hero 与机器面板标题改远程访问口径 - 用例:面板单测补 4 条(入口行、占位动作、一次性密码、进度), e2e 更新为「回环也出二维码、开启暴露自动带认证、隧道强制认证并有进度」 - 产物:重跑 build:taiwindcss 提交新增 utility 后的 CSS - 验证:typecheck、eslint、全量单测 5058 通过、插件 e2e 19 通过 * fix(remote): 面板嵌套层改用已注册的 layer-3 令牌 插件 Tailwind 主题只登记 layer-1 / layer-3(tailwind.plugins.config.js), 写 bg-layer-2 会静默无产出:入口行与自动密码提示会没有背景。 只改令牌,产物回到生成器精确输出(CI 的 Tailwind 漂移检查通过)。 * fix(remote): 落实评审——摘要校验、凭据不入 argv、损坏档案守卫、清理顺序、并发启停 按 SkanderBog 的故障注入清单逐条修: - cloudflared 下载:先取 GitHub Release API 的官方 sha256 与体积,边下边算摘要、 比对通过才解压执行;镜像只兜底传输,校验值始终来自官方;缺失摘要或校验失败一律拒绝 (可用 CLOUDFLARED_VERSION 固定版本) - 具名隧道凭据改走 TUNNEL_TOKEN 环境变量,不再出现在进程参数里(ps 可见) - access.json 损坏时 apply/ensureAuth/rotate/revoke 全部拒绝写入并给出可读原因, 不再用默认文档覆盖原文件 - 插件退出:清理串行化,先停各服务、回收子进程,最后才清运行时依赖 (此前 void dispose + 立即 clearHostRuntime 会让在途清理读不到 deps) - 隧道启停串行化(single-flight 队列)+ 激活世代守卫:并发 start/stop 不再各起一个进程, 被取代的激活会立刻回收刚起的 cloudflared - 拿到域名后落盘失败不再从子进程日志回调抛错,转为可读失败状态 - 用例:摘要不匹配拒绝执行、凭据不入 argv、损坏档案拒绝写、清理顺序、并发启停、落盘失败 * test(remote): 面板草稿保留与 e2e 前置复位(评审复现的两处乱序失败) - 面板:密码/端口写失败时保留用户草稿,不再静默清空或回退旧值 - e2e:每个用例前置把暴露与监听地址复位到确定值;档案直读容忍文件尚未创建 * fix(remote): 分区顺序退回 50,并给移动端设置用例补详情落定等待 - 分区 order 不能取 -1:移动端设置弹窗以「首个分区」为首屏详情页,语言切换项在通用设置里, 我们的分区排最前会让移动端首屏落到远程访问(dsh-tauri-mobile-settings 契约失败)。 改为 50:仍在插件分区里最靠前,但不越过核心分区。 - 上游 mobile-settings 用例在分类列表因新增分区回流后立刻点语言项会踩竞态 (超时 30s):等详情标题落到「通用设置」再操作。本地连跑 4 次(含 shuffle seed 201)稳定通过。 * fix(remote): 落实评审——壳分享弹层、设置三卡、短期配对凭据与共享路由 - 壳:右上新增「远程」分享弹层(局域网/公网隧道、短期二维码与配对码、Bridge Mobile 占位),齿轮直达本机「远程访问」设置页 - 设置:远程访问页重排为局域网/公网隧道/访问控制三张紧凑分组卡,草稿取消/保存、日志按需展开;SSH 开关移入机器 Tab;删除整页 Hero 与全局禁用按钮 - 宿主:临时配对码 5 分钟轮换,分享链接与二维码只带短期凭据,?auth= 同时接受配对码 - 路由:入站分享 4 条路由提升到管理表,同步 rust 白名单、契约测试与两份生成客户端;完整凭据管理仍只在面板表 - 机器人意见:apply 清理失败不再吞异常、隧道 stop 保留失败原因、tunnel.test 还原 DSH_WEB_PORT、README/manifest 更名、面板 e2e 按 API 收尾 * chore(ui): 同步合并后的 Tailwind 产物 面板二维码由 132px 收窄为 96px、新增 max-w-[320px],重新生成的合并产物与源码一致。 * fix(ui): 壳分享弹层不再覆盖 HeroUI 主题圆角,面板 e2e 复位与永久码断言对齐 壳层主题测试禁止在 HeroUI 组件上使用 rounded 工具类;面板 e2e 收尾改为完整复位(关认证、清密码、撤凭据、停隧道),永久码断言改为 base64url 43 位。 * fix(remote): 短期配对凭据不再依赖局域网监听 - 宿主:pairing 改为按入口独立构造(access.utils.ts pairingLinksOf), 只开公网隧道、不开局域网时也能拿到短期隧道链接与二维码; RemoteAccessPairing.link/qr 变为可选,客户端解析器同步放宽。 - 壳分享弹层:动作返回的一次性访问密码明文在弹层内保留,可复制、可关闭; 轮询与动作分开计数,动作进行中暂停轮询,按钮不再可能永久锁死。 - 面板配对行回退到隧道短期链接/二维码,不再引用永久凭据。 * fix(remote): 对齐分享菜单与配置页并接通一次性配对 * fix(remote): 阻止远程访问管理页面与接口 * fix(remote): 设置入口改用网络图标 * test(ui): 等待迁移弹层销毁后再结束用例 --------- Co-authored-by: hairyf <wwu710632@gmail.com> | 7 天前 | |
chore(core): 升级推荐内核到 0.2.1-rc.2 (#1123) * chore(core): upgrade recommended baseline to dsh 0.2.1-rc.2 * test(core): align native and local recommendation assertions with rc2 * test(desktop): assert rc2 cold assembly and isolated CLI admission | 3 天前 | |
fix(native): stop iPadOS window controls from covering the page in windowed mode (#1164) iPadOS 26 窗口模式下,左上角的窗口控件会盖住页面,侧栏按钮点不到。现在 ios.infoPlist 设 UIDesignRequiresCompatibility,让系统像对旧版应用那样在顶部 为控件留一条窄带,内容随之落到控件下方。代价是系统原生控件保持 iOS 26 之前 的外观。这是临时方案:Xcode 27 构建会忽略该键,后续应升级到 Expo SDK 58, 在 SceneDelegate 中返回 .minimal 窗口控件样式。 In windowed mode on iPadOS 26, the window controls in the top-left corner cover the page, so the sidebar toggle can't be tapped. Setting UIDesignRequiresCompatibility in ios.infoPlist makes the system reserve a thin band at the top for the controls, as it does for older apps, so content moves below them. The trade-off is that native system UI keeps the pre-iOS 26 look. This is a stopgap: Xcode 27 builds ignore the key, so the follow-up is upgrading to Expo SDK 58 and returning the .minimal windowing control style from its SceneDelegate. | 10 小时前 | |
fix(profile): 额外窗口切换档案不再顶替主窗口的启动档案 (#1162) | 16 小时前 | |
fix(desktop): restore main window hide and release v0.24.5 (#1148) | 2 天前 | |
refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 (#1155) * refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 * fix(ui): 同步删除 kernel 后重新生成的 Tailwind 产物 | 20 小时前 | |
feat(extension): 将官方插件合并到扩展管理首 Tab (#870) * feat(extension): embed official plugins as first management tab * test(extension): cover official plugins on current core * test(extension): count retained hidden tabs explicitly * style(extension): align official plugin header and rename panel * fix(extension): target nested official detail header * test(extension): compare only the skills page heading * fix(extension): adjust position of plugins icon in extension panel | 17 天前 | |
feat: one-click install desktop app for DeepSeek Harness - Installer/download page modeled on the early n8n-based damn-reports Installer + StepStatus: icon, headline, detail, value-labelled progress bar, and a terminal-style log panel fed by install-progress events. - 4-phase tracked install (Node runtime + packaged harness download/extract) with skip-if-installed and hash verification of the cached archive. - Bundled Node v22.22.0 runtime (meets v22.15.0+ / v23.8.0+ requirement); dsh entry resolved from the packaged manifest. - Launch dsh web on 127.0.0.1:3080, health-check, then embed the UI in an iframe with a sidebar for logs, settings and version info. | 2 个月前 | |
fix(plugin): repair broken preset plugin installs and unblock uninstall (fix #90) issue #90(macOS):首次引导安装全部推荐插件后服务启动失败——profile 清单(dependencies + dsh.profile.bundles)已写入插件,但 node_modules 中的产物缺失/损坏,dsh loader 报 ERR_MODULE_NOT_FOUND(Cannot find package ...)整体失败;且 pnpm 在损坏的 node_modules 上同样无法卸载/升级,用户被卡死在「启动失败 + 卸载失败」组合。 修复: - internal:内置插件(dsh-tauri)由构建期 prebuild 拉取产物随包分发,安装固定走 file: 本地依赖,启动时强制核对已安装且路径指向当前捆绑目录,缺失/被卸载即自动重装,不再依赖 git/registry; - verify:服务启动前核对清单引用的预装插件是否真实存在于 profile node_modules,缺失时以现有 manifest+lockfile 为准执行 pnpm install 重建依赖图;仍缺失给插件记录错误标记(前端插件面板暴露问题); - install::remove:dsh plugin remove 失败且插件仍被清单引用时也回落离线卸载(此前仅「命令成功但插件仍在」才兜底,pnpm 失败场景用户无路可退);受保护包仍如实上报。 | 1 个月前 | |
fix(chat): honor reduced motion during scroll following (#1116) | 3 天前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
feat(remote): SSH 与远端访问统一化为 dsh-tauri-remote(S1–S6) (#990) * refactor(remote): dsh-tauri-ssh → dsh-tauri-remote 统一化(新基线 S1) - 插件包、宿主路由、客户端生成物、桌面壳桥白名单、manifest/README/文档全部迁到 dsh-tauri-remote 与 /api/tauri/remote 命名空间 - 保留上游新基线的 h3 结构:host/server/index.ts + defineWebServer + h3 handler - 状态目录迁到 ~/.dsh/remote,附 ~/.dsh/ssh 旧目录迁移与用例 - 类型面对齐:Remote* 命名、transport 契约下沉(transport.ts + transport.ssh.ts) - 验证:pnpm typecheck、插件单测 632 通过、eslint 干净 * feat(remote): S2 网关内核 (#10-05) 出站与入站共用的唯一 HTTP 反向代理内核:上游 URL 抽象与双向转发、Host/Origin 改写与 DSH 原生 cookie 注入、认证门禁与来源信任判定、zstd/gzip 压缩协商、 内部来源标识注入、监听生命周期,以及面向消费者的进程内状态与事件面。 - cookie 注入:铸造只由内核执行,token provider 每次重取、按 authority 缓存, 仅对可重放请求在 401 时重铸一次;失败不缓存、不阻断转发 - 认证:4 种来源 × 3 种认证配置的判定矩阵,登录页/401/403 三种表现, PBKDF2 密码 + HMAC 会话票据(含签发与过期时间)+ 链接 Token + 失败限速 - 压缩:zstd → gzip → identity,体积阈值、不可压缩类型、上游已编码重压; SSE 逐块 flush 不聚合;Upgrade 只跳压缩与缓冲,改写与注入照旧 - 内部来源标识(x-dsh-remote-source / x-dsh-remote-session):先剥离再注入, 隧道入口恒按公网,会话标识不出现在日志、响应与出站请求头中 - 转发请求体必须自描述:按上游声明的分帧重建,杜绝裸尾部字节构成的请求走私 - 升级腿在 cookie 铸造窗口内也持有 liveness,客户端中途断开即归位,连接数不泄漏 - 入站/隧道腿剥离上游 set-cookie(Spec 未规定的额外加固,已用用例钉住) (cherry picked from commit 623c182c021c3174460397e8c3473f85ed564cce) * test(remote): S2 用例回到新基线(scriptedCtx 转发 logger + 迁移/transport 覆盖补回) * chore(remote): 补交重命名遗漏(壳侧桥 /api/tauri/remote 白名单 + 锁文件包名) * feat(remote): S3 出站改造 (#10-05) 远端机器连接改走网关内核:连接产物收敛为无凭据的回环入口 http://127.0.0.1:<gatewayPort>,每台已连接机器一个 outbound 网关实例 (上游=本次连接的 SSH 隧道监听口,随连接生灭,端口跨重连复用); 启动 token 以 provider 形式交给网关按需铸造,远端实例重启后重铸仍成功, 读取失败不阻断连接也不缓存;网关未就绪不得报 connected,网关/上游异常 退出转 reconnecting,端口全部占用直接 given-up;SSH 链路启用 zlib@openssh.com(置于 none 之前,协商不到静默降级);隧道层不再注入 cookie。 (cherry picked from commit b9331ee4c7188221e033ce7d560e0e880d8bb0df) * fix(remote): S3 出站网关的链路判死与状态守卫 (#10-05) - 网关 `upstream` 是逐请求错误通道(504 超时 / 502 不可达),不再一律拆除链路: 仅入口自身 stopped 或回环隧道口 ECONNREFUSED 才重连,其余记可读事件并保留 link - 补齐 `gateway.start` 之后的 generation 复查:断开不再被成功路径静默覆盖 - 内核 cookie 铸造失败事件接入事件环(S3 §8 的「记录一条可读事件」) - 端口耗尽改用类型化错误码,去掉对中文文案的脆弱耦合 - detach 原因统一脱敏;beginReconnect 合并 pending 回收 (cherry picked from commit cd0102cdd95c5aa158ca2a9d8d3215035fd1c16a) * feat(remote): S4 入站暴露 (#10-05) (cherry picked from commit 88d023d9f9f13659ee6f1aa55e6d1ffb189966ee) * feat(remote): S5 公网隧道与宿主端口发现 (#10-05 S5) - 新增 service/tunnel.ts 与 utils/cloudflared.ts:quick/具名两种模式、tunnel 网关入口、 进程生命周期与事件环、PATH→数据目录→官方源/ghfast.top 镜像的二进制解析与自检 - access.json 的 tunnel 段收敛为显式 schema(enabled/mode/token/hostname), GET /access 带出隧道运行期视图(完整分支含链接与二维码,脱敏分支去掉 token/链接/二维码) - 新增 POST|DELETE /api/desktop/dsh-tauri-remote/access/tunnel(loopbackOnly) - 本机上游端口优先取宿主 webserver.port,DSH_WEB_PORT 仅作回退,非桌面载体可用 - 脱敏分支保留凭据存在性(hasPassword/hasToken),仍不含任何凭据内容 - genapi 管道登记面板专用路由目录:pnpm genapi 不再把 /access* 写进两份生成客户端 - 补测试:tunnel 9 例、cloudflared 8 例、/access 端口耗尽 error 路径、宿主端口优先、tunnel 段解析 (cherry picked from commit 3115e88cdf88072884f327f1c0e11c4456a6cf96) * feat(remote): S6 入站访问面板与部署闭包修复 (#10-05 S6) - 面板新增「本机访问」标签页:监听地址/端口、认证与 scope、密码与链接 Token 管理、 完整/掩码链接与二维码、公网隧道(quick/具名)启停与事件尾 - client/service/access.ts 直连同源 /access* 路由并逐字段容错解析;store 新增 access 模块; 隧道启动期由既有 1.5s 轮询刷新,其余时刻靠操作回包更新 - shell 深链常量补 REMOTE_TAB_ACCESS,契约测试同步 tab 与 4 条 access 路由 - qrcode 改为构建期内联 + devDependency:S1/S4 引入的运行时依赖会让 build:plugins 的部署闭包校验在 pngjs/browser.js(browserify 产物)上失败,已复现并修复 - 测试:面板组件 6 例、数据层 4 例,路由表/apply/remote-section 契约用例同步 (cherry picked from commit 36e5d0729dcf86ab518f4a0eb6651e0e65dfa7bd) * test(remote): 入站暴露真实宿主 e2e 与出站 WebSocket 升级覆盖 (#10-05) - 新增 dsh-tauri-remote-access.e2e.ts:真实 dsh web 宿主(非桌面载体)上的 /access 契约—— 默认态、回环监听真转发 + 宿主 webserver 端口发现、链接 Token 轮换/作废 + 二维码、 隧道参数校验不落盘、非回环来源 403 - dsh-tauri-remote.e2e.ts:出站网关在升级请求上注入铸造出的会话 cookie 并透传 WebSocket, 回显一帧往返,堵上 S3 评审点名的「RPC 与对话流全跑在 WS 上却无升级用例」验证缺口 (cherry picked from commit 8a62a9a14b5a0092ed977150b84513d61debd02a) * style(remote): eslint --fix 收敛三元换行、类型导入与数字字面量大小写 (cherry picked from commit 65924d3c8a008f3e122fcee285b71dbfd8d2a9c3) * feat(remote): 面板实测修正——清除密码、隧道状态行常驻与真实浏览器走查 本机实测(真实 dsh web 宿主 + Chromium + 真实 cloudflared quick 隧道)发现并修掉三处: - 设了访问密码就无法清除:POST /access 的 password 支持 null(清除),面板补「清除密码」按钮 - cloudflared 启动日志把「公网隧道已就绪」挤出事件尾:状态行单独常驻,进程尾只留最近 4 条 - 监听地址下拉在 iframe 里的指针命中不稳:面板补 aria-label,用例改以 HTTP 路由布置前置状态 新增 test/e2e/plugins/dsh-tauri-remote-panel.e2e.ts:真实浏览器走查(面板渲染、回环链接、 带 Token 的链接与二维码、凭据开关回写、DSH_E2E_TUNNEL=1 时真实 quick 隧道启停),截图落 test/e2e/.artifacts/;access 用例收尾同时清认证与密码,共享宿主不再把凭据留给后续文件。 (cherry picked from commit bde7cd9913ba971fb9ef08844379a1c8a5934c6c) * fix(remote): 新基线上补齐面板来源门禁、前缀与生成配置 - 面板写入口恢复自建 loopbackOnly:宿主 guard 只看 socket 地址,而暴露/隧道请求都从回环网关转发进来, 仅靠 guard 会让公网来源直接改本机暴露配置(网关标识声明为公网的用例已覆盖) - 面板客户端与 e2e 的 API 前缀统一为 /api/tauri/remote,修掉面板 404 - genapi transform 增加 Remote 段,生成物与提交一致(pnpm genapi 幂等) - qrcode/@types/qrcode 进入 workspace catalog(新基线无 utils/types 条目) - 验证:pnpm typecheck、全量 unit 3690 通过、eslint 干净、插件 e2e 19 通过 * style(remote): 面板服务导入排序 * refactor(remote): 面板来源门禁改为入口中间件并补 L1 路由测试 - 按 plugin.host.md 的口径:共享安全检查统一在 server 入口 app.use,业务 handler 不再各自包一层 remoteSourceGuard 只拦写方法,读方法仍由 handler 返回脱敏状态 - 新增 server/panel.test.ts:真实注册 + HTTP 验证三条面板路径、方法集、回环/公网读写行为 - 验证:pnpm typecheck、插件单测 1031 通过、eslint 干净、插件 e2e 19 通过 * test(e2e): 入站暴露用例挂进共享宿主并在当前内核批次运行 - 共享宿主 DEFAULT_ALSO 增加 dsh-tauri-remote:两条新用例依赖插件真的被挂载 (CI 之前默认只挂 pet + DEFAULT_ALSO 列表,故 404 / 找不到「服务器」分区) - CI:legacy 内核(0.1.5-rc)排除这两条用例,与 extension-panel 同口径; 改由「Host and migrated plugin contracts」(当前内核)批次运行 - 提交 dsh-tauri-ui 生成的 Tailwind 产物:面板新增 utility 后产物漂移, CI 的 Verify generated Tailwind CSS is current 会拦 - 本地验证:当前内核批次 19 通过(1 跳过),全插件 project 除既有 dsh-tauri-ui 用例(当前内核下本就不适配)外全绿 * test(remote): 面板分区切换按 aria-selected 重试点击 CI 上 iframe 内的指针命中会偶发丢失(与监听地址下拉同一现象),一次性 click 会 30s 超时; 改为断言 aria-selected 并重试点击,仍以面板挂载为最终判据 * feat(remote): 隧道腿按公网主机名判同源,暴露默认带凭据,隧道启动显示阶段进度 - 网关:isCrossSiteRequest 接受 Origin/Referer 命中隧道公网主机名的请求 (Safari/旧浏览器不发 Sec-Fetch,此前一律 403,用户无法在隧道页面改密码); 入口新增 publicHost 提供者,跨站拦截记 source 事件,拒绝文案补可执行建议 - 暴露:开启暴露且完全没有凭据时自动生成 20 位随机强密码并开认证,明文只在响应里出现一次 - 隧道:启动前强制开认证(先做纯校验,参数不合法不改认证),响应可带一次性 generatedPassword - cloudflared:下载改流式并回报 check/download/extract/verify 进度与字节数; 隧道把进度写进状态与事件环(每 5% 或 400ms 节流),下载阶段带 url - 状态:链接一律生成二维码并标注来源(loopback/lan) - 验证:插件单测 1031 通过 * feat(remote): 「远程访问」面板重排——入口置顶、进度可见、一次性密码提示 - 面板:访问入口置顶(本机/局域网 + 公网两行,各带来源标签、地址、复制、二维码; 未监听或隧道未运行时给占位与「开启暴露 / 启动隧道」动作) - 面板:认证 → 监听 → 公网隧道 → 事件;监听区写明「转发到桌面端当前后端」 - 面板:自动生成密码用高亮 callout 展示一次(复制 / 知道了),轮询刷新不覆盖 - 面板:隧道启动按阶段显示进度文字,下载阶段带百分比进度条与镜像地址 - 文案:分区与标签改「远程访问 / 连接到 SSH 服务器 / 同步到远端」,分区 order 置顶(-1); Hero 与机器面板标题改远程访问口径 - 用例:面板单测补 4 条(入口行、占位动作、一次性密码、进度), e2e 更新为「回环也出二维码、开启暴露自动带认证、隧道强制认证并有进度」 - 产物:重跑 build:taiwindcss 提交新增 utility 后的 CSS - 验证:typecheck、eslint、全量单测 5058 通过、插件 e2e 19 通过 * fix(remote): 面板嵌套层改用已注册的 layer-3 令牌 插件 Tailwind 主题只登记 layer-1 / layer-3(tailwind.plugins.config.js), 写 bg-layer-2 会静默无产出:入口行与自动密码提示会没有背景。 只改令牌,产物回到生成器精确输出(CI 的 Tailwind 漂移检查通过)。 * fix(remote): 落实评审——摘要校验、凭据不入 argv、损坏档案守卫、清理顺序、并发启停 按 SkanderBog 的故障注入清单逐条修: - cloudflared 下载:先取 GitHub Release API 的官方 sha256 与体积,边下边算摘要、 比对通过才解压执行;镜像只兜底传输,校验值始终来自官方;缺失摘要或校验失败一律拒绝 (可用 CLOUDFLARED_VERSION 固定版本) - 具名隧道凭据改走 TUNNEL_TOKEN 环境变量,不再出现在进程参数里(ps 可见) - access.json 损坏时 apply/ensureAuth/rotate/revoke 全部拒绝写入并给出可读原因, 不再用默认文档覆盖原文件 - 插件退出:清理串行化,先停各服务、回收子进程,最后才清运行时依赖 (此前 void dispose + 立即 clearHostRuntime 会让在途清理读不到 deps) - 隧道启停串行化(single-flight 队列)+ 激活世代守卫:并发 start/stop 不再各起一个进程, 被取代的激活会立刻回收刚起的 cloudflared - 拿到域名后落盘失败不再从子进程日志回调抛错,转为可读失败状态 - 用例:摘要不匹配拒绝执行、凭据不入 argv、损坏档案拒绝写、清理顺序、并发启停、落盘失败 * test(remote): 面板草稿保留与 e2e 前置复位(评审复现的两处乱序失败) - 面板:密码/端口写失败时保留用户草稿,不再静默清空或回退旧值 - e2e:每个用例前置把暴露与监听地址复位到确定值;档案直读容忍文件尚未创建 * fix(remote): 分区顺序退回 50,并给移动端设置用例补详情落定等待 - 分区 order 不能取 -1:移动端设置弹窗以「首个分区」为首屏详情页,语言切换项在通用设置里, 我们的分区排最前会让移动端首屏落到远程访问(dsh-tauri-mobile-settings 契约失败)。 改为 50:仍在插件分区里最靠前,但不越过核心分区。 - 上游 mobile-settings 用例在分类列表因新增分区回流后立刻点语言项会踩竞态 (超时 30s):等详情标题落到「通用设置」再操作。本地连跑 4 次(含 shuffle seed 201)稳定通过。 * fix(remote): 落实评审——壳分享弹层、设置三卡、短期配对凭据与共享路由 - 壳:右上新增「远程」分享弹层(局域网/公网隧道、短期二维码与配对码、Bridge Mobile 占位),齿轮直达本机「远程访问」设置页 - 设置:远程访问页重排为局域网/公网隧道/访问控制三张紧凑分组卡,草稿取消/保存、日志按需展开;SSH 开关移入机器 Tab;删除整页 Hero 与全局禁用按钮 - 宿主:临时配对码 5 分钟轮换,分享链接与二维码只带短期凭据,?auth= 同时接受配对码 - 路由:入站分享 4 条路由提升到管理表,同步 rust 白名单、契约测试与两份生成客户端;完整凭据管理仍只在面板表 - 机器人意见:apply 清理失败不再吞异常、隧道 stop 保留失败原因、tunnel.test 还原 DSH_WEB_PORT、README/manifest 更名、面板 e2e 按 API 收尾 * chore(ui): 同步合并后的 Tailwind 产物 面板二维码由 132px 收窄为 96px、新增 max-w-[320px],重新生成的合并产物与源码一致。 * fix(ui): 壳分享弹层不再覆盖 HeroUI 主题圆角,面板 e2e 复位与永久码断言对齐 壳层主题测试禁止在 HeroUI 组件上使用 rounded 工具类;面板 e2e 收尾改为完整复位(关认证、清密码、撤凭据、停隧道),永久码断言改为 base64url 43 位。 * fix(remote): 短期配对凭据不再依赖局域网监听 - 宿主:pairing 改为按入口独立构造(access.utils.ts pairingLinksOf), 只开公网隧道、不开局域网时也能拿到短期隧道链接与二维码; RemoteAccessPairing.link/qr 变为可选,客户端解析器同步放宽。 - 壳分享弹层:动作返回的一次性访问密码明文在弹层内保留,可复制、可关闭; 轮询与动作分开计数,动作进行中暂停轮询,按钮不再可能永久锁死。 - 面板配对行回退到隧道短期链接/二维码,不再引用永久凭据。 * fix(remote): 对齐分享菜单与配置页并接通一次性配对 * fix(remote): 阻止远程访问管理页面与接口 * fix(remote): 设置入口改用网络图标 * test(ui): 等待迁移弹层销毁后再结束用例 --------- Co-authored-by: hairyf <wwu710632@gmail.com> | 7 天前 | |
docs(agents): 增加沟通与评论规范 (#953) 回复用户与在 issue/PR 下留言时:默认三五句说完、不复述过程;不要 AI 套话与 emoji 排版; 同一话题只留一条(写长了原地 PATCH,不补发);推荐方案必须给可复制的命令并标明限制。 | 13 天前 | |
fix(license): make license identifiable by GitHub Replace modified MIT text in LICENSE with the standard MIT text and move the non-commercial condition to LICENSE.details, so GitHub's license detection (and the shields.io badge) resolves to MIT instead of 'not identifiable'. | 2 个月前 | |
chore: update change readme | 1 个月前 | |
refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 (#1155) * refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 * fix(ui): 同步删除 kernel 后重新生成的 Tailwind 产物 | 20 小时前 | |
refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 (#1155) * refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 * fix(ui): 同步删除 kernel 后重新生成的 Tailwind 产物 | 20 小时前 | |
refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 (#1155) * refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 * fix(ui): 同步删除 kernel 后重新生成的 Tailwind 产物 | 20 小时前 | |
chore(core): 升级推荐内核到 0.2.1-rc.2 (#1123) * chore(core): upgrade recommended baseline to dsh 0.2.1-rc.2 * test(core): align native and local recommendation assertions with rc2 * test(desktop): assert rc2 cold assembly and isolated CLI admission | 3 天前 | |
feat(native): let Android tablets rotate while phones stay portrait (#1159) Expo's `orientation: "portrait"` is shared by iOS and Android, and Android has no tablet-only key like iPad's `UISupportedInterfaceOrientations~ipad`, so every Android device was locked to portrait. Android 16+ already ignores that lock on large screens for apps targeting API 36, but tablets on Android 15 and older stayed portrait and letterboxed. At prebuild, app.config.js now sets MainActivity's manifest orientation to fullUser and locks portrait again in onCreate and onConfigurationChanged when smallestScreenWidthDp is below 600, the same line Android 16 uses. Phones and folded foldables stay portrait; tablets and unfolded foldables follow the system rotation setting. iOS is unchanged, and prebuild fails if MainActivity no longer matches the Expo template. Expo 的 `orientation: "portrait"` 由 iOS / Android 共用,Android 也没有 iPad 那样的平板专用键,所以所有 Android 设备都锁竖屏。Android 16+ 对 targetSdk 36 的应用在大屏上本就忽略这把锁,但 Android 15 及更早的平板仍锁竖屏并加黑边。现在 prebuild 时 app.config.js 把 MainActivity 的清单方向改为 fullUser,并在 onCreate / onConfigurationChanged 里按 smallestScreenWidthDp < 600(与 Android 16 同一条线)锁回竖屏:手机与折叠屏外屏保持竖屏,平板与展开的折叠屏随系统旋转 设置。iOS 不变;MainActivity 不再匹配 Expo 模板时 prebuild 直接报错。 | 16 小时前 | |
fix(native): stop iPadOS window controls from covering the page in windowed mode (#1164) iPadOS 26 窗口模式下,左上角的窗口控件会盖住页面,侧栏按钮点不到。现在 ios.infoPlist 设 UIDesignRequiresCompatibility,让系统像对旧版应用那样在顶部 为控件留一条窄带,内容随之落到控件下方。代价是系统原生控件保持 iOS 26 之前 的外观。这是临时方案:Xcode 27 构建会忽略该键,后续应升级到 Expo SDK 58, 在 SceneDelegate 中返回 .minimal 窗口控件样式。 In windowed mode on iPadOS 26, the window controls in the top-left corner cover the page, so the sidebar toggle can't be tapped. Setting UIDesignRequiresCompatibility in ios.infoPlist makes the system reserve a thin band at the top for the controls, as it does for older apps, so content moves below them. The trade-off is that native system UI keeps the pre-iOS 26 look. This is a stopgap: Xcode 27 builds ignore the key, so the follow-up is upgrading to Expo SDK 58 and returning the .minimal windowing control style from its SceneDelegate. | 10 小时前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 (#1155) * refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 * fix(ui): 同步删除 kernel 后重新生成的 Tailwind 产物 | 20 小时前 | |
chore: display app name as Deepseek Harness Desktop; bump to v0.1.2 | 2 个月前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
fix(desktop): restore main window hide and release v0.24.5 (#1148) | 2 天前 | |
Revert "Merge pull request #355 from dsh-tauri-desk/revert/pr-319-pet" This reverts commit ba75694f754669b361016f58008cbd74b28e3ca1, reversing changes made to 03881b676ad27356deb766b6ba31a9d71377945b. | 1 个月前 | |
refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 (#1155) * refactor(plugins): 删除 dsh-tauri-kernel 并列入废弃名单 * fix(ui): 同步删除 kernel 后重新生成的 Tailwind 产物 | 20 小时前 | |
fix(pet): 修复 Codex 宠物拖动始终朝右 (#1140) * fix(pet): respect Codex drag direction over bubble animations * test(pet): align tooling contract and scope drag precedence to Codex | 2 天前 | |
chore(lint): add ESLint with antfu config and apply fixes Introduce ESLint with @antfu/eslint-config and React support (@eslint-react/eslint-plugin, eslint-plugin-react-refresh). Add lint / lint:fix scripts, tsconfig lint flags, and apply the auto-fixes across the codebase (single quotes, no semicolons, import order, yml/toml formatting). | 2 个月前 | |
Implement code changes to enhance functionality and improve performance | 7 天前 | |
feat(appearance): add local scale and backdrop blur (#924) * feat(appearance): add local scale and backdrop blur * test(appearance): cover blur in persisted settings * feat(appearance): apply native backdrop effects * fix(appearance): keep the shell bar and the sidebar at the same opacity The embedded sidebar painted its translucent fill on `body`, whose background propagates to the root canvas; the `backdrop-filter` on that same element then pulled the propagated fill into its backdrop, so the fill was composited twice (effective 1-(1-α)²) while the shell bar composited it once. Under transparency + blur the bar therefore always read darker than the sidebar (about 53 levels at opacity 60, 22 at 28), and the sidebar needed a manual α' = 1-√(1-α) per opacity. - drop the backdrop filter from the sidebar fill so it composites once - share `appearanceSidebarFill` between both halves: canvas@α while translucent, panel while opaque, so the bar and the sidebar cannot drift apart again * fix(appearance): make backdrop effects reliable * feat(appearance): enhance sidebar background handling and add blur effect * feat(appearance): keep startup and boot screens as translucent as the shell 启动页(HARNESS + Loading plugins…)原本铺满不透明底色,只有 navbar 跟着透明度走, 窗口背后因此只透出一半。现在启动页、预装引导与全屏恢复页统一使用 --color-startup, 与 navbar / 侧边栏取同一层 alpha;关闭透明度时全部回到原实心底色。 内嵌 dsh 的 boot 页由内核在插件加载之前绘出,插件来不及生效,因此改在 document-start 注入一个只做外观收发的小接收器(initialization_script_for_all_frames): - 帧内请求 boot CSS,宿主用 shared/appearance 的唯一投影来源生成后下发; - 样式只写独立 head style(#root > [data-dsh-boot] 结构性选择器), 不给 boot 节点注入 inline style,避免 BootHandoff 的 hydration 不匹配; - alpha 只画一层(boot 节点铺底时 body 保持透明),避免 0.7×0.7≈0.91 的叠色; - 帧内请求/确认带 40ms 起步、上限 400ms、最多 12 次的有界退避重试,只接受 event.source === window.parent 且 source === 'dsh-desktop' 的宿主消息; - 宿主与内嵌服务跨源,回发一律用 '*';确认到达前 iframe 保持隐蔽(同构的壳层 Loadable 顶上),并有 4s 兜底,不会长期挡住建。 透明度关闭时下发空 boot CSS,帧内据此清掉上一轮样式。 * fix(appearance): drop boot styles when the frame document leaves 帧内导航离开时旧 boot 样式会短暂作用在新文档上。现在帧内 pagehide 立即摘掉 style 并 作废确认(新文档自己重新请求,bfcache 还原走 pageshow);宿主侧复用既有的 dsh://plugin-boot:leaving 信号重新隐蔽 iframe 并重挂 4s 兜底。 * refactor(appearance): drop the redundant layers around the boot handshake 帧内接收器不再做 12 次指数退避:宿主在 iframe 挂载时必定重推同一份 CSS,宿主侧 4s 兜底仍在,重启重试只是和同一份投影赛跑。 宿主侧模块级 boot 投影单例(含 ack/TTL 与全仓零调用的 bootAppearancePending)整体删除:hook 与 iframe 各自已算同一份 CSS,单例只在两者之间转手。 appearance.rs 的源码子串断言测试删除,覆盖由真正执行脚本的 test/appearance-boot.test.ts 负责。 单调用点 helper 内联(decode_clamped_u8 / normalizeInteger / appearanceBackdropFilter);半透明判定抽成 appearanceTranslucent 单一 SSOT(原 6 处重复表达式)。 删除死键与死 token:ui.zoom、messages.zoom_save_failed、--color-load-bg 与 tailwind 的 load-bg 别名。 builder.rs 安全测试去掉自证断言,保留 default.json 不得含 set-effects 的跨文件负向断言;压缩注释。 * feat(appearance): apply the transparent-mode defaults when native transparency is enabled 开启「原生透明」时一并给出初始组合:内容区保持不透明、背景 80%、开启模糊。 e2e 相应改为在开启后断言这套组合,并改用内容区不透明开关验证开关持久化(模糊此时已由默认值开启,再点会把它关掉)。 * fix(e2e): 原生透明默认 80% 后滑块左移应断言 79% b176f6cd 让开启原生透明时一并写入 opacity 80,同一测试里紧邻的断言 也据此校验了 80;但滑块左移的期望值仍是旧默认 100 之下的 99,导致 Desktop E2E 在等待 99 时超时。改为 79 与默认值保持一致。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> | 14 天前 | |
refactor(ui): 整合Tailwind CSS插件至dsh-tauri-ui包,移除独立插件 删除原dsh-tauri-taiwindcss独立插件包的全部文件,将Tailwind构建逻辑与样式能力整合进dsh-tauri-ui包: 1. 更新根目录构建脚本路径,调整依赖配置与应用manifest配置 2. 修改eslint、tailwind配置文件的扫描与排除规则 3. 重构UI包的样式注册逻辑,集成Tailwind产物挂载功能 4. 清理冗余的cssr样式文件,改用tailwind工具类实现组件内联样式 | 22 天前 | |
refactor(tooling): share compatible scanners and configuration (#824) | 19 天前 | |
refactor(plugins): 全面迁移服务与 API 生成为 dsh-h3 (#978) * refactor(plugins): migrate servers and API generation to dsh-h3 * test(host): verify embedded gate retains native route fence * docs(specs): align plugin contracts with dsh-h3 servers * refactor(host): use native context generics from dsh-h3 0.2.1 * refactor: move plugin APIs to /api/tauri namespace * fix: compare complete request origins using socket protocol | 11 天前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
refactor(plugins): 全面迁移服务与 API 生成为 dsh-h3 (#978) * refactor(plugins): migrate servers and API generation to dsh-h3 * test(host): verify embedded gate retains native route fence * docs(specs): align plugin contracts with dsh-h3 servers * refactor(host): use native context generics from dsh-h3 0.2.1 * refactor: move plugin APIs to /api/tauri namespace * fix: compare complete request origins using socket protocol | 11 天前 | |
refactor(tooling): share compatible scanners and configuration (#824) | 19 天前 | |
refactor(tooling): share compatible scanners and configuration (#824) | 19 天前 | |
test(e2e): global-setup 改名 setup-plugin,新增桌面车道 setup-desktop test/e2e/global-setup.ts -> test/e2e/setup-plugin.ts(plugin project 的 globalSetup,vitest.plugin.config.ts 同步);新增 test/e2e/setup-desktop.ts 作为 desktop project 的 globalSetup,整条车道只做一次车道级前置:清理遗留 scratch 根 + 校验 debug 二进制与 WebDriver/debug 端口,避免每个用例文件重复报同一个错;应用实例仍由各文件 startDesktopApp() 起(fileParallelism: false 串行)。\n\ndocs/specs/plugin.test.md 的目录树与编排说明改指 setup-plugin.ts;desktop.test.md §2 补「车道入口」行说明 setup-desktop.ts。\n\n验收(TAURI_WEBDRIVER_PORT=14445):tsc --noEmit 0 error;plugin 11 files / 92 tests;desktop 2 files / 5 tests。 | 29 天前 | |
feat(native): 合入移动端子工程并接入 Android / IOS 构建/发版流水线 (#875) * feat(native): 合入 DSH Bridge 移动端子工程并接入 Android 构建/发版流水线 - 新增 native 子工程 `src-native/`(Expo 57 / React Native 0.86 / expo-router):在局域网 发现 DSH 主机并以全屏 WebView 承载 Harness 网页。相对原移动端仓库做了这些重构: store 改用 `valtio-define` 的 persist 插件 + AsyncStorage(磁盘载荷与旧版逐字节兼容, 适配器内做归一化与去重),连接令牌仍只存 `expo-secure-store`;文案改由 i18next 承担 (非 React 模块走 `i18n.t()`);`bridge-client` 拆为 `services/probe-bridge.ts` 与 `services/scan-candidates.ts`;网络访问收敛到 `apis/`;健康轮询改由 React Query 负责 (`focusManager` 接 RN `AppState`)。 - 新增根配置 `app.json`、`app.config.js`、`metro.config.js`、`tsconfig.native.json`、 `vitest.native.config.ts`、`assets/native/`、`scripts/native-app-version.mjs` 与 native scripts;依赖集中在 `pnpm-workspace.yaml` 的 `catalogs.native`(React 复用共享 `catalog:runtime`,不引入第二份 React)。 - 三个 workflow 职责分离:`build-android-test.yml`(PR 校验,产出 debug APK 工件)、 `build-android.yml`(main 常规构建)、`release-native.yml`(桌面端 `Build & Release` 完成后 workflow_run 触发发版)。发布资产名固定为 `Deepseek.Harness.Android_<version>.apk`,版本从 `app.json` 读取并与 `package.json` 断言 相等;release job 只上传资产、从不创建或编辑 Release;iOS 不发布。 - `bump.config.ts` 增 `execute: node scripts/native-app-version.mjs`:发版时同步 `app.json` 的 `expo.version`,并让 `android.versionCode` 在版本变化时严格递增(幂等,版本未变不动)。 - 文档:新增 `docs/specs/native.md`(目录结构、启动与连接、状态分层、持久化与凭据、 测试约定、构建与发版),README 增「移动端支持」小节。 验证:native 16 files / 550 tests 全通过;native 与桌面端 `tsc` 均 0 错误;全仓 eslint 0 error(16 条既有 warning 均在未改动的 `packages/**`);`pnpm install --frozen-lockfile` 通过;`pnpm build:native` 成功(4189 modules,产物中检索不到任何桌面端代码标记)。 * fix(native): Android 测试构建改为仅手动触发,bumpp 经 patch 同步 app.json - build-android-test.yml: name 改为 Build Android Test,删除 pull_request 触发,只保留 workflow_dispatch(对齐 build-test.yml) - bump.config.ts: 去掉 all + execute,把 app.json 列入 files - patches/bumpp.patch: 新增 app.json 分支,写 expo.version 并把 android.versionCode 递增 1 - 删除仅被 execute 使用的 scripts/native-app-version.mjs(.d.mts) - app.json 同步到 0.21.2 / versionCode 21001(原 0.21.0 与 package.json 漂移,app-config 测试失败) - docs/specs/native.md 同步 workflow 与 bump 描述 * fix(native): 读盘失败时不得用空快照覆盖连接历史 (#896) `connectionStorage` 的 `getItem` 用一个 catch 同时处理两类失败: - `AsyncStorage.getItem` 抛错(数据库被锁、I/O 错误、Android CursorWindow 上限); - 已读到内容但 `JSON.parse`/归一化失败(负载损坏)。 两者都返回 `null` 且不更新 `persistedPayload`,于是 `markHydrated` 之后 persist 插件用默认空状态调用 `setItem`,与 `persistedPayload`(仍为 null)不等 → 把 `{"version":1,"history":[],"guidedHosts":[]}` 写回磁盘。一次临时读失败就会永久 清空所有已保存主机,且这些主机的 SecureStore 令牌因不再被 history 追踪而成为 孤儿、无法恢复。负载损坏时覆盖是合理的恢复策略,读失败时覆盖是数据丢失。 改为区分两类失败:读失败置 `readFailed`,本次会话 `setItem` 直接返回;负载损坏 仍按原路径覆盖。`resetConnectionStorageCache()` 一并复位该标记,保证测试隔离。 更新 `storage.test.ts`:原用例断言 `native.write` 会写入空负载(把这个 bug 固化 成了契约),改为断言不再写盘;新增用例覆盖「读失败后不写盘,读成功后可恢复写入」。 变异验证:去掉 `if (readFailed) return` 守卫后,两条用例均变红。 Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): serialize connection history writes before deduplication (#899) Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * ci(native): 为 native 和 main 的 PR 独立运行类型检查与单测 (#897) * ci: 把 src-native 的类型检查与单测接入 PR CI `build-android-test.yml` 改为仅 `workflow_dispatch` 后,移动端验证只在手动运行或 合并后的 main 构建里执行;而 `ci.yml` 的 typecheck 用桌面 `tsconfig.json` (`@/* -> src/*`),`test:unit` 也只跑 `--project unit`(include 不含 `src-native`), 两者都不覆盖子工程。结果是移动端的类型错误或测试回归可以伴随全绿 PR 检查合入。 在 `unit` 任务里补两条轻量步骤(放在现有类型检查之后、`Lint` 之前): - `pnpm run typecheck:native` → `tsc --project tsconfig.native.json --noEmit` - `pnpm run test:native` → `vitest run --config vitest.native.config.ts` `tsconfig.native.json` 与 `vitest.native.config.ts` 都是自洽的(前者 extends `expo/tsconfig.base`、别名指到 `src-native`;后者 environment: node、alias `@` → `src-native`),不依赖 workspace 包构建产物,所以不占用「Build Plugins」之后的顺序, 放在类型检查段即可。 验证(本地实测,非静态推断): - 两条命令原样跑通:typecheck 0 错误;native 单测 16 files / 551 passed。 - **变异验证 1**:给 `connection/index.ts` 注入一个真实类型错误 (`const typecheckProbe: number = 'not-a-number'`)→ `tsc` 退出 2 并报 `error TS2322`;还原后退出 0。 - **变异验证 2**:把 `app-config.test.ts` 里的包名断言改错 → vitest 退出 1、该用例变红; 还原后通过。 未跑 Android/gradle 构建——本机没有 Android SDK,编译仍以 `build-android-test.yml` 为真实证据;本 PR 只补类型与单测门禁。 * ci(native): run isolated checks for native and main pull requests * docs(ci): clarify same-ref Android build cancellation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * fix(native): extend iOS webview to the bottom edge (#902) * chore(native): 同步 main 到 native(解 README 冲突、对齐 app.json 版本、补齐三语 README) (#910) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 (#884) * fix(update): 下载安装包期间隐藏更新对话框底部按钮 下载中底部「稍后」「立即更新」两个按钮点击无意义(主按钮只会阻塞等待 同一次在途下载),隐藏后仅保留进度条与右上角关闭按钮。 * test(update): 覆盖下载期间隐藏底部按钮的契约 * fix(pet): end native window drag on left-button release (#885) 桌宠拖动松手后拖拽态要多挂一拍,而 dsh-pet-component 的网页案例(纯指针事件) 没有这个问题。 根因:原生窗口拖拽跑在系统模态循环里,按钮事件被它吞掉。tao 0.35.3 的 handle_os_dragging(src/platform_impl/windows/window.rs:529)在 ReleaseCapture 后 PostMessageW(WM_NCLBUTTONDOWN, HTCAPTION) 就返回,startDragging 的 Promise 瞬间 resolve,也拿不到结束时刻。webview 在整个拖拽期间与松手瞬间都收不到 pointerup / pointercancel(src/hooks/use-window-draggable.ts 的既有注释已记录这一点),前端 只能靠「Moved 事件停歇」收尾:DRAG_SESSION_TIMEOUT = 1500 让 dragging 多挂 1.5s (桌宠的 squash 与拖动动画都挂在 dragging 上),方向态多挂 DRAG_DIRECTION_IDLE_TIMEOUT = 350ms。 全局鼠标设备流本来就是 OS 侧观察者,能看到真正的松开时刻,因此让它顺带上报左键 状态: - src-tauri/src/desktop/pet_mouse.rs:新增 device-mouse-button 事件与 MouseButtonState { pressed };坐标与按键在 MouseStateStore 里分开存放,节流 线程各自去重(should_emit),接收窗口换代(revision)时强制补发一次。 Windows 在既有 8ms GetCursorPos 轮询里加 GetAsyncKeyState(VK_LBUTTON) 的最高位 (此刻按着,而不是「自上次查询以来按过」);macOS 在 CGEventTap 的 MACOS_MOUSE_EVENTS 里加 LeftMouseDown/LeftMouseUp;Linux 走 rdev 的 ButtonPress/ButtonRelease(Left)。 - src-tauri/Cargo.toml:windows-sys 打开 Win32_UI_Input_KeyboardAndMouse。 - src/hooks/use-window-draggable.ts:订阅 device-mouse-button,松开即 endDrag(), 1.5s 停歇降级为事件流失联时的兜底;hook 自己幂等拉起 start_pet_mouse_stream, 不依赖兄弟 hook 的挂载顺序。 验证:vitest run --project unit src/hooks/use-window-draggable.test.ts → 5 passed (新增用例覆盖「松开即收尾 / 仍按下不收尾 / 设备流失联退回停歇兜底 / 重新进入拖拽 / 挂载时拉起设备流」;把事件名改错后 3 个用例变红);cargo test --lib → 837 passed (含新增 should_emit 与 MouseStateStore 用例);tsc、eslint 对改动文件 0 报错。 * fix(startup): 回环探测不再白等 2s,并记录启动耗时与慢启动归因 (#886) * fix(startup): 健康探测对未监听的回环端口快速失败 TUN 式网络过滤器(Clash/Mihomo TUN、Fake-IP)会把**失败**的回环 connect 拖到固定 约 2s(实测 2014-2067ms,curl/.NET/裸 winsock 一致),而探测客户端只设了响应级 timeout:端口尚未监听时每一轮探测都要白等 2s,前端就绪轮询因此被拉到每 ~3s 才推进 一步(`[Harness] health check failed ... elapsed_ms=2019..2046`)。 回环探测客户端单独加 connect_timeout(LOOPBACK_CONNECT_TIMEOUT=250ms)。本机实测 同一个 closed-port 探测:修复前 2052.94ms -> 修复后 260.08ms。 带 connect 上限后,建连阶段失败会同时带上 reqwest 的 timeout=true(原先的 timeout=false 断言只在没有过滤器的机器上成立),用例改为继续断言 connect=true, 并新增 boot_probe_gives_up_on_closed_port_without_os_connect_timeout 锁定 「未监听的端口必须在 1s 内失败」。 * feat(startup): 记录 spawn 到就绪的耗时并在慢启动时归因 startup.rs 记录 spawn、首个 HTTP 响应与客户端模块就绪三个时刻:STARTUP_HTTP 给出子进程开始应答的耗时,STARTUP_READY 给出 UI 可用的耗时,超过 SLOW_STARTUP_THRESHOLD(15s) 时以 STARTUP_SLOW 拆出 spawn→HTTP 与 HTTP→就绪两段,便于下次启动变慢时直接定位是哪一段。 * fix(startup): 修正 reqwest timeout 语义描述并消除观测竞态 reqwest 的 .timeout() 是从建连开始到响应体读完的整体截止时间,不是响应读取超时,两处注释按实际语义重写;note_http_answer 改用 HTTP_AT_MS 的 compare_exchange 记录,避免先置标志后写时间戳时被 note_client_modules_ready 读到 0 而丢失 spawn→HTTP 拆分。 * feat(pet): fling the pet window after a fast drag release (#887) dsh-pet-component 0.2.3 把甩动物理留给宿主(组件 `docs/spec/pet-interactions.md` 的职责划分: 宿主管指针会话、轨迹估速、弹簧、飞行积分、边界与落地判定)。本仓此前对 fling/bounce/physics/squash 零实现,拖拽松手只会把窗口停住。这里补上宿主侧实现: - `src/pet/hooks/use-pet-physics.helpers.ts`:逐条移植组件 playground 参考实现 (`estimateReleaseVelocity` / `bodyBounds` / `stepThrow`)—— 150ms 回看窗、3600 px/s 指数软限速、500 px/s 死区、`feet = dsh ? height*330/360 : body.bottom - y` 贴地锚点、 单帧步长上限 50ms、落地帧才触发挤压且 `impactSpeed` 取积分前的下坠速度。 - `src/pet/hooks/use-pet-physics.ts`:新增 `usePetPhysics(pet, kind)`。原生拖拽跑在系统 模态循环里、webview 收不到 pointermove,因此轨迹采窗口 `Moved`(物理像素 / `devicePixelRatio`),松手时刻用 #885 引入的 `device-mouse-button` (`src-tauri/src/desktop/pet_mouse.rs`);松手先以 `throwPower = 1` 判断这一下要不要甩, 组件回吐的 `PetPhysicsEvent` 才带着合并后的 `physics`(prop > 配置 > 默认),所以在 `onFling` 里把 vx/vy 乘 `throwPower` 后起飞行。飞行每帧 `setPosition`,舞台取 `currentMonitor().workArea`,宠物本体用窗口边界对齐 `bodyBounds`;`landed` 时 `pet.squash(impactSpeed)`,`atRest` 后停帧,抓取(`pressed`)即刹车。 - `src/pet/app.tsx`:接线 `const onFling = usePetPhysics(pet, source?.kind)` 与 `<Pet onFling={onFling} />`。 单宠物窗口没有宠物间碰撞,`physics.petCollision` 与 `pet.bounce` 不适用;`physics` prop 未传,按组件默认值/配置合并值执行。测试:`use-pet-physics.helpers.test.ts` 22 例覆盖估速、 边界与步进分支;`use-pet-physics.test.ts` 用 jsdom + 假 timers 驱动 rAF,覆盖 Moved 采轨、 松手甩出、落地挤压、抓取刹车与「窗口没动就不甩」。 基于 #885(`fix/pet-drag-release-latency`)叠加:#885 提供 OS 侧的左键松开时刻与桌面端 拖拽会话,本 PR 只加甩动。 * fix(windows): 避免缓存失败导致静默退出并提示低完整性启动 (#892) * test: reproduce HTTP cookie cache startup failure * fix: diagnose restricted startup and remove unused HTTP cookie storage * test: embed Common Controls manifest for Windows harnesses * test: verify native startup dialog under real Low integrity --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * test(macos): cover idle titlebar sync enumeration (#888) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE (#894) * fix(startup): 孤儿清扫穿过 cmd.exe 转发层,消除首次启动 EADDRINUSE nvmd 等 Node 版本管理器放在 PATH 上的 node.exe 是垫片:垫片作为父进程去跑 `cmd.exe /C node <入口>`,真正的服务是孙子进程。垫片退出后(实测启动约 64s 后 `Owned Harness process <pid> exited with code 1`,无 stderr)真身仍占着 3080, 而旧谓词只在「父进程已退出或父进程创建时间晚于子进程」时才判为孤儿 —— 此时真身 的父进程 `cmd.exe` 还活着,于是漏杀。下一次启动直接 EADDRINUSE:webserver 是 必需插件,`startup failed: 2 required plugins did not activate`,harness 以 1 退出,前端 `HARNESS_NOT_OWNED` 快速失败;用户点重启时 cmd.exe 已经消失,清扫 命中,才进得来。2026-09-28 至 10-04 期间该形态在 desktop.log 中复现 12 次。 - `src-tauri/src/service/workflow/process.rs`:枚举脚本改为上报候选进程的整条祖先链 (pid/name/created/commandLine,深度上限 8),判定改由 `orphan_harness_pids` 完成:从服务本体向上穿过所有 `cmd.exe /C node <本应用入口>` 转发层,若顶层之上 还活着任何非转发层进程(另一个桌面实例的垫片、用户自己的 shell)就不清扫;清扫 目标是转发链最顶层,`taskkill /T` 会连带结束转发层与服务本体。 - 祖先创建时间晚于后代(PID 已复用、父子关系失效)时按子进程本身清扫,与旧谓词 的 `parent.CreationDate > child.CreationDate` 分支一致。 - 回归用例 `orphan_service_is_swept_through_live_relay_ancestors` 在真实 `cmd.exe /C node` 转发链上锁定「垫片已退出的真身必须被结束」,修复前失败 (等待进程结束超时);`orphan_selection_sweeps_through_relay_ancestors` 覆盖 仍被活垫片持有、真身直挂实例、用户 shell 三种不得清扫的形态。 * fix(startup): 转发层只认 /C 转交,祖先链截断时放弃清扫 代码评审指出的两处误判都收敛到「什么样的祖先才算转发层」和「祖先链是否完整」: - is_relay_ancestor 原先只看 cmd.exe + 命令行含 --no-open。用户自己开着 cmd.exe /K 交互 shell 跑同一个入口时同样满足,而那是服务的宿主而不是转发 层,清扫会连带关掉用户的窗口。改为同时要求命令行是 `cmd.exe /C node ...` 这种一次性转交形式。 - PowerShell 枚举脚本最多上溯 8 层,被截断时原先不区分「链到此为止」和「还有 父进程但没继续查」,调用方会把最后一条转发层当成链顶清扫,而上方的持有者被 漏掉。现在输出 ancestorsTruncated,截断的条目直接跳过。 * docs(startup): 说明 Unix 孤儿清扫与转发层的关系并补测试 * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) (#889) * feat(data-dir): 数据目录可迁移、可回滚,安装期即可选择 (#871) 正式版数据目录由用户级 DSH_HOME 决定(未设置时 ~/.dsh),此前只能装完后手工搬目录、 改环境变量,既没有副本校验也没有退路。现在安装期、设置面板都能改,且随时可再改: - 安装器新增「数据存放目录」页:预填注册表与环境变量里的现值,都为空则 ~/.dsh; 静默、被动与更新安装跳过;值与旧值不同才写 HKCU\Environment 并广播 WM_SETTINGCHANGE, 取默认目录时改为删除该变量。 - 设置 → 数据目录:预检(文件数、字节数、目标可用空间,需要空间留 5% 余量)、开始迁移、 回滚到任一备份,备份按会话数再按时间排序,无会话的备份会明确警告。 - 迁移顺序保证任何一步失败都不丢数据:先按文件数与总字节数校验副本,再把旧目录改名成 .moved-<时间戳>(永不删除),然后才改写 DSH_HOME 并 set_var;写环境变量失败会把旧目录 改回来。 - 回滚把备份目录搬回原位;当前目录非空则改名成 .current-<时间戳> 保留,空目录才删。 原位置就是默认目录时清除环境变量,不留指向默认目录的冗余值。 - 迁移前必须确认 Harness 已退出(pid 文件 + 进程存活双重判断),否则拒绝执行;迁移与 回滚都在核心转换锁 + 插件操作锁下先停服务再执行。 - 用户级环境变量读写收敛到 service::cli 的唯一写点,PATH 相关实现改为其薄封装。 - debug 构建固定 ~/.dsh.dev 且忽略 DSH_HOME,面板据此隐藏操作并说明原因。 - 安装器模板改存为带 BOM 的 UTF-8:NSIS 3.x 编译中文 LangString 的必要条件。 测试:data_dir 模块 23 条 Rust 单测(含迁移顺序、备份排序、跨盘回滚)、24 条契约测试 (面板/桥接/安装器/双语对齐)、13 条面板单测。 * fix(installer): 数据目录页预填优先取注册表持久值 (#871) 安装器进程继承的 DSH_HOME 可能比用户最后一次修改更旧;原实现让继承值优先,用户直接点下一步就会把陈旧路径写回。改为持久值优先、继承值仅作回落,并补契约测试。 * docs(readme): 功能列表补充数据目录说明 (#871) 安装期选择与设置内迁移、回滚在 README 三语功能列表中可见,避免新能力只存在于开发文档。 * fix(data-dir): 数据目录暂不存在时占用空间区走空态而非报错 (#871) 迁移刚把旧目录改名搬走、Harness 还没来得及重建它,或者安装器把 DSH_HOME 指向 一个尚未创建的目录时,list_data_dir_entries 会以 ENOENT 失败,面板的「占用空间」 区于是显示一片红色 DATA_DIR_READ;而 data_dir.entries_empty_desc 本来就是为这一刻 写的空态文案。 - 把读目录逻辑抽成不依赖 AppHandle 的 entries_in(root),NotFound 返回空列表, 其余 IO 错误仍然照报 DATA_DIR_READ(权限/类型错误不会被误吞) - 补两个 Rust 单测与一条前端契约断言 * fix(data-dir): 让 same_component 在非 Windows 平台也能编译 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 报 E0425: cannot find function `same_component_ignore_case` in this scope。 原因是调用点用 cfg! 判断平台,被调函数却用 #[cfg(windows)] 门控。 cfg! 只把条件替换成 true/false,两个分支在所有平台都要参与编译, 所以非 Windows 平台会在调用点找不到函数(附带 Component 未使用的警告)。 改成不加门控的 same_component,平台差异放进函数体里的 cfg!, 行为完全不变,只是所有平台都能编译。 * test(data-dir): 平台相关断言按 Windows / 非 Windows 分家 (#871) CI 的 Rust tests (ubuntu-22.04 / macos-14) 在第一轮编译错误修掉之后,暴露出 3 个必然失败的断言: - migrate::tests::is_within_compares_path_components_not_prefixes - migrate::tests::same_path_matches_only_the_same_directory - state::tests::with_current_suffix_keeps_the_original_path_recoverable 原因:非 Windows 上 `\` 只是普通字符,`Path::new("D:\Data\sub")` 是**一个**组件, `file_name()` 会把整串吐回来,Windows 盘符字面量的断言在那边毫无意义。 - 前两条按 cfg! 分家:Windows 用盘符字面量,其它平台用 /data、/data/sub 这类前斜杠路径 - 第三条改用 /DSHHome:这条测试验的是「拼后缀不改叶子名」,与盘符无关,两平台都能跑 行为未变,只改测试。整仓 cargo test --all-features --locked = 870 passed。 * fix(data-dir): 迁移前复核 Harness 是否真的退出 (#871) CodeRabbit review 三条意见: 1. 回滚最后一步(写环境变量)失败时直接返回,会让 DSH_HOME 继续指着那个 刚被搬空的目录,下次启动在那儿建一份空数据。现在先 undo_rollback() 把 备份放回 chosen、把挪到一边的当前目录放回原位,再报错。 2. data_dir::harness_stopped 从 fail open 改成 fail closed:只有「标记不存在」 才算已退出,读失败或内容解析不出来都按「可能还在跑」处理。 3. stop() 成功后会把 .harness.pid 删掉,崩溃残留、不在 owned 注册表里的 Harness 因此会看起来「已退出」。现在在停之前先把标记里的 PID 记下来,停完用 confirm_harness_stopped() 复核它是否真的没了;判据刻意只取标记里的 PID, 不按命令行/端口清扫(命令行不含数据目录信息,照它清扫会杀掉用户自己的实例)。 新增/更新契约用例:confirm_harness_stopped 的先后顺序与调用次数、undo_rollback 的存在与错误码。 实测:cargo test --all-features --locked 870 passed;test/data-dir.test.ts 28、 data-dir.test.tsx 13 全通过;pnpm lint 干净;真机 E2E 三例全绿(占用存在时被拦下 且 DSH_HOME 未被改写、结束占用者后迁移成功、重启后用户级 DSH_HOME 仍生效)。 * fix(data-dir): 区分「标记不在」与「标记读不出来」 (#871) `.harness.pid` 读不出来或内容不是 PID 时,原来和「文件不在」一样被当成 「Harness 已经退出」。但 `stop()` 无论哪种情况都会把标记删掉,删掉之后就再也 查不出「有个不在 owned 注册表里的 Harness 还在写这个目录」——迁移会照常放行, 复制出来的是一份撕裂的数据。 改成三态分类 `HarnessMarker::{Missing, Invalid, Pid}`:只有 `Missing` 才算已退出, `Invalid` 一律 fail closed。调用方在 `stop()` 之前就先复核一次,非法标记直接拒绝 (`DATA_DIR_HARNESS_MARKER_INVALID`),不再给 `stop()` 删标记的机会;停完再复核一次, 覆盖「标记里那个 PID 停服后仍然存活」的情况。 * fix(data-dir): 停服前只拦非法标记,停服后才复核 PID (#871) `stop()` 无论标记是否可读都会把它删掉,所以「读不出来」必须在停之前就拦下: 读不出来(占用、权限、写到一半)说明有人正动这个文件,此时既拿不到 PID,也无法 排除「有个不在 owned 注册表里的 Harness 还在写这个目录」。 但「标记里写着一个存活 PID」不能在停之前拦:那通常就是本应用自己刚拉起的 Harness(`launch.rs` 启动后立刻写下标记),提前拒绝会让 `stop()` 永远没机会 执行,正常迁移永远走不下去。于是拆成两步: - `reject_unreadable_marker`(停之前):只拒 `Invalid`,`Missing` 与 `Pid(_)` 放行; - `confirm_harness_stopped`(停之后):复核那个 PID 是否真的退出,仍存活则报 `DATA_DIR_HARNESS_RUNNING`。 实测(真机 debug 二进制,三条用例全绿):非法标记与真实占用都被拦下且现场不变, 正常路径(标记里是本应用自己拉起的 Harness)迁移成功、重启后用户级 DSH_HOME 仍生效。 * test(data-dir): 顺序断言按命令分家,回滚复原改为行为测试 (#871) CodeRabbit 第四轮的两条 nitpick: 1. `test/data-dir.test.ts` 的停服断言不再只数「`confirm_harness_stopped` 出现 两次」:按 `#[tauri::command]` 切开两个命令,逐个核对 `harness_marker` → `reject_unreadable_marker` → `workflow::stop` → `confirm_harness_stopped` 的先后。数量断言在某一处把复核挪到 `stop()` 之前时 照样通过,而那种顺序在「崩溃残留、不在 owned 注册表里的 Harness」上会直接 放行迁移——正是这道守卫要拦的场景。 2. 回滚的环境变量写失败路径改为行为测试:改写动作抽成 `migrate::rewrite_env_or_undo(chosen, restore_to, aside, apply)`,真正调注册表 / 广播 `WM_SETTINGCHANGE` 的闭包由 `rollback` 传入。新增 Rust 用例 `env_write_failure_puts_both_directories_back` 注入一个必然失败的写入,核对 备份回到 `chosen`、被挪到一边的当前目录回到 `restore_to`(`aside` 为空串时 只搬回备份),并核对写成功时一个字节都不搬动。注册表是机器级状态,单元测试 不能真去改它,所以失败必须由闭包注入——但「写失败时到底有没有复原」这条路径 现在是真的被跑到了。 实测:真机 E2E 第 11 轮 3/3 通过(130969ms);`cargo test --lib data_dir` 27 passed; `cargo test --all-features --locked` 872 passed / 0 failed; `pnpm vitest run test/data-dir.test.ts src/ui/config/data-dir.test.tsx` 41 passed。 * feat(data-dir): 数据目录迁移扩展到 macOS 与 Linux (#871) Windows 侧的迁移(PR #889)此前只在 Windows 生效:migrate / rollback 开头直接 返回 platform_unsupported,选目录用的是 Windows 专属的 rfd 特性。这次把同一套 「复制 → 校验 → 改写用户级 DSH_HOME → 旧目录改名」流程接到 macOS / Linux: - 用户级 DSH_HOME 的落点按平台分流:macOS 写 launchctl setenv,并用 ~/Library/LaunchAgents 下的 LaunchAgent 在每次登录重放(plist 自带的 EnvironmentVariables 键到不了 Finder / Dock 启动的应用);Linux 写 ~/.config/environment.d/dsh-tauri.conf 与 ~/.profile 标记块。 - GUI 应用由 launchd / 显示管理器拉起时不继承登录 shell 的环境,因此在 logger 初始化之后、任何 config::get_dsh_data_path 之前从磁盘回读并注入本进程。 - 时间戳与可用空间探测补齐 Unix 实现(libc::localtime_r 与 statvfs),形状与 Windows 完全一致,回滚时才认得出 .moved-<stamp> 目录。 - rfd 从 Windows 专属依赖提为三平台共享并显式列出特性:Windows 只要 common-controls-v6,Linux 用 xdg-portal + wayland + tokio,不为一次选目录 再引入 async-std 整套运行时。 - 复用 CLI PATH 注入已有的「标记块 + 备份写回」原语,把标记对提成参数:同一个 rc 文件里现在有两份本应用的块(PATH 与 DSH_HOME),各自只认自己的标记。 - 安装期选择目录仍只存在于 Windows 的 NSIS 安装包(deb / dmg 没有安装页), 文档按平台分别写明迁移入口与持久化位置。 验证:Windows 上 cargo test --all-features --lib 873 passed;Unix 侧只能由 CI 的 ubuntu-22.04 / macos-14 任务覆盖,本机没有 rustup 无法交叉编译。 * fix(data-dir): Unix 侧补齐 rc 原语导出与 DATA_DIR_ENV 引用 (#871) CI 上 ubuntu-22.04 与 macos-14 两个 job 编译失败,都是 Unix 侧才参与编译的代码: 1. `service/cli/path/mod.rs` 没有把 rc 模块的标记块原语再导出, `unix_env` 里的 `use crate::service::cli::{strip_rc_block, upsert_rc_block, write_rc_with_backup}` 找不到符号。补 `#[cfg(unix)] pub(crate) use rc::{...}`。 2. `unix_env.rs` 从 `super::DATA_DIR_ENV` 取常量,但常量定义在 `super::env` 里。 3. `APP_IDENTIFIER` 只有 macOS 分支用得到,Linux 上会变成 unused import, 按 `target_os = "macos"` 门控。 Windows 侧不受影响(这三处都在 unix / macos 门控内)。 Refs #871 * fix(i18n): update data directory label in Chinese locale * chore(navbar): refactor CONFIG_TABS definition and improve Dropdown.Item rendering --------- Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * feat(settings): add environment info copy action (#895) * chore: release v0.22.0 (#900) * chore: release v0.22.0 * feat(pet): follow the pointer with a spring drag (#893) * fix(pet): follow pointer movement while dragging * fix(pet): follow the pointer with a spring drag 桌宠窗口拖动改为上游 dsh-pet-component playground 里 usePetDrag 的手感:命中箱 pointerdown 开手势,越过 5px 门槛后由 K=200 / C=30 的显式弹簧每帧把窗口拉向指针(同参数、同 0.05s 单帧上限与 1/120 子步),停止移动时窗口收敛到指针处、零偏移。 指针位置取后端 device-mouse-move(虚拟屏幕物理像素),不再用 DOM pointermove:被拖的「盒子」就是窗口本身,clientX 会随窗口移动反向变化形成自我反馈,既抖又会把窗口留在指针后面。弹簧目标是绝对量 base + (cursor - anchor),因此门槛前丢掉的位移会在门槛后补齐。 useOmitIgnoreCursorEvents 增加 dragging 入参,拖拽期间不做穿透切换,避免中途丢掉指针会话;结束以 device-mouse-button 松开为主、命中箱 pointerup 为快路径,pointercancel 不结束(穿透切换会取消指针会话),另有 1.5s 会话兜底。注意:本次提交只改前端动画层,仍使用 main 上已有的 move_pet_window/设备流命令,不含 Rust 变更。 * fix(pet): stop the fling only on a real grab, not on a global click `device-mouse-button` 是**全屏**左键流:在桌面别处点击同样会上报「按下」,原先 拿它当抓取就会给甩出中的宠物刹车(用户反馈:点击不是 Pet 的位置却停止了甩动)。 抓取信号改由命中箱 `pointerdown` 决定,与上游 playground `usePetDrag` 一致 (那份实现把 `handleRef.current?.contains(event.target)` 作为开手势的前置条件, 并在 `onStart` 里 `stop()` 停飞行): - `src/hooks/use-window-draggable.ts`:新增 `UseWindowDraggableOptions.onGrab`, 在左键/主指针/无进行中手势的检查之后、手势门槛之前回调(命中箱按下即抓住, 刹车不能等 5px);app 侧接线不变,仍是 `onHitboxPointerDown={draggable.onPointerDown}`。 - `src/pet/hooks/use-pet-physics.ts`:`usePetPhysics` 改为返回 `{ onFling, onGrab }`;`onGrab` 负责刹车 + 清空轨迹(抓取前的窗口位移不再算进 这次甩动的速度),设备流只保留「OS 侧松开时刻」的职责。 - `src/hooks/use-omit-ignore-cursor-events.ts`:窗口 `Moved` 之后按最近光标样本 重算一次穿透态 —— 甩出时窗口在动、光标不动(`device-mouse-move` 不会再来), 否则命中判定会停在「宠物还在指针下面」的旧结论上。 回归用例:`use-pet-physics.test.ts` 把原先断言「全屏左键按下即刹车」的用例改成 「在别处点击不停飞、也不会被当成甩动」+「`onGrab` 立刻停飞」; `use-window-draggable.test.ts` 覆盖 `onGrab` 的触发条件(右键/非主指针/重复按下 都不触发);`test/pet-dragging.test.ts` 守住跨文件接线与新文档契约。 * fix(pet): keep the pet on the edge and settle the release once 贴边松手被弹开:跟手期间没有任何夹取,松手时 `move_pet_window(0, 0)` 又按**整个窗口** 夹回显示器,而宠物只是窗口底部居中的一块、四周是透明留白,于是贴边松手的瞬间被推开 约 100 物理像素(100% 大小)。 - `src/pet/hooks/use-pet-window-clamp.ts`(新增)+ 拖拽钩子的 `clampPosition`:跟手每帧 按「宠物本体 / 脚底」边界夹取(上游 playground `usePetDrag` 的 `getBounds` 等价物), 与飞行共用 `bodyBounds`,越界位移不会攒到松手时一次性修正; - 收尾新增 `persist_pet_window_position`(Rust + bridge + handler):只落盘、不夹取, 位置合法性由前端每帧夹取保证; 松开结算只做一次:命中箱 `pointerup` 与后端 `device-mouse-button` 都汇到物理层的 `onRelease`。此前只有设备流能清掉「按住」状态,一旦丢事件,轨迹会继续采样窗口位移, 很久以后一次无关点击就能把宠物甩出去。 验证:目标 jsdom 用例 45 passed(4 文件)、`tsc --noEmit`、`eslint`、 `cargo check --all-targets` 全部通过。 * feat: 在启动配置中支持 HTTP / SOCKS 代理 (#891) * feat: add desktop HTTP and SOCKS proxy configuration * test: reproduce plugin update cache reuse after proxy changes * fix: scope plugin update cache to the request proxy * feat(proxy): add proxy configuration and validation; remove network settings component * fix(ci): 修复插件更新缓存稳定性用例的 bash 语法与工作目录 新增的 `Check plugin update cache stability` 步骤有两个缺陷,在 b9550f7 上 让三个 Rust 测试任务全部失败(exit code 2,测试根本没跑): 1. `for iteration in ...; do` 之后缺少 `done`,bash 解析失败直接退出 2; 2. 缺少 `working-directory: src-tauri`,即便补上 `done`,`cargo test` 也会在 仓库根目录执行、找不到 workspace(Cargo.toml 在 src-tauri/ 下)。 按下方 `Check startup regression stability` 的同形写法补齐。 * fix(config): 保存代理时不抹掉在途输入 `onSuccess` 无条件 `setProxyInput(undefined)`:用户在按下保存、请求尚未返回 期间又改了输入框,这次改动会被丢弃、输入框跳回上一次提交的值。改为 mutationFn 返回本次提交值,onSuccess 仅在输入仍等于该值(比较前 trim,与提交时一致)时 才收拢编辑态;输入已被改动则原样保留。 * test(e2e): 代理用例点击已存在的配置页签,并让清理不再二次报错 b9550f7 把代理设置从「网络」页签移入「应用」面板并删除了 network 页签, 但用例仍在点击 `dsh-navbar-item-application` 之外的 `dsh-navbar-item-network` ——该 testid 已无从渲染。`click()` 在 5s 超时后抛出,对话框因此从未打开, 后续 `dsh-config-dialog-close` 才报「still not clickable」。改为点击应用页签。 finally 里的收尾点击也改为先判可点击:用例主体失败时对话框可能已经是关的, 原来会再抛一次超时错误、掩盖真正的失败原因。 * test(config): 覆盖代理保存的输入保留与收拢 b9550f7 删除了 network.test.tsx(86 行),代理输入行为失去单测覆盖。补回组件级 用例,锁定两条契约: - 保存期间用户改动了输入 → 在途输入必须保留(对应上面的修复); - 保存期间未改动、或改成了与提交值等价的内容(仅空白差异)→ 仍回落到已保存值。 变异验证:把 onSuccess 还原成无条件的 `setProxyInput(undefined)` 后,第一条 用例变红(Received 为已保存值),第二条仍绿,说明单测确实卡住了这个回归。 * test(e2e): 配置对话框收尾改用 Escape,不再依赖关闭按钮可点 上一轮修复后用例主体已经跑通(能保存、重开校验、清空),失败点收敛到 finally 的收尾:`click()` 复用 `waitForClickable()`,用例主体一旦失败、对话框已是关闭态, 收尾会再抛一次「still not clickable」超时,把真正的失败原因盖掉。 收尾只做一件事——把对话框关回去,因此按「对话框是否还开着」判断,用 Escape 关闭, 避开弹窗顶层对按压的拦截,并等待其真正消失。这样无论主体成功与否,收尾都不会 再制造第二个错误。 * test(e2e): 把配置面板用例移出 L3 车道 代理用例放错了车道。`docs/specs/desktop.test.md` §1 明确把两类断言挡在 L3 之外: - 「嵌入在 dsh iframe 内的 DOM(业务面板、侧栏、Tab、对话框等)」; - 「配置、语言、档案、插件生命周期、更新等非壳层职责的业务行为」。 这条用例正是驱动配置对话框并断言其内部输入框,两条都踩中;`plugin.test.md` 也重述了 「配置 … 业务逻辑严禁建立 L3」。上一轮修掉失效的 `dsh-navbar-item-network` 选择器后, 用例主体能跑通到保存成功,但仍卡在保存后第一次关闭对话框(`dsh-config-dialog-close` 5s 不可点)。与其继续在没有作业日志的情况下猜弹层遮挡,不如按车道归属处理:该断言 不属于 L3,移除它,保留新增的组件级单测。 行为覆盖没有丢: - `src/ui/config/debug.test.tsx` 覆盖代理保存的输入保留/收拢(含变异验证); - Rust 侧 `config/proxy.rs`、`config/setting.rs` 覆盖地址校验、归一化、持久化与 `preserve_persisted_fields`; - 代理是否真的作用于下载/更新/插件元数据请求,由 Rust 单测以真实 TCP 代理覆盖。 顺带删掉随之失去消费者的 `PROXY_URL` / `PROXY_SAVE` 选择器常量(`selectors.ts` 的注释 要求「被删用例的选择器随用例一起移除」)。若后续仍要覆盖配置面板的 UI,应按 `desktop.test.md` 落在 C 浏览器层,需要先在 `test/e2e/support/browser.ts` 建立驱动 配置对话框的能力——当前 C 层是通过 `postMessage` 注入配置,没有点 UI 的现成抓手。 --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * fix(data-dir): 防止路径别名让迁移递归复制源目录 (#898) * fix(data-dir): resolve migration paths before checking nesting * fix(data-dir): reject file ancestors on Windows path resolution * test(data-dir): distinguish link inspection from creation --------- Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 (#901) * feat(plugin): 支持从本地目录安装插件,并为本地插件源码提供热重载 面板与手动安装现在都接受本地目录:输入框可直接粘贴路径,Windows 上还可以点 文件夹按钮从原生选择器挑目录。宿主把目录规范成 link:<绝对路径> 再交给 pnpm, 并在调用 pnpm 之前先校验目录与 package.json,避免装出一个没有清单的联接后只 报出「命令成功但没有产物」这种与真实原因无关的错误。 link: 装出来的插件是软链,源码改动在核心 HMR 下本可即时生效,缺的只是「监听 哪些目录」——核心 HMR 在档案下恒为启用,但 config.root 默认为空。新增的 service/plugin/hmr 把已挂载的本地路径插件源码目录折算成一组 watch root,写进 桌面端独占的补丁层 cordis.hmr.patch.yml,随启动以 --patch 注入核心 HMR。 补丁层每次启动前按当前安装状态重算:没有本地插件时不传 --patch(指向不存在的 文件会让核心在读取补丁层时直接失败),写入失败也只退化为「不传 --patch」,不 阻断启动。开关默认打开,只有确实装了本地路径插件时才会产生监听目录。 三条实测结论写进了实现: - 补丁的 config 是整体替换,条目里必须写完整的 base/root/ignored/debounce; - watch root 必须是真实长路径,Windows 8.3 短名会让 chokidar 原生断言崩溃; - 相对 root 里一旦出现 ..,忽略规则会被拼成跨盘串而全部失效,故同盘 root 用 不含 .. 的相对路径、跨盘 root 用绝对路径,base 收敛到它们的公共祖先。 面板新增热重载开关与状态说明(未挂载本地插件时提示 idle),切换后提示重启服务 生效。 * fix(plugin): 修正本地插件热重载与本地路径解析在 Unix 上的跨平台缺陷 - hmr.rs: file_url 按需补分隔符,避免 Unix 根目录被拼成四斜杠 file:////opt/x (new URL() 会解析成 host 为空、pathname 以 // 开头,fileURLToPath 得到错路径); Windows 盘符形态 file:///D:/x 逐字节不变 - hmr.rs: 三个测试夹具改用真实临时目录,不再硬编码 X:\hmr-tmp 这类 Windows 字面量 (Unix 上反斜杠是合法文件名字符而非分隔符,路径不绝对会让 common_ancestor/pick_base 返回 None);golden 里的 base 行按真实目录归一化,其余字节逐字固定 - spec.rs: local_dir_of 测试删除「把 / 替换成字面反斜杠」的断言,改为尾斜杠剥离与 原生分隔符归一;file:/// 的盘符还原断言加 #[cfg(windows)] 门控,并补充 Unix 形态 (link:/opt/... 与 file:///opt/...)的 spec 识别用例 * fix(plugin): 修正本地插件热重载补丁层的跨平台 base 还原 render_layer_round_trips_a_real_link_dependency 用 trim_start_matches("file:///") 还原补丁层的 base:Windows 的 file:///C:/x 剥掉后恰好是绝对路径,Unix 的 file:///tmp/x 剥掉后却丢掉前导斜杠变成相对路径 tmp/x,拼出的 root 与真实源码目录 不等(ubuntu / macos 的 rust-test 各挂 1 个)。 改为只剥 "file://" 前缀(Unix 保留前导斜杠),Windows 再额外剥一层根斜杠;断言语义 不变:base + root 拼回来必须正好是插件的真实源码目录。 * fix(plugin): 修正本地插件热重载基址 URL 的保留字符转义 补丁层的 base 之前靠拼字符串生成,路径里的保留字符不会做百分号编码。 核心用 fileURLToPath(new URL(config.base, ctx.baseUrl)) 还原目录,# 会被 当成 fragment 起始符,只还原出 # 之前的半截路径:本地插件装在 D:/plugins/my#plugin 时,热重载会去监听 D:/plugins/my,源码改动全部丢失。 改为 Url::from_file_path,#、?、空格、% 等字符按 URL 规范编码, to_file_path 能逐字还原;非磁盘盘符(UNC)与非 UTF-8 路径依旧返回 None。 --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> * fix(bundle): 去掉 NSIS 模板自带的 BOM,修好 Windows 安装包打包 (#905) tauri-bundler 的 write_utf8_with_bom 会给渲染后的 installer.nsi 无条件写一个 UTF-8 BOM;仓库自定义模板 src-tauri/templates/installer.nsi 在 #889 之后也自带 一个,两个 BOM 连排让 makensis 3.x 在第 1 行直接报 `Invalid command: "\ufeff;"` 并中止打包。 实测(本机 Windows,tauri-cli 2.11.4 / makensis 3.11,只差模板这 3 个字节): - 模板带 BOM:`pnpm tauri build --debug --bundles nsis` exit 1,报错与 release v0.22.0 的 Build Windows 逐字一致(Invalid command / on line 1 / aborting)。 - 模板去掉 BOM:同一条命令 exit 0,产出安装包。 模板必须是裸 UTF-8:BOM 只由 bundler 补,makensis 仍会走 (UTF8) 分支,模板里的 中文串照旧能编译。test/data-dir.test.ts 原来断言模板「必须带 BOM」,现改为断言 「不得带 BOM」并保留中文串存在性断言,同时补上原因说明。 * test(plugin): 把 HMR 补丁层落盘改为显式开关 (#906) * test(plugin): 把 HMR 补丁层落盘改为显式开关 两个渲染测试会把生产字节写进系统共享临时目录,断言并不读它:既在测试目录之外 留下文件,也在只读临时目录上让测试失败。改为仅在显式给出 DSH_HMR_LAYER_DUMP 目录时才落盘,实机验证脚本自行设置该变量并复用同一份字节。 * test(plugin): 空 DSH_HMR_LAYER_DUMP 不再落盘到当前目录 CodeRabbit 指出:变量存在但为空时,Path::new("").join(name) 会把两份 yml 写到进程当前 工作目录,与「只有显式配置目录才落盘」的意图不符。 改为 var_os(...).filter(|dir| !dir.is_empty()):变量未设置与设置为空都直接返回。 实测(cargo test --lib service::plugin::hmr,12 passed): - DSH_HMR_LAYER_DUMP='' 时 cwd 下 dsh-hmr-layer*.yml 计数为 0; - 设置为真实目录时两份 yml(341 B / 347 B)正常落盘。 * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 (#907) * feat(plugin): 本地目录选择器补上 Linux/macOS 支持 宿主侧 pick_local_plugin_dir 的非 Windows 分支原本直接返回 PLUGIN_PICK_FOLDER_UNSUPPORTED,前端据此隐藏入口。改成走 rfd 的异步 对话框(与 pick_data_dir 同一条路):macOS 后端自己把面板派发到主线程、 不设父窗也会挂到主窗,Linux 用 xdg-desktop-portal,没有 portal 的会话 会回退 zenity。拿不到选择时同样返回 None,与「用户取消」一致,前端按 取消处理。 Windows 分支一字未动,行为与已合并版本完全相同。 前端去掉 pickFolderSupported()/PICK_FOLDER_SUPPORTED 与对应的 <If>, 按钮在三平台都渲染;新增一条用例断言按钮始终存在。 * chore: 重新触发 CI(上一轮 windows-latest 的孤儿清扫用例偶发失败) * fix(navbar): adjust layout of update chip and improve description for macOS * fix(navbar): refactor update chip rendering for improved visibility across platforms * chore: lint fix * chore: release v0.22.1 (#908) * fix(native): 让 app.json 版本跟随桌面端 0.22.1 合并 main 后 package.json 已是 0.22.1,而 app.json 仍停在 0.21.2。 build-android.yml / build-android-test.yml / release-native.yml 都断言 `app.json expo.version == package.json version`,不一致直接 `::error::` 失败;app.json 是 native 独有的新文件,0.22.0 / 0.22.1 两次桌面端发版都 没有写回过它,所以这个不一致会随 #875 一起进入 main 并让 Android 构建 变红。此处把 expo.version 对齐到 0.22.1。 versionCode 21001 → 21002:Android 要求严格递增,而可观测到的最后一次 写入是 21001(0.21.2)。本仓库 Releases 里从未有过 APK 资产(已核对 v0.21.2 / v0.22.0 / v0.22.1),因此 21002 已高于所有已记录的编号;若迁移 前的移动端仓库在 Play Console 已用到 21002 以上,请在此基础上继续上调。 * docs(readme): 补齐英文 / 西班牙文 README 的移动端支持小节 「移动端支持」原本只有中文 README 有(迁移时只改了 README.md)。 main 的三份 README 一直是同步维护的,缺了 en / es 会让这两份文档 完全不提移动端,因此在与中文 README 相同的位置补上对应小节。 en 放在代理段落之后、`## Runtime` 之前;es 的代理段落本就缺失 (#891 当时未同步到 es),故该小节直接置于 `## Runtime` 之前。 * docs(readme): 补齐西班牙文 README 缺失的代理与低完整性说明 #891(代理)与 #892(STARTUP_LOW_INTEGRITY)只更新了 README.md 与 README.en.md,README.es.md 至今没有这两段:`grep -i SOCKS5` 与 `grep -i STARTUP_LOW_INTEGRITY` 在 es 版本里都是 0 命中,而数据目录 条目(#889)当时是同步过去的。于是西语读者既看不到代理设置,也看不到 启动低完整性的说明。 按 en 版本的措辞补上对应译文,位置与另两份一致(代理条目留在 `## Inicio rápido` 的列表里,低完整性段落置于 `## Runtime` 之下)。 三份 README 现在都是 11 个二级标题、同一组内容条目。 只改文档,未改动任何代码。 --------- Co-authored-by: Mao Mr <49724027+hairyf@users.noreply.github.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: hairyf <wwu710632@gmail.com> * test(windows): publish relay fixture PID before readiness * fix(mobile): polish sidebar controls and safe-area ownership (#963) * fix(ui): sync generated Tailwind styles after native merge * fix(native): allow slow WebView connections to recover * fix(native): build standalone Android test APKs * fix(native): widen right drawer swipe area to 96dp * feat(ios): prepare iPhone and iPad TestFlight release * fix(mobile): restore detached navigation and remove touch focus boxes * perf(native): shrink release APK code and compress payloads * perf(native): target arm64 Android phones from Expo config * fix(mobile): keep the foreign fixed dock toggle off the topbar dsh-better-sidebar 的空白会话兜底入口 [data-dsh-dock-fallback] 按宿主右侧栏角落控件的 rect 定位;手机版顶栏隐藏那个会话头后锚点退化成 0x0,它会退到视口左上角压住顶栏的抽屉开关。 * fix(native): pad the scanner modal from the safe-area context A modal route mounts in its own native view tree, so RNCSafeAreaView cannot find the root provider and its insets stay 0; the scanner title and close button rendered under the iOS status bar. Read the insets from context instead. * feat(native): manage saved connections and clear pointer focus rings * fix(native): retain branding while connecting to host * feat(mobile): rename app and dismiss composer keyboard * test: drain update dialog timers before DOM teardown * docs(native): replace remaining mobile app branding * fix(native): scan ports 3088 and 3080 by default * Merge branch 'main' into native --------- Co-authored-by: SkanderBot <marrakesh1444@gmail.com> Co-authored-by: SkanderBog <317299592+SkanderBog@users.noreply.github.com> Co-authored-by: 云游君 <me@yunyoujun.cn> Co-authored-by: iuuuuuuuu <49019449+iuuuuuuuu@users.noreply.github.com> Co-authored-by: iuuuuuuuu <iuuuuuuuu@users.noreply.github.com> Co-authored-by: idealclover <idealclover@live.com> | 7 天前 | |
feat(core): 内核基线迁移至 dsh 0.2.1-rc.1,最低支持提升到 0.1.7-rc.1 (#1117) * feat(core): 内核基线迁移至 dsh 0.2.1-rc.1,最低支持提升到 0.1.7-rc.1 - 推荐内核 0.2.0-rc.2 → 0.2.1-rc.1,最低支持 0.1.5-rc.1 → 0.1.7-rc.1;同步子模块指针到 dsh-v0.2.1-rc.1(1600d8a1) - 移除 0.1.5 兼容层:清单预设版本映射、pnpm shell 拼接规格处理与旧内核分支 - “更多”页只保留唯一真实官方入口与唯一回退按钮,移除旧的破坏性确认钩子 - 核心升级改为活动本地隔离:旧快照选核、原位更新与锁等待均不覆盖活动核心 - 同步依赖 catalog 与锁文件、THIRD_PARTY_NOTICES、README/DEVELOPMENT、CI 双版本矩阵与打包元数据 - 更新基线回归、远程 bootstrap、e2e 支撑、插件版本门禁与兼容记录文档 * fix(ci): 插件 E2E 作业的 runner 路径改到步骤级 env * fix(ext): 插件页四项体验修复 - 回退按钮不再注入 data-dsh-plugins-back-label 属性,文案 span 保留 - 抵消官方分段栏 -16px 负边距,开启自定义插件市场后恢复 14px 间距 - DSH Market 取消默认勾选与推荐标记,顺序移到 Billion Context 之后 - DSH GenUI 补 Sparkles 图标、DSH Context 补 Layers 图标 * fix(e2e): 修复插件与桌面 E2E 的四处失真 - vitest.plugin.config.ts 补 __REMOTE_DSH_RECOMMEND__/__REMOTE_DSH_MINIMUM__ define,与 unit 配置同源 - 插件页头部选择器去掉 :scope 前缀,Playwright 下 :scope 与 :has 组合恒不命中 - scheduler 夹具正则加 i 标志,容忍 mkdtemp 大写随机后缀 - 桌面 E2E 恢复 stop() 的孤儿 harness 清理并等待应用端口释放 * fix: 修复双内核插件兼容与端到端测试隔离 * style(test): 规范会话订阅清理断言格式 * fix(e2e): 稳定触屏夹具并兼容旧版停靠面板定位 * fix(e2e): 修正未选工作区前置并同步桌宠测试帧就绪 * fix(e2e): 帧就绪探测立即传播非瞬态错误 * fix(e2e): 使用支持 beforeAll 的帧就绪等待 * test(desktop): 桌宠尺寸边界仅依赖原生 IPC | 3 天前 | |
fix(pet): 修复 Codex 宠物拖动始终朝右 (#1140) * fix(pet): respect Codex drag direction over bubble animations * test(pet): align tooling contract and scope drag precedence to Codex | 2 天前 |
DeepSeek Harness Tauri 桌面版
在桌面上运行 DeepSeek Harness ——
无需手动安装 Node.js、pnpm 或 Docker;普通安装首次启动可能需要联网初始化。
Download · English · Español · 文档 · 中文
功能
- 🪶 原生桌面 — Tauri 2 + React 19 + Hero 3,内嵌本地 Harness 界面。
- 📱 远程连接 — Android / iOS 移动端扫码连接电脑上的 DSH,支持局域网与 Cloudflare 公网隧道。
- 🔄 运行时管理 — 安装依赖、选择内核版本,并提供桌面端与内核更新入口。
- 🧩 插件管理 — 12 个内置插件;支持本地路径安装、内置插件停用、社区插件安装、升级、卸载与错误查看。
- 🗂️ 档案配置 — 分别管理插件与设置,支持一键迁移档案数据、备份、克隆;
- 💽 数据目录 — 安装时(Windows)可选择数据存放位置;能在设置里整体迁移与回滚。
- ⌨️ 命令行集成 — 通过托管 shim 提供
dsh/pnpm命令行工具,不是全局 npm 内核安装。 - 🐾 桌宠 — Pets / Codex 资源管理、资源包导入与会话状态展示;预设素材来自远端。
- 🎨 个性化 — 8 种配色、本机界面缩放、终端模式,以及可调不透明度与可选磨砂模糊。
内置插件
随桌面资源分发的一方插件:
- DSH Tauri — 核心插件,桌面端与 Harness 通信、版本适配器、插件依赖管理
- DSH Tauri UI — 桌面设置界面、原生核心组件、主题与配色
- DSH Tauri Mobile UI — 触屏布局与移动端偏好设置
- DSH Tauri Worktree — 会话级 Git 工作树与检出
- DSH Tauri Extension — 官方插件管理、插件市场、Skills 与 MCP
- DSH Tauri Scheduler — 定时任务与执行记录
- DSH Tauri Archive — 聊天归档与恢复
- DSH Tauri Pet — 桌宠与活动状态设置
- DSH Tauri Rightclick Menu — 会话、工作区与正文右键菜单
- DSH Tauri Model — 模型选择与参数、自动配置模型
- DSH Tauri Remote — SSH 远端 Harness 连接与同步,并把本机 Harness 分享到局域网或公网隧道
- DSH Tauri Notification — 原生级会话通知与交互操作
可选预设
启动引导提供以下社区插件,可按需安装;
- DSH Better Sidebar — 按会话隔离的右编辑器侧栏
- Billion Context — 上下文压缩与历史恢复
- DSH Market — 社区插件市场
- DSH Rewind — 对话回退与工作区备份
- DSH IM — IM 渠道与机器人管理
新增或更新预设请提交 Issue;可用版本由清单中的内核兼容规则决定。
快速开始
从 Releases 下载对应平台与架构的安装包:
| 平台 | 要求 | 安装包 |
|---|---|---|
| Windows | Windows 10+、WebView2 | x64 .exe / .msi |
| macOS | macOS 12+;Web 界面需 Safari 17.4+ 能力 | Intel / Apple Silicon .dmg |
| Linux | WebKit2GTK 4.1 运行库 | x64 .AppImage / .deb |
macOS 也可通过 Homebrew 安装:
brew install dsh-tauri/desktop/deepseek-harness
- 普通安装包首次启动需联网下载缺失的运行时与内核;Git 功能需要可用的 Git。
- 包含
Bundle后缀为离线安装包(_Bundle_*.exe、_Bundle_*.deb、两种_Bundle_*.dmg)。 - 本地运行不等于完全离线:模型服务、插件安装、更新与桌宠预设素材仍可能联网。
- Linux 显示、Wayland、AppImage 与权限问题的处理见安装与故障排查文档。
桌面代理
- 在配置 → 应用 → 代理地址设置 HTTP、HTTPS、SOCKS5 或 SOCKS5H 代理。
- 仅用于桌面端依赖/内核下载、更新检查及插件元数据,不修改模型请求或插件子进程的网络配置。
- 留空沿用系统/环境代理;保存后对新请求生效,可重试失败的下载。
- SOCKS5H 通过代理解析目标域名;本机回环连接始终直连。
移动端支持
DSH Mobile 通过局域网或公网隧道访问电脑上的 Harness,不在手机运行内核。
- Android:从 Releases 下载 APK。
- iOS:通过 TestFlight 邀请安装测试版,以审核与名额为准;Release 中的未签名 IPA 需自行签名。
在桌面顶部远程开启分享,手机扫码配对;电脑、Harness 与所用隧道须在线。远程访问并非只读,请勿公开二维码或访问码,详见手机远程访问指南。
移动端仅在官方移动服务推出前维护;开发说明见移动端规范。
运行方式
Windows 启动失败时会通过原生对话框显示具体错误。若出现 STARTUP_LOW_INTEGRITY:
- 进程低于 Medium 完整性级别,无法写入正常用户数据;更新后的程序可能继承安装目录的 Low 标签。
- 用
icacls检查目录与程序;将可信安装恢复为 Medium,或重新安装到正常目录。 - 以管理员身份运行不会移除 Low 标签限制;无需删除会话或修改
DSH_HOME。
| 当前基线 | 版本 |
|---|---|
| 推荐 Harness 内核 | 0.2.1-rc.2 |
| 支持的内核最低版本 | 0.1.7-rc.1,不保证所有插件兼容 |
更早内核不再支持;旧兼容记录仅保留历史事实,不代表当前支持范围。
- 内核与预设选择遵循资源清单及插件声明的版本范围;不保证任意最新上游版本都兼容。
- 正式版默认启用 CLI 集成:Windows / macOS / Linux 自动更新 PATH。需重新打开终端,其他 shell 仍需要手动配置。
交流
![]() QQ 群 |
![]() 微信群(已满,请先加微信) → |
![]() 微信 |
开发
说明
Warning
开发预览 — 上游 dsh 仍在快速迭代,可能存在破坏性变更;升级前请确认兼容性。
Note
安全声明 — dsh 具备本地代码执行能力。仅供学习 / 研究 / 测试,请在可信、隔离的环境中使用。
社区推荐
- dsh-tauri-notify-webhook — 任务异常停止或完成时把通知转发到 webhook、邮件(SMTP)或 pushplus by @iuuuuuuuu
- dsh-pdf-reader — 按页 PDF 阅读、本地 OCR、引用、搜索和视觉裁剪 by @SkanderBog
- dsh-context-manager — 应用前审阅上下文,并按完整历史组选择保留、总结或省略较早内容 by @SkanderBog
- @ryuu-64/dsh-find-all — 在当前对话中按 Ctrl+F,查找已保存的历史内容 by @Ryuu-64
相关项目
- deepseek-harness — 上游
dshagent 平台 - deepseek-harness-pkg — 预打包 Harness 发行版与下载源
- dsh-pet · dsh-pet-mov · dsh-pet-component — 桌宠素材与渲染组件
- dsh-plugin-codex-pets · BongoCat · dsh-dafeiyu · codex-to-dsh-pet · dsh-pet-sticker — 桌宠参考项目


