Skip to content

Commit f694e37

Browse files
committed
fix(runtime): append --config to recovery hints for named profiles
1 parent 5098748 commit f694e37

6 files changed

Lines changed: 100 additions & 21 deletions

File tree

‎docs/agents/error-hint-change.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ process.exit(err.exitCode)
109109
- [ ] 追加时若已有 hint(如 video task id),拼接并按标记去重;不要 `hint` 非空就跳过
110110
- [ ] 渲染产品命令前须经能力门控(命令表是否暴露对应 path),不要对 `kscli` 等缺命令的产品硬拼 `usage freetier`
111111
- [ ] 命令名使用 `commandPrefix`,不要硬编码 `bl`
112+
- [ ] 恢复命令经 `recoveryCommand` 渲染:命名 Profile 激活时追加 `--config <name>`(default / 未指定不加)
112113
- [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"`
113114

114115
### C. command / runtime 直接抛错

‎packages/runtime/src/create-cli.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
352352
localize,
353353
commandPrefix,
354354
supportsFreeTierRecovery,
355+
configName: sources.configName,
355356
});
356357
setupProxyFromEnv();
357358
const registry = await getRegistry(translator);
@@ -361,6 +362,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
361362
localize,
362363
commandPrefix,
363364
supportsFreeTierRecovery,
365+
configName: sources.configName,
364366
});
365367
for (const warning of warnings.splice(0)) reporter.output.warning(warning);
366368
return reporter;

‎packages/runtime/src/error-handler.ts‎

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ export function normalizeError(
1414
error: unknown,
1515
commandPrefix: string,
1616
localize: Localize,
17+
configName?: string,
1718
): BailianError {
1819
if (error instanceof BailianError) return error;
1920
if (error instanceof Error) {
2021
return (
21-
classifyRuntimeError(error, commandPrefix, localize) ??
22+
classifyRuntimeError(error, commandPrefix, localize, configName) ??
2223
new BailianError(error.message, ExitCode.GENERAL, undefined, { cause: error })
2324
);
2425
}
@@ -32,9 +33,18 @@ export function normalizeError(
3233
type Localize = (text: LocalizedText) => string;
3334
const english: Localize = (text) => (typeof text === "string" ? text : text["en-US"]);
3435

36+
/**
37+
* 恢复命令统一入口:命名 Profile 激活时追加 `--config <name>`,
38+
* 避免用户按 hint 操作打到错误的凭证/计费配置。
39+
*/
40+
function recoveryCommand(commandPrefix: string, args: string, configName?: string): string {
41+
const withConfig = configName ? `${args} --config ${configName}` : args;
42+
return formatCommand(commandPrefix, withConfig);
43+
}
44+
3545
/** Complete recovery text supplied before Console errors can be wrapped by commands. */
36-
export function consoleRecoveryHint(commandPrefix: string): string {
37-
return `Run \`${formatCommand(commandPrefix, "auth login --console")}\` to sign in or refresh your console session.`;
46+
export function consoleRecoveryHint(commandPrefix: string, configName?: string): string {
47+
return `Run \`${recoveryCommand(commandPrefix, "auth login --console", configName)}\` to sign in or refresh your console session.`;
3848
}
3949

4050
/** Native DashScope code for free-tier-only exhaustion (not TPS/TPM insufficient_quota). */
@@ -46,6 +56,8 @@ export interface ErrorHintOptions {
4656
* under this product's commandPrefix. Derived from the product command map.
4757
*/
4858
supportsFreeTierRecovery?: boolean;
59+
/** 当前命名 Profile;未指定或 default 时为 undefined,恢复命令不加 `--config`。 */
60+
configName?: string;
4961
}
5062

5163
/** Detect AllocationQuota.FreeTierOnly without matching TPS/TPM insufficient_quota. */
@@ -61,10 +73,18 @@ function isFreeTierOnlyExhausted(error: BailianError): boolean {
6173
}
6274

6375
/** Localized free-tier recovery steps; only call when the product exposes usage commands. */
64-
function freeTierRecoveryHint(commandPrefix: string, localize: Localize): string {
65-
const statusCommand = formatCommand(commandPrefix, "usage free --model <model>");
76+
function freeTierRecoveryHint(
77+
commandPrefix: string,
78+
localize: Localize,
79+
configName?: string,
80+
): string {
81+
const statusCommand = recoveryCommand(commandPrefix, "usage free --model <model>", configName);
6682
// 不带 --yes:调用方先撞确认门槛(exit 7),再按 skill 规范向用户确认。
67-
const disableCommand = formatCommand(commandPrefix, "usage freetier --off --model <model>");
83+
const disableCommand = recoveryCommand(
84+
commandPrefix,
85+
"usage freetier --off --model <model>",
86+
configName,
87+
);
6888
return localize({
6989
"en-US": `Check status: ${statusCommand}\nTo keep calling after free quota runs out (pay-as-you-go), disable auto-stop — confirmation required: ${disableCommand}`,
7090
"zh-CN": `查看状态:${statusCommand}\n若要在免费额度用尽后按量继续调用,请停用自动停止(需确认):${disableCommand}`,
@@ -78,20 +98,21 @@ export function errorHint(
7898
localize: Localize,
7999
options: ErrorHintOptions = {},
80100
): string | undefined {
101+
const configName = options.configName;
81102
let hint = error.hint;
82103
if (error instanceof CredentialError) {
83104
switch (error.reason) {
84105
case "missing-api-key":
85-
hint = `Set DASHSCOPE_API_KEY, pass --api-key, or run \`${formatCommand(commandPrefix, "auth login")}\`.`;
106+
hint = `Set DASHSCOPE_API_KEY, pass --api-key, or run \`${recoveryCommand(commandPrefix, "auth login", configName)}\`.`;
86107
break;
87108
case "missing-console-token":
88-
return `Run \`${formatCommand(commandPrefix, "auth login --console")}\`.`;
109+
return `Run \`${recoveryCommand(commandPrefix, "auth login --console", configName)}\`.`;
89110
case "expired-console-session":
90-
return consoleRecoveryHint(commandPrefix);
111+
return consoleRecoveryHint(commandPrefix, configName);
91112
case "missing-openapi-pair":
92-
return `Set BAILIAN_ACCESS_KEY_ID and BAILIAN_ACCESS_KEY_SECRET (or ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET), pass --access-key-id and --access-key-secret, or run \`${formatCommand(commandPrefix, "auth login --open-api")}\`.`;
113+
return `Set BAILIAN_ACCESS_KEY_ID and BAILIAN_ACCESS_KEY_SECRET (or ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET), pass --access-key-id and --access-key-secret, or run \`${recoveryCommand(commandPrefix, "auth login --open-api", configName)}\`.`;
93114
case "incomplete-openapi-config":
94-
return `Run \`${formatCommand(commandPrefix, "auth login --open-api --access-key-id <id> --access-key-secret <secret>")}\` again to save a complete pair.`;
115+
return `Run \`${recoveryCommand(commandPrefix, "auth login --open-api --access-key-id <id> --access-key-secret <secret>", configName)}\` again to save a complete pair.`;
95116
case "incomplete-openapi-flag":
96117
case "incomplete-openapi-env":
97118
return hint;
@@ -105,7 +126,7 @@ export function errorHint(
105126
isFreeTierOnlyExhausted(error) &&
106127
!hint?.includes("usage freetier")
107128
) {
108-
const recovery = freeTierRecoveryHint(commandPrefix, localize);
129+
const recovery = freeTierRecoveryHint(commandPrefix, localize, configName);
109130
hint = hint ? `${hint}\n${recovery}` : recovery;
110131
}
111132
if (error.exitCode !== ExitCode.AUTH) return hint;
@@ -121,8 +142,8 @@ export function errorHint(
121142
return hint;
122143
return [
123144
hint,
124-
`${formatCommand(commandPrefix, "auth login --api-key <your-key>")}`,
125-
`${formatCommand(commandPrefix, "auth status")}`,
145+
`${recoveryCommand(commandPrefix, "auth login --api-key <your-key>", configName)}`,
146+
`${recoveryCommand(commandPrefix, "auth status", configName)}`,
126147
localize({ "en-US": `Get API Key: ${API_KEY_PAGE}`, "zh-CN": `获取 API Key:${API_KEY_PAGE}` }),
127148
]
128149
.filter((line) => line !== undefined)
@@ -134,11 +155,12 @@ export function classifyRuntimeError(
134155
error: Error,
135156
commandPrefix: string,
136157
localize: Localize = english,
158+
configName?: string,
137159
): BailianError | undefined {
138160
if (error instanceof BailianError) return error;
139161
const baseUrlHint = localize({
140-
"en-US": `If the DashScope host is wrong, check baseUrl (--base-url, ${formatCommand(commandPrefix, "config show")}, or DASHSCOPE_BASE_URL).`,
141-
"zh-CN": `如果 DashScope 主机地址有误,请检查 baseUrl(--base-url、${formatCommand(commandPrefix, "config show")} 或 DASHSCOPE_BASE_URL)。`,
162+
"en-US": `If the DashScope host is wrong, check baseUrl (--base-url, ${recoveryCommand(commandPrefix, "config show", configName)}, or DASHSCOPE_BASE_URL).`,
163+
"zh-CN": `如果 DashScope 主机地址有误,请检查 baseUrl(--base-url、${recoveryCommand(commandPrefix, "config show", configName)} 或 DASHSCOPE_BASE_URL)。`,
142164
});
143165
if (
144166
error.name === "AbortError" ||
@@ -149,8 +171,8 @@ export function classifyRuntimeError(
149171
localize({ "en-US": "Request timed out.", "zh-CN": "请求超时。" }),
150172
ExitCode.TIMEOUT,
151173
localize({
152-
"en-US": `Try increasing --timeout (e.g. --timeout 60).\n${baseUrlHint}\nRun: ${formatCommand(commandPrefix, "auth status")} — to check credentials.`,
153-
"zh-CN": `请尝试增大 --timeout(例如 --timeout 60)。\n${baseUrlHint}\n运行 ${formatCommand(commandPrefix, "auth status")} 检查凭证。`,
174+
"en-US": `Try increasing --timeout (e.g. --timeout 60).\n${baseUrlHint}\nRun: ${recoveryCommand(commandPrefix, "auth status", configName)} — to check credentials.`,
175+
"zh-CN": `请尝试增大 --timeout(例如 --timeout 60)。\n${baseUrlHint}\n运行 ${recoveryCommand(commandPrefix, "auth status", configName)} 检查凭证。`,
154176
}),
155177
{ cause: error },
156178
);

‎packages/runtime/src/middleware.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ export const authStage: Middleware = async (ctx, next) => {
190190
const base = {
191191
identity: ctx.identity,
192192
settings,
193-
consoleRecoveryHint: consoleRecoveryHint(formatCommand(ctx.identity)),
193+
consoleRecoveryHint: consoleRecoveryHint(formatCommand(ctx.identity), sources.configName),
194194
debug: (diagnostic: Parameters<CommandOutput["debug"]>[0]) => ctx.output.debug(diagnostic),
195195
...resolveModelBaseUrlState(sources),
196196
};

‎packages/runtime/src/output/reporter.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface OutputReporterOptions {
2525
commandPrefix?: string;
2626
/** When true, free-tier recovery may render usage free / usage freetier commands. */
2727
supportsFreeTierRecovery?: boolean;
28+
/** 当前命名 Profile;未指定或 default 时为 undefined。 */
29+
configName?: string;
2830
}
2931

3032
export interface OutputReporter {
@@ -97,9 +99,15 @@ export function createOutputReporter(options: OutputReporterOptions): OutputRepo
9799
},
98100
error(error) {
99101
if (errorCode !== undefined) return errorCode;
100-
const failure = normalizeError(error, options.commandPrefix ?? "cli", localize);
102+
const failure = normalizeError(
103+
error,
104+
options.commandPrefix ?? "cli",
105+
localize,
106+
options.configName,
107+
);
101108
const hint = errorHint(failure, options.commandPrefix ?? "cli", localize, {
102109
supportsFreeTierRecovery: options.supportsFreeTierRecovery,
110+
configName: options.configName,
103111
});
104112
if (verbose && error instanceof Error && !(error instanceof BailianError)) {
105113
diagnostic("debug", {

‎packages/runtime/tests/error-handler.test.ts‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ import { ConfirmationRequiredError } from "../src/confirm.ts";
77
function render(
88
error: unknown,
99
output: "text" | "json" = "json",
10-
options: { commandPrefix?: string; supportsFreeTierRecovery?: boolean } = {},
10+
options: {
11+
commandPrefix?: string;
12+
supportsFreeTierRecovery?: boolean;
13+
configName?: string;
14+
} = {},
1115
) {
1216
let stderr = "";
1317
const reporter = createOutputReporter({
1418
settings: { output, quiet: true, verbose: false },
1519
commandPrefix: options.commandPrefix ?? "kscli",
1620
supportsFreeTierRecovery: options.supportsFreeTierRecovery,
21+
configName: options.configName,
1722
stderr: {
1823
write: (chunk) => {
1924
stderr += chunk;
@@ -167,3 +172,44 @@ test("free-tier hint does not duplicate when existing hint already mentions usag
167172
expect(payload.hint).toBe(existing);
168173
expect(payload.hint?.split("usage freetier").length - 1).toBe(1);
169174
});
175+
176+
test("recovery hints append --config when a named profile is active", () => {
177+
const freeTier = new BailianError(FREE_TIER_MESSAGE, ExitCode.GENERAL, undefined, {
178+
api: { httpStatus: 403, apiCode: FREE_TIER_ONLY_API_CODE },
179+
});
180+
const freeTierHint = JSON.parse(
181+
render(freeTier, "json", {
182+
commandPrefix: "bl",
183+
supportsFreeTierRecovery: true,
184+
configName: "prod",
185+
}),
186+
).error.hint as string;
187+
expect(freeTierHint).toContain("bl usage free --model <model> --config prod");
188+
expect(freeTierHint).toContain("bl usage freetier --off --model <model> --config prod");
189+
190+
const authHint = JSON.parse(
191+
render(new BailianError("No key", ExitCode.AUTH), "json", {
192+
commandPrefix: "bl",
193+
configName: "prod",
194+
}),
195+
).error.hint as string;
196+
expect(authHint).toContain("bl auth login --api-key <your-key> --config prod");
197+
expect(authHint).toContain("bl auth status --config prod");
198+
199+
const timeoutHint = JSON.parse(
200+
render(Object.assign(new Error("timed out"), { name: "TimeoutError" }), "json", {
201+
commandPrefix: "bl",
202+
configName: "prod",
203+
}),
204+
).error.hint as string;
205+
expect(timeoutHint).toContain("bl config show --config prod");
206+
expect(timeoutHint).toContain("bl auth status --config prod");
207+
});
208+
209+
test("recovery hints omit --config for the default profile", () => {
210+
const payload = JSON.parse(
211+
render(new BailianError("No key", ExitCode.AUTH), "json", { commandPrefix: "bl" }),
212+
).error;
213+
expect(payload.hint).toContain("bl auth status");
214+
expect(payload.hint).not.toContain("--config");
215+
});

0 commit comments

Comments
 (0)