Repository navigation
flashCat 1.5.4:新增 flashcat 命令行与本地控制接口,概览指标改为按请求实测
flashCat 1.5.4
这一版把 CLI 与本地控制接口补上:flashcat 能列出/启停引擎、切模型、流式对话、
读日志与网关状态,应用没在跑时也能做离线操作。概览的取数口径同步重做:预填充
与解码优先读引擎按请求自报的 timings,量不出来就画「—」;第四格从内存压力
换成按 token 加权的缓存命中率。
前缀缓存改为落盘,重启应用不用从零预填充;切走对话不再中断生成,回到列表能点
回正在跑的那一段;应用内不再注册任何快捷键,键盘效率交给终端。
新增
命令行与本机控制接口
flashcat命令行。 随.app分发在Contents/Helpers/flashcat,.pkg
安装时链接到/usr/local/bin。支持status、engines(list/start/stop/
restart/use/install/uninstall/port/logs)、models(list/use/download/delete/
path)、chat(stdin、流式、思考过程、JSON)、gateway、logs(级别过滤与
--follow)、config、app、doctor、completion、cli。- 应用侧本机控制接口。 默认
127.0.0.1:9100,端口被占用时自动顺延;每次
启动生成随机令牌,control.json以0600保存。设置页新增「命令行接口」开关
与端口。接口与推理网关分离,网关关闭时仍可驱动应用。 - 共享路径与协议。
FlashCatPaths、EngineInstallations与
FlashCatKit/Control/ControlProtocol.swift收进 Kit,应用和 CLI 使用同一套
支持目录、模型目录、引擎发现规则与控制文档。 - 命令行脚本契约。 结果型命令都支持
--json;结果走 stdout、进度走 stderr;
固定退出码(应用未运行3、找不到4、超时5、冲突6、令牌7);
zsh / bash / fish 补全;--timeout、--quiet、--no-color。 - 离线能力。 应用未运行时仍可
status、列出引擎与磁盘模型、安装/卸载引擎、
读日志与读取/修改纯配置;需要应用的动作明确报错,不伪造成功。
前缀缓存落盘
- 重启后不再从零预填充。 引擎的 hot prefix cache 只在内存里,App 一重启就
清零,上一次会话的整段历史只能重算——同一段 1.5 万 token 的会话,带缓存
恢复是 0.1 秒,重启后无缓存是 13.8 秒,十万 token 级能到 3 分钟。四家引擎
本来都有这条 SSD 层,只是没人打开:Splash 的--persistent-cache、oMLX 的
--paged-ssd-cache-dir(不传这个目录它的前缀缓存等于全关)、MLX-Serve 的
--prefix-cache-disk默认都是关的,MTPLX 默认开着但恢复被自己的
boundary 预算挡掉。现在引擎配置页有「前缀缓存」卡片:开关 + 磁盘预算
(默认 20 GB),关闭后显式下发各家的 off 参数而不是留空。 - 统一缓存根。 各家的落盘快照都收到
Application Support/flashCat/prefix-cache/<engine>/下,容量、清理和备份
只需要管一个目录。MLX-Serve 的路径写死在$HOME/.mlx-serve/kv-cache、没有
对应开关,用符号链接指进来。 - 模型搬家不再作废缓存。 缓存条目按模型的绝对路径做键,所以
ModelStorageMigration把权重从~/.mtplx/models搬进应用目录时,该模型
已有的快照全部失配——实测 23 个条目、约 29 GB 从此再也命中不了,
同时同一个模型在新路径下有 0 命中。迁移现在改写 manifest 里的model_path
而不是放任它变孤儿,并把~/.mtplx/session-bank一并搬到新根下。 - 测试:
PrefixCacheTuningTests钉住每家的参数拼写(Splash 只认20G,
其余认20GB,写错直接被拒绝)与「0 是关闭、不是回退默认」。
变更
- 标题栏去掉「命令」按钮。 胶囊形入口不再占用标题栏宽度;命令面板仍可从
「页面 → 命令面板」菜单打开,行为与之前一致。 - 每个指标格只有一个 owner。 取数口径收进
AppState的 owner 表
(decodeReading/averageDecodeReading/prefillReading,输出
MetricReading/MetricOrigin),曲线、「最近请求」、灵动岛、菜单栏从
同一组函数取数,数值与角标同源。分工按物理边界:当前速度 owner = 网关
(引擎只给聚合口径,五家还各不相同);预填充、TTFT 分布、显存、草稿接受
率、队列、上下文占用、生命周期阶段 owner = 引擎(网关量不到)。引擎
.live只在网关没有会话在跑(客户端直连引擎端口)时兜底;网关读数超过
两拍未刷新同样降级——「过去的现在」不配冒充当前速度。 - 概览第四格从「内存压力」改为「缓存命中率」。 命中量 / 提示词总量由网关
按请求打点累加,按 token 加权而不是逐请求平均(一段 100K 上下文命中 95%
比十次 1K 冷请求更能代表缓存效率);只统计真的收到 usage 的请求,窗口是
会话表最近 ≤32 条,换引擎 / 换模型 / 引擎重启后从「—」重新等样本。客户端
绕过网关直连引擎端口时,退回引擎自报的开机以来比值(mlx-serve / MTPLX 的
cached / prompt计数器,oMLX 的total_cached_tokens / total_prompt_tokens)。
角标只有AVG;0% 是合法读数(有样本、一次都没命中),「没样本」才画「—」。
内存占用仍在菜单栏的读数里。 - hero 只显示模型启动以来平均。 大数字改读网关按会话累计的解码均速:
分子是生成 token,分母是首 token 到末 token 的解码时长(不含 TTFT、排队、
空闲),换引擎 / 换模型 / 引擎重启时清零。曲线继续画实时值,两者回答的
不再混为一谈;没有累计样本时画「—」。 - 取消 LAST 采集入口。 解码与预填充的当前速度只认 LIVE;预填充保留
引擎真的累计过的 AVG(文案写明「开机以来均值」),空闲时画「—」,不再把
上一请求的实测值挂成当前读数。角标只剩「网关」/「引擎实时」与AVG。 - 应用内对话的直播条继续走低通。
DecodeRateMeter(τ=2.5s)服务逐分
片回调的对话页——那边要的是「最后一个脉冲的强度」;概览只在 1 秒节拍上
读网关,分片不再顺带刷新概览。 - 模型广场不再展示推荐引擎标签。 结果卡与详情页去掉那枚强调色的引擎名
(含「无推荐」);量化、已在本机、内存下限、许可与 Hub 标签照旧保留,权重
归属仍由下载条里的「默认授权给 …」交代。 - 对话输入框回车发送、⌘回车换行。 SwiftUI 的
TextField(axis: .vertical)
只会回车换行、没有换行组合键可留,输入框换成NSTextView包装
(UI/ChatComposerField.swift):回车发送、⌘回车换行,输入法组字期间不误发;
仍按内容长高、封顶 6 行后滚动,聚焦上抬一截的动效与点击聚焦不变。这是应用里
唯一的按键判定——属于文本编辑而不是全局快捷键;testReturnKeyIsOnlyHandledByComposer
把keyDown覆盖钉在这一个文件里,UI-SPEC「键盘策略」与发送按钮提示同步写明。 - 应用内取消所有快捷键。 菜单不再带按键等价物;命令面板只响应鼠标点击,
不再接管方向键、回车或 esc;对话输入框回车换行、发送改为点击按钮;搜索框
不再回车提交;设置页端口改为点击「应用」;弹窗去掉.defaultAction。
设置页与 README 的快捷键说明同步删除。 - 设置页新增「命令行接口」卡片。 显示开关、控制端口与
flashcat --help
的复制入口;「恢复默认」会把控制接口恢复为开启、端口 9100。 - 发版产物。
scripts/build-native-app.sh同时编译并嵌入flashcat;
make-installer.sh的.pkgpostinstall 创建/usr/local/bin/flashcat;
install-app.sh在/usr/local/bin可写时同样链接;make uninstall会清掉
指向已安装应用的链接。
修复
- 切走对话会把正在生成的回答弄丢。 以前切去概览、新建对话或从列表点开
另一段,都会直接cancelGeneration(),而它只写conversation.json——
「最近对话」里留着的还是上一轮快照,正在流的那条回答既没了、生成也停了。
现在切走时把整段对话连同它自己的threadID搬到后台继续跑,跑完按自己的
id 落回历史并提示「已在后台生成完成」;开始生成时先把问题写进最近对话,
列表里立刻有一行可点,点回来就是把后台那份接回前台接着写。侧栏给正在生成
的那一行标「生成中」;删除或清空对话会先停掉后台那一轮,别让它跑完再写回来。 - 切页回来对话区是空的。
ChatPage重建时ScrollView报 0 高度,
scrollTo按 0 视口算偏移,等高度补齐就落到内容之外。现在先挂一笔「欠一次
落底」,由onScrollGeometryChange量到真实高度再补;超大回答首次布局只量到
一部分高度,150 ms 后再补一次,读者已经往上翻时不抢回位置。 - 设置页改「默认模型」不再换掉正在跑的引擎。 这一栏的菜单按引擎分组,
选别的引擎的模型以前会直接走一次引擎切换:停掉当前引擎、等待进程退出、
再加载新权重。现在它只是启动偏好:选择记到那台引擎的配置里,当前引擎
继续服务,行内标注「下次启动」;只有显式切换引擎(或下次启动)才会让默认
跟着动。引擎存储里的default与active因此分开,后台的安装/卸载重建
也不会再把activeID挪到默认引擎上。 - 下载中心的删除按钮发糊。 危险色原来交给玻璃
tint
(Native.red.opacity(0.55)):tint 是层半透明色雾,红色图标压在同一片红雾
上,截图取样底色rgb(185,102,93)、图标rgb(244,107,94),对比度只有约
1.4∶1——看上去像失焦。现在危险态不再染玻璃,改成中性玻璃 + 自绘的 12% 红底
与 0.8 pt 发丝边,图标用满饱和的Console.danger;边缘由我们画,不经过玻璃
模糊,深浅色下都清晰。 - 「最近请求」报出 37 769 tok/s 这种数。 非流式响应(agent 客户端、
stream: false的调用)整段正文一口气回来,网关手里只有一个到达点;
「全程平均」却拿到达窗口的下限 0.35 秒当分母,14 155 token 的回答于是被摊成
40 443 tok/s,同一批数字还混进了 hero 的「模型启动以来平均」。现在按请求先读
引擎在响应里自报的timings(prompt_n/cached_n/prompt_ms与
predicted_n/predicted_ms,mlx-serve 在流式末片和非流式正文里都带),
分子分母都只取引擎真正前向/生成的那一段;没有timings的引擎只要没观测到
真实的到达跨度就画「—」,不再拿窗口下限编一个。单点会话同样不再进累计均值。 - 预填充速度始终为空(换到 mlx-serve 后又出现)。 根因是这格只认引擎自报的
预填充字段:MTPLX 在in_flight[].prefill_state/latest.prefill_tok_s里给,
但几百 token 的预填充只要 0.2~0.4 秒,1 Hz 轮询几乎抓不到 LIVE;mlx-serve
干脆不报预填充速率,于是卡片又整格变空。现在同样以逐请求的实测值为准:
引擎给了timings就用(prompt_n − cached_n) / prompt_ms(非流式也有),
没给才退回网关自己的口径——分子是 usage 里的提示词 token 减去
cached_tokens,分母是请求发出到首 token 的墙钟,且只对流式响应成立。
前向不足 128 token 的样本一概不报:命中缓存后剩几十个 token 时,耗时由缓存
恢复的固定开销主导(实测 2195 个 token 里缓存 2164,31 个前向摊 92ms → 334
tok/s),那个数字既不是算力也不是缓存速度。预填充进行中引擎的.live仍然
优先,其余时间标LAST(「网关 · 最近一次预填充实测」),引擎的 LAST/AVG
只在客户端绕过网关直连引擎端口时兜底。AI 服务自己也会报进程级均值,
prometheusJSON现在把generation_tokens_total / decode_time_seconds.sum
与prefill_tokens_total / prefill_time_seconds.sum读成.lifetimeMean,
网关一个样本都没有时 hero 与预填充格也能说出「开机以来多快」。 - Claude 的流式请求只回空正文。 Anthropic 方言翻译出来的上游请求体没有带
stream: true(只有调用方拿到的stream标志),引擎于是按非流式回一整块
JSON,而转发层是按data:行解析 SSE 的——一行都认不出来。Claude Code 这类
客户端只会收到message_start/ping/message_stop,正文全空,概览里
这一路连一个 token 都没有。现在翻译层把stream写进上游请求体,流式时再带
stream_options.include_usage,末片的 usage 才拿得到 completion_tokens 与
缓存命中量。 - 组字中的输入被自动清掉。 输入框原来拿
textView.string直接跟草稿
Binding 比:输入法组字的 marked text 已经在文本视图里、却还没进 Binding,
任何一次视图刷新(每秒指标、焦点变化)一比就把没确认的拼音回写成空串。现在
只在「视图没有未回报的编辑」且「草稿确实被外部改写」时才回写,组字期间一律
不碰,等提交后由textDidChange对齐;也不再由草稿状态反向抢第一响应者——
组字中途被夺焦同样会丢候选。同一份源码跑了对照组:旧逻辑属性一更新就是
marked=false 视图="",新逻辑保持marked=true 视图="zhong"直到提交。 - 背景的透明度与模糊「拖一个条,另一个条的改动才生效」。 窗口表面
(isOpaque/ 底色)和毛玻璃层的强度都只在DesktopBackdrop.updateNSView
里更新,而这个 representable 的入参只有blur:只拖 APP 透明度时它一次
都不会被回调,窗口仍是不透明的,桌面根本不露出来;等到下次拖模糊,才连带
把上一步的透明度一起落下去。现在改由BackgroundSettings的didSet直接
驱动BackgroundSurface,参数一变窗口和毛玻璃层立刻跟上,不再依赖任何视图
的更新时机;透明度为 0、又没有背景图时把模糊滑杆禁用并说明原因,不再留一根
按了没反应的滑杆。 - 图片模式的透明度后半程没有变化。 保底渐变原来写死
max(1 - opacity, floor),深色下超过 58% 之后整段都停在 0.42,滑杆最后
近一半行程是空的;现在把剩余行程线性铺到保底值上,两端数值不变
(BackgroundMath.veilOpacity)。 - 概览解码速度闪烁。 根因不在引擎上报,在我们自己的打点:每个 SSE
分片喂一个脉冲进连续时间低通,逐次读取。低通的输出随取样时刻连续
衰减(τ=2.5s 下 0.5s 静默就掉 18%),秒拍与预填充期的 0.4s 快拍各读
各的,衰减的谷值紧跟着下一个分片的加热峰值——谷—峰交替正是「忽高忽低」
的本体;分片成批到达(SSE 抖动、MTP/DFlash2 一次验证多个 draft token、
下游消费慢时 TCP 背压把上游憋成脉冲串)时更严重。新口径是墙钟窗口:
DecodeWindowMeter.rate(at:)= 窗[t−W, t]的量 ÷ 已观测跨度,分子
分母一起滑,稳态恒等于吞吐,与分片大小、到达分布、轮询相位全部无关;
边缘压着的分片按「铺在自己的到达时刻上」部分计。窗口右缘钳到最后一个
事件——秒拍与快拍共用同一个读数也不会错开,早读几十毫秒不会把窗口
掏空成 0。 - 收尾后曲线冻结,不再挂 LAST。
GatewayMeter.overview只把没收尾的
会话算作isLive;finish一落下,当前速度归零、60 秒曲线的右缘冻结在
那一刻(throughputFrozenAt),不再随挂钟把终点挤出窗口。上一请求的终值
不再冒充当前速度,也不进曲线;新请求开始后自动解冻。 - 总解码曲线按 1 秒一拍。 概览不再被 SSE 分片或预填充 chunk 推着
「立即变化」:引擎轮询统一 1 秒,曲线每拍最多落一个点(≥0.9s 的抖动
余量),静默段不拿 0 填谷。请求收尾停住整张图;横轴上 1~2 个样本的
短请求改画点标记,不再被当成空图。 - 首字时刻不再漏记 role-only 分片。
addTokens的记账顺序修正:旧
实现先guard count > 0再记firstTokenAt,空 delta(role-only 首帧、
finish_reason 帧)整帧忽略——TTFT 被记晚、预填充读数随之偏低。现在
「流开始回东西」的第一帧就记下(0 token 只记时、不计生成量)。 - 灵动岛与概览同口径。 胶囊以前引擎 LIVE 计数优先、再落应用内这一路
的本地测速,与概览 hero 的取数顺序相反,两处数字天然对不上;现在统一
走 owner 表,只认 LIVE(网关或引擎实时),绝不回落 LAST/AVG。
命令行与控制接口
- 退出改为同步收尾。
applicationShouldTerminate不再返回.terminateLater后靠
一个Task去回信:当前 macOS 会在任务被调度前销毁进程,导致引擎仍驻留、最后
一次历史写盘丢失。现在在委托里同步走完网关/引擎/下载收尾,最坏等待仍被 1.5 秒
的引擎宽限期封顶;flashcat app quit因此能真正等到进程消失并清掉control.json。 - 概览保留最后一次请求。
GatewayMeter不再在结束 6 秒后删除会话:
「最近请求」保留完成 / 失败行(最多显示 6 条,底层保留 32 条),直到被后续
请求挤出列表。已结束行的耗时固定在收尾时刻,不会跟着挂钟继续涨;它的全程
均值只留在行内与累计平均里,当前速度与曲线在收尾时归零 / 冻结,不再挂
「网关 LAST」。 - 命令行不再误删版本、也不再误判终端。
engines uninstall omlx@0.7.2在只装了
0.7.3 时报错,而不是确认后删掉 0.7.3;chat -与--yes确认改看 stdin 是否
终端,echo … | flashcat chat -在终端里也能用;重复--for累加而不是只留
最后一个;表格按 CJK 显示宽度对齐;--control-url只接受回环地址或 HTTPS,
不把控制令牌明文发给网络;logs --app --level与服务端日志过滤口径一致;
control.json从创建起就是0600,不再有"先 0644 再 chmod"的窗口。 - 控制接口启停竞态。 关闭开关时,正在绑定端口的启动会被作废并关掉新监听,
不会在设置关闭后留下一个还能驱动应用的接口。 flashcat engines start报「未能就绪:等待超时」,引擎其实已经起来。
startService()把启动丢进一个Task,控制器要到下一次 actor 跳转才进
.starting;等待方第一眼看到的还是.stopped,旧实现直接判失败。现在
.stopped在 5 秒宽限期内不算失败,见过.starting之后再停才是真的停了。
MTPLX 的近前缀恢复
- GDN boundary 撑爆条目。 MTPLX 的 session bank 预算有 1 GiB 下限,而一条
19K token 的条目光 GDN boundary 记录就要 700 MB 以上(每条 90 MB、默认最多
8 条),put()把这份开销计入条目大小后整条拒绝,bank 里只剩不含
boundary 的条目;recurrent_boundary_at_or_below()对这类条目返回 None,
近前缀恢复每次都被判boundary_not_better淘汰,最终报成ssd_prefix_miss
——把 admission 的真因伪装成「SSD 没命中」。启动时下发
MTPLX_SESSION_BANK_SHED_BOUNDARIES=1,让条目「能留几条 boundary 就留几条」
地进来,而不是整条被丢;boundary 本身是可重建的加速件,代价只是少几个恢复
检查点。已用/health的shed_gdn_boundaries_to_fit字段确认策略生效。
性能
- 「最近请求」的行改为按请求 id 稳定身份(以前按枚举下标,新请求进表后
整列行重排、每秒重绘出跳动)。
测试
- 新增
FlashCatCLICoreTests:参数解析、别名、SSE 解码、控制路由、运行时文件
权限(含旧文件收紧到0600)、控制地址校验、版本选择、CJK 表格宽度、配置/医生
文档编码、输出格式。 LocalizationTests用源码扫描守住「应用内不得再出现快捷键/按键监听」,并覆盖
新增的 CLI 设置文案。- 新增
CacheHitTests:缓存命中率的 owner 顺序(网关样本优先、0% 与「没有样本」
分开)与按 token 加权的算法;新增EnginePreferenceTests:写默认模型只记偏好,
不动activeID。
工程
本版全部单元测试通过(swift test,含新增的 CacheHitTests、EnginePreferenceTests
与 FlashCatCLICoreTests)。