Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/agents/url-change.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +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 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
Expand Down
57 changes: 38 additions & 19 deletions packages/commands/src/commands/auth/login-api-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
chatPath,
requestJson,
normalizeModelBaseUrl,
applyChatEnableThinking,
resolveChatEnableThinking,
withEnableThinkingRetry,
type AuthPersistPatch,
type AuthStore,
type Identity,
Expand Down Expand Up @@ -58,32 +61,48 @@ export async function validateAndPersistApiKey(
? normalizeModelBaseUrl(profile.persistBaseUrl)
: undefined;
const validationModel = profile.defaultTextModel || "qwen3.7-max";
const body: {
model: string;
messages: Array<{ role: string; content: string }>;
max_tokens: number;
stream: boolean;
enable_thinking?: boolean;
} = {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
};

const requestOpts = {
url: baseUrl + chatPath(),
method: "POST",
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, 30),
body: {
model: validationModel,
messages: [{ role: "user", content: "hi" }],
max_tokens: 1,
stream: false,
enable_thinking: validationModel === "qwen3.8-max-preview",
},
body,
};

for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
break;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) {
process.stderr.write("Failed\n");
throw error;
}
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
try {
await withEnableThinkingRetry({
// Validation requests are always non-streaming.
initial: resolveChatEnableThinking({ stream: false }),
apply: (value) => applyChatEnableThinking(body, value),
run: async () => {
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await requestJson<unknown>(httpDeps, requestOpts);
return;
} catch (error) {
if (attempt >= 3 || !canRetry(error)) throw error;
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
}
},
});
} catch (error) {
process.stderr.write("Failed\n");
throw error;
}

process.stderr.write("Valid\n");
Expand Down
39 changes: 39 additions & 0 deletions packages/commands/src/commands/mcp/activate-hint.ts
Original file line number Diff line number Diff line change
@@ -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;
}
22 changes: 15 additions & 7 deletions packages/commands/src/commands/mcp/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
emitResult(result, format);
} catch (error) {
if (!flags.url) {
rethrowWithMcpActivateHint(error, serverCode);
}
throw error;
}
},
});
14 changes: 11 additions & 3 deletions packages/commands/src/commands/mcp/tools.ts
Original file line number Diff line number Diff line change
@@ -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)",
Expand Down Expand Up @@ -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;
}
},
});
18 changes: 18 additions & 0 deletions packages/commands/src/commands/search/web-activate-hint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {
isMcpNotActivated,
mcpActivateHint,
rethrowWithMcpActivateHint,
} from "../mcp/activate-hint.ts";

/** Detect WebSearch MCP not-activated / invalid 404 errors. */
export const isWebSearchMcpNotActivated = isMcpNotActivated;

/** WebSearch activation hint. */
export function webSearchActivateHint(): string {
return mcpActivateHint("WebSearch");
}

/** Keep the original message; append a hint for WebSearch not-activated errors. */
export function rethrowWithWebSearchActivateHint(error: unknown): never {
rethrowWithMcpActivateHint(error, "WebSearch");
}
19 changes: 11 additions & 8 deletions packages/commands/src/commands/search/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
mcpWebSearchPath,
type FlagsDef,
} from "bailian-cli-core";
import { createSpinner } from "bailian-cli-runtime";
import { emitResult } from "bailian-cli-runtime";
import { createSpinner, emitResult } from "bailian-cli-runtime";
import { rethrowWithWebSearchActivateHint } from "./web-activate-hint.ts";

const WEB_SEARCH_FLAGS = {
query: { type: "string", valueHint: "<text>", description: "Search query text" },
Expand Down Expand Up @@ -41,11 +41,14 @@ export default defineCommand({
return;
}

const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();
const tools = await client.listTools();

emitResult({ tools }, format);
try {
const client = ctx.client.mcp(mcpWebSearchPath());
await client.initialize();
const tools = await client.listTools();
emitResult({ tools }, format);
} catch (error) {
rethrowWithWebSearchActivateHint(error);
}
return;
}

Expand Down Expand Up @@ -123,7 +126,7 @@ export default defineCommand({
}
} catch (error) {
spinner.stop("Failed.");
throw error;
rethrowWithWebSearchActivateHint(error);
}
},
});
38 changes: 22 additions & 16 deletions packages/commands/src/commands/text/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
parseSSE,
detectOutputFormat,
readTextFromPathOrStdin,
applyChatEnableThinkingWithBudget,
resolveChatEnableThinking,
withEnableThinkingRetry,
type ChatMessage,
type ChatRequest,
type ChatResponse,
Expand Down Expand Up @@ -124,7 +127,8 @@ export default defineCommand({
const { system, messages } = parseMessages(flags);

const model = flags.model || settings.defaultTextModel || "qwen3.7-max";
const shouldStream = flags.stream || process.stdout.isTTY;
// Coerce isTTY (may be undefined) so stream:false is serialized.
const shouldStream = Boolean(flags.stream || process.stdout.isTTY);
const format = detectOutputFormat(settings.output);

// Build messages array with system prompt
Expand All @@ -144,17 +148,14 @@ export default defineCommand({
if (flags.temperature !== undefined) body.temperature = flags.temperature;
if (flags.topP !== undefined) body.top_p = flags.topP;

if (flags.enableThinking) {
body.enable_thinking = true;
if (flags.thinkingBudget !== undefined) {
body.thinking_budget = flags.thinkingBudget;
}
} else if (!shouldStream) {
// DashScope qwen3 models default to enable_thinking=true server-side, but
// non-streaming calls require it to be explicitly false. Stream calls
// support thinking, so leave the field unset there (server handles it).
body.enable_thinking = false;
}
const enableThinking = resolveChatEnableThinking({
enableThinking: flags.enableThinking,
stream: shouldStream,
});
const applyThinking = (value: boolean | undefined) => {
applyChatEnableThinkingWithBudget(body, value, flags.thinkingBudget);
};
applyThinking(enableThinking);

if (flags.tool) {
const tools = flags.tool.map((t) => {
Expand Down Expand Up @@ -229,10 +230,15 @@ export default defineCommand({
resultOut.write("\n");
}
} else {
const response = await ctx.client.requestJson<ChatResponse>({
path: chatPath(),
method: "POST",
body,
const response = await withEnableThinkingRetry({
initial: enableThinking,
apply: applyThinking,
run: () =>
ctx.client.requestJson<ChatResponse>({
path: chatPath(),
method: "POST",
body,
}),
});

const text = response.choices?.[0]?.message?.content ?? "";
Expand Down
2 changes: 1 addition & 1 deletion packages/commands/tests/e2e/auth.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ describe("e2e: auth", () => {
body: {
model: "qwen3.8-max-preview",
stream: false,
enable_thinking: true,
enable_thinking: false,
},
});

Expand Down
13 changes: 10 additions & 3 deletions packages/commands/tests/e2e/text-chat.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,25 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => {
"--model",
"qwen3.7-max",
"--message",
"干跑",
"dry-run",
"--max-tokens",
"8",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
request?: { model?: string; messages?: Array<{ content?: string }> };
request?: {
model?: string;
messages?: Array<{ content?: string }>;
enable_thinking?: boolean;
stream?: boolean;
};
}>(stdout);
expect(data.request?.model).toBe("qwen3.7-max");
expect(data.request?.messages?.some((m) => m.content === "干跑")).toBe(true);
expect(data.request?.messages?.some((message) => message.content === "dry-run")).toBe(true);
expect(data.request?.stream).toBe(false);
expect(data.request?.enable_thinking).toBe(false);
});

test("【qwen3.7-max】文本对话", async () => {
Expand Down
Loading