@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
@@ -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
+ }
@@ -0,0 +1,341 @@
1
+ import { computeAgentPassport } from '../../layer1-agent/L1-2-identity/agent-passport.js';
2
+ export function registerAgentGovernanceRoutes(app, deps) {
3
+ const { db, generateId, auth, requireRootAdmin, invalidateAgentBlockedCache, requireHumanPresence, issueAgentStrike, custodianFingerprint, signPassport, issuerAddress } = deps;
4
+ // /api/me/agents — 列出本账号所有 agent + declaration / strikes
5
+ app.get('/api/me/agents', (req, res) => {
6
+ const user = auth(req, res);
7
+ if (!user)
8
+ return;
9
+ const keys = db.prepare(`SELECT api_key FROM users WHERE id = ? UNION SELECT api_key FROM agent_reputation WHERE user_id = ?`).all(user.id, user.id);
10
+ const items = keys.map(k => {
11
+ const decl = db.prepare(`SELECT operator_name, operator_contact, purpose, declared_scope, attestations, repo_url, revoked_at FROM agent_declarations WHERE api_key = ?`).get(k.api_key);
12
+ // P1 fix 4.4:附 signals JSON
13
+ const rep = db.prepare(`SELECT trust_score, level, signals, last_calculated_at FROM agent_reputation WHERE api_key = ?`).get(k.api_key);
14
+ if (rep && rep.signals) {
15
+ try {
16
+ rep.signals = JSON.parse(rep.signals);
17
+ }
18
+ catch {
19
+ rep.signals = null;
20
+ }
21
+ }
22
+ const calls30d = db.prepare(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND created_at > datetime('now', '-30 days')`).get(k.api_key).n;
23
+ const last = db.prepare(`SELECT endpoint, method, status_code, created_at FROM agent_call_log WHERE api_key = ? ORDER BY created_at DESC LIMIT 1`).get(k.api_key);
24
+ const strikes = db.prepare(`SELECT severity, reason_code, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`).all(k.api_key);
25
+ let passport = null;
26
+ try {
27
+ passport = computeAgentPassport(db, k.api_key, user.id, custodianFingerprint);
28
+ }
29
+ catch { /* read-only, never break the list */ }
30
+ return {
31
+ api_key_prefix: k.api_key.slice(0, 12) + '...',
32
+ api_key_full: k.api_key === user.api_key ? k.api_key : null,
33
+ declaration: decl || null,
34
+ reputation: rep || null,
35
+ calls_30d: calls30d,
36
+ last_call: last || null,
37
+ recent_strikes: strikes,
38
+ passport,
39
+ };
40
+ });
41
+ // Phase 2 监护人总览(只读/软绑定):真人态 + 旗下 agent 聚合 + 连带
42
+ const hasPasskey = (db.prepare('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?').get(user.id)?.n || 0) > 0;
43
+ const pps = items.map(i => i.passport).filter(Boolean);
44
+ const depthRank = { shallow: 0, medium: 1, deep: 2, profound: 3 };
45
+ const deepest = pps.reduce((d, p) => (depthRank[p.engagement_depth] ?? 0) > (depthRank[d] ?? 0) ? p.engagement_depth : d, 'shallow');
46
+ const custodian = {
47
+ fingerprint: custodianFingerprint(user.id),
48
+ has_passkey: hasPasskey,
49
+ agent_count: items.length,
50
+ max_risk: pps.reduce((m, p) => Math.max(m, p.risk_score), 0),
51
+ high_risk_count: pps.filter(p => p.risk_score >= 50).length,
52
+ deepest_engagement: pps.length ? deepest : null,
53
+ };
54
+ res.json({ items, custodian });
55
+ });
56
+ // Phase 4:签名可移植护照导出(VC/DID,跨协议 ecrecover 可验)。owner 自取自己 agent 的凭证。
57
+ app.get('/api/me/agents/:apiKeyPrefix/passport', async (req, res) => {
58
+ const user = auth(req, res);
59
+ if (!user)
60
+ return;
61
+ const prefix = String(req.params.apiKeyPrefix || '').replace('...', '');
62
+ if (prefix.length < 6)
63
+ return void res.status(400).json({ error: 'apiKeyPrefix 太短' });
64
+ const keys = db.prepare(`SELECT api_key FROM users WHERE id = ? UNION SELECT api_key FROM agent_reputation WHERE user_id = ?`).all(user.id, user.id);
65
+ const match = keys.find(k => k.api_key.startsWith(prefix));
66
+ if (!match)
67
+ return void res.status(404).json({ error: '未找到该 agent(或不属于你)' });
68
+ const pp = computeAgentPassport(db, match.api_key, user.id, custodianFingerprint);
69
+ const issued_at = new Date().toISOString();
70
+ const expires_at = new Date(Date.now() + 7 * 86400_000).toISOString();
71
+ const issuer = issuerAddress();
72
+ const keyPrefix = match.api_key.slice(0, 12) + '...';
73
+ const bp = pp.behavior_profile;
74
+ // 规范化签名串(verifier 用相同格式重建 → verifyMessage(issuer, canonical, signature))
75
+ const canonical = `webaz-agent-passport|v1|${issuer}|${issued_at}|${expires_at}|${pp.custodian_fingerprint}|${keyPrefix}|risk=${pp.risk_score}|depth=${pp.engagement_depth}|bp=${bp.query},${bp.transact},${bp.govern}`;
76
+ let signature = '';
77
+ try {
78
+ signature = await signPassport(canonical);
79
+ }
80
+ catch (e) {
81
+ return void res.status(503).json({ error: '签名服务暂不可用', detail: e.message });
82
+ }
83
+ res.json({
84
+ type: 'WebAZAgentPassport', version: 1,
85
+ issuer: 'did:webaz:' + issuer, issuer_address: issuer,
86
+ issued_at, expires_at,
87
+ subject: { custodian_fingerprint: pp.custodian_fingerprint, agent_key_prefix: keyPrefix },
88
+ claims: { risk_score: pp.risk_score, engagement_depth: pp.engagement_depth, behavior_profile: bp },
89
+ canonical, signature,
90
+ verify: { scheme: 'eip191', how: 'verifyMessage(issuer_address, canonical, signature)' },
91
+ });
92
+ });
93
+ app.get('/api/me/agents/:apiKeyPrefix/log', (req, res) => {
94
+ const user = auth(req, res);
95
+ if (!user)
96
+ return;
97
+ const prefix = String(req.params.apiKeyPrefix || '').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32);
98
+ if (prefix.length < 8)
99
+ return void res.status(400).json({ error: 'apiKeyPrefix 至少 8 字符' });
100
+ const targetKey = db.prepare(`SELECT api_key FROM users WHERE id = ? AND api_key LIKE ? || '%'
101
+ UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'`)
102
+ .get(user.id, prefix, user.id, prefix);
103
+ if (!targetKey)
104
+ return void res.status(404).json({ error: '未找到匹配的 agent api_key(仅可查本人的)' });
105
+ const limit = Math.min(500, Math.max(10, Number(req.query.limit) || 100));
106
+ const rows = db.prepare(`SELECT endpoint, method, status_code, created_at FROM agent_call_log
107
+ WHERE api_key = ? AND created_at > datetime('now', '-30 days') ORDER BY id DESC LIMIT ?`).all(targetKey.api_key, limit);
108
+ res.json({ items: rows });
109
+ });
110
+ app.post('/api/me/agents/declarations', (req, res) => {
111
+ const user = auth(req, res);
112
+ if (!user)
113
+ return;
114
+ const b = req.body;
115
+ const targetApiKey = b.api_key ? String(b.api_key) : user.api_key;
116
+ const ownership = db.prepare(`SELECT id FROM users WHERE id = ? AND api_key = ?
117
+ UNION SELECT user_id as id FROM agent_reputation WHERE user_id = ? AND api_key = ?`)
118
+ .get(user.id, targetApiKey, user.id, targetApiKey);
119
+ if (!ownership)
120
+ return void res.status(403).json({ error: 'api_key 不属于本账号' });
121
+ const operator_name = String(b.operator_name || '').trim().slice(0, 60);
122
+ if (operator_name.length < 2)
123
+ return void res.status(400).json({ error: 'operator_name 2-60 字' });
124
+ const operator_contact = String(b.operator_contact || '').trim().slice(0, 120);
125
+ if (operator_contact.length < 3)
126
+ return void res.status(400).json({ error: 'operator_contact 必填' });
127
+ const purpose = String(b.purpose || '').trim().slice(0, 200);
128
+ if (purpose.length < 5)
129
+ return void res.status(400).json({ error: 'purpose 5-200 字' });
130
+ let scopeJson;
131
+ try {
132
+ const s = b.declared_scope;
133
+ if (!s || typeof s !== 'object')
134
+ return void res.status(400).json({ error: 'declared_scope 必须是对象' });
135
+ scopeJson = JSON.stringify(s).slice(0, 2000);
136
+ }
137
+ catch {
138
+ return void res.status(400).json({ error: 'declared_scope 无法序列化' });
139
+ }
140
+ const attestationsJson = b.attestations && typeof b.attestations === 'object' ? JSON.stringify(b.attestations).slice(0, 2000) : null;
141
+ const repo_url = b.repo_url ? String(b.repo_url).slice(0, 200) : null;
142
+ const homepage = b.homepage ? String(b.homepage).slice(0, 200) : null;
143
+ db.prepare(`INSERT INTO agent_declarations (
144
+ api_key, user_id, operator_name, operator_contact, purpose, declared_scope, attestations, repo_url, homepage
145
+ ) VALUES (?,?,?,?,?,?,?,?,?)
146
+ ON CONFLICT(api_key) DO UPDATE SET
147
+ operator_name = excluded.operator_name,
148
+ operator_contact = excluded.operator_contact,
149
+ purpose = excluded.purpose,
150
+ declared_scope = excluded.declared_scope,
151
+ attestations = excluded.attestations,
152
+ repo_url = excluded.repo_url,
153
+ homepage = excluded.homepage,
154
+ revoked_at = NULL,
155
+ updated_at = datetime('now')`).run(targetApiKey, user.id, operator_name, operator_contact, purpose, scopeJson, attestationsJson, repo_url, homepage);
156
+ invalidateAgentBlockedCache(targetApiKey);
157
+ res.json({ ok: true });
158
+ });
159
+ // 用户撤销 agent(铁律 §4 human presence)
160
+ app.post('/api/me/agents/:apiKeyPrefix/revoke', (req, res) => {
161
+ const user = auth(req, res);
162
+ if (!user)
163
+ return;
164
+ const prefix = String(req.params.apiKeyPrefix || '').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32);
165
+ if (prefix.length < 8)
166
+ return void res.status(400).json({ error: 'apiKeyPrefix 至少 8 字符' });
167
+ const targetKey = db.prepare(`SELECT api_key FROM users WHERE id = ? AND api_key LIKE ? || '%'
168
+ UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'`)
169
+ .get(user.id, prefix, user.id, prefix);
170
+ if (!targetKey)
171
+ return void res.status(404).json({ error: '未找到匹配的 agent api_key' });
172
+ const hpCheck = requireHumanPresence(user.id, 'agent_revoke', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_revoke', () => true);
173
+ if (!hpCheck.ok)
174
+ return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
175
+ const reason = String((req.body || {}).reason || '').slice(0, 300);
176
+ db.prepare(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
177
+ VALUES ('api_key', ?, ?, 'self', ?)
178
+ ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING`).run(targetKey.api_key, user.id, reason);
179
+ db.prepare(`UPDATE agent_declarations SET revoked_at = datetime('now'), revoked_reason = ? WHERE api_key = ?`).run(reason, targetKey.api_key);
180
+ invalidateAgentBlockedCache(targetKey.api_key);
181
+ res.json({ ok: true });
182
+ });
183
+ // 撤销同 operator 名下所有 agent(仅撤销本用户给 operator 旗下 agent 的 attestation)
184
+ app.post('/api/me/agents/operators/:operator_name/revoke', (req, res) => {
185
+ const user = auth(req, res);
186
+ if (!user)
187
+ return;
188
+ const opName = String(req.params.operator_name || '').trim().slice(0, 60);
189
+ if (opName.length < 2)
190
+ return void res.status(400).json({ error: 'operator_name 至少 2 字' });
191
+ const hpCheck = requireHumanPresence(user.id, 'agent_revoke', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_revoke', () => true);
192
+ if (!hpCheck.ok)
193
+ return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
194
+ const reason = String((req.body || {}).reason || '').slice(0, 300);
195
+ const affected = db.prepare(`UPDATE agent_attestations SET revoked_at = datetime('now')
196
+ WHERE user_id = ? AND revoked_at IS NULL
197
+ AND api_key IN (SELECT api_key FROM agent_declarations WHERE operator_name = ?)`)
198
+ .run(user.id, opName);
199
+ db.prepare(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
200
+ VALUES ('operator_name', ?, ?, 'self', ?)
201
+ ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING`).run(opName, user.id, reason);
202
+ const keys = db.prepare(`SELECT api_key FROM agent_declarations WHERE operator_name = ?`).all(opName);
203
+ for (const k of keys)
204
+ invalidateAgentBlockedCache(k.api_key);
205
+ res.json({ ok: true, attestations_revoked: affected.changes });
206
+ });
207
+ // P0 audit fix 4.2: 申诉 strike
208
+ app.post('/api/me/agents/strikes/:strikeId/appeal', (req, res) => {
209
+ const user = auth(req, res);
210
+ if (!user)
211
+ return;
212
+ const strikeId = Number(req.params.strikeId);
213
+ if (!Number.isInteger(strikeId) || strikeId <= 0)
214
+ return void res.status(400).json({ error: 'strikeId 必须是正整数' });
215
+ const strike = db.prepare(`SELECT id, api_key, user_id, severity, issued_at, appeal_status FROM agent_strikes WHERE id = ?`).get(strikeId);
216
+ if (!strike)
217
+ return void res.status(404).json({ error: 'strike 不存在' });
218
+ if (strike.user_id !== user.id)
219
+ return void res.status(403).json({ error: '只能申诉自己 agent 的 strike' });
220
+ if (strike.appeal_status !== 'none')
221
+ return void res.status(409).json({ error: `已申诉过(状态:${strike.appeal_status})` });
222
+ const issuedAt = new Date(String(strike.issued_at).replace(' ', 'T') + 'Z').getTime();
223
+ if (Date.now() - issuedAt > 30 * 86400_000)
224
+ return void res.status(410).json({ error: '已过 30 天申诉窗口' });
225
+ const reason = String((req.body || {}).reason || '').trim().slice(0, 500);
226
+ if (reason.length < 10)
227
+ return void res.status(400).json({ error: '申诉理由 ≥10 字' });
228
+ db.prepare(`UPDATE agent_strikes SET appeal_status = 'pending', appeal_reason = ? WHERE id = ?`).run(reason, strikeId);
229
+ res.json({ ok: true, message: '申诉已提交,等待 root admin 审核' });
230
+ });
231
+ // Admin: 审核 strike 申诉
232
+ app.post('/api/admin/agent-strikes/:strikeId/decide', (req, res) => {
233
+ const user = requireRootAdmin(req, res);
234
+ if (!user)
235
+ return;
236
+ const strikeId = Number(req.params.strikeId);
237
+ if (!Number.isInteger(strikeId) || strikeId <= 0)
238
+ return void res.status(400).json({ error: 'strikeId 必须是正整数' });
239
+ const decision = String((req.body || {}).decision || '');
240
+ if (!['approved', 'denied'].includes(decision))
241
+ return void res.status(400).json({ error: 'decision 必须是 approved / denied' });
242
+ const strike = db.prepare(`SELECT id, api_key, appeal_status FROM agent_strikes WHERE id = ?`).get(strikeId);
243
+ if (!strike)
244
+ return void res.status(404).json({ error: 'strike 不存在' });
245
+ if (strike.appeal_status !== 'pending')
246
+ return void res.status(409).json({ error: `当前状态 ${strike.appeal_status} 不可裁决` });
247
+ db.prepare(`UPDATE agent_strikes SET appeal_status = ?, appeal_decided_by = ?, appeal_decided_at = datetime('now') WHERE id = ?`)
248
+ .run(decision, user.id, strikeId);
249
+ invalidateAgentBlockedCache(strike.api_key);
250
+ // P1 fix 5.3: appeal approved → 恢复因 strike 自动停用的 skills
251
+ if (decision === 'approved') {
252
+ try {
253
+ const uRow = db.prepare(`SELECT id FROM users WHERE api_key = ?`).get(strike.api_key);
254
+ if (uRow) {
255
+ const r = db.prepare(`UPDATE skills SET active = 1, disabled_by_strike_at = NULL
256
+ WHERE seller_id = ? AND disabled_by_strike_at IS NOT NULL AND active = 0`).run(uRow.id);
257
+ if (r.changes > 0)
258
+ console.log(`[appeal approved→skill] restored ${r.changes} skills for ${uRow.id}`);
259
+ }
260
+ }
261
+ catch (e) {
262
+ console.error('[appeal skills restore]', e);
263
+ }
264
+ }
265
+ res.json({ ok: true, decision });
266
+ });
267
+ // Admin: 列出待审 strike 申诉
268
+ app.get('/api/admin/agent-strikes/pending', (req, res) => {
269
+ const user = requireRootAdmin(req, res);
270
+ if (!user)
271
+ return;
272
+ const rows = db.prepare(`SELECT s.id, s.api_key, s.user_id, u.handle, s.severity, s.reason_code, s.reason_detail, s.issued_at, s.appeal_reason
273
+ FROM agent_strikes s JOIN users u ON u.id = s.user_id
274
+ WHERE s.appeal_status = 'pending' ORDER BY s.id DESC LIMIT 100`).all();
275
+ res.json({ items: rows });
276
+ });
277
+ // P1 fix 4.3: admin 主动 issue strike
278
+ app.post('/api/admin/agent-strikes/issue', (req, res) => {
279
+ const adminUser = requireRootAdmin(req, res);
280
+ if (!adminUser)
281
+ return;
282
+ const b = req.body;
283
+ const apiKey = String(b.api_key || '').trim();
284
+ if (apiKey.length < 8)
285
+ return void res.status(400).json({ error: 'api_key 必填' });
286
+ const targetUser = db.prepare(`SELECT id, handle FROM users WHERE api_key = ?`).get(apiKey);
287
+ if (!targetUser)
288
+ return void res.status(404).json({ error: '未找到该 api_key' });
289
+ const reasonCode = String(b.reason_code || '').trim().slice(0, 40);
290
+ if (!/^[a-z_]{3,40}$/.test(reasonCode))
291
+ return void res.status(400).json({ error: 'reason_code 必须是 3-40 位 [a-z_](如 fake_shipment / spam)' });
292
+ const reasonDetail = b.reason_detail ? String(b.reason_detail).slice(0, 500) : null;
293
+ const initialSeverity = (b.severity ? String(b.severity) : 'warning');
294
+ if (!['warning', 'suspend_7d', 'permanent'].includes(initialSeverity)) {
295
+ return void res.status(400).json({ error: 'severity 必须是 warning / suspend_7d / permanent' });
296
+ }
297
+ const result = issueAgentStrike({
298
+ apiKey, userId: targetUser.id,
299
+ reasonCode, reasonDetail: reasonDetail || undefined,
300
+ reportedBy: adminUser.id,
301
+ relatedRef: b.related_ref ? String(b.related_ref) : undefined,
302
+ initialSeverity,
303
+ });
304
+ res.json({ ok: true, target_handle: targetUser.handle, ...result });
305
+ });
306
+ // bilateral attestation(用户批准某 agent 的 scope)
307
+ app.post('/api/me/agents/attestations', (req, res) => {
308
+ const user = auth(req, res);
309
+ if (!user)
310
+ return;
311
+ const b = req.body;
312
+ const apiKey = String(b.api_key || '');
313
+ if (!apiKey)
314
+ return void res.status(400).json({ error: 'api_key 必填' });
315
+ const decl = db.prepare(`SELECT declared_scope, operator_name, purpose FROM agent_declarations WHERE api_key = ? AND revoked_at IS NULL`).get(apiKey);
316
+ if (!decl)
317
+ return void res.status(404).json({ error: '该 agent 未声明 / 已撤销,无法授权' });
318
+ let approvedScopeJson;
319
+ try {
320
+ const s = b.approved_scope;
321
+ if (!s || typeof s !== 'object')
322
+ return void res.status(400).json({ error: 'approved_scope 必须是对象' });
323
+ approvedScopeJson = JSON.stringify(s).slice(0, 2000);
324
+ }
325
+ catch {
326
+ return void res.status(400).json({ error: 'approved_scope 无法序列化' });
327
+ }
328
+ const spendCapPerOrder = b.spend_cap_per_order != null ? Math.max(0, Number(b.spend_cap_per_order)) : null;
329
+ const spendCapDaily = b.spend_cap_daily != null ? Math.max(0, Number(b.spend_cap_daily)) : null;
330
+ const id = generateId('aat');
331
+ db.prepare(`INSERT INTO agent_attestations (id, api_key, user_id, approved_scope, spend_cap_per_order, spend_cap_daily)
332
+ VALUES (?,?,?,?,?,?)
333
+ ON CONFLICT(api_key, user_id) DO UPDATE SET
334
+ approved_scope = excluded.approved_scope,
335
+ spend_cap_per_order = excluded.spend_cap_per_order,
336
+ spend_cap_daily = excluded.spend_cap_daily,
337
+ revoked_at = NULL,
338
+ granted_at = datetime('now')`).run(id, apiKey, user.id, approvedScopeJson, spendCapPerOrder, spendCapDaily);
339
+ res.json({ ok: true });
340
+ });
341
+ }
@@ -0,0 +1,34 @@
1
+ export function registerAgentReputationRoutes(app, deps) {
2
+ const { auth, getAgentTrustCached, getRawModeMinTrust } = deps;
3
+ app.get('/api/agents/me/reputation', (req, res) => {
4
+ const user = auth(req, res);
5
+ if (!user)
6
+ return;
7
+ const key = req.headers.authorization?.replace('Bearer ', '') ?? '';
8
+ const t = getAgentTrustCached(key);
9
+ if (!t)
10
+ return void res.status(404).json({ error: 'agent_not_found' });
11
+ const min = getRawModeMinTrust();
12
+ res.json({
13
+ api_key_prefix: key.slice(0, 12) + '…',
14
+ user_id: t.user_id,
15
+ trust_score: t.trust_score,
16
+ level: t.level,
17
+ signals: t.signals,
18
+ raw_mode_enabled: t.trust_score >= min,
19
+ raw_mode_min_trust: min,
20
+ });
21
+ });
22
+ app.get('/api/admin/agents/:api_key/reputation', (req, res) => {
23
+ const admin = auth(req, res);
24
+ if (!admin)
25
+ return;
26
+ if (admin.role !== 'admin')
27
+ return void res.status(403).json({ error: '仅管理员可查询其他 agent' });
28
+ const target = req.params.api_key;
29
+ const t = getAgentTrustCached(target);
30
+ if (!t)
31
+ return void res.status(404).json({ error: 'agent_not_found' });
32
+ res.json(t);
33
+ });
34
+ }