chaimi-bookkeeping-mcp 3.1.21 → 3.1.23
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 +20 -0
- package/oauth.js +72 -2
- package/package.json +1 -1
- package/server.js +134 -35
package/README.md
CHANGED
|
@@ -8,6 +8,15 @@
|
|
|
8
8
|
- macOS / Windows / Linux
|
|
9
9
|
- 微信(已安装"柴米AI记账"小程序)
|
|
10
10
|
|
|
11
|
+
## ⚠️ 重要提示(Agent开发者必读)
|
|
12
|
+
|
|
13
|
+
**如果您是 AI Agent 开发者,请注意:**
|
|
14
|
+
|
|
15
|
+
1. **不要缓存工具定义**:MCP 工具定义可能会更新,请在每次调用前通过 `mcporter list` 获取最新定义
|
|
16
|
+
2. **关注版本号**:工具描述中包含版本号(如【v3.1.21】),版本变化时请更新您的 Skill
|
|
17
|
+
3. **save_receipt 工具要求**:items 数组中每个商品必须包含完整的4个字段:`name`、`amount`、`price`、`quantity`
|
|
18
|
+
4. **自动填充字段**:`agentType`、`apiProvider`、`rawInput`、`mcp_version` 为自动填充字段,无需用户输入
|
|
19
|
+
|
|
11
20
|
## 快速安装
|
|
12
21
|
|
|
13
22
|
### 方式一:npm 全局安装(推荐)
|
|
@@ -134,6 +143,17 @@ AI 会自动调用 `save_income` 工具记录收入。
|
|
|
134
143
|
|
|
135
144
|
## 更新日志
|
|
136
145
|
|
|
146
|
+
### v3.1.23 (2026-04-13)
|
|
147
|
+
- **安全** 新增请求签名验证,防止 Agent 直接 curl 访问云函数
|
|
148
|
+
- **安全** Token 加密存储(AES-256-CBC),绑定机器标识
|
|
149
|
+
- **安全** URL 加密存储,防止明文暴露
|
|
150
|
+
- **新增** SKILL.md 定义安全约束和触发规则
|
|
151
|
+
- **优化** 授权提示文案,增加 AUTH_JUST_COMPLETED 标记
|
|
152
|
+
|
|
153
|
+
### v3.1.22 (2026-04-12)
|
|
154
|
+
- **修复** fillDefaults 金额计算逻辑(根据 price × quantity 计算 amount)
|
|
155
|
+
- **修复** 解决辣可可小票价格显示为0元的问题
|
|
156
|
+
|
|
137
157
|
### v3.1.21 (2026-04-12)
|
|
138
158
|
- **修复** 版本号同步更新
|
|
139
159
|
|
package/oauth.js
CHANGED
|
@@ -8,6 +8,7 @@ const fetch = require('node-fetch');
|
|
|
8
8
|
const { exec } = require('child_process');
|
|
9
9
|
const util = require('util');
|
|
10
10
|
const execPromise = util.promisify(exec);
|
|
11
|
+
const crypto = require('crypto');
|
|
11
12
|
|
|
12
13
|
class OAuthManager {
|
|
13
14
|
constructor(config) {
|
|
@@ -375,20 +376,73 @@ class TokenStorage {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
379
|
+
// 生成机器唯一标识(用于加密密钥)
|
|
380
|
+
function getMachineId() {
|
|
381
|
+
const os = require('os');
|
|
382
|
+
const interfaces = os.networkInterfaces();
|
|
383
|
+
let macAddress = '';
|
|
384
|
+
|
|
385
|
+
// 获取第一个非本地 MAC 地址
|
|
386
|
+
for (const name of Object.keys(interfaces)) {
|
|
387
|
+
for (const iface of interfaces[name]) {
|
|
388
|
+
if (iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
389
|
+
macAddress = iface.mac;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (macAddress) break;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 结合主机名和平台信息生成密钥
|
|
397
|
+
const keyMaterial = `${macAddress}-${os.hostname()}-${os.platform()}`;
|
|
398
|
+
return crypto.createHash('sha256').update(keyMaterial).digest('hex').substring(0, 32);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// 加密 Token
|
|
402
|
+
function encryptToken(token, key) {
|
|
403
|
+
const iv = crypto.randomBytes(16);
|
|
404
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
405
|
+
let encrypted = cipher.update(JSON.stringify(token), 'utf8', 'hex');
|
|
406
|
+
encrypted += cipher.final('hex');
|
|
407
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 解密 Token
|
|
411
|
+
function decryptToken(encryptedData, key) {
|
|
412
|
+
const parts = encryptedData.split(':');
|
|
413
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
414
|
+
const encrypted = parts[1];
|
|
415
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
|
|
416
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
417
|
+
decrypted += decipher.final('utf8');
|
|
418
|
+
return JSON.parse(decrypted);
|
|
419
|
+
}
|
|
420
|
+
|
|
378
421
|
class FileTokenStorage extends TokenStorage {
|
|
379
422
|
constructor(filePath) {
|
|
380
423
|
super();
|
|
381
424
|
this.filePath = filePath;
|
|
382
425
|
this.fs = require('fs').promises;
|
|
383
426
|
this.path = require('path');
|
|
427
|
+
this.key = getMachineId();
|
|
384
428
|
}
|
|
385
429
|
|
|
386
430
|
async save(token) {
|
|
387
431
|
const dir = this.path.dirname(this.filePath);
|
|
388
432
|
await this.fs.mkdir(dir, { recursive: true });
|
|
433
|
+
|
|
434
|
+
// 加密 Token 后保存
|
|
435
|
+
const encrypted = encryptToken(token, this.key);
|
|
436
|
+
const data = {
|
|
437
|
+
version: '2.0',
|
|
438
|
+
encrypted: encrypted,
|
|
439
|
+
algorithm: 'aes-256-cbc',
|
|
440
|
+
updatedAt: new Date().toISOString()
|
|
441
|
+
};
|
|
442
|
+
|
|
389
443
|
await this.fs.writeFile(
|
|
390
444
|
this.filePath,
|
|
391
|
-
JSON.stringify(
|
|
445
|
+
JSON.stringify(data, null, 2),
|
|
392
446
|
{ mode: 0o600 }
|
|
393
447
|
);
|
|
394
448
|
}
|
|
@@ -396,7 +450,23 @@ class FileTokenStorage extends TokenStorage {
|
|
|
396
450
|
async load() {
|
|
397
451
|
try {
|
|
398
452
|
const data = await this.fs.readFile(this.filePath, 'utf8');
|
|
399
|
-
|
|
453
|
+
const parsed = JSON.parse(data);
|
|
454
|
+
|
|
455
|
+
// 向后兼容:检测旧版明文格式
|
|
456
|
+
if (!parsed.version || parsed.version === '1.0') {
|
|
457
|
+
console.error('检测到旧版 Token 格式,自动升级...');
|
|
458
|
+
// 返回明文数据,但下次保存时会自动加密
|
|
459
|
+
return {
|
|
460
|
+
accessToken: parsed.accessToken,
|
|
461
|
+
refreshToken: parsed.refreshToken,
|
|
462
|
+
expiresAt: parsed.expiresAt,
|
|
463
|
+
expiresIn: parsed.expiresIn,
|
|
464
|
+
tokenType: parsed.tokenType
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 新版加密格式,解密后返回
|
|
469
|
+
return decryptToken(parsed.encrypted, this.key);
|
|
400
470
|
} catch (err) {
|
|
401
471
|
if (err.code === 'ENOENT') {
|
|
402
472
|
return null;
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
const os = require('os');
|
|
23
23
|
const fs = require('fs');
|
|
24
|
+
const crypto = require('crypto');
|
|
24
25
|
|
|
25
26
|
// 读取 package.json 获取版本
|
|
26
27
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
@@ -29,10 +30,34 @@ const MCP_VERSION = packageJson.version;
|
|
|
29
30
|
// 导入 OAuth 模块
|
|
30
31
|
const { OAuthManager, FileTokenStorage } = require('./oauth.js');
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// API 签名密钥(用于验证请求来自合法 MCP Server)
|
|
34
|
+
const CHAIMI_API_SECRET = 'chaimi-mcp-secret-2024';
|
|
35
|
+
|
|
36
|
+
// URL 加密密钥
|
|
37
|
+
const URL_ENCRYPT_KEY = 'chaimi-url-key-2024';
|
|
38
|
+
|
|
39
|
+
// 加密的云函数 URL
|
|
40
|
+
const ENCRYPTED_URLS = {
|
|
41
|
+
hub: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571320140b40044e05',
|
|
42
|
+
oauth: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f571327201c1901',
|
|
43
|
+
prompt: 'enc:v1:0b1c15191e53025a1100421e0148000057545156020903080f1d431054180f484819030203035158595043085d5801044c0502114c5b1e53441346150a01065811100d5e0e4b1a425f1f5f5713381306001959'
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 解密 URL 函数
|
|
47
|
+
function decryptUrl(encrypted, key) {
|
|
48
|
+
const hex = encrypted.replace('enc:v1:', '');
|
|
49
|
+
let result = '';
|
|
50
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
51
|
+
const byte = parseInt(hex.substr(i, 2), 16);
|
|
52
|
+
result += String.fromCharCode(byte ^ key.charCodeAt((i / 2) % key.length));
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 配置 - 微信云函数(运行时解密)
|
|
58
|
+
const MCP_HUB_URL = process.env.MCP_HUB_URL || decryptUrl(ENCRYPTED_URLS.hub, URL_ENCRYPT_KEY);
|
|
59
|
+
const MCP_PROMPT_URL = process.env.MCP_PROMPT_URL || decryptUrl(ENCRYPTED_URLS.prompt, URL_ENCRYPT_KEY);
|
|
60
|
+
const MCP_OAUTH_URL = process.env.MCP_OAUTH_URL || decryptUrl(ENCRYPTED_URLS.oauth, URL_ENCRYPT_KEY);
|
|
36
61
|
|
|
37
62
|
// Token 缓存
|
|
38
63
|
let cachedToken = null;
|
|
@@ -118,41 +143,41 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
118
143
|
},
|
|
119
144
|
{
|
|
120
145
|
name: 'save_receipt',
|
|
121
|
-
description: '
|
|
146
|
+
description: '【v3.1.21】【图片小票专用】保存购物小票/发票/收据。⚠️ 重要:items数组中每个商品必须包含完整的4个字段:name(商品名称)、amount(金额=单价×数量)、price(单价)、quantity(数量)。示例:[{"name":"苹果","amount":5.5,"price":5.5,"quantity":1}]',
|
|
122
147
|
inputSchema: {
|
|
123
148
|
type: 'object',
|
|
124
149
|
properties: {
|
|
125
|
-
store: { type: 'string', description: '
|
|
150
|
+
store: { type: 'string', description: '商家名称(必填)' },
|
|
126
151
|
date: { type: 'string', description: '消费时间(ISO 8601 格式,必须包含日期和时间,如:2026-04-10T13:06:21)' },
|
|
127
|
-
totalAmount: { type: 'number', description: '
|
|
128
|
-
originalAmount: { type: 'number', description: '
|
|
152
|
+
totalAmount: { type: 'number', description: '总金额(所有商品amount之和)' },
|
|
153
|
+
originalAmount: { type: 'number', description: '应付金额(优惠前)' },
|
|
129
154
|
discountAmount: { type: 'number', description: '优惠金额' },
|
|
130
|
-
actualAmount: { type: 'number', description: '
|
|
131
|
-
paymentMethod: { type: 'string', description: '
|
|
155
|
+
actualAmount: { type: 'number', description: '实付金额(优惠后)' },
|
|
156
|
+
paymentMethod: { type: 'string', description: '支付方式,如:微信支付、支付宝' },
|
|
132
157
|
receiptNo: { type: 'string', description: '小票编号' },
|
|
133
158
|
items: {
|
|
134
159
|
type: 'array',
|
|
135
|
-
description: '
|
|
160
|
+
description: '【必填】商品列表,必须是数组格式。每个商品必须包含:name、amount、price、quantity。⚠️ 注意:amount=price×quantity',
|
|
136
161
|
items: {
|
|
137
162
|
type: 'object',
|
|
138
163
|
properties: {
|
|
139
|
-
name: { type: 'string', description: '
|
|
140
|
-
originalName: { type: 'string', description: '
|
|
141
|
-
price: { type: 'number', description: '
|
|
142
|
-
quantity: { type: 'number', description: '
|
|
143
|
-
unit: { type: 'string', description: '
|
|
144
|
-
amount: { type: 'number', description: '
|
|
164
|
+
name: { type: 'string', description: '【必填】商品名称' },
|
|
165
|
+
originalName: { type: 'string', description: '原始商品名称(含规格)' },
|
|
166
|
+
price: { type: 'number', description: '【必填】单价' },
|
|
167
|
+
quantity: { type: 'number', description: '【必填】数量' },
|
|
168
|
+
unit: { type: 'string', description: '单位,如:斤、个、份' },
|
|
169
|
+
amount: { type: 'number', description: '【必填】金额,必须等于 price × quantity' },
|
|
145
170
|
weight: { type: 'string', description: '重量' },
|
|
146
|
-
marketPrice: { type: 'string', description: '
|
|
147
|
-
category: { type: 'string', description: '
|
|
171
|
+
marketPrice: { type: 'string', description: '市场单价(元/500g)' },
|
|
172
|
+
category: { type: 'string', description: '分类,如:餐饮、食品、购物' },
|
|
148
173
|
},
|
|
149
174
|
required: ['name', 'amount', 'price', 'quantity'],
|
|
150
175
|
},
|
|
151
176
|
},
|
|
152
|
-
agentType: { type: 'string', description: '
|
|
153
|
-
apiProvider: { type: 'string', description: '
|
|
154
|
-
rawInput: { type: 'string', description: '
|
|
155
|
-
mcp_version: { type: 'string', description: 'MCP Server
|
|
177
|
+
agentType: { type: 'string', description: '【自动填充】Agent类型:claude-desktop、cursor、openclaw、workbuddy、trae' },
|
|
178
|
+
apiProvider: { type: 'string', description: '【自动填充】AI服务提供商:anthropic、openai、doubao、aliyun' },
|
|
179
|
+
rawInput: { type: 'string', description: '【自动填充】用户的原始输入内容' },
|
|
180
|
+
mcp_version: { type: 'string', description: '【自动填充】MCP Server版本号' },
|
|
156
181
|
},
|
|
157
182
|
required: ['store', 'items'],
|
|
158
183
|
},
|
|
@@ -278,14 +303,15 @@ async function getToken() {
|
|
|
278
303
|
await oauthManager.tokenStorage.save(token);
|
|
279
304
|
await oauthManager.clearAuthState();
|
|
280
305
|
console.error('✅ 授权成功!可以继续使用记账功能');
|
|
281
|
-
|
|
306
|
+
// 返回特殊标记,让上层知道这是刚完成授权的情况
|
|
307
|
+
throw new Error(`AUTH_JUST_COMPLETED:${cachedToken}`);
|
|
282
308
|
}
|
|
283
309
|
// 用户还未授权,返回同一个验证码
|
|
284
310
|
authState.deviceCode = savedAuthState.deviceCode;
|
|
285
311
|
authState.userCode = savedAuthState.userCode;
|
|
286
312
|
throw new Error(`NEED_AUTH:${savedAuthState.userCode}`);
|
|
287
313
|
} catch (err) {
|
|
288
|
-
if (err.message.startsWith('NEED_AUTH:')) {
|
|
314
|
+
if (err.message.startsWith('NEED_AUTH:') || err.message.startsWith('AUTH_JUST_COMPLETED:')) {
|
|
289
315
|
throw err;
|
|
290
316
|
}
|
|
291
317
|
// 其他错误,继续获取新的验证码
|
|
@@ -348,18 +374,33 @@ async function callMcpPrompt(tool, params, token) {
|
|
|
348
374
|
return await response.json();
|
|
349
375
|
}
|
|
350
376
|
|
|
377
|
+
// 生成请求签名(用于验证请求来自合法 MCP Server)
|
|
378
|
+
function generateSignature(body, timestamp, secret) {
|
|
379
|
+
const payload = JSON.stringify(body) + timestamp;
|
|
380
|
+
return crypto
|
|
381
|
+
.createHmac('sha256', secret)
|
|
382
|
+
.update(payload)
|
|
383
|
+
.digest('hex');
|
|
384
|
+
}
|
|
385
|
+
|
|
351
386
|
// 调用 mcpHub 云函数
|
|
352
387
|
async function callMcpHub(tool, params, token) {
|
|
388
|
+
const body = {
|
|
389
|
+
tool: tool,
|
|
390
|
+
params: params,
|
|
391
|
+
};
|
|
392
|
+
const timestamp = Date.now().toString();
|
|
393
|
+
const signature = generateSignature(body, timestamp, CHAIMI_API_SECRET);
|
|
394
|
+
|
|
353
395
|
const response = await fetch(MCP_HUB_URL, {
|
|
354
396
|
method: 'POST',
|
|
355
397
|
headers: {
|
|
356
398
|
'Content-Type': 'application/json',
|
|
357
399
|
'Authorization': `Bearer ${token}`,
|
|
400
|
+
'X-Chaimi-Signature': signature,
|
|
401
|
+
'X-Chaimi-Timestamp': timestamp,
|
|
358
402
|
},
|
|
359
|
-
body: JSON.stringify(
|
|
360
|
-
tool: tool,
|
|
361
|
-
params: params,
|
|
362
|
-
}),
|
|
403
|
+
body: JSON.stringify(body),
|
|
363
404
|
});
|
|
364
405
|
|
|
365
406
|
if (!response.ok) {
|
|
@@ -418,6 +459,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
418
459
|
}
|
|
419
460
|
|
|
420
461
|
case 'save_receipt': {
|
|
462
|
+
// 0. 参数格式强制检查 - 确保 items 中每个商品都有完整的字段
|
|
463
|
+
if (processedArgs.items && Array.isArray(processedArgs.items)) {
|
|
464
|
+
const invalidItems = [];
|
|
465
|
+
processedArgs.items.forEach((item, index) => {
|
|
466
|
+
const missingFields = [];
|
|
467
|
+
if (!item.hasOwnProperty('name')) missingFields.push('name');
|
|
468
|
+
if (!item.hasOwnProperty('amount')) missingFields.push('amount');
|
|
469
|
+
if (!item.hasOwnProperty('price')) missingFields.push('price');
|
|
470
|
+
if (!item.hasOwnProperty('quantity')) missingFields.push('quantity');
|
|
471
|
+
|
|
472
|
+
if (missingFields.length > 0) {
|
|
473
|
+
invalidItems.push(`商品${index + 1}(${item.name || '未命名'})缺少字段: ${missingFields.join(', ')}`);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (invalidItems.length > 0) {
|
|
478
|
+
result = {
|
|
479
|
+
success: false,
|
|
480
|
+
error: `参数格式错误:${invalidItems.join('; ')}。请更新您的 MCP Skill 或检查 items 数组格式。每个商品必须包含:name, amount, price, quantity`,
|
|
481
|
+
code: 400
|
|
482
|
+
};
|
|
483
|
+
userMessage = `❌ 保存失败\n\n错误:商品数据格式不完整\n\n${invalidItems.join('\n')}\n\n💡 解决方案:\n1. 请更新您的柴米记账 MCP Skill\n2. 确保每个商品包含完整的4个字段:name, amount, price, quantity\n3. amount 必须等于 price × quantity`;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
421
488
|
// 1. 调用 parseReceipt 重新提取小票信息(覆盖 Agent 传的数据)
|
|
422
489
|
if (processedArgs.rawInput) {
|
|
423
490
|
const parseResult = await callMcpPrompt(
|
|
@@ -592,15 +659,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
592
659
|
type: 'text',
|
|
593
660
|
text: `🔐 首次使用需要授权
|
|
594
661
|
|
|
595
|
-
|
|
662
|
+
━━━━━━━━━━━━━━
|
|
663
|
+
📱 验证码:**${userCode}**
|
|
664
|
+
━━━━━━━━━━━━━━
|
|
596
665
|
|
|
597
666
|
请在"柴米AI记账"小程序中完成授权:
|
|
598
|
-
1. 打开微信小程序"柴米AI记账"
|
|
599
|
-
2. 点击"我的" → "🤖 Agent 授权"
|
|
600
|
-
3. 输入验证码:**${userCode}**
|
|
601
|
-
4. 点击确认授权
|
|
602
667
|
|
|
603
|
-
|
|
668
|
+
1️⃣ 打开微信小程序"柴米AI记账"
|
|
669
|
+
2️⃣ 点击"我的" → "🤖 Agent 授权"
|
|
670
|
+
3️⃣ 输入验证码:**${userCode}**
|
|
671
|
+
4️⃣ 点击确认授权
|
|
672
|
+
|
|
673
|
+
⚠️ 重要提示:
|
|
674
|
+
• 授权完成后,MCP Server 会自动退出(正常现象)
|
|
675
|
+
• 请重新发送您的记账指令,我才能为您完成记账
|
|
676
|
+
• 首次授权后,后续记账将无需再次授权
|
|
677
|
+
|
|
678
|
+
⏳ 验证码有效期:30分钟`,
|
|
604
679
|
},
|
|
605
680
|
],
|
|
606
681
|
};
|
|
@@ -616,6 +691,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
616
691
|
}
|
|
617
692
|
}
|
|
618
693
|
|
|
694
|
+
// 处理刚完成授权的情况
|
|
695
|
+
if (error.message.startsWith('AUTH_JUST_COMPLETED:')) {
|
|
696
|
+
return {
|
|
697
|
+
content: [
|
|
698
|
+
{
|
|
699
|
+
type: 'text',
|
|
700
|
+
text: `✅ 授权成功!
|
|
701
|
+
|
|
702
|
+
━━━━━━━━━━━━━━
|
|
703
|
+
🎉 恭喜!您已完成授权
|
|
704
|
+
━━━━━━━━━━━━━━
|
|
705
|
+
|
|
706
|
+
现在可以正常使用记账功能了!
|
|
707
|
+
|
|
708
|
+
请重新发送您的记账指令,例如:
|
|
709
|
+
• "记录一笔午餐 35元"
|
|
710
|
+
• "保存小票,商家是沃尔玛,商品有..."
|
|
711
|
+
|
|
712
|
+
首次授权后,后续记账将无需再次授权。`,
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
619
718
|
return {
|
|
620
719
|
content: [
|
|
621
720
|
{
|