@xinizai/pi-image-gen 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +13 -15
- package/dist/core/global-config.d.ts +2 -0
- package/dist/core/global-config.js +2 -2
- package/dist/core/global-config.js.map +1 -1
- package/dist/core/model-browser.js +34 -26
- package/dist/core/model-browser.js.map +1 -1
- package/dist/core/model-registry.js +6 -1
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/index.js +76 -23
- package/dist/index.js.map +1 -1
- package/dist/providers/openai-compatible.js +1 -2
- package/dist/providers/openai-compatible.js.map +1 -1
- package/dist/tools/shared.d.ts +4 -0
- package/dist/tools/shared.js +11 -7
- package/dist/tools/shared.js.map +1 -1
- package/dist/utils/config.js +0 -3
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/download.d.ts +17 -2
- package/dist/utils/download.js +31 -29
- package/dist/utils/download.js.map +1 -1
- package/dist/utils/files.js +5 -2
- package/package.json +11 -3
- package/src/core/global-config.ts +3 -3
- package/src/core/model-browser.ts +30 -20
- package/src/core/model-registry.ts +6 -1
- package/src/index.ts +73 -20
- package/src/tools/shared.ts +13 -11
- package/src/utils/files.ts +4 -2
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { configErrorText, configText, createService, deleteProvider, discoverPro
|
|
|
4
4
|
import { GlobalConfigStore } from "./core/global-config.js";
|
|
5
5
|
import { maskApiKey, ImageGenError } from "./core/errors.js";
|
|
6
6
|
import { ModelRegistry } from "./core/model-registry.js";
|
|
7
|
-
import type { DiscoveryResult, StoredProviderConfig } from "./core/types.js";
|
|
7
|
+
import type { DiscoveryResult, ModelCapability, ModelInfo, StoredProviderConfig } from "./core/types.js";
|
|
8
8
|
|
|
9
9
|
const OutputFormat = Type.Union([Type.Literal("png"), Type.Literal("jpeg"), Type.Literal("jpg"), Type.Literal("webp")]);
|
|
10
10
|
|
|
@@ -181,13 +181,17 @@ async function interactiveAddProvider(ctx: ExtensionCommandContext): Promise<voi
|
|
|
181
181
|
const discovery = await discoverProviderCandidate({ name, baseUrl, apiKey }, ctx.signal);
|
|
182
182
|
ctx.ui.notify(renderDiscoveryChoiceText(discovery), "info");
|
|
183
183
|
const defaultModel = await selectDefaultImageModel(ctx, discovery);
|
|
184
|
-
if (!defaultModel) { ctx.ui.notify("
|
|
184
|
+
if (!defaultModel) { ctx.ui.notify("已取消添加:未选择默认生图模型。", "info"); return; }
|
|
185
|
+
const defaultEditModel = await selectOptionalOperationModel(ctx, discovery, "image_edit", defaultModel);
|
|
186
|
+
const defaultVariationModel = await selectOptionalOperationModel(ctx, discovery, "image_variation", defaultModel);
|
|
185
187
|
let setDefault = before.providers.length === 0;
|
|
186
188
|
if (!setDefault && before.defaultProviderId) setDefault = await ctx.ui.confirm("设置默认提供商", `是否将 ${name} 设置为默认图片提供商?`);
|
|
187
189
|
const responseFormat = await chooseResponseFormat(ctx, "自动兼容(推荐)");
|
|
188
190
|
if (!responseFormat) return;
|
|
189
|
-
const input: { name: string; baseUrl: string; apiKey: string; defaultModel?: string; responseFormat?: "auto" | "url" | "b64_json"; setDefault?: boolean } = { name, baseUrl, apiKey, responseFormat, setDefault };
|
|
191
|
+
const input: { name: string; baseUrl: string; apiKey: string; defaultModel?: string; defaultEditModel?: string; defaultVariationModel?: string; responseFormat?: "auto" | "url" | "b64_json"; setDefault?: boolean } = { name, baseUrl, apiKey, responseFormat, setDefault };
|
|
190
192
|
if (defaultModel) input.defaultModel = defaultModel;
|
|
193
|
+
if (defaultEditModel) input.defaultEditModel = defaultEditModel;
|
|
194
|
+
if (defaultVariationModel) input.defaultVariationModel = defaultVariationModel;
|
|
191
195
|
ctx.ui.notify(await saveProviderWithDiscovery(input, discovery, store), "info");
|
|
192
196
|
ctx.ui.notify(await configText(ctx.signal), "info");
|
|
193
197
|
} catch (e) {
|
|
@@ -249,10 +253,12 @@ async function interactiveEditProvider(ctx: ExtensionCommandContext, id: string)
|
|
|
249
253
|
: await (await createService(provider.id)).discover(false, ctx.signal);
|
|
250
254
|
ctx.ui.notify(renderDiscoveryChoiceText(discovery), "info");
|
|
251
255
|
const defaultModel = await selectDefaultImageModel(ctx, discovery, modelInput || provider.defaultModel);
|
|
252
|
-
if (!defaultModel) { ctx.ui.notify("
|
|
256
|
+
if (!defaultModel) { ctx.ui.notify("已取消编辑:未选择默认生图模型。", "info"); return; }
|
|
257
|
+
const defaultEditModel = await selectOptionalOperationModel(ctx, discovery, "image_edit", defaultModel, provider.defaultEditModel);
|
|
258
|
+
const defaultVariationModel = await selectOptionalOperationModel(ctx, discovery, "image_variation", defaultModel, provider.defaultVariationModel);
|
|
253
259
|
const responseFormat = await chooseResponseFormat(ctx, responseFormatLabel(provider.responseFormat ?? "auto"));
|
|
254
260
|
if (!responseFormat) return;
|
|
255
|
-
const input: { id: string; name?: string; baseUrl?: string; apiKey?: string; defaultModel?: string; responseFormat?: "auto" | "url" | "b64_json" } = { id: provider.id, responseFormat };
|
|
261
|
+
const input: { id: string; name?: string; baseUrl?: string; apiKey?: string; defaultModel?: string; defaultEditModel?: string | undefined; defaultVariationModel?: string | undefined; responseFormat?: "auto" | "url" | "b64_json" } = { id: provider.id, responseFormat, defaultEditModel, defaultVariationModel };
|
|
256
262
|
if (nextName !== provider.name) input.name = nextName;
|
|
257
263
|
if (nextBaseUrl !== provider.baseUrl) input.baseUrl = nextBaseUrl;
|
|
258
264
|
if (nextApiKey !== provider.apiKey) input.apiKey = nextApiKey;
|
|
@@ -285,16 +291,16 @@ async function chooseProviderModel(ctx: ExtensionCommandContext, id: string): Pr
|
|
|
285
291
|
try {
|
|
286
292
|
ctx.ui.notify("正在测试连接并获取模型,请稍候……", "info");
|
|
287
293
|
const discovery = await discoverProviderCandidate({ name: provider.name, baseUrl: provider.baseUrl, apiKey: provider.apiKey }, ctx.signal);
|
|
288
|
-
const
|
|
289
|
-
|
|
294
|
+
const refresh = () => discoverProviderCandidate({ name: provider.name, baseUrl: provider.baseUrl, apiKey: provider.apiKey }, ctx.signal);
|
|
295
|
+
const defaultModel = await selectDefaultImageModel(ctx, discovery, provider.defaultModel, refresh);
|
|
296
|
+
if (!defaultModel) return;
|
|
297
|
+
const defaultEditModel = await selectOptionalOperationModel(ctx, discovery, "image_edit", defaultModel, provider.defaultEditModel, refresh);
|
|
298
|
+
const defaultVariationModel = await selectOptionalOperationModel(ctx, discovery, "image_variation", defaultModel, provider.defaultVariationModel, refresh);
|
|
299
|
+
const changed = defaultModel !== provider.defaultModel || defaultEditModel !== provider.defaultEditModel || defaultVariationModel !== provider.defaultVariationModel;
|
|
300
|
+
if (changed) ctx.ui.notify(await updateProvider({ id: provider.id, defaultModel, defaultEditModel, defaultVariationModel }, ctx.signal, store, false), "info");
|
|
290
301
|
else ctx.ui.notify("默认图片模型未改变。", "info");
|
|
291
302
|
} catch (error) {
|
|
292
303
|
ctx.ui.notify(configErrorText(error), "error");
|
|
293
|
-
const fallback = await ctx.ui.select("无法自动获取模型", ["手动输入模型 ID", "返回"]);
|
|
294
|
-
if (fallback === "手动输入模型 ID") {
|
|
295
|
-
const model = await prefilledInput(ctx, "手动输入默认图片模型 ID", provider.defaultModel ?? "");
|
|
296
|
-
if (model) ctx.ui.notify(await updateProvider({ id: provider.id, defaultModel: model }, ctx.signal, store, false), "info");
|
|
297
|
-
}
|
|
298
304
|
}
|
|
299
305
|
}
|
|
300
306
|
|
|
@@ -323,13 +329,13 @@ async function selectDefaultImageModel(ctx: ExtensionCommandContext, discovery:
|
|
|
323
329
|
let query = "";
|
|
324
330
|
let page = 0;
|
|
325
331
|
while (true) {
|
|
326
|
-
const models = new ModelRegistry(activeDiscovery.models).byCapability("image_generation")
|
|
327
|
-
const filtered = query ? models.filter((model) => model.toLowerCase().includes(query.toLowerCase())) : models;
|
|
332
|
+
const models = new ModelRegistry(activeDiscovery.models).byCapability("image_generation");
|
|
333
|
+
const filtered = query ? models.filter((model) => model.id.toLowerCase().includes(query.toLowerCase())) : models;
|
|
328
334
|
const pageCount = Math.max(1, Math.ceil(filtered.length / MODEL_PAGE_SIZE));
|
|
329
335
|
page = Math.min(page, pageCount - 1);
|
|
330
336
|
const visible = filtered.slice(page * MODEL_PAGE_SIZE, (page + 1) * MODEL_PAGE_SIZE);
|
|
331
|
-
const options = visible.map((model, index) => `${model === current ? "●" : "○"} ${
|
|
332
|
-
if (current && !filtered.
|
|
337
|
+
const options = visible.map((model, index) => `${model.id === current ? "●" : "○"} ${modelDisplayLabel(model)} [#${index + 1}]`);
|
|
338
|
+
if (current && !filtered.some((model) => model.id === current)) options.unshift(`● 当前模型:${modelLabel(current)}${models.some((model) => model.id === current) ? "(被筛选隐藏)" : "(API 未返回)"}`);
|
|
333
339
|
if (page > 0) options.push("‹ 上一页模型");
|
|
334
340
|
if (page + 1 < pageCount) options.push("下一页模型 ›");
|
|
335
341
|
options.push("⌕ 搜索/过滤模型", "↻ 重新获取模型", "取消");
|
|
@@ -350,12 +356,59 @@ async function selectDefaultImageModel(ctx: ExtensionCommandContext, discovery:
|
|
|
350
356
|
continue;
|
|
351
357
|
}
|
|
352
358
|
if (picked.startsWith("● 当前模型:")) return current;
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
if (selected) return selected;
|
|
359
|
+
const model = picked.replace(/^[●○] /, "").replace(/ \[#\d+\]$/, "").replace(/ \[.*\]$/, "");
|
|
360
|
+
if (visible.some((item) => item.id === model)) return model;
|
|
356
361
|
}
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
async function selectOptionalOperationModel(
|
|
365
|
+
ctx: ExtensionCommandContext,
|
|
366
|
+
discovery: DiscoveryResult,
|
|
367
|
+
capability: "image_edit" | "image_variation",
|
|
368
|
+
_generationModel: string,
|
|
369
|
+
current?: string,
|
|
370
|
+
refresh?: () => Promise<DiscoveryResult>,
|
|
371
|
+
): Promise<string | undefined> {
|
|
372
|
+
const title = capability === "image_edit" ? "默认图片编辑模型" : "默认图片变体模型";
|
|
373
|
+
const useGeneration = "自动使用生图模型(推荐)";
|
|
374
|
+
const configure = "选择专用模型";
|
|
375
|
+
const picked = await ctx.ui.select(`选择${title}`, [useGeneration, configure]);
|
|
376
|
+
if (picked !== configure) return undefined;
|
|
377
|
+
|
|
378
|
+
let activeDiscovery = discovery;
|
|
379
|
+
while (true) {
|
|
380
|
+
const models = new ModelRegistry(activeDiscovery.models).byCapability(capability);
|
|
381
|
+
if (models.length === 0) {
|
|
382
|
+
ctx.ui.notify(`未发现可确认支持${title.replace("默认", "")}的模型;将使用生图模型。`, "warning");
|
|
383
|
+
return undefined;
|
|
384
|
+
}
|
|
385
|
+
const options = models.map((model) => `${model.id === current ? "●" : "○"} ${modelDisplayLabel(model)}`);
|
|
386
|
+
options.push(useGeneration, "↻ 重新获取模型", "返回");
|
|
387
|
+
const selected = await ctx.ui.select(`${title}(仅显示检测到支持该操作的模型)`, options);
|
|
388
|
+
if (!selected || selected === "返回") return undefined;
|
|
389
|
+
if (selected === useGeneration) return undefined;
|
|
390
|
+
if (selected === "↻ 重新获取模型") {
|
|
391
|
+
if (!refresh) { ctx.ui.notify("当前流程无法重新获取模型,请返回后再试。", "info"); continue; }
|
|
392
|
+
ctx.ui.notify("正在重新获取模型,请稍候……", "info");
|
|
393
|
+
try { activeDiscovery = await refresh(); }
|
|
394
|
+
catch (error) { ctx.ui.notify(configErrorText(error), "error"); }
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const model = selected.replace(/^[●○] /, "").replace(/ \[.*\]$/, "");
|
|
398
|
+
if (models.some((item) => item.id === model)) return model;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function modelDisplayLabel(model: ModelInfo): string {
|
|
403
|
+
return `${modelLabel(model.id)} [${modelCapabilitiesLabel(model)}]`;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function modelCapabilitiesLabel(model: ModelInfo): string {
|
|
407
|
+
const labels: Record<ModelCapability, string> = { image_generation: "生图", image_edit: "编辑", image_variation: "变体", vision: "视觉", text: "文本", unknown: "未知" };
|
|
408
|
+
const source: Record<ModelInfo["capabilitySource"], string> = { metadata: "服务端声明", heuristic: "名称推测", manual: "手动配置", unknown: "无法判断" };
|
|
409
|
+
return `${model.capabilities.map((capability) => labels[capability]).join("、")};${source[model.capabilitySource]}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
359
412
|
function modelLabel(model: string): string {
|
|
360
413
|
return model.length > MODEL_LABEL_LIMIT ? `${model.slice(0, MODEL_LABEL_LIMIT - 3)}...` : model;
|
|
361
414
|
}
|
|
@@ -387,7 +440,7 @@ function normalizeEditedApiKey(input: string | undefined, current: string): stri
|
|
|
387
440
|
return input;
|
|
388
441
|
}
|
|
389
442
|
|
|
390
|
-
async function saveProviderWithoutDiscovery(input: { name: string; baseUrl: string; apiKey: string; defaultModel: string; setDefault: boolean }, store: GlobalConfigStore): Promise<string> {
|
|
443
|
+
async function saveProviderWithoutDiscovery(input: { name: string; baseUrl: string; apiKey: string; defaultModel: string; defaultEditModel?: string; defaultVariationModel?: string; setDefault: boolean }, store: GlobalConfigStore): Promise<string> {
|
|
391
444
|
const provider = await store.addProvider(input);
|
|
392
445
|
if (input.setDefault) await store.setDefaultProvider(provider.id);
|
|
393
446
|
return [`提供商已保存:${provider.name} (${provider.id})`, `默认模型:${provider.defaultModel ?? "未设置"}`, "模型发现:未执行,已手动指定模型。"].join("\n");
|
package/src/tools/shared.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { normalizeBaseUrl } from "../utils/config.js";
|
|
|
6
6
|
import { GlobalConfigStore, PROVIDERS_FILE, resolveRuntimeConfig } from "../core/global-config.js";
|
|
7
7
|
import { ImageGenError, maskApiKey } from "../core/errors.js";
|
|
8
8
|
import { DEFAULT_MODEL_LIMIT, renderModelBrowser, renderRefreshSummary } from "../core/model-browser.js";
|
|
9
|
-
import type { DiscoveryResult, ImageOperationResult, ModelBrowserOptions, ModelCapability, StoredProviderConfig } from "../core/types.js";
|
|
9
|
+
import type { DiscoveryResult, ImageOperationResult, ModelBrowserOptions, ModelCapability, ModelInfo, StoredProviderConfig } from "../core/types.js";
|
|
10
10
|
|
|
11
11
|
export async function createService(providerId?: string, env = process.env): Promise<ImageService> {
|
|
12
12
|
const config = await resolveRuntimeConfig(providerId, env);
|
|
@@ -62,11 +62,11 @@ export async function configText(signal?: AbortSignal, store = new GlobalConfigS
|
|
|
62
62
|
lines.push(`${index}. ${provider.name} (${provider.id})${global.defaultProviderId === provider.id ? " [默认]" : ""}`);
|
|
63
63
|
lines.push(` 状态:${snapshot.status}${snapshot.reason ? `,原因:${snapshot.reason}` : ""}`);
|
|
64
64
|
lines.push(` 模型来源:${provider.defaultModel && snapshot.totalModels === 1 && snapshot.summary.image_generation[0] === provider.defaultModel ? "手动指定或缓存 fallback" : "API 发现/缓存"}`);
|
|
65
|
-
lines.push(`
|
|
66
|
-
lines.push(`
|
|
67
|
-
lines.push(`
|
|
68
|
-
lines.push(`
|
|
69
|
-
lines.push(
|
|
65
|
+
lines.push(` 默认生图模型:${provider.defaultModel ?? "未设置"}`);
|
|
66
|
+
lines.push(` 默认编辑模型:${provider.defaultEditModel ?? "跟随生图模型(该模型需支持编辑)"}`);
|
|
67
|
+
lines.push(` 默认变体模型:${provider.defaultVariationModel ?? "跟随生图模型(该模型需支持变体)"}`);
|
|
68
|
+
lines.push(` 可用模型:生图 ${snapshot.summary.image_generation.length} / 编辑 ${snapshot.summary.image_edit.length} / 变体 ${snapshot.summary.image_variation.length} / 视觉 ${snapshot.summary.vision.length}(共 ${snapshot.totalModels})`);
|
|
69
|
+
lines.push(" 详细模型:运行 image_models,或在 /image-config 中选择模型。");
|
|
70
70
|
lines.push("");
|
|
71
71
|
index += 1;
|
|
72
72
|
}
|
|
@@ -101,7 +101,7 @@ export async function discoverProviderCandidate(input: { name: string; baseUrl:
|
|
|
101
101
|
return { providerId: config.providerId, providerName: config.providerName, providerType: provider.type, baseUrl: config.baseUrl, models: await provider.discoverModels(signal), discoveredAt: new Date().toISOString(), apiKeyFingerprint: cache.apiKeyFingerprint(config) };
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export async function saveProviderWithDiscovery(input: { name: string; baseUrl: string; apiKey: string; defaultModel?: string; responseFormat?: "auto" | "url" | "b64_json"; setDefault?: boolean }, discovery: DiscoveryResult, store = new GlobalConfigStore()): Promise<string> {
|
|
104
|
+
export async function saveProviderWithDiscovery(input: { name: string; baseUrl: string; apiKey: string; defaultModel?: string; defaultEditModel?: string; defaultVariationModel?: string; responseFormat?: "auto" | "url" | "b64_json"; setDefault?: boolean }, discovery: DiscoveryResult, store = new GlobalConfigStore()): Promise<string> {
|
|
105
105
|
const existing = await store.read();
|
|
106
106
|
const provider = await store.addProvider(input);
|
|
107
107
|
const shouldSetDefault = existing.providers.length === 0 || Boolean(input.setDefault);
|
|
@@ -141,12 +141,14 @@ export async function addProvider(input: { name: string; baseUrl: string; apiKey
|
|
|
141
141
|
return lines.join("\n");
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export async function updateProvider(input: { id: string; name?: string; baseUrl?: string; apiKey?: string; defaultModel?: string; responseFormat?: "auto" | "url" | "b64_json" }, signal?: AbortSignal, store = new GlobalConfigStore(), refresh = true): Promise<string> {
|
|
144
|
+
export async function updateProvider(input: { id: string; name?: string; baseUrl?: string; apiKey?: string; defaultModel?: string; defaultEditModel?: string | undefined; defaultVariationModel?: string | undefined; responseFormat?: "auto" | "url" | "b64_json" }, signal?: AbortSignal, store = new GlobalConfigStore(), refresh = true): Promise<string> {
|
|
145
145
|
const patch: Partial<StoredProviderConfig> = {};
|
|
146
146
|
if (input.name) patch.name = input.name;
|
|
147
147
|
if (input.baseUrl) patch.baseUrl = input.baseUrl;
|
|
148
148
|
if (input.apiKey) patch.apiKey = input.apiKey;
|
|
149
149
|
if (input.defaultModel) patch.defaultModel = input.defaultModel;
|
|
150
|
+
if ("defaultEditModel" in input) patch.defaultEditModel = input.defaultEditModel;
|
|
151
|
+
if ("defaultVariationModel" in input) patch.defaultVariationModel = input.defaultVariationModel;
|
|
150
152
|
if (input.responseFormat) patch.responseFormat = input.responseFormat;
|
|
151
153
|
const provider = await store.updateProvider(input.id, patch);
|
|
152
154
|
const snapshot = await inspectProvider(provider.id, store, signal, refresh).catch((error) => ({ status: statusFromError(error), reason: reasonFromError(error), totalModels: 0, summary: emptySummary() }));
|
|
@@ -194,16 +196,16 @@ async function refreshProvider(id: string, store: GlobalConfigStore, signal?: Ab
|
|
|
194
196
|
return service.discover(true, signal);
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
async function inspectProvider(id: string, store: GlobalConfigStore, signal?: AbortSignal, refresh = false): Promise<{ provider: StoredProviderConfig; status: string; reason?: string; totalModels: number; summary: Record<ModelCapability, string[]> }> {
|
|
199
|
+
async function inspectProvider(id: string, store: GlobalConfigStore, signal?: AbortSignal, refresh = false): Promise<{ provider: StoredProviderConfig; status: string; reason?: string; totalModels: number; models: ModelInfo[]; summary: Record<ModelCapability, string[]> }> {
|
|
198
200
|
const provider = await store.resolveProvider(id);
|
|
199
201
|
if (!provider) throw new ImageGenError("missing_config", `提供商 ${id} 不存在。`, "请在 /image-config 中重新添加或选择其他提供商。");
|
|
200
202
|
const runtime = await store.toRuntimeConfig(provider);
|
|
201
203
|
const service = new ImageService(runtime, resolveProvider(runtime));
|
|
202
204
|
try {
|
|
203
205
|
const registry = await service.registry(refresh, signal);
|
|
204
|
-
return { provider, status: "已连接", totalModels: registry.all().length, summary: registry.summary() };
|
|
206
|
+
return { provider, status: "已连接", totalModels: registry.all().length, models: registry.all(), summary: registry.summary() };
|
|
205
207
|
} catch (error) {
|
|
206
|
-
return { provider, status: statusFromError(error), reason: reasonFromError(error), totalModels: 0, summary: emptySummary() };
|
|
208
|
+
return { provider, status: statusFromError(error), reason: reasonFromError(error), totalModels: 0, models: [], summary: emptySummary() };
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
211
|
|
package/src/utils/files.ts
CHANGED
|
@@ -70,9 +70,11 @@ function validateImageSignature(buffer: Buffer, ext: string): void {
|
|
|
70
70
|
|
|
71
71
|
export function decodeBase64Image(data: string): { buffer: Buffer; mimeType: string; ext: string } {
|
|
72
72
|
const match = data.match(/^data:(image\/(png|jpeg|jpg|webp));base64,(.+)$/i);
|
|
73
|
-
const mimeType = match?.[1] ?? "image/png";
|
|
74
73
|
const payload = match?.[3] ?? data;
|
|
75
74
|
const buffer = Buffer.from(payload, "base64");
|
|
76
75
|
if (buffer.length === 0) throw new ImageGenError("invalid_image", "API 返回了空 base64 图片。", "检查服务商响应。 ");
|
|
77
|
-
|
|
76
|
+
const detected = detectImageFormat(buffer);
|
|
77
|
+
const mimeType = match?.[1] ?? detected?.mimeType ?? "image/png";
|
|
78
|
+
const ext = match?.[1] ? extensionForMime(mimeType) : detected?.ext ?? "png";
|
|
79
|
+
return { buffer, mimeType, ext };
|
|
78
80
|
}
|