@seasonkoh/webaz 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/layer1-agent/L1-1-mcp-server/server.js +295 -9
- package/dist/pwa/public/app.js +3062 -0
- package/dist/pwa/public/i18n.js +580 -0
- package/dist/pwa/public/index.html +18 -0
- package/dist/pwa/public/manifest.json +13 -0
- package/dist/pwa/public/style.css +262 -0
- package/dist/pwa/public/sw.js +24 -0
- package/dist/pwa/server.js +1840 -9
- package/package.json +4 -2
|
@@ -23,6 +23,10 @@ import { initSkillSchema, publishSkill, listSkills, getMySkills, subscribeSkill,
|
|
|
23
23
|
import { initReputationSchema, recordOrderReputation, recordDisputeReputation, getReputation, getSearchBoost, getStakeDiscount, } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
|
|
24
24
|
import { generateManifest, getManifestSummary, MANIFEST_URI, } from '../../layer0-foundation/L0-5-manifest/manifest.js';
|
|
25
25
|
import { requireAuth } from './auth.js';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
27
|
+
const SERVER_VERSION = '0.1.8';
|
|
28
|
+
const TELEMETRY_URL = process.env.WEBAZ_TELEMETRY_URL ?? 'https://webaz.xyz/api/mcp-telemetry';
|
|
29
|
+
const TELEMETRY_ENABLED = (process.env.WEBAZ_TELEMETRY ?? 'on').toLowerCase() !== 'off';
|
|
26
30
|
// ─── 初始化 ──────────────────────────────────────────────────
|
|
27
31
|
const db = initDatabase();
|
|
28
32
|
initSystemUser(db);
|
|
@@ -30,6 +34,52 @@ initDisputeSchema(db);
|
|
|
30
34
|
initNotificationSchema(db);
|
|
31
35
|
initSkillSchema(db);
|
|
32
36
|
initReputationSchema(db);
|
|
37
|
+
// 结构化商品字段迁移(幂等)
|
|
38
|
+
const MCP_PRODUCT_COLS = [
|
|
39
|
+
'ALTER TABLE products ADD COLUMN specs TEXT',
|
|
40
|
+
'ALTER TABLE products ADD COLUMN brand TEXT',
|
|
41
|
+
'ALTER TABLE products ADD COLUMN model TEXT',
|
|
42
|
+
'ALTER TABLE products ADD COLUMN source_url TEXT',
|
|
43
|
+
'ALTER TABLE products ADD COLUMN source_price REAL',
|
|
44
|
+
'ALTER TABLE products ADD COLUMN ship_regions TEXT DEFAULT "全国"',
|
|
45
|
+
'ALTER TABLE products ADD COLUMN handling_hours INTEGER DEFAULT 24',
|
|
46
|
+
'ALTER TABLE products ADD COLUMN estimated_days TEXT',
|
|
47
|
+
'ALTER TABLE products ADD COLUMN fragile INTEGER DEFAULT 0',
|
|
48
|
+
'ALTER TABLE products ADD COLUMN return_days INTEGER DEFAULT 7',
|
|
49
|
+
'ALTER TABLE products ADD COLUMN return_condition TEXT',
|
|
50
|
+
'ALTER TABLE products ADD COLUMN warranty_days INTEGER DEFAULT 0',
|
|
51
|
+
];
|
|
52
|
+
for (const sql of MCP_PRODUCT_COLS) {
|
|
53
|
+
try {
|
|
54
|
+
db.exec(sql);
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
}
|
|
58
|
+
db.exec(`
|
|
59
|
+
CREATE TABLE IF NOT EXISTS price_sessions (
|
|
60
|
+
token TEXT PRIMARY KEY,
|
|
61
|
+
product_id TEXT NOT NULL,
|
|
62
|
+
user_id TEXT NOT NULL,
|
|
63
|
+
price REAL NOT NULL,
|
|
64
|
+
quantity INTEGER NOT NULL DEFAULT 1,
|
|
65
|
+
created_at TEXT NOT NULL,
|
|
66
|
+
expires_at TEXT NOT NULL,
|
|
67
|
+
used_at TEXT
|
|
68
|
+
)
|
|
69
|
+
`);
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS mcp_tool_calls (
|
|
72
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
73
|
+
tool_name TEXT NOT NULL,
|
|
74
|
+
user_id TEXT,
|
|
75
|
+
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
76
|
+
outcome TEXT NOT NULL,
|
|
77
|
+
latency_ms INTEGER NOT NULL,
|
|
78
|
+
error_msg TEXT
|
|
79
|
+
)
|
|
80
|
+
`);
|
|
81
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tool_calls_ts ON mcp_tool_calls(ts)`);
|
|
82
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tool_calls_tool ON mcp_tool_calls(tool_name, ts)`);
|
|
33
83
|
// ─── 工具定义(Agent 读这些来理解如何使用协议)────────────────
|
|
34
84
|
const TOOLS = [
|
|
35
85
|
{
|
|
@@ -76,23 +126,45 @@ const TOOLS = [
|
|
|
76
126
|
name: 'webaz_search',
|
|
77
127
|
description: `搜索 WebAZ中的在售商品。
|
|
78
128
|
无需登录即可搜索,买家或 Agent 可以自由浏览。
|
|
79
|
-
|
|
129
|
+
返回匹配的商品列表,包含价格、结构化规格(specs)、物流信息、售后政策、agent_summary。
|
|
130
|
+
agent_summary 是一句话决策摘要,Agent 可直接用于比价决策。`,
|
|
80
131
|
inputSchema: {
|
|
81
132
|
type: 'object',
|
|
82
133
|
properties: {
|
|
83
134
|
query: { type: 'string', description: '搜索关键词(商品名称或描述)' },
|
|
84
135
|
category: { type: 'string', description: '商品分类过滤(可选)' },
|
|
85
136
|
max_price: { type: 'number', description: '最高价格过滤(可选)' },
|
|
137
|
+
min_return_days: { type: 'number', description: '最少退货天数(可选,如 7 表示只看支持7天退货的商品)' },
|
|
138
|
+
max_handling_hours: { type: 'number', description: '最长发货时效小时数(可选,如 24 表示只看24h内发货的)' },
|
|
86
139
|
limit: { type: 'number', description: '返回数量上限,默认 10' },
|
|
87
140
|
},
|
|
88
141
|
},
|
|
89
142
|
},
|
|
143
|
+
{
|
|
144
|
+
name: 'webaz_verify_price',
|
|
145
|
+
description: `在下单前锁定商品价格,获取 session_token。
|
|
146
|
+
Agent 代理用户下单时,应先调用此工具验证并锁定价格,再调用 webaz_place_order 传入 session_token。
|
|
147
|
+
这样可以:1)确保下单时价格与展示价格一致;2)若价格已变动,会提前告知用户;3)平台只对 T0 时刻价格负责。
|
|
148
|
+
session_token 有效期 10 分钟,使用一次后失效。`,
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
api_key: { type: 'string', description: '买家的 api_key' },
|
|
153
|
+
product_id: { type: 'string', description: '商品 ID(从 webaz_search 获得)' },
|
|
154
|
+
quantity: { type: 'number', description: '购买数量,默认 1' },
|
|
155
|
+
},
|
|
156
|
+
required: ['api_key', 'product_id'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
90
159
|
{
|
|
91
160
|
name: 'webaz_list_product',
|
|
92
161
|
description: `卖家上架新商品到 WebAZ。
|
|
93
162
|
需要卖家角色的 api_key。
|
|
94
163
|
上架时系统会自动计算建议质押金额(商品价格的 15%),用于保障买家权益。
|
|
95
|
-
|
|
164
|
+
商品上架后买家可以搜索到并下单。
|
|
165
|
+
|
|
166
|
+
填得越完整,agent_summary 越能帮买家 Agent 做比价决策(品牌/型号/退货/发货时效/质保等)。
|
|
167
|
+
注意:如需价格对标外部链接(独家价/补贴模式),请通过 PWA Web 端上架——链接认领需要走众包验证流程。`,
|
|
96
168
|
inputSchema: {
|
|
97
169
|
type: 'object',
|
|
98
170
|
properties: {
|
|
@@ -102,6 +174,26 @@ const TOOLS = [
|
|
|
102
174
|
price: { type: 'number', description: '商品价格(WAZ)' },
|
|
103
175
|
stock: { type: 'number', description: '库存数量,默认 1' },
|
|
104
176
|
category: { type: 'string', description: '商品分类(可选)' },
|
|
177
|
+
specs: {
|
|
178
|
+
type: 'object',
|
|
179
|
+
description: '结构化规格键值对,如 {"颜色":"黑色","内存":"16GB","存储":"512GB"}(可选)',
|
|
180
|
+
},
|
|
181
|
+
brand: { type: 'string', description: '品牌(可选)' },
|
|
182
|
+
model: { type: 'string', description: '型号(可选)' },
|
|
183
|
+
source_price: {
|
|
184
|
+
type: 'number',
|
|
185
|
+
description: '同款商品的外部参考价(可选,仅作展示,不参与独家价认证——需通过 PWA 完成链接认领)',
|
|
186
|
+
},
|
|
187
|
+
ship_regions: { type: 'string', description: '发货地区,默认"全国"' },
|
|
188
|
+
handling_hours: { type: 'number', description: '发货时效(小时),默认 24' },
|
|
189
|
+
estimated_days: {
|
|
190
|
+
type: 'object',
|
|
191
|
+
description: '预计送达天数:数字(如 4)或区域映射(如 {"江浙沪":2,"全国":4})',
|
|
192
|
+
},
|
|
193
|
+
fragile: { type: 'boolean', description: '是否易碎品,默认 false' },
|
|
194
|
+
return_days: { type: 'number', description: '支持退货天数,默认 7(填 0 表示不支持退货)' },
|
|
195
|
+
return_condition: { type: 'string', description: '退货条件说明(可选)' },
|
|
196
|
+
warranty_days: { type: 'number', description: '质保天数,默认 0' },
|
|
105
197
|
},
|
|
106
198
|
required: ['api_key', 'title', 'description', 'price'],
|
|
107
199
|
},
|
|
@@ -120,6 +212,10 @@ const TOOLS = [
|
|
|
120
212
|
quantity: { type: 'number', description: '购买数量,默认 1' },
|
|
121
213
|
shipping_address: { type: 'string', description: '收货地址' },
|
|
122
214
|
notes: { type: 'string', description: '给卖家的备注(可选)' },
|
|
215
|
+
session_token: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
description: '价格锁定 token(推荐):由 webaz_verify_price 返回,确保下单价格与展示价格一致',
|
|
218
|
+
},
|
|
123
219
|
promoter_api_key: {
|
|
124
220
|
type: 'string',
|
|
125
221
|
description: '推荐人的 api_key(可选,如果是通过推荐链接来的)',
|
|
@@ -363,7 +459,8 @@ function handleInfo() {
|
|
|
363
459
|
},
|
|
364
460
|
quick_start: {
|
|
365
461
|
seller: '1. webaz_register(role=seller) → 2. webaz_list_product() → 3. 等通知 webaz_update_order(accept/ship)',
|
|
366
|
-
buyer: '1. webaz_register(role=buyer) → 2. webaz_search() → 3.
|
|
462
|
+
buyer: '1. webaz_register(role=buyer) → 2. webaz_search() → 3. webaz_verify_price() → 4. webaz_place_order(session_token) → 5. webaz_update_order(confirm)',
|
|
463
|
+
agent_buying: '用户提供链接 → 1. webaz_search(query) 找到更优方案 → 2. webaz_verify_price(product_id) 锁定价格 → 3. webaz_place_order(session_token) 下单 → 返回成交理由',
|
|
367
464
|
logistics: '1. webaz_register(role=logistics) → 2. webaz_update_order(pickup) → webaz_update_order(deliver)',
|
|
368
465
|
},
|
|
369
466
|
available_tools: TOOLS.map((t) => ({ name: t.name, description: t.description.split('\n')[0] })),
|
|
@@ -397,10 +494,52 @@ function handleRegister(args) {
|
|
|
397
494
|
: '等待订单分配给你',
|
|
398
495
|
};
|
|
399
496
|
}
|
|
497
|
+
function buildAgentSummary(p) {
|
|
498
|
+
const parts = [];
|
|
499
|
+
if (p.brand)
|
|
500
|
+
parts.push(String(p.brand));
|
|
501
|
+
if (p.model)
|
|
502
|
+
parts.push(String(p.model));
|
|
503
|
+
const returnDays = p.return_days != null ? Number(p.return_days) : null;
|
|
504
|
+
if (returnDays != null && returnDays > 0)
|
|
505
|
+
parts.push(`${returnDays}天退货`);
|
|
506
|
+
else if (returnDays === 0)
|
|
507
|
+
parts.push('不支持退货');
|
|
508
|
+
const warranty = p.warranty_days != null ? Number(p.warranty_days) : null;
|
|
509
|
+
if (warranty && warranty > 0)
|
|
510
|
+
parts.push(`${warranty}天质保`);
|
|
511
|
+
const handling = p.handling_hours != null ? Number(p.handling_hours) : null;
|
|
512
|
+
if (handling != null)
|
|
513
|
+
parts.push(`${handling}h发货`);
|
|
514
|
+
if (p.fragile)
|
|
515
|
+
parts.push('易碎品');
|
|
516
|
+
return parts.join(',') || '暂无物流信息';
|
|
517
|
+
}
|
|
518
|
+
function parseProductForAgent(p) {
|
|
519
|
+
let specs = null;
|
|
520
|
+
if (p.specs) {
|
|
521
|
+
try {
|
|
522
|
+
specs = JSON.parse(p.specs);
|
|
523
|
+
}
|
|
524
|
+
catch { }
|
|
525
|
+
}
|
|
526
|
+
let estimated_days = null;
|
|
527
|
+
if (p.estimated_days) {
|
|
528
|
+
try {
|
|
529
|
+
estimated_days = JSON.parse(p.estimated_days);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
estimated_days = null;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { ...p, specs, estimated_days, agent_summary: buildAgentSummary(p) };
|
|
536
|
+
}
|
|
400
537
|
function handleSearch(args) {
|
|
401
538
|
const query = args.query ?? '';
|
|
402
539
|
const category = args.category;
|
|
403
540
|
const maxPrice = args.max_price;
|
|
541
|
+
const minReturnDays = args.min_return_days;
|
|
542
|
+
const maxHandlingHours = args.max_handling_hours;
|
|
404
543
|
const limit = args.limit ?? 10;
|
|
405
544
|
let sql = `
|
|
406
545
|
SELECT p.*, u.name as seller_name
|
|
@@ -421,6 +560,14 @@ function handleSearch(args) {
|
|
|
421
560
|
sql += ` AND p.price <= ?`;
|
|
422
561
|
params.push(maxPrice);
|
|
423
562
|
}
|
|
563
|
+
if (minReturnDays !== undefined) {
|
|
564
|
+
sql += ` AND p.return_days >= ?`;
|
|
565
|
+
params.push(minReturnDays);
|
|
566
|
+
}
|
|
567
|
+
if (maxHandlingHours !== undefined) {
|
|
568
|
+
sql += ` AND p.handling_hours <= ?`;
|
|
569
|
+
params.push(maxHandlingHours);
|
|
570
|
+
}
|
|
424
571
|
sql += ` ORDER BY p.created_at DESC LIMIT ?`;
|
|
425
572
|
params.push(limit);
|
|
426
573
|
const products = db.prepare(sql).all(...params);
|
|
@@ -439,20 +586,79 @@ function handleSearch(args) {
|
|
|
439
586
|
products: sorted.map((p) => {
|
|
440
587
|
const levelMeta = { new: '', trusted: '⭐', quality: '🌟', star: '💫', legend: '🔥' };
|
|
441
588
|
const badge = levelMeta[p._rep_level] ?? '';
|
|
589
|
+
const parsed = parseProductForAgent(p);
|
|
442
590
|
return {
|
|
443
591
|
id: p.id,
|
|
444
592
|
title: p.title,
|
|
445
|
-
|
|
446
|
-
|
|
593
|
+
price: p.price,
|
|
594
|
+
price_display: `${p.price} WAZ`,
|
|
447
595
|
stock: p.stock,
|
|
448
596
|
category: p.category,
|
|
597
|
+
specs: parsed.specs,
|
|
598
|
+
agent_summary: parsed.agent_summary,
|
|
599
|
+
logistics: {
|
|
600
|
+
handling_hours: p.handling_hours ?? 24,
|
|
601
|
+
estimated_days: parsed.estimated_days,
|
|
602
|
+
ship_regions: p.ship_regions ?? '全国',
|
|
603
|
+
fragile: !!p.fragile,
|
|
604
|
+
},
|
|
605
|
+
after_sales: {
|
|
606
|
+
return_days: p.return_days ?? 7,
|
|
607
|
+
return_condition: p.return_condition ?? '',
|
|
608
|
+
warranty_days: p.warranty_days ?? 0,
|
|
609
|
+
},
|
|
449
610
|
seller: badge ? `${badge} ${p.seller_name}` : p.seller_name,
|
|
450
611
|
seller_id: p.seller_id,
|
|
451
|
-
seller_reputation: p._rep_level !== 'new'
|
|
612
|
+
seller_reputation: p._rep_level !== 'new'
|
|
613
|
+
? `${badge} ${['', '可信', '优质', '明星', '传奇'][['new', 'trusted', 'quality', 'star', 'legend'].indexOf(p._rep_level)]}(${p._rep_points}分)`
|
|
614
|
+
: undefined,
|
|
452
615
|
};
|
|
453
616
|
}),
|
|
454
617
|
};
|
|
455
618
|
}
|
|
619
|
+
function handleVerifyPrice(args) {
|
|
620
|
+
const auth = requireAuth(db, args.api_key);
|
|
621
|
+
if ('error' in auth)
|
|
622
|
+
return auth;
|
|
623
|
+
const { user } = auth;
|
|
624
|
+
const productId = args.product_id;
|
|
625
|
+
const qty = Number(args.quantity ?? 1);
|
|
626
|
+
if (!productId)
|
|
627
|
+
return { error: '请提供 product_id' };
|
|
628
|
+
const product = db.prepare(`
|
|
629
|
+
SELECT p.*, u.name as seller_name FROM products p
|
|
630
|
+
JOIN users u ON p.seller_id = u.id
|
|
631
|
+
WHERE p.id = ? AND p.status = 'active'
|
|
632
|
+
`).get(productId);
|
|
633
|
+
if (!product)
|
|
634
|
+
return { error: `商品不存在或已下架:${productId}` };
|
|
635
|
+
if (product.stock < qty) {
|
|
636
|
+
return { error: `库存不足:当前库存 ${product.stock},请求数量 ${qty}` };
|
|
637
|
+
}
|
|
638
|
+
const now = new Date();
|
|
639
|
+
const expiresAt = new Date(now.getTime() + 10 * 60_000);
|
|
640
|
+
const token = `pst_${now.getTime().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
641
|
+
db.prepare(`
|
|
642
|
+
INSERT INTO price_sessions (token, product_id, user_id, price, quantity, created_at, expires_at)
|
|
643
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
644
|
+
`).run(token, productId, user.id, product.price, qty, now.toISOString(), expiresAt.toISOString());
|
|
645
|
+
const parsed = parseProductForAgent(product);
|
|
646
|
+
return {
|
|
647
|
+
session_token: token,
|
|
648
|
+
verified_price: product.price,
|
|
649
|
+
quantity: qty,
|
|
650
|
+
total: product.price * qty,
|
|
651
|
+
product: {
|
|
652
|
+
id: product.id,
|
|
653
|
+
title: product.title,
|
|
654
|
+
agent_summary: parsed.agent_summary,
|
|
655
|
+
specs: parsed.specs,
|
|
656
|
+
},
|
|
657
|
+
expires_at: expiresAt.toISOString(),
|
|
658
|
+
expires_in_seconds: 600,
|
|
659
|
+
next: `调用 webaz_place_order 时传入 session_token="${token}" 确保以此价格成交`,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
456
662
|
function handleListProduct(args) {
|
|
457
663
|
const auth = requireAuth(db, args.api_key);
|
|
458
664
|
if ('error' in auth)
|
|
@@ -475,10 +681,20 @@ function handleListProduct(args) {
|
|
|
475
681
|
};
|
|
476
682
|
}
|
|
477
683
|
const id = generateId('prd');
|
|
684
|
+
const specsJson = args.specs != null
|
|
685
|
+
? (typeof args.specs === 'string' ? args.specs : JSON.stringify(args.specs))
|
|
686
|
+
: null;
|
|
687
|
+
const estJson = args.estimated_days != null
|
|
688
|
+
? (typeof args.estimated_days === 'string' ? args.estimated_days : JSON.stringify(args.estimated_days))
|
|
689
|
+
: null;
|
|
478
690
|
db.prepare(`
|
|
479
|
-
INSERT INTO products (
|
|
480
|
-
|
|
481
|
-
|
|
691
|
+
INSERT INTO products (
|
|
692
|
+
id, seller_id, title, description, price, stock, category, stake_amount,
|
|
693
|
+
specs, brand, model, source_price,
|
|
694
|
+
ship_regions, handling_hours, estimated_days, fragile,
|
|
695
|
+
return_days, return_condition, warranty_days
|
|
696
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
697
|
+
`).run(id, user.id, args.title, args.description, price, args.stock ?? 1, args.category ?? null, stakeAmount, specsJson, args.brand ?? null, args.model ?? null, args.source_price != null ? Number(args.source_price) : null, args.ship_regions ?? '全国', args.handling_hours != null ? Number(args.handling_hours) : 24, estJson, args.fragile ? 1 : 0, args.return_days != null ? Number(args.return_days) : 7, args.return_condition ?? '', args.warranty_days != null ? Number(args.warranty_days) : 0);
|
|
482
698
|
// 扣除质押金额
|
|
483
699
|
db.prepare(`
|
|
484
700
|
UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?
|
|
@@ -513,6 +729,28 @@ function handlePlaceOrder(args) {
|
|
|
513
729
|
if (product.stock < quantity) {
|
|
514
730
|
return { error: `库存不足:当前库存 ${product.stock},你要购买 ${quantity}` };
|
|
515
731
|
}
|
|
732
|
+
// 验证 session_token(如果提供)
|
|
733
|
+
if (args.session_token) {
|
|
734
|
+
const session = db.prepare(`
|
|
735
|
+
SELECT * FROM price_sessions WHERE token = ? AND product_id = ? AND user_id = ?
|
|
736
|
+
`).get(args.session_token, args.product_id, user.id);
|
|
737
|
+
if (!session)
|
|
738
|
+
return { error: 'session_token 无效,请重新调用 webaz_verify_price' };
|
|
739
|
+
if (session.used_at)
|
|
740
|
+
return { error: 'session_token 已使用,请重新调用 webaz_verify_price' };
|
|
741
|
+
if (new Date(session.expires_at) < new Date()) {
|
|
742
|
+
return { error: 'session_token 已过期(10分钟有效),请重新调用 webaz_verify_price' };
|
|
743
|
+
}
|
|
744
|
+
if (session.price !== product.price) {
|
|
745
|
+
return {
|
|
746
|
+
error: 'price_changed',
|
|
747
|
+
message: `商品价格已变动:验证时 ${session.price} WAZ,当前 ${product.price} WAZ`,
|
|
748
|
+
new_price: product.price,
|
|
749
|
+
hint: '请重新调用 webaz_verify_price 获取新价格后再下单',
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
db.prepare(`UPDATE price_sessions SET used_at = datetime('now') WHERE token = ?`).run(args.session_token);
|
|
753
|
+
}
|
|
516
754
|
const totalAmount = product.price * quantity;
|
|
517
755
|
const wallet = db
|
|
518
756
|
.prepare('SELECT * FROM wallets WHERE user_id = ?')
|
|
@@ -1098,6 +1336,7 @@ export async function startMCPServer() {
|
|
|
1098
1336
|
});
|
|
1099
1337
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1100
1338
|
const { name, arguments: args = {} } = request.params;
|
|
1339
|
+
const t0 = Date.now();
|
|
1101
1340
|
let result;
|
|
1102
1341
|
try {
|
|
1103
1342
|
switch (name) {
|
|
@@ -1110,6 +1349,9 @@ export async function startMCPServer() {
|
|
|
1110
1349
|
case 'webaz_search':
|
|
1111
1350
|
result = handleSearch(args);
|
|
1112
1351
|
break;
|
|
1352
|
+
case 'webaz_verify_price':
|
|
1353
|
+
result = handleVerifyPrice(args);
|
|
1354
|
+
break;
|
|
1113
1355
|
case 'webaz_list_product':
|
|
1114
1356
|
result = handleListProduct(args);
|
|
1115
1357
|
break;
|
|
@@ -1146,6 +1388,7 @@ export async function startMCPServer() {
|
|
|
1146
1388
|
catch (err) {
|
|
1147
1389
|
result = { error: `执行出错:${err.message}` };
|
|
1148
1390
|
}
|
|
1391
|
+
recordToolCall(name, args, result, Date.now() - t0);
|
|
1149
1392
|
return {
|
|
1150
1393
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1151
1394
|
};
|
|
@@ -1158,3 +1401,46 @@ export async function startMCPServer() {
|
|
|
1158
1401
|
function addHours(date, hours) {
|
|
1159
1402
|
return new Date(date.getTime() + hours * 3_600_000).toISOString();
|
|
1160
1403
|
}
|
|
1404
|
+
function recordToolCall(tool, args, result, latencyMs) {
|
|
1405
|
+
let userId = null;
|
|
1406
|
+
try {
|
|
1407
|
+
const apiKey = args.api_key;
|
|
1408
|
+
if (apiKey) {
|
|
1409
|
+
const row = db.prepare('SELECT id FROM users WHERE api_key = ?').get(apiKey);
|
|
1410
|
+
if (row)
|
|
1411
|
+
userId = row.id;
|
|
1412
|
+
}
|
|
1413
|
+
const isError = !!result && typeof result === 'object' && 'error' in result;
|
|
1414
|
+
const errorMsg = isError
|
|
1415
|
+
? String(result.error).slice(0, 200)
|
|
1416
|
+
: null;
|
|
1417
|
+
db.prepare(`INSERT INTO mcp_tool_calls (tool_name, user_id, outcome, latency_ms, error_msg)
|
|
1418
|
+
VALUES (?, ?, ?, ?, ?)`).run(tool, userId, isError ? 'error' : 'success', latencyMs, errorMsg);
|
|
1419
|
+
sendTelemetry({
|
|
1420
|
+
tool_name: tool,
|
|
1421
|
+
outcome: isError ? 'error' : 'success',
|
|
1422
|
+
latency_ms: latencyMs,
|
|
1423
|
+
user_id_hash: userId ? createHash('sha256').update(userId).digest('hex').slice(0, 16) : null,
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
catch (e) {
|
|
1427
|
+
console.error('[telemetry-write-failed]', e.message);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function sendTelemetry(payload) {
|
|
1431
|
+
if (!TELEMETRY_ENABLED)
|
|
1432
|
+
return;
|
|
1433
|
+
try {
|
|
1434
|
+
void fetch(TELEMETRY_URL, {
|
|
1435
|
+
method: 'POST',
|
|
1436
|
+
headers: { 'content-type': 'application/json' },
|
|
1437
|
+
body: JSON.stringify({
|
|
1438
|
+
...payload,
|
|
1439
|
+
server_version: SERVER_VERSION,
|
|
1440
|
+
ts: new Date().toISOString(),
|
|
1441
|
+
}),
|
|
1442
|
+
signal: AbortSignal.timeout(2000),
|
|
1443
|
+
}).catch(() => { });
|
|
1444
|
+
}
|
|
1445
|
+
catch { /* never block the tool call */ }
|
|
1446
|
+
}
|