账号、Coding Plan 与闲时计划
zcode login 怎样经 ZCode 平台中转完成浏览器授权,z.ai 与 bigmodel 两个账号区域怎样区分,令牌与 API Key 存在哪、怎样加密;个人、团队 Coding Plan、Start Plan 与隐藏的闲时计划各自用什么凭据、配哪些模型,额度又从哪里查。
智谱把 ZCode 的模型服务做成了订阅:登录 z.ai 或 bigmodel 账号,开通 Coding Plan,就能用上 GLM 系列模型,不必自己填 API Key。这一篇讲这条“账号型”路径:命令行登录怎样完成授权,登录态存在哪里,怎样在每次模型请求前变成一份凭据;各档套餐对应哪些内置模型,隐藏的闲时计划又是什么。套餐在规则集里长什么样见Provider 规则、模型目录与选项映射,凭据怎样被塞进请求见模型适配层。
代码分两处。Agent CLI 自己能完成登录:apps/zcode-cli/packages/adapters/src/auth 负责授权客户端、API Key 解析与凭据文件,bootstrap/src/auth-login*.ts 串起流程,cli/src/login-command.ts 与 tui-auth.ts 是入口。桌面端的登录、权益判定、购买与额度在 packages/services/src 的 oauth、model-provider、bigmodel、coding-plan-subscription、usage-stats 里,跨端类型在 packages/shared/src。
怎么用
| 场景 | 做法 |
|---|---|
| 用 z.ai 账号登录 | zcode login,等价于 zcode login zai |
| 用 bigmodel 账号登录 | zcode login bigmodel |
| 不自动打开浏览器 | 加 --no-browser,只打印授权地址 |
| 机器可读输出 | 加 --json,授权地址改写到 stderr,stdout 只有一段 JSON |
| 退出登录 | zcode logout |
| TUI 里配置 | /login 弹出四个选项:Z.AI、BigModel 两家的浏览器授权,以及两家的 Coding Plan API Key 手工粘贴 |
用法字符串是 zcode login [zai|bigmodel] [--no-browser],别的参数一律报错(apps/zcode-cli/packages/cli/src/login-command.ts:15)。TUI 的四个选项分别对应 /login zai-coding-plan、/login bigmodel-coding-plan、/login zai-coding-plan-api-key <key>、/login bigmodel-coding-plan-api-key <key>,粘贴 Key 的输入框会掩码显示(apps/zcode-cli/packages/cli/src/command-center/login-flow.ts:12、49)。登录成功后打印用户名、选中的模型、凭据文件与模型选择文件的路径(login-command.ts:67)。
登录流程:经平台中转的轮询授权
命令行登录既不是标准的设备码流程,也不在本机起回调端口:CLI 生成一个随机的轮询令牌,请 ZCode 平台开一个短期 flow,用户在浏览器里授权,CLI 凭同一个令牌轮询结果。
- 地址:平台 API 根取控制面地址加
/api/v1,缺省https://zcode.z.ai/api/v1(apps/zcode-cli/packages/adapters/src/auth/cli-oauth.ts:4,apps/zcode-cli/packages/bootstrap/src/auth-login.ts:366)。轮询令牌是 32 字节随机数的十六进制串,init 与 poll 都以Authorization: Bearer <令牌>发送(cli-oauth.ts:6、89、110)。 - 校验:init 的返回必须有
flow_id、https 的authorize_url、expires_at,且poll_interval_sec不小于 1;每个响应最多读 64 KB(cli-oauth.ts:138、172)。 - 时限:整个登录默认 5 分钟,并且不会超过服务端给的
expires_at(auth-login.ts:39、148)。被取消或超时的尝试,即使之后收到迟到的 ready,也不会落盘(auth-login.ts:186)。 - 结果:ready 里
token是 ZCode 平台的 JWT,平台账号令牌按家族放在zai或bigmodel字段下,含access_token与可选的refresh_token(cli-oauth.ts:214)。
轮询循环本身(apps/zcode-cli/packages/bootstrap/src/auth-login-polling.ts:23):
const expiresAtMs = input.initData.expires_at * 1_000;
const deadlineMs = Math.min(input.now() + input.timeoutMs, expiresAtMs);
const pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, input.initData.poll_interval_sec * 1_000);
while (input.now() < deadlineMs) {
throwIfAborted(input.abortSignal);
let data: CliOAuthPollData = { status: "pending" };
try {
data = await waitWithAbort(
input.oauthClient.poll(
{
flowId: input.initData.flow_id,
pollToken: input.pollToken,
},
{ signal: input.abortSignal },
),
input.abortSignal,
);
} catch (error) {
throwIfAborted(input.abortSignal);
if (
error instanceof CliOAuthError &&
!(error.httpStatus === 408 || error.httpStatus === 429 || (error.httpStatus ?? 0) >= 500)
)
throw error;
// Match App polling: transient network and server failures retry at the server interval.
}408、429、5xx 和网络错误都当作暂时失败,按原间隔继续轮询;其余平台错误立即结束登录。桌面端 OAuth 服务的 startOAuthWithPolling 用的是同一对 /api/v1/oauth/cli/init 与 poll 接口,只是浏览器回调最终经官网中转页跳回 zcode://oauth/callback(packages/services/src/oauth/oauthService.ts:609,packages/services/src/oauth/providers/configUtils.ts:34)。接口注释说后端 flow “当前仅 Z.AI 支持”(packages/services/src/oauth/oauth.ts:40),实际实现对 z.ai 和 bigmodel 都走这条路(oauthService.ts:595)。adapters/src/auth 里还有一个 bigmodel 授权码交换客户端 bigmodel-oauth.ts,非测试代码里找不到调用方;localhost-callback.ts 的本地回调服务器只给 MCP 的 OAuth 用(apps/zcode-cli/packages/adapters/src/mcp/oauth-interactive.ts:15)。
两个账号区域:z.ai 与 bigmodel
两个区域在代码里叫 Provider Family,各自一套 ID 与域名(packages/shared/src/model-provider-family.ts:26):
| 家族 | 根域名 | OAuth provider | 账号型 Provider |
|---|---|---|---|
zai(Z.ai) | z.ai | zai | account:zai-individual-coding-plan、account:zai-team-coding-plan、account:zai-start-plan |
bigmodel(BigModel) | bigmodel.cn | bigmodel | account:bigmodel-individual-coding-plan、account:bigmodel-team-coding-plan、account:bigmodel-start-plan |
区分靠几处事实:命令行登录时显式指定,缺省 zai(login-command.ts:15);凭据文件里的 oauth:active_provider 记下当前登录的是哪家(apps/zcode-cli/packages/adapters/src/auth/shared-credentials.ts:16);桌面端只展示与当前登录同一家族的套餐(model-provider-family.ts:139);拿不到身份时还可以按 Provider 地址的根域名反推(model-provider-family.ts:78)。
登录拿到的 OAuth 令牌并不直接用于模型请求。CLI 随后用它在开放平台上找一把 API Key,两家只在第一步不同(apps/zcode-cli/packages/adapters/src/auth/coding-plan-api-key.ts:81):bigmodel 直接拿 access token 调 bigmodel.cn 的业务接口;z.ai 要先在 https://api.z.ai/api/auth/z/login 把它换成业务令牌(coding-plan-api-key.ts:108)。接下来两家一样:读客户信息,挑名字含“默认机构”的组织和含“默认项目”的项目,找不到就取第一个;在该项目下找名为 zcode-api-key 的 Key,没有就新建一把;最后取出密钥,有 secretKey 时拼成 apiKey.secretKey 的形式,z.ai 缺了它就报错(coding-plan-api-key.ts:143、172、190、237)。桌面端的个人套餐走同样的逻辑,团队套餐则在团队项目下维护一把 zcode-team-api-key,类型号为 2(packages/services/src/model-provider/accountProviderApiKeyResolver.ts:123,packages/services/src/bigmodel/teamPlanApiKey.ts:4)。
令牌存在哪、怎样续期
CLI 与桌面端共用一个凭据文件:~/.zcode/v2/credentials.json,数据基目录可用 ZCODE_DATA_BASE_DIR 改(shared-credentials.ts:280,packages/services/src/credential/credentialService.ts:31)。里面有这些键,前几个是固定键名(shared-credentials.ts:15):
| 键 | 内容 |
|---|---|
oauth:active_provider | 当前登录的家族 |
zcodejwttoken | ZCode 平台 JWT |
oauth:zai:access_token、oauth:zai:user_info、oauth:zai:refresh_token | z.ai 账号令牌与用户信息 |
oauth:bigmodel:access_token、oauth:bigmodel:user_info、oauth:bigmodel:refresh_token | bigmodel 同上 |
account-provider:coding-plan:<providerId>:account:<身份>:api-key | 某个账号型 Provider 的请求 Key |
account-provider:<providerId>:identity | 这个 Provider 当前绑定的账号身份 |
后两类键名由 standaloneAccountProviderCredentialKey 与 standaloneAccountIdentityCredentialKey 生成,团队与 Start Plan 在桌面端有各自的变体(apps/zcode-cli/packages/bootstrap/src/app/standalone-account-provider-runtime.ts:82,packages/services/src/model-provider/accountProviderCredentialKey.ts:20)。CLI 的 z.ai 登录只写 access token、JWT 与用户信息,不保存 refresh token;bigmodel 登录在服务端给了时才保存(auth-login.ts:189、196)。
每个值写入前都单独加密,格式是 enc:v1: 加 AES-256-GCM 的 IV、认证标签和密文。密钥是一个口令的 SHA-256(apps/zcode-cli/packages/adapters/src/auth/credential-cipher.ts:83),口令优先取环境变量 ZCODE_CREDENTIAL_SECRET,没设时退回一个由本机事实拼出的字符串(credential-cipher.ts:87):
function resolveCredentialSecret(env: Record<string, string | undefined>): string {
const configuredSecret = env[CREDENTIAL_SECRET_ENV_KEY]?.trim();
if (configuredSecret) {
return configuredSecret;
}
let username = "unknown";
try {
username = userInfo().username;
} catch {
// Some packaged or sandboxed runtimes cannot resolve OS user info.
}
return `zcode-credential-fallback:${platform()}:${homedir()}:${username}`;
}从代码看,缺省密钥只由平台、主目录和用户名决定,能挡住“直接打开文件看到明文”,挡不住同一用户下的其他程序;桌面端的注释也写着以后可能改用 Electron safeStorage 托管密钥(credentialService.ts:22)。写入在文件锁内完成整段读改写,再以 0600 权限原子替换;文件损坏时先备份现场再报错,不会当成空文件覆盖掉别的进程写的凭据(shared-credentials.ts:306、316)。
续期。桌面端的 OAuth 服务接口里有 refreshToken,但 z.ai 与 bigmodel 两个适配器都没有实现刷新方法,调用会直接报“暂未提供 refresh token 交换接口,请重新登录”(oauthService.ts:964,packages/services/src/oauth/providers/providerAdapter.ts:31)。桌面端的做法是被动失效:带当前 JWT 的请求,或带当前 access token 查询用户信息的请求返回 401,就退出当前会话(packages/services/src/oauth/oauthUnauthorizedRequest.ts:21)。CLI 里没有刷新逻辑:模型请求用的是登录时取到的那把 API Key,不依赖 OAuth 令牌。zcode logout 删除所有共享键和各个人套餐 Provider 的身份与 Key,而且只删除值仍与读取时相同的项,避免误删并发登录刚写入的凭据(auth-login.ts:282)。
从登录态到请求凭据
账号型 Provider 的配置里没有 Key。Registry 只从账号层拿到“有没有权益、是不是当前连接”,真正的凭据在每次模型请求尝试之前现取,用完不进配置、不进协议(模型适配层讲了它怎样被塞进请求)。
桌面端按套餐类型决定凭据(packages/services/src/model-provider/accountProviderRequestAuthService.ts:68):
async resolveCurrent(input: AccountRequestAuthInput): Promise<AccountRequestAuthMaterial> {
const providerId = input.providerId.trim();
const access = await this.#resolveAccess(input.accountAccess);
if (!access) throw new AccountRequestCredentialUnavailableError(providerId);
if (access.planKind === "start-plan") {
const tokenSet = await this.#options.loadOAuthTokenSet(resolveOAuthProviderId(access.family));
return { apiKey: requireApiKey(tokenSet?.zcodeJwtToken, providerId) };
}
if (access.planKind === "individual-coding-plan") {
const apiKey = await this.#options.loadIndividualPlanApiKey(providerId, access.family);
return { apiKey: requireApiKey(apiKey, providerId) };
}
const apiKey = await this.#options.resolveTeamPlanApiKey(access);
return { apiKey: requireApiKey(apiKey, providerId) };
}个人套餐用那把 zcode-api-key,团队套餐用团队 Key,Start Plan 则直接把 ZCode JWT 当作 API Key。桌面端拉起的 Agent 进程通过协议向 Host 请求这份材料,单次最多等 180000 毫秒(apps/zcode-cli/packages/bootstrap/src/zcode-protocol/provider-runtime-headers.ts:16)。
独立运行的 CLI 更简单,也更窄:它只认各家的个人 Coding Plan Provider(standalone-account-provider-runtime.ts:40)。凭据文件里有这个 Provider 的身份和 Key,账号层就把它标成有权益,否则显式标成无权益,好让它退出 Registry(standalone-account-provider-runtime.ts:137);请求前的端口每次重新读身份与 Key(standalone-account-provider-runtime.ts:169)。也就是说,CLI 本地并不查询订阅状态,是否真的开通了套餐,要等请求发到官方端点、被改写到 ZCode 平台网关后由服务端判定(apps/zcode-cli/packages/adapters/src/model/official-coding-plan-gateway.ts:7)。从代码看,团队套餐、Start Plan 和闲时计划在独立 CLI 里拿不到权益,都进不了 Registry。
Coding Plan 各档与内置模型
四档账号型套餐都用 anthropic-messages 协议,端点与内置模型清单以规则集为准(config/provider/zcode-builtin.json:697、739、823):
| 套餐 | 端点 | 内置模型 | 说明 |
|---|---|---|---|
| 个人 Coding Plan | z.ai:https://api.z.ai/api/anthropic;bigmodel:https://open.bigmodel.cn/api/anthropic | GLM-5.3、GLM-5.3-Flash | 独立 CLI 登录后默认选中 GLM-5.3 |
| 团队 Coding Plan | 同个人版 | GLM-5.3、GLM-5.3-Flash | 仅桌面端,按组织与项目选连接 |
| Start Plan | 两家都是 https://zcode.z.ai/api/v1/zcode-plan/anthropic | GLM-5.3-Flash、GLM-5.2、GLM-5-Turbo | 实际清单由服务端下发 |
| 闲时(Idle plan) | 两家都是 https://zcode.z.ai/api/v1/off-peak/anthropic | GLM-5.3、GLM-5.3-Flash | 隐藏,只供闲时任务 |
builtinProviderModelRules 另外为个人版与团队版登记了 GLM-5.2、GLM-5-Turbo 的启用规则(zcode-builtin.json:6026),但一个 Provider 的模型列表只由内置清单加个人追加的模型 ID 组成(packages/provider/src/resolver.ts:237);从代码看,这两个模型要用户自己追加才会出现在个人与团队套餐下。Start Plan 的内置清单只是起点,账号层会用服务端返回的模型名单整体替换它,空名单也算数(packages/provider/src/account-provider-resolution.ts:106)。代码注释里 Start Plan 又叫“体验套餐”,是免费档(packages/ui/src/settings/model-provider-section/StatusCards.tsx:447)。
按规则集的叠加顺序算下来(叠加方式见Provider 规则一篇),Coding Plan 站点上四个模型的有效能力如下:
| 模型 | 上下文 | 输入 | 推理档位 | 输出上限 |
|---|---|---|---|---|
| GLM-5.3 | 1000000 | 文本、图片、视频 | low、high、max | 128000 |
| GLM-5.3-Flash | 1000000 | 文本、图片、视频、PDF | low、high、max | 128000 |
| GLM-5.2 | 1000000 | 文本、图片、视频 | disabled、high、max | 128000 |
| GLM-5-Turbo | 200000 | 文本、图片、视频 | disabled、enabled | 64000 |
图片与视频输入是 Coding Plan 与 Start Plan 站点规则统一打开的(zcode-builtin.json:4012);闲时端点没有这条规则,GLM-5.3 在那里只收文本,也不开原生 WebSearch(zcode-builtin.json:4125)。
桌面端判定权益的规则(packages/services/src/bigmodel/codingPlanEntitlement.ts:45、119):
- 个人版:订阅列表里有一条产品 ID 或名称含
coding的记录,状态为VALID且在当前周期内;列表请求超时 15 秒(packages/services/src/model-provider/codingPlanProviderAvailability.ts:33)。 - 团队版:
EXPIRED判为过期,EFFECTIVE但成员未分配(UNASSIGNED)判为未分配,EFFECTIVE且成员授权VALID才算可用,其余新状态一律记为未知,不擅自当成无权益。
不可用的原因只有四种:未登录、未连接、凭据获取失败、明确无权益(packages/shared/src/account-provider-state.ts:5)。已保存的模型选择若指向个人或团队套餐,执行前会被换成当前账号唯一处于“当前连接”的那档(packages/provider/src/effective-model-selection.ts:29),换了套餐不必重选模型。购买与续费(国内支付宝、微信,海外 Stripe、PayPal,以及团队的企业订单)都是桌面端服务,CLI 不涉及(packages/shared/src/coding-plan-subscription.ts:5)。
API Key 模式与账号模式
“用 API Key 接 Coding Plan”在代码里有两种形态,结果不一样:
| TUI 粘贴 Coding Plan Key | Coding Plan API Key 模板 | |
|---|---|---|
| 入口 | /login zai-coding-plan-api-key <key> 等 | 新建 Provider 时选 zai-api 或 bigmodel-api |
| 落在哪个 Provider | 账号型的个人 Coding Plan Provider | 个人 Provider,访问方式 zhipu-coding-plan-api-key |
| Key 存在哪 | credentials.json,逐项加密 | provider_config.json,明文 |
| 身份 | Key 的 SHA-256 前 24 位,形如 key-… | 无 |
| 权益与当前连接 | 参与账号层判定 | 不参与 |
| 请求路径 | 官方端点,经平台网关 | 同一端点,同样经网关 |
TUI 粘贴的 Key 由 configureCodingPlanApiKey 当作一次“登录”处理:用 Key 的摘要当账号身份,写进与 OAuth 登录相同的凭据键(auth-login.ts:258,standalone-account-provider-runtime.ts:101)。所以在独立 CLI 里,OAuth 登录与粘贴 Key 最终都表现为同一个 account:*-individual-coding-plan Provider,差别只是 Key 的来源。模板方式则完全是普通的 BYOK:两个模板默认启用 GLM-5.3 与 GLM-5.3-Flash(zcode-builtin.json:4507),因为地址相同,请求照样被改写到平台网关,但它们不算“账号连接”,闲时任务用不了(packages/services/src/session/offPeakRuntimeModel.ts:91)。
闲时计划(Idle plan)
闲时计划是两个隐藏的账号型 Provider,account:zai-offpeak-idle-plan 与 account:bigmodel-offpeak-idle-plan(packages/shared/src/off-peak-types.ts:41)。隐藏意味着它们不出现在模型选择器里,只由桌面端的调度器在派发闲时任务时使用;任务怎样排队、派发和防递归,见定时任务与闲时任务。这里只看与套餐相关的部分:
- 什么时候可用:远端
client/configs里的灰度开关offPeak.enable_offpeak_task为真,且 Registry 里确有闲时模型(packages/services/src/coding-plan-subscription/bigmodelCodingPlanSubscriptionProvider.ts:1328)。服务端的票据先排队,低峰窗口开且排到号才进入可派发状态;类型注释记录了两个时限,就绪票据 5 分钟、执行中票据 3 小时(off-peak-types.ts:32、157)。 - 谁能用:当前选中的连接必须是同一家族的个人或团队 Coding Plan,并且已登录、有 ZCode JWT;Start Plan 明确不支持(
offPeakRuntimeModel.ts:98、164)。 - 凭据:一次闲时执行同时带三样东西,
Authorization: Bearer <JWT>、X-Coding-Plan-Api-Key与X-Off-Peak-Ticket-ID,BigModel 团队版另带组织与项目头(offPeakRuntimeModel.ts:246)。 - 排队语义:适配层只对闲时 Provider 特判,429 或业务码 3105 表示还在排队,按服务端给的等待时间、最多 5 分钟探测一次,缺省 60 秒,不消耗重试预算;400 带 3102(兼容旧的 3001)表示票据失效,错误信息前缀
off-peak-ticket-expired,桌面端据此重新取号并续跑同一个会话(apps/zcode-cli/packages/adapters/src/model/offpeak-retry.ts:1、40)。 - 工具限制:
OffPeakCreate、OffPeakList两个工具让模型在对话里创建和查看闲时任务;闲时派发的回合里,它们以及会绕过本轮模型重新拉起子 Agent、落到用户付费套餐上的工具都会被拒绝(apps/zcode-cli/packages/core/src/tool/handlers/off-peak.ts:39)。额度用尽(3103)、没有合格套餐(3101)、模型不在允许名单等失败,会翻译成模型能转述给用户的固定文案(off-peak.ts:80)。
用量与额度
从代码看,CLI 与 TUI 里没有额度查询,用量与额度只在桌面端展示,服务接口是 IUsageStatsService(packages/services/src/usage-stats/usageStats.ts:20):
| 数据 | 来源 | 出处 |
|---|---|---|
| Coding Plan 额度 | 所属家族业务域的 /api/monitor/usage/quota/limit,超时 15 秒 | packages/services/src/usage-stats/providers/bigmodelUsageQuotaProvider.ts:72 |
| 额度重置机会 | 控制面 /api/v1/coding-plan/reset 下的状态、领取、使用接口,分 5 小时与每周两种 | bigmodelUsageQuotaProvider.ts:528,packages/shared/src/coding-plan-reset.ts:1 |
| Start Plan 余额 | 控制面的 /api/v1/zcode-plan/billing/balance | packages/shared/src/zcodeEndpoint.ts:269 |
| 官方 MCP 调用额度 | GET /api/v1/mcp/usage | packages/services/src/usage-stats/providers/zcodeMcpQuotaProvider.ts:24 |
额度快照是一个等级加若干条限额,每条有类型、已用、剩余、已用占比、下次重置时间和按模型的用量明细;Start Plan 的额度桶还带桶 ID、套餐实例与周期起止,用于提醒去重(packages/shared/src/usage-quota.ts:9)。
下一篇:子 Agent——主会话怎样派出 explore、general-purpose 等子 Agent,它们用什么模型、带什么工具、怎样把结果交回来。