ima2-gen 2.0.15 → 2.0.16
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/README.md +30 -0
- package/bin/commands/skill.js +336 -17
- package/bin/ima2.js +22 -2
- package/bin/lib/doctor-checks.js +6 -4
- package/config.js +1 -0
- package/docs/CLI.md +25 -3
- package/docs/PROMPT_STUDIO.ko.md +29 -0
- package/docs/PROMPT_STUDIO.md +31 -0
- package/docs/migration/runtime-test-inventory.md +7 -1
- package/lib/agentImageVideoGen.js +8 -0
- package/lib/agentQueueStore.js +36 -10
- package/lib/agentQueueWorker.js +79 -7
- package/lib/agentRuntime.js +2 -0
- package/lib/assetLifecycle.js +40 -8
- package/lib/canvasVersionStore.js +86 -0
- package/lib/cardNewsGenerator.js +37 -3
- package/lib/cardNewsJobStore.js +10 -0
- package/lib/cardNewsManifestStore.js +6 -12
- package/lib/cardNewsPath.js +25 -0
- package/lib/db.js +2 -0
- package/lib/generatePipeline.js +474 -0
- package/lib/generationInputValidation.js +32 -0
- package/lib/grokImageCore.js +18 -1
- package/lib/historyIndex.js +11 -4
- package/lib/historyList.js +3 -0
- package/lib/imageMetadata.js +3 -0
- package/lib/inflight.js +30 -3
- package/lib/multimodePipeline.js +462 -0
- package/lib/nodeGeneration.js +440 -0
- package/lib/nodeValidation.js +23 -0
- package/lib/oauthProxy/generators.js +3 -246
- package/lib/oauthProxy/multimodeGenerators.js +251 -0
- package/lib/routeHelpers.js +9 -2
- package/package.json +2 -2
- package/routes/agent.js +4 -4
- package/routes/canvasVersions.js +21 -1
- package/routes/cardNews.js +2 -1
- package/routes/edit.js +25 -9
- package/routes/events.js +5 -1
- package/routes/generate.js +2 -481
- package/routes/keys.js +31 -39
- package/routes/multimode.js +2 -502
- package/routes/nodes.js +3 -448
- package/routes/video.js +12 -3
- package/routes/videoExtended.js +19 -4
- package/server.js +53 -1
- package/skills/ima2/SKILL.md +204 -0
- package/skills/ima2-front/SKILL.md +611 -0
- package/skills/ima2-front/references/a11y-patterns.md +134 -0
- package/skills/ima2-front/references/aesthetics.md +327 -0
- package/skills/ima2-front/references/anti-slop.md +241 -0
- package/skills/ima2-front/references/asset-requirements.md +431 -0
- package/skills/ima2-front/references/brand-asset-sourcing.md +122 -0
- package/skills/ima2-front/references/color-system.md +132 -0
- package/skills/ima2-front/references/consistency-locks.md +20 -0
- package/skills/ima2-front/references/crud-ui.md +53 -0
- package/skills/ima2-front/references/dropdown-layer.md +110 -0
- package/skills/ima2-front/references/i18n-global.md +104 -0
- package/skills/ima2-front/references/iterative-design.md +90 -0
- package/skills/ima2-front/references/korea-2026.md +174 -0
- package/skills/ima2-front/references/layout-discipline.md +161 -0
- package/skills/ima2-front/references/liquid-glass.md +181 -0
- package/skills/ima2-front/references/logo-sections.md +194 -0
- package/skills/ima2-front/references/mobile-ux.md +144 -0
- package/skills/ima2-front/references/performance-budget.md +106 -0
- package/skills/ima2-front/references/preflight-full.md +58 -0
- package/skills/ima2-front/references/product-density.md +53 -0
- package/skills/ima2-front/references/prototype-variants.md +54 -0
- package/skills/ima2-front/references/reference-capture.md +120 -0
- package/skills/ima2-front/references/responsive-viewport.md +139 -0
- package/skills/ima2-front/references/seo-baseline.md +123 -0
- package/skills/ima2-front/references/soft-3d-asset-gates.md +70 -0
- package/skills/ima2-front/references/stacks/astro.md +204 -0
- package/skills/ima2-front/references/stacks/mobile-native.md +281 -0
- package/skills/ima2-front/references/stacks/nextjs.md +219 -0
- package/skills/ima2-front/references/stacks/react.md +264 -0
- package/skills/ima2-front/references/stacks/svelte.md +235 -0
- package/skills/ima2-front/references/stacks/vanilla.md +226 -0
- package/skills/ima2-front/references/theme-switching.md +82 -0
- package/skills/ima2-front/references/top-bar.md +126 -0
- package/skills/ima2-front/references/typography-wrapping.md +270 -0
- package/skills/ima2-front/references/ux-writing-ko.md +62 -0
- package/skills/ima2-front/references/visual-verification.md +102 -0
- package/skills/ima2-uiux/SKILL.md +477 -0
- package/skills/ima2-uiux/references/anti-slop-judgment.md +39 -0
- package/skills/ima2-uiux/references/color-system.md +80 -0
- package/skills/ima2-uiux/references/design-isms.md +194 -0
- package/skills/ima2-uiux/references/design-read-example.md +33 -0
- package/skills/ima2-uiux/references/design-system-bootstrap.md +133 -0
- package/skills/ima2-uiux/references/favicon-logo.md +355 -0
- package/skills/ima2-uiux/references/form-patterns.md +118 -0
- package/skills/ima2-uiux/references/intent-discovery-ladder.md +86 -0
- package/skills/ima2-uiux/references/korean-design-vocabulary.md +52 -0
- package/skills/ima2-uiux/references/layout-macrostructures.md +54 -0
- package/skills/ima2-uiux/references/logo-trust-sections.md +96 -0
- package/skills/ima2-uiux/references/mobile-native-ux.md +207 -0
- package/skills/ima2-uiux/references/product-personalities.md +118 -0
- package/skills/ima2-uiux/references/responsive-nav.md +66 -0
- package/skills/ima2-uiux/references/typography-line-breaks.md +193 -0
- package/skills/ima2-uiux/references/ux-preflight.md +65 -0
- package/skills/ima2-uiux/references/ux-states.md +197 -0
- package/skills/ima2-uiux/references/visual-hierarchy.md +115 -0
- package/ui/dist/.vite/manifest.json +13 -13
- package/ui/dist/assets/AgentWorkspace-CtYt4SF5.js +3 -0
- package/ui/dist/assets/{CardNewsWorkspace-Cdxn1ohU.js → CardNewsWorkspace-DN5cVqG5.js} +1 -1
- package/ui/dist/assets/GenerationRequestLogPanel-DnmUQdKR.js +1 -0
- package/ui/dist/assets/{NodeCanvas-C67sLboA.js → NodeCanvas-CxpfIBI0.js} +1 -1
- package/ui/dist/assets/PromptBuilderPanel-C2R3ObtR.js +2 -0
- package/ui/dist/assets/{PromptImportDialog-DeQXsEEv.js → PromptImportDialog-BIxz_yEx.js} +2 -2
- package/ui/dist/assets/PromptImportDiscoverySection-BQTlPMME.js +1 -0
- package/ui/dist/assets/PromptImportFolderSection-Ds_Th9O3.js +1 -0
- package/ui/dist/assets/{PromptLibraryPanel-Db3xEPM_.js → PromptLibraryPanel-Rs4l45pV.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-BDk_1W-V.js → SettingsWorkspace-p1AZ6uT9.js} +1 -1
- package/ui/dist/assets/index-0-_vgFGs.js +4 -0
- package/ui/dist/assets/index-Dm3pFxV4.js +23 -0
- package/ui/dist/assets/index-J8yDF3Ch.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/AgentWorkspace-DAg_7zpY.js +0 -3
- package/ui/dist/assets/GenerationRequestLogPanel-C_qvEUi2.js +0 -1
- package/ui/dist/assets/PromptBuilderPanel-DjvVdAie.js +0 -2
- package/ui/dist/assets/PromptImportDiscoverySection-CrwyUKYI.js +0 -1
- package/ui/dist/assets/PromptImportFolderSection-CToGMqYE.js +0 -1
- package/ui/dist/assets/index-2oG6in1i.css +0 -1
- package/ui/dist/assets/index-CoxMOXAU.js +0 -23
- package/ui/dist/assets/index-p8egnGHk.js +0 -4
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { setJobPhase } from "../inflight.js";
|
|
2
2
|
import { logEvent } from "../logger.js";
|
|
3
|
-
import { compressReferenceB64ForOAuth } from "../referenceImageCompress.js";
|
|
4
3
|
import { safeReferenceDiagnostics } from "../refs.js";
|
|
5
4
|
import { throwOAuthHttpError, throwOAuthTimeoutError } from "./errors.js";
|
|
6
|
-
import {
|
|
5
|
+
import { GENERATE_DEVELOPER_PROMPT, GENERATE_NO_SEARCH_DEVELOPER_PROMPT, buildUserTextPrompt, } from "./prompts.js";
|
|
7
6
|
import { normalizeReferenceForOAuth } from "./references.js";
|
|
8
7
|
import { buildImageTools, createOAuthGenerationTimeout, fetchOAuth, getOAuthUrl, resolveReasoningEffort, resolveWebSearchEnabled, summarizeEventTypes, waitForOAuthReady, } from "./runtime.js";
|
|
9
|
-
import { readImageStream
|
|
10
|
-
import { config } from "../../config.js";
|
|
11
|
-
const RESEARCH_SUFFIX = config.oauth.researchSuffix;
|
|
8
|
+
import { readImageStream } from "./streams.js";
|
|
12
9
|
export async function generateViaOAuth(prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx = {}, options = {}) {
|
|
13
10
|
await waitForOAuthReady(ctx);
|
|
14
11
|
const oauthUrl = getOAuthUrl(ctx);
|
|
@@ -184,244 +181,4 @@ export async function generateViaOAuth(prompt, quality, size, moderation = "low"
|
|
|
184
181
|
timeout.clear();
|
|
185
182
|
}
|
|
186
183
|
}
|
|
187
|
-
export
|
|
188
|
-
await waitForOAuthReady(ctx);
|
|
189
|
-
const oauthUrl = getOAuthUrl(ctx);
|
|
190
|
-
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
|
|
191
|
-
const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config?.limits?.maxGeneratedImages) || 24));
|
|
192
|
-
const maxImages = Math.min(maxGeneratedImages, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
|
|
193
|
-
const webSearchEnabled = resolveWebSearchEnabled(options);
|
|
194
|
-
const tools = buildImageTools(webSearchEnabled, {
|
|
195
|
-
quality,
|
|
196
|
-
size,
|
|
197
|
-
moderation,
|
|
198
|
-
...(options.partialImages ? { partial_images: options.partialImages } : {}),
|
|
199
|
-
});
|
|
200
|
-
const referenceInputs = references.map(normalizeReferenceForOAuth);
|
|
201
|
-
const userText = buildMultimodeSequencePrompt(mode === "direct"
|
|
202
|
-
? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
|
|
203
|
-
: `${prompt}${webSearchEnabled ? RESEARCH_SUFFIX : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled, size });
|
|
204
|
-
const userContent = referenceInputs.length
|
|
205
|
-
? [
|
|
206
|
-
...referenceInputs.map(({ b64, requestMime }) => ({
|
|
207
|
-
type: "input_image",
|
|
208
|
-
image_url: `data:${requestMime};base64,${b64}`,
|
|
209
|
-
})),
|
|
210
|
-
{ type: "input_text", text: userText },
|
|
211
|
-
]
|
|
212
|
-
: userText;
|
|
213
|
-
logEvent("oauth-multimode", "request", {
|
|
214
|
-
requestId,
|
|
215
|
-
model,
|
|
216
|
-
refsCount: referenceInputs.length,
|
|
217
|
-
maxImages,
|
|
218
|
-
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
219
|
-
webSearchEnabled,
|
|
220
|
-
});
|
|
221
|
-
const reasoningEffort = resolveReasoningEffort(ctx, options);
|
|
222
|
-
const developerPrompt = webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT;
|
|
223
|
-
const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-multimode");
|
|
224
|
-
try {
|
|
225
|
-
const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
226
|
-
method: "POST",
|
|
227
|
-
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
228
|
-
signal: options.signal || timeout.signal,
|
|
229
|
-
body: JSON.stringify({
|
|
230
|
-
model,
|
|
231
|
-
input: [
|
|
232
|
-
{ role: "developer", content: `${developerPrompt}\n\nN = ${maxImages}.` },
|
|
233
|
-
{ role: "user", content: userContent },
|
|
234
|
-
],
|
|
235
|
-
tools,
|
|
236
|
-
tool_choice: "required",
|
|
237
|
-
reasoning: { effort: reasoningEffort },
|
|
238
|
-
stream: true,
|
|
239
|
-
}),
|
|
240
|
-
}, { requestId, scope: "oauth-multimode" });
|
|
241
|
-
logEvent("oauth-multimode", "response", {
|
|
242
|
-
requestId,
|
|
243
|
-
model,
|
|
244
|
-
status: res.status,
|
|
245
|
-
contentType: res.headers.get("content-type"),
|
|
246
|
-
});
|
|
247
|
-
if (!res.ok) {
|
|
248
|
-
const text = await res.text();
|
|
249
|
-
logEvent("oauth-multimode", "error_response", { requestId, status: res.status, errorChars: text.length });
|
|
250
|
-
throwOAuthHttpError(res, text, {
|
|
251
|
-
requestId,
|
|
252
|
-
scope: "oauth-multimode",
|
|
253
|
-
fallbackMessage: `OAuth proxy returned ${res.status}`,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
if (requestId)
|
|
257
|
-
setJobPhase(requestId, "streaming");
|
|
258
|
-
const contentType = res.headers.get("content-type") || "";
|
|
259
|
-
if (!contentType.includes("text/event-stream")) {
|
|
260
|
-
const json = await res.json();
|
|
261
|
-
const images = [];
|
|
262
|
-
for (const item of json.output || []) {
|
|
263
|
-
if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
|
|
264
|
-
images.push({
|
|
265
|
-
b64: item.result,
|
|
266
|
-
revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return {
|
|
271
|
-
images,
|
|
272
|
-
usage: json.usage || null,
|
|
273
|
-
webSearchCalls: 0,
|
|
274
|
-
eventCount: 0,
|
|
275
|
-
eventTypes: {},
|
|
276
|
-
extraIgnored: 0,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
const result = await readMultimodeImageStream(res, {
|
|
280
|
-
requestId,
|
|
281
|
-
maxImages,
|
|
282
|
-
scope: "oauth-multimode",
|
|
283
|
-
onPartialImage: options.onPartialImage,
|
|
284
|
-
});
|
|
285
|
-
logEvent("oauth-multimode", "stream_end", {
|
|
286
|
-
requestId,
|
|
287
|
-
events: result.eventCount,
|
|
288
|
-
imageCount: result.images.length,
|
|
289
|
-
extraIgnored: result.extraIgnored,
|
|
290
|
-
...summarizeEventTypes(result.eventTypes),
|
|
291
|
-
});
|
|
292
|
-
return result;
|
|
293
|
-
}
|
|
294
|
-
catch (err) {
|
|
295
|
-
if (timeout.isTimeoutError(err)) {
|
|
296
|
-
throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-multimode" });
|
|
297
|
-
}
|
|
298
|
-
throw err;
|
|
299
|
-
}
|
|
300
|
-
finally {
|
|
301
|
-
timeout.clear();
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null, options = {}) {
|
|
305
|
-
await waitForOAuthReady(ctx);
|
|
306
|
-
const maskPresent = typeof options.mask === "string" && options.mask.length > 0;
|
|
307
|
-
if (maskPresent && !ctx.config?.oauth?.maskedEditEnabled) {
|
|
308
|
-
logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
|
|
309
|
-
const err = new Error("Masked edit is not supported by the current OAuth image provider");
|
|
310
|
-
err.status = 400;
|
|
311
|
-
err.code = "EDIT_MASK_NOT_SUPPORTED";
|
|
312
|
-
throw err;
|
|
313
|
-
}
|
|
314
|
-
if (maskPresent) {
|
|
315
|
-
// TODO(#31): enable upstream mask payload after STEP-0 verification
|
|
316
|
-
logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
|
|
317
|
-
const err = new Error("Masked edit is not supported by the current OAuth image provider");
|
|
318
|
-
err.status = 400;
|
|
319
|
-
err.code = "EDIT_MASK_NOT_SUPPORTED";
|
|
320
|
-
throw err;
|
|
321
|
-
}
|
|
322
|
-
const oauthUrl = getOAuthUrl(ctx);
|
|
323
|
-
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
|
|
324
|
-
const webSearchEnabled = resolveWebSearchEnabled(options);
|
|
325
|
-
const textPrompt = buildEditTextPrompt(prompt, mode, { webSearchEnabled, size });
|
|
326
|
-
const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
|
|
327
|
-
maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
|
|
328
|
-
force: true,
|
|
329
|
-
});
|
|
330
|
-
const references = Array.isArray(options.references) ? options.references : [];
|
|
331
|
-
const referenceImagesForRequest = await Promise.all(references.map((ref) => compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
|
|
332
|
-
maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
|
|
333
|
-
force: true,
|
|
334
|
-
})));
|
|
335
|
-
const referenceContent = referenceImagesForRequest.map(({ b64 }) => ({
|
|
336
|
-
type: "input_image",
|
|
337
|
-
image_url: `data:image/jpeg;base64,${b64}`,
|
|
338
|
-
}));
|
|
339
|
-
const tools = buildImageTools(webSearchEnabled, { quality, size, moderation });
|
|
340
|
-
logEvent("oauth-edit", "request", {
|
|
341
|
-
requestId,
|
|
342
|
-
model,
|
|
343
|
-
refsCount: references.length,
|
|
344
|
-
inputImageCount: 1 + references.length,
|
|
345
|
-
parentImagePresent: true,
|
|
346
|
-
webSearchEnabled,
|
|
347
|
-
inputImageCompressed: imageForRequest.compressed,
|
|
348
|
-
inputImageChars: imageForRequest.inputBytes,
|
|
349
|
-
inputImageRequestChars: imageForRequest.outputBytes,
|
|
350
|
-
});
|
|
351
|
-
const reasoningEffort = resolveReasoningEffort(ctx, options);
|
|
352
|
-
const developerPrompt = webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT;
|
|
353
|
-
const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-edit");
|
|
354
|
-
try {
|
|
355
|
-
const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
356
|
-
method: "POST",
|
|
357
|
-
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
358
|
-
signal: timeout.signal,
|
|
359
|
-
body: JSON.stringify({
|
|
360
|
-
model,
|
|
361
|
-
input: [
|
|
362
|
-
{ role: "developer", content: developerPrompt },
|
|
363
|
-
{
|
|
364
|
-
role: "user",
|
|
365
|
-
content: [
|
|
366
|
-
{ type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
|
|
367
|
-
...referenceContent,
|
|
368
|
-
{ type: "input_text", text: textPrompt },
|
|
369
|
-
],
|
|
370
|
-
},
|
|
371
|
-
],
|
|
372
|
-
tools,
|
|
373
|
-
tool_choice: "required",
|
|
374
|
-
reasoning: { effort: reasoningEffort },
|
|
375
|
-
stream: true,
|
|
376
|
-
}),
|
|
377
|
-
}, { requestId, scope: "oauth-edit" });
|
|
378
|
-
logEvent("oauth-edit", "response", {
|
|
379
|
-
requestId,
|
|
380
|
-
model,
|
|
381
|
-
status: res.status,
|
|
382
|
-
contentType: res.headers.get("content-type"),
|
|
383
|
-
});
|
|
384
|
-
if (!res.ok) {
|
|
385
|
-
const text = await res.text();
|
|
386
|
-
logEvent("oauth-edit", "error_response", { requestId, status: res.status, errorChars: text.length });
|
|
387
|
-
throwOAuthHttpError(res, text, {
|
|
388
|
-
requestId,
|
|
389
|
-
scope: "oauth-edit",
|
|
390
|
-
fallbackMessage: `OAuth edit returned ${res.status}`,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
if (requestId)
|
|
394
|
-
setJobPhase(requestId, "streaming");
|
|
395
|
-
const { imageB64: resultB64, usage, revisedPrompt, webSearchCalls, eventCount, eventTypes } = await readImageStream(res, {
|
|
396
|
-
scope: "oauth-edit",
|
|
397
|
-
requestId,
|
|
398
|
-
});
|
|
399
|
-
logEvent("oauth-edit", "stream_end", {
|
|
400
|
-
requestId,
|
|
401
|
-
events: eventCount,
|
|
402
|
-
hasImage: !!resultB64,
|
|
403
|
-
...summarizeEventTypes(eventTypes),
|
|
404
|
-
});
|
|
405
|
-
if (resultB64)
|
|
406
|
-
return { b64: resultB64, usage, revisedPrompt, webSearchCalls };
|
|
407
|
-
const emptyErr = new Error("No image data received from OAuth edit");
|
|
408
|
-
emptyErr.eventCount = eventCount;
|
|
409
|
-
emptyErr.eventTypes = eventTypes;
|
|
410
|
-
emptyErr.size = size;
|
|
411
|
-
emptyErr.quality = quality;
|
|
412
|
-
emptyErr.model = model;
|
|
413
|
-
emptyErr.refsCount = references.length;
|
|
414
|
-
emptyErr.inputImageCount = 1 + references.length;
|
|
415
|
-
emptyErr.parentImagePresent = true;
|
|
416
|
-
throw emptyErr;
|
|
417
|
-
}
|
|
418
|
-
catch (err) {
|
|
419
|
-
if (timeout.isTimeoutError(err)) {
|
|
420
|
-
throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-edit" });
|
|
421
|
-
}
|
|
422
|
-
throw err;
|
|
423
|
-
}
|
|
424
|
-
finally {
|
|
425
|
-
timeout.clear();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
184
|
+
export { generateMultimodeViaOAuth, editViaOAuth } from "./multimodeGenerators.js";
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { setJobPhase } from "../inflight.js";
|
|
2
|
+
import { logEvent } from "../logger.js";
|
|
3
|
+
import { compressReferenceB64ForOAuth } from "../referenceImageCompress.js";
|
|
4
|
+
import { throwOAuthHttpError, throwOAuthTimeoutError } from "./errors.js";
|
|
5
|
+
import { AUTO_PROMPT_FIDELITY_SUFFIX, DIRECT_PROMPT_FIDELITY_SUFFIX, EDIT_DEVELOPER_PROMPT, EDIT_NO_SEARCH_DEVELOPER_PROMPT, MULTIMODE_DEVELOPER_PROMPT, MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT, buildEditTextPrompt, buildMultimodeSequencePrompt, } from "./prompts.js";
|
|
6
|
+
import { normalizeReferenceForOAuth } from "./references.js";
|
|
7
|
+
import { buildImageTools, createOAuthGenerationTimeout, fetchOAuth, getOAuthUrl, resolveReasoningEffort, resolveWebSearchEnabled, summarizeEventTypes, waitForOAuthReady, } from "./runtime.js";
|
|
8
|
+
import { readImageStream, readMultimodeImageStream } from "./streams.js";
|
|
9
|
+
import { config } from "../../config.js";
|
|
10
|
+
const RESEARCH_SUFFIX = config.oauth.researchSuffix;
|
|
11
|
+
export async function generateMultimodeViaOAuth(prompt, quality, size, moderation = "low", references = [], requestId = null, mode = "auto", ctx = {}, options = {}) {
|
|
12
|
+
await waitForOAuthReady(ctx);
|
|
13
|
+
const oauthUrl = getOAuthUrl(ctx);
|
|
14
|
+
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
|
|
15
|
+
const maxGeneratedImages = Math.max(1, Math.trunc(Number(ctx.config?.limits?.maxGeneratedImages) || 24));
|
|
16
|
+
const maxImages = Math.min(maxGeneratedImages, Math.max(1, Math.trunc(Number(options.maxImages) || 1)));
|
|
17
|
+
const webSearchEnabled = resolveWebSearchEnabled(options);
|
|
18
|
+
const tools = buildImageTools(webSearchEnabled, {
|
|
19
|
+
quality,
|
|
20
|
+
size,
|
|
21
|
+
moderation,
|
|
22
|
+
...(options.partialImages ? { partial_images: options.partialImages } : {}),
|
|
23
|
+
});
|
|
24
|
+
const referenceInputs = references.map(normalizeReferenceForOAuth);
|
|
25
|
+
const userText = buildMultimodeSequencePrompt(mode === "direct"
|
|
26
|
+
? `${prompt}${DIRECT_PROMPT_FIDELITY_SUFFIX}`
|
|
27
|
+
: `${prompt}${webSearchEnabled ? RESEARCH_SUFFIX : ""}${AUTO_PROMPT_FIDELITY_SUFFIX}`, maxImages, { webSearchEnabled, size });
|
|
28
|
+
const userContent = referenceInputs.length
|
|
29
|
+
? [
|
|
30
|
+
...referenceInputs.map(({ b64, requestMime }) => ({
|
|
31
|
+
type: "input_image",
|
|
32
|
+
image_url: `data:${requestMime};base64,${b64}`,
|
|
33
|
+
})),
|
|
34
|
+
{ type: "input_text", text: userText },
|
|
35
|
+
]
|
|
36
|
+
: userText;
|
|
37
|
+
logEvent("oauth-multimode", "request", {
|
|
38
|
+
requestId,
|
|
39
|
+
model,
|
|
40
|
+
refsCount: referenceInputs.length,
|
|
41
|
+
maxImages,
|
|
42
|
+
promptChars: typeof prompt === "string" ? prompt.length : 0,
|
|
43
|
+
webSearchEnabled,
|
|
44
|
+
});
|
|
45
|
+
const reasoningEffort = resolveReasoningEffort(ctx, options);
|
|
46
|
+
const developerPrompt = webSearchEnabled ? MULTIMODE_DEVELOPER_PROMPT : MULTIMODE_NO_SEARCH_DEVELOPER_PROMPT;
|
|
47
|
+
const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-multimode");
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
52
|
+
signal: options.signal || timeout.signal,
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
model,
|
|
55
|
+
input: [
|
|
56
|
+
{ role: "developer", content: `${developerPrompt}\n\nN = ${maxImages}.` },
|
|
57
|
+
{ role: "user", content: userContent },
|
|
58
|
+
],
|
|
59
|
+
tools,
|
|
60
|
+
tool_choice: "required",
|
|
61
|
+
reasoning: { effort: reasoningEffort },
|
|
62
|
+
stream: true,
|
|
63
|
+
}),
|
|
64
|
+
}, { requestId, scope: "oauth-multimode" });
|
|
65
|
+
logEvent("oauth-multimode", "response", {
|
|
66
|
+
requestId,
|
|
67
|
+
model,
|
|
68
|
+
status: res.status,
|
|
69
|
+
contentType: res.headers.get("content-type"),
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const text = await res.text();
|
|
73
|
+
logEvent("oauth-multimode", "error_response", { requestId, status: res.status, errorChars: text.length });
|
|
74
|
+
throwOAuthHttpError(res, text, {
|
|
75
|
+
requestId,
|
|
76
|
+
scope: "oauth-multimode",
|
|
77
|
+
fallbackMessage: `OAuth proxy returned ${res.status}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (requestId)
|
|
81
|
+
setJobPhase(requestId, "streaming");
|
|
82
|
+
const contentType = res.headers.get("content-type") || "";
|
|
83
|
+
if (!contentType.includes("text/event-stream")) {
|
|
84
|
+
const json = await res.json();
|
|
85
|
+
const images = [];
|
|
86
|
+
for (const item of json.output || []) {
|
|
87
|
+
if (item.type === "image_generation_call" && item.result && images.length < maxImages) {
|
|
88
|
+
images.push({
|
|
89
|
+
b64: item.result,
|
|
90
|
+
revisedPrompt: typeof item.revised_prompt === "string" ? item.revised_prompt : null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
images,
|
|
96
|
+
usage: json.usage || null,
|
|
97
|
+
webSearchCalls: 0,
|
|
98
|
+
eventCount: 0,
|
|
99
|
+
eventTypes: {},
|
|
100
|
+
extraIgnored: 0,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const result = await readMultimodeImageStream(res, {
|
|
104
|
+
requestId,
|
|
105
|
+
maxImages,
|
|
106
|
+
scope: "oauth-multimode",
|
|
107
|
+
onPartialImage: options.onPartialImage,
|
|
108
|
+
});
|
|
109
|
+
logEvent("oauth-multimode", "stream_end", {
|
|
110
|
+
requestId,
|
|
111
|
+
events: result.eventCount,
|
|
112
|
+
imageCount: result.images.length,
|
|
113
|
+
extraIgnored: result.extraIgnored,
|
|
114
|
+
...summarizeEventTypes(result.eventTypes),
|
|
115
|
+
});
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (timeout.isTimeoutError(err)) {
|
|
120
|
+
throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-multimode" });
|
|
121
|
+
}
|
|
122
|
+
throw err;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
timeout.clear();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function editViaOAuth(prompt, imageB64, quality, size, moderation = "low", mode = "auto", ctx = {}, requestId = null, options = {}) {
|
|
129
|
+
await waitForOAuthReady(ctx);
|
|
130
|
+
const maskPresent = typeof options.mask === "string" && options.mask.length > 0;
|
|
131
|
+
if (maskPresent && !ctx.config?.oauth?.maskedEditEnabled) {
|
|
132
|
+
logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
|
|
133
|
+
const err = new Error("Masked edit is not supported by the current OAuth image provider");
|
|
134
|
+
err.status = 400;
|
|
135
|
+
err.code = "EDIT_MASK_NOT_SUPPORTED";
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
if (maskPresent) {
|
|
139
|
+
// TODO(#31): enable upstream mask payload after STEP-0 verification
|
|
140
|
+
logEvent("oauth-edit", "mask_unsupported", { requestId, maskPresent: true });
|
|
141
|
+
const err = new Error("Masked edit is not supported by the current OAuth image provider");
|
|
142
|
+
err.status = 400;
|
|
143
|
+
err.code = "EDIT_MASK_NOT_SUPPORTED";
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
const oauthUrl = getOAuthUrl(ctx);
|
|
147
|
+
const model = options.model || ctx.config?.imageModels?.default || "gpt-5.4-mini";
|
|
148
|
+
const webSearchEnabled = resolveWebSearchEnabled(options);
|
|
149
|
+
const textPrompt = buildEditTextPrompt(prompt, mode, { webSearchEnabled, size });
|
|
150
|
+
const imageForRequest = await compressReferenceB64ForOAuth(imageB64, {
|
|
151
|
+
maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
|
|
152
|
+
force: true,
|
|
153
|
+
});
|
|
154
|
+
const references = Array.isArray(options.references) ? options.references : [];
|
|
155
|
+
const referenceImagesForRequest = await Promise.all(references.map((ref) => compressReferenceB64ForOAuth(typeof ref === "string" ? ref : ref?.b64, {
|
|
156
|
+
maxB64Bytes: ctx.config?.limits?.maxRefB64Bytes,
|
|
157
|
+
force: true,
|
|
158
|
+
})));
|
|
159
|
+
const referenceContent = referenceImagesForRequest.map(({ b64 }) => ({
|
|
160
|
+
type: "input_image",
|
|
161
|
+
image_url: `data:image/jpeg;base64,${b64}`,
|
|
162
|
+
}));
|
|
163
|
+
const tools = buildImageTools(webSearchEnabled, { quality, size, moderation });
|
|
164
|
+
logEvent("oauth-edit", "request", {
|
|
165
|
+
requestId,
|
|
166
|
+
model,
|
|
167
|
+
refsCount: references.length,
|
|
168
|
+
inputImageCount: 1 + references.length,
|
|
169
|
+
parentImagePresent: true,
|
|
170
|
+
webSearchEnabled,
|
|
171
|
+
inputImageCompressed: imageForRequest.compressed,
|
|
172
|
+
inputImageChars: imageForRequest.inputBytes,
|
|
173
|
+
inputImageRequestChars: imageForRequest.outputBytes,
|
|
174
|
+
});
|
|
175
|
+
const reasoningEffort = resolveReasoningEffort(ctx, options);
|
|
176
|
+
const developerPrompt = webSearchEnabled ? EDIT_DEVELOPER_PROMPT : EDIT_NO_SEARCH_DEVELOPER_PROMPT;
|
|
177
|
+
const timeout = createOAuthGenerationTimeout(ctx, requestId, "oauth-edit");
|
|
178
|
+
try {
|
|
179
|
+
const res = await fetchOAuth(`${oauthUrl}/v1/responses`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json", Accept: "text/event-stream" },
|
|
182
|
+
signal: timeout.signal,
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
model,
|
|
185
|
+
input: [
|
|
186
|
+
{ role: "developer", content: developerPrompt },
|
|
187
|
+
{
|
|
188
|
+
role: "user",
|
|
189
|
+
content: [
|
|
190
|
+
{ type: "input_image", image_url: `data:image/jpeg;base64,${imageForRequest.b64}` },
|
|
191
|
+
...referenceContent,
|
|
192
|
+
{ type: "input_text", text: textPrompt },
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
tools,
|
|
197
|
+
tool_choice: "required",
|
|
198
|
+
reasoning: { effort: reasoningEffort },
|
|
199
|
+
stream: true,
|
|
200
|
+
}),
|
|
201
|
+
}, { requestId, scope: "oauth-edit" });
|
|
202
|
+
logEvent("oauth-edit", "response", {
|
|
203
|
+
requestId,
|
|
204
|
+
model,
|
|
205
|
+
status: res.status,
|
|
206
|
+
contentType: res.headers.get("content-type"),
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok) {
|
|
209
|
+
const text = await res.text();
|
|
210
|
+
logEvent("oauth-edit", "error_response", { requestId, status: res.status, errorChars: text.length });
|
|
211
|
+
throwOAuthHttpError(res, text, {
|
|
212
|
+
requestId,
|
|
213
|
+
scope: "oauth-edit",
|
|
214
|
+
fallbackMessage: `OAuth edit returned ${res.status}`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (requestId)
|
|
218
|
+
setJobPhase(requestId, "streaming");
|
|
219
|
+
const { imageB64: resultB64, usage, revisedPrompt, webSearchCalls, eventCount, eventTypes } = await readImageStream(res, {
|
|
220
|
+
scope: "oauth-edit",
|
|
221
|
+
requestId,
|
|
222
|
+
});
|
|
223
|
+
logEvent("oauth-edit", "stream_end", {
|
|
224
|
+
requestId,
|
|
225
|
+
events: eventCount,
|
|
226
|
+
hasImage: !!resultB64,
|
|
227
|
+
...summarizeEventTypes(eventTypes),
|
|
228
|
+
});
|
|
229
|
+
if (resultB64)
|
|
230
|
+
return { b64: resultB64, usage, revisedPrompt, webSearchCalls };
|
|
231
|
+
const emptyErr = new Error("No image data received from OAuth edit");
|
|
232
|
+
emptyErr.eventCount = eventCount;
|
|
233
|
+
emptyErr.eventTypes = eventTypes;
|
|
234
|
+
emptyErr.size = size;
|
|
235
|
+
emptyErr.quality = quality;
|
|
236
|
+
emptyErr.model = model;
|
|
237
|
+
emptyErr.refsCount = references.length;
|
|
238
|
+
emptyErr.inputImageCount = 1 + references.length;
|
|
239
|
+
emptyErr.parentImagePresent = true;
|
|
240
|
+
throw emptyErr;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (timeout.isTimeoutError(err)) {
|
|
244
|
+
throwOAuthTimeoutError(err, { timeoutMs: timeout.timeoutMs, requestId, scope: "oauth-edit" });
|
|
245
|
+
}
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
timeout.clear();
|
|
250
|
+
}
|
|
251
|
+
}
|
package/lib/routeHelpers.js
CHANGED
|
@@ -12,8 +12,15 @@ export function imageFormatFromMime(mime) {
|
|
|
12
12
|
return "png";
|
|
13
13
|
}
|
|
14
14
|
export function writeSse(res, event, data) {
|
|
15
|
-
res.
|
|
16
|
-
|
|
15
|
+
if (res.writableEnded || res.destroyed)
|
|
16
|
+
return false;
|
|
17
|
+
try {
|
|
18
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
export function dataUrlFromB64(format, b64) {
|
|
19
26
|
return `data:image/${format === "jpeg" ? "jpeg" : format};base64,${b64}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ima2-gen",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"packageManager": "npm@11.18.0",
|
|
5
5
|
"description": "Local OAuth image generation studio with classic and node workflows",
|
|
6
6
|
"type": "module",
|
|
@@ -105,5 +105,5 @@
|
|
|
105
105
|
"tsx": "^4.21.0",
|
|
106
106
|
"typescript": "^5.9.3"
|
|
107
107
|
},
|
|
108
|
-
"gitHead": "
|
|
108
|
+
"gitHead": "579055388e5959dcca3f6bcb3e2e61409ead0a7e"
|
|
109
109
|
}
|
package/routes/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendAgentTurn, compactAgentSession, createAgentSession, deleteAgentSession, getAgentGenerationSettings, getAgentSession, getAgentWorkspacePayload, importAgentImage, renameAgentSession, setAgentCurrentImage, setAgentGenerationSettings, setAgentLocks, setAgentWebSearch, } from "../lib/agentStore.js";
|
|
2
|
-
import {
|
|
3
|
-
import { ensureAgentQueueWorker, tickAgentQueueWorker } from "../lib/agentQueueWorker.js";
|
|
2
|
+
import { createAgentQueueItem, getAgentGenerationErrors, getAgentQueueItem, listAgentQueueItems, retryAgentQueueItem, } from "../lib/agentQueueStore.js";
|
|
3
|
+
import { cancelRunningAgentQueueItem, ensureAgentQueueWorker, tickAgentQueueWorker } from "../lib/agentQueueWorker.js";
|
|
4
4
|
import { parseAgentSlashCommand, formatAgentQuestionReply, formatAgentSlashHelp } from "../lib/agentCommandParser.js";
|
|
5
5
|
import { requestAgentQuestionAnswer } from "../lib/agentQuestionResponder.js";
|
|
6
6
|
import { agentAllowedToolPayload, runAgentTurn } from "../lib/agentRuntime.js";
|
|
@@ -181,9 +181,9 @@ export function registerAgentRoutes(app, ctxRaw) {
|
|
|
181
181
|
const item = getAgentQueueItem(req.params.itemId);
|
|
182
182
|
if (!item)
|
|
183
183
|
return sendError(res, queueItemNotFound(req.params.itemId));
|
|
184
|
-
const ok =
|
|
184
|
+
const ok = cancelRunningAgentQueueItem(item.id);
|
|
185
185
|
if (!ok)
|
|
186
|
-
return sendError(res, queueActionError("AGENT_QUEUE_CANCEL_FAILED", "Only queued Agent work can be canceled."));
|
|
186
|
+
return sendError(res, queueActionError("AGENT_QUEUE_CANCEL_FAILED", "Only queued or running Agent work can be canceled."));
|
|
187
187
|
res.json(getAgentWorkspacePayload(item.sessionId));
|
|
188
188
|
});
|
|
189
189
|
app.post("/api/agent/queue/:itemId/retry", (req, res) => {
|
package/routes/canvasVersions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
-
import { createCanvasVersion, updateCanvasVersion } from "../lib/canvasVersionStore.js";
|
|
2
|
+
import { createCanvasVersion, recordCanvasAnnotationBake, revertCanvasAnnotations, updateCanvasVersion, } from "../lib/canvasVersionStore.js";
|
|
3
3
|
import { errInfo } from "../lib/errInfo.js";
|
|
4
4
|
import { requireRuntimeContext } from "../lib/runtimeContext.js";
|
|
5
5
|
function decodeHeader(value) {
|
|
@@ -51,6 +51,7 @@ export function registerCanvasVersionRoutes(app, ctxRaw) {
|
|
|
51
51
|
sourceFilename,
|
|
52
52
|
prompt: getPrompt(req),
|
|
53
53
|
buffer: getRequestBuffer(req),
|
|
54
|
+
pixelEdited: req.headers["x-ima2-canvas-pixel-edited"] === "true",
|
|
54
55
|
});
|
|
55
56
|
res.json({ item });
|
|
56
57
|
}
|
|
@@ -62,4 +63,23 @@ export function registerCanvasVersionRoutes(app, ctxRaw) {
|
|
|
62
63
|
});
|
|
63
64
|
}
|
|
64
65
|
});
|
|
66
|
+
app.put("/api/canvas-versions/:filename/annotation-bake", express.json({ limit: ctx.config.server.bodyLimit }), async (req, res) => {
|
|
67
|
+
try {
|
|
68
|
+
const item = await recordCanvasAnnotationBake(ctx, decodeURIComponent(req.params.filename), req.body?.snapshot, req.body?.annotationOnly === true);
|
|
69
|
+
res.json({ item });
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
const err = errInfo(e);
|
|
73
|
+
res.status(err.status || 500).json({ error: err.message, code: err.code || "CANVAS_ANNOTATION_BAKE_FAILED" });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
app.post("/api/canvas-versions/:filename/revert-annotations", async (req, res) => {
|
|
77
|
+
try {
|
|
78
|
+
res.json(await revertCanvasAnnotations(ctx, decodeURIComponent(req.params.filename)));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
const err = errInfo(e);
|
|
82
|
+
res.status(err.status || 500).json({ error: err.message, code: err.code || "CANVAS_ANNOTATION_REVERT_FAILED" });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
65
85
|
}
|
package/routes/cardNews.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { listImageTemplates, readTemplatePreview } from "../lib/cardNewsTemplateStore.js";
|
|
2
2
|
import { listRoleTemplates } from "../lib/cardNewsRoleTemplateStore.js";
|
|
3
3
|
import { createCardNewsDraft } from "../lib/cardNewsPlanner.js";
|
|
4
|
-
import { generateCardNewsSet } from "../lib/cardNewsGenerator.js";
|
|
4
|
+
import { generateCardNewsSet, validateCardNewsInput } from "../lib/cardNewsGenerator.js";
|
|
5
5
|
import { createCardNewsJob, finishCardNewsJob, getCardNewsJob, getCardNewsJobPlan, retryCardNewsJob, updateCardNewsJob, updateCardNewsJobCard, } from "../lib/cardNewsJobStore.js";
|
|
6
6
|
import { listCardNewsSets, readCardNewsManifest, readCardNewsSetPlan } from "../lib/cardNewsManifestStore.js";
|
|
7
7
|
import { errInfo } from "../lib/errInfo.js";
|
|
@@ -126,6 +126,7 @@ export function registerCardNewsRoutes(app, ctxRaw) {
|
|
|
126
126
|
app.post("/api/cardnews/jobs", (req, res) => {
|
|
127
127
|
try {
|
|
128
128
|
const body = (req.body ?? {});
|
|
129
|
+
validateCardNewsInput(body);
|
|
129
130
|
const summary = createCardNewsJob(body);
|
|
130
131
|
runCardNewsJob(ctx, summary.jobId, body);
|
|
131
132
|
res.status(202).json(summary);
|