@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
|
@@ -4,6 +4,9 @@ import { transition, settleFault, settleDeclinedNoFault } from '../../layer0-fou
|
|
|
4
4
|
// RFC-014 PR5 — 争议后佣金/基金 clawback 走整数 base-units + 绝对值落库。
|
|
5
5
|
import { toUnits, toDecimal, mulRate } from '../../money.js';
|
|
6
6
|
import { applyWalletDelta } from '../../ledger.js';
|
|
7
|
+
// RFC-016 Phase 1 — 纯只读端点/校验读/SNF 分发读/标记写 → async seam;arbitrate 仲裁核心(原子领取 +
|
|
8
|
+
// 2 settlement db.transaction + reputation/strike/publish)与 tx 内 appendAuditLog 保持同步(Phase 3 迁 pg)。
|
|
9
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js';
|
|
7
10
|
export function registerDisputesWriteRoutes(app, deps) {
|
|
8
11
|
const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam } = deps;
|
|
9
12
|
// ── RFC-007 stage 5:客观拒单【临时判责】的仲裁翻案 ─────────────────────────────
|
|
@@ -11,23 +14,23 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
11
14
|
// 仲裁员(真实人工 + WebAuthn)裁决:uphold → declined_nofault(免责全退+退质押);reject → 违约结算。
|
|
12
15
|
const SYS = 'sys_protocol';
|
|
13
16
|
// 仲裁员待办:列出所有被举证的临时判责拒单
|
|
14
|
-
app.get('/api/admin/decline-contests', (req, res) => {
|
|
17
|
+
app.get('/api/admin/decline-contests', async (req, res) => {
|
|
15
18
|
const user = auth(req, res);
|
|
16
19
|
if (!user)
|
|
17
20
|
return;
|
|
18
21
|
const elig = isEligibleArbitrator(user.id);
|
|
19
22
|
if (!elig.ok)
|
|
20
23
|
return void errorRes(res, 403, 'NOT_ARBITRATOR', elig.reason || '仅限仲裁员');
|
|
21
|
-
const rows =
|
|
24
|
+
const rows = await dbAll(`
|
|
22
25
|
SELECT id AS order_id, buyer_id, seller_id, product_id, total_amount, decline_reason_code, declined_at, decline_contest_deadline
|
|
23
26
|
FROM orders
|
|
24
27
|
WHERE status = 'fault_seller' AND COALESCE(decline_objective_pending,0)=1 AND COALESCE(decline_contested,0)=1 AND settled_fault_at IS NULL
|
|
25
28
|
ORDER BY declined_at ASC
|
|
26
|
-
`)
|
|
29
|
+
`);
|
|
27
30
|
res.json({ contests: rows });
|
|
28
31
|
});
|
|
29
32
|
// 仲裁员裁决
|
|
30
|
-
app.post('/api/admin/decline-contests/:orderId/resolve', (req, res) => {
|
|
33
|
+
app.post('/api/admin/decline-contests/:orderId/resolve', async (req, res) => {
|
|
31
34
|
const user = auth(req, res);
|
|
32
35
|
if (!user)
|
|
33
36
|
return;
|
|
@@ -43,7 +46,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
43
46
|
return void errorRes(res, 400, 'BAD_DECISION', "decision 必须为 'uphold'(认定无责) 或 'reject'(驳回,判违约)");
|
|
44
47
|
if (!reason || !String(reason).trim())
|
|
45
48
|
return void errorRes(res, 400, 'REASON_REQUIRED', '请提供裁决理由');
|
|
46
|
-
const order =
|
|
49
|
+
const order = await dbOne('SELECT * FROM orders WHERE id = ?', [req.params.orderId]);
|
|
47
50
|
if (!order)
|
|
48
51
|
return void res.status(404).json({ error: '订单不存在' });
|
|
49
52
|
if (order.status !== 'fault_seller' || Number(order.decline_objective_pending) !== 1 || Number(order.decline_contested) !== 1 || order.settled_fault_at) {
|
|
@@ -73,12 +76,12 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
73
76
|
res.json({ success: true, outcome: 'fault_seller', note: '裁决:驳回。按违约结算,买家已全额退款,卖家质押按规则罚没。' });
|
|
74
77
|
});
|
|
75
78
|
// 被诉方反驳
|
|
76
|
-
app.post('/api/disputes/:id/respond', (req, res) => {
|
|
79
|
+
app.post('/api/disputes/:id/respond', async (req, res) => {
|
|
77
80
|
const user = auth(req, res);
|
|
78
81
|
if (!user)
|
|
79
82
|
return;
|
|
80
83
|
const { notes = '', evidence_description = '' } = req.body;
|
|
81
|
-
const dispute = getDisputeDetails(db, req.params.id);
|
|
84
|
+
const dispute = await getDisputeDetails(db, req.params.id);
|
|
82
85
|
if (!dispute)
|
|
83
86
|
return void res.status(404).json({ error: '争议不存在' });
|
|
84
87
|
if (dispute.defendant_id !== user.id)
|
|
@@ -97,7 +100,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
97
100
|
res.json({ success: true, message: result.message });
|
|
98
101
|
});
|
|
99
102
|
// 仲裁员裁定
|
|
100
|
-
app.post('/api/disputes/:id/arbitrate', (req, res) => {
|
|
103
|
+
app.post('/api/disputes/:id/arbitrate', async (req, res) => {
|
|
101
104
|
const user = auth(req, res);
|
|
102
105
|
if (!user)
|
|
103
106
|
return;
|
|
@@ -128,7 +131,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
|
-
const dispute = getDisputeDetails(db, req.params.id);
|
|
134
|
+
const dispute = await getDisputeDetails(db, req.params.id);
|
|
132
135
|
if (!dispute)
|
|
133
136
|
return void res.status(404).json({ error: '争议不存在' });
|
|
134
137
|
// P0: 防"任意仲裁员裁决任意争议"
|
|
@@ -345,7 +348,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
345
348
|
res.json({ success: true, message: result.message, settlement: result.settlement });
|
|
346
349
|
});
|
|
347
350
|
// 参与方主动举证(text)+ SNF 信封分发
|
|
348
|
-
app.post('/api/disputes/:id/add-evidence', (req, res) => {
|
|
351
|
+
app.post('/api/disputes/:id/add-evidence', async (req, res) => {
|
|
349
352
|
const user = auth(req, res);
|
|
350
353
|
if (!user)
|
|
351
354
|
return;
|
|
@@ -360,14 +363,13 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
360
363
|
const evReasons = detectFraud(rawDesc);
|
|
361
364
|
if (evReasons.length > 0 && result.evidenceId) {
|
|
362
365
|
try {
|
|
363
|
-
|
|
364
|
-
.run(JSON.stringify(evReasons), result.evidenceId);
|
|
366
|
+
await dbRun(`UPDATE evidence SET flag_reasons = ? WHERE id = ?`, [JSON.stringify(evReasons), result.evidenceId]);
|
|
365
367
|
}
|
|
366
368
|
catch { }
|
|
367
369
|
}
|
|
368
370
|
// 协议层:作为签名 SNF 信封投到对方 + 已分配仲裁员 inbox
|
|
369
371
|
try {
|
|
370
|
-
const d =
|
|
372
|
+
const d = await dbOne(`SELECT order_id, initiator_id, defendant_id, assigned_arbitrators FROM disputes WHERE id = ?`, [req.params.id]);
|
|
371
373
|
if (d) {
|
|
372
374
|
const uid = user.id;
|
|
373
375
|
const recipients = new Set();
|
|
@@ -419,7 +421,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
419
421
|
next();
|
|
420
422
|
};
|
|
421
423
|
// N: limit 精确 = EVIDENCE_MAX_BYTES
|
|
422
|
-
app.post('/api/disputes/:id/evidence-blob', lightAuthGuard, express.raw({ type: 'application/octet-stream', limit: EVIDENCE_MAX_BYTES }), (req, res) => {
|
|
424
|
+
app.post('/api/disputes/:id/evidence-blob', lightAuthGuard, express.raw({ type: 'application/octet-stream', limit: EVIDENCE_MAX_BYTES }), async (req, res) => {
|
|
423
425
|
const user = auth(req, res);
|
|
424
426
|
if (!user)
|
|
425
427
|
return;
|
|
@@ -458,15 +460,13 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
458
460
|
const evReasons = detectFraud(description);
|
|
459
461
|
if (evReasons.length > 0 && out.id) {
|
|
460
462
|
try {
|
|
461
|
-
|
|
462
|
-
.run(JSON.stringify(evReasons), out.id);
|
|
463
|
+
await dbRun(`UPDATE evidence SET flag_reasons = ? WHERE id = ?`, [JSON.stringify(evReasons), out.id]);
|
|
463
464
|
}
|
|
464
465
|
catch { }
|
|
465
466
|
}
|
|
466
467
|
// SNF 信封投递
|
|
467
468
|
try {
|
|
468
|
-
const d =
|
|
469
|
-
.get(req.params.id);
|
|
469
|
+
const d = await dbOne(`SELECT order_id, initiator_id, defendant_id, assigned_arbitrators FROM disputes WHERE id = ?`, [req.params.id]);
|
|
470
470
|
if (d) {
|
|
471
471
|
const uid = user.id;
|
|
472
472
|
const recipients = new Set();
|
|
@@ -549,8 +549,8 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
549
549
|
// Both endpoints require caller is one of dispute.assigned_arbitrators.
|
|
550
550
|
// Repause(extend) allowed — each pause writes an audit_log entry.
|
|
551
551
|
// No Iron-Rule Passkey: routine arbitrator action, fully audit-traceable.
|
|
552
|
-
function isAssignedArbitrator(disputeId, userId) {
|
|
553
|
-
const row =
|
|
552
|
+
async function isAssignedArbitrator(disputeId, userId) {
|
|
553
|
+
const row = await dbOne(`SELECT assigned_arbitrators FROM disputes WHERE id = ?`, [disputeId]);
|
|
554
554
|
if (!row)
|
|
555
555
|
return false;
|
|
556
556
|
let arr = [];
|
|
@@ -585,7 +585,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
585
585
|
return text; // unparseable — leave as is
|
|
586
586
|
return new Date(ms + secondsToAdd * 1000).toISOString();
|
|
587
587
|
}
|
|
588
|
-
app.post('/api/disputes/:id/arbitrator-pause-auto-judge', (req, res) => {
|
|
588
|
+
app.post('/api/disputes/:id/arbitrator-pause-auto-judge', async (req, res) => {
|
|
589
589
|
const user = auth(req, res);
|
|
590
590
|
if (!user)
|
|
591
591
|
return;
|
|
@@ -605,7 +605,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
605
605
|
if (untilTs > maxAllowed) {
|
|
606
606
|
return void errorRes(res, 400, 'EXCEEDS_MAX_HOURS', `until_ts 超过最大暂停窗口 ${maxHours}h(playbook §2.1)`);
|
|
607
607
|
}
|
|
608
|
-
const dispute =
|
|
608
|
+
const dispute = await dbOne(`SELECT id, status, ruling_type, assigned_arbitrators, auto_judge_paused_until, respond_deadline, arbitrate_deadline FROM disputes WHERE id = ?`, [disputeId]);
|
|
609
609
|
if (!dispute)
|
|
610
610
|
return void errorRes(res, 404, 'NOT_FOUND', 'dispute 不存在');
|
|
611
611
|
if (dispute.ruling_type) {
|
|
@@ -614,7 +614,7 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
614
614
|
if (dispute.status !== 'open' && dispute.status !== 'in_review') {
|
|
615
615
|
return void errorRes(res, 409, 'WRONG_STATUS', `status='${dispute.status}',只能 pause open / in_review`);
|
|
616
616
|
}
|
|
617
|
-
if (!isAssignedArbitrator(disputeId, userId)) {
|
|
617
|
+
if (!await isAssignedArbitrator(disputeId, userId)) {
|
|
618
618
|
return void errorRes(res, 403, 'NOT_ASSIGNED_ARBITRATOR', '仅 assigned_arbitrators 可暂停自动判定时钟');
|
|
619
619
|
}
|
|
620
620
|
// P0 fix:计算 deadline 扩展秒数
|
|
@@ -622,32 +622,64 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
622
622
|
// - repause(已经 paused):increment = untilTs - existing_paused_until(可能 < 0,clamp 0)
|
|
623
623
|
// 这样多次 pause 累加正确;repause 缩短无效果(只 audit_log 记)
|
|
624
624
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
625
|
+
// Codex #229 P1:上面的 await 预检与同步 tx 之间有 yield,dispute 的 status/ruling/
|
|
626
|
+
// assignment 可能已变。所有授权+状态判定 + baseline/increment 计算必须基于【tx 内重读】的行,
|
|
627
|
+
// 先于任何写抛回滚;预检仅作友好 fast-fail。
|
|
628
|
+
let incrementSec = 0;
|
|
629
|
+
let isRepause = false;
|
|
630
|
+
try {
|
|
631
|
+
db.transaction(() => {
|
|
632
|
+
const d = db.prepare(`SELECT status, ruling_type, assigned_arbitrators, auto_judge_paused_until, respond_deadline, arbitrate_deadline FROM disputes WHERE id = ?`).get(disputeId);
|
|
633
|
+
if (!d)
|
|
634
|
+
throw new Error('DW_NOT_FOUND');
|
|
635
|
+
if (d.ruling_type)
|
|
636
|
+
throw new Error('DW_ALREADY_RULED');
|
|
637
|
+
if (d.status !== 'open' && d.status !== 'in_review')
|
|
638
|
+
throw new Error('DW_WRONG_STATUS');
|
|
639
|
+
let assigned = [];
|
|
640
|
+
try {
|
|
641
|
+
assigned = JSON.parse(d.assigned_arbitrators || '[]');
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
assigned = [];
|
|
645
|
+
}
|
|
646
|
+
if (!assigned.includes(userId))
|
|
647
|
+
throw new Error('DW_NOT_ASSIGNED');
|
|
648
|
+
const baseline = d.auto_judge_paused_until && d.auto_judge_paused_until > nowSec ? d.auto_judge_paused_until : nowSec;
|
|
649
|
+
incrementSec = Math.max(0, untilTs - baseline);
|
|
650
|
+
isRepause = d.auto_judge_paused_until !== null && d.auto_judge_paused_until > nowSec;
|
|
651
|
+
// 扩展 deadline(若 increment > 0)— 基于 tx 内重读的 deadline,非陈旧预检值
|
|
652
|
+
if (incrementSec > 0) {
|
|
653
|
+
const newRespondDeadline = extendIsoDeadlineBySeconds(d.respond_deadline, incrementSec);
|
|
654
|
+
const newArbitrateDeadline = extendIsoDeadlineBySeconds(d.arbitrate_deadline, incrementSec);
|
|
655
|
+
db.prepare(`UPDATE disputes SET respond_deadline = ?, arbitrate_deadline = ? WHERE id = ?`)
|
|
656
|
+
.run(newRespondDeadline, newArbitrateDeadline, disputeId);
|
|
657
|
+
}
|
|
658
|
+
db.prepare(`UPDATE disputes SET auto_judge_paused_until = ?, auto_judge_pause_reason = ? WHERE id = ?`)
|
|
659
|
+
.run(untilTs, reason, disputeId);
|
|
660
|
+
appendAuditLog(disputeId, {
|
|
661
|
+
event: 'arbitrator_pause_auto_judge',
|
|
662
|
+
actor: userId,
|
|
663
|
+
reason,
|
|
664
|
+
until_ts: untilTs,
|
|
665
|
+
deadline_extended_seconds: incrementSec,
|
|
666
|
+
is_repause: isRepause,
|
|
667
|
+
spec_ref: 'playbook §2.1',
|
|
668
|
+
});
|
|
669
|
+
})();
|
|
670
|
+
}
|
|
671
|
+
catch (e) {
|
|
672
|
+
const msg = e.message;
|
|
673
|
+
if (msg === 'DW_NOT_FOUND')
|
|
674
|
+
return void errorRes(res, 404, 'NOT_FOUND', 'dispute 不存在');
|
|
675
|
+
if (msg === 'DW_ALREADY_RULED')
|
|
676
|
+
return void errorRes(res, 409, 'ALREADY_RULED', '已裁决的 dispute 不能暂停自动判定时钟');
|
|
677
|
+
if (msg === 'DW_WRONG_STATUS')
|
|
678
|
+
return void errorRes(res, 409, 'WRONG_STATUS', 'dispute 状态已变更,只能 pause open / in_review');
|
|
679
|
+
if (msg === 'DW_NOT_ASSIGNED')
|
|
680
|
+
return void errorRes(res, 403, 'NOT_ASSIGNED_ARBITRATOR', '仅 assigned_arbitrators 可暂停自动判定时钟');
|
|
681
|
+
throw e;
|
|
682
|
+
}
|
|
651
683
|
res.json({
|
|
652
684
|
success: true,
|
|
653
685
|
dispute_id: disputeId,
|
|
@@ -660,13 +692,13 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
660
692
|
: 'pause 已记录(repause 缩短无 deadline 变化)。',
|
|
661
693
|
});
|
|
662
694
|
});
|
|
663
|
-
app.post('/api/disputes/:id/arbitrator-resume-auto-judge', (req, res) => {
|
|
695
|
+
app.post('/api/disputes/:id/arbitrator-resume-auto-judge', async (req, res) => {
|
|
664
696
|
const user = auth(req, res);
|
|
665
697
|
if (!user)
|
|
666
698
|
return;
|
|
667
699
|
const userId = user.id;
|
|
668
700
|
const disputeId = req.params.id;
|
|
669
|
-
const dispute =
|
|
701
|
+
const dispute = await dbOne(`SELECT id, ruling_type, auto_judge_paused_until FROM disputes WHERE id = ?`, [disputeId]);
|
|
670
702
|
if (!dispute)
|
|
671
703
|
return void errorRes(res, 404, 'NOT_FOUND', 'dispute 不存在');
|
|
672
704
|
if (dispute.ruling_type) {
|
|
@@ -675,18 +707,49 @@ export function registerDisputesWriteRoutes(app, deps) {
|
|
|
675
707
|
if (!dispute.auto_judge_paused_until) {
|
|
676
708
|
return void errorRes(res, 409, 'NOT_PAUSED', '当前未暂停,无需 resume');
|
|
677
709
|
}
|
|
678
|
-
if (!isAssignedArbitrator(disputeId, userId)) {
|
|
710
|
+
if (!await isAssignedArbitrator(disputeId, userId)) {
|
|
679
711
|
return void errorRes(res, 403, 'NOT_ASSIGNED_ARBITRATOR', '仅 assigned_arbitrators 可 resume');
|
|
680
712
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
713
|
+
// Codex #229 P1:tx 内重读 + 重判授权/状态,先于任何写抛回滚;上面 await 预检仅友好 fast-fail。
|
|
714
|
+
try {
|
|
715
|
+
db.transaction(() => {
|
|
716
|
+
const d = db.prepare(`SELECT ruling_type, assigned_arbitrators, auto_judge_paused_until FROM disputes WHERE id = ?`).get(disputeId);
|
|
717
|
+
if (!d)
|
|
718
|
+
throw new Error('DW_NOT_FOUND');
|
|
719
|
+
if (d.ruling_type)
|
|
720
|
+
throw new Error('DW_ALREADY_RULED');
|
|
721
|
+
if (!d.auto_judge_paused_until)
|
|
722
|
+
throw new Error('DW_NOT_PAUSED');
|
|
723
|
+
let assigned = [];
|
|
724
|
+
try {
|
|
725
|
+
assigned = JSON.parse(d.assigned_arbitrators || '[]');
|
|
726
|
+
}
|
|
727
|
+
catch {
|
|
728
|
+
assigned = [];
|
|
729
|
+
}
|
|
730
|
+
if (!assigned.includes(userId))
|
|
731
|
+
throw new Error('DW_NOT_ASSIGNED');
|
|
732
|
+
db.prepare(`UPDATE disputes SET auto_judge_paused_until = NULL, auto_judge_pause_reason = NULL WHERE id = ?`)
|
|
733
|
+
.run(disputeId);
|
|
734
|
+
appendAuditLog(disputeId, {
|
|
735
|
+
event: 'arbitrator_resume_auto_judge',
|
|
736
|
+
actor: userId,
|
|
737
|
+
spec_ref: 'playbook §2.1',
|
|
738
|
+
});
|
|
739
|
+
})();
|
|
740
|
+
}
|
|
741
|
+
catch (e) {
|
|
742
|
+
const msg = e.message;
|
|
743
|
+
if (msg === 'DW_NOT_FOUND')
|
|
744
|
+
return void errorRes(res, 404, 'NOT_FOUND', 'dispute 不存在');
|
|
745
|
+
if (msg === 'DW_ALREADY_RULED')
|
|
746
|
+
return void errorRes(res, 409, 'ALREADY_RULED', '已裁决的 dispute 不需 resume');
|
|
747
|
+
if (msg === 'DW_NOT_PAUSED')
|
|
748
|
+
return void errorRes(res, 409, 'NOT_PAUSED', '当前未暂停,无需 resume');
|
|
749
|
+
if (msg === 'DW_NOT_ASSIGNED')
|
|
750
|
+
return void errorRes(res, 403, 'NOT_ASSIGNED_ARBITRATOR', '仅 assigned_arbitrators 可 resume');
|
|
751
|
+
throw e;
|
|
752
|
+
}
|
|
690
753
|
res.json({ success: true, dispute_id: disputeId, note: '自动判定时钟已解冻' });
|
|
691
754
|
});
|
|
692
755
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { readEvidenceBlob, withdrawEvidence, verifyEvidenceSig, listEvidence as listEvidenceFiles } from '../../layer3-trust/L3-1-dispute-engine/evidence-storage.js';
|
|
2
2
|
import { submitEvidenceForRequest } from '../../layer3-trust/L3-1-dispute-engine/dispute-engine.js';
|
|
3
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
4
|
export function registerEvidenceRoutes(app, deps) {
|
|
4
5
|
const { db, auth, detectFraud } = deps;
|
|
5
6
|
// 下载证据 blob(仅参与方/仲裁员)
|
|
6
|
-
app.get('/api/evidence/:id/blob', (req, res) => {
|
|
7
|
+
app.get('/api/evidence/:id/blob', async (req, res) => {
|
|
7
8
|
const user = auth(req, res);
|
|
8
9
|
if (!user)
|
|
9
10
|
return;
|
|
10
11
|
try {
|
|
11
|
-
const out = readEvidenceBlob(db, req.params.id, user.id);
|
|
12
|
+
const out = await readEvidenceBlob(db, req.params.id, user.id);
|
|
12
13
|
res.setHeader('Content-Type', out.mime);
|
|
13
14
|
res.setHeader('X-Content-Hash', out.hash);
|
|
14
15
|
res.setHeader('Cache-Control', 'private, max-age=300');
|
|
@@ -45,23 +46,23 @@ export function registerEvidenceRoutes(app, deps) {
|
|
|
45
46
|
}
|
|
46
47
|
});
|
|
47
48
|
// 验签 — 任意参与方
|
|
48
|
-
app.get('/api/evidence/:id/verify', (req, res) => {
|
|
49
|
+
app.get('/api/evidence/:id/verify', async (req, res) => {
|
|
49
50
|
const user = auth(req, res);
|
|
50
51
|
if (!user)
|
|
51
52
|
return;
|
|
52
|
-
const ev =
|
|
53
|
+
const ev = await dbOne('SELECT dispute_id FROM evidence WHERE id = ?', [req.params.id]);
|
|
53
54
|
if (!ev)
|
|
54
55
|
return void res.status(404).json({ error: 'evidence_not_found' });
|
|
55
56
|
try {
|
|
56
|
-
listEvidenceFiles(db, ev.dispute_id, user.id);
|
|
57
|
+
await listEvidenceFiles(db, ev.dispute_id, user.id);
|
|
57
58
|
} // 复用鉴权
|
|
58
59
|
catch {
|
|
59
60
|
return void res.status(403).json({ error: 'not_dispute_party' });
|
|
60
61
|
}
|
|
61
|
-
res.json(verifyEvidenceSig(db, req.params.id));
|
|
62
|
+
res.json(await verifyEvidenceSig(db, req.params.id));
|
|
62
63
|
});
|
|
63
64
|
// 当事人提交补充证据响应(仲裁员 request 后用)
|
|
64
|
-
app.post('/api/evidence-requests/:requestId/submit', (req, res) => {
|
|
65
|
+
app.post('/api/evidence-requests/:requestId/submit', async (req, res) => {
|
|
65
66
|
const user = auth(req, res);
|
|
66
67
|
if (!user)
|
|
67
68
|
return;
|
|
@@ -76,8 +77,7 @@ export function registerEvidenceRoutes(app, deps) {
|
|
|
76
77
|
const evReasons = detectFraud(rawDesc);
|
|
77
78
|
if (evReasons.length > 0 && result.evidenceId) {
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
-
.run(JSON.stringify(evReasons), result.evidenceId);
|
|
80
|
+
await dbRun(`UPDATE evidence SET flag_reasons = ? WHERE id = ?`, [JSON.stringify(evReasons), result.evidenceId]);
|
|
81
81
|
}
|
|
82
82
|
catch { }
|
|
83
83
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
import { createAnchor, verifyAnchorSignature, revokeAnchor, issueOwnershipToken, submitVerification, getAnchor, listAnchorsByProduct, listAnchorsBySeller, distributeAnchorRewards, ANCHOR_VERIFICATION_FEE_RECOMMENDED, } from '../../layer1-agent/L1-2-external-anchor/anchor-engine.js';
|
|
2
3
|
export function registerExternalAnchorsRoutes(app, deps) {
|
|
3
4
|
const { db, auth } = deps;
|
|
@@ -24,14 +25,14 @@ export function registerExternalAnchorsRoutes(app, deps) {
|
|
|
24
25
|
}
|
|
25
26
|
});
|
|
26
27
|
// 透出推荐 fee + anchor 的奖励情况
|
|
27
|
-
app.get('/api/external-anchors/:id/rewards', (req, res) => {
|
|
28
|
-
const a = getAnchor(db, req.params.id);
|
|
28
|
+
app.get('/api/external-anchors/:id/rewards', async (req, res) => {
|
|
29
|
+
const a = await getAnchor(db, req.params.id);
|
|
29
30
|
if (!a)
|
|
30
31
|
return void res.status(404).json({ error: 'anchor 不存在' });
|
|
31
|
-
const verifications =
|
|
32
|
+
const verifications = await dbAll(`
|
|
32
33
|
SELECT verifier_id, verifier_role, content_matches, token_found, reward_amount, verified_at
|
|
33
34
|
FROM external_anchor_verifications WHERE anchor_id = ? ORDER BY verified_at ASC
|
|
34
|
-
|
|
35
|
+
`, [req.params.id]);
|
|
35
36
|
res.json({
|
|
36
37
|
verification_fee: a.verification_fee || 0,
|
|
37
38
|
fee_paid_out: !!a.fee_paid_out,
|
|
@@ -51,20 +52,20 @@ export function registerExternalAnchorsRoutes(app, deps) {
|
|
|
51
52
|
const paid = distributeAnchorRewards(db, req.params.id);
|
|
52
53
|
res.json({ ok: true, paid });
|
|
53
54
|
});
|
|
54
|
-
app.get('/api/external-anchors/by-product/:id', (req, res) => {
|
|
55
|
-
res.json({ items: listAnchorsByProduct(db, req.params.id) });
|
|
55
|
+
app.get('/api/external-anchors/by-product/:id', async (req, res) => {
|
|
56
|
+
res.json({ items: await listAnchorsByProduct(db, req.params.id) });
|
|
56
57
|
});
|
|
57
|
-
app.get('/api/external-anchors/by-seller/:id', (req, res) => {
|
|
58
|
-
res.json({ items: listAnchorsBySeller(db, req.params.id) });
|
|
58
|
+
app.get('/api/external-anchors/by-seller/:id', async (req, res) => {
|
|
59
|
+
res.json({ items: await listAnchorsBySeller(db, req.params.id) });
|
|
59
60
|
});
|
|
60
|
-
app.get('/api/external-anchors/:id', (req, res) => {
|
|
61
|
-
const a = getAnchor(db, req.params.id);
|
|
61
|
+
app.get('/api/external-anchors/:id', async (req, res) => {
|
|
62
|
+
const a = await getAnchor(db, req.params.id);
|
|
62
63
|
if (!a)
|
|
63
64
|
return void res.status(404).json({ error: 'anchor 不存在' });
|
|
64
65
|
res.json(a);
|
|
65
66
|
});
|
|
66
|
-
app.get('/api/external-anchors/:id/verify-sig', (req, res) => {
|
|
67
|
-
res.json(verifyAnchorSignature(db, req.params.id));
|
|
67
|
+
app.get('/api/external-anchors/:id/verify-sig', async (req, res) => {
|
|
68
|
+
res.json(await verifyAnchorSignature(db, req.params.id));
|
|
68
69
|
});
|
|
69
70
|
app.post('/api/external-anchors/:id/revoke', (req, res) => {
|
|
70
71
|
const user = auth(req, res);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
const VALID_FEEDBACK_CAT = new Set(['bug', 'abuse', 'feature', 'account', 'other']);
|
|
2
3
|
const VALID_FEEDBACK_SEV = new Set(['low', 'medium', 'high']);
|
|
3
4
|
export function registerFeedbackRoutes(app, deps) {
|
|
4
5
|
const { db, generateId, auth, broadcastSystemEvent, detectFraud, anthropic } = deps;
|
|
5
|
-
app.post('/api/feedback', (req, res) => {
|
|
6
|
+
app.post('/api/feedback', async (req, res) => {
|
|
6
7
|
const user = auth(req, res);
|
|
7
8
|
if (!user)
|
|
8
9
|
return;
|
|
@@ -16,12 +17,11 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
16
17
|
return void res.status(400).json({ error: '标题 4-80 字' });
|
|
17
18
|
if (bod.length < 10 || bod.length > 2000)
|
|
18
19
|
return void res.status(400).json({ error: '正文 10-2000 字' });
|
|
19
|
-
const recent =
|
|
20
|
+
const recent = (await dbOne(`SELECT COUNT(*) as n FROM feedback_tickets WHERE user_id = ? AND created_at > datetime('now', '-1 hour')`, [user.id])).n;
|
|
20
21
|
if (recent >= 5)
|
|
21
22
|
return void res.status(429).json({ error: '提交过于频繁,请稍后再试' });
|
|
22
23
|
const id = generateId('fbk');
|
|
23
|
-
|
|
24
|
-
.run(id, user.id, String(category), sev, sub, bod);
|
|
24
|
+
await dbRun(`INSERT INTO feedback_tickets (id, user_id, category, severity, subject, body) VALUES (?,?,?,?,?,?)`, [id, user.id, String(category), sev, sub, bod]);
|
|
25
25
|
try {
|
|
26
26
|
broadcastSystemEvent('feedback', '💬', `反馈工单 ${id} · ${category}/${sev}`, id);
|
|
27
27
|
}
|
|
@@ -43,8 +43,7 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
43
43
|
}],
|
|
44
44
|
});
|
|
45
45
|
const text = message.content[0]?.text || '';
|
|
46
|
-
|
|
47
|
-
.run(text.trim(), id);
|
|
46
|
+
await dbRun(`UPDATE feedback_tickets SET ai_suggested_reply = ?, ai_generated_at = datetime('now') WHERE id = ?`, [text.trim(), id]);
|
|
48
47
|
}
|
|
49
48
|
catch (e) {
|
|
50
49
|
console.error('[ai feedback draft]', e.message);
|
|
@@ -52,12 +51,12 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
52
51
|
})();
|
|
53
52
|
res.json({ success: true, id });
|
|
54
53
|
});
|
|
55
|
-
app.get('/api/feedback/mine', (req, res) => {
|
|
54
|
+
app.get('/api/feedback/mine', async (req, res) => {
|
|
56
55
|
const user = auth(req, res);
|
|
57
56
|
if (!user)
|
|
58
57
|
return;
|
|
59
|
-
const rows =
|
|
60
|
-
FROM feedback_tickets WHERE user_id = ? ORDER BY created_at DESC LIMIT 100
|
|
58
|
+
const rows = await dbAll(`SELECT id, category, severity, subject, body, status, admin_reply, replied_at, user_seen_reply_at, created_at, updated_at
|
|
59
|
+
FROM feedback_tickets WHERE user_id = ? ORDER BY created_at DESC LIMIT 100`, [user.id]);
|
|
61
60
|
// 派生 has_unread_reply
|
|
62
61
|
let unreadReplyCount = 0;
|
|
63
62
|
for (const r of rows) {
|
|
@@ -71,16 +70,16 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
71
70
|
}
|
|
72
71
|
res.json({ items: rows, unread_reply_count: unreadReplyCount });
|
|
73
72
|
});
|
|
74
|
-
app.post('/api/feedback/seen', (req, res) => {
|
|
73
|
+
app.post('/api/feedback/seen', async (req, res) => {
|
|
75
74
|
const user = auth(req, res);
|
|
76
75
|
if (!user)
|
|
77
76
|
return;
|
|
78
|
-
|
|
79
|
-
WHERE user_id = ? AND admin_reply IS NOT NULL AND (user_seen_reply_at IS NULL OR replied_at > user_seen_reply_at)
|
|
77
|
+
await dbRun(`UPDATE feedback_tickets SET user_seen_reply_at = datetime('now')
|
|
78
|
+
WHERE user_id = ? AND admin_reply IS NOT NULL AND (user_seen_reply_at IS NULL OR replied_at > user_seen_reply_at)`, [user.id]);
|
|
80
79
|
res.json({ success: true });
|
|
81
80
|
});
|
|
82
81
|
// admin 列出工单
|
|
83
|
-
app.get('/api/admin/feedback', (req, res) => {
|
|
82
|
+
app.get('/api/admin/feedback', async (req, res) => {
|
|
84
83
|
const user = auth(req, res);
|
|
85
84
|
if (!user)
|
|
86
85
|
return;
|
|
@@ -99,7 +98,7 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
99
98
|
params.push(category);
|
|
100
99
|
}
|
|
101
100
|
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
102
|
-
const rows =
|
|
101
|
+
const rows = await dbAll(`
|
|
103
102
|
SELECT f.*, u.name as user_name, u.handle as user_handle, u.role as user_role
|
|
104
103
|
FROM feedback_tickets f
|
|
105
104
|
JOIN users u ON u.id = f.user_id
|
|
@@ -109,17 +108,17 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
109
108
|
CASE f.severity WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END,
|
|
110
109
|
f.created_at DESC
|
|
111
110
|
LIMIT 200
|
|
112
|
-
|
|
111
|
+
`, params);
|
|
113
112
|
res.json({ items: rows });
|
|
114
113
|
});
|
|
115
114
|
// admin 回复 + 切状态
|
|
116
|
-
app.post('/api/admin/feedback/:id/reply', (req, res) => {
|
|
115
|
+
app.post('/api/admin/feedback/:id/reply', async (req, res) => {
|
|
117
116
|
const user = auth(req, res);
|
|
118
117
|
if (!user)
|
|
119
118
|
return;
|
|
120
119
|
if (user.role !== 'admin')
|
|
121
120
|
return void res.status(403).json({ error: '仅 admin 可回复' });
|
|
122
|
-
const ticket =
|
|
121
|
+
const ticket = await dbOne('SELECT user_id, status FROM feedback_tickets WHERE id = ?', [req.params.id]);
|
|
123
122
|
if (!ticket)
|
|
124
123
|
return void res.status(404).json({ error: '工单不存在' });
|
|
125
124
|
const { reply, status } = req.body || {};
|
|
@@ -137,8 +136,7 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
137
136
|
})();
|
|
138
137
|
try {
|
|
139
138
|
const actions = JSON.stringify([{ kind: 'navigate', label: '查看工单', href: `#ticket/${req.params.id}`, style: 'primary' }]);
|
|
140
|
-
|
|
141
|
-
.run(generateId('ntf'), ticket.user_id, 'ticket_reply', `💬 客服回复了你的工单`, replyStr.slice(0, 100), null, actions);
|
|
139
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), ticket.user_id, 'ticket_reply', `💬 客服回复了你的工单`, replyStr.slice(0, 100), null, actions]);
|
|
142
140
|
}
|
|
143
141
|
catch (e) {
|
|
144
142
|
console.warn('[notif ticket_reply]', e.message);
|
|
@@ -147,25 +145,25 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
147
145
|
});
|
|
148
146
|
// ─── W7 ticket-thread ────────────────────────────────────
|
|
149
147
|
// 工单详情 + timeline
|
|
150
|
-
app.get('/api/feedback/:id', (req, res) => {
|
|
148
|
+
app.get('/api/feedback/:id', async (req, res) => {
|
|
151
149
|
const user = auth(req, res);
|
|
152
150
|
if (!user)
|
|
153
151
|
return;
|
|
154
|
-
const t =
|
|
152
|
+
const t = await dbOne(`
|
|
155
153
|
SELECT f.*, u.name as user_name, u.handle as user_handle, u.role as user_role
|
|
156
154
|
FROM feedback_tickets f JOIN users u ON u.id = f.user_id WHERE f.id = ?
|
|
157
|
-
|
|
155
|
+
`, [req.params.id]);
|
|
158
156
|
if (!t)
|
|
159
157
|
return void res.status(404).json({ error: '工单不存在' });
|
|
160
158
|
const isOwner = t.user_id === user.id;
|
|
161
159
|
const isAdmin = user.role === 'admin';
|
|
162
160
|
if (!isOwner && !isAdmin)
|
|
163
161
|
return void res.status(403).json({ error: '无权查看' });
|
|
164
|
-
const messages =
|
|
162
|
+
const messages = await dbAll(`
|
|
165
163
|
SELECT m.*, u.name as sender_name, u.handle as sender_handle
|
|
166
164
|
FROM feedback_messages m LEFT JOIN users u ON u.id = m.sender_id
|
|
167
165
|
WHERE m.ticket_id = ? ORDER BY m.created_at ASC
|
|
168
|
-
|
|
166
|
+
`, [t.id]);
|
|
169
167
|
const events = [];
|
|
170
168
|
events.push({
|
|
171
169
|
id: `create-${t.id}`,
|
|
@@ -206,24 +204,24 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
206
204
|
events.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
207
205
|
if (isOwner) {
|
|
208
206
|
try {
|
|
209
|
-
|
|
207
|
+
await dbRun(`UPDATE feedback_tickets SET user_seen_reply_at = datetime('now') WHERE id = ? AND admin_reply IS NOT NULL`, [t.id]);
|
|
210
208
|
}
|
|
211
209
|
catch { }
|
|
212
210
|
}
|
|
213
211
|
if (isAdmin) {
|
|
214
212
|
try {
|
|
215
|
-
|
|
213
|
+
await dbRun(`UPDATE feedback_tickets SET admin_seen_at = datetime('now') WHERE id = ?`, [t.id]);
|
|
216
214
|
}
|
|
217
215
|
catch { }
|
|
218
216
|
}
|
|
219
217
|
res.json({ item: t, timeline: events, is_admin: isAdmin });
|
|
220
218
|
});
|
|
221
219
|
// 工单内追加消息(user 或 admin)
|
|
222
|
-
app.post('/api/feedback/:id/messages', (req, res) => {
|
|
220
|
+
app.post('/api/feedback/:id/messages', async (req, res) => {
|
|
223
221
|
const user = auth(req, res);
|
|
224
222
|
if (!user)
|
|
225
223
|
return;
|
|
226
|
-
const t =
|
|
224
|
+
const t = await dbOne(`SELECT id, user_id, status FROM feedback_tickets WHERE id = ?`, [req.params.id]);
|
|
227
225
|
if (!t)
|
|
228
226
|
return void res.status(404).json({ error: '工单不存在' });
|
|
229
227
|
const isOwner = t.user_id === user.id;
|
|
@@ -251,15 +249,13 @@ export function registerFeedbackRoutes(app, deps) {
|
|
|
251
249
|
try {
|
|
252
250
|
const tktAction = JSON.stringify([{ kind: 'navigate', label: '查看工单', href: `#ticket/${t.id}`, style: 'primary' }]);
|
|
253
251
|
if (isOwner) {
|
|
254
|
-
const admins =
|
|
252
|
+
const admins = await dbAll(`SELECT id FROM users WHERE role = 'admin'`, []);
|
|
255
253
|
for (const a of admins) {
|
|
256
|
-
|
|
257
|
-
.run(generateId('ntf'), a.id, 'ticket_followup', '💬 用户追问了工单', body.slice(0, 100), null, tktAction);
|
|
254
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), a.id, 'ticket_followup', '💬 用户追问了工单', body.slice(0, 100), null, tktAction]);
|
|
258
255
|
}
|
|
259
256
|
}
|
|
260
257
|
else {
|
|
261
|
-
|
|
262
|
-
.run(generateId('ntf'), t.user_id, 'ticket_reply', '💬 客服回复了你的工单', body.slice(0, 100), null, tktAction);
|
|
258
|
+
await dbRun(`INSERT INTO notifications (id, user_id, type, title, body, order_id, actions) VALUES (?,?,?,?,?,?,?)`, [generateId('ntf'), t.user_id, 'ticket_reply', '💬 客服回复了你的工单', body.slice(0, 100), null, tktAction]);
|
|
263
259
|
}
|
|
264
260
|
}
|
|
265
261
|
catch (e) {
|