6b0edbb1 + 03113937 已推到 develop,一次性落地六个 openspec change(端口层补齐 / 原子解除配对 / 设备名端口 / Web 取消与邀请预览 / 改名不重启节点)并跑了一轮 /simplify + 代码审查。动了不少前端契约,接着做 Web 应用区之前先看这一页,免得照着旧接口写。
关联:#98 #99 #100 #103 #104 已随这两个提交关闭。
一、破坏性契约变更(照旧写法会编不过或行为不对)
1. WebNode.inbox_items() 的返回类型变了
// 旧
inbox_items(includeArchived: boolean): Promise<InboxItemSummary[]>
// 新
inbox_items(includeArchived: boolean): Promise<InboxItemDetail[]>
返回的 detail 是完整的:files 已填、transfer 投影已在同一把锁下批量补好,语义与逐条 inbox_item() 一致(软删不返回、归档按开关、receivedAt 倒序)。
所以不要再写 1+N。旧代码是这样的,已经删掉了:
const summaries = await node.inbox_items(false);
const details = await Promise.all(summaries.map((i) => node.inbox_item(i.id)));
return details.filter((d): d is InboxItemDetail => d !== null); // ← 这个 filter 是在补一个自制竞态
那个 filter 不是防御性编程,是在补「拉列表与拉详情之间条目可能已删」的窟窿——一次调用之后窟窿本身就不存在了。
inbox_item() / inbox_item_by_session() 没动,仍返回 InboxItemDetail | null。
InboxItemSummary 已从 docs/app/app/_lib/view-types.ts 的 re-export 里删掉(应用区已零引用)。
2. 设备名:setDeviceName / rename_device 现在回传归一化结果
三端统一改成返回「真正落库的那个值」(string | null),不要再回读:
// 旧:set 完再 get 一次,才知道后端把名字截成了什么
await commands.setDeviceName(name);
const stored = await commands.getDeviceName();
// 新
const stored = await commands.setDeviceName(name);
回读不只是多一次往返:两次调用之间另一个窗口或 MCP 改了名,回读拿到的是别人的值。桌面 src/lib/device-name.ts 与移动端都已改完,Web 的主路径(_lib/node-runtime.ts 节点在跑那条)本来就是对的。
⚠️ Web 还有一条 fallback 路径(节点没起来时走模块级 mod.set_device_name)仍然需要回读,因为那个导出的签名是 Result<(), JsValue>、不回传结果。那条我刻意没改,注释里写了原因。
3. 改设备名不再重启节点
给 libp2p identify 加了运行时 agent_version setter(已提上游 libp2p/rust-libp2p#6576)。此前改一次名要断开所有连接、中断进行中的传输,现在既有连接原地更新、一个 RTT 内生效。
对前端的意义:改名可以做成一个普通的设置项,不需要任何「这会断开连接」的警示文案,也不需要在传输进行中禁用它。
4. 桌面传输历史查询不再依赖节点启动
get_transfer_projections / clear_transfer_history / get_transfer_source_paths 改成直接吃 store,不再经 NetManager。此前 /transfer 路由挂载即 loadProjections(),节点没 start 或 start 失败时整页报错。现在账本随时可查。
delete_transfer_session 仍走 manager——「进行中不可删」的域守卫在那里。
5. 三份自动生成的 bindings 都重生成了
src/lib/bindings.ts、crates/web/bindings/bindings.ts、mobile/packages/swarmdrop-core/src/generated/。
它们此前都早已落后于已提交的 Rust,而且没有任何门禁拦这件事。所以这次的 diff 里有一批不属于本轮改动的存量修复:start 的参数个数、pauseTransfer 拆成 pauseSend/pauseReceive、新增 deviceRenamed / pairedDeviceRemoved 事件……前端能跑只是因为调用的恰好是交集。
规矩:改了跨 IPC/FFI 边界的类型就当场重生成并一起提交,别指望「下次 tauri dev 会自动更新」——那只在有人恰好跑 dev 时才发生,CI 从不跑 dev。命令与坑记在 dev-notes/knowledge/toolchain.md。
二、新增的可复用件
docs/app/app/_components/confirm-action.tsx
两态确认(点一下 → 换成「警示文案 + 红色确认 + 返回」)。传输面板里三份手写的同款状态机已收进去。应用区新加破坏性操作用它,别再手写 useState(false)。
两个出口,按 DOM 结构选:
| 用哪个 |
什么时候 |
<ConfirmAction /> |
确认态就地顶掉触发按钮(列表项动作行)。绝大多数场景 |
useConfirmAction() → { trigger, panel } |
触发按钮与确认区不在同一个位置(如按钮在面板头部、横幅在头部下方),两段节点交回调用方各自摆放 |
两条已经定死、别再各写各的语义:
- 复位时机是「点确认的同一拍」,不等异步结果。 在途由调用方的
pending 表达(按钮文案变「清空中」),失败由调用方在自己的错误位渲染。改成「成功后复位」会让确认条在整个请求期间赖着不走。
error 不在组件里。 三处的错误卡位置各不相同(有的在横幅下方独立一行、有的与 projection.errorMessage 有固定先后),纳进组件就会挪动展示位置。
一个行为变化(我认为更对,但确实变了)
传输项的确认态此前是 item 级的,折叠期间会赖着——点了「取消」进确认态、折叠、再展开,看到的是上一次没点完的确认条。现在动作区只在展开时挂载,折叠即复位。确认是瞬时意图,离开视图就该作废。
三、几条容易踩的(不是新规矩,但这轮反复撞到)
- 改了
crates/web 必须 pnpm build:wasm 重烘,否则 docs/ 下 tsc 看到的是旧类型签名,会报出一堆假错误。这轮就有 agent 因此差点把正确的代码改回去。
docs/app/app/_lib/create-store.ts 是自研 store,不是 zustand,同样有「selector 里派生新数组/对象 → 无限重渲染」的陷阱,而 pnpm check:zustand-access 只扫仓库根 src/,不覆盖 docs/。这块没有机器兜底,selector 一律只返回原始值或 store 内的稳定引用。
- 静态导出三限制没变:无
redirect()、运行时 ID 不进路由段(用 query param)、useSearchParams() 要套 <Suspense>;内部导航走 next/link(手写 <a href> 不加 basePath,GitHub Pages 子路径下全 404)。
- 收件箱是真表不是「已完成接收会话」的投影。 会话表有
HISTORY_CAP = 100 淘汰,收件箱条目不参与——「清空传输历史不动收件箱」这条不变量在浏览器上才成立。前端做「删除」时要分清删的是账本还是内容。
细节都在 dev-notes/knowledge/web-app-frontend.md 与 storage-abstraction.md,这轮补了新的几节。
四、后续
前端相关的 follow-up 会另开 issue 指派过来,这一条只作背景,不用在它下面做事。有对不上的地方直接在这里问。
6b0edbb1+03113937已推到develop,一次性落地六个 openspec change(端口层补齐 / 原子解除配对 / 设备名端口 / Web 取消与邀请预览 / 改名不重启节点)并跑了一轮/simplify+ 代码审查。动了不少前端契约,接着做 Web 应用区之前先看这一页,免得照着旧接口写。关联:#98 #99 #100 #103 #104 已随这两个提交关闭。
一、破坏性契约变更(照旧写法会编不过或行为不对)
1.
WebNode.inbox_items()的返回类型变了返回的 detail 是完整的:
files已填、transfer投影已在同一把锁下批量补好,语义与逐条inbox_item()一致(软删不返回、归档按开关、receivedAt倒序)。所以不要再写 1+N。旧代码是这样的,已经删掉了:
那个
filter不是防御性编程,是在补「拉列表与拉详情之间条目可能已删」的窟窿——一次调用之后窟窿本身就不存在了。inbox_item()/inbox_item_by_session()没动,仍返回InboxItemDetail | null。InboxItemSummary已从docs/app/app/_lib/view-types.ts的 re-export 里删掉(应用区已零引用)。2. 设备名:
setDeviceName/rename_device现在回传归一化结果三端统一改成返回「真正落库的那个值」(
string | null),不要再回读:回读不只是多一次往返:两次调用之间另一个窗口或 MCP 改了名,回读拿到的是别人的值。桌面
src/lib/device-name.ts与移动端都已改完,Web 的主路径(_lib/node-runtime.ts节点在跑那条)本来就是对的。mod.set_device_name)仍然需要回读,因为那个导出的签名是Result<(), JsValue>、不回传结果。那条我刻意没改,注释里写了原因。3. 改设备名不再重启节点
给 libp2p identify 加了运行时
agent_versionsetter(已提上游 libp2p/rust-libp2p#6576)。此前改一次名要断开所有连接、中断进行中的传输,现在既有连接原地更新、一个 RTT 内生效。对前端的意义:改名可以做成一个普通的设置项,不需要任何「这会断开连接」的警示文案,也不需要在传输进行中禁用它。
4. 桌面传输历史查询不再依赖节点启动
get_transfer_projections/clear_transfer_history/get_transfer_source_paths改成直接吃 store,不再经NetManager。此前/transfer路由挂载即loadProjections(),节点没 start 或 start 失败时整页报错。现在账本随时可查。delete_transfer_session仍走 manager——「进行中不可删」的域守卫在那里。5. 三份自动生成的 bindings 都重生成了
src/lib/bindings.ts、crates/web/bindings/bindings.ts、mobile/packages/swarmdrop-core/src/generated/。它们此前都早已落后于已提交的 Rust,而且没有任何门禁拦这件事。所以这次的 diff 里有一批不属于本轮改动的存量修复:
start的参数个数、pauseTransfer拆成pauseSend/pauseReceive、新增deviceRenamed/pairedDeviceRemoved事件……前端能跑只是因为调用的恰好是交集。规矩:改了跨 IPC/FFI 边界的类型就当场重生成并一起提交,别指望「下次
tauri dev会自动更新」——那只在有人恰好跑 dev 时才发生,CI 从不跑 dev。命令与坑记在dev-notes/knowledge/toolchain.md。二、新增的可复用件
docs/app/app/_components/confirm-action.tsx两态确认(点一下 → 换成「警示文案 + 红色确认 + 返回」)。传输面板里三份手写的同款状态机已收进去。应用区新加破坏性操作用它,别再手写
useState(false)。两个出口,按 DOM 结构选:
<ConfirmAction />useConfirmAction()→{ trigger, panel }两条已经定死、别再各写各的语义:
pending表达(按钮文案变「清空中」),失败由调用方在自己的错误位渲染。改成「成功后复位」会让确认条在整个请求期间赖着不走。error不在组件里。 三处的错误卡位置各不相同(有的在横幅下方独立一行、有的与projection.errorMessage有固定先后),纳进组件就会挪动展示位置。一个行为变化(我认为更对,但确实变了)
传输项的确认态此前是 item 级的,折叠期间会赖着——点了「取消」进确认态、折叠、再展开,看到的是上一次没点完的确认条。现在动作区只在展开时挂载,折叠即复位。确认是瞬时意图,离开视图就该作废。
三、几条容易踩的(不是新规矩,但这轮反复撞到)
crates/web必须pnpm build:wasm重烘,否则docs/下tsc看到的是旧类型签名,会报出一堆假错误。这轮就有 agent 因此差点把正确的代码改回去。docs/app/app/_lib/create-store.ts是自研 store,不是 zustand,同样有「selector 里派生新数组/对象 → 无限重渲染」的陷阱,而pnpm check:zustand-access只扫仓库根src/,不覆盖docs/。这块没有机器兜底,selector 一律只返回原始值或 store 内的稳定引用。redirect()、运行时 ID 不进路由段(用 query param)、useSearchParams()要套<Suspense>;内部导航走next/link(手写<a href>不加 basePath,GitHub Pages 子路径下全 404)。HISTORY_CAP = 100淘汰,收件箱条目不参与——「清空传输历史不动收件箱」这条不变量在浏览器上才成立。前端做「删除」时要分清删的是账本还是内容。细节都在
dev-notes/knowledge/web-app-frontend.md与storage-abstraction.md,这轮补了新的几节。四、后续
前端相关的 follow-up 会另开 issue 指派过来,这一条只作背景,不用在它下面做事。有对不上的地方直接在这里问。