Sub2API 账号整理
把“列表看起来连续”当作显示问题处理。优先利用账号页现有的 ORDER BY name ASC, id ASC,不要为显示顺序修改主键或调度优先级。
不变量
- 只把
accounts.name作为显示排序键;允许数据库自动更新updated_at,并把scheduler_outbox(account_changed)作为唯一关联表写入,用于刷新名称缓存。 - 不修改
accounts.id、priority、schedulable、status、credentials、extra、代理、分组、用量、日志、序列或其他业务关联表。 - 不删除或恢复账号,不处理软删除账号,不刷新 token,不测试上游。
- 不递归替换 JSON 中的
account_id,不清 Redis,不停服务。 - 把
credentials.chatgpt_account_id、extra.crs_account_id视为外部标识,绝不作为本地主键修改。 - 如果用户坚持在 ID 升序视图中连续排列,说明该需求需要新的纯展示字段或前端排序功能;本技能不得退化为主键换号。
选择策略
默认执行“URL 名称前缀”方案:
- 从管理员 API 获取全部未删除账号的脱敏响应;
credentials.base_url会保留。脚本若发现敏感 token 或私钥键意外返回,会在落盘前中止。 - 计算有效路由类别:Spark 影子继承母账号;Anthropic OAuth/SetupToken 优先启用的
extra.custom_base_url;已知会忽略存储 URL 的 OAuth 类型按运行时默认端点归类;其余优先credentials.base_url,没有 URL 时按platform/type/default归类。 - 为同一类别生成可排序前缀。Grok 固定使用 5 字符
zzzz-,所有未删除 Grok 账号共用此前缀并排在普通名称之后;该规则优先于 URL/标记前缀,但--exclude-platform grok仍可明确跳过。其他平台默认使用[@url:api.example.com:4f21ab93c2];用户指定名称标记顺序且 URL 组不超过 36 个时使用紧凑前缀!<URL组base36><组内标记base36>-,例如!00-原名。类别按 scheme/host/port/path 计算并忽略 query、userinfo、fragment;前缀不含原始 URL。 - 保留原名称作为
base_name;超过 100 字符时只截断写回名称,完整原名仍保存在权限为0600的计划中。 - 用名称升序查看账号;同一前缀的账号会连续出现。
可选策略优先读取技能目录下的 local/sort-policy.json。该文件只保存当前安装实例的偏好,通用 Python 逻辑不得写死供应商、模型或 hostname;文件不存在时完全回到上述 URL/Grok 默认行为。可用 --policy PATH 显式选择其他策略,或用 --no-policy 忽略默认策略,两者不能同时传。未传二者时先找默认本地策略,不存在才使用无策略默认行为。文件存在但 JSON、schema、字段、hostname 或重复规则无效时必须停止,不得静默回退。
策略中的 model_buckets 按 credentials.model_mapping 的非空映射键匹配,并可用 platforms、account_types 限定作用域;它代表“显式映射证据”,不根据账号名称猜测,也不把运行时空映射的宽松回退推断为已明确支持。route_buckets 只按有效路由的精确 hostname 匹配。排序键依次是:平台固定尾部规则、模型能力桶、路由桶、原名类别、组内标记和稳定 URL 次序。模型能力优先与 URL 连续性冲突时(同一有效 URL 混有不同模型能力层级)必须停止并报告冲突,不得悄悄违反其中任一要求。计划同时绑定策略 SHA256 和模型映射指纹。
用户明确给出账号 ID 与类别时,可用 overrides 覆盖 URL 自动分组。不要根据邮箱域名、名称相似、创建时间或“看起来像同类”自行合并。
工作流
1. 只读发现
先完整阅读 references/runtime-runbook.zh-CN.md。确认部署环境、管理 API 地址和账号范围。生产、预发布或身份不明环境均按生产处理。
使用管理员 API而不是账号导出接口抓取列表;账号导出会包含 token,不适合本任务。调用:
python3 scripts/fetch_redacted_accounts.py \
--output /root/backups/sub2api/account-organizer/<run-id>/accounts.before.json
脚本从 SUB2API_BASE_URL 和 SUB2API_ADMIN_API_KEY 或 SUB2API_JWT 读取认证,不打印认证值,并自动分页。
2. 生成预览计划
python3 scripts/plan_account_names.py plan \
--input /root/backups/sub2api/account-organizer/<run-id>/accounts.before.json \
--output /root/backups/sub2api/account-organizer/<run-id>/plan.json
上例会自动读取存在的 local/sort-policy.json。需要验证纯通用缺省行为时加 --no-policy;使用一次性或其他实例策略时加 --policy /absolute/path/policy.json。
如有人工类别,使用 --overrides overrides.json。格式为账号 ID 到类别标签的对象,例如 {"2011":"sharedchat","2048":"sharedchat"}。
如需排除平台并指定名称标记顺序,按顺序重复参数:
python3 scripts/plan_account_names.py plan \
--input accounts.before.json \
--output plan.json \
--exclude-platform grok \
--name-bucket 'any-' \
--name-bucket claude \
--order-marker 6945 \
--order-marker 1223 \
--order-marker 2548
如果 CLI 显式提供任意 --exclude-platform、--name-bucket 或 --order-marker,该字段整体覆盖策略文件中的同名字段;未显式提供的字段继续使用策略。URL 组取组内最靠前的标记作为组顺序;同一 URL 内的账号再按各自最靠前的标记排序。一个名称命中多个标记时取用户列表中最靠前者。排除平台的账号不改名,也不进入 SQL。
--name-bucket 是标记之上的原名类别顺序。例如先传 any-、再传 claude,则包含 any- 的 URL 组优先;同一 URL 内先排完 Any,再排 Claude,两个类别内部各自按 --order-marker 顺序。类别和标记会合并编码到同一个 base36 字符中,排序码保持三字符,随后用一个 - 与原名分隔。
标记只能从剥离本技能旧前缀后的原始 base_name 中匹配,采用 Unicode 规范化后的不区分大小写子串比较。绝不把用户给出的标记补进原本不包含它的名称;未命中标记的账号只获得三字符排序码和一个分隔符 -,并排在其 URL 组内已命中账号之后。若一个 URL 组包含多个标记,必须保持 URL 组完整:组按最靠前标记定位,组内再按标记顺序排列。
规划器会同时剥离旧的 [@url:...] 、旧紧凑前缀和 Grok 的 zzzz-,再从 base_name 生成目标名称;重复规划不得叠加 zzzz-zzzz-。非 Grok 原名自然以 zzzz- 开头时必须保留,不得误剥离。
向用户报告:环境、策略来源及 SHA256、账号总数、模型能力命中数、变更数、类别数、每类的安全标签和 ID 列表、唯一会变化的数据库列、回滚文件路径。不要输出完整 URL、query、凭据、token 或整份账号响应。
3. 生产写入确认
默认停在预览。只有用户在看过目标 ID、影响范围和回滚方式后,仍明确要求应用,才允许生产写入。既往“帮我整理一下”不能替代这一步的生产 DML 确认。
写入前重新抓取账号并运行:
python3 scripts/plan_account_names.py verify \
--plan /root/backups/sub2api/account-organizer/<run-id>/plan.json \
--accounts /root/backups/sub2api/account-organizer/<run-id>/accounts.before-latest.json \
--expect before
任何账号增删、名称变化、有效路由类别变化、模型映射变化、平台/类型/调度保护指纹变化都必须中止并重新生成计划。
4. 生成和执行受控 SQL
python3 scripts/render_name_update_sql.py \
--plan /root/backups/sub2api/account-organizer/<run-id>/plan.json \
--direction apply \
--output /root/backups/sub2api/account-organizer/<run-id>/apply.sql
生成器只产生固定结构 SQL:事务级 advisory lock、旧名称乐观条件、活动账号检查、精确更新 name/updated_at、逐 ID 写入 scheduler_outbox。它不会连接数据库。按运行手册发现真实 PostgreSQL 容器、用户、数据库和 schema 后再执行;不得假定容器名。
5. 验证
重新通过管理 API抓取列表,运行 verify --expect after。再确认:
- 所有目标账号仍可见,ID、platform、type、priority、status、schedulable、group_ids、proxy_id 和 parent_account_id 的保护指纹未变。
- 按
name asc获取列表时,每个 URL 类别连续。 - 所有未排除 Grok 账号仅有一个
zzzz-前缀,且没有非 Grok 活跃账号排在该前缀之后。 scheduler_outbox已存在相应account_changed事件或已被消费;调度快照中的名称最终刷新。- 健康检查正常。不要为验收发送真实上游请求。
6. 回滚
使用同一计划生成 --direction rollback SQL。回滚也会检查当前名称必须等于计划中的新名称;用户后来手动改名的账号会使整个事务中止,避免覆盖人工修改。回滚后重新抓取并执行 verify --expect before。
停止条件
遇到以下任一情况立即停止:
- 用户真实目标是修复本地主键、跨系统 ID 对齐或消除主键冲突,而不是列表显示。
- 账号页被持久化为
id排序且用户拒绝改用name asc。 - 运行库不是 PostgreSQL、缺少
scheduler_outbox、schema 不明,或账号表结构与运行手册前提不符。 - 管理 API 响应包含未脱敏敏感字段,或 URL 只能通过完整凭据导出获得。
- 计划涉及软删除账号、名称超过约束且无法安全截断、未知父账号,或相同 ID 重复出现。
- 生产写入未得到明确确认。
用户可直接这样说
- “用
$sub2api-account-organizer预览把所有相同 URL 的账号放在一起。” - “用
$sub2api-account-organizer把所有 Grok 账号统一为短前缀并排到最后。” - “把 2011、2048、2053 视为同一类,先给我看整理方案,不要应用。”
- “应用刚才确认的账号整理计划。”
- “撤销上一次 Sub2API 账号名称整理。”