LocalSend 工程指南:从 AGENTS.md 深入其 Flutter + Rust 混合单体仓库的开发工作流与协议架构
LocalSend 是一个用 Flutter 实现 UI、用 Rust 实现底层协议与网络服务的跨平台开源项目。本文以仓库根目录下的 AGENTS.md 为主线,完整梳理这个多语言 monorepo 的目录结构、双工作区(Cargo workspace + pub workspace)组织方式、必须牢记的构建与检查命令,以及 HTTP 服务、组播发现、Isolate 分层等核心架构设计,帮助你在动手修改任何模块前就建立起与 CI 一致的正确认知。
仓库布局:Flutter 应用骑在 Rust 协议实现之上
AGENTS.md 开宗明义:这是一个多语言 monorepo,"a Flutter app on top of a Rust protocol implementation"。各路径的职责如下表(继承自原文档并补充说明):
| 路径 | 内容 |
|---|---|
app/ |
Flutter 应用(localsend_app):UI、providers、持久化、平台通道 |
packages/localsend_isolates/ |
Dart isolate 层 + flutter_rust_bridge(FRB)绑定;内部拥有 rust/(Flutter 插件 crate rust_lib_localsend_app)与 rust_builder/(cargokit 构建器) |
packages/core/ |
Rust crate localsend:协议、HTTP 服务端/客户端、加密、WebRTC,不依赖 Flutter |
packages/typed_isolates/ |
小型独立包,用类型化的 send/receive 通道封装 Dart Isolate |
server/ |
供 WebRTC 使用的 Axum WebSocket 信令服务器(/v1/ws),独立部署,见 server/Dockerfile |
cli/ |
Rust CLI crate(localsend-cli):基于 packages/core(v2 HTTP + 组播)的交互式终端客户端 |
support/scripts/ |
发布/打包脚本(各平台构建、MSIX、Inno Setup、FOSS 剥离) |
从源码结构看,packages/typed_isolates 的入口 typed_isolates.dart 仅导出 isolate_helper、isolate_task、isolate_task_helper、isolate_task_result 四类符号,印证了文档中"小工具包"的定位。
双工作区:Cargo workspace 与 pub workspace
四个 Rust crate(packages/core、packages/localsend_isolates/rust、server、cli)构成以仓库根为根的单一 Cargo workspace:共享一份 Cargo.lock 与 target/ 目录。这一点可以从根 Cargo.toml 得到直接确认——[workspace] 段显式列出四个成员,并附带两条值得注意的工程决策注释:
[workspace]
resolver = "3"
members = [
"cli",
"packages/core",
"packages/localsend_isolates/rust",
"server",
]
# Profiles are only honored in the workspace root manifest.
# Full debuginfo dominates target/ size; line tables keep backtraces usable.
[profile.dev]
debug = "line-tables-only"
# RSA key generation is bignum-heavy and takes ~10x longer unoptimized;
# keep the crypto crates optimized in dev so tests stay fast.
[profile.dev.package.rsa]
opt-level = 2
[profile.dev.package.num-bigint-dig]
opt-level = 2
由此可提炼出三条硬性规则:
[profile.*]设置只写在根Cargo.toml,成员 crate 里的 profile 会被直接忽略——这也是根文件注释"Profiles are only honored in the workspace root manifest"的含义;debug = "line-tables-only"是为压缩target/体积而做的取舍(保留行表让 backtrace 可用);- 对
rsa与num-bigint-dig单独提升opt-level,因为大数运算在未优化状态下会慢约 10 倍,直接影响加密相关测试的速度。
需要注意的一个例外:Flutter 构建时,cargokit 仍会把插件 crate 编译进它自己的 target 目录,而非共享的 target/。
Dart 侧同样如此:三个包(app、packages/localsend_isolates、packages/typed_isolates)构成以仓库根为根的 pub workspace。根 pubspec.yaml 只有 9 行:
name: _localsend_workspace
publish_to: none
environment:
sdk: ^3.11.0
workspace:
- app
- packages/localsend_isolates
- packages/typed_isolates
共享一份 pubspec.lock 与 .dart_tool/,从任意成员执行 pub get 都会解析整个工作区。但 rust_builder 与 vendored 的 cargokit/build_tool 被刻意排除在工作区之外,仍独立解析依赖。
依赖方向
app → localsend_isolates → (typed_isolates, rust_lib_localsend_app → localsend core)
一个容易踩坑的约束:app 只依赖 localsend_isolates——不直接依赖 flutter_rust_bridge、typed_isolates 或插件 crate。做 import 时若发现需要绕过这一层直连底层,应视为架构违规信号。
Flutter 版本管理:用 fvm,别用系统工具链
仓库通过 .fvmrc 固定 Flutter 版本,该版本同时镜像在 CI 工作流与 app/pubspec.yaml 中,还有 support/submodules/flutter 这个 git submodule。因此所有命令都应使用 fvm flutter / fvm dart 而不是系统全局的 flutter / dart——CLAUDE.md 同样反复强调这一点,因为它只有一句话核心:"Always use fvm flutter / fvm dart, never the bare binaries",并把完整指引指向 AGENTS.md。
版本号一旦升级,需同步更新四处(.fvmrc、CI 工作流、app/pubspec.yaml、flutter submodule),具体步骤见 CONTRIBUTING.md 的 "Bump Flutter" 小节。
开发命令全集:与 CI 保持一致
以下命令除特别注明外都在 app/ 下执行,全部继承自原文档:
fvm flutter pub get
fvm dart run build_runner build # dart_mappable, freezed, flutter_gen, mockito
fvm dart run slang # i18n 代码生成(slang_build_runner 在 build.yaml 中被关闭)
fvm flutter run
CI 执行的检查项:
fvm dart format --set-exit-if-changed lib test # CI 会先删除 lib/gen;生成代码不做格式检查
fvm flutter analyze
fvm flutter test
fvm flutter test test/unit/util/security_helper_test.dart # 单文件
fvm flutter test --plain-name 'some test name' # 单条用例
两个格式细节直接影响代码评审体验:
-
行宽为 150 列,且
trailing_commas: preserve。可在 app/analysis_options.yaml 中确认:formatter: trailing_commas: preserve page_width: 150任何把生成代码重排成 80 列的工具都会制造纯噪音,事后要用
fvm dart format修正; -
CI 在格式检查前会先删掉
lib/gen,所以生成代码的格式状态不必纠结。
Rust 侧命令:
cargo test --features full # 在 packages/core 下 —— 见下文“Core crate features”
cargo clippy --features full
cargo check # 在 packages/localsend_isolates/rust、server、cli 下
FRB 代码生成在 packages/localsend_isolates/ 下运行,配置见 flutter_rust_bridge.yaml:
rust_input: crate::api
rust_root: rust/
dart_output: lib/rust
dart_format_line_length: 150
即输入是插件 crate 的 crate::api 模块,Dart 输出到 lib/rust,且生成代码沿用 150 列格式。有两个实战注意点:
- 代码生成器会把
app/test/mocks.mocks.dart重写成 80 列——如果这个文件出现在 diff 里,直接还原它; packages/localsend_isolates有自己独立的build.yaml,其 model 变化后需要在该包内单独再跑一次build_runner(pub get由工作区共享);CI 还会额外在packages/localsend_isolates/rust_builder/cargokit/build_tool里跑一次flutter pub get。
Core crate 的 feature 门控:永远加 --features full
packages/core/Cargo.toml 把几乎一切都藏在 Cargo feature 后面,且 default = []:
[features]
default = []
crypto = ["ed25519-dalek", "rcgen", "rsa", "sha2", "tokio-util"]
discovery = ["http", "multicast"]
http = ["crypto", "form_urlencoded", "http-body-util", "hyper", "hyper-util", "if-addrs", "pem", "percent-encoding", "reqwest", "rustls", "socket2", "time", "tokio-rustls", "tokio-util", "x509-parser"]
multicast = ["if-addrs", "socket2", "tokio-util"]
webrtc-signaling = ["tokio-tungstenite"]
webrtc = ["crypto", "flate2", "dep:webrtc", "webrtc-signaling", "x509-parser"]
full = ["crypto", "discovery", "http", "multicast", "webrtc"]
文档给出的结论必须严格执行:构建与测试 packages/core 时永远带 --features full。裸 cargo check / cargo build 会失败,原因是各模块被无条件声明而依赖却是可选的——这是历史遗留的既定状态,不是回归 bug,排查时不要误判。
架构:状态管理、Isolate 分层与 Rust 网络栈
状态管理:Refena,不是 Riverpod
应用使用 refena_flutter 管理状态。providers 位于 app/lib/provider/:
- 普通状态用
NotifierProvider; - 凡是 isolate 层会触碰的状态,一律用
ReduxProvider+ 分发的 action 类; - app/lib/config/init.dart 中的
preInit是启动引导:依次初始化日志、RustLib.init()、持久化、isolate 容器、tray/window,最终返回main.dart挂载用的RefenaContainer。
数据模型层有两套代码生成:
dart_mappable(@MappableClass+.mapper.dartpart 文件,仓库中如app/lib/model/state/settings_state.mapper.dart等可见其产物),并且方法被重命名:fromJson/toJson是 Map 转换器,deserialize/serialize才是字符串转换器(在两个build.yaml中配置);- Freezed 用于 FRB 侧的 union 类型。
Isolate 分层:重网络永不跑在主 isolate
packages/localsend_isolates/lib/src/isolate/ 的职责划分非常清晰:
parent/parent_isolate_provider.dart——ParentIsolateState为每个子 isolate(http 扫描发现、组播发现、http 上传、http 服务器)各持有一个IsolateConnector,外加一份镜像进所有子 isolate 的SyncState;IsolateSetupAction负责生成它们;parent/actions.dart、parent/actions_sync.dart—— 这是应用与子 isolate 通信的唯一受支持方式;child/*_isolate.dart—— 子 isolate 入口,把类型化任务消息翻译为对lib/src/task/的调用;lib/src/task/—— 只放纯工具函数。目录内 README.md 原文只有两行,但态度坚决:"All files in this directory should only call utility functions. Isolate logic is prohibited in this directory."(本目录禁止出现任何 isolate 逻辑)
一条时序规则值得警惕:子 isolate 需要的状态(alias、端口、协议、服务器是否运行、web send 是否开启)通过 IsolateSyncServerStateAction 推送,而子 isolate 是在启动时读取 syncState 的——所以必须先同步、后启动服务器。
Rust 网络栈:HTTP 服务器与事件通道
HTTP 服务端与客户端都写在 Rust 里,位于 packages/core/src/http/server/:
- 实现协议 v2(不提供 v1 端点),外加 "web send" 下载流(
server/web.rs)以及一个内部show端点,用于把已运行的实例拉到前台; - 集成是基于 channel 的:
start_with_port接收ServerConfigV2 { pin, event_tx, web_send },向外发出ServerEventV2事件——Register、携带decision_txoneshot 的PrepareUpload、携带字节流与result_tx的FileUpload、PrepareDownload、SessionEnd、PrepareUploadAborted、CancelReceived。
设计约束逐条展开:
- 同一时刻只有一个上传会话处于活动状态;取消安全性由 drop 守卫(
PendingSessionGuard、UploadGuard、PendingWebSessionGuard)保证,即会话随 guard 析构而自动清理; - core 中故意没有
auto_accept——自动接受是由应用层立即回答decision_tx实现的。这个设计把"策略"(是否接受)留在 Dart 应用层,core 只提供"机制"; - 新增服务器→应用交互应扩展
ServerEventV2,而不是另开旁路通道。
FRB 层(packages/localsend_isolates/rust/src/api/server.rs)暴露 start_server 与一个不透明句柄 RsHttpServer,其 listen 把 v2、web-send、internal 三条事件流合并为一条 RsServerEvent 流;responder oneshot 保留在 Rust 侧。Dart 侧 child/server_isolate.dart 将其转为 HttpServerEvent,再由 app/lib/provider/network/server/server_provider.dart 路由到 ReceiveController / SendController——注意它们是事件处理器,不是路由处理器。
其余几个容易踩坑的事实:
- 保存目标在 Dart 端决定(
prepareFileSaveTarget),实际写入由 Rust 执行:要么是一个路径,要么是通过org.localsend.localsend_app/localsendmethod channel 获得的 Android SAF 文件描述符;保存进相册的文件先经过缓存文件中转; - 服务器事件里的
ip是PeerIp(IP + IPv6 scope),链路对端会显示为fe80::1%3这类带 scope 的形式,而 HTTP 客户端也接受它作为 host 回拨——这样事件中的 ip 保持可直连(dialable); - TLS 采用每设备即时生成的证书 + 强制客户端证书(浏览器访问 web 页面时证书可选,保证浏览器可连);对端身份是客户端证书 DER 的大写十六进制 SHA-256,若 payload 声明的指纹与证书不符,
Register事件干脆不发出。代码中应优先取event.certFingerprint ?? event.info.fingerprint——payload 回退只服务于关闭加密的模式; - 接收 pin 与 web-send pin 都在服务器启动时固定,改任何一个 pin 都会重启服务器;
- 浏览器下载页的 web 资产内嵌自 packages/core/assets/web/(
download.html、upload.html、error-403.html)。
组播发现:UDP 只负责宣告,应答走 HTTP
packages/core/src/multicast/(feature multicast,与 http 独立)实现协议 v2.2 的 UDP 组播发现,v1 消息不再解析。集成方式与 HTTP 服务器对称:multicast::start 接收 MulticastConfig { group, group_v6, port, interface_filter, device, event_tx },发出 MulticastEvent::Discovered { ip, message },返回的 MulticastHandle 提供 announce(宣告突发)与 wait_stopped。
关键设计点:
- UDP 只做宣告(announce-only):应答通过 HTTP 以单播 register 请求发回宣告方;
- 每个接口的 IPv4 地址各绑定一个 socket(
SO_REUSEPORT/SO_REUSEADDR+IP_MULTICAST_IF),因为单个 socket 只能从一个接口发送; - 组播回环(loopback)保持开启,让同一主机上的多个实例能互相发现;自己的消息由指纹识别后丢弃;
- IPv6 是 LocalSend 的扩展实现(组播组
ff12::fd3a:e420,常量DEFAULT_MULTICAST_GROUP_V6),通过设置group_v6启用:每个接口一个IPV6_V6ONLYsocket,按接口索引加入;Discovered事件携带源端的 scope ID(接口索引),链路本地 IPv6 源回 HTTP 应答时需要它。
i18n 与 FOSS 构建
i18n:Slang + Weblate
翻译源文件在 app/assets/i18n/(<locale>.json 加 _missing_translations_<locale>.json,仓库中可见 60+ 语言文件),生成输出在 app/lib/gen/。翻译托管在 Weblate;以 @ 开头的字段是面向译者的元数据,应用本身不使用。app/test/unit/i18n_test.dart 守护语言集合的完整性。
FOSS 构建:靠注释标记剥离
F-Droid 版由 support/scripts/remove_proprietary_dependencies.sh 剥离 in_app_purchase 与捐赠 UI,机制完全依赖三处标记:
pubspec.yaml中依赖行的# [FOSS_REMOVE]注释(脚本用sed '/# \[FOSS_REMOVE\]/d'整行删除);- Dart 文件中的
// [FOSS_REMOVE_START]/// [FOSS_REMOVE_END]注释对(脚本将其改写为/*与*/,形成块注释); lib/provider/purchase_provider.dart被整文件删除,且donationPageVmProvider被替换为donationPageNoopVmProvider。
因此编辑 app/lib/config/init.dart、app/lib/pages/donation/*、app/lib/provider/purchase_provider.dart 时必须保留这些标记,否则 FOSS 构建会损坏。
发布注意事项:三处版本号必须一致
app/pubspec.yaml 的 version 必须与 support/scripts/compile_windows_exe-inno.iss 中的 #define MyAppVersion 以及 cli/Cargo.toml 的 version(CLI 在启动横幅中打印它)保持一致——CI 对不一致直接判失败。各平台构建命令与发布流程详见 README.md 的 "Building" 小节与 CONTRIBUTING.md 的 "Release" 小节。
小结
AGENTS.md 的核心信息可以浓缩为五件事:
- 记住双工作区(Cargo + pub)与四条依赖方向,改 profile 只改根
Cargo.toml,改依赖先想清楚该落在哪个 crate/包; - 全程
fvm,格式 150 列,生成文件(lib/gen、mocks.mocks.dart)的格式噪音要手动清理; - 碰
packages/core就带--features full,裸编译失败是预期行为; - 网络事件只走
ServerEventV2/ 合并后的RsServerEvent单流,策略(如 auto-accept)留在 Dart 应用层,isolate 逻辑禁止下沉到task/; - 动到捐赠、内购相关文件或准备发版时,核对 FOSS 标记与三处版本号。
按这套约束工作,你的本地构建、检查与 CI 的行为会保持完全一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00