@ykurilov/agent-chat-images 0.80.1 → 0.82.0

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/dist/postgres.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  findErrorCause,
9
9
  imageJobBody,
10
10
  isOpenImageJob
11
- } from "./chunk-5IORNMTX.js";
11
+ } from "./chunk-JRBKNWG3.js";
12
12
 
13
13
  // src/postgres.ts
14
14
  import { fileURLToPath } from "url";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  aspectRatios
3
- } from "../chunk-33SNN6BB.js";
3
+ } from "../chunk-W74LUWVQ.js";
4
4
 
5
5
  // src/react/images-client.ts
6
6
  var TOOL_NAME = "generate_images";
package/dist/sqlite.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  findErrorCause,
8
8
  imageJobBody,
9
9
  isOpenImageJob
10
- } from "./chunk-5IORNMTX.js";
10
+ } from "./chunk-JRBKNWG3.js";
11
11
 
12
12
  // src/sqlite.ts
13
13
  import { fileURLToPath } from "url";
@@ -73,6 +73,9 @@ export interface CreateImageGenerationOptions {
73
73
  * картинка приходит позже: во вложения (и `onLateImage`) или в `deliver`. Фоновые задачи лежат в `jobStore`.
74
74
  */
75
75
  mode?: "wait" | "background";
76
+ /** Фоновый режим: долговечное уведомление конечного сбоя, как у createImageQueue. */
77
+ onFailed?: import("./image-queue/create-image-queue.js").CreateImageQueueOptions["onFailed"];
78
+ failureRetryMs?: number;
76
79
  /** Фоновый режим: где лежат задачи; по умолчанию в памяти. `sqliteImageJobStore` или `postgresImageJobStore` — переживают перезапуск. */
77
80
  jobStore?: ImageJobStore;
78
81
  /** Фоновый режим: шаг проверки задачи у провайдера с очередью; по умолчанию 3000 мс. */
@@ -83,6 +86,11 @@ export interface CreateImageGenerationOptions {
83
86
  maxImagesPerTurn?: number;
84
87
  /** Фоновый режим: поля для ответа модели о заказанной картинке (например, куда она ляжет). */
85
88
  onQueued?: (job: ImageJob) => Record<string, unknown> | void | Promise<Record<string, unknown> | void>;
89
+ /**
90
+ * Свой `fetch` приложения: запросы к провайдеру и скачивание картинок. Это граница доверия приложения: закрепить
91
+ * соединение за проверенным адресом (от подмены DNS) должен он сам (`createPinnedFetch` из
92
+ * `@ykurilov/agent-chat-web-tools/network`). Без него картинку со склада провайдера качает закреплённый `fetch`.
93
+ */
86
94
  fetch?: typeof globalThis.fetch;
87
95
  logError?: (message: string) => void;
88
96
  now?: () => Date;
@@ -1,3 +1,4 @@
1
+ import type { HostResolver } from "@ykurilov/agent-chat-web-tools/network";
1
2
  import type { ProcessedImage } from "./postprocess-image.js";
2
3
  /** Скачивание одной картинки — не дольше этого: задача у провайдера уже готова, файл лежит на его складе. */
3
4
  export declare const DOWNLOAD_TIMEOUT_MS = 60000;
@@ -6,7 +7,14 @@ export declare const imageExtensions: Record<string, string>;
6
7
  export interface DownloadGeneratedImageInput {
7
8
  /** Адрес готовой картинки у провайдера. */
8
9
  url: string;
9
- fetch: typeof globalThis.fetch;
10
+ /**
11
+ * Свой `fetch` приложения (прокси) — граница доверия приложения: по нему идут все адреса, а закрепить соединение за
12
+ * проверенным адресом (от подмены DNS) должен он сам. Нет — склад провайдера качает общий закреплённый `fetch`:
13
+ * имя проверяется в момент соединения, во внутреннюю сеть не пускает. Сайт `trustedOrigin` — обычным `fetch`.
14
+ */
15
+ fetch?: typeof globalThis.fetch;
16
+ /** Как закреплённый `fetch` разрешает имена. По умолчанию системный DNS; заменяется в тестах. */
17
+ resolveHost?: HostResolver;
10
18
  maxImageBytes: number;
11
19
  /** Сайт подключения провайдера из настройки (`https://host:port`): с него можно скачивать и по http, и с localhost. */
12
20
  trustedOrigin?: string;
@@ -1,3 +1,4 @@
1
+ import type { ImageFailureKind } from "./image-failure.js";
1
2
  /** Пропорции, которые модель может заказать у `generate_images`. */
2
3
  export type AspectRatio = "1:1" | "3:2" | "2:3" | "16:9" | "9:16";
3
4
  export declare const aspectRatios: readonly AspectRatio[];
@@ -64,6 +65,8 @@ export type ImageTaskState = {
64
65
  status: "failed";
65
66
  reason: string;
66
67
  costUsd?: number;
68
+ failureKind?: ImageFailureKind;
69
+ providerCode?: string;
67
70
  };
68
71
  /**
69
72
  * Очередь провайдера (KIE и подобные): задача отправляется и проверяется отдельно. Так генерация переживает срок
@@ -0,0 +1,11 @@
1
+ /** Причина конечного сбоя. Неизвестное не считается временным сбоем. */
2
+ export type ImageFailureKind = "transient" | "content" | "limits" | "configuration" | "delivery" | "unknown";
3
+ export interface ImageFailure {
4
+ kind: ImageFailureKind;
5
+ /** Короткая причина без ключей и заголовков подключения. */
6
+ reason: string;
7
+ providerCode?: string;
8
+ }
9
+ /** Классифицирует только ответ провайдера, никогда не исходный prompt. */
10
+ export declare function providerFailureKind(status: number | undefined, code: string | undefined, message: string): ImageFailureKind;
11
+ export declare const canRetryImageFailure: (kind: ImageFailureKind | undefined) => kind is "transient" | "unknown" | undefined;
@@ -1,3 +1,4 @@
1
+ import type { ImageFailureKind } from "./image-failure.js";
1
2
  /**
2
3
  * Сбой одной задачи картинки. `reason` — короткий текст для модели (без ключа и заголовков), `refusedBeforeWork` —
3
4
  * провайдер отказал до работы (неверный ключ, нет денег, неверные параметры): расхода нет.
@@ -5,7 +6,11 @@
5
6
  export declare class ImageGenerationError extends Error {
6
7
  readonly reason: string;
7
8
  readonly refusedBeforeWork: boolean;
9
+ readonly kind: ImageFailureKind;
10
+ readonly providerCode?: string;
8
11
  constructor(reason: string, options: {
9
12
  refusedBeforeWork: boolean;
13
+ kind?: ImageFailureKind;
14
+ providerCode?: string;
10
15
  });
11
16
  }
@@ -1,5 +1,6 @@
1
1
  import type { ChatStorage, SpendingLimits } from "@ykurilov/agent-chat-core";
2
2
  import type { AspectRatio, ImageCatalog, ImageQuality, ImageSize } from "../image-catalog.js";
3
+ import type { ImageFailure } from "../image-failure.js";
3
4
  import type { ImagePurpose } from "../image-setup.js";
4
5
  import type { ImageOutputOptions } from "../postprocess-image.js";
5
6
  import type { ImageJobFilter, ImageJobStatus, ImageJobStore } from "./image-job-store.js";
@@ -29,6 +30,8 @@ export interface ImageJob {
29
30
  status: ImageJobStatus;
30
31
  /** Почему не получилась. */
31
32
  reason?: string;
33
+ /** Есть только у конечного сбоя; failed с lateUntil ещё не конечный. */
34
+ failure?: ImageFailure;
32
35
  /** Ответ приёмника. */
33
36
  result?: unknown;
34
37
  attempt: number;
@@ -38,6 +41,13 @@ export interface ImageJob {
38
41
  updatedAt: string;
39
42
  finishedAt?: string;
40
43
  }
44
+ /** Стабильное событие конечного сбоя одного запуска задачи. */
45
+ export interface ImageFailureEvent {
46
+ id: string;
47
+ run: number;
48
+ job: ImageJob;
49
+ failure: ImageFailure;
50
+ }
41
51
  /**
42
52
  * Доставка ещё нужна? Задачу могут вытеснить новым заказом на то же место, пока приёмник пишет файл: запись должна быть
43
53
  * условной — перед ней спросить `isCurrent()`, а долгую работу обрывать по `signal`.
@@ -96,6 +106,15 @@ export interface CreateImageQueueOptions {
96
106
  * вытеснить: запись делать условной, по `context.isCurrent()`.
97
107
  */
98
108
  deliver(image: GeneratedImage, job: ImageJob, context: ImageDeliveryContext): unknown;
109
+ /**
110
+ * Конечный сбой уже сохранён. Записать durable-действие приложения по event.id и завершиться; генерацию здесь не
111
+ * запускать. Успех подтверждает событие, ошибка повторяет только callback, без заказов провайдеру. Доставка хотя бы
112
+ * один раз: после crash callback может повториться, используйте event.id как unique key. Проверяйте context.isCurrent
113
+ * перед записью и соблюдайте signal. Восстановления/платные повторы ограничивает бюджет приложения.
114
+ */
115
+ onFailed?: (event: ImageFailureEvent, context: ImageDeliveryContext) => void | Promise<void>;
116
+ /** Пауза повторной доставки события после ошибки callback/crash, по умолчанию 30 с. */
117
+ failureRetryMs?: number;
99
118
  /** Где лежат задачи; по умолчанию в памяти (до перезапуска). `sqliteImageJobStore` или `postgresImageJobStore` — переживают перезапуск. */
100
119
  store?: ImageJobStore;
101
120
  /** Расход в лимиты: запись на каждую оплаченную задачу провайдера и проверка лимита до заказа. */
@@ -134,6 +153,11 @@ export interface CreateImageQueueOptions {
134
153
  forgetFinishedAfterMs?: number;
135
154
  /** Предел одного файла; по умолчанию 10 МБ. */
136
155
  maxImageBytes?: number;
156
+ /**
157
+ * Свой `fetch` приложения: запросы к провайдеру и скачивание картинок. Это граница доверия приложения: закрепить
158
+ * соединение за проверенным адресом (от подмены DNS) должен он сам (`createPinnedFetch` из
159
+ * `@ykurilov/agent-chat-web-tools/network`). Без него картинку со склада провайдера качает закреплённый `fetch`.
160
+ */
137
161
  fetch?: typeof globalThis.fetch;
138
162
  logError?: (message: string) => void;
139
163
  now?: () => Date;
@@ -145,10 +169,14 @@ export interface ImageQueue {
145
169
  * «Повторить» последнюю задачу места. Не получилась — новый запуск: идущая у провайдера задача проверяется, а не
146
170
  * заказывается заново; оплаченная, но не доставленная картинка скачивается с того же адреса. Задача идёт или уже
147
171
  * готова — она же, без нового заказа. Нет задачи — `null`.
172
+ * expectedJobId + expectedRun вместе: повторить только выбранный job/run; сменился — `null`.
173
+ * Это CAS записи запуска, не транзакция с файлами приложения и не запрет будущих заказов другого job.id.
148
174
  */
149
175
  retry(input: {
150
176
  ownerId: string;
151
177
  key: string;
178
+ expectedJobId?: string;
179
+ expectedRun?: number;
152
180
  }): Promise<ImageJob | ImageOrderRefusal | null>;
153
181
  get(id: string): Promise<ImageJob | null>;
154
182
  /** По порядку создания. */
@@ -1,3 +1,4 @@
1
+ import type { ImageFailure } from "../image-failure.js";
1
2
  import type { AspectRatio } from "../image-catalog.js";
2
3
  /**
3
4
  * Что с задачей картинки.
@@ -53,9 +54,17 @@ export interface ImageJobRecord {
53
54
  data?: unknown;
54
55
  status: ImageJobStatus;
55
56
  reason?: string;
57
+ failure?: ImageFailure;
58
+ /** Outbox terminal failure: active до подтверждения; callback может повториться. */
59
+ failureNotification?: {
60
+ id: string;
61
+ attempts: number;
62
+ nextAttemptAt: string;
63
+ deliveredAt?: string;
64
+ };
56
65
  /** Ответ приёмника (JSON). */
57
66
  result?: unknown;
58
- /** У задачи есть работа: опрос, позднее окно или незаписанный расход. Такие задачи подбирает старт сервера. */
67
+ /** У задачи есть работа: опрос, позднее окно, незаписанный расход или неподтверждённое событие. Такие задачи подбирает старт сервера. */
59
68
  active: boolean;
60
69
  /** Номер запуска: «Повторить» начинает новый, прежний больше ничего не делает. */
61
70
  run: number;
@@ -85,6 +94,8 @@ export interface ImageJobFilter {
85
94
  }
86
95
  /** У задачи ещё идёт работа с провайдером: генерация или позднее окно. Открытая на место (`ownerId` + `key`) — одна. */
87
96
  export declare const isOpenImageJob: (job: Pick<ImageJobRecord, "status" | "lateUntil">) => boolean;
97
+ /** Долговечное уведомление ещё не подтверждено приложением. */
98
+ export declare const hasPendingImageFailure: (job: Pick<ImageJobRecord, "failureNotification">) => boolean;
88
99
  /** На это место (`ownerId` + `key`) уже есть открытая задача: вторую база не пускает, даже с двух копий сервера. */
89
100
  export declare class ImageJobKeyTakenError extends Error {
90
101
  readonly ownerId: string;
@@ -53,6 +53,8 @@ export interface ImageStep {
53
53
  prompt?: string;
54
54
  /** Сайт подключения из настройки (свой прокси, поддельный провайдер): с него картинку можно скачать и по http. */
55
55
  trustedOrigin?: string;
56
+ /** Свой `fetch` приложения для скачивания картинки; нет — закреплённый `fetch` (download-image). */
57
+ downloadFetch?: typeof globalThis.fetch;
56
58
  }
57
59
  /** Как создаётся картинка одного назначения (или единственной модели). */
58
60
  export interface ImageSetup {
@@ -14,4 +14,5 @@ export type { ImageUsage } from "./images-usage.js";
14
14
  export { ImageJobKeyTakenError, isOpenImageJob, memoryImageJobStore } from "./image-queue/image-job-store.js";
15
15
  export type { ImageJobCharge, ImageJobFilter, ImageJobPatch, ImageJobRecord, ImageJobStatus, ImageJobStep, ImageJobStore, ImageJobTask } from "./image-queue/image-job-store.js";
16
16
  export { createImageQueue } from "./image-queue/create-image-queue.js";
17
- export type { CreateImageQueueOptions, GeneratedImage, ImageDeliveryContext, ImageJob, ImageOrder, ImageOrderRefusal, ImageQueue, ImageQueuePurpose } from "./image-queue/create-image-queue.js";
17
+ export type { ImageFailure, ImageFailureKind } from "./image-failure.js";
18
+ export type { ImageFailureEvent, CreateImageQueueOptions, GeneratedImage, ImageDeliveryContext, ImageJob, ImageOrder, ImageOrderRefusal, ImageQueue, ImageQueuePurpose } from "./image-queue/create-image-queue.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykurilov/agent-chat-images",
3
- "version": "0.80.1",
3
+ "version": "0.82.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -49,14 +49,15 @@
49
49
  "drizzle-orm": "^0.45.3",
50
50
  "sharp": "^0.35.5",
51
51
  "zod": "^4.6.5",
52
- "@ykurilov/agent-chat-attachments": "0.80.1",
53
- "@ykurilov/agent-chat-core": "0.80.1"
52
+ "@ykurilov/agent-chat-attachments": "0.82.0",
53
+ "@ykurilov/agent-chat-web-tools": "0.82.0",
54
+ "@ykurilov/agent-chat-core": "0.82.0"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "better-sqlite3": "^13.0.3",
57
58
  "pg": "^8.23.0",
58
59
  "react": "^19.3.0",
59
- "@ykurilov/agent-chat-react": "^0.80.1"
60
+ "@ykurilov/agent-chat-react": "^0.82.0"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@electric-sql/pglite": "0.5.8",
@@ -71,7 +72,7 @@
71
72
  "pg": "^8.23.0",
72
73
  "react": "^19.3.0",
73
74
  "tsx": "4.23.15",
74
- "@ykurilov/agent-chat-react": "0.80.1"
75
+ "@ykurilov/agent-chat-react": "0.82.0"
75
76
  },
76
77
  "peerDependenciesMeta": {
77
78
  "react": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/image-generation-error.ts","../src/hide-secrets.ts","../src/image-task-polling.ts","../src/kie-provider.ts","../src/runware-provider.ts","../src/image-catalog.ts"],"sourcesContent":["/**\n * Сбой одной задачи картинки. `reason` — короткий текст для модели (без ключа и заголовков), `refusedBeforeWork` —\n * провайдер отказал до работы (неверный ключ, нет денег, неверные параметры): расхода нет.\n */\nexport class ImageGenerationError extends Error {\n readonly reason: string;\n readonly refusedBeforeWork: boolean;\n\n constructor(reason: string, options: { refusedBeforeWork: boolean }) {\n super(reason);\n this.name = \"ImageGenerationError\";\n this.reason = reason;\n this.refusedBeforeWork = options.refusedBeforeWork;\n }\n}\n","/** Значения короче этого не прячутся: «1» или «on» встречаются в любом тексте. */\nconst MIN_SECRET_LENGTH = 4;\n\n/** Текст без ключа провайдера и значений заголовков подключения (прокси): для ошибок, итога вызова и журнала. */\nexport function hideSecrets(text: string, connection: { apiKey?: string; headers?: Record<string, string> }): string {\n const secrets = [connection.apiKey, ...Object.values(connection.headers ?? {})]\n .filter((value): value is string => typeof value === \"string\" && value.length >= MIN_SECRET_LENGTH)\n .sort((a, b) => b.length - a.length);\n return secrets.reduce((result, secret) => result.split(secret).join(\"[скрыто]\"), text);\n}\n","import type { ImageTaskState } from \"./image-catalog\";\nimport { ImageGenerationError } from \"./image-generation-error\";\n\n/** Первая проверка задачи — через это время: быстрее картинка не бывает готова. */\nconst FIRST_CHECK_MS = 2_000;\n/** Каждый следующий интервал во столько раз длиннее… */\nconst INTERVAL_GROWTH = 1.5;\n/** …но не длиннее этого: готовую картинку не ждём лишнего. */\nconst MAX_INTERVAL_MS = 10_000;\n\nexport interface WaitForImageTaskInput {\n check(abortSignal: AbortSignal | undefined): Promise<ImageTaskState>;\n /** Срок по `Date.now()`: дальше опроса нет, итог — `timeout`. */\n deadline: number;\n /** «Стоп»: ожидание обрывается сразу, наружу — причина сигнала. */\n abortSignal?: AbortSignal;\n /** Первая проверка сразу, а не через интервал: задача отправлена давно (повтор, фоновый опрос после перерыва). */\n checkNow?: boolean;\n}\n\nexport type WaitedImageTask = Exclude<ImageTaskState, { status: \"pending\" }> | { status: \"timeout\" };\n\n/** Пауза до `ms`, которую «Стоп» обрывает. Таймер не держит процесс: ждать фоновую задачу при выходе незачем. */\nfunction pause(ms: number, signal: AbortSignal | undefined): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason);\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason);\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n (timer as { unref?: () => void }).unref?.();\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/**\n * Опрос задачи в очереди провайдера: 2 с, затем интервал растёт в полтора раза до 10 с. Последняя проверка — ровно в\n * срок. Временный сбой проверки (сеть, 5xx) не прерывает опрос; `ImageGenerationError` — прерывает.\n */\nexport async function waitForImageTask(input: WaitForImageTaskInput): Promise<WaitedImageTask> {\n let interval = FIRST_CHECK_MS;\n let first = true;\n for (;;) {\n if (!(first && input.checkNow)) {\n const left = input.deadline - Date.now();\n if (left <= 0) return { status: \"timeout\" };\n await pause(Math.min(interval, left), input.abortSignal);\n interval = Math.min(interval * INTERVAL_GROWTH, MAX_INTERVAL_MS);\n }\n first = false;\n try {\n const state = await input.check(input.abortSignal);\n if (state.status !== \"pending\") return state;\n } catch (error) {\n if (input.abortSignal?.aborted) throw input.abortSignal.reason ?? error;\n if (error instanceof ImageGenerationError) throw error;\n }\n if (Date.now() >= input.deadline) return { status: \"timeout\" };\n }\n}\n","import type { ImageGenerationRequest, ImageGenerationResult, ImageProviderConnection, ImageProviderDescription, ImageTaskState } from \"./image-catalog\";\nimport { ImageGenerationError } from \"./image-generation-error\";\nimport { hideSecrets } from \"./hide-secrets\";\nimport { waitForImageTask } from \"./image-task-polling\";\n\nconst KIE_URL = \"https://api.kie.ai/api/v1\";\n/** Цена кредита KIE (пакеты от $5 за 1000 кредитов). */\nconst CREDIT_USD = 0.005;\n/** Причина для модели — не длиннее этого, знаков. */\nconst MAX_REASON_LENGTH = 300;\n/** Ответ KIE на неизвестную задачу: `{ code: 422, msg: \"recordInfo is null\" }`. */\nconst UNKNOWN_TASK_CODE = 422;\n\ninterface KieAnswer {\n code?: unknown;\n msg?: unknown;\n data?: {\n taskId?: unknown;\n state?: unknown;\n resultJson?: unknown;\n failCode?: unknown;\n failMsg?: unknown;\n creditsConsumed?: unknown;\n } | null;\n}\n\nconst isStatus = (code: number) => Number.isInteger(code) && code >= 100 && code < 600;\n\nasync function readAnswer(response: Response): Promise<KieAnswer | undefined> {\n try {\n return (await response.json()) as KieAnswer;\n } catch {\n return undefined;\n }\n}\n\n/** Код ответа: KIE часто кладёт его в тело при HTTP 200. */\nfunction answerCode(response: Response, answer: KieAnswer | undefined): number {\n const code = typeof answer?.code === \"number\" ? answer.code : response.status;\n return response.ok && isStatus(code) ? code : response.status;\n}\n\nconst reasonOf = (code: number, answer: KieAnswer | undefined, connection: ImageProviderConnection) =>\n hideSecrets(`KIE ${code}${typeof answer?.msg === \"string\" && answer.msg ? `: ${answer.msg}` : \"\"}`, connection).slice(0, MAX_REASON_LENGTH);\n\nconst costOf = (credits: unknown) => (typeof credits === \"number\" && Number.isFinite(credits) && credits >= 0 ? credits * CREDIT_USD : undefined);\n\nfunction headersOf(connection: ImageProviderConnection): Record<string, string> {\n return { ...connection.headers, Authorization: `Bearer ${connection.apiKey}`, \"Content-Type\": \"application/json\" };\n}\n\n/** Тело `createTask`: модель и `input` с полями профиля. Модели правки получают исходник, а пропорцию берут у него. */\nfunction taskBody(request: ImageGenerationRequest) {\n return {\n model: request.providerModelId,\n input: {\n prompt: request.prompt,\n ...(request.imageUrl === undefined ? { aspect_ratio: request.aspectRatio ?? `${request.width}:${request.height}` } : { image_url: request.imageUrl }),\n ...request.providerSettings,\n },\n };\n}\n\n/** Адреса из `resultJson` (JSON-строка `{\"resultUrls\":[…]}`). */\nfunction resultUrls(resultJson: unknown): string[] {\n if (typeof resultJson !== \"string\") return [];\n try {\n const urls = (JSON.parse(resultJson) as { resultUrls?: unknown }).resultUrls;\n return Array.isArray(urls) ? urls.filter((url): url is string => typeof url === \"string\" && url.length > 0) : [];\n } catch {\n return [];\n }\n}\n\nasync function submit(request: ImageGenerationRequest, connection: ImageProviderConnection): Promise<string> {\n let response: Response;\n let answer: KieAnswer | undefined;\n try {\n response = await connection.fetch(`${connection.baseURL ?? KIE_URL}/jobs/createTask`, {\n method: \"POST\",\n headers: headersOf(connection),\n body: JSON.stringify(taskBody(request)),\n ...(request.abortSignal && { signal: request.abortSignal }),\n });\n answer = await readAnswer(response);\n } catch (error) {\n if (request.abortSignal?.aborted) throw request.abortSignal.reason ?? error;\n throw new ImageGenerationError(\"Провайдер недоступен\", { refusedBeforeWork: false });\n }\n const code = answerCode(response, answer);\n const taskId = answer?.data?.taskId;\n if (code === 200 && typeof taskId === \"string\" && taskId.length > 0) return taskId;\n if (code >= 400 && code < 500 && code !== 408) throw new ImageGenerationError(reasonOf(code, answer, connection), { refusedBeforeWork: true });\n throw new ImageGenerationError(`Провайдер ответил ошибкой: ${reasonOf(code, answer, connection)}`, { refusedBeforeWork: false });\n}\n\nasync function check(taskId: string, connection: ImageProviderConnection, abortSignal?: AbortSignal): Promise<ImageTaskState> {\n const response = await connection.fetch(`${connection.baseURL ?? KIE_URL}/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`, {\n headers: headersOf(connection),\n ...(abortSignal && { signal: abortSignal }),\n });\n const answer = await readAnswer(response);\n const code = answerCode(response, answer);\n if (code === UNKNOWN_TASK_CODE && !answer?.data) return { status: \"failed\", reason: \"Задача не найдена у провайдера\" };\n // Временный сбой: опрос повторит.\n if (code >= 500 || code === 408 || code === 429 || !answer) throw new Error(`KIE ответил ${code}`);\n if (code !== 200) throw new ImageGenerationError(reasonOf(code, answer, connection), { refusedBeforeWork: false });\n\n const data = answer.data ?? {};\n const costUsd = costOf(data.creditsConsumed);\n if (data.state === \"success\") {\n const urls = resultUrls(data.resultJson);\n if (urls.length === 0) return { status: \"failed\", reason: \"Провайдер не вернул картинку\", ...(costUsd !== undefined && { costUsd }) };\n // Цена — за задачу целиком: на первом адресе.\n const results: ImageGenerationResult[] = urls.map((url, index) => ({ url, ...(index === 0 && costUsd !== undefined && { costUsd }) }));\n return { status: \"ready\", results };\n }\n if (data.state === \"fail\") {\n const reason = [data.failCode, data.failMsg].filter((part) => (typeof part === \"string\" || typeof part === \"number\") && String(part).length > 0).join(\": \");\n return { status: \"failed\", reason: hideSecrets(reason || \"Провайдер не смог создать картинку\", connection).slice(0, MAX_REASON_LENGTH), ...(costUsd !== undefined && { costUsd }) };\n }\n return { status: \"pending\" };\n}\n\n/**\n * KIE.ai: задача в очереди (`createTask`) и проверка её состояния (`recordInfo`), Bearer-ключ `KIE_API_KEY`. Цена —\n * `creditsConsumed` × $0.005. Отказ 4xx при отправке — до работы, расхода нет; 5xx и обрыв — задача могла уйти.\n */\nexport const kieProvider: ImageProviderDescription = {\n id: \"kie\",\n displayName: \"KIE.ai\",\n apiKeyEnv: \"KIE_API_KEY\",\n baseURL: KIE_URL,\n queue: { submit, check },\n async generate(request, connection) {\n const deadline = Date.now() + request.timeoutMs;\n const taskId = await submit(request, connection);\n const outcome = await waitForImageTask({\n check: (abortSignal) => check(taskId, connection, abortSignal),\n deadline,\n ...(request.abortSignal && { abortSignal: request.abortSignal }),\n });\n if (outcome.status === \"ready\") return outcome.results;\n if (outcome.status === \"failed\") throw new ImageGenerationError(outcome.reason, { refusedBeforeWork: false });\n throw new ImageGenerationError(`Провайдер не ответил за ${Math.round(request.timeoutMs / 1000)} с`, { refusedBeforeWork: false });\n },\n};\n","import type { ImageGenerationRequest, ImageProviderConnection, ImageProviderDescription } from \"./image-catalog\";\nimport { ImageGenerationError } from \"./image-generation-error\";\nimport { hideSecrets } from \"./hide-secrets\";\n\nconst RUNWARE_URL = \"https://api.runware.ai/v1\";\n/** Причина для модели — не длиннее этого, знаков. */\nconst MAX_REASON_LENGTH = 300;\n/** Формат файла у Runware; без формата — JPG, как было. */\nconst outputFormats = { png: \"PNG\", jpeg: \"JPG\", webp: \"WEBP\" } as const;\n\ninterface RunwareError {\n code?: unknown;\n message?: unknown;\n}\ninterface RunwareAnswer {\n data?: Array<{ imageURL?: unknown; cost?: unknown }>;\n errors?: RunwareError[];\n}\n\n/** Задача `imageInference` по contracts/images.md: одна картинка, адрес вместо байтов, цена в ответе. */\nfunction taskBody(request: ImageGenerationRequest) {\n return [\n {\n taskType: \"imageInference\",\n taskUUID: request.taskUUID,\n model: request.providerModelId,\n positivePrompt: request.prompt,\n width: request.width,\n height: request.height,\n numberResults: 1,\n outputType: \"URL\",\n outputFormat: outputFormats[request.outputFormat ?? \"jpeg\"],\n deliveryMethod: \"sync\",\n includeCost: true,\n ...request.providerSettings,\n },\n ];\n}\n\n/** «код: текст» первых ошибок Runware, без ключа и заголовков подключения. */\nfunction errorsText(errors: readonly RunwareError[] | undefined, connection: ImageProviderConnection): string | undefined {\n const lines = (errors ?? [])\n .map((error) => [error.code, error.message].filter((part) => typeof part === \"string\" && part.length > 0).join(\": \"))\n .filter((line) => line.length > 0);\n if (lines.length === 0) return undefined;\n return hideSecrets(lines.join(\"; \"), connection).slice(0, MAX_REASON_LENGTH);\n}\n\nasync function readAnswer(response: Response): Promise<RunwareAnswer | undefined> {\n try {\n return (await response.json()) as RunwareAnswer;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Runware: `POST` задачи с Bearer-ключом и заголовками подключения. Отказ 4xx — до работы, расхода нет; 5xx, пустой\n * ответ, обрыв и срок — задача могла выполниться. «Стоп» пробрасывается как есть.\n */\nexport const runwareProvider: ImageProviderDescription = {\n id: \"runware\",\n displayName: \"Runware\",\n apiKeyEnv: \"RUNWARE_API_KEY\",\n baseURL: RUNWARE_URL,\n async generate(request, connection) {\n const timeout = new AbortController();\n const timer = setTimeout(() => timeout.abort(new DOMException(\"timeout\", \"TimeoutError\")), request.timeoutMs);\n const signal = request.abortSignal ? AbortSignal.any([request.abortSignal, timeout.signal]) : timeout.signal;\n let response: Response;\n let answer: RunwareAnswer | undefined;\n try {\n response = await connection.fetch(connection.baseURL ?? RUNWARE_URL, {\n method: \"POST\",\n headers: { ...connection.headers, Authorization: `Bearer ${connection.apiKey}`, \"Content-Type\": \"application/json\" },\n body: JSON.stringify(taskBody(request)),\n signal,\n });\n answer = await readAnswer(response);\n } catch (error) {\n if (request.abortSignal?.aborted) throw request.abortSignal.reason ?? error;\n if (timeout.signal.aborted) {\n throw new ImageGenerationError(`Провайдер не ответил за ${Math.round(request.timeoutMs / 1000)} с`, { refusedBeforeWork: false });\n }\n throw new ImageGenerationError(\"Провайдер недоступен\", { refusedBeforeWork: false });\n } finally {\n clearTimeout(timer);\n }\n\n const reason = errorsText(answer?.errors, connection);\n if (response.status >= 400 && response.status < 500 && response.status !== 408) {\n throw new ImageGenerationError(reason ?? `Провайдер отказал: ${response.status}`, { refusedBeforeWork: true });\n }\n if (!response.ok) throw new ImageGenerationError(`Провайдер ответил ошибкой ${response.status}${reason ? `: ${reason}` : \"\"}`, { refusedBeforeWork: false });\n const image = answer?.data?.[0];\n if (!image || typeof image.imageURL !== \"string\") {\n throw new ImageGenerationError(reason ?? \"Провайдер не вернул картинку\", { refusedBeforeWork: false });\n }\n const cost = typeof image.cost === \"number\" && Number.isFinite(image.cost) && image.cost >= 0 ? image.cost : undefined;\n return [{ url: image.imageURL, ...(cost !== undefined && { costUsd: cost }) }];\n },\n};\n","import { kieProvider } from \"./kie-provider\";\nimport { runwareProvider } from \"./runware-provider\";\n\n/** Пропорции, которые модель может заказать у `generate_images`. */\nexport type AspectRatio = \"1:1\" | \"3:2\" | \"2:3\" | \"16:9\" | \"9:16\";\n\nexport const aspectRatios: readonly AspectRatio[] = [\"1:1\", \"3:2\", \"2:3\", \"16:9\", \"9:16\"];\n\n/** Качество GPT Image у Runware. */\nexport type ImageQuality = \"low\" | \"medium\" | \"high\" | \"auto\";\n\nexport const imageQualities: readonly ImageQuality[] = [\"low\", \"medium\", \"high\", \"auto\"];\n\n/** Размер картинки: `default` — размер модели по умолчанию (`aspectRatios`), `1k` — длинная сторона около 1024. */\nexport type ImageSize = \"default\" | \"1k\";\n\nexport const imageSizes: readonly ImageSize[] = [\"default\", \"1k\"];\n\n/** Стороны картинки для каждой пропорции. */\nexport type ImageSizesByAspectRatio = Record<AspectRatio, { width: number; height: number }>;\n\n/** Размер модели, кроме размера по умолчанию: стороны и цена картинки в нём. */\nexport interface ImageSizeOption {\n aspectRatios: ImageSizesByAspectRatio;\n /** Цена картинки этого размера в качестве по умолчанию; нет — `pricePerImageUsd` модели. */\n pricePerImageUsd?: number;\n}\n\n/** Формат файла картинки. */\nexport type ImageFileFormat = \"png\" | \"jpeg\" | \"webp\";\n\n/** Заказ одной картинки провайдеру. */\nexport interface ImageGenerationRequest {\n /** Id задачи у провайдера: свой у каждой картинки. */\n taskUUID: string;\n /** Id модели у этого провайдера (`ImageModelProfile.providerModelIds[provider.id]`). */\n providerModelId: string;\n prompt: string;\n width: number;\n height: number;\n /** Пропорция для провайдеров, которые принимают её строкой (KIE): `ImageModelProfile.aspectRatioValues` или своя. */\n aspectRatio?: string;\n /** Исходная картинка для моделей правки (разделение на слои): адрес готовой картинки предыдущего шага. */\n imageUrl?: string;\n /** Формат файла; не указан — формат провайдера по умолчанию (у Runware JPG). Прозрачному фону нужен `png`. */\n outputFormat?: ImageFileFormat;\n /** Настройки модели у провайдера из профиля (`ImageModelProfile.providerSettings`) с качеством и фоном приложения. */\n providerSettings?: Record<string, unknown>;\n /** «Стоп» хода. */\n abortSignal?: AbortSignal;\n /** Срок одной генерации. */\n timeoutMs: number;\n}\n\n/** Как достучаться до провайдера: ключ из окружения сервера, адрес и заголовки подключения (прокси). */\nexport interface ImageProviderConnection {\n apiKey: string;\n baseURL?: string;\n headers?: Record<string, string>;\n fetch: typeof globalThis.fetch;\n}\n\n/** Результат одной задачи: адрес готовой картинки и цена, если провайдер её сообщил. */\nexport interface ImageGenerationResult {\n url: string;\n costUsd?: number;\n}\n\n/** Состояние задачи в очереди провайдера. `costUsd` провала — если провайдер сообщил, сколько списал. */\nexport type ImageTaskState =\n | { status: \"pending\" }\n | { status: \"ready\"; results: ImageGenerationResult[] }\n | { status: \"failed\"; reason: string; costUsd?: number };\n\n/**\n * Очередь провайдера (KIE и подобные): задача отправляется и проверяется отдельно. Так генерация переживает срок\n * вызова: опрос продолжается в фоне, а повтор того же вызова сначала проверяет старую задачу, а не платит за новую.\n * Отказ отправки — `ImageGenerationError` (как у `generate`). Сбой проверки: `ImageGenerationError` — проверять дальше\n * бессмысленно (неверный ключ), любая другая ошибка — временная, опрос повторит.\n */\nexport interface ImageTaskQueue {\n /** Отправить задачу; ответ — id задачи у провайдера. */\n submit(request: ImageGenerationRequest, connection: ImageProviderConnection): Promise<string>;\n check(taskId: string, connection: ImageProviderConnection, abortSignal?: AbortSignal): Promise<ImageTaskState>;\n}\n\n/**\n * Провайдер картинок (Runware и подобные). Сбой задачи — `ImageGenerationError`: его причина уходит модели, а\n * `refusedBeforeWork` говорит, что провайдер отказал до работы и расхода нет. Любая другая ошибка — «провайдер\n * недоступен», текст её наружу не идёт (в нём могут оказаться ключ или адрес).\n */\nexport interface ImageProviderDescription {\n id: string;\n displayName: string;\n /** Имя переменной окружения с ключом: `RUNWARE_API_KEY`. */\n apiKeyEnv: string;\n /** Адрес по умолчанию: `https://api.runware.ai/v1`. */\n baseURL?: string;\n headers?: Record<string, string>;\n /** Задача целиком: отправка и ожидание результата не дольше `request.timeoutMs`. */\n generate(request: ImageGenerationRequest, connection: ImageProviderConnection): Promise<ImageGenerationResult[]>;\n /** Есть у провайдеров с очередью: тогда `createImageGeneration` ведёт задачу сам (фоновый опрос, безопасный повтор). */\n queue?: ImageTaskQueue;\n}\n\n/** Профиль модели картинок. */\nexport interface ImageModelProfile {\n /** `gpt-image-2`. */\n id: string;\n displayName: string;\n /** Id модели у провайдера: `{ runware: \"openai:gpt-image@2\" }`. */\n providerModelIds: Record<string, string>;\n /** Размер по умолчанию для каждой пропорции. */\n aspectRatios: ImageSizesByAspectRatio;\n /** Другие размеры, которые приложение выбирает настройкой `size`. */\n sizes?: Partial<Record<Exclude<ImageSize, \"default\">, ImageSizeOption>>;\n /** Поля задачи для этой модели у провайдера: у Runware `providerSettings` или `settings` с качеством, у KIE — поля `input`. */\n providerSettings?: Record<string, unknown>;\n /**\n * Где в `providerSettings` лежат качество и фон: у Runware GPT Image 2 — `[\"providerSettings\", \"openai\"]`, у 2.5 —\n * `[\"settings\"]`. Нет — модель не принимает `quality`, и настройка с ним — ошибка.\n */\n settingsPath?: readonly string[];\n /** Модель умеет прозрачный фон (`background: \"transparent\"` по `settingsPath`). */\n transparentBackground?: boolean;\n /** Пропорция у провайдера, если она не совпадает с заказанной: z-image не знает 3:2 — берётся 4:3. */\n aspectRatioValues?: Partial<Record<AspectRatio, string>>;\n /** Модель правит готовую картинку (разделение на слои): ей нужен `imageUrl`, сама по себе она не рисует. */\n needsSourceImage?: boolean;\n /** Первый адрес ответа — основа (фон без предмета), а не результат: сохраняется следующий. */\n firstResultIsBase?: boolean;\n /**\n * Цена картинки размера по умолчанию в качестве по умолчанию. По ней (с размером и качеством) заказ бронируется в\n * лимитах, и она же записывается, если провайдер не сообщил цену или человек нажал «Стоп». С запасом: лучше записать лишнее.\n */\n pricePerImageUsd: number;\n /** Во сколько раз дороже картинка в другом качестве, чем в качестве по умолчанию; нет — так же. */\n qualityPriceFactors?: Partial<Record<ImageQuality, number>>;\n}\n\n/** Каталог моделей и провайдеров картинок; правится `extendImageCatalog`, как каталог моделей ядра. */\nexport interface ImageCatalog {\n providers: ImageProviderDescription[];\n models: ImageModelProfile[];\n}\n\n/** Подключение провайдера через прокси, как у провайдеров моделей ядра. */\nexport interface ImageProviderConnectionChange {\n baseURL?: string;\n headers?: Record<string, string>;\n}\n\nexport interface ImageCatalogChange {\n removeModels?: string[];\n addModels?: ImageModelProfile[];\n addProviders?: ImageProviderDescription[];\n connectProviders?: Record<string, ImageProviderConnectionChange>;\n}\n\n/**\n * Наименьший размер GPT Image у Runware: стороны кратны 16, площадь не меньше 655 360 пикселей,\n * пропорция точнее 1 %.\n */\nconst gptImageSizes: ImageSizesByAspectRatio = {\n \"1:1\": { width: 816, height: 816 },\n \"3:2\": { width: 1008, height: 672 },\n \"2:3\": { width: 672, height: 1008 },\n \"16:9\": { width: 1088, height: 608 },\n \"9:16\": { width: 608, height: 1088 },\n};\n\n/**\n * GPT Image в 1K: длинная сторона около 1024 при тех же условиях Runware. Кроме 1:1 совпадает с наименьшим размером:\n * 1024×576 для 16:9 Runware отвергает (меньше 655 360 пикселей).\n */\nconst gptImage1kSizes: ImageSizesByAspectRatio = { ...gptImageSizes, \"1:1\": { width: 1024, height: 1024 } };\n\n/**\n * Seedream 5.0 в разрешении 1K: площадь от 0,92 Мп. Проверено живым запросом 3 октября 2026: Runware принимает `width` и `height`\n * из этой таблицы без отдельного поля разрешения (1024×1024, цена $0.0193 в ответе), поэтому профиль без `providerSettings`.\n */\nconst seedreamSizes: ImageSizesByAspectRatio = {\n \"1:1\": { width: 1024, height: 1024 },\n \"3:2\": { width: 1248, height: 832 },\n \"2:3\": { width: 832, height: 1248 },\n \"16:9\": { width: 1280, height: 720 },\n \"9:16\": { width: 720, height: 1280 },\n};\n\n/**\n * z-image у KIE: размер задаётся пропорцией, 1:1 — 1024×1024 (живой запрос 5 октября 2026). Остальные стороны —\n * примерные для заглушки на экране: настоящий размер берётся из скачанного PNG.\n */\nconst zImageSizes: ImageSizesByAspectRatio = {\n \"1:1\": { width: 1024, height: 1024 },\n \"3:2\": { width: 1152, height: 864 },\n \"2:3\": { width: 864, height: 1152 },\n \"16:9\": { width: 1280, height: 720 },\n \"9:16\": { width: 720, height: 1280 },\n};\n\nconst lowQualityGpt2 = { providerSettings: { openai: { quality: \"low\" } } };\nconst lowQualityGpt25 = { settings: { quality: \"low\" } };\n/**\n * Цена GPT Image растёт с качеством примерно как у OpenAI: medium вчетверо дороже low, high — в 16 раз. `auto` может\n * выбрать high — бронируется как high.\n */\nconst gptQualityPriceFactors = { medium: 4, high: 16, auto: 16 };\n/**\n * Цены — замер Runware 5 октября 2026 (цена из ответа, low): 1024×1024 — $0.00602 у 2 и 2.5 Flare, $0.00599 у 2.5 Sunburst,\n * с прозрачным фоном так же. Наименьший размер — по площади (816² ≈ 0,64 от 1024²), с запасом.\n */\nconst gptPrices = { pricePerImageUsd: 0.005, qualityPriceFactors: gptQualityPriceFactors, sizes: { \"1k\": { aspectRatios: gptImage1kSizes, pricePerImageUsd: 0.007 } } };\n/** GPT Image 2 у Runware не умеет прозрачный фон: на `background: \"transparent\"` — `invalidOpenAiBackground` (только opaque и auto). */\nconst gpt2Settings = { providerSettings: lowQualityGpt2, settingsPath: [\"providerSettings\", \"openai\"], ...gptPrices };\nconst gpt25Settings = { providerSettings: lowQualityGpt25, settingsPath: [\"settings\"], transparentBackground: true, ...gptPrices };\n/** Seedream и z-image рисуют в 1K и так: размер 1K у них — размер по умолчанию. */\nconst sizes1k = (aspectRatios: ImageSizesByAspectRatio) => ({ sizes: { \"1k\": { aspectRatios } } });\n/** Seedream у KIE: 1K и PNG (разделению на слои PNG нужен и для основы). */\nconst kieSeedreamSettings = { size: \"1K\", output_format: \"png\" };\n\n/**\n * Runware и пять моделей (research.md, раздел 9); каждая в наименьшем разрешении и низшем качестве. KIE — три модели:\n * z-image (дешёвая картинка без текста), Seedream 5.0 Flash (держит надписи) и разделение готовой картинки на слои\n * (предмет без фона). Цены KIE — живые запросы 5 октября 2026, с запасом.\n */\nexport const standardImageCatalog: ImageCatalog = {\n providers: [runwareProvider, kieProvider],\n models: [\n {\n id: \"gpt-image-2\",\n displayName: \"GPT Image 2\",\n providerModelIds: { runware: \"openai:gpt-image@2\" },\n aspectRatios: gptImageSizes,\n ...gpt2Settings,\n },\n {\n id: \"gpt-image-2.5-flare\",\n displayName: \"GPT Image 2.5 Flare\",\n providerModelIds: { runware: \"openai:gpt-image@2.5-flare\" },\n aspectRatios: gptImageSizes,\n ...gpt25Settings,\n },\n {\n id: \"gpt-image-2.5-sunburst\",\n displayName: \"GPT Image 2.5 Sunburst\",\n providerModelIds: { runware: \"openai:gpt-image@2.5-sunburst\" },\n aspectRatios: gptImageSizes,\n ...gpt25Settings,\n },\n {\n id: \"seedream-5-flash\",\n displayName: \"Seedream 5.0 Flash\",\n providerModelIds: { runware: \"bytedance:seedream@5.0-flash\" },\n aspectRatios: seedreamSizes,\n ...sizes1k(seedreamSizes),\n pricePerImageUsd: 0.02,\n },\n {\n id: \"seedream-5-lite\",\n displayName: \"Seedream 5.0 Lite\",\n providerModelIds: { runware: \"bytedance:seedream@5.0-lite\" },\n aspectRatios: seedreamSizes,\n ...sizes1k(seedreamSizes),\n pricePerImageUsd: 0.04,\n },\n {\n id: \"z-image\",\n displayName: \"Z-Image\",\n providerModelIds: { kie: \"z-image\" },\n aspectRatios: zImageSizes,\n ...sizes1k(zImageSizes),\n aspectRatioValues: { \"3:2\": \"4:3\", \"2:3\": \"3:4\" },\n // 0,8 кредита = $0.004.\n pricePerImageUsd: 0.005,\n },\n {\n id: \"seedream-5-flash-kie\",\n displayName: \"Seedream 5.0 Flash (KIE)\",\n providerModelIds: { kie: \"seedream/5-flash-text-to-image\" },\n aspectRatios: seedreamSizes,\n ...sizes1k(seedreamSizes),\n providerSettings: kieSeedreamSettings,\n // 3,24 кредита = $0.0162 за 1K.\n pricePerImageUsd: 0.02,\n },\n {\n id: \"seedream-5-flash-layers\",\n displayName: \"Seedream 5.0 Flash: разделение на слои\",\n providerModelIds: { kie: \"seedream/5-flash-layer-decomposition\" },\n aspectRatios: seedreamSizes,\n ...sizes1k(seedreamSizes),\n providerSettings: kieSeedreamSettings,\n needsSourceImage: true,\n firstResultIsBase: true,\n // Основа и один слой — 6,48 кредита ($0.032); без подсказки слоёв больше и дороже ($0.065).\n pricePerImageUsd: 0.07,\n },\n ],\n};\n\n/** Каталог с правками приложения; исходный каталог не меняется. Несогласованная правка — ошибка настройки. */\nexport function extendImageCatalog(base: ImageCatalog, change: ImageCatalogChange): ImageCatalog {\n for (const id of change.removeModels ?? []) {\n if (!base.models.some((model) => model.id === id)) throw new Error(`Убрать нечего: модели картинок «${id}» нет в каталоге`);\n }\n const removed = new Set(change.removeModels ?? []);\n const added = change.addModels ?? [];\n const addedIds = new Set(added.map((model) => model.id));\n const models = [...base.models.filter((model) => !removed.has(model.id) && !addedIds.has(model.id)), ...added];\n\n const addedProviders = change.addProviders ?? [];\n const addedProviderIds = new Set(addedProviders.map((provider) => provider.id));\n const providers = [...base.providers.filter((provider) => !addedProviderIds.has(provider.id)), ...addedProviders];\n const connections = change.connectProviders ?? {};\n for (const providerId of Object.keys(connections)) {\n if (!providers.some((provider) => provider.id === providerId)) {\n throw new Error(`Подключить через прокси нечего: провайдера картинок «${providerId}» нет в каталоге`);\n }\n }\n const connected = providers.map((provider) => {\n const connection = connections[provider.id];\n if (!connection) return provider;\n return {\n ...provider,\n ...(connection.baseURL !== undefined && { baseURL: connection.baseURL }),\n ...(connection.headers !== undefined && { headers: { ...connection.headers } }),\n };\n });\n\n const providerIds = new Set(connected.map((provider) => provider.id));\n for (const model of added) {\n const unknown = Object.keys(model.providerModelIds).filter((providerId) => !providerIds.has(providerId));\n if (unknown.length > 0) throw new Error(`Модель картинок «${model.id}»: провайдера ${unknown.map((id) => `«${id}»`).join(\", \")} нет в каталоге`);\n }\n return { providers: connected, models };\n}\n"],"mappings":"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","names":["MAX_REASON_LENGTH","taskBody","readAnswer","aspectRatios"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/image-queue/image-job-store.ts"],"sourcesContent":["import type { AspectRatio } from \"../image-catalog\";\n\n/**\n * Что с задачей картинки.\n * - `generating` — готовится: отправка, опрос, скачивание, доставка.\n * - `ready` — приёмник принял картинку.\n * - `failed` — не получилась; при `lateUntil` задача у провайдера ещё опрашивается и может прийти поздно.\n * - `superseded` — на то же место заказана новая картинка или задачу остановили.\n * - `discarded` — приёмник отказался от готовой картинки (место уже занято).\n */\nexport type ImageJobStatus = \"generating\" | \"ready\" | \"failed\" | \"superseded\" | \"discarded\";\n\n/** Оплата одной задачи провайдера. `usageId` — id записи расхода: вторая вставка того же id отвергается базой. */\nexport interface ImageJobCharge {\n usageId: string;\n modelId: string;\n providerId: string;\n costUsd: number;\n /** Запись расхода сделана. */\n recorded: boolean;\n}\n\n/** Задача шага, которая ушла провайдеру (или уходит прямо сейчас) и ещё не оплачена. */\nexport interface ImageJobTask {\n usageId: string;\n modelId: string;\n providerId: string;\n /** Цена модели: так задача оплачивается, если провайдер не сообщил свою (срок, обрыв, вытеснение). */\n priceUsd: number;\n sentAt: string;\n /** Id задачи в очереди провайдера; нет — отправка не закончилась или провайдер без очереди. */\n taskId?: string;\n}\n\n/** Шаг задачи: картинка, затем, если нужно, её же без фона. */\nexport interface ImageJobStep {\n task?: ImageJobTask;\n /** Адреса готового результата: дальше шаг не заказывается, только скачивается. */\n urls?: string[];\n charges: ImageJobCharge[];\n}\n\n/** Задача картинки, как она лежит в хранилище. Времена — ISO 8601. */\nexport interface ImageJobRecord {\n id: string;\n /** Место картинки: новый заказ с тем же `key` у того же владельца вытесняет прежний. */\n key: string;\n ownerId: string;\n chatId: string;\n turnId?: string;\n /** Группа для списка (например, приложение, в которое идут картинки). */\n group?: string;\n /** Назначение; `\"\"` — без назначений. */\n purpose: string;\n prompt: string;\n aspectRatio: AspectRatio;\n /** Данные приложения (путь, размер): отдаются приёмнику как есть. Только JSON. */\n data?: unknown;\n status: ImageJobStatus;\n reason?: string;\n /** Ответ приёмника (JSON). */\n result?: unknown;\n /** У задачи есть работа: опрос, позднее окно или незаписанный расход. Такие задачи подбирает старт сервера. */\n active: boolean;\n /** Номер запуска: «Повторить» начинает новый, прежний больше ничего не делает. */\n run: number;\n /** Номер попытки запуска, с 1. */\n attempt: number;\n attemptDeadline: string;\n /** Позднее окно: попытки кончились, а задача у провайдера ещё опрашивается. */\n lateUntil?: string;\n /** Готовая картинка не дошла до приёмника (скачивание, обработка, сам приёмник): «Повторить» сначала пробует тот же адрес. */\n deliveryFailed?: boolean;\n steps: ImageJobStep[];\n leaseOwner?: string;\n leaseUntil?: string;\n /** Версия записи: каждая правка условна по ней и прибавляет 1. */\n version: number;\n createdAt: string;\n updatedAt: string;\n finishedAt?: string;\n}\n\n/** Правка задачи: переданный `undefined` стирает поле. */\nexport type ImageJobPatch = Partial<Omit<ImageJobRecord, \"id\" | \"key\" | \"ownerId\" | \"chatId\" | \"createdAt\" | \"version\">>;\n\nexport interface ImageJobFilter {\n ownerId?: string;\n chatId?: string;\n key?: string;\n group?: string;\n}\n\n/** У задачи ещё идёт работа с провайдером: генерация или позднее окно. Открытая на место (`ownerId` + `key`) — одна. */\nexport const isOpenImageJob = (job: Pick<ImageJobRecord, \"status\" | \"lateUntil\">) => job.status === \"generating\" || job.lateUntil !== undefined;\n\n/** На это место (`ownerId` + `key`) уже есть открытая задача: вторую база не пускает, даже с двух копий сервера. */\nexport class ImageJobKeyTakenError extends Error {\n constructor(readonly ownerId: string, readonly key: string) {\n super(`Картинки: на место ${key} уже есть открытая задача`);\n this.name = \"ImageJobKeyTakenError\";\n }\n}\n\n/**\n * Хранилище задач картинок: память, SQLite, PostgreSQL (договор один). Открытая задача на место — одна: `insert` и\n * `update`, после которых их стало бы две, бросают `ImageJobKeyTakenError` и ничего не пишут.\n */\nexport interface ImageJobStore {\n insert(job: ImageJobRecord): Promise<void>;\n get(id: string): Promise<ImageJobRecord | null>;\n /** По порядку создания. */\n list(filter: ImageJobFilter): Promise<ImageJobRecord[]>;\n /** Условная запись: версия в базе равна `expectedVersion` — правка и версия +1; иначе `null`, запись прежняя. */\n update(id: string, patch: ImageJobPatch, expectedVersion: number): Promise<ImageJobRecord | null>;\n /** Задачи с работой (`active`), чья аренда свободна или истекла к `now`. */\n listResumable(now: string): Promise<ImageJobRecord[]>;\n /** Удаляет задачи без работы, законченные раньше `before`; ответ — сколько удалено. */\n deleteFinishedBefore(before: string): Promise<number>;\n close?(): Promise<void>;\n}\n\n/** Ошибка базы или её причина (`cause`, драйвер под drizzle), подходящая под `test`. */\nexport function findErrorCause(error: unknown, test: (cause: Record<string, unknown>) => boolean): boolean {\n for (let current = error, depth = 0; current !== null && typeof current === \"object\" && depth < 5; current = (current as { cause?: unknown }).cause, depth++) {\n if (test(current as Record<string, unknown>)) return true;\n }\n return false;\n}\n\n/** Колонки поиска; остальное хранилища держат одним JSON. */\nexport const IMAGE_JOB_COLUMNS = [\"id\", \"key\", \"ownerId\", \"chatId\", \"group\", \"status\", \"active\", \"leaseOwner\", \"leaseUntil\", \"version\", \"createdAt\", \"updatedAt\", \"finishedAt\"] as const;\n\n/** Запись с правкой: `undefined` в правке стирает поле. */\nexport function applyImageJobPatch(job: ImageJobRecord, patch: ImageJobPatch): ImageJobRecord {\n const next = { ...job } as Record<string, unknown>;\n for (const [field, value] of Object.entries(patch)) {\n if (value === undefined) delete next[field];\n else next[field] = value;\n }\n return next as unknown as ImageJobRecord;\n}\n\n/** Часть записи, которая хранится JSON: всё, кроме колонок поиска. */\nexport function imageJobBody(job: ImageJobRecord): Record<string, unknown> {\n const body = { ...job } as Record<string, unknown>;\n for (const column of IMAGE_JOB_COLUMNS) delete body[column];\n return body;\n}\n\nconst matches = (job: ImageJobRecord, filter: ImageJobFilter) =>\n (filter.ownerId === undefined || job.ownerId === filter.ownerId) &&\n (filter.chatId === undefined || job.chatId === filter.chatId) &&\n (filter.key === undefined || job.key === filter.key) &&\n (filter.group === undefined || job.group === filter.group);\n\n/** Хранилище в памяти: по умолчанию и для тестов, живёт до перезапуска. */\nexport function memoryImageJobStore(): ImageJobStore {\n const jobs = new Map<string, ImageJobRecord>();\n const copy = (job: ImageJobRecord) => structuredClone(job);\n const checkPlace = (job: ImageJobRecord) => {\n if (!isOpenImageJob(job)) return;\n for (const other of jobs.values()) {\n if (other.id !== job.id && other.ownerId === job.ownerId && other.key === job.key && isOpenImageJob(other)) throw new ImageJobKeyTakenError(job.ownerId, job.key);\n }\n };\n return {\n async insert(job) {\n if (jobs.has(job.id)) throw new Error(`Задача картинки ${job.id} уже есть`);\n checkPlace(job);\n jobs.set(job.id, copy(job));\n },\n async get(id) {\n const job = jobs.get(id);\n return job ? copy(job) : null;\n },\n async list(filter) {\n return [...jobs.values()].filter((job) => matches(job, filter)).sort((a, b) => a.createdAt.localeCompare(b.createdAt)).map(copy);\n },\n async update(id, patch, expectedVersion) {\n const job = jobs.get(id);\n if (!job || job.version !== expectedVersion) return null;\n const next = { ...applyImageJobPatch(job, structuredClone(patch)), version: job.version + 1 };\n checkPlace(next);\n jobs.set(id, next);\n return copy(next);\n },\n async listResumable(now) {\n return [...jobs.values()].filter((job) => job.active && (job.leaseUntil === undefined || job.leaseUntil < now)).map(copy);\n },\n async deleteFinishedBefore(before) {\n let deleted = 0;\n for (const job of jobs.values()) {\n if (!job.active && job.status !== \"generating\" && job.finishedAt !== undefined && job.finishedAt < before) {\n jobs.delete(job.id);\n deleted++;\n }\n }\n return deleted;\n },\n };\n}\n"],"mappings":"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","names":[]}