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
|
BAILIAN_CONSOLE BAILIAN_CONSOLE_ROOT/cn-beijing
|
||||||
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
|
API_KEY_PAGE BAILIAN_CONSOLE/?tab=app#/api-key
|
||||||
TOKEN_PLAN_PAGE BAILIAN_CONSOLE_ROOT/cn-beijing?tab=plan#/efm/subscription/overview
|
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)
|
core/files/upload.ts ← 文件上传 endpoint(cn-pinned)
|
||||||
UPLOAD_API ${REGIONS.cn}/api/v1/uploads
|
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,
|
type ParsedFlags,
|
||||||
} from "bailian-cli-core";
|
} from "bailian-cli-core";
|
||||||
import { emitResult } from "bailian-cli-runtime";
|
import { emitResult } from "bailian-cli-runtime";
|
||||||
|
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
|
||||||
|
|
||||||
const CALL_FLAGS = {
|
const CALL_FLAGS = {
|
||||||
target: {
|
target: {
|
||||||
@@ -130,14 +131,21 @@ export default defineCommand({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const client = ctx.client.mcp(url);
|
const client = ctx.client.mcp(url);
|
||||||
await client.initialize();
|
try {
|
||||||
const result = await client.callTool(toolName, toolArgs);
|
await client.initialize();
|
||||||
|
const result = await client.callTool(toolName, toolArgs);
|
||||||
|
|
||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
const errText = result.content.map((c) => c.text || "").join("\n");
|
const errText = result.content.map((c) => c.text || "").join("\n");
|
||||||
throw new BailianError(`Tool error: ${errText}`);
|
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 { defineCommand, bailianMcpPath, detectOutputFormat } from "bailian-cli-core";
|
||||||
import { emitResult } from "bailian-cli-runtime";
|
import { emitResult } from "bailian-cli-runtime";
|
||||||
|
import { rethrowWithMcpActivateHint } from "./activate-hint.ts";
|
||||||
|
|
||||||
export default defineCommand({
|
export default defineCommand({
|
||||||
description: "List tools exposed by an MCP server (tools/list)",
|
description: "List tools exposed by an MCP server (tools/list)",
|
||||||
@@ -36,8 +37,15 @@ export default defineCommand({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const client = ctx.client.mcp(url);
|
const client = ctx.client.mcp(url);
|
||||||
await client.initialize();
|
try {
|
||||||
const tools = await client.listTools();
|
await client.initialize();
|
||||||
emitResult({ server: code, url, tools }, format);
|
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 {
|
||||||
import { MCP_WEBSEARCH_PAGE } from "bailian-cli-runtime";
|
isMcpNotActivated,
|
||||||
|
mcpActivateHint,
|
||||||
|
rethrowWithMcpActivateHint,
|
||||||
|
} from "../mcp/activate-hint.ts";
|
||||||
|
|
||||||
/** recoginze WebSearch MCP not activated / invalid caused 404 (CLI wrapped message from server)。 */
|
/** Detect WebSearch MCP not-activated / invalid 404 errors. */
|
||||||
export function isWebSearchMcpNotActivated(error: unknown): boolean {
|
export const isWebSearchMcpNotActivated = isMcpNotActivated;
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** activate hint; URL from runtime/urls.ts。 */
|
/** WebSearch activation hint. */
|
||||||
export function webSearchActivateHint(): string {
|
export function webSearchActivateHint(): string {
|
||||||
return [
|
return mcpActivateHint("WebSearch");
|
||||||
"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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Keep the original message; append a hint for WebSearch not-activated errors. */
|
||||||
* keep original message / exitCode for not activated errors, add hint only; other errors throw as is.
|
|
||||||
* do not replace server error message.
|
|
||||||
*/
|
|
||||||
export function rethrowWithWebSearchActivateHint(error: unknown): never {
|
export function rethrowWithWebSearchActivateHint(error: unknown): never {
|
||||||
if (isWebSearchMcpNotActivated(error) && error instanceof BailianError && !error.hint) {
|
rethrowWithMcpActivateHint(error, "WebSearch");
|
||||||
throw new BailianError(error.message, error.exitCode, webSearchActivateHint(), {
|
|
||||||
cause: error,
|
|
||||||
api: error.api,
|
|
||||||
rawResponse: error.rawResponse,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
parseSSE,
|
parseSSE,
|
||||||
detectOutputFormat,
|
detectOutputFormat,
|
||||||
readTextFromPathOrStdin,
|
readTextFromPathOrStdin,
|
||||||
applyChatEnableThinking,
|
applyChatEnableThinkingWithBudget,
|
||||||
resolveChatEnableThinking,
|
resolveChatEnableThinking,
|
||||||
withEnableThinkingRetry,
|
withEnableThinkingRetry,
|
||||||
type ChatMessage,
|
type ChatMessage,
|
||||||
@@ -152,10 +152,10 @@ export default defineCommand({
|
|||||||
enableThinking: flags.enableThinking,
|
enableThinking: flags.enableThinking,
|
||||||
stream: shouldStream,
|
stream: shouldStream,
|
||||||
});
|
});
|
||||||
applyChatEnableThinking(body, enableThinking);
|
const applyThinking = (value: boolean | undefined) => {
|
||||||
if (enableThinking === true && flags.thinkingBudget !== undefined) {
|
applyChatEnableThinkingWithBudget(body, value, flags.thinkingBudget);
|
||||||
body.thinking_budget = flags.thinkingBudget;
|
};
|
||||||
}
|
applyThinking(enableThinking);
|
||||||
|
|
||||||
if (flags.tool) {
|
if (flags.tool) {
|
||||||
const tools = flags.tool.map((t) => {
|
const tools = flags.tool.map((t) => {
|
||||||
@@ -232,7 +232,7 @@ export default defineCommand({
|
|||||||
} else {
|
} else {
|
||||||
const response = await withEnableThinkingRetry({
|
const response = await withEnableThinkingRetry({
|
||||||
initial: enableThinking,
|
initial: enableThinking,
|
||||||
apply: (value) => applyChatEnableThinking(body, value),
|
apply: applyThinking,
|
||||||
run: () =>
|
run: () =>
|
||||||
ctx.client.requestJson<ChatResponse>({
|
ctx.client.requestJson<ChatResponse>({
|
||||||
path: chatPath(),
|
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 {
|
export {
|
||||||
adjustEnableThinkingAfterError,
|
adjustEnableThinkingAfterError,
|
||||||
applyChatEnableThinking,
|
applyChatEnableThinking,
|
||||||
|
applyChatEnableThinkingWithBudget,
|
||||||
resolveChatEnableThinking,
|
resolveChatEnableThinking,
|
||||||
withEnableThinkingRetry,
|
withEnableThinkingRetry,
|
||||||
type EnableThinkingAdjustResult,
|
type EnableThinkingAdjustResult,
|
||||||
|
|||||||
@@ -53,6 +53,18 @@ export function applyChatEnableThinking(
|
|||||||
body.enable_thinking = true;
|
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 {
|
function errorMessageOf(error: unknown): string {
|
||||||
if (error instanceof Error) return error.message;
|
if (error instanceof Error) return error.message;
|
||||||
return String(error);
|
return String(error);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { expect, test } from "vite-plus/test";
|
|||||||
import {
|
import {
|
||||||
adjustEnableThinkingAfterError,
|
adjustEnableThinkingAfterError,
|
||||||
applyChatEnableThinking,
|
applyChatEnableThinking,
|
||||||
|
applyChatEnableThinkingWithBudget,
|
||||||
resolveChatEnableThinking,
|
resolveChatEnableThinking,
|
||||||
withEnableThinkingRetry,
|
withEnableThinkingRetry,
|
||||||
} from "../src/models/thinking.ts";
|
} from "../src/models/thinking.ts";
|
||||||
@@ -123,6 +124,43 @@ test("withEnableThinkingRetry:must-be-false 时从 omit 重试为 false", asyn
|
|||||||
expect(values).toEqual([undefined, false]);
|
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 () => {
|
test("withEnableThinkingRetry:无关错误原样抛出", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
withEnableThinkingRetry({
|
withEnableThinkingRetry({
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export {
|
|||||||
API_KEY_PAGE,
|
API_KEY_PAGE,
|
||||||
TOKEN_PLAN_PAGE,
|
TOKEN_PLAN_PAGE,
|
||||||
MCP_WEBSEARCH_PAGE,
|
MCP_WEBSEARCH_PAGE,
|
||||||
|
mcpMarketplaceDetailPage,
|
||||||
VOICE_TTS_PAGE,
|
VOICE_TTS_PAGE,
|
||||||
} from "./urls.ts";
|
} 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. */
|
/** 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`;
|
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.
|
* MCP marketplace detail for the built-in WebSearch server.
|
||||||
* Users must activate (or re-activate for Streamable HTTP) before `search web` works.
|
* 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. */
|
/** Voice TTS experience center — browse system and custom voices. */
|
||||||
export const VOICE_TTS_PAGE = "https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list";
|
export const VOICE_TTS_PAGE = "https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list";
|
||||||
|
|||||||
Reference in New Issue
Block a user