@seasonkoh/webaz 0.1.16 → 0.1.18
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 +60 -5
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +3 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +899 -720
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +287 -0
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +102 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +180 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +16 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +1 -0
- package/dist/mcp.js +7 -3
- package/dist/pwa/data/onboarding-cases.js +345 -0
- package/dist/pwa/data/onboarding-quiz.js +247 -0
- package/dist/pwa/public/app.js +1459 -96
- package/dist/pwa/public/i18n.js +303 -2
- package/dist/pwa/public/icon-192.png +0 -0
- package/dist/pwa/public/icon-512.png +0 -0
- package/dist/pwa/public/manifest.json +5 -2
- package/dist/pwa/public/openapi.json +1 -1
- package/dist/pwa/public/sw.js +1 -1
- package/dist/pwa/routes/admin-protocol-params.js +80 -2
- package/dist/pwa/routes/admin-reports.js +14 -9
- package/dist/pwa/routes/auth-read.js +3 -1
- package/dist/pwa/routes/build-feedback.js +82 -0
- package/dist/pwa/routes/build-reputation.js +10 -0
- package/dist/pwa/routes/build-tasks.js +73 -0
- package/dist/pwa/routes/disputes-write.js +149 -1
- package/dist/pwa/routes/governance-auto-deactivate.js +108 -0
- package/dist/pwa/routes/governance-onboarding.js +785 -0
- package/dist/pwa/routes/leaderboard.js +10 -2
- package/dist/pwa/routes/orders-action.js +5 -1
- package/dist/pwa/routes/products-meta.js +30 -0
- package/dist/pwa/routes/profile-identity.js +1 -1
- package/dist/pwa/routes/public-utils.js +44 -0
- package/dist/pwa/routes/rewards-apply.js +210 -0
- package/dist/pwa/routes/rewards-auto-downgrade.js +65 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +48 -0
- package/dist/pwa/routes/wallet-write.js +17 -31
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +641 -64
- package/package.json +6 -3
package/dist/pwa/server.js
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PWA HTTP Server
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* PWA HTTP Server — WebAZ 的人类入口 + 生产 HTTP API(端口 3000)
|
|
3
|
+
*
|
|
4
|
+
* 作用 / What:
|
|
5
|
+
* - 服务 PWA 静态前端(src/pwa/public/*)给手机/桌面浏览器(人类)
|
|
6
|
+
* - 暴露 /api/* HTTP API —— **人 + agent 共用的生产端点**(MCP NETWORK 模式也打这里,见 RFC-003)
|
|
7
|
+
*
|
|
8
|
+
* 本文件做什么 / Structure(这是最大的文件,改动前先定位区块):
|
|
9
|
+
* - 启动:initDatabase() + 各 initXxxSchema(db) + 内联 CREATE TABLE(部分表直接建在这里)
|
|
10
|
+
* - 鉴权:auth()/getUser()(统一 `Authorization: Bearer <api_key>`)、requireAdmin/requireAdminPermission
|
|
11
|
+
* - 中间件:api_key 速率/封禁 + agent 声明 scope 门(getDeclaredActions/getAgentRiskInfo)
|
|
12
|
+
* - 路由:大部分按主题拆到 src/pwa/routes/*,在文件下半部 registerXxxRoutes(app, deps) 接线;
|
|
13
|
+
* 少数端点仍内联在本文件
|
|
14
|
+
*
|
|
15
|
+
* 关联 / Related: AGENTS.md(项目地图) · 元规则 #2 代码即规则 / #3 不偷数据 / #6 不滥用 ·
|
|
16
|
+
* RFC-003(MCP NETWORK 共用这些端点) · CHARTER §3.2(改动审批分档)
|
|
5
17
|
*/
|
|
6
18
|
import express from 'express';
|
|
7
19
|
import path from 'path';
|
|
@@ -126,6 +138,12 @@ import { registerExternalAnchorsRoutes } from './routes/external-anchors.js';
|
|
|
126
138
|
import { registerAnchorsRoutes } from './routes/anchors.js';
|
|
127
139
|
// 仲裁员申请 (#1013 Phase 44) — 4 user + 3 admin endpoints
|
|
128
140
|
import { registerArbitratorRoutes } from './routes/arbitrator.js';
|
|
141
|
+
// Governance onboarding (W3.5-B 实施 #1093 阶段 1) — 2 user endpoints
|
|
142
|
+
import { registerGovernanceOnboardingRoutes } from './routes/governance-onboarding.js';
|
|
143
|
+
import { startAutoDeactivateCron, runAutoDeactivateSweep } from './routes/governance-auto-deactivate.js';
|
|
144
|
+
import { startEscrowExpireCron } from './routes/rewards-escrow-expire.js';
|
|
145
|
+
import { startAutoDowngradeCron } from './routes/rewards-auto-downgrade.js';
|
|
146
|
+
import { registerRewardsApplyRoutes } from './routes/rewards-apply.js';
|
|
129
147
|
// 卖家配额 + 数据中心 (#1013 Phase 45) — 4 user + 3 admin
|
|
130
148
|
import { registerSellerQuotaRoutes } from './routes/seller-quota.js';
|
|
131
149
|
// 验证员用户侧 (#1013 Phase 46) — 5 endpoints
|
|
@@ -276,6 +294,12 @@ import { registerAuthReadRoutes } from './routes/auth-read.js';
|
|
|
276
294
|
import { registerAuthLoginRoutes } from './routes/auth-login.js';
|
|
277
295
|
// Auth register (#1013 Phase 118) — 1 endpoint
|
|
278
296
|
import { registerAuthRegisterRoutes } from './routes/auth-register.js';
|
|
297
|
+
import { registerBuildFeedbackRoutes } from './routes/build-feedback.js';
|
|
298
|
+
import { initBuildFeedbackSchema } from '../layer2-business/L2-8-feedback/build-feedback-engine.js';
|
|
299
|
+
import { registerBuildTasksRoutes } from './routes/build-tasks.js';
|
|
300
|
+
import { initBuildTasksSchema } from '../layer2-business/L2-9-contribution/build-tasks-engine.js';
|
|
301
|
+
import { registerBuildReputationRoutes } from './routes/build-reputation.js';
|
|
302
|
+
import { initBuildReputationSchema } from '../layer2-business/L2-9-contribution/build-reputation-engine.js';
|
|
279
303
|
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
|
280
304
|
// ─── 链上地址派生 ──────────────────────────────────────────────
|
|
281
305
|
const MASTER_SEED = process.env.WALLET_MASTER_SEED ?? 'webaz-dev-seed-changeme';
|
|
@@ -327,6 +351,9 @@ initSkillSchema(db);
|
|
|
327
351
|
initSkillMarketSchema(db);
|
|
328
352
|
initReputationSchema(db);
|
|
329
353
|
initOrderChainSchema(db);
|
|
354
|
+
initBuildFeedbackSchema(db); // RFC-004 build_feedback
|
|
355
|
+
initBuildTasksSchema(db); // RFC-006 build_tasks(协调层)
|
|
356
|
+
initBuildReputationSchema(db); // RFC-006 build_reputation(独立池 + 贡献者看板)
|
|
330
357
|
initSnfSchema(db);
|
|
331
358
|
initExternalAnchorSchema(db);
|
|
332
359
|
// 启动时检查月衰减(last_decay_at ≥25 天才触发,重启幂等)
|
|
@@ -609,9 +636,10 @@ function getProductShareChain(productId, buyerId, depth = 3) {
|
|
|
609
636
|
db.exec(`
|
|
610
637
|
CREATE TABLE IF NOT EXISTS region_config (
|
|
611
638
|
region TEXT PRIMARY KEY,
|
|
612
|
-
max_levels INTEGER NOT NULL, -- 0=完全禁 MLM / 1=仅 L1 / 2=L1+L2 / 3
|
|
639
|
+
max_levels INTEGER NOT NULL, -- 0=完全禁 MLM / 1=仅 L1 / 2=L1+L2 / 3=全三级(仅控佣金层级)
|
|
613
640
|
active INTEGER DEFAULT 1,
|
|
614
|
-
mlm_ui_visible INTEGER DEFAULT 1
|
|
641
|
+
mlm_ui_visible INTEGER DEFAULT 1, -- 0=UI 全面隐藏推土机/分润链/佣金展示
|
|
642
|
+
pv_enabled INTEGER DEFAULT 0 -- 2026-06-04 解耦:PV 双轨/对碰系统是否开启(独立于 max_levels)。默认 0=全关
|
|
615
643
|
)
|
|
616
644
|
`);
|
|
617
645
|
// Phase B 迁移:加 mlm_ui_visible 列
|
|
@@ -619,6 +647,11 @@ try {
|
|
|
619
647
|
db.exec('ALTER TABLE region_config ADD COLUMN mlm_ui_visible INTEGER DEFAULT 1');
|
|
620
648
|
}
|
|
621
649
|
catch { /* 已存在 */ }
|
|
650
|
+
// 2026-06-04 解耦迁移:加 pv_enabled 列(PV 双轨独立开关,与佣金层级 max_levels 分离)
|
|
651
|
+
try {
|
|
652
|
+
db.exec('ALTER TABLE region_config ADD COLUMN pv_enabled INTEGER DEFAULT 0');
|
|
653
|
+
}
|
|
654
|
+
catch { /* 已存在 */ }
|
|
622
655
|
// Phase B 初始值:max_levels=0 或 =1 的地区同时隐藏 MLM UI
|
|
623
656
|
// 目前所有已配置地区 ≥2,不自动降为 0(由 admin 手动配置真正禁 MLM 的地区)
|
|
624
657
|
// mlm_ui_visible=1 保持默认,只有 max_levels=0 时前端才完全隐藏
|
|
@@ -724,7 +757,7 @@ db.exec(`
|
|
|
724
757
|
old_value TEXT,
|
|
725
758
|
new_value TEXT,
|
|
726
759
|
changed_by TEXT,
|
|
727
|
-
action TEXT, -- 'update' | 'reset'
|
|
760
|
+
action TEXT, -- 'update' | 'reset' | 'constitutional_reject_patch' | 'constitutional_reject_reset'
|
|
728
761
|
created_at TEXT DEFAULT (datetime('now'))
|
|
729
762
|
)
|
|
730
763
|
`);
|
|
@@ -766,6 +799,21 @@ const DEFAULT_PARAMS = [
|
|
|
766
799
|
{ key: 'require_human_presence_for_arbitrate', value: '1', type: 'number', description: 'Arbitrator 仲裁需 WebAuthn 一次性 token(1=强制 / 0=不强制)— spec §4 铁律', category: 'security', min: 0, max: 1 },
|
|
767
800
|
{ key: 'require_human_presence_for_agent_revoke', value: '1', type: 'number', description: '用户撤销 agent 需 WebAuthn 一次性 token — spec §4 铁律', category: 'security', min: 0, max: 1 },
|
|
768
801
|
{ key: 'require_human_presence_for_delete_passkey', value: '1', type: 'number', description: '删除 Passkey 自身需 WebAuthn 一次性 token — 防失窃 Passkey 不需 Passkey 就可删它,堵死自我无效化漏洞', category: 'security', min: 0, max: 1 },
|
|
802
|
+
{ key: 'require_human_presence_for_governance_apply', value: '1', type: 'number', description: '治理岗位申请(apply)需 WebAuthn 一次性 token — spec §3.1 Iron-Rule 反诱导 + 真人门', category: 'security', min: 0, max: 1 },
|
|
803
|
+
{ key: 'require_human_presence_for_governance_activate', value: '1', type: 'number', description: 'maintainer 激活治理岗位(activate)需 WebAuthn 一次性 token — spec §4.4 Iron-Rule 真人签发', category: 'security', min: 0, max: 1 },
|
|
804
|
+
{ key: 'require_human_presence_for_governance_resign', value: '1', type: 'number', description: '主动卸任治理岗位(resign)需 WebAuthn 一次性 token — spec §6.1 二次验证', category: 'security', min: 0, max: 1 },
|
|
805
|
+
{ key: 'require_human_presence_for_governance_appeal_resolve', value: '1', type: 'number', description: 'maintainer 裁决申诉(resolve appeal)需 WebAuthn 一次性 token — spec §7.2 Iron-Rule', category: 'security', min: 0, max: 1 },
|
|
806
|
+
{ key: 'require_human_presence_for_withdraw', value: '1', type: 'number', description: '提现(资金转出)需 WebAuthn 一次性 token — 资金转出=真人在场铁律,email-OTP 在 agent 威胁模型下不足(agent 可读收件箱);#1115 全额对齐', category: 'security', min: 0, max: 1 },
|
|
807
|
+
{ key: 'governance_resign_cooldown_days', value: '30', type: 'number', description: '主动卸任后冷却期(天)— 防止 farming 切换洗票 / 误操作反复', category: 'governance', min: 7, max: 365 },
|
|
808
|
+
{ key: 'governance_appeal_window_days', value: '14', type: 'number', description: '收到 auto_deactivate 通知后申诉窗口(天)— spec §7.2', category: 'governance', min: 7, max: 90 },
|
|
809
|
+
{ key: 'governance_appeal_min_reason_chars', value: '100', type: 'number', description: '申诉理由最少字符数(防空 appeal)', category: 'governance', min: 30, max: 2000 },
|
|
810
|
+
// 2026-06-02 task #1093 阶段 5:auto-deactivate cron(per playbook §6.2 anchor=confirmed_wrong,not outlier)
|
|
811
|
+
{ key: 'governance_auto_deactivate_threshold_count', value: '5', type: 'number', description: '被复核确认判错累计次数阈值(触发 auto_deactivate)— playbook §6.2', category: 'governance', min: 1, max: 100 },
|
|
812
|
+
{ key: 'governance_auto_deactivate_threshold_pct', value: '0.3', type: 'number', description: '被确认判错比例阈值(0.3 = 30% 案件被推翻)— playbook §6.2', category: 'governance', min: 0.05, max: 1.0 },
|
|
813
|
+
{ key: 'governance_auto_deactivate_min_sample', value: '10', type: 'number', description: '最小样本数(tasks_done ≥ N 才参与判定)— 防小样本误杀', category: 'governance', min: 3, max: 1000 },
|
|
814
|
+
{ key: 'governance_auto_deactivate_cron_hours', value: '24', type: 'number', description: 'auto-deactivate cron 扫描间隔(小时)', category: 'governance', min: 1, max: 168 },
|
|
815
|
+
// 2026-06-02 task #1093 stage 6:arbitrator pause/resume auto-judge clock(playbook §2.1)
|
|
816
|
+
{ key: 'arbitration_max_pause_hours', value: '168', type: 'number', description: 'arbitrator 暂停自动判定时钟的最大窗口(小时)— playbook §2.1 防无限拖延', category: 'governance', min: 24, max: 720 },
|
|
769
817
|
// 2026-05-23 Agent 治理 — Trust 阶梯 rate limit(per minute)
|
|
770
818
|
// 默认值偏宽松(人类正常浏览也走 /api/*);DAO 治理可逐档收紧
|
|
771
819
|
{ key: 'agent_rate_new_per_min', value: '120', type: 'number', description: 'new 级 agent 速率:每分钟最多调用次数', category: 'limit', min: 10, max: 1000 },
|
|
@@ -776,6 +824,24 @@ const DEFAULT_PARAMS = [
|
|
|
776
824
|
// cell_precision_deg: 经纬度截断精度(0.1° ≈ 11km × 11km;0.05° ≈ 5.5km;0.5° ≈ 55km)
|
|
777
825
|
{ key: 'nearby_cell_precision_deg', value: '0.1', type: 'number', description: '雷达扫描 cell 精度(度)— 越小越精细但匹配人数变少。0.1=11km / 0.05=5.5km / 0.5=55km', category: 'privacy', min: 0.05, max: 1.0 },
|
|
778
826
|
{ key: 'nearby_k_anonymity', value: '3', type: 'number', description: '雷达扫描 k-匿名阈值(≥ N 人才显示聚合数据)', category: 'privacy', min: 3, max: 50 },
|
|
827
|
+
// 2026-06-02 W3.5-B:治理岗位上岗参数(docs/GOVERNANCE-ONBOARDING.md §2 + §6)
|
|
828
|
+
{ key: 'governance_onboarding.min_registration_days', value: '30', type: 'number', description: '申请治理岗位前最少注册天数', category: 'governance', min: 0, max: 365 },
|
|
829
|
+
{ key: 'governance_onboarding.min_completed_orders', value: '5', type: 'number', description: '申请前最少完成订单数', category: 'governance', min: 0, max: 100 },
|
|
830
|
+
{ key: 'governance_onboarding.arbitrator_min_reputation', value: '95', type: 'number', description: '申请 arbitrator 最低 reputation', category: 'governance', min: 0, max: 100 },
|
|
831
|
+
{ key: 'governance_onboarding.verifier_min_reputation', value: '90', type: 'number', description: '申请 verifier 最低 reputation', category: 'governance', min: 0, max: 100 },
|
|
832
|
+
{ key: 'governance_onboarding.role_switch_cooldown_days', value: '30', type: 'number', description: '卸任后再申请同角色冷却天数', category: 'governance', min: 0, max: 365 },
|
|
833
|
+
{ key: 'governance_onboarding.consent_delay_seconds', value: '8', type: 'number', description: '同意勾选反诱导延迟秒数(借鉴 RFC-002 §3.3)', category: 'governance', min: 0, max: 60 },
|
|
834
|
+
{ key: 'governance_onboarding.quiz_pass_score', value: '80', type: 'number', description: 'onboarding 题目合格分数线(百分制)', category: 'governance', min: 50, max: 100 },
|
|
835
|
+
// 2026-06-02 #1094 audit: arbitration.outlier_threshold_count/pct DELETED — playbook §6.2 明确
|
|
836
|
+
// "outlier 标记仅作信号,无触发,无 protocol_params"。两个 key 直接违反 spec(stage 5 阶段已用
|
|
837
|
+
// governance_auto_deactivate_* 取代,锚 confirmed_wrong 而非 outlier)。完整 audit 见
|
|
838
|
+
// docs/PROTOCOL-PARAMS-AUDIT.md。
|
|
839
|
+
{ key: 'arbitration.escalation_amount_threshold', value: '1000', type: 'number', description: '触发多 arbitrator 联审的 dispute_amount 阈值(WAZ)— phase B 实施,phase A 装饰', category: 'governance', min: 100, max: 100000 },
|
|
840
|
+
// 2026-06-03 task #1095:CHARTER §4 I-4 宪法级修改保护(去人格化)
|
|
841
|
+
// category='constitutional' 的 param 触发 only-increase 锁(防"先松保护再改一切")
|
|
842
|
+
// 假设:这两个 param 都满足"increase = more protection"语义(见 admin-protocol-params.ts 头部注释)
|
|
843
|
+
{ key: 'constitutional_supermajority_ratio', value: '0.667', type: 'number', description: 'CHARTER §4 I-4:宪法级修改超级多数比例(phase A: user solo 1-of-1;phase B+: maintainer 多签 ratio)— only-increase 防绕过', category: 'constitutional', min: 0.5, max: 1.0 },
|
|
844
|
+
{ key: 'constitutional_notice_days', value: '60', type: 'number', description: 'CHARTER §4 I-4:宪法级修改 RFC 公示期(天)— only-increase 防绕过', category: 'constitutional', min: 30, max: 365 },
|
|
779
845
|
];
|
|
780
846
|
for (const p of DEFAULT_PARAMS) {
|
|
781
847
|
try {
|
|
@@ -790,6 +856,27 @@ for (const p of DEFAULT_PARAMS) {
|
|
|
790
856
|
}
|
|
791
857
|
catch { }
|
|
792
858
|
}
|
|
859
|
+
// 2026-06-02 #1094 audit:清除 spec violation 的遗留 keys(stage 5 用 governance_auto_deactivate_* 取代)
|
|
860
|
+
// playbook §6.2:"outlier 标记仅作信号,无触发,无 protocol_params"
|
|
861
|
+
try {
|
|
862
|
+
db.prepare(`DELETE FROM protocol_params WHERE key IN ('arbitration.outlier_threshold_count', 'arbitration.outlier_threshold_pct')`).run();
|
|
863
|
+
}
|
|
864
|
+
catch { }
|
|
865
|
+
// 2026-06-03 task #1097: boot guard — 所有 category='constitutional' 的 param 必须 type='number'
|
|
866
|
+
// 理由:admin-protocol-params.ts only-increase hook 假设 "increase = more protection",
|
|
867
|
+
// 当前仅对 type='number' 生效。若有 bool 或 string constitutional param 被加入但未显式 override,
|
|
868
|
+
// only-increase 锁会**静默失效**,导致 CHARTER §4 I-4 防绕过被绕过。
|
|
869
|
+
// 这里 boot 时主动 assert,迫使加 param 的人 evaluate semantics 或显式扩展 hook。
|
|
870
|
+
;
|
|
871
|
+
(() => {
|
|
872
|
+
const offenders = DEFAULT_PARAMS.filter(p => p.category === 'constitutional' && p.type !== 'number');
|
|
873
|
+
if (offenders.length > 0) {
|
|
874
|
+
const list = offenders.map(p => `${p.key}(type=${p.type})`).join(', ');
|
|
875
|
+
throw new Error(`[#1097 boot guard] constitutional params must be type='number' for only-increase hook to apply. ` +
|
|
876
|
+
`Offenders: ${list}. ` +
|
|
877
|
+
`Either change type to 'number', or move to a non-constitutional category, or extend the hook in admin-protocol-params.ts to cover this type.`);
|
|
878
|
+
}
|
|
879
|
+
})();
|
|
793
880
|
// 2026-05-25 #1006:三个铁律节点默认值从 0 升级到 1(spec §4)
|
|
794
881
|
// 幂等迁移:仅对 admin 未显式调整过的行(updated_by IS NULL)启用强制
|
|
795
882
|
// 已有 admin 显式设置 0 的不覆盖(防意外覆写明确的关闭决策)
|
|
@@ -2265,12 +2352,12 @@ db.exec(`
|
|
|
2265
2352
|
)
|
|
2266
2353
|
`);
|
|
2267
2354
|
// 慈善基金(单例 id='main')
|
|
2268
|
-
//
|
|
2355
|
+
// 2026-06-04 起【纯净】:仅服务慈善许愿板块(捐款/还愿/拨款),不再承接任何佣金兜底。
|
|
2269
2356
|
// total_donated — 用户主动捐款
|
|
2270
2357
|
// total_redirected — 还愿转入(原义保留)
|
|
2271
|
-
// total_chain_gap — 自发现订单 commission L2/L3 空缺兜底("无人引流→公益")
|
|
2272
|
-
// total_orphan_sponsor — 孤儿注册 sponsor 链兜底("无推荐人→公益")
|
|
2273
2358
|
// total_disbursed — 累计已 disburse(出金)
|
|
2359
|
+
// total_chain_gap / total_orphan_sponsor / total_region_cap 三列为历史遗留(解耦前佣金兜底曾入此),
|
|
2360
|
+
// 现已停写、仅作历史审计;新佣金兜底全部入 commission_reserve(三级公池,见下)。
|
|
2274
2361
|
db.exec(`
|
|
2275
2362
|
CREATE TABLE IF NOT EXISTS charity_fund (
|
|
2276
2363
|
id TEXT PRIMARY KEY,
|
|
@@ -2293,12 +2380,9 @@ for (const stmt of [
|
|
|
2293
2380
|
}
|
|
2294
2381
|
catch { /* 已存在 */ }
|
|
2295
2382
|
}
|
|
2296
|
-
//
|
|
2297
|
-
// kind 全集(Phase A 扩展):
|
|
2383
|
+
// 资金流水(2026-06-04 起 charity 纯净,仅以下 3 类;redirect_* 历史 kind 不再新写):
|
|
2298
2384
|
// donation — 用户主动捐款
|
|
2299
2385
|
// repay_redirect — 还愿转入(不可达原施善人或主动选 fund)
|
|
2300
|
-
// redirect_chain_gap — 自发现订单 commission 兜底(L2/L3 缺失)
|
|
2301
|
-
// redirect_orphan_sponsor — 孤儿注册兜底
|
|
2302
2386
|
// disburse — 出金(拨付/还愿 grant 等)
|
|
2303
2387
|
db.exec(`
|
|
2304
2388
|
CREATE TABLE IF NOT EXISTS charity_fund_txns (
|
|
@@ -2330,6 +2414,46 @@ try {
|
|
|
2330
2414
|
db.exec("CREATE INDEX IF NOT EXISTS idx_cft_order ON charity_fund_txns(related_order_id)");
|
|
2331
2415
|
}
|
|
2332
2416
|
catch { }
|
|
2417
|
+
// ─── 三级公池 / 佣金储备(commission_reserve,单例 id='main')─────────────
|
|
2418
|
+
// 2026-06-04:佣金兜底科目从 charity_fund 拆出,慈善科目自此【纯净】只服务慈善许愿板块。
|
|
2419
|
+
// 三级佣金中【无资格人 / 无资格 / 区域档位截断 / max=0 整池 / opt-out 放弃 / escrow 到期】
|
|
2420
|
+
// 的部分,统一入此【独立科目】。定位 = 协议储备,**只进不出**,用途由治理(DAO/创始人)决定。
|
|
2421
|
+
// 与 global_fund(PV 资金,由 1% base 注资发对碰) 互不流通 —— 三套科目彻底独立。
|
|
2422
|
+
// 命名注意:本表是【持久储备科目】,区别于每单的 commission_pool/commissionPool(= total×rate 预算变量)。
|
|
2423
|
+
// total_chain_gap — L2/L3 空缺(自发现/上家断链)
|
|
2424
|
+
// total_orphan_sponsor — sponsor 被封/无资格 + opt-out 主动放弃 + escrow 到期(无合格受益人桶)
|
|
2425
|
+
// total_region_cap — level>maxLevels 区域档位截断 + max_levels=0 整池
|
|
2426
|
+
db.exec(`
|
|
2427
|
+
CREATE TABLE IF NOT EXISTS commission_reserve (
|
|
2428
|
+
id TEXT PRIMARY KEY,
|
|
2429
|
+
balance REAL DEFAULT 0,
|
|
2430
|
+
total_chain_gap REAL DEFAULT 0,
|
|
2431
|
+
total_orphan_sponsor REAL DEFAULT 0,
|
|
2432
|
+
total_region_cap REAL DEFAULT 0,
|
|
2433
|
+
total_disbursed REAL DEFAULT 0,
|
|
2434
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2435
|
+
)
|
|
2436
|
+
`);
|
|
2437
|
+
db.prepare("INSERT OR IGNORE INTO commission_reserve (id) VALUES ('main')").run();
|
|
2438
|
+
db.exec(`
|
|
2439
|
+
CREATE TABLE IF NOT EXISTS commission_reserve_txns (
|
|
2440
|
+
id TEXT PRIMARY KEY,
|
|
2441
|
+
kind TEXT NOT NULL,
|
|
2442
|
+
from_user_id TEXT,
|
|
2443
|
+
amount REAL NOT NULL,
|
|
2444
|
+
related_order_id TEXT,
|
|
2445
|
+
note TEXT,
|
|
2446
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2447
|
+
)
|
|
2448
|
+
`);
|
|
2449
|
+
try {
|
|
2450
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_crt_kind ON commission_reserve_txns(kind, created_at DESC)");
|
|
2451
|
+
}
|
|
2452
|
+
catch { }
|
|
2453
|
+
try {
|
|
2454
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_crt_order ON commission_reserve_txns(related_order_id)");
|
|
2455
|
+
}
|
|
2456
|
+
catch { }
|
|
2333
2457
|
// 还愿记录
|
|
2334
2458
|
db.exec(`
|
|
2335
2459
|
CREATE TABLE IF NOT EXISTS wish_repayments (
|
|
@@ -2484,6 +2608,9 @@ for (const stmt of [
|
|
|
2484
2608
|
'ALTER TABLE users ADD COLUMN total_left_pv REAL DEFAULT 0',
|
|
2485
2609
|
'ALTER TABLE users ADD COLUMN total_right_pv REAL DEFAULT 0',
|
|
2486
2610
|
'ALTER TABLE users ADD COLUMN pv_dirty_at TEXT',
|
|
2611
|
+
// 二叉树左右【整棵子树】人数 — 增量维护(joinPowerLeg 上溯 +1), pickPreferredSide team_count O(1) 读
|
|
2612
|
+
'ALTER TABLE users ADD COLUMN left_count INTEGER DEFAULT 0',
|
|
2613
|
+
'ALTER TABLE users ADD COLUMN right_count INTEGER DEFAULT 0',
|
|
2487
2614
|
// V3 用户成长等级(基于历史累积 score = 历史累积 WAZ 收益)
|
|
2488
2615
|
'ALTER TABLE users ADD COLUMN lifetime_score REAL DEFAULT 0',
|
|
2489
2616
|
]) {
|
|
@@ -2562,6 +2689,13 @@ try {
|
|
|
2562
2689
|
db.prepare('INSERT OR IGNORE INTO global_fund (id) VALUES (1)').run();
|
|
2563
2690
|
}
|
|
2564
2691
|
catch { }
|
|
2692
|
+
// 2026-06-04 (#1106):PV escrow 隔离负债账。结算时给"已承诺但 opt-out 待激活"的 PV 奖励
|
|
2693
|
+
// 从 pool_balance 移入此列(不再留在可分配池中被后续周期发给别人);
|
|
2694
|
+
// 兑付(opt-in)从此列出,到期退回 pool_balance。守恒:pool + pv_escrow_reserve + wallets = 常量。
|
|
2695
|
+
try {
|
|
2696
|
+
db.exec('ALTER TABLE global_fund ADD COLUMN pv_escrow_reserve REAL DEFAULT 0');
|
|
2697
|
+
}
|
|
2698
|
+
catch { /* 已存在 */ }
|
|
2565
2699
|
db.exec(`
|
|
2566
2700
|
CREATE TABLE IF NOT EXISTS management_bonus_pool (
|
|
2567
2701
|
id INTEGER PRIMARY KEY CHECK(id=1),
|
|
@@ -3040,6 +3174,38 @@ try {
|
|
|
3040
3174
|
db.prepare("UPDATE users SET placement_pref = 'team_count' WHERE placement_pref IN ('left','right')").run();
|
|
3041
3175
|
}
|
|
3042
3176
|
catch { }
|
|
3177
|
+
// 增量计数 backfill(一次性,幂等):从现有 placement 树重算 left_count/right_count。
|
|
3178
|
+
// 新装 / 空树 = no-op。有历史 placement 的库(local/已运行)首次启动重算一次。
|
|
3179
|
+
// 2026-06-04 引入 left_count/right_count 增量字段时的迁移。
|
|
3180
|
+
try {
|
|
3181
|
+
const done = db.prepare("SELECT value FROM system_state WHERE key = 'placement_count_backfilled'").get()?.value === '1';
|
|
3182
|
+
if (!done) {
|
|
3183
|
+
const placed = db.prepare("SELECT id, placement_id, placement_side FROM users WHERE placement_id IS NOT NULL").all();
|
|
3184
|
+
const bf = db.transaction(() => {
|
|
3185
|
+
db.exec("UPDATE users SET left_count = 0, right_count = 0");
|
|
3186
|
+
for (const p of placed) {
|
|
3187
|
+
let upParent = p.placement_id;
|
|
3188
|
+
let upSide = p.placement_side;
|
|
3189
|
+
let safety = 10_000;
|
|
3190
|
+
while (upParent && safety-- > 0) {
|
|
3191
|
+
const col = upSide === 'left' ? 'left_count' : 'right_count';
|
|
3192
|
+
db.prepare(`UPDATE users SET ${col} = ${col} + 1 WHERE id = ?`).run(upParent);
|
|
3193
|
+
const pr = db.prepare("SELECT placement_id, placement_side FROM users WHERE id = ?").get(upParent);
|
|
3194
|
+
if (!pr?.placement_id)
|
|
3195
|
+
break;
|
|
3196
|
+
upSide = pr.placement_side || 'left';
|
|
3197
|
+
upParent = pr.placement_id;
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
});
|
|
3201
|
+
bf();
|
|
3202
|
+
db.prepare("INSERT OR REPLACE INTO system_state (key, value) VALUES ('placement_count_backfilled', '1')").run();
|
|
3203
|
+
console.log(`[placement_count] backfill 完成,重算 ${placed.length} 个已挂载节点`);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
catch (e) {
|
|
3207
|
+
console.error('[placement_count backfill]', e.message);
|
|
3208
|
+
}
|
|
3043
3209
|
// ─── 身份码派生 helpers ─────────────────────────────────
|
|
3044
3210
|
// permanent_code:6 位 Crockford base32(去歧义字符 I L O U),永久唯一,不可改
|
|
3045
3211
|
// 32 字母表:0-9 + ABCDEFGHJKMNPQRSTVWXYZ
|
|
@@ -3279,6 +3445,23 @@ catch { }
|
|
|
3279
3445
|
}
|
|
3280
3446
|
catch { }
|
|
3281
3447
|
});
|
|
3448
|
+
// ─── P0-2 PRE-LAUNCH 全局 clamp:max_levels ≤ 1(2026-06-03 user decision B)───
|
|
3449
|
+
// 上方按辖区分档的 seed(0/1/2/3)是【知识/基础设施】,保留不删。
|
|
3450
|
+
// 但 pre-launch 阶段【未请律师】,operator 明确"max_levels ≤ 1 everywhere 是不请律师
|
|
3451
|
+
// 也安全的唯一前提"。因此强制把所有地区压到 ≤ 1(0 保持 0,更严不动)。
|
|
3452
|
+
// 这是【唯一的 pre-launch 合规闸门】—— 单点可逆:
|
|
3453
|
+
// re-trigger(见 docs/LEGAL-DISCLOSURES.md §7):真实用户 > 100 / GMV > $10k /
|
|
3454
|
+
// 首次监管 inquiry / 进入新辖区 / Phase D 上线 —— 届时【请律师背书后】删除本块,
|
|
3455
|
+
// 即按上方 seed 的辖区分档自动放开。
|
|
3456
|
+
// display == enforcement:clamp DB 值(而非仅运行时 cap),admin/UI 显示值 = 实际生效值。
|
|
3457
|
+
try {
|
|
3458
|
+
db.exec("UPDATE region_config SET max_levels = 1 WHERE max_levels > 1");
|
|
3459
|
+
}
|
|
3460
|
+
catch { }
|
|
3461
|
+
try {
|
|
3462
|
+
db.exec("UPDATE region_config SET mlm_ui_visible = 0 WHERE max_levels = 0");
|
|
3463
|
+
}
|
|
3464
|
+
catch { }
|
|
3282
3465
|
// 卖家发新品配额(模块 A)
|
|
3283
3466
|
for (const stmt of [
|
|
3284
3467
|
'ALTER TABLE users ADD COLUMN max_products INTEGER DEFAULT 200',
|
|
@@ -5064,7 +5247,63 @@ function endpointToAction(method, path) {
|
|
|
5064
5247
|
return 'rfq';
|
|
5065
5248
|
if (method === 'POST' && /^\/api\/auctions\/[^/]+\/bid/.test(path))
|
|
5066
5249
|
return 'bid';
|
|
5067
|
-
|
|
5250
|
+
// 权限审计 #1115 P0:花钱/价值写纳入问责门(与 place_order 同档:无 Passkey 须声明 scope)。
|
|
5251
|
+
// 之前这些漏在 default-allow 之外,无声明无 Passkey 的 agent 可花掉账户余额 —— 补齐一致性。
|
|
5252
|
+
if (method === 'POST' && /^\/api\/skill-market\/[^/]+\/purchase/.test(path))
|
|
5253
|
+
return 'purchase';
|
|
5254
|
+
if (method === 'POST' && /^\/api\/secondhand\/[^/]+\/order/.test(path))
|
|
5255
|
+
return 'buy_secondhand';
|
|
5256
|
+
if (method === 'POST' && /^\/api\/group-buys\/[^/]+\/join/.test(path))
|
|
5257
|
+
return 'group_buy_join';
|
|
5258
|
+
// #1115 P1:写 PII(收货地址)也需问责。含 addresses 增删改 + profile 默认地址。
|
|
5259
|
+
if (method !== 'GET' && (/^\/api\/addresses(\/|$)/.test(path) || path === '/api/profile/default-address'))
|
|
5260
|
+
return 'set_address';
|
|
5261
|
+
// #1115 P2:钱包写(转出/充值/白名单/连接)统一纳入 'wallet' 问责门。
|
|
5262
|
+
// 之前 wallet/* 整段在 SAFE 放行 → api_key agent 可直达 withdraw,小额零真人门分批盗刷;
|
|
5263
|
+
// 现要求声明 'wallet' scope(或 '*' / Passkey)。withdraw 另在 handler 内强制 Passkey 真人门(铁律,见 wallet-write.ts)。
|
|
5264
|
+
if (method !== 'GET' && /^\/api\/wallet\//.test(path))
|
|
5265
|
+
return 'wallet';
|
|
5266
|
+
// #1115 P2:profile **PII/身份/接管向量**写纳入 'set_profile' —— 仅这一子集需问责。
|
|
5267
|
+
// 改恢复邮箱(bind/confirm-email)=账户接管;改 handle/name=身份;set/clear-location=地理 PII。
|
|
5268
|
+
// ⚠️ 不一刀切整段 profile/*:role/region/placement/password/verify 是"无 Passkey 也要能用"的
|
|
5269
|
+
// 自助操作(见原则:没身份验证也要解决的问题不要求身份),它们留在下方 SAFE。
|
|
5270
|
+
// default-address 已在上面归 set_address(更具体,优先)。
|
|
5271
|
+
if (method !== 'GET' && /^\/api\/profile\/(bind-email|confirm-email|change-handle|change-name|set-location|clear-location)$/.test(path))
|
|
5272
|
+
return 'set_profile';
|
|
5273
|
+
// ── #1115 B4 结构性:default-deny ──────────────────────────────
|
|
5274
|
+
// 上面是细粒度命名 token;下面 safe-list 放行(返回 null),其余一切写 → 'write'(需 Passkey 或声明 scope)。
|
|
5275
|
+
// 目的:新增的敏感写默认就被门控,不会因"忘了加映射"而裸奔(把 default-allow 翻成 default-deny)。
|
|
5276
|
+
// safe-list 三类必须放行(否则误伤 / 死锁):
|
|
5277
|
+
// (a) 脱困入口:绑 Passkey(webauthn)/ 声明 scope(me/agents)/ 登录注册/找回 —— 若门控则永远无法获得通行资格
|
|
5278
|
+
// (b) 自带门:build-feedback(分级门)/ admin(requireAdmin)。
|
|
5279
|
+
// 注意:wallet / profile 不再 SAFE —— #1115 P2 移到上方命名 token(wallet / set_profile / set_address)。
|
|
5280
|
+
// (c) 公开 + 低值自我状态写(管理自己的 cart/wishlist/通知/关注 等,登录即可)
|
|
5281
|
+
const SAFE = [
|
|
5282
|
+
// (a) 脱困 / 鉴权 / 身份(bootstrap 通行资格所必需 —— wallet 与 profile 的 PII 子集不在此列,见上方命名 token)
|
|
5283
|
+
/^\/api\/(login|register)$/, /^\/api\/recover-key/, /^\/api\/webauthn\//,
|
|
5284
|
+
/^\/api\/me\/agents\//,
|
|
5285
|
+
// profile 自助子集(无 Passkey 真人也要能用:切角色/选地区/PV 配置/改密码/二次确认)。
|
|
5286
|
+
// PII/身份子集(bind-email/change-handle/set-location 等)已在上方归 set_profile,不在此放行。
|
|
5287
|
+
/^\/api\/profile\/(switch-role|add-role|region|placement-pref|bind-placement|feed-visible|verify-password|set-password|remove-password)$/,
|
|
5288
|
+
// (b) 自带门(build-feedback=分级门;build-tasks=协调层登录即可;admin=requireAdmin。wallet/profile 已移到上方命名 token #1115 P2)
|
|
5289
|
+
/^\/api\/build-feedback/, /^\/api\/build-tasks/, /^\/api\/admin\//,
|
|
5290
|
+
// (c) 公开 / 无需登录
|
|
5291
|
+
/^\/api\/(public-ideas|error-report|mcp-telemetry|email-subscriptions|search-by-link|feedback)(\/|$)/,
|
|
5292
|
+
// (c) 低值自我状态
|
|
5293
|
+
/^\/api\/cart$/, /^\/api\/cart\/(?!checkout)[^/]+$/,
|
|
5294
|
+
/^\/api\/wishlist/, /^\/api\/products\/[^/]+\/waitlist$/,
|
|
5295
|
+
/^\/api\/notifications\/read$/, /^\/api\/announcements\/[^/]+\/read$/,
|
|
5296
|
+
/^\/api\/follows\//, /^\/api\/blocklist\//,
|
|
5297
|
+
/^\/api\/checkin$/, /^\/api\/growth\/tasks\//, /^\/api\/tasks\/[^/]+\/claim$/,
|
|
5298
|
+
/^\/api\/push\//, /^\/api\/auth\//,
|
|
5299
|
+
/^\/api\/me\/(delete-cancel|notify-claim-tasks)/,
|
|
5300
|
+
/^\/api\/peers\//, /^\/api\/signaling\//,
|
|
5301
|
+
/^\/api\/product-share\/touch$/, /^\/api\/anchor\/[^/]+\/touch$/,
|
|
5302
|
+
/^\/api\/reviews\//,
|
|
5303
|
+
];
|
|
5304
|
+
if (SAFE.some(r => r.test(path)))
|
|
5305
|
+
return null;
|
|
5306
|
+
return 'write'; // 默认拒绝:其余写需问责
|
|
5068
5307
|
}
|
|
5069
5308
|
// Phase 3b(B1):敏感读 → read-scope token 映射。只挑「跨用户聚合 / 批量扫描」这类剽窃向读取,
|
|
5070
5309
|
// 普通读(自己的 profile / products 列表 / 订单等)一律 null 不约束,避免误伤声明 agent 的日常读。
|
|
@@ -5522,6 +5761,18 @@ registerAgentReputationRoutes(app, {
|
|
|
5522
5761
|
});
|
|
5523
5762
|
// #1013 Phase 47: 6 公开用户主页 endpoints 已迁出到 routes/users-public.ts
|
|
5524
5763
|
registerUsersPublicRoutes(app, { db, auth, noteAuthenticityBadges });
|
|
5764
|
+
// RFC-004 build_feedback — agent-native "use → build" 反馈管道
|
|
5765
|
+
registerBuildFeedbackRoutes(app, {
|
|
5766
|
+
db, auth,
|
|
5767
|
+
requireSupportAdmin: (req, res) => requireAdminPermission(req, res, 'support'),
|
|
5768
|
+
});
|
|
5769
|
+
// RFC-006 Gap 1:协调层(build_tasks "谁在做什么")
|
|
5770
|
+
registerBuildTasksRoutes(app, {
|
|
5771
|
+
db, auth,
|
|
5772
|
+
requireSupportAdmin: (req, res) => requireAdminPermission(req, res, 'support'),
|
|
5773
|
+
});
|
|
5774
|
+
// RFC-006 Gap 2:贡献者自查看板(build_reputation 独立池)
|
|
5775
|
+
registerBuildReputationRoutes(app, { db, auth });
|
|
5525
5776
|
// #1013 Phase 48: 3 auth/sessions endpoints 已迁出到 routes/auth-sessions.ts
|
|
5526
5777
|
registerAuthSessionsRoutes(app, { db, auth, verifyPassword, recordSession, generateSecureKey });
|
|
5527
5778
|
// 个人资料:查看 API Key + 联系方式
|
|
@@ -6438,27 +6689,13 @@ registerAdminReportsRoutes(app, {
|
|
|
6438
6689
|
});
|
|
6439
6690
|
// ─── 原子能轨道(Phase 2):双轨树挂靠 ───────────────────────
|
|
6440
6691
|
const PV_PROPAGATION_DEPTH_LIMIT = 5000; // PV 累积深度上限(admin 可调,存 system_state 之后)
|
|
6441
|
-
//
|
|
6442
|
-
|
|
6443
|
-
const childField = side === 'left' ? 'left_child_id' : 'right_child_id';
|
|
6444
|
-
let count = 0;
|
|
6445
|
-
let current = rootId;
|
|
6446
|
-
let safety = 10_000;
|
|
6447
|
-
while (safety-- > 0) {
|
|
6448
|
-
const row = db.prepare(`SELECT ${childField} FROM users WHERE id = ?`).get(current);
|
|
6449
|
-
const next = row?.[childField];
|
|
6450
|
-
if (!next)
|
|
6451
|
-
break;
|
|
6452
|
-
count++;
|
|
6453
|
-
current = next;
|
|
6454
|
-
}
|
|
6455
|
-
return count;
|
|
6456
|
-
}
|
|
6692
|
+
// (2026-06-04 移除 countSubtreeUsers — 旧实现只数单条脊链, 名实不符;
|
|
6693
|
+
// team_count 改读增量维护的 users.left_count/right_count, 见 pickPreferredSide + joinPowerLeg)
|
|
6457
6694
|
// 根据 inviter 偏好自动选边(链接不带 side 时用)
|
|
6458
6695
|
// 支持 2 档:team_count(默认,下线人数少)/ pv_count(近 90 天 PV 累计少)
|
|
6459
6696
|
// 兼容 legacy: left/right 视为 team_count(启动时会被静默迁移;这里防御性兜底)
|
|
6460
6697
|
function pickPreferredSide(inviterId) {
|
|
6461
|
-
const u = db.prepare("SELECT placement_pref, total_left_pv, total_right_pv FROM users WHERE id = ?")
|
|
6698
|
+
const u = db.prepare("SELECT placement_pref, total_left_pv, total_right_pv, left_count, right_count FROM users WHERE id = ?")
|
|
6462
6699
|
.get(inviterId);
|
|
6463
6700
|
const pref = u?.placement_pref || 'team_count';
|
|
6464
6701
|
if (pref === 'pv_count') {
|
|
@@ -6471,9 +6708,11 @@ function pickPreferredSide(inviterId) {
|
|
|
6471
6708
|
const rightPv = Number(u?.total_right_pv ?? 0) + Number(w.r);
|
|
6472
6709
|
return leftPv <= rightPv ? 'left' : 'right';
|
|
6473
6710
|
}
|
|
6474
|
-
// team_count
|
|
6475
|
-
|
|
6476
|
-
|
|
6711
|
+
// team_count(默认):左右【整棵子树】人数,挂少的一边。
|
|
6712
|
+
// 读增量维护的 left_count/right_count(O(1))。2026-06-04 修:旧实现 countSubtreeUsers
|
|
6713
|
+
// 只数单条脊链(不含旁支子树),名实不符 → 病毒增长下选边失真。改为增量全子树计数。
|
|
6714
|
+
const lCount = Number(u?.left_count ?? 0);
|
|
6715
|
+
const rCount = Number(u?.right_count ?? 0);
|
|
6477
6716
|
return lCount <= rCount ? 'left' : 'right';
|
|
6478
6717
|
}
|
|
6479
6718
|
/**
|
|
@@ -6501,6 +6740,20 @@ function joinPowerLeg(inviterId, side, newUserId) {
|
|
|
6501
6740
|
db.prepare(`UPDATE users SET ${childField} = ? WHERE id = ?`).run(newUserId, current);
|
|
6502
6741
|
db.prepare(`UPDATE users SET placement_id = ?, placement_side = ?, placement_path = ?, placement_depth = ? WHERE id = ?`)
|
|
6503
6742
|
.run(current, side, newPath, newDepth, newUserId);
|
|
6743
|
+
// 增量维护 left_count/right_count:从新人上溯,每个祖先的对应腿 +1(整棵子树计数,与 total_*_pv 同模式)。
|
|
6744
|
+
// 首轮:current 的 [side] 腿 +1(新人直接落在 current 的 side 侧);逐级上溯,按各节点 placement_side 归边。
|
|
6745
|
+
let upParent = current;
|
|
6746
|
+
let upSide = side;
|
|
6747
|
+
let safety = 10_000;
|
|
6748
|
+
while (upParent && safety-- > 0) {
|
|
6749
|
+
const col = upSide === 'left' ? 'left_count' : 'right_count';
|
|
6750
|
+
db.prepare(`UPDATE users SET ${col} = ${col} + 1 WHERE id = ?`).run(upParent);
|
|
6751
|
+
const pr = db.prepare("SELECT placement_id, placement_side FROM users WHERE id = ?").get(upParent);
|
|
6752
|
+
if (!pr?.placement_id)
|
|
6753
|
+
break;
|
|
6754
|
+
upSide = pr.placement_side || 'left';
|
|
6755
|
+
upParent = pr.placement_id;
|
|
6756
|
+
}
|
|
6504
6757
|
return { tail: current, depth: newDepth };
|
|
6505
6758
|
});
|
|
6506
6759
|
return place();
|
|
@@ -6585,6 +6838,9 @@ function runBinarySettlement() {
|
|
|
6585
6838
|
const now = new Date().toISOString();
|
|
6586
6839
|
const periodStart = new Date(Date.now() - 7 * 86400_000).toISOString();
|
|
6587
6840
|
for (const u of dirty) {
|
|
6841
|
+
// 注:对碰【分数照常计算累积】,不在此 gate region。PV 经济闸在【兑付】层
|
|
6842
|
+
// (score → WAZ 时按 region pv_enabled 决定是否真实发放;未开启则分数挂账,
|
|
6843
|
+
// 待辖区开启 / 用户迁移到可用辖区再兑现)。2026-06-04 解耦设计。
|
|
6588
6844
|
// 周期内已得 Score(paranoia 封顶)
|
|
6589
6845
|
const weekScore = db.prepare(`SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND created_at > ?`).get(u.id, periodStart).s;
|
|
6590
6846
|
if (weekScore >= SINGLE_USER_PERIOD_CAP) {
|
|
@@ -6620,7 +6876,14 @@ function executeSafeSettlementCron() {
|
|
|
6620
6876
|
return;
|
|
6621
6877
|
}
|
|
6622
6878
|
// 2. 全网当期 pending Score 汇总
|
|
6623
|
-
|
|
6879
|
+
// 2026-06-04 解耦:PV 隐藏不关闭 —— 分数照常计算累积。兑付层按 earner【当前 region】
|
|
6880
|
+
// 的 pv_enabled 过滤:未开启的 earner 分数【保留 pending(不入本期分配、不稀释合格者单价)】,
|
|
6881
|
+
// 待辖区开启 / 用户迁移到可用辖区,下期自动兑现。挂账的钱按实际发放扣减(见步骤7)自然留池。
|
|
6882
|
+
const allPending = db.prepare(`SELECT id, user_id, score FROM binary_score_records WHERE settled_at IS NULL`).all();
|
|
6883
|
+
const pending = allPending.filter(r => {
|
|
6884
|
+
const reg = db.prepare("SELECT region FROM users WHERE id = ?").get(r.user_id)?.region || 'global';
|
|
6885
|
+
return regionPvEnabled(reg);
|
|
6886
|
+
});
|
|
6624
6887
|
if (pending.length === 0) {
|
|
6625
6888
|
result.status = 'no_pending';
|
|
6626
6889
|
return;
|
|
@@ -6705,6 +6968,37 @@ function executeSafeSettlementCron() {
|
|
|
6705
6968
|
}
|
|
6706
6969
|
// V1:1 WAZ = 1 元(场景 B 杠杆延后)
|
|
6707
6970
|
const wazAmount = cashDue;
|
|
6971
|
+
// RFC-002 §3.5 PV-pair opt-in gate (PR-1c-b) — symmetric to settleCommission L1/L2/L3 gate
|
|
6972
|
+
// opted-in → normal wallet credit
|
|
6973
|
+
// opted-out + 'deactivate' → 不发放,waz 留在 PV 资金池(forfeit,非慈善)
|
|
6974
|
+
// opted-out + other → escrow(pv_pair),金额从 pool 移入 pv_escrow_reserve(#1106 隔离负债)
|
|
6975
|
+
// 2026-06-04 修双计 bug:deactivate 旧版 redirectToCharity 新增慈善余额,但 waz 因不计入
|
|
6976
|
+
// cashDistributed 已留在 pool → 钱印了两份。现 deactivate 仅标记 settled,钱留 PV 资金池。
|
|
6977
|
+
const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(r.user_id)?.rewards_opted_in ?? 0;
|
|
6978
|
+
if (optIn !== 1) {
|
|
6979
|
+
const lastAction = db.prepare("SELECT action FROM rewards_applications WHERE user_id = ? ORDER BY created_at DESC LIMIT 1").get(r.user_id)?.action;
|
|
6980
|
+
const isEscrow = lastAction !== 'deactivate';
|
|
6981
|
+
db.transaction(() => {
|
|
6982
|
+
if (isEscrow) {
|
|
6983
|
+
// never_activated / auto_downgrade → escrow(pv_pair)。
|
|
6984
|
+
// #1106:把 waz 从可分配池移入 pv_escrow_reserve(隔离负债),避免后续周期把这笔预留发给别人、
|
|
6985
|
+
// 到兑付时池里没钱却仍给钱包加钱(凭空印钱)。兑付从 reserve 出、到期退回 pool。
|
|
6986
|
+
const escrowDays = Number(db.prepare("SELECT value FROM protocol_params WHERE key = 'rewards_opt_in.escrow_days'").get()?.value ?? 30);
|
|
6987
|
+
const nowMs = Date.now();
|
|
6988
|
+
db.prepare(`INSERT INTO pending_commission_escrow (recipient_user_id, order_id, amount, attribution_path, status, created_at, expires_at) VALUES (?, NULL, ?, 'pv_pair', 'pending', ?, ?)`)
|
|
6989
|
+
.run(r.user_id, wazAmount, nowMs, nowMs + escrowDays * 86400 * 1000);
|
|
6990
|
+
db.prepare("UPDATE global_fund SET pv_escrow_reserve = pv_escrow_reserve + ? WHERE id = 1").run(wazAmount);
|
|
6991
|
+
}
|
|
6992
|
+
// deactivate:什么都不转,waz 自然留在 pool(cashRetained)
|
|
6993
|
+
db.prepare("UPDATE binary_score_records SET settled_at = datetime('now'), waz_amount = 0 WHERE id = ?").run(r.id);
|
|
6994
|
+
})();
|
|
6995
|
+
// escrow 分支:计入 cashDistributed → 末尾 cashRetained 会把这笔从 pool 扣掉(已移入 reserve)。
|
|
6996
|
+
// deactivate 分支:不计入 → 留在 pool。
|
|
6997
|
+
if (isEscrow)
|
|
6998
|
+
cashDistributed += cashDue;
|
|
6999
|
+
// Do NOT credit wallet, lifetime_score, or count as settledUser — opt-out path
|
|
7000
|
+
continue;
|
|
7001
|
+
}
|
|
6708
7002
|
db.prepare("UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?").run(wazAmount, wazAmount, r.user_id);
|
|
6709
7003
|
db.prepare("UPDATE binary_score_records SET settled_at = datetime('now'), waz_amount = ? WHERE id = ?").run(wazAmount, r.id);
|
|
6710
7004
|
// V3 用户成长等级:累加 lifetime_score(实际兑现的 WAZ 金额,不是 pending score)
|
|
@@ -7109,6 +7403,36 @@ registerArbitratorRoutes(app, {
|
|
|
7109
7403
|
checkArbitratorEligibility, getArbitratorState, errorRes, logAdminAction,
|
|
7110
7404
|
ARB_STAKE_REQUIRED, ARB_APP_REJECT_COOLDOWN_DAYS,
|
|
7111
7405
|
});
|
|
7406
|
+
// Governance onboarding (W3.5-B #1093) — apply + quiz + cases + admin activation + resign/appeal + auto_deactivate audit
|
|
7407
|
+
registerGovernanceOnboardingRoutes(app, {
|
|
7408
|
+
db, generateId, auth, errorRes,
|
|
7409
|
+
checkArbitratorEligibility, checkVerifierEligibility,
|
|
7410
|
+
consumeGateToken, getProtocolParam,
|
|
7411
|
+
requireGovernanceAdmin: (req, res) => requireAdminPermission(req, res, 'arbitration'),
|
|
7412
|
+
logAdminAction,
|
|
7413
|
+
});
|
|
7414
|
+
// #1090 RFC-002 PR-2a: rewards apply/deactivate/status endpoints
|
|
7415
|
+
registerRewardsApplyRoutes(app, {
|
|
7416
|
+
db, auth, errorRes, consumeGateToken, getProtocolParam,
|
|
7417
|
+
});
|
|
7418
|
+
// task #1093 stage 5: admin manual auto-deactivate sweep trigger
|
|
7419
|
+
// Useful for ops + testing. The scheduled cron also runs every N hours.
|
|
7420
|
+
app.post('/api/admin/governance/run-auto-deactivate', (req, res) => {
|
|
7421
|
+
const admin = requireAdminPermission(req, res, 'arbitration');
|
|
7422
|
+
if (!admin)
|
|
7423
|
+
return;
|
|
7424
|
+
try {
|
|
7425
|
+
const result = runAutoDeactivateSweep({ db, generateId, getProtocolParam });
|
|
7426
|
+
logAdminAction(admin.id, 'governance_auto_deactivate_sweep', null, null, {
|
|
7427
|
+
scanned: result.scanned,
|
|
7428
|
+
deactivated_count: result.deactivated.length,
|
|
7429
|
+
});
|
|
7430
|
+
res.json({ success: true, ...result });
|
|
7431
|
+
}
|
|
7432
|
+
catch (e) {
|
|
7433
|
+
res.status(500).json({ error: e.message });
|
|
7434
|
+
}
|
|
7435
|
+
});
|
|
7112
7436
|
// link-challenges/verify — Phase 113 已迁出
|
|
7113
7437
|
// 2026-05-24 价格下降时通知 wishlist 中开启了价格提醒的用户
|
|
7114
7438
|
function notifyWishlistPriceDrop(productId, productTitle, oldPrice, newPrice) {
|
|
@@ -7287,6 +7611,7 @@ registerDisputesWriteRoutes(app, {
|
|
|
7287
7611
|
FUND_BASE_RATE: () => FUND_BASE_RATE(),
|
|
7288
7612
|
settleCommission, depositToFund, calculatePv,
|
|
7289
7613
|
recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend,
|
|
7614
|
+
getProtocolParam,
|
|
7290
7615
|
});
|
|
7291
7616
|
// lightAuthGuard:轻量 Authorization 头守门(在 raw 解析之前挡掉无 auth 请求)
|
|
7292
7617
|
// 被 Phase 13 shareables(视频上传)+ Phase 87 disputes evidence-blob 共享
|
|
@@ -8374,6 +8699,12 @@ function getRegionMaxLevels(region) {
|
|
|
8374
8699
|
// 已显式审计且合规允许的地区在 region_config 表里设 2 或 3
|
|
8375
8700
|
return row?.max_levels ?? 1;
|
|
8376
8701
|
}
|
|
8702
|
+
// 2026-06-04 解耦:PV 双轨/对碰系统是否在该 region 开启 —— 与 max_levels(佣金层级)独立。
|
|
8703
|
+
// 未配置 / 未知地区一律 false(保守默认 PV 全关,需治理显式开 pv_enabled=1)。
|
|
8704
|
+
function regionPvEnabled(region) {
|
|
8705
|
+
const row = db.prepare("SELECT pv_enabled FROM region_config WHERE region = ?").get(region);
|
|
8706
|
+
return Number(row?.pv_enabled ?? 0) === 1;
|
|
8707
|
+
}
|
|
8377
8708
|
// effectiveBase 可选 — partial_refund / liability_split 时按实际成交金额发放
|
|
8378
8709
|
// 默认 undefined → 用 order.total_amount(正常完成 / release_seller)
|
|
8379
8710
|
function settleCommission(orderId, effectiveBase) {
|
|
@@ -8394,11 +8725,13 @@ function settleCommission(orderId, effectiveBase) {
|
|
|
8394
8725
|
}
|
|
8395
8726
|
const maxLevels = getRegionMaxLevels(region);
|
|
8396
8727
|
const pool = Math.round(total * rate * 100) / 100;
|
|
8397
|
-
//
|
|
8728
|
+
// max_levels=0 → 完全禁 MLM,整个 commission pool 入 commission_reserve(三级公池,只进不出)
|
|
8729
|
+
// 2026-06-04 修双计 bug:旧版既 redirectToCharity 又 return redirected:pool 让 depositToFund 再入 global_fund → 印钱。
|
|
8730
|
+
// 现统一入 commission_reserve 一次,return redirected:0(depositToFund 只拿 1% base)。
|
|
8398
8731
|
if (maxLevels === 0) {
|
|
8399
|
-
|
|
8732
|
+
redirectToCommissionReserve(pool, 'redirect_region_cap', { orderId, note: '区域禁 MLM — max_levels=0,整池入三级公池' });
|
|
8400
8733
|
db.prepare("UPDATE orders SET settled_commission_at = datetime('now') WHERE id = ?").run(orderId);
|
|
8401
|
-
return { pool, redirected:
|
|
8734
|
+
return { pool, redirected: 0, source: 'static' };
|
|
8402
8735
|
}
|
|
8403
8736
|
// 100% per-product attribution:L1/L2/L3 已在订单创建期由 getProductShareChain() 写入
|
|
8404
8737
|
const l1Uid = order.l1_uid;
|
|
@@ -8427,31 +8760,54 @@ function settleCommission(orderId, effectiveBase) {
|
|
|
8427
8760
|
return 'sponsor';
|
|
8428
8761
|
return sh.type === 'note' ? 'note' : 'link';
|
|
8429
8762
|
}
|
|
8430
|
-
|
|
8431
|
-
|
|
8763
|
+
// 2026-06-04:所有兜底统一入 commission_reserve(三级公池,独立科目,只进不出)。
|
|
8764
|
+
// commission 不再回流 global_fund(PV 资金)—— 三套科目解耦,redirected 始终 0。
|
|
8765
|
+
let toCommissionReserve = 0; // → commission_reserve(仅作日志/返回信息用)
|
|
8432
8766
|
for (const { level, beneficiary } of recipients) {
|
|
8433
8767
|
const amount = Math.round(pool * LEVEL_RATES[level] * 100) / 100;
|
|
8434
8768
|
if (amount <= 0)
|
|
8435
8769
|
continue;
|
|
8436
|
-
// ① region 截断 →
|
|
8770
|
+
// ① region 截断 (level>maxLevels) → 三级公池
|
|
8437
8771
|
if (level > maxLevels) {
|
|
8438
|
-
|
|
8772
|
+
redirectToCommissionReserve(amount, 'redirect_region_cap', { orderId, fromUserId: order.buyer_id, note: `L${level} > maxLevels=${maxLevels} 区域截断` });
|
|
8773
|
+
toCommissionReserve += amount;
|
|
8439
8774
|
continue;
|
|
8440
8775
|
}
|
|
8441
|
-
// ② chain 缺失 (自发现 / 上家断链) →
|
|
8442
|
-
// Phase A 2026-05-21:原 sys_protocol 兜底改入 charity_fund,统一公益化
|
|
8776
|
+
// ② chain 缺失 (自发现 / 上家断链) → 三级公池
|
|
8443
8777
|
if (!beneficiary) {
|
|
8444
|
-
|
|
8445
|
-
|
|
8778
|
+
redirectToCommissionReserve(amount, 'redirect_chain_gap', { orderId, fromUserId: order.buyer_id, note: `L${level} 空缺` });
|
|
8779
|
+
toCommissionReserve += amount;
|
|
8446
8780
|
continue;
|
|
8447
8781
|
}
|
|
8448
|
-
// ③ sponsor 资格无效 (被封 / 无 verify) →
|
|
8782
|
+
// ③ sponsor 资格无效 (被封 / 无 verify) → 三级公池
|
|
8449
8783
|
if (!isAllowedSponsor(beneficiary)) {
|
|
8450
|
-
|
|
8451
|
-
|
|
8784
|
+
redirectToCommissionReserve(amount, 'redirect_orphan_sponsor', { orderId, fromUserId: order.buyer_id, note: `L${level} sponsor 不合规: ${beneficiary}` });
|
|
8785
|
+
toCommissionReserve += amount;
|
|
8786
|
+
continue;
|
|
8787
|
+
}
|
|
8788
|
+
// ④ RFC-002 §3.5 opt-in gate (PR-1c-a)
|
|
8789
|
+
// opted-in → normal credit (⑤)
|
|
8790
|
+
// opted-out + last action 'deactivate' → directly 三级公池 (主动放弃)
|
|
8791
|
+
// opted-out + other (never_activated | auto_downgrade) → pending_commission_escrow
|
|
8792
|
+
const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(beneficiary)?.rewards_opted_in ?? 0;
|
|
8793
|
+
if (optIn !== 1) {
|
|
8794
|
+
const lastAction = db.prepare("SELECT action FROM rewards_applications WHERE user_id = ? ORDER BY created_at DESC LIMIT 1").get(beneficiary)?.action;
|
|
8795
|
+
if (lastAction === 'deactivate') {
|
|
8796
|
+
redirectToCommissionReserve(amount, 'redirect_opt_out_deactivated', { orderId, fromUserId: order.buyer_id, note: `L${level} ${beneficiary} actively deactivated rewards` });
|
|
8797
|
+
toCommissionReserve += amount;
|
|
8798
|
+
continue;
|
|
8799
|
+
}
|
|
8800
|
+
// never_activated OR auto_downgrade → escrow (30d window per protocol_params.rewards_opt_in.escrow_days)
|
|
8801
|
+
const escrowDays = Number(db.prepare("SELECT value FROM protocol_params WHERE key = 'rewards_opt_in.escrow_days'").get()?.value ?? 30);
|
|
8802
|
+
const now = Date.now();
|
|
8803
|
+
try {
|
|
8804
|
+
db.prepare(`INSERT INTO pending_commission_escrow (recipient_user_id, order_id, amount, attribution_path, status, created_at, expires_at) VALUES (?,?,?,?,'pending',?,?)`)
|
|
8805
|
+
.run(beneficiary, orderId, amount, `L${level}`, now, now + escrowDays * 86400 * 1000);
|
|
8806
|
+
}
|
|
8807
|
+
catch (e) { /* UNIQUE 冲突 — settleCommission 重入幂等 */ }
|
|
8452
8808
|
continue;
|
|
8453
8809
|
}
|
|
8454
|
-
//
|
|
8810
|
+
// ⑤ 正常分账
|
|
8455
8811
|
try {
|
|
8456
8812
|
const srcType = resolveSourceType(beneficiary);
|
|
8457
8813
|
db.prepare(`INSERT INTO commission_records (id, order_id, beneficiary_id, source_buyer_id, level, amount, rate, region, source, source_type)
|
|
@@ -8463,9 +8819,10 @@ function settleCommission(orderId, effectiveBase) {
|
|
|
8463
8819
|
catch (e) { /* UNIQUE 冲突 */ }
|
|
8464
8820
|
}
|
|
8465
8821
|
db.prepare("UPDATE orders SET settled_commission_at = datetime('now') WHERE id = ?").run(orderId);
|
|
8466
|
-
//
|
|
8467
|
-
//
|
|
8468
|
-
|
|
8822
|
+
// redirected 恒为 0:commission 兜底全部入 commission_reserve,不再回流 global_fund(PV 资金)。
|
|
8823
|
+
// toCommissionReserve 仅作日志参考(实际入账已在循环里逐笔落 commission_reserve_txns)。
|
|
8824
|
+
void toCommissionReserve;
|
|
8825
|
+
return { pool, redirected: 0, source: routeSource };
|
|
8469
8826
|
}
|
|
8470
8827
|
// ─── 原子能:基金池入金 (depositToFund) ──────────────────────────
|
|
8471
8828
|
// 1% 永远入池(默认);commission 端回流由 settleCommission 返回,作为 extraFromCommission 传入
|
|
@@ -8541,17 +8898,21 @@ function depositToFund(orderId, extraFromCommission = 0, effectiveBase) {
|
|
|
8541
8898
|
}
|
|
8542
8899
|
return { base: amountBase, redirect: amountRedirect, total: totalDeposit };
|
|
8543
8900
|
}
|
|
8544
|
-
function
|
|
8901
|
+
function redirectToCommissionReserve(amount, kind, args = {}) {
|
|
8545
8902
|
if (!Number.isFinite(amount) || amount <= 0)
|
|
8546
8903
|
return;
|
|
8547
8904
|
const a = Math.round(amount * 100) / 100;
|
|
8905
|
+
// Aggregate column mapping (commission_reserve_txns.kind records exact kind):
|
|
8906
|
+
// chain_gap → total_chain_gap
|
|
8907
|
+
// orphan_sponsor / opt_out_deactivated / escrow_expired → total_orphan_sponsor (no-eligible-recipient bucket)
|
|
8908
|
+
// region_cap → total_region_cap
|
|
8548
8909
|
const totalCol = kind === 'redirect_chain_gap' ? 'total_chain_gap'
|
|
8549
|
-
: kind === '
|
|
8550
|
-
: '
|
|
8910
|
+
: kind === 'redirect_region_cap' ? 'total_region_cap'
|
|
8911
|
+
: 'total_orphan_sponsor';
|
|
8551
8912
|
db.transaction(() => {
|
|
8552
|
-
db.prepare(`UPDATE
|
|
8553
|
-
db.prepare(`INSERT INTO
|
|
8554
|
-
VALUES (?,?,?,?,?,?)`).run(generateId('
|
|
8913
|
+
db.prepare(`UPDATE commission_reserve SET balance = balance + ?, ${totalCol} = ${totalCol} + ?, updated_at = datetime('now') WHERE id = 'main'`).run(a, a);
|
|
8914
|
+
db.prepare(`INSERT INTO commission_reserve_txns (id, kind, from_user_id, amount, related_order_id, note)
|
|
8915
|
+
VALUES (?,?,?,?,?,?)`).run(generateId('crt'), kind, args.fromUserId || null, a, args.orderId || null, args.note || null);
|
|
8555
8916
|
})();
|
|
8556
8917
|
}
|
|
8557
8918
|
function settleOrder(orderId) {
|
|
@@ -8610,10 +8971,10 @@ function settleOrder(orderId) {
|
|
|
8610
8971
|
db.prepare("UPDATE management_bonus_pool SET balance = balance + ? WHERE id = 1").run(protocolToBonus);
|
|
8611
8972
|
if (protocolToOps > 0)
|
|
8612
8973
|
db.prepare("UPDATE wallets SET balance = balance + ? WHERE user_id = 'sys_protocol'").run(protocolToOps);
|
|
8613
|
-
//
|
|
8614
|
-
|
|
8615
|
-
//
|
|
8616
|
-
depositToFund(orderId
|
|
8974
|
+
// 推土机分享分润:正常分账 → 钱包;兜底 → commission_reserve(三级公池,独立科目)
|
|
8975
|
+
settleCommission(orderId);
|
|
8976
|
+
// 原子能:PV 资金池入金 = 仅 1% base(2026-06-04 起 commission 不再回流此池,三科目解耦)
|
|
8977
|
+
depositToFund(orderId);
|
|
8617
8978
|
// P-Distrib β:若 buyer 是经 pinner 传输内容才看到本商品 → settlePinRewards 从 basin 拨 0.5%
|
|
8618
8979
|
try {
|
|
8619
8980
|
const pr = settlePinRewards(orderId);
|
|
@@ -9434,8 +9795,18 @@ registerPublicUtilsRoutes(app, {
|
|
|
9434
9795
|
issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
|
|
9435
9796
|
});
|
|
9436
9797
|
// ─── 静态文件 + SPA 回退(必须在所有 API 路由之后)────────────
|
|
9437
|
-
|
|
9798
|
+
// PWA 壳文件必须 no-cache(否则 CF/浏览器 4h 缓存挡新版本);
|
|
9799
|
+
// 其他静态资产(图标/字体)走 CF 默认。
|
|
9800
|
+
app.use(express.static(path.join(__dirname, 'public'), {
|
|
9801
|
+
setHeaders: (res, filePath) => {
|
|
9802
|
+
const base = path.basename(filePath);
|
|
9803
|
+
if (base === 'app.js' || base === 'sw.js' || base === 'i18n.js' || base === 'index.html' || base === 'manifest.json') {
|
|
9804
|
+
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
|
9805
|
+
}
|
|
9806
|
+
},
|
|
9807
|
+
}));
|
|
9438
9808
|
app.get('/{*path}', (_req, res) => {
|
|
9809
|
+
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
|
9439
9810
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
9440
9811
|
});
|
|
9441
9812
|
// POST /api/orders/:id/force-timeout-check — Phase 84 已迁出
|
|
@@ -9802,6 +10173,199 @@ try {
|
|
|
9802
10173
|
db.exec('CREATE INDEX IF NOT EXISTS idx_error_log_created ON error_log(created_at)');
|
|
9803
10174
|
}
|
|
9804
10175
|
catch { }
|
|
10176
|
+
// ─── 治理岗位上岗(W3.5-B,2026-06-02)──────────────────────────
|
|
10177
|
+
// docs/GOVERNANCE-ONBOARDING.md — arbitrator + verifier 申请 / 上岗 / 卸任 / 申诉
|
|
10178
|
+
// 1 表:governance_applications(append-only,记录 apply/activate/resign/auto_deactivate/appeal)
|
|
10179
|
+
db.exec(`
|
|
10180
|
+
CREATE TABLE IF NOT EXISTS governance_applications (
|
|
10181
|
+
id TEXT PRIMARY KEY,
|
|
10182
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
10183
|
+
role TEXT NOT NULL, -- 'arbitrator' | 'verifier'
|
|
10184
|
+
action TEXT NOT NULL, -- 'apply' | 'activate' | 'resign' | 'auto_deactivate' | 'appeal' | 'reconfirm'
|
|
10185
|
+
status TEXT NOT NULL, -- 'pending_onboarding' | 'active' | 'inactive' | 'rejected' | 'cooldown'
|
|
10186
|
+
consent_hash TEXT, -- apply 时披露文本 hash
|
|
10187
|
+
passkey_sig TEXT, -- apply / activate / resign Passkey 签发证据
|
|
10188
|
+
iron_rule_method TEXT, -- 'passkey' | 'password' | 'system_auto'
|
|
10189
|
+
quiz_score INTEGER, -- 0-100(activate 时)
|
|
10190
|
+
case_review_text TEXT, -- onboarding §4.2 案例分析(摘要)
|
|
10191
|
+
cooldown_until INTEGER, -- resign / auto_deactivate 后到期 timestamp
|
|
10192
|
+
appeal_reason TEXT, -- appeal 时填
|
|
10193
|
+
appeal_resolution TEXT, -- maintainer 处置 + 理由
|
|
10194
|
+
ip_hash TEXT,
|
|
10195
|
+
ua_hash TEXT,
|
|
10196
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))
|
|
10197
|
+
)
|
|
10198
|
+
`);
|
|
10199
|
+
try {
|
|
10200
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_gov_apps_user ON governance_applications(user_id, created_at DESC)');
|
|
10201
|
+
}
|
|
10202
|
+
catch { }
|
|
10203
|
+
try {
|
|
10204
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_gov_apps_role_status ON governance_applications(role, status, created_at DESC)');
|
|
10205
|
+
}
|
|
10206
|
+
catch { }
|
|
10207
|
+
try {
|
|
10208
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_gov_apps_cooldown ON governance_applications(user_id, role, cooldown_until)');
|
|
10209
|
+
}
|
|
10210
|
+
catch { }
|
|
10211
|
+
// 2026-06-02 PR #22 review fix P1-3:quiz pass 推进状态时间戳(spec §4.3 onboarding 题目环节完成标记)
|
|
10212
|
+
try {
|
|
10213
|
+
db.exec('ALTER TABLE governance_applications ADD COLUMN quiz_passed_at INTEGER');
|
|
10214
|
+
}
|
|
10215
|
+
catch { }
|
|
10216
|
+
// 2026-06-02 task #1093 阶段 4:appeal 行指向被申诉的 auto_deactivate 原行(链接审计 + 防重复 appeal)
|
|
10217
|
+
try {
|
|
10218
|
+
db.exec('ALTER TABLE governance_applications ADD COLUMN source_application_id TEXT');
|
|
10219
|
+
}
|
|
10220
|
+
catch { }
|
|
10221
|
+
try {
|
|
10222
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_gov_apps_source ON governance_applications(source_application_id) WHERE source_application_id IS NOT NULL');
|
|
10223
|
+
}
|
|
10224
|
+
catch { }
|
|
10225
|
+
// ─── RFC-002 PR-1a schema(task #1090,2026-06-03)/ rewards opt-in 基础设施 ──────
|
|
10226
|
+
// spec: docs/rfcs/RFC-002-rewards-opt-in.md §3.6
|
|
10227
|
+
// scope:本 PR 仅 schema(3 新表 + 2 ALTER + 5 新 protocol_params)。
|
|
10228
|
+
// 所有表为空,无 code 读 — 零行为变化。后续 PR-1b/1c 接入估值层 gate + escrow 逻辑。
|
|
10229
|
+
// 1. users 加列(rewards_opt_in flag,默认 0 = opt-out)
|
|
10230
|
+
try {
|
|
10231
|
+
db.exec('ALTER TABLE users ADD COLUMN rewards_opted_in INTEGER DEFAULT 0');
|
|
10232
|
+
}
|
|
10233
|
+
catch { }
|
|
10234
|
+
// 2. protocol_params 加 requires_meta_rule_change 列(P0-4 闭环 — 把声明保护转为执行保护)
|
|
10235
|
+
try {
|
|
10236
|
+
db.exec('ALTER TABLE protocol_params ADD COLUMN requires_meta_rule_change INTEGER DEFAULT 0');
|
|
10237
|
+
}
|
|
10238
|
+
catch { }
|
|
10239
|
+
// 3. rewards_consent_texts:同意文本版本化(§3.10),先建,被 rewards_applications FK 引用
|
|
10240
|
+
db.exec(`
|
|
10241
|
+
CREATE TABLE IF NOT EXISTS rewards_consent_texts (
|
|
10242
|
+
version TEXT PRIMARY KEY, -- e.g. '1.0', '1.1', '2.0'
|
|
10243
|
+
hash TEXT NOT NULL, -- sha256 of canonical text
|
|
10244
|
+
change_class TEXT NOT NULL, -- 'major' | 'minor'
|
|
10245
|
+
effective_at INTEGER NOT NULL,
|
|
10246
|
+
text_zh TEXT NOT NULL,
|
|
10247
|
+
text_en TEXT NOT NULL,
|
|
10248
|
+
changelog TEXT -- human-readable diff summary
|
|
10249
|
+
)
|
|
10250
|
+
`);
|
|
10251
|
+
(function seedConsentV1() {
|
|
10252
|
+
const existing = db.prepare("SELECT version FROM rewards_consent_texts WHERE version = '1.0'").get();
|
|
10253
|
+
if (existing)
|
|
10254
|
+
return;
|
|
10255
|
+
const textZh = 'WebAZ 共建身份(rewards opt-in)v1.0 — 由 RFC-002 §3.3 / §3.10 定义。本同意涉及经济关系登记 + Passkey 真人签名 + 三级佣金 + 二元配对树参与。详见 RFC-002 全文。本流程与购物无关,可随时退出,不影响订单。';
|
|
10256
|
+
const textEn = 'WebAZ Builder Identity (rewards opt-in) v1.0 — defined by RFC-002 §3.3 / §3.10. This consent records an economic relationship with Passkey-signed proof of personhood + participation in 3-tier commission + binary PV matching tree. See full RFC-002. This flow is not part of shopping; you may leave anytime without affecting orders.';
|
|
10257
|
+
const hash = createHash('sha256').update(textZh + '\n---\n' + textEn).digest('hex');
|
|
10258
|
+
db.prepare(`INSERT INTO rewards_consent_texts (version, hash, change_class, effective_at, text_zh, text_en, changelog)
|
|
10259
|
+
VALUES (?, ?, 'major', ?, ?, ?, ?)`)
|
|
10260
|
+
.run('1.0', hash, Date.now(), textZh, textEn, 'Initial v1.0 lock — placeholder canonical text pointing to RFC-002');
|
|
10261
|
+
})();
|
|
10262
|
+
// 4. rewards_applications:申请留痕表(append-only audit;action='activate'|'deactivate'|'auto_downgrade'|'reconfirm')
|
|
10263
|
+
db.exec(`
|
|
10264
|
+
CREATE TABLE IF NOT EXISTS rewards_applications (
|
|
10265
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10266
|
+
user_id TEXT NOT NULL,
|
|
10267
|
+
action TEXT NOT NULL, -- 'activate' | 'deactivate' | 'auto_downgrade' | 'reconfirm'
|
|
10268
|
+
consent_version TEXT, -- FK rewards_consent_texts(version); activate/reconfirm 必填
|
|
10269
|
+
consent_hash TEXT, -- sha256 of versioned disclosure; activate/reconfirm 必填
|
|
10270
|
+
passkey_sig TEXT, -- WebAuthn sig blob; activate/reconfirm required; auto_downgrade 系统侧无签名
|
|
10271
|
+
verification_method TEXT NOT NULL, -- 'passkey' | 'password' | 'system_auto'
|
|
10272
|
+
ip_hash TEXT, -- anonymized IP audit
|
|
10273
|
+
ua_hash TEXT,
|
|
10274
|
+
created_at INTEGER NOT NULL,
|
|
10275
|
+
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
10276
|
+
FOREIGN KEY (consent_version) REFERENCES rewards_consent_texts(version)
|
|
10277
|
+
)
|
|
10278
|
+
`);
|
|
10279
|
+
try {
|
|
10280
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_rewards_apps_user ON rewards_applications(user_id, created_at DESC)');
|
|
10281
|
+
}
|
|
10282
|
+
catch { }
|
|
10283
|
+
try {
|
|
10284
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_rewards_apps_action ON rewards_applications(user_id, action, created_at DESC)');
|
|
10285
|
+
}
|
|
10286
|
+
catch { }
|
|
10287
|
+
// 5. pending_commission_escrow:opt-out promoter 待激活领取队列(§3.5b)
|
|
10288
|
+
// PR-1c-b: order_id is NULLable so attribution_path='pv_pair' rows (which accrue
|
|
10289
|
+
// across many orders) can record amount without inventing a fake order_id.
|
|
10290
|
+
// L1/L2/L3 rows still carry a real order_id, enforced by the gate code path
|
|
10291
|
+
// in settleCommission (not by NOT NULL constraint).
|
|
10292
|
+
db.exec(`
|
|
10293
|
+
CREATE TABLE IF NOT EXISTS pending_commission_escrow (
|
|
10294
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10295
|
+
recipient_user_id TEXT NOT NULL,
|
|
10296
|
+
order_id TEXT, -- NULL for pv_pair (PR-1c-b)
|
|
10297
|
+
amount REAL NOT NULL, -- WAZ amount
|
|
10298
|
+
attribution_path TEXT NOT NULL, -- 'L1' | 'L2' | 'L3' | 'pv_pair' | etc.
|
|
10299
|
+
status TEXT NOT NULL DEFAULT 'pending', -- 'pending' | 'settled' | 'expired'
|
|
10300
|
+
created_at INTEGER NOT NULL,
|
|
10301
|
+
expires_at INTEGER NOT NULL,
|
|
10302
|
+
settled_at INTEGER,
|
|
10303
|
+
expired_to_charity_at INTEGER,
|
|
10304
|
+
FOREIGN KEY (recipient_user_id) REFERENCES users(id),
|
|
10305
|
+
FOREIGN KEY (order_id) REFERENCES orders(id)
|
|
10306
|
+
)
|
|
10307
|
+
`);
|
|
10308
|
+
(function migrateEscrowOrderIdNullable() {
|
|
10309
|
+
const cols = db.prepare("PRAGMA table_info(pending_commission_escrow)").all();
|
|
10310
|
+
const orderIdCol = cols.find(c => c.name === 'order_id');
|
|
10311
|
+
if (!orderIdCol || orderIdCol.notnull === 0)
|
|
10312
|
+
return; // already nullable (or table missing entirely)
|
|
10313
|
+
console.log('[pc-escrow-migrate] order_id is NOT NULL — recreating table to allow NULL for pv_pair');
|
|
10314
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
10315
|
+
db.transaction(() => {
|
|
10316
|
+
db.exec(`
|
|
10317
|
+
CREATE TABLE pending_commission_escrow_new (
|
|
10318
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10319
|
+
recipient_user_id TEXT NOT NULL,
|
|
10320
|
+
order_id TEXT,
|
|
10321
|
+
amount REAL NOT NULL,
|
|
10322
|
+
attribution_path TEXT NOT NULL,
|
|
10323
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
10324
|
+
created_at INTEGER NOT NULL,
|
|
10325
|
+
expires_at INTEGER NOT NULL,
|
|
10326
|
+
settled_at INTEGER,
|
|
10327
|
+
expired_to_charity_at INTEGER,
|
|
10328
|
+
FOREIGN KEY (recipient_user_id) REFERENCES users(id),
|
|
10329
|
+
FOREIGN KEY (order_id) REFERENCES orders(id)
|
|
10330
|
+
)
|
|
10331
|
+
`);
|
|
10332
|
+
db.exec('INSERT INTO pending_commission_escrow_new SELECT * FROM pending_commission_escrow');
|
|
10333
|
+
db.exec('DROP TABLE pending_commission_escrow');
|
|
10334
|
+
db.exec('ALTER TABLE pending_commission_escrow_new RENAME TO pending_commission_escrow');
|
|
10335
|
+
})();
|
|
10336
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
10337
|
+
})();
|
|
10338
|
+
try {
|
|
10339
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_escrow_recipient ON pending_commission_escrow(recipient_user_id, status, expires_at)');
|
|
10340
|
+
}
|
|
10341
|
+
catch { }
|
|
10342
|
+
try {
|
|
10343
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_escrow_expiry ON pending_commission_escrow(status, expires_at)');
|
|
10344
|
+
}
|
|
10345
|
+
catch { }
|
|
10346
|
+
// PR-1c-a: UNIQUE (recipient, order, path) defends against double-insert if settleCommission ever retries
|
|
10347
|
+
// Note: NULL order_id (PR-1c-b pv_pair) is distinct in SQLite UNIQUE — idempotency for pv_pair relies
|
|
10348
|
+
// on binary_score_records.settled_at instead (source-side dedup).
|
|
10349
|
+
try {
|
|
10350
|
+
db.exec('CREATE UNIQUE INDEX IF NOT EXISTS uniq_escrow_recipient_order_path ON pending_commission_escrow(recipient_user_id, order_id, attribution_path)');
|
|
10351
|
+
}
|
|
10352
|
+
catch { }
|
|
10353
|
+
// 6. INSERT 5 个 RFC-002 protocol_params(独立 INSERT,不动 DEFAULT_PARAMS array)
|
|
10354
|
+
// 两个标 requires_meta_rule_change=1:require_passkey + consent_delay_seconds(P0-4 闭环)
|
|
10355
|
+
const RFC002_PARAMS = [
|
|
10356
|
+
{ key: 'rewards_opt_in.min_completed_orders', value: '1', type: 'number', description: 'RFC-002 §3.2:申请 rewards opt-in 的最小已完成订单数', category: 'rewards', min: 0, max: 100, metaRuleLocked: false },
|
|
10357
|
+
{ key: 'rewards_opt_in.require_passkey', value: '1', type: 'number', description: 'RFC-002 §3.3:申请 / 关闭是否需 Passkey(1=必须,0=允许 password)— META-RULE LOCKED,降低需 60d meta-rule track', category: 'rewards', min: 0, max: 1, metaRuleLocked: true },
|
|
10358
|
+
{ key: 'rewards_opt_in.escrow_days', value: '30', type: 'number', description: 'RFC-002 §3.5b:pending commission escrow 过期天数(过期后流入 charity_fund)', category: 'rewards', min: 7, max: 180, metaRuleLocked: false },
|
|
10359
|
+
{ key: 'rewards_opt_in.consent_delay_seconds', value: '8', type: 'number', description: 'RFC-002 §3.3:server-side 8s 反诱导延迟 — META-RULE LOCKED,降低需 60d meta-rule track', category: 'rewards', min: 0, max: 60, metaRuleLocked: true },
|
|
10360
|
+
{ key: 'rewards_opt_in.reconfirm_grace_days', value: '14', type: 'number', description: 'RFC-002 §3.10:major consent 变更后用户重新确认 grace 期(过期 auto_downgrade)', category: 'rewards', min: 3, max: 90, metaRuleLocked: false },
|
|
10361
|
+
];
|
|
10362
|
+
for (const p of RFC002_PARAMS) {
|
|
10363
|
+
try {
|
|
10364
|
+
db.prepare(`INSERT OR IGNORE INTO protocol_params (key, value, type, description, category, default_value, min_value, max_value, requires_meta_rule_change) VALUES (?,?,?,?,?,?,?,?,?)`)
|
|
10365
|
+
.run(p.key, p.value, p.type, p.description, p.category, p.value, p.min ?? null, p.max ?? null, p.metaRuleLocked ? 1 : 0);
|
|
10366
|
+
}
|
|
10367
|
+
catch { }
|
|
10368
|
+
}
|
|
9805
10369
|
function logError(source, message, extra = {}) {
|
|
9806
10370
|
try {
|
|
9807
10371
|
db.prepare(`INSERT INTO error_log (source, message, stack, url, user_agent, user_id) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
@@ -9874,4 +10438,17 @@ app.listen(PORT, () => {
|
|
|
9874
10438
|
};
|
|
9875
10439
|
setInterval(cleanWebAuthnExpired, 6 * 60 * 60 * 1000); // 每 6h 跑一次
|
|
9876
10440
|
console.log(`🧹 webauthn 过期清理 cron 已启动(每 6h 清 >1d 残留)`);
|
|
10441
|
+
// task #1093 stage 5: governance auto-deactivate cron
|
|
10442
|
+
// Spec docs/ARBITRATION-PLAYBOOK.md §6.2 + GOVERNANCE-ONBOARDING.md §6.2
|
|
10443
|
+
// Anchor: confirmed_wrong (NOT outlier). Phase A: verifier only.
|
|
10444
|
+
startAutoDeactivateCron({ db, generateId, getProtocolParam });
|
|
10445
|
+
// #1090 RFC-002 PR-1c-a: escrow expire cron (every 1h)
|
|
10446
|
+
startEscrowExpireCron({ db, redirectToCommissionReserve });
|
|
10447
|
+
// #1090 RFC-002 PR-3 slice 2: auto_downgrade cron (every 24h)
|
|
10448
|
+
// Triggered when a new major consent text is published; opted-in users
|
|
10449
|
+
// who don't reconfirm within reconfirm_grace_days (14d default) get
|
|
10450
|
+
// rewards_opted_in flipped to 0 with action='auto_downgrade'. Per
|
|
10451
|
+
// PR-1c-a settleCommission gate, future commissions then route to
|
|
10452
|
+
// escrow (not charity) for re-activation recovery.
|
|
10453
|
+
startAutoDowngradeCron({ db, getProtocolParam });
|
|
9877
10454
|
});
|