Skip to content

修复插件家的凭证自动续期、备注名、有效期、刷新入口与出网代理显示 - #338

Open
coulsontl wants to merge 2 commits into
aimod-cc:betafrom
coulsontl:fix/plugin-capability-projection
Open

coulsontl wants to merge 2 commits into
aimod-cc:betafrom
coulsontl:fix/plugin-capability-projection

Conversation

@coulsontl

@coulsontl coulsontl commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

背景

插件架构迁移后,协议层已经定义并声明了一批账号相关能力,但网关的公开视图层没有消费它们。结果是一串「插件能力被平台丢掉」的问题 —— 全部影响插件家(account.server 的 to_plugin_public_account 路径),不区分具体是哪一家。

本 PR 修五处,都在这条链路上。

一、凭证不再自动续期

credential_maintenance.rs 的 refresh_expiring_accounts 调的是同步版 credentials_expiring:

if !adapter.credentials_expiring(store, &id) {

而插件适配器(RpcAdapter)的同步版是硬编码 false,它自己的注释写着「真正的实现在 credentials_expiring_async」—— 那个版本会发 token.expiring RPC 去问插件。

后果:所有插件家在这一步被判成「凭证未过期或临期」而永远跳过。凭证过期后每轮维护都报「无需刷新」,接口一路 401/过期错误,而真正能续期的 RPC 一次都没被问过。

修复:改调异步版。内置家的异步版默认转调同步版(trait 的默认实现),所以对它们无影响。

二、备注名被插件覆盖

备注名走 PATCH /api/accounts/{id} → apply_patch,只写记录顶层的 name + nameCustom。而 12 家插件都会往 pluginPublic.name 里写自己按口径算的展示名(邮箱、app key 前缀……),合并时把备注名盖掉了 —— 用户改完备注名,列表里纹丝不动。

修复:给「插件赢」加一个例外 —— nameCustom 为真时用回顶层 name。

判据用 nameCustom 而不是「顶层 name 非空」:插件在添加账号时写的种子名也落在顶层,拿它当用户意图会把默认名误判成备注名。

三、有效期列永远是空的

协议里 AccountFeatures::expiry 就是为这一列准备的,注释甚至写明了注意事项:

/// ⚠️ 各家的键名真的不同(OrcaTerm 就是 `tokenExpireAt`,少一个 s),
/// 写错不会报错,只会让有效期列一直显示「—」。
pub expiry: Vec<String>,

插件在握手时申报「我用哪个键表示到期」,但网关从来没读过这个申报 —— 全仓搜 account_features,只有 usage_records.rs 用了其中一个字段(default_limiters),expiry 一次都没被用过。

修复:按申报的键名从 secret 取值放上公开视图。值放在申报的原键名下(不是统一改名 expiresAt),因为前端那张表就是按各家自己的键名取的。

四、手动「刷新 Token」入口消失

公开视图的 hasRefreshToken 原先恒写 false:

public.insert("hasRefreshToken".to_string(), Value::Bool(false));

界面据此不渲染刷新项(hasRefreshToken ? <刷新项> : …),于是插件家一律没有刷新按钮 —— 哪怕插件申报了 refresh: true 并且实现了 token.refresh,用户在凭证过期时只剩「重新登录」一条路。

修复:新增 plugin_host::refresh_capable(provider) 读握手带回的 ProviderInfo.capabilities.refresh。

与既有的 account_features_of 同一形态 —— account_store 层拿不到适配器,而插件家的 id 也不在 ProviderKind 枚举里,所以只能在 plugin_host 开一个读数入口。

五、出网代理列恒显示「直连」

proxy 是跨家通用的账号属性(apply_patch 统一写入 record.proxy),但公开形态只在各家的 to_*_public_account 里各自投影一份 —— 插件家走 to_plugin_public_account,那个骨架里没有这个键。

这一处比前几处更隐蔽:值是对的,只是读不到。

1. 界面点代理下拉 → apply_patch 正确写入 → changes: ['代理 → 通用']
2. 库里确实存着 {source:"pool",proxyId:"…"}
3. 公开形态缺 proxy → 前端读到 undefined → 整列显示「直连」
4. 再选同一条 → apply_patch 判「值没变」→ changes: []
   → 前端仍弹「✅ 代理已更新」(假成功)

于是用户反复点都改不动,而后端日志里连一条「账号已更新」都没有 —— 排查时会一路误判成「请求没发出去」或「前端 bug」。

修法与 maxConcurrent 完全一致:放进 public_account 的跨家统一注入块。那里本来就承载 maxConcurrent / limiters / usageQuery / lowBalance 这些「所有家都有」的账号属性,注释里写明的原因正是这个坑(逐字段白名单的家漏了 maxConcurrent,缺键被选路当成 0 = 不限,上限静默失效)。

恒为「对象或 null」(null = 直连,与写入侧 normalize_account_proxy 的 None → Value::Null 同口径)。

验证

实测环境:3.3.0-beta.3 + 插件包 0.2.2-beta.2,宿主机 x86_64 Linux。

以 OfficeAce(申报 refresh: true + expiry: ["expiresAt"])与 CodeArts(同)为样本:

项目 修复前 修复后
hasRefreshToken false(无刷新入口) true
expiresAt 缺失(列显示「—」) 1792895925000 → 2026-10-25 10:38
备注名 被插件展示名覆盖 显示用户设置的名字
维护日志 「无需刷新的账号(共 62 个)」 按实际临期情况判定并续期
proxy 缺失(列显示「直连」) 14 家 provider 全部返回;已设置的池引用正确显示

不影响的部分

  • 内置家:前四处改动都限定在插件路径上,或走 trait 默认实现转调原逻辑。第五处(proxy)对内置家是幂等覆盖 —— 各家自己投影的值与统一注入的值来自同一次 describe_account_proxy(record.proxy()) 调用,结果一致。
  • 未申报能力的插件(旧版本):account_features_of / refresh_capable 返回 None / false,公开视图保持原形态,界面走自己的兜底表 —— 升级是渐进的。
  • ZCode 的 hasRefreshToken: false 是正确的:它走的是存量内置记录投影(to_zcode_public_account),且上游确实没有续期协议。这个 PR 没有改动那条路径。

影响面

这五处不是某一家插件的问题 —— 任何「插件申报了能力但网关没消费」的家都会中。目前 30 家插件里有 12 家会往 pluginPublic.name 写展示名,OfficeAce / CodeArts 等多家申报了 refresh: true 与 expiry。


🤖 本条回复由 AI Agent 生成
Generated by an AI agent.

插件架构迁移时,协议层已经定义并声明了这些能力,但网关的公开视图层
没有消费它们,导致插件家出现一串「能力被平台丢掉」的问题。

一、凭证不再自动续期(credential_maintenance.rs)

`refresh_expiring_accounts` 调的是同步版 `credentials_expiring`,而
插件适配器(`RpcAdapter`)的同步版是硬编码 `false`,真正的实现在
`credentials_expiring_async` 里(会发 `token.expiring` RPC 问插件)。

后果是所有插件家在这一步被判成「凭证未过期或临期」而永远跳过:凭证
过期后每轮维护都报「无需刷新」,接口一路 401,而能续期的 RPC 一次都
没被问过。内置家的异步版默认转调同步版,所以这一改对它们无影响。

二、备注名被插件覆盖(plugin_accounts.rs)

备注名走 `PATCH /api/accounts/{id}`,只写记录顶层的 `name` 与
`nameCustom`;而 12 家插件都会往 `pluginPublic.name` 里写自己按口径
算的展示名(邮箱、app key 前缀……),合并时把备注名盖掉了 —— 用户改
完备注名在列表里纹丝不动。

给「插件赢」加一个例外:`nameCustom` 为真时用回顶层 `name`。判据用
`nameCustom` 而不是「顶层 name 非空」,因为插件在添加账号时写的种子
名也落在顶层,拿它当用户意图会把默认名误判成备注名。

三、有效期列永远是空的(plugin_accounts.rs)

协议里 `AccountFeatures::expiry` 就是为这一列准备的 —— 各家键名真的
不同(OrcaTerm 是 `tokenExpireAt`、比别家少一个 s),插件在握手时
申报「我用哪个键表示到期」。但网关从来没读过这个申报。

改成按声明的键名从 secret 取值放上公开视图。值放在申报的原键名下面
(不是统一改名 `expiresAt`),因为前端那张表就是按各家自己的键名取的。

四、手动刷新 Token 入口消失(plugin_accounts.rs + plugin_host/mod.rs)

公开视图的 `hasRefreshToken` 原先恒写 `false`,界面据此不渲染刷新项
(`hasRefreshToken ? <刷新项> : …`),于是插件家一律没有刷新按钮 ——
哪怕插件申报了 `refresh: true` 并且实现了 `token.refresh`。

新增 `plugin_host::refresh_capable(provider)` 读握手带回的
`ProviderInfo.capabilities.refresh`,与既有的 `account_features_of`
同一形态(`account_store` 层拿不到适配器,插件家的 id 也不在
`ProviderKind` 枚举里)。

验证(OfficeAce / CodeArts,均为插件家):

- `hasRefreshToken` 由 `false` 变为 `true`,刷新入口恢复;
- `expiresAt` 出现在公开视图,续期写入的新值能正确显示;
- 维护日志由「无需刷新的账号」变为按实际情况判定临期并续期。
`proxy` 是跨家通用的账号属性(`apply_patch` 统一写入 `record.proxy`),
但公开形态只在各家的 `to_*_public_account` 里各自投影一份 —— 插件家走
`to_plugin_public_account`,那个骨架里没有这个键。

后果比前几处更隐蔽:**值是对的,只是读不到**。

  1. 界面点代理下拉 → `apply_patch` 正确写入 → `changes: ['代理 → 通用']`;
  2. 库里确实存着 `{source:"pool",proxyId:"…"}`;
  3. 公开形态缺 `proxy` → 前端读到 `undefined` → 整列显示「直连」;
  4. 再选同一条 → `apply_patch` 判「值没变」→ `changes: []`
     → 前端仍弹「✅ 代理已更新」(假成功)。

于是用户反复点都改不动,而后端日志里连一条「账号已更新」都没有 ——
排查时会一路误判成「请求没发出去」或「前端 bug」。

修法与 `maxConcurrent` 完全一致:放进 `public_account` 的跨家统一注入块。
那里本来就承载 `maxConcurrent` / `limiters` / `usageQuery` / `lowBalance`
这些「所有家都有」的账号属性,注释里写明的原因正是这个坑(逐字段白名单
的家漏了 `maxConcurrent`,缺键被选路当成 0 = 不限,上限静默失效)。

恒为「对象或 null」(null = 直连,与写入侧 `normalize_account_proxy` 的
`None → Value::Null` 同口径),前端 `ProxyCell` 按 `proxy?.config?.source`
读,两种形态都能吃。

验证:修复后 14 家 provider 的公开形态都带上 `proxy`;之前设过但显示
「直连」的 Cline Free 账号恢复显示「通用」,OpenCode Zen 的轮询档正常。
@coulsontl coulsontl changed the title 修复插件家的凭证自动续期、备注名与有效期显示 修复插件家的凭证自动续期、备注名、有效期、刷新入口与出网代理显示 Oct 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant