@productmaker/mcp 3.1.2 → 3.3.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/http.js +99 -17
- package/dist/stdio.js +99 -17
- package/dist/ui/meta-upload-app.html +203 -185
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -2600,7 +2600,13 @@ var VideoGenerator = z18.object({
|
|
|
2600
2600
|
videoResolution: VideoResolution.optional(),
|
|
2601
2601
|
/** Requested controls and the effective provider route that produced this result. */
|
|
2602
2602
|
execution: VideoExecutionContract.optional(),
|
|
2603
|
-
adCopy: z18.string().optional()
|
|
2603
|
+
adCopy: z18.string().optional(),
|
|
2604
|
+
/** TRUE when the seller APPROVED a script for this angle that the plan path
|
|
2605
|
+
* could not honor, so a fresh script was re-authored and rendered instead
|
|
2606
|
+
* (PR #1470). The angle still delivered — nothing was lost and nothing is
|
|
2607
|
+
* refundable — but the video is NOT the script they signed off on, and the
|
|
2608
|
+
* UI must say so. Absent (never `false`) on every honest delivery. */
|
|
2609
|
+
scriptRewritten: z18.boolean().optional()
|
|
2604
2610
|
});
|
|
2605
2611
|
var VideoScriptSceneForReview = z18.object({
|
|
2606
2612
|
index: z18.number().int().nonnegative(),
|
|
@@ -4136,8 +4142,20 @@ var AdminUserListResponse = z27.object({
|
|
|
4136
4142
|
page: z27.number().int().positive(),
|
|
4137
4143
|
pageSize: z27.number().int().positive()
|
|
4138
4144
|
});
|
|
4145
|
+
var AdminCreditGrantReason = z27.enum([
|
|
4146
|
+
"compensation",
|
|
4147
|
+
"goodwill",
|
|
4148
|
+
"promo",
|
|
4149
|
+
"internal-testing",
|
|
4150
|
+
"correction"
|
|
4151
|
+
]);
|
|
4139
4152
|
var UpdateUserCreditsRequest = z27.object({
|
|
4140
|
-
creditCents: z27.number().nonnegative()
|
|
4153
|
+
creditCents: z27.number().nonnegative(),
|
|
4154
|
+
reason: AdminCreditGrantReason,
|
|
4155
|
+
/** Free-text detail. Required when the enum alone does not explain the grant. */
|
|
4156
|
+
notes: z27.string().trim().min(1).max(500).optional(),
|
|
4157
|
+
/** The task the grant compensates, when there is one. */
|
|
4158
|
+
taskId: z27.string().uuid().optional()
|
|
4141
4159
|
});
|
|
4142
4160
|
var QualityReportStatus = z27.enum([
|
|
4143
4161
|
"open",
|
|
@@ -8906,7 +8924,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
8906
8924
|
], [
|
|
8907
8925
|
"creative_variant"
|
|
8908
8926
|
]),
|
|
8909
|
-
defineCapability("create_product_task", "
|
|
8927
|
+
defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
|
|
8910
8928
|
"credit_consuming",
|
|
8911
8929
|
"state_change"
|
|
8912
8930
|
], [
|
|
@@ -8947,6 +8965,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
8947
8965
|
"productContext"
|
|
8948
8966
|
]),
|
|
8949
8967
|
slot("video_options", false, [
|
|
8968
|
+
"videoPreset",
|
|
8969
|
+
"durationSeconds",
|
|
8970
|
+
"pauseForAngleReview",
|
|
8971
|
+
// MCP-only: WhatsApp runs its own confirmation policy and does not
|
|
8972
|
+
// offer a "decide everything" mode, so the field is not mirrored there.
|
|
8973
|
+
"aiDecidesAll",
|
|
8974
|
+
"videoTier",
|
|
8975
|
+
"videoResolution",
|
|
8976
|
+
"multiAngleVideos",
|
|
8977
|
+
"angleNarratives",
|
|
8978
|
+
"competitorScanEnabled"
|
|
8979
|
+
], [
|
|
8950
8980
|
"videoPreset",
|
|
8951
8981
|
"durationSeconds",
|
|
8952
8982
|
"pauseForAngleReview",
|
|
@@ -9172,7 +9202,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9172
9202
|
], [
|
|
9173
9203
|
"creative"
|
|
9174
9204
|
]),
|
|
9175
|
-
defineCapability("generate_landing", "
|
|
9205
|
+
defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
|
|
9176
9206
|
"credit_consuming",
|
|
9177
9207
|
"state_change"
|
|
9178
9208
|
], [
|
|
@@ -9214,6 +9244,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9214
9244
|
"subheadline"
|
|
9215
9245
|
]),
|
|
9216
9246
|
slot("landing_options", false, [
|
|
9247
|
+
"competitorScanEnabled",
|
|
9248
|
+
"landingStrategy",
|
|
9249
|
+
"landingImageFormat",
|
|
9250
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9251
|
+
"aiDecidesAll"
|
|
9252
|
+
], [
|
|
9217
9253
|
"competitorScanEnabled",
|
|
9218
9254
|
"landingStrategy",
|
|
9219
9255
|
"landingImageFormat"
|
|
@@ -9242,7 +9278,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9242
9278
|
fieldCompletion: "all"
|
|
9243
9279
|
}
|
|
9244
9280
|
]),
|
|
9245
|
-
defineCapability("generate_video_creative", "
|
|
9281
|
+
defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
|
|
9246
9282
|
"credit_consuming",
|
|
9247
9283
|
"state_change"
|
|
9248
9284
|
], [
|
|
@@ -9264,6 +9300,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9264
9300
|
"country"
|
|
9265
9301
|
]),
|
|
9266
9302
|
slot("render_options", true, [
|
|
9303
|
+
"aspectRatio",
|
|
9304
|
+
"durationSeconds",
|
|
9305
|
+
"videoPreset",
|
|
9306
|
+
"videoTier",
|
|
9307
|
+
"videoResolution",
|
|
9308
|
+
"features",
|
|
9309
|
+
"cta",
|
|
9310
|
+
"scriptNotes",
|
|
9311
|
+
"pauseForVideoScriptReview",
|
|
9312
|
+
"pauseForAngleReview",
|
|
9313
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9314
|
+
"aiDecidesAll",
|
|
9315
|
+
"competitorScanEnabled"
|
|
9316
|
+
], [
|
|
9267
9317
|
"aspectRatio",
|
|
9268
9318
|
"durationSeconds",
|
|
9269
9319
|
"videoPreset",
|
|
@@ -11042,7 +11092,7 @@ var START_DROPSHIPPING_PROMPT = [
|
|
|
11042
11092
|
"",
|
|
11043
11093
|
"After I choose a product, call start_dropshipping_project. Ask me for the real supplier cost and required costing inputs before cost_dropshipping_project; never treat a marketplace sale price as supplier cost. When nextAction.kind is cost_product, call cost_product first as a free read-only estimate with those real supplier inputs, carry its price and compareAtPrice into the creative task, then call cost_dropshipping_project with the canonical inputs to persist costing. After every guided mutation, call guide_dropshipping_launch and follow ProductMaker's returned nextAction. Do not maintain a parallel workflow state in chat.",
|
|
11044
11094
|
"",
|
|
11045
|
-
"Use the existing creative/task tools for generation, preserve their estimate and confirmation gates,
|
|
11095
|
+
"Use the existing creative/task tools for generation, preserve their estimate and confirmation gates, and keep polling get_task_status until the task is done, errored, or paused \u2014 a timed-out poll means it is still running, so call again. When it pauses, show me the draft and ask me; only answer it yourself through resolve_pending_action if I delegated that decision to you or the task runs with aiDecidesAll / reviewMode automatic. Verify actual outputs before calling the creative stage complete.",
|
|
11046
11096
|
"",
|
|
11047
11097
|
"Before Shopify publication, call list_shopify_shops, check the real task readiness, ask whether the product has variants, show the intended publication, and wait for explicit approval before calling publish_to_shopify. Report the exact blocker when publication is not ready.",
|
|
11048
11098
|
"",
|
|
@@ -11368,7 +11418,7 @@ var ApiClient = class {
|
|
|
11368
11418
|
};
|
|
11369
11419
|
|
|
11370
11420
|
// src/index.ts
|
|
11371
|
-
var MCP_VERSION = "3.
|
|
11421
|
+
var MCP_VERSION = "3.3.0";
|
|
11372
11422
|
|
|
11373
11423
|
// src/meta-upload-app.ts
|
|
11374
11424
|
import { readFileSync } from "fs";
|
|
@@ -12818,7 +12868,12 @@ async function fetchTaskOutputs(api, taskId, options) {
|
|
|
12818
12868
|
return {
|
|
12819
12869
|
angleKey: vg.angleKey ?? "",
|
|
12820
12870
|
angleIndex: vg.angleIndex ?? 0,
|
|
12821
|
-
variants: variants2.length ? variants2 : fallback
|
|
12871
|
+
variants: variants2.length ? variants2 : fallback,
|
|
12872
|
+
// Only ever true — an honest delivery leaves the key off entirely so no
|
|
12873
|
+
// consumer has to distinguish `false` from "the API is older than this".
|
|
12874
|
+
...vg.scriptRewritten === true ? {
|
|
12875
|
+
scriptRewritten: true
|
|
12876
|
+
} : {}
|
|
12822
12877
|
};
|
|
12823
12878
|
}).filter((v) => v.angleKey && v.variants.length);
|
|
12824
12879
|
const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
|
|
@@ -12881,6 +12936,9 @@ function renderOutputsMarkdown(outputs) {
|
|
|
12881
12936
|
for (const variant of v.variants) {
|
|
12882
12937
|
parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
|
|
12883
12938
|
}
|
|
12939
|
+
if (v.scriptRewritten) {
|
|
12940
|
+
parts.push(` \u26A0\uFE0F No pudimos usar el guion que aprobaste para este \xE1ngulo, as\xED que generamos el video con un guion nuevo. Si no te sirve, ed\xEDtalo o vuelve a generarlo.`);
|
|
12941
|
+
}
|
|
12884
12942
|
}
|
|
12885
12943
|
}
|
|
12886
12944
|
return parts.join("\n");
|
|
@@ -12906,6 +12964,7 @@ var CreateProductTaskInput = z49.object({
|
|
|
12906
12964
|
z49.literal(30)
|
|
12907
12965
|
]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
12908
12966
|
pauseForAngleReview: z49.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
|
|
12967
|
+
aiDecidesAll: z49.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n, as\xED que `pendingUserAction` no deber\xEDa aparecer. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
|
|
12909
12968
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
12910
12969
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
|
|
12911
12970
|
multiAngleVideos: z49.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
@@ -13233,6 +13292,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
13233
13292
|
"options.subheadline": input.subheadline,
|
|
13234
13293
|
"options.landingStrategy": input.landingStrategy,
|
|
13235
13294
|
"options.landingImageFormat": input.landingImageFormat,
|
|
13295
|
+
"options.aiDecidesAll": input.aiDecidesAll,
|
|
13236
13296
|
pauseForAngleReview: input.pauseForAngleReview,
|
|
13237
13297
|
url: input.productUrl
|
|
13238
13298
|
}),
|
|
@@ -13304,7 +13364,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
13304
13364
|
throw translateError(e);
|
|
13305
13365
|
}
|
|
13306
13366
|
});
|
|
13307
|
-
server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO: muestra el draft y
|
|
13367
|
+
server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSIGUE LLAM\xC1NDOLA hasta que el estado sea terminal (`done` o `error`) o aparezca `pendingUserAction`. Un retorno sin ninguna de esas dos cosas significa que la tarea sigue corriendo y que alcanzaste el timeout, NO que se colg\xF3 ni que termin\xF3: vuelve a llamarla con waitSeconds alto. Si dejas de llamarla, la tarea sigue avanzando sin que nadie la mire y el usuario se queda esperando en silencio.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO esperando una decisi\xF3n del USUARIO: muestra el draft, PREG\xDANTALE y espera su respuesta. Resp\xF3ndela t\xFA mismo con `resolve_pending_action` \xFAnicamente si el usuario ya te deleg\xF3 esa decisi\xF3n \u2014 porque te lo dijo en la conversaci\xF3n, o porque la tarea corre con `aiDecidesAll` / `reviewMode: automatic`. Nunca deduzcas la delegaci\xF3n del contenido del draft: que la propuesta te parezca obvia o correcta no es un permiso. Al responder, repite el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
|
|
13308
13368
|
try {
|
|
13309
13369
|
const signal = extra?.signal;
|
|
13310
13370
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
|
|
@@ -13387,7 +13447,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
13387
13447
|
throw translateError(e);
|
|
13388
13448
|
}
|
|
13389
13449
|
});
|
|
13390
|
-
server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
|
|
13450
|
+
server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Esta llamada habla EN NOMBRE DEL USUARIO: \xFAsala solo con una respuesta que \xE9l te haya dado, o cuando te haya delegado la decisi\xF3n de forma expl\xEDcita o v\xEDa `aiDecidesAll` / `reviewMode: automatic`. Si nadie te deleg\xF3 nada, pregunta y espera en vez de llamarla. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
|
|
13391
13451
|
try {
|
|
13392
13452
|
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
13393
13453
|
const status = StatusForResolveSchema.parse(raw);
|
|
@@ -13690,6 +13750,7 @@ var VideoInput = z50.object({
|
|
|
13690
13750
|
scriptNotes: z50.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
|
|
13691
13751
|
pauseForVideoScriptReview: z50.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
|
|
13692
13752
|
pauseForAngleReview: z50.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
|
|
13753
|
+
aiDecidesAll: z50.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
|
|
13693
13754
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
13694
13755
|
brandId: CreativeRefs.brandId,
|
|
13695
13756
|
projectId: CreativeRefs.projectId
|
|
@@ -13767,6 +13828,7 @@ var EditImageCreativeInput = z50.object({
|
|
|
13767
13828
|
instruction: z50.string().min(1).max(1e3).describe('Qu\xE9 cambiar en esa imagen. Ej. "quita el badge de env\xEDo gratis", "cambia el fondo a blanco".')
|
|
13768
13829
|
});
|
|
13769
13830
|
var LandingInput = z50.object({
|
|
13831
|
+
aiDecidesAll: z50.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
|
|
13770
13832
|
images: CreativeRefs.images,
|
|
13771
13833
|
language: CreativeRefs.language,
|
|
13772
13834
|
country: CreativeRefs.country,
|
|
@@ -14973,7 +15035,7 @@ function uploadSessionText(result) {
|
|
|
14973
15035
|
text: [
|
|
14974
15036
|
`### Prepara tu ${mediaLabel} en ProductMaker`,
|
|
14975
15037
|
`Usa primero el App inline de este chat. Si tu host no soporta el bridge de archivos, usa este fallback: [Abrir carga segura](${result.uploadPageUrl})`,
|
|
14976
|
-
`Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}.
|
|
15038
|
+
`Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. El App sube cada archivo solo; cuando quieras seguir, escr\xEDbemelo en el chat.`,
|
|
14977
15039
|
`Este enlace expira: ${result.expiresAt}`
|
|
14978
15040
|
].join("\n\n")
|
|
14979
15041
|
}
|
|
@@ -14988,7 +15050,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
|
|
|
14988
15050
|
function registerMediaUploadTools(server, api) {
|
|
14989
15051
|
registerAppTool(server, "request_media_upload", {
|
|
14990
15052
|
title: "Subir archivos",
|
|
14991
|
-
description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos.
|
|
15053
|
+
description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
|
|
14992
15054
|
inputSchema: UploadSessionToolInputShape,
|
|
14993
15055
|
outputSchema: UniversalUploadStructuredResponse.shape,
|
|
14994
15056
|
_meta: {
|
|
@@ -15028,7 +15090,7 @@ function registerMediaUploadTools(server, api) {
|
|
|
15028
15090
|
}
|
|
15029
15091
|
});
|
|
15030
15092
|
server.registerTool("check_media_upload", {
|
|
15031
|
-
description: "Comprueba la carga universal. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
|
|
15093
|
+
description: "Comprueba la carga universal. Ll\xE1mala cuando el usuario diga que ya subi\xF3 sus archivos: es la \xFAnica forma de enterarte, porque el App no manda mensajes al chat. Si status=pending, el usuario todav\xEDa no ha terminado: d\xEDselo y espera, no inventes un assetId ni abras otra sesi\xF3n de carga. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
|
|
15032
15094
|
inputSchema: UploadStatusInput.shape,
|
|
15033
15095
|
_meta: {
|
|
15034
15096
|
"openai/widgetAccessible": true
|
|
@@ -15948,7 +16010,7 @@ var CostProjectInput = z62.object({
|
|
|
15948
16010
|
var GuideInput = z62.object({
|
|
15949
16011
|
projectId: ProjectId.optional()
|
|
15950
16012
|
}).strict();
|
|
15951
|
-
var
|
|
16013
|
+
var NEXT_ACTION_KINDS = [
|
|
15952
16014
|
"cost_product",
|
|
15953
16015
|
"provide_product_media",
|
|
15954
16016
|
"generate_creatives",
|
|
@@ -15962,7 +16024,8 @@ var DropshippingProjectNextAction = z62.discriminatedUnion("kind", [
|
|
|
15962
16024
|
"create_meta_campaign",
|
|
15963
16025
|
"repair_meta_campaign",
|
|
15964
16026
|
"review_paused_campaign"
|
|
15965
|
-
]
|
|
16027
|
+
];
|
|
16028
|
+
var DropshippingProjectNextAction = z62.discriminatedUnion("kind", NEXT_ACTION_KINDS.map((kind) => z62.object({
|
|
15966
16029
|
kind: z62.literal(kind),
|
|
15967
16030
|
href: z62.string().min(1)
|
|
15968
16031
|
})));
|
|
@@ -16036,9 +16099,25 @@ var ProjectResponseSchema = z62.object({
|
|
|
16036
16099
|
createdAt: z62.string().datetime(),
|
|
16037
16100
|
updatedAt: z62.string().datetime()
|
|
16038
16101
|
}).passthrough();
|
|
16102
|
+
var NEXT_ACTION_GUIDANCE = {
|
|
16103
|
+
cost_product: "Llama `cost_dropshipping_project` con el costo real del proveedor. Nunca lo inventes: p\xEDdeselo al usuario.",
|
|
16104
|
+
provide_product_media: "Llama `request_media_upload` con mediaKind=image y uploadPurpose=product_reference. Cuando el usuario diga que ya subi\xF3, llama `check_media_upload` y usa los assetId. No pidas ruta local ni URL.",
|
|
16105
|
+
generate_creatives: "Llama `create_product_task` con los assetId del producto. Antes dile al usuario qu\xE9 outputs vas a generar y confirma precio y oferta.",
|
|
16106
|
+
wait_for_creatives: "No hay nada que hacer todav\xEDa: la tarea est\xE1 corriendo. Llama `get_task_status` y espera. No crees otra tarea.",
|
|
16107
|
+
repair_creatives: "La tarea fall\xF3. Llama `get_task_status` para leer el error y expl\xEDcaselo al usuario antes de reintentar nada.",
|
|
16108
|
+
complete_creative_set: "Faltan outputs por terminar. Llama `get_task_status` y revisa si hay una acci\xF3n pendiente que resolver con `resolve_pending_action`.",
|
|
16109
|
+
connect_shopify: "Llama `list_shopify_shops`. Si no hay ninguna tienda conectada, el usuario debe conectarla en la web: d\xE1selo como paso manual.",
|
|
16110
|
+
publish_storefront: "Llama `publish_to_shopify` con el taskId y la tienda elegida.",
|
|
16111
|
+
wait_for_storefront: "La publicaci\xF3n en Shopify est\xE1 en curso. Llama `get_task_status` y espera; no publiques otra vez.",
|
|
16112
|
+
connect_meta: "Llama `list_meta_destinations`. Si no hay cuentas, el usuario debe conectar Meta en la web: d\xE1selo como paso manual.",
|
|
16113
|
+
create_meta_campaign: "Llama `list_meta_destinations`, pregunta en el chat por portfolioId, adAccountId y pageId, y luego `publish_to_meta`. La campa\xF1a queda PAUSED.",
|
|
16114
|
+
repair_meta_campaign: "La campa\xF1a qued\xF3 en error o pendiente. Llama `get_meta_campaign_status` y expl\xEDcale al usuario qu\xE9 pas\xF3 antes de reintentar.",
|
|
16115
|
+
review_paused_campaign: "Todo est\xE1 listo. La campa\xF1a qued\xF3 PAUSED a prop\xF3sito: dile al usuario que la active \xE9l mismo en Ads Manager."
|
|
16116
|
+
};
|
|
16039
16117
|
function projectText(raw) {
|
|
16040
16118
|
const project = ProjectResponseSchema.parse(raw);
|
|
16041
|
-
const
|
|
16119
|
+
const kind = project.nextAction?.kind;
|
|
16120
|
+
const guidance = kind ? NEXT_ACTION_GUIDANCE[kind] : void 0;
|
|
16042
16121
|
return {
|
|
16043
16122
|
content: [
|
|
16044
16123
|
{
|
|
@@ -16048,6 +16127,9 @@ function projectText(raw) {
|
|
|
16048
16127
|
productName: project.productName,
|
|
16049
16128
|
phase: project.phase,
|
|
16050
16129
|
nextAction: project.nextAction,
|
|
16130
|
+
// The href is a web anchor and is useless to an agent; the guidance
|
|
16131
|
+
// is the executable form of the same step.
|
|
16132
|
+
nextActionGuidance: guidance ?? null,
|
|
16051
16133
|
hasReferenceImage: project.hasReferenceImage,
|
|
16052
16134
|
costingSaved: Boolean(project.costingResult),
|
|
16053
16135
|
rewardProgress: {
|
|
@@ -16056,7 +16138,7 @@ function projectText(raw) {
|
|
|
16056
16138
|
milestones: project.rewards.milestones
|
|
16057
16139
|
},
|
|
16058
16140
|
version: project.version,
|
|
16059
|
-
message: `Siguiente paso: ${
|
|
16141
|
+
message: guidance ? `Siguiente paso: ${guidance} Relee este proyecto despu\xE9s de cada mutaci\xF3n.` : "El proyecto no tiene ning\xFAn paso pendiente."
|
|
16060
16142
|
})
|
|
16061
16143
|
}
|
|
16062
16144
|
]
|
package/dist/stdio.js
CHANGED
|
@@ -264,7 +264,7 @@ var ApiClient = class {
|
|
|
264
264
|
};
|
|
265
265
|
|
|
266
266
|
// src/index.ts
|
|
267
|
-
var MCP_VERSION = "3.
|
|
267
|
+
var MCP_VERSION = "3.3.0";
|
|
268
268
|
|
|
269
269
|
// src/meta-upload-app.ts
|
|
270
270
|
import { readFileSync } from "fs";
|
|
@@ -3221,7 +3221,13 @@ var VideoGenerator = z18.object({
|
|
|
3221
3221
|
videoResolution: VideoResolution.optional(),
|
|
3222
3222
|
/** Requested controls and the effective provider route that produced this result. */
|
|
3223
3223
|
execution: VideoExecutionContract.optional(),
|
|
3224
|
-
adCopy: z18.string().optional()
|
|
3224
|
+
adCopy: z18.string().optional(),
|
|
3225
|
+
/** TRUE when the seller APPROVED a script for this angle that the plan path
|
|
3226
|
+
* could not honor, so a fresh script was re-authored and rendered instead
|
|
3227
|
+
* (PR #1470). The angle still delivered — nothing was lost and nothing is
|
|
3228
|
+
* refundable — but the video is NOT the script they signed off on, and the
|
|
3229
|
+
* UI must say so. Absent (never `false`) on every honest delivery. */
|
|
3230
|
+
scriptRewritten: z18.boolean().optional()
|
|
3225
3231
|
});
|
|
3226
3232
|
var VideoScriptSceneForReview = z18.object({
|
|
3227
3233
|
index: z18.number().int().nonnegative(),
|
|
@@ -4757,8 +4763,20 @@ var AdminUserListResponse = z27.object({
|
|
|
4757
4763
|
page: z27.number().int().positive(),
|
|
4758
4764
|
pageSize: z27.number().int().positive()
|
|
4759
4765
|
});
|
|
4766
|
+
var AdminCreditGrantReason = z27.enum([
|
|
4767
|
+
"compensation",
|
|
4768
|
+
"goodwill",
|
|
4769
|
+
"promo",
|
|
4770
|
+
"internal-testing",
|
|
4771
|
+
"correction"
|
|
4772
|
+
]);
|
|
4760
4773
|
var UpdateUserCreditsRequest = z27.object({
|
|
4761
|
-
creditCents: z27.number().nonnegative()
|
|
4774
|
+
creditCents: z27.number().nonnegative(),
|
|
4775
|
+
reason: AdminCreditGrantReason,
|
|
4776
|
+
/** Free-text detail. Required when the enum alone does not explain the grant. */
|
|
4777
|
+
notes: z27.string().trim().min(1).max(500).optional(),
|
|
4778
|
+
/** The task the grant compensates, when there is one. */
|
|
4779
|
+
taskId: z27.string().uuid().optional()
|
|
4762
4780
|
});
|
|
4763
4781
|
var QualityReportStatus = z27.enum([
|
|
4764
4782
|
"open",
|
|
@@ -9527,7 +9545,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9527
9545
|
], [
|
|
9528
9546
|
"creative_variant"
|
|
9529
9547
|
]),
|
|
9530
|
-
defineCapability("create_product_task", "
|
|
9548
|
+
defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
|
|
9531
9549
|
"credit_consuming",
|
|
9532
9550
|
"state_change"
|
|
9533
9551
|
], [
|
|
@@ -9568,6 +9586,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9568
9586
|
"productContext"
|
|
9569
9587
|
]),
|
|
9570
9588
|
slot("video_options", false, [
|
|
9589
|
+
"videoPreset",
|
|
9590
|
+
"durationSeconds",
|
|
9591
|
+
"pauseForAngleReview",
|
|
9592
|
+
// MCP-only: WhatsApp runs its own confirmation policy and does not
|
|
9593
|
+
// offer a "decide everything" mode, so the field is not mirrored there.
|
|
9594
|
+
"aiDecidesAll",
|
|
9595
|
+
"videoTier",
|
|
9596
|
+
"videoResolution",
|
|
9597
|
+
"multiAngleVideos",
|
|
9598
|
+
"angleNarratives",
|
|
9599
|
+
"competitorScanEnabled"
|
|
9600
|
+
], [
|
|
9571
9601
|
"videoPreset",
|
|
9572
9602
|
"durationSeconds",
|
|
9573
9603
|
"pauseForAngleReview",
|
|
@@ -9793,7 +9823,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9793
9823
|
], [
|
|
9794
9824
|
"creative"
|
|
9795
9825
|
]),
|
|
9796
|
-
defineCapability("generate_landing", "
|
|
9826
|
+
defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
|
|
9797
9827
|
"credit_consuming",
|
|
9798
9828
|
"state_change"
|
|
9799
9829
|
], [
|
|
@@ -9835,6 +9865,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9835
9865
|
"subheadline"
|
|
9836
9866
|
]),
|
|
9837
9867
|
slot("landing_options", false, [
|
|
9868
|
+
"competitorScanEnabled",
|
|
9869
|
+
"landingStrategy",
|
|
9870
|
+
"landingImageFormat",
|
|
9871
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9872
|
+
"aiDecidesAll"
|
|
9873
|
+
], [
|
|
9838
9874
|
"competitorScanEnabled",
|
|
9839
9875
|
"landingStrategy",
|
|
9840
9876
|
"landingImageFormat"
|
|
@@ -9863,7 +9899,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9863
9899
|
fieldCompletion: "all"
|
|
9864
9900
|
}
|
|
9865
9901
|
]),
|
|
9866
|
-
defineCapability("generate_video_creative", "
|
|
9902
|
+
defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
|
|
9867
9903
|
"credit_consuming",
|
|
9868
9904
|
"state_change"
|
|
9869
9905
|
], [
|
|
@@ -9885,6 +9921,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9885
9921
|
"country"
|
|
9886
9922
|
]),
|
|
9887
9923
|
slot("render_options", true, [
|
|
9924
|
+
"aspectRatio",
|
|
9925
|
+
"durationSeconds",
|
|
9926
|
+
"videoPreset",
|
|
9927
|
+
"videoTier",
|
|
9928
|
+
"videoResolution",
|
|
9929
|
+
"features",
|
|
9930
|
+
"cta",
|
|
9931
|
+
"scriptNotes",
|
|
9932
|
+
"pauseForVideoScriptReview",
|
|
9933
|
+
"pauseForAngleReview",
|
|
9934
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9935
|
+
"aiDecidesAll",
|
|
9936
|
+
"competitorScanEnabled"
|
|
9937
|
+
], [
|
|
9888
9938
|
"aspectRatio",
|
|
9889
9939
|
"durationSeconds",
|
|
9890
9940
|
"videoPreset",
|
|
@@ -11620,7 +11670,12 @@ async function fetchTaskOutputs(api, taskId, options) {
|
|
|
11620
11670
|
return {
|
|
11621
11671
|
angleKey: vg.angleKey ?? "",
|
|
11622
11672
|
angleIndex: vg.angleIndex ?? 0,
|
|
11623
|
-
variants: variants2.length ? variants2 : fallback
|
|
11673
|
+
variants: variants2.length ? variants2 : fallback,
|
|
11674
|
+
// Only ever true — an honest delivery leaves the key off entirely so no
|
|
11675
|
+
// consumer has to distinguish `false` from "the API is older than this".
|
|
11676
|
+
...vg.scriptRewritten === true ? {
|
|
11677
|
+
scriptRewritten: true
|
|
11678
|
+
} : {}
|
|
11624
11679
|
};
|
|
11625
11680
|
}).filter((v) => v.angleKey && v.variants.length);
|
|
11626
11681
|
const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
|
|
@@ -11683,6 +11738,9 @@ function renderOutputsMarkdown(outputs) {
|
|
|
11683
11738
|
for (const variant of v.variants) {
|
|
11684
11739
|
parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
|
|
11685
11740
|
}
|
|
11741
|
+
if (v.scriptRewritten) {
|
|
11742
|
+
parts.push(` \u26A0\uFE0F No pudimos usar el guion que aprobaste para este \xE1ngulo, as\xED que generamos el video con un guion nuevo. Si no te sirve, ed\xEDtalo o vuelve a generarlo.`);
|
|
11743
|
+
}
|
|
11686
11744
|
}
|
|
11687
11745
|
}
|
|
11688
11746
|
return parts.join("\n");
|
|
@@ -11708,6 +11766,7 @@ var CreateProductTaskInput = z48.object({
|
|
|
11708
11766
|
z48.literal(30)
|
|
11709
11767
|
]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
11710
11768
|
pauseForAngleReview: z48.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
|
|
11769
|
+
aiDecidesAll: z48.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n, as\xED que `pendingUserAction` no deber\xEDa aparecer. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
|
|
11711
11770
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
11712
11771
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez, entre 3% y 15% m\xE1s caro seg\xFAn formato y tier. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
|
|
11713
11772
|
multiAngleVideos: z48.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
@@ -12035,6 +12094,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
12035
12094
|
"options.subheadline": input.subheadline,
|
|
12036
12095
|
"options.landingStrategy": input.landingStrategy,
|
|
12037
12096
|
"options.landingImageFormat": input.landingImageFormat,
|
|
12097
|
+
"options.aiDecidesAll": input.aiDecidesAll,
|
|
12038
12098
|
pauseForAngleReview: input.pauseForAngleReview,
|
|
12039
12099
|
url: input.productUrl
|
|
12040
12100
|
}),
|
|
@@ -12106,7 +12166,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
12106
12166
|
throw translateError(e);
|
|
12107
12167
|
}
|
|
12108
12168
|
});
|
|
12109
|
-
server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO: muestra el draft y
|
|
12169
|
+
server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSIGUE LLAM\xC1NDOLA hasta que el estado sea terminal (`done` o `error`) o aparezca `pendingUserAction`. Un retorno sin ninguna de esas dos cosas significa que la tarea sigue corriendo y que alcanzaste el timeout, NO que se colg\xF3 ni que termin\xF3: vuelve a llamarla con waitSeconds alto. Si dejas de llamarla, la tarea sigue avanzando sin que nadie la mire y el usuario se queda esperando en silencio.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO esperando una decisi\xF3n del USUARIO: muestra el draft, PREG\xDANTALE y espera su respuesta. Resp\xF3ndela t\xFA mismo con `resolve_pending_action` \xFAnicamente si el usuario ya te deleg\xF3 esa decisi\xF3n \u2014 porque te lo dijo en la conversaci\xF3n, o porque la tarea corre con `aiDecidesAll` / `reviewMode: automatic`. Nunca deduzcas la delegaci\xF3n del contenido del draft: que la propuesta te parezca obvia o correcta no es un permiso. Al responder, repite el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
|
|
12110
12170
|
try {
|
|
12111
12171
|
const signal = extra?.signal;
|
|
12112
12172
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
|
|
@@ -12189,7 +12249,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
12189
12249
|
throw translateError(e);
|
|
12190
12250
|
}
|
|
12191
12251
|
});
|
|
12192
|
-
server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
|
|
12252
|
+
server.tool("resolve_pending_action", "Resuelve la acci\xF3n pendiente devuelta por `get_task_status`; `action.kind` debe coincidir exactamente. Esta llamada habla EN NOMBRE DEL USUARIO: \xFAsala solo con una respuesta que \xE9l te haya dado, o cuando te haya delegado la decisi\xF3n de forma expl\xEDcita o v\xEDa `aiDecidesAll` / `reviewMode: automatic`. Si nadie te deleg\xF3 nada, pregunta y espera en vez de llamarla. Puede aprobar el conjunto exacto de referencias del producto, confirmar agrupaci\xF3n, o resolver revisiones de \xE1ngulos/servicios. Nunca env\xEDes una lista de referencias: el servidor conserva el conjunto mostrado y rechaza snapshots obsoletos. La recuperaci\xF3n que necesita fotos nuevas se completa en ProductMaker Web porque MCP no acepta URLs arbitrarias sin ticket de upload ligado al usuario.", ResolvePendingActionInput.shape, async (input) => {
|
|
12193
12253
|
try {
|
|
12194
12254
|
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
12195
12255
|
const status = StatusForResolveSchema.parse(raw);
|
|
@@ -12492,6 +12552,7 @@ var VideoInput = z49.object({
|
|
|
12492
12552
|
scriptNotes: z49.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
|
|
12493
12553
|
pauseForVideoScriptReview: z49.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
|
|
12494
12554
|
pauseForAngleReview: z49.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
|
|
12555
|
+
aiDecidesAll: z49.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
|
|
12495
12556
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
12496
12557
|
brandId: CreativeRefs.brandId,
|
|
12497
12558
|
projectId: CreativeRefs.projectId
|
|
@@ -12569,6 +12630,7 @@ var EditImageCreativeInput = z49.object({
|
|
|
12569
12630
|
instruction: z49.string().min(1).max(1e3).describe('Qu\xE9 cambiar en esa imagen. Ej. "quita el badge de env\xEDo gratis", "cambia el fondo a blanco".')
|
|
12570
12631
|
});
|
|
12571
12632
|
var LandingInput = z49.object({
|
|
12633
|
+
aiDecidesAll: z49.boolean().optional().describe("Opcional. Si true, ProductMaker decide TODO y no vuelve a interrumpir al usuario: resuelve solo las confirmaciones de referencias del producto y no abre gates de aprobaci\xF3n. Act\xEDvalo \xFAnicamente cuando el usuario pida expl\xEDcitamente que la IA decida todo o que no le pregunten m\xE1s. Default false."),
|
|
12572
12634
|
images: CreativeRefs.images,
|
|
12573
12635
|
language: CreativeRefs.language,
|
|
12574
12636
|
country: CreativeRefs.country,
|
|
@@ -13775,7 +13837,7 @@ function uploadSessionText(result) {
|
|
|
13775
13837
|
text: [
|
|
13776
13838
|
`### Prepara tu ${mediaLabel} en ProductMaker`,
|
|
13777
13839
|
`Usa primero el App inline de este chat. Si tu host no soporta el bridge de archivos, usa este fallback: [Abrir carga segura](${result.uploadPageUrl})`,
|
|
13778
|
-
`Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}.
|
|
13840
|
+
`Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. El App sube cada archivo solo; cuando quieras seguir, escr\xEDbemelo en el chat.`,
|
|
13779
13841
|
`Este enlace expira: ${result.expiresAt}`
|
|
13780
13842
|
].join("\n\n")
|
|
13781
13843
|
}
|
|
@@ -13790,7 +13852,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
|
|
|
13790
13852
|
function registerMediaUploadTools(server, api) {
|
|
13791
13853
|
registerAppTool(server, "request_media_upload", {
|
|
13792
13854
|
title: "Subir archivos",
|
|
13793
|
-
description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos.
|
|
13855
|
+
description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
|
|
13794
13856
|
inputSchema: UploadSessionToolInputShape,
|
|
13795
13857
|
outputSchema: UniversalUploadStructuredResponse.shape,
|
|
13796
13858
|
_meta: {
|
|
@@ -13830,7 +13892,7 @@ function registerMediaUploadTools(server, api) {
|
|
|
13830
13892
|
}
|
|
13831
13893
|
});
|
|
13832
13894
|
server.registerTool("check_media_upload", {
|
|
13833
|
-
description: "Comprueba la carga universal. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
|
|
13895
|
+
description: "Comprueba la carga universal. Ll\xE1mala cuando el usuario diga que ya subi\xF3 sus archivos: es la \xFAnica forma de enterarte, porque el App no manda mensajes al chat. Si status=pending, el usuario todav\xEDa no ha terminado: d\xEDselo y espera, no inventes un assetId ni abras otra sesi\xF3n de carga. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
|
|
13834
13896
|
inputSchema: UploadStatusInput.shape,
|
|
13835
13897
|
_meta: {
|
|
13836
13898
|
"openai/widgetAccessible": true
|
|
@@ -14750,7 +14812,7 @@ var CostProjectInput = z61.object({
|
|
|
14750
14812
|
var GuideInput = z61.object({
|
|
14751
14813
|
projectId: ProjectId.optional()
|
|
14752
14814
|
}).strict();
|
|
14753
|
-
var
|
|
14815
|
+
var NEXT_ACTION_KINDS = [
|
|
14754
14816
|
"cost_product",
|
|
14755
14817
|
"provide_product_media",
|
|
14756
14818
|
"generate_creatives",
|
|
@@ -14764,7 +14826,8 @@ var DropshippingProjectNextAction = z61.discriminatedUnion("kind", [
|
|
|
14764
14826
|
"create_meta_campaign",
|
|
14765
14827
|
"repair_meta_campaign",
|
|
14766
14828
|
"review_paused_campaign"
|
|
14767
|
-
]
|
|
14829
|
+
];
|
|
14830
|
+
var DropshippingProjectNextAction = z61.discriminatedUnion("kind", NEXT_ACTION_KINDS.map((kind) => z61.object({
|
|
14768
14831
|
kind: z61.literal(kind),
|
|
14769
14832
|
href: z61.string().min(1)
|
|
14770
14833
|
})));
|
|
@@ -14838,9 +14901,25 @@ var ProjectResponseSchema = z61.object({
|
|
|
14838
14901
|
createdAt: z61.string().datetime(),
|
|
14839
14902
|
updatedAt: z61.string().datetime()
|
|
14840
14903
|
}).passthrough();
|
|
14904
|
+
var NEXT_ACTION_GUIDANCE = {
|
|
14905
|
+
cost_product: "Llama `cost_dropshipping_project` con el costo real del proveedor. Nunca lo inventes: p\xEDdeselo al usuario.",
|
|
14906
|
+
provide_product_media: "Llama `request_media_upload` con mediaKind=image y uploadPurpose=product_reference. Cuando el usuario diga que ya subi\xF3, llama `check_media_upload` y usa los assetId. No pidas ruta local ni URL.",
|
|
14907
|
+
generate_creatives: "Llama `create_product_task` con los assetId del producto. Antes dile al usuario qu\xE9 outputs vas a generar y confirma precio y oferta.",
|
|
14908
|
+
wait_for_creatives: "No hay nada que hacer todav\xEDa: la tarea est\xE1 corriendo. Llama `get_task_status` y espera. No crees otra tarea.",
|
|
14909
|
+
repair_creatives: "La tarea fall\xF3. Llama `get_task_status` para leer el error y expl\xEDcaselo al usuario antes de reintentar nada.",
|
|
14910
|
+
complete_creative_set: "Faltan outputs por terminar. Llama `get_task_status` y revisa si hay una acci\xF3n pendiente que resolver con `resolve_pending_action`.",
|
|
14911
|
+
connect_shopify: "Llama `list_shopify_shops`. Si no hay ninguna tienda conectada, el usuario debe conectarla en la web: d\xE1selo como paso manual.",
|
|
14912
|
+
publish_storefront: "Llama `publish_to_shopify` con el taskId y la tienda elegida.",
|
|
14913
|
+
wait_for_storefront: "La publicaci\xF3n en Shopify est\xE1 en curso. Llama `get_task_status` y espera; no publiques otra vez.",
|
|
14914
|
+
connect_meta: "Llama `list_meta_destinations`. Si no hay cuentas, el usuario debe conectar Meta en la web: d\xE1selo como paso manual.",
|
|
14915
|
+
create_meta_campaign: "Llama `list_meta_destinations`, pregunta en el chat por portfolioId, adAccountId y pageId, y luego `publish_to_meta`. La campa\xF1a queda PAUSED.",
|
|
14916
|
+
repair_meta_campaign: "La campa\xF1a qued\xF3 en error o pendiente. Llama `get_meta_campaign_status` y expl\xEDcale al usuario qu\xE9 pas\xF3 antes de reintentar.",
|
|
14917
|
+
review_paused_campaign: "Todo est\xE1 listo. La campa\xF1a qued\xF3 PAUSED a prop\xF3sito: dile al usuario que la active \xE9l mismo en Ads Manager."
|
|
14918
|
+
};
|
|
14841
14919
|
function projectText(raw) {
|
|
14842
14920
|
const project = ProjectResponseSchema.parse(raw);
|
|
14843
|
-
const
|
|
14921
|
+
const kind = project.nextAction?.kind;
|
|
14922
|
+
const guidance = kind ? NEXT_ACTION_GUIDANCE[kind] : void 0;
|
|
14844
14923
|
return {
|
|
14845
14924
|
content: [
|
|
14846
14925
|
{
|
|
@@ -14850,6 +14929,9 @@ function projectText(raw) {
|
|
|
14850
14929
|
productName: project.productName,
|
|
14851
14930
|
phase: project.phase,
|
|
14852
14931
|
nextAction: project.nextAction,
|
|
14932
|
+
// The href is a web anchor and is useless to an agent; the guidance
|
|
14933
|
+
// is the executable form of the same step.
|
|
14934
|
+
nextActionGuidance: guidance ?? null,
|
|
14853
14935
|
hasReferenceImage: project.hasReferenceImage,
|
|
14854
14936
|
costingSaved: Boolean(project.costingResult),
|
|
14855
14937
|
rewardProgress: {
|
|
@@ -14858,7 +14940,7 @@ function projectText(raw) {
|
|
|
14858
14940
|
milestones: project.rewards.milestones
|
|
14859
14941
|
},
|
|
14860
14942
|
version: project.version,
|
|
14861
|
-
message: `Siguiente paso: ${
|
|
14943
|
+
message: guidance ? `Siguiente paso: ${guidance} Relee este proyecto despu\xE9s de cada mutaci\xF3n.` : "El proyecto no tiene ning\xFAn paso pendiente."
|
|
14862
14944
|
})
|
|
14863
14945
|
}
|
|
14864
14946
|
]
|
|
@@ -14932,7 +15014,7 @@ var START_DROPSHIPPING_PROMPT = [
|
|
|
14932
15014
|
"",
|
|
14933
15015
|
"After I choose a product, call start_dropshipping_project. Ask me for the real supplier cost and required costing inputs before cost_dropshipping_project; never treat a marketplace sale price as supplier cost. When nextAction.kind is cost_product, call cost_product first as a free read-only estimate with those real supplier inputs, carry its price and compareAtPrice into the creative task, then call cost_dropshipping_project with the canonical inputs to persist costing. After every guided mutation, call guide_dropshipping_launch and follow ProductMaker's returned nextAction. Do not maintain a parallel workflow state in chat.",
|
|
14934
15016
|
"",
|
|
14935
|
-
"Use the existing creative/task tools for generation, preserve their estimate and confirmation gates,
|
|
15017
|
+
"Use the existing creative/task tools for generation, preserve their estimate and confirmation gates, and keep polling get_task_status until the task is done, errored, or paused \u2014 a timed-out poll means it is still running, so call again. When it pauses, show me the draft and ask me; only answer it yourself through resolve_pending_action if I delegated that decision to you or the task runs with aiDecidesAll / reviewMode automatic. Verify actual outputs before calling the creative stage complete.",
|
|
14936
15018
|
"",
|
|
14937
15019
|
"Before Shopify publication, call list_shopify_shops, check the real task readiness, ask whether the product has variants, show the intended publication, and wait for explicit approval before calling publish_to_shopify. Report the exact blocker when publication is not ready.",
|
|
14938
15020
|
"",
|
|
@@ -214,24 +214,6 @@
|
|
|
214
214
|
line-height: 1;
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
.complete-button {
|
|
218
|
-
justify-self: start;
|
|
219
|
-
min-width: 76px;
|
|
220
|
-
margin-top: 10px;
|
|
221
|
-
padding: 10px 20px;
|
|
222
|
-
border: 0;
|
|
223
|
-
border-radius: 12px;
|
|
224
|
-
background: var(--pm-primary);
|
|
225
|
-
color: #ffffff;
|
|
226
|
-
cursor: pointer;
|
|
227
|
-
font-weight: 780;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
.complete-button:disabled {
|
|
231
|
-
cursor: not-allowed;
|
|
232
|
-
opacity: 0.5;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
217
|
.drop-help {
|
|
236
218
|
position: absolute;
|
|
237
219
|
width: 1px;
|
|
@@ -511,15 +493,6 @@
|
|
|
511
493
|
aria-live="polite"
|
|
512
494
|
></p>
|
|
513
495
|
</div>
|
|
514
|
-
<button
|
|
515
|
-
class="complete-button"
|
|
516
|
-
type="button"
|
|
517
|
-
data-testid="meta-upload-complete-btn"
|
|
518
|
-
hidden
|
|
519
|
-
disabled
|
|
520
|
-
>
|
|
521
|
-
OK
|
|
522
|
-
</button>
|
|
523
496
|
</section>
|
|
524
497
|
</article>
|
|
525
498
|
</main>
|
|
@@ -576,9 +549,6 @@
|
|
|
576
549
|
batchError: document.querySelector(
|
|
577
550
|
'[data-testid="meta-upload-batch-error"]',
|
|
578
551
|
),
|
|
579
|
-
completeButton: document.querySelector(
|
|
580
|
-
'[data-testid="meta-upload-complete-btn"]',
|
|
581
|
-
),
|
|
582
552
|
uploadPrompt: document.querySelector(
|
|
583
553
|
'[data-testid="meta-upload-prompt"]',
|
|
584
554
|
),
|
|
@@ -591,7 +561,6 @@
|
|
|
591
561
|
disposed: false,
|
|
592
562
|
initialized: false,
|
|
593
563
|
standardHostReady: false,
|
|
594
|
-
hostCanMessage: false,
|
|
595
564
|
hostCanServerTools: false,
|
|
596
565
|
hostContainerDimensions: null,
|
|
597
566
|
rpcId: 0,
|
|
@@ -602,9 +571,6 @@
|
|
|
602
571
|
session: null,
|
|
603
572
|
toolInput: {},
|
|
604
573
|
items: [],
|
|
605
|
-
assets: [],
|
|
606
|
-
completionNotified: false,
|
|
607
|
-
completionNotificationInFlight: false,
|
|
608
574
|
consumedBridgeIds: new Set(),
|
|
609
575
|
consumingBridgeFiles: false,
|
|
610
576
|
bridgeConsumptionQueued: false,
|
|
@@ -677,38 +643,6 @@
|
|
|
677
643
|
scheduleIntrinsicSizeReport(true);
|
|
678
644
|
}
|
|
679
645
|
|
|
680
|
-
function canMessageHost() {
|
|
681
|
-
return (
|
|
682
|
-
hasOpenAiFunction("sendFollowUpMessage") || state.hostCanMessage
|
|
683
|
-
);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
function canCompleteUpload() {
|
|
687
|
-
return (
|
|
688
|
-
!state.disposed &&
|
|
689
|
-
Boolean(state.session) &&
|
|
690
|
-
!sessionExpired() &&
|
|
691
|
-
!state.busyBatch &&
|
|
692
|
-
!state.completionNotified &&
|
|
693
|
-
!state.completionNotificationInFlight &&
|
|
694
|
-
state.assets.length > 0 &&
|
|
695
|
-
state.items.length > 0 &&
|
|
696
|
-
state.assets.length === state.items.length &&
|
|
697
|
-
state.items.every((item) => item.status === "uploaded") &&
|
|
698
|
-
canMessageHost()
|
|
699
|
-
);
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
function renderCompletionAction() {
|
|
703
|
-
const hasVerifiedAssets = state.assets.length > 0;
|
|
704
|
-
elements.completeButton.hidden =
|
|
705
|
-
state.completionNotified || !hasVerifiedAssets || !canMessageHost();
|
|
706
|
-
elements.completeButton.disabled = !canCompleteUpload();
|
|
707
|
-
elements.completeButton.textContent =
|
|
708
|
-
state.completionNotificationInFlight ? "…" : "OK";
|
|
709
|
-
scheduleIntrinsicSizeReport(true);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
646
|
function parseFinitePositiveNumber(value) {
|
|
713
647
|
return typeof value === "number" &&
|
|
714
648
|
Number.isFinite(value) &&
|
|
@@ -927,8 +861,6 @@
|
|
|
927
861
|
!state.session ||
|
|
928
862
|
sessionExpired() ||
|
|
929
863
|
state.uploadBlocked ||
|
|
930
|
-
state.completionNotified ||
|
|
931
|
-
state.completionNotificationInFlight ||
|
|
932
864
|
state.busyBatch ||
|
|
933
865
|
state.items.length >= safeSessionLimit();
|
|
934
866
|
elements.input.disabled = disabled;
|
|
@@ -1044,7 +976,6 @@
|
|
|
1044
976
|
hostContext.containerDimensions,
|
|
1045
977
|
);
|
|
1046
978
|
applyHostContainerDimensions();
|
|
1047
|
-
state.hostCanMessage = capabilityEnabled(capabilities.message);
|
|
1048
979
|
state.hostCanServerTools = capabilityEnabled(
|
|
1049
980
|
capabilities.serverTools,
|
|
1050
981
|
);
|
|
@@ -1152,7 +1083,6 @@
|
|
|
1152
1083
|
abortActiveWork();
|
|
1153
1084
|
for (const item of state.items) revokePreviewUrl(item);
|
|
1154
1085
|
state.items = [];
|
|
1155
|
-
state.assets = [];
|
|
1156
1086
|
state.consumedBridgeIds.clear();
|
|
1157
1087
|
state.consumingBridgeFiles = false;
|
|
1158
1088
|
state.bridgeConsumptionQueued = false;
|
|
@@ -1162,8 +1092,6 @@
|
|
|
1162
1092
|
renderItems();
|
|
1163
1093
|
state.uploadBlocked = false;
|
|
1164
1094
|
state.fallbackRequired = false;
|
|
1165
|
-
state.completionNotified = false;
|
|
1166
|
-
state.completionNotificationInFlight = false;
|
|
1167
1095
|
state.workCancelled = false;
|
|
1168
1096
|
if (state.abortController?.signal.aborted) {
|
|
1169
1097
|
state.abortController = null;
|
|
@@ -1181,9 +1109,9 @@
|
|
|
1181
1109
|
} else {
|
|
1182
1110
|
setFallbackVisibility(state.fallbackRequired);
|
|
1183
1111
|
setSessionStatus("Sesión lista para recibir archivos.", "neutral");
|
|
1112
|
+
if (sessionChanged) void refreshSessionStatus({ onMount: true });
|
|
1184
1113
|
}
|
|
1185
1114
|
setPickerAvailability();
|
|
1186
|
-
renderCompletionAction();
|
|
1187
1115
|
void consumePendingBridgeFiles();
|
|
1188
1116
|
scheduleIntrinsicSizeReport(true);
|
|
1189
1117
|
}
|
|
@@ -1334,9 +1262,7 @@
|
|
|
1334
1262
|
state.disposed ||
|
|
1335
1263
|
!state.session ||
|
|
1336
1264
|
sessionExpired() ||
|
|
1337
|
-
state.uploadBlocked
|
|
1338
|
-
state.completionNotified ||
|
|
1339
|
-
state.completionNotificationInFlight
|
|
1265
|
+
state.uploadBlocked
|
|
1340
1266
|
)
|
|
1341
1267
|
return;
|
|
1342
1268
|
state.workCancelled = false;
|
|
@@ -1355,9 +1281,7 @@
|
|
|
1355
1281
|
state.disposed ||
|
|
1356
1282
|
!state.session ||
|
|
1357
1283
|
sessionExpired() ||
|
|
1358
|
-
state.uploadBlocked
|
|
1359
|
-
state.completionNotified ||
|
|
1360
|
-
state.completionNotificationInFlight
|
|
1284
|
+
state.uploadBlocked
|
|
1361
1285
|
)
|
|
1362
1286
|
return;
|
|
1363
1287
|
const bridge = openAiBridge();
|
|
@@ -1531,8 +1455,6 @@
|
|
|
1531
1455
|
!state.session ||
|
|
1532
1456
|
sessionExpired() ||
|
|
1533
1457
|
state.uploadBlocked ||
|
|
1534
|
-
state.completionNotified ||
|
|
1535
|
-
state.completionNotificationInFlight ||
|
|
1536
1458
|
state.busyBatch
|
|
1537
1459
|
)
|
|
1538
1460
|
return;
|
|
@@ -1552,10 +1474,7 @@
|
|
|
1552
1474
|
if (!selected.length) return;
|
|
1553
1475
|
|
|
1554
1476
|
state.busyBatch = true;
|
|
1555
|
-
state.assets = [];
|
|
1556
|
-
state.completionNotified = false;
|
|
1557
1477
|
setPickerAvailability();
|
|
1558
|
-
renderCompletionAction();
|
|
1559
1478
|
for (const file of selected) {
|
|
1560
1479
|
const mediaKind = mediaKindForType(file.type) || "image";
|
|
1561
1480
|
const item = createItem(file, mediaKind);
|
|
@@ -1567,7 +1486,6 @@
|
|
|
1567
1486
|
}
|
|
1568
1487
|
state.busyBatch = false;
|
|
1569
1488
|
setPickerAvailability();
|
|
1570
|
-
renderCompletionAction();
|
|
1571
1489
|
if (state.items.some((item) => item.status === "uploaded")) {
|
|
1572
1490
|
await refreshSessionStatus();
|
|
1573
1491
|
}
|
|
@@ -1588,6 +1506,7 @@
|
|
|
1588
1506
|
failureStage: "",
|
|
1589
1507
|
assetId: "",
|
|
1590
1508
|
previewUrl: "",
|
|
1509
|
+
slotReserved: false,
|
|
1591
1510
|
};
|
|
1592
1511
|
}
|
|
1593
1512
|
|
|
@@ -1610,12 +1529,15 @@
|
|
|
1610
1529
|
item.status = "uploaded";
|
|
1611
1530
|
item.progress = 100;
|
|
1612
1531
|
item.error = "";
|
|
1532
|
+
item.slotReserved = false;
|
|
1613
1533
|
emitAnalytics("mcp_media_upload_completed", item.mediaKind);
|
|
1614
1534
|
} catch (error) {
|
|
1615
1535
|
const failure = safeFailure(error);
|
|
1616
1536
|
item.status = "failed";
|
|
1617
1537
|
item.error = failure.message;
|
|
1618
1538
|
item.failureStage = failure.stage;
|
|
1539
|
+
item.failureCode = failure.code;
|
|
1540
|
+
void releaseUploadSlot(item);
|
|
1619
1541
|
emitAnalytics("mcp_media_upload_error", item.mediaKind, {
|
|
1620
1542
|
failureStage: failure.stage,
|
|
1621
1543
|
errorType: failure.code,
|
|
@@ -1626,6 +1548,10 @@
|
|
|
1626
1548
|
"danger",
|
|
1627
1549
|
true,
|
|
1628
1550
|
);
|
|
1551
|
+
} else if (failure.code === "resolve_rejected") {
|
|
1552
|
+
// Permanent: the server already refused this exact file. Offering a
|
|
1553
|
+
// retry here is what let one rejection look like nine flaky ones.
|
|
1554
|
+
requireFallback(failure.message, "danger");
|
|
1629
1555
|
} else if (failure.stage !== "validation") {
|
|
1630
1556
|
requireFallback(
|
|
1631
1557
|
"La carga no terminó. Puedes reintentarlo o continuar desde el enlace seguro.",
|
|
@@ -1904,6 +1830,9 @@
|
|
|
1904
1830
|
item.uploadFile,
|
|
1905
1831
|
item.uploadId,
|
|
1906
1832
|
);
|
|
1833
|
+
// From here the server holds a slot for this assetId until the
|
|
1834
|
+
// object lands or the TTL expires.
|
|
1835
|
+
item.slotReserved = true;
|
|
1907
1836
|
await putFileWithProgress(
|
|
1908
1837
|
resolved.uploadUrl,
|
|
1909
1838
|
item.uploadFile,
|
|
@@ -1916,6 +1845,7 @@
|
|
|
1916
1845
|
return;
|
|
1917
1846
|
} catch (error) {
|
|
1918
1847
|
if (error && error.message === "session_expired") throw error;
|
|
1848
|
+
if (error && error.permanent) throw error;
|
|
1919
1849
|
if (state.disposed || state.workCancelled) throw error;
|
|
1920
1850
|
lastError = error;
|
|
1921
1851
|
}
|
|
@@ -1933,6 +1863,96 @@
|
|
|
1933
1863
|
throw error;
|
|
1934
1864
|
}
|
|
1935
1865
|
|
|
1866
|
+
/**
|
|
1867
|
+
* Classifies a failed resolve.
|
|
1868
|
+
*
|
|
1869
|
+
* 410 means the upload link is spent and the whole session is dead; any
|
|
1870
|
+
* other 4xx means the API refused this particular file (unsupported type,
|
|
1871
|
+
* session already at its file limit). Both are deterministic, and
|
|
1872
|
+
* retrying them three times only produced three identical failures before
|
|
1873
|
+
* telling the user to "retry" something that could never succeed. Only a
|
|
1874
|
+
* server fault or a network blip earns another attempt, so anything below
|
|
1875
|
+
* 500 stops at once and carries the server's own reason to the card.
|
|
1876
|
+
*/
|
|
1877
|
+
async function resolveFailure(response) {
|
|
1878
|
+
const status = response.status;
|
|
1879
|
+
const expired = status === 401 || status === 410;
|
|
1880
|
+
const error = new Error(
|
|
1881
|
+
expired ? "session_expired" : "resolve_rejected",
|
|
1882
|
+
);
|
|
1883
|
+
error.safeCode = expired ? "session_expired" : "resolve_rejected";
|
|
1884
|
+
error.safeStage = "resolve";
|
|
1885
|
+
error.permanent = status < 500;
|
|
1886
|
+
error.safeMessage = await resolveFailureMessage(response);
|
|
1887
|
+
return error;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function resolveFailureMessage(response) {
|
|
1891
|
+
// Expiry means one thing and the App says it better than the API does;
|
|
1892
|
+
// a per-file rejection is the opposite, so there the server's own
|
|
1893
|
+
// reason is the only useful text and the copy below is just a floor.
|
|
1894
|
+
if (response.status === 401 || response.status === 410)
|
|
1895
|
+
return "La sesión expiró. Abre el enlace seguro para crear otra.";
|
|
1896
|
+
const fallback =
|
|
1897
|
+
response.status === 413
|
|
1898
|
+
? "El archivo es demasiado grande para esta carga."
|
|
1899
|
+
: response.status < 500
|
|
1900
|
+
? "ProductMaker no aceptó este archivo."
|
|
1901
|
+
: "No pudimos preparar la carga. Reintenta o abre la carga segura.";
|
|
1902
|
+
try {
|
|
1903
|
+
const body = await response.json();
|
|
1904
|
+
const detail = isRecord(body)
|
|
1905
|
+
? boundedString(
|
|
1906
|
+
typeof body.message === "string"
|
|
1907
|
+
? body.message
|
|
1908
|
+
: Array.isArray(body.message)
|
|
1909
|
+
? body.message.join(", ")
|
|
1910
|
+
: "",
|
|
1911
|
+
200,
|
|
1912
|
+
)
|
|
1913
|
+
: "";
|
|
1914
|
+
return detail || fallback;
|
|
1915
|
+
} catch {
|
|
1916
|
+
return fallback;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
/**
|
|
1921
|
+
* Hands the session's slot back when an upload will never finish.
|
|
1922
|
+
*
|
|
1923
|
+
* A resolved reservation is held for its full TTL, so one failed or
|
|
1924
|
+
* removed file used to keep a seat for fifteen minutes and every later
|
|
1925
|
+
* pick in that session was refused as "maximum number of files".
|
|
1926
|
+
*
|
|
1927
|
+
* Best effort: this runs on a path that is already failing, and the TTL
|
|
1928
|
+
* still frees the slot on its own, so no error here may reach the user.
|
|
1929
|
+
*/
|
|
1930
|
+
async function releaseUploadSlot(item) {
|
|
1931
|
+
if (!item || !item.slotReserved || !state.session) return;
|
|
1932
|
+
item.slotReserved = false;
|
|
1933
|
+
try {
|
|
1934
|
+
const uploadPage = new URL(state.session.uploadPageUrl);
|
|
1935
|
+
const releaseUrl = new URL(
|
|
1936
|
+
state.session.resolveEndpoint.replace(/\/resolve$/, "/release"),
|
|
1937
|
+
uploadPage.origin,
|
|
1938
|
+
);
|
|
1939
|
+
if (!safeReleaseEndpointUrl(releaseUrl)) return;
|
|
1940
|
+
const token =
|
|
1941
|
+
uploadPage.searchParams.get("token") ||
|
|
1942
|
+
new URLSearchParams(uploadPage.hash.slice(1)).get("token");
|
|
1943
|
+
if (!token) return;
|
|
1944
|
+
await fetch(releaseUrl.toString(), {
|
|
1945
|
+
method: "POST",
|
|
1946
|
+
headers: { "content-type": "application/json" },
|
|
1947
|
+
body: JSON.stringify({ token, uploadId: item.uploadId }),
|
|
1948
|
+
credentials: "omit",
|
|
1949
|
+
redirect: "error",
|
|
1950
|
+
});
|
|
1951
|
+
} catch {
|
|
1952
|
+
// The reservation expires on its own; never surface this.
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1936
1956
|
async function resolveUpload(file, uploadId) {
|
|
1937
1957
|
const uploadPage = new URL(state.session.uploadPageUrl);
|
|
1938
1958
|
const resolveUrl = new URL(
|
|
@@ -1967,13 +1987,7 @@
|
|
|
1967
1987
|
signal: activeAbortSignal(),
|
|
1968
1988
|
});
|
|
1969
1989
|
if (!response.ok) {
|
|
1970
|
-
|
|
1971
|
-
response.status === 401 || response.status === 410
|
|
1972
|
-
? "session_expired"
|
|
1973
|
-
: "resolve_failed",
|
|
1974
|
-
);
|
|
1975
|
-
error.safeStage = "resolve";
|
|
1976
|
-
throw error;
|
|
1990
|
+
throw await resolveFailure(response);
|
|
1977
1991
|
}
|
|
1978
1992
|
const body = await response.json();
|
|
1979
1993
|
if (!isRecord(body)) throw new Error("invalid_resolve_response");
|
|
@@ -2071,6 +2085,31 @@
|
|
|
2071
2085
|
return officialApi || sameOriginWebProxy || localApi || configuredApi;
|
|
2072
2086
|
}
|
|
2073
2087
|
|
|
2088
|
+
/**
|
|
2089
|
+
* Same origin rules as the resolver, for the sibling /release path.
|
|
2090
|
+
*
|
|
2091
|
+
* The web fallback page proxies through /api/upload-media/resolve and has
|
|
2092
|
+
* no release route, so a session using it simply skips the release and
|
|
2093
|
+
* lets the TTL free the slot. The inline App always resolves against the
|
|
2094
|
+
* API directly, which is where an abandoned reservation actually hurts.
|
|
2095
|
+
*/
|
|
2096
|
+
function safeReleaseEndpointUrl(url) {
|
|
2097
|
+
if (url.username || url.password) return false;
|
|
2098
|
+
if (url.search || url.hash) return false;
|
|
2099
|
+
const releasePath = "/v1/uploaded-assets/sessions/release";
|
|
2100
|
+
if (url.pathname !== releasePath) return false;
|
|
2101
|
+
if (url.origin === "https://api.productmaker.app") return true;
|
|
2102
|
+
if (
|
|
2103
|
+
url.protocol === "http:" &&
|
|
2104
|
+
["localhost", "127.0.0.1", "::1"].includes(url.hostname)
|
|
2105
|
+
)
|
|
2106
|
+
return true;
|
|
2107
|
+
return (
|
|
2108
|
+
Boolean(CONFIGURED_RESOLVE_ENDPOINT) &&
|
|
2109
|
+
url.origin === new URL(CONFIGURED_RESOLVE_ENDPOINT).origin
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2074
2113
|
function putFileWithProgress(url, file, onProgress) {
|
|
2075
2114
|
return new Promise((resolve, reject) => {
|
|
2076
2115
|
const request = new XMLHttpRequest();
|
|
@@ -2140,7 +2179,8 @@
|
|
|
2140
2179
|
!item ||
|
|
2141
2180
|
item.status !== "failed" ||
|
|
2142
2181
|
!item.file ||
|
|
2143
|
-
item.failureStage === "validation"
|
|
2182
|
+
item.failureStage === "validation" ||
|
|
2183
|
+
item.failureCode === "resolve_rejected"
|
|
2144
2184
|
)
|
|
2145
2185
|
return;
|
|
2146
2186
|
item.attempts = 0;
|
|
@@ -2150,8 +2190,14 @@
|
|
|
2150
2190
|
|
|
2151
2191
|
function removeItem(index) {
|
|
2152
2192
|
if (state.busyBatch) return;
|
|
2193
|
+
// A rehydrated item is a receipt for bytes already in staging; dropping
|
|
2194
|
+
// the card here would hide an asset the agent can still see and use.
|
|
2195
|
+
if (state.items[index]?.rehydrated) return;
|
|
2153
2196
|
const [item] = state.items.splice(index, 1);
|
|
2154
|
-
if (item)
|
|
2197
|
+
if (item) {
|
|
2198
|
+
revokePreviewUrl(item);
|
|
2199
|
+
if (item.status !== "uploaded") void releaseUploadSlot(item);
|
|
2200
|
+
}
|
|
2155
2201
|
renderItems();
|
|
2156
2202
|
setPickerAvailability();
|
|
2157
2203
|
}
|
|
@@ -2273,6 +2319,7 @@
|
|
|
2273
2319
|
if (
|
|
2274
2320
|
item.status === "failed" &&
|
|
2275
2321
|
item.failureStage !== "validation" &&
|
|
2322
|
+
item.failureCode !== "resolve_rejected" &&
|
|
2276
2323
|
item.file
|
|
2277
2324
|
) {
|
|
2278
2325
|
const retry = document.createElement("button");
|
|
@@ -2319,7 +2366,6 @@
|
|
|
2319
2366
|
fragment.append(card);
|
|
2320
2367
|
});
|
|
2321
2368
|
elements.fileList.replaceChildren(fragment);
|
|
2322
|
-
renderCompletionAction();
|
|
2323
2369
|
scheduleIntrinsicSizeReport(true);
|
|
2324
2370
|
}
|
|
2325
2371
|
|
|
@@ -2331,10 +2377,54 @@
|
|
|
2331
2377
|
return "Pendiente";
|
|
2332
2378
|
}
|
|
2333
2379
|
|
|
2334
|
-
|
|
2380
|
+
function uploadedStatusMessage(assets) {
|
|
2381
|
+
return assets.length === 1
|
|
2382
|
+
? "Archivo listo. Escribe en el chat para continuar."
|
|
2383
|
+
: `${assets.length} archivos listos. Escribe en el chat para continuar.`;
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Rebuilds the file list from assets the session already holds, so a
|
|
2388
|
+
* re-rendered App shows what was uploaded instead of an empty picker.
|
|
2389
|
+
* These items have no local File: they cannot be retried or removed,
|
|
2390
|
+
* because the bytes live in the staging bucket and not in this page.
|
|
2391
|
+
*/
|
|
2392
|
+
function adoptAlreadyUploadedAssets(assets) {
|
|
2393
|
+
for (const item of state.items) revokePreviewUrl(item);
|
|
2394
|
+
state.items = assets.map((asset, index) => ({
|
|
2395
|
+
id: `rehydrated-${asset.assetId}`,
|
|
2396
|
+
uploadId: asset.assetId,
|
|
2397
|
+
file: null,
|
|
2398
|
+
displayName: `Archivo ${index + 1}`,
|
|
2399
|
+
uploadFile: null,
|
|
2400
|
+
mediaKind: asset.mediaKind,
|
|
2401
|
+
status: "uploaded",
|
|
2402
|
+
progress: 100,
|
|
2403
|
+
attempts: 0,
|
|
2404
|
+
error: "",
|
|
2405
|
+
failureStage: "",
|
|
2406
|
+
assetId: asset.assetId,
|
|
2407
|
+
previewUrl: "",
|
|
2408
|
+
rehydrated: true,
|
|
2409
|
+
}));
|
|
2410
|
+
renderItems();
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
/**
|
|
2414
|
+
* Reads the staging session's real contents.
|
|
2415
|
+
*
|
|
2416
|
+
* `onMount` marks the call the App makes when a host re-renders a saved
|
|
2417
|
+
* tool result — reopening the chat, switching away and back. That render
|
|
2418
|
+
* starts from an empty widget with no items, so without this the user is
|
|
2419
|
+
* shown a fresh picker for files they already uploaded. On mount an empty
|
|
2420
|
+
* session is the normal case (nothing uploaded yet) and must stay silent;
|
|
2421
|
+
* after an upload the same emptiness means the upload did not land.
|
|
2422
|
+
*/
|
|
2423
|
+
async function refreshSessionStatus({ onMount = false } = {}) {
|
|
2335
2424
|
if (!state.session) return;
|
|
2336
2425
|
const requestedSessionId = state.session.sessionId;
|
|
2337
|
-
|
|
2426
|
+
if (!onMount)
|
|
2427
|
+
setSessionStatus("Comprobando el staging privado…", "neutral");
|
|
2338
2428
|
try {
|
|
2339
2429
|
const result = await callReadOnlyTool(state.session.checkToolName, {
|
|
2340
2430
|
sessionId: requestedSessionId,
|
|
@@ -2346,9 +2436,8 @@
|
|
|
2346
2436
|
)
|
|
2347
2437
|
return;
|
|
2348
2438
|
const status = parseStatusResult(result);
|
|
2349
|
-
state.assets = status.assets;
|
|
2350
|
-
renderCompletionAction();
|
|
2351
2439
|
if (!status.assets.length) {
|
|
2440
|
+
if (onMount) return;
|
|
2352
2441
|
requireFallback(
|
|
2353
2442
|
"La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
|
|
2354
2443
|
"warning",
|
|
@@ -2356,33 +2445,38 @@
|
|
|
2356
2445
|
return;
|
|
2357
2446
|
}
|
|
2358
2447
|
|
|
2448
|
+
if (onMount) {
|
|
2449
|
+
// The probe is async: the user can pick a file while it is in
|
|
2450
|
+
// flight. Their selection is the live truth, so never overwrite it
|
|
2451
|
+
// with the snapshot this call started from.
|
|
2452
|
+
if (state.items.length > 0 || state.busyBatch) return;
|
|
2453
|
+
adoptAlreadyUploadedAssets(status.assets);
|
|
2454
|
+
setSessionStatus(uploadedStatusMessage(status.assets), "success");
|
|
2455
|
+
setPickerAvailability();
|
|
2456
|
+
scheduleIntrinsicSizeReport(true);
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2359
2460
|
const partial =
|
|
2360
2461
|
status.status === "pending" ||
|
|
2361
2462
|
(state.items.length > 0 &&
|
|
2362
2463
|
status.assets.length < state.items.length);
|
|
2363
|
-
const canContinueInChat = canMessageHost();
|
|
2364
2464
|
if (partial) {
|
|
2365
2465
|
requireFallback(
|
|
2366
2466
|
"Carga parcial verificada. Puedes continuar desde el enlace seguro si algún archivo no terminó.",
|
|
2367
2467
|
"warning",
|
|
2368
2468
|
);
|
|
2369
|
-
} else if (!canContinueInChat) {
|
|
2370
|
-
requireFallback(
|
|
2371
|
-
"Carga completada. Este chat no puede continuar desde el widget. Abre el enlace seguro para seguir.",
|
|
2372
|
-
"warning",
|
|
2373
|
-
);
|
|
2374
2469
|
} else {
|
|
2375
2470
|
setFallbackVisibility(state.fallbackRequired);
|
|
2376
2471
|
}
|
|
2377
2472
|
setSessionStatus(
|
|
2378
2473
|
partial
|
|
2379
2474
|
? "Carga parcial verificada. Continuaremos solo con los assets listos."
|
|
2380
|
-
:
|
|
2381
|
-
|
|
2382
|
-
: "Carga completada. Abre el enlace seguro para seguir.",
|
|
2383
|
-
partial || !canContinueInChat ? "warning" : "success",
|
|
2475
|
+
: uploadedStatusMessage(status.assets),
|
|
2476
|
+
partial ? "warning" : "success",
|
|
2384
2477
|
);
|
|
2385
2478
|
} catch (error) {
|
|
2479
|
+
if (onMount) return;
|
|
2386
2480
|
const expired =
|
|
2387
2481
|
sessionExpired() ||
|
|
2388
2482
|
(error && /expired|expirad/i.test(String(error.message)));
|
|
@@ -2461,82 +2555,6 @@
|
|
|
2461
2555
|
return { status, assets };
|
|
2462
2556
|
}
|
|
2463
2557
|
|
|
2464
|
-
async function notifyChatUploadComplete() {
|
|
2465
|
-
if (
|
|
2466
|
-
state.completionNotified ||
|
|
2467
|
-
state.completionNotificationInFlight ||
|
|
2468
|
-
!state.session
|
|
2469
|
-
)
|
|
2470
|
-
return;
|
|
2471
|
-
const completionSessionId = state.session.sessionId;
|
|
2472
|
-
state.completionNotificationInFlight = true;
|
|
2473
|
-
setPickerAvailability();
|
|
2474
|
-
renderCompletionAction();
|
|
2475
|
-
const assets = state.assets
|
|
2476
|
-
.map((asset) => `${asset.mediaKind} asset_id ${asset.assetId}`)
|
|
2477
|
-
.join(", ");
|
|
2478
|
-
const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again. Ask only for the remaining inputs required by my original request.`;
|
|
2479
|
-
try {
|
|
2480
|
-
const bridge = openAiBridge();
|
|
2481
|
-
if (bridge && typeof bridge.sendFollowUpMessage === "function") {
|
|
2482
|
-
await bridge.sendFollowUpMessage({ prompt: message });
|
|
2483
|
-
if (
|
|
2484
|
-
!state.session ||
|
|
2485
|
-
state.session.sessionId !== completionSessionId
|
|
2486
|
-
)
|
|
2487
|
-
return;
|
|
2488
|
-
state.completionNotified = true;
|
|
2489
|
-
setPickerAvailability();
|
|
2490
|
-
renderCompletionAction();
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2493
|
-
if (state.hostCanMessage) {
|
|
2494
|
-
const messageResult = await rpcRequest("ui/message", {
|
|
2495
|
-
role: "user",
|
|
2496
|
-
content: [{ type: "text", text: message }],
|
|
2497
|
-
});
|
|
2498
|
-
if (isRecord(messageResult) && messageResult.isError === true)
|
|
2499
|
-
throw new Error("host_message_rejected");
|
|
2500
|
-
if (
|
|
2501
|
-
!state.session ||
|
|
2502
|
-
state.session.sessionId !== completionSessionId
|
|
2503
|
-
)
|
|
2504
|
-
return;
|
|
2505
|
-
state.completionNotified = true;
|
|
2506
|
-
setPickerAvailability();
|
|
2507
|
-
renderCompletionAction();
|
|
2508
|
-
return;
|
|
2509
|
-
}
|
|
2510
|
-
setSessionStatus(
|
|
2511
|
-
"La carga terminó, pero este host no puede continuar el chat desde el widget. Vuelve al chat para seguir.",
|
|
2512
|
-
"warning",
|
|
2513
|
-
);
|
|
2514
|
-
} catch {
|
|
2515
|
-
if (
|
|
2516
|
-
!state.session ||
|
|
2517
|
-
state.session.sessionId !== completionSessionId
|
|
2518
|
-
)
|
|
2519
|
-
return;
|
|
2520
|
-
setSessionStatus(
|
|
2521
|
-
"No pudimos continuar el chat. Intenta nuevamente.",
|
|
2522
|
-
"danger",
|
|
2523
|
-
);
|
|
2524
|
-
} finally {
|
|
2525
|
-
if (
|
|
2526
|
-
state.session &&
|
|
2527
|
-
state.session.sessionId === completionSessionId
|
|
2528
|
-
) {
|
|
2529
|
-
state.completionNotificationInFlight = false;
|
|
2530
|
-
setPickerAvailability();
|
|
2531
|
-
renderCompletionAction();
|
|
2532
|
-
}
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
elements.completeButton.addEventListener("click", () => {
|
|
2537
|
-
if (canCompleteUpload()) void notifyChatUploadComplete();
|
|
2538
|
-
});
|
|
2539
|
-
|
|
2540
2558
|
document.addEventListener("DOMContentLoaded", initialize, {
|
|
2541
2559
|
once: true,
|
|
2542
2560
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Product Maker MCP server — cost products, generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from Claude, ChatGPT, or Gemini.",
|