@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
package/dist/pwa/routes/shops.js
CHANGED
|
@@ -1,54 +1,59 @@
|
|
|
1
|
+
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerShopsRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth } = deps;
|
|
5
|
+
app.get('/api/shops/:identifier', async (req, res) => {
|
|
4
6
|
const id = String(req.params.identifier || '').replace(/^@/, '');
|
|
5
7
|
// 先按 handle 查,找不到再按 id
|
|
6
|
-
let seller =
|
|
8
|
+
let seller = await dbOne(`
|
|
7
9
|
SELECT id, name, handle, role, bio, shop_banner_url, shop_intro, created_at, region
|
|
8
10
|
FROM users WHERE handle = ? AND role = 'seller'
|
|
9
|
-
|
|
11
|
+
`, [id]);
|
|
10
12
|
if (!seller) {
|
|
11
|
-
seller =
|
|
13
|
+
seller = await dbOne(`
|
|
12
14
|
SELECT id, name, handle, role, bio, shop_banner_url, shop_intro, created_at, region
|
|
13
15
|
FROM users WHERE id = ? AND role = 'seller'
|
|
14
|
-
|
|
16
|
+
`, [id]);
|
|
15
17
|
}
|
|
16
18
|
if (!seller)
|
|
17
19
|
return void res.status(404).json({ error: '店铺不存在' });
|
|
18
20
|
const sellerId = String(seller.id);
|
|
19
|
-
const products =
|
|
21
|
+
const products = await dbAll(`
|
|
20
22
|
SELECT p.id, p.title, p.price, p.stock, p.category, p.images, p.has_variants, p.commission_rate,
|
|
21
23
|
(SELECT COUNT(1) FROM orders o WHERE o.product_id = p.id AND o.status = 'completed') as sales_count
|
|
22
24
|
FROM products p
|
|
23
25
|
WHERE p.seller_id = ? AND p.status = 'active'
|
|
24
26
|
ORDER BY sales_count DESC, p.created_at DESC
|
|
25
27
|
LIMIT 50
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
28
|
+
`, [sellerId]);
|
|
29
|
+
// 双盲铁律(店铺主页公开面):rating agg + 最近评价只算/只展示已揭晓的评价。
|
|
30
|
+
// 揭晓 = 双方都评过(buyer_ratings 存在) OR 无盲评窗口 OR 盲评期已过 —— 与 /products|sellers/:id/ratings 同条件。
|
|
31
|
+
const blindOpen = `(EXISTS (SELECT 1 FROM buyer_ratings br WHERE br.order_id = r.order_id) OR r.hidden_until IS NULL OR datetime(r.hidden_until) <= datetime('now'))`;
|
|
32
|
+
const ratingsAgg = (await dbOne(`
|
|
33
|
+
SELECT COUNT(*) as cnt, COALESCE(AVG(stars), 0) as avg_stars FROM order_ratings r WHERE r.seller_id = ? AND ${blindOpen}
|
|
34
|
+
`, [sellerId]));
|
|
35
|
+
const followers = (await dbOne(`SELECT COUNT(*) as n FROM follows WHERE followee_id = ?`, [sellerId])).n;
|
|
36
|
+
const completedOrders = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE seller_id = ? AND status = 'completed'`, [sellerId])).n;
|
|
32
37
|
// 当前 viewer 是否关注
|
|
33
38
|
let is_following = false;
|
|
34
39
|
try {
|
|
35
40
|
const token = (req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
36
41
|
if (token) {
|
|
37
|
-
const u =
|
|
42
|
+
const u = await dbOne('SELECT id FROM users WHERE api_key = ?', [token]);
|
|
38
43
|
if (u)
|
|
39
|
-
is_following = !!
|
|
44
|
+
is_following = !!(await dbOne('SELECT 1 FROM follows WHERE follower_id = ? AND followee_id = ?', [u.id, sellerId]));
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
catch { }
|
|
43
|
-
const recentRatings =
|
|
48
|
+
const recentRatings = await dbAll(`
|
|
44
49
|
SELECT r.stars, r.comment, r.reply, r.created_at,
|
|
45
50
|
u.handle as buyer_handle, p.title as product_title
|
|
46
51
|
FROM order_ratings r
|
|
47
52
|
JOIN users u ON u.id = r.buyer_id
|
|
48
53
|
JOIN products p ON p.id = r.product_id
|
|
49
|
-
WHERE r.seller_id = ?
|
|
54
|
+
WHERE r.seller_id = ? AND ${blindOpen}
|
|
50
55
|
ORDER BY r.created_at DESC LIMIT 5
|
|
51
|
-
|
|
56
|
+
`, [sellerId]);
|
|
52
57
|
res.json({
|
|
53
58
|
seller,
|
|
54
59
|
stats: {
|
|
@@ -64,7 +69,7 @@ export function registerShopsRoutes(app, deps) {
|
|
|
64
69
|
});
|
|
65
70
|
});
|
|
66
71
|
// 卖家更新自己店铺装饰
|
|
67
|
-
app.patch('/api/shops/me', (req, res) => {
|
|
72
|
+
app.patch('/api/shops/me', async (req, res) => {
|
|
68
73
|
const user = auth(req, res);
|
|
69
74
|
if (!user)
|
|
70
75
|
return;
|
|
@@ -92,7 +97,7 @@ export function registerShopsRoutes(app, deps) {
|
|
|
92
97
|
return void res.status(400).json({ error: '无可更新字段' });
|
|
93
98
|
sets.push(`updated_at = datetime('now')`);
|
|
94
99
|
args.push(user.id);
|
|
95
|
-
|
|
100
|
+
await dbRun(`UPDATE users SET ${sets.join(', ')} WHERE id = ?`, args);
|
|
96
101
|
res.json({ success: true });
|
|
97
102
|
});
|
|
98
103
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
export function registerSignalingRoutes(app, deps) {
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
// db 已走 RFC-016 异步 seam(dbAll/dbRun),不再直接用 deps.db
|
|
4
|
+
const { auth, generateId } = deps;
|
|
5
|
+
app.post('/api/signaling/send', async (req, res) => {
|
|
4
6
|
const me = auth(req, res);
|
|
5
7
|
if (!me)
|
|
6
8
|
return;
|
|
@@ -11,23 +13,22 @@ export function registerSignalingRoutes(app, deps) {
|
|
|
11
13
|
return void res.json({ error: 'type 不合法' });
|
|
12
14
|
if (JSON.stringify(data).length > 50000)
|
|
13
15
|
return void res.json({ error: 'data 过大' });
|
|
14
|
-
|
|
15
|
-
VALUES (?,?,?,?,?,datetime('now'))
|
|
16
|
-
.run(generateId('sig'), to, me.id, type, JSON.stringify(data));
|
|
16
|
+
await dbRun(`INSERT INTO signaling_queue (id, to_peer_id, from_peer_id, signal_type, signal_data, created_at)
|
|
17
|
+
VALUES (?,?,?,?,?,datetime('now'))`, [generateId('sig'), to, me.id, type, JSON.stringify(data)]);
|
|
17
18
|
res.json({ ok: true });
|
|
18
19
|
});
|
|
19
|
-
app.get('/api/signaling/poll', (req, res) => {
|
|
20
|
+
app.get('/api/signaling/poll', async (req, res) => {
|
|
20
21
|
const me = auth(req, res);
|
|
21
22
|
if (!me)
|
|
22
23
|
return;
|
|
23
|
-
const rows =
|
|
24
|
+
const rows = await dbAll(`
|
|
24
25
|
SELECT id, from_peer_id, signal_type, signal_data, created_at FROM signaling_queue
|
|
25
26
|
WHERE to_peer_id = ? AND delivered_at IS NULL AND created_at > datetime('now', '-2 minutes')
|
|
26
27
|
ORDER BY created_at ASC LIMIT 50
|
|
27
|
-
|
|
28
|
+
`, [me.id]);
|
|
28
29
|
if (rows.length > 0) {
|
|
29
30
|
const ids = rows.map(r => r.id);
|
|
30
|
-
|
|
31
|
+
await dbRun(`UPDATE signaling_queue SET delivered_at = datetime('now') WHERE id IN (${ids.map(() => '?').join(',')})`, ids);
|
|
31
32
|
}
|
|
32
33
|
res.json({ signals: rows.map(r => {
|
|
33
34
|
let signal_data = null;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import { dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
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
3
|
export function registerSkillMarketRoutes(app, deps) {
|
|
3
4
|
const { db, generateId, auth, getUser, requireContentAdmin, getProtocolParam } = deps;
|
|
4
5
|
const feeRate = () => getProtocolParam('skill_fee_rate', 0.05);
|
|
5
|
-
const notify = (userId, title, body) => {
|
|
6
|
+
const notify = async (userId, title, body) => {
|
|
6
7
|
try {
|
|
7
|
-
|
|
8
|
-
.run(generateId('ntf'), userId, title, body, null);
|
|
8
|
+
await dbRun('INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)', [generateId('ntf'), userId, title, body, null]);
|
|
9
9
|
}
|
|
10
10
|
catch { /* notifications best-effort */ }
|
|
11
11
|
};
|
|
12
12
|
// ─── 公开列表 ───────────────────────────────────────────────
|
|
13
|
-
app.get('/api/skill-market', (req, res) => {
|
|
13
|
+
app.get('/api/skill-market', async (req, res) => {
|
|
14
14
|
const user = getUser(req);
|
|
15
|
-
res.json(listMarket(db, {
|
|
15
|
+
res.json(await listMarket(db, {
|
|
16
16
|
category: req.query.category,
|
|
17
17
|
skillKind: req.query.kind,
|
|
18
18
|
billingMode: req.query.billing,
|
|
@@ -22,23 +22,23 @@ export function registerSkillMarketRoutes(app, deps) {
|
|
|
22
22
|
}));
|
|
23
23
|
});
|
|
24
24
|
// ─── 我发布的(须在 /:id 之前注册)───────────────────────────
|
|
25
|
-
app.get('/api/skill-market/mine', (req, res) => {
|
|
25
|
+
app.get('/api/skill-market/mine', async (req, res) => {
|
|
26
26
|
const user = auth(req, res);
|
|
27
27
|
if (!user)
|
|
28
28
|
return;
|
|
29
|
-
res.json(getMyListings(db, user.id));
|
|
29
|
+
res.json(await getMyListings(db, user.id));
|
|
30
30
|
});
|
|
31
31
|
// ─── 我的技能库 ─────────────────────────────────────────────
|
|
32
|
-
app.get('/api/skill-market/library', (req, res) => {
|
|
32
|
+
app.get('/api/skill-market/library', async (req, res) => {
|
|
33
33
|
const user = auth(req, res);
|
|
34
34
|
if (!user)
|
|
35
35
|
return;
|
|
36
|
-
res.json(getMyLibrary(db, user.id));
|
|
36
|
+
res.json(await getMyLibrary(db, user.id));
|
|
37
37
|
});
|
|
38
38
|
// ─── 公开详情 ───────────────────────────────────────────────
|
|
39
|
-
app.get('/api/skill-market/:id', (req, res) => {
|
|
39
|
+
app.get('/api/skill-market/:id', async (req, res) => {
|
|
40
40
|
const user = getUser(req);
|
|
41
|
-
const detail = getMarketDetail(db, req.params.id, user?.id);
|
|
41
|
+
const detail = await getMarketDetail(db, req.params.id, user?.id);
|
|
42
42
|
if (!detail)
|
|
43
43
|
return void res.status(404).json({ error: '技能不存在或未上架' });
|
|
44
44
|
res.json(detail);
|
|
@@ -141,14 +141,14 @@ export function registerSkillMarketRoutes(app, deps) {
|
|
|
141
141
|
}
|
|
142
142
|
});
|
|
143
143
|
// ─── Admin:待审列表 ────────────────────────────────────────
|
|
144
|
-
app.get('/api/admin/skill-market/pending', (req, res) => {
|
|
144
|
+
app.get('/api/admin/skill-market/pending', async (req, res) => {
|
|
145
145
|
const admin = requireContentAdmin(req, res);
|
|
146
146
|
if (!admin)
|
|
147
147
|
return;
|
|
148
|
-
res.json({ items: listPendingAudit(db) });
|
|
148
|
+
res.json({ items: await listPendingAudit(db) });
|
|
149
149
|
});
|
|
150
150
|
// ─── Admin:审核 ────────────────────────────────────────────
|
|
151
|
-
app.post('/api/admin/skill-market/:id/audit', (req, res) => {
|
|
151
|
+
app.post('/api/admin/skill-market/:id/audit', async (req, res) => {
|
|
152
152
|
const admin = requireContentAdmin(req, res);
|
|
153
153
|
if (!admin)
|
|
154
154
|
return;
|
|
@@ -159,10 +159,10 @@ export function registerSkillMarketRoutes(app, deps) {
|
|
|
159
159
|
try {
|
|
160
160
|
const listing = auditListing(db, req.params.id, admin.id, decision, note);
|
|
161
161
|
if (decision === 'approve') {
|
|
162
|
-
notify(listing.author_id, '✓ 技能审核通过', `「${listing.title}」已上架技能市场`);
|
|
162
|
+
await notify(listing.author_id, '✓ 技能审核通过', `「${listing.title}」已上架技能市场`);
|
|
163
163
|
}
|
|
164
164
|
else {
|
|
165
|
-
notify(listing.author_id, '✗ 技能审核未通过', `「${listing.title}」被退回:${note ?? ''}`);
|
|
165
|
+
await notify(listing.author_id, '✗ 技能审核未通过', `「${listing.title}」被退回:${note ?? ''}`);
|
|
166
166
|
}
|
|
167
167
|
res.json({ success: true, listing });
|
|
168
168
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { publishSkill, listSkills, getMySkills, subscribeSkill, unsubscribeSkill, getMySubscriptions, } from '../../layer4-economics/L4-4-skill-market/skill-engine.js';
|
|
2
|
+
import { dbOne, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
3
|
const SKILL_TRUST_REQ = {
|
|
3
4
|
price_negotiation: 'quality',
|
|
4
5
|
quality_guarantee: 'quality',
|
|
@@ -10,9 +11,9 @@ const LEVEL_ORDER = ['new', 'trusted', 'quality', 'legend'];
|
|
|
10
11
|
export function registerSkillsRoutes(app, deps) {
|
|
11
12
|
const { db, auth, getUser } = deps;
|
|
12
13
|
// 公开浏览
|
|
13
|
-
app.get('/api/skills', (req, res) => {
|
|
14
|
+
app.get('/api/skills', async (req, res) => {
|
|
14
15
|
const user = getUser(req);
|
|
15
|
-
const skills = listSkills(db, {
|
|
16
|
+
const skills = await listSkills(db, {
|
|
16
17
|
skillType: req.query.type,
|
|
17
18
|
query: req.query.q,
|
|
18
19
|
subscriberId: user?.id,
|
|
@@ -20,20 +21,20 @@ export function registerSkillsRoutes(app, deps) {
|
|
|
20
21
|
});
|
|
21
22
|
res.json(skills);
|
|
22
23
|
});
|
|
23
|
-
app.get('/api/skills/mine', (req, res) => {
|
|
24
|
+
app.get('/api/skills/mine', async (req, res) => {
|
|
24
25
|
const user = auth(req, res);
|
|
25
26
|
if (!user)
|
|
26
27
|
return;
|
|
27
|
-
res.json(getMySkills(db, user.id));
|
|
28
|
+
res.json(await getMySkills(db, user.id));
|
|
28
29
|
});
|
|
29
|
-
app.get('/api/skills/subscriptions', (req, res) => {
|
|
30
|
+
app.get('/api/skills/subscriptions', async (req, res) => {
|
|
30
31
|
const user = auth(req, res);
|
|
31
32
|
if (!user)
|
|
32
33
|
return;
|
|
33
|
-
res.json(getMySubscriptions(db, user.id));
|
|
34
|
+
res.json(await getMySubscriptions(db, user.id));
|
|
34
35
|
});
|
|
35
36
|
// 发布
|
|
36
|
-
app.post('/api/skills', (req, res) => {
|
|
37
|
+
app.post('/api/skills', async (req, res) => {
|
|
37
38
|
const user = auth(req, res);
|
|
38
39
|
if (!user)
|
|
39
40
|
return;
|
|
@@ -45,7 +46,7 @@ export function registerSkillsRoutes(app, deps) {
|
|
|
45
46
|
// trust level 门槛
|
|
46
47
|
const required = SKILL_TRUST_REQ[skill_type] || 'new';
|
|
47
48
|
if (required !== 'new') {
|
|
48
|
-
const rep =
|
|
49
|
+
const rep = await dbOne(`SELECT level FROM agent_reputation WHERE api_key = ?`, [user.api_key]);
|
|
49
50
|
const myLevel = rep?.level || 'new';
|
|
50
51
|
if (LEVEL_ORDER.indexOf(myLevel) < LEVEL_ORDER.indexOf(required)) {
|
|
51
52
|
return void res.status(403).json({
|
|
@@ -103,11 +104,11 @@ export function registerSkillsRoutes(app, deps) {
|
|
|
103
104
|
}
|
|
104
105
|
});
|
|
105
106
|
// 卖家:修改 Skill
|
|
106
|
-
app.patch('/api/skills/:id', (req, res) => {
|
|
107
|
+
app.patch('/api/skills/:id', async (req, res) => {
|
|
107
108
|
const user = auth(req, res);
|
|
108
109
|
if (!user)
|
|
109
110
|
return;
|
|
110
|
-
const skill =
|
|
111
|
+
const skill = await dbOne('SELECT seller_id FROM skills WHERE id = ?', [req.params.id]);
|
|
111
112
|
if (!skill)
|
|
112
113
|
return void res.status(404).json({ error: 'Skill 不存在' });
|
|
113
114
|
if (skill.seller_id !== user.id)
|
|
@@ -134,20 +135,20 @@ export function registerSkillsRoutes(app, deps) {
|
|
|
134
135
|
if (!updates.length)
|
|
135
136
|
return void res.json({ error: '无任何修改' });
|
|
136
137
|
args.push(req.params.id);
|
|
137
|
-
|
|
138
|
+
await dbRun(`UPDATE skills SET ${updates.join(', ')} WHERE id = ?`, args);
|
|
138
139
|
res.json({ success: true });
|
|
139
140
|
});
|
|
140
141
|
// 卖家:停用
|
|
141
|
-
app.post('/api/skills/:id/disable', (req, res) => {
|
|
142
|
+
app.post('/api/skills/:id/disable', async (req, res) => {
|
|
142
143
|
const user = auth(req, res);
|
|
143
144
|
if (!user)
|
|
144
145
|
return;
|
|
145
|
-
const skill =
|
|
146
|
+
const skill = await dbOne('SELECT seller_id FROM skills WHERE id = ?', [req.params.id]);
|
|
146
147
|
if (!skill)
|
|
147
148
|
return void res.status(404).json({ error: 'Skill 不存在' });
|
|
148
149
|
if (skill.seller_id !== user.id)
|
|
149
150
|
return void res.status(403).json({ error: '仅 Skill owner 可停用' });
|
|
150
|
-
|
|
151
|
+
await dbRun("UPDATE skills SET active = 0 WHERE id = ?", [req.params.id]);
|
|
151
152
|
res.json({ success: true });
|
|
152
153
|
});
|
|
153
154
|
// 订阅
|
package/dist/pwa/routes/snf.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
1
2
|
import { snfSend, snfPullInbox, snfListInbox, snfAck, snfPendingCount, snfVerify, snfDesignate, snfGetDesignation, snfNack, snfListDeadLetter, snfRevive, } from '../../layer2-business/L2-7-snf/snf-engine.js';
|
|
2
3
|
export function registerSnfRoutes(app, deps) {
|
|
3
4
|
const { db, auth } = deps;
|
|
@@ -26,13 +27,13 @@ export function registerSnfRoutes(app, deps) {
|
|
|
26
27
|
}
|
|
27
28
|
});
|
|
28
29
|
// 只读列表(不消费)
|
|
29
|
-
app.get('/api/snf/inbox', (req, res) => {
|
|
30
|
+
app.get('/api/snf/inbox', async (req, res) => {
|
|
30
31
|
const user = auth(req, res);
|
|
31
32
|
if (!user)
|
|
32
33
|
return;
|
|
33
34
|
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 80));
|
|
34
35
|
const sinceDays = Math.min(180, Math.max(1, Number(req.query.since_days) || 30));
|
|
35
|
-
const msgs = snfListInbox(db, user.id, limit, sinceDays);
|
|
36
|
+
const msgs = await snfListInbox(db, user.id, limit, sinceDays);
|
|
36
37
|
res.json({ items: msgs, count: msgs.length });
|
|
37
38
|
});
|
|
38
39
|
// 协议级 pull — 一次性消费,agent / 内部组件用
|
|
@@ -56,12 +57,12 @@ export function registerSnfRoutes(app, deps) {
|
|
|
56
57
|
const r = snfNack(db, user.id, ids, error);
|
|
57
58
|
res.json({ ok: true, reopened: r.reopened, dead_lettered: r.deadLettered });
|
|
58
59
|
});
|
|
59
|
-
app.get('/api/snf/dead-letter', (req, res) => {
|
|
60
|
+
app.get('/api/snf/dead-letter', async (req, res) => {
|
|
60
61
|
const user = auth(req, res);
|
|
61
62
|
if (!user)
|
|
62
63
|
return;
|
|
63
64
|
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 50));
|
|
64
|
-
const items = snfListDeadLetter(db, user.id, limit);
|
|
65
|
+
const items = await snfListDeadLetter(db, user.id, limit);
|
|
65
66
|
res.json({ items, count: items.length });
|
|
66
67
|
});
|
|
67
68
|
app.post('/api/snf/revive/:id', (req, res) => {
|
|
@@ -76,7 +77,7 @@ export function registerSnfRoutes(app, deps) {
|
|
|
76
77
|
res.json({ ok: true });
|
|
77
78
|
});
|
|
78
79
|
// 显式 ack(无 ids → ack 全部未读)
|
|
79
|
-
app.post('/api/snf/ack', (req, res) => {
|
|
80
|
+
app.post('/api/snf/ack', async (req, res) => {
|
|
80
81
|
const user = auth(req, res);
|
|
81
82
|
if (!user)
|
|
82
83
|
return;
|
|
@@ -85,29 +86,29 @@ export function registerSnfRoutes(app, deps) {
|
|
|
85
86
|
const r = snfAck(db, user.id, ids);
|
|
86
87
|
return void res.json({ ok: true, acked: r.acked });
|
|
87
88
|
}
|
|
88
|
-
const all = snfListInbox(db, user.id, 200, 365).filter(m => !m.delivered_at).map(m => m.id);
|
|
89
|
+
const all = (await snfListInbox(db, user.id, 200, 365)).filter(m => !m.delivered_at).map(m => m.id);
|
|
89
90
|
const r = snfAck(db, user.id, all);
|
|
90
91
|
res.json({ ok: true, acked: r.acked });
|
|
91
92
|
});
|
|
92
|
-
app.get('/api/snf/pending', (req, res) => {
|
|
93
|
+
app.get('/api/snf/pending', async (req, res) => {
|
|
93
94
|
const user = auth(req, res);
|
|
94
95
|
if (!user)
|
|
95
96
|
return;
|
|
96
|
-
res.json({ pending: snfPendingCount(db, user.id) });
|
|
97
|
+
res.json({ pending: await snfPendingCount(db, user.id) });
|
|
97
98
|
});
|
|
98
99
|
// 验签(仅当事人或 arbitrator/admin)
|
|
99
|
-
app.get('/api/snf/:id/verify', (req, res) => {
|
|
100
|
+
app.get('/api/snf/:id/verify', async (req, res) => {
|
|
100
101
|
const user = auth(req, res);
|
|
101
102
|
if (!user)
|
|
102
103
|
return;
|
|
103
|
-
const r =
|
|
104
|
+
const r = await dbOne(`SELECT sender_id, recipient_id FROM snf_messages WHERE id = ?`, [req.params.id]);
|
|
104
105
|
if (!r)
|
|
105
106
|
return void res.status(404).json({ error: '消息不存在' });
|
|
106
107
|
const uid = user.id;
|
|
107
108
|
if (uid !== r.sender_id && uid !== r.recipient_id && user.role !== 'arbitrator' && user.role !== 'admin') {
|
|
108
109
|
return void res.status(403).json({ error: '无权验证' });
|
|
109
110
|
}
|
|
110
|
-
res.json(snfVerify(db, req.params.id));
|
|
111
|
+
res.json(await snfVerify(db, req.params.id));
|
|
111
112
|
});
|
|
112
113
|
app.post('/api/snf/designate', (req, res) => {
|
|
113
114
|
const user = auth(req, res);
|
|
@@ -117,10 +118,10 @@ export function registerSnfRoutes(app, deps) {
|
|
|
117
118
|
snfDesignate(db, user.id, peers);
|
|
118
119
|
res.json({ ok: true, peers });
|
|
119
120
|
});
|
|
120
|
-
app.get('/api/snf/designate', (req, res) => {
|
|
121
|
+
app.get('/api/snf/designate', async (req, res) => {
|
|
121
122
|
const user = auth(req, res);
|
|
122
123
|
if (!user)
|
|
123
124
|
return;
|
|
124
|
-
res.json({ peers: snfGetDesignation(db, user.id), server_implicit: true });
|
|
125
|
+
res.json({ peers: await snfGetDesignation(db, user.id), server_implicit: true });
|
|
125
126
|
});
|
|
126
127
|
}
|
package/dist/pwa/routes/tags.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
export function registerTagsRoutes(app, _deps) {
|
|
3
|
+
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll),不再用 deps.db
|
|
4
|
+
app.get('/api/tags/:tag/notes', async (req, res) => {
|
|
4
5
|
const tag = String(req.params.tag || '').trim().toLowerCase();
|
|
5
6
|
if (!tag || tag.length > 30)
|
|
6
7
|
return void res.status(400).json({ error: 'tag invalid' });
|
|
7
8
|
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 30));
|
|
8
|
-
const rows =
|
|
9
|
+
const rows = await dbAll(`
|
|
9
10
|
SELECT s.id, s.owner_id, s.owner_code, s.type, s.title, s.native_text,
|
|
10
11
|
s.related_product_id, s.related_anchor, s.photo_hashes,
|
|
11
12
|
s.click_count, s.like_count, s.created_at,
|
|
@@ -18,7 +19,7 @@ export function registerTagsRoutes(app, deps) {
|
|
|
18
19
|
LEFT JOIN users u ON u.id = s.owner_id
|
|
19
20
|
WHERE t.tag = ? AND s.status = 'active'
|
|
20
21
|
ORDER BY s.created_at DESC LIMIT ?
|
|
21
|
-
|
|
22
|
+
`, [tag, limit]);
|
|
22
23
|
for (const r of rows) {
|
|
23
24
|
if (typeof r.photo_hashes === 'string') {
|
|
24
25
|
try {
|
|
@@ -29,19 +30,19 @@ export function registerTagsRoutes(app, deps) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
|
-
const stat =
|
|
33
|
+
const stat = (await dbOne(`SELECT COUNT(*) as count FROM shareable_tags WHERE tag = ?`, [tag]));
|
|
33
34
|
res.json({ tag, count: stat.count, items: rows });
|
|
34
35
|
});
|
|
35
36
|
// 热门标签:24h + 总数综合排序
|
|
36
|
-
app.get('/api/tags/trending', (_req, res) => {
|
|
37
|
-
const rows =
|
|
37
|
+
app.get('/api/tags/trending', async (_req, res) => {
|
|
38
|
+
const rows = await dbAll(`
|
|
38
39
|
SELECT tag, COUNT(*) as total,
|
|
39
40
|
SUM(CASE WHEN created_at > datetime('now', '-1 day') THEN 1 ELSE 0 END) as recent_24h
|
|
40
41
|
FROM shareable_tags
|
|
41
42
|
GROUP BY tag
|
|
42
43
|
HAVING total >= 1
|
|
43
44
|
ORDER BY recent_24h DESC, total DESC LIMIT 20
|
|
44
|
-
`)
|
|
45
|
+
`);
|
|
45
46
|
res.json({ items: rows });
|
|
46
47
|
});
|
|
47
48
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { validateProposalInput, insertTaskProposal, listTaskProposals, reviewTaskProposal } from '../../layer2-business/L2-9-contribution/task-proposal-store.js';
|
|
2
|
+
import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
|
|
3
|
+
import { getCanonicalContributionTarget } from '../../layer2-business/L2-9-contribution/canonical-contribution-target.js';
|
|
4
|
+
import { createDraftFromProposal, listDraftBuildTasks, publishDraftBuildTask } from '../../layer2-business/L2-9-contribution/task-proposal-draft.js';
|
|
5
|
+
import { recommendForProposal, insertAiSuggestion, listAiSuggestions, getProposalLite } from '../../layer2-business/L2-9-contribution/task-proposal-ai-store.js';
|
|
6
|
+
const AI_NOTICE = 'AI suggestion — assistant only, NOT a decision. A human maintainer must explicitly create / publish / reject the formal task. AI never auto-publishes, auto-rejects, hides proposals, or assigns reward / credit.';
|
|
7
|
+
const PROPOSAL_NOTICE = 'A task proposal is a SUGGESTION in the maintainer review inbox. It is NOT a contribution fact, formal participation, or any reward / payout / score, and it never appears on the public task board until a maintainer reviews and (manually) converts it. source_ref is a reference only; the canonical contribution target is fixed by trusted config.';
|
|
8
|
+
function withProposalEnvelope(payload) {
|
|
9
|
+
return withUncommittedValueBoundary({ ...payload, proposal_notice: PROPOSAL_NOTICE, canonical_contribution_target: getCanonicalContributionTarget() });
|
|
10
|
+
}
|
|
11
|
+
export function registerTaskProposalsRoutes(app, deps) {
|
|
12
|
+
const { db, errorRes, requireSupportAdmin, rateLimitOk } = deps;
|
|
13
|
+
// public submit — anonymous; proposer_account_id is never taken from the body (anti-spoof).
|
|
14
|
+
app.post('/api/public/task-proposals', (req, res) => {
|
|
15
|
+
// anti-flood: per-IP rate limit (counts every attempt, before validation) then a recent-window dedup.
|
|
16
|
+
const ip = (typeof req.headers['x-forwarded-for'] === 'string' ? req.headers['x-forwarded-for'].split(',')[0].trim() : '') || req.ip || 'unknown';
|
|
17
|
+
if (!rateLimitOk(`proposal:${ip}`))
|
|
18
|
+
return void errorRes(res, 429, 'RATE_LIMITED', '提交过于频繁,请稍后再试');
|
|
19
|
+
const v = validateProposalInput(req.body);
|
|
20
|
+
if (!v.ok)
|
|
21
|
+
return void errorRes(res, 400, v.code, v.message);
|
|
22
|
+
const result = insertTaskProposal(db, v.input, null);
|
|
23
|
+
if ('duplicate' in result)
|
|
24
|
+
return void errorRes(res, 409, 'DUPLICATE_PROPOSAL', '相同建议已在收件箱中,请勿重复提交', { existing_id: result.existing_id });
|
|
25
|
+
res.json(withProposalEnvelope({ proposal: { id: result.id, status: result.status } }));
|
|
26
|
+
});
|
|
27
|
+
// admin list (maintainer only)
|
|
28
|
+
app.get('/api/admin/task-proposals', (req, res) => {
|
|
29
|
+
const admin = requireSupportAdmin(req, res);
|
|
30
|
+
if (!admin)
|
|
31
|
+
return;
|
|
32
|
+
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
|
|
33
|
+
res.json(withProposalEnvelope({ proposals: listTaskProposals(db, { status }) }));
|
|
34
|
+
});
|
|
35
|
+
// admin review (maintainer only): needs_info | rejected | converted — no build_task is created here.
|
|
36
|
+
app.post('/api/admin/task-proposals/:id/review', (req, res) => {
|
|
37
|
+
const admin = requireSupportAdmin(req, res);
|
|
38
|
+
if (!admin)
|
|
39
|
+
return;
|
|
40
|
+
const { status, note, converted_ref } = req.body ?? {};
|
|
41
|
+
const result = reviewTaskProposal(db, String(req.params.id), admin.id, String(status), note, converted_ref);
|
|
42
|
+
if ('error' in result) {
|
|
43
|
+
const code = result.code === 'NOT_FOUND' ? 404 : result.code === 'ALREADY_TERMINAL' ? 409 : 400;
|
|
44
|
+
return void errorRes(res, code, result.code, result.error);
|
|
45
|
+
}
|
|
46
|
+
res.json(withProposalEnvelope({ proposal: result }));
|
|
47
|
+
});
|
|
48
|
+
// ── AI-assist (ASSISTANT ONLY) ─────────────────────────────────────────────
|
|
49
|
+
// Classify + suggest draft fields; stored as recommendation/evidence with accountability metadata.
|
|
50
|
+
// NEVER a decision: no auto-publish / auto-reject / hide / reward. A human admin must act explicitly.
|
|
51
|
+
app.post('/api/admin/task-proposals/:id/ai-assist', (req, res) => {
|
|
52
|
+
const admin = requireSupportAdmin(req, res);
|
|
53
|
+
if (!admin)
|
|
54
|
+
return;
|
|
55
|
+
const p = getProposalLite(db, String(req.params.id));
|
|
56
|
+
if (!p)
|
|
57
|
+
return void errorRes(res, 404, 'NOT_FOUND', 'proposal not found');
|
|
58
|
+
const { recommendation, model, provider } = recommendForProposal(db, p);
|
|
59
|
+
const stored = insertAiSuggestion(db, { proposalId: p.id, reviewerType: 'ai', model, provider,
|
|
60
|
+
inputSummary: `${p.title}\n${p.summary}`, outputJson: JSON.stringify(recommendation) });
|
|
61
|
+
res.json(withProposalEnvelope({ ai_suggestion: recommendation, model, provider, suggestion_id: stored.id, requested_by: admin.id, ai_notice: AI_NOTICE }));
|
|
62
|
+
});
|
|
63
|
+
// stored AI suggestions (evidence) for a proposal
|
|
64
|
+
app.get('/api/admin/task-proposals/:id/ai-suggestions', (req, res) => {
|
|
65
|
+
const admin = requireSupportAdmin(req, res);
|
|
66
|
+
if (!admin)
|
|
67
|
+
return;
|
|
68
|
+
res.json(withProposalEnvelope({ suggestions: listAiSuggestions(db, String(req.params.id)), ai_notice: AI_NOTICE }));
|
|
69
|
+
});
|
|
70
|
+
// ── create an UNPUBLISHED formal task draft from a proposal — explicit maintainer action ──
|
|
71
|
+
// No auto-publish (draft is internal/unclaimable until an explicit publish); no reward/credit side effect.
|
|
72
|
+
app.post('/api/admin/task-proposals/:id/create-task-draft', (req, res) => {
|
|
73
|
+
const admin = requireSupportAdmin(req, res);
|
|
74
|
+
if (!admin)
|
|
75
|
+
return;
|
|
76
|
+
const b = (req.body ?? {});
|
|
77
|
+
if (!b.title || String(b.title).trim().length < 3)
|
|
78
|
+
return void errorRes(res, 400, 'TITLE_REQUIRED', 'title is required (>=3 chars) — the maintainer must review/confirm the formal title');
|
|
79
|
+
const r = createDraftFromProposal(db, {
|
|
80
|
+
proposalId: String(req.params.id), adminId: admin.id,
|
|
81
|
+
title: String(b.title), area: b.area ?? null, description: b.description ?? null,
|
|
82
|
+
sourceRef: b.source_ref ?? null,
|
|
83
|
+
acceptanceCriteria: Array.isArray(b.acceptance_criteria) ? b.acceptance_criteria : [],
|
|
84
|
+
verificationCommands: Array.isArray(b.verification_commands) ? b.verification_commands : [],
|
|
85
|
+
deliverables: Array.isArray(b.deliverables) ? b.deliverables : [],
|
|
86
|
+
allowedPaths: Array.isArray(b.allowed_paths) ? b.allowed_paths : [],
|
|
87
|
+
forbiddenPaths: Array.isArray(b.forbidden_paths) ? b.forbidden_paths : [],
|
|
88
|
+
forbiddenActions: Array.isArray(b.forbidden_actions) ? b.forbidden_actions : [],
|
|
89
|
+
requiredCapabilities: Array.isArray(b.required_capabilities) ? b.required_capabilities : [],
|
|
90
|
+
definitionOfDone: b.definition_of_done ?? null,
|
|
91
|
+
expectedResults: b.expected_results ?? null,
|
|
92
|
+
autoClaimable: b.auto_claimable === false ? false : undefined,
|
|
93
|
+
riskLevel: b.risk_level, taskType: b.task_type, note: b.note ?? null,
|
|
94
|
+
});
|
|
95
|
+
if ('error' in r) {
|
|
96
|
+
const code = r.error_code === 'PROPOSAL_NOT_FOUND' ? 404 : r.error_code === 'PROPOSAL_TERMINAL' ? 409 : r.error_code === 'RATE_LIMITED' ? 429 : 400;
|
|
97
|
+
return void errorRes(res, code, r.error_code, r.error);
|
|
98
|
+
}
|
|
99
|
+
res.json(withProposalEnvelope({ draft: { draft_task_id: r.draft_task_id, status: 'draft', audience: 'internal', published: false }, created_by: admin.id }));
|
|
100
|
+
});
|
|
101
|
+
// admin list of UNPUBLISHED drafts (internal, open) + source proposal id
|
|
102
|
+
app.get('/api/admin/build-task-drafts', (req, res) => {
|
|
103
|
+
const admin = requireSupportAdmin(req, res);
|
|
104
|
+
if (!admin)
|
|
105
|
+
return;
|
|
106
|
+
res.json(withProposalEnvelope({ drafts: listDraftBuildTasks(db) }));
|
|
107
|
+
});
|
|
108
|
+
// PUBLISH a draft → public open task — explicit human/admin action; records the acting admin
|
|
109
|
+
app.post('/api/admin/build-task-drafts/:id/publish', (req, res) => {
|
|
110
|
+
const admin = requireSupportAdmin(req, res);
|
|
111
|
+
if (!admin)
|
|
112
|
+
return;
|
|
113
|
+
const r = publishDraftBuildTask(db, String(req.params.id), admin.id);
|
|
114
|
+
if ('error' in r) {
|
|
115
|
+
const code = r.error_code === 'NOT_FOUND' ? 404
|
|
116
|
+
: (r.error_code === 'PROPOSAL_REJECTED' || r.error_code === 'PROPOSAL_CONVERTED_ELSEWHERE') ? 409 : 400;
|
|
117
|
+
return void errorRes(res, code, r.error_code, r.error, r.missing ? { missing: r.missing } : undefined);
|
|
118
|
+
}
|
|
119
|
+
res.json(withProposalEnvelope({ published: { task_id: r.task_id, published: true }, published_by: admin.id }));
|
|
120
|
+
});
|
|
121
|
+
}
|