@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
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { computeAgentPassport } from '../../layer1-agent/L1-2-identity/agent-passport.js';
|
|
2
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
export function registerAgentGovernanceRoutes(app, deps) {
|
|
3
4
|
const { db, generateId, auth, requireRootAdmin, invalidateAgentBlockedCache, requireHumanPresence, issueAgentStrike, custodianFingerprint, signPassport, issuerAddress } = deps;
|
|
4
5
|
// /api/me/agents — 列出本账号所有 agent + declaration / strikes
|
|
5
|
-
app.get('/api/me/agents', (req, res) => {
|
|
6
|
+
app.get('/api/me/agents', async (req, res) => {
|
|
6
7
|
const user = auth(req, res);
|
|
7
8
|
if (!user)
|
|
8
9
|
return;
|
|
9
|
-
const keys =
|
|
10
|
-
const items = keys.map(k => {
|
|
11
|
-
const decl =
|
|
10
|
+
const keys = await dbAll(`SELECT api_key FROM users WHERE id = ? UNION SELECT api_key FROM agent_reputation WHERE user_id = ?`, [user.id, user.id]);
|
|
11
|
+
const items = await Promise.all(keys.map(async (k) => {
|
|
12
|
+
const decl = await dbOne(`SELECT operator_name, operator_contact, purpose, declared_scope, attestations, repo_url, revoked_at FROM agent_declarations WHERE api_key = ?`, [k.api_key]);
|
|
12
13
|
// P1 fix 4.4:附 signals JSON
|
|
13
|
-
const rep =
|
|
14
|
+
const rep = await dbOne(`SELECT trust_score, level, signals, last_calculated_at FROM agent_reputation WHERE api_key = ?`, [k.api_key]);
|
|
14
15
|
if (rep && rep.signals) {
|
|
15
16
|
try {
|
|
16
17
|
rep.signals = JSON.parse(rep.signals);
|
|
@@ -19,9 +20,9 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
19
20
|
rep.signals = null;
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
|
-
const calls30d =
|
|
23
|
-
const last =
|
|
24
|
-
const strikes =
|
|
23
|
+
const calls30d = (await dbOne(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND created_at > datetime('now', '-30 days')`, [k.api_key])).n;
|
|
24
|
+
const last = await dbOne(`SELECT endpoint, method, status_code, created_at FROM agent_call_log WHERE api_key = ? ORDER BY created_at DESC LIMIT 1`, [k.api_key]);
|
|
25
|
+
const strikes = await dbAll(`SELECT severity, reason_code, issued_at, expires_at, appeal_status FROM agent_strikes WHERE api_key = ? ORDER BY issued_at DESC LIMIT 5`, [k.api_key]);
|
|
25
26
|
let passport = null;
|
|
26
27
|
try {
|
|
27
28
|
passport = computeAgentPassport(db, k.api_key, user.id, custodianFingerprint);
|
|
@@ -37,9 +38,9 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
37
38
|
recent_strikes: strikes,
|
|
38
39
|
passport,
|
|
39
40
|
};
|
|
40
|
-
});
|
|
41
|
+
}));
|
|
41
42
|
// Phase 2 监护人总览(只读/软绑定):真人态 + 旗下 agent 聚合 + 连带
|
|
42
|
-
const hasPasskey = (
|
|
43
|
+
const hasPasskey = ((await dbOne('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?', [user.id]))?.n || 0) > 0;
|
|
43
44
|
const pps = items.map(i => i.passport).filter(Boolean);
|
|
44
45
|
const depthRank = { shallow: 0, medium: 1, deep: 2, profound: 3 };
|
|
45
46
|
const deepest = pps.reduce((d, p) => (depthRank[p.engagement_depth] ?? 0) > (depthRank[d] ?? 0) ? p.engagement_depth : d, 'shallow');
|
|
@@ -65,7 +66,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
65
66
|
const prefix = String(req.params.apiKeyPrefix || '').replace('...', '');
|
|
66
67
|
if (prefix.length < 6)
|
|
67
68
|
return void res.status(400).json({ error: 'apiKeyPrefix 太短' });
|
|
68
|
-
const keys =
|
|
69
|
+
const keys = await dbAll(`SELECT api_key FROM users WHERE id = ? UNION SELECT api_key FROM agent_reputation WHERE user_id = ?`, [user.id, user.id]);
|
|
69
70
|
const match = keys.find(k => k.api_key.startsWith(prefix));
|
|
70
71
|
if (!match)
|
|
71
72
|
return void res.status(404).json({ error: '未找到该 agent(或不属于你)' });
|
|
@@ -135,32 +136,30 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
135
136
|
webaz_format, // 显式重复让消费者明确选哪个
|
|
136
137
|
});
|
|
137
138
|
});
|
|
138
|
-
app.get('/api/me/agents/:apiKeyPrefix/log', (req, res) => {
|
|
139
|
+
app.get('/api/me/agents/:apiKeyPrefix/log', async (req, res) => {
|
|
139
140
|
const user = auth(req, res);
|
|
140
141
|
if (!user)
|
|
141
142
|
return;
|
|
142
143
|
const prefix = String(req.params.apiKeyPrefix || '').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32);
|
|
143
144
|
if (prefix.length < 8)
|
|
144
145
|
return void res.status(400).json({ error: 'apiKeyPrefix 至少 8 字符' });
|
|
145
|
-
const targetKey =
|
|
146
|
-
UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'
|
|
147
|
-
.get(user.id, prefix, user.id, prefix);
|
|
146
|
+
const targetKey = await dbOne(`SELECT api_key FROM users WHERE id = ? AND api_key LIKE ? || '%'
|
|
147
|
+
UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'`, [user.id, prefix, user.id, prefix]);
|
|
148
148
|
if (!targetKey)
|
|
149
149
|
return void res.status(404).json({ error: '未找到匹配的 agent api_key(仅可查本人的)' });
|
|
150
150
|
const limit = Math.min(500, Math.max(10, Number(req.query.limit) || 100));
|
|
151
|
-
const rows =
|
|
152
|
-
WHERE api_key = ? AND created_at > datetime('now', '-30 days') ORDER BY id DESC LIMIT
|
|
151
|
+
const rows = await dbAll(`SELECT endpoint, method, status_code, created_at FROM agent_call_log
|
|
152
|
+
WHERE api_key = ? AND created_at > datetime('now', '-30 days') ORDER BY id DESC LIMIT ?`, [targetKey.api_key, limit]);
|
|
153
153
|
res.json({ items: rows });
|
|
154
154
|
});
|
|
155
|
-
app.post('/api/me/agents/declarations', (req, res) => {
|
|
155
|
+
app.post('/api/me/agents/declarations', async (req, res) => {
|
|
156
156
|
const user = auth(req, res);
|
|
157
157
|
if (!user)
|
|
158
158
|
return;
|
|
159
159
|
const b = req.body;
|
|
160
160
|
const targetApiKey = b.api_key ? String(b.api_key) : user.api_key;
|
|
161
|
-
const ownership =
|
|
162
|
-
UNION SELECT user_id as id FROM agent_reputation WHERE user_id = ? AND api_key =
|
|
163
|
-
.get(user.id, targetApiKey, user.id, targetApiKey);
|
|
161
|
+
const ownership = await dbOne(`SELECT id FROM users WHERE id = ? AND api_key = ?
|
|
162
|
+
UNION SELECT user_id as id FROM agent_reputation WHERE user_id = ? AND api_key = ?`, [user.id, targetApiKey, user.id, targetApiKey]);
|
|
164
163
|
if (!ownership)
|
|
165
164
|
return void res.status(403).json({ error: 'api_key 不属于本账号' });
|
|
166
165
|
const operator_name = String(b.operator_name || '').trim().slice(0, 60);
|
|
@@ -185,7 +184,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
185
184
|
const attestationsJson = b.attestations && typeof b.attestations === 'object' ? JSON.stringify(b.attestations).slice(0, 2000) : null;
|
|
186
185
|
const repo_url = b.repo_url ? String(b.repo_url).slice(0, 200) : null;
|
|
187
186
|
const homepage = b.homepage ? String(b.homepage).slice(0, 200) : null;
|
|
188
|
-
|
|
187
|
+
await dbRun(`INSERT INTO agent_declarations (
|
|
189
188
|
api_key, user_id, operator_name, operator_contact, purpose, declared_scope, attestations, repo_url, homepage
|
|
190
189
|
) VALUES (?,?,?,?,?,?,?,?,?)
|
|
191
190
|
ON CONFLICT(api_key) DO UPDATE SET
|
|
@@ -197,36 +196,35 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
197
196
|
repo_url = excluded.repo_url,
|
|
198
197
|
homepage = excluded.homepage,
|
|
199
198
|
revoked_at = NULL,
|
|
200
|
-
updated_at = datetime('now')
|
|
199
|
+
updated_at = datetime('now')`, [targetApiKey, user.id, operator_name, operator_contact, purpose, scopeJson, attestationsJson, repo_url, homepage]);
|
|
201
200
|
invalidateAgentBlockedCache(targetApiKey);
|
|
202
201
|
res.json({ ok: true });
|
|
203
202
|
});
|
|
204
203
|
// 用户撤销 agent(铁律 §4 human presence)
|
|
205
|
-
app.post('/api/me/agents/:apiKeyPrefix/revoke', (req, res) => {
|
|
204
|
+
app.post('/api/me/agents/:apiKeyPrefix/revoke', async (req, res) => {
|
|
206
205
|
const user = auth(req, res);
|
|
207
206
|
if (!user)
|
|
208
207
|
return;
|
|
209
208
|
const prefix = String(req.params.apiKeyPrefix || '').replace(/[^A-Za-z0-9_]/g, '').slice(0, 32);
|
|
210
209
|
if (prefix.length < 8)
|
|
211
210
|
return void res.status(400).json({ error: 'apiKeyPrefix 至少 8 字符' });
|
|
212
|
-
const targetKey =
|
|
213
|
-
UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'
|
|
214
|
-
.get(user.id, prefix, user.id, prefix);
|
|
211
|
+
const targetKey = await dbOne(`SELECT api_key FROM users WHERE id = ? AND api_key LIKE ? || '%'
|
|
212
|
+
UNION SELECT api_key FROM agent_reputation WHERE user_id = ? AND api_key LIKE ? || '%'`, [user.id, prefix, user.id, prefix]);
|
|
215
213
|
if (!targetKey)
|
|
216
214
|
return void res.status(404).json({ error: '未找到匹配的 agent api_key' });
|
|
217
215
|
const hpCheck = requireHumanPresence(user.id, 'agent_revoke', (req.body || {}).webauthn_token, 'require_human_presence_for_agent_revoke', () => true);
|
|
218
216
|
if (!hpCheck.ok)
|
|
219
217
|
return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
|
|
220
218
|
const reason = String((req.body || {}).reason || '').slice(0, 300);
|
|
221
|
-
|
|
219
|
+
await dbRun(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
|
|
222
220
|
VALUES ('api_key', ?, ?, 'self', ?)
|
|
223
|
-
ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING
|
|
224
|
-
|
|
221
|
+
ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING`, [targetKey.api_key, user.id, reason]);
|
|
222
|
+
await dbRun(`UPDATE agent_declarations SET revoked_at = datetime('now'), revoked_reason = ? WHERE api_key = ?`, [reason, targetKey.api_key]);
|
|
225
223
|
invalidateAgentBlockedCache(targetKey.api_key);
|
|
226
224
|
res.json({ ok: true });
|
|
227
225
|
});
|
|
228
226
|
// 撤销同 operator 名下所有 agent(仅撤销本用户给 operator 旗下 agent 的 attestation)
|
|
229
|
-
app.post('/api/me/agents/operators/:operator_name/revoke', (req, res) => {
|
|
227
|
+
app.post('/api/me/agents/operators/:operator_name/revoke', async (req, res) => {
|
|
230
228
|
const user = auth(req, res);
|
|
231
229
|
if (!user)
|
|
232
230
|
return;
|
|
@@ -237,27 +235,26 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
237
235
|
if (!hpCheck.ok)
|
|
238
236
|
return void res.status(412).json({ error: hpCheck.reason, error_code: hpCheck.error_code });
|
|
239
237
|
const reason = String((req.body || {}).reason || '').slice(0, 300);
|
|
240
|
-
const affected =
|
|
238
|
+
const affected = await dbRun(`UPDATE agent_attestations SET revoked_at = datetime('now')
|
|
241
239
|
WHERE user_id = ? AND revoked_at IS NULL
|
|
242
|
-
AND api_key IN (SELECT api_key FROM agent_declarations WHERE operator_name = ?)
|
|
243
|
-
|
|
244
|
-
db.prepare(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
|
|
240
|
+
AND api_key IN (SELECT api_key FROM agent_declarations WHERE operator_name = ?)`, [user.id, opName]);
|
|
241
|
+
await dbRun(`INSERT INTO agent_revocations (target_kind, target_value, revoked_by, revoked_by_role, reason)
|
|
245
242
|
VALUES ('operator_name', ?, ?, 'self', ?)
|
|
246
|
-
ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING
|
|
247
|
-
const keys =
|
|
243
|
+
ON CONFLICT(target_kind, target_value, revoked_by) DO NOTHING`, [opName, user.id, reason]);
|
|
244
|
+
const keys = await dbAll(`SELECT api_key FROM agent_declarations WHERE operator_name = ?`, [opName]);
|
|
248
245
|
for (const k of keys)
|
|
249
246
|
invalidateAgentBlockedCache(k.api_key);
|
|
250
247
|
res.json({ ok: true, attestations_revoked: affected.changes });
|
|
251
248
|
});
|
|
252
249
|
// P0 audit fix 4.2: 申诉 strike
|
|
253
|
-
app.post('/api/me/agents/strikes/:strikeId/appeal', (req, res) => {
|
|
250
|
+
app.post('/api/me/agents/strikes/:strikeId/appeal', async (req, res) => {
|
|
254
251
|
const user = auth(req, res);
|
|
255
252
|
if (!user)
|
|
256
253
|
return;
|
|
257
254
|
const strikeId = Number(req.params.strikeId);
|
|
258
255
|
if (!Number.isInteger(strikeId) || strikeId <= 0)
|
|
259
256
|
return void res.status(400).json({ error: 'strikeId 必须是正整数' });
|
|
260
|
-
const strike =
|
|
257
|
+
const strike = await dbOne(`SELECT id, api_key, user_id, severity, issued_at, appeal_status FROM agent_strikes WHERE id = ?`, [strikeId]);
|
|
261
258
|
if (!strike)
|
|
262
259
|
return void res.status(404).json({ error: 'strike 不存在' });
|
|
263
260
|
if (strike.user_id !== user.id)
|
|
@@ -270,11 +267,11 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
270
267
|
const reason = String((req.body || {}).reason || '').trim().slice(0, 500);
|
|
271
268
|
if (reason.length < 10)
|
|
272
269
|
return void res.status(400).json({ error: '申诉理由 ≥10 字' });
|
|
273
|
-
|
|
270
|
+
await dbRun(`UPDATE agent_strikes SET appeal_status = 'pending', appeal_reason = ? WHERE id = ?`, [reason, strikeId]);
|
|
274
271
|
res.json({ ok: true, message: '申诉已提交,等待 root admin 审核' });
|
|
275
272
|
});
|
|
276
273
|
// Admin: 审核 strike 申诉
|
|
277
|
-
app.post('/api/admin/agent-strikes/:strikeId/decide', (req, res) => {
|
|
274
|
+
app.post('/api/admin/agent-strikes/:strikeId/decide', async (req, res) => {
|
|
278
275
|
const user = requireRootAdmin(req, res);
|
|
279
276
|
if (!user)
|
|
280
277
|
return;
|
|
@@ -284,21 +281,20 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
284
281
|
const decision = String((req.body || {}).decision || '');
|
|
285
282
|
if (!['approved', 'denied'].includes(decision))
|
|
286
283
|
return void res.status(400).json({ error: 'decision 必须是 approved / denied' });
|
|
287
|
-
const strike =
|
|
284
|
+
const strike = await dbOne(`SELECT id, api_key, appeal_status FROM agent_strikes WHERE id = ?`, [strikeId]);
|
|
288
285
|
if (!strike)
|
|
289
286
|
return void res.status(404).json({ error: 'strike 不存在' });
|
|
290
287
|
if (strike.appeal_status !== 'pending')
|
|
291
288
|
return void res.status(409).json({ error: `当前状态 ${strike.appeal_status} 不可裁决` });
|
|
292
|
-
|
|
293
|
-
.run(decision, user.id, strikeId);
|
|
289
|
+
await dbRun(`UPDATE agent_strikes SET appeal_status = ?, appeal_decided_by = ?, appeal_decided_at = datetime('now') WHERE id = ?`, [decision, user.id, strikeId]);
|
|
294
290
|
invalidateAgentBlockedCache(strike.api_key);
|
|
295
291
|
// P1 fix 5.3: appeal approved → 恢复因 strike 自动停用的 skills
|
|
296
292
|
if (decision === 'approved') {
|
|
297
293
|
try {
|
|
298
|
-
const uRow =
|
|
294
|
+
const uRow = await dbOne(`SELECT id FROM users WHERE api_key = ?`, [strike.api_key]);
|
|
299
295
|
if (uRow) {
|
|
300
|
-
const r =
|
|
301
|
-
WHERE seller_id = ? AND disabled_by_strike_at IS NOT NULL AND active = 0
|
|
296
|
+
const r = await dbRun(`UPDATE skills SET active = 1, disabled_by_strike_at = NULL
|
|
297
|
+
WHERE seller_id = ? AND disabled_by_strike_at IS NOT NULL AND active = 0`, [uRow.id]);
|
|
302
298
|
if (r.changes > 0)
|
|
303
299
|
console.log(`[appeal approved→skill] restored ${r.changes} skills for ${uRow.id}`);
|
|
304
300
|
}
|
|
@@ -310,17 +306,17 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
310
306
|
res.json({ ok: true, decision });
|
|
311
307
|
});
|
|
312
308
|
// Admin: 列出待审 strike 申诉
|
|
313
|
-
app.get('/api/admin/agent-strikes/pending', (req, res) => {
|
|
309
|
+
app.get('/api/admin/agent-strikes/pending', async (req, res) => {
|
|
314
310
|
const user = requireRootAdmin(req, res);
|
|
315
311
|
if (!user)
|
|
316
312
|
return;
|
|
317
|
-
const rows =
|
|
313
|
+
const rows = await dbAll(`SELECT s.id, s.api_key, s.user_id, u.handle, s.severity, s.reason_code, s.reason_detail, s.issued_at, s.appeal_reason
|
|
318
314
|
FROM agent_strikes s JOIN users u ON u.id = s.user_id
|
|
319
|
-
WHERE s.appeal_status = 'pending' ORDER BY s.id DESC LIMIT 100`)
|
|
315
|
+
WHERE s.appeal_status = 'pending' ORDER BY s.id DESC LIMIT 100`);
|
|
320
316
|
res.json({ items: rows });
|
|
321
317
|
});
|
|
322
318
|
// P1 fix 4.3: admin 主动 issue strike
|
|
323
|
-
app.post('/api/admin/agent-strikes/issue', (req, res) => {
|
|
319
|
+
app.post('/api/admin/agent-strikes/issue', async (req, res) => {
|
|
324
320
|
const adminUser = requireRootAdmin(req, res);
|
|
325
321
|
if (!adminUser)
|
|
326
322
|
return;
|
|
@@ -328,7 +324,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
328
324
|
const apiKey = String(b.api_key || '').trim();
|
|
329
325
|
if (apiKey.length < 8)
|
|
330
326
|
return void res.status(400).json({ error: 'api_key 必填' });
|
|
331
|
-
const targetUser =
|
|
327
|
+
const targetUser = await dbOne(`SELECT id, handle FROM users WHERE api_key = ?`, [apiKey]);
|
|
332
328
|
if (!targetUser)
|
|
333
329
|
return void res.status(404).json({ error: '未找到该 api_key' });
|
|
334
330
|
const reasonCode = String(b.reason_code || '').trim().slice(0, 40);
|
|
@@ -349,7 +345,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
349
345
|
res.json({ ok: true, target_handle: targetUser.handle, ...result });
|
|
350
346
|
});
|
|
351
347
|
// bilateral attestation(用户批准某 agent 的 scope)
|
|
352
|
-
app.post('/api/me/agents/attestations', (req, res) => {
|
|
348
|
+
app.post('/api/me/agents/attestations', async (req, res) => {
|
|
353
349
|
const user = auth(req, res);
|
|
354
350
|
if (!user)
|
|
355
351
|
return;
|
|
@@ -357,7 +353,7 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
357
353
|
const apiKey = String(b.api_key || '');
|
|
358
354
|
if (!apiKey)
|
|
359
355
|
return void res.status(400).json({ error: 'api_key 必填' });
|
|
360
|
-
const decl =
|
|
356
|
+
const decl = await dbOne(`SELECT declared_scope, operator_name, purpose FROM agent_declarations WHERE api_key = ? AND revoked_at IS NULL`, [apiKey]);
|
|
361
357
|
if (!decl)
|
|
362
358
|
return void res.status(404).json({ error: '该 agent 未声明 / 已撤销,无法授权' });
|
|
363
359
|
let approvedScopeJson;
|
|
@@ -373,14 +369,14 @@ export function registerAgentGovernanceRoutes(app, deps) {
|
|
|
373
369
|
const spendCapPerOrder = b.spend_cap_per_order != null ? Math.max(0, Number(b.spend_cap_per_order)) : null;
|
|
374
370
|
const spendCapDaily = b.spend_cap_daily != null ? Math.max(0, Number(b.spend_cap_daily)) : null;
|
|
375
371
|
const id = generateId('aat');
|
|
376
|
-
|
|
372
|
+
await dbRun(`INSERT INTO agent_attestations (id, api_key, user_id, approved_scope, spend_cap_per_order, spend_cap_daily)
|
|
377
373
|
VALUES (?,?,?,?,?,?)
|
|
378
374
|
ON CONFLICT(api_key, user_id) DO UPDATE SET
|
|
379
375
|
approved_scope = excluded.approved_scope,
|
|
380
376
|
spend_cap_per_order = excluded.spend_cap_per_order,
|
|
381
377
|
spend_cap_daily = excluded.spend_cap_daily,
|
|
382
378
|
revoked_at = NULL,
|
|
383
|
-
granted_at = datetime('now')
|
|
379
|
+
granted_at = datetime('now')`, [id, apiKey, user.id, approvedScopeJson, spendCapPerOrder, spendCapDaily]);
|
|
384
380
|
res.json({ ok: true });
|
|
385
381
|
});
|
|
386
382
|
}
|
package/dist/pwa/routes/ai.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerAiRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne),不再直接用 deps.db
|
|
4
|
+
const { auth, anthropic } = deps;
|
|
3
5
|
// G-2: AI 价格建议
|
|
4
6
|
app.post('/api/ai/price-suggestion', async (req, res) => {
|
|
5
7
|
const user = auth(req, res);
|
|
@@ -11,17 +13,17 @@ export function registerAiRoutes(app, deps) {
|
|
|
11
13
|
if (!title)
|
|
12
14
|
return void res.status(400).json({ error: '请提供 title' });
|
|
13
15
|
// 类目历史价位
|
|
14
|
-
const stats =
|
|
16
|
+
const stats = (await dbOne(`
|
|
15
17
|
SELECT COUNT(*) as cnt, COALESCE(AVG(price), 0) as avg, COALESCE(MIN(price), 0) as min, COALESCE(MAX(price), 0) as max,
|
|
16
18
|
COALESCE((SELECT price FROM products WHERE status='active' AND category = ? ORDER BY price LIMIT 1 OFFSET CAST((SELECT COUNT(*) FROM products WHERE status='active' AND category = ?) / 2 AS INTEGER)), 0) as median
|
|
17
19
|
FROM products WHERE status = 'active' AND category = ?
|
|
18
|
-
|
|
20
|
+
`, [category || '', category || '', category || '']));
|
|
19
21
|
// 近 30 天成交均价(更可信)
|
|
20
|
-
const recentAvg =
|
|
22
|
+
const recentAvg = (await dbOne(`
|
|
21
23
|
SELECT COALESCE(AVG(total_amount), 0) as avg FROM orders o
|
|
22
24
|
JOIN products p ON p.id = o.product_id
|
|
23
25
|
WHERE p.category = ? AND o.status = 'completed' AND o.created_at > datetime('now', '-30 days')
|
|
24
|
-
|
|
26
|
+
`, [category || ''])).avg;
|
|
25
27
|
try {
|
|
26
28
|
const message = await anthropic.messages.create({
|
|
27
29
|
model: 'claude-haiku-4-5-20251001',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
function median(arr) {
|
|
2
3
|
if (arr.length === 0)
|
|
3
4
|
return null;
|
|
@@ -6,9 +7,10 @@ function median(arr) {
|
|
|
6
7
|
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
7
8
|
}
|
|
8
9
|
export function registerAnalyticsRoutes(app, deps) {
|
|
9
|
-
|
|
10
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再直接用 deps.db
|
|
11
|
+
const { auth } = deps;
|
|
10
12
|
// 物流绩效卡 (Wave B-4)
|
|
11
|
-
app.get('/api/logistics/me/performance', (req, res) => {
|
|
13
|
+
app.get('/api/logistics/me/performance', async (req, res) => {
|
|
12
14
|
const user = auth(req, res);
|
|
13
15
|
if (!user)
|
|
14
16
|
return;
|
|
@@ -16,22 +18,22 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
16
18
|
return void res.status(403).json({ error: '仅物流角色可访问' });
|
|
17
19
|
}
|
|
18
20
|
const windowDays = Math.max(7, Math.min(365, Number(req.query.window) || 30));
|
|
19
|
-
const orders =
|
|
21
|
+
const orders = await dbAll(`
|
|
20
22
|
SELECT id, status, created_at, updated_at,
|
|
21
23
|
pickup_deadline, delivery_deadline
|
|
22
24
|
FROM orders
|
|
23
25
|
WHERE logistics_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
24
|
-
|
|
26
|
+
`, [user.id, windowDays]);
|
|
25
27
|
const orderIds = orders.map(o => o.id);
|
|
26
28
|
let history = [];
|
|
27
29
|
if (orderIds.length > 0) {
|
|
28
30
|
const placeholders = orderIds.map(() => '?').join(',');
|
|
29
|
-
history =
|
|
31
|
+
history = await dbAll(`
|
|
30
32
|
SELECT order_id, from_status, to_status, created_at
|
|
31
33
|
FROM order_state_history
|
|
32
34
|
WHERE order_id IN (${placeholders})
|
|
33
35
|
ORDER BY created_at ASC
|
|
34
|
-
|
|
36
|
+
`, orderIds);
|
|
35
37
|
}
|
|
36
38
|
const histByOrder = new Map();
|
|
37
39
|
for (const h of history) {
|
|
@@ -78,24 +80,24 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
|
-
const disputes =
|
|
83
|
+
const disputes = (await dbOne(`
|
|
82
84
|
SELECT COUNT(*) as n FROM disputes d
|
|
83
85
|
JOIN orders o ON o.id = d.order_id
|
|
84
86
|
WHERE o.logistics_id = ? AND d.created_at > datetime('now', '-' || ? || ' days')
|
|
85
|
-
|
|
87
|
+
`, [user.id, windowDays])).n;
|
|
86
88
|
// 败诉两路:auto-fault 判物流 + 仲裁裁定物流为被告且退款
|
|
87
|
-
const autoFaultLost =
|
|
89
|
+
const autoFaultLost = (await dbOne(`
|
|
88
90
|
SELECT COUNT(*) as n FROM orders
|
|
89
91
|
WHERE logistics_id = ? AND status = 'fault_logistics'
|
|
90
92
|
AND updated_at > datetime('now', '-' || ? || ' days')
|
|
91
|
-
|
|
92
|
-
const arbitratedLost =
|
|
93
|
+
`, [user.id, windowDays])).n;
|
|
94
|
+
const arbitratedLost = (await dbOne(`
|
|
93
95
|
SELECT COUNT(*) as n FROM disputes d
|
|
94
96
|
JOIN orders o ON o.id = d.order_id
|
|
95
97
|
WHERE o.logistics_id = ? AND d.defendant_id = ?
|
|
96
98
|
AND d.ruling_type IN ('refund_buyer','partial_refund')
|
|
97
99
|
AND d.created_at > datetime('now', '-' || ? || ' days')
|
|
98
|
-
|
|
100
|
+
`, [user.id, user.id, windowDays])).n;
|
|
99
101
|
const disputeLoss = autoFaultLost + arbitratedLost;
|
|
100
102
|
const pickupTotalEvaluated = pickupOnTime + pickupOverdue;
|
|
101
103
|
const deliveryTotalEvaluated = deliveryOnTime + deliveryOverdue;
|
|
@@ -125,14 +127,14 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
125
127
|
});
|
|
126
128
|
});
|
|
127
129
|
// 卖家销售分析 (Wave C-5)
|
|
128
|
-
app.get('/api/sellers/me/analytics', (req, res) => {
|
|
130
|
+
app.get('/api/sellers/me/analytics', async (req, res) => {
|
|
129
131
|
const user = auth(req, res);
|
|
130
132
|
if (!user)
|
|
131
133
|
return;
|
|
132
134
|
if (user.role !== 'seller')
|
|
133
135
|
return void res.status(403).json({ error: '仅卖家可访问' });
|
|
134
136
|
const windowDays = Math.max(7, Math.min(365, Number(req.query.window) || 30));
|
|
135
|
-
const ordersAgg =
|
|
137
|
+
const ordersAgg = (await dbOne(`
|
|
136
138
|
SELECT
|
|
137
139
|
COUNT(*) as total_orders,
|
|
138
140
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_orders,
|
|
@@ -141,8 +143,8 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
141
143
|
COALESCE(SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END), 0) as gmv,
|
|
142
144
|
COALESCE(AVG(CASE WHEN status = 'completed' THEN total_amount END), 0) as aov
|
|
143
145
|
FROM orders WHERE seller_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
144
|
-
|
|
145
|
-
const topProducts =
|
|
146
|
+
`, [user.id, windowDays]));
|
|
147
|
+
const topProducts = await dbAll(`
|
|
146
148
|
SELECT p.id, p.title, p.price, COUNT(o.id) as sales,
|
|
147
149
|
COALESCE(SUM(o.total_amount), 0) as revenue
|
|
148
150
|
FROM products p
|
|
@@ -153,27 +155,27 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
153
155
|
GROUP BY p.id
|
|
154
156
|
HAVING sales > 0
|
|
155
157
|
ORDER BY sales DESC LIMIT 10
|
|
156
|
-
|
|
157
|
-
const buyerStats =
|
|
158
|
+
`, [windowDays, user.id]);
|
|
159
|
+
const buyerStats = (await dbOne(`
|
|
158
160
|
SELECT
|
|
159
161
|
COUNT(DISTINCT buyer_id) as unique_buyers,
|
|
160
162
|
COUNT(*) as orders_count
|
|
161
163
|
FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
162
164
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
163
|
-
|
|
164
|
-
const repeatBuyers =
|
|
165
|
+
`, [user.id, windowDays]));
|
|
166
|
+
const repeatBuyers = (await dbOne(`
|
|
165
167
|
SELECT COUNT(*) as n FROM (
|
|
166
168
|
SELECT buyer_id FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
167
169
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
168
170
|
GROUP BY buyer_id HAVING COUNT(*) > 1
|
|
169
171
|
)
|
|
170
|
-
|
|
171
|
-
const wishlistAdds =
|
|
172
|
+
`, [user.id, windowDays])).n;
|
|
173
|
+
const wishlistAdds = (await dbOne(`
|
|
172
174
|
SELECT COUNT(*) as n FROM user_wishlist w
|
|
173
175
|
JOIN products p ON p.id = w.product_id
|
|
174
176
|
WHERE p.seller_id = ? AND w.created_at > datetime('now', '-' || ? || ' days')
|
|
175
|
-
|
|
176
|
-
const dailyTrend =
|
|
177
|
+
`, [user.id, windowDays])).n;
|
|
178
|
+
const dailyTrend = await dbAll(`
|
|
177
179
|
SELECT DATE(created_at) as date,
|
|
178
180
|
COUNT(*) as orders,
|
|
179
181
|
COALESCE(SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END), 0) as gmv
|
|
@@ -181,30 +183,30 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
181
183
|
WHERE seller_id = ? AND created_at > datetime('now', '-' || ? || ' days')
|
|
182
184
|
GROUP BY DATE(created_at)
|
|
183
185
|
ORDER BY date ASC
|
|
184
|
-
|
|
185
|
-
const ratingsAgg =
|
|
186
|
+
`, [user.id, Math.min(windowDays, 30)]);
|
|
187
|
+
const ratingsAgg = await dbOne(`
|
|
186
188
|
SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars
|
|
187
189
|
FROM order_ratings WHERE seller_id = ?
|
|
188
190
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
189
|
-
|
|
190
|
-
const refundsCount =
|
|
191
|
+
`, [user.id, windowDays]);
|
|
192
|
+
const refundsCount = (await dbOne(`
|
|
191
193
|
SELECT COUNT(*) as n FROM return_requests
|
|
192
194
|
WHERE seller_id = ? AND status = 'refunded'
|
|
193
195
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
194
|
-
|
|
196
|
+
`, [user.id, windowDays])).n;
|
|
195
197
|
// S1: 平均备货时长(paid → shipped 中位 hours)
|
|
196
|
-
const handlingRow =
|
|
198
|
+
const handlingRow = (await dbOne(`
|
|
197
199
|
SELECT COALESCE(AVG((julianday(h_ship.created_at) - julianday(h_paid.created_at)) * 24), 0) as avg_handling_hours,
|
|
198
200
|
COUNT(*) as sample_n
|
|
199
201
|
FROM orders o
|
|
200
202
|
JOIN order_state_history h_paid ON h_paid.order_id = o.id AND h_paid.to_status = 'paid'
|
|
201
203
|
JOIN order_state_history h_ship ON h_ship.order_id = o.id AND h_ship.to_status = 'shipped'
|
|
202
204
|
WHERE o.seller_id = ? AND o.created_at > datetime('now', '-' || ? || ' days')
|
|
203
|
-
|
|
205
|
+
`, [user.id, windowDays]));
|
|
204
206
|
const completedN = Number(ordersAgg.completed_orders) || 0;
|
|
205
207
|
const returnRate = completedN > 0 ? refundsCount / completedN : 0;
|
|
206
208
|
// S1: 上一窗口对比
|
|
207
|
-
const prevAgg =
|
|
209
|
+
const prevAgg = (await dbOne(`
|
|
208
210
|
SELECT
|
|
209
211
|
COUNT(*) as total_orders,
|
|
210
212
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_orders,
|
|
@@ -212,7 +214,7 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
212
214
|
FROM orders WHERE seller_id = ?
|
|
213
215
|
AND created_at > datetime('now', '-' || ? || ' days')
|
|
214
216
|
AND created_at <= datetime('now', '-' || ? || ' days')
|
|
215
|
-
|
|
217
|
+
`, [user.id, windowDays * 2, windowDays]));
|
|
216
218
|
res.json({
|
|
217
219
|
window_days: windowDays,
|
|
218
220
|
orders: ordersAgg,
|
|
@@ -247,19 +249,19 @@ export function registerAnalyticsRoutes(app, deps) {
|
|
|
247
249
|
});
|
|
248
250
|
});
|
|
249
251
|
// 卖家退货仪表盘
|
|
250
|
-
app.get('/api/sellers/me/return-stats', (req, res) => {
|
|
252
|
+
app.get('/api/sellers/me/return-stats', async (req, res) => {
|
|
251
253
|
const user = auth(req, res);
|
|
252
254
|
if (!user)
|
|
253
255
|
return;
|
|
254
|
-
const totalReturns =
|
|
255
|
-
const refunded =
|
|
256
|
-
const rejected =
|
|
257
|
-
const pending =
|
|
258
|
-
const totalOrders =
|
|
259
|
-
const reasonBreakdown =
|
|
256
|
+
const totalReturns = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ?`, [user.id])).n;
|
|
257
|
+
const refunded = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ? AND status = 'refunded'`, [user.id])).n;
|
|
258
|
+
const rejected = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ? AND status = 'rejected'`, [user.id])).n;
|
|
259
|
+
const pending = (await dbOne(`SELECT COUNT(*) as n FROM return_requests WHERE seller_id = ? AND status = 'pending'`, [user.id])).n;
|
|
260
|
+
const totalOrders = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status IN ('delivered','completed','refunded')`, [user.id])).n;
|
|
261
|
+
const reasonBreakdown = await dbAll(`
|
|
260
262
|
SELECT reason, COUNT(*) as cnt FROM return_requests
|
|
261
263
|
WHERE seller_id = ? GROUP BY reason ORDER BY cnt DESC
|
|
262
|
-
|
|
264
|
+
`, [user.id]);
|
|
263
265
|
const returnRate = totalOrders > 0 ? (refunded / totalOrders) : 0;
|
|
264
266
|
res.json({
|
|
265
267
|
total_returns: totalReturns,
|