@meuecommerce/frete-adapter-node 0.12.1 → 0.14.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.
@@ -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
- body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo }),
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
- // tipoRotulo is also accepted as a query param on the download (PPN-285).
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: { Accept: "application/pdf, application/json", Authorization: `Bearer ${token}` },
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
- if (response.status !== 200) {
113
- const body = await response.text().catch(() => "");
114
- // A retryable business message can arrive with a non-2xx status too.
115
- if (isRetryableLabelMessage(body))
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.length ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
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
- // 200 without a PDF carries a business message (e.g. "status Pendente" or
130
- // "ainda não foi gerado, consulte novamente"). Retryable ones keep polling.
131
- const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
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
  };
@@ -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-11.responses.v4-indice";
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,49 @@ function descreverProduto(p, i) {
80
80
  .join("\n");
81
81
  return `${linha}\n${contexto}`;
82
82
  }
83
- function createPrompt(products, boxes, samples) {
83
+ /**
84
+ * Uma URL de foto só é aceita se for utilizável como está.
85
+ *
86
+ * `http(s)` e `data:` são o que a API aceita. Qualquer outra coisa — caminho
87
+ * relativo, `wix:image://`, string vazia — faria a requisição inteira ser
88
+ * rejeitada, e com ela os cinco produtos do lote, não só o da foto ruim.
89
+ * Descartar a foto e estimar pelo texto é sempre melhor que perder o lote.
90
+ */
91
+ function fotoUtilizavel(url) {
92
+ if (!url)
93
+ return false;
94
+ return /^https?:\/\//i.test(url) || /^data:image\//i.test(url);
95
+ }
96
+ /**
97
+ * As fotos do lote, na ordem dos produtos, com o índice a que cada uma pertence.
98
+ *
99
+ * **A ordem é o único vínculo entre foto e produto.** A API recebe uma lista de
100
+ * imagens sem rótulo: se os produtos 2 e 4 têm foto e os outros não, o modelo vê
101
+ * duas imagens soltas e nada diz de quem são. Por isso o prompt declara a
102
+ * correspondência explicitamente (ver {@link descreverFotos}) e esta função
103
+ * preserva a ordem — trocar as duas coisas de lugar faria o modelo descrever a
104
+ * caneca olhando para o tapete, e a resposta voltaria com cara de certa.
105
+ */
106
+ function fotosDoLote(products) {
107
+ return products
108
+ .map((p, i) => ({ indice: i + 1, url: p.imageUrl }))
109
+ .filter((f) => fotoUtilizavel(f.url));
110
+ }
111
+ /** A linha do prompt que amarra cada foto ao produto dela. */
112
+ function descreverFotos(fotos, total) {
113
+ if (fotos.length === 0)
114
+ return "";
115
+ const mapa = fotos.map((f, i) => `a ${i + 1}ª é do produto ${f.indice}`).join(", ");
116
+ const parcial = fotos.length < total
117
+ ? ` Os outros ${total - fotos.length} produtos não têm foto — para eles vale só o texto.`
118
+ : "";
119
+ return (`\n📷 FOTOS (${fotos.length}, nesta ordem): ${mapa}.${parcial}\n` +
120
+ `Use a foto sobretudo para os traços do campo attrs — se cede, se empilha, se precisa ir em pé, ` +
121
+ `se quebra. Para isso ela vale mais que o nome.\n` +
122
+ `Para os centímetros, desconfie dela: foto sem referência de escala não dá medida. ` +
123
+ `Peso e nome continuam mandando no tamanho; a foto ajusta a forma.\n`);
124
+ }
125
+ function createPrompt(products, boxes, samples, fotos = []) {
84
126
  const box = largestBox(boxes);
85
127
  const maxWidth = Math.floor(box.width * 0.8);
86
128
  const maxHeight = Math.floor(box.height * 0.8);
@@ -92,6 +134,7 @@ function createPrompt(products, boxes, samples) {
92
134
  return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
93
135
  `🚨 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
136
  `📦 PRODUTOS PARA ESTIMAR:\n${products.map(descreverProduto).join("\n")}\n` +
137
+ descreverFotos(fotos, products.length) +
95
138
  samplesSection +
96
139
  `\n🧱 COMO O PRODUTO OCUPA ESPAÇO (campo attrs, um por produto):\n` +
97
140
  `- fragile: quebra se levar peso em cima — vidro, cerâmica, garrafa, ovo\n` +
@@ -169,11 +212,13 @@ function createOpenAIDimensionEstimator(options) {
169
212
  async estimate(products, boxes, productSamples = []) {
170
213
  if (!boxes.length)
171
214
  throw new Error("estimate: no boxes provided");
215
+ const fotos = fotosDoLote(products);
172
216
  const result = await agent.run({
173
217
  task: "estimate-dimensions",
174
218
  promptVersion: exports.DIMENSION_PROMPT_VERSION,
175
219
  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),
220
+ user: createPrompt(products, boxes, productSamples, fotos),
221
+ ...(fotos.length ? { images: fotos.map((f) => f.url) } : {}),
177
222
  schema: { name: "product_dimensions", schema: DIMENSIONS_SCHEMA },
178
223
  temperature,
179
224
  });
@@ -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
- body: JSON.stringify({ idsPrePostagem: ids, tipoRotulo }),
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
- // tipoRotulo is also accepted as a query param on the download (PPN-285).
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: { Accept: "application/pdf, application/json", Authorization: `Bearer ${token}` },
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
- if (response.status !== 200) {
110
- const body = await response.text().catch(() => "");
111
- // A retryable business message can arrive with a non-2xx status too.
112
- if (isRetryableLabelMessage(body))
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.length ? { ready: true, pdfBase64: buf.toString("base64") } : { ready: false };
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
- // 200 without a PDF carries a business message (e.g. "status Pendente" or
127
- // "ainda não foi gerado, consulte novamente"). Retryable ones keep polling.
128
- const message = typeof json.mensagem === "string" ? json.mensagem : undefined;
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
  };
@@ -25,7 +25,7 @@ type FetchLike = typeof globalThis.fetch;
25
25
  * `L3` record so a prompt change can invalidate stale estimates in background
26
26
  * instead of at checkout.
27
27
  */
28
- export declare const DIMENSION_PROMPT_VERSION = "2026-09-11.responses.v4-indice";
28
+ export declare const DIMENSION_PROMPT_VERSION = "2026-09-15.responses.v5-foto";
29
29
  export interface OpenAIDimensionEstimatorOptions {
30
30
  apiKey?: string;
31
31
  fetch?: FetchLike;
@@ -5,7 +5,7 @@ import { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.j
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-11.responses.v4-indice";
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,49 @@ function descreverProduto(p, i) {
76
76
  .join("\n");
77
77
  return `${linha}\n${contexto}`;
78
78
  }
79
- function createPrompt(products, boxes, samples) {
79
+ /**
80
+ * Uma URL de foto só é aceita se for utilizável como está.
81
+ *
82
+ * `http(s)` e `data:` são o que a API aceita. Qualquer outra coisa — caminho
83
+ * relativo, `wix:image://`, string vazia — faria a requisição inteira ser
84
+ * rejeitada, e com ela os cinco produtos do lote, não só o da foto ruim.
85
+ * Descartar a foto e estimar pelo texto é sempre melhor que perder o lote.
86
+ */
87
+ function fotoUtilizavel(url) {
88
+ if (!url)
89
+ return false;
90
+ return /^https?:\/\//i.test(url) || /^data:image\//i.test(url);
91
+ }
92
+ /**
93
+ * As fotos do lote, na ordem dos produtos, com o índice a que cada uma pertence.
94
+ *
95
+ * **A ordem é o único vínculo entre foto e produto.** A API recebe uma lista de
96
+ * imagens sem rótulo: se os produtos 2 e 4 têm foto e os outros não, o modelo vê
97
+ * duas imagens soltas e nada diz de quem são. Por isso o prompt declara a
98
+ * correspondência explicitamente (ver {@link descreverFotos}) e esta função
99
+ * preserva a ordem — trocar as duas coisas de lugar faria o modelo descrever a
100
+ * caneca olhando para o tapete, e a resposta voltaria com cara de certa.
101
+ */
102
+ function fotosDoLote(products) {
103
+ return products
104
+ .map((p, i) => ({ indice: i + 1, url: p.imageUrl }))
105
+ .filter((f) => fotoUtilizavel(f.url));
106
+ }
107
+ /** A linha do prompt que amarra cada foto ao produto dela. */
108
+ function descreverFotos(fotos, total) {
109
+ if (fotos.length === 0)
110
+ return "";
111
+ const mapa = fotos.map((f, i) => `a ${i + 1}ª é do produto ${f.indice}`).join(", ");
112
+ const parcial = fotos.length < total
113
+ ? ` Os outros ${total - fotos.length} produtos não têm foto — para eles vale só o texto.`
114
+ : "";
115
+ return (`\n📷 FOTOS (${fotos.length}, nesta ordem): ${mapa}.${parcial}\n` +
116
+ `Use a foto sobretudo para os traços do campo attrs — se cede, se empilha, se precisa ir em pé, ` +
117
+ `se quebra. Para isso ela vale mais que o nome.\n` +
118
+ `Para os centímetros, desconfie dela: foto sem referência de escala não dá medida. ` +
119
+ `Peso e nome continuam mandando no tamanho; a foto ajusta a forma.\n`);
120
+ }
121
+ function createPrompt(products, boxes, samples, fotos = []) {
80
122
  const box = largestBox(boxes);
81
123
  const maxWidth = Math.floor(box.width * 0.8);
82
124
  const maxHeight = Math.floor(box.height * 0.8);
@@ -88,6 +130,7 @@ function createPrompt(products, boxes, samples) {
88
130
  return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
89
131
  `🚨 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
132
  `📦 PRODUTOS PARA ESTIMAR:\n${products.map(descreverProduto).join("\n")}\n` +
133
+ descreverFotos(fotos, products.length) +
91
134
  samplesSection +
92
135
  `\n🧱 COMO O PRODUTO OCUPA ESPAÇO (campo attrs, um por produto):\n` +
93
136
  `- fragile: quebra se levar peso em cima — vidro, cerâmica, garrafa, ovo\n` +
@@ -165,11 +208,13 @@ export function createOpenAIDimensionEstimator(options) {
165
208
  async estimate(products, boxes, productSamples = []) {
166
209
  if (!boxes.length)
167
210
  throw new Error("estimate: no boxes provided");
211
+ const fotos = fotosDoLote(products);
168
212
  const result = await agent.run({
169
213
  task: "estimate-dimensions",
170
214
  promptVersion: DIMENSION_PROMPT_VERSION,
171
215
  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),
216
+ user: createPrompt(products, boxes, productSamples, fotos),
217
+ ...(fotos.length ? { images: fotos.map((f) => f.url) } : {}),
173
218
  schema: { name: "product_dimensions", schema: DIMENSIONS_SCHEMA },
174
219
  temperature,
175
220
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meuecommerce/frete-adapter-node",
3
- "version": "0.12.1",
3
+ "version": "0.14.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.12.1"
34
+ "@meuecommerce/frete": "^0.14.0"
35
35
  },
36
36
  "module": "./dist/index.js",
37
37
  "repository": {