@seasonkoh/webaz 0.1.25 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +3 -1
  2. package/dist/layer1-agent/L1-1-mcp-server/server.js +129 -150
  3. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +9 -0
  4. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +1 -1
  5. package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
  6. package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
  7. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
  8. package/dist/pwa/admin-bearer-auth.js +21 -0
  9. package/dist/pwa/email-delivery.js +127 -0
  10. package/dist/pwa/public/app.js +940 -245
  11. package/dist/pwa/public/i18n.js +269 -40
  12. package/dist/pwa/public/openapi.json +4 -4
  13. package/dist/pwa/public/whitepaper/en/index.html +153 -0
  14. package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
  15. package/dist/pwa/routes/admin-atomic.js +10 -4
  16. package/dist/pwa/routes/admin-moderation.js +25 -1
  17. package/dist/pwa/routes/admin-ops.js +13 -2
  18. package/dist/pwa/routes/admin-users-query.js +12 -1
  19. package/dist/pwa/routes/admin-wallet-ops.js +26 -3
  20. package/dist/pwa/routes/auction.js +4 -2
  21. package/dist/pwa/routes/auth-read.js +10 -1
  22. package/dist/pwa/routes/auth-register.js +82 -12
  23. package/dist/pwa/routes/contribution-identity.js +17 -0
  24. package/dist/pwa/routes/growth.js +1 -1
  25. package/dist/pwa/routes/orders-action.js +19 -13
  26. package/dist/pwa/routes/profile-credentials.js +7 -4
  27. package/dist/pwa/routes/profile-placement.js +7 -8
  28. package/dist/pwa/routes/promoter.js +3 -17
  29. package/dist/pwa/routes/ratings.js +64 -4
  30. package/dist/pwa/routes/recover-key.js +58 -19
  31. package/dist/pwa/routes/referral.js +4 -24
  32. package/dist/pwa/routes/share-redirects.js +4 -3
  33. package/dist/pwa/routes/shop-referral.js +6 -5
  34. package/dist/pwa/routes/shops.js +5 -2
  35. package/dist/pwa/routes/task-proposals.js +76 -0
  36. package/dist/pwa/routes/trial.js +4 -2
  37. package/dist/pwa/routes/users-public.js +2 -12
  38. package/dist/pwa/routes/wallet-read.js +1 -1
  39. package/dist/pwa/server.js +67 -9
  40. package/package.json +31 -3
@@ -693,6 +693,7 @@ async function render(page, params) {
693
693
  if (params[0] === 'users' && params[1]) return renderAdminUserDetail(app, params[1])
694
694
  if (params[0] === 'users') return renderAdminUsers(app)
695
695
  if (params[0] === 'audit') return renderAdminAudit(app)
696
+ if (params[0] === 'security') return renderAdminSecurity(app)
696
697
  if (params[0] === 'products') return renderAdminProducts(app)
697
698
  if (params[0] === 'orders') return renderAdminOrders(app)
698
699
  if (params[0] === 'disputes') return renderAdminDisputes(app)
@@ -1012,6 +1013,40 @@ function preLaunchBannerHTML() {
1012
1013
  </div>`
1013
1014
  }
1014
1015
 
1016
+ // 账户无任何恢复方式 → 首页顶部持续红色风险横幅。
1017
+ // 恢复方式 = 密码 OR 已验证邮箱。Passkey【不算】恢复方式:它是敏感操作的"真人在场"门,
1018
+ // 没有 Passkey 登录 / 找回路径,丢 key 换设备后 Passkey 救不回账号(与弹窗"增强不替代恢复邮箱"一致)。
1019
+ function recoveryBannerHTML() {
1020
+ const u = state.user
1021
+ if (!u || u.role === 'admin') return ''
1022
+ const hasRecovery = u.has_password || u.email_verified
1023
+ if (hasRecovery) return ''
1024
+ 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">
1025
+ 🚨 <strong>${t('账户还没有恢复方式')}</strong> — ${t('闪退或换设备清缓存后可能永久无法登录。')}
1026
+ <div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
1027
+ <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>
1028
+ </div>
1029
+ </div>`
1030
+ }
1031
+
1032
+ // 卖家后台安全提醒(P1):卖家涉及商品/履约/钱包,恢复方式不齐尤其危险。
1033
+ // 全局红横幅(零恢复)已在 shell 顶部覆盖;此处补"有邮箱但还缺密码"等部分缺口的黄色软提醒,避免与红横幅叠加。
1034
+ function sellerRecoveryReminderHTML() {
1035
+ const u = state.user
1036
+ if (!u) return ''
1037
+ // 全局红横幅判定一致:无密码且无邮箱 → 红横幅已覆盖,这里不再叠加(Passkey 不算恢复方式)
1038
+ const globalRedShowing = !u.has_password && !u.email_verified
1039
+ if (globalRedShowing) return ''
1040
+ const gaps = []
1041
+ if (!u.has_password) gaps.push(t('未设置登录密码'))
1042
+ if (!u.email_verified) gaps.push(t('未绑定找回邮箱'))
1043
+ if (gaps.length === 0) return ''
1044
+ return `<div class="alert" style="background:#fffbeb;border:1px solid #fde68a;color:#92400e;font-size:12px;line-height:1.6;margin-bottom:12px">
1045
+ 🛡 <strong>${t('建议补全账户恢复方式')}</strong>:${gaps.join(' · ')}。${t('卖家涉及商品/履约/钱包,闪退或换设备清缓存后可能无法登录。')}
1046
+ <button class="btn btn-sm" style="font-size:11px;padding:3px 10px;margin-left:6px;background:#d97706;color:#fff;border:none;border-radius:6px;cursor:pointer" onclick="navigate('#me/settings')">${t('去设置')} →</button>
1047
+ </div>`
1048
+ }
1049
+
1015
1050
  function shell(content, activeTab, opts) {
1016
1051
  // opts = { hideTabbar?: bool, bottomBar?: html }(第三参数可选,向后兼容)
1017
1052
  const _opts = opts || {}
@@ -1116,7 +1151,7 @@ function shell(content, activeTab, opts) {
1116
1151
  : `<button class="btn btn-primary btn-sm" onclick="navigate('#login')">${t('登录')}</button>`}
1117
1152
  </div>
1118
1153
  </nav>
1119
- <main class="main">${content}</main>
1154
+ <main class="main">${recoveryBannerHTML()}${content}</main>
1120
1155
  ${state.user?.role === 'buyer' ? `
1121
1156
  <button id="compare-fab" onclick="openCompare()" title="${t('对比商品')}"
1122
1157
  style="position:fixed;bottom:136px;right:14px;background:#4f46e5;color:#fff;border:none;cursor:pointer;font-size:12px;font-weight:600;padding:8px 14px;border-radius:99px;box-shadow:0 4px 12px rgba(79,70,229,0.3);z-index:98;display:none;align-items:center;gap:4px">
@@ -1311,7 +1346,9 @@ async function renderMyAdvanced(app) {
1311
1346
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
1312
1347
  ${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
1313
1348
  ${card('⚖', t('判例库'), t('争议判决公开'), '#judgments')}
1314
- ${card('🎁', t('分享分润身份'), t('申请制 · 含锁仓金'), '#rewards-me')}
1349
+ ${card('🛠', t('我的共建'), t('贡献 / GitHub 认领 / 建设信誉 — 无购买门槛'), '#my-contributions')}
1350
+ ${card('📋', t('公开共建任务'), t('浏览可认领任务、提交建议、参与共建'), '#contribute/tasks')}
1351
+ ${card('🎁', t('分享分润管理'), t('分享佣金 / PV / escrow · 经济关系登记'), '#rewards-me')}
1315
1352
  </div>
1316
1353
 
1317
1354
  <div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📧 ${t('联系我们')}</div>
@@ -3113,9 +3150,14 @@ async function renderAdminTaskProposals(app) {
3113
3150
  const T = (zh, e) => en && e ? e : zh
3114
3151
  app.innerHTML = shell(loading$(), 'admin')
3115
3152
  const sf = state._proposalStatus || '' // '' | new | needs_info | rejected | converted
3116
- const r = await GET('/admin/task-proposals' + (sf ? '?status=' + encodeURIComponent(sf) : ''))
3153
+ const [r, dr] = await Promise.all([
3154
+ GET('/admin/task-proposals' + (sf ? '?status=' + encodeURIComponent(sf) : '')),
3155
+ GET('/admin/build-task-drafts'),
3156
+ ])
3117
3157
  if (r.error) { app.innerHTML = shell(alert$('error', r.error), 'admin'); return }
3118
3158
  const proposals = r.proposals || []
3159
+ const drafts = (dr && dr.drafts) || []
3160
+ const draftedIds = new Set(drafts.map((d) => d.source_proposal_id).filter(Boolean)) // proposals that already have an unpublished draft
3119
3161
  const notice = en ? (r.value_boundary?.notice_en || '') : (r.value_boundary?.notice_zh || '')
3120
3162
  const STATUS = {
3121
3163
  new: { bg: '#fef3c7', fg: '#92400e', label: T('待审', 'New') },
@@ -3126,6 +3168,26 @@ async function renderAdminTaskProposals(app) {
3126
3168
  const badge = (s) => { const c = STATUS[s] || { bg: '#f3f4f6', fg: '#6b7280', label: s }; return `<span style="font-size:10px;background:${c.bg};color:${c.fg};padding:2px 8px;border-radius:99px;font-weight:600">${c.label}</span>` }
3127
3169
  const chip = (val, label) => `<button onclick="setProposalStatusFilter('${val}')" style="padding:5px 12px;border-radius:99px;font-size:11px;cursor:pointer;border:1px solid ${sf === val ? '#6366f1' : '#e5e7eb'};background:${sf === val ? '#eef2ff' : '#fff'};color:${sf === val ? '#4338ca' : '#6b7280'};font-weight:600">${label}</button>`
3128
3170
  const field = (label, val) => val ? `<div style="font-size:12px;color:#374151;margin-top:4px"><b>${label}:</b> ${escHtml(String(val))}</div>` : ''
3171
+ // inline "create formal task draft" form (prefilled from the proposal; AI can also prefill it). All list
3172
+ // fields are newline-separated. These are the agent-handoff fields the formal task model requires.
3173
+ const ta = (id, ph, val, h) => `<textarea id="${id}" placeholder="${ph}" style="width:100%;box-sizing:border-box;min-height:${h || 38}px;padding:6px 8px;border:1px solid #d4d4d8;border-radius:6px;font-size:12px;margin-top:6px">${val ? escHtml(String(val)) : ''}</textarea>`
3174
+ const draftForm = (p) => `<div id="df-${escHtml(p.id)}" style="display:none;margin-top:10px;border:1px dashed #c7d2fe;background:#f5f7ff;border-radius:8px;padding:10px">
3175
+ <div style="font-size:11px;color:#4338ca;font-weight:600;margin-bottom:2px">${T('建正式任务草稿(未发布)', 'Create formal task draft (unpublished)')}</div>
3176
+ <div style="font-size:10px;color:#6b7280;margin-bottom:4px">${T('草稿默认隐藏不可认领;填齐 agent 交接字段后由人工显式「发布」才进任务板。', 'A draft is hidden + unclaimable; only an explicit human “Publish” (after the agent-handoff fields are filled) puts it on the board.')}</div>
3177
+ ${ta('df-title-' + escHtml(p.id), T('标题', 'Title'), p.title)}
3178
+ ${ta('df-area-' + escHtml(p.id), T('领域(可选)', 'Area (optional)'), p.suggested_area, 30)}
3179
+ ${ta('df-source-' + escHtml(p.id), T('来源引用(文件 / RFC / issue,可选)', 'Source ref (file / RFC / issue, optional)'), p.source_ref, 30)}
3180
+ ${ta('df-desc-' + escHtml(p.id), T('说明 / 原因', 'Summary / reason'), p.summary, 48)}
3181
+ ${ta('df-allowed-' + escHtml(p.id), T('允许路径(每行一条)', 'Allowed paths (one per line)'), '')}
3182
+ ${ta('df-fpaths-' + escHtml(p.id), T('禁止路径(每行一条)', 'Forbidden paths (one per line)'), '')}
3183
+ ${ta('df-forbidden-' + escHtml(p.id), T('禁止动作(每行一条)', 'Forbidden actions (one per line)'), '')}
3184
+ ${ta('df-accept-' + escHtml(p.id), T('验收标准(每行一条)', 'Acceptance criteria (one per line)'), p.expected_outcome)}
3185
+ ${ta('df-verify-' + escHtml(p.id), T('验证命令(每行一条)', 'Verification commands (one per line)'), '')}
3186
+ ${ta('df-deliver-' + escHtml(p.id), T('交付物(每行一条)', 'Deliverables (one per line)'), '')}
3187
+ ${ta('df-dod-' + escHtml(p.id), T('完成定义', 'Definition of done'), '')}
3188
+ ${ta('df-expect-' + escHtml(p.id), T('预期结果(留空则用说明)', 'Expected results (blank = use summary)'), '')}
3189
+ <button onclick="createTaskDraft('${escHtml(p.id)}')" style="margin-top:8px;padding:7px 14px;border:none;background:#4338ca;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">${T('保存草稿', 'Save draft')}</button>
3190
+ </div>`
3129
3191
  const row = (p) => {
3130
3192
  const terminal = p.status === 'rejected' || p.status === 'converted'
3131
3193
  return `<div class="card" style="padding:14px;margin-bottom:10px">
@@ -3146,13 +3208,29 @@ async function renderAdminTaskProposals(app) {
3146
3208
  <textarea id="pr-note-${escHtml(p.id)}" placeholder="${T('审阅备注(可选)', 'Review note (optional)')}" style="width:100%;box-sizing:border-box;min-height:44px;padding:6px 8px;border:1px solid #d4d4d8;border-radius:6px;font-size:12px"></textarea>
3147
3209
  <input id="pr-ref-${escHtml(p.id)}" placeholder="${T('转任务时:关联正式 task / PR / release(可选)', 'On convert: link the real task / PR / release (optional)')}" style="width:100%;box-sizing:border-box;margin-top:6px;padding:6px 8px;border:1px solid #d4d4d8;border-radius:6px;font-size:12px">
3148
3210
  <div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap">
3211
+ <button onclick="aiAssistProposal('${escHtml(p.id)}')" style="padding:6px 12px;border:1px solid #8b5cf6;background:#fff;color:#6d28d9;border-radius:6px;font-size:12px;cursor:pointer">🤖 ${T('AI 建议', 'AI suggest')}</button>
3212
+ ${draftedIds.has(p.id)
3213
+ ? `<span style="padding:6px 10px;font-size:11px;color:#4338ca;background:#eef2ff;border-radius:6px">📝 ${T('已建草稿(在上方草稿区发布;发布即接受)', 'Draft created — publish it in the drafts panel above (publish = accept)')}</span>`
3214
+ : `<button onclick="toggleDraftForm('${escHtml(p.id)}')" style="padding:6px 12px;border:1px solid #6366f1;background:#fff;color:#4338ca;border-radius:6px;font-size:12px;cursor:pointer">${T('建任务草稿', 'Create task draft')}</button>
3149
3215
  <button onclick="reviewProposal('${escHtml(p.id)}','needs_info')" style="padding:6px 12px;border:1px solid #3b82f6;background:#fff;color:#1e40af;border-radius:6px;font-size:12px;cursor:pointer">${T('需补充', 'Needs info')}</button>
3150
3216
  <button onclick="reviewProposal('${escHtml(p.id)}','rejected')" style="padding:6px 12px;border:1px solid #ef4444;background:#fff;color:#991b1b;border-radius:6px;font-size:12px;cursor:pointer">${T('拒绝', 'Reject')}</button>
3151
- <button onclick="reviewProposal('${escHtml(p.id)}','converted')" style="padding:6px 12px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;cursor:pointer">${T('转为正式任务', 'Convert')}</button>
3217
+ <button onclick="reviewProposal('${escHtml(p.id)}','converted')" style="padding:6px 12px;border:1px solid #16a34a;background:#fff;color:#166534;border-radius:6px;font-size:12px;cursor:pointer">${T('仅记审阅决定', 'Mark reviewed')}</button>`}
3152
3218
  </div>
3219
+ <div id="ai-${escHtml(p.id)}"></div>
3220
+ ${draftedIds.has(p.id) ? '' : draftForm(p)}
3153
3221
  </div>`}
3154
3222
  </div>`
3155
3223
  }
3224
+ const draftRow = (d) => `<div class="card" style="padding:12px;margin-bottom:8px;border-left:3px solid #6366f1">
3225
+ <div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
3226
+ <div style="font-weight:600;font-size:13px">${escHtml(d.title)}</div>
3227
+ <span style="font-size:10px;background:#eef2ff;color:#4338ca;padding:2px 8px;border-radius:99px;font-weight:600">${T('未发布草稿', 'Unpublished draft')}</span>
3228
+ </div>
3229
+ ${field(T('风险', 'Risk'), d.risk_level)}${field(T('可自助认领', 'Auto-claimable'), d.auto_claimable === 1 || d.auto_claimable === true ? T('是', 'yes') : T('否(需真人)', 'no (human)'))}
3230
+ ${field(T('来源建议', 'Source proposal'), d.source_proposal_id)}${field(T('创建人', 'Created by'), d.created_by)}
3231
+ <div style="font-size:10px;color:#9ca3af;margin-top:6px">${T('发布前会校验交接字段;发布后进入正常任务板,可被参与者 agent 发现 / 认领 / 提交 PR。', 'Publish validates the handoff fields; once published it enters the normal task board — discoverable / claimable / PR-submittable by participant agents.')}</div>
3232
+ <button onclick="publishDraft('${escHtml(d.id)}')" style="margin-top:8px;padding:6px 14px;border:none;background:#16a34a;color:#fff;border-radius:6px;font-size:12px;cursor:pointer;font-weight:600">${T('发布到任务板', 'Publish to board')}</button>
3233
+ </div>`
3156
3234
  app.innerHTML = shell(`
3157
3235
  <div style="padding:14px;max-width:920px;margin:0 auto">
3158
3236
  <h1 class="page-title">🛠️ ${T('任务建议收件箱', 'Task Proposal Inbox')}</h1>
@@ -3160,6 +3238,10 @@ async function renderAdminTaskProposals(app) {
3160
3238
  ${T('建议是陌生人 / agent 提交的想法,不是贡献事实 / 奖励 / 正式参与。「转为正式任务」只记录评审决定与证据链(proposer → reviewer → 关联引用),不会自动创建 build_task。', 'A proposal is a stranger / agent suggestion — NOT a contribution fact / reward / participation. “Convert” only records the review decision + the proposer → reviewer → ref evidence chain; it does NOT auto-create a build_task.')}
3161
3239
  ${notice ? `<br>${escHtml(notice)}` : ''}
3162
3240
  </div>
3241
+ ${drafts.length ? `<div style="margin-bottom:14px">
3242
+ <div style="font-size:12px;font-weight:700;color:#4338ca;margin-bottom:6px">📝 ${T('未发布任务草稿', 'Unpublished task drafts')} (${drafts.length})</div>
3243
+ ${drafts.map(draftRow).join('')}
3244
+ </div>` : ''}
3163
3245
  <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px">
3164
3246
  ${chip('', T('全部', 'All'))}${chip('new', STATUS.new.label)}${chip('needs_info', STATUS.needs_info.label)}${chip('rejected', STATUS.rejected.label)}${chip('converted', STATUS.converted.label)}
3165
3247
  </div>
@@ -3179,6 +3261,56 @@ window.reviewProposal = async (id, status) => {
3179
3261
  toast$(window._lang === 'en' ? 'Updated' : '已更新')
3180
3262
  renderAdminTaskProposals(document.getElementById('app'))
3181
3263
  }
3264
+ window.toggleDraftForm = (id) => { const el = document.getElementById('df-' + id); if (el) el.style.display = el.style.display === 'none' ? 'block' : 'none' }
3265
+ // AI-assist is ASSISTANT-ONLY: it renders a suggestion + prefills the draft form; it never publishes/decides.
3266
+ window.aiAssistProposal = async (id) => {
3267
+ const en = window._lang === 'en'
3268
+ const box = document.getElementById('ai-' + id); if (box) box.innerHTML = `<div style="font-size:11px;color:#8b5cf6;margin-top:8px">🤖 ${en ? 'thinking…' : '分析中…'}</div>`
3269
+ const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/ai-assist', {})
3270
+ if (r.error) { if (box) box.innerHTML = ''; toast$(r.error || 'failed'); return }
3271
+ const s = r.ai_suggestion || {}
3272
+ window._aiSuggest = window._aiSuggest || {}; window._aiSuggest[id] = s.suggested || {}
3273
+ const list = (arr) => (arr || []).map(x => '• ' + escHtml(String(x))).join('<br>')
3274
+ if (box) box.innerHTML = `<div style="margin-top:8px;border:1px solid #ddd6fe;background:#faf5ff;border-radius:8px;padding:10px">
3275
+ <div style="font-size:11px;font-weight:700;color:#6d28d9">🤖 ${en ? 'AI suggestion' : 'AI 建议'} <span style="font-weight:400;color:#9ca3af">(${escHtml(String(r.model || ''))})</span></div>
3276
+ <div style="font-size:10px;color:#b45309;margin:2px 0 6px">${escHtml(String(r.ai_notice || ''))}</div>
3277
+ <div style="font-size:12px;color:#374151;line-height:1.6">
3278
+ <b>${en ? 'Category' : '分类'}:</b> ${escHtml(String(s.category || ''))} · <b>${en ? 'Risk' : '风险'}:</b> ${escHtml(String(s.risk || ''))} · <b>${en ? 'Effort' : '工作量'}:</b> ${escHtml(String(s.effort || ''))} · <b>${en ? 'Duplicate' : '疑似重复'}:</b> ${escHtml(String(s.duplicate_likelihood || ''))}
3279
+ ${(s.missing_info && s.missing_info.length) ? `<div style="margin-top:4px"><b>${en ? 'Missing info' : '缺失信息'}:</b><br>${list(s.missing_info)}</div>` : ''}
3280
+ </div>
3281
+ <button onclick="applyAiToDraft('${id}')" style="margin-top:8px;padding:5px 12px;border:1px solid #8b5cf6;background:#fff;color:#6d28d9;border-radius:6px;font-size:11px;cursor:pointer">${en ? 'Fill draft form with this' : '用此填充草稿表单'}</button>
3282
+ </div>`
3283
+ }
3284
+ window.applyAiToDraft = (id) => {
3285
+ const suggested = (window._aiSuggest && window._aiSuggest[id]) || {}
3286
+ document.getElementById('df-' + id).style.display = 'block'
3287
+ const set = (sfx, v) => { const el = document.getElementById('df-' + sfx + '-' + id); if (el && v != null && v !== '') el.value = v }
3288
+ set('title', suggested.title); set('area', suggested.area); set('desc', suggested.description)
3289
+ set('accept', (suggested.acceptance_criteria || []).join('\n')); set('verify', (suggested.verification_commands || []).join('\n'))
3290
+ toast$(window._lang === 'en' ? 'Draft prefilled (review before saving)' : '已填充草稿(保存前请人工核对)')
3291
+ }
3292
+ window.createTaskDraft = async (id) => {
3293
+ const en = window._lang === 'en'
3294
+ const v = (sfx) => (document.getElementById('df-' + sfx + '-' + id)?.value || '').trim()
3295
+ const lines = (sfx) => v(sfx).split('\n').map(x => x.trim()).filter(Boolean)
3296
+ const body = {
3297
+ title: v('title'), area: v('area') || null, source_ref: v('source') || null, description: v('desc'),
3298
+ allowed_paths: lines('allowed'), forbidden_paths: lines('fpaths'), forbidden_actions: lines('forbidden'),
3299
+ acceptance_criteria: lines('accept'), verification_commands: lines('verify'), deliverables: lines('deliver'),
3300
+ definition_of_done: v('dod'), expected_results: v('expect'),
3301
+ }
3302
+ const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/create-task-draft', body)
3303
+ if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Missing: ' : '缺少:') + r.missing.join(', ')) : (r.error || 'failed')); return }
3304
+ toast$(en ? 'Draft saved (unpublished)' : '草稿已保存(未发布)')
3305
+ renderAdminTaskProposals(document.getElementById('app'))
3306
+ }
3307
+ window.publishDraft = async (taskId) => {
3308
+ const en = window._lang === 'en'
3309
+ const r = await POST('/admin/build-task-drafts/' + encodeURIComponent(taskId) + '/publish', {})
3310
+ if (r.error) { toast$((r.missing && r.missing.length) ? ((en ? 'Fill before publish: ' : '发布前请填齐:') + r.missing.join(', ')) : (r.error || 'failed')); return }
3311
+ toast$(en ? 'Published to task board' : '已发布到任务板')
3312
+ renderAdminTaskProposals(document.getElementById('app'))
3313
+ }
3182
3314
 
3183
3315
  async function renderAdminKPI(app) {
3184
3316
  if (!state.user) { renderLogin(); return }
@@ -3383,6 +3515,11 @@ async function renderAdminDashboard(app) {
3383
3515
  <div style="font-size:13px;color:#6b7280;margin:16px 0 8px">⚛ ${t('Tokenomics')}</div>
3384
3516
  <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
3385
3517
  ${quickAction('#admin/tokenomics', '⚛', t('积分基金 / Tier 配置 / 高额榜'))}
3518
+ </div>
3519
+ <div style="font-size:13px;color:#6b7280;margin:16px 0 8px">🔐 ${t('安全与审计')}</div>
3520
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-bottom:16px">
3521
+ ${quickAction('#admin/security', '🪪', t('我的管理身份与权限'))}
3522
+ ${quickAction('#admin/audit', '📜', t('审计日志'))}
3386
3523
  </div>`
3387
3524
 
3388
3525
  // A5 重设:渐变标题 + 分区标题 + 颜色块分组
@@ -3437,6 +3574,90 @@ function renderTagBadges(tags, max = 3) {
3437
3574
  }).join('') + (more > 0 ? `<span style="font-size:11px;color:#6b7280">+${more}</span>` : '')
3438
3575
  }
3439
3576
 
3577
+ // 管理身份与权限自查面板(只读)。回答"我正在以什么身份/级别/权限操作?",
3578
+ // Passkey 责任绑定状态 + GitHub 关联 + 普通 admin vs root/破玻璃 + 经济操作审计须知。
3579
+ // 纯前端:数据来自 /me(state.user)+ 只读 /contribution-identity/github/me;无新后端、无经济动作。
3580
+ async function renderAdminSecurity(app) {
3581
+ if (!state.user) { renderLogin(); return }
3582
+ if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
3583
+ app.innerHTML = shell(loading$(), 'admin')
3584
+ const u = state.user
3585
+ const gid = await GET('/contribution-identity/github/me').catch(() => null)
3586
+ const bindings = (gid && !gid.error && Array.isArray(gid.bindings)) ? gid.bindings : []
3587
+
3588
+ const adminType = u.admin_type || 'root'
3589
+ const isRoot = adminType === 'root'
3590
+ const scope = u.admin_scope || 'global'
3591
+ let perms = []
3592
+ try { perms = isRoot ? ['all'] : JSON.parse(u.admin_permissions || '[]') } catch { perms = [] }
3593
+ const hasPasskey = !!u.has_passkey
3594
+
3595
+ const PERM_LABEL = () => ({ all: t('全部'), users: t('用户'), content: t('内容'), arbitration: t('仲裁'), protocol: t('协议 / 经济'), verifier_mgmt: t('审核员管理'), support: t('支持') })
3596
+ const permChips = (perms.length === 0)
3597
+ ? `<span style="font-size:12px;color:#dc2626">${t('无任何权限(请联系 root 配置)')}</span>`
3598
+ : perms.map(p => `<span style="display:inline-block;background:#eef2ff;color:#3730a3;font-size:11px;padding:2px 8px;border-radius:99px;margin:0 4px 4px 0">${PERM_LABEL()[p] || p}</span>`).join('')
3599
+
3600
+ const row = (label, value) => `<div style="display:flex;justify-content:space-between;gap:10px;padding:7px 0;border-bottom:1px solid #f3f4f6"><span style="font-size:12px;color:#6b7280">${label}</span><span style="font-size:12px;color:#111827;text-align:right;word-break:break-all">${value}</span></div>`
3601
+
3602
+ const passkeyRow = hasPasskey
3603
+ ? `<span style="color:#16a34a;font-weight:600">✓ ${t('已绑定')}</span>`
3604
+ : `<span style="color:#dc2626;font-weight:600">⚠ ${t('未绑定')}</span> <a href="#me/settings" style="color:#6366f1;font-size:11px">${t('去绑定')} →</a>`
3605
+ const githubRow = bindings.length > 0
3606
+ ? bindings.map(b => `<code style="font-size:11px">github:${escHtml(String(b.github_actor_id))}</code>`).join(' ')
3607
+ : `<span style="color:#9ca3af">${t('未关联')}</span> <a href="#my-contributions" style="color:#6366f1;font-size:11px">${t('去认领')} →</a>`
3608
+
3609
+ app.innerHTML = shell(`
3610
+ ${adminPageHeader('🪪', t('我的管理身份与权限'), t('你正在以此身份操作 · 只读自查'))}
3611
+
3612
+ ${isRoot ? `
3613
+ <div class="card" style="padding:12px;background:#fffbeb;border:1px solid #fcd34d;margin-bottom:10px">
3614
+ <div style="font-size:13px;font-weight:700;color:#92400e">🚧 ${t('创始人 / 引导管理员(Founder Admin · Bootstrap Operator)')}</div>
3615
+ <div style="font-size:12px;color:#78350f;margin-top:4px;line-height:1.6">${t('这是 pre-launch 引导期的【临时治理模式】:更广的只读可见性 + 有限的应急写权限 —— 不是日常全能账号。')}</div>
3616
+ <div style="font-size:11px;color:#78350f;margin-top:6px;line-height:1.6">${t('设计目标:launch 后把创始人权力拆成更窄的角色 —— maintainer / support operator / arbitrator / finance reviewer / security admin(用 regional admin + 权限位逐步收窄)。')}</div>
3617
+ </div>` : ''}
3618
+
3619
+ <div class="card" style="padding:14px">
3620
+ <div style="font-size:13px;font-weight:700;margin-bottom:8px">👤 ${t('账户')}</div>
3621
+ ${row(t('名称'), escHtml(u.name || ''))}
3622
+ ${row(t('用户名'), '@' + escHtml(u.handle || ''))}
3623
+ ${row(t('账户 ID'), `<code style="font-size:11px">${escHtml(u.id || '')}</code>`)}
3624
+ </div>
3625
+
3626
+ <div class="card" style="padding:14px">
3627
+ <div style="font-size:13px;font-weight:700;margin-bottom:8px">🛡 ${t('角色与级别')}</div>
3628
+ ${row(t('角色'), t('管理员'))}
3629
+ ${row(t('级别'), isRoot
3630
+ ? `<span style="color:#b91c1c;font-weight:700">ROOT</span> · <span style="font-size:11px;color:#6b7280">${t('破玻璃 / 系统操作员')}</span>`
3631
+ : `<span style="color:#0369a1;font-weight:700">REGIONAL</span>`)}
3632
+ ${row(t('范围'), `<code style="font-size:11px">${escHtml(scope)}</code>`)}
3633
+ <div style="font-size:12px;color:#6b7280;margin-top:8px;margin-bottom:4px">${t('有效权限')}</div>
3634
+ <div>${permChips}</div>
3635
+ </div>
3636
+
3637
+ <div class="card" style="padding:14px">
3638
+ <div style="font-size:13px;font-weight:700;margin-bottom:8px">🔐 ${t('问责绑定')}</div>
3639
+ ${row('Passkey', passkeyRow)}
3640
+ ${row(t('GitHub 关联'), githubRow)}
3641
+ <div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:8px 10px;margin-top:8px;line-height:1.6">
3642
+ ${t('管理身份应绑定 Passkey(真人问责)。个人 GitHub 账号用于提交 PR;仓库所有权 / 设置由组织/管理身份治理 —— 独立审阅不应由同一人用另一账号假冒。')}
3643
+ </div>
3644
+ </div>
3645
+
3646
+ <div class="card" style="padding:14px">
3647
+ <div style="font-size:13px;font-weight:700;margin-bottom:8px">⚠️ ${t('操作安全须知')}</div>
3648
+ <div style="font-size:12px;color:#374151;line-height:1.8">
3649
+ • ${t('普通 admin 与 root / 破玻璃 不同:经济 / 协议级操作需 protocol 权限;按治理铁律须记入审计日志 —— 部分手动结算 / 评估入口的审计仍在补齐中。')}<br>
3650
+ • ${t('危险操作(封禁 / 角色 / 资金 / 协议参数)须带原因,且不可绕过争议 / 仲裁规则。')}<br>
3651
+ • ${t('不要在公共设备暴露 API Key;管理操作均可追溯到你的账户。')}
3652
+ </div>
3653
+ <div style="display:flex;gap:8px;margin-top:10px">
3654
+ <a href="#admin/audit" style="text-decoration:none"><button class="btn btn-outline btn-sm" style="font-size:12px">📜 ${t('查看审计日志')}</button></a>
3655
+ ${isRoot ? `<a href="#admin/manage-admins" style="text-decoration:none"><button class="btn btn-outline btn-sm" style="font-size:12px">👥 ${t('管理管理员')}</button></a>` : ''}
3656
+ </div>
3657
+ </div>
3658
+ `, 'admin')
3659
+ }
3660
+
3440
3661
  async function renderAdminUsers(app, opts = {}) {
3441
3662
  if (!state.user) { renderLogin(); return }
3442
3663
  if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin-users'); return }
@@ -4434,20 +4655,25 @@ async function renderApplyRewards(app) {
4434
4655
  <h1 class="page-title">🎁 ${t('申请分享分润')}</h1>
4435
4656
 
4436
4657
  <div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
4437
- <div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程与购物无关')}</div>
4658
+ <div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程不是购物流程;下方"已完成订单"门槛只是分享分润的反女巫要求')}</div>
4438
4659
  <div style="font-size:12px;color:#7f1d1d;line-height:1.6">
4439
- ${t('你可以随时退出,不影响任何已下单或未来订单。本流程涉及经济关系登记(三级佣金 + 积分配对),请仔细阅读全部条款。')}<br>
4440
- <span style="opacity:0.85">This flow is not part of shopping. You can leave anytime without affecting any orders. This is an economic-relationship registration — please read all terms.</span>
4660
+ ${t('你可以随时退出,不影响任何已下单或未来订单。本流程是分享分润的经济关系登记(佣金 / PV / escrow 结算规则;层级按地区配置),请仔细阅读全部条款。')}<br>
4661
+ <span style="opacity:0.85">This is not a shopping flow; the completed-order threshold below is only the anti-sybil requirement for share-commission. You can leave anytime without affecting any orders. This registers the share-commission economic relationship (commission / PV / escrow settlement rules; levels per region config) — please read all terms.</span>
4441
4662
  </div>
4442
4663
  <div style="font-size:11px;color:#7f1d1d;line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid #fca5a5;opacity:0.9">
4443
4664
  ${t('注:本「分享分润 / rewards opt-in」仅为 commission / PV / escrow 经济关系登记,不是贡献资格,与贡献任务(#contribute/tasks)/ 我的共建 无关。RFC-002 同意书可能沿用更早措辞,以本 UI 含义为准。')}<br>
4444
4665
  <span style="opacity:0.85">Note: this “share-commission / rewards opt-in” is only a commission / PV / escrow economic-relationship registration — NOT contribution eligibility, and unrelated to the contribution funnel (#contribute/tasks) / My contributions. RFC-002’s consent text may retain older wording; this note governs the UI meaning.</span>
4445
4666
  </div>
4667
+ <div style="font-size:11px;color:#7f1d1d;line-height:1.6;margin-top:8px;padding-top:8px;border-top:1px solid #fca5a5;opacity:0.9">
4668
+ ${t('现实性说明:佣金层级按地区合规配置生效;当前预发布期全局上限为 1 级(仅 L1)。"三级(7:2:1)"是协议最大设计,不构成对未来层级的承诺。')}<br>
4669
+ <span style="opacity:0.85">Reality note: commission levels follow per-region compliance config; during pre-launch a global cap of 1 level (L1 only) applies. “Three tiers (7:2:1)” is the protocol’s maximum design — not a promise of future levels.</span>
4670
+ </div>
4446
4671
  </div>
4447
4672
 
4448
4673
  <div class="card" style="margin-bottom:16px">
4449
- <div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('门槛(全部通过才能申请)')}:</div>
4674
+ <div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('分享分润开通门槛(只适用于分润,不适用于贡献)')}:</div>
4450
4675
  ${checklist}
4676
+ <div style="font-size:11px;color:#6b7280;margin-top:8px;padding-top:8px;border-top:1px solid #f3f4f6">${t('此购买门槛只适用于分享分润(经济关系登记),不适用于贡献任务或 GitHub 贡献认领 — 贡献无需购买。')}</div>
4451
4677
  </div>
4452
4678
 
4453
4679
  <div class="card" style="margin-bottom:16px;background:#fafafa">
@@ -5668,7 +5894,7 @@ async function renderAdminTokenomics(app) {
5668
5894
  <div style="padding:8px 12px;font-size:11px;color:#6b7280">${t('调整:POST /api/admin/tokenomics/tier with body {tier, pv_threshold, score_per_hit, active}')}</div>
5669
5895
  </div>
5670
5896
 
5671
- <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 三级佣金')} (Top 10)</h2>
5897
+ <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 分享佣金')} (Top 10)</h2>
5672
5898
  <div class="card" style="padding:0;margin-bottom:12px">${commRows}</div>
5673
5899
 
5674
5900
  <h2 style="font-size:15px;font-weight:600;margin:16px 0 8px">🏆 ${t('Top 匹配收益')} (Top 10)</h2>
@@ -5961,6 +6187,9 @@ function renderLogin() {
5961
6187
  function renderWelcome(app) {
5962
6188
  const en = window._lang === 'en'
5963
6189
  const T = (zh, e) => en ? e : zh
6190
+ // 创始白皮书(canonical founding doc)— 跟随界面语言指向 webaz.xyz 公开静态页(匿名可达,不走私有 GitHub)
6191
+ // 这两个路径由 scripts/build-whitepaper-html.ts 从 docs/WHITEPAPER*.md 生成,经 express.static 公开服务
6192
+ const WP_URL = en ? '/whitepaper/en' : '/whitepaper/zh-CN'
5964
6193
  // 2026-05-26 排版品控 — 区块标题 / 副标题 改为 inline style,避免 CSS 级联 / @media 覆写
5965
6194
  // 最低线:h2 ≥ 24px(手机) / 32px(桌面);sub ≥ 16px
5966
6195
  // ⚠️ 不用 calc(),避免 +/- 两侧空格被压掉导致整条 font-size 失效
@@ -6129,6 +6358,7 @@ function renderWelcome(app) {
6129
6358
  <p style="font-size:clamp(17px,2.6vh,20px);color:#71717A;line-height:${en ? '1.6' : '1.75'};margin:0;max-width:65ch;margin-left:auto;margin-right:auto">
6130
6359
  ${T('买家、卖家、创作者三位一体的去中心化商业协议', 'A decentralized commerce protocol where buyers, sellers, and creators are one.')}
6131
6360
  </p>
6361
+ <div style="margin-top:28px"><a href="${WP_URL}" target="_blank" rel="noopener" style="font-size:15px;color:#6366f1;text-decoration:none;font-weight:500">${T('📖 阅读创始白皮书:我参与,故我被看见 →', '📖 Read the founding whitepaper: I participate, therefore I am seen →')}</a></div>
6132
6362
  </section>
6133
6363
 
6134
6364
  <!-- 区块 1: Buyers -->
@@ -6356,7 +6586,7 @@ function renderWelcome(app) {
6356
6586
  <div class="w-card-desc">${T('已经准备好了?直接进入协议。', 'Ready already? Step into the protocol.')}</div>
6357
6587
  </div>
6358
6588
  <div class="w-join-card-right">
6359
- <button class="w-btn-full w-btn-primary" onclick="scrollToJoinWithRole('')">${T('立即注册', 'Sign Up Now')}</button>
6589
+ <button class="w-btn-full w-btn-primary" onclick="openAuthSheet('reg')">${T('立即注册', 'Sign Up Now')}</button>
6360
6590
  </div>
6361
6591
  </div>
6362
6592
  </div>
@@ -6369,7 +6599,7 @@ function renderWelcome(app) {
6369
6599
  <div style="margin-top:10px">
6370
6600
  <a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
6371
6601
  <a href="https://github.com/seasonsagents-art/webaz" target="_blank" rel="noopener">GitHub</a>
6372
- <a href="#">${T('协议白皮书', 'Whitepaper')}</a>
6602
+ <a href="${WP_URL}" target="_blank" rel="noopener">${T('协议白皮书', 'Whitepaper')}</a>
6373
6603
  <a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
6374
6604
  </div>
6375
6605
  </footer>
@@ -6715,13 +6945,33 @@ window.submitContributeProposal = async () => {
6715
6945
 
6716
6946
  const CONTRIBUTE_PAGE_STYLE = `max-width:880px;margin:0 auto;padding:24px 16px 80px;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','PingFang SC','Microsoft YaHei UI',sans-serif`
6717
6947
 
6948
+ window.contributeSetLang = (lang) => {
6949
+ if (lang !== 'zh' && lang !== 'en') return
6950
+ setLang(lang)
6951
+ document.getElementById('html-root')?.setAttribute('lang', lang === 'en' ? 'en' : 'zh-CN')
6952
+ route(true)
6953
+ }
6954
+
6955
+ function contributeLangSwitchHTML(T) {
6956
+ const en = window._lang === 'en'
6957
+ const btn = (lang, label, active) => `<button type="button" ${active ? 'disabled' : `onclick="contributeSetLang('${lang}')"`} style="border:0;background:${active ? '#18181B' : 'transparent'};color:${active ? '#fff' : '#71717A'};padding:5px 10px;border-radius:7px;font-size:12px;font-weight:600;cursor:${active ? 'default' : 'pointer'}">${label}</button>`
6958
+ return `<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 18px;padding-bottom:10px;border-bottom:1px solid #e4e4e7">
6959
+ <a href="#welcome" style="color:#52525B;text-decoration:none;font-size:13px;font-weight:700">← ${T('WebAZ 欢迎页', 'WebAZ Welcome')}</a>
6960
+ <div role="group" aria-label="${T('语言切换', 'Language switch')}" style="display:flex;align-items:center;gap:2px;border:1px solid #e4e4e7;border-radius:9px;padding:2px;background:#fff">
6961
+ ${btn('zh', '中文', !en)}${btn('en', 'EN', en)}
6962
+ </div>
6963
+ </div>`
6964
+ }
6965
+
6966
+ function contributePageShell(T, inner) {
6967
+ return `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">${contributeLangSwitchHTML(T)}${inner}</div>`
6968
+ }
6969
+
6718
6970
  async function renderContributeTasks(app) {
6719
6971
  const en = window._lang === 'en'
6720
6972
  const T = (zh, e) => en && e ? e : zh
6721
6973
  const q = state._urlQuery || {}
6722
- app.innerHTML = `
6723
- ${preLaunchBannerHTML()}
6724
- <div style="${CONTRIBUTE_PAGE_STYLE}">
6974
+ app.innerHTML = contributePageShell(T, `
6725
6975
  <header style="margin-bottom:20px">
6726
6976
  <h1 style="font-size:clamp(24px,5vw,32px);margin:0 0 8px;color:#18181B">🛠️ ${T('公开任务板', 'Open Task Board')}</h1>
6727
6977
  <p style="color:#52525B;font-size:15px;margin:0;line-height:1.6">${T('面向任何人和他们的 agent 的公开共建任务。只读取公开任务;认领需登录。', 'Open contribution tasks for anyone and their agents. Read-only here; claiming requires login.')}</p>
@@ -6752,7 +7002,7 @@ async function renderContributeTasks(app) {
6752
7002
  <button onclick="location.hash='#contribute/tasks/suggest'" style="padding:8px 16px;background:#fff;color:#18181B;border:1px solid #d4d4d8;border-radius:8px;font-size:13px;cursor:pointer">💡 ${T('建议新任务', 'Suggest a task')}</button>
6753
7003
  </div>
6754
7004
  <div id="ct-list"><div style="color:#a1a1aa;text-align:center;padding:30px">${T('加载中…', 'Loading…')}</div></div>
6755
- </div>`
7005
+ `)
6756
7006
 
6757
7007
  const params = new URLSearchParams()
6758
7008
  for (const k of ['area', 'risk_level', 'auto_claimable', 'agent_capabilities', 'max_duration_minutes', 'estimated_context_size', 'estimated_agent_budget']) if (q[k]) params.set(k, q[k])
@@ -6789,18 +7039,18 @@ async function renderContributeTasks(app) {
6789
7039
  async function renderContributeTaskDetail(app, id) {
6790
7040
  const en = window._lang === 'en'
6791
7041
  const T = (zh, e) => en && e ? e : zh
6792
- app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}"><div style="color:#a1a1aa;text-align:center;padding:40px">${T('加载中…', 'Loading…')}</div></div>`
7042
+ app.innerHTML = contributePageShell(T, `<div style="color:#a1a1aa;text-align:center;padding:40px">${T('加载中…', 'Loading…')}</div>`)
6793
7043
  let j, res
6794
7044
  try {
6795
7045
  res = await fetch('/api/public/build-tasks/' + encodeURIComponent(id), { signal: AbortSignal.timeout(10000) })
6796
7046
  j = await res.json().catch(() => ({}))
6797
7047
  } catch (e) {
6798
- app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}"><div style="color:#dc2626;text-align:center;padding:30px">${T('加载失败', 'Load failed')}: ${_cEsc(e.message || 'unknown')}</div></div>`; return
7048
+ app.innerHTML = contributePageShell(T, `<div style="color:#dc2626;text-align:center;padding:30px">${T('加载失败', 'Load failed')}: ${_cEsc(e.message || 'unknown')}</div>`); return
6799
7049
  }
6800
7050
  if (!res.ok || !j.task) {
6801
- app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">
7051
+ app.innerHTML = contributePageShell(T, `
6802
7052
  <div style="color:#52525B;text-align:center;padding:30px">${T('任务不存在或非公开。', 'Task not found or not public.')}</div>
6803
- <div style="text-align:center"><button onclick="location.hash='#contribute/tasks'" style="padding:8px 16px;background:#fff;border:1px solid #d4d4d8;border-radius:8px;cursor:pointer">← ${T('返回任务板', 'Back to board')}</button></div></div>`; return
7053
+ <div style="text-align:center"><button onclick="location.hash='#contribute/tasks'" style="padding:8px 16px;background:#fff;border:1px solid #d4d4d8;border-radius:8px;cursor:pointer">← ${T('返回任务板', 'Back to board')}</button></div>`); return
6804
7054
  }
6805
7055
  const task = j.task
6806
7056
  const cct = j.canonical_contribution_target || {}
@@ -6814,7 +7064,7 @@ async function renderContributeTaskDetail(app, id) {
6814
7064
  : `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#fff;color:#18181B;border:1px solid #6366f1;border-radius:8px;font-size:14px;cursor:pointer">🔑 ${T('登录后认领', 'Log in to claim')}</button><div style="font-size:11px;color:#6b7280;margin-top:6px">${T('认领需要登录(真人 Passkey 账户);浏览器不会自动执行 GitHub 操作。', 'Claiming requires login (a real Passkey account); the browser performs no automatic GitHub action.')}</div>`)
6815
7065
  : `<div style="font-size:12px;color:#6b7280">${T('此任务需人工认领流程,不可自动认领。', 'This task uses a manual claim flow; it is not auto-claimable.')}</div>`
6816
7066
 
6817
- app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">
7067
+ app.innerHTML = contributePageShell(T, `
6818
7068
  <button onclick="location.hash='#contribute/tasks'" style="background:none;border:none;color:#6366f1;cursor:pointer;font-size:13px;padding:0;margin-bottom:12px">← ${T('返回任务板', 'Back to board')}</button>
6819
7069
  <h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">${_cEsc(task.title)}</h1>
6820
7070
  <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:6px">
@@ -6857,7 +7107,7 @@ async function renderContributeTaskDetail(app, id) {
6857
7107
  ${section('✋', T('认领', 'Claim'), claimBtn)}
6858
7108
 
6859
7109
  <div style="font-size:11px;color:#9ca3af;margin-top:8px;line-height:1.6">${T('提示:sandbox 运行或本地草稿不是正式参与,也不是贡献。只有在 canonical 仓库被合并的 PR(或被认可的 issue / task / RFC)才进入贡献记录。', 'Note: a sandbox run or local draft is not participation and is not a contribution. Only a merged PR (or recognized issue / task / RFC) on the canonical repo enters the contribution record.')}</div>
6860
- </div>`
7110
+ `)
6861
7111
  }
6862
7112
 
6863
7113
  async function renderContributeSuggest(app) {
@@ -6868,7 +7118,7 @@ async function renderContributeSuggest(app) {
6868
7118
  ${ta
6869
7119
  ? `<textarea id="${id}" placeholder="${_cEsc(ph)}" style="width:100%;box-sizing:border-box;min-height:90px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px;font-family:inherit"></textarea>`
6870
7120
  : `<input id="${id}" placeholder="${_cEsc(ph)}" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">`}`
6871
- app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">
7121
+ app.innerHTML = contributePageShell(T, `
6872
7122
  <button onclick="location.hash='#contribute/tasks'" style="background:none;border:none;color:#6366f1;cursor:pointer;font-size:13px;padding:0;margin-bottom:12px">← ${T('返回任务板', 'Back to board')}</button>
6873
7123
  <h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">💡 ${T('建议一个任务', 'Suggest a task')}</h1>
6874
7124
  <p style="color:#52525B;font-size:14px;margin:0 0 8px;line-height:1.6">${T('任何人都可以提建议(无需登录)。建议进入维护者收件箱待审,绝不会自动变成正式任务或出现在公开任务板。', 'Anyone can suggest (no login needed). Suggestions enter the maintainer review inbox and never auto-become tasks or appear on the public board.')}</p>
@@ -6881,7 +7131,7 @@ async function renderContributeSuggest(app) {
6881
7131
  ${field('cs-login', T('你的 GitHub 用户名(可选)', 'Your GitHub login (optional)'), 'octocat')}
6882
7132
  <button onclick="submitContributeProposal()" style="margin-top:16px;padding:10px 22px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer">${T('提交建议', 'Submit suggestion')}</button>
6883
7133
  <div id="ct-suggest-result" style="margin-top:14px"></div>
6884
- </div>`
7134
+ `)
6885
7135
  }
6886
7136
 
6887
7137
  // The boundary constant is server-authoritative; the client mirrors the SAME frozen stance for pages that
@@ -7043,19 +7293,30 @@ window.openAuthSheet = (defaultTab) => {
7043
7293
  </div>
7044
7294
 
7045
7295
  <div style="margin-top:14px;text-align:center">
7046
- <a href="#recover" onclick="closeSheet()" style="font-size:13px;color:#6366f1;text-decoration:none">🔑 ${t('忘了密钥?点此找回 →')}</a>
7296
+ <a href="#recover" onclick="closeSheet()" style="font-size:13px;color:#6366f1;text-decoration:none">🔑 ${t('忘记 API Key / 密码?邮箱找回并重置 →')}</a>
7047
7297
  </div>
7048
7298
  </div>
7049
7299
 
7050
7300
  <div id="panel-reg" style="${defaultTab==='reg'?'':'display:none'}">
7051
7301
  <div id="reg-gate-hint" style="display:none;background:#fef3c7;border:1px solid #fde68a;padding:10px 12px;border-radius:8px;margin-bottom:12px;font-size:13px;color:#92400e"></div>
7302
+ <div class="form-group">
7303
+ <label class="form-label">${t('找回邮箱')} <span style="color:#dc2626">*</span> <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(先验证邮箱,丢号才能找回)')}</span></label>
7304
+ <div style="display:flex;gap:6px;align-items:stretch">
7305
+ <input class="form-control" id="inp-reg-email" type="email" placeholder="your@example.com" style="flex:1" oninput="window._onRegEmailInput && window._onRegEmailInput()">
7306
+ <button id="btn-reg-sendcode" type="button" onclick="doRegSendCode()" style="white-space:nowrap;padding:0 12px;background:#6366f1;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('发送验证码')}</button>
7307
+ </div>
7308
+ <div id="reg-code-row" class="form-group" style="display:none;margin-top:8px;margin-bottom:0">
7309
+ <input class="form-control" id="inp-reg-code" inputmode="numeric" maxlength="6" placeholder="${t('输入 6 位邮箱验证码')}">
7310
+ <div style="font-size:11px;color:#16a34a;margin-top:4px">${t('验证码已发送,请查收(含垃圾箱),10 分钟内有效')}</div>
7311
+ </div>
7312
+ </div>
7052
7313
  <div class="form-group">
7053
7314
  <label class="form-label">${t('邀请码')} <span style="color:#dc2626">*</span></label>
7054
7315
  <div style="display:flex;gap:6px;align-items:stretch">
7055
7316
  <input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;flex:1">
7056
7317
  <button id="btn-fetch-ref" type="button" disabled title="${t('该功能默认关闭,由管理员开启后可用')}" onclick="doFetchInviteCode()" style="white-space:nowrap;padding:0 12px;background:#e5e7eb;color:#9ca3af;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:not-allowed">${t('获取邀请码')}</button>
7057
7318
  </div>
7058
- <div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码,可带 -L/-R;没有就联系老用户拿邀请链接')}</div>
7319
+ <div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码;没有就联系老用户拿邀请链接')}</div>
7059
7320
  </div>
7060
7321
  <div class="form-group">
7061
7322
  <label class="form-label">${t('名称 / 店铺名')}</label>
@@ -7207,8 +7468,7 @@ async function renderPromoter(app) {
7207
7468
  // 邀请短链只用 permanent_code,绝不兜底 usr_xxx;缺失时显示"暂不可用"。
7208
7469
  const code = data.permanent_code || null
7209
7470
  const refLinkShort = code ? `${origin}/i/${code}` : ''
7210
- const leftLink = code ? `${origin}/i/${code}-L` : ''
7211
- const rightLink = code ? `${origin}/i/${code}-R` : ''
7471
+ // pre-public 去左右码:不再生成 -L/-R 侧链,只用唯一推荐码 refLinkShort
7212
7472
  const inviteUnavailableHtml = `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:12px;margin-bottom:12px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
7213
7473
  const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '&quot;')
7214
7474
 
@@ -7234,8 +7494,8 @@ async function renderPromoter(app) {
7234
7494
 
7235
7495
  <div style="font-size:11px;line-height:1.6;color:#6b7280;margin-bottom:8px">
7236
7496
  ${data.permissions?.can_l1_share
7237
- ? `<span style="color:#16a34a">✅ ${t('已获得分享奖励资格')}</span>` + (data.permissions.l1_share_override === 1 ? ` · <span style="color:#7c3aed">${t('Admin 强制授予')}</span>` : '')
7238
- : `<span style="color:#d97706">⏳ ${t('分享奖励待解锁')}</span> · <span>${t('完成首笔购买即可')}</span>`}
7497
+ ? `<span style="color:#16a34a">✅ ${t('已开通分享分润资格')}</span>` + (data.permissions.l1_share_override === 1 ? ` · <span style="color:#7c3aed">${t('Admin 强制授予')}</span>` : '')
7498
+ : `<span style="color:#d97706">⏳ ${t('分享分润待开通')}</span> · <span>${t('完成首笔购买即可')}</span>`}
7239
7499
  ${data.my_sponsor ? `<br>${t('邀请人')}: <strong>${escHtml(t(data.my_sponsor.name))}</strong>` : ''}
7240
7500
  ${_pvAllowed && atomic.my_placement ? ` · ${t('挂靠')}: <strong>${escHtml(t(atomic.my_placement.name))}</strong> ${atomic.my_placement.side === 'left' ? '🔵' : '🟢'}` : ''}
7241
7501
  · ${t('所在地区')}: ${regionLabel(data.region || 'global')}
@@ -7243,32 +7503,14 @@ async function renderPromoter(app) {
7243
7503
 
7244
7504
  ${!_pvAllowed ? '' : `
7245
7505
  <details style="margin-top:4px">
7246
- <summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('左右区设置')}</summary>
7506
+ <summary style="font-size:11px;color:#6366f1;cursor:pointer;padding:6px 0">⚙ ${t('自动放置设置')}</summary>
7247
7507
  <div style="padding:8px 0">
7248
- <div style="font-size:11px;color:#6b7280;margin-bottom:6px">${t('指定左/右轨')}</div>
7249
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:14px">
7250
- <div style="background:#eff6ff;border:1px solid #bfdbfe;border-radius:8px;padding:10px">
7251
- <div style="font-size:12px;font-weight:600;color:#1e40af;text-align:center;margin-bottom:8px">🔵 ${t('左区码')}</div>
7252
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
7253
- <button onclick="copyPlacementLink('${esc(leftLink)}', 'left', this)" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#3b82f6;color:#fff;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📋 ${t('复制')}</button>
7254
- <button onclick="showQRModal('${esc(leftLink)}','🔵 ${t('左区码')}')" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#fff;color:#3b82f6;border:1.5px solid #3b82f6;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📱 ${t('二维码')}</button>
7255
- </div>
7256
- </div>
7257
- <div style="background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;padding:10px">
7258
- <div style="font-size:12px;font-weight:600;color:#166534;text-align:center;margin-bottom:8px">🟢 ${t('右区码')}</div>
7259
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
7260
- <button onclick="copyPlacementLink('${esc(rightLink)}', 'right', this)" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#16a34a;color:#fff;border:none;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📋 ${t('复制')}</button>
7261
- <button onclick="showQRModal('${esc(rightLink)}','🟢 ${t('右区码')}')" style="display:flex;align-items:center;justify-content:center;gap:3px;padding:7px 4px;background:#fff;color:#16a34a;border:1.5px solid #16a34a;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer">📱 ${t('二维码')}</button>
7262
- </div>
7263
- </div>
7264
- </div>
7265
-
7266
- <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动选边偏好')}</div>
7508
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('新人通过你的推荐码注册后,系统自动安排积分树位置(无需选择左右)。')}</div>
7509
+ <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('自动放置依据')}</div>
7267
7510
  <select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
7268
7511
  <option value="team_count">${t('推荐少的一边(默认)')}</option>
7269
7512
  <option value="pv_count">${t('近 90 天积分少的一边')}</option>
7270
7513
  </select>
7271
- <p style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('💡 想为这一次分享强制走左/右轨?复制上方「🔵 左区推荐码 / 🟢 右区推荐码」直接发,仅当次有效,不会改长期偏好。')}</p>
7272
7514
  <div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
7273
7515
  </div>
7274
7516
  </details>`}
@@ -7765,24 +8007,12 @@ function renderAtomicInner(a, leftPv, rightPv, weak, nextTier, nextProgress) {
7765
8007
 
7766
8008
  ${renderBinaryTree(a.binary_tree)}
7767
8009
 
7768
- <div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('弱侧匹配量')}: <strong>${weak.toLocaleString()}</strong> PV</div>
7769
- ${nextTier ? `
7770
- <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('距离 tier')} ${nextTier.tier} ${t('门槛')} ${Number(nextTier.pv_threshold).toLocaleString()} PV (+${nextTier.score_per_hit} Score)</div>
7771
- <div style="background:#f3f4f6;height:6px;border-radius:3px;overflow:hidden;margin-bottom:10px">
7772
- <div style="background:#6366f1;height:6px;width:${nextProgress.toFixed(1)}%"></div>
7773
- </div>` : ''}
7774
-
7775
- <details style="margin-bottom:10px">
7776
- <summary style="font-size:12px;color:#6366f1;cursor:pointer">${t('匹配档位表')}</summary>
7777
- <table style="width:100%;margin-top:6px;border-collapse:collapse">
7778
- <tr style="background:#f9fafb;font-size:11px;color:#6b7280">
7779
- <th style="padding:5px 8px;text-align:left">Tier</th>
7780
- <th style="padding:5px 8px;text-align:right">${t('门槛 PV')}</th>
7781
- <th style="padding:5px 8px;text-align:right">${t('Score / 次')}</th>
7782
- </tr>
7783
- ${tierTable}
7784
- </table>
7785
- </details>
8010
+ <!-- pre-public de-MLM: 弱腿/对碰 tier 进度 + Score/次 档位表已下线;PV/位置仅为参与记录,非收益路径 -->
8011
+ <div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
8012
+ ${window._lang === 'en'
8013
+ ? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
8014
+ : 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
8015
+ </div>
7786
8016
  <h4 style="font-size:12px;font-weight:600;margin:6px 0">📊 ${t('最近匹配')}</h4>
7787
8017
  ${recentRows}`
7788
8018
  }
@@ -7950,38 +8180,29 @@ function renderAtomicSection(a) {
7950
8180
  <h2 style="font-size:15px;font-weight:600;margin:24px 0 8px">⚛ ${t('积分 — 积分匹配')}</h2>
7951
8181
 
7952
8182
  <div class="card" style="margin-bottom:12px;background:linear-gradient(135deg,#dbeafe,#f0fdf4)">
7953
- <div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('左右码(完全对称 / 末端垂直挂靠)')}</div>
7954
- <div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('点击下方任一卡片复制整条推荐链接。新人通过左/右链接注册会挂到对应区。')}</div>
8183
+ <div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('我的推荐码')}</div>
8184
+ <div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('复制你的推荐链接分享给新人;新人注册后由系统自动安排积分树位置(无需选择左右)。')}</div>
7955
8185
  ${(() => {
7956
- // placement 短链只用 permanent_code(/i/CODE-L / -R),绝不用 usr_xxx;缺失则提示不可用
8186
+ // pre-public 去左右码:只用唯一的推荐码(/i/CODE,不带 -L/-R);缺失则提示不可用
7957
8187
  const myCode = state.user?.permanent_code || null
7958
8188
  const origin = location.origin
7959
8189
  if (!myCode) return `<div style="background:#fef2f2;border:1px solid #fca5a5;border-radius:10px;padding:10px;font-size:12px;color:#991b1b">⚠️ ${t('邀请码暂不可用,请刷新或联系支持')}</div>`
7960
- const leftLink = `${origin}/i/${myCode}-L`
7961
- const rightLink = `${origin}/i/${myCode}-R`
7962
- const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '&quot;')
8190
+ const link = `${origin}/i/${myCode}`
8191
+ const esc = (s) => s.replace(/'/g, "\\'")
7963
8192
  return `
7964
- <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
7965
- <div onclick="copyPlacementLink('${esc(leftLink)}', 'left', this)" style="cursor:pointer;background:#eff6ff;border:2px solid #3b82f6;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#dbeafe'" onmouseout="this.style.background='#eff6ff'">
7966
- <div style="font-size:13px;font-weight:700;color:#1e40af;margin-bottom:6px">🔵 ${t('左区推荐码')}</div>
7967
- <div style="font-size:10px;color:#1e40af;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(leftLink)}</div>
7968
- <div style="font-size:11px;color:#3b82f6;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
7969
- </div>
7970
- <div onclick="copyPlacementLink('${esc(rightLink)}', 'right', this)" style="cursor:pointer;background:#f0fdf4;border:2px solid #16a34a;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#dcfce7'" onmouseout="this.style.background='#f0fdf4'">
7971
- <div style="font-size:13px;font-weight:700;color:#166534;margin-bottom:6px">🟢 ${t('右区推荐码')}</div>
7972
- <div style="font-size:10px;color:#166534;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(rightLink)}</div>
7973
- <div style="font-size:11px;color:#16a34a;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
7974
- </div>
8193
+ <div onclick="copyRefLink('${esc(link)}')" style="cursor:pointer;background:#eef2ff;border:2px solid #6366f1;border-radius:10px;padding:10px;transition:all 0.15s" onmouseover="this.style.background='#e0e7ff'" onmouseout="this.style.background='#eef2ff'">
8194
+ <div style="font-size:13px;font-weight:700;color:#4338ca;margin-bottom:6px">${t('推荐链接')}</div>
8195
+ <div style="font-size:10px;color:#4338ca;word-break:break-all;line-height:1.4;background:#fff;padding:6px 8px;border-radius:6px;font-family:monospace">${escHtml(link)}</div>
8196
+ <div style="font-size:11px;color:#6366f1;margin-top:6px;text-align:center">${t('点击复制 →')}</div>
7975
8197
  </div>`
7976
8198
  })()}
7977
8199
  <details style="margin-top:10px">
7978
- <summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('自动选边偏好(无 side 的链接 / 商品分享适用)')}</summary>
8200
+ <summary style="font-size:11px;color:#6366f1;cursor:pointer">⚙ ${t('自动放置依据')}</summary>
7979
8201
  <div style="padding:8px 0">
7980
8202
  <select id="placement-pref-select" class="form-control" style="font-size:12px" onchange="doSetPlacementPref()">
7981
8203
  <option value="team_count">${t('推荐少的一边(默认)')}</option>
7982
8204
  <option value="pv_count">${t('近 90 天积分少的一边')}</option>
7983
8205
  </select>
7984
- <p style="font-size:11px;color:#9ca3af;margin-top:6px;line-height:1.5">${t('💡 想为这一次分享强制走左/右轨?复制上方「🔵 左区推荐码 / 🟢 右区推荐码」直接发,仅当次有效,不会改长期偏好。')}</p>
7985
8206
  <div id="placement-pref-msg" style="font-size:11px;color:#6b7280;margin-top:4px"></div>
7986
8207
  </div>
7987
8208
  </details>
@@ -8003,13 +8224,13 @@ function renderAtomicSection(a) {
8003
8224
 
8004
8225
  ${renderBinaryTree(a.binary_tree)}
8005
8226
 
8227
+ <!-- pre-public de-MLM: 弱腿/对碰 tier 进度已下线;PV/位置仅为参与记录,非收益路径 -->
8006
8228
  <div class="card" style="margin-bottom:12px">
8007
- <div style="font-size:13px;color:#6b7280;margin-bottom:6px">${t('弱侧匹配量')}: <strong>${pair.toLocaleString()}</strong> PV</div>
8008
- ${nextTier ? `
8009
- <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('距离 tier')} ${nextTier.tier} ${t('门槛')} ${Number(nextTier.pv_threshold).toLocaleString()} PV (${nextTier.score_per_hit} Score)</div>
8010
- <div style="background:#f3f4f6;height:6px;border-radius:3px;overflow:hidden">
8011
- <div style="background:#6366f1;height:6px;width:${nextProgress.toFixed(1)}%"></div>
8012
- </div>` : ''}
8229
+ <div style="font-size:11px;color:#9ca3af;line-height:1.6">
8230
+ ${window._lang === 'en'
8231
+ ? 'PV tiers / pairing are pre-launch and currently disabled. PV / placement is a participation record only — not an earning path or payout promise.'
8232
+ : 'PV 档位 / 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
8233
+ </div>
8013
8234
  </div>
8014
8235
 
8015
8236
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
@@ -8023,18 +8244,6 @@ function renderAtomicSection(a) {
8023
8244
  </div>
8024
8245
  </div>
8025
8246
 
8026
- <details style="margin-bottom:12px">
8027
- <summary style="font-size:12px;color:#6366f1;cursor:pointer">${t('匹配档位表')}</summary>
8028
- <table style="width:100%;margin-top:8px;border-collapse:collapse">
8029
- <tr style="background:#f9fafb;font-size:11px;color:#6b7280">
8030
- <th style="padding:6px 8px;text-align:left">Tier</th>
8031
- <th style="padding:6px 8px;text-align:right">${t('门槛 PV')}</th>
8032
- <th style="padding:6px 8px;text-align:right">${t('Score / 次')}</th>
8033
- </tr>
8034
- ${tierTable}
8035
- </table>
8036
- </details>
8037
-
8038
8247
  <h3 style="font-size:13px;font-weight:600;margin:8px 0">📊 ${t('最近匹配')}</h3>
8039
8248
  <div class="card" style="padding:0">
8040
8249
  ${recentRows}
@@ -8097,10 +8306,7 @@ window.showNodePvModal = async (userId) => {
8097
8306
  body.innerHTML = alert$('error', data.error)
8098
8307
  return
8099
8308
  }
8100
- const weak = data.weak_leg_pv || 0
8101
- const ratio = (data.total_left_pv + data.total_right_pv) > 0
8102
- ? (weak / Math.max(data.total_left_pv, data.total_right_pv) * 100).toFixed(0)
8103
- : '0'
8309
+ // pre-public de-MLM: 不再展示弱腿/对碰收益(weak-leg / pairing earnings)。PV / 位置仅为参与记录。
8104
8310
  body.innerHTML = `
8105
8311
  <div style="text-align:left">
8106
8312
  <div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f3f4f6">
@@ -8128,10 +8334,10 @@ window.showNodePvModal = async (userId) => {
8128
8334
  </div>
8129
8335
  </div>
8130
8336
 
8131
- <div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#6b7280;line-height:1.7;margin-bottom:10px">
8132
- ${t('弱侧匹配量')}: <strong style="color:#374151">${weak.toLocaleString()}</strong> PV · ${t('双腿均衡')} ${ratio}%<br>
8133
- ${t('累计匹配')}: <strong style="color:#374151">${data.total_hits || 0}</strong> ${t('次')} · ${t('累计获 WAZ')}: <strong style="color:#16a34a">${Number(data.settled_waz || 0).toFixed(2)}</strong><br>
8134
- ${t('待结 Score')}: <strong style="color:#d97706">${Number(data.pending_score || 0).toFixed(1)}</strong>
8337
+ <div style="background:#f9fafb;border-radius:6px;padding:8px 10px;font-size:11px;color:#9ca3af;line-height:1.6;margin-bottom:10px">
8338
+ ${window._lang === 'en'
8339
+ ? 'PV pairing is pre-launch and currently disabled. PV / placement is a participation record only not an earning path or payout promise.'
8340
+ : 'PV 对碰为 pre-launch 阶段、当前未启用。PV / 位置仅为参与记录,非收益路径或兑付承诺。'}
8135
8341
  </div>
8136
8342
 
8137
8343
  <a href="#u/${data.id}" onclick="closeModal()" style="display:block;text-align:center;font-size:12px;color:#4f46e5;text-decoration:none;padding:8px">→ ${t('查看 TA 的主页')}</a>
@@ -8187,7 +8393,7 @@ async function webShareOrCopy(opts) {
8187
8393
 
8188
8394
  window.copyRefLink = async (link) => {
8189
8395
  const meName = state.user?.name || t('一位老用户')
8190
- const text = t('我在 WebAZ 用 AI 比价下单,省钱还能拿分享佣金 + PV 双激励。通过我的链接注册有奖励,TA 也得佣金 — 双方共赢') + '\n— ' + meName
8396
+ const text = t('我在 WebAZ 用 AI 比价下单,体验不错,推荐你也试试。分享链接仅作参与 / 归因记录,不构成收益承诺。') + '\n— ' + meName
8191
8397
  const r = await webShareOrCopy({ title: 'WebAZ', text, url: link })
8192
8398
  if (r === 'shared') toast$(t('已分享'))
8193
8399
  else if (r === 'copied') toast$(t('已复制(含邀请文案)'))
@@ -8265,9 +8471,9 @@ async function maybePromptPlacementBind() {
8265
8471
  const st = await GET('/profile/placement-status')
8266
8472
  if (!st.can_bind) return // 已有 placement 或 有下线 → 不弹
8267
8473
  setTimeout(() => {
8268
- const ok = confirm(`📍 ${t('系统检测到邀请链接')}\n\n${t('inviter')}: ${inviter}\n\n${t('是否加入对方的积分配对?')}\n${t('系统将按推荐人偏好自动选择左/右区。一旦加入永久不变。')}`)
8474
+ const ok = confirm(`📍 ${t('系统检测到邀请链接')}\n\n${t('inviter')}: ${inviter}\n\n${t('是否加入对方的积分配对?')}\n${t('系统将自动安排积分树位置。一旦加入永久不变。')}`)
8269
8475
  if (ok) {
8270
- POST('/profile/bind-placement', { inviter_id: inviter, side: hint.placement_side || undefined }).then(res => {
8476
+ POST('/profile/bind-placement', { inviter_id: inviter }).then(res => {
8271
8477
  if (res.error) alert(`✗ ${res.error}`)
8272
8478
  else alert(`✓ ${t('已加入积分树')}\n${t('侧')}: ${res.side}\n${t('深度')}: ${res.depth}`)
8273
8479
  })
@@ -10223,6 +10429,10 @@ async function renderMyContributions(app) {
10223
10429
  app.innerHTML = shell(loading$(), 'me')
10224
10430
  const p = await GET('/build-reputation/me')
10225
10431
  if (!p || p.error) { app.innerHTML = shell(`<div class="card" style="padding:16px;color:#b91c1c">${escHtml(p?.error || t('加载失败'))}</div>`, 'me'); return }
10432
+ // F9 — GitHub 身份归属面(只读自己的 bindings + attributable facts;失败时优雅降级,不挡整页)
10433
+ const gid = await GET('/contribution-identity/github/me').catch(() => null)
10434
+ // F10 — 自动发现可认领的 GitHub 贡献(只读;失败优雅降级)
10435
+ const claimable = await GET('/contribution-identity/github/claimable').catch(() => null)
10226
10436
  const lang = window._lang === 'zh' ? 'zh' : 'en'
10227
10437
  const tier = p.tier || {}
10228
10438
  const k = p.kpi || {}
@@ -10259,12 +10469,175 @@ async function renderMyContributions(app) {
10259
10469
  </div>
10260
10470
  ${provRows ? `<div class="card" style="padding:10px;margin-bottom:12px"><div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('署名构成(自报)')}</div>${provRows}</div>` : ''}
10261
10471
  ${!p.passkey_anchor_present ? `<div class="card" style="padding:10px;margin-bottom:12px;border-left:3px solid #d97706"><div style="font-size:12px;color:#92400e">${t('未绑 Passkey:贡献可受理致谢,但需绑定真人锚点才记入建设信誉。')}</div></div>` : ''}
10472
+ ${ghClaimSectionHtml(gid, claimable, lang)}
10473
+ <div class="card" onclick="location.hash='#contribute/tasks'" style="padding:12px 14px;margin-bottom:12px;cursor:pointer;display:flex;align-items:center;gap:10px">
10474
+ <div style="font-size:20px;flex-shrink:0">📋</div>
10475
+ <div style="flex:1;min-width:0">
10476
+ <div style="font-weight:600;font-size:13px">${t('查看公开共建任务')}</div>
10477
+ <div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('可浏览 / 可认领的公共任务板(独立于我的贡献记录)')}</div>
10478
+ </div>
10479
+ <div style="color:#9ca3af">›</div>
10480
+ </div>
10262
10481
  <div style="font-size:13px;font-weight:600;margin:14px 0 6px">${t('限制与申诉')}</div>
10263
10482
  ${restrHtml}
10264
10483
  <div style="font-size:11px;color:#9ca3af;margin-top:14px">${t('看板仅自己可见,不做公开排行。')}</div>
10265
10484
  `, 'me')
10266
10485
  }
10267
10486
 
10487
+ // ─── F9 — GitHub 贡献认领 UI(身份/归属认领;不是奖励、付款或提现)──────────────────────
10488
+ // 服务端三步契约:claim-challenge(签发挑战+proof_marker)→ 用户把 marker 发到自己 GitHub 账号的
10489
+ // public gist → requestPasskeyGate('identity_claim', {github_actor_id, source_event_key, challenge_id})
10490
+ // 拿一次性 webauthn_token → claim-complete。accountId 恒为 session 用户(前端绝不传 account_id);
10491
+ // GitHub read token 只在服务端(前端不收不传)。F10 discovery 已接入(GET .../github/claimable →
10492
+ // 「可认领的 GitHub 贡献」列表,点「认领此贡献」预填并发起);手动输入仅作找不到时的 fallback。
10493
+ let _ghClaimCtx = null // { challenge_id, expires_at, proof_marker, actor, sek }
10494
+
10495
+ // typed error code → 可读提示(认领失败绝不伪装成功)
10496
+ function ghClaimErrText(code, fallback) {
10497
+ const m = {
10498
+ GITHUB_READ_NOT_CONFIGURED: t('身份认领暂不可用(服务端未配置 GitHub 读取凭证),请稍后再试或联系 maintainer'),
10499
+ FACT_NOT_CLAIMABLE: t('没有可认领的、经凭证背书的 GitHub 贡献记录 — 请检查 source_event_key 是否正确'),
10500
+ ACTOR_MISMATCH: t('该贡献记录的执行者与所填 GitHub 身份不符'),
10501
+ ALREADY_BOUND: t('该 GitHub 身份已被其他账号认领'),
10502
+ CHALLENGE_EXPIRED: t('认领挑战已过期,请重新生成'),
10503
+ CHALLENGE_ALREADY_USED: t('认领挑战已被使用,请重新生成'),
10504
+ CHALLENGE_NOT_FOUND: t('认领挑战不存在或不属于当前账号'),
10505
+ PROOF_REJECTED: t('gist 证明未通过 — 请确认 gist 公开、归属于该 GitHub 账号、且内容为完整 proof_marker'),
10506
+ HUMAN_PRESENCE_REQUIRED: t('此操作需要真人 Passkey 验证'),
10507
+ AGENT_SCOPE_UNDECLARED: t('写操作需问责锚点 — 请先在「安全」页绑定 Passkey'),
10508
+ INVALID_REQUEST: t('请求参数无效 — 请检查两个输入框'),
10509
+ }
10510
+ return m[code] || fallback || t('操作失败')
10511
+ }
10512
+
10513
+ function ghClaimSectionHtml(gid, claimable, lang) {
10514
+ const ok = gid && !gid.error
10515
+ const bindings = ok ? (gid.bindings || []) : []
10516
+ const facts = ok ? (gid.attributable_facts || []) : []
10517
+ const notice = ok && gid.value_boundary ? (lang === 'zh' ? gid.value_boundary.notice_zh : gid.value_boundary.notice_en) : ''
10518
+ const bindRows = bindings.length === 0
10519
+ ? `<div style="font-size:12px;color:#9ca3af">${t('尚未绑定 GitHub 身份')}</div>`
10520
+ : bindings.map(b => `<div style="font-size:12px;color:#374151">🔗 <code>github:${escHtml(String(b.github_actor_id))}</code> · ${escHtml(String(b.visibility))} · ${t('绑定于')} ${escHtml(String(b.bound_at || ''))}</div>`).join('')
10521
+ const factRows = facts.length === 0
10522
+ ? `<div style="font-size:12px;color:#9ca3af">${t('暂无可归属的贡献事实')}</div>`
10523
+ : facts.map(f => `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#f9fafb;border-radius:6px;margin-bottom:4px">
10524
+ <div><b>${escHtml(String(f.type || f.source))}</b> · ${escHtml(String(f.status))} · <code style="font-size:10px">${escHtml(String(f.fact_id).slice(0, 18))}…</code></div>
10525
+ <div style="color:#6b7280;word-break:break-all">${escHtml(String(f.source_event_key))}</div>
10526
+ </div>`).join('')
10527
+ // F10 — 自动发现的可认领贡献(actor 未被任何账号绑定的 credential-backed facts)
10528
+ const cl = (claimable && !claimable.error) ? (claimable.claimable_facts || []) : null
10529
+ const claimRows = cl === null
10530
+ ? `<div style="font-size:12px;color:#9ca3af">${t('可认领列表暂不可用,稍后再试')}</div>`
10531
+ : cl.length === 0
10532
+ ? `<div style="font-size:12px;color:#9ca3af">${t('暂无自动发现的可认领贡献')}</div>`
10533
+ : cl.map(r => `<div style="font-size:11px;color:#374151;padding:6px 8px;background:#eff6ff;border-radius:6px;margin-bottom:4px">
10534
+ <div><b>PR #${escHtml(String(r.pr_number))}</b> · <code>github:${escHtml(String(r.github_actor_id))}</code> · ${escHtml(String(r.merged_at || r.created_at || ''))}</div>
10535
+ <div style="color:#6b7280;word-break:break-all">${escHtml(String(r.source_event_key))}</div>
10536
+ <div style="color:#9ca3af;font-size:10px">fact <code>${escHtml(String(r.fact_id).slice(0, 18))}…</code> · ${escHtml(String(r.merge_commit_sha || '').slice(0, 10))}</div>
10537
+ <button class="btn btn-outline btn-sm" style="width:auto;margin-top:4px;font-size:11px" onclick="ghClaimFromRow('${escHtml(String(r.github_actor_id))}','${escHtml(String(r.source_event_key))}')">${t('认领此贡献')}</button>
10538
+ </div>`).join('')
10539
+ const ctx = _ghClaimCtx
10540
+ return `
10541
+ <div class="card" style="padding:14px;margin-bottom:12px" id="gh-claim-card">
10542
+ <div style="font-size:13px;font-weight:600;margin-bottom:4px">🔗 ${t('GitHub 贡献认领(身份归属)')}</div>
10543
+ <div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('把"以 GitHub 身份完成、已被凭证背书"的贡献事实归属到本账号。这只是身份与归属的认领 — 不是奖励、不是付款。')}</div>
10544
+ <div style="font-size:11px;color:#166534;background:#f0fdf4;border-radius:6px;padding:6px 8px;margin-bottom:8px">✓ ${t('GitHub 贡献认领不需要先购买,也不需要开通分享分润。')}</div>
10545
+ ${notice ? `<div style="font-size:10px;color:#9ca3af;background:#f4f4f5;border-radius:6px;padding:6px 8px;margin-bottom:8px">🔒 ${escHtml(notice)}</div>` : ''}
10546
+ ${ok ? `
10547
+ <div style="font-size:12px;font-weight:600;margin:8px 0 4px">${t('已绑定身份')}</div>${bindRows}
10548
+ <div style="font-size:12px;font-weight:600;margin:10px 0 4px">${t('可归属的贡献事实')}</div>${factRows}
10549
+ <div style="font-size:12px;font-weight:600;margin:10px 0 4px">${t('可认领的 GitHub 贡献(自动发现)')}</div>${claimRows}
10550
+ <details style="margin-top:10px" ${ctx ? 'open' : ''} id="gh-claim-details">
10551
+ <summary style="font-size:12px;font-weight:600;cursor:pointer">${t('手动认领一条贡献(找不到时的备用入口)')}</summary>
10552
+ <div style="font-size:11px;color:#92400e;background:#fef3c7;border-radius:6px;padding:6px 8px;margin:8px 0">${t('上方列表找不到时,可手动输入 source_event_key 与 github_actor_id。')}</div>
10553
+ <input id="gh-claim-actor" placeholder="github_actor_id (${t('如')} 262558625)" value="${escHtml(ctx?.actor || '')}" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:12px;margin-bottom:6px">
10554
+ <input id="gh-claim-sek" placeholder="source_event_key (github:<repo>:<pr>:merged)" value="${escHtml(ctx?.sek || '')}" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:12px;margin-bottom:6px">
10555
+ <button class="btn btn-outline btn-sm" style="width:auto" onclick="ghClaimIssue()">1️⃣ ${t('生成认领挑战')}</button>
10556
+ <div id="gh-claim-step2" style="margin-top:8px">${ctx ? ghClaimStep2Html(ctx) : ''}</div>
10557
+ <div id="gh-claim-msg" style="font-size:12px;margin-top:6px"></div>
10558
+ </details>` : `<div style="font-size:12px;color:#9ca3af">${t('身份归属面暂不可用,稍后再试')}</div>`}
10559
+ </div>`
10560
+ }
10561
+
10562
+ function ghClaimStep2Html(ctx) {
10563
+ return `
10564
+ <div style="background:#eef2ff;border-radius:8px;padding:10px;margin-top:4px">
10565
+ <div style="font-size:11px;color:#3730a3;margin-bottom:2px">🔒 ${t('本挑战锁定于')}: <code>github:${escHtml(ctx.actor)}</code> · <code style="word-break:break-all">${escHtml(ctx.sek)}</code></div>
10566
+ <div style="font-size:11px;color:#3730a3;margin-bottom:4px">challenge_id: <code>${escHtml(ctx.challenge_id)}</code> · ${t('过期于')} ${escHtml(ctx.expires_at || '')}</div>
10567
+ <div style="font-size:11px;color:#374151;margin-bottom:4px">2️⃣ ${t('把下方 proof_marker 原样发布到【该 GitHub 账号拥有的 public gist】,然后回来填 gist_id:')}</div>
10568
+ <div style="font-family:ui-monospace,Menlo,monospace;font-size:10px;background:#fff;border:1px solid #e0e7ff;border-radius:6px;padding:6px 8px;word-break:break-all" id="gh-claim-marker">${escHtml(ctx.proof_marker)}</div>
10569
+ <button class="btn btn-outline btn-sm" style="width:auto;margin-top:6px" onclick="copyText(document.getElementById('gh-claim-marker').textContent).then(ok=>toast$(ok?t('已复制'):t('复制失败,请手动复制'),ok?'success':'error'))">📋 ${t('复制 proof_marker')}</button>
10570
+ <input id="gh-claim-gist" placeholder="gist_id" style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:12px;margin-top:8px">
10571
+ <button class="btn btn-primary btn-sm" style="width:auto;margin-top:6px" onclick="ghClaimComplete()">3️⃣ 🔑 ${t('用 Passkey 完成认领')}</button>
10572
+ </div>`
10573
+ }
10574
+
10575
+ // F10:点「认领此贡献」→ 预填 actor + source_event_key,展开手动区,直接走 F9 既有三步流程(签发挑战)
10576
+ window.ghClaimFromRow = (actor, sek) => {
10577
+ const d = document.getElementById('gh-claim-details'); if (d) d.open = true
10578
+ const ia = document.getElementById('gh-claim-actor'); if (ia) ia.value = actor
10579
+ const is = document.getElementById('gh-claim-sek'); if (is) is.value = sek
10580
+ ghClaimIssue()
10581
+ }
10582
+
10583
+ window.ghClaimIssue = async () => {
10584
+ const msg = document.getElementById('gh-claim-msg')
10585
+ const actor = (document.getElementById('gh-claim-actor')?.value || '').trim()
10586
+ const sek = (document.getElementById('gh-claim-sek')?.value || '').trim()
10587
+ if (!actor || !sek) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请先填写 github_actor_id 与 source_event_key')}</span>`; return }
10588
+ if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('签发中…')}</span>`
10589
+ const r = await POST('/contribution-identity/github/claim-challenge', { source_event_key: sek, github_actor_id: actor })
10590
+ if (r?.status === 'issued') {
10591
+ _ghClaimCtx = { challenge_id: r.challenge_id, expires_at: r.expires_at, proof_marker: r.proof_marker, actor, sek }
10592
+ const s2 = document.getElementById('gh-claim-step2'); if (s2) s2.innerHTML = ghClaimStep2Html(_ghClaimCtx)
10593
+ if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('挑战已签发 — 按第 2 步发布 gist')}</span>`
10594
+ return
10595
+ }
10596
+ if (r?.status === 'already_bound_self') { if (msg) msg.innerHTML = `<span style="color:#16a34a">✓ ${t('该 GitHub 身份已绑定到本账号,无需重复认领')}</span>`; return }
10597
+ // 签发失败 → 清空旧挑战上下文(防止旧 challenge 被误用于新输入)
10598
+ _ghClaimCtx = null
10599
+ const s2f = document.getElementById('gh-claim-step2'); if (s2f) s2f.innerHTML = ''
10600
+ if (msg) msg.innerHTML = `<span style="color:#dc2626">${escHtml(ghClaimErrText(r?.error_code, r?.error))}${r?.error_code ? ` <code style="font-size:10px">[${escHtml(r.error_code)}]</code>` : ''}</span>`
10601
+ }
10602
+
10603
+ window.ghClaimComplete = async () => {
10604
+ const msg = document.getElementById('gh-claim-msg')
10605
+ const ctx = _ghClaimCtx
10606
+ if (!ctx) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请先生成认领挑战')}</span>`; return }
10607
+ // 输入漂移守卫:签发后若用户改了 actor/source 输入框,旧挑战不再适用 — 清空并要求重新生成,
10608
+ // 防止"以为在认领当前输入的那条"的错觉(服务端本就只认 challenge 绑定的三元组)。
10609
+ const curActor = (document.getElementById('gh-claim-actor')?.value || '').trim()
10610
+ const curSek = (document.getElementById('gh-claim-sek')?.value || '').trim()
10611
+ if (curActor !== ctx.actor || curSek !== ctx.sek) {
10612
+ _ghClaimCtx = null
10613
+ const s2 = document.getElementById('gh-claim-step2'); if (s2) s2.innerHTML = ''
10614
+ if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('输入已更改,与已签发的挑战不一致 — 请重新生成认领挑战')}</span>`
10615
+ return
10616
+ }
10617
+ const gistId = (document.getElementById('gh-claim-gist')?.value || '').trim()
10618
+ if (!gistId) { if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('请填写 gist_id')}</span>`; return }
10619
+ let token
10620
+ try {
10621
+ // purpose_data 必须与服务端校验完全一致:{github_actor_id, source_event_key, challenge_id}
10622
+ token = await requestPasskeyGate('identity_claim', { github_actor_id: ctx.actor, source_event_key: ctx.sek, challenge_id: ctx.challenge_id })
10623
+ } catch (e) {
10624
+ if (msg) msg.innerHTML = `<span style="color:#dc2626">${t('Passkey 验证未完成')}: ${escHtml(e?.message || '')}</span>`
10625
+ return
10626
+ }
10627
+ if (msg) msg.innerHTML = `<span style="color:#6366f1">${t('提交认领…')}</span>`
10628
+ const r = await POST('/contribution-identity/github/claim-complete', {
10629
+ source_event_key: ctx.sek, github_actor_id: ctx.actor,
10630
+ challenge_id: ctx.challenge_id, gist_id: gistId, webauthn_token: token,
10631
+ })
10632
+ if (r?.status === 'claimed' || r?.status === 'already_bound_self') {
10633
+ _ghClaimCtx = null
10634
+ toast$(t('✓ 认领成功 — 贡献事实已归属到本账号'), 'success')
10635
+ renderMyContributions(document.getElementById('app')) // 刷新归属面 + 建设信誉面板
10636
+ return
10637
+ }
10638
+ if (msg) msg.innerHTML = `<span style="color:#dc2626">${escHtml(ghClaimErrText(r?.error_code, r?.error))}${r?.error_code ? ` <code style="font-size:10px">[${escHtml(r.error_code)}]</code>` : ''}</span>`
10639
+ }
10640
+
10268
10641
  // W7 客服 ticket-thread 视图
10269
10642
  const TICKET_TYPE_META = {
10270
10643
  created: { icon: '🛟', title: '新建工单', border: '#d97706' },
@@ -11187,7 +11560,63 @@ async function renderSellerAnalytics(app) {
11187
11560
  <div style="font-size:10px;color:#9ca3af">${t('退款')}</div>
11188
11561
  </div>
11189
11562
  </div>
11563
+
11564
+ <div class="card" style="padding:14px;margin-bottom:10px">
11565
+ <div style="font-size:13px;font-weight:600;margin-bottom:8px">⭐ ${t('店铺评价')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(买家评价 · 每条可回应一次)')}</span></div>
11566
+ <div id="seller-reviews-area" style="font-size:12px;color:#6b7280">${loading$()}</div>
11567
+ </div>
11190
11568
  `, 'me')
11569
+ hydrateSellerReviews()
11570
+ }
11571
+
11572
+ // 店铺评价汇总 + 逐条回应(P2)。复用既有 POST /orders/:order_id/rating/reply(卖家一回一限);
11573
+ // 读 /sellers/me/ratings(authed,含 order_id)。不改评价 / 资金逻辑。
11574
+ async function hydrateSellerReviews() {
11575
+ const area = document.getElementById('seller-reviews-area')
11576
+ if (!area) return
11577
+ const r = await GET('/sellers/me/ratings?limit=50').catch(() => null)
11578
+ const items = Array.isArray(r?.items) ? r.items : []
11579
+ if (items.length === 0) { area.innerHTML = `<div style="color:#9ca3af;text-align:center;padding:12px">${t('暂无评价')}</div>`; return }
11580
+ const unreplied = Number(r?.agg?.unreplied || 0)
11581
+ const starStr = (n) => '★'.repeat(Math.max(0, Math.min(5, Number(n) || 0))) + '☆'.repeat(5 - Math.max(0, Math.min(5, Number(n) || 0)))
11582
+ area.innerHTML = `
11583
+ ${unreplied > 0 ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-bottom:8px">📝 ${unreplied} ${t('条评价待回应')}</div>` : ''}
11584
+ ${items.map(it => it.masked ? `
11585
+ <div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px;background:#fafafa">
11586
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
11587
+ <span style="font-size:12px;color:#6b7280">🔒 ${t('评价双盲遮蔽中')}</span>
11588
+ <span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
11589
+ </div>
11590
+ <div style="font-size:11px;color:#9ca3af;margin-bottom:4px">📦 ${escHtml(it.product_title || '')}</div>
11591
+ <div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px">${t('买家已评价,但需你先评价买家,或盲评期结束后才能查看与回应(防互相影响打分)。')}</div>
11592
+ </div>` : `
11593
+ <div style="border:1px solid #f3f4f6;border-radius:8px;padding:10px;margin-bottom:8px">
11594
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:4px">
11595
+ <span style="color:#f59e0b;font-size:13px">${starStr(it.stars)}</span>
11596
+ <span style="font-size:10px;color:#9ca3af">${fmtTime(it.created_at)}</span>
11597
+ </div>
11598
+ <div style="font-size:11px;color:#6b7280;margin-bottom:4px">@${escHtml(it.buyer_handle || it.buyer_name || '')} · 📦 ${escHtml(it.product_title || '')}</div>
11599
+ ${it.comment ? `<div style="font-size:12px;color:#374151;margin-bottom:6px">${escHtml(it.comment)}</div>` : `<div style="font-size:12px;color:#9ca3af;margin-bottom:6px">${t('买家未留言')}</div>`}
11600
+ ${it.reply ? `<div style="background:#f0f9ff;border-radius:6px;padding:6px 8px;font-size:12px;color:#0369a1"><strong>${t('你的回应')}:</strong>${escHtml(it.reply)}</div>${it.buyer_followup ? `<div style="background:#fafafa;border-radius:6px;padding:6px 8px;font-size:12px;color:#374151;margin-top:4px"><strong>${t('买家追问')}:</strong>${escHtml(it.buyer_followup)}</div>` : ''}` : `
11601
+ <div style="display:flex;gap:6px;align-items:flex-end">
11602
+ <textarea id="rev-reply-${it.order_id}" rows="1" maxlength="500" placeholder="${t('回应这条评价(最多 500 字 · 仅一次)')}" style="flex:1;padding:6px 8px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;resize:none"></textarea>
11603
+ <button class="btn btn-primary btn-sm" style="padding:6px 12px;font-size:11px" onclick="submitSellerReviewReply('${it.order_id}')">${t('回应')}</button>
11604
+ </div>
11605
+ <div id="rev-reply-err-${it.order_id}" style="font-size:11px;color:#dc2626;margin-top:4px"></div>`}
11606
+ </div>`).join('')}
11607
+ `
11608
+ }
11609
+
11610
+ window.submitSellerReviewReply = async (orderId) => {
11611
+ const ta = document.getElementById('rev-reply-' + orderId)
11612
+ const errEl = document.getElementById('rev-reply-err-' + orderId)
11613
+ const reply = (ta?.value || '').trim()
11614
+ if (errEl) errEl.textContent = ''
11615
+ if (!reply) { if (errEl) errEl.textContent = t('回应不能为空'); return }
11616
+ const res = await POST(`/orders/${orderId}/rating/reply`, { reply })
11617
+ if (res.error) { if (errEl) errEl.textContent = res.error; return }
11618
+ toast$(t('已回应'))
11619
+ hydrateSellerReviews()
11191
11620
  }
11192
11621
 
11193
11622
  window.switchAnalyticsWindow = (days) => {
@@ -11263,15 +11692,17 @@ async function renderReturnsCenter(app) {
11263
11692
  }
11264
11693
 
11265
11694
  // L3 Phase 2:卖家确认收到退货 → 触发退款
11266
- window.confirmReturnReceived = async (id) => {
11695
+ // orderId 可选:从订单详情内联调用时传入 → 处理完回到订单详情(否则回退货中心)
11696
+ window.confirmReturnReceived = async (id, orderId) => {
11267
11697
  if (!confirm(t('确认已收到退货商品?此操作将触发退款(不可撤销)'))) return
11268
11698
  const r = await POST(`/return-requests/${id}/received`, {})
11269
11699
  if (r.error) { toast$(r.error, 'error'); return }
11270
11700
  toast$(t('已退款 · 退货完成'))
11271
- renderReturnsCenter(document.getElementById('app'))
11701
+ if (orderId) renderOrderDetail(document.getElementById('app'), orderId)
11702
+ else renderReturnsCenter(document.getElementById('app'))
11272
11703
  }
11273
11704
 
11274
- window.decideReturn = (id, decision) => {
11705
+ window.decideReturn = (id, decision, orderId) => {
11275
11706
  const isAccept = decision === 'accept'
11276
11707
  const title = isAccept ? `✓ ${t('确认接受退款')}` : `✗ ${t('拒绝退货')}`
11277
11708
  const hint = isAccept
@@ -11290,7 +11721,7 @@ window.decideReturn = (id, decision) => {
11290
11721
  <div id="ret-decide-msg" style="margin:8px 0"></div>
11291
11722
  <div style="display:flex;gap:8px;margin-top:12px">
11292
11723
  <button class="btn btn-gray" style="flex:1" onclick="this.closest('[style*=position]').remove()">${t('取消')}</button>
11293
- <button class="btn ${isAccept ? 'btn-success' : 'btn-primary'}" style="flex:1" onclick="confirmDecideReturn('${id}','${decision}')">${isAccept ? t('接受退款') : t('拒绝')}</button>
11724
+ <button class="btn ${isAccept ? 'btn-success' : 'btn-primary'}" style="flex:1" onclick="confirmDecideReturn('${id}','${decision}','${orderId || ''}')">${isAccept ? t('接受退款') : t('拒绝')}</button>
11294
11725
  </div>
11295
11726
  </div>
11296
11727
  </div>
@@ -11300,7 +11731,7 @@ window.decideReturn = (id, decision) => {
11300
11731
  document.body.appendChild(div.firstElementChild)
11301
11732
  }
11302
11733
 
11303
- window.confirmDecideReturn = async (id, decision) => {
11734
+ window.confirmDecideReturn = async (id, decision, orderId) => {
11304
11735
  const response = document.getElementById('ret-decide-text').value.trim()
11305
11736
  const msg = document.getElementById('ret-decide-msg')
11306
11737
  if (decision === 'reject' && !response) {
@@ -11311,7 +11742,8 @@ window.confirmDecideReturn = async (id, decision) => {
11311
11742
  if (res.error) { msg.innerHTML = alert$('error', res.error); return }
11312
11743
  document.querySelector('.js-modal')?.remove()
11313
11744
  toast$(t('已处理'))
11314
- renderReturnsCenter(document.getElementById('app'))
11745
+ if (orderId) renderOrderDetail(document.getElementById('app'), orderId)
11746
+ else renderReturnsCenter(document.getElementById('app'))
11315
11747
  }
11316
11748
 
11317
11749
  window.sharePromoLink = async (productId, title) => {
@@ -11328,7 +11760,7 @@ window.sharePromoLink = async (productId, title) => {
11328
11760
  }
11329
11761
  const link = `${location.origin}${res.short_url}`
11330
11762
  const meName = state.user.name || ''
11331
- const text = `${t('我在 WebAZ 看上「')}${title}${t('」,AI 比价 + 双激励分享')}\n— ${meName}`
11763
+ const text = `${t('我在 WebAZ 看上「')}${title}${t('」,用 AI 比价,体验不错')}\n— ${meName}`
11332
11764
  const r = await webShareOrCopy({ title: 'WebAZ — ' + title, text, url: link })
11333
11765
  if (r === 'shared') toast$(t('已分享'))
11334
11766
  else if (r === 'copied') toast$(t('已复制(含商品文案 + 链接)') + (res.reused ? '' : ' · ' + t('已自动加入「我的分享」')))
@@ -11942,32 +12374,8 @@ async function syncPlacementPref() {
11942
12374
  } catch {}
11943
12375
  }
11944
12376
 
11945
- // 仅平台分享(不绑 sponsor,只建 PV 条线)
11946
- // P-Polish 3:点击左/右区推荐码卡片复制(替代旧 alert 体验)
11947
- window.copyPlacementLink = async (link, side, el) => {
11948
- const ok = await copyText(link)
11949
- if (ok) {
11950
- toast$((side === 'left' ? '🔵 ' : '🟢 ') + t('推荐码已复制'))
11951
- if (el) {
11952
- const orig = el.style.background
11953
- el.style.background = side === 'left' ? '#bfdbfe' : '#bbf7d0'
11954
- setTimeout(() => { el.style.background = orig }, 600)
11955
- }
11956
- } else {
11957
- prompt(t('请手动复制'), link)
11958
- }
11959
- }
11960
-
11961
- // 兼容:旧调用点保留
11962
- window.sharePlatformLink = (side) => {
11963
- if (!state.user?.id) return alert(t('请先登录'))
11964
- if (side !== 'left' && side !== 'right') return
11965
- // 用 permanent_code 短链(/i/CODE-L|R),绝不用 usr_xxx;缺码时拒绝
11966
- const code = state.user?.permanent_code
11967
- if (!code) return alert(t('邀请码暂不可用,请刷新或联系支持'))
11968
- const link = `${location.origin}/i/${code}-${side === 'left' ? 'L' : 'R'}`
11969
- copyPlacementLink(link, side, null)
11970
- }
12377
+ // pre-public 去左右码:copyPlacementLink / sharePlatformLink(生成 /i/CODE-L|R 侧链)已移除 —
12378
+ // 统一用唯一推荐码 copyRefLink('/i/CODE'),放置侧别由系统自动决定。
11971
12379
 
11972
12380
  function renderRecover(app) {
11973
12381
  app.innerHTML = `
@@ -11979,8 +12387,8 @@ function renderRecover(app) {
11979
12387
 
11980
12388
  <div id="rec-step1">
11981
12389
  <div class="form-group">
11982
- <label class="form-label">${t('注册时使用的名称')}</label>
11983
- <input class="form-control" id="rec-name" placeholder="${t('例:陈小明')}">
12390
+ <label class="form-label">${t('名称或 @用户名')}</label>
12391
+ <input class="form-control" id="rec-name" placeholder="${t('例:陈小明 或 @chenxiaoming')}">
11984
12392
  </div>
11985
12393
  <div class="form-group">
11986
12394
  <label class="form-label">${t('绑定的邮箱')}</label>
@@ -11994,7 +12402,11 @@ function renderRecover(app) {
11994
12402
  <div class="form-group">
11995
12403
  <input class="form-control" id="rec-code" placeholder="${t('6 位验证码')}" maxlength="6">
11996
12404
  </div>
11997
- <button class="btn btn-primary" onclick="doRecoverConfirm()">${t('验证并找回密钥')}</button>
12405
+ <div class="form-group">
12406
+ <label class="form-label">${t('设置新登录密码')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(可选 · 至少 8 位 · 留空则只找回 API Key)')}</span></label>
12407
+ <input class="form-control" id="rec-newpw" type="password" placeholder="${t('新密码(至少 8 位)')}" autocomplete="new-password" maxlength="200">
12408
+ </div>
12409
+ <button class="btn btn-primary" onclick="doRecoverConfirm()">${t('验证并找回')}</button>
11998
12410
  <button class="btn btn-outline" onclick="recoverBackToStep1()" style="margin-left:8px">${t('重新开始')}</button>
11999
12411
  </div>
12000
12412
 
@@ -12034,13 +12446,15 @@ window.doRecoverConfirm = async () => {
12034
12446
  const name = codeInp?.dataset?.name
12035
12447
  const email = codeInp?.dataset?.email
12036
12448
  const code = codeInp?.value?.trim()
12449
+ const newpw = document.getElementById('rec-newpw')?.value || ''
12037
12450
  const result = document.getElementById('rec-result')
12038
12451
  if (!code) { result.innerHTML = alert$('error', t('请填写验证码')); return }
12452
+ if (newpw && newpw.length < 8) { result.innerHTML = alert$('error', t('新密码至少 8 字符')); return }
12039
12453
  result.innerHTML = loading$()
12040
- const res = await api('POST', '/recover-key/confirm', { name, email, code })
12454
+ const res = await api('POST', '/recover-key/confirm', { name, email, code, ...(newpw ? { new_password: newpw } : {}) })
12041
12455
  if (res.error) { result.innerHTML = alert$('error', res.error); return }
12042
12456
  result.innerHTML = `
12043
- <div class="alert alert-success" style="font-size:13px">${t('✓ 找回成功')}</div>
12457
+ <div class="alert alert-success" style="font-size:13px">${res.password_reset ? t('✓ 找回成功 · 新密码已设置') : t('✓ 找回成功')}</div>
12044
12458
  <div style="background:#f3f4f6;border-radius:8px;padding:12px;margin-top:10px">
12045
12459
  <div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('你的 API Key')}</div>
12046
12460
  <code style="font-size:13px;word-break:break-all">${res.api_key}</code>
@@ -12149,36 +12563,27 @@ window.doLoginByPassword = async () => {
12149
12563
 
12150
12564
 
12151
12565
  // 从 URL 解析分享 hint(30 天 cookie 持久化)
12152
- // ?ref=usr_xxx 仅三级佣金 sponsor
12153
- // ?placement=usr_xxx&side=left|right → 仅积分挂靠
12154
- // ?ref=xxx&side=left|right → 两轨道(ref 同时作 placement)
12155
- // ?ref=xxx&placement=xxx&side=... → 显式两轨道
12566
+ // ?ref=CODE 三级佣金 sponsor(同时作 placement inviter)
12567
+ // ?placement=CODE → 仅积分挂靠
12568
+ // pre-public 去左右码:不再解析 side / -L/-R 侧别,放置侧别由注册时系统自动决定。
12156
12569
  function readShareHint() {
12157
12570
  const params = new URLSearchParams(location.search)
12158
12571
  let urlRef = params.get('ref')
12159
12572
  let urlPlace = params.get('placement')
12160
- let urlSide = params.get('side')
12161
- // 兼容 ref/placement 带 -L/-R 后缀:自动剥离并填入 side(用户手工粘 VKSF9P-L 时也工作)
12162
- const stripSide = (v) => {
12163
- if (!v) return [v, null]
12164
- const m = v.match(/^(.+?)-([lLrR])$/)
12165
- return m ? [m[1], m[2].toLowerCase() === 'l' ? 'left' : 'right'] : [v, null]
12166
- }
12167
- let extraSide = null
12168
- ;[urlRef, extraSide] = stripSide(urlRef)
12169
- if (!extraSide) [urlPlace, extraSide] = stripSide(urlPlace)
12170
- if (!urlSide && extraSide) urlSide = extraSide
12171
- // ref / placement 仅接受邀请码(6-7 位永久码,可带 -L/-R)。usr_xxx / @handle / 裸 handle 不再作为邀请引用 —
12573
+ // pre-public 去左右码:兼容旧的 -L/-R 后缀但只做归一化(剥离回基础码),不再提取/存储 side
12574
+ // 放置侧别由注册时系统自动决定。
12575
+ const stripSide = (v) => (v ? v.replace(/-[lLrR]$/, '') : v)
12576
+ urlRef = stripSide(urlRef)
12577
+ urlPlace = stripSide(urlPlace)
12578
+ // ref / placement 仅接受邀请码(6-7 位永久码)。usr_xxx / @handle / handle 不再作为邀请引用
12172
12579
  // 收窄公开邀请面,消除歧义(权威由服务端 resolveInviteCodeRef 二次校验)。
12173
12580
  const refPattern = /^[A-Za-z0-9]{6,7}$/
12174
12581
  const validRef = urlRef && refPattern.test(urlRef)
12175
12582
  const validPlace= urlPlace && refPattern.test(urlPlace)
12176
12583
  if (validRef || validPlace) {
12177
- const side = (urlSide === 'left' || urlSide === 'right') ? urlSide : null
12178
12584
  const obj = {
12179
12585
  sponsor_id: validRef ? urlRef : null,
12180
- placement_inviter_id: validPlace ? urlPlace : (side && validRef ? urlRef : null),
12181
- placement_side: side,
12586
+ placement_inviter_id: validPlace ? urlPlace : null,
12182
12587
  expiry: Date.now() + 30 * 86400_000,
12183
12588
  }
12184
12589
  localStorage.setItem('webaz_share_hint', JSON.stringify(obj))
@@ -12211,8 +12616,7 @@ function readShareHint() {
12211
12616
  const o = JSON.parse(old)
12212
12617
  if (o.expiry > Date.now() && okCode(o.ref)) return {
12213
12618
  sponsor_id: o.ref || null,
12214
- placement_inviter_id: o.side ? o.ref : null,
12215
- placement_side: o.side || null,
12619
+ placement_inviter_id: null,
12216
12620
  expiry: o.expiry,
12217
12621
  }
12218
12622
  localStorage.removeItem('webaz_ref') // expired or non-code → drop
@@ -12273,7 +12677,7 @@ async function maybeClaimPendingShopReferral() {
12273
12677
  writeShareCtx({ pending_shop_referral: null }); return
12274
12678
  }
12275
12679
  try {
12276
- await POST('/shop-referral/touch', { seller_identifier: p.seller_identifier, ref_code: p.ref_code, ...(p.side === 'left' || p.side === 'right' ? { side: p.side } : {}) })
12680
+ await POST('/shop-referral/touch', { seller_identifier: p.seller_identifier, ref_code: p.ref_code })
12277
12681
  // 任何已送达的响应(成功 / self-skip / already_locked / typed 错误)都清;仅网络异常保留待重试
12278
12682
  writeShareCtx({ pending_shop_referral: null })
12279
12683
  } catch {}
@@ -12402,7 +12806,6 @@ async function initShareCtx() {
12402
12806
  if (hint?.sponsor_id && !ctx?.sponsor_id) patch.sponsor_id = hint.sponsor_id
12403
12807
  if (hint?.placement_inviter_id && !ctx?.placement_inviter_id) {
12404
12808
  patch.placement_inviter_id = hint.placement_inviter_id
12405
- patch.placement_side = hint.placement_side
12406
12809
  }
12407
12810
  if (fromHash && !ctx?.target_hash) patch.target_hash = fromHash
12408
12811
 
@@ -12413,11 +12816,11 @@ async function initShareCtx() {
12413
12816
  }
12414
12817
 
12415
12818
  // 店铺推荐:?ref=CODE + #shop/<seller> → 暂存待登录后 touch(只锚定推荐关系,非全店佣金权)。
12416
- // hint.sponsor_id 已由 readShareHint 收窄为邀请码(-L/-R 已剥进 placement_side),usr_xxx/@handle 进不来。
12819
+ // hint.sponsor_id 已由 readShareHint 收窄为邀请码(-L/-R 已归一化为基础码),usr_xxx/@handle 进不来。
12417
12820
  if (hint?.sponsor_id && fromHash && fromHash.startsWith('#shop/') && !ctx?.pending_shop_referral) {
12418
12821
  const sellerIdent = fromHash.slice('#shop/'.length).split('?')[0].trim()
12419
12822
  if (sellerIdent && sellerIdent !== 'agent') {
12420
- patch.pending_shop_referral = { seller_identifier: sellerIdent, ref_code: hint.sponsor_id, side: hint.placement_side || null }
12823
+ patch.pending_shop_referral = { seller_identifier: sellerIdent, ref_code: hint.sponsor_id }
12421
12824
  }
12422
12825
  }
12423
12826
 
@@ -12507,11 +12910,47 @@ window._mountTurnstileIfEnabled = async () => {
12507
12910
  } catch { /* 加载失败不阻塞 dev */ }
12508
12911
  }
12509
12912
 
12913
+ // 注册邮箱验证:发码。成功后展开验证码输入框。
12914
+ window._regCodeSent = false
12915
+ window._onRegEmailInput = () => {
12916
+ // 邮箱改了就要求重新发码(旧码对不上新邮箱)
12917
+ window._regCodeSent = false
12918
+ const row = document.getElementById('reg-code-row')
12919
+ if (row) row.style.display = 'none'
12920
+ }
12921
+ window.doRegSendCode = async () => {
12922
+ const email = document.getElementById('inp-reg-email')?.value?.trim()
12923
+ const btn = document.getElementById('btn-reg-sendcode')
12924
+ if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return showMsg('error', t('请填写有效邮箱'))
12925
+ if (btn) { btn.disabled = true; btn.textContent = t('发送中...') }
12926
+ const res = await POST('/register/send-code', { email })
12927
+ if (res.error) {
12928
+ if (btn) { btn.disabled = false; btn.textContent = t('发送验证码') }
12929
+ return showMsg('error', res.error)
12930
+ }
12931
+ window._regCodeSent = true
12932
+ const row = document.getElementById('reg-code-row')
12933
+ if (row) row.style.display = ''
12934
+ // 60s 冷却
12935
+ let left = 60
12936
+ if (btn) {
12937
+ btn.disabled = true
12938
+ const tick = () => { if (left <= 0) { btn.disabled = false; btn.textContent = t('重新发送'); return } btn.textContent = left + 's'; left--; setTimeout(tick, 1000) }
12939
+ tick()
12940
+ }
12941
+ showMsg('success', t('验证码已发送至邮箱,请查收'))
12942
+ }
12943
+
12510
12944
  window.doRegister = async () => {
12511
12945
  const name = document.getElementById('inp-name').value.trim()
12512
12946
  const role = document.getElementById('inp-role').value
12513
12947
  const region = document.getElementById('inp-region')?.value || ''
12514
12948
  const sponsorInput = document.getElementById('inp-sponsor')?.value?.trim() || ''
12949
+ const email = document.getElementById('inp-reg-email')?.value?.trim() || ''
12950
+ const code = document.getElementById('inp-reg-code')?.value?.trim() || ''
12951
+ if (!email) return showMsg('error', t('请填写找回邮箱'))
12952
+ if (!window._regCodeSent) return showMsg('error', t('请先点"发送验证码"并验证邮箱'))
12953
+ if (!code) return showMsg('error', t('请输入邮箱验证码'))
12515
12954
  if (!name) return showMsg('error', t('请填写名称'))
12516
12955
  if (!region) return showMsg('error', t('请选择国家 / 地区'))
12517
12956
  // A3 软 gate:sponsor_id 视觉必填但允许空(后端按 require_ref_to_register 系统设置裁决)
@@ -12519,13 +12958,12 @@ window.doRegister = async () => {
12519
12958
  if (!confirm(t('没有填写邀请码 — 你将绑定到平台公库(推荐找老用户拿邀请链接)\n\n确认继续?'))) return
12520
12959
  }
12521
12960
  const hint = readShareHint()
12522
- const body = { name, role, region }
12961
+ const body = { name, role, region, email, code }
12523
12962
  // 优先 input 值,回退 URL hint
12524
12963
  const sponsorFinal = sponsorInput || hint?.sponsor_id
12525
12964
  if (sponsorFinal) body.sponsor_id = sponsorFinal
12526
- if (hint?.placement_inviter_id && hint?.placement_side) {
12965
+ if (hint?.placement_inviter_id) {
12527
12966
  body.placement_inviter_id = hint.placement_inviter_id
12528
- body.placement_side = hint.placement_side
12529
12967
  }
12530
12968
  // #1049 Turnstile token(若启用)
12531
12969
  if (window._turnstileToken) body.turnstile_token = window._turnstileToken
@@ -12568,41 +13006,91 @@ function showRegisterSuccessModal(res) {
12568
13006
  // P1 (QA 轮 14.a): 区分"邀请人"(referral sponsor) 与"积分树挂靠"(binary placement)
12569
13007
  // 旧版挤一行 "已绑定邀请人 X · 左区 depth N" → 用户误以为 depth 是相对邀请人
12570
13008
  const placementLine = res.placement
12571
- ? `<div style="font-size:11px;color:#9ca3af;margin-bottom:14px">${t('积分树挂靠')}: ${res.placement.side === 'left' ? '🔵 ' + t('左区') : '🟢 ' + t('右区')} · ${t('深度')} ${res.placement.depth}<br><span style="font-size:10px">${t('(挂靠位置由系统按弱区自动决定,与邀请人不一定相邻)')}</span></div>`
13009
+ ? `<div style="font-size:11px;color:#9ca3af;margin-bottom:14px">${t('积分树挂靠')}: ${res.placement.side === 'left' ? '🔵 ' + t('左区') : '🟢 ' + t('右区')} · ${t('深度')} ${res.placement.depth}<br><span style="font-size:10px">${t('积分树挂靠为系统内部记录,不代表收益权或兑付承诺。')}</span></div>`
12572
13010
  : ''
12573
13011
  _openModal(`
12574
13012
  <h2 style="font-size:18px;font-weight:600;margin-bottom:8px;color:#16a34a">🎉 ${t('注册成功!')}</h2>
12575
13013
  ${res.sponsor_id && ctx?.sponsor_name ? `<div style="font-size:12px;color:#6366f1;margin-bottom:6px">${t('邀请人')}: <strong>${escHtml(ctx.sponsor_name)}</strong></div>` : ''}
12576
13014
  ${placementLine}
12577
13015
 
12578
- <div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-bottom:14px">
12579
- <div style="font-size:12px;color:#92400e;font-weight:600;margin-bottom:6px">⚠️ ${t('立即保存你的 API Key')}</div>
12580
- <div style="font-size:11px;color:#78350f;margin-bottom:6px">${t('这是你的唯一登录凭证。丢了只能通过绑定邮箱找回。')}</div>
13016
+ <div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 12px;margin-bottom:12px">
13017
+ <div style="font-size:12px;color:#92400e;font-weight:600;margin-bottom:6px">⚠️ ${t('立即保存你的 API Key(登录凭证)')}</div>
12581
13018
  <div style="display:flex;gap:6px;align-items:center">
12582
13019
  <code id="reg-apikey-display" style="font-size:12px;background:#fff;padding:6px 10px;border-radius:6px;flex:1;word-break:break-all;border:1px solid #fde68a">${res.api_key}</code>
12583
- <button class="btn btn-outline btn-sm" style="font-size:11px;padding:6px 10px;white-space:nowrap" onclick="copyText('${res.api_key}').then(ok=>toast$(ok?'${t('已复制')}':'${t('复制失败,请手动复制')}',ok?'success':'error'))">${t('复制')}</button>
13020
+ </div>
13021
+ <div style="display:flex;gap:6px;margin-top:8px">
13022
+ <button class="btn btn-outline btn-sm" style="font-size:11px;padding:6px 10px;flex:1" onclick="window._regMarkSaved();copyText('${res.api_key}').then(ok=>toast$(ok?'${t('已复制')}':'${t('复制失败,请手动复制')}',ok?'success':'error'))">📋 ${t('复制 Key')}</button>
13023
+ <button class="btn btn-outline btn-sm" style="font-size:11px;padding:6px 10px;flex:1" onclick="window._downloadCredBackup()">⬇️ ${t('下载备份 .txt')}</button>
12584
13024
  </div>
12585
13025
  </div>
12586
13026
 
12587
- <div style="font-size:13px;color:#374151;font-weight:600;margin-bottom:6px">${t('完成 4 步开始使用')}</div>
12588
- <div style="background:#f9fafb;border-radius:8px;padding:6px 10px;margin-bottom:12px;font-size:12px;line-height:1.8">
12589
- ${t('设置默认配送地址(智能下单需要)')}<br>
12590
- ${t('设置登录密码(备用凭证,可选)')}<br>
12591
- ${t('完成首笔购买后,可申请分享分润。')}<br>
12592
- ${t('🔐 绑定 Passkey — 强烈推荐,大额提现自动启用,防账号被盗')}
13027
+ <div style="font-size:13px;color:#374151;font-weight:600;margin-bottom:6px">${t('保存凭证检查清单')}</div>
13028
+ <div style="background:#f9fafb;border-radius:8px;padding:8px 10px;margin-bottom:8px;font-size:12px;line-height:1.9">
13029
+ <div id="reg-chk-save">☐ ${t('复制 API Key 或下载备份文件')}</div>
13030
+ <div>✅ ${t('已验证找回邮箱')}:${escHtml(res.email || '')}</div>
13031
+ <div>☐ ${t('设置登录密码(备用凭证)')}</div>
13032
+ <div>☐ ${t('🔐 绑定 Passkey — 大额提现自动启用,防账号被盗')}</div>
13033
+ </div>
13034
+ <div style="font-size:11px;color:#6b7280;line-height:1.6;margin-bottom:12px">
13035
+ ${t('邮箱已验证:可用于找回账号或重置登录密码。API Key 仍是主要身份凭证,强烈建议保存;Passkey 是增强保护,不替代恢复邮箱。')}
12593
13036
  </div>
12594
13037
 
12595
13038
  ${targetBtn}
12596
13039
  <button class="btn btn-primary" style="width:100%;margin-bottom:8px;background:linear-gradient(135deg,#7c3aed,#6d28d9);border-color:transparent" onclick="window._closeRegModal('passkey')">🔐 ${t('立即绑定 Passkey(强烈推荐)')}</button>
12597
- <button class="btn btn-outline" style="width:100%;margin-bottom:8px" onclick="window._closeRegModal('profile')">⚙️ ${t('先去填资料')}</button>
12598
- <button class="btn btn-outline" style="width:100%" onclick="window._closeRegModal(false)">${t('稍后再说,先逛逛')}</button>
13040
+ <button class="btn btn-outline" style="width:100%;margin-bottom:8px" onclick="window._closeRegModal('password')">🔑 ${t('设置登录密码')}</button>
13041
+ <button id="reg-skip-btn" class="btn btn-outline btn-sm" style="width:100%;color:#9ca3af;font-size:12px" onclick="window._closeRegModal(false)">${t('稍后再说,先逛逛')}</button>
12599
13042
  `)
13043
+ // 凭证保存状态:复制或下载前,"稍后"按钮弱化 + 强提示。
13044
+ window._regSaved = false
13045
+ window._regMarkSaved = () => {
13046
+ window._regSaved = true
13047
+ const chk = document.getElementById('reg-chk-save')
13048
+ if (chk) { chk.innerHTML = '✅ ' + t('复制 API Key 或下载备份文件'); chk.style.color = '#16a34a' }
13049
+ const skip = document.getElementById('reg-skip-btn')
13050
+ if (skip) skip.style.color = ''
13051
+ }
13052
+ window._downloadCredBackup = () => {
13053
+ try {
13054
+ const lines = [
13055
+ 'WebAZ 账户备份 / Account Backup',
13056
+ '================================',
13057
+ `${t('名称')} / Name: ${res.name || ''}`,
13058
+ `Handle: @${res.handle || ''}`,
13059
+ `${t('角色')} / Role: ${res.role || ''}`,
13060
+ `${t('找回邮箱')} / Recovery email: ${res.email || ''}`,
13061
+ `${t('注册时间')} / Registered: ${new Date().toISOString()}`,
13062
+ '--------------------------------',
13063
+ 'API Key (登录凭证 / login credential):',
13064
+ res.api_key || '',
13065
+ '--------------------------------',
13066
+ '⚠️ 这是你的登录凭证,等同密码。妥善保管,不要截图分享或发给任何人。',
13067
+ '⚠️ This is your login credential (equivalent to a password). Keep it private — do not screenshot, forward, or share it.',
13068
+ '丢失 API Key 可用上面的找回邮箱在 https://webaz.xyz/#recover 重置(也可重置登录密码)。',
13069
+ 'Lost your API Key? Reset it (and your password) with the recovery email above at https://webaz.xyz/#recover',
13070
+ ]
13071
+ const blob = new Blob([lines.join('\n')], { type: 'text/plain;charset=utf-8' })
13072
+ const url = URL.createObjectURL(blob)
13073
+ const a = document.createElement('a')
13074
+ a.href = url
13075
+ a.download = `webaz-backup-${(res.handle || res.user_id || 'account')}.txt`
13076
+ document.body.appendChild(a); a.click(); a.remove()
13077
+ setTimeout(() => URL.revokeObjectURL(url), 2000)
13078
+ window._regMarkSaved()
13079
+ toast$(t('备份已下载'), 'success')
13080
+ } catch (e) {
13081
+ toast$(t('下载失败,请改用复制'), 'error')
13082
+ }
13083
+ }
12600
13084
  window._closeRegModal = (action) => {
13085
+ // 复制/下载前点"稍后" → 强提示(已绑邮箱可找回,但仍建议先存 key)
13086
+ if (action === false && !window._regSaved) {
13087
+ if (!confirm(t('你还没复制 API Key 或下载备份。\n虽然已绑定找回邮箱(丢号可重置),仍强烈建议先保存 Key。\n\n确定先逛逛?'))) return
13088
+ }
12601
13089
  closeModal()
12602
13090
  if (action === true) {
12603
13091
  // 跳 intended_hash
12604
13092
  navigateIntended(roleHome(res.role))
12605
- } else if (action === 'profile') {
13093
+ } else if (action === 'password' || action === 'profile') {
12606
13094
  sessionStorage.removeItem('webaz_intended_hash')
12607
13095
  location.hash = '#me/settings'
12608
13096
  } else if (action === 'passkey') {
@@ -13732,8 +14220,8 @@ async function renderFeedView() {
13732
14220
  const amount = Number(extra.amount || 0).toFixed(2)
13733
14221
  body = `${actor} ${t('因推广')} <a href="#order-product/${e.product_id}" style="color:#111">${escHtml(e.product_title)}</a> ${t('获得 L')}${extra.level} ${t('佣金')} <strong style="color:#059669">+${amount} WAZ</strong>`
13734
14222
  } else if (e.kind === 'join_binary') {
13735
- const side = extra.placement_side === 'left' ? '🔵 左区' : '🟢 右区'
13736
- body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('')} ${side}`
14223
+ // pre-public 去左右码:活动流不再广播左/右区,只显示加入了某人的积分树
14224
+ body = `${actor} ${t('加入了')} ${escHtml(extra.placement_name || '—')} ${t('的积分树')}`
13737
14225
  }
13738
14226
  const icon = e.kind === 'purchase' ? '🛒' : e.kind === 'commission' ? '💰' : '⚛'
13739
14227
  return `<div class="card" style="margin-bottom:8px;padding:10px 12px;display:flex;gap:10px;align-items:flex-start">
@@ -18831,20 +19319,21 @@ window.setSellerSubTab = (k) => {
18831
19319
  // C-4: 批量发货 modal
18832
19320
  window.openBatchShipModal = async (ids) => {
18833
19321
  if (!Array.isArray(ids) || ids.length === 0) return
18834
- // 拉物流公司
19322
+ // 拉物流公司(可空:自发货不需要物流方)
18835
19323
  const lc = await GET('/logistics/companies').catch(() => [])
18836
19324
  const companies = Array.isArray(lc) ? lc : []
18837
- if (companies.length === 0) { alert(t('暂无可用物流公司')); return }
18838
19325
  const html = `
18839
19326
  <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()">
18840
19327
  <div style="background:#fff;width:100%;max-width:560px;border-radius:16px 16px 0 0;padding:20px;max-height:80vh;overflow-y:auto" onclick="event.stopPropagation()">
18841
19328
  <h2 style="font-size:16px;font-weight:700;margin-bottom:8px">📦 ${t('批量发货')}</h2>
18842
19329
  <div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('将')} <strong>${ids.length}</strong> ${t('个订单一次性标记为已发货')}</div>
18843
19330
  <div class="form-group">
18844
- <label class="form-label">${t('物流公司')} *</label>
19331
+ <label class="form-label">${t('发货方式')}</label>
18845
19332
  <select class="form-control" id="bs-logistics">
19333
+ <option value="self">${t('📦 我自己发货(自提自送)')}</option>
18846
19334
  ${companies.map(c => `<option value="${escHtml(c.id)}">${escHtml(c.name)}</option>`).join('')}
18847
19335
  </select>
19336
+ <div style="font-size:11px;color:#92400e;margin-top:6px;line-height:1.5">${t('自己发货:你负责揽收 / 运输 / 送达,超时或虚假发货仍按卖家责任处理。选物流公司则由物流方流转。')}</div>
18848
19337
  </div>
18849
19338
  <div style="font-size:11px;color:#9ca3af;margin-bottom:8px">${t('快递单号可发货后单独补填,或先填部分')}</div>
18850
19339
  <div id="bs-msg" style="margin:8px 0"></div>
@@ -18861,10 +19350,12 @@ window.openBatchShipModal = async (ids) => {
18861
19350
  }
18862
19351
 
18863
19352
  window.submitBatchShip = async (ids) => {
18864
- const logistics_company_id = document.getElementById('bs-logistics').value
19353
+ const choice = document.getElementById('bs-logistics').value
18865
19354
  const msg = document.getElementById('bs-msg')
18866
19355
  msg.innerHTML = loading$()
18867
- const res = await POST('/orders/batch-ship', { order_ids: ids, logistics_company_id })
19356
+ // 自发货:不传 logistics_company_id(后端保持 logistics_id seller self-fulfill)
19357
+ const body = (choice && choice !== 'self') ? { order_ids: ids, logistics_company_id: choice } : { order_ids: ids }
19358
+ const res = await POST('/orders/batch-ship', body)
18868
19359
  if (res.error) { msg.innerHTML = alert$('error', res.error); return }
18869
19360
  document.querySelector('.js-modal')?.remove()
18870
19361
  toast$(`${t('已发货')} ${res.shipped} / ${ids.length}`)
@@ -19794,6 +20285,7 @@ async function renderOrderDetail(app, orderId) {
19794
20285
 
19795
20286
  <div id="action-area">
19796
20287
  ${actions ? renderActions(orderId, actions, order, logisticsCompanies) : ''}
20288
+ ${sellerDeclineContestPanel(order, orderId, isSeller)}
19797
20289
  </div>
19798
20290
 
19799
20291
  ${(isBuyer && order.status === 'completed' && product?.id) ? `
@@ -19815,6 +20307,12 @@ async function renderOrderDetail(app, orderId) {
19815
20307
  <div id="ret-area-${order.id}" style="font-size:12px;color:#6b7280">${loading$()}</div>
19816
20308
  </div>` : ''}
19817
20309
 
20310
+ ${(isSeller && order.status === 'completed') ? `
20311
+ <div class="card" id="ret-card-${order.id}">
20312
+ <div style="font-size:14px;font-weight:600;margin-bottom:6px">↩ ${t('退货处理')}</div>
20313
+ <div id="ret-area-${order.id}" style="font-size:12px;color:#6b7280">${loading$()}</div>
20314
+ </div>` : ''}
20315
+
19818
20316
  ${((isBuyer || isSeller) && order.status === 'completed') ? `
19819
20317
  <div class="card" id="rate-card-${order.id}">
19820
20318
  <div style="font-size:14px;font-weight:600;margin-bottom:6px">⭐ ${t('交易评价')}</div>
@@ -19839,7 +20337,8 @@ async function renderOrderDetail(app, orderId) {
19839
20337
  `, 'orders')
19840
20338
 
19841
20339
  // Wave B-3: 退货 widget — 异步加载(仅 completed 订单可退)
19842
- if (isBuyer && order.status === 'completed' && Number(product?.return_days || 0) > 0) {
20340
+ // 买家:有退货窗口可申请/查看;卖家:有退货申请时内联查看+处理(accept/reject/received),无申请则隐藏卡
20341
+ if (((isBuyer && Number(product?.return_days || 0) > 0) || isSeller) && order.status === 'completed') {
19843
20342
  try { await renderReturnWidgetForOrder(order, product) } catch (e) { console.error(e) }
19844
20343
  }
19845
20344
  // Wave C-3: 评价 widget
@@ -20285,7 +20784,13 @@ async function renderReturnWidgetForOrder(order, product) {
20285
20784
  // P1-5: 订单级直查(取代拉全量列表过滤)
20286
20785
  const r = await GET(`/orders/${order.id}/return-request`).catch(() => ({ item: null }))
20287
20786
  const mine = r?.item || null
20288
- const returnDays = Number(product.return_days || 0)
20787
+ const isSellerView = state.user && state.user.id === order.seller_id
20788
+ // 卖家视角:无退货申请则隐藏整张卡(卖家不申请退货,只在有申请时处理)
20789
+ if (isSellerView && !mine) {
20790
+ const card = area.closest('.card'); if (card) card.style.display = 'none'
20791
+ return
20792
+ }
20793
+ const returnDays = Number(product?.return_days || 0)
20289
20794
  const baseTime = order.updated_at || order.created_at
20290
20795
  const deadline = new Date(baseTime).getTime() + returnDays * 86400 * 1000
20291
20796
  const remainMs = deadline - Date.now()
@@ -20315,6 +20820,14 @@ async function renderReturnWidgetForOrder(order, product) {
20315
20820
  ${item.status === 'pending' && isBuyer ? `<button class="btn btn-outline btn-sm" style="font-size:11px;padding:4px 10px" onclick="cancelReturnRequest('${item.id}', '${order.id}')">${t('取消申请')}</button>` : ''}
20316
20821
  </div>
20317
20822
 
20823
+ ${isSellerView && item.status === 'pending' ? `
20824
+ <div style="display:flex;gap:8px;margin-bottom:8px">
20825
+ <button class="btn btn-success btn-sm" style="flex:1;font-size:12px" onclick="decideReturn('${item.id}','accept','${order.id}')">${t('接受退款')}</button>
20826
+ <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>
20827
+ </div>` : ''}
20828
+ ${isSellerView && item.status === 'picked_up' ? `
20829
+ <button class="btn btn-success btn-sm" style="width:100%;font-size:12px;margin-bottom:8px" onclick="confirmReturnReceived('${item.id}','${order.id}')">${t('✓ 已收到退货 · 触发退款')}</button>` : ''}
20830
+
20318
20831
  <div style="padding:10px;background:#fafafa;border-radius:8px;margin-bottom:8px">
20319
20832
  <div style="font-size:11px;font-weight:600;color:#6b7280;margin-bottom:8px">🧾 ${t('协商时间线')} · ${events.length} ${t('条')}</div>
20320
20833
  ${events.map(ev => buildReturnTimelineEvent(ev, isBuyer)).join('')}
@@ -20651,26 +21164,32 @@ window.cancelReturnRequest = async (id, orderId) => {
20651
21164
  function getActions(order, isBuyer, isSeller, isLogistic) {
20652
21165
  const s = order.status
20653
21166
  const isInPerson = order.fulfillment_mode === 'in_person'
21167
+ const isSelfFulfillSeller = isSeller && !order.logistics_id
20654
21168
  // M8 面交订单:买家在 paid / accepted 都可"面交完成"直接结算
20655
21169
  if (isInPerson && isBuyer && (s === 'paid' || s === 'accepted')) {
20656
21170
  return [{ action: 'confirm_in_person', label: '🤝 面交完成 / 确认收货', style: 'success' }]
20657
21171
  }
20658
21172
  if (isSeller && s === 'paid')
20659
- return [{ action: 'accept', label: '接单', style: 'success' }]
21173
+ return [
21174
+ { action: 'accept', label: '接单', style: 'success' },
21175
+ { action: 'decline', label: '拒绝接单', style: 'danger', custom: 'decline' },
21176
+ ]
20660
21177
  if (isSeller && s === 'accepted' && !isInPerson)
20661
21178
  return [{ action: 'ship', label: '确认发货', style: 'success', logisticsSelector: true,
20662
21179
  trackingInput: true,
20663
21180
  evidencePlaceholder: '包装状态描述 / 货物说明(可选)' }]
20664
21181
  if (isSeller && s === 'accepted' && isInPerson)
20665
21182
  return [{ action: 'noop_in_person', label: '🤝 面交中(等待买家确认)', style: 'secondary', disabled: true }]
20666
- if (isLogistic && s === 'shipped')
21183
+ if ((isLogistic || isSelfFulfillSeller) && s === 'shipped')
20667
21184
  return [{ action: 'pickup', label: '✅ 确认揽收', style: 'success', needsEvidence: true,
20668
- noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890' }]
20669
- if (isLogistic && s === 'picked_up')
21185
+ noteLabel: '快递单号 *', evidencePlaceholder: '如:SF1234567890',
21186
+ helperText: isSelfFulfillSeller ? '自履约订单:你负责回传揽收/单号,超时仍按卖家责任处理。' : '' }]
21187
+ if ((isLogistic || isSelfFulfillSeller) && s === 'picked_up')
20670
21188
  return [{ action: 'transit', label: '🚛 开始运输', style: 'primary' }]
20671
- if (isLogistic && s === 'in_transit')
21189
+ if ((isLogistic || isSelfFulfillSeller) && s === 'in_transit')
20672
21190
  return [{ action: 'deliver', label: '📬 确认投递', style: 'success', needsEvidence: true,
20673
- noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间' }]
21191
+ noteLabel: '投递凭证', evidencePlaceholder: '门牌照片描述 / 收件人姓名 / 签收时间',
21192
+ helperText: isSelfFulfillSeller ? '自履约投递需留存签收/门牌/交付说明,买家确认后才结算。' : '' }]
20674
21193
  if (isBuyer && s === 'delivered')
20675
21194
  return [
20676
21195
  { action: 'confirm', label: '确认收货', style: 'success' },
@@ -20683,19 +21202,22 @@ function getActions(order, isBuyer, isSeller, isLogistic) {
20683
21202
  function renderActions(orderId, actions, order, logisticsCompanies = []) {
20684
21203
  return `
20685
21204
  <div class="action-area">
20686
- <div class="action-title">我的操作</div>
21205
+ <div class="action-title">${t('我的操作')}</div>
20687
21206
  <div id="action-msg"></div>
20688
21207
  ${actions.map((a, i) => `
20689
- ${a.logisticsSelector ? `
21208
+ ${a.custom === 'decline' ? `
21209
+ <button class="btn btn-${a.style}" style="margin-bottom:8px;background:#fff;color:#dc2626;border:1px solid #dc2626"
21210
+ onclick="openDeclineModal('${orderId}')">
21211
+ ${t(a.label)}
21212
+ </button>` :
21213
+ a.logisticsSelector ? `
20690
21214
  <div class="form-group">
20691
- <label class="form-label">${t('选择物流公司')} <span style="color:#dc2626">*</span></label>
21215
+ <label class="form-label">${t('发货方式')}</label>
20692
21216
  <select class="form-control" id="logi-select-${i}">
20693
- <option value="">${t(' 请选择物流公司 —')}</option>
21217
+ <option value="self">${t('📦 我自己发货(自提自送)')}</option>
20694
21218
  ${logisticsCompanies.map(c => `<option value="${escHtml(c.id)}">${escHtml(c.name)}</option>`).join('')}
20695
21219
  </select>
20696
- ${logisticsCompanies.length === 0
20697
- ? `<div class="alert alert-warning" style="margin-top:6px;font-size:13px">${t('暂无已注册的物流公司,请先让物流方注册账号')}</div>`
20698
- : ''}
21220
+ <div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-top:6px;line-height:1.5">${t('自己发货:你负责揽收 / 运输 / 送达,超时或虚假发货仍按卖家责任处理。选物流公司则由物流方流转。')}</div>
20699
21221
  </div>
20700
21222
  ${a.trackingInput ? `
20701
21223
  <div class="form-group">
@@ -20704,22 +21226,23 @@ function renderActions(orderId, actions, order, logisticsCompanies = []) {
20704
21226
  </div>` : ''}
20705
21227
  <button class="btn btn-${a.style}" style="margin-bottom:8px"
20706
21228
  onclick="handleAction('${orderId}','${a.action}',${i},false,true)">
20707
- ${a.label}
21229
+ ${t(a.label)}
20708
21230
  </button>` :
20709
21231
  a.needsEvidence ? `
20710
21232
  <div class="form-group">
20711
- <label class="form-label">${a.noteLabel || '证据说明'}</label>
21233
+ <label class="form-label">${t(a.noteLabel || '证据说明')}</label>
20712
21234
  ${a.action === 'pickup'
20713
- ? `<input type="text" class="form-control" id="evid-${i}" placeholder="${a.evidencePlaceholder || '快递单号'}">`
20714
- : `<textarea class="form-control" id="evid-${i}" placeholder="${a.evidencePlaceholder || ''}"></textarea>`}
21235
+ ? `<input type="text" class="form-control" id="evid-${i}" placeholder="${t(a.evidencePlaceholder || '快递单号')}">`
21236
+ : `<textarea class="form-control" id="evid-${i}" placeholder="${t(a.evidencePlaceholder || '')}"></textarea>`}
21237
+ ${a.helperText ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-top:6px">${t(a.helperText)}</div>` : ''}
20715
21238
  </div>
20716
21239
  <button class="btn btn-${a.style}" style="margin-bottom:8px"
20717
21240
  onclick="handleAction('${orderId}','${a.action}',${i},true,false)">
20718
- ${a.label}
21241
+ ${t(a.label)}
20719
21242
  </button>` : `
20720
21243
  <button class="btn btn-${a.style}" style="margin-bottom:8px"
20721
21244
  onclick="handleAction('${orderId}','${a.action}',${i},false,false)">
20722
- ${a.label}
21245
+ ${t(a.label)}
20723
21246
  </button>`}
20724
21247
  `).join('')}
20725
21248
  </div>`
@@ -20740,6 +21263,13 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
20740
21263
  return
20741
21264
  }
20742
21265
  if (action === 'noop_in_person') return
21266
+ const confirmText = {
21267
+ ship: t('确认已经发货?发货后买家将看到物流信息,超时/虚假发货可能进入争议或判责。'),
21268
+ pickup: t('确认已揽收并回传凭证?请确保单号或揽收说明真实可追踪。'),
21269
+ deliver: t('确认已投递?请确保投递凭证真实,买家仍需确认收货后才结算。'),
21270
+ confirm: t('确认收货?escrow 将进入结算流程,无法撤销。'),
21271
+ }[action]
21272
+ if (confirmText && !confirm(confirmText)) return
20743
21273
 
20744
21274
  let evidDesc = (needsEvidence || hasLogisticsSelector)
20745
21275
  ? (document.getElementById(`evid-${idx}`)?.value?.trim() || '') : ''
@@ -20758,15 +21288,21 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
20758
21288
  let logisticsCompanyId = ''
20759
21289
  if (hasLogisticsSelector) {
20760
21290
  const sel = document.getElementById(`logi-select-${idx}`)
20761
- logisticsCompanyId = sel?.value || ''
20762
- if (!logisticsCompanyId) { msgEl.innerHTML = alert$('error', t('请选择物流公司')); return }
20763
- const companyName = sel.options[sel.selectedIndex]?.text || logisticsCompanyId
21291
+ const choice = sel?.value || 'self'
20764
21292
  const trackingInp = document.getElementById(`tracking-${idx}`)
20765
21293
  const trackingNo = trackingInp?.value?.trim() || ''
20766
- if (trackingNo) {
20767
- evidDesc = `${t('快递单号:')}${trackingNo} · ${companyName}`
21294
+ if (choice === 'self') {
21295
+ // 自发货:不绑定物流公司(logistics_id 留空 → seller self-fulfill 流转)
21296
+ logisticsCompanyId = ''
21297
+ evidDesc = trackingNo
21298
+ ? `${t('快递单号:')}${trackingNo}${t('(自己发货)')}`
21299
+ : t('卖家自己发货(自提自送)—— 后续由你揽收 / 运输 / 送达')
20768
21300
  } else {
20769
- evidDesc = `${t('已交付物流公司:')}${companyName}${t(',快递单号待物流揽收后回传')}`
21301
+ logisticsCompanyId = choice
21302
+ const companyName = sel.options[sel.selectedIndex]?.text || logisticsCompanyId
21303
+ evidDesc = trackingNo
21304
+ ? `${t('快递单号:')}${trackingNo} · ${companyName}`
21305
+ : `${t('已交付物流公司:')}${companyName}${t(',快递单号待物流揽收后回传')}`
20770
21306
  }
20771
21307
  }
20772
21308
 
@@ -20788,6 +21324,107 @@ window.handleAction = async (orderId, action, idx, needsEvidence, hasLogisticsSe
20788
21324
  }
20789
21325
  }
20790
21326
 
21327
+ // ─── 卖家拒单(decline) + 临时判责举证(contest_decline)─── RFC-007 stage 2/3/5 ───────────────
21328
+ // reason code 元数据(与后端 orders-action.ts DECLINE_REASON_CODES / OBJECTIVE_DECLINE_REASONS 对齐)。
21329
+ // objective(客观声称)→ 临时判责,需在举证窗口内发起仲裁,不是自动免责;subjective(主观)→ 立即按卖家违约、买家退款。
21330
+ const DECLINE_REASONS = [
21331
+ { code: 'stock_consumed_concurrent', objective: true, zh: '并发售罄(库存被同时下单耗尽)', en: 'Stock consumed by a concurrent order' },
21332
+ { code: 'stale_price_snapshot', objective: true, zh: '价格快照过期(下单价已失效)', en: 'Stale price snapshot' },
21333
+ { code: 'force_majeure', objective: true, zh: '不可抗力', en: 'Force majeure' },
21334
+ { code: 'price_regret', objective: false, zh: '价格反悔(不想按此价卖)', en: 'Price regret' },
21335
+ { code: 'cherry_pick', objective: false, zh: '挑单(选择性拒单)', en: 'Cherry-picking' },
21336
+ { code: 'other', objective: false, zh: '其他(主观)', en: 'Other (subjective)' },
21337
+ ]
21338
+ function declineReasonOptions() {
21339
+ const en = window._lang === 'en'
21340
+ return DECLINE_REASONS.map(r => `<option value="${r.code}" data-objective="${r.objective ? '1' : '0'}">${en ? r.en : r.zh}${r.objective ? ` · ${t('客观')}` : ` · ${t('主观')}`}</option>`).join('')
21341
+ }
21342
+ // 选中 reason 后的诚实后果提示(主观 vs 客观,绝不把客观拒单说成自动免责)
21343
+ window.onDeclineReasonChange = () => {
21344
+ const sel = document.getElementById('decline-reason')
21345
+ const note = document.getElementById('decline-consequence')
21346
+ if (!sel || !note) return
21347
+ const objective = sel.selectedOptions[0]?.dataset?.objective === '1'
21348
+ if (!sel.value) { note.innerHTML = ''; return }
21349
+ note.innerHTML = objective
21350
+ ? `<div style="background:#fffbeb;border:1px solid #fcd34d;color:#92400e;border-radius:8px;padding:10px;font-size:12px;line-height:1.6">⚠️ ${t('客观理由:订单将先转入【临时判责·卖家违约】并冻结结算。你需在举证窗口内发起仲裁举证翻案 —— 这不是自动免责;窗口过期未举证将按违约终结、买家退款。')}</div>`
21351
+ : `<div style="background:#fef2f2;border:1px solid #fca5a5;color:#991b1b;border-radius:8px;padding:10px;font-size:12px;line-height:1.6">⚠️ ${t('主观理由:将立即按【卖家违约】处理,买家全额退款。此操作不可撤销。')}</div>`
21352
+ }
21353
+ window.openDeclineModal = (orderId) => {
21354
+ const en = window._lang === 'en'
21355
+ _openModal(`
21356
+ <div style="max-width:440px">
21357
+ <h3 style="font-size:16px;font-weight:700;margin-bottom:6px">${t('拒绝接单')}</h3>
21358
+ <div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('拒单会影响买家。请如实选择理由 —— 系统按理由区分【主观违约】与【客观临时判责】,后者需举证。')}</div>
21359
+ <div class="form-group">
21360
+ <label class="form-label">${t('拒单理由')} <span style="color:#dc2626">*</span></label>
21361
+ <select class="form-control" id="decline-reason" onchange="onDeclineReasonChange()">
21362
+ <option value="">${en ? '— select a reason —' : '— 请选择理由 —'}</option>
21363
+ ${declineReasonOptions()}
21364
+ </select>
21365
+ </div>
21366
+ <div id="decline-consequence" style="margin-bottom:10px"></div>
21367
+ <div class="form-group">
21368
+ <label class="form-label">${t('说明')} <span style="font-size:11px;color:#9ca3af;font-weight:400">${t('(可选)')}</span></label>
21369
+ <textarea class="form-control" id="decline-notes" rows="2" placeholder="${t('补充说明(客观理由建议写清,便于后续举证)')}"></textarea>
21370
+ </div>
21371
+ <div id="decline-msg"></div>
21372
+ <div style="display:flex;gap:8px;margin-top:8px">
21373
+ <button class="btn btn-outline" style="flex:1" onclick="closeModal()">${t('取消')}</button>
21374
+ <button class="btn btn-danger" style="flex:1;background:#dc2626;border-color:#dc2626;color:#fff" onclick="submitDecline('${orderId}')">${t('确认拒单')}</button>
21375
+ </div>
21376
+ </div>`)
21377
+ }
21378
+ window.submitDecline = async (orderId) => {
21379
+ const code = document.getElementById('decline-reason')?.value || ''
21380
+ const notes = document.getElementById('decline-notes')?.value?.trim() || ''
21381
+ const msg = document.getElementById('decline-msg')
21382
+ if (!code) { if (msg) msg.innerHTML = alert$('error', t('请选择拒单理由')); return }
21383
+ if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('处理中...')}</div>`
21384
+ const res = await POST(`/orders/${orderId}/action`, { action: 'decline', decline_reason_code: code, notes })
21385
+ if (res.error) { if (msg) msg.innerHTML = alert$('error', res.error); return }
21386
+ closeModal()
21387
+ toast$(res.outcome === 'fault_seller_provisional'
21388
+ ? t('已拒单 — 临时判责,请在举证窗口内发起仲裁')
21389
+ : t('已拒单 — 已按卖家违约处理,买家将获退款'), 'success')
21390
+ setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
21391
+ }
21392
+ // 临时判责举证面板:仅卖家、订单 fault_seller + decline_objective_pending=1 + 未结算时显示
21393
+ function sellerDeclineContestPanel(order, orderId, isSeller) {
21394
+ if (!isSeller || order.status !== 'fault_seller' || Number(order.decline_objective_pending) !== 1 || order.settled_fault_at) return ''
21395
+ const contested = Number(order.decline_contested) === 1
21396
+ const deadline = order.decline_contest_deadline ? fmtTime(order.decline_contest_deadline) : ''
21397
+ if (contested) {
21398
+ return `<div class="card" style="padding:14px;margin-top:10px;border-left:3px solid #4f46e5;background:#eef2ff">
21399
+ <div style="font-size:13px;font-weight:600;color:#3730a3;margin-bottom:4px">⚖️ ${t('已发起仲裁举证')}</div>
21400
+ <div style="font-size:12px;color:#374151;line-height:1.6">${t('自动终结已暂停,等待仲裁员裁决:维持→免责全退+退质押;驳回→按违约结算。')}</div>
21401
+ </div>`
21402
+ }
21403
+ return `<div class="card" style="padding:14px;margin-top:10px;border-left:3px solid #d97706;background:#fffbeb">
21404
+ <div style="font-size:13px;font-weight:600;color:#92400e;margin-bottom:4px">⏳ ${t('临时判责 — 举证窗口开放')}</div>
21405
+ <div style="font-size:12px;color:#7f1d1d;line-height:1.6;margin-bottom:8px">
21406
+ ${t('你以客观理由拒单,订单暂判【卖家违约】但尚未结算。你可在窗口内发起人工仲裁举证翻案 —— 这不是自动免责。')}
21407
+ ${deadline ? `<br><strong>${t('举证截止')}:</strong> ${escHtml(deadline)} · ${t('过期未举证将按违约终结、买家退款。')}` : ''}
21408
+ </div>
21409
+ <div class="form-group">
21410
+ <label class="form-label">${t('举证说明')} <span style="color:#dc2626">*</span></label>
21411
+ <textarea class="form-control" id="contest-evidence" rows="3" placeholder="${t('客观说明拒单理由的证据(如并发订单号、价格变更时间、不可抗力凭证)')}"></textarea>
21412
+ </div>
21413
+ <div id="contest-msg"></div>
21414
+ <button class="btn btn-primary" style="width:100%" onclick="submitContestDecline('${orderId}')">${t('提交举证 / 发起仲裁')}</button>
21415
+ </div>`
21416
+ }
21417
+ window.submitContestDecline = async (orderId) => {
21418
+ const evid = document.getElementById('contest-evidence')?.value?.trim() || ''
21419
+ const msg = document.getElementById('contest-msg')
21420
+ if (!evid) { if (msg) msg.innerHTML = alert$('error', t('请填写举证说明')); return }
21421
+ if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
21422
+ const res = await POST(`/orders/${orderId}/action`, { action: 'contest_decline', evidence_description: evid })
21423
+ if (res.error) { if (msg) msg.innerHTML = alert$('error', res.error); return }
21424
+ toast$(t('已发起仲裁举证 — 等待仲裁员裁决'), 'success')
21425
+ setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
21426
+ }
21427
+
20791
21428
  // S8: 检查买家是否完成首单,若是 + ShareCtx 还有 sponsor → 一次性致谢 toast + 清 ctx
20792
21429
  async function maybeThankSponsorAndClear() {
20793
21430
  const ctx = readShareCtx()
@@ -22271,48 +22908,61 @@ async function renderSeller(app) {
22271
22908
  }
22272
22909
 
22273
22910
  app.innerHTML = shell(loading$(), 'seller')
22274
- const [productsRaw, ordersRaw, mySkillsRaw, quotaRaw, insightsRaw] = await Promise.all([
22911
+ const [productsRaw, ordersRaw, mySkillsRaw, quotaRaw, insightsRaw, returnsRaw] = await Promise.all([
22275
22912
  GET('/my-products'),
22276
22913
  GET('/orders'),
22277
22914
  GET('/skills/mine'),
22278
22915
  GET('/seller/quota-status'),
22279
22916
  GET('/seller/insights').catch(() => null),
22917
+ GET('/return-requests?role=seller').catch(() => ({ items: [] })),
22280
22918
  ])
22281
22919
  const mySkills = Array.isArray(mySkillsRaw) ? mySkillsRaw : []
22282
22920
  const orders = Array.isArray(ordersRaw) ? ordersRaw : []
22283
22921
  const products = Array.isArray(productsRaw) ? productsRaw : []
22284
22922
  const quota = (quotaRaw && !quotaRaw.error) ? quotaRaw : null
22285
22923
  const insights = (insightsRaw && !insightsRaw.error) ? insightsRaw : null
22924
+ const returns = Array.isArray(returnsRaw?.items) ? returnsRaw.items : []
22286
22925
 
22287
22926
  const pendingOrders = orders.filter(o => ['paid', 'accepted'].includes(o.status) && o.seller_id === state.user.id)
22927
+ const paidOrders = orders.filter(o => o.status === 'paid' && o.seller_id === state.user.id)
22928
+ const acceptedOrders = orders.filter(o => o.status === 'accepted' && o.seller_id === state.user.id)
22288
22929
  const myProducts = products
22289
22930
 
22290
22931
  // KPI(基于最近 50 单的派生统计)+ 今日维度
22291
22932
  const mySoldOrders = orders.filter(o => o.seller_id === state.user.id)
22292
22933
  const today = new Date().toISOString().slice(0, 10)
22293
22934
  const todayOrders = mySoldOrders.filter(o => (o.created_at || '').startsWith(today))
22294
- const kpiPending = mySoldOrders.filter(o => ['paid','accepted'].includes(o.status)).length
22935
+ const kpiPaid = paidOrders.length
22936
+ const kpiAccepted = acceptedOrders.length
22295
22937
  const kpiInTransit = mySoldOrders.filter(o => ['shipped','picked_up','in_transit','delivered'].includes(o.status)).length
22938
+ const kpiDisputes = mySoldOrders.filter(o => o.status === 'disputed').length
22939
+ const kpiProvisionalDeclines = mySoldOrders.filter(o => o.status === 'fault_seller' && Number(o.decline_objective_pending) === 1 && !o.settled_fault_at).length
22940
+ const kpiReturnExceptions = returns.filter(r => ['pending','picked_up','rejected','accepted_pickup_pending'].includes(r.status)).length
22941
+ const kpiExceptions = kpiDisputes + kpiProvisionalDeclines + kpiReturnExceptions
22296
22942
  const kpiSales = mySoldOrders.filter(o => o.status === 'completed').reduce((s,o) => s + Number(o.total_amount || 0), 0)
22297
22943
  const kpiTodayCount = todayOrders.length
22298
22944
  const kpiTodaySales = todayOrders.filter(o => o.status === 'completed').reduce((s,o) => s + Number(o.total_amount || 0), 0)
22299
22945
  const sellerKpis = `
22300
- <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-bottom:14px">
22946
+ <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;margin-bottom:14px">
22301
22947
  <div class="card" style="padding:10px;text-align:center;background:linear-gradient(135deg,#eff6ff,#dbeafe);border-color:#bfdbfe">
22302
22948
  <div style="font-size:18px;font-weight:800;color:#1d4ed8">${kpiTodayCount}</div>
22303
22949
  <div style="font-size:10px;color:#6b7280;margin-top:2px">${t('今日订单')}</div>
22304
22950
  </div>
22305
- <div class="card" style="padding:10px;text-align:center;background:${kpiPending > 0 ? 'linear-gradient(135deg,#fef3c7,#fde68a)' : '#f9fafb'};border-color:${kpiPending > 0 ? '#fcd34d' : '#e5e7eb'}">
22306
- <div style="font-size:18px;font-weight:800;color:${kpiPending > 0 ? '#b45309' : '#9ca3af'}">${kpiPending}</div>
22951
+ <div class="card" style="padding:10px;text-align:center;background:${kpiPaid > 0 ? 'linear-gradient(135deg,#fef3c7,#fde68a)' : '#f9fafb'};border-color:${kpiPaid > 0 ? '#fcd34d' : '#e5e7eb'}">
22952
+ <div style="font-size:18px;font-weight:800;color:${kpiPaid > 0 ? '#b45309' : '#9ca3af'}">${kpiPaid}</div>
22953
+ <div style="font-size:10px;color:#6b7280;margin-top:2px">${t('待接单')}</div>
22954
+ </div>
22955
+ <div class="card" style="padding:10px;text-align:center;background:${kpiAccepted > 0 ? 'linear-gradient(135deg,#fff7ed,#fed7aa)' : '#f9fafb'};border-color:${kpiAccepted > 0 ? '#fdba74' : '#e5e7eb'}">
22956
+ <div style="font-size:18px;font-weight:800;color:${kpiAccepted > 0 ? '#c2410c' : '#9ca3af'}">${kpiAccepted}</div>
22307
22957
  <div style="font-size:10px;color:#6b7280;margin-top:2px">${t('待发货')}</div>
22308
22958
  </div>
22309
22959
  <div class="card" style="padding:10px;text-align:center">
22310
22960
  <div style="font-size:18px;font-weight:800;color:#374151">${kpiInTransit}</div>
22311
22961
  <div style="font-size:10px;color:#6b7280;margin-top:2px">${t('在途')}</div>
22312
22962
  </div>
22313
- <div class="card" style="padding:10px;text-align:center;background:linear-gradient(135deg,#f0fdf4,#dcfce7);border-color:#86efac">
22314
- <div style="font-size:16px;font-weight:800;color:#15803d">${kpiSales.toFixed(0)}</div>
22315
- <div style="font-size:10px;color:#6b7280;margin-top:2px">${t('累计 WAZ')}</div>
22963
+ <div class="card" style="padding:10px;text-align:center;background:${kpiExceptions > 0 ? 'linear-gradient(135deg,#fef2f2,#fee2e2)' : '#f9fafb'};border-color:${kpiExceptions > 0 ? '#fca5a5' : '#e5e7eb'}">
22964
+ <div style="font-size:18px;font-weight:800;color:${kpiExceptions > 0 ? '#dc2626' : '#9ca3af'}">${kpiExceptions}</div>
22965
+ <div style="font-size:10px;color:#6b7280;margin-top:2px">${t('异常')}</div>
22316
22966
  </div>
22317
22967
  </div>
22318
22968
  `
@@ -22394,9 +23044,7 @@ async function renderSeller(app) {
22394
23044
  </div>
22395
23045
  ` : ''
22396
23046
 
22397
- const pendingHtml = pendingOrders.length === 0
22398
- ? `<div class="empty" style="padding:24px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无待处理订单')}</div></div>`
22399
- : batchAcceptBar + batchShipBar + pendingOrders.map(o => `
23047
+ const pendingOrderRows = (list) => list.map(o => `
22400
23048
  <div class="card" onclick="navigate('#order/${o.id}')" style="cursor:pointer">
22401
23049
  <div class="order-item">
22402
23050
  <div class="order-icon">📦</div>
@@ -22408,6 +23056,39 @@ async function renderSeller(app) {
22408
23056
  <div class="order-amount">${o.total_amount} WAZ</div>
22409
23057
  </div>
22410
23058
  </div>`).join('')
23059
+ const acceptHtml = paidOrders.length === 0
23060
+ ? `<div class="empty" style="padding:24px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无待处理订单')}</div></div>`
23061
+ : batchAcceptBar + pendingOrderRows(paidOrders)
23062
+ const shipHtml = acceptedOrders.length === 0
23063
+ ? `<div class="empty" style="padding:18px"><div class="empty-icon">📦</div><div class="empty-text">${t('暂无待发货订单')}</div></div>`
23064
+ : batchShipBar + pendingOrderRows(acceptedOrders)
23065
+ const exceptionOrders = mySoldOrders.filter(o => o.status === 'disputed' || (o.status === 'fault_seller' && Number(o.decline_objective_pending) === 1 && !o.settled_fault_at))
23066
+ const exceptionReturns = returns.filter(r => ['pending','picked_up','rejected','accepted_pickup_pending'].includes(r.status))
23067
+ const exceptionsHtml = (exceptionOrders.length + exceptionReturns.length) === 0
23068
+ ? `<div class="empty" style="padding:18px"><div class="empty-icon">✅</div><div class="empty-text">${t('暂无异常待处理')}</div></div>`
23069
+ : `
23070
+ ${exceptionOrders.map(o => `
23071
+ <div class="card" onclick="navigate('#order/${o.id}')" style="cursor:pointer;border-left:3px solid ${o.status === 'disputed' ? '#dc2626' : '#f59e0b'};padding:10px 12px;margin-bottom:8px">
23072
+ <div style="display:flex;justify-content:space-between;gap:10px;align-items:center">
23073
+ <div style="min-width:0">
23074
+ <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(o.product_title)}</div>
23075
+ <div style="font-size:11px;color:#6b7280;margin-top:2px">${o.status === 'disputed' ? t('争议订单') : t('临时判责拒单')} · ${fmtTime(o.updated_at || o.created_at)}</div>
23076
+ </div>
23077
+ <span style="font-size:11px;color:#dc2626;font-weight:600;white-space:nowrap">${t('查看处理')} →</span>
23078
+ </div>
23079
+ </div>`).join('')}
23080
+ ${exceptionReturns.map(r => `
23081
+ <div class="card" onclick="navigate('#order/${r.order_id}')" style="cursor:pointer;border-left:3px solid #0891b2;padding:10px 12px;margin-bottom:8px">
23082
+ <div style="display:flex;justify-content:space-between;gap:10px;align-items:center">
23083
+ <div style="min-width:0">
23084
+ <div style="font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(r.product_title)}</div>
23085
+ <div style="font-size:11px;color:#6b7280;margin-top:2px">${t('退货处理')} · ${r.refund_amount} WAZ · ${fmtTime(r.created_at)}</div>
23086
+ </div>
23087
+ <span style="font-size:11px;color:#0891b2;font-weight:600;white-space:nowrap">${t('查看订单')} →</span>
23088
+ </div>
23089
+ </div>`).join('')}
23090
+ <button class="btn btn-outline btn-sm" style="width:auto;font-size:12px" onclick="navigate('#returns')">↩ ${t('退货管理')}</button>
23091
+ `
22411
23092
 
22412
23093
  const activeProducts = products.filter(p => p.status === 'active')
22413
23094
  const warehouseProducts = products.filter(p => p.status !== 'active' && p.status !== 'deleted')
@@ -22587,11 +23268,24 @@ async function renderSeller(app) {
22587
23268
  // 各 sub-tab 内容
22588
23269
  const dashboardSection = sellerSubTab === 'dashboard' ? `
22589
23270
  ${sellerKpis}
23271
+ ${sellerRecoveryReminderHTML()}
22590
23272
  ${stockAlertBanner}
22591
23273
  ${quotaBanner}
22592
23274
  ${pendingOrders.length > 0 ? `<div class="alert alert-warning">📬 ${t('你有')} ${pendingOrders.length} ${t('个订单需要处理')}</div>` : ''}
22593
- <div style="font-weight:700;margin-bottom:12px">${t('待处理订单')}</div>
22594
- ${pendingHtml}
23275
+ <div style="display:grid;grid-template-columns:1fr;gap:10px;margin-bottom:12px">
23276
+ <section>
23277
+ <div style="font-weight:700;margin-bottom:8px">📬 ${t('待接单')}</div>
23278
+ ${acceptHtml}
23279
+ </section>
23280
+ <section>
23281
+ <div style="font-weight:700;margin-bottom:8px">📦 ${t('待发货')}</div>
23282
+ ${shipHtml}
23283
+ </section>
23284
+ <section>
23285
+ <div style="font-weight:700;margin-bottom:8px">⚠ ${t('退货 · 争议 · 异常')}</div>
23286
+ ${exceptionsHtml}
23287
+ </section>
23288
+ </div>
22595
23289
  ${insightsBlock}
22596
23290
  ` : ''
22597
23291
 
@@ -23564,6 +24258,7 @@ function skillCard(s, context) {
23564
24258
  <span class="badge badge-green">${t('运行中')}</span>
23565
24259
  </div>
23566
24260
  <div style="font-size:13px;color:#6b7280;margin-top:8px">${s.description}</div>
24261
+ ${s.skill_type === 'auto_accept' ? `<div style="font-size:11px;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:6px;padding:6px 8px;margin-top:8px;line-height:1.5">⚠️ ${t('自动接单仍受约束:接单后须按时发货,超时按卖家违约判责;跳过「拒绝接单」窗口;不校验库存。')}</div>` : ''}
23567
24262
  </div>`
23568
24263
  }
23569
24264
  // buyer context
@@ -23584,7 +24279,7 @@ function skillCard(s, context) {
23584
24279
 
23585
24280
  const SKILL_CONFIG_HINTS = {
23586
24281
  catalog_sync: '目录同步:订阅此 Skill 的买家在搜索时会优先看到你的商品。成交后协议自动给你 0.5% 推荐佣金。',
23587
- auto_accept: '自动接单:买家下单后无需手动操作,系统自动接受。可设置每日上限、金额范围。',
24282
+ auto_accept: '⚠️ 自动接单:买家下单后系统自动接受(省去手动「接单」)。注意责任不变:①接单后你仍须按时发货,超时/不发货仍按卖家违约判责;②会跳过「拒绝接单」窗口,订单自动进入已接单后无法再拒单;③不校验库存,售罄商品也会被自动接单,你仍要履约或担责。建议用金额范围/每日上限控制风险。',
23588
24283
  price_negotiation: '价格协商:允许买家 Agent 在你设定的折扣范围内自动议价,减少沟通成本。',
23589
24284
  quality_guarantee: '质量承诺:额外质押 WAZ 作为品质担保,增强买家信任,适合高客单价商品。',
23590
24285
  instant_ship: '极速发货:承诺接单后 24h 内发货,违约自动赔付。适合有充足现货的卖家。',
@@ -33528,7 +34223,7 @@ async function renderMyNotes(app) {
33528
34223
  <h2 style="font-size:18px;font-weight:700">📝 ${t('我的笔记')}</h2>
33529
34224
  ${draftCount > 0 ? `<button class="btn btn-outline btn-sm" style="font-size:11px;padding:5px 10px" onclick="openDraftsModal()">📋 ${t('草稿')} ${draftCount}</button>` : ''}
33530
34225
  </div>
33531
- <div style="font-size:11px;color:#6b7280;margin-bottom:14px">${state.user?.mlm_ui_visible !== false ? t('协议自动按 70/20/10 把分享返佣给你和上下游') : t('你所在地区不分多级返佣 — 产生的佣金已自动捐入公益基金')}</div>
34226
+ <div style="font-size:11px;color:#6b7280;margin-bottom:14px">${state.user?.mlm_ui_visible !== false ? t('协议按地区层级自动拆分分享返佣(当前预发布期仅 L1)') : t('你所在地区不分多级返佣 — 产生的佣金已自动捐入公益基金')}</div>
33532
34227
  <div class="card" style="padding:14px;margin-bottom:14px;background:linear-gradient(135deg,#fef3c7,#fff7ed)">
33533
34228
  <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;text-align:center">
33534
34229
  <div><div style="font-size:18px;font-weight:800;color:#92400e">${notes.length}</div><div style="font-size:10px;color:#9ca3af">${t('笔记数')}</div></div>