@seasonkoh/webaz 0.1.26 → 0.1.28

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.
Files changed (99) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +69 -0
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +270 -82
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  13. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  15. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  16. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  20. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  22. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  23. package/dist/ledger.js +1 -1
  24. package/dist/pwa/admin-audit.js +38 -0
  25. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  26. package/dist/pwa/cf-origin-guard.js +33 -0
  27. package/dist/pwa/contract-fingerprint.js +1 -0
  28. package/dist/pwa/data/onboarding-cases.js +2 -2
  29. package/dist/pwa/data/onboarding-quiz.js +1 -1
  30. package/dist/pwa/economic-participation.js +2 -2
  31. package/dist/pwa/integration-contract.js +46 -4
  32. package/dist/pwa/internal/pv-settlement.js +12 -0
  33. package/dist/pwa/internal/wallet-signer.js +26 -0
  34. package/dist/pwa/public/app-account.js +977 -0
  35. package/dist/pwa/public/app-admin.js +608 -0
  36. package/dist/pwa/public/app-agents.js +63 -0
  37. package/dist/pwa/public/app-ai.js +2162 -0
  38. package/dist/pwa/public/app-contribution.js +836 -0
  39. package/dist/pwa/public/app-discover.js +1296 -0
  40. package/dist/pwa/public/app-listings.js +226 -0
  41. package/dist/pwa/public/app-profile.js +1692 -0
  42. package/dist/pwa/public/app-seller.js +199 -0
  43. package/dist/pwa/public/app-shop.js +1145 -0
  44. package/dist/pwa/public/app.js +15075 -23960
  45. package/dist/pwa/public/i18n.js +31 -28
  46. package/dist/pwa/public/index.html +11 -1
  47. package/dist/pwa/public/openapi.json +4851 -2776
  48. package/dist/pwa/pv-kill-switch.js +31 -0
  49. package/dist/pwa/routes/admin-admins.js +48 -1
  50. package/dist/pwa/routes/admin-analytics.js +1 -10
  51. package/dist/pwa/routes/admin-atomic.js +4 -17
  52. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  53. package/dist/pwa/routes/admin-reports.js +4 -26
  54. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  55. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  56. package/dist/pwa/routes/admin-users-query.js +23 -1
  57. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  58. package/dist/pwa/routes/agent-grants.js +255 -0
  59. package/dist/pwa/routes/auth-read.js +1 -5
  60. package/dist/pwa/routes/auth-register.js +3 -13
  61. package/dist/pwa/routes/build-task-quota.js +113 -0
  62. package/dist/pwa/routes/claim-verify.js +15 -11
  63. package/dist/pwa/routes/contribution-facts.js +18 -0
  64. package/dist/pwa/routes/dispute-cases.js +5 -4
  65. package/dist/pwa/routes/growth.js +3 -3
  66. package/dist/pwa/routes/orders-action.js +27 -10
  67. package/dist/pwa/routes/orders-create.js +1 -1
  68. package/dist/pwa/routes/products-meta.js +19 -6
  69. package/dist/pwa/routes/profile-placement.js +1 -1
  70. package/dist/pwa/routes/promoter.js +10 -29
  71. package/dist/pwa/routes/public-build-tasks.js +5 -1
  72. package/dist/pwa/routes/public-utils.js +9 -12
  73. package/dist/pwa/routes/referral.js +5 -26
  74. package/dist/pwa/routes/rewards-apply.js +3 -2
  75. package/dist/pwa/routes/share-redirects.js +1 -1
  76. package/dist/pwa/routes/shareables-interactions.js +2 -1
  77. package/dist/pwa/routes/task-proposals.js +85 -9
  78. package/dist/pwa/routes/users-public.js +1 -4
  79. package/dist/pwa/routes/wallet-read.js +2 -14
  80. package/dist/pwa/routes/webauthn.js +7 -2
  81. package/dist/pwa/server-schema.js +9 -0
  82. package/dist/pwa/server.js +319 -2034
  83. package/dist/runtime/agent-grant-scopes.js +128 -0
  84. package/dist/runtime/agent-grant-verifier.js +67 -0
  85. package/dist/runtime/agent-pairing.js +60 -0
  86. package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
  87. package/dist/runtime/webaz-schema-helpers.js +1848 -0
  88. package/dist/settlement-math.js +3 -3
  89. package/dist/version.js +6 -4
  90. package/package.json +43 -8
  91. package/dist/index.js +0 -182
  92. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  93. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  94. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  95. package/dist/test-dispute.js +0 -153
  96. package/dist/test-manifest.js +0 -61
  97. package/dist/test-mcp-tools.js +0 -135
  98. package/dist/test-reputation.js +0 -116
  99. package/dist/test-skill-market.js +0 -101
@@ -24,7 +24,10 @@ import { setSeamDb } from '../layer0-foundation/L0-1-database/db.js'; // RFC-016
24
24
  import { initSystemUser, transition, getOrderStatus, checkTimeouts, settleFault } from '../layer0-foundation/L0-2-state-machine/engine.js';
25
25
  import { endpointToAction, endpointToReadAction } from './endpoint-actions.js';
26
26
  import { AGENT_RATE_PER_MIN_DEFAULTS, CROSS_USER_READ_DAILY_CAP, MASS_ACTION_TYPES, MASS_ACTION_DAILY_CAPS } from './limits.js';
27
+ // #420 P1-2/P1-3/P1-4 — 反滥用阈值单一真相源(governance-adjustable protocol_params)+ 纯决策函数
28
+ import { ANTI_ABUSE_PARAMS, readAntiAbuseThresholds, agentTrustLevel, agentSybilPenalty, agentStrikeSeverity, verifierOutlierBand } from './anti-abuse-thresholds.js';
27
29
  import { initOrderChainSchema, appendOrderEvent, getOrderChain, verifyOrderChain } from '../layer0-foundation/L0-2-state-machine/order-chain.js';
30
+ import { initVerifierWhitelistSchema, initMcpToolCallsSchema, initNotePhotoIndexSchema, initUserWishlistSchema, initProductQaSchema, initCouponsSchema, initAnnouncementsSchema, initProductWaitlistSchema, initFlashSalesSchema, initPublicIdeasSchema, initAuctionRemindersSchema, initEmailSubscriptionsSchema, initFeedbackTicketsSchema, initFeedbackMessagesSchema, initDisputeCasesSchema, initDisputeCommentsSchema, initDisputeCommentRepliesSchema, initShareableCommentsSchema, initDisputeFairnessVotesSchema, initOrderRatingsSchema, initBuyerRatingsSchema, initUserAddressesSchema, initP2pShopsSchema, initShareableLikesSchema, initShareableBookmarksSchema, initShareableTagsSchema, initManifestRegistrySchema, initPeerDirectorySchema, initSignalingQueueSchema, initConversationsSchema, initMessagesSchema, initChatReportsSchema, initQuotaIncreaseApplicationsSchema, initVerifierApplicationsSchema, initArbitratorReviewSchema, initVerifierAppealsSchema, initUserModerationSchema, initAdminAuditLogSchema, initVerificationCodesSchema, initAgentCallLogSchema, initAgentReputationSchema, initAgentDeclarationsSchema, initAgentAttestationsSchema, initAgentStrikesSchema, initAgentRevocationsSchema, initProductAliasesSchema, initRegionChangeLogSchema, initCartItemsSchema, initFollowsSchema, initPushSubscriptionsSchema, initUserSessionsSchema, initUserBlocklistSchema, initImportLogsSchema, initErrorLogSchema, initSecondhandItemsSchema, initProductTrialCampaignsSchema, initProductTrialClaimsSchema, initReturnRequestsSchema, initReturnMessagesSchema, initProductVariantsSchema, initEditorPicksSchema, initKycRecordsSchema, initWebauthnSchema, initClaimVerificationBaseSchema, initClaimVerifierSuspensionsSchema, initProductClaimSchema, initReviewClaimSchema, initSecondhandClaimSchema, initAuctionClaimSchema, initWishClaimSchema, initShareableClickLogSchema, initCommissionAuditLogSchema, initRegistrationAuditLogSchema, initProductExternalLinksBaseSchema, initLinkChallengesSchema, initVerifyTasksSchema, initVerifySubmissionsSchema, initVerifierStatsSchema, initRegisterListSearchColumns } from './server-schema.js';
28
31
  // RFC-014 PR4 — 正常成交结算走整数 base-units + allocate + 绝对值落库。
29
32
  import { toUnits, toDecimal, mulRate, allocate } from '../money.js';
30
33
  import { applyWalletDelta, creditColumns } from '../ledger.js';
@@ -42,7 +45,6 @@ import { initSkillSchema, shouldAutoAccept, } from '../layer4-economics/L4-4-ski
42
45
  import { initReputationSchema, recordOrderReputation, recordViolationReputation, recordDisputeReputation, recordRepEvent, getReputation, getStakeDiscount, applyDecayIfDue, } from '../layer4-economics/L4-3-reputation/reputation-engine.js';
43
46
  import { generateManifest } from '../layer0-foundation/L0-5-manifest/manifest.js';
44
47
  import Anthropic from '@anthropic-ai/sdk';
45
- import { privateKeyToAddress, privateKeyToAccount } from 'viem/accounts';
46
48
  import { createPublicClient, createWalletClient, http, parseAbiItem, parseAbi, parseEther } from 'viem';
47
49
  import { baseSepolia, base } from 'viem/chains';
48
50
  import { createHmac, createHash, randomBytes, scryptSync, timingSafeEqual } from 'node:crypto';
@@ -73,9 +75,10 @@ import { registerDisputeCasesRoutes } from './routes/dispute-cases.js';
73
75
  // Claim verify (#1013 Phase 9) — 8 endpoints + 三路径结算 cron + 铁律 §4
74
76
  // requireHumanPresence 仍在 server.ts(arbitrate / agent_revoke / vote 3 处用),通过 deps 注入
75
77
  // settleClaimTask + 多个内部 helper 已 export 供 server.ts 其它路径调用(如 product-claims)
76
- import { registerClaimVerifyRoutes, processClaimTaskQueue, isEligibleClaimVerifier as isEligibleClaimVerifierRaw, activeClaimTaskCountForVerifier as activeClaimTaskCountForVerifierRaw, settleClaimTask as settleClaimTaskRaw, notifyEligibleVerifiers as notifyEligibleVerifiersRaw,
77
- // 跨域共用常量(checkVerifierOutlier 跨多 vote table 聚合用)
78
- CLAIM_SUSPEND_THRESHOLD, CLAIM_REVOKE_THRESHOLD, CLAIM_SUSPEND_DAYS, CLAIM_OUTLIER_WINDOW_DAYS, } from './routes/claim-verify.js';
78
+ import { registerClaimVerifyRoutes, processClaimTaskQueue, isEligibleClaimVerifier as isEligibleClaimVerifierRaw, activeClaimTaskCountForVerifier as activeClaimTaskCountForVerifierRaw, settleClaimTask as settleClaimTaskRaw, notifyEligibleVerifiers as notifyEligibleVerifiersRaw,
79
+ // #420 P1-3:verifier outlier 阈值改由 protocol_params 驱动(见 anti-abuse-thresholds.ts),
80
+ // checkVerifierOutlier 不再 import claim-verify 的 CLAIM_*_THRESHOLD 常量。
81
+ } from './routes/claim-verify.js';
79
82
  // Follows (#1013 Phase 10) — 4 endpoints (status/post/delete/me)
80
83
  // /api/follows/feed 留 server.ts(依赖 products 跨域,待商品域拆分时一并处理)
81
84
  import { registerFollowsRoutes } from './routes/follows.js';
@@ -174,7 +177,7 @@ import { registerShareRedirectsRoutes } from './routes/share-redirects.js';
174
177
  import { registerShopReferralRoutes } from './routes/shop-referral.js';
175
178
  // Profile 凭据 (#1013 Phase 55) — 5 endpoints (密码 + 邮箱绑定)
176
179
  import { registerProfileCredentialsRoutes } from './routes/profile-credentials.js';
177
- // Profile 双轨挂靠 (#1013 Phase 56) — 3 endpoints
180
+ // Profile 放置挂靠 (#1013 Phase 56) — 3 endpoints
178
181
  import { registerProfilePlacementRoutes } from './routes/profile-placement.js';
179
182
  // Profile 位置 (#1013 Phase 57) — 2 endpoints
180
183
  import { registerProfileLocationRoutes } from './routes/profile-location.js';
@@ -314,15 +317,24 @@ import { initBuildTaskAgentMetadataSchema } from '../layer2-business/L2-9-contri
314
317
  import { initTaskProposalSchema } from '../layer2-business/L2-9-contribution/task-proposal-store.js';
315
318
  import { initTaskProposalAiSchema } from '../layer2-business/L2-9-contribution/task-proposal-ai-store.js';
316
319
  import { initTaskProposalDraftLinkSchema } from '../layer2-business/L2-9-contribution/task-proposal-draft.js';
320
+ import { initBuildTaskQuotaSchema } from '../layer2-business/L2-9-contribution/build-task-quota.js';
321
+ import { registerBuildTaskQuotaRoutes } from './routes/build-task-quota.js';
322
+ import { registerAdminOperatorClaimRoutes } from './routes/admin-operator-claims.js';
317
323
  import { registerTaskProposalsRoutes } from './routes/task-proposals.js';
324
+ import { participationRecordingActive, matchingRewardsActive } from './pv-kill-switch.js'; // Category C: participation recording (default ON) vs matching-rewards payout (default OFF)
325
+ import { createPvSettlementEngine } from './internal/pv-settlement.js'; // matching-rewards engine EXCISED — no-op stub (see internal/pv-settlement.ts)
326
+ import { createLocalSeedSigner } from './internal/wallet-signer.js'; // Phase 0: hot-wallet custody signer seam (docs/HOT-WALLET-CUSTODY-MIGRATION.md)
327
+ import { createCfOriginGuard } from './cf-origin-guard.js'; // Cloudflare-only origin guard (off by default)
318
328
  import { createSlidingWindowLimiter } from './rate-limit.js';
319
329
  import { registerBuildReputationRoutes } from './routes/build-reputation.js';
320
330
  import { initBuildReputationSchema } from '../layer2-business/L2-9-contribution/build-reputation-engine.js';
321
331
  import { initGithubCredentialStoreSchema } from '../layer2-business/L2-9-contribution/github-credential-store.js';
322
332
  import { initIdentityBindingSchema } from '../layer2-business/L2-9-contribution/identity-binding-store.js';
323
333
  import { initIdentityClaimChallengeSchema } from '../layer2-business/L2-9-contribution/identity-claim-challenge-store.js';
334
+ import { initAdminCoordinationSchema } from '../layer2-business/L2-9-contribution/admin-coordination-store.js';
324
335
  import { registerContributionIdentityRoutes } from './routes/contribution-identity.js';
325
336
  import { registerContributionScoreRoutes } from './routes/contribution-score.js';
337
+ import { registerContributionFactsRoutes } from './routes/contribution-facts.js';
326
338
  const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
327
339
  // ─── 链上地址派生 ──────────────────────────────────────────────
328
340
  const MASTER_SEED = process.env.WALLET_MASTER_SEED ?? 'webaz-dev-seed-changeme';
@@ -353,11 +365,13 @@ else if (MASTER_SEED.length < 32) {
353
365
  function generateSecureKey(prefix) {
354
366
  return `${prefix}_${randomBytes(32).toString('hex')}`;
355
367
  }
356
- function derivePrivKey(seed) {
357
- return `0x${createHmac('sha256', MASTER_SEED).update(seed).digest('hex')}`;
358
- }
368
+ // Phase 0 (docs/HOT-WALLET-CUSTODY-MIGRATION.md): all USDC-custody key derivation / signing goes
369
+ // through the WalletSigner seam. LocalSeedSigner reproduces the historical HMAC-SHA256(MASTER_SEED, role)
370
+ // derivation EXACTLY — addresses + signatures unchanged. Phase 1+ swaps in KMS / multisig signers
371
+ // (HOT_WALLET_SIGNER env) behind the same interface, no call-site changes.
372
+ const walletSigner = createLocalSeedSigner(MASTER_SEED);
359
373
  function deriveDepositAddress(userId) {
360
- return privateKeyToAddress(derivePrivKey(userId));
374
+ return walletSigner.depositAddress(userId);
361
375
  }
362
376
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
363
377
  const db = initDatabase();
@@ -381,10 +395,13 @@ initBuildTaskAgentMetadataSchema(db); // PR9B — agent-ready task metadata sate
381
395
  initTaskProposalSchema(db); // Task Proposal Inbox v1 — suggestion inbox(maintainer review;never auto build_task)
382
396
  initTaskProposalAiSchema(db); // Task Proposal AI-assist — assistant-only recommendation/evidence(human decides)
383
397
  initTaskProposalDraftLinkSchema(db); // Task Proposal draft links — source proposal ↔ draft task(converted at publish)
398
+ initBuildTaskQuotaSchema(db); // PR #18 — build_task create quota-increase requests(non-root request → root grant)
384
399
  initBuildReputationSchema(db); // RFC-006 build_reputation(独立池 + 贡献者看板)
385
400
  initGithubCredentialStoreSchema(db); // PR 3B-3a — GitHub credential store + RFC-017 fact layer (schema only)
386
401
  initIdentityBindingSchema(db); // PR 4a — GitHub identity → WebAZ account binding (append-only events + active projection)
387
402
  initIdentityClaimChallengeSchema(db); // PR-F1 — identity-claim publication-challenge state (server-side nonce hash; schema only)
403
+ // NB: initAdminCoordinationSchema is intentionally NOT called here — it FKs admin_audit_log, which is
404
+ // created later; it runs right after the admin_audit_log block below (search initAdminCoordinationSchema).
388
405
  initSnfSchema(db);
389
406
  initExternalAnchorSchema(db);
390
407
  // 启动时检查月衰减(last_decay_at ≥25 天才触发,重启幂等)
@@ -401,11 +418,11 @@ ensureEvidenceColumns(db);
401
418
  initAnchorRegistrySchema(db);
402
419
  // boot-order fix(2026-05-26):anchor migration 引用 users.handle / search_anchor,
403
420
  // 但对应 ALTER TABLE 在 735+/958+ 行才跑。旧 DB(v3 era)触发 prepare 失败 → 此处 catch
404
- // 后 warn 不阻塞 server,但日志噪音 → 预热那两列让 migration 真正能跑
405
- try {
406
- db.exec("ALTER TABLE users ADD COLUMN handle TEXT");
407
- }
408
- catch { }
421
+ // 后 warn 不阻塞 server,但日志噪音 → 预热那两列让 migration 真正能跑。
422
+ // handle 现由 initRegisterListSearchColumns 在此预热(与 MCP runtime schema 同源,见
423
+ // src/runtime/webaz-schema-helpers.ts);该 helper 同时建 permanent_code/region + 11
424
+ // products 结构化字段(纯非钱列,从下方各 inline 块单点收口到此处,CREATE-before-ALTER 不变)。
425
+ initRegisterListSearchColumns(db);
409
426
  try {
410
427
  db.exec("ALTER TABLE users ADD COLUMN search_anchor TEXT");
411
428
  }
@@ -443,27 +460,9 @@ catch (e) {
443
460
  console.warn('[anchor-registry] migration:', e.message);
444
461
  }
445
462
  // ─── 验证员白名单表 ───────────────────────────────────────────────
446
- db.exec(`
447
- CREATE TABLE IF NOT EXISTS verifier_whitelist (
448
- user_id TEXT PRIMARY KEY,
449
- added_at TEXT DEFAULT (datetime('now')),
450
- note TEXT
451
- )
452
- `);
463
+ initVerifierWhitelistSchema(db);
453
464
  // ─── MCP 工具调用埋点表(远程上报)─────────────────────────────────
454
- db.exec(`
455
- CREATE TABLE IF NOT EXISTS mcp_tool_calls (
456
- id INTEGER PRIMARY KEY AUTOINCREMENT,
457
- tool_name TEXT NOT NULL,
458
- user_id_hash TEXT,
459
- server_version TEXT,
460
- outcome TEXT NOT NULL,
461
- latency_ms INTEGER NOT NULL,
462
- ts TEXT NOT NULL DEFAULT (datetime('now'))
463
- )
464
- `);
465
- db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tc_ts ON mcp_tool_calls(ts)`);
466
- db.exec(`CREATE INDEX IF NOT EXISTS idx_mcp_tc_tool ON mcp_tool_calls(tool_name, ts)`);
465
+ initMcpToolCallsSchema(db);
467
466
  // ─── 内部审核账号(固定 ID,密钥由 MASTER_SEED 派生,幂等)────────
468
467
  const INTERNAL_AUDITOR_ID = 'usr_iaudit_001';
469
468
  const INTERNAL_AUDITOR_KEY = 'key_iaudit_' + createHmac('sha256', MASTER_SEED).update('internal_auditor_v1').digest('hex').slice(0, 32);
@@ -521,11 +520,10 @@ try {
521
520
  }
522
521
  catch { }
523
522
  // Tokenomics 推土机轨道 — Phase 1(分享现金分润)
524
- // 详见 docs/modules/tokenomics-pv-commission.md
525
523
  for (const stmt of [
526
524
  'ALTER TABLE users ADD COLUMN sponsor_id TEXT',
527
525
  'ALTER TABLE users ADD COLUMN sponsor_path TEXT',
528
- "ALTER TABLE users ADD COLUMN region TEXT DEFAULT 'global'",
526
+ // users.region moved to initRegisterListSearchColumns (single source, shared w/ MCP) — see ~line 494.
529
527
  // Admin 分级:root 全权 / regional 按 admin_scope 区域受限
530
528
  "ALTER TABLE users ADD COLUMN admin_type TEXT", // root | regional
531
529
  "ALTER TABLE users ADD COLUMN admin_scope TEXT", // global | china | us | eu | india | singapore
@@ -583,28 +581,7 @@ for (const stmt of [
583
581
  // M8 二手板块:独立表,避免污染 products 商家货架
584
582
  // 关键差异:1 件即 1 件(无库存)、个人卖家无需 seller 角色、无质保、协议费 1%(vs 商家 2%)
585
583
  try {
586
- db.exec(`CREATE TABLE IF NOT EXISTS secondhand_items (
587
- id TEXT PRIMARY KEY,
588
- seller_id TEXT NOT NULL,
589
- title TEXT NOT NULL,
590
- description TEXT,
591
- category TEXT NOT NULL, -- phone/computer/appliance/furniture/clothing/book/toy/sports/other
592
- condition_grade TEXT NOT NULL, -- brand_new/like_new/lightly_used/well_used/heavily_used
593
- price REAL NOT NULL,
594
- negotiable INTEGER DEFAULT 0,
595
- images TEXT, -- JSON 数组:dataURL 字符串 (最多 9 张)
596
- region TEXT,
597
- fulfillment TEXT DEFAULT 'both', -- shipping / in_person / both
598
- status TEXT DEFAULT 'available', -- available / reserved / sold / closed
599
- view_count INTEGER DEFAULT 0,
600
- created_at TEXT DEFAULT (datetime('now')),
601
- updated_at TEXT DEFAULT (datetime('now')),
602
- sold_at TEXT,
603
- sold_order_id TEXT
604
- )`);
605
- db.exec(`CREATE INDEX IF NOT EXISTS idx_si_status_created ON secondhand_items(status, created_at DESC)`);
606
- db.exec(`CREATE INDEX IF NOT EXISTS idx_si_seller ON secondhand_items(seller_id, status)`);
607
- db.exec(`CREATE INDEX IF NOT EXISTS idx_si_cat ON secondhand_items(category, status)`);
584
+ initSecondhandItemsSchema(db);
608
585
  }
609
586
  catch (e) {
610
587
  console.error('[secondhand schema]', e);
@@ -729,7 +706,7 @@ db.exec(`
729
706
  max_levels INTEGER NOT NULL, -- 0=完全禁 MLM / 1=仅 L1 / 2=L1+L2 / 3=全三级(仅控佣金层级)
730
707
  active INTEGER DEFAULT 1,
731
708
  mlm_ui_visible INTEGER DEFAULT 1, -- 0=UI 全面隐藏推土机/分润链/佣金展示
732
- pv_enabled INTEGER DEFAULT 0 -- 2026-06-04 解耦:PV 双轨/对碰系统是否开启(独立于 max_levels)。默认 0=全关
709
+ pv_enabled INTEGER DEFAULT 0 -- 区域级 PV 开关(独立于佣金层级 max_levels)。默认 0=关
733
710
  )
734
711
  `);
735
712
  // Phase B 迁移:加 mlm_ui_visible 列
@@ -737,7 +714,7 @@ try {
737
714
  db.exec('ALTER TABLE region_config ADD COLUMN mlm_ui_visible INTEGER DEFAULT 1');
738
715
  }
739
716
  catch { /* 已存在 */ }
740
- // 2026-06-04 解耦迁移:加 pv_enabled 列(PV 双轨独立开关,与佣金层级 max_levels 分离)
717
+ // 2026-06-04 解耦迁移:加 pv_enabled 列(区域级 PV 开关,与佣金层级 max_levels 分离)
741
718
  try {
742
719
  db.exec('ALTER TABLE region_config ADD COLUMN pv_enabled INTEGER DEFAULT 0');
743
720
  }
@@ -780,29 +757,9 @@ catch { }
780
757
  }
781
758
  catch { }
782
759
  });
783
- // P13: 购物车
784
- db.exec(`
785
- CREATE TABLE IF NOT EXISTS cart_items (
786
- user_id TEXT NOT NULL,
787
- product_id TEXT NOT NULL,
788
- qty INTEGER NOT NULL DEFAULT 1,
789
- added_at TEXT DEFAULT (datetime('now')),
790
- PRIMARY KEY (user_id, product_id)
791
- )
792
- `);
793
- // P14: 关注关系(社交电商)
794
- db.exec(`
795
- CREATE TABLE IF NOT EXISTS follows (
796
- follower_id TEXT NOT NULL,
797
- followee_id TEXT NOT NULL,
798
- created_at TEXT DEFAULT (datetime('now')),
799
- PRIMARY KEY (follower_id, followee_id)
800
- )
801
- `);
802
- try {
803
- db.exec('CREATE INDEX IF NOT EXISTS idx_follows_followee ON follows(followee_id)');
804
- }
805
- catch { }
760
+ // P13: 购物车 / P14: 关注关系(社交电商)→ server-schema.ts
761
+ initCartItemsSchema(db);
762
+ initFollowsSchema(db);
806
763
  // P14: 用户 feed 可见性开关(默认公开)
807
764
  try {
808
765
  db.exec("ALTER TABLE users ADD COLUMN feed_visible INTEGER DEFAULT 1");
@@ -857,6 +814,13 @@ try {
857
814
  catch { }
858
815
  // 已注册默认参数(首次启动 seed) — P0-2 加 min/max 边界
859
816
  const DEFAULT_PARAMS = [
817
+ // Category C:参与记录 vs 奖励兑付,分开两套开关。
818
+ // · 参与记录默认 ON:PV 是参与/贡献记录(非收益/非兑付/非权益),默认允许记录(只在显式 =0 时关)。
819
+ // · 匹配奖励引擎已切除(#401):该标志保留但只门控一个 no-op stub,无兑付路径;
820
+ // matching_rewards_activation_cleared = 法律/治理放行、matching_rewards_active = 运营开关。pre-launch 均 0。
821
+ { key: 'participation_recording_active', value: '1', type: 'number', description: '参与记录开关:PV 生成+聚合(参与记录,非收益/非兑付);默认 1=开。置 0 才停止记录。', category: 'system', min: 0, max: 1 },
822
+ { key: 'matching_rewards_active', value: '0', type: 'number', description: '匹配奖励运营开关(引擎已切除 #401,现仅门控 no-op stub;无兑付);默认 0=关。', category: 'system', min: 0, max: 1 },
823
+ { key: 'matching_rewards_activation_cleared', value: '0', type: 'number', description: '奖励兑付法律/治理放行标志(开启奖励前必须经合规+治理审批置 1);默认 0。', category: 'system', min: 0, max: 1 },
860
824
  // RFC-008:平台费硬帽 2%(=当前稳态 → 治理只能在 0–2% 减免、永不涨)。合计封顶 = 平台费2% + fund_base1% = 3%。宪法级合法性见 CHARTER 修订(单独治理步)。
861
825
  { key: 'protocol_fee_rate_shop', value: '0.02', type: 'number', description: '商家订单平台费率(RFC-008 硬帽 2%,只减不涨;前期可减免)', category: 'fee', min: 0, max: 0.02 },
862
826
  { key: 'protocol_fee_rate_secondhand', value: '0.01', type: 'number', description: '二手订单平台费率(RFC-008 硬帽 2%,只减不涨)', category: 'fee', min: 0, max: 0.02 },
@@ -881,6 +845,8 @@ const DEFAULT_PARAMS = [
881
845
  { key: 'max_addresses_per_user', value: '20', type: 'number', description: '单用户最多收货地址数', category: 'limit', min: 1, max: 100 },
882
846
  { key: 'max_compare_items', value: '4', type: 'number', description: '商品对比最多件数', category: 'limit', min: 2, max: 10 },
883
847
  { key: 'feedback_rate_per_hour', value: '5', type: 'number', description: '反馈工单每小时上限', category: 'limit', min: 1, max: 100 },
848
+ { key: 'max_quota_extra_count', value: '50', type: 'number', description: 'PR#18 build_task 扩容申请:单次最多额外任务数', category: 'limit', min: 1, max: 500 },
849
+ { key: 'max_quota_duration_hours', value: '72', type: 'number', description: 'PR#18 build_task 扩容授权:最长有效期(小时)', category: 'limit', min: 1, max: 2160 },
884
850
  { key: 'export_csv_limit', value: '5000', type: 'number', description: '订单导出 CSV 行数上限', category: 'limit', min: 100, max: 50000 },
885
851
  { key: 'return_window_extension_days', value: '0', type: 'number', description: '退货窗口全局延长天数', category: 'general', min: 0, max: 90 },
886
852
  // Wave G-2: USDC / 链上配置
@@ -948,6 +914,9 @@ const DEFAULT_PARAMS = [
948
914
  // 假设:这两个 param 都满足"increase = more protection"语义(见 admin-protocol-params.ts 头部注释)
949
915
  { 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 },
950
916
  { key: 'constitutional_notice_days', value: '60', type: 'number', description: 'CHARTER §4 I-4:宪法级修改 RFC 公示期(天)— only-increase 防绕过', category: 'constitutional', min: 30, max: 365 },
917
+ // #420 P1-2/P1-3/P1-4:反滥用阈值(agent 信任公式 / strike 阶梯 / verifier outlier)→ 治理可调。
918
+ // 默认值 === 抽取前硬编码字面量(单一真相源在 anti-abuse-thresholds.ts;测试强制校验一致)。
919
+ ...ANTI_ABUSE_PARAMS,
951
920
  ];
952
921
  for (const p of DEFAULT_PARAMS) {
953
922
  try {
@@ -1161,23 +1130,7 @@ function disbursePlatformReward(userId, amount, source, ref) {
1161
1130
  // Wave E-5: PWA Push 订阅
1162
1131
  // 注:实际 push 投递需要 web-push 库(npm i web-push)+ VAPID 私钥签名;
1163
1132
  // 当前实现只做订阅层 + SW push 事件处理,留待 web-push 接入后即可发送
1164
- db.exec(`
1165
- CREATE TABLE IF NOT EXISTS push_subscriptions (
1166
- id TEXT PRIMARY KEY,
1167
- user_id TEXT NOT NULL,
1168
- endpoint TEXT NOT NULL,
1169
- p256dh TEXT NOT NULL,
1170
- auth TEXT NOT NULL,
1171
- user_agent TEXT,
1172
- enabled INTEGER DEFAULT 1,
1173
- created_at TEXT DEFAULT (datetime('now')),
1174
- UNIQUE(user_id, endpoint)
1175
- )
1176
- `);
1177
- try {
1178
- db.exec('CREATE INDEX IF NOT EXISTS idx_push_user ON push_subscriptions(user_id, enabled)');
1179
- }
1180
- catch { }
1133
+ initPushSubscriptionsSchema(db);
1181
1134
  // 2026-05-22 V2:verifier 新任务通知偏好(默认开,可关)
1182
1135
  try {
1183
1136
  db.exec('ALTER TABLE users ADD COLUMN notify_claim_tasks INTEGER DEFAULT 1');
@@ -1250,14 +1203,8 @@ catch { } // 完整店铺介绍(多段)
1250
1203
  // ─── 4 层身份模型 ─────────────────────────────────────────
1251
1204
  // id (内部 usr_xxx, 永不可改) + permanent_code (6 位 Crockford base32, 永不可改, 对外短码)
1252
1205
  // + handle (@username, 可改 7天1次/年3次) + name (昵称, 可重复可改)
1253
- try {
1254
- db.exec("ALTER TABLE users ADD COLUMN permanent_code TEXT");
1255
- }
1256
- catch { }
1257
- try {
1258
- db.exec("ALTER TABLE users ADD COLUMN handle TEXT");
1259
- }
1260
- catch { }
1206
+ // permanent_code / handle + 其唯一索引已上移到 initRegisterListSearchColumns(~line 494,
1207
+ // MCP runtime schema 同源);此处仅保留 handle 的附属列(不在 register/list/search 路径上)
1261
1208
  try {
1262
1209
  db.exec("ALTER TABLE users ADD COLUMN handle_last_created_at TEXT");
1263
1210
  }
@@ -1266,14 +1213,6 @@ try {
1266
1213
  db.exec("ALTER TABLE users ADD COLUMN handle_change_log TEXT");
1267
1214
  }
1268
1215
  catch { } // JSON: [{at, from}], 保留近 365 天
1269
- try {
1270
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_permanent_code ON users(permanent_code) WHERE permanent_code IS NOT NULL");
1271
- }
1272
- catch { }
1273
- try {
1274
- db.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_users_handle ON users(handle) WHERE handle IS NOT NULL");
1275
- }
1276
- catch { }
1277
1216
  // P15 雷达扫描:粗粒度地理位置(0.1° ≈ 11km × 11km,QVOD 风格匿名聚合)
1278
1217
  try {
1279
1218
  db.exec("ALTER TABLE users ADD COLUMN geo_lat REAL");
@@ -1369,27 +1308,7 @@ const LARGE_WITHDRAW_THRESHOLD = 100;
1369
1308
  // 用途:防 api_key 泄露后无法吊销的根本问题。每个 api_key 关联一个 session 行;
1370
1309
  // 用户可在 "活跃会话" 页查看 IP/UA/最后活跃,单点吊销或一键全登出。
1371
1310
  // "一键全登出" = rotate users.api_key(所有旧 key 即刻 401,新 key 在 session 表里)。
1372
- db.exec(`
1373
- CREATE TABLE IF NOT EXISTS user_sessions (
1374
- id TEXT PRIMARY KEY,
1375
- user_id TEXT NOT NULL,
1376
- api_key TEXT NOT NULL,
1377
- ip TEXT,
1378
- user_agent TEXT,
1379
- fingerprint_hash TEXT,
1380
- created_at TEXT DEFAULT (datetime('now')),
1381
- last_seen_at TEXT DEFAULT (datetime('now')),
1382
- revoked_at TEXT
1383
- )
1384
- `);
1385
- try {
1386
- db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_user ON user_sessions(user_id, revoked_at)');
1387
- }
1388
- catch { }
1389
- try {
1390
- db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_key ON user_sessions(api_key)');
1391
- }
1392
- catch { }
1311
+ initUserSessionsSchema(db);
1393
1312
  // A4 智能下单:用户默认地址(搜索时自动过滤不可达商品 + 下单时预填)
1394
1313
  try {
1395
1314
  db.exec("ALTER TABLE users ADD COLUMN default_address_text TEXT");
@@ -1405,19 +1324,7 @@ try {
1405
1324
  }
1406
1325
  catch { }
1407
1326
  // A2 黑名单(精准匹配护栏):买家可拉黑卖家,搜索时自动过滤
1408
- db.exec(`
1409
- CREATE TABLE IF NOT EXISTS user_blocklist (
1410
- blocker_id TEXT NOT NULL,
1411
- blocked_id TEXT NOT NULL,
1412
- reason TEXT,
1413
- created_at TEXT DEFAULT (datetime('now')),
1414
- PRIMARY KEY (blocker_id, blocked_id)
1415
- )
1416
- `);
1417
- try {
1418
- db.exec("CREATE INDEX IF NOT EXISTS idx_blocklist_blocker ON user_blocklist(blocker_id)");
1419
- }
1420
- catch { }
1327
+ initUserBlocklistSchema(db);
1421
1328
  // P-Distrib β:分布式内容层(外链 shareables + P2P 原生 manifests + pin 经济)
1422
1329
  // shareables = 外链分享(YouTube/TikTok/小红书 等外部内容)— 仅索引 URL,零内容存储
1423
1330
  db.exec(`
@@ -1503,99 +1410,12 @@ try {
1503
1410
  db.exec("CREATE INDEX IF NOT EXISTS idx_share_order ON shareables(related_order_id) WHERE related_order_id IS NOT NULL");
1504
1411
  }
1505
1412
  catch { }
1506
- // 审计修 C-1:笔记图片 hash 索引表 O(1) 剽窃检测,替代全表扫描
1507
- // hash PRIMARY KEY 天然唯一约束;删笔记时同步删对应行
1508
- db.exec(`
1509
- CREATE TABLE IF NOT EXISTS note_photo_index (
1510
- hash TEXT PRIMARY KEY,
1511
- shareable_id TEXT NOT NULL
1512
- )
1513
- `);
1514
- try {
1515
- db.exec("CREATE INDEX IF NOT EXISTS idx_npi_shareable ON note_photo_index(shareable_id)");
1516
- }
1517
- catch { }
1518
- // Wave A-1: 个人心愿单(独立于慈善 wishes)
1519
- db.exec(`
1520
- CREATE TABLE IF NOT EXISTS user_wishlist (
1521
- user_id TEXT NOT NULL,
1522
- product_id TEXT NOT NULL,
1523
- note TEXT,
1524
- notify_price_drop INTEGER DEFAULT 1,
1525
- notify_back_in_stock INTEGER DEFAULT 1,
1526
- price_at_add REAL,
1527
- created_at TEXT DEFAULT (datetime('now')),
1528
- PRIMARY KEY(user_id, product_id)
1529
- )
1530
- `);
1531
- try {
1532
- db.exec('CREATE INDEX IF NOT EXISTS idx_wl_product ON user_wishlist(product_id)');
1533
- }
1534
- catch { }
1535
- // Wave A-2: 商品 Q&A(公开问答 — 自动 FAQ + 防虚假承诺)
1536
- db.exec(`
1537
- CREATE TABLE IF NOT EXISTS product_qa (
1538
- id TEXT PRIMARY KEY,
1539
- product_id TEXT NOT NULL,
1540
- asker_id TEXT NOT NULL,
1541
- seller_id TEXT NOT NULL,
1542
- question TEXT NOT NULL,
1543
- answer TEXT,
1544
- answered_at TEXT,
1545
- is_public INTEGER DEFAULT 1,
1546
- helpful_count INTEGER DEFAULT 0,
1547
- created_at TEXT DEFAULT (datetime('now'))
1548
- )
1549
- `);
1550
- try {
1551
- db.exec('CREATE INDEX IF NOT EXISTS idx_qa_product ON product_qa(product_id, created_at DESC)');
1552
- }
1553
- catch { }
1554
- try {
1555
- db.exec('CREATE INDEX IF NOT EXISTS idx_qa_seller ON product_qa(seller_id, answered_at)');
1556
- }
1557
- catch { }
1558
- try {
1559
- db.exec('CREATE INDEX IF NOT EXISTS idx_qa_asker ON product_qa(asker_id)');
1560
- }
1561
- catch { }
1562
- // 防重复 +1 的 votes 表
1563
- db.exec(`
1564
- CREATE TABLE IF NOT EXISTS product_qa_helpful_voters (
1565
- qa_id TEXT NOT NULL,
1566
- user_id TEXT NOT NULL,
1567
- voted_at TEXT DEFAULT (datetime('now')),
1568
- PRIMARY KEY (qa_id, user_id)
1569
- )
1570
- `);
1571
- // Wave A-3: 优惠券 / 限时折扣(卖家发券 · 全店满减 · 单品限时)
1572
- db.exec(`
1573
- CREATE TABLE IF NOT EXISTS coupons (
1574
- id TEXT PRIMARY KEY,
1575
- seller_id TEXT NOT NULL,
1576
- code TEXT NOT NULL,
1577
- scope TEXT NOT NULL, -- 'product' | 'shop' | 'all'
1578
- scope_id TEXT, -- product_id when scope='product'
1579
- discount_type TEXT NOT NULL, -- 'percentage' | 'fixed'
1580
- discount_value REAL NOT NULL,
1581
- min_order_amount REAL DEFAULT 0,
1582
- max_uses INTEGER DEFAULT 0, -- 0 = unlimited
1583
- uses_count INTEGER DEFAULT 0,
1584
- starts_at TEXT,
1585
- expires_at TEXT,
1586
- is_active INTEGER DEFAULT 1,
1587
- created_at TEXT DEFAULT (datetime('now')),
1588
- UNIQUE(seller_id, code)
1589
- )
1590
- `);
1591
- try {
1592
- db.exec('CREATE INDEX IF NOT EXISTS idx_coupons_seller ON coupons(seller_id, is_active)');
1593
- }
1594
- catch { }
1595
- try {
1596
- db.exec('CREATE INDEX IF NOT EXISTS idx_coupons_scope ON coupons(scope, scope_id) WHERE is_active = 1');
1597
- }
1598
- catch { }
1413
+ // 笔记图片 hash 索引 / Wave A 购物表(心愿单 · 商品Q&A · 优惠券)→ server-schema.ts
1414
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1415
+ initNotePhotoIndexSchema(db);
1416
+ initUserWishlistSchema(db);
1417
+ initProductQaSchema(db);
1418
+ initCouponsSchema(db);
1599
1419
  // Orders 表加 coupon 字段(记录使用了哪张券、折扣多少)
1600
1420
  for (const stmt of [
1601
1421
  'ALTER TABLE orders ADD COLUMN coupon_id TEXT',
@@ -1606,147 +1426,20 @@ for (const stmt of [
1606
1426
  }
1607
1427
  catch { }
1608
1428
  }
1609
- // Wave A-4: 平台公告(admin 发布 角色 / 区域定向)
1610
- db.exec(`
1611
- CREATE TABLE IF NOT EXISTS announcements (
1612
- id TEXT PRIMARY KEY,
1613
- author_id TEXT NOT NULL,
1614
- title TEXT NOT NULL,
1615
- body TEXT NOT NULL,
1616
- target_roles TEXT, -- JSON array: ['buyer','seller'] or null=all
1617
- target_regions TEXT, -- JSON array: ['china','us'] or null=all
1618
- severity TEXT DEFAULT 'info', -- 'info' | 'warning' | 'critical'
1619
- is_active INTEGER DEFAULT 1,
1620
- starts_at TEXT,
1621
- expires_at TEXT,
1622
- created_at TEXT DEFAULT (datetime('now'))
1623
- )
1624
- `);
1625
- try {
1626
- db.exec('CREATE INDEX IF NOT EXISTS idx_ann_active ON announcements(is_active, created_at DESC)');
1627
- }
1628
- catch { }
1629
- // 用户阅读记录(PK 防重复 dismiss)
1630
- db.exec(`
1631
- CREATE TABLE IF NOT EXISTS announcement_reads (
1632
- user_id TEXT NOT NULL,
1633
- announcement_id TEXT NOT NULL,
1634
- read_at TEXT DEFAULT (datetime('now')),
1635
- PRIMARY KEY (user_id, announcement_id)
1636
- )
1637
- `);
1638
- // Wave B-2: 预售 / waitlist(缺货商品允许买家排队 → 回货时通知)
1639
- db.exec(`
1640
- CREATE TABLE IF NOT EXISTS product_waitlist (
1641
- user_id TEXT NOT NULL,
1642
- product_id TEXT NOT NULL,
1643
- desired_qty INTEGER DEFAULT 1,
1644
- note TEXT,
1645
- notified_at TEXT, -- 回货时填,表示已发通知
1646
- created_at TEXT DEFAULT (datetime('now')),
1647
- PRIMARY KEY (user_id, product_id)
1648
- )
1649
- `);
1650
- try {
1651
- db.exec('CREATE INDEX IF NOT EXISTS idx_waitlist_product ON product_waitlist(product_id) WHERE notified_at IS NULL');
1652
- }
1653
- catch { }
1654
- // Wave D-4: 限时促销 / Flash Sale
1655
- db.exec(`
1656
- CREATE TABLE IF NOT EXISTS flash_sales (
1657
- id TEXT PRIMARY KEY,
1658
- seller_id TEXT NOT NULL,
1659
- product_id TEXT NOT NULL,
1660
- variant_id TEXT, -- 可选,绑定具体规格
1661
- sale_price REAL NOT NULL,
1662
- original_price REAL NOT NULL, -- 创建时快照,用于显示「省 X」
1663
- max_qty INTEGER DEFAULT 0, -- 0 = 不限
1664
- sold_count INTEGER DEFAULT 0,
1665
- starts_at TEXT NOT NULL,
1666
- ends_at TEXT NOT NULL,
1667
- is_active INTEGER DEFAULT 1,
1668
- created_at TEXT DEFAULT (datetime('now'))
1669
- )
1670
- `);
1671
- try {
1672
- db.exec('CREATE INDEX IF NOT EXISTS idx_flash_product ON flash_sales(product_id, is_active)');
1673
- }
1674
- catch { }
1675
- try {
1676
- db.exec('CREATE INDEX IF NOT EXISTS idx_flash_seller ON flash_sales(seller_id, ends_at DESC)');
1677
- }
1678
- catch { }
1429
+ // Wave A-4 公告+阅读 / Wave B-2 预售waitlist / Wave D-4 限时促销 → server-schema.ts
1430
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1431
+ initAnnouncementsSchema(db);
1432
+ initProductWaitlistSchema(db);
1433
+ initFlashSalesSchema(db);
1679
1434
  // 公共助手:拿商品(含 variant 选项)当前生效的 flash sale
1680
1435
  // #1013 Phase 23: 已迁出到 routes/flash-sales.ts,本地 wrapper 让 orders 流程签名不变
1681
1436
  const getActiveFlashSale = (productId, variantId) => getActiveFlashSaleRaw(db, productId, variantId);
1682
1437
  // 2026-05-24 #978: 测评免单 (Trial Review Refund)
1683
1438
  // 卖家发新品时可开启「测评免单」计划:买家以原价正常下单,发笔记达 reach 阈值后系统自动退款
1684
1439
  // reach_score = views * 0.1 + shares * 1 + conversions * 10
1685
- db.exec(`
1686
- CREATE TABLE IF NOT EXISTS product_trial_campaigns (
1687
- id TEXT PRIMARY KEY, -- ptc_xxxx
1688
- -- 1 product 1 row (复用同一行:关闭后再开 = UPDATE status='active',避免 UNIQUE 阻断 reopen)
1689
- product_id TEXT NOT NULL UNIQUE REFERENCES products(id),
1690
- seller_id TEXT NOT NULL REFERENCES users(id),
1691
- quota_total INTEGER NOT NULL, -- 总名额 1-200
1692
- quota_claimed INTEGER NOT NULL DEFAULT 0,
1693
- reach_threshold INTEGER NOT NULL, -- 综合 reach 阈值 (默认 50)
1694
- min_chars INTEGER NOT NULL DEFAULT 50, -- 笔记最少字数
1695
- min_days_live INTEGER NOT NULL DEFAULT 7, -- 笔记需 live 至少 N 天才评估
1696
- status TEXT NOT NULL DEFAULT 'active', -- active / paused / closed
1697
- created_at TEXT DEFAULT (datetime('now')),
1698
- closed_at TEXT
1699
- )
1700
- `);
1701
- try {
1702
- db.exec("CREATE INDEX IF NOT EXISTS idx_ptc_seller ON product_trial_campaigns(seller_id, status)");
1703
- }
1704
- catch { }
1705
- try {
1706
- db.exec("CREATE INDEX IF NOT EXISTS idx_ptc_product ON product_trial_campaigns(product_id, status)");
1707
- }
1708
- catch { }
1709
- db.exec(`
1710
- CREATE TABLE IF NOT EXISTS product_trial_claims (
1711
- id TEXT PRIMARY KEY, -- pcl_xxxx
1712
- campaign_id TEXT NOT NULL REFERENCES product_trial_campaigns(id),
1713
- product_id TEXT NOT NULL REFERENCES products(id),
1714
- seller_id TEXT NOT NULL REFERENCES users(id),
1715
- buyer_id TEXT NOT NULL REFERENCES users(id),
1716
- order_id TEXT NOT NULL REFERENCES orders(id),
1717
- note_id TEXT, -- shareables.id with type='note'
1718
- status TEXT NOT NULL DEFAULT 'pending_note', -- pending_note / pending_threshold / refunded / expired / cancelled
1719
- reach_score REAL DEFAULT 0,
1720
- metrics_json TEXT, -- 最新评估的 {views, shares, conversions} 快照
1721
- refund_amount REAL,
1722
- refunded_at TEXT,
1723
- expired_at TEXT,
1724
- last_eval_at TEXT,
1725
- claimed_at TEXT DEFAULT (datetime('now')),
1726
- note_linked_at TEXT,
1727
- UNIQUE(buyer_id, product_id) -- 一买家一商品仅 1 个名额
1728
- )
1729
- `);
1730
- try {
1731
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_campaign ON product_trial_claims(campaign_id, status)");
1732
- }
1733
- catch { }
1734
- try {
1735
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_buyer ON product_trial_claims(buyer_id, status)");
1736
- }
1737
- catch { }
1738
- try {
1739
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_seller ON product_trial_claims(seller_id, status)");
1740
- }
1741
- catch { }
1742
- try {
1743
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_eval ON product_trial_claims(status, last_eval_at)");
1744
- }
1745
- catch { }
1746
- try {
1747
- db.exec("CREATE INDEX IF NOT EXISTS idx_pcl_note ON product_trial_claims(note_id) WHERE note_id IS NOT NULL");
1748
- }
1749
- catch { }
1440
+ // 测评免单计划 + 认领 → server-schema.ts;claims 的 snap/audit ALTER 刻意留原位(紧跟下方)
1441
+ initProductTrialCampaignsSchema(db);
1442
+ initProductTrialClaimsSchema(db);
1750
1443
  // 审计 P0-1:claim 时快照 campaign 配置,cron 评估按快照而非当前活动(防卖家中途上调阈值白嫖)
1751
1444
  for (const col of [
1752
1445
  'ALTER TABLE product_trial_claims ADD COLUMN snap_reach_threshold INTEGER',
@@ -1763,29 +1456,8 @@ for (const col of [
1763
1456
  }
1764
1457
  catch { /* 已存在 */ }
1765
1458
  }
1766
- // 2026-05-25 邮箱订阅独立表(GDPR-ready)— ideas 解耦
1767
- // consent 显式存;unsubscribe_token 让用户主动退订;source 区分来源
1768
- db.exec(`
1769
- CREATE TABLE IF NOT EXISTS email_subscriptions (
1770
- id TEXT PRIMARY KEY,
1771
- email TEXT NOT NULL UNIQUE,
1772
- source TEXT NOT NULL DEFAULT 'welcome',
1773
- consent_at TEXT NOT NULL DEFAULT (datetime('now')),
1774
- unsubscribe_token TEXT NOT NULL UNIQUE,
1775
- unsubscribed_at TEXT,
1776
- ip_hash TEXT,
1777
- user_id TEXT,
1778
- created_at TEXT DEFAULT (datetime('now'))
1779
- )
1780
- `);
1781
- try {
1782
- db.exec("CREATE INDEX IF NOT EXISTS idx_es_status ON email_subscriptions(unsubscribed_at, created_at DESC)");
1783
- }
1784
- catch { }
1785
- try {
1786
- db.exec("CREATE INDEX IF NOT EXISTS idx_es_source ON email_subscriptions(source, created_at DESC)");
1787
- }
1788
- catch { }
1459
+ // 邮箱订阅独立表(GDPR-ready)→ server-schema.ts;后续 ALTER 列扩展刻意留原位(紧跟下方)
1460
+ initEmailSubscriptionsSchema(db);
1789
1461
  // 2026-05-26: 用户期望身份 + 备注(welcome 表单丰富化)
1790
1462
  try {
1791
1463
  db.exec("ALTER TABLE email_subscriptions ADD COLUMN role_preference TEXT");
@@ -1808,71 +1480,13 @@ try {
1808
1480
  db.exec("ALTER TABLE email_subscriptions ADD COLUMN handled_by TEXT");
1809
1481
  }
1810
1482
  catch { }
1811
- // 2026-05-24 首屏「我有建议」公开收集(匿名可投,登录态自动绑 user_id)
1812
- db.exec(`
1813
- CREATE TABLE IF NOT EXISTS public_ideas (
1814
- id TEXT PRIMARY KEY,
1815
- user_id TEXT, -- 可空(匿名提交)
1816
- contact TEXT, -- 可选 email / handle / 任何联系方式
1817
- content TEXT NOT NULL,
1818
- ip_hash TEXT,
1819
- ua_hash TEXT,
1820
- status TEXT NOT NULL DEFAULT 'new', -- new / triaged / resolved / spam
1821
- created_at TEXT DEFAULT (datetime('now'))
1822
- )
1823
- `);
1824
- try {
1825
- db.exec("CREATE INDEX IF NOT EXISTS idx_pi_status ON public_ideas(status, created_at DESC)");
1826
- }
1827
- catch { }
1828
- try {
1829
- db.exec("CREATE INDEX IF NOT EXISTS idx_pi_rate ON public_ideas(ip_hash, created_at)");
1830
- }
1831
- catch { }
1832
- // 2026-05-24 #959: 拍卖「⏰ 提醒我」
1833
- // 买家订阅拍卖,cron 在 deadline - lead_minutes 时发通知;1 个订阅 = 多行(每个 lead 时间一行)
1834
- // 默认订阅 = [60, 10](结束前 1h + 10min 各提醒一次)
1835
- db.exec(`
1836
- CREATE TABLE IF NOT EXISTS auction_reminders (
1837
- id TEXT PRIMARY KEY, -- arm_xxxx
1838
- auction_id TEXT NOT NULL REFERENCES auctions(id),
1839
- user_id TEXT NOT NULL REFERENCES users(id),
1840
- lead_minutes INTEGER NOT NULL, -- 提前多少分钟提醒
1841
- fire_at TEXT NOT NULL, -- deadline - lead_minutes(创建时算好)
1842
- sent_at TEXT,
1843
- created_at TEXT DEFAULT (datetime('now')),
1844
- UNIQUE(auction_id, user_id, lead_minutes)
1845
- )
1846
- `);
1847
- try {
1848
- db.exec("CREATE INDEX IF NOT EXISTS idx_arm_due ON auction_reminders(sent_at, fire_at) WHERE sent_at IS NULL");
1849
- }
1850
- catch { }
1851
- try {
1852
- db.exec("CREATE INDEX IF NOT EXISTS idx_arm_user ON auction_reminders(user_id, auction_id)");
1853
- }
1854
- catch { }
1855
- // Wave D-3: 用户反馈 / 客服工单(buyer-to-platform,独立于 disputes)
1856
- db.exec(`
1857
- CREATE TABLE IF NOT EXISTS feedback_tickets (
1858
- id TEXT PRIMARY KEY,
1859
- user_id TEXT NOT NULL,
1860
- category TEXT NOT NULL, -- 'bug' | 'abuse' | 'feature' | 'account' | 'other'
1861
- severity TEXT DEFAULT 'medium', -- 'low' | 'medium' | 'high'
1862
- subject TEXT NOT NULL,
1863
- body TEXT NOT NULL,
1864
- status TEXT NOT NULL DEFAULT 'open', -- open | in_progress | resolved | closed
1865
- admin_reply TEXT,
1866
- replied_by TEXT,
1867
- replied_at TEXT,
1868
- created_at TEXT DEFAULT (datetime('now')),
1869
- updated_at TEXT DEFAULT (datetime('now'))
1870
- )
1871
- `);
1872
- try {
1873
- db.exec('CREATE INDEX IF NOT EXISTS idx_feedback_user ON feedback_tickets(user_id, created_at DESC)');
1874
- }
1875
- catch { }
1483
+ // 首屏「我有建议」公开收集 / #959 拍卖「⏰ 提醒我」 → server-schema.ts
1484
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变(email_subscriptions 仍留原位)
1485
+ initPublicIdeasSchema(db);
1486
+ initAuctionRemindersSchema(db);
1487
+ // Wave D-3: 用户反馈 / 客服工单(buyer-to-platform,独立于 disputes)→ server-schema.ts
1488
+ // 后续 ALTER 列扩展刻意留原位(紧跟下方)
1489
+ initFeedbackTicketsSchema(db);
1876
1490
  // G-4: AI 建议回复
1877
1491
  try {
1878
1492
  db.exec('ALTER TABLE feedback_tickets ADD COLUMN ai_suggested_reply TEXT');
@@ -1891,21 +1505,9 @@ try {
1891
1505
  db.exec('ALTER TABLE feedback_tickets ADD COLUMN admin_seen_at TEXT');
1892
1506
  }
1893
1507
  catch { }
1894
- // W7 客服 ticket-thread — 多轮消息(user ↔ admin
1895
- db.exec(`
1896
- CREATE TABLE IF NOT EXISTS feedback_messages (
1897
- id TEXT PRIMARY KEY, -- fmsg_xxx
1898
- ticket_id TEXT NOT NULL,
1899
- sender_id TEXT NOT NULL,
1900
- sender_role TEXT NOT NULL, -- 'user' | 'admin'
1901
- body TEXT NOT NULL,
1902
- created_at TEXT DEFAULT (datetime('now'))
1903
- )
1904
- `);
1905
- try {
1906
- db.exec('CREATE INDEX IF NOT EXISTS idx_fmsg_ticket ON feedback_messages(ticket_id, created_at)');
1907
- }
1908
- catch { }
1508
+ // W7 客服 ticket-thread — 多轮消息(user ↔ admin)→ server-schema.ts
1509
+ // 后续 ALTER 列扩展刻意留原位(紧跟下方)
1510
+ initFeedbackMessagesSchema(db);
1909
1511
  // 跨窗反诈一致性:所有 thread 消息表加 flagged + flag_reasons
1910
1512
  try {
1911
1513
  db.exec('ALTER TABLE feedback_messages ADD COLUMN flagged INTEGER DEFAULT 0');
@@ -1916,50 +1518,10 @@ try {
1916
1518
  }
1917
1519
  catch { }
1918
1520
  // ─── 公开仲裁判例 (P1) ─────────────────────────────────────
1919
- // disputes 是当事人/仲裁员私域;dispute_cases 是裁决后的公开脱敏版本
1920
- db.exec(`
1921
- CREATE TABLE IF NOT EXISTS dispute_cases (
1922
- id TEXT PRIMARY KEY, -- dcase_xxx
1923
- dispute_id TEXT, -- 原始 disputes.id (内部追溯)
1924
- order_id TEXT,
1925
- product_id TEXT, -- 关键索引:按商品查公开判例
1926
- seller_id TEXT,
1927
- buyer_id TEXT, -- 仅内部使用,不外露
1928
- category_tag TEXT, -- 物流 / 质量 / 描述不符 / 售后 / 拒收 / 其他
1929
- winner TEXT, -- buyer / seller / split / dismissed
1930
- resolution TEXT, -- 简短人读判决 (如 '全额退款')
1931
- amount_bucket TEXT, -- '0-100' / '100-500' / '500-2000' / '2000+' WAZ
1932
- buyer_argument TEXT, -- 脱敏后买家陈述
1933
- seller_argument TEXT, -- 脱敏后卖家陈述
1934
- ruling_text TEXT, -- 仲裁员判决书
1935
- arbitrator_id TEXT,
1936
- fairness_yes INTEGER DEFAULT 0,
1937
- fairness_no INTEGER DEFAULT 0,
1938
- comment_count INTEGER DEFAULT 0,
1939
- published_at TEXT DEFAULT (datetime('now')),
1940
- created_at TEXT DEFAULT (datetime('now'))
1941
- )
1942
- `);
1943
- try {
1944
- db.exec('CREATE INDEX IF NOT EXISTS idx_dcase_product ON dispute_cases(product_id, published_at DESC)');
1945
- }
1946
- catch { }
1947
- try {
1948
- db.exec('CREATE INDEX IF NOT EXISTS idx_dcase_seller ON dispute_cases(seller_id, published_at DESC)');
1949
- }
1950
- catch { }
1951
- db.exec(`
1952
- CREATE TABLE IF NOT EXISTS dispute_comments (
1953
- id TEXT PRIMARY KEY, -- dcom_xxx
1954
- case_id TEXT NOT NULL,
1955
- commenter_id TEXT NOT NULL,
1956
- body TEXT NOT NULL,
1957
- flagged INTEGER DEFAULT 0,
1958
- likes INTEGER DEFAULT 0,
1959
- created_at TEXT DEFAULT (datetime('now')),
1960
- UNIQUE(case_id, commenter_id) -- 一案一人一次(防刷)
1961
- )
1962
- `);
1521
+ // 公开判例(裁决后脱敏版本,disputes 是当事人/仲裁员私域)→ server-schema.ts
1522
+ initDisputeCasesSchema(db);
1523
+ // 公开判例评论 server-schema.ts;anonymous ALTER + idx_dcom_case 刻意留原位
1524
+ initDisputeCommentsSchema(db);
1963
1525
  try {
1964
1526
  db.exec('ALTER TABLE dispute_comments ADD COLUMN anonymous INTEGER DEFAULT 0');
1965
1527
  }
@@ -1968,27 +1530,8 @@ try {
1968
1530
  db.exec('CREATE INDEX IF NOT EXISTS idx_dcom_case ON dispute_comments(case_id, created_at DESC)');
1969
1531
  }
1970
1532
  catch { }
1971
- // W5 仲裁公开评论楼中楼 — 单层子回复;保留原 dispute_comments UNIQUE
1972
- db.exec(`
1973
- CREATE TABLE IF NOT EXISTS dispute_comment_replies (
1974
- id TEXT PRIMARY KEY, -- drep_xxx
1975
- parent_comment_id TEXT NOT NULL, -- 指向 dispute_comments.id
1976
- case_id TEXT NOT NULL,
1977
- replier_id TEXT NOT NULL,
1978
- body TEXT NOT NULL,
1979
- anonymous INTEGER DEFAULT 0,
1980
- likes INTEGER DEFAULT 0,
1981
- created_at TEXT DEFAULT (datetime('now'))
1982
- )
1983
- `);
1984
- try {
1985
- db.exec('CREATE INDEX IF NOT EXISTS idx_drep_parent ON dispute_comment_replies(parent_comment_id, created_at)');
1986
- }
1987
- catch { }
1988
- try {
1989
- db.exec('CREATE INDEX IF NOT EXISTS idx_drep_case ON dispute_comment_replies(case_id, created_at DESC)');
1990
- }
1991
- catch { }
1533
+ // W5 仲裁公开评论楼中楼 — 单层子回复 server-schema.ts;后续 ALTER 刻意留原位
1534
+ initDisputeCommentRepliesSchema(db);
1992
1535
  // 跨窗反诈一致性
1993
1536
  try {
1994
1537
  db.exec('ALTER TABLE dispute_comment_replies ADD COLUMN flagged INTEGER DEFAULT 0');
@@ -2003,68 +1546,21 @@ try {
2003
1546
  db.exec('ALTER TABLE dispute_comments ADD COLUMN flag_reasons TEXT');
2004
1547
  }
2005
1548
  catch { }
2006
- // W6 笔记评论 — 原生 parent_id 楼中楼(仅 1 层;非 root 不可再 reply)
2007
- db.exec(`
2008
- CREATE TABLE IF NOT EXISTS shareable_comments (
2009
- id TEXT PRIMARY KEY, -- scom_xxx
2010
- shareable_id TEXT NOT NULL, -- shareables.id
2011
- commenter_id TEXT NOT NULL,
2012
- parent_id TEXT, -- 子评论指向父评论;root = NULL
2013
- body TEXT NOT NULL,
2014
- flagged INTEGER DEFAULT 0,
2015
- likes INTEGER DEFAULT 0,
2016
- created_at TEXT DEFAULT (datetime('now'))
2017
- )
2018
- `);
2019
- try {
2020
- db.exec('CREATE INDEX IF NOT EXISTS idx_scom_shareable ON shareable_comments(shareable_id, parent_id, created_at DESC)');
2021
- }
2022
- catch { }
1549
+ // W6 笔记评论 — 原生 parent_id 楼中楼(仅 1 层)→ server-schema.ts;flag_reasons ALTER 刻意留原位
1550
+ initShareableCommentsSchema(db);
2023
1551
  try {
2024
1552
  db.exec('ALTER TABLE shareable_comments ADD COLUMN flag_reasons TEXT');
2025
1553
  }
2026
1554
  catch { }
2027
- db.exec(`
2028
- CREATE TABLE IF NOT EXISTS dispute_fairness_votes (
2029
- case_id TEXT NOT NULL,
2030
- voter_id TEXT NOT NULL,
2031
- vote TEXT NOT NULL, -- 'yes' / 'no'
2032
- created_at TEXT DEFAULT (datetime('now')),
2033
- PRIMARY KEY (case_id, voter_id)
2034
- )
2035
- `);
1555
+ // 公开判例公平性投票 → server-schema.ts;idx_feedback_open 刻意留原位(非本表索引,不相邻)
1556
+ initDisputeFairnessVotesSchema(db);
2036
1557
  try {
2037
1558
  db.exec('CREATE INDEX IF NOT EXISTS idx_feedback_open ON feedback_tickets(status, created_at DESC) WHERE status IN (\'open\', \'in_progress\')');
2038
1559
  }
2039
1560
  catch { }
2040
- // Wave C-3: 买家评价 / 评分 完成订单后给卖家 1-5 + 文字
2041
- db.exec(`
2042
- CREATE TABLE IF NOT EXISTS order_ratings (
2043
- order_id TEXT PRIMARY KEY,
2044
- buyer_id TEXT NOT NULL,
2045
- seller_id TEXT NOT NULL,
2046
- product_id TEXT NOT NULL,
2047
- stars INTEGER NOT NULL, -- 1-5
2048
- comment TEXT,
2049
- reply TEXT, -- seller 可回复
2050
- replied_at TEXT,
2051
- created_at TEXT DEFAULT (datetime('now'))
2052
- )
2053
- `);
2054
- try {
2055
- db.exec('CREATE INDEX IF NOT EXISTS idx_rating_seller ON order_ratings(seller_id, created_at DESC)');
2056
- }
2057
- catch { }
2058
- try {
2059
- db.exec('CREATE INDEX IF NOT EXISTS idx_rating_product ON order_ratings(product_id, created_at DESC)');
2060
- }
2061
- catch { }
2062
- // P2 hot-path:覆盖 recommend_count 子查询(COUNT DISTINCT buyer_id WHERE product_id=? AND stars>=4)
2063
- try {
2064
- db.exec('CREATE INDEX IF NOT EXISTS idx_rating_recommend ON order_ratings(product_id, stars, buyer_id)');
2065
- }
2066
- catch { }
2067
- // P2 hot-path:覆盖 sales_count 子查询(COUNT WHERE product_id=? AND status=completed)
1561
+ // Wave C-3: 买家评价 / 评分 server-schema.ts;后续结构化维度 ALTER + 跨表 orders 索引刻意留原位
1562
+ initOrderRatingsSchema(db);
1563
+ // P2 hot-path:覆盖 sales_count 子查询(COUNT WHERE product_id=? AND status=completed)—— orders 表索引,留原位
2068
1564
  try {
2069
1565
  db.exec('CREATE INDEX IF NOT EXISTS idx_orders_product_status ON orders(product_id, status)');
2070
1566
  }
@@ -2095,73 +1591,12 @@ try {
2095
1591
  db.exec('ALTER TABLE order_ratings ADD COLUMN buyer_followup_at TEXT');
2096
1592
  }
2097
1593
  catch { }
2098
- db.exec(`
2099
- CREATE TABLE IF NOT EXISTS buyer_ratings (
2100
- order_id TEXT PRIMARY KEY,
2101
- seller_id TEXT NOT NULL,
2102
- buyer_id TEXT NOT NULL,
2103
- stars INTEGER NOT NULL,
2104
- comment TEXT,
2105
- dim_payment_speed INTEGER,
2106
- dim_communication INTEGER,
2107
- dim_responsiveness INTEGER,
2108
- hidden_until TEXT,
2109
- created_at TEXT DEFAULT (datetime('now'))
2110
- )
2111
- `);
2112
- try {
2113
- db.exec('CREATE INDEX IF NOT EXISTS idx_buyer_ratings_buyer ON buyer_ratings(buyer_id, created_at DESC)');
2114
- }
2115
- catch { }
2116
- // Wave C-2: 多收货地址簿 — buyer 保存常用地址,下单时选择默认地址
2117
- db.exec(`
2118
- CREATE TABLE IF NOT EXISTS user_addresses (
2119
- id TEXT PRIMARY KEY,
2120
- user_id TEXT NOT NULL,
2121
- label TEXT NOT NULL, -- 标签(家 / 公司 / 父母家)
2122
- recipient TEXT NOT NULL,
2123
- phone TEXT,
2124
- region TEXT, -- 省/市/区
2125
- detail TEXT NOT NULL, -- 详细地址
2126
- is_default INTEGER DEFAULT 0,
2127
- created_at TEXT DEFAULT (datetime('now')),
2128
- updated_at TEXT DEFAULT (datetime('now'))
2129
- )
2130
- `);
2131
- try {
2132
- db.exec('CREATE INDEX IF NOT EXISTS idx_addr_user ON user_addresses(user_id, is_default DESC)');
2133
- }
2134
- catch { }
2135
- // Wave B-3: 退货请求 — 买家在 return_days 窗口内可发起,卖家可接受/拒绝;拒绝后可走 dispute
2136
- db.exec(`
2137
- CREATE TABLE IF NOT EXISTS return_requests (
2138
- id TEXT PRIMARY KEY,
2139
- order_id TEXT NOT NULL,
2140
- buyer_id TEXT NOT NULL,
2141
- seller_id TEXT NOT NULL,
2142
- product_id TEXT NOT NULL,
2143
- reason TEXT NOT NULL, -- 'quality' | 'wrong_item' | 'damaged' | 'no_longer_needed' | 'other'
2144
- reason_text TEXT,
2145
- refund_amount DECIMAL(18,2), -- 默认 = order.total_amount
2146
- status TEXT NOT NULL DEFAULT 'pending', -- pending | accepted | rejected | refunded | escalated | cancelled
2147
- seller_response TEXT,
2148
- escalated_dispute_id TEXT,
2149
- created_at TEXT DEFAULT (datetime('now')),
2150
- resolved_at TEXT
2151
- )
2152
- `);
2153
- try {
2154
- db.exec('CREATE INDEX IF NOT EXISTS idx_returns_seller_pending ON return_requests(seller_id, status) WHERE status = \'pending\'');
2155
- }
2156
- catch { }
2157
- try {
2158
- db.exec('CREATE INDEX IF NOT EXISTS idx_returns_buyer ON return_requests(buyer_id, created_at)');
2159
- }
2160
- catch { }
2161
- try {
2162
- db.exec('CREATE INDEX IF NOT EXISTS idx_returns_order ON return_requests(order_id)');
2163
- }
2164
- catch { }
1594
+ // 反向评价:卖家给买家评分(双盲)→ server-schema.ts
1595
+ initBuyerRatingsSchema(db);
1596
+ // Wave C-2: 多收货地址簿 → server-schema.ts
1597
+ initUserAddressesSchema(db);
1598
+ // Wave B-3: 退货请求 → server-schema.ts;pickup ALTER 刻意留原位(紧跟下方)
1599
+ initReturnRequestsSchema(db);
2165
1600
  // 2026-05-22 L3+B3:退货上门取件(MVP — 仅声明阶段)
2166
1601
  // 完整状态机(accepted_pickup_pending → picked_up → refunded)留 Phase 2
2167
1602
  try {
@@ -2172,21 +1607,8 @@ try {
2172
1607
  db.exec('ALTER TABLE return_requests ADD COLUMN pickup_address TEXT');
2173
1608
  }
2174
1609
  catch { }
2175
- // W2 售后协商时间线 — 多轮消息(buyer ↔ seller
2176
- db.exec(`
2177
- CREATE TABLE IF NOT EXISTS return_messages (
2178
- id TEXT PRIMARY KEY, -- rmsg_xxx
2179
- return_id TEXT NOT NULL,
2180
- sender_id TEXT NOT NULL,
2181
- sender_role TEXT NOT NULL, -- 'buyer' | 'seller' | 'system'
2182
- body TEXT NOT NULL,
2183
- created_at TEXT DEFAULT (datetime('now'))
2184
- )
2185
- `);
2186
- try {
2187
- db.exec('CREATE INDEX IF NOT EXISTS idx_rmsg_return ON return_messages(return_id, created_at)');
2188
- }
2189
- catch { }
1610
+ // W2 售后协商时间线 — 多轮消息(buyer ↔ seller)→ server-schema.ts;flagged/flag_reasons ALTER 刻意留原位(紧跟下方)
1611
+ initReturnMessagesSchema(db);
2190
1612
  // 跨窗反诈一致性
2191
1613
  try {
2192
1614
  db.exec('ALTER TABLE return_messages ADD COLUMN flagged INTEGER DEFAULT 0');
@@ -2198,24 +1620,8 @@ try {
2198
1620
  catch { }
2199
1621
  // Wave B-1 Phase 1: 商品 variants(同款多 SKU — 颜色/尺寸/规格组合)
2200
1622
  // schema + CRUD 端点;Phase 2 再集成订单/购物车
2201
- db.exec(`
2202
- CREATE TABLE IF NOT EXISTS product_variants (
2203
- id TEXT PRIMARY KEY,
2204
- product_id TEXT NOT NULL,
2205
- sku TEXT, -- 卖家内部 SKU 编号(可选)
2206
- options_json TEXT NOT NULL, -- {"颜色":"红","尺寸":"L"} 必填
2207
- price_override REAL, -- null = 用 product.price
2208
- stock INTEGER DEFAULT 0,
2209
- images_json TEXT, -- variant 专属图(可选,null = 用 product.images)
2210
- is_active INTEGER DEFAULT 1,
2211
- created_at TEXT DEFAULT (datetime('now')),
2212
- updated_at TEXT DEFAULT (datetime('now'))
2213
- )
2214
- `);
2215
- try {
2216
- db.exec('CREATE INDEX IF NOT EXISTS idx_pv_product ON product_variants(product_id, is_active)');
2217
- }
2218
- catch { }
1623
+ // Wave B-1: 商品 variants → server-schema.ts;has_variants/options_key ALTER + 回填 + uniq 索引刻意留原位(紧跟下方)
1624
+ initProductVariantsSchema(db);
2219
1625
  // 给 products 加 has_variants 标记(避免每次查 join 检查)
2220
1626
  try {
2221
1627
  db.exec('ALTER TABLE products ADD COLUMN has_variants INTEGER DEFAULT 0');
@@ -2274,146 +1680,15 @@ try {
2274
1680
  db.exec('CREATE INDEX IF NOT EXISTS idx_products_p2p ON products(p2p_mode, status)');
2275
1681
  }
2276
1682
  catch { }
2277
- db.exec(`
2278
- CREATE TABLE IF NOT EXISTS p2p_shops (
2279
- id TEXT PRIMARY KEY,
2280
- owner_id TEXT NOT NULL,
2281
- name TEXT NOT NULL,
2282
- description TEXT,
2283
- thumbnail_uri TEXT,
2284
- peer_endpoint TEXT,
2285
- peer_pubkey TEXT,
2286
- status TEXT NOT NULL DEFAULT 'active',
2287
- created_at TEXT DEFAULT (datetime('now')),
2288
- updated_at TEXT DEFAULT (datetime('now'))
2289
- )
2290
- `);
2291
- try {
2292
- db.exec('CREATE INDEX IF NOT EXISTS idx_p2p_shops_owner ON p2p_shops(owner_id, status)');
2293
- }
2294
- catch { }
2295
- db.exec(`
2296
- CREATE TABLE IF NOT EXISTS shareable_likes (
2297
- id TEXT PRIMARY KEY,
2298
- shareable_id TEXT NOT NULL,
2299
- user_id TEXT NOT NULL,
2300
- created_at TEXT DEFAULT (datetime('now')),
2301
- UNIQUE(shareable_id, user_id)
2302
- )
2303
- `);
2304
- try {
2305
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_likes_shr ON shareable_likes(shareable_id)');
2306
- }
2307
- catch { }
2308
- try {
2309
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_likes_user ON shareable_likes(user_id, created_at DESC)');
2310
- }
2311
- catch { }
2312
- // 2026-05-22 audit P2:收藏功能(小红书风格"收藏" tab)
2313
- db.exec(`
2314
- CREATE TABLE IF NOT EXISTS shareable_bookmarks (
2315
- id TEXT PRIMARY KEY,
2316
- shareable_id TEXT NOT NULL,
2317
- user_id TEXT NOT NULL,
2318
- created_at TEXT DEFAULT (datetime('now')),
2319
- UNIQUE(shareable_id, user_id)
2320
- )
2321
- `);
2322
- try {
2323
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_bm_user ON shareable_bookmarks(user_id, created_at DESC)');
2324
- }
2325
- catch { }
2326
- try {
2327
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_bm_shr ON shareable_bookmarks(shareable_id)');
2328
- }
2329
- catch { }
2330
- // 2026-05-22 audit P1 backlog:# 话题/标签系统(小红书风格内容分发)
2331
- db.exec(`
2332
- CREATE TABLE IF NOT EXISTS shareable_tags (
2333
- id INTEGER PRIMARY KEY AUTOINCREMENT,
2334
- shareable_id TEXT NOT NULL,
2335
- tag TEXT NOT NULL, -- 已 lowercase + trim,最长 30 字符
2336
- created_at TEXT DEFAULT (datetime('now')),
2337
- UNIQUE(shareable_id, tag)
2338
- )
2339
- `);
2340
- try {
2341
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_tags_tag ON shareable_tags(tag, created_at DESC)');
2342
- }
2343
- catch { }
2344
- try {
2345
- db.exec('CREATE INDEX IF NOT EXISTS idx_shr_tags_shr ON shareable_tags(shareable_id)');
2346
- }
2347
- catch { }
2348
- // manifest_registry = 原生 P2P 内容索引(仅 hash + 签名 + 元数据;字节在用户设备)
2349
- db.exec(`
2350
- CREATE TABLE IF NOT EXISTS manifest_registry (
2351
- hash TEXT PRIMARY KEY,
2352
- owner_id TEXT NOT NULL,
2353
- content_type TEXT NOT NULL,
2354
- byte_size INTEGER NOT NULL,
2355
- title TEXT,
2356
- description TEXT,
2357
- thumbnail_data_uri TEXT,
2358
- signature TEXT NOT NULL,
2359
- signed_at TEXT NOT NULL,
2360
- related_product_id TEXT,
2361
- related_anchor TEXT,
2362
- status TEXT DEFAULT 'active',
2363
- takedown_reason TEXT,
2364
- takedown_at TEXT,
2365
- takedown_by TEXT,
2366
- created_at TEXT DEFAULT (datetime('now'))
2367
- )
2368
- `);
2369
- try {
2370
- db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_owner ON manifest_registry(owner_id, status)");
2371
- }
2372
- catch { }
2373
- try {
2374
- db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_product ON manifest_registry(related_product_id, status)");
2375
- }
2376
- catch { }
2377
- try {
2378
- db.exec("CREATE INDEX IF NOT EXISTS idx_mfst_anchor ON manifest_registry(related_anchor, status)");
2379
- }
2380
- catch { }
2381
- // peer_directory = 在线 peer 注册(哪些 user 持有哪些 hash 的 cache,heartbeat 5min 失效)
2382
- db.exec(`
2383
- CREATE TABLE IF NOT EXISTS peer_directory (
2384
- peer_id TEXT NOT NULL,
2385
- manifest_hash TEXT NOT NULL,
2386
- is_owner INTEGER DEFAULT 0,
2387
- pin_intent INTEGER DEFAULT 0,
2388
- last_heartbeat TEXT NOT NULL,
2389
- bytes_served_total INTEGER DEFAULT 0,
2390
- PRIMARY KEY (peer_id, manifest_hash)
2391
- )
2392
- `);
2393
- try {
2394
- db.exec("CREATE INDEX IF NOT EXISTS idx_peer_hash ON peer_directory(manifest_hash, last_heartbeat DESC)");
2395
- }
2396
- catch { }
2397
- try {
2398
- db.exec("CREATE INDEX IF NOT EXISTS idx_peer_heartbeat ON peer_directory(last_heartbeat)");
2399
- }
2400
- catch { }
2401
- // signaling_queue = WebRTC SDP/ICE 中继(TTL 2min,cron 清理)
2402
- db.exec(`
2403
- CREATE TABLE IF NOT EXISTS signaling_queue (
2404
- id TEXT PRIMARY KEY,
2405
- to_peer_id TEXT NOT NULL,
2406
- from_peer_id TEXT NOT NULL,
2407
- signal_type TEXT NOT NULL,
2408
- signal_data TEXT NOT NULL,
2409
- created_at TEXT NOT NULL,
2410
- delivered_at TEXT
2411
- )
2412
- `);
2413
- try {
2414
- db.exec("CREATE INDEX IF NOT EXISTS idx_sig_to ON signaling_queue(to_peer_id, delivered_at)");
2415
- }
2416
- catch { }
1683
+ // P2P 店铺 / 笔记点赞·收藏·标签 / manifest·peer·signaling(原生 P2P 内容层)→ server-schema.ts
1684
+ // 纯幂等建表/建索引 DDL,原位调用保持 boot 顺序不变
1685
+ initP2pShopsSchema(db);
1686
+ initShareableLikesSchema(db);
1687
+ initShareableBookmarksSchema(db);
1688
+ initShareableTagsSchema(db);
1689
+ initManifestRegistrySchema(db);
1690
+ initPeerDirectorySchema(db);
1691
+ initSignalingQueueSchema(db);
2417
1692
  // pin_receipts = 服务证明(pinner 给 recipient 传输 N bytes 时双签的回执)
2418
1693
  // recipient 之后下单同商品 → settlePinRewards 从 basin 拨 0.5% 订单额分给最近 5 个 pinners
2419
1694
  db.exec(`
@@ -2617,7 +1892,7 @@ catch { }
2617
1892
  // 2026-06-04:佣金兜底科目从 charity_fund 拆出,慈善科目自此【纯净】只服务慈善许愿板块。
2618
1893
  // 三级佣金中【无资格人 / 无资格 / 区域档位截断 / max=0 整池 / opt-out 放弃 / escrow 到期】
2619
1894
  // 的部分,统一入此【独立科目】。定位 = 协议储备,**只进不出**,用途由治理(DAO/创始人)决定。
2620
- // 与 global_fund(PV 资金,由 1% base 注资发对碰) 互不流通 —— 三套科目彻底独立。
1895
+ // 与 global_fund( 1% base 注资的预留池;匹配奖励引擎已切除,当前无消费方) 互不流通 —— 三套科目彻底独立。
2621
1896
  // 命名注意:本表是【持久储备科目】,区别于每单的 commission_pool/commissionPool(= total×rate 预算变量)。
2622
1897
  // total_chain_gap — L2/L3 空缺(自发现/上家断链)
2623
1898
  // total_orphan_sponsor — sponsor 被封/无资格 + opt-out 主动放弃 + escrow 到期(无合格受益人桶)
@@ -2796,7 +2071,7 @@ try {
2796
2071
  db.exec("CREATE INDEX IF NOT EXISTS idx_cft_from_kind_time ON charity_fund_txns(from_user_id, kind, created_at DESC)");
2797
2072
  }
2798
2073
  catch { }
2799
- // 原子能轨道 Phase 2 — 双轨对碰数据层
2074
+ // 放置树 / PV 参与记录数据层(中性参与记录;匹配奖励引擎已切除)
2800
2075
  for (const stmt of [
2801
2076
  'ALTER TABLE users ADD COLUMN placement_id TEXT',
2802
2077
  "ALTER TABLE users ADD COLUMN placement_side TEXT",
@@ -2895,14 +2170,20 @@ try {
2895
2170
  db.exec('ALTER TABLE global_fund ADD COLUMN pv_escrow_reserve REAL DEFAULT 0');
2896
2171
  }
2897
2172
  catch { /* 已存在 */ }
2173
+ // 迁移:management_bonus_pool → protocol_reserve_pool(中性标识,去 comp-plan;保留既有余额)。
2174
+ // RENAME 必须在 CREATE IF NOT EXISTS 之前:旧库重命名保余额;新库无旧表→ALTER 抛错被吞,由下方 CREATE 建新表。
2175
+ try {
2176
+ db.exec('ALTER TABLE management_bonus_pool RENAME TO protocol_reserve_pool');
2177
+ }
2178
+ catch { /* 已迁移 / 全新库 */ }
2898
2179
  db.exec(`
2899
- CREATE TABLE IF NOT EXISTS management_bonus_pool (
2180
+ CREATE TABLE IF NOT EXISTS protocol_reserve_pool (
2900
2181
  id INTEGER PRIMARY KEY CHECK(id=1),
2901
2182
  balance REAL DEFAULT 0
2902
2183
  )
2903
2184
  `);
2904
2185
  try {
2905
- db.prepare('INSERT OR IGNORE INTO management_bonus_pool (id) VALUES (1)').run();
2186
+ db.prepare('INSERT OR IGNORE INTO protocol_reserve_pool (id) VALUES (1)').run();
2906
2187
  }
2907
2188
  catch { }
2908
2189
  db.exec(`
@@ -2911,95 +2192,24 @@ db.exec(`
2911
2192
  pv_threshold REAL NOT NULL,
2912
2193
  score_per_hit REAL NOT NULL,
2913
2194
  active INTEGER DEFAULT 1
2914
- )
2915
- `);
2916
- [
2917
- [1, 30_000, 100],
2918
- [2, 100_000, 180],
2919
- [3, 300_000, 250],
2920
- [4, 1_000_000, 320],
2921
- [5, 3_000_000, 380],
2922
- ].forEach(([tier, threshold, score]) => {
2923
- try {
2924
- db.prepare("INSERT OR IGNORE INTO binary_tier_config (tier, pv_threshold, score_per_hit) VALUES (?,?,?)").run(tier, threshold, score);
2925
- }
2926
- catch { }
2927
- });
2928
- // ─── 7档级差对碰升级(资金/PV 解耦控盘版)─────────────────────
2929
- // 资金流:1% 永远入基金池;区域 max_levels<3 时 L3 分润那份也回流入池
2930
- // PV 流:order.total × product.category.pv_multiplier(防交叉补贴)
2931
- // 对碰:7 档阶梯门槛 300→33000,折扣系数 1.00→0.70;成功匹配单次双侧全清
2932
- // 安全阀:30%/50%/70% 拨出率 × pool;100 元/点 cap;剩余沉淀
2933
- for (const stmt of [
2934
- 'ALTER TABLE binary_tier_config ADD COLUMN base_score REAL',
2935
- 'ALTER TABLE binary_tier_config ADD COLUMN discount_coef REAL DEFAULT 1.0',
2936
- ]) {
2937
- try {
2938
- db.exec(stmt);
2939
- }
2940
- catch { }
2941
- }
2942
- // V3 升级(方案 D · 强激励):score_per_hit 直接 = 元(不再 base × coef 中间步骤)
2943
- // PV 阈值跳跃 2.3–2.5x 平均 → 完美对碰拨比 1.20,实际拨比 ~1.5(考虑 min/max 不平衡)
2944
- // 实际沉淀率 ~26%(vs 旧方案 28%),但 T7 门槛大幅降低 → 推广友好度 5×
2945
- //
2946
- // 升级触发:用独立 system_state.tokenomics_version 标志,幂等 + 防 admin 改 tier 后误触发
2947
- // (之前用 T7 score_per_hit 检测,admin 改值会导致重启时重复 ×100 score —— 严重 bug)
2948
- // 提前 CREATE 一次 system_state(line 1723 之前),不影响后续幂等创建
2949
- try {
2950
- db.exec("CREATE TABLE IF NOT EXISTS system_state (key TEXT PRIMARY KEY, value TEXT)");
2951
- }
2952
- catch { }
2953
- const _currentTokenomicsVersion = (() => {
2954
- try {
2955
- const row = db.prepare("SELECT value FROM system_state WHERE key = 'tokenomics_version'").get();
2956
- return row?.value ?? null;
2957
- }
2958
- catch {
2959
- return null;
2960
- }
2961
- })();
2962
- const _needsTierV3Seed = _currentTokenomicsVersion !== 'v3';
2963
- if (_needsTierV3Seed) {
2964
- try {
2965
- db.prepare("DELETE FROM binary_tier_config").run();
2966
- }
2967
- catch { }
2968
- ;
2969
- [
2970
- // [tier, pv_threshold, score (= 元)]
2971
- [1, 300, 100],
2972
- [2, 700, 200],
2973
- [3, 1600, 400],
2974
- [4, 3800, 800],
2975
- [5, 9000, 1600],
2976
- [6, 22000, 3200],
2977
- [7, 55000, 6400],
2978
- ].forEach(([tier, threshold, score]) => {
2979
- try {
2980
- db.prepare(`INSERT INTO binary_tier_config (tier, pv_threshold, base_score, discount_coef, score_per_hit, active)
2981
- VALUES (?,?,?,?,?,1)`).run(tier, threshold, score, 1.0, score);
2982
- }
2983
- catch { }
2984
- });
2985
- // V3 迁移:旧 score (1, 1.9, 3.6, ...) 是基于 cap=100 设计的;新规则 cap=1.0
2986
- // 所有未结算 score × 100,保证用户应得金额不变(旧 score×100 + 新 cap1.0 = 旧 score×100)
2987
- // 仅在 version 从 null/旧值 → 'v3' 时执行一次,admin 后续改 tier 不重触发
2988
- if (_currentTokenomicsVersion !== 'v3') {
2989
- try {
2990
- db.prepare("UPDATE binary_score_records SET score = score * 100 WHERE settled_at IS NULL").run();
2991
- }
2992
- catch (e) {
2993
- console.error('[V3 score migration]', e);
2994
- }
2995
- }
2195
+ )
2196
+ `);
2197
+ // binary_tier_config 保留为【预留空表 / dormant structure】—— 不 seed 任何档位 / 阈值 / 分数参数。
2198
+ // 匹配奖励引擎已切除(#401);若未来经法律 / 治理放行重启奖励功能,再按届时合规设计填充。
2199
+ // (base_score / discount_coef 列仅为历史 schema 兼容保留,不写入)
2200
+ for (const stmt of [
2201
+ 'ALTER TABLE binary_tier_config ADD COLUMN base_score REAL',
2202
+ 'ALTER TABLE binary_tier_config ADD COLUMN discount_coef REAL DEFAULT 1.0',
2203
+ ]) {
2996
2204
  try {
2997
- db.prepare("INSERT OR REPLACE INTO system_state (key, value) VALUES ('tokenomics_version', 'v3')").run();
2998
- }
2999
- catch (e) {
3000
- console.error('[V3 version flag]', e);
2205
+ db.exec(stmt);
3001
2206
  }
2207
+ catch { }
2208
+ }
2209
+ try {
2210
+ db.exec("CREATE TABLE IF NOT EXISTS system_state (key TEXT PRIMARY KEY, value TEXT)");
3002
2211
  }
2212
+ catch { }
3003
2213
  // 商品类目(PV 乘数 = 资金/PV 解耦核心)
3004
2214
  db.exec(`
3005
2215
  CREATE TABLE IF NOT EXISTS product_categories (
@@ -3226,56 +2436,10 @@ try {
3226
2436
  db.exec('CREATE INDEX IF NOT EXISTS idx_aucbids_buyer ON auction_bids(buyer_id, status, submitted_at DESC)');
3227
2437
  }
3228
2438
  catch { }
3229
- // CHAT — 上下文绑定聊天(order / rfq / listing_qa
3230
- db.exec(`
3231
- CREATE TABLE IF NOT EXISTS conversations (
3232
- id TEXT PRIMARY KEY,
3233
- kind TEXT NOT NULL,
3234
- context_id TEXT NOT NULL,
3235
- user_a TEXT NOT NULL,
3236
- user_b TEXT NOT NULL,
3237
- last_message_at TEXT,
3238
- last_preview TEXT,
3239
- unread_a INTEGER NOT NULL DEFAULT 0,
3240
- unread_b INTEGER NOT NULL DEFAULT 0,
3241
- status TEXT NOT NULL DEFAULT 'active',
3242
- created_at TEXT DEFAULT (datetime('now')),
3243
- UNIQUE(kind, context_id, user_a, user_b)
3244
- )
3245
- `);
3246
- try {
3247
- db.exec('CREATE INDEX IF NOT EXISTS idx_conv_a ON conversations(user_a, last_message_at DESC)');
3248
- }
3249
- catch { }
3250
- try {
3251
- db.exec('CREATE INDEX IF NOT EXISTS idx_conv_b ON conversations(user_b, last_message_at DESC)');
3252
- }
3253
- catch { }
3254
- try {
3255
- db.exec('CREATE INDEX IF NOT EXISTS idx_conv_ctx ON conversations(kind, context_id)');
3256
- }
3257
- catch { }
3258
- db.exec(`
3259
- CREATE TABLE IF NOT EXISTS messages (
3260
- id TEXT PRIMARY KEY,
3261
- conversation_id TEXT NOT NULL,
3262
- sender_id TEXT NOT NULL,
3263
- body TEXT NOT NULL DEFAULT '',
3264
- attachments TEXT,
3265
- flagged INTEGER NOT NULL DEFAULT 0,
3266
- flag_reasons TEXT,
3267
- read_at TEXT,
3268
- created_at TEXT DEFAULT (datetime('now'))
3269
- )
3270
- `);
3271
- try {
3272
- db.exec('CREATE INDEX IF NOT EXISTS idx_msg_conv ON messages(conversation_id, created_at)');
3273
- }
3274
- catch { }
3275
- try {
3276
- db.exec('CREATE INDEX IF NOT EXISTS idx_msg_sender ON messages(sender_id, created_at DESC)');
3277
- }
3278
- catch { }
2439
+ // CHAT — 上下文绑定聊天(order / rfq / listing_qa)→ server-schema.ts
2440
+ initConversationsSchema(db);
2441
+ // 聊天消息 server-schema.ts;kind/meta ALTER 刻意留原位(紧跟下方)
2442
+ initMessagesSchema(db);
3279
2443
  // W1 私信结构化消息:kind = 'text' | 'offer' | 'tracking';meta = JSON payload
3280
2444
  try {
3281
2445
  db.exec("ALTER TABLE messages ADD COLUMN kind TEXT DEFAULT 'text'");
@@ -3285,24 +2449,8 @@ try {
3285
2449
  db.exec('ALTER TABLE messages ADD COLUMN meta TEXT');
3286
2450
  }
3287
2451
  catch { }
3288
- // 反诈举报表(chat report → 人工审核)
3289
- db.exec(`
3290
- CREATE TABLE IF NOT EXISTS chat_reports (
3291
- id TEXT PRIMARY KEY,
3292
- conversation_id TEXT NOT NULL,
3293
- message_id TEXT,
3294
- reporter_id TEXT NOT NULL,
3295
- reported_id TEXT NOT NULL,
3296
- reason TEXT NOT NULL,
3297
- note TEXT,
3298
- status TEXT NOT NULL DEFAULT 'pending',
3299
- created_at TEXT DEFAULT (datetime('now'))
3300
- )
3301
- `);
3302
- try {
3303
- db.exec('CREATE INDEX IF NOT EXISTS idx_chatrpt_status ON chat_reports(status, created_at)');
3304
- }
3305
- catch { }
2452
+ // 反诈举报表(chat report → 人工审核)→ server-schema.ts
2453
+ initChatReportsSchema(db);
3306
2454
  // 基金池入池流水(depositToFund 审计 + 4 周历史均值数据源)
3307
2455
  db.exec(`
3308
2456
  CREATE TABLE IF NOT EXISTS fund_deposits (
@@ -3347,7 +2495,7 @@ try {
3347
2495
  db.exec('CREATE INDEX IF NOT EXISTS idx_settle_periods_status ON settlement_periods(status, started_at)');
3348
2496
  }
3349
2497
  catch { }
3350
- // 管理津贴门控:默认关闭,等运营稳定后再开启 + 仅特定资格用户可得
2498
+ // 休眠:管理津贴 payout 已随匹配引擎切除(#401);列 + state 作为休眠结构保留(可逆,默认关闭、无消费方)。
3351
2499
  try {
3352
2500
  db.exec("ALTER TABLE users ADD COLUMN mgmt_bonus_eligible INTEGER DEFAULT 0");
3353
2501
  }
@@ -3562,39 +2710,15 @@ try {
3562
2710
  catch (e) {
3563
2711
  console.warn('[D1b] migration', e);
3564
2712
  }
3565
- // 邀请码轮询:5 用户固定列表(按 handle),admin 可切开关;
3566
- // 默认关闭,前端按钮置灰。开启后访客点 "获取邀请码" → 按"自纠偏轮询"派号。
3567
- // 算法:
3568
- // · 每槽维护 issued_count / registered_count
3569
- // · max_reg - min_reg ≥ 3(不均衡)→ 发 registered 最少的(补齐)
3570
- // · 否则 → 发 issued 最少的(顺序均分)
3571
- // · 点击即 issued++;注册成功 → registered++
3572
- try {
3573
- db.prepare("INSERT OR IGNORE INTO system_state (key, value) VALUES ('invite_rotation_enabled', '0')").run();
3574
- }
3575
- catch { }
3576
- try {
3577
- db.exec(`CREATE TABLE IF NOT EXISTS invite_rotation_stats (
3578
- slot INTEGER PRIMARY KEY,
3579
- issued_count INTEGER NOT NULL DEFAULT 0,
3580
- registered_count INTEGER NOT NULL DEFAULT 0
3581
- )`);
3582
- for (let i = 0; i < 5; i++) {
3583
- db.prepare("INSERT OR IGNORE INTO invite_rotation_stats (slot) VALUES (?)").run(i);
3584
- }
3585
- }
3586
- catch (e) {
3587
- console.error('[invite-rotation schema]', e);
3588
- }
3589
2713
  // 让 sys_protocol 可作为公库 sponsor(孤儿注册时分润自动归公库)
3590
2714
  try {
3591
2715
  db.prepare("UPDATE users SET l1_share_override = 1 WHERE id = 'sys_protocol'").run();
3592
2716
  }
3593
2717
  catch { }
3594
2718
  // M7.2.6 + 2026-05-21 PV 合规扩展:按全球各国监管态度分档
3595
- // 详细法律依据 + 风险评估见 docs/MLM-COMPLIANCE.md
2719
+ // 详细法律依据 + 风险评估见 docs/PARTICIPATION-ATTRIBUTION-COMPLIANCE.md
3596
2720
  //
3597
- // max_levels=0(完全禁 MLM,整池入 charity_fund):
2721
+ // max_levels=0(完全禁 MLM,整池入 commission_reserve):
3598
2722
  // GCC 国家 / 伊朗 / 朝鲜 / 缅甸 — 法律完全禁止任何下线计酬
3599
2723
  // max_levels=1(仅 L1,类联盟营销):
3600
2724
  // 越南 / 印尼 / 菲律宾 — 严格 license 制度,多级风险高
@@ -3691,68 +2815,13 @@ for (const stmt of [
3691
2815
  }
3692
2816
  catch { }
3693
2817
  }
3694
- db.exec(`
3695
- CREATE TABLE IF NOT EXISTS quota_increase_applications (
3696
- id TEXT PRIMARY KEY,
3697
- user_id TEXT NOT NULL,
3698
- current_quota INTEGER,
3699
- requested_quota INTEGER,
3700
- reason TEXT,
3701
- status TEXT DEFAULT 'pending',
3702
- applied_at TEXT DEFAULT (datetime('now')),
3703
- reviewed_at TEXT,
3704
- reviewed_by TEXT,
3705
- decision_note TEXT
3706
- )
3707
- `);
3708
- try {
3709
- db.exec('CREATE INDEX IF NOT EXISTS idx_quota_apps_status ON quota_increase_applications(status)');
3710
- }
3711
- catch { }
3712
- // Verifier 申请记录
3713
- db.exec(`
3714
- CREATE TABLE IF NOT EXISTS verifier_applications (
3715
- id TEXT PRIMARY KEY,
3716
- user_id TEXT NOT NULL,
3717
- status TEXT DEFAULT 'pending',
3718
- applied_at TEXT DEFAULT (datetime('now')),
3719
- reviewed_at TEXT,
3720
- reviewed_by TEXT,
3721
- decision_note TEXT,
3722
- snapshot TEXT
3723
- )
3724
- `);
3725
- try {
3726
- db.exec('CREATE INDEX IF NOT EXISTS idx_verifier_apps_status ON verifier_applications(status)');
3727
- }
3728
- catch { }
3729
- // Arbitrator 申请 + 白名单(外部仲裁员路径 — 与 verifier 平行)
3730
- // 内部仲裁员:role='arbitrator'(admin 通过 /admin/admins 创建,自动 is_system=1)
3731
- // 外部仲裁员:role='buyer' + arbitrator_whitelist 行(buyer 申请→admin 批准)
3732
- db.exec(`
3733
- CREATE TABLE IF NOT EXISTS arbitrator_applications (
3734
- id TEXT PRIMARY KEY,
3735
- user_id TEXT NOT NULL,
3736
- status TEXT DEFAULT 'pending',
3737
- applied_at TEXT DEFAULT (datetime('now')),
3738
- reviewed_at TEXT,
3739
- reviewed_by TEXT,
3740
- decision_note TEXT,
3741
- snapshot TEXT
3742
- );
3743
- CREATE TABLE IF NOT EXISTS arbitrator_whitelist (
3744
- user_id TEXT PRIMARY KEY,
3745
- added_at TEXT DEFAULT (datetime('now')),
3746
- note TEXT,
3747
- is_system INTEGER DEFAULT 0,
3748
- granted_by TEXT,
3749
- stake_amount INTEGER DEFAULT 0
3750
- )
3751
- `);
3752
- try {
3753
- db.exec('CREATE INDEX IF NOT EXISTS idx_arb_apps_status ON arbitrator_applications(status)');
3754
- }
3755
- catch { }
2818
+ // 配额提升申请 → server-schema.ts
2819
+ initQuotaIncreaseApplicationsSchema(db);
2820
+ // Verifier 申请记录 → server-schema.ts
2821
+ initVerifierApplicationsSchema(db);
2822
+ // Arbitrator 申请 + 白名单(外部仲裁员路径)→ server-schema.ts
2823
+ // legacy 内部仲裁员 → 白名单的 migration INSERT 刻意留原位(紧跟下方)
2824
+ initArbitratorReviewSchema(db);
3756
2825
  // Migration:legacy 内部仲裁员 (role='arbitrator') → 自动加入白名单(is_system=1)
3757
2826
  try {
3758
2827
  db.prepare(`
@@ -3763,26 +2832,8 @@ try {
3763
2832
  catch (e) {
3764
2833
  console.warn('[arb migration]', e.message);
3765
2834
  }
3766
- // Verifier 申诉记录
3767
- db.exec(`
3768
- CREATE TABLE IF NOT EXISTS verifier_appeals (
3769
- id TEXT PRIMARY KEY,
3770
- user_id TEXT NOT NULL,
3771
- task_id TEXT,
3772
- submission_id TEXT,
3773
- reason TEXT NOT NULL,
3774
- evidence_urls TEXT DEFAULT '[]',
3775
- status TEXT DEFAULT 'pending',
3776
- admin_note TEXT,
3777
- reviewed_by TEXT,
3778
- reviewed_at TEXT,
3779
- created_at TEXT DEFAULT (datetime('now'))
3780
- )
3781
- `);
3782
- try {
3783
- db.exec('CREATE INDEX IF NOT EXISTS idx_verifier_appeals_status ON verifier_appeals(status)');
3784
- }
3785
- catch { }
2835
+ // Verifier 申诉记录 → server-schema.ts
2836
+ initVerifierAppealsSchema(db);
3786
2837
  // 扩展 verifier_whitelist
3787
2838
  for (const stmt of [
3788
2839
  "ALTER TABLE verifier_whitelist ADD COLUMN tier TEXT DEFAULT 'active-2'", // 旧数据兼容:当满级
@@ -3805,34 +2856,14 @@ try {
3805
2856
  db.prepare("UPDATE verifier_whitelist SET is_system = 1, tier = 'active-2', daily_quota = 9999 WHERE user_id = ?").run(INTERNAL_AUDITOR_ID);
3806
2857
  }
3807
2858
  catch { }
3808
- // 用户暂停状态(admin 管理)
3809
- db.exec(`
3810
- CREATE TABLE IF NOT EXISTS user_moderation (
3811
- user_id TEXT PRIMARY KEY,
3812
- suspended INTEGER DEFAULT 0,
3813
- reason TEXT,
3814
- suspended_by TEXT,
3815
- suspended_at TEXT
3816
- )
3817
- `);
3818
- // admin 操作审计日志
3819
- db.exec(`
3820
- CREATE TABLE IF NOT EXISTS admin_audit_log (
3821
- id TEXT PRIMARY KEY,
3822
- admin_id TEXT NOT NULL,
3823
- action TEXT NOT NULL,
3824
- target_type TEXT,
3825
- target_id TEXT,
3826
- detail TEXT,
3827
- created_at TEXT DEFAULT (datetime('now'))
3828
- )
3829
- `);
3830
- try {
3831
- db.exec('CREATE INDEX IF NOT EXISTS idx_admin_audit_log_created ON admin_audit_log(created_at)');
3832
- }
3833
- catch { }
3834
- // Bootstrap admin(env BOOTSTRAP_ADMIN_NAME → 该用户升为 admin,幂等)
3835
- ;
2859
+ // 用户暂停状态(admin 管理)→ server-schema.ts
2860
+ initUserModerationSchema(db);
2861
+ // admin 操作审计日志 server-schema.ts(initAdminCoordinationSchema FK 依赖本表,须先建)
2862
+ initAdminAuditLogSchema(db);
2863
+ // admin/agent coordination contribution — operator-claim + agent-mandate event logs + fact-source link
2864
+ // (schema only). Placed HERE because it FKs users + contribution_facts (both created above) AND
2865
+ // admin_audit_log (created just above). No ingestion runs at boot.
2866
+ initAdminCoordinationSchema(db);
3836
2867
  (() => {
3837
2868
  const bootName = process.env.BOOTSTRAP_ADMIN_NAME;
3838
2869
  if (!bootName?.trim())
@@ -3855,41 +2886,16 @@ catch { }
3855
2886
  .run(JSON.stringify(roles), u.id);
3856
2887
  console.log(`[WebAZ] ✓ ${u.name} 已升级为 admin (bootstrap)`);
3857
2888
  })();
3858
- // 验证码表(邮箱绑定 / 找回密钥 / 改密码 等共用)
3859
- db.exec(`
3860
- CREATE TABLE IF NOT EXISTS verification_codes (
3861
- id TEXT PRIMARY KEY,
3862
- user_id TEXT NOT NULL,
3863
- channel TEXT NOT NULL, -- 'email' / 'phone'
3864
- target TEXT NOT NULL, -- 邮箱地址 / 手机号
3865
- code TEXT NOT NULL, -- 6 位数字
3866
- purpose TEXT NOT NULL, -- 'bind_email' / 'recover_key' / ...
3867
- attempts INTEGER DEFAULT 0,
3868
- used_at TEXT,
3869
- expires_at TEXT NOT NULL,
3870
- created_at TEXT DEFAULT (datetime('now'))
3871
- )
3872
- `);
3873
- try {
3874
- db.exec('CREATE INDEX IF NOT EXISTS idx_verification_codes_lookup ON verification_codes(channel, target, purpose)');
3875
- }
3876
- catch { }
2889
+ // 验证码表(邮箱绑定 / 找回密钥 / 改密码 等共用)→ server-schema.ts
2890
+ initVerificationCodesSchema(db);
3877
2891
  const NEW_PRODUCT_COLS = [
3878
- 'ALTER TABLE products ADD COLUMN specs TEXT',
3879
- 'ALTER TABLE products ADD COLUMN brand TEXT',
3880
- 'ALTER TABLE products ADD COLUMN model TEXT',
2892
+ // specs/brand/model/source_price/ship_regions/handling_hours/estimated_days/
2893
+ // fragile/return_days/return_condition/warranty_days moved to
2894
+ // initRegisterListSearchColumns (single source, shared w/ MCP) — see ~line 494.
3881
2895
  'ALTER TABLE products ADD COLUMN source_url TEXT',
3882
- 'ALTER TABLE products ADD COLUMN source_price REAL',
3883
2896
  'ALTER TABLE products ADD COLUMN source_price_at TEXT',
3884
2897
  'ALTER TABLE products ADD COLUMN weight_kg REAL',
3885
- 'ALTER TABLE products ADD COLUMN ship_regions TEXT DEFAULT "全国"',
3886
2898
  'ALTER TABLE products ADD COLUMN excluded_regions TEXT',
3887
- 'ALTER TABLE products ADD COLUMN handling_hours INTEGER DEFAULT 24',
3888
- 'ALTER TABLE products ADD COLUMN estimated_days TEXT',
3889
- 'ALTER TABLE products ADD COLUMN fragile INTEGER DEFAULT 0',
3890
- 'ALTER TABLE products ADD COLUMN return_days INTEGER DEFAULT 7',
3891
- 'ALTER TABLE products ADD COLUMN return_condition TEXT',
3892
- 'ALTER TABLE products ADD COLUMN warranty_days INTEGER DEFAULT 0',
3893
2899
  'ALTER TABLE products ADD COLUMN commitment_hash TEXT',
3894
2900
  'ALTER TABLE products ADD COLUMN description_hash TEXT',
3895
2901
  'ALTER TABLE products ADD COLUMN price_hash TEXT',
@@ -3993,20 +2999,12 @@ catch { }
3993
2999
  })();
3994
3000
  // ─── 里程碑 3:反操纵层 schema ─────────────────────────────────
3995
3001
  try {
3996
- db.exec(`CREATE TABLE IF NOT EXISTS shareable_click_log (
3997
- id INTEGER PRIMARY KEY AUTOINCREMENT,
3998
- shareable_id TEXT NOT NULL,
3999
- ip_hash TEXT NOT NULL,
4000
- ua_hash TEXT NOT NULL,
4001
- ref_path TEXT,
4002
- created_at TEXT DEFAULT (datetime('now'))
4003
- )`);
4004
- db.exec(`CREATE INDEX IF NOT EXISTS idx_scl_share_ts ON shareable_click_log(shareable_id, created_at)`);
4005
- db.exec(`CREATE INDEX IF NOT EXISTS idx_scl_share_ipua ON shareable_click_log(shareable_id, ip_hash, ua_hash, created_at)`);
3002
+ initShareableClickLogSchema(db);
4006
3003
  }
4007
3004
  catch (e) {
4008
3005
  console.error('[M3 schema scl]', e);
4009
3006
  }
3007
+ // shareables ALTER 刻意留原位(scl init 之后、cal init 之前)
4010
3008
  try {
4011
3009
  db.exec('ALTER TABLE shareables ADD COLUMN unique_click_count INTEGER DEFAULT 0');
4012
3010
  }
@@ -4016,137 +3014,42 @@ try {
4016
3014
  }
4017
3015
  catch { }
4018
3016
  try {
4019
- db.exec(`CREATE TABLE IF NOT EXISTS commission_audit_log (
4020
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4021
- order_id TEXT,
4022
- buyer_id TEXT NOT NULL,
4023
- seller_id TEXT NOT NULL,
4024
- flag TEXT NOT NULL, -- 'sponsor_chain_cross' / 'self_in_chain'
4025
- detail TEXT, -- JSON: { relation: 'buyer_ancestor_of_seller' | ..., path: '...' }
4026
- created_at TEXT DEFAULT (datetime('now'))
4027
- )`);
4028
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cal_buyer ON commission_audit_log(buyer_id, created_at)`);
3017
+ initCommissionAuditLogSchema(db);
4029
3018
  }
4030
3019
  catch (e) {
4031
3020
  console.error('[M3 schema cal]', e);
4032
3021
  }
4033
3022
  try {
4034
- db.exec(`CREATE TABLE IF NOT EXISTS registration_audit_log (
4035
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4036
- user_id TEXT NOT NULL,
4037
- ip_hash TEXT NOT NULL,
4038
- ua_hash TEXT NOT NULL,
4039
- sponsor_id TEXT,
4040
- created_at TEXT DEFAULT (datetime('now'))
4041
- )`);
4042
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ral_ip_ts ON registration_audit_log(ip_hash, created_at)`);
3023
+ initRegistrationAuditLogSchema(db);
4043
3024
  }
4044
3025
  catch (e) {
4045
3026
  console.error('[M3 schema ral]', e);
4046
3027
  }
4047
- // ─── 里程碑 4:Agent Reputation schema ─────────────────────────
3028
+ // ─── 里程碑 4:Agent observability/reputation schema → server-schema.ts ─
4048
3029
  try {
4049
- db.exec(`CREATE TABLE IF NOT EXISTS agent_call_log (
4050
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4051
- api_key TEXT,
4052
- user_id TEXT,
4053
- endpoint TEXT NOT NULL,
4054
- method TEXT,
4055
- status_code INTEGER,
4056
- created_at TEXT DEFAULT (datetime('now'))
4057
- )`);
4058
- db.exec(`CREATE INDEX IF NOT EXISTS idx_acl_apikey_ts ON agent_call_log(api_key, created_at)`);
4059
- db.exec(`CREATE INDEX IF NOT EXISTS idx_acl_user_ts ON agent_call_log(user_id, created_at)`);
3030
+ initAgentCallLogSchema(db);
4060
3031
  }
4061
3032
  catch (e) {
4062
3033
  console.error('[M4 schema acl]', e);
4063
3034
  }
4064
3035
  try {
4065
- db.exec(`CREATE TABLE IF NOT EXISTS agent_reputation (
4066
- api_key TEXT PRIMARY KEY,
4067
- user_id TEXT NOT NULL,
4068
- trust_score REAL DEFAULT 0,
4069
- level TEXT DEFAULT 'new',
4070
- signals TEXT, -- JSON
4071
- last_calculated_at TEXT,
4072
- created_at TEXT DEFAULT (datetime('now'))
4073
- )`);
4074
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ar_user ON agent_reputation(user_id)`);
3036
+ initAgentReputationSchema(db);
4075
3037
  }
4076
3038
  catch (e) {
4077
3039
  console.error('[M4 schema ar]', e);
4078
3040
  }
4079
- // ─── 2026-05-23 Agent 治理(spec: docs/AGENT-GOVERNANCE.md)─────
4080
- try {
4081
- // agent_declarations:agent 自声明(trust > new 必须先登记)
4082
- db.exec(`CREATE TABLE IF NOT EXISTS agent_declarations (
4083
- api_key TEXT PRIMARY KEY,
4084
- user_id TEXT NOT NULL,
4085
- operator_name TEXT NOT NULL, -- 公司/开发者名
4086
- operator_contact TEXT NOT NULL, -- email/handle/DID
4087
- purpose TEXT NOT NULL, -- ≤200 字
4088
- declared_scope TEXT NOT NULL, -- JSON: {roles, actions, regions}
4089
- attestations TEXT, -- JSON: {gdpr, kids_safe, no_pii_export, ...}
4090
- repo_url TEXT,
4091
- homepage TEXT,
4092
- revoked_at TEXT, -- 撤销时间(用户主动 revoke)
4093
- revoked_reason TEXT,
4094
- created_at TEXT DEFAULT (datetime('now')),
4095
- updated_at TEXT DEFAULT (datetime('now'))
4096
- )`);
4097
- db.exec(`CREATE INDEX IF NOT EXISTS idx_agd_operator ON agent_declarations(operator_name)`);
4098
- db.exec(`CREATE INDEX IF NOT EXISTS idx_agd_revoked ON agent_declarations(revoked_at) WHERE revoked_at IS NOT NULL`);
4099
- // agent_attestations:bilateral consent(用户主动同意某 agent 的 scope)
4100
- db.exec(`CREATE TABLE IF NOT EXISTS agent_attestations (
4101
- id TEXT PRIMARY KEY,
4102
- api_key TEXT NOT NULL, -- agent 的 api_key
4103
- user_id TEXT NOT NULL, -- 同意此 agent 行动的用户
4104
- approved_scope TEXT NOT NULL, -- JSON:用户实际批准的子集
4105
- spend_cap_per_order REAL, -- 该用户给此 agent 的单笔下单上限(可空 = 沿用 declared_scope)
4106
- spend_cap_daily REAL, -- 24h 累计上限
4107
- granted_at TEXT DEFAULT (datetime('now')),
4108
- revoked_at TEXT,
4109
- UNIQUE(api_key, user_id)
4110
- )`);
4111
- db.exec(`CREATE INDEX IF NOT EXISTS idx_aat_user ON agent_attestations(user_id, revoked_at)`);
4112
- // agent_strikes:违规累积(3-strike state machine)
4113
- db.exec(`CREATE TABLE IF NOT EXISTS agent_strikes (
4114
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4115
- api_key TEXT NOT NULL,
4116
- user_id TEXT NOT NULL,
4117
- severity TEXT NOT NULL, -- 'warning' | 'suspend_7d' | 'permanent'
4118
- reason_code TEXT NOT NULL, -- 'fake_shipment' | 'mass_spam' | 'overlimit_order' | 'fraud_claim' | ...
4119
- reason_detail TEXT,
4120
- reported_by TEXT, -- user_id(举报人 / system / admin)
4121
- related_ref TEXT, -- 关联 order/dispute/claim_task id
4122
- issued_at TEXT DEFAULT (datetime('now')),
4123
- expires_at TEXT, -- warning=24h; suspend_7d=7d; permanent=null
4124
- appeal_status TEXT DEFAULT 'none', -- 'none' | 'pending' | 'approved' | 'denied'
4125
- appeal_reason TEXT,
4126
- appeal_decided_by TEXT,
4127
- appeal_decided_at TEXT
4128
- )`);
4129
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_apikey ON agent_strikes(api_key, issued_at DESC)`);
4130
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_user ON agent_strikes(user_id, issued_at DESC)`);
4131
- // 注:SQLite 不允许 partial index 用非确定性函数 (datetime('now'));用 expires_at 普通索引代替
4132
- db.exec(`CREATE INDEX IF NOT EXISTS idx_ast_active ON agent_strikes(api_key, expires_at)`);
4133
- // 2026-05-23 P1 fix 5.3:skills 加 disabled_by_strike_at(被 strike 自动停用后可恢复)
3041
+ // ─── 2026-05-23 Agent 治理(spec: docs/AGENT-GOVERNANCE.md)→ server-schema.ts ─
3042
+ // 顺序须保持:declarations → attestations → strikes →(ALTER skills 留原位)→ revocations
3043
+ try {
3044
+ initAgentDeclarationsSchema(db);
3045
+ initAgentAttestationsSchema(db);
3046
+ initAgentStrikesSchema(db);
3047
+ // 2026-05-23 P1 fix 5.3:skills 加 disabled_by_strike_at(被 strike 自动停用后可恢复)—— 留 server.ts 原位
4134
3048
  try {
4135
3049
  db.exec(`ALTER TABLE skills ADD COLUMN disabled_by_strike_at TEXT`);
4136
3050
  }
4137
3051
  catch { }
4138
- // agent_revocations:operator-级撤销(封禁同 operator 名下所有 agent)
4139
- db.exec(`CREATE TABLE IF NOT EXISTS agent_revocations (
4140
- id INTEGER PRIMARY KEY AUTOINCREMENT,
4141
- target_kind TEXT NOT NULL, -- 'api_key' | 'operator_name'
4142
- target_value TEXT NOT NULL,
4143
- revoked_by TEXT NOT NULL, -- user_id(用户自己 OR root admin)
4144
- revoked_by_role TEXT, -- 'self' | 'admin'
4145
- reason TEXT,
4146
- revoked_at TEXT DEFAULT (datetime('now')),
4147
- UNIQUE(target_kind, target_value, revoked_by)
4148
- )`);
4149
- db.exec(`CREATE INDEX IF NOT EXISTS idx_arev_target ON agent_revocations(target_kind, target_value)`);
3052
+ initAgentRevocationsSchema(db);
4150
3053
  }
4151
3054
  catch (e) {
4152
3055
  console.error('[agent_governance schema]', e);
@@ -4155,73 +3058,22 @@ catch (e) {
4155
3058
  // 协议级精准匹配:卖家声明该 SKU 的多种 alias(外部 id / 标题 / 短链 / 淘口令 token / 标题片段)
4156
3059
  // 服务端用 findProductsByAlias 做"完全相等 + 包含"判定。alias 至少 6 字符,反通用词。
4157
3060
  try {
4158
- db.exec(`CREATE TABLE IF NOT EXISTS product_aliases (
4159
- id TEXT PRIMARY KEY,
4160
- product_id TEXT NOT NULL,
4161
- alias_type TEXT NOT NULL, -- 'external_id' | 'external_title' | 'short_url' | 'kouling_token' | 'title_substring'
4162
- alias_value TEXT NOT NULL,
4163
- min_match_chars INTEGER DEFAULT 6,
4164
- created_at TEXT DEFAULT (datetime('now')),
4165
- challenged_at TEXT, -- M7.4 verifier 挑战时间
4166
- status TEXT DEFAULT 'active', -- 'active' | 'revoked' | 'challenged'
4167
- UNIQUE(alias_type, alias_value, product_id)
4168
- )`);
4169
- db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_value ON product_aliases(alias_value)`);
4170
- db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_product ON product_aliases(product_id)`);
4171
- db.exec(`CREATE INDEX IF NOT EXISTS idx_alias_type ON product_aliases(alias_type)`);
3061
+ initProductAliasesSchema(db);
4172
3062
  }
4173
3063
  catch (e) {
4174
3064
  console.error('[M7.2 schema product_aliases]', e);
4175
3065
  }
4176
- // M-5:region 切换 audit log + 24h 限流
3066
+ // M-5:region 切换 audit log + 24h 限流 → server-schema.ts
4177
3067
  try {
4178
- db.exec(`CREATE TABLE IF NOT EXISTS region_change_log (
4179
- id TEXT PRIMARY KEY,
4180
- user_id TEXT NOT NULL,
4181
- from_region TEXT,
4182
- to_region TEXT NOT NULL,
4183
- ip TEXT,
4184
- created_at TEXT DEFAULT (datetime('now'))
4185
- )`);
4186
- db.exec(`CREATE INDEX IF NOT EXISTS idx_region_change_user_ts ON region_change_log(user_id, created_at DESC)`);
3068
+ initRegionChangeLogSchema(db);
4187
3069
  }
4188
3070
  catch (e) {
4189
3071
  console.error('[M-5 schema region_change_log]', e);
4190
3072
  }
4191
- // WebAuthn / Passkey — 大额提现 等敏感操作的二次确认(commit B
3073
+ // WebAuthn / Passkey — 大额提现 等敏感操作的二次确认(commit B)→ server-schema.ts
3074
+ // users.webauthn_required_for_withdraw ALTER 刻意留原位(init 后、同一 try 内)
4192
3075
  try {
4193
- db.exec(`CREATE TABLE IF NOT EXISTS webauthn_credentials (
4194
- id TEXT PRIMARY KEY, -- credential.id (base64url)
4195
- user_id TEXT NOT NULL,
4196
- public_key BLOB NOT NULL, -- COSE public key
4197
- counter INTEGER NOT NULL DEFAULT 0,
4198
- transports TEXT, -- JSON array
4199
- device_label TEXT, -- user-friendly label
4200
- created_at TEXT DEFAULT (datetime('now')),
4201
- last_used_at TEXT
4202
- )`);
4203
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_user ON webauthn_credentials(user_id)`);
4204
- db.exec(`CREATE TABLE IF NOT EXISTS webauthn_challenges (
4205
- id TEXT PRIMARY KEY,
4206
- user_id TEXT NOT NULL,
4207
- challenge TEXT NOT NULL,
4208
- purpose TEXT NOT NULL, -- 'register' | 'withdraw' | 'change-password' | 'reveal-key' | 'region'
4209
- purpose_data TEXT, -- JSON:例如 {amount: 1000, to_address: '0x...'}
4210
- created_at TEXT DEFAULT (datetime('now')),
4211
- expires_at TEXT NOT NULL,
4212
- consumed_at TEXT
4213
- )`);
4214
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_chall_user ON webauthn_challenges(user_id, expires_at)`);
4215
- // gate token:auth/finish 成功后颁发,绑定 user + purpose + 业务参数(防重放)
4216
- db.exec(`CREATE TABLE IF NOT EXISTS webauthn_gate_tokens (
4217
- id TEXT PRIMARY KEY, -- token
4218
- user_id TEXT NOT NULL,
4219
- purpose TEXT NOT NULL,
4220
- purpose_data TEXT, -- JSON
4221
- expires_at TEXT NOT NULL, -- now + 60s
4222
- consumed_at TEXT
4223
- )`);
4224
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wac_gate_user ON webauthn_gate_tokens(user_id, expires_at)`);
3076
+ initWebauthnSchema(db);
4225
3077
  // 用户 opt-in 设置
4226
3078
  try {
4227
3079
  db.exec("ALTER TABLE users ADD COLUMN webauthn_required_for_withdraw INTEGER DEFAULT 0");
@@ -4243,41 +3095,8 @@ const WEBAUTHN_CHALLENGE_TTL_MS = 5 * 60 * 1000; // 5 分钟
4243
3095
  const WEBAUTHN_GATE_TTL_MS = 90 * 1000; // 90 秒
4244
3096
  // M7.3:claim 验证任务系统 — 买家对推荐理由发起验证,3 verifier 共识仲裁
4245
3097
  try {
4246
- db.exec(`CREATE TABLE IF NOT EXISTS claim_verification_tasks (
4247
- id TEXT PRIMARY KEY,
4248
- order_id TEXT NOT NULL,
4249
- buyer_id TEXT NOT NULL,
4250
- seller_id TEXT NOT NULL,
4251
- product_id TEXT NOT NULL,
4252
- claim_target TEXT NOT NULL, -- 'price' | 'commission' | 'protection' | 'return' | 'warranty' | 'handling' | 'other'
4253
- claim_text TEXT NOT NULL, -- 买家陈述(≤ 500 字)
4254
- evidence_uri TEXT, -- 买家证据(URL / hash)
4255
- stake_buyer REAL NOT NULL, -- 买家锁定的质押金
4256
- seller_evidence_uri TEXT, -- 卖家提交的证据
4257
- seller_evidence_at TEXT,
4258
- deadline_at TEXT NOT NULL, -- 默认 48h;卖家提交证据后 +24h
4259
- status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'sealed' | 'resolved_pass' | 'resolved_fail' | 'resolved_no_fault' | 'timeout_pass' | 'timeout_fail'
4260
- resolved_at TEXT,
4261
- created_at TEXT DEFAULT (datetime('now')),
4262
- UNIQUE(order_id)
4263
- )`);
4264
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_status ON claim_verification_tasks(status)`);
4265
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_buyer ON claim_verification_tasks(buyer_id)`);
4266
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_seller ON claim_verification_tasks(seller_id)`);
4267
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvt_deadline ON claim_verification_tasks(deadline_at) WHERE status = 'open'`);
4268
- db.exec(`CREATE TABLE IF NOT EXISTS claim_verification_votes (
4269
- id TEXT PRIMARY KEY,
4270
- task_id TEXT NOT NULL,
4271
- verifier_id TEXT NOT NULL,
4272
- vote TEXT NOT NULL, -- 'pass' | 'fail' | 'no_fault'
4273
- evidence_uri TEXT,
4274
- note TEXT,
4275
- voted_at TEXT DEFAULT (datetime('now')),
4276
- UNIQUE(task_id, verifier_id)
4277
- )`);
4278
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvv_task ON claim_verification_votes(task_id)`);
4279
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvv_verifier ON claim_verification_votes(verifier_id)`);
4280
- // M7.3b 结算扩展
3098
+ initClaimVerificationBaseSchema(db);
3099
+ // M7.3b 结算扩展 — 刻意留 server.ts 原位(base init 之后,按原顺序)
4281
3100
  try {
4282
3101
  db.exec(`ALTER TABLE claim_verification_tasks ADD COLUMN majority_vote TEXT`);
4283
3102
  }
@@ -4286,187 +3105,18 @@ try {
4286
3105
  db.exec(`ALTER TABLE claim_verification_votes ADD COLUMN was_majority INTEGER`);
4287
3106
  }
4288
3107
  catch { }
4289
- // verifier 禁言 / 永封记录(outlier 累计触发)
4290
- db.exec(`CREATE TABLE IF NOT EXISTS claim_verifier_suspensions (
4291
- id TEXT PRIMARY KEY,
4292
- user_id TEXT NOT NULL,
4293
- type TEXT NOT NULL, -- 'suspended' | 'revoked'
4294
- until_at TEXT, -- NULL = permanent (revoked)
4295
- reason TEXT,
4296
- outlier_count INTEGER,
4297
- created_at TEXT DEFAULT (datetime('now'))
4298
- )`);
4299
- db.exec(`CREATE INDEX IF NOT EXISTS idx_cvs_user ON claim_verifier_suspensions(user_id, created_at DESC)`);
4300
- // Sprint 1: 商品声明验证(与 order claim 平行,但作用于 product 层)
4301
- // 任何登录用户(除 seller 本人)可对商品的某项声明发起验证
4302
- // 3 verifier 共识投票判定 upheld(声明不实,卖家失分)/ dismissed(声明属实,发起人失质押)
4303
- db.exec(`CREATE TABLE IF NOT EXISTS product_claim_tasks (
4304
- id TEXT PRIMARY KEY,
4305
- product_id TEXT NOT NULL,
4306
- claimant_id TEXT NOT NULL,
4307
- seller_id TEXT NOT NULL,
4308
- claim_target TEXT NOT NULL, -- 'title' | 'description' | 'condition' | 'return_days' | 'handling_hours' | 'warranty_days' | 'shipping_regions' | 'origin' | 'other'
4309
- claim_text TEXT NOT NULL, -- 发起人陈述 6-500 字
4310
- evidence_uri TEXT, -- 发起人证据 URL
4311
- stake_claimant REAL NOT NULL, -- 发起人锁定质押
4312
- seller_evidence_uri TEXT, -- 卖家反驳证据
4313
- seller_evidence_at TEXT,
4314
- deadline_at TEXT NOT NULL, -- 默认 72h;卖家提交证据后 +24h
4315
- status TEXT NOT NULL DEFAULT 'open', -- 'open' | 'sealed' | 'resolved_upheld' | 'resolved_dismissed' | 'expired'
4316
- ruling TEXT, -- 'upheld' | 'dismissed' | 'insufficient'
4317
- majority_vote TEXT,
4318
- resolved_at TEXT,
4319
- created_at TEXT DEFAULT (datetime('now'))
4320
- )`);
4321
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_status ON product_claim_tasks(status)`);
4322
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_product ON product_claim_tasks(product_id)`);
4323
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_claimant ON product_claim_tasks(claimant_id)`);
4324
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pct_seller ON product_claim_tasks(seller_id)`);
4325
- db.exec(`CREATE TABLE IF NOT EXISTS product_claim_votes (
4326
- id TEXT PRIMARY KEY,
4327
- claim_id TEXT NOT NULL,
4328
- verifier_id TEXT NOT NULL,
4329
- vote TEXT NOT NULL, -- 'upheld' | 'dismissed' | 'insufficient'
4330
- evidence_uri TEXT,
4331
- note TEXT,
4332
- was_majority INTEGER,
4333
- voted_at TEXT DEFAULT (datetime('now')),
4334
- UNIQUE(claim_id, verifier_id)
4335
- )`);
4336
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pcv_claim ON product_claim_votes(claim_id)`);
4337
- db.exec(`CREATE INDEX IF NOT EXISTS idx_pcv_verifier ON product_claim_votes(verifier_id)`);
4338
- // Sprint 2-A: 测评真实性验证(针对 shareables / manifests)
4339
- // 任何登录用户可对某条评测发起验证(不是真实购买/付费推广/利益相关等)
4340
- db.exec(`CREATE TABLE IF NOT EXISTS review_claim_tasks (
4341
- id TEXT PRIMARY KEY,
4342
- review_type TEXT NOT NULL, -- 'shareable' | 'manifest'
4343
- review_id TEXT NOT NULL, -- shareable.id 或 manifest.hash
4344
- product_id TEXT, -- 关联商品(用于显示)
4345
- reviewer_id TEXT NOT NULL, -- 被诉评测作者
4346
- claimant_id TEXT NOT NULL,
4347
- claim_target TEXT NOT NULL, -- 'not_real_purchase' | 'paid_promo' | 'incentivized' | 'misleading' | 'fake' | 'other'
4348
- claim_text TEXT NOT NULL,
4349
- evidence_uri TEXT,
4350
- stake_claimant REAL NOT NULL,
4351
- deadline_at TEXT NOT NULL,
4352
- status TEXT NOT NULL DEFAULT 'open',
4353
- ruling TEXT,
4354
- majority_vote TEXT,
4355
- resolved_at TEXT,
4356
- created_at TEXT DEFAULT (datetime('now'))
4357
- )`);
4358
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_status ON review_claim_tasks(status)`);
4359
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_review ON review_claim_tasks(review_type, review_id)`);
4360
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rct_reviewer ON review_claim_tasks(reviewer_id)`);
4361
- db.exec(`CREATE TABLE IF NOT EXISTS review_claim_votes (
4362
- id TEXT PRIMARY KEY,
4363
- claim_id TEXT NOT NULL,
4364
- verifier_id TEXT NOT NULL,
4365
- vote TEXT NOT NULL, -- 'upheld' | 'dismissed' | 'insufficient'
4366
- evidence_uri TEXT,
4367
- note TEXT,
4368
- was_majority INTEGER,
4369
- voted_at TEXT DEFAULT (datetime('now')),
4370
- UNIQUE(claim_id, verifier_id)
4371
- )`);
4372
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rcv_claim ON review_claim_votes(claim_id)`);
4373
- db.exec(`CREATE INDEX IF NOT EXISTS idx_rcv_verifier ON review_claim_votes(verifier_id)`);
4374
- // Sprint 2-B: 二手成色验证(针对 secondhand_items)
4375
- db.exec(`CREATE TABLE IF NOT EXISTS secondhand_claim_tasks (
4376
- id TEXT PRIMARY KEY,
4377
- sh_item_id TEXT NOT NULL,
4378
- seller_id TEXT NOT NULL, -- 二手卖家
4379
- claimant_id TEXT NOT NULL,
4380
- claim_target TEXT NOT NULL, -- 'condition' | 'images' | 'description' | 'title' | 'price' | 'other'
4381
- claim_text TEXT NOT NULL,
4382
- evidence_uri TEXT,
4383
- stake_claimant REAL NOT NULL,
4384
- deadline_at TEXT NOT NULL,
4385
- status TEXT NOT NULL DEFAULT 'open',
4386
- ruling TEXT,
4387
- majority_vote TEXT,
4388
- resolved_at TEXT,
4389
- created_at TEXT DEFAULT (datetime('now'))
4390
- )`);
4391
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_status ON secondhand_claim_tasks(status)`);
4392
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_item ON secondhand_claim_tasks(sh_item_id)`);
4393
- db.exec(`CREATE INDEX IF NOT EXISTS idx_sct_seller ON secondhand_claim_tasks(seller_id)`);
4394
- db.exec(`CREATE TABLE IF NOT EXISTS secondhand_claim_votes (
4395
- id TEXT PRIMARY KEY,
4396
- claim_id TEXT NOT NULL,
4397
- verifier_id TEXT NOT NULL,
4398
- vote TEXT NOT NULL,
4399
- evidence_uri TEXT,
4400
- note TEXT,
4401
- was_majority INTEGER,
4402
- voted_at TEXT DEFAULT (datetime('now')),
4403
- UNIQUE(claim_id, verifier_id)
4404
- )`);
4405
- db.exec(`CREATE INDEX IF NOT EXISTS idx_scv_claim ON secondhand_claim_votes(claim_id)`);
4406
- // Sprint 3-A: 拍卖声明(auctions)
4407
- // 任何非 seller 用户可对 active auction 的合理性发起验证
4408
- db.exec(`CREATE TABLE IF NOT EXISTS auction_claim_tasks (
4409
- id TEXT PRIMARY KEY,
4410
- auction_id TEXT NOT NULL,
4411
- seller_id TEXT NOT NULL,
4412
- claimant_id TEXT NOT NULL,
4413
- claim_target TEXT NOT NULL, -- 'unreasonable_reserve' | 'shill_bidding' | 'collusion' | 'fake_listing' | 'other'
4414
- claim_text TEXT NOT NULL,
4415
- evidence_uri TEXT,
4416
- stake_claimant REAL NOT NULL,
4417
- deadline_at TEXT NOT NULL,
4418
- status TEXT NOT NULL DEFAULT 'open',
4419
- ruling TEXT,
4420
- majority_vote TEXT,
4421
- resolved_at TEXT,
4422
- created_at TEXT DEFAULT (datetime('now'))
4423
- )`);
4424
- db.exec(`CREATE INDEX IF NOT EXISTS idx_act_status ON auction_claim_tasks(status)`);
4425
- db.exec(`CREATE INDEX IF NOT EXISTS idx_act_auction ON auction_claim_tasks(auction_id)`);
4426
- db.exec(`CREATE TABLE IF NOT EXISTS auction_claim_votes (
4427
- id TEXT PRIMARY KEY,
4428
- claim_id TEXT NOT NULL,
4429
- verifier_id TEXT NOT NULL,
4430
- vote TEXT NOT NULL,
4431
- evidence_uri TEXT,
4432
- note TEXT,
4433
- was_majority INTEGER,
4434
- voted_at TEXT DEFAULT (datetime('now')),
4435
- UNIQUE(claim_id, verifier_id)
4436
- )`);
4437
- db.exec(`CREATE INDEX IF NOT EXISTS idx_acv_claim ON auction_claim_votes(claim_id)`);
4438
- // Sprint 3-B: 慈善许愿声明(wishes)
4439
- // 任何非 wisher 用户可对 wish 真实性发起验证
4440
- db.exec(`CREATE TABLE IF NOT EXISTS wish_claim_tasks (
4441
- id TEXT PRIMARY KEY,
4442
- wish_id TEXT NOT NULL,
4443
- wisher_id TEXT NOT NULL,
4444
- claimant_id TEXT NOT NULL,
4445
- claim_target TEXT NOT NULL, -- 'fake_identity' | 'fake_story' | 'already_fulfilled' | 'duplicate' | 'inappropriate' | 'other'
4446
- claim_text TEXT NOT NULL,
4447
- evidence_uri TEXT,
4448
- stake_claimant REAL NOT NULL,
4449
- deadline_at TEXT NOT NULL,
4450
- status TEXT NOT NULL DEFAULT 'open',
4451
- ruling TEXT,
4452
- majority_vote TEXT,
4453
- resolved_at TEXT,
4454
- created_at TEXT DEFAULT (datetime('now'))
4455
- )`);
4456
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wct_status ON wish_claim_tasks(status)`);
4457
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wct_wish ON wish_claim_tasks(wish_id)`);
4458
- db.exec(`CREATE TABLE IF NOT EXISTS wish_claim_votes (
4459
- id TEXT PRIMARY KEY,
4460
- claim_id TEXT NOT NULL,
4461
- verifier_id TEXT NOT NULL,
4462
- vote TEXT NOT NULL,
4463
- evidence_uri TEXT,
4464
- note TEXT,
4465
- was_majority INTEGER,
4466
- voted_at TEXT DEFAULT (datetime('now')),
4467
- UNIQUE(claim_id, verifier_id)
4468
- )`);
4469
- db.exec(`CREATE INDEX IF NOT EXISTS idx_wcv_claim ON wish_claim_votes(claim_id)`);
3108
+ // verifier 禁言 / 永封记录 → server-schema.ts
3109
+ initClaimVerifierSuspensionsSchema(db);
3110
+ // Sprint 1: 商品声明验证 → server-schema.ts
3111
+ initProductClaimSchema(db);
3112
+ // Sprint 2-A: 测评真实性验证 server-schema.ts
3113
+ initReviewClaimSchema(db);
3114
+ // Sprint 2-B: 二手成色验证 → server-schema.ts
3115
+ initSecondhandClaimSchema(db);
3116
+ // Sprint 3-A: 拍卖声明 → server-schema.ts
3117
+ initAuctionClaimSchema(db);
3118
+ // Sprint 3-B: 慈善许愿声明 server-schema.ts
3119
+ initWishClaimSchema(db);
4470
3120
  }
4471
3121
  catch (e) {
4472
3122
  console.error('[M7.3 schema claim_verification]', e);
@@ -4603,24 +3253,23 @@ function computeAgentTrust(apiKey) {
4603
3253
  ? db.prepare(`SELECT COUNT(DISTINCT user_id) as n FROM registration_audit_log WHERE ip_hash = ?`).get(myReg.ip_hash).n
4604
3254
  : 0;
4605
3255
  const sameIpOthers = Math.max(0, sybilSize - 1); // 排除自己
4606
- // 双轨同支审计 / 上架限速命中
3256
+ // 放置同支审计 / 上架限速命中
4607
3257
  const crossHits = db.prepare(`SELECT COUNT(*) as n FROM commission_audit_log WHERE buyer_id = ? OR seller_id = ?`).get(user.id, user.id).n;
4608
3258
  // 限速命中 — 简化:30 天内 429 状态码次数
4609
3259
  const ratelimitHits = db.prepare(`SELECT COUNT(*) as n FROM agent_call_log WHERE api_key = ? AND status_code = 429 AND created_at > datetime('now', '-30 days')`).get(apiKey).n;
4610
- // 公式
3260
+ // 公式 — #420 P1-2:penalty 系数 / sybil 阈值 / 等级 cutoff 由 protocol_params 驱动(默认 = 原字面量)
3261
+ const t = readAntiAbuseThresholds(db);
4611
3262
  const agePts = Math.min(ageDays, 90) * 0.5;
4612
3263
  const orderPts = Math.min(completedBuyer + completedSeller, 50) * 0.5;
4613
3264
  const sharePts = Math.min(shareConversions, 20) * 1.0;
4614
3265
  const diversityPts = Math.min(diversity, 25) * 0.4;
4615
- const disputeP = -disputeLoss * 10;
4616
- const sybilP = sybilSize > 3 ? -(sybilSize - 3) * 5 : 0;
4617
- const crossP = -crossHits * 3;
4618
- const ratelimitP = -ratelimitHits * 2;
3266
+ const disputeP = -disputeLoss * t.trustDisputePenalty;
3267
+ const sybilP = agentSybilPenalty(sybilSize, t);
3268
+ const crossP = -crossHits * t.trustCrossPenalty;
3269
+ const ratelimitP = -ratelimitHits * t.trustRatelimitPenalty;
4619
3270
  const raw = agePts + orderPts + sharePts + diversityPts + disputeP + sybilP + crossP + ratelimitP;
4620
3271
  const trust = Math.max(0, Math.round(raw * 100) / 100);
4621
- const level = trust >= 80 ? 'legend' :
4622
- trust >= 50 ? 'quality' :
4623
- trust >= 20 ? 'trusted' : 'new';
3272
+ const level = agentTrustLevel(trust, t);
4624
3273
  const signals = {
4625
3274
  age_days: Math.round(ageDays * 10) / 10,
4626
3275
  completed_buyer: completedBuyer,
@@ -4672,7 +3321,7 @@ function antiCheatHash(input) {
4672
3321
  return 'unknown';
4673
3322
  return createHmac('sha256', MASTER_SEED).update('m3:' + input).digest('hex').slice(0, 24);
4674
3323
  }
4675
- // 双轨同支检测:写入 commission_audit_log(监测+证据;不阻断)
3324
+ // 放置同支检测:写入 commission_audit_log(监测+证据;不阻断)
4676
3325
  function auditSponsorChainCross(orderId, buyerId, sellerId, buyerSponsorPath) {
4677
3326
  if (buyerId === sellerId)
4678
3327
  return;
@@ -4803,7 +3452,7 @@ function extractUrlFromText(text) {
4803
3452
  return m?.[0] ?? null;
4804
3453
  }
4805
3454
  // Agent-first 设计:粘贴外链匹配 **只允许精准匹配**,没有 LIKE 兜底。
4806
- // 模糊匹配与外推荐留给未来独立的"模糊搜索"入口(参见 memory: WebAZ Search Philosophy)。
3455
+ // 模糊匹配与外推荐留给未来独立的"模糊搜索"入口(精准 trust,0 命中引导用户去 discover,不加 LIKE 兜底)。
4807
3456
  function searchByExternalLink(opts) {
4808
3457
  const cols = `p.id, p.title, p.description, p.price, p.stock, p.category, p.seller_id,
4809
3458
  p.specs, p.brand, p.model, p.handling_hours, p.return_days, p.warranty_days, p.ship_regions, p.fragile,
@@ -5021,19 +3670,8 @@ db.exec(`
5021
3670
  used_at TEXT
5022
3671
  )
5023
3672
  `);
5024
- db.exec(`
5025
- CREATE TABLE IF NOT EXISTS product_external_links (
5026
- id TEXT PRIMARY KEY,
5027
- product_id TEXT NOT NULL,
5028
- url TEXT NOT NULL,
5029
- source TEXT DEFAULT 'manual',
5030
- verified INTEGER DEFAULT 0,
5031
- verify_note TEXT,
5032
- added_at TEXT DEFAULT (datetime('now')),
5033
- verified_at TEXT,
5034
- UNIQUE(product_id, url)
5035
- )
5036
- `);
3673
+ // 外部链接验证 base → server-schema.ts;ALTER/index/回填 IIFE 刻意留原位(紧跟下方)
3674
+ initProductExternalLinksBaseSchema(db);
5037
3675
  try {
5038
3676
  db.exec('ALTER TABLE product_external_links ADD COLUMN revoked INTEGER DEFAULT 0');
5039
3677
  }
@@ -5084,58 +3722,12 @@ catch { }
5084
3722
  console.error('[backfill failed]', e.message);
5085
3723
  }
5086
3724
  })();
5087
- // link_challenges 保留用于向后兼容,新流程用 verify_tasks
5088
- db.exec(`
5089
- CREATE TABLE IF NOT EXISTS link_challenges (
5090
- id TEXT PRIMARY KEY,
5091
- product_id TEXT NOT NULL,
5092
- url TEXT NOT NULL,
5093
- code TEXT NOT NULL,
5094
- status TEXT DEFAULT 'pending',
5095
- created_at TEXT DEFAULT (datetime('now')),
5096
- expires_at TEXT NOT NULL,
5097
- verified_at TEXT
5098
- )
5099
- `);
5100
- db.exec(`
5101
- CREATE TABLE IF NOT EXISTS verify_tasks (
5102
- id TEXT PRIMARY KEY,
5103
- type TEXT NOT NULL DEFAULT 'code_check',
5104
- product_id TEXT NOT NULL,
5105
- url TEXT NOT NULL,
5106
- code TEXT,
5107
- verifiers_needed INTEGER NOT NULL DEFAULT 3,
5108
- reward_per_verifier REAL NOT NULL DEFAULT 0.1,
5109
- fee_locked REAL NOT NULL DEFAULT 0,
5110
- status TEXT NOT NULL DEFAULT 'open',
5111
- result TEXT,
5112
- created_at TEXT DEFAULT (datetime('now')),
5113
- expires_at TEXT NOT NULL,
5114
- settled_at TEXT
5115
- )
5116
- `);
5117
- db.exec(`
5118
- CREATE TABLE IF NOT EXISTS verify_submissions (
5119
- id TEXT PRIMARY KEY,
5120
- task_id TEXT NOT NULL,
5121
- verifier_id TEXT NOT NULL,
5122
- submission TEXT,
5123
- verdict TEXT,
5124
- claimed_at TEXT DEFAULT (datetime('now')),
5125
- submitted_at TEXT,
5126
- UNIQUE(task_id, verifier_id)
5127
- )
5128
- `);
5129
- db.exec(`
5130
- CREATE TABLE IF NOT EXISTS verifier_stats (
5131
- user_id TEXT PRIMARY KEY,
5132
- verify_rights INTEGER NOT NULL DEFAULT 3,
5133
- tasks_done INTEGER NOT NULL DEFAULT 0,
5134
- tasks_correct INTEGER NOT NULL DEFAULT 0,
5135
- tasks_wrong INTEGER NOT NULL DEFAULT 0,
5136
- suspended_until TEXT
5137
- )
5138
- `);
3725
+ // link_challenges 保留用于向后兼容,新流程用 verify_tasks → server-schema.ts
3726
+ initLinkChallengesSchema(db);
3727
+ initVerifyTasksSchema(db);
3728
+ initVerifySubmissionsSchema(db);
3729
+ // verifier_stats 须在 PERMANENT_ACCOUNTS bootstrap 之前完成 → server-schema.ts
3730
+ initVerifierStatsSchema(db);
5139
3731
  (() => {
5140
3732
  for (const acc of PERMANENT_ACCOUNTS) {
5141
3733
  const apiKey = 'key_perm_' + createHmac('sha256', MASTER_SEED).update(acc.seed).digest('hex');
@@ -5181,6 +3773,9 @@ const app = express();
5181
3773
  // — 防伪造(开发:直连 ::1/127 → req.ip 返回 socket IP;生产:信任同机/同 VPC 内的反代)
5182
3774
  // — 部署在 Cloudflare/nginx 后面时若不在同 VPC,需改为 ['cloudflare-cidr', ...] 或具体 IP 列表,绝不要写 true
5183
3775
  app.set('trust proxy', 'loopback, linklocal, uniquelocal');
3776
+ // Cloudflare-only origin guard (defense-in-depth vs direct-to-origin bypass). OFF by default;
3777
+ // configured via CF_ORIGIN_GUARD_MODE (off|observe|enforce) + CF_ORIGIN_SHARED_SECRET — see cf-origin-guard.ts.
3778
+ app.use(createCfOriginGuard());
5184
3779
  app.use(express.json());
5185
3780
  // ─── Security headers (CSP + nosniff + frame-options) ─────────
5186
3781
  // 注意:现有代码大量使用 inline onclick="..." / style="...",无法启用纯净 CSP
@@ -5296,32 +3891,23 @@ function invalidateAgentRiskCacheForUser(userId) {
5296
3891
  function issueAgentStrike(opts) {
5297
3892
  const { apiKey, userId, reasonCode } = opts;
5298
3893
  const initial = opts.initialSeverity || 'warning';
3894
+ // #420 P1-4:升级阶梯阈值/窗口/过期 由 protocol_params 驱动(默认 = 原 7d/30d/≥1/≥2/24h/7d)
3895
+ const t = readAntiAbuseThresholds(db);
5299
3896
  // 看是否需要升级
5300
3897
  const warnings7d = db.prepare(`SELECT COUNT(*) as n FROM agent_strikes
5301
- WHERE api_key = ? AND severity = 'warning' AND issued_at > datetime('now', '-7 days')
3898
+ WHERE api_key = ? AND severity = 'warning' AND issued_at > datetime('now', '-${t.strikeWarnWindowDays} days')
5302
3899
  AND appeal_status NOT IN ('approved')`).get(apiKey).n;
5303
3900
  const suspends30d = db.prepare(`SELECT COUNT(*) as n FROM agent_strikes
5304
- WHERE api_key = ? AND severity = 'suspend_7d' AND issued_at > datetime('now', '-30 days')
3901
+ WHERE api_key = ? AND severity = 'suspend_7d' AND issued_at > datetime('now', '-${t.strikeSuspendWindowDays} days')
5305
3902
  AND appeal_status NOT IN ('approved')`).get(apiKey).n;
5306
- let severity = initial;
5307
- let escalated = false;
5308
- if (initial === 'warning' && warnings7d >= 1) { // 已有 1 次 warning + 本次新 warning = 累计 2 → 升 suspend
5309
- severity = 'suspend_7d';
5310
- escalated = true;
5311
- }
5312
- if (initial === 'suspend_7d' || severity === 'suspend_7d') {
5313
- if (suspends30d >= 2) { // 已有 2 次 suspend + 本次 = 3 → 升 permanent
5314
- severity = 'permanent';
5315
- escalated = true;
5316
- }
5317
- }
3903
+ const { severity, escalated } = agentStrikeSeverity(initial, warnings7d, suspends30d, t);
5318
3904
  // expires_at
5319
3905
  let expiresAt = null;
5320
3906
  if (severity === 'warning') {
5321
- expiresAt = new Date(Date.now() + 24 * 3600_000).toISOString().replace('T', ' ').slice(0, 19);
3907
+ expiresAt = new Date(Date.now() + t.strikeWarnExpiryHours * 3600_000).toISOString().replace('T', ' ').slice(0, 19);
5322
3908
  }
5323
3909
  else if (severity === 'suspend_7d') {
5324
- expiresAt = new Date(Date.now() + 7 * 86400_000).toISOString().replace('T', ' ').slice(0, 19);
3910
+ expiresAt = new Date(Date.now() + t.strikeSuspendExpiryDays * 86400_000).toISOString().replace('T', ' ').slice(0, 19);
5325
3911
  }
5326
3912
  db.prepare(`INSERT INTO agent_strikes (api_key, user_id, severity, reason_code, reason_detail, reported_by, related_ref, expires_at)
5327
3913
  VALUES (?,?,?,?,?,?,?,?)`).run(apiKey, userId, severity, reasonCode, opts.reasonDetail || null, opts.reportedBy || 'system', opts.relatedRef || null, expiresAt);
@@ -5854,8 +4440,6 @@ registerAuthRegisterRoutes(app, {
5854
4440
  clientIpHash, clientUaHash,
5855
4441
  get VALID_REGIONS() { return VALID_REGIONS; },
5856
4442
  pickPreferredSide, joinPowerLeg,
5857
- get INVITE_ROTATION_HANDLES() { return INVITE_ROTATION_HANDLES; },
5858
- inviteRotationLookup,
5859
4443
  // 邮箱验证优先注册 — issueCode/findActiveCode 是 hoisted 函数声明、isVerificationEmailReady/
5860
4444
  // emailDeliveryNotConfigured 是 import,均可在此安全引用;CODE_TTL_MIN/MAX_CODE_ATTEMPTS 是后置 const,
5861
4445
  // 走 getter 延迟读避免 TDZ。
@@ -5894,6 +4478,21 @@ registerTaskProposalsRoutes(app, {
5894
4478
  db, errorRes,
5895
4479
  requireSupportAdmin: (req, res) => requireAdminPermission(req, res, 'support'),
5896
4480
  rateLimitOk: (key) => proposalRateLimiter(key),
4481
+ auth, // required auth for proposer-facing /api/me/task-proposals
4482
+ resolveUser: (req) => getUser(req), // optional resolver — links a submission to the logged-in submitter
4483
+ });
4484
+ // PR #18 — build_task create quota-increase requests(requester submit + ROOT-only review/approve/reject/revoke)
4485
+ registerBuildTaskQuotaRoutes(app, {
4486
+ db, errorRes, auth,
4487
+ requireRootAdmin: (req, res) => requireRootAdmin(req, res),
4488
+ });
4489
+ // Phase 2 — admin operator-claim workflow: link an admin SEAT → a real contributor account
4490
+ // (propose → confirm → approve → revoke/supersede). Claim workflow only; writes NO contribution_facts.
4491
+ registerAdminOperatorClaimRoutes(app, {
4492
+ db, errorRes, auth,
4493
+ requireAdmin: (req, res) => requireAdmin(req, res),
4494
+ requireRootAdmin: (req, res) => requireRootAdmin(req, res),
4495
+ consumeGateToken, // unlink REQUEST requires a fresh passkey gate (purpose 'operator_claim_unlink')
5897
4496
  });
5898
4497
  // RFC-006 Gap 2:贡献者自查看板(build_reputation 独立池)
5899
4498
  registerBuildReputationRoutes(app, { db, auth });
@@ -5908,6 +4507,10 @@ registerContributionIdentityRoutes(app, {
5908
4507
  // PR5F — Contribution Score v1 evidence READ surface (logged-in self-view; read-only, no score).
5909
4508
  // Returns the caller's OWN component evidence wrapped in the PR5A uncommitted-value boundary.
5910
4509
  registerContributionScoreRoutes(app, { auth, errorRes });
4510
+ // Contribution read-out V1 — the caller's OWN attributable facts (GitHub + admin coordination), grouped
4511
+ // by source, read-only, wrapped in the uncommitted-value boundary. Attribution is read-time (GitHub
4512
+ // binding overlay + operator-claim as-of); writes nothing, no reward/payout.
4513
+ registerContributionFactsRoutes(app, { db, auth, errorRes });
5911
4514
  // #1013 Phase 48: 3 auth/sessions endpoints 已迁出到 routes/auth-sessions.ts
5912
4515
  registerAuthSessionsRoutes(app, { db, auth, verifyPassword, recordSession, generateSecureKey });
5913
4516
  // 个人资料:查看 API Key + 联系方式
@@ -5956,8 +4559,8 @@ registerAgentGovernanceRoutes(app, {
5956
4559
  // 监护人指纹:HMAC(MASTER_SEED) over owner id — 可追溯(协议持 seed)不暴露身份
5957
4560
  custodianFingerprint: (ownerId) => createHmac('sha256', MASTER_SEED).update('custodian:' + ownerId).digest('hex').slice(0, 16),
5958
4561
  // Phase 4 护照签名:用协议热钱包私钥签(eip191),issuer 地址 = DID 锚点,任何人 ecrecover 可验(闭包→调用时求值,晚于热钱包初始化)
5959
- signPassport: (message) => privateKeyToAccount(derivePrivKey('platform-hot-wallet')).signMessage({ message }),
5960
- issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
4562
+ signPassport: (message) => walletSigner.issuerSignMessage(message),
4563
+ issuerAddress: () => walletSigner.issuerAddress(),
5961
4564
  });
5962
4565
  // ─── 2026-05-22 COP 飞轮 + COP P0-1 数据导出 ─────────────
5963
4566
  // #1013 Phase 39: 2 endpoints (note-prompts + export) 已迁出到 routes/me-data.ts
@@ -6623,60 +5226,9 @@ registerAdminTokenomicsRoutes(app, {
6623
5226
  logAdminAction,
6624
5227
  });
6625
5228
  // system-flags — Phase 107 已迁出
6626
- // 邀请码轮询 — "issued + registered 自纠偏"模型
6627
- const INVITE_ROTATION_HANDLES = ['xiaohua', 'mian', 'holden', 'jiayi', 'qingliang'];
6628
- const INVITE_REBALANCE_THRESHOLD = 5; // max_reg - min_reg ≥ 5 → 进入补齐模式
6629
- function readInviteStats() {
6630
- return db.prepare("SELECT slot, issued_count as issued, registered_count as registered FROM invite_rotation_stats ORDER BY slot")
6631
- .all();
6632
- }
6633
- function inviteRotationLookup(handleIdx) {
6634
- const handle = INVITE_ROTATION_HANDLES[handleIdx];
6635
- const u = db.prepare("SELECT id, permanent_code, handle, name FROM users WHERE handle = ?").get(handle);
6636
- if (!u?.permanent_code)
6637
- return null;
6638
- return { id: u.id, code: u.permanent_code, handle: u.handle, name: u.name };
6639
- }
6640
- // 派号:取 (registered 不均衡 ? min_registered : min_issued);同值取低 slot
6641
- function pickInviteSlot(stats) {
6642
- const regs = stats.map(s => s.registered);
6643
- const maxReg = Math.max(...regs), minReg = Math.min(...regs);
6644
- const useReg = (maxReg - minReg) >= INVITE_REBALANCE_THRESHOLD;
6645
- const key = useReg ? 'registered' : 'issued';
6646
- const minVal = useReg ? minReg : Math.min(...stats.map(s => s.issued));
6647
- for (const s of stats) {
6648
- if (s[key] === minVal)
6649
- return s.slot;
6650
- }
6651
- return 0;
6652
- }
6653
- // 派号 + issued++ 包在 transaction(better-sqlite3 同步顺序执行)
6654
- const issueInviteSlot = db.transaction(() => {
6655
- const stats = readInviteStats();
6656
- const slot = pickInviteSlot(stats);
6657
- db.prepare("UPDATE invite_rotation_stats SET issued_count = issued_count + 1 WHERE slot = ?").run(slot);
6658
- return slot;
6659
- });
6660
5229
  // #1013 Phase 73: GET /api/reviews/recent 已迁出(claim 2 端点也由同模块注册,定义在下游)
6661
- // B-4: 编辑精选 / 每周推荐
6662
- db.exec(`
6663
- CREATE TABLE IF NOT EXISTS editor_picks (
6664
- id TEXT PRIMARY KEY,
6665
- kind TEXT NOT NULL, -- 'product' | 'seller'
6666
- target_id TEXT NOT NULL,
6667
- title TEXT, -- 编辑推荐语
6668
- note TEXT,
6669
- starts_at TEXT NOT NULL,
6670
- ends_at TEXT NOT NULL,
6671
- sort_order INTEGER DEFAULT 0,
6672
- created_by TEXT,
6673
- created_at TEXT DEFAULT (datetime('now'))
6674
- )
6675
- `);
6676
- try {
6677
- db.exec('CREATE INDEX IF NOT EXISTS idx_ep_active ON editor_picks(kind, ends_at, sort_order)');
6678
- }
6679
- catch { }
5230
+ // B-4: 编辑精选 / 每周推荐 → server-schema.ts
5231
+ initEditorPicksSchema(db);
6680
5232
  // editor-picks 公开 — Phase 107 已迁出
6681
5233
  // #1013 Phase 66: 3 admin/editor-picks endpoints 已迁出
6682
5234
  registerAdminEditorPicksRoutes(app, {
@@ -6738,25 +5290,8 @@ registerAdminOpsRoutes(app, {
6738
5290
  });
6739
5291
  // AI 2 endpoints — Phase 100 已迁出
6740
5292
  registerAiRoutes(app, { db, auth, anthropic });
6741
- // D-3: KYC light — 实名认证(轻度,不存原始证件号)
6742
- db.exec(`
6743
- CREATE TABLE IF NOT EXISTS kyc_records (
6744
- user_id TEXT PRIMARY KEY,
6745
- real_name TEXT NOT NULL,
6746
- id_type TEXT NOT NULL, -- 'passport' | 'national_id' | 'driver_license'
6747
- id_number_hash TEXT NOT NULL, -- sha256(id_number + MASTER_SEED)
6748
- id_number_last4 TEXT, -- 末 4 位明文(便于核对)
6749
- status TEXT NOT NULL DEFAULT 'pending', -- pending / approved / rejected
6750
- reject_reason TEXT,
6751
- reviewed_by TEXT,
6752
- reviewed_at TEXT,
6753
- submitted_at TEXT DEFAULT (datetime('now'))
6754
- )
6755
- `);
6756
- try {
6757
- db.exec('CREATE INDEX IF NOT EXISTS idx_kyc_status ON kyc_records(status, submitted_at)');
6758
- }
6759
- catch { }
5293
+ // D-3: KYC light — 实名认证(轻度,不存原始证件号)→ server-schema.ts
5294
+ initKycRecordsSchema(db);
6760
5295
  // KYC 2 endpoints — Phase 97 已迁出
6761
5296
  registerKycRoutes(app, { db, auth, MASTER_SEED });
6762
5297
  // #1013 Phase 68: 6 admin/kyc+risk endpoints 已迁出
@@ -6766,11 +5301,9 @@ registerAdminModerationRoutes(app, {
6766
5301
  authFailures, INTERNAL_AUDITOR_ID, broadcastSystemEvent,
6767
5302
  logAdminAction,
6768
5303
  });
6769
- // 邀请码 3 endpoints — Phase 98 已迁出
5304
+ // 邀请 endpoints — Phase 98 已迁出
6770
5305
  registerReferralRoutes(app, {
6771
5306
  db, auth,
6772
- requireProtocolAdmin: (req, res) => requireAdminPermission(req, res, 'protocol'),
6773
- logAdminAction, issueInviteSlot, inviteRotationLookup,
6774
5307
  });
6775
5308
  // 推土机权限:是否允许作为 sponsor 拿分享佣金
6776
5309
  // 默认:必须有 ≥ 1 笔 completed 订单(verified buyer)才能 sponsor
@@ -6833,7 +5366,7 @@ registerAdminReportsRoutes(app, {
6833
5366
  requireArbitrationAdmin: (req, res) => requireAdminPermission(req, res, 'arbitration'),
6834
5367
  requireProtocolAdmin: (req, res) => requireAdminPermission(req, res, 'protocol'),
6835
5368
  });
6836
- // ─── 原子能轨道(Phase 2):双轨树挂靠 ───────────────────────
5369
+ // ─── 放置树挂靠(中性参与记录:position + 每腿 PV 累计) ───────────────────────
6837
5370
  const PV_PROPAGATION_DEPTH_LIMIT = 5000; // PV 累积深度上限(admin 可调,存 system_state 之后)
6838
5371
  // (2026-06-04 移除 countSubtreeUsers — 旧实现只数单条脊链, 名实不符;
6839
5372
  // team_count 改读增量维护的 users.left_count/right_count, 见 pickPreferredSide + joinPowerLeg)
@@ -6907,12 +5440,15 @@ function joinPowerLeg(inviterId, side, newUserId) {
6907
5440
  // ─── 原子能 Cron 结算引擎 ─────────────────────────────────────
6908
5441
  // Step 1: 处理 pv_ledger → 累积到上线 total_left/right_pv(最多 5000 层)
6909
5442
  function processPvLedger() {
5443
+ // Category C: 纯聚合(pv_ledger → total_left/right_pv 计数,不产生 score/WAZ/权益)= 参与记录 → 默认 ON。
5444
+ if (!participationRecordingActive(db))
5445
+ return 0;
6910
5446
  const pending = db.prepare(`SELECT * FROM pv_ledger WHERE processed = 0 ORDER BY created_at ASC LIMIT 1000`).all();
6911
5447
  for (const lg of pending) {
6912
5448
  try {
6913
5449
  const buyer = db.prepare("SELECT placement_id, placement_side, placement_path FROM users WHERE id = ?").get(lg.buyer_id);
6914
5450
  if (!buyer?.placement_id || !buyer.placement_side) {
6915
- // buyer 不在双轨树(无 placement)→ 流水跳过但标 processed
5451
+ // buyer 不在放置树(无 placement)→ 流水跳过但标 processed
6916
5452
  db.prepare("UPDATE pv_ledger SET processed = 1 WHERE id = ?").run(lg.id);
6917
5453
  continue;
6918
5454
  }
@@ -6957,258 +5493,17 @@ function processPvLedger() {
6957
5493
  }
6958
5494
  return pending.length;
6959
5495
  }
6960
- export function calculate7LevelTaperingScore(leftPv, rightPv, tiersDescByThreshold) {
6961
- const minPv = Math.min(leftPv, rightPv);
6962
- if (minPv <= 0)
6963
- return null;
6964
- for (const t of tiersDescByThreshold) {
6965
- if (minPv >= t.pv_threshold) {
6966
- return {
6967
- tier: t.tier,
6968
- score: t.score_per_hit,
6969
- consumedLeft: leftPv,
6970
- consumedRight: rightPv,
6971
- };
6972
- }
6973
- }
6974
- return null; // 不达最低档(< 300 PV)→ PV 保留滚动到下一期
6975
- }
6976
- // Step 2: 对碰结算 — 单次匹配 + 双侧全清
6977
- function runBinarySettlement() {
6978
- const tiers = db.prepare("SELECT tier, pv_threshold, base_score, discount_coef, score_per_hit FROM binary_tier_config WHERE active = 1 ORDER BY pv_threshold DESC").all();
6979
- if (tiers.length === 0)
6980
- return 0;
6981
- const SINGLE_USER_PERIOD_CAP = 1500; // paranoia 上限(实际上 single-match 已天然限速)
6982
- const dirty = db.prepare(`SELECT id, total_left_pv, total_right_pv FROM users WHERE pv_dirty_at IS NOT NULL`).all();
6983
- let settled = 0;
6984
- const now = new Date().toISOString();
6985
- const periodStart = new Date(Date.now() - 7 * 86400_000).toISOString();
6986
- for (const u of dirty) {
6987
- // 注:对碰【分数照常计算累积】,不在此 gate region。PV 经济闸在【兑付】层
6988
- // (score → WAZ 时按 region pv_enabled 决定是否真实发放;未开启则分数挂账,
6989
- // 待辖区开启 / 用户迁移到可用辖区再兑现)。2026-06-04 解耦设计。
6990
- // 周期内已得 Score(paranoia 封顶)
6991
- 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;
6992
- if (weekScore >= SINGLE_USER_PERIOD_CAP) {
6993
- db.prepare("UPDATE users SET pv_dirty_at = NULL WHERE id = ?").run(u.id);
6994
- continue;
6995
- }
6996
- const match = calculate7LevelTaperingScore(u.total_left_pv, u.total_right_pv, tiers);
6997
- if (!match) {
6998
- // 不达最低档 → PV 保留滚动到下一期;只清 dirty 标记
6999
- db.prepare("UPDATE users SET pv_dirty_at = NULL WHERE id = ?").run(u.id);
7000
- continue;
7001
- }
7002
- // 命中 → 记 score + 双侧全清(含溢出归零,按 spec "对碰完成后双侧清零")
7003
- db.prepare(`INSERT INTO binary_score_records (id, user_id, tier, score, consumed_left_pv, consumed_right_pv, period_start, period_end)
7004
- VALUES (?,?,?,?,?,?,?,?)`)
7005
- .run(generateId('bsr'), u.id, match.tier, match.score, match.consumedLeft, match.consumedRight, periodStart, now);
7006
- db.prepare(`UPDATE global_fund SET total_scores_pending = total_scores_pending + ? WHERE id = 1`).run(match.score);
7007
- db.prepare("UPDATE users SET total_left_pv = 0, total_right_pv = 0, pv_dirty_at = NULL WHERE id = ?").run(u.id);
7008
- settled++;
7009
- }
7010
- return settled;
7011
- }
7012
- // 管理津贴比例(大博主的 L1/L2/L3 对碰获利时,博主额外得 10/5/2%)
7013
- const MGMT_BONUS_RATES = [0.10, 0.05, 0.02];
7014
- function executeSafeSettlementCron() {
7015
- const result = { periodId: '', status: 'noop' };
7016
- const txn = db.transaction(() => {
7017
- // 1. 快照基金池余额
7018
- const fund = db.prepare("SELECT pool_balance FROM global_fund WHERE id = 1").get();
7019
- const fundBalanceStart = Math.round(Number(fund?.pool_balance ?? 0) * 100) / 100;
7020
- if (fundBalanceStart <= 0) {
7021
- result.status = 'empty_pool';
7022
- return;
7023
- }
7024
- // 2. 全网当期 pending Score 汇总
7025
- // 2026-06-04 解耦:PV 隐藏不关闭 —— 分数照常计算累积。兑付层按 earner【当前 region】
7026
- // 的 pv_enabled 过滤:未开启的 earner 分数【保留 pending(不入本期分配、不稀释合格者单价)】,
7027
- // 待辖区开启 / 用户迁移到可用辖区,下期自动兑现。挂账的钱按实际发放扣减(见步骤7)自然留池。
7028
- const allPending = db.prepare(`SELECT id, user_id, score FROM binary_score_records WHERE settled_at IS NULL`).all();
7029
- const pending = allPending.filter(r => {
7030
- const reg = db.prepare("SELECT region FROM users WHERE id = ?").get(r.user_id)?.region || 'global';
7031
- return regionPvEnabled(reg);
7032
- });
7033
- if (pending.length === 0) {
7034
- result.status = 'no_pending';
7035
- return;
7036
- }
7037
- const totalScores = pending.reduce((s, r) => s + Number(r.score), 0);
7038
- if (totalScores <= 0) {
7039
- result.status = 'no_pending';
7040
- return;
7041
- }
7042
- // 3. 4 周历史均值(settlement_periods 中近 28 天 completed 行的 deposited 均值)
7043
- const histRow = db.prepare(`
7044
- SELECT AVG(deposited_this_period) as avg_dep
7045
- FROM settlement_periods
7046
- WHERE status = 'completed' AND started_at > datetime('now', '-28 days')
7047
- `).get();
7048
- const historyAverage = Math.round(Number(histRow?.avg_dep ?? 0) * 100) / 100;
7049
- // 3.5. R11 风控:基金池水位硬停(防挤兑)
7050
- // - 相对底线:池子 < 历史均值的 20% → 硬停(已稳定运行后保护机制)
7051
- // - 绝对底线:池子 < 100 WAZ → 硬停(冷启动期 / 极端情况保护)
7052
- // 触发时写一条 paused 周期日志(审计透明,便于 admin 监控 + 用户解释)
7053
- const POOL_FLOOR_RATIO = 0.2;
7054
- const POOL_FLOOR_ABSOLUTE = 100;
7055
- let pauseReason = null;
7056
- if (fundBalanceStart < POOL_FLOOR_ABSOLUTE) {
7057
- pauseReason = `pool_balance ${fundBalanceStart} < absolute floor ${POOL_FLOOR_ABSOLUTE}`;
7058
- }
7059
- else if (historyAverage > 0 && fundBalanceStart < historyAverage * POOL_FLOOR_RATIO) {
7060
- pauseReason = `pool_balance ${fundBalanceStart} < ${POOL_FLOOR_RATIO * 100}% of hist avg ${historyAverage}`;
7061
- }
7062
- if (pauseReason) {
7063
- const periodId = generateId('sp');
7064
- db.prepare(`INSERT INTO settlement_periods (
7065
- period_id, started_at, completed_at, fund_balance_start,
7066
- history_average, total_scores, status, note
7067
- ) VALUES (?, datetime('now'), datetime('now'), ?, ?, ?, 'paused_low_water', ?)`)
7068
- .run(periodId, fundBalanceStart, historyAverage, totalScores, pauseReason);
7069
- Object.assign(result, {
7070
- periodId, status: 'paused_low_water',
7071
- fund_balance_start: fundBalanceStart, history_average: historyAverage,
7072
- total_scores: totalScores, pause_reason: pauseReason,
7073
- });
7074
- return;
7075
- }
7076
- // 4. 动态拨出率(30% / 50% / 70%)
7077
- let payoutRate = 0.5;
7078
- if (historyAverage > 0) {
7079
- if (fundBalanceStart > historyAverage * 2)
7080
- payoutRate = 0.7; // 蓄水池极度健康
7081
- else if (fundBalanceStart < historyAverage * 0.5)
7082
- payoutRate = 0.3; // 水位见底保护
7083
- }
7084
- // 冷启动(无历史)默认 0.5
7085
- const poolToDistribute = Math.round(fundBalanceStart * payoutRate * 100) / 100;
7086
- const nValueCash = poolToDistribute / totalScores;
7087
- // V3 动态分配率:score_per_hit 已经 = 元,cap 是"票面比例"
7088
- // 池子健康度 = pool / hist_avg
7089
- // ≥ 2.0 → cap 1.6 (奖励早期用户,多发 60%)
7090
- // 1.0-2.0 → cap 1.0-1.6 线性插值
7091
- // < 1.0 → cap 1.0 (保底拿满票面)
7092
- // 冷启动期(hist=0)默认 1.0
7093
- let distributionCap = 1.0;
7094
- if (historyAverage > 0) {
7095
- const healthRatio = fundBalanceStart / historyAverage;
7096
- distributionCap = Math.max(1.0, Math.min(1.6, healthRatio));
7097
- }
7098
- const effectiveUnitCash = Math.min(nValueCash, distributionCap);
7099
- // 5. 本期入池额(用于下次的历史均值数据源)
7100
- const lastPeriodEnd = db.prepare(`SELECT MAX(started_at) as t FROM settlement_periods WHERE status = 'completed'`).get().t;
7101
- const depositedThisPeriod = Number(db.prepare(lastPeriodEnd
7102
- ? `SELECT COALESCE(SUM(amount_base + amount_l3), 0) as s FROM fund_deposits WHERE deposited_at > ?`
7103
- : `SELECT COALESCE(SUM(amount_base + amount_l3), 0) as s FROM fund_deposits`).get(...(lastPeriodEnd ? [lastPeriodEnd] : [])).s);
7104
- // 6. 派发(按 effectiveUnitCash 计算每用户 cash,等额 WAZ 入钱包)
7105
- let cashDistributed = 0;
7106
- let bonusPaid = 0;
7107
- let settledUsers = 0;
7108
- const mgmtEnabled = db.prepare("SELECT value FROM system_state WHERE key = 'mgmt_bonus_enabled'").get()?.value === '1';
7109
- for (const r of pending) {
7110
- const cashDue = Math.round(Number(r.score) * effectiveUnitCash * 100) / 100;
7111
- if (cashDue <= 0) {
7112
- db.prepare("UPDATE binary_score_records SET settled_at = datetime('now'), waz_amount = 0 WHERE id = ?").run(r.id);
7113
- continue;
7114
- }
7115
- // V1:1 WAZ = 1 元(场景 B 杠杆延后)
7116
- const wazAmount = cashDue;
7117
- // RFC-002 §3.5 PV-pair opt-in gate (PR-1c-b) — symmetric to settleCommission L1/L2/L3 gate
7118
- // opted-in → normal wallet credit
7119
- // opted-out + 'deactivate' → 不发放,waz 留在 PV 资金池(forfeit,非慈善)
7120
- // opted-out + other → escrow(pv_pair),金额从 pool 移入 pv_escrow_reserve(#1106 隔离负债)
7121
- // 2026-06-04 修双计 bug:deactivate 旧版 redirectToCharity 新增慈善余额,但 waz 因不计入
7122
- // cashDistributed 已留在 pool → 钱印了两份。现 deactivate 仅标记 settled,钱留 PV 资金池。
7123
- const optIn = db.prepare("SELECT rewards_opted_in FROM users WHERE id = ?").get(r.user_id)?.rewards_opted_in ?? 0;
7124
- if (optIn !== 1) {
7125
- const lastAction = db.prepare("SELECT action FROM rewards_applications WHERE user_id = ? ORDER BY created_at DESC LIMIT 1").get(r.user_id)?.action;
7126
- const isEscrow = lastAction !== 'deactivate';
7127
- db.transaction(() => {
7128
- if (isEscrow) {
7129
- // never_activated / auto_downgrade → escrow(pv_pair)。
7130
- // #1106:把 waz 从可分配池移入 pv_escrow_reserve(隔离负债),避免后续周期把这笔预留发给别人、
7131
- // 到兑付时池里没钱却仍给钱包加钱(凭空印钱)。兑付从 reserve 出、到期退回 pool。
7132
- const escrowDays = Number(db.prepare("SELECT value FROM protocol_params WHERE key = 'rewards_opt_in.escrow_days'").get()?.value ?? 30);
7133
- const nowMs = Date.now();
7134
- 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', ?, ?)`)
7135
- .run(r.user_id, wazAmount, nowMs, nowMs + escrowDays * 86400 * 1000);
7136
- db.prepare("UPDATE global_fund SET pv_escrow_reserve = pv_escrow_reserve + ? WHERE id = 1").run(wazAmount);
7137
- }
7138
- // deactivate:什么都不转,waz 自然留在 pool(cashRetained)
7139
- db.prepare("UPDATE binary_score_records SET settled_at = datetime('now'), waz_amount = 0 WHERE id = ?").run(r.id);
7140
- })();
7141
- // escrow 分支:计入 cashDistributed → 末尾 cashRetained 会把这笔从 pool 扣掉(已移入 reserve)。
7142
- // deactivate 分支:不计入 → 留在 pool。
7143
- if (isEscrow)
7144
- cashDistributed += cashDue;
7145
- // Do NOT credit wallet, lifetime_score, or count as settledUser — opt-out path
7146
- continue;
7147
- }
7148
- db.prepare("UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?").run(wazAmount, wazAmount, r.user_id);
7149
- db.prepare("UPDATE binary_score_records SET settled_at = datetime('now'), waz_amount = ? WHERE id = ?").run(wazAmount, r.id);
7150
- // V3 用户成长等级:累加 lifetime_score(实际兑现的 WAZ 金额,不是 pending score)
7151
- db.prepare("UPDATE users SET lifetime_score = COALESCE(lifetime_score, 0) + ? WHERE id = ?").run(wazAmount, r.user_id);
7152
- cashDistributed += cashDue;
7153
- settledUsers++;
7154
- // 管理津贴(默认关闭;仅 mgmt_bonus_enabled='1' 且 ancestor mgmt_bonus_eligible=1 时发)
7155
- if (!mgmtEnabled)
7156
- continue;
7157
- try {
7158
- const sp = db.prepare("SELECT sponsor_path FROM users WHERE id = ?").get(r.user_id);
7159
- const ancestors = sp?.sponsor_path ? sp.sponsor_path.split('>').reverse().slice(0, 3) : [];
7160
- for (let i = 0; i < ancestors.length; i++) {
7161
- const elig = db.prepare("SELECT mgmt_bonus_eligible FROM users WHERE id = ?").get(ancestors[i]);
7162
- if (!elig?.mgmt_bonus_eligible)
7163
- continue;
7164
- const bonus = Math.round(wazAmount * MGMT_BONUS_RATES[i] * 100) / 100;
7165
- if (bonus <= 0)
7166
- continue;
7167
- const pool = db.prepare("SELECT balance FROM management_bonus_pool WHERE id=1").get();
7168
- if (pool.balance < bonus)
7169
- continue;
7170
- db.prepare("UPDATE management_bonus_pool SET balance = balance - ? WHERE id=1").run(bonus);
7171
- db.prepare("UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?").run(bonus, bonus, ancestors[i]);
7172
- bonusPaid += bonus;
7173
- }
7174
- }
7175
- catch (e) {
7176
- console.error('[mgmt bonus]', e);
7177
- }
7178
- }
7179
- cashDistributed = Math.round(cashDistributed * 100) / 100;
7180
- // 7. 沉淀剩余(pool - distributed = surplus 留在基金池,下期使用)
7181
- const cashRetained = Math.round((fundBalanceStart - cashDistributed) * 100) / 100;
7182
- db.prepare("UPDATE global_fund SET pool_balance = ?, total_scores_pending = 0, current_n = ?, last_settled_at = datetime('now') WHERE id = 1")
7183
- .run(cashRetained, effectiveUnitCash);
7184
- // 8. 周期日志(幂等保障 + 4 周历史源 + 审计)
7185
- const periodId = generateId('sp');
7186
- db.prepare(`INSERT INTO settlement_periods (
7187
- period_id, started_at, completed_at, fund_balance_start, deposited_this_period,
7188
- history_average, payout_rate, pool_to_distribute, total_scores, n_value_cash,
7189
- effective_unit_cash, cash_distributed, cash_retained, settled_users, status
7190
- ) VALUES (?, datetime('now'), datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'completed')`)
7191
- .run(periodId, fundBalanceStart, depositedThisPeriod, historyAverage, payoutRate, poolToDistribute, totalScores, nValueCash, effectiveUnitCash, cashDistributed, cashRetained, settledUsers);
7192
- Object.assign(result, {
7193
- periodId, status: 'completed',
7194
- fund_balance_start: fundBalanceStart, history_average: historyAverage,
7195
- payout_rate: payoutRate, pool_to_distribute: poolToDistribute,
7196
- total_scores: totalScores, n_value_cash: Math.round(nValueCash * 100) / 100,
7197
- effective_unit_cash: Math.round(effectiveUnitCash * 100) / 100,
7198
- cash_distributed: cashDistributed, cash_retained: cashRetained,
7199
- settled_users: settledUsers, mgmt_bonus_paid: Math.round(bonusPaid * 100) / 100,
7200
- });
7201
- });
7202
- try {
7203
- txn();
7204
- }
7205
- catch (e) {
7206
- console.error('[safeSettlement]', e);
7207
- result.status = 'failed';
7208
- }
7209
- return result;
7210
- }
7211
- // 启动定时任务(每 1h 处理 ledger + settle;每 24h 安全阀结算)
5496
+ // ─── 匹配奖励结算引擎(Category C) 已切除 / EXCISED ───────────────────────────
5497
+ // 匹配奖励结算 + 兑付 = REWARD 路径,已从公开代码切除:internal/pv-settlement.ts
5498
+ // 现为永久 no-op stub(runBinarySettlement 0;executeSafeSettlementCron → disabled,无视 kill-switch)
5499
+ // 比门控更强 —— 即便翻 matching_rewards_active='1',公开代码也无引擎可跑、不会兑付。完整引擎归档在
5500
+ // docs/modules/pv-settlement-engine.INTERNAL.md(gitignored)+ git 历史;重启需律师/治理放行 + 重接,非翻 flag。
5501
+ // 留在本文件的中性【参与记录】(默认 ON,不受影响):joinPowerLeg(放置树)/ processPvLedger(PV 聚合)/ calculatePv。
5502
+ // 注:工厂签名不变,下游 1h/24h cron 调用点零改动(stub 安全返回)。regionPvEnabled 为函数声明(hoisted)。
5503
+ const { runBinarySettlement, executeSafeSettlementCron } = createPvSettlementEngine({ db, generateId, regionPvEnabled });
5504
+ // 启动定时任务(每 1h 处理 ledger + settle;每 24h 兑付结算)
5505
+ // Category C:1h cron 混了【参与记录】(processPvLedger,默认 ON)+【奖励】(runBinarySettlement,默认 OFF) —
5506
+ // 不加外层守卫,各函数自门控(recording vs rewards)。24h 兑付是纯 REWARD → 外层 matchingRewardsActive 守卫。
7212
5507
  setInterval(() => {
7213
5508
  try {
7214
5509
  processPvLedger();
@@ -7224,6 +5519,8 @@ setInterval(() => {
7224
5519
  }
7225
5520
  }, 60 * 60_000);
7226
5521
  setInterval(() => {
5522
+ if (!matchingRewardsActive(db))
5523
+ return;
7227
5524
  try {
7228
5525
  const r = executeSafeSettlementCron();
7229
5526
  if (r.status === 'completed') {
@@ -7275,7 +5572,7 @@ registerProfilePlacementRoutes(app, {
7275
5572
  // shares/dashboard — Phase 110 已迁出
7276
5573
  // ─── 推土机轨道:推广统计端点 ─────────────────────────────────
7277
5574
  // #1013 Phase 77: 2 promoter endpoints 已迁出
7278
- registerPromoterRoutes(app, { db, auth, isAllowedSponsor });
5575
+ registerPromoterRoutes(app, { db, auth, isAllowedSponsor, participationRecordingActive: () => participationRecordingActive(db), matchingRewardsActive: () => matchingRewardsActive(db) });
7279
5576
  // ─── 成长任务(分享达人养成主线)─────────────────────────────
7280
5577
  // #1013 Phase 30: 4 endpoints + catalog + evaluator 已迁出到 routes/growth.ts
7281
5578
  registerGrowthRoutes(app, { db, auth });
@@ -7732,8 +6029,8 @@ registerOrdersCreateRoutes(app, {
7732
6029
  getProductShareChain, isAllowedSponsor, checkStockAndMaybeDelist, auditSponsorChainCross,
7733
6030
  appendOrderEvent, transition, notifyTransition, shouldAutoAccept, ensureCharityRep,
7734
6031
  broadcastSystemEvent, resolveInviteCodeRef,
7735
- signPassport: (message) => privateKeyToAccount(derivePrivKey('platform-hot-wallet')).signMessage({ message }),
7736
- issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
6032
+ signPassport: (message) => walletSigner.issuerSignMessage(message),
6033
+ issuerAddress: () => walletSigner.issuerAddress(),
7737
6034
  });
7738
6035
  // #1013 Phase 45: 卖家配额 + 数据中心 7 endpoints — 2026-05-31 修补:之前 import 了但忘了 register,
7739
6036
  // 导致 /api/seller/quota-status + /api/seller/insights 落入 SPA fallback 返回 HTML,前端 JSON.parse 死循环
@@ -8375,14 +6672,8 @@ setInterval(() => {
8375
6672
  // #1013 Phase 4: 8 endpoints + 4 helpers 已迁出到 routes/chat.ts
8376
6673
  // ============================================================
8377
6674
  registerChatRoutes(app, { db, auth, generateId, rateLimitOk });
8378
- // 初始化导入次数追踪表
8379
- db.exec(`
8380
- CREATE TABLE IF NOT EXISTS import_logs (
8381
- id TEXT PRIMARY KEY,
8382
- user_id TEXT NOT NULL,
8383
- created_at TEXT DEFAULT (datetime('now'))
8384
- )
8385
- `);
6675
+ // 初始化导入次数追踪表 → server-schema.ts
6676
+ initImportLogsSchema(db);
8386
6677
  const FREE_IMPORT_LIMIT = 10;
8387
6678
  // #1013 Phase 114: import-product 已迁出
8388
6679
  registerImportProductRoutes(app, {
@@ -8779,8 +7070,8 @@ registerExternalAnchorsRoutes(app, { db, auth });
8779
7070
  // #1013 Phase 109: checkout/tax-preview + verify-price 已迁出
8780
7071
  registerCheckoutHelpersRoutes(app, {
8781
7072
  db, auth, generateId, formatProductForAgent,
8782
- signPassport: (message) => privateKeyToAccount(derivePrivKey('platform-hot-wallet')).signMessage({ message }),
8783
- issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
7073
+ signPassport: (message) => walletSigner.issuerSignMessage(message),
7074
+ issuerAddress: () => walletSigner.issuerAddress(),
8784
7075
  });
8785
7076
  // ─── M8 二手板块 ────────────────────────────────────────────
8786
7077
  // #1013 Phase 27: 6 endpoints + 4 SH_* sets + addHours 已迁出到 routes/secondhand.ts
@@ -8848,8 +7139,10 @@ function getRegionMaxLevels(region) {
8848
7139
  // 已显式审计且合规允许的地区在 region_config 表里设 2 或 3
8849
7140
  return row?.max_levels ?? 1;
8850
7141
  }
8851
- // 2026-06-04 解耦:PV 双轨/对碰系统是否在该 region 开启 —— 与 max_levels(佣金层级)独立。
8852
- // 未配置 / 未知地区一律 false(保守默认 PV 全关,需治理显式开 pv_enabled=1)。
7142
+ // PV 匹配奖励的【区域兑付过滤器】—— 不是奖励总闸。与 max_levels(佣金层级)独立。
7143
+ // 总闸是全局 Category C 双闸(见 pv-kill-switch.ts):实际兑付仍必须同时满足
7144
+ // matching_rewards_active='1' + matching_rewards_activation_cleared='1';本函数只是其后的额外区域过滤。
7145
+ // 未配置 / 未知地区返回 false,表示该地区过滤器不允许兑付(保守默认)。
8853
7146
  function regionPvEnabled(region) {
8854
7147
  const row = db.prepare("SELECT pv_enabled FROM region_config WHERE region = ?").get(region);
8855
7148
  return Number(row?.pv_enabled ?? 0) === 1;
@@ -9013,10 +7306,13 @@ function getUserLevel(lifetimeScore) {
9013
7306
  }
9014
7307
  // V3 PV 单位:每 100 元成交 = 1 PV(pv_multiplier 默认 1.0)
9015
7308
  // MAX_PV_PER_ORDER 1000 防单笔暴增 — 单笔订单最多产生 1000 PV (= 10 万元封顶)
9016
- // 溢出部分作"协议留存",等同基金池入金但不发对碰
7309
+ // 溢出部分作"协议留存",等同基金池入金(预留池,当前无消费方)
9017
7310
  const PV_PER_YUAN = 0.01;
9018
7311
  const MAX_PV_PER_ORDER = 1000;
9019
7312
  function calculatePv(amount, multiplier = 1.0) {
7313
+ // Category C: PV 是【参与记录】(非收益/非兑付)→ 默认 ON,只在 participation_recording 显式关闭时不生成。
7314
+ if (!participationRecordingActive(db))
7315
+ return 0;
9020
7316
  // 防御:负值 / NaN / Infinity 直接返回 0(不写入 pv_ledger)
9021
7317
  if (!Number.isFinite(amount) || amount <= 0)
9022
7318
  return 0;
@@ -9132,9 +7428,9 @@ function settleOrder(orderId) {
9132
7428
  }
9133
7429
  if (split.stakeToLockU > 0)
9134
7430
  applyWalletDelta(db, order.seller_id, { staked: split.stakeToLockU });
9135
- // 协议费拆分:50% 注入管理津贴池,50% 入 sys_protocol 运营
9136
- if (split.protocolToBonusU > 0)
9137
- creditColumns(db, 'management_bonus_pool', 'id = 1', [], { balance: split.protocolToBonusU });
7431
+ // 协议费拆分:50% 注入协议储备池,50% 入 sys_protocol 运营
7432
+ if (split.protocolToReserveU > 0)
7433
+ creditColumns(db, 'protocol_reserve_pool', 'id = 1', [], { balance: split.protocolToReserveU });
9138
7434
  if (split.protocolToOpsU > 0)
9139
7435
  applyWalletDelta(db, 'sys_protocol', { balance: split.protocolToOpsU });
9140
7436
  // 推土机分享分润:正常分账 → 钱包;兜底 → commission_reserve(三级公池,独立科目)
@@ -9529,7 +7825,7 @@ registerEvidenceRoutes(app, { db, auth, detectFraud });
9529
7825
  // #1013 Phase 1: 7 endpoint handlers 已迁出到 src/pwa/routes/webauthn.ts
9530
7826
  // helpers (consumeGateToken / requireHumanPresence) 仍在本文件,被 withdraw/arbitrate/vote 等引用
9531
7827
  registerWebauthnRoutes(app, {
9532
- db, auth, generateId,
7828
+ db, auth, generateId, rateLimitOk,
9533
7829
  rpId: WEBAUTHN_RP_ID,
9534
7830
  rpName: WEBAUTHN_RP_NAME,
9535
7831
  origin: WEBAUTHN_ORIGIN,
@@ -9695,7 +7991,7 @@ function settleGenericClaim(taskTable, voteTable, claimId) {
9695
7991
  return { ok: true, majority, ruling: majority };
9696
7992
  }
9697
7993
  // Sprint 4/5 — claim 结算后的后续影响(声誉 + 目标对象计数 + 自动下架 + voter outlier)
9698
- // 复用 line 13482-13485 已存在的 CLAIM_SUSPEND_THRESHOLD / CLAIM_REVOKE_THRESHOLD / CLAIM_SUSPEND_DAYS / CLAIM_OUTLIER_WINDOW_DAYS
7994
+ // #420 P1-3:voter outlier 的窗口/暂停/撤销阈值由 protocol_params 驱动(见 checkVerifierOutlier + anti-abuse-thresholds.ts)
9699
7995
  const CLAIM_AUTO_SUSPEND_THRESHOLD = 3; // 商品 / 拍卖 / 二手 累计 N 次 upheld → 自动下架
9700
7996
  // Wave A-5: 通用 claim 撤回 helper(只有 0 票时 claimant 可撤回,退 stake)
9701
7997
  function withdrawClaim(taskTable, voteTable, claimId, userId) {
@@ -9799,10 +8095,12 @@ function checkVerifierOutlier(verifierId) {
9799
8095
  const existing = db.prepare(`SELECT type FROM claim_verifier_suspensions WHERE user_id = ? AND type = 'revoked' LIMIT 1`).get(verifierId);
9800
8096
  if (existing)
9801
8097
  return;
9802
- // 统计 180d outlier 票数(跨所有 5 套 vote table)
8098
+ // #420 P1-3:窗口/阈值/暂停时长由 protocol_params 驱动(默认 = 原 180d/≥5/≥3/30d)
8099
+ const t = readAntiAbuseThresholds(db);
8100
+ // 统计窗口内 outlier 票数(跨所有 vote table)
9803
8101
  const VOTE_TABLES = ['claim_verification_votes', 'product_claim_votes', 'review_claim_votes', 'secondhand_claim_votes', 'auction_claim_votes', 'wish_claim_votes'];
9804
8102
  let outlierCount = 0;
9805
- const since = new Date(Date.now() - CLAIM_OUTLIER_WINDOW_DAYS * 86400_000).toISOString();
8103
+ const since = new Date(Date.now() - t.outlierWindowDays * 86400_000).toISOString();
9806
8104
  for (const tbl of VOTE_TABLES) {
9807
8105
  try {
9808
8106
  const n = db.prepare(`SELECT COUNT(*) as n FROM ${tbl} WHERE verifier_id = ? AND was_majority = 0 AND voted_at > ?`).get(verifierId, since).n;
@@ -9810,26 +8108,27 @@ function checkVerifierOutlier(verifierId) {
9810
8108
  }
9811
8109
  catch { }
9812
8110
  }
9813
- if (outlierCount >= CLAIM_REVOKE_THRESHOLD) {
8111
+ const band = verifierOutlierBand(outlierCount, t);
8112
+ if (band === 'revoke') {
9814
8113
  // 永久撤销
9815
8114
  db.prepare(`INSERT INTO claim_verifier_suspensions (id, user_id, type, until_at, reason, outlier_count) VALUES (?,?,?,NULL,?,?)`)
9816
- .run(generateId('cvs'), verifierId, 'revoked', `累计 ${outlierCount} 次 outlier(180d 内)→ 永久撤销 verifier 资格`, outlierCount);
8115
+ .run(generateId('cvs'), verifierId, 'revoked', `累计 ${outlierCount} 次 outlier(${t.outlierWindowDays}d 内)→ 永久撤销 verifier 资格`, outlierCount);
9817
8116
  try {
9818
8117
  db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
9819
- .run(generateId('ntf'), verifierId, '⚠ Verifier 资格已撤销', `你在 180 天内累计 ${outlierCount} 次 outlier 投票,按协议规则资格被永久撤销。`, null);
8118
+ .run(generateId('ntf'), verifierId, '⚠ Verifier 资格已撤销', `你在 ${t.outlierWindowDays} 天内累计 ${outlierCount} 次 outlier 投票,按协议规则资格被永久撤销。`, null);
9820
8119
  }
9821
8120
  catch { }
9822
8121
  }
9823
- else if (outlierCount >= CLAIM_SUSPEND_THRESHOLD) {
8122
+ else if (band === 'suspend') {
9824
8123
  // 临时 suspend,避免重复 suspend
9825
8124
  const dup = db.prepare(`SELECT id FROM claim_verifier_suspensions WHERE user_id = ? AND type = 'suspended' AND (until_at IS NULL OR until_at > datetime('now')) LIMIT 1`).get(verifierId);
9826
8125
  if (!dup) {
9827
- const until = new Date(Date.now() + CLAIM_SUSPEND_DAYS * 86400_000).toISOString();
8126
+ const until = new Date(Date.now() + t.outlierSuspendDays * 86400_000).toISOString();
9828
8127
  db.prepare(`INSERT INTO claim_verifier_suspensions (id, user_id, type, until_at, reason, outlier_count) VALUES (?,?,?,?,?,?)`)
9829
- .run(generateId('cvs'), verifierId, 'suspended', until, `累计 ${outlierCount} 次 outlier(180d 内)→ 暂停 ${CLAIM_SUSPEND_DAYS} 天`, outlierCount);
8128
+ .run(generateId('cvs'), verifierId, 'suspended', until, `累计 ${outlierCount} 次 outlier(${t.outlierWindowDays}d 内)→ 暂停 ${t.outlierSuspendDays} 天`, outlierCount);
9830
8129
  try {
9831
8130
  db.prepare(`INSERT INTO notifications (id, user_id, title, body, order_id) VALUES (?,?,?,?,?)`)
9832
- .run(generateId('ntf'), verifierId, '⏳ Verifier 资格已暂停', `你在 180 天内累计 ${outlierCount} 次 outlier 投票,资格暂停 ${CLAIM_SUSPEND_DAYS} 天直至 ${until.slice(0, 10)}。`, null);
8131
+ .run(generateId('ntf'), verifierId, '⏳ Verifier 资格已暂停', `你在 ${t.outlierWindowDays} 天内累计 ${outlierCount} 次 outlier 投票,资格暂停 ${t.outlierSuspendDays} 天直至 ${until.slice(0, 10)}。`, null);
9833
8132
  }
9834
8133
  catch { }
9835
8134
  }
@@ -9915,7 +8214,7 @@ registerPublicUtilsRoutes(app, {
9915
8214
  db, MASTER_SEED, NODE_ENV, SERVICE_START_MS,
9916
8215
  rateLimitOk, generateManifest, getUser, logError,
9917
8216
  // #1045 信任锚:Phase 4 同一把签名 key 的地址,公开发布让第三方验真者锚定
9918
- issuerAddress: () => privateKeyToAddress(derivePrivKey('platform-hot-wallet')),
8217
+ issuerAddress: () => walletSigner.issuerAddress(),
9919
8218
  });
9920
8219
  // ─── 静态文件 + SPA 回退(必须在所有 API 路由之后)────────────
9921
8220
  // PWA 壳文件必须 no-cache(否则 CF/浏览器 4h 缓存挡新版本);
@@ -10041,7 +8340,8 @@ function runEnforcement() {
10041
8340
  // 2. BASE_RPC_URL=https://mainnet.base.org(或自有 RPC)
10042
8341
  // 3. USDC_CONTRACT=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913(Base mainnet USDC)
10043
8342
  // 4. WALLET_MASTER_SEED=<高熵随机值,建议来自 KMS / 硬件签名器>
10044
- // 5. 强烈建议把 HOT_WALLET 切到多签(如 Gnosis Safe),把 derivePrivKey 替换为 KMS 调用
8343
+ // 5. 强烈建议把 HOT_WALLET 切到多签(如 Gnosis Safe)+ KMS 签名器 —— 经 WalletSigner seam
8344
+ // (internal/wallet-signer.ts) 换 LocalSeedSigner → KMS/Safe 实现,见 docs/HOT-WALLET-CUSTODY-MIGRATION.md
10045
8345
  // 6. NODE_ENV=production(启用默认 seed 拒启 + bootstrap key 脱敏)
10046
8346
  const NETWORK = (process.env.NETWORK || 'testnet').toLowerCase();
10047
8347
  const IS_MAINNET = NETWORK === 'mainnet';
@@ -10060,7 +8360,7 @@ if (IS_MAINNET && (MASTER_SEED === 'webaz-dev-seed-changeme' || !process.env.WAL
10060
8360
  }
10061
8361
  if (IS_MAINNET && !process.env.HOT_WALLET_KMS_ACK) {
10062
8362
  console.warn('⚠ 主网热钱包 HOT_WALLET 私钥仍由 MASTER_SEED 派生 — 强烈建议改 KMS / 多签');
10063
- console.warn(' 设 HOT_WALLET_KMS_ACK=1 表示你已知悉风险继续运行(生产应替换 derivePrivKey 调用)');
8363
+ console.warn(' 设 HOT_WALLET_KMS_ACK=1 表示你已知悉风险继续运行(生产应把 WalletSigner 换成 KMS/多签实现,见 docs/HOT-WALLET-CUSTODY-MIGRATION.md)');
10064
8364
  }
10065
8365
  const USDC_ABI = parseAbi([
10066
8366
  'function transfer(address to, uint256 value) returns (bool)',
@@ -10074,10 +8374,10 @@ const publicClient = createPublicClient({
10074
8374
  transport: http(rpcUrl),
10075
8375
  });
10076
8376
  // ─── 热钱包(归集 + 提现出账)────────────────────────────────────
10077
- const HOT_WALLET_PRIV = derivePrivKey('platform-hot-wallet');
10078
- const HOT_WALLET_ADDR = privateKeyToAddress(HOT_WALLET_PRIV);
8377
+ // Phase 0: hot-wallet signing via the WalletSigner seam (Phase 1 swaps LocalSeedSigner → KMS here).
8378
+ const HOT_WALLET_ADDR = walletSigner.hotAddress();
10079
8379
  const hotWalletClient = createWalletClient({
10080
- account: privateKeyToAccount(HOT_WALLET_PRIV),
8380
+ account: walletSigner.hotAccount(),
10081
8381
  chain: ACTIVE_CHAIN,
10082
8382
  transport: http(rpcUrl),
10083
8383
  });
@@ -10099,7 +8399,7 @@ async function sweepToHotWallet(userId, depositAddress) {
10099
8399
  await publicClient.waitForTransactionReceipt({ hash: ethHash });
10100
8400
  // 充值地址把 USDC 转给热钱包
10101
8401
  const depClient = createWalletClient({
10102
- account: privateKeyToAccount(derivePrivKey(userId)),
8402
+ account: walletSigner.depositAccount(userId),
10103
8403
  chain: ACTIVE_CHAIN,
10104
8404
  transport: http(rpcUrl),
10105
8405
  });
@@ -10277,22 +8577,7 @@ function startDepositWatcher() {
10277
8577
  // 轻量级自建错误上报 — 避免外部 Sentry 依赖
10278
8578
  // 后端:进程级 uncaughtException + unhandledRejection
10279
8579
  // 前端:POST /api/error-report(window.onerror → 入此表)
10280
- db.exec(`
10281
- CREATE TABLE IF NOT EXISTS error_log (
10282
- id INTEGER PRIMARY KEY AUTOINCREMENT,
10283
- source TEXT NOT NULL, -- 'server-uncaught' | 'server-rejection' | 'client'
10284
- message TEXT NOT NULL,
10285
- stack TEXT,
10286
- url TEXT, -- 客户端 location.href
10287
- user_agent TEXT, -- 客户端 UA
10288
- user_id TEXT, -- 已登录用户(可空)
10289
- created_at TEXT DEFAULT (datetime('now'))
10290
- )
10291
- `);
10292
- try {
10293
- db.exec('CREATE INDEX IF NOT EXISTS idx_error_log_created ON error_log(created_at)');
10294
- }
10295
- catch { }
8580
+ initErrorLogSchema(db);
10296
8581
  // ─── 治理岗位上岗(W3.5-B,2026-06-02)──────────────────────────
10297
8582
  // docs/GOVERNANCE-ONBOARDING.md — arbitrator + verifier 申请 / 上岗 / 卸任 / 申诉
10298
8583
  // 1 表:governance_applications(append-only,记录 apply/activate/resign/auto_deactivate/appeal)