diff --git a/bun.lock b/bun.lock index e328ce1..7d530a1 100644 --- a/bun.lock +++ b/bun.lock @@ -65,7 +65,7 @@ }, "packages/plugins/xiaoyunque-generation": { "name": "@microvoid/convax-plugin-xiaoyunque-generation", - "version": "0.3.5", + "version": "0.3.6", "dependencies": { "@microvoid/convax-xiaoyunque-mcp": "workspace:*" } @@ -157,7 +157,7 @@ }, "packages/tools/xiaoyunque-mcp": { "name": "@microvoid/convax-xiaoyunque-mcp", - "version": "0.3.3", + "version": "0.3.4", "bin": { "convax-xiaoyunque-mcp": "dist/convax-xiaoyunque-mcp" }, diff --git a/packages/plugins/xiaoyunque-generation/convax-package.json b/packages/plugins/xiaoyunque-generation/convax-package.json index b29110e..41b0cbc 100644 --- a/packages/plugins/xiaoyunque-generation/convax-package.json +++ b/packages/plugins/xiaoyunque-generation/convax-package.json @@ -4,7 +4,7 @@ "id": "xiaoyunque-generation", "name": "小云雀生成", "description": "通过宿主管理的小云雀网页授权展示服务状态,并使用小云雀第一方网页能力生成图片和视频。", - "version": "0.3.5", + "version": "0.3.6", "license": "MIT", "compatibility": { "pluginSchema": "convax.plugin/3", @@ -13,7 +13,7 @@ "companions": [ { "command": "convax-xiaoyunque-mcp", - "version": "0.3.3", + "version": "0.3.4", "source": "packages/tools/xiaoyunque-mcp", "targets": [ { diff --git a/packages/plugins/xiaoyunque-generation/package.json b/packages/plugins/xiaoyunque-generation/package.json index a07f003..af2f496 100644 --- a/packages/plugins/xiaoyunque-generation/package.json +++ b/packages/plugins/xiaoyunque-generation/package.json @@ -1,6 +1,6 @@ { "name": "@microvoid/convax-plugin-xiaoyunque-generation", - "version": "0.3.5", + "version": "0.3.6", "private": true, "type": "module", "dependencies": { diff --git a/packages/plugins/xiaoyunque-generation/package/manifest.json b/packages/plugins/xiaoyunque-generation/package/manifest.json index 5989add..e320ee9 100644 --- a/packages/plugins/xiaoyunque-generation/package/manifest.json +++ b/packages/plugins/xiaoyunque-generation/package/manifest.json @@ -3,7 +3,7 @@ "id": "xiaoyunque-generation", "name": "小云雀生成", "description": "通过宿主管理的小云雀网页授权展示服务状态,并使用小云雀第一方网页能力生成图片和视频。", - "version": "0.3.5", + "version": "0.3.6", "contributes": { "generation": { "models": [ diff --git a/packages/tools/xiaoyunque-mcp/package.json b/packages/tools/xiaoyunque-mcp/package.json index 626418c..2d5c5da 100644 --- a/packages/tools/xiaoyunque-mcp/package.json +++ b/packages/tools/xiaoyunque-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@microvoid/convax-xiaoyunque-mcp", - "version": "0.3.3", + "version": "0.3.4", "private": true, "type": "module", "bin": { diff --git a/packages/tools/xiaoyunque-mcp/scripts/live-image-smoke.ts b/packages/tools/xiaoyunque-mcp/scripts/live-image-smoke.ts index c46292e..19e7a1c 100644 --- a/packages/tools/xiaoyunque-mcp/scripts/live-image-smoke.ts +++ b/packages/tools/xiaoyunque-mcp/scripts/live-image-smoke.ts @@ -55,7 +55,7 @@ function notify(method: string, params: unknown) { try { const initialized = await request("initialize", { capabilities: {}, - clientInfo: { name: "convax-xiaoyunque-live-smoke", version: "0.3.3" }, + clientInfo: { name: "convax-xiaoyunque-live-smoke", version: "0.3.4" }, protocolVersion: "2025-03-26", }) if ((initialized.result as Record | undefined)?.protocolVersion !== "2025-03-26") { diff --git a/packages/tools/xiaoyunque-mcp/scripts/live-video-smoke.ts b/packages/tools/xiaoyunque-mcp/scripts/live-video-smoke.ts index eadb676..06ba0b1 100644 --- a/packages/tools/xiaoyunque-mcp/scripts/live-video-smoke.ts +++ b/packages/tools/xiaoyunque-mcp/scripts/live-video-smoke.ts @@ -117,7 +117,7 @@ try { progress("mcp.initialize") const initialized = await request("initialize", { capabilities: {}, - clientInfo: { name: "convax-xiaoyunque-live-video-smoke", version: "0.3.3" }, + clientInfo: { name: "convax-xiaoyunque-live-video-smoke", version: "0.3.4" }, protocolVersion: "2025-03-26", }) if ((initialized.result as Record | undefined)?.protocolVersion !== "2025-03-26") { diff --git a/packages/tools/xiaoyunque-mcp/src/contracts.ts b/packages/tools/xiaoyunque-mcp/src/contracts.ts index 5d8372e..4cf06b1 100644 --- a/packages/tools/xiaoyunque-mcp/src/contracts.ts +++ b/packages/tools/xiaoyunque-mcp/src/contracts.ts @@ -9,6 +9,15 @@ const authorizationIdPattern = /^[A-Za-z0-9_-]{16,128}$/u const cookieNamePattern = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]{1,128}$/u const maximumCookieValueBytes = 16 * 1024 const maximumCookieBytes = 32 * 1024 +export const maximumGenerationPromptBytes = 20_000 + +export class XiaoYunquePromptTooLongError extends Error { + override name = "XiaoYunquePromptTooLongError" + + constructor() { + super("XiaoYunque generation prompt is too long") + } +} export type GenerationOutput = "image" | "video" export type GenerationReferenceRole = @@ -215,6 +224,21 @@ function requireTrimmedText(value: unknown, label: string, maxLength: number) { return value } +export function parseGenerationPrompt(value: unknown) { + if ( + typeof value !== "string" + || value.length === 0 + || value !== value.trim() + || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(value) + ) { + throw new Error("generation prompt must be non-empty trimmed text") + } + if (Buffer.byteLength(value, "utf8") > maximumGenerationPromptBytes) { + throw new XiaoYunquePromptTooLongError() + } + return value +} + const referenceRoles = new Set([ "reference_image", "reference_video", @@ -268,7 +292,7 @@ export function parseGenerationCall(value: unknown, expectedOutput: GenerationOu operation_id: requireTrimmedString(input.operation_id, "generation operation_id", 256), output: expectedOutput, output_directory: requireTrimmedString(input.output_directory, "generation output_directory", 4_096), - prompt: requireTrimmedText(input.prompt, "generation prompt", 20_000), + prompt: parseGenerationPrompt(input.prompt), references, schema: generationCallSchema, } diff --git a/packages/tools/xiaoyunque-mcp/src/generator.ts b/packages/tools/xiaoyunque-mcp/src/generator.ts index 67e4b3f..0bb50c4 100644 --- a/packages/tools/xiaoyunque-mcp/src/generator.ts +++ b/packages/tools/xiaoyunque-mcp/src/generator.ts @@ -65,6 +65,22 @@ export class XiaoYunqueUnsupportedImageModelError extends Error { } } +export type XiaoYunqueTerminalGenerationReason = + | "failed" + | "cancelled" + | "additional-input" + | "human-input" + +export class XiaoYunqueTerminalGenerationError extends Error { + override name = "XiaoYunqueTerminalGenerationError" + readonly reason: XiaoYunqueTerminalGenerationReason + + constructor(reason: XiaoYunqueTerminalGenerationReason) { + super(`XiaoYunque generation entered terminal state: ${reason}`) + this.reason = reason + } +} + function abortError() { return new DOMException("Generation was cancelled", "AbortError") } @@ -313,13 +329,13 @@ export class GenerationEngine { if (result.terminalDiagnosticCode === "unsupported-image-model") { throw new XiaoYunqueUnsupportedImageModelError() } - throw new Error(result.error || `XiaoYunque ${output} generation failed`) + throw new XiaoYunqueTerminalGenerationError(result.state === 4 ? "failed" : "cancelled") } if (result.state === 6) { - throw new Error(`XiaoYunque ${output} generation requires additional input`) + throw new XiaoYunqueTerminalGenerationError("additional-input") } if (result.state === 9) { - throw new Error(result.error || `XiaoYunque ${output} generation was interrupted for human input`) + throw new XiaoYunqueTerminalGenerationError("human-input") } await abortableDelay(this.#pollIntervalMs, signal) } diff --git a/packages/tools/xiaoyunque-mcp/src/mcp-server.ts b/packages/tools/xiaoyunque-mcp/src/mcp-server.ts index a7fdb45..a222382 100644 --- a/packages/tools/xiaoyunque-mcp/src/mcp-server.ts +++ b/packages/tools/xiaoyunque-mcp/src/mcp-server.ts @@ -4,6 +4,7 @@ import { type GenerationCall, type JsonRpcRequest, parseGenerationCall, + XiaoYunquePromptTooLongError, parsePluginServiceBrowserAuthorizationCompletion, type PluginServiceBrowserAuthorizationCompletion, type PluginServiceBrowserAuthorizationRequest, @@ -14,6 +15,7 @@ import { XiaoYunqueCredentialConfigurationError } from "./configuration-error.ts import { XiaoYunqueGenerationInputError, XiaoYunqueObservationRejectedError, + XiaoYunqueTerminalGenerationError, XiaoYunqueUnsupportedImageModelError, } from "./generator.ts" import { @@ -140,6 +142,9 @@ function isJsonRpcRequest(value: unknown): value is JsonRpcRequest { export function publicGenerationErrorMessage(error: unknown) { if (error instanceof XiaoYunqueCredentialConfigurationError) return error.publicMessage if (error instanceof XiaoYunqueGenerationInputError) return error.publicMessage + if (error instanceof XiaoYunquePromptTooLongError) { + return "The XiaoYunque prompt is too long. Shorten the prompt or remove some @ text context and try again." + } if (error instanceof XiaoYunqueAuthenticationError) { return "XiaoYunque sign-in expired. Open Convax Services and reconnect XiaoYunque." } @@ -152,6 +157,18 @@ export function publicGenerationErrorMessage(error: unknown) { if (error instanceof XiaoYunqueUnsupportedImageModelError) { return "The selected XiaoYunque image model is no longer available. Choose another image model and try again." } + if (error instanceof XiaoYunqueTerminalGenerationError) { + if (error.reason === "failed") { + return "XiaoYunque accepted the generation, but the service reported that it failed. Check XiaoYunque before starting another paid generation." + } + if (error.reason === "cancelled") { + return "XiaoYunque accepted the generation, but the service reported that it was cancelled. Check XiaoYunque before starting another paid generation." + } + if (error.reason === "additional-input") { + return "XiaoYunque accepted the generation, but it requires additional input in XiaoYunque. Complete it there before starting another paid generation." + } + return "XiaoYunque accepted the generation, but it was interrupted for human input. Check XiaoYunque before starting another paid generation." + } if (error instanceof XiaoYunqueReferenceAssetRegistrationError) { return error.referenceType === "video" ? "XiaoYunque could not prepare the reference video for generation. No generation was submitted; try again." @@ -166,10 +183,15 @@ export function publicGenerationErrorMessage(error: unknown) { export const safeGenerationDiagnosticCodes = [ "local-setup-required", "local-input-rejected", + "prompt-too-long", "sign-in-expired", "status-check-rejected", "status-check-timeout", "unsupported-image-model", + "generation-failed", + "generation-cancelled", + "generation-input-required", + "generation-interrupted", "reference-image-registration-failed", "reference-video-registration-failed", "upstream-envelope-rejected", @@ -183,10 +205,20 @@ export type SafeGenerationDiagnosticCode = typeof safeGenerationDiagnosticCodes[ export function safeGenerationDiagnosticCode(error: unknown): SafeGenerationDiagnosticCode { if (error instanceof XiaoYunqueCredentialConfigurationError) return "local-setup-required" if (error instanceof XiaoYunqueGenerationInputError) return "local-input-rejected" + if (error instanceof XiaoYunquePromptTooLongError) return "prompt-too-long" if (error instanceof XiaoYunqueAuthenticationError) return "sign-in-expired" if (error instanceof XiaoYunqueObservationRejectedError) return "status-check-rejected" if (error instanceof XiaoYunqueQueryTimeoutError) return "status-check-timeout" if (error instanceof XiaoYunqueUnsupportedImageModelError) return "unsupported-image-model" + if (error instanceof XiaoYunqueTerminalGenerationError) { + return error.reason === "failed" + ? "generation-failed" + : error.reason === "cancelled" + ? "generation-cancelled" + : error.reason === "additional-input" + ? "generation-input-required" + : "generation-interrupted" + } if (error instanceof XiaoYunqueReferenceAssetRegistrationError) { return error.referenceType === "video" ? "reference-video-registration-failed" @@ -309,7 +341,7 @@ export class McpServer { this.#sendResult(request.id, { capabilities: { tools: {} }, protocolVersion, - serverInfo: { name: "convax-xiaoyunque-mcp", version: "0.3.3" }, + serverInfo: { name: "convax-xiaoyunque-mcp", version: "0.3.4" }, }) return } diff --git a/packages/tools/xiaoyunque-mcp/src/xiaoyunque-api.ts b/packages/tools/xiaoyunque-mcp/src/xiaoyunque-api.ts index 095c775..ca9f072 100644 --- a/packages/tools/xiaoyunque-mcp/src/xiaoyunque-api.ts +++ b/packages/tools/xiaoyunque-mcp/src/xiaoyunque-api.ts @@ -1,6 +1,10 @@ import { randomUUID } from "node:crypto" import { lstat } from "node:fs/promises" -import type { FileGenerationReference, GenerationOutput } from "./contracts.ts" +import { + type FileGenerationReference, + type GenerationOutput, + parseGenerationPrompt, +} from "./contracts.ts" import type { XiaoYunqueImageModel, XiaoYunqueVideoModel } from "./models.ts" import { type StoredWebSession, @@ -274,6 +278,10 @@ async function boundedJson(response: Response, label: string) { await response.body?.cancel().catch(() => undefined) throw new XiaoYunqueAuthenticationError("XiaoYunque authorization is no longer valid") } + if (response.status < 200 || response.status >= 300) { + await response.body?.cancel().catch(() => undefined) + throw new XiaoYunqueRequestRejectedError(`${label} was rejected`, "upstream-http-rejected") + } const declaredLength = response.headers.get("content-length") if (declaredLength !== null) { const parsed = Number(declaredLength) @@ -697,7 +705,7 @@ export class XiaoYunqueApi { session: StoredWebSession, signal: AbortSignal, ): Promise { - const prompt = boundedText(options.prompt.trim(), "XiaoYunque image prompt", 32_768) + const prompt = parseGenerationPrompt(options.prompt.trim()) if (options.assets.length > 9) throw new Error("XiaoYunque accepts at most nine reference images") const task = { runId: randomUUID(), threadId: randomUUID() } const agentName = "pippit_novel_agent_cn_v2" @@ -744,7 +752,7 @@ export class XiaoYunqueApi { session: StoredWebSession, signal: AbortSignal, ): Promise { - const prompt = boundedText(options.prompt.trim(), "XiaoYunque video prompt", 32_768) + const prompt = parseGenerationPrompt(options.prompt.trim()) const maximumImages = options.model === "Seedance_1.0_fast" ? 1 : 9 if (options.imageAssets.length > maximumImages) { throw new Error(`XiaoYunque ${options.model} accepts at most ${maximumImages} reference image${maximumImages === 1 ? "" : "s"}`) diff --git a/packages/tools/xiaoyunque-mcp/test/contracts.test.ts b/packages/tools/xiaoyunque-mcp/test/contracts.test.ts index 49ce62b..435ef65 100644 --- a/packages/tools/xiaoyunque-mcp/test/contracts.test.ts +++ b/packages/tools/xiaoyunque-mcp/test/contracts.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "bun:test" -import { parseGenerationCall } from "../src/contracts.ts" +import { + maximumGenerationPromptBytes, + parseGenerationCall, + XiaoYunquePromptTooLongError, +} from "../src/contracts.ts" function generationCall(prompt: string, text = "reference line one\n\treference line two") { return { @@ -36,4 +40,16 @@ describe("XiaoYunque generation call contract", () => { .toThrow() }, ) + + test("bounds prompts by UTF-8 bytes instead of JavaScript characters", () => { + const exactAscii = "a".repeat(maximumGenerationPromptBytes) + const exactChinese = "画".repeat(Math.floor(maximumGenerationPromptBytes / 3)) + + expect(parseGenerationCall(generationCall(exactAscii), "image").prompt).toBe(exactAscii) + expect(parseGenerationCall(generationCall(exactChinese), "image").prompt).toBe(exactChinese) + expect(() => parseGenerationCall(generationCall(`${exactAscii}a`), "image")) + .toThrow(XiaoYunquePromptTooLongError) + expect(() => parseGenerationCall(generationCall(`${exactChinese}画`), "image")) + .toThrow(XiaoYunquePromptTooLongError) + }) }) diff --git a/packages/tools/xiaoyunque-mcp/test/generator.test.ts b/packages/tools/xiaoyunque-mcp/test/generator.test.ts index 4abe54b..7968b81 100644 --- a/packages/tools/xiaoyunque-mcp/test/generator.test.ts +++ b/packages/tools/xiaoyunque-mcp/test/generator.test.ts @@ -7,6 +7,7 @@ import { GenerationEngine, XiaoYunqueGenerationInputError, XiaoYunqueObservationRejectedError, + XiaoYunqueTerminalGenerationError, XiaoYunqueUnsupportedImageModelError, } from "../src/generator.ts" import { fingerprintGenerationCall, OperationStore } from "../src/operation-store.ts" @@ -1082,8 +1083,17 @@ describe("XiaoYunque generation engine", () => { output_directory: output, }), videoModel, new AbortController().signal).catch((error: unknown) => error) - expect(failure).toBeInstanceOf(Error) - expect((failure as Error).message).toBe(terminal.message) + expect(failure).toBeInstanceOf(XiaoYunqueTerminalGenerationError) + expect((failure as XiaoYunqueTerminalGenerationError).reason).toBe( + terminal.state === "4" + ? "failed" + : terminal.state === "5" + ? "cancelled" + : terminal.state === "6" + ? "additional-input" + : "human-input", + ) + expect((failure as Error).message).not.toContain("at the vendor") expect(queryCount).toBe(1) }) } @@ -1176,8 +1186,10 @@ describe("XiaoYunque generation engine", () => { output_directory: output, }), imageModel, new AbortController().signal).catch((error: unknown) => error) - expect(failure).toBeInstanceOf(Error) - expect((failure as Error).message).toBe(terminal.message) + expect(failure).toBeInstanceOf(XiaoYunqueTerminalGenerationError) + expect((failure as XiaoYunqueTerminalGenerationError).reason).toBe( + terminal.state === 6 ? "additional-input" : "human-input", + ) expect(queryCount).toBe(1) }) } diff --git a/packages/tools/xiaoyunque-mcp/test/mcp-process.test.ts b/packages/tools/xiaoyunque-mcp/test/mcp-process.test.ts index 36924d9..3ba55c3 100644 --- a/packages/tools/xiaoyunque-mcp/test/mcp-process.test.ts +++ b/packages/tools/xiaoyunque-mcp/test/mcp-process.test.ts @@ -253,7 +253,7 @@ describe("MCP process", () => { }) expect(initialized).toMatchObject({ result: { - serverInfo: { name: "convax-xiaoyunque-mcp", version: "0.3.3" }, + serverInfo: { name: "convax-xiaoyunque-mcp", version: "0.3.4" }, }, }) const authorization = await harness.request("tools/call", { name: "service.authorize", arguments: {} }) diff --git a/packages/tools/xiaoyunque-mcp/test/mcp-server-errors.test.ts b/packages/tools/xiaoyunque-mcp/test/mcp-server-errors.test.ts index efb2257..1fc373f 100644 --- a/packages/tools/xiaoyunque-mcp/test/mcp-server-errors.test.ts +++ b/packages/tools/xiaoyunque-mcp/test/mcp-server-errors.test.ts @@ -1,8 +1,10 @@ import { describe, expect, test } from "bun:test" import { XiaoYunqueCredentialConfigurationError } from "../src/configuration-error.ts" +import { XiaoYunquePromptTooLongError } from "../src/contracts.ts" import { XiaoYunqueGenerationInputError, XiaoYunqueObservationRejectedError, + XiaoYunqueTerminalGenerationError, XiaoYunqueUnsupportedImageModelError, } from "../src/generator.ts" import { publicGenerationErrorMessage, safeGenerationDiagnosticCode } from "../src/mcp-server.ts" @@ -20,6 +22,8 @@ describe("safe public generation errors", () => { expect(publicGenerationErrorMessage( new XiaoYunqueGenerationInputError("last-frame-without-first"), )).toBe("A video last frame requires exactly one first frame.") + expect(publicGenerationErrorMessage(new XiaoYunquePromptTooLongError())) + .toBe("The XiaoYunque prompt is too long. Shorten the prompt or remove some @ text context and try again.") expect(publicGenerationErrorMessage(new XiaoYunqueAuthenticationError("private upstream detail"))) .toBe("XiaoYunque sign-in expired. Open Convax Services and reconnect XiaoYunque.") expect(publicGenerationErrorMessage(new XiaoYunqueQueryTimeoutError("private timeout detail"))) @@ -46,11 +50,22 @@ describe("safe public generation errors", () => { .toBe( "XiaoYunque did not accept this generation request. Refresh Services and try a model listed for this capability.", ) + expect(publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("failed"))) + .toBe( + "XiaoYunque accepted the generation, but the service reported that it failed. Check XiaoYunque before starting another paid generation.", + ) + expect(publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("cancelled"))) + .toContain("the service reported that it was cancelled") + expect(publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("additional-input"))) + .toContain("requires additional input in XiaoYunque") + expect(publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("human-input"))) + .toContain("was interrupted for human input") }) test("keeps known guidance compatible with the host diagnostic safety filter", () => { const messages = [ publicGenerationErrorMessage(new XiaoYunqueCredentialConfigurationError()), + publicGenerationErrorMessage(new XiaoYunquePromptTooLongError()), publicGenerationErrorMessage(new XiaoYunqueAuthenticationError()), publicGenerationErrorMessage(new XiaoYunqueObservationRejectedError("upstream-envelope-rejected")), publicGenerationErrorMessage(new XiaoYunqueQueryTimeoutError()), @@ -58,6 +73,10 @@ describe("safe public generation errors", () => { publicGenerationErrorMessage(new XiaoYunqueReferenceAssetRegistrationError("video")), publicGenerationErrorMessage(new XiaoYunqueRequestRejectedError("private rejection detail")), publicGenerationErrorMessage(new XiaoYunqueUnsupportedImageModelError()), + publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("failed")), + publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("cancelled")), + publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("additional-input")), + publicGenerationErrorMessage(new XiaoYunqueTerminalGenerationError("human-input")), ] const forbiddenDiagnostic = /\b(?:authorization|cookie|set-cookie|password|passwd|secret|api[-_ ]?key|access[-_ ]?key|secret[-_ ]?key|token|ak|sk)\b/i for (const message of messages) expect(message).not.toMatch(forbiddenDiagnostic) @@ -76,11 +95,16 @@ describe("safe public generation errors", () => { const privateDetail = "Cookie=session-secret /Users/private/input.png https://private.invalid/task" const categories = [ safeGenerationDiagnosticCode(new XiaoYunqueAuthenticationError(privateDetail)), + safeGenerationDiagnosticCode(new XiaoYunquePromptTooLongError()), safeGenerationDiagnosticCode(new XiaoYunqueObservationRejectedError("upstream-http-rejected")), safeGenerationDiagnosticCode(new XiaoYunqueQueryTimeoutError(privateDetail)), safeGenerationDiagnosticCode(new XiaoYunqueReferenceAssetRegistrationError()), safeGenerationDiagnosticCode(new XiaoYunqueReferenceAssetRegistrationError("video")), safeGenerationDiagnosticCode(new XiaoYunqueUnsupportedImageModelError()), + safeGenerationDiagnosticCode(new XiaoYunqueTerminalGenerationError("failed")), + safeGenerationDiagnosticCode(new XiaoYunqueTerminalGenerationError("cancelled")), + safeGenerationDiagnosticCode(new XiaoYunqueTerminalGenerationError("additional-input")), + safeGenerationDiagnosticCode(new XiaoYunqueTerminalGenerationError("human-input")), safeGenerationDiagnosticCode(new XiaoYunqueRequestRejectedError( privateDetail, "upstream-envelope-rejected", @@ -90,11 +114,16 @@ describe("safe public generation errors", () => { expect(categories).toEqual([ "sign-in-expired", + "prompt-too-long", "status-check-rejected", "status-check-timeout", "reference-image-registration-failed", "reference-video-registration-failed", "unsupported-image-model", + "generation-failed", + "generation-cancelled", + "generation-input-required", + "generation-interrupted", "upstream-envelope-rejected", "unclassified-failure", ]) diff --git a/packages/tools/xiaoyunque-mcp/test/xiaoyunque-api.test.ts b/packages/tools/xiaoyunque-mcp/test/xiaoyunque-api.test.ts index 308c69d..9a8f294 100644 --- a/packages/tools/xiaoyunque-mcp/test/xiaoyunque-api.test.ts +++ b/packages/tools/xiaoyunque-mcp/test/xiaoyunque-api.test.ts @@ -1094,6 +1094,21 @@ describe("XiaoYunque first-party Web generation API", () => { expect((httpFailure as XiaoYunqueRequestRejectedError).diagnosticCode) .toBe("upstream-http-rejected") expect(String(httpFailure)).not.toContain("response body") + + for (const response of [ + new Response("private upstream failure", { + headers: { "Content-Type": "text/html" }, + status: 502, + }), + new Response(null, { status: 503 }), + ]) { + const nonJsonFailure = await apiFor(() => response.clone()).query(task, "video", session, signal) + .catch((error: unknown) => error) + expect(nonJsonFailure).toBeInstanceOf(XiaoYunqueRequestRejectedError) + expect((nonJsonFailure as XiaoYunqueRequestRejectedError).diagnosticCode) + .toBe("upstream-http-rejected") + expect(String(nonJsonFailure)).not.toContain("private upstream failure") + } }) test("keeps a real loopback fake API available without weakening production origins", async () => { diff --git a/registry/config.json b/registry/config.json index 8bd01ca..063c492 100644 --- a/registry/config.json +++ b/registry/config.json @@ -1,4 +1,4 @@ { - "sequence": 33, + "sequence": 34, "yanked": [] } diff --git a/tooling/registry.test.js b/tooling/registry.test.js index 5e17d75..10b6dbf 100644 --- a/tooling/registry.test.js +++ b/tooling/registry.test.js @@ -187,7 +187,7 @@ describe("source packages", () => { } expect(xiaoyunque.metadata.companions).toEqual([{ command: "convax-xiaoyunque-mcp", - version: "0.3.3", + version: "0.3.4", source: "packages/tools/xiaoyunque-mcp", targets: [{ platform: "darwin", @@ -287,9 +287,9 @@ describe("source packages", () => { ]) expect(readStoredZip(xiaoyunque.zip).map((entry) => entry.relativePath)).toEqual(["LICENSE", "manifest.json"]) expect(xiaoyunque.companionAssets.map((asset) => asset.assetName)).toEqual([ - "convax-companion-convax-xiaoyunque-mcp-0.3.3-darwin-arm64", + "convax-companion-convax-xiaoyunque-mcp-0.3.4-darwin-arm64", ]) - expect(xiaoyunque.tag).toBe("plugin-xiaoyunque-generation-v0.3.5") + expect(xiaoyunque.tag).toBe("plugin-xiaoyunque-generation-v0.3.6") expect(await fs.readFile(xiaoyunque.companionAssets[0].path)).toEqual(xiaoyunque.companionAssets[0].data) expect(readStoredZip(skill.zip).map((entry) => entry.relativePath)).toContain("SKILL.md") expect(readStoredZip(ffmpeg.zip).map((entry) => entry.relativePath)).toEqual([ @@ -435,7 +435,7 @@ describe("source packages", () => { expect(helloEntry.artifact.url).toContain("/plugin-hello-convax-v0.2.0/") expect(xiaoyunqueEntry.manifest.schema).toBe("convax.plugin/3") expect(xiaoyunqueEntry.companions[0].targets[0].artifact.url).toContain( - "/convax-companion-convax-xiaoyunque-mcp-0.3.3-darwin-arm64", + "/convax-companion-convax-xiaoyunque-mcp-0.3.4-darwin-arm64", ) expect(firstSkill).not.toHaveProperty("manifest") expect(ffmpegSkillEntry.ownerPluginId).toBe("ffmpeg-tools")