@seasonkoh/webaz 0.1.8 → 0.1.10

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 (154) 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 +3679 -852
  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 +31362 -2320
  22. package/dist/pwa/public/i18n.js +5308 -111
  23. package/dist/pwa/public/icon.svg +11 -0
  24. package/dist/pwa/public/index.html +4 -1
  25. package/dist/pwa/public/manifest.json +39 -4
  26. package/dist/pwa/public/openapi.json +5946 -0
  27. package/dist/pwa/public/style.css +278 -5
  28. package/dist/pwa/public/sw.js +41 -2
  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 +386 -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/ap2-mandate.js +121 -0
  58. package/dist/pwa/routes/arbitrator.js +117 -0
  59. package/dist/pwa/routes/auction.js +436 -0
  60. package/dist/pwa/routes/auth-login.js +40 -0
  61. package/dist/pwa/routes/auth-read.js +66 -0
  62. package/dist/pwa/routes/auth-register.js +161 -0
  63. package/dist/pwa/routes/auth-sessions.js +62 -0
  64. package/dist/pwa/routes/blocklist.js +60 -0
  65. package/dist/pwa/routes/buyer-feeds.js +224 -0
  66. package/dist/pwa/routes/cart.js +155 -0
  67. package/dist/pwa/routes/charity.js +816 -0
  68. package/dist/pwa/routes/chat.js +318 -0
  69. package/dist/pwa/routes/checkin-tasks.js +122 -0
  70. package/dist/pwa/routes/checkout-helpers.js +106 -0
  71. package/dist/pwa/routes/claim-initiators.js +88 -0
  72. package/dist/pwa/routes/claim-verify.js +615 -0
  73. package/dist/pwa/routes/claim-voting.js +114 -0
  74. package/dist/pwa/routes/claim-withdrawals.js +20 -0
  75. package/dist/pwa/routes/coupons.js +165 -0
  76. package/dist/pwa/routes/dashboards.js +99 -0
  77. package/dist/pwa/routes/dispute-cases.js +267 -0
  78. package/dist/pwa/routes/disputes-read.js +358 -0
  79. package/dist/pwa/routes/disputes-write.js +475 -0
  80. package/dist/pwa/routes/evidence.js +86 -0
  81. package/dist/pwa/routes/external-anchors.js +107 -0
  82. package/dist/pwa/routes/feedback.js +270 -0
  83. package/dist/pwa/routes/flash-sales.js +130 -0
  84. package/dist/pwa/routes/follows.js +103 -0
  85. package/dist/pwa/routes/group-buys.js +208 -0
  86. package/dist/pwa/routes/growth.js +199 -0
  87. package/dist/pwa/routes/import-product.js +153 -0
  88. package/dist/pwa/routes/kyc.js +40 -0
  89. package/dist/pwa/routes/leaderboard.js +149 -0
  90. package/dist/pwa/routes/listings.js +281 -0
  91. package/dist/pwa/routes/logistics.js +35 -0
  92. package/dist/pwa/routes/manifests.js +126 -0
  93. package/dist/pwa/routes/me-data.js +101 -0
  94. package/dist/pwa/routes/notifications.js +48 -0
  95. package/dist/pwa/routes/offers.js +96 -0
  96. package/dist/pwa/routes/orders-action.js +285 -0
  97. package/dist/pwa/routes/orders-create.js +382 -0
  98. package/dist/pwa/routes/orders-read.js +180 -0
  99. package/dist/pwa/routes/p2p-products.js +178 -0
  100. package/dist/pwa/routes/payments-governance.js +311 -0
  101. package/dist/pwa/routes/peers.js +34 -0
  102. package/dist/pwa/routes/pin-receipts.js +39 -0
  103. package/dist/pwa/routes/products-aliases.js +119 -0
  104. package/dist/pwa/routes/products-claims.js +60 -0
  105. package/dist/pwa/routes/products-create.js +206 -0
  106. package/dist/pwa/routes/products-crud.js +73 -0
  107. package/dist/pwa/routes/products-links.js +129 -0
  108. package/dist/pwa/routes/products-list.js +424 -0
  109. package/dist/pwa/routes/products-meta.js +155 -0
  110. package/dist/pwa/routes/products-update.js +125 -0
  111. package/dist/pwa/routes/profile-credentials.js +105 -0
  112. package/dist/pwa/routes/profile-identity.js +174 -0
  113. package/dist/pwa/routes/profile-location.js +35 -0
  114. package/dist/pwa/routes/profile-placement.js +70 -0
  115. package/dist/pwa/routes/profile-prefs.js +93 -0
  116. package/dist/pwa/routes/promoter.js +208 -0
  117. package/dist/pwa/routes/public-utils.js +227 -0
  118. package/dist/pwa/routes/push.js +54 -0
  119. package/dist/pwa/routes/ratings.js +220 -0
  120. package/dist/pwa/routes/recover-key.js +100 -0
  121. package/dist/pwa/routes/referral.js +58 -0
  122. package/dist/pwa/routes/reputation.js +34 -0
  123. package/dist/pwa/routes/returns.js +493 -0
  124. package/dist/pwa/routes/reviews.js +81 -0
  125. package/dist/pwa/routes/rfqs.js +443 -0
  126. package/dist/pwa/routes/search.js +172 -0
  127. package/dist/pwa/routes/secondhand.js +278 -0
  128. package/dist/pwa/routes/seller-quota.js +225 -0
  129. package/dist/pwa/routes/share-redirects.js +164 -0
  130. package/dist/pwa/routes/shareables-interactions.js +212 -0
  131. package/dist/pwa/routes/shareables.js +470 -0
  132. package/dist/pwa/routes/shops.js +98 -0
  133. package/dist/pwa/routes/signaling.js +43 -0
  134. package/dist/pwa/routes/skill-market.js +173 -0
  135. package/dist/pwa/routes/skills.js +174 -0
  136. package/dist/pwa/routes/snf.js +126 -0
  137. package/dist/pwa/routes/tags.js +47 -0
  138. package/dist/pwa/routes/trial.js +333 -0
  139. package/dist/pwa/routes/trusted-kpi.js +87 -0
  140. package/dist/pwa/routes/url-claim.js +113 -0
  141. package/dist/pwa/routes/users-public.js +317 -0
  142. package/dist/pwa/routes/variants.js +156 -0
  143. package/dist/pwa/routes/verifier-user.js +107 -0
  144. package/dist/pwa/routes/verify-tasks.js +120 -0
  145. package/dist/pwa/routes/waitlist.js +65 -0
  146. package/dist/pwa/routes/wallet-read.js +218 -0
  147. package/dist/pwa/routes/wallet-write.js +273 -0
  148. package/dist/pwa/routes/webauthn.js +188 -0
  149. package/dist/pwa/routes/webhooks.js +162 -0
  150. package/dist/pwa/routes/welcome.js +226 -0
  151. package/dist/pwa/routes/wishlist-qa.js +135 -0
  152. package/dist/pwa/security/ssrf.js +110 -0
  153. package/dist/pwa/server.js +9317 -2097
  154. package/package.json +8 -3
@@ -0,0 +1,66 @@
1
+ export function registerAdminWalletOpsRoutes(app, deps) {
2
+ const { db, requireProtocolAdmin, adminAuth, getPublicClient, getUsdcAddr, getUsdcAbi, getHotWalletAddr, wazToUsdc, getIsMainnet, getNetwork, executeWithdrawal } = deps;
3
+ // P2-5: protocol 权限(区域 admin 看不到全局热钱包)
4
+ app.get('/api/admin/hot-wallet/status', async (req, res) => {
5
+ const admin = requireProtocolAdmin(req, res);
6
+ if (!admin)
7
+ return;
8
+ try {
9
+ const pc = getPublicClient();
10
+ const hw = getHotWalletAddr();
11
+ const usdcBal = await pc.readContract({
12
+ address: getUsdcAddr(), abi: getUsdcAbi(), functionName: 'balanceOf', args: [hw],
13
+ });
14
+ const ethBal = await pc.getBalance({ address: hw });
15
+ const pending = db.prepare("SELECT COALESCE(SUM(amount), 0) as t FROM withdrawal_requests WHERE status = 'pending'").get();
16
+ const pendingUsdc = wazToUsdc(Number(pending.t));
17
+ res.json({
18
+ address: hw,
19
+ usdc_balance: Number(usdcBal) / 1e6,
20
+ eth_balance: Number(ethBal) / 1e18,
21
+ pending_withdrawals_waz: pending.t,
22
+ pending_withdrawals_usdc: pendingUsdc,
23
+ shortfall_usdc: Math.max(0, pendingUsdc - Number(usdcBal) / 1e6),
24
+ chain: getIsMainnet() ? 'base-mainnet' : 'base-sepolia',
25
+ network: getNetwork(),
26
+ });
27
+ }
28
+ catch (e) {
29
+ res.status(500).json({ error: 'RPC 读取失败: ' + e.message });
30
+ }
31
+ });
32
+ // Legacy x-admin-key 入口:仅余额
33
+ app.get('/api/admin/hot-wallet', async (req, res) => {
34
+ if (!adminAuth(req, res))
35
+ return;
36
+ const hw = getHotWalletAddr();
37
+ try {
38
+ const balance = await getPublicClient().readContract({
39
+ address: getUsdcAddr(), abi: getUsdcAbi(),
40
+ functionName: 'balanceOf', args: [hw],
41
+ });
42
+ res.json({ address: hw, usdc_balance: Number(balance) / 1e6 });
43
+ }
44
+ catch (e) {
45
+ res.json({ address: hw, usdc_balance: null, error: e.message });
46
+ }
47
+ });
48
+ app.get('/api/admin/withdrawals', (req, res) => {
49
+ if (!adminAuth(req, res))
50
+ return;
51
+ const list = db.prepare(`
52
+ SELECT wr.*, u.name as user_name
53
+ FROM withdrawal_requests wr JOIN users u ON wr.user_id = u.id
54
+ WHERE wr.status = 'pending' ORDER BY wr.created_at ASC
55
+ `).all();
56
+ res.json(list);
57
+ });
58
+ app.post('/api/admin/withdrawals/:id/approve', async (req, res) => {
59
+ if (!adminAuth(req, res))
60
+ return;
61
+ const result = await executeWithdrawal(req.params.id).catch(e => ({ success: false, error: e.message, txHash: undefined }));
62
+ if (!result.success)
63
+ return void res.json({ error: result.error });
64
+ res.json({ success: true, tx_hash: result.txHash });
65
+ });
66
+ }
@@ -0,0 +1,215 @@
1
+ export function registerAgentBuyRoutes(app, deps) {
2
+ const { db, auth, safeFetch, rateLimitOk, generateId, anthropic, AnthropicCtor, formatProductForAgent, checkStockAndMaybeDelist, addHours, transition, notifyTransition, shouldAutoAccept } = deps;
3
+ app.post('/api/agent-buy', async (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ if (user.role !== 'buyer')
8
+ return void res.json({ error: '仅买家可使用智能下单' });
9
+ const { source_url, shipping_address, auto_buy = false, user_api_key } = req.body;
10
+ if (!source_url)
11
+ return void res.json({ error: '请提供商品链接' });
12
+ if (auto_buy && !shipping_address)
13
+ return void res.json({ error: '自动下单需提供收货地址' });
14
+ if (!rateLimitOk(req.ip || 'unknown', 6, 60_000))
15
+ return void res.status(429).json({ error: '请求过于频繁,请稍后再试' });
16
+ let html = '';
17
+ try {
18
+ const ctrl = new AbortController();
19
+ const timer = setTimeout(() => ctrl.abort(), 10000);
20
+ const resp = await safeFetch(String(source_url), {
21
+ signal: ctrl.signal,
22
+ headers: {
23
+ 'User-Agent': 'Mozilla/5.0 (compatible; WebAZ/1.0; +https://webaz.xyz)',
24
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
25
+ },
26
+ });
27
+ clearTimeout(timer);
28
+ html = (await resp.text()).slice(0, 20000);
29
+ }
30
+ catch (e) {
31
+ const msg = e.message;
32
+ if (msg.startsWith('ssrf_'))
33
+ return void res.json({ error: '链接指向私网/localhost 或经 redirect 触达内部地址,已拦截' });
34
+ return void res.json({ error: `无法访问该链接:${msg}` });
35
+ }
36
+ const client = (typeof user_api_key === 'string' && user_api_key.trim().startsWith('sk-ant-'))
37
+ ? new AnthropicCtor({ apiKey: user_api_key.trim() })
38
+ : anthropic;
39
+ let source;
40
+ try {
41
+ const msg = await client.messages.create({
42
+ model: 'claude-haiku-4-5-20251001',
43
+ max_tokens: 512,
44
+ messages: [{ role: 'user', content: `从以下网页提取商品关键信息,仅返回JSON:
45
+ {
46
+ "title": "商品全名",
47
+ "price_cny": 数字或null,
48
+ "category": "分类",
49
+ "search_terms": ["独立短词1","独立短词2","独立短词3"]
50
+ }
51
+ search_terms 是3-5个独立的中文短词(每个2-4个汉字),用于在数据库里搜索同类商品。
52
+ 例:九阳炒菜机器人 → ["炒菜机","九阳","炒菜机器人","自动炒菜"]
53
+ HTML:${html}` }],
54
+ });
55
+ const text = msg.content[0].type === 'text' ? msg.content[0].text : '';
56
+ const m = text.match(/\{[\s\S]*\}/);
57
+ if (!m)
58
+ throw new Error('no json');
59
+ source = JSON.parse(m[0]);
60
+ }
61
+ catch {
62
+ return void res.json({ error: '无法从链接提取商品信息,请尝试其他链接' });
63
+ }
64
+ if (!source.title)
65
+ return void res.json({ error: '链接无法提取商品信息(可能需要登录或动态渲染)' });
66
+ const urlMatchIds = db.prepare(`
67
+ SELECT DISTINCT product_id FROM product_external_links WHERE url = ? AND verified = 1
68
+ `).all(source_url).map(r => r.product_id);
69
+ const urlMatchProducts = urlMatchIds.length > 0
70
+ ? db.prepare(`
71
+ SELECT p.*, u.name as seller_name,
72
+ COALESCE(rs.total_points, 0) as rep_points, COALESCE(rs.level, 'new') as rep_level
73
+ FROM products p
74
+ JOIN users u ON p.seller_id = u.id
75
+ LEFT JOIN reputation_scores rs ON rs.user_id = p.seller_id
76
+ WHERE p.id IN (${urlMatchIds.map(() => '?').join(',')}) AND p.status = 'active' AND p.stock > 0
77
+ `).all(...urlMatchIds)
78
+ : [];
79
+ let keywordProducts = [];
80
+ if (urlMatchProducts.length < 3) {
81
+ const rawTerms = Array.isArray(source.search_terms) ? source.search_terms : [];
82
+ if (rawTerms.length === 0) {
83
+ const t = source.title;
84
+ for (let i = 0; i + 2 <= t.length && rawTerms.length < 4; i += 2)
85
+ rawTerms.push(t.slice(i, i + 4));
86
+ }
87
+ const terms = rawTerms.filter((t) => t && t.length >= 2).slice(0, 6);
88
+ if (terms.length > 0) {
89
+ const termClauses = terms.map(() => `p.title LIKE ? ESCAPE '\\' OR p.description LIKE ? ESCAPE '\\'`).join(' OR ');
90
+ const termParams = terms.flatMap((t) => { const e = t.replace(/[\\%_]/g, '\\$&'); return [`%${e}%`, `%${e}%`]; });
91
+ const catClause = source.category ? ` OR p.category = ?` : '';
92
+ const catParam = source.category ? [source.category] : [];
93
+ const alreadyIds = urlMatchProducts.map(p => p.id);
94
+ const excludeClause = alreadyIds.length > 0 ? ` AND p.id NOT IN (${alreadyIds.map(() => '?').join(',')})` : '';
95
+ keywordProducts = db.prepare(`
96
+ SELECT p.*, u.name as seller_name,
97
+ COALESCE(rs.total_points, 0) as rep_points, COALESCE(rs.level, 'new') as rep_level
98
+ FROM products p
99
+ JOIN users u ON p.seller_id = u.id
100
+ LEFT JOIN reputation_scores rs ON rs.user_id = p.seller_id
101
+ WHERE p.status = 'active' AND p.stock > 0
102
+ AND (${termClauses}${catClause})${excludeClause}
103
+ ORDER BY rep_points DESC, p.price ASC LIMIT ${5 - urlMatchProducts.length}
104
+ `).all(...termParams, ...catParam, ...alreadyIds);
105
+ }
106
+ }
107
+ const webazProducts = [...urlMatchProducts, ...keywordProducts];
108
+ const webazFormatted = webazProducts.map(p => ({
109
+ ...formatProductForAgent(p),
110
+ url_match: urlMatchIds.includes(p.id),
111
+ }));
112
+ let decision;
113
+ try {
114
+ const msg = await client.messages.create({
115
+ model: 'claude-haiku-4-5-20251001',
116
+ max_tokens: 512,
117
+ messages: [{ role: 'user', content: `你是一个购物助手。用户想买以下商品,我们找到了 WebAZ 平台上的替代选项,请做出购买建议。
118
+
119
+ 原商品:
120
+ - 标题:${source.title}
121
+ - 原平台价格:${source.price_cny ? `¥${source.price_cny} CNY` : '未知'}
122
+ - 链接:${source_url}
123
+
124
+ WebAZ 平台替代方案(WAZ ≈ CNY):
125
+ ${webazFormatted.length > 0 ? JSON.stringify(webazFormatted.map(p => ({
126
+ id: p.id,
127
+ title: p.title,
128
+ price: p.price,
129
+ agent_summary: p.agent_summary,
130
+ seller: p.seller_name,
131
+ rep: p.rep_level,
132
+ })), null, 2) : '暂无匹配商品'}
133
+
134
+ 仅返回JSON(不要其他文字):
135
+ {
136
+ "recommendation": "buy_webaz" | "buy_source" | "no_match",
137
+ "best_product_id": "WebAZ商品ID(recommendation=buy_webaz时填写,否则null)",
138
+ "reason": "一句话购买建议,说明为什么选这个方案(包含价格对比、售后优势等)",
139
+ "savings_note": "省了多少或更优在哪(简短,可null)"
140
+ }` }],
141
+ });
142
+ const text = msg.content[0].type === 'text' ? msg.content[0].text : '';
143
+ const m = text.match(/\{[\s\S]*\}/);
144
+ if (!m)
145
+ throw new Error('no json');
146
+ decision = JSON.parse(m[0]);
147
+ }
148
+ catch {
149
+ decision = { recommendation: 'no_match', reason: '无法完成比价分析,请手动选购' };
150
+ }
151
+ let orderId = null;
152
+ let sessionToken = null;
153
+ let verifiedPrice = null;
154
+ if (auto_buy && decision.recommendation === 'buy_webaz' && decision.best_product_id) {
155
+ const product = db.prepare(`SELECT * FROM products WHERE id = ? AND status = 'active'`)
156
+ .get(decision.best_product_id);
157
+ if (product && Number(product.has_variants) === 1) {
158
+ decision.reason = (decision.reason || '') + ' · 该商品需手动选规格,跳过 auto_buy';
159
+ }
160
+ else if (product && product.stock > 0) {
161
+ const wallet = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(user.id);
162
+ if (wallet.balance >= product.price) {
163
+ const now = new Date();
164
+ const expiresAt = new Date(now.getTime() + 10 * 60_000);
165
+ sessionToken = generateId('pst');
166
+ db.prepare(`INSERT INTO price_sessions (token, product_id, user_id, price, quantity, created_at, expires_at) VALUES (?,?,?,?,1,?,?)`)
167
+ .run(sessionToken, product.id, user.id, product.price, now.toISOString(), expiresAt.toISOString());
168
+ verifiedPrice = product.price;
169
+ const oId = generateId('ord');
170
+ const totalAmount = product.price;
171
+ const seller = db.prepare('SELECT id FROM users WHERE id = ?').get(product.seller_id);
172
+ db.prepare(`INSERT INTO orders (
173
+ id, product_id, buyer_id, seller_id, quantity, unit_price, total_amount, escrow_amount,
174
+ status, shipping_address, notes, pay_deadline, accept_deadline, ship_deadline,
175
+ pickup_deadline, delivery_deadline, confirm_deadline
176
+ ) VALUES (?,?,?,?,1,?,?,?,'created',?,?,?,?,?,?,?,?)`).run(oId, product.id, user.id, seller.id, totalAmount, totalAmount, totalAmount, shipping_address, `[智能下单] ${decision.reason}`, addHours(now, 24), addHours(now, 48), addHours(now, 120), addHours(now, 168), addHours(now, 336), addHours(now, 408));
177
+ db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ?')
178
+ .run(totalAmount, totalAmount, user.id);
179
+ db.prepare('UPDATE products SET stock = stock - 1 WHERE id = ?').run(product.id);
180
+ checkStockAndMaybeDelist(String(product.id));
181
+ db.prepare(`UPDATE price_sessions SET used_at = datetime('now') WHERE token = ?`).run(sessionToken);
182
+ transition(db, oId, 'paid', user.id, [], '智能下单:模拟支付完成');
183
+ notifyTransition(db, oId, 'created', 'paid');
184
+ if (shouldAutoAccept(db, oId)) {
185
+ const sys = db.prepare("SELECT id FROM users WHERE id = 'sys_protocol'").get();
186
+ if (sys) {
187
+ const ar = transition(db, oId, 'accepted', sys.id, [], '⚡ auto_accept Skill 自动接单');
188
+ if (ar.success)
189
+ notifyTransition(db, oId, 'paid', 'accepted');
190
+ }
191
+ }
192
+ orderId = oId;
193
+ }
194
+ }
195
+ }
196
+ const bestProduct = decision.best_product_id
197
+ ? webazFormatted.find(p => p.id === decision.best_product_id) ?? null
198
+ : null;
199
+ res.json({
200
+ source: {
201
+ title: source.title,
202
+ price_cny: source.price_cny ?? null,
203
+ url: source_url,
204
+ },
205
+ webaz_products: webazFormatted.slice(0, 3),
206
+ recommendation: decision.recommendation,
207
+ best_product: bestProduct,
208
+ reason: decision.reason,
209
+ savings_note: decision.savings_note ?? null,
210
+ auto_bought: !!orderId,
211
+ order_id: orderId,
212
+ verified_price: verifiedPrice,
213
+ });
214
+ });
215
+ }