@seasonkoh/webaz 0.1.6 → 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.
@@ -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(可选,如果是通过推荐链接来的)',
@@ -227,7 +323,7 @@ action 说明:
227
323
  ruling 裁定选项(arbitrate 时使用):
228
324
  - refund_buyer:全额退款给买家,扣押卖家部分保证金
229
325
  - release_seller:资金释放给卖家(卖家胜诉)
230
- - partial_refund:部分退款(需指定 refund_amount)`,
326
+ - partial_refund:部分退款(需指定 refund_amount);若责任在第三方(如物流),同时指定 liable_party(责任方 user_id),此时卖家全额结算,赔偿金从责任方扣除`,
231
327
  inputSchema: {
232
328
  type: 'object',
233
329
  properties: {
@@ -247,6 +343,7 @@ ruling 裁定选项(arbitrate 时使用):
247
343
  description: '裁定结果(arbitrate 时必填)',
248
344
  },
249
345
  refund_amount: { type: 'number', description: '部分退款金额,仅 ruling=partial_refund 时使用' },
346
+ liable_party: { type: 'string', description: '第三方责任方 user_id(partial_refund 时可选):指定后赔偿金从该方扣除,卖家全额结算' },
250
347
  ruling_reason: { type: 'string', description: '裁定理由(arbitrate 时必填,将永久记录在链上)' },
251
348
  },
252
349
  required: ['api_key', 'action'],
@@ -362,7 +459,8 @@ function handleInfo() {
362
459
  },
363
460
  quick_start: {
364
461
  seller: '1. webaz_register(role=seller) → 2. webaz_list_product() → 3. 等通知 webaz_update_order(accept/ship)',
365
- buyer: '1. webaz_register(role=buyer) → 2. webaz_search() → 3. webaz_place_order() → 4. webaz_update_order(confirm)',
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) 下单 → 返回成交理由',
366
464
  logistics: '1. webaz_register(role=logistics) → 2. webaz_update_order(pickup) → webaz_update_order(deliver)',
367
465
  },
368
466
  available_tools: TOOLS.map((t) => ({ name: t.name, description: t.description.split('\n')[0] })),
@@ -396,10 +494,52 @@ function handleRegister(args) {
396
494
  : '等待订单分配给你',
397
495
  };
398
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
+ }
399
537
  function handleSearch(args) {
400
538
  const query = args.query ?? '';
401
539
  const category = args.category;
402
540
  const maxPrice = args.max_price;
541
+ const minReturnDays = args.min_return_days;
542
+ const maxHandlingHours = args.max_handling_hours;
403
543
  const limit = args.limit ?? 10;
404
544
  let sql = `
405
545
  SELECT p.*, u.name as seller_name
@@ -420,6 +560,14 @@ function handleSearch(args) {
420
560
  sql += ` AND p.price <= ?`;
421
561
  params.push(maxPrice);
422
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
+ }
423
571
  sql += ` ORDER BY p.created_at DESC LIMIT ?`;
424
572
  params.push(limit);
425
573
  const products = db.prepare(sql).all(...params);
@@ -438,20 +586,79 @@ function handleSearch(args) {
438
586
  products: sorted.map((p) => {
439
587
  const levelMeta = { new: '', trusted: '⭐', quality: '🌟', star: '💫', legend: '🔥' };
440
588
  const badge = levelMeta[p._rep_level] ?? '';
589
+ const parsed = parseProductForAgent(p);
441
590
  return {
442
591
  id: p.id,
443
592
  title: p.title,
444
- description: p.description,
445
- price: `${p.price} WAZ`,
593
+ price: p.price,
594
+ price_display: `${p.price} WAZ`,
446
595
  stock: p.stock,
447
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
+ },
448
610
  seller: badge ? `${badge} ${p.seller_name}` : p.seller_name,
449
611
  seller_id: p.seller_id,
450
- seller_reputation: p._rep_level !== 'new' ? `${badge} ${['', '可信', '优质', '明星', '传奇'][['new', 'trusted', 'quality', 'star', 'legend'].indexOf(p._rep_level)]}(${p._rep_points}分)` : undefined,
612
+ seller_reputation: p._rep_level !== 'new'
613
+ ? `${badge} ${['', '可信', '优质', '明星', '传奇'][['new', 'trusted', 'quality', 'star', 'legend'].indexOf(p._rep_level)]}(${p._rep_points}分)`
614
+ : undefined,
451
615
  };
452
616
  }),
453
617
  };
454
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
+ }
455
662
  function handleListProduct(args) {
456
663
  const auth = requireAuth(db, args.api_key);
457
664
  if ('error' in auth)
@@ -474,10 +681,20 @@ function handleListProduct(args) {
474
681
  };
475
682
  }
476
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;
477
690
  db.prepare(`
478
- INSERT INTO products (id, seller_id, title, description, price, stock, category, stake_amount)
479
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
480
- `).run(id, user.id, args.title, args.description, price, args.stock ?? 1, args.category ?? null, stakeAmount);
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);
481
698
  // 扣除质押金额
482
699
  db.prepare(`
483
700
  UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?
@@ -512,6 +729,28 @@ function handlePlaceOrder(args) {
512
729
  if (product.stock < quantity) {
513
730
  return { error: `库存不足:当前库存 ${product.stock},你要购买 ${quantity}` };
514
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
+ }
515
754
  const totalAmount = product.price * quantity;
516
755
  const wallet = db
517
756
  .prepare('SELECT * FROM wallets WHERE user_id = ?')
@@ -866,7 +1105,7 @@ function handleDispute(args) {
866
1105
  if (args.ruling === 'partial_refund' && !args.refund_amount) {
867
1106
  return { error: 'partial_refund 需要提供 refund_amount' };
868
1107
  }
869
- const result = arbitrateDispute(db, args.dispute_id, user.id, args.ruling, args.ruling_reason, args.refund_amount);
1108
+ const result = arbitrateDispute(db, args.dispute_id, user.id, args.ruling, args.ruling_reason, args.refund_amount, undefined, args.liable_party);
870
1109
  // L4-3 争议声誉:裁定完成后更新声誉
871
1110
  if (result.success) {
872
1111
  const dispute = getDisputeDetails(db, args.dispute_id);
@@ -1097,6 +1336,7 @@ export async function startMCPServer() {
1097
1336
  });
1098
1337
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1099
1338
  const { name, arguments: args = {} } = request.params;
1339
+ const t0 = Date.now();
1100
1340
  let result;
1101
1341
  try {
1102
1342
  switch (name) {
@@ -1109,6 +1349,9 @@ export async function startMCPServer() {
1109
1349
  case 'webaz_search':
1110
1350
  result = handleSearch(args);
1111
1351
  break;
1352
+ case 'webaz_verify_price':
1353
+ result = handleVerifyPrice(args);
1354
+ break;
1112
1355
  case 'webaz_list_product':
1113
1356
  result = handleListProduct(args);
1114
1357
  break;
@@ -1145,6 +1388,7 @@ export async function startMCPServer() {
1145
1388
  catch (err) {
1146
1389
  result = { error: `执行出错:${err.message}` };
1147
1390
  }
1391
+ recordToolCall(name, args, result, Date.now() - t0);
1148
1392
  return {
1149
1393
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1150
1394
  };
@@ -1157,3 +1401,46 @@ export async function startMCPServer() {
1157
1401
  function addHours(date, hours) {
1158
1402
  return new Date(date.getTime() + hours * 3_600_000).toISOString();
1159
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
+ }
@@ -143,7 +143,8 @@ export function respondToDispute(db, disputeId, responderId, notes, evidenceIds)
143
143
  message: '反驳证据已提交,争议进入仲裁阶段。仲裁员将在 72 小时内做出裁定。',
144
144
  };
145
145
  }
146
- export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, refundAmount, liabilityParties) {
146
+ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, refundAmount, liabilityParties, liablePartyId // 指定责任方 user_id(用于 partial_refund 第三方责任场景)
147
+ ) {
147
148
  const dispute = db.prepare('SELECT * FROM disputes WHERE id = ?').get(disputeId);
148
149
  if (!dispute)
149
150
  return { success: false, error: `争议不存在:${disputeId}` };
@@ -159,10 +160,10 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
159
160
  // 执行资金处置
160
161
  const settlement = ruling === 'liability_split' && liabilityParties
161
162
  ? executeLiabilitySplit(db, dispute.order_id, liabilityParties, refundAmount)
162
- : executeSettlement(db, dispute.order_id, ruling, refundAmount);
163
+ : executeSettlement(db, dispute.order_id, ruling, refundAmount, liablePartyId);
163
164
  if (!settlement.success)
164
165
  return { success: false, error: settlement.error };
165
- // 收取仲裁费(败诉方付 1%,最低 1 WAZ)
166
+ // 收取仲裁费(败诉/责任方付 1%,最低 1 WAZ)
166
167
  const order = db.prepare('SELECT total_amount, buyer_id, seller_id FROM orders WHERE id = ?')
167
168
  .get(dispute.order_id);
168
169
  const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
@@ -170,29 +171,35 @@ export function arbitrateDispute(db, disputeId, arbitratorId, ruling, reason, re
170
171
  if (order) {
171
172
  const amt = order.total_amount;
172
173
  if (ruling === 'refund_buyer') {
173
- // 卖家败诉
174
174
  const f = chargeArbitrationFee(db, order.seller_id, amt, arbitratorId, sysUser.id);
175
175
  if (f.fee > 0)
176
176
  arbFees[order.seller_id] = f.fee;
177
177
  }
178
178
  else if (ruling === 'release_seller') {
179
- // 买家败诉
180
179
  const f = chargeArbitrationFee(db, order.buyer_id, amt, arbitratorId, sysUser.id);
181
180
  if (f.fee > 0)
182
181
  arbFees[order.buyer_id] = f.fee;
183
182
  }
184
183
  else if (ruling === 'partial_refund') {
185
- // 折中:双方各付 0.5%(最低各 0.5 WAZ,实际按 chargeArbitrationFee 的 min 逻辑)
186
- const halfAmt = amt * 0.5;
187
- const fb = chargeArbitrationFee(db, order.buyer_id, halfAmt, arbitratorId, sysUser.id);
188
- const fs = chargeArbitrationFee(db, order.seller_id, halfAmt, arbitratorId, sysUser.id);
189
- if (fb.fee > 0)
190
- arbFees[order.buyer_id] = fb.fee;
191
- if (fs.fee > 0)
192
- arbFees[order.seller_id] = fs.fee;
184
+ // 有指定责任方:仲裁费全由责任方承担
185
+ // 无责任方:买卖双方各付 0.5%
186
+ const payerId = liablePartyId ?? null;
187
+ if (payerId) {
188
+ const f = chargeArbitrationFee(db, payerId, amt, arbitratorId, sysUser.id);
189
+ if (f.fee > 0)
190
+ arbFees[payerId] = f.fee;
191
+ }
192
+ else {
193
+ const halfAmt = amt * 0.5;
194
+ const fb = chargeArbitrationFee(db, order.buyer_id, halfAmt, arbitratorId, sysUser.id);
195
+ const fs = chargeArbitrationFee(db, order.seller_id, halfAmt, arbitratorId, sysUser.id);
196
+ if (fb.fee > 0)
197
+ arbFees[order.buyer_id] = fb.fee;
198
+ if (fs.fee > 0)
199
+ arbFees[order.seller_id] = fs.fee;
200
+ }
193
201
  }
194
202
  else if (ruling === 'liability_split' && liabilityParties) {
195
- // 各责任方按比例分担
196
203
  const totalLiability = liabilityParties.reduce((s, p) => s + p.amount, 0) || amt;
197
204
  for (const p of liabilityParties) {
198
205
  const share = (p.amount / totalLiability) * amt;
@@ -378,7 +385,7 @@ function chargeArbitrationFee(db, loserId, orderAmount, arbitratorId, sysUserId)
378
385
  }
379
386
  return { fee: actualFee, arbitratorShare, protocolShare };
380
387
  }
381
- function executeSettlement(db, orderId, ruling, refundAmount) {
388
+ function executeSettlement(db, orderId, ruling, refundAmount, liablePartyId) {
382
389
  const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
383
390
  if (!order)
384
391
  return { success: false, error: '订单不存在' };
@@ -447,35 +454,88 @@ function executeSettlement(db, orderId, ruling, refundAmount) {
447
454
  };
448
455
  }
449
456
  else if (ruling === 'partial_refund') {
450
- // ── 折中处理:部分退款 ────────────────────────────────────────
451
457
  const refund = refundAmount ?? Math.round(totalAmount * 0.5 * 100) / 100;
452
458
  if (refund > totalAmount)
453
459
  return { success: false, error: `退款金额 ${refund} 超出订单总额 ${totalAmount}` };
454
- const sellerGet = Math.round((totalAmount - refund) * 100) / 100;
455
- const stakeReturn = Math.round(stakeAmount * 0.5 * 100) / 100; // 质押返一半
456
- db.transaction(() => {
457
- db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
458
- if (refund > 0) {
459
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refund, buyerId);
460
- }
461
- if (sellerGet > 0) {
462
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerGet, sellerId);
463
- }
464
- if (stakeAmount > 0) {
465
- db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
466
- .run(stakeAmount, stakeReturn, sellerId);
467
- }
468
- transition(db, orderId, 'cancelled', sysUser.id, [], `争议裁定:部分退款 ${refund} WAZ`);
469
- })();
470
- return {
471
- success: true,
472
- detail: {
473
- ruling: 'partial_refund',
474
- buyer_refund: refund,
475
- seller_received: sellerGet,
476
- seller_stake_returned: stakeReturn,
477
- }
478
- };
460
+ if (liablePartyId) {
461
+ // ── 第三方责任 partial_refund ────────────────────────────────
462
+ // 卖家全额结算(正常收款),买家赔偿由责任方钱包直接支付
463
+ const protocolFee = Math.round(totalAmount * 0.02 * 100) / 100;
464
+ const logisticsFee = order.logistics_id ? Math.round(totalAmount * 0.05 * 100) / 100 : 0;
465
+ const sellerAmount = totalAmount - protocolFee - logisticsFee;
466
+ // 检查责任方余额是否足够
467
+ const liableWallet = db.prepare('SELECT balance, staked FROM wallets WHERE user_id = ?')
468
+ .get(liablePartyId);
469
+ const liableAvailable = (liableWallet?.balance ?? 0) + (liableWallet?.staked ?? 0);
470
+ const actualRefund = Math.min(refund, liableAvailable);
471
+ db.transaction(() => {
472
+ // 1. 释放托管 → 正常结算给卖家
473
+ db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
474
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerAmount, sellerId);
475
+ if (order.logistics_id && logisticsFee > 0) {
476
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(logisticsFee, order.logistics_id);
477
+ }
478
+ // 2. 返还卖家质押
479
+ if (stakeAmount > 0) {
480
+ db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
481
+ .run(stakeAmount, stakeAmount, sellerId);
482
+ }
483
+ // 3. 从责任方钱包扣除赔偿(先质押后余额)
484
+ if (actualRefund > 0) {
485
+ const liableStaked = liableWallet?.staked ?? 0;
486
+ if (liableStaked >= actualRefund) {
487
+ db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(actualRefund, liablePartyId);
488
+ }
489
+ else {
490
+ const fromBalance = actualRefund - liableStaked;
491
+ db.prepare('UPDATE wallets SET staked = 0, balance = balance - ? WHERE user_id = ?').run(fromBalance, liablePartyId);
492
+ }
493
+ // 4. 赔偿金给买家
494
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(actualRefund, buyerId);
495
+ }
496
+ transition(db, orderId, 'completed', sysUser.id, [], `争议裁定:第三方责任赔偿 ${actualRefund} WAZ,卖家全额结算`);
497
+ })();
498
+ return {
499
+ success: true,
500
+ detail: {
501
+ ruling: 'partial_refund',
502
+ liable_party: liablePartyId,
503
+ buyer_compensation: actualRefund,
504
+ seller_received: sellerAmount,
505
+ logistics_fee: logisticsFee,
506
+ protocol_fee: protocolFee,
507
+ seller_stake_returned: stakeAmount,
508
+ }
509
+ };
510
+ }
511
+ else {
512
+ // ── 买卖双方协商 partial_refund(原逻辑)───────────────────────
513
+ const sellerGet = Math.round((totalAmount - refund) * 100) / 100;
514
+ const stakeReturn = Math.round(stakeAmount * 0.5 * 100) / 100; // 质押返一半
515
+ db.transaction(() => {
516
+ db.prepare('UPDATE wallets SET escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, buyerId);
517
+ if (refund > 0) {
518
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(refund, buyerId);
519
+ }
520
+ if (sellerGet > 0) {
521
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(sellerGet, sellerId);
522
+ }
523
+ if (stakeAmount > 0) {
524
+ db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
525
+ .run(stakeAmount, stakeReturn, sellerId);
526
+ }
527
+ transition(db, orderId, 'cancelled', sysUser.id, [], `争议裁定:部分退款 ${refund} WAZ`);
528
+ })();
529
+ return {
530
+ success: true,
531
+ detail: {
532
+ ruling: 'partial_refund',
533
+ buyer_refund: refund,
534
+ seller_received: sellerGet,
535
+ seller_stake_returned: stakeReturn,
536
+ }
537
+ };
538
+ }
479
539
  }
480
540
  return { success: false, error: `未知裁定类型:${ruling}` };
481
541
  }