Repository navigation
Releases
v1.10.4:局域网发现 + 邀请加入
本次更新
局域网发现:不必再手填主机地址。从机在同步页点「搜索局域网」,主机自己应答,列出名字、地址与是否已配对,点一下填入地址。
邀请加入:主机也能反过来邀请设备。设备收到邀请弹一次确认框;加入 = 写地址(密钥指纹不一致时要求填共享密钥)+ 开定时自动同步 + 立刻同步一轮;忽略则记进偏好,同一台不再打扰。
变更
- App:同步页新增「搜索局域网」;主机模式开着时后台应答别人的探测(报出端口与密钥指纹)
- App:应用根上挂一次性邀请确认框(邀请可能在任何页面到达)
- 中心(
server/):新增--discover、--invite IP|DEVICE_ID、--invite-all、--no-discovery;配置项discovery(默认开)、invite_on_discovery(默认关) - 协议:UDP 8766,三种报文
probe/hello/invite;只收私网来源、报文上限 1 KB、同来源 2 秒限速、kn不为 1 丢弃 - 安全:发现报文不含共享密钥,只带
key_hint = sha256(key + "\n" + device_id)前 8 位(判断「是不是我配对过的那台」);真正的把关仍是/sync上的共享密钥 + 用户那一次点击 - 修复:同步地址解析接受从 Termux
ip addr粘来的192.168.1.29/24这类带掩码写法(先前整串当主机名,必失败) - 修复:探测同时发有限广播 + 组播组 + 每张网卡的定向广播(只发
255.255.255.255时多网卡主机上出不去、零应答) - 修复:发现监听 socket 曾因
apply作用域里port取到自身getPort()(未绑定为 -1)而从未绑上
验证
| 项目 | 结果 |
|---|---|
| 中心测试 | 75/75(PC 与 Termux 真机同数) |
| App 单元测试 | 76/76 |
| 仪器化测试 | Android 13 64/64 |
| 真机发现 | PC 问一圈答出手机上的中心(角色 hub、端口 8765、指纹);开着主机模式的安卓机回角色 app 并报出自己地址 |
| 邀请弹窗 | 真机弹出一次窗(标题带主机名、正文给主机地址、指纹不一致则要求填共享密钥、按钮 忽略/加入);点「忽略」后落盘,再发一次不再弹 |
升级
- App:覆盖安装即可,数据保留
- 中心:替换
knownote_hub.py后重启(bash start-hub.sh restart);devices与密钥不用改 - 端口:发现走 UDP 8766,同步仍是 TCP 8765;不需要新增任何权限(广播无需运行时权限)
- 限制:广播与组播不跨网段;开了客户端隔离(AP isolation)的网络里发现会失败,手填地址仍然可用
v1.10.3 · 修「自动同步从不触发」
1.10.3 · 修「自动同步从不触发」
修复
- 症状:手机上开着「定时自动同步」,界面上的时间永远停在打开开关那一刻,之后一次也不跑
- 根因一(设备侧):周期任务要求
UNMETERED(不计费网络),而手机热点、被系统标成计费的 Wi-Fi 全都不满足 ——
任务长期停在「等约束满足」。两台设备的 WorkSpec 都是period_count = 0,
JobScheduler 报Unsatisfied constraints: CONNECTIVITY,界面上却什么都看不出来 - 根因二(失败退避):失败时返回
retry(),指数退避会把下一次执行推到几小时以后 - 根因三(中心侧):配置里的设备地址是样例值
192.168.1.7,不在本机网段,
于是「中心主动轮询」520 次全部超时,日志里全是同一条 - 修复:设备侧去掉网络类型约束;失败一律不退回退(周期任务到下个周期自己再来);
卡片显示后台任务状态与超期提示;中心在配置地址连不上时改用该设备最近接入过的地址
变更
- 设备侧不再限制网络类型:局域网直连本就不产生流量费用,该拦的是「对端不在同一个局域网」,
由LanGuard/OffLan负责(会说明原因) - 同步卡片可诊断:新增「后台任务:已排程(每 N 分钟)/ 正在执行 / 未排程」一行;
距上次执行超过两个间隔时提示可能被省电或后台限制压住 - 跨网段失败说人话:对端与本机不在同一网段时,失败原因写成两边各自的网段,
而不是系统的failed to connect to /192.168.1.7 ... after 4000ms。
判定只在能确定时下结论(域名、枚举不到本机网卡时不猜) - 中心地址自学习:配置地址连不上 → 按「该设备最近接入过」的地址依次试(最多 3 个):
上次成功同步记下的自称名优先,其次配置名一致,再次唯一候选,都对不上就按最近接入顺序试
(配置里写「Y700」、设备自报「LENOVO TB322FC」这类名字对不上的情况就靠这一档;候选都是已配对过的自家设备) - 配置地址不在本机网段时直接跳过它(不白等一次连接超时),启动横幅与
--status点出来;
每轮试了哪些地址、最后连上谁,日志都写明;全部失败时错误里列出试过的每个地址 - 中心日志节流:同一台设备同一原因连续失败,只在第 1~3 次与之后每 12 次(约 1 小时)记一条,
原因变了立刻记(v1.10.2 在 20 小时里攒了 500 多条一模一样的超时)
安装
| 文件 | 说明 |
|---|---|
KnowNote-1.10.3-debug.apk |
Android App。debug 签名,versionCode 17 / versionName 1.10.3,minSdk 26 / targetSdk 36(Android 8.0+)。覆盖安装不丢数据。四语言:简中 / 繁中 / English / 日本語 |
knownote-hub-1.10.3-termux.zip |
同步中心,纯 Python 3 标准库:零依赖、零编译,Termux 下 pkg install python 即可运行,不需要 root;也可跑在 PC 上(start-hub.bat) |
unzip knownote-hub-1.10.3-termux.zip && cd knownote-hub-1.10.3
python3 knownote_hub.py --init-config hub.conf.json # 生成配置;共享密钥留空会拒绝启动
./start-hub.sh # 前台;后台用 ./start-hub.sh bg协议未变(仍是 3),App 与中心可以分开升级:先升级哪一端都不会互相回 426。
App 侧:「更多 → 局域网同步」填中心地址与同一串共享密钥,即可作为从机同步;也支持 App 当主机、中心反向拉取。
默认只在局域网内工作(两端都拦):非私有来源一律 403,手机在移动数据上会被 App 自身拦住。
测试
开发期在本机与真机上跑出的结果;测试代码不在仓库与压缩包内。
| 项 | 结果 |
|---|---|
| 定位证据 | 两台设备 WorkSpec period_count = 0、JobScheduler Unsatisfied constraints: CONNECTIVITY、当前网络 Metered hint: true;中心 520/520 次轮询超时、last_ok_at = 0 |
| App 单测 | 65/65(新增 LanSubnetTest 3 例:按真实前缀长度比网段、判不了时不误报、说明里带上两侧网段) |
| App 仪器化(Android 13 模拟器) | 59/59(4 例新增 / 改写:无网络类型约束、任务状态可查、跨网段按 skipped: 记并写明原因、失败不再要求退避) |
| 同步中心(PC) | 60/60(新增 10 例:网段判定、本机网段解析、候选按最近接入排序、上限 3 个、不抢别的设备占用的地址、逐个往下试到连上、全失败时列出试过的每个地址、无历史时不猜、自称名对齐、失败日志节流) |
| 同步中心(真 Termux:Android 11 设备,Python 3.14.6 / arm64) | 60/60(22.7 秒) |
| 真机实测(计费热点) | 两台测试机当时都是 Metered hint: true —— 原缺陷的触发条件。装上 1.10.3 并启动一次后,系统里的任务约束只剩 TIMING_DELAY(原为 TIMING_DELAY CONNECTIVITY 且要求 NOT_METERED);触发后两台都 sync_auto_last_ok = true(一台推 36 条、一台拉 1 条) |
| 地址自学习 | 单测覆盖候选排序 / 上限 / 不抢别的设备的地址 / 逐个往下试到连上 / 全失败时列出每个试过的地址;真中心升到 1.10.3 后配置仍留着样例地址(跳过它,按最近接入顺序试三台曾接入的设备,日志逐条写明) |
隐私
仓库只放程序源码(开发脚本、单元 / 仪器化测试、演示数据脚本均不入库,历史中一并移除);
发布附件与说明均已脱敏:示例地址、示例域名、通用设备描述;仓库内无任何密钥,样例配置的 key 为空串。
许可
MIT。
v1.10.2 · 同步改为双向库存比对(协议 3)
1.10.2 · 同步改为双向库存比对(协议 3)
修复
- 症状:老设备连新对端时只同步到零星几条笔记
- 根因:增量由水位线
last_sync_at推断,而水位线的语义是「与上一个对端同步到哪」——换对端后算出的增量为空
变更
- 每轮请求与响应都带全量库存:
guid+updated_at+ 墓碑位 + 内容指纹(FNV-1a 32 位),不含正文 - 两端跑同一个纯函数算差集(Kotlin
SyncDiff与 Python 中心逐位一致):对方没有 / 我更新 / 打平但指纹不同 → 我发;我没有 / 对方更新 → 我要;两边相同(含双方持有的同一枚墓碑)→ 不动 - 正文只在被点名时传输:对方在响应里用
want_guids列出所需 guid,下一轮按 guid 发送正文 - 轮数:无差别 1 轮,比差集 + 发正文 2 轮,有上限兜底
last_sync_at降级为「省一轮往返的快路径 + 界面显示」,正确性不再依赖它- 同批修复:协议 3 的
inventory/want_guids只加进请求解析、漏在响应的from_json(症状是每轮重传整库,日志显示 5 轮);补「线结构体往返」与「轮数断言」两条回归
升级顺序(协议 2 → 3 为断点)
- 先装新 APK(覆盖安装,数据不动)
- 再重启同步中心(旧进程仍在说协议 2)
v1.10.1 的 App 与协议 3 的中心互回 426 Upgrade Required。
附件
| 文件 | 说明 |
|---|---|
KnowNote-1.10.2-debug.apk |
Android App。debug 签名,versionCode 16 / versionName 1.10.2,minSdk 26 / targetSdk 36(Android 8.0+)。覆盖安装不丢数据(包名、签名、数据库 schema 均未变)。四语言:简中 / 繁中 / English / 日本語 |
knownote-hub-1.10.2-termux.zip |
同步中心,纯 Python 3 标准库:零依赖、零编译,Termux 下 pkg install python 即可运行,不需要 root;也可跑在 PC 上(start-hub.bat) |
运行(Termux)
unzip knownote-hub-1.10.2-termux.zip && cd knownote-hub-1.10.2
python3 knownote_hub.py --init-config hub.conf.json # 生成配置;共享密钥留空会拒绝启动
./start-hub.sh # 前台;后台用 ./start-hub.sh bgApp 侧:「更多 → 局域网同步」填中心地址与同一串共享密钥,即可作为从机同步;也支持 App 当主机、中心反向拉取。
默认只在局域网内工作(两端都拦):非私有来源一律 403,手机在移动数据上会被 App 自身拦住不发请求。
测试
开发期在本机与真机上跑出的结果;测试代码不在仓库与压缩包内。
| 项 | 结果 |
|---|---|
| App 单测 | 62/62 |
| App 仪器化(Android 13 / 15 模拟器各一遍) | 57/57 |
| 同步中心(PC) | 50/50 |
| 同步中心(真 Termux:Android 11 设备,Python 3.14.6 / arm64) | 50/50 |
| 真机跨实现:Termux 中心 ⇄ PC 中心(协议 3) | 空库第一轮拉 3 条 → 稳态 0/0(1 轮)→ 新建一条后推 1 条(2 轮) |
| 真机跨实现:PC 中心 ⇄ 模拟器上的真 App(协议 3) | App 点名要 2 条并落库(直接读 App 数据库核实),稳态 0/0 |
| 两端逐位一致处 | 指纹固定值 + 一整串黄金 JSON,Kotlin / Python 两侧各钉一半 |
隐私
仓库只放程序源码(开发脚本、单元/仪器化测试、演示数据脚本均不入库,历史中一并移除);发布附件与说明均已脱敏:示例地址(192.168.1.x / 203.0.113.7)、示例域名(example.com)、通用设备描述;历史旧提交一并重写。仓库内无任何密钥,样例配置的 key 为空串。
许可
MIT。
v1.9.0 · 图片参与局域网同步
1.9.0 · 图片参与局域网同步
协议升到 2(两端须同版本)
旧版主机遇新版从机回 426 Protocol version mismatch。云端无数据,两台设备版本不一致只导致同步不通,不会损坏数据;两端各装本版本即可。
两个设计取舍
- 图片不绑在笔记变更日志上:否则升级后首次同步会因「笔记一条未变」而永远传不过去。判据改为文件级存在性(本机有、对端集合中无即传),与变更日志无关,可反复重试
- 谁缺什么由对端当次广播(
images_i_have),服务端不保存任何对端状态;本地只把「对端已有图片」当缓存(key 带host:port,多台对端各存各的),以缓存换掉一次数据库迁移
传输
- 图片与笔记在同一次请求内走(base64 承载),不新增端点、不额外鉴权
- 单批上限 6 张 / 4MB;超量由协调器自动多轮传完(最多 4 轮,其余留给下次同步)
- 同一张只传一次:每台设备记住对端已有集合,重复同步不重传
- 收图时已存在的跳过(不覆盖、不重复计数);落盘用
.part临时文件 + rename,不留半截文件 - 同步日志与结果包含图片收发张数与轮数
验证
- 仪器化测试 40(37 → 40),其中三条为真回环(真
ServerSocket+ 真HttpURLConnection):① 从机 → 主机传图,内容逐字节一致且第二次同步不重传;② 主机 → 从机反方向;③ 8 张图自动分多轮传完 - 回环测试中两台「设备」各持一份互不共享的内存图片仓库(共享同一目录会让测试无条件通过)
- 单元测试 55,全部通过
已知边界
导出(.json / .csv / .db)只带图片引用、不带图片文件;后续可将导出改为 zip(正文 + 图片一起打包)。
安装
下载 KnowNote-1.9.0-debug.apk 覆盖安装(不要先卸载,卸载会清掉本地笔记)。
v1.8.0 · 正文插入图片(图片独占整行)
1.8.0 · 正文插入图片(图片独占整行)
存储
- 相册所选的图拷进应用私有目录,正文只存文件名:
- 不存相册 URI:读权限会过期、原图被删即断链、换设备失效
- 不存绝对路径:私有目录路径随设备与重装变化
- 插入时等比缩到 1600px 内 + 重编码 JPEG(相册原图可达十几兆,直接存既占空间又易 OOM);照片方向由
ImageDecoder自动纠正(API 28+) - 数据库结构未变(schema v3),导出 / 同步 / 回收站均不受影响
渲染
- 正文按「文字段 / 图片块」切分渲染:图片独占整行、按宽度铺满;行内穿插的图片也单独成行
- 用块切分而非
AnnotatedString行内图片:后者随文字换行,无法保证独占一行
- 用块切分而非
- 渲染时按需降采样 +
LruCache(上限为八分之一堆内存),解码在 IO 线程,避免大图卡住页面 - 图片文件缺失时(清过数据、或笔记来自其他设备)显示「图片已丢失:文件名」,不留空白
摘要与检索
- 卡片摘要显示
[说明]占位(无说明时[图片]),不暴露文件名;全文索引保留说明文字(可搜到)
编辑器
- 工具栏新增「插入图片」,走系统相册选择器
PickVisualMedia(不需要读相册权限)
验证
- 单元测试 55(+13:块切分 7 条 + 摘要 / 索引的图片处理等);仪器化测试 37,全部通过
- 真机(Android 13):图片横向铺满整行、上下文字各自成段;行内插入的图片同样独占一行;列表卡片显示
[示例图];编辑器工具栏「插入图片」在位
安装
下载 KnowNote-1.8.0-debug.apk 覆盖安装(不要先卸载,卸载会清掉本地笔记)。数据库 schema 未变,仍为 v3。
v1.7.0 · 阅读页可调 + 编辑器行内格式
1.7.0 · 阅读页可调 + 编辑器行内格式
阅读页显示(只作用于阅读页,不动设置页的全局字号)
- 顶栏新增「阅读显示」菜单(位于「编辑」左侧):字号滑块 0.8×–1.8×,旁边实时显示换算后的 sp 值
- 存的是倍率而非绝对值:全局字号改变后,阅读页仍按同一比例,两处设置不冲突
- MD 模式 / 文本模式切换:文本模式原样显示(含
<color>/<size>标记本身),便于手改标记 - 该设置由底部常驻条收进菜单,长笔记不再被压住正文
编辑器行内格式
- 选中文字后可加:粗体 / 斜体 / 三档字号 / 六色(红黄绿青蓝紫)
- 粗体、斜体为开关式(再点一次取消);颜色与字号为换值式,识别两种已有标记形态(连标记一起选中的整段、只选中标记内文字且标记紧贴选区外侧),直接替换而不嵌套 —— 否则红改蓝会叠成
<color=blue><color=red>x</color></color> - 标记写进纯文本正文(
<color=red>…</color>、<size=1.25>…</size>),数据库结构未变(schema v3),导出 / 同步 / 回收站均不受影响 - 字号用相对倍率(em),可与阅读页滑块叠加;每种颜色带明暗两套取值
渲染器
- 支持
<color>/<size>,并递归解析行内内容:**<color=blue>x</color>**(粗体在外、颜色在内)正确输出粗体 + 蓝色 - 未闭合或未支持的标记原样显示,不猜颜色、不吞文字
列表与卡片摘要只显示可读文字
- 摘要去掉
<color=…>、<size=…>与 Markdown 记号(**粗体**、*斜体*、~~删除~~、`代码`),只留文字 - 两处防误伤边界:
2*3*4不被当作斜体、my_file_name.txt不被下划线斜体削掉 - 全文索引同样不收录自定义标记,避免搜
color有结果却没有可高亮的位置 - 正文本身未改动;查看原文切「文本模式」
验证
- 单元测试 46(+30:选区格式化 13、标记解析 9、摘要去记号 8 等);仪器化测试 37,全部通过
- 真机(Android 13):滑块 14 sp → 25 sp 并落盘;长按选词后「加粗 + 蓝色 + 特大」三步叠加,落库为
**<color=blue><size=1.5>标记</size></color>**,阅读页三效叠加生效;列表与瀑布流摘要残留记号 0 处
安装
下载 KnowNote-1.7.0-debug.apk 覆盖安装(不要先卸载,卸载会清掉本地笔记)。数据库 schema 未变,仍为 v3。
v1.6.0 · 可调阅读显示 + 检索范围可选
1.6.0 · 可调阅读显示 + 检索范围可选
阅读显示(设置页新增「阅读」区)
- 字号五档:小 / 标准 / 大 / 特大 / 超大。整棵 Typography 等比缩放(只乘
fontSize与lineHeight),标题与正文的相对层级保持不变 - 文字颜色七种:跟随主题 / 深墨 / 纯黑 / 暖褐 / 墨绿 / 深蓝 / 酒红;每种带明暗两套取值,深色模式下不会深字压深底
- 实现要点:只覆盖
LocalContentColor无效 —— 页面里 60 多处Text显式写了颜色,会盖掉兜底。改为覆盖主题色角色(onSurface/onBackground/onSurfaceVariant/tertiary)才生效;primary(高亮、标签)与outline(时间戳)刻意不动
检索范围可选
- 可单独勾选 标题 / 正文 / 标签,默认全选;全部取消时自动回到全选(否则一条都搜不到)
- 范围筛选放在结果侧:FTS 的列限定(
title:xxx)需要SQLITE_ENABLE_FTS3_PARENTHESIS,Android 的 FTS4 未编译该选项,结果侧过滤让 FTS 与兜底两条路径语义一致;范围收窄时候选集上限放宽 4 倍 - 选择写入
UiPrefs,下次打开保持
检索忽略大小写
- 索引侧与查询侧统一
lowercase(Locale.ROOT),避免土耳其语环境把I折成ı导致两侧不一致 - 无全文引擎时的兜底由 SQL
LIKE改为 Kotlin 侧比较:SQLite 的LIKE/LOWER()只折叠 ASCII(É与é不算同词) - 搜索历史按忽略大小写合并同词,不再出现
Android/android两条
验证
- 单元测试 16(+8:范围与大小写判定的纯逻辑);仪器化测试 37(+2:三字段大小写无关命中、搜索历史大小写合并),全部通过
- 真机(Android 13):
gradle全范围 1 条 → 只留标签 0 条 → 恢复全范围 1 条;GRADLE/GrAdLe/hashmap/HASHMAP均命中;暖褐文字实测RGB(94,74,59),与定义的#5B4636相符
安装
下载 KnowNote-1.6.0-debug.apk 覆盖安装(不要先卸载,卸载会清掉本地笔记)。数据库 schema 未变,仍为 v3。
v1.5.0 · 四语言界面 + 新图标
1.5.0 · 四语言界面 + 新图标
首个不再带 -demo 后缀、可直接日常使用的版本。
新增
- 四语言界面:跟随系统 / 简体中文 / 繁體中文 / English / 日本語;Android 13+ 同步到系统「应用语言」设置
- 应用命名:KnowNote · 碎片笔记(桌面显示「碎片笔记」)
- 自适应图标 + 单色层:Android 13+ 开启「主题图标」后跟随壁纸取色
修复
- 译文里的 Kotlin 模板被原样写进字符串资源,切到外语后界面出现
$days之类的未展开字面量
已有能力
笔记 / 标签 / 分组 · 中文子串全文检索(FTS5 → FTS4 → LIKE 三级降级) · Markdown 预览 ·
回收站与定时清理 · 局域网一主多从同步(设备间直传,不经过任何服务器) · 导出 JSON / CSV / DB
安装
- 附件为 debug 签名构建,供自测直装,非商店发布版
- 应用 ID 为
com.xinjigalaxy.knownotes.debug,可与正式包共存
测试
- 单元测试 8/8
- 仪器化测试 35/35:Android 13(SQLite 3.32.2)与 Android 15(SQLite 3.44.3)
- 真机验证:迁移、检索降级自愈、二级页面、同步回环、主题与语言切换(明细见 README 验证记录)
English version: README.en.md