纯局域网内同步笔记的跨平台应用:手机随手记,电脑自动同步,不依赖公网服务器。 一套 Flutter 代码覆盖 iOS / Android / Windows / macOS。
个人开发者场景:手机和电脑处于同一 WiFi/局域网时,笔记自动或手动同步,数据只存在于局域网内设备,不经过任何第三方服务器。
核心功能
- 笔记增删改查、搜索、回收站(删除进回收站,可恢复/清空)、本地持久化(SQLite / drift)
- 局域网内设备发现(「可被发现」广播 30s + 「扫描设备」搜索 30s,手动互联)
- WebSocket 点对点同步:握手 → 全量对齐 → 增量推送(增/改/删)
- 自动同步 + 手动「立即同步」,冲突按「版本号 + 最后写入时间」合并(LWW)
- 断线重连(重新上线方凭地址缓存自动直连),重连后全量对齐、数据无丢失
- P2P 对等拓扑:每台设备既是服务端又是客户端,多台设备全互联 mesh
Releases 页提供 macOS / Windows / Android 三端构建产物(打 v* tag 时由 GitHub
Actions 自动构建并上传):https://github.com/tianemon/lan-notes/releases
| 平台 | 产物 | 说明 |
|---|---|---|
| macOS | EasyNote-macos.zip |
解压后把 EasyNote.app 拖进「应用程序」 |
| Windows | EasyNote-windows.zip |
解压后运行 lan_notes.exe(需整个目录,不能只取 exe) |
| Android | app-release.apk |
直接安装到手机 |
关于签名提示:自动构建的产物未做商业代码签名,首次运行需要手动放行一次:
- macOS:应用为 ad-hoc 签名(未经 Apple 公证)。双击会被 Gatekeeper 拦下, 选「右键 → 打开」即可;或到「系统设置 → 隐私与安全性」里点「仍要打开」。
- Windows:无 Authenticode 签名,SmartScreen 会弹蓝色窗口,点「更多信息 → 仍要运行」。
- Android:release 包使用 debug 签名,仅供自行安装,不能上架应用市场。
- Flutter 3.44.9 / Dart 3.12(版本在 pubspec.yaml 的
environment.flutter锁定,CI 与本机一致) - Windows 桌面:Visual Studio 2019+(C++ 工具链)
- Android:Android SDK(API 35+);构建 APK 需要能访问 Maven 仓库
- iOS / macOS:需要 Mac 环境(本仓库在 Windows 上以 Windows + Android 验证)
flutter pub get # 安装依赖
flutter pub run build_runner build # 生成 drift 代码(改表结构后需重跑)
flutter run -d windows # Windows 桌面版
flutter run -d <android-device> # Android 版(需设备/模拟器)
flutter analyze # 静态检查(必须 0 issues)
flutter test # 单元测试flutter build windows # Windows Release 版
flutter build apk --debug # Android debug APK已验证:
flutter analyze0 issues、flutter build windows成功、flutter build apk --debug成功(详见下方已知限制中 Android 运行时说明);flutter build macos --release成功(task-23 在 macOS 环境验证)。
flutter test # 单元/组件/安全回归(54 项)
flutter test --run-skipped --tags manual test/verify_sync_runner_test.dart # 驱动真实网络栈的 97 场景全链路(temp/drafts/verify_sync.dart 包装)覆盖:本地 CRUD、UDP 广播发布与发现、握手/全量/增量/LWW 冲突、回收站(软删/恢复/清空同步 + 墓碑防复活 + 时间裁决)、断线重连自动对齐、多向广播 fan-out、请求-同意配对(HMAC 挑战认证,无密码)、P2P 自动连接、设备 ID 冲突防护与重置身份、图片跨设备同步(附件自动请求 → 分片传输 → sha256 校验落盘,verify_sync.dart 场景二十)。
v2 起为 P2P 对等模式:每台设备完全平等,同时担任服务端(固定端口 58888 监听)+ 客户端(UDP 广播发现,task-24 起)。设备身份凭持久化 deviceId(首次启动生成,重启 不变),不依赖 IP。
- 两台设备都打开同步:进入「同步」页,打开顶部同步开关——本机只监听 固定端口 58888(开启 WebSocket 服务端),不持续广播(互联手动化)。
- 可被发现 + 扫描设备:让被发现的设备点「发现的设备」卡中的**「可被发现」** (向外广播 30 秒,每 5 秒一次);另一台点**「扫描设备」**(搜索局域网 30 秒), 找到目标设备(未配对设备标「未配对」)。
- 点设备发起请求:点击目标设备 → 向对方发送配对请求。
- 对方同意:对方任意页面弹出「xx 请求连接」→ 点「同意」即配对成功 (双方写入信任列表并交换 HMAC 认证密钥);点「拒绝」则请求方收到 被拒提示。
- 自动连接:配对成功后双方互信,之后只要在同一局域网内打开同步, 重新上线方就会凭地址缓存自动直连已配对设备、自动全量对齐(重连责任在 重新上线方,在线方不主动重连)。
配对弹窗为全局弹窗:被动收到配对请求时,无论当前在哪个页面都会弹出 同意/拒绝确认框;多台设备同时请求时按队列依次展示。
- 自动同步:打开 App 自动开启同步(默认开启,可在「设置 → 启动时自动同步」 关闭),无需手动点开关;连接建立后任一端增删改笔记,其他已连接设备 2 秒内 自动收到;3 台及以上设备时任一设备的新变更会经已连接对端多向广播到所有设备。
- 后台自动恢复:App 从后台回到前台时,若同步被系统关闭(安卓后台省电限制/ 进程挂起)会自动重新开启并恢复发现/连接;手动关闭同步开关后回到前台不会 自动打开(尊重用户操作,重启 App 后按配置恢复)。
- 手动同步:无「立即同步」按钮——任一对端会话就绪即自动触发一次全量对齐(同步页顶部「同步中」转圈指示)。
- 可被发现 / 扫描设备:互联手动化——「发现的设备」卡点「可被发现」 向外广播 30 秒(每 5 秒一次)供对方扫描;点「扫描设备」搜索局域网 30 秒 (窗口结束列表定格)。常态不广播不扫描,已配对设备开同步后凭缓存自动直连。
- 已配对设备:同步页「已配对设备」列表显示每台对端的设备名与连接状态 (已连接 / 连接中 / 已断开),每台设备行有手动连接(上)与自动连接(下) 两个开关:手动连接开关关闭 = 断开(当下不自动重连),打开 = 手动重连; 自动连接开关控制上线自动连,关闭后在线方会拒绝该设备的连接(被拒方自动 关闭自己的自动连接开关)。
- 冲突处理:同一笔记被多端修改时,按「版本号大者胜;版本号相等时最后写入 时间新者胜」合并,采用远端后版本取两者较大值(单调递增),最终两端一致。
- 断线重连:连接中断后在线方不主动重连——重新上线方(开启同步 / 回前台) 凭地址缓存直连已配对设备自动重连并全量对齐;换 IP 后旧缓存失效时,手动 「扫描设备」发现新地址后自动重连。
- 设备名:同步页右上角设置图标,修改后立即生效(设备名同步刷新 UDP 通告)。v4 起无连接密码——配对改为请求-同意 + HMAC 认证。
- 启动时自动同步:设置弹窗内开关(默认开)。开启时打开 App 自动开启同步, 无需手动点开关;从后台回到前台时若同步被系统关闭自动恢复。关闭后仅手动 开启同步(同步页顶部开关),重启 App 也不自动开启;手动关闭同步开关后回到 前台不会自动打开(重启 App 后按此配置恢复)。
- 重置设备 ID:设置弹窗底部。重新生成 deviceId 并清空全部配对关系—— 用于修复设备 ID 冲突:检测到与本机相同 deviceId 的设备(常见于从备份 恢复数据导致两台设备身份相同)时,同步页会置顶提示「设备 ID 冲突」,重置 其中一台的 ID 后,两台设备身份不同即可正常配对连接;旧配对关系需重新请求 配对。
v4(task-27)起配对与认证协议变更,所有设备需升级到同一新版本:
- 配对去密码:v3 及以前的「设备密码 + 输入验证」废弃——v4 改为请求-同意 (点击设备 → 对方弹窗同意/拒绝),无密码设置/输入;
- HMAC 挑战认证:配对时交换随机密钥(
trusted_devices表新增secret), 每次连接握手经 challenge/challenge_response 验证真身(防伪装 deviceId); - 协议版本升级:通告(UDP JSON
v:3)与 hello 携带协议版本 3—— v2 及以下旧协议设备不互连(提示「协议版本不兼容,请升级应用」); - 旧配对迁移:v3 已配对的设备升级后信任列表仍在但无密钥——首次连接 按未配对处理,需重新走一次请求-同意配对(之后自动连接恢复);
- 取消配对双边解除:任一方取消,对端同步移除信任并断开(不再单侧残留)。
v5(task-29)起笔记正文改为富文本(flutter_quill),所有设备需升级到同一新版本:
- 正文编辑:编辑页正文由纯文本输入框换为 QuillEditor(所见即所得),工具栏支持 加粗 / 斜体 / 下划线 / 标题 / 列表 / 引用 / 代码块 / 清除格式 / 本地插图;
- 内容格式:
notes.content存 delta JSON(Quill 文档序列化)——存量纯文本笔记在 升级时自动转换为 delta(首次打开旧库自动迁移,无需手动操作);列表摘要/字数按纯文本提取; - 同步协议 v3:通告(UDP JSON
v:3)与 hello 携带协议版本 3——v2 及以下旧版本 (纯文本 content)设备不互连(提示「协议版本不兼容,请升级应用」),需全员升级; - 本地插图:插入的图片压缩后存本机(应用支持目录
attachments/),delta embed 节点 引用相对路径——协议 v4(task-30)起图片跨设备同步(见下方升级说明),全部设备 升级到 v4 后插图自动互传(此前仅本机可见,对端显示占位提示); - 存量旧库自动迁移:首次启动旧版数据库时 schemaVersion 7→8 自动执行,纯文本 content 逐行转为 delta,原有笔记内容不丢失。
协议 v4(task-30)起笔记内图片跨设备同步,所有设备需升级到同一新版本:
- 图片自动互传:一端插入的图片(
attachments/<hash>.jpg)随笔记引用自动同步—— 对端收到含图笔记后检测本地缺失附件 → 自动请求 → 分片传输(64KB,WebSocket 保序)→ sha256 校验落盘 → 编辑器图片自动刷新(无需手动操作); - 校验与容错:分片传输完成后校验内容 hash,数据损坏自动丢弃并可重试;断线中断的 传输在重连后自动重新请求;单文件上限 20MB(超限拒绝);
- 协议版本升级:通告(UDP JSON
v:4)与 hello 携带协议版本 4——v3 及以下旧版本 (无图片同步协议)设备不互连(提示「协议版本不兼容,请升级应用」),需全员升级; - 增量同步不受影响:文本/回收站/墓碑的同步协议(v3 合并规则)不变,图片仅在笔记 合并后按需补齐(不阻塞笔记同步)。
协议 v6(task-32)起新增文件夹归类(文件夹增删改/置顶/排序 + 笔记归属 跨设备同步),所有设备需升级到同一新版本:
- 文件夹跨设备同步:新建/重命名/删除文件夹、置顶、拖拽排序随
folder_upsert消息实时同步;删除文件夹二选一(笔记移到全部 / 同时 删除笔记)同样同步到所有设备; - 笔记归属同步:移动笔记到文件夹 / 拖拽批量移动随
note_upsert(携带folderId)同步; - 协议版本升级:通告(UDP JSON
v:6)与 hello 携带协议版本 6—— v5 及以下旧版本(无文件夹协议)设备不互连(提示「协议版本不兼容, 请升级应用」),需全员升级; - 升级无损:存量笔记升级后均为未分类(
folderId为空),文件夹 功能从空开始使用,笔记数据不受影响。
- 未开启同步时,本地笔记功能完全可用,数据离线保存,联网后自动/手动同步。
- 同步仅限同一局域网:跨网段或公网不可用(设计如此)。
删除机制自 v3 起改为「回收站 + 墓碑」:删除不再是立即物理删除,而是先移到 回收站(内容保留、跨设备同步),确认后清空才物理删除。
- 删除:列表长按笔记 / 编辑页删除按钮 →「移到回收站」,笔记从列表移除、内容保留,其他已连接设备同步进入回收站。
- 恢复:列表页右上角垃圾桶图标进入回收站,点击「恢复」回到列表,跨设备同步恢复。
- 清空:回收站支持清空单条(🗑)与清空全部(顶部按钮),均需二次确认;清空 = 物理删除 + 写墓碑,不可恢复,并同步到其他设备(物理删除 + 防复活墓碑)。
- 离线防复活:清空后即使离线设备带回旧数据,重连时也会被墓碑拦截,不会复活;离线设备在清空之后的新修改(时间裁决)仍会正确保留并同步回其他设备。
回收站依赖 deletedAt 字段与墓碑协议,需要所有设备升级到支持回收站的版本:
- 旧版设备不识别
deletedAt/墓碑:旧设备上的「删除」仍按旧语义硬删除广播(新设备收到后按清空处理);新设备上的「移到回收站/恢复」旧设备不会生效(旧设备会当作普通内容更新合并)。 - 升级过渡期内删除同步受限:建议尽快将全部设备升级到同一新版本;全部升级后删除 / 恢复 / 清空全链路一致。
发现层自 task-24 起由 mDNS 改为 UDP 广播(参考 Syncthing 周期广播 + LocalSend 上线三连发/回播):每台设备在 UDP 58888 端口周期广播 JSON 通告
{v:1, deviceId, name, port, ts},同网段设备零配置互相发现。
需要所有设备升级到新版:
- 旧版(mDNS)与新版(UDP)互不可见——发现协议完全不同(mDNS 多播 5353 vs UDP 广播 58888),新旧版本设备无法互相发现,也就无法自动连接/同步;
- 升级过渡期内请勿混用:全部设备升级到同一新版本后再恢复日常同步;
- 已在旧版本完成配对的设备,升级后凭持久化 deviceId 与信任列表无需重新配对 (配对关系与发现协议无关)。
- iOS / macOS:macOS 桌面端已在本仓库 macOS 环境验证(
flutter build macos --release成功,EasyNote.app 44.7MB);iOS 需 Mac + Apple 开发者证书签名,未验证。 - Android 运行时 SQLite:APK 已打包
libsqlite3.so(arm64-v8a / armeabi-v7a / x86_64,来自eu.simonbinder:sqlite3-native-library:3.52.0AAR,放入android/app/src/main/jniLibs/,构建离线);flutter build apk --debug后unzip -l已验证存在。真机运行时验证仍待补(本机无 Android 设备/模拟器;dlopen 命中 jniLibs 为 Android 标准机制)。如需升级 SQLite 版本,从 Maven 镜像 (如 mirrors.cloud.tencent.com)拉取新版 AAR 替换 jniLibs 即可。 - UDP 广播实测结论(macOS/Windows):
- 同机多实例发布 + 发现已实测通过(macOS 需
SO_REUSEPORT支持多 socket 同端口, Windows 不支持SO_REUSEPORT但SO_REUSEADDR下广播投递给全部同端口 socket; 代码按平台自动降级)。上线三连发、新设备回播、周期通告维持在线、离线移除 均由 verify_sync.dart 场景十三/十四覆盖(真实 UDP,93 PASS)。 - UDP 广播依赖网段路由:不同网段 / 隔离 AP(访客网络)/ 开了「AP 隔离」的
WiFi 可能无法发现设备;广播地址按私有网段惯例推导(
10/8、172.16/12、192.168/24),非标准网段由255.255.255.255兜底。 - 无需 MulticastLock / Bonjour:Android WiFi 省电只过滤多播包,广播不受影响。
- 同机多实例发布 + 发现已实测通过(macOS 需
- 同步端口:固定 58888(可配置,UDP 广播发现与 WebSocket 同步同端口号, UDP/TCP 独立不冲突);对端重启 / IP 变化后通过重新 UDP 广播发现自动重连。
- 同步仅限同一局域网:跨网段或公网不可用(设计如此)。
- 冲突策略:个人单写者场景采用 LWW,极端并发编辑以「版本 + 最后写入时间」为准。
- Flutter 3.44 / Dart 3.12,Material 3 flat 风格
- drift(SQLite)本地持久化,Riverpod 状态管理,go_router 路由
- 自研 UDP 广播发现(JSON 通告,端口 58888;参考 Syncthing/LocalSend),web_socket_channel(WebSocket 同步)