Skip to content

Commit 3b3cb92

Browse files
committed
fix(runtime): preserve explicit --config default in recovery hints
1 parent f694e37 commit 3b3cb92

10 files changed

Lines changed: 120 additions & 50 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +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 / 未指定不加)
112+
- [ ] 恢复命令经 `recoveryCommand` 渲染;Profile 名用 `resolveRecoveryConfigName`(显式 `--config default` 保留,隐式 default 不加)
113113
- [ ] URL 必须是 `import { API_KEY_PAGE } from "./urls.ts"`
114114

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

‎packages/commands/src/commands/knowledge/chunk-delete.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,12 @@ export default defineCommand({
103103
deletedCount += batchIds.length;
104104
completedBatches.push(batchIds);
105105
} catch (error) {
106-
const original = normalizeError(error, formatCommand(ctx.identity), ctx.localize);
106+
const original = normalizeError(
107+
error,
108+
formatCommand(ctx.identity),
109+
ctx.localize,
110+
ctx.settings.configName,
111+
);
107112
const failedBatch = completedBatches.length + 1;
108113
const progress = [
109114
ctx.localize({

‎packages/commands/src/commands/usage/freetier.ts‎

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -58,30 +58,12 @@ export default defineCommand({
5858
},
5959
},
6060
exampleArgs: [
61-
{
62-
"en-US": "--model qwen3-max --yes",
63-
"zh-CN": "--model qwen3-max --yes",
64-
},
65-
{
66-
"en-US": "--model qwen3-max,qwen-turbo --yes",
67-
"zh-CN": "--model qwen3-max,qwen-turbo --yes",
68-
},
69-
{
70-
"en-US": "--all --yes",
71-
"zh-CN": "--all --yes",
72-
},
73-
{
74-
"en-US": "--on --model qwen3-max --yes",
75-
"zh-CN": "--on --model qwen3-max --yes",
76-
},
77-
{
78-
"en-US": "--off --model qwen3-max --yes",
79-
"zh-CN": "--off --model qwen3-max --yes",
80-
},
81-
{
82-
"en-US": "--off --all --yes",
83-
"zh-CN": "--off --all --yes",
84-
},
61+
"--model qwen3-max --yes",
62+
"--model qwen3-max,qwen-turbo --yes",
63+
"--all --yes",
64+
"--on --model qwen3-max --yes",
65+
"--off --model qwen3-max --yes",
66+
"--off --all --yes",
8567
],
8668
notes: [
8769
{

‎packages/commands/tests/e2e/usage-freetier.e2e.test.ts‎

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { describe, expect, test } from "vite-plus/test";
2-
import { runCommandE2e, runCommandHelp } from "./helpers.ts";
2+
import { parseStdoutJson, runCommandHelp, runCommandInProcess } from "./helpers.ts";
33
import { USAGE_ROUTES } from "./topic-routes.ts";
44

5+
/** Clear host OpenAPI env so console auth cannot exchange a local AK/SK into a session. */
6+
const CLEAR_OPENAPI_ENV = {
7+
BAILIAN_ACCESS_KEY_ID: undefined,
8+
BAILIAN_ACCESS_KEY_SECRET: undefined,
9+
ALIBABA_CLOUD_ACCESS_KEY_ID: undefined,
10+
ALIBABA_CLOUD_ACCESS_KEY_SECRET: undefined,
11+
};
12+
513
describe("e2e: usage freetier", () => {
614
test("usage freetier --help 展示 risk、--yes 与确认示例", async () => {
715
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
@@ -19,7 +27,7 @@ describe("e2e: usage freetier", () => {
1927
});
2028

2129
test("usage freetier 无 --yes 返回确认请求 (7)", async () => {
22-
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
30+
const { stderr, exitCode } = await runCommandInProcess(USAGE_ROUTES, [
2331
"usage",
2432
"freetier",
2533
"--model",
@@ -34,20 +42,54 @@ describe("e2e: usage freetier", () => {
3442
});
3543

3644
test("usage freetier --yes 通过确认门槛后落到 AUTH (3)", async () => {
37-
// Clear OpenAPI env so console auth cannot exchange a host AK/SK into a session.
38-
const { stderr, exitCode } = await runCommandE2e(
45+
const { stderr, exitCode } = await runCommandInProcess(
3946
USAGE_ROUTES,
4047
["usage", "freetier", "--model", "qwen-plus", "--yes", "--output", "json"],
41-
{
42-
BAILIAN_ACCESS_KEY_ID: undefined,
43-
BAILIAN_ACCESS_KEY_SECRET: undefined,
44-
ALIBABA_CLOUD_ACCESS_KEY_ID: undefined,
45-
ALIBABA_CLOUD_ACCESS_KEY_SECRET: undefined,
46-
},
48+
CLEAR_OPENAPI_ENV,
4749
);
4850
expect(exitCode).toBe(3);
4951
expect(JSON.parse(stderr)).toMatchObject({
5052
error: { code: 3 },
5153
});
5254
});
55+
56+
test("usage freetier --dry-run 跳过确认并输出计划(零网络)", async () => {
57+
const { stdout, stderr, exitCode } = await runCommandInProcess(USAGE_ROUTES, [
58+
"usage",
59+
"freetier",
60+
"--model",
61+
"qwen-plus",
62+
"--dry-run",
63+
"--output",
64+
"json",
65+
]);
66+
expect(exitCode, stderr).toBe(0);
67+
expect(stderr).not.toContain("requires_confirmation");
68+
const data = parseStdoutJson<{
69+
api: string;
70+
data: { BatchActivateFreeTierOnlyRequest: { models: string[] } };
71+
}>(stdout);
72+
expect(data.api).toBe("zeldaEasy.bailian-commerce.freeTrial.batchActivateFreeTierOnly");
73+
expect(data.data.BatchActivateFreeTierOnlyRequest.models).toEqual(["qwen-plus"]);
74+
});
75+
76+
test("usage freetier --off --dry-run 输出停用计划", async () => {
77+
const { stdout, stderr, exitCode } = await runCommandInProcess(USAGE_ROUTES, [
78+
"usage",
79+
"freetier",
80+
"--off",
81+
"--model",
82+
"qwen-plus",
83+
"--dry-run",
84+
"--output",
85+
"json",
86+
]);
87+
expect(exitCode, stderr).toBe(0);
88+
const data = parseStdoutJson<{
89+
api: string;
90+
data: { BatchDeactivateFreeTierOnlyRequest: { models: string[] } };
91+
}>(stdout);
92+
expect(data.api).toBe("zeldaEasy.bailian-commerce.freeTrial.batchDeactivateFreeTierOnly");
93+
expect(data.data.BatchDeactivateFreeTierOnlyRequest.models).toEqual(["qwen-plus"]);
94+
});
5395
});

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import {
4040
Client,
4141
} from "bailian-cli-core";
4242
import { setupProxyFromEnv } from "./proxy.ts";
43-
import { handleError } from "./error-handler.ts";
43+
import { handleError, resolveRecoveryConfigName } from "./error-handler.ts";
4444
import { type OutputReporter, createOutputReporter } from "./output/reporter.ts";
4545
import { loadCommandPacks } from "./command-packs/load.ts";
4646
import { createCommandPackManager } from "./command-packs/manager.ts";
@@ -346,13 +346,14 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
346346
const cleanup = installProcessHandlers(() => reporter, localize);
347347
try {
348348
const sources = buildSources(flags, (warning) => warnings.push(warning));
349+
const recoveryConfigName = resolveRecoveryConfigName(sources);
349350
translator = createTranslator(sources.file.language ?? DEFAULT_LANGUAGE);
350351
reporter = createOutputReporter({
351352
settings: buildOutputSettings(sources),
352353
localize,
353354
commandPrefix,
354355
supportsFreeTierRecovery,
355-
configName: sources.configName,
356+
configName: recoveryConfigName,
356357
});
357358
setupProxyFromEnv();
358359
const registry = await getRegistry(translator);
@@ -362,7 +363,7 @@ export function createCli(commands: Record<string, AnyCommand>, opts: CliOptions
362363
localize,
363364
commandPrefix,
364365
supportsFreeTierRecovery,
365-
configName: sources.configName,
366+
configName: recoveryConfigName,
366367
});
367368
for (const warning of warnings.splice(0)) reporter.output.warning(warning);
368369
return reporter;

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,23 @@ type Localize = (text: LocalizedText) => string;
3434
const english: Localize = (text) => (typeof text === "string" ? text : text["en-US"]);
3535

3636
/**
37-
* 恢复命令统一入口:命名 Profile 激活时追加 `--config <name>`,
38-
* 避免用户按 hint 操作打到错误的凭证/计费配置。
37+
* Config name for recovery hints (does not change IO-layer `configName` semantics).
38+
* Explicit `--config default` is normalized to undefined in the loader; keep
39+
* `"default"` here so copying the hint does not fall back to an active named profile.
40+
*/
41+
export function resolveRecoveryConfigName(sources: {
42+
flags: { config?: unknown };
43+
configName?: string;
44+
}): string | undefined {
45+
if (sources.flags.config !== undefined) {
46+
return sources.configName ?? "default";
47+
}
48+
return sources.configName;
49+
}
50+
51+
/**
52+
* Format a recovery command; append `--config <name>` when a profile must be pinned
53+
* so following the hint does not hit the wrong credentials/billing config.
3954
*/
4055
function recoveryCommand(commandPrefix: string, args: string, configName?: string): string {
4156
const withConfig = configName ? `${args} --config ${configName}` : args;
@@ -56,7 +71,10 @@ export interface ErrorHintOptions {
5671
* under this product's commandPrefix. Derived from the product command map.
5772
*/
5873
supportsFreeTierRecovery?: boolean;
59-
/** 当前命名 Profile;未指定或 default 时为 undefined,恢复命令不加 `--config`。 */
74+
/**
75+
* Profile to pin in recovery commands (via `resolveRecoveryConfigName`).
76+
* Implicit default is undefined; explicit `--config default` is `"default"`.
77+
*/
6078
configName?: string;
6179
}
6280

@@ -79,7 +97,7 @@ function freeTierRecoveryHint(
7997
configName?: string,
8098
): string {
8199
const statusCommand = recoveryCommand(commandPrefix, "usage free --model <model>", configName);
82-
// 不带 --yes:调用方先撞确认门槛(exit 7),再按 skill 规范向用户确认。
100+
// Omit --yes so the caller hits the confirmation gate (exit 7) first.
83101
const disableCommand = recoveryCommand(
84102
commandPrefix,
85103
"usage freetier --off --model <model>",
@@ -120,7 +138,7 @@ export function errorHint(
120138
}
121139
// Append free-tier recovery before the AUTH-only early return so GENERAL
122140
// service errors (and async paths that already set a hint) still get guidance.
123-
// 已含恢复指引(命令自带或上游已追加)时不重复追加。
141+
// Skip when recovery guidance is already present (command-owned or upstream).
124142
if (
125143
options.supportsFreeTierRecovery &&
126144
isFreeTierOnlyExhausted(error) &&

‎packages/runtime/src/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export type { ParsePathResult } from "./args.ts";
2727

2828
// Process-level setup / error handling
2929
export { setupProxyFromEnv } from "./proxy.ts";
30-
export { handleError, normalizeError, errorHint, type ErrorHintOptions } from "./error-handler.ts";
30+
export { handleError, normalizeError } from "./error-handler.ts";
3131
export { CLI_VERSION } from "./version.ts";
3232

3333
// Console URLs referenced by commands (e.g. auth/status, banner)

‎packages/runtime/src/middleware.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { consoleRecoveryHint } from "./error-handler.ts";
1+
import { consoleRecoveryHint, resolveRecoveryConfigName } from "./error-handler.ts";
22
import type {
33
AnyCommand,
44
CommandOutput,
@@ -190,7 +190,10 @@ export const authStage: Middleware = async (ctx, next) => {
190190
const base = {
191191
identity: ctx.identity,
192192
settings,
193-
consoleRecoveryHint: consoleRecoveryHint(formatCommand(ctx.identity), sources.configName),
193+
consoleRecoveryHint: consoleRecoveryHint(
194+
formatCommand(ctx.identity),
195+
resolveRecoveryConfigName(sources),
196+
),
194197
debug: (diagnostic: Parameters<CommandOutput["debug"]>[0]) => ctx.output.debug(diagnostic),
195198
...resolveModelBaseUrlState(sources),
196199
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ 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。 */
28+
/** Profile pinned in recovery hints (via resolveRecoveryConfigName). */
2929
configName?: string;
3030
}
3131

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BailianError, ExitCode, mapApiError } from "bailian-cli-core";
22
import { expect, test, vi } from "vite-plus/test";
3-
import { errorHint, handleError } from "../src/error-handler.ts";
3+
import { errorHint, handleError, resolveRecoveryConfigName } from "../src/error-handler.ts";
44
import { createOutputReporter } from "../src/output/reporter.ts";
55
import { ConfirmationRequiredError } from "../src/confirm.ts";
66

@@ -206,10 +206,29 @@ test("recovery hints append --config when a named profile is active", () => {
206206
expect(timeoutHint).toContain("bl auth status --config prod");
207207
});
208208

209-
test("recovery hints omit --config for the default profile", () => {
209+
test("recovery hints omit --config for the implicit default profile", () => {
210210
const payload = JSON.parse(
211211
render(new BailianError("No key", ExitCode.AUTH), "json", { commandPrefix: "bl" }),
212212
).error;
213213
expect(payload.hint).toContain("bl auth status");
214214
expect(payload.hint).not.toContain("--config");
215215
});
216+
217+
test("recovery hints keep explicit --config default when active profile is named", () => {
218+
// Loader normalizes explicit default to undefined; resolveRecoveryConfigName must restore it.
219+
expect(resolveRecoveryConfigName({ flags: { config: "default" }, configName: undefined })).toBe(
220+
"default",
221+
);
222+
expect(resolveRecoveryConfigName({ flags: { config: "prod" }, configName: "prod" })).toBe("prod");
223+
expect(resolveRecoveryConfigName({ flags: {}, configName: "prod" })).toBe("prod");
224+
expect(resolveRecoveryConfigName({ flags: {}, configName: undefined })).toBeUndefined();
225+
226+
const hint = JSON.parse(
227+
render(new BailianError("No key", ExitCode.AUTH), "json", {
228+
commandPrefix: "bl",
229+
configName: "default",
230+
}),
231+
).error.hint as string;
232+
expect(hint).toContain("bl auth login --api-key <your-key> --config default");
233+
expect(hint).toContain("bl auth status --config default");
234+
});

0 commit comments

Comments
 (0)