@productmaker/mcp 12.2.6 → 12.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -14
- package/dist/http.js +1426 -1107
- package/dist/stdio.js +1429 -1112
- package/dist/ui/meta-upload-app.html +221 -80
- package/package.json +1 -1
|
@@ -569,6 +569,7 @@
|
|
|
569
569
|
forceSizeReport: false,
|
|
570
570
|
lastReportedHeight: 0,
|
|
571
571
|
session: null,
|
|
572
|
+
sessionGeneration: 0,
|
|
572
573
|
toolInput: {},
|
|
573
574
|
items: [],
|
|
574
575
|
consumedBridgeIds: new Set(),
|
|
@@ -812,6 +813,33 @@
|
|
|
812
813
|
return state.abortController?.signal;
|
|
813
814
|
}
|
|
814
815
|
|
|
816
|
+
// An immutable work context prevents old async continuations from
|
|
817
|
+
// acquiring a replacement session's credentials or mutating its UI.
|
|
818
|
+
function captureUploadWork() {
|
|
819
|
+
return state.session
|
|
820
|
+
? {
|
|
821
|
+
session: state.session,
|
|
822
|
+
generation: state.sessionGeneration,
|
|
823
|
+
signal: activeAbortSignal(),
|
|
824
|
+
}
|
|
825
|
+
: null;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function uploadWorkIsCurrent(work) {
|
|
829
|
+
return Boolean(
|
|
830
|
+
work &&
|
|
831
|
+
!state.disposed &&
|
|
832
|
+
!state.workCancelled &&
|
|
833
|
+
!work.signal?.aborted &&
|
|
834
|
+
work.generation === state.sessionGeneration &&
|
|
835
|
+
work.session.sessionId === state.session?.sessionId,
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function requireCurrentUploadWork(work) {
|
|
840
|
+
if (!uploadWorkIsCurrent(work)) throw new Error("upload_cancelled");
|
|
841
|
+
}
|
|
842
|
+
|
|
815
843
|
function abortActiveWork() {
|
|
816
844
|
state.abortController?.abort();
|
|
817
845
|
for (const request of state.activeXhrs) request.abort();
|
|
@@ -1013,7 +1041,12 @@
|
|
|
1013
1041
|
}
|
|
1014
1042
|
|
|
1015
1043
|
function handleHostMessage(event) {
|
|
1016
|
-
if (
|
|
1044
|
+
if (
|
|
1045
|
+
event.source !== window.parent ||
|
|
1046
|
+
!isRecord(event.data) ||
|
|
1047
|
+
event.data.jsonrpc !== "2.0"
|
|
1048
|
+
)
|
|
1049
|
+
return;
|
|
1017
1050
|
const message = event.data;
|
|
1018
1051
|
if (message.method === "ui/resource-teardown") {
|
|
1019
1052
|
cleanup();
|
|
@@ -1046,7 +1079,58 @@
|
|
|
1046
1079
|
applyHostContainerDimensions();
|
|
1047
1080
|
return;
|
|
1048
1081
|
}
|
|
1082
|
+
// Host requests have their own ID namespace: a ping may reuse an
|
|
1083
|
+
// outstanding app request ID without being its response.
|
|
1084
|
+
if (message.method === "ping") {
|
|
1085
|
+
if (
|
|
1086
|
+
(typeof message.id === "string" ||
|
|
1087
|
+
Number.isInteger(message.id)) &&
|
|
1088
|
+
!Object.prototype.hasOwnProperty.call(message, "result") &&
|
|
1089
|
+
!Object.prototype.hasOwnProperty.call(message, "error") &&
|
|
1090
|
+
(message.params === undefined || isRecord(message.params))
|
|
1091
|
+
)
|
|
1092
|
+
window.parent.postMessage(
|
|
1093
|
+
{ jsonrpc: "2.0", id: message.id, result: {} },
|
|
1094
|
+
"*",
|
|
1095
|
+
);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
if (typeof message.method === "string") {
|
|
1099
|
+
if (
|
|
1100
|
+
(typeof message.id === "string" ||
|
|
1101
|
+
Number.isInteger(message.id)) &&
|
|
1102
|
+
!Object.prototype.hasOwnProperty.call(message, "result") &&
|
|
1103
|
+
!Object.prototype.hasOwnProperty.call(message, "error") &&
|
|
1104
|
+
(message.params === undefined ||
|
|
1105
|
+
isRecord(message.params) ||
|
|
1106
|
+
Array.isArray(message.params))
|
|
1107
|
+
) {
|
|
1108
|
+
window.parent.postMessage(
|
|
1109
|
+
{
|
|
1110
|
+
jsonrpc: "2.0",
|
|
1111
|
+
id: message.id,
|
|
1112
|
+
error: { code: -32601, message: "Method not found" },
|
|
1113
|
+
},
|
|
1114
|
+
"*",
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
// Notifications have no id and are handled below.
|
|
1118
|
+
if (Object.prototype.hasOwnProperty.call(message, "id")) return;
|
|
1119
|
+
}
|
|
1049
1120
|
if (Object.prototype.hasOwnProperty.call(message, "id")) {
|
|
1121
|
+
const successResponse =
|
|
1122
|
+
isRecord(message.result) &&
|
|
1123
|
+
!Object.prototype.hasOwnProperty.call(message, "error");
|
|
1124
|
+
const errorResponse =
|
|
1125
|
+
isRecord(message.error) &&
|
|
1126
|
+
Number.isInteger(message.error.code) &&
|
|
1127
|
+
typeof message.error.message === "string" &&
|
|
1128
|
+
!Object.prototype.hasOwnProperty.call(message, "result");
|
|
1129
|
+
if (
|
|
1130
|
+
Object.prototype.hasOwnProperty.call(message, "method") ||
|
|
1131
|
+
(!successResponse && !errorResponse)
|
|
1132
|
+
)
|
|
1133
|
+
return;
|
|
1050
1134
|
const pending = state.pendingRpc.get(message.id);
|
|
1051
1135
|
if (!pending) return;
|
|
1052
1136
|
window.clearTimeout(pending.timeout);
|
|
@@ -1080,6 +1164,7 @@
|
|
|
1080
1164
|
const sessionChanged =
|
|
1081
1165
|
!state.session || state.session.sessionId !== session.sessionId;
|
|
1082
1166
|
if (sessionChanged) {
|
|
1167
|
+
state.sessionGeneration += 1;
|
|
1083
1168
|
abortActiveWork();
|
|
1084
1169
|
for (const item of state.items) revokePreviewUrl(item);
|
|
1085
1170
|
state.items = [];
|
|
@@ -1190,21 +1275,25 @@
|
|
|
1190
1275
|
!Array.isArray(state.toolInput.files)
|
|
1191
1276
|
)
|
|
1192
1277
|
return;
|
|
1278
|
+
const work = captureUploadWork();
|
|
1193
1279
|
state.bridgeConsumptionQueued = true;
|
|
1194
1280
|
void enqueueIngestion(async () => {
|
|
1281
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1195
1282
|
state.bridgeConsumptionQueued = false;
|
|
1196
|
-
await consumePendingBridgeFilesNow();
|
|
1283
|
+
await consumePendingBridgeFilesNow(work);
|
|
1197
1284
|
}).catch(() => {
|
|
1198
|
-
|
|
1285
|
+
if (uploadWorkIsCurrent(work))
|
|
1286
|
+
state.bridgeConsumptionQueued = false;
|
|
1199
1287
|
});
|
|
1200
1288
|
}
|
|
1201
1289
|
|
|
1202
|
-
async function consumePendingBridgeFilesNow() {
|
|
1290
|
+
async function consumePendingBridgeFilesNow(work) {
|
|
1203
1291
|
if (
|
|
1204
1292
|
state.disposed ||
|
|
1205
1293
|
!state.session ||
|
|
1206
1294
|
!Array.isArray(state.toolInput.files) ||
|
|
1207
|
-
state.consumingBridgeFiles
|
|
1295
|
+
state.consumingBridgeFiles ||
|
|
1296
|
+
!uploadWorkIsCurrent(work)
|
|
1208
1297
|
)
|
|
1209
1298
|
return;
|
|
1210
1299
|
state.consumingBridgeFiles = true;
|
|
@@ -1229,35 +1318,41 @@
|
|
|
1229
1318
|
for (const reference of pending.slice(0, availableSlots)) {
|
|
1230
1319
|
const id = normalizedBridgeReferenceId(reference);
|
|
1231
1320
|
try {
|
|
1232
|
-
const file = await downloadBridgeReference(reference);
|
|
1321
|
+
const file = await downloadBridgeReference(reference, work);
|
|
1322
|
+
requireCurrentUploadWork(work);
|
|
1233
1323
|
state.consumedBridgeIds.add(id);
|
|
1234
1324
|
files.push(file);
|
|
1235
1325
|
} catch {
|
|
1326
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1236
1327
|
state.consumedBridgeIds.add(id);
|
|
1237
1328
|
addBridgeFailure(reference);
|
|
1238
1329
|
}
|
|
1239
1330
|
}
|
|
1240
|
-
if (files.length) await processFiles(files);
|
|
1331
|
+
if (files.length) await processFiles(files, work);
|
|
1241
1332
|
} finally {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1333
|
+
if (uploadWorkIsCurrent(work)) {
|
|
1334
|
+
state.consumingBridgeFiles = false;
|
|
1335
|
+
const hasUnconsumed =
|
|
1336
|
+
Array.isArray(state.toolInput.files) &&
|
|
1337
|
+
state.toolInput.files.some((reference) => {
|
|
1338
|
+
const id = normalizedBridgeReferenceId(reference);
|
|
1339
|
+
return id && !state.consumedBridgeIds.has(id);
|
|
1340
|
+
});
|
|
1341
|
+
if (
|
|
1342
|
+
hasUnconsumed &&
|
|
1343
|
+
hasOpenAiFunction("getFileDownloadUrl") &&
|
|
1344
|
+
state.items.length < safeSessionLimit()
|
|
1345
|
+
) {
|
|
1346
|
+
queueMicrotask(() => void consumePendingBridgeFiles());
|
|
1347
|
+
}
|
|
1255
1348
|
}
|
|
1256
1349
|
}
|
|
1257
1350
|
}
|
|
1258
1351
|
|
|
1259
1352
|
function processLocalFiles(files) {
|
|
1353
|
+
const work = captureUploadWork();
|
|
1260
1354
|
return enqueueIngestion(async () => {
|
|
1355
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1261
1356
|
if (
|
|
1262
1357
|
state.disposed ||
|
|
1263
1358
|
!state.session ||
|
|
@@ -1271,12 +1366,14 @@
|
|
|
1271
1366
|
`Puedes subir hasta ${safeSessionLimit()} archivos por sesión.`,
|
|
1272
1367
|
);
|
|
1273
1368
|
}
|
|
1274
|
-
await processFiles(files);
|
|
1369
|
+
await processFiles(files, work);
|
|
1275
1370
|
});
|
|
1276
1371
|
}
|
|
1277
1372
|
|
|
1278
1373
|
function chooseLibraryFiles() {
|
|
1374
|
+
const work = captureUploadWork();
|
|
1279
1375
|
return enqueueIngestion(async () => {
|
|
1376
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1280
1377
|
if (
|
|
1281
1378
|
state.disposed ||
|
|
1282
1379
|
!state.session ||
|
|
@@ -1293,6 +1390,7 @@
|
|
|
1293
1390
|
return;
|
|
1294
1391
|
try {
|
|
1295
1392
|
const result = await bridge.selectFiles();
|
|
1393
|
+
requireCurrentUploadWork(work);
|
|
1296
1394
|
const selected = Array.isArray(result) ? result : [];
|
|
1297
1395
|
const availableSlots = Math.max(
|
|
1298
1396
|
0,
|
|
@@ -1307,18 +1405,23 @@
|
|
|
1307
1405
|
if (!fileId || !fileName || !mimeType) continue;
|
|
1308
1406
|
try {
|
|
1309
1407
|
files.push(
|
|
1310
|
-
await downloadBridgeReference(
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1408
|
+
await downloadBridgeReference(
|
|
1409
|
+
{
|
|
1410
|
+
fileId,
|
|
1411
|
+
fileName,
|
|
1412
|
+
mimeType,
|
|
1413
|
+
},
|
|
1414
|
+
work,
|
|
1415
|
+
),
|
|
1315
1416
|
);
|
|
1316
1417
|
} catch {
|
|
1418
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1317
1419
|
addBridgeFailure({ fileId, fileName, mimeType });
|
|
1318
1420
|
}
|
|
1319
1421
|
}
|
|
1320
|
-
await processFiles(files);
|
|
1422
|
+
await processFiles(files, work);
|
|
1321
1423
|
} catch {
|
|
1424
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1322
1425
|
setSessionStatus(
|
|
1323
1426
|
"No se pudo abrir el selector del host. Usa el selector del navegador.",
|
|
1324
1427
|
"warning",
|
|
@@ -1337,7 +1440,8 @@
|
|
|
1337
1440
|
);
|
|
1338
1441
|
}
|
|
1339
1442
|
|
|
1340
|
-
async function downloadBridgeReference(reference) {
|
|
1443
|
+
async function downloadBridgeReference(reference, work) {
|
|
1444
|
+
requireCurrentUploadWork(work);
|
|
1341
1445
|
const bridge = openAiBridge();
|
|
1342
1446
|
if (!bridge || typeof bridge.getFileDownloadUrl !== "function") {
|
|
1343
1447
|
throw new Error("bridge_download_unavailable");
|
|
@@ -1345,6 +1449,7 @@
|
|
|
1345
1449
|
const fileId = normalizedBridgeReferenceId(reference);
|
|
1346
1450
|
if (!fileId) throw new Error("invalid_bridge_file");
|
|
1347
1451
|
const resolved = await bridge.getFileDownloadUrl({ fileId });
|
|
1452
|
+
requireCurrentUploadWork(work);
|
|
1348
1453
|
const temporaryUrl = normalizedBridgeDownloadUrl(resolved);
|
|
1349
1454
|
const parsedUrl = new URL(temporaryUrl);
|
|
1350
1455
|
if (!safeBridgeDownloadUrl(parsedUrl))
|
|
@@ -1353,10 +1458,11 @@
|
|
|
1353
1458
|
method: "GET",
|
|
1354
1459
|
credentials: "omit",
|
|
1355
1460
|
redirect: "error",
|
|
1356
|
-
signal:
|
|
1461
|
+
signal: work.signal,
|
|
1357
1462
|
});
|
|
1358
1463
|
if (!response.ok) throw new Error("bridge_download_failed");
|
|
1359
1464
|
const blob = await readBoundedBridgeBlob(response);
|
|
1465
|
+
requireCurrentUploadWork(work);
|
|
1360
1466
|
const preferredType = boundedString(
|
|
1361
1467
|
reference.mime_type || reference.mimeType || reference.type,
|
|
1362
1468
|
100,
|
|
@@ -1449,13 +1555,14 @@
|
|
|
1449
1555
|
});
|
|
1450
1556
|
}
|
|
1451
1557
|
|
|
1452
|
-
async function processFiles(fileCandidates) {
|
|
1558
|
+
async function processFiles(fileCandidates, work) {
|
|
1453
1559
|
if (
|
|
1454
1560
|
state.disposed ||
|
|
1455
1561
|
!state.session ||
|
|
1456
1562
|
sessionExpired() ||
|
|
1457
1563
|
state.uploadBlocked ||
|
|
1458
|
-
state.busyBatch
|
|
1564
|
+
state.busyBatch ||
|
|
1565
|
+
!uploadWorkIsCurrent(work)
|
|
1459
1566
|
)
|
|
1460
1567
|
return;
|
|
1461
1568
|
const files = fileCandidates.filter(isBrowserFile);
|
|
@@ -1476,14 +1583,17 @@
|
|
|
1476
1583
|
state.busyBatch = true;
|
|
1477
1584
|
setPickerAvailability();
|
|
1478
1585
|
for (const file of selected) {
|
|
1586
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1479
1587
|
const mediaKind = mediaKindForType(file.type) || "image";
|
|
1480
1588
|
const item = createItem(file, mediaKind);
|
|
1589
|
+
item.work = work;
|
|
1481
1590
|
state.items.push(item);
|
|
1482
1591
|
renderItems();
|
|
1483
1592
|
// Thumbnail is cosmetic and must never delay or block the upload.
|
|
1484
1593
|
void upgradePreviewToDataUrl(item);
|
|
1485
|
-
await prepareAndUpload(item);
|
|
1594
|
+
await prepareAndUpload(item, work);
|
|
1486
1595
|
}
|
|
1596
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1487
1597
|
state.busyBatch = false;
|
|
1488
1598
|
setPickerAvailability();
|
|
1489
1599
|
if (state.items.some((item) => item.status === "uploaded")) {
|
|
@@ -1510,7 +1620,8 @@
|
|
|
1510
1620
|
};
|
|
1511
1621
|
}
|
|
1512
1622
|
|
|
1513
|
-
async function prepareAndUpload(item) {
|
|
1623
|
+
async function prepareAndUpload(item, work) {
|
|
1624
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1514
1625
|
if (!item.file) return;
|
|
1515
1626
|
item.status = "validating";
|
|
1516
1627
|
item.progress = 0;
|
|
@@ -1518,20 +1629,31 @@
|
|
|
1518
1629
|
item.failureStage = "";
|
|
1519
1630
|
renderItems();
|
|
1520
1631
|
try {
|
|
1521
|
-
const inspection = await inspectFile(item.file);
|
|
1632
|
+
const inspection = await inspectFile(item.file, work.session);
|
|
1633
|
+
requireCurrentUploadWork(work);
|
|
1522
1634
|
item.mediaKind = inspection.mediaKind;
|
|
1523
1635
|
item.uploadFile =
|
|
1524
1636
|
inspection.mediaKind === "image"
|
|
1525
1637
|
? await compressImage(item.file, inspection)
|
|
1526
1638
|
: item.file;
|
|
1639
|
+
requireCurrentUploadWork(work);
|
|
1527
1640
|
emitAnalytics("mcp_media_upload_started", item.mediaKind);
|
|
1528
|
-
await uploadWithRetries(item);
|
|
1641
|
+
await uploadWithRetries(item, work);
|
|
1642
|
+
requireCurrentUploadWork(work);
|
|
1529
1643
|
item.status = "uploaded";
|
|
1530
1644
|
item.progress = 100;
|
|
1531
1645
|
item.error = "";
|
|
1532
1646
|
item.slotReserved = false;
|
|
1533
1647
|
emitAnalytics("mcp_media_upload_completed", item.mediaKind);
|
|
1534
1648
|
} catch (error) {
|
|
1649
|
+
if (!uploadWorkIsCurrent(work)) {
|
|
1650
|
+
void releaseUploadSlot(item);
|
|
1651
|
+
emitAnalytics("mcp_media_upload_error", item.mediaKind, {
|
|
1652
|
+
failureStage: error?.safeStage || "upload",
|
|
1653
|
+
errorType: "upload_cancelled",
|
|
1654
|
+
});
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1535
1657
|
const failure = safeFailure(error);
|
|
1536
1658
|
item.status = "failed";
|
|
1537
1659
|
item.error = failure.message;
|
|
@@ -1562,7 +1684,7 @@
|
|
|
1562
1684
|
renderItems();
|
|
1563
1685
|
}
|
|
1564
1686
|
|
|
1565
|
-
async function inspectFile(file) {
|
|
1687
|
+
async function inspectFile(file, session) {
|
|
1566
1688
|
const mediaKind = mediaKindForType(file.type);
|
|
1567
1689
|
if (!ACCEPTED_TYPES.has(file.type) || !mediaKind) {
|
|
1568
1690
|
throw validationError(
|
|
@@ -1571,18 +1693,15 @@
|
|
|
1571
1693
|
);
|
|
1572
1694
|
}
|
|
1573
1695
|
if (
|
|
1574
|
-
|
|
1575
|
-
|
|
1696
|
+
session.mediaKind !== "mixed" &&
|
|
1697
|
+
session.mediaKind !== mediaKind
|
|
1576
1698
|
) {
|
|
1577
1699
|
throw validationError(
|
|
1578
1700
|
"wrong_media_kind",
|
|
1579
1701
|
"El archivo no coincide con el tipo solicitado.",
|
|
1580
1702
|
);
|
|
1581
1703
|
}
|
|
1582
|
-
if (
|
|
1583
|
-
state.session.assetRole === "thumbnail" &&
|
|
1584
|
-
mediaKind !== "image"
|
|
1585
|
-
) {
|
|
1704
|
+
if (session.assetRole === "thumbnail" && mediaKind !== "image") {
|
|
1586
1705
|
throw validationError(
|
|
1587
1706
|
"thumbnail_requires_image",
|
|
1588
1707
|
"La miniatura debe ser una imagen.",
|
|
@@ -1818,35 +1937,33 @@
|
|
|
1818
1937
|
});
|
|
1819
1938
|
}
|
|
1820
1939
|
|
|
1821
|
-
async function uploadWithRetries(item) {
|
|
1940
|
+
async function uploadWithRetries(item, work) {
|
|
1822
1941
|
let lastError = null;
|
|
1823
1942
|
for (let attempt = 1; attempt <= MAX_UPLOAD_ATTEMPTS; attempt += 1) {
|
|
1943
|
+
requireCurrentUploadWork(work);
|
|
1824
1944
|
item.attempts = attempt;
|
|
1825
1945
|
item.status = "uploading";
|
|
1826
1946
|
item.progress = 0;
|
|
1827
1947
|
renderItems();
|
|
1828
1948
|
try {
|
|
1829
|
-
const resolved = await resolveUpload(
|
|
1830
|
-
|
|
1831
|
-
item.uploadId,
|
|
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;
|
|
1949
|
+
const resolved = await resolveUpload(item, work);
|
|
1950
|
+
requireCurrentUploadWork(work);
|
|
1836
1951
|
await putFileWithProgress(
|
|
1837
1952
|
resolved.uploadUrl,
|
|
1838
1953
|
item.uploadFile,
|
|
1839
1954
|
(progress) => {
|
|
1955
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
1840
1956
|
item.progress = progress;
|
|
1841
1957
|
renderItems();
|
|
1842
1958
|
},
|
|
1843
1959
|
);
|
|
1960
|
+
requireCurrentUploadWork(work);
|
|
1844
1961
|
item.assetId = resolved.assetId;
|
|
1845
1962
|
return;
|
|
1846
1963
|
} catch (error) {
|
|
1847
1964
|
if (error && error.message === "session_expired") throw error;
|
|
1848
1965
|
if (error && error.permanent) throw error;
|
|
1849
|
-
if (
|
|
1966
|
+
if (!uploadWorkIsCurrent(work)) throw error;
|
|
1850
1967
|
lastError = error;
|
|
1851
1968
|
}
|
|
1852
1969
|
}
|
|
@@ -1863,17 +1980,8 @@
|
|
|
1863
1980
|
throw error;
|
|
1864
1981
|
}
|
|
1865
1982
|
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
-
*/
|
|
1983
|
+
// Deterministic API refusals stop immediately. Transport timeouts and
|
|
1984
|
+
// throttling remain retryable within the same bounded upload identity.
|
|
1877
1985
|
async function resolveFailure(response) {
|
|
1878
1986
|
const status = response.status;
|
|
1879
1987
|
const expired = status === 401 || status === 410;
|
|
@@ -1882,7 +1990,7 @@
|
|
|
1882
1990
|
);
|
|
1883
1991
|
error.safeCode = expired ? "session_expired" : "resolve_rejected";
|
|
1884
1992
|
error.safeStage = "resolve";
|
|
1885
|
-
error.permanent = status < 500;
|
|
1993
|
+
error.permanent = status < 500 && ![408, 425, 429].includes(status);
|
|
1886
1994
|
error.safeMessage = await resolveFailureMessage(response);
|
|
1887
1995
|
return error;
|
|
1888
1996
|
}
|
|
@@ -1928,12 +2036,15 @@
|
|
|
1928
2036
|
* still frees the slot on its own, so no error here may reach the user.
|
|
1929
2037
|
*/
|
|
1930
2038
|
async function releaseUploadSlot(item) {
|
|
1931
|
-
if (!item || !item.slotReserved || !
|
|
2039
|
+
if (!item || !item.slotReserved || !item.work) return;
|
|
1932
2040
|
item.slotReserved = false;
|
|
1933
2041
|
try {
|
|
1934
|
-
const uploadPage = new URL(
|
|
2042
|
+
const uploadPage = new URL(item.work.session.uploadPageUrl);
|
|
1935
2043
|
const releaseUrl = new URL(
|
|
1936
|
-
|
|
2044
|
+
item.work.session.resolveEndpoint.replace(
|
|
2045
|
+
/\/resolve$/,
|
|
2046
|
+
"/release",
|
|
2047
|
+
),
|
|
1937
2048
|
uploadPage.origin,
|
|
1938
2049
|
);
|
|
1939
2050
|
if (!safeReleaseEndpointUrl(releaseUrl)) return;
|
|
@@ -1953,10 +2064,11 @@
|
|
|
1953
2064
|
}
|
|
1954
2065
|
}
|
|
1955
2066
|
|
|
1956
|
-
async function resolveUpload(
|
|
1957
|
-
|
|
2067
|
+
async function resolveUpload(item, work) {
|
|
2068
|
+
requireCurrentUploadWork(work);
|
|
2069
|
+
const uploadPage = new URL(work.session.uploadPageUrl);
|
|
1958
2070
|
const resolveUrl = new URL(
|
|
1959
|
-
|
|
2071
|
+
work.session.resolveEndpoint,
|
|
1960
2072
|
uploadPage.origin,
|
|
1961
2073
|
);
|
|
1962
2074
|
if (
|
|
@@ -1977,26 +2089,41 @@
|
|
|
1977
2089
|
headers: { "content-type": "application/json" },
|
|
1978
2090
|
body: JSON.stringify({
|
|
1979
2091
|
token,
|
|
1980
|
-
uploadId,
|
|
1981
|
-
contentType:
|
|
1982
|
-
fileName:
|
|
1983
|
-
fileSize:
|
|
2092
|
+
uploadId: item.uploadId,
|
|
2093
|
+
contentType: item.uploadFile.type,
|
|
2094
|
+
fileName: item.uploadFile.name,
|
|
2095
|
+
fileSize: item.uploadFile.size,
|
|
1984
2096
|
}),
|
|
1985
2097
|
credentials: "omit",
|
|
1986
2098
|
redirect: "error",
|
|
1987
|
-
signal:
|
|
2099
|
+
signal: work.signal,
|
|
1988
2100
|
});
|
|
1989
2101
|
if (!response.ok) {
|
|
1990
2102
|
throw await resolveFailure(response);
|
|
1991
2103
|
}
|
|
2104
|
+
// A successful resolver response already owns a server reservation,
|
|
2105
|
+
// even if its body is invalid or this session was replaced meanwhile.
|
|
2106
|
+
// Track it before parsing so terminal cleanup releases the original slot.
|
|
2107
|
+
item.slotReserved = true;
|
|
2108
|
+
requireCurrentUploadWork(work);
|
|
1992
2109
|
const body = await response.json();
|
|
1993
2110
|
if (!isRecord(body)) throw new Error("invalid_resolve_response");
|
|
1994
|
-
const uploadUrl = boundedString(body.uploadUrl, 3000);
|
|
1995
2111
|
const assetId = boundedString(body.assetId, 300);
|
|
1996
|
-
|
|
1997
|
-
|
|
2112
|
+
let parsedUploadUrl;
|
|
2113
|
+
try {
|
|
2114
|
+
if (
|
|
2115
|
+
typeof body.uploadUrl !== "string" ||
|
|
2116
|
+
!body.uploadUrl ||
|
|
2117
|
+
body.uploadUrl.length > 3000
|
|
2118
|
+
)
|
|
2119
|
+
throw new Error("invalid_resolve_response");
|
|
2120
|
+
parsedUploadUrl = new URL(body.uploadUrl);
|
|
2121
|
+
if (!safeWebProtocol(parsedUploadUrl) || !assetId)
|
|
2122
|
+
throw new Error("invalid_resolve_response");
|
|
2123
|
+
} catch {
|
|
1998
2124
|
const error = new Error("invalid_resolve_response");
|
|
1999
2125
|
error.safeStage = "resolve";
|
|
2126
|
+
error.permanent = true;
|
|
2000
2127
|
throw error;
|
|
2001
2128
|
}
|
|
2002
2129
|
return { uploadUrl: parsedUploadUrl.toString(), assetId };
|
|
@@ -2140,6 +2267,9 @@
|
|
|
2140
2267
|
request.addEventListener("timeout", () =>
|
|
2141
2268
|
finish(() => reject(uploadTransportError("upload_timeout"))),
|
|
2142
2269
|
);
|
|
2270
|
+
request.addEventListener("abort", () =>
|
|
2271
|
+
finish(() => reject(uploadTransportError("upload_cancelled"))),
|
|
2272
|
+
);
|
|
2143
2273
|
request.send(file);
|
|
2144
2274
|
});
|
|
2145
2275
|
}
|
|
@@ -2183,8 +2313,10 @@
|
|
|
2183
2313
|
item.failureCode === "resolve_rejected"
|
|
2184
2314
|
)
|
|
2185
2315
|
return;
|
|
2316
|
+
const work = item.work;
|
|
2317
|
+
if (!uploadWorkIsCurrent(work)) return;
|
|
2186
2318
|
item.attempts = 0;
|
|
2187
|
-
await prepareAndUpload(item);
|
|
2319
|
+
await prepareAndUpload(item, work);
|
|
2188
2320
|
if (item.status === "uploaded") await refreshSessionStatus();
|
|
2189
2321
|
}
|
|
2190
2322
|
|
|
@@ -2245,6 +2377,7 @@
|
|
|
2245
2377
|
let source;
|
|
2246
2378
|
try {
|
|
2247
2379
|
source = await decodeImageSource(item.file);
|
|
2380
|
+
if (item.work && !uploadWorkIsCurrent(item.work)) return;
|
|
2248
2381
|
const { width, height } = sourceDimensions(source);
|
|
2249
2382
|
if (!validDimension(width) || !validDimension(height)) return;
|
|
2250
2383
|
const scale = Math.min(
|
|
@@ -2423,6 +2556,7 @@
|
|
|
2423
2556
|
async function refreshSessionStatus({ onMount = false } = {}) {
|
|
2424
2557
|
if (!state.session) return;
|
|
2425
2558
|
const requestedSessionId = state.session.sessionId;
|
|
2559
|
+
const requestedGeneration = state.sessionGeneration;
|
|
2426
2560
|
if (!onMount)
|
|
2427
2561
|
setSessionStatus("Comprobando el staging privado…", "neutral");
|
|
2428
2562
|
try {
|
|
@@ -2432,7 +2566,8 @@
|
|
|
2432
2566
|
if (
|
|
2433
2567
|
state.disposed ||
|
|
2434
2568
|
!state.session ||
|
|
2435
|
-
state.session.sessionId !== requestedSessionId
|
|
2569
|
+
state.session.sessionId !== requestedSessionId ||
|
|
2570
|
+
state.sessionGeneration !== requestedGeneration
|
|
2436
2571
|
)
|
|
2437
2572
|
return;
|
|
2438
2573
|
const status = parseStatusResult(result);
|
|
@@ -2476,7 +2611,13 @@
|
|
|
2476
2611
|
partial ? "warning" : "success",
|
|
2477
2612
|
);
|
|
2478
2613
|
} catch (error) {
|
|
2479
|
-
if (
|
|
2614
|
+
if (
|
|
2615
|
+
onMount ||
|
|
2616
|
+
state.disposed ||
|
|
2617
|
+
state.session?.sessionId !== requestedSessionId ||
|
|
2618
|
+
state.sessionGeneration !== requestedGeneration
|
|
2619
|
+
)
|
|
2620
|
+
return;
|
|
2480
2621
|
const expired =
|
|
2481
2622
|
sessionExpired() ||
|
|
2482
2623
|
(error && /expired|expirad/i.test(String(error.message)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "12.2.
|
|
3
|
+
"version": "12.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "ProductMaker MCP server — cost products, generate landing pages, UGC videos, and ad creatives, and publish to Shopify, Meta Ads, and TikTok Ads.",
|