传输修复:P2P 接收混合有界内存 sink + 发送进度/完成语义修正 + 前端 CJK 文字累积修复 + 传输速度显示

- P2P 接收端(桌面)改用混合有界内存 sink:wails:// 自定义 scheme 无持久 WebView 存储致
  OPFS 降级、createWritable 回压不畅,把接收链堵死、饿死 dc.onmessage → 接收方 rwnd=0 →
  发送方 bufferedAmount 卡死 16MiB(PWA→Mac 约 15MB 死锁)。小文件(暂存 < 256MB)纯内存、
  close 整文件过 Go 写盘;超上限大文件流式 spill 到 Go 临时文件(每 64MB 一批),内存恒定
  有界。浏览器 / iOS 接收仍走 OPFS
- 新增桌面流式落盘:download.go 的 Begin/Append/Finalize/AbortStreamingDownload(复用
  SaveDownload 的目录解析 / 文件名 sanitize / 不可写回退 / reveal,临时文件同目录便于原子
  rename),app.go 加 4 个绑定 + net/desktop.ts 封装;含单测 download_stream_test.go;
  revealInFileManager 加 CDROP_NO_REVEAL 守卫(headless / 测试不弹 Finder)
- IncomingSink.close() 返回 SinkResult(blob | 已落盘 path),新增 deliverIncoming 统一
  落地;relay 接收一并改用,桌面大文件经中继也有界落盘
- P2P 发送端进度改按“已交付字节”(bytesSent − bufferedAmount)计,而非已塞进本地 16MiB
  buffer 的量——后者在 buffer 秒满后定格、误报“疑似卡住”;并加 250ms 轮询在 backpressure
  等待期持续刷新进度
- P2P 发送端送完后等 bufferedAmount 抽干到 0 再宣告 completed(原先入 buffer 即标完成,
  与接收端仍在下载的状态不一致)
- 前端动态 CJK 文本累积修复:聚珍 shim 把就地更新的 CJK 文本节点 charify 拆段后,旧碎片
  残留累积(典型“发送到 X”切设备多出“到”)。新增 DynText(key=文本内容触发整体重挂、
  保留中西排版),应用于发送按钮 / 设备名 / 在线数 / 相对时间 / 会话与令牌活跃时间 / 消息
  计数 / 剪贴板来源 / 问候语等就地更新处
- 传输卡片新增实时速度:store upsertTransfer 据相邻样本差分 + EMA 平滑算 bytesPerSec,
  TransferRow 在字节流动且未卡死时以“X/秒”展示(点分隔 meta 行追加)
This commit is contained in:
2026-06-23 15:19:40 +08:00
parent 018a77b13a
commit c79b176b87
19 changed files with 686 additions and 57 deletions
+23
View File
@@ -332,6 +332,29 @@ func (a *App) SaveDownload(name string, data []byte) (string, error) {
return platform.SaveDownload(platform.ResolveDownloadDir(), name, data)
}
// BeginDownload / AppendDownload / FinalizeDownload / AbortDownload are the
// streaming counterpart of SaveDownload, bound to the WebView. The receiver's
// hybrid sink keeps a file in memory until it crosses a cap, then spills the
// overflow here so large files don't sit in the WebView heap (the wails:// no-OPFS
// deadlock). Begin opens a temp file in the download dir, Append streams batches
// (base64 over the bridge), Finalize renames it into place and returns the path,
// Abort discards it on cancel / failure.
func (a *App) BeginDownload(sessionId string) error {
return platform.BeginStreamingDownload(platform.ResolveDownloadDir(), sessionId)
}
func (a *App) AppendDownload(sessionId string, data []byte) error {
return platform.AppendStreamingDownload(sessionId, data)
}
func (a *App) FinalizeDownload(sessionId, name string) (string, error) {
return platform.FinalizeStreamingDownload(sessionId, name)
}
func (a *App) AbortDownload(sessionId string) {
platform.AbortStreamingDownload(sessionId)
}
// ChooseDownloadDir opens a native directory picker (title localised by the
// caller) and returns the chosen path, or "" when the user cancels. The page
// persists the result via SaveSettings.