 wuguanganandClaude Opus 5
|
e3b9153b66
|
feat: 拦住 AI 把密钥搬出去;README 补 AI 时代的隐私那一节
## 密钥泄漏闸
把活交给 agent 之后,泄密的形态变了。以前是人手滑,现在是**每一步都很合理的
一串操作**:git add . 顺手把 .env 提了、把真实 token 内联进配置文件当"示例"、
为了确认配置 cat 一下密钥(值就进了 transcript,0644、永久、每轮重发给 API)。
**没有一条能靠「这条命令危不危险」判断出来。** 它们都不危险。唯一可靠的信号是
这次调用的内容里有没有出现真实的密钥值。
所以加了一道闸,默认开:参数里出现库里的真实值 → 拒;git add/commit 会把
.env 一类文件或含真实值的文件带进仓库 → 拒。拒绝理由里写明正确做法
(用 $KEY、先加 .gitignore),否则模型只会换个写法再试一次。
**它跑在 hubctl 本地,不依赖 app 在不在跑。** 仓库其它拦截都是「app 没开就放行」
(一个监控工具不该让你的命令跑不起来),这一条反过来 —— 密钥漏出去不可逆。
几条守住的边界,判宽了比判窄了危险:值短于 8 字符不参与匹配(abc/true/test
会命中一切);.example/.template/.sample 结尾放行(模板文件本来就该提交,
误拦一次用户就把整个功能关掉了);只认 EnvRenderer 写出来的格式,不做通用
dotenv 解析;不是 git add/commit 就一个子进程都不起。
拒了之后事件照发给 app(不等应答),进审批日志 —— 一道看不见的闸,
用户第一次被拦时只会觉得 Claude 抽风。开关在密钥页底部,
落成 ~/.vibe-foreman/guard.json,且**只在关掉时才写**:
默认开,「文件不存在 = 开着」,这样没打开过密钥页的人也受保护。
明确写了它挡不住什么:python -c 读、软链、base64 转一道、库外手写的值。
它是一道减少手滑的闸,不是沙箱。
## 截图
主窗口现在也能自动截了:HUB_WINDOW_SECTION 钉住打开哪一页,
配 DemoFixtures.seed 造的假密钥和假账号(刻意编得一眼假 —— 一个像真的
token 混进去,读者会去试)。
两个坑:find-window 按面积取第一个会截到岛(它比主窗口先出现一瞬),
改成按宽度 ≥900 筛;.accessory 的进程从后台 shell 起来抢不到焦点,
而非活跃窗口里的系统控件会被去饱和 —— 截出来开关全是灰的,
正好把要展示的东西说反了,所以截图模式下改用 .regular。
顺带:进密钥页自动选中第一组。有组却什么都不选,右边一大片空白,
那是"还没建组"的样子而不是"有四组等你挑"的样子。
646 个测试全过(原 625)。真 hubctl 二进制端到端验过四种情况:
写入真值被拒、约定的 source 用法放行、git add . 带 .env 被拒、普通命令放行。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-18 10:46:46 +08:00 |
|
 wuguanganandClaude Opus 5
|
8510e1de28
|
feat: 读密钥文件的 Read/Grep/Glob 拦一下
`RiskClassifier` 里 readOnlyTools 是无条件早退的(注释写明理由:Read 一个讲
`rm -rf` 的 README 不该触发审批)。但「读密钥文件」恰恰是要拦的那件事——
共用密钥交给 AI 的契约是 `set -a; source`,值只进子进程环境;改用 Read
之后文件内容就进了 ~/.claude/projects/*.jsonl(0644、永久、每轮重发给 API)。
判断**只放在 readOnlyTools 那个分支里,不放在 classify 开头**。放开头的话,
约定用法里那条 source 命令每次都会弹审批,正常流程当场堵死。Bash 走下面的
模式匹配,`\.env\b` 已经把它定成 dangerous(记录不拦截),不动。
写清楚它挡不住什么:python -c 读、软链、base64 转一道都绕得过,
而且 hubctl 超时是 fail-open——app 没开着的时候这道闸是关的。
它是一次提醒,不是安全边界,README 和注释都按这个写。
路径常量抄在 HubIPC 里(那个模块刻意不依赖 HubProjects,hubctl 要小而快)。
抄就会漂,漂了的症状是静默的——改个文件名,闸就再也命不中而界面毫无变化。
所以加了 SensitivePathsAgreementTests,在同时看得见两个模块的测试目录里
把两边的真实路径对一遍。
625 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-17 12:48:30 +08:00 |
|
 wuguanganandClaude Opus 5
|
a9363e43c9
|
feat: 共用密钥中心 + 项目账号密码库
两个功能来自同一个痛点,但威胁模型正好相反,这是整个设计的支点。
**共用密钥**:平台密钥(飞书、领星、OpenAI)在十几个项目里要各配一遍,
而最省事的做法——直接粘进聊天框告诉 AI——是最糟的那个。现在按平台建组、
勾给项目,物化成 ~/.vibe-foreman/env/by-project/<项目>-<哈希>.env
(目录 0700、文件 0600),复制路径给 AI。这份数据**故意是明文、故意给 AI 用**。
**账号密码**:本地/测试/线上分开记,只给人看。AES-GCM 加密,
查看和复制过 Touch ID。这份数据**绝不能被任何自动化读到**。
两者不共用存储、不共用目录、不共用界面,并用一条哨兵测试钉死
「密码的值不会出现在给 AI 的那个目录里」。
三个决定了做法的发现:
1. 「让 AI Read 那个文件」会把密钥写进两个明文文件——
~/.claude/projects/*.jsonl(0644、永久、每轮重发给 API),
以及 Hub 自己的验收清单(Evidence.ran 会存命令和输出尾巴,同样不过期)。
所以「复制给 AI」给的不是裸路径,是带 `set -a; source` 用法和
「不要用 Read/cat/grep」禁令的三句话。换成 source 之后,
transcript 里只剩路径和变量名,值一次都不出现。
2. 按项目分文件**是分装不是隔离**——同 uid 下 cat ../别的.env 照样拿得到。
README 和代码注释都按分装写,不吹成边界。
3. Keychain 的授权弹窗**就是那道防线**。SecItemAdd 不传 kSecAttrAccess 时,
默认 ACL 只允许创建它的 app 无提示读取,而在「攻击者 = 同 uid 的自动化」
这个模型下,能区分「Hub 在读」和「security 命令在读」的只有它——
文件权限做不到(同 uid),Touch ID 也做不到(LAContext 不参与取密钥)。
为了消弹窗放宽 ACL,等于一条 Bash 就能取走主密钥。
几个不写下来一定会再踩的坑(已进 NOTES.md):
- .atomic 写换 inode,chmod 必须每次都做,只在创建时设一次从第二次写起就是 0644
- GCM 的 AAD 必须用文件里存下来的那段字节,重新序列化会让历史文件永久打不开
- 「钥匙不对」和「文件坏了」处置完全相反,靠 header 里的 keyID 分辨;
EncryptedFile.save() 的 guard state.canWrite 是这套区分唯一的落地点
- LAContext 复用才有 reuse duration,每次新建等于每条密码按一次指纹
- 剪贴板定时清空必须比对 changeCount,否则用户复制的别的东西会被莫名清掉
- .env 的引号:shell / node dotenv / python-dotenv 三家的交集只有单引号;
多行值用字面换行不能用 \n;含单引号的值没有共同解,改写成 _B64 显式降级
617 个测试全过(原 530)。真 bash `set -a; source` 验过含空格、$、反引号、
#、多行 PEM 和含单引号的值。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-17 12:45:20 +08:00 |
|
 wuguanganandClaude Opus 5
|
11786954cf
|
docs: 功能长图 + 全部截图用演示数据重截
## 长图
docs/poster.png:六段式,一张图讲完这个工具是什么。
生成脚本 Resources/make-poster.swift,改文案或调顺序改那个数组即可。
修了个坑:NSImage.size 返回的是**点**不是像素,这批截图是 2x 抓的,
1880px 的图它报 940,于是大图被当小图处理,第一版只占了半幅。
## 截图
scripts/shoot-screenshots.sh:用 HUB_DEMO=1 + HUB_ISLAND_STATE 把形态钉死,
自动截 + 自动裁掉透明留白,五种形态全自动。
之前那批是改名前截的,岛头还写着旧名。重截之后顺带修好三件事:
- 演示工作区路径 claude-hub-demo → vibe-foreman-demo
- 新图带上了「在问你」横幅和状态徽章,比旧图更能说明问题
- 删掉 04-main-window.png:验收页本来就带主窗口侧边栏,一张顶两张,
而且那张的标题栏是旧名
两个配套小工具:
- scripts/find-window.swift 拿窗口 id 给 screencapture -l 用。
CGWindowList 报的是显示名不是可执行名,写死 ClaudeHub 一个都找不到。
- scripts/crop-alpha.swift 裁到不透明内容的外接矩形。岛画在 640×720 的
透明容器窗口里,不裁的话拼进长图就是一大块空白。
截图脚本踩了 NOTES.md 里记过的那个 bash 坑:变量后面紧跟中文标点
(`$state)`)会被算进变量名,set -u 下直接报未定义。加花括号。
窗口探测改成轮询而不是固定 sleep —— 实测 4 秒对 hover 够、对 expanded 不够。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-15 00:04:25 +08:00 |
|
 wuguanganandClaude Opus 5
|
3e23d1efba
|
docs: 加上应用图标和社交预览图
- docs/icon.png 满幅方图,去掉 icns 里四周 100px 的透明留白 ——
GitHub 会自己加圆角,留白会让图标缩成小小一颗
- docs/social-preview.png 1280×640。仓库被分享到 Twitter / Slack / 飞书时
唯一会被看到的东西;没有它链接展开是一片灰底加一行小字
- README 头部放上图标
社交预览图由 Resources/make-social.swift 生成,图标直接读 AppIcon.icns
而不是重画一遍 —— 两处各画一遍必然漂移,"两个地方的图标不一样"
是最廉价也最刺眼的破绽。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 23:24:16 +08:00 |
|
 wuguanganandClaude Opus 5
|
3af92f27cf
|
chore: 仓库改名 vibe-foreman,代码认新旧两个目录名
仓库 gulugulu-cn/claude-hub → gulugulu-cn/vibe-foreman。
GitHub 自动重定向旧地址,6 个 Release 全部保留。
代码里按 ~/Documents/code/claude-hub 找脚本和 projects.yaml 的地方,
改成新旧两个目录名都试 —— 老用户的工作区还叫旧名,新 clone 的是新名,
少认一个的表现是"从界面开项目时项目配置没被补齐",而那是静默的。
**Application Support 下的 claude-hub 目录不改名。** 那是用户数据落盘的
地方(验收清单、盯梢配置、已结束会话名册、置顶、审批日志),
改了等于把现有配置全孤立掉。
530 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 23:05:03 +08:00 |
|
 wuguanganandClaude Opus 5
|
bae4f9af86
|
feat: 零配置装 hook;改名 Vibe Foreman Free
## 零配置
装法从三步变两步:拖进应用程序 → 打开。中间那步「双击安装 hook.command」
没有任何用户能做的决定,漏点它的后果却是**静默的** —— app 开着、
界面正常、一条事件都收不到,看起来像"装了但没用"。
HookInstaller 在启动时自己装。四条边界,都有测试钉着:
- 只碰自己的条目:只删命令里含 `hubctl hook` / `hub-hook-` 的(我们的旧版),
用户手写的 hook 和 permissions / env 等其它键一律原样保留
- 幂等:内容没变一个字节都不写。`.sortedKeys` 是这条的前提 ——
不排序时 JSON 键序跟着哈希走,"没变就不写"的判断会随机失效
- 坏 JSON 不覆盖:用户可能只是手改时漏了个逗号,覆盖等于删了他的配置
- 失败不阻断启动:hook 没装好的 app 仍能显示会话,起不来的 app 什么都不是
hubctl 每次比内容再决定要不要拷 —— 升级 app 后 hook 还指着旧二进制的话,
协议一变整条链路静默失效。设置页显示状态 + 一键修复。
dmg 里那个 command 文件改名成「手工修复 hook(通常用不到)」,留作兜底。
实测:本机 settings.json 装前装后逐键对比,顶层键无增无减、
permissions 等非 hooks 值全部原样、9 类 hook 各 1 条无重复;
第二次启动 sha256 未变。
## 改名
显示名 Claude Hub → Vibe Foreman Free。
**bundle id、签名证书名、可执行文件名 ClaudeHub 全部不动** ——
换任何一个都会让已授予的 TCC 权限(控制 iTerm 的 Automation、通知)失效,
而 `pgrep -x ClaudeHub` 那条判活约束也依赖可执行名(见 NOTES.md)。
安装时清掉旧名字那份,避免 /Applications 里同时躺着两个共用 bundle id 的 app。
530 个测试全过。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 22:27:28 +08:00 |
|
 wuguanganandClaude Opus 5
|
985d77eca6
|
docs: 恢复图文版落地页,安装与实现细节挪进 docs
强推源码时我把公开仓库的落地页换成了纯文字的安装手册 ——
那份图文介绍(8 张排过版的截图 + 说明文案)是特意设计的,被我弄丢了。
这是回退。
现在的分工:
- README.md 产品介绍。8 张图 + "它能做什么",给第一次看到的人。
- docs/安装与实现.md 装机、开发、故障排查、目录结构、各模块实现细节。
顺带把三个新能力按同样的语气写进落地页(验收复核、盯梢追问、
关窗即结束),而不是只堆在技术文档里 —— 它们是这个工具最不一样的地方。
旧落地页的「许可」段写的是"本仓库不含源代码,所以没套源码许可证",
源码进来之后这句不成立,换成 MIT。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 22:08:46 +08:00 |
|
 wuguanganandClaude Opus 5
|
c6dccca3d4
|
chore: 2.4.0 —— 补齐截图,署名 Artux AI
- LICENSE 版权人:Artux AI
- 补上分发仓库独有的 4 张截图(nudge / answer / stall-detail / ticker),
逐张确认过是 DemoFixtures 合成数据
- 版本 2.3.0 → 2.4.0:上游 v2.3.0 已发布过,今天这批改动(iTerm 检测、
会话回收与接回、右键菜单、失效项目清理)不能再挂同一个号
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-08-14 17:48:17 +08:00 |
|
 
|
ef76328ec8
|
滞留守望:从「事件通知」升级成「状态守望」 (#5)
* feat: 演示模式(合成会话 + 真 git 工作区)用于脱敏截图
真实数据里全是内部项目名、分支名和 commit 标题,不能对外;而且要等真实
会话凑出"1 个在等你 + 2 个在忙 + 1 个跑命令 + 几个闲着"这种展示效果好的
组合纯靠运气。合成数据同时解决不泄露和每次截图完全一致两件事。
实现上往临时目录写 sessions/<PID>.json 再让 reader 指过去,而不是在 store
里塞假数据 —— 这样走的是和生产完全一样的解析路径,截出来的图能反映真实
渲染结果。git 工作区也建真仓库(真分支、真提交、真未提交改动),理由相同。
ClaudeSessionReader 因此需要 requiresLiveProcess 开关:合成会话的进程当然
不存在,kill(pid,0) 会把它们全过滤掉。生产必须保持 true。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(hubctl): 加 HUB_JUDGE 守卫,封死 AI 判定的 hook 回环
Hub 接下来要自己起 claude -p 给滞留的会话生成摘要。那个子进程会照常
触发 Stop/SessionEnd hook,于是 hook → Hub → claude -p → hook 形成
无限回环,每判定一次多生一个会话。
守卫见到 HUB_JUDGE=1 就在发 socket 之前 exit(0)。这道防线不依赖 Claude
CLI 的任何行为开关,是唯一保证有效的。
顺带加 HUB_TRACE:指向文件路径时每次执行追加一行。这是本机唯一能把
"Hub 起的那个 claude"和"用户另外八个会话"区分开的探针 —— 环境变量会被
子进程继承,别人的会话没有它。之前试过用 hubctl 的 atime 做间接探测,
全是其他会话的噪音,毫无结论。
实测(trace 文件为证):
claude -p --setting-sources '' → 0 行,hook 根本没被调起
claude -p(无开关)+ HUB_JUDGE=1 → 2 行 stop/sessionend,均 judge=true
两道防线各自独立有效,实现时两条都上。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(probe): 滞留检测的确定性探针与五类判定
岛现在只在事件发生那一瞬间提醒(闯入 2 秒然后回落成一个静止徽章),
之后再也不喊。用户给的五张实拍截图都是"那一瞬没看到就永远错过"。
这一层负责回答"哪个会话卡住了、为什么",纯逻辑、不碰 UI。
TranscriptTail —— 只读文件尾部 64 KB。这些 jsonl 单个几十 MB、全目录实测
945 MB,UsageStats 早期整个读进内存导致用量页永远转不出来的教训不能重犯。
窗口切在行中间时残缺的首行必须丢掉,否则会解析出半截内容。
定位不猜编码规则:主路径按"非字母数字换 -"拼(30 个真实样本全中),
拼不中就按 sessionId 全局 glob 兜底 —— UUID 唯一,兜底是精确的。
TaskStateReader —— 读 ~/.claude/tasks/。对过账:gateway-config-management
读出 {completed:2, in_progress:1, pending:1},与 Claude 自己界面上的
"4 tasks (2 done, 1 in progress, 1 open)" 完全一致。
StallDetector —— 五类原因按优先级判定:
断线 > 在提问 > 等授权 > 有后续 > 待验收
断线是异常,立即报不等阈值。提问的启发式只看**最后一个非空行**是否以问号
结尾,不扫全文 —— 正文中间的反问句非常常见,扫全文必然误判。
拿真实数据验证:storefront-a-47 被正确判为"❓ 在问你 · 需要我帮你把这两个提交
精准推送到 live 吗?",正是用户截图里那一条,且全程没用 AI。
分级:只有"干了没多久就安静收工"降级成不主动闯入。干活时长必须自己观测
(Claude 显示的 "Crunched for 29m 54s" 不落盘),观测时排除 waiting ——
否则等了两小时授权的会话会被记成"干了两小时"。
hubprobe stall 子命令:滞留提醒是时间驱动的,靠 UI 验证意味着干等几分钟。
这个子命令把每个会话的判定和判据一次打出来,理由同已有的 usage 子命令。
164 个测试通过(原 131)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(probe): StallJudge —— 用订阅额度给滞留会话生成一句人话
职责刻意收窄:**不判断要不要提醒**,只把事情说清楚(摘要、是不是在提问、
回答选项、建议动作)。让模型决定要不要打扰用户是高风险的,误报一次用户
就会把整个功能关掉。收窄之后它失败的代价很低 —— 解析不出来就退回
"完成了一轮,去看看",提醒照发,只是文案糙一点。
走 claude 二进制而不是直接调 API,用的是用户已登录的订阅额度,
不需要另配 ANTHROPIC_API_KEY。不能用 --bare:它跳过 hooks 的同时也跳过
OAuth(帮助原文写明 keychain is never read),等于用不上订阅额度。
踩到的三个坑,都写进注释了:
1. **--disallowedTools 是变长参数,会吞掉后面的位置参数。**
提示词放在它后面时被当成又一个工具名吃掉,模型压根收不到指令。
表现极具迷惑性:三次调用全部"成功"(有返回、扣了钱),但模型只收到
正文,于是直接去回答正文里的问题,返回散文而不是 JSON。
提示词必须紧跟 -p,排在所有变长选项之前。
2. **正文不能走 stdin。** 待分析的文本经常是直接对着读者提问的
("需要我帮你推送到 live 吗?"),和指令混在一条消息里时模型分不清
边界,会把它当成对自己说的话去执行。改成全塞进提示词、用 <待分析>
标签把数据围死。顺带 stdin 接 /dev/null,省掉 claude -p 干等 3 秒。
3. **字数限制不能指望模型守约。** 实测它给 5 个选项(要求最多 4 个)、
14 字的 nextAction(要求 ≤10 字)。岛上位置是死的,解析时必须再截一次。
围栏也必须剥:即使明说"只输出 JSON 对象",它照样包 ```json。
实测(hubprobe stall --ai):2 次调用 0 失败,$0.024,约 9 秒/次。
storefront-a-47 拿到 4 个可直接做成岛上按钮的选项。
HUB_TRACE 回环探针全程 0 行 —— 守卫有效。
175 个测试通过(原 164)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(probe): NudgeCoordinator —— 会闭嘴的升级阶梯
0 / 3 / 8 / 20 分钟四档递增,之后每 20 分钟。递增而不是固定间隔:
固定间隔在用户"知道但暂时不想处理"时会变成必须关掉的骚扰。
第一轮静默(刚停下来就叫太急躁,而且这时用户多半就在机器前面),
第二轮起出声,第三轮起换 identifier 重发通知 —— 同 id 的通知在通知中心
里是静默替换,不换 id 用户根本看不到"又催了一次"。
**加了计划里没有的两条封顶**,是拿真实数据逼出来的:
本机有个会话已经 idle 46 小时,按"每 20 分钟一轮、不再加码"会主动弹
138 次。那不是提醒,那是必须被关掉的东西。
maxActiveRounds = 5 主动闯入最多五次
giveUpAfter = 2 小时 之后只留跑马灯和下唇染色
放弃"闯入"不等于放弃"告知"—— passive 列表照常带着它,信息一直在岛上。
giveUpAfter 顺手解决了另一个场景:app 重启时会一次性看到几个隔夜会话,
它们的 since 早就超过阈值,所以启动瞬间不会被一串闯入淹没。
多个会话合并成一次提醒,按原因优先级排序,不排队弹 N 次。
用户跳转或应答只压制一轮 —— 跳过去看一眼不等于处理完了。真正的处理完是
会话重新开工,那时它就从 findings 里消失,状态自然清掉,下次再停是新的一件事。
纯状态机,now 显式传入,10 个时序测试跑完整条阶梯不用真等 20 分钟。
185 个测试通过(原 175)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(island): 滞留提醒的岛上表现 + 应答 + 跨屏跟随
## 两个新形态
.nudge —— 卡住的会话的像素小人从刘海下沿弹出排开,逐个延迟 60ms
(一起弹是一整块在动,错开才读得出"一个一个冒出来")。每个小人上方一个
原因图标、下方截断的会话名,整格可点。刻意不复用 .intrusion:后者是
"某件事刚发生",单会话两秒划过;前者是"你有 N 件事卡着",聚合、停 5 秒、可交互。
.answer —— 显示 AI 的问题和它给的快捷回答。**点了按钮不直接发**,
先出"将发送到 iTerm · xxx:是"让用户确认一次。多这一步是因为刘海很小、
用户可能在剔背景的情况下随手点一下,而那一下可能是往生产推代码。
两态取同一个高度:不同的话点按钮时岛会跳,而那一跳恰好发生在用户
正要去点"确认发送"的时候,按钮会在指针底下移位。
折叠态:有东西卡着时点阵换成跑马灯(点阵回答的是"跑着几个",
卡住时要知道的是"哪个、为什么",这时点阵是纯噪音),整条下唇按最高优先级
原因染色。只在有 stall 时才滚,平时回到静态点阵,不引入常驻动画耗电。
## 回写终端(唯一不可逆的动作)
TerminalReply 的五条护栏:发送前重读会话状态(变 busy 说明用户已经自己
在那边动手了,取消并如实说,绝不硬发);只发一行纯文本 + 回车(换行会被
当成多敲一次回车,控制字符能触发终端快捷键);绑定只走 PID 祖先链,
绝不用窗口名匹配;定位不到就退化成跳转;每次发送留痕。
## 跨屏跟随
岛以前锁死在内建刘海屏,用户看着外接屏时提醒在物理上不在他视野里 ——
这条不解决,跑马灯和主动闯入全是白做。screenUnderCursor() 其实一开始
就写好了,只是全项目零调用。只在折叠态搬家:展开/审批/应答态搬家会把
用户正在点的东西从手底下挪走,尤其应答态按钮一移位,本来要点"取消"的
那一下可能落到"确认发送"上。
## 实机验证发现并修掉
点快捷回答 → 出预览而不是直接发(护栏生效);确认发送 → 目标会话不存在时
如实报错。但失败提示在点"取消"回到选项后仍挂着,读起来像这一次也失败了 ——
加 onClearFeedback,任何新动作先抹掉上一次的回显。
199 个测试通过(原 185),含 nudge/answer 的尺寸回归 ——
SwiftUI 的布局裁切是完全静默的失败,只能靠算术断言挡住。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: 小人多长了一只右手;滞留信息补进展开态;钉住不再卡死界面
## 挥手的小人有两只右手(用户在 48pt 详情卡上看出来的)
waiting 用挥手姿势,bodyRows 在肩膀那行画了举起来的右手,而 laptopRows
仍然把两只手都画在笔记本两侧 —— 同一个人两只右手。而 waiting 恰恰是
最需要被看清的状态,偏偏是它画错。
9 ##ssssssss##f ← 举起来挥的
13 #LLLggggLLL#f ← 同一只手又画了一遍
新增 SpriteAnatomyTests 断言每侧最多一只手。像素画的错误在 32pt 下肉眼
几乎发现不了,但在字符网格上可以精确断言。**回插 bug 验证过测试会红**
(24 条断言失败),不然写了等于没写。
## 滞留信息只活在 nudge 和跑马灯里
用户一展开列表 —— 也就是他真正要动手的地方 —— 滞留信息就消失了,
反而在最该看到的位置看不到。现在:
- 详情卡加滞留横幅(原因 + 具体内容),卡住时**顶掉**提交记录:
两者都要位置,而"这件事在等你"永远比"三天前提交了什么"更急;
是提问的话直接给「回答」按钮进应答态。
- 列表行加原因徽章,优先于笼统的 waitingFor("在问你"比 "input needed" 具体)。
- 折叠态点跑马灯**直达那件事**,不再跳到通用列表让用户自己再找一遍。
- 非提问的滞留点开是展开+选中,不直接把人甩到终端 ——
用户点这一下是想先看清楚是什么事。
## HUB_ISLAND_STATE 会把界面卡死
实机暴露:岛钉在展开态、又跟着光标跑到了外接屏,用户点收起没反应,
只能去杀进程。钉住的本意是挡掉自动迁移(悬停会把它收回去),
不该把用户主动的操作也冻掉。现在 setState 分 userInitiated,
收起和展开一律放行。
顺带:收起时强制重新对齐到光标所在屏 —— 岛跟随只在折叠态生效,
而刚收起正是最该对齐的时刻(用户很可能已经换屏了)。
203 个测试通过(原 199)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat: 换掉占位图标,并入 agentx 系列视觉
原来那个三机器人是占位图,和岛里的像素小人、刘海形态完全是两套语言。
沿用 agentx 系列的记号(青→蓝→紫→品红→橙的斜向渐变 + 四角星芒 + 外辉光,
对着 AgentX 和浏览器扩展的图标取的),但主体换成**灵动岛的胶囊形** ——
那是这个 app 唯一的门面,也是它在无刘海屏上的真实形态。
极简的判据是"16pt 下还认得出":一个胶囊 + 一颗星,没有任何细节要分辨。
实测 16pt 下胶囊清楚,但星芒只剩两三个像素、糊成一个污点反而像脏了,
所以 <32pt 不画星芒 —— 光学尺寸的常规做法,小尺寸减细节。
图标用代码生成(Resources/make-icon.swift)而不是塞一个二进制资产:
可以 diff、可以 code review、改一个参数不用重新导出,
和这个项目里像素小人 art 写在代码里是同一个理由。
swift Resources/make-icon.swift AppIcon.iconset
iconutil -c icns AppIcon.iconset -o Resources/AppIcon.icns
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(nudge): 升级阶梯按第一次提醒计时,不是按会话卡住的时刻
实机验证抓出来的。修复前的真实时间线:
17:38:10 第 0 轮
17:38:26 第 1 轮 有声 ← 隔 16 秒,阶梯说该隔 3 分钟
延迟原来从 since(会话变 idle 的时刻)算。product-library-fixes 被发现时
已经 idle 4 分钟(240 秒),于是一次就同时满足第 0 轮(≥0 秒)和
第 1 轮(≥180 秒),下一次扫描(30 秒后)立刻又喊。
关键在于这不是边角情况:**"被发现时已经卡了一会儿"才是常态** ——
app 刚启动会看到一批、刚满阈值才被判出来的也都是。等于阶梯对绝大多数
会话都是失效的,会连着把前几轮一次性喊完。
改成记 firstNudgedAt,阶梯从第一次喊开始计时。giveUpAfter 仍然按 since
算 —— 它要挡的正是"这事早就在那儿了,你显然不打算现在处理"。
原来那条阶梯测试让 since 和 now 从同一时刻起步,正好绕开了这个场景,
所以一直是绿的。新测试直接构造"发现时已卡 4 分钟",并**回插 bug 验证过
它会红**(2 条断言失败)。
顺带给 present() 加了提醒日志。滞留提醒是时间驱动的,没有日志根本没法
验证"到底喊没喊、第几轮、为什么"—— 这次的 bug 就是靠它才看出来的。
实机同时确认:多会话合并成一次弹出;idle 了 446 分钟的会话一次都没喊
(闭嘴上限有效);折叠态跑马灯在真实数据上正常滚动。
204 个测试通过(原 203)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(nudge): 封住"反复变原因"绕过两道闭嘴上限的洞
"原因变了就重新计时"本身是对的("断线了"和"待验收"是两件事,
不该沿用旧轮次),但它同时开了两个洞:
1. 每翻一次从第 0 轮重来,于是既不升级、也永远撞不到 maxActiveRounds;
2. since 跟着被推到现在,giveUpAfter 那条"卡了两小时"永远够不着。
结果是一个在 idle / waiting 之间来回翻的会话可以无限期地每隔几分钟
弹一次同一个项目 —— 而这正是用户最容易直接关掉整个功能的那种行为。
两处修正:
maxLadderRestarts = 2 重置本身封顶,超过之后原因照常更新但轮次不再
归零,阶梯继续往上走直到闭嘴
firstSeenAt 记"连续卡着从何时开始",原因变了也不重置;
giveUpAfter 改按它算
会话真的恢复(从 findings 里消失)时整条记录被丢弃,重置额度也跟着清零 ——
否则一个用了很久的会话会因为历史上的几次翻转而再也不能重新计时。
测试断言的是**最终会安静**这个性质,不是某个具体次数:90 分钟里每 5 分钟
翻一次原因,断言最后半小时完全无声、且进入只做被动提示的状态。
回插两个洞验证过会红 —— 无防护时 18 次翻转全部触发,且换原因能绕过 giveUpAfter。
208 个测试通过(原 204)。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: 版本升到 2.1.0
滞留守望、岛上应答、跨屏跟随、新图标 —— 都是新能力,走 minor。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* chore: 截图模式不启动守望;钉住日志打实际形态
守望 30 秒后会用自己对演示会话的判定覆盖预置的那组,而演示会话没有真实
transcript 和任务清单,判出来清一色待验收—— 五类原因的区分恰恰是要展示的。
钉住日志原来只打环境变量的值,钉住失效时照样显示已钉住,把排查带偏过一次。
改成同时打实际形态和滞留条数。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: wuguangan <anjaihd0206@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
2026-07-31 19:43:01 +08:00 |
|