Skip to content

Commit 8211268

Browse files
committed
fix(text,mcp): keep thinking_budget on enable_thinking retry and hint MCP activation on 404
1 parent 36ebd63 commit 8211268

12 files changed

Lines changed: 223 additions & 45 deletions

File tree

docs/agents/url-change.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
2020
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
2121
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
2222
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
23-
MCP_WEBSEARCH_PAGE BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/WebSearch
23+
MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch")
24+
mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/<serverCode>
2425
2526
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
2627
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { BailianError } from "bailian-cli-core";
2+
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
3+
4+
/** Detect MCP-not-activated / invalid 404 errors (CLI-wrapped server message). */
5+
export function isMcpNotActivated(error: unknown): boolean {
6+
if (!(error instanceof BailianError)) return false;
7+
const message = error.message;
8+
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
9+
return /|MCP|MCP_IS_INVALID/i.test(message);
10+
}
11+
12+
/** Activation hint; URL from runtime/urls.ts. */
13+
export function mcpActivateHint(serverCode: string): string {
14+
const lines = [
15+
`Activate (or re-activate) the ${serverCode} MCP in the Bailian MCP marketplace, then retry.`,
16+
];
17+
if (serverCode === "WebSearch") {
18+
lines.push(
19+
"If it was previously on SSE, cancel and activate again to upgrade to Streamable HTTP.",
20+
);
21+
}
22+
lines.push(`Open: ${mcpMarketplaceDetailPage(serverCode)}`);
23+
return lines.join("\n");
24+
}
25+
26+
/**
27+
* For not-activated errors, keep the original message / exitCode and append a hint only.
28+
* Do not replace the server error message.
29+
*/
30+
export function rethrowWithMcpActivateHint(error: unknown, serverCode: string): never {
31+
if (isMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
32+
throw new BailianError(error.message, error.exitCode, mcpActivateHint(serverCode), {
33+
cause: error,
34+
api: error.api,
35+
rawResponse: error.rawResponse,
36+
});
37+
}
38+
throw error;
39+
}

packages/commands/src/commands/mcp/call.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type ParsedFlags,
99
} from "bailian-cli-core";
1010
import { emitResult } from "bailian-cli-runtime";
11+
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
1112

1213
const CALL_FLAGS = {
1314
target: {
@@ -130,14 +131,21 @@ export default defineCommand({
130131
}
131132

132133
const client = ctx.client.mcp(url);
133-
await client.initialize();
134-
const result = await client.callTool(toolName, toolArgs);
134+
try {
135+
await client.initialize();
136+
const result = await client.callTool(toolName, toolArgs);
135137

136-
if (result.isError) {
137-
const errText = result.content.map((c) => c.text || "").join("\n");
138-
throw new BailianError(`Tool error: ${errText}`);
139-
}
138+
if (result.isError) {
139+
const errText = result.content.map((c) => c.text || "").join("\n");
140+
throw new BailianError(`Tool error: ${errText}`);
141+
}
140142

141-
emitResult(result, format);
143+
emitResult(result, format);
144+
} catch (error) {
145+
if (!flags.url) {
146+
rethrowWithMcpActivateHint(error, serverCode);
147+
}
148+
throw error;
149+
}
142150
},
143151
});

packages/commands/src/commands/mcp/tools.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { defineCommand, bailianMcpPath, detectOutputFormat } from "bailian-cli-core";
22
import { emitResult } from "bailian-cli-runtime";
3+
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
34

45
export default defineCommand({
56
description: "List tools exposed by an MCP server (tools/list)",
@@ -36,8 +37,15 @@ export default defineCommand({
3637
}
3738

3839
const client = ctx.client.mcp(url);
39-
await client.initialize();
40-
const tools = await client.listTools();
41-
emitResult({ server: code, url, tools }, format);
40+
try {
41+
await client.initialize();
42+
const tools = await client.listTools();
43+
emitResult({ server: code, url, tools }, format);
44+
} catch (error) {
45+
if (!flags.url) {
46+
rethrowWithMcpActivateHint(error, code);
47+
}
48+
throw error;
49+
}
4250
},
4351
});
Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,18 @@
1-
import { BailianError } from "bailian-cli-core";
2-
import { MCP_WEBSEARCH_PAGE } from "bailian-cli-runtime";
1+
import {
2+
isMcpNotActivated,
3+
mcpActivateHint,
4+
rethrowWithMcpActivateHint,
5+
} from "../mcp/activate-hint.ts";
36

4-
/** recoginze WebSearch MCP not activated / invalid caused 404 (CLI wrapped message from server)。 */
5-
export function isWebSearchMcpNotActivated(error: unknown): boolean {
6-
if (!(error instanceof BailianError)) return false;
7-
const message = error.message;
8-
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
9-
return /|MCP|MCP_IS_INVALID/i.test(message);
10-
}
7+
/** Detect WebSearch MCP not-activated / invalid 404 errors. */
8+
export const isWebSearchMcpNotActivated = isMcpNotActivated;
119

12-
/** activate hint; URL from runtime/urls.ts。 */
10+
/** WebSearch activation hint. */
1311
export function webSearchActivateHint(): string {
14-
return [
15-
"Activate (or re-activate) the WebSearch MCP in the Bailian MCP marketplace, then retry.",
16-
"If it was previously on SSE, cancel and activate again to upgrade to Streamable HTTP.",
17-
`Open: ${MCP_WEBSEARCH_PAGE}`,
18-
].join("\n");
12+
return mcpActivateHint("WebSearch");
1913
}
2014

21-
/**
22-
* keep original message / exitCode for not activated errors, add hint only; other errors throw as is.
23-
* do not replace server error message.
24-
*/
15+
/** Keep the original message; append a hint for WebSearch not-activated errors. */
2516
export function rethrowWithWebSearchActivateHint(error: unknown): never {
26-
if (isWebSearchMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
27-
throw new BailianError(error.message, error.exitCode, webSearchActivateHint(), {
28-
cause: error,
29-
api: error.api,
30-
rawResponse: error.rawResponse,
31-
});
32-
}
33-
throw error;
17+
rethrowWithMcpActivateHint(error, "WebSearch");
3418
}

packages/commands/src/commands/text/chat.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
parseSSE,
55
detectOutputFormat,
66
readTextFromPathOrStdin,
7-
applyChatEnableThinking,
7+
applyChatEnableThinkingWithBudget,
88
resolveChatEnableThinking,
99
withEnableThinkingRetry,
1010
type ChatMessage,
@@ -152,10 +152,10 @@ export default defineCommand({
152152
enableThinking: flags.enableThinking,
153153
stream: shouldStream,
154154
});
155-
applyChatEnableThinking(body, enableThinking);
156-
if (enableThinking === true && flags.thinkingBudget !== undefined) {
157-
body.thinking_budget = flags.thinkingBudget;
158-
}
155+
const applyThinking = (value: boolean | undefined) => {
156+
applyChatEnableThinkingWithBudget(body, value, flags.thinkingBudget);
157+
};
158+
applyThinking(enableThinking);
159159

160160
if (flags.tool) {
161161
const tools = flags.tool.map((t) => {
@@ -232,7 +232,7 @@ export default defineCommand({
232232
} else {
233233
const response = await withEnableThinkingRetry({
234234
initial: enableThinking,
235-
apply: (value) => applyChatEnableThinking(body, value),
235+
apply: applyThinking,
236236
run: () =>
237237
ctx.client.requestJson<ChatResponse>({
238238
path: chatPath(),
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, test } from "vite-plus/test";
2+
import { BailianError, ExitCode } from "bailian-cli-core";
3+
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
4+
import {
5+
isMcpNotActivated,
6+
mcpActivateHint,
7+
rethrowWithMcpActivateHint,
8+
} from "../src/commands/mcp/activate-hint.ts";
9+
10+
describe("mcp-activate-hint", () => {
11+
test("识别 404 + 未开通 / MCP不存在 / MCP_IS_INVALID", () => {
12+
expect(
13+
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP不存在或未开通")),
14+
).toBe(true);
15+
expect(
16+
isMcpNotActivated(new BailianError("MCP request failed: 404 - MCP不存在或未开通")),
17+
).toBe(true);
18+
expect(
19+
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP_IS_INVALID")),
20+
).toBe(true);
21+
});
22+
23+
test("裸 404 或非 MCP 错误不加开通判定", () => {
24+
expect(isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found"))).toBe(false);
25+
expect(isMcpNotActivated(new BailianError("MCP request failed: 405 Method Not Allowed"))).toBe(
26+
false,
27+
);
28+
expect(isMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
29+
});
30+
31+
test("hint 含对应 server 的 MCP 广场深链", () => {
32+
const serverCode = "market-cmapi00073529";
33+
expect(mcpActivateHint(serverCode)).toContain(mcpMarketplaceDetailPage(serverCode));
34+
expect(mcpActivateHint(serverCode)).toMatch(/Activate|re-activate/i);
35+
});
36+
37+
test("WebSearch hint 含 SSE 升级说明", () => {
38+
expect(mcpActivateHint("WebSearch")).toMatch(/SSE|Streamable HTTP/i);
39+
});
40+
41+
test("rethrow 保留原 message,补 hint", () => {
42+
const serverCode = "market-cmapi00073529";
43+
const original = new BailianError(
44+
"MCP request failed: 404 Not Found - MCP不存在或未开通",
45+
ExitCode.GENERAL,
46+
);
47+
try {
48+
rethrowWithMcpActivateHint(original, serverCode);
49+
expect.unreachable("should throw");
50+
} catch (error) {
51+
expect(error).toBeInstanceOf(BailianError);
52+
const wrapped = error as BailianError;
53+
expect(wrapped.message).toBe(original.message);
54+
expect(wrapped.exitCode).toBe(ExitCode.GENERAL);
55+
expect(wrapped.hint).toContain(mcpMarketplaceDetailPage(serverCode));
56+
expect(wrapped.cause).toBe(original);
57+
}
58+
});
59+
60+
test("已有 hint 或非未开通错误原样抛出", () => {
61+
const withHint = new BailianError(
62+
"MCP request failed: 404 Not Found - MCP不存在或未开通",
63+
ExitCode.GENERAL,
64+
"already hinted",
65+
);
66+
try {
67+
rethrowWithMcpActivateHint(withHint, "WebSearch");
68+
expect.unreachable("should throw");
69+
} catch (error) {
70+
expect(error).toBe(withHint);
71+
}
72+
73+
const other = new BailianError("MCP request failed: 401 Unauthorized");
74+
try {
75+
rethrowWithMcpActivateHint(other, "WebSearch");
76+
expect.unreachable("should throw");
77+
} catch (error) {
78+
expect(error).toBe(other);
79+
}
80+
});
81+
});

packages/core/src/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export {
22
adjustEnableThinkingAfterError,
33
applyChatEnableThinking,
4+
applyChatEnableThinkingWithBudget,
45
resolveChatEnableThinking,
56
withEnableThinkingRetry,
67
type EnableThinkingAdjustResult,

packages/core/src/models/thinking.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ export function applyChatEnableThinking(
5353
body.enable_thinking = true;
5454
}
5555

56+
/** Set `enable_thinking` and optionally write `thinking_budget` when enabled. */
57+
export function applyChatEnableThinkingWithBudget(
58+
body: { enable_thinking?: boolean; thinking_budget?: number },
59+
value: boolean | undefined,
60+
thinkingBudget?: number,
61+
): void {
62+
applyChatEnableThinking(body, value);
63+
if (value === true && thinkingBudget !== undefined) {
64+
body.thinking_budget = thinkingBudget;
65+
}
66+
}
67+
5668
function errorMessageOf(error: unknown): string {
5769
if (error instanceof Error) return error.message;
5870
return String(error);

packages/core/tests/thinking.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { expect, test } from "vite-plus/test";
22
import {
33
adjustEnableThinkingAfterError,
44
applyChatEnableThinking,
5+
applyChatEnableThinkingWithBudget,
56
resolveChatEnableThinking,
67
withEnableThinkingRetry,
78
} from "../src/models/thinking.ts";
@@ -123,6 +124,43 @@ test("withEnableThinkingRetry:must-be-false 时从 omit 重试为 false", asyn
123124
expect(values).toEqual([undefined, false]);
124125
});
125126

127+
test("applyChatEnableThinkingWithBudget:仅在 enable_thinking=true 时写入 budget", () => {
128+
const body: { enable_thinking?: boolean; thinking_budget?: number } = {};
129+
applyChatEnableThinkingWithBudget(body, false, 2048);
130+
expect(body.enable_thinking).toBe(false);
131+
expect(body).not.toHaveProperty("thinking_budget");
132+
133+
applyChatEnableThinkingWithBudget(body, undefined, 2048);
134+
expect(body).not.toHaveProperty("enable_thinking");
135+
expect(body).not.toHaveProperty("thinking_budget");
136+
137+
applyChatEnableThinkingWithBudget(body, true, 2048);
138+
expect(body.enable_thinking).toBe(true);
139+
expect(body.thinking_budget).toBe(2048);
140+
});
141+
142+
test("withEnableThinkingRetry:restricted-to-true 重试时保留 thinking_budget", async () => {
143+
const body: { enable_thinking?: boolean; thinking_budget?: number } = {};
144+
let calls = 0;
145+
146+
const result = await withEnableThinkingRetry({
147+
initial: false,
148+
apply: (value) => applyChatEnableThinkingWithBudget(body, value, 2048),
149+
run: async () => {
150+
calls += 1;
151+
if (calls === 1) {
152+
throw new Error("The value of the enable_thinking parameter is restricted to True.");
153+
}
154+
return "ok";
155+
},
156+
});
157+
158+
expect(result).toBe("ok");
159+
expect(calls).toBe(2);
160+
expect(body.enable_thinking).toBe(true);
161+
expect(body.thinking_budget).toBe(2048);
162+
});
163+
126164
test("withEnableThinkingRetry:无关错误原样抛出", async () => {
127165
await expect(
128166
withEnableThinkingRetry({

0 commit comments

Comments
 (0)