@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/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("已取消添加:未选择默认图片模型。", "info"); return; }
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("已取消编辑:未选择默认图片模型。", "info"); return; }
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 model = await selectDefaultImageModel(ctx, discovery, provider.defaultModel, () => discoverProviderCandidate({ name: provider.name, baseUrl: provider.baseUrl, apiKey: provider.apiKey }, ctx.signal));
289
- if (model && model !== provider.defaultModel) ctx.ui.notify(await updateProvider({ id: provider.id, defaultModel: model }, ctx.signal, store, false), "info");
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").map((model) => model.id);
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 ? "●" : "○"} ${modelLabel(model)} [#${index + 1}]`);
332
- if (current && !filtered.includes(current)) options.unshift(`● 当前模型:${modelLabel(current)}${models.includes(current) ? "(被筛选隐藏)" : "(API 未返回)"}`);
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 match = picked.match(/\[#(\d+)\]$/);
354
- const selected = match ? visible[Number(match[1]) - 1] : undefined;
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");
@@ -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(` 模型总数:${snapshot.totalModels}`);
66
- lines.push(` 生图模型:${snapshot.summary.image_generation.length}`);
67
- lines.push(` 图片编辑:${snapshot.summary.image_edit.length}`);
68
- lines.push(` 图片变体:${snapshot.summary.image_variation.length}`);
69
- lines.push(` 视觉理解:${snapshot.summary.vision.length}`);
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
 
@@ -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
- return { buffer, mimeType, ext: extensionForMime(mimeType) };
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
  }