@seasonkoh/webaz 0.1.26 → 0.1.27
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 +2 -2
- package/NOTICE +24 -3
- package/README.md +74 -330
- package/README.zh-CN.md +419 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
- package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
- package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
- package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
- package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
- package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
- package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
- package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
- package/dist/ledger.js +1 -1
- package/dist/pwa/admin-audit.js +38 -0
- package/dist/pwa/anti-abuse-thresholds.js +135 -0
- package/dist/pwa/cf-origin-guard.js +33 -0
- package/dist/pwa/contract-fingerprint.js +1 -0
- package/dist/pwa/data/onboarding-cases.js +2 -2
- package/dist/pwa/data/onboarding-quiz.js +1 -1
- package/dist/pwa/economic-participation.js +2 -2
- package/dist/pwa/integration-contract.js +46 -4
- package/dist/pwa/internal/pv-settlement.js +12 -0
- package/dist/pwa/internal/wallet-signer.js +26 -0
- package/dist/pwa/public/app.js +679 -679
- package/dist/pwa/public/i18n.js +15 -28
- package/dist/pwa/public/index.html +1 -1
- package/dist/pwa/public/openapi.json +4760 -2769
- package/dist/pwa/pv-kill-switch.js +31 -0
- package/dist/pwa/routes/admin-admins.js +48 -1
- package/dist/pwa/routes/admin-analytics.js +1 -10
- package/dist/pwa/routes/admin-atomic.js +4 -17
- package/dist/pwa/routes/admin-operator-claims.js +280 -0
- package/dist/pwa/routes/admin-reports.js +4 -26
- package/dist/pwa/routes/admin-tokenomics.js +2 -76
- package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
- package/dist/pwa/routes/admin-users-query.js +23 -1
- package/dist/pwa/routes/admin-wallet-ops.js +1 -1
- package/dist/pwa/routes/auth-read.js +1 -5
- package/dist/pwa/routes/auth-register.js +3 -13
- package/dist/pwa/routes/build-task-quota.js +113 -0
- package/dist/pwa/routes/claim-verify.js +15 -11
- package/dist/pwa/routes/contribution-facts.js +18 -0
- package/dist/pwa/routes/dispute-cases.js +5 -4
- package/dist/pwa/routes/growth.js +3 -3
- package/dist/pwa/routes/orders-action.js +27 -10
- package/dist/pwa/routes/orders-create.js +1 -1
- package/dist/pwa/routes/products-meta.js +19 -6
- package/dist/pwa/routes/profile-placement.js +1 -1
- package/dist/pwa/routes/promoter.js +10 -29
- package/dist/pwa/routes/public-build-tasks.js +5 -1
- package/dist/pwa/routes/public-utils.js +9 -12
- package/dist/pwa/routes/referral.js +5 -26
- package/dist/pwa/routes/rewards-apply.js +3 -2
- package/dist/pwa/routes/share-redirects.js +1 -1
- package/dist/pwa/routes/shareables-interactions.js +2 -1
- package/dist/pwa/routes/task-proposals.js +85 -9
- package/dist/pwa/routes/users-public.js +1 -4
- package/dist/pwa/routes/wallet-read.js +2 -14
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +156 -469
- package/dist/settlement-math.js +3 -3
- package/dist/version.js +6 -4
- package/package.json +33 -7
- package/dist/index.js +0 -182
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
- package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
- package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
- package/dist/test-dispute.js +0 -153
- package/dist/test-manifest.js +0 -61
- package/dist/test-mcp-tools.js +0 -135
- package/dist/test-reputation.js +0 -116
- package/dist/test-skill-market.js +0 -101
|
@@ -66,12 +66,10 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
66
66
|
});
|
|
67
67
|
app.get('/api/system-flags', async (_req, res) => {
|
|
68
68
|
const requireRef = (await dbOne("SELECT value FROM system_state WHERE key='require_ref_to_register'"))?.value === '1';
|
|
69
|
-
const inviteRotation = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
|
|
70
69
|
// #1049 Turnstile 公钥(若启用),前端注册表单 widget 用
|
|
71
70
|
const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || null;
|
|
72
71
|
res.json({
|
|
73
72
|
require_ref_to_register: requireRef,
|
|
74
|
-
invite_rotation_enabled: inviteRotation,
|
|
75
73
|
turnstile_site_key: turnstileSiteKey,
|
|
76
74
|
});
|
|
77
75
|
});
|
|
@@ -134,13 +132,12 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
134
132
|
},
|
|
135
133
|
// 公开披露文档(#1050) — 协议层"钱怎么流"的源真理(协议外可读)
|
|
136
134
|
disclosures: {
|
|
137
|
-
//
|
|
138
|
-
source_status: 'repo
|
|
139
|
-
economic_model: 'https://github.com/
|
|
140
|
-
mlm_compliance: 'https://github.com/
|
|
141
|
-
agent_governance: 'https://github.com/
|
|
142
|
-
|
|
143
|
-
changelog: 'https://github.com/seasonsagents-art/webaz/blob/main/CHANGELOG.md',
|
|
135
|
+
// 源码仓库已公开(github.com/webaz-protocol/webaz);机器可读 spec 也全在 /.well-known/*。
|
|
136
|
+
source_status: 'repo is public (github.com/webaz-protocol/webaz); the full spec is also available via /.well-known/*.',
|
|
137
|
+
economic_model: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ECONOMIC-MODEL.md',
|
|
138
|
+
mlm_compliance: 'https://github.com/webaz-protocol/webaz/blob/main/docs/PARTICIPATION-ATTRIBUTION-COMPLIANCE.md',
|
|
139
|
+
agent_governance: 'https://github.com/webaz-protocol/webaz/blob/main/docs/AGENT-GOVERNANCE.md',
|
|
140
|
+
changelog: 'https://github.com/webaz-protocol/webaz/blob/main/CHANGELOG.md',
|
|
144
141
|
// RFC-011 §②:agent 可读能力矩阵(写边界 action-scope + 敏感读 scope),live doc=code
|
|
145
142
|
capability_matrix: 'https://webaz.xyz/.well-known/webaz-capabilities.json',
|
|
146
143
|
},
|
|
@@ -457,9 +454,9 @@ export function registerPublicUtilsRoutes(app, deps) {
|
|
|
457
454
|
eligibility,
|
|
458
455
|
quiz_pass_score: quizPassScore,
|
|
459
456
|
spec_urls: {
|
|
460
|
-
onboarding: 'https://github.com/
|
|
461
|
-
playbook: 'https://github.com/
|
|
462
|
-
leaderboard: 'https://github.com/
|
|
457
|
+
onboarding: 'https://github.com/webaz-protocol/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md',
|
|
458
|
+
playbook: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md',
|
|
459
|
+
leaderboard: 'https://github.com/webaz-protocol/webaz/blob/main/docs/GOVERNANCE-LEADERBOARD-SPEC.md',
|
|
463
460
|
},
|
|
464
461
|
});
|
|
465
462
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { dbOne, dbAll
|
|
1
|
+
import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
|
|
2
3
|
export function registerReferralRoutes(app, deps) {
|
|
3
4
|
// db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
|
|
4
|
-
const { auth
|
|
5
|
+
const { auth } = deps;
|
|
5
6
|
// B-1: 个人邀请 dashboard
|
|
6
7
|
app.get('/api/referral/me', async (req, res) => {
|
|
7
8
|
const user = auth(req, res);
|
|
@@ -36,28 +37,6 @@ export function registerReferralRoutes(app, deps) {
|
|
|
36
37
|
},
|
|
37
38
|
});
|
|
38
39
|
});
|
|
39
|
-
// 公开邀请码轮询(开关 ON 时)
|
|
40
|
-
app.post('/api/invite/rotate', async (_req, res) => {
|
|
41
|
-
const enabled = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
|
|
42
|
-
if (!enabled)
|
|
43
|
-
return void res.status(403).json({ error: '邀请码获取暂未开放', enabled: false });
|
|
44
|
-
const slot = issueInviteSlot();
|
|
45
|
-
const u = inviteRotationLookup(slot);
|
|
46
|
-
if (!u)
|
|
47
|
-
return void res.status(503).json({ error: `轮询用户未就绪,请联系管理员`, enabled: true });
|
|
48
|
-
res.json({ enabled: true, code: u.code });
|
|
49
|
-
});
|
|
50
|
-
// protocol 开关
|
|
51
|
-
app.post('/api/admin/invite-rotation/toggle', async (req, res) => {
|
|
52
|
-
const admin = requireProtocolAdmin(req, res);
|
|
53
|
-
if (!admin)
|
|
54
|
-
return;
|
|
55
|
-
const { enabled } = req.body;
|
|
56
|
-
const v = enabled ? '1' : '0';
|
|
57
|
-
await dbRun("INSERT OR REPLACE INTO system_state (key, value) VALUES ('invite_rotation_enabled', ?)", [v]);
|
|
58
|
-
logAdminAction(admin.id, 'invite_rotation_toggle', 'system', 'invite_rotation_enabled', { value: v });
|
|
59
|
-
res.json({ success: true, enabled: !!enabled });
|
|
60
|
-
});
|
|
61
40
|
// RFC-003 #1122: 生成商品分享链接(把 MCP webaz_share_link 的本地计算搬到服务端,
|
|
62
41
|
// 让 MCP NETWORK 模式可代理)。RFC-002 §3.5 valuation-layer gate:需 rewards opt-in。
|
|
63
42
|
// pre-public 去左右码:不再接受/返回 side,放置侧别由注册时系统自动决定。
|
|
@@ -77,7 +56,7 @@ export function registerReferralRoutes(app, deps) {
|
|
|
77
56
|
};
|
|
78
57
|
const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
|
|
79
58
|
const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
|
|
80
|
-
const totalCompleted = (await dbOne(
|
|
59
|
+
const totalCompleted = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
|
|
81
60
|
const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
|
|
82
61
|
const missing = [];
|
|
83
62
|
if (totalCompleted < minOrders)
|
|
@@ -101,7 +80,7 @@ export function registerReferralRoutes(app, deps) {
|
|
|
101
80
|
if (!product)
|
|
102
81
|
return void res.status(404).json({ error: '商品不存在或已下架', error_code: 'PRODUCT_NOT_FOUND' });
|
|
103
82
|
// pre-public 去左右码:分享链接不再计算/携带 side(放置侧别由注册时系统自动决定)
|
|
104
|
-
const completed = (await dbOne(
|
|
83
|
+
const completed = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
|
|
105
84
|
const override = (await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]))?.l1_share_override ?? 0;
|
|
106
85
|
const canL1 = override === 1 || (override === 0 && completed > 0);
|
|
107
86
|
const rate = Number(product.commission_rate ?? 0);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
3
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
|
|
3
4
|
function sha256_hex(s) {
|
|
4
5
|
return createHash('sha256').update(s).digest('hex');
|
|
5
6
|
}
|
|
@@ -26,7 +27,7 @@ export function registerRewardsApplyRoutes(app, deps) {
|
|
|
26
27
|
state = 'auto_downgraded';
|
|
27
28
|
else
|
|
28
29
|
state = 'never_activated';
|
|
29
|
-
const completedOrders = (await dbOne(
|
|
30
|
+
const completedOrders = (await dbOne(`SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
|
|
30
31
|
const passkeyCount = (await dbOne("SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
|
|
31
32
|
const minOrders = Number(getProtocolParam('rewards_opt_in.min_completed_orders', 1));
|
|
32
33
|
const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));
|
|
@@ -103,7 +104,7 @@ export function registerRewardsApplyRoutes(app, deps) {
|
|
|
103
104
|
}
|
|
104
105
|
// 5. Pre-conditions (re-check inside server)
|
|
105
106
|
const minOrders = Number(getProtocolParam('rewards_opt_in.min_completed_orders', 1));
|
|
106
|
-
const completedOrders = (await dbOne(
|
|
107
|
+
const completedOrders = (await dbOne(`SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
|
|
107
108
|
if (completedOrders < minOrders)
|
|
108
109
|
return void errorRes(res, 403, 'INSUFFICIENT_ORDERS', `需 ${minOrders} 笔已完成订单,目前 ${completedOrders}`);
|
|
109
110
|
const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));
|
|
@@ -158,7 +158,7 @@ export function registerShareRedirectsRoutes(app, deps) {
|
|
|
158
158
|
// 邀请短链 /i/CODE — invite-code ONLY (permanent_code, 兼容旧的 -L/-R 后缀). usr_xxx / @handle / 裸 handle
|
|
159
159
|
// 一律 404(不再做 handle 解析)。pre-public 去左右码:/i/CODE 与旧 /i/CODE-L、/i/CODE-R 一律
|
|
160
160
|
// 规范化重定向到 /?ref=CODE(丢弃 side;放置侧别由注册时系统自动决定),旧链接/二维码仍可用。
|
|
161
|
-
//
|
|
161
|
+
// 不受注册门控开关影响:已有用户分享出的 /i/CODE 链接和二维码必须始终可用。
|
|
162
162
|
app.get('/i/:code', (req, res) => {
|
|
163
163
|
const ref = resolveInviteCodeRef(String(req.params.code || ''));
|
|
164
164
|
if (!ref)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
|
|
2
|
+
import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
|
|
2
3
|
export function registerShareablesInteractionsRoutes(app, deps) {
|
|
3
4
|
const { db, auth, generateId, rateLimitOk, piiSanitize, detectFraud, commentBlocklistHit, llmModerateComment, parseMentions, notifyMentions } = deps;
|
|
4
5
|
app.post('/api/shareables/:id/click', async (req, res) => {
|
|
@@ -21,7 +22,7 @@ export function registerShareablesInteractionsRoutes(app, deps) {
|
|
|
21
22
|
if (sh.owner_id === user.id)
|
|
22
23
|
return void res.json({ error: '不能给自己点赞' });
|
|
23
24
|
// P1 Sybil 软门槛:至少完成过 1 笔订单(不限购买该商品,只需活跃用户)
|
|
24
|
-
const completed = (await dbOne(
|
|
25
|
+
const completed = (await dbOne(`SELECT COUNT(1) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [user.id])).n; // 真实成交,排除退款/违约
|
|
25
26
|
if (completed < 1)
|
|
26
27
|
return void res.json({ error: '完成首笔购买后才能点赞(防止刷赞)' });
|
|
27
28
|
// P0 fix:SELECT existing 进 transaction
|
|
@@ -1,15 +1,25 @@
|
|
|
1
|
-
import { validateProposalInput, insertTaskProposal, listTaskProposals, reviewTaskProposal } from '../../layer2-business/L2-9-contribution/task-proposal-store.js';
|
|
1
|
+
import { validateProposalInput, insertTaskProposal, listTaskProposals, listMyProposals, reviewTaskProposal } from '../../layer2-business/L2-9-contribution/task-proposal-store.js';
|
|
2
2
|
import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
|
|
3
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';
|
|
4
|
+
import { createDraftFromProposal, listDraftBuildTasks, getDraftBuildTaskDetail, publishDraftBuildTask, discardDraft, withdrawPublishedTask } from '../../layer2-business/L2-9-contribution/task-proposal-draft.js';
|
|
5
5
|
import { recommendForProposal, insertAiSuggestion, listAiSuggestions, getProposalLite } from '../../layer2-business/L2-9-contribution/task-proposal-ai-store.js';
|
|
6
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
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
8
|
function withProposalEnvelope(payload) {
|
|
9
9
|
return withUncommittedValueBoundary({ ...payload, proposal_notice: PROPOSAL_NOTICE, canonical_contribution_target: getCanonicalContributionTarget() });
|
|
10
10
|
}
|
|
11
|
+
// Agent-readable next-step hint per status (the channel is agent-native both ways).
|
|
12
|
+
function nextActionFor(status) {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case 'new': return 'Awaiting maintainer review — no action needed.';
|
|
15
|
+
case 'needs_info': return 'Maintainer needs more detail. Submit an updated proposal (webaz_feedback type=proposal) referencing this id; see public_reply for what is missing.';
|
|
16
|
+
case 'rejected': return 'Not converted to a task. See public_reply for the reason; you may submit a revised proposal.';
|
|
17
|
+
case 'converted': return 'Converted to a task. See converted_ref for the linked task / PR / decision.';
|
|
18
|
+
default: return 'No action.';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
11
21
|
export function registerTaskProposalsRoutes(app, deps) {
|
|
12
|
-
const { db, errorRes, requireSupportAdmin, rateLimitOk } = deps;
|
|
22
|
+
const { db, errorRes, requireSupportAdmin, rateLimitOk, auth, resolveUser } = deps;
|
|
13
23
|
// public submit — anonymous; proposer_account_id is never taken from the body (anti-spoof).
|
|
14
24
|
app.post('/api/public/task-proposals', (req, res) => {
|
|
15
25
|
// anti-flood: per-IP rate limit (counts every attempt, before validation) then a recent-window dedup.
|
|
@@ -19,10 +29,23 @@ export function registerTaskProposalsRoutes(app, deps) {
|
|
|
19
29
|
const v = validateProposalInput(req.body);
|
|
20
30
|
if (!v.ok)
|
|
21
31
|
return void errorRes(res, 400, v.code, v.message);
|
|
22
|
-
|
|
32
|
+
// Link to the submitter when authenticated (account id comes from the session, NEVER the body — anti-spoof);
|
|
33
|
+
// anonymous submit still works (account id null) — it just won't show up in "my proposals".
|
|
34
|
+
const submitter = resolveUser(req);
|
|
35
|
+
const accountId = submitter ? String(submitter.id) : null;
|
|
36
|
+
const result = insertTaskProposal(db, v.input, accountId);
|
|
23
37
|
if ('duplicate' in result)
|
|
24
38
|
return void errorRes(res, 409, 'DUPLICATE_PROPOSAL', '相同建议已在收件箱中,请勿重复提交', { existing_id: result.existing_id });
|
|
25
|
-
res.json(withProposalEnvelope({ proposal: { id: result.id, status: result.status } }));
|
|
39
|
+
res.json(withProposalEnvelope({ proposal: { id: result.id, status: result.status }, linked_to_account: !!accountId }));
|
|
40
|
+
});
|
|
41
|
+
// proposer-facing read: the caller's OWN proposals + status + public_reply (agent-readable). No review_note; own rows only.
|
|
42
|
+
app.get('/api/me/task-proposals', (req, res) => {
|
|
43
|
+
const user = auth(req, res);
|
|
44
|
+
if (!user)
|
|
45
|
+
return;
|
|
46
|
+
// case_id threads the whole case (proposal → task → PR). For a proposal it is the proposal's own id.
|
|
47
|
+
const proposals = listMyProposals(db, String(user.id)).map(p => ({ ...p, case_id: p.id, next_action: nextActionFor(String(p.status)) }));
|
|
48
|
+
res.json(withProposalEnvelope({ proposals }));
|
|
26
49
|
});
|
|
27
50
|
// admin list (maintainer only)
|
|
28
51
|
app.get('/api/admin/task-proposals', (req, res) => {
|
|
@@ -30,15 +53,17 @@ export function registerTaskProposalsRoutes(app, deps) {
|
|
|
30
53
|
if (!admin)
|
|
31
54
|
return;
|
|
32
55
|
const status = typeof req.query.status === 'string' ? req.query.status : undefined;
|
|
33
|
-
|
|
56
|
+
// case_id = the proposal's own id (the case originates at the proposal); shown in the management inbox.
|
|
57
|
+
const proposals = listTaskProposals(db, { status }).map(p => ({ ...p, case_id: p.id }));
|
|
58
|
+
res.json(withProposalEnvelope({ proposals }));
|
|
34
59
|
});
|
|
35
60
|
// admin review (maintainer only): needs_info | rejected | converted — no build_task is created here.
|
|
36
61
|
app.post('/api/admin/task-proposals/:id/review', (req, res) => {
|
|
37
62
|
const admin = requireSupportAdmin(req, res);
|
|
38
63
|
if (!admin)
|
|
39
64
|
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);
|
|
65
|
+
const { status, note, converted_ref, public_reply } = req.body ?? {};
|
|
66
|
+
const result = reviewTaskProposal(db, String(req.params.id), admin.id, String(status), note, converted_ref, public_reply);
|
|
42
67
|
if ('error' in result) {
|
|
43
68
|
const code = result.code === 'NOT_FOUND' ? 404 : result.code === 'ALREADY_TERMINAL' ? 409 : 400;
|
|
44
69
|
return void errorRes(res, code, result.code, result.error);
|
|
@@ -90,6 +115,11 @@ export function registerTaskProposalsRoutes(app, deps) {
|
|
|
90
115
|
definitionOfDone: b.definition_of_done ?? null,
|
|
91
116
|
expectedResults: b.expected_results ?? null,
|
|
92
117
|
autoClaimable: b.auto_claimable === false ? false : undefined,
|
|
118
|
+
// optional real effort estimate (#34/#5); if omitted the draft is a 0–0 placeholder and publish is fail-closed.
|
|
119
|
+
estimatedDurationMinMinutes: typeof b.estimated_duration_min_minutes === 'number' ? b.estimated_duration_min_minutes : undefined,
|
|
120
|
+
estimatedDurationMaxMinutes: typeof b.estimated_duration_max_minutes === 'number' ? b.estimated_duration_max_minutes : undefined,
|
|
121
|
+
estimatedAgentBudget: b.estimated_agent_budget,
|
|
122
|
+
estimatedContextSize: b.estimated_context_size,
|
|
93
123
|
riskLevel: b.risk_level, taskType: b.task_type, note: b.note ?? null,
|
|
94
124
|
});
|
|
95
125
|
if ('error' in r) {
|
|
@@ -105,12 +135,30 @@ export function registerTaskProposalsRoutes(app, deps) {
|
|
|
105
135
|
return;
|
|
106
136
|
res.json(withProposalEnvelope({ drafts: listDraftBuildTasks(db) }));
|
|
107
137
|
});
|
|
138
|
+
// full stored body of ONE unpublished internal draft — for PRE-PUBLISH PREVIEW (publish against visible content).
|
|
139
|
+
app.get('/api/admin/build-task-drafts/:id', (req, res) => {
|
|
140
|
+
const admin = requireSupportAdmin(req, res);
|
|
141
|
+
if (!admin)
|
|
142
|
+
return;
|
|
143
|
+
const draft = getDraftBuildTaskDetail(db, String(req.params.id));
|
|
144
|
+
if (!draft)
|
|
145
|
+
return void errorRes(res, 404, 'NOT_FOUND', 'draft not found (or not an unpublished internal draft)');
|
|
146
|
+
res.json(withProposalEnvelope({ draft }));
|
|
147
|
+
});
|
|
108
148
|
// PUBLISH a draft → public open task — explicit human/admin action; records the acting admin
|
|
109
149
|
app.post('/api/admin/build-task-drafts/:id/publish', (req, res) => {
|
|
110
150
|
const admin = requireSupportAdmin(req, res);
|
|
111
151
|
if (!admin)
|
|
112
152
|
return;
|
|
113
|
-
const
|
|
153
|
+
const b = (req.body ?? {});
|
|
154
|
+
// a published task MUST carry a real effort estimate (#34/#5). The maintainer supplies it here when the
|
|
155
|
+
// draft is still a 0–0 placeholder; without it publishDraftBuildTask fails closed (DRAFT_ESTIMATE_REQUIRED).
|
|
156
|
+
const r = publishDraftBuildTask(db, String(req.params.id), admin.id, {
|
|
157
|
+
minMinutes: typeof b.estimated_duration_min_minutes === 'number' ? b.estimated_duration_min_minutes : undefined,
|
|
158
|
+
maxMinutes: typeof b.estimated_duration_max_minutes === 'number' ? b.estimated_duration_max_minutes : undefined,
|
|
159
|
+
budget: b.estimated_agent_budget,
|
|
160
|
+
contextSize: b.estimated_context_size,
|
|
161
|
+
});
|
|
114
162
|
if ('error' in r) {
|
|
115
163
|
const code = r.error_code === 'NOT_FOUND' ? 404
|
|
116
164
|
: (r.error_code === 'PROPOSAL_REJECTED' || r.error_code === 'PROPOSAL_CONVERTED_ELSEWHERE') ? 409 : 400;
|
|
@@ -118,4 +166,32 @@ export function registerTaskProposalsRoutes(app, deps) {
|
|
|
118
166
|
}
|
|
119
167
|
res.json(withProposalEnvelope({ published: { task_id: r.task_id, published: true }, published_by: admin.id }));
|
|
120
168
|
});
|
|
169
|
+
// DISCARD an unpublished internal draft (soft-delete → frees the proposal's draft slot; provenance retained).
|
|
170
|
+
// Fail-closed: refuses a published / claimed draft or an already-converted source proposal. Scope = discard only.
|
|
171
|
+
app.post('/api/admin/build-task-drafts/:id/discard', (req, res) => {
|
|
172
|
+
const admin = requireSupportAdmin(req, res);
|
|
173
|
+
if (!admin)
|
|
174
|
+
return;
|
|
175
|
+
const r = discardDraft(db, String(req.params.id), admin.id);
|
|
176
|
+
if ('error' in r) {
|
|
177
|
+
const code = r.error_code === 'NOT_FOUND' ? 404
|
|
178
|
+
: (r.error_code === 'ALREADY_PUBLISHED' || r.error_code === 'DRAFT_CLAIMED' || r.error_code === 'ALREADY_CONVERTED') ? 409 : 400;
|
|
179
|
+
return void errorRes(res, code, r.error_code, r.error);
|
|
180
|
+
}
|
|
181
|
+
res.json(withProposalEnvelope({ discarded: { task_id: r.task_id, status: 'discarded', already_discarded: !!r.already_discarded }, discarded_by: admin.id }));
|
|
182
|
+
});
|
|
183
|
+
// RECOVERY: withdraw an UNCLAIMED published task off the board + reopen its source proposal (so a corrected
|
|
184
|
+
// draft can be built). Fail-closed: refuses a claimed task or a non-published task. Soft-delete (provenance kept).
|
|
185
|
+
app.post('/api/admin/build-tasks/:id/withdraw', (req, res) => {
|
|
186
|
+
const admin = requireSupportAdmin(req, res);
|
|
187
|
+
if (!admin)
|
|
188
|
+
return;
|
|
189
|
+
const r = withdrawPublishedTask(db, String(req.params.id), admin.id);
|
|
190
|
+
if ('error' in r) {
|
|
191
|
+
const code = r.error_code === 'NOT_FOUND' ? 404
|
|
192
|
+
: (r.error_code === 'TASK_CLAIMED' || r.error_code === 'NOT_PUBLISHED') ? 409 : 400;
|
|
193
|
+
return void errorRes(res, code, r.error_code, r.error);
|
|
194
|
+
}
|
|
195
|
+
res.json(withProposalEnvelope({ withdrawn: { task_id: r.task_id, reopened_proposal_id: r.reopened_proposal_id }, withdrawn_by: admin.id }));
|
|
196
|
+
});
|
|
121
197
|
}
|
|
@@ -62,8 +62,7 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
62
62
|
const rightChildName = u.right_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [u.right_child_id]))?.name : null;
|
|
63
63
|
const leftPv = Number(u.total_left_pv ?? 0);
|
|
64
64
|
const rightPv = Number(u.total_right_pv ?? 0);
|
|
65
|
-
//
|
|
66
|
-
// (weak_leg_pv / pending_score / settled_waz / total_hits)。位置 + 左右区 PV 仅作为参与记录保留。
|
|
65
|
+
// 匹配奖励引擎已切除(#401):public 端口只保留位置 + 左右区 PV 作为参与记录,不暴露任何奖励指标。
|
|
67
66
|
res.json({
|
|
68
67
|
id: u.id,
|
|
69
68
|
name: u.name,
|
|
@@ -263,13 +262,11 @@ export function registerUsersPublicRoutes(app, deps) {
|
|
|
263
262
|
if (isOwner) {
|
|
264
263
|
const w = await dbOne('SELECT balance, earned FROM wallets WHERE user_id = ?', [me.id]);
|
|
265
264
|
const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [me.id]);
|
|
266
|
-
const score = (await dbOne("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL", [me.id])).s;
|
|
267
265
|
privateStats = {
|
|
268
266
|
wallet_balance: Number(w?.balance ?? 0),
|
|
269
267
|
wallet_earned: Number(w?.earned ?? 0),
|
|
270
268
|
total_left_pv: Number(pv?.total_left_pv ?? 0),
|
|
271
269
|
total_right_pv: Number(pv?.total_right_pv ?? 0),
|
|
272
|
-
pending_score: Number(score),
|
|
273
270
|
};
|
|
274
271
|
}
|
|
275
272
|
// D2 信誉徽章墙
|
|
@@ -175,26 +175,14 @@ export function registerWalletReadRoutes(app, deps) {
|
|
|
175
175
|
if (commMap[key])
|
|
176
176
|
commMap[key] = { count: r.cnt, total: Number(r.total.toFixed(2)) };
|
|
177
177
|
}
|
|
178
|
-
|
|
179
|
-
SELECT
|
|
180
|
-
COUNT(CASE WHEN settled_at IS NOT NULL THEN 1 END) as settled_cnt,
|
|
181
|
-
COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount END), 0) as settled_waz,
|
|
182
|
-
COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score END), 0) as pending_score
|
|
183
|
-
FROM binary_score_records WHERE user_id = ?
|
|
184
|
-
`, [user.id]));
|
|
178
|
+
// matching-rewards income removed — engine excised (#401). Income = affiliate commission (real sales) + own sales.
|
|
185
179
|
const sales = (await dbOne(`
|
|
186
180
|
SELECT COUNT(*) as cnt, COALESCE(SUM(total_amount),0) as total
|
|
187
181
|
FROM orders WHERE seller_id = ? AND status = 'completed'
|
|
188
182
|
`, [user.id]));
|
|
189
|
-
const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total +
|
|
190
|
-
Number(binary.settled_waz) + Number(sales.total);
|
|
183
|
+
const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total + Number(sales.total);
|
|
191
184
|
res.json({
|
|
192
185
|
commissions: commMap,
|
|
193
|
-
binary: {
|
|
194
|
-
settled_count: binary.settled_cnt,
|
|
195
|
-
settled_waz: Number(Number(binary.settled_waz).toFixed(2)),
|
|
196
|
-
pending_score: Number(Number(binary.pending_score).toFixed(2)),
|
|
197
|
-
},
|
|
198
186
|
sales: { count: sales.cnt, total: Number(Number(sales.total).toFixed(2)) },
|
|
199
187
|
total_income: Number(totalIncome.toFixed(2)),
|
|
200
188
|
});
|
|
@@ -73,7 +73,7 @@ export function registerWebauthnRoutes(app, deps) {
|
|
|
73
73
|
if (!user)
|
|
74
74
|
return;
|
|
75
75
|
const purpose = String(req.body?.purpose || '').trim();
|
|
76
|
-
const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim']);
|
|
76
|
+
const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink']);
|
|
77
77
|
if (!allowed.has(purpose))
|
|
78
78
|
return void res.status(400).json({ error: 'invalid purpose' });
|
|
79
79
|
const purpose_data = req.body?.purpose_data ?? null;
|