Skip to content

Releases: browser-rs/Desire

v0.4.2

Choose a tag to compare

@siwilizhao siwilizhao released this 27 Sep 09:43

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 对已解析项反复重解析。

Changed

  • 纯逻辑单测新增 BatchMediaPlan 用例(规划 16 项 + 命名风格 9 项,
    共 166 项)。

安装

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

  1. 解压 Desire-v0.4.2-macos-arm64.zip(双击,或
    ditto -x -k Desire-v0.4.2-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 到它所在目录。

v0.4.1

Choose a tag to compare

@siwilizhao siwilizhao released this 26 Sep 13:27

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.py 10 步全绿 + 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×630 og.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+)。

  1. 解压 Desire-v0.4.1-macos-arm64.zip(双击,或
    ditto -x -k Desire-v0.4.1-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 到它所在目录。

v0.4.0

Choose a tag to compare

@siwilizhao siwilizhao released this 25 Sep 08:07

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 到它所在目录。

v0.3.14

Choose a tag to compare

@siwilizhao siwilizhao released this 24 Sep 03:04

Added

  • 中断恢复入口:未回答的提问一键续跑(优化清单"检查点/恢复"的 UX 切片):会话以
    未获回答的用户提问结尾时(工具执行中途被杀的典型残留 —— 提问已落盘、回答没有),
    面板在输入框上方显示"上一条提问还没有收到回答 · 继续回答"提示条 ✓;点击为已有
    的那条提问直接开一轮(不重复 append、不重复记历史 ✓)。桥端点
    POST /agent/resume 同能力(自动化用)✓。

  • 空回合自动重试一次(优化清单收尾):模型偶发返回空内容(上游抖动、网关抽风)时,
    不再把可见警告直接甩给用户 —— 自动再试一次,仍空才给出说清原因的警告 ✓。
    有界:至多多一次调用,不会循环 ✓;与瞬态重试、超限减半重试的既有语义并存 ✓。

    • 实测(假端点 EMPTYSTREAM 每次都返回空):fixture 收到 3 次请求 = 2 次回合尝试
      (原发 + 自动重试)+ 1 次标题生成,最终以警告收尾、无死循环 ✓。
  • 只读工具并行执行(优化清单 P2):连续的 .readonly 工具(getPageText、快照、
    readFile、检索类……)从逐个 await 改为整段并发、按原顺序落结果 —— gate 仍逐个过
    (readonly 从不弹审批、取消即拒),toolCallId 配对不受执行顺序影响 ✓。
    多读类回合(检查多个元素、读多个文件)的墙钟直接省一半以上 ✓。

    • 顺带修掉一个被它暴露的轨迹 bug:步骤-结果的认领用的是"最近一个没观察的步骤"
      (位置配对)—— 串行时代恰好不出错,并行完成后结果全部张冠李戴(实测 3 个并行
      readFile 的结果互相错位)✓。现在按 toolCallId 精确认领(旧会话没有 id 的退回
      位置配对)✓;单测加了并行批的配对回归(旧算法下该用例必失败)✓。
  • 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
      单独计 ✓ —— 对账无重复、无遗漏 ✓。
  • 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 还在 = 仍在
      编辑中,不报失焦)✓。

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
      显示为未配置属预期,设置里重存一次即可 ✓。
  • 脱敏代码把应用打成了"僵尸":主 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+)。

  1. 解压 Desire-v0.3.14-macos-arm64.zip(双击,或
    ditto -x -k Desire-v0.3.14-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 到它所在目录。

v0.3.13

Choose a tag to compare

@siwilizhao siwilizhao released this 23 Sep 10:44

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 计数与
    上下文占用,于是"轻任务走轻模型"没法度量):面板状态行多一个成本 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 键、零缺口)。
  • 桥端点 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 键、零缺口)。
  • 轨迹页(可观测性落地到 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 →
      嵌套自评 → 评语作为工具结果回到模型 → 模型据此修正 ✓。
  • 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...
Read more

v0.3.12

Choose a tag to compare

@siwilizhao siwilizhao released this 22 Sep 18:56

视频下载直出 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、maxBandwidth 400k→640x360 / 5M→960x540、
      音轨分离+上限 → audio+video、直播 → 内置+转封装出 mp4(带 live 提示)、
      无后缀播放列表 / .bin 分片 / 无扩展名分片 → 均由 ffmpeg 出 mp4、
      段全 404 → 失败且不留空文件。产物用 ffprobe 核对容器、流与分辨率。

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 写进无关消息),数组变短
      后还会越界。
  • 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+)。

  1. 解压 Desire-v0.3.12-macos-arm64.zip(双击,或
    ditto -x -k Desire-v0.3.12-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 到它所在目录。

v0.3.11

Choose a tag to compare

@siwilizhao siwilizhao released this 21 Sep 22:54

调试面板大扩建 + 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: … 备注。此前这一轮必须等下载结束才能返回。
  • 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}。
  • 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 字节文件)。
  • 网络拦截接进面板:请求详情的菜单里新增"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 可编辑:点值或铅笔进编辑(回车提交)、
      单条删除、"新增键"行内新增。写入走页面 JS setItem,改完立即重新采集——
      实测桥写 regress=ok 后页面 localStorage.getItem 立刻读到。
    • 新增"扩展存储"子页签:按插件分组列出各自的 chrome.storage.local
      (就是插件代码里 browser.storage.local 看到的那份),可改值 / 新增键 /
      删除 / 清空;0.2.13 的共享桶单列为一组(有数据才显示)。写入的值能解析成
      JSON 就按 JSON 存,插件读回的是对象而不是字符串。
    • 子页签选择移到 store(跨面板重建保持,也便于自动化直接选中某一节)。
  • 桥端点: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 条三语文案。
  • 调试面板新增 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 两种口径(排查登录态最常用)。
    • 子页签、搜索、刷新、清空按钮与其余页签同一套观感。
  • 桥端点: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 就按缩进展示(接口排查的常见场景),
      并给"复制...
Read more

v0.3.10

Choose a tag to compare

@siwilizhao siwilizhao released this 21 Sep 22:59

分栏拖拽终局: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+)。

  1. 解压 Desire-v0.3.10-macos-arm64.zip(双击,或 ditto -x -k Desire-v0.3.10-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 到它所在目录。

本 tag 为补发:v0.3.10 就绪当时 GitHub Actions 免费额度用尽,从未发布。
产物由 v0.3.11 发布时从 1dc2029 本地构建。

v0.3.8

Choose a tag to compare

@siwilizhao siwilizhao released this 22 Sep 00:02

分发成熟化:应用内自更新、首启动引导、崩溃回收。

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+)。

  1. 解压 Desire-v0.3.8-macos-arm64.zip(双击,或 ditto -x -k Desire-v0.3.8-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 到它所在目录。

本 tag 为补发:发布当时 GitHub Actions 免费额度用尽,产物为后续
从对应版本戳提交在最新 Xcode 上重建的未签名 Release 构建。

v0.3.7

Choose a tag to compare

@siwilizhao siwilizhao released this 22 Sep 00:02

阅读与研究模式:阅读器可调样式 + 页面批注 v1。

Added

  • 阅读器设置:字号步进(14–26)、三级行距、四主题(跟随系统/
    浅色/羊皮纸/深色),UserDefaults 持久化。
  • 页面批注 v1:
    • 划选文本 → SelectionAIBar 尾部四色点(黄/绿/蓝/粉)→ 包裹
      <mark> 高亮(跨元素选区 extract 兜底)。
    • 按页持久化(URL 归一化哈希为桶键;锚定用文本而非 XPath,
      动态页面可靠);页面加载时自动按文本重新包裹(已包裹处跳过)。
    • Agent 工具 getPageHighlights(只读级):"总结我在这页标了什么"
      直达。桥 GET /annotations?index=N。
    • 同页同文本重复划选 = 改色不追加。

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+)。

  1. 解压 Desire-v0.3.7-macos-arm64.zip(双击,或 ditto -x -k Desire-v0.3.7-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 到它所在目录。

本 tag 为补发:发布当时 GitHub Actions 免费额度用尽,产物为后续
从对应版本戳提交在最新 Xcode 上重建的未签名 Release 构建。