@meuecommerce/frete-adapter-node 0.13.0 → 0.15.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/cjs/correiosLabelHttpClient.js +45 -23
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/openAIDimensionEstimator.js +37 -4
- package/dist/cjs/openAIResponsesAgentRunner.js +30 -4
- package/dist/correiosLabelHttpClient.d.ts +2 -0
- package/dist/correiosLabelHttpClient.js +45 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/openAIDimensionEstimator.d.ts +11 -1
- package/dist/openAIDimensionEstimator.js +38 -5
- package/dist/openAIResponsesAgentRunner.d.ts +29 -0
- package/dist/openAIResponsesAgentRunner.js +29 -4
- package/package.json +2 -2
|
@@ -30,8 +30,23 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
30
30
|
function isRetryableLabelMessage(msg) {
|
|
31
31
|
return /consulte novamente|ainda n[ãa]o foi gerado|n[ãa]o (foi )?gerado|PPN-291/i.test(msg);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A well-formed PDF starts with the `%PDF-` header and ends with the `%%EOF`
|
|
35
|
+
* trailer. Correios can answer a mid-render poll with a *partial* byte stream
|
|
36
|
+
* (correct content-type, non-empty body) — accepting that yields the corrupted
|
|
37
|
+
* label users saw. The Wix/Shopify apps sidestep it by reading the base64 from
|
|
38
|
+
* JSON (`dados`), which only appears once the render is complete; when we do get
|
|
39
|
+
* raw bytes we apply the same "only if complete" rule here.
|
|
40
|
+
*/
|
|
41
|
+
function isCompletePdf(buf) {
|
|
42
|
+
if (buf.length < 100)
|
|
43
|
+
return false;
|
|
44
|
+
if (buf.subarray(0, 5).toString("latin1") !== "%PDF-")
|
|
45
|
+
return false;
|
|
46
|
+
return buf.subarray(Math.max(0, buf.length - 1024)).toString("latin1").includes("%%EOF");
|
|
47
|
+
}
|
|
33
48
|
function createCorreiosLabelHttpClient(options = {}) {
|
|
34
|
-
const { fetch = globalThis.fetch, baseUrl = frete_1.correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
|
|
49
|
+
const { fetch = globalThis.fetch, baseUrl = frete_1.correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", formatoRotulo = "EN", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
|
|
35
50
|
if (typeof fetch !== "function") {
|
|
36
51
|
throw new Error("createCorreiosLabelHttpClient: no fetch available (Node 18+ or pass options.fetch)");
|
|
37
52
|
}
|
|
@@ -89,7 +104,8 @@ function createCorreiosLabelHttpClient(options = {}) {
|
|
|
89
104
|
const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens/rotulo/assincrono/pdf`, {
|
|
90
105
|
method: "POST",
|
|
91
106
|
headers: authHeaders(token),
|
|
92
|
-
|
|
107
|
+
// formatoRotulo mirrors the Wix/Shopify apps — Correios expects it.
|
|
108
|
+
body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo, formatoRotulo }),
|
|
93
109
|
});
|
|
94
110
|
const json = (await response.json().catch(() => ({})));
|
|
95
111
|
if (!json.idRecibo)
|
|
@@ -97,40 +113,46 @@ function createCorreiosLabelHttpClient(options = {}) {
|
|
|
97
113
|
return { idRecibo: json.idRecibo };
|
|
98
114
|
},
|
|
99
115
|
async downloadLabelPdf(idRecibo, token) {
|
|
116
|
+
// Mirror the Wix/Shopify apps: plain GET asking for JSON, and the base64
|
|
117
|
+
// PDF is read from `dados`. Reading JSON (not raw bytes) means we only ever
|
|
118
|
+
// accept a *complete* PDF — a mid-render poll simply has no `dados` yet, so
|
|
119
|
+
// we keep polling instead of handing back a corrupted file.
|
|
100
120
|
const path = downloadPath.replace("{idRecibo}", encodeURIComponent(idRecibo));
|
|
101
|
-
|
|
102
|
-
const sep = path.includes("?") ? "&" : "?";
|
|
103
|
-
const url = `${baseUrl}${path}${sep}tipoRotulo=${encodeURIComponent(tipoRotulo)}`;
|
|
104
|
-
const response = await fetchWithRetry(url, {
|
|
121
|
+
const response = await fetchWithRetry(`${baseUrl}${path}`, {
|
|
105
122
|
method: "GET",
|
|
106
|
-
headers:
|
|
123
|
+
headers: authHeaders(token),
|
|
107
124
|
});
|
|
108
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
109
125
|
// 202 (or 204) = still processing; tell the use case to poll again.
|
|
110
126
|
if (response.status === 202 || response.status === 204)
|
|
111
127
|
return { ready: false };
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return { ready: false };
|
|
117
|
-
return { ready: false, message: `http_${response.status}: ${body.slice(0, 200)}` };
|
|
118
|
-
}
|
|
119
|
-
// Correios may return the PDF as raw bytes (application/pdf) or wrapped in
|
|
120
|
-
// JSON as base64 (dados/pdf). Handle both.
|
|
128
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
129
|
+
// Correios occasionally answers with raw bytes anyway — accept them only if
|
|
130
|
+
// they form a complete PDF (%PDF header + %%EOF trailer); otherwise it's a
|
|
131
|
+
// half-rendered stream, so treat it as not-ready and keep polling.
|
|
121
132
|
if (contentType.includes("application/pdf") || contentType.includes("octet-stream")) {
|
|
122
133
|
const buf = Buffer.from(await response.arrayBuffer());
|
|
123
|
-
return buf
|
|
134
|
+
return isCompletePdf(buf) ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
|
|
135
|
+
}
|
|
136
|
+
// JSON path (the happy path the apps use).
|
|
137
|
+
const body = await response.text().catch(() => "");
|
|
138
|
+
let json = {};
|
|
139
|
+
try {
|
|
140
|
+
json = body ? JSON.parse(body) : {};
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
json = {};
|
|
124
144
|
}
|
|
125
|
-
const json = (await response.json().catch(() => ({})));
|
|
126
145
|
const pdfBase64 = (json.dados ?? json.pdf ?? json.base64 ?? json.arquivo);
|
|
127
146
|
if (pdfBase64)
|
|
128
147
|
return { ready: true, pdfBase64 };
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
if (message && isRetryableLabelMessage(message))
|
|
148
|
+
// No PDF yet. A transient message (PPN-291 / "consulte novamente") → keep
|
|
149
|
+
// polling; a terminal one (e.g. "status Pendente") → surface it.
|
|
150
|
+
if (isRetryableLabelMessage(body))
|
|
133
151
|
return { ready: false };
|
|
152
|
+
const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
|
|
153
|
+
if (response.status !== 200) {
|
|
154
|
+
return { ready: false, message: message ?? `http_${response.status}: ${body.slice(0, 200)}` };
|
|
155
|
+
}
|
|
134
156
|
return message ? { ready: false, message } : { ready: false };
|
|
135
157
|
},
|
|
136
158
|
};
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createOpenAIBoxDistributor = exports.PgDimensionMemory = exports.DIMENSION_MEMORY_SCHEMA_SQL = exports.createOpenAIResponsesAgentRunner = exports.createOpenAIDimensionEstimator = exports.DIMENSION_PROMPT_VERSION = exports.EnvSecretStore = exports.InMemoryDimensionMemory = exports.InMemorySettingsStore = exports.createCorreiosLabelHttpClient = exports.createCorreiosHttpClient = void 0;
|
|
3
|
+
exports.createOpenAIBoxDistributor = exports.PgDimensionMemory = exports.DIMENSION_MEMORY_SCHEMA_SQL = exports.createOpenAIResponsesAgentRunner = exports.DEFAULT_IMAGE_DETAIL = exports.createOpenAIDimensionEstimator = exports.DIMENSION_PROMPT_VERSION = exports.EnvSecretStore = exports.InMemoryDimensionMemory = exports.InMemorySettingsStore = exports.createCorreiosLabelHttpClient = exports.createCorreiosHttpClient = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @meuecommerce/frete-adapter-node — Node implementations of @meuecommerce/frete ports.
|
|
6
6
|
*
|
|
@@ -21,6 +21,7 @@ var openAIDimensionEstimator_js_1 = require("./openAIDimensionEstimator.js");
|
|
|
21
21
|
Object.defineProperty(exports, "DIMENSION_PROMPT_VERSION", { enumerable: true, get: function () { return openAIDimensionEstimator_js_1.DIMENSION_PROMPT_VERSION; } });
|
|
22
22
|
Object.defineProperty(exports, "createOpenAIDimensionEstimator", { enumerable: true, get: function () { return openAIDimensionEstimator_js_1.createOpenAIDimensionEstimator; } });
|
|
23
23
|
var openAIResponsesAgentRunner_js_1 = require("./openAIResponsesAgentRunner.js");
|
|
24
|
+
Object.defineProperty(exports, "DEFAULT_IMAGE_DETAIL", { enumerable: true, get: function () { return openAIResponsesAgentRunner_js_1.DEFAULT_IMAGE_DETAIL; } });
|
|
24
25
|
Object.defineProperty(exports, "createOpenAIResponsesAgentRunner", { enumerable: true, get: function () { return openAIResponsesAgentRunner_js_1.createOpenAIResponsesAgentRunner; } });
|
|
25
26
|
var pgDimensionMemory_js_1 = require("./pgDimensionMemory.js");
|
|
26
27
|
Object.defineProperty(exports, "DIMENSION_MEMORY_SCHEMA_SQL", { enumerable: true, get: function () { return pgDimensionMemory_js_1.DIMENSION_MEMORY_SCHEMA_SQL; } });
|
|
@@ -9,7 +9,7 @@ const openAIResponsesAgentRunner_js_1 = require("./openAIResponsesAgentRunner.js
|
|
|
9
9
|
* `L3` record so a prompt change can invalidate stale estimates in background
|
|
10
10
|
* instead of at checkout.
|
|
11
11
|
*/
|
|
12
|
-
exports.DIMENSION_PROMPT_VERSION = "2026-09-
|
|
12
|
+
exports.DIMENSION_PROMPT_VERSION = "2026-09-15.responses.v5-foto";
|
|
13
13
|
/**
|
|
14
14
|
* Strict JSON schema for the response.
|
|
15
15
|
*
|
|
@@ -80,7 +80,36 @@ function descreverProduto(p, i) {
|
|
|
80
80
|
.join("\n");
|
|
81
81
|
return `${linha}\n${contexto}`;
|
|
82
82
|
}
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* As fotos do lote, na ordem dos produtos, com o índice a que cada uma pertence.
|
|
85
|
+
*
|
|
86
|
+
* **A ordem é o único vínculo entre foto e produto.** A API recebe uma lista de
|
|
87
|
+
* imagens sem rótulo: se os produtos 2 e 4 têm foto e os outros não, o modelo vê
|
|
88
|
+
* duas imagens soltas e nada diz de quem são. Por isso o prompt declara a
|
|
89
|
+
* correspondência explicitamente (ver {@link descreverFotos}) e esta função
|
|
90
|
+
* preserva a ordem — trocar as duas coisas de lugar faria o modelo descrever a
|
|
91
|
+
* caneca olhando para o tapete, e a resposta voltaria com cara de certa.
|
|
92
|
+
*/
|
|
93
|
+
function fotosDoLote(products) {
|
|
94
|
+
return products
|
|
95
|
+
.map((p, i) => ({ indice: i + 1, url: p.imageUrl }))
|
|
96
|
+
.filter((f) => (0, frete_1.usablePhotoUrl)(f.url));
|
|
97
|
+
}
|
|
98
|
+
/** A linha do prompt que amarra cada foto ao produto dela. */
|
|
99
|
+
function descreverFotos(fotos, total) {
|
|
100
|
+
if (fotos.length === 0)
|
|
101
|
+
return "";
|
|
102
|
+
const mapa = fotos.map((f, i) => `a ${i + 1}ª é do produto ${f.indice}`).join(", ");
|
|
103
|
+
const parcial = fotos.length < total
|
|
104
|
+
? ` Os outros ${total - fotos.length} produtos não têm foto — para eles vale só o texto.`
|
|
105
|
+
: "";
|
|
106
|
+
return (`\n📷 FOTOS (${fotos.length}, nesta ordem): ${mapa}.${parcial}\n` +
|
|
107
|
+
`Use a foto sobretudo para os traços do campo attrs — se cede, se empilha, se precisa ir em pé, ` +
|
|
108
|
+
`se quebra. Para isso ela vale mais que o nome.\n` +
|
|
109
|
+
`Para os centímetros, desconfie dela: foto sem referência de escala não dá medida. ` +
|
|
110
|
+
`Peso e nome continuam mandando no tamanho; a foto ajusta a forma.\n`);
|
|
111
|
+
}
|
|
112
|
+
function createPrompt(products, boxes, samples, fotos = []) {
|
|
84
113
|
const box = largestBox(boxes);
|
|
85
114
|
const maxWidth = Math.floor(box.width * 0.8);
|
|
86
115
|
const maxHeight = Math.floor(box.height * 0.8);
|
|
@@ -92,6 +121,7 @@ function createPrompt(products, boxes, samples) {
|
|
|
92
121
|
return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
|
|
93
122
|
`🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
|
|
94
123
|
`📦 PRODUTOS PARA ESTIMAR:\n${products.map(descreverProduto).join("\n")}\n` +
|
|
124
|
+
descreverFotos(fotos, products.length) +
|
|
95
125
|
samplesSection +
|
|
96
126
|
`\n🧱 COMO O PRODUTO OCUPA ESPAÇO (campo attrs, um por produto):\n` +
|
|
97
127
|
`- fragile: quebra se levar peso em cima — vidro, cerâmica, garrafa, ovo\n` +
|
|
@@ -156,7 +186,7 @@ function validateAndOptimize(aiResult, products, boxes) {
|
|
|
156
186
|
return { products: optimized };
|
|
157
187
|
}
|
|
158
188
|
function createOpenAIDimensionEstimator(options) {
|
|
159
|
-
const { apiKey, fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl, timeoutMs, runner } = options;
|
|
189
|
+
const { apiKey, fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl, timeoutMs, imageDetail, runner } = options;
|
|
160
190
|
const agent = runner ??
|
|
161
191
|
(0, openAIResponsesAgentRunner_js_1.createOpenAIResponsesAgentRunner)({
|
|
162
192
|
apiKey: apiKey ?? "",
|
|
@@ -164,16 +194,19 @@ function createOpenAIDimensionEstimator(options) {
|
|
|
164
194
|
model,
|
|
165
195
|
...(apiUrl ? { apiUrl } : {}),
|
|
166
196
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
197
|
+
...(imageDetail ? { imageDetail } : {}),
|
|
167
198
|
});
|
|
168
199
|
return {
|
|
169
200
|
async estimate(products, boxes, productSamples = []) {
|
|
170
201
|
if (!boxes.length)
|
|
171
202
|
throw new Error("estimate: no boxes provided");
|
|
203
|
+
const fotos = fotosDoLote(products);
|
|
172
204
|
const result = await agent.run({
|
|
173
205
|
task: "estimate-dimensions",
|
|
174
206
|
promptVersion: exports.DIMENSION_PROMPT_VERSION,
|
|
175
207
|
system: "Você é um especialista em dimensões de produtos. Use os exemplos personalizados como prioridade máxima quando o nome bater.",
|
|
176
|
-
user: createPrompt(products, boxes, productSamples),
|
|
208
|
+
user: createPrompt(products, boxes, productSamples, fotos),
|
|
209
|
+
...(fotos.length ? { images: fotos.map((f) => f.url) } : {}),
|
|
177
210
|
schema: { name: "product_dimensions", schema: DIMENSIONS_SCHEMA },
|
|
178
211
|
temperature,
|
|
179
212
|
});
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_IMAGE_DETAIL = void 0;
|
|
3
4
|
exports.extractOutputText = extractOutputText;
|
|
4
5
|
exports.findRefusal = findRefusal;
|
|
5
6
|
exports.createOpenAIResponsesAgentRunner = createOpenAIResponsesAgentRunner;
|
|
7
|
+
/**
|
|
8
|
+
* `low` de propósito, e é uma escolha de custo medida, não um palpite.
|
|
9
|
+
*
|
|
10
|
+
* No gpt-4o-mini a imagem é cobrada por ladrilho de 512px: 2.833 de base mais
|
|
11
|
+
* 5.667 por ladrilho. Uma foto de produto quadrada, que é o formato padrão da
|
|
12
|
+
* Wix, vira quatro ladrilhos — 25.501 tokens. Com `low` a conta é fixa em 2.833
|
|
13
|
+
* **independentemente do tamanho do original**, o que dá 9x menos na parte da
|
|
14
|
+
* imagem.
|
|
15
|
+
*
|
|
16
|
+
* Isso saiu de produção, não de estimativa: o primeiro lote com foto, em
|
|
17
|
+
* 2026-09-15, gastou 180.113 tokens de entrada para 7 produtos. A fórmula
|
|
18
|
+
* acima prevê 180.119.
|
|
19
|
+
*
|
|
20
|
+
* **O que ainda não foi medido é a qualidade.** Não existe dataset de eval com
|
|
21
|
+
* foto — o `amostras-lojistas.json` tem nome e dimensões digitadas pelo
|
|
22
|
+
* lojista, nada de imagem — então qual `detail` estima melhor é pergunta em
|
|
23
|
+
* aberto. Por isso isto é opção com padrão, e não constante: quando o dataset
|
|
24
|
+
* com foto existir, comparar os dois é trocar este argumento.
|
|
25
|
+
*/
|
|
26
|
+
exports.DEFAULT_IMAGE_DETAIL = "low";
|
|
6
27
|
/** The model's text, accepting both shapes the Responses API delivers it in. */
|
|
7
28
|
function extractOutputText(json) {
|
|
8
29
|
if (typeof json.output_text === "string" && json.output_text.trim())
|
|
@@ -31,7 +52,7 @@ function findRefusal(json) {
|
|
|
31
52
|
return null;
|
|
32
53
|
}
|
|
33
54
|
function createOpenAIResponsesAgentRunner(options) {
|
|
34
|
-
const { apiKey, fetch = globalThis.fetch, model: defaultModel = "gpt-4o-mini", apiUrl = "https://api.openai.com/v1/responses", timeoutMs = 20000, } = options;
|
|
55
|
+
const { apiKey, fetch = globalThis.fetch, model: defaultModel = "gpt-4o-mini", apiUrl = "https://api.openai.com/v1/responses", timeoutMs = 20000, imageDetail = exports.DEFAULT_IMAGE_DETAIL, } = options;
|
|
35
56
|
if (!apiKey)
|
|
36
57
|
throw new Error("createOpenAIResponsesAgentRunner: apiKey is required");
|
|
37
58
|
if (typeof fetch !== "function")
|
|
@@ -41,14 +62,19 @@ function createOpenAIResponsesAgentRunner(options) {
|
|
|
41
62
|
const input = [];
|
|
42
63
|
if (request.system)
|
|
43
64
|
input.push({ role: "system", content: request.system });
|
|
44
|
-
//
|
|
45
|
-
//
|
|
65
|
+
// Com imagem o turno do usuário vira lista em vez de string. O `detail`
|
|
66
|
+
// vai sempre explícito: omiti-lo é o que deixa a OpenAI escolher `high`,
|
|
67
|
+
// e essa omissão custou 9x na primeira passada em produção.
|
|
46
68
|
if (request.images?.length) {
|
|
47
69
|
input.push({
|
|
48
70
|
role: "user",
|
|
49
71
|
content: [
|
|
50
72
|
{ type: "input_text", text: request.user },
|
|
51
|
-
...request.images.map((url) => ({
|
|
73
|
+
...request.images.map((url) => ({
|
|
74
|
+
type: "input_image",
|
|
75
|
+
image_url: url,
|
|
76
|
+
detail: imageDetail,
|
|
77
|
+
})),
|
|
52
78
|
],
|
|
53
79
|
});
|
|
54
80
|
}
|
|
@@ -30,6 +30,8 @@ export interface CorreiosLabelHttpClientOptions {
|
|
|
30
30
|
retryDelayMs?: number;
|
|
31
31
|
/** Label size sent to Correios: "P" padrão or "R" reduzido. Default "P". */
|
|
32
32
|
tipoRotulo?: string;
|
|
33
|
+
/** Label layout sent to Correios: "EN" etiqueta or "ET" etiqueta+recibo. Default "EN". */
|
|
34
|
+
formatoRotulo?: string;
|
|
33
35
|
/**
|
|
34
36
|
* Download endpoint template; `{idRecibo}` is substituted. Default follows the
|
|
35
37
|
* Correios async-download convention. Override if homolog differs.
|
|
@@ -27,8 +27,23 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
27
27
|
function isRetryableLabelMessage(msg) {
|
|
28
28
|
return /consulte novamente|ainda n[ãa]o foi gerado|n[ãa]o (foi )?gerado|PPN-291/i.test(msg);
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* A well-formed PDF starts with the `%PDF-` header and ends with the `%%EOF`
|
|
32
|
+
* trailer. Correios can answer a mid-render poll with a *partial* byte stream
|
|
33
|
+
* (correct content-type, non-empty body) — accepting that yields the corrupted
|
|
34
|
+
* label users saw. The Wix/Shopify apps sidestep it by reading the base64 from
|
|
35
|
+
* JSON (`dados`), which only appears once the render is complete; when we do get
|
|
36
|
+
* raw bytes we apply the same "only if complete" rule here.
|
|
37
|
+
*/
|
|
38
|
+
function isCompletePdf(buf) {
|
|
39
|
+
if (buf.length < 100)
|
|
40
|
+
return false;
|
|
41
|
+
if (buf.subarray(0, 5).toString("latin1") !== "%PDF-")
|
|
42
|
+
return false;
|
|
43
|
+
return buf.subarray(Math.max(0, buf.length - 1024)).toString("latin1").includes("%%EOF");
|
|
44
|
+
}
|
|
30
45
|
export function createCorreiosLabelHttpClient(options = {}) {
|
|
31
|
-
const { fetch = globalThis.fetch, baseUrl = correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
|
|
46
|
+
const { fetch = globalThis.fetch, baseUrl = correiosApiUrl, timeoutMs = 20000, retries = 2, retryDelayMs = 500, tipoRotulo = "P", formatoRotulo = "EN", downloadPath = "/prepostagem/v1/prepostagens/rotulo/download/assincrono/{idRecibo}", logger = console, } = options;
|
|
32
47
|
if (typeof fetch !== "function") {
|
|
33
48
|
throw new Error("createCorreiosLabelHttpClient: no fetch available (Node 18+ or pass options.fetch)");
|
|
34
49
|
}
|
|
@@ -86,7 +101,8 @@ export function createCorreiosLabelHttpClient(options = {}) {
|
|
|
86
101
|
const response = await fetchWithRetry(`${baseUrl}/prepostagem/v1/prepostagens/rotulo/assincrono/pdf`, {
|
|
87
102
|
method: "POST",
|
|
88
103
|
headers: authHeaders(token),
|
|
89
|
-
|
|
104
|
+
// formatoRotulo mirrors the Wix/Shopify apps — Correios expects it.
|
|
105
|
+
body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo, formatoRotulo }),
|
|
90
106
|
});
|
|
91
107
|
const json = (await response.json().catch(() => ({})));
|
|
92
108
|
if (!json.idRecibo)
|
|
@@ -94,40 +110,46 @@ export function createCorreiosLabelHttpClient(options = {}) {
|
|
|
94
110
|
return { idRecibo: json.idRecibo };
|
|
95
111
|
},
|
|
96
112
|
async downloadLabelPdf(idRecibo, token) {
|
|
113
|
+
// Mirror the Wix/Shopify apps: plain GET asking for JSON, and the base64
|
|
114
|
+
// PDF is read from `dados`. Reading JSON (not raw bytes) means we only ever
|
|
115
|
+
// accept a *complete* PDF — a mid-render poll simply has no `dados` yet, so
|
|
116
|
+
// we keep polling instead of handing back a corrupted file.
|
|
97
117
|
const path = downloadPath.replace("{idRecibo}", encodeURIComponent(idRecibo));
|
|
98
|
-
|
|
99
|
-
const sep = path.includes("?") ? "&" : "?";
|
|
100
|
-
const url = `${baseUrl}${path}${sep}tipoRotulo=${encodeURIComponent(tipoRotulo)}`;
|
|
101
|
-
const response = await fetchWithRetry(url, {
|
|
118
|
+
const response = await fetchWithRetry(`${baseUrl}${path}`, {
|
|
102
119
|
method: "GET",
|
|
103
|
-
headers:
|
|
120
|
+
headers: authHeaders(token),
|
|
104
121
|
});
|
|
105
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
106
122
|
// 202 (or 204) = still processing; tell the use case to poll again.
|
|
107
123
|
if (response.status === 202 || response.status === 204)
|
|
108
124
|
return { ready: false };
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { ready: false };
|
|
114
|
-
return { ready: false, message: `http_${response.status}: ${body.slice(0, 200)}` };
|
|
115
|
-
}
|
|
116
|
-
// Correios may return the PDF as raw bytes (application/pdf) or wrapped in
|
|
117
|
-
// JSON as base64 (dados/pdf). Handle both.
|
|
125
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
126
|
+
// Correios occasionally answers with raw bytes anyway — accept them only if
|
|
127
|
+
// they form a complete PDF (%PDF header + %%EOF trailer); otherwise it's a
|
|
128
|
+
// half-rendered stream, so treat it as not-ready and keep polling.
|
|
118
129
|
if (contentType.includes("application/pdf") || contentType.includes("octet-stream")) {
|
|
119
130
|
const buf = Buffer.from(await response.arrayBuffer());
|
|
120
|
-
return buf
|
|
131
|
+
return isCompletePdf(buf) ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
|
|
132
|
+
}
|
|
133
|
+
// JSON path (the happy path the apps use).
|
|
134
|
+
const body = await response.text().catch(() => "");
|
|
135
|
+
let json = {};
|
|
136
|
+
try {
|
|
137
|
+
json = body ? JSON.parse(body) : {};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
json = {};
|
|
121
141
|
}
|
|
122
|
-
const json = (await response.json().catch(() => ({})));
|
|
123
142
|
const pdfBase64 = (json.dados ?? json.pdf ?? json.base64 ?? json.arquivo);
|
|
124
143
|
if (pdfBase64)
|
|
125
144
|
return { ready: true, pdfBase64 };
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
if (message && isRetryableLabelMessage(message))
|
|
145
|
+
// No PDF yet. A transient message (PPN-291 / "consulte novamente") → keep
|
|
146
|
+
// polling; a terminal one (e.g. "status Pendente") → surface it.
|
|
147
|
+
if (isRetryableLabelMessage(body))
|
|
130
148
|
return { ready: false };
|
|
149
|
+
const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
|
|
150
|
+
if (response.status !== 200) {
|
|
151
|
+
return { ready: false, message: message ?? `http_${response.status}: ${body.slice(0, 200)}` };
|
|
152
|
+
}
|
|
131
153
|
return message ? { ready: false, message } : { ready: false };
|
|
132
154
|
},
|
|
133
155
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -14,10 +14,10 @@ export { InMemoryDimensionMemory } from "./inMemoryDimensionMemory.js";
|
|
|
14
14
|
export { EnvSecretStore } from "./envSecretStore.js";
|
|
15
15
|
export type { EnvSecretStoreOptions } from "./envSecretStore.js";
|
|
16
16
|
export { DIMENSION_PROMPT_VERSION, createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
17
|
-
export { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
17
|
+
export { DEFAULT_IMAGE_DETAIL, createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
18
18
|
export { DIMENSION_MEMORY_SCHEMA_SQL, PgDimensionMemory } from "./pgDimensionMemory.js";
|
|
19
19
|
export type { PgDimensionMemoryOptions, SqlClient } from "./pgDimensionMemory.js";
|
|
20
|
-
export type { OpenAIResponsesAgentRunnerOptions } from "./openAIResponsesAgentRunner.js";
|
|
20
|
+
export type { ImageDetail, OpenAIResponsesAgentRunnerOptions } from "./openAIResponsesAgentRunner.js";
|
|
21
21
|
export type { OpenAIDimensionEstimatorOptions } from "./openAIDimensionEstimator.js";
|
|
22
22
|
export { createOpenAIBoxDistributor } from "./openAIBoxDistributor.js";
|
|
23
23
|
export type { OpenAIBoxDistributorOptions } from "./openAIBoxDistributor.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,6 @@ export { InMemorySettingsStore } from "./inMemorySettingsStore.js";
|
|
|
10
10
|
export { InMemoryDimensionMemory } from "./inMemoryDimensionMemory.js";
|
|
11
11
|
export { EnvSecretStore } from "./envSecretStore.js";
|
|
12
12
|
export { DIMENSION_PROMPT_VERSION, createOpenAIDimensionEstimator } from "./openAIDimensionEstimator.js";
|
|
13
|
-
export { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
13
|
+
export { DEFAULT_IMAGE_DETAIL, createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
14
14
|
export { DIMENSION_MEMORY_SCHEMA_SQL, PgDimensionMemory } from "./pgDimensionMemory.js";
|
|
15
15
|
export { createOpenAIBoxDistributor } from "./openAIBoxDistributor.js";
|
|
@@ -19,13 +19,14 @@
|
|
|
19
19
|
* (e.g. via a `SecretStore`), keeping secret handling out of this module.
|
|
20
20
|
*/
|
|
21
21
|
import type { LlmAgentRunner, ProductDimensionEstimator } from "@meuecommerce/frete";
|
|
22
|
+
import { type ImageDetail } from "./openAIResponsesAgentRunner.js";
|
|
22
23
|
type FetchLike = typeof globalThis.fetch;
|
|
23
24
|
/**
|
|
24
25
|
* Bump whenever the prompt or the schema changes. It is written next to every
|
|
25
26
|
* `L3` record so a prompt change can invalidate stale estimates in background
|
|
26
27
|
* instead of at checkout.
|
|
27
28
|
*/
|
|
28
|
-
export declare const DIMENSION_PROMPT_VERSION = "2026-09-
|
|
29
|
+
export declare const DIMENSION_PROMPT_VERSION = "2026-09-15.responses.v5-foto";
|
|
29
30
|
export interface OpenAIDimensionEstimatorOptions {
|
|
30
31
|
apiKey?: string;
|
|
31
32
|
fetch?: FetchLike;
|
|
@@ -33,6 +34,15 @@ export interface OpenAIDimensionEstimatorOptions {
|
|
|
33
34
|
temperature?: number;
|
|
34
35
|
apiUrl?: string;
|
|
35
36
|
timeoutMs?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Quanto da imagem o modelo olha. Omitido, vale o padrão do runner (`low`).
|
|
39
|
+
*
|
|
40
|
+
* Existe aqui, e não só no runner, para que comparar `low` contra `high` num
|
|
41
|
+
* eval seja trocar um argumento — que é como a pergunta em aberto sobre
|
|
42
|
+
* qualidade vai ser respondida quando houver dataset com foto. Não tem efeito
|
|
43
|
+
* nenhum sobre produto sem foto.
|
|
44
|
+
*/
|
|
45
|
+
imageDetail?: ImageDetail;
|
|
36
46
|
/**
|
|
37
47
|
* Run the call through an existing {@link LlmAgentRunner} instead of building
|
|
38
48
|
* one. Pass it to share a runner across use cases, or to substitute the
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { productNameKey } from "@meuecommerce/frete";
|
|
1
|
+
import { productNameKey, usablePhotoUrl } from "@meuecommerce/frete";
|
|
2
2
|
import { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
|
|
3
3
|
/**
|
|
4
4
|
* Bump whenever the prompt or the schema changes. It is written next to every
|
|
5
5
|
* `L3` record so a prompt change can invalidate stale estimates in background
|
|
6
6
|
* instead of at checkout.
|
|
7
7
|
*/
|
|
8
|
-
export const DIMENSION_PROMPT_VERSION = "2026-09-
|
|
8
|
+
export const DIMENSION_PROMPT_VERSION = "2026-09-15.responses.v5-foto";
|
|
9
9
|
/**
|
|
10
10
|
* Strict JSON schema for the response.
|
|
11
11
|
*
|
|
@@ -76,7 +76,36 @@ function descreverProduto(p, i) {
|
|
|
76
76
|
.join("\n");
|
|
77
77
|
return `${linha}\n${contexto}`;
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* As fotos do lote, na ordem dos produtos, com o índice a que cada uma pertence.
|
|
81
|
+
*
|
|
82
|
+
* **A ordem é o único vínculo entre foto e produto.** A API recebe uma lista de
|
|
83
|
+
* imagens sem rótulo: se os produtos 2 e 4 têm foto e os outros não, o modelo vê
|
|
84
|
+
* duas imagens soltas e nada diz de quem são. Por isso o prompt declara a
|
|
85
|
+
* correspondência explicitamente (ver {@link descreverFotos}) e esta função
|
|
86
|
+
* preserva a ordem — trocar as duas coisas de lugar faria o modelo descrever a
|
|
87
|
+
* caneca olhando para o tapete, e a resposta voltaria com cara de certa.
|
|
88
|
+
*/
|
|
89
|
+
function fotosDoLote(products) {
|
|
90
|
+
return products
|
|
91
|
+
.map((p, i) => ({ indice: i + 1, url: p.imageUrl }))
|
|
92
|
+
.filter((f) => usablePhotoUrl(f.url));
|
|
93
|
+
}
|
|
94
|
+
/** A linha do prompt que amarra cada foto ao produto dela. */
|
|
95
|
+
function descreverFotos(fotos, total) {
|
|
96
|
+
if (fotos.length === 0)
|
|
97
|
+
return "";
|
|
98
|
+
const mapa = fotos.map((f, i) => `a ${i + 1}ª é do produto ${f.indice}`).join(", ");
|
|
99
|
+
const parcial = fotos.length < total
|
|
100
|
+
? ` Os outros ${total - fotos.length} produtos não têm foto — para eles vale só o texto.`
|
|
101
|
+
: "";
|
|
102
|
+
return (`\n📷 FOTOS (${fotos.length}, nesta ordem): ${mapa}.${parcial}\n` +
|
|
103
|
+
`Use a foto sobretudo para os traços do campo attrs — se cede, se empilha, se precisa ir em pé, ` +
|
|
104
|
+
`se quebra. Para isso ela vale mais que o nome.\n` +
|
|
105
|
+
`Para os centímetros, desconfie dela: foto sem referência de escala não dá medida. ` +
|
|
106
|
+
`Peso e nome continuam mandando no tamanho; a foto ajusta a forma.\n`);
|
|
107
|
+
}
|
|
108
|
+
function createPrompt(products, boxes, samples, fotos = []) {
|
|
80
109
|
const box = largestBox(boxes);
|
|
81
110
|
const maxWidth = Math.floor(box.width * 0.8);
|
|
82
111
|
const maxHeight = Math.floor(box.height * 0.8);
|
|
@@ -88,6 +117,7 @@ function createPrompt(products, boxes, samples) {
|
|
|
88
117
|
return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
|
|
89
118
|
`🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
|
|
90
119
|
`📦 PRODUTOS PARA ESTIMAR:\n${products.map(descreverProduto).join("\n")}\n` +
|
|
120
|
+
descreverFotos(fotos, products.length) +
|
|
91
121
|
samplesSection +
|
|
92
122
|
`\n🧱 COMO O PRODUTO OCUPA ESPAÇO (campo attrs, um por produto):\n` +
|
|
93
123
|
`- fragile: quebra se levar peso em cima — vidro, cerâmica, garrafa, ovo\n` +
|
|
@@ -152,7 +182,7 @@ function validateAndOptimize(aiResult, products, boxes) {
|
|
|
152
182
|
return { products: optimized };
|
|
153
183
|
}
|
|
154
184
|
export function createOpenAIDimensionEstimator(options) {
|
|
155
|
-
const { apiKey, fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl, timeoutMs, runner } = options;
|
|
185
|
+
const { apiKey, fetch, model = "gpt-4o-mini", temperature = 0.1, apiUrl, timeoutMs, imageDetail, runner } = options;
|
|
156
186
|
const agent = runner ??
|
|
157
187
|
createOpenAIResponsesAgentRunner({
|
|
158
188
|
apiKey: apiKey ?? "",
|
|
@@ -160,16 +190,19 @@ export function createOpenAIDimensionEstimator(options) {
|
|
|
160
190
|
model,
|
|
161
191
|
...(apiUrl ? { apiUrl } : {}),
|
|
162
192
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
193
|
+
...(imageDetail ? { imageDetail } : {}),
|
|
163
194
|
});
|
|
164
195
|
return {
|
|
165
196
|
async estimate(products, boxes, productSamples = []) {
|
|
166
197
|
if (!boxes.length)
|
|
167
198
|
throw new Error("estimate: no boxes provided");
|
|
199
|
+
const fotos = fotosDoLote(products);
|
|
168
200
|
const result = await agent.run({
|
|
169
201
|
task: "estimate-dimensions",
|
|
170
202
|
promptVersion: DIMENSION_PROMPT_VERSION,
|
|
171
203
|
system: "Você é um especialista em dimensões de produtos. Use os exemplos personalizados como prioridade máxima quando o nome bater.",
|
|
172
|
-
user: createPrompt(products, boxes, productSamples),
|
|
204
|
+
user: createPrompt(products, boxes, productSamples, fotos),
|
|
205
|
+
...(fotos.length ? { images: fotos.map((f) => f.url) } : {}),
|
|
173
206
|
schema: { name: "product_dimensions", schema: DIMENSIONS_SCHEMA },
|
|
174
207
|
temperature,
|
|
175
208
|
});
|
|
@@ -12,12 +12,41 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { LlmAgentRunner } from "@meuecommerce/frete";
|
|
14
14
|
type FetchLike = typeof globalThis.fetch;
|
|
15
|
+
/**
|
|
16
|
+
* Quanto da imagem o modelo olha. Ver {@link DEFAULT_IMAGE_DETAIL}.
|
|
17
|
+
*
|
|
18
|
+
* `auto` é o padrão da OpenAI quando o campo não vai, e para foto de produto
|
|
19
|
+
* ele resolve como `high` — foi assim que o custo apareceu em produção.
|
|
20
|
+
*/
|
|
21
|
+
export type ImageDetail = "low" | "high" | "auto";
|
|
22
|
+
/**
|
|
23
|
+
* `low` de propósito, e é uma escolha de custo medida, não um palpite.
|
|
24
|
+
*
|
|
25
|
+
* No gpt-4o-mini a imagem é cobrada por ladrilho de 512px: 2.833 de base mais
|
|
26
|
+
* 5.667 por ladrilho. Uma foto de produto quadrada, que é o formato padrão da
|
|
27
|
+
* Wix, vira quatro ladrilhos — 25.501 tokens. Com `low` a conta é fixa em 2.833
|
|
28
|
+
* **independentemente do tamanho do original**, o que dá 9x menos na parte da
|
|
29
|
+
* imagem.
|
|
30
|
+
*
|
|
31
|
+
* Isso saiu de produção, não de estimativa: o primeiro lote com foto, em
|
|
32
|
+
* 2026-09-15, gastou 180.113 tokens de entrada para 7 produtos. A fórmula
|
|
33
|
+
* acima prevê 180.119.
|
|
34
|
+
*
|
|
35
|
+
* **O que ainda não foi medido é a qualidade.** Não existe dataset de eval com
|
|
36
|
+
* foto — o `amostras-lojistas.json` tem nome e dimensões digitadas pelo
|
|
37
|
+
* lojista, nada de imagem — então qual `detail` estima melhor é pergunta em
|
|
38
|
+
* aberto. Por isso isto é opção com padrão, e não constante: quando o dataset
|
|
39
|
+
* com foto existir, comparar os dois é trocar este argumento.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DEFAULT_IMAGE_DETAIL: ImageDetail;
|
|
15
42
|
export interface OpenAIResponsesAgentRunnerOptions {
|
|
16
43
|
apiKey: string;
|
|
17
44
|
fetch?: FetchLike;
|
|
18
45
|
model?: string;
|
|
19
46
|
apiUrl?: string;
|
|
20
47
|
timeoutMs?: number;
|
|
48
|
+
/** Ver {@link DEFAULT_IMAGE_DETAIL}. */
|
|
49
|
+
imageDetail?: ImageDetail;
|
|
21
50
|
}
|
|
22
51
|
interface ResponsesPayload {
|
|
23
52
|
error?: {
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `low` de propósito, e é uma escolha de custo medida, não um palpite.
|
|
3
|
+
*
|
|
4
|
+
* No gpt-4o-mini a imagem é cobrada por ladrilho de 512px: 2.833 de base mais
|
|
5
|
+
* 5.667 por ladrilho. Uma foto de produto quadrada, que é o formato padrão da
|
|
6
|
+
* Wix, vira quatro ladrilhos — 25.501 tokens. Com `low` a conta é fixa em 2.833
|
|
7
|
+
* **independentemente do tamanho do original**, o que dá 9x menos na parte da
|
|
8
|
+
* imagem.
|
|
9
|
+
*
|
|
10
|
+
* Isso saiu de produção, não de estimativa: o primeiro lote com foto, em
|
|
11
|
+
* 2026-09-15, gastou 180.113 tokens de entrada para 7 produtos. A fórmula
|
|
12
|
+
* acima prevê 180.119.
|
|
13
|
+
*
|
|
14
|
+
* **O que ainda não foi medido é a qualidade.** Não existe dataset de eval com
|
|
15
|
+
* foto — o `amostras-lojistas.json` tem nome e dimensões digitadas pelo
|
|
16
|
+
* lojista, nada de imagem — então qual `detail` estima melhor é pergunta em
|
|
17
|
+
* aberto. Por isso isto é opção com padrão, e não constante: quando o dataset
|
|
18
|
+
* com foto existir, comparar os dois é trocar este argumento.
|
|
19
|
+
*/
|
|
20
|
+
export const DEFAULT_IMAGE_DETAIL = "low";
|
|
1
21
|
/** The model's text, accepting both shapes the Responses API delivers it in. */
|
|
2
22
|
export function extractOutputText(json) {
|
|
3
23
|
if (typeof json.output_text === "string" && json.output_text.trim())
|
|
@@ -26,7 +46,7 @@ export function findRefusal(json) {
|
|
|
26
46
|
return null;
|
|
27
47
|
}
|
|
28
48
|
export function createOpenAIResponsesAgentRunner(options) {
|
|
29
|
-
const { apiKey, fetch = globalThis.fetch, model: defaultModel = "gpt-4o-mini", apiUrl = "https://api.openai.com/v1/responses", timeoutMs = 20000, } = options;
|
|
49
|
+
const { apiKey, fetch = globalThis.fetch, model: defaultModel = "gpt-4o-mini", apiUrl = "https://api.openai.com/v1/responses", timeoutMs = 20000, imageDetail = DEFAULT_IMAGE_DETAIL, } = options;
|
|
30
50
|
if (!apiKey)
|
|
31
51
|
throw new Error("createOpenAIResponsesAgentRunner: apiKey is required");
|
|
32
52
|
if (typeof fetch !== "function")
|
|
@@ -36,14 +56,19 @@ export function createOpenAIResponsesAgentRunner(options) {
|
|
|
36
56
|
const input = [];
|
|
37
57
|
if (request.system)
|
|
38
58
|
input.push({ role: "system", content: request.system });
|
|
39
|
-
//
|
|
40
|
-
//
|
|
59
|
+
// Com imagem o turno do usuário vira lista em vez de string. O `detail`
|
|
60
|
+
// vai sempre explícito: omiti-lo é o que deixa a OpenAI escolher `high`,
|
|
61
|
+
// e essa omissão custou 9x na primeira passada em produção.
|
|
41
62
|
if (request.images?.length) {
|
|
42
63
|
input.push({
|
|
43
64
|
role: "user",
|
|
44
65
|
content: [
|
|
45
66
|
{ type: "input_text", text: request.user },
|
|
46
|
-
...request.images.map((url) => ({
|
|
67
|
+
...request.images.map((url) => ({
|
|
68
|
+
type: "input_image",
|
|
69
|
+
image_url: url,
|
|
70
|
+
detail: imageDetail,
|
|
71
|
+
})),
|
|
47
72
|
],
|
|
48
73
|
});
|
|
49
74
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meuecommerce/frete-adapter-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Node implementation of @meuecommerce/frete ports (Correios HTTP client) using global fetch. Used by the Shopify/Fly deployment and the MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@meuecommerce/frete": "^0.
|
|
34
|
+
"@meuecommerce/frete": "^0.15.0"
|
|
35
35
|
},
|
|
36
36
|
"module": "./dist/index.js",
|
|
37
37
|
"repository": {
|