From 11dd95057bad7c1bb3d880a7db6b09d140156402 Mon Sep 17 00:00:00 2001 From: Uddoo <19704816+Uddoo@users.noreply.github.com> Date: Tue, 20 Oct 2026 10:49:28 +0800 Subject: [PATCH 1/4] fix(files): use the active endpoint for temporary uploads Pass the resolved credential origin through the shared client and uploader. Keep the direct SDK CN default, show the effective policy endpoint on failure, and preserve service errors without cross-region retries. Fixes #267. AI disclosure: implemented, tested and self-reviewed by autonomous Codex on behalf of Uddoo. No human review or live provider validation is claimed. --- docs/agents/url-change.md | 10 +- packages/core/src/client/client.ts | 4 +- packages/core/src/files/upload.ts | 19 ++-- packages/core/tests/file-upload.test.ts | 136 ++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 11 deletions(-) create mode 100644 packages/core/tests/file-upload.test.ts diff --git a/docs/agents/url-change.md b/docs/agents/url-change.md index 2ac1fd17..753fce23 100644 --- a/docs/agents/url-change.md +++ b/docs/agents/url-change.md @@ -23,10 +23,16 @@ runtime/src/urls.ts ← 用户面控制台 URL(cn-only) MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch") mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/ -core/files/upload.ts ← 文件上传 endpoint(cn-pinned) - UPLOAD_API ${REGIONS.cn}/api/v1/uploads +core/files/upload.ts ← 文件上传 endpoint(与 model 域凭证同源) + UploadOptions.baseUrl Client 传入有效凭证的 baseUrl,经 normalizeModelBaseUrl 取 origin + /api/v1/uploads 直接 SDK 调用未提供 baseUrl 时保持 REGIONS.cn 默认值 ``` +临时文件上传与后续模型请求使用同一 API origin,包括国际区、Workspace 和自定义代理。 +上传服务的区域/模型支持由服务端决定:错误中附上有效上传 endpoint 并保留原始服务响应, +不自动换区域重试。 +如果所选服务不支持临时上传,使用服务可访问的 HTTPS 媒体 URL;已有 URL 会跳过本地上传。 + ## 必查清单 ### A. TS 源码(必须 import,不准硬编码) diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index acc2b41d..8186aea8 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -151,8 +151,10 @@ export class Client { /** Resolve a file arg: upload a local path to OSS (returns oss:// URL), or pass a URL through. */ uploadFile(source: string, model: string, opts: { signal?: AbortSignal } = {}): Promise { if (!isLocalFile(source)) return Promise.resolve(source); - return resolveFileUrl(source, this.requireApi().token, model, { + const credential = this.requireApi(); + return resolveFileUrl(source, credential.token, model, { ...opts, + baseUrl: credential.baseUrl, identity: this.deps.identity, timeout: this.deps.settings.timeout, }); diff --git a/packages/core/src/files/upload.ts b/packages/core/src/files/upload.ts index dc81c976..e7df81cf 100644 --- a/packages/core/src/files/upload.ts +++ b/packages/core/src/files/upload.ts @@ -11,9 +11,7 @@ import { BailianError } from "../errors/base.ts"; import { ExitCode } from "../errors/codes.ts"; import { trackingHeaders, type TrackingIdentity } from "../client/headers.ts"; import { REGIONS } from "../config/schema.ts"; - -// Pinned to cn region; thread baseUrl through if overseas upload becomes a requirement. -const UPLOAD_API = `${REGIONS.cn}/api/v1/uploads`; +import { normalizeModelBaseUrl } from "../config/model-base-url.ts"; interface UploadPolicy { upload_host: string; @@ -37,9 +35,11 @@ async function getUploadPolicy( apiKey: string, model: string, identity: TrackingIdentity, + baseUrl: string, signal?: AbortSignal, ): Promise { - const url = `${UPLOAD_API}?action=getPolicy&model=${encodeURIComponent(model)}`; + const uploadApi = `${normalizeModelBaseUrl(baseUrl)}/api/v1/uploads`; + const url = `${uploadApi}?action=getPolicy&model=${encodeURIComponent(model)}`; const policySignal = combineWithTimeout(15_000, signal); const res = await fetch(url, { headers: { @@ -53,7 +53,7 @@ async function getUploadPolicy( if (!res.ok) { const text = await res.text().catch(() => ""); throw new BailianError( - `Failed to get upload policy (HTTP ${res.status}): ${text}`, + `Failed to get upload policy (HTTP ${res.status}, ${uploadApi}): ${text}`, ExitCode.GENERAL, ); } @@ -108,6 +108,8 @@ async function uploadToOSS( export interface UploadOptions { apiKey: string; + /** Defaults to CN for direct SDK callers; Client supplies its resolved credential origin. */ + baseUrl?: string; model: string; filePath: string; identity: TrackingIdentity; @@ -163,7 +165,7 @@ export function redactDataUri(input: string): string { * The URL is valid for 48 hours. */ export async function uploadFile(opts: UploadOptions): Promise { - const { apiKey, model, filePath, identity, signal, timeout } = opts; + const { apiKey, baseUrl = REGIONS.cn, model, filePath, identity, signal, timeout } = opts; if (!existsSync(filePath)) { throw new BailianError(`File not found: ${filePath}`, ExitCode.USAGE); @@ -174,7 +176,7 @@ export async function uploadFile(opts: UploadOptions): Promise { throw new BailianError(`Not a file: ${filePath}`, ExitCode.USAGE); } - const policy = await getUploadPolicy(apiKey, model, identity, signal); + const policy = await getUploadPolicy(apiKey, model, identity, baseUrl, signal); return uploadToOSS(policy, filePath, signal, timeout); } @@ -196,11 +198,12 @@ export async function resolveFileUrl( input: string, apiKey: string, model: string, - opts: { identity: TrackingIdentity; signal?: AbortSignal; timeout?: number }, + opts: { identity: TrackingIdentity; baseUrl?: string; signal?: AbortSignal; timeout?: number }, ): Promise { if (!isLocalFile(input)) return input; return uploadFile({ apiKey, + baseUrl: opts.baseUrl, model, filePath: input, identity: opts.identity, diff --git a/packages/core/tests/file-upload.test.ts b/packages/core/tests/file-upload.test.ts new file mode 100644 index 00000000..9dd35dba --- /dev/null +++ b/packages/core/tests/file-upload.test.ts @@ -0,0 +1,136 @@ +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vite-plus/test"; +import { Client } from "../src/client/client.ts"; +import { REGIONS, type Settings } from "../src/config/schema.ts"; +import { ExitCode } from "../src/errors/codes.ts"; +import { resolveFileUrl, uploadFile } from "../src/files/upload.ts"; + +const identity = { + binName: "test-cli", + version: "test", + npmPackage: "test-cli", + clientName: "test-client", +}; +const settings: Settings = { + output: "json", + outputExplicit: true, + timeout: 30, + watermark: true, + verbose: false, + quiet: true, + dryRun: false, + telemetry: false, +}; +const policy = { + upload_host: "https://uploads.example.test", + upload_dir: "temporary", + oss_access_key_id: "test-access-key", + signature: "test-signature", + policy: "test-policy", + x_oss_object_acl: "private", + x_oss_forbid_overwrite: "true", +}; +const fetchMock = vi.fn(); +let fixtureDir: string; +let fixturePath: string; + +beforeEach(() => { + fixtureDir = mkdtempSync(join(tmpdir(), "bailian-regional-upload-")); + fixturePath = join(fixtureDir, "sample.mp4"); + writeFileSync(fixturePath, "synthetic media fixture"); + fetchMock.mockReset(); + fetchMock.mockResolvedValueOnce(Response.json({ data: policy })); + fetchMock.mockResolvedValueOnce(new Response(null, { status: 200 })); + vi.stubGlobal("fetch", fetchMock); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + rmSync(fixtureDir, { recursive: true, force: true }); +}); + +function makeClient(baseUrl: string): Client { + return new Client({ + identity, + settings, + baseUrl: REGIONS.cn, + apiCred: { token: "synthetic-profile-key", baseUrl, source: "config" }, + }); +} + +describe("regional temporary file uploads", () => { + test.each([ + REGIONS.cn, + REGIONS.intl, + REGIONS.us, + "https://ws-example.ap-southeast-1.maas.aliyuncs.com", + "https://proxy.example.test", + ])("pairs the upload policy host with the credential for %s", async (baseUrl) => { + await expect(makeClient(baseUrl).uploadFile(fixturePath, "wan3.0-video")).resolves.toBe( + "oss://temporary/sample.mp4", + ); + + expect(fetchMock).toHaveBeenCalledTimes(2); + const [policyUrl, policyOptions] = fetchMock.mock.calls[0]!; + expect(policyUrl).toBe(`${baseUrl}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); + expect(new Headers(policyOptions?.headers).get("Authorization")).toBe( + "Bearer synthetic-profile-key", + ); + const [uploadUrl, uploadOptions] = fetchMock.mock.calls[1]!; + expect(uploadUrl).toBe(policy.upload_host); + expect(uploadOptions?.method).toBe("POST"); + expect((uploadOptions?.body as FormData).get("key")).toBe("temporary/sample.mp4"); + expect(new Headers(uploadOptions?.headers).has("Authorization")).toBe(false); + }); + + test("keeps the CN default for existing direct uploadFile callers", async () => { + await uploadFile({ + apiKey: "synthetic-profile-key", + model: "wan3.0-video", + filePath: fixturePath, + identity, + }); + expect(fetchMock.mock.calls[0]![0]).toBe( + `${REGIONS.cn}/api/v1/uploads?action=getPolicy&model=wan3.0-video`, + ); + }); + + test("normalizes a supplied upload base URL using the shared origin contract", async () => { + const options = { + identity, + baseUrl: `${REGIONS.intl}/compatible-mode/v1/?unused=value#fragment`, + }; + await resolveFileUrl(fixturePath, "synthetic-profile-key", "wan3.0-video", options); + expect(fetchMock.mock.calls[0]![0]).toBe( + `${REGIONS.intl}/api/v1/uploads?action=getPolicy&model=wan3.0-video`, + ); + }); + + test("preserves unsupported-service errors without retrying another region", async () => { + fetchMock.mockReset(); + const response = '{"code":"UnsupportedOperation","message":"Temporary uploads unavailable"}'; + fetchMock.mockResolvedValueOnce(new Response(response, { status: 404 })); + await expect( + makeClient(REGIONS.intl).uploadFile(fixturePath, "wan3.0-video"), + ).rejects.toMatchObject({ + exitCode: ExitCode.GENERAL, + message: `Failed to get upload policy (HTTP 404, ${REGIONS.intl}/api/v1/uploads): ${response}`, + }); + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock.mock.calls[0]![0]).toBe( + `${REGIONS.intl}/api/v1/uploads?action=getPolicy&model=wan3.0-video`, + ); + }); + + test.each([ + "https://media.example.test/sample.mp4", + "oss://existing/sample.mp4", + "data:image/png;base64,AQID", + ])("passes %s through without credentials or requests", async (source) => { + const client = new Client({ identity, settings, baseUrl: REGIONS.intl }); + await expect(client.uploadFile(source, "wan3.0-video")).resolves.toBe(source); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); From 9344dbb673c5ebb1c31acdefa880782c6b02b655 Mon Sep 17 00:00:00 2001 From: Uddoo <19704816+Uddoo@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:13:45 +0800 Subject: [PATCH 2/4] fix(video): use the Prime workspace origin for uploads Honor the existing lazy service-default origin when uploading local files and keyframes for Prime video generation. Keep configured origins ahead of the workspace default and validate missing workspace before upload. Add command-level mocked HTTP regressions for workspace/default routing, explicit origins, both keyframes, and existing media URL passthrough. AI disclosure: autonomous Codex implemented, tested and self-reviewed this follow-up on behalf of Uddoo. No human review or live provider validation is claimed. --- docs/agents/url-change.md | 3 + .../commands/src/commands/video/generate.ts | 10 +- packages/commands/tests/prime.test.ts | 160 +++++++++++++++++- packages/core/src/client/client.ts | 16 +- 4 files changed, 179 insertions(+), 10 deletions(-) diff --git a/docs/agents/url-change.md b/docs/agents/url-change.md index 753fce23..8e4f553b 100644 --- a/docs/agents/url-change.md +++ b/docs/agents/url-change.md @@ -29,6 +29,9 @@ core/files/upload.ts ← 文件上传 endpoint(与 model 域 ``` 临时文件上传与后续模型请求使用同一 API origin,包括国际区、Workspace 和自定义代理。 +Prime 视频命令通过 `Client.uploadFile` / `resolveImageInput` 的 `defaultBaseUrl` 回调, +复用 `resolvePrimeEndpoint` 推导默认 Workspace origin;显式配置的 API origin 仍优先, +已有媒体 URL 不触发上传或求值上传默认地址。 上传服务的区域/模型支持由服务端决定:错误中附上有效上传 endpoint 并保留原始服务响应, 不自动换区域重试。 如果所选服务不支持临时上传,使用服务可访问的 HTTPS 媒体 URL;已有 URL 会跳过本地上传。 diff --git a/packages/commands/src/commands/video/generate.ts b/packages/commands/src/commands/video/generate.ts index 1108ca98..892a7de5 100644 --- a/packages/commands/src/commands/video/generate.ts +++ b/packages/commands/src/commands/video/generate.ts @@ -203,14 +203,18 @@ export default defineCommand({ ); } + const uploadOptions = flags.prime + ? { defaultBaseUrl: () => resolvePrimeEndpoint(ctx, "") } + : undefined; + // Auto-upload local image file for i2v let resolvedImageUrl: string | undefined; if (imageUrl) { - resolvedImageUrl = await ctx.client.resolveImageInput(imageUrl, model); + resolvedImageUrl = await ctx.client.resolveImageInput(imageUrl, model, uploadOptions); } let resolvedLastFrameUrl: string | undefined; if (lastFrameUrl) { - resolvedLastFrameUrl = await ctx.client.resolveImageInput(lastFrameUrl, model); + resolvedLastFrameUrl = await ctx.client.resolveImageInput(lastFrameUrl, model, uploadOptions); } // kf2v mode: both --image and --last-frame provided. @@ -225,7 +229,7 @@ export default defineCommand({ let resolvedFileUrl: string | undefined; if (fileUrl) { - resolvedFileUrl = await ctx.client.uploadFile(fileUrl, model); + resolvedFileUrl = await ctx.client.uploadFile(fileUrl, model, uploadOptions); } const watermark = resolveWatermark(flags.watermark, settings.watermark); diff --git a/packages/commands/tests/prime.test.ts b/packages/commands/tests/prime.test.ts index e17725b2..6670d36b 100644 --- a/packages/commands/tests/prime.test.ts +++ b/packages/commands/tests/prime.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, test } from "vite-plus/test"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Client, REGIONS, type ApiKeyCredential, type Settings } from "bailian-cli-core"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vite-plus/test"; import { resolvePrimeEndpoint, validatePrimeFlags } from "../src/commands/shared/prime.ts"; +import videoGenerate from "../src/commands/video/generate.ts"; function createUrlResolver(baseUrl: string, usesDefaultBaseUrl: boolean) { return { @@ -48,3 +53,156 @@ describe("Prime command helpers", () => { ); }); }); + +describe("Prime video upload origins", () => { + const identity = { + binName: "test-cli", + version: "test", + npmPackage: "test-cli", + clientName: "test-client", + }; + const settings: Settings = { + output: "json", + outputExplicit: true, + timeout: 30, + watermark: true, + verbose: false, + quiet: true, + dryRun: false, + telemetry: false, + }; + const workspaceOrigin = "https://ws_test.cn-beijing.maas.aliyuncs.com"; + const policy = { + upload_host: "https://uploads.example.test", + upload_dir: "temporary", + oss_access_key_id: "test-access-key", + signature: "test-signature", + policy: "test-policy", + x_oss_object_acl: "private", + x_oss_forbid_overwrite: "true", + }; + const fetchMock = vi.fn(); + let fixtureDir: string; + let fixturePath: string; + + beforeEach(() => { + fixtureDir = mkdtempSync(join(tmpdir(), "bailian-prime-upload-")); + fixturePath = join(fixtureDir, "sample.png"); + writeFileSync(fixturePath, "synthetic media fixture"); + fetchMock.mockReset(); + fetchMock.mockImplementation(async (input) => { + const url = new URL(input instanceof Request ? input.url : input); + if (url.pathname === "/api/v1/uploads") { + return Response.json({ data: policy }); + } + if (url.origin === policy.upload_host) return new Response(null, { status: 200 }); + if (url.pathname === "/api/v1/services/aigc/video-generation/video-synthesis") { + return Response.json({ output: { task_id: "synthetic-task" } }); + } + throw new Error(`Unexpected test request: ${url.href}`); + }); + vi.stubGlobal("fetch", fetchMock); + vi.spyOn(process.stdout, "write").mockImplementation(() => true); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + rmSync(fixtureDir, { recursive: true, force: true }); + }); + + function runVideo( + flags: Record, + options: { + baseUrl?: string; + settingsWorkspaceId?: string; + credentialSource?: ApiKeyCredential["source"]; + } = {}, + ) { + const baseUrl = options.baseUrl ?? REGIONS.cn; + const commandSettings = { ...settings, workspaceId: options.settingsWorkspaceId }; + return videoGenerate.run({ + identity, + settings: commandSettings, + flags: { + prime: true, + model: "wan3.0-video", + prompt: "synthetic video", + async: true, + ...flags, + }, + client: new Client({ + identity, + settings: commandSettings, + baseUrl, + baseUrlIsDefault: options.baseUrl === undefined, + apiCred: { + token: "synthetic-workspace-key", + baseUrl, + source: options.credentialSource ?? "flag", + }, + }), + } as never); + } + + function expectUploadOrigin(origin: string, uploadCount = 1) { + const policyCalls = fetchMock.mock.calls.filter(([input]) => { + const url = input instanceof Request ? input.url : input.toString(); + return url.includes("/api/v1/uploads?"); + }); + expect(policyCalls).toHaveLength(uploadCount); + for (const [policyUrl, policyOptions] of policyCalls) { + expect(policyUrl).toBe(`${origin}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); + expect(new Headers(policyOptions?.headers).get("Authorization")).toBe( + "Bearer synthetic-workspace-key", + ); + } + const [submitUrl, submitOptions] = fetchMock.mock.calls.at(-1)!; + expect(submitUrl).toBe(`${origin}/api/v1/services/aigc/video-generation/video-synthesis`); + expect(new Headers(submitOptions?.headers).get("Authorization")).toBe( + "Bearer synthetic-workspace-key", + ); + expect(fetchMock).toHaveBeenCalledTimes(uploadCount * 2 + 1); + } + + test.each(["flag", "env"] as const)( + "uploads local files to the derived workspace with a %s key", + async (credentialSource) => { + await runVideo( + { file: fixturePath, ...(credentialSource === "flag" ? { workspaceId: "ws_test" } : {}) }, + { + credentialSource, + settingsWorkspaceId: credentialSource === "env" ? "ws_test" : undefined, + }, + ); + expectUploadOrigin(workspaceOrigin); + }, + ); + + test("uploads both local keyframes to the derived workspace", async () => { + await runVideo({ image: fixturePath, lastFrame: fixturePath, workspaceId: "ws_test" }); + expectUploadOrigin(workspaceOrigin, 2); + }); + + test.each([ + REGIONS.intl, + REGIONS.us, + "https://configured-workspace.ap-southeast-1.maas.aliyuncs.com", + "https://proxy.example.test", + ])("keeps the configured origin %s without requiring a workspace", async (baseUrl) => { + await runVideo({ file: fixturePath }, { baseUrl }); + expectUploadOrigin(baseUrl); + }); + + test("rejects a missing default workspace before making an upload request", async () => { + await expect(runVideo({ file: fixturePath })).rejects.toMatchObject({ + message: "Workspace ID is required for the default Prime endpoint.", + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + test("passes an existing media URL through without uploading", async () => { + await runVideo({ file: "https://media.example.test/video.mp4", workspaceId: "ws_test" }); + expectUploadOrigin(workspaceOrigin, 0); + }); +}); diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index 8186aea8..7c50eab0 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -148,13 +148,17 @@ export class Client { return requestJson(this.http, this.toOpts(opts)); } - /** Resolve a file arg: upload a local path to OSS (returns oss:// URL), or pass a URL through. */ - uploadFile(source: string, model: string, opts: { signal?: AbortSignal } = {}): Promise { + /** Resolve a file arg, honoring a lazy service default origin just like {@link url}. */ + uploadFile( + source: string, + model: string, + opts: { signal?: AbortSignal; defaultBaseUrl?: () => string } = {}, + ): Promise { if (!isLocalFile(source)) return Promise.resolve(source); const credential = this.requireApi(); return resolveFileUrl(source, credential.token, model, { - ...opts, - baseUrl: credential.baseUrl, + signal: opts.signal, + baseUrl: this.url("", opts.defaultBaseUrl), identity: this.deps.identity, timeout: this.deps.settings.timeout, }); @@ -168,13 +172,13 @@ export class Client { resolveImageInput( source: string, model: string, - opts: { signal?: AbortSignal } = {}, + opts: { signal?: AbortSignal; defaultBaseUrl?: () => string } = {}, ): Promise { if (!isLocalFile(source)) return Promise.resolve(source); if (this.usesTokenPlanEndpoint()) { return Promise.resolve(imageFileToDataUri(source)); } - return this.uploadFile(source, model, { signal: opts.signal }); + return this.uploadFile(source, model, opts); } usesTokenPlanEndpoint(): boolean { From b364d96e4ba6ddb136c8e334cbd462a191a72e96 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Mon, 26 Oct 2026 13:56:30 +0800 Subject: [PATCH 3/4] fix(files): restrict temporary uploads to supported regions --- docs/agents/url-change.md | 23 +++-- packages/commands/tests/prime.test.ts | 64 ++++++++++---- packages/core/src/files/upload.ts | 83 +++++++++++++++++-- packages/core/tests/file-upload.test.ts | 106 +++++++++++++++++------- 4 files changed, 215 insertions(+), 61 deletions(-) diff --git a/docs/agents/url-change.md b/docs/agents/url-change.md index 8e4f553b..ea5090a5 100644 --- a/docs/agents/url-change.md +++ b/docs/agents/url-change.md @@ -23,18 +23,23 @@ runtime/src/urls.ts ← 用户面控制台 URL(cn-only) MCP_WEBSEARCH_PAGE mcpMarketplaceDetailPage("WebSearch") mcpMarketplaceDetailPage BAILIAN_CONSOLE?tab=mcp#/mcp-market/detail/ -core/files/upload.ts ← 文件上传 endpoint(与 model 域凭证同源) - UploadOptions.baseUrl Client 传入有效凭证的 baseUrl,经 normalizeModelBaseUrl 取 origin - /api/v1/uploads 直接 SDK 调用未提供 baseUrl 时保持 REGIONS.cn 默认值 +core/files/upload.ts ← model baseUrl → 同源临时上传 endpoint + 北京/新加坡公共站 保持当前公共站 origin + /api/v1/uploads + 北京/新加坡 Workspace 保持当前 Workspace origin + /api/v1/uploads + 显式自定义代理 保持代理 origin,不改写到公共站 + 其他官方地域 / Token Plan 联网前报 usage error,引导使用 HTTPS URL + UploadOptions.baseUrl 直接 SDK 调用未提供时保持北京公共站默认值 ``` -临时文件上传与后续模型请求使用同一 API origin,包括国际区、Workspace 和自定义代理。 Prime 视频命令通过 `Client.uploadFile` / `resolveImageInput` 的 `defaultBaseUrl` 回调, -复用 `resolvePrimeEndpoint` 推导默认 Workspace origin;显式配置的 API origin 仍优先, -已有媒体 URL 不触发上传或求值上传默认地址。 -上传服务的区域/模型支持由服务端决定:错误中附上有效上传 endpoint 并保留原始服务响应, -不自动换区域重试。 -如果所选服务不支持临时上传,使用服务可访问的 HTTPS 媒体 URL;已有 URL 会跳过本地上传。 +复用 `resolvePrimeEndpoint` 推导 model Workspace origin;上传策略与模型提交均保持该 +Workspace origin。显式配置的 model origin 仍优先;已有媒体 URL 不触发上传或求值上传默认地址。 + +官方临时文件上传当前只支持 `cn-beijing` 与 `ap-southeast-1`;其他已知官方地域与 +Token Plan 在发送 API Key 前失败。显式自定义代理视为自身路由的权威来源,保持同源并由 +代理/服务决定能力。任何路径都不得回退公共站或跨 origin 重试;不支持时用户可改用服务 +可访问的 HTTPS 媒体 URL。服务端错误保留 message/code/request ID/raw response,并在 +message 的结构化上下文中展示本次有效上传 endpoint。 ## 必查清单 diff --git a/packages/commands/tests/prime.test.ts b/packages/commands/tests/prime.test.ts index 6670d36b..3cb578eb 100644 --- a/packages/commands/tests/prime.test.ts +++ b/packages/commands/tests/prime.test.ts @@ -1,7 +1,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { Client, REGIONS, type ApiKeyCredential, type Settings } from "bailian-cli-core"; +import { Client, ExitCode, REGIONS, type ApiKeyCredential, type Settings } from "bailian-cli-core"; import { afterEach, beforeEach, describe, expect, test, vi } from "vite-plus/test"; import { resolvePrimeEndpoint, validatePrimeFlags } from "../src/commands/shared/prime.ts"; import videoGenerate from "../src/commands/video/generate.ts"; @@ -145,20 +145,25 @@ describe("Prime video upload origins", () => { } as never); } - function expectUploadOrigin(origin: string, uploadCount = 1) { + function expectRequestOrigins(options: { + uploadOrigin: string; + submitOrigin: string; + uploadCount?: number; + }) { + const { uploadOrigin, submitOrigin, uploadCount = 1 } = options; const policyCalls = fetchMock.mock.calls.filter(([input]) => { const url = input instanceof Request ? input.url : input.toString(); return url.includes("/api/v1/uploads?"); }); expect(policyCalls).toHaveLength(uploadCount); for (const [policyUrl, policyOptions] of policyCalls) { - expect(policyUrl).toBe(`${origin}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); + expect(policyUrl).toBe(`${uploadOrigin}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); expect(new Headers(policyOptions?.headers).get("Authorization")).toBe( "Bearer synthetic-workspace-key", ); } const [submitUrl, submitOptions] = fetchMock.mock.calls.at(-1)!; - expect(submitUrl).toBe(`${origin}/api/v1/services/aigc/video-generation/video-synthesis`); + expect(submitUrl).toBe(`${submitOrigin}/api/v1/services/aigc/video-generation/video-synthesis`); expect(new Headers(submitOptions?.headers).get("Authorization")).toBe( "Bearer synthetic-workspace-key", ); @@ -175,24 +180,49 @@ describe("Prime video upload origins", () => { settingsWorkspaceId: credentialSource === "env" ? "ws_test" : undefined, }, ); - expectUploadOrigin(workspaceOrigin); + expectRequestOrigins({ uploadOrigin: workspaceOrigin, submitOrigin: workspaceOrigin }); }, ); test("uploads both local keyframes to the derived workspace", async () => { await runVideo({ image: fixturePath, lastFrame: fixturePath, workspaceId: "ws_test" }); - expectUploadOrigin(workspaceOrigin, 2); + expectRequestOrigins({ + uploadOrigin: workspaceOrigin, + submitOrigin: workspaceOrigin, + uploadCount: 2, + }); }); test.each([ - REGIONS.intl, - REGIONS.us, - "https://configured-workspace.ap-southeast-1.maas.aliyuncs.com", - "https://proxy.example.test", - ])("keeps the configured origin %s without requiring a workspace", async (baseUrl) => { - await runVideo({ file: fixturePath }, { baseUrl }); - expectUploadOrigin(baseUrl); - }); + [REGIONS.cn, REGIONS.cn], + [REGIONS.intl, REGIONS.intl], + [ + "https://configured-workspace.cn-beijing.maas.aliyuncs.com", + "https://configured-workspace.cn-beijing.maas.aliyuncs.com", + ], + [ + "https://configured-workspace.ap-southeast-1.maas.aliyuncs.com", + "https://configured-workspace.ap-southeast-1.maas.aliyuncs.com", + ], + ["https://proxy.example.test", "https://proxy.example.test"], + ])( + "routes configured origin %s through upload service %s without requiring a workspace", + async (baseUrl, uploadOrigin) => { + await runVideo({ file: fixturePath }, { baseUrl }); + expectRequestOrigins({ uploadOrigin, submitOrigin: baseUrl }); + }, + ); + + test.each([REGIONS.us, "https://configured-workspace.us-east-1.maas.aliyuncs.com"])( + "rejects unsupported local upload origin %s before submitting", + async (baseUrl) => { + await expect(runVideo({ file: fixturePath }, { baseUrl })).rejects.toMatchObject({ + exitCode: ExitCode.USAGE, + hint: expect.stringContaining("HTTPS media URL"), + }); + expect(fetchMock).not.toHaveBeenCalled(); + }, + ); test("rejects a missing default workspace before making an upload request", async () => { await expect(runVideo({ file: fixturePath })).rejects.toMatchObject({ @@ -203,6 +233,10 @@ describe("Prime video upload origins", () => { test("passes an existing media URL through without uploading", async () => { await runVideo({ file: "https://media.example.test/video.mp4", workspaceId: "ws_test" }); - expectUploadOrigin(workspaceOrigin, 0); + expectRequestOrigins({ + uploadOrigin: workspaceOrigin, + submitOrigin: workspaceOrigin, + uploadCount: 0, + }); }); }); diff --git a/packages/core/src/files/upload.ts b/packages/core/src/files/upload.ts index e7df81cf..051ed2a2 100644 --- a/packages/core/src/files/upload.ts +++ b/packages/core/src/files/upload.ts @@ -10,9 +10,49 @@ import { basename, extname } from "path"; import { BailianError } from "../errors/base.ts"; import { ExitCode } from "../errors/codes.ts"; import { trackingHeaders, type TrackingIdentity } from "../client/headers.ts"; +import { getApiKeyLoginKind } from "../config/profile-presets.ts"; import { REGIONS } from "../config/schema.ts"; import { normalizeModelBaseUrl } from "../config/model-base-url.ts"; +const SUPPORTED_UPLOAD_REGIONS = ["cn-beijing", "ap-southeast-1"] as const; + +function unsupportedTemporaryUpload(target: string): BailianError { + return new BailianError( + `Local-file temporary upload is supported only in ${SUPPORTED_UPLOAD_REGIONS.join(", ")}; "${target}" is not supported.`, + ExitCode.USAGE, + "Use a reachable HTTPS media URL instead.", + ); +} + +/** Keep the configured origin when supported; reject known unsupported official regions. */ +function resolveUploadBaseUrl(baseUrl: string, apiKey: string): string { + const normalizedBaseUrl = normalizeModelBaseUrl(baseUrl); + const hostname = new URL(normalizedBaseUrl).hostname.toLowerCase(); + + if ( + getApiKeyLoginKind(apiKey) === "token-plan" || + /^token-plan\.[a-z0-9-]+\.maas\.aliyuncs\.com$/.test(hostname) + ) { + throw unsupportedTemporaryUpload("Token Plan"); + } + + if (hostname === new URL(REGIONS.cn).hostname) return normalizedBaseUrl; + if (hostname === new URL(REGIONS.intl).hostname) return normalizedBaseUrl; + if (hostname === new URL(REGIONS.us).hostname) { + throw unsupportedTemporaryUpload(hostname); + } + + const maasMatch = /^[^.]+\.([a-z0-9-]+)\.maas\.aliyuncs\.com$/.exec(hostname); + const cloudRegion = maasMatch?.[1]; + if (cloudRegion === "cn-beijing" || cloudRegion === "ap-southeast-1") { + return normalizedBaseUrl; + } + if (cloudRegion) throw unsupportedTemporaryUpload(cloudRegion); + + // Explicit custom proxies are authoritative for their own upload routing. + return normalizedBaseUrl; +} + interface UploadPolicy { upload_host: string; upload_dir: string; @@ -28,6 +68,12 @@ interface UploadPolicyResponse { request_id?: string; } +interface UploadPolicyErrorResponse { + code?: unknown; + message?: unknown; + request_id?: unknown; +} + /** * Step 1: Fetch the upload policy (presigned credentials) from DashScope. */ @@ -38,7 +84,7 @@ async function getUploadPolicy( baseUrl: string, signal?: AbortSignal, ): Promise { - const uploadApi = `${normalizeModelBaseUrl(baseUrl)}/api/v1/uploads`; + const uploadApi = `${resolveUploadBaseUrl(baseUrl, apiKey)}/api/v1/uploads`; const url = `${uploadApi}?action=getPolicy&model=${encodeURIComponent(model)}`; const policySignal = combineWithTimeout(15_000, signal); const res = await fetch(url, { @@ -52,10 +98,35 @@ async function getUploadPolicy( if (!res.ok) { const text = await res.text().catch(() => ""); - throw new BailianError( - `Failed to get upload policy (HTTP ${res.status}, ${uploadApi}): ${text}`, - ExitCode.GENERAL, - ); + let serviceError: UploadPolicyErrorResponse | undefined; + try { + serviceError = JSON.parse(text) as UploadPolicyErrorResponse; + } catch { + // Non-JSON service responses pass through below. + } + const serviceMessage = + typeof serviceError?.message === "string" && serviceError.message + ? serviceError.message + : text || `${res.status} ${res.statusText}`; + const apiCode = typeof serviceError?.code === "string" ? serviceError.code : undefined; + const requestId = + typeof serviceError?.request_id === "string" ? serviceError.request_id : undefined; + const responseContext = [ + `HTTP ${res.status}`, + apiCode ? `code=${apiCode}` : undefined, + requestId ? `request_id=${requestId}` : undefined, + `upload_endpoint=${uploadApi}`, + ] + .filter((item): item is string => item !== undefined) + .join("; "); + throw new BailianError(`${responseContext} - ${serviceMessage}`, ExitCode.GENERAL, undefined, { + api: { + httpStatus: res.status, + apiCode, + requestId, + }, + rawResponse: text.slice(0, 500), + }); } const body = (await res.json()) as UploadPolicyResponse; @@ -108,7 +179,7 @@ async function uploadToOSS( export interface UploadOptions { apiKey: string; - /** Defaults to CN for direct SDK callers; Client supplies its resolved credential origin. */ + /** Model origin used only to resolve the supported regional upload service. Defaults to CN. */ baseUrl?: string; model: string; filePath: string; diff --git a/packages/core/tests/file-upload.test.ts b/packages/core/tests/file-upload.test.ts index 9dd35dba..07c4c59c 100644 --- a/packages/core/tests/file-upload.test.ts +++ b/packages/core/tests/file-upload.test.ts @@ -51,72 +51,116 @@ afterEach(() => { rmSync(fixtureDir, { recursive: true, force: true }); }); -function makeClient(baseUrl: string): Client { +function makeClient(baseUrl: string, token = "sk-synthetic-profile-key"): Client { return new Client({ identity, settings, baseUrl: REGIONS.cn, - apiCred: { token: "synthetic-profile-key", baseUrl, source: "config" }, + apiCred: { token, baseUrl, source: "config" }, }); } +function expectSuccessfulUpload(uploadOrigin: string): void { + expect(fetchMock).toHaveBeenCalledTimes(2); + const [policyUrl, policyOptions] = fetchMock.mock.calls[0]!; + expect(policyUrl).toBe(`${uploadOrigin}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); + expect(new Headers(policyOptions?.headers).get("Authorization")).toBe( + "Bearer sk-synthetic-profile-key", + ); + const [uploadUrl, uploadOptions] = fetchMock.mock.calls[1]!; + expect(uploadUrl).toBe(policy.upload_host); + expect(uploadOptions).toBeDefined(); + const uploadRequestOptions = uploadOptions!; + expect(uploadRequestOptions.method).toBe("POST"); + expect((uploadRequestOptions.body as FormData).get("key")).toBe("temporary/sample.mp4"); + expect(new Headers(uploadRequestOptions.headers).has("Authorization")).toBe(false); +} + describe("regional temporary file uploads", () => { test.each([ - REGIONS.cn, - REGIONS.intl, - REGIONS.us, - "https://ws-example.ap-southeast-1.maas.aliyuncs.com", - "https://proxy.example.test", - ])("pairs the upload policy host with the credential for %s", async (baseUrl) => { + [REGIONS.cn, REGIONS.cn], + [REGIONS.intl, REGIONS.intl], + [ + "https://ws-example.cn-beijing.maas.aliyuncs.com", + "https://ws-example.cn-beijing.maas.aliyuncs.com", + ], + [ + "https://ws-example.ap-southeast-1.maas.aliyuncs.com", + "https://ws-example.ap-southeast-1.maas.aliyuncs.com", + ], + ["https://proxy.example.test", "https://proxy.example.test"], + ])("routes %s through the supported upload service %s", async (baseUrl, uploadOrigin) => { await expect(makeClient(baseUrl).uploadFile(fixturePath, "wan3.0-video")).resolves.toBe( "oss://temporary/sample.mp4", ); - - expect(fetchMock).toHaveBeenCalledTimes(2); - const [policyUrl, policyOptions] = fetchMock.mock.calls[0]!; - expect(policyUrl).toBe(`${baseUrl}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); - expect(new Headers(policyOptions?.headers).get("Authorization")).toBe( - "Bearer synthetic-profile-key", - ); - const [uploadUrl, uploadOptions] = fetchMock.mock.calls[1]!; - expect(uploadUrl).toBe(policy.upload_host); - expect(uploadOptions?.method).toBe("POST"); - expect((uploadOptions?.body as FormData).get("key")).toBe("temporary/sample.mp4"); - expect(new Headers(uploadOptions?.headers).has("Authorization")).toBe(false); + expectSuccessfulUpload(uploadOrigin); }); test("keeps the CN default for existing direct uploadFile callers", async () => { await uploadFile({ - apiKey: "synthetic-profile-key", + apiKey: "sk-synthetic-profile-key", model: "wan3.0-video", filePath: fixturePath, identity, }); - expect(fetchMock.mock.calls[0]![0]).toBe( - `${REGIONS.cn}/api/v1/uploads?action=getPolicy&model=wan3.0-video`, - ); + expectSuccessfulUpload(REGIONS.cn); }); - test("normalizes a supplied upload base URL using the shared origin contract", async () => { + test("normalizes a supported model base URL before resolving its upload region", async () => { const options = { identity, baseUrl: `${REGIONS.intl}/compatible-mode/v1/?unused=value#fragment`, }; - await resolveFileUrl(fixturePath, "synthetic-profile-key", "wan3.0-video", options); - expect(fetchMock.mock.calls[0]![0]).toBe( - `${REGIONS.intl}/api/v1/uploads?action=getPolicy&model=wan3.0-video`, + await resolveFileUrl(fixturePath, "sk-synthetic-profile-key", "wan3.0-video", options); + expectSuccessfulUpload(REGIONS.intl); + }); + + test.each([ + [REGIONS.us, "dashscope-us.aliyuncs.com"], + ["https://ws-example.us-east-1.maas.aliyuncs.com", "us-east-1"], + ["https://ws-example.cn-shanghai.maas.aliyuncs.com", "cn-shanghai"], + ["https://token-plan.cn-beijing.maas.aliyuncs.com", "Token Plan"], + ])("rejects unsupported local uploads for %s", async (baseUrl, target) => { + await expect(makeClient(baseUrl).uploadFile(fixturePath, "wan3.0-video")).rejects.toMatchObject( + { + exitCode: ExitCode.USAGE, + message: expect.stringContaining(target), + hint: expect.stringContaining("HTTPS media URL"), + }, ); + expect(fetchMock).not.toHaveBeenCalled(); }); - test("preserves unsupported-service errors without retrying another region", async () => { + test("rejects Token Plan keys before requesting a policy", async () => { + await expect( + makeClient(REGIONS.cn, "sk-sp-synthetic-plan-key").uploadFile(fixturePath, "wan3.0-video"), + ).rejects.toMatchObject({ + exitCode: ExitCode.USAGE, + message: expect.stringContaining("Token Plan"), + }); + expect(fetchMock).not.toHaveBeenCalled(); + }); + + test("preserves upload-policy service errors without retrying another region", async () => { fetchMock.mockReset(); - const response = '{"code":"UnsupportedOperation","message":"Temporary uploads unavailable"}'; + const response = JSON.stringify({ + code: "UnsupportedOperation", + message: "Temporary uploads unavailable", + request_id: "request-test", + }); fetchMock.mockResolvedValueOnce(new Response(response, { status: 404 })); await expect( makeClient(REGIONS.intl).uploadFile(fixturePath, "wan3.0-video"), ).rejects.toMatchObject({ exitCode: ExitCode.GENERAL, - message: `Failed to get upload policy (HTTP 404, ${REGIONS.intl}/api/v1/uploads): ${response}`, + message: `HTTP 404; code=UnsupportedOperation; request_id=request-test; upload_endpoint=${REGIONS.intl}/api/v1/uploads - Temporary uploads unavailable`, + hint: undefined, + api: { + httpStatus: 404, + apiCode: "UnsupportedOperation", + requestId: "request-test", + }, + rawResponse: response, }); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock.mock.calls[0]![0]).toBe( From 7bc48511f40c9c051b33782c8d60e00100b68b27 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Tue, 27 Oct 2026 15:07:15 +0800 Subject: [PATCH 4/4] fix(files): let Token Plan keys reach explicit upload proxies --- docs/agents/url-change.md | 10 +++---- packages/core/src/files/upload.ts | 23 ++++++++++------ packages/core/tests/file-upload.test.ts | 36 +++++++++++++++++-------- 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/docs/agents/url-change.md b/docs/agents/url-change.md index ea5090a5..0035e96c 100644 --- a/docs/agents/url-change.md +++ b/docs/agents/url-change.md @@ -35,11 +35,11 @@ Prime 视频命令通过 `Client.uploadFile` / `resolveImageInput` 的 `defaultB 复用 `resolvePrimeEndpoint` 推导 model Workspace origin;上传策略与模型提交均保持该 Workspace origin。显式配置的 model origin 仍优先;已有媒体 URL 不触发上传或求值上传默认地址。 -官方临时文件上传当前只支持 `cn-beijing` 与 `ap-southeast-1`;其他已知官方地域与 -Token Plan 在发送 API Key 前失败。显式自定义代理视为自身路由的权威来源,保持同源并由 -代理/服务决定能力。任何路径都不得回退公共站或跨 origin 重试;不支持时用户可改用服务 -可访问的 HTTPS 媒体 URL。服务端错误保留 message/code/request ID/raw response,并在 -message 的结构化上下文中展示本次有效上传 endpoint。 +官方临时文件上传当前只支持 `cn-beijing` 与 `ap-southeast-1`。Token Plan 主机,以及这些官方 +endpoint 上的 `sk-sp-` 密钥,在发送 API Key 前失败。显式自定义代理视为自身路由的权威来源, +保持同源且不按密钥前缀拦截,由代理/服务决定能力。任何路径都不得回退公共站或跨 origin 重试; +不支持时用户可改用服务可访问的 HTTPS 媒体 URL。服务端错误保留 message/code/request ID/raw +response,并在 message 的结构化上下文中展示本次有效上传 endpoint。 ## 必查清单 diff --git a/packages/core/src/files/upload.ts b/packages/core/src/files/upload.ts index 051ed2a2..4827384b 100644 --- a/packages/core/src/files/upload.ts +++ b/packages/core/src/files/upload.ts @@ -24,20 +24,26 @@ function unsupportedTemporaryUpload(target: string): BailianError { ); } -/** Keep the configured origin when supported; reject known unsupported official regions. */ +/** Reject Token Plan keys only after the endpoint is recognized as official. */ +function rejectOfficialTokenPlanKey(apiKey: string): void { + if (getApiKeyLoginKind(apiKey) === "token-plan") { + throw unsupportedTemporaryUpload("Token Plan"); + } +} + +/** Keep supported official origins; reject known unsupported official regions. Custom proxies skip key-prefix checks. */ function resolveUploadBaseUrl(baseUrl: string, apiKey: string): string { const normalizedBaseUrl = normalizeModelBaseUrl(baseUrl); const hostname = new URL(normalizedBaseUrl).hostname.toLowerCase(); - if ( - getApiKeyLoginKind(apiKey) === "token-plan" || - /^token-plan\.[a-z0-9-]+\.maas\.aliyuncs\.com$/.test(hostname) - ) { + if (/^token-plan\.[a-z0-9-]+\.maas\.aliyuncs\.com$/.test(hostname)) { throw unsupportedTemporaryUpload("Token Plan"); } - if (hostname === new URL(REGIONS.cn).hostname) return normalizedBaseUrl; - if (hostname === new URL(REGIONS.intl).hostname) return normalizedBaseUrl; + if (hostname === new URL(REGIONS.cn).hostname || hostname === new URL(REGIONS.intl).hostname) { + rejectOfficialTokenPlanKey(apiKey); + return normalizedBaseUrl; + } if (hostname === new URL(REGIONS.us).hostname) { throw unsupportedTemporaryUpload(hostname); } @@ -45,11 +51,12 @@ function resolveUploadBaseUrl(baseUrl: string, apiKey: string): string { const maasMatch = /^[^.]+\.([a-z0-9-]+)\.maas\.aliyuncs\.com$/.exec(hostname); const cloudRegion = maasMatch?.[1]; if (cloudRegion === "cn-beijing" || cloudRegion === "ap-southeast-1") { + rejectOfficialTokenPlanKey(apiKey); return normalizedBaseUrl; } if (cloudRegion) throw unsupportedTemporaryUpload(cloudRegion); - // Explicit custom proxies are authoritative for their own upload routing. + // Explicit custom proxies decide temporary-upload support themselves; do not gate on key prefix. return normalizedBaseUrl; } diff --git a/packages/core/tests/file-upload.test.ts b/packages/core/tests/file-upload.test.ts index 07c4c59c..dc68d94d 100644 --- a/packages/core/tests/file-upload.test.ts +++ b/packages/core/tests/file-upload.test.ts @@ -60,13 +60,11 @@ function makeClient(baseUrl: string, token = "sk-synthetic-profile-key"): Client }); } -function expectSuccessfulUpload(uploadOrigin: string): void { +function expectSuccessfulUpload(uploadOrigin: string, token = "sk-synthetic-profile-key"): void { expect(fetchMock).toHaveBeenCalledTimes(2); const [policyUrl, policyOptions] = fetchMock.mock.calls[0]!; expect(policyUrl).toBe(`${uploadOrigin}/api/v1/uploads?action=getPolicy&model=wan3.0-video`); - expect(new Headers(policyOptions?.headers).get("Authorization")).toBe( - "Bearer sk-synthetic-profile-key", - ); + expect(new Headers(policyOptions?.headers).get("Authorization")).toBe(`Bearer ${token}`); const [uploadUrl, uploadOptions] = fetchMock.mock.calls[1]!; expect(uploadUrl).toBe(policy.upload_host); expect(uploadOptions).toBeDefined(); @@ -131,14 +129,30 @@ describe("regional temporary file uploads", () => { expect(fetchMock).not.toHaveBeenCalled(); }); - test("rejects Token Plan keys before requesting a policy", async () => { + test.each([ + REGIONS.cn, + REGIONS.intl, + "https://ws-example.cn-beijing.maas.aliyuncs.com", + "https://ws-example.ap-southeast-1.maas.aliyuncs.com", + ])( + "rejects Token Plan keys on official endpoint %s before requesting a policy", + async (baseUrl) => { + await expect( + makeClient(baseUrl, "sk-sp-synthetic-plan-key").uploadFile(fixturePath, "wan3.0-video"), + ).rejects.toMatchObject({ + exitCode: ExitCode.USAGE, + message: expect.stringContaining("Token Plan"), + }); + expect(fetchMock).not.toHaveBeenCalled(); + }, + ); + + test("lets a Token Plan key reach an explicit custom proxy", async () => { + const proxy = "https://proxy.example.test"; await expect( - makeClient(REGIONS.cn, "sk-sp-synthetic-plan-key").uploadFile(fixturePath, "wan3.0-video"), - ).rejects.toMatchObject({ - exitCode: ExitCode.USAGE, - message: expect.stringContaining("Token Plan"), - }); - expect(fetchMock).not.toHaveBeenCalled(); + makeClient(proxy, "sk-sp-synthetic-plan-key").uploadFile(fixturePath, "wan3.0-video"), + ).resolves.toBe("oss://temporary/sample.mp4"); + expectSuccessfulUpload(proxy, "sk-sp-synthetic-plan-key"); }); test("preserves upload-policy service errors without retrying another region", async () => {