@productmaker/mcp 3.1.2 → 3.2.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 +46 -10
- package/dist/stdio.js +46 -10
- package/dist/ui/meta-upload-app.html +203 -185
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -8906,7 +8906,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
8906
8906
|
], [
|
|
8907
8907
|
"creative_variant"
|
|
8908
8908
|
]),
|
|
8909
|
-
defineCapability("create_product_task", "
|
|
8909
|
+
defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
|
|
8910
8910
|
"credit_consuming",
|
|
8911
8911
|
"state_change"
|
|
8912
8912
|
], [
|
|
@@ -8947,6 +8947,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
8947
8947
|
"productContext"
|
|
8948
8948
|
]),
|
|
8949
8949
|
slot("video_options", false, [
|
|
8950
|
+
"videoPreset",
|
|
8951
|
+
"durationSeconds",
|
|
8952
|
+
"pauseForAngleReview",
|
|
8953
|
+
// MCP-only: WhatsApp runs its own confirmation policy and does not
|
|
8954
|
+
// offer a "decide everything" mode, so the field is not mirrored there.
|
|
8955
|
+
"aiDecidesAll",
|
|
8956
|
+
"videoTier",
|
|
8957
|
+
"videoResolution",
|
|
8958
|
+
"multiAngleVideos",
|
|
8959
|
+
"angleNarratives",
|
|
8960
|
+
"competitorScanEnabled"
|
|
8961
|
+
], [
|
|
8950
8962
|
"videoPreset",
|
|
8951
8963
|
"durationSeconds",
|
|
8952
8964
|
"pauseForAngleReview",
|
|
@@ -9172,7 +9184,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9172
9184
|
], [
|
|
9173
9185
|
"creative"
|
|
9174
9186
|
]),
|
|
9175
|
-
defineCapability("generate_landing", "
|
|
9187
|
+
defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
|
|
9176
9188
|
"credit_consuming",
|
|
9177
9189
|
"state_change"
|
|
9178
9190
|
], [
|
|
@@ -9214,6 +9226,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9214
9226
|
"subheadline"
|
|
9215
9227
|
]),
|
|
9216
9228
|
slot("landing_options", false, [
|
|
9229
|
+
"competitorScanEnabled",
|
|
9230
|
+
"landingStrategy",
|
|
9231
|
+
"landingImageFormat",
|
|
9232
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9233
|
+
"aiDecidesAll"
|
|
9234
|
+
], [
|
|
9217
9235
|
"competitorScanEnabled",
|
|
9218
9236
|
"landingStrategy",
|
|
9219
9237
|
"landingImageFormat"
|
|
@@ -9242,7 +9260,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9242
9260
|
fieldCompletion: "all"
|
|
9243
9261
|
}
|
|
9244
9262
|
]),
|
|
9245
|
-
defineCapability("generate_video_creative", "
|
|
9263
|
+
defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
|
|
9246
9264
|
"credit_consuming",
|
|
9247
9265
|
"state_change"
|
|
9248
9266
|
], [
|
|
@@ -9264,6 +9282,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9264
9282
|
"country"
|
|
9265
9283
|
]),
|
|
9266
9284
|
slot("render_options", true, [
|
|
9285
|
+
"aspectRatio",
|
|
9286
|
+
"durationSeconds",
|
|
9287
|
+
"videoPreset",
|
|
9288
|
+
"videoTier",
|
|
9289
|
+
"videoResolution",
|
|
9290
|
+
"features",
|
|
9291
|
+
"cta",
|
|
9292
|
+
"scriptNotes",
|
|
9293
|
+
"pauseForVideoScriptReview",
|
|
9294
|
+
"pauseForAngleReview",
|
|
9295
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9296
|
+
"aiDecidesAll",
|
|
9297
|
+
"competitorScanEnabled"
|
|
9298
|
+
], [
|
|
9267
9299
|
"aspectRatio",
|
|
9268
9300
|
"durationSeconds",
|
|
9269
9301
|
"videoPreset",
|
|
@@ -11042,7 +11074,7 @@ var START_DROPSHIPPING_PROMPT = [
|
|
|
11042
11074
|
"",
|
|
11043
11075
|
"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
11076
|
"",
|
|
11045
|
-
"Use the existing creative/task tools for generation, preserve their estimate and confirmation gates,
|
|
11077
|
+
"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
11078
|
"",
|
|
11047
11079
|
"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
11080
|
"",
|
|
@@ -11368,7 +11400,7 @@ var ApiClient = class {
|
|
|
11368
11400
|
};
|
|
11369
11401
|
|
|
11370
11402
|
// src/index.ts
|
|
11371
|
-
var MCP_VERSION = "3.
|
|
11403
|
+
var MCP_VERSION = "3.2.0";
|
|
11372
11404
|
|
|
11373
11405
|
// src/meta-upload-app.ts
|
|
11374
11406
|
import { readFileSync } from "fs";
|
|
@@ -12906,6 +12938,7 @@ var CreateProductTaskInput = z49.object({
|
|
|
12906
12938
|
z49.literal(30)
|
|
12907
12939
|
]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
12908
12940
|
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."),
|
|
12941
|
+
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
12942
|
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
12943
|
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
12944
|
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 +13266,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
13233
13266
|
"options.subheadline": input.subheadline,
|
|
13234
13267
|
"options.landingStrategy": input.landingStrategy,
|
|
13235
13268
|
"options.landingImageFormat": input.landingImageFormat,
|
|
13269
|
+
"options.aiDecidesAll": input.aiDecidesAll,
|
|
13236
13270
|
pauseForAngleReview: input.pauseForAngleReview,
|
|
13237
13271
|
url: input.productUrl
|
|
13238
13272
|
}),
|
|
@@ -13304,7 +13338,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
13304
13338
|
throw translateError(e);
|
|
13305
13339
|
}
|
|
13306
13340
|
});
|
|
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
|
|
13341
|
+
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
13342
|
try {
|
|
13309
13343
|
const signal = extra?.signal;
|
|
13310
13344
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
|
|
@@ -13387,7 +13421,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
13387
13421
|
throw translateError(e);
|
|
13388
13422
|
}
|
|
13389
13423
|
});
|
|
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) => {
|
|
13424
|
+
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
13425
|
try {
|
|
13392
13426
|
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
13393
13427
|
const status = StatusForResolveSchema.parse(raw);
|
|
@@ -13690,6 +13724,7 @@ var VideoInput = z50.object({
|
|
|
13690
13724
|
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
13725
|
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
13726
|
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."),
|
|
13727
|
+
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
13728
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
13694
13729
|
brandId: CreativeRefs.brandId,
|
|
13695
13730
|
projectId: CreativeRefs.projectId
|
|
@@ -13767,6 +13802,7 @@ var EditImageCreativeInput = z50.object({
|
|
|
13767
13802
|
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
13803
|
});
|
|
13769
13804
|
var LandingInput = z50.object({
|
|
13805
|
+
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
13806
|
images: CreativeRefs.images,
|
|
13771
13807
|
language: CreativeRefs.language,
|
|
13772
13808
|
country: CreativeRefs.country,
|
|
@@ -14973,7 +15009,7 @@ function uploadSessionText(result) {
|
|
|
14973
15009
|
text: [
|
|
14974
15010
|
`### Prepara tu ${mediaLabel} en ProductMaker`,
|
|
14975
15011
|
`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"}.
|
|
15012
|
+
`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
15013
|
`Este enlace expira: ${result.expiresAt}`
|
|
14978
15014
|
].join("\n\n")
|
|
14979
15015
|
}
|
|
@@ -14988,7 +15024,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
|
|
|
14988
15024
|
function registerMediaUploadTools(server, api) {
|
|
14989
15025
|
registerAppTool(server, "request_media_upload", {
|
|
14990
15026
|
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.
|
|
15027
|
+
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
15028
|
inputSchema: UploadSessionToolInputShape,
|
|
14993
15029
|
outputSchema: UniversalUploadStructuredResponse.shape,
|
|
14994
15030
|
_meta: {
|
|
@@ -15028,7 +15064,7 @@ function registerMediaUploadTools(server, api) {
|
|
|
15028
15064
|
}
|
|
15029
15065
|
});
|
|
15030
15066
|
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.",
|
|
15067
|
+
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
15068
|
inputSchema: UploadStatusInput.shape,
|
|
15033
15069
|
_meta: {
|
|
15034
15070
|
"openai/widgetAccessible": true
|
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.2.0";
|
|
268
268
|
|
|
269
269
|
// src/meta-upload-app.ts
|
|
270
270
|
import { readFileSync } from "fs";
|
|
@@ -9527,7 +9527,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9527
9527
|
], [
|
|
9528
9528
|
"creative_variant"
|
|
9529
9529
|
]),
|
|
9530
|
-
defineCapability("create_product_task", "
|
|
9530
|
+
defineCapability("create_product_task", "a6a2d3c4d462e43496d440f0bea37292894a2b818c5ccfa364fcdaac7982544e", "credit_consuming", "creative", [
|
|
9531
9531
|
"credit_consuming",
|
|
9532
9532
|
"state_change"
|
|
9533
9533
|
], [
|
|
@@ -9568,6 +9568,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9568
9568
|
"productContext"
|
|
9569
9569
|
]),
|
|
9570
9570
|
slot("video_options", false, [
|
|
9571
|
+
"videoPreset",
|
|
9572
|
+
"durationSeconds",
|
|
9573
|
+
"pauseForAngleReview",
|
|
9574
|
+
// MCP-only: WhatsApp runs its own confirmation policy and does not
|
|
9575
|
+
// offer a "decide everything" mode, so the field is not mirrored there.
|
|
9576
|
+
"aiDecidesAll",
|
|
9577
|
+
"videoTier",
|
|
9578
|
+
"videoResolution",
|
|
9579
|
+
"multiAngleVideos",
|
|
9580
|
+
"angleNarratives",
|
|
9581
|
+
"competitorScanEnabled"
|
|
9582
|
+
], [
|
|
9571
9583
|
"videoPreset",
|
|
9572
9584
|
"durationSeconds",
|
|
9573
9585
|
"pauseForAngleReview",
|
|
@@ -9793,7 +9805,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9793
9805
|
], [
|
|
9794
9806
|
"creative"
|
|
9795
9807
|
]),
|
|
9796
|
-
defineCapability("generate_landing", "
|
|
9808
|
+
defineCapability("generate_landing", "75947c5a70d26305ca07ab8d7bc0be710e862eb9319010ee66dd43e64fab2050", "credit_consuming", "creative", [
|
|
9797
9809
|
"credit_consuming",
|
|
9798
9810
|
"state_change"
|
|
9799
9811
|
], [
|
|
@@ -9835,6 +9847,12 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9835
9847
|
"subheadline"
|
|
9836
9848
|
]),
|
|
9837
9849
|
slot("landing_options", false, [
|
|
9850
|
+
"competitorScanEnabled",
|
|
9851
|
+
"landingStrategy",
|
|
9852
|
+
"landingImageFormat",
|
|
9853
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9854
|
+
"aiDecidesAll"
|
|
9855
|
+
], [
|
|
9838
9856
|
"competitorScanEnabled",
|
|
9839
9857
|
"landingStrategy",
|
|
9840
9858
|
"landingImageFormat"
|
|
@@ -9863,7 +9881,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9863
9881
|
fieldCompletion: "all"
|
|
9864
9882
|
}
|
|
9865
9883
|
]),
|
|
9866
|
-
defineCapability("generate_video_creative", "
|
|
9884
|
+
defineCapability("generate_video_creative", "417f185d4bf8bbcdf044f98f4b8366473178aca8e7a99cc6f8d58cb0221442dc", "credit_consuming", "creative", [
|
|
9867
9885
|
"credit_consuming",
|
|
9868
9886
|
"state_change"
|
|
9869
9887
|
], [
|
|
@@ -9885,6 +9903,20 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
|
|
|
9885
9903
|
"country"
|
|
9886
9904
|
]),
|
|
9887
9905
|
slot("render_options", true, [
|
|
9906
|
+
"aspectRatio",
|
|
9907
|
+
"durationSeconds",
|
|
9908
|
+
"videoPreset",
|
|
9909
|
+
"videoTier",
|
|
9910
|
+
"videoResolution",
|
|
9911
|
+
"features",
|
|
9912
|
+
"cta",
|
|
9913
|
+
"scriptNotes",
|
|
9914
|
+
"pauseForVideoScriptReview",
|
|
9915
|
+
"pauseForAngleReview",
|
|
9916
|
+
// MCP-only, like create_product_task's aiDecidesAll.
|
|
9917
|
+
"aiDecidesAll",
|
|
9918
|
+
"competitorScanEnabled"
|
|
9919
|
+
], [
|
|
9888
9920
|
"aspectRatio",
|
|
9889
9921
|
"durationSeconds",
|
|
9890
9922
|
"videoPreset",
|
|
@@ -11708,6 +11740,7 @@ var CreateProductTaskInput = z48.object({
|
|
|
11708
11740
|
z48.literal(30)
|
|
11709
11741
|
]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
11710
11742
|
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."),
|
|
11743
|
+
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
11744
|
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
11745
|
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
11746
|
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 +12068,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
12035
12068
|
"options.subheadline": input.subheadline,
|
|
12036
12069
|
"options.landingStrategy": input.landingStrategy,
|
|
12037
12070
|
"options.landingImageFormat": input.landingImageFormat,
|
|
12071
|
+
"options.aiDecidesAll": input.aiDecidesAll,
|
|
12038
12072
|
pauseForAngleReview: input.pauseForAngleReview,
|
|
12039
12073
|
url: input.productUrl
|
|
12040
12074
|
}),
|
|
@@ -12106,7 +12140,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
12106
12140
|
throw translateError(e);
|
|
12107
12141
|
}
|
|
12108
12142
|
});
|
|
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
|
|
12143
|
+
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
12144
|
try {
|
|
12111
12145
|
const signal = extra?.signal;
|
|
12112
12146
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
|
|
@@ -12189,7 +12223,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
12189
12223
|
throw translateError(e);
|
|
12190
12224
|
}
|
|
12191
12225
|
});
|
|
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) => {
|
|
12226
|
+
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
12227
|
try {
|
|
12194
12228
|
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
12195
12229
|
const status = StatusForResolveSchema.parse(raw);
|
|
@@ -12492,6 +12526,7 @@ var VideoInput = z49.object({
|
|
|
12492
12526
|
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
12527
|
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
12528
|
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."),
|
|
12529
|
+
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
12530
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
12496
12531
|
brandId: CreativeRefs.brandId,
|
|
12497
12532
|
projectId: CreativeRefs.projectId
|
|
@@ -12569,6 +12604,7 @@ var EditImageCreativeInput = z49.object({
|
|
|
12569
12604
|
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
12605
|
});
|
|
12571
12606
|
var LandingInput = z49.object({
|
|
12607
|
+
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
12608
|
images: CreativeRefs.images,
|
|
12573
12609
|
language: CreativeRefs.language,
|
|
12574
12610
|
country: CreativeRefs.country,
|
|
@@ -13775,7 +13811,7 @@ function uploadSessionText(result) {
|
|
|
13775
13811
|
text: [
|
|
13776
13812
|
`### Prepara tu ${mediaLabel} en ProductMaker`,
|
|
13777
13813
|
`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"}.
|
|
13814
|
+
`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
13815
|
`Este enlace expira: ${result.expiresAt}`
|
|
13780
13816
|
].join("\n\n")
|
|
13781
13817
|
}
|
|
@@ -13790,7 +13826,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
|
|
|
13790
13826
|
function registerMediaUploadTools(server, api) {
|
|
13791
13827
|
registerAppTool(server, "request_media_upload", {
|
|
13792
13828
|
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.
|
|
13829
|
+
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
13830
|
inputSchema: UploadSessionToolInputShape,
|
|
13795
13831
|
outputSchema: UniversalUploadStructuredResponse.shape,
|
|
13796
13832
|
_meta: {
|
|
@@ -13830,7 +13866,7 @@ function registerMediaUploadTools(server, api) {
|
|
|
13830
13866
|
}
|
|
13831
13867
|
});
|
|
13832
13868
|
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.",
|
|
13869
|
+
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
13870
|
inputSchema: UploadStatusInput.shape,
|
|
13835
13871
|
_meta: {
|
|
13836
13872
|
"openai/widgetAccessible": true
|
|
@@ -14932,7 +14968,7 @@ var START_DROPSHIPPING_PROMPT = [
|
|
|
14932
14968
|
"",
|
|
14933
14969
|
"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
14970
|
"",
|
|
14935
|
-
"Use the existing creative/task tools for generation, preserve their estimate and confirmation gates,
|
|
14971
|
+
"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
14972
|
"",
|
|
14937
14973
|
"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
14974
|
"",
|
|
@@ -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.2.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.",
|