Skip to content

v0.4.0

Choose a tag to compare

@siwilizhao siwilizhao released this 25 Sep 08:07
· 135 commits to main since this release

Added

  • 注册体验重做 + 口令策略升级:设置 → Sync 的登录/注册表单改为模式切换(登录/
    注册分段选择),注册模式提供确认密码(二次输入一致性校验)、实时字段校验(用户名
    规则/密码规则镜像服务端,错误即时报出不出网)与密码强度条(长度/字符组合 0-3 档),
    密码可见性切换。服务端口令策略升级为 8-72 位且必须同时包含字母和数字(注册与
    改密码共用;已有账号的旧弱口令仍可登录——登录不校验复杂度,只在下次改密时收口);
    desire-admin 重置密码同策略。纯数字等弱口令注册返回 422(api-smoke 新增回归步骤)。
  • 运维管理 CLI(desire-admin):账号与注册开关的命令行入口——user list / reset-password / delete / disable / enable(重置密码与禁用都会吊销全部刷新令牌,
    delete 级联清同步数据)、registration status|on|off(开关存 server_settings 表
    立即生效;env DESIRE_API_ALLOW_REGISTRATION 显式设置时优先,此时 CLI 拒绝切换并
    提示)、stats(用户数/各域密文行数)。顺带修复:登录未拦截禁用账号(此前
    禁用用户仍可换取新令牌,仅中间件事后 403)——login 现校验 status。
  • Agent 内容纳入同步类目(用户可选):新增 agent_memory(Agent 记忆:画像/
    事实/摘要,逐条 LWW 合并 + tombstone,能力衰减/容量淘汰/一键清空均下推删除)
    与 agent_prefs(自定义系统提示词,快照 diff 盖戳)两个类目,设置 → Sync 的
    类目列表自动出现开关。AgentMemoryStore 补同步支持(replaceForSync、待删清单、
    pin/内容修改盖戳);对话本身仍按此前决定永久留本地。AgentMemorySync.apply
    合并语义进纯逻辑单测(117 项)。
  • 同步数据端到端加密(E2E):主密钥(256 位随机)只在客户端 Keychain、永不上传;
    每域 HKDF-SHA256 派生独立密钥做 AES-256-GCM 载荷加密(信封 {v,ct}),真实 id/
    设置键名在密文内部;线上 client_id = 独立派生密钥的 HMAC(服务器只见不透明
    标签,唯一性保留、读不出内容)。服务器只新增 users.sync_key_check(密钥指纹,
    0003 迁移)与 GET/PUT /sync/key-check——拖库只能拿到密文、用户 id 和时间戳。
    新设备靠手动导入 base64 密钥;指纹不一致时拒绝同步(防拿错密钥把旧密文全量覆盖,
    服务端 409)。已知明文元数据:用户 id、域名、行数、时间戳、删除标记。
    E2E 实测:81 行全密文零泄漏、错误密钥导入被拒且不污染服务端指纹、解密回环后
    本地数据完好。
  • 同步协作审计:五项修复(浏览器 ↔ 后端全链路复查):① 服务端 LWW 仲裁改 >=,
    同刻时间戳 = 幂等 applied(此前拉取/采纳过的条目每轮全量 push 必吃 conflict
    回包,纯噪音且随数据量膨胀);② 客户端清账改"全部 results"——输掉 LWW 的删除
    不再每周期重推
    (旧逻辑只清 applied,conflict 的待删 tombstone 永久残留);
    ③ 拉取游标升级为复合 updated_at|id + 服务端 since_id/次级排序(消除同刻行
    恰跨分页边界的静默丢失,ts-only 旧语义兼容);④ 书签合并两段式孤儿归位(父在
    批次晚于子出现时挂回,不再永久落根,纯逻辑单测覆盖);⑤ 加固:登录双重限流
    (IP 20/min + 用户名 10/min → 429)、注册 IP 10/小时 + DESIRE_API_ALLOW_REGISTRATION
    开关、时间戳钳制 [2000, now+5min](防 .distantPast 撞 DATETIME 下限/快钟霸占 LWW)、
    单条 payload ≤256KB。E2E:同戳幂等步驟进 sync 冒烟(10 步)、限流 25 连发出现 429、
    桥真机验证"删除输 LWW → 待删清零 + 远端版本复活"。
  • 同步类目由用户选择:设置 → Sync 新增"同步类目"区块,五个域(书签/快拨/阅读
    列表/快捷键/设置)各有独立开关(默认全开,存 UserDefaults sync.enabled.<域>)。
    关闭 = 跳过该域的 push/pull,游标保留——重新打开后自动补齐关闭期间的增量;
    服务端数据不删除。桥补 POST /sync/domain,/sync/status 带各域 enabled 态。
    E2E:关快拨 → 第二设备推送 → 应用拉不到;重开 → 同步后补齐 ✓。
  • 后端部署体系(照 trove 搬):docker/Dockerfile.api|Dockerfile.migrate(多阶段:
    rsproxy 镜像源 + 按架构分 id 的 cargo cache mount;api 非 root 运行、migrate 冷拷
    迁移 SQL)+ .dockerignore(Swift 应用/构建产物/秘密不进构建层)+ 五个脚本:
    build-api.sh / build-migrate.sh(多架构镜像)、push.sh(latest + commit 短
    hash 双 tag)、run.sh(起服务 / 一次性迁移容器)、migrate.sh(本地直跑)。
    部署顺序铁律:迁移执行器先行,改迁移不牵连业务镜像;prod 下 api 不自动跑迁移。
    清单与 env 表见 scripts/README.md。
  • 云同步收官:settings KV 域 + 服务器地址设置:23 个功能偏好(搜索引擎/主页/外观/
    强调色/书签栏/下载/SponsorBlock/缩放/自动播放等)全量入同步;载荷 = 带类型标签的
    SettingsSyncValue(string/bool/number),目录白名单 SettingsSync.catalog 刻意排除
    机器相关项(截图文件夹路径、自定义搜索引擎引用)。设置没有 per-key 时间戳——由
    SyncStore 维护"快照 diff 检测本地变更 → 变更盖新戳",其余交给服务端 LWW。设置页
    Sync 区块新增服务器地址行(即时生效),桥补 /sync/setting(写一个可同步设置项)。
    E2E 双向验证:远端推 homePage → 应用偏好落盘;桥写偏好 → 服务端行 {"b":true} 带
    新戳。注意:设置推送若被远端拒(conflict)是 LWW 正常行为——测试时远端时间戳
    要真的更新(秒级 now 会输给应用侧微秒戳)。
  • 云同步扩展到四域 + 自动化桥端点:快拨/阅读列表/快捷键接入 SyncEngine(FlatSyncMerge
    通用平铺合并核心,与书签同一套 LWW 规则;QuickDial 增加 sort 字段、每次结构变更
    重编号并盖戳——位移不改戳会被远端 LWW 拒收导致跨设备顺序分叉;阅读列表清空 = 逐条
    tombstone
    ;快捷键无删除语义,重置即更新)。桥新增 /sync/status|now|login|register| logout|server,同步链路可全程 curl 验证。E2E(真机 + 真库):注册→四域全量入库
    (书签 5/快捷键 43/快拨 8)→ 第二设备 CLI 推送 → 应用增量拉取可见 → 删除下推
    tombstone(payload 置 NULL)→ 登出,测试数据已全部清理。
  • 云同步客户端(首域 = 书签):新增 Features/Sync/——SyncStore(登录态/Keychain
    令牌非交互读写/游标/启动后 + 每 5 分钟自动同步)、SyncAPIClient(信封解码、401
    刷新令牌单次重试)、SyncModels + SyncMerge(树 ↔ 条目展平/合并,LWW 仲裁与
    tombstone 收敛有纯逻辑单测覆盖)。Bookmark 增加 updatedAt(optional + 合成
    Codable,旧文件缺键解码为 nil 不清数据);BookmarkStore 本地删除进 pendingDeletions
    待删清单(显式推 tombstone,push 成功后清除,防止其他设备把已删节点"救活")。设置页
    新增 Sync 区块(登录/注册/状态/立即同步/退出),i18n 补 21 键三语。已知边界:双端都
    有书签时首绑为并集(UUID 不同不去重);同步当前活跃 Profile 的桶;服务器地址默认
    http://127.0.0.1:18090(sync.serverBaseURL 可覆盖)。
  • 云服务后端 M1(同步引擎,crates/):通用"域 + 文档"同步——0002_sync_items.sql
    单表按 (user_id, domain, client_id) 存 JSON 文档,GET /sync/{domain}?since=<游标>
    增量拉(含 tombstone)+ POST /sync/{domain} 批量推(单批 ≤500)。仲裁 =
    client_updated_at LWW:推送逐条事务内 SELECT .. FOR UPDATE,旧改动拒收并回传
    服务端胜者;删除 = tombstone(deleted_at 置位 + payload 置 NULL,已删内容不留库)。
    首批域:bookmarks / quickdials / reading_list / keyboard_shortcuts / settings(KV)。
    tools/api-sync-smoke.sh 9 步全绿(conflict 回胜者 / 游标增量 / tombstone / 未知域
    404),auth 冒烟回归通过。
  • 云服务后端 M0(账号底座,crates/):仓库新增 Rust workspace(照 trove 的组织方式)——
    crates/api(desire-api,axum,默认 :18090)+ crates/common(增量迁移 +
    desire-migrate 执行器)。功能:注册/登录(bcrypt)、JWT access + refresh token
    事务内轮换(旧 token 重放 401)、设备登记与吊销(客户端稳定 device_id;
    吊销联动该设备全部 refresh token 失效,重复登录自动恢复)、/auth/me 资料/改密码、
    dev-only /openapi.json(utoipa)。迁移基线 0001_init.sql = users / devices /
    user_refresh_tokens。全链路冒烟 tools/api-smoke.sh 9 步(含 409/401 反例)在真实
    MySQL 8.4 上全绿;cargo test --workspace 11 用例。组织约定、迁移纪律与运行方式
    写进 AGENTS.md「后端」章节。
  • 发布流程一键化(scripts/release.sh):v0.3.14 的发布把每个手工步骤的坑都踩了一遍
    (Release 工作流禁晚了被 tag 触发、gh 用错 repo 名 404、CI 红着就打了 tag、冒烟/打包/
    校验全凭记忆排顺序)——现在全部固化成一个脚本:scripts/release.sh <版本号>,阶段
    prep(冻结 CHANGELOG + 版本号 + 构建号自增,推 main)→ build(clean Release +
    零警告闸门 + 产物版本核对)→ ci(HEAD 质量闸门,红着不许发)→ smoke(从
    非 DerivedData 路径启动冒烟 + 桥/统计/档案端点探活)→ package(zip + SHASUMS)
    → publish(先禁 Release 工作流再推 tag、正文 = CHANGELOG 段 + 安装说明模板、
    --repo 从 git remote 推导)→ verify(从 release 重新下载验校验和/版本/启动,
    完成后恢复 Release 工作流)。任一步失败 --from <阶段> 续跑;body <tag> 可单独
    预览 release 正文。安装说明抽成 scripts/install-note.template.md(CI 的
    release.yml 与脚本共用,单一真相)。GitHub 免费额度烧完也能发:ci 阶段
    --ci auto(默认;Actions 不可用时自动回落)/ gh / local 三档——local 用
    本地单测 + 评估套件顶上 CI 的覆盖(build 排在 ci 之前就是为了给它供产物);
    gh API(推 tag、建 release、workflow 开关)不走 Actions 分钟数,额度烧尽照样发。

Fixed

  • 桥的 HTTP 收包循环会把被 TCP 分段的请求整个丢弃(CI 上的 agent 评估抓到的,
    本地几乎复现不出来):AutomationServer 的连接处理只调一次 receive() 就把
    缓冲当完整请求去路由——TCP 不保证一次 receive 收全(CI 虚机的网络栈经常把
    头和 body 拆成两段交付)。被截断的请求 body 解析成空字典:/agent/send 报
    "missing text"、任何带 body 的端点随机失败,且重试也救不了确定性分段的那次。
    现在按 Content-Length 攒齐再路由(按字节找 \r\n\r\n 头尾——多字节字符
    被分段处 String(data:) 会直接失败,不能用字符串定位);对端关闭但仍不完整
    的连接直接取消。8KB body × 60 次连发压力验证 0 失败。

安装

未签名构建(CI 无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。

  1. 解压 Desire-v0.4.0-macos-arm64.zip(双击,或
    ditto -x -k Desire-v0.4.0-macos-arm64.zip .)。
  2. 安装:把解压出来的 Desire.app 拖进 /Applications。
  3. 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
  1. 打开 Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。

报 xattr: No such file: Desire.app 说明当前目录里没有解压好的 app——
命令要写完整路径(如 /Applications/Desire.app),或先 cd 到它所在目录。