From 0f23527bfc3a87be9b254357e7f2b8d1e520bfd2 Mon Sep 17 00:00:00 2001 From: "lisheng.lisheng" Date: Fri, 10 Jul 2026 00:00:57 +0800 Subject: [PATCH] feat(core): move generateCLIAccessToken to core and auto-refresh on NotLogined Move the GenerateCLIAccessToken API call logic into core/auth/refresh-token.ts so it can be reused across packages. Add refreshAccessToken() which reads AK/SK from config, calls the API, and persists the new access_token. Client.console() now catches NotLogined errors and automatically retries with a refreshed token when AK/SK are available in config. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../commands/auth/generate-access-token.ts | 58 +----------- packages/core/src/auth/index.ts | 1 + packages/core/src/auth/refresh-token.ts | 89 +++++++++++++++++++ packages/core/src/client/client.ts | 33 +++++-- 4 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 packages/core/src/auth/refresh-token.ts diff --git a/packages/commands/src/commands/auth/generate-access-token.ts b/packages/commands/src/commands/auth/generate-access-token.ts index 0470ef1..7a2c36d 100644 --- a/packages/commands/src/commands/auth/generate-access-token.ts +++ b/packages/commands/src/commands/auth/generate-access-token.ts @@ -1,57 +1 @@ -import { Client, REGIONS, type Identity, type Region, type Settings } from "bailian-cli-core"; - -const API_VERSION = "2026-02-10"; -const API_ACTION = "GenerateCLIAccessToken"; -const API_PATH = "/modelstudio/cli/generateAccessToken"; - -const MODEL_STUDIO_HOSTS: Partial> = { - cn: "modelstudio.cn-beijing.aliyuncs.com", - intl: "modelstudio.ap-southeast-1.aliyuncs.com", -}; - -function resolveRegion(baseUrl: string): Region { - for (const [region, url] of Object.entries(REGIONS) as Array<[Region, string]>) { - if (baseUrl === url || baseUrl.startsWith(`${url}/`)) return region; - } - return "cn"; -} - -function modelStudioHost(baseUrl: string): string { - const region = resolveRegion(baseUrl); - return MODEL_STUDIO_HOSTS[region] ?? MODEL_STUDIO_HOSTS.cn!; -} - -interface GenerateCLIAccessTokenResponse { - Success?: boolean; - Code?: string; - Message?: string; - Data?: Record; -} - -export async function generateCLIAccessToken(opts: { - identity: Identity; - settings: Settings; - baseUrl: string; - accessKeyId: string; - accessKeySecret: string; -}): Promise { - const { identity, settings, baseUrl, accessKeyId, accessKeySecret } = opts; - - const client = new Client({ - identity, - settings, - baseUrl, - openApiCred: { accessKeyId, accessKeySecret, source: "flag" }, - }); - - const host = modelStudioHost(baseUrl); - - return client.openApiQueryJson({ - host, - path: API_PATH, - action: API_ACTION, - version: API_VERSION, - method: "POST", - queryParams: {}, - }); -} +export { generateCLIAccessToken } from "bailian-cli-core"; diff --git a/packages/core/src/auth/index.ts b/packages/core/src/auth/index.ts index 76a966f..00acf13 100644 --- a/packages/core/src/auth/index.ts +++ b/packages/core/src/auth/index.ts @@ -13,3 +13,4 @@ export type { AuthState, CredentialSource, } from "./types.ts"; +export { generateCLIAccessToken, refreshAccessToken } from "./refresh-token.ts"; diff --git a/packages/core/src/auth/refresh-token.ts b/packages/core/src/auth/refresh-token.ts new file mode 100644 index 0000000..93533e8 --- /dev/null +++ b/packages/core/src/auth/refresh-token.ts @@ -0,0 +1,89 @@ +import { REGIONS, type Region } from "../config/schema.ts"; +import type { Identity, Settings } from "../config/schema.ts"; +import { readConfigFile, writeConfigFile } from "../config/loader.ts"; +import { Client } from "../client/client.ts"; + +const API_VERSION = "2026-02-10"; +const API_ACTION = "GenerateCLIAccessToken"; +const API_PATH = "/modelstudio/cli/generateAccessToken"; + +const MODEL_STUDIO_HOSTS: Partial> = { + cn: "modelstudio.cn-beijing.aliyuncs.com", + intl: "modelstudio.ap-southeast-1.aliyuncs.com", +}; + +function resolveRegion(baseUrl: string): Region { + for (const [region, url] of Object.entries(REGIONS) as Array<[Region, string]>) { + if (baseUrl === url || baseUrl.startsWith(`${url}/`)) return region; + } + return "cn"; +} + +function modelStudioHost(baseUrl: string): string { + const region = resolveRegion(baseUrl); + return MODEL_STUDIO_HOSTS[region] ?? MODEL_STUDIO_HOSTS.cn!; +} + +export async function generateCLIAccessToken(opts: { + identity: Identity; + settings: Settings; + baseUrl: string; + accessKeyId: string; + accessKeySecret: string; +}): Promise { + const { identity, settings, baseUrl, accessKeyId, accessKeySecret } = opts; + + const client = new Client({ + identity, + settings, + baseUrl, + openApiCred: { accessKeyId, accessKeySecret, source: "flag" }, + }); + + const host = modelStudioHost(baseUrl); + + return client.openApiQueryJson({ + host, + path: API_PATH, + action: API_ACTION, + version: API_VERSION, + method: "POST", + queryParams: {}, + }); +} + +/** + * Try to refresh the console access_token using stored AK/SK. + * Returns the new token on success, or null if AK/SK are not available. + */ +export async function refreshAccessToken(opts: { + identity: Identity; + settings: Settings; + baseUrl: string; +}): Promise { + const config = readConfigFile(); + const accessKeyId = config.access_key_id; + const accessKeySecret = config.access_key_secret; + if (!accessKeyId || !accessKeySecret) return null; + + if (opts.settings.verbose) { + process.stderr.write("Refreshing access token...\n"); + } + + const resp = await generateCLIAccessToken({ + identity: opts.identity, + settings: opts.settings, + baseUrl: opts.baseUrl, + accessKeyId, + accessKeySecret, + }); + + const token: string | undefined = resp.cliAccessToken; + if (!token) return null; + + const existing = readConfigFile() as Record; + existing.access_token = token; + await writeConfigFile(existing); + + return token; +} diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index eb1dc2c..e668214 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -7,6 +7,7 @@ import { buildAcsCanonicalQuery, signAcsRequest, type AcsQueryParams } from "./a import { isLocalFile, resolveFileUrl } from "../files/upload.ts"; import { McpClient } from "./mcp.ts"; import { callConsoleGateway } from "../console/gateway.ts"; +import { refreshAccessToken } from "../auth/refresh-token.ts"; import { maskToken } from "../utils/token.ts"; import { trackingHeaders } from "./headers.ts"; @@ -112,15 +113,35 @@ export class Client { return new McpClient(this.http, url, this.deps.apiCred?.token); } - console(api: string, data: Record): Promise { + async console(api: string, data: Record): Promise { if (!this.deps.consoleCred) { throw new BailianError("This command needs a console access token.", ExitCode.AUTH); } - // region / site / switchAgent 已解析在 consoleCred 里,gateway 不再回读 config。 - return callConsoleGateway(this.deps.consoleCred, this.deps.settings.timeout, { - api, - data, - }) as Promise; + try { + return (await callConsoleGateway(this.deps.consoleCred, this.deps.settings.timeout, { + api, + data, + })) as T; + } catch (err) { + if ( + !(err instanceof BailianError) || + err.exitCode !== ExitCode.AUTH || + !err.message.includes("not logged in") + ) { + throw err; + } + const newToken = await refreshAccessToken({ + identity: this.deps.identity, + settings: this.deps.settings, + baseUrl: this.deps.baseUrl, + }); + if (!newToken) throw err; + return (await callConsoleGateway( + { ...this.deps.consoleCred, token: newToken }, + this.deps.settings.timeout, + { api, data }, + )) as T; + } } async openApiQueryJson(opts: ClientOpenApiQueryOpts): Promise {