receipt-ocr 1.0.1 → 1.0.2
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 +264 -260
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +56 -56
package/README.md
CHANGED
|
@@ -1,260 +1,264 @@
|
|
|
1
|
-
# ReceiptOCR
|
|
2
|
-
|
|
3
|
-
一个可复用的 TypeScript 库,用于借助多模态大语言模型从购物小票图片中提取结构化商品数据。
|
|
4
|
-
|
|
5
|
-
## 特性
|
|
6
|
-
|
|
7
|
-
- 🚀 **函数式 API**:无状态、异步、可组合
|
|
8
|
-
- 🎯 **类型安全**:完整的 TypeScript 类型定义
|
|
9
|
-
- 🔌 **依赖注入**:验证逻辑由调用方提供
|
|
10
|
-
- 📦 **双模块支持**:同时支持 ESM 和 CommonJS
|
|
11
|
-
- 🤖 **Gemini 驱动**:使用 Google Gemini 多模态模型
|
|
12
|
-
|
|
13
|
-
## 安装
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install receipt-ocr
|
|
17
|
-
# 或
|
|
18
|
-
pnpm add receipt-ocr
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## 环境配置
|
|
22
|
-
|
|
23
|
-
在使用前,需要设置环境变量:
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# 必需
|
|
27
|
-
export GEMINI_API_KEY=your-gemini-api-key
|
|
28
|
-
|
|
29
|
-
# 可选(默认:gemini-2.0-flash)
|
|
30
|
-
export GEMINI_MODEL=gemini-2.0-flash
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## 基础用法
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { extractReceiptItems } from 'receipt-ocr';
|
|
37
|
-
import fs from 'fs';
|
|
38
|
-
|
|
39
|
-
// 从文件读取图片
|
|
40
|
-
const imageBuffer = fs.readFileSync('receipt.jpg');
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
const receipt = await extractReceiptItems(imageBuffer);
|
|
44
|
-
|
|
45
|
-
console.log(receipt);
|
|
46
|
-
// {
|
|
47
|
-
// items: [
|
|
48
|
-
// {
|
|
49
|
-
// name: "有机牛奶 1L",
|
|
50
|
-
// price: 12.5,
|
|
51
|
-
// quantity: 1,
|
|
52
|
-
// hasTax: false
|
|
53
|
-
// },
|
|
54
|
-
// {
|
|
55
|
-
// name: "可口可乐瓶装",
|
|
56
|
-
// price: 3.5,
|
|
57
|
-
// quantity: 2,
|
|
58
|
-
// hasTax: true,
|
|
59
|
-
// taxAmount: 0.35,
|
|
60
|
-
// deposit: 0.5, // 押金已自动合并
|
|
61
|
-
// discount: -0.5 // 折扣已自动合并
|
|
62
|
-
// },
|
|
63
|
-
// ...
|
|
64
|
-
// ],
|
|
65
|
-
// total: 95.75
|
|
66
|
-
// }
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## 数据结构
|
|
70
|
-
|
|
71
|
-
### 小票数据
|
|
72
|
-
|
|
73
|
-
```typescript
|
|
74
|
-
interface ReceiptData {
|
|
75
|
-
items: ReceiptItem[]; // 商品列表
|
|
76
|
-
total: number; // 小票总金额
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### 商品数据
|
|
81
|
-
|
|
82
|
-
每个商品包含以下字段:
|
|
83
|
-
|
|
84
|
-
```typescript
|
|
85
|
-
interface ReceiptItem {
|
|
86
|
-
name: string; // 商品名称
|
|
87
|
-
price: number; // 单价
|
|
88
|
-
quantity: number; // 数量(默认 1)
|
|
89
|
-
hasTax: boolean; // 是否含税
|
|
90
|
-
taxAmount?: number; // 税额(可选)
|
|
91
|
-
deposit?: number; // 押金(可选,自动合并)
|
|
92
|
-
discount?: number; // 折扣(可选,自动合并)
|
|
93
|
-
}
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### 附加费用自动合并
|
|
97
|
-
|
|
98
|
-
库会自动识别并合并押金(Deposit)和折扣(TPD)到对应的商品中,而不是作为独立的商品项返回:
|
|
99
|
-
|
|
100
|
-
- **押金(deposit)**:如 "Deposit VL",会被合并到对应的瓶装商品中
|
|
101
|
-
- **折扣(discount)**:如 "TPD",会被合并到对应的商品中(通常为负数)
|
|
102
|
-
|
|
103
|
-
这意味着您不需要手动处理这些附加费用,它们会自动关联到正确的商品上。
|
|
104
|
-
|
|
105
|
-
## 高级用法
|
|
106
|
-
|
|
107
|
-
### 1.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { extractReceiptItems } from 'receipt-ocr';
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
console.log(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
//
|
|
206
|
-
const
|
|
207
|
-
await extractReceiptItems(
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
const
|
|
211
|
-
await extractReceiptItems(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
1
|
+
# ReceiptOCR
|
|
2
|
+
|
|
3
|
+
一个可复用的 TypeScript 库,用于借助多模态大语言模型从购物小票图片中提取结构化商品数据。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🚀 **函数式 API**:无状态、异步、可组合
|
|
8
|
+
- 🎯 **类型安全**:完整的 TypeScript 类型定义
|
|
9
|
+
- 🔌 **依赖注入**:验证逻辑由调用方提供
|
|
10
|
+
- 📦 **双模块支持**:同时支持 ESM 和 CommonJS
|
|
11
|
+
- 🤖 **Gemini 驱动**:使用 Google Gemini 多模态模型
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install receipt-ocr
|
|
17
|
+
# 或
|
|
18
|
+
pnpm add receipt-ocr
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 环境配置
|
|
22
|
+
|
|
23
|
+
在使用前,需要设置环境变量:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 必需
|
|
27
|
+
export GEMINI_API_KEY=your-gemini-api-key
|
|
28
|
+
|
|
29
|
+
# 可选(默认:gemini-2.0-flash)
|
|
30
|
+
export GEMINI_MODEL=gemini-2.0-flash
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 基础用法
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { extractReceiptItems } from 'receipt-ocr';
|
|
37
|
+
import fs from 'fs';
|
|
38
|
+
|
|
39
|
+
// 从文件读取图片
|
|
40
|
+
const imageBuffer = fs.readFileSync('receipt.jpg');
|
|
41
|
+
|
|
42
|
+
// 提取商品信息和总金额(默认启用自动验证)
|
|
43
|
+
const receipt = await extractReceiptItems(imageBuffer);
|
|
44
|
+
|
|
45
|
+
console.log(receipt);
|
|
46
|
+
// {
|
|
47
|
+
// items: [
|
|
48
|
+
// {
|
|
49
|
+
// name: "有机牛奶 1L",
|
|
50
|
+
// price: 12.5,
|
|
51
|
+
// quantity: 1,
|
|
52
|
+
// hasTax: false
|
|
53
|
+
// },
|
|
54
|
+
// {
|
|
55
|
+
// name: "可口可乐瓶装",
|
|
56
|
+
// price: 3.5,
|
|
57
|
+
// quantity: 2,
|
|
58
|
+
// hasTax: true,
|
|
59
|
+
// taxAmount: 0.35,
|
|
60
|
+
// deposit: 0.5, // 押金已自动合并
|
|
61
|
+
// discount: -0.5 // 折扣已自动合并
|
|
62
|
+
// },
|
|
63
|
+
// ...
|
|
64
|
+
// ],
|
|
65
|
+
// total: 95.75
|
|
66
|
+
// }
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 数据结构
|
|
70
|
+
|
|
71
|
+
### 小票数据
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
interface ReceiptData {
|
|
75
|
+
items: ReceiptItem[]; // 商品列表
|
|
76
|
+
total: number; // 小票总金额
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 商品数据
|
|
81
|
+
|
|
82
|
+
每个商品包含以下字段:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface ReceiptItem {
|
|
86
|
+
name: string; // 商品名称
|
|
87
|
+
price: number; // 单价
|
|
88
|
+
quantity: number; // 数量(默认 1)
|
|
89
|
+
hasTax: boolean; // 是否含税
|
|
90
|
+
taxAmount?: number; // 税额(可选)
|
|
91
|
+
deposit?: number; // 押金(可选,自动合并)
|
|
92
|
+
discount?: number; // 折扣(可选,自动合并)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 附加费用自动合并
|
|
97
|
+
|
|
98
|
+
库会自动识别并合并押金(Deposit)和折扣(TPD)到对应的商品中,而不是作为独立的商品项返回:
|
|
99
|
+
|
|
100
|
+
- **押金(deposit)**:如 "Deposit VL",会被合并到对应的瓶装商品中
|
|
101
|
+
- **折扣(discount)**:如 "TPD",会被合并到对应的商品中(通常为负数)
|
|
102
|
+
|
|
103
|
+
这意味着您不需要手动处理这些附加费用,它们会自动关联到正确的商品上。
|
|
104
|
+
|
|
105
|
+
## 高级用法
|
|
106
|
+
|
|
107
|
+
### 1. 自动验证(默认启用)
|
|
108
|
+
|
|
109
|
+
库默认使用 Google Search grounding 自动批量验证不确定的商品名称:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { extractReceiptItems } from 'receipt-ocr';
|
|
113
|
+
|
|
114
|
+
// 默认启用自动验证
|
|
115
|
+
const receipt = await extractReceiptItems(imageBuffer);
|
|
116
|
+
|
|
117
|
+
// 如需禁用自动验证,显式设置为 false
|
|
118
|
+
const receiptWithoutVerify = await extractReceiptItems(imageBuffer, {
|
|
119
|
+
autoVerify: false, // 禁用自动验证
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log(receipt.items); // 商品列表
|
|
123
|
+
console.log(receipt.total); // 总金额
|
|
124
|
+
|
|
125
|
+
// 库会自动验证并补全模糊的商品名称
|
|
126
|
+
// 如果验证失败,会保持原始名称
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**优势**:
|
|
130
|
+
- ✅ 批量处理,只需 1 次额外 API 调用
|
|
131
|
+
- ✅ 使用 Google Search,覆盖面广
|
|
132
|
+
- ✅ 自动处理,无需额外代码
|
|
133
|
+
- ✅ 验证失败时自动保持原始数据
|
|
134
|
+
|
|
135
|
+
详细文档:[自动验证功能](./docs/AUTO_VERIFICATION.md)
|
|
136
|
+
|
|
137
|
+
### 2. 自定义验证回调
|
|
138
|
+
|
|
139
|
+
当需要连接特定产品库时,可以使用自定义验证回调:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { extractReceiptItems } from 'receipt-ocr';
|
|
143
|
+
|
|
144
|
+
const receipt = await extractReceiptItems(imageBuffer, {
|
|
145
|
+
verifyCallback: async (name, context) => {
|
|
146
|
+
// 调用外部搜索服务验证/补全商品名称
|
|
147
|
+
const result = await myProductDatabase.search(name);
|
|
148
|
+
|
|
149
|
+
if (result) {
|
|
150
|
+
return { verifiedName: result.fullName };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 返回 null 保持原样
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 3. 组合使用
|
|
160
|
+
|
|
161
|
+
两种验证方式可以同时使用(自动验证默认启用):
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
const receipt = await extractReceiptItems(imageBuffer, {
|
|
165
|
+
// autoVerify 默认为 true,会先用 Google Search 批量验证
|
|
166
|
+
verifyCallback: async (name, context) => {
|
|
167
|
+
// 如果自动验证失败,再用自定义逻辑
|
|
168
|
+
const result = await myProductDatabase.search(name);
|
|
169
|
+
return result ? { verifiedName: result.name } : null;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 验证回调接口
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
type VerificationCallback = (
|
|
178
|
+
name: string,
|
|
179
|
+
context: {
|
|
180
|
+
rawText: string; // OCR 原始文本
|
|
181
|
+
allItems: ReceiptItem[]; // 所有已解析商品(不含 total)
|
|
182
|
+
}
|
|
183
|
+
) => Promise<{ verifiedName: string } | null>;
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 访问总金额
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const receipt = await extractReceiptItems(imageBuffer);
|
|
190
|
+
|
|
191
|
+
// 访问商品列表
|
|
192
|
+
receipt.items.forEach(item => {
|
|
193
|
+
console.log(`${item.name}: ¥${item.price} × ${item.quantity}`);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 访问总金额
|
|
197
|
+
console.log(`总计: ¥${receipt.total}`);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## 图片输入格式
|
|
201
|
+
|
|
202
|
+
支持以下三种格式:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// 1. Buffer
|
|
206
|
+
const buffer = fs.readFileSync('receipt.jpg');
|
|
207
|
+
await extractReceiptItems(buffer);
|
|
208
|
+
|
|
209
|
+
// 2. Base64 字符串
|
|
210
|
+
const base64 = 'iVBORw0KGgoAAAANSUhEUgAA...';
|
|
211
|
+
await extractReceiptItems(base64);
|
|
212
|
+
|
|
213
|
+
// 3. 图片 URL
|
|
214
|
+
const url = 'https://example.com/receipt.jpg';
|
|
215
|
+
await extractReceiptItems(url);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 注意事项
|
|
219
|
+
|
|
220
|
+
- **图片大小限制**:单次请求(包括图片和提示文本)总大小不能超过 **20MB**
|
|
221
|
+
- **URL 处理方式**:URL 图片会被自动下载并转换为 base64 后发送给 API
|
|
222
|
+
- **性能建议**:对于购物小票等文档图片,通常大小在几百 KB 到几 MB 之间,完全在限制范围内
|
|
223
|
+
|
|
224
|
+
## 策略接口(供扩展)
|
|
225
|
+
|
|
226
|
+
库预留了完整的策略接口,方便未来扩展:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { VerificationStrategy } from 'receipt-ocr';
|
|
230
|
+
|
|
231
|
+
const myStrategy: VerificationStrategy = {
|
|
232
|
+
verify: async (name, context) => {
|
|
233
|
+
const verified = await searchProductDB(name);
|
|
234
|
+
return { verifiedName: verified };
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## 开发
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
# 安装依赖
|
|
243
|
+
npm install
|
|
244
|
+
|
|
245
|
+
# 类型检查
|
|
246
|
+
npm run type-check
|
|
247
|
+
|
|
248
|
+
# 构建
|
|
249
|
+
npm run build
|
|
250
|
+
|
|
251
|
+
# 开发模式(监听变化)
|
|
252
|
+
npm run dev
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 设计原则
|
|
256
|
+
|
|
257
|
+
1. **无状态**:每次调用独立,无副作用
|
|
258
|
+
2. **确定性**:不猜测不确定的数据,通过验证机制确保准确性
|
|
259
|
+
3. **可组合性**:验证逻辑通过依赖注入提供
|
|
260
|
+
4. **正确性优先**:内部处理不确定性,对外只返回可靠数据
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -415,7 +415,7 @@ async function extractReceiptItems(image, options) {
|
|
|
415
415
|
const responseText = await callGemini(image, EXTRACTION_PROMPT);
|
|
416
416
|
const { items: parsedItems, total } = parseResponse(responseText);
|
|
417
417
|
const itemsNeedingVerification = parsedItems.filter((item) => item.needsVerification);
|
|
418
|
-
if (options?.autoVerify && itemsNeedingVerification.length > 0) {
|
|
418
|
+
if (options?.autoVerify !== false && itemsNeedingVerification.length > 0) {
|
|
419
419
|
try {
|
|
420
420
|
const verificationMap = await batchVerifyItems(itemsNeedingVerification);
|
|
421
421
|
for (const item of parsedItems) {
|
package/dist/index.cjs.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":["GoogleGenerativeAI","getGeminiConfig","result","item"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { ImageInput } from '../types.js';\n\n/**\n * 图片数据接口,用于传递给 Gemini API\n */\nexport interface ProcessedImage {\n /** 图片的 MIME 类型 */\n mimeType: string;\n /** base64 编码的图片数据(不包含 data URI 前缀) */\n data: string;\n}\n\n/**\n * 检测是否为 URL\n */\nfunction isUrl(input: string): boolean {\n return input.startsWith('http://') || input.startsWith('https://');\n}\n\n/**\n * 检测是否为 base64 字符串\n */\nfunction isBase64(input: string): boolean {\n // 简单检测:如果以 data: 开头,或者看起来像 base64\n if (input.startsWith('data:')) {\n return true;\n }\n // Base64 字符串通常很长,且只包含特定字符\n return input.length > 100 && /^[A-Za-z0-9+/=]+$/.test(input);\n}\n\n/**\n * 从 data URI 中提取 MIME 类型和数据\n */\nfunction parseDataUri(dataUri: string): { mimeType: string; data: string } {\n const match = dataUri.match(/^data:([^;,]+);base64,(.+)$/);\n if (match) {\n return {\n mimeType: match[1],\n data: match[2],\n };\n }\n // 如果没有匹配到,假设是纯 base64,默认 MIME 类型\n return {\n mimeType: 'image/jpeg',\n data: dataUri,\n };\n}\n\n/**\n * 从文件扩展名推断 MIME 类型\n */\nfunction getMimeTypeFromUrl(url: string): string {\n const lower = url.toLowerCase();\n if (lower.includes('.png')) return 'image/png';\n if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';\n if (lower.includes('.webp')) return 'image/webp';\n if (lower.includes('.gif')) return 'image/gif';\n // 默认\n return 'image/jpeg';\n}\n\n/**\n * 处理图片输入,转换为 Gemini API 可用的格式\n * \n * 根据官方文档,对于 URL 图片,需要先 fetch 获取数据,再转换为 base64。\n * Gemini API 只接受 inlineData(base64)或通过 File API 上传的文件 URI。\n * \n * @param image - 图片输入(Buffer、base64 字符串或 URL)\n * @returns 处理后的图片数据\n */\nexport async function processImage(image: ImageInput): Promise<ProcessedImage> {\n // 如果是 Buffer\n if (Buffer.isBuffer(image)) {\n return {\n mimeType: 'image/jpeg', // 默认,实际中可能需要更精确的检测\n data: image.toString('base64'),\n };\n }\n\n // 如果是字符串\n if (typeof image === 'string') {\n // URL - 需要 fetch 并转换为 base64\n if (isUrl(image)) {\n try {\n const response = await fetch(image);\n if (!response.ok) {\n throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n const base64Data = Buffer.from(arrayBuffer).toString('base64');\n \n return {\n mimeType: getMimeTypeFromUrl(image),\n data: base64Data,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to fetch image from URL: ${error.message}`);\n }\n throw new Error('Failed to fetch image from URL');\n }\n }\n\n // data URI\n if (image.startsWith('data:')) {\n const { mimeType, data } = parseDataUri(image);\n return { mimeType, data };\n }\n\n // 纯 base64\n if (isBase64(image)) {\n return {\n mimeType: 'image/jpeg',\n data: image,\n };\n }\n\n // 无法识别,抛出错误\n throw new Error('Unsupported image format: string is not a valid URL or base64');\n }\n\n throw new Error('Unsupported image input type');\n}\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 = await processImage(image);\n\n // 构建请求内容\n const contents = [];\n\n // 添加图片部分(统一使用 inlineData)\n contents.push({\n inlineData: {\n mimeType: processedImage.mimeType,\n data: processedImage.data,\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","/**\n * 使用 Google Search grounding 验证商品名称\n */\n\nimport { GoogleGenerativeAI } from '@google/generative-ai';\nimport type { ReceiptItem } from '../types.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 * 构建批量验证 prompt\n */\nfunction buildVerificationPrompt(items: Partial<ReceiptItem>[]): string {\n const itemList = items\n .map((item, index) => `${index + 1}. \"${item.name}\"`)\n .join('\\n');\n\n return `这是从 Costco 超市小票中识别出的商品名称,部分名称可能不完整或有缩写。\n请使用 Google Search 查找并补全这些商品的完整正确名称。\n\n需要验证的商品:\n${itemList}\n\n验证方法建议:\n- 在搜索引擎中输入:商品原名 + \"Costco\"(例如:\"CEMOI 6X Costco\")\n- 这样能更准确地找到 Costco 销售的对应商品\n- 注意确认商品的包装规格(如数量、容量等)\n\n请返回 JSON 数组格式,每个商品包含:\n- index: 序号(1-based)\n- originalName: 原始名称\n- verifiedName: 验证后的完整名称(如果找到)\n- found: 是否找到匹配(布尔值)\n\n示例输出:\n[\n {\"index\": 1, \"originalName\": \"ORG MLK\", \"verifiedName\": \"Kirkland Signature Organic 2% Milk 1L\", \"found\": true},\n {\"index\": 2, \"originalName\": \"CEMΟΙ 6Χ\", \"verifiedName\": \"CEMOI 82% Dark Chocolate Bars, 6 × 100 g\", \"found\": true}\n]\n\n只返回 JSON 数组,不要其他文字。`;\n}\n\n/**\n * 解析验证响应\n */\ninterface VerificationResult {\n index: number;\n originalName: string;\n verifiedName: string;\n found: boolean;\n}\n\nfunction parseVerificationResponse(responseText: string): VerificationResult[] {\n try {\n // 移除可能的 markdown 代码块标记\n let cleaned = responseText.trim();\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (codeBlockMatch) {\n cleaned = codeBlockMatch[1].trim();\n }\n\n const parsed = JSON.parse(cleaned);\n \n if (!Array.isArray(parsed)) {\n throw new Error('Response is not an array');\n }\n\n return parsed;\n } catch (error) {\n console.error('Failed to parse verification response:', error);\n return [];\n }\n}\n\n/**\n * 批量验证商品名称\n * 使用 Google Search grounding 查找完整商品名\n * \n * @param items - 需要验证的商品列表\n * @returns 验证结果映射 (原始名称 -> 验证后名称)\n */\nexport async function batchVerifyItems(\n items: Partial<ReceiptItem>[]\n): Promise<Map<string, string>> {\n if (items.length === 0) {\n return new Map();\n }\n\n const { apiKey, model } = getGeminiConfig();\n const genAI = new GoogleGenerativeAI(apiKey);\n const geminiModel = genAI.getGenerativeModel({ model });\n\n // 构建验证 prompt\n const prompt = buildVerificationPrompt(items);\n\n try {\n // 使用 Google Search grounding\n // 注意:tools 应该直接作为 generateContent 的顶级参数,而不是嵌套在 config 里\n const result = await geminiModel.generateContent({\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\n tools: [{ googleSearch: {} }],\n } as any);\n\n const response = result.response;\n const text = response.text();\n\n if (!text) {\n console.warn('Verification returned empty response');\n return new Map();\n }\n\n // 解析验证结果\n const verificationResults = parseVerificationResponse(text);\n\n // 构建映射\n const resultMap = new Map<string, string>();\n verificationResults.forEach((result) => {\n if (result.found && result.verifiedName) {\n resultMap.set(result.originalName, result.verifiedName);\n }\n });\n\n return resultMap;\n } catch (error) {\n console.error('Batch verification failed:', error);\n return new Map();\n }\n}\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- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n**关于 total(总金额)的提取规则:**\n- 从小票底部找到 \"TOTAL\"、\"总计\"、\"合计\" 等标记\n- 提取对应的金额数字\n- 这是小票的最终应付金额\n\n只返回 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. 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 // 折扣:累加(通常已经是负数)\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';\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 receipt = await extractReceiptItems(imageBuffer);\n * console.log(receipt.items); // 商品列表\n * console.log(receipt.total); // 总金额\n * \n * // 使用自动验证(Google Search)\n * const receipt = await extractReceiptItems(imageBuffer, {\n * autoVerify: true\n * });\n * \n * // 带自定义验证回调\n * const receipt = 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<ReceiptData> {\n // 1. 调用 Gemini API\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\n\n // 2. 解析响应\n const { items: parsedItems, total } = 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 for (const item of parsedItems) {\n if (item.needsVerification) {\n try {\n // 每次调用时动态创建验证上下文,确保包含最新的验证状态\n // 创建深拷贝以防止外部修改内部数据\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({...item}));\n const verificationContext: VerificationContext = {\n rawText: responseText,\n allItems: publicItems,\n };\n \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 }) => ({\n ...item // 创建新对象,确保外部无法修改内部数据\n }));\n\n return {\n items: finalItems,\n total,\n };\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","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,IAAIA,+BAAA,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,SAASC,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,IAAID,+BAAAA,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,CAACE,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,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.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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -77,7 +77,7 @@ interface ExtractOptions {
|
|
|
77
77
|
verifyCallback?: VerificationCallback;
|
|
78
78
|
/**
|
|
79
79
|
* 自动使用 Google Search 验证不确定的商品名称
|
|
80
|
-
* 默认
|
|
80
|
+
* 默认 true
|
|
81
81
|
*/
|
|
82
82
|
autoVerify?: boolean;
|
|
83
83
|
}
|
|
@@ -102,9 +102,12 @@ interface ExtractOptions {
|
|
|
102
102
|
* console.log(receipt.items); // 商品列表
|
|
103
103
|
* console.log(receipt.total); // 总金额
|
|
104
104
|
*
|
|
105
|
-
* //
|
|
105
|
+
* // 使用自动验证(默认已启用)
|
|
106
|
+
* const receipt = await extractReceiptItems(imageBuffer);
|
|
107
|
+
*
|
|
108
|
+
* // 禁用自动验证
|
|
106
109
|
* const receipt = await extractReceiptItems(imageBuffer, {
|
|
107
|
-
* autoVerify:
|
|
110
|
+
* autoVerify: false
|
|
108
111
|
* });
|
|
109
112
|
*
|
|
110
113
|
* // 带自定义验证回调
|
package/dist/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ interface ExtractOptions {
|
|
|
77
77
|
verifyCallback?: VerificationCallback;
|
|
78
78
|
/**
|
|
79
79
|
* 自动使用 Google Search 验证不确定的商品名称
|
|
80
|
-
* 默认
|
|
80
|
+
* 默认 true
|
|
81
81
|
*/
|
|
82
82
|
autoVerify?: boolean;
|
|
83
83
|
}
|
|
@@ -102,9 +102,12 @@ interface ExtractOptions {
|
|
|
102
102
|
* console.log(receipt.items); // 商品列表
|
|
103
103
|
* console.log(receipt.total); // 总金额
|
|
104
104
|
*
|
|
105
|
-
* //
|
|
105
|
+
* // 使用自动验证(默认已启用)
|
|
106
|
+
* const receipt = await extractReceiptItems(imageBuffer);
|
|
107
|
+
*
|
|
108
|
+
* // 禁用自动验证
|
|
106
109
|
* const receipt = await extractReceiptItems(imageBuffer, {
|
|
107
|
-
* autoVerify:
|
|
110
|
+
* autoVerify: false
|
|
108
111
|
* });
|
|
109
112
|
*
|
|
110
113
|
* // 带自定义验证回调
|
package/dist/index.js
CHANGED
|
@@ -413,7 +413,7 @@ async function extractReceiptItems(image, options) {
|
|
|
413
413
|
const responseText = await callGemini(image, EXTRACTION_PROMPT);
|
|
414
414
|
const { items: parsedItems, total } = parseResponse(responseText);
|
|
415
415
|
const itemsNeedingVerification = parsedItems.filter((item) => item.needsVerification);
|
|
416
|
-
if (options?.autoVerify && itemsNeedingVerification.length > 0) {
|
|
416
|
+
if (options?.autoVerify !== false && itemsNeedingVerification.length > 0) {
|
|
417
417
|
try {
|
|
418
418
|
const verificationMap = await batchVerifyItems(itemsNeedingVerification);
|
|
419
419
|
for (const item of parsedItems) {
|
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":";;;;;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,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;;;AC1IA,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;AAGlF,EAAA,IAAI,OAAA,EAAS,UAAA,IAAc,wBAAA,CAAyB,MAAA,GAAS,CAAA,EAAG;AAC9D,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';\n\n/**\n * 图片数据接口,用于传递给 Gemini API\n */\nexport interface ProcessedImage {\n /** 图片的 MIME 类型 */\n mimeType: string;\n /** base64 编码的图片数据(不包含 data URI 前缀) */\n data: string;\n}\n\n/**\n * 检测是否为 URL\n */\nfunction isUrl(input: string): boolean {\n return input.startsWith('http://') || input.startsWith('https://');\n}\n\n/**\n * 检测是否为 base64 字符串\n */\nfunction isBase64(input: string): boolean {\n // 简单检测:如果以 data: 开头,或者看起来像 base64\n if (input.startsWith('data:')) {\n return true;\n }\n // Base64 字符串通常很长,且只包含特定字符\n return input.length > 100 && /^[A-Za-z0-9+/=]+$/.test(input);\n}\n\n/**\n * 从 data URI 中提取 MIME 类型和数据\n */\nfunction parseDataUri(dataUri: string): { mimeType: string; data: string } {\n const match = dataUri.match(/^data:([^;,]+);base64,(.+)$/);\n if (match) {\n return {\n mimeType: match[1],\n data: match[2],\n };\n }\n // 如果没有匹配到,假设是纯 base64,默认 MIME 类型\n return {\n mimeType: 'image/jpeg',\n data: dataUri,\n };\n}\n\n/**\n * 从文件扩展名推断 MIME 类型\n */\nfunction getMimeTypeFromUrl(url: string): string {\n const lower = url.toLowerCase();\n if (lower.includes('.png')) return 'image/png';\n if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';\n if (lower.includes('.webp')) return 'image/webp';\n if (lower.includes('.gif')) return 'image/gif';\n // 默认\n return 'image/jpeg';\n}\n\n/**\n * 处理图片输入,转换为 Gemini API 可用的格式\n * \n * 根据官方文档,对于 URL 图片,需要先 fetch 获取数据,再转换为 base64。\n * Gemini API 只接受 inlineData(base64)或通过 File API 上传的文件 URI。\n * \n * @param image - 图片输入(Buffer、base64 字符串或 URL)\n * @returns 处理后的图片数据\n */\nexport async function processImage(image: ImageInput): Promise<ProcessedImage> {\n // 如果是 Buffer\n if (Buffer.isBuffer(image)) {\n return {\n mimeType: 'image/jpeg', // 默认,实际中可能需要更精确的检测\n data: image.toString('base64'),\n };\n }\n\n // 如果是字符串\n if (typeof image === 'string') {\n // URL - 需要 fetch 并转换为 base64\n if (isUrl(image)) {\n try {\n const response = await fetch(image);\n if (!response.ok) {\n throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);\n }\n const arrayBuffer = await response.arrayBuffer();\n const base64Data = Buffer.from(arrayBuffer).toString('base64');\n \n return {\n mimeType: getMimeTypeFromUrl(image),\n data: base64Data,\n };\n } catch (error) {\n if (error instanceof Error) {\n throw new Error(`Failed to fetch image from URL: ${error.message}`);\n }\n throw new Error('Failed to fetch image from URL');\n }\n }\n\n // data URI\n if (image.startsWith('data:')) {\n const { mimeType, data } = parseDataUri(image);\n return { mimeType, data };\n }\n\n // 纯 base64\n if (isBase64(image)) {\n return {\n mimeType: 'image/jpeg',\n data: image,\n };\n }\n\n // 无法识别,抛出错误\n throw new Error('Unsupported image format: string is not a valid URL or base64');\n }\n\n throw new Error('Unsupported image input type');\n}\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 = await processImage(image);\n\n // 构建请求内容\n const contents = [];\n\n // 添加图片部分(统一使用 inlineData)\n contents.push({\n inlineData: {\n mimeType: processedImage.mimeType,\n data: processedImage.data,\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","/**\n * 使用 Google Search grounding 验证商品名称\n */\n\nimport { GoogleGenerativeAI } from '@google/generative-ai';\nimport type { ReceiptItem } from '../types.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 * 构建批量验证 prompt\n */\nfunction buildVerificationPrompt(items: Partial<ReceiptItem>[]): string {\n const itemList = items\n .map((item, index) => `${index + 1}. \"${item.name}\"`)\n .join('\\n');\n\n return `这是从 Costco 超市小票中识别出的商品名称,部分名称可能不完整或有缩写。\n请使用 Google Search 查找并补全这些商品的完整正确名称。\n\n需要验证的商品:\n${itemList}\n\n验证方法建议:\n- 在搜索引擎中输入:商品原名 + \"Costco\"(例如:\"CEMOI 6X Costco\")\n- 这样能更准确地找到 Costco 销售的对应商品\n- 注意确认商品的包装规格(如数量、容量等)\n\n请返回 JSON 数组格式,每个商品包含:\n- index: 序号(1-based)\n- originalName: 原始名称\n- verifiedName: 验证后的完整名称(如果找到)\n- found: 是否找到匹配(布尔值)\n\n示例输出:\n[\n {\"index\": 1, \"originalName\": \"ORG MLK\", \"verifiedName\": \"Kirkland Signature Organic 2% Milk 1L\", \"found\": true},\n {\"index\": 2, \"originalName\": \"CEMΟΙ 6Χ\", \"verifiedName\": \"CEMOI 82% Dark Chocolate Bars, 6 × 100 g\", \"found\": true}\n]\n\n只返回 JSON 数组,不要其他文字。`;\n}\n\n/**\n * 解析验证响应\n */\ninterface VerificationResult {\n index: number;\n originalName: string;\n verifiedName: string;\n found: boolean;\n}\n\nfunction parseVerificationResponse(responseText: string): VerificationResult[] {\n try {\n // 移除可能的 markdown 代码块标记\n let cleaned = responseText.trim();\n const codeBlockMatch = cleaned.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n if (codeBlockMatch) {\n cleaned = codeBlockMatch[1].trim();\n }\n\n const parsed = JSON.parse(cleaned);\n \n if (!Array.isArray(parsed)) {\n throw new Error('Response is not an array');\n }\n\n return parsed;\n } catch (error) {\n console.error('Failed to parse verification response:', error);\n return [];\n }\n}\n\n/**\n * 批量验证商品名称\n * 使用 Google Search grounding 查找完整商品名\n * \n * @param items - 需要验证的商品列表\n * @returns 验证结果映射 (原始名称 -> 验证后名称)\n */\nexport async function batchVerifyItems(\n items: Partial<ReceiptItem>[]\n): Promise<Map<string, string>> {\n if (items.length === 0) {\n return new Map();\n }\n\n const { apiKey, model } = getGeminiConfig();\n const genAI = new GoogleGenerativeAI(apiKey);\n const geminiModel = genAI.getGenerativeModel({ model });\n\n // 构建验证 prompt\n const prompt = buildVerificationPrompt(items);\n\n try {\n // 使用 Google Search grounding\n // 注意:tools 应该直接作为 generateContent 的顶级参数,而不是嵌套在 config 里\n const result = await geminiModel.generateContent({\n contents: [{ role: 'user', parts: [{ text: prompt }] }],\n tools: [{ googleSearch: {} }],\n } as any);\n\n const response = result.response;\n const text = response.text();\n\n if (!text) {\n console.warn('Verification returned empty response');\n return new Map();\n }\n\n // 解析验证结果\n const verificationResults = parseVerificationResponse(text);\n\n // 构建映射\n const resultMap = new Map<string, string>();\n verificationResults.forEach((result) => {\n if (result.found && result.verifiedName) {\n resultMap.set(result.originalName, result.verifiedName);\n }\n });\n\n return resultMap;\n } catch (error) {\n console.error('Batch verification failed:', error);\n return new Map();\n }\n}\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- 系统会自动将附加费用合并到它前面的商品中\n- 这些附加费用不会作为独立商品返回\n\n归属规则(按照这个顺序排列):\n- 商品A\n- 商品A的押金(如果有)\n- 商品A的折扣(如果有)\n- 商品B\n- 商品B的押金(如果有)\n- ...\n\n**关于 total(总金额)的提取规则:**\n- 从小票底部找到 \"TOTAL\"、\"总计\"、\"合计\" 等标记\n- 提取对应的金额数字\n- 这是小票的最终应付金额\n\n只返回 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. 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 // 折扣:累加(通常已经是负数)\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';\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 receipt = await extractReceiptItems(imageBuffer);\n * console.log(receipt.items); // 商品列表\n * console.log(receipt.total); // 总金额\n * \n * // 使用自动验证(Google Search)\n * const receipt = await extractReceiptItems(imageBuffer, {\n * autoVerify: true\n * });\n * \n * // 带自定义验证回调\n * const receipt = 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<ReceiptData> {\n // 1. 调用 Gemini API\n const responseText = await callGemini(image, EXTRACTION_PROMPT);\n\n // 2. 解析响应\n const { items: parsedItems, total } = 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 for (const item of parsedItems) {\n if (item.needsVerification) {\n try {\n // 每次调用时动态创建验证上下文,确保包含最新的验证状态\n // 创建深拷贝以防止外部修改内部数据\n const publicItems: ReceiptItem[] = parsedItems.map(({ needsVerification, ...item }) => ({...item}));\n const verificationContext: VerificationContext = {\n rawText: responseText,\n allItems: publicItems,\n };\n \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 }) => ({\n ...item // 创建新对象,确保外部无法修改内部数据\n }));\n\n return {\n items: finalItems,\n total,\n };\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","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"]}
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "receipt-ocr",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "A TypeScript library for extracting structured product data from receipt images using multimodal LLMs",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./dist/index.cjs",
|
|
7
|
-
"module": "./dist/index.js",
|
|
8
|
-
"types": "./dist/index.d.ts",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.js",
|
|
13
|
-
"require": "./dist/index.cjs"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "tsup",
|
|
21
|
-
"dev": "tsup --watch",
|
|
22
|
-
"type-check": "tsc --noEmit",
|
|
23
|
-
"test": "vitest run",
|
|
24
|
-
"test:watch": "vitest"
|
|
25
|
-
},
|
|
26
|
-
"keywords": [
|
|
27
|
-
"receipt",
|
|
28
|
-
"ocr",
|
|
29
|
-
"gemini",
|
|
30
|
-
"llm",
|
|
31
|
-
"multimodal",
|
|
32
|
-
"typescript"
|
|
33
|
-
],
|
|
34
|
-
"author": "Beinan Hu",
|
|
35
|
-
"license": "MIT",
|
|
36
|
-
"repository": {
|
|
37
|
-
"type": "git",
|
|
38
|
-
"url": "git+https://github.com/cosplit-now/receipt-ocr.git"
|
|
39
|
-
},
|
|
40
|
-
"homepage": "https://github.com/cosplit-now/receipt-ocr#readme",
|
|
41
|
-
"bugs": {
|
|
42
|
-
"url": "https://github.com/cosplit-now/receipt-ocr/issues"
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"@google/generative-ai": "^0.21.0"
|
|
46
|
-
},
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"@types/node": "^22.10.2",
|
|
49
|
-
"tsup": "^8.3.5",
|
|
50
|
-
"typescript": "^5.7.2",
|
|
51
|
-
"vitest": "^2.1.8"
|
|
52
|
-
},
|
|
53
|
-
"engines": {
|
|
54
|
-
"node": ">=18.0.0"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "receipt-ocr",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "A TypeScript library for extracting structured product data from receipt images using multimodal LLMs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"dev": "tsup --watch",
|
|
22
|
+
"type-check": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:watch": "vitest"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"receipt",
|
|
28
|
+
"ocr",
|
|
29
|
+
"gemini",
|
|
30
|
+
"llm",
|
|
31
|
+
"multimodal",
|
|
32
|
+
"typescript"
|
|
33
|
+
],
|
|
34
|
+
"author": "Beinan Hu",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/cosplit-now/receipt-ocr.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/cosplit-now/receipt-ocr#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/cosplit-now/receipt-ocr/issues"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@google/generative-ai": "^0.21.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^22.10.2",
|
|
49
|
+
"tsup": "^8.3.5",
|
|
50
|
+
"typescript": "^5.7.2",
|
|
51
|
+
"vitest": "^2.1.8"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=18.0.0"
|
|
55
|
+
}
|
|
56
|
+
}
|