@seasonkoh/webaz 0.1.29 → 0.1.30

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 (126) hide show
  1. package/dist/bond-refund-blockers.js +50 -0
  2. package/dist/bond-slash.js +100 -0
  3. package/dist/bond-terms.js +31 -0
  4. package/dist/direct-pay-bond-rail-clearance.js +10 -5
  5. package/dist/direct-pay-cancel-refund.js +160 -0
  6. package/dist/direct-pay-create.js +78 -9
  7. package/dist/direct-pay-disclosures.js +17 -11
  8. package/dist/direct-pay-fee-ar.js +2 -2
  9. package/dist/direct-pay-fee-prepay-request.js +80 -0
  10. package/dist/direct-pay-returns.js +104 -0
  11. package/dist/direct-pay-stock.js +9 -0
  12. package/dist/direct-receive-deferral.js +30 -1
  13. package/dist/direct-receive-deposits.js +122 -17
  14. package/dist/free-shipping.js +37 -0
  15. package/dist/fx-rates.js +7 -0
  16. package/dist/layer0-foundation/L0-1-database/schema.js +246 -0
  17. package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
  18. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +73 -0
  19. package/dist/layer1-agent/L1-1-mcp-server/server.js +81 -16
  20. package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
  21. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
  22. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
  23. package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
  24. package/dist/platform-receive-accounts.js +94 -0
  25. package/dist/pwa/arbitration-read-admin.js +37 -0
  26. package/dist/pwa/arbitrator-lifecycle.js +100 -0
  27. package/dist/pwa/contract-fingerprint.js +15 -0
  28. package/dist/pwa/direct-pay-order-redaction.js +20 -2
  29. package/dist/pwa/human-presence.js +2 -6
  30. package/dist/pwa/public/app-account.js +9 -9
  31. package/dist/pwa/public/app-admin-disputes.js +55 -0
  32. package/dist/pwa/public/app-agent-appeal.js +90 -0
  33. package/dist/pwa/public/app-agent-approvals.js +93 -0
  34. package/dist/pwa/public/app-agent-pair.js +127 -0
  35. package/dist/pwa/public/app-arbitrator-admin.js +87 -0
  36. package/dist/pwa/public/app-arbitrator-entry.js +9 -0
  37. package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
  38. package/dist/pwa/public/app-bond-refund-ui.js +66 -0
  39. package/dist/pwa/public/app-bond-slash-ui.js +74 -0
  40. package/dist/pwa/public/app-bond-terms-ui.js +23 -0
  41. package/dist/pwa/public/app-bond-ui.js +108 -0
  42. package/dist/pwa/public/app-chat-poll.js +6 -6
  43. package/dist/pwa/public/app-contribution-hub.js +23 -0
  44. package/dist/pwa/public/app-direct-pay-accounts.js +1 -1
  45. package/dist/pwa/public/app-direct-pay-buyer.js +1 -1
  46. package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
  47. package/dist/pwa/public/app-direct-pay-copy.js +12 -0
  48. package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
  49. package/dist/pwa/public/app-direct-pay-fee-ops.js +1 -1
  50. package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
  51. package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
  52. package/dist/pwa/public/app-direct-pay-memo.js +14 -0
  53. package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
  54. package/dist/pwa/public/app-direct-pay-pay.js +15 -0
  55. package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
  56. package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
  57. package/dist/pwa/public/app-direct-pay-returns.js +56 -0
  58. package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
  59. package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
  60. package/dist/pwa/public/app-direct-pay.js +36 -37
  61. package/dist/pwa/public/app-dispute-close-ui.js +38 -0
  62. package/dist/pwa/public/app-free-shipping-ui.js +29 -0
  63. package/dist/pwa/public/app-gmv-rail-split.js +12 -0
  64. package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
  65. package/dist/pwa/public/app-mutual-cancel.js +54 -0
  66. package/dist/pwa/public/app-notif-templates-orders.js +43 -0
  67. package/dist/pwa/public/app-notif-templates.js +22 -0
  68. package/dist/pwa/public/app-order-accept-ui.js +158 -0
  69. package/dist/pwa/public/app-order-errors.js +50 -0
  70. package/dist/pwa/public/app-order-labels.js +19 -0
  71. package/dist/pwa/public/app-order-rail-filter.js +26 -0
  72. package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
  73. package/dist/pwa/public/app-poll-governor.js +22 -0
  74. package/dist/pwa/public/app-profile.js +4 -4
  75. package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
  76. package/dist/pwa/public/app-sale-regions-ui.js +38 -0
  77. package/dist/pwa/public/app-seller.js +1 -1
  78. package/dist/pwa/public/app-trade-tax-ui.js +33 -0
  79. package/dist/pwa/public/app.js +133 -160
  80. package/dist/pwa/public/i18n.js +675 -5
  81. package/dist/pwa/public/index.html +41 -0
  82. package/dist/pwa/public/openapi.json +709 -11
  83. package/dist/pwa/public/style.css +3 -0
  84. package/dist/pwa/routes/admin-direct-receive-deposits.js +215 -3
  85. package/dist/pwa/routes/admin-protocol-params.js +16 -0
  86. package/dist/pwa/routes/admin-reports.js +31 -5
  87. package/dist/pwa/routes/agent-governance.js +1 -1
  88. package/dist/pwa/routes/agent-grants.js +253 -32
  89. package/dist/pwa/routes/analytics.js +2 -0
  90. package/dist/pwa/routes/arbitrator.js +67 -14
  91. package/dist/pwa/routes/bond-seller.js +162 -0
  92. package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
  93. package/dist/pwa/routes/direct-pay-disclosure-acks.js +9 -4
  94. package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
  95. package/dist/pwa/routes/direct-pay-returns.js +74 -0
  96. package/dist/pwa/routes/direct-pay-timeouts.js +186 -9
  97. package/dist/pwa/routes/disputes-read.js +20 -6
  98. package/dist/pwa/routes/disputes-write.js +91 -33
  99. package/dist/pwa/routes/external-anchors.js +4 -2
  100. package/dist/pwa/routes/fee-prepay-requests.js +66 -0
  101. package/dist/pwa/routes/governance-onboarding.js +46 -8
  102. package/dist/pwa/routes/logistics.js +4 -4
  103. package/dist/pwa/routes/me-data.js +2 -2
  104. package/dist/pwa/routes/mutual-cancel.js +62 -0
  105. package/dist/pwa/routes/orders-action.js +182 -9
  106. package/dist/pwa/routes/orders-create.js +18 -7
  107. package/dist/pwa/routes/orders-read.js +76 -14
  108. package/dist/pwa/routes/platform-receive-accounts.js +111 -0
  109. package/dist/pwa/routes/products-create.js +13 -4
  110. package/dist/pwa/routes/products-update.js +15 -1
  111. package/dist/pwa/routes/profile-identity.js +4 -2
  112. package/dist/pwa/routes/returns.js +39 -8
  113. package/dist/pwa/routes/seller-directpay-report.js +110 -0
  114. package/dist/pwa/routes/seller-quota.js +5 -1
  115. package/dist/pwa/routes/shipping-templates.js +219 -0
  116. package/dist/pwa/routes/snf.js +4 -1
  117. package/dist/pwa/routes/webauthn.js +3 -3
  118. package/dist/pwa/server.js +59 -36
  119. package/dist/runtime/agent-grant-scopes.js +69 -1
  120. package/dist/runtime/webaz-schema-helpers.js +57 -3
  121. package/dist/sale-regions.js +116 -0
  122. package/dist/shipping-templates.js +119 -0
  123. package/dist/trade-tax.js +99 -0
  124. package/dist/trade-terms.js +76 -0
  125. package/dist/version.js +1 -1
  126. package/package.json +59 -2
@@ -171,7 +171,7 @@ window.doRegisterPasskey = async (deviceLabel) => {
171
171
  window.requestPasskeyGate = async (purpose, purposeData) => {
172
172
  if (!isWebAuthnSupported()) throw new Error(t('当前设备不支持 Passkey'))
173
173
  const start = await POST('/webauthn/auth/start', { purpose, purpose_data: purposeData })
174
- if (start.error) throw new Error(start.error)
174
+ if (start.error) { const _e = new Error(start.error); _e.code = start.error_code; throw _e } // code=NO_PASSKEY_REGISTERED → 前端才引导注册;其余(取消/设备)不误导
175
175
  const opts = start.options
176
176
  const publicKey = {
177
177
  ...opts,
@@ -566,7 +566,7 @@ async function render(page, params) {
566
566
  const { status, body } = await apiWithStatus('GET', '/me')
567
567
  if (status === 200 && body && !body.error) {
568
568
  state.user = body
569
- connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800)
569
+ connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800); if (window.arbEntryHydrate) arbEntryHydrate()
570
570
  try { p2pStart() } catch (e) { console.warn('[P2P] start err', e) }
571
571
  } else if (status === 401 || status === 403) {
572
572
  // 真·鉴权失败:清掉两层存储
@@ -644,11 +644,11 @@ async function render(page, params) {
644
644
  return renderContributeTasks(app)
645
645
  case 'seller':
646
646
  if (state.user?.role === 'logistics') return renderLogistics(app)
647
- if (state.user?.role === 'arbitrator') return renderDisputeList(app)
647
+ if (state.canArbitrate) return renderDisputeList(app) // PR-E:仲裁入口跟随 can_arbitrate,非 role==='arbitrator'
648
648
  return renderSeller(app)
649
649
  case 'edit-product': return renderEditProduct(app, params[0])
650
650
  case 'wallet': return renderWallet(app)
651
- case 'agents': return renderConnectedAgents(app)
651
+ case 'agents': return renderConnectedAgents(app); case 'pair': return window.renderAgentPair(app); case 'agent-approvals': return window.renderAgentApprovals(app) // RFC-020 #pair 配对授权页 / #agent-approvals 扩权审批页
652
652
  case 'note': return renderNoteLanding(app, params[0])
653
653
  case 'tag': return renderTagPage(app, decodeURIComponent(params[0] || ''))
654
654
  case 'governance': return renderGovernance(app)
@@ -721,7 +721,7 @@ async function render(page, params) {
721
721
  if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
722
722
  if (params[0] === 'task-proposals') return renderAdminTaskProposals(app)
723
723
  if (params[0] === 'quota-requests') return renderAdminBuildTaskQuota(app)
724
- if (params[0] === 'operator-claims') return renderAdminOperatorClaims(app); if (params[0] === 'deferrals') return renderAdminDirectPayDeferrals(app); if (params[0] === 'product-verifications') return renderAdminProductVerifications(app); if (params[0] === 'store-verifications') return renderAdminStoreVerifications(app); if (params[0] === 'compliance') return renderAdminDirectReceiveCompliance(app); if (params[0] === 'dp-ops') return renderAdminDirectPayHub(app); if (params[0] === 'dp-fee') return renderAdminDirectPayFeeOps(app) // PR-②c 缓交 / PR-④b 逐品 / PR-⑤b 店铺 / PR-⑧ 合规录入 / PR-B 商户运营 hub + 平台服务费
724
+ if (params[0] === 'operator-claims') return renderAdminOperatorClaims(app); if (params[0] === 'deferrals') return renderAdminDirectPayDeferrals(app); if (params[0] === 'product-verifications') return renderAdminProductVerifications(app); if (params[0] === 'store-verifications') return renderAdminStoreVerifications(app); if (params[0] === 'compliance') return renderAdminDirectReceiveCompliance(app); if (params[0] === 'dp-ops') return renderAdminDirectPayHub(app); if (params[0] === 'dp-fee') return renderAdminDirectPayFeeOps(app); if (params[0] === 'platform-receive') return renderAdminPlatformReceiveAccounts(app); if (params[0] === 'fee-prepay-requests') return renderAdminFeePrepayRequests(app); if (params[0] === 'arbitrators') return renderAdminArbitrators(app); if (params[0] === 'contribution-ops') return renderAdminContributionHub(app); if (params[0] === 'bond-deposits') return renderAdminBondDeposits(app); if (params[0] === 'agent-strikes') return renderAdminAgentStrikes(app) // PR-②c 缓交 / PR-④b 逐品 / PR-⑤b 店铺 / PR-⑧ 合规录入 / PR-B 商户运营 hub + 平台服务费 + 平台收款方式 + 预充值申请审核 / PR-F 仲裁员管理
725
725
  if (params[0] === 'params') return renderAdminParams(app)
726
726
  if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
727
727
  if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
@@ -845,14 +845,14 @@ async function render(page, params) {
845
845
  // M7.4:order 含 has_pending_claim=1 时附加一枚"验证中"chip
846
846
  function orderStatusBadges(order) {
847
847
  if (!order) return ''
848
- const main = statusBadge(order.status)
848
+ const main = statusBadge(order.status, order.payment_rail)
849
849
  if (Number(order.has_pending_claim) === 1) {
850
850
  return `${main} <span style="display:inline-block;font-size:10px;background:#eef2ff;color:#3730a3;padding:2px 8px;border-radius:99px;font-weight:600;margin-left:4px">🔎 ${t('验证中')}</span>`
851
851
  }
852
852
  return main
853
853
  }
854
854
 
855
- function statusBadge(status) {
855
+ function statusBadge(status, rail) {
856
856
  const map = {
857
857
  created: ['gray', t('待付款')],
858
858
  paid: ['blue', t('待接单')],
@@ -875,7 +875,8 @@ function statusBadge(status) {
875
875
  dispute_dismissed: ['gray', t('争议驳回')],
876
876
  expired: ['gray', t('已过期')],
877
877
  }
878
- const [color, label] = map[status] || ['gray', status]
878
+ // 直付(非托管)争议终态:不发生退款/资金释放 用信誉裁决语义(dpTerminalBadge,见 app-order-labels.js)
879
+ const [color, label] = (rail === 'direct_p2p' && ((window.dpTerminalBadge && window.dpTerminalBadge(status)) || (window.dpNegotiationBadge && window.dpNegotiationBadge(status)) || (window.dpAcceptBadge && window.dpAcceptBadge(status)))) || map[status] || ['gray', status]
879
880
  return `<span class="badge badge-${color}">${label}</span>`
880
881
  }
881
882
 
@@ -1027,10 +1028,10 @@ function preLaunchBannerHTML() {
1027
1028
  // 没有 Passkey 登录 / 找回路径,丢 key 换设备后 Passkey 救不回账号(与弹窗"增强不替代恢复邮箱"一致)。
1028
1029
  function recoveryBannerHTML() {
1029
1030
  const u = state.user
1030
- if (!u || u.role === 'admin') return ''
1031
- const hasRecovery = u.has_password || u.email_verified
1032
- if (hasRecovery) return ''
1033
- return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:8px;padding:10px 14px;font-size:12px;color:#991b1b;line-height:1.6;margin:8px 12px 12px">
1031
+ if (!u || u.role === 'admin' || u.has_password || u.email_verified) return ''
1032
+ if (!location.hash.startsWith('#me') && Date.now() < +(localStorage.getItem('webaz_recovery_dismiss_until') || 0)) return '' // 走查批次2·横幅降噪:可关闭 7 天冷却;「我的」域恒显(设置入口所在,安全提醒不静音到死角)
1033
+ return `<div data-rcv style="background:#fef2f2;border:1px solid #fca5a5;border-radius:8px;padding:10px 14px;font-size:12px;color:#991b1b;line-height:1.6;margin:8px 12px 12px;position:relative">
1034
+ <button title="${t('暂时隐藏(7 天)')}" onclick="localStorage.setItem('webaz_recovery_dismiss_until', String(Date.now() + 7 * 86400e3)); this.closest('[data-rcv]').remove()" style="position:absolute;top:6px;right:8px;background:none;border:none;cursor:pointer;color:#b91c1c;font-size:14px;padding:2px 6px">×</button>
1034
1035
  🚨 <strong>${t('账户还没有恢复方式')}</strong> — ${t('闪退或换设备清缓存后可能永久无法登录。')}
1035
1036
  <div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
1036
1037
  <button class="btn btn-sm" style="font-size:11px;padding:4px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer" onclick="navigate('#me/settings')">${t('立即设置密码 / 绑定邮箱')} →</button>
@@ -2645,7 +2646,7 @@ async function renderAdminOrders(app, opts = {}) {
2645
2646
  <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
2646
2647
  <div style="flex:1;min-width:0">
2647
2648
  <div style="font-weight:600">${escHtml(o.product_title)}</div>
2648
- <div style="font-size:12px;color:#6b7280;margin-top:2px">${o.total_amount} WAZ · ${fmtTime(o.created_at)}</div>
2649
+ <div style="font-size:12px;color:#6b7280;margin-top:2px">${o.payment_rail === 'direct_p2p' ? `<span style="background:#fef3c7;color:#92400e;border-radius:99px;padding:1px 8px;font-size:10px;font-weight:600;margin-right:4px">${t('直付')}</span>${o.total_amount} USDC` : `${o.total_amount} WAZ`} · ${fmtTime(o.created_at)}</div>
2649
2650
  <div style="font-size:11px;color:#6b7280;margin-top:4px">${t('买家')}: ${escHtml(o.buyer_name)} · ${t('卖家')}: ${escHtml(o.seller_name)}</div>
2650
2651
  <div style="font-size:11px;color:#9ca3af;margin-top:2px">${o.id}</div>
2651
2652
  </div>
@@ -2671,34 +2672,7 @@ window.doAdminOrderFilter = () => {
2671
2672
  renderAdminOrders(document.getElementById('app'), { status })
2672
2673
  }
2673
2674
 
2674
- async function renderAdminDisputes(app) {
2675
- if (!state.user) { renderLogin(); return }
2676
- if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
2677
- app.innerHTML = shell(loading$(), 'admin')
2678
- const data = await GET('/admin/disputes')
2679
- if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
2680
- const list = data.disputes.map(d => `
2681
- <div class="card" style="margin-bottom:10px;font-size:13px">
2682
- <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
2683
- <div style="flex:1;min-width:0">
2684
- <div style="font-weight:600">${escHtml(d.product_title || '—')}</div>
2685
- <div style="font-size:12px;color:#6b7280;margin-top:2px">${escHtml((d.reason || '').slice(0, 80))}</div>
2686
- <div style="font-size:11px;color:#6b7280;margin-top:6px">${t('原告')}: ${escHtml(d.initiator_name || '—')} → ${t('被告')}: ${escHtml(d.defendant_name || '—')}</div>
2687
- <div style="font-size:11px;color:#9ca3af;margin-top:4px">${d.id} · ${fmtTime(d.created_at)}</div>
2688
- </div>
2689
- <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
2690
- <span style="background:#fef3c7;color:#92400e;padding:2px 8px;border-radius:8px;font-size:11px">${d.status}</span>
2691
- ${d.total_amount ? `<div style="font-size:12px;color:#374151">${d.total_amount} WAZ</div>` : ''}
2692
- </div>
2693
- </div>
2694
- </div>`).join('') || `<div class="empty"><div class="empty-icon">⚖️</div><div class="empty-text">${t('暂无争议')}</div></div>`
2695
- app.innerHTML = shell(`
2696
- <h1 class="page-title">⚖️ ${t('争议查看')}</h1>
2697
- <div style="margin-bottom:16px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
2698
- <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('共')} ${data.disputes.length} ${t('条结果')}(${t('含已结案')})</div>
2699
- ${list}
2700
- `, 'admin')
2701
- }
2675
+ // renderAdminDisputes 已抽出到 app-admin-disputes.js(window.renderAdminDisputes;可区分状态/直付-托管/进度/指派/结案 + 可管理:过滤+钻取)
2702
2676
 
2703
2677
  async function renderAdminTasks(app, opts = {}) {
2704
2678
  if (!state.user) { renderLogin(); return }
@@ -8115,8 +8089,8 @@ async function renderReturnsCenter(app) {
8115
8089
  ])
8116
8090
  const items = list?.items || []
8117
8091
  const reasons = RETURN_REASON_LABEL()
8118
- const STATUS_COLORS = { pending:'#d97706', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', accepted_pickup_pending:'#0891b2', picked_up:'#4f46e5' }
8119
- const STATUS_LABEL = () => ({ pending: t('待处理'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), accepted_pickup_pending: '📦 ' + t('待物流上门取件'), picked_up: '🚛 ' + t('已揽收,待卖家确认收到') })
8092
+ const STATUS_COLORS = { pending:'#d97706', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', accepted_pickup_pending:'#0891b2', picked_up:'#4f46e5', ...(window.dpReturnStatusColors ? window.dpReturnStatusColors() : {}) }
8093
+ const STATUS_LABEL = () => ({ pending: t('待处理'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), accepted_pickup_pending: '📦 ' + t('待物流上门取件'), picked_up: '🚛 ' + t('已揽收,待卖家确认收到'), ...(window.dpReturnStatusLabels ? window.dpReturnStatusLabels() : {}) })
8120
8094
 
8121
8095
  const statsHtml = stats ? `
8122
8096
  <div class="card" style="background:linear-gradient(135deg,#fff7ed,#fef3c7);margin-bottom:14px">
@@ -8982,7 +8956,7 @@ window.doLogin = async () => {
8982
8956
  if (!key) return showMsg('error', t('请粘贴 api_key'))
8983
8957
  state.apiKey = key
8984
8958
  const user = await GET('/me')
8985
- if (user.error) { state.apiKey = null; return showMsg('error', t('无效的 api_key,请重新输入')) }
8959
+ if (user.error) { if (user.error_code === 'AGENT_BLOCKED' && window.renderAgentBlockedAppeal) { try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}; return void window.renderAgentBlockedAppeal(user.error) } state.apiKey = null; return showMsg('error', t('无效的 api_key,请重新输入')) }
8986
8960
  state.user = user
8987
8961
  await persistApiKey(key)
8988
8962
  connectSSE()
@@ -10463,7 +10437,7 @@ window.openBuySheet = function(productId) {
10463
10437
  </div>
10464
10438
  </details>
10465
10439
 
10466
- ${window.dpRailSelectorHtml ? window.dpRailSelectorHtml(prod.id, prod.price) : ''}
10440
+ ${window.dpRailSelectorHtml ? window.dpRailSelectorHtml(prod.id, prod.price) : ''}${window.shipRegionBlockHtml ? window.shipRegionBlockHtml(prod.id) : ''}${window.purchaseTermsBlockHtml ? window.purchaseTermsBlockHtml(prod.id) : ''}
10467
10441
  <div id="buy-msg" style="margin-top:10px"></div>
10468
10442
  <div id="tax-preview-slot" style="margin-top:8px"></div>
10469
10443
 
@@ -11311,7 +11285,7 @@ window.doBuy = async (productId, price) => {
11311
11285
  const qtyInp = document.getElementById('inp-qty')
11312
11286
  const quantity = qtyInp ? Math.max(1, Math.min(Number(qtyInp.max) || 1, Math.floor(Number(qtyInp.value) || 1))) : 1
11313
11287
  const payment_rail = window.dpSelectedRail ? window.dpSelectedRail() : 'escrow'; if (!payment_rail) { const _dp=document.querySelector('input[name="dp-rail"]:checked')?.value==='direct_p2p'; const _m=document.getElementById('buy-msg'); if(_m) _m.innerHTML = alert$('error', _dp ? t('直付暂未就绪或不可用,请稍候重试或改选支付方式') : t('请选择支付方式')); const _b=document.getElementById('dp-rail-block'); if(_b) _b.open=true; return } if (window.closeSheet) window.closeSheet() // #28 空 rail 一律拦(永不静默落 escrow);[PRELAUNCH-WAZ-SIM] 未选→空;校验通过才关 sheet(块内 return 不关→提示+展开可见,修 P2)
11314
- const res = await POST('/orders', { product_id: productId, shipping_address: addr, notes, sponsor_hint, coupon_code, delivery_window, variant_id, expected_price, buy_insurance, anonymous_recipient, donation_pct, quantity, payment_rail, direct_receive_account_id: (payment_rail === 'direct_p2p' && window.dpSelectedAccountId) ? (window.dpSelectedAccountId() || undefined) : undefined, ...giftPayload })
11288
+ const res = await POST('/orders', { product_id: productId, shipping_address: addr, notes, sponsor_hint, coupon_code, delivery_window, variant_id, expected_price, buy_insurance, anonymous_recipient, donation_pct, quantity, payment_rail, ship_to_region: (window.shipSelectedRegion ? window.shipSelectedRegion() : undefined), direct_receive_account_id: (payment_rail === 'direct_p2p' && window.dpSelectedAccountId) ? (window.dpSelectedAccountId() || undefined) : undefined, ...giftPayload })
11315
11289
  if (payment_rail === 'direct_p2p') return void (window.dpAfterCreate && window.dpAfterCreate(res))
11316
11290
  if (res.error_code === 'PRICE_CHANGED') {
11317
11291
  document.getElementById('buy-msg').innerHTML = alert$('error', `${t('价格已变动')}:${window.fmtPrice(res.old_price)} → ${window.fmtPrice(res.new_price)} · ${t('请刷新页面')}`)
@@ -11709,6 +11683,7 @@ async function renderOrders(app) {
11709
11683
  const ordersScope = state.ordersScope || (state.user.role === 'seller' ? 'sold' : 'all')
11710
11684
  if (ordersScope === 'sold') orders = orders.filter(o => o.seller_id === state.user.id)
11711
11685
  else if (ordersScope === 'bought') orders = orders.filter(o => o.buyer_id === state.user.id)
11686
+ const railChipsHtml = window.orderRailChipsHtml(orders); orders = window.orderRailApply(orders) // 类型(支付轨)筛选 chip + 过滤;逻辑在 app-order-rail-filter.js
11712
11687
  const groups = {
11713
11688
  all: { label: t('全部'), icon: '📋', match: () => true },
11714
11689
  pay: { label: t('待付款'), icon: '💳', match: o => o.status === 'created' },
@@ -11778,18 +11753,18 @@ async function renderOrders(app) {
11778
11753
  <div class="order-icon">${pending ? '⏰' : getCategoryIcon(o.category)}</div>
11779
11754
  <div class="order-info">
11780
11755
  <div class="order-title">${escHtml(o.product_title)}</div>
11781
- <div class="order-meta">${fmtTime(o.created_at)} · ${o.buyer_id === state.user.id ? t('我买的') : t('我卖的')}</div>
11756
+ <div class="order-meta">${fmtTime(o.created_at)} · ${o.buyer_id === state.user.id ? t('我买的') : t('我卖的')}${window.orderRailBadge(o)}</div>
11782
11757
  <div style="margin-top:6px">${orderStatusBadges(o)}</div>
11783
11758
  </div>
11784
- <div class="order-amount">${o.total_amount}</div>
11759
+ <div class="order-amount">${window.orderAmountHtml(o)}</div>
11785
11760
  </div>
11786
11761
  ${canRebuy ? `<div style="border-top:1px solid #f3f4f6;padding:8px 14px;display:flex;justify-content:flex-end"><button onclick="event.stopPropagation();navigate('#order-product/${o.product_id}')" style="background:#eef2ff;color:#3730a3;border:1px solid #c7d2fe;padding:4px 12px;border-radius:99px;font-size:11px;font-weight:600;cursor:pointer">🔁 ${t('再次购买')}</button></div>` : ''}
11787
11762
  </div>`
11788
11763
  }).join('')
11789
11764
 
11790
11765
  const exportRole = state.user?.role === 'seller' ? 'seller' : 'buyer'
11791
- const exportBtn = `<button class="btn btn-outline btn-sm" style="float:right;font-size:11px;padding:4px 10px" onclick="exportOrders('${exportRole}')">📥 ${t('导出 CSV')}</button>`
11792
- app.innerHTML = shell(`<h1 class="page-title">${t('我的订单')} ${exportBtn}</h1>${mainTabsHtml}${scopeChipsHtml}${notePromptPlaceholder('orders')}${tabsHtml}${pendingBanner}${list}`, 'orders')
11766
+ const exportBtn = `<button class="btn btn-outline btn-sm" style="float:right;font-size:11px;padding:4px 10px" onclick="exportOrders('${exportRole}','${state.ordersRail || 'all'}')">📥 ${t('导出 CSV')}</button>`
11767
+ app.innerHTML = shell(`<h1 class="page-title">${t('我的订单')} ${exportBtn}</h1>${mainTabsHtml}${scopeChipsHtml}${railChipsHtml}${notePromptPlaceholder('orders')}${tabsHtml}${pendingBanner}${list}`, 'orders')
11793
11768
  hydrateNotePrompt('orders')
11794
11769
  }
11795
11770
 
@@ -11821,9 +11796,9 @@ window.printOrderReceipt = async (orderId) => {
11821
11796
  <div class="row"><span>${t('商品')}</span><strong>${escHtml(product?.title || '—')}</strong></div>
11822
11797
  ${variantStr ? `<div class="row"><span>${t('规格')}</span><span>${escHtml(variantStr)}</span></div>` : ''}
11823
11798
  <div class="row"><span>${t('数量')}</span><span>${order.quantity}</span></div>
11824
- <div class="row"><span>${t('单价')}</span><span>${order.unit_price} WAZ</span></div>
11825
- ${Number(order.coupon_discount) > 0 ? `<div class="row"><span>${t('优惠')}</span><span style="color:#dc2626">-${order.coupon_discount} WAZ</span></div>` : ''}
11826
- <div class="total">${t('总计')}: ${order.total_amount} WAZ</div>
11799
+ <div class="row"><span>${t('单价')}</span><span>${order.unit_price} USDC</span></div>
11800
+ ${Number(order.coupon_discount) > 0 ? `<div class="row"><span>${t('优惠')}</span><span style="color:#dc2626">-${order.coupon_discount} USDC</span></div>` : ''}
11801
+ <div class="total">${t('总计')}: ${order.total_amount} USDC</div>
11827
11802
  <hr style="margin:14px 0;border:none;border-top:1px solid #ddd">
11828
11803
  <div class="muted">${t('收货地址')}: ${escHtml(order.shipping_address || '—')}</div>
11829
11804
  <div class="muted">${t('订单状态')}: ${order.status}</div>
@@ -11832,12 +11807,10 @@ window.printOrderReceipt = async (orderId) => {
11832
11807
  w.document.close()
11833
11808
  }
11834
11809
 
11835
- window.exportOrders = (role) => {
11836
- // 直接打开端点 浏览器下载(Authorization 头通过 token query 传递)
11837
- const url = `/api/orders/export?role=${role}&token=${encodeURIComponent(state.apiKey)}`
11838
- // 浏览器导航触发下载(Content-Disposition: attachment)
11839
- // 由于 auth() 当前只读 Authorization 头,导出端点需支持 query token;改用 fetch + blob 下载更稳
11840
- fetch(`/api/orders/export?role=${role}`, { headers: { Authorization: 'Bearer ' + state.apiKey } })
11810
+ window.exportOrders = (role, rail) => {
11811
+ const railQ = (rail === 'escrow' || rail === 'direct_p2p') ? `&rail=${rail}` : '' // 导出与当前筛选一致(对账分轨)
11812
+ // auth() 只读 Authorization 头 → 用 fetch + blob 下载(Content-Disposition: attachment)
11813
+ fetch(`/api/orders/export?role=${role}${railQ}`, { headers: { Authorization: 'Bearer ' + state.apiKey } })
11841
11814
  .then(async (r) => {
11842
11815
  const truncated = r.headers.get('X-Truncated') === '1'
11843
11816
  const limit = r.headers.get('X-Truncated-Limit')
@@ -11937,7 +11910,7 @@ async function renderOrdersShare(app, mainTabsHtml) {
11937
11910
  <div style="font-size:28px;flex-shrink:0">${getCategoryIcon(o.category)}</div>
11938
11911
  <div style="flex:1;min-width:0">
11939
11912
  <div style="font-size:13px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(o.product_title)}</div>
11940
- <div style="font-size:11px;color:#6b7280;margin-top:2px"><a href="#order/${o.id}" style="color:#6b7280;text-decoration:none">${t('订单')} ${orderShort}</a> · ${fmtTime(o.updated_at || o.created_at)}${typeof o.total_amount !== 'undefined' ? ` · ${o.total_amount} WAZ` : ''}</div>
11913
+ <div style="font-size:11px;color:#6b7280;margin-top:2px"><a href="#order/${o.id}" style="color:#6b7280;text-decoration:none">${t('订单')} ${orderShort}</a> · ${fmtTime(o.updated_at || o.created_at)}${typeof o.total_amount !== 'undefined' ? ` · ${window.orderAmountHtml(o)}` : ''}</div>
11941
11914
  </div>
11942
11915
  <div style="display:flex;gap:4px;flex-shrink:0">
11943
11916
  <button class="btn btn-outline btn-sm" style="width:auto;padding:5px 8px;font-size:10px;white-space:nowrap" onclick="shareCompletedOrder('${o.product_id}', '${escHtml(o.product_title || '').replace(/'/g, "\\'")}')">🔗 ${t('添加口令')}</button>
@@ -12181,10 +12154,10 @@ function orderStageTimeline(order, history) {
12181
12154
  { key: 'completed', label: t('完成'), icon: '✓' },
12182
12155
  ]
12183
12156
  // 异常状态:单独 banner,不画时间线(防误导)
12184
- const ANOMALY = ['disputed', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12157
+ const ANOMALY = ['disputed', 'payment_query', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12185
12158
  if (ANOMALY.includes(order.status)) {
12186
12159
  const colorMap = {
12187
- disputed: '#f59e0b', cancelled: '#6b7280',
12160
+ disputed: '#f59e0b', payment_query: '#f59e0b', cancelled: '#6b7280',
12188
12161
  fault_seller: '#dc2626', fault_buyer: '#dc2626', fault_logistics: '#dc2626',
12189
12162
  refunded_full: '#16a34a', refunded_partial: '#3b82f6', dispute_dismissed: '#6b7280', expired: '#9ca3af',
12190
12163
  }
@@ -12198,7 +12171,7 @@ function orderStageTimeline(order, history) {
12198
12171
  return `<div style="background:#fff;border:0.5px solid #e5e7eb;border-radius:12px;padding:14px 16px;margin-bottom:10px;display:flex;align-items:center;gap:10px">
12199
12172
  <div style="width:8px;height:8px;border-radius:50%;background:${c};flex-shrink:0"></div>
12200
12173
  <div style="flex:1">
12201
- <div style="font-size:14px;font-weight:600;color:#1f2937">${labelMap[order.status] || order.status}</div>
12174
+ <div style="font-size:14px;font-weight:600;color:#1f2937">${(order.payment_rail === 'direct_p2p' && ((window.dpTerminalLabel && window.dpTerminalLabel(order.status)) || (window.dpNegotiationLabel && window.dpNegotiationLabel(order.status)) || (window.dpAcceptLabel && window.dpAcceptLabel(order.status)))) || labelMap[order.status] || order.status}</div>
12202
12175
  <div style="font-size:11px;color:#8e8e93;margin-top:2px">${t('查看下方时间线了解流转详情')}</div>
12203
12176
  </div>
12204
12177
  </div>`
@@ -12218,7 +12191,7 @@ function orderStageTimeline(order, history) {
12218
12191
  // "当前" = 正在等待的下一阶段(不是已完成的最后阶段)
12219
12192
  // created → 等付款;paid → 等接单;accepted → 等发货;shipped/in_transit → 等送达;
12220
12193
  // delivered → 等买家确认;confirmed/completed → 全部完成
12221
- const statusToIdx = { created: 1, paid: 2, accepted: 3, shipped: 4, picked_up: 4, in_transit: 4, delivered: 5, confirmed: 6, completed: 6 }
12194
+ const statusToIdx = { created: 1, direct_pay_window: 1, direct_expired_unconfirmed: 1, paid: 2, accepted: 3, shipped: 4, picked_up: 4, in_transit: 4, delivered: 5, confirmed: 6, completed: 6 }
12222
12195
  let currentIdx = statusToIdx[order.status] ?? 0
12223
12196
 
12224
12197
  const dot = (s, i) => {
@@ -12252,7 +12225,7 @@ function orderStageTimeline(order, history) {
12252
12225
  // 与顶部紧凑 stepper 互补:stepper 给总览,timeline 给细节
12253
12226
  function orderTrackingTimeline(order, history, trackingInfo, STATUS_ZH) {
12254
12227
  // 异常订单不显示物流时间线(争议/取消等)— 由顶部 stepper 异常 banner 兜底
12255
- const ANOMALY = ['disputed', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12228
+ const ANOMALY = ['disputed', 'payment_query', 'cancelled', 'fault_seller', 'fault_buyer', 'fault_logistics', 'refunded_partial', 'refunded_full', 'dispute_dismissed', 'expired']
12256
12229
  if (ANOMALY.includes(order.status)) return ''
12257
12230
 
12258
12231
  // 已完成节点 lookup
@@ -12382,14 +12355,18 @@ function nextActionCard(order, isBuyer, isSeller, activeDeadline, isOverdue) {
12382
12355
  // 我自己是违约方时只看到警告,不显示按钮(不能自己触发对自己的判责)
12383
12356
  const meAtFault = hint.overdueFault && hint.overdueFault === ('fault_' + (isBuyer ? 'buyer' : isSeller ? 'seller' : ''))
12384
12357
  const canForceTrigger = isOverdue && hint.overdueFault && !meAtFault && (isBuyer || isSeller)
12358
+ // 直付(非托管):WebAZ 不退款/不放款 → 超时后果只写信誉责任,绝不写"退款/资金释放"
12359
+ const overdueConsequence = order.payment_rail === 'direct_p2p'
12360
+ ? ({ paid: '卖家信誉扣分;直付非托管,WebAZ 不退款/不放款', accepted: '卖家信誉扣分;直付非托管,WebAZ 不退款/不放款', in_transit: '物流/履约信誉扣分;直付非托管,WebAZ 不退款', delivered: '系统自动确认直付订单完成;不涉及平台放款' }[order.status] || hint.overdueConsequence)
12361
+ : hint.overdueConsequence
12385
12362
  return `<div style="background:${bg};border:0.5px solid ${isOverdue ? '#fecaca' : '#e5e7eb'};border-radius:12px;padding:14px 16px;margin-bottom:10px">
12386
12363
  <div style="display:flex;align-items:center;gap:10px">
12387
12364
  <div style="font-size:22px;line-height:1">${icon}</div>
12388
12365
  <div style="flex:1">
12389
12366
  <div style="font-size:14px;font-weight:600;color:${tone}">${t(myHint)}</div>
12390
12367
  ${activeDeadline?.deadline ? `<div style="font-size:11px;color:#8e8e93;margin-top:2px">${isOverdue ? t('已超时,协议将自动判责') : fmtCountdown(activeDeadline.deadline)}</div>` : ''}
12391
- ${isOverdue && hint.overdueConsequence ? `<div style="margin-top:8px;padding:8px 10px;background:#fff;border-radius:6px;font-size:12px;color:#7c2d12;border:1px dashed #fecaca">
12392
- ⚖️ ${t('协议处置')}:${t(hint.overdueConsequence)}
12368
+ ${isOverdue && overdueConsequence ? `<div style="margin-top:8px;padding:8px 10px;background:#fff;border-radius:6px;font-size:12px;color:#7c2d12;border:1px dashed #fecaca">
12369
+ ⚖️ ${t('协议处置')}:${t(overdueConsequence)}
12393
12370
  </div>` : ''}
12394
12371
  ${canForceTrigger ? `<button onclick="forceTimeoutCheck('${order.id}')" style="margin-top:10px;background:#dc2626;color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:13px;font-weight:600;cursor:pointer">⚡ ${t('立即触发判责')}</button>
12395
12372
  <div style="font-size:10px;color:#9ca3af;margin-top:6px">${t('协议巡检每 5 分钟一次;你也可立即触发,无需等待。')}</div>` : ''}
@@ -12450,10 +12427,10 @@ async function renderOrderDetail(app, orderId) {
12450
12427
  const historyHtml = (history || []).map(h => `
12451
12428
  <div class="timeline-item">
12452
12429
  <div><span class="timeline-status">${t(STATUS_ZH[h.from_status] || h.from_status)} → ${t(STATUS_ZH[h.to_status] || h.to_status)}</span></div>
12453
- <div class="timeline-actor">${h.actor_name}(${h.actor_role_name || h.actor_role})</div>
12430
+ <div class="timeline-actor">${escHtml(h.actor_name)}(${escHtml(h.actor_role_name || h.actor_role)})</div>
12454
12431
  <div class="timeline-time">${fmtTime(h.created_at)}</div>
12455
- ${h.notes ? `<div class="timeline-evidence" style="color:#6b7280">💬 ${h.notes}</div>` : ''}
12456
- ${(h.evidence_items || []).map(e => `<div class="timeline-evidence">📎 ${e.description}</div>`).join('')}
12432
+ ${h.notes ? `<div class="timeline-evidence" style="color:#6b7280">💬 ${escHtml(h.notes)}</div>` : ''}
12433
+ ${(h.evidence_items || []).map(e => `<div class="timeline-evidence">📎 ${escHtml(e.description)}</div>`).join('')}
12457
12434
  </div>`).join('')
12458
12435
 
12459
12436
  // 物流追踪完整时间轴 — 含订单全流转 + 未到达节点的截止提示
@@ -12503,17 +12480,17 @@ async function renderOrderDetail(app, orderId) {
12503
12480
  ${nextActionCard(order, isBuyer, isSeller, activeDeadline, isOverdue)}
12504
12481
 
12505
12482
  <div class="card">
12506
- <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
12507
- <div style="font-size:13px;color:#6b7280;font-family:monospace">${order.id}</div>
12508
- <div style="display:flex;gap:6px;align-items:center">
12509
- ${(isBuyer || isSeller) ? `<button class="btn btn-sm" style="background:#eef2ff;color:#4338ca;font-size:11px;padding:4px 10px" onclick="openChatForContext('order','${order.id}')">💬 ${t('联系对方')}</button>` : ''}
12483
+ <div style="display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px;margin-bottom:12px">
12484
+ <div style="font-size:12px;color:#9ca3af;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 120px;min-width:0" title="${escHtml(order.id)}">${order.id}</div>
12485
+ <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;flex-shrink:0;white-space:nowrap">
12486
+ ${(isBuyer || isSeller) ? `<button class="btn btn-sm" style="background:#eef2ff;color:#4338ca;font-size:11px;padding:4px 10px;white-space:nowrap" onclick="openChatForContext('order','${order.id}')">💬 ${t('联系对方')}</button>` : ''}
12510
12487
  ${orderStatusBadges(order)}
12511
12488
  </div>
12512
12489
  </div>
12513
- <div class="detail-row"><span class="detail-label">${t('商品')}</span><span class="detail-value">${product?.title || ''}</span></div>
12514
- <div class="detail-row"><span class="detail-label">${t('金额')}</span><span class="detail-value" style="color:#4f46e5">${order.total_amount} WAZ${usdHint(order.total_amount)}${Number(order.insurance_premium) > 0 ? ` <span style="font-size:10px;color:#6366f1;background:#eef2ff;padding:1px 6px;border-radius:99px;margin-left:4px">🛡 ${t('已购保险')} +${order.insurance_premium}</span>` : ''}</span></div>
12490
+ <div class="detail-row" style="align-items:flex-start"><span class="detail-label" style="flex-shrink:0;margin-right:12px">${t('商品')}</span><span class="detail-value" style="text-align:left;max-width:78%;word-break:break-word;line-height:1.5">${escHtml(product?.title || '')}</span></div>
12491
+ <div class="detail-row"><span class="detail-label">${t('金额')}</span><span class="detail-value" style="color:#4f46e5">${window.orderAmountHtml(order)}${Number(order.insurance_premium) > 0 ? ` <span style="font-size:10px;color:#6366f1;background:#eef2ff;padding:1px 6px;border-radius:99px;margin-left:4px">🛡 ${t('已购保险')} +${order.insurance_premium}</span>` : ''}</span></div>
12515
12492
  <div class="detail-row"><span class="detail-label">${t('下单时间')}</span><span class="detail-value">${fmtTime(order.created_at)}</span></div>
12516
- ${order.shipping_address ? `<div class="detail-row"><span class="detail-label">${t('收货地址')}</span><span class="detail-value">${order.shipping_address}</span></div>` : ''}
12493
+ ${order.shipping_address ? `<div class="detail-row"><span class="detail-label">${t('收货地址')}</span><span class="detail-value">${escHtml(order.shipping_address)}</span></div>` : ''}
12517
12494
  ${order.anonymous_recipient && order.recipient_code ? `<div class="detail-row"><span class="detail-label">🔒 ${t('取件代号')}</span><span class="detail-value" style="font-family:monospace;font-size:14px;font-weight:700;color:#166534;background:#dcfce7;padding:2px 10px;border-radius:6px;letter-spacing:1px">${escHtml(order.recipient_code)}</span></div><div style="font-size:11px;color:#15803d;padding:0 14px 8px">${t('凭此代号到自提点取件 · 卖家和物流不知道你的真实姓名')}</div>` : ''}
12518
12495
  ${order.anonymous_recipient && !order.recipient_code ? `<div class="detail-row"><span class="detail-label">🔒 ${t('匿名订单')}</span><span class="detail-value" style="font-size:11px;color:#6b7280">${t('买家选择了匿名收货')}</span></div>` : ''}
12519
12496
  ${Number(order.donation_amount) > 0 ? `<div class="detail-row"><span class="detail-label">❤️ ${t('随单捐赠')}</span><span class="detail-value" style="color:#dc2626;font-weight:600">${order.donation_amount} WAZ</span></div>` : ''}
@@ -12523,7 +12500,7 @@ async function renderOrderDetail(app, orderId) {
12523
12500
  ${order.content_hash_at_order ? `<div class="detail-row"><span class="detail-label">🔒 ${t('P2P 内容哈希')}</span><span class="detail-value" style="font-family:monospace;font-size:11px;word-break:break-all">${escHtml(order.content_hash_at_order)}</span></div>` : ''}
12524
12501
  ${activeDeadline?.deadline ? `<div class="detail-row"><span class="detail-label">${t('截止')}</span><span class="detail-value" style="color:${isOverdue ? '#dc2626' : '#6b7280'};font-size:12px">${fmtTime(activeDeadline.deadline)}</span></div>` : ''}
12525
12502
  </div>
12526
- ${order.payment_rail === 'direct_p2p' && window.dpOrderDisclosureHtml ? window.dpOrderDisclosureHtml(order) : ''}
12503
+ ${order.payment_rail === 'direct_p2p' && isBuyer && window.dpOrderDisclosureHtml ? window.dpOrderDisclosureHtml(order) : ''}${window.dpReconcileCard ? window.dpReconcileCard(order, isSeller) : ''}${order.payment_rail === 'direct_p2p' && window.dpNegotiationCard ? window.dpNegotiationCard(order) : ''}${window.dpCancelRefundCard ? window.dpCancelRefundCard(order, isBuyer, isSeller) : ''}${window.dpPendingAcceptCard ? window.dpPendingAcceptCard(order, isBuyer, isSeller) : ''}${window.mutualCancelCard ? window.mutualCancelCard(order, isBuyer, isSeller) : ''}
12527
12504
 
12528
12505
  ${trackingHtml}
12529
12506
  ${disputeHtml}
@@ -12590,7 +12567,7 @@ async function renderOrderDetail(app, orderId) {
12590
12567
  <div class="action-title">${t('完整状态历史')}</div>
12591
12568
  <div class="timeline">${historyHtml || `<div style="color:#6b7280;font-size:13px">${t('暂无记录')}</div>`}</div>
12592
12569
  </div>
12593
- `, 'orders'); if (order.payment_rail === 'direct_p2p' && window.dpHydrateOrderDisclosure) window.dpHydrateOrderDisclosure(order.id) // direct_p2p:最终 DOM 已就位,#dp-order-instr 必命中(both-acked→收款说明快照;否则 D1/D2 门;非买家→错误提示)。退货/评价 widget 注入各自子容器,不动此框
12570
+ `, 'orders'); if (order.payment_rail === 'direct_p2p' && isBuyer && window.dpHydrateOrderDisclosure) window.dpHydrateOrderDisclosure(order.id) // direct_p2p 买家:最终 DOM 已就位,#dp-order-instr 必命中(both-acked→收款说明快照;否则 D1/D2 )。卡片与 hydrate 均 isBuyer 门(端点本就 NOT_ORDER_BUYER,非买家渲染只会得红色报错噪音)。退货/评价 widget 注入各自子容器,不动此框
12594
12571
 
12595
12572
  // Wave B-3: 退货 widget — 异步加载(仅 completed 订单可退)
12596
12573
  // 买家:有退货窗口可申请/查看;卖家:有退货申请时内联查看+处理(accept/reject/received),无申请则隐藏卡
@@ -13061,14 +13038,14 @@ async function renderReturnWidgetForOrder(order, product) {
13061
13038
  const events = detail?.timeline || []
13062
13039
  const item = detail?.item || mine
13063
13040
  const isBuyer = state.user && state.user.id === item.buyer_id
13064
- const STATUS_COLORS = { pending:'#d97706', accepted:'#16a34a', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', escalated:'#6b21a8' }
13065
- const STATUS_LABEL = () => ({ pending: t('待卖家处理'), accepted: t('已同意'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), escalated: t('已升级仲裁') })
13041
+ const STATUS_COLORS = { pending:'#d97706', accepted:'#16a34a', refunded:'#16a34a', rejected:'#dc2626', cancelled:'#6b7280', escalated:'#6b21a8', ...(window.dpReturnStatusColors ? window.dpReturnStatusColors() : {}) }
13042
+ const STATUS_LABEL = () => ({ pending: t('待卖家处理'), accepted: t('已同意'), refunded: t('已退款'), rejected: t('已拒绝'), cancelled: t('已取消'), escalated: t('已升级仲裁'), ...(window.dpReturnStatusLabels ? window.dpReturnStatusLabels() : {}) })
13066
13043
 
13067
13044
  const isActive = !['refunded', 'cancelled', 'escalated'].includes(item.status)
13068
13045
  const canEscalate = isBuyer && (
13069
13046
  item.status === 'rejected' ||
13070
- (item.status === 'pending' && (Date.now() - new Date(item.created_at).getTime()) >= 7 * 86400 * 1000)
13071
- )
13047
+ (item.status === 'pending' && (Date.now() - new Date(item.created_at).getTime()) >= 7 * 86400 * 1000) ||
13048
+ (window.dpReturnCanEscalate ? window.dpReturnCanEscalate(item, order) : false))
13072
13049
 
13073
13050
  area.innerHTML = `
13074
13051
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
@@ -13078,11 +13055,11 @@ async function renderReturnWidgetForOrder(order, product) {
13078
13055
 
13079
13056
  ${isSellerView && item.status === 'pending' ? `
13080
13057
  <div style="display:flex;gap:8px;margin-bottom:8px">
13081
- <button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${t('接受退款')}</button>
13058
+ <button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${order.payment_rail === 'direct_p2p' ? t('同意退货(场外退款)') : t('接受退款')}</button>
13082
13059
  <button class="btn btn-outline btn-sm" style="flex:1;font-size:12px;color:#dc2626;border-color:#fecaca" onclick="decideReturn('${item.id}','reject','${order.id}')">${t('拒绝退货')}</button>
13083
13060
  </div>` : ''}
13084
13061
  ${isSellerView && item.status === 'picked_up' ? `
13085
- <button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${t('✓ 已收到退货 · 触发退款')}</button>` : ''}
13062
+ <button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${order.payment_rail === 'direct_p2p' ? '✓ ' + t('已收到退货') : t('✓ 已收到退货 · 触发退款')}</button>` : ''}${window.dpReturnHandshake ? window.dpReturnHandshake(item, isBuyer, isSellerView, order) : ''}
13086
13063
 
13087
13064
  <div style="padding:10px;background:#fafafa;border-radius:8px;margin-bottom:8px">
13088
13065
  <div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:8px">🧾 ${t('协商时间线')} · ${events.length} ${t('条')}</div>
@@ -13099,8 +13076,8 @@ async function renderReturnWidgetForOrder(order, product) {
13099
13076
 
13100
13077
  ${canEscalate ? `
13101
13078
  <div style="margin-top:8px;padding:8px 10px;background:#fef2f2;border:1px solid #fecaca;border-radius:6px">
13102
- <div style="font-size:11px;color:#991b1b;margin-bottom:6px">${item.status === 'rejected' ? t('卖家已拒绝 — 如有异议可升级至平台仲裁') : t('卖家 7 天内未回应 — 可升级至平台仲裁')}</div>
13103
- <button class="btn btn-sm" style="background:#6b21a8;color:#fff;padding:5px 14px;font-size:11px" onclick="escalateReturn('${item.id}','${order.id}')">⚖️ ${t('升级至仲裁')}</button>
13079
+ <div style="font-size:11px;color:#991b1b;margin-bottom:6px">${(window.dpReturnCanEscalate && window.dpReturnCanEscalate(item, order)) ? window.dpReturnEscalateHint(item) : item.status === 'rejected' ? t('卖家已拒绝 — 如有异议可升级至平台仲裁') : t('卖家 7 天内未回应 — 可升级至平台仲裁')}</div>
13080
+ <button class="btn btn-sm" style="background:#6b21a8;color:#fff;padding:5px 14px;font-size:11px" onclick="escalateReturn('${item.id}','${order.id}','${order.payment_rail || ''}')">⚖️ ${t('升级至仲裁')}</button>
13104
13081
  </div>
13105
13082
  ` : ''}
13106
13083
  `
@@ -13114,7 +13091,7 @@ async function renderReturnWidgetForOrder(order, product) {
13114
13091
 
13115
13092
  area.innerHTML = `
13116
13093
  <div style="margin-bottom:8px">${t('退货窗口')}:${returnDays} ${t('天')} · ${remainText}</div>
13117
- <button class="btn btn-outline btn-sm" onclick="openReturnRequestModal('${order.id}', ${Number(order.total_amount)})">${t('申请退货')}</button>
13094
+ <button class="btn btn-outline btn-sm" onclick="openReturnRequestModal('${order.id}', ${Number(order.total_amount)}, '${order.payment_rail === 'direct_p2p' ? 'USDC' : 'WAZ'}')">${t('申请退货')}</button>
13118
13095
  `
13119
13096
  }
13120
13097
 
@@ -13134,8 +13111,8 @@ window.sendReturnMessage = async (returnId, orderId) => {
13134
13111
  }
13135
13112
  }
13136
13113
 
13137
- window.escalateReturn = async (returnId, orderId) => {
13138
- if (!confirm(t('确定升级到仲裁?协议会冻结卖家资产并指派仲裁员审核。'))) return
13114
+ window.escalateReturn = async (returnId, orderId, rail) => {
13115
+ if (!confirm(rail === 'direct_p2p' ? t('确定升级到仲裁?直付订单为信誉裁决:仲裁不经手资金,按证据对卖家作信誉处罚。') : t('确定升级到仲裁?协议会冻结卖家资产并指派仲裁员审核。'))) return
13139
13116
  const res = await POST(`/return-requests/${returnId}/escalate`, {})
13140
13117
  if (res.error) return toast$(res.error, 'error')
13141
13118
  toast$(t('已升级仲裁'), 'success')
@@ -13286,18 +13263,21 @@ window.toggleOrderAddrEdit = (expand) => {
13286
13263
  }
13287
13264
  }
13288
13265
 
13289
- // Wave G-5: WAZ USD 近似(用 USDC peg)显示
13290
- function wazToUsd(waz) {
13291
- const rate = Number(state._wazUsdcRate || 1)
13292
- if (rate <= 0) return null
13293
- return Number(waz) / rate // 1 USDC ≈ 1 USD
13294
- }
13295
- // 渲染 $X.XX 小字(默认 buyer / 全局,可选 hidePrefix)
13296
- function usdHint(waz, opts = {}) {
13297
- const usd = wazToUsd(waz)
13298
- if (usd == null || !Number.isFinite(usd)) return ''
13299
- const txt = `≈ $${usd.toFixed(2)}`
13300
- return `<span style="font-size:10px;color:#9ca3af;margin-left:4px;font-weight:400">${txt}</span>`
13266
+ // 订单金额显示(RFC-014 USDC 计价):escrowUSDC + 买家本地币参考(fmtPrice,纯展示,无真实托管);
13267
+ // direct_p2p → USDC + 【买家下单时所选卖家收款账户币种】的应付额(dpFxInCurrency,不是本地币——买家只能按卖家
13268
+ // 支持的币种付款)。币种取自账号快照(currency 属非敏感元数据,pre-ack 也可见;仅 instruction/qr 受披露门)
13269
+ // 该币种无汇率 dpFxInCurrency 回落币种码,绝不臆造换算。
13270
+ window.orderAmountHtml = (o) => {
13271
+ const usdc = Number(o && o.total_amount)
13272
+ if (o && o.payment_rail === 'direct_p2p') {
13273
+ let cur = ''
13274
+ try { cur = String(JSON.parse(o.direct_pay_account_snapshot || '{}').currency || '').toUpperCase() } catch {}
13275
+ const base = `${Number.isFinite(usdc) ? (Number.isInteger(usdc) ? usdc : usdc.toFixed(2)) : ''} USDC`
13276
+ if (!cur || cur === 'USDC' || cur === 'USD') return window.fmtPrice(usdc) // USDC 账户:USDC + 买家本地法币参考(fmtPrice)
13277
+ const pay = window.dpFxInCurrency ? window.dpFxInCurrency(usdc, cur) : cur
13278
+ return `${base} <span style="font-size:10px;color:#9ca3af;margin-left:4px;font-weight:400">${t('应付')} ≈ ${pay}</span>`
13279
+ }
13280
+ return window.fmtPrice(usdc)
13301
13281
  }
13302
13282
 
13303
13283
  // Wave C-1: 规格格式化(订单详情 / 列表用)
@@ -13350,7 +13330,7 @@ function RETURN_REASON_LABEL() {
13350
13330
  }
13351
13331
  }
13352
13332
 
13353
- window.openReturnRequestModal = (orderId, total) => {
13333
+ window.openReturnRequestModal = (orderId, total, cur = 'WAZ') => {
13354
13334
  const labels = RETURN_REASON_LABEL()
13355
13335
  const html = `
13356
13336
  <div class="js-modal" style="background:rgba(0,0,0,0.6);position:fixed;inset:0;z-index:1000;display:flex;align-items:flex-end;justify-content:center" onclick="this.remove()">
@@ -13367,9 +13347,9 @@ window.openReturnRequestModal = (orderId, total) => {
13367
13347
  <textarea class="form-control" id="ret-text" rows="3" maxlength="500" placeholder="${t('描述问题(可选,便于卖家判断)')}"></textarea>
13368
13348
  </div>
13369
13349
  <div class="form-group">
13370
- <label class="form-label">${t('退款金额')} (WAZ) *</label>
13350
+ <label class="form-label">${t('退款金额')} (${cur}) *</label>
13371
13351
  <input class="form-control" id="ret-amount" type="number" min="0.01" max="${total}" step="0.01" value="${total}">
13372
- <div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('订单总额')}: ${total} WAZ · ${t('可申请部分退款')}</div>
13352
+ <div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('订单总额')}: ${total} ${cur} · ${t('可申请部分退款')}${cur === 'USDC' ? ' · ' + t('直付退款在协议外完成,金额为参考') : ''}</div>
13373
13353
  </div>
13374
13354
  <div class="form-group" style="background:#f0fdf4;border:1px solid #86efac;border-radius:8px;padding:10px 12px">
13375
13355
  <label style="display:flex;align-items:flex-start;gap:8px;font-size:12px;color:#166534;cursor:pointer">
@@ -13424,13 +13404,14 @@ function getActions(order, isBuyer, isSeller, isLogistic) {
13424
13404
  // 直付(direct_p2p):买家在 direct_pay_window 标记"我已付款"(Passkey 门) 或取消(不门控);其余阶段沿用通用规则
13425
13405
  if (order.payment_rail === 'direct_p2p' && isBuyer && s === 'direct_pay_window')
13426
13406
  return [{ action: 'mark_paid', label: '✅ 我已付款', style: 'success' }, { action: 'cancel', label: '取消订单', style: 'secondary' }]
13407
+ if (order.payment_rail === 'direct_p2p' && window.dpNegotiationActions) { const _na = window.dpNegotiationActions(order, isBuyer, isSeller); if (_na) return _na } // 货款协商(payment_query/disputed)动作
13427
13408
  // M8 面交订单:买家在 paid / accepted 都可"面交完成"直接结算
13428
13409
  if (isInPerson && isBuyer && (s === 'paid' || s === 'accepted'))
13429
13410
  return [{ action: 'confirm_in_person', label: '🤝 面交完成 / 确认收货', style: 'success' }]
13430
13411
  if (isSeller && s === 'paid')
13431
13412
  return [{ action: 'accept', label: '接单', style: 'success' }, { action: 'decline', label: '拒绝接单', style: 'danger', custom: 'decline' }]
13432
13413
  if (isSeller && s === 'accepted' && !isInPerson)
13433
- return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true, trackingInput: true, evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }]
13414
+ return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true, trackingInput: true, evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }, ...(order.payment_rail === 'direct_p2p' ? [{ action: 'report_nonpayment', label: '未收到货款(告知买家核实)', style: 'danger' }] : [])]
13434
13415
  if (isSeller && s === 'accepted' && isInPerson)
13435
13416
  return [{ action: 'noop_in_person', label: '🤝 面交中(等待买家确认)', style: 'secondary', disabled: true }]
13436
13417
  if ((isLogistic || isSelfFulfillSeller) && s === 'shipped')
@@ -13440,7 +13421,7 @@ function getActions(order, isBuyer, isSeller, isLogistic) {
13440
13421
  if ((isLogistic || isSelfFulfillSeller) && s === 'in_transit')
13441
13422
  return [{ action: 'deliver', label: '📬 确认投递', style: 'success', needsEvidence: true, noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间', helperText: isSelfFulfillSeller ? '自履约投递需留存签收/门牌/交付说明,买家确认后才结算。' : '' }]
13442
13423
  if (isBuyer && s === 'delivered')
13443
- return [{ action: 'confirm', label: '确认收货', style: 'success' }, { action: 'dispute', label: '发起争议', style: 'danger', needsEvidence: true, noteLabel: '争议理由', evidencePlaceholder: '描述问题(货不对版/货损/未收到等)' }]
13424
+ return [{ action: 'confirm', label: '确认收货', style: 'success' }, { action: 'dispute', label: '未收到货 / 有问题?', style: 'danger', needsEvidence: true, noteLabel: '争议理由', evidencePlaceholder: '描述问题(未收到 / 货不对版 / 货损等)', helperText: '未收到货?可先联系卖家核实(可能晚到或放了代收点)。确认异常再提交 —— 发起争议后自动确认将暂停,不会因超时被视为已收货。' }]
13444
13425
  return null
13445
13426
  }
13446
13427
 
@@ -13503,7 +13484,7 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
13503
13484
  if (!confirm(t('确认已收到物品?escrow 将立即释放给卖家,无法撤销。'))) return
13504
13485
  msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('确认中...')}</div>`
13505
13486
  const res = await POST(`/orders/${orderId}/confirm-in-person`, {})
13506
- if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
13487
+ if (res.error) { msgEl.innerHTML = alert$('error', window.orderErrorText ? window.orderErrorText(res.error_code, res.error) : res.error); return }
13507
13488
  msgEl.innerHTML = alert$('success', t('面交完成 — escrow 已释放'))
13508
13489
  maybeThankSponsorAndClear()
13509
13490
  setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
@@ -13511,7 +13492,7 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
13511
13492
  }
13512
13493
  if (action === 'noop_in_person') return
13513
13494
  const confirmText = {
13514
- ship: t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
13495
+ ship: (window._dpOrderRail === 'direct_p2p' && window.dpPayRef) ? `${t('直付订单:发货前请核实货款已到账 —— 银行流水附言应为')} ${window.dpPayRef(orderId)}${t('(同买家同金额多单务必逐单核对参考号)。发货即视为你确认已收到货款;发货后不可再报告未收款,只能走争议。')}` : t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
13515
13496
  pickup: t('确认已揽收并回传凭证?请确保单号或揽收说明真实可追踪。'),
13516
13497
  deliver: t('确认已投递?请确保投递凭证真实,买家仍需确认收货后才结算。'),
13517
13498
  confirm: t('确认收货?escrow 将进入结算流程,无法撤销。'),
@@ -13560,7 +13541,7 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
13560
13541
 
13561
13542
  const res = await POST(`/orders/${orderId}/action`, body)
13562
13543
  if (res.error) {
13563
- msgEl.innerHTML = alert$('error', res.error)
13544
+ msgEl.innerHTML = alert$('error', window.orderErrorText ? window.orderErrorText(res.error_code, res.error) : res.error)
13564
13545
  } else {
13565
13546
  msgEl.innerHTML = alert$('success', t('操作成功!'))
13566
13547
  // S8: confirm action 触发 completed → 检查是否首单,若是则致谢 + 清 ctx
@@ -13828,8 +13809,8 @@ function buildTimelineEvent(ev, dispute, user, actors) {
13828
13809
  const isMe = user && actor && actor.id === user.id
13829
13810
  // 当无 actor(如 system resolved 事件),不重复显示与 role chip 相同的名字
13830
13811
  const actorLabel = actor
13831
- ? (actor.handle ? '@' + escHtml(actor.handle) : escHtml(actor.name))
13832
- : ''
13812
+ ? (actor.name ? escHtml(actor.name) : (actor.handle ? '@' + escHtml(actor.handle) : ''))
13813
+ : '' // 走查批次3:昵称优先(自动生成的 @handle 可读性差;上方 party chips 本就用昵称,对齐)
13833
13814
 
13834
13815
  // 内容主体 — 按 type 分支
13835
13816
  let bodyHtml = ''
@@ -13876,14 +13857,14 @@ function buildTimelineEvent(ev, dispute, user, actors) {
13876
13857
  } else if (ev.type === 'ruling' || ev.type === 'resolved') {
13877
13858
  const meta = ev.meta || {}
13878
13859
  const rulingLabel = meta.ruling || meta.ruling_type
13879
- const rulingText = rulingLabel ? t(RULING_LABELS[rulingLabel] || rulingLabel) : ''
13860
+ const rulingText = rulingLabel ? ((dispute.payment_rail === 'direct_p2p' && window.dpRulingLabel && window.dpRulingLabel(rulingLabel)) || t(RULING_LABELS[rulingLabel] || rulingLabel)) : ''
13880
13861
  let liability = []
13881
13862
  try { liability = typeof meta.liability_parties === 'string' ? JSON.parse(meta.liability_parties) : (meta.liability_parties || []) } catch {}
13882
13863
  bodyHtml = `
13883
13864
  ${rulingText ? `<div style="font-weight:700;font-size:14px;color:${typeMeta.border};margin-bottom:4px">${t(rulingText)}</div>` : ''}
13884
13865
  ${ev.body ? `<div style="font-size:13px;line-height:1.5;white-space:pre-wrap;color:#374151">${escHtml(ev.body)}</div>` : ''}
13885
- ${meta.refund_amount != null ? `<div style="font-size:12px;margin-top:6px">💸 ${t('退款金额')}:<strong>${meta.refund_amount} WAZ</strong></div>` : ''}
13886
- ${liability.length > 0 ? `<div style="margin-top:6px">
13866
+ ${dispute.payment_rail !== 'direct_p2p' && meta.refund_amount != null ? `<div style="font-size:12px;margin-top:6px">💸 ${t('退款金额')}:<strong>${meta.refund_amount} WAZ</strong></div>` : ''}
13867
+ ${dispute.payment_rail !== 'direct_p2p' && liability.length > 0 ? `<div style="margin-top:6px">
13887
13868
  <div style="font-size:11px;color:#6b7280;margin-bottom:2px">${t('责任分配')}:</div>
13888
13869
  ${liability.map(lp => `<div style="font-size:12px;margin-top:2px">${(TL_ROLE_META[lp.role]||TL_ROLE_META.unknown).icon} ${t(lp.role)} ${t('承担')} ${lp.amount} WAZ${lp.insurance_cap!=null?` (${t('保险上限')} ${lp.insurance_cap})`:''}</div>`).join('')}
13889
13870
  </div>` : ''}`
@@ -13928,7 +13909,7 @@ function buildDisputeTimelineSection(dispute, user) {
13928
13909
 
13929
13910
  function buildDisputeHtml(dispute, user) {
13930
13911
  const isDefendant = user && user.id === dispute.defendant_id
13931
- const isArbitrator = user && user.role === 'arbitrator'
13912
+ const isArbitrator = !!state.canArbitrate // PR-E:跟随后端 can_arbitrate(active whitelist),非 user.role;后端 COI/assign 才是边界
13932
13913
  // is_party is set by the server; fallback to local check
13933
13914
  const isParty = dispute.is_party || (user && dispute.parties && dispute.parties.some(p => p.id === user.id))
13934
13915
  const parties = dispute.parties || []
@@ -14072,20 +14053,13 @@ function buildDisputeHtml(dispute, user) {
14072
14053
  const arbitrateSection = isArbitrator && (dispute.status === 'open' || dispute.status === 'in_review') ? `
14073
14054
  <div style="margin-top:12px;border-top:1px solid #fecaca;padding-top:12px">
14074
14055
  <div style="font-weight:600;font-size:13px;margin-bottom:8px">⚖️ 仲裁员裁定(截止 ${fmtTime(dispute.arbitrate_deadline)})</div>
14075
- <div style="font-size:12px;color:#92400e;background:#fffbeb;border:1px solid #fcd34d;border-radius:6px;padding:6px 10px;margin-bottom:8px">
14076
- 💰 败诉方须缴纳仲裁费:订单金额 × 1%(最低 1 WAZ)。部分退款时双方各付 0.5%。仲裁费 50% 归仲裁员,50% 归协议。
14077
- </div>
14078
- <div id="arbitrate-msg"></div>
14056
+ ${window.dpArbFeeNote ? window.dpArbFeeNote(dispute.payment_rail) : ''}
14057
+ <input type="hidden" id="arb-rail" value="${dispute.payment_rail || ''}"><div id="arbitrate-msg"></div>
14079
14058
 
14080
14059
  <div class="form-group" style="margin-bottom:10px">
14081
14060
  <label style="font-size:12px;color:#6b7280;display:block;margin-bottom:4px">裁定方式</label>
14082
14061
  <div style="display:flex;flex-direction:column;gap:6px">
14083
- ${[
14084
- ['refund_buyer', '🔵 全额退款买家(买家胜诉,卖家承担)'],
14085
- ['release_seller', '🟢 资金释放给卖家(卖家胜诉)'],
14086
- ['partial_refund', '🟡 部分退款(折中,需填金额)'],
14087
- ['liability_split', '⚖️ 责任分配(指定各方赔付额)'],
14088
- ].map(([val, label]) => `
14062
+ ${(window.dpArbRulingOptions ? window.dpArbRulingOptions(dispute.payment_rail, dispute.can_dismiss_to_negotiation) : []).map(([val, label]) => `
14089
14063
  <label style="display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;padding:8px;background:#f9fafb;border-radius:6px">
14090
14064
  <input type="radio" name="arb-ruling-radio" value="${val}" onclick="onArbRulingChange('${val}')"> ${label}
14091
14065
  </label>`).join('')}
@@ -14334,14 +14308,15 @@ window.handleArbitrate = async (disputeId) => {
14334
14308
  if (!reason) { msgEl.innerHTML = alert$('error', t('请填写裁定理由')); return }
14335
14309
 
14336
14310
  let body = { ruling, reason }
14311
+ const nc = document.getElementById('arb-rail')?.value === 'direct_p2p' // 直付:仅信誉裁决 → 不读/不发任何退款/赔付金额
14337
14312
 
14338
- if (ruling === 'partial_refund') {
14313
+ if (!nc && ruling === 'partial_refund') {
14339
14314
  const amount = document.getElementById('arb-amount')?.value
14340
14315
  if (!amount) { msgEl.innerHTML = alert$('error', t('部分退款需填写退款金额')); return }
14341
14316
  body = { ...body, refund_amount: Number(amount) }
14342
14317
  }
14343
14318
 
14344
- if (ruling === 'liability_split') {
14319
+ if (!nc && ruling === 'liability_split') {
14345
14320
  const liabilityParties = []
14346
14321
  let totalCalc = 0
14347
14322
  for (const chk of document.querySelectorAll('.arb-liable-chk:checked')) {
@@ -14367,8 +14342,8 @@ window.handleArbitrate = async (disputeId) => {
14367
14342
  body = { ...body, liability_parties: liabilityParties, refund_amount: totalCalc }
14368
14343
  }
14369
14344
 
14370
- msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('裁定中...')}</div>`
14371
- const res = await POST(`/disputes/${disputeId}/arbitrate`, body)
14345
+ let _arbTk; try { _arbTk = await requestPasskeyGate('arbitrate', { dispute_id: disputeId }) } catch (e) { msgEl.innerHTML = alert$('error', (e && e.message ? e.message + ' — ' : '') + t('仲裁裁定需现场真人 Passkey 验证')); return }; msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('裁定中...')}</div>` // 所有 ruling(含驳回)先取 arbitrate gate token
14346
+ const res = await POST(`/disputes/${disputeId}/arbitrate`, { ...body, webauthn_token: _arbTk })
14372
14347
  if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
14373
14348
 
14374
14349
  // 显示仲裁费明细
@@ -14381,10 +14356,11 @@ window.handleArbitrate = async (disputeId) => {
14381
14356
 
14382
14357
  // 裁定方式切换 → 控制相关输入框显隐
14383
14358
  window.onArbRulingChange = (ruling) => {
14359
+ const nc = document.getElementById('arb-rail')?.value === 'direct_p2p' // 直付:无金额/无赔付 → 两个金额区块永不出现
14384
14360
  const partialRow = document.getElementById('arb-partial-row')
14385
14361
  const liabilityBlock = document.getElementById('arb-liability-block')
14386
- if (partialRow) partialRow.style.display = ruling === 'partial_refund' ? '' : 'none'
14387
- if (liabilityBlock) liabilityBlock.style.display = ruling === 'liability_split' ? '' : 'none'
14362
+ if (partialRow) partialRow.style.display = (!nc && ruling === 'partial_refund') ? '' : 'none'
14363
+ if (liabilityBlock) liabilityBlock.style.display = (!nc && ruling === 'liability_split') ? '' : 'none'
14388
14364
  }
14389
14365
 
14390
14366
  // 责任分配:勾选责任方时启用金额输入框,并实时计算合计
@@ -14567,7 +14543,7 @@ async function hydrateSimilarCases(disputeId) {
14567
14543
 
14568
14544
  async function renderDisputeList(app) {
14569
14545
  if (!state.user) { renderLogin(); return }
14570
- if (state.user.role !== 'arbitrator') {
14546
+ if (!state.canArbitrate) { // PR-E:跟随后端 can_arbitrate(active whitelist),whitelist-only(role=buyer)真人仲裁员也能进
14571
14547
  app.innerHTML = shell(`
14572
14548
  <h1 class="page-title">${t('争议仲裁台')}</h1>
14573
14549
  <div class="alert alert-info">${t('此功能仅限仲裁员使用。')}<br>${t('你的角色')}:${state.user.role}</div>
@@ -14757,7 +14733,7 @@ async function renderLogistics(app) {
14757
14733
  <div style="flex:1;min-width:0">
14758
14734
  <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(o.product_title)}${windowChip}</div>
14759
14735
  ${o.shipping_address ? `<div style="font-size:11px;color:#6b7280;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px">📍 ${escHtml(o.shipping_address)}</div>` : ''}
14760
- <div style="font-size:10px;color:${urgent ? '#dc2626' : '#9ca3af'};margin-top:2px${urgent ? ';font-weight:600' : ''}">${o.total_amount} WAZ · ${o.ship_deadline ? `${urgent ? '⚠ ' : ''}${t('截止')} ${fmtTime(o.ship_deadline)}` : fmtTime(o.created_at)}</div>
14736
+ <div style="font-size:10px;color:${urgent ? '#dc2626' : '#9ca3af'};margin-top:2px${urgent ? ';font-weight:600' : ''}">${window.orderAmountHtml(o)} · ${o.ship_deadline ? `${urgent ? '⚠ ' : ''}${t('截止')} ${fmtTime(o.ship_deadline)}` : fmtTime(o.created_at)}</div>
14761
14737
  </div>
14762
14738
  ${act ? `<div style="font-size:11px;color:#065f46;font-weight:600;flex-shrink:0;white-space:nowrap">${act.short} →</div>` : ''}
14763
14739
  </div>
@@ -14977,7 +14953,7 @@ function renderInsightsBlock(d) {
14977
14953
  <div class="card" style="padding:12px">
14978
14954
  <div style="font-size:10px;color:#9ca3af;text-transform:uppercase">${t('30 天 GMV')}</div>
14979
14955
  <div style="font-size:18px;font-weight:800;color:#15803d;margin-top:2px">${Number(s.gmv||0).toFixed(0)} <span style="font-size:11px;font-weight:600;color:#6b7280">WAZ</span></div>
14980
- <div style="font-size:10px;color:#6b7280;margin-top:2px">${deltaChip(vs.gmv_pct||0, t('环比'))}</div>
14956
+ <div style="font-size:10px;color:#6b7280;margin-top:2px">${deltaChip(vs.gmv_pct||0, t('环比'))}</div>${window.gmvRailSplitHtml ? window.gmvRailSplitHtml(s.gmv_escrow, s.gmv_direct_pay) : ''}
14981
14957
  </div>
14982
14958
  <div class="card" style="padding:12px">
14983
14959
  <div style="font-size:10px;color:#9ca3af;text-transform:uppercase">${t('30 天订单')}</div>
@@ -15300,7 +15276,7 @@ async function renderSeller(app) {
15300
15276
  <div class="order-meta">${fmtTime(o.created_at)}</div>
15301
15277
  <div style="margin-top:6px">${orderStatusBadges(o)}</div>
15302
15278
  </div>
15303
- <div class="order-amount">${o.total_amount} WAZ</div>
15279
+ <div class="order-amount">${window.orderAmountHtml(o)}</div>
15304
15280
  </div>
15305
15281
  </div>`).join('')
15306
15282
  const acceptHtml = paidOrders.length === 0
@@ -15509,7 +15485,7 @@ async function renderSeller(app) {
15509
15485
  ${subTabBtn('settings', '⚙️ ' + t('设置'))}
15510
15486
  </div>
15511
15487
  `
15512
- const settingsSection = sellerSubTab === 'settings' ? ((window.dpSellerReadinessSection ? window.dpSellerReadinessSection() : '') + (window.dpSellerFeeSection ? window.dpSellerFeeSection() : '') + (window.dpSellerDeferralSection ? window.dpSellerDeferralSection() : '') + (window.dpSellerProductVerifySection ? window.dpSellerProductVerifySection() : '') + (window.dpSellerStoreVerifySection ? window.dpSellerStoreVerifySection() : '') + (window.dpSellerInstructionSection ? window.dpSellerInstructionSection() : '') + (window.draAccountsSection ? window.draAccountsSection() : '')) : ''
15488
+ const settingsSection = sellerSubTab === 'settings' ? ((window.dpSellerReadinessSection ? window.dpSellerReadinessSection() : '') + (window.dpSellerFeeSection ? window.dpSellerFeeSection() : '') + (window.dpSalesReportSection ? window.dpSalesReportSection() : '') + (window.dpFeeRequestSection ? window.dpFeeRequestSection() : '') + (window.dpSellerDeferralSection ? window.dpSellerDeferralSection() : '') + (window.dpSellerProductVerifySection ? window.dpSellerProductVerifySection() : '') + (window.dpSellerStoreVerifySection ? window.dpSellerStoreVerifySection() : '') + (window.dpSellerInstructionSection ? window.dpSellerInstructionSection() : '') + (window.draAccountsSection ? window.draAccountsSection() : '') + (window.shipSellerSettingsSection ? window.shipSellerSettingsSection() : '') + (window.bondSellerSection ? window.bondSellerSection() : '')) : ''
15513
15489
 
15514
15490
  // 数据中心区块(30 天聚合 — 销售曲线 / Top 商品 / 客户洞察 / 状态分布)
15515
15491
  const insightsBlock = insights ? renderInsightsBlock(insights) : ''
@@ -15561,6 +15537,7 @@ async function renderSeller(app) {
15561
15537
  <div style="font-weight:600;font-size:13px;color:#1e40af;margin-top:6px">${t('普通上架')}</div>
15562
15538
  <div style="font-size:10px;color:#1d4ed8;margin-top:2px">${t('标准商品 · 即买即发')}</div>
15563
15539
  </div>
15540
+ ${window.freeShippingMarketingCard ? window.freeShippingMarketingCard() : ''}
15564
15541
  <div onclick="location.hash='#seller-trials'" class="card" style="padding:14px;cursor:pointer;background:linear-gradient(135deg,#faf5ff,#fdf2f8);border-color:#ddd6fe">
15565
15542
  <div style="font-size:24px">🎁</div>
15566
15543
  <div style="font-weight:600;font-size:13px;color:#6b21a8;margin-top:6px">${t('测评免单活动')}</div>
@@ -15572,8 +15549,7 @@ async function renderSeller(app) {
15572
15549
  const skillsSection = sellerSubTab === 'skills' ? `
15573
15550
  <div style="font-size:12px;color:#6b7280;margin-bottom:10px">${t('Skill 自动化 · 让 Agent 替你接单/报价/发货')}</div>
15574
15551
  ` : ''
15575
- // 注:Skill 真实内容由下方 #my-skills-list 同步渲染(mySkills 已同步就绪);
15576
- // 此前这里有个 #skill-mgmt-content loading$() 占位但全文件无人填充 → spinner 永转,已删除。
15552
+ // 注:Skill 真实内容由下方 #my-skills-list 同步渲染(mySkills 已同步就绪);此前 #skill-mgmt-content loading$() 占位无人填充 → spinner 永转,已删除。
15577
15553
 
15578
15554
  const productsSection = sellerSubTab === 'products' ? `
15579
15555
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
@@ -15751,7 +15727,7 @@ async function renderSeller(app) {
15751
15727
  <input type="checkbox" id="prd-fragile" style="width:16px;height:16px">
15752
15728
  <label for="prd-fragile" style="font-size:13px">${t('易碎品,需特殊包装')}</label>
15753
15729
  </div>
15754
- </details>
15730
+ </details>${window.listingCommerceSectionHtml ? window.listingCommerceSectionHtml(null) : ''}
15755
15731
  <div style="display:flex;gap:8px;align-items:center;margin-bottom:12px"><label class="form-label" style="margin:0;font-size:13px">${t('低库存阈值')}</label><input class="form-control" id="prd-low-stock" type="number" value="3" min="0" style="width:90px"><input type="checkbox" id="prd-auto-delist" checked style="width:16px;height:16px"><label for="prd-auto-delist" style="font-size:13px">${t('售罄自动下架')}</label></div>
15756
15732
  <!-- 2026-05-24 #981:测评免单 (Trial Review Refund) -->
15757
15733
  <details style="margin-bottom:16px" id="prd-trial-section">
@@ -15844,7 +15820,7 @@ async function renderSeller(app) {
15844
15820
  </div>
15845
15821
  </div>
15846
15822
  `, 'seller')
15847
- if (sellerSubTab === 'settings') { window.dpHydrateInstruction && window.dpHydrateInstruction(); window.dpHydrateSellerReadiness && window.dpHydrateSellerReadiness(); window.dpHydrateSellerFee && window.dpHydrateSellerFee(); window.dpHydrateSellerDeferral && window.dpHydrateSellerDeferral(); window.dpHydrateSellerProductVerify && window.dpHydrateSellerProductVerify(); window.dpHydrateSellerStoreVerify && window.dpHydrateSellerStoreVerify(); window.draHydrateAccounts && window.draHydrateAccounts() }
15823
+ if (sellerSubTab === 'settings') { window.dpHydrateInstruction && window.dpHydrateInstruction(); window.dpHydrateSellerReadiness && window.dpHydrateSellerReadiness(); window.dpHydrateSellerFee && window.dpHydrateSellerFee(); window.dpHydrateSalesReport && window.dpHydrateSalesReport(); window.dpHydrateFeeRequest && window.dpHydrateFeeRequest(); window.dpHydrateSellerDeferral && window.dpHydrateSellerDeferral(); window.dpHydrateSellerProductVerify && window.dpHydrateSellerProductVerify(); window.dpHydrateSellerStoreVerify && window.dpHydrateSellerStoreVerify(); window.draHydrateAccounts && window.draHydrateAccounts(); window.shipHydrateSellerSettings && window.shipHydrateSellerSettings(); window.bondHydrateSeller && window.bondHydrateSeller() }
15848
15824
 
15849
15825
  // 智能下单"我也要上架"跳过来时:自动切到商品 tab + 展开手工上架表单 + 预填标题
15850
15826
  // hashchange 可能多次触发 renderSeller — 用 window cache 保证每次重渲都能应用
@@ -16144,10 +16120,10 @@ window.doAddProduct = async () => {
16144
16120
  commission_rate: commissionPct / 100,
16145
16121
  ...(extTitle ? { external_title: extTitle } : {}),
16146
16122
  ...(checkedAliases.length ? { aliases: checkedAliases } : {}),
16147
- ...(state._addProductImgs?.length ? { image_hashes: state._addProductImgs.map(it => it.hash) } : {}),
16123
+ ...(state._addProductImgs?.length ? { image_hashes: state._addProductImgs.map(it => it.hash) } : {}), ...(window.listingCommerceHasOverrides && window.listingCommerceHasOverrides() ? { create_status: 'warehouse' } : {}), // S4:含覆盖的新品先落仓库,全落定后激活
16148
16124
  }
16149
16125
  const res = await POST('/products', payload)
16150
- if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
16126
+ if (res.error) { msgEl.innerHTML = alert$('error', res.error); return } if (res.product_id && window.listingCommerceSave) { const lc = await window.listingCommerceSave(res.product_id, { activate: payload.create_status === 'warehouse' }); if (!lc.ok) { msgEl.innerHTML = alert$('error', (payload.create_status === 'warehouse' ? t('商品已存为仓库(未公开),配送/税费设置失败,请到「我的商品」编辑修复后上架:') : t('单品配送/税费设置失败:')) + lc.error); return } }
16151
16127
 
16152
16128
  // 产品创建成功 → 把每张图的 blob 存到本地 IDB + 注册 manifest(带 thumbnail)
16153
16129
  // 服务端只拿到了 hash 数组;blob 字节留在卖家节点
@@ -16697,7 +16673,7 @@ async function renderEditProduct(app, productId) {
16697
16673
  <label for="ep-fragile" style="font-size:13px">${t('易碎品,需特殊包装')}</label>
16698
16674
  </div>
16699
16675
  </div>
16700
- </details>
16676
+ </details>${window.listingCommerceSectionHtml ? window.listingCommerceSectionHtml(p) : ''}
16701
16677
  <button class="btn btn-primary" onclick="doUpdateProduct('${productId}')">${t('保存修改')}</button>
16702
16678
  </div>
16703
16679
 
@@ -16882,7 +16858,7 @@ window.doUpdateProduct = async (productId) => {
16882
16858
  body: JSON.stringify(payload),
16883
16859
  }).then(r => r.json())
16884
16860
  if (btn) { btn.disabled = false; btn.textContent = t('保存修改') }
16885
- if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
16861
+ if (res.error) { msgEl.innerHTML = alert$('error', res.error); return } if (window.listingCommerceSave) { const lc = await window.listingCommerceSave(productId); if (!lc.ok) { msgEl.innerHTML = alert$('error', t('单品配送/税费设置失败:') + lc.error); return } } // S4 单品覆盖
16886
16862
  msgEl.innerHTML = alert$('success', t('已保存'))
16887
16863
  setTimeout(() => msgEl.innerHTML = '', 2000)
16888
16864
  }
@@ -17110,7 +17086,7 @@ async function p2pConnectTo(peerId) {
17110
17086
 
17111
17087
  const offer = await pc.createOffer()
17112
17088
  await pc.setLocalDescription(offer)
17113
- await POST('/signaling/send', { to: peerId, type: 'offer', data: offer })
17089
+ await POST('/signaling/send', { to: peerId, type: 'offer', data: offer }); if (window.pollBoost) pollBoost() // 主动建 P2P 会话 → 握手期间信令保持 3s
17114
17090
  return entry
17115
17091
  }
17116
17092
 
@@ -17294,8 +17270,8 @@ async function p2pStart() {
17294
17270
  p2pStop()
17295
17271
  await p2pHeartbeatTick()
17296
17272
  await p2pSignalingTick()
17297
- _heartbeatTimer = setInterval(p2pHeartbeatTick, 60_000)
17298
- _signalingTimer = setInterval(p2pSignalingTick, 3_000)
17273
+ _heartbeatTimer = setInterval(() => { if (!document.hidden) p2pHeartbeatTick() }, 60_000) // 后台/锁屏暂停(poll-governor)
17274
+ _signalingTimer = setInterval(() => { if (!window.pollGate || window.pollGate('signaling')) p2pSignalingTick() }, 3_000); window._p2pSigTick = p2pSignalingTick // 按需化:governor 定 3s/60s 节奏+后台暂停(缺 governor fail-open 维持 3s);_p2pSigTick 供回前台补拉
17299
17275
  }
17300
17276
 
17301
17277
  function p2pStop() {
@@ -17336,7 +17312,7 @@ window.p2pUnpinContent = async (hash) => {
17336
17312
  async function p2pSignalingTick() {
17337
17313
  if (!state.user) return
17338
17314
  try {
17339
- const data = await GET('/signaling/poll')
17315
+ const data = await GET('/signaling/poll'); if (window.pollActivity) pollActivity('signaling', (data.signals || []).length) // 有信令进来 → governor 恢复 3s 快节奏
17340
17316
  for (const s of (data.signals || [])) {
17341
17317
  try { await p2pHandleSignal(s) } catch (e) { console.warn('[P2P] signal handle err', e) }
17342
17318
  }
@@ -17699,7 +17675,7 @@ async function renderVerifyTasks(app) {
17699
17675
  // ── Verifier 状态 banner(依据 /verifier/status) ─────────────
17700
17676
  const vState = vStatus?.state || 'none'
17701
17677
  let verifierBanner = ''
17702
- if (vState === 'none') {
17678
+ if (vState === 'none' && state.user?.role !== 'verifier' && !(state.user?.roles || []).includes('verifier')) { // 已是审核员(role 直授,无申请记录)不再邀请"申请审核员资格"(走查:身份矛盾卡)
17703
17679
  verifierBanner = `
17704
17680
  <div class="card" style="margin-bottom:16px;background:linear-gradient(135deg,#f5f3ff,#fce7f3);border-color:#c4b5fd">
17705
17681
  <div style="font-size:15px;font-weight:600;margin-bottom:6px">🛡 ${t('申请审核员资格')}</div>
@@ -18788,10 +18764,10 @@ function connectSSE() {
18788
18764
  if (data.type === 'init') {
18789
18765
  updateBadge(data.unread)
18790
18766
  } else {
18791
- // 实时推送:更新角标 + 显示 toast
18767
+ // 实时推送:更新角标 + 显示 toast(N1:模板通知按 viewer locale 渲染,旧推送回退原文)
18792
18768
  state.unread++
18793
18769
  updateBadge(state.unread)
18794
- showToast(data.title, data.body)
18770
+ const _tn = window.notifRender ? window.notifRender(data) : data; showToast(_tn.title, _tn.body)
18795
18771
  }
18796
18772
  }
18797
18773
  state.sse.onerror = () => {
@@ -21518,9 +21494,9 @@ async function renderMessages(app, deepSub) {
21518
21494
  }
21519
21495
  body = notifications.length === 0
21520
21496
  ? `<div style="text-align:center;color:#9ca3af;padding:40px 0;font-size:13px">🔔 ${t('暂无通知')}</div>`
21521
- : notifications.map(n => {
21522
- let actions = []
21523
- try { actions = n.actions ? JSON.parse(n.actions) : [] } catch {}
21497
+ : notifications.map(nRaw => {
21498
+ const n = window.notifRender ? window.notifRender(nRaw) : nRaw // N1:template_key → viewer locale 渲染;旧行回退存量 title/body
21499
+ let actions = []; try { actions = n.actions ? JSON.parse(n.actions) : [] } catch {}
21524
21500
  const route = routeFor(n)
21525
21501
  const hasActions = Array.isArray(actions) && actions.length > 0
21526
21502
  // 卡片 onclick:仅当无 actions 时整卡可点(兼容旧 notif)
@@ -22913,11 +22889,8 @@ async function renderAdminProtocol(app) {
22913
22889
  ${adminLinkCard('🏦', t('慈善基金拨款'), t('从基金池拨款给指定用户'), '#admin/fund-disburse')}
22914
22890
  ${adminLinkCard('📜', t('审计日志'), t('全部 admin 写操作'), '#admin/audit')}
22915
22891
  ${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
22916
- ${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
22917
- ${adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 转正式任务'), '#admin/task-proposals')}
22918
- ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🎟️', t('建任务额度审核'), t('非根管理员的建任务扩容申请;批准 = 限时计数授权(仅 root)'), '#admin/quota-requests') : ''}
22919
- ${adminLinkCard('🔗', t('关联个人贡献账号'), t('把本管理席位的协调贡献归属到你的真实个人账号(需对方确认 + root 审批)'), '#me/operator-claims')}
22920
- ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('🪪', t('操作席位关联审批'), t('管理席位→个人贡献账号的关联申请;确认 + 审批 / 撤销(仅 root)'), '#admin/operator-claims') : ''}${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('💳', t('Direct Pay 商户运营'), t('直付:合规录入 / 缓交 / 商品·店铺验证 / 平台服务费预充值 / 上线控制(仅 root)'), '#admin/dp-ops') : ''}
22892
+ ${adminLinkCard('🌱', t('共建运营'), t('共建任务建议 / Welcome 提交 / 建任务额度 / 贡献账号关联'), '#admin/contribution-ops')}
22893
+ ${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('💳', t('Direct Pay 商户运营'), t('直付:合规录入 / 缓交 / 商品·店铺验证 / 平台服务费预充值 / 上线控制(仅 root)'), '#admin/dp-ops') : ''}${((state.user && state.user.admin_type || 'root') === 'root') ? adminLinkCard('⚖', t('仲裁员管理'), t('授权/暂停/撤销真人仲裁员(唯一授权源=active 白名单,需现场 Passkey)'), '#admin/arbitrators') + adminLinkCard('🚦', t('Agent 封禁与申诉'), t('待审申诉队列(批准=立即解封)/ 主动签发 strike'), '#admin/agent-strikes') : ''}
22921
22894
  </div>
22922
22895
  `, 'admin-protocol')
22923
22896
  }