Skip to content

bridge: 账号列表支持删除/清理重复条目,复制 server config 可选 host:port - #4

Open
std-external wants to merge 1 commit into
std-microblock:mainfrom
std-external:feat/account-cleanup
Open

std-external wants to merge 1 commit into
std-microblock:mainfrom
std-external:feat/account-cleanup

Conversation

@std-external

@std-external std-external commented Oct 9, 2026 •

Copy link
Copy Markdown

需求与来源

两个需求都在 /platform-accounts 这一页,报告来源是 QQ 群「Koishi On Topic」的群管理员:

  1. 他在测 bridge: 复制 server config 时输出单行 JSON(Android 端粘贴报「非法 JSON」) #3 的 server config 复制时,把同一个平台插件条目配了几次,账号列表里就多了几条重复账号,页面上没有任何办法清掉:
    「我现在列表里创建了几个重复的,是不是加一个删除功能好一些」
  2. 复制 server config 时用的是某一个 endpoint,想给别的网络里的客户端复制备用地址(altEndpoints)只能复制出来再手改:
    「之前有做一个 alt endpoint 的功能,可以在复制配置左边加一个下拉菜单 控制复制出哪个 host:port 不然需要复制出来再手改」

本 PR 单独基于 main(此前 stack 在 #3 上,已经 rebase 掉,#3 的提交不在历史里,也不依赖它的代码)。
复制配置的文本格式沿用 main(多行 pretty JSON),#3 的单行改动没有带进来 —— 理由见文末「#3 怎么办」。

一、删除账号与清理重复条目

「账号」就是一条 Cordis platform entry

docs/IM_PLATFORM.zh.md:同一个 Cordis platform entry 对应一个账号和一个虚拟手机号。列表页的 platformId 就是 entry 的配置
id(resolvePlatformPluginId())。所以只清数据库是治不好的:entry 还在的话,下次 provision 又把账号建回来。
删除必须把那条 config entry 一起删掉。

删除的语义

deleteAccounts(platformIds):

  1. 先校验全部目标:条目不存在、或者不是配置文件(loader)管理的条目,直接报错,一个都不删(避免删到一半)
  2. 停该平台的订阅、注销 runtime session
  3. 删掉这个账号的登录面:mtproto_platform_session、mtproto_auth_session(虚拟号 + TOTP secret + 两步验证密码)、
    mtproto_auth_binding、mtproto_client_authorization、mtproto_authorization_settings
  4. 吊销对应的 auth key:已经用这个账号登录的客户端会被登出
  5. 最后从所属的那棵 loader 树里删掉 entry,app.yml 同步落盘,重启也不会回来

不删消息/会话历史:它们按 platform session 隔离,条目删掉后不再可达;连带删历史既不是这次的需求,风险也大。
如果以后用同一个 entry id 再配回来,mtproto_im_user 身份行还在,旧消息不会变成孤儿。

每次删除都会弹二次确认,内容来自后端 describeAccountRemoval():

  • 列出要删的条目(名字 + platformId + kind)
  • 有客户端在线时明确写「N 个 Telegram 客户端会被登出」
  • 条目不由配置文件管理时给出提示并禁用确认按钮

单条删除(卡片上的 Delete account)和多选删除(勾选后 Delete selected)走同一个确认弹窗和同一个 RPC。

重复判定口径

findDuplicateAccounts() 只报能证明的重复,两类:

口径 判据 适用
identity 两个条目解析到同一个平台用户(platformKind 和 userId 都相同) 非 QQ 平台(虚拟号随机,重复条目能正常 provision)
virtual-phone 条目 provision 失败的原因是它的虚拟号已被另一个条目占用 QQ(虚拟号由 QQ 号决定 888{qq},占错本身就证明是同一个账号)
  • 保留哪条:客户端已经登录的那条(client authorization 数最多),平手按 platformId 稳定排序取第一条。
  • 原来虚拟号冲突抛的是匿名 Error,现在换成 VirtualPhoneClaimedError 并把占用者的 platformId 带出来,
    所以这类重复是「由数据本身证明」的,不是猜的。
  • 页面上 Select duplicates 只做选中,用户看到列表确认后才删;没有「一键直接删」的路径。

为什么不做全自动去重:删除会登出客户端、删掉虚拟号和两步验证密码,而「同名」或「同 server config」都不足以判定重复
(同一份 config 合法地跑多个不同账号是正常的)。所以最终一步必须落到人看得到的确认列表上。

一个真实 loader 的坑(已修)

app.yml 在生产里是经 @cordisjs/plugin-cli-cordis → @cordisjs/plugin-include 加载的,条目在 loader 里的 key 是
<config-tree>:<configured-id>(例如 7ffc5890:qqnt),而 bridge 里的 platformId 是配置里的 id(qqnt):

  • 按 entry.id 匹配会找不到条目 → 改成同时接受 entry.options.id;
  • ctx.loader.remove(<qualified>) 在嵌套树里会静默不删(EntryGroup.remove 用的是所属子树的 store)。
    现在改为通过条目所属的树删 options.id,那也正是负责写回 app.yml 的那棵树。

这两点是靠一个直接跑真实 Loader + Include 的测试守住的(删完 app.yml 里确实没有这条了),只 mock loader 是测不出来的。

二、复制 server config 时可选 host:port

  • dashboard 新增 serverEndpoints:主地址(serverHost:serverPort)在前,altEndpoints 按配置顺序解析后跟在后面,去重。
  • Copy server configuration 左侧出现 Endpoint 下拉:只有一个地址时不显示(行为与之前完全一致),默认选中主地址。
  • 下拉只改写文本里的 host / port 和 dcs[].ip/port,不改别的字段;页面 <pre> 展示的是当前选中地址的版本(所见即所复制)。
  • 页面展示和剪贴板是同一份文档(沿用 main 的 JSON.stringify(config, null, 2)),只把 host/port 与
    dcs[].ip/port 换成选中的地址,其他字段逐字不变。

改动

文件 内容
packages/bridge/src/account-removal.ts(新) 条目查找/删除(按所属树)、账号登录面清理、客户端数统计
packages/bridge/src/account-duplicates.ts(新) 纯函数:按身份或虚拟号占用分组重复条目,决定保留哪条
packages/bridge/src/platform-account.ts 虚拟号被占用改为 VirtualPhoneClaimedError(带占用者 id)
packages/bridge/src/account-dashboard.ts 新增 makeServerEndpoints()
packages/bridge/src/dashboard-types.ts serverEndpoints、findDuplicateAccounts、describeAccountRemoval、deleteAccounts 的线上契约
packages/bridge/src/index.ts dashboard 接线:删除流程(停订阅 → 清理 → 吊销 auth key → 删 entry)、重复扫描、删除预览、endpoint 列表
packages/bridge/client/accounts.tsx 卡片复选框 + Delete account、多选工具条、重复提示条、确认弹窗、Endpoint 下拉
packages/bridge/client/bridge-model.ts formatEndpoint、withServerEndpoint、duplicateOwners/duplicatePlatformIds/describeDuplicateGroup
packages/bridge/client/style.css 上述 UI 的样式

验证

yarn build:webui
npx tsc --project tsconfig.test.json                                                     # 通过
npx vitest run packages/bridge/src/account-duplicates.test.ts \
  packages/bridge/src/account-removal.test.ts \
  packages/bridge/src/account-removal-loader.test.ts \
  packages/bridge/src/account-dashboard.test.ts                                          # 4 files passed
npx vitest run --config vitest.webui-solid.config.mts \
  packages/bridge/client/bridge-model.test.ts \
  packages/cordis-webui-solidjs/client/pages/crossgram-components.test.tsx \
  packages/cordis-webui-solidjs/src/bridge.browser.e2e.test.ts                            # 3 files passed
  • 单元:重复分组(身份/虚拟号/无关失败不算重复/不去重指向已消失条目)、账号清理(只删自己的行、计数、吊销的 key、
    遗留 session 缺失的旧数据)、真实 loader 上的条目查找与删除(含 app.yml 写回)、endpoint 文档改写(只改 host/port 与 dcs)。
  • e2e(390×844 手机视口,真实 Chromium):Endpoint 默认是主地址、切到 backup.test:8443 后剪贴板里
    host/port/dcs 都跟着变(其余字段不变)、无 Clipboard API 的回退路径复制出同样内容;重复提示条与卡片上的 Duplicate of;
    打开 offline 的删除确认会提示「不由配置文件管理」且确认按钮禁用;打开 qq-main 的删除确认会提示
    「2 个客户端会被登出」;Select duplicates → 确认 → 该条目从列表里消失、删除请求只带这一个 id。

回归:把 withServerEndpoint() 从复制路径去掉,e2e 里备用地址的断言立刻失败;重复清理那条如果不做确认,也会被
「确认按钮禁用」的断言拦住。

全量:npx vitest run → 1741 passed / 1 failed(voice-worker-client.test.ts 需要 cargo,本机没装,spawn cargo ENOENT,
与本次改动无关);npx vitest run --config vitest.webui-solid.config.mts → 85 passed / 1 failed
(loader.browser.e2e.test.ts 的 loader 页横向溢出,未改动的 main 上同样失败)。

截图

以下是 e2e 跑出来的真实页面(截图放在 fork 的分支上,避免往仓库里塞二进制):

复制配置前(主地址,默认):

endpoint main

切到备用地址后(文本里的 host/port 与 dcs 已经跟着变):

endpoint backup

(Chromium 没法截原生 <select> 展开后的弹层,所以这里是「选中前 / 选中后」两张,选中后顺带展开配置文件对照。)

重复条目提示条:

duplicates banner

重复条目卡片:

duplicate card

删除确认:

delete confirm

#3 怎么办:建议关掉

边界与未纳入

  • 删除入口只做在 WebUI 的账号页(bridgeManagement 那层 seam 没动),管理 Bot 里没有加删除按钮。
  • 非 QQ 平台的重复条目(虚拟号随机)只能在 identity 口径下被发现;adapter 报错(离线、unsupported)而拿不到
    平台用户 id 的条目不算重复,需要手动多选删除。
  • 消息、会话、mtproto_im_user 等历史数据不删(见上);如果以后要「连历史一起删」,建议单开一个 PR。
  • 删除会登出该账号下所有 Telegram 客户端,这是有意的;确认框里每次都写明数量。

账号列表管理:
- 卡片新增复选框与 Delete account,多选后可 Delete selected。删除都经过确认弹窗:
  列出具体条目、写明会有几个已登录的 Telegram 客户端被登出;条目不由配置文件管理时
  直接禁用确认按钮
- findDuplicateAccounts() 只报能证明的重复:adapter 解析到同一个平台用户
  (platformKind + userId),或该条目因虚拟手机号被另一条目占用而 provision 失败
  (VirtualPhoneClaimedError)。保留客户端已登录的那条,平手按 platformId 稳定排序。
  页面的 Select duplicates 只做选中,看到确认列表后才删
- deleteAccounts() 先校验全部目标,再停订阅、清理平台会话/虚拟号/TOTP secret/两步
  验证密码/binding/客户端授权、吊销对应 auth key,最后通过所属的 loader 树删掉 config
  entry(app.yml 会写回,重启后不会再被 provision)。消息与会话历史保留
- 新增 account-removal.ts / account-duplicates.ts 及单元测试;另有直接跑真实
  Loader + Include 的测试,覆盖嵌套树的 entry key 与删除写回

复制 server config:
- dashboard 新增 serverEndpoints:主地址在前,altEndpoints 按配置顺序解析后跟在后面并去重
- Copy 按钮左侧新增 Endpoint 下拉(只有一个地址时不显示,默认主地址),只改写 host/port
  与 dcs[].ip/port;页面展示与剪贴板仍是同一份文档,格式与 main 一致
@std-external

Copy link
Copy Markdown
Author

已重新基于 main 推送(force-push):历史里不再有 #3 的提交,也不依赖它的代码,可以单独合并。

#3 的建议写在描述末尾:这一页的改动已经完全独立,建议关掉它;若要保留单行剪贴板,单独合掉即可(同一处需要一次很小的 rebase)。

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