Repository navigation
feat(sidebar): 主侧栏「设置」新增可折叠分类子项 - #252
Open
sunny-00001 wants to merge 99 commits into
Open
sunny-00001 wants to merge 99 commits into
sunny-00001 wants to merge 99 commits into
Conversation
把设置页的 10 个分类做成主侧栏「设置」底下的快捷入口,在任意页面都能
一步跳到对应设置分类;设置页内部那份分类导航保留(窄屏时它转横排,仍是
主入口),两侧选中态双向同步。
- 新增 ui/sidebar-settings-nav.js(宿主侧经典脚本,零依赖):运行期构建
子项 DOM 与折叠箭头,点子项依次写偏好 → wbApp.showPage('settings') →
wbSettingsPanel.showCategory(id)(顺序不可改,设置页 load() 头一行是同步的
restoreCategory(),会读同一个 localStorage 键),并 by MutationObserver
盯 .settings-nav-item.active 反向上高亮
- 新增 ui/css/sidebar-subnav.css:.nav-sub / .nav-sub-item / .nav-caret
- ui/index.html:仅插入一条 <link> 与一条 <script>,DOM 全部运行期构建
子项类名刻意用 .nav-sub-item 而非 .nav-item:app.js 的 showPage 会遍历所有
.nav-item 按 data-page 重置 active,复用会清掉分类选中态(上游
page-settings.css 注释已注明同一坑)。折叠箭头用 <span role="button"> 并
stopPropagation —— nav-item 自身是 <button>,button 不可嵌套,且不拦截会顺带
触发 #nav 的点击委托切页。
验证:在真实 Tauri 应用(WebView2 + CDP)内跑通 8 项交互(渲染/图标/展开
折叠/偏好写入/首次自动展开/跨页跳转切分类/双向同步/深链同步)。
- 账号页工具条新增「作用域」切换器:全局(账号表)或某一个模型 - 切到某模型时,列表区整块换成该模型的调度队列(只读): 调度顺序(位次 + 原因)+ 被跳过者(原因)+ 每家的倍率 - 排序一行不自己写:行序直接取 GET /api/routing/queue?model=, 与转发选路共用后端 routing::plan_route,界面与转发不可能分叉 - 倍率((提供商,模型) 级属性)放分组带而不做成一列:同模型作用域下 每行同值,做列是 N 行重复;顺带保住列集合不变,不动 wbColSettings 存盘结构 - 账号表渲染路径一个字未动,切回全局原样回来 - 后端零改动:队列端点与 getRoutingQueue / getModelManage 两个桥上游早已备好 - 新增 ui-islands/src/islands/accounts-schedule.tsx;accounts-page.tsx 仅 6 处挂载点
- 模型上百条(实测 77 个)纯下拉太长:按承载家分组(模型多的家在前, Trae 一家 22 个),浮层顶部加搜索框 - 过滤按选项 label 走,而 label 是「模型名 · 承载家」→ 模型名与厂商名 都能当关键词;过滤后空组连同组标题一起隐藏 - 用 @base-ui/react/combobox 实现:ui-islands 的直接依赖,零新依赖、 不改 ui-kit;视觉口径照上游 ui-kit/src/multi-select.tsx 改写成单选版 - 「全局」项钉在最上面,且不画组标题(只有一项,画标题反而像小标题) - 同名多承载家的模型仍只出现一次(同 value 出现两次会让选中态认不出来), 只归第一家名下,label 里把全部承载家列出来 - 踩坑:Root 的 items 必须是「分组数组」(元素带 items 数组属性),传 摊平数组会让过滤与渲染各读一份数据 —— 症状是搜索框里明明有值、空态 文案也弹出来了,但列表一项都没被滤掉
- 左栏 = 全局(账号表)+ 按厂商分组的模型叶子,厂商可折叠:默认只露 1+9 行 (各家收起,当前作用域所在的那家自动展开),顶上留搜索框(模型名与厂商名都能命中) - 窄窗口自动收成 44px 图标条:阈值由 ResizeObserver 量「面板实宽」判定 (= 表格最小宽 + 230),不用媒体查询 —— 后者看不见「应用侧栏收起后面板变宽 148px」; 展开态是 218px 的浮层,不把表格挤走(实测悬停展开前后列表区宽度恒定) - 图标条上每枚 chip 点一下 = 展开那一家,不是死图标;当前作用域所在的那家亮起 - 分组行只折叠 / 展开,不筛账号 —— 「只看某一家」是工具条那个提供商下拉的事,两者不合并 - 复用模型管理页左栏的 .rail-scroll / .rail-label / .rail-empty(账号页的账号设置弹窗 左栏早就在复用同一套);容器自己写(.prov-rail 带 overflow:hidden,会裁掉窄态浮层) - 上一版的 Combobox 分组 + 搜索没白做:scopeModel() 的分组 / 去重 / 排序口径原样复用 踩坑(已写进台账 §8.1.1 与文件注释): 1. 上游 .page[data-page="accounts"] .panel(权重 3)压掉 .panel.account-table(权重 2) 的 display:grid —— 症状是左栏横铺满面板,而 getComputedStyle().gridTemplateColumns 照样报得出 218px(那是指定值,不是使用值) 2. 窄态展开条件必须用 :has(:focus-visible)::focus-within 会因「点过的按钮留焦点」 导致浮层再不收起 3. 测悬停时 CDP 单步 mouseMoved 会让 :hover 延迟一拍,读数自相矛盾(测试侧假象) 验证:真实应用(真实数据,77 模型 / 9 家)—— 宽态两栏 218px、账号列 613→395 且不横滚; 树默认 10 行、展开小浣熊 22 行;搜索 trae→23 行 / glm 跨厂商 / zzz→空态;点模型切调度视图 且左栏选中态唯一;窄态 44px + 悬停浮层;页面零错误
用户实测反馈两条:「从全局表切换到单个模型,这个工具栏和列标题都变了」、
「列标题不能排序」。根因是上一版把列表区整块换成独立调度视图(SchedulePane),
配套收走工具条四个筛选组 / 批量栏 / 页脚,表头也换成自制的四列 —— 同一张表
被做成了两张,列宽把手、列设置、分页、勾选在模型域下全部失效。
改法:引入 TableSection 抽象,两种作用域产出同一种分段形状(全局 = 厂商分组;
模型域 = 队列按连续段切,不按 provider 归堆),于是行渲染 / 分组带 / 折叠 /
⋯菜单 / 勾选 / 分页 / 页脚只有一处实现。切作用域只换「行集合 + 行序 + 空态文案」。
- 「被跳过者 + 原因」融进现有列(按用户拍板):状态列在模型域画调度状态徽章、
原因进徽章 title、整行 .skipped 置灰;列集合一个字不改,wbColSettings 存盘
结构不必动
- 表头排序(按用户拍板做成可选项):三态循环(升 → 降 → 取消,取消是一等动作,
因为默认次序有含义);组内 / 整表两档都摆出来、当前置亮;缺失值恒在各段末;
排序生效即锁拖拽(行序已不是队列序,拖出来的先后写回去就是错的)
- 比较器唯一实现在 accounts-sort.ts(「一列取什么值」只此一份),字符串统一
localeCompare(…, 'zh-Hans-CN', { numeric: true });状态列按 stateRank 排,
与徽章共用同一份 STATE_META 次序
- 倍率仍走分组带 extra((provider, model) 级属性,做成一列是 N 行重复);
整表档分组散开时行内挂厂商标记顶上
E2E 抓到的两个真 bug:
1. 排序白排 —— shown 排好了,渲染却按 section.rows(默认次序)取行,升序与降序
画出来一模一样。改成分页只决定「留哪些行」,次序一律取 pageRows 位次。
2. 删 {scheduleModel ? null : ( 时漏删配对的 )},tsc 报 TS1381。成对符号的删除
改完必须跑 tsc。
验证:真实 WebView2(CDP 9222)跑 .workbuddy/cdp-verify-sort.cjs,41 条断言全过 ——
切作用域前后「表结构快照」逐字一致(工具条 4 组 / 表头 10 列 / 8 个可排序表头 /
批量栏 / 页脚);模型域 glm-5.3-flash 11 行(10 可用 + 1 被 limiter 跳过);
升 / 降行序确实变、第三次点击复原、整表档 7 段 → 1 段 + 11 枚厂商标记、
有效期段内非降、缺失值升序降序都垫底;回全局无回归。tsc --noEmit + vite build 双过。
用户诉求:列标题缺少最近到期时间和最近到期积分字段。
拍板:加两列;「最近到期积分」取最先作废那一份的剩余量(非面额、非合计)。
数据源复用「余额」格已有的 credits[] 契约,不新增后端字段:
- accounts-credit-detail.tsx 新增 nearestCreditOf()(唯一取值实现):
扫一遍,跳过 daily / expireAt 缺失 / 已过期,取第一个命中的份。
- CreditRow 带出 expireAt/remain/unit/daily 原始值,避免从显示串反解(第三份解析)。
两家后端已保证按 expireAt 升序(Trae 缺失 i64::MAX 垫底、
WorkBuddy 先套餐优先级后 expireAt 升序、缺失 f64::INFINITY 垫底),
故前端直接取首个命中,不再排序。
设计取舍:
- 「最近到期积分」的 0 要画(「马上作废 0」比「—」准确),单位跟着那一份走。
- creditExpiry 可排序(「谁先作废」是核心问题);creditLeft 刻意不排(跨天比大小无语义)。
修掉一个真 bug:ColGroup 是静态字面量逐列写,加列忘了补
{col('creditExpiry')} / {col('creditLeft')} —— 表头有、colgroup 没有,
colspan 与列宽把手全部错位。
验证:.workbuddy/cdp-verify-credit-cols.cjs 24 条断言全过(真实 WebView2),
含列集合顺序、colgroup 与列宽、两列可排序性正反、倒计时形态与悬停说明、
按「最近到期」排序段内非降且缺失值垫底、取消排序复原、列设置认这两列。
typecheck + build 双过;9 个文件行尾均 LF。
用户回调:上一次刻意不给这一列排序按钮(理由「跨天比大小无语义」)是错的。
拍板:按积分多少排 —— 纯数值,三态循环,缺失值恒排段末。
改动:
- accounts-sort.ts:SORTABLE_COLUMNS 加入 creditLeft;
新增 creditLeftOf() 取 nearestCreditOf().remain(与显示同一个字段);
valueOf 加 case 'creditLeft'。注释按新决策重写,保留语义边界提示。
- accounts-panels.tsx:该表头格从裸 <span class='th-label'> 换成
sortable('creditLeft', ...) + aria-sort + grip。
- 重建 ui/islands/ui.js。
两条与显示同源的红线:
- 两列取值都走同一个 nearestCreditOf() 产物,显示与排序读同一字段;
- remain 缺失(无限量那类)给 null,恒排段末,与那一列的「—」一致。
验证:cdp-verify-credit-cols.cjs 26 条断言全过(真实 WebView2)。
新增 §5b:按积分列排序后段内非降(实测 [0, 62.41, 150])、
缺失值恒排段末、且「积分列有值 ⟺ 时间列有值」证明两列同源。
typecheck + build 双过;行尾均 LF。
用户提议两条:① 每日重置的显示「离当天结束时间」;② 没有即将到期的按有效期算。 第 1 条采纳、第 2 条否决(用户拍板:空值仍显示「—」)。 第 1 条不需要前端算任何时间 —— 后端早就把「今天 24 点」发过来了。 core/billing/usage.rs 按 isDaily 故意换过到期字段: daily 包取 CycleEndTime(今天 24 点回满),其余取 DeductionEndTime(真正作废)。 前端 nearestCreditOf() 原来那句 `if (row.daily || ...) continue` 是白扔了这份数据。 改为一轮筛选拆两轮: ① 非 daily 且未过期里最早的一份(主答案,语义不变) ② ① 没有时退到 daily 且未过点的一份(读数 = 今日剩余) 同一份 expireAt、两套说法:NearestCredit 加一位 daily,界面据此换措辞 —— 一次性包说「最近到期 … 到期时刻 T」,日额度包说「今天剩余 … T 回满(不是作废)」。 照「到期」写会被读成「免费额度明天就没了」,真相是用完回满。表头 title 一并更新。 第 2 条否决理由:有效期列就在左边一格(画两遍无新信息);两列语义不同 (凭据何时重登 vs 钱何时作废),兜底会让花光的账号显示「最近到期 30 天后」 而明细里全是已过期,列名与内容打架,且连带「最近到期积分」显示不存在的份。 保留「—」是真实信息(这家没有要作废的额度),不是缺数据。 另一事实:daily:true 只有 WorkBuddy 会发(is_daily_credit 只认 FREE 免费版每日额度), Trae 侧完全不发;实测本机 daily 份数为 0 —— 这是修逻辑盲区,不是修显示缺失。 验证:活数据触发不到兜底,另写 cdp-verify-daily-fallback.cjs 做判据复算,6 用例全过 (只有日额度→退到它 / 混合→一次性胜出 / 日额度已过点→— / 一次性已过期→退到日额度 / 空数组→— / 只剩不限量→保留)。回归 cdp-verify-credit-cols.cjs 26 条全过。 typecheck + build 双过;行尾均 LF。
点表头排序不再只改视图 —— 同一次点击把排出来的先后整队重编号写回 (POST /api/accounts/reorder),屏幕上那一列的先后就是转发会走的先后。 后端这条本来就通,缺的只是「写完没写回」: - POST /api/accounts/reorder(store_order.rs:35)按给定顺序把全局 priority 重编成 0,1,2…,一个事务写完;校验「必须是全部账号的一个排列」 - priority 正是 routing::plan_route 的第一排序键(compare_by_priority), 而转发选路与队列端点调的是同一个 plan_route - 桥与落盘函数全现成(bridge.rs:327 / accounts-data.ts:770 reorderQueue), 拖拽一直在用,排序从没调过 实现(accounts-page.tsx,reorderBySort): - 源行取 allGroups.flatMap(g => g.members)(= 当前队列序),不是 shown —— shown 只是筛选/折叠下看得见的那些,拿它写回会漏行、后端 400 - 整表档:比较器排全部行直接取序;组内档:按 allGroups 的组序折叠 (组序不动,只改组内先后) - next === null(取消排序)不写回;只在全局账号表下写回(模型域是投影) - 撤掉排序锁(locked 去掉 Boolean(sort))——行序=队列序,拖动反而是对的 验证(真实 WebView2 + CDP 9222): - cdp-verify-sort-writeback.cjs 13/13:屏幕序==priority 序(16 行、p 0..15); priority 被重编成 0..N-1 连续段;取消排序不写回;幂等 - cdp-verify-sort-group-scope.cjs 4/4:组序不变、组内按 priority 单调不减
审查「排序写回调度队列」后的两处收口(均落在 dev,不碰 main): 1. 真实 bug —— 切「组内 / 整表」档不写回 切档此前只调 setSortScope(只 patch store 重绘),屏幕行序变了、队列没变, 从「所见即所得」要消灭的分叉里漏了回去。 修法:新增 onSortScope(scope),切档也走 reorderBySort;两个档位按钮接线到它。 守护:cdp-verify-scope-switch-writeback.cjs(7/7)。 2. 「顺序」列撤掉排序按钮 这一列显示的是「组内位次」(rankMap,每组从 aimod-cc#1 起,aimod-cc#1 在屏幕上重复 10 次), 而排序读的是「全队列位次」(seatOf)—— 两个口径不同,导致: · 点升序 = 空操作(屏幕一格不动) · 点降序只产生部分反转 排序写回队列做实后,这个矛盾更会真的挪动队列,故撤掉入口。 改法:accounts-panels 表头 sortable('order',…) → 纯 span.th-label(保留列宽把手); accounts-sort 的 SORTABLE_COLUMNS 去掉 'order';三处注释同步改口径。 验证(真实 WebView2 应用,CDP 9222 / 3066): cdp-verify-sort-writeback.cjs 13/13 cdp-verify-sort-group-scope.cjs 4/4 cdp-verify-order-col-unsortable.cjs 8/8(新增) cdp-verify-scope-switch-writeback.cjs 7/7 另查明(未修,非本次引入):按「最近到期」排序时 12/16 行的键为 null(usage 未就绪), null 项彼此并列,稳定排序保留输入序 —— 在 usage 刷新窗口内点排序,结果可能不稳定。 详见 docs/下游开发与同步方案.md §8.1.5。
原来按面板宽度自动切「常驻展开 / 收成 44px 图标条 + 悬停浮层展开」,
阈值 = tableMinWidth(1505) + 230 = 1735,而本机 2048 窗口下面板实宽只有
1638(版心 --content-max: 80vw 再减 20px padding ×2)——这个自动逻辑在本机
永远是收窄态,用户看到的「悬浮折叠」是常态而非偶发。
改为:
- 默认常驻展开 218px,不随窗口宽度自动收(ResizeObserver 整段删掉);
- 收起只能手动点按钮,44px 图标条;
- 收起态点厂商图标直接切作用域(原来的「悬停展开浮层」语义整体删除);
- 状态存 localStorage['workbuddy-desktop-accounts-rail-collapsed']。
接受的代价:左栏占 218px,窗口不够宽时表格横向滚动。
改动:
- accounts-page.tsx:删 railThreshold/railNarrow/ResizeObserver,
改 railCollapsed + toggleRailCollapsed + 持久化键;
- accounts-schedule.tsx:ScopeRail props 改 {collapsed,onToggleCollapse},
collapsed 提前 return 图标条,展开态新增 .acct-rail-top;
- page-accounts-rail.css:删浮层规则,.rail-narrow → .rail-collapsed,
新增收起态样式;
- ui.js / ui.css:vite build 重新生成。
验证:tsc --noEmit ✅;vite build ✅;cdp-verify-rail.cjs 23/23;
probe-rail-collapsed-switch.cjs 7/7;改动的 5 个文件行尾全 LF(CR=0)。
上游 33 个提交(e8a9d57..f8da217 / v3.0.5),含移动端适配 + iOS PWA、 网络代理页重构(内核下载弹窗 / 订阅管理弹窗 / 代理池搜索筛选分页)、 凭证与网关 Key 加密落库、模型页「自定义 · 全部」聚合、家内轮询档位与会话 粘性、长度截断自动续写等。 冲突 3 处,均与下游自有改动重叠: 1. accounts-page.tsx(源码,手工解) - `locked` / `lockReason`:上游把「顺序不可拖」拆成 groupLocked(组顺位) + 组内不锁(落点按可见行换算)。我们保留含 scheduleScope 的 locked 作 文案用,groupLocked 只喂拖拽模块;两处 lockReason 合并成一份。 - `dragRef`:上游新增 renderedRows(组内落点按真画出来的行换算)。 我们那侧变量名是 storeGroups(上游叫 groups),合并后 `{ displayed: storeGroups, all: allGroups, renderedRows }`。 - 页脚锁定条:两段内容都要(我们的排序状态 + 模型域状态 + 上游锁定条), 锁定条文案按作用域分叉(模型域说「顺序」、全局说「组顺位」)。 - 核对:renderedRows 用 pageRows 算、我们的 renderSections 排序后行序取 自 shown,两者行集合与序一致(filter 保序),无冲突。 2. ui.js / ui.css(构建产物):取上游版后 `npx vite build` 重新生成 (ui.js 1,167,00 kB / ui.css 72,334 B;含下游左栏 / 排序 / 侧栏子项的代码)。 验证:`npx tsc --noEmit` ✅;`npx vite build` ✅;改动文件行尾全 LF(CR=0)。 应用内 CDP 验证:左栏 23/23、排序写回 13/13、顺序列 8/8、切档写回 7/7、 合并完整性 8/8(页面齐全 / 左栏在 / 两列表头在 / 上游 16 个 CSS link 一个不少 / 上游新端点可达 / 无 JS 错误)。
原来主键相等时比较器返回 0,并列段的先后就由「传入数组的顺序」决定。 `Array.prototype.sort` 的"稳定"只保证「保输入序」——而输入序每次渲染都可能 不同(屏幕画的是 shown、写回拼的是 allGroups,两者不是同一个数组实例)。 于是 usage 未就绪时(16 行里 12 行的排序键为 null)会出现:点一次排序, 屏幕行序 ≠ 已落盘的队列序。实测冷启动窗口偶发,落定后 52 次 0 失败。 改法:主键相等时不再返回 0,改按 (addedAt, id) 定序,且**不随方向翻转** (与「缺失值恒排最后」同理 —— 它表达的是「同一批里的固有先后」,不是排序维度)。 为什么取 addedAt 而不是别的: - 后端 `compare_by_priority`(core/routing.rs)的键就是 `(priority, addedAt)` 字典序,前端默认序 `byPriorityOrder` 与它同构。尾键取 addedAt 让「主键并列的 那一段」与「取消排序后回到的默认序」次序一致 —— 取消排序不该把并列段换个队。 - id 兜底:addedAt 可缺失(按 0 参与),两个都缺时再按 id 定序,保证任何输入都 得到同一个结果(不是"大概率稳定"而是"确定")。 实测(本机 16 账号):addedAt 全部非空且互不重复 → 尾键第一层即全序; id 唯一 → 第二层是防御性兜底(应对手工编辑的账号文件)。 验证(真实 WebView2 / CDP 9222,dev 3066): - 新增 `cdp-verify-sort-tiebreak.cjs` —— 8/8:全并列时结果与输入序无关 / addedAt 与 id 可用性 / 升降序屏幕行序 == 队列序 / 反复切换结果幂等。 - **冷启动 usage 未就绪窗口**下同样 8/8(这正是原问题出现的场景)。 - `cdp-verify-scope-switch-writeback.cjs` 连跑 3 轮 7/7(原 §2c 在此窗口会 6/7)。 - 全量回归:左栏 23/23、排序写回 13/13、顺序列 8/8、组内档 4/4、合并完整性 8/8。 - 文件行尾全 LF(CR=0);提交前已复核索引里的 ui.js 含本次改动(addedAt 出现 2 次)。
2. 補充 macOS 首次啟動的放行步驟與功能差異說明
上游 16 个提交(f8da217..68d0a4e / v3.0.8),跨 v3.0.6 / 3.0.7 / 3.0.8 三个版本。 上游主线: - 余额查询大扩展:自定义提供商接入 7 种方案(Sub2API / NewAPI / DeepSeek / OpenCode / 商汤日日新 / 智谱 / 智谱 ZAI)+ 独立查询凭证加密存储;Antigravity 新增 5 小时 / 每周窗口余额读数;「积分明细」扩展到小浣熊 / AutoClaw / Qoder / ZCode / Loomy / OfficeAce - 新增 OrcaTerm 全套(Web 控制台扫码登录 / 693 行 chat / 模型清单对齐) - AI 助手入口与对话页恢复(新增 assistant-page / state / tools / settings-pane 共 2154 行 + page-assistant.css 423 行) - 开发者角色兼容开关(developer → system)、Cline Pass 预置卡、导入凭证文件 (小浣熊 / CatPaw auth.json、Cline providers.json、KukuAI Cookies SQLite) - 出网新增「轮询」档(proxy_rotate.rs 273 行);代理池监听器双向对账 - 修复:Antigravity 思考签名、404 不原地重发(aimod-cc#255)、CodeArts 积分翻倍(aimod-cc#261)、 Trae 中国版有效期(aimod-cc#263)、设置页 290 处文案恢复多语言管道 - 文档重整:README 只留快速上手,多语言版收进 docs/ 冲突 3 处(比上次少),均与下游自有改动重叠: 1. accounts-page.tsx(源码,手工解) 上游只改了 2 行 —— 厂商/组名走翻译管道(`item.label` → `t(item.label)`、 `group.label` → `t(group.label)`)。但两处都在我们新增的「模型作用域」整段 (227-401 行)的**相邻行**上,git 3-way 因此把整段判成冲突。解法:吸收上游 那一行(翻译管道是上游修复,该吸收)+ 全留我们那一段 + 保留我们对 `shownDefault` 的注释改名(「真正会渲染出来的行」→「默认次序下的全部可见行」, 因为有了排序后它表示的是默认次序)。 2. accounts-panels.tsx(上游 103+/23-,**自动合并成功**) 已核对:上游是「翻译管道 + 纯新增(UsageFlatRowView 余额两行式、ProxyCell 轮询档补位项)」,未触碰我们的「顺序」列表头(已确认 cell-order 段完好)。 3. ui.js / ui.css(构建产物):取上游版后 `npx vite build` 重新生成 (ui.js 1,261.46 kB / ui.css 73.01 kB)。⚠️ 上游 v3.0.8 把 .gitignore 的 `/docs/` 改成 `/docs/release-notes/` —— 我们的 私有台账 docs/下游开发与同步方案.md 因此不再被忽略(git status 显示 ??)。 已在 .git/info/exclude 加一行挡回(本机私有,不动上游文件)。 验证:`npx tsc --noEmit` ✅;`npx vite build` ✅;改动文件行尾全 LF(CR=0); 提交前复核索引里的 ui.js 含下游代码(acct-rail-collapse 出现 1 次,非 0)。
1. 新增 Windows / Linux 服务端形态(无界面、命令行安装、注册成系统服务,开机自启、崩溃自动拉起);Linux 服务端包同时用于容器内一键更新 2. 智谱清言接入任务中心:新手任务并入新手福利、每日任务并入每日签到(访客如实跳过) 3. 模型管理页新增「可见性」列与筛选,默认只展示官方客户端会列出的模型;不展示的模型默认关闭(含默认绑定) 4. 说明文件补充 Windows / Linux 服务端部署指引 优化 1. 智谱清言余额改用账户真实积分并修正显示单位;「积分明细」列出真实收支流水 2. 智谱清言单轮对话不再携带标记,官方客户端聊天记录里不再出现 <|user|> 之类的字样 3. 服务端日志支持写入文件,服务形态下不再「起来了却什么都看不到」 修复BUG 1. 智谱清言回答被重复下发两遍 2. 智谱清言账号在模型管理与余额查询里整家消失(升级后自动修复) 3. 智谱清言余额放大一百倍、未参与活动账号余额显示为 0 4. Qoder / WorkBuddy 可见模型被旧默认名单误关,升级后自动重新打开 5. 本地文件订阅导入失败
2. 新增:服务端一键安装脚本 —— 升级重跑脚本,卸载可连数据一起删 3. 新增:Linux 桌面端安装包(deb / AppImage),与 Windows、macOS 对齐 4. 新增:Linux 服务端包 —— 容器内一键原地更新 5. 新增:智谱清言提供商(chatglm.cn) 6. 新增:智谱清言三种添加方式 —— 访客免登录、粘贴令牌、导入客户端登录态(仅 Windows) 7. 新增:智谱清言对话(直连上游 API,不是网页对话) 8. 新增:智谱清言余额读真实积分流水(积分明细逐条列出收支与到期) 9. 新增:智谱清言任务中心 —— 新手任务并入「新手福利」,每日任务并入「每日签到」 10. 新增:智谱清言加入自动签到;访客账号无积分体系,如实跳过 11. 新增:Antigravity 放开 Claude 与 GPT-OSS 家族(此前只接 Gemini) 12. 新增:Antigravity 分组额度窗口读数(Gemini / Claude-GPT 各 5 小时与每周) 13. 新增:模型管理页新增「可见性」列与筛选,不展示的模型默认关闭 14. 更新:OrcaTerm 登录改走官方 OAuth 授权页扫码 15. 更新:更新检查与安装适配 Linux —— deb 走系统安装器、AppImage 换文件 16. 更新:说明文档补充服务端部署指引与网页面板功能差异 17. 更新:发版与协作规范改由 AGENTS.md + release-flow skill 承载 18. 优化:Qoder 清单不再只留一个模型 —— 该关的交给可见性判据 19. 优化:账号页余额格支持一组多窗口的紧凑读数(Antigravity 四条横排) 20. 优化:自定义提供商的静态额外头上限 8 → 16 21. 修复:Loomy 图片适配按模型是否支持识图判断,避免上下文体积翻倍 22. 修复:订阅编辑时换本地文件不生效(字段名与后端读的键不一致) 23. 修复:对话预览把带保活注释行开头的 SSE 响应误判成 JSON 24. 修复:数据库整理(VACUUM)后总占用不降反升 25. 版本:3.0.8 → 3.0.9-beta.1
本轮审查(对 dev 自查)发现三项,一并收口。
── 1. 收起态厂商图标点下去不是"选这家"(语义缺陷)──────────
`ScopeRail` 收起态的厂商图标原为 `setScheduleScope(group.items[0]?.value)`,
取的是该组**第一个模型**。实测点「Trae」图标 → 作用域变成模型 `Trae28`:
用户按的是厂商,落到的是某个具体模型。
根因:作用域的单位是**模型名**(`plan_route` 收窄候选走的就是它),只有
「全局 / 某个模型」两级,**没有「厂商」这一级** —— 图标本无处可切,只能拿
第一个模型顶替。且与展开态口径冲突:展开态的组标题是**文件夹**(只
`toggleGroup` 折叠,源码注释明写"点它只折叠/展开,不筛账号")。
改法:图标改为「展开左栏 + 展开该组」(`revealGroup`),两态语义统一为
「厂商这一级永远是文件夹,永远不变成一次筛选」。真正切作用域的是图标条上
那枚「全部」(它切到 `SCOPE_ALL`,不是文件夹)。title/aria-label 同步改成
「展开左栏,看 {label} 下的 {n} 个模型」。
── 2. 排序比较器依赖 Date.now(),破坏传递性(正确性隐患)──────
`nearestCreditOf` 每次调用现取 `Date.now()` 用于滤掉已过期的积分包,而它被
`accountComparator → creditExpiryOf / creditLeftOf` 在一轮 `sort()` 里调用
n log n 次。若某份包恰好在这轮排序的时间窗内跨过 `expireAt`,**同一行**会先
后算出两个不同值 → 比较器失去传递性 → 排序结果不可预测(概率极低,机制真实)。
改法:
- `creditRowsOf(entry, now = Date.now())` 与 `nearestCreditOf(entry, now = ...)`
都把 `now` 提成可传参(默认值保持"渲染时用此刻"的语义不变);
- 顺带修一处内部不一致:`nearestCreditOf` 原先自己取 `Date.now()`、再调
`creditRowsOf`(后者又取一次)—— 两次取值可能不同,导致「算最近到期」与
「标已过期」对同一份包得出相反结论。现在 `now` 一路传下去,只有一个时刻。
- `SortContext` 新增可选 `now`;`accountComparator` 在**构造时**定格一次,
整轮比较复用(调用方不必给)。
── 3. aria 不对称 ──────────────────────────────────────────
收起态 `aria-expanded={false}`、展开态写裸 `aria-expanded`(= true),
语义含糊(它们控制的是左栏本体,不是子菜单)。两个切换按钮都补
`aria-controls="acct-scope-rail"` 指向左栏,展开/收起口径一致。
验证(真实 WebView2 / CDP 9222,dev 3066):
- 新增 `cdp-verify-rail-chip-fix.cjs` —— §1a 点击后回到展开态 218px /
§1b 作用域未被改成某个模型(行数 16→16)/ §1c 该组展开后可见 28 个叶子 /
§1d title 是「展开左栏…」/ §2a-b 两个按钮都带 aria-controls / §2c
aria-expanded=false 正确 / §4 无 JS 错误。§3b 首次失败经查是**脚本判据写错**
(连点两次后在降序状态比对升序应有的队列序),改用
`cdp-verify-sort-queue-equality.cjs` 严格「无排序→点一次」后通过。
- 全量回归:左栏 23/23、排序写回 13/13、顺序列 8/8、切档写回 7/7、
组内档 4/4、稳定尾键 8/8、队列相等 1/1。
- `npx tsc --noEmit` ✅;`npx vite build` ✅(ui.js 1,261.77 kB);
改动文件行尾全 LF(CR=0);提交前已复核索引里的 ui.js 含本次特征串。
1. 修复:server crate 关掉 bin 自动发现 —— src/bin/service.rs 是 agent2api-server 的 模块(#[cfg(windows)] pub mod service;),却被打包成一个「没有 main、又依赖 Windows-only crate」的假二进制;docker 与 linux-server 走 cargo build -p(不带 --bin),在 Linux 上直接 E0601 挂掉(实测 E0601: main function not found in crate service) 2. 修复:windows-server 作业改在 desktop-tauri/src-tauri 下构建 —— 产物目录由 src-tauri/.cargo/config.toml 的 target-dir=../../target 决定,而 cargo 只从 CWD 往上找配置文件;从仓库根跑会落到 src-tauri/target,打包步骤找不到 exe 而失败
2. 新增:服务端一键安装脚本 —— 升级重跑脚本,卸载可连数据一起删 3. 新增:Linux 桌面端安装包(deb / AppImage),与 Windows、macOS 对齐 4. 新增:Linux 服务端包 —— 容器内一键原地更新 5. 新增:智谱清言提供商(chatglm.cn) 6. 新增:智谱清言三种添加方式 —— 访客免登录、粘贴令牌、导入客户端登录态(仅 Windows) 7. 新增:智谱清言对话(直连上游 API,不是网页对话) 8. 新增:智谱清言余额读真实积分流水(积分明细逐条列出收支与到期) 9. 新增:智谱清言任务中心 —— 新手任务并入「新手福利」,每日任务并入「每日签到」 10. 新增:智谱清言加入自动签到;访客账号无积分体系,如实跳过 11. 新增:Antigravity 放开 Claude 与 GPT-OSS 家族(此前只接 Gemini) 12. 新增:Antigravity 分组额度窗口读数(Gemini / Claude-GPT 各 5 小时与每周) 13. 新增:模型管理页新增「可见性」列与筛选,不展示的模型默认关闭 14. 更新:OrcaTerm 登录改走官方 OAuth 授权页扫码 15. 更新:更新检查与安装适配 Linux —— deb 走系统安装器、AppImage 换文件 16. 更新:说明文档补充服务端部署指引与网页面板功能差异 17. 更新:发版与协作规范改由 AGENTS.md + release-flow skill 承载 18. 优化:Qoder 清单不再只留一个模型 —— 该关的交给可见性判据 19. 优化:账号页余额格支持一组多窗口的紧凑读数(Antigravity 四条横排) 20. 优化:自定义提供商的静态额外头上限 8 → 16 21. 修复:Loomy 图片适配按模型是否支持识图判断,避免上下文体积翻倍 22. 修复:订阅编辑时换本地文件不生效(字段名与后端读的键不一致) 23. 修复:对话预览把带保活注释行开头的 SSE 响应误判成 JSON 24. 修复:数据库整理(VACUUM)后总占用不降反升 25. 版本:3.0.8 → 3.0.9-beta.1
本轮审查有效期 / 最近到期 / 最近到期积分三个字段,查出两个真实错误,
都在 `nearestCreditOf` 里,且都能在屏幕上复现。
── 错误 1:信任「后端已按到期升序发出」——这个前提对 WorkBuddy 不成立 ──
旧实现取「第一个未过期的一次性包」,理由写在注释里:「后端两家的 credits[]
都已按到期升序发出」。实测这个前提是错的:
`billing::usage` 的排序是**先按套餐优先级、同优先级才看到期时间**——
代码里 `plan_priority` 一不同就直接 `return`,到期时间根本不参与比较。
于是 Free Plan Subscription(高优先级、**2035 年**到期)会排在
Bonus Pack(低优先级、**10 天后**到期)前面。
实测 sunny-00001:旧实现选「Free Plan Subscription」剩 100 / 2035 年,
而真正 10-25 就要作废、还剩 250 的「Bonus Pack」被完全藏住。
改法:不再信任顺序,自己按 `expireAt` 升序排一遍(缺失视为 +∞ 垫底)。
「前端再排一次会让两处口径分叉」那个顾虑针对的是**明细列表的展示顺序**
(那里仍照后端,`creditRowsOf` 不排序),与「挑哪一份」是两个问题。
── 错误 2:已花光的包占着这一列,遮住还有余额的份 ──────────
「最近到期」问的是「哪笔钱要作废了」,已经花光的包没有钱可作废。
旧实现只跳过「已过期」,不跳过「已用尽」,于是:
实测 sunnyzt001(officeace):选「每日签到有礼」remain=0(今晚 23:59 归零),
而「新用户专属福利」还剩 1400+ 分、11-13 才到期,被整个遮住。
改法:分两级 ——
①-a 优先取「还有钱」的最先作废包;
①-b 只有当**所有**一次性包都花光时,才回头报最先作废的那份(读数显示 0)。
这一列问「什么时候作废」,全花完时显示 `—` 反而丢信息,而「0」准确
说出了「那一刻会作废 0 分」。
判据直接复用 `CreditRow.spent`(remain ≤ 0,或 remain 缺失时 used ≥ total;
两者都读不到时不猜),不另写第二份。
── 顺带两处 ──────────────────────────────────────────────
- `CreditExpiryCell`:`Date.now()` 改为取一次、`nearestCreditOf` 与倒计时共用
(各取一次会让「选中哪份」与「还剩多久」差一个瞬间)。
- 修正 `NearestCredit` 上方那段已过期的注释(照抄了错误前提)。
验证(真实 WebView2 / CDP 9222):
- 新增 `cdp-verify-credit-fields.cjs` —— **7/7**:有余额可花时不选已花光的 /
所选是"有钱份"里到期最早的 / 「最近到期积分」数值 == 所选包 remain /
有效期列每行有读数 / 两个真实修复实例 / 无 JS 错误。
- 屏幕实测对比:
· sunny-00001 2035 年 Free Plan(100) → **10-25 Bonus Pack(250)** ✅
· sunnyzt001 每日签到有礼(0) → **新用户专属福利(1400+)** ✅
- 全量回归:左栏 23/23、排序写回 13/13、稳定尾键 8/8、积分列全绿、
顺序列 8/8、切档 7/7、组内档 4/4。
- `npx tsc --noEmit` ✅;`npx vite build` ✅;改动文件行尾全 LF(CR=0)。
1. 修复:linux-desktop 附件扫不到 .deb / .AppImage —— 上传侧 path 是
target/release/bundle/{deb,appimage}/*,upload-artifact 以两者的最近公共祖先为
包根,解出来是 linux-desktop/deb/… 与 linux-desktop/appimage/…;脚本原先只扫
一层,于是 Release 建出来了却没有这两个包(2026-10-17 实测)。改为 find -maxdepth 2
2. 修复:附件改逐个 --clobber 上传 —— 一次传一批时 gh 会在某个已存在的附件上报
HTTP 422 ReleaseAsset.name already exists,整批中断,症状是附件少一半
下载回显原写 "${LINUX_PKGS[@]}",本机 Git Bash 的 /usr/bin/basename
是 POSIX 形态(只取一个操作数),数组里后几个被静默吞掉 —— 回显只剩 AppImage 时
看着像「deb 没下载到」。改用 bash 自己的 ${arr[*]##*/}
继续审查「最近到期」字段,又查出两处小问题(都不影响当前数据下的显示, 但都是"迟早会咬人"的类型)。 ── 1. 两列各取一次 Date.now(),可能指向不同份额度 ────────── `CreditExpiryCell`(最近到期)调 `nearestCreditOf(entry, now)` 有传时刻, `CreditLeftCell`(最近到期积分)却是 `nearestCreditOf(entry)` —— 自己再取一次 `Date.now()`。两列本该描述**同一份**额度,但只要某个包的过期时刻正好落在 两次调用之间(毫秒级窗口),左列报 A 份、右列报 B 份。 实测风险面存在:17665482016 有个包在 20 分钟后到期,正是这种临界点。 改法:`CreditLeftCell` 也先取一次 `now` 再传进去,与左列同一处置。 ── 2. `expireAt` 没做秒/毫秒归一,与 `expiryMillis` 不同待遇 ── `creditRowsOf` 直接 `numberOrNull(pack?.expireAt)`,而「有效期」列走 `expiryMillis`(`raw < 1e11 ? raw*1000 : raw`)。当前所有 provider 实测都发 毫秒(76 个 expireAt / 73 个 grantAt / 4 个 startAt 全是毫秒),但少这一层 等于把「上游哪天改成秒」变成一次静默的 1970 年 —— 而这类字段的单位在同一份 文档里混用是常见的事。 改法:`creditRowsOf` 的 expireAt 改走 `expiryMillis`(0 → null,与 `CreditRow.expireAt` 的既有约定一致),并 import `expiryMillis`。 验证(真实 WebView2 / CDP 9222): - `cdp-verify-credit-fields.cjs` **7/7**(含两个真实修复实例) - `cdp-verify-credit-cols.cjs` 全绿、`cdp-verify-sort-tiebreak.cjs` **8/8** - `npx tsc --noEmit` ✅;`npx vite build` ✅(ui.js 1,262.03 kB); 改动文件行尾全 LF(CR=0);提交前已复核索引产物。 顺带记录的审查结论(**不改**): - 「最近到期」与「有效期」用同一套倒计时措辞(分钟/小时/天)是**有意的** —— 两列读数形态一致,扫读时不必切换写法; - `unlimited` 包与负数 `remain` 在当前数据里都是 0 例,`spent` 对它们的处置 正确(不限量恒不 spent、负数 remain 被判 exhausted),无需改。
1. 新增:docs/install.md —— 中文「安装与部署」完整说明(选哪种形态 / 桌面端 /
服务端(Windows Server、Linux systemd、装完之后、无人值守)/ Docker /
验证与客户端接入 / 局域网访问),三块内容由 README 安装段原样搬入,路径与锚点已修
2. 新增:docs/install.{en,zh-Hant,ja,ko,pt-BR}.md —— 五份译文。安装段沿用各语言
README 的既有译文;「服务端(Windows / Linux,无界面)」整节此前只有中文版,
本次补齐;同时纠正「那两份 tar.gz 不是独立部署形态」的过时说法 —— 同一个包
也是 Linux 服务端的安装包(见 selfupdate 的取包逻辑)
3. 更新:六份 README 的安装段瘦身为「快速开始」速查表(形态 / 产物 / 界面三行表
+ 每种形态的最短命令),完整说明走链接;导航里的 Docker 入口换成安装文档入口
4. 更新:README 418 → 219 行,五份译文 295 / 296 → 219 行,六份结构对齐
5. 更新:AGENTS.md 与 release-flow Skill 记下安装文档落点(六份一一对应,
产物名 / 端口 / 环境变量 / 安装脚本参数有改动就一起改),验收清单加一条
「安装文档对得上实际产物」
侧边栏「智能」组新增一级菜单,紧挨 AI 助手。两页都是「对话 + 模型下拉」,
但打的**不是同一条接口**,这正是这一页存在的理由:
· AI 助手 → /api/assistant/chat(面板面,自带系统提示词与工具集)
· 试炼场 → /v1/chat/completions(网关面,客户端实际打的那一条)
于是试炼场的结论能直接代表「我的 base_url + Key 能不能用、模型通不通」,
助手页给不了这个结论。接客户端之前先在这里自检,不必再开第三方客户端。
功能
· 模型下拉:从桥取(含映射名,与 /v1/models 同一口径)
· 流式逐字渲染;推理模型的思维链单独折叠(默认收起)
· System Prompt 可折叠,留空则不发送 system 消息(与多数客户端默认一致)
· 温度(勾选才传,与「传 1」不是一回事)/ 最大输出
· 多轮对话(失败的轮次整轮丢弃,不把空回答回灌给上游)
· 停止 / 清空
· 统计条:首字节 / 总耗时 / 输入输出 token / 缓存命中 / 结束原因
设计依据:全站 20+ 同类项目横评(New API 游乐场、OctaFuse Playground+Simulator、
Cherry Studio 多模型并排)中定为优先级 1 —— 复用最多、增量最小。
实现
· 凭证复用 AI 助手那条直连链(问桥取第一把启用的网关 Key + 401 清缓存重试),
assistant-state 的 requestDirect 由私有改 export,其余零改动
· 三个纯新增文件(playground-page.tsx / playground-state.ts / page-playground.css)
· 挂载点最小:index.html 三行(nav 按钮 / section / css link)+ app.js 两处
(PAGES 与 PAGE_LABELS —— 漏了会被 showPage 回退成 overview)
验证
· npx tsc --noEmit 通过;npx vite build 通过(531 模块)
· 真实应用(WebView2 + CDP 9222)结构验证 16/16 通过
· 真实应用端到端实发 5/5 通过 —— 实测回「你好」,
首字节 2.03s / 总耗时 2.39s / 输入 22 / 输出 56 / 结束原因 stop
同时入库 docs/功能路线与侧栏导航规划.md(20+ 同类项目横评与缺口分级)。
审查模型调度系统(提 PR 前自查)发现三处:两处「注释声称有防护、实际没有」,
一处「视图承诺与后端硬不变量冲突」。逐条修掉,并附运行时验证。
一、模型作用域拖拽会静默改写全局队列(P0)
groupLocked 不含 scheduleScope,而注释声称「由 MainTable 的 readOnly 挡着」
—— MainTable 与 readOnly 都不存在,那句是没有实现的假设。后果是页面一边
显示「顺序已锁定」,一边真的能拖、真的把队列重编号。
· QueueDragHandlers 的 isLocked / onLockedAttempt 改成按把手粒度(kind)
· groupLocked 补 Boolean(scheduleScope)
· 组把手看 groupLocked,行把手只看 scheduleScope(筛选/分页下组内仍可拖,
落点按可见行换算回全量,映射是确定的)
· onDrop 加 scopeRef 守卫:它是 useCallback([]),闭包读不到最新 scheduleScope
二、模型作用域点表头排序:屏幕变了、队列没变(P0)
cycleSort 无条件执行,只有 reorderBySort 被 scheduleScope 挡住 ——
排序状态被写进全局 store(sort 不区分作用域),屏幕行序从队列序变成排序序,
队列一字未动。改为前置 return,连 cycleSort 一起挡。
三、整表(flat)档写回破坏「同一家连续」硬不变量(P1)
flat 会打散厂商分组,写回的队列里同家的号被别家隔开;后端 reorder_accounts
只校验「是全部账号的一个排列」(组连续性由调用方保证),而新增账号那条链
(sql::put → normalize_grouped_queue)会强制整理回去 —— 那个顺序存不住。
改为 flat 档不写回,UI 加 data-sort-saved 标注 + 「仅用于查看」文案,
不再给一个下次必然被推翻的承诺。
四、清理
· 删死代码 entryDisplayName(全仓库零引用)
· 清两处 docs/下游开发与同步方案.md §8.1.5 死链(该文件不入库、上游不存在)
· 「上游 v3.0.5」等 fork 视角措辞改中性
五、移除「账号健康」页
删 health-page.tsx / health-state.ts / page-health.css;index.html 三处
(nav 按钮 / section / css link)、app.js 两处(PAGES / PAGE_LABELS);
三处注释里「在账号健康页看」改指向「账号」页。该页靠
import.meta.glob 自动注册,删文件即卸载。侧栏 13 页。
验证(真实 WebView2 / CDP 9222 / API 3066,30 断言全过)
· 模型域拖组把手/行把手 → 均未写回队列,未进入拖拽态
· 模型域点表头 → 无排序态、屏幕行序不变、后端队列不变
· 整表档队列不变(saved=0)、切回组内档写回(saved=1)
· 回归:换列排序仍写回、priority 重编 0..N-1、「同一家连续」仍成立
· 健康页:侧栏 13 页、无按钮/section/link、产物 0 命中
· tsc --noEmit EXIT=0;vite build 536 模块;无 JS 运行时异常
docstring 里点的 docs/下游开发与同步方案.md 不入库,是这个 fork 的内部文档 → 对任何读代码的人都是死链。只删引用、保留标题。(PR 分支上同样已清。)
优化:访客判据只认确凿证据(记录 isGuest / JWT 声明),只粘登录令牌的账号不再被误判踢出签到清单 更新:账号页「限流」列改为弹层明细 —— 逐行列出限流中的模型、还要等多久、恢复时刻与原因,可单条 / 全部清除,表格行高不再忽高忽低 优化:账号页与「网络代理」页共用同一份代理池缓存,禁用节点 / 删订阅后下拉即时刷新,禁用条目置灰不可选 优化:轮询多选支持禁用项置灰(后端取用本就会跳过,列出能选等于让用户白挑) 优化:更新面板检查失败时保留上次成功的版本号与更新日志,标注「来自上次成功检查」,不再整块清空连旧版本都看不到 修复:账号迁移凭证判据改为按凭证键表检查 —— OfficeAce 等用 modelAppKey 的账号不再「导得出去、导不回来」 更新:发版流程接入 Android APK —— CI 新增 android job,Release 挂 APK 与 .sha256,发版文档补第六处版本号与签名密钥配置 优化:删除 Node 版遗留的积分 / 新手任务服务文件,忽略 curl 调试残留文件 更新:五语言 i18n 同步(限流明细、更新面板新文案)
2. 修复部分多语言界面文案显示不准确的问题
两件事:把「每天点亮成长任务」自动化,并让通知覆盖「必须动手」的场景。
## 成长任务定时排程(国内版)
成长任务原本只有手动入口(签到页「一键完成」)—— 其中时段敏感的任务更要求
用户半夜起来点(夜猫子 black_cat 仅 23:00-08:00 CST 可点亮),这正是最该
自动接手的一类。
- `core/growth_task_scheduler.rs`(新,539 行):每天定点型排程(时刻数组 +
当天去重 + 启动补跑),与 `auto_checkin` / `growth_scheduler` 同一形状。
与猫猫旅行**互不相干**:旅行是派出/领奖状态机,这里是点亮/领奖任务链,
配置键各自独立(`growthTaskSchedule` vs `growthSchedule`)。
- **默认关闭**(与旅行相反,刻意):它会发上报事件、领积分,是一串写操作;
对上游而言「每天自动把任务全点亮」比「一天两趟旅行」更容易被风控注意。
- 去重标记按**计划时刻**落而非实际执行时刻 —— 到点后最多晚一个 tick(30 秒)
才跑,按实际时刻比对永远不相等,结果每个 tick 都重跑一遍(与
`growth_scheduler` 踩过的同一个坑)。单测钉住了这条。
- 缺省时刻 23:30(落在夜猫窗口内),否则开箱即用时那一趟会被时段判定整个
挡掉,用户看到「跑了但一项没完成」。单测钉住了这条。
- `api/growth_task_schedule.rs`(新):GET / POST / run 三个端点,形状与
`/api/travel-schedule` 逐字相同,挂 protected;`checkin_center` 快照新增
`growthTask` 段;两端桥(bridge.rs / web_shim.rs)成对添加三个方法;
签到页新增 `GrowthTaskPanel`(与签到、旅行分卡,各自独立开关)。
- 零 unwrap/expect(release 是 panic=abort)。
## 三条通知事件源
新增 `account_disabled` / `no_available_account` / `credit_expiring`,都在
`available_kinds()` 里列出(否则前端勾选项渲染不出来 = 静默不会发)。
- **账号被自动禁用**:挂在余额与 Token 两条禁用钩子**真正写入禁用之后**。
禁用不会自动恢复,必须用户手动启用 —— 躺在日志里没人看见,主动推一条才对。
- **无可用账号**:`provider_loop` 的两条路径(无状态 / 有状态)同一出口。
这是全局事件,去重键留空,6 小时窗口兜住「空池时每条请求都撞上」。
- **积分即将到期**:挂在查询成功之后(读数是查询的副产品,另起任务反而要
重查上游)。三条判定:3 天内到期、尚有剩余、非日额度包 —— 日包的 expireAt
是「今天 24 点回满」的周期边界而不是作废,对日包提醒会每天喊一次且内容是错的。
验证:
- `cargo build --release`:零 warning
- `cargo test --release`:**704 passed, 0 failed**(新模块含 6 个单测)
- `tsc --noEmit` 通过;vite 构建 536 模块
- E2E(独立数据目录,端口 3096):GET 默认 `{enabled:false, times:["23:30"]}`;
POST 开启+改时刻生效;非法时刻「25:00」→ 400 带明确文案;空字段 → 400;
run 在空账号池下返回 `accounts:0` 空摘要而不报错;
`checkin-center` 三个排程段齐全(auto / travel / growthTask);
`/api/notify` 列出 5 个事件(原 2 + 新 3)
复查时发现 `getCaptchaSetting` / `saveCaptchaSetting` 只在 web_shim 实现,
桌面壳的 bridge 没有。这与 `bridge.rs` 里 getCors 那段注释记的是**同一个错误
的复发**:「上一轮加这个开关时只补了 web_shim 那一侧,桌面壳这侧漏了」。
症状比「读不到」更隐蔽 —— 调用点写的是可选链:
`await shared().workbuddyDesktop?.getCaptchaSetting()`
方法不存在时表达式得到 `undefined` 而**不抛错**,于是不进 catch,
`captcha.available` 被判成 `true`、`enabled` 为 `false`。结果是桌面端显示出
一个「可设置且当前关闭」的开关,点下去 `saveCaptchaSetting` 同样静默,
还会弹「✅ 机器人校验已开启」—— 用户以为开了,实际什么都没发生。
修法:
- `bridge.rs` 补上两个方法,与 getSanitize / getDeveloperRoleCompat 同形
(GET 读、PUT 写)。端点 `/api/captcha` 在网关服务端,桌面壳跑的是同一个
网关,本就能走同源 HTTP —— 所以「加方法」比「给类型补 ?」正确。
- `settings-state.ts` 的 loadCaptcha / saveCaptcha 补**存在性判断**:方法缺失
时降级为「不可用」并提示,不再假成功。这条是防复发的关键 —— 补了 bridge
只解决当前版本,判存在解决「新界面配旧壳」这类跨版本组合。
- `panelLogout` 同样处理:原先方法缺失时会**跳过调用直接跳 /login**,用户以为
登出了、服务端会话(含 30 天续期链)还在 —— 这是安全问题。现在缺方法就报错
不跳转。注意 `panelLogout` 桌面端仍不实现是对的:设置页那块整卡由
`snap.panelLogin` 门控,桌面壳没有「登录面板」的概念。
- 三处类型声明据实调整:captcha 两个虽已两端齐备,仍标 `?`(为跨版本组合留
退路);panelLogout 标 `?` 并注明「仅网页端」。
验证:
- `cargo check --release -p workbuddy-proxy-desktop`:通过(含 bridge 改动)
- `tsc --noEmit` 通过;vite 构建 536 模块
- 脚本核对两端桥:captcha 两方法 bridge=Y / shim=Y,panelLogout bridge=N / shim=Y(符合设计)
- 实测 `/api/captcha`:GET → `{captchaEnabled:true}`;PUT 写 → 回读一致
2. 优化:恢复时取消文件选择改为静默提示「已取消恢复」,不再误报为失败 3. 优化:统一整库备份 / 恢复在不受支持环境下的提示文案
- swBuildStamp 在哈希与落盘前统一 CRLF→LF,版本号只由内容决定, 不再随检出环境(本机 autocrlf / CI Linux)变化 - 新增 .githooks/pre-commit:本次提交动了前端源码时自动重建三份入库 产物并一并暂存,把「忘了构建」拦在本地,而不是等 CI 报错 - 根 package.json 加 prepare,npm install 时自动启用钩子 - 补回上两次提交漏跑 scan 的三条 settings 译文(源词此前缺键, 重建会把 HEAD 产物里已有的译文丢掉)
1. 账号分组设置弹窗:分组带新增齿轮入口,可配置组内排序规则。 2. 分组排序规则:按积分到期时间或账号添加时间排序,方向可切换,默认关闭。 3. 分组带整条可点击展开 / 收起,按钮与拖动把手除外。 4. 账号合计支持按当前筛选或整组统计。 5. 助手设置移至助手页右上角,可配置默认模型、思考等级与系统提示词。 6. 助手配置变更执行前确认并展示改动,跳过确认时不扩大工具权限。 7. 助手输入框支持粘贴图片,支持 Enter 发送、Shift+Enter 换行。 8. 机器人校验可按桌面端 / 网页端、按提供商分别开关。 9. Antigravity 登录支持粘贴回调地址完成授权。 10. 支持在新标签页完成账号授权,成功后自动加入列表。 11. 网页端支持整库备份与恢复,文件通过浏览器下载 / 上传。 12. 接入 AtomCode:直通转发 + 次数额度 + 轮换续期,支持轮询网页登录与每日领取。 13. 接入 LobsterAI:直通转发 + 网页登录 + 每日签到。 14. 接入 ModelScope 国内版 / 国际版:单一令牌兼作推理与网页站身份,含签到与余额读数。 优化: 15. 限流列改为弹层明细,可查看模型、等待时长与恢复时刻,支持逐条清除。 16. 账号页与网络代理页共用代理池缓存,禁用条目置灰不可选。 17. 账号并发上限支持填 0 使用服务商默认值。 18. 网关地址支持 http/https 前缀,缺端口时按协议自动补全。 19. 记录列表支持下拉刷新,并可继续加载更早记录。 20. 网页登录的「粘贴回调地址」改为兜底入口,不再默认弹出。 21. 更新面板检查失败时保留上次成功的版本与日志。 22. 多语言文案补全并修正多处翻译不准确。 修复: 23. 修复整组拖动时没有落点提示线。 24. 修复登录过期后多标签页操作偶发退出登录。 25. 修复修改密码后其他设备登录未失效。 26. 修复节点监听端口未按账号引用范围分配。 27. 修复配额拒答返回 502 而非 429 + Retry-After。 28. 修复 AtomCode 配额耗尽被当成正常答复。 29. 修复 AtomCode 读不到额度时误判余额为 0。 30. 修复 LobsterAI 签到 404 与请求缺少 Content-Type。 31. 修复账号迁移时凭证判据不全导致导入失败。 32. 修复 sw.js 版本号漂移导致产物校验失败。 移除: 33. 移除智谱清言访客账号的每日签到。 版本:3.1.0 → 3.2.0-beta.1
…issing script "build:apk";改为 `cd ..`
千问办公(qwenwork):
- 认证链复用 Qoder 形态(PKCE 设备授权 + auth-v2.dat 本机登录态),
网关 gateway.qwenwork.cn 签发 sk-mm-v1.*,推理走 maas.xiaoluozi.cn
- adapter 族 `{code:"ok",data:{}}` 信封解包;wallets 给余额与最近到期,
account-context?include=plan 给套餐与 next_due_date
- 每日签到 `POST daily-credit-notice/claim`(**必须空 body**,带 {} 会 400)
- 有效期三级兜底:解析时读 JWT exp、落库时兜底、读时兜底
(存量记录不重导也有 expiry —— 注意取值必须从 record 原始字段取,
公开形态会把 accessToken 抹掉)
- 单位统一「积分」(前端 unitLabel 认中文键)
QClaw:
- 导入本机网关配置,积分随登录自动发放、无签到端点
→ 不进 CHECKIN_PROVIDERS,签到中心走 extras.autoGrant 只读分组
签到框架:CHECKIN_PROVIDERS 加 qwenwork,PROVIDERS_VERSION 4
同时:
- 前端重建产物(ui.js / mobile.js / sw.js)
- qwenwork / qclaw 图标从客户端 RT_GROUP_ICON 提取 256×256
- .gitignore 补 /dist-local/(本地打包验包,不入库)
…bsterAI / ModelScope)
上游本次带来 3 家新 provider(共 4 个 id):
- **AtomCode**(AtomGit / GitCode):直通转发 + 次数额度 + 轮换续期,
轮询网页登录 + 每日「领取免费 CodingPlan」(按需:套餐有效时只回读额度)
- **LobsterAI**(网易有道「有道龙虾」):直通转发 + 浏览器登录 + 每日活动签到
(+100 积分/号/天,幂等靠 idempotencyKey + 上游 claimedToday)
- **ModelScope 国内版 / 国际版**:一枚 `ms-` 令牌两用(Bearer 调推理 +
Cookie 值读网页站身份/魔粒/签到)。上游没有独立签到接口 ——
调一次 `GET {网页站}/api/v1/users/login/info` 本身就触发当日魔粒签到(幂等)
冲突解决要点(39 个文件):
1. **providers 注册表类**(mod.rs / adapter.rs / catalog*.rs / auto_checkin.rs /
family 判定):两边各自在末尾追加,全部保留 —— 31 家
2. **auto_checkin.rs 版本号撞车(关键)**:本地线与上游线各自用过「4」
(本地 = 千问办公,上游 = ModelScope/AtomCode)→ 若沿用原值,配置已记到 4 的
存量用户永远补不进这几家。统一把上游三家记 **5**,并把 qwenwork 登记为 4
3. **identity.rs 整体取上游版**:上游用 `GENERIC_IDENTITY_KEYS` 通用链根治了
「每加一家回来补分支」这个负担,本地为 OfficeAce / OrcaTerm 加的专属分支
已被它取代(且通用链排在专属分支之前,留着永不执行)。只补回 Qwenwork / Qclaw
的 Generic 登记
4. **设置页分类**:本地「通知与告警」与上游「账户」并存(account 在前)
5. **captcha 分端**:取上游版(`desktop`/`web`/`localPanel` 三态 + 面板账户方法)
6. **accounts-page.tsx**:保留本地的排序作用域 / 模型作用域重构,
接上上游的 `sortActive`(组排序规则锁把手);
上游已删 `PanelsRow`/`panelOpen`(限流明细改 `LimitDetailButton` 弹层)→
`rowNodes` 里的 PanelsRow 一并去掉(MEMORY 里早已记下这条)
7. **生成产物**全部重建:`ui-islands`(ui.js/ui.css)、`mobile`(mobile.js)、
i18n(96 文件;`scan.mjs` 补骨架 → `build.mjs`)
验证:
- `cargo test -p agent2api-server` **964 passed / 0 failed**
- `cargo check`(server + desktop crate)0 error
- `tsc --noEmit` 0 error;`vite build` 通过
- `node scan.mjs --check` 通过(无缺失键)
上游公告 QClaw 2026-12-24 停服,本站不再发放积分 —— 分组标题与说明 按实情改写(还叫「自动发放」会让人以为每天仍有到账)。逐行说明与 「不参与每日签到」折叠文案同步。⚠️ 后端 `auto_grant_note` 仍写「积分随登录自动发放」,与前端口径不一致, 待决定是否一并改(该文案也用于账号页的余额说明,改动面涉及两处)。
「两边各自追加 provider」时同一段 match 臂被拼接了两遍,第二个永远不可达: - `allowed_hosts`:整条 provider 列表重复(含 AtomCode / LobsterAI 两段注释) - 窗口标题的 `matches!`:`qoder-intl | antigravity | officeace` 重复 合并为单臂(qwenwork / atomcode / lobsterai 一并列入), 并在注释里留一句「新增 provider 追加到本臂,别重复列出同一 id」。 `cargo check` 现在 unreachable pattern 归零。
上一版只改了前端分组标题,后端 `auto_grant_note` 仍写「积分随登录自动发放」,
账号页与签到中心的说法互相矛盾。本次按已核实的官方公告统一:
- `auto_grant_note("qclaw")` → 「已停止发放积分(2026-12-24 停服)。余额在原
有效期内仍可用;去 QClaw 官网「迁移到 WorkBuddy」完成迁移可领 1000 积分补贴
(发到 WorkBuddy 账号)」
- `out_of_scope_reason` 兜底 → 「已进入停运迁移期,本站不再发放积分」
- `AUTO_GRANT_PROVIDERS` 文档:语义从「随登录自动发放」改为「本站不再发放、
只剩只读余额」,判据三条保留不动(只改文案与语义,不改判据)
- `api::checkin_center` 模块头补 ⚠️ 2026-10-20 语义更正段,写明五点官方事实
(停服日 / 迁移补贴条件 / 已有积分有效期不调整 / 可申请退款 / 停新注册续费)
- `qclaw/mod.rs`、`qclaw/points.rs`、`auto_checkin.rs`、`checkin-state.ts`
的注释一并对齐
依据:2026-09-24 官方公告,2026-12-24 00:00 停服;旁证是同类网关已主动下架
QClaw 通道。用户此前明确指出「自动发放」的说法在停运期会误导人。
验证:`cargo test -p agent2api-server` 964 passed / 0 failed;前端重建通过。
原先设置页的分类只能进设置页后在左栏切,侧栏虽有一个本地脚本挂的子导航,
但它的分类表已过时(10 项 vs 页面 13 项)。本次删掉那套子导航,改为把设置页
的 13 个分类**平铺成一级菜单**,全部落在「系统」组下。
## 实现要点
**一个页面 + 13 个导航项**(不是造 13 个页面):分类项的 `data-page` 一律是
`settings`,`data-cat` 才是分类。这样 `PAGES` / `currentPage` / 设置页自持加载
(`wbSettingsPanel.load()`)全都不用动 —— 它们的语义本来就是「页」。
导航项因此有三个属性各司其职:`data-nav`(导航身份,选中态按它比对,否则
13 项会一起亮)、`data-page`(切到哪页)、`data-cat`(落到哪一栏)。
**分类状态的唯一投影仍是设置页左栏**:状态住在 React 岛里(settings-state.ts
只在模块内 publish,不派发 DOM 事件),宿主读不到。新增 `sidebar-settings-sync.js`
观察 `.settings-nav-item.active` 的 data-cat,投影到 `<html data-settings-cat>`,
供 app.js 的 `markActiveNav` / 面包屑读用。这样侧栏点击、页内切分类、深链
(update-panel 的 showCategory('about'))三条路都能同步。
**删除过时的子导航**:`sidebar-settings-nav.js` + `css/sidebar-subnav.css` 及
其 link/script 引用。它的 10 项分类表与页面已不一致,且与新的一级项重复。
**老存盘原地迁移**:老布局里那条合并的 `settings` 展开成 13 项,**原地展开**
而不是「丢掉 + 靠 missing 补位」—— 补位会把它们全塞进第一组(概览),
用户的「系统」组当场空掉,等于丢掉他调过的分组安排。
## 顺带修掉两个既有缺陷
1. **`显示` 分类名改「界面」**:中文即键的词典里「显示」已被 Key 列表 /
表格列设置的「显示/隐藏」占用(英文译作 Shown),做分类名会在英文界面
显示成 "Shown" 而不是 "Display"。这一栏装的是主题 / 缩放 / 单位 / 侧边栏
菜单,叫「界面」也更贴切。设置页 CATEGORIES 与侧栏表同步改(id 仍是 display)。
2. **`scan.mjs` 补 `settings-model.ts`**:分类表 CATEGORIES 在这个文件里,
而它原先不在扫描清单中 —— 症状是部分分类名没进词典(同一串文字恰好也出现
在别处的能被动扫到,「显示」「重试」这类只此一处的就永远缺)。
同时补上 QClaw 停运文案的英文译文(上一轮改中文时漏跑 scan)。
## 验证
- `node --check` 三个脚本通过
- 侧栏 13 个 cat 与设置页 CATEGORIES **逐字一致**(含顺序与标签名)
- 老存盘迁移单测:13 项落地到用户自己的「系统」组,组隐藏状态与 hidden 列表保留
- `cargo test -p agent2api-server` 964 passed / 0 failed
- i18n `scan --check` 通过;`build` 产物 96 文件已重建
- 桌面产物 `ui.js` 已随 settings-model.ts 重建
⚠️ 已知代价:侧栏一级项 13 → 25 项。窄屏(≤900px)导航收成纯图标后可滚动,
但项目较密;「设置」作为独立入口不复存在,设置页只能从这 13 项进。
`chat_plan_moves_model_into_header` 在**装了 QClaw 的机器上必然失败**,
在 CI / 干净机器上却通过 —— 它测的不是代码,是本机状态。
── 根因 ─────────────────────────────────────────────────
`gateway_for_session` 优先读**本机真实文件** `~/.qclaw/openclaw.json`,
只有读不到才退回账号快照。而 `gateway.port` 由 QClaw **每次启动随机分配**
(实测 10959),测试断言的却是快照里的 9434:
left: "http://127.0.0.1:10959/v1/chat/completions"
right: "http://127.0.0.1:9434/v1/chat/completions"
原作者已意识到「令牌读自本机文件」所以刻意不断言令牌值,
**但漏了端口读自同一个文件**。
── 修法:加一层仅测试期生效的接缝 ────────────────────────
`local.rs` 加 `#[cfg(test)] thread_local! HOME_OVERRIDE` +
`with_home_override(home, || …)`,生产路径**零改动**。测试把 home 指到临时
目录后,两条分支都可**确定性地**走到:传空目录 → 快照(端口 9434,
令牌也能确定断言了);写入 openclaw.json → 真文件(端口/令牌以文件为准)。
- 用 `thread_local!` 而非 `std::env::set_var`:后者是**进程全局**状态,
与并行跑的其它测试互扰,且 Rust 2024 起是 unsafe。用
`catch_unwind` 包住测试体,确保断言 panic 时清理仍会执行。
- 临时目录走仓库既有惯例 `std::env::temp_dir().join(format!("…-{pid}"))`
+ 显式清理 —— `tempfile` 不在 dev-dependencies,不为两个用例引依赖。
── 补反向用例(只测一半会漏掉真问题)────────────────────
新增 `chat_plan_prefers_the_local_config_file_over_the_snapshot`:断言
「现场读盘优先于快照」这个优先级。否则把顺序写反了,原用例照样全绿。
── 顺带 ─────────────────────────────────────────────────
`local.rs` 的 `qclaw_home()` 加测试期覆盖分支;模块头与各测试点明
「为什么必须隔离」——这是一类**容易再犯**的错(任何读本机文件 / 环境变量 /
端口的生产函数都有同样风险),把理由留在代码里比留在我脑子里有用。
验证:`cargo test -p agent2api-server --lib -j 1` → 965 passed / 0 failed
(原 963 passed / 1 failed,含新增用例)。
设置页左栏那 13 个分类(通用 / 界面 / 网关 / …)现在平铺成了主侧栏的
一级菜单,页内再留一列同样的导航等于同一件事说两遍,还白占 140px 内容宽度。
整列不再显示。
── 只 display:none,不从 DOM 删节点(关键)────────────────
`sidebar-settings-sync.js` 靠 `.settings-nav-item.active` 的 `data-cat`
读「当前停在哪个分类」,再投影到 `<html data-settings-cat>`;而 app.js 的
`markActiveNav` / `crumbText` / `syncUpdateBadge` 全读那个属性。
**删掉节点会让侧栏高亮、面包屑、更新角标一起失灵**。
`display:none` 不影响 MutationObserver 的 class 变化回调(元素仍在 DOM 里)。
改 React 侧(`settings-page.tsx` 不渲染 `<nav>`)也能达到同样效果,但那是
上游文件,还要连带重建 1.4MB 的 `ui/islands/ui.js` —— 下游 fork 每次同步
上游都得多解一次冲突。一行 CSS 更划算。上游若哪天自己撤掉这列,本段可整段删除。
── 窄屏兜底 ──────────────────────────────────────────────
`mobile.css` 在 ≤900px 用 `.settings-layout.is-detail` 把「分类列表 / 分类
面板」分成两级,其中 `:not(.is-detail) .settings-panes { display: none }`
会把内容区藏起来。导航列撤掉后「列表层」不复存在,内容区必须常显,否则进
设置页是一片空白。mobile.css 在 index.html 里排在 page-settings.css 之后,
同特异性下后者会输,所以叠一层类名把特异性提到 (0,4,0) 压过它的 (0,3,0):
.settings-layout.settings-layout:not(.is-detail) > .settings-panes { display: block; }
── 顺带修一处合并残留的缩进 ───────────────────────────────
`index.html` 里 `page-assistant.css` 那行行首是 4 空格,而同文件其余 28 个
`<link>` 都是 2 空格。只是缩进,无功能影响。
新增三家提供商(README 支持矩阵 + 全部登记点) - Coze 扣子:网页登录(壳侧读 WebView2 整包 Cookie)/ 粘贴 Cookie / 文本导入, PAT 按需铸造、v3/chat 会话式转发、机器人目录、积分余额与两条领奖链 - 马维斯 Marvis:粘贴三件套(mv_ 令牌 + openid + guid)、OpenAI 兼容转发、 余额读本机客户端日志的 token.wallet;上游无签到,签到中心如实说明 - MiniMax Code 国内版 / 国际版:设备码(RFC 8628)+ PKCE 登录、Anthropic 转发、 积分余额与七天签到;两地按 provider 拆家(目录缓存分格) 同时修复 1. MiniMax 同一账号「先粘贴不透明令牌、后网页登录」会产生两条记录,且旧那条的 刷新令牌已被轮换掉、永久报登录过期。认领逻辑新增第 4 层:仅当本次拿到 userId、 写入路径会轮换旧 RT(web/desktop)、且本地区只有一条无 userId 的记录时认领; 手工粘贴永不顶掉别人的记录,候选不唯一时不猜 2. Marvis 的到期时刻没有落盘:expiresAt / expireTime 双认并写入,重贴同一令牌时 沿用旧值(否则有效期列会从真实读数退回「长期有效」) 3. Coze 的余额 / 签到 / 模型目录绕过账号代理直连,与转发链出口不一致:四处改为 透传 session.proxy,并删掉不带代理的 first_claw_id 包装 4. MiniMax 取消登录后设备码仍然有效:本地划掉该码(含上游 expired_token / access_denied 两条判死路径),后台轮询下一拍即退出,不再每 3 秒空打上游; 顺带修掉「done 被当竞态」导致正常保存被吞的落盘判定 其他 - 无刷新令牌的 MiniMax 账号不再被排进续期调度(清掉 RT 之后维护任务原本每分钟 空跑一次必然 400 的续期) - .gitignore 补 _recon/(181MB、含真实凭证的逆向取证工作区)与 JVM 崩溃转储 - docs 新增 QClaw 网页登录逆向报告(已脱敏) 验证:两 crate cargo check 干净;cargo test -p agent2api-server -j 1 1112 passed / 0 failed; tsc --noEmit 无错;i18n scan --check 通过
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
把设置页的 10 个分类做成主侧栏「设置」底下的快捷入口,在任意页面都能
一步跳到对应设置分类;设置页内部那份分类导航保留(窄屏时它转横排,仍是
主入口),两侧选中态双向同步。
子项类名刻意用 .nav-sub-item 而非 .nav-item:app.js 的 showPage 会遍历所有 .nav-item 按 data-page 重置 active,复用会清掉分类选中态(上游
page-settings.css 注释已注明同一坑)。折叠箭头用 并 stopPropagation —— nav-item 自身是 ,button 不可嵌套,且不拦截会顺带 触发 #nav 的点击委托切页。
验证:在真实 Tauri 应用(WebView2 + CDP)内跑通 8 项交互(渲染/图标/展开
折叠/偏好写入/首次自动展开/跨页跳转切分类/双向同步/深链同步)。