@seasonkoh/webaz 0.1.23 → 0.1.25
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 +2 -0
- 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 +198 -83
- 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 +173 -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 +10 -2
- 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-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-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/contract-fingerprint.js +2 -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 +575 -68
- package/dist/pwa/public/i18n.js +29 -20
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +2 -2
- 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-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 +26 -29
- package/dist/pwa/routes/admin-ops.js +22 -21
- 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 +54 -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 +7 -5
- 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 +153 -114
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +11 -9
- package/dist/pwa/routes/auth-register.js +35 -20
- 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 +147 -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 +33 -30
- 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 +33 -17
- 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 +14 -16
- 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 +19 -17
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +55 -49
- 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 +30 -30
- package/dist/pwa/routes/recover-key.js +13 -12
- package/dist/pwa/routes/referral.js +37 -32
- 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 +59 -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 +57 -0
- package/dist/pwa/routes/shops.js +20 -18
- 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 +45 -0
- package/dist/pwa/routes/trial.js +69 -51
- 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 -60
- 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 +74 -36
- 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 +237 -81
- package/dist/version.js +1 -1
- package/package.json +47 -2
package/dist/pwa/routes/chat.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const VALID_CHAT_KINDS = new Set(['order', 'rfq', 'listing_qa']);
|
|
2
3
|
// 反诈正则(命中即 flag,仍发出)
|
|
3
4
|
const FRAUD_PATTERNS = [
|
|
@@ -21,9 +22,9 @@ export function detectFraud(text) {
|
|
|
21
22
|
export function registerChatRoutes(app, deps) {
|
|
22
23
|
const { db, auth, generateId, rateLimitOk } = deps;
|
|
23
24
|
// 上下文 + 对方校验:只允许有真实商业关系的两方建会话
|
|
24
|
-
function resolveChatParticipants(kind, contextId, requesterId, recipientId) {
|
|
25
|
+
async function resolveChatParticipants(kind, contextId, requesterId, recipientId) {
|
|
25
26
|
if (kind === 'order') {
|
|
26
|
-
const o =
|
|
27
|
+
const o = await dbOne('SELECT buyer_id, seller_id FROM orders WHERE id = ?', [contextId]);
|
|
27
28
|
if (!o)
|
|
28
29
|
return { user_a: '', user_b: '', allowed: false, reason: '订单不存在' };
|
|
29
30
|
if (requesterId !== o.buyer_id && requesterId !== o.seller_id)
|
|
@@ -31,26 +32,26 @@ export function registerChatRoutes(app, deps) {
|
|
|
31
32
|
return { user_a: o.buyer_id, user_b: o.seller_id, allowed: true };
|
|
32
33
|
}
|
|
33
34
|
if (kind === 'rfq') {
|
|
34
|
-
const r =
|
|
35
|
+
const r = await dbOne('SELECT buyer_id, status FROM rfqs WHERE id = ?', [contextId]);
|
|
35
36
|
if (!r)
|
|
36
37
|
return { user_a: '', user_b: '', allowed: false, reason: 'RFQ 不存在' };
|
|
37
38
|
if (requesterId === r.buyer_id) {
|
|
38
39
|
// buyer 主动找某个 bidder
|
|
39
40
|
if (!recipientId)
|
|
40
41
|
return { user_a: '', user_b: '', allowed: false, reason: '需指定 recipient' };
|
|
41
|
-
const hasBid =
|
|
42
|
+
const hasBid = await dbOne('SELECT 1 FROM bids WHERE rfq_id = ? AND seller_id = ?', [contextId, recipientId]);
|
|
42
43
|
if (!hasBid)
|
|
43
44
|
return { user_a: '', user_b: '', allowed: false, reason: '对方未对此 RFQ 报价' };
|
|
44
45
|
return { user_a: r.buyer_id, user_b: recipientId, allowed: true };
|
|
45
46
|
}
|
|
46
47
|
// seller 找 buyer:必须自己已 bid
|
|
47
|
-
const myBid =
|
|
48
|
+
const myBid = await dbOne('SELECT 1 FROM bids WHERE rfq_id = ? AND seller_id = ?', [contextId, requesterId]);
|
|
48
49
|
if (!myBid)
|
|
49
50
|
return { user_a: '', user_b: '', allowed: false, reason: '需先报价才能联系买家' };
|
|
50
51
|
return { user_a: r.buyer_id, user_b: requesterId, allowed: true };
|
|
51
52
|
}
|
|
52
53
|
if (kind === 'listing_qa') {
|
|
53
|
-
const l =
|
|
54
|
+
const l = await dbOne('SELECT created_by FROM listings WHERE id = ?', [contextId]);
|
|
54
55
|
if (!l)
|
|
55
56
|
return { user_a: '', user_b: '', allowed: false, reason: 'listing 不存在' };
|
|
56
57
|
// 仅非创建者可主动发起(避免 listing 创建者主动 spam)
|
|
@@ -59,7 +60,7 @@ export function registerChatRoutes(app, deps) {
|
|
|
59
60
|
if (!recipientId)
|
|
60
61
|
return { user_a: '', user_b: '', allowed: false, reason: '请等买家先发起咨询' };
|
|
61
62
|
const [a, b] = l.created_by < recipientId ? [l.created_by, recipientId] : [recipientId, l.created_by];
|
|
62
|
-
const exists =
|
|
63
|
+
const exists = await dbOne("SELECT 1 FROM conversations WHERE kind = 'listing_qa' AND context_id = ? AND user_a = ? AND user_b = ?", [contextId, a, b]);
|
|
63
64
|
if (!exists)
|
|
64
65
|
return { user_a: '', user_b: '', allowed: false, reason: '请等买家先发起咨询' };
|
|
65
66
|
return { user_a: l.created_by, user_b: recipientId, allowed: true };
|
|
@@ -68,22 +69,21 @@ export function registerChatRoutes(app, deps) {
|
|
|
68
69
|
}
|
|
69
70
|
return { user_a: '', user_b: '', allowed: false, reason: 'kind 无效' };
|
|
70
71
|
}
|
|
71
|
-
function findOrCreateConv(kind, contextId, userA, userB) {
|
|
72
|
+
async function findOrCreateConv(kind, contextId, userA, userB) {
|
|
72
73
|
// 规范化:user_a 字典序较小
|
|
73
74
|
const [a, b] = userA < userB ? [userA, userB] : [userB, userA];
|
|
74
|
-
const
|
|
75
|
-
const existing =
|
|
75
|
+
const SELECT_SQL = 'SELECT id FROM conversations WHERE kind = ? AND context_id = ? AND user_a = ? AND user_b = ?';
|
|
76
|
+
const existing = await dbOne(SELECT_SQL, [kind, contextId, a, b]);
|
|
76
77
|
if (existing)
|
|
77
78
|
return existing.id;
|
|
78
79
|
// 并发场景下 UNIQUE 可能触发;用 INSERT OR IGNORE + 再次 SELECT 兜底
|
|
79
80
|
const id = generateId('cv');
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const final = selectStmt.get(kind, contextId, a, b);
|
|
81
|
+
await dbRun(`INSERT OR IGNORE INTO conversations (id, kind, context_id, user_a, user_b) VALUES (?,?,?,?,?)`, [id, kind, contextId, a, b]);
|
|
82
|
+
const final = await dbOne(SELECT_SQL, [kind, contextId, a, b]);
|
|
83
83
|
return final.id;
|
|
84
84
|
}
|
|
85
85
|
// 开会话(idempotent — 已存在则返回 id)
|
|
86
|
-
app.post('/api/conversations/start', (req, res) => {
|
|
86
|
+
app.post('/api/conversations/start', async (req, res) => {
|
|
87
87
|
const user = auth(req, res);
|
|
88
88
|
if (!user)
|
|
89
89
|
return;
|
|
@@ -92,18 +92,18 @@ export function registerChatRoutes(app, deps) {
|
|
|
92
92
|
return void res.json({ error: 'kind 无效' });
|
|
93
93
|
if (!context_id)
|
|
94
94
|
return void res.json({ error: 'context_id 必填' });
|
|
95
|
-
const r = resolveChatParticipants(String(kind), String(context_id), user.id, recipient_id ? String(recipient_id) : null);
|
|
95
|
+
const r = await resolveChatParticipants(String(kind), String(context_id), user.id, recipient_id ? String(recipient_id) : null);
|
|
96
96
|
if (!r.allowed)
|
|
97
97
|
return void res.json({ error: r.reason || '无权开启会话' });
|
|
98
|
-
const id = findOrCreateConv(String(kind), String(context_id), r.user_a, r.user_b);
|
|
98
|
+
const id = await findOrCreateConv(String(kind), String(context_id), r.user_a, r.user_b);
|
|
99
99
|
res.json({ id, kind, context_id });
|
|
100
100
|
});
|
|
101
101
|
// 我的会话列表
|
|
102
|
-
app.get('/api/conversations', (req, res) => {
|
|
102
|
+
app.get('/api/conversations', async (req, res) => {
|
|
103
103
|
const user = auth(req, res);
|
|
104
104
|
if (!user)
|
|
105
105
|
return;
|
|
106
|
-
const rows =
|
|
106
|
+
const rows = await dbAll(`
|
|
107
107
|
SELECT c.*,
|
|
108
108
|
CASE WHEN c.user_a = ? THEN c.unread_a ELSE c.unread_b END as my_unread,
|
|
109
109
|
CASE WHEN c.user_a = ? THEN c.user_b ELSE c.user_a END as other_id,
|
|
@@ -113,15 +113,15 @@ export function registerChatRoutes(app, deps) {
|
|
|
113
113
|
WHERE (c.user_a = ? OR c.user_b = ?) AND c.status NOT IN ('blocked','archived')
|
|
114
114
|
ORDER BY COALESCE(c.last_message_at, c.created_at) DESC
|
|
115
115
|
LIMIT 100
|
|
116
|
-
|
|
116
|
+
`, [user.id, user.id, user.id, user.id, user.id, user.id]);
|
|
117
117
|
res.json({ items: rows });
|
|
118
118
|
});
|
|
119
119
|
// 会话详情 + 消息分页
|
|
120
|
-
app.get('/api/conversations/:id', (req, res) => {
|
|
120
|
+
app.get('/api/conversations/:id', async (req, res) => {
|
|
121
121
|
const user = auth(req, res);
|
|
122
122
|
if (!user)
|
|
123
123
|
return;
|
|
124
|
-
const conv =
|
|
124
|
+
const conv = await dbOne('SELECT * FROM conversations WHERE id = ?', [req.params.id]);
|
|
125
125
|
if (!conv)
|
|
126
126
|
return void res.status(404).json({ error: '会话不存在' });
|
|
127
127
|
if (conv.user_a !== user.id && conv.user_b !== user.id)
|
|
@@ -135,13 +135,13 @@ export function registerChatRoutes(app, deps) {
|
|
|
135
135
|
args.push(before);
|
|
136
136
|
}
|
|
137
137
|
args.push(limit);
|
|
138
|
-
const messages =
|
|
138
|
+
const messages = await dbAll(`
|
|
139
139
|
SELECT id, sender_id, body, attachments, flagged, flag_reasons, read_at, kind, meta, created_at
|
|
140
140
|
FROM messages
|
|
141
141
|
WHERE conversation_id = ?${whereExtra}
|
|
142
142
|
ORDER BY created_at DESC
|
|
143
143
|
LIMIT ?
|
|
144
|
-
|
|
144
|
+
`, args);
|
|
145
145
|
messages.reverse(); // 返回时间正序,便于前端 append
|
|
146
146
|
// QA 轮 11 P1:read 端 flag_reasons 是 JSON string,send 端是 array → agent 双向 parse 易错
|
|
147
147
|
// 统一为 array (send 端格式)
|
|
@@ -170,18 +170,18 @@ export function registerChatRoutes(app, deps) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
const otherId = conv.user_a === user.id ? conv.user_b : conv.user_a;
|
|
173
|
-
const other =
|
|
173
|
+
const other = await dbOne('SELECT id, handle, name, region FROM users WHERE id = ?', [otherId]);
|
|
174
174
|
res.json({ conv, messages, other });
|
|
175
175
|
});
|
|
176
176
|
// 发消息
|
|
177
|
-
app.post('/api/conversations/:id/messages', (req, res) => {
|
|
177
|
+
app.post('/api/conversations/:id/messages', async (req, res) => {
|
|
178
178
|
const user = auth(req, res);
|
|
179
179
|
if (!user)
|
|
180
180
|
return;
|
|
181
181
|
// P1: 频率限制 — 同用户每分钟 ≤ 60 条(≈ 1/s 持续 + 短时突发)
|
|
182
182
|
if (!rateLimitOk(`chat_msg:${user.id}`, 60, 60_000))
|
|
183
183
|
return void res.status(429).json({ error: '发送过于频繁,请稍等' });
|
|
184
|
-
const conv =
|
|
184
|
+
const conv = await dbOne('SELECT * FROM conversations WHERE id = ?', [req.params.id]);
|
|
185
185
|
if (!conv)
|
|
186
186
|
return void res.status(404).json({ error: '会话不存在' });
|
|
187
187
|
if (conv.status === 'blocked')
|
|
@@ -238,9 +238,8 @@ export function registerChatRoutes(app, deps) {
|
|
|
238
238
|
// 通知接收方
|
|
239
239
|
try {
|
|
240
240
|
const recipient = isFromA ? conv.user_b : conv.user_a;
|
|
241
|
-
|
|
242
|
-
VALUES (?,?,'chat_new',?,?,datetime('now'))
|
|
243
|
-
.run(generateId('ntf'), recipient, `💬 新消息`, preview);
|
|
241
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, created_at)
|
|
242
|
+
VALUES (?,?,'chat_new',?,?,datetime('now'))`, [generateId('ntf'), recipient, `💬 新消息`, preview]);
|
|
244
243
|
}
|
|
245
244
|
catch (e) {
|
|
246
245
|
console.error('[chat notify]', e);
|
|
@@ -248,11 +247,11 @@ export function registerChatRoutes(app, deps) {
|
|
|
248
247
|
res.json({ id, flagged: reasons.length > 0, flag_reasons: reasons });
|
|
249
248
|
});
|
|
250
249
|
// 标记已读
|
|
251
|
-
app.post('/api/conversations/:id/read', (req, res) => {
|
|
250
|
+
app.post('/api/conversations/:id/read', async (req, res) => {
|
|
252
251
|
const user = auth(req, res);
|
|
253
252
|
if (!user)
|
|
254
253
|
return;
|
|
255
|
-
const conv =
|
|
254
|
+
const conv = await dbOne('SELECT user_a, user_b FROM conversations WHERE id = ?', [req.params.id]);
|
|
256
255
|
if (!conv)
|
|
257
256
|
return void res.status(404).json({ error: '会话不存在' });
|
|
258
257
|
if (conv.user_a !== user.id && conv.user_b !== user.id)
|
|
@@ -266,37 +265,37 @@ export function registerChatRoutes(app, deps) {
|
|
|
266
265
|
res.json({ success: true });
|
|
267
266
|
});
|
|
268
267
|
// 归档(仅自己侧)
|
|
269
|
-
app.post('/api/conversations/:id/archive', (req, res) => {
|
|
268
|
+
app.post('/api/conversations/:id/archive', async (req, res) => {
|
|
270
269
|
const user = auth(req, res);
|
|
271
270
|
if (!user)
|
|
272
271
|
return;
|
|
273
|
-
const conv =
|
|
272
|
+
const conv = await dbOne('SELECT user_a, user_b, status FROM conversations WHERE id = ?', [req.params.id]);
|
|
274
273
|
if (!conv)
|
|
275
274
|
return void res.status(404).json({ error: '会话不存在' });
|
|
276
275
|
if (conv.user_a !== user.id && conv.user_b !== user.id)
|
|
277
276
|
return void res.status(403).json({ error: '无权操作' });
|
|
278
|
-
|
|
277
|
+
await dbRun("UPDATE conversations SET status = 'archived' WHERE id = ?", [req.params.id]);
|
|
279
278
|
res.json({ success: true });
|
|
280
279
|
});
|
|
281
280
|
// 拉黑(双向屏蔽)
|
|
282
|
-
app.post('/api/conversations/:id/block', (req, res) => {
|
|
281
|
+
app.post('/api/conversations/:id/block', async (req, res) => {
|
|
283
282
|
const user = auth(req, res);
|
|
284
283
|
if (!user)
|
|
285
284
|
return;
|
|
286
|
-
const conv =
|
|
285
|
+
const conv = await dbOne('SELECT user_a, user_b FROM conversations WHERE id = ?', [req.params.id]);
|
|
287
286
|
if (!conv)
|
|
288
287
|
return void res.status(404).json({ error: '会话不存在' });
|
|
289
288
|
if (conv.user_a !== user.id && conv.user_b !== user.id)
|
|
290
289
|
return void res.status(403).json({ error: '无权操作' });
|
|
291
|
-
|
|
290
|
+
await dbRun("UPDATE conversations SET status = 'blocked' WHERE id = ?", [req.params.id]);
|
|
292
291
|
res.json({ success: true });
|
|
293
292
|
});
|
|
294
293
|
// 举报(人工审核)
|
|
295
|
-
app.post('/api/conversations/:id/report', (req, res) => {
|
|
294
|
+
app.post('/api/conversations/:id/report', async (req, res) => {
|
|
296
295
|
const user = auth(req, res);
|
|
297
296
|
if (!user)
|
|
298
297
|
return;
|
|
299
|
-
const conv =
|
|
298
|
+
const conv = await dbOne('SELECT user_a, user_b FROM conversations WHERE id = ?', [req.params.id]);
|
|
300
299
|
if (!conv)
|
|
301
300
|
return void res.status(404).json({ error: '会话不存在' });
|
|
302
301
|
if (conv.user_a !== user.id && conv.user_b !== user.id)
|
|
@@ -306,13 +305,13 @@ export function registerChatRoutes(app, deps) {
|
|
|
306
305
|
if (!reason)
|
|
307
306
|
return void res.json({ error: '原因必填' });
|
|
308
307
|
// P1: 同 (reporter, conversation) 24h 内最多 3 次
|
|
309
|
-
const recentRpt =
|
|
308
|
+
const recentRpt = (await dbOne(`SELECT COUNT(1) as n FROM chat_reports WHERE conversation_id = ? AND reporter_id = ? AND created_at > datetime('now','-1 day')`, [req.params.id, user.id])).n;
|
|
310
309
|
if (recentRpt >= 3)
|
|
311
310
|
return void res.status(429).json({ error: '24 小时内对同一会话最多举报 3 次' });
|
|
312
311
|
const reportedId = conv.user_a === user.id ? conv.user_b : conv.user_a;
|
|
313
|
-
|
|
314
|
-
VALUES (?,?,?,?,?,?,?)
|
|
315
|
-
|
|
312
|
+
await dbRun(`INSERT INTO chat_reports (id, conversation_id, message_id, reporter_id, reported_id, reason, note)
|
|
313
|
+
VALUES (?,?,?,?,?,?,?)`, [generateId('rpt'), req.params.id, body.message_id ? String(body.message_id) : null,
|
|
314
|
+
user.id, reportedId, reason, body.note ? String(body.note).slice(0, 500) : null]);
|
|
316
315
|
res.json({ success: true });
|
|
317
316
|
});
|
|
318
317
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerCheckinTasksRoutes(app, deps) {
|
|
2
3
|
const { db, auth, isTrustedRole, errorRes, generateId, getProtocolParam, resolveCheckinDate, TASK_DEFS, computeTaskProgress, disbursePlatformReward, broadcastSystemEvent } = deps;
|
|
3
|
-
app.get('/api/checkin/status', (req, res) => {
|
|
4
|
+
app.get('/api/checkin/status', async (req, res) => {
|
|
4
5
|
const user = auth(req, res);
|
|
5
6
|
if (!user)
|
|
6
7
|
return;
|
|
7
8
|
if (isTrustedRole(user))
|
|
8
9
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无此功能');
|
|
9
10
|
const today = resolveCheckinDate(req.query.local_date ? String(req.query.local_date) : undefined);
|
|
10
|
-
const todayCheckin =
|
|
11
|
+
const todayCheckin = await dbOne('SELECT reward, streak FROM daily_checkins WHERE user_id = ? AND checkin_date = ?', [user.id, today]);
|
|
11
12
|
// streak: 连续签到 — 检查昨日
|
|
12
13
|
const yesterday = new Date(new Date(today + 'T00:00:00Z').getTime() - 86400000).toISOString().slice(0, 10);
|
|
13
|
-
const yesterdayCheckin =
|
|
14
|
+
const yesterdayCheckin = await dbOne('SELECT streak FROM daily_checkins WHERE user_id = ? AND checkin_date = ?', [user.id, yesterday]);
|
|
14
15
|
const currentStreak = todayCheckin?.streak || (yesterdayCheckin ? yesterdayCheckin.streak + 1 : 1);
|
|
15
16
|
// F-2: 里程碑参数 admin 可调
|
|
16
17
|
const bonus7 = getProtocolParam('streak_bonus_7', 5);
|
|
@@ -22,7 +23,7 @@ export function registerCheckinTasksRoutes(app, deps) {
|
|
|
22
23
|
// 任务列表
|
|
23
24
|
const progress = computeTaskProgress(String(user.id));
|
|
24
25
|
const claimed = new Map();
|
|
25
|
-
for (const row of
|
|
26
|
+
for (const row of await dbAll('SELECT task_key, claimed_at FROM task_completions WHERE user_id = ?', [user.id])) {
|
|
26
27
|
if (row.claimed_at)
|
|
27
28
|
claimed.set(row.task_key, row.claimed_at);
|
|
28
29
|
}
|
|
@@ -43,18 +44,18 @@ export function registerCheckinTasksRoutes(app, deps) {
|
|
|
43
44
|
tasks,
|
|
44
45
|
});
|
|
45
46
|
});
|
|
46
|
-
app.post('/api/checkin', (req, res) => {
|
|
47
|
+
app.post('/api/checkin', async (req, res) => {
|
|
47
48
|
const user = auth(req, res);
|
|
48
49
|
if (!user)
|
|
49
50
|
return;
|
|
50
51
|
if (isTrustedRole(user))
|
|
51
52
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_TRADE', '受信角色无此功能');
|
|
52
53
|
const today = resolveCheckinDate(req.body?.local_date ? String(req.body.local_date) : undefined);
|
|
53
|
-
const existing =
|
|
54
|
+
const existing = await dbOne('SELECT reward FROM daily_checkins WHERE user_id = ? AND checkin_date = ?', [user.id, today]);
|
|
54
55
|
if (existing)
|
|
55
56
|
return void res.status(400).json({ error: '今日已签到', error_code: 'ALREADY_CHECKED_IN' });
|
|
56
57
|
const yesterday = new Date(new Date(today + 'T00:00:00Z').getTime() - 86400000).toISOString().slice(0, 10);
|
|
57
|
-
const yesterdayCheckin =
|
|
58
|
+
const yesterdayCheckin = await dbOne('SELECT streak FROM daily_checkins WHERE user_id = ? AND checkin_date = ?', [user.id, yesterday]);
|
|
58
59
|
const streak = yesterdayCheckin ? yesterdayCheckin.streak + 1 : 1;
|
|
59
60
|
// F-2: admin 可调里程碑
|
|
60
61
|
const bonus7 = getProtocolParam('streak_bonus_7', 5);
|
|
@@ -84,7 +85,7 @@ export function registerCheckinTasksRoutes(app, deps) {
|
|
|
84
85
|
catch { }
|
|
85
86
|
res.json({ success: true, reward, streak, milestone_bonus: milestoneBonus });
|
|
86
87
|
});
|
|
87
|
-
app.post('/api/tasks/:key/claim', (req, res) => {
|
|
88
|
+
app.post('/api/tasks/:key/claim', async (req, res) => {
|
|
88
89
|
const user = auth(req, res);
|
|
89
90
|
if (!user)
|
|
90
91
|
return;
|
|
@@ -97,7 +98,7 @@ export function registerCheckinTasksRoutes(app, deps) {
|
|
|
97
98
|
const progress = computeTaskProgress(String(user.id));
|
|
98
99
|
if (!progress[taskKey].eligible)
|
|
99
100
|
return void res.status(400).json({ error: '任务未完成', progress: progress[taskKey] });
|
|
100
|
-
const existing =
|
|
101
|
+
const existing = await dbOne('SELECT claimed_at FROM task_completions WHERE user_id = ? AND task_key = ?', [user.id, taskKey]);
|
|
101
102
|
if (existing?.claimed_at)
|
|
102
103
|
return void res.status(400).json({ error: '任务奖励已领取' });
|
|
103
104
|
db.transaction(() => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { buildIntentMandate, signMandate } from './ap2-mandate.js';
|
|
2
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
export function registerCheckoutHelpersRoutes(app, deps) {
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbRun),不再直接用 deps.db
|
|
5
|
+
const { auth, generateId, formatProductForAgent, signPassport, issuerAddress } = deps;
|
|
6
|
+
app.get('/api/checkout/tax-preview', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
6
8
|
if (!user)
|
|
7
9
|
return;
|
|
@@ -9,9 +11,9 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
9
11
|
const qtyN = Math.max(1, Math.floor(Number(req.query.quantity) || 1));
|
|
10
12
|
if (!productId)
|
|
11
13
|
return void res.status(400).json({ error: 'product_id required' });
|
|
12
|
-
const product =
|
|
14
|
+
const product = await dbOne(`SELECT p.id, p.price, p.title, u.region as seller_region
|
|
13
15
|
FROM products p JOIN users u ON u.id = p.seller_id
|
|
14
|
-
WHERE p.id =
|
|
16
|
+
WHERE p.id = ?`, [productId]);
|
|
15
17
|
if (!product)
|
|
16
18
|
return void res.status(404).json({ error: '商品不存在' });
|
|
17
19
|
const buyerRegion = user.region || 'global';
|
|
@@ -24,8 +26,8 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
24
26
|
disclaimer: '同地区订单,无跨境关税',
|
|
25
27
|
});
|
|
26
28
|
}
|
|
27
|
-
const cfg =
|
|
28
|
-
FROM region_config WHERE region =
|
|
29
|
+
const cfg = await dbOne(`SELECT est_import_duty_pct, est_import_threshold_waz
|
|
30
|
+
FROM region_config WHERE region = ?`, [buyerRegion]);
|
|
29
31
|
const pct = Number(cfg?.est_import_duty_pct || 0);
|
|
30
32
|
const threshold = Number(cfg?.est_import_threshold_waz || 0);
|
|
31
33
|
const orderTotal = Number(product.price) * qtyN;
|
|
@@ -51,14 +53,14 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
51
53
|
const { product_id, quantity = 1 } = req.body;
|
|
52
54
|
if (!product_id)
|
|
53
55
|
return void res.json({ error: '请提供 product_id' });
|
|
54
|
-
const product =
|
|
56
|
+
const product = await dbOne(`
|
|
55
57
|
SELECT p.*, u.name as seller_name,
|
|
56
58
|
COALESCE(rs.level, 'new') as rep_level
|
|
57
59
|
FROM products p
|
|
58
60
|
JOIN users u ON p.seller_id = u.id
|
|
59
61
|
LEFT JOIN reputation_scores rs ON rs.user_id = p.seller_id
|
|
60
62
|
WHERE p.id = ? AND p.status = 'active'
|
|
61
|
-
|
|
63
|
+
`, [product_id]);
|
|
62
64
|
if (!product)
|
|
63
65
|
return void res.json({ error: '商品不存在或已下架' });
|
|
64
66
|
const qty = Number(quantity);
|
|
@@ -68,10 +70,10 @@ export function registerCheckoutHelpersRoutes(app, deps) {
|
|
|
68
70
|
const now = new Date();
|
|
69
71
|
const expiresAt = new Date(now.getTime() + 10 * 60_000);
|
|
70
72
|
const token = generateId('pst');
|
|
71
|
-
|
|
73
|
+
await dbRun(`
|
|
72
74
|
INSERT INTO price_sessions (token, product_id, user_id, price, quantity, created_at, expires_at)
|
|
73
75
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
74
|
-
|
|
76
|
+
`, [token, product_id, user.id, product.price, qty, now.toISOString(), expiresAt.toISOString()]);
|
|
75
77
|
// AP2 (B.4 b) — Intent Mandate 并存输出;不破坏现有 session_token
|
|
76
78
|
let ap2_intent_mandate = null;
|
|
77
79
|
try {
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerClaimInitiatorsRoutes(app, deps) {
|
|
3
|
+
// 只读/单写站点走 RFC-016 异步 seam;db 保留:claim 是质押/escrow 资金路径,
|
|
4
|
+
// dup 门 + 钱包扣减 + INSERT 任务必须原子(db.transaction),Phase 3 迁 pg 行锁。
|
|
2
5
|
const { db, auth, isTrustedRole, errorRes, generateId } = deps;
|
|
3
6
|
const wire = (cfg) => {
|
|
4
7
|
const { entityPath, entityTable, entityIdCol, entityPartyCol, taskTable, taskPartyCol, voteTable, targets, stake, deadlineHours, idPrefix, allowedStatuses, notFoundMsg, ownClaimMsg, statusErrMsg, dupErrMsg, taskAlias: a } = cfg;
|
|
5
|
-
app.post(`/api/${entityPath}/:id/claim`, (req, res) => {
|
|
8
|
+
app.post(`/api/${entityPath}/:id/claim`, async (req, res) => {
|
|
6
9
|
const user = auth(req, res);
|
|
7
10
|
if (!user)
|
|
8
11
|
return;
|
|
9
12
|
if (isTrustedRole(user)) {
|
|
10
13
|
return void errorRes(res, 403, 'TRUSTED_ROLE_NO_CLAIM', '受信角色不可发起声明');
|
|
11
14
|
}
|
|
12
|
-
const entity =
|
|
13
|
-
.get(req.params.id);
|
|
15
|
+
const entity = await dbOne(`SELECT id, ${entityPartyCol}, status FROM ${entityTable} WHERE id = ?`, [req.params.id]);
|
|
14
16
|
if (!entity)
|
|
15
17
|
return void res.status(404).json({ error: notFoundMsg });
|
|
16
18
|
const partyId = entity[entityPartyCol];
|
|
@@ -25,22 +27,40 @@ export function registerClaimInitiatorsRoutes(app, deps) {
|
|
|
25
27
|
if (text.length < 6 || text.length > 500)
|
|
26
28
|
return void res.status(400).json({ error: 'claim_text 长度需 6-500 字' });
|
|
27
29
|
const evidence = req.body?.evidence_uri ? String(req.body.evidence_uri).trim().slice(0, 500) : null;
|
|
28
|
-
|
|
30
|
+
// 友好预检查(读):余额不足直接早退;真正的守恒门在事务内(WHERE balance >= stake)。
|
|
31
|
+
const wallet = await dbOne('SELECT balance FROM wallets WHERE user_id = ?', [user.id]);
|
|
29
32
|
if (!wallet || wallet.balance < stake)
|
|
30
33
|
return void res.status(400).json({ error: `余额不足:发起需锁 ${stake} WAZ` });
|
|
31
|
-
const dup = db.prepare(`SELECT id FROM ${taskTable} WHERE ${entityIdCol} = ? AND claimant_id = ? AND claim_target = ? AND status = 'open'`)
|
|
32
|
-
.get(req.params.id, user.id, target);
|
|
33
|
-
if (dup)
|
|
34
|
-
return void res.status(409).json({ error: dupErrMsg });
|
|
35
34
|
const id = generateId(idPrefix);
|
|
36
35
|
const deadline = new Date(Date.now() + deadlineHours * 3600_000).toISOString();
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
36
|
+
// 质押/escrow 原子段(同步事务):dup 门 + 钱包扣减(守恒 guard)+ INSERT 任务,
|
|
37
|
+
// 任一失败整段回滚 → 不会出现"任务已建但钱没锁"或"双重 open 声明"或透支。
|
|
38
|
+
try {
|
|
39
|
+
db.transaction(() => {
|
|
40
|
+
const dup = db.prepare(`SELECT id FROM ${taskTable} WHERE ${entityIdCol} = ? AND claimant_id = ? AND claim_target = ? AND status = 'open'`)
|
|
41
|
+
.get(req.params.id, user.id, target);
|
|
42
|
+
if (dup)
|
|
43
|
+
throw new Error('CLAIM_DUP');
|
|
44
|
+
const debit = db.prepare('UPDATE wallets SET balance = balance - ?, escrowed = escrowed + ? WHERE user_id = ? AND balance >= ?')
|
|
45
|
+
.run(stake, stake, user.id, stake);
|
|
46
|
+
if (debit.changes === 0)
|
|
47
|
+
throw new Error('CLAIM_INSUFFICIENT');
|
|
48
|
+
db.prepare(`INSERT INTO ${taskTable} (id, ${entityIdCol}, ${taskPartyCol}, claimant_id, claim_target, claim_text, evidence_uri, stake_claimant, deadline_at, status) VALUES (?,?,?,?,?,?,?,?,?,'open')`)
|
|
49
|
+
.run(id, req.params.id, partyId, user.id, target, text, evidence, stake, deadline);
|
|
50
|
+
})();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
const msg = e.message;
|
|
54
|
+
if (msg === 'CLAIM_DUP')
|
|
55
|
+
return void res.status(409).json({ error: dupErrMsg });
|
|
56
|
+
if (msg === 'CLAIM_INSUFFICIENT')
|
|
57
|
+
return void res.status(400).json({ error: `余额不足:发起需锁 ${stake} WAZ` });
|
|
58
|
+
console.error('[claim-initiators tx]', msg);
|
|
59
|
+
return void res.status(500).json({ error: '发起声明失败,请重试' });
|
|
60
|
+
}
|
|
41
61
|
res.json({ success: true, claim_id: id, deadline_at: deadline, stake_locked: stake });
|
|
42
62
|
});
|
|
43
|
-
app.get(`/api/${entityPath}/:id/claims`, (req, res) => {
|
|
63
|
+
app.get(`/api/${entityPath}/:id/claims`, async (req, res) => {
|
|
44
64
|
const sql = `
|
|
45
65
|
SELECT ${a}.id, ${a}.claim_target, ${a}.claim_text, ${a}.evidence_uri, ${a}.status, ${a}.ruling, ${a}.deadline_at, ${a}.resolved_at, ${a}.created_at,
|
|
46
66
|
u.name as claimant_name,
|
|
@@ -48,7 +68,7 @@ export function registerClaimInitiatorsRoutes(app, deps) {
|
|
|
48
68
|
FROM ${taskTable} ${a} JOIN users u ON u.id = ${a}.claimant_id
|
|
49
69
|
WHERE ${a}.${entityIdCol} = ? ORDER BY ${a}.created_at DESC LIMIT 50
|
|
50
70
|
`;
|
|
51
|
-
const rows =
|
|
71
|
+
const rows = await dbAll(sql, [req.params.id]);
|
|
52
72
|
res.json({ claims: rows, votes_needed: 3 });
|
|
53
73
|
});
|
|
54
74
|
};
|