Skip to content

Commit fec8dc5

Browse files
author
wangmingxiang
committed
fix(core): 增加MCP连接可用性检测及输入映射支持
- 新增断言函数assertAutoMcpAvailable,防止Token Plan连接使用auto模型触发MCP请求 - 增加resolveAutoImageSize函数,实现auto模式--size比例与像素的映射及校验 - 新增assertMcpTtsVoice函数,限制auto模式合成语音使用MCP枚举音色 - 修改Image、Speech、Video等命令,调用断言与输入映射函数提升兼容性和报错提示 - 添加McpClient实现,统一底层SDK适配及初始化流程,支持握手超时及关闭中止 - video任务失败时抛出服务器原始错误信息,带失败任务ID上下文提示 - 在client模块新增usesTokenPlanConnection方法判断连接适用性 - 在测试中覆盖新增断言、解析、映射函数,完善SDK适配器握手超时及中止测试 - 测试用例调整“auto”模式下采用MCP枚举音色及--size比例映射验证,确保正确行为
1 parent bbb3722 commit fec8dc5

16 files changed

Lines changed: 689 additions & 55 deletions

File tree

‎packages/commands/src/commands/image/edit.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { join } from "path";
3737
import { BOOL_FLAG_PROMPT_EXTEND_CLI_TRUE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
3838
import {
3939
isAutoModel,
40+
assertAutoMcpAvailable,
4041
assertMcpCompatible,
4142
withAigcMcpSession,
4243
resolveAutoImageInput,
@@ -371,6 +372,7 @@ async function handleAutoMcpMode(
371372
rawImages: string[],
372373
format: OutputFormat,
373374
): Promise<void> {
375+
assertAutoMcpAvailable(client, localize);
374376
assertMcpCompatible("imageEdit", flags, localize);
375377

376378
// bailian_image_edit takes a single source image; bailian_image_fusion is out

‎packages/commands/src/commands/image/generate.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import { resolveImageSize } from "bailian-cli-runtime";
3434
import { BOOL_FLAG_PROMPT_EXTEND_IMAGE_GENERATE, BOOL_FLAG_WATERMARK } from "bailian-cli-runtime";
3535
import {
3636
isAutoModel,
37+
assertAutoMcpAvailable,
3738
assertMcpCompatible,
39+
resolveAutoImageSize,
3840
withAigcMcpSession,
3941
type Localize,
4042
} from "../shared/aigc-mcp.ts";
@@ -278,12 +280,13 @@ async function handleAutoMcpMode(
278280
flags: GenerateFlags,
279281
format: string,
280282
): Promise<void> {
283+
assertAutoMcpAvailable(client, localize);
281284
assertMcpCompatible("imageGen", flags, localize);
282285

283286
const concurrent = getConcurrency(flags);
284287
const args = buildImageGenArgs({
285288
prompt: flags.prompt,
286-
size: flags.size || undefined,
289+
size: resolveAutoImageSize(flags.size, localize),
287290
n: flags.n,
288291
negativePrompt: flags.negativePrompt || undefined,
289292
promptExtend: flags.promptExtend,

‎packages/commands/src/commands/shared/aigc-mcp.ts‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
parseAigcToolResult,
2121
imageFileToDataUri,
2222
audioFileToDataUri,
23+
isMcpTtsVoice,
24+
MCP_TTS_VOICES,
2325
MCP_CALL_SERVICE,
26+
resolveMcpImageSize,
2427
type Client,
2528
type LocalizedText,
2629
type McpConnectedClient,
@@ -199,6 +202,70 @@ export function mcpSingleImageOnlyError(localize: Localize): BailianError {
199202
return new BailianError(message, ExitCode.USAGE, hint);
200203
}
201204

205+
// ---- Connection applicability gate ----
206+
207+
/**
208+
* Reject `auto` mode up front when the effective connection cannot reach the
209+
* Bailian AIGC MCP servers. Both Token Plan shapes were verified against the
210+
* live gateways: the token-plan endpoint has no `/api/v1/mcps` route at all
211+
* (bare 404, empty body) and plan keys (`sk-sp-`) are refused on the standard
212+
* MCP path (401 InvalidApiKey). Neither server response is self-explanatory,
213+
* and this applicability fact is authoritative on the client side, so it is
214+
* raised before any request instead of passing the opaque error through.
215+
*/
216+
export function assertAutoMcpAvailable(client: Client, localize: Localize): void {
217+
if (!client.usesTokenPlanConnection()) return;
218+
const message = localize({
219+
"en-US":
220+
"The current model (auto) routes generation through Bailian MCP, which the Token Plan connection (endpoint or API key) does not provide.",
221+
"zh-CN": "当前模型(auto)经百炼 MCP 路由生成,Token Plan 连接(端点或 API Key)不提供该能力。",
222+
});
223+
const hint = localize({
224+
"en-US":
225+
"Switch to a config profile with a standard DashScope API key, or pass --model <id> to keep using Token Plan models.",
226+
"zh-CN":
227+
"请切换到含标准 DashScope API Key 的配置,或传入 --model <id> 继续使用 Token Plan 模型。",
228+
});
229+
throw new BailianError(message, ExitCode.USAGE, hint);
230+
}
231+
232+
// ---- MCP input value gates (auto mode) ----
233+
234+
/**
235+
* Resolve an `auto`-mode `--size` for MCP image generation: ratios map to the
236+
* server's pixel presets and pixel input passes through; values MCP cannot
237+
* accept are rejected up front with the unsupported-parameter USAGE error.
238+
*/
239+
export function resolveAutoImageSize(
240+
size: string | undefined,
241+
localize: Localize,
242+
): string | undefined {
243+
if (size === undefined) return undefined;
244+
const mapped = resolveMcpImageSize(size);
245+
if (mapped === undefined) {
246+
throw mcpUnsupportedError(localize, [`--size ${size}`]);
247+
}
248+
return mapped;
249+
}
250+
251+
/**
252+
* Reject `auto`-mode TTS voices outside the MCP enum before any request: REST
253+
* voice IDs (e.g. `longxiaochun_v3`) are not accepted by `bailian_tts`.
254+
*/
255+
export function assertMcpTtsVoice(voice: string, localize: Localize): void {
256+
if (isMcpTtsVoice(voice)) return;
257+
const list = MCP_TTS_VOICES.join(", ");
258+
const message = localize({
259+
"en-US": `The current model (auto) does not support voice "${voice}". MCP TTS voices: ${list}.`,
260+
"zh-CN": `当前模型(auto)不支持音色 "${voice}"。MCP 可选音色:${list}。`,
261+
});
262+
const hint = localize({
263+
"en-US": "Pass one of the listed MCP voices, or use --model <id> for the REST system voices.",
264+
"zh-CN": "请传入所列 MCP 音色,或使用 --model <id> 改用 REST 系统音色。",
265+
});
266+
throw new BailianError(message, ExitCode.USAGE, hint);
267+
}
268+
202269
// ---- Local file input (auto mode inlines files as base64 data URIs) ----
203270

204271
/**

‎packages/commands/src/commands/speech/recognize.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { poll } from "bailian-cli-runtime";
3636
import { emitResult, emitBare } from "bailian-cli-runtime";
3737
import {
3838
isAutoModel,
39+
assertAutoMcpAvailable,
3940
assertMcpCompatible,
4041
withAigcMcpSession,
4142
resolveAutoAudioInput,
@@ -351,6 +352,7 @@ async function handleAutoMcpMode(
351352
rawUrls: string[],
352353
format: OutputFormat,
353354
): Promise<void> {
355+
assertAutoMcpAvailable(client, localize);
354356
assertMcpCompatible("asr", flags, localize);
355357

356358
if (rawUrls.length === 0) {

‎packages/commands/src/commands/speech/synthesize.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
3131
import { VOICE_TTS_PAGE } from "bailian-cli-runtime";
3232
import {
3333
isAutoModel,
34+
assertAutoMcpAvailable,
3435
assertMcpCompatible,
36+
assertMcpTtsVoice,
3537
withAigcMcpSession,
3638
type Localize,
3739
} from "../shared/aigc-mcp.ts";
@@ -484,6 +486,7 @@ async function handleAutoMcpMode(
484486
flags: SynthesizeFlags,
485487
format: OutputFormat,
486488
): Promise<void> {
489+
assertAutoMcpAvailable(client, localize);
487490
assertMcpCompatible("tts", flags, localize);
488491

489492
// Same text precedence as REST: --text, else --text-file.
@@ -496,9 +499,10 @@ async function handleAutoMcpMode(
496499
throw new BailianError(`Cannot read text file: ${filePath}`, ExitCode.USAGE);
497500
}
498501
}
499-
// Voice is passed through verbatim (validate enforces its presence); the MCP
500-
// server validates it against its own voice enum.
502+
// Voice must be one of the MCP enum values; REST voice IDs are rejected up
503+
// front so dry-run never previews a request the server cannot accept.
501504
const voice = flags.voice as string;
505+
assertMcpTtsVoice(voice, localize);
502506
const args = buildTtsArgs({ text, voice });
503507

504508
if (settings.dryRun) {

‎packages/commands/src/commands/video/generate.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@ import { BOOL_FLAG_PROMPT_EXTEND_API_DEFAULT, BOOL_FLAG_WATERMARK } from "bailia
3434
import { PRIME_FLAGS, resolvePrimeEndpoint, validatePrimeFlags } from "../shared/prime.ts";
3535
import {
3636
isAutoModel,
37+
assertAutoMcpAvailable,
3738
assertMcpCompatible,
3839
withAigcMcpSession,
3940
resolveAutoImageInput,
4041
type AigcMcpCallTool,
42+
type Localize,
4143
} from "../shared/aigc-mcp.ts";
4244

4345
export default defineCommand({
@@ -214,6 +216,7 @@ export default defineCommand({
214216

215217
// auto model → official AIGC MCP (VideoGenerate); specific models keep the REST path.
216218
if (isAutoModel(resolvedModel)) {
219+
assertAutoMcpAvailable(ctx.client, ctx.localize);
217220
const imageInput = flags.image;
218221
assertMcpCompatible(
219222
imageInput !== undefined ? "videoImageToVideo" : "videoTextToVideo",
@@ -273,7 +276,9 @@ export default defineCommand({
273276
"tasks",
274277
);
275278
return Promise.all(
276-
taskIds.map((taskId) => pollMcpVideoTask(callTool, taskId, settings, pollInterval)),
279+
taskIds.map((taskId) =>
280+
pollMcpVideoTask(callTool, taskId, settings, pollInterval, ctx.localize),
281+
),
277282
);
278283
},
279284
);
@@ -519,6 +524,14 @@ export default defineCommand({
519524
},
520525
});
521526

527+
/** Supplementary context for a failed MCP video task (server message is primary). */
528+
function videoTaskIdHint(localize: Localize, taskId: string): string {
529+
return localize({
530+
"en-US": `The failed video task id is ${taskId}.`,
531+
"zh-CN": `失败的视频任务 ID 为 ${taskId}。`,
532+
});
533+
}
534+
522535
/**
523536
* Poll a video task to completion over MCP `bailian_text_to_video_fetch_result`.
524537
* Mirrors the REST `poll` UX (spinner + status), but the transport is a repeated
@@ -529,6 +542,7 @@ async function pollMcpVideoTask(
529542
taskId: string,
530543
settings: Settings,
531544
pollInterval: number,
545+
localize: Localize,
532546
): Promise<{ taskId: string; videoUrl: string }> {
533547
const deadline = Date.now() + settings.timeout * 1000;
534548
const spinner = createSpinner("Polling...");
@@ -554,6 +568,12 @@ async function pollMcpVideoTask(
554568

555569
if (result.taskStatus === "FAILED") {
556570
spinner.stop("Failed.");
571+
// Pass the server's failure reason through verbatim (AGENTS.md §3);
572+
// the task id stays as supplementary context in the hint.
573+
const serverReason = result.message ?? result.code;
574+
if (serverReason) {
575+
throw new BailianError(serverReason, ExitCode.GENERAL, videoTaskIdHint(localize, taskId));
576+
}
557577
throw new BailianError(`Video task failed: ${taskId}`, ExitCode.GENERAL);
558578
}
559579

‎packages/commands/tests/aigc-mcp-gate.test.ts‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { describe, expect, test } from "vite-plus/test";
2-
import { BailianError, ExitCode } from "bailian-cli-core";
2+
import { BailianError, ExitCode, type Client } from "bailian-cli-core";
33
import {
44
AUTO_MODEL,
5+
assertAutoMcpAvailable,
56
assertMcpCompatible,
7+
assertMcpTtsVoice,
68
isAutoModel,
79
mcpSingleImageOnlyError,
810
mcpUnsupportedError,
11+
resolveAutoImageSize,
912
type AigcMcpOperation,
1013
type Localize,
1114
} from "../src/commands/shared/aigc-mcp.ts";
@@ -147,3 +150,54 @@ describe("error message builders", () => {
147150
expect(singleImageError.message).toContain("single image");
148151
});
149152
});
153+
154+
describe("assertAutoMcpAvailable rejects Token Plan connections", () => {
155+
/** Minimal Client stub: only the accessor the applicability gate reads. */
156+
function stubClient(tokenPlanConnection: boolean): Client {
157+
return {
158+
usesTokenPlanConnection: () => tokenPlanConnection,
159+
} as unknown as Client;
160+
}
161+
162+
test("Token Plan connection throws USAGE with an actionable hint (en-US)", () => {
163+
const error = captureUsage(() => assertAutoMcpAvailable(stubClient(true), enUS));
164+
expect(error.message).toContain("Token Plan");
165+
expect(error.hint).toContain("--model");
166+
});
167+
168+
test("zh-CN localization mentions Token Plan and the model fallback", () => {
169+
const error = captureUsage(() => assertAutoMcpAvailable(stubClient(true), zhCN));
170+
expect(error.message).toContain("Token Plan");
171+
expect(error.hint).toContain("Token Plan 模型");
172+
});
173+
174+
test("non-Token-Plan connection passes", () => {
175+
expect(() => assertAutoMcpAvailable(stubClient(false), enUS)).not.toThrow();
176+
});
177+
});
178+
179+
describe("resolveAutoImageSize maps or rejects --size for MCP", () => {
180+
test("ratios map to pixel presets and pixels pass through", () => {
181+
expect(resolveAutoImageSize("16:9", enUS)).toBe("1664*928");
182+
expect(resolveAutoImageSize("2048*2048", enUS)).toBe("2048*2048");
183+
expect(resolveAutoImageSize(undefined, enUS)).toBeUndefined();
184+
});
185+
186+
test("unmappable values throw the unsupported-parameter USAGE error", () => {
187+
const error = captureUsage(() => resolveAutoImageSize("2:3", enUS));
188+
expect(error.message).toContain("--size 2:3");
189+
});
190+
});
191+
192+
describe("assertMcpTtsVoice rejects REST voice IDs", () => {
193+
test("enum voices pass", () => {
194+
expect(() => assertMcpTtsVoice("温柔女声", enUS)).not.toThrow();
195+
});
196+
197+
test("REST voice IDs throw USAGE listing the MCP enum", () => {
198+
const error = captureUsage(() => assertMcpTtsVoice("longxiaochun_v3", enUS));
199+
expect(error.message).toContain("longxiaochun_v3");
200+
expect(error.message).toContain("温柔女声");
201+
expect(error.hint).toContain("--model");
202+
});
203+
});

0 commit comments

Comments
 (0)