@seasonkoh/webaz 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +156 -20
- package/dist/layer0-foundation/L0-1-database/schema.js +5 -4
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +228 -7
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +156 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +53 -12
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +14 -1
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +3691 -714
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +324 -0
- package/dist/layer1-agent/L1-2-identity/agent-passport.js +100 -0
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +72 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +287 -0
- package/dist/layer2-business/L2-anchor-registry/anchor-registry.js +396 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +133 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +6 -6
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +246 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +95 -1
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +31 -2
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +358 -0
- package/dist/pwa/public/app.js +31947 -0
- package/dist/pwa/public/i18n.js +5751 -0
- package/dist/pwa/public/icon.svg +11 -0
- package/dist/pwa/public/index.html +21 -0
- package/dist/pwa/public/manifest.json +48 -0
- package/dist/pwa/public/openapi.json +5946 -0
- package/dist/pwa/public/style.css +535 -0
- package/dist/pwa/public/sw.js +63 -0
- package/dist/pwa/public/vendor/jsQR.js +10102 -0
- package/dist/pwa/public/webaz-logo.png +0 -0
- package/dist/pwa/routes/account-deletion.js +53 -0
- package/dist/pwa/routes/addresses.js +105 -0
- package/dist/pwa/routes/admin-admins.js +151 -0
- package/dist/pwa/routes/admin-analytics.js +253 -0
- package/dist/pwa/routes/admin-atomic.js +21 -0
- package/dist/pwa/routes/admin-catalog.js +64 -0
- package/dist/pwa/routes/admin-editor-picks.js +45 -0
- package/dist/pwa/routes/admin-events.js +60 -0
- package/dist/pwa/routes/admin-health.js +66 -0
- package/dist/pwa/routes/admin-moderation.js +120 -0
- package/dist/pwa/routes/admin-ops.js +179 -0
- package/dist/pwa/routes/admin-protocol-params.js +79 -0
- package/dist/pwa/routes/admin-reports.js +154 -0
- package/dist/pwa/routes/admin-tokenomics.js +113 -0
- package/dist/pwa/routes/admin-users-lifecycle.js +237 -0
- package/dist/pwa/routes/admin-users-query.js +390 -0
- package/dist/pwa/routes/admin-verifier-flow.js +126 -0
- package/dist/pwa/routes/admin-verifier-whitelist.js +111 -0
- package/dist/pwa/routes/admin-wallet-ops.js +66 -0
- package/dist/pwa/routes/agent-buy.js +215 -0
- package/dist/pwa/routes/agent-governance.js +341 -0
- package/dist/pwa/routes/agent-reputation.js +34 -0
- package/dist/pwa/routes/ai.js +101 -0
- package/dist/pwa/routes/analytics.js +272 -0
- package/dist/pwa/routes/anchors.js +169 -0
- package/dist/pwa/routes/announcements.js +110 -0
- package/dist/pwa/routes/arbitrator.js +117 -0
- package/dist/pwa/routes/auction.js +436 -0
- package/dist/pwa/routes/auth-login.js +40 -0
- package/dist/pwa/routes/auth-read.js +66 -0
- package/dist/pwa/routes/auth-register.js +138 -0
- package/dist/pwa/routes/auth-sessions.js +62 -0
- package/dist/pwa/routes/blocklist.js +60 -0
- package/dist/pwa/routes/buyer-feeds.js +224 -0
- package/dist/pwa/routes/cart.js +155 -0
- package/dist/pwa/routes/charity.js +816 -0
- package/dist/pwa/routes/chat.js +318 -0
- package/dist/pwa/routes/checkin-tasks.js +122 -0
- package/dist/pwa/routes/checkout-helpers.js +85 -0
- package/dist/pwa/routes/claim-initiators.js +88 -0
- package/dist/pwa/routes/claim-verify.js +615 -0
- package/dist/pwa/routes/claim-voting.js +114 -0
- package/dist/pwa/routes/claim-withdrawals.js +20 -0
- package/dist/pwa/routes/coupons.js +165 -0
- package/dist/pwa/routes/dashboards.js +99 -0
- package/dist/pwa/routes/dispute-cases.js +267 -0
- package/dist/pwa/routes/disputes-read.js +358 -0
- package/dist/pwa/routes/disputes-write.js +475 -0
- package/dist/pwa/routes/evidence.js +86 -0
- package/dist/pwa/routes/external-anchors.js +107 -0
- package/dist/pwa/routes/feedback.js +270 -0
- package/dist/pwa/routes/flash-sales.js +130 -0
- package/dist/pwa/routes/follows.js +103 -0
- package/dist/pwa/routes/group-buys.js +208 -0
- package/dist/pwa/routes/growth.js +199 -0
- package/dist/pwa/routes/import-product.js +153 -0
- package/dist/pwa/routes/kyc.js +40 -0
- package/dist/pwa/routes/leaderboard.js +149 -0
- package/dist/pwa/routes/listings.js +281 -0
- package/dist/pwa/routes/logistics.js +35 -0
- package/dist/pwa/routes/manifests.js +126 -0
- package/dist/pwa/routes/me-data.js +101 -0
- package/dist/pwa/routes/notifications.js +48 -0
- package/dist/pwa/routes/offers.js +96 -0
- package/dist/pwa/routes/orders-action.js +285 -0
- package/dist/pwa/routes/orders-create.js +339 -0
- package/dist/pwa/routes/orders-read.js +180 -0
- package/dist/pwa/routes/p2p-products.js +178 -0
- package/dist/pwa/routes/payments-governance.js +311 -0
- package/dist/pwa/routes/peers.js +34 -0
- package/dist/pwa/routes/pin-receipts.js +39 -0
- package/dist/pwa/routes/products-aliases.js +119 -0
- package/dist/pwa/routes/products-claims.js +60 -0
- package/dist/pwa/routes/products-create.js +206 -0
- package/dist/pwa/routes/products-crud.js +73 -0
- package/dist/pwa/routes/products-links.js +129 -0
- package/dist/pwa/routes/products-list.js +424 -0
- package/dist/pwa/routes/products-meta.js +155 -0
- package/dist/pwa/routes/products-update.js +125 -0
- package/dist/pwa/routes/profile-credentials.js +105 -0
- package/dist/pwa/routes/profile-identity.js +174 -0
- package/dist/pwa/routes/profile-location.js +35 -0
- package/dist/pwa/routes/profile-placement.js +70 -0
- package/dist/pwa/routes/profile-prefs.js +93 -0
- package/dist/pwa/routes/promoter.js +208 -0
- package/dist/pwa/routes/public-utils.js +170 -0
- package/dist/pwa/routes/push.js +54 -0
- package/dist/pwa/routes/ratings.js +220 -0
- package/dist/pwa/routes/recover-key.js +100 -0
- package/dist/pwa/routes/referral.js +58 -0
- package/dist/pwa/routes/reputation.js +34 -0
- package/dist/pwa/routes/returns.js +493 -0
- package/dist/pwa/routes/reviews.js +81 -0
- package/dist/pwa/routes/rfqs.js +443 -0
- package/dist/pwa/routes/search.js +172 -0
- package/dist/pwa/routes/secondhand.js +278 -0
- package/dist/pwa/routes/seller-quota.js +225 -0
- package/dist/pwa/routes/share-redirects.js +164 -0
- package/dist/pwa/routes/shareables-interactions.js +212 -0
- package/dist/pwa/routes/shareables.js +470 -0
- package/dist/pwa/routes/shops.js +98 -0
- package/dist/pwa/routes/signaling.js +43 -0
- package/dist/pwa/routes/skill-market.js +173 -0
- package/dist/pwa/routes/skills.js +174 -0
- package/dist/pwa/routes/snf.js +126 -0
- package/dist/pwa/routes/tags.js +47 -0
- package/dist/pwa/routes/trial.js +333 -0
- package/dist/pwa/routes/trusted-kpi.js +87 -0
- package/dist/pwa/routes/url-claim.js +113 -0
- package/dist/pwa/routes/users-public.js +317 -0
- package/dist/pwa/routes/variants.js +156 -0
- package/dist/pwa/routes/verifier-user.js +107 -0
- package/dist/pwa/routes/verify-tasks.js +120 -0
- package/dist/pwa/routes/waitlist.js +65 -0
- package/dist/pwa/routes/wallet-read.js +218 -0
- package/dist/pwa/routes/wallet-write.js +273 -0
- package/dist/pwa/routes/webauthn.js +188 -0
- package/dist/pwa/routes/webhooks.js +162 -0
- package/dist/pwa/routes/welcome.js +226 -0
- package/dist/pwa/routes/wishlist-qa.js +135 -0
- package/dist/pwa/security/ssrf.js +110 -0
- package/dist/pwa/server.js +9679 -698
- package/package.json +11 -4
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { publishListing, updateListing, delistListing, resubmitListing, listMarket, getMarketDetail, getMyListings, purchaseListing, readContent, getMyLibrary, listPendingAudit, auditListing, } from '../../layer4-economics/L4-4-skill-market/skill-listing-engine.js';
|
|
2
|
+
export function registerSkillMarketRoutes(app, deps) {
|
|
3
|
+
const { db, generateId, auth, getUser, requireContentAdmin, getProtocolParam } = deps;
|
|
4
|
+
const feeRate = () => getProtocolParam('skill_fee_rate', 0.05);
|
|
5
|
+
const notify = (userId, title, body) => {
|
|
6
|
+
try {
|
|
7
|
+
db.prepare('INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)')
|
|
8
|
+
.run(generateId('ntf'), userId, title, body, null);
|
|
9
|
+
}
|
|
10
|
+
catch { /* notifications best-effort */ }
|
|
11
|
+
};
|
|
12
|
+
// ─── 公开列表 ───────────────────────────────────────────────
|
|
13
|
+
app.get('/api/skill-market', (req, res) => {
|
|
14
|
+
const user = getUser(req);
|
|
15
|
+
res.json(listMarket(db, {
|
|
16
|
+
category: req.query.category,
|
|
17
|
+
skillKind: req.query.kind,
|
|
18
|
+
billingMode: req.query.billing,
|
|
19
|
+
query: req.query.q,
|
|
20
|
+
viewerId: user?.id,
|
|
21
|
+
limit: 30,
|
|
22
|
+
}));
|
|
23
|
+
});
|
|
24
|
+
// ─── 我发布的(须在 /:id 之前注册)───────────────────────────
|
|
25
|
+
app.get('/api/skill-market/mine', (req, res) => {
|
|
26
|
+
const user = auth(req, res);
|
|
27
|
+
if (!user)
|
|
28
|
+
return;
|
|
29
|
+
res.json(getMyListings(db, user.id));
|
|
30
|
+
});
|
|
31
|
+
// ─── 我的技能库 ─────────────────────────────────────────────
|
|
32
|
+
app.get('/api/skill-market/library', (req, res) => {
|
|
33
|
+
const user = auth(req, res);
|
|
34
|
+
if (!user)
|
|
35
|
+
return;
|
|
36
|
+
res.json(getMyLibrary(db, user.id));
|
|
37
|
+
});
|
|
38
|
+
// ─── 公开详情 ───────────────────────────────────────────────
|
|
39
|
+
app.get('/api/skill-market/:id', (req, res) => {
|
|
40
|
+
const user = getUser(req);
|
|
41
|
+
const detail = getMarketDetail(db, req.params.id, user?.id);
|
|
42
|
+
if (!detail)
|
|
43
|
+
return void res.status(404).json({ error: '技能不存在或未上架' });
|
|
44
|
+
res.json(detail);
|
|
45
|
+
});
|
|
46
|
+
// ─── 发布(任意登录用户)────────────────────────────────────
|
|
47
|
+
app.post('/api/skill-market', (req, res) => {
|
|
48
|
+
const user = auth(req, res);
|
|
49
|
+
if (!user)
|
|
50
|
+
return;
|
|
51
|
+
const b = req.body;
|
|
52
|
+
try {
|
|
53
|
+
const listing = publishListing(db, {
|
|
54
|
+
authorId: user.id,
|
|
55
|
+
title: String(b.title ?? ''),
|
|
56
|
+
summary: b.summary != null ? String(b.summary) : undefined,
|
|
57
|
+
preview: b.preview != null ? String(b.preview) : undefined,
|
|
58
|
+
content: String(b.content ?? ''),
|
|
59
|
+
category: b.category != null ? String(b.category) : undefined,
|
|
60
|
+
skillKind: b.skill_kind,
|
|
61
|
+
billingMode: b.billing_mode,
|
|
62
|
+
price: b.price != null ? Number(b.price) : 0,
|
|
63
|
+
});
|
|
64
|
+
res.json({ success: true, listing });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
res.status(400).json({ error: err.message });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// ─── 修改 ───────────────────────────────────────────────────
|
|
71
|
+
app.patch('/api/skill-market/:id', (req, res) => {
|
|
72
|
+
const user = auth(req, res);
|
|
73
|
+
if (!user)
|
|
74
|
+
return;
|
|
75
|
+
const b = req.body;
|
|
76
|
+
try {
|
|
77
|
+
const listing = updateListing(db, req.params.id, user.id, {
|
|
78
|
+
title: b.title != null ? String(b.title) : undefined,
|
|
79
|
+
summary: b.summary != null ? String(b.summary) : undefined,
|
|
80
|
+
preview: b.preview != null ? String(b.preview) : undefined,
|
|
81
|
+
content: b.content != null ? String(b.content) : undefined,
|
|
82
|
+
category: b.category != null ? String(b.category) : undefined,
|
|
83
|
+
skillKind: b.skill_kind,
|
|
84
|
+
billingMode: b.billing_mode,
|
|
85
|
+
price: b.price != null ? Number(b.price) : undefined,
|
|
86
|
+
});
|
|
87
|
+
res.json({ success: true, listing });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
res.status(400).json({ error: err.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
// ─── 下架 ───────────────────────────────────────────────────
|
|
94
|
+
app.post('/api/skill-market/:id/delist', (req, res) => {
|
|
95
|
+
const user = auth(req, res);
|
|
96
|
+
if (!user)
|
|
97
|
+
return;
|
|
98
|
+
try {
|
|
99
|
+
delistListing(db, req.params.id, user.id);
|
|
100
|
+
res.json({ success: true });
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
res.status(400).json({ error: err.message });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
// ─── 重新提交审核 ───────────────────────────────────────────
|
|
107
|
+
app.post('/api/skill-market/:id/resubmit', (req, res) => {
|
|
108
|
+
const user = auth(req, res);
|
|
109
|
+
if (!user)
|
|
110
|
+
return;
|
|
111
|
+
try {
|
|
112
|
+
resubmitListing(db, req.params.id, user.id);
|
|
113
|
+
res.json({ success: true });
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
res.status(400).json({ error: err.message });
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// ─── 购买 / 解锁(free | one_time)──────────────────────────
|
|
120
|
+
app.post('/api/skill-market/:id/purchase', (req, res) => {
|
|
121
|
+
const user = auth(req, res);
|
|
122
|
+
if (!user)
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
res.json(purchaseListing(db, user.id, req.params.id, feeRate()));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
res.status(400).json({ error: err.message });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// ─── 读取正文(per_use 按次扣费)────────────────────────────
|
|
132
|
+
app.post('/api/skill-market/:id/read', (req, res) => {
|
|
133
|
+
const user = auth(req, res);
|
|
134
|
+
if (!user)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
res.json(readContent(db, user.id, req.params.id, feeRate()));
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
res.status(400).json({ error: err.message });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
// ─── Admin:待审列表 ────────────────────────────────────────
|
|
144
|
+
app.get('/api/admin/skill-market/pending', (req, res) => {
|
|
145
|
+
const admin = requireContentAdmin(req, res);
|
|
146
|
+
if (!admin)
|
|
147
|
+
return;
|
|
148
|
+
res.json({ items: listPendingAudit(db) });
|
|
149
|
+
});
|
|
150
|
+
// ─── Admin:审核 ────────────────────────────────────────────
|
|
151
|
+
app.post('/api/admin/skill-market/:id/audit', (req, res) => {
|
|
152
|
+
const admin = requireContentAdmin(req, res);
|
|
153
|
+
if (!admin)
|
|
154
|
+
return;
|
|
155
|
+
const { decision, note } = req.body;
|
|
156
|
+
if (decision !== 'approve' && decision !== 'reject') {
|
|
157
|
+
return void res.status(400).json({ error: 'decision 必须是 approve 或 reject' });
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const listing = auditListing(db, req.params.id, admin.id, decision, note);
|
|
161
|
+
if (decision === 'approve') {
|
|
162
|
+
notify(listing.author_id, '✓ 技能审核通过', `「${listing.title}」已上架技能市场`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
notify(listing.author_id, '✗ 技能审核未通过', `「${listing.title}」被退回:${note ?? ''}`);
|
|
166
|
+
}
|
|
167
|
+
res.json({ success: true, listing });
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
res.status(400).json({ error: err.message });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { publishSkill, listSkills, getMySkills, subscribeSkill, unsubscribeSkill, getMySubscriptions, } from '../../layer4-economics/L4-4-skill-market/skill-engine.js';
|
|
2
|
+
const SKILL_TRUST_REQ = {
|
|
3
|
+
price_negotiation: 'quality',
|
|
4
|
+
quality_guarantee: 'quality',
|
|
5
|
+
catalog_sync: 'trusted',
|
|
6
|
+
auto_accept: 'new',
|
|
7
|
+
instant_ship: 'new',
|
|
8
|
+
};
|
|
9
|
+
const LEVEL_ORDER = ['new', 'trusted', 'quality', 'legend'];
|
|
10
|
+
export function registerSkillsRoutes(app, deps) {
|
|
11
|
+
const { db, auth, getUser } = deps;
|
|
12
|
+
// 公开浏览
|
|
13
|
+
app.get('/api/skills', (req, res) => {
|
|
14
|
+
const user = getUser(req);
|
|
15
|
+
const skills = listSkills(db, {
|
|
16
|
+
skillType: req.query.type,
|
|
17
|
+
query: req.query.q,
|
|
18
|
+
subscriberId: user?.id,
|
|
19
|
+
limit: 30,
|
|
20
|
+
});
|
|
21
|
+
res.json(skills);
|
|
22
|
+
});
|
|
23
|
+
app.get('/api/skills/mine', (req, res) => {
|
|
24
|
+
const user = auth(req, res);
|
|
25
|
+
if (!user)
|
|
26
|
+
return;
|
|
27
|
+
res.json(getMySkills(db, user.id));
|
|
28
|
+
});
|
|
29
|
+
app.get('/api/skills/subscriptions', (req, res) => {
|
|
30
|
+
const user = auth(req, res);
|
|
31
|
+
if (!user)
|
|
32
|
+
return;
|
|
33
|
+
res.json(getMySubscriptions(db, user.id));
|
|
34
|
+
});
|
|
35
|
+
// 发布
|
|
36
|
+
app.post('/api/skills', (req, res) => {
|
|
37
|
+
const user = auth(req, res);
|
|
38
|
+
if (!user)
|
|
39
|
+
return;
|
|
40
|
+
if (user.role !== 'seller')
|
|
41
|
+
return void res.json({ error: '只有卖家才能发布 Skill' });
|
|
42
|
+
const { name, description, category, skill_type, config } = req.body;
|
|
43
|
+
if (!name || !description || !skill_type)
|
|
44
|
+
return void res.json({ error: '请填写 name、description、skill_type' });
|
|
45
|
+
// trust level 门槛
|
|
46
|
+
const required = SKILL_TRUST_REQ[skill_type] || 'new';
|
|
47
|
+
if (required !== 'new') {
|
|
48
|
+
const rep = db.prepare(`SELECT level FROM agent_reputation WHERE api_key = ?`).get(user.api_key);
|
|
49
|
+
const myLevel = rep?.level || 'new';
|
|
50
|
+
if (LEVEL_ORDER.indexOf(myLevel) < LEVEL_ORDER.indexOf(required)) {
|
|
51
|
+
return void res.status(403).json({
|
|
52
|
+
error: `发布 ${skill_type} 类型 Skill 需要 ${required}+ 等级(你当前 ${myLevel})`,
|
|
53
|
+
error_code: 'SKILL_TRUST_LEVEL_REQUIRED',
|
|
54
|
+
required, current: myLevel,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// config 边界
|
|
59
|
+
const cfg = (config && typeof config === 'object') ? config : {};
|
|
60
|
+
if (skill_type === 'price_negotiation') {
|
|
61
|
+
const maxDiscount = Number(cfg.max_discount_pct ?? 0);
|
|
62
|
+
if (!Number.isFinite(maxDiscount) || maxDiscount < 0 || maxDiscount > 0.5) {
|
|
63
|
+
return void res.json({ error: 'price_negotiation: max_discount_pct 必须 0-0.5(即 0%-50%)' });
|
|
64
|
+
}
|
|
65
|
+
const minQty = Number(cfg.min_quantity ?? 1);
|
|
66
|
+
if (!Number.isInteger(minQty) || minQty < 1 || minQty > 10000) {
|
|
67
|
+
return void res.json({ error: 'price_negotiation: min_quantity 必须 1-10000 整数' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (skill_type === 'quality_guarantee') {
|
|
71
|
+
const guarantee = Number(cfg.guarantee_amount ?? 0);
|
|
72
|
+
if (!Number.isFinite(guarantee) || guarantee < 0 || guarantee > 100000) {
|
|
73
|
+
return void res.json({ error: 'quality_guarantee: guarantee_amount 必须 0-100000 WAZ' });
|
|
74
|
+
}
|
|
75
|
+
const coverDays = Number(cfg.coverage_days ?? 0);
|
|
76
|
+
if (!Number.isInteger(coverDays) || coverDays < 0 || coverDays > 365) {
|
|
77
|
+
return void res.json({ error: 'quality_guarantee: coverage_days 必须 0-365 整数' });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (skill_type === 'instant_ship') {
|
|
81
|
+
const shipHrs = Number(cfg.ship_within_hours ?? 24);
|
|
82
|
+
if (!Number.isInteger(shipHrs) || shipHrs < 1 || shipHrs > 168) {
|
|
83
|
+
return void res.json({ error: 'instant_ship: ship_within_hours 必须 1-168 整数(最多 7 天)' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (skill_type === 'auto_accept') {
|
|
87
|
+
const maxDaily = Number(cfg.max_daily_orders ?? 0);
|
|
88
|
+
if (cfg.max_daily_orders != null && (!Number.isInteger(maxDaily) || maxDaily < 1 || maxDaily > 10000)) {
|
|
89
|
+
return void res.json({ error: 'auto_accept: max_daily_orders 必须 1-10000 整数' });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const skill = publishSkill(db, {
|
|
94
|
+
sellerId: user.id,
|
|
95
|
+
name, description, category,
|
|
96
|
+
skillType: skill_type,
|
|
97
|
+
config: cfg,
|
|
98
|
+
});
|
|
99
|
+
res.json({ success: true, skill });
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
res.json({ error: err.message });
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// 卖家:修改 Skill
|
|
106
|
+
app.patch('/api/skills/:id', (req, res) => {
|
|
107
|
+
const user = auth(req, res);
|
|
108
|
+
if (!user)
|
|
109
|
+
return;
|
|
110
|
+
const skill = db.prepare('SELECT seller_id FROM skills WHERE id = ?').get(req.params.id);
|
|
111
|
+
if (!skill)
|
|
112
|
+
return void res.status(404).json({ error: 'Skill 不存在' });
|
|
113
|
+
if (skill.seller_id !== user.id)
|
|
114
|
+
return void res.status(403).json({ error: '仅 Skill owner 可修改' });
|
|
115
|
+
const body = req.body;
|
|
116
|
+
const updates = [];
|
|
117
|
+
const args = [];
|
|
118
|
+
if (body.config !== undefined) {
|
|
119
|
+
updates.push('config = ?');
|
|
120
|
+
args.push(JSON.stringify(body.config ?? {}));
|
|
121
|
+
}
|
|
122
|
+
if (body.active !== undefined) {
|
|
123
|
+
updates.push('active = ?');
|
|
124
|
+
args.push(body.active ? 1 : 0);
|
|
125
|
+
}
|
|
126
|
+
if (body.name && typeof body.name === 'string') {
|
|
127
|
+
updates.push('name = ?');
|
|
128
|
+
args.push(body.name);
|
|
129
|
+
}
|
|
130
|
+
if (body.description && typeof body.description === 'string') {
|
|
131
|
+
updates.push('description = ?');
|
|
132
|
+
args.push(body.description);
|
|
133
|
+
}
|
|
134
|
+
if (!updates.length)
|
|
135
|
+
return void res.json({ error: '无任何修改' });
|
|
136
|
+
args.push(req.params.id);
|
|
137
|
+
db.prepare(`UPDATE skills SET ${updates.join(', ')} WHERE id = ?`).run(...args);
|
|
138
|
+
res.json({ success: true });
|
|
139
|
+
});
|
|
140
|
+
// 卖家:停用
|
|
141
|
+
app.post('/api/skills/:id/disable', (req, res) => {
|
|
142
|
+
const user = auth(req, res);
|
|
143
|
+
if (!user)
|
|
144
|
+
return;
|
|
145
|
+
const skill = db.prepare('SELECT seller_id FROM skills WHERE id = ?').get(req.params.id);
|
|
146
|
+
if (!skill)
|
|
147
|
+
return void res.status(404).json({ error: 'Skill 不存在' });
|
|
148
|
+
if (skill.seller_id !== user.id)
|
|
149
|
+
return void res.status(403).json({ error: '仅 Skill owner 可停用' });
|
|
150
|
+
db.prepare("UPDATE skills SET active = 0 WHERE id = ?").run(req.params.id);
|
|
151
|
+
res.json({ success: true });
|
|
152
|
+
});
|
|
153
|
+
// 订阅
|
|
154
|
+
app.post('/api/skills/:id/subscribe', (req, res) => {
|
|
155
|
+
const user = auth(req, res);
|
|
156
|
+
if (!user)
|
|
157
|
+
return;
|
|
158
|
+
try {
|
|
159
|
+
const result = subscribeSkill(db, user.id, req.params.id, req.body?.config ?? {});
|
|
160
|
+
res.json(result);
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
res.json({ error: err.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// 取消订阅
|
|
167
|
+
app.delete('/api/skills/:id/subscribe', (req, res) => {
|
|
168
|
+
const user = auth(req, res);
|
|
169
|
+
if (!user)
|
|
170
|
+
return;
|
|
171
|
+
unsubscribeSkill(db, user.id, req.params.id);
|
|
172
|
+
res.json({ success: true });
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { snfSend, snfPullInbox, snfListInbox, snfAck, snfPendingCount, snfVerify, snfDesignate, snfGetDesignation, snfNack, snfListDeadLetter, snfRevive, } from '../../layer2-business/L2-7-snf/snf-engine.js';
|
|
2
|
+
export function registerSnfRoutes(app, deps) {
|
|
3
|
+
const { db, auth } = deps;
|
|
4
|
+
app.post('/api/snf/send', (req, res) => {
|
|
5
|
+
const user = auth(req, res);
|
|
6
|
+
if (!user)
|
|
7
|
+
return;
|
|
8
|
+
const { recipient_id, message_type, payload, related_order_id, priority } = req.body || {};
|
|
9
|
+
if (!recipient_id || !message_type || !payload)
|
|
10
|
+
return void res.status(400).json({ error: '缺少必要字段' });
|
|
11
|
+
if (typeof payload !== 'object')
|
|
12
|
+
return void res.status(400).json({ error: 'payload 必须是 object' });
|
|
13
|
+
try {
|
|
14
|
+
const r = snfSend(db, {
|
|
15
|
+
senderId: user.id,
|
|
16
|
+
recipientId: String(recipient_id),
|
|
17
|
+
messageType: String(message_type),
|
|
18
|
+
payload: payload,
|
|
19
|
+
relatedOrderId: related_order_id ? String(related_order_id) : null,
|
|
20
|
+
priority: priority === 1 ? 1 : 0,
|
|
21
|
+
});
|
|
22
|
+
res.json({ ok: true, id: r.id, signature: r.signature });
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
res.status(400).json({ error: e.message });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
// 只读列表(不消费)
|
|
29
|
+
app.get('/api/snf/inbox', (req, res) => {
|
|
30
|
+
const user = auth(req, res);
|
|
31
|
+
if (!user)
|
|
32
|
+
return;
|
|
33
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 80));
|
|
34
|
+
const sinceDays = Math.min(180, Math.max(1, Number(req.query.since_days) || 30));
|
|
35
|
+
const msgs = snfListInbox(db, user.id, limit, sinceDays);
|
|
36
|
+
res.json({ items: msgs, count: msgs.length });
|
|
37
|
+
});
|
|
38
|
+
// 协议级 pull — 一次性消费,agent / 内部组件用
|
|
39
|
+
app.get('/api/snf/inbox/pull', (req, res) => {
|
|
40
|
+
const user = auth(req, res);
|
|
41
|
+
if (!user)
|
|
42
|
+
return;
|
|
43
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
|
|
44
|
+
const msgs = snfPullInbox(db, user.id, limit);
|
|
45
|
+
res.json({ items: msgs, count: msgs.length });
|
|
46
|
+
});
|
|
47
|
+
// Agent 处理失败 → nack 回放(超 5 次自动死信化)
|
|
48
|
+
app.post('/api/snf/nack', (req, res) => {
|
|
49
|
+
const user = auth(req, res);
|
|
50
|
+
if (!user)
|
|
51
|
+
return;
|
|
52
|
+
const ids = Array.isArray(req.body?.ids) ? req.body.ids.map(String).slice(0, 100) : [];
|
|
53
|
+
if (ids.length === 0)
|
|
54
|
+
return void res.json({ error: 'ids 为空' });
|
|
55
|
+
const error = req.body?.error ? String(req.body.error) : undefined;
|
|
56
|
+
const r = snfNack(db, user.id, ids, error);
|
|
57
|
+
res.json({ ok: true, reopened: r.reopened, dead_lettered: r.deadLettered });
|
|
58
|
+
});
|
|
59
|
+
app.get('/api/snf/dead-letter', (req, res) => {
|
|
60
|
+
const user = auth(req, res);
|
|
61
|
+
if (!user)
|
|
62
|
+
return;
|
|
63
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
|
|
64
|
+
const items = snfListDeadLetter(db, user.id, limit);
|
|
65
|
+
res.json({ items, count: items.length });
|
|
66
|
+
});
|
|
67
|
+
app.post('/api/snf/revive/:id', (req, res) => {
|
|
68
|
+
const user = auth(req, res);
|
|
69
|
+
if (!user)
|
|
70
|
+
return;
|
|
71
|
+
const r = snfRevive(db, user.id, String(req.params.id));
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
const status = r.reason === 'not_found' ? 404 : r.reason === 'not_owner' ? 403 : 400;
|
|
74
|
+
return void res.status(status).json({ error: r.reason });
|
|
75
|
+
}
|
|
76
|
+
res.json({ ok: true });
|
|
77
|
+
});
|
|
78
|
+
// 显式 ack(无 ids → ack 全部未读)
|
|
79
|
+
app.post('/api/snf/ack', (req, res) => {
|
|
80
|
+
const user = auth(req, res);
|
|
81
|
+
if (!user)
|
|
82
|
+
return;
|
|
83
|
+
const ids = Array.isArray(req.body?.ids) ? req.body.ids.map(String).slice(0, 200) : null;
|
|
84
|
+
if (ids && ids.length > 0) {
|
|
85
|
+
const r = snfAck(db, user.id, ids);
|
|
86
|
+
return void res.json({ ok: true, acked: r.acked });
|
|
87
|
+
}
|
|
88
|
+
const all = snfListInbox(db, user.id, 200, 365).filter(m => !m.delivered_at).map(m => m.id);
|
|
89
|
+
const r = snfAck(db, user.id, all);
|
|
90
|
+
res.json({ ok: true, acked: r.acked });
|
|
91
|
+
});
|
|
92
|
+
app.get('/api/snf/pending', (req, res) => {
|
|
93
|
+
const user = auth(req, res);
|
|
94
|
+
if (!user)
|
|
95
|
+
return;
|
|
96
|
+
res.json({ pending: snfPendingCount(db, user.id) });
|
|
97
|
+
});
|
|
98
|
+
// 验签(仅当事人或 arbitrator/admin)
|
|
99
|
+
app.get('/api/snf/:id/verify', (req, res) => {
|
|
100
|
+
const user = auth(req, res);
|
|
101
|
+
if (!user)
|
|
102
|
+
return;
|
|
103
|
+
const r = db.prepare(`SELECT sender_id, recipient_id FROM snf_messages WHERE id = ?`).get(req.params.id);
|
|
104
|
+
if (!r)
|
|
105
|
+
return void res.status(404).json({ error: '消息不存在' });
|
|
106
|
+
const uid = user.id;
|
|
107
|
+
if (uid !== r.sender_id && uid !== r.recipient_id && user.role !== 'arbitrator' && user.role !== 'admin') {
|
|
108
|
+
return void res.status(403).json({ error: '无权验证' });
|
|
109
|
+
}
|
|
110
|
+
res.json(snfVerify(db, req.params.id));
|
|
111
|
+
});
|
|
112
|
+
app.post('/api/snf/designate', (req, res) => {
|
|
113
|
+
const user = auth(req, res);
|
|
114
|
+
if (!user)
|
|
115
|
+
return;
|
|
116
|
+
const peers = Array.isArray(req.body?.peers) ? req.body.peers.map(String).slice(0, 5) : [];
|
|
117
|
+
snfDesignate(db, user.id, peers);
|
|
118
|
+
res.json({ ok: true, peers });
|
|
119
|
+
});
|
|
120
|
+
app.get('/api/snf/designate', (req, res) => {
|
|
121
|
+
const user = auth(req, res);
|
|
122
|
+
if (!user)
|
|
123
|
+
return;
|
|
124
|
+
res.json({ peers: snfGetDesignation(db, user.id), server_implicit: true });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function registerTagsRoutes(app, deps) {
|
|
2
|
+
const { db } = deps;
|
|
3
|
+
app.get('/api/tags/:tag/notes', (req, res) => {
|
|
4
|
+
const tag = String(req.params.tag || '').trim().toLowerCase();
|
|
5
|
+
if (!tag || tag.length > 30)
|
|
6
|
+
return void res.status(400).json({ error: 'tag invalid' });
|
|
7
|
+
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 30));
|
|
8
|
+
const rows = db.prepare(`
|
|
9
|
+
SELECT s.id, s.owner_id, s.owner_code, s.type, s.title, s.native_text,
|
|
10
|
+
s.related_product_id, s.related_anchor, s.photo_hashes,
|
|
11
|
+
s.click_count, s.like_count, s.created_at,
|
|
12
|
+
p.title AS product_title,
|
|
13
|
+
u.handle as owner_handle, u.name as owner_name,
|
|
14
|
+
t.created_at as tagged_at
|
|
15
|
+
FROM shareable_tags t
|
|
16
|
+
JOIN shareables s ON s.id = t.shareable_id
|
|
17
|
+
LEFT JOIN products p ON p.id = s.related_product_id
|
|
18
|
+
LEFT JOIN users u ON u.id = s.owner_id
|
|
19
|
+
WHERE t.tag = ? AND s.status = 'active'
|
|
20
|
+
ORDER BY s.created_at DESC LIMIT ?
|
|
21
|
+
`).all(tag, limit);
|
|
22
|
+
for (const r of rows) {
|
|
23
|
+
if (typeof r.photo_hashes === 'string') {
|
|
24
|
+
try {
|
|
25
|
+
r.photo_hashes = JSON.parse(r.photo_hashes);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
r.photo_hashes = [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const stat = db.prepare(`SELECT COUNT(*) as count FROM shareable_tags WHERE tag = ?`).get(tag);
|
|
33
|
+
res.json({ tag, count: stat.count, items: rows });
|
|
34
|
+
});
|
|
35
|
+
// 热门标签:24h + 总数综合排序
|
|
36
|
+
app.get('/api/tags/trending', (_req, res) => {
|
|
37
|
+
const rows = db.prepare(`
|
|
38
|
+
SELECT tag, COUNT(*) as total,
|
|
39
|
+
SUM(CASE WHEN created_at > datetime('now', '-1 day') THEN 1 ELSE 0 END) as recent_24h
|
|
40
|
+
FROM shareable_tags
|
|
41
|
+
GROUP BY tag
|
|
42
|
+
HAVING total >= 1
|
|
43
|
+
ORDER BY recent_24h DESC, total DESC LIMIT 20
|
|
44
|
+
`).all();
|
|
45
|
+
res.json({ items: rows });
|
|
46
|
+
});
|
|
47
|
+
}
|