Skip to content

Commit 6411f35

Browse files
committed
add token plan
1 parent 006ea23 commit 6411f35

51 files changed

Lines changed: 948 additions & 221 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/cli/src/commands/app/call.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ export default defineCommand({
102102
}
103103

104104
if (config.dryRun) {
105-
emitResult({ endpoint: appCompletionEndpoint(config.baseUrl, appId), request: body }, format);
105+
emitResult({ endpoint: appCompletionEndpoint(config, appId), request: body }, format);
106106
return;
107107
}
108108

109-
const url = appCompletionEndpoint(config.baseUrl, appId);
109+
const url = appCompletionEndpoint(config, appId);
110110

111111
if (shouldStream) {
112112
const headers: Record<string, string> = { "X-DashScope-SSE": "enable" };

packages/cli/src/commands/auth/login.ts

Lines changed: 153 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {
2+
AUTH_MODES,
23
BailianError,
4+
CODING_PLAN_REGIONS,
35
ExitCode,
6+
TOKEN_PLAN_REGIONS,
47
chatEndpoint,
58
defineCommand,
69
getConfigPath,
@@ -9,12 +12,16 @@ import {
912
readConfigFile,
1013
requestJson,
1114
writeConfigFile,
15+
type AuthMode,
16+
type CodingPlanRegion,
1217
type Config,
1318
type GlobalFlags,
19+
type TokenPlanRegion,
1420
} from "bailian-cli-core";
1521
import { printQuickStart } from "../../output/banner.ts";
1622
import { emitBare } from "../../output/output.ts";
1723
import { promptConfirm } from "../../output/prompt.ts";
24+
import { interactiveAuthSetup } from "../../utils/auth-wizard.ts";
1825
import { printCurrentCommandHelp } from "../../utils/command-help.ts";
1926
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
2027

@@ -39,11 +46,26 @@ function canRetry(err: unknown): boolean {
3946
return false;
4047
}
4148

42-
async function validateKeyAndPersist(config: Config, key: string): Promise<void> {
49+
async function validateKeyAndPersist(
50+
config: Config,
51+
key: string,
52+
mode: AuthMode,
53+
codingPlanRegion: CodingPlanRegion,
54+
tokenPlanRegion: TokenPlanRegion,
55+
): Promise<void> {
4356
process.stderr.write("Testing key... ");
44-
const testConfig = { ...config, apiKey: key };
57+
const testConfig: Config = {
58+
...config,
59+
activeAuthMode: mode,
60+
apiKey: undefined,
61+
fileApiKey: mode === "standard-api-key" ? key : undefined,
62+
codingPlanApiKey: mode === "coding-plan" ? key : config.codingPlanApiKey,
63+
codingPlanRegion,
64+
tokenPlanApiKey: mode === "token-plan" ? key : config.tokenPlanApiKey,
65+
tokenPlanRegion,
66+
};
4567
const requestOpts = {
46-
url: chatEndpoint(testConfig.baseUrl),
68+
url: chatEndpoint(testConfig),
4769
method: "POST",
4870
timeout: Math.min(config.timeout, 30),
4971
body: {
@@ -64,7 +86,6 @@ async function validateKeyAndPersist(config: Config, key: string): Promise<void>
6486
cause: err,
6587
});
6688
}
67-
// retry delay: 500ms, 1000ms, 2000ms
6889
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
6990
await new Promise((resolve) => setTimeout(resolve, delayMs));
7091
}
@@ -73,25 +94,49 @@ async function validateKeyAndPersist(config: Config, key: string): Promise<void>
7394
process.stderr.write("Valid\n");
7495

7596
const existing = readConfigFile() as Record<string, unknown>;
76-
existing.api_key = key;
97+
existing.active_auth_mode = mode;
98+
if (mode === "standard-api-key") existing.api_key = key;
99+
if (mode === "coding-plan") {
100+
existing.coding_plan_api_key = key;
101+
existing.coding_plan_region = codingPlanRegion;
102+
}
103+
if (mode === "token-plan") {
104+
existing.token_plan_api_key = key;
105+
existing.token_plan_region = tokenPlanRegion;
106+
}
77107
await writeConfigFile(existing);
108+
process.stderr.write(`Active auth mode set to ${mode}\n`);
78109
process.stderr.write(`Saved to ${getConfigPath()}\n`);
79110
}
80111

81112
export default defineCommand({
82113
name: "auth login",
83-
description: "Authenticate with API key or console browser login (credentials can coexist)",
84-
usage: "bl auth login --api-key <key> | bl auth login --console",
114+
description: "Authenticate with API key, console browser login, or interactive wizard",
115+
usage: "bl auth login --mode <mode> --api-key <key> | bl auth login --console",
85116
options: [
86-
{ flag: "--api-key <key>", description: "DashScope API key to store" },
117+
{
118+
flag: "--mode <mode>",
119+
description: "Auth mode: standard-api-key, coding-plan, token-plan",
120+
},
121+
{ flag: "--api-key <key>", description: "API key for the selected auth mode" },
122+
{ flag: "--coding-plan-api-key <key>", description: "Coding Plan API key (sets mode)" },
123+
{ flag: "--coding-plan-region <region>", description: "Coding Plan region: cn, intl" },
124+
{ flag: "--token-plan-api-key <key>", description: "Token Plan API key (sets mode)" },
125+
{ flag: "--token-plan-region <region>", description: "Token Plan region: cn, intl" },
87126
{
88127
flag: "--console",
89128
description: "Sign in via browser; opens the console login URL in your default browser",
90129
type: "boolean",
91130
},
92131
],
93-
examples: ["bl auth login --api-key sk-xxxxx", "bl auth login --console"],
132+
examples: [
133+
"bl auth login --api-key sk-xxxxx",
134+
"bl auth login --mode coding-plan --api-key sk-sp-xxxxx",
135+
"bl auth login --token-plan-api-key sk-xxxxx --token-plan-region intl",
136+
"bl auth login --console",
137+
],
94138
async run(config: Config, flags: GlobalFlags) {
139+
// Console login branch (cli-specific)
95140
if (flags.console) {
96141
if (config.dryRun) {
97142
emitBare(
@@ -102,11 +147,32 @@ export default defineCommand({
102147
const hasApiKey = !!(config.apiKey || config.fileApiKey);
103148
await runConsoleLogin(resolveConsoleOrigin(), {
104149
needApiKey: !hasApiKey,
105-
onApiKey: (key) => validateKeyAndPersist(config, key),
150+
onApiKey: (key) =>
151+
validateKeyAndPersist(
152+
config,
153+
key,
154+
"standard-api-key",
155+
config.codingPlanRegion,
156+
config.tokenPlanRegion,
157+
),
106158
});
107159
return;
108160
}
109161

162+
const hasFlags = !!(
163+
flags.mode ||
164+
flags.apiKey ||
165+
flags.codingPlanApiKey ||
166+
flags.tokenPlanApiKey ||
167+
flags.codingPlanRegion ||
168+
flags.tokenPlanRegion
169+
);
170+
171+
let mode: AuthMode;
172+
let key: string | undefined;
173+
let codingPlanRegion: CodingPlanRegion;
174+
let tokenPlanRegion: TokenPlanRegion;
175+
110176
const envKey = process.env.DASHSCOPE_API_KEY;
111177
if (envKey && !flags.apiKey) {
112178
const maskedEnvKey = maskToken(envKey);
@@ -119,22 +185,93 @@ export default defineCommand({
119185
process.stdout.write("Login skipped. Using environment variables.\n");
120186
process.exit(0);
121187
}
122-
} else {
188+
} else if (hasFlags) {
123189
process.stderr.write(`Warning: DASHSCOPE_API_KEY is already set in environment.\n`);
124190
}
125191
}
126192

127-
const key = (flags.apiKey as string) || config.apiKey;
193+
if (!hasFlags && isInteractive({ nonInteractive: config.nonInteractive })) {
194+
const result = await interactiveAuthSetup(config);
195+
mode = result.mode;
196+
key = result.key;
197+
codingPlanRegion = result.codingPlanRegion ?? config.codingPlanRegion;
198+
tokenPlanRegion = result.tokenPlanRegion ?? config.tokenPlanRegion;
199+
} else {
200+
const explicitMode = flags.mode as string | undefined;
201+
mode = resolveLoginMode(explicitMode, flags);
202+
key =
203+
(flags.apiKey as string | undefined) ||
204+
(flags.codingPlanApiKey as string | undefined) ||
205+
(flags.tokenPlanApiKey as string | undefined) ||
206+
(mode === "standard-api-key" ? config.apiKey : undefined);
207+
codingPlanRegion = resolveCodingPlanRegion(flags, config);
208+
tokenPlanRegion = resolveTokenPlanRegion(flags, config);
209+
}
210+
128211
if (!key) {
129-
printCurrentCommandHelp(process.stderr);
130-
process.exit(0);
212+
if (!hasFlags) {
213+
printCurrentCommandHelp(process.stderr);
214+
process.exit(0);
215+
}
216+
throw new BailianError(
217+
"--api-key is required.",
218+
ExitCode.USAGE,
219+
"bl auth login --mode <mode> --api-key <key>",
220+
);
221+
}
222+
223+
if (mode === "coding-plan" && !key.startsWith("sk-sp-")) {
224+
throw new BailianError(
225+
'Invalid API key. Coding Plan API keys start with "sk-sp-".',
226+
ExitCode.USAGE,
227+
);
131228
}
132229

133230
if (!config.dryRun) {
134-
await validateKeyAndPersist(config, key);
231+
await validateKeyAndPersist(config, key, mode, codingPlanRegion, tokenPlanRegion);
135232
printQuickStart();
136233
} else {
137-
emitBare("Would validate and save API key.");
234+
emitBare(`Would validate and save ${mode} API key.`);
138235
}
139236
},
140237
});
238+
239+
function resolveLoginMode(explicitMode: string | undefined, flags: GlobalFlags): AuthMode {
240+
if (explicitMode) {
241+
if (!AUTH_MODES.has(explicitMode)) {
242+
throw new BailianError(
243+
`Invalid auth mode "${explicitMode}".`,
244+
ExitCode.USAGE,
245+
"Valid modes: standard-api-key, coding-plan, token-plan",
246+
);
247+
}
248+
return explicitMode as AuthMode;
249+
}
250+
if (flags.codingPlanApiKey) return "coding-plan";
251+
if (flags.tokenPlanApiKey) return "token-plan";
252+
return "standard-api-key";
253+
}
254+
255+
function resolveCodingPlanRegion(flags: GlobalFlags, config: Config): CodingPlanRegion {
256+
const region = (flags.codingPlanRegion as string | undefined) || config.codingPlanRegion;
257+
if (!CODING_PLAN_REGIONS.has(region)) {
258+
throw new BailianError(
259+
`Invalid Coding Plan region "${region}".`,
260+
ExitCode.USAGE,
261+
"Valid Coding Plan regions: cn, intl",
262+
);
263+
}
264+
return region as CodingPlanRegion;
265+
}
266+
267+
function resolveTokenPlanRegion(flags: GlobalFlags, config: Config): TokenPlanRegion {
268+
const region = (flags.tokenPlanRegion as string | undefined) || config.tokenPlanRegion;
269+
if (!TOKEN_PLAN_REGIONS.has(region)) {
270+
throw new BailianError(
271+
`Invalid Token Plan region "${region}".`,
272+
ExitCode.USAGE,
273+
"Valid Token Plan regions: cn, intl",
274+
);
275+
}
276+
return region as TokenPlanRegion;
277+
}
Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {
22
defineCommand,
3-
clearApiKey,
43
readConfigFile,
54
writeConfigFile,
65
getConfigPath,
@@ -9,34 +8,27 @@ import {
98
} from "bailian-cli-core";
109
import { emitBare } from "../../output/output.ts";
1110

12-
async function clearConsoleToken(): Promise<boolean> {
13-
const file = readConfigFile() as Record<string, unknown>;
14-
if (!file.access_token) return false;
15-
delete file.access_token;
16-
await writeConfigFile(file);
17-
return true;
18-
}
19-
2011
export default defineCommand({
2112
name: "auth logout",
2213
description: "Clear stored credentials",
23-
usage: "bl auth logout [--console] [--yes] [--dry-run]",
14+
usage: "bl auth logout [--console] [--all] [--yes] [--dry-run]",
2415
options: [
2516
{
2617
flag: "--console",
2718
description: "Only clear the console access_token, keep api_key intact",
2819
type: "boolean",
2920
},
21+
{ flag: "--all", description: "Clear all stored auth credentials", type: "boolean" },
3022
{ flag: "--yes", description: "Skip confirmation prompt" },
3123
],
3224
examples: [
3325
"bl auth logout",
3426
"bl auth logout --console",
27+
"bl auth logout --all",
3528
"bl auth logout --dry-run",
36-
"bl auth logout --yes",
3729
],
3830
async run(config: Config, flags: GlobalFlags) {
39-
const file = readConfigFile();
31+
const file = readConfigFile() as Record<string, unknown>;
4032

4133
if (flags.console) {
4234
const hasToken = !!file.access_token;
@@ -47,7 +39,8 @@ export default defineCommand({
4739
return;
4840
}
4941
if (hasToken) {
50-
await clearConsoleToken();
42+
delete file.access_token;
43+
await writeConfigFile(file);
5144
process.stderr.write(`Cleared access_token from ${getConfigPath()}\n`);
5245
if (file.api_key) {
5346
process.stderr.write(
@@ -60,20 +53,44 @@ export default defineCommand({
6053
return;
6154
}
6255

63-
const hasKey = !!(file.api_key || file.access_token);
56+
const keys = keysToClear(config, !!flags.all);
57+
const hasKey = keys.some(
58+
(key) => typeof file[key] === "string" && (file[key] as string).length,
59+
);
6460

6561
if (config.dryRun) {
66-
if (hasKey) emitBare("Would clear api_key / access_token from ~/.bailian/config.json");
62+
if (hasKey) emitBare(`Would clear ${keys.join(", ")} from ~/.bailian/config.json`);
6763
else emitBare("No credentials to clear.");
6864
emitBare("No changes made.");
6965
return;
7066
}
7167

7268
if (hasKey) {
73-
await clearApiKey();
74-
process.stderr.write("Cleared api_key / access_token from ~/.bailian/config.json\n");
69+
for (const key of keys) delete file[key];
70+
if (config.activeAuthMode !== "standard-api-key") {
71+
file.active_auth_mode = "standard-api-key";
72+
}
73+
await writeConfigFile(file);
74+
process.stderr.write(`Cleared ${keys.join(", ")} from ${getConfigPath()}\n`);
75+
if (config.activeAuthMode !== "standard-api-key") {
76+
process.stderr.write("Active auth mode reset to standard-api-key.\n");
77+
}
7578
} else {
7679
process.stderr.write("No credentials to clear.\n");
7780
}
7881
},
7982
});
83+
84+
function keysToClear(config: Config, all: boolean): string[] {
85+
if (all)
86+
return [
87+
"api_key",
88+
"coding_plan_api_key",
89+
"token_plan_api_key",
90+
"active_auth_mode",
91+
"access_token",
92+
];
93+
if (config.activeAuthMode === "coding-plan") return ["coding_plan_api_key"];
94+
if (config.activeAuthMode === "token-plan") return ["token_plan_api_key"];
95+
return ["api_key", "access_token"];
96+
}

0 commit comments

Comments
 (0)