mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(text,mcp): keep thinking_budget on enable_thinking retry and hint MCP activation on 404
This commit is contained in:
@@ -20,7 +20,8 @@ runtime/src/urls.ts ← 用户面控制台 URL(cn-only)
|
||||
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
|
||||
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
|
||||
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
|
||||
MCP_WEBSEARCH_PAGE BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/WebSearch
|
||||
MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch")
|
||||
mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/<serverCode>
|
||||
|
||||
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
|
||||
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BailianError } from "bailian-cli-core";
|
||||
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
|
||||
|
||||
/** Detect MCP-not-activated / invalid 404 errors (CLI-wrapped server message). */
|
||||
export function isMcpNotActivated(error: unknown): boolean {
|
||||
if (!(error instanceof BailianError)) return false;
|
||||
const message = error.message;
|
||||
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
|
||||
return /未开通|MCP不存在|MCP_IS_INVALID/i.test(message);
|
||||
}
|
||||
|
||||
/** Activation hint; URL from runtime/urls.ts. */
|
||||
export function mcpActivateHint(serverCode: string): string {
|
||||
const lines = [
|
||||
`Activate (or re-activate) the ${serverCode} MCP in the Bailian MCP marketplace, then retry.`,
|
||||
];
|
||||
if (serverCode === "WebSearch") {
|
||||
lines.push(
|
||||
"If it was previously on SSE, cancel and activate again to upgrade to Streamable HTTP.",
|
||||
);
|
||||
}
|
||||
lines.push(`Open: ${mcpMarketplaceDetailPage(serverCode)}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* For not-activated errors, keep the original message / exitCode and append a hint only.
|
||||
* Do not replace the server error message.
|
||||
*/
|
||||
export function rethrowWithMcpActivateHint(error: unknown, serverCode: string): never {
|
||||
if (isMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
|
||||
throw new BailianError(error.message, error.exitCode, mcpActivateHint(serverCode), {
|
||||
cause: error,
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type ParsedFlags,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
|
||||
|
||||
const CALL_FLAGS = {
|
||||
target: {
|
||||
@@ -130,14 +131,21 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(url);
|
||||
await client.initialize();
|
||||
const result = await client.callTool(toolName, toolArgs);
|
||||
try {
|
||||
await client.initialize();
|
||||
const result = await client.callTool(toolName, toolArgs);
|
||||
|
||||
if (result.isError) {
|
||||
const errText = result.content.map((c) => c.text || "").join("\n");
|
||||
throw new BailianError(`Tool error: ${errText}`);
|
||||
if (result.isError) {
|
||||
const errText = result.content.map((c) => c.text || "").join("\n");
|
||||
throw new BailianError(`Tool error: ${errText}`);
|
||||
}
|
||||
|
||||
emitResult(result, format);
|
||||
} catch (error) {
|
||||
if (!flags.url) {
|
||||
rethrowWithMcpActivateHint(error, serverCode);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
emitResult(result, format);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { defineCommand, bailianMcpPath, detectOutputFormat } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
|
||||
|
||||
export default defineCommand({
|
||||
description: "List tools exposed by an MCP server (tools/list)",
|
||||
@@ -36,8 +37,15 @@ export default defineCommand({
|
||||
}
|
||||
|
||||
const client = ctx.client.mcp(url);
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
emitResult({ server: code, url, tools }, format);
|
||||
try {
|
||||
await client.initialize();
|
||||
const tools = await client.listTools();
|
||||
emitResult({ server: code, url, tools }, format);
|
||||
} catch (error) {
|
||||
if (!flags.url) {
|
||||
rethrowWithMcpActivateHint(error, code);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import { BailianError } from "bailian-cli-core";
|
||||
import { MCP_WEBSEARCH_PAGE } from "bailian-cli-runtime";
|
||||
import {
|
||||
isMcpNotActivated,
|
||||
mcpActivateHint,
|
||||
rethrowWithMcpActivateHint,
|
||||
} from "../mcp/activate-hint.ts";
|
||||
|
||||
/** recoginze WebSearch MCP not activated / invalid caused 404 (CLI wrapped message from server)。 */
|
||||
export function isWebSearchMcpNotActivated(error: unknown): boolean {
|
||||
if (!(error instanceof BailianError)) return false;
|
||||
const message = error.message;
|
||||
if (!/MCP request failed:\s*404\b/i.test(message)) return false;
|
||||
return /未开通|MCP不存在|MCP_IS_INVALID/i.test(message);
|
||||
}
|
||||
/** Detect WebSearch MCP not-activated / invalid 404 errors. */
|
||||
export const isWebSearchMcpNotActivated = isMcpNotActivated;
|
||||
|
||||
/** activate hint; URL from runtime/urls.ts。 */
|
||||
/** WebSearch activation hint. */
|
||||
export function webSearchActivateHint(): string {
|
||||
return [
|
||||
"Activate (or re-activate) the WebSearch MCP in the Bailian MCP marketplace, then retry.",
|
||||
"If it was previously on SSE, cancel and activate again to upgrade to Streamable HTTP.",
|
||||
`Open: ${MCP_WEBSEARCH_PAGE}`,
|
||||
].join("\n");
|
||||
return mcpActivateHint("WebSearch");
|
||||
}
|
||||
|
||||
/**
|
||||
* keep original message / exitCode for not activated errors, add hint only; other errors throw as is.
|
||||
* do not replace server error message.
|
||||
*/
|
||||
/** Keep the original message; append a hint for WebSearch not-activated errors. */
|
||||
export function rethrowWithWebSearchActivateHint(error: unknown): never {
|
||||
if (isWebSearchMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
|
||||
throw new BailianError(error.message, error.exitCode, webSearchActivateHint(), {
|
||||
cause: error,
|
||||
api: error.api,
|
||||
rawResponse: error.rawResponse,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
rethrowWithMcpActivateHint(error, "WebSearch");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
parseSSE,
|
||||
detectOutputFormat,
|
||||
readTextFromPathOrStdin,
|
||||
applyChatEnableThinking,
|
||||
applyChatEnableThinkingWithBudget,
|
||||
resolveChatEnableThinking,
|
||||
withEnableThinkingRetry,
|
||||
type ChatMessage,
|
||||
@@ -152,10 +152,10 @@ export default defineCommand({
|
||||
enableThinking: flags.enableThinking,
|
||||
stream: shouldStream,
|
||||
});
|
||||
applyChatEnableThinking(body, enableThinking);
|
||||
if (enableThinking === true && flags.thinkingBudget !== undefined) {
|
||||
body.thinking_budget = flags.thinkingBudget;
|
||||
}
|
||||
const applyThinking = (value: boolean | undefined) => {
|
||||
applyChatEnableThinkingWithBudget(body, value, flags.thinkingBudget);
|
||||
};
|
||||
applyThinking(enableThinking);
|
||||
|
||||
if (flags.tool) {
|
||||
const tools = flags.tool.map((t) => {
|
||||
@@ -232,7 +232,7 @@ export default defineCommand({
|
||||
} else {
|
||||
const response = await withEnableThinkingRetry({
|
||||
initial: enableThinking,
|
||||
apply: (value) => applyChatEnableThinking(body, value),
|
||||
apply: applyThinking,
|
||||
run: () =>
|
||||
ctx.client.requestJson<ChatResponse>({
|
||||
path: chatPath(),
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { BailianError, ExitCode } from "bailian-cli-core";
|
||||
import { mcpMarketplaceDetailPage } from "bailian-cli-runtime";
|
||||
import {
|
||||
isMcpNotActivated,
|
||||
mcpActivateHint,
|
||||
rethrowWithMcpActivateHint,
|
||||
} from "../src/commands/mcp/activate-hint.ts";
|
||||
|
||||
describe("mcp-activate-hint", () => {
|
||||
test("识别 404 + 未开通 / MCP不存在 / MCP_IS_INVALID", () => {
|
||||
expect(
|
||||
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP不存在或未开通")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isMcpNotActivated(new BailianError("MCP request failed: 404 - MCP不存在或未开通")),
|
||||
).toBe(true);
|
||||
expect(
|
||||
isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found - MCP_IS_INVALID")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("裸 404 或非 MCP 错误不加开通判定", () => {
|
||||
expect(isMcpNotActivated(new BailianError("MCP request failed: 404 Not Found"))).toBe(false);
|
||||
expect(isMcpNotActivated(new BailianError("MCP request failed: 405 Method Not Allowed"))).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isMcpNotActivated(new Error("MCP不存在或未开通"))).toBe(false);
|
||||
});
|
||||
|
||||
test("hint 含对应 server 的 MCP 广场深链", () => {
|
||||
const serverCode = "market-cmapi00073529";
|
||||
expect(mcpActivateHint(serverCode)).toContain(mcpMarketplaceDetailPage(serverCode));
|
||||
expect(mcpActivateHint(serverCode)).toMatch(/Activate|re-activate/i);
|
||||
});
|
||||
|
||||
test("WebSearch hint 含 SSE 升级说明", () => {
|
||||
expect(mcpActivateHint("WebSearch")).toMatch(/SSE|Streamable HTTP/i);
|
||||
});
|
||||
|
||||
test("rethrow 保留原 message,补 hint", () => {
|
||||
const serverCode = "market-cmapi00073529";
|
||||
const original = new BailianError(
|
||||
"MCP request failed: 404 Not Found - MCP不存在或未开通",
|
||||
ExitCode.GENERAL,
|
||||
);
|
||||
try {
|
||||
rethrowWithMcpActivateHint(original, serverCode);
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(BailianError);
|
||||
const wrapped = error as BailianError;
|
||||
expect(wrapped.message).toBe(original.message);
|
||||
expect(wrapped.exitCode).toBe(ExitCode.GENERAL);
|
||||
expect(wrapped.hint).toContain(mcpMarketplaceDetailPage(serverCode));
|
||||
expect(wrapped.cause).toBe(original);
|
||||
}
|
||||
});
|
||||
|
||||
test("已有 hint 或非未开通错误原样抛出", () => {
|
||||
const withHint = new BailianError(
|
||||
"MCP request failed: 404 Not Found - MCP不存在或未开通",
|
||||
ExitCode.GENERAL,
|
||||
"already hinted",
|
||||
);
|
||||
try {
|
||||
rethrowWithMcpActivateHint(withHint, "WebSearch");
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(withHint);
|
||||
}
|
||||
|
||||
const other = new BailianError("MCP request failed: 401 Unauthorized");
|
||||
try {
|
||||
rethrowWithMcpActivateHint(other, "WebSearch");
|
||||
expect.unreachable("should throw");
|
||||
} catch (error) {
|
||||
expect(error).toBe(other);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
export {
|
||||
adjustEnableThinkingAfterError,
|
||||
applyChatEnableThinking,
|
||||
applyChatEnableThinkingWithBudget,
|
||||
resolveChatEnableThinking,
|
||||
withEnableThinkingRetry,
|
||||
type EnableThinkingAdjustResult,
|
||||
|
||||
@@ -53,6 +53,18 @@ export function applyChatEnableThinking(
|
||||
body.enable_thinking = true;
|
||||
}
|
||||
|
||||
/** Set `enable_thinking` and optionally write `thinking_budget` when enabled. */
|
||||
export function applyChatEnableThinkingWithBudget(
|
||||
body: { enable_thinking?: boolean; thinking_budget?: number },
|
||||
value: boolean | undefined,
|
||||
thinkingBudget?: number,
|
||||
): void {
|
||||
applyChatEnableThinking(body, value);
|
||||
if (value === true && thinkingBudget !== undefined) {
|
||||
body.thinking_budget = thinkingBudget;
|
||||
}
|
||||
}
|
||||
|
||||
function errorMessageOf(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from "vite-plus/test";
|
||||
import {
|
||||
adjustEnableThinkingAfterError,
|
||||
applyChatEnableThinking,
|
||||
applyChatEnableThinkingWithBudget,
|
||||
resolveChatEnableThinking,
|
||||
withEnableThinkingRetry,
|
||||
} from "../src/models/thinking.ts";
|
||||
@@ -123,6 +124,43 @@ test("withEnableThinkingRetry:must-be-false 时从 omit 重试为 false", asyn
|
||||
expect(values).toEqual([undefined, false]);
|
||||
});
|
||||
|
||||
test("applyChatEnableThinkingWithBudget:仅在 enable_thinking=true 时写入 budget", () => {
|
||||
const body: { enable_thinking?: boolean; thinking_budget?: number } = {};
|
||||
applyChatEnableThinkingWithBudget(body, false, 2048);
|
||||
expect(body.enable_thinking).toBe(false);
|
||||
expect(body).not.toHaveProperty("thinking_budget");
|
||||
|
||||
applyChatEnableThinkingWithBudget(body, undefined, 2048);
|
||||
expect(body).not.toHaveProperty("enable_thinking");
|
||||
expect(body).not.toHaveProperty("thinking_budget");
|
||||
|
||||
applyChatEnableThinkingWithBudget(body, true, 2048);
|
||||
expect(body.enable_thinking).toBe(true);
|
||||
expect(body.thinking_budget).toBe(2048);
|
||||
});
|
||||
|
||||
test("withEnableThinkingRetry:restricted-to-true 重试时保留 thinking_budget", async () => {
|
||||
const body: { enable_thinking?: boolean; thinking_budget?: number } = {};
|
||||
let calls = 0;
|
||||
|
||||
const result = await withEnableThinkingRetry({
|
||||
initial: false,
|
||||
apply: (value) => applyChatEnableThinkingWithBudget(body, value, 2048),
|
||||
run: async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) {
|
||||
throw new Error("The value of the enable_thinking parameter is restricted to True.");
|
||||
}
|
||||
return "ok";
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe("ok");
|
||||
expect(calls).toBe(2);
|
||||
expect(body.enable_thinking).toBe(true);
|
||||
expect(body.thinking_budget).toBe(2048);
|
||||
});
|
||||
|
||||
test("withEnableThinkingRetry:无关错误原样抛出", async () => {
|
||||
await expect(
|
||||
withEnableThinkingRetry({
|
||||
|
||||
@@ -35,6 +35,7 @@ export {
|
||||
API_KEY_PAGE,
|
||||
TOKEN_PLAN_PAGE,
|
||||
MCP_WEBSEARCH_PAGE,
|
||||
mcpMarketplaceDetailPage,
|
||||
VOICE_TTS_PAGE,
|
||||
} from "./urls.ts";
|
||||
|
||||
|
||||
@@ -18,11 +18,16 @@ export const API_KEY_PAGE = `${BAILIAN_CONSOLE}/?tab=app#/api-key`;
|
||||
/** Direct deep link to the Token Plan subscription overview and API key entry. */
|
||||
export const TOKEN_PLAN_PAGE = `${BAILIAN_CONSOLE_ROOT}/cn-beijing?tab=plan#/efm/subscription/overview`;
|
||||
|
||||
/** MCP marketplace detail page for a server code (e.g. WebSearch, market-cmapi00073529). */
|
||||
export function mcpMarketplaceDetailPage(serverCode: string): string {
|
||||
return `${BAILIAN_CONSOLE}?tab=mcp#/mcp-market/detail/${serverCode}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP marketplace detail for the built-in WebSearch server.
|
||||
* Users must activate (or re-activate for Streamable HTTP) before `search web` works.
|
||||
*/
|
||||
export const MCP_WEBSEARCH_PAGE = `${BAILIAN_CONSOLE}?tab=mcp#/mcp-market/detail/WebSearch`;
|
||||
export const MCP_WEBSEARCH_PAGE = mcpMarketplaceDetailPage("WebSearch");
|
||||
|
||||
/** Voice TTS experience center — browse system and custom voices. */
|
||||
export const VOICE_TTS_PAGE = "https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list";
|
||||
|
||||
Reference in New Issue
Block a user