@seasonkoh/webaz 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- package/package.json +76 -3
|
@@ -1,61 +1,63 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerPromoterRoutes(app, deps) {
|
|
2
3
|
const { db, auth, isAllowedSponsor } = deps;
|
|
3
|
-
|
|
4
|
+
void db; // RFC-016: 本文件已全量走异步 seam;db 仍在 deps 由调用方注入,此处不直接使用
|
|
5
|
+
app.get('/api/promoter/dashboard', async (req, res) => {
|
|
4
6
|
const user = auth(req, res);
|
|
5
7
|
if (!user)
|
|
6
8
|
return;
|
|
7
9
|
const userId = user.id;
|
|
8
|
-
const l1 =
|
|
9
|
-
const l2 =
|
|
10
|
+
const l1 = (await dbOne("SELECT COUNT(*) as n FROM users WHERE sponsor_id = ?", [userId])).n;
|
|
11
|
+
const l2 = (await dbOne(`
|
|
10
12
|
SELECT COUNT(*) as n FROM users
|
|
11
13
|
WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
|
|
12
|
-
|
|
13
|
-
const l3 =
|
|
14
|
+
`, [userId])).n;
|
|
15
|
+
const l3 = (await dbOne(`
|
|
14
16
|
SELECT COUNT(*) as n FROM users
|
|
15
17
|
WHERE sponsor_id IN (
|
|
16
18
|
SELECT id FROM users WHERE sponsor_id IN (SELECT id FROM users WHERE sponsor_id = ?)
|
|
17
19
|
)
|
|
18
|
-
|
|
19
|
-
const earned =
|
|
20
|
+
`, [userId])).n;
|
|
21
|
+
const earned = await dbAll(`
|
|
20
22
|
SELECT level, COUNT(*) as orders, COALESCE(SUM(amount),0) as total
|
|
21
23
|
FROM commission_records WHERE beneficiary_id = ?
|
|
22
24
|
GROUP BY level
|
|
23
|
-
|
|
25
|
+
`, [userId]);
|
|
24
26
|
const byLevel = { 1: { orders: 0, total: 0 }, 2: { orders: 0, total: 0 }, 3: { orders: 0, total: 0 } };
|
|
25
27
|
for (const r of earned)
|
|
26
28
|
byLevel[r.level] = { orders: r.orders, total: r.total };
|
|
27
29
|
const grand = byLevel[1].total + byLevel[2].total + byLevel[3].total;
|
|
28
|
-
const recent =
|
|
30
|
+
const recent = await dbAll(`
|
|
29
31
|
SELECT cr.id, cr.order_id, cr.level, cr.amount, cr.rate, cr.created_at,
|
|
30
32
|
u.name as source_buyer_name
|
|
31
33
|
FROM commission_records cr
|
|
32
34
|
LEFT JOIN users u ON u.id = cr.source_buyer_id
|
|
33
35
|
WHERE cr.beneficiary_id = ?
|
|
34
36
|
ORDER BY cr.created_at DESC LIMIT 20
|
|
35
|
-
|
|
36
|
-
const me =
|
|
37
|
-
const mySponsor = me?.sponsor_id ?
|
|
38
|
-
const myUser =
|
|
39
|
-
const leftChildName = myUser?.left_child_id ?
|
|
40
|
-
const rightChildName = myUser?.right_child_id ?
|
|
41
|
-
const myPlacementName = myUser?.placement_id ?
|
|
42
|
-
const scoreAgg =
|
|
37
|
+
`, [userId]);
|
|
38
|
+
const me = (await dbOne("SELECT sponsor_id, sponsor_path, region FROM users WHERE id = ?", [userId]));
|
|
39
|
+
const mySponsor = me?.sponsor_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [me.sponsor_id])) : null;
|
|
40
|
+
const myUser = await dbOne("SELECT total_left_pv, total_right_pv, left_child_id, right_child_id, placement_id, placement_side FROM users WHERE id = ?", [userId]);
|
|
41
|
+
const leftChildName = myUser?.left_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.left_child_id]))?.name : null;
|
|
42
|
+
const rightChildName = myUser?.right_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.right_child_id]))?.name : null;
|
|
43
|
+
const myPlacementName = myUser?.placement_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [myUser.placement_id]))?.name : null;
|
|
44
|
+
const scoreAgg = (await dbOne(`
|
|
43
45
|
SELECT
|
|
44
46
|
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score ELSE 0 END),0) as pending_score,
|
|
45
47
|
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount ELSE 0 END),0) as settled_waz,
|
|
46
48
|
COUNT(*) as total_hits
|
|
47
49
|
FROM binary_score_records WHERE user_id = ?
|
|
48
|
-
|
|
49
|
-
const recentBinary =
|
|
50
|
+
`, [userId]));
|
|
51
|
+
const recentBinary = await dbAll(`
|
|
50
52
|
SELECT id, tier, score, settled_at, waz_amount, created_at
|
|
51
53
|
FROM binary_score_records WHERE user_id = ?
|
|
52
54
|
ORDER BY created_at DESC LIMIT 10
|
|
53
|
-
|
|
54
|
-
const tiers =
|
|
55
|
+
`, [userId]);
|
|
56
|
+
const tiers = await dbAll("SELECT tier, pv_threshold, score_per_hit FROM binary_tier_config WHERE active=1 ORDER BY tier ASC");
|
|
55
57
|
const canL1Share = isAllowedSponsor(userId);
|
|
56
|
-
const completedOrders =
|
|
57
|
-
const overrideRow =
|
|
58
|
-
const shareableProducts =
|
|
58
|
+
const completedOrders = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
|
|
59
|
+
const overrideRow = await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]);
|
|
60
|
+
const shareableProducts = await dbAll(`
|
|
59
61
|
SELECT p.id, p.title, p.price, p.category, p.commission_rate,
|
|
60
62
|
(SELECT COUNT(*) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as total_sales,
|
|
61
63
|
COALESCE((SELECT SUM(cr.amount) FROM commission_records cr
|
|
@@ -66,20 +68,20 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
66
68
|
AND p.commission_rate IS NOT NULL AND p.commission_rate > 0
|
|
67
69
|
AND p.status = 'active'
|
|
68
70
|
ORDER BY my_earned DESC, total_sales DESC LIMIT 20
|
|
69
|
-
|
|
70
|
-
const earnedLast30 =
|
|
71
|
+
`, [userId, userId]);
|
|
72
|
+
const earnedLast30 = (await dbOne(`
|
|
71
73
|
SELECT COALESCE(SUM(amount),0) as total FROM commission_records
|
|
72
74
|
WHERE beneficiary_id = ? AND created_at >= datetime('now','-30 days')
|
|
73
|
-
|
|
74
|
-
const earnedPrev30 =
|
|
75
|
+
`, [userId])).total;
|
|
76
|
+
const earnedPrev30 = (await dbOne(`
|
|
75
77
|
SELECT COALESCE(SUM(amount),0) as total FROM commission_records
|
|
76
78
|
WHERE beneficiary_id = ? AND created_at >= datetime('now','-60 days')
|
|
77
79
|
AND created_at < datetime('now','-30 days')
|
|
78
|
-
|
|
79
|
-
const wazLast30 =
|
|
80
|
+
`, [userId])).total;
|
|
81
|
+
const wazLast30 = (await dbOne(`
|
|
80
82
|
SELECT COALESCE(SUM(waz_amount),0) as total FROM binary_score_records
|
|
81
83
|
WHERE user_id = ? AND settled_at >= datetime('now','-30 days')
|
|
82
|
-
|
|
84
|
+
`, [userId])).total;
|
|
83
85
|
const projection = {
|
|
84
86
|
last_30_commission: earnedLast30,
|
|
85
87
|
prev_30_commission: earnedPrev30,
|
|
@@ -88,18 +90,9 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
88
90
|
next_30_estimate: earnedLast30 + wazLast30,
|
|
89
91
|
};
|
|
90
92
|
const insights = [];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const max = Math.max(leftPv, rightPv), min = Math.min(leftPv, rightPv);
|
|
95
|
-
const ratio = max > 0 ? min / max : 0;
|
|
96
|
-
const weak = leftPv < rightPv ? '左区' : '右区';
|
|
97
|
-
if (ratio < 0.5)
|
|
98
|
-
insights.push({ type: 'weak_leg', level: 'warn', text: `${weak} PV 仅为强腿的 ${(ratio * 100).toFixed(0)}% — 建议主推${weak},对碰 = min(L,R) × tier_score` });
|
|
99
|
-
else
|
|
100
|
-
insights.push({ type: 'balanced', level: 'success', text: `双腿均衡度 ${(ratio * 100).toFixed(0)}% — 对碰节奏健康` });
|
|
101
|
-
}
|
|
102
|
-
const lastInvite = db.prepare(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`).get(userId);
|
|
93
|
+
// pre-public de-MLM:移除弱腿 / pairing / PV-tier 经营建议 —— PV 对碰为 pre-launch、未对用户启用,
|
|
94
|
+
// 不在用户面 surface 营销主推弱腿 / pairing 公式 / PV-tier 进度等玩法。位置 / PV 仅为参与记录,非收益路径。
|
|
95
|
+
const lastInvite = (await dbOne(`SELECT MAX(created_at) as t FROM users WHERE sponsor_id = ?`, [userId]));
|
|
103
96
|
if (lastInvite.t) {
|
|
104
97
|
const days = Math.floor((Date.now() - new Date(lastInvite.t).getTime()) / 86400_000);
|
|
105
98
|
if (days > 14)
|
|
@@ -110,21 +103,16 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
110
103
|
else if (l1 === 0) {
|
|
111
104
|
insights.push({ type: 'no_team', level: 'info', text: `还没有直推 — 先分享你买过且好评的商品给好友` });
|
|
112
105
|
}
|
|
113
|
-
const pair = Math.min(leftPv, rightPv);
|
|
114
|
-
const nextTier = tiers.find(t => t.pv_threshold > pair);
|
|
115
|
-
if (nextTier && pair > 0 && pair / nextTier.pv_threshold > 0.7) {
|
|
116
|
-
insights.push({ type: 'near_tier', level: 'success', text: `距离 tier ${nextTier.tier} 仅差 ${(nextTier.pv_threshold - pair).toLocaleString()} PV (+${nextTier.score_per_hit} Score / 次)` });
|
|
117
|
-
}
|
|
118
106
|
if (!canL1Share && completedOrders === 0) {
|
|
119
|
-
insights.push({ type: '
|
|
107
|
+
insights.push({ type: 'share_hint', level: 'info', text: `完成首笔购买后可使用分享功能;分享记录仅作归因 / 参与记录,不构成收益承诺。` });
|
|
120
108
|
}
|
|
121
109
|
if (shareableProducts.length > 0 && grand === 0) {
|
|
122
110
|
insights.push({ type: 'first_share', level: 'info', text: `你有 ${shareableProducts.length} 个可分享商品但暂无成交 — 试着把链接发给身边的人` });
|
|
123
111
|
}
|
|
124
|
-
const treeNode = (uid) => {
|
|
112
|
+
const treeNode = async (uid) => {
|
|
125
113
|
if (!uid)
|
|
126
114
|
return null;
|
|
127
|
-
const u =
|
|
115
|
+
const u = await dbOne("SELECT id, name, total_left_pv, total_right_pv, left_child_id, right_child_id FROM users WHERE id = ?", [uid]);
|
|
128
116
|
if (!u)
|
|
129
117
|
return null;
|
|
130
118
|
return {
|
|
@@ -135,25 +123,29 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
135
123
|
right_id: u.right_child_id ?? null,
|
|
136
124
|
};
|
|
137
125
|
};
|
|
138
|
-
const me_node = treeNode(userId);
|
|
139
|
-
const left_node = treeNode(myUser?.left_child_id);
|
|
140
|
-
const right_node = treeNode(myUser?.right_child_id);
|
|
126
|
+
const me_node = await treeNode(userId);
|
|
127
|
+
const left_node = await treeNode(myUser?.left_child_id);
|
|
128
|
+
const right_node = await treeNode(myUser?.right_child_id);
|
|
141
129
|
const binaryTree = {
|
|
142
130
|
me: me_node,
|
|
143
131
|
left: left_node,
|
|
144
132
|
right: right_node,
|
|
145
|
-
ll: treeNode(left_node?.left_id),
|
|
146
|
-
lr: treeNode(left_node?.right_id),
|
|
147
|
-
rl: treeNode(right_node?.left_id),
|
|
148
|
-
rr: treeNode(right_node?.right_id),
|
|
133
|
+
ll: await treeNode(left_node?.left_id),
|
|
134
|
+
lr: await treeNode(left_node?.right_id),
|
|
135
|
+
rl: await treeNode(right_node?.left_id),
|
|
136
|
+
rr: await treeNode(right_node?.right_id),
|
|
149
137
|
};
|
|
150
|
-
const meCard =
|
|
151
|
-
|
|
138
|
+
const meCard = await dbOne("SELECT permanent_code, handle FROM users WHERE id = ?", [userId]);
|
|
139
|
+
// invite links use permanent_code ONLY — never fall back to user_id (would leak usr_xxx into ?ref).
|
|
140
|
+
const codeForLink = meCard?.permanent_code || null;
|
|
141
|
+
const host = `${req.protocol}://${req.get('host')}`;
|
|
152
142
|
res.json({
|
|
153
143
|
user_id: userId,
|
|
154
144
|
permanent_code: meCard?.permanent_code || null,
|
|
155
145
|
handle: meCard?.handle || null,
|
|
156
|
-
|
|
146
|
+
invite_code_available: !!codeForLink,
|
|
147
|
+
referral_link: codeForLink ? `${host}/i/${codeForLink}` : null,
|
|
148
|
+
invite_unavailable_reason: codeForLink ? null : 'permanent_code_missing — refresh or contact support',
|
|
157
149
|
region: me?.region || 'global',
|
|
158
150
|
my_sponsor: mySponsor ? { id: me.sponsor_id, name: mySponsor.name } : null,
|
|
159
151
|
permissions: {
|
|
@@ -176,8 +168,8 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
176
168
|
projection,
|
|
177
169
|
insights,
|
|
178
170
|
atomic: {
|
|
179
|
-
left_invite_url: `${
|
|
180
|
-
right_invite_url: `${
|
|
171
|
+
left_invite_url: codeForLink ? `${host}/i/${codeForLink}-L` : null,
|
|
172
|
+
right_invite_url: codeForLink ? `${host}/i/${codeForLink}-R` : null,
|
|
181
173
|
total_left_pv: Number(myUser?.total_left_pv ?? 0),
|
|
182
174
|
total_right_pv: Number(myUser?.total_right_pv ?? 0),
|
|
183
175
|
left_child: myUser?.left_child_id ? { id: myUser.left_child_id, name: leftChildName } : null,
|
|
@@ -191,18 +183,18 @@ export function registerPromoterRoutes(app, deps) {
|
|
|
191
183
|
});
|
|
192
184
|
});
|
|
193
185
|
// 直推 L1 列表
|
|
194
|
-
app.get('/api/promoter/team', (req, res) => {
|
|
186
|
+
app.get('/api/promoter/team', async (req, res) => {
|
|
195
187
|
const user = auth(req, res);
|
|
196
188
|
if (!user)
|
|
197
189
|
return;
|
|
198
190
|
const userId = user.id;
|
|
199
|
-
const rows =
|
|
191
|
+
const rows = await dbAll(`
|
|
200
192
|
SELECT u.id, u.name, u.created_at, u.region,
|
|
201
193
|
(SELECT COUNT(*) FROM users WHERE sponsor_id = u.id) as their_l1,
|
|
202
194
|
COALESCE((SELECT SUM(amount) FROM commission_records WHERE beneficiary_id = ? AND source_buyer_id = u.id), 0) as my_earned_from_them
|
|
203
195
|
FROM users u WHERE u.sponsor_id = ?
|
|
204
196
|
ORDER BY u.created_at DESC LIMIT 100
|
|
205
|
-
|
|
197
|
+
`, [userId, userId]);
|
|
206
198
|
res.json({ team: rows });
|
|
207
199
|
});
|
|
208
200
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listBuildTasksWithAgentMetadata, getBuildTaskWithAgentMetadata, validateTaskFilters, withContributionReadEnvelope } from '../../layer2-business/L2-9-contribution/build-task-read.js';
|
|
2
|
+
export function registerPublicBuildTasksRoutes(app, deps) {
|
|
3
|
+
const { db, errorRes } = deps;
|
|
4
|
+
app.get('/api/public/build-tasks', (req, res) => {
|
|
5
|
+
const v = validateTaskFilters(req.query);
|
|
6
|
+
if (!v.ok)
|
|
7
|
+
return void errorRes(res, 400, v.code, v.detail); // fail-closed: bad filter → typed 400
|
|
8
|
+
const tasks = listBuildTasksWithAgentMetadata(db, v.filters, 'public');
|
|
9
|
+
res.json(withContributionReadEnvelope({ tasks }));
|
|
10
|
+
});
|
|
11
|
+
app.get('/api/public/build-tasks/:id', (req, res) => {
|
|
12
|
+
// 'public' scope already restricts to audience=public + status=open; a non-visible task returns null →
|
|
13
|
+
// 404 (same as truly-missing, so existence of a restricted/internal task is never disclosed).
|
|
14
|
+
const task = getBuildTaskWithAgentMetadata(db, String(req.params.id), 'public');
|
|
15
|
+
if (!task)
|
|
16
|
+
return void errorRes(res, 404, 'NOT_FOUND', '任务不存在');
|
|
17
|
+
res.json(withContributionReadEnvelope({ task }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { SOFTWARE_VERSION, CONTRACT_VERSION } from '../../version.js';
|
|
5
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
5
6
|
import { capabilityMatrix } from '../endpoint-actions.js';
|
|
6
7
|
import { buildEntityDictionary } from '../entity-dictionary.js';
|
|
7
8
|
import { buildGoalIndex } from '../goal-index.js';
|
|
@@ -13,13 +14,13 @@ import { buildNegativeSpace } from '../negative-space.js';
|
|
|
13
14
|
import { buildAcpProductFeed } from '../acp-feed.js';
|
|
14
15
|
export function registerPublicUtilsRoutes(app, deps) {
|
|
15
16
|
const { db, MASTER_SEED, NODE_ENV, SERVICE_START_MS, rateLimitOk, generateManifest, getUser, logError, issuerAddress } = deps;
|
|
16
|
-
app.get('/api/health', (_req, res) => {
|
|
17
|
+
app.get('/api/health', async (_req, res) => {
|
|
17
18
|
const t0 = Date.now();
|
|
18
19
|
let dbOk = false;
|
|
19
20
|
let dbLatency = 0;
|
|
20
21
|
try {
|
|
21
22
|
const t1 = Date.now();
|
|
22
|
-
const r =
|
|
23
|
+
const r = await dbOne('SELECT 1 as ok');
|
|
23
24
|
dbLatency = Date.now() - t1;
|
|
24
25
|
dbOk = r?.ok === 1;
|
|
25
26
|
}
|
|
@@ -37,7 +38,7 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
37
38
|
check_ms: Date.now() - t0,
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
|
-
app.post('/api/mcp-telemetry', (req, res) => {
|
|
41
|
+
app.post('/api/mcp-telemetry', async (req, res) => {
|
|
41
42
|
const ip = req.ip || 'unknown';
|
|
42
43
|
if (!rateLimitOk(ip))
|
|
43
44
|
return void res.status(429).json({ error: 'rate-limited' });
|
|
@@ -55,17 +56,17 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
55
56
|
const uih = typeof user_id_hash === 'string' && /^[0-9a-f]{1,32}$/.test(user_id_hash) ? user_id_hash : null;
|
|
56
57
|
const sv = typeof server_version === 'string' && server_version.length <= 32 ? server_version : null;
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
+
await dbRun(`
|
|
59
60
|
INSERT INTO mcp_tool_calls (tool_name, user_id_hash, server_version, outcome, latency_ms)
|
|
60
61
|
VALUES (?, ?, ?, ?, ?)
|
|
61
|
-
|
|
62
|
+
`, [tool_name, uih, sv, outcome, Math.round(lat)]);
|
|
62
63
|
}
|
|
63
64
|
catch { /* swallow — never fail telemetry */ }
|
|
64
65
|
res.json({ ok: true });
|
|
65
66
|
});
|
|
66
|
-
app.get('/api/system-flags', (_req, res) => {
|
|
67
|
-
const requireRef =
|
|
68
|
-
const inviteRotation =
|
|
67
|
+
app.get('/api/system-flags', async (_req, res) => {
|
|
68
|
+
const requireRef = (await dbOne("SELECT value FROM system_state WHERE key='require_ref_to_register'"))?.value === '1';
|
|
69
|
+
const inviteRotation = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
|
|
69
70
|
// #1049 Turnstile 公钥(若启用),前端注册表单 widget 用
|
|
70
71
|
const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || null;
|
|
71
72
|
res.json({
|
|
@@ -79,11 +80,11 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
79
80
|
// /api/protocol-status — JSON API 别名(同份内容)
|
|
80
81
|
// 内容:network_state(协议处于哪一阶段 + 诚实免责) + issuers(信任锚地址 + 轮换历史)
|
|
81
82
|
// 信任锚是 Phase 4 凭证(/api/me/agents/:prefix/passport)的验签依据,陌生第三方靠这个端点找到"webaz 官方地址"。
|
|
82
|
-
function buildProtocolManifest() {
|
|
83
|
-
const phase =
|
|
83
|
+
async function buildProtocolManifest() {
|
|
84
|
+
const phase = (await dbOne("SELECT value FROM system_state WHERE key='protocol_phase'"))?.value || 'pre_launch';
|
|
84
85
|
// real_users = 已绑 Passkey 的账号数(我们的"真人"定义);pre-launch 应当 ≈0
|
|
85
|
-
const realUsers =
|
|
86
|
-
const issuerActiveSince =
|
|
86
|
+
const realUsers = (await dbOne("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials"))?.n ?? 0;
|
|
87
|
+
const issuerActiveSince = (await dbOne("SELECT value FROM system_state WHERE key='issuer_active_since'"))?.value || '2026-05-30';
|
|
87
88
|
return {
|
|
88
89
|
name: 'WebAZ Protocol',
|
|
89
90
|
// RFC-011 §④ 两轴版本(单一来源 src/version.ts):
|
|
@@ -152,11 +153,12 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
152
153
|
change_feed: 'https://webaz.xyz/api/agent/changes', // ④ 契约变更 + 指纹 + 弃用
|
|
153
154
|
verifiability_index: 'https://webaz.xyz/.well-known/webaz-verifiability.json', // ⑤ 什么可验+怎么验
|
|
154
155
|
economic_participation: 'https://webaz.xyz/.well-known/webaz-economic.json', // ⑧ value-participant 角色经济条款(费率实时)
|
|
156
|
+
launch_pulse: 'https://webaz.xyz/.well-known/webaz-launch-pulse.json', // L2 follow-the-launch:诚实 live 计数 + 动量 + 里程碑
|
|
155
157
|
negative_space: 'https://webaz.xyz/.well-known/webaz-negative-space.json', // ② 负空间(禁区 + 限额 + 后果阶梯)
|
|
156
158
|
event_stream: 'https://webaz.xyz/api/agent/events?since=<cursor>', // ⑥ 事件游标流(party-gated,需 auth)
|
|
157
159
|
passport: 'https://webaz.xyz/api/me/agents/:apiKeyPrefix/passport', // ⑤ 可验护照
|
|
158
160
|
did: 'https://webaz.xyz/.well-known/did.json',
|
|
159
|
-
acp_product_feed: 'https://webaz.xyz/.well-known/webaz-acp-feed.json', // RFC-015 P0 — ACP
|
|
161
|
+
acp_product_feed: 'https://webaz.xyz/.well-known/webaz-acp-feed.json', // RFC-015 P0 — ACP-inspired 商品【发现】投影(非 strict ACP-ingestable;只读;is_eligible_checkout=false;见 feed.compatibility)
|
|
160
162
|
},
|
|
161
163
|
// 路线图 — 回应"知道还有哪些没做"的诚实化第三层。哲学:公开当前到达点 + 已知未做项,不承诺时间表。
|
|
162
164
|
roadmap: {
|
|
@@ -183,13 +185,66 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
183
185
|
},
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
|
-
app.get('/.well-known/webaz-protocol.json', (_req, res) => {
|
|
188
|
+
app.get('/.well-known/webaz-protocol.json', async (_req, res) => {
|
|
187
189
|
res.setHeader('Cache-Control', 'public, max-age=300'); // 5min 边缘缓存,降轮询
|
|
188
|
-
res.json(buildProtocolManifest());
|
|
190
|
+
res.json(await buildProtocolManifest());
|
|
189
191
|
});
|
|
190
|
-
app.get('/api/protocol-status', (_req, res) => {
|
|
192
|
+
app.get('/api/protocol-status', async (_req, res) => {
|
|
191
193
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
192
|
-
res.json(buildProtocolManifest());
|
|
194
|
+
res.json(await buildProtocolManifest());
|
|
195
|
+
});
|
|
196
|
+
// L2 onboarding「follow the launch」(2026-06-08):pre-launch 没有"买"作为回访理由,
|
|
197
|
+
// 给早期信徒一个【诚实】的"看协议苏醒"面 —— 真实计数 + 7d 动量 + 里程碑(firsts),零粉饰。
|
|
198
|
+
// 纯公开读(无 auth),网站侧(不进 MCP 包,Railway 部署即生效)。
|
|
199
|
+
// RFC-016 Phase 0 试点:本函数改用异步 DB seam(dbOne)。其余 call site 后续分批迁。
|
|
200
|
+
async function buildLaunchPulse() {
|
|
201
|
+
const count = async (sql) => ((await dbOne(sql))?.n ?? 0);
|
|
202
|
+
const firstAt = async (sql) => ((await dbOne(sql))?.t ?? null);
|
|
203
|
+
const phase = (await dbOne("SELECT value FROM system_state WHERE key='protocol_phase'"))?.value || 'pre_launch';
|
|
204
|
+
const [passkey, sellers, products, completed, disputesResolved, newPasskey7d, orders7d, firstSeller, firstProduct, firstOrder, firstCompleted, firstDispute] = await Promise.all([
|
|
205
|
+
count("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials"),
|
|
206
|
+
count("SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%seller%' AND (deleted_at IS NULL OR deleted_at = '')"),
|
|
207
|
+
count("SELECT COUNT(*) AS n FROM products WHERE status='active'"),
|
|
208
|
+
count("SELECT COUNT(*) AS n FROM orders WHERE status='completed'"),
|
|
209
|
+
count("SELECT COUNT(*) AS n FROM disputes WHERE status='resolved'"),
|
|
210
|
+
count("SELECT COUNT(DISTINCT user_id) AS n FROM webauthn_credentials WHERE created_at > datetime('now','-7 day')"),
|
|
211
|
+
count("SELECT COUNT(*) AS n FROM orders WHERE created_at > datetime('now','-7 day')"),
|
|
212
|
+
firstAt("SELECT MIN(created_at) AS t FROM users WHERE roles LIKE '%seller%'"),
|
|
213
|
+
firstAt("SELECT MIN(created_at) AS t FROM products"),
|
|
214
|
+
firstAt("SELECT MIN(created_at) AS t FROM orders"),
|
|
215
|
+
firstAt("SELECT MIN(updated_at) AS t FROM orders WHERE status='completed'"),
|
|
216
|
+
firstAt("SELECT MIN(resolved_at) AS t FROM disputes WHERE status='resolved'"),
|
|
217
|
+
]);
|
|
218
|
+
return {
|
|
219
|
+
phase,
|
|
220
|
+
as_of: new Date().toISOString(),
|
|
221
|
+
note: 'Honest pre-launch pulse — real counts, zero inflation. Watch the protocol wake up; come back to see it grow.',
|
|
222
|
+
participants: { passkey_bound_humans: passkey, sellers },
|
|
223
|
+
catalog: { active_products: products },
|
|
224
|
+
activity: {
|
|
225
|
+
completed_orders: completed,
|
|
226
|
+
disputes_resolved: disputesResolved,
|
|
227
|
+
new_passkey_humans_7d: newPasskey7d,
|
|
228
|
+
orders_7d: orders7d,
|
|
229
|
+
},
|
|
230
|
+
milestones: {
|
|
231
|
+
first_seller_at: firstSeller,
|
|
232
|
+
first_product_listed_at: firstProduct,
|
|
233
|
+
first_order_at: firstOrder,
|
|
234
|
+
first_order_completed_at: firstCompleted,
|
|
235
|
+
first_dispute_resolved_at: firstDispute,
|
|
236
|
+
},
|
|
237
|
+
next: 'Public launch unlocks real settlement. Follow it / get notified at launch + request an invite: https://webaz.xyz/#welcome',
|
|
238
|
+
honesty: 'Pre-launch: WAZ is a simulated test currency; no real money settles yet. These numbers are the live protocol state, not market-size or investment signal.',
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
app.get('/.well-known/webaz-launch-pulse.json', async (_req, res) => {
|
|
242
|
+
res.setHeader('Cache-Control', 'public, max-age=120');
|
|
243
|
+
res.json(await buildLaunchPulse());
|
|
244
|
+
});
|
|
245
|
+
app.get('/api/launch-pulse', async (_req, res) => {
|
|
246
|
+
res.setHeader('Cache-Control', 'public, max-age=120');
|
|
247
|
+
res.json(await buildLaunchPulse());
|
|
193
248
|
});
|
|
194
249
|
// RFC-011 §② — agent 可读能力矩阵(写边界 action-scope + 敏感读 scope)。
|
|
195
250
|
// live = 直接序列化 enforce 用的规则表(src/pwa/endpoint-actions.ts),doc=code 零漂移。
|
|
@@ -266,32 +321,39 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
266
321
|
});
|
|
267
322
|
// RFC-011 §⑧ 经济参与索引 —— value-participant 角色 × 赚什么/押什么/担什么责,
|
|
268
323
|
// 费率【实时】从 protocol_params 读(doc=code,永不和 enforced 经济漂移)。
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
324
|
+
// RFC-016: 一次性异步预取全部 protocol_params → Map,再返回同步 getter,喂给
|
|
325
|
+
// buildEconomicParticipation / buildNegativeSpace(保持其同步签名,不动共享 module),仍是 doc=code 实时读。
|
|
326
|
+
const loadLiveParam = async () => {
|
|
327
|
+
const paramRows = await dbAll('SELECT key, value, type FROM protocol_params');
|
|
328
|
+
const paramMap = new Map(paramRows.map(r => [r.key, r]));
|
|
329
|
+
return (key, fallback) => {
|
|
330
|
+
const row = paramMap.get(key);
|
|
331
|
+
if (!row)
|
|
332
|
+
return fallback;
|
|
333
|
+
if (row.type === 'number')
|
|
334
|
+
return Number(row.value);
|
|
335
|
+
if (row.type === 'boolean')
|
|
336
|
+
return (row.value === 'true' || row.value === '1');
|
|
337
|
+
return row.value;
|
|
338
|
+
};
|
|
278
339
|
};
|
|
279
|
-
const economic = (_req, res) => {
|
|
340
|
+
const economic = async (_req, res) => {
|
|
280
341
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
281
|
-
res.json(buildEconomicParticipation(
|
|
342
|
+
res.json(buildEconomicParticipation(await loadLiveParam()));
|
|
282
343
|
};
|
|
283
344
|
app.get('/.well-known/webaz-economic.json', economic);
|
|
284
345
|
app.get('/api/agent/economic-participation', economic);
|
|
285
346
|
// RFC-011 §② 负空间 —— 禁区 + enforced 限额 + 后果阶梯(per-agent 速率实时读)。
|
|
286
|
-
const negativeSpace = (_req, res) => {
|
|
347
|
+
const negativeSpace = async (_req, res) => {
|
|
287
348
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
288
|
-
res.json(buildNegativeSpace(
|
|
349
|
+
res.json(buildNegativeSpace(await loadLiveParam()));
|
|
289
350
|
};
|
|
290
351
|
app.get('/.well-known/webaz-negative-space.json', negativeSpace);
|
|
291
352
|
app.get('/api/agent/negative-space', negativeSpace);
|
|
292
|
-
// RFC-015 P0 —— ACP
|
|
293
|
-
// 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)
|
|
294
|
-
// (ACP /complete 是卡+PSP,WebAZ 未接)
|
|
353
|
+
// RFC-015 P0 —— ACP-inspired 商品【发现】投影:把现有商品投影成 OpenAI Agentic Commerce 的 feed 形状,
|
|
354
|
+
// 让 ACP/ChatGPT agent 能【发现】WebAZ 商品(只读,无钱)。【非 strict ACP-ingestable feed】(Codex #151):
|
|
355
|
+
// currency=WAZ 非 ISO 4217、is_eligible_checkout 恒 false(ACP /complete 是卡+PSP,WebAZ 未接)、
|
|
356
|
+
// 不发 target_countries/store_country 等商家必填字段 —— 非合规点逐条见 feed.compatibility。详见 buildAcpProductFeed + RFC-015。
|
|
295
357
|
const acpFeed = (_req, res) => {
|
|
296
358
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
297
359
|
res.json(buildAcpProductFeed(db));
|
|
@@ -338,8 +400,8 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
338
400
|
],
|
|
339
401
|
});
|
|
340
402
|
});
|
|
341
|
-
app.get('/api/editor-picks', (_req, res) => {
|
|
342
|
-
const products =
|
|
403
|
+
app.get('/api/editor-picks', async (_req, res) => {
|
|
404
|
+
const products = await dbAll(`
|
|
343
405
|
SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
|
|
344
406
|
p.title as product_title, p.price, p.images, p.category,
|
|
345
407
|
u.handle as seller_handle
|
|
@@ -348,30 +410,30 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
348
410
|
JOIN users u ON u.id = p.seller_id
|
|
349
411
|
WHERE ep.kind = 'product' AND ep.starts_at <= datetime('now') AND ep.ends_at > datetime('now')
|
|
350
412
|
ORDER BY ep.sort_order ASC, ep.created_at DESC LIMIT 20
|
|
351
|
-
`)
|
|
352
|
-
const sellers =
|
|
413
|
+
`);
|
|
414
|
+
const sellers = await dbAll(`
|
|
353
415
|
SELECT ep.id, ep.target_id, ep.title, ep.note, ep.starts_at, ep.ends_at, ep.sort_order,
|
|
354
416
|
u.handle, u.name, u.shop_banner_url, u.bio
|
|
355
417
|
FROM editor_picks ep
|
|
356
418
|
JOIN users u ON u.id = ep.target_id AND u.role = 'seller'
|
|
357
419
|
WHERE ep.kind = 'seller' AND ep.starts_at <= datetime('now') AND ep.ends_at > datetime('now')
|
|
358
420
|
ORDER BY ep.sort_order ASC, ep.created_at DESC LIMIT 20
|
|
359
|
-
`)
|
|
421
|
+
`);
|
|
360
422
|
res.json({ products, sellers });
|
|
361
423
|
});
|
|
362
|
-
app.get('/api/manifest', (_req, res) => {
|
|
363
|
-
res.json(generateManifest(db));
|
|
424
|
+
app.get('/api/manifest', async (_req, res) => {
|
|
425
|
+
res.json(await generateManifest(db));
|
|
364
426
|
});
|
|
365
427
|
// W3.5-B 治理上岗公开 stats(docs/GOVERNANCE-ONBOARDING.md)
|
|
366
428
|
// 无 auth — agent / 用户 / 第三方都可读;不暴露 PII
|
|
367
|
-
app.get('/api/governance/onboarding-stats', (_req, res) => {
|
|
429
|
+
app.get('/api/governance/onboarding-stats', async (_req, res) => {
|
|
368
430
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
369
431
|
try {
|
|
370
432
|
// active counts(users.roles 含 arbitrator / verifier 的人数,fixture 也算)
|
|
371
|
-
const arbitratorCount =
|
|
372
|
-
const verifierCount =
|
|
433
|
+
const arbitratorCount = (await dbOne(`SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%arbitrator%' AND (deleted_at IS NULL OR deleted_at = '')`))?.n ?? 0;
|
|
434
|
+
const verifierCount = (await dbOne(`SELECT COUNT(*) AS n FROM users WHERE roles LIKE '%verifier%' AND (deleted_at IS NULL OR deleted_at = '')`))?.n ?? 0;
|
|
373
435
|
// pending applications
|
|
374
|
-
const pendingCount =
|
|
436
|
+
const pendingCount = (await dbOne(`SELECT COUNT(*) AS n FROM governance_applications WHERE status = 'pending_onboarding'`))?.n ?? 0;
|
|
375
437
|
// 资格门槛 snapshot(给前端 pre-check 显示)
|
|
376
438
|
// ⚠️ 2026-06-03 #4 修:此前这里 dump 装饰性 protocol_params.governance_onboarding.*,
|
|
377
439
|
// 与代码实际 enforce 的门槛不符(例 min_completed_orders param=5,但代码 arbitrator 要 50 /
|
|
@@ -380,10 +442,10 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
380
442
|
// ⚠️ 必须与 server.ts checkArbitratorEligibility / checkVerifierEligibility 保持同步。
|
|
381
443
|
const eligibility = {
|
|
382
444
|
arbitrator: { registration_days: 90, completed_orders: 50, reputation: 300, balance_waz: 500, email_verified: true, zero_disputes_lost: true, never_suspended: true },
|
|
383
|
-
verifier: { registration_days: 60, completed_orders: 20, email_verified: true, zero_disputes_lost: true, never_suspended: true },
|
|
445
|
+
verifier: { registration_days: 60, completed_orders: 20, reputation: 110, balance_waz: 200, email_verified: true, zero_disputes_lost: true, never_suspended: true },
|
|
384
446
|
};
|
|
385
447
|
// quiz_pass_score 是真正被代码读取的 param(governance-onboarding.ts quiz-submit),保留。
|
|
386
|
-
const quizPassRow =
|
|
448
|
+
const quizPassRow = await dbOne(`SELECT value FROM protocol_params WHERE key = 'governance_onboarding.quiz_pass_score'`);
|
|
387
449
|
const quizPassScore = Number(quizPassRow?.value ?? 80);
|
|
388
450
|
res.json({
|
|
389
451
|
phase: 'A',
|
package/dist/pwa/routes/push.js
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
/** web-push 失败回调:删除已失效订阅。导出以备 P1-5 任务调用。
|
|
3
|
+
* RFC-016: db 参数保留(调用方签名兼容),内部走异步 seam(同实例,setSeamDb)。 */
|
|
4
|
+
export async function cleanupStaleSubscription(_db, endpoint) {
|
|
3
5
|
if (!endpoint)
|
|
4
6
|
return;
|
|
5
|
-
|
|
7
|
+
await dbRun('DELETE FROM push_subscriptions WHERE endpoint = ?', [endpoint]);
|
|
6
8
|
}
|
|
7
9
|
export function registerPushRoutes(app, deps) {
|
|
8
|
-
|
|
10
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
11
|
+
const { generateId, auth, vapidPublicKey } = deps;
|
|
9
12
|
app.get('/api/push/vapid-public-key', (_req, res) => {
|
|
10
13
|
if (!vapidPublicKey)
|
|
11
14
|
return void res.status(503).json({ error: '推送未配置,请联系管理员设置 VAPID_PUBLIC_KEY' });
|
|
12
15
|
res.json({ key: vapidPublicKey });
|
|
13
16
|
});
|
|
14
|
-
app.post('/api/push/subscribe', (req, res) => {
|
|
17
|
+
app.post('/api/push/subscribe', async (req, res) => {
|
|
15
18
|
const user = auth(req, res);
|
|
16
19
|
if (!user)
|
|
17
20
|
return;
|
|
@@ -21,34 +24,32 @@ export function registerPushRoutes(app, deps) {
|
|
|
21
24
|
}
|
|
22
25
|
const id = generateId('psub');
|
|
23
26
|
// 同 user + endpoint 视作重新订阅
|
|
24
|
-
const existing =
|
|
27
|
+
const existing = await dbOne('SELECT id FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [user.id, String(endpoint)]);
|
|
25
28
|
if (existing) {
|
|
26
|
-
|
|
27
|
-
.run(String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null, existing.id);
|
|
29
|
+
await dbRun('UPDATE push_subscriptions SET p256dh = ?, auth = ?, user_agent = ?, enabled = 1 WHERE id = ?', [String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null, existing.id]);
|
|
28
30
|
return void res.json({ success: true, id: existing.id });
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
.run(id, user.id, String(endpoint), String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null);
|
|
32
|
+
await dbRun(`INSERT INTO push_subscriptions (id, user_id, endpoint, p256dh, auth, user_agent) VALUES (?,?,?,?,?,?)`, [id, user.id, String(endpoint), String(keys.p256dh), String(keys.auth), user_agent ? String(user_agent).slice(0, 200) : null]);
|
|
32
33
|
res.json({ success: true, id });
|
|
33
34
|
});
|
|
34
|
-
app.delete('/api/push/subscribe', (req, res) => {
|
|
35
|
+
app.delete('/api/push/subscribe', async (req, res) => {
|
|
35
36
|
const user = auth(req, res);
|
|
36
37
|
if (!user)
|
|
37
38
|
return;
|
|
38
39
|
const { endpoint } = req.body || {};
|
|
39
40
|
if (endpoint) {
|
|
40
|
-
|
|
41
|
+
await dbRun('DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?', [user.id, String(endpoint)]);
|
|
41
42
|
}
|
|
42
43
|
else {
|
|
43
|
-
|
|
44
|
+
await dbRun('DELETE FROM push_subscriptions WHERE user_id = ?', [user.id]);
|
|
44
45
|
}
|
|
45
46
|
res.json({ success: true });
|
|
46
47
|
});
|
|
47
|
-
app.get('/api/push/status', (req, res) => {
|
|
48
|
+
app.get('/api/push/status', async (req, res) => {
|
|
48
49
|
const user = auth(req, res);
|
|
49
50
|
if (!user)
|
|
50
51
|
return;
|
|
51
|
-
const cnt =
|
|
52
|
+
const cnt = (await dbOne('SELECT COUNT(*) as n FROM push_subscriptions WHERE user_id = ? AND enabled = 1', [user.id])).n;
|
|
52
53
|
res.json({ subscribed: cnt > 0, count: cnt, vapid_configured: !!vapidPublicKey });
|
|
53
54
|
});
|
|
54
55
|
}
|