给接手的人(包括未来的自己)。只写踩过并且会再踩的坑,不写通用常识。
- 仓库根就是 Dshnext:原生 Rust 重写(iced 0.14 + wgpu,无 WebView)。阶段 0~5 已完成(可行性 / 视觉地基 / 后端接入 / 六页移植 / 收尾 / 切页动画与排版收口)。见
phase0/REPORT.md与DESIGN.md。剩的活是干净 VM 上复核首帧与零依赖。 - 第一代(Tauri 2 + React)已于 2026-09-05 删除,代码在 git 历史
11a2401^之前(git show 11a2401^:src-tauri/src可翻出)。它的src-tauri/src/就是本仓库src/core/的来源;数据目录%LOCALAPPDATA%\DshDesk\不变,老用户无缝换用。 phase0/:探针工程,保留不删——它是唯一能快速复现性能数字的地方,每次改动都该重跑tools/measure-idle.ps1。
阶段 0 验过的三样已搬进产品代码:theme.rs 令牌、字体加载、WGPU_BACKEND=dx12 限定(白省 38 MB)。
每条 cargo / npm 命令前都要 export no_proxy='*' NO_PROXY='*'。 127.0.0.1:7890 是 FlClash(tasklist //FI "IMAGENAME eq FlClash.exe" 可查):它没开时代理是死的,不绕过就一切网络操作超时;它开着时代理可用,rsproxy/domestic 源照样直连不受影响。cargo 用的是 rsproxy-sparse 源。
被墙域名单独走 FlClash:raw.githubusercontent.com 直连必挂(api.github.com 恰好能直连,别被它骗了以为网络没问题),fetch 这类文件用 curl -x http://127.0.0.1:7890 ...;FlClash 没开就退回 WebFetch(服务端抓取,不吃本机网络)。
- Shell 是 Git Bash on Windows,工作目录
D:\DshDesk /tmp就是%TEMP%(C:\Users\24453\AppData\Local\Temp)。nohup cmd > /tmp/x.log之后用 Python 读/tmp/x.log会 FileNotFound——Python 看的是真正的 POSIX 路径。写日志用"$TEMP/x.log",读的时候用完整 Windows 路径。- Rust 1.92.0 stable MSVC,edition 2024 可用
- 机器级已配 sccache(2026-09-05):用户环境变量
RUSTC_WRAPPER=C:\Users\24453\.cargo\bin\sccache.exe+CARGO_PROFILE_DEV_DEBUG=line-tables-only。依赖 crate 编译结果跨项目共享,cargo clean后重编从十几分钟降到一两分钟;debug 构建不再生成 GB 级 PDB。新项目自动继承,无需再配。看缓存命中sccache --show-stats;缓存目录%LOCALAPPDATA%下默认上限 10 GB。若某次构建报找不到 rustc wrapper,是 setx 未被当前 shell 读到——重开终端。 - Python 3.11.9,已装
fonttoolsbrotlipillow phase0/tools/NotoSansSC-var.ttf(17 MB 变体源字体)在磁盘上但不入库,别重新下载;下载地址在build_fonts.py头注释里
改代码前先 taskkill //F //IM dshnext.exe,否则 cargo build 报 拒绝访问 (os error 5)——Windows 不让覆盖正在运行的 exe。
实测前必须 cargo build --release——cargo test 不更新 target/release/dshnext.exe。一天内两次拿旧 exe 跑 e2e/自愈测试得出错误结论(「StartProbed 没出现」「自动重启不生效」,其实都生效了)。
Git Bash 里别写 >nul——那是 cmd 的设备名,bash 会创建真实文件 nul,git add -A 直接撞死(unable to index file 'nul')。重定向到黑洞用 >/dev/null。
.ps1必须是纯 ASCII,或者加 UTF-8 BOM(node tools/add-bom.cjs <file>)。Windows PowerShell 没 BOM 时按 ANSI 读,中文字面量变成鎺у埗鍙?,所有按中文名匹配的逻辑静默失败。第一代 e2e 因此挂了 8 个断言。.bat同理且没有 BOM 出路——cmd 按 ACP 读,UTF-8 中文注释变乱码后把行解析搅碎(实测 cmd 拿'see'/'iles'这种碎片当命令执行),dev.bat 里只能写 ASCII 注释。Add-Type里用System.Drawing要显式-ReferencedAssemblies System.Drawing(不是System.Drawing.Common),否则内联类编译失败、类型根本不存在,报错是莫名的TypeNotFound。GetCurrentThreadId在 kernel32,不在 user32。P/Invoke 声明错了会运行时才崩。SendKeys表达不了 Ctrl+Space("^{ }"报「关键字无效」)。切输入法用PostMessage(WM_INPUTLANGCHANGEREQUEST)。- Rust 程序的日志是 UTF-8,PowerShell 和 grep 显示为乱码。要读内容用 Python:
open(p,'rb').read().decode('utf-8')。
SetForegroundWindow在调用方不是前台进程时静默失败。必须先AttachThreadInput借用当前前台线程的输入队列。不做这步,所有按键会跑到别的窗口,看起来像被测程序坏了。- CapsLock 开着时微软拼音直接透传英文,IME 根本不打开。测中文输入前先
GetKeyState(0x14)检测并关掉。 - 不要往 iced 窗口里点鼠标。
text_input在任何落空的点击上都会 unfocus,点在卡片背景上就把焦点弄丢了,on_input再也不触发。聚焦交给程序自己(operation::focus),脚本只负责激活窗口和敲键。 - 读坐标前先
SetProcessDPIAware()。屏幕 2560×1600 @125% 缩放,不声明会拿到逻辑坐标、截错区域。 - UIA 或截图之前先恢复并重定位窗口。 最小化的窗口 rect 是 -25600,-25600,UIA 树是空的,截出来是 200×34 的残片。
- 截图用
PrintWindow(hwnd, hdc, 2)(PW_RENDERFULLCONTENT)抓窗口自己的表面,别的程序抢焦点也污染不了。 - iced 窗口在 UIA 树里后代数为 0(无 AccessKit)。第一代的
scripts/e2e.ps1(已随第一代删除)对 Dshnext 完全失效,UI 测试走iced_test。 - 点击测试前必须确认目标点上没有别的窗口。
WindowFromPoint返回的不是被测窗口就白点了——ZCode 的应用内浏览器窗格(Chrome_RenderWidgetHostHWND,属 msedge 进程)会盖在屏幕右侧,害我一度以为 iced 的on_press坏了。可靠做法:先SetWindowPos(hwnd, HWND_TOPMOST, x, y, w, h, SWP_NOACTIVATE)把窗口顶到最上并挪到已知位置,每次点击前用WindowFromPoint断言命中。脚本见phase0/tools/click-probe.ps1。 - 窗口几何变了要重算坐标。 最大化后客户区原点和宽度全变,拿旧坐标点「还原」按钮会点空,看起来像按钮失灵。每次点击前重新
ClientToScreen+GetClientRect,再按缩放比(125%)换算逻辑像素。
-
单实例互斥对自动化放行(2026-09-07 起):裸启动撞上已有实例会前置对方窗口然后退出;
--shot/--autotest/--e2e/--migrate-to/--migrate-go不拿互斥——批量出图跟用户正开的实例并存是设计内的。反过来,测「双开拦截」时不要带这些旗子。 -
dsh 的 WebUI 只挂在带 token 的地址上(裸
http://127.0.0.1:3080/是 401「authentication required」),token 只出现在 dsh 启动时的 stdout(CoreEvent::Url解析),dsh 侧不落盘、CLI 也没有补打的开关。例外(2026-09-09,有意为之):启动器在 Url 事件把最近地址覆盖写进<数据目录>/webui-url.txt(store::save_webui_url),供DeepseekHarness.exe双击独立打开;文件在用户级 LOCALAPPDATA,ACL 天然只本用户。除此之外启动器仍任何路径都不许开裸地址:Started里auto_open走pending_open等 Url 事件;端口被残留/外部实例占着(探到 Http 且非自己起的)时拿不到 token,只能提示、不能开。启动器重启后对已在跑的实例永远无法恢复入口——要换入口只能停了重起。 -
--page <名>直接开在某一页(home/profiles/plugins/env/console/settings),--tall把窗口开到 1280×1400 好把设置页一屏截完 -
--shot 路径 --after 毫秒自截图退出。--after要给够:六页都会在开窗时发环境探测(三次xx --version,每次可能几秒),2 秒会截到还没填好的界面,给 4000 稳当 -
批量出图必须一进程一杀一 sleep(
taskkill //F //IM dshnext.exe; sleep 1.2再启下一个)。紧循环连开 12 个--shot进程时,上一个窗口没退干净,PrintWindow会抓到残留窗口——表现成某页截成了另一页(首页截出设置页)、或截成 min-size 的 1100×702 残片。出完用「选中项图标是 accent 蓝」这个主题无关的信号逐张校验页面身份,别只看尺寸。 -
出图后交 judge 子代理做视觉验收,不要自己看图。两轮下来六页从 3 fail 到全 pass
-
judge 只看得到图上有的东西。 插件页默认停在「已安装」分页,市场行根本没渲染,让它验「市场行按钮样式」只会换回一条 Unverified。要验分页/悬停/展开后的状态,得先加命令行开关把程序开在那个状态上。
-
judge 抓对齐问题比人靠谱,但结论要自己量一遍再改。 它报「说明行比标签多缩进 20 物理像素」,量下来确实是 375 vs 405(缩进 24 逻辑像素)——但那 24px 当初是故意加的(对齐复选框标签)。真正的毛病是它成了整张卡里唯一不在内容列上的一行,所以改法是删掉缩进、回到内容列,不是微调数值。
- 进程树必须递归遍历。 WebView2 是
dshdesk.exe→msedgewebview2.exe(browser) → GPU/renderer/utility 共 7 个进程。只走一层父子关系只找到 2 个,内存少算 230 MB、CPU 少算 100 倍——照那个数字比,反倒是 WebView2「更省」。 - 报内存要用
\Process(*)\Working Set - Private(任务管理器「内存」列),不是PrivateMemorySize64。后者是提交的虚拟内存,NVIDIA 驱动能撑到 200+ MB 而页面并不驻留。 - 冷启动要测「首帧画好」,不是「窗口出现」。 Tauri 空窗口 92 ms 就弹出来了,内容要等 WebView2 加载 bundle(236 ms)。判定方式见
phase0/tools/first-paint.ps1(数窗口表面的颜色数)。 - 测空闲出帧数不能走 Message,否则订阅自己会触发下一帧,测出来是假的。探针的做法是自定义 widget 在
draw()里累加原子计数。
- 零尺寸 widget 永远不会被绘制。
Column::draw用bounds().intersects(viewport)剔除子元素,零面积矩形永不相交。(这条剔除逻辑对日志虚拟滚动是好消息。) .theme()/.style()要传具名函数,不能传闭包,否则报implementation of FnOnce is not general enough(HRTB 推不出来)。- 令牌结构体做成
Copy按值传,用&Palette会撞view()返回Element<'_>的生命周期。 - 是
iced::widget::space::horizontal(),没有horizontal_space()。 Shadow没有 spread。 第一代 CSS 大量用负 spread 收缩阴影(0 20px 40px -24px),iced 只能靠调小blur_radius近似(40 → 30 观感等价)。- OpenType feature 用不了。 iced 从不设置 cosmic-text 的
font_features,tnum不可达。等宽数字只能靠字体天然等宽——换字体前先验十个数字的 advance 是否一致。 - 子集字体必须写 name ID 16/17(typographic family/subfamily)。
fontdb优先用 ID 16 做家族键;只改 ID 1 会让 SemiBold 注册成独立家族,Font::with_name(...) + Weight::Semibold静默落到系统字体,截图上看不出来。varLib.instancer还要带--update-name-table。 - 后端切换:
ICED_BACKEND=tiny-skia选软件渲染器;WGPU_BACKEND=dx12限定 GPU 后端。wgpu 是在 compositor 创建时才读后者,晚于main,所以进程内set_var有效。 Text<'a>对'a不变(invariant)。 文本封装函数签名写impl IntoFragment<'static> -> Text<'static>会让format!出来的串塞不进短生命周期的Column(报 "borrowed data escapes")。统一写<'a>。mouse_area要求Message: Clone + 'static。 一路传染到所有包了它的组件函数签名。stack!里的覆盖层会吃掉下层点击。Stack::update逆序派发、先到的先 capture。透明热区放最上层是对的,但热区之间的空隙必须是裸Space,不能包mouse_area,否则整个内容区点不动。- 无边框窗口(
decorations: false)连缩放边框一起没了,八向drag_resize热区、拖动window::drag、最小化/最大化/关闭全要自己接。细节见 DESIGN.md §7.6。 Subscription::run的 builder 是裸函数指针fn(),捕获不了任何状态。 channel 不能建好再传进去,只能放全局(OnceLock+Mutex<Option<_>>等订阅第一次take())。同理Task::perform的 future 要'static,跨 Task 共享的东西(如ProcMap)也得放全局。做法见src/bridge.rs。- 事件流订阅要返回原始类型让调用方自己
.map()(bridge::events() -> Subscription<CoreEvent>),否则为了把fn(CoreEvent) -> Message塞进裸函数指针得 transmute。 - 关掉
web-colors后 iced 按物理(线性空间)混色,深底上叠带色相的半透明会被放大得离谱。 5% 的#5b76ff叠在#18181a上实测出(31,36,69)——蓝通道从 26 冲到 69,选中行直接盖过行内按钮。纪律:半透明叠色只用于中性灰(hover),带色相的一律写死不透明值。 checkbox()在 0.14 只收is_checked,标签走.label()(0.13 是checkbox(label, value))。opaque()必须包在模态遮罩上,否则点遮罩会穿透到下层按钮。ESC 关模态只能走全局键盘订阅——覆盖层拿不到键盘焦点。pick_list的L: Borrow<[T]>接受Vec<T>,不用为了凑&'a [T]去 leak。Text<'a>的IntoFragment参数别钉'static(同第 9 条),页面里到处是format!出来的串。- 默认后端已锁
vulkan,别改回dx12(2026-09-06):dx12 帧提交追不上快速拖边的窗口尺寸,DWM 拉伸上一帧成「放大再缩小」残影(用户实测 vulkan 全无);vulkan 首帧 ~500ms 也比 dx12 ~1300ms 快。旧机器没 vulkan 时用WGPU_BACKEND=dx12环境变量兜底。windows_subsystem="windows"(GUI 子系统)会让 wgpu 首帧慢 ~1.1 s(dx12 实测数字)。 同一份代码只改子系统标志:控制台子系统 136 ms、GUI 子系统 1270 ms(本机 RTX 4060,驱动枚举出 6 个重复适配器)。慢在init-closure → 首次 view之间的 compositor 建设备,与 LTO/日志/内容复杂度都无关。换 Vulkan(497)/GL(456) 快些但不通用,Vulkan,DX12掩码反而更慢(仍枚举 DX12)。测首帧必须用产品同款子系统——阶段 0 探针没设这个属性,那个 145 ms 是控制台数字,误导了一版。 iced_test的&str选择器是精确相等(content == self),不是子串;Selector对widget::Id也有实现(控件设.id()即可按 id 选)。click要求目标visible_bounds非空(滚出视口点不到),find不要求。禁用按钮上的文字被点会冒泡到外层mouse_area——空草稿点模态「创建」不触发DialogConfirm,但会产生遮罩的CloseDialog,所以断言要写「没有 DialogConfirm」而不是「没有任何消息」。喂Start/Stop前必须先bridge::init()(update 里同步取bridge::sink())。- 「零运行时依赖」要用
dumpbin /DEPENDENTS验,别信「Rust 肯定静态」的直觉。 默认 MSVC 构建动态依赖VCRUNTIME140.dll(VC++ 运行库)+ 一批api-ms-win-crt-*(UCRT),干净裸机上没有就直接起不来。解法:.cargo/config.toml里[build] rustflags = ["-C","target-feature=+crt-static"],之后只剩系统 DLL(代价 +0.21 MB)。注意 rustflags 是全局的,cargo test/debug 也会静态链(功能无碍,链接稍慢);临时要动态 CRT 就命令行RUSTFLAGS=""覆盖。 - 没有全局 opacity,淡入只能靠「盖一层背景色面纱」,而面纱必须自己
with_layer。renderer::Style只有text_color;wgpu/tiny-skia 在同一层内都是先画完所有 quad 再画所有 text,面纱和内容同层的话文字会盖在面纱上面——底色淡入而文字全程清晰,比不做动画更怪。with_layer走push_clip,新层序号更大,稳定画在内容之后。但面纱盖的是整块区域,不是内容本身:区域实际背景比面纱色亮时背景会跟着变暗——切页面纱因此被用户否掉(2026-09-05 删,只留位移)。模态遮罩用同一招没问题,因为「变暗」在那里正是目的。 - 入场位移用
renderer.with_translation,不要动 padding/height。 后者每帧重新布局(设置页六张卡整树重排),前者只影响 draw、布局逐帧复用。也不要用Transformation::scale:会连文字一起缩,cosmic-text 非整数缩放要么每帧重栅格化要么拉伸图集,本来就没 hinting 的中文会更糊。 - 要在动画中途截图,光靠
--shot --after不行——落定态永远是它截到的样子。加了--switch-to <页> --switch-at <毫秒>:开窗后定时切页,--after与它的差就是快门落在过渡的第几毫秒。 AnimState::animate_to起不了「重播」。 它从当前值出发,而上一次入场落定后当前值已等于目标值,再调等于什么都不动。切页入场要用restart(key, from, to, dur, now)强制从头跑。补间方向刻意写成 1 = 刚切、0 = 落定:value()对无记录的 key 返回 0.0,正好是落定态,冷启动和--page出图都不必预置初值。image::Handle::from_bytes的 id 是Id::unique(),在view()里现造 = 每次都是「新图」,缓存穿透 → 悬停任何按钮触发 view 重建时头像闪烁。内嵌位图要用OnceLock把 Handle 存成全局单例(brand_handle())。SVG 的from_memory没这个问题——它的 id 按内容 hash(iced_core/src/svg.rs),&'static [u8]天然稳定,所以界面图标可以直接现造。- iced 0.14 没有
window::hide/show(runtime 全文只有 minimize)——「关闭到托盘」只能自己ShowWindow,hwnd 按标题FindWindowW(src/win32.rs)。标题是找窗的钥匙:.title()与找窗必须同源(app::WINDOW_TITLE常量)。 tray-icon的TrayIcon不是 Send(内部Rc<RefCell>)——托盘对象放thread_local,只在主线程 update 里创建/移除;事件走 crate 全局 channel(订阅流跑在 executor worker 线程,只能轮询 channel,别碰托盘对象)。Windows 上托盘的消息窗口依赖创建线程的消息泵——winit 主线程满足。Task::perform的 future 借不了局部变量(latest(&url)报 E0597)——API 设计成收所有权String,调用方 clone 进去。- 自定义 wgpu 管线走 iced_wgpu 0.14 官方 primitive 通路(
primitive::{Primitive, Pipeline}+Renderer::draw_primitive,draw()返回 true 可直接画进 iced 的大 pass;见src/ui/glass_pipeline.rs)。三个保命点:同层 flush 顺序固定 quads→triangles→primitives→images→text(layer.rsstart()/end()),primitive 永远在同层 quad/mesh 之后、image/text 之前;prepare拿到的 bounds 已带上层变换(滚动 with_translation、reveal 位移),卡片矩形不用自己做平移数学;Pipeline::trim()每帧末调用,帧内槽位计数靠它复位。 - dst-read 混合(真 backdrop-filter / 逐像素混合模式)在 wgpu 27 下不可达:primitive 的
render()回调只给TextureView,拿不到Texture句柄做copy_texture_to_texture,wgpu 也没有 framebuffer fetch。已知底图时用解析求值替代读画布(glass.wgsl 在 fragment 里重算光球场——数学上还更准)。 - 双后端编译时
iced::Renderer是 fallback 枚举(Primary=wgpu、Secondary=tiny-skia,变体公开可 match),后端专属 widget 按变体分路(frosted.rs/glass_pipeline.rs的背景 widget)。iced::renderer模块是私有的,枚举要从iced_renderercrate 引;wgpu 本体用iced::wgpu再导出(版本与 iced 锁死,别单独加 wgpu 依赖,否则 trait 签名对不上)。 - 嵌套
with_layer的 bounds 不与父裁剪求交。push_clip(iced_graphics layer.rs)直接layers.push(with_bounds(bounds)),不复交——scrollable 用with_layer(visible_bounds)裁内容,里面的 frosted 卡片再开自己的层时若传内容坐标的全尺寸 bounds,就逃出滚动裁剪:滚上去的卡片画到标题栏上。修法:层边界一律传bounds.intersection(viewport)(frosted.rs 的clipped)。viewport 已是内容坐标系,push_clip 会乘上层变换把它变回物理坐标,不用再手算滚动偏移。 - 玻璃图元有「viewport == rect」逐位相等契约(glass_pipeline
snap_to_physical,2026-09-06):顶点着色器画全屏大三角形,fragment 用phys = rect.xy + uv × rect.wh反推屏幕坐标,而 uv 是按draw_primitive传入的 bounds(→ set_viewport,小数照设)映射的——bounds 与 u.rect 差半像素,整个坐标系被拉伸,1px 描边的 SDF 随机落到错误的行上,表现为「卡片描边亮线随机缺某一段/某一条边」(实测还有弧形咬痕)。draw 与 prepare 必须传同一个取整到物理网格的矩形;draw 拿不到 scale,用 prepare 存的原子 static 中转。裁剪层可再放宽 1px(层里只有玻璃图元,圆角外 alpha=0)。 - 滚动页顶/底渐隐帏幕(
glass_pipeline::fade_veil,shader 模式 2)是不透明背景场 + 带高 alpha 渐隐,色 = bg_app + 光球,与窗外背景续上;顶帏带宽必须窄于内容顶 padding(现 14 < 44)——等宽的话稍滚一点首行标题就整个被吃掉(用户实测否掉 44)。帏幕不实现 update,stack 逆序派发返回 Ignored,滚轮/拖拽照常到达 scrollable。
「桌面窗口」打开方式:用系统 WebView2 开一个启动器自己的顶层窗口(独立图标/标题,dock 里不跟浏览器混)。踩过的坑:
- 不能用 wry 的 tao 窗口层:tao 是 winit 分支,与 iced 主线程的 winit 在同进程抢全局状态(窗口类注册/DPI context),副线程建 tao 窗口会原生崩溃(不是 Rust panic,
any_thread+COM 初始化都压不住,panic=abort下整个进程没)。所以窗口用windowscrate 手写原生 Win32(RegisterClassEx/CreateWindowEx/GetMessage 泵),只借 wry 做 WebView2 那层(经 raw-window-handle 递 HWND)。 - 桌面窗口是独立二进制
DeepseekHarness.exe(workspace 成员webui/,2026-09-09 二次改;此前「同进程线程 → --webview-host 子进程 → --webview-relay 中继 → 硬链接换皮」四代方案全部作废,教训见下)。为什么必须独立 bin:dock/任务管理器按进程/文件名归组,而 MyDockFinder 的图标与显示名读 exe 内嵌资源(图标资源 + FileDescription)——硬链接与启动器同字节,改名不改皮,图标永远甩不开。webui/ 只有薄 main + 自己的 build.rs(鲸鱼assets/icons/webui.ico+[package.metadata.winresource]写 FileDescription/ProductName;winresource 0.1.31 的字符串版信息只能走 Cargo.toml metadata,没有 set_file_description 这类方法)。链接器把没用到的 iced 全剥了,宿主 exe 仅 ~1.9 MB。ensure_host()spawn 同目录兄弟DeepseekHarness.exe(缺 → Err 提示重装);管道协议不变:stdin 每行一个 URL(token 不上命令行)、关窗 stdout 写closed、父退出 → EOF →WM_APP_QUIT自毁(DestroyWindow 必须属主线程,走 PostMessage)。host_main()双模式判据 =GetFileType(GetStdHandle(STD_INPUT_HANDLE))是否 PIPE:非管道(双击,GUI 子系统无 stdio)→ 独立模式:win32::raise_if_exists(WEBUI_TITLE)有窗即前置退出,否则读数据目录webui-url.txt(见「token 落盘例外」);读不到 MessageBox 提示后退出。启动器侧open()在 HOST 为空时先raise_if_exists——已有独立窗口就复用不起第二个(代价:不导航新 URL、close_stops 不生效)。 - 每窗口 AUMID 走属性存储,不走 ITaskbarList3(2026-09-09 修正,原结论「走不通」作废):
ITaskbarList3::SetAppUserModelID确实没在 windows 0.62 投影,但 per-window 身份的正路是SHGetPropertyStoreForWindow::<IPropertyStore>(hwnd)+SetValue(&PKEY_AppUserModel_ID, VT_BSTR PROPVARIANT)+Commit()。features 要Win32_UI_Shell_PropertiesSystem(子模块单独门控)+Win32_Storage_EnhancedStorage+Win32_System_Com_StructuredStorage+Win32_System_Variant。PROPVARIANT 的联合体套ManuallyDrop字段不自动 DerefMut,要显式(*pv.Anonymous.Anonymous).vt = VT_BSTR这样写,用完ManuallyDrop::drop释放 BSTR。⚠️ 读回陷阱:GetValue回来的类型是VT_VECTOR|VT_UI1(字节序列化 UTF-16),不是写进去的 VT_BSTR——按 bstrVal 解释直接段错误(实测崩在窗口创建后一行)。先验vt再取。SetCurrentProcessExplicitAppUserModelID仍是进程级(会连主窗口一起改名),别碰。 - 图标:
dsh.ico用include_bytes!内嵌,运行时解析 ICO 目录取最大图 →CreateIconFromResourceEx→ 设进WNDCLASSEXW.hIcon/hIconSm(类图标,WM_GETICON查不到属正常)。 - DPI 感知只能运行时设,manifest 声明会 14001 拒启(2026-09-10):宿主不跑 winit,缺声明时系统按 96 DPI 虚拟化再拉伸 1.25x → 整窗文字发糊;往 winresource 的 manifest 里加
<application><windowsSettings><dpiAwareness>段后 exe 直接起不来(「并行配置不正确」os error 14001,rc/SXS 对那段命名空间组合不认)。正路 =host_main()开头SetProcessDpiAwarenessContext(PMv2)(winit 同款),失败退SetProcessDPIAware();建窗尺寸 1200×800 逻辑值自己乘 DPI;WM_DPICHANGED按建议矩形跟缩。验证:GetDpiForWindow(hwnd)应为 120(125% 屏)。 - 手柄环透明的设计是真的:
WM_ERASEBKGND返回 1 = 环带 alpha=0 透出桌面。别「修」成刷色——2026-09-09 用户截图白圈其实是桌面本来就白,刷成 RING_BG 反而把透明外圈毁了,已回滚。 - 运行时检测:
reg queryEdgeUpdate 客户端 GUID{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}(HKLM WOW6432/HKLM/HKCU 任一存在即在)。缺则webview::open返回 Err、toast 提示安装,按用户要求不静默回退浏览器。 - 依赖:
wry+windows(Foundation/WindowsAndMessaging/Gdi/LibraryLoader/Com)+raw-window-handle。exe 体积 +~0.3MB(不打包内核,WebView2Loader 系统自带动态加载)。
src/core/是从第一代src-tauri/src/复制的,业务逻辑一行不改。 改完grep -rn tauri src/core必须为零。唯一批量改动是模块路径crate::xxx→crate::core::xxx。EventSink用 unbounded channel 是刻意的。 日志洪峰时反压会把 dsh 卡在写管道上;上限交给 UI 侧的环形缓冲(VecDeque上限 2000)。core/mod.rs顶部有#![allow(dead_code)]。 阶段 2 只接了四条链路,安装/插件/市场的函数还没调用方,但它们是上一代验证过的逻辑,别删。- 插件市场用 npm
keywords:dsh-plugin检索,不是 GitHub topic——后者返回一堆无关仓库(deepseek-harness 21 万星、reactive-resume 之类)。 - 验证后端链路用
--e2e,不要用鼠标坐标。 程序自己跑「启动第一个 profile → 8s → 停止」,RUST_LOG=dshnext=debug能看到每个CoreEvent到达update()。 RUST_LOG=dshnext=debug会打出每条 Message(update.rs开头,已排除高频的 Tick/ToastTick)。交互测不出效果时先看这个——分得清「消息没到」和「到了但逻辑不对」。- 交互实测用
phase0/tools/interact-probe.ps1:模态、ESC、Ctrl+1..6、侧边栏点击一套跑完。坐标是逻辑像素,脚本按 1.25 缩放换算;窗口 1600×1120 物理 = 1280×896 逻辑,x 超过 1280 就点到客户区外面去了(第一版就是这么白点的)。 - 「在途请求」不能只看结果是否为空判重。 反复进环境页会重复拉版本列表——
dsh_versions.is_empty()在请求飞在半路时仍为真。要单独一个versions_loading标志。
- npm 依赖树里全是 NTFS junction,
fs::copy对目录链接直接「拒绝访问 (os error 5)」。 第一版迁移就在home/profiles/node_modules半路翻车。检测用fs::symlink_metadata().is_symlink()(Python 的os.path.islink不认 junction,排查时别被骗);重建用junctioncrate(std 没有symlink_junction,symlink_dir又要特权)。junction 目标是带\\?\前缀的绝对路径,重建前必须剥前缀做旧根→新根改写,否则删了旧位置链接全断。 - 迁移是两阶段:先全量复制(单文件 3 次退避重试,吃杀软/索引器的瞬时锁),全部成功才删源。 复制失败 = 什么都不动原地可重试;删除失败只记 warnings(数据已双份)。
- home 复制必须排在数据目录删除之前、「当前生效 home」必须在改道之前抓取——默认 home 就住在数据目录里(
<数据>/home),顺序不对会把要复制的源先删掉。这条 bug 曾被测试执行顺序掩盖,加了 junction 测试才现形;迁移测试务必把 home 覆盖设成非默认值再跑。 - 验证迁移结果别信启动日志的
data_dir=——main.rs 那行打的是迁移前捕获的局部变量。看 pointer 文件(%LOCALAPPDATA%\DshDesk\launcher-dir.txt)或迁移后新起的进程。
makensis报Can't open output file通常是上一次装测的 setup 进程还活着。 NSIS 静默安装(/S)在文件拷完后自身可能仍驻留,占着输出文件名。tasklist //FI "IMAGENAME eq Dshnext_0.1.0_x64-setup.exe"一查就见,taskkill //F //PID掉再编。/D=参数必须走cmd //c。 Git Bash 会把/D=C:\...当路径改写(/S "/D=..."也一样),NSIS 收到畸形参数直接退 2 且什么都不装。写cmd //c "setup.exe /S /D=C:\目标"。/D=还必须是最后一个参数且不能加引号(NSIS 的规矩)。- 卸载段只写
RMDir "$SMPROGRAMS\..."删不掉开始菜单文件夹——RMDir不删非空目录,两个.lnk还在里面,于是每次卸载都留一个死文件夹。必须先逐个Delete快捷方式再RMDir。这条只有真装真卸一遍才看得见。 - 验证包内 exe 是不是当前构建,用哈希不用大小。
7z e setup.exe dshnext.exe抽出来跟target/release/dshnext.exe比 SHA-256;只看字节数会被「改了代码但体积没变」骗过去。 .ps1那条 BOM 纪律不适用于.nsi——installer.nsi保持纯 ASCII(文件头注释里也写着),makensis 按 ACP 读脚本,中文进去就是乱码。