@seasonkoh/webaz 0.1.23 → 0.1.25

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 (187) hide show
  1. package/README.md +2 -0
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +198 -83
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
@@ -549,7 +549,7 @@ async function render(page, params) {
549
549
  try { await initShareCtx() } catch (e) { console.warn('[ShareCtx]', e) }
550
550
 
551
551
  // 未登录时只允许看登录页、找回密钥页、商品、welcome 预发布页、governance-onboarding 公开页
552
- if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== 'governance-onboarding' && page !== '') {
552
+ if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== 'governance-onboarding' && page !== 'contribute' && page !== '') {
553
553
  // 保存目标 hash 以便登录/注册后跳回
554
554
  if (location.hash && !['#login', '#recover'].includes(location.hash)) {
555
555
  sessionStorage.setItem('webaz_intended_hash', location.hash)
@@ -562,7 +562,7 @@ async function render(page, params) {
562
562
  const { status, body } = await apiWithStatus('GET', '/me')
563
563
  if (status === 200 && body && !body.error) {
564
564
  state.user = body
565
- connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800)
565
+ connectSSE(); maybePromptPlacementBind(); refreshCartBadge(); maybeShowInstallBanner(); maybeClaimPendingProductShare(); maybeClaimPendingShopReferral(); setTimeout(refreshCompareBadge, 0); refreshChatsBadge(); refreshFeedbackBadge(); refreshSnfBadge(); setTimeout(ensureSnfPull, 800)
566
566
  try { p2pStart() } catch (e) { console.warn('[P2P] start err', e) }
567
567
  } else if (status === 401 || status === 403) {
568
568
  // 真·鉴权失败:清掉两层存储
@@ -632,6 +632,12 @@ async function render(page, params) {
632
632
  case 'welcome': return renderWelcome(app)
633
633
  // 2026-06-02 W3.5-B:#governance-onboarding 治理岗位申请页(公开页)
634
634
  case 'governance-onboarding': return renderGovernanceOnboarding(app)
635
+ // PR9E-1 公开共建页(无需登录):任务板 / 详情 + agent 提示 / 建议任务
636
+ case 'contribute':
637
+ if (params[0] === 'tasks' && params[1] === 'suggest') return renderContributeSuggest(app)
638
+ if (params[0] === 'tasks' && params[1]) return renderContributeTaskDetail(app, params[1])
639
+ if (params[0] === 'suggest') return renderContributeSuggest(app)
640
+ return renderContributeTasks(app)
635
641
  case 'seller':
636
642
  if (state.user?.role === 'logistics') return renderLogistics(app)
637
643
  if (state.user?.role === 'arbitrator') return renderDisputeList(app)
@@ -705,6 +711,7 @@ async function render(page, params) {
705
711
  if (params[0] === 'kpi') return renderAdminKPI(app)
706
712
  // 2026-05-24 #welcome 公开 ideas/邮箱订阅查看
707
713
  if (params[0] === 'public-ideas') return renderAdminPublicIdeas(app)
714
+ if (params[0] === 'task-proposals') return renderAdminTaskProposals(app)
708
715
  if (params[0] === 'params') return renderAdminParams(app)
709
716
  if (params[0] === 'timeline' && params[1]) return renderAdminUserTimeline(app, params[1])
710
717
  if (params[0] === 'timeline') return renderAdminUserTimelinePicker(app)
@@ -1304,7 +1311,7 @@ async function renderMyAdvanced(app) {
1304
1311
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
1305
1312
  ${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
1306
1313
  ${card('⚖', t('判例库'), t('争议判决公开'), '#judgments')}
1307
- ${card('🎁', t('共建身份'), t('申请制 · 含锁仓金'), '#rewards-me')}
1314
+ ${card('🎁', t('分享分润身份'), t('申请制 · 含锁仓金'), '#rewards-me')}
1308
1315
  </div>
1309
1316
 
1310
1317
  <div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📧 ${t('联系我们')}</div>
@@ -3096,6 +3103,83 @@ window.setAdminIdeaStatus = async (id, status) => {
3096
3103
  setTimeout(() => renderAdminPublicIdeas(document.getElementById('app')), 300)
3097
3104
  }
3098
3105
 
3106
+ // PR9I — Task Proposal Inbox admin review (maintainer-only). Calls the #331 admin endpoints. A proposal is
3107
+ // a SUGGESTION, never a contribution fact / reward / participation; "Convert" only records the review
3108
+ // decision + the proposer→reviewer→ref evidence chain — it does NOT auto-create a build_task.
3109
+ async function renderAdminTaskProposals(app) {
3110
+ if (!state.user) { renderLogin(); return }
3111
+ if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
3112
+ const en = window._lang === 'en'
3113
+ const T = (zh, e) => en && e ? e : zh
3114
+ app.innerHTML = shell(loading$(), 'admin')
3115
+ const sf = state._proposalStatus || '' // '' | new | needs_info | rejected | converted
3116
+ const r = await GET('/admin/task-proposals' + (sf ? '?status=' + encodeURIComponent(sf) : ''))
3117
+ if (r.error) { app.innerHTML = shell(alert$('error', r.error), 'admin'); return }
3118
+ const proposals = r.proposals || []
3119
+ const notice = en ? (r.value_boundary?.notice_en || '') : (r.value_boundary?.notice_zh || '')
3120
+ const STATUS = {
3121
+ new: { bg: '#fef3c7', fg: '#92400e', label: T('待审', 'New') },
3122
+ needs_info: { bg: '#dbeafe', fg: '#1e40af', label: T('待补充', 'Needs info') },
3123
+ rejected: { bg: '#fee2e2', fg: '#991b1b', label: T('已拒绝', 'Rejected') },
3124
+ converted: { bg: '#dcfce7', fg: '#166534', label: T('已转任务', 'Converted') },
3125
+ }
3126
+ 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
+ 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
+ const field = (label, val) => val ? `<div style="font-size:12px;color:#374151;margin-top:4px"><b>${label}:</b> ${escHtml(String(val))}</div>` : ''
3129
+ const row = (p) => {
3130
+ const terminal = p.status === 'rejected' || p.status === 'converted'
3131
+ return `<div class="card" style="padding:14px;margin-bottom:10px">
3132
+ <div style="display:flex;justify-content:space-between;gap:8px;align-items:flex-start">
3133
+ <div style="font-weight:600;font-size:14px">${escHtml(p.title)}</div>${badge(p.status)}
3134
+ </div>
3135
+ <div style="font-size:13px;color:#52525B;line-height:1.5;margin-top:6px;white-space:pre-wrap">${escHtml(p.summary)}</div>
3136
+ ${field(T('建议领域', 'Area'), p.suggested_area)}
3137
+ ${field(T('预期结果', 'Outcome'), p.expected_outcome)}
3138
+ ${field(T('参考', 'Source ref'), p.source_ref)}
3139
+ ${field('GitHub', p.proposer_github_login)}
3140
+ ${field(T('提交时间', 'Created'), p.created_at)}
3141
+ ${field(T('审阅备注', 'Review note'), p.review_note)}
3142
+ ${field(T('已关联', 'Converted ref'), p.converted_ref)}
3143
+ ${terminal
3144
+ ? `<div style="font-size:11px;color:#9ca3af;margin-top:8px">${T('终态,不可再审', 'Terminal — locked')}${p.reviewer_id ? ' · ' + escHtml(String(p.reviewer_id)) : ''}</div>`
3145
+ : `<div style="margin-top:10px;border-top:1px solid #f1f1f4;padding-top:10px">
3146
+ <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
+ <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
+ <div style="display:flex;gap:6px;margin-top:8px;flex-wrap:wrap">
3149
+ <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
+ <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>
3152
+ </div>
3153
+ </div>`}
3154
+ </div>`
3155
+ }
3156
+ app.innerHTML = shell(`
3157
+ <div style="padding:14px;max-width:920px;margin:0 auto">
3158
+ <h1 class="page-title">🛠️ ${T('任务建议收件箱', 'Task Proposal Inbox')}</h1>
3159
+ <div style="background:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:10px;font-size:11px;color:#52525B;line-height:1.6;margin-bottom:12px">
3160
+ ${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
+ ${notice ? `<br>${escHtml(notice)}` : ''}
3162
+ </div>
3163
+ <div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px">
3164
+ ${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
+ </div>
3166
+ ${proposals.length === 0 ? `<div style="color:#a1a1aa;text-align:center;padding:30px;font-size:14px">${T('收件箱为空', 'Inbox is empty')}</div>` : proposals.map(row).join('')}
3167
+ </div>
3168
+ `, 'admin')
3169
+ }
3170
+ window.setProposalStatusFilter = (s) => { state._proposalStatus = s; renderAdminTaskProposals(document.getElementById('app')) }
3171
+ window.reviewProposal = async (id, status) => {
3172
+ const note = (document.getElementById('pr-note-' + id)?.value || '').trim()
3173
+ const ref = (document.getElementById('pr-ref-' + id)?.value || '').trim()
3174
+ const body = { status }
3175
+ if (note) body.note = note
3176
+ if (status === 'converted' && ref) body.converted_ref = ref
3177
+ const r = await POST('/admin/task-proposals/' + encodeURIComponent(id) + '/review', body)
3178
+ if (r.error) { toast$(r.error || r.error_code || (window._lang === 'en' ? 'failed' : '操作失败')); return }
3179
+ toast$(window._lang === 'en' ? 'Updated' : '已更新')
3180
+ renderAdminTaskProposals(document.getElementById('app'))
3181
+ }
3182
+
3099
3183
  async function renderAdminKPI(app) {
3100
3184
  if (!state.user) { renderLogin(); return }
3101
3185
  if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
@@ -4315,10 +4399,10 @@ async function renderApplyRewards(app) {
4315
4399
 
4316
4400
  if (status.opted_in) {
4317
4401
  app.innerHTML = shell(`
4318
- <h1 class="page-title">${t('已是共建身份')}</h1>
4402
+ <h1 class="page-title">${t('已开通分享分润')}</h1>
4319
4403
  <div class="card" style="background:#ecfdf5;border-color:#86efac;padding:12px;font-size:13px;color:#064e3b">
4320
4404
  ${t('你已 opted-in。如需查看状态或退出,前往 ')}
4321
- <a href="#rewards-me" style="color:#16a34a;font-weight:600">${t('共建身份管理')}</a>
4405
+ <a href="#rewards-me" style="color:#16a34a;font-weight:600">${t('分享分润管理')}</a>
4322
4406
  </div>
4323
4407
  `, returnNav)
4324
4408
  return
@@ -4347,7 +4431,7 @@ async function renderApplyRewards(app) {
4347
4431
  </div>`
4348
4432
 
4349
4433
  app.innerHTML = shell(`
4350
- <h1 class="page-title">🎁 ${t('申请共建身份')}</h1>
4434
+ <h1 class="page-title">🎁 ${t('申请分享分润')}</h1>
4351
4435
 
4352
4436
  <div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
4353
4437
  <div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程与购物无关')}</div>
@@ -4355,6 +4439,10 @@ async function renderApplyRewards(app) {
4355
4439
  ${t('你可以随时退出,不影响任何已下单或未来订单。本流程涉及经济关系登记(三级佣金 + 积分配对),请仔细阅读全部条款。')}<br>
4356
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>
4357
4441
  </div>
4442
+ <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
+ ${t('注:本「分享分润 / rewards opt-in」仅为 commission / PV / escrow 经济关系登记,不是贡献资格,与贡献任务(#contribute/tasks)/ 我的共建 无关。RFC-002 同意书可能沿用更早措辞,以本 UI 含义为准。')}<br>
4444
+ <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
+ </div>
4358
4446
  </div>
4359
4447
 
4360
4448
  <div class="card" style="margin-bottom:16px">
@@ -4463,7 +4551,7 @@ window.doSubmitRewardsApply = async (consentVersion, pageLoadedAt) => {
4463
4551
  const drainNote = drained.count > 0
4464
4552
  ? '<br>' + t('已从 escrow 拨回:') + ' ' + drained.total + ' WAZ (' + drained.count + ' ' + t('笔') + ')'
4465
4553
  : ''
4466
- msg.innerHTML = alert$('success', t('✅ 共建身份激活成功') + drainNote)
4554
+ msg.innerHTML = alert$('success', t('✅ 分享分润开通成功') + drainNote)
4467
4555
  setTimeout(() => { location.hash = '#rewards-me' }, 2000)
4468
4556
  }
4469
4557
 
@@ -4474,7 +4562,7 @@ async function renderRewardsMe(app) {
4474
4562
  if (status.error) { app.innerHTML = shell(alert$('error', status.error), 'me'); return }
4475
4563
 
4476
4564
  const stateLabel = {
4477
- opted_in: { icon: '✅', color: '#16a34a', label: t('共建身份激活中') },
4565
+ opted_in: { icon: '✅', color: '#16a34a', label: t('分享分润已开通') },
4478
4566
  never_activated: { icon: '⚪', color: '#6b7280', label: t('未激活') },
4479
4567
  auto_downgraded: { icon: '⚠️', color: '#f59e0b', label: t('已自动降级(consent 未确认)') },
4480
4568
  deactivated: { icon: '🔒', color: '#9ca3af', label: t('已退出') },
@@ -4484,7 +4572,7 @@ async function renderRewardsMe(app) {
4484
4572
  const expired = status.expired_to_charity || { count: 0, total_amount: 0 }
4485
4573
 
4486
4574
  app.innerHTML = shell(`
4487
- <h1 class="page-title">🎁 ${t('共建身份管理')}</h1>
4575
+ <h1 class="page-title">🎁 ${t('分享分润管理')}</h1>
4488
4576
 
4489
4577
  <div class="card" style="margin-bottom:16px;border-left:4px solid ${stateLabel.color};padding:14px">
4490
4578
  <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
@@ -4498,27 +4586,27 @@ async function renderRewardsMe(app) {
4498
4586
  <div class="card" style="margin-bottom:16px;background:#fef3c7;border-color:#fde68a;padding:12px">
4499
4587
  <div style="font-weight:600;color:#92400e;font-size:13px;margin-bottom:4px">💰 ${t('待领 escrow')}</div>
4500
4588
  <div style="font-size:13px;color:#7c2d12">${escrow.count} ${t('笔')} · ${escrow.total_amount} WAZ</div>
4501
- <div style="font-size:11px;color:#a16207;margin-top:6px">${t('opt-in 后这笔钱会拨回钱包。30 天未领过期入公益。')}</div>
4589
+ <div style="font-size:11px;color:#a16207;margin-top:6px">${t('opt-in 后这笔钱会拨回钱包。30 天未领过期入协议公池。')}</div>
4502
4590
  </div>
4503
4591
  ` : ''}
4504
4592
 
4505
4593
  ${expired.count > 0 ? `
4506
4594
  <div class="card" style="margin-bottom:16px;background:#f3f4f6;padding:10px;font-size:12px;color:#6b7280">
4507
- ${t('历史过期入公益')}: ${expired.count} ${t('笔')} · ${expired.total_amount} WAZ
4595
+ ${t('历史过期入协议公池')}: ${expired.count} ${t('笔')} · ${expired.total_amount} WAZ
4508
4596
  </div>
4509
4597
  ` : ''}
4510
4598
 
4511
4599
  ${status.state === 'opted_in' ? `
4512
4600
  <div class="card" style="margin-bottom:16px;padding:14px">
4513
- <div style="font-size:13px;color:#374151;margin-bottom:10px">${t('退出共建身份后,未来 commission 直接入公益(无 escrow)。可随时再申请。')}</div>
4601
+ <div style="font-size:13px;color:#374151;margin-bottom:10px">${t('退出分享分润后,未来 commission 进入协议公池(commission_reserve),不是慈善基金。可随时重新开通。')}</div>
4514
4602
  <button class="btn" style="background:#fee2e2;color:#991b1b;border:1px solid #fca5a5" onclick="doDeactivateRewards()">
4515
- 🔒 ${t('退出共建身份')}
4603
+ 🔒 ${t('退出分享分润')}
4516
4604
  </button>
4517
4605
  <div id="rwd-deact-msg" style="margin-top:10px"></div>
4518
4606
  </div>
4519
4607
  ` : `
4520
4608
  <button class="btn btn-primary" onclick="location.hash='#apply-rewards'">
4521
- 🎁 ${status.state === 'never_activated' ? t('申请共建身份') : t('重新申请')}
4609
+ 🎁 ${status.state === 'never_activated' ? t('申请分享分润') : t('重新申请')}
4522
4610
  </button>
4523
4611
  `}
4524
4612
 
@@ -4531,7 +4619,7 @@ async function renderRewardsMe(app) {
4531
4619
  window.doDeactivateRewards = async () => {
4532
4620
  const msg = document.getElementById('rwd-deact-msg')
4533
4621
  if (!msg) return
4534
- if (!confirm(t('确认退出共建身份?未来 commission 将直接入公益,可随时再申请。'))) return
4622
+ if (!confirm(t('确认退出分享分润?未来 commission 将进入协议公池(commission_reserve,非慈善基金),可随时重新开通。'))) return
4535
4623
 
4536
4624
  msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
4537
4625
  let passkeyToken
@@ -4548,7 +4636,7 @@ window.doDeactivateRewards = async () => {
4548
4636
  msg.innerHTML = alert$('error', res.error)
4549
4637
  return
4550
4638
  }
4551
- msg.innerHTML = alert$('success', t('✅ 已退出共建身份'))
4639
+ msg.innerHTML = alert$('success', t('✅ 已退出分享分润'))
4552
4640
  setTimeout(() => renderRewardsMe(document.getElementById('app')), 1200)
4553
4641
  }
4554
4642
 
@@ -5815,7 +5903,7 @@ function renderLogin() {
5815
5903
  <!-- ② 主 CTA:参与共建 -->
5816
5904
  <div style="text-align:center;margin-top:clamp(20px,3.6vh,34px)">
5817
5905
  <button onclick="openParticipateSheet('${defaultTab}')" style="padding:13px 48px;font-size:16px;font-weight:600;width:auto;min-width:220px;border-radius:99px;border:none;cursor:pointer;background:linear-gradient(135deg,#6366f1 0%,#8b5cf6 100%);color:#fff;box-shadow:0 12px 28px rgba(99,102,241,0.32),0 4px 8px rgba(99,102,241,0.18);letter-spacing:0.2px;transition:all 0.2s ease;font-family:inherit" onmouseover="this.style.transform='translateY(-1px)';this.style.boxShadow='0 16px 36px rgba(99,102,241,0.38),0 6px 12px rgba(99,102,241,0.22)'" onmouseout="this.style.transform='';this.style.boxShadow='0 12px 28px rgba(99,102,241,0.32),0 4px 8px rgba(99,102,241,0.18)'">
5818
- ${window._lang === 'en' ? 'Participate' : '参与共建'}
5906
+ ${window._lang === 'en' ? 'Get started' : '开始使用'}
5819
5907
  </button>
5820
5908
  </div>
5821
5909
 
@@ -6046,15 +6134,15 @@ function renderWelcome(app) {
6046
6134
  <!-- 区块 1: Buyers -->
6047
6135
  <section class="w-section">
6048
6136
  <h2 style="${H2_STYLE}">${T('作为买家<br>你不只是买家', "As a buyer<br>you're more than a buyer")}</h2>
6049
- <p style="${SUB_STYLE}">${T('消费也是贡献,贡献就有回报', 'To spend is to build. To build is to own.')}</p>
6137
+ <p style="${SUB_STYLE}">${T('真实使用,留下可验证的记录', 'Use for real leave a verifiable record.')}</p>
6050
6138
  <div class="w-cards">
6051
6139
  <div class="w-card">
6052
- <div class="w-card-title">🎁 ${T('你的购物体验可以赚回来', 'Your shopping experience pays back')}</div>
6140
+ <div class="w-card-title">🛍 ${T('真实使用,真实记录', 'Real use, real record')}</div>
6053
6141
  <ul class="w-card-list">
6054
6142
  <li>${T('真实购买,真实体验', 'Buy real. Use real. Review real.')}</li>
6055
- <li>${T('写测评 / 推荐朋友 / 发视频', 'Write a review / post a video')}</li>
6056
- <li>${T('达标返现,购物费回到你口袋', 'Hit the bar, get your money back')}</li>
6057
- <li>${T('你是买家、创作者、贡献者——一体', 'Buyer, creator, contributor one')}</li>
6143
+ <li>${T('你的体验可以帮助后来者判断', 'Your experience helps others decide')}</li>
6144
+ <li>${T('购物、评价、分享是使用记录', 'Shopping, reviews, sharing are usage records')}</li>
6145
+ <li>${T('想正式贡献?走公开任务板 #contribute/tasks', 'Want to contribute? Use the public task board #contribute/tasks')}</li>
6058
6146
  </ul>
6059
6147
  </div>
6060
6148
  <div class="w-card">
@@ -6105,18 +6193,18 @@ function renderWelcome(app) {
6105
6193
  <div class="w-card">
6106
6194
  <div class="w-card-title">🎯 ${T('跨平台引流,数据归你', 'Cross-platform traffic, your data')}</div>
6107
6195
  <ul class="w-card-list">
6108
- <li>${T('在小红书、抖音、B 站、IG、TikTok 发内容', 'Post on Xiaohongshu, Douyin, Bilibili, IG, TikTok')}</li>
6196
+ <li>${T('在小红书、抖音、B 站、视频号发内容', 'Post on TikTok, Instagram, YouTube, Reddit, or Pinterest')}</li>
6109
6197
  <li>${T('用 webaz 口令引流', 'Drive traffic with webaz codes')}</li>
6110
6198
  <li>${T('真实访问、真实转化都看得见', 'See real visits and conversions')}</li>
6111
6199
  <li>${T("数据是你的,不是平台的", "Data is yours, not the platform's")}</li>
6112
6200
  </ul>
6113
6201
  </div>
6114
6202
  <div class="w-card">
6115
- <div class="w-card-title">💰 ${T('真实带货,实在回报', 'Real sales, real rewards')}</div>
6203
+ <div class="w-card-title">🔗 ${T('分享与归因', 'Sharing & attribution')}</div>
6116
6204
  <ul class="w-card-list">
6117
- <li>${T('真实购买 真实测评 → 真实免单', 'Real purchase Real review Real refund')}</li>
6118
- <li>${T('你带回的客户、你产生的成交,都能转化为你的收益', 'Customers you bring, sales you drive all turn into earnings')}</li>
6119
- <li>${T('不靠广告分成,不靠平台施舍', 'No ad shares. No platform alms')}</li>
6205
+ <li>${T('分享商品 / 内容,留下可追溯的归因记录', 'Share products / content leave a traceable attribution record')}</li>
6206
+ <li>${T('符合条件并完成「分享分润」opt-in 后,协议按规则处理分润', 'If eligible and after you opt in to share-commission, the protocol settles commission per the rules')}</li>
6207
+ <li>${T('不承诺收益;内容发布不等同于正式贡献', 'No promised earnings; publishing content is not formal contribution')}</li>
6120
6208
  </ul>
6121
6209
  </div>
6122
6210
  </div>
@@ -6177,10 +6265,10 @@ function renderWelcome(app) {
6177
6265
  )}
6178
6266
  ${renderTL(
6179
6267
  '19:00',
6180
- T('你作为创作者', 'You as a creator'),
6181
- T('小红书发护手霜测评 · 带 webaz 口令', 'Post the review on Xiaohongshu · with a webaz code'),
6182
- T('使用 + 建设 + 贡献', 'Use + Build + Contribute'),
6183
- T('别人凭口令购买 → 你拿推广分成 · 真实推广,真实回报', 'Others buy via your code → you earn share · real promotion, real return')
6268
+ T('你作为分享者', 'You as a sharer'),
6269
+ T('小红书发护手霜测评 · 带 webaz 口令', 'Post the review on TikTok · with a webaz code'),
6270
+ T('使用 + 分享', 'Use + Share'),
6271
+ T('别人凭口令购买 → 留下归因记录(分润需单独 opt-in)', 'Others buy via your code → an attribution record (commission needs a separate opt-in)')
6184
6272
  )}
6185
6273
  ${renderTL(
6186
6274
  '22:00',
@@ -6214,7 +6302,7 @@ function renderWelcome(app) {
6214
6302
  <option value="">${T('我想以什么身份开始?(可选)', 'How would you like to start? (optional)')}</option>
6215
6303
  <option value="buyer">${T('买家 · Agent 帮我找货', 'Buyer · let agents find me deals')}</option>
6216
6304
  <option value="seller">${T('卖家 · 上架商品并获取 Earn-Back', 'Seller · list products and earn back')}</option>
6217
- <option value="creator">${T('创作者 · 测评、笔记、推广分成', 'Creator · reviews, notes, promotion share')}</option>
6305
+ <option value="creator">${T('分享者 · 测评、笔记、内容归因', 'Sharer · reviews, notes, content attribution')}</option>
6218
6306
  <option value="verifier">${T('审核员 · 链接 / 内容验证', 'Verifier · link & content verification')}</option>
6219
6307
  <option value="arbitrator">${T('仲裁员 · 争议裁决', 'Arbitrator · dispute resolution')}</option>
6220
6308
  <option value="other">${T('其他 / 都看看', 'Other / just curious')}</option>
@@ -6224,6 +6312,16 @@ function renderWelcome(app) {
6224
6312
  <div id="w-email-msg" style="font-size:12px;text-align:center;min-height:1.5em"></div>
6225
6313
  </div>
6226
6314
  </div>
6315
+ <div class="w-card w-join-card">
6316
+ <div class="w-join-card-left">
6317
+ <div class="w-card-title">🛠 ${T('想直接改进 WebAZ?', 'Want to improve WebAZ directly?')}</div>
6318
+ <div class="w-card-desc">${T('公开任务板:浏览 / 认领可做的任务,或提个新任务。建议无需登录;认领与提交需登录。只有 canonical 仓库被合并的 PR(或维护者认可的 issue / task / RFC)才进入贡献记录 —— sandbox、本地草稿、普通购物 / 分享都不是正式贡献。', 'Public task board: browse / claim tasks, or propose a new one. Suggesting needs no login; claiming & submitting need login. Only a merged PR on the canonical repo (or a maintainer-recognized issue / task / RFC) enters the contribution record — a sandbox run, a local draft, or ordinary shopping / sharing is not formal contribution.')}</div>
6319
+ </div>
6320
+ <div class="w-join-card-right">
6321
+ <a class="w-btn-full w-btn-outline" href="#contribute/tasks" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('公开任务板', 'Public task board')}</a>
6322
+ <a class="w-btn-full w-btn-outline" href="#contribute/tasks/suggest" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('建议新任务', 'Suggest a task')}</a>
6323
+ </div>
6324
+ </div>
6227
6325
  <div class="w-card w-join-card">
6228
6326
  <div class="w-join-card-left">
6229
6327
  <div class="w-card-title">💡 ${T('随便逛逛?提个建议', 'Just browsing? Drop an idea')}</div>
@@ -6389,7 +6487,7 @@ async function renderGovernanceOnboarding(app) {
6389
6487
  <ul style="margin:0;padding-left:20px;color:#1e3a8a;line-height:1.8;font-size:14px">
6390
6488
  <li><strong>arbitrator</strong> — ${T('仲裁纠纷 dispute(Iron-Rule 真人 Passkey 必备)', 'Arbitrate disputes (Iron-Rule requires real human Passkey)')}</li>
6391
6489
  <li><strong>verifier</strong> — ${T('验证 claim(同 Iron-Rule)', 'Verify claims (same Iron-Rule)')}</li>
6392
- <li>${T('卸任 30 天冷却 / outlier 自动 deactivate / 申诉路径', 'Resignation 30d cooldown / outlier auto-deactivate / appeal path')}</li>
6490
+ <li>${T('卸任 30 天冷却 · outlier 是风险信号,经复核确认错误(confirmed-wrong)才触发 auto-deactivate · 有申诉路径', 'Resignation 30d cooldown · an outlier is a risk signal — auto-deactivate only triggers when review confirms it wrong (confirmed-wrong) · with an appeal path')}</li>
6393
6491
  </ul>
6394
6492
  </section>
6395
6493
 
@@ -6460,6 +6558,8 @@ async function renderGovernanceOnboarding(app) {
6460
6558
  <div style="font-size:13px;line-height:1.9;color:#3f3f46">
6461
6559
  📅 ${T('注册', 'Registered')} ≥ <strong>${esc(ver.registration_days ?? '?')}</strong> ${T('天', 'd')}<br>
6462
6560
  📦 ${T('完成订单', 'Completed orders')} ≥ <strong>${esc(ver.completed_orders ?? '?')}</strong><br>
6561
+ ⭐ ${T('信誉', 'Reputation')} ≥ <strong>${esc(ver.reputation ?? '?')}</strong><br>
6562
+ 💰 ${T('钱包余额', 'Balance')} ≥ <strong>${esc(ver.balance_waz ?? '?')}</strong> WAZ<br>
6463
6563
  ✉️ ${T('邮箱验证', 'Email verified')} · 🚫 ${T('零仲裁判输', 'Zero disputes lost')} · ✅ ${T('未曾暂停', 'Never suspended')}
6464
6564
  </div>
6465
6565
  </div>
@@ -6471,6 +6571,329 @@ async function renderGovernanceOnboarding(app) {
6471
6571
  }
6472
6572
  }
6473
6573
 
6574
+ // ─── PR9E-1 Public Contribution Pages (#contribute/tasks · /:id · /tasks/suggest) ────────────────────
6575
+ // Public, no-auth PWA surface over the #329/#330/#331 APIs so a stranger + their agent can participate at
6576
+ // low friction. READ + SUGGEST only. Hard boundaries (mirror the server contract):
6577
+ // · Restricted/internal tasks NEVER reach these pages — the list/detail call ONLY the public-scope
6578
+ // endpoints (/api/public/build-tasks[/:id]); the server hides everything but audience=public+open.
6579
+ // · The agent prompt instructs PRs to the CANONICAL repo only (from the response, trusted config); a
6580
+ // sandbox run / local draft is explicitly NOT participation; the agent is only an executor (DCO).
6581
+ // · No economic field is ever rendered (no reward/payout/amount/score) — value stays uncommitted
6582
+ // (RFC-017 I-12). No auto GitHub op, no merge, no PR submission from the browser. Claim needs login.
6583
+ const CONTRIBUTE_PROMPT_STATE = { text: '' } // last-built copy-ready prompt, for the copy button
6584
+ function _cEsc(s) { return escHtml(String(s ?? '')) }
6585
+ function _cArrList(arr, empty) {
6586
+ if (!Array.isArray(arr) || arr.length === 0) return `<div style="color:#a1a1aa;font-size:13px">${_cEsc(empty)}</div>`
6587
+ return '<ul style="margin:6px 0 0;padding-left:18px;line-height:1.8">' + arr.map(x => `<li><code style="font-size:12px;word-break:break-all">${_cEsc(x)}</code></li>`).join('') + '</ul>'
6588
+ }
6589
+ function _cBoundaryHTML(vb, T) {
6590
+ if (!vb) return ''
6591
+ const notice = window._lang === 'en' ? (vb.notice_en || '') : (vb.notice_zh || '')
6592
+ return `<div style="background:#f4f4f5;border:1px solid #e4e4e7;border-radius:8px;padding:12px;font-size:12px;color:#52525B;line-height:1.6;margin:12px 0">
6593
+ 🔒 ${T('价值边界', 'Value boundary')}: <strong>${_cEsc(vb.value_state || 'uncommitted')}</strong> · ${_cEsc(vb.boundary_ref || 'RFC-017 I-12')}<br>${_cEsc(notice)}</div>`
6594
+ }
6595
+ function _cRiskBadge(level, T) {
6596
+ if (!level) return ''
6597
+ const c = { low: '#16a34a', medium: '#d97706', high: '#dc2626', critical: '#7f1d1d' }[level] || '#6b7280'
6598
+ return `<span style="background:${c}1a;color:${c};padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('风险', 'risk')}:${_cEsc(level)}</span>`
6599
+ }
6600
+ function _cDuration(d, T) {
6601
+ if (!d || (d.min_minutes == null && d.max_minutes == null)) return ''
6602
+ const lo = d.min_minutes, hi = d.max_minutes
6603
+ const span = lo != null && hi != null ? `${lo}–${hi}` : (lo ?? hi)
6604
+ return `<span style="font-size:11px;color:#6b7280">⏱ ${T('预计', 'est.')} ${_cEsc(span)} ${T('分钟', 'min')}</span>`
6605
+ }
6606
+
6607
+ // Build a copy-ready agent prompt from the (trusted) public detail payload. Names the boundary, forbidden
6608
+ // paths, prohibited actions, verification commands, the canonical-repo PR requirement, and that a
6609
+ // sandbox/local draft is NOT participation. It states the contribution confers no economic right (it does
6610
+ // NOT restate any reward/payout/amount/score — value stays uncommitted).
6611
+ function buildContributeAgentPrompt(task, cct, T) {
6612
+ const m = task.agent_metadata || {}
6613
+ const join = (a, sep) => (Array.isArray(a) && a.length ? a.join(sep) : null)
6614
+ const L = []
6615
+ L.push(`# WebAZ contribution task: ${task.title || task.task_id || ''}`)
6616
+ L.push(`Task id: ${task.task_id || ''}`)
6617
+ if (task.area) L.push(`Area: ${task.area}`)
6618
+ if (m.task_type) L.push(`Type: ${m.task_type}`)
6619
+ L.push('')
6620
+ L.push('## Boundary — stay strictly inside this')
6621
+ L.push('Allowed paths: ' + (join(m.allowed_paths, ', ') || '(none specified — ask the maintainer before editing)'))
6622
+ L.push('Forbidden paths (NEVER touch): ' + (join(m.forbidden_paths, ', ') || '(none listed)'))
6623
+ L.push('Prohibited actions (NEVER do): ' + (join(m.prohibited_actions, '; ') || '(none listed)'))
6624
+ if (join(m.required_capabilities, ', ')) L.push('Required capabilities: ' + join(m.required_capabilities, ', '))
6625
+ L.push('')
6626
+ L.push('## Acceptance & verification')
6627
+ L.push('Acceptance criteria: ' + (join(m.acceptance_criteria, '; ') || '(see task)'))
6628
+ L.push('Verification commands (must pass): ' + (join(m.verification_commands, ' && ') || '(see task)'))
6629
+ if (m.expected_results) L.push('Expected results: ' + m.expected_results)
6630
+ L.push('Deliverables: ' + (join(m.deliverables, '; ') || '(see task)'))
6631
+ if (m.definition_of_done) L.push('Definition of done: ' + m.definition_of_done)
6632
+ L.push('')
6633
+ L.push('## Submitting — canonical repository only')
6634
+ L.push(`Open a Pull Request whose BASE repository is the canonical WebAZ repo: ${cct.expected_pr_base_repo || ''} (${cct.canonical_github_url || ''}), base branch ${cct.base_branch || 'main'}.`)
6635
+ L.push('If any target repo differs from this canonical repo, STOP and ask the human to confirm — never contribute to a non-canonical repository.')
6636
+ L.push('Sign every commit (DCO): `git commit -s`. A real human/org is the accountable party; the agent is only an executor.')
6637
+ L.push('')
6638
+ L.push('## Not participation')
6639
+ L.push('A sandbox run or a local-only 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.')
6640
+ L.push('This task confers no economic or redemption right; contribution value is uncommitted (RFC-017 I-12).')
6641
+ return L.join('\n')
6642
+ }
6643
+
6644
+ window.contributeApplyFilters = () => {
6645
+ const area = (document.getElementById('ct-f-area')?.value || '').trim()
6646
+ const risk = document.getElementById('ct-f-risk')?.value || ''
6647
+ const ac = document.getElementById('ct-f-ac')?.value || ''
6648
+ const agentcaps = (document.getElementById('ct-f-agentcaps')?.value || '').trim()
6649
+ const maxdur = (document.getElementById('ct-f-maxdur')?.value || '').trim()
6650
+ const ctx = document.getElementById('ct-f-ctx')?.value || ''
6651
+ const budget = document.getElementById('ct-f-budget')?.value || ''
6652
+ const p = new URLSearchParams()
6653
+ if (area) p.set('area', area)
6654
+ if (risk) p.set('risk_level', risk)
6655
+ if (ac) p.set('auto_claimable', ac)
6656
+ if (agentcaps) p.set('agent_capabilities', agentcaps)
6657
+ if (maxdur) p.set('max_duration_minutes', maxdur)
6658
+ if (ctx) p.set('estimated_context_size', ctx)
6659
+ if (budget) p.set('estimated_agent_budget', budget)
6660
+ const qs = p.toString()
6661
+ location.hash = '#contribute/tasks' + (qs ? '?' + qs : '')
6662
+ }
6663
+
6664
+ window.contributeCopyPrompt = async () => {
6665
+ const msg = document.getElementById('ct-prompt-msg')
6666
+ try {
6667
+ await navigator.clipboard.writeText(CONTRIBUTE_PROMPT_STATE.text || '')
6668
+ if (msg) msg.textContent = window._lang === 'en' ? '✓ Copied' : '✓ 已复制'
6669
+ } catch {
6670
+ // clipboard unavailable (insecure context / permission) — select the textarea so the user can copy
6671
+ const ta = document.getElementById('ct-prompt'); if (ta) { ta.focus(); ta.select() }
6672
+ if (msg) msg.textContent = window._lang === 'en' ? 'Select the text above and copy manually' : '请选中上方文本手动复制'
6673
+ }
6674
+ }
6675
+
6676
+ window.contributeClaim = async (id) => {
6677
+ const T = (zh, e) => window._lang === 'en' && e ? e : zh
6678
+ if (!state.apiKey) { // not logged in → save intended task and go to login
6679
+ try { sessionStorage.setItem('webaz_intended_hash', '#contribute/tasks/' + id) } catch {}
6680
+ navigate('#login'); return
6681
+ }
6682
+ const { status, body } = await apiWithStatus('POST', '/build-tasks/' + id + '/claim', { provenance: 'unspecified' })
6683
+ if (status === 200 && body && !body.error) { toast$(T('已认领 — 去「我的共建」查看', 'Claimed — see “My contributions”')); route(true); return }
6684
+ toast$((body && (body.error || body.error_code)) || T('认领失败', 'Claim failed'))
6685
+ }
6686
+
6687
+ window.submitContributeProposal = async () => {
6688
+ const T = (zh, e) => window._lang === 'en' && e ? e : zh
6689
+ const val = (id) => (document.getElementById(id)?.value || '').trim()
6690
+ const out = document.getElementById('ct-suggest-result')
6691
+ const body = {
6692
+ title: val('cs-title'), summary: val('cs-summary'),
6693
+ suggested_area: val('cs-area') || undefined, expected_outcome: val('cs-outcome') || undefined,
6694
+ source_ref: val('cs-source') || undefined, proposer_github_login: val('cs-login') || undefined,
6695
+ }
6696
+ if (out) out.innerHTML = `<div style="color:#a1a1aa;font-size:13px">${T('提交中…', 'Submitting…')}</div>`
6697
+ const { status, body: r } = await apiWithStatus('POST', '/public/task-proposals', body)
6698
+ if (status === 200 && r && r.proposal) {
6699
+ const notice = window._lang === 'en' ? (r.value_boundary?.notice_en || '') : (r.value_boundary?.notice_zh || '')
6700
+ if (out) out.innerHTML = `<div style="background:#dcfce7;border:1px solid #16a34a;border-radius:8px;padding:14px">
6701
+ <div style="font-weight:600;color:#166534">✓ ${T('已收到你的建议', 'Your suggestion was received')}</div>
6702
+ <div style="font-size:13px;color:#15803d;margin-top:6px">${T('建议编号', 'Proposal id')}: <code>${_cEsc(r.proposal.id)}</code></div>
6703
+ <div style="font-size:12px;color:#15803d;margin-top:8px">${T('这是一条建议,不是贡献事实 / 奖励 / 正式参与。它进入维护者收件箱待审,绝不会自动变成正式任务或出现在公开任务板。', 'This is a SUGGESTION — not a contribution fact, not a reward, not formal participation. It enters the maintainer review inbox and never auto-becomes a task or appears on the public board.')}</div>
6704
+ <div style="font-size:11px;color:#52525B;margin-top:8px">${_cEsc(notice)}</div></div>`
6705
+ return
6706
+ }
6707
+ // typed errors — make RATE_LIMITED / DUPLICATE_PROPOSAL / validation legible
6708
+ const code = (r && r.error_code) || ''
6709
+ let human
6710
+ if (status === 429 || code === 'RATE_LIMITED') human = T('提交太频繁,请稍后再试(每小时有上限)。', 'Too many submissions — please try again later (there is an hourly limit).')
6711
+ else if (status === 409 || code === 'DUPLICATE_PROPOSAL') human = T('相同建议已在收件箱中,请勿重复提交。', 'An identical suggestion is already in the inbox — no need to submit it again.') + (r && r.existing_id ? ` (#${_cEsc(r.existing_id)})` : '')
6712
+ else human = ((r && r.error) || T('提交失败,请检查输入。', 'Submission failed — please check your input.')) + (code ? ` [${_cEsc(code)}]` : '')
6713
+ if (out) out.innerHTML = `<div style="background:#fee2e2;border:1px solid #ef4444;border-radius:8px;padding:14px;color:#991b1b;font-size:13px">⚠️ ${human}</div>`
6714
+ }
6715
+
6716
+ 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
+
6718
+ async function renderContributeTasks(app) {
6719
+ const en = window._lang === 'en'
6720
+ const T = (zh, e) => en && e ? e : zh
6721
+ const q = state._urlQuery || {}
6722
+ app.innerHTML = `
6723
+ ${preLaunchBannerHTML()}
6724
+ <div style="${CONTRIBUTE_PAGE_STYLE}">
6725
+ <header style="margin-bottom:20px">
6726
+ <h1 style="font-size:clamp(24px,5vw,32px);margin:0 0 8px;color:#18181B">🛠️ ${T('公开任务板', 'Open Task Board')}</h1>
6727
+ <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>
6728
+ </header>
6729
+ ${_cBoundaryHTML(UNCOMMITTED_VALUE_BOUNDARY_HINT(), T)}
6730
+ <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px">
6731
+ <input id="ct-f-area" placeholder="${T('领域 area', 'area')}" value="${_cEsc(q.area || '')}" style="flex:1;min-width:120px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6732
+ <select id="ct-f-risk" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6733
+ <option value="">${T('风险(全部)', 'risk (all)')}</option>
6734
+ ${['low','medium','high','critical'].map(r => `<option value="${r}" ${q.risk_level === r ? 'selected' : ''}>${r}</option>`).join('')}
6735
+ </select>
6736
+ <select id="ct-f-ac" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6737
+ <option value="">${T('可自动认领(全部)', 'auto-claimable (all)')}</option>
6738
+ <option value="true" ${q.auto_claimable === 'true' ? 'selected' : ''}>${T('仅可自动认领', 'auto-claimable only')}</option>
6739
+ <option value="false" ${q.auto_claimable === 'false' ? 'selected' : ''}>${T('仅需人工认领', 'manual-claim only')}</option>
6740
+ </select>
6741
+ <input id="ct-f-agentcaps" placeholder="${T('你的能力(逗号分隔)→ 你能做的任务', 'your capabilities (comma-sep) → tasks you can do')}" value="${_cEsc(q.agent_capabilities || '')}" title="${T('只显示所需能力都在你能力集内的任务', 'shows only tasks whose required capabilities are all within yours')}" style="flex:1;min-width:160px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6742
+ <input id="ct-f-maxdur" type="number" min="1" placeholder="${T('≤ 分钟', '≤ min')}" value="${_cEsc(q.max_duration_minutes || '')}" title="${T('只显示预计耗时不超过该分钟数的任务', 'tasks whose estimated max duration fits within this many minutes')}" style="width:96px;padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6743
+ <select id="ct-f-ctx" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6744
+ <option value="">${T('上下文(全部)', 'context (all)')}</option>
6745
+ ${['small','medium','large'].map(c => `<option value="${c}" ${q.estimated_context_size === c ? 'selected' : ''}>${c}</option>`).join('')}
6746
+ </select>
6747
+ <select id="ct-f-budget" title="${T('按 agent 预计工作量筛选(不是费用 / 报酬)', 'filter by estimated agent effort (not a cost / payment)')}" style="padding:8px 10px;border:1px solid #d4d4d8;border-radius:8px;font-size:13px">
6748
+ <option value="">${T('agent 工作量(全部)', 'agent effort (all)')}</option>
6749
+ ${['minimal','small','moderate','large','xlarge'].map(b => `<option value="${b}" ${q.estimated_agent_budget === b ? 'selected' : ''}>${b}</option>`).join('')}
6750
+ </select>
6751
+ <button onclick="contributeApplyFilters()" style="padding:8px 16px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer">${T('筛选', 'Filter')}</button>
6752
+ <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
+ </div>
6754
+ <div id="ct-list"><div style="color:#a1a1aa;text-align:center;padding:30px">${T('加载中…', 'Loading…')}</div></div>
6755
+ </div>`
6756
+
6757
+ const params = new URLSearchParams()
6758
+ 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])
6759
+ const list = document.getElementById('ct-list')
6760
+ try {
6761
+ const res = await fetch('/api/public/build-tasks' + (params.toString() ? '?' + params : ''), { signal: AbortSignal.timeout(10000) })
6762
+ const j = await res.json().catch(() => ({}))
6763
+ if (!res.ok) { list.innerHTML = `<div style="background:#fee2e2;border:1px solid #ef4444;border-radius:8px;padding:14px;color:#991b1b;font-size:13px">⚠️ ${_cEsc(j.error || T('筛选无效', 'Invalid filter'))}${j.error_code ? ` [${_cEsc(j.error_code)}]` : ''}</div>`; return }
6764
+ const tasks = j.tasks || []
6765
+ if (tasks.length === 0) { list.innerHTML = `<div style="color:#a1a1aa;text-align:center;padding:30px;font-size:14px">${T('暂无符合条件的公开任务。', 'No open tasks match the filters.')}</div>`; return }
6766
+ list.innerHTML = tasks.map(task => {
6767
+ const m = task.agent_metadata || {}
6768
+ const caps = Array.isArray(m.required_capabilities) && m.required_capabilities.length
6769
+ ? `<div style="font-size:11px;color:#6b7280;margin-top:6px">${T('所需能力', 'capabilities')}: ${m.required_capabilities.map(c => `<code style="font-size:11px">${_cEsc(c)}</code>`).join(' ')}</div>` : ''
6770
+ return `<div onclick="location.hash='#contribute/tasks/${_cEsc(task.task_id)}'" style="background:#fff;border:1px solid #e4e4e7;border-radius:10px;padding:14px;margin-bottom:10px;cursor:pointer">
6771
+ <div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start">
6772
+ <div style="font-weight:600;color:#18181B;font-size:15px">${_cEsc(task.title)}</div>
6773
+ ${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600;white-space:nowrap">${T('可自动认领', 'auto-claimable')}</span>` : ''}
6774
+ </div>
6775
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px">
6776
+ ${task.area ? `<span style="font-size:11px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
6777
+ ${_cRiskBadge(m.risk_level, T)}
6778
+ ${_cDuration(m.estimated_duration, T)}
6779
+ </div>
6780
+ ${caps}
6781
+ <div style="font-size:12px;color:#6366f1;margin-top:8px">${T('查看详情 + 复制 agent 提示', 'View detail + copy agent prompt')} →</div>
6782
+ </div>`
6783
+ }).join('')
6784
+ } catch (e) {
6785
+ list.innerHTML = `<div style="color:#dc2626;text-align:center;padding:20px;font-size:14px">${T('加载失败', 'Load failed')}: ${_cEsc(e.message || 'unknown')}</div>`
6786
+ }
6787
+ }
6788
+
6789
+ async function renderContributeTaskDetail(app, id) {
6790
+ const en = window._lang === 'en'
6791
+ 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>`
6793
+ let j, res
6794
+ try {
6795
+ res = await fetch('/api/public/build-tasks/' + encodeURIComponent(id), { signal: AbortSignal.timeout(10000) })
6796
+ j = await res.json().catch(() => ({}))
6797
+ } 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
6799
+ }
6800
+ if (!res.ok || !j.task) {
6801
+ app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">
6802
+ <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
6804
+ }
6805
+ const task = j.task
6806
+ const cct = j.canonical_contribution_target || {}
6807
+ const m = task.agent_metadata || {}
6808
+ CONTRIBUTE_PROMPT_STATE.text = buildContributeAgentPrompt(task, cct, T)
6809
+ const section = (icon, title, inner) => `<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:18px;margin-bottom:14px">
6810
+ <h3 style="margin:0 0 10px;color:#18181B;font-size:16px">${icon} ${title}</h3>${inner}</section>`
6811
+ const claimBtn = m.auto_claimable
6812
+ ? (state.apiKey
6813
+ ? `<button onclick="contributeClaim('${_cEsc(task.task_id)}')" style="padding:10px 20px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer">✋ ${T('认领此任务', 'Claim this task')}</button>`
6814
+ : `<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
+ : `<div style="font-size:12px;color:#6b7280">${T('此任务需人工认领流程,不可自动认领。', 'This task uses a manual claim flow; it is not auto-claimable.')}</div>`
6816
+
6817
+ app.innerHTML = `${preLaunchBannerHTML()}<div style="${CONTRIBUTE_PAGE_STYLE}">
6818
+ <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
+ <h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">${_cEsc(task.title)}</h1>
6820
+ <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:6px">
6821
+ ${task.area ? `<span style="font-size:12px;color:#6b7280">📂 ${_cEsc(task.area)}</span>` : ''}
6822
+ ${m.task_type ? `<span style="font-size:12px;color:#6b7280">🔖 ${_cEsc(m.task_type)}</span>` : ''}
6823
+ ${_cRiskBadge(m.risk_level, T)} ${_cDuration(m.estimated_duration, T)}
6824
+ ${m.auto_claimable ? `<span style="background:#dbeafe;color:#1e40af;padding:1px 8px;border-radius:99px;font-size:11px;font-weight:600">${T('可自动认领', 'auto-claimable')}</span>` : ''}
6825
+ </div>
6826
+ ${_cBoundaryHTML(task.value_boundary || j.value_boundary, T)}
6827
+
6828
+ ${section('🧭', T('执行边界', 'Execution boundary'), `
6829
+ <div style="font-size:13px;color:#374151;font-weight:600">${T('允许修改的路径', 'Allowed paths')}</div>${_cArrList(m.allowed_paths, T('未指定 — 请先向维护者确认', 'Not specified — confirm with the maintainer'))}
6830
+ <div style="font-size:13px;color:#991b1b;font-weight:600;margin-top:10px">${T('禁止触碰的路径', 'Forbidden paths')}</div>${_cArrList(m.forbidden_paths, T('无', 'none'))}
6831
+ <div style="font-size:13px;color:#991b1b;font-weight:600;margin-top:10px">${T('禁止的动作', 'Prohibited actions')}</div>${_cArrList(m.prohibited_actions, T('无', 'none'))}
6832
+ ${Array.isArray(m.required_capabilities) && m.required_capabilities.length ? `<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('所需能力', 'Required capabilities')}</div>${_cArrList(m.required_capabilities, '')}` : ''}`)}
6833
+
6834
+ ${section('✅', T('验收与验证', 'Acceptance & verification'), `
6835
+ <div style="font-size:13px;color:#374151;font-weight:600">${T('验收标准', 'Acceptance criteria')}</div>${_cArrList(m.acceptance_criteria, T('见任务描述', 'see task'))}
6836
+ <div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('验证命令(须通过)', 'Verification commands (must pass)')}</div>${_cArrList(m.verification_commands, T('见任务描述', 'see task'))}
6837
+ ${m.expected_results ? `<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('预期结果', 'Expected results')}</div><div style="font-size:13px;color:#52525B;line-height:1.6;white-space:pre-wrap">${_cEsc(m.expected_results)}</div>` : ''}
6838
+ <div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('交付物', 'Deliverables')}</div>${_cArrList(m.deliverables, T('见任务描述', 'see task'))}
6839
+ ${m.definition_of_done ? `<div style="font-size:13px;color:#374151;font-weight:600;margin-top:10px">${T('完成定义', 'Definition of done')}</div><div style="font-size:13px;color:#52525B;line-height:1.6;white-space:pre-wrap">${_cEsc(m.definition_of_done)}</div>` : ''}`)}
6840
+
6841
+ ${section('🎯', T('提交目标(必须是 canonical 仓库)', 'Submit to the canonical repository'), `
6842
+ <div style="font-size:13px;color:#52525B;line-height:1.7">
6843
+ ${T('PR 的 base 仓库必须是', 'A PR\'s base repository MUST be')} <code>${_cEsc(cct.expected_pr_base_repo || cct.canonical_repository_full_name || '')}</code>
6844
+ ${cct.canonical_github_url ? ` · <a href="${_cEsc(cct.canonical_github_url)}" target="_blank" rel="noopener" style="color:#1d4ed8">${_cEsc(cct.canonical_github_url)}</a>` : ''}
6845
+ ${cct.base_branch ? ` · base <code>${_cEsc(cct.base_branch)}</code>` : ''}
6846
+ </div>
6847
+ <div style="font-size:12px;color:#92400e;background:#fef3c7;border-radius:8px;padding:10px;margin-top:10px;line-height:1.6">⚠️ ${T('若目标仓库与上述 canonical 仓库不一致,请停止并向真人确认 —— 不要向非 canonical 仓库提交。提交需 DCO 签名(git commit -s),问责主体是真人/组织,agent 只是执行者。', 'If a target repo differs from this canonical repo, STOP and ask a human to confirm — never contribute to a non-canonical repository. Commits need DCO sign-off (git commit -s); the accountable party is a real human/org, the agent is only an executor.')}</div>`)}
6848
+
6849
+ ${section('📋', T('复制给你的 agent', 'Copy-ready agent prompt'), `
6850
+ <div style="font-size:12px;color:#6b7280;margin-bottom:8px">${T('包含边界 / 禁止动作 / 验证命令 / 提交到 canonical 仓库的要求,并明确 sandbox / 本地草稿不算正式参与。', 'Includes the boundary / prohibited actions / verification commands / the canonical-repo PR requirement, and states that a sandbox / local draft is not participation.')}</div>
6851
+ <textarea id="ct-prompt" readonly style="width:100%;box-sizing:border-box;height:200px;font-family:ui-monospace,'SF Mono',Menlo,monospace;font-size:11px;line-height:1.5;border:1px solid #d4d4d8;border-radius:8px;padding:10px;color:#374151">${_cEsc(CONTRIBUTE_PROMPT_STATE.text)}</textarea>
6852
+ <div style="display:flex;gap:10px;align-items:center;margin-top:8px">
6853
+ <button onclick="contributeCopyPrompt()" style="padding:8px 16px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer">📋 ${T('复制提示', 'Copy prompt')}</button>
6854
+ <span id="ct-prompt-msg" style="font-size:12px;color:#16a34a"></span>
6855
+ </div>`)}
6856
+
6857
+ ${section('✋', T('认领', 'Claim'), claimBtn)}
6858
+
6859
+ <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>`
6861
+ }
6862
+
6863
+ async function renderContributeSuggest(app) {
6864
+ const en = window._lang === 'en'
6865
+ const T = (zh, e) => en && e ? e : zh
6866
+ const field = (id, label, ph, ta) => `
6867
+ <label style="display:block;font-size:13px;color:#374151;font-weight:600;margin:12px 0 4px">${label}</label>
6868
+ ${ta
6869
+ ? `<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
+ : `<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}">
6872
+ <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
+ <h1 style="font-size:clamp(22px,4.5vw,28px);margin:0 0 8px;color:#18181B">💡 ${T('建议一个任务', 'Suggest a task')}</h1>
6874
+ <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>
6875
+ ${_cBoundaryHTML(UNCOMMITTED_VALUE_BOUNDARY_HINT(), T)}
6876
+ ${field('cs-title', T('标题', 'Title') + ' *', T('简短描述这个任务', 'Short description of the task'))}
6877
+ ${field('cs-summary', T('原因 / 说明', 'Reason / summary') + ' *', T('为什么值得做?要解决什么?', 'Why is it worth doing? What does it solve?'), true)}
6878
+ ${field('cs-area', T('建议领域(可选)', 'Suggested area (optional)'), 'docs / api / pwa …')}
6879
+ ${field('cs-outcome', T('预期结果(可选)', 'Expected outcome (optional)'), T('完成后应达成什么', 'What should be true when done'), true)}
6880
+ ${field('cs-source', T('参考链接(可选)', 'Source reference (optional)'), 'https://… ' + T('(仅作参考,不决定提交目标仓库)', '(reference only; does not set the target repo)'))}
6881
+ ${field('cs-login', T('你的 GitHub 用户名(可选)', 'Your GitHub login (optional)'), 'octocat')}
6882
+ <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
+ <div id="ct-suggest-result" style="margin-top:14px"></div>
6884
+ </div>`
6885
+ }
6886
+
6887
+ // The boundary constant is server-authoritative; the client mirrors the SAME frozen stance for pages that
6888
+ // render before any task payload arrives (list/suggest). It restates NO economic value (RFC-017 I-12).
6889
+ function UNCOMMITTED_VALUE_BOUNDARY_HINT() {
6890
+ return {
6891
+ value_state: 'uncommitted', boundary_ref: 'RFC-017 I-12',
6892
+ notice_zh: '仅为贡献事实与归属的信息性记录,不是金融工具,不授予任何经济或兑现权利,此处不作任何承诺或保证(RFC-017 I-12 / §7)。',
6893
+ notice_en: 'Informational record of contribution facts and attribution only. It is not a financial instrument and confers no economic or redemption right; nothing here is promised or guaranteed (RFC-017 I-12 / §7).',
6894
+ }
6895
+ }
6896
+
6474
6897
  function renderRule(num, text) {
6475
6898
  const [zh, en] = text.includes(' / ') ? text.split(' / ') : [text, '']
6476
6899
  return `<div class="w-rule w-rule-item">
@@ -6534,16 +6957,19 @@ window.openParticipateSheet = (defaultTab) => {
6534
6957
  <div style="padding:18px 16px 28px">
6535
6958
  <div style="text-align:center;margin-bottom:18px">
6536
6959
  <div style="font-size:22px;margin-bottom:4px">🚀</div>
6537
- <div style="font-size:15px;font-weight:700;color:#1f2937">${t('参与共建')}</div>
6960
+ <div style="font-size:15px;font-weight:700;color:#1f2937">${t('开始使用 WebAZ')}</div>
6538
6961
  <div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('选一个开始的方式')}</div>
6539
6962
  </div>
6540
- <button onclick="(document.querySelector('.js-modal')?.remove(),openAuthSheet('${defaultTab||'login'}'))" class="btn btn-primary" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between">
6541
- <span>🔑 ${t('注册登陆')}</span><span style="font-size:13px;opacity:0.6">›</span>
6963
+ <button onclick="(closeSheet(),openAuthSheet('${defaultTab||'login'}'))" class="btn btn-primary" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between">
6964
+ <span>🔑 ${t('注册 / 登录')}</span><span style="font-size:13px;opacity:0.6">›</span>
6965
+ </button>
6966
+ <button onclick="(closeSheet(),navigate('#contribute/tasks'))" class="btn btn-outline" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;margin-bottom:10px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #6366f1;color:#4338ca">
6967
+ <span>🛠 ${t('浏览公开任务板')}</span><span style="font-size:13px;opacity:0.6">›</span>
6542
6968
  </button>
6543
- <button onclick="(document.querySelector('.js-modal')?.remove(),navigate('#welcome'))" class="btn btn-outline" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #6366f1;color:#4338ca">
6544
- <span>💬 ${t('了解更多')}</span><span style="font-size:13px;opacity:0.6">›</span>
6969
+ <button onclick="(closeSheet(),navigate('#contribute/tasks/suggest'))" class="btn btn-outline" style="width:100%;padding:14px;font-size:15px;font-weight:600;border-radius:10px;display:flex;align-items:center;justify-content:space-between;border:1.5px solid #6366f1;color:#4338ca">
6970
+ <span>💡 ${t('提建议(无需登录)')}</span><span style="font-size:13px;opacity:0.6">›</span>
6545
6971
  </button>
6546
- <div style="font-size:10px;color:#9ca3af;text-align:center;margin-top:14px;line-height:1.5">${t('了解协议设计 · 多种角色 · 元规则')}</div>
6972
+ <div style="font-size:10px;color:#9ca3af;text-align:center;margin-top:14px;line-height:1.5"><a href="#welcome" onclick="closeSheet()" style="color:#9ca3af;text-decoration:none">${t('了解协议设计 · 多种角色 · 元规则')}</a></div>
6547
6973
  </div>
6548
6974
  `, { maxWidth: 460 })
6549
6975
  }
@@ -6629,7 +7055,7 @@ window.openAuthSheet = (defaultTab) => {
6629
7055
  <input class="form-control" id="inp-sponsor" placeholder="${t('陆续开放中,请期待')}" style="font-family:monospace;font-size:13px;flex:1">
6630
7056
  <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>
6631
7057
  </div>
6632
- <div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('没有邀请码?联系老用户拿邀请链接')}</div>
7058
+ <div style="font-size:11px;color:#6b7280;margin-top:4px" id="sponsor-hint-msg">${t('邀请码为 6-7 位永久码,可带 -L/-R;没有就联系老用户拿邀请链接')}</div>
6633
7059
  </div>
6634
7060
  <div class="form-group">
6635
7061
  <label class="form-label">${t('名称 / 店铺名')}</label>
@@ -6778,16 +7204,18 @@ async function renderPromoter(app) {
6778
7204
  // ─── ③ 我要分享 ───
6779
7205
  const myId = state.user?.id || ''
6780
7206
  const origin = location.origin
6781
- // 优先用 permanent_code 短链;旧用户兜底用 usr_xxx
6782
- const code = data.permanent_code || myId
6783
- const refLinkShort = `${origin}/i/${code}`
6784
- const leftLink = `${origin}/i/${code}-L`
6785
- const rightLink = `${origin}/i/${code}-R`
7207
+ // 邀请短链只用 permanent_code,绝不兜底 usr_xxx;缺失时显示"暂不可用"。
7208
+ const code = data.permanent_code || null
7209
+ const refLinkShort = code ? `${origin}/i/${code}` : ''
7210
+ const leftLink = code ? `${origin}/i/${code}-L` : ''
7211
+ const rightLink = code ? `${origin}/i/${code}-R` : ''
7212
+ 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>`
6786
7213
  const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '&quot;')
6787
7214
 
6788
7215
  const toolboxInner = `
6789
7216
  <h2 style="font-size:16px;font-weight:600;margin-bottom:12px">🔗 ${t('我要分享')}</h2>
6790
7217
 
7218
+ ${!code ? inviteUnavailableHtml : `
6791
7219
  <div style="background:linear-gradient(135deg,#eef2ff 0%,#e0e7ff 100%);border:1px solid #c7d2fe;border-radius:10px;padding:12px;margin-bottom:12px">
6792
7220
  <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px">
6793
7221
  <div style="font-size:13px;font-weight:600;color:#3730a3">📎 ${t('我的邀请链接')}</div>
@@ -6795,14 +7223,14 @@ async function renderPromoter(app) {
6795
7223
  </div>
6796
7224
  <div style="font-family:ui-monospace,Consolas,monospace;font-size:11px;color:#3730a3;background:#fff;padding:7px 10px;border-radius:6px;margin-bottom:10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border:1px solid #e0e7ff">${escHtml(refLinkShort)}</div>
6797
7225
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
6798
- <button onclick="copyRefLink('${esc(data.referral_link)}')" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 8px;background:#4f46e5;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 1px 2px rgba(79,70,229,0.2)">
7226
+ <button onclick="copyRefLink('${esc(refLinkShort)}')" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 8px;background:#4f46e5;color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 1px 2px rgba(79,70,229,0.2)">
6799
7227
  <span style="font-size:15px">📋</span><span>${t('复制链接')}</span>
6800
7228
  </button>
6801
- <button onclick="showQRModal('${esc(data.referral_link)}','${t('我的邀请链接')}')" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 8px;background:#fff;color:#4f46e5;border:2px solid #4f46e5;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">
7229
+ <button onclick="showQRModal('${esc(refLinkShort)}','${t('我的邀请链接')}')" style="display:flex;align-items:center;justify-content:center;gap:6px;padding:10px 8px;background:#fff;color:#4f46e5;border:2px solid #4f46e5;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer">
6802
7230
  <span style="font-size:15px">📱</span><span>${t('二维码')}</span>
6803
7231
  </button>
6804
7232
  </div>
6805
- </div>
7233
+ </div>`}
6806
7234
 
6807
7235
  <div style="font-size:11px;line-height:1.6;color:#6b7280;margin-bottom:8px">
6808
7236
  ${data.permissions?.can_l1_share
@@ -7439,7 +7867,13 @@ function renderGrowthTasksSection(tasks, summary) {
7439
7867
  <details ${claimed.length ? '' : 'open'} style="margin-top:${claimed.length?'10px':'0'}">
7440
7868
  <summary style="font-size:11px;color:#374151;font-weight:600;cursor:pointer;padding:4px 0;list-style:none">▸ ${t('推荐领取')} (${available.length}) ▾</summary>
7441
7869
  <div style="padding-top:4px">
7442
- ${available.map(taskRow).join('')}
7870
+ ${available.slice(0, 3).map(taskRow).join('')}
7871
+ ${available.length > 3 ? `
7872
+ <details style="margin-top:2px">
7873
+ <summary style="font-size:11px;color:#6b7280;cursor:pointer;padding:4px 0;list-style:none">▸ ${t('查看更多推荐')} (${available.length - 3}) ▾</summary>
7874
+ <div style="padding-top:4px">${available.slice(3).map(taskRow).join('')}</div>
7875
+ </details>
7876
+ ` : ''}
7443
7877
  </div>
7444
7878
  </details>
7445
7879
  ` : ''}
@@ -7519,10 +7953,12 @@ function renderAtomicSection(a) {
7519
7953
  <div style="font-size:13px;color:#6b7280;margin-bottom:6px">🔗 ${t('左右码(完全对称 / 末端垂直挂靠)')}</div>
7520
7954
  <div style="font-size:11px;color:#6b7280;margin-bottom:10px">${t('点击下方任一卡片复制整条推荐链接。新人通过左/右链接注册会挂到对应区。')}</div>
7521
7955
  ${(() => {
7522
- const myId = state.user?.id || ''
7956
+ // placement 短链只用 permanent_code(/i/CODE-L / -R),绝不用 usr_xxx;缺失则提示不可用
7957
+ const myCode = state.user?.permanent_code || null
7523
7958
  const origin = location.origin
7524
- const leftLink = `${origin}/?placement=${myId}&side=left`
7525
- const rightLink = `${origin}/?placement=${myId}&side=right`
7959
+ 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`
7526
7962
  const esc = (s) => s.replace(/'/g, "\\'").replace(/"/g, '&quot;')
7527
7963
  return `
7528
7964
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
@@ -8704,6 +9140,10 @@ async function renderShopPage(app, identifier) {
8704
9140
  const followBtn = state.user && !isOwnShop && state.user.role === 'buyer'
8705
9141
  ? `<button class="btn btn-${is_following ? 'gray' : 'primary'} btn-sm" style="width:auto" id="shop-follow-btn" data-following="${is_following ? '1' : '0'}" onclick="toggleShopFollow('${seller.id}', this)">${is_following ? '✓ ' + t('已关注') : '+ ' + t('关注')}</button>`
8706
9142
  : ''
9143
+ // 推荐店铺:只锚定推荐关系/二叉树位置/店铺来源 —— 不是全店佣金权;商品分润仍要求推荐人真实成交过同款
9144
+ const shopReferralBtn = state.user?.permanent_code
9145
+ ? `<button class="btn btn-outline btn-sm" style="width:auto;font-size:11px" title="${t('店铺推荐只锚定推荐关系;只有你真实成交过的同款商品,后续成交才可能形成商品推荐关系')}" onclick="copyShopReferralLink('${seller.id}')">🔗 ${t('推荐店铺')}</button>`
9146
+ : ''
8707
9147
  const productCards = products.length === 0
8708
9148
  ? `<div style="text-align:center;padding:30px;color:#9ca3af;font-size:13px">${t('该卖家暂无商品')}</div>`
8709
9149
  : products.map(p => {
@@ -8749,6 +9189,7 @@ async function renderShopPage(app, identifier) {
8749
9189
  </div>
8750
9190
  <div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
8751
9191
  ${followBtn}
9192
+ ${shopReferralBtn}
8752
9193
  ${isOwnShop ? `<a href="#shop-edit" style="font-size:11px;color:#6366f1">${t('编辑店铺')} →</a>` : ''}
8753
9194
  </div>
8754
9195
  </div>
@@ -8765,6 +9206,17 @@ async function renderShopPage(app, identifier) {
8765
9206
  `, 'discover')
8766
9207
  }
8767
9208
 
9209
+ // 复制店铺推荐链接 — /?ref=CODE#shop/<seller>(target URL 形态:ref 在 query,目标页在 hash,服务端可见 ref)。
9210
+ // 只用 permanent_code,绝不用 usr_xxx;诚实文案:不暗示"分享店铺即可获得全店佣金"。
9211
+ window.copyShopReferralLink = (sellerId) => {
9212
+ const code = state.user?.permanent_code
9213
+ if (!code) return alert(t('邀请码暂不可用,请刷新或联系支持'))
9214
+ const link = `${location.origin}/?ref=${code}#shop/${sellerId}`
9215
+ copyText(link).then(ok => toast$(ok
9216
+ ? t('店铺推荐链接已复制 — 商品分润仍需你真实成交过同款并 opt-in')
9217
+ : t('复制失败,请手动复制'), ok ? 'success' : 'error'))
9218
+ }
9219
+
8768
9220
  window.toggleShopFollow = async (sellerId, btn) => {
8769
9221
  const following = btn.dataset.following === '1'
8770
9222
  btn.disabled = true
@@ -9688,7 +10140,7 @@ window.openBuildFeedback = () => {
9688
10140
  if (!state.user) { toast$(t('请先登录')); navigate('#login'); return }
9689
10141
  const page = (location.hash || '#/').split('?')[0] // 非 PII:只取当前页面路由作上下文
9690
10142
  const html = `
9691
- <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()">
10143
+ <div class="js-modal" data-page="${escHtml(page)}" 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()">
9692
10144
  <div style="background:#fff;width:100%;max-width:560px;border-radius:16px 16px 0 0;padding:20px;max-height:82vh;overflow-y:auto" onclick="event.stopPropagation()">
9693
10145
  <h2 style="font-size:16px;font-weight:700;margin-bottom:4px">💬 ${t('反馈 / 建议')}</h2>
9694
10146
  <div style="font-size:11px;color:#9ca3af;margin-bottom:12px">${t('帮我们改进 WebAZ。会自动附上当前页面')} <code style="background:#f3f4f6;padding:1px 5px;border-radius:4px">${escHtml(page)}</code> ${t('作为上下文(不含个人信息)')}</div>
@@ -9707,7 +10159,7 @@ window.openBuildFeedback = () => {
9707
10159
  <div id="bfb-msg" style="margin:8px 0"></div>
9708
10160
  <div style="display:flex;gap:8px">
9709
10161
  <button class="btn btn-gray" style="flex:1" onclick="this.closest('.js-modal').remove()">${t('取消')}</button>
9710
- <button class="btn btn-primary" style="flex:1" onclick="submitBuildFeedback('${escHtml(page)}')">${t('提交')}</button>
10162
+ <button class="btn btn-primary" style="flex:1" onclick="submitBuildFeedbackFromModal(this)">${t('提交')}</button>
9711
10163
  </div>
9712
10164
  <div style="text-align:center;margin-top:12px">
9713
10165
  <a onclick="this.closest('.js-modal').remove();navigate('#build-feedback')" style="font-size:12px;color:#4f46e5;cursor:pointer">${t('查看我的反馈进度 →')}</a>
@@ -9717,7 +10169,10 @@ window.openBuildFeedback = () => {
9717
10169
  const div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div.firstElementChild)
9718
10170
  }
9719
10171
 
9720
- window.submitBuildFeedback = async (page) => {
10172
+ window.submitBuildFeedbackFromModal = async (btn) => {
10173
+ // Codex #93:page 从 modal dataset 读,绝不把 location.hash 拼进内联 onclick 的 JS 字符串
10174
+ // (escHtml 是 HTML escape,放进 JS 字符串上下文会被 HTML attribute 解码 → hash 注入/XSS)。
10175
+ const page = btn.closest('.js-modal')?.dataset.page || '#/'
9721
10176
  const type = document.getElementById('bfb-type').value
9722
10177
  const text = document.getElementById('bfb-text').value.trim()
9723
10178
  const msg = document.getElementById('bfb-msg')
@@ -9750,8 +10205,8 @@ async function renderMyBuildFeedback(app) {
9750
10205
  ${Number(f.credited_points) > 0 ? `<div style="font-size:11px;color:#16a34a;margin-top:4px">🏅 +${f.credited_points} ${t('共建信誉')}</div>` : ''}
9751
10206
  ${(f.credit_pending_anchor && !(Number(f.credited_points) > 0)) ? `
9752
10207
  <div style="font-size:11px;color:#854d0e;background:#fef3c7;border-radius:6px;padding:8px;margin-top:6px">
9753
- 🔐 ${t('这条贡献已被采纳 —— 绑定 Passkey 即可领取建设信誉(奖励需锚定可问责真人)')}
9754
- <button class="btn btn-sm" style="margin-top:6px;font-size:11px;padding:4px 10px;background:#7c3aed;color:#fff;border-color:transparent" onclick="(async()=>{ if(await doRegisterPasskey('')){ alert(t('已绑定,建设信誉已补发')); navigate('#build-feedback') } })()">${t('绑定 Passkey 领取')}</button>
10208
+ 🔐 ${t('这条贡献已被采纳 —— 绑定 Passkey 即可记入建设信誉(建设信誉需锚定可问责真人)')}
10209
+ <button class="btn btn-sm" style="margin-top:6px;font-size:11px;padding:4px 10px;background:#7c3aed;color:#fff;border-color:transparent" onclick="(async()=>{ if(await doRegisterPasskey('')){ alert(t('已绑定,建设信誉已记入')); navigate('#build-feedback') } })()">${t('绑定 Passkey 记入建设信誉')}</button>
9755
10210
  </div>` : ''}
9756
10211
  </div>`).join('')
9757
10212
  app.innerHTML = shell(`
@@ -9803,7 +10258,7 @@ async function renderMyContributions(app) {
9803
10258
  ${stat(t('提任务'), k.tasks_created)}
9804
10259
  </div>
9805
10260
  ${provRows ? `<div class="card" style="padding:10px;margin-bottom:12px"><div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('署名构成(自报)')}</div>${provRows}</div>` : ''}
9806
- ${!p.reward_anchored ? `<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>` : ''}
10261
+ ${!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>` : ''}
9807
10262
  <div style="font-size:13px;font-weight:600;margin:14px 0 6px">${t('限制与申诉')}</div>
9808
10263
  ${restrHtml}
9809
10264
  <div style="font-size:11px;color:#9ca3af;margin-top:14px">${t('看板仅自己可见,不做公开排行。')}</div>
@@ -10867,7 +11322,7 @@ window.sharePromoLink = async (productId, title) => {
10867
11322
  if (res.error === 'rewards_opt_in_required') {
10868
11323
  const msg = (window._lang === 'en' ? res.message_en : res.message_zh) || res.message_zh || res.error
10869
11324
  const missing = Array.isArray(res.missing_requirements) ? `\n\n${t('待补')}: ${res.missing_requirements.join(', ')}` : ''
10870
- return alert(`⚠ ${msg}${missing}\n\n${t('前往 #me 申请共建身份')}`)
11325
+ return alert(`⚠ ${msg}${missing}\n\n${t('前往 #me 申请分享分润')}`)
10871
11326
  }
10872
11327
  return alert(`⚠ ${res.error}${res.completed_orders === 0 ? '\n' + t('完成该商品的购买后再分享') : ''}`)
10873
11328
  }
@@ -11507,7 +11962,10 @@ window.copyPlacementLink = async (link, side, el) => {
11507
11962
  window.sharePlatformLink = (side) => {
11508
11963
  if (!state.user?.id) return alert(t('请先登录'))
11509
11964
  if (side !== 'left' && side !== 'right') return
11510
- const link = `${location.origin}/?placement=${state.user.id}&side=${side}`
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'}`
11511
11969
  copyPlacementLink(link, side, null)
11512
11970
  }
11513
11971
 
@@ -11590,6 +12048,7 @@ window.doRecoverConfirm = async () => {
11590
12048
  <button class="btn btn-outline btn-sm" onclick="copyApiKey('${res.api_key}')">${t('复制')}</button>
11591
12049
  <button class="btn btn-primary btn-sm" onclick="useKey('${res.api_key}')">${t('立即登录')}</button>
11592
12050
  </div>
12051
+ <div style="font-size:11px;color:#b91c1c;margin-top:10px;line-height:1.5">⚠️ ${t('仅在私密设备打开;不要截图、转发或发送给任何人。')}</div>
11593
12052
  </div>`
11594
12053
  }
11595
12054
 
@@ -11655,6 +12114,7 @@ window.doLogin = async () => {
11655
12114
  await persistApiKey(key)
11656
12115
  connectSSE()
11657
12116
  maybeClaimPendingProductShare()
12117
+ maybeClaimPendingShopReferral()
11658
12118
  // 2026-05-28 修:登录 sheet 是 .js-sheet(openSheet 建),之前只删 .js-modal → 登录成功后窗口不消失
11659
12119
  try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}
11660
12120
  navigateIntended(roleHome(user.role))
@@ -11681,6 +12141,7 @@ window.doLoginByPassword = async () => {
11681
12141
  else state.user = { id: res.user_id, name: res.name, role: res.role }
11682
12142
  connectSSE()
11683
12143
  maybeClaimPendingProductShare()
12144
+ maybeClaimPendingShopReferral()
11684
12145
  // 2026-05-28 修:同 doLogin — 关 .js-sheet 登录窗口
11685
12146
  try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}
11686
12147
  navigateIntended(roleHome(res.role))
@@ -11707,8 +12168,9 @@ function readShareHint() {
11707
12168
  ;[urlRef, extraSide] = stripSide(urlRef)
11708
12169
  if (!extraSide) [urlPlace, extraSide] = stripSide(urlPlace)
11709
12170
  if (!urlSide && extraSide) urlSide = extraSide
11710
- // ref / placement 接受三态:usr_xxx / VKSF9P (permanent_code 6-7 大写字母数字) / @handle handle
11711
- const refPattern = /^(usr_[A-Za-z0-9_]+|[A-Z0-9]{6,7}|@?[a-z0-9._]{3,20})$/
12171
+ // ref / placement 仅接受邀请码(6-7 位永久码,可带 -L/-R)。usr_xxx / @handle / handle 不再作为邀请引用 —
12172
+ // 收窄公开邀请面,消除歧义(权威由服务端 resolveInviteCodeRef 二次校验)
12173
+ const refPattern = /^[A-Za-z0-9]{6,7}$/
11712
12174
  const validRef = urlRef && refPattern.test(urlRef)
11713
12175
  const validPlace= urlPlace && refPattern.test(urlPlace)
11714
12176
  if (validRef || validPlace) {
@@ -11722,12 +12184,24 @@ function readShareHint() {
11722
12184
  localStorage.setItem('webaz_share_hint', JSON.stringify(obj))
11723
12185
  return obj
11724
12186
  }
12187
+ // 旧 localStorage hint 里的 sponsor_id / placement_inviter_id 若是 usr_xxx / @handle / 裸 handle(非邀请码),
12188
+ // 注册时不再使用 —— 清掉非码引用,只保留合法邀请码。
12189
+ const okCode = (v) => typeof v === 'string' && /^[A-Za-z0-9]{6,7}$/.test(v)
12190
+ const sanitizeHint = (o) => {
12191
+ if (!o || typeof o !== 'object') return null
12192
+ if (o.sponsor_id && !okCode(o.sponsor_id)) o.sponsor_id = null
12193
+ if (o.placement_inviter_id && !okCode(o.placement_inviter_id)) o.placement_inviter_id = null
12194
+ return (o.sponsor_id || o.placement_inviter_id) ? o : null
12195
+ }
11725
12196
  try {
11726
12197
  const raw = localStorage.getItem('webaz_share_hint')
11727
12198
  if (raw) {
11728
12199
  const obj = JSON.parse(raw)
11729
- if (obj.expiry > Date.now()) return obj
11730
- localStorage.removeItem('webaz_share_hint')
12200
+ if (obj.expiry > Date.now()) {
12201
+ const clean = sanitizeHint(obj)
12202
+ if (clean) return clean
12203
+ }
12204
+ localStorage.removeItem('webaz_share_hint') // expired or non-code → drop
11731
12205
  }
11732
12206
  } catch {}
11733
12207
  // 向后兼容:旧 'webaz_ref' 格式
@@ -11735,13 +12209,13 @@ function readShareHint() {
11735
12209
  const old = localStorage.getItem('webaz_ref')
11736
12210
  if (old) {
11737
12211
  const o = JSON.parse(old)
11738
- if (o.expiry > Date.now()) return {
12212
+ if (o.expiry > Date.now() && okCode(o.ref)) return {
11739
12213
  sponsor_id: o.ref || null,
11740
12214
  placement_inviter_id: o.side ? o.ref : null,
11741
12215
  placement_side: o.side || null,
11742
12216
  expiry: o.expiry,
11743
12217
  }
11744
- localStorage.removeItem('webaz_ref')
12218
+ localStorage.removeItem('webaz_ref') // expired or non-code → drop
11745
12219
  }
11746
12220
  } catch {}
11747
12221
  return null
@@ -11787,6 +12261,24 @@ async function maybeClaimPendingProductShare() {
11787
12261
  writeShareCtx({ pending_share_id: null })
11788
12262
  }
11789
12263
 
12264
+ // 店铺推荐锚定:从 ?ref=CODE + #shop/<seller> 进来时 ShareCtx 暂存 pending_shop_referral,
12265
+ // 登录/注册后调一次 touch 落库(first-touch 30 天锁)。只锚定推荐关系,不是全店佣金权。
12266
+ async function maybeClaimPendingShopReferral() {
12267
+ if (!state.user?.id) return
12268
+ const ctx = readShareCtx()
12269
+ const p = ctx?.pending_shop_referral
12270
+ if (!p) return
12271
+ // 只接受邀请码形态;旧 localStorage 里的 usr_xxx / @handle 残留 → 直接清理不发送
12272
+ if (!p.ref_code || !/^[A-Za-z0-9]{6,7}$/.test(p.ref_code) || !p.seller_identifier) {
12273
+ writeShareCtx({ pending_shop_referral: null }); return
12274
+ }
12275
+ 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 } : {}) })
12277
+ // 任何已送达的响应(成功 / self-skip / already_locked / typed 错误)都清;仅网络异常保留待重试
12278
+ writeShareCtx({ pending_shop_referral: null })
12279
+ } catch {}
12280
+ }
12281
+
11790
12282
  // 还原意图 hash(注册/登录后调用,没意图就走 fallback)
11791
12283
  function navigateIntended(fallback) {
11792
12284
  const intended = sessionStorage.getItem('webaz_intended_hash')
@@ -11825,7 +12317,7 @@ function renderShareBanner(variant) {
11825
12317
  <div id="share-banner-hero" style="background:linear-gradient(135deg,#eef2ff,#fce7f3);border:1px solid #c7d2fe;border-radius:12px;padding:14px 16px;margin-bottom:14px">
11826
12318
  <div style="font-size:13px;color:#3730a3;font-weight:600;margin-bottom:4px">👋 ${t('你被')} <strong>${escHtml(name)}</strong> ${t('邀请来 WebAZ')}</div>
11827
12319
  ${tgtLine ? `<div style="font-size:12px;color:#374151;line-height:1.6;margin-top:6px">${tgtLine}</div>` : ''}
11828
- <div style="font-size:11px;color:#6366f1;margin-top:8px">${t('注册即拿邀请奖励,TA 也得分润 — 双方共赢')}</div>
12320
+ <div style="font-size:11px;color:#6366f1;margin-top:8px">${t('通过邀请进入会记录归因;分享分润需登录后单独申请。')}</div>
11829
12321
  </div>`
11830
12322
  }
11831
12323
  // compact
@@ -11920,6 +12412,15 @@ async function initShareCtx() {
11920
12412
  patch.pending_share_id = urlShareId
11921
12413
  }
11922
12414
 
12415
+ // 店铺推荐:?ref=CODE + #shop/<seller> → 暂存待登录后 touch(只锚定推荐关系,非全店佣金权)。
12416
+ // hint.sponsor_id 已由 readShareHint 收窄为邀请码(-L/-R 已剥进 placement_side),usr_xxx/@handle 进不来。
12417
+ if (hint?.sponsor_id && fromHash && fromHash.startsWith('#shop/') && !ctx?.pending_shop_referral) {
12418
+ const sellerIdent = fromHash.slice('#shop/'.length).split('?')[0].trim()
12419
+ if (sellerIdent && sellerIdent !== 'agent') {
12420
+ patch.pending_shop_referral = { seller_identifier: sellerIdent, ref_code: hint.sponsor_id, side: hint.placement_side || null }
12421
+ }
12422
+ }
12423
+
11923
12424
  if (Object.keys(patch).length > 0 || !ctx) {
11924
12425
  ctx = writeShareCtx(patch)
11925
12426
  }
@@ -11933,6 +12434,10 @@ async function initShareCtx() {
11933
12434
  // 不论成功失败都清掉 pending(避免重复尝试)
11934
12435
  writeShareCtx({ pending_share_id: null })
11935
12436
  }
12437
+ // 已登录 + 有待锚定店铺推荐 → 立即 touch(网络失败保留待下次)
12438
+ if (state.user?.id && ctx.pending_shop_referral) {
12439
+ await maybeClaimPendingShopReferral()
12440
+ }
11936
12441
 
11937
12442
  // Enrich:并发 fetch sponsor 公开卡 + target 预览(阻塞返回,确保 banner 渲染时数据齐)
11938
12443
  // 2026-05-31 修:每个 fetch 加 AbortSignal.timeout(5000),防 boot 阶段任一 fetch hang
@@ -12045,6 +12550,7 @@ window.doRegister = async () => {
12045
12550
  await persistApiKey(res.api_key)
12046
12551
  connectSSE()
12047
12552
  maybeClaimPendingProductShare()
12553
+ maybeClaimPendingShopReferral()
12048
12554
  // 2026-05-24 修:关闭 auth sheet,再开成功 modal(避免堆叠)
12049
12555
  // 2026-05-28 修:auth sheet 是 .js-sheet(openSheet 建),需一并删除
12050
12556
  try { document.querySelectorAll('.js-modal, .js-sheet').forEach(m => m.remove()) } catch {}
@@ -12082,7 +12588,7 @@ function showRegisterSuccessModal(res) {
12082
12588
  <div style="background:#f9fafb;border-radius:8px;padding:6px 10px;margin-bottom:12px;font-size:12px;line-height:1.8">
12083
12589
  ☐ ${t('设置默认配送地址(智能下单需要)')}<br>
12084
12590
  ☐ ${t('设置登录密码(备用凭证,可选)')}<br>
12085
- ☐ ${t('完成首笔购买,解锁分享奖励')}<br>
12591
+ ☐ ${t('完成首笔购买后,可申请分享分润。')}<br>
12086
12592
  ☐ ${t('🔐 绑定 Passkey — 强烈推荐,大额提现自动启用,防账号被盗')}
12087
12593
  </div>
12088
12594
 
@@ -30605,6 +31111,7 @@ async function renderAdminProtocol(app) {
30605
31111
  ${adminLinkCard('📜', t('审计日志'), t('全部 admin 写操作'), '#admin/audit')}
30606
31112
  ${adminLinkCard('🛑', t('错误监控'), t('24h 趋势 + burst 告警'), '#admin/errors')}
30607
31113
  ${adminLinkCard('📨', t('Welcome 提交'), t('#welcome 留下的邮箱订阅 + 建议'), '#admin/public-ideas')}
31114
+ ${adminLinkCard('🛠️', t('任务建议收件箱'), t('陌生人 / agent 提交的共建任务建议;审阅 → 转正式任务'), '#admin/task-proposals')}
30608
31115
  </div>
30609
31116
  `, 'admin-protocol')
30610
31117
  }