receipt-ocr 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -218,9 +218,31 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
218
218
  - taxAmount: \u7A0E\u989D\uFF08\u6570\u5B57\uFF0C\u53EF\u9009\uFF09
219
219
 
220
220
  \u5173\u4E8E needsVerification \u7684\u5224\u65AD\u89C4\u5219\uFF1A
221
- - \u5982\u679C\u5546\u54C1\u540D\u79F0\u662F\u7F29\u5199\u3001\u4E0D\u5B8C\u6574\u3001\u88AB\u622A\u65AD\u6216\u5B58\u5728\u6B67\u4E49\uFF0C\u8BBE\u4E3A true
222
- - \u5982\u679C\u5546\u54C1\u540D\u79F0\u6E05\u6670\u5B8C\u6574\uFF0C\u8BBE\u4E3A false
223
- - \u4E0D\u8981\u731C\u6D4B\u4E0D\u786E\u5B9A\u7684\u540D\u79F0\uFF0C\u800C\u662F\u4FDD\u7559\u539F\u6837\u5E76\u8BBE needsVerification \u4E3A true
221
+ **\u91CD\u8981\u539F\u5219\uFF1A\u5B81\u53EF\u591A\u9A8C\u8BC1\uFF0C\u4E0D\u8981\u731C\u6D4B\u3002\u5F53\u4E0D\u786E\u5B9A\u65F6\uFF0C\u4F18\u5148\u8BBE\u4E3A true\u3002**
222
+
223
+ \u5FC5\u987B\u8BBE\u4E3A true \u7684\u60C5\u51B5\uFF1A
224
+ - \u5546\u54C1\u540D\u79F0\u662F\u7F29\u5199\uFF08\u5982 "ORG MLK"\u3001"VEG"\u3001"FRZ"\uFF09
225
+ - \u5546\u54C1\u540D\u79F0\u4E0D\u5B8C\u6574\u6216\u88AB\u622A\u65AD\uFF08\u5982 "CHOCO..."\u3001"\u6709\u673A..."\uFF09
226
+ - \u5305\u542B\u6570\u5B57\u6216\u5B57\u6BCD\u7EC4\u5408\u4F46\u542B\u4E49\u4E0D\u660E\u786E\uFF08\u5982 "CEM\u039F\u0399 6\u03A7"\u3001"KS 12X"\uFF09
227
+ - \u5546\u54C1\u540D\u79F0\u6A21\u7CCA\u6216\u53EF\u80FD\u6709\u591A\u79CD\u89E3\u91CA
228
+ - \u5546\u54C1\u540D\u79F0\u5305\u542B\u54C1\u724C\u7F29\u5199\u6216\u4EE3\u7801
229
+ - \u53EA\u6709\u54C1\u7C7B\u6CA1\u6709\u5177\u4F53\u54C1\u540D\uFF08\u5982 "\u9762\u5305"\u3001"\u996E\u6599"\uFF09
230
+ - \u5546\u54C1\u540D\u79F0\u4E2D\u6DF7\u6742\u4E86\u6570\u5B57\u4F46\u4E0D\u6E05\u695A\u5177\u4F53\u89C4\u683C\uFF08\u5982 "\u725B\u5976 2"\uFF09
231
+ - \u4EFB\u4F55\u4F60\u4E0D\u80FD100%\u786E\u5B9A\u5B8C\u6574\u542B\u4E49\u7684\u540D\u79F0
232
+
233
+ \u53EF\u4EE5\u8BBE\u4E3A false \u7684\u60C5\u51B5\uFF08\u5FC5\u987B\u540C\u65F6\u6EE1\u8DB3\u4EE5\u4E0B\u6240\u6709\u6761\u4EF6\uFF09\uFF1A
234
+ - \u5546\u54C1\u540D\u79F0\u5B8C\u6574\u3001\u6E05\u6670\u3001\u65E0\u7F29\u5199
235
+ - \u5305\u542B\u5B8C\u6574\u7684\u54C1\u724C\u548C\u89C4\u683C\u4FE1\u606F\uFF08\u5982 "\u53EF\u53E3\u53EF\u4E50\u74F6\u88C5 330ml"\uFF09
236
+ - \u4F60\u80FD100%\u786E\u5B9A\u8FD9\u4E2A\u540D\u79F0\u7684\u51C6\u786E\u542B\u4E49
237
+ - \u666E\u901A\u6D88\u8D39\u8005\u770B\u5230\u8FD9\u4E2A\u540D\u79F0\u80FD\u7ACB\u5373\u7406\u89E3\u662F\u4EC0\u4E48\u5546\u54C1
238
+
239
+ \u793A\u4F8B\uFF1A
240
+ - "ORG MLK" \u2192 needsVerification: true\uFF08\u7F29\u5199\uFF09
241
+ - "CEM\u039F\u0399 6\u03A7" \u2192 needsVerification: true\uFF08\u5305\u542B\u4E0D\u660E\u786E\u7684\u5B57\u7B26\uFF09
242
+ - "\u9762\u5305" \u2192 needsVerification: true\uFF08\u53EA\u6709\u54C1\u7C7B\uFF0C\u6CA1\u6709\u5177\u4F53\u4FE1\u606F\uFF09
243
+ - "KS Milk" \u2192 needsVerification: true\uFF08\u54C1\u724C\u7F29\u5199\uFF09
244
+ - "\u6709\u673A\u725B\u5976 Kirkland Signature 1L" \u2192 needsVerification: false\uFF08\u5B8C\u6574\u6E05\u6670\uFF09
245
+ - "\u5BCC\u58EB\u82F9\u679C" \u2192 needsVerification: false\uFF08\u5B8C\u6574\u4E14\u660E\u786E\uFF09
224
246
 
225
247
  **\u91CD\u8981\uFF1A\u9644\u52A0\u8D39\u7528\u5904\u7406\u89C4\u5219**
226
248
  \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
@@ -242,11 +264,13 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
242
264
 
243
265
  \u793A\u4F8B\u8F93\u51FA\uFF1A
244
266
  [
245
- {"name": "\u6709\u673A\u725B\u5976 1L", "price": 12.5, "quantity": 1, "needsVerification": false, "hasTax": false},
246
- {"name": "\u53EF\u53E3\u53EF\u4E50\u74F6\u88C5", "price": 3.5, "quantity": 2, "needsVerification": false, "hasTax": true, "taxAmount": 0.35},
267
+ {"name": "Kirkland Signature \u6709\u673A\u725B\u5976 1L", "price": 12.5, "quantity": 1, "needsVerification": false, "hasTax": false},
268
+ {"name": "\u53EF\u53E3\u53EF\u4E50", "price": 3.5, "quantity": 2, "needsVerification": true, "hasTax": true, "taxAmount": 0.35},
247
269
  {"name": "Deposit VL", "price": 0.5, "quantity": 2, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "deposit"},
248
270
  {"name": "TPD", "price": -0.5, "quantity": 1, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "discount"},
249
- {"name": "ORG BRD", "price": 8.0, "quantity": 1, "needsVerification": true, "hasTax": true, "taxAmount": 0.8}
271
+ {"name": "ORG BRD", "price": 8.0, "quantity": 1, "needsVerification": true, "hasTax": true, "taxAmount": 0.8},
272
+ {"name": "CEM\u039F\u0399 6\u03A7", "price": 15.0, "quantity": 1, "needsVerification": true, "hasTax": false},
273
+ {"name": "KS Apple", "price": 4.5, "quantity": 3, "needsVerification": true, "hasTax": false}
250
274
  ]`;
251
275
 
252
276
  // src/processors/parser.ts
@@ -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"],"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 /** 图片 URL(如果输入是 URL) */\r\n url?: 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 * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport function processImage(image: ImageInput): 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\r\n if (isUrl(image)) {\r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n url: image,\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';\nimport type { ImageInput } from '../types.js';\nimport { processImage } from '../processors/image.js';\n\n/**\n * 从环境变量读取 Gemini API 配置\n */\nfunction getGeminiConfig() {\n const apiKey = process.env.GEMINI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\n );\n }\n\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\n\n return { apiKey, model };\n}\n\n/**\n * 调用 Gemini API 进行图片分析\n * \n * @param image - 图片输入(Buffer、base64 或 URL)\n * @param prompt - 提示词\n * @param useGrounding - 是否使用 Google Search grounding\n * @returns LLM 返回的文本响应\n */\nexport async function callGemini(\n image: ImageInput,\n prompt: string,\n useGrounding?: boolean\n): Promise<string> {\n const { apiKey, model } = getGeminiConfig();\n\n // 初始化 Gemini API 客户端\n const genAI = new GoogleGenerativeAI(apiKey);\n const geminiModel = genAI.getGenerativeModel({ model });\n\n // 处理图片\n const processedImage = processImage(image);\n\n // 构建请求内容\n const contents = [];\n\n // 添加图片部分\n if (processedImage.url) {\n // 如果是 URL,使用 fileData\n contents.push({\n fileData: {\n mimeType: processedImage.mimeType,\n fileUri: processedImage.url,\n },\n });\n } else if (processedImage.data) {\n // 如果是 base64 数据,使用 inlineData\n contents.push({\n inlineData: {\n mimeType: processedImage.mimeType,\n data: processedImage.data,\n },\n });\n }\n\n // 添加文本提示\n contents.push({\n text: prompt,\n });\n\n try {\n // 调用 Gemini API\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\n const requestParams: any = {\n contents: [{ role: 'user', parts: contents }],\n };\n \n if (useGrounding) {\n requestParams.tools = [{ googleSearch: {} }];\n }\n \n const result = await geminiModel.generateContent(requestParams);\n\n const response = result.response;\n const text = response.text();\n\n if (!text) {\n throw new Error('Gemini API returned empty response');\n }\n\n return text;\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Gemini API call failed: ${error.message}`);\n }\n throw new Error('Gemini API call failed with unknown error');\n }\n}\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- name: 商品名称(字符串)\n- price: 单价(数字)\n- quantity: 数量(数字,默认 1)\n- needsVerification: 是否需要验证(布尔值)\n- hasTax: 是否含税(布尔值)\n- taxAmount: 税额(数字,可选)\n\n关于 needsVerification 的判断规则:\n- 如果商品名称是缩写、不完整、被截断或存在歧义,设为 true\n- 如果商品名称清晰完整,设为 false\n- 不要猜测不确定的名称,而是保留原样并设 needsVerification 为 true\n\n**重要:附加费用处理规则**\n对于押金(Deposit、deposit、押金等)和折扣(TPD、discount、折扣等)这类附加费用:\n- 添加额外字段 isAttachment: true\n- 添加 attachmentType: \"deposit\" 或 \"discount\"\n- **重要**:将附加费用紧跟在它所属的商品后面排列\n- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n只返回 JSON 数组,不要其他文字。\n\n示例输出:\n[\n {\"name\": \"有机牛奶 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false},\n {\"name\": \"可口可乐瓶装\", \"price\": 3.5, \"quantity\": 2, \"needsVerification\": false, \"hasTax\": true, \"taxAmount\": 0.35},\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\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8}\n]`;\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 // 折扣:累加(通常已经是负数)\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 * 解析 LLM 返回的 JSON 响应为内部商品数组\n * \n * @param responseText - LLM 返回的文本响应\n * @returns 解析后的商品数组(包含 needsVerification 内部字段)\n * @throws 如果解析失败\n */\nexport function parseResponse(responseText: string): InternalReceiptItem[] {\n try {\n // 提取 JSON\n const jsonText = extractJson(responseText);\n\n // 解析 JSON\n const parsed = JSON.parse(jsonText);\n\n // 确保是数组\n if (!Array.isArray(parsed)) {\n throw new Error('Response is not an array');\n }\n\n // 验证并规范化每个商品\n const items = parsed.map((raw, index) => {\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 mergedItems;\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, VerificationContext } from './types.js';\nimport { callGemini } from './adapters/gemini.js';\nimport { batchVerifyItems } from './adapters/verifier.js';\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\nimport { parseResponse } from './processors/parser.js';\n\n/**\n * 从小票图片中提取商品数据\n * \n * 这是一个无状态的异步函数,每次调用独立执行。\n * \n * @param image - 图片输入(Buffer、base64 字符串或 URL)\n * @param options - 可选配置(包括验证回调)\n * @returns 商品列表\n * \n * @throws 如果环境变量 GEMINI_API_KEY 未设置\n * @throws 如果 API 调用失败\n * @throws 如果响应解析失败\n * \n * @example\n * ```typescript\n * // 基础用法\n * const items = await extractReceiptItems(imageBuffer);\n * \n * // 使用自动验证(Google Search)\n * const items = await extractReceiptItems(imageBuffer, {\n * autoVerify: true\n * });\n * \n * // 带自定义验证回调\n * const items = await extractReceiptItems(imageBuffer, {\n * verifyCallback: async (name, context) => {\n * const result = await myProductSearch(name);\n * return result ? { verifiedName: result.name } : null;\n * }\n * });\n * ```\n */\nexport async function extractReceiptItems(\n image: ImageInput,\n options?: ExtractOptions\n): Promise<ReceiptItem[]> {\n // 1. 调用 Gemini API\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\n\n // 2. 解析响应\n const parsedItems = parseResponse(responseText);\n\n // 3. 处理需要验证的商品\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\n \n // 3a. 自动验证(使用 Google Search grounding)\n if (options?.autoVerify && itemsNeedingVerification.length > 0) {\n try {\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\n \n // 应用验证结果\n for (const item of parsedItems) {\n if (item.needsVerification) {\n const verifiedName = verificationMap.get(item.name);\n if (verifiedName) {\n item.name = verifiedName;\n item.needsVerification = false;\n }\n // 如果未找到,保持原名称和 needsVerification=true\n }\n }\n } catch (error) {\n console.error('Auto verification failed:', error);\n // 失败时保持原始数据\n }\n }\n \n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\n if (options?.verifyCallback) {\n // 准备验证上下文(转换为公开类型)\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n const verificationContext: VerificationContext = {\n rawText: responseText,\n allItems: publicItems,\n };\n\n for (const item of parsedItems) {\n if (item.needsVerification) {\n try {\n const result = await options.verifyCallback(item.name, verificationContext);\n if (result && result.verifiedName) {\n // 更新商品名称\n item.name = result.verifiedName;\n // 验证成功后,标记为不再需要验证\n item.needsVerification = false;\n }\n } catch (error) {\n // 验证失败,静默忽略,保留原始数据\n console.error(`Verification failed for item \"${item.name}\":`, error);\n }\n }\n }\n }\n\n // 4. 转换为公开类型:移除内部字段 needsVerification\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n\n return finalItems;\n}\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"],"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 /** 图片 URL(如果输入是 URL) */\r\n url?: 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 * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport function processImage(image: ImageInput): 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\r\n if (isUrl(image)) {\r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n url: image,\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';\nimport type { ImageInput } from '../types.js';\nimport { processImage } from '../processors/image.js';\n\n/**\n * 从环境变量读取 Gemini API 配置\n */\nfunction getGeminiConfig() {\n const apiKey = process.env.GEMINI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\n );\n }\n\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\n\n return { apiKey, model };\n}\n\n/**\n * 调用 Gemini API 进行图片分析\n * \n * @param image - 图片输入(Buffer、base64 或 URL)\n * @param prompt - 提示词\n * @param useGrounding - 是否使用 Google Search grounding\n * @returns LLM 返回的文本响应\n */\nexport async function callGemini(\n image: ImageInput,\n prompt: string,\n useGrounding?: boolean\n): Promise<string> {\n const { apiKey, model } = getGeminiConfig();\n\n // 初始化 Gemini API 客户端\n const genAI = new GoogleGenerativeAI(apiKey);\n const geminiModel = genAI.getGenerativeModel({ model });\n\n // 处理图片\n const processedImage = processImage(image);\n\n // 构建请求内容\n const contents = [];\n\n // 添加图片部分\n if (processedImage.url) {\n // 如果是 URL,使用 fileData\n contents.push({\n fileData: {\n mimeType: processedImage.mimeType,\n fileUri: processedImage.url,\n },\n });\n } else if (processedImage.data) {\n // 如果是 base64 数据,使用 inlineData\n contents.push({\n inlineData: {\n mimeType: processedImage.mimeType,\n data: processedImage.data,\n },\n });\n }\n\n // 添加文本提示\n contents.push({\n text: prompt,\n });\n\n try {\n // 调用 Gemini API\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\n const requestParams: any = {\n contents: [{ role: 'user', parts: contents }],\n };\n \n if (useGrounding) {\n requestParams.tools = [{ googleSearch: {} }];\n }\n \n const result = await geminiModel.generateContent(requestParams);\n\n const response = result.response;\n const text = response.text();\n\n if (!text) {\n throw new Error('Gemini API returned empty response');\n }\n\n return text;\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Gemini API call failed: ${error.message}`);\n }\n throw new Error('Gemini API call failed with unknown error');\n }\n}\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- name: 商品名称(字符串)\n- price: 单价(数字)\n- quantity: 数量(数字,默认 1)\n- needsVerification: 是否需要验证(布尔值)\n- hasTax: 是否含税(布尔值)\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- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n只返回 JSON 数组,不要其他文字。\n\n示例输出:\n[\n {\"name\": \"Kirkland Signature 有机牛奶 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false},\n {\"name\": \"可口可乐\", \"price\": 3.5, \"quantity\": 2, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.35},\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\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8},\n {\"name\": \"CEMΟΙ 6Χ\", \"price\": 15.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": false},\n {\"name\": \"KS Apple\", \"price\": 4.5, \"quantity\": 3, \"needsVerification\": true, \"hasTax\": false}\n]`;\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 // 折扣:累加(通常已经是负数)\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 * 解析 LLM 返回的 JSON 响应为内部商品数组\n * \n * @param responseText - LLM 返回的文本响应\n * @returns 解析后的商品数组(包含 needsVerification 内部字段)\n * @throws 如果解析失败\n */\nexport function parseResponse(responseText: string): InternalReceiptItem[] {\n try {\n // 提取 JSON\n const jsonText = extractJson(responseText);\n\n // 解析 JSON\n const parsed = JSON.parse(jsonText);\n\n // 确保是数组\n if (!Array.isArray(parsed)) {\n throw new Error('Response is not an array');\n }\n\n // 验证并规范化每个商品\n const items = parsed.map((raw, index) => {\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 mergedItems;\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, VerificationContext } from './types.js';\nimport { callGemini } from './adapters/gemini.js';\nimport { batchVerifyItems } from './adapters/verifier.js';\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\nimport { parseResponse } from './processors/parser.js';\n\n/**\n * 从小票图片中提取商品数据\n * \n * 这是一个无状态的异步函数,每次调用独立执行。\n * \n * @param image - 图片输入(Buffer、base64 字符串或 URL)\n * @param options - 可选配置(包括验证回调)\n * @returns 商品列表\n * \n * @throws 如果环境变量 GEMINI_API_KEY 未设置\n * @throws 如果 API 调用失败\n * @throws 如果响应解析失败\n * \n * @example\n * ```typescript\n * // 基础用法\n * const items = await extractReceiptItems(imageBuffer);\n * \n * // 使用自动验证(Google Search)\n * const items = await extractReceiptItems(imageBuffer, {\n * autoVerify: true\n * });\n * \n * // 带自定义验证回调\n * const items = await extractReceiptItems(imageBuffer, {\n * verifyCallback: async (name, context) => {\n * const result = await myProductSearch(name);\n * return result ? { verifiedName: result.name } : null;\n * }\n * });\n * ```\n */\nexport async function extractReceiptItems(\n image: ImageInput,\n options?: ExtractOptions\n): Promise<ReceiptItem[]> {\n // 1. 调用 Gemini API\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\n\n // 2. 解析响应\n const parsedItems = parseResponse(responseText);\n\n // 3. 处理需要验证的商品\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\n \n // 3a. 自动验证(使用 Google Search grounding)\n if (options?.autoVerify && itemsNeedingVerification.length > 0) {\n try {\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\n \n // 应用验证结果\n for (const item of parsedItems) {\n if (item.needsVerification) {\n const verifiedName = verificationMap.get(item.name);\n if (verifiedName) {\n item.name = verifiedName;\n item.needsVerification = false;\n }\n // 如果未找到,保持原名称和 needsVerification=true\n }\n }\n } catch (error) {\n console.error('Auto verification failed:', error);\n // 失败时保持原始数据\n }\n }\n \n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\n if (options?.verifyCallback) {\n // 准备验证上下文(转换为公开类型)\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n const verificationContext: VerificationContext = {\n rawText: responseText,\n allItems: publicItems,\n };\n\n for (const item of parsedItems) {\n if (item.needsVerification) {\n try {\n const result = await options.verifyCallback(item.name, verificationContext);\n if (result && result.verifiedName) {\n // 更新商品名称\n item.name = result.verifiedName;\n // 验证成功后,标记为不再需要验证\n item.needsVerification = false;\n }\n } catch (error) {\n // 验证失败,静默忽略,保留原始数据\n console.error(`Verification failed for item \"${item.name}\":`, error);\n }\n }\n }\n }\n\n // 4. 转换为公开类型:移除内部字段 needsVerification\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n\n return finalItems;\n}\n"]}
package/dist/index.js CHANGED
@@ -216,9 +216,31 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
216
216
  - taxAmount: \u7A0E\u989D\uFF08\u6570\u5B57\uFF0C\u53EF\u9009\uFF09
217
217
 
218
218
  \u5173\u4E8E needsVerification \u7684\u5224\u65AD\u89C4\u5219\uFF1A
219
- - \u5982\u679C\u5546\u54C1\u540D\u79F0\u662F\u7F29\u5199\u3001\u4E0D\u5B8C\u6574\u3001\u88AB\u622A\u65AD\u6216\u5B58\u5728\u6B67\u4E49\uFF0C\u8BBE\u4E3A true
220
- - \u5982\u679C\u5546\u54C1\u540D\u79F0\u6E05\u6670\u5B8C\u6574\uFF0C\u8BBE\u4E3A false
221
- - \u4E0D\u8981\u731C\u6D4B\u4E0D\u786E\u5B9A\u7684\u540D\u79F0\uFF0C\u800C\u662F\u4FDD\u7559\u539F\u6837\u5E76\u8BBE needsVerification \u4E3A true
219
+ **\u91CD\u8981\u539F\u5219\uFF1A\u5B81\u53EF\u591A\u9A8C\u8BC1\uFF0C\u4E0D\u8981\u731C\u6D4B\u3002\u5F53\u4E0D\u786E\u5B9A\u65F6\uFF0C\u4F18\u5148\u8BBE\u4E3A true\u3002**
220
+
221
+ \u5FC5\u987B\u8BBE\u4E3A true \u7684\u60C5\u51B5\uFF1A
222
+ - \u5546\u54C1\u540D\u79F0\u662F\u7F29\u5199\uFF08\u5982 "ORG MLK"\u3001"VEG"\u3001"FRZ"\uFF09
223
+ - \u5546\u54C1\u540D\u79F0\u4E0D\u5B8C\u6574\u6216\u88AB\u622A\u65AD\uFF08\u5982 "CHOCO..."\u3001"\u6709\u673A..."\uFF09
224
+ - \u5305\u542B\u6570\u5B57\u6216\u5B57\u6BCD\u7EC4\u5408\u4F46\u542B\u4E49\u4E0D\u660E\u786E\uFF08\u5982 "CEM\u039F\u0399 6\u03A7"\u3001"KS 12X"\uFF09
225
+ - \u5546\u54C1\u540D\u79F0\u6A21\u7CCA\u6216\u53EF\u80FD\u6709\u591A\u79CD\u89E3\u91CA
226
+ - \u5546\u54C1\u540D\u79F0\u5305\u542B\u54C1\u724C\u7F29\u5199\u6216\u4EE3\u7801
227
+ - \u53EA\u6709\u54C1\u7C7B\u6CA1\u6709\u5177\u4F53\u54C1\u540D\uFF08\u5982 "\u9762\u5305"\u3001"\u996E\u6599"\uFF09
228
+ - \u5546\u54C1\u540D\u79F0\u4E2D\u6DF7\u6742\u4E86\u6570\u5B57\u4F46\u4E0D\u6E05\u695A\u5177\u4F53\u89C4\u683C\uFF08\u5982 "\u725B\u5976 2"\uFF09
229
+ - \u4EFB\u4F55\u4F60\u4E0D\u80FD100%\u786E\u5B9A\u5B8C\u6574\u542B\u4E49\u7684\u540D\u79F0
230
+
231
+ \u53EF\u4EE5\u8BBE\u4E3A false \u7684\u60C5\u51B5\uFF08\u5FC5\u987B\u540C\u65F6\u6EE1\u8DB3\u4EE5\u4E0B\u6240\u6709\u6761\u4EF6\uFF09\uFF1A
232
+ - \u5546\u54C1\u540D\u79F0\u5B8C\u6574\u3001\u6E05\u6670\u3001\u65E0\u7F29\u5199
233
+ - \u5305\u542B\u5B8C\u6574\u7684\u54C1\u724C\u548C\u89C4\u683C\u4FE1\u606F\uFF08\u5982 "\u53EF\u53E3\u53EF\u4E50\u74F6\u88C5 330ml"\uFF09
234
+ - \u4F60\u80FD100%\u786E\u5B9A\u8FD9\u4E2A\u540D\u79F0\u7684\u51C6\u786E\u542B\u4E49
235
+ - \u666E\u901A\u6D88\u8D39\u8005\u770B\u5230\u8FD9\u4E2A\u540D\u79F0\u80FD\u7ACB\u5373\u7406\u89E3\u662F\u4EC0\u4E48\u5546\u54C1
236
+
237
+ \u793A\u4F8B\uFF1A
238
+ - "ORG MLK" \u2192 needsVerification: true\uFF08\u7F29\u5199\uFF09
239
+ - "CEM\u039F\u0399 6\u03A7" \u2192 needsVerification: true\uFF08\u5305\u542B\u4E0D\u660E\u786E\u7684\u5B57\u7B26\uFF09
240
+ - "\u9762\u5305" \u2192 needsVerification: true\uFF08\u53EA\u6709\u54C1\u7C7B\uFF0C\u6CA1\u6709\u5177\u4F53\u4FE1\u606F\uFF09
241
+ - "KS Milk" \u2192 needsVerification: true\uFF08\u54C1\u724C\u7F29\u5199\uFF09
242
+ - "\u6709\u673A\u725B\u5976 Kirkland Signature 1L" \u2192 needsVerification: false\uFF08\u5B8C\u6574\u6E05\u6670\uFF09
243
+ - "\u5BCC\u58EB\u82F9\u679C" \u2192 needsVerification: false\uFF08\u5B8C\u6574\u4E14\u660E\u786E\uFF09
222
244
 
223
245
  **\u91CD\u8981\uFF1A\u9644\u52A0\u8D39\u7528\u5904\u7406\u89C4\u5219**
224
246
  \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
@@ -240,11 +262,13 @@ var EXTRACTION_PROMPT = `\u5206\u6790\u8FD9\u5F20\u8D2D\u7269\u5C0F\u7968\u56FE\
240
262
 
241
263
  \u793A\u4F8B\u8F93\u51FA\uFF1A
242
264
  [
243
- {"name": "\u6709\u673A\u725B\u5976 1L", "price": 12.5, "quantity": 1, "needsVerification": false, "hasTax": false},
244
- {"name": "\u53EF\u53E3\u53EF\u4E50\u74F6\u88C5", "price": 3.5, "quantity": 2, "needsVerification": false, "hasTax": true, "taxAmount": 0.35},
265
+ {"name": "Kirkland Signature \u6709\u673A\u725B\u5976 1L", "price": 12.5, "quantity": 1, "needsVerification": false, "hasTax": false},
266
+ {"name": "\u53EF\u53E3\u53EF\u4E50", "price": 3.5, "quantity": 2, "needsVerification": true, "hasTax": true, "taxAmount": 0.35},
245
267
  {"name": "Deposit VL", "price": 0.5, "quantity": 2, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "deposit"},
246
268
  {"name": "TPD", "price": -0.5, "quantity": 1, "needsVerification": false, "hasTax": false, "isAttachment": true, "attachmentType": "discount"},
247
- {"name": "ORG BRD", "price": 8.0, "quantity": 1, "needsVerification": true, "hasTax": true, "taxAmount": 0.8}
269
+ {"name": "ORG BRD", "price": 8.0, "quantity": 1, "needsVerification": true, "hasTax": true, "taxAmount": 0.8},
270
+ {"name": "CEM\u039F\u0399 6\u03A7", "price": 15.0, "quantity": 1, "needsVerification": true, "hasTax": false},
271
+ {"name": "KS Apple", "price": 4.5, "quantity": 3, "needsVerification": true, "hasTax": false}
248
272
  ]`;
249
273
 
250
274
  // src/processors/parser.ts
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"],"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 /** 图片 URL(如果输入是 URL) */\r\n url?: 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 * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport function processImage(image: ImageInput): 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\r\n if (isUrl(image)) {\r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n url: image,\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';\nimport type { ImageInput } from '../types.js';\nimport { processImage } from '../processors/image.js';\n\n/**\n * 从环境变量读取 Gemini API 配置\n */\nfunction getGeminiConfig() {\n const apiKey = process.env.GEMINI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\n );\n }\n\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\n\n return { apiKey, model };\n}\n\n/**\n * 调用 Gemini API 进行图片分析\n * \n * @param image - 图片输入(Buffer、base64 或 URL)\n * @param prompt - 提示词\n * @param useGrounding - 是否使用 Google Search grounding\n * @returns LLM 返回的文本响应\n */\nexport async function callGemini(\n image: ImageInput,\n prompt: string,\n useGrounding?: boolean\n): Promise<string> {\n const { apiKey, model } = getGeminiConfig();\n\n // 初始化 Gemini API 客户端\n const genAI = new GoogleGenerativeAI(apiKey);\n const geminiModel = genAI.getGenerativeModel({ model });\n\n // 处理图片\n const processedImage = processImage(image);\n\n // 构建请求内容\n const contents = [];\n\n // 添加图片部分\n if (processedImage.url) {\n // 如果是 URL,使用 fileData\n contents.push({\n fileData: {\n mimeType: processedImage.mimeType,\n fileUri: processedImage.url,\n },\n });\n } else if (processedImage.data) {\n // 如果是 base64 数据,使用 inlineData\n contents.push({\n inlineData: {\n mimeType: processedImage.mimeType,\n data: processedImage.data,\n },\n });\n }\n\n // 添加文本提示\n contents.push({\n text: prompt,\n });\n\n try {\n // 调用 Gemini API\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\n const requestParams: any = {\n contents: [{ role: 'user', parts: contents }],\n };\n \n if (useGrounding) {\n requestParams.tools = [{ googleSearch: {} }];\n }\n \n const result = await geminiModel.generateContent(requestParams);\n\n const response = result.response;\n const text = response.text();\n\n if (!text) {\n throw new Error('Gemini API returned empty response');\n }\n\n return text;\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Gemini API call failed: ${error.message}`);\n }\n throw new Error('Gemini API call failed with unknown error');\n }\n}\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- name: 商品名称(字符串)\n- price: 单价(数字)\n- quantity: 数量(数字,默认 1)\n- needsVerification: 是否需要验证(布尔值)\n- hasTax: 是否含税(布尔值)\n- taxAmount: 税额(数字,可选)\n\n关于 needsVerification 的判断规则:\n- 如果商品名称是缩写、不完整、被截断或存在歧义,设为 true\n- 如果商品名称清晰完整,设为 false\n- 不要猜测不确定的名称,而是保留原样并设 needsVerification 为 true\n\n**重要:附加费用处理规则**\n对于押金(Deposit、deposit、押金等)和折扣(TPD、discount、折扣等)这类附加费用:\n- 添加额外字段 isAttachment: true\n- 添加 attachmentType: \"deposit\" 或 \"discount\"\n- **重要**:将附加费用紧跟在它所属的商品后面排列\n- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n只返回 JSON 数组,不要其他文字。\n\n示例输出:\n[\n {\"name\": \"有机牛奶 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false},\n {\"name\": \"可口可乐瓶装\", \"price\": 3.5, \"quantity\": 2, \"needsVerification\": false, \"hasTax\": true, \"taxAmount\": 0.35},\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\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8}\n]`;\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 // 折扣:累加(通常已经是负数)\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 * 解析 LLM 返回的 JSON 响应为内部商品数组\n * \n * @param responseText - LLM 返回的文本响应\n * @returns 解析后的商品数组(包含 needsVerification 内部字段)\n * @throws 如果解析失败\n */\nexport function parseResponse(responseText: string): InternalReceiptItem[] {\n try {\n // 提取 JSON\n const jsonText = extractJson(responseText);\n\n // 解析 JSON\n const parsed = JSON.parse(jsonText);\n\n // 确保是数组\n if (!Array.isArray(parsed)) {\n throw new Error('Response is not an array');\n }\n\n // 验证并规范化每个商品\n const items = parsed.map((raw, index) => {\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 mergedItems;\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, VerificationContext } from './types.js';\nimport { callGemini } from './adapters/gemini.js';\nimport { batchVerifyItems } from './adapters/verifier.js';\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\nimport { parseResponse } from './processors/parser.js';\n\n/**\n * 从小票图片中提取商品数据\n * \n * 这是一个无状态的异步函数,每次调用独立执行。\n * \n * @param image - 图片输入(Buffer、base64 字符串或 URL)\n * @param options - 可选配置(包括验证回调)\n * @returns 商品列表\n * \n * @throws 如果环境变量 GEMINI_API_KEY 未设置\n * @throws 如果 API 调用失败\n * @throws 如果响应解析失败\n * \n * @example\n * ```typescript\n * // 基础用法\n * const items = await extractReceiptItems(imageBuffer);\n * \n * // 使用自动验证(Google Search)\n * const items = await extractReceiptItems(imageBuffer, {\n * autoVerify: true\n * });\n * \n * // 带自定义验证回调\n * const items = await extractReceiptItems(imageBuffer, {\n * verifyCallback: async (name, context) => {\n * const result = await myProductSearch(name);\n * return result ? { verifiedName: result.name } : null;\n * }\n * });\n * ```\n */\nexport async function extractReceiptItems(\n image: ImageInput,\n options?: ExtractOptions\n): Promise<ReceiptItem[]> {\n // 1. 调用 Gemini API\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\n\n // 2. 解析响应\n const parsedItems = parseResponse(responseText);\n\n // 3. 处理需要验证的商品\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\n \n // 3a. 自动验证(使用 Google Search grounding)\n if (options?.autoVerify && itemsNeedingVerification.length > 0) {\n try {\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\n \n // 应用验证结果\n for (const item of parsedItems) {\n if (item.needsVerification) {\n const verifiedName = verificationMap.get(item.name);\n if (verifiedName) {\n item.name = verifiedName;\n item.needsVerification = false;\n }\n // 如果未找到,保持原名称和 needsVerification=true\n }\n }\n } catch (error) {\n console.error('Auto verification failed:', error);\n // 失败时保持原始数据\n }\n }\n \n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\n if (options?.verifyCallback) {\n // 准备验证上下文(转换为公开类型)\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n const verificationContext: VerificationContext = {\n rawText: responseText,\n allItems: publicItems,\n };\n\n for (const item of parsedItems) {\n if (item.needsVerification) {\n try {\n const result = await options.verifyCallback(item.name, verificationContext);\n if (result && result.verifiedName) {\n // 更新商品名称\n item.name = result.verifiedName;\n // 验证成功后,标记为不再需要验证\n item.needsVerification = false;\n }\n } catch (error) {\n // 验证失败,静默忽略,保留原始数据\n console.error(`Verification failed for item \"${item.name}\":`, error);\n }\n }\n }\n }\n\n // 4. 转换为公开类型:移除内部字段 needsVerification\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n\n return finalItems;\n}\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"],"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 /** 图片 URL(如果输入是 URL) */\r\n url?: 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 * @param image - 图片输入(Buffer、base64 字符串或 URL)\r\n * @returns 处理后的图片数据\r\n */\r\nexport function processImage(image: ImageInput): 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\r\n if (isUrl(image)) {\r\n return {\r\n mimeType: getMimeTypeFromUrl(image),\r\n url: image,\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';\nimport type { ImageInput } from '../types.js';\nimport { processImage } from '../processors/image.js';\n\n/**\n * 从环境变量读取 Gemini API 配置\n */\nfunction getGeminiConfig() {\n const apiKey = process.env.GEMINI_API_KEY;\n if (!apiKey) {\n throw new Error(\n 'GEMINI_API_KEY environment variable is not set. Please set it to use the Gemini adapter.'\n );\n }\n\n const model = process.env.GEMINI_MODEL || 'gemini-2.0-flash';\n\n return { apiKey, model };\n}\n\n/**\n * 调用 Gemini API 进行图片分析\n * \n * @param image - 图片输入(Buffer、base64 或 URL)\n * @param prompt - 提示词\n * @param useGrounding - 是否使用 Google Search grounding\n * @returns LLM 返回的文本响应\n */\nexport async function callGemini(\n image: ImageInput,\n prompt: string,\n useGrounding?: boolean\n): Promise<string> {\n const { apiKey, model } = getGeminiConfig();\n\n // 初始化 Gemini API 客户端\n const genAI = new GoogleGenerativeAI(apiKey);\n const geminiModel = genAI.getGenerativeModel({ model });\n\n // 处理图片\n const processedImage = processImage(image);\n\n // 构建请求内容\n const contents = [];\n\n // 添加图片部分\n if (processedImage.url) {\n // 如果是 URL,使用 fileData\n contents.push({\n fileData: {\n mimeType: processedImage.mimeType,\n fileUri: processedImage.url,\n },\n });\n } else if (processedImage.data) {\n // 如果是 base64 数据,使用 inlineData\n contents.push({\n inlineData: {\n mimeType: processedImage.mimeType,\n data: processedImage.data,\n },\n });\n }\n\n // 添加文本提示\n contents.push({\n text: prompt,\n });\n\n try {\n // 调用 Gemini API\n // 如果启用 Google Search grounding,直接将 tools 作为顶级参数\n const requestParams: any = {\n contents: [{ role: 'user', parts: contents }],\n };\n \n if (useGrounding) {\n requestParams.tools = [{ googleSearch: {} }];\n }\n \n const result = await geminiModel.generateContent(requestParams);\n\n const response = result.response;\n const text = response.text();\n\n if (!text) {\n throw new Error('Gemini API returned empty response');\n }\n\n return text;\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Gemini API call failed: ${error.message}`);\n }\n throw new Error('Gemini API call failed with unknown error');\n }\n}\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- name: 商品名称(字符串)\n- price: 单价(数字)\n- quantity: 数量(数字,默认 1)\n- needsVerification: 是否需要验证(布尔值)\n- hasTax: 是否含税(布尔值)\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- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n只返回 JSON 数组,不要其他文字。\n\n示例输出:\n[\n {\"name\": \"Kirkland Signature 有机牛奶 1L\", \"price\": 12.5, \"quantity\": 1, \"needsVerification\": false, \"hasTax\": false},\n {\"name\": \"可口可乐\", \"price\": 3.5, \"quantity\": 2, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.35},\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\": \"ORG BRD\", \"price\": 8.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": true, \"taxAmount\": 0.8},\n {\"name\": \"CEMΟΙ 6Χ\", \"price\": 15.0, \"quantity\": 1, \"needsVerification\": true, \"hasTax\": false},\n {\"name\": \"KS Apple\", \"price\": 4.5, \"quantity\": 3, \"needsVerification\": true, \"hasTax\": false}\n]`;\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 // 折扣:累加(通常已经是负数)\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 * 解析 LLM 返回的 JSON 响应为内部商品数组\n * \n * @param responseText - LLM 返回的文本响应\n * @returns 解析后的商品数组(包含 needsVerification 内部字段)\n * @throws 如果解析失败\n */\nexport function parseResponse(responseText: string): InternalReceiptItem[] {\n try {\n // 提取 JSON\n const jsonText = extractJson(responseText);\n\n // 解析 JSON\n const parsed = JSON.parse(jsonText);\n\n // 确保是数组\n if (!Array.isArray(parsed)) {\n throw new Error('Response is not an array');\n }\n\n // 验证并规范化每个商品\n const items = parsed.map((raw, index) => {\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 mergedItems;\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, VerificationContext } from './types.js';\nimport { callGemini } from './adapters/gemini.js';\nimport { batchVerifyItems } from './adapters/verifier.js';\nimport { EXTRACTION_PROMPT } from './utils/prompt.js';\nimport { parseResponse } from './processors/parser.js';\n\n/**\n * 从小票图片中提取商品数据\n * \n * 这是一个无状态的异步函数,每次调用独立执行。\n * \n * @param image - 图片输入(Buffer、base64 字符串或 URL)\n * @param options - 可选配置(包括验证回调)\n * @returns 商品列表\n * \n * @throws 如果环境变量 GEMINI_API_KEY 未设置\n * @throws 如果 API 调用失败\n * @throws 如果响应解析失败\n * \n * @example\n * ```typescript\n * // 基础用法\n * const items = await extractReceiptItems(imageBuffer);\n * \n * // 使用自动验证(Google Search)\n * const items = await extractReceiptItems(imageBuffer, {\n * autoVerify: true\n * });\n * \n * // 带自定义验证回调\n * const items = await extractReceiptItems(imageBuffer, {\n * verifyCallback: async (name, context) => {\n * const result = await myProductSearch(name);\n * return result ? { verifiedName: result.name } : null;\n * }\n * });\n * ```\n */\nexport async function extractReceiptItems(\n image: ImageInput,\n options?: ExtractOptions\n): Promise<ReceiptItem[]> {\n // 1. 调用 Gemini API\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\n\n // 2. 解析响应\n const parsedItems = parseResponse(responseText);\n\n // 3. 处理需要验证的商品\n const itemsNeedingVerification = parsedItems.filter(item => item.needsVerification);\n \n // 3a. 自动验证(使用 Google Search grounding)\n if (options?.autoVerify && itemsNeedingVerification.length > 0) {\n try {\n const verificationMap = await batchVerifyItems(itemsNeedingVerification);\n \n // 应用验证结果\n for (const item of parsedItems) {\n if (item.needsVerification) {\n const verifiedName = verificationMap.get(item.name);\n if (verifiedName) {\n item.name = verifiedName;\n item.needsVerification = false;\n }\n // 如果未找到,保持原名称和 needsVerification=true\n }\n }\n } catch (error) {\n console.error('Auto verification failed:', error);\n // 失败时保持原始数据\n }\n }\n \n // 3b. 用户提供的验证回调(可与 autoVerify 共存)\n if (options?.verifyCallback) {\n // 准备验证上下文(转换为公开类型)\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n const verificationContext: VerificationContext = {\n rawText: responseText,\n allItems: publicItems,\n };\n\n for (const item of parsedItems) {\n if (item.needsVerification) {\n try {\n const result = await options.verifyCallback(item.name, verificationContext);\n if (result && result.verifiedName) {\n // 更新商品名称\n item.name = result.verifiedName;\n // 验证成功后,标记为不再需要验证\n item.needsVerification = false;\n }\n } catch (error) {\n // 验证失败,静默忽略,保留原始数据\n console.error(`Verification failed for item \"${item.name}\":`, error);\n }\n }\n }\n }\n\n // 4. 转换为公开类型:移除内部字段 needsVerification\n const finalItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => item);\n\n return finalItems;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "receipt-ocr",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",