@seasonkoh/webaz 0.1.7 → 0.1.9

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.
Files changed (153) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +156 -20
  3. package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
  4. package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
  5. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
  7. package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
  11. package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
  12. package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
  13. package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
  14. package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
  15. package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
  16. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
  17. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
  18. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
  19. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
  20. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
  21. package/dist/pwa/public/app.js +31947 -0
  22. package/dist/pwa/public/i18n.js +5751 -0
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +21 -0
  25. package/dist/pwa/public/manifest.json +48 -0
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +535 -0
  28. package/dist/pwa/public/sw.js +63 -0
  29. package/dist/pwa/public/vendor/jsQR.js +10102 -0
  30. package/dist/pwa/public/webaz-logo.png +0 -0
  31. package/dist/pwa/routes/account-deletion.js +53 -0
  32. package/dist/pwa/routes/addresses.js +105 -0
  33. package/dist/pwa/routes/admin-admins.js +151 -0
  34. package/dist/pwa/routes/admin-analytics.js +253 -0
  35. package/dist/pwa/routes/admin-atomic.js +21 -0
  36. package/dist/pwa/routes/admin-catalog.js +64 -0
  37. package/dist/pwa/routes/admin-editor-picks.js +45 -0
  38. package/dist/pwa/routes/admin-events.js +60 -0
  39. package/dist/pwa/routes/admin-health.js +66 -0
  40. package/dist/pwa/routes/admin-moderation.js +120 -0
  41. package/dist/pwa/routes/admin-ops.js +179 -0
  42. package/dist/pwa/routes/admin-protocol-params.js +79 -0
  43. package/dist/pwa/routes/admin-reports.js +154 -0
  44. package/dist/pwa/routes/admin-tokenomics.js +113 -0
  45. package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
  46. package/dist/pwa/routes/admin-users-query.js +390 -0
  47. package/dist/pwa/routes/admin-verifier-flow.js +126 -0
  48. package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
  49. package/dist/pwa/routes/admin-wallet-ops.js +66 -0
  50. package/dist/pwa/routes/agent-buy.js +215 -0
  51. package/dist/pwa/routes/agent-governance.js +341 -0
  52. package/dist/pwa/routes/agent-reputation.js +34 -0
  53. package/dist/pwa/routes/ai.js +101 -0
  54. package/dist/pwa/routes/analytics.js +272 -0
  55. package/dist/pwa/routes/anchors.js +169 -0
  56. package/dist/pwa/routes/announcements.js +110 -0
  57. package/dist/pwa/routes/arbitrator.js +117 -0
  58. package/dist/pwa/routes/auction.js +436 -0
  59. package/dist/pwa/routes/auth-login.js +40 -0
  60. package/dist/pwa/routes/auth-read.js +66 -0
  61. package/dist/pwa/routes/auth-register.js +138 -0
  62. package/dist/pwa/routes/auth-sessions.js +62 -0
  63. package/dist/pwa/routes/blocklist.js +60 -0
  64. package/dist/pwa/routes/buyer-feeds.js +224 -0
  65. package/dist/pwa/routes/cart.js +155 -0
  66. package/dist/pwa/routes/charity.js +816 -0
  67. package/dist/pwa/routes/chat.js +318 -0
  68. package/dist/pwa/routes/checkin-tasks.js +122 -0
  69. package/dist/pwa/routes/checkout-helpers.js +85 -0
  70. package/dist/pwa/routes/claim-initiators.js +88 -0
  71. package/dist/pwa/routes/claim-verify.js +615 -0
  72. package/dist/pwa/routes/claim-voting.js +114 -0
  73. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  74. package/dist/pwa/routes/coupons.js +165 -0
  75. package/dist/pwa/routes/dashboards.js +99 -0
  76. package/dist/pwa/routes/dispute-cases.js +267 -0
  77. package/dist/pwa/routes/disputes-read.js +358 -0
  78. package/dist/pwa/routes/disputes-write.js +475 -0
  79. package/dist/pwa/routes/evidence.js +86 -0
  80. package/dist/pwa/routes/external-anchors.js +107 -0
  81. package/dist/pwa/routes/feedback.js +270 -0
  82. package/dist/pwa/routes/flash-sales.js +130 -0
  83. package/dist/pwa/routes/follows.js +103 -0
  84. package/dist/pwa/routes/group-buys.js +208 -0
  85. package/dist/pwa/routes/growth.js +199 -0
  86. package/dist/pwa/routes/import-product.js +153 -0
  87. package/dist/pwa/routes/kyc.js +40 -0
  88. package/dist/pwa/routes/leaderboard.js +149 -0
  89. package/dist/pwa/routes/listings.js +281 -0
  90. package/dist/pwa/routes/logistics.js +35 -0
  91. package/dist/pwa/routes/manifests.js +126 -0
  92. package/dist/pwa/routes/me-data.js +101 -0
  93. package/dist/pwa/routes/notifications.js +48 -0
  94. package/dist/pwa/routes/offers.js +96 -0
  95. package/dist/pwa/routes/orders-action.js +285 -0
  96. package/dist/pwa/routes/orders-create.js +339 -0
  97. package/dist/pwa/routes/orders-read.js +180 -0
  98. package/dist/pwa/routes/p2p-products.js +178 -0
  99. package/dist/pwa/routes/payments-governance.js +311 -0
  100. package/dist/pwa/routes/peers.js +34 -0
  101. package/dist/pwa/routes/pin-receipts.js +39 -0
  102. package/dist/pwa/routes/products-aliases.js +119 -0
  103. package/dist/pwa/routes/products-claims.js +60 -0
  104. package/dist/pwa/routes/products-create.js +206 -0
  105. package/dist/pwa/routes/products-crud.js +73 -0
  106. package/dist/pwa/routes/products-links.js +129 -0
  107. package/dist/pwa/routes/products-list.js +424 -0
  108. package/dist/pwa/routes/products-meta.js +155 -0
  109. package/dist/pwa/routes/products-update.js +125 -0
  110. package/dist/pwa/routes/profile-credentials.js +105 -0
  111. package/dist/pwa/routes/profile-identity.js +174 -0
  112. package/dist/pwa/routes/profile-location.js +35 -0
  113. package/dist/pwa/routes/profile-placement.js +70 -0
  114. package/dist/pwa/routes/profile-prefs.js +93 -0
  115. package/dist/pwa/routes/promoter.js +208 -0
  116. package/dist/pwa/routes/public-utils.js +170 -0
  117. package/dist/pwa/routes/push.js +54 -0
  118. package/dist/pwa/routes/ratings.js +220 -0
  119. package/dist/pwa/routes/recover-key.js +100 -0
  120. package/dist/pwa/routes/referral.js +58 -0
  121. package/dist/pwa/routes/reputation.js +34 -0
  122. package/dist/pwa/routes/returns.js +493 -0
  123. package/dist/pwa/routes/reviews.js +81 -0
  124. package/dist/pwa/routes/rfqs.js +443 -0
  125. package/dist/pwa/routes/search.js +172 -0
  126. package/dist/pwa/routes/secondhand.js +278 -0
  127. package/dist/pwa/routes/seller-quota.js +225 -0
  128. package/dist/pwa/routes/share-redirects.js +164 -0
  129. package/dist/pwa/routes/shareables-interactions.js +212 -0
  130. package/dist/pwa/routes/shareables.js +470 -0
  131. package/dist/pwa/routes/shops.js +98 -0
  132. package/dist/pwa/routes/signaling.js +43 -0
  133. package/dist/pwa/routes/skill-market.js +173 -0
  134. package/dist/pwa/routes/skills.js +174 -0
  135. package/dist/pwa/routes/snf.js +126 -0
  136. package/dist/pwa/routes/tags.js +47 -0
  137. package/dist/pwa/routes/trial.js +333 -0
  138. package/dist/pwa/routes/trusted-kpi.js +87 -0
  139. package/dist/pwa/routes/url-claim.js +113 -0
  140. package/dist/pwa/routes/users-public.js +317 -0
  141. package/dist/pwa/routes/variants.js +156 -0
  142. package/dist/pwa/routes/verifier-user.js +107 -0
  143. package/dist/pwa/routes/verify-tasks.js +120 -0
  144. package/dist/pwa/routes/waitlist.js +65 -0
  145. package/dist/pwa/routes/wallet-read.js +218 -0
  146. package/dist/pwa/routes/wallet-write.js +273 -0
  147. package/dist/pwa/routes/webauthn.js +188 -0
  148. package/dist/pwa/routes/webhooks.js +162 -0
  149. package/dist/pwa/routes/welcome.js +226 -0
  150. package/dist/pwa/routes/wishlist-qa.js +135 -0
  151. package/dist/pwa/security/ssrf.js +110 -0
  152. package/dist/pwa/server.js +9679 -698
  153. package/package.json +11 -4
@@ -17,12 +17,16 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
17
17
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
18
18
  import { initDatabase, generateId } from '../../layer0-foundation/L0-1-database/schema.js';
19
19
  import { transition, getOrderStatus, initSystemUser, } from '../../layer0-foundation/L0-2-state-machine/engine.js';
20
- import { initDisputeSchema, createDispute, respondToDispute, arbitrateDispute, getDisputeDetails, getOrderDispute, getOpenDisputes, } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
20
+ import { initDisputeSchema, createDispute, respondToDispute, getDisputeDetails, getOrderDispute, getOpenDisputes, } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
21
21
  import { initNotificationSchema, notifyTransition, getNotifications, getUnreadCount, markRead, } from '../../layer2-business/L2-6-notifications/notification-engine.js';
22
22
  import { initSkillSchema, publishSkill, listSkills, getMySkills, subscribeSkill, unsubscribeSkill, getMySubscriptions, formatSkillForAgent, shouldAutoAccept, SKILL_TYPE_META, } from '../../layer4-economics/L4-4-skill-market/skill-engine.js';
23
- import { initReputationSchema, recordOrderReputation, recordDisputeReputation, getReputation, getSearchBoost, getStakeDiscount, } from '../../layer4-economics/L4-3-reputation/reputation-engine.js';
23
+ import { initReputationSchema, recordOrderReputation, 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, randomBytes } 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,14 +34,129 @@ 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)`);
83
+ // ─── 4 层身份模型 helpers(与 PWA server 同源逻辑)───────────────
84
+ const PERMA_ALPHABET_MCP = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
85
+ function mcpGeneratePermanentCode() {
86
+ for (let attempt = 0; attempt < 20; attempt++) {
87
+ let code = '';
88
+ for (let i = 0; i < 6; i++)
89
+ code += PERMA_ALPHABET_MCP[Math.floor(Math.random() * 32)];
90
+ const exists = db.prepare("SELECT 1 FROM users WHERE permanent_code = ?").get(code);
91
+ if (!exists)
92
+ return code;
93
+ }
94
+ for (let attempt = 0; attempt < 20; attempt++) {
95
+ let code = '';
96
+ for (let i = 0; i < 7; i++)
97
+ code += PERMA_ALPHABET_MCP[Math.floor(Math.random() * 32)];
98
+ const exists = db.prepare("SELECT 1 FROM users WHERE permanent_code = ?").get(code);
99
+ if (!exists)
100
+ return code;
101
+ }
102
+ throw new Error('permanent_code generation exhausted');
103
+ }
104
+ function mcpDeriveHandle(name) {
105
+ let base = String(name || '').normalize('NFKD').replace(/[̀-ͯ]/g, '');
106
+ base = base.replace(/[^a-zA-Z0-9._]/g, '').toLowerCase();
107
+ base = base.replace(/^[._]+|[._]+$/g, '');
108
+ if (base.length < 3)
109
+ base = 'user' + Math.random().toString(36).slice(2, 7);
110
+ if (base.length > 18)
111
+ base = base.slice(0, 18);
112
+ if (/^(usr|sys|admin|webaz|anonymous|null)/.test(base))
113
+ base = 'u_' + base;
114
+ const requested = base;
115
+ let candidate = base, i = 1;
116
+ while (db.prepare("SELECT 1 FROM users WHERE handle = ?").get(candidate)) {
117
+ candidate = base.slice(0, 16) + i.toString();
118
+ i++;
119
+ if (i > 9999)
120
+ throw new Error('handle generation exhausted');
121
+ }
122
+ return { handle: candidate, requested, modified: candidate !== requested };
123
+ }
124
+ // 多形态用户引用解析:usr_xxx / VKSF9P / @handle / handle → 内部 id
125
+ function mcpResolveUserRef(raw) {
126
+ if (!raw || typeof raw !== 'string')
127
+ return null;
128
+ const ref = raw.trim();
129
+ if (!ref)
130
+ return null;
131
+ if (/^usr_[A-Za-z0-9_]+$/.test(ref)) {
132
+ const r = db.prepare("SELECT id FROM users WHERE id = ?").get(ref);
133
+ return r?.id || null;
134
+ }
135
+ if (/^[A-Z0-9]{6,7}$/i.test(ref) && !ref.startsWith('@')) {
136
+ const r = db.prepare("SELECT id FROM users WHERE permanent_code = ?").get(ref.toUpperCase());
137
+ if (r)
138
+ return r.id;
139
+ }
140
+ const h = ref.replace(/^@/, '').toLowerCase();
141
+ if (/^[a-z0-9._]+$/.test(h)) {
142
+ const r = db.prepare("SELECT id FROM users WHERE handle = ?").get(h);
143
+ if (r)
144
+ return r.id;
145
+ }
146
+ return null;
147
+ }
33
148
  // ─── 工具定义(Agent 读这些来理解如何使用协议)────────────────
34
149
  const TOOLS = [
35
150
  {
36
151
  name: 'webaz_info',
37
- description: `获取 WebAZ的说明和使用指南。
38
- 这是新 Agent 接入协议时应该调用的第一个工具。
39
- 返回:协议简介、所有可用工具、每个角色的职责和操作流程。
40
- 无需任何参数,无需身份验证。`,
152
+ description: `Get WebAZ documentation and usage guide. Call this FIRST when onboarding a new agent.
153
+ Returns: protocol overview, available tools, role responsibilities, operation flows, **network_state (pre-launch disclaimer)**, **commission_model.compliance_notice (MLM-form disclosure)**.
154
+ No auth required, no parameters needed.
155
+
156
+ ⚠️ Important: WebAZ is currently **pre-launch** with ~0 real users on the canonical endpoint. All stats / counts returned by this and other tools come from the **local MCP SQLite DB**, not protocol-wide prod state. Read network_state field BEFORE you treat any number as real-economy data.
157
+
158
+ ──
159
+ 中文:获取 WebAZ 说明 — 新 Agent 接入协议时应首先调用,返回协议简介、可用工具、角色职责、**网络状态(pre-launch 披露)** + **佣金结构合规提示(MLM 形态披露)**。⚠️ 协议尚未上线,统计来自本地库,不是真实运营数据。`,
41
160
  inputSchema: {
42
161
  type: 'object',
43
162
  properties: {},
@@ -45,16 +164,29 @@ const TOOLS = [
45
164
  },
46
165
  {
47
166
  name: 'webaz_register',
48
- description: `在 WebAZ中注册新账户。
49
- 注册后获得唯一的 api_key,后续所有操作都需要这个 key。
50
- 请将 api_key 安全保存,它代表你在协议中的身份。
51
-
52
- 角色说明:
53
- - buyer(买家):浏览商品、下单、确认收货
54
- - seller(卖家):上架商品、接单、发货
55
- - logistics(物流):揽收包裹、更新运输状态、确认投递
56
- - reviewer(测评员):对商品进行结构化测评
57
- - arbitrator(仲裁员):处理争议,做出裁定`,
167
+ description: `Register a new WebAZ account. Returns:
168
+ - api_key (credential, 36 chars, 128-bit crypto random — store securely)
169
+ - permanent_code (6-char recovery code — use with handle via webaz_mykey to recover lost api_key)
170
+ - handle (URL-safe unique ID derived from name; if requested handle is taken, system auto-appends numeric suffix — check handle_modified flag)
171
+ - created_at (ISO timestamp)
172
+
173
+ ⚠️ Consent required: WebAZ is pre-launch and contains a 3-tier commission + binary PV pairing structure (see webaz_info.commission_model.compliance_notice — may overlap with MLM legal definitions). An agent acting for a human user MUST get the user's explicit, informed consent BEFORE creating an account on their behalf. Do NOT auto-register users just because they asked a generic shopping question.
174
+
175
+ Roles:
176
+ - buyer: browse, order, confirm receipt
177
+ - seller: list products, accept orders, ship
178
+ - logistics: pickup, transit updates, delivery confirm
179
+ - reviewer: structured product reviews
180
+ - arbitrator: handle disputes, issue rulings
181
+
182
+ ⚠️ MCP register limitations (by-design anti-bot):
183
+ - Does NOT set placement_id / sponsor_id (binary tree + referral chain not built) — MCP registration does NOT enroll the user into the multi-tier structure
184
+ - To build a referral chain: user must arrive via webaz_share_link ?ref=<uid> URL clicked in browser (PWA flow) — explicit user action required
185
+ - region defaults to 'global' if omitted; valid: singapore, china, usa, malaysia, indonesia, thailand, vietnam, taiwan, hk, global
186
+
187
+ ──
188
+ 中文:注册 WebAZ 账户。返回 api_key(128-bit 凭证)+ permanent_code(恢复码)+ handle(唯一标识,冲突自动加后缀,看 handle_modified)+ created_at。可选角色:买家/卖家/物流/测评员/仲裁员。
189
+ 注意:MCP register 不建立 placement/sponsor 链(防 bot),需要建链时用 webaz_share_link + PWA 浏览器注册。`,
58
190
  inputSchema: {
59
191
  type: 'object',
60
192
  properties: {
@@ -68,50 +200,191 @@ const TOOLS = [
68
200
  type: 'number',
69
201
  description: '初始模拟余额(测试用,默认 1000 WAZ)',
70
202
  },
203
+ region: {
204
+ type: 'string',
205
+ enum: ['global', 'singapore', 'china', 'usa', 'malaysia', 'indonesia', 'thailand', 'vietnam', 'taiwan', 'hk'],
206
+ description: '账号所在地区(默认 global;影响 referral region cap 与佣金 redirect_region_cap 规则)',
207
+ },
71
208
  },
72
209
  required: ['name', 'role'],
73
210
  },
74
211
  },
75
212
  {
76
213
  name: 'webaz_search',
77
- description: `搜索 WebAZ中的在售商品。
78
- 无需登录即可搜索,买家或 Agent 可以自由浏览。
79
- 返回匹配的商品列表,包含价格、卖家信息、库存数量。`,
214
+ description: `Search WebAZ marketplace listings. No auth required.
215
+ Returns structured specs + logistics + after-sales + agent_summary (one-line decision hint).
216
+
217
+ 【Keyword search】Pass query / category / max_price / min_return_days / max_handling_hours.
218
+
219
+ 【External-link paste search】When user pastes share text from Taobao/Tmall/JD/Pinduoduo/1688/Douyin/Xiaohongshu
220
+ (e.g. '【淘宝】「Product Name」 https://e.tb.cn/xxx ...'), do:
221
+ 1) Prefer your own LLM parsing → pass external_link object with:
222
+ - platform: 'taobao'|'tmall'|'jd'|'pdd'|'1688'|'douyin'|'xhs'
223
+ - external_id: canonical product ID (if resolvable from short URL; else omit)
224
+ - external_title: the title text inside 「」 brackets verbatim
225
+ 2) If you can't parse, drop the raw text into paste_text — WebAZ server does light regex parsing (no outbound calls)
226
+
227
+ Match rules (STRICT, no guessing):
228
+ - Level 1: external_id exact match
229
+ - Level 2: external_title string exact match
230
+ - Neither hit → matched_by: 'none', **NO fuzzy fallback, NO keyword degradation**
231
+
232
+ IMPORTANT: matched_by='none' is an honest signal — tell the user "no exact match found".
233
+ Do NOT silently fall back to keyword search (that's a separate tool's job).
234
+ Do NOT guess similar products from title impressions.
235
+ WebAZ's trust premise is that match results are TRULY precise, not "looks similar".
236
+
237
+ Note: paste-link matching hits webaz.xyz production data, not your local webaz.db.
238
+
239
+ ──
240
+ 中文:商品搜索(无需登录)。两种模式:关键词搜索 + 粘贴外链精准匹配。外链匹配强制精准(仅 external_id 或 external_title 完全相等命中),不命中返回 matched_by:'none',不做模糊降级。`,
80
241
  inputSchema: {
81
242
  type: 'object',
82
243
  properties: {
83
244
  query: { type: 'string', description: '搜索关键词(商品名称或描述)' },
84
245
  category: { type: 'string', description: '商品分类过滤(可选)' },
85
246
  max_price: { type: 'number', description: '最高价格过滤(可选)' },
86
- limit: { type: 'number', description: '返回数量上限,默认 10' },
247
+ min_return_days: { type: 'number', description: '最少退货天数(可选,如 7 表示只看支持7天退货的商品)' },
248
+ max_handling_hours: { type: 'number', description: '最长发货时效小时数(可选,如 24 表示只看24h内发货的)' },
249
+ paste_text: { type: 'string', description: '用户粘贴的分享文本/外链原文(可选,服务端做轻量解析)' },
250
+ external_link: {
251
+ type: 'object',
252
+ description: '外链结构化匹配(可选,由 agent 解析得到)',
253
+ properties: {
254
+ platform: { type: 'string', description: "'taobao'|'tmall'|'jd'|'pdd'|'1688'|'douyin'|'xhs'" },
255
+ external_id: { type: 'string', description: '平台商品 canonical ID(可选)' },
256
+ external_title: { type: 'string', description: '平台商品标题全文(可选)' },
257
+ canonical_url: { type: 'string', description: '规范 URL(可选)' },
258
+ },
259
+ },
260
+ limit: { type: 'number', description: '返回数量上限,默认 10,agent 模式可调到 200' },
261
+ sort: { type: 'string', enum: ['trending', 'newest', 'rating', 'price_asc', 'price_desc', 'random'], description: '排序:trending=综合分(默认),newest=最新,rating=信誉,price_asc/price_desc=价格,random=随机探索' },
262
+ has_sales: { type: 'string', enum: ['true', 'false'], description: 'true=只看已成交,false=只看新品' },
263
+ ship_to: { type: 'string', description: '配送目的地(省/市),自动过滤不可派送商品' },
264
+ seller_id: { type: 'string', description: '只看某卖家的商品' },
265
+ cursor: { type: 'string', description: '分页 cursor(上次返回的 next_cursor)' },
266
+ },
267
+ },
268
+ },
269
+ {
270
+ name: 'webaz_verify_price',
271
+ description: `Lock product price before placing order — returns session_token.
272
+ Recommended flow for agents: call this first to verify & lock price, then call webaz_place_order with session_token.
273
+ Benefits: (1) order price matches displayed; (2) caller is alerted if price changed; (3) platform only liable for T0 price.
274
+ session_token: 10-minute TTL, single-use.
275
+
276
+ ──
277
+ 中文:下单前锁价 — 10 分钟一次性 session_token;agent 应先调此再调 place_order。`,
278
+ inputSchema: {
279
+ type: 'object',
280
+ properties: {
281
+ api_key: { type: 'string', description: '买家的 api_key' },
282
+ product_id: { type: 'string', description: '商品 ID(从 webaz_search 获得)' },
283
+ quantity: { type: 'number', description: '购买数量,默认 1' },
87
284
  },
285
+ required: ['api_key', 'product_id'],
88
286
  },
89
287
  },
90
288
  {
91
289
  name: 'webaz_list_product',
92
- description: `卖家上架新商品到 WebAZ。
93
- 需要卖家角色的 api_key
94
- 上架时系统会自动计算建议质押金额(商品价格的 15%),用于保障买家权益。
95
- 商品上架后买家可以搜索到并下单。`,
290
+ description: `Seller product catalog management (create / update / shelf / archive / query own listings).
291
+ Requires seller-role api_key. On create, system auto-suggests stake (~15% of price) to secure buyer protection.
292
+
293
+ Fill fields completely — better agent_summary helps buyer agents make comparison decisions (brand / return / handling / warranty).
294
+ Note: for "exclusive price vs external link" listing, use PWA Web only — link claim needs crowd-verification.
295
+
296
+ Actions:
297
+ - create list a new product (default; needs title/description/price)
298
+ - mine list all my products (no product_id needed)
299
+ - update modify fields (needs product_id; only changed fields)
300
+ - delist move to warehouse (status: warehouse; needs product_id)
301
+ - relist re-shelve from warehouse (status: active; needs product_id)
302
+ - trash move to trash (status: deleted; needs product_id)
303
+ - delete permanent delete (only trash items with no active orders; needs product_id)
304
+
305
+ ──
306
+ 中文:卖家商品目录管理(上架/更新/下架/回收/删除/查询)。Actions: create / mine / update / delist / relist / trash / delete。上架时系统自动建议 15% 价格的质押金。`,
96
307
  inputSchema: {
97
308
  type: 'object',
98
309
  properties: {
99
310
  api_key: { type: 'string', description: '卖家的 api_key' },
100
- title: { type: 'string', description: '商品名称' },
101
- description: { type: 'string', description: '商品详细描述' },
102
- price: { type: 'number', description: '商品价格(WAZ)' },
311
+ action: {
312
+ type: 'string',
313
+ enum: ['create', 'mine', 'update', 'delist', 'relist', 'trash', 'delete'],
314
+ description: '要执行的操作(缺省 = create)',
315
+ },
316
+ product_id: { type: 'string', description: '商品 ID(update / delist / relist / trash / delete 时必填)' },
317
+ title: { type: 'string', description: '商品名称(create 必填;update 可选)' },
318
+ description: { type: 'string', description: '商品详细描述(create 必填;update 可选)' },
319
+ price: { type: 'number', description: '商品价格 WAZ(create 必填;update 可选)' },
103
320
  stock: { type: 'number', description: '库存数量,默认 1' },
104
321
  category: { type: 'string', description: '商品分类(可选)' },
322
+ specs: {
323
+ type: 'object',
324
+ description: '结构化规格键值对,如 {"颜色":"黑色","内存":"16GB","存储":"512GB"}(可选)',
325
+ },
326
+ brand: { type: 'string', description: '品牌(可选)' },
327
+ model: { type: 'string', description: '型号(可选)' },
328
+ source_price: {
329
+ type: 'number',
330
+ description: '同款商品的外部参考价(可选,仅作展示,不参与独家价认证——需通过 PWA 完成链接认领)',
331
+ },
332
+ ship_regions: { type: 'string', description: '发货地区,默认"全国"' },
333
+ handling_hours: { type: 'number', description: '发货时效(小时),默认 24' },
334
+ estimated_days: {
335
+ type: 'object',
336
+ description: '预计送达天数:数字(如 4)或区域映射(如 {"江浙沪":2,"全国":4})',
337
+ },
338
+ fragile: { type: 'boolean', description: '是否易碎品,默认 false' },
339
+ return_days: { type: 'number', description: '支持退货天数,默认 7(填 0 表示不支持退货)' },
340
+ return_condition: { type: 'string', description: '退货条件说明(可选)' },
341
+ warranty_days: { type: 'number', description: '质保天数,默认 0' },
342
+ // S2 库存预警
343
+ low_stock_threshold: {
344
+ type: 'number',
345
+ description: '【S2】库存预警阈值(≤ 此数时通知卖家;填 0 = 关闭预警)',
346
+ },
347
+ auto_delist_on_zero: {
348
+ type: 'boolean',
349
+ description: '【S2】库存归零时自动下架到仓库(防超卖)',
350
+ },
351
+ // S3 跨境上架多语言
352
+ i18n_titles: {
353
+ type: 'object',
354
+ description: '【S3】多语言标题,key 是语言码(en/ja/ko/fr/de/es/pt/ru/ar),value 是该语言标题(≤500 字)。zh 用 title 字段,无需填这里。例: {"en":"Wireless Earbuds","ja":"ワイヤレスイヤホン"}',
355
+ },
356
+ i18n_descs: {
357
+ type: 'object',
358
+ description: '【S3】多语言描述,结构同 i18n_titles',
359
+ },
360
+ // S4 商品溯源(origin_claims)— 协议级可挑战
361
+ origin_claims: {
362
+ type: 'object',
363
+ description: '【S4】商品溯源声明(可挑战)。例: {"made_in":"日本京都","material":"100% 棉 GOTS 认证","certs":[{"name":"GOTS","sha256":"<64 位 hex>"}]}。整体 JSON ≤ 4KB;任一证书 sha256 必须是 64 位 hex。任何买家可发起 claim 验证。',
364
+ },
105
365
  },
106
- required: ['api_key', 'title', 'description', 'price'],
366
+ required: ['api_key'],
107
367
  },
108
368
  },
109
369
  {
110
370
  name: 'webaz_place_order',
111
- description: `买家下单购买商品。
112
- 需要买家角色的 api_key。
113
- 下单后资金自动进入协议托管,卖家需在 24 小时内接单。
114
- 如卖家超时不接单,协议自动退款并记录违约。`,
371
+ description: `Buyer places an order. Requires buyer-role api_key.
372
+
373
+ On order, funds auto-enter protocol escrow. Order deadlines (all stored as absolute ISO timestamps in the response):
374
+ - accept_deadline: T+48h (i.e., seller has 24h after the buyer's payment which is itself T+24h)
375
+ - ship_deadline: T+120h (72h after accept)
376
+ - pickup_deadline: T+168h (48h after ship)
377
+ - delivery_deadline: T+336h (7d after pickup)
378
+ - confirm_deadline: T+408h (72h after delivery)
379
+
380
+ Missing any deadline auto-judges fault against the responsible party and triggers compensation per protocol economics.
381
+
382
+ 【B1 Cross-border tax】For cross-border orders, server auto-estimates import duty (seller's est_import_duty_pct × price) and returns it in the response.
383
+ 【B2 Privacy mode】Pass anonymous_recipient=true → system generates a PR-XXXXX alias instead of real name on shipping label.
384
+ 【B5 Donation】Optional donation_pct (0 / 0.5% / 1% / 2% / 5%) — sends that fraction of order amount to charity_fund.
385
+
386
+ ──
387
+ 中文:买家下单 — 资金进托管。订单 6 个 deadline 都是绝对时间戳,每个相对前一步的允许时长:accept 24h→ship 72h→pickup 48h→deliver 7d→confirm 72h。任何一个超时由对应责任方违约。可选:跨境关税估算 / 匿名收件 / 慈善捐赠。`,
115
388
  inputSchema: {
116
389
  type: 'object',
117
390
  properties: {
@@ -120,9 +393,24 @@ const TOOLS = [
120
393
  quantity: { type: 'number', description: '购买数量,默认 1' },
121
394
  shipping_address: { type: 'string', description: '收货地址' },
122
395
  notes: { type: 'string', description: '给卖家的备注(可选)' },
396
+ session_token: {
397
+ type: 'string',
398
+ description: '价格锁定 token(推荐):由 webaz_verify_price 返回,确保下单价格与展示价格一致',
399
+ },
123
400
  promoter_api_key: {
124
401
  type: 'string',
125
- description: '推荐人的 api_key(可选,如果是通过推荐链接来的)',
402
+ description: '推荐人的 api_key(可选)。⚠️ 仅记 L1(直接推荐人,70% commission);L2/L3 无法经 MCP 推断,会按 region 规则 redirect(singapore 等高 max_levels → charity chain_gap;global max_levels=1 → global_fund region cap)。完整 7:2:1 三级链需买家经 webaz_share_link 生成的 ?ref= URL 浏览器点击(建 product_share_attribution)。',
403
+ },
404
+ // B2 隐私购物
405
+ anonymous_recipient: {
406
+ type: 'boolean',
407
+ description: '【B2】匿名收件:系统生成 PR-XXXXX 代号代替真实姓名(卖家面单只见代号 + 地址)',
408
+ },
409
+ // B5 公益捐赠(按订单总额百分比,定额选项防机器人滥用)
410
+ donation_pct: {
411
+ type: 'number',
412
+ enum: [0, 0.005, 0.01, 0.02, 0.05],
413
+ description: '【B5】随单捐赠占订单金额的比例(0 / 0.5% / 1% / 2% / 5%)。捐赠金额另行计算并入 charity_fund,订单完成后落账。',
126
414
  },
127
415
  },
128
416
  required: ['api_key', 'product_id', 'shipping_address'],
@@ -130,22 +418,25 @@ const TOOLS = [
130
418
  },
131
419
  {
132
420
  name: 'webaz_update_order',
133
- description: `更新订单状态(每个角色只能执行自己的操作)。
421
+ description: `Update order status (each role can only perform their own actions).
422
+
423
+ Seller actions:
424
+ - accept: accept order (within 24h of payment)
425
+ - ship: confirm dispatch (needs tracking number, within promised handling time)
134
426
 
135
- 卖家可执行的 action:
136
- - accept:接受订单(付款后 24h 内必须执行)
137
- - ship:确认发货(需要物流单号,接单后按承诺时间内执行)
427
+ Logistics actions:
428
+ - pickup: confirm parcel pickup (within 48h of ship)
429
+ - transit: update to in-transit
430
+ - deliver: confirm delivery (needs delivery proof description)
138
431
 
139
- 物流方可执行的 action:
140
- - pickup:确认揽收包裹(发货后 48h 内)
141
- - transit:更新为运输中
142
- - deliver:确认投递完成(需要投递证明描述)
432
+ Buyer actions:
433
+ - confirm: confirm receipt → triggers fund settlement
434
+ - dispute: raise dispute (needs reason; freezes funds pending arbitration)
143
435
 
144
- 买家可执行的 action:
145
- - confirm:确认收货,触发资金结算
146
- - dispute:发起争议(需要说明原因,会冻结资金等待仲裁)
436
+ If a deadline is missed, protocol auto-marks that party as in default.
147
437
 
148
- 超过截止时间未操作,协议会自动判定该方违约。`,
438
+ ──
439
+ 中文:更新订单状态 — 角色专属 action(卖家 accept/ship · 物流 pickup/transit/deliver · 买家 confirm/dispute)。截止超时自动判违约。`,
149
440
  inputSchema: {
150
441
  type: 'object',
151
442
  properties: {
@@ -167,9 +458,12 @@ const TOOLS = [
167
458
  },
168
459
  {
169
460
  name: 'webaz_get_status',
170
- description: `查询订单的当前状态、完整历史记录和当前责任方。
171
- 需要参与该订单的 api_key(买家、卖家或物流方均可查询)。
172
- 返回:当前状态、状态历史(谁在什么时候做了什么)、当前应该由谁操作、截止时间。`,
461
+ description: `Query order status, full history, and current responsible party.
462
+ Requires api_key of an order participant (buyer / seller / logistics may all query).
463
+ Returns: current status, status history (who did what when), next responsible actor, deadline.
464
+
465
+ ──
466
+ 中文:查订单状态 + 历史 + 当前责任方 + 截止时间。任一参与方可查。`,
173
467
  inputSchema: {
174
468
  type: 'object',
175
469
  properties: {
@@ -181,21 +475,40 @@ const TOOLS = [
181
475
  },
182
476
  {
183
477
  name: 'webaz_wallet',
184
- description: `查看自己的钱包余额和收益统计。
185
- 返回:可用余额、质押中金额、托管中金额、累计总收益。`,
478
+ description: `Wallet query (balance + earnings stats + deposit/withdrawal/income history).
479
+
480
+ Actions:
481
+ - view available balance + staked + in-escrow + total earnings + reputation tier (default)
482
+ - deposits last 10 on-chain deposit records (tx_hash / amount / block_number)
483
+ - withdrawals last 10 withdrawal requests (with status / tx_hash)
484
+ - income income breakdown: referral L1/L2/L3 + binary PV matching + sales net
485
+
486
+ NOTE: actual withdrawals / deposits / whitelist management require Passkey + email OTP via PWA Web only.
487
+ This is a security boundary — agents CANNOT bypass it. Use this tool for read-only queries only.
488
+
489
+ ──
490
+ 中文:钱包查询(余额/收益/充提历史)。仅查询用,实际充提需 PWA Web + Passkey + 邮件 OTP,agent 不可绕过。`,
186
491
  inputSchema: {
187
492
  type: 'object',
188
493
  properties: {
189
494
  api_key: { type: 'string', description: '你的 api_key' },
495
+ action: {
496
+ type: 'string',
497
+ enum: ['view', 'deposits', 'withdrawals', 'income'],
498
+ description: '操作类型(缺省 = view)',
499
+ },
190
500
  },
191
501
  required: ['api_key'],
192
502
  },
193
503
  },
194
504
  {
195
505
  name: 'webaz_notifications',
196
- description: `查询当前用户的通知消息(L2-6 通知系统)。
197
- Agent 应定期调用此工具检查是否有待处理的订单事件。
198
- 每次有状态变更(新订单/发货/争议等),相关参与方都会收到通知。`,
506
+ description: `Query user notifications (L2-6 notification system).
507
+ Agents should poll this periodically to check for pending order events.
508
+ Every status change (new order / ship / dispute / etc.) notifies relevant participants.
509
+
510
+ ──
511
+ 中文:查通知 — agent 定期调用检查待处理事件(新订单/发货/争议等会通知所有相关方)。`,
199
512
  inputSchema: {
200
513
  type: 'object',
201
514
  properties: {
@@ -208,72 +521,168 @@ Agent 应定期调用此工具检查是否有待处理的订单事件。
208
521
  },
209
522
  {
210
523
  name: 'webaz_dispute',
211
- description: `管理争议流程(L3 争议系统)。
212
-
213
- 当买家认为货不对版、货损、卖家欺诈时,可通过 webaz_update_order action=dispute 发起争议,
214
- 然后用本工具进行后续操作。
215
-
216
- 协议保障机制(无需人工干预):
217
- - 被诉方有 48 小时提交反驳证据,否则协议自动判发起方胜诉
218
- - 仲裁员有 120 小时做出裁定,否则协议默认退款给买家
219
- - 裁定一旦执行,资金立即自动分配,无法撤销
220
-
221
- action 说明:
222
- - view:查看争议详情(任何参与方可调用)
223
- - list_open:查看所有待处理争议(仅仲裁员)
224
- - respond:被诉方提交反驳证据(必须在 48h 截止时间前)
225
- - arbitrate:仲裁员做出裁定并执行资金处置
226
-
227
- ruling 裁定选项(arbitrate 时使用):
228
- - refund_buyer:全额退款给买家,扣押卖家部分保证金
229
- - release_seller:资金释放给卖家(卖家胜诉)
230
- - partial_refund:部分退款(需指定 refund_amount);若责任在第三方(如物流),同时指定 liable_party(责任方 user_id),此时卖家全额结算,赔偿金从责任方扣除`,
524
+ description: `Manage dispute lifecycle (L3 dispute system).
525
+
526
+ When buyer claims item-not-as-described / damaged / seller fraud, first raise dispute via webaz_update_order action=dispute,
527
+ then use this tool for follow-ups.
528
+
529
+ Protocol guarantees (no human intervention):
530
+ - Respondent has 48h to submit rebuttal evidence, else protocol auto-judges in initiator's favor
531
+ - Arbitrator has 120h to issue ruling, else protocol defaults to refund-buyer
532
+ - Once executed, ruling fund movement is instant and irreversible
533
+
534
+ Actions:
535
+ - view: view dispute details (any participant)
536
+ - list_open: list all pending disputes (arbitrators only)
537
+ - respond: respondent submits rebuttal evidence (before 48h deadline)
538
+ - add_evidence: any participant (incl. logistics) submits supplementary evidence (Wave 3)
539
+ - arbitrate: arbitrator issues ruling + executes fund disposition (must be assigned arbitrator)
540
+
541
+ Ruling options (when arbitrate):
542
+ - refund_buyer: full refund to buyer + partial stake forfeit from seller
543
+ - release_seller: release funds to seller (seller wins)
544
+ - partial_refund: partial refund (needs refund_amount); if third-party fault, set liable_party (user_id) → seller settled full, compensation deducted from liable party
545
+ - liability_split: distributed liability (Wave 3) — needs liability_parties: [{user_id, amount}] for proportional stake deductions
546
+
547
+ ─────────────────────
548
+ ⚠️ Iron-Rule boundary (spec §4): arbitrate is a protocol iron-rule node requiring arbitrator to complete Passkey re-confirmation via PWA Web UI.
549
+ Direct agent calls return 412 HUMAN_PRESENCE_REQUIRED — DO NOT retry; instead, guide the user to the browser.
550
+ view / list_open / respond / add_evidence can be agent-proxied; only arbitrate needs human.
551
+
552
+ ──
553
+ 中文:管理争议(L3)。Actions: view / list_open / respond / add_evidence (agent 可代) · arbitrate (需 PWA + Passkey 人工)。被诉方 48h 反驳,仲裁员 120h 裁定,超时自动判。`,
231
554
  inputSchema: {
232
555
  type: 'object',
233
556
  properties: {
234
557
  api_key: { type: 'string', description: '操作者的 api_key' },
235
558
  action: {
236
559
  type: 'string',
237
- enum: ['view', 'list_open', 'respond', 'arbitrate'],
560
+ enum: ['view', 'list_open', 'respond', 'add_evidence', 'arbitrate'],
238
561
  description: '要执行的操作',
239
562
  },
240
- dispute_id: { type: 'string', description: '争议 ID(respond/arbitrate 时必填,view 时与 order_id 二选一)' },
563
+ dispute_id: { type: 'string', description: '争议 ID(respond/add_evidence/arbitrate 时必填,view 时与 order_id 二选一)' },
241
564
  order_id: { type: 'string', description: '订单 ID(view 时可替代 dispute_id)' },
242
565
  notes: { type: 'string', description: '回应说明 / 反驳理由(respond 时填写)' },
243
- evidence_description: { type: 'string', description: '证据描述(respond 时建议填写)' },
566
+ evidence_description: { type: 'string', description: '证据描述(respond/add_evidence 时填写)' },
244
567
  ruling: {
245
568
  type: 'string',
246
- enum: ['refund_buyer', 'release_seller', 'partial_refund'],
569
+ enum: ['refund_buyer', 'release_seller', 'partial_refund', 'liability_split'],
247
570
  description: '裁定结果(arbitrate 时必填)',
248
571
  },
249
572
  refund_amount: { type: 'number', description: '部分退款金额,仅 ruling=partial_refund 时使用' },
250
573
  liable_party: { type: 'string', description: '第三方责任方 user_id(partial_refund 时可选):指定后赔偿金从该方扣除,卖家全额结算' },
574
+ liability_parties: {
575
+ type: 'array',
576
+ description: '责任分配数组(liability_split 时必填),每项 { user_id: string, amount: number }',
577
+ items: {
578
+ type: 'object',
579
+ properties: {
580
+ user_id: { type: 'string', description: '责任方 user_id' },
581
+ amount: { type: 'number', description: '该责任方需承担的金额(WAZ)' },
582
+ },
583
+ required: ['user_id', 'amount'],
584
+ },
585
+ },
251
586
  ruling_reason: { type: 'string', description: '裁定理由(arbitrate 时必填,将永久记录在链上)' },
252
587
  },
253
588
  required: ['api_key', 'action'],
254
589
  },
255
590
  },
591
+ {
592
+ name: 'webaz_claim_verify',
593
+ description: `Crowd-sourced claim verification — no central arbitrator, 3 eligible verifiers reach consensus by vote.
594
+
595
+ When to use:
596
+ - Buyer: suspects a seller's product claim (e.g. "brand new sealed", "ships within 24h") → wants decentralized verification
597
+ - Seller: defends against an opened verification by submitting counter-evidence → extends task by 24h
598
+ - Verifier: browses open tasks, votes on eligible ones (pass/fail/no_fault/abstain), settles on 3rd vote
599
+
600
+ Actions:
601
+ [Buyer]
602
+ - create open a verification on your own order's claim (locks 10 WAZ stake; order must be paid/delivered, NOT completed; settles when 3 votes collected)
603
+ - view task details (visible to participants + voters + eligible verifiers)
604
+ - mine all my tasks (buyer + seller + verifier perspectives)
605
+
606
+ [Seller]
607
+ - submit_seller_evidence submit rebuttal → +24h extension, task stays open
608
+
609
+ [Verifier — gated by 7-condition eligibility, NOT just read-only]
610
+ - available list open tasks I can take (REQUIRES eligibility: age≥60d / email verified / ≥20 completed orders / 0 arbitration losses / never suspended / wallet ≥200 WAZ / reputation ≥110)
611
+ - vote vote on a task (pass / fail / no_fault / abstain)
612
+ pass = seller's claim is true / product OK
613
+ fail = seller's claim is false / product mismatch
614
+ no_fault = neither party clearly at fault (dispute has merit but inconclusive)
615
+ abstain = "not my expertise, skipping" — not counted in 3-vote consensus, doesn't affect accuracy (V3 right-to-decline)
616
+
617
+ [Become Verifier]
618
+ - eligibility check my qualification status (reputation signals)
619
+ - verifier_status my quota / tier / stake
620
+ - apply apply to verifier whitelist (needs reputation + stake lock)
621
+ - withdraw_application withdraw application, refund stake
622
+ - appeal appeal a suspension (only when suspended)
623
+
624
+ ─────────────────────
625
+ ⚠️ Iron-Rule boundary (spec §4): vote is a protocol iron-rule node requiring verifier to complete Passkey re-confirmation via PWA Web UI.
626
+ Direct agent calls to vote return 412 HUMAN_PRESENCE_REQUIRED — DO NOT retry; instead, guide the user to the browser.
627
+ Other actions (create / view / mine / available / eligibility / apply / appeal etc.) can be agent-proxied; only vote needs human.
628
+
629
+ ──
630
+ 中文:众包索赔验证 — 3 verifier 共识投票判定。买家创建、卖家反证、verifier 投票。仅 vote 需 PWA + Passkey 人工,其他 actions agent 可代。`,
631
+ inputSchema: {
632
+ type: 'object',
633
+ properties: {
634
+ api_key: { type: 'string', description: '你的 api_key' },
635
+ action: {
636
+ type: 'string',
637
+ enum: ['create', 'view', 'mine', 'submit_seller_evidence', 'available', 'vote', 'eligibility', 'verifier_status', 'apply', 'withdraw_application', 'appeal'],
638
+ description: '要执行的操作',
639
+ },
640
+ // create
641
+ order_id: { type: 'string', description: '订单 ID(create 时必填)。订单状态须在 paid/delivered(completed 终态不可发起)' },
642
+ claim_target: {
643
+ type: 'string',
644
+ enum: ['price', 'commission', 'protection', 'return', 'warranty', 'handling', 'other'],
645
+ description: '声明对象(create 时必填)。7 类: price(价格争议)/ commission(佣金)/ protection(买家保护)/ return(退货)/ warranty(保修)/ handling(履约时效)/ other',
646
+ },
647
+ claim_text: { type: 'string', description: '声明文本 6-500 字(create 时必填)。create 会锁定 10 WAZ 防 spam(无责败诉退回)' },
648
+ evidence_uri: { type: 'string', description: '证据 URI(create/vote/submit_seller_evidence 可选;buyer 自带 / verifier 附证据 / seller 反驳证据)' },
649
+ // view / vote / submit_seller_evidence
650
+ task_id: { type: 'string', description: '任务 ID(view / vote / submit_seller_evidence 时必填)' },
651
+ // vote
652
+ vote: { type: 'string', enum: ['pass', 'fail', 'no_fault', 'abstain'], description: 'vote 时必填;abstain = 不熟悉弃投(V3)' },
653
+ note: { type: 'string', description: '投票说明(vote 可选,≤500 字)' },
654
+ // appeal
655
+ reason: { type: 'string', description: '申诉理由(appeal 时必填,≤500 字)' },
656
+ },
657
+ required: ['api_key', 'action'],
658
+ },
659
+ },
256
660
  {
257
661
  name: 'webaz_skill',
258
- description: `L4-4 Skill 市场——让卖家发布可复用的 Agent 能力插件,买家 Agent 一键订阅。
259
-
260
- Skill 是解决冷启动的核心机制:现有 Amazon/Shopify 卖家零成本接入 WebAZ,
261
- 买家 Agent 订阅后可自动发现、优先呈现这些卖家的商品,成交后 Skill 发布者获得推荐佣金。
262
-
263
- Skill 类型(skill_type):
264
- - catalog_sync 目录同步:将外部店铺(Amazon/Shopify/自定义)接入 WebAZ 搜索,买家订阅后优先看到
265
- - auto_accept 自动接单:买家下单后立即接受,无需等待(config: min_amount, max_amount, max_daily_orders)
266
- - price_negotiation 价格协商:允许 Agent 在限定范围内议价(config: max_discount_pct, min_quantity)
267
- - quality_guarantee 质量承诺:额外质押保证金,问题可额外赔偿(config: guarantee_amount, coverage_days)
268
- - instant_ship 极速发货:承诺 24h 内发货(config: ship_within_hours)
269
-
270
- action 说明:
271
- - list 浏览 Skill 市场(无需登录)
272
- - publish 发布新 Skill(仅卖家)
273
- - subscribe 订阅 Skill(买家订阅后可获得额外好处)
274
- - unsubscribe 取消订阅
275
- - my_skills 查看自己发布的 Skill(卖家)
276
- - my_subs 查看自己订阅的 Skill(买家)`,
662
+ description: `L4-4 Skill marketplace sellers publish reusable seller-side behaviour configs; buyer Agents subscribe with one click.
663
+
664
+ ⚠️ Important — Skill is NOT executable code distribution. There are exactly 5 typed Skill kinds (below), each accepts only **structured config parameters** (numbers / enums / amounts). There is NO path for an Agent to download and run arbitrary third-party code via this marketplace. Subscribing = setting a flag + data binding, not installing a plugin. (Common Web2 "plugin marketplace" risk model does NOT apply here.)
665
+
666
+ Skill is WebAZ's cold-start mechanism: existing Amazon/Shopify sellers integrate with zero cost,
667
+ buyer agents subscribe and auto-discover those sellers' products with priority; on sale, the Skill publisher earns referral commission.
668
+
669
+ Skill types (typed, NOT free-form code):
670
+ - catalog_sync sync external store (Amazon/Shopify/custom) into WebAZ search → subscribers see priority
671
+ - auto_accept auto-accept incoming orders, no wait (config: min_amount, max_amount, max_daily_orders)
672
+ - price_negotiation allow Agent-side haggling within limits (config: max_discount_pct, min_quantity)
673
+ - quality_guarantee extra stake for issue compensation (config: guarantee_amount, coverage_days)
674
+ - instant_ship guarantee 24h dispatch (config: ship_within_hours)
675
+
676
+ Actions:
677
+ - list browse Skill marketplace (no auth)
678
+ - publish publish new Skill (sellers only)
679
+ - subscribe subscribe to a Skill (buyer perks)
680
+ - unsubscribe cancel subscription
681
+ - my_skills my published Skills (seller view)
682
+ - my_subs my subscribed Skills (buyer view)
683
+
684
+ ──
685
+ 中文:Skill 市场 — 卖家发布 agent 能力插件(catalog_sync / auto_accept / price_negotiation / quality_guarantee / instant_ship),买家 agent 订阅。Actions: list / publish / subscribe / unsubscribe / my_skills / my_subs。`,
277
686
  inputSchema: {
278
687
  type: 'object',
279
688
  properties: {
@@ -306,731 +715,3146 @@ action 说明:
306
715
  },
307
716
  {
308
717
  name: 'webaz_mykey',
309
- description: 'Recover your api_key by name, or view your profile and roles. Use this if you forgot your api_key.',
718
+ description: `Confirm account existence by handle + permanent_code. Returns redacted api_key hint only full api_key disclosure requires PWA + Passkey verification (Iron-Rule).
719
+
720
+ Rate-limited: 5 attempts per handle per hour. Excessive failures lock the handle for 1 hour.
721
+
722
+ Returns:
723
+ - found: boolean
724
+ - api_key_hint (e.g. "key_7d3d***faa7b") — confirms which account, never the full key
725
+ - full_api_key_recovery — instructions to use PWA for the real recovery
726
+
727
+ ──
728
+ 中文:handle + permanent_code 双因素确认账户存在,仅返回 redact 过的 api_key_hint。完整 api_key 必须经 PWA + Passkey 二次验证(Iron-Rule)。同 handle 每小时最多 5 次。`,
310
729
  inputSchema: {
311
730
  type: 'object',
312
731
  properties: {
313
- name: { type: 'string', description: 'The name you registered with' },
732
+ handle: { type: 'string', description: 'Unique handle assigned at registration (check handle_modified flag if name was taken)' },
733
+ permanent_code: { type: 'string', description: '6-char recovery code from registration response (uppercase)' },
314
734
  },
315
- required: ['name'],
735
+ required: ['handle', 'permanent_code'],
316
736
  },
317
737
  },
318
738
  {
319
739
  name: 'webaz_profile',
320
- description: 'View your profile, manage roles (add a new role or switch active role). One account can hold multiple roles.',
740
+ description: `View your own profile / manage roles, AND view any user's public profile + content streams (个人主页内容流).
741
+
742
+ Self actions (need api_key):
743
+ - view show your profile & wallet & api_key hint
744
+ - add_role add a new role
745
+ - switch_role switch active role (one account can hold multiple roles)
746
+
747
+ Public-profile actions:
748
+ - view_user another user's public profile (user_id = usr_xxx / permanent_code / @handle; needs api_key)
749
+ - feed a user's content stream (user_id + feed). feed options:
750
+ secondhand | auctions | reviews | products | shares | reputation | pv | liked
751
+ (secondhand/auctions/reviews/products/shares/reputation are public;
752
+ pv needs api_key; liked is owner-only and needs your own api_key)
753
+
754
+ ──
755
+ 中文:看自己资料/管角色,也能看他人公开主页 + 内容流(二手/拍卖/测评/商品/笔记/信誉/PV/点赞)。Actions: view / add_role / switch_role / view_user / feed。`,
321
756
  inputSchema: {
322
757
  type: 'object',
323
758
  properties: {
324
- api_key: { type: 'string', description: 'Your api_key' },
759
+ api_key: { type: 'string', description: 'Your api_key(view/add_role/switch_role/view_user 必填;公开 feed 可省略)' },
325
760
  action: {
326
761
  type: 'string',
327
- enum: ['view', 'add_role', 'switch_role'],
328
- description: 'view: show profile & api_key | add_role: add a new role | switch_role: switch active role',
762
+ enum: ['view', 'add_role', 'switch_role', 'view_user', 'feed'],
763
+ description: 'view/add_role/switch_role = 自己;view_user/feed = 看他人主页/内容流',
329
764
  },
330
765
  role: {
331
766
  type: 'string',
332
767
  enum: ['buyer', 'seller', 'logistics', 'arbitrator'],
333
768
  description: 'Role to add or switch to (required for add_role / switch_role)',
334
769
  },
770
+ user_id: { type: 'string', description: '目标用户:usr_xxx / 永久码 / @handle(view_user / feed 时必填)' },
771
+ feed: {
772
+ type: 'string',
773
+ enum: ['secondhand', 'auctions', 'reviews', 'products', 'shares', 'reputation', 'pv', 'liked'],
774
+ description: '内容流类型(feed 时必填)',
775
+ },
776
+ },
777
+ required: ['action'],
778
+ },
779
+ },
780
+ {
781
+ name: 'webaz_revoke_key',
782
+ description: `Initiate api_key revocation. Iron-Rule: actual revocation requires PWA + Passkey confirmation — MCP only registers the intent and returns the PWA URL where you finish the action.
783
+
784
+ Use when:
785
+ - Your api_key was leaked or you suspect unauthorized access
786
+ - You are decommissioning an agent / device
787
+
788
+ After PWA confirmation, the old api_key returns 401 on all tools. Plan ahead: have a new key (via webaz_rotate_key) before revoking, or you lose access.
789
+
790
+ ──
791
+ 中文:发起 api_key 吊销。Iron-Rule:真正吊销必须 PWA + Passkey 二次确认(agent 不能单方面执行不可逆操作)。MCP 仅登记意图,返回 PWA 确认 URL。`,
792
+ inputSchema: {
793
+ type: 'object',
794
+ properties: {
795
+ api_key: { type: 'string', description: 'Your current api_key (the one to revoke)' },
796
+ reason: { type: 'string', description: 'Optional: leaked / lost_device / rotation / unspecified' },
797
+ },
798
+ required: ['api_key'],
799
+ },
800
+ },
801
+ {
802
+ name: 'webaz_rotate_key',
803
+ description: `Initiate api_key rotation (invalidate old key + issue new one). Iron-Rule: requires PWA + Passkey confirmation. After PWA confirms, old key returns 401 and new key is shown once — store immediately.
804
+
805
+ Safer than webaz_revoke_key because issuance is atomic with invalidation — no access gap.
806
+
807
+ ──
808
+ 中文:发起 api_key 轮换(旧 key 失效 + 新 key 同时签发)。比 revoke 安全,原子操作无访问中断。需 PWA + Passkey。`,
809
+ inputSchema: {
810
+ type: 'object',
811
+ properties: {
812
+ api_key: { type: 'string', description: 'Your current api_key (will be invalidated after PWA confirm)' },
813
+ reason: { type: 'string', description: 'Optional: rotation / leaked / scheduled' },
814
+ },
815
+ required: ['api_key'],
816
+ },
817
+ },
818
+ {
819
+ name: 'webaz_referral',
820
+ description: `View your full referral status: 3-tier commission (推土机) team + earnings + invite links + binary PV matching (原子能) tree + tier progress + L1 share permission gate. Use this to know how to promote and what you have earned.
821
+
822
+ ⚠️ Commission structure involves multi-tier referral + binary pairing tree which may overlap with MLM legal definitions in many jurisdictions (see webaz_info.commission_model.compliance_notice). Agents acting on behalf of a human user MUST get the user's explicit consent before generating referral links or promoting on their behalf. Do NOT auto-recruit.`,
823
+ inputSchema: {
824
+ type: 'object',
825
+ properties: {
826
+ api_key: { type: 'string', description: 'Your api_key' },
827
+ },
828
+ required: ['api_key'],
829
+ },
830
+ },
831
+ {
832
+ name: 'webaz_share_link',
833
+ description: `Generate a product share link with your referral attached. Open this in any social platform (TikTok / WeChat / Telegram). Whoever clicks and registers/buys will count toward your 3-tier commission (if you are verified buyer) and PV tree.
834
+
835
+ ⚠️ This tool builds the referral chain. Generating a link is taking an active promotion step in a multi-tier commission + binary pairing structure (see webaz_info.commission_model.compliance_notice — may overlap with MLM legal definitions). Agents acting for a human user MUST get explicit consent before calling this on their behalf. Do NOT auto-generate.`,
836
+ inputSchema: {
837
+ type: 'object',
838
+ properties: {
839
+ api_key: { type: 'string', description: 'Your api_key' },
840
+ product_id: { type: 'string', description: 'Product to promote (from webaz_search)' },
841
+ side: {
842
+ type: 'string',
843
+ enum: ['left', 'right', 'auto'],
844
+ description: 'Which side of your binary tree the new user lands. auto = weaker leg (default)',
845
+ },
846
+ },
847
+ required: ['api_key', 'product_id'],
848
+ },
849
+ },
850
+ {
851
+ name: 'webaz_blocklist',
852
+ description: `Manage your blocklist of sellers/users. Blocked users' products are auto-hidden from your searches.
853
+
854
+ Scope (元规则 #5 不偏袒 design):
855
+ - ✓ Hides blocked user's products from YOUR search results
856
+ - ✓ Prevents YOU from following them (webaz_follows respects this)
857
+ - ✗ Does NOT prevent placing orders on their products (商品发布即承诺销售 — public listing = sales commitment)
858
+ - ✗ Does NOT silence existing chat conversations (business context overrides)
859
+ - ✗ Does NOT prevent NEW chat starts on shared order/rfq context
860
+ - For active rejection: delist products, don't bid on RFQs, or cancel the order.
861
+
862
+ Returns the action result.`,
863
+ inputSchema: {
864
+ type: 'object',
865
+ properties: {
866
+ api_key: { type: 'string', description: 'Your api_key' },
867
+ action: { type: 'string', enum: ['list', 'block', 'unblock'], description: 'list: my blocked users | block: add | unblock: remove' },
868
+ user_id: { type: 'string', description: 'Target user id (required for block/unblock)' },
869
+ reason: { type: 'string', description: 'Optional reason for block (e.g. "fake product", "abuse")' },
335
870
  },
336
871
  required: ['api_key', 'action'],
337
872
  },
338
873
  },
339
- ];
340
- // ─── 工具处理函数 ─────────────────────────────────────────────
341
- function handleInfo() {
342
- const summary = getManifestSummary();
343
- const stats = (() => {
344
- try {
345
- const users = db.prepare("SELECT COUNT(*) as n FROM users WHERE id != 'sys_protocol'").get().n;
346
- const products = db.prepare("SELECT COUNT(*) as n FROM products WHERE status='active'").get().n;
347
- const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE status='completed'").get().n;
348
- return { participants: users, active_products: products, completed_orders: completed };
349
- }
350
- catch {
351
- return null;
352
- }
353
- })();
354
- return {
355
- ...summary,
356
- description: 'WebAZ 是一个去中心化商业协议。每笔交易通过状态机流转,每个状态转移都需要对应责任方的操作证明。任何超时未操作,协议自动判定该方违约并执行处置。',
357
- live_stats: stats,
358
- roles: {
359
- buyer: '下单、付款、确认收货或发起争议',
360
- seller: '上架商品、接单、按时发货(质押保证金确保履约)',
361
- logistics: '揽收包裹、更新运输状态、确认投递(获得 5% 物流费)',
362
- arbitrator: '处理争议,做出裁定(120h 内必须裁定,否则系统自动退款买家)',
874
+ {
875
+ name: 'webaz_follows',
876
+ description: 'Follow/unfollow users for the social feed. Or list your followers / following. Helps agents build the user\'s social graph.',
877
+ inputSchema: {
878
+ type: 'object',
879
+ properties: {
880
+ api_key: { type: 'string', description: 'Your api_key' },
881
+ action: { type: 'string', enum: ['list', 'follow', 'unfollow', 'status'], description: 'list: my follows + followers | follow/unfollow: change relation | status: check if I follow a user' },
882
+ user_id: { type: 'string', description: 'Target user (required for follow/unfollow/status)' },
883
+ },
884
+ required: ['api_key', 'action'],
363
885
  },
364
- quick_start: {
365
- 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. webaz_place_order() → 4. webaz_update_order(confirm)',
367
- logistics: '1. webaz_register(role=logistics) 2. webaz_update_order(pickup) webaz_update_order(deliver)',
886
+ },
887
+ {
888
+ name: 'webaz_nearby',
889
+ description: 'Query anonymized nearby (~11km cell) purchase aggregation. k-anonymity ≥ 3 privacy guard. Or set/clear your coarse geo location (0.1° = 11km precision, never stores exact GPS).',
890
+ inputSchema: {
891
+ type: 'object',
892
+ properties: {
893
+ api_key: { type: 'string', description: 'Your api_key' },
894
+ action: { type: 'string', enum: ['query', 'set_location', 'clear_location'], description: 'query: get aggregated nearby activity | set_location: set your geo cell | clear_location: remove' },
895
+ lat: { type: 'number', description: 'Latitude -90..90 (for set_location, auto-truncated to 0.1°)' },
896
+ lng: { type: 'number', description: 'Longitude -180..180 (for set_location, auto-truncated to 0.1°)' },
897
+ },
898
+ required: ['api_key', 'action'],
368
899
  },
369
- available_tools: TOOLS.map((t) => ({ name: t.name, description: t.description.split('\n')[0] })),
370
- full_manifest: `读取 MCP Resource "${MANIFEST_URI}" 获取完整协议规范(状态机/经济模型/争议系统/Skill 市场/声誉系统)`,
371
- };
372
- }
373
- function handleRegister(args) {
374
- const name = args.name;
375
- const role = args.role;
376
- const initialBalance = args.initial_balance ?? 1000;
377
- const validRoles = ['buyer', 'seller', 'logistics', 'reviewer', 'arbitrator'];
378
- if (!validRoles.includes(role)) {
379
- return { error: `无效角色:${role}。可选:${validRoles.join(', ')}` };
380
- }
381
- const id = generateId('usr');
382
- const apiKey = generateId('key');
383
- db.prepare('INSERT INTO users (id, name, role, roles, api_key) VALUES (?, ?, ?, ?, ?)').run(id, name, role, JSON.stringify([role]), apiKey);
384
- db.prepare('INSERT INTO wallets (user_id, balance) VALUES (?, ?)').run(id, initialBalance);
385
- return {
386
- success: true,
387
- message: `注册成功!请妥善保管你的 api_key,这是你在协议中的唯一身份凭证。`,
388
- user_id: id,
389
- name,
390
- role,
391
- api_key: apiKey,
392
- initial_balance: initialBalance,
393
- next_step: role === 'seller'
394
- ? '现在可以用 webaz_list_product 上架你的第一件商品'
395
- : role === 'buyer'
396
- ? '现在可以用 webaz_search 搜索商品'
397
- : '等待订单分配给你',
398
- };
399
- }
400
- function handleSearch(args) {
401
- const query = args.query ?? '';
402
- const category = args.category;
403
- const maxPrice = args.max_price;
404
- const limit = args.limit ?? 10;
405
- let sql = `
406
- SELECT p.*, u.name as seller_name
407
- FROM products p
408
- JOIN users u ON p.seller_id = u.id
409
- WHERE p.status = 'active' AND p.stock > 0
410
- `;
411
- const params = [];
412
- if (query) {
413
- sql += ` AND (p.title LIKE ? OR p.description LIKE ?)`;
414
- params.push(`%${query}%`, `%${query}%`);
415
- }
416
- if (category) {
417
- sql += ` AND p.category = ?`;
418
- params.push(category);
419
- }
420
- if (maxPrice !== undefined) {
421
- sql += ` AND p.price <= ?`;
900
+ },
901
+ {
902
+ name: 'webaz_default_address',
903
+ description: `Read or set your default shipping address. Used to auto-filter unshippable products in search + fallback when webaz_rfq/place_order omits shipping_address.
904
+
905
+ ⚠️ set 仅接受 2 个字段:text(自由格式地址字符串)+ region(可选;用于 unshippable 过滤)。
906
+ 不接受 structured 字段(recipient/line1/city/country/phone 等)— 跟传统电商地址 API 不同,agent 必须自己拼接。
907
+
908
+ ──
909
+ 中文:读取/设置默认收货地址。set 需要 text 必填 + region 可选;不收 structured 字段。`,
910
+ inputSchema: {
911
+ type: 'object',
912
+ properties: {
913
+ api_key: { type: 'string', description: 'Your api_key' },
914
+ action: { type: 'string', enum: ['read', 'set'], description: 'read: get current default | set: update' },
915
+ text: { type: 'string', description: 'Full address as free-text string (e.g. "John Doe / 1 Test St / Singapore SG / +65 12345678"). Required for set. ≤ 200 chars.' },
916
+ region: { type: 'string', description: 'Region tag for shipping match (e.g. "global", "china", "SG"). Optional for set. ≤ 40 chars.' },
917
+ },
918
+ required: ['api_key', 'action'],
919
+ },
920
+ },
921
+ {
922
+ name: 'webaz_shareables',
923
+ description: 'Manage your external content "shareables" (YouTube / TikTok / 小红书 / B 站 / IG / Twitter links bound to a product or anchor). WebAZ indexes only the URL, never the content bytes. Or query by product_id / anchor.',
924
+ inputSchema: {
925
+ type: 'object',
926
+ properties: {
927
+ api_key: { type: 'string', description: 'Your api_key' },
928
+ action: { type: 'string', enum: ['list_mine', 'add', 'delete', 'by_product', 'by_anchor'], description: 'list_mine | add (need external_url + product/anchor) | delete | by_product | by_anchor' },
929
+ external_url: { type: 'string', description: 'For action=add' },
930
+ title: { type: 'string', description: 'For action=add (optional)' },
931
+ description: { type: 'string', description: 'For action=add (optional)' },
932
+ related_product_id: { type: 'string', description: 'For action=add or by_product' },
933
+ related_anchor: { type: 'string', description: 'For action=add or by_anchor' },
934
+ shareable_id: { type: 'string', description: 'For action=delete' },
935
+ },
936
+ required: ['api_key', 'action'],
937
+ },
938
+ },
939
+ // ── P3 RFQ / bid / chat / auto_bid(MCP 通过 HTTP 调 PWA,复用所有校验+状态机)────
940
+ {
941
+ name: 'webaz_rfq',
942
+ description: `RFQ (Request-for-Quotation) — buyer posts demand, sellers bid within a time window.
943
+
944
+ Actions:
945
+ - create (buyer): publish RFQ; needs title/qty/max_price/category/urgency/award_mode
946
+ - mine (buyer): my RFQ list
947
+ - browse (seller): board view (filter by region/category/urgency/unbidded)
948
+ - detail: full detail (buyer sees all bids; seller sees only own)
949
+ - award (buyer): pick winner (pass bid_id for manual; omit for auto-lowest)
950
+ - cancel (buyer): cancel (only if no bid awarded; 30% deposit forfeited)
951
+
952
+ Economics:
953
+ - Buyer deposit = clamp(0.1, 1) of (max_price × qty × 0.01) — i.e. 1% capped at 1 WAZ (anti-spam, NOT anti-fraud; real anti-fraud is 30% cancel penalty)
954
+ - No-max-price RFQ deposit = 0.1 WAZ flat
955
+ - Seller bid stake = max(0.5, price × qty × 0.05) — 5% with floor 0.5 WAZ
956
+ - Cancel before award: 30% deposit forfeit to charity_fund
957
+ - Award winner → standard order lifecycle, BUT with snapshot_commission_rate = 0 (RFQ orders 0% commission — direct deal between buyer/seller, no promoter chain to compensate). Seller gets price minus protocol_fee (2%) and fund_base (1%); no L1/L2/L3 dilution.
958
+ - Seller bid stake (4.5+ WAZ) becomes bid_stake_held on the order; released to seller balance on successful complete; 50/50 split to buyer+sys_protocol on fault_seller (settleFault).
959
+ - Window defaults: now=15min / today=60min / flex=1440min(24h)
960
+
961
+ Shipping address: if omitted, falls back to webaz_default_address (set via that tool first).
962
+
963
+ ──
964
+ 中文:求购单 RFQ。买家押金 1%(封顶 1 WAZ,仅防 spam);卖家 bid stake 5%(最少 0.5 WAZ);撤单扣 30% 入慈善池。`,
965
+ inputSchema: {
966
+ type: 'object',
967
+ properties: {
968
+ api_key: { type: 'string', description: '你的 api_key' },
969
+ action: { type: 'string', enum: ['create', 'mine', 'browse', 'detail', 'award', 'cancel'] },
970
+ // create
971
+ title: { type: 'string' },
972
+ qty: { type: 'number' },
973
+ max_price: { type: 'number', description: 'WAZ 预算上限(可选;不填时 buyer 押金为 1 WAZ)' },
974
+ category: { type: 'string', enum: ['standard', 'general', 'highvalue', 'restricted'] },
975
+ urgency: { type: 'string', enum: ['now', 'today', 'flex'] },
976
+ award_mode: { type: 'string', enum: ['manual', 'first_match', 'time_window'] },
977
+ award_window_min: { type: 'number' },
978
+ notes: { type: 'string' },
979
+ shipping_address: { type: 'string', description: '可选;缺省取 buyer 默认地址' },
980
+ // browse filters
981
+ region: { type: 'string' },
982
+ unbidded: { type: 'boolean' },
983
+ // detail/award/cancel
984
+ rfq_id: { type: 'string' },
985
+ bid_id: { type: 'string', description: 'award 时可选 — 不传则自动选当前最低价' },
986
+ },
987
+ required: ['api_key', 'action'],
988
+ },
989
+ },
990
+ {
991
+ name: 'webaz_bid',
992
+ description: `Bid on RFQs (seller-side).
993
+ Actions:
994
+ - submit: new bid; needs rfq_id + price + qty_offered + fulfillment_type; optional eta_hours/note
995
+ - patch: edit price/ETA/fulfillment/note (only active; stake delta auto-settled)
996
+ - cancel: withdraw (only active; deposit released immediately)
997
+ - list_mine: full bid history
998
+
999
+ ──
1000
+ 中文:对 RFQ 报价(卖家)。Actions: submit / patch / cancel / list_mine。改价时 stake 自动结算差额。`,
1001
+ inputSchema: {
1002
+ type: 'object',
1003
+ properties: {
1004
+ api_key: { type: 'string', description: '卖家 api_key' },
1005
+ action: { type: 'string', enum: ['submit', 'patch', 'cancel', 'list_mine'] },
1006
+ rfq_id: { type: 'string' },
1007
+ bid_id: { type: 'string' },
1008
+ price: { type: 'number' },
1009
+ qty_offered: { type: 'number' },
1010
+ eta_hours: { type: 'number' },
1011
+ fulfillment_type: { type: 'string', enum: ['instant_pickup', 'same_day', 'next_day', 'standard'] },
1012
+ note: { type: 'string' },
1013
+ offer_id: { type: 'string', description: '可选;引用已有 offer 商品' },
1014
+ },
1015
+ required: ['api_key', 'action'],
1016
+ },
1017
+ },
1018
+ {
1019
+ name: 'webaz_chat',
1020
+ description: `Context-bound DM (no open DM — only order / rfq / listing_qa contexts allowed).
1021
+ Actions:
1022
+ - start: open conversation — needs kind + context_id (for rfq, also recipient_id)
1023
+ - list: my conversation list
1024
+ - read: read messages (last 50; flag_reasons is array)
1025
+ - send: send message (anti-scam regex; matches get flagged=true but still delivered; flag_reasons is array)
1026
+ - mark_read: mark as read
1027
+ - block: block this specific conversation (sets conv.status=blocked; ≠ webaz_blocklist which hides from search)
1028
+
1029
+ Anti-scam regex names (returned in flag_reasons array):
1030
+ phone_cn (CN mobile 11-digit), wechat (微信/vx/wechat/weixin), alipay (支付宝/alipay),
1031
+ qq (qq:digits), bank_card (16-19 consecutive digits), telegram (@handle/t.me/telegram),
1032
+ external_url (any http(s) URL except localhost/webaz.app/webaz.io).
1033
+
1034
+ Rate limits (PWA-enforced):
1035
+ - Short-term: 60 messages / minute / user
1036
+ - AGENT_DAILY_CAP (reset at UTC midnight): new=30, trusted=100, quality=300, legend=1000
1037
+ (returns 429 + error_code=AGENT_DAILY_CAP + cap + used + level fields)
1038
+ 3 overruns/day → auto agent warning strike.
1039
+
1040
+ Behavior note: webaz_blocklist hides target from your search; per-conversation block (this tool's "block")
1041
+ silences the specific conv. Existing conversations are NOT auto-silenced by webaz_blocklist
1042
+ (business context overrides — block prevents future ad-hoc contact but keeps active deal channels open).
1043
+
1044
+ ──
1045
+ 中文:上下文私聊 — 仅 order/rfq/listing_qa 三种场景可发起,无自由 DM。Actions: start / list / read / send / mark_read / block。
1046
+ 反诈 regex 7 类 + 60/min rate limit。blocklist 不影响已建立 conversation(商业上下文优先)。`,
1047
+ inputSchema: {
1048
+ type: 'object',
1049
+ properties: {
1050
+ api_key: { type: 'string' },
1051
+ action: { type: 'string', enum: ['start', 'list', 'read', 'send', 'mark_read', 'block'] },
1052
+ kind: { type: 'string', enum: ['order', 'rfq', 'listing_qa'] },
1053
+ context_id: { type: 'string' },
1054
+ recipient_id: { type: 'string' },
1055
+ conversation_id: { type: 'string' },
1056
+ body: { type: 'string', description: 'send action 的消息正文 (≤ 2000 字)' },
1057
+ },
1058
+ required: ['api_key', 'action'],
1059
+ },
1060
+ },
1061
+ {
1062
+ name: 'webaz_price_history',
1063
+ description: `Product historical sale price + volume distribution — helps agents avoid bottom-price dumping bait.
1064
+ Returns:
1065
+ - windows: { d30, d90, lifetime } each {sales, volume, avg, median, p25, p75}
1066
+ - price_buckets: distribution array [{price, count, qty, pct}]
1067
+ - daily_avg: 30-day daily avg price trend [{date, sales, avg}]
1068
+ - category_avg_30d: same-category 30-day average
1069
+ - anomaly_flags: ('current_below_70pct_median' / 'far_below_category_avg' / 'far_above_category_avg')
1070
+ Returns insufficient_data:true when data sparse.
1071
+
1072
+ ──
1073
+ 中文:商品历史成交价 + 量分布 — 防底价倾销。返回多时窗统计 + 价位分布 + 同类均价 + 异常预警。`,
1074
+ inputSchema: {
1075
+ type: 'object',
1076
+ properties: {
1077
+ product_id: { type: 'string', description: '商品 ID' },
1078
+ },
1079
+ required: ['product_id'],
1080
+ },
1081
+ },
1082
+ {
1083
+ name: 'webaz_charity',
1084
+ description: `Charity wish pool + repayment + community fund — double-anonymous + dual-signed anchoring + isolated prestige.
1085
+ Actions:
1086
+ - list: browse public wishes (filter by category/target_kind; anonymous accessible)
1087
+ - detail: single wish details (commit_hash + fulfillment + repayments)
1088
+ - create: (auth) publish a wish
1089
+ - claim: (auth) fulfiller claims (1:1 exclusive; 30-day self-claim lock; auto-release if no proof in 48h)
1090
+ - proof: (auth) submit completion evidence
1091
+ - confirm: (auth) wisher confirms → fulfiller +10 prestige
1092
+ - disclose: (auth) apply for public disclosure (both parties must agree)
1093
+ - cancel: (auth) wisher cancels (only open wishes)
1094
+ - me: (auth) my charity profile (prestige breakdown + pending repayment queue)
1095
+ - stories / leaderboard: public
1096
+ - repay: (auth) wisher initiates repayment (≥ 0.1 WAZ; auto-accepted if no response in 7d)
1097
+ - repay_respond:(auth) fulfiller responds (accept | decline_to_fund) — decline → funds go to community fund, wisher +8 / fulfiller +2 grace
1098
+ - donate: (auth) donate to community fund (≥ 0.1 WAZ; daily 50 WAZ matched 1:1 → donation_honor)
1099
+ - fund: fund balance + ledger + philanthropist leaderboard (public)
1100
+
1101
+ ──
1102
+ 中文:慈善许愿池 + 还愿 + 慈善基金。双匿名 + 双签锚定 + 隔离 prestige。Actions: list/detail/create/claim/proof/confirm/disclose/cancel/me/stories/leaderboard/repay/repay_respond/donate/fund。`,
1103
+ inputSchema: {
1104
+ type: 'object',
1105
+ properties: {
1106
+ api_key: { type: 'string' },
1107
+ action: { type: 'string', enum: ['list', 'detail', 'create', 'claim', 'proof', 'confirm', 'disclose', 'cancel', 'me', 'stories', 'leaderboard', 'repay', 'repay_respond', 'donate', 'fund'] },
1108
+ wish_id: { type: 'string' },
1109
+ fulfillment_id: { type: 'string' },
1110
+ repay_id: { type: 'string' },
1111
+ choice: { type: 'string', enum: ['accept', 'decline_to_fund'] },
1112
+ category: { type: 'string', enum: ['medical', 'education', 'daily', 'elderly', 'disaster', 'tech', 'other'] },
1113
+ target_kind: { type: 'string', enum: ['item', 'service', 'cash'] },
1114
+ target_waz: { type: 'number', description: 'cash 模式必填,≤ 500' },
1115
+ escrow_self: { type: 'number', description: '1=自我托管锁仓全额,0=纯协调' },
1116
+ title: { type: 'string' }, content: { type: 'string' },
1117
+ window_hours: { type: 'number', description: '24-720 小时' },
1118
+ allow_public: { type: 'number' },
1119
+ proof_hash: { type: 'string', description: 'sha256 hex of proof_text' },
1120
+ proof_note: { type: 'string' },
1121
+ amount: { type: 'number', description: 'repay / donate 金额(WAZ)' },
1122
+ note: { type: 'string' },
1123
+ limit: { type: 'number' },
1124
+ },
1125
+ required: ['action'],
1126
+ },
1127
+ },
1128
+ {
1129
+ name: 'webaz_p2p_product',
1130
+ description: `P2P native store — product detail lives on seller's node; WebAZ only anchors hash + key fields.
1131
+ Actions:
1132
+ - create (seller): publish anchor — needs title/price/stock + content_hash (sha256) + content_signature (HMAC-SHA256(api_key, hash|signed_at))
1133
+ - list: browse public P2P products (incl. hash + peer_endpoint)
1134
+ - detail: single product (incl. hash + endpoint; agent should self-fetch peer_endpoint and verify sha256)
1135
+ - patch (seller): edit (price/stock/title direct; detail JSON change → must re-sign hash + signature)
1136
+
1137
+ Verification flow (agent implements):
1138
+ 1. GET peer_endpoint/<product_id> → raw JSON
1139
+ 2. canonicalize (sort keys, drop nulls) → JSON.stringify
1140
+ 3. sha256(canonical) === product.content_hash ? accept : reject trade
1141
+
1142
+ ──
1143
+ 中文:P2P 原生商店 — 商品详情存卖家节点,WebAZ 只锚定 hash + 关键字段。Actions: create / list / detail / patch。Agent 须自行 GET peer_endpoint 校验 sha256。`,
1144
+ inputSchema: {
1145
+ type: 'object',
1146
+ properties: {
1147
+ api_key: { type: 'string' },
1148
+ action: { type: 'string', enum: ['create', 'list', 'detail', 'patch'] },
1149
+ product_id: { type: 'string' },
1150
+ title: { type: 'string' }, price: { type: 'number' }, stock: { type: 'number' },
1151
+ content_hash: { type: 'string', description: 'sha256 hex' },
1152
+ content_signature: { type: 'string', description: 'HMAC-SHA256(api_key, hash|signed_at)' },
1153
+ content_signed_at: { type: 'string', description: '"YYYY-MM-DD HH:MM:SS"' },
1154
+ peer_endpoint: { type: 'string' },
1155
+ thumbnail_uri: { type: 'string', description: 'data:image/* base64, ≤16KB' },
1156
+ category: { type: 'string' }, region: { type: 'string' },
1157
+ },
1158
+ required: ['action'],
1159
+ },
1160
+ },
1161
+ {
1162
+ name: 'webaz_like',
1163
+ description: `Like a shareable (others' shared content) to boost product ranking.
1164
+ Actions:
1165
+ - toggle: like or unlike (same endpoint; second call auto-unlikes)
1166
+ - status: my like status on a shareable + total count
1167
+ Threshold: must have ≥1 completed order (anti-Sybil). One vote per person, can't like own.
1168
+
1169
+ ──
1170
+ 中文:对 shareable 点赞 — 提升商品 ranking。Actions: toggle / status。门槛:≥1 完成订单,不能给自己点。`,
1171
+ inputSchema: {
1172
+ type: 'object',
1173
+ properties: {
1174
+ api_key: { type: 'string' },
1175
+ action: { type: 'string', enum: ['toggle', 'status'] },
1176
+ shareable_id: { type: 'string' },
1177
+ },
1178
+ required: ['api_key', 'action', 'shareable_id'],
1179
+ },
1180
+ },
1181
+ {
1182
+ name: 'webaz_leaderboard',
1183
+ description: `WebAZ leaderboards — no centralized traffic distribution, pure real-signal ranking. Privacy-first: GMV / revenue amounts are NEVER exposed.
1184
+ Kinds:
1185
+ - products trending products (rank_score = sales×0.5 + referrals×2.0 + likes×1.0)
1186
+ - value_products value-certified (💎 cheapest 20% per category · daily batch)
1187
+ - creators creator board (by total likes received)
1188
+ - sellers seller board (by rating × log(reviews+1); GMV hidden)
1189
+ - buyers buyer activity (by completed order count; GMV hidden)
1190
+ - verifiers verifier board (correct count / accuracy)
1191
+ - arbitrators arbitrator reputation (fairness_score)
1192
+ - agents agent eval (trust_score + 30d call count)
1193
+
1194
+ ──
1195
+ 中文:排行榜(无中心流量分发,纯真实信号)。8 类:products / value_products / creators / sellers / buyers / verifiers / arbitrators / agents。隐私第一不露 GMV。`,
1196
+ inputSchema: {
1197
+ type: 'object',
1198
+ properties: {
1199
+ kind: {
1200
+ type: 'string',
1201
+ enum: ['products', 'value_products', 'creators', 'sellers', 'buyers', 'verifiers', 'arbitrators', 'agents'],
1202
+ description: '榜单类型',
1203
+ },
1204
+ limit: { type: 'number', description: '默认 20,最多 50' },
1205
+ },
1206
+ required: ['kind'],
1207
+ },
1208
+ },
1209
+ {
1210
+ name: 'webaz_auction',
1211
+ description: `English forward auction — seller posts → buyers raise bids → anti-sniping extension → highest bid wins.
1212
+ Actions:
1213
+ - create (seller): start auction; needs title/qty/category/starting_price, optional min_increment/reserve_price/window_min/sniper_extend_min
1214
+ - browse: public board
1215
+ - mine: my created (seller) + my participated (buyer)
1216
+ - detail: full detail incl. bid history (buyer_id redacted for non-seller / non-bidder viewers)
1217
+ - bid (buyer): place bid; needs auction_id + price (first ≥ starting; subsequent ≥ current + increment)
1218
+ - cancel (seller): cancel (only before any bid placed)
1219
+
1220
+ ──
1221
+ 中文:加价拍卖(English forward)— 反狙击延时 + 最高价中标。Actions: create / browse / mine / detail / bid / cancel。仅未出价时可取消。`,
1222
+ inputSchema: {
1223
+ type: 'object',
1224
+ properties: {
1225
+ api_key: { type: 'string' },
1226
+ action: { type: 'string', enum: ['create', 'browse', 'mine', 'detail', 'bid', 'cancel'] },
1227
+ title: { type: 'string' },
1228
+ qty: { type: 'number' },
1229
+ category: { type: 'string', enum: ['standard', 'general', 'highvalue', 'restricted'] },
1230
+ starting_price: { type: 'number' },
1231
+ min_increment: { type: 'number' },
1232
+ reserve_price: { type: 'number' },
1233
+ window_min: { type: 'number' },
1234
+ sniper_extend_min: { type: 'number' },
1235
+ notes: { type: 'string' },
1236
+ auction_id: { type: 'string' },
1237
+ price: { type: 'number' },
1238
+ },
1239
+ required: ['api_key', 'action'],
1240
+ },
1241
+ },
1242
+ {
1243
+ name: 'webaz_auto_bid',
1244
+ description: `Seller auto_bid Skill config — auto-quote on RFQ creation instantly.
1245
+ Actions:
1246
+ - get: read current Skill config
1247
+ - set: create/update Skill (needs categories[] / regions[] / max_eta_h / bid_strategy etc.)
1248
+ - disable: one-click disable (keeps config)
1249
+
1250
+ ──
1251
+ 中文:卖家 auto_bid Skill — RFQ 创建瞬间自动报价。Actions: get / set / disable。`,
1252
+ inputSchema: {
1253
+ type: 'object',
1254
+ properties: {
1255
+ api_key: { type: 'string' },
1256
+ action: { type: 'string', enum: ['get', 'set', 'disable'] },
1257
+ categories: { type: 'array', items: { type: 'string' } },
1258
+ regions: { type: 'array', items: { type: 'string' } },
1259
+ max_eta_h: { type: 'number' },
1260
+ fulfillment_type: { type: 'string', enum: ['instant_pickup', 'same_day', 'next_day', 'standard'] },
1261
+ bid_strategy: { type: 'string', enum: ['cheapest_undercut', 'match_budget'] },
1262
+ undercut_pct: { type: 'number', description: '0–0.5;折价幅度' },
1263
+ max_price_cap: { type: 'number' },
1264
+ daily_cap: { type: 'number' },
1265
+ cooldown_min: { type: 'number' },
1266
+ enabled: { type: 'boolean' },
1267
+ },
1268
+ required: ['api_key', 'action'],
1269
+ },
1270
+ },
1271
+ {
1272
+ name: 'webaz_skill_market',
1273
+ description: `Knowledge-skill marketplace — anyone publishes reusable content skills (templates / prompts / guides / checklists); others pay to unlock. DISTINCT from webaz_skill (that one is seller behaviour-automation plugins).
1274
+
1275
+ Lifecycle: publish → WebAZ content review (human admin, not via MCP) → listed → buyers unlock.
1276
+ Billing modes: free / one_time (buy once, permanent) / per_use (charged each read).
1277
+ Revenue is an independent flow (author net → wallet, 5% protocol fee → sys_protocol); it does NOT enter PV / referral commission engines.
1278
+
1279
+ Actions:
1280
+ - list browse listed skills (filters: kind / billing / query; no auth needed)
1281
+ - detail one skill's public detail (skill_id; content is NOT included)
1282
+ - publish publish a new skill (title + content + billing_mode required) → enters review queue
1283
+ - update edit own skill (editing an approved one re-enters review)
1284
+ - delist take own skill offline
1285
+ - resubmit resubmit a rejected/delisted skill for review
1286
+ - purchase unlock free / one_time skill (cannot buy your own)
1287
+ - read read the unlocked content; per_use charges price each call
1288
+ - my_skills my published skills (all statuses)
1289
+ - library my unlocked / used skills
1290
+
1291
+ ──
1292
+ 中文:知识技能市场 — 人人发布内容型技能(模板/提示词/指南/清单),他人付费解锁。与 webaz_skill(卖家自动化插件)是两套。计费 free/one_time/per_use;收入独立流转,不进 PV/佣金。审计是人工 admin 节点,不经 MCP。Actions: list / detail / publish / update / delist / resubmit / purchase / read / my_skills / library。`,
1293
+ inputSchema: {
1294
+ type: 'object',
1295
+ properties: {
1296
+ api_key: { type: 'string', description: '你的 api_key(list / detail 时可省略)' },
1297
+ action: {
1298
+ type: 'string',
1299
+ enum: ['list', 'detail', 'publish', 'update', 'delist', 'resubmit', 'purchase', 'read', 'my_skills', 'library'],
1300
+ description: '要执行的操作',
1301
+ },
1302
+ skill_id: { type: 'string', description: '技能 ID(detail/update/delist/resubmit/purchase/read 时必填)' },
1303
+ // publish / update
1304
+ title: { type: 'string', description: '标题(publish 必填)' },
1305
+ content: { type: 'string', description: '技能正文,购买后才可见(publish 必填)' },
1306
+ summary: { type: 'string', description: '一句话简介(可选)' },
1307
+ preview: { type: 'string', description: '公开试读,未购可见(可选)' },
1308
+ skill_kind: { type: 'string', enum: ['template', 'prompt', 'guide', 'checklist'], description: '技能类型(默认 template)' },
1309
+ billing_mode: { type: 'string', enum: ['free', 'one_time', 'per_use'], description: '计费模式(publish 必填)' },
1310
+ price: { type: 'number', description: 'WAZ 价格;free 必须为 0,付费必须 >0,上限 100000' },
1311
+ category: { type: 'string', description: '分类(可选)' },
1312
+ // list filters
1313
+ kind: { type: 'string', enum: ['template', 'prompt', 'guide', 'checklist'], description: '过滤类型(list 时可选)' },
1314
+ billing: { type: 'string', enum: ['free', 'one_time', 'per_use'], description: '过滤计费模式(list 时可选)' },
1315
+ query: { type: 'string', description: '关键词搜索(list 时可选)' },
1316
+ },
1317
+ required: ['action'],
1318
+ },
1319
+ },
1320
+ {
1321
+ name: 'webaz_secondhand',
1322
+ description: `Secondhand market (个人闲置二手) — peer-to-peer pre-owned goods, 1% protocol fee, escrow-protected. Supports shipping and in-person handoff.
1323
+
1324
+ Actions:
1325
+ - browse list available items (filters: category / condition / region / min_price / max_price / query / sort; no auth needed; excludes your own when api_key given)
1326
+ - detail one item's detail + seller's other listings (item_id; no auth needed)
1327
+ - publish list an item (title + category + condition + price + images[] required; ≥1 image)
1328
+ - update edit own item (item_id + fields; can set status available/reserved/closed)
1329
+ - mine my listings + stats (available/sold/earned)
1330
+ - buy place an order on an item (item_id + fulfillment_mode; shipping needs shipping_address)
1331
+
1332
+ category: phone / computer / appliance / furniture / clothing / book / toy / sports / other
1333
+ condition: brand_new / like_new / lightly_used / well_used / heavily_used
1334
+ fulfillment: shipping / in_person / both
1335
+
1336
+ ──
1337
+ 中文:二手集市 — 个人闲置 P2P,协议费 1%,escrow 保护,支持快递/面交。Actions: browse / detail / publish / update / mine / buy。`,
1338
+ inputSchema: {
1339
+ type: 'object',
1340
+ properties: {
1341
+ api_key: { type: 'string', description: '你的 api_key(browse / detail 时可省略)' },
1342
+ action: { type: 'string', enum: ['browse', 'detail', 'publish', 'update', 'mine', 'buy'], description: '要执行的操作' },
1343
+ item_id: { type: 'string', description: '物品 ID(detail/update/buy 时必填)' },
1344
+ // publish / update
1345
+ title: { type: 'string', description: '标题 2-60 字(publish 必填)' },
1346
+ description: { type: 'string', description: '详细描述(≤1000 字,可选)' },
1347
+ category: { type: 'string', enum: ['phone', 'computer', 'appliance', 'furniture', 'clothing', 'book', 'toy', 'sports', 'other'], description: '类目(publish 必填)' },
1348
+ condition_grade: { type: 'string', enum: ['brand_new', 'like_new', 'lightly_used', 'well_used', 'heavily_used'], description: '成色(publish 必填)' },
1349
+ price: { type: 'number', description: 'WAZ 价格 0-100000(publish 必填)' },
1350
+ negotiable: { type: 'boolean', description: '是否可议价(可选)' },
1351
+ images: { type: 'array', items: { type: 'string' }, description: '图片 dataURL/URL 数组,≥1 张,最多 9 张(publish 必填)' },
1352
+ region: { type: 'string', description: '地点(≤40 字,可选)' },
1353
+ fulfillment: { type: 'string', enum: ['shipping', 'in_person', 'both'], description: '履约方式(默认 both)' },
1354
+ status: { type: 'string', enum: ['available', 'reserved', 'closed'], description: '改状态(update 时可选)' },
1355
+ // buy
1356
+ fulfillment_mode: { type: 'string', enum: ['shipping', 'in_person'], description: '本单履约方式(buy 必填,默认 shipping)' },
1357
+ shipping_address: { type: 'string', description: '收货地址(buy 且 shipping 时必填)' },
1358
+ notes: { type: 'string', description: '订单备注(buy 可选,可写还价)' },
1359
+ // browse filters
1360
+ condition: { type: 'string', description: '过滤成色,逗号分隔多选(browse 可选)' },
1361
+ min_price: { type: 'number' },
1362
+ max_price: { type: 'number' },
1363
+ query: { type: 'string', description: '关键词(browse 可选)' },
1364
+ sort: { type: 'string', enum: ['newest', 'price_asc', 'price_desc', 'popular'], description: '排序(browse 可选)' },
1365
+ },
1366
+ required: ['action'],
1367
+ },
1368
+ },
1369
+ {
1370
+ name: 'webaz_trial',
1371
+ description: `Trial-for-review (测评免单) — sellers offer order refunds to buyers who post a qualifying review note; when the review reaches a target view threshold the order is auto-refunded.
1372
+
1373
+ Anti-abuse is enforced server-side (buyer≠seller, must have a confirmed/completed order, account ≥3 days old, IP/UA rate limits, config snapshot at claim time) — MCP just passes through.
1374
+
1375
+ Buyer actions:
1376
+ - get_campaign read a product's active trial campaign (product_id; no auth)
1377
+ - apply claim a trial slot for a product you've bought (product_id)
1378
+ - link_note attach your review note to a claim (claim_id + note_id)
1379
+ - my_claims my trial claims + statuses
1380
+ Seller actions:
1381
+ - create_campaign open/update a campaign on your product (product_id + quota_total + reach_threshold + min_chars + min_days_live)
1382
+ - cancel_campaign close your product's campaign (product_id)
1383
+ - my_campaigns my seller-side campaigns
1384
+ - campaign_claims claims under a campaign (campaign_id)
1385
+
1386
+ ──
1387
+ 中文:测评免单 — 卖家给"发合格测评笔记且达到浏览阈值"的买家退款。反作弊后端强制(买≠卖/须完成订单/账号≥3天/IP频控/配置快照)。买家: get_campaign / apply / link_note / my_claims;卖家: create_campaign / cancel_campaign / my_campaigns / campaign_claims。`,
1388
+ inputSchema: {
1389
+ type: 'object',
1390
+ properties: {
1391
+ api_key: { type: 'string', description: '你的 api_key(get_campaign 可省略)' },
1392
+ action: { type: 'string', enum: ['get_campaign', 'apply', 'link_note', 'my_claims', 'create_campaign', 'cancel_campaign', 'my_campaigns', 'campaign_claims'], description: '要执行的操作' },
1393
+ product_id: { type: 'string', description: '商品 ID(get_campaign/apply/create_campaign/cancel_campaign 时必填)' },
1394
+ claim_id: { type: 'string', description: '申请 ID(link_note 时必填)' },
1395
+ campaign_id: { type: 'string', description: '活动 ID(campaign_claims 时必填)' },
1396
+ note_id: { type: 'string', description: '测评笔记 ID(link_note 时必填;须 type=note + 绑定该商品 + active)' },
1397
+ // create_campaign config
1398
+ quota_total: { type: 'number', description: '名额数 1-200(create_campaign 必填)' },
1399
+ reach_threshold: { type: 'number', description: '浏览阈值 10-10000(默认 50)' },
1400
+ min_chars: { type: 'number', description: '笔记最少字数 20-5000(默认 50)' },
1401
+ min_days_live: { type: 'number', description: '笔记最少存活天数 1-90(默认 7)' },
1402
+ },
1403
+ required: ['action'],
1404
+ },
1405
+ },
1406
+ ];
1407
+ // ─── 工具处理函数 ─────────────────────────────────────────────
1408
+ function handleInfo() {
1409
+ const summary = getManifestSummary();
1410
+ // QA 轮 3 抓到:live_stats 不是 hardcoded、不是 remote — 就是本地 SQLite count。这里加 source 字段澄清。
1411
+ const stats = (() => {
1412
+ try {
1413
+ const users = db.prepare("SELECT COUNT(*) as n FROM users WHERE id != 'sys_protocol'").get().n;
1414
+ const products = db.prepare("SELECT COUNT(*) as n FROM products WHERE status='active'").get().n;
1415
+ const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE status='completed'").get().n;
1416
+ return {
1417
+ participants: users,
1418
+ active_products: products,
1419
+ completed_orders: completed,
1420
+ source: 'local SQLite (~/.webaz/webaz.db) — counts reflect *this* MCP server\'s DB, not protocol-wide prod state',
1421
+ };
1422
+ }
1423
+ catch {
1424
+ return null;
1425
+ }
1426
+ })();
1427
+ // QA 轮 3 FAIL:webaz_info 没暴露经济参数。从 protocol_params 表读 fee 类参数 + charity_fund 当前余额。
1428
+ const economics = (() => {
1429
+ try {
1430
+ const feeParams = db.prepare("SELECT key, value, description FROM protocol_params WHERE category='fee' ORDER BY key").all();
1431
+ const fund = db.prepare("SELECT balance, total_donated, total_disbursed, total_redirected FROM charity_fund WHERE id='main'").get();
1432
+ return {
1433
+ protocol_params: feeParams.map(p => ({ key: p.key, value: Number(p.value), note: p.description })),
1434
+ charity_fund: fund || { balance: 0, total_donated: 0, total_disbursed: 0, total_redirected: 0 },
1435
+ note: 'Numeric values reflect current DB state, queried fresh on each call.',
1436
+ };
1437
+ }
1438
+ catch {
1439
+ return null;
1440
+ }
1441
+ })();
1442
+ return {
1443
+ ...summary,
1444
+ // 诚实化披露 — 排在 stats / quick_start 之前,让 agent 先建立正确预期。
1445
+ // 第三方尽调 agent 抓到过的破绽:live_stats 是本地库数字、引导词推 agent 拉新、佣金结构有 MLM 形态。
1446
+ // 此处主动披露,不抗辩;agent 自己判断如何处理。
1447
+ network_state: {
1448
+ phase: 'pre_launch',
1449
+ real_users_on_canonical: 0,
1450
+ canonical_endpoint: 'https://webaz.xyz',
1451
+ economic_flow: 'simulated WAZ (test currency, 1 WAZ ≈ 1 USDC peg is a模拟基准, not a real exchange rate). No fiat/crypto settles yet.',
1452
+ disclaimer: '本协议尚未公开上线,prod 真实用户≈0,无真实经济流转。下方 live_stats / 工具示例所有计数均来自**本机 MCP 服务器的本地 SQLite**(~/.webaz/webaz.db),仅供 dev / demo,不代表协议全网真实状态。请勿据此评估市场规模、做投资决策、或替终端用户承诺任何经济关系。',
1453
+ },
1454
+ description: 'WebAZ 是一个去中心化商业协议。每笔交易通过状态机流转,每个状态转移都需要对应责任方的操作证明。任何超时未操作,协议自动判定该方违约并执行处置。',
1455
+ live_stats: stats,
1456
+ economics,
1457
+ // QA 轮 14.c P1:显式归因机制(与传统 MLM 自动吃下线的根本区别)
1458
+ commission_model: {
1459
+ split: '7:2:1 — L1 70% / L2 20% / L3 10% of commission_pool',
1460
+ attribution: 'EXPLICIT per-order — commission goes to the promoter attributed at purchase time, NOT auto-derived from the buyer\'s sponsor chain. 防 MLM 寡头化(上线不会自动吃下线每一单)。',
1461
+ how_to_attribute: 'L1: webaz_place_order(promoter_api_key) records direct promoter. Full L2/L3 chain requires buyer to arrive via webaz_share_link ?ref= URL clicked in browser (builds product_share_attribution).',
1462
+ redirect_rules: 'chain_gap (无 L / sponsor 无效) → charity_fund; region cap (level > region max_levels) → global_fund. global region max_levels=1 (仅 L1);singapore 等 max_levels=3.',
1463
+ l1_gate: 'promoter 必须是 verified buyer(≥1 笔完成订单)才能领 commission,否则该份 redirect。',
1464
+ // 主动披露,不要让外部尽调 agent 自己读出来再写差评
1465
+ compliance_notice: '⚠️ 本协议含三级佣金(L1/L2/L3 7:2:1) + 二元配对树(binary PV matching) + 邀请链结构。这类形态在多数司法辖区可能与**多层级营销(MLM)的法律定义重叠**。本协议通过显式 per-order 归因(非自动吃下线)规避了 MLM 寡头化的核心病灶,但形态层面仍带 MLM 特征,我们诚实告知。**代理 AI 不应默认替终端用户生成推广链接或拉新**——这需要用户的显式授权,且用户自身要先评估当地合规性。',
1466
+ },
1467
+ // QA 轮 3 FAIL:roles 漏 reviewer。register 工具支持 5 个角色,info 必须列全。
1468
+ roles: {
1469
+ buyer: '下单、付款、确认收货或发起争议',
1470
+ seller: '上架商品、接单、按时发货(质押保证金确保履约)',
1471
+ logistics: '揽收包裹、更新运输状态、确认投递(获得 5% 物流费)',
1472
+ reviewer: '结构化商品测评(trial campaign 申请试用 + 真实购买后写评)',
1473
+ arbitrator: '处理争议,做出裁定(120h 内必须裁定,否则系统自动退款买家)',
1474
+ },
1475
+ quick_start: {
1476
+ seller: '1. webaz_register(role=seller) → 2. webaz_list_product() → 3. 等通知 webaz_update_order(accept/ship)',
1477
+ buyer: '1. webaz_register(role=buyer) → 2. webaz_search() → 3. webaz_verify_price() → 4. webaz_place_order(session_token) → 5. webaz_update_order(confirm)',
1478
+ agent_buying: '用户提供链接 → 1. webaz_search(query) 找到更优方案 → 2. webaz_verify_price(product_id) 锁定价格 → 3. webaz_place_order(session_token) 下单 → 返回成交理由',
1479
+ logistics: '1. webaz_register(role=logistics) → 2. webaz_update_order(pickup) → webaz_update_order(deliver)',
1480
+ reviewer: '1. webaz_register(role=reviewer) → 2. webaz_claim_verify(action=apply) 申请测评免单 → 3. 收货后写真实评价',
1481
+ arbitrator: '1. webaz_register(role=arbitrator) → 2. webaz_dispute(action=list_open) 看待裁案件 → 3. webaz_dispute(action=arbitrate) — ⚠️ 需 PWA + Passkey(Iron-Rule)',
1482
+ },
1483
+ available_tools: TOOLS.map((t) => ({ name: t.name, description: t.description.split('\n')[0] })),
1484
+ full_manifest: `读取 MCP Resource "${MANIFEST_URI}" 获取完整协议规范(状态机/经济模型/争议系统/Skill 市场/声誉系统)`,
1485
+ };
1486
+ }
1487
+ function handleRegister(args) {
1488
+ const name = args.name;
1489
+ const role = args.role;
1490
+ const initialBalance = args.initial_balance ?? 1000;
1491
+ // QA 轮 14.a P1:MCP register 之前不接受 region → 默认 global(PWA register 强制选)
1492
+ // 协议级 by-design:MCP register 不设 placement_id(防 bot 刷链)
1493
+ // 但 region 是 sales / commission 字段,应允许显式传入
1494
+ const VALID_REGIONS = new Set(['global', 'singapore', 'china', 'usa', 'malaysia', 'indonesia', 'thailand', 'vietnam', 'taiwan', 'hk']);
1495
+ const regionInput = String(args.region || 'global').toLowerCase();
1496
+ const region = VALID_REGIONS.has(regionInput) ? regionInput : 'global';
1497
+ const validRoles = ['buyer', 'seller', 'logistics', 'reviewer', 'arbitrator'];
1498
+ if (!validRoles.includes(role)) {
1499
+ return { error: `无效角色:${role}。可选:${validRoles.join(', ')}` };
1500
+ }
1501
+ const id = generateId('usr');
1502
+ const apiKey = generateId('key');
1503
+ const permaCode = mcpGeneratePermanentCode();
1504
+ const { handle, requested: handleRequested, modified: handleModified } = mcpDeriveHandle(name);
1505
+ const createdAt = new Date().toISOString();
1506
+ db.prepare('INSERT INTO users (id, name, role, roles, api_key, permanent_code, handle, region, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)').run(id, name, role, JSON.stringify([role]), apiKey, permaCode, handle, region, createdAt);
1507
+ db.prepare('INSERT INTO wallets (user_id, balance) VALUES (?, ?)').run(id, initialBalance);
1508
+ const baseNext = role === 'seller' ? '现在可以用 webaz_list_product 上架你的第一件商品'
1509
+ : role === 'buyer' ? '现在可以用 webaz_search 搜索商品'
1510
+ : '等待订单分配给你';
1511
+ return {
1512
+ success: true,
1513
+ message: '注册成功!妥善保管 api_key(身份凭证)+ permanent_code(恢复码,丢失 api_key 用它配 handle 找回)',
1514
+ user_id: id,
1515
+ api_key: apiKey,
1516
+ permanent_code: permaCode,
1517
+ permanent_code_purpose: 'Recovery code. Use webaz_mykey with handle + permanent_code to recover lost api_key. Save it somewhere safe — without it, lost api_key is lost forever.',
1518
+ handle,
1519
+ handle_requested: handleRequested,
1520
+ handle_modified: handleModified,
1521
+ ...(handleModified && {
1522
+ handle_modification_note: `Requested handle "${handleRequested}" already taken; auto-appended numeric suffix for uniqueness. Tell users your actual handle is "${handle}".`,
1523
+ }),
1524
+ name,
1525
+ role,
1526
+ initial_balance: initialBalance,
1527
+ created_at: createdAt,
1528
+ next_step: handleModified
1529
+ ? `${baseNext}。⚠️ 你请求的 handle "${handleRequested}" 已被占用,实际分配 "${handle}" — 分享/被找时用后者。`
1530
+ : baseNext,
1531
+ };
1532
+ }
1533
+ function buildAgentSummary(p) {
1534
+ const parts = [];
1535
+ if (p.brand)
1536
+ parts.push(String(p.brand));
1537
+ if (p.model)
1538
+ parts.push(String(p.model));
1539
+ const returnDays = p.return_days != null ? Number(p.return_days) : null;
1540
+ if (returnDays != null && returnDays > 0)
1541
+ parts.push(`${returnDays}天退货`);
1542
+ else if (returnDays === 0)
1543
+ parts.push('不支持退货');
1544
+ const warranty = p.warranty_days != null ? Number(p.warranty_days) : null;
1545
+ if (warranty && warranty > 0)
1546
+ parts.push(`${warranty}天质保`);
1547
+ const handling = p.handling_hours != null ? Number(p.handling_hours) : null;
1548
+ if (handling != null)
1549
+ parts.push(`${handling}h发货`);
1550
+ if (p.fragile)
1551
+ parts.push('易碎品');
1552
+ return parts.join(',') || '暂无物流信息';
1553
+ }
1554
+ function parseProductForAgent(p) {
1555
+ let specs = null;
1556
+ if (p.specs) {
1557
+ try {
1558
+ specs = JSON.parse(p.specs);
1559
+ }
1560
+ catch { }
1561
+ }
1562
+ let estimated_days = null;
1563
+ if (p.estimated_days) {
1564
+ try {
1565
+ estimated_days = JSON.parse(p.estimated_days);
1566
+ }
1567
+ catch {
1568
+ estimated_days = null;
1569
+ }
1570
+ }
1571
+ return { ...p, specs, estimated_days, agent_summary: buildAgentSummary(p) };
1572
+ }
1573
+ async function handleSearch(args) {
1574
+ // 外链/粘贴文本模式 → relay 到 webaz.xyz/api/search-by-link(生产数据有索引)
1575
+ if (args.paste_text || args.external_link) {
1576
+ const apiUrl = process.env.WEBAZ_API_URL ?? 'https://webaz.xyz';
1577
+ const body = {};
1578
+ if (args.paste_text)
1579
+ body.text = args.paste_text;
1580
+ if (args.external_link)
1581
+ body.external_link = args.external_link;
1582
+ try {
1583
+ const resp = await fetch(`${apiUrl}/api/search-by-link`, {
1584
+ method: 'POST',
1585
+ headers: { 'content-type': 'application/json' },
1586
+ body: JSON.stringify(body),
1587
+ signal: AbortSignal.timeout(5000),
1588
+ });
1589
+ if (!resp.ok)
1590
+ return { error: `链接搜索失败:HTTP ${resp.status}` };
1591
+ const data = (await resp.json());
1592
+ if (data.error)
1593
+ return data;
1594
+ return {
1595
+ ...data,
1596
+ hint: data.products?.length
1597
+ ? `通过 ${data.matched_by} 匹配到 ${data.products.length} 件商品。下单前用 webaz_verify_price 锁价。`
1598
+ : '未找到关联商品。可改用 query 参数做关键词搜索。',
1599
+ };
1600
+ }
1601
+ catch (e) {
1602
+ return { error: `链接搜索网络错误:${e.message}` };
1603
+ }
1604
+ }
1605
+ const query = args.query ?? '';
1606
+ const category = args.category;
1607
+ const maxPrice = args.max_price;
1608
+ const minReturnDays = args.min_return_days;
1609
+ const maxHandlingHours = args.max_handling_hours;
1610
+ const hasSales = args.has_sales;
1611
+ const sellerId = args.seller_id;
1612
+ let limit = Number(args.limit ?? 10);
1613
+ if (!Number.isFinite(limit) || limit < 1)
1614
+ limit = 10;
1615
+ if (limit > 200)
1616
+ limit = 200;
1617
+ const sortMode = args.sort ?? 'trending';
1618
+ let sql = `
1619
+ SELECT p.*, u.name as seller_name,
1620
+ COALESCE((SELECT total_points FROM reputation_scores WHERE user_id = p.seller_id), 0) as rep_points,
1621
+ COALESCE((SELECT level FROM reputation_scores WHERE user_id = p.seller_id), 'new') as rep_level
1622
+ FROM products p
1623
+ JOIN users u ON p.seller_id = u.id
1624
+ WHERE p.status = 'active' AND p.stock > 0
1625
+ `;
1626
+ const params = [];
1627
+ // M7.2 协议级精准匹配(与 PWA server 一致的 alias 引擎):
1628
+ // query 命中条件 = (1) 完全等于 product.title OR
1629
+ // (2) 完全等于 任一 external_title OR
1630
+ // (3) 用户文本 包含 任一卖家声明的 alias_value (≥ 6 字符 且 active)
1631
+ if (query) {
1632
+ const aliasRows = db.prepare(`
1633
+ SELECT product_id, alias_value FROM product_aliases
1634
+ WHERE status = 'active' AND length(alias_value) >= 6 AND length(alias_value) <= ?
1635
+ `).all(query.length);
1636
+ const aliasIds = new Set();
1637
+ for (const a of aliasRows) {
1638
+ if (query.includes(a.alias_value))
1639
+ aliasIds.add(a.product_id);
1640
+ }
1641
+ if (aliasIds.size === 0) {
1642
+ sql += ` AND (
1643
+ p.title = ?
1644
+ OR EXISTS (SELECT 1 FROM product_external_links pel WHERE pel.product_id = p.id AND pel.external_title = ?)
1645
+ )`;
1646
+ params.push(query, query);
1647
+ }
1648
+ else {
1649
+ sql += ` AND (
1650
+ p.id IN (${[...aliasIds].map(() => '?').join(',')})
1651
+ OR p.title = ?
1652
+ OR EXISTS (SELECT 1 FROM product_external_links pel WHERE pel.product_id = p.id AND pel.external_title = ?)
1653
+ )`;
1654
+ params.push(...aliasIds, query, query);
1655
+ }
1656
+ }
1657
+ if (category) {
1658
+ sql += ` AND p.category = ?`;
1659
+ params.push(category);
1660
+ }
1661
+ if (maxPrice !== undefined) {
1662
+ sql += ` AND p.price <= ?`;
422
1663
  params.push(maxPrice);
423
1664
  }
424
- sql += ` ORDER BY p.created_at DESC LIMIT ?`;
425
- params.push(limit);
426
- const products = db.prepare(sql).all(...params);
427
- if (products.length === 0) {
428
- return { found: 0, message: '没有找到匹配的商品', products: [] };
1665
+ if (minReturnDays !== undefined) {
1666
+ sql += ` AND p.return_days >= ?`;
1667
+ params.push(minReturnDays);
1668
+ }
1669
+ if (maxHandlingHours !== undefined) {
1670
+ sql += ` AND p.handling_hours <= ?`;
1671
+ params.push(maxHandlingHours);
1672
+ }
1673
+ if (sellerId) {
1674
+ sql += ` AND p.seller_id = ?`;
1675
+ params.push(sellerId);
1676
+ }
1677
+ if (hasSales === 'true') {
1678
+ sql += ` AND EXISTS (SELECT 1 FROM orders WHERE product_id = p.id AND status = 'completed')`;
1679
+ }
1680
+ else if (hasSales === 'false') {
1681
+ sql += ` AND NOT EXISTS (SELECT 1 FROM orders WHERE product_id = p.id AND status = 'completed')`;
1682
+ }
1683
+ if (sortMode === 'newest')
1684
+ sql += ` ORDER BY p.created_at DESC`;
1685
+ else if (sortMode === 'rating')
1686
+ sql += ` ORDER BY rep_points DESC, p.created_at DESC`;
1687
+ else if (sortMode === 'price_asc')
1688
+ sql += ` ORDER BY p.price ASC`;
1689
+ else if (sortMode === 'price_desc')
1690
+ sql += ` ORDER BY p.price DESC`;
1691
+ else if (sortMode === 'random')
1692
+ sql += ` ORDER BY RANDOM()`;
1693
+ // 'trending' 默认:先取候选,下面 JS 再算分排序(兼容旧 db 没有 metric 列的场景)
1694
+ else
1695
+ sql += ` ORDER BY p.created_at DESC`;
1696
+ sql += ` LIMIT ?`;
1697
+ params.push(Math.min(limit * 3, 500)); // 取更多候选,便于 trending 排序
1698
+ const products = db.prepare(sql).all(...params);
1699
+ if (products.length === 0) {
1700
+ return { found: 0, message: '没有找到匹配的商品', products: [] };
1701
+ }
1702
+ const enriched = products.map((p) => {
1703
+ const boost = getSearchBoost(db, p.seller_id);
1704
+ const rep_level = p.rep_level || 'new';
1705
+ const rep_points = Number(p.rep_points) || 0;
1706
+ const completion = Number(p.completion_count) || 0;
1707
+ const dispute = Number(p.dispute_loss_count) || 0;
1708
+ const sharer = Number(p.unique_sharer_count) || 0;
1709
+ // 与 PWA 端 TRENDING_SCORE_EXPR 阶梯曲线保持一致
1710
+ const lastSold = p.last_sold_at;
1711
+ let freshness = 0;
1712
+ if (lastSold) {
1713
+ const days = (Date.now() - new Date(lastSold.replace(' ', 'T') + 'Z').getTime()) / 86400_000;
1714
+ if (days < 30)
1715
+ freshness = 10;
1716
+ else if (days < 90)
1717
+ freshness = 10 * (1 - (days - 30) / 60);
1718
+ else if (days < 180)
1719
+ freshness = -5;
1720
+ else
1721
+ freshness = -15;
1722
+ }
1723
+ // 14 天首单 boost
1724
+ const firstSold = p.first_sold_at;
1725
+ const firstSaleBoost = firstSold && (Date.now() - new Date(firstSold.replace(' ', 'T') + 'Z').getTime()) < 14 * 86400_000 ? 5 : 0;
1726
+ const score = completion * 0.5 + rep_points * 0.1 + sharer * 2.0 + freshness + firstSaleBoost - dispute * 5.0;
1727
+ return { ...p, _boost: boost, _rep_level: rep_level, _rep_points: rep_points, _score: score, _freshness: freshness, _first_sale_boost: firstSaleBoost };
1728
+ });
1729
+ const sorted = sortMode === 'trending'
1730
+ ? enriched.sort((a, b) => b._score - a._score || b._boost - a._boost).slice(0, limit)
1731
+ : enriched.slice(0, limit);
1732
+ return {
1733
+ found: sorted.length,
1734
+ products: sorted.map((p) => {
1735
+ const levelMeta = { new: '', trusted: '⭐', quality: '🌟', star: '💫', legend: '🔥' };
1736
+ const badge = levelMeta[p._rep_level] ?? '';
1737
+ const parsed = parseProductForAgent(p);
1738
+ return {
1739
+ id: p.id,
1740
+ title: p.title,
1741
+ price: p.price,
1742
+ price_display: `${p.price} WAZ`,
1743
+ stock: p.stock,
1744
+ category: p.category,
1745
+ specs: parsed.specs,
1746
+ agent_summary: parsed.agent_summary,
1747
+ logistics: {
1748
+ handling_hours: p.handling_hours ?? 24,
1749
+ estimated_days: parsed.estimated_days,
1750
+ ship_regions: p.ship_regions ?? '全国',
1751
+ fragile: !!p.fragile,
1752
+ },
1753
+ after_sales: {
1754
+ return_days: p.return_days ?? 7,
1755
+ return_condition: p.return_condition ?? '',
1756
+ warranty_days: p.warranty_days ?? 0,
1757
+ },
1758
+ seller: badge ? `${badge} ${p.seller_name}` : p.seller_name,
1759
+ seller_id: p.seller_id,
1760
+ seller_reputation: p._rep_level !== 'new'
1761
+ ? `${badge} ${['', '可信', '优质', '明星', '传奇'][['new', 'trusted', 'quality', 'star', 'legend'].indexOf(p._rep_level)]}(${p._rep_points}分)`
1762
+ : undefined,
1763
+ // Tier 7 + 里程碑 5:agent-friendly 排序指标
1764
+ metrics: {
1765
+ completion_count: Number(p.completion_count) || 0,
1766
+ dispute_loss_count: Number(p.dispute_loss_count) || 0,
1767
+ unique_sharer_count: Number(p.unique_sharer_count) || 0,
1768
+ last_sold_at: p.last_sold_at || null,
1769
+ first_sold_at: p.first_sold_at || null,
1770
+ rep_points: p._rep_points,
1771
+ rep_level: p._rep_level,
1772
+ },
1773
+ score: Math.round(p._score * 100) / 100,
1774
+ score_breakdown: {
1775
+ freshness: Math.round(p._freshness * 100) / 100,
1776
+ first_sale_boost: p._first_sale_boost,
1777
+ },
1778
+ };
1779
+ }),
1780
+ sort: sortMode,
1781
+ limit,
1782
+ hint: 'agent 模式:products 已附带 metrics + score(含阶梯新鲜度 + 14d 首单 boost),可基于自身策略二次排序',
1783
+ };
1784
+ }
1785
+ function handleVerifyPrice(args) {
1786
+ const auth = requireAuth(db, args.api_key);
1787
+ if ('error' in auth)
1788
+ return auth;
1789
+ const { user } = auth;
1790
+ const productId = args.product_id;
1791
+ const qty = Number(args.quantity ?? 1);
1792
+ if (!productId)
1793
+ return { error: '请提供 product_id' };
1794
+ const product = db.prepare(`
1795
+ SELECT p.*, u.name as seller_name FROM products p
1796
+ JOIN users u ON p.seller_id = u.id
1797
+ WHERE p.id = ? AND p.status = 'active'
1798
+ `).get(productId);
1799
+ if (!product)
1800
+ return { error: `商品不存在或已下架:${productId}` };
1801
+ if (product.stock < qty) {
1802
+ return { error: `库存不足:当前库存 ${product.stock},请求数量 ${qty}` };
1803
+ }
1804
+ const now = new Date();
1805
+ const expiresAt = new Date(now.getTime() + 10 * 60_000);
1806
+ // 与 P0 #1 (commit 07f9e49 generateId) 一致:用 crypto.randomBytes,128-bit 熵
1807
+ const token = `pst_${randomBytes(16).toString('hex')}`;
1808
+ db.prepare(`
1809
+ INSERT INTO price_sessions (token, product_id, user_id, price, quantity, created_at, expires_at)
1810
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1811
+ `).run(token, productId, user.id, product.price, qty, now.toISOString(), expiresAt.toISOString());
1812
+ const parsed = parseProductForAgent(product);
1813
+ return {
1814
+ session_token: token,
1815
+ verified_price: product.price,
1816
+ quantity: qty,
1817
+ total: product.price * qty,
1818
+ product: {
1819
+ id: product.id,
1820
+ title: product.title,
1821
+ agent_summary: parsed.agent_summary,
1822
+ specs: parsed.specs,
1823
+ },
1824
+ expires_at: expiresAt.toISOString(),
1825
+ expires_in_seconds: 600,
1826
+ next: `调用 webaz_place_order 时传入 session_token="${token}" 确保以此价格成交`,
1827
+ };
1828
+ }
1829
+ async function handleListProduct(args) {
1830
+ // Wave 3 audit P0: 加 action 分发 — agent 卖家能完整管理目录(不止 create)
1831
+ const action = args.action || 'create';
1832
+ const apiKey = args.api_key;
1833
+ if (!apiKey)
1834
+ return { error: 'api_key required' };
1835
+ if (action !== 'create') {
1836
+ const auth0 = requireAuth(db, apiKey);
1837
+ if ('error' in auth0)
1838
+ return auth0;
1839
+ const { user: u0 } = auth0;
1840
+ if (action === 'mine') {
1841
+ // QA 轮 7 P1:旧版走 PWA HTTP,本地 dev 没起 PWA 就挂;改直读 SQLite
1842
+ const rows = db.prepare(`SELECT id, title, price, stock, status, stake_amount, category, created_at, updated_at
1843
+ FROM products
1844
+ WHERE seller_id = ?
1845
+ ORDER BY created_at DESC
1846
+ LIMIT 100`).all(u0.id);
1847
+ const summary = rows.reduce((acc, p) => {
1848
+ const s = p.status || 'active';
1849
+ acc[s] = (acc[s] || 0) + 1;
1850
+ return acc;
1851
+ }, {});
1852
+ return {
1853
+ found: rows.length,
1854
+ products: rows,
1855
+ summary_by_status: summary,
1856
+ seller_id: u0.id,
1857
+ note: 'Direct SQLite query — no PWA dependency.',
1858
+ };
1859
+ }
1860
+ const productId = args.product_id;
1861
+ if (!productId)
1862
+ return { error: `product_id required for action=${action}` };
1863
+ if (action === 'update') {
1864
+ const body = {};
1865
+ const updatable = [
1866
+ 'title', 'description', 'price', 'stock', 'specs', 'brand', 'model', 'handling_hours', 'ship_regions', 'estimated_days', 'fragile', 'return_days', 'return_condition', 'warranty_days',
1867
+ // S2 库存预警 / S3 多语言 / S4 商品溯源
1868
+ 'low_stock_threshold', 'auto_delist_on_zero', 'i18n_titles', 'i18n_descs', 'origin_claims',
1869
+ ];
1870
+ for (const k of updatable) {
1871
+ if (args[k] !== undefined)
1872
+ body[k] = args[k];
1873
+ }
1874
+ return await pwaApi('PUT', `/products/${productId}`, apiKey, body);
1875
+ }
1876
+ if (action === 'delist')
1877
+ return await pwaApi('PATCH', `/products/${productId}/status`, apiKey, { status: 'warehouse' });
1878
+ if (action === 'relist')
1879
+ return await pwaApi('PATCH', `/products/${productId}/status`, apiKey, { status: 'active' });
1880
+ if (action === 'trash')
1881
+ return await pwaApi('PATCH', `/products/${productId}/status`, apiKey, { status: 'deleted' });
1882
+ if (action === 'delete')
1883
+ return await pwaApi('DELETE', `/products/${productId}`, apiKey);
1884
+ return { error: `unknown action: ${action}. Valid actions: create | mine | update | delist | relist | trash | delete` };
1885
+ }
1886
+ // ─── action === 'create' (默认) — 沿用旧逻辑 ────────────────────
1887
+ const auth = requireAuth(db, apiKey);
1888
+ if ('error' in auth)
1889
+ return auth;
1890
+ const { user } = auth;
1891
+ if (user.role !== 'seller') {
1892
+ return { error: `只有 seller 角色可以上架商品,你的角色是:${user.role}` };
1893
+ }
1894
+ const price = args.price;
1895
+ // Per-order stake 模型(QA 轮 7 P0 修复):list_product 不再预扣 stake。
1896
+ // stake 在 place_order 那一刻按"该订单总额 × stake_rate"现锁,订单结算时退该笔,违约时扣该笔。
1897
+ // 这样每个 active 订单都有独立 stake 担保,多 stock 商品也不会被空头薅。
1898
+ // product.stake_amount 字段保留为"indicative rate × price"(前端展示用),不强制 lock。
1899
+ const stakeDiscount = getStakeDiscount(db, user.id);
1900
+ const stakeRate = Math.max(0.05, 0.15 - stakeDiscount); // 最低 5%,声誉越高折扣越大
1901
+ const stakeAmount = Math.round(price * stakeRate * 100) / 100; // indicative only; actual lock per-order
1902
+ const id = generateId('prd');
1903
+ const specsJson = args.specs != null
1904
+ ? (typeof args.specs === 'string' ? args.specs : JSON.stringify(args.specs))
1905
+ : null;
1906
+ const estJson = args.estimated_days != null
1907
+ ? (typeof args.estimated_days === 'string' ? args.estimated_days : JSON.stringify(args.estimated_days))
1908
+ : null;
1909
+ db.prepare(`
1910
+ INSERT INTO products (
1911
+ id, seller_id, title, description, price, stock, category, stake_amount,
1912
+ specs, brand, model, source_price,
1913
+ ship_regions, handling_hours, estimated_days, fragile,
1914
+ return_days, return_condition, warranty_days
1915
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1916
+ `).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);
1917
+ // 注:per-order stake 模型不在此 lock — 移到 place_order 时按订单总额锁
1918
+ // S2/S3/S4 新字段:通过 HTTP PUT 走标准校验路径(i18n 语言白名单 / origin_claims 4KB+sha256 / 库存阈值)
1919
+ const hasExtra = ['i18n_titles', 'i18n_descs', 'origin_claims', 'low_stock_threshold', 'auto_delist_on_zero']
1920
+ .some(k => args[k] !== undefined);
1921
+ let extraResult = null;
1922
+ if (hasExtra) {
1923
+ const body = {};
1924
+ for (const k of ['i18n_titles', 'i18n_descs', 'origin_claims', 'low_stock_threshold', 'auto_delist_on_zero']) {
1925
+ if (args[k] !== undefined)
1926
+ body[k] = args[k];
1927
+ }
1928
+ extraResult = await pwaApi('PUT', `/products/${id}`, apiKey, body);
1929
+ }
1930
+ const rep = getReputation(db, user.id);
1931
+ return {
1932
+ success: true,
1933
+ product_id: id,
1934
+ title: args.title,
1935
+ price: `${price} WAZ`,
1936
+ // QA 轮 7 P2:旧文案 "stake_locked: 15 WAZ(质押保证金,交易完成后返还)" 误导
1937
+ // 实际 per-order 模型不在 list 时锁;改成 stake_per_order_estimate + 明示策略
1938
+ stake_per_order_estimate: `${stakeAmount} WAZ(按当前 price × ${(stakeRate * 100).toFixed(0)}% 估算;实际每笔订单按订单总额×rate 在 place_order 时从 seller balance 锁)`,
1939
+ stake_strategy: 'per_order',
1940
+ stake_rate: stakeDiscount > 0 ? `${(stakeRate * 100).toFixed(0)}%(声誉折扣 -${(stakeDiscount * 100).toFixed(0)}%,原 15%)` : '15%',
1941
+ stake_locked_now: 0, // list 不再 lock;明示给 agent
1942
+ reputation_level: rep.level.label,
1943
+ status: 'active(买家现在可以搜索到这件商品)',
1944
+ ...(extraResult ? { extra_fields_applied: !('error' in extraResult), extra_result: extraResult } : {}),
1945
+ };
1946
+ }
1947
+ function handlePlaceOrder(args) {
1948
+ const auth = requireAuth(db, args.api_key);
1949
+ if ('error' in auth)
1950
+ return auth;
1951
+ const { user } = auth;
1952
+ if (user.role !== 'buyer') {
1953
+ return { error: `只有 buyer 角色可以下单,你的角色是:${user.role}` };
1954
+ }
1955
+ const product = db
1956
+ .prepare("SELECT p.*, u.name as seller_name, u.id as seller_uid FROM products p JOIN users u ON p.seller_id = u.id WHERE p.id = ? AND p.status = 'active'")
1957
+ .get(args.product_id);
1958
+ if (!product) {
1959
+ return { error: `商品不存在或已下架:${args.product_id}` };
1960
+ }
1961
+ const quantity = args.quantity ?? 1;
1962
+ if (product.stock < quantity) {
1963
+ return { error: `库存不足:当前库存 ${product.stock},你要购买 ${quantity}` };
1964
+ }
1965
+ // 验证 session_token(如果提供)
1966
+ if (args.session_token) {
1967
+ const session = db.prepare(`
1968
+ SELECT * FROM price_sessions WHERE token = ? AND product_id = ? AND user_id = ?
1969
+ `).get(args.session_token, args.product_id, user.id);
1970
+ if (!session)
1971
+ return { error: 'session_token 无效,请重新调用 webaz_verify_price' };
1972
+ if (session.used_at)
1973
+ return { error: 'session_token 已使用,请重新调用 webaz_verify_price' };
1974
+ if (new Date(session.expires_at) < new Date()) {
1975
+ return { error: 'session_token 已过期(10分钟有效),请重新调用 webaz_verify_price' };
1976
+ }
1977
+ if (session.price !== product.price) {
1978
+ return {
1979
+ error: 'price_changed',
1980
+ message: `商品价格已变动:验证时 ${session.price} WAZ,当前 ${product.price} WAZ`,
1981
+ new_price: product.price,
1982
+ hint: '请重新调用 webaz_verify_price 获取新价格后再下单',
1983
+ };
1984
+ }
1985
+ db.prepare(`UPDATE price_sessions SET used_at = datetime('now') WHERE token = ?`).run(args.session_token);
1986
+ }
1987
+ const totalAmount = product.price * quantity;
1988
+ const wallet = db
1989
+ .prepare('SELECT * FROM wallets WHERE user_id = ?')
1990
+ .get(user.id);
1991
+ if (wallet.balance < totalAmount) {
1992
+ return {
1993
+ error: `余额不足:订单金额 ${totalAmount} WAZ,你的余额 ${wallet.balance} WAZ`,
1994
+ };
1995
+ }
1996
+ // B5 公益捐赠:donation_pct 必须是固定档位之一(同后端 DONATION_VALID_PCTS)
1997
+ const DONATION_VALID_PCTS = new Set([0, 0.005, 0.01, 0.02, 0.05]);
1998
+ const donationPctNum = Number(args.donation_pct || 0);
1999
+ if (!DONATION_VALID_PCTS.has(donationPctNum)) {
2000
+ return { error: 'donation_pct 必须是 0 / 0.005 / 0.01 / 0.02 / 0.05 之一' };
2001
+ }
2002
+ const donationAmount = donationPctNum > 0 ? Math.round(totalAmount * donationPctNum * 100) / 100 : 0;
2003
+ // B2 匿名收件:服务端生成 PR-XXXXX 代号(同后端 generateRecipientCode 逻辑)
2004
+ const anonymousFlag = args.anonymous_recipient ? 1 : 0;
2005
+ let recipientCode = null;
2006
+ if (anonymousFlag === 1) {
2007
+ const ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
2008
+ const buf = randomBytes(8);
2009
+ let s = '';
2010
+ for (let i = 0; i < 5; i++)
2011
+ s += ALPHABET[buf[i] % ALPHABET.length];
2012
+ recipientCode = 'PR-' + s;
2013
+ }
2014
+ const now = new Date();
2015
+ const orderId = generateId('ord');
2016
+ // 找推荐人
2017
+ let promoterId = null;
2018
+ if (args.promoter_api_key) {
2019
+ const promoter = db
2020
+ .prepare('SELECT id FROM users WHERE api_key = ?')
2021
+ .get(args.promoter_api_key);
2022
+ if (promoter)
2023
+ promoterId = promoter.id;
2024
+ }
2025
+ db.prepare(`
2026
+ INSERT INTO orders (
2027
+ id, product_id, buyer_id, seller_id, promoter_id,
2028
+ quantity, unit_price, total_amount, escrow_amount,
2029
+ status, shipping_address, notes,
2030
+ pay_deadline, accept_deadline, ship_deadline,
2031
+ pickup_deadline, delivery_deadline, confirm_deadline,
2032
+ anonymous_recipient, recipient_code, donation_amount
2033
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2034
+ `).run(orderId, product.id, user.id, product.seller_uid, promoterId, quantity, product.price, totalAmount, totalAmount, args.shipping_address, args.notes ?? null, addHours(now, 24), // 买家 24h 内必须付款
2035
+ addHours(now, 48), // 卖家 24h 内接单
2036
+ addHours(now, 120), // 卖家 72h 内发货
2037
+ addHours(now, 168), // 物流 48h 内揽收
2038
+ addHours(now, 336), // 物流 7 天内投递
2039
+ addHours(now, 408), // 买家 72h 内确认
2040
+ anonymousFlag, recipientCode, donationAmount);
2041
+ // QA 轮 9.4 P0 修复(2026-05-27):MCP 之前没填 settleCommission 真正读的 l1_uid + snapshot_commission_rate
2042
+ // 后果:promoter 永远拿不到 commission(PWA 看 l1_uid=NULL → chain_gap → 14 WAZ 进 charity)
2043
+ // 并因 settleOrder default 0 vs settleCommission default 0.10 还印 20 WAZ from thin air
2044
+ // 修:place_order 时同步 PWA 的写法 — l1_uid = promoter(如有),snapshot_commission_rate 从 product 拷贝
2045
+ // 注:promoter_api_key 路径下 L2/L3 不可推断(需走 share_link 点击的 product_share_attribution 链)→ 保持 NULL
2046
+ // L2/L3 NULL 在 global region (max_levels=1) 走 region 截断 → global_fund,不进 charity
2047
+ // L2/L3 NULL 在更高 max_levels region 走 chain_gap → charity
2048
+ const snapshotCommissionRate = Number(product.commission_rate ?? 0.10);
2049
+ db.prepare('UPDATE orders SET l1_uid = ?, snapshot_commission_rate = ? WHERE id = ?')
2050
+ .run(promoterId, snapshotCommissionRate, orderId);
2051
+ // 扣除库存
2052
+ db.prepare('UPDATE products SET stock = stock - ? WHERE id = ?').run(quantity, product.id);
2053
+ // 模拟"付款":锁定买家余额
2054
+ db.prepare(`
2055
+ UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?
2056
+ `).run(totalAmount, totalAmount, user.id);
2057
+ // QA 轮 7 P0 修复:per-order 卖家 stake — 下单瞬间从 seller.balance 锁 15% 到 staked
2058
+ // 该 stake 在 settleOrder 退还 / fault_seller 时扣给 buyer 50% + sys_protocol 50%
2059
+ // 多 stock 商品每笔都独立 lock,不再被空头薅
2060
+ const sellerStakeRate = 0.15; // TODO: protocol_params 化(default_commission_rate 现是 0.10 不同义)
2061
+ const sellerStake = Math.round(totalAmount * sellerStakeRate * 100) / 100;
2062
+ const sellerWalletNow = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(product.seller_uid);
2063
+ if (!sellerWalletNow || sellerWalletNow.balance < sellerStake) {
2064
+ // rollback buyer escrow + stock (因为 stake 不够无法担保订单)
2065
+ db.prepare('UPDATE wallets SET balance = balance + ?, escrowed = escrowed - ? WHERE user_id = ?').run(totalAmount, totalAmount, user.id);
2066
+ db.prepare('UPDATE products SET stock = stock + ? WHERE id = ?').run(quantity, product.id);
2067
+ return {
2068
+ error: 'seller_insufficient_stake_balance',
2069
+ error_code: 'SELLER_INSUFFICIENT_BALANCE',
2070
+ message: `卖家余额不足以锁定订单 stake(需要 ${sellerStake} WAZ,卖家余 ${sellerWalletNow?.balance ?? 0})。卖家先充值才能继续接单。`,
2071
+ seller_stake_required: sellerStake,
2072
+ seller_balance: sellerWalletNow?.balance ?? 0,
2073
+ next_step: '换其他卖家的同类商品,或建议该卖家充值后重试。',
2074
+ };
2075
+ }
2076
+ db.prepare('UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?').run(sellerStake, sellerStake, product.seller_uid);
2077
+ // 直接进入 paid 状态(Phase 0 模拟支付)
2078
+ transition(db, orderId, 'paid', user.id, [], '模拟支付完成,资金已托管');
2079
+ notifyTransition(db, orderId, 'created', 'paid');
2080
+ // 检查卖家是否开启了 auto_accept Skill,若是则自动接单
2081
+ let autoAccepted = false;
2082
+ if (shouldAutoAccept(db, orderId)) {
2083
+ const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
2084
+ const acceptResult = transition(db, orderId, 'accepted', sysUser.id, [], '⚡ auto_accept Skill 自动接单');
2085
+ if (acceptResult.success) {
2086
+ notifyTransition(db, orderId, 'paid', 'accepted');
2087
+ autoAccepted = true;
2088
+ }
2089
+ }
2090
+ return {
2091
+ success: true,
2092
+ order_id: orderId,
2093
+ product: product.title,
2094
+ seller: product.seller_name,
2095
+ quantity,
2096
+ total_amount: `${totalAmount} WAZ(已托管,等待交易完成后自动结算)`,
2097
+ status: autoAccepted ? 'accepted' : 'paid',
2098
+ auto_accepted: autoAccepted || undefined,
2099
+ next: autoAccepted
2100
+ ? '⚡ 卖家已开启自动接单,订单已立即接受!等待卖家发货。'
2101
+ : '卖家须在 accept_deadline 前接单(付款后 24h 内,即下单后 ~48h)。超时未接单系统自动退款。详见 deadline 字段。',
2102
+ track: `用 webaz_get_status 查看订单进展`,
2103
+ };
2104
+ }
2105
+ async function handleUpdateOrder(args) {
2106
+ const auth = requireAuth(db, args.api_key);
2107
+ if ('error' in auth)
2108
+ return auth;
2109
+ const { user } = auth;
2110
+ const orderId = args.order_id;
2111
+ const action = args.action;
2112
+ const notes = args.notes ?? '';
2113
+ const evidenceDesc = args.evidence_description ?? '';
2114
+ // QA 轮 9.4 P0 修复(2026-05-27):MCP confirm 转发 PWA endpoint,单一真相源。
2115
+ // 旧 MCP settleOrder 写死 3% promoter 单 L1,跟 PWA settleCommission 的
2116
+ // commission_rate × 7:2:1 + chain_gap → charity 完全不同。
2117
+ // agent-native 协议要求"哪个接口进结果一致"。MCP confirm 不再自己结算,
2118
+ // 走 PWA /api/orders/:id/action 的 settleOrder + settleCommission(authoritative)。
2119
+ if (action === 'confirm') {
2120
+ const apiKey = args.api_key;
2121
+ const result = await pwaApi('POST', `/orders/${encodeURIComponent(orderId)}/action`, apiKey, {
2122
+ action: 'confirm',
2123
+ notes,
2124
+ });
2125
+ // 透传 PWA 响应;其中包含真实 settlement 结果。
2126
+ // 若 PWA 不可达(本地 dev 没起 PWA),返回明确错误而非 fallback 到旧 MCP 路径
2127
+ return result;
2128
+ }
2129
+ // 验证订单存在且该用户是参与方
2130
+ let order = db
2131
+ .prepare('SELECT * FROM orders WHERE id = ?')
2132
+ .get(orderId);
2133
+ if (!order)
2134
+ return { error: `订单不存在:${orderId}` };
2135
+ // 物流首次操作:先绑定再做参与方检查
2136
+ if ((action === 'pickup' || action === 'transit') &&
2137
+ !order.logistics_id &&
2138
+ user.role === 'logistics') {
2139
+ db.prepare('UPDATE orders SET logistics_id = ? WHERE id = ?').run(user.id, orderId);
2140
+ order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
2141
+ }
2142
+ const isParticipant = order.buyer_id === user.id ||
2143
+ order.seller_id === user.id ||
2144
+ order.logistics_id === user.id;
2145
+ if (!isParticipant && user.role !== 'arbitrator') {
2146
+ return { error: '你不是这笔订单的参与方,无法操作' };
2147
+ }
2148
+ // action → 状态映射
2149
+ const actionMap = {
2150
+ accept: 'accepted',
2151
+ ship: 'shipped',
2152
+ pickup: 'picked_up',
2153
+ transit: 'in_transit',
2154
+ deliver: 'delivered',
2155
+ confirm: 'confirmed',
2156
+ dispute: 'disputed',
2157
+ };
2158
+ const toStatus = actionMap[action];
2159
+ if (!toStatus)
2160
+ return { error: `未知操作:${action}` };
2161
+ // 如果有证据描述,先创建证据记录
2162
+ const evidenceIds = [];
2163
+ if (evidenceDesc) {
2164
+ const evidenceId = generateId('evt');
2165
+ db.prepare(`
2166
+ INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash)
2167
+ VALUES (?, ?, ?, 'description', ?, ?)
2168
+ `).run(evidenceId, orderId, user.id, evidenceDesc, `hash_${Date.now()}`);
2169
+ evidenceIds.push(evidenceId);
2170
+ }
2171
+ const result = transition(db, orderId, toStatus, user.id, evidenceIds, notes);
2172
+ if (!result.success) {
2173
+ return { error: result.error };
429
2174
  }
430
- const sorted = products
431
- .map((p) => {
432
- const boost = getSearchBoost(db, p.seller_id);
433
- const rep = db.prepare('SELECT total_points, level FROM reputation_scores WHERE user_id = ?').get(p.seller_id);
434
- return { ...p, _boost: boost, _rep_level: rep?.level ?? 'new', _rep_points: rep?.total_points ?? 0 };
435
- })
436
- .sort((a, b) => b._boost - a._boost);
437
- return {
438
- found: sorted.length,
439
- products: sorted.map((p) => {
440
- const levelMeta = { new: '', trusted: '⭐', quality: '🌟', star: '💫', legend: '🔥' };
441
- const badge = levelMeta[p._rep_level] ?? '';
2175
+ // 通知相关参与方(L2-6)
2176
+ notifyTransition(db, orderId, order.status, toStatus);
2177
+ // 如果是 dispute,写入 disputes 表(L3-1)
2178
+ if (toStatus === 'disputed') {
2179
+ const disputeResult = createDispute(db, orderId, user.id, notes || evidenceDesc || '买家发起争议', evidenceIds);
2180
+ if (disputeResult.success) {
442
2181
  return {
443
- id: p.id,
444
- title: p.title,
445
- description: p.description,
446
- price: `${p.price} WAZ`,
447
- stock: p.stock,
448
- category: p.category,
449
- seller: badge ? `${badge} ${p.seller_name}` : p.seller_name,
450
- seller_id: p.seller_id,
451
- seller_reputation: p._rep_level !== 'new' ? `${badge} ${['', '可信', '优质', '明星', '传奇'][['new', 'trusted', 'quality', 'star', 'legend'].indexOf(p._rep_level)]}(${p._rep_points}分)` : undefined,
2182
+ success: true,
2183
+ new_status: 'disputed',
2184
+ dispute_id: disputeResult.disputeId,
2185
+ message: disputeResult.message,
2186
+ respond_deadline: disputeResult.respondDeadline,
2187
+ next: `用 webaz_dispute action=view dispute_id=${disputeResult.disputeId} 查看争议详情`,
452
2188
  };
453
- }),
2189
+ }
2190
+ // 争议记录写入失败不影响状态,仍返回成功
2191
+ return { success: true, new_status: 'disputed', message: '争议已发起,资金已冻结', warning: disputeResult.error };
2192
+ }
2193
+ // 如果是 confirmed,自动触发结算
2194
+ if (toStatus === 'confirmed') {
2195
+ const sysUser = db
2196
+ .prepare("SELECT id FROM users WHERE id = 'sys_protocol'")
2197
+ .get();
2198
+ transition(db, orderId, 'completed', sysUser.id, [], '系统自动结算');
2199
+ const breakdown = settleOrder(db, orderId);
2200
+ return {
2201
+ success: true,
2202
+ new_status: 'completed',
2203
+ message: '确认收货成功!资金已自动分配给各参与方。',
2204
+ settlement_breakdown: breakdown, // QA P1 transparency: 详列每分钱去哪
2205
+ detail: `用 webaz_wallet 查看你的收益`,
2206
+ };
2207
+ }
2208
+ const statusMessages = {
2209
+ accepted: '接单成功!请在承诺时间内发货,超时将自动判违约。',
2210
+ shipped: '发货成功!物流方 48 小时内需要完成揽收。',
2211
+ picked_up: '揽收确认!请尽快安排运输。',
2212
+ in_transit: '运输状态已更新。',
2213
+ delivered: '投递确认!买家 72 小时内确认收货,超时自动确认。',
2214
+ disputed: '争议已发起,资金冻结,等待仲裁员介入。',
2215
+ };
2216
+ return {
2217
+ success: true,
2218
+ new_status: result.newStatus,
2219
+ message: statusMessages[toStatus] ?? '状态已更新',
2220
+ history_record: result.historyId,
454
2221
  };
455
2222
  }
456
- function handleListProduct(args) {
2223
+ function handleGetStatus(args) {
457
2224
  const auth = requireAuth(db, args.api_key);
2225
+ if ('error' in auth)
2226
+ return auth;
2227
+ const statusInfo = getOrderStatus(db, args.order_id);
2228
+ if (!statusInfo)
2229
+ return { error: `订单不存在:${args.order_id}` };
2230
+ const { order, history, currentResponsible, activeDeadline, isOverdue } = statusInfo;
2231
+ return {
2232
+ order_id: order.id,
2233
+ current_status: order.status,
2234
+ current_responsible: currentResponsible
2235
+ ? `${currentResponsible}(当前应由此角色操作)`
2236
+ : '无(等待系统处理)',
2237
+ deadline: activeDeadline
2238
+ ? {
2239
+ field: activeDeadline.field,
2240
+ time: activeDeadline.deadline,
2241
+ overdue: isOverdue ? '⚠️ 已超时!协议将自动判责' : '未超时',
2242
+ }
2243
+ : null,
2244
+ history: history.map((h) => ({
2245
+ from: h.from_status,
2246
+ to: h.to_status,
2247
+ by: `${h.actor_name}(${h.actor_role_name})`,
2248
+ at: h.created_at,
2249
+ evidence_count: JSON.parse(h.evidence_ids || '[]').length,
2250
+ notes: h.notes,
2251
+ })),
2252
+ };
2253
+ }
2254
+ async function handleWallet(args) {
2255
+ // Wave 3 audit P0: 加 action 分发 — agent 能查充值/提现/收入历史(写操作仍 UI-only 走 2FA)
2256
+ const action = args.action || 'view';
2257
+ const apiKey = args.api_key;
2258
+ if (!apiKey)
2259
+ return { error: 'api_key required' };
2260
+ if (action === 'deposits')
2261
+ return await pwaApi('GET', '/wallet/deposits', apiKey);
2262
+ if (action === 'withdrawals')
2263
+ return await pwaApi('GET', '/wallet/withdrawals', apiKey);
2264
+ if (action === 'income')
2265
+ return await pwaApi('GET', '/wallet/income', apiKey);
2266
+ if (action !== 'view') {
2267
+ return { error: `unknown action: ${action}. Valid: view | deposits | withdrawals | income. 注意:提现/充值/白名单管理需 Passkey + 邮件 OTP,仅 PWA Web 端可用` };
2268
+ }
2269
+ // ─── action === 'view' (默认) — 沿用旧逻辑 ─────────────────────
2270
+ const auth = requireAuth(db, apiKey);
458
2271
  if ('error' in auth)
459
2272
  return auth;
460
2273
  const { user } = auth;
461
- if (user.role !== 'seller') {
462
- return { error: `只有 seller 角色可以上架商品,你的角色是:${user.role}` };
2274
+ const wallet = db
2275
+ .prepare('SELECT * FROM wallets WHERE user_id = ?')
2276
+ .get(user.id);
2277
+ if (!wallet)
2278
+ return { error: '钱包不存在' };
2279
+ // QA 轮 9.4-retry-v3 P1:旧版用 SUM(payouts.amount) 算 earned
2280
+ // 但 PWA settleOrder 只更 wallets.earned 列不写 payouts 表 → MCP 视图 stale
2281
+ // PWA confirm 后真实 earned 在 wallets.earned;payouts 仅 MCP legacy settleOrder 写
2282
+ // 改用 wallet.earned 列作单一真相源
2283
+ const totalEarned = Number(wallet.earned ?? 0);
2284
+ const rep = getReputation(db, user.id);
2285
+ const nextLevel = ['new', 'trusted', 'quality', 'star', 'legend'];
2286
+ const nextIdx = nextLevel.indexOf(rep.level.key) + 1;
2287
+ const nextLevelDef = nextIdx < nextLevel.length ? { trusted: 200, quality: 800, star: 2000, legend: 5000 }[nextLevel[nextIdx]] : null;
2288
+ return {
2289
+ user: user.name,
2290
+ role: user.role,
2291
+ balance: `${wallet.balance} WAZ(可用)`,
2292
+ staked: `${wallet.staked} WAZ(质押中,不可用)`,
2293
+ escrowed: `${wallet.escrowed} WAZ(托管中,交易完成后结算)`,
2294
+ total_earned: `${totalEarned} WAZ(历史累计收益)`,
2295
+ reputation: {
2296
+ level: `${rep.level.icon} ${rep.level.label}`,
2297
+ total_points: rep.total_points,
2298
+ transactions_done: rep.transactions_done,
2299
+ disputes_won: rep.disputes_won,
2300
+ disputes_lost: rep.disputes_lost,
2301
+ violations: rep.violations,
2302
+ stake_discount: rep.level.stakeDiscount > 0 ? `-${(rep.level.stakeDiscount * 100).toFixed(0)}% 质押优惠` : '暂无(升级后享优惠)',
2303
+ next_level: nextLevelDef ? `距下一等级还需 ${nextLevelDef - rep.total_points} 分` : '已达最高等级!',
2304
+ recent_events: rep.recent_events.slice(0, 5).map(e => `${e.points > 0 ? '+' : ''}${e.points} ${e.reason}`),
2305
+ },
2306
+ };
2307
+ }
2308
+ // ─── 通知处理 ─────────────────────────────────────────────────
2309
+ function handleNotifications(args) {
2310
+ const auth = requireAuth(db, args.api_key);
2311
+ if ('error' in auth)
2312
+ return auth;
2313
+ const { user } = auth;
2314
+ const onlyUnread = args.unread === true;
2315
+ const notifs = getNotifications(db, user.id, onlyUnread, 30);
2316
+ const unread = getUnreadCount(db, user.id);
2317
+ if (args.mark_read) {
2318
+ markRead(db, user.id);
2319
+ }
2320
+ return {
2321
+ unread_count: unread,
2322
+ notifications: notifs.map(n => ({
2323
+ id: n.id,
2324
+ title: n.title,
2325
+ body: n.body,
2326
+ order_id: n.order_id,
2327
+ read: n.read === 1,
2328
+ time: n.created_at,
2329
+ })),
2330
+ };
2331
+ }
2332
+ // ─── 争议处理 ─────────────────────────────────────────────────
2333
+ async function handleDispute(args) {
2334
+ const apiKey = args.api_key;
2335
+ if (!apiKey)
2336
+ return { error: 'api_key required' };
2337
+ const auth = requireAuth(db, apiKey);
2338
+ if ('error' in auth)
2339
+ return auth;
2340
+ const { user } = auth;
2341
+ const action = args.action;
2342
+ // ── 查看争议详情 ────────────────────────────────────────────
2343
+ if (action === 'view') {
2344
+ let dispute = args.dispute_id
2345
+ ? getDisputeDetails(db, args.dispute_id)
2346
+ : args.order_id
2347
+ ? getOrderDispute(db, args.order_id)
2348
+ : null;
2349
+ if (!dispute)
2350
+ return { error: '找不到争议记录,请提供 dispute_id 或 order_id' };
2351
+ const evidenceList = (orderId, uploaderRole) => db.prepare(`
2352
+ SELECT e.description, e.type, e.file_hash, e.created_at, u.name as uploader
2353
+ FROM evidence e JOIN users u ON e.uploader_id = u.id
2354
+ WHERE e.order_id = ? AND u.role = ?
2355
+ ORDER BY e.created_at ASC
2356
+ `).all(orderId, uploaderRole);
2357
+ return {
2358
+ dispute_id: dispute.id,
2359
+ order_id: dispute.order_id,
2360
+ status: dispute.status,
2361
+ initiator: `${dispute.initiator_name}(${dispute.initiator_role})`,
2362
+ defendant: `${dispute.defendant_name}(${dispute.defendant_role})`,
2363
+ reason: dispute.reason,
2364
+ respond_deadline: dispute.respond_deadline,
2365
+ arbitrate_deadline: dispute.arbitrate_deadline,
2366
+ plaintiff_evidence: evidenceList(dispute.order_id, dispute.initiator_role),
2367
+ defendant_notes: dispute.defendant_notes ?? '(被诉方尚未提交回应)',
2368
+ defendant_evidence: JSON.parse(dispute.defendant_evidence_ids || '[]'),
2369
+ ruling: dispute.ruling_type
2370
+ ? { type: dispute.ruling_type, refund_amount: dispute.refund_amount, reason: dispute.verdict_reason }
2371
+ : null,
2372
+ // QA 轮 15 P1:未裁定时暴露可选 ruling 列表(与 PWA disputes-write.ts validRulings 对齐)
2373
+ ruling_options: dispute.ruling_type ? undefined : [
2374
+ { type: 'refund_buyer', desc: '全额退款给买家(卖家败诉)' },
2375
+ { type: 'release_seller', desc: '放款给卖家(买家败诉)' },
2376
+ { type: 'partial_refund', desc: '部分退款(需传 refund_amount)' },
2377
+ { type: 'liability_split', desc: '按责任分配(需传 liability_parties 数组)' },
2378
+ ],
2379
+ resolved_at: dispute.resolved_at,
2380
+ };
2381
+ }
2382
+ // ── 仲裁员查看所有待处理争议 ───────────────────────────────
2383
+ if (action === 'list_open') {
2384
+ if (user.role !== 'arbitrator') {
2385
+ return { error: '只有仲裁员可以查看所有待处理争议' };
2386
+ }
2387
+ const disputes = getOpenDisputes(db);
2388
+ return {
2389
+ open_count: disputes.length,
2390
+ disputes: disputes.map(d => ({
2391
+ dispute_id: d.id,
2392
+ order_id: d.order_id,
2393
+ status: d.status,
2394
+ initiator: `${d.initiator_name}(${d.initiator_role})`,
2395
+ defendant: `${d.defendant_name}(${d.defendant_role})`,
2396
+ reason: d.reason,
2397
+ amount: `${d.total_amount} WAZ`,
2398
+ respond_deadline: d.respond_deadline,
2399
+ arbitrate_deadline: d.arbitrate_deadline,
2400
+ created_at: d.created_at,
2401
+ }))
2402
+ };
2403
+ }
2404
+ // ── 被诉方提交反驳 ──────────────────────────────────────────
2405
+ if (action === 'respond') {
2406
+ if (!args.dispute_id)
2407
+ return { error: '请提供 dispute_id' };
2408
+ // 如有证据描述,先创建证据记录
2409
+ const evidenceIds = [];
2410
+ if (args.evidence_description) {
2411
+ const dispute = getDisputeDetails(db, args.dispute_id);
2412
+ if (dispute) {
2413
+ const eid = generateId('evt');
2414
+ db.prepare(`
2415
+ INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash)
2416
+ VALUES (?, ?, ?, 'description', ?, ?)
2417
+ `).run(eid, dispute.order_id, user.id, args.evidence_description, `hash_${Date.now()}`);
2418
+ evidenceIds.push(eid);
2419
+ }
2420
+ }
2421
+ return respondToDispute(db, args.dispute_id, user.id, args.notes ?? '', evidenceIds);
2422
+ }
2423
+ // ── 争议参与方补充证据 (Wave 3 新增) ────────────────────────
2424
+ if (action === 'add_evidence') {
2425
+ if (!args.dispute_id)
2426
+ return { error: '请提供 dispute_id' };
2427
+ if (!args.evidence_description)
2428
+ return { error: '请提供 evidence_description(证据描述)' };
2429
+ // QA 轮 15 P1:PWA add-evidence 路由收 `description` 字段,MCP 之前发 `evidence_description` → 不匹配证据写不进
2430
+ return await pwaApi('POST', `/disputes/${args.dispute_id}/add-evidence`, apiKey, {
2431
+ description: args.evidence_description,
2432
+ evidence_type: 'text',
2433
+ });
2434
+ }
2435
+ // ── 仲裁员裁定(Iron-Rule:MCP 仅登记意图,真正裁定需 PWA + Passkey 二次确认)─
2436
+ // QA 轮 7 P1 修复:旧版直接调 PWA HTTP /api/disputes/:id/arbitrate,
2437
+ // 1) 违反 Iron-Rule 模型(应该跟 revoke_key/rotate_key 同模式 — agent 不能单方面执行不可逆操作)
2438
+ // 2) 本地 dev 没起 PWA 就挂
2439
+ if (action === 'arbitrate') {
2440
+ if (!args.dispute_id)
2441
+ return { error: '请提供 dispute_id' };
2442
+ if (!args.ruling)
2443
+ return { error: '请提供 ruling(refund_buyer / release_seller / partial_refund / liability_split)' };
2444
+ if (!args.ruling_reason)
2445
+ return { error: '请提供 ruling_reason(裁定理由将永久记录)' };
2446
+ if (args.ruling === 'partial_refund' && !args.refund_amount && !args.liable_party) {
2447
+ return { error: 'partial_refund 需要提供 refund_amount,或 liable_party(第三方责任方)' };
2448
+ }
2449
+ if (args.ruling === 'liability_split' && (!Array.isArray(args.liability_parties) || args.liability_parties.length === 0)) {
2450
+ return { error: 'liability_split 需要提供 liability_parties 数组,每项 { user_id, amount }' };
2451
+ }
2452
+ return {
2453
+ success: false,
2454
+ requires_human_action: true,
2455
+ iron_rule: 'Arbitration ruling is irreversible, affects multiple parties, and locks fund distribution permanently. Agent cannot execute unilaterally. Same Iron-Rule gating as webaz_revoke_key / claim_verify vote.',
2456
+ action: 'arbitrate_dispute',
2457
+ dispute_id: args.dispute_id,
2458
+ proposed_ruling: {
2459
+ ruling: args.ruling,
2460
+ reason: args.ruling_reason,
2461
+ ...(args.refund_amount !== undefined ? { refund_amount: args.refund_amount } : {}),
2462
+ ...(args.liable_party ? { liable_party: args.liable_party } : {}),
2463
+ ...(args.liability_parties ? { liability_parties: args.liability_parties } : {}),
2464
+ },
2465
+ next_step: {
2466
+ via: 'PWA + Passkey (arbitrator role)',
2467
+ url: `https://webaz.xyz/arbitrate?dispute=${encodeURIComponent(args.dispute_id)}`,
2468
+ instructions: '1) Open URL in browser 2) Sign in as arbitrator 3) Review evidence 4) Confirm with Passkey 5) Submit final ruling. Action is irreversible after confirmation.',
2469
+ },
2470
+ };
2471
+ }
2472
+ return { error: `未知 action:${action}。Valid: view | list_open | respond | add_evidence | arbitrate` };
2473
+ }
2474
+ // ─── 索赔验证(claim-verification)处理 — Wave 6 新增 ────────────
2475
+ async function handleClaimVerify(args) {
2476
+ const apiKey = args.api_key;
2477
+ if (!apiKey)
2478
+ return { error: 'api_key required' };
2479
+ const action = String(args.action || '');
2480
+ // 【买家】发起验证
2481
+ if (action === 'create') {
2482
+ if (!args.order_id)
2483
+ return { error: '请提供 order_id(待验证的订单)' };
2484
+ if (!args.claim_target)
2485
+ return { error: '请提供 claim_target(声明对象,如 title / description / condition / shipping_time 等)' };
2486
+ if (!args.claim_text)
2487
+ return { error: '请提供 claim_text(声明文本,6-500 字)' };
2488
+ return await pwaApi('POST', `/orders/${args.order_id}/claim-verification`, apiKey, {
2489
+ claim_target: args.claim_target,
2490
+ claim_text: args.claim_text,
2491
+ evidence_uri: args.evidence_uri,
2492
+ });
2493
+ }
2494
+ // 【任意参与方】查详情
2495
+ if (action === 'view') {
2496
+ if (!args.task_id)
2497
+ return { error: '请提供 task_id' };
2498
+ return await pwaApi('GET', `/claim-tasks/${args.task_id}`, apiKey);
2499
+ }
2500
+ // 【我相关】三类视角
2501
+ if (action === 'mine') {
2502
+ return await pwaApi('GET', '/claim-tasks/mine', apiKey);
2503
+ }
2504
+ // 【卖家】提交反驳证据(延期 24h)
2505
+ if (action === 'submit_seller_evidence') {
2506
+ if (!args.task_id)
2507
+ return { error: '请提供 task_id' };
2508
+ if (!args.evidence_uri)
2509
+ return { error: '请提供 evidence_uri(证据链接,4-500 字符)' };
2510
+ return await pwaApi('POST', `/claim-tasks/${args.task_id}/seller-evidence`, apiKey, {
2511
+ evidence_uri: args.evidence_uri,
2512
+ });
2513
+ }
2514
+ // 【Verifier】可接任务
2515
+ if (action === 'available') {
2516
+ return await pwaApi('GET', '/claim-tasks/available', apiKey);
2517
+ }
2518
+ // 【Verifier】投票
2519
+ if (action === 'vote') {
2520
+ if (!args.task_id)
2521
+ return { error: '请提供 task_id' };
2522
+ const VALID_VOTES = ['pass', 'fail', 'no_fault', 'abstain'];
2523
+ if (!args.vote || !VALID_VOTES.includes(String(args.vote))) {
2524
+ return { error: `vote 必须是 ${VALID_VOTES.join(' / ')}`, error_code: 'VOTE_INVALID' };
2525
+ }
2526
+ return await pwaApi('POST', `/claim-tasks/${args.task_id}/vote`, apiKey, {
2527
+ vote: args.vote,
2528
+ evidence_uri: args.evidence_uri,
2529
+ note: args.note,
2530
+ });
463
2531
  }
464
- const price = args.price;
465
- const stakeDiscount = getStakeDiscount(db, user.id);
466
- const stakeRate = Math.max(0.05, 0.15 - stakeDiscount); // 最低 5%,声誉越高折扣越大
467
- const stakeAmount = Math.round(price * stakeRate * 100) / 100;
468
- // 检查卖家是否有足够余额质押
469
- const wallet = db
470
- .prepare('SELECT * FROM wallets WHERE user_id = ?')
471
- .get(user.id);
472
- if (wallet.balance < stakeAmount) {
2532
+ // 【Verifier 申请】资格查询
2533
+ if (action === 'eligibility') {
2534
+ return await pwaApi('GET', '/verifier/eligibility', apiKey);
2535
+ }
2536
+ // 【Verifier 状态】tier/quota/stake
2537
+ if (action === 'verifier_status') {
2538
+ return await pwaApi('GET', '/verifier/status', apiKey);
2539
+ }
2540
+ // 【Verifier 申请】提交
2541
+ if (action === 'apply') {
2542
+ return await pwaApi('POST', '/verifier/apply', apiKey);
2543
+ }
2544
+ // 【Verifier 申请】撤回
2545
+ if (action === 'withdraw_application') {
2546
+ return await pwaApi('POST', '/verifier/withdraw-application', apiKey);
2547
+ }
2548
+ // 【Verifier】被暂停后申诉
2549
+ if (action === 'appeal') {
2550
+ if (!args.reason)
2551
+ return { error: '请提供 reason(申诉理由,≤500 字)' };
2552
+ return await pwaApi('POST', '/verifier/appeal', apiKey, {
2553
+ reason: args.reason,
2554
+ task_id: args.task_id,
2555
+ });
2556
+ }
2557
+ return { error: `未知 action:${action}。Valid: create | view | mine | submit_seller_evidence | available | vote | eligibility | verifier_status | apply | withdraw_application | appeal` };
2558
+ }
2559
+ // ─── Skill 市场处理 ────────────────────────────────────────────
2560
+ function handleSkill(args) {
2561
+ const action = args.action;
2562
+ // ── 浏览 Skill 市场 ────────────────────────────────────────
2563
+ if (action === 'list') {
2564
+ let userId;
2565
+ if (args.api_key) {
2566
+ const a = requireAuth(db, args.api_key);
2567
+ if (!('error' in a))
2568
+ userId = a.user.id;
2569
+ }
2570
+ const skills = listSkills(db, {
2571
+ skillType: args.skill_type,
2572
+ query: args.query,
2573
+ subscriberId: userId,
2574
+ limit: 20,
2575
+ });
473
2576
  return {
474
- error: `余额不足:上架此商品需要质押 ${stakeAmount} WAZ,你的余额为 ${wallet.balance} WAZ`,
2577
+ total: skills.length,
2578
+ skill_types: Object.entries(SKILL_TYPE_META).map(([k, v]) => ({ type: k, label: v.label, icon: v.icon, description: v.description })),
2579
+ skills: skills.map(formatSkillForAgent),
475
2580
  };
476
2581
  }
477
- const id = generateId('prd');
478
- db.prepare(`
479
- INSERT INTO products (id, seller_id, title, description, price, stock, category, stake_amount)
480
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
481
- `).run(id, user.id, args.title, args.description, price, args.stock ?? 1, args.category ?? null, stakeAmount);
482
- // 扣除质押金额
483
- db.prepare(`
484
- UPDATE wallets SET balance = balance - ?, staked = staked + ? WHERE user_id = ?
485
- `).run(stakeAmount, stakeAmount, user.id);
486
- const rep = getReputation(db, user.id);
487
- return {
488
- success: true,
489
- product_id: id,
490
- title: args.title,
491
- price: `${price} WAZ`,
492
- stake_locked: `${stakeAmount} WAZ(质押保证金,交易完成后返还)`,
493
- stake_rate: stakeDiscount > 0 ? `${(stakeRate * 100).toFixed(0)}%(声誉折扣 -${(stakeDiscount * 100).toFixed(0)}%,原 15%)` : '15%',
494
- reputation_level: rep.level.label,
495
- status: 'active(买家现在可以搜索到这件商品)',
496
- };
497
- }
498
- function handlePlaceOrder(args) {
2582
+ // 以下操作需要身份验证
499
2583
  const auth = requireAuth(db, args.api_key);
500
2584
  if ('error' in auth)
501
2585
  return auth;
502
2586
  const { user } = auth;
503
- if (user.role !== 'buyer') {
504
- return { error: `只有 buyer 角色可以下单,你的角色是:${user.role}` };
2587
+ // ── 发布 Skill ────────────────────────────────────────────
2588
+ if (action === 'publish') {
2589
+ if (!args.name)
2590
+ return { error: '请填写 Skill 名称(name)' };
2591
+ if (!args.description)
2592
+ return { error: '请填写 Skill 描述(description)' };
2593
+ if (!args.skill_type)
2594
+ return { error: '请选择 Skill 类型(skill_type)' };
2595
+ const skill = publishSkill(db, {
2596
+ sellerId: user.id,
2597
+ name: args.name,
2598
+ description: args.description,
2599
+ category: args.category,
2600
+ skillType: args.skill_type,
2601
+ config: args.config,
2602
+ });
2603
+ const meta = SKILL_TYPE_META[skill.skill_type];
2604
+ return {
2605
+ success: true,
2606
+ skill_id: skill.id,
2607
+ message: `✅ Skill 「${skill.name}」已发布到 WebAZ Skill 市场!买家 Agent 现在可以订阅它。`,
2608
+ type: `${meta.icon} ${meta.label}`,
2609
+ tip: 'auto_accept Skill 发布后,买家新订单将自动被接受(无需手动操作)',
2610
+ };
505
2611
  }
506
- const product = db
507
- .prepare("SELECT p.*, u.name as seller_name, u.id as seller_uid FROM products p JOIN users u ON p.seller_id = u.id WHERE p.id = ? AND p.status = 'active'")
508
- .get(args.product_id);
509
- if (!product) {
510
- return { error: `商品不存在或已下架:${args.product_id}` };
2612
+ // ── 订阅 Skill ────────────────────────────────────────────
2613
+ if (action === 'subscribe') {
2614
+ if (!args.skill_id)
2615
+ return { error: '请提供 skill_id' };
2616
+ const result = subscribeSkill(db, user.id, args.skill_id, args.config);
2617
+ return { ...result, skill_id: args.skill_id };
511
2618
  }
512
- const quantity = args.quantity ?? 1;
513
- if (product.stock < quantity) {
514
- return { error: `库存不足:当前库存 ${product.stock},你要购买 ${quantity}` };
2619
+ // ── 取消订阅 ──────────────────────────────────────────────
2620
+ if (action === 'unsubscribe') {
2621
+ if (!args.skill_id)
2622
+ return { error: '请提供 skill_id' };
2623
+ unsubscribeSkill(db, user.id, args.skill_id);
2624
+ return { success: true, message: '已取消订阅' };
515
2625
  }
516
- const totalAmount = product.price * quantity;
517
- const wallet = db
518
- .prepare('SELECT * FROM wallets WHERE user_id = ?')
519
- .get(user.id);
520
- if (wallet.balance < totalAmount) {
2626
+ // ── 我发布的 Skill ────────────────────────────────────────
2627
+ if (action === 'my_skills') {
2628
+ const skills = getMySkills(db, user.id);
521
2629
  return {
522
- error: `余额不足:订单金额 ${totalAmount} WAZ,你的余额 ${wallet.balance} WAZ`,
2630
+ total: skills.length,
2631
+ skills: skills.map(formatSkillForAgent),
2632
+ tip: skills.length === 0 ? '还没有发布任何 Skill。用 webaz_skill action=publish 发布你的第一个 Skill。' : undefined,
523
2633
  };
524
2634
  }
525
- const now = new Date();
526
- const orderId = generateId('ord');
527
- // 找推荐人
528
- let promoterId = null;
529
- if (args.promoter_api_key) {
530
- const promoter = db
531
- .prepare('SELECT id FROM users WHERE api_key = ?')
532
- .get(args.promoter_api_key);
533
- if (promoter)
534
- promoterId = promoter.id;
535
- }
536
- db.prepare(`
537
- INSERT INTO orders (
538
- id, product_id, buyer_id, seller_id, promoter_id,
539
- quantity, unit_price, total_amount, escrow_amount,
540
- status, shipping_address, notes,
541
- pay_deadline, accept_deadline, ship_deadline,
542
- pickup_deadline, delivery_deadline, confirm_deadline
543
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'created', ?, ?, ?, ?, ?, ?, ?, ?)
544
- `).run(orderId, product.id, user.id, product.seller_uid, promoterId, quantity, product.price, totalAmount, totalAmount, args.shipping_address, args.notes ?? null, addHours(now, 24), // 买家 24h 内必须付款
545
- addHours(now, 48), // 卖家 24h 内接单
546
- addHours(now, 120), // 卖家 72h 内发货
547
- addHours(now, 168), // 物流 48h 内揽收
548
- addHours(now, 336), // 物流 7 天内投递
549
- addHours(now, 408));
550
- // 扣除库存
551
- db.prepare('UPDATE products SET stock = stock - ? WHERE id = ?').run(quantity, product.id);
552
- // 模拟"付款":锁定买家余额
553
- db.prepare(`
554
- UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?
555
- `).run(totalAmount, totalAmount, user.id);
556
- // 直接进入 paid 状态(Phase 0 模拟支付)
557
- transition(db, orderId, 'paid', user.id, [], '模拟支付完成,资金已托管');
558
- notifyTransition(db, orderId, 'created', 'paid');
559
- // 检查卖家是否开启了 auto_accept Skill,若是则自动接单
560
- let autoAccepted = false;
561
- if (shouldAutoAccept(db, orderId)) {
562
- const sysUser = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
563
- const acceptResult = transition(db, orderId, 'accepted', sysUser.id, [], '⚡ auto_accept Skill 自动接单');
564
- if (acceptResult.success) {
565
- notifyTransition(db, orderId, 'paid', 'accepted');
566
- autoAccepted = true;
567
- }
2635
+ // ── 我订阅的 Skill ────────────────────────────────────────
2636
+ if (action === 'my_subs') {
2637
+ const skills = getMySubscriptions(db, user.id);
2638
+ return {
2639
+ total: skills.length,
2640
+ subscriptions: skills.map(formatSkillForAgent),
2641
+ tip: skills.length === 0 ? '还没有订阅任何 Skill。用 webaz_skill action=list 浏览市场。' : undefined,
2642
+ };
568
2643
  }
569
- return {
570
- success: true,
571
- order_id: orderId,
572
- product: product.title,
573
- seller: product.seller_name,
574
- quantity,
575
- total_amount: `${totalAmount} WAZ(已托管,等待交易完成后自动结算)`,
576
- status: autoAccepted ? 'accepted' : 'paid',
577
- auto_accepted: autoAccepted || undefined,
578
- next: autoAccepted
579
- ? '⚡ 卖家已开启自动接单,订单已立即接受!等待卖家发货。'
580
- : '等待卖家 24 小时内接单。卖家超时不接单将自动退款。',
581
- track: `用 webaz_get_status 查看订单进展`,
582
- };
2644
+ return { error: `未知 action:${action}。可选:list, publish, subscribe, unsubscribe, my_skills, my_subs` };
583
2645
  }
584
- function handleUpdateOrder(args) {
585
- const auth = requireAuth(db, args.api_key);
586
- if ('error' in auth)
587
- return auth;
588
- const { user } = auth;
589
- const orderId = args.order_id;
590
- const action = args.action;
591
- const notes = args.notes ?? '';
592
- const evidenceDesc = args.evidence_description ?? '';
593
- // 验证订单存在且该用户是参与方
594
- let order = db
595
- .prepare('SELECT * FROM orders WHERE id = ?')
596
- .get(orderId);
597
- if (!order)
598
- return { error: `订单不存在:${orderId}` };
599
- // 物流首次操作:先绑定再做参与方检查
600
- if ((action === 'pickup' || action === 'transit') &&
601
- !order.logistics_id &&
602
- user.role === 'logistics') {
603
- db.prepare('UPDATE orders SET logistics_id = ? WHERE id = ?').run(user.id, orderId);
604
- order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
2646
+ // P0-1 修复(QA 轮 5 抓到):旧版只要 name → 直接吐所有匹配账户 api_key 明文,且无 rate limit。
2647
+ // 新版:handle + permanent_code 双因素 + redact + in-memory rate limit (5/hr per handle)
2648
+ // 完整 api_key 不在 MCP 暴露 — 走 PWA + Passkey 才给(Iron-Rule,跟 claim_verify / arbitrate 同模型)。
2649
+ const MYKEY_RATE_LIMIT = new Map();
2650
+ const MYKEY_MAX_PER_HOUR = 5;
2651
+ function redactKey(key) {
2652
+ if (!key || key.length < 12)
2653
+ return '***';
2654
+ return `${key.slice(0, 8)}***${key.slice(-4)}`;
2655
+ }
2656
+ function handleMyKey(args) {
2657
+ const handle = args.handle?.trim();
2658
+ const permaCode = args.permanent_code?.trim()?.toUpperCase();
2659
+ if (!handle || !permaCode) {
2660
+ return {
2661
+ error: 'invalid_request',
2662
+ error_code: 'MISSING_PARAMS',
2663
+ message: 'Both handle and permanent_code required. Check the registration response — handle may have been auto-suffixed if requested name was taken (look for handle_modified=true).',
2664
+ };
605
2665
  }
606
- const isParticipant = order.buyer_id === user.id ||
607
- order.seller_id === user.id ||
608
- order.logistics_id === user.id;
609
- if (!isParticipant && user.role !== 'arbitrator') {
610
- return { error: '你不是这笔订单的参与方,无法操作' };
2666
+ const now = Date.now();
2667
+ const entry = MYKEY_RATE_LIMIT.get(handle) || { count: 0, resetAt: now + 3600_000 };
2668
+ if (now > entry.resetAt) {
2669
+ entry.count = 0;
2670
+ entry.resetAt = now + 3600_000;
611
2671
  }
612
- // action 状态映射
613
- const actionMap = {
614
- accept: 'accepted',
615
- ship: 'shipped',
616
- pickup: 'picked_up',
617
- transit: 'in_transit',
618
- deliver: 'delivered',
619
- confirm: 'confirmed',
620
- dispute: 'disputed',
621
- };
622
- const toStatus = actionMap[action];
623
- if (!toStatus)
624
- return { error: `未知操作:${action}` };
625
- // 如果有证据描述,先创建证据记录
626
- const evidenceIds = [];
627
- if (evidenceDesc) {
628
- const evidenceId = generateId('evt');
629
- db.prepare(`
630
- INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash)
631
- VALUES (?, ?, ?, 'description', ?, ?)
632
- `).run(evidenceId, orderId, user.id, evidenceDesc, `hash_${Date.now()}`);
633
- evidenceIds.push(evidenceId);
2672
+ if (entry.count >= MYKEY_MAX_PER_HOUR) {
2673
+ return {
2674
+ error: 'rate_limit_exceeded',
2675
+ error_code: 'RATE_LIMIT',
2676
+ message: `Too many recovery attempts for this handle. Try again in ${Math.ceil((entry.resetAt - now) / 60_000)} minutes.`,
2677
+ retry_after_seconds: Math.ceil((entry.resetAt - now) / 1000),
2678
+ };
634
2679
  }
635
- const result = transition(db, orderId, toStatus, user.id, evidenceIds, notes);
636
- if (!result.success) {
637
- return { error: result.error };
2680
+ entry.count++;
2681
+ MYKEY_RATE_LIMIT.set(handle, entry);
2682
+ const user = db.prepare(`SELECT id, name, handle, role, roles, api_key FROM users WHERE handle = ? AND permanent_code = ? AND id != 'sys_protocol'`).get(handle, permaCode);
2683
+ if (!user) {
2684
+ return {
2685
+ error: 'invalid_credentials',
2686
+ error_code: 'AUTH_FAILED',
2687
+ message: 'No account matches handle + permanent_code. Handle is case-sensitive; permanent_code is uppercase.',
2688
+ attempts_remaining: MYKEY_MAX_PER_HOUR - entry.count,
2689
+ };
638
2690
  }
639
- // 通知相关参与方(L2-6)
640
- notifyTransition(db, orderId, order.status, toStatus);
641
- // 如果是 dispute,写入 disputes 表(L3-1)
642
- if (toStatus === 'disputed') {
643
- const disputeResult = createDispute(db, orderId, user.id, notes || evidenceDesc || '买家发起争议', evidenceIds);
644
- if (disputeResult.success) {
645
- return {
646
- success: true,
647
- new_status: 'disputed',
648
- dispute_id: disputeResult.disputeId,
649
- message: disputeResult.message,
650
- respond_deadline: disputeResult.respondDeadline,
651
- next: `用 webaz_dispute action=view dispute_id=${disputeResult.disputeId} 查看争议详情`,
652
- };
653
- }
654
- // 争议记录写入失败不影响状态,仍返回成功
655
- return { success: true, new_status: 'disputed', message: '争议已发起,资金已冻结', warning: disputeResult.error };
2691
+ return {
2692
+ found: true,
2693
+ user_id: user.id,
2694
+ name: user.name,
2695
+ handle: user.handle,
2696
+ active_role: user.role,
2697
+ roles: JSON.parse(user.roles || JSON.stringify([user.role])),
2698
+ api_key_hint: redactKey(user.api_key),
2699
+ full_api_key_recovery: {
2700
+ via: 'PWA + Passkey',
2701
+ url: 'https://webaz.xyz/recover',
2702
+ note: 'For security, full api_key disclosure requires Passkey verification on PWA (Iron-Rule). This MCP response only confirms the account exists and shows a redacted hint to help you identify which account.',
2703
+ },
2704
+ next_step: 'If you cannot access PWA, use webaz_rotate_key to invalidate the lost key and get a new one (also Passkey-gated).',
2705
+ };
2706
+ }
2707
+ async function handleProfile(args) {
2708
+ const action = args.action;
2709
+ const apiKey = String(args.api_key || '');
2710
+ // 看他人公开主页 / 内容流
2711
+ if (action === 'view_user') {
2712
+ if (!args.user_id)
2713
+ return { error: 'user_id required' };
2714
+ if (!apiKey)
2715
+ return { error: 'api_key required(公开主页端点需鉴权)' };
2716
+ return await pwaApi('GET', '/users/' + encodeURIComponent(String(args.user_id)), apiKey);
656
2717
  }
657
- // 如果是 confirmed,自动触发结算
658
- if (toStatus === 'confirmed') {
659
- const sysUser = db
660
- .prepare("SELECT id FROM users WHERE id = 'sys_protocol'")
661
- .get();
662
- transition(db, orderId, 'completed', sysUser.id, [], '系统自动结算');
663
- settleOrder(db, orderId);
2718
+ if (action === 'feed') {
2719
+ if (!args.user_id || !args.feed)
2720
+ return { error: 'user_id + feed required' };
2721
+ const FEED_PATH = {
2722
+ secondhand: 'secondhand', auctions: 'auctions', reviews: 'reviews', products: 'products',
2723
+ shares: 'shareables', reputation: 'reputation', pv: 'pv-summary', liked: 'liked-shareables',
2724
+ };
2725
+ const seg = FEED_PATH[String(args.feed)];
2726
+ if (!seg)
2727
+ return { error: `unknown feed: ${args.feed}. options: ${Object.keys(FEED_PATH).join(', ')}` };
2728
+ // liked = owner-only,需自己的 api_key;其余公开(无 key 也可)
2729
+ return await pwaApi('GET', '/users/' + encodeURIComponent(String(args.user_id)) + '/' + seg, apiKey);
2730
+ }
2731
+ const auth = requireAuth(db, apiKey);
2732
+ if ('error' in auth)
2733
+ return auth;
2734
+ const { user } = auth;
2735
+ const roles = JSON.parse(user.roles || JSON.stringify([user.role]));
2736
+ if (action === 'view') {
2737
+ const wallet = db.prepare('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?').get(user.id);
2738
+ // P0-2 修复(QA 轮 5):旧版直接回显完整 api_key 明文。Agent 上下文 / 日志 / 截屏一旦留痕等同于凭据泄漏。
2739
+ // 改返 redact 过的 hint,足够 agent 确认"我是哪个账户",不再让完整 key 进 conversation transcript。
664
2740
  return {
665
- success: true,
666
- new_status: 'completed',
667
- message: '确认收货成功!资金已自动分配给各参与方。',
668
- detail: `用 webaz_wallet 查看你的收益`,
2741
+ id: user.id,
2742
+ name: user.name,
2743
+ handle: user.handle,
2744
+ active_role: user.role,
2745
+ roles,
2746
+ api_key_hint: redactKey(user.api_key),
2747
+ wallet,
2748
+ tip: 'Use add_role to add a new role, switch_role to change your active role. api_key is no longer returned in plaintext — use webaz_rotate_key if you need a new key.',
669
2749
  };
670
2750
  }
671
- const statusMessages = {
672
- accepted: '接单成功!请在承诺时间内发货,超时将自动判违约。',
673
- shipped: '发货成功!物流方 48 小时内需要完成揽收。',
674
- picked_up: '揽收确认!请尽快安排运输。',
675
- in_transit: '运输状态已更新。',
676
- delivered: '投递确认!买家 72 小时内确认收货,超时自动确认。',
677
- disputed: '争议已发起,资金冻结,等待仲裁员介入。',
678
- };
2751
+ const validRoles = ['buyer', 'seller', 'logistics', 'arbitrator'];
2752
+ const role = args.role;
2753
+ if (action === 'add_role') {
2754
+ if (!validRoles.includes(role))
2755
+ return { error: `Invalid role. Options: ${validRoles.join(', ')}` };
2756
+ if (roles.includes(role))
2757
+ return { error: `You already have the "${role}" role` };
2758
+ roles.push(role);
2759
+ db.prepare("UPDATE users SET roles = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(roles), user.id);
2760
+ return { success: true, active_role: user.role, roles, message: `Role "${role}" added. Use switch_role to activate it.` };
2761
+ }
2762
+ if (action === 'switch_role') {
2763
+ if (!validRoles.includes(role))
2764
+ return { error: `Invalid role. Options: ${validRoles.join(', ')}` };
2765
+ if (!roles.includes(role))
2766
+ return { error: `You don't have the "${role}" role yet. Use add_role first.` };
2767
+ db.prepare("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?").run(role, user.id);
2768
+ return { success: true, active_role: role, roles, message: `Switched to "${role}" mode.` };
2769
+ }
2770
+ return { error: `Unknown action: ${action}. Options: view, add_role, switch_role, view_user, feed` };
2771
+ }
2772
+ // ─── api_key 生命周期(Iron-Rule: 真正吊销/轮换走 PWA + Passkey) ─────
2773
+ // QA 轮 5 抓到 P0:旧版没任何 logout/revoke/rotate 工具,api_key 一旦泄漏永久接管。
2774
+ // 这里给 agent 两个"声明意图"工具:MCP 验 api_key 合法 → 返回 PWA URL 让用户 Passkey 二次确认。
2775
+ // 真正改 DB 的动作放 PWA endpoint,跟 claim_verify / arbitrate 同模型。
2776
+ function handleRevokeKey(args) {
2777
+ const auth = requireAuth(db, args.api_key);
2778
+ if ('error' in auth)
2779
+ return auth;
2780
+ const { user } = auth;
2781
+ const reason = (args.reason || 'unspecified').trim().slice(0, 100);
679
2782
  return {
680
- success: true,
681
- new_status: result.newStatus,
682
- message: statusMessages[toStatus] ?? '状态已更新',
683
- history_record: result.historyId,
2783
+ success: false,
2784
+ requires_human_action: true,
2785
+ iron_rule: 'API key revocation is a destructive, irreversible operation. Agent cannot execute unilaterally. Same gating as claim_verify and arbitrate.',
2786
+ action: 'revoke_api_key',
2787
+ user_id: user.id,
2788
+ api_key_hint: redactKey(user.api_key),
2789
+ reason_logged: reason,
2790
+ next_step: {
2791
+ via: 'PWA + Passkey',
2792
+ url: `https://webaz.xyz/revoke?user=${encodeURIComponent(user.id)}`,
2793
+ instructions: '1) Open URL in browser 2) Sign in 3) Confirm with Passkey 4) Click "Revoke". After confirm the old api_key returns 401 on all tools.',
2794
+ warning: 'After revoke you cannot call any auth\'d tool until you have a new api_key. Use webaz_rotate_key instead for atomic invalidate + re-issue.',
2795
+ },
684
2796
  };
685
2797
  }
686
- function handleGetStatus(args) {
2798
+ function handleRotateKey(args) {
687
2799
  const auth = requireAuth(db, args.api_key);
688
2800
  if ('error' in auth)
689
2801
  return auth;
690
- const statusInfo = getOrderStatus(db, args.order_id);
691
- if (!statusInfo)
692
- return { error: `订单不存在:${args.order_id}` };
693
- const { order, history, currentResponsible, activeDeadline, isOverdue } = statusInfo;
2802
+ const { user } = auth;
2803
+ const reason = (args.reason || 'rotation').trim().slice(0, 100);
694
2804
  return {
695
- order_id: order.id,
696
- current_status: order.status,
697
- current_responsible: currentResponsible
698
- ? `${currentResponsible}(当前应由此角色操作)`
699
- : '无(等待系统处理)',
700
- deadline: activeDeadline
701
- ? {
702
- field: activeDeadline.field,
703
- time: activeDeadline.deadline,
704
- overdue: isOverdue ? '⚠️ 已超时!协议将自动判责' : '未超时',
705
- }
706
- : null,
707
- history: history.map((h) => ({
708
- from: h.from_status,
709
- to: h.to_status,
710
- by: `${h.actor_name}(${h.actor_role_name})`,
711
- at: h.created_at,
712
- evidence_count: JSON.parse(h.evidence_ids || '[]').length,
713
- notes: h.notes,
714
- })),
2805
+ success: false,
2806
+ requires_human_action: true,
2807
+ iron_rule: 'API key rotation requires Passkey verification (Iron-Rule). MCP registers intent only.',
2808
+ action: 'rotate_api_key',
2809
+ user_id: user.id,
2810
+ old_api_key_hint: redactKey(user.api_key),
2811
+ reason_logged: reason,
2812
+ next_step: {
2813
+ via: 'PWA + Passkey',
2814
+ url: `https://webaz.xyz/rotate?user=${encodeURIComponent(user.id)}`,
2815
+ instructions: '1) Open URL 2) Sign in 3) Confirm with Passkey 4) PWA returns new api_key — copy immediately, shown once. Old key invalidated atomically with new key issuance.',
2816
+ },
715
2817
  };
716
2818
  }
717
- function handleWallet(args) {
2819
+ // ─── 推广 / 双轨 (Tokenomics) ───────────────────────────────────
2820
+ function handleReferral(args) {
718
2821
  const auth = requireAuth(db, args.api_key);
719
2822
  if ('error' in auth)
720
2823
  return auth;
721
2824
  const { user } = auth;
722
- const wallet = db
723
- .prepare('SELECT * FROM wallets WHERE user_id = ?')
724
- .get(user.id);
725
- if (!wallet)
726
- return { error: '钱包不存在' };
727
- const payouts = db
728
- .prepare('SELECT SUM(amount) as total FROM payouts WHERE recipient_id = ?')
729
- .get(user.id);
730
- const rep = getReputation(db, user.id);
731
- const nextLevel = ['new', 'trusted', 'quality', 'star', 'legend'];
732
- const nextIdx = nextLevel.indexOf(rep.level.key) + 1;
733
- const nextLevelDef = nextIdx < nextLevel.length ? { trusted: 200, quality: 800, star: 2000, legend: 5000 }[nextLevel[nextIdx]] : null;
2825
+ const userId = user.id;
2826
+ // 团队 L1/L2/L3
2827
+ const l1 = db.prepare("SELECT COUNT(*) as n FROM users WHERE sponsor_id = ?").get(userId).n;
2828
+ const l2 = db.prepare(`SELECT COUNT(*) as n FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)`).get(userId).n;
2829
+ const l3 = db.prepare(`SELECT COUNT(*) as n FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?))`).get(userId).n;
2830
+ // 累计佣金(按 level 聚合)
2831
+ const earnings = db.prepare(`SELECT level, COUNT(*) as orders, COALESCE(SUM(amount),0) as total FROM commission_records WHERE beneficiary_id = ? GROUP BY level`).all(userId);
2832
+ const byLevel = { 1: { orders: 0, total: 0 }, 2: { orders: 0, total: 0 }, 3: { orders: 0, total: 0 } };
2833
+ for (const r of earnings)
2834
+ byLevel[r.level] = { orders: r.orders, total: r.total };
2835
+ // 推土机 L1 资格判定
2836
+ const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
2837
+ const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
2838
+ const canL1 = override === 1 || (override === 0 && completed > 0);
2839
+ // 原子能双轨
2840
+ const me = db.prepare("SELECT total_left_pv, total_right_pv, left_child_id, right_child_id, placement_id, placement_side FROM users WHERE id = ?").get(userId);
2841
+ const score = db.prepare(`
2842
+ SELECT COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending,
2843
+ COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz
2844
+ FROM binary_score_records WHERE user_id = ?
2845
+ `).get(userId);
2846
+ const tiers = db.prepare("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC").all();
2847
+ const pair = Math.min(Number(me?.total_left_pv ?? 0), Number(me?.total_right_pv ?? 0));
2848
+ const nextTier = tiers.find(t => t.pv_threshold > pair);
734
2849
  return {
735
- user: user.name,
736
- role: user.role,
737
- balance: `${wallet.balance} WAZ(可用)`,
738
- staked: `${wallet.staked} WAZ(质押中,不可用)`,
739
- escrowed: `${wallet.escrowed} WAZ(托管中,交易完成后结算)`,
740
- total_earned: `${payouts.total ?? 0} WAZ(历史累计收益)`,
741
- reputation: {
742
- level: `${rep.level.icon} ${rep.level.label}`,
743
- total_points: rep.total_points,
744
- transactions_done: rep.transactions_done,
745
- disputes_won: rep.disputes_won,
746
- disputes_lost: rep.disputes_lost,
747
- violations: rep.violations,
748
- stake_discount: rep.level.stakeDiscount > 0 ? `-${(rep.level.stakeDiscount * 100).toFixed(0)}% 质押优惠` : '暂无(升级后享优惠)',
749
- next_level: nextLevelDef ? `距下一等级还需 ${nextLevelDef - rep.total_points} 分` : '已达最高等级!',
750
- recent_events: rep.recent_events.slice(0, 5).map(e => `${e.points > 0 ? '+' : ''}${e.points} ${e.reason}`),
2850
+ user_id: userId,
2851
+ name: user.name,
2852
+ base_referral_link: `/?ref=${userId}`, // 仅推土机
2853
+ region: user.region ?? 'global',
2854
+ permissions: {
2855
+ can_earn_l1_commission: canL1,
2856
+ completed_orders: completed,
2857
+ reason: canL1
2858
+ ? (override === 1 ? 'admin_grant' : 'verified_buyer')
2859
+ : 'need_completed_order — share PV-only link until verified',
2860
+ },
2861
+ team: { l1, l2, l3, total: l1 + l2 + l3 },
2862
+ earnings: {
2863
+ l1: byLevel[1], l2: byLevel[2], l3: byLevel[3],
2864
+ grand_total: byLevel[1].total + byLevel[2].total + byLevel[3].total,
2865
+ },
2866
+ binary: {
2867
+ left_invite_link: `/?ref=${userId}&side=left`,
2868
+ right_invite_link: `/?ref=${userId}&side=right`,
2869
+ platform_left: `/?placement=${userId}&side=left`, // 仅 PV 条线
2870
+ platform_right: `/?placement=${userId}&side=right`,
2871
+ total_left_pv: Number(me?.total_left_pv ?? 0),
2872
+ total_right_pv: Number(me?.total_right_pv ?? 0),
2873
+ pair_volume: pair,
2874
+ next_tier: nextTier ? { tier: nextTier.tier, pv_threshold: nextTier.pv_threshold, score_per_hit: nextTier.score_per_hit, pv_needed: nextTier.pv_threshold - pair } : null,
2875
+ score_pending: score.pending,
2876
+ waz_total_earned: score.settled_waz,
751
2877
  },
2878
+ tip: canL1
2879
+ ? 'Use webaz_share_link(product_id) to generate a product share link. Both 3-tier commission and PV tree will apply.'
2880
+ : 'Complete 1 purchase first, then your share link will earn 3-tier commission. Until then, your share builds PV tree only.',
752
2881
  };
753
2882
  }
754
- // ─── 通知处理 ─────────────────────────────────────────────────
755
- function handleNotifications(args) {
2883
+ function handleShareLink(args) {
756
2884
  const auth = requireAuth(db, args.api_key);
757
2885
  if ('error' in auth)
758
2886
  return auth;
759
2887
  const { user } = auth;
760
- const onlyUnread = args.unread === true;
761
- const notifs = getNotifications(db, user.id, onlyUnread, 30);
762
- const unread = getUnreadCount(db, user.id);
763
- if (args.mark_read) {
764
- markRead(db, user.id);
2888
+ const userId = user.id;
2889
+ const productId = args.product_id;
2890
+ const sideArg = args.side || 'auto';
2891
+ const product = db.prepare("SELECT id, title, price, commission_rate FROM products WHERE id = ? AND status='active'").get(productId);
2892
+ if (!product)
2893
+ return { error: '商品不存在或已下架' };
2894
+ let side = 'right';
2895
+ if (sideArg === 'left' || sideArg === 'right') {
2896
+ side = sideArg;
2897
+ }
2898
+ else {
2899
+ // auto = 与 PWA pickPreferredSide 对齐:尊重 placement_pref(team_count | pv_count)
2900
+ // 老版只看 total_left_pv vs total_right_pv,team_count 用户被错算
2901
+ const u = db.prepare("SELECT placement_pref, total_left_pv, total_right_pv, left_child_id, right_child_id FROM users WHERE id = ?")
2902
+ .get(userId);
2903
+ const pref = u?.placement_pref || 'team_count';
2904
+ if (pref === 'pv_count') {
2905
+ const since = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString().slice(0, 19).replace('T', ' ');
2906
+ const w = db.prepare(`SELECT COALESCE(SUM(consumed_left_pv),0) AS l, COALESCE(SUM(consumed_right_pv),0) AS r
2907
+ FROM binary_score_records WHERE user_id = ? AND created_at >= ?`)
2908
+ .get(userId, since);
2909
+ const leftPv = Number(u?.total_left_pv ?? 0) + Number(w.l);
2910
+ const rightPv = Number(u?.total_right_pv ?? 0) + Number(w.r);
2911
+ side = leftPv <= rightPv ? 'left' : 'right';
2912
+ }
2913
+ else {
2914
+ // team_count: 沿子链数下线人数(与 server.ts countSubtreeUsers 一致)
2915
+ const countLeg = (rootId, childField) => {
2916
+ let count = 0, current = rootId, safety = 10_000;
2917
+ while (safety-- > 0) {
2918
+ const row = db.prepare(`SELECT ${childField} FROM users WHERE id = ?`).get(current);
2919
+ const next = row?.[childField] || null;
2920
+ if (!next)
2921
+ break;
2922
+ count++;
2923
+ current = next;
2924
+ }
2925
+ return count;
2926
+ };
2927
+ const lCount = countLeg(userId, 'left_child_id');
2928
+ const rCount = countLeg(userId, 'right_child_id');
2929
+ side = lCount <= rCount ? 'left' : 'right';
2930
+ }
765
2931
  }
2932
+ const completed = db.prepare("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'").get(userId).n;
2933
+ const override = db.prepare("SELECT l1_share_override FROM users WHERE id = ?").get(userId)?.l1_share_override ?? 0;
2934
+ const canL1 = override === 1 || (override === 0 && completed > 0);
2935
+ const rate = Number(product.commission_rate ?? 0);
2936
+ const link = `/?ref=${userId}&side=${side}#order-product/${productId}`;
766
2937
  return {
767
- unread_count: unread,
768
- notifications: notifs.map(n => ({
769
- id: n.id,
770
- title: n.title,
771
- body: n.body,
772
- order_id: n.order_id,
773
- read: n.read === 1,
774
- time: n.created_at,
775
- })),
2938
+ product: { id: product.id, title: product.title, price: product.price, commission_rate: rate },
2939
+ share_link: link,
2940
+ full_url_hint: 'Prepend webaz.xyz (production) or http://localhost:3000 (local) to get the absolute URL',
2941
+ side,
2942
+ binary_explanation: `New user via this link → placed in your ${side === 'left' ? '🔵 left' : '🟢 right'} subtree (tail anchor)`,
2943
+ commission_eligibility: canL1
2944
+ ? `You will earn 3-tier commission: L1=${(rate * 0.70 * 100).toFixed(1)}% L2=${(rate * 0.20 * 100).toFixed(1)}% L3=${(rate * 0.10 * 100).toFixed(1)}% of sale price`
2945
+ : 'You are NOT verified yet (need 1 completed purchase). 3-tier commission will be skipped, but PV tree still builds.',
2946
+ next_steps: 'Share on TikTok / WeChat / Telegram. New user clicks → 30-day attribution window starts.',
776
2947
  };
777
2948
  }
778
- // ─── 争议处理 ─────────────────────────────────────────────────
779
- function handleDispute(args) {
2949
+ // ─── 黑名单 / 关注 / 雷达 / 默认地址 / shareables ─────────
2950
+ function handleBlocklist(args) {
780
2951
  const auth = requireAuth(db, args.api_key);
781
2952
  if ('error' in auth)
782
2953
  return auth;
783
2954
  const { user } = auth;
784
- const action = args.action;
785
- // ── 查看争议详情 ────────────────────────────────────────────
786
- if (action === 'view') {
787
- let dispute = args.dispute_id
788
- ? getDisputeDetails(db, args.dispute_id)
789
- : args.order_id
790
- ? getOrderDispute(db, args.order_id)
791
- : null;
792
- if (!dispute)
793
- return { error: '找不到争议记录,请提供 dispute_id 或 order_id' };
794
- const evidenceList = (orderId, uploaderRole) => db.prepare(`
795
- SELECT e.description, e.type, e.file_hash, e.created_at, u.name as uploader
796
- FROM evidence e JOIN users u ON e.uploader_id = u.id
797
- WHERE e.order_id = ? AND u.role = ?
798
- ORDER BY e.created_at ASC
799
- `).all(orderId, uploaderRole);
2955
+ const action = String(args.action || '');
2956
+ // 多形态识别:usr_xxx / VKSF9P / @handle
2957
+ const targetId = args.user_id ? mcpResolveUserRef(String(args.user_id)) : undefined;
2958
+ if (action === 'list') {
2959
+ const rows = db.prepare(`
2960
+ SELECT b.blocked_id, b.reason, b.created_at, u.name as blocked_name, u.role as blocked_role
2961
+ FROM user_blocklist b LEFT JOIN users u ON u.id = b.blocked_id
2962
+ WHERE b.blocker_id = ? ORDER BY b.created_at DESC
2963
+ `).all(user.id);
2964
+ return { blocked: rows };
2965
+ }
2966
+ if (!targetId)
2967
+ return { error: 'user_id required for block/unblock' };
2968
+ if (targetId === user.id)
2969
+ return { error: 'cannot block yourself' };
2970
+ if (targetId === 'sys_protocol')
2971
+ return { error: 'cannot block system account' };
2972
+ if (action === 'block') {
2973
+ const exists = db.prepare("SELECT 1 FROM users WHERE id = ?").get(targetId);
2974
+ if (!exists)
2975
+ return { error: 'target user not found' };
2976
+ const reason = (args.reason || '').slice(0, 200);
2977
+ db.prepare("INSERT OR IGNORE INTO user_blocklist (blocker_id, blocked_id, reason) VALUES (?, ?, ?)")
2978
+ .run(user.id, targetId, reason || null);
2979
+ return { ok: true, blocked: targetId };
2980
+ }
2981
+ if (action === 'unblock') {
2982
+ db.prepare("DELETE FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ?").run(user.id, targetId);
2983
+ return { ok: true, unblocked: targetId };
2984
+ }
2985
+ return { error: `unknown action: ${action}` };
2986
+ }
2987
+ function handleFollows(args) {
2988
+ const auth = requireAuth(db, args.api_key);
2989
+ if ('error' in auth)
2990
+ return auth;
2991
+ const { user } = auth;
2992
+ const action = String(args.action || '');
2993
+ // 多形态识别:usr_xxx / VKSF9P / @handle
2994
+ const targetId = args.user_id ? mcpResolveUserRef(String(args.user_id)) : undefined;
2995
+ if (action === 'list') {
2996
+ const followers = db.prepare(`
2997
+ SELECT u.id, u.name, u.role, f.created_at FROM follows f JOIN users u ON u.id = f.follower_id
2998
+ WHERE f.followee_id = ? ORDER BY f.created_at DESC LIMIT 100
2999
+ `).all(user.id);
3000
+ const following = db.prepare(`
3001
+ SELECT u.id, u.name, u.role, f.created_at FROM follows f JOIN users u ON u.id = f.followee_id
3002
+ WHERE f.follower_id = ? ORDER BY f.created_at DESC LIMIT 100
3003
+ `).all(user.id);
3004
+ return { followers, following };
3005
+ }
3006
+ if (!targetId)
3007
+ return { error: 'user_id required' };
3008
+ if (targetId === user.id)
3009
+ return { error: 'cannot self-follow' };
3010
+ if (action === 'follow') {
3011
+ const target = db.prepare("SELECT id FROM users WHERE id=?").get(targetId);
3012
+ if (!target)
3013
+ return { error: 'target not found' };
3014
+ // 尊重 blocklist:若 target 已 block 自己 → 拒绝(防绕过私聊封锁)
3015
+ const blockedByTarget = db.prepare("SELECT 1 FROM user_blocklist WHERE blocker_id = ? AND blocked_id = ? LIMIT 1").get(targetId, user.id);
3016
+ if (blockedByTarget)
3017
+ return { error: 'target has blocked you', error_code: 'BLOCKED_BY_TARGET' };
3018
+ db.prepare("INSERT OR IGNORE INTO follows (follower_id, followee_id) VALUES (?, ?)").run(user.id, targetId);
3019
+ return { ok: true, following: true };
3020
+ }
3021
+ if (action === 'unfollow') {
3022
+ db.prepare("DELETE FROM follows WHERE follower_id=? AND followee_id=?").run(user.id, targetId);
3023
+ return { ok: true, following: false };
3024
+ }
3025
+ if (action === 'status') {
3026
+ const isFollowing = !!db.prepare("SELECT 1 FROM follows WHERE follower_id=? AND followee_id=?").get(user.id, targetId);
3027
+ const followers = db.prepare("SELECT COUNT(*) as n FROM follows WHERE followee_id=?").get(targetId).n;
3028
+ const followingCount = db.prepare("SELECT COUNT(*) as n FROM follows WHERE follower_id=?").get(targetId).n;
3029
+ return { following: isFollowing, target_followers: followers, target_following_count: followingCount };
3030
+ }
3031
+ return { error: `unknown action: ${action}` };
3032
+ }
3033
+ function handleNearby(args) {
3034
+ const auth = requireAuth(db, args.api_key);
3035
+ if ('error' in auth)
3036
+ return auth;
3037
+ const { user } = auth;
3038
+ const action = String(args.action || '');
3039
+ if (action === 'set_location') {
3040
+ const lat = Number(args.lat), lng = Number(args.lng);
3041
+ if (!Number.isFinite(lat) || !Number.isFinite(lng))
3042
+ return { error: 'lat/lng required and numeric' };
3043
+ if (lat < -90 || lat > 90 || lng < -180 || lng > 180)
3044
+ return { error: 'lat/lng out of bounds' };
3045
+ const truncLat = Math.round(lat * 10) / 10;
3046
+ const truncLng = Math.round(lng * 10) / 10;
3047
+ db.prepare("UPDATE users SET geo_lat = ?, geo_lng = ?, geo_updated_at = datetime('now') WHERE id = ?")
3048
+ .run(truncLat, truncLng, user.id);
3049
+ return { ok: true, lat: truncLat, lng: truncLng, precision_deg: 0.1, approx_km: 11 };
3050
+ }
3051
+ if (action === 'clear_location') {
3052
+ db.prepare("UPDATE users SET geo_lat = NULL, geo_lng = NULL, geo_updated_at = NULL WHERE id = ?").run(user.id);
3053
+ return { ok: true };
3054
+ }
3055
+ if (action === 'query') {
3056
+ const u = db.prepare("SELECT geo_lat, geo_lng FROM users WHERE id = ?").get(user.id);
3057
+ if (u?.geo_lat == null || u?.geo_lng == null)
3058
+ return { has_location: false, hint: 'call set_location first' };
3059
+ const lat = u.geo_lat, lng = u.geo_lng;
3060
+ const K = 3;
3061
+ // 防指纹:count < K 时返回 null(不暴露具体小数字),与 topProducts 一致
3062
+ const rawActive24 = db.prepare(`SELECT COUNT(DISTINCT o.buyer_id) as n FROM orders o JOIN users u ON u.id = o.buyer_id WHERE u.geo_lat = ? AND u.geo_lng = ? AND o.status = 'completed' AND o.updated_at > datetime('now', '-1 day')`).get(lat, lng).n;
3063
+ const rawActive7 = db.prepare(`SELECT COUNT(DISTINCT o.buyer_id) as n FROM orders o JOIN users u ON u.id = o.buyer_id WHERE u.geo_lat = ? AND u.geo_lng = ? AND o.status = 'completed' AND o.updated_at > datetime('now', '-7 day')`).get(lat, lng).n;
3064
+ const active24 = rawActive24 >= K ? rawActive24 : null;
3065
+ const active7 = rawActive7 >= K ? rawActive7 : null;
3066
+ const topProducts = db.prepare(`
3067
+ SELECT p.id, p.title, p.price, COUNT(DISTINCT o.buyer_id) as buyers
3068
+ FROM orders o JOIN users u ON u.id = o.buyer_id JOIN products p ON p.id = o.product_id
3069
+ WHERE u.geo_lat = ? AND u.geo_lng = ? AND o.status = 'completed' AND o.updated_at > datetime('now', '-1 day')
3070
+ GROUP BY p.id HAVING buyers >= ? ORDER BY buyers DESC LIMIT 10
3071
+ `).all(lat, lng, K);
800
3072
  return {
801
- dispute_id: dispute.id,
802
- order_id: dispute.order_id,
803
- status: dispute.status,
804
- initiator: `${dispute.initiator_name}(${dispute.initiator_role})`,
805
- defendant: `${dispute.defendant_name}(${dispute.defendant_role})`,
806
- reason: dispute.reason,
807
- respond_deadline: dispute.respond_deadline,
808
- arbitrate_deadline: dispute.arbitrate_deadline,
809
- plaintiff_evidence: evidenceList(dispute.order_id, dispute.initiator_role),
810
- defendant_notes: dispute.defendant_notes ?? '(被诉方尚未提交回应)',
811
- defendant_evidence: JSON.parse(dispute.defendant_evidence_ids || '[]'),
812
- ruling: dispute.ruling_type
813
- ? { type: dispute.ruling_type, refund_amount: dispute.refund_amount, reason: dispute.verdict_reason }
814
- : null,
815
- resolved_at: dispute.resolved_at,
3073
+ has_location: true,
3074
+ cell: { lat, lng, precision_deg: 0.1, approx_km: 11 },
3075
+ k_threshold: K,
3076
+ active_users_24h: active24,
3077
+ active_users_7d: active7,
3078
+ top_products_24h: topProducts,
816
3079
  };
817
3080
  }
818
- // ── 仲裁员查看所有待处理争议 ───────────────────────────────
819
- if (action === 'list_open') {
820
- if (user.role !== 'arbitrator') {
821
- return { error: '只有仲裁员可以查看所有待处理争议' };
3081
+ return { error: `unknown action: ${action}` };
3082
+ }
3083
+ function handleDefaultAddress(args) {
3084
+ const auth = requireAuth(db, args.api_key);
3085
+ if ('error' in auth)
3086
+ return auth;
3087
+ const { user } = auth;
3088
+ const action = String(args.action || '');
3089
+ if (action === 'read') {
3090
+ const u = db.prepare("SELECT default_address_text, default_address_region FROM users WHERE id = ?").get(user.id);
3091
+ return {
3092
+ address_text: u?.default_address_text || null,
3093
+ address_region: u?.default_address_region || null,
3094
+ hint: u?.default_address_text ? null : 'No default set. Setting it auto-filters unshippable products in search.',
3095
+ };
3096
+ }
3097
+ if (action === 'set') {
3098
+ // QA 轮 10.2-B P1 修复:旧版无校验,传不对的字段(recipient/line1/city 等)silently 写 NULL,
3099
+ // 返回 ok: true 但 text/region 都是 null —— 元规则 #4 不撒谎被违反。
3100
+ // 修:text 必填校验;返回里加 success/stored 字段更明确。
3101
+ const text = (args.text || '').trim().slice(0, 200);
3102
+ const region = (args.region || '').trim().slice(0, 40);
3103
+ if (!text) {
3104
+ return {
3105
+ error: 'missing_text',
3106
+ error_code: 'TEXT_REQUIRED',
3107
+ message: 'webaz_default_address action=set 需要 "text" 字段(自由格式地址字符串,≤200 字符)。不接受 structured 字段如 recipient/line1/city。如要 region 联动过滤 unshippable,请同时传 "region" 字段(如 "global" / "china" / "SG")。',
3108
+ valid_params: { text: 'required', region: 'optional' },
3109
+ };
822
3110
  }
823
- const disputes = getOpenDisputes(db);
3111
+ db.prepare("UPDATE users SET default_address_text = ?, default_address_region = ?, updated_at = datetime('now') WHERE id = ?")
3112
+ .run(text, region || null, user.id);
824
3113
  return {
825
- open_count: disputes.length,
826
- disputes: disputes.map(d => ({
827
- dispute_id: d.id,
828
- order_id: d.order_id,
829
- status: d.status,
830
- initiator: `${d.initiator_name}(${d.initiator_role})`,
831
- defendant: `${d.defendant_name}(${d.defendant_role})`,
832
- reason: d.reason,
833
- amount: `${d.total_amount} WAZ`,
834
- respond_deadline: d.respond_deadline,
835
- arbitrate_deadline: d.arbitrate_deadline,
836
- created_at: d.created_at,
837
- }))
3114
+ success: true,
3115
+ stored: { text, region: region || null },
3116
+ hint: 'webaz_search 现在会自动按 region 过滤 unshippable 商品;webaz_rfq create 不传 shipping_address 时会 fallback 到此地址',
3117
+ };
3118
+ }
3119
+ return { error: `unknown action: ${action}` };
3120
+ }
3121
+ function handleShareables(args) {
3122
+ const auth = requireAuth(db, args.api_key);
3123
+ if ('error' in auth)
3124
+ return auth;
3125
+ const { user } = auth;
3126
+ const action = String(args.action || '');
3127
+ if (action === 'list_mine') {
3128
+ const rows = db.prepare(`
3129
+ SELECT s.*, p.title as product_title FROM shareables s
3130
+ LEFT JOIN products p ON p.id = s.related_product_id
3131
+ WHERE s.owner_id = ? AND s.status != 'removed' ORDER BY s.created_at DESC LIMIT 100
3132
+ `).all(user.id);
3133
+ return { shareables: rows };
3134
+ }
3135
+ if (action === 'by_product') {
3136
+ const pid = args.related_product_id;
3137
+ if (!pid)
3138
+ return { error: 'related_product_id required' };
3139
+ const rows = db.prepare(`
3140
+ SELECT s.*, u.name as owner_name FROM shareables s LEFT JOIN users u ON u.id = s.owner_id
3141
+ WHERE s.related_product_id = ? AND s.status = 'active'
3142
+ ORDER BY s.click_count DESC, s.created_at DESC LIMIT 20
3143
+ `).all(pid);
3144
+ return { shareables: rows };
3145
+ }
3146
+ if (action === 'by_anchor') {
3147
+ const anchor = args.related_anchor;
3148
+ if (!anchor)
3149
+ return { error: 'related_anchor required' };
3150
+ const rows = db.prepare(`
3151
+ SELECT s.*, u.name as owner_name FROM shareables s LEFT JOIN users u ON u.id = s.owner_id
3152
+ WHERE s.related_anchor = ? AND s.status = 'active' ORDER BY s.created_at DESC LIMIT 50
3153
+ `).all(anchor);
3154
+ return { shareables: rows };
3155
+ }
3156
+ if (action === 'add') {
3157
+ const url = (args.external_url || '').trim();
3158
+ const product_id = args.related_product_id;
3159
+ const anchor = args.related_anchor;
3160
+ if (!url)
3161
+ return { error: 'external_url required' };
3162
+ if (!product_id && !anchor)
3163
+ return { error: 'related_product_id or related_anchor required' };
3164
+ if (url.length > 500)
3165
+ return { error: 'external_url too long (max 500)', error_code: 'URL_TOO_LONG' };
3166
+ if (!/^https?:\/\//i.test(url))
3167
+ return { error: 'external_url must be http(s)://', error_code: 'URL_SCHEME_INVALID' };
3168
+ const todayCount = db.prepare(`SELECT COUNT(*) as n FROM shareables WHERE owner_id = ? AND created_at > datetime('now', '-1 day')`).get(user.id).n;
3169
+ if (todayCount >= 10)
3170
+ return { error: 'daily limit 10 reached' };
3171
+ // 全局唯一:同 URL 已被任何用户认领 → 归因模糊,拒绝(首认领者拿)
3172
+ const globalDup = db.prepare(`SELECT id, owner_id FROM shareables WHERE external_url = ? AND status != 'removed' LIMIT 1`).get(url);
3173
+ if (globalDup) {
3174
+ if (globalDup.owner_id === user.id)
3175
+ return { error: 'duplicate URL exists', existing_id: globalDup.id, error_code: 'DUP_OWN' };
3176
+ return { error: 'URL already claimed by another user (attribution conflict)', error_code: 'URL_CLAIMED' };
3177
+ }
3178
+ let type = 'external_url', platform = 'unknown';
3179
+ if (/youtube\.com|youtu\.be/i.test(url)) {
3180
+ type = 'external_youtube';
3181
+ platform = 'youtube';
3182
+ }
3183
+ else if (/tiktok\.com/i.test(url)) {
3184
+ type = 'external_tiktok';
3185
+ platform = 'tiktok';
3186
+ }
3187
+ else if (/xiaohongshu|xhslink/i.test(url)) {
3188
+ type = 'external_xhs';
3189
+ platform = 'xiaohongshu';
3190
+ }
3191
+ else if (/bilibili\.com/i.test(url)) {
3192
+ type = 'external_bilibili';
3193
+ platform = 'bilibili';
3194
+ }
3195
+ else if (/instagram\.com/i.test(url)) {
3196
+ type = 'external_ig';
3197
+ platform = 'instagram';
3198
+ }
3199
+ else if (/twitter\.com|x\.com/i.test(url)) {
3200
+ type = 'external_twitter';
3201
+ platform = 'twitter';
3202
+ }
3203
+ const id = generateId('shr');
3204
+ const title = (args.title || '').slice(0, 100) || null;
3205
+ const description = (args.description || '').slice(0, 200) || null;
3206
+ db.prepare(`INSERT INTO shareables (id, owner_id, type, external_url, external_platform, title, description, related_product_id, related_anchor) VALUES (?,?,?,?,?,?,?,?,?)`)
3207
+ .run(id, user.id, type, url, platform, title, description, product_id || null, anchor || null);
3208
+ return { ok: true, id, type, platform };
3209
+ }
3210
+ if (action === 'delete') {
3211
+ const id = args.shareable_id;
3212
+ if (!id)
3213
+ return { error: 'shareable_id required' };
3214
+ const row = db.prepare("SELECT owner_id FROM shareables WHERE id = ?").get(id);
3215
+ if (!row || row.owner_id !== user.id)
3216
+ return { error: 'not owner or not found' };
3217
+ db.prepare(`UPDATE shareables SET status = 'removed', updated_at = datetime('now') WHERE id = ?`).run(id);
3218
+ return { ok: true, deleted: id };
3219
+ }
3220
+ return { error: `unknown action: ${action}` };
3221
+ }
3222
+ // ─── P3 RFQ / bid / chat / auto_bid(HTTP 转发到 PWA,复用所有校验+状态机)────
3223
+ const PWA_API_BASE = process.env.WEBAZ_PWA_API_BASE || 'http://localhost:3000/api';
3224
+ async function pwaApi(method, path, apiKey, body) {
3225
+ const opts = {
3226
+ method,
3227
+ headers: {
3228
+ 'Content-Type': 'application/json',
3229
+ Authorization: `Bearer ${apiKey}`,
3230
+ },
3231
+ };
3232
+ if (body !== undefined)
3233
+ opts.body = JSON.stringify(body);
3234
+ const url = PWA_API_BASE + path;
3235
+ try {
3236
+ const r = await fetch(url, opts);
3237
+ // QA 轮 13 P1:检测 PWA 返回 HTML (SPA 兜底 fallback 404 时)
3238
+ // 之前直接 r.json() 抛 SyntaxError → agent 收到 "Unexpected token <" 无法 structurally 处理
3239
+ const ct = r.headers.get('content-type') || '';
3240
+ if (!ct.includes('application/json')) {
3241
+ const text = await r.text();
3242
+ return {
3243
+ error: `PWA upstream returned non-JSON (likely 404 SPA fallback or routing error)`,
3244
+ error_code: 'PWA_UPSTREAM_NOT_JSON',
3245
+ status: r.status,
3246
+ url: path,
3247
+ body_preview: text.slice(0, 200),
3248
+ };
3249
+ }
3250
+ return await r.json();
3251
+ }
3252
+ catch (e) {
3253
+ return {
3254
+ error: `PWA API unreachable: ${e.message}`,
3255
+ error_code: 'PWA_UNREACHABLE',
3256
+ url: path,
838
3257
  };
839
3258
  }
840
- // ── 被诉方提交反驳 ──────────────────────────────────────────
841
- if (action === 'respond') {
842
- if (!args.dispute_id)
843
- return { error: '请提供 dispute_id' };
844
- // 如有证据描述,先创建证据记录
845
- const evidenceIds = [];
846
- if (args.evidence_description) {
847
- const dispute = getDisputeDetails(db, args.dispute_id);
848
- if (dispute) {
849
- const eid = generateId('evt');
850
- db.prepare(`
851
- INSERT INTO evidence (id, order_id, uploader_id, type, description, file_hash)
852
- VALUES (?, ?, ?, 'description', ?, ?)
853
- `).run(eid, dispute.order_id, user.id, args.evidence_description, `hash_${Date.now()}`);
854
- evidenceIds.push(eid);
3259
+ }
3260
+ async function handleSecondhand(args) {
3261
+ const apiKey = String(args.api_key || '');
3262
+ const action = String(args.action || '');
3263
+ const isPublic = action === 'browse' || action === 'detail';
3264
+ if (!isPublic) {
3265
+ if (!apiKey)
3266
+ return { error: 'api_key required' };
3267
+ const auth = requireAuth(db, apiKey);
3268
+ if ('error' in auth)
3269
+ return auth;
3270
+ }
3271
+ const iid = () => encodeURIComponent(String(args.item_id || ''));
3272
+ switch (action) {
3273
+ case 'browse': {
3274
+ const qs = new URLSearchParams();
3275
+ if (args.category)
3276
+ qs.set('category', String(args.category));
3277
+ if (args.condition)
3278
+ qs.set('condition', String(args.condition));
3279
+ if (args.region)
3280
+ qs.set('region', String(args.region));
3281
+ if (args.min_price != null)
3282
+ qs.set('min_price', String(args.min_price));
3283
+ if (args.max_price != null)
3284
+ qs.set('max_price', String(args.max_price));
3285
+ if (args.query)
3286
+ qs.set('q', String(args.query));
3287
+ if (args.sort)
3288
+ qs.set('sort', String(args.sort));
3289
+ return await pwaApi('GET', '/secondhand?' + qs.toString(), apiKey);
3290
+ }
3291
+ case 'detail': {
3292
+ if (!args.item_id)
3293
+ return { error: 'item_id required' };
3294
+ return await pwaApi('GET', '/secondhand/' + iid(), apiKey);
3295
+ }
3296
+ case 'publish': {
3297
+ if (!args.title || !args.category || !args.condition_grade || args.price == null || !Array.isArray(args.images)) {
3298
+ return { error: 'title + category + condition_grade + price + images[] required' };
3299
+ }
3300
+ return await pwaApi('POST', '/secondhand', apiKey, {
3301
+ title: args.title, description: args.description, category: args.category,
3302
+ condition_grade: args.condition_grade, price: args.price, negotiable: args.negotiable,
3303
+ images: args.images, region: args.region, fulfillment: args.fulfillment,
3304
+ });
3305
+ }
3306
+ case 'update': {
3307
+ if (!args.item_id)
3308
+ return { error: 'item_id required' };
3309
+ const body = {};
3310
+ for (const k of ['title', 'description', 'category', 'condition_grade', 'price', 'negotiable', 'region', 'fulfillment', 'status']) {
3311
+ if (args[k] !== undefined)
3312
+ body[k] = args[k];
3313
+ }
3314
+ return await pwaApi('PATCH', '/secondhand/' + iid(), apiKey, body);
3315
+ }
3316
+ case 'mine': return await pwaApi('GET', '/secondhand/mine', apiKey);
3317
+ case 'buy': {
3318
+ if (!args.item_id)
3319
+ return { error: 'item_id required' };
3320
+ return await pwaApi('POST', '/secondhand/' + iid() + '/order', apiKey, {
3321
+ fulfillment_mode: args.fulfillment_mode ?? 'shipping',
3322
+ shipping_address: args.shipping_address, notes: args.notes,
3323
+ });
3324
+ }
3325
+ default: return { error: `unknown action: ${action}` };
3326
+ }
3327
+ }
3328
+ async function handleTrial(args) {
3329
+ const apiKey = String(args.api_key || '');
3330
+ const action = String(args.action || '');
3331
+ const isPublic = action === 'get_campaign';
3332
+ if (!isPublic) {
3333
+ if (!apiKey)
3334
+ return { error: 'api_key required' };
3335
+ const auth = requireAuth(db, apiKey);
3336
+ if ('error' in auth)
3337
+ return auth;
3338
+ }
3339
+ const pid = () => encodeURIComponent(String(args.product_id || ''));
3340
+ switch (action) {
3341
+ case 'get_campaign': {
3342
+ if (!args.product_id)
3343
+ return { error: 'product_id required' };
3344
+ return await pwaApi('GET', '/products/' + pid() + '/trial-campaign', apiKey);
3345
+ }
3346
+ case 'apply': {
3347
+ if (!args.product_id)
3348
+ return { error: 'product_id required' };
3349
+ return await pwaApi('POST', '/products/' + pid() + '/trial-claim', apiKey, {});
3350
+ }
3351
+ case 'link_note': {
3352
+ if (!args.claim_id || !args.note_id)
3353
+ return { error: 'claim_id + note_id required' };
3354
+ return await pwaApi('POST', '/trial-claims/' + encodeURIComponent(String(args.claim_id)) + '/link-note', apiKey, { note_id: args.note_id });
3355
+ }
3356
+ case 'my_claims': return await pwaApi('GET', '/me/trial-claims', apiKey);
3357
+ case 'create_campaign': {
3358
+ if (!args.product_id || args.quota_total == null)
3359
+ return { error: 'product_id + quota_total required' };
3360
+ return await pwaApi('POST', '/products/' + pid() + '/trial-campaign', apiKey, {
3361
+ quota_total: args.quota_total, reach_threshold: args.reach_threshold,
3362
+ min_chars: args.min_chars, min_days_live: args.min_days_live,
3363
+ });
3364
+ }
3365
+ case 'cancel_campaign': {
3366
+ if (!args.product_id)
3367
+ return { error: 'product_id required' };
3368
+ return await pwaApi('DELETE', '/products/' + pid() + '/trial-campaign', apiKey);
3369
+ }
3370
+ case 'my_campaigns': return await pwaApi('GET', '/me/seller/trial-campaigns', apiKey);
3371
+ case 'campaign_claims': {
3372
+ if (!args.campaign_id)
3373
+ return { error: 'campaign_id required' };
3374
+ return await pwaApi('GET', '/trial-campaigns/' + encodeURIComponent(String(args.campaign_id)) + '/claims', apiKey);
3375
+ }
3376
+ default: return { error: `unknown action: ${action}` };
3377
+ }
3378
+ }
3379
+ async function handleSkillMarket(args) {
3380
+ const apiKey = String(args.api_key || '');
3381
+ const action = String(args.action || '');
3382
+ const isPublic = action === 'list' || action === 'detail';
3383
+ if (!isPublic) {
3384
+ if (!apiKey)
3385
+ return { error: 'api_key required' };
3386
+ const auth = requireAuth(db, apiKey);
3387
+ if ('error' in auth)
3388
+ return auth;
3389
+ }
3390
+ const sid = () => encodeURIComponent(String(args.skill_id || ''));
3391
+ switch (action) {
3392
+ case 'list': {
3393
+ const qs = new URLSearchParams();
3394
+ if (args.kind)
3395
+ qs.set('kind', String(args.kind));
3396
+ if (args.billing)
3397
+ qs.set('billing', String(args.billing));
3398
+ if (args.query)
3399
+ qs.set('q', String(args.query));
3400
+ return await pwaApi('GET', '/skill-market?' + qs.toString(), apiKey);
3401
+ }
3402
+ case 'detail': {
3403
+ if (!args.skill_id)
3404
+ return { error: 'skill_id required' };
3405
+ return await pwaApi('GET', '/skill-market/' + sid(), apiKey);
3406
+ }
3407
+ case 'publish': {
3408
+ if (!args.title || !args.content || !args.billing_mode)
3409
+ return { error: 'title + content + billing_mode required' };
3410
+ return await pwaApi('POST', '/skill-market', apiKey, {
3411
+ title: args.title, content: args.content, summary: args.summary, preview: args.preview,
3412
+ skill_kind: args.skill_kind, billing_mode: args.billing_mode, price: args.price, category: args.category,
3413
+ });
3414
+ }
3415
+ case 'update': {
3416
+ if (!args.skill_id)
3417
+ return { error: 'skill_id required' };
3418
+ const body = {};
3419
+ for (const k of ['title', 'content', 'summary', 'preview', 'skill_kind', 'billing_mode', 'price', 'category']) {
3420
+ if (args[k] !== undefined)
3421
+ body[k] = args[k];
855
3422
  }
3423
+ return await pwaApi('PATCH', '/skill-market/' + sid(), apiKey, body);
856
3424
  }
857
- return respondToDispute(db, args.dispute_id, user.id, args.notes ?? '', evidenceIds);
3425
+ case 'delist': {
3426
+ if (!args.skill_id)
3427
+ return { error: 'skill_id required' };
3428
+ return await pwaApi('POST', '/skill-market/' + sid() + '/delist', apiKey, {});
3429
+ }
3430
+ case 'resubmit': {
3431
+ if (!args.skill_id)
3432
+ return { error: 'skill_id required' };
3433
+ return await pwaApi('POST', '/skill-market/' + sid() + '/resubmit', apiKey, {});
3434
+ }
3435
+ case 'purchase': {
3436
+ if (!args.skill_id)
3437
+ return { error: 'skill_id required' };
3438
+ return await pwaApi('POST', '/skill-market/' + sid() + '/purchase', apiKey, {});
3439
+ }
3440
+ case 'read': {
3441
+ if (!args.skill_id)
3442
+ return { error: 'skill_id required' };
3443
+ return await pwaApi('POST', '/skill-market/' + sid() + '/read', apiKey, {});
3444
+ }
3445
+ case 'my_skills': return await pwaApi('GET', '/skill-market/mine', apiKey);
3446
+ case 'library': return await pwaApi('GET', '/skill-market/library', apiKey);
3447
+ default: return { error: `unknown action: ${action}` };
858
3448
  }
859
- // ── 仲裁员裁定 ─────────────────────────────────────────────
860
- if (action === 'arbitrate') {
861
- if (!args.dispute_id)
862
- return { error: '请提供 dispute_id' };
863
- if (!args.ruling)
864
- return { error: '请提供 ruling(refund_buyer / release_seller / partial_refund)' };
865
- if (!args.ruling_reason)
866
- return { error: '请提供 ruling_reason(裁定理由将永久记录)' };
867
- if (args.ruling === 'partial_refund' && !args.refund_amount) {
868
- return { error: 'partial_refund 需要提供 refund_amount' };
3449
+ }
3450
+ async function handleRfq(args) {
3451
+ const apiKey = String(args.api_key || '');
3452
+ const action = String(args.action || '');
3453
+ if (!apiKey)
3454
+ return { error: 'api_key required' };
3455
+ const auth = requireAuth(db, apiKey);
3456
+ if ('error' in auth)
3457
+ return auth;
3458
+ switch (action) {
3459
+ case 'create': return await pwaApi('POST', '/rfqs', apiKey, {
3460
+ title: args.title, qty: args.qty, max_price: args.max_price,
3461
+ category: args.category, urgency: args.urgency,
3462
+ award_mode: args.award_mode, award_window_min: args.award_window_min,
3463
+ notes: args.notes, shipping_address: args.shipping_address,
3464
+ });
3465
+ case 'mine': return await pwaApi('GET', '/rfqs/mine', apiKey);
3466
+ case 'browse': {
3467
+ const qs = new URLSearchParams();
3468
+ if (args.region)
3469
+ qs.set('region', String(args.region));
3470
+ if (args.category)
3471
+ qs.set('category', String(args.category));
3472
+ if (args.urgency)
3473
+ qs.set('urgency', String(args.urgency));
3474
+ if (args.unbidded)
3475
+ qs.set('unbidded', '1');
3476
+ return await pwaApi('GET', '/rfqs?' + qs.toString(), apiKey);
869
3477
  }
870
- const result = arbitrateDispute(db, args.dispute_id, user.id, args.ruling, args.ruling_reason, args.refund_amount, undefined, args.liable_party);
871
- // L4-3 争议声誉:裁定完成后更新声誉
872
- if (result.success) {
873
- const dispute = getDisputeDetails(db, args.dispute_id);
874
- if (dispute?.order_id) {
875
- const ruling = args.ruling;
876
- // refund_buyer → 原告(买家)胜,被告(卖家)败;release_seller → 反之
877
- const initiatorId = dispute.initiator_id;
878
- const defendantId = dispute.defendant_id;
879
- const winnerId = ruling === 'refund_buyer' ? initiatorId : defendantId;
880
- const loserId = ruling === 'refund_buyer' ? defendantId : initiatorId;
881
- recordDisputeReputation(db, dispute.order_id, winnerId, loserId);
3478
+ case 'detail': {
3479
+ if (!args.rfq_id)
3480
+ return { error: 'rfq_id required' };
3481
+ return await pwaApi('GET', '/rfqs/' + encodeURIComponent(String(args.rfq_id)), apiKey);
3482
+ }
3483
+ case 'award': {
3484
+ if (!args.rfq_id)
3485
+ return { error: 'rfq_id required' };
3486
+ const body = args.bid_id ? { bid_id: args.bid_id } : {};
3487
+ return await pwaApi('POST', '/rfqs/' + encodeURIComponent(String(args.rfq_id)) + '/award', apiKey, body);
3488
+ }
3489
+ case 'cancel': {
3490
+ if (!args.rfq_id)
3491
+ return { error: 'rfq_id required' };
3492
+ return await pwaApi('DELETE', '/rfqs/' + encodeURIComponent(String(args.rfq_id)), apiKey);
3493
+ }
3494
+ default: return { error: `unknown action: ${action}` };
3495
+ }
3496
+ }
3497
+ async function handleBid(args) {
3498
+ const apiKey = String(args.api_key || '');
3499
+ const action = String(args.action || '');
3500
+ if (!apiKey)
3501
+ return { error: 'api_key required' };
3502
+ const auth = requireAuth(db, apiKey);
3503
+ if ('error' in auth)
3504
+ return auth;
3505
+ switch (action) {
3506
+ case 'submit': {
3507
+ if (!args.rfq_id || !args.price)
3508
+ return { error: 'rfq_id + price required' };
3509
+ return await pwaApi('POST', '/rfqs/' + encodeURIComponent(String(args.rfq_id)) + '/bids', apiKey, {
3510
+ price: args.price, qty_offered: args.qty_offered,
3511
+ eta_hours: args.eta_hours, fulfillment_type: args.fulfillment_type ?? 'standard',
3512
+ note: args.note, offer_id: args.offer_id,
3513
+ });
3514
+ }
3515
+ case 'patch': {
3516
+ if (!args.bid_id)
3517
+ return { error: 'bid_id required' };
3518
+ const body = {};
3519
+ for (const k of ['price', 'qty_offered', 'eta_hours', 'fulfillment_type', 'note']) {
3520
+ if (args[k] !== undefined)
3521
+ body[k] = args[k];
882
3522
  }
3523
+ return await pwaApi('PATCH', '/bids/' + encodeURIComponent(String(args.bid_id)), apiKey, body);
883
3524
  }
884
- return result;
3525
+ case 'cancel': {
3526
+ if (!args.bid_id)
3527
+ return { error: 'bid_id required' };
3528
+ return await pwaApi('DELETE', '/bids/' + encodeURIComponent(String(args.bid_id)), apiKey);
3529
+ }
3530
+ case 'list_mine': {
3531
+ const rows = db.prepare(`
3532
+ SELECT b.*, r.title as rfq_title, r.buyer_id, r.status as rfq_status
3533
+ FROM bids b JOIN rfqs r ON r.id = b.rfq_id
3534
+ WHERE b.seller_id = ? ORDER BY b.submitted_at DESC LIMIT 100
3535
+ `).all(auth.user.id);
3536
+ return { items: rows };
3537
+ }
3538
+ default: return { error: `unknown action: ${action}` };
885
3539
  }
886
- return { error: `未知 action:${action}` };
887
3540
  }
888
- // ─── Skill 市场处理 ────────────────────────────────────────────
889
- function handleSkill(args) {
890
- const action = args.action;
891
- // ── 浏览 Skill 市场 ────────────────────────────────────────
892
- if (action === 'list') {
893
- let userId;
894
- if (args.api_key) {
895
- const a = requireAuth(db, args.api_key);
896
- if (!('error' in a))
897
- userId = a.user.id;
3541
+ async function handleChat(args) {
3542
+ const apiKey = String(args.api_key || '');
3543
+ const action = String(args.action || '');
3544
+ if (!apiKey)
3545
+ return { error: 'api_key required' };
3546
+ const auth = requireAuth(db, apiKey);
3547
+ if ('error' in auth)
3548
+ return auth;
3549
+ switch (action) {
3550
+ case 'start': {
3551
+ if (!args.kind || !args.context_id)
3552
+ return { error: 'kind + context_id required' };
3553
+ return await pwaApi('POST', '/conversations/start', apiKey, {
3554
+ kind: args.kind, context_id: args.context_id, recipient_id: args.recipient_id,
3555
+ });
898
3556
  }
899
- const skills = listSkills(db, {
900
- skillType: args.skill_type,
901
- query: args.query,
902
- subscriberId: userId,
903
- limit: 20,
904
- });
905
- return {
906
- total: skills.length,
907
- skill_types: Object.entries(SKILL_TYPE_META).map(([k, v]) => ({ type: k, label: v.label, icon: v.icon, description: v.description })),
908
- skills: skills.map(formatSkillForAgent),
909
- };
3557
+ case 'list': return await pwaApi('GET', '/conversations', apiKey);
3558
+ case 'read': {
3559
+ if (!args.conversation_id)
3560
+ return { error: 'conversation_id required' };
3561
+ return await pwaApi('GET', '/conversations/' + encodeURIComponent(String(args.conversation_id)), apiKey);
3562
+ }
3563
+ case 'send': {
3564
+ if (!args.conversation_id || !args.body)
3565
+ return { error: 'conversation_id + body required' };
3566
+ return await pwaApi('POST', '/conversations/' + encodeURIComponent(String(args.conversation_id)) + '/messages', apiKey, { body: args.body });
3567
+ }
3568
+ case 'mark_read': {
3569
+ if (!args.conversation_id)
3570
+ return { error: 'conversation_id required' };
3571
+ return await pwaApi('POST', '/conversations/' + encodeURIComponent(String(args.conversation_id)) + '/read', apiKey);
3572
+ }
3573
+ case 'block': {
3574
+ if (!args.conversation_id)
3575
+ return { error: 'conversation_id required' };
3576
+ return await pwaApi('POST', '/conversations/' + encodeURIComponent(String(args.conversation_id)) + '/block', apiKey);
3577
+ }
3578
+ default: return { error: `unknown action: ${action}` };
910
3579
  }
911
- // 以下操作需要身份验证
912
- const auth = requireAuth(db, args.api_key);
3580
+ }
3581
+ async function handleAutoBidSkill(args) {
3582
+ const apiKey = String(args.api_key || '');
3583
+ const action = String(args.action || '');
3584
+ if (!apiKey)
3585
+ return { error: 'api_key required' };
3586
+ const auth = requireAuth(db, apiKey);
913
3587
  if ('error' in auth)
914
3588
  return auth;
915
- const { user } = auth;
916
- // ── 发布 Skill ────────────────────────────────────────────
917
- if (action === 'publish') {
918
- if (!args.name)
919
- return { error: '请填写 Skill 名称(name)' };
920
- if (!args.description)
921
- return { error: '请填写 Skill 描述(description)' };
922
- if (!args.skill_type)
923
- return { error: '请选择 Skill 类型(skill_type)' };
924
- const skill = publishSkill(db, {
925
- sellerId: user.id,
926
- name: args.name,
927
- description: args.description,
928
- category: args.category,
929
- skillType: args.skill_type,
930
- config: args.config,
931
- });
932
- const meta = SKILL_TYPE_META[skill.skill_type];
933
- return {
934
- success: true,
935
- skill_id: skill.id,
936
- message: `✅ Skill 「${skill.name}」已发布到 WebAZ Skill 市场!买家 Agent 现在可以订阅它。`,
937
- type: `${meta.icon} ${meta.label}`,
938
- tip: 'auto_accept Skill 发布后,买家新订单将自动被接受(无需手动操作)',
3589
+ if (auth.user.role !== 'seller')
3590
+ return { error: 'only seller can use auto_bid' };
3591
+ const existing = db.prepare(`SELECT id, config, active FROM skills WHERE seller_id = ? AND skill_type = 'auto_bid' ORDER BY created_at DESC LIMIT 1`).get(auth.user.id);
3592
+ if (action === 'get') {
3593
+ if (!existing)
3594
+ return { exists: false };
3595
+ let cfg = {};
3596
+ try {
3597
+ cfg = JSON.parse(existing.config || '{}');
3598
+ }
3599
+ catch { }
3600
+ return { exists: true, id: existing.id, active: existing.active, config: cfg };
3601
+ }
3602
+ if (action === 'set') {
3603
+ const config = {
3604
+ enabled: args.enabled !== false,
3605
+ categories: Array.isArray(args.categories) ? args.categories : ['standard'],
3606
+ regions: Array.isArray(args.regions) ? args.regions : [],
3607
+ max_eta_h: Number(args.max_eta_h ?? 24),
3608
+ fulfillment_type: String(args.fulfillment_type ?? 'standard'),
3609
+ bid_strategy: String(args.bid_strategy ?? 'cheapest_undercut'),
3610
+ undercut_pct: Math.max(0, Math.min(0.5, Number(args.undercut_pct ?? 0.05))),
3611
+ max_price_cap: args.max_price_cap ?? null,
3612
+ daily_cap: Number(args.daily_cap ?? 20),
3613
+ cooldown_min: Number(args.cooldown_min ?? 60),
939
3614
  };
3615
+ if (existing) {
3616
+ return await pwaApi('PATCH', '/skills/' + encodeURIComponent(existing.id), apiKey, { config, active: config.enabled ? 1 : 0 });
3617
+ }
3618
+ return await pwaApi('POST', '/skills', apiKey, {
3619
+ name: '我的自动报价 (MCP)', description: 'auto_bid via MCP', category: 'rfq', skill_type: 'auto_bid', config,
3620
+ });
940
3621
  }
941
- // ── 订阅 Skill ────────────────────────────────────────────
942
- if (action === 'subscribe') {
943
- if (!args.skill_id)
944
- return { error: '请提供 skill_id' };
945
- const result = subscribeSkill(db, user.id, args.skill_id, args.config);
946
- return { ...result, skill_id: args.skill_id };
3622
+ if (action === 'disable') {
3623
+ if (!existing)
3624
+ return { error: '尚未创建 auto_bid Skill' };
3625
+ return await pwaApi('POST', '/skills/' + encodeURIComponent(existing.id) + '/disable', apiKey);
947
3626
  }
948
- // ── 取消订阅 ──────────────────────────────────────────────
949
- if (action === 'unsubscribe') {
950
- if (!args.skill_id)
951
- return { error: '请提供 skill_id' };
952
- unsubscribeSkill(db, user.id, args.skill_id);
953
- return { success: true, message: '已取消订阅' };
3627
+ return { error: `unknown action: ${action}` };
3628
+ }
3629
+ async function handlePriceHistory(args) {
3630
+ const pid = String(args.product_id || '');
3631
+ if (!pid)
3632
+ return { error: 'product_id required' };
3633
+ try {
3634
+ const r = await fetch(PWA_API_BASE + '/products/' + encodeURIComponent(pid) + '/price-history');
3635
+ return await r.json();
954
3636
  }
955
- // ── 我发布的 Skill ────────────────────────────────────────
956
- if (action === 'my_skills') {
957
- const skills = getMySkills(db, user.id);
958
- return {
959
- total: skills.length,
960
- skills: skills.map(formatSkillForAgent),
961
- tip: skills.length === 0 ? '还没有发布任何 Skill。用 webaz_skill action=publish 发布你的第一个 Skill。' : undefined,
962
- };
3637
+ catch (e) {
3638
+ return { error: String(e.message) };
963
3639
  }
964
- // ── 我订阅的 Skill ────────────────────────────────────────
965
- if (action === 'my_subs') {
966
- const skills = getMySubscriptions(db, user.id);
967
- return {
968
- total: skills.length,
969
- subscriptions: skills.map(formatSkillForAgent),
970
- tip: skills.length === 0 ? '还没有订阅任何 Skill。用 webaz_skill action=list 浏览市场。' : undefined,
971
- };
3640
+ }
3641
+ async function handleCharity(args) {
3642
+ const action = String(args.action || '');
3643
+ if (action === 'list') {
3644
+ const qs = new URLSearchParams();
3645
+ if (args.category)
3646
+ qs.set('category', String(args.category));
3647
+ if (args.target_kind)
3648
+ qs.set('target_kind', String(args.target_kind));
3649
+ if (args.limit)
3650
+ qs.set('limit', String(args.limit));
3651
+ try {
3652
+ const r = await fetch(PWA_API_BASE + '/wishes' + (qs.toString() ? '?' + qs : ''));
3653
+ return await r.json();
3654
+ }
3655
+ catch (e) {
3656
+ return { error: String(e.message) };
3657
+ }
972
3658
  }
973
- return { error: `未知 action:${action}。可选:list, publish, subscribe, unsubscribe, my_skills, my_subs` };
3659
+ if (action === 'detail') {
3660
+ if (!args.wish_id)
3661
+ return { error: 'wish_id required' };
3662
+ try {
3663
+ const r = await fetch(PWA_API_BASE + '/wishes/' + encodeURIComponent(String(args.wish_id)));
3664
+ return await r.json();
3665
+ }
3666
+ catch (e) {
3667
+ return { error: String(e.message) };
3668
+ }
3669
+ }
3670
+ if (action === 'stories') {
3671
+ try {
3672
+ const r = await fetch(PWA_API_BASE + '/charity/stories');
3673
+ return await r.json();
3674
+ }
3675
+ catch (e) {
3676
+ return { error: String(e.message) };
3677
+ }
3678
+ }
3679
+ if (action === 'leaderboard') {
3680
+ try {
3681
+ const r = await fetch(PWA_API_BASE + '/charity/leaderboard');
3682
+ return await r.json();
3683
+ }
3684
+ catch (e) {
3685
+ return { error: String(e.message) };
3686
+ }
3687
+ }
3688
+ if (action === 'fund') {
3689
+ try {
3690
+ const r = await fetch(PWA_API_BASE + '/charity/fund');
3691
+ return await r.json();
3692
+ }
3693
+ catch (e) {
3694
+ return { error: String(e.message) };
3695
+ }
3696
+ }
3697
+ const apiKey = String(args.api_key || '');
3698
+ if (!apiKey)
3699
+ return { error: 'api_key required for this action' };
3700
+ const auth = requireAuth(db, apiKey);
3701
+ if ('error' in auth)
3702
+ return auth;
3703
+ if (action === 'create') {
3704
+ return await pwaApi('POST', '/wishes', apiKey, {
3705
+ title: args.title, content: args.content, category: args.category,
3706
+ target_kind: args.target_kind, target_waz: args.target_waz, escrow_self: args.escrow_self,
3707
+ window_hours: args.window_hours, allow_public: args.allow_public,
3708
+ });
3709
+ }
3710
+ if (action === 'me')
3711
+ return await pwaApi('GET', '/charity/me', apiKey);
3712
+ if (action === 'donate')
3713
+ return await pwaApi('POST', '/charity/fund/donate', apiKey, { amount: args.amount, note: args.note });
3714
+ if (!args.wish_id)
3715
+ return { error: 'wish_id required' };
3716
+ const id = encodeURIComponent(String(args.wish_id));
3717
+ if (action === 'claim')
3718
+ return await pwaApi('POST', '/wishes/' + id + '/fulfill', apiKey);
3719
+ if (action === 'proof')
3720
+ return await pwaApi('POST', '/wishes/' + id + '/proof', apiKey, { proof_hash: args.proof_hash, proof_note: args.proof_note });
3721
+ if (action === 'confirm')
3722
+ return await pwaApi('POST', '/wishes/' + id + '/confirm', apiKey, { fulfillment_id: args.fulfillment_id });
3723
+ if (action === 'disclose')
3724
+ return await pwaApi('POST', '/wishes/' + id + '/disclose', apiKey);
3725
+ if (action === 'cancel')
3726
+ return await pwaApi('POST', '/wishes/' + id + '/cancel', apiKey);
3727
+ if (action === 'repay')
3728
+ return await pwaApi('POST', '/wishes/' + id + '/repay', apiKey, { fulfillment_id: args.fulfillment_id, amount: args.amount, note: args.note });
3729
+ if (action === 'repay_respond') {
3730
+ if (!args.repay_id)
3731
+ return { error: 'repay_id required' };
3732
+ return await pwaApi('POST', '/wishes/' + id + '/repay/' + encodeURIComponent(String(args.repay_id)) + '/respond', apiKey, { choice: args.choice });
3733
+ }
3734
+ return { error: `unknown action: ${action}` };
974
3735
  }
975
- function handleMyKey(args) {
976
- const name = args.name?.trim();
977
- if (!name)
978
- return { error: 'name is required' };
979
- const users = db.prepare(`SELECT name, role, roles, api_key FROM users WHERE name = ? AND id != 'sys_protocol'`).all(name);
980
- if (users.length === 0)
981
- return { error: `No account found with name "${name}"` };
982
- return {
983
- found: users.length,
984
- accounts: users.map(u => ({
985
- name: u.name,
986
- role: u.role,
987
- roles: JSON.parse(u.roles || JSON.stringify([u.role])),
988
- api_key: u.api_key,
989
- })),
990
- tip: 'Keep your api_key safe it is your identity on the protocol.',
991
- };
3736
+ async function handleP2pProduct(args) {
3737
+ const action = String(args.action || '');
3738
+ if (action === 'list') {
3739
+ try {
3740
+ const r = await fetch(PWA_API_BASE + '/p2p-products');
3741
+ return await r.json();
3742
+ }
3743
+ catch (e) {
3744
+ return { error: String(e.message) };
3745
+ }
3746
+ }
3747
+ if (action === 'detail') {
3748
+ if (!args.product_id)
3749
+ return { error: 'product_id required' };
3750
+ try {
3751
+ const r = await fetch(PWA_API_BASE + '/p2p-products/' + encodeURIComponent(String(args.product_id)));
3752
+ return await r.json();
3753
+ }
3754
+ catch (e) {
3755
+ return { error: String(e.message) };
3756
+ }
3757
+ }
3758
+ const apiKey = String(args.api_key || '');
3759
+ if (!apiKey)
3760
+ return { error: 'api_key required for create/patch' };
3761
+ const auth = requireAuth(db, apiKey);
3762
+ if ('error' in auth)
3763
+ return auth;
3764
+ if (action === 'create') {
3765
+ return await pwaApi('POST', '/p2p-products', apiKey, {
3766
+ title: args.title, price: args.price, stock: args.stock,
3767
+ content_hash: args.content_hash, content_signature: args.content_signature, content_signed_at: args.content_signed_at,
3768
+ peer_endpoint: args.peer_endpoint, thumbnail_uri: args.thumbnail_uri,
3769
+ category: args.category, region: args.region,
3770
+ });
3771
+ }
3772
+ if (action === 'patch') {
3773
+ if (!args.product_id)
3774
+ return { error: 'product_id required' };
3775
+ const body = {};
3776
+ for (const k of ['title', 'price', 'stock', 'content_hash', 'content_signature', 'content_signed_at', 'peer_endpoint']) {
3777
+ if (args[k] !== undefined)
3778
+ body[k] = args[k];
3779
+ }
3780
+ return await pwaApi('PATCH', '/p2p-products/' + encodeURIComponent(String(args.product_id)), apiKey, body);
3781
+ }
3782
+ return { error: `unknown action: ${action}` };
992
3783
  }
993
- function handleProfile(args) {
994
- const auth = requireAuth(db, args.api_key);
3784
+ async function handleLike(args) {
3785
+ const apiKey = String(args.api_key || '');
3786
+ const action = String(args.action || '');
3787
+ const sid = String(args.shareable_id || '');
3788
+ if (!apiKey || !sid)
3789
+ return { error: 'api_key + shareable_id required' };
3790
+ const auth = requireAuth(db, apiKey);
995
3791
  if ('error' in auth)
996
3792
  return auth;
997
- const { user } = auth;
998
- const action = args.action;
999
- const roles = JSON.parse(user.roles || JSON.stringify([user.role]));
1000
- if (action === 'view') {
1001
- const wallet = db.prepare('SELECT balance, staked, escrowed, earned FROM wallets WHERE user_id = ?').get(user.id);
1002
- return {
1003
- id: user.id,
1004
- name: user.name,
1005
- active_role: user.role,
1006
- roles,
1007
- api_key: user.api_key,
1008
- wallet,
1009
- tip: 'Use add_role to add a new role, switch_role to change your active role.',
1010
- };
3793
+ if (action === 'toggle')
3794
+ return await pwaApi('POST', '/shareables/' + encodeURIComponent(sid) + '/like', apiKey);
3795
+ if (action === 'status')
3796
+ return await pwaApi('GET', '/shareables/' + encodeURIComponent(sid) + '/like-status', apiKey);
3797
+ return { error: `unknown action: ${action}` };
3798
+ }
3799
+ async function handleLeaderboard(args) {
3800
+ const kind = String(args.kind || 'products');
3801
+ const limit = Number(args.limit || 20);
3802
+ const VALID_KINDS = ['products', 'creators', 'buyers', 'sellers', 'value_products', 'agents', 'arbitrators', 'verifiers'];
3803
+ if (!VALID_KINDS.includes(kind))
3804
+ return { error: `kind 必须是 ${VALID_KINDS.join(' / ')}` };
3805
+ // 排行榜公开(不需 api_key);但 pwaApi 需要 用空 key 调(PWA leaderboard 路由没 auth)
3806
+ // 简化:直接调 fetch
3807
+ try {
3808
+ const r = await fetch(PWA_API_BASE + '/leaderboard?kind=' + kind + '&limit=' + limit);
3809
+ return await r.json();
1011
3810
  }
1012
- const validRoles = ['buyer', 'seller', 'logistics', 'arbitrator'];
1013
- const role = args.role;
1014
- if (action === 'add_role') {
1015
- if (!validRoles.includes(role))
1016
- return { error: `Invalid role. Options: ${validRoles.join(', ')}` };
1017
- if (roles.includes(role))
1018
- return { error: `You already have the "${role}" role` };
1019
- roles.push(role);
1020
- db.prepare("UPDATE users SET roles = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(roles), user.id);
1021
- return { success: true, active_role: user.role, roles, message: `Role "${role}" added. Use switch_role to activate it.` };
3811
+ catch (e) {
3812
+ return { error: `PWA API unreachable: ${e.message}` };
1022
3813
  }
1023
- if (action === 'switch_role') {
1024
- if (!validRoles.includes(role))
1025
- return { error: `Invalid role. Options: ${validRoles.join(', ')}` };
1026
- if (!roles.includes(role))
1027
- return { error: `You don't have the "${role}" role yet. Use add_role first.` };
1028
- db.prepare("UPDATE users SET role = ?, updated_at = datetime('now') WHERE id = ?").run(role, user.id);
1029
- return { success: true, active_role: role, roles, message: `Switched to "${role}" mode.` };
3814
+ }
3815
+ async function handleAuction(args) {
3816
+ const apiKey = String(args.api_key || '');
3817
+ const action = String(args.action || '');
3818
+ if (!apiKey)
3819
+ return { error: 'api_key required' };
3820
+ const auth = requireAuth(db, apiKey);
3821
+ if ('error' in auth)
3822
+ return auth;
3823
+ switch (action) {
3824
+ case 'create': return await pwaApi('POST', '/auctions', apiKey, {
3825
+ title: args.title, qty: args.qty, category: args.category,
3826
+ starting_price: args.starting_price, min_increment: args.min_increment,
3827
+ reserve_price: args.reserve_price, window_min: args.window_min,
3828
+ sniper_extend_min: args.sniper_extend_min, notes: args.notes,
3829
+ });
3830
+ case 'browse': {
3831
+ const qs = new URLSearchParams();
3832
+ if (args.category)
3833
+ qs.set('category', String(args.category));
3834
+ return await pwaApi('GET', '/auctions?' + qs.toString(), apiKey);
3835
+ }
3836
+ case 'mine': return await pwaApi('GET', '/auctions/mine', apiKey);
3837
+ case 'detail': {
3838
+ if (!args.auction_id)
3839
+ return { error: 'auction_id required' };
3840
+ return await pwaApi('GET', '/auctions/' + encodeURIComponent(String(args.auction_id)), apiKey);
3841
+ }
3842
+ case 'bid': {
3843
+ if (!args.auction_id || !args.price)
3844
+ return { error: 'auction_id + price required' };
3845
+ return await pwaApi('POST', '/auctions/' + encodeURIComponent(String(args.auction_id)) + '/bids', apiKey, { price: args.price });
3846
+ }
3847
+ case 'cancel': {
3848
+ if (!args.auction_id)
3849
+ return { error: 'auction_id required' };
3850
+ return await pwaApi('DELETE', '/auctions/' + encodeURIComponent(String(args.auction_id)), apiKey);
3851
+ }
3852
+ default: return { error: `unknown action: ${action}` };
1030
3853
  }
1031
- return { error: `Unknown action: ${action}. Options: view, add_role, switch_role` };
1032
3854
  }
1033
3855
  // ─── 结算逻辑(买家确认后自动执行)──────────────────────────────
3856
+ // QA P1(轮 6j):旧版 2% protocolFee 从 buyer escrow 扣下来但不 payout 给任何账户 → 钱凭空消失。
3857
+ // 修:路由到 sys_protocol;返回 settlement_breakdown 显式列出每分钱去向 + sum_check 验证恒等。
1034
3858
  function settleOrder(db, orderId) {
1035
3859
  const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
1036
3860
  const totalAmount = order.total_amount;
@@ -1038,11 +3862,15 @@ function settleOrder(db, orderId) {
1038
3862
  const buyerId = order.buyer_id;
1039
3863
  const logisticsId = order.logistics_id;
1040
3864
  const promoterId = order.promoter_id;
1041
- // 分成比例(协议参数,未来可治理调整)
1042
- const protocolFee = Math.round(totalAmount * 0.02 * 100) / 100; // 2% 协议费
1043
- const logisticsFee = Math.round(totalAmount * 0.05 * 100) / 100; // 5% 物流
1044
- const promoterFee = promoterId ? Math.round(totalAmount * 0.03 * 100) / 100 : 0; // 3% 推荐
1045
- const sellerAmount = totalAmount - protocolFee - logisticsFee - promoterFee;
3865
+ const donationAmount = order.donation_amount || 0;
3866
+ // 分成比例(协议参数,未来可治理调整 注意:这里写死的比率跟 protocol_params 表里的 0.05/0.10 不一致,QA 也抓到,单独 follow-up)
3867
+ const round2 = (n) => Math.round(n * 100) / 100;
3868
+ const isSelfFulfill = !logisticsId; // Phase 1: logistics_id seller 自负物流
3869
+ const protocolFee = round2(totalAmount * 0.02);
3870
+ // self-fulfill 时 seller 已承担 logistics 责任,不再扣 5% — 否则违反"无责方零成本 / 责任方应得回报"原则
3871
+ const logisticsFee = isSelfFulfill ? 0 : round2(totalAmount * 0.05);
3872
+ const promoterFee = promoterId ? round2(totalAmount * 0.03) : 0;
3873
+ const sellerAmount = round2(totalAmount - protocolFee - logisticsFee - promoterFee - donationAmount);
1046
3874
  const payout = (recipientId, role, amount, reason) => {
1047
3875
  if (amount <= 0)
1048
3876
  return;
@@ -1059,12 +3887,41 @@ function settleOrder(db, orderId) {
1059
3887
  payout(logisticsId, 'logistics', logisticsFee, 'logistics_fee');
1060
3888
  if (promoterId)
1061
3889
  payout(promoterId, 'promoter', promoterFee, 'promoter_fee');
1062
- // 归还卖家质押
1063
- const product = db.prepare('SELECT stake_amount FROM products WHERE id = ?').get(order.product_id);
1064
- db.prepare(`UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?`)
1065
- .run(product.stake_amount, product.stake_amount, sellerId);
1066
- // L4-3 声誉积分:交易完成后自动更新各方声誉
3890
+ // P1 修复:protocolFee 实际收款方 — 之前没人收
3891
+ if (protocolFee > 0)
3892
+ payout('sys_protocol', 'protocol_fund', protocolFee, 'protocol_fee_2pct');
3893
+ // 捐赠(如有)→ 进 charity_fund 池子
3894
+ if (donationAmount > 0) {
3895
+ db.prepare(`UPDATE charity_fund SET balance = balance + ?, total_donated = total_donated + ?, updated_at = datetime('now') WHERE id = 'main'`).run(donationAmount, donationAmount);
3896
+ }
3897
+ // QA 轮 7 P0 修复 — 改 per-order stake 释放
3898
+ // 不再读 product.stake_amount(旧 per-product 模型残留),改按本订单总额的 stake_rate 计算
3899
+ // 跟 handlePlaceOrder 锁的值一一对应,保证守恒
3900
+ const sellerStakeRate = 0.15;
3901
+ const stakeReturned = Math.round(totalAmount * sellerStakeRate * 100) / 100;
3902
+ if (stakeReturned > 0) {
3903
+ db.prepare(`UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?`)
3904
+ .run(stakeReturned, stakeReturned, sellerId);
3905
+ }
3906
+ // L4-3 声誉积分
1067
3907
  recordOrderReputation(db, orderId);
3908
+ // P1 修复:显式 breakdown 让 caller 把每分钱去向暴给 agent
3909
+ const sumCheck = round2(sellerAmount + logisticsFee + promoterFee + protocolFee + donationAmount);
3910
+ return {
3911
+ order_amount: totalAmount,
3912
+ distribution: {
3913
+ seller_net: { amount: sellerAmount, to: sellerId, rate: isSelfFulfill ? '~98% (含 logistics work)' : '~93%' },
3914
+ logistics_fee: { amount: logisticsFee, to: logisticsId, rate: isSelfFulfill ? 'N/A (self-fulfill: seller 承担)' : '5%' },
3915
+ promoter_fee: { amount: promoterFee, to: promoterId, rate: promoterId ? '3%' : 'N/A' },
3916
+ protocol_fund: { amount: protocolFee, to: 'sys_protocol', rate: '2%' },
3917
+ charity_donation: { amount: donationAmount, to: 'charity_fund', rate: donationAmount > 0 ? 'buyer-chosen' : 'N/A' },
3918
+ },
3919
+ fulfillment_mode: isSelfFulfill ? 'self' : 'market',
3920
+ sum_check: sumCheck,
3921
+ sum_check_ok: Math.abs(sumCheck - totalAmount) < 0.01,
3922
+ seller_stake_returned: stakeReturned,
3923
+ note: 'sum_check_ok=true 说明 buyer 付的金额完全等于所有分配之和,无遗漏。',
3924
+ };
1068
3925
  }
1069
3926
  // ─── MCP Server 主体 ──────────────────────────────────────────
1070
3927
  export async function startMCPServer() {
@@ -1098,6 +3955,7 @@ export async function startMCPServer() {
1098
3955
  });
1099
3956
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1100
3957
  const { name, arguments: args = {} } = request.params;
3958
+ const t0 = Date.now();
1101
3959
  let result;
1102
3960
  try {
1103
3961
  switch (name) {
@@ -1108,25 +3966,31 @@ export async function startMCPServer() {
1108
3966
  result = handleRegister(args);
1109
3967
  break;
1110
3968
  case 'webaz_search':
1111
- result = handleSearch(args);
3969
+ result = await handleSearch(args);
3970
+ break;
3971
+ case 'webaz_verify_price':
3972
+ result = handleVerifyPrice(args);
1112
3973
  break;
1113
3974
  case 'webaz_list_product':
1114
- result = handleListProduct(args);
3975
+ result = await handleListProduct(args);
1115
3976
  break;
1116
3977
  case 'webaz_place_order':
1117
3978
  result = handlePlaceOrder(args);
1118
3979
  break;
1119
3980
  case 'webaz_update_order':
1120
- result = handleUpdateOrder(args);
3981
+ result = await handleUpdateOrder(args);
1121
3982
  break;
1122
3983
  case 'webaz_get_status':
1123
3984
  result = handleGetStatus(args);
1124
3985
  break;
1125
3986
  case 'webaz_wallet':
1126
- result = handleWallet(args);
3987
+ result = await handleWallet(args);
1127
3988
  break;
1128
3989
  case 'webaz_dispute':
1129
- result = handleDispute(args);
3990
+ result = await handleDispute(args);
3991
+ break;
3992
+ case 'webaz_claim_verify':
3993
+ result = await handleClaimVerify(args);
1130
3994
  break;
1131
3995
  case 'webaz_notifications':
1132
3996
  result = handleNotifications(args);
@@ -1134,11 +3998,77 @@ export async function startMCPServer() {
1134
3998
  case 'webaz_skill':
1135
3999
  result = handleSkill(args);
1136
4000
  break;
4001
+ case 'webaz_skill_market':
4002
+ result = await handleSkillMarket(args);
4003
+ break;
4004
+ case 'webaz_secondhand':
4005
+ result = await handleSecondhand(args);
4006
+ break;
4007
+ case 'webaz_trial':
4008
+ result = await handleTrial(args);
4009
+ break;
1137
4010
  case 'webaz_mykey':
1138
4011
  result = handleMyKey(args);
1139
4012
  break;
1140
4013
  case 'webaz_profile':
1141
- result = handleProfile(args);
4014
+ result = await handleProfile(args);
4015
+ break;
4016
+ case 'webaz_revoke_key':
4017
+ result = handleRevokeKey(args);
4018
+ break;
4019
+ case 'webaz_rotate_key':
4020
+ result = handleRotateKey(args);
4021
+ break;
4022
+ case 'webaz_referral':
4023
+ result = handleReferral(args);
4024
+ break;
4025
+ case 'webaz_share_link':
4026
+ result = handleShareLink(args);
4027
+ break;
4028
+ case 'webaz_blocklist':
4029
+ result = handleBlocklist(args);
4030
+ break;
4031
+ case 'webaz_follows':
4032
+ result = handleFollows(args);
4033
+ break;
4034
+ case 'webaz_nearby':
4035
+ result = handleNearby(args);
4036
+ break;
4037
+ case 'webaz_default_address':
4038
+ result = handleDefaultAddress(args);
4039
+ break;
4040
+ case 'webaz_shareables':
4041
+ result = handleShareables(args);
4042
+ break;
4043
+ case 'webaz_rfq':
4044
+ result = await handleRfq(args);
4045
+ break;
4046
+ case 'webaz_bid':
4047
+ result = await handleBid(args);
4048
+ break;
4049
+ case 'webaz_chat':
4050
+ result = await handleChat(args);
4051
+ break;
4052
+ case 'webaz_auto_bid':
4053
+ result = await handleAutoBidSkill(args);
4054
+ break;
4055
+ case 'webaz_auction':
4056
+ result = await handleAuction(args);
4057
+ break;
4058
+ case 'webaz_like':
4059
+ result = await handleLike(args);
4060
+ break;
4061
+ case 'webaz_p2p_product':
4062
+ result = await handleP2pProduct(args);
4063
+ break;
4064
+ case 'webaz_charity':
4065
+ result = await handleCharity(args);
4066
+ break;
4067
+ case 'webaz_price_history':
4068
+ result = await handlePriceHistory(args);
4069
+ break;
4070
+ case 'webaz_leaderboard':
4071
+ result = await handleLeaderboard(args);
1142
4072
  break;
1143
4073
  default: result = { error: `未知工具:${name}` };
1144
4074
  }
@@ -1146,6 +4076,7 @@ export async function startMCPServer() {
1146
4076
  catch (err) {
1147
4077
  result = { error: `执行出错:${err.message}` };
1148
4078
  }
4079
+ recordToolCall(name, args, result, Date.now() - t0);
1149
4080
  return {
1150
4081
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1151
4082
  };
@@ -1158,3 +4089,49 @@ export async function startMCPServer() {
1158
4089
  function addHours(date, hours) {
1159
4090
  return new Date(date.getTime() + hours * 3_600_000).toISOString();
1160
4091
  }
4092
+ function recordToolCall(tool, args, result, latencyMs) {
4093
+ let userId = null;
4094
+ try {
4095
+ const apiKey = args.api_key;
4096
+ if (apiKey) {
4097
+ const row = db.prepare('SELECT id FROM users WHERE api_key = ?').get(apiKey);
4098
+ if (row)
4099
+ userId = row.id;
4100
+ }
4101
+ const isError = !!result && typeof result === 'object' && 'error' in result;
4102
+ const errorMsg = isError
4103
+ ? String(result.error).slice(0, 200)
4104
+ : null;
4105
+ // #1017 fix: mcp_tool_calls schema 是 user_id_hash 不是 user_id;无 error_msg 列
4106
+ // errorMsg 单独走 sendTelemetry → 服务端聚合,DB 只留汇总指标
4107
+ const userIdHash = userId ? createHash('sha256').update(userId).digest('hex').slice(0, 16) : null;
4108
+ db.prepare(`INSERT INTO mcp_tool_calls (tool_name, user_id_hash, outcome, latency_ms)
4109
+ VALUES (?, ?, ?, ?)`).run(tool, userIdHash, isError ? 'error' : 'success', latencyMs);
4110
+ sendTelemetry({
4111
+ tool_name: tool,
4112
+ outcome: isError ? 'error' : 'success',
4113
+ latency_ms: latencyMs,
4114
+ user_id_hash: userId ? createHash('sha256').update(userId).digest('hex').slice(0, 16) : null,
4115
+ });
4116
+ }
4117
+ catch (e) {
4118
+ console.error('[telemetry-write-failed]', e.message);
4119
+ }
4120
+ }
4121
+ function sendTelemetry(payload) {
4122
+ if (!TELEMETRY_ENABLED)
4123
+ return;
4124
+ try {
4125
+ void fetch(TELEMETRY_URL, {
4126
+ method: 'POST',
4127
+ headers: { 'content-type': 'application/json' },
4128
+ body: JSON.stringify({
4129
+ ...payload,
4130
+ server_version: SERVER_VERSION,
4131
+ ts: new Date().toISOString(),
4132
+ }),
4133
+ signal: AbortSignal.timeout(2000),
4134
+ }).catch(() => { });
4135
+ }
4136
+ catch { /* never block the tool call */ }
4137
+ }