receipt-ocr 1.0.2 → 1.0.4

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 CHANGED
@@ -58,10 +58,12 @@ console.log(receipt);
58
58
  // hasTax: true,
59
59
  // taxAmount: 0.35,
60
60
  // deposit: 0.5, // 押金已自动合并
61
- // discount: -0.5 // 折扣已自动合并
61
+ // discount: 0.5 // 折扣已自动合并(存储为正数)
62
62
  // },
63
63
  // ...
64
64
  // ],
65
+ // subtotal: 94.5, // 如果小票上有显示
66
+ // totalTax: 1.25, // 如果小票上有显示
65
67
  // total: 95.75
66
68
  // }
67
69
  ```
@@ -73,6 +75,8 @@ console.log(receipt);
73
75
  ```typescript
74
76
  interface ReceiptData {
75
77
  items: ReceiptItem[]; // 商品列表
78
+ subtotal?: number; // 小计金额(可选 - 如果小票上有 SUBTOTAL 行)
79
+ totalTax?: number; // 税费总额(可选 - 如果小票上有 TAX 行)
76
80
  total: number; // 小票总金额
77
81
  }
78
82
  ```
@@ -89,7 +93,7 @@ interface ReceiptItem {
89
93
  hasTax: boolean; // 是否含税
90
94
  taxAmount?: number; // 税额(可选)
91
95
  deposit?: number; // 押金(可选,自动合并)
92
- discount?: number; // 折扣(可选,自动合并)
96
+ discount?: number; // 折扣(可选,自动合并,存储为正数)
93
97
  }
94
98
  ```
95
99
 
@@ -98,7 +102,7 @@ interface ReceiptItem {
98
102
  库会自动识别并合并押金(Deposit)和折扣(TPD)到对应的商品中,而不是作为独立的商品项返回:
99
103
 
100
104
  - **押金(deposit)**:如 "Deposit VL",会被合并到对应的瓶装商品中
101
- - **折扣(discount)**:如 "TPD",会被合并到对应的商品中(通常为负数)
105
+ - **折扣(discount)**:如 "TPD",会被合并到对应的商品中(存储为正数,如 0.5 表示减免 0.5 元)
102
106
 
103
107
  这意味着您不需要手动处理这些附加费用,它们会自动关联到正确的商品上。
104
108
 
@@ -183,7 +187,7 @@ type VerificationCallback = (
183
187
  ) => Promise<{ verifiedName: string } | null>;
184
188
  ```
185
189
 
186
- ### 访问总金额
190
+ ### 访问小票数据
187
191
 
188
192
  ```typescript
189
193
  const receipt = await extractReceiptItems(imageBuffer);
@@ -193,7 +197,13 @@ receipt.items.forEach(item => {
193
197
  console.log(`${item.name}: ¥${item.price} × ${item.quantity}`);
194
198
  });
195
199
 
196
- // 访问总金额
200
+ // 访问金额汇总
201
+ if (receipt.subtotal) {
202
+ console.log(`小计: ¥${receipt.subtotal}`);
203
+ }
204
+ if (receipt.totalTax) {
205
+ console.log(`税费: ¥${receipt.totalTax}`);
206
+ }
197
207
  console.log(`总计: ¥${receipt.total}`);
198
208
  ```
199
209
 
package/dist/index.cjs CHANGED
@@ -264,6 +264,10 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
264
264
  \u5BF9\u4E8E\u62BC\u91D1\uFF08Deposit\u3001deposit\u3001\u62BC\u91D1\u7B49\uFF09\u548C\u6298\u6263\uFF08TPD\u3001discount\u3001\u6298\u6263\u7B49\uFF09\u8FD9\u7C7B\u9644\u52A0\u8D39\u7528\uFF1A
265
265
  - \u6DFB\u52A0\u989D\u5916\u5B57\u6BB5 isAttachment: true
266
266
  - \u6DFB\u52A0 attachmentType: "deposit" \u6216 "discount"
267
+ - **\u4EF7\u683C\u683C\u5F0F\u8981\u6C42**\uFF1A
268
+ - \u62BC\u91D1\u7684 price \u5B57\u6BB5\u5FC5\u987B\u662F**\u6B63\u6570**\uFF08\u5982 0.5\uFF09
269
+ - \u6298\u6263\u7684 price \u5B57\u6BB5\u4E5F\u5FC5\u987B\u662F**\u6B63\u6570**\uFF08\u5982 0.5\uFF0C\u800C\u4E0D\u662F -0.5\uFF09
270
+ - \u7CFB\u7EDF\u4F1A\u81EA\u52A8\u5C06\u6298\u6263\u91D1\u989D\u5904\u7406\u4E3A\u8D1F\u503C\uFF0C\u4F60\u53EA\u9700\u8F93\u51FA\u6298\u6263\u7684\u7EDD\u5BF9\u503C
267
271
  - **\u91CD\u8981**\uFF1A\u5C06\u9644\u52A0\u8D39\u7528\u7D27\u8DDF\u5728\u5B83\u6240\u5C5E\u7684\u5546\u54C1\u540E\u9762\u6392\u5217
268
272
  - \u7CFB\u7EDF\u4F1A\u81EA\u52A8\u5C06\u9644\u52A0\u8D39\u7528\u5408\u5E76\u5230\u5B83\u524D\u9762\u7684\u5546\u54C1\u4E2D
269
273
  - \u8FD9\u4E9B\u9644\u52A0\u8D39\u7528\u4E0D\u4F1A\u4F5C\u4E3A\u72EC\u7ACB\u5546\u54C1\u8FD4\u56DE
@@ -281,7 +285,11 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
281
285
  - \u63D0\u53D6\u5BF9\u5E94\u7684\u91D1\u989D\u6570\u5B57
282
286
  - \u8FD9\u662F\u5C0F\u7968\u7684\u6700\u7EC8\u5E94\u4ED8\u91D1\u989D
283
287
 
284
- \u53EA\u8FD4\u56DE JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u5176\u4ED6\u6587\u5B57\u3002
288
+ **\u8F93\u51FA\u683C\u5F0F\u8981\u6C42\uFF08\u6781\u5176\u91CD\u8981\uFF09**\uFF1A
289
+ 1. \u53EA\u8FD4\u56DE JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3001\u89E3\u91CA\u6216markdown\u6807\u8BB0
290
+ 2. \u6240\u6709 price \u5B57\u6BB5\u5FC5\u987B\u662F**\u6B63\u6570**\uFF0C\u5305\u62EC\u6298\u6263\u9879
291
+ 3. \u4E25\u683C\u9075\u5FAA\u4E0A\u8FF0\u5B57\u6BB5\u5B9A\u4E49\uFF0C\u4E0D\u8981\u6DFB\u52A0\u989D\u5916\u5B57\u6BB5
292
+ 4. \u786E\u4FDD JSON \u683C\u5F0F\u6B63\u786E\uFF0C\u53EF\u4EE5\u88AB\u76F4\u63A5\u89E3\u6790
285
293
 
286
294
  \u793A\u4F8B\u8F93\u51FA\uFF1A
287
295
  \u5047\u8BBE\u5C0F\u7968\u4E0A\u663E\u793A\uFF1A
@@ -297,7 +305,7 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
297
305
  {"name": "KS ORG MLK 1L", "price": 12.5, "quantity": 1, "needsVerification": true, "hasTax": false},
298
306
  {"name": "ORG BRD", "price": 8.0, "quantity": 1, "needsVerification": true, "hasTax": true, "taxAmount": 0.8},
299
307
  {"name": "Deposit VL", "price": 0.5, "quantity": 2, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "deposit"},
300
- {"name": "TPD", "price": -0.5, "quantity": 1, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "discount"},
308
+ {"name": "TPD", "price": 0.5, "quantity": 1, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "discount"},
301
309
  {"name": "CEM\u039F\u0399 6\u03A7", "price": 15.0, "quantity": 1, "needsVerification": true, "hasTax": true},
302
310
  {"name": "KS Apple", "price": 4.5, "quantity": 3, "needsVerification": true, "hasTax": false}
303
311
  ],
@@ -306,7 +314,9 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
306
314
 
307
315
  \u6CE8\u610F\uFF1A
308
316
  1. \u5546\u54C1\u540D\u79F0\u4E2D\u5DF2\u53BB\u6389 "H" \u6807\u8BB0\uFF0C\u4F46\u6839\u636E\u539F\u5C0F\u7968\u4E0A\u7684 "H" \u6807\u8BB0\u8BBE\u7F6E\u4E86\u6B63\u786E\u7684 hasTax \u503C
309
- 2. total \u662F\u5C0F\u7968\u4E0A\u663E\u793A\u7684\u6700\u7EC8\u5E94\u4ED8\u91D1\u989D`;
317
+ 2. TPD \u6298\u6263\u9879\u7684 price \u662F**\u6B63\u6570 0.5**\uFF08\u4E0D\u662F -0.5\uFF09\uFF0C\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u5904\u7406\u4E3A\u8D1F\u503C
318
+ 3. \u6240\u6709\u9644\u52A0\u8D39\u7528\uFF08\u62BC\u91D1\u3001\u6298\u6263\uFF09\u7684 price \u90FD\u5FC5\u987B\u662F\u6B63\u6570
319
+ 4. total \u662F\u5C0F\u7968\u4E0A\u663E\u793A\u7684\u6700\u7EC8\u5E94\u4ED8\u91D1\u989D`;
310
320
 
311
321
  // src/processors/parser.ts
312
322
  function extractJson(text) {
@@ -357,7 +367,7 @@ function mergeAttachments(items) {
357
367
  if (item.attachmentType === "deposit") {
358
368
  currentItem.deposit = (currentItem.deposit || 0) + item.price * item.quantity;
359
369
  } else if (item.attachmentType === "discount") {
360
- currentItem.discount = (currentItem.discount || 0) + item.price;
370
+ currentItem.discount = (currentItem.discount || 0) - item.price;
361
371
  }
362
372
  }
363
373
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/processors/image.ts","../src/adapters/gemini.ts","../src/adapters/verifier.ts","../src/utils/prompt.ts","../src/processors/parser.ts","../src/extract.ts"],"names":["GoogleGenerativeAI","getGeminiConfig","result","item"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ImageInput } from '../types.js';\r\n\r\n/**\r\n * 图片数据接口,用于传递给 Gemini API\r\n */\r\nexport interface ProcessedImage {\r\n /** 图片的 MIME 类型 */\r\n mimeType: string;\r\n /** base64 编码的图片数据(不包含 data URI 前缀) */\r\n data: string;\r\n}\r\n\r\n/**\r\n * 检测是否为 URL\r\n */\r\nfunction isUrl(input: string): boolean {\r\n return input.startsWith('http://') || input.startsWith('https://');\r\n}\r\n\r\n/**\r\n * 检测是否为 base64 字符串\r\n */\r\nfunction isBase64(input: string): boolean {\r\n // 简单检测:如果以 data: 开头,或者看起来像 base64\r\n if (input.startsWith('data:')) {\r\n return true;\r\n }\r\n // Base64 字符串通常很长,且只包含特定字符\r\n return input.length > 100 && /^[A-Za-z0-9+/=]+$/.test(input);\r\n}\r\n\r\n/**\r\n * 从 data URI 中提取 MIME 类型和数据\r\n */\r\nfunction parseDataUri(dataUri: string): { mimeType: string; data: string } {\r\n const match = dataUri.match(/^data:([^;,]+);base64,(.+)$/);\r\n if (match) {\r\n return {\r\n mimeType: match[1],\r\n data: match[2],\r\n };\r\n }\r\n // 如果没有匹配到,假设是纯 base64,默认 MIME 类型\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: dataUri,\r\n };\r\n}\r\n\r\n/**\r\n * 从文件扩展名推断 MIME 类型\r\n */\r\nfunction getMimeTypeFromUrl(url: string): string {\r\n const lower = url.toLowerCase();\r\n if (lower.includes('.png')) return 'image/png';\r\n if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';\r\n if (lower.includes('.webp')) return 'image/webp';\r\n if (lower.includes('.gif')) return 'image/gif';\r\n // 默认\r\n return 'image/jpeg';\r\n}\r\n\r\n/**\r\n * 处理图片输入,转换为 Gemini API 可用的格式\r\n * \r\n * 根据官方文档,对于 URL 图片,需要先 fetch 获取数据,再转换为 base64。\r\n * Gemini API 只接受 inlineData(base64)或通过 File API 上传的文件 URI。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport async function processImage(image: ImageInput): Promise<ProcessedImage> {\r\n // 如果是 Buffer\r\n if (Buffer.isBuffer(image)) {\r\n return {\r\n mimeType: 'image/jpeg', // 默认,实际中可能需要更精确的检测\r\n data: image.toString('base64'),\r\n };\r\n }\r\n\r\n // 如果是字符串\r\n if (typeof image === 'string') {\r\n // URL - 需要 fetch 并转换为 base64\r\n if (isUrl(image)) {\r\n try {\r\n const response = await fetch(image);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);\r\n }\r\n const arrayBuffer = await response.arrayBuffer();\r\n const base64Data = Buffer.from(arrayBuffer).toString('base64');\r\n \r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n data: base64Data,\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Failed to fetch image from URL: ${error.message}`);\r\n }\r\n throw new Error('Failed to fetch image from URL');\r\n }\r\n }\r\n\r\n // data URI\r\n if (image.startsWith('data:')) {\r\n const { mimeType, data } = parseDataUri(image);\r\n return { mimeType, data };\r\n }\r\n\r\n // 纯 base64\r\n if (isBase64(image)) {\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: image,\r\n };\r\n }\r\n\r\n // 无法识别,抛出错误\r\n throw new Error('Unsupported image format: string is not a valid URL or base64');\r\n }\r\n\r\n throw new Error('Unsupported image input type');\r\n}\r\n","import { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ImageInput } from '../types.js';\r\nimport { processImage } from '../processors/image.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 调用 Gemini API 进行图片分析\r\n * \r\n * @param image - 图片输入(Buffer、base64 或 URL)\r\n * @param prompt - 提示词\r\n * @param useGrounding - 是否使用 Google Search grounding\r\n * @returns LLM 返回的文本响应\r\n */\r\nexport async function callGemini(\r\n image: ImageInput,\r\n prompt: string,\r\n useGrounding?: boolean\r\n): Promise<string> {\r\n const { apiKey, model } = getGeminiConfig();\r\n\r\n // 初始化 Gemini API 客户端\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 处理图片(现在是异步的)\r\n const processedImage = await processImage(image);\r\n\r\n // 构建请求内容\r\n const contents = [];\r\n\r\n // 添加图片部分(统一使用 inlineData)\r\n contents.push({\r\n inlineData: {\r\n mimeType: processedImage.mimeType,\r\n data: processedImage.data,\r\n },\r\n });\r\n\r\n // 添加文本提示\r\n contents.push({\r\n text: prompt,\r\n });\r\n\r\n try {\r\n // 调用 Gemini API\r\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\r\n const requestParams: any = {\r\n contents: [{ role: 'user', parts: contents }],\r\n };\r\n \r\n if (useGrounding) {\r\n requestParams.tools = [{ googleSearch: {} }];\r\n }\r\n \r\n const result = await geminiModel.generateContent(requestParams);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n throw new Error('Gemini API returned empty response');\r\n }\r\n\r\n return text;\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Gemini API call failed: ${error.message}`);\r\n }\r\n throw new Error('Gemini API call failed with unknown error');\r\n }\r\n}\r\n","/**\r\n * 使用 Google Search grounding 验证商品名称\r\n */\r\n\r\nimport { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ReceiptItem } from '../types.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 构建批量验证 prompt\r\n */\r\nfunction buildVerificationPrompt(items: Partial<ReceiptItem>[]): string {\r\n const itemList = items\r\n .map((item, index) => `${index + 1}. \"${item.name}\"`)\r\n .join('\\n');\r\n\r\n return `这是从 Costco 超市小票中识别出的商品名称,部分名称可能不完整或有缩写。\r\n请使用 Google Search 查找并补全这些商品的完整正确名称。\r\n\r\n需要验证的商品:\r\n${itemList}\r\n\r\n验证方法建议:\r\n- 在搜索引擎中输入:商品原名 + \"Costco\"(例如:\"CEMOI 6X Costco\")\r\n- 这样能更准确地找到 Costco 销售的对应商品\r\n- 注意确认商品的包装规格(如数量、容量等)\r\n\r\n请返回 JSON 数组格式,每个商品包含:\r\n- index: 序号(1-based)\r\n- originalName: 原始名称\r\n- verifiedName: 验证后的完整名称(如果找到)\r\n- found: 是否找到匹配(布尔值)\r\n\r\n示例输出:\r\n[\r\n {\"index\": 1, \"originalName\": \"ORG MLK\", \"verifiedName\": \"Kirkland Signature Organic 2% Milk 1L\", \"found\": true},\r\n {\"index\": 2, \"originalName\": \"CEMΟΙ 6Χ\", \"verifiedName\": \"CEMOI 82% Dark Chocolate Bars, 6 × 100 g\", \"found\": true}\r\n]\r\n\r\n只返回 JSON 数组,不要其他文字。`;\r\n}\r\n\r\n/**\r\n * 解析验证响应\r\n */\r\ninterface VerificationResult {\r\n index: number;\r\n originalName: string;\r\n verifiedName: string;\r\n found: boolean;\r\n}\r\n\r\nfunction parseVerificationResponse(responseText: string): VerificationResult[] {\r\n try {\r\n // 移除可能的 markdown 代码块标记\r\n let cleaned = responseText.trim();\r\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\r\n if (codeBlockMatch) {\r\n cleaned = codeBlockMatch[1].trim();\r\n }\r\n\r\n const parsed = JSON.parse(cleaned);\r\n \r\n if (!Array.isArray(parsed)) {\r\n throw new Error('Response is not an array');\r\n }\r\n\r\n return parsed;\r\n } catch (error) {\r\n console.error('Failed to parse verification response:', error);\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * 批量验证商品名称\r\n * 使用 Google Search grounding 查找完整商品名\r\n * \r\n * @param items - 需要验证的商品列表\r\n * @returns 验证结果映射 (原始名称 -> 验证后名称)\r\n */\r\nexport async function batchVerifyItems(\r\n items: Partial<ReceiptItem>[]\r\n): Promise<Map<string, string>> {\r\n if (items.length === 0) {\r\n return new Map();\r\n }\r\n\r\n const { apiKey, model } = getGeminiConfig();\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 构建验证 prompt\r\n const prompt = buildVerificationPrompt(items);\r\n\r\n try {\r\n // 使用 Google Search grounding\r\n // 注意:tools 应该直接作为 generateContent 的顶级参数,而不是嵌套在 config 里\r\n const result = await geminiModel.generateContent({\r\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\r\n tools: [{ googleSearch: {} }],\r\n } as any);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n console.warn('Verification returned empty response');\r\n return new Map();\r\n }\r\n\r\n // 解析验证结果\r\n const verificationResults = parseVerificationResponse(text);\r\n\r\n // 构建映射\r\n const resultMap = new Map<string, string>();\r\n verificationResults.forEach((result) => {\r\n if (result.found && result.verifiedName) {\r\n resultMap.set(result.originalName, result.verifiedName);\r\n }\r\n });\r\n\r\n return resultMap;\r\n } catch (error) {\r\n console.error('Batch verification failed:', error);\r\n return new Map();\r\n }\r\n}\r\n","/**\r\n * 小票商品提取的 Prompt 模板\r\n * \r\n * 该模板要求 LLM:\r\n * 1. 从小票图片中提取所有商品信息\r\n * 2. 直接判断每个商品名称是否需要验证(needsVerification)\r\n * 3. 识别附加费用(押金、折扣)并标记归属关系\r\n * 4. 返回结构化的 JSON 数组\r\n */\r\nexport const EXTRACTION_PROMPT = `分析这张购物小票图片,提取所有商品信息和总金额。\r\n\r\n输出格式为包含两个字段的 JSON 对象:\r\n{\r\n \"items\": [...], // 商品数组\r\n \"total\": 123.45 // 小票总金额\r\n}\r\n\r\n每个商品包含:\r\n- name: 商品名称(字符串)\r\n- price: 单价(数字)\r\n- quantity: 数量(数字,默认 1)\r\n- needsVerification: 是否需要验证(布尔值)\r\n- hasTax: 是否含税(布尔值)\r\n- taxAmount: 税额(数字,可选)\r\n\r\n**关于 hasTax 的判断规则(Costco 小票):**\r\n- **如果商品名称后面有 \"H\" 标记**(如 \"ORG MLK H\"、\"CEMOI 6X H\"),则 hasTax = true\r\n- **如果商品名称后面没有 \"H\" 标记**,则 hasTax = false\r\n- **重要**:提取商品名称时,请去掉末尾的 \"H\" 标记,只保留商品名称本身\r\n- 如果小票上有明确的税费金额,填写到 taxAmount 字段\r\n\r\n关于 needsVerification 的判断规则:\r\n**重要原则:宁可多验证,不要猜测。当不确定时,优先设为 true。**\r\n\r\n必须设为 true 的情况:\r\n- 商品名称是缩写(如 \"ORG MLK\"、\"VEG\"、\"FRZ\")\r\n- 商品名称不完整或被截断(如 \"CHOCO...\"、\"有机...\")\r\n- 包含数字或字母组合但含义不明确(如 \"CEMΟΙ 6Χ\"、\"KS 12X\")\r\n- 商品名称模糊或可能有多种解释\r\n- 商品名称包含品牌缩写或代码\r\n- 只有品类没有具体品名(如 \"面包\"、\"饮料\")\r\n- 商品名称中混杂了数字但不清楚具体规格(如 \"牛奶 2\")\r\n- 任何你不能100%确定完整含义的名称\r\n\r\n可以设为 false 的情况(必须同时满足以下所有条件):\r\n- 商品名称完整、清晰、无缩写\r\n- 包含完整的品牌和规格信息(如 \"可口可乐瓶装 330ml\")\r\n- 你能100%确定这个名称的准确含义\r\n- 普通消费者看到这个名称能立即理解是什么商品\r\n\r\n示例:\r\n- \"ORG MLK\" → needsVerification: true(缩写)\r\n- \"CEMΟΙ 6Χ\" → needsVerification: true(包含不明确的字符)\r\n- \"面包\" → needsVerification: true(只有品类,没有具体信息)\r\n- \"KS Milk\" → needsVerification: true(品牌缩写)\r\n- \"有机牛奶 Kirkland Signature 1L\" → needsVerification: false(完整清晰)\r\n- \"富士苹果\" → needsVerification: false(完整且明确)\r\n\r\n**重要:附加费用处理规则**\r\n对于押金(Deposit、deposit、押金等)和折扣(TPD、discount、折扣等)这类附加费用:\r\n- 添加额外字段 isAttachment: true\r\n- 添加 attachmentType: \"deposit\" 或 \"discount\"\r\n- **重要**:将附加费用紧跟在它所属的商品后面排列\r\n- 系统会自动将附加费用合并到它前面的商品中\r\n- 这些附加费用不会作为独立商品返回\r\n\r\n归属规则(按照这个顺序排列):\r\n- 商品A\r\n- 商品A的押金(如果有)\r\n- 商品A的折扣(如果有)\r\n- 商品B\r\n- 商品B的押金(如果有)\r\n- ...\r\n\r\n**关于 total(总金额)的提取规则:**\r\n- 从小票底部找到 \"TOTAL\"、\"总计\"、\"合计\" 等标记\r\n- 提取对应的金额数字\r\n- 这是小票的最终应付金额\r\n\r\n只返回 JSON 对象,不要其他文字。\r\n\r\n示例输出:\r\n假设小票上显示:\r\n- \"KS ORG MLK 1L\" (无 H) → 不含税,¥12.50\r\n- \"ORG BRD H\" → 含税,¥8.00(税¥0.80)\r\n- \"CEMΟΙ 6Χ H\" → 含税,¥15.00\r\n- \"KS Apple\" (无 H) → 不含税,¥4.50 x 3\r\n- TOTAL: ¥37.30\r\n\r\n则输出为:\r\n{\r\n \"items\": [\r\n {\"name\": \"KS ORG MLK 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": false},\r\n {\"name\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8},\r\n {\"name\": \"Deposit VL\", \"price\": 0.5, \"quantity\": 2, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"deposit\"},\r\n {\"name\": \"TPD\", \"price\": -0.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"discount\"},\r\n {\"name\": \"CEMΟΙ 6Χ\", \"price\": 15.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true},\r\n {\"name\": \"KS Apple\", \"price\": 4.5, \"quantity\": 3, \"needsVerification\": true, \"hasTax\": false}\r\n ],\r\n \"total\": 37.30\r\n}\r\n\r\n注意:\r\n1. 商品名称中已去掉 \"H\" 标记,但根据原小票上的 \"H\" 标记设置了正确的 hasTax 值\r\n2. total 是小票上显示的最终应付金额`;\r\n","import type { InternalReceiptItem } from '../types.js';\r\n\r\n/**\r\n * LLM 返回的原始商品数据结构\r\n */\r\ninterface RawReceiptItem {\r\n name: string;\r\n price: number;\r\n quantity: number; // 必填,normalizeRawItem 会提供默认值 1\r\n needsVerification: boolean;\r\n hasTax: boolean;\r\n taxAmount?: number;\r\n deposit?: number;\r\n discount?: number;\r\n // 附加费用标记(用于解析时的临时字段)\r\n isAttachment?: boolean;\r\n attachmentType?: 'deposit' | 'discount';\r\n attachedTo?: number;\r\n}\r\n\r\n/**\r\n * 从 LLM 响应中提取 JSON\r\n * 处理可能的 markdown 代码块包裹\r\n */\r\nfunction extractJson(text: string): string {\r\n // 移除可能的 markdown 代码块标记\r\n let cleaned = text.trim();\r\n \r\n // 匹配 ```json ... ``` 或 ``` ... ```\r\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\r\n if (codeBlockMatch) {\r\n cleaned = codeBlockMatch[1].trim();\r\n }\r\n \r\n return cleaned;\r\n}\r\n\r\n/**\r\n * 验证并规范化原始商品数据\r\n */\r\nfunction normalizeRawItem(raw: any): RawReceiptItem {\r\n if (!raw || typeof raw !== 'object') {\r\n throw new Error('Invalid item: not an object');\r\n }\r\n\r\n if (typeof raw.name !== 'string' || !raw.name) {\r\n throw new Error('Invalid item: missing or invalid name field');\r\n }\r\n\r\n if (typeof raw.price !== 'number' || raw.price < 0) {\r\n throw new Error('Invalid item: missing or invalid price field');\r\n }\r\n\r\n return {\r\n name: raw.name,\r\n price: raw.price,\r\n quantity: typeof raw.quantity === 'number' ? raw.quantity : 1,\r\n needsVerification: Boolean(raw.needsVerification),\r\n hasTax: Boolean(raw.hasTax),\r\n taxAmount: typeof raw.taxAmount === 'number' ? raw.taxAmount : undefined,\r\n deposit: typeof raw.deposit === 'number' ? raw.deposit : undefined,\r\n discount: typeof raw.discount === 'number' ? raw.discount : undefined,\r\n // 保留附加费用标记用于后续处理\r\n isAttachment: raw.isAttachment === true ? true : undefined,\r\n attachmentType: raw.attachmentType,\r\n attachedTo: typeof raw.attachedTo === 'number' ? raw.attachedTo : undefined,\r\n };\r\n}\r\n\r\n/**\r\n * 合并附加费用(押金、折扣)到对应的商品中\r\n * 使用位置关系:附加费用紧跟在对应商品后面\r\n * \r\n * @param items - 包含附加费用标记的商品列表\r\n * @returns 合并后的商品列表(不包含独立的附加费用项)\r\n */\r\nfunction mergeAttachments(items: RawReceiptItem[]): RawReceiptItem[] {\r\n const result: RawReceiptItem[] = [];\r\n let currentItem: RawReceiptItem | null = null;\r\n \r\n for (let i = 0; i < items.length; i++) {\r\n const item = items[i];\r\n \r\n if (!item.isAttachment) {\r\n // 如果之前有商品,先保存\r\n if (currentItem) {\r\n result.push(currentItem);\r\n }\r\n // 开始新商品(深拷贝)\r\n currentItem = { ...item };\r\n } else {\r\n // 这是附加费用,合并到当前商品\r\n if (currentItem) {\r\n if (item.attachmentType === 'deposit') {\r\n // 押金:累加(考虑数量)\r\n currentItem.deposit = (currentItem.deposit || 0) + (item.price * item.quantity);\r\n } else if (item.attachmentType === 'discount') {\r\n // 折扣:累加(通常已经是负数)\r\n currentItem.discount = (currentItem.discount || 0) + item.price;\r\n }\r\n }\r\n // 如果没有前置商品,跳过这个孤立的附加费用\r\n }\r\n }\r\n \r\n // 保存最后一个商品\r\n if (currentItem) {\r\n result.push(currentItem);\r\n }\r\n \r\n // 移除临时字段\r\n return result.map(item => {\r\n const { isAttachment, attachmentType, attachedTo, ...cleanItem } = item;\r\n return cleanItem;\r\n });\r\n}\r\n\r\n/**\r\n * 解析结果接口\r\n */\r\nexport interface ParsedReceiptData {\r\n items: InternalReceiptItem[];\r\n total: number;\r\n}\r\n\r\n/**\r\n * 解析 LLM 返回的 JSON 响应\r\n * \r\n * @param responseText - LLM 返回的文本响应\r\n * @returns 解析后的小票数据(包含商品数组和总金额)\r\n * @throws 如果解析失败\r\n */\r\nexport function parseResponse(responseText: string): ParsedReceiptData {\r\n try {\r\n // 提取 JSON\r\n const jsonText = extractJson(responseText);\r\n\r\n // 解析 JSON\r\n const parsed = JSON.parse(jsonText);\r\n\r\n // 验证响应格式\r\n if (!parsed || typeof parsed !== 'object') {\r\n throw new Error('Response is not an object');\r\n }\r\n\r\n // 提取 items 数组\r\n if (!Array.isArray(parsed.items)) {\r\n throw new Error('Response.items is not an array');\r\n }\r\n\r\n // 提取 total\r\n if (typeof parsed.total !== 'number' || parsed.total < 0) {\r\n throw new Error('Response.total is missing or invalid');\r\n }\r\n\r\n // 验证并规范化每个商品\r\n const items = parsed.items.map((raw: any, index: number) => {\r\n try {\r\n return normalizeRawItem(raw);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : 'Unknown error';\r\n throw new Error(`Invalid item at index ${index}: ${message}`);\r\n }\r\n });\r\n\r\n // 合并附加费用到对应的商品\r\n const mergedItems = mergeAttachments(items);\r\n\r\n return {\r\n items: mergedItems,\r\n total: parsed.total,\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Failed to parse LLM response: ${error.message}\\n\\nResponse:\\n${responseText}`);\r\n }\r\n throw new Error(`Failed to parse LLM response with unknown error\\n\\nResponse:\\n${responseText}`);\r\n }\r\n}\r\n\r\n","import type { ImageInput, ExtractOptions, ReceiptItem, ReceiptData, VerificationContext } from './types.js';\r\nimport { callGemini } from './adapters/gemini.js';\r\nimport { batchVerifyItems } from './adapters/verifier.js';\r\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\r\nimport { parseResponse } from './processors/parser.js';\r\n\r\n/**\r\n * 从小票图片中提取商品数据和总金额\r\n * \r\n * 这是一个无状态的异步函数,每次调用独立执行。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @param options - 可选配置(包括验证回调)\r\n * @returns 小票数据(包含商品列表和总金额)\r\n * \r\n * @throws 如果环境变量 GEMINI_API_KEY 未设置\r\n * @throws 如果 API 调用失败\r\n * @throws 如果响应解析失败\r\n * \r\n * @example\r\n * ```typescript\r\n * // 基础用法\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * console.log(receipt.items); // 商品列表\r\n * console.log(receipt.total); // 总金额\r\n * \r\n * // 使用自动验证(默认已启用)\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * \r\n * // 禁用自动验证\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * autoVerify: false\r\n * });\r\n * \r\n * // 带自定义验证回调\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * verifyCallback: async (name, context) => {\r\n * const result = await myProductSearch(name);\r\n * return result ? { verifiedName: result.name } : null;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport async function extractReceiptItems(\r\n image: ImageInput,\r\n options?: ExtractOptions\r\n): Promise<ReceiptData> {\r\n // 1. 调用 Gemini API\r\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\r\n\r\n // 2. 解析响应\r\n const { items: parsedItems, total } = parseResponse(responseText);\r\n\r\n // 3. 处理需要验证的商品\r\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\r\n \r\n // 3a. 自动验证(使用 Google Search grounding)\r\n // 默认启用,除非显式设置为 false\r\n if (options?.autoVerify !== false && itemsNeedingVerification.length > 0) {\r\n try {\r\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\r\n \r\n // 应用验证结果\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n const verifiedName = verificationMap.get(item.name);\r\n if (verifiedName) {\r\n item.name = verifiedName;\r\n item.needsVerification = false;\r\n }\r\n // 如果未找到,保持原名称和 needsVerification=true\r\n }\r\n }\r\n } catch (error) {\r\n console.error('Auto verification failed:', error);\r\n // 失败时保持原始数据\r\n }\r\n }\r\n \r\n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\r\n if (options?.verifyCallback) {\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n try {\r\n // 每次调用时动态创建验证上下文,确保包含最新的验证状态\r\n // 创建深拷贝以防止外部修改内部数据\r\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({...item}));\r\n const verificationContext: VerificationContext = {\r\n rawText: responseText,\r\n allItems: publicItems,\r\n };\r\n \r\n const result = await options.verifyCallback(item.name, verificationContext);\r\n if (result && result.verifiedName) {\r\n // 更新商品名称\r\n item.name = result.verifiedName;\r\n // 验证成功后,标记为不再需要验证\r\n item.needsVerification = false;\r\n }\r\n } catch (error) {\r\n // 验证失败,静默忽略,保留原始数据\r\n console.error(`Verification failed for item \"${item.name}\":`, error);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // 4. 转换为公开类型:移除内部字段 needsVerification,创建完全独立的副本\r\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({\r\n ...item // 创建新对象,确保外部无法修改内部数据\r\n }));\r\n\r\n return {\r\n items: finalItems,\r\n total,\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/processors/image.ts","../src/adapters/gemini.ts","../src/adapters/verifier.ts","../src/utils/prompt.ts","../src/processors/parser.ts","../src/extract.ts"],"names":["GoogleGenerativeAI","getGeminiConfig","result","item"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ImageInput } from '../types.js';\r\n\r\n/**\r\n * 图片数据接口,用于传递给 Gemini API\r\n */\r\nexport interface ProcessedImage {\r\n /** 图片的 MIME 类型 */\r\n mimeType: string;\r\n /** base64 编码的图片数据(不包含 data URI 前缀) */\r\n data: string;\r\n}\r\n\r\n/**\r\n * 检测是否为 URL\r\n */\r\nfunction isUrl(input: string): boolean {\r\n return input.startsWith('http://') || input.startsWith('https://');\r\n}\r\n\r\n/**\r\n * 检测是否为 base64 字符串\r\n */\r\nfunction isBase64(input: string): boolean {\r\n // 简单检测:如果以 data: 开头,或者看起来像 base64\r\n if (input.startsWith('data:')) {\r\n return true;\r\n }\r\n // Base64 字符串通常很长,且只包含特定字符\r\n return input.length > 100 && /^[A-Za-z0-9+/=]+$/.test(input);\r\n}\r\n\r\n/**\r\n * 从 data URI 中提取 MIME 类型和数据\r\n */\r\nfunction parseDataUri(dataUri: string): { mimeType: string; data: string } {\r\n const match = dataUri.match(/^data:([^;,]+);base64,(.+)$/);\r\n if (match) {\r\n return {\r\n mimeType: match[1],\r\n data: match[2],\r\n };\r\n }\r\n // 如果没有匹配到,假设是纯 base64,默认 MIME 类型\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: dataUri,\r\n };\r\n}\r\n\r\n/**\r\n * 从文件扩展名推断 MIME 类型\r\n */\r\nfunction getMimeTypeFromUrl(url: string): string {\r\n const lower = url.toLowerCase();\r\n if (lower.includes('.png')) return 'image/png';\r\n if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';\r\n if (lower.includes('.webp')) return 'image/webp';\r\n if (lower.includes('.gif')) return 'image/gif';\r\n // 默认\r\n return 'image/jpeg';\r\n}\r\n\r\n/**\r\n * 处理图片输入,转换为 Gemini API 可用的格式\r\n * \r\n * 根据官方文档,对于 URL 图片,需要先 fetch 获取数据,再转换为 base64。\r\n * Gemini API 只接受 inlineData(base64)或通过 File API 上传的文件 URI。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport async function processImage(image: ImageInput): Promise<ProcessedImage> {\r\n // 如果是 Buffer\r\n if (Buffer.isBuffer(image)) {\r\n return {\r\n mimeType: 'image/jpeg', // 默认,实际中可能需要更精确的检测\r\n data: image.toString('base64'),\r\n };\r\n }\r\n\r\n // 如果是字符串\r\n if (typeof image === 'string') {\r\n // URL - 需要 fetch 并转换为 base64\r\n if (isUrl(image)) {\r\n try {\r\n const response = await fetch(image);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);\r\n }\r\n const arrayBuffer = await response.arrayBuffer();\r\n const base64Data = Buffer.from(arrayBuffer).toString('base64');\r\n \r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n data: base64Data,\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Failed to fetch image from URL: ${error.message}`);\r\n }\r\n throw new Error('Failed to fetch image from URL');\r\n }\r\n }\r\n\r\n // data URI\r\n if (image.startsWith('data:')) {\r\n const { mimeType, data } = parseDataUri(image);\r\n return { mimeType, data };\r\n }\r\n\r\n // 纯 base64\r\n if (isBase64(image)) {\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: image,\r\n };\r\n }\r\n\r\n // 无法识别,抛出错误\r\n throw new Error('Unsupported image format: string is not a valid URL or base64');\r\n }\r\n\r\n throw new Error('Unsupported image input type');\r\n}\r\n","import { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ImageInput } from '../types.js';\r\nimport { processImage } from '../processors/image.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 调用 Gemini API 进行图片分析\r\n * \r\n * @param image - 图片输入(Buffer、base64 或 URL)\r\n * @param prompt - 提示词\r\n * @param useGrounding - 是否使用 Google Search grounding\r\n * @returns LLM 返回的文本响应\r\n */\r\nexport async function callGemini(\r\n image: ImageInput,\r\n prompt: string,\r\n useGrounding?: boolean\r\n): Promise<string> {\r\n const { apiKey, model } = getGeminiConfig();\r\n\r\n // 初始化 Gemini API 客户端\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 处理图片(现在是异步的)\r\n const processedImage = await processImage(image);\r\n\r\n // 构建请求内容\r\n const contents = [];\r\n\r\n // 添加图片部分(统一使用 inlineData)\r\n contents.push({\r\n inlineData: {\r\n mimeType: processedImage.mimeType,\r\n data: processedImage.data,\r\n },\r\n });\r\n\r\n // 添加文本提示\r\n contents.push({\r\n text: prompt,\r\n });\r\n\r\n try {\r\n // 调用 Gemini API\r\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\r\n const requestParams: any = {\r\n contents: [{ role: 'user', parts: contents }],\r\n };\r\n \r\n if (useGrounding) {\r\n requestParams.tools = [{ googleSearch: {} }];\r\n }\r\n \r\n const result = await geminiModel.generateContent(requestParams);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n throw new Error('Gemini API returned empty response');\r\n }\r\n\r\n return text;\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Gemini API call failed: ${error.message}`);\r\n }\r\n throw new Error('Gemini API call failed with unknown error');\r\n }\r\n}\r\n","/**\r\n * 使用 Google Search grounding 验证商品名称\r\n */\r\n\r\nimport { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ReceiptItem } from '../types.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 构建批量验证 prompt\r\n */\r\nfunction buildVerificationPrompt(items: Partial<ReceiptItem>[]): string {\r\n const itemList = items\r\n .map((item, index) => `${index + 1}. \"${item.name}\"`)\r\n .join('\\n');\r\n\r\n return `这是从 Costco 超市小票中识别出的商品名称,部分名称可能不完整或有缩写。\r\n请使用 Google Search 查找并补全这些商品的完整正确名称。\r\n\r\n需要验证的商品:\r\n${itemList}\r\n\r\n验证方法建议:\r\n- 在搜索引擎中输入:商品原名 + \"Costco\"(例如:\"CEMOI 6X Costco\")\r\n- 这样能更准确地找到 Costco 销售的对应商品\r\n- 注意确认商品的包装规格(如数量、容量等)\r\n\r\n请返回 JSON 数组格式,每个商品包含:\r\n- index: 序号(1-based)\r\n- originalName: 原始名称\r\n- verifiedName: 验证后的完整名称(如果找到)\r\n- found: 是否找到匹配(布尔值)\r\n\r\n示例输出:\r\n[\r\n {\"index\": 1, \"originalName\": \"ORG MLK\", \"verifiedName\": \"Kirkland Signature Organic 2% Milk 1L\", \"found\": true},\r\n {\"index\": 2, \"originalName\": \"CEMΟΙ 6Χ\", \"verifiedName\": \"CEMOI 82% Dark Chocolate Bars, 6 × 100 g\", \"found\": true}\r\n]\r\n\r\n只返回 JSON 数组,不要其他文字。`;\r\n}\r\n\r\n/**\r\n * 解析验证响应\r\n */\r\ninterface VerificationResult {\r\n index: number;\r\n originalName: string;\r\n verifiedName: string;\r\n found: boolean;\r\n}\r\n\r\nfunction parseVerificationResponse(responseText: string): VerificationResult[] {\r\n try {\r\n // 移除可能的 markdown 代码块标记\r\n let cleaned = responseText.trim();\r\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\r\n if (codeBlockMatch) {\r\n cleaned = codeBlockMatch[1].trim();\r\n }\r\n\r\n const parsed = JSON.parse(cleaned);\r\n \r\n if (!Array.isArray(parsed)) {\r\n throw new Error('Response is not an array');\r\n }\r\n\r\n return parsed;\r\n } catch (error) {\r\n console.error('Failed to parse verification response:', error);\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * 批量验证商品名称\r\n * 使用 Google Search grounding 查找完整商品名\r\n * \r\n * @param items - 需要验证的商品列表\r\n * @returns 验证结果映射 (原始名称 -> 验证后名称)\r\n */\r\nexport async function batchVerifyItems(\r\n items: Partial<ReceiptItem>[]\r\n): Promise<Map<string, string>> {\r\n if (items.length === 0) {\r\n return new Map();\r\n }\r\n\r\n const { apiKey, model } = getGeminiConfig();\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 构建验证 prompt\r\n const prompt = buildVerificationPrompt(items);\r\n\r\n try {\r\n // 使用 Google Search grounding\r\n // 注意:tools 应该直接作为 generateContent 的顶级参数,而不是嵌套在 config 里\r\n const result = await geminiModel.generateContent({\r\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\r\n tools: [{ googleSearch: {} }],\r\n } as any);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n console.warn('Verification returned empty response');\r\n return new Map();\r\n }\r\n\r\n // 解析验证结果\r\n const verificationResults = parseVerificationResponse(text);\r\n\r\n // 构建映射\r\n const resultMap = new Map<string, string>();\r\n verificationResults.forEach((result) => {\r\n if (result.found && result.verifiedName) {\r\n resultMap.set(result.originalName, result.verifiedName);\r\n }\r\n });\r\n\r\n return resultMap;\r\n } catch (error) {\r\n console.error('Batch verification failed:', error);\r\n return new Map();\r\n }\r\n}\r\n","/**\n * 小票商品提取的 Prompt 模板\n * \n * 该模板要求 LLM:\n * 1. 从小票图片中提取所有商品信息\n * 2. 直接判断每个商品名称是否需要验证(needsVerification)\n * 3. 识别附加费用(押金、折扣)并标记归属关系\n * 4. 返回结构化的 JSON 数组\n */\nexport const EXTRACTION_PROMPT = `分析这张购物小票图片,提取所有商品信息和总金额。\n\n输出格式为包含两个字段的 JSON 对象:\n{\n \"items\": [...], // 商品数组\n \"total\": 123.45 // 小票总金额\n}\n\n每个商品包含:\n- name: 商品名称(字符串)\n- price: 单价(数字)\n- quantity: 数量(数字,默认 1)\n- needsVerification: 是否需要验证(布尔值)\n- hasTax: 是否含税(布尔值)\n- taxAmount: 税额(数字,可选)\n\n**关于 hasTax 的判断规则(Costco 小票):**\n- **如果商品名称后面有 \"H\" 标记**(如 \"ORG MLK H\"、\"CEMOI 6X H\"),则 hasTax = true\n- **如果商品名称后面没有 \"H\" 标记**,则 hasTax = false\n- **重要**:提取商品名称时,请去掉末尾的 \"H\" 标记,只保留商品名称本身\n- 如果小票上有明确的税费金额,填写到 taxAmount 字段\n\n关于 needsVerification 的判断规则:\n**重要原则:宁可多验证,不要猜测。当不确定时,优先设为 true。**\n\n必须设为 true 的情况:\n- 商品名称是缩写(如 \"ORG MLK\"、\"VEG\"、\"FRZ\")\n- 商品名称不完整或被截断(如 \"CHOCO...\"、\"有机...\")\n- 包含数字或字母组合但含义不明确(如 \"CEMΟΙ 6Χ\"、\"KS 12X\")\n- 商品名称模糊或可能有多种解释\n- 商品名称包含品牌缩写或代码\n- 只有品类没有具体品名(如 \"面包\"、\"饮料\")\n- 商品名称中混杂了数字但不清楚具体规格(如 \"牛奶 2\")\n- 任何你不能100%确定完整含义的名称\n\n可以设为 false 的情况(必须同时满足以下所有条件):\n- 商品名称完整、清晰、无缩写\n- 包含完整的品牌和规格信息(如 \"可口可乐瓶装 330ml\")\n- 你能100%确定这个名称的准确含义\n- 普通消费者看到这个名称能立即理解是什么商品\n\n示例:\n- \"ORG MLK\" → needsVerification: true(缩写)\n- \"CEMΟΙ 6Χ\" → needsVerification: true(包含不明确的字符)\n- \"面包\" → needsVerification: true(只有品类,没有具体信息)\n- \"KS Milk\" → needsVerification: true(品牌缩写)\n- \"有机牛奶 Kirkland Signature 1L\" → needsVerification: false(完整清晰)\n- \"富士苹果\" → needsVerification: false(完整且明确)\n\n**重要:附加费用处理规则**\n对于押金(Deposit、deposit、押金等)和折扣(TPD、discount、折扣等)这类附加费用:\n- 添加额外字段 isAttachment: true\n- 添加 attachmentType: \"deposit\" 或 \"discount\"\n- **价格格式要求**:\n - 押金的 price 字段必须是**正数**(如 0.5)\n - 折扣的 price 字段也必须是**正数**(如 0.5,而不是 -0.5)\n - 系统会自动将折扣金额处理为负值,你只需输出折扣的绝对值\n- **重要**:将附加费用紧跟在它所属的商品后面排列\n- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n**关于 total(总金额)的提取规则:**\n- 从小票底部找到 \"TOTAL\"、\"总计\"、\"合计\" 等标记\n- 提取对应的金额数字\n- 这是小票的最终应付金额\n\n**输出格式要求(极其重要)**:\n1. 只返回 JSON 对象,不要任何其他文字、解释或markdown标记\n2. 所有 price 字段必须是**正数**,包括折扣项\n3. 严格遵循上述字段定义,不要添加额外字段\n4. 确保 JSON 格式正确,可以被直接解析\n\n示例输出:\n假设小票上显示:\n- \"KS ORG MLK 1L\" (无 H) → 不含税,¥12.50\n- \"ORG BRD H\" → 含税,¥8.00(税¥0.80)\n- \"CEMΟΙ 6Χ H\" → 含税,¥15.00\n- \"KS Apple\" (无 H) → 不含税,¥4.50 x 3\n- TOTAL: ¥37.30\n\n则输出为:\n{\n \"items\": [\n {\"name\": \"KS ORG MLK 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": false},\n {\"name\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8},\n {\"name\": \"Deposit VL\", \"price\": 0.5, \"quantity\": 2, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"deposit\"},\n {\"name\": \"TPD\", \"price\": 0.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"discount\"},\n {\"name\": \"CEMΟΙ 6Χ\", \"price\": 15.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true},\n {\"name\": \"KS Apple\", \"price\": 4.5, \"quantity\": 3, \"needsVerification\": true, \"hasTax\": false}\n ],\n \"total\": 37.30\n}\n\n注意:\n1. 商品名称中已去掉 \"H\" 标记,但根据原小票上的 \"H\" 标记设置了正确的 hasTax 值\n2. TPD 折扣项的 price 是**正数 0.5**(不是 -0.5),系统会自动处理为负值\n3. 所有附加费用(押金、折扣)的 price 都必须是正数\n4. total 是小票上显示的最终应付金额`;\n","import type { InternalReceiptItem } from '../types.js';\n\n/**\n * LLM 返回的原始商品数据结构\n */\ninterface RawReceiptItem {\n name: string;\n price: number;\n quantity: number; // 必填,normalizeRawItem 会提供默认值 1\n needsVerification: boolean;\n hasTax: boolean;\n taxAmount?: number;\n deposit?: number;\n discount?: number;\n // 附加费用标记(用于解析时的临时字段)\n isAttachment?: boolean;\n attachmentType?: 'deposit' | 'discount';\n attachedTo?: number;\n}\n\n/**\n * 从 LLM 响应中提取 JSON\n * 处理可能的 markdown 代码块包裹\n */\nfunction extractJson(text: string): string {\n // 移除可能的 markdown 代码块标记\n let cleaned = text.trim();\n \n // 匹配 ```json ... ``` 或 ``` ... ```\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (codeBlockMatch) {\n cleaned = codeBlockMatch[1].trim();\n }\n \n return cleaned;\n}\n\n/**\n * 验证并规范化原始商品数据\n */\nfunction normalizeRawItem(raw: any): RawReceiptItem {\n if (!raw || typeof raw !== 'object') {\n throw new Error('Invalid item: not an object');\n }\n\n if (typeof raw.name !== 'string' || !raw.name) {\n throw new Error('Invalid item: missing or invalid name field');\n }\n\n if (typeof raw.price !== 'number' || raw.price < 0) {\n throw new Error('Invalid item: missing or invalid price field');\n }\n\n return {\n name: raw.name,\n price: raw.price,\n quantity: typeof raw.quantity === 'number' ? raw.quantity : 1,\n needsVerification: Boolean(raw.needsVerification),\n hasTax: Boolean(raw.hasTax),\n taxAmount: typeof raw.taxAmount === 'number' ? raw.taxAmount : undefined,\n deposit: typeof raw.deposit === 'number' ? raw.deposit : undefined,\n discount: typeof raw.discount === 'number' ? raw.discount : undefined,\n // 保留附加费用标记用于后续处理\n isAttachment: raw.isAttachment === true ? true : undefined,\n attachmentType: raw.attachmentType,\n attachedTo: typeof raw.attachedTo === 'number' ? raw.attachedTo : undefined,\n };\n}\n\n/**\n * 合并附加费用(押金、折扣)到对应的商品中\n * 使用位置关系:附加费用紧跟在对应商品后面\n * \n * @param items - 包含附加费用标记的商品列表\n * @returns 合并后的商品列表(不包含独立的附加费用项)\n */\nfunction mergeAttachments(items: RawReceiptItem[]): RawReceiptItem[] {\n const result: RawReceiptItem[] = [];\n let currentItem: RawReceiptItem | null = null;\n \n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n \n if (!item.isAttachment) {\n // 如果之前有商品,先保存\n if (currentItem) {\n result.push(currentItem);\n }\n // 开始新商品(深拷贝)\n currentItem = { ...item };\n } else {\n // 这是附加费用,合并到当前商品\n if (currentItem) {\n if (item.attachmentType === 'deposit') {\n // 押金:累加(考虑数量)\n currentItem.deposit = (currentItem.deposit || 0) + (item.price * item.quantity);\n } else if (item.attachmentType === 'discount') {\n // 折扣:LLM 输出正数,我们转为负数累加\n currentItem.discount = (currentItem.discount || 0) - item.price;\n }\n }\n // 如果没有前置商品,跳过这个孤立的附加费用\n }\n }\n \n // 保存最后一个商品\n if (currentItem) {\n result.push(currentItem);\n }\n \n // 移除临时字段\n return result.map(item => {\n const { isAttachment, attachmentType, attachedTo, ...cleanItem } = item;\n return cleanItem;\n });\n}\n\n/**\n * 解析结果接口\n */\nexport interface ParsedReceiptData {\n items: InternalReceiptItem[];\n total: number;\n}\n\n/**\n * 解析 LLM 返回的 JSON 响应\n * \n * @param responseText - LLM 返回的文本响应\n * @returns 解析后的小票数据(包含商品数组和总金额)\n * @throws 如果解析失败\n */\nexport function parseResponse(responseText: string): ParsedReceiptData {\n try {\n // 提取 JSON\n const jsonText = extractJson(responseText);\n\n // 解析 JSON\n const parsed = JSON.parse(jsonText);\n\n // 验证响应格式\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('Response is not an object');\n }\n\n // 提取 items 数组\n if (!Array.isArray(parsed.items)) {\n throw new Error('Response.items is not an array');\n }\n\n // 提取 total\n if (typeof parsed.total !== 'number' || parsed.total < 0) {\n throw new Error('Response.total is missing or invalid');\n }\n\n // 验证并规范化每个商品\n const items = parsed.items.map((raw: any, index: number) => {\n try {\n return normalizeRawItem(raw);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n throw new Error(`Invalid item at index ${index}: ${message}`);\n }\n });\n\n // 合并附加费用到对应的商品\n const mergedItems = mergeAttachments(items);\n\n return {\n items: mergedItems,\n total: parsed.total,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to parse LLM response: ${error.message}\\n\\nResponse:\\n${responseText}`);\n }\n throw new Error(`Failed to parse LLM response with unknown error\\n\\nResponse:\\n${responseText}`);\n }\n}\n\n","import type { ImageInput, ExtractOptions, ReceiptItem, ReceiptData, VerificationContext } from './types.js';\r\nimport { callGemini } from './adapters/gemini.js';\r\nimport { batchVerifyItems } from './adapters/verifier.js';\r\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\r\nimport { parseResponse } from './processors/parser.js';\r\n\r\n/**\r\n * 从小票图片中提取商品数据和总金额\r\n * \r\n * 这是一个无状态的异步函数,每次调用独立执行。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @param options - 可选配置(包括验证回调)\r\n * @returns 小票数据(包含商品列表和总金额)\r\n * \r\n * @throws 如果环境变量 GEMINI_API_KEY 未设置\r\n * @throws 如果 API 调用失败\r\n * @throws 如果响应解析失败\r\n * \r\n * @example\r\n * ```typescript\r\n * // 基础用法\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * console.log(receipt.items); // 商品列表\r\n * console.log(receipt.total); // 总金额\r\n * \r\n * // 使用自动验证(默认已启用)\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * \r\n * // 禁用自动验证\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * autoVerify: false\r\n * });\r\n * \r\n * // 带自定义验证回调\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * verifyCallback: async (name, context) => {\r\n * const result = await myProductSearch(name);\r\n * return result ? { verifiedName: result.name } : null;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport async function extractReceiptItems(\r\n image: ImageInput,\r\n options?: ExtractOptions\r\n): Promise<ReceiptData> {\r\n // 1. 调用 Gemini API\r\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\r\n\r\n // 2. 解析响应\r\n const { items: parsedItems, total } = parseResponse(responseText);\r\n\r\n // 3. 处理需要验证的商品\r\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\r\n \r\n // 3a. 自动验证(使用 Google Search grounding)\r\n // 默认启用,除非显式设置为 false\r\n if (options?.autoVerify !== false && itemsNeedingVerification.length > 0) {\r\n try {\r\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\r\n \r\n // 应用验证结果\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n const verifiedName = verificationMap.get(item.name);\r\n if (verifiedName) {\r\n item.name = verifiedName;\r\n item.needsVerification = false;\r\n }\r\n // 如果未找到,保持原名称和 needsVerification=true\r\n }\r\n }\r\n } catch (error) {\r\n console.error('Auto verification failed:', error);\r\n // 失败时保持原始数据\r\n }\r\n }\r\n \r\n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\r\n if (options?.verifyCallback) {\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n try {\r\n // 每次调用时动态创建验证上下文,确保包含最新的验证状态\r\n // 创建深拷贝以防止外部修改内部数据\r\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({...item}));\r\n const verificationContext: VerificationContext = {\r\n rawText: responseText,\r\n allItems: publicItems,\r\n };\r\n \r\n const result = await options.verifyCallback(item.name, verificationContext);\r\n if (result && result.verifiedName) {\r\n // 更新商品名称\r\n item.name = result.verifiedName;\r\n // 验证成功后,标记为不再需要验证\r\n item.needsVerification = false;\r\n }\r\n } catch (error) {\r\n // 验证失败,静默忽略,保留原始数据\r\n console.error(`Verification failed for item \"${item.name}\":`, error);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // 4. 转换为公开类型:移除内部字段 needsVerification,创建完全独立的副本\r\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({\r\n ...item // 创建新对象,确保外部无法修改内部数据\r\n }));\r\n\r\n return {\r\n items: finalItems,\r\n total,\r\n };\r\n}\r\n"]}
package/dist/index.js CHANGED
@@ -262,6 +262,10 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
262
262
  \u5BF9\u4E8E\u62BC\u91D1\uFF08Deposit\u3001deposit\u3001\u62BC\u91D1\u7B49\uFF09\u548C\u6298\u6263\uFF08TPD\u3001discount\u3001\u6298\u6263\u7B49\uFF09\u8FD9\u7C7B\u9644\u52A0\u8D39\u7528\uFF1A
263
263
  - \u6DFB\u52A0\u989D\u5916\u5B57\u6BB5 isAttachment: true
264
264
  - \u6DFB\u52A0 attachmentType: "deposit" \u6216 "discount"
265
+ - **\u4EF7\u683C\u683C\u5F0F\u8981\u6C42**\uFF1A
266
+ - \u62BC\u91D1\u7684 price \u5B57\u6BB5\u5FC5\u987B\u662F**\u6B63\u6570**\uFF08\u5982 0.5\uFF09
267
+ - \u6298\u6263\u7684 price \u5B57\u6BB5\u4E5F\u5FC5\u987B\u662F**\u6B63\u6570**\uFF08\u5982 0.5\uFF0C\u800C\u4E0D\u662F -0.5\uFF09
268
+ - \u7CFB\u7EDF\u4F1A\u81EA\u52A8\u5C06\u6298\u6263\u91D1\u989D\u5904\u7406\u4E3A\u8D1F\u503C\uFF0C\u4F60\u53EA\u9700\u8F93\u51FA\u6298\u6263\u7684\u7EDD\u5BF9\u503C
265
269
  - **\u91CD\u8981**\uFF1A\u5C06\u9644\u52A0\u8D39\u7528\u7D27\u8DDF\u5728\u5B83\u6240\u5C5E\u7684\u5546\u54C1\u540E\u9762\u6392\u5217
266
270
  - \u7CFB\u7EDF\u4F1A\u81EA\u52A8\u5C06\u9644\u52A0\u8D39\u7528\u5408\u5E76\u5230\u5B83\u524D\u9762\u7684\u5546\u54C1\u4E2D
267
271
  - \u8FD9\u4E9B\u9644\u52A0\u8D39\u7528\u4E0D\u4F1A\u4F5C\u4E3A\u72EC\u7ACB\u5546\u54C1\u8FD4\u56DE
@@ -279,7 +283,11 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
279
283
  - \u63D0\u53D6\u5BF9\u5E94\u7684\u91D1\u989D\u6570\u5B57
280
284
  - \u8FD9\u662F\u5C0F\u7968\u7684\u6700\u7EC8\u5E94\u4ED8\u91D1\u989D
281
285
 
282
- \u53EA\u8FD4\u56DE JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u5176\u4ED6\u6587\u5B57\u3002
286
+ **\u8F93\u51FA\u683C\u5F0F\u8981\u6C42\uFF08\u6781\u5176\u91CD\u8981\uFF09**\uFF1A
287
+ 1. \u53EA\u8FD4\u56DE JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3001\u89E3\u91CA\u6216markdown\u6807\u8BB0
288
+ 2. \u6240\u6709 price \u5B57\u6BB5\u5FC5\u987B\u662F**\u6B63\u6570**\uFF0C\u5305\u62EC\u6298\u6263\u9879
289
+ 3. \u4E25\u683C\u9075\u5FAA\u4E0A\u8FF0\u5B57\u6BB5\u5B9A\u4E49\uFF0C\u4E0D\u8981\u6DFB\u52A0\u989D\u5916\u5B57\u6BB5
290
+ 4. \u786E\u4FDD JSON \u683C\u5F0F\u6B63\u786E\uFF0C\u53EF\u4EE5\u88AB\u76F4\u63A5\u89E3\u6790
283
291
 
284
292
  \u793A\u4F8B\u8F93\u51FA\uFF1A
285
293
  \u5047\u8BBE\u5C0F\u7968\u4E0A\u663E\u793A\uFF1A
@@ -295,7 +303,7 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
295
303
  {"name": "KS ORG MLK 1L", "price": 12.5, "quantity": 1, "needsVerification": true, "hasTax": false},
296
304
  {"name": "ORG BRD", "price": 8.0, "quantity": 1, "needsVerification": true, "hasTax": true, "taxAmount": 0.8},
297
305
  {"name": "Deposit VL", "price": 0.5, "quantity": 2, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "deposit"},
298
- {"name": "TPD", "price": -0.5, "quantity": 1, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "discount"},
306
+ {"name": "TPD", "price": 0.5, "quantity": 1, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "discount"},
299
307
  {"name": "CEM\u039F\u0399 6\u03A7", "price": 15.0, "quantity": 1, "needsVerification": true, "hasTax": true},
300
308
  {"name": "KS Apple", "price": 4.5, "quantity": 3, "needsVerification": true, "hasTax": false}
301
309
  ],
@@ -304,7 +312,9 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
304
312
 
305
313
  \u6CE8\u610F\uFF1A
306
314
  1. \u5546\u54C1\u540D\u79F0\u4E2D\u5DF2\u53BB\u6389 "H" \u6807\u8BB0\uFF0C\u4F46\u6839\u636E\u539F\u5C0F\u7968\u4E0A\u7684 "H" \u6807\u8BB0\u8BBE\u7F6E\u4E86\u6B63\u786E\u7684 hasTax \u503C
307
- 2. total \u662F\u5C0F\u7968\u4E0A\u663E\u793A\u7684\u6700\u7EC8\u5E94\u4ED8\u91D1\u989D`;
315
+ 2. TPD \u6298\u6263\u9879\u7684 price \u662F**\u6B63\u6570 0.5**\uFF08\u4E0D\u662F -0.5\uFF09\uFF0C\u7CFB\u7EDF\u4F1A\u81EA\u52A8\u5904\u7406\u4E3A\u8D1F\u503C
316
+ 3. \u6240\u6709\u9644\u52A0\u8D39\u7528\uFF08\u62BC\u91D1\u3001\u6298\u6263\uFF09\u7684 price \u90FD\u5FC5\u987B\u662F\u6B63\u6570
317
+ 4. total \u662F\u5C0F\u7968\u4E0A\u663E\u793A\u7684\u6700\u7EC8\u5E94\u4ED8\u91D1\u989D`;
308
318
 
309
319
  // src/processors/parser.ts
310
320
  function extractJson(text) {
@@ -355,7 +365,7 @@ function mergeAttachments(items) {
355
365
  if (item.attachmentType === "deposit") {
356
366
  currentItem.deposit = (currentItem.deposit || 0) + item.price * item.quantity;
357
367
  } else if (item.attachmentType === "discount") {
358
- currentItem.discount = (currentItem.discount || 0) + item.price;
368
+ currentItem.discount = (currentItem.discount || 0) - item.price;
359
369
  }
360
370
  }
361
371
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/processors/image.ts","../src/adapters/gemini.ts","../src/adapters/verifier.ts","../src/utils/prompt.ts","../src/processors/parser.ts","../src/extract.ts"],"names":["getGeminiConfig","GoogleGenerativeAI","result","item"],"mappings":"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","file":"index.js","sourcesContent":["import type { ImageInput } from '../types.js';\r\n\r\n/**\r\n * 图片数据接口,用于传递给 Gemini API\r\n */\r\nexport interface ProcessedImage {\r\n /** 图片的 MIME 类型 */\r\n mimeType: string;\r\n /** base64 编码的图片数据(不包含 data URI 前缀) */\r\n data: string;\r\n}\r\n\r\n/**\r\n * 检测是否为 URL\r\n */\r\nfunction isUrl(input: string): boolean {\r\n return input.startsWith('http://') || input.startsWith('https://');\r\n}\r\n\r\n/**\r\n * 检测是否为 base64 字符串\r\n */\r\nfunction isBase64(input: string): boolean {\r\n // 简单检测:如果以 data: 开头,或者看起来像 base64\r\n if (input.startsWith('data:')) {\r\n return true;\r\n }\r\n // Base64 字符串通常很长,且只包含特定字符\r\n return input.length > 100 && /^[A-Za-z0-9+/=]+$/.test(input);\r\n}\r\n\r\n/**\r\n * 从 data URI 中提取 MIME 类型和数据\r\n */\r\nfunction parseDataUri(dataUri: string): { mimeType: string; data: string } {\r\n const match = dataUri.match(/^data:([^;,]+);base64,(.+)$/);\r\n if (match) {\r\n return {\r\n mimeType: match[1],\r\n data: match[2],\r\n };\r\n }\r\n // 如果没有匹配到,假设是纯 base64,默认 MIME 类型\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: dataUri,\r\n };\r\n}\r\n\r\n/**\r\n * 从文件扩展名推断 MIME 类型\r\n */\r\nfunction getMimeTypeFromUrl(url: string): string {\r\n const lower = url.toLowerCase();\r\n if (lower.includes('.png')) return 'image/png';\r\n if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';\r\n if (lower.includes('.webp')) return 'image/webp';\r\n if (lower.includes('.gif')) return 'image/gif';\r\n // 默认\r\n return 'image/jpeg';\r\n}\r\n\r\n/**\r\n * 处理图片输入,转换为 Gemini API 可用的格式\r\n * \r\n * 根据官方文档,对于 URL 图片,需要先 fetch 获取数据,再转换为 base64。\r\n * Gemini API 只接受 inlineData(base64)或通过 File API 上传的文件 URI。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport async function processImage(image: ImageInput): Promise<ProcessedImage> {\r\n // 如果是 Buffer\r\n if (Buffer.isBuffer(image)) {\r\n return {\r\n mimeType: 'image/jpeg', // 默认,实际中可能需要更精确的检测\r\n data: image.toString('base64'),\r\n };\r\n }\r\n\r\n // 如果是字符串\r\n if (typeof image === 'string') {\r\n // URL - 需要 fetch 并转换为 base64\r\n if (isUrl(image)) {\r\n try {\r\n const response = await fetch(image);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);\r\n }\r\n const arrayBuffer = await response.arrayBuffer();\r\n const base64Data = Buffer.from(arrayBuffer).toString('base64');\r\n \r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n data: base64Data,\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Failed to fetch image from URL: ${error.message}`);\r\n }\r\n throw new Error('Failed to fetch image from URL');\r\n }\r\n }\r\n\r\n // data URI\r\n if (image.startsWith('data:')) {\r\n const { mimeType, data } = parseDataUri(image);\r\n return { mimeType, data };\r\n }\r\n\r\n // 纯 base64\r\n if (isBase64(image)) {\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: image,\r\n };\r\n }\r\n\r\n // 无法识别,抛出错误\r\n throw new Error('Unsupported image format: string is not a valid URL or base64');\r\n }\r\n\r\n throw new Error('Unsupported image input type');\r\n}\r\n","import { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ImageInput } from '../types.js';\r\nimport { processImage } from '../processors/image.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 调用 Gemini API 进行图片分析\r\n * \r\n * @param image - 图片输入(Buffer、base64 或 URL)\r\n * @param prompt - 提示词\r\n * @param useGrounding - 是否使用 Google Search grounding\r\n * @returns LLM 返回的文本响应\r\n */\r\nexport async function callGemini(\r\n image: ImageInput,\r\n prompt: string,\r\n useGrounding?: boolean\r\n): Promise<string> {\r\n const { apiKey, model } = getGeminiConfig();\r\n\r\n // 初始化 Gemini API 客户端\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 处理图片(现在是异步的)\r\n const processedImage = await processImage(image);\r\n\r\n // 构建请求内容\r\n const contents = [];\r\n\r\n // 添加图片部分(统一使用 inlineData)\r\n contents.push({\r\n inlineData: {\r\n mimeType: processedImage.mimeType,\r\n data: processedImage.data,\r\n },\r\n });\r\n\r\n // 添加文本提示\r\n contents.push({\r\n text: prompt,\r\n });\r\n\r\n try {\r\n // 调用 Gemini API\r\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\r\n const requestParams: any = {\r\n contents: [{ role: 'user', parts: contents }],\r\n };\r\n \r\n if (useGrounding) {\r\n requestParams.tools = [{ googleSearch: {} }];\r\n }\r\n \r\n const result = await geminiModel.generateContent(requestParams);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n throw new Error('Gemini API returned empty response');\r\n }\r\n\r\n return text;\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Gemini API call failed: ${error.message}`);\r\n }\r\n throw new Error('Gemini API call failed with unknown error');\r\n }\r\n}\r\n","/**\r\n * 使用 Google Search grounding 验证商品名称\r\n */\r\n\r\nimport { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ReceiptItem } from '../types.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 构建批量验证 prompt\r\n */\r\nfunction buildVerificationPrompt(items: Partial<ReceiptItem>[]): string {\r\n const itemList = items\r\n .map((item, index) => `${index + 1}. \"${item.name}\"`)\r\n .join('\\n');\r\n\r\n return `这是从 Costco 超市小票中识别出的商品名称,部分名称可能不完整或有缩写。\r\n请使用 Google Search 查找并补全这些商品的完整正确名称。\r\n\r\n需要验证的商品:\r\n${itemList}\r\n\r\n验证方法建议:\r\n- 在搜索引擎中输入:商品原名 + \"Costco\"(例如:\"CEMOI 6X Costco\")\r\n- 这样能更准确地找到 Costco 销售的对应商品\r\n- 注意确认商品的包装规格(如数量、容量等)\r\n\r\n请返回 JSON 数组格式,每个商品包含:\r\n- index: 序号(1-based)\r\n- originalName: 原始名称\r\n- verifiedName: 验证后的完整名称(如果找到)\r\n- found: 是否找到匹配(布尔值)\r\n\r\n示例输出:\r\n[\r\n {\"index\": 1, \"originalName\": \"ORG MLK\", \"verifiedName\": \"Kirkland Signature Organic 2% Milk 1L\", \"found\": true},\r\n {\"index\": 2, \"originalName\": \"CEMΟΙ 6Χ\", \"verifiedName\": \"CEMOI 82% Dark Chocolate Bars, 6 × 100 g\", \"found\": true}\r\n]\r\n\r\n只返回 JSON 数组,不要其他文字。`;\r\n}\r\n\r\n/**\r\n * 解析验证响应\r\n */\r\ninterface VerificationResult {\r\n index: number;\r\n originalName: string;\r\n verifiedName: string;\r\n found: boolean;\r\n}\r\n\r\nfunction parseVerificationResponse(responseText: string): VerificationResult[] {\r\n try {\r\n // 移除可能的 markdown 代码块标记\r\n let cleaned = responseText.trim();\r\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\r\n if (codeBlockMatch) {\r\n cleaned = codeBlockMatch[1].trim();\r\n }\r\n\r\n const parsed = JSON.parse(cleaned);\r\n \r\n if (!Array.isArray(parsed)) {\r\n throw new Error('Response is not an array');\r\n }\r\n\r\n return parsed;\r\n } catch (error) {\r\n console.error('Failed to parse verification response:', error);\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * 批量验证商品名称\r\n * 使用 Google Search grounding 查找完整商品名\r\n * \r\n * @param items - 需要验证的商品列表\r\n * @returns 验证结果映射 (原始名称 -> 验证后名称)\r\n */\r\nexport async function batchVerifyItems(\r\n items: Partial<ReceiptItem>[]\r\n): Promise<Map<string, string>> {\r\n if (items.length === 0) {\r\n return new Map();\r\n }\r\n\r\n const { apiKey, model } = getGeminiConfig();\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 构建验证 prompt\r\n const prompt = buildVerificationPrompt(items);\r\n\r\n try {\r\n // 使用 Google Search grounding\r\n // 注意:tools 应该直接作为 generateContent 的顶级参数,而不是嵌套在 config 里\r\n const result = await geminiModel.generateContent({\r\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\r\n tools: [{ googleSearch: {} }],\r\n } as any);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n console.warn('Verification returned empty response');\r\n return new Map();\r\n }\r\n\r\n // 解析验证结果\r\n const verificationResults = parseVerificationResponse(text);\r\n\r\n // 构建映射\r\n const resultMap = new Map<string, string>();\r\n verificationResults.forEach((result) => {\r\n if (result.found && result.verifiedName) {\r\n resultMap.set(result.originalName, result.verifiedName);\r\n }\r\n });\r\n\r\n return resultMap;\r\n } catch (error) {\r\n console.error('Batch verification failed:', error);\r\n return new Map();\r\n }\r\n}\r\n","/**\r\n * 小票商品提取的 Prompt 模板\r\n * \r\n * 该模板要求 LLM:\r\n * 1. 从小票图片中提取所有商品信息\r\n * 2. 直接判断每个商品名称是否需要验证(needsVerification)\r\n * 3. 识别附加费用(押金、折扣)并标记归属关系\r\n * 4. 返回结构化的 JSON 数组\r\n */\r\nexport const EXTRACTION_PROMPT = `分析这张购物小票图片,提取所有商品信息和总金额。\r\n\r\n输出格式为包含两个字段的 JSON 对象:\r\n{\r\n \"items\": [...], // 商品数组\r\n \"total\": 123.45 // 小票总金额\r\n}\r\n\r\n每个商品包含:\r\n- name: 商品名称(字符串)\r\n- price: 单价(数字)\r\n- quantity: 数量(数字,默认 1)\r\n- needsVerification: 是否需要验证(布尔值)\r\n- hasTax: 是否含税(布尔值)\r\n- taxAmount: 税额(数字,可选)\r\n\r\n**关于 hasTax 的判断规则(Costco 小票):**\r\n- **如果商品名称后面有 \"H\" 标记**(如 \"ORG MLK H\"、\"CEMOI 6X H\"),则 hasTax = true\r\n- **如果商品名称后面没有 \"H\" 标记**,则 hasTax = false\r\n- **重要**:提取商品名称时,请去掉末尾的 \"H\" 标记,只保留商品名称本身\r\n- 如果小票上有明确的税费金额,填写到 taxAmount 字段\r\n\r\n关于 needsVerification 的判断规则:\r\n**重要原则:宁可多验证,不要猜测。当不确定时,优先设为 true。**\r\n\r\n必须设为 true 的情况:\r\n- 商品名称是缩写(如 \"ORG MLK\"、\"VEG\"、\"FRZ\")\r\n- 商品名称不完整或被截断(如 \"CHOCO...\"、\"有机...\")\r\n- 包含数字或字母组合但含义不明确(如 \"CEMΟΙ 6Χ\"、\"KS 12X\")\r\n- 商品名称模糊或可能有多种解释\r\n- 商品名称包含品牌缩写或代码\r\n- 只有品类没有具体品名(如 \"面包\"、\"饮料\")\r\n- 商品名称中混杂了数字但不清楚具体规格(如 \"牛奶 2\")\r\n- 任何你不能100%确定完整含义的名称\r\n\r\n可以设为 false 的情况(必须同时满足以下所有条件):\r\n- 商品名称完整、清晰、无缩写\r\n- 包含完整的品牌和规格信息(如 \"可口可乐瓶装 330ml\")\r\n- 你能100%确定这个名称的准确含义\r\n- 普通消费者看到这个名称能立即理解是什么商品\r\n\r\n示例:\r\n- \"ORG MLK\" → needsVerification: true(缩写)\r\n- \"CEMΟΙ 6Χ\" → needsVerification: true(包含不明确的字符)\r\n- \"面包\" → needsVerification: true(只有品类,没有具体信息)\r\n- \"KS Milk\" → needsVerification: true(品牌缩写)\r\n- \"有机牛奶 Kirkland Signature 1L\" → needsVerification: false(完整清晰)\r\n- \"富士苹果\" → needsVerification: false(完整且明确)\r\n\r\n**重要:附加费用处理规则**\r\n对于押金(Deposit、deposit、押金等)和折扣(TPD、discount、折扣等)这类附加费用:\r\n- 添加额外字段 isAttachment: true\r\n- 添加 attachmentType: \"deposit\" 或 \"discount\"\r\n- **重要**:将附加费用紧跟在它所属的商品后面排列\r\n- 系统会自动将附加费用合并到它前面的商品中\r\n- 这些附加费用不会作为独立商品返回\r\n\r\n归属规则(按照这个顺序排列):\r\n- 商品A\r\n- 商品A的押金(如果有)\r\n- 商品A的折扣(如果有)\r\n- 商品B\r\n- 商品B的押金(如果有)\r\n- ...\r\n\r\n**关于 total(总金额)的提取规则:**\r\n- 从小票底部找到 \"TOTAL\"、\"总计\"、\"合计\" 等标记\r\n- 提取对应的金额数字\r\n- 这是小票的最终应付金额\r\n\r\n只返回 JSON 对象,不要其他文字。\r\n\r\n示例输出:\r\n假设小票上显示:\r\n- \"KS ORG MLK 1L\" (无 H) → 不含税,¥12.50\r\n- \"ORG BRD H\" → 含税,¥8.00(税¥0.80)\r\n- \"CEMΟΙ 6Χ H\" → 含税,¥15.00\r\n- \"KS Apple\" (无 H) → 不含税,¥4.50 x 3\r\n- TOTAL: ¥37.30\r\n\r\n则输出为:\r\n{\r\n \"items\": [\r\n {\"name\": \"KS ORG MLK 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": false},\r\n {\"name\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8},\r\n {\"name\": \"Deposit VL\", \"price\": 0.5, \"quantity\": 2, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"deposit\"},\r\n {\"name\": \"TPD\", \"price\": -0.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"discount\"},\r\n {\"name\": \"CEMΟΙ 6Χ\", \"price\": 15.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true},\r\n {\"name\": \"KS Apple\", \"price\": 4.5, \"quantity\": 3, \"needsVerification\": true, \"hasTax\": false}\r\n ],\r\n \"total\": 37.30\r\n}\r\n\r\n注意:\r\n1. 商品名称中已去掉 \"H\" 标记,但根据原小票上的 \"H\" 标记设置了正确的 hasTax 值\r\n2. total 是小票上显示的最终应付金额`;\r\n","import type { InternalReceiptItem } from '../types.js';\r\n\r\n/**\r\n * LLM 返回的原始商品数据结构\r\n */\r\ninterface RawReceiptItem {\r\n name: string;\r\n price: number;\r\n quantity: number; // 必填,normalizeRawItem 会提供默认值 1\r\n needsVerification: boolean;\r\n hasTax: boolean;\r\n taxAmount?: number;\r\n deposit?: number;\r\n discount?: number;\r\n // 附加费用标记(用于解析时的临时字段)\r\n isAttachment?: boolean;\r\n attachmentType?: 'deposit' | 'discount';\r\n attachedTo?: number;\r\n}\r\n\r\n/**\r\n * 从 LLM 响应中提取 JSON\r\n * 处理可能的 markdown 代码块包裹\r\n */\r\nfunction extractJson(text: string): string {\r\n // 移除可能的 markdown 代码块标记\r\n let cleaned = text.trim();\r\n \r\n // 匹配 ```json ... ``` 或 ``` ... ```\r\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\r\n if (codeBlockMatch) {\r\n cleaned = codeBlockMatch[1].trim();\r\n }\r\n \r\n return cleaned;\r\n}\r\n\r\n/**\r\n * 验证并规范化原始商品数据\r\n */\r\nfunction normalizeRawItem(raw: any): RawReceiptItem {\r\n if (!raw || typeof raw !== 'object') {\r\n throw new Error('Invalid item: not an object');\r\n }\r\n\r\n if (typeof raw.name !== 'string' || !raw.name) {\r\n throw new Error('Invalid item: missing or invalid name field');\r\n }\r\n\r\n if (typeof raw.price !== 'number' || raw.price < 0) {\r\n throw new Error('Invalid item: missing or invalid price field');\r\n }\r\n\r\n return {\r\n name: raw.name,\r\n price: raw.price,\r\n quantity: typeof raw.quantity === 'number' ? raw.quantity : 1,\r\n needsVerification: Boolean(raw.needsVerification),\r\n hasTax: Boolean(raw.hasTax),\r\n taxAmount: typeof raw.taxAmount === 'number' ? raw.taxAmount : undefined,\r\n deposit: typeof raw.deposit === 'number' ? raw.deposit : undefined,\r\n discount: typeof raw.discount === 'number' ? raw.discount : undefined,\r\n // 保留附加费用标记用于后续处理\r\n isAttachment: raw.isAttachment === true ? true : undefined,\r\n attachmentType: raw.attachmentType,\r\n attachedTo: typeof raw.attachedTo === 'number' ? raw.attachedTo : undefined,\r\n };\r\n}\r\n\r\n/**\r\n * 合并附加费用(押金、折扣)到对应的商品中\r\n * 使用位置关系:附加费用紧跟在对应商品后面\r\n * \r\n * @param items - 包含附加费用标记的商品列表\r\n * @returns 合并后的商品列表(不包含独立的附加费用项)\r\n */\r\nfunction mergeAttachments(items: RawReceiptItem[]): RawReceiptItem[] {\r\n const result: RawReceiptItem[] = [];\r\n let currentItem: RawReceiptItem | null = null;\r\n \r\n for (let i = 0; i < items.length; i++) {\r\n const item = items[i];\r\n \r\n if (!item.isAttachment) {\r\n // 如果之前有商品,先保存\r\n if (currentItem) {\r\n result.push(currentItem);\r\n }\r\n // 开始新商品(深拷贝)\r\n currentItem = { ...item };\r\n } else {\r\n // 这是附加费用,合并到当前商品\r\n if (currentItem) {\r\n if (item.attachmentType === 'deposit') {\r\n // 押金:累加(考虑数量)\r\n currentItem.deposit = (currentItem.deposit || 0) + (item.price * item.quantity);\r\n } else if (item.attachmentType === 'discount') {\r\n // 折扣:累加(通常已经是负数)\r\n currentItem.discount = (currentItem.discount || 0) + item.price;\r\n }\r\n }\r\n // 如果没有前置商品,跳过这个孤立的附加费用\r\n }\r\n }\r\n \r\n // 保存最后一个商品\r\n if (currentItem) {\r\n result.push(currentItem);\r\n }\r\n \r\n // 移除临时字段\r\n return result.map(item => {\r\n const { isAttachment, attachmentType, attachedTo, ...cleanItem } = item;\r\n return cleanItem;\r\n });\r\n}\r\n\r\n/**\r\n * 解析结果接口\r\n */\r\nexport interface ParsedReceiptData {\r\n items: InternalReceiptItem[];\r\n total: number;\r\n}\r\n\r\n/**\r\n * 解析 LLM 返回的 JSON 响应\r\n * \r\n * @param responseText - LLM 返回的文本响应\r\n * @returns 解析后的小票数据(包含商品数组和总金额)\r\n * @throws 如果解析失败\r\n */\r\nexport function parseResponse(responseText: string): ParsedReceiptData {\r\n try {\r\n // 提取 JSON\r\n const jsonText = extractJson(responseText);\r\n\r\n // 解析 JSON\r\n const parsed = JSON.parse(jsonText);\r\n\r\n // 验证响应格式\r\n if (!parsed || typeof parsed !== 'object') {\r\n throw new Error('Response is not an object');\r\n }\r\n\r\n // 提取 items 数组\r\n if (!Array.isArray(parsed.items)) {\r\n throw new Error('Response.items is not an array');\r\n }\r\n\r\n // 提取 total\r\n if (typeof parsed.total !== 'number' || parsed.total < 0) {\r\n throw new Error('Response.total is missing or invalid');\r\n }\r\n\r\n // 验证并规范化每个商品\r\n const items = parsed.items.map((raw: any, index: number) => {\r\n try {\r\n return normalizeRawItem(raw);\r\n } catch (error) {\r\n const message = error instanceof Error ? error.message : 'Unknown error';\r\n throw new Error(`Invalid item at index ${index}: ${message}`);\r\n }\r\n });\r\n\r\n // 合并附加费用到对应的商品\r\n const mergedItems = mergeAttachments(items);\r\n\r\n return {\r\n items: mergedItems,\r\n total: parsed.total,\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Failed to parse LLM response: ${error.message}\\n\\nResponse:\\n${responseText}`);\r\n }\r\n throw new Error(`Failed to parse LLM response with unknown error\\n\\nResponse:\\n${responseText}`);\r\n }\r\n}\r\n\r\n","import type { ImageInput, ExtractOptions, ReceiptItem, ReceiptData, VerificationContext } from './types.js';\r\nimport { callGemini } from './adapters/gemini.js';\r\nimport { batchVerifyItems } from './adapters/verifier.js';\r\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\r\nimport { parseResponse } from './processors/parser.js';\r\n\r\n/**\r\n * 从小票图片中提取商品数据和总金额\r\n * \r\n * 这是一个无状态的异步函数,每次调用独立执行。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @param options - 可选配置(包括验证回调)\r\n * @returns 小票数据(包含商品列表和总金额)\r\n * \r\n * @throws 如果环境变量 GEMINI_API_KEY 未设置\r\n * @throws 如果 API 调用失败\r\n * @throws 如果响应解析失败\r\n * \r\n * @example\r\n * ```typescript\r\n * // 基础用法\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * console.log(receipt.items); // 商品列表\r\n * console.log(receipt.total); // 总金额\r\n * \r\n * // 使用自动验证(默认已启用)\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * \r\n * // 禁用自动验证\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * autoVerify: false\r\n * });\r\n * \r\n * // 带自定义验证回调\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * verifyCallback: async (name, context) => {\r\n * const result = await myProductSearch(name);\r\n * return result ? { verifiedName: result.name } : null;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport async function extractReceiptItems(\r\n image: ImageInput,\r\n options?: ExtractOptions\r\n): Promise<ReceiptData> {\r\n // 1. 调用 Gemini API\r\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\r\n\r\n // 2. 解析响应\r\n const { items: parsedItems, total } = parseResponse(responseText);\r\n\r\n // 3. 处理需要验证的商品\r\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\r\n \r\n // 3a. 自动验证(使用 Google Search grounding)\r\n // 默认启用,除非显式设置为 false\r\n if (options?.autoVerify !== false && itemsNeedingVerification.length > 0) {\r\n try {\r\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\r\n \r\n // 应用验证结果\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n const verifiedName = verificationMap.get(item.name);\r\n if (verifiedName) {\r\n item.name = verifiedName;\r\n item.needsVerification = false;\r\n }\r\n // 如果未找到,保持原名称和 needsVerification=true\r\n }\r\n }\r\n } catch (error) {\r\n console.error('Auto verification failed:', error);\r\n // 失败时保持原始数据\r\n }\r\n }\r\n \r\n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\r\n if (options?.verifyCallback) {\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n try {\r\n // 每次调用时动态创建验证上下文,确保包含最新的验证状态\r\n // 创建深拷贝以防止外部修改内部数据\r\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({...item}));\r\n const verificationContext: VerificationContext = {\r\n rawText: responseText,\r\n allItems: publicItems,\r\n };\r\n \r\n const result = await options.verifyCallback(item.name, verificationContext);\r\n if (result && result.verifiedName) {\r\n // 更新商品名称\r\n item.name = result.verifiedName;\r\n // 验证成功后,标记为不再需要验证\r\n item.needsVerification = false;\r\n }\r\n } catch (error) {\r\n // 验证失败,静默忽略,保留原始数据\r\n console.error(`Verification failed for item \"${item.name}\":`, error);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // 4. 转换为公开类型:移除内部字段 needsVerification,创建完全独立的副本\r\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({\r\n ...item // 创建新对象,确保外部无法修改内部数据\r\n }));\r\n\r\n return {\r\n items: finalItems,\r\n total,\r\n };\r\n}\r\n"]}
1
+ {"version":3,"sources":["../src/processors/image.ts","../src/adapters/gemini.ts","../src/adapters/verifier.ts","../src/utils/prompt.ts","../src/processors/parser.ts","../src/extract.ts"],"names":["getGeminiConfig","GoogleGenerativeAI","result","item"],"mappings":";;;;;AAeA,SAAS,MAAM,KAAA,EAAwB;AACrC,EAAA,OAAO,MAAM,UAAA,CAAW,SAAS,CAAA,IAAK,KAAA,CAAM,WAAW,UAAU,CAAA;AACnE;AAKA,SAAS,SAAS,KAAA,EAAwB;AAExC,EAAA,IAAI,KAAA,CAAM,UAAA,CAAW,OAAO,CAAA,EAAG;AAC7B,IAAA,OAAO,IAAA;AAAA,EACT;AAEA,EAAA,OAAO,KAAA,CAAM,MAAA,GAAS,GAAA,IAAO,mBAAA,CAAoB,KAAK,KAAK,CAAA;AAC7D;AAKA,SAAS,aAAa,OAAA,EAAqD;AACzE,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,6BAA6B,CAAA;AACzD,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,MAAM,CAAC,CAAA;AAAA,MACjB,IAAA,EAAM,MAAM,CAAC;AAAA,KACf;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,YAAA;AAAA,IACV,IAAA,EAAM;AAAA,GACR;AACF;AAKA,SAAS,mBAAmB,GAAA,EAAqB;AAC/C,EAAA,MAAM,KAAA,GAAQ,IAAI,WAAA,EAAY;AAC9B,EAAA,IAAI,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,WAAA;AACnC,EAAA,IAAI,KAAA,CAAM,SAAS,MAAM,CAAA,IAAK,MAAM,QAAA,CAAS,OAAO,GAAG,OAAO,YAAA;AAC9D,EAAA,IAAI,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA,EAAG,OAAO,YAAA;AACpC,EAAA,IAAI,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,EAAG,OAAO,WAAA;AAEnC,EAAA,OAAO,YAAA;AACT;AAWA,eAAsB,aAAa,KAAA,EAA4C;AAE7E,EAAA,IAAI,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAG;AAC1B,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,YAAA;AAAA;AAAA,MACV,IAAA,EAAM,KAAA,CAAM,QAAA,CAAS,QAAQ;AAAA,KAC/B;AAAA,EACF;AAGA,EAAA,IAAI,OAAO,UAAU,QAAA,EAAU;AAE7B,IAAA,IAAI,KAAA,CAAM,KAAK,CAAA,EAAG;AAChB,MAAA,IAAI;AACF,QAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,KAAK,CAAA;AAClC,QAAA,IAAI,CAAC,SAAS,EAAA,EAAI;AAChB,UAAA,MAAM,IAAI,MAAM,CAAA,uBAAA,EAA0B,QAAA,CAAS,MAAM,CAAA,CAAA,EAAI,QAAA,CAAS,UAAU,CAAA,CAAE,CAAA;AAAA,QACpF;AACA,QAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,EAAY;AAC/C,QAAA,MAAM,aAAa,MAAA,CAAO,IAAA,CAAK,WAAW,CAAA,CAAE,SAAS,QAAQ,CAAA;AAE7D,QAAA,OAAO;AAAA,UACL,QAAA,EAAU,mBAAmB,KAAK,CAAA;AAAA,UAClC,IAAA,EAAM;AAAA,SACR;AAAA,MACF,SAAS,KAAA,EAAO;AACd,QAAA,IAAI,iBAAiB,KAAA,EAAO;AAC1B,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,gCAAA,EAAmC,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,QACpE;AACA,QAAA,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAAA,MAClD;AAAA,IACF;AAGA,IAAA,IAAI,KAAA,CAAM,UAAA,CAAW,OAAO,CAAA,EAAG;AAC7B,MAAA,MAAM,EAAE,QAAA,EAAU,IAAA,EAAK,GAAI,aAAa,KAAK,CAAA;AAC7C,MAAA,OAAO,EAAE,UAAU,IAAA,EAAK;AAAA,IAC1B;AAGA,IAAA,IAAI,QAAA,CAAS,KAAK,CAAA,EAAG;AACnB,MAAA,OAAO;AAAA,QACL,QAAA,EAAU,YAAA;AAAA,QACV,IAAA,EAAM;AAAA,OACR;AAAA,IACF;AAGA,IAAA,MAAM,IAAI,MAAM,+DAA+D,CAAA;AAAA,EACjF;AAEA,EAAA,MAAM,IAAI,MAAM,8BAA8B,CAAA;AAChD;;;ACpHA,SAAS,eAAA,GAAkB;AACzB,EAAA,MAAM,MAAA,GAAS,QAAQ,GAAA,CAAI,cAAA;AAC3B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,YAAA,IAAgB,kBAAA;AAE1C,EAAA,OAAO,EAAE,QAAQ,KAAA,EAAM;AACzB;AAUA,eAAsB,UAAA,CACpB,KAAA,EACA,MAAA,EACA,YAAA,EACiB;AACjB,EAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAM,GAAI,eAAA,EAAgB;AAG1C,EAAA,MAAM,KAAA,GAAQ,IAAI,kBAAA,CAAmB,MAAM,CAAA;AAC3C,EAAA,MAAM,WAAA,GAAc,KAAA,CAAM,kBAAA,CAAmB,EAAE,OAAO,CAAA;AAGtD,EAAA,MAAM,cAAA,GAAiB,MAAM,YAAA,CAAa,KAAK,CAAA;AAG/C,EAAA,MAAM,WAAW,EAAC;AAGlB,EAAA,QAAA,CAAS,IAAA,CAAK;AAAA,IACZ,UAAA,EAAY;AAAA,MACV,UAAU,cAAA,CAAe,QAAA;AAAA,MACzB,MAAM,cAAA,CAAe;AAAA;AACvB,GACD,CAAA;AAGD,EAAA,QAAA,CAAS,IAAA,CAAK;AAAA,IACZ,IAAA,EAAM;AAAA,GACP,CAAA;AAED,EAAA,IAAI;AAGF,IAAA,MAAM,aAAA,GAAqB;AAAA,MACzB,UAAU,CAAC,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,UAAU;AAAA,KAC9C;AAEA,IAAA,IAAI,YAAA,EAAc;AAIlB,IAAA,MAAM,MAAA,GAAS,MAAM,WAAA,CAAY,eAAA,CAAgB,aAAa,CAAA;AAE9D,IAAA,MAAM,WAAW,MAAA,CAAO,QAAA;AACxB,IAAA,MAAM,IAAA,GAAO,SAAS,IAAA,EAAK;AAE3B,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,MAAM,IAAI,MAAM,oCAAoC,CAAA;AAAA,IACtD;AAEA,IAAA,OAAO,IAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,IAAI,iBAAiB,KAAA,EAAO;AAC1B,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,wBAAA,EAA2B,KAAA,CAAM,OAAO,CAAA,CAAE,CAAA;AAAA,IAC5D;AACA,IAAA,MAAM,IAAI,MAAM,2CAA2C,CAAA;AAAA,EAC7D;AACF;AC3EA,SAASA,gBAAAA,GAAkB;AACzB,EAAA,MAAM,MAAA,GAAS,QAAQ,GAAA,CAAI,cAAA;AAC3B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACX,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,YAAA,IAAgB,kBAAA;AAE1C,EAAA,OAAO,EAAE,QAAQ,KAAA,EAAM;AACzB;AAKA,SAAS,wBAAwB,KAAA,EAAuC;AACtE,EAAA,MAAM,QAAA,GAAW,KAAA,CACd,GAAA,CAAI,CAAC,MAAM,KAAA,KAAU,CAAA,EAAG,KAAA,GAAQ,CAAC,MAAM,IAAA,CAAK,IAAI,CAAA,CAAA,CAAG,CAAA,CACnD,KAAK,IAAI,CAAA;AAEZ,EAAA,OAAO,CAAA;AAAA;;AAAA;AAAA,EAIP,QAAQ;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA,oFAAA,CAAA;AAoBV;AAYA,SAAS,0BAA0B,YAAA,EAA4C;AAC7E,EAAA,IAAI;AAEF,IAAA,IAAI,OAAA,GAAU,aAAa,IAAA,EAAK;AAChC,IAAA,MAAM,cAAA,GAAiB,OAAA,CAAQ,KAAA,CAAM,8BAA8B,CAAA;AACnE,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,OAAA,GAAU,cAAA,CAAe,CAAC,CAAA,CAAE,IAAA,EAAK;AAAA,IACnC;AAEA,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA;AAEjC,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG;AAC1B,MAAA,MAAM,IAAI,MAAM,0BAA0B,CAAA;AAAA,IAC5C;AAEA,IAAA,OAAO,MAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,0CAA0C,KAAK,CAAA;AAC7D,IAAA,OAAO,EAAC;AAAA,EACV;AACF;AASA,eAAsB,iBACpB,KAAA,EAC8B;AAC9B,EAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACtB,IAAA,2BAAW,GAAA,EAAI;AAAA,EACjB;AAEA,EAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAM,GAAIA,gBAAAA,EAAgB;AAC1C,EAAA,MAAM,KAAA,GAAQ,IAAIC,kBAAAA,CAAmB,MAAM,CAAA;AAC3C,EAAA,MAAM,WAAA,GAAc,KAAA,CAAM,kBAAA,CAAmB,EAAE,OAAO,CAAA;AAGtD,EAAA,MAAM,MAAA,GAAS,wBAAwB,KAAK,CAAA;AAE5C,EAAA,IAAI;AAGF,IAAA,MAAM,MAAA,GAAS,MAAM,WAAA,CAAY,eAAA,CAAgB;AAAA,MAC/C,QAAA,EAAU,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,KAAA,EAAO,CAAC,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAA,EAAG,CAAA;AAAA,MACtD,OAAO,CAAC,EAAE,YAAA,EAAc,IAAI;AAAA,KACtB,CAAA;AAER,IAAA,MAAM,WAAW,MAAA,CAAO,QAAA;AACxB,IAAA,MAAM,IAAA,GAAO,SAAS,IAAA,EAAK;AAE3B,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,OAAA,CAAQ,KAAK,sCAAsC,CAAA;AACnD,MAAA,2BAAW,GAAA,EAAI;AAAA,IACjB;AAGA,IAAA,MAAM,mBAAA,GAAsB,0BAA0B,IAAI,CAAA;AAG1D,IAAA,MAAM,SAAA,uBAAgB,GAAA,EAAoB;AAC1C,IAAA,mBAAA,CAAoB,OAAA,CAAQ,CAACC,OAAAA,KAAW;AACtC,MAAA,IAAIA,OAAAA,CAAO,KAAA,IAASA,OAAAA,CAAO,YAAA,EAAc;AACvC,QAAA,SAAA,CAAU,GAAA,CAAIA,OAAAA,CAAO,YAAA,EAAcA,OAAAA,CAAO,YAAY,CAAA;AAAA,MACxD;AAAA,IACF,CAAC,CAAA;AAED,IAAA,OAAO,SAAA;AAAA,EACT,SAAS,KAAA,EAAO;AACd,IAAA,OAAA,CAAQ,KAAA,CAAM,8BAA8B,KAAK,CAAA;AACjD,IAAA,2BAAW,GAAA,EAAI;AAAA,EACjB;AACF;;;ACrIO,IAAM,iBAAA,GAAoB,CAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA,uFAAA,CAAA;;;ACejC,SAAS,YAAY,IAAA,EAAsB;AAEzC,EAAA,IAAI,OAAA,GAAU,KAAK,IAAA,EAAK;AAGxB,EAAA,MAAM,cAAA,GAAiB,OAAA,CAAQ,KAAA,CAAM,8BAA8B,CAAA;AACnE,EAAA,IAAI,cAAA,EAAgB;AAClB,IAAA,OAAA,GAAU,cAAA,CAAe,CAAC,CAAA,CAAE,IAAA,EAAK;AAAA,EACnC;AAEA,EAAA,OAAO,OAAA;AACT;AAKA,SAAS,iBAAiB,GAAA,EAA0B;AAClD,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,QAAA,EAAU;AACnC,IAAA,MAAM,IAAI,MAAM,6BAA6B,CAAA;AAAA,EAC/C;AAEA,EAAA,IAAI,OAAO,GAAA,CAAI,IAAA,KAAS,QAAA,IAAY,CAAC,IAAI,IAAA,EAAM;AAC7C,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,EAC/D;AAEA,EAAA,IAAI,OAAO,GAAA,CAAI,KAAA,KAAU,QAAA,IAAY,GAAA,CAAI,QAAQ,CAAA,EAAG;AAClD,IAAA,MAAM,IAAI,MAAM,8CAA8C,CAAA;AAAA,EAChE;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,GAAA,CAAI,IAAA;AAAA,IACV,OAAO,GAAA,CAAI,KAAA;AAAA,IACX,UAAU,OAAO,GAAA,CAAI,QAAA,KAAa,QAAA,GAAW,IAAI,QAAA,GAAW,CAAA;AAAA,IAC5D,iBAAA,EAAmB,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA;AAAA,IAChD,MAAA,EAAQ,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAA;AAAA,IAC1B,WAAW,OAAO,GAAA,CAAI,SAAA,KAAc,QAAA,GAAW,IAAI,SAAA,GAAY,MAAA;AAAA,IAC/D,SAAS,OAAO,GAAA,CAAI,OAAA,KAAY,QAAA,GAAW,IAAI,OAAA,GAAU,MAAA;AAAA,IACzD,UAAU,OAAO,GAAA,CAAI,QAAA,KAAa,QAAA,GAAW,IAAI,QAAA,GAAW,MAAA;AAAA;AAAA,IAE5D,YAAA,EAAc,GAAA,CAAI,YAAA,KAAiB,IAAA,GAAO,IAAA,GAAO,MAAA;AAAA,IACjD,gBAAgB,GAAA,CAAI,cAAA;AAAA,IACpB,YAAY,OAAO,GAAA,CAAI,UAAA,KAAe,QAAA,GAAW,IAAI,UAAA,GAAa;AAAA,GACpE;AACF;AASA,SAAS,iBAAiB,KAAA,EAA2C;AACnE,EAAA,MAAM,SAA2B,EAAC;AAClC,EAAA,IAAI,WAAA,GAAqC,IAAA;AAEzC,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,KAAA,CAAM,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAM,IAAA,GAAO,MAAM,CAAC,CAAA;AAEpB,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AAEtB,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,MAAA,CAAO,KAAK,WAAW,CAAA;AAAA,MACzB;AAEA,MAAA,WAAA,GAAc,EAAE,GAAG,IAAA,EAAK;AAAA,IAC1B,CAAA,MAAO;AAEL,MAAA,IAAI,WAAA,EAAa;AACf,QAAA,IAAI,IAAA,CAAK,mBAAmB,SAAA,EAAW;AAErC,UAAA,WAAA,CAAY,WAAW,WAAA,CAAY,OAAA,IAAW,CAAA,IAAM,IAAA,CAAK,QAAQ,IAAA,CAAK,QAAA;AAAA,QACxE,CAAA,MAAA,IAAW,IAAA,CAAK,cAAA,KAAmB,UAAA,EAAY;AAE7C,UAAA,WAAA,CAAY,QAAA,GAAA,CAAY,WAAA,CAAY,QAAA,IAAY,CAAA,IAAK,IAAA,CAAK,KAAA;AAAA,QAC5D;AAAA,MACF;AAAA,IAEF;AAAA,EACF;AAGA,EAAA,IAAI,WAAA,EAAa;AACf,IAAA,MAAA,CAAO,KAAK,WAAW,CAAA;AAAA,EACzB;AAGA,EAAA,OAAO,MAAA,CAAO,IAAI,CAAA,IAAA,KAAQ;AACxB,IAAA,MAAM,EAAE,YAAA,EAAc,cAAA,EAAgB,UAAA,EAAY,GAAG,WAAU,GAAI,IAAA;AACnE,IAAA,OAAO,SAAA;AAAA,EACT,CAAC,CAAA;AACH;AAiBO,SAAS,cAAc,YAAA,EAAyC;AACrE,EAAA,IAAI;AAEF,IAAA,MAAM,QAAA,GAAW,YAAY,YAAY,CAAA;AAGzC,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,KAAA,CAAM,QAAQ,CAAA;AAGlC,IAAA,IAAI,CAAC,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,EAAU;AACzC,MAAA,MAAM,IAAI,MAAM,2BAA2B,CAAA;AAAA,IAC7C;AAGA,IAAA,IAAI,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAA,CAAO,KAAK,CAAA,EAAG;AAChC,MAAA,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAAA,IAClD;AAGA,IAAA,IAAI,OAAO,MAAA,CAAO,KAAA,KAAU,QAAA,IAAY,MAAA,CAAO,QAAQ,CAAA,EAAG;AACxD,MAAA,MAAM,IAAI,MAAM,sCAAsC,CAAA;AAAA,IACxD;AAGA,IAAA,MAAM,QAAQ,MAAA,CAAO,KAAA,CAAM,GAAA,CAAI,CAAC,KAAU,KAAA,KAAkB;AAC1D,MAAA,IAAI;AACF,QAAA,OAAO,iBAAiB,GAAG,CAAA;AAAA,MAC7B,SAAS,KAAA,EAAO;AACd,QAAA,MAAM,OAAA,GAAU,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,eAAA;AACzD,QAAA,MAAM,IAAI,KAAA,CAAM,CAAA,sBAAA,EAAyB,KAAK,CAAA,EAAA,EAAK,OAAO,CAAA,CAAE,CAAA;AAAA,MAC9D;AAAA,IACF,CAAC,CAAA;AAGD,IAAA,MAAM,WAAA,GAAc,iBAAiB,KAAK,CAAA;AAE1C,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,WAAA;AAAA,MACP,OAAO,MAAA,CAAO;AAAA,KAChB;AAAA,EACF,SAAS,KAAA,EAAO;AACd,IAAA,IAAI,iBAAiB,KAAA,EAAO;AAC1B,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,8BAAA,EAAiC,KAAA,CAAM,OAAO;;AAAA;AAAA,EAAkB,YAAY,CAAA,CAAE,CAAA;AAAA,IAChG;AACA,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA;;AAAA;AAAA,EAAiE,YAAY,CAAA,CAAE,CAAA;AAAA,EACjG;AACF;;;ACvIA,eAAsB,mBAAA,CACpB,OACA,OAAA,EACsB;AAEtB,EAAA,MAAM,YAAA,GAAe,MAAM,UAAA,CAAW,KAAA,EAAO,iBAAiB,CAAA;AAG9D,EAAA,MAAM,EAAE,KAAA,EAAO,WAAA,EAAa,KAAA,EAAM,GAAI,cAAc,YAAY,CAAA;AAGhE,EAAA,MAAM,wBAAA,GAA2B,WAAA,CAAY,MAAA,CAAO,CAAA,IAAA,KAAQ,KAAK,iBAAiB,CAAA;AAIlF,EAAA,IAAI,OAAA,EAAS,UAAA,KAAe,KAAA,IAAS,wBAAA,CAAyB,SAAS,CAAA,EAAG;AACxE,IAAA,IAAI;AACF,MAAA,MAAM,eAAA,GAAkB,MAAM,gBAAA,CAAiB,wBAAwB,CAAA;AAGvE,MAAA,KAAA,MAAW,QAAQ,WAAA,EAAa;AAC9B,QAAA,IAAI,KAAK,iBAAA,EAAmB;AAC1B,UAAA,MAAM,YAAA,GAAe,eAAA,CAAgB,GAAA,CAAI,IAAA,CAAK,IAAI,CAAA;AAClD,UAAA,IAAI,YAAA,EAAc;AAChB,YAAA,IAAA,CAAK,IAAA,GAAO,YAAA;AACZ,YAAA,IAAA,CAAK,iBAAA,GAAoB,KAAA;AAAA,UAC3B;AAAA,QAEF;AAAA,MACF;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,KAAA,CAAM,6BAA6B,KAAK,CAAA;AAAA,IAElD;AAAA,EACF;AAGA,EAAA,IAAI,SAAS,cAAA,EAAgB;AAC3B,IAAA,KAAA,MAAW,QAAQ,WAAA,EAAa;AAC9B,MAAA,IAAI,KAAK,iBAAA,EAAmB;AAC1B,QAAA,IAAI;AAGF,UAAA,MAAM,WAAA,GAA6B,WAAA,CAAY,GAAA,CAAI,CAAC,EAAE,iBAAA,EAAmB,GAAGC,KAAAA,EAAK,MAAO,EAAC,GAAGA,KAAAA,EAAI,CAAE,CAAA;AAClG,UAAA,MAAM,mBAAA,GAA2C;AAAA,YAC/C,OAAA,EAAS,YAAA;AAAA,YACT,QAAA,EAAU;AAAA,WACZ;AAEA,UAAA,MAAM,SAAS,MAAM,OAAA,CAAQ,cAAA,CAAe,IAAA,CAAK,MAAM,mBAAmB,CAAA;AAC1E,UAAA,IAAI,MAAA,IAAU,OAAO,YAAA,EAAc;AAEjC,YAAA,IAAA,CAAK,OAAO,MAAA,CAAO,YAAA;AAEnB,YAAA,IAAA,CAAK,iBAAA,GAAoB,KAAA;AAAA,UAC3B;AAAA,QACF,SAAS,KAAA,EAAO;AAEd,UAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiC,IAAA,CAAK,IAAI,MAAM,KAAK,CAAA;AAAA,QACrE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,EAAA,MAAM,UAAA,GAA4B,YAAY,GAAA,CAAI,CAAC,EAAE,iBAAA,EAAmB,GAAG,MAAK,MAAO;AAAA,IACrF,GAAG;AAAA;AAAA,GACL,CAAE,CAAA;AAEF,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,UAAA;AAAA,IACP;AAAA,GACF;AACF","file":"index.js","sourcesContent":["import type { ImageInput } from '../types.js';\r\n\r\n/**\r\n * 图片数据接口,用于传递给 Gemini API\r\n */\r\nexport interface ProcessedImage {\r\n /** 图片的 MIME 类型 */\r\n mimeType: string;\r\n /** base64 编码的图片数据(不包含 data URI 前缀) */\r\n data: string;\r\n}\r\n\r\n/**\r\n * 检测是否为 URL\r\n */\r\nfunction isUrl(input: string): boolean {\r\n return input.startsWith('http://') || input.startsWith('https://');\r\n}\r\n\r\n/**\r\n * 检测是否为 base64 字符串\r\n */\r\nfunction isBase64(input: string): boolean {\r\n // 简单检测:如果以 data: 开头,或者看起来像 base64\r\n if (input.startsWith('data:')) {\r\n return true;\r\n }\r\n // Base64 字符串通常很长,且只包含特定字符\r\n return input.length > 100 && /^[A-Za-z0-9+/=]+$/.test(input);\r\n}\r\n\r\n/**\r\n * 从 data URI 中提取 MIME 类型和数据\r\n */\r\nfunction parseDataUri(dataUri: string): { mimeType: string; data: string } {\r\n const match = dataUri.match(/^data:([^;,]+);base64,(.+)$/);\r\n if (match) {\r\n return {\r\n mimeType: match[1],\r\n data: match[2],\r\n };\r\n }\r\n // 如果没有匹配到,假设是纯 base64,默认 MIME 类型\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: dataUri,\r\n };\r\n}\r\n\r\n/**\r\n * 从文件扩展名推断 MIME 类型\r\n */\r\nfunction getMimeTypeFromUrl(url: string): string {\r\n const lower = url.toLowerCase();\r\n if (lower.includes('.png')) return 'image/png';\r\n if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';\r\n if (lower.includes('.webp')) return 'image/webp';\r\n if (lower.includes('.gif')) return 'image/gif';\r\n // 默认\r\n return 'image/jpeg';\r\n}\r\n\r\n/**\r\n * 处理图片输入,转换为 Gemini API 可用的格式\r\n * \r\n * 根据官方文档,对于 URL 图片,需要先 fetch 获取数据,再转换为 base64。\r\n * Gemini API 只接受 inlineData(base64)或通过 File API 上传的文件 URI。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport async function processImage(image: ImageInput): Promise<ProcessedImage> {\r\n // 如果是 Buffer\r\n if (Buffer.isBuffer(image)) {\r\n return {\r\n mimeType: 'image/jpeg', // 默认,实际中可能需要更精确的检测\r\n data: image.toString('base64'),\r\n };\r\n }\r\n\r\n // 如果是字符串\r\n if (typeof image === 'string') {\r\n // URL - 需要 fetch 并转换为 base64\r\n if (isUrl(image)) {\r\n try {\r\n const response = await fetch(image);\r\n if (!response.ok) {\r\n throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);\r\n }\r\n const arrayBuffer = await response.arrayBuffer();\r\n const base64Data = Buffer.from(arrayBuffer).toString('base64');\r\n \r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n data: base64Data,\r\n };\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Failed to fetch image from URL: ${error.message}`);\r\n }\r\n throw new Error('Failed to fetch image from URL');\r\n }\r\n }\r\n\r\n // data URI\r\n if (image.startsWith('data:')) {\r\n const { mimeType, data } = parseDataUri(image);\r\n return { mimeType, data };\r\n }\r\n\r\n // 纯 base64\r\n if (isBase64(image)) {\r\n return {\r\n mimeType: 'image/jpeg',\r\n data: image,\r\n };\r\n }\r\n\r\n // 无法识别,抛出错误\r\n throw new Error('Unsupported image format: string is not a valid URL or base64');\r\n }\r\n\r\n throw new Error('Unsupported image input type');\r\n}\r\n","import { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ImageInput } from '../types.js';\r\nimport { processImage } from '../processors/image.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 调用 Gemini API 进行图片分析\r\n * \r\n * @param image - 图片输入(Buffer、base64 或 URL)\r\n * @param prompt - 提示词\r\n * @param useGrounding - 是否使用 Google Search grounding\r\n * @returns LLM 返回的文本响应\r\n */\r\nexport async function callGemini(\r\n image: ImageInput,\r\n prompt: string,\r\n useGrounding?: boolean\r\n): Promise<string> {\r\n const { apiKey, model } = getGeminiConfig();\r\n\r\n // 初始化 Gemini API 客户端\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 处理图片(现在是异步的)\r\n const processedImage = await processImage(image);\r\n\r\n // 构建请求内容\r\n const contents = [];\r\n\r\n // 添加图片部分(统一使用 inlineData)\r\n contents.push({\r\n inlineData: {\r\n mimeType: processedImage.mimeType,\r\n data: processedImage.data,\r\n },\r\n });\r\n\r\n // 添加文本提示\r\n contents.push({\r\n text: prompt,\r\n });\r\n\r\n try {\r\n // 调用 Gemini API\r\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\r\n const requestParams: any = {\r\n contents: [{ role: 'user', parts: contents }],\r\n };\r\n \r\n if (useGrounding) {\r\n requestParams.tools = [{ googleSearch: {} }];\r\n }\r\n \r\n const result = await geminiModel.generateContent(requestParams);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n throw new Error('Gemini API returned empty response');\r\n }\r\n\r\n return text;\r\n } catch (error) {\r\n if (error instanceof Error) {\r\n throw new Error(`Gemini API call failed: ${error.message}`);\r\n }\r\n throw new Error('Gemini API call failed with unknown error');\r\n }\r\n}\r\n","/**\r\n * 使用 Google Search grounding 验证商品名称\r\n */\r\n\r\nimport { GoogleGenerativeAI } from '@google/generative-ai';\r\nimport type { ReceiptItem } from '../types.js';\r\n\r\n/**\r\n * 从环境变量读取 Gemini API 配置\r\n */\r\nfunction getGeminiConfig() {\r\n const apiKey = process.env.GEMINI_API_KEY;\r\n if (!apiKey) {\r\n throw new Error(\r\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\r\n );\r\n }\r\n\r\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\r\n\r\n return { apiKey, model };\r\n}\r\n\r\n/**\r\n * 构建批量验证 prompt\r\n */\r\nfunction buildVerificationPrompt(items: Partial<ReceiptItem>[]): string {\r\n const itemList = items\r\n .map((item, index) => `${index + 1}. \"${item.name}\"`)\r\n .join('\\n');\r\n\r\n return `这是从 Costco 超市小票中识别出的商品名称,部分名称可能不完整或有缩写。\r\n请使用 Google Search 查找并补全这些商品的完整正确名称。\r\n\r\n需要验证的商品:\r\n${itemList}\r\n\r\n验证方法建议:\r\n- 在搜索引擎中输入:商品原名 + \"Costco\"(例如:\"CEMOI 6X Costco\")\r\n- 这样能更准确地找到 Costco 销售的对应商品\r\n- 注意确认商品的包装规格(如数量、容量等)\r\n\r\n请返回 JSON 数组格式,每个商品包含:\r\n- index: 序号(1-based)\r\n- originalName: 原始名称\r\n- verifiedName: 验证后的完整名称(如果找到)\r\n- found: 是否找到匹配(布尔值)\r\n\r\n示例输出:\r\n[\r\n {\"index\": 1, \"originalName\": \"ORG MLK\", \"verifiedName\": \"Kirkland Signature Organic 2% Milk 1L\", \"found\": true},\r\n {\"index\": 2, \"originalName\": \"CEMΟΙ 6Χ\", \"verifiedName\": \"CEMOI 82% Dark Chocolate Bars, 6 × 100 g\", \"found\": true}\r\n]\r\n\r\n只返回 JSON 数组,不要其他文字。`;\r\n}\r\n\r\n/**\r\n * 解析验证响应\r\n */\r\ninterface VerificationResult {\r\n index: number;\r\n originalName: string;\r\n verifiedName: string;\r\n found: boolean;\r\n}\r\n\r\nfunction parseVerificationResponse(responseText: string): VerificationResult[] {\r\n try {\r\n // 移除可能的 markdown 代码块标记\r\n let cleaned = responseText.trim();\r\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\r\n if (codeBlockMatch) {\r\n cleaned = codeBlockMatch[1].trim();\r\n }\r\n\r\n const parsed = JSON.parse(cleaned);\r\n \r\n if (!Array.isArray(parsed)) {\r\n throw new Error('Response is not an array');\r\n }\r\n\r\n return parsed;\r\n } catch (error) {\r\n console.error('Failed to parse verification response:', error);\r\n return [];\r\n }\r\n}\r\n\r\n/**\r\n * 批量验证商品名称\r\n * 使用 Google Search grounding 查找完整商品名\r\n * \r\n * @param items - 需要验证的商品列表\r\n * @returns 验证结果映射 (原始名称 -> 验证后名称)\r\n */\r\nexport async function batchVerifyItems(\r\n items: Partial<ReceiptItem>[]\r\n): Promise<Map<string, string>> {\r\n if (items.length === 0) {\r\n return new Map();\r\n }\r\n\r\n const { apiKey, model } = getGeminiConfig();\r\n const genAI = new GoogleGenerativeAI(apiKey);\r\n const geminiModel = genAI.getGenerativeModel({ model });\r\n\r\n // 构建验证 prompt\r\n const prompt = buildVerificationPrompt(items);\r\n\r\n try {\r\n // 使用 Google Search grounding\r\n // 注意:tools 应该直接作为 generateContent 的顶级参数,而不是嵌套在 config 里\r\n const result = await geminiModel.generateContent({\r\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\r\n tools: [{ googleSearch: {} }],\r\n } as any);\r\n\r\n const response = result.response;\r\n const text = response.text();\r\n\r\n if (!text) {\r\n console.warn('Verification returned empty response');\r\n return new Map();\r\n }\r\n\r\n // 解析验证结果\r\n const verificationResults = parseVerificationResponse(text);\r\n\r\n // 构建映射\r\n const resultMap = new Map<string, string>();\r\n verificationResults.forEach((result) => {\r\n if (result.found && result.verifiedName) {\r\n resultMap.set(result.originalName, result.verifiedName);\r\n }\r\n });\r\n\r\n return resultMap;\r\n } catch (error) {\r\n console.error('Batch verification failed:', error);\r\n return new Map();\r\n }\r\n}\r\n","/**\n * 小票商品提取的 Prompt 模板\n * \n * 该模板要求 LLM:\n * 1. 从小票图片中提取所有商品信息\n * 2. 直接判断每个商品名称是否需要验证(needsVerification)\n * 3. 识别附加费用(押金、折扣)并标记归属关系\n * 4. 返回结构化的 JSON 数组\n */\nexport const EXTRACTION_PROMPT = `分析这张购物小票图片,提取所有商品信息和总金额。\n\n输出格式为包含两个字段的 JSON 对象:\n{\n \"items\": [...], // 商品数组\n \"total\": 123.45 // 小票总金额\n}\n\n每个商品包含:\n- name: 商品名称(字符串)\n- price: 单价(数字)\n- quantity: 数量(数字,默认 1)\n- needsVerification: 是否需要验证(布尔值)\n- hasTax: 是否含税(布尔值)\n- taxAmount: 税额(数字,可选)\n\n**关于 hasTax 的判断规则(Costco 小票):**\n- **如果商品名称后面有 \"H\" 标记**(如 \"ORG MLK H\"、\"CEMOI 6X H\"),则 hasTax = true\n- **如果商品名称后面没有 \"H\" 标记**,则 hasTax = false\n- **重要**:提取商品名称时,请去掉末尾的 \"H\" 标记,只保留商品名称本身\n- 如果小票上有明确的税费金额,填写到 taxAmount 字段\n\n关于 needsVerification 的判断规则:\n**重要原则:宁可多验证,不要猜测。当不确定时,优先设为 true。**\n\n必须设为 true 的情况:\n- 商品名称是缩写(如 \"ORG MLK\"、\"VEG\"、\"FRZ\")\n- 商品名称不完整或被截断(如 \"CHOCO...\"、\"有机...\")\n- 包含数字或字母组合但含义不明确(如 \"CEMΟΙ 6Χ\"、\"KS 12X\")\n- 商品名称模糊或可能有多种解释\n- 商品名称包含品牌缩写或代码\n- 只有品类没有具体品名(如 \"面包\"、\"饮料\")\n- 商品名称中混杂了数字但不清楚具体规格(如 \"牛奶 2\")\n- 任何你不能100%确定完整含义的名称\n\n可以设为 false 的情况(必须同时满足以下所有条件):\n- 商品名称完整、清晰、无缩写\n- 包含完整的品牌和规格信息(如 \"可口可乐瓶装 330ml\")\n- 你能100%确定这个名称的准确含义\n- 普通消费者看到这个名称能立即理解是什么商品\n\n示例:\n- \"ORG MLK\" → needsVerification: true(缩写)\n- \"CEMΟΙ 6Χ\" → needsVerification: true(包含不明确的字符)\n- \"面包\" → needsVerification: true(只有品类,没有具体信息)\n- \"KS Milk\" → needsVerification: true(品牌缩写)\n- \"有机牛奶 Kirkland Signature 1L\" → needsVerification: false(完整清晰)\n- \"富士苹果\" → needsVerification: false(完整且明确)\n\n**重要:附加费用处理规则**\n对于押金(Deposit、deposit、押金等)和折扣(TPD、discount、折扣等)这类附加费用:\n- 添加额外字段 isAttachment: true\n- 添加 attachmentType: \"deposit\" 或 \"discount\"\n- **价格格式要求**:\n - 押金的 price 字段必须是**正数**(如 0.5)\n - 折扣的 price 字段也必须是**正数**(如 0.5,而不是 -0.5)\n - 系统会自动将折扣金额处理为负值,你只需输出折扣的绝对值\n- **重要**:将附加费用紧跟在它所属的商品后面排列\n- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n**关于 total(总金额)的提取规则:**\n- 从小票底部找到 \"TOTAL\"、\"总计\"、\"合计\" 等标记\n- 提取对应的金额数字\n- 这是小票的最终应付金额\n\n**输出格式要求(极其重要)**:\n1. 只返回 JSON 对象,不要任何其他文字、解释或markdown标记\n2. 所有 price 字段必须是**正数**,包括折扣项\n3. 严格遵循上述字段定义,不要添加额外字段\n4. 确保 JSON 格式正确,可以被直接解析\n\n示例输出:\n假设小票上显示:\n- \"KS ORG MLK 1L\" (无 H) → 不含税,¥12.50\n- \"ORG BRD H\" → 含税,¥8.00(税¥0.80)\n- \"CEMΟΙ 6Χ H\" → 含税,¥15.00\n- \"KS Apple\" (无 H) → 不含税,¥4.50 x 3\n- TOTAL: ¥37.30\n\n则输出为:\n{\n \"items\": [\n {\"name\": \"KS ORG MLK 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": false},\n {\"name\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8},\n {\"name\": \"Deposit VL\", \"price\": 0.5, \"quantity\": 2, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"deposit\"},\n {\"name\": \"TPD\", \"price\": 0.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false, \"isAttachment\": true, \"attachmentType\": \"discount\"},\n {\"name\": \"CEMΟΙ 6Χ\", \"price\": 15.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true},\n {\"name\": \"KS Apple\", \"price\": 4.5, \"quantity\": 3, \"needsVerification\": true, \"hasTax\": false}\n ],\n \"total\": 37.30\n}\n\n注意:\n1. 商品名称中已去掉 \"H\" 标记,但根据原小票上的 \"H\" 标记设置了正确的 hasTax 值\n2. TPD 折扣项的 price 是**正数 0.5**(不是 -0.5),系统会自动处理为负值\n3. 所有附加费用(押金、折扣)的 price 都必须是正数\n4. total 是小票上显示的最终应付金额`;\n","import type { InternalReceiptItem } from '../types.js';\n\n/**\n * LLM 返回的原始商品数据结构\n */\ninterface RawReceiptItem {\n name: string;\n price: number;\n quantity: number; // 必填,normalizeRawItem 会提供默认值 1\n needsVerification: boolean;\n hasTax: boolean;\n taxAmount?: number;\n deposit?: number;\n discount?: number;\n // 附加费用标记(用于解析时的临时字段)\n isAttachment?: boolean;\n attachmentType?: 'deposit' | 'discount';\n attachedTo?: number;\n}\n\n/**\n * 从 LLM 响应中提取 JSON\n * 处理可能的 markdown 代码块包裹\n */\nfunction extractJson(text: string): string {\n // 移除可能的 markdown 代码块标记\n let cleaned = text.trim();\n \n // 匹配 ```json ... ``` 或 ``` ... ```\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (codeBlockMatch) {\n cleaned = codeBlockMatch[1].trim();\n }\n \n return cleaned;\n}\n\n/**\n * 验证并规范化原始商品数据\n */\nfunction normalizeRawItem(raw: any): RawReceiptItem {\n if (!raw || typeof raw !== 'object') {\n throw new Error('Invalid item: not an object');\n }\n\n if (typeof raw.name !== 'string' || !raw.name) {\n throw new Error('Invalid item: missing or invalid name field');\n }\n\n if (typeof raw.price !== 'number' || raw.price < 0) {\n throw new Error('Invalid item: missing or invalid price field');\n }\n\n return {\n name: raw.name,\n price: raw.price,\n quantity: typeof raw.quantity === 'number' ? raw.quantity : 1,\n needsVerification: Boolean(raw.needsVerification),\n hasTax: Boolean(raw.hasTax),\n taxAmount: typeof raw.taxAmount === 'number' ? raw.taxAmount : undefined,\n deposit: typeof raw.deposit === 'number' ? raw.deposit : undefined,\n discount: typeof raw.discount === 'number' ? raw.discount : undefined,\n // 保留附加费用标记用于后续处理\n isAttachment: raw.isAttachment === true ? true : undefined,\n attachmentType: raw.attachmentType,\n attachedTo: typeof raw.attachedTo === 'number' ? raw.attachedTo : undefined,\n };\n}\n\n/**\n * 合并附加费用(押金、折扣)到对应的商品中\n * 使用位置关系:附加费用紧跟在对应商品后面\n * \n * @param items - 包含附加费用标记的商品列表\n * @returns 合并后的商品列表(不包含独立的附加费用项)\n */\nfunction mergeAttachments(items: RawReceiptItem[]): RawReceiptItem[] {\n const result: RawReceiptItem[] = [];\n let currentItem: RawReceiptItem | null = null;\n \n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n \n if (!item.isAttachment) {\n // 如果之前有商品,先保存\n if (currentItem) {\n result.push(currentItem);\n }\n // 开始新商品(深拷贝)\n currentItem = { ...item };\n } else {\n // 这是附加费用,合并到当前商品\n if (currentItem) {\n if (item.attachmentType === 'deposit') {\n // 押金:累加(考虑数量)\n currentItem.deposit = (currentItem.deposit || 0) + (item.price * item.quantity);\n } else if (item.attachmentType === 'discount') {\n // 折扣:LLM 输出正数,我们转为负数累加\n currentItem.discount = (currentItem.discount || 0) - item.price;\n }\n }\n // 如果没有前置商品,跳过这个孤立的附加费用\n }\n }\n \n // 保存最后一个商品\n if (currentItem) {\n result.push(currentItem);\n }\n \n // 移除临时字段\n return result.map(item => {\n const { isAttachment, attachmentType, attachedTo, ...cleanItem } = item;\n return cleanItem;\n });\n}\n\n/**\n * 解析结果接口\n */\nexport interface ParsedReceiptData {\n items: InternalReceiptItem[];\n total: number;\n}\n\n/**\n * 解析 LLM 返回的 JSON 响应\n * \n * @param responseText - LLM 返回的文本响应\n * @returns 解析后的小票数据(包含商品数组和总金额)\n * @throws 如果解析失败\n */\nexport function parseResponse(responseText: string): ParsedReceiptData {\n try {\n // 提取 JSON\n const jsonText = extractJson(responseText);\n\n // 解析 JSON\n const parsed = JSON.parse(jsonText);\n\n // 验证响应格式\n if (!parsed || typeof parsed !== 'object') {\n throw new Error('Response is not an object');\n }\n\n // 提取 items 数组\n if (!Array.isArray(parsed.items)) {\n throw new Error('Response.items is not an array');\n }\n\n // 提取 total\n if (typeof parsed.total !== 'number' || parsed.total < 0) {\n throw new Error('Response.total is missing or invalid');\n }\n\n // 验证并规范化每个商品\n const items = parsed.items.map((raw: any, index: number) => {\n try {\n return normalizeRawItem(raw);\n } catch (error) {\n const message = error instanceof Error ? error.message : 'Unknown error';\n throw new Error(`Invalid item at index ${index}: ${message}`);\n }\n });\n\n // 合并附加费用到对应的商品\n const mergedItems = mergeAttachments(items);\n\n return {\n items: mergedItems,\n total: parsed.total,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to parse LLM response: ${error.message}\\n\\nResponse:\\n${responseText}`);\n }\n throw new Error(`Failed to parse LLM response with unknown error\\n\\nResponse:\\n${responseText}`);\n }\n}\n\n","import type { ImageInput, ExtractOptions, ReceiptItem, ReceiptData, VerificationContext } from './types.js';\r\nimport { callGemini } from './adapters/gemini.js';\r\nimport { batchVerifyItems } from './adapters/verifier.js';\r\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\r\nimport { parseResponse } from './processors/parser.js';\r\n\r\n/**\r\n * 从小票图片中提取商品数据和总金额\r\n * \r\n * 这是一个无状态的异步函数,每次调用独立执行。\r\n * \r\n * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @param options - 可选配置(包括验证回调)\r\n * @returns 小票数据(包含商品列表和总金额)\r\n * \r\n * @throws 如果环境变量 GEMINI_API_KEY 未设置\r\n * @throws 如果 API 调用失败\r\n * @throws 如果响应解析失败\r\n * \r\n * @example\r\n * ```typescript\r\n * // 基础用法\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * console.log(receipt.items); // 商品列表\r\n * console.log(receipt.total); // 总金额\r\n * \r\n * // 使用自动验证(默认已启用)\r\n * const receipt = await extractReceiptItems(imageBuffer);\r\n * \r\n * // 禁用自动验证\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * autoVerify: false\r\n * });\r\n * \r\n * // 带自定义验证回调\r\n * const receipt = await extractReceiptItems(imageBuffer, {\r\n * verifyCallback: async (name, context) => {\r\n * const result = await myProductSearch(name);\r\n * return result ? { verifiedName: result.name } : null;\r\n * }\r\n * });\r\n * ```\r\n */\r\nexport async function extractReceiptItems(\r\n image: ImageInput,\r\n options?: ExtractOptions\r\n): Promise<ReceiptData> {\r\n // 1. 调用 Gemini API\r\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\r\n\r\n // 2. 解析响应\r\n const { items: parsedItems, total } = parseResponse(responseText);\r\n\r\n // 3. 处理需要验证的商品\r\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\r\n \r\n // 3a. 自动验证(使用 Google Search grounding)\r\n // 默认启用,除非显式设置为 false\r\n if (options?.autoVerify !== false && itemsNeedingVerification.length > 0) {\r\n try {\r\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\r\n \r\n // 应用验证结果\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n const verifiedName = verificationMap.get(item.name);\r\n if (verifiedName) {\r\n item.name = verifiedName;\r\n item.needsVerification = false;\r\n }\r\n // 如果未找到,保持原名称和 needsVerification=true\r\n }\r\n }\r\n } catch (error) {\r\n console.error('Auto verification failed:', error);\r\n // 失败时保持原始数据\r\n }\r\n }\r\n \r\n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\r\n if (options?.verifyCallback) {\r\n for (const item of parsedItems) {\r\n if (item.needsVerification) {\r\n try {\r\n // 每次调用时动态创建验证上下文,确保包含最新的验证状态\r\n // 创建深拷贝以防止外部修改内部数据\r\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({...item}));\r\n const verificationContext: VerificationContext = {\r\n rawText: responseText,\r\n allItems: publicItems,\r\n };\r\n \r\n const result = await options.verifyCallback(item.name, verificationContext);\r\n if (result && result.verifiedName) {\r\n // 更新商品名称\r\n item.name = result.verifiedName;\r\n // 验证成功后,标记为不再需要验证\r\n item.needsVerification = false;\r\n }\r\n } catch (error) {\r\n // 验证失败,静默忽略,保留原始数据\r\n console.error(`Verification failed for item \"${item.name}\":`, error);\r\n }\r\n }\r\n }\r\n }\r\n\r\n // 4. 转换为公开类型:移除内部字段 needsVerification,创建完全独立的副本\r\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({\r\n ...item // 创建新对象,确保外部无法修改内部数据\r\n }));\r\n\r\n return {\r\n items: finalItems,\r\n total,\r\n };\r\n}\r\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "receipt-ocr",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "A TypeScript library for extracting structured product data from receipt images using multimodal LLMs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",