Releases: browser-rs/Desire
Release list
v0.4.2
Added
- 视频列表批量下载(两种模式):
- 模式 A
downloadAllPageVideos(喂食流):列表页卡片内直接播放的
(feed/瀑布流),当前页嗅探 + DOM 扫描到的视频全部入队——去重、跳过
blob:/DASH/纯音频,文件按01- 02-序号落<保存位置>/<文件夹>/
(默认以页面 host 命名)。 - 模式 B
downloadVideoList(逐页解析):每项有独立详情页的列表——
由 Agent 收集详情页地址清单(用户确认后)交给原生引擎:隐藏 WebView
逐页加载(串行 + 2-5s 抖动,复用应用桌面 Safari UA 与嗅探脚本),
等播放器起流拿真实媒体地址,≤2 并发下载。解析梯度:等嗅探 → 周期
DOM 扫描 → 点播放(静音)。 - Cloudflare 三层防线:隐藏 WebView 与浏览标签页共享 Cookie 池,
非交互挑战自动放行;交互式 Turnstile 先自主通过——解析器 webview
弹成小窗后用SyntheticInput(真实 NSEvent,isTrusted=true)点复选框
(最多 3 次);仍不成才降级为等人工(人工是最后兜底,不是第一响应),
引擎继续轮询挑战解除后不重载页面直接续跑。 - 失败自动重试(同批同文件夹):失败项冷却后自动重跑(≤3 次尝试,
每次重新解析拿新签名 URL)——2026-09-26 真实站点 12 部批量实测:403 过期
靠模型手动开新批次,文件散落三个目录;本轮根治。新增
retryBatchDownloads工具作规范重试入口,工具描述明确禁止失败后用
downloadMedia单补(散落根源)。 - 解析节流:下载占满并发槽时不预解析下一页——签名 URL 按需签,
提前解析的地址会在队列里过期(403 主因)。 - 低空间主动询问:剩余空间低于阈值(默认 20 GB)时队列挂起,经
UserPromptCenter问用户(继续 / 换位置 / 取消,超时视为继续);
"换位置"走目录选择器并把位置落为偏好。新桥端点
GET /agent/prompt+POST /agent/prompt/answer。 - 命名偏好体系:
clean(默认——按最小周期折叠站点标题模板的重复段,
真实站点那批"同一句话截三遍"的文件名根治;尾部模板残留番号一并去除)、
code(番号/代号优先)、title(原样)。工具naming参数显式传入的
值会持久化并写入长期记忆(addFact, category=preference)——模型
下次直接知道;自定义保存位置同理。 - 磁盘预留硬底线 + 人性化配置:剩余空间低于预留线(
reserveGB,
默认 5GB)时挂起整批——在跑项收回 pending(不烧 attempts)、
发会话备注 + 系统通知、面板提问(换位置/取消/已清理空间继续);
空间监视每 30s 巡检,恢复到预留 + 512MB迟滞线以上自动续跑。
预留不做"继续"绕过——防止写满磁盘是目的。配置经
GET/POST /media/batch/config(reserveGB / naming / baseDirectory)。 - 批量任务管理:新增
manageBatchDownloads工具与桥端点——
暂停/恢复整批、skip 单项(含下载中的项)、向既有批次追加任务
(按 sourceURL 去重、序号接续,已完成批次追加后自动重跑)。 - 可观测性:快照带批次级聚合(各状态计数、暂停/挂起原因)与
逐项进度(done/total/单位,ffmpeg 路径按秒、内置下载器按段);
引擎关键转移(解析/完成/失败/挂起/恢复)进统一日志
(subsystem == "me.siwi.Desire", category == "downloads")。 - 清晰度智能选择 + 变体族去重:"按最高清晰度下载"真正进引擎——
stream 候选里 master 播放列表(无画质标记形态)优先于任何带画质标记
的变体(master 内部由 MediaExporter 取最高码率),无 master 时按画质
标记(720p/1080P…)取最高;page 模式规划做变体族去重:master 与
其目录下的画质变体同时被嗅探到时只留 master(此前同一视频会按两个
画质各下一份)。 - 已下载索引(跨批次去重):完成的项把来源/媒体 URL 记入索引
(DiskStore,2000 条上限按时间淘汰),重跑同一列表自动跳过已下载项
(skipped 注明原路径),force参数(工具/桥)可强制重下;
batch.skipDownloaded开关可全局关闭。 - 下载原子化 + 完整性校验:所有下载一律写
<final>.part、成功后
原地改名——崩溃/强杀只留一个.part(下次尝试原地覆盖),根治崩溃
残件触发-1后缀链的问题;完成后用 ffprobe 校验输出(时长 + 分辨
率),摘要带✓ 60.0s, 320x240(无 ffprobe 或校验不过则不显示)。 - 下载历史 + 并发配置:
GET /media/batch/history(索引只读视图,
最新在前,供回答"之前下过什么");batch.maxConcurrent(1-4,默认 2)
经 config 端点可调。 - DASH (.mpd) 支持的可行性结论:本机 Homebrew ffmpeg 无 dash
demuxer(Unknown input format: 'dash',仅 webm_dash_manifest)——
ffmpeg 直连不可行,维持跳过并注明;要支持需自写 mpd 解析 + 分段下载- 合流,暂不做。
- 批量断点续传:未完成批次持久化到 DiskStore(状态转移点即时落),
app 强杀/崩溃后重启恢复到队列(暂停态)——在途项收回到 pending
(下载地址保留,签名过期由自动重试兜底),resume 续跑;终态自动清理。 listBatchDownloads查进度(含 attempts);批次完成/取消/需要人工
验证时发会话备注 + 系统通知(单项下载静默,批次统一汇总)。- 下载器 Cookie 透传:
MediaExporter把同域 webview Cookie(30s
缓存)附到 URLSession 分段请求、Cookie头给 ffmpeg——媒体 CDN 也挂
Cloudflare 的站点(此前分段 403)现在可下。 - 已知边界:DASH (.mpd) 与 DRM 流不支持(批量中明确跳过并注明原因);
IntersectionObserver 懒加载的播放器靠点播放缓解,个别站点可能需把该页
转到可见标签页人工触发。 - 配套修复:批次取消后快照残留
resolving行;重试后文件名序号叠加;
低空间询问回答后永久卡住;resolveAll对已解析项反复重解析。
- 模式 A
Changed
- 纯逻辑单测新增
BatchMediaPlan用例(规划 16 项 + 命名风格 9 项,
共 166 项)。
安装
未签名构建(CI 无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.4.2-macos-arm64.zip(双击,或
ditto -x -k Desire-v0.4.2-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
v0.4.1
Added
- Desire Remote 与桌面 Agent 能力对齐(手机可介入 Agent 的每一个卡点):
快照增补审批 / 反问 / 计划 / 子代理 / 排队 / 目标页 / 全权限 / 可重生成 /
快捷动作 / 暂停 / token / 成本;手机上可审批工具调用(允许一次 / 始终
允许 / 拒绝,按PendingToolApproval.id强校验以防停在旧审批上误批新审批)、
回答 Agent 的反问(选项一键作答)、逐条移除排队消息、快捷动作与重新
生成。此前这两类挂起状态完全没有走远程协议——Agent 在桌面等人工介入,
手机既看不到也回不了,表现为"卡住不动"。 - iOS 新增 Agent 页(取代原「Agent 看板」那个只读数字列表):工具与技能
(按只读 / 改变状态 / 执行代码三档风险分级,与桌面ToolRisk同源)、
用量统计(跨会话 token / 成本 / 每模型明细,与桌面「用量」页同一口径)、
执行轨迹(每回合目标、工具序列与每步耗时、被拒 / 失败、慢工具榜);
模型、FULL ACCESS、本回合控制收进输入条左侧的状态弹窗。 - 扫码登录一次完成两件事:登录二维码在远程控制开启时顺带携带配对信息
(配对码 + 会话密钥 + 设备 id),手机扫一次即完成"登录 Mac"与"远程配对"。
会话密钥仍只在二维码里传,服务器读不到。 - 远程会话控制:新对话(先沉淀记忆摘要)、暂停 / 继续、停止。
Changed
- iOS 信息架构重做:抽屉换成底部 Tab(会话 / Agent / 设置);对话从会话
列表进入并隐藏 TabBar(全屏聊天,导航栏透明、保留系统返回键与左滑手势);
状态、计划、子代理、排队、审批、提问统一到输入区上方的停靠层,模型与
全权限等常驻信息移入弹窗——底部只留需要即时处置的内容。 - Mac 设置与列表视觉:新增表单行排布(固定标签列 + 撑满剩余宽度的控件,
替代"标签左、控件贴最右"造成的半屏空白)、侧栏图标列固定宽度对齐(修正
iphone.radiowaves.left.and.right过宽导致的标题参差)、会话列表重做
(单图标 + 内容预览行 + 时间/条数分列)、外观选择改为带图标的行式选择。 - iOS 三个 Tab 统一为
List(.insetGrouped)+ 同一套分组标题与行样式。
Fixed
- Mac 登出后远程仍显示开启:登出只在内部把连接状态置静默,开关的持久化
真值仍是 true、心跳定时器空转。现在登出即拆链路、清设备列表与配对码、关开关。 - 手机被莫名退出登录:refresh token 在服务端是一次性轮换的,而客户端三条
并发路径(pull 轮询 / push 上行 / WS 重连)会各自刷新,第二个必然被拒并被
当成会话过期 → 清凭证回登录页。改为刷新单飞,且只有服务端明确 401 才结束
会话(网络抖动 / 5xx 不再清凭证)。 - 扫码不识别:
startScanning()只在updateUIViewController里尝试,而
sheet 弹出首帧view.window仍为 nil、之后又无人重试。改为轮询等视图上屏;
同时补didUpdate回调、相机启动失败不再被try?静默吞掉、相机不可用时
按钮不再是"点了没反应"的死按钮。 - 连接区块自相矛盾(上"未连接 Mac"、下"已连接"):Mac 名字只在配对响应
里拿过一次且未持久化。现在随每帧快照下发并本地保存,主标题只放设备名、
连接状态由带状态点的一行表达。 - 状态漂移:漏掉"回合结束"那一帧会让界面停在"工作中";三个 Tab 进页时
强制requestSync()拉一帧权威状态。 - 输入框被消息穿透(停靠区补不透明底板、输入胶囊改实色)。
Added
- 远程控制(Desire Remote,M0+M1,当日重构为可水平扩展传输):手机 App
经 api 中继远程对话本机 Agent——工作仍全部在 Mac 本地执行,手机发指令、
看进度。服务端modules/remote(0008 配对码/留言 + 0009 双信箱迁移);
Mac 设置"远程"区(Features/Remote/);apps/ios/DesireRemote.xcodeproj
(登录 → 扫码配对 → 控制台)。信道端到端加密(AES-256-GCM,密钥只在
二维码中,服务器只路由密文)。传输架构(照 Trove im_ws 模式,两实例 +
nginx 轮询部署下正确):上行一律POST /remote/push(入库持久 + Redis
express 发布,快照 replace 语义);下行 = WS 订阅自己频道(即时)+
GET /remote/pull每秒兜底,按信箱行 id 去重;在线判定基于 DB
desktop_last_seen_at(15s 窗口,跨实例一致);服务端/客户端双向 20s
心跳(解 nginx 空闲回收)。配对认领即时通知桌面收起二维码。
快照走独立 lane(controller_snap):replace 只清同 lane,不再误删
先落地的 sessions 回包;远程"新建会话"修复(Mac 端 create 后补 save
落盘进列表 + 手机端点"+"乐观进空聊天室、按快照 session 字段锁定选中)。
远程开关关闭后不再有任何广播(拆 WS + 停链路循环 + isEnabled 守卫)。
E2E:tools/api-remote-smoke.py10 步全绿 + Mac 桥真机链路验证(prompt
送达 Agent 执行、快照回传手机可解密、newSession 列表/落盘/快照三确认)。
APNs 推送、审批卡片远程应答(M2)、按需截图未做。 - Desire Remote iOS 支持 Markdown 渲染:Agent 气泡接入 MarkdownUI
(上一版手写的无依赖轻量渲染移除),主题参照 IrsClawApp——GitHub 基础
主题 + 气泡内边距收紧、表格横向滚动(手机宽度放不下整表)、代码块/引用/
有序无序列表全覆盖;流式更新不整棵重建(按 message.id 稳定身份)。
修复工具消息不渲染:快照里调用名在 assistant 帧、结果 content 在
tool 帧(toolCalls 为空)——此前 assistant 分支丢弃调用名、tool 分支
无展开钮导致结果永远折叠;现 assistant 帧显示"工具调用名"行(点击展开
参数摘要),tool 帧结果常驻显示(默认 4 行,点击展开全文)。
Agent 看板:聊天页状态胶囊升级(忙时"工作中·用时",闲时"已连接·
模型·上下文 N%"),点击进入看板——状态区(模型/上下文占用%/排队/回合
用时)+ 记忆区(用户画像/事实/对话摘要,滑动删除走桌面同款 tombstone
语义);快照协议相应扩展 model/contextPercent/queueCount/elapsed/
toolArgs,新增 getMemory/deleteMemory/memory 帧。
Added
- Mac 端扫码登录(照 Trove 三步流):设置 → Sync 未登录时新增"扫码
登录"——桌面出票渲染二维码,已登录的 iPhone DesireRemote 扫码并在
手机上确认,桌面轮询领走 token 对,免密码登录。服务端新增
auth_qr_logins(0010 迁移)与四步端点qr/create|status|scan|confirm
(create/status 无鉴权带 IP 限流;scan/confirm 走手机 JWT;refresh
token 绑定桌面设备行,吊销设备即吊销扫码登录)。token 一次性消费
(被领走时原子清空,防同 token 被第二个轮询方领走,Trove 踩过的坑)。
扫码登录不经密码——E2E 主密钥沿用本机 Keychain 已有值,无主密钥的
全新机器仍需密码登录一次完成托管恢复。另修复 iOS 切后台时任务切换
卡片黑底(窗口底色钉为系统背景色)。
Changed
-
DesireRemote 视图层照 IrsClawApp 架构重写:单页聊天为根(无 Tab、
无独立会话列表页);左上 ≡ 弹出 Menu 半屏 sheet(.medium/.large
detents)——Agent 状态卡(模型/上下文%/排队/连接)+ Browse 磁贴网格
(会话/记忆/看板/扫码登录)+ 最近会话前 5;磁贴经 navigationDestination
push 全屏页:会话列表(今天/昨天/本周/更早分组 + 左滑删除 + 左滑重命名)、
记忆页(画像/事实/摘要 + 左滑删除)、看板页;聊天页照 IrsClaw ChatView
(bottom 锚点滚动、safeAreaInset 漂浮输入胶囊、mic/send 圆钮、busy 变
停止、PulsingDot 录音条、断线/排队横幅)。sessions 帧补 date 时间戳。 -
服务器地址改为"覆盖"语义(Mac + iOS):内置生产地址不再出现在
任何界面(登录页删掉服务器区块、设置页占位符不露地址);设置项留空
= 使用内置默认,填了才覆盖(旧版把默认值写进覆盖位的自动迁移清除,
配对/登录二维码自带服务器仍是显式覆盖来源)。Mac 端"Apply"空值 =
回内置默认。 -
远程链路三个双端 bug 修复:① iOS"已断开"永不恢复——
login()从未存
refresh token、WS 用过期 access 直连、断线后无重连逻辑;现 401 自动刷新
重试一次、断线 5s→30s 退避重连、会话彻底过期回登录页。② Mac 配对二维码
认领后不消失——认领是纯 REST、桌面端无从得知;现认领即时推送通知 +
二维码显示期间 2s 设备数轮询兜底。③ iOS 解除配对无效——只清本地不清
服务器;现先 best-effort 调/remote/pairing/revoke再清本地。 -
默认同步服务器切至生产:全新安装(未手动配置过服务器地址)直接使用
https://api.mankong.icu/v9;已手动配置过的设备保留原值不受影响。
服务器地址输入现在会去除尾斜杠(避免拼出//auth/…双斜杠),设置页
占位符同步更新。本地开发用桥POST /sync/server钉回本地实例。
Added
- 浏览历史同步(第八类,opt-in 默认关闭):服务端新建专表
sync_history_items(0007 迁移,与通用引擎同构、按域路由表名)——历史是
高频写入的日志型数据,与关键小域分开治理;服务端 90 天 TTL(history
push 后顺带清理该用户超期行,含 tombstone)。客户端HistoryEntry补
updatedAt盖戳(旧文件缺键以访问时间兜底归一)、墓碑清单(只有用户
显式删除——单删/清空/按域删/按时间删——才推 tombstone;滚动裁剪与合并
溢出不推,靠 TTL 收敛)、replaceForSync合并回写;合并语义
HistorySync(FlatSyncMerge 直配)进纯逻辑单测。设置 → Sync 类目列表
自动出现"浏览历史"开关(默认关);桥/sync/status的 cursors/待删计数
补 history,/command补clearHistory。desire-admin stats 并表统计。 - 会话过期自愈:refresh 令牌被服务端拒绝(吊销/轮换丢失/换 JWT 密钥)
时不再每轮空转报 401——干净登出(清令牌/游标/戳,主密钥保留)并给出
"同步会话已过期,请重新登录"的明确提示(全局与逐域状态一致)。 - 退出前补推:退出时有未上推的本地变更,
applicationShouldTerminate
走.terminateLater做只 push 不 pull 的限时补推(5 秒内必回调,
推不上去的域放回脏集合由下次启动首轮对账兜底);无脏域照常立即退出。 - 产品介绍页(
website/index.html):单文件静态落地页,"墨与朱"编辑风
(宣纸底 + 墨色正文 + 朱砂点睛,「欲」字印章记忆点);字体全用 macOS 内置
Hoefler Text / 宋体 / 楷体,零外部依赖(无 CDN,国内访问无阻碍)。
内容:英雄区 + 六大特性 + 智能体轨迹示例 + 端到端加密三步图解(含七类
同步 chips 与"AI 对话永留本地"的诚实标注)+ 下载四步(含 xattr 命令)。
桌面/移动自适应;动效纯 CSS,支持prefers-reduced-motion与无 JS 降级。
已部署生产 https://desire.mankong.icu/,主 README 顶部与 Download 段
均已指向;更新与发布方式见website/README.md。SEO/GEO 已优化:canonical、
完整 OG/Twitter Card(含 1200×630og.jpg)、JSON-LD(WebSite +
SoftwareApplication + FAQPage,问答与页面可见 FAQ 逐字一致)、新增可见
FAQ 段与导航锚点、robots.txt(显式放行 GPTBot/ClaudeBot/PerplexityBot
等 AI 爬虫)、sitemap.xml、llms.txt。 - 同步改为变更驱动(近实时):源 store(书签/快速拨号/阅读列表/快捷键/设置/
Agent 记忆/Agent 提示词)的变更经objectWillChange标脏对应域,5 秒防抖后只
上推脏域——不再等 5 分钟定时轮;远端合并回写由applyingRemote守卫包住,
拉回来的数据不会把自己标脏。启动/登录首轮仍全量对账;push 失败保留脏标记,
退避重试(5s 翻倍至 5min 封顶,等同旧定时节奏);睡眠唤醒、断网恢复后自动补一轮。 - 每域同步状态:设置 → Sync 的类目列表逐域显示"✓ 相对时间 / ⚠ 错误文案",
单域失败不阻断其他域(错误按域隔离聚合进全局错误文案);自动同步的失败只落
逐域状态、不打扰全局错误(手动同步才刷新全局 lastError);"上次同步"改相对时间。
桥/sync/status新增逐域dirty/status观测面。
Changed
- push 增量化 + 大库分块:脏域才发 push(空集合不发请求);服务端单请求
500 条上限(MAX_PUSH_ITEMS)不再会让大书签库整单被拒——客户端按 400 一块
顺序推;pull 整页(1000 条)时自动翻页拉全,中途失败游标不落盘(下轮幂等重拉)。 - 设置/提示词域只推变化键:快照 diff 收敛单次推送范围,并修掉旧缺陷——本地
改过的设置键以前每轮都被重复上推(collect 盖新戳但快照不落盘,diff 永远不等);
现在"推送成功才落快照",/sync/setting桥写路径同步修正(标脏 + 盖戳,不写快照)。 - i18n:补齐 v0.4.0 同步功能漏翻的 4 条文案(验证码加载失败/密码策略/加密失败/
未设置),新增同步副标题,目录 1301 键三语 100%。
安装
未签名构建(CI 无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.4.1-macos-arm64.zip(双击,或
ditto -x -k Desire-v0.4.1-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
v0.4.0
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 表
立即生效;envDESIRE_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 新增"同步类目"区块,五个域(书签/快拨/阅读
列表/快捷键/设置)各有独立开关(默认全开,存 UserDefaultssync.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_atLWW:推送逐条事务内SELECT .. FOR UPDATE,旧改动拒收并回传
服务端胜者;删除 = tombstone(deleted_at置位 + payload 置 NULL,已删内容不留库)。
首批域:bookmarks / quickdials / reading_list / keyboard_shortcuts / settings(KV)。
tools/api-sync-smoke.sh9 步全绿(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.sh9 步(含 409/401 反例)在真实
MySQL 8.4 上全绿;cargo test --workspace11 用例。组织约定、迁移纪律与运行方式
写进 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+)。
- 解压
Desire-v0.4.0-macos-arm64.zip(双击,或
ditto -x -k Desire-v0.4.0-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
v0.3.14
Added
-
中断恢复入口:未回答的提问一键续跑(优化清单"检查点/恢复"的 UX 切片):会话以
未获回答的用户提问结尾时(工具执行中途被杀的典型残留 —— 提问已落盘、回答没有),
面板在输入框上方显示"上一条提问还没有收到回答 · 继续回答"提示条 ✓;点击为已有
的那条提问直接开一轮(不重复 append、不重复记历史 ✓)。桥端点
POST /agent/resume同能力(自动化用)✓。 -
空回合自动重试一次(优化清单收尾):模型偶发返回空内容(上游抖动、网关抽风)时,
不再把可见警告直接甩给用户 —— 自动再试一次,仍空才给出说清原因的警告 ✓。
有界:至多多一次调用,不会循环 ✓;与瞬态重试、超限减半重试的既有语义并存 ✓。- 实测(假端点 EMPTYSTREAM 每次都返回空):fixture 收到 3 次请求 = 2 次回合尝试
(原发 + 自动重试)+ 1 次标题生成,最终以警告收尾、无死循环 ✓。
- 实测(假端点 EMPTYSTREAM 每次都返回空):fixture 收到 3 次请求 = 2 次回合尝试
-
只读工具并行执行(优化清单 P2):连续的
.readonly工具(getPageText、快照、
readFile、检索类……)从逐个 await 改为整段并发、按原顺序落结果 —— gate 仍逐个过
(readonly 从不弹审批、取消即拒),toolCallId配对不受执行顺序影响 ✓。
多读类回合(检查多个元素、读多个文件)的墙钟直接省一半以上 ✓。- 顺带修掉一个被它暴露的轨迹 bug:步骤-结果的认领用的是"最近一个没观察的步骤"
(位置配对)—— 串行时代恰好不出错,并行完成后结果全部张冠李戴(实测 3 个并行
readFile 的结果互相错位)✓。现在按 toolCallId 精确认领(旧会话没有 id 的退回
位置配对)✓;单测加了并行批的配对回归(旧算法下该用例必失败)✓。
- 顺带修掉一个被它暴露的轨迹 bug:步骤-结果的认领用的是"最近一个没观察的步骤"
-
Agent 评估脚本(
tests/agent-eval.py)(优化清单 P1):固定 prompt 集 → 假端点 →
断言轨迹与消息,四个确定性用例:系统提示契约(system 唯一且在开头)、失败约定
(工具结果/模型所见均为Error:开头 + 机械核验触发)、脱敏(工具结果与模型所见
均为[redacted]、原文不出现)、超限重试(首答 context length → 自动重试并完成)✓。
清理只删脚本记录的会话 id;档案自动恢复 ✓。 -
上下文管理:摘要顶替 + 超限自动重试 + 预算自校准(优化清单 P1):
① 长对话被裁掉的轮次不再无声消失 —— 压缩时为被裁轮次生成机械摘要(每轮
"用户目标|结论",非模型生成、有长度上限),并入开头 system 提示的
"Earlier conversation (compacted)" 一节,模型仍知道前文聊过什么 ✓;
② 服务端报"上下文/输入过长"(各家措辞不一,按关键词归一识别)时,压缩预算自动减半
并重试一次,成功后记住该校准预算,后续回合沿用 —— 不再把超限当成普通失败丢给用户 ✓;
③ 面板的"上下文占用%"同步改用生效预算 ✓。- 单测:摘要包含被裁轮次的目标、预算内 digest 为 nil、压缩后逐日无断档等 ✓。
- 实测(fixture 首次请求报 context length、重试放行):应用自动重试并正常收尾 ✓。
-
纯逻辑单测 harness(
tests/run.sh)(优化清单 P0):解析/压缩/脱敏/用量折算/轨迹派生
这些纯 Foundation 逻辑现在有一套不依赖 Xcode 的测试 ——swiftc直接编译受测文件 +
用例入口,CI 在构建前跑 ✓。首批 36 项:脱敏(含多形态命中 + CJK 混排的越界回归用例、
PEM、已知 Key、无命中原样返回)、金额格式化边界(< $0.0001/$0)、用量汇总(混价
不给总额)、统计派生(连续天数 / 峰值 / 分模型 / 逐日连续)、上下文压缩(裁最老整轮、
最终块保留、工具配对不拆散、单轮超预算宁可超发)、轨迹派生(answer / 失败标记)✓。
顺带把compactForContext从AgentSessionStore抽到ContextCompaction.swift
(纯 Foundation 文件才进得了这个 harness)✓。 -
工具失败统一约定:所有工具失败一律返回
Error:前缀(优化清单 P0):此前只有
executeJS写Error:,其余失败("File not found"、"Missing path"、runCommand 非零退出、
MCP 报错、子代理流失败…)都是普通文本 —— 机械核验看不见、轨迹的threwError统计低估、
模型也难以可靠识别失败。现在约 120 处失败返回全部收口(BrowserToolProvider.fail),
且 runCommand 的非零退出/超时与 MCP / 子代理失败一并进约定 ✓(输出原样保留,
模型仍能看到 stdout/stderr)。查询成功但结果为空不是失败("No bookmarks" 等),
避免把正常空答案误标成错误 ✓。- 实测(假端点强制
readFile一个不存在的文件):工具结果 =Error: File not found: …✓;
机械核验第一次对这类失败触发了硬提示("本轮所有工具调用都被拒或报错")✓;
轨迹threwError = true、stats.threwError = 1✓;模型实际收到的工具消息同样以
Error:开头 ✓。
- 实测(假端点强制
-
crew 用量记账 + 状态可见(优化清单 P1):多标签 crew 的 worker 跑在各自标签页里,
此前用量无处记账(成本与统计都会低估,一次 crew 可能比主循环本身还贵)。现在:
worker 的 token 按 crew 累计 ✓;落定时写成一条带 token 字段的系统备注进会话 ——
统计与成本随之把 crew 算进去 ✓(字段不进模型请求正文)✓;crewStatus的返回也带上
[usage] …✓。- 实测(1 子任务 crew,假端点):落定后会话出现"消耗 12.8k tokens(in 12.0k / out 800)"
的系统备注 ✓;统计 totalTokens 精确 +12800、归入"子代理"桶 ✓;领队聚合轮的 12800
单独计 ✓ —— 对账无重复、无遗漏 ✓。
- 实测(1 子任务 crew,假端点):落定后会话出现"消耗 12.8k tokens(in 12.0k / out 800)"
-
Agent 面板输入框自动聚焦(优化清单 P0;用户此前反馈过"必须先点一下输入框"):
打开面板或从子页回来时,焦点自动交给输入框(跳一帧 + 400ms 延迟 ——@FocusState
在 onAppear 事务里直接置真走不进 AppKit 的 first responder,AGENTS 记录在案);
停在子页(轨迹/统计等)时不抢焦点 ✓。 -
地址栏:输入被立刻清空 / 候选闪一下 / 有网址时不出候选(用户反馈):三个症状是同一个 bug ——
聚焦时那次"把缓冲播种成当前 URL"的写入(Toolbar.swift的.onChange(of: isUrlFocused)分支)✓。
它本来就多余(未聚焦期间.onChange(of: displayedURL)一直在同步),而它依赖的聚焦通知是
晚一帧到的(地址栏是 NSViewRepresentable,同步置位会报 "Publishing changes from within view
updates",所以当初跳了一帧)✓ —— 于是"点进去马上打字"时,这一帧的播种把刚打的字覆盖回 URL ✓;
紧接着候选浮层因为失焦而收起 ✓(看起来就是"闪一下")✓;"有网址时输入不出候选"同样出在这里
(缓冲被重置成 URL,模型自然不会为"你打的那串"给候选)✓。- 修法:聚焦不再播种 ✓;并给"失焦"通知加了真实状态校验(跳一帧后 field editor 还在 = 仍在
编辑中,不报失焦)✓。
- 修法:聚焦不再播种 ✓;并给"失焦"通知加了真实状态校验(跳一帧后 field editor 还在 = 仍在
Fixed
-
Keychain 授权窗能把应用钉死在启动里(本次发版冒烟抓到,v0.3.13 同样中招):
本地构建是 adhoc 签名——每次重建/换路径 cdhash 都变,Keychain 条目的 ACL 认不出
当前构建时,SecItemCopyMatching会向 SecurityAgent 申请授权;实测那个授权窗
可能永远不渲染(进程和窗口都在、屏幕上什么也没有),而AgentPreferenceStore.init
在applicationWillFinishLaunching的主线程上同步等它 —— 整个应用死在启动里,
且之后每次启动都排在同一个隐窗后面,全部挂死(桥无响应、进程活着、无崩溃报告,
与"主 actor 僵尸"外观一致但根因不同)。- 修法:启动与回合中路径的 Keychain 读全部改为非交互
(kSecUseAuthenticationUI = Fail:失配时失败成"无 Key",绝不等 UI)——
init 的refreshKeyState、迁移读、secretsForRedaction(桥驱动的回合没有用户在场)、
critic 档案检查、PasswordStore.loadAll(init 全量读);设置页等用户在场的路径
保持交互(授权窗可答,答一次"总是允许"即恢复)✓。adhoc 构建换路径后 API Key
显示为未配置属预期,设置里重存一次即可 ✓。
- 修法:启动与回合中路径的 Keychain 读全部改为非交互
-
脱敏代码把应用打成了"僵尸":主 actor 永久卡死,界面却照旧(排查"桥端点忽然没响应"时
挖到底):SecretRedactor的NSRange在循环外算了一次 ✓,而循环里每次都改写文本
([redacted]比任何命中都短 → 串必然变短 ✓)—— 下一轮firstMatch带着越界的旧范围
调用 Foundation ✓,直接抛NSRangeException✓。异常从 Swift async 帧里穿出去(工具结果
入会话这条路径 ✓),被 HIServices 的处理器吞掉 ✓,主 actor 的执行器就此损坏:此后所有
@MainActor任务只是排队、永不执行 ✓ —— 桥端点全部无响应(连/state都挂)✓、数据文件
停在出事那一秒 ✓,但窗口照常渲染、AppKit 事件循环照常、甚至还能被osascript quit
优雅退出 ✓,所以看起来完全不像崩了(没有崩溃报告 ✓,只有log里那行 NSRangeException)。- 修法:
NSRange每轮重算、现算现用 ✓;并审计了全仓另外两处正则
(MarkdownRendererView/DevToolsPanel)—— 都是现算现用 ✓,只有这一处踩了。
- 修法:
-
悬空 tool_calls(工具执行中途被杀 → 之后每一轮请求都被服务端拒绝)
(优化清单"检查点/恢复"的最小切片):应用在工具还没跑完时崩溃/退出,最后一条
assistant 的 tool_calls 没有等到结果 —— OpenAI 兼容服务会拒绝
("assistant message with tool_calls must be followed by tool messages"),且之后每一轮
都拒绝,会话等于报废(模型永远答不上来)。
现在构造请求前把末尾悬空的调用剥掉(正文保留;正文也为空则整条丢弃)✓,
只处理崩溃形态,不猜更多 ✓;单测 4 项(剥字段/整条丢弃/正常会话不受影响)✓。
安装
未签名构建(CI 无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.3.14-macos-arm64.zip(双击,或
ditto -x -k Desire-v0.3.14-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
v0.3.13
Agent 补上闭环的后半段:独立评审档案、对话轨迹(页面 + JSONL)、回答质量反馈、
收到收尾的反思与机械核验、历史对话检索,以及输出护栏(凭据在进入对话之前就被屏蔽)
与成本估算 / 使用统计面板(token 折算成钱,逐日热力图、按模型趋势与占比);
系统提示词补全(工具索引自动生成 + 安全边界);并修掉一批真问题:脱敏正则越界把主 actor
打成僵尸、回合最后一条回答不落盘、地址栏输入被清空与三条运行时警告、聊天面板按钮压住正文。
Added
-
Token 使用统计面板(用户:"token 统计面板也要加 参照这样的面板"):Agent 面板头部多一个
入口 ✓,页内四段——头条(累计 Token / 单日峰值 / 最长对话 / 当前连续 / 最长连续,
填过单价还会多一个成本)✓、Token 活动热力图(GitHub 风格:列 = 周、行 = 周一到周日,
颜色深浅按当天 token,悬停给"日期 · token · 轮数")✓、每日 Token 趋势(按模型分色 +
近 7 / 30 日切换)✓、模型用量(环形图 + 每模型百分比 / token / 金额)✓。- 数据全部从已存盘的会话派生(token、模型、时间戳都记在消息上)✓ —— 与轨迹页同一个
原则,所以统计永远和聊天记录对得上;桥GET /agent/stats?days=N与页面同一套实现 ✓。 - 两条如实说明的前提(写在页脚):只有服务端上报过用量的调用才计入;本功能上线前的
历史对话没有记录,显示 0 ✓(实测:既有 8 个会话、33 轮全是 0,只有新增的用量有数)✓。 - 没有模型归属的用量(子代理)单列**"子代理"**一行 ✓;金额沿用"没单价就不显示"的规矩:
只要有一笔没定价,总额就不显示(各模型自己的金额照常显示)✓。 - 对账实测:造一份跨 40 天、4 个模型、含空日与连续段的合成数据,桥端点与独立重算
(Python 直接读会话文件)逐项一致 —— 累计 215860 / 峰值 51600 / 当前连续 5 / 最长连续 8 /
最长对话 2160300s / 各模型 {gpt-4o 100440、gpt-4o-mini 44620、deepseek-chat 35960、
glm-4.5 30540、子代理 4300} ✓;填上单价后各模型金额也与手算一致
(0.3024→$0.302、0.008007→$0.0080…)✓。 - 布局在 380 / 420 / 900 三种宽度下各渲染验证 ✓(面板可拖宽拖窄,窄面板最容易挤坏)——
走新增的GET /panel/snapshot?name=agentstats&w=&h=(进程内渲染,不依赖屏幕录制)✓。 - UI 重做(用户:"使用统计 UI 还要继续美化 还要考虑窗口是可以拉宽的"):每段改成
卡片(淡底 + 发丝描边 + 12pt 圆角 + 分区图标)✓;头条变成一条卡片里按列数排开、
列间竖分隔线(宽面板一行六格、窄面板自动两列三行)✓;热力图与模型列表按可用宽度
换档 / 铺满剩余宽度 ✓;内容限宽 1100 并居中 ✓——不限宽的话热力图最多 53 周,
拉到 2000pt 时右边会空掉一大半(比对称留白更难看)。 - 顺带修掉三处:趋势图图例列出了没画的模型(Charts 按 domain 出图例,得只给画出来的
前 5 个)✓、窄面板下热力图档位跨度太大右侧空一块(补 30/22/14 周三档)✓、
月份标签落在相邻列时会撞在一起("5月6月")✓。 - 空状态:一条用量都没有时不再给一张空仪表盘,改成一条说明卡并如实告知"已保存 N 轮
对话" ✓ —— 这正是现有用户第一次打开时看到的样子(本功能之前的历史对话没有用量)✓。 - 按 380 / 700 / 1100 / 1500 四个宽度渲染验证 ✓(含空状态)✓;三语共 18 个新键
(目录 1238 键、零缺口)。 - 热力图改为"量宽度、现算格子边长"(用户:"方块要加点圆角 要撑满宽度啊 自动调整"):
原来用固定档位表(53/44/34…周 × 11…18pt),档与档之间必然落差几十到上百点——
700pt 面板挑中"34 周 @13pt = 508pt",右边就空 136pt ✓。现在先用"格子不小于 11pt"
反推能放多少周(上限 53 周 = 一年),再把宽度平分给这些周,任何宽度都正好铺满 ✓;
圆角也随格子边长走(min(5, max(2, 边长×0.24)))✓,小格子不会变成圆点、大格子看得出圆角 ✓。
实测 420 / 700 / 1100 三档铺满效果 ✓。 - 配套:桥的快照要多等一拍才能拍到"量完宽度"的那版;等法是
Task.sleep而不是
RunLoop.current.run—— 后者在 async 上下文里阻塞协作线程池(Release 构建报两条
"unavailable from asynchronous contexts",Debug 增量构建看不出来)✓。
- 数据全部从已存盘的会话派生(token、模型、时间戳都记在消息上)✓ —— 与轨迹页同一个
-
成本估算:把 token 折算成钱(对照评估里"成本与延迟平衡"那条——此前只有 token 计数与
上下文占用,于是"轻任务走轻模型"没法度量):面板状态行多一个成本 chip ✓($0.038),
悬停给出本对话的 in/out token 与金额 ✓;轨迹页总览多Tokens/Cost✓、每个回合的摘要行带
12.8k tok · $0.038✓、展开能看到这一回合实际用的模型 ✓;桥GET /agent/trace的
stats / 每回合 / 新增的usage段与面板同一口径 ✓。- 单价由用户填(设置 → Agent → 成本,或桥
GET|POST /ai/prices),口径为美元 / 每百万
token ✓。不内置价格表(服务商改价是常事,内置一份很快变成错的信息)、也不做前缀匹配
(gpt-4o会顺手套到gpt-4o-mini头上,差 10 倍)✓——填了才算。 - 诚实性规则:没填单价 → 只显示 token、绝不显示
$0(那会被读成"免费")✓;
一段对话里只要有一笔没定价,总额就不给(改标≥)✓;比 4 位小数还小的非零值写成
< $0.0001✓(四舍五入成$0.0000是另一种谎)。
实测:12000 in / 800 out +$2.5/$10单价 →$0.038(与手算一致)✓;换成未定价模型
再发一轮 → 会话总额消失、该回合仍只显示 token ✓;改单价后已有的对话立刻按新价重算 ✓。 - 落盘:逐条消息记 token 与产生它的模型(响应里的
model优先——网关会路由/改写,
成本得按真跑的那个算)✓,所以历史会话也能重新定价、重新算钱 ✓。 - 子代理的用量记在
spawnSubagent的工具消息上 ✓:它跑在自己的消息数组里、不进会话,
不认领的话对话成本会明显少报(一次 crew 可能比主循环本身还贵)。
已知缺口:多标签 crew、自评 critic、标题/记忆整理这些旁路调用不计入对话成本。 - 三语 8 个新键(目录 1214 键、零缺口)。
- 单价由用户填(设置 → Agent → 成本,或桥
-
桥端点
POST /conversations/delete(E2E 收尾清测试遗留用):按id/ids批量删会话 ✓。
写操作走UI 持有的那份 store(活会话 →AppState→ 读盘兜底,响应里如实回报
scope✓)——用新实例删只会删掉盘上的文件,界面列表里那一行还在 ✓。
实测:删完等过DiskStore的 500ms 防抖,检索里就没有了 ✓。 -
输出护栏:凭据在进入对话之前就被屏蔽(用户:"继续" → 输出护栏):两道判据,一严一宽 ✓
——① 应用自己配置的密钥(所有服务档案的 Key)精确匹配 ✓(自建网关的 Key 没有任何
形态特征,只能靠这条认 ✓);② 常见密钥形态(OpenAI / Anthropic / GitHub / AWS /
Slack / Google / GitLab、Bearer …、PEM 私钥块)保守匹配 ✓ —— 每条都带长度下限,
宁可漏也不能误伤普通正文(正文里出现 "sk-" 很正常 ✓)。- 屏蔽发生在消息入会话之前(工具结果追加时就地脱敏 ✓ + 回合收尾对助手文本再扫一遍 ✓),
于是会话文件、轨迹文件、发往模型服务的请求三者都拿不到原文 ✓ —— 模型看不到,
也就无从复述 ✓。工具最容易把凭据带出来(cat一个配置、curl -v打印请求头…)✓,
而把这段结果交给第三方模型服务,正是这条护栏最该堵的路 ✓。 - 实测(假端点 + 让模型调
readFile读一个含假密钥的文件):工具结果进对话时已经是
OPENAI_API_KEY=[redacted]/Authorization: [redacted]✓;再把模型实际收到的
工具消息原文回显出来(MODEL-SAW>>>…)✓ 也全是[redacted]✓ —— 端到端证明上游
从没看到原文 ✓;轨迹里那一步的result同样是脱敏后的文本 ✓。
- 屏蔽发生在消息入会话之前(工具结果追加时就地脱敏 ✓ + 回合收尾对助手文本再扫一遍 ✓),
-
独立评审档案(critic)(评估里"自评是自己在评自己"那条):设置 → Agent 里新增
评审者 选择 ✓ —— 选另一个服务后,reflect工具与回合收尾的自动自评都走它 ✓
(评审者 ≠ 被评审者 ✓ 换一个模型才能看到被评审者自己看不到的问题 ✓);保持"与当前
对话同一服务"就是原来的同模型自评 ✓。- 实现上不碰当前会话的模型 ✓:给评审单独建一个轻量偏好实例(provider 是无状态的、
只读传入的 prefs ✓)→ 无竞态、不影响正在进行的回合 ✓。 - 评审档案没配 Key 时自动回退到当前档案 ✓ 并记一条 error 日志 ✓ —— 否则自评会因为
"API Key not configured" 静默失败 ✓,用户看到的只是"自评忽然不工作了" ✓(实测踩到 ✓)。 - 实测(两个假端点):主对话走 A、评审走 B ✓ —— 会话里存下的评语正是 B 独有的文本 ✓,
证明评审确实换到了独立服务 ✓;A 全程没有收到评审请求 ✓。 - 新增 3 条三语文案(目录 1206 键、零缺口)。
- 实现上不碰当前会话的模型 ✓:给评审单独建一个轻量偏好实例(provider 是无状态的、
-
轨迹页(可观测性落地到 UI)(用户:"trace 关键"):
AgentTrace的派生内容现在有个
页面能看了 ✓ —— 面板头部新增轨迹入口 ✓,页内分三段:- 总览:回合数、工具调用数、失败/被拒、平均工具耗时、未验证回合数、👍/👎 计数 ✓;
- 工具:最慢的几个(平均耗时)与最易错的几个(失败/调用)✓ —— "哪个工具慢、哪个爱挂"
一眼可见 ✓; - 回合:逐条展开 Thought → Action → Observation ✓(动作 + 参数摘要 + 观察 + 每步耗时 +
denied/threwError标记 + 回答 + 自评 + 未验证提示 + 用户投票 ✓),顶部可切换会话 ✓。 - 数据全部从会话派生(
AgentTrace一份口径 ✓,与桥的GET /agent/trace同源 ✓,
stats也一并在该端点返回 ✓),所以这一页与历史消息永远对得上,也不需要在热路径埋点 ✓。 - 实测:真实会话 9 个回合 →
toolCalls=8 / failed=1 / avgToolMs=104.9 / unverified=2✓,
并用独立重算对账(从导出的 JSONL 自己统计得 8/1 ✓ 与 stats 完全一致 ✓)。 - 新增 11 条三语文案(目录 1206 键、零缺口)。
-
轨迹导出(Thought → Action → Observation)+ 分工具耗时(评估里"没有结构化轨迹"那条,
它是后面所有分析的前提):新增AgentTrace,把一条会话编译成一行一个回合的 JSONL——
每行含目标、步骤(动作 + 参数 + 观察 + 耗时 ms + 两个如实命名的失败标记denied/
threwError)、最终回答、自评、机械核验、以及用户的 👍/👎 ✓。- 轨迹从会话本身派生,不另存一份:会话里已有目标/调用/观察/回答/自评/反馈 ✓;唯一
派不出来的是工具耗时 ✓ → 所以它记在工具消息上(AgentMessage.toolDurationMs✓)
随会话落盘 ✓ → 历史回合也能导出 ✓,且导出的内容与用户看到的永远一致 ✓。 - 桥:
GET /agent/trace?conversation=<uuid>&limit=N✓(默认活动会话 ✓)。 - 聊天里也直接显示耗时:工具卡片上标
0.1s/1.2fs✓ —— "哪个工具慢"一眼可见 ✓。 - 实测:真实会话导出 2 个回合并逐字段核对 ✓;新回合的
ms是真实数字(如getPageTitle ms=104.9✓),加计时之前的老回合如实为null✓(不编造 ✓);失败标记与机械核验同一
口径(只认应用自己写的两种 ✓)。
- 轨迹从会话本身派生,不另存一份:会话里已有目标/调用/观察/回答/自评/反馈 ✓;唯一
-
回答质量反馈(👍/👎)(评估里"最便宜也最真实的标签"那条):助手回答 hover 时出现
两个小按钮(与复制按钮同规格、同排 ✓),投过票会保持高亮 ✓;投票随会话文件落盘 ✓
——这是将来做评估集的第一桶数据 ✓。- 存储
AgentMessage.feedback(up/down/清除 ✓);面板走活动会话 ✓,桥端点
POST /agent/feedback {"messageId","vote"}找不到活动会话里那条时会落到已存盘的会话 ✓
并如实回报scope: live|saved✓(此前那种"静默无效却报成功"的情况已修 ✓)。 /agent/messages现在带每条消息的id✓(自动化要用它投票 ✓)。- 实测:
down → 落盘 ✓ → 改 up ✓ → 清除 ✓,端点回报scope: saved✓。 - 注意:按钮用回调下传而不是
@EnvironmentObject✓ —— 面板是显式传参持有 store 的 ✓,
环境里没有它 ✓,用 EnvironmentObject 会在运行时直接崩 ✓。 - 新增 2 条三语文案(目录 1195 键、零缺口)。
- 存储
-
回合收尾的机械核验(对照评估里"缺机械核验"那条,0 次模型调用):只看客观事实,
抓两类"连自评都可能漏掉"的情况,结果作为橙色不折叠的提示挂在回答下方:- 硬提示(可证):本轮每一个工具调用都带应用自己写的失败标记(拒绝执行
[User denied…]或 JS 异常Error:)→ "上面的回答背后没有真正执行过的东西"。 - 软提示(保守措辞):没有任何一条工具返回看起来是成功的 → "请当作未经验证"。
刻意不冒充确证——普通工具失败没有统一约定,靠关键词猜会误报。 - 另有"同工具同参数被拒/报错两次"的提示(反复重试同一调用通常没用)。
- 实测(假端点驱动,含一次真实的
executeJS失败):提示准确出现 ✓。三条新文案三语齐全
(目录 1193 键、零缺口)。
- 硬提示(可证):本轮每一个工具调用都带应用自己写的失败标记(拒绝执行
-
Agent 会反思自己的做法了(对照现代 Agent 框架里最明显的缺口"学习/反思"):
- 自动自评:一轮里跑了 3 个以上工具、或碰过高风险动作(如
executeJS)时,
回合收尾让同一个模型回头审一遍自己的轨迹——"有没有没验证就宣布完成"、"有没有失败
被吞掉"、"有没有更简单做法"、"有没有漏掉要求" ✓;结果作为折叠的"自评"块挂在回答
下方 ✓,普通闲聊不触发(不多花模型调用 ✓),设置里可关(默认开 ✓)。 - 按需自评:新增只读工具
reflect(question?)✓ —— 模型在关键节点可以自己要求审一遍 ✓,
评语返回给模型 ✓ 让它先修正再回答;用户问"你确定吗/检查一下"也能直接触发 ✓。 - 轨迹从已有消息派生(最后一条 user 之后的工具调用 + 观察 + 结论 ✓),不在热路径上额外
记账 ✓;自评失败/取消一律静默 ✓(绝不能把一轮正常回合变成失败 ✓);跑在
isProcessing = false之后 ✓(沿用"别让收尾占着忙碌状态"的既有约定 ✓)。 - 端到端验证(假端点驱动):① 一轮 3 个工具(含一个真实失败)→ 收尾自动发起自评调用 ✓ →
评语准确指出"「都跑完了」没附工具返回,属于未验证结论" ✓;② 模型调用reflect→
嵌套自评 → 评语作为工具结果回到模型 → 模型据此修正 ✓。
- 自动自评:一轮里跑了 3 个以上工具、或碰过高风险动作(如
-
Agent 能检索自己的历史对话了(对照现代 Agent 框架的"记忆/检索"缺口):此前模型
看不到自己的过去——只有用户在历史面板里能搜,模型遇到"我们上次说的那个…"只能让用户
自己去找。- 新增两个只读工具:
searchConversations(query, limit?)(标题 + 正文,大小写不敏感,
返回 id/标题/时间/条数/命中处前后各 140 字的片段,自动排除当前对话 ✓ 它已在模型
上下文里)与readConversation(id, maxChars?)(按角色标记的紧凑转录,超长截断)。 - 检索逻辑落在
ConversationStore.search(_:limit:excluding:),与历史面板的搜索同一口径;
桥新增GET /conversations/search?q=&limit=(走同一份代码,便于回归)。 - 验证(你的真实数据 + 端到端):
q=视频命中 2 条(标题命中 + 片段);q=github命中 1 条
(正文命中,片段带上下文);再用假端点让模型发起searchConversations调用——app 执行后
会话里出现带真实检索结果的 tool 消息,模型据此继续作答 ✓。
- 新增两个只读工具:
-
Agent 对话历史列表换成原生
List(用户需求:"要支持批量操作 要支持左右滑动操作
原生 list 那种"):此前是ScrollView+ 自绘卡片的列表,批量与滑动一个都没有。- 批量操作:⌘/⇧ 点击多选(
List(selection:)原生行为);选中 1 条=打开该会话
(Mail 式语义),选中多条=进入批量模式——顶部出现操作条(N 已选/ 全选 / 删除 /
取消选择),删除只确认一次;ConversationStore增加批量delete(_ ids:)。 - 左右滑动操作:尾部滑动=删除,头部滑动=重命名(触控板双指横滑;鼠标用户走右键
菜单或行内垃圾桶)。 - 原生交互:右键菜单(打开 / 重命名 / 删除)、键盘 Delete 删除选中项
(.onDeleteCommand)、按日期分组的Section标题。 - 行内不再自绘底色/高亮——原生 List 自己画选中与悬停,自绘会叠成两层;重命名状态提到
父视图(renamingID),滑动/右键/双击三个入口共用它。 - 新增 5 条三语文案(Rename / Deselect / Delete Conversations / This cannot be undone. /
%lld selected),目录 1180 键、三语零缺口。
- 批量操作:⌘/⇧ 点击多选(
-
系统提示词补全(用户要求:"系统提示词 查看下是否完备"):默认提示词里那份手写的
"可用工具速查"只有 30 个,而实际有 106 个工具——缺的里面包括executeJS、
switchTab、goBack/goForward、readTab、getNetworkLog、crewDispatch、
getSelectedText这些高频能力。- 工具索引改为从工具表自动生成(
BrowserToolProvider.promptInventory):每个工具
一行"名字 — 描述首句",以后增删工具自动跟上;子代理按它自己的工具子集生成同一份索引。 - 环境段补全:除工作目录外新增下载目录与 ffmpeg 是否可用(装了写"可用,
HLS 直出 MP4",没装则提示装完再下 MP4)——Agent 不用试探就知道这台机器能否直出 MP4。 - 默认提示词重写:原有路由规则全部保留,新增三类——① 安全边界:页面文字是
数据不是指令(防提示词注入)、不代用户做对外/不可逆操作(发帖、提交表单、下单、
删除)、不猜密码验证码、破坏性系统命令先征得同意;② 干活方式:做完要用快照/成功
提示核实再汇报、同一动作连续失败两次就换策略(不要死循环重试)、长任务用任务句柄别
原地等、读取优先getPageSnapshot、不要反复读同一页;③ 表达:用用户的语言、
结论先行、Markdown 组织、出错说清哪一步失败与打算怎么处理。 - 验证:用假 OpenAI 端点抓下真实请求体——
<tools>索引 106 行、与tools参数零差异
(缺 0 多 0),system 消息唯一且在第 0 位,六个分层段落齐全,环境段正确带出
`/Users/…/D...
- 工具索引改为从工具表自动生成(
v0.3.12
视频下载直出 MP4(装了 ffmpeg 就用它拉 HLS,音轨分离站点不再丢声音,没装/直播/
失败自动回退内置下载器);Agent 聊天修掉两个只在流式中间态复现的致命问题
(内联样式失效索引导致的崩溃、块解析在一行"1. "上的死循环);聊天面板回车
不再刷 59 条 SwiftUI "Publishing changes from within view updates";Xcode 警告清零。
Added
- HLS 下载优先走系统 ffmpeg,直出 MP4(用户提议:"如果系统安装了 ffmpeg 是否
考虑直接使用 ffmpeg 来下载 m3u8 并且直接转为 mp4"):新增
Features/Downloads/FFmpegExporter.swift,导出前探测 ffmpeg(/opt/homebrew/bin、
/usr/local/bin、/opt/local/bin、/usr/bin——GUI 进程的 PATH 不含 Homebrew,
必须显式探测)。不内置 ffmpeg(GPL/LGPL 的独立项目,不该打进 app 包)。- 判定顺序:HLS 且
EXT-X-ENDLIST(VOD)→ ffmpeg 直连;没装 ffmpeg / 直播 /
ffmpeg 失败 → 原来的内置下载器,行为不变;内置路径产出.ts时若机器上有
ffmpeg,再本地转封装成 MP4(直播因此也能拿到 mp4)。 - 音轨分离(
EXT-X-MEDIA)站点不再丢声音:这类站点的媒体播放列表里只有视频,
只有把 master 交给 ffmpeg 才会把AUDIO="…"的音轨接上(实测 master →
h264+aac,只给 variant → 只有 h264;内置下载器正是后者,属于原有缺陷)。 - 码率上限(桥的
maxBandwidth)用-map 0:p:N选第 N 个 variant(program 顺序
= 播放列表文件顺序,不是按码率排序的),并连带它的音频组;不限速时交给
ffmpeg 自己挑(实测它选最高码率)。 - 防盗链走
-referer/-user_agent;-progress pipe:1 -nostats解析
out_time_us换算进度(总时长取播放列表EXTINF之和)。 - 实测定下的几条硬约束(
FFmpegExporter里逐条有注释):①-map是输出
选项,放在-i之前会被 ffmpeg 拒收(退出码 234);② 站点常用没有.m3u8
后缀的播放列表、.bin分片、完全无扩展名的分片,ffmpeg ≥7.1 默认的
-extension_picky一律拒收——要-f hls -allowed_segment_extensions ALL -extension_picky 0,且老版本不认这些选项("Unrecognized option")时自动退回
只给-f hls重试;③ live 播放列表绝不能交给 ffmpeg:它没有新分片就一直
等,-t也拦不住(实测 40s 不退出);④ 取消走 SIGTERM(实测 0.00s 退出、不留
残件),仍会兜底删文件。 - 验证(app 内、经桥
/media/download的真实路径,端到端 9 例全过):音轨分离
master → mp4 640x360 video+audio、maxBandwidth400k→640x360 / 5M→960x540、
音轨分离+上限 → audio+video、直播 → 内置+转封装出 mp4(带 live 提示)、
无后缀播放列表 /.bin分片 / 无扩展名分片 → 均由 ffmpeg 出 mp4、
段全 404 → 失败且不留空文件。产物用ffprobe核对容器、流与分辨率。
- 判定顺序:HLS 且
Fixed
-
聊天面板里按回车不再刷 "Publishing changes from within view updates"(用户贴出的
Xcode 运行时警告,一次回车 59 条):.onKeyPress的处理器在 SwiftUI 的更新事务内
执行,而提交消息会写十几处@Published,于是每条写入都报一次。现在回车、⌘↩、Esc 取消
以及"语音结束后自动发送"这几条路径都跳到下一个主线程回合再动 store——行为不变,
只是不再在视图更新中发布。- 定位手法(可复用):这些警告也会进统一日志
(subsystem == "com.apple.runtime-issues",--style json还带线程/activity/调用栈)。
本次 59 条挤在 36ms 内、同一线程同一 activity ——说明是一个动作连环发布而非用户点了
59 次;再看同一时刻的 app 日志,紧跟在LegacyTextInputActions signal:DidAction
(键盘输入)之后、SecItemCopyMatching+MCP(回合启动)之前,对上被标记的
sendMessage写入行,触发点就锁定了。 - 顺带确认(有证据、别再瞎改):
.onReceive(CommandBus)(菜单/桥命令)与
.onChange(initial: true)这两条路径不触发该警告。
- 定位手法(可复用):这些警告也会进统一日志
-
Xcode 编译警告清零(用户贴出的清单,逐条修):
DevToolsPanel:DOM 树分支里if let root = …的root从未使用 → 改成
if treeRoot != nil。DevToolsStore.sameSiteLabel:HTTPCookieStringPolicy是 struct,写
policy == .none实际在跟Optional.none比、恒为 false,导致"看 properties
里有没有 samesite 键"那段成了死代码(没显式声明 SameSite 的 Cookie 也会被挂上徽章)
→ 判定完全改走 properties。MediaExportStore:@preconcurrency import UserNotifications,并且不再把
UNUserNotificationCenter(非 Sendable)捕获进 @sendable 回调,改为各自取
.current()。MarkdownRendererView:parse标了nonisolated(要在 detached 任务里跑),
但它的四个辅助函数还是 MainActor 隔离 → 一并标nonisolated。
-
设置页服务档案行每次重绘读两次 Keychain:
SecItemCopyMatching是阻塞系统调用,
Xcode 的 Performance Diagnostics 会报 "This method should not be called on the main
thread as it may lead to UI unresponsiveness"(同一次运行里 12 条)。同一个StatusPill
的两个分支各读一次 → 现在只读一次复用。 -
一个段都没下到时不再"假装成功":内置下载器在全部段失败时会留下一个 0 字节
的.ts并报完成(用户点开是空的)。现在会删掉残件并报
Every segment failed to download — nothing was saved;与 ffmpeg 的失败原因合并成
一条错误(fallbackFailed),两条信息都不丢。 -
Agent 消息渲染的死循环已修复(块解析器不推进):
MarkdownParser.parse的有序列表
分支里入口条件用原始行、循环条件用 trim 后的行,于是"1. "这种行(首字符是数字、
以 ". " 结尾)能通过入口,进循环后却匹配不上(尾空格被 trim 掉,". " 不复存在),三个
分支全不中 →else里break退出内层循环,但i一次都没加 → 外层continue
回到同一行,无限循环,同时把空列表块越堆越多。- 触发面正好落在流式输出上:模型写有序列表时的中间态就是
"1. ";而完整落盘的消息
里不常有这种行——350 条历史消息整体跑一遍不复现,只有盯着实时流式才会撞上。 - 后果:解析跑在
Task.detached里,而.task(id:)的取消不会传给 detached 任务,
所以每撞一次就永久泄漏一个满核空转的解析任务,blocks同时无限增长——表现为聊天
卡顿、发热,最终可能被系统杀掉。 - 修法三层:① 入口条件改用 trim 后的行(
"1. "现在正确渲染成段落,而不是凭空
消失);② 循环顶部加defer { if i == iterationStart { i += 1 } }兜底——任何分支
忘了推进都会被补上,解析结构性终止,不依赖每个分支自觉;③parse新增
isCancelled参数(并标nonisolated),视图用withTaskCancellationHandler把
.task的取消显式转发进去,文本一变化旧解析立刻收手。 - 验证:
parse("1. ")修前必卡死(跟踪里同一行重复了几百次)、修后立即返回
para("1. ");对抗语料 1418 条(含 200 条真实历史消息)+ 9600 条专攻分支
条件(数字开头 + 尾随空白/制表符、CRLF/CR、阿拉伯-印度数字、全角数字、混合标记),
每条再按 1/8 前缀各渲染 9 轮跑完整管线(解析 → 每块内联),合计 99,162 次
((1418 + 9600) × 9),全部通过,无卡死无崩溃。
- 触发面正好落在流式输出上:模型写有序列表时的中间态就是
-
聊天渲染路径的下标改为快照枚举:
ForEach(x.indices, id: \.self)配x[index]是
SwiftUI 的经典越界形态——流式期间块数会减少(围栏一开吞掉后面几块、列表合并),
下标当 id 时 SwiftUI 可能在缩容的那次更新里拿旧下标去取新数组 →Index out of range。- Markdown 渲染器 5 处(块、无序列表、有序列表、表头、表体)与输入框附件条 1 处,
全部改成ForEach(Array(x.enumerated()), id: \.offset),闭包内只碰快照值。 - 流式尾部写回由"append 时记下的下标"改为按 message id 找回(
flushTail):会话
在流式中途被清空/切换时,旧下标会指向另一条消息(把 token 写进无关消息),数组变短
后还会越界。
- Markdown 渲染器 5 处(块、无序列表、有序列表、表头、表体)与输入框附件条 1 处,
-
Agent 消息渲染崩溃(EXC_BREAKPOINT / SIGTRAP)已修复(用户报告:"看一下智能体最近
的聊天记录 导致崩溃了"):MarkdownRendererView.buildInlineContent原来在同一个
AttributedString上按link → bareURL → code → bold → italic顺序做五次
replaceSubrange,但每一趟的 range 都是按原文本匹配出来的,而Range(_:in:)只做
偏移映射、并不知道字符串已经被前一趟改短了。偏移一对不上,替换边界就会落在多字节字符
中间,接着在AttributedString.Guts.replaceSubrange内部触发
CollectionsInternal/BigString+Chunk+UnicodeScalar.swift:137断言,进程直接 SIGTRAP
(崩溃报告Desire-2026-09-23-002707.ips:EXC_BREAKPOINT ← CollectionsInternal ←
buildInlineContent,触发点是列表项里的粗体一趟)。- 最小复现(同一个函数、
-Onone,且用应用里逐字一致的正则验证过):[a](b)**粗***斜体*
— 链接那趟先把字符串缩短了,粗体、斜体两趟却还拿着旧偏移继续替换。纯 ASCII 的
[](u)**a***b*不复现,必须有多字节字符参与才会命中 scalars 中间;19 条历史对话的
373 个内联块(含每块 8 个流式前缀共 2984 次渲染)在已落盘文本上也都不复现——
崩溃发生在流式的中间状态,所以是按崩溃栈定位的根因。 - 改法:先收集片段、再一次性拼接。按原文本匹配出所有 span(重叠时先占先得:
链接 > 裸链接 > 行内代码 > 粗体 > 斜体,与旧顺序语义一致),按位置排序后逐段切原文
拼进结果。全程不再对已经变过长度的AttributedString使用旧索引,这类失效索引在
结构上不再可能出现。样式与旧实现完全对齐(链接用强调色 + 下划线、行内代码等宽 +
底色、粗体/斜体各自字重字型)。
- 最小复现(同一个函数、
安装
未签名构建(无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.3.12-macos-arm64.zip(双击,或
ditto -x -k Desire-v0.3.12-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
v0.3.11
调试面板大扩建 + Agent 流式成熟化。DevTools 四页签补全(Console
REPL 与真对象、Network 长连接与拦截动作、Element DOM 树与 CSS 级联、
Application 全存储读写);模型服务重做为一等公民(自定义端点自带
Key / 模型清单 / 请求头);流式输出的性能与滚动稳定性收敛(卡死、
抖动、无法上滑、"流式不完"全部修复);思考过程折叠块、媒体下载
后台化、AI 识别广告、首击劫持防护、社区过滤列表更新失败三处根因。
Added
-
Agent 输入框支持 ↑/↓ 翻阅输入历史(用户需求:"按照对话记录下输入记录,可以上下
切换最近的输入"):历史按对话记录、随会话文件落盘(重启、切回该对话都还在),
上限 100 条、相邻重复不重复记(连发两次同样的提示只留一条)。- 交互:输入框为空时按 ↑ 取最近一条、继续 ↑ 往前翻;↓ 往新翻,翻过最新一条即回到
空白草稿;手打任何字就退出翻阅(此时 ↑/↓ 交还 TextEditor 移动光标,多行编辑
不受影响)。 - 记录点在
sendMessage(默认记录 = 用户输入):面板提交、排队发送都覆盖;桥与
调度等自动化调用显式传recordHistory: false,不把机器人的提示词混进用户历史。 - 实测:连发三条(其中一条与上一条相同)→ 历史
["第一条历史测试","第二条历史测试"]
(相邻去重生效);重启应用后历史仍在(会话恢复后读回)。 - 桥:
GET /agent/messages新增inputHistory;POST /agent/send新增
recordHistory?:bool(默认 false,回归用)。
- 交互:输入框为空时按 ↑ 取最近一条、继续 ↑ 往前翻;↓ 往新翻,翻过最新一条即回到
-
首次点击劫持防护(用户反馈:"很多视频页面播放按钮第一次点击跳转广告"):影视站
常见套路是播放键上盖一层透明层(站外<a target="_blank">或带 click 处理器的浮层),
第一次点击不播放、先弹/跳广告。新增UserScripts/first-click-guard.js,随"拦截视频
广告"开关、主框架 + 文档开始注入(必须比页面自己的处理器先注册才拦得住):- 只管页面上的第一次点击,且只在页面里存在
<video>时生效(普通网页零干预); - 这一击期间临时禁掉
window.open(脚本弹窗直接失效,1.2s 后恢复); - 若这一击落在站外链接或覆盖视口 ≥25% 的定位浮层上:吞掉点击并把该层
display: none,下一次点击自然落到真正的播放控件;站内链接与播放器控件不受影响; - 拦下的目标经既有的
videoAdBlocked通道上报,提示条显示
已拦截 1 个 <域名> 广告(首次点击防护)(非内置站点直接显示域名,便于定位)。 - 实测(本地 fixture 三种变体):① 覆盖播放器的站外锚点 → 首次点击不开新标签页
(tabs 1→1)且该层被隐藏;② 脚本window.open弹窗 → 首次点击被拦(tabs 2→2)、
1.3s 后window.open恢复可用;③ 站内链接首次点击正常跳转(无误伤)。
- 只管页面上的第一次点击,且只在页面里存在
-
新增 1 条三语文案。
-
思考过程可见、可折叠(用户需求):推理模型的
reasoning_content(DeepSeek / Qwen
vLLM)以及部分网关的reasoning/thinking增量现在会被接收,收敛到该条消息的
reasoning字段,在助手气泡上方以折叠块展示:- 折叠块标题是"思考中…"(带强调色小图标)或"思考过程 + 字数";流式思考时自动展开,
正文一开始就自动收起;用户点过标题之后不再自动切换(手动选择优先)。 - 思考过程不与正文混在一起(正文仍是 Markdown 渲染),也不会回传给模型——
encodeMessage只发 role/content/tool_calls(DeepSeek 等服务回传 reasoning 会直接
报错)。 - 只有思考、没有正文时不再算"空回合"警告的普通情形:会明确提示"模型只输出了思考过程、
没有给出答复"。 - 子代理(spawnSubagent)的思考也照收(同一条消息里可折叠)。
- 桥端点
GET /agent/messages的每条消息新增reasoning(截断 600 字符)。 - 实测(fixture 端点先流 4 段 reasoning_content 再流正文):会话里
reasoning与
content分别落库,内容互不混淆。折叠交互请看面板(截图没法验证交互)。
- 折叠块标题是"思考中…"(带强调色小图标)或"思考过程 + 字数";流式思考时自动展开,
-
新增 4 条三语文案。
-
Agent 的媒体下载不再阻塞对话(用户实测反馈:"下载的时候一直在等待,可以改成后台
异步吗"):downloadMedia此前阻塞整轮直到导出结束——HLS 视频动辄几分钟,面板上
就是"一直在等待"。现在它立刻返回任务 id,导出在后台跑:- 完成后 ① 往会话追加一条 system 备注(面板不渲染 system 消息,但模型下一轮看得
到,用户也能在历史里看到)② 发一条 macOS 系统通知(首次真正要通知时才请求
授权,守 TCC 懒请求约定)。 - 新增
listMediaExports工具让模型自己查进度;桥端点GET /media/exports/
POST /media/exports/cancel。 - 实测:20MB 慢速文件(服务端限速约 10s)——这一轮在 12.3s 结束(模型延迟),此时
任务仍是 running;随后状态转 finished(slow.bin.ts — 1 segment(s), 20.0 MB),
会话里出现一条Download finished: …备注。此前这一轮必须等下载结束才能返回。
- 完成后 ① 往会话追加一条 system 备注(面板不渲染 system 消息,但模型下一轮看得
-
AI 识别广告并一键屏蔽(用户需求:"通过 AI 识别页面广告并且设置拦截"):
- 新脚本
UserScripts/ad-candidates.js:纯启发式只读扫描,返回带理由的候选
(class/id 关键词、跨域 iframe、广告联盟域名、覆盖层 z-index、标准广告位尺寸、
"广告/Sponsored" 文案、块内 iframe),按证据条数与面积排序。 - 新工具
findAdCandidates(扫描并解释)与blockElements(按选择器批量屏蔽:
写 ElementBlockStore 规则 + 立刻把隐藏 CSS 注进当前页,以后每次打开该站点自动
生效;可选blockRequests一并加网络层拦截)。 - 内置技能
clean-page-ads固化了流程:先讲给用户听、由用户挑、只屏蔽广告不误伤正文、
可回滚。 - 桥端点:
GET /ads/candidates、POST /ads/block、GET /ads/rules、
POST /ads/rules/clear(可回滚)。 - 实测(本地 fixture,故意用规则拦不住的形态):5 个候选——跨域广告 iframe
(iframe:ad-host+slot-size)、"Sponsored: Acme Corp" 行(link:ad-host)、
300×250 卡位(slot-size)、全屏 cookie 覆盖层(overlay:z2147483000);屏蔽后
三者display: none,正文标题与段落仍是block(无误伤),规则落库 3 条。 - 顺带发现:class 里带
ad-的元素内置过滤列表已经隐藏(fixture 里那个
.ad-banner实测 0×0),所以 AI 这条链路真正的价值在"规则拦不住的"那类广告。
- 新脚本
-
Console 的对象是"真对象"了:此前参数在捕获时就
JSON.stringify,于是
console.log(document.body)只显示{}(元素没有可枚举自有属性),对象也没法
展开。现在:- 参数按片段上报:文本照旧,对象给短预览(
Object {a: 1, …}、Array(3)、
<h1#title>、Error: …)并登记一个页面侧句柄; - 点 chip 就地展开一层属性——值是活对象、留在页面里,属性仍是对象时给新句柄可
继续展开(句柄表 300 条 FIFO,过期显示"句柄已失效"); - DOM 元素单独给一组字段(tagName / id / className / childElementCount /
textContent / 属性 / 前 10 个子元素各带句柄),因为元素用Object.keys是空的; - REPL 便捷绑定:
$0= 最后检查的元素、$_= 上一次的结果、$(sel)/
$$(sel)简写(页面自己有$就不覆盖)。 - 实测:
console.log('with object:', {a:1,b:'two',nested:{deep:true},list:[1,2,3]})
→ 预览Object {a: 1, b: "two", nested: {…}, …},展开句柄得 4 个属性(嵌套两项
各带新句柄);console.log(document.getElementById('title'))→<h1#title>,
展开得 tagName/id/textContent/@id;REPL:$0.tagName→H1、1+1→2、$_ + 1→3、
$("#title").textContent→console fixture、$$("div").length→1。
- 参数按片段上报:文本照旧,对象给短预览(
-
桥端点:
GET /devtools/console/ref?ref=(展开一个句柄,一层);
GET /devtools的 console 段新增objects(最近消息里的句柄与预览)。新增 2 条
三语文案。 -
Application 页签补上 IndexedDB / Cache Storage / Service Worker(新脚本
page-storage.js,都在页面侧列举,一次只取一层/带上限):- IndexedDB:
indexedDB.databases()列出本站源的库,逐个只读打开(不带
版本号,避免触发upgradeneeded)列出对象存储与条数;行上给
库名 · vN,删除按库(该库所有存储一起删)。 - Cache Storage:按缓存名列出条目(上限 300),行上给缓存名 + 请求 URL;
单条删除 / 一键清空。 - Service Worker:列出注册(scriptURL / scope / state),单个或全部注销。
- 实测(本地 fixture 建 1 库 2 存储 3 条、1 缓存 2 条、1 个 SW):三节分别
列出 2 / 2 / 1 条;删除后 0 / 0 / 0;注销返回{"unregistered":1}。
- IndexedDB:
-
Cookie 可写:值可改(行内编辑,走
WKHTTPCookieStore.setCookie,所以
HttpOnly 的也能写——document.cookie那条路写不了),"+"可新增(域默认当前
页面主机、路径/)。实测桥写desire_probe=42→ Cookie 计数 106 → 107。 -
Application 的节选择移到独立一行:7 个节(新增 3 个)与搜索/动作挤一行放
不下,现在上一行是节、下一行是搜索 + 全部域 + 新增/重载/清空。 -
桥端点:
GET /devtools/application增加indexedDB/cacheStorage/
serviceWorkers三节的计数与样例;POST /devtools/application/set支持
kind:"cookie"(需domain);delete支持indexedDB(key = 库名)、
cache(key =缓存名<TAB>URL)、cacheAll、serviceWorker(无 key = 全部)。
新增 10 条三语文案。 -
Element 页签有了 DOM 树:此前只有"一次一个元素"的检查器,看不到结构。
新脚本dom-tree.js按 nth-child 链一次取一层(懒展开,path形如
0/2/1),行上给tag#id.class+ 文本预览 + 子元素数;点行就用它的
nth-child 选择器跑既有采集链(详情区不变),换标签页自动重挂。实测:
html→head(4 个子元素)/body→div.box→h1#target,选择器与
DOM 一致;失效路径干净报错。 -
命中的 CSS 规则(级联排查):
element-inspect.js顺带走一遍
document.styleSheets,列出能匹配该元素的选择器与声明(上限 40 条),跨域
样式表读不到就计数说明("N 张跨域样式表无法读取"),详情里单列一节。实测
#target→h1 { letter-spacing: 2px }(外部样式表)+
#target { color: rgb(255, 0, 0) }(页内 style)。 -
桥端点:
GET /devtools/tree?path=(一层树,与面板懒展开同路径);
POST /devtools/inspect的返回里加了cssPath/matchingRules/
crossOriginSheets。新增 4 条三语文案。 -
Network 页签补上长连接与发起者:
- WebSocket / SSE:
network-monitor.js包装了两个构造函数,连接本身是一条
请求(状态 101 / 200),每条消息是一帧(phase:"frame",方向 in/out/system,
每条截断 4KB、每条连接保留 200 帧)。详情里按"消息(N)"列表展示,出站用
强调色箭头。实测:页面开一条 WS + 一条 SSE →GET 101 ws://…(4 in / 1 out,
最后一帧in: bye)、SSE 3 帧in: tick-3。 - 发起者(Initiator):fetch / XHR / WS / SSE 的调用点(
url:行)随请求上报,
详情里单列一行。实测行号与页面里new WebSocket(…)/fetch(…)的行一致。 - 图片预览:详情里"Preview Image"按需在页面里 fetch 该资源(带 cookie,
同源必成、跨域看 CORS,上限 512KB)内联显示;另有POST /devtools/preview
把同一路径落盘成 PNG(实测 16×16 fixture → 119 字节文件)。
- WebSocket / SSE:
-
网络拦截接进面板:请求详情的菜单里新增"Block This URL / Block This Host /
Redirect To…"(行内输入目标,不用模态框)——此前InterceptStore只有桥能写
规则。过滤串由InterceptRule.exactFilter/hostFilter生成(WebKit 的
url-filter是正则,URL 里的.?*必须转义)。实测:加规则后重载,
该请求变成GET 0(被拦),清空规则后恢复。 -
桥端点:
POST /devtools/replay(重放一条已记录请求,与面板 ↻ 同路径)、
POST /devtools/preview;GET /devtools的 network 段新增streams(帧数 /
出入方向 / 最后一帧 / 发起者),last行带上发起者。 -
新增 12 条三语文案。
-
调试面板 · 应用页签的存储可读写(不只是看):
- localStorage / sessionStorage 可编辑:点值或铅笔进编辑(回车提交)、
单条删除、"新增键"行内新增。写入走页面 JSsetItem,改完立即重新采集——
实测桥写regress=ok后页面localStorage.getItem立刻读到。 - 新增"扩展存储"子页签:按插件分组列出各自的
chrome.storage.local
(就是插件代码里browser.storage.local看到的那份),可改值 / 新增键 /
删除 / 清空;0.2.13 的共享桶单列为一组(有数据才显示)。写入的值能解析成
JSON 就按 JSON 存,插件读回的是对象而不是字符串。 - 子页签选择移到 store(跨面板重建保持,也便于自动化直接选中某一节)。
- localStorage / sessionStorage 可编辑:点值或铅笔进编辑(回车提交)、
-
桥端点:
POST /devtools/application/set(写/新增 localStorage、
sessionStorage、插件扩展存储),/devtools/application/delete增加
kind:"extension",GET /devtools/application增加section与extensions
(每个插件的键名全量,不只是样例);POST /devtools/config可切applicationSection。 -
GET /panel/snapshot在截图前给异步加载留渲染节拍(约 0.7s 上限)——面板里
localStorage/扩展存储这类.task异步拉的数据,早先拍出来一律是空态。 -
新增 7 条三语文案。
-
调试面板第四轮补全:
- Element 可编辑:内联样式与属性行都可点值直接改(回车提交)、单条删除、
"+" 新增;悬停任一属性/样式行会在页面上给该元素描边。编辑走
el.style.setProperty/setAttribute,改完立即重新采集——实测把h1
的 color 改成红色后,页面的getComputedStyle立刻是rgb(255, 0, 0)。 - Network:缓存命中徽章(
transferSize === 0且已知体积 ⇒ 缓存,来自资源
计时)、复制菜单里新增"导出日志…"(JSON,含方法/状态/耗时/字节/缓存标记/
响应头)、详情里可重放请求(页面内fetch重发同样方法/头/体,结果与
CORS 报错都写进控制台)与"保存响应体到文件"。 - Console:搜索支持正则(写错自动退回普通包含匹配)、导航时清空
开关(默认关,保留日志便于对比两次加载)。 - 桥端点:
POST /devtools/edit(改样式/属性,等价于面板里编辑)、
POST /devtools/config(运行期开关);GET /devtools增加缓存命中计数与
开关状态。新增 14 条三语文案。
- Element 可编辑:内联样式与属性行都可点值直接改(回车提交)、单条删除、
-
调试面板新增 Application 页签(Chrome 同名页签的核心部分):
- Cookie:读的是当前标签页所在的
WKWebsiteDataStore(容器标签、无痕
标签各看各的),因此 HttpOnly 的 Cookie 也在(document.cookie看不到)——
实测 YouTube 页 27 条(含HSID/LOGIN_INFO的 HttpOnly/Secure 标记)。
默认只列当前站点(按域名后缀匹配,含父域 Cookie),工具栏有"全部站点"
开关切到整个数据存储。 - 本地存储 / 会话存储:走页面 JS 读 localStorage / sessionStorage,
带字节数、可搜索、单条复制/删除、一键清空(两步确认,不用模态框)。 - 行内显示 HttpOnly / Secure / SameSite / 会话 Cookie 标记与所属域+路径,
复制支持name=value与document.cookie两种口径(排查登录态最常用)。 - 子页签、搜索、刷新、清空按钮与其余页签同一套观感。
- Cookie:读的是当前标签页所在的
-
桥端点:
GET /devtools/application(Cookie 与两种 Web 存储的计数与样例)、
POST /devtools/application/delete(kind+key)。新增 12 条三语文案。 -
调试面板再扩展(第二轮):
- Network 瀑布条:Time 列改为"相对起始位置 + 时长"的横条(按当前可见集合
最早请求对齐,颜色跟随状态码/失败),右侧仍给毫秒数——一眼看出哪个请求拖了
时间线。 - Network 过滤与批量操作:URL 搜索框、"只看失败"开关、复制全部 URL /
全部复制为 cURL。 - 耗时分解:资源计时的分段(排队 / DNS / 连接 / TLS / 首字节 / 下载)进了
详情面板,来自performance的PerformanceResourceTiming;fetch/XHR 钩子用
墙钟补 ttfb。 - 响应体 JSON 美化:body 能解析成 JSON 就按缩进展示(接口排查的常见场景),
并给"复制...
- Network 瀑布条:Time 列改为"相对起始位置 + 时长"的横条(按当前可见集合
v0.3.10
分栏拖拽终局:HSplitView 原生分栏。v0.3.9 链路(系统
.inspector
六轮实验)经用户实测否定后整体回退、从未发布,版本号跳过。
Changed
- 分栏拖拽重写:分屏右栏 / 媒体查询检查器 / Agent / DevTools
四个尾侧面板全部改为 HSplitView 原生子视图,分隔条与拖拽由
SwiftUI 提供,视图层零拖拽代码。历轮自研机制(冻结状态机、快照
层、等值门控、DragHookDivider,约 300 行)全部删除;面板宽度只经
minWidth/idealWidth/maxWidth 协商,内部固定宽度禁令保留。 - WKWebView 恢复默认不透明:0.3.9 用 KVC 关闭 drawsBackground 试图
消 resize 过场闪,实测反而加剧(透明内容无法旧帧拉伸),撤销。 - MARKETING_VERSION 自本项目首次对齐发布序列(此前恒为 1.0);
build 号 = git 提交数。
Fixed
- favicon 直连候选改用页面原始 origin(此前
http://127.0.0.1:8877
被退化成https://127.0.0.1——丢端口、强改 https,甚至生成
https://www.127.0.0.1);IP/localhost 不再生成 www 变体。 - 过滤列表编译失败不再吞错:两级编译失败均记录错误详情与 JSON
大小;新增"下载到 HTML 页"前置校验(代理劫持场景报真实原因)。 - CHANGELOG 0.3.1–0.3.7 各版本的重复空壳标题(7 处)。
安装
未签名构建(本地构建,无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.3.10-macos-arm64.zip(双击,或ditto -x -k Desire-v0.3.10-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
本 tag 为补发:v0.3.10 就绪当时 GitHub Actions 免费额度用尽,从未发布。
产物由 v0.3.11 发布时从 1dc2029 本地构建。
v0.3.8
分发成熟化:应用内自更新、首启动引导、崩溃回收。
Added
- 应用内自更新(无 Sparkle 依赖):更新横幅新增 "Update &
Relaunch"——下载 Release 的 macOS zip 资产 → SHA256 校验
(对照 SHASUMS256.txt,不匹配即拒绝)→ ditto 解包 → 原子替换
/Applications/Desire.app → 自动重启。仅对装在 /Applications 的
正式包开放;仓库私有时资产 404 会干净报错。 - 崩溃回收:启动哨兵(启动置位/干净退出清除)检测异常终止;
崩溃后 index 是上次干净退出的(过期),改按文件 mtime 取最新
session 文件恢复(15s 定时器崩溃前一直在写),toast 提示"异常
退出后已恢复"。 - 首启动引导:三步窗口(欢迎定位 / 默认浏览器设置 / Agent
配置入口),desire.onboardingDone 门控,自动化模式不打扰。
Verified
- E2E:开 5 个特征标签 → 等 16s 落盘 → kill -9 → 重启恢复全部 5
标签(mtime 路径而非过期 index)。自更新流程的下载/校验/替换需
公开 Release 才能自动化,校验逻辑本地构造 SHASUMS 验证过解析。
安装
未签名构建(本地重建,无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.3.8-macos-arm64.zip(双击,或ditto -x -k Desire-v0.3.8-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
本 tag 为补发:发布当时 GitHub Actions 免费额度用尽,产物为后续
从对应版本戳提交在最新 Xcode 上重建的未签名 Release 构建。
v0.3.7
阅读与研究模式:阅读器可调样式 + 页面批注 v1。
Added
- 阅读器设置:字号步进(14–26)、三级行距、四主题(跟随系统/
浅色/羊皮纸/深色),UserDefaults 持久化。 - 页面批注 v1:
- 划选文本 → SelectionAIBar 尾部四色点(黄/绿/蓝/粉)→ 包裹
<mark>高亮(跨元素选区 extract 兜底)。 - 按页持久化(URL 归一化哈希为桶键;锚定用文本而非 XPath,
动态页面可靠);页面加载时自动按文本重新包裹(已包裹处跳过)。 - Agent 工具
getPageHighlights(只读级):"总结我在这页标了什么"
直达。桥GET /annotations?index=N。 - 同页同文本重复划选 = 改色不追加。
- 划选文本 → SelectionAIBar 尾部四色点(黄/绿/蓝/粉)→ 包裹
Known Limitations
- 笔记侧栏列表与 Markdown 导出留 v2;高亮恢复按文本首次出现锚定,
同文本多次出现时可能包错位置;翻译对照留 v2。
Verified
- E2E:选区包裹(返回文本 + mark 入 DOM)→ restore 按"Sign in"
文本锚定重包 1/1 → collect 接口在位;持久化链路(SelectionAIBar →
store → DiskStore)编译接线,UI 交互待用户验收。
安装
未签名构建(本地重建,无 Developer ID 证书),仅支持 Apple Silicon(arm64,macOS 26.5+)。
- 解压
Desire-v0.3.7-macos-arm64.zip(双击,或ditto -x -k Desire-v0.3.7-macos-arm64.zip .)。 - 安装:把解压出来的
Desire.app拖进/Applications。 - 移除一次隔离标记(未签名构建必需),路径写你放 app 的那个:
xattr -cr /Applications/Desire.app
- 打开
Desire.app。首次启动 Gatekeeper 会检查一次;做过第 3 步就能正常打开。
报
xattr: No such file: Desire.app说明当前目录里没有解压好的 app——
命令要写完整路径(如/Applications/Desire.app),或先cd到它所在目录。
本 tag 为补发:发布当时 GitHub Actions 免费额度用尽,产物为后续
从对应版本戳提交在最新 Xcode 上重建的未签名 Release 构建。