@seasonkoh/webaz 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -5
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +3 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +899 -720
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +287 -0
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +102 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +180 -0
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +16 -0
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +1 -0
- package/dist/mcp.js +7 -3
- package/dist/pwa/data/onboarding-cases.js +345 -0
- package/dist/pwa/data/onboarding-quiz.js +247 -0
- package/dist/pwa/public/app.js +1459 -96
- package/dist/pwa/public/i18n.js +303 -2
- package/dist/pwa/public/icon-192.png +0 -0
- package/dist/pwa/public/icon-512.png +0 -0
- package/dist/pwa/public/manifest.json +5 -2
- package/dist/pwa/public/openapi.json +1 -1
- package/dist/pwa/public/sw.js +1 -1
- package/dist/pwa/routes/admin-protocol-params.js +80 -2
- package/dist/pwa/routes/admin-reports.js +14 -9
- package/dist/pwa/routes/auth-read.js +3 -1
- package/dist/pwa/routes/build-feedback.js +82 -0
- package/dist/pwa/routes/build-reputation.js +10 -0
- package/dist/pwa/routes/build-tasks.js +73 -0
- package/dist/pwa/routes/disputes-write.js +149 -1
- package/dist/pwa/routes/governance-auto-deactivate.js +108 -0
- package/dist/pwa/routes/governance-onboarding.js +785 -0
- package/dist/pwa/routes/leaderboard.js +10 -2
- package/dist/pwa/routes/orders-action.js +5 -1
- package/dist/pwa/routes/products-meta.js +30 -0
- package/dist/pwa/routes/profile-identity.js +1 -1
- package/dist/pwa/routes/public-utils.js +44 -0
- package/dist/pwa/routes/rewards-apply.js +210 -0
- package/dist/pwa/routes/rewards-auto-downgrade.js +65 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +48 -0
- package/dist/pwa/routes/wallet-write.js +17 -31
- package/dist/pwa/routes/webauthn.js +1 -1
- package/dist/pwa/server.js +641 -64
- package/package.json +6 -3
package/dist/pwa/public/app.js
CHANGED
|
@@ -548,8 +548,8 @@ async function render(page, params) {
|
|
|
548
548
|
// 只在首次(没有 sponsor_name 时)阻塞等 fetch;后续重渲染走缓存秒过
|
|
549
549
|
try { await initShareCtx() } catch (e) { console.warn('[ShareCtx]', e) }
|
|
550
550
|
|
|
551
|
-
// 未登录时只允许看登录页、找回密钥页、商品、welcome
|
|
552
|
-
if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== '') {
|
|
551
|
+
// 未登录时只允许看登录页、找回密钥页、商品、welcome 预发布页、governance-onboarding 公开页
|
|
552
|
+
if (!state.apiKey && page !== 'login' && page !== 'shop' && page !== 'recover' && page !== 'welcome' && page !== 'governance-onboarding' && page !== '') {
|
|
553
553
|
// 保存目标 hash 以便登录/注册后跳回
|
|
554
554
|
if (location.hash && !['#login', '#recover'].includes(location.hash)) {
|
|
555
555
|
sessionStorage.setItem('webaz_intended_hash', location.hash)
|
|
@@ -630,6 +630,8 @@ async function render(page, params) {
|
|
|
630
630
|
case 'seller-trials': return renderSellerTrials(app)
|
|
631
631
|
// 2026-05-24 #991:/welcome 预发布页 — 6 区块协议介绍
|
|
632
632
|
case 'welcome': return renderWelcome(app)
|
|
633
|
+
// 2026-06-02 W3.5-B:#governance-onboarding 治理岗位申请页(公开页)
|
|
634
|
+
case 'governance-onboarding': return renderGovernanceOnboarding(app)
|
|
633
635
|
case 'seller':
|
|
634
636
|
if (state.user?.role === 'logistics') return renderLogistics(app)
|
|
635
637
|
if (state.user?.role === 'arbitrator') return renderDisputeList(app)
|
|
@@ -714,9 +716,15 @@ async function render(page, params) {
|
|
|
714
716
|
if (params[0] === 'kyc') return renderAdminKyc(app)
|
|
715
717
|
if (params[0] === 'export') return renderAdminExport(app)
|
|
716
718
|
if (params[0] === 'errors') return renderAdminErrors(app)
|
|
719
|
+
if (params[0] === 'governance') return renderAdminGovernance(app)
|
|
717
720
|
return renderAdminDashboard(app)
|
|
718
721
|
case 'apply-verifier': return renderApplyVerifier(app)
|
|
719
722
|
case 'apply-arbitrator': return renderApplyArbitrator(app)
|
|
723
|
+
case 'apply-rewards': return renderApplyRewards(app)
|
|
724
|
+
case 'rewards-me': return renderRewardsMe(app)
|
|
725
|
+
case 'onboarding-verifier': return renderOnboarding(app, 'verifier')
|
|
726
|
+
case 'onboarding-arbitrator': return renderOnboarding(app, 'arbitrator')
|
|
727
|
+
case 'governance-me': return renderGovernanceMe(app)
|
|
720
728
|
case 'wishlist': return renderWishlist(app)
|
|
721
729
|
case 'waitlist': return renderWaitlist(app)
|
|
722
730
|
case 'returns': return renderReturnsCenter(app)
|
|
@@ -727,6 +735,8 @@ async function render(page, params) {
|
|
|
727
735
|
case 'blocklist': return renderBlocklist(app)
|
|
728
736
|
case 'kyc': return renderKyc(app)
|
|
729
737
|
case 'feedback': return renderFeedback(app)
|
|
738
|
+
case 'build-feedback': return renderMyBuildFeedback(app)
|
|
739
|
+
case 'my-contributions': return renderMyContributions(app)
|
|
730
740
|
case 'admin-feedback': return renderAdminFeedback(app)
|
|
731
741
|
case 'admin-payments': return renderAdminPayments(app)
|
|
732
742
|
case 'my-agents': return renderMyAgents(app)
|
|
@@ -991,7 +1001,7 @@ function preLaunchBannerHTML() {
|
|
|
991
1001
|
if (window._protocolPhase && window._protocolPhase !== 'pre_launch') return ''
|
|
992
1002
|
return `<div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:10px 14px;font-size:12px;color:#92400e;text-align:center;line-height:1.5;margin:8px 12px 12px">
|
|
993
1003
|
⚠️ <strong>${t('协议尚未公开上线 · 数据为测试 / demo · 请勿据此投资或承诺第三方')}</strong>
|
|
994
|
-
<a href="
|
|
1004
|
+
<a href="#welcome" style="margin-left:8px;color:#7c2d12;text-decoration:underline;font-weight:600">${t('详情')}</a>
|
|
995
1005
|
</div>`
|
|
996
1006
|
}
|
|
997
1007
|
|
|
@@ -1129,6 +1139,12 @@ function shell(content, activeTab, opts) {
|
|
|
1129
1139
|
</div>
|
|
1130
1140
|
</div>
|
|
1131
1141
|
` : ''}
|
|
1142
|
+
${state.user ? `
|
|
1143
|
+
<button id="feedback-fab" onclick="openBuildFeedback()" title="${t('反馈 / 建议')}"
|
|
1144
|
+
style="position:fixed;bottom:74px;left:14px;width:48px;height:48px;border-radius:50%;background:#fff;color:#4f46e5;border:1px solid #c7d2fe;cursor:pointer;font-size:22px;box-shadow:0 4px 12px rgba(0,0,0,0.12);z-index:99;display:flex;align-items:center;justify-content:center" aria-label="${t('反馈 / 建议')}">
|
|
1145
|
+
💬
|
|
1146
|
+
</button>
|
|
1147
|
+
` : ''}
|
|
1132
1148
|
${_opts.bottomBar ? `<div class="page-bottom-bar" style="position:fixed;bottom:0;left:0;right:0;z-index:50;background:#fff;border-top:1px solid #e5e7eb;padding:10px 14px;box-shadow:0 -2px 12px rgba(0,0,0,0.04);padding-bottom:max(10px, env(safe-area-inset-bottom))">${_opts.bottomBar}</div>` : ''}
|
|
1133
1149
|
${_opts.hideTabbar ? '' : `<nav class="tabbar">
|
|
1134
1150
|
${tabs.map(tb => `
|
|
@@ -1179,7 +1195,7 @@ window.openAvatarMenu = function() {
|
|
|
1179
1195
|
${item('🏠', t('我的主页'), '#me')}
|
|
1180
1196
|
${item('⚙️', t('设置 / 角色'), '#me/settings')}
|
|
1181
1197
|
${!isTrusted ? item('👁', t('公开主页'), '#u/' + u.id) : ''}
|
|
1182
|
-
${item('🤖', t('我的
|
|
1198
|
+
${item('🤖', t('我的 agents'), '#my-agents')}
|
|
1183
1199
|
|
|
1184
1200
|
${sectionTitle(t('协议'))}
|
|
1185
1201
|
${item('🏛', t('协议治理'), '#governance')}
|
|
@@ -1266,7 +1282,7 @@ async function renderMyAdvanced(app) {
|
|
|
1266
1282
|
|
|
1267
1283
|
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🤖 ${t('Agent 治理')}</div>
|
|
1268
1284
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1269
|
-
${card('🤖', t('我的
|
|
1285
|
+
${card('🤖', t('我的 agents'), t('谁替我做事 · 撤销控制'), '#my-agents')}
|
|
1270
1286
|
${card('⚡', t('卖家自动化'), skillCount > 0 ? skillCount + ' ' + t('个') : t('未发布'), '#skills')}
|
|
1271
1287
|
${!isTrusted ? card('🪄', t('AI 推荐'), t('给我推商品'), '#ai-recommend') : ''}
|
|
1272
1288
|
${role === 'seller' ? card('🎯', t('Auto-bid'), t('RFQ 自动报价'), '#auto-bid') : ''}
|
|
@@ -1288,7 +1304,18 @@ async function renderMyAdvanced(app) {
|
|
|
1288
1304
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
1289
1305
|
${card('🏛', t('协议治理'), t('参数公开 · 变更可追溯'), '#governance')}
|
|
1290
1306
|
${card('⚖', t('判例库'), t('争议判决公开'), '#judgments')}
|
|
1307
|
+
${card('🎁', t('共建身份'), t('申请制 · 含锁仓金'), '#rewards-me')}
|
|
1291
1308
|
</div>
|
|
1309
|
+
|
|
1310
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">📧 ${t('联系我们')}</div>
|
|
1311
|
+
<a href="mailto:contact@webaz.xyz" class="card" style="padding:14px;display:flex;align-items:center;gap:10px;min-height:64px;text-decoration:none;color:inherit">
|
|
1312
|
+
<div style="font-size:24px;flex-shrink:0">📧</div>
|
|
1313
|
+
<div style="flex:1;min-width:0">
|
|
1314
|
+
<div style="font-weight:600;font-size:14px">contact@webaz.xyz</div>
|
|
1315
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:2px">${t('合作 / 反馈 / 合规咨询')}</div>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div style="color:#9ca3af">›</div>
|
|
1318
|
+
</a>
|
|
1292
1319
|
`
|
|
1293
1320
|
app.innerHTML = shell(sections, 'me')
|
|
1294
1321
|
}
|
|
@@ -1685,7 +1712,7 @@ async function renderProfile(app) {
|
|
|
1685
1712
|
</div>
|
|
1686
1713
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1687
1714
|
<span style="color:#374151">${t('协议')}</span>
|
|
1688
|
-
<a href="https://github.com/
|
|
1715
|
+
<a href="https://github.com/seasonsagents-art/webaz" target="_blank" style="color:#6366f1;text-decoration:none;font-size:12px">${t('源码仓库')} ↗</a>
|
|
1689
1716
|
</div>
|
|
1690
1717
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
1691
1718
|
<span style="color:#374151">🔔 ${t('推送通知')}</span>
|
|
@@ -4193,75 +4220,1078 @@ window.doAdminTaskFilter = () => {
|
|
|
4193
4220
|
}
|
|
4194
4221
|
|
|
4195
4222
|
// ─── 用户:申请审核员 ──────────────────────────────────────────
|
|
4196
|
-
|
|
4223
|
+
// ─── 治理岗位申请(W3.5-B governance onboarding,#1093 阶段 1b) ─────
|
|
4224
|
+
// docs/GOVERNANCE-ONBOARDING.md §3.1 流程图前端:
|
|
4225
|
+
// eligibility 检查 + 顶部红色 disclosure + 8s 延迟双勾选 + Passkey 签发 + POST /governance/onboarding/apply
|
|
4226
|
+
async function _renderApplyGovernance(app, role) {
|
|
4197
4227
|
if (!state.user) { renderLogin(); return }
|
|
4198
|
-
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4228
|
+
const returnNav = role === 'arbitrator' ? 'me' : 'verify-tasks'
|
|
4229
|
+
const roleTitle = role === 'arbitrator' ? t('申请仲裁员') : t('申请审核员')
|
|
4230
|
+
const roleIcon = role === 'arbitrator' ? '⚖' : '🛡'
|
|
4231
|
+
|
|
4232
|
+
app.innerHTML = shell(loading$(), returnNav)
|
|
4233
|
+
const data = await GET('/' + role + '/eligibility')
|
|
4234
|
+
if (data.error) { app.innerHTML = shell(alert$('error', data.error), returnNav); return }
|
|
4235
|
+
|
|
4201
4236
|
const checklist = (data.items || []).map(i => `
|
|
4202
4237
|
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
4203
4238
|
<span style="${i.ok ? 'color:#16a34a' : 'color:#dc2626'}">${i.ok ? '✓' : '✗'} ${i.label}</span>
|
|
4204
4239
|
<span style="color:#6b7280">${i.current} / ${i.required}</span>
|
|
4205
4240
|
</div>`).join('')
|
|
4206
4241
|
const canApply = data.eligible
|
|
4242
|
+
const pageLoadedAt = Date.now()
|
|
4243
|
+
|
|
4207
4244
|
app.innerHTML = shell(`
|
|
4208
|
-
<h1 class="page-title"
|
|
4245
|
+
<h1 class="page-title">${roleIcon} ${roleTitle}</h1>
|
|
4246
|
+
|
|
4247
|
+
<div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
|
|
4248
|
+
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程是治理岗位申请,不是赚钱机会')}</div>
|
|
4249
|
+
<div style="font-size:12px;color:#7f1d1d;line-height:1.6">
|
|
4250
|
+
${t('phase A 不发放任何现金 / WAZ 报酬。治理是公共贡献。详 docs/GOVERNANCE-ONBOARDING.md。')}<br>
|
|
4251
|
+
<span style="opacity:0.85">This is a governance application, not an income opportunity. Phase A pays NO cash / WAZ.</span>
|
|
4252
|
+
</div>
|
|
4253
|
+
</div>
|
|
4254
|
+
|
|
4209
4255
|
<div class="card" style="margin-bottom:16px">
|
|
4210
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('
|
|
4256
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('门槛(全部通过才能申请)')}:</div>
|
|
4211
4257
|
${checklist}
|
|
4212
4258
|
</div>
|
|
4213
|
-
${canApply
|
|
4214
|
-
? `<button class="btn btn-primary" onclick="doApplyVerifier()">${t('提交申请')}</button>`
|
|
4215
|
-
: `<button class="btn btn-primary" disabled style="opacity:0.5">${t('提交申请')}</button>
|
|
4216
|
-
<div style="margin-top:8px;font-size:12px;color:#6b7280">${t('请先完成上述未达标指标')}</div>`}
|
|
4217
|
-
<div id="apply-msg" style="margin-top:12px"></div>
|
|
4218
|
-
<div style="margin-top:16px"><a href="#verify-tasks" style="font-size:13px;color:#6b7280">← ${t('返回审核任务')}</a></div>
|
|
4219
|
-
`, 'verify-tasks')
|
|
4220
|
-
}
|
|
4221
4259
|
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4260
|
+
${!canApply ? `
|
|
4261
|
+
<div class="card" style="background:#fff7ed;border-color:#fdba74;padding:12px;font-size:13px;color:#7c2d12">
|
|
4262
|
+
${t('请先完成上述未达标指标,完成后再来申请')}
|
|
4263
|
+
</div>
|
|
4264
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4265
|
+
` : `
|
|
4266
|
+
<div class="card" style="margin-bottom:16px">
|
|
4267
|
+
<div style="font-size:13px;color:#374151;margin-bottom:12px;font-weight:600">${t('知情同意(双勾选)')}</div>
|
|
4268
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer">
|
|
4269
|
+
<input type="checkbox" id="gov-consent-1" onchange="updateGovApplyState()">
|
|
4270
|
+
<span style="font-size:13px;color:#374151">${t('我已阅读 GOVERNANCE-ONBOARDING / ARBITRATION-PLAYBOOK / META-RULES / CHARTER,理解角色责任')}</span>
|
|
4271
|
+
</label>
|
|
4272
|
+
<label id="gov-consent-2-wrap" style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer;opacity:0.5">
|
|
4273
|
+
<input type="checkbox" id="gov-consent-2" disabled onchange="updateGovApplyState()">
|
|
4274
|
+
<span style="font-size:13px;color:#374151">${t('我自愿申请,无人收买 / 诱导')} <span id="gov-countdown" style="color:#dc2626;font-weight:600"></span></span>
|
|
4275
|
+
</label>
|
|
4276
|
+
</div>
|
|
4277
|
+
|
|
4278
|
+
<button class="btn btn-primary" id="gov-submit-btn" onclick="doSubmitGovernanceApply('${role}', ${pageLoadedAt})" disabled style="opacity:0.5">
|
|
4279
|
+
🔑 ${t('用 Passkey 签发提交申请')}
|
|
4280
|
+
</button>
|
|
4281
|
+
|
|
4282
|
+
<div id="gov-apply-msg" style="margin-top:12px"></div>
|
|
4283
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4284
|
+
`}
|
|
4285
|
+
`, returnNav)
|
|
4286
|
+
|
|
4287
|
+
if (canApply) {
|
|
4288
|
+
let secondsLeft = 8
|
|
4289
|
+
const tick = () => {
|
|
4290
|
+
const el = document.getElementById('gov-countdown')
|
|
4291
|
+
if (!el) return
|
|
4292
|
+
if (secondsLeft > 0) {
|
|
4293
|
+
el.textContent = '(' + t('等') + ' ' + secondsLeft + 's)'
|
|
4294
|
+
secondsLeft--
|
|
4295
|
+
setTimeout(tick, 1000)
|
|
4296
|
+
} else {
|
|
4297
|
+
el.textContent = ''
|
|
4298
|
+
const cb2 = document.getElementById('gov-consent-2')
|
|
4299
|
+
if (cb2) cb2.disabled = false
|
|
4300
|
+
const wrap = document.getElementById('gov-consent-2-wrap')
|
|
4301
|
+
if (wrap) wrap.style.opacity = '1'
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
tick()
|
|
4305
|
+
}
|
|
4229
4306
|
}
|
|
4230
4307
|
|
|
4231
|
-
|
|
4232
|
-
async function renderApplyArbitrator(app) {
|
|
4308
|
+
async function renderApplyRewards(app) {
|
|
4233
4309
|
if (!state.user) { renderLogin(); return }
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
const
|
|
4310
|
+
const returnNav = 'rewards-me'
|
|
4311
|
+
|
|
4312
|
+
app.innerHTML = shell(loading$(), returnNav)
|
|
4313
|
+
const status = await GET('/rewards/status')
|
|
4314
|
+
if (status.error) { app.innerHTML = shell(alert$('error', status.error), returnNav); return }
|
|
4315
|
+
|
|
4316
|
+
if (status.opted_in) {
|
|
4317
|
+
app.innerHTML = shell(`
|
|
4318
|
+
<h1 class="page-title">${t('已是共建身份')}</h1>
|
|
4319
|
+
<div class="card" style="background:#ecfdf5;border-color:#86efac;padding:12px;font-size:13px;color:#064e3b">
|
|
4320
|
+
${t('你已 opted-in。如需查看状态或退出,前往 ')}
|
|
4321
|
+
<a href="#rewards-me" style="color:#16a34a;font-weight:600">${t('共建身份管理')}</a>
|
|
4322
|
+
</div>
|
|
4323
|
+
`, returnNav)
|
|
4324
|
+
return
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
const eli = status.eligibility || {}
|
|
4328
|
+
const canApply = !!eli.can_apply
|
|
4329
|
+
const consentText = status.consent_text_zh || ''
|
|
4330
|
+
const consentTextEn = status.consent_text_en || ''
|
|
4331
|
+
const consentVersion = status.consent_version || ''
|
|
4332
|
+
const delaySec = Number(eli.consent_delay_seconds || 8)
|
|
4333
|
+
const pageLoadedAt = Date.now()
|
|
4334
|
+
|
|
4335
|
+
const checklist = `
|
|
4238
4336
|
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
4239
|
-
<span style="${
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4337
|
+
<span style="${eli.completed_orders >= eli.min_completed_orders ? 'color:#16a34a' : 'color:#dc2626'}">
|
|
4338
|
+
${eli.completed_orders >= eli.min_completed_orders ? '✓' : '✗'} ${t('已完成订单')}
|
|
4339
|
+
</span>
|
|
4340
|
+
<span style="color:#6b7280">${eli.completed_orders} / ${eli.min_completed_orders}</span>
|
|
4341
|
+
</div>
|
|
4342
|
+
<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #f3f4f6;font-size:13px">
|
|
4343
|
+
<span style="${!eli.require_passkey || eli.passkey_count > 0 ? 'color:#16a34a' : 'color:#dc2626'}">
|
|
4344
|
+
${!eli.require_passkey || eli.passkey_count > 0 ? '✓' : '✗'} ${t('Passkey 已注册')}
|
|
4345
|
+
</span>
|
|
4346
|
+
<span style="color:#6b7280">${eli.passkey_count} ${eli.require_passkey ? `(${t('必需')})` : `(${t('可选')})`}</span>
|
|
4347
|
+
</div>`
|
|
4348
|
+
|
|
4243
4349
|
app.innerHTML = shell(`
|
|
4244
|
-
<h1 class="page-title"
|
|
4350
|
+
<h1 class="page-title">🎁 ${t('申请共建身份')}</h1>
|
|
4351
|
+
|
|
4352
|
+
<div class="card" style="margin-bottom:16px;background:#fef2f2;border-color:#fca5a5">
|
|
4353
|
+
<div style="font-weight:600;color:#991b1b;margin-bottom:6px;font-size:14px">⚠️ ${t('本流程与购物无关')}</div>
|
|
4354
|
+
<div style="font-size:12px;color:#7f1d1d;line-height:1.6">
|
|
4355
|
+
${t('你可以随时退出,不影响任何已下单或未来订单。本流程涉及经济关系登记(三级佣金 + 双轨配对),请仔细阅读全部条款。')}<br>
|
|
4356
|
+
<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
|
+
</div>
|
|
4358
|
+
</div>
|
|
4359
|
+
|
|
4245
4360
|
<div class="card" style="margin-bottom:16px">
|
|
4246
|
-
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('
|
|
4361
|
+
<div style="font-size:13px;color:#6b7280;margin-bottom:10px">${t('门槛(全部通过才能申请)')}:</div>
|
|
4247
4362
|
${checklist}
|
|
4248
4363
|
</div>
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4364
|
+
|
|
4365
|
+
<div class="card" style="margin-bottom:16px;background:#fafafa">
|
|
4366
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:6px">${t('同意文本')} (v${consentVersion}):</div>
|
|
4367
|
+
<div style="font-size:13px;color:#374151;line-height:1.7;margin-bottom:8px">${escHtml(consentText)}</div>
|
|
4368
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.7;font-style:italic">${escHtml(consentTextEn)}</div>
|
|
4369
|
+
</div>
|
|
4370
|
+
|
|
4371
|
+
${!canApply ? `
|
|
4372
|
+
<div class="card" style="background:#fff7ed;border-color:#fdba74;padding:12px;font-size:13px;color:#7c2d12">
|
|
4373
|
+
${t('请先完成上述未达标指标,完成后再来申请')}
|
|
4374
|
+
</div>
|
|
4375
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4376
|
+
` : `
|
|
4377
|
+
<div class="card" style="margin-bottom:16px">
|
|
4378
|
+
<div style="font-size:13px;color:#374151;margin-bottom:12px;font-weight:600">${t('知情同意(双勾选)')}</div>
|
|
4379
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer">
|
|
4380
|
+
<input type="checkbox" id="rwd-consent-1" onchange="updateRwdApplyState()">
|
|
4381
|
+
<span style="font-size:13px;color:#374151">${t('我已阅读全部条款,理解 commission 与 PV 树结构的合规边界')}</span>
|
|
4382
|
+
</label>
|
|
4383
|
+
<label id="rwd-consent-2-wrap" style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer;opacity:0.5">
|
|
4384
|
+
<input type="checkbox" id="rwd-consent-2" disabled onchange="updateRwdApplyState()">
|
|
4385
|
+
<span style="font-size:13px;color:#374151">${t('我自愿申请,无人收买 / 诱导')} <span id="rwd-countdown" style="color:#dc2626;font-weight:600"></span></span>
|
|
4386
|
+
</label>
|
|
4387
|
+
</div>
|
|
4388
|
+
|
|
4389
|
+
<button class="btn btn-primary" id="rwd-submit-btn" onclick="doSubmitRewardsApply('${consentVersion}', ${pageLoadedAt})" disabled style="opacity:0.5">
|
|
4390
|
+
🔑 ${t('用 Passkey 签发提交申请')}
|
|
4391
|
+
</button>
|
|
4392
|
+
|
|
4393
|
+
<div id="rwd-apply-msg" style="margin-top:12px"></div>
|
|
4394
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4395
|
+
`}
|
|
4396
|
+
`, returnNav)
|
|
4397
|
+
|
|
4398
|
+
if (canApply) {
|
|
4399
|
+
let secondsLeft = delaySec
|
|
4400
|
+
const tick = () => {
|
|
4401
|
+
const el = document.getElementById('rwd-countdown')
|
|
4402
|
+
if (!el) return
|
|
4403
|
+
if (secondsLeft > 0) {
|
|
4404
|
+
el.textContent = '(' + t('等') + ' ' + secondsLeft + 's)'
|
|
4405
|
+
secondsLeft--
|
|
4406
|
+
setTimeout(tick, 1000)
|
|
4407
|
+
} else {
|
|
4408
|
+
el.textContent = ''
|
|
4409
|
+
const cb2 = document.getElementById('rwd-consent-2')
|
|
4410
|
+
if (cb2) cb2.disabled = false
|
|
4411
|
+
const wrap = document.getElementById('rwd-consent-2-wrap')
|
|
4412
|
+
if (wrap) wrap.style.opacity = '1'
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
tick()
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
window.updateRwdApplyState = () => {
|
|
4420
|
+
const cb1 = document.getElementById('rwd-consent-1')
|
|
4421
|
+
const cb2 = document.getElementById('rwd-consent-2')
|
|
4422
|
+
const btn = document.getElementById('rwd-submit-btn')
|
|
4423
|
+
if (cb1 && cb2 && btn) {
|
|
4424
|
+
const ok = cb1.checked && cb2.checked && !cb2.disabled
|
|
4425
|
+
btn.disabled = !ok
|
|
4426
|
+
btn.style.opacity = ok ? '1' : '0.5'
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
window.doSubmitRewardsApply = async (consentVersion, pageLoadedAt) => {
|
|
4431
|
+
const msg = document.getElementById('rwd-apply-msg')
|
|
4432
|
+
if (!msg) return
|
|
4433
|
+
|
|
4434
|
+
// Reconstruct consent_hash matching server's expectedApplyConsentHash() exactly
|
|
4435
|
+
const consentText = 'rewards_apply|consent_version=' + consentVersion + '|user=' + state.user.id + '|page_loaded_at=' + pageLoadedAt
|
|
4436
|
+
const encoder = new TextEncoder()
|
|
4437
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(consentText))
|
|
4438
|
+
const consentHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2,'0')).join('')
|
|
4439
|
+
|
|
4440
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
4441
|
+
let passkeyToken
|
|
4442
|
+
try {
|
|
4443
|
+
passkeyToken = await requestPasskeyGate('rewards_apply', { consent_version: consentVersion, consent_hash: consentHash })
|
|
4444
|
+
} catch (e) {
|
|
4445
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
4446
|
+
return
|
|
4447
|
+
}
|
|
4448
|
+
|
|
4449
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4450
|
+
const res = await POST('/rewards/apply', {
|
|
4451
|
+
consent_version: consentVersion,
|
|
4452
|
+
consent_hash: consentHash,
|
|
4453
|
+
page_loaded_at: pageLoadedAt,
|
|
4454
|
+
webauthn_token: passkeyToken,
|
|
4455
|
+
})
|
|
4456
|
+
|
|
4457
|
+
if (res.error) {
|
|
4458
|
+
msg.innerHTML = alert$('error', res.error)
|
|
4459
|
+
return
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
const drained = res.drained_from_escrow || { count: 0, total: 0 }
|
|
4463
|
+
const drainNote = drained.count > 0
|
|
4464
|
+
? '<br>' + t('已从 escrow 拨回:') + ' ' + drained.total + ' WAZ (' + drained.count + ' ' + t('笔') + ')'
|
|
4465
|
+
: ''
|
|
4466
|
+
msg.innerHTML = alert$('success', t('✅ 共建身份激活成功') + drainNote)
|
|
4467
|
+
setTimeout(() => { location.hash = '#rewards-me' }, 2000)
|
|
4468
|
+
}
|
|
4469
|
+
|
|
4470
|
+
async function renderRewardsMe(app) {
|
|
4471
|
+
if (!state.user) { renderLogin(); return }
|
|
4472
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
4473
|
+
const status = await GET('/rewards/status')
|
|
4474
|
+
if (status.error) { app.innerHTML = shell(alert$('error', status.error), 'me'); return }
|
|
4475
|
+
|
|
4476
|
+
const stateLabel = {
|
|
4477
|
+
opted_in: { icon: '✅', color: '#16a34a', label: t('共建身份激活中') },
|
|
4478
|
+
never_activated: { icon: '⚪', color: '#6b7280', label: t('未激活') },
|
|
4479
|
+
auto_downgraded: { icon: '⚠️', color: '#f59e0b', label: t('已自动降级(consent 未确认)') },
|
|
4480
|
+
deactivated: { icon: '🔒', color: '#9ca3af', label: t('已退出') },
|
|
4481
|
+
}[status.state] || { icon: '?', color: '#6b7280', label: status.state }
|
|
4482
|
+
|
|
4483
|
+
const escrow = status.pending_escrow || { count: 0, total_amount: 0 }
|
|
4484
|
+
const expired = status.expired_to_charity || { count: 0, total_amount: 0 }
|
|
4485
|
+
|
|
4486
|
+
app.innerHTML = shell(`
|
|
4487
|
+
<h1 class="page-title">🎁 ${t('共建身份管理')}</h1>
|
|
4488
|
+
|
|
4489
|
+
<div class="card" style="margin-bottom:16px;border-left:4px solid ${stateLabel.color};padding:14px">
|
|
4490
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
|
|
4491
|
+
<span style="font-size:28px">${stateLabel.icon}</span>
|
|
4492
|
+
<span style="font-size:16px;font-weight:600;color:${stateLabel.color}">${stateLabel.label}</span>
|
|
4493
|
+
</div>
|
|
4494
|
+
${status.consent_version ? `<div style="font-size:12px;color:#9ca3af">${t('Consent version')}: v${status.consent_version}</div>` : ''}
|
|
4495
|
+
</div>
|
|
4496
|
+
|
|
4497
|
+
${escrow.count > 0 ? `
|
|
4498
|
+
<div class="card" style="margin-bottom:16px;background:#fef3c7;border-color:#fde68a;padding:12px">
|
|
4499
|
+
<div style="font-weight:600;color:#92400e;font-size:13px;margin-bottom:4px">💰 ${t('待领 escrow')}</div>
|
|
4500
|
+
<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>
|
|
4502
|
+
</div>
|
|
4503
|
+
` : ''}
|
|
4504
|
+
|
|
4505
|
+
${expired.count > 0 ? `
|
|
4506
|
+
<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
|
|
4508
|
+
</div>
|
|
4509
|
+
` : ''}
|
|
4510
|
+
|
|
4511
|
+
${status.state === 'opted_in' ? `
|
|
4512
|
+
<div class="card" style="margin-bottom:16px;padding:14px">
|
|
4513
|
+
<div style="font-size:13px;color:#374151;margin-bottom:10px">${t('退出共建身份后,未来 commission 直接入公益(无 escrow)。可随时再申请。')}</div>
|
|
4514
|
+
<button class="btn" style="background:#fee2e2;color:#991b1b;border:1px solid #fca5a5" onclick="doDeactivateRewards()">
|
|
4515
|
+
🔒 ${t('退出共建身份')}
|
|
4516
|
+
</button>
|
|
4517
|
+
<div id="rwd-deact-msg" style="margin-top:10px"></div>
|
|
4518
|
+
</div>
|
|
4519
|
+
` : `
|
|
4520
|
+
<button class="btn btn-primary" onclick="location.hash='#apply-rewards'">
|
|
4521
|
+
🎁 ${status.state === 'never_activated' ? t('申请共建身份') : t('重新申请')}
|
|
4522
|
+
</button>
|
|
4523
|
+
`}
|
|
4524
|
+
|
|
4525
|
+
<div style="margin-top:20px;font-size:11px;color:#9ca3af;line-height:1.6">
|
|
4526
|
+
${t('协议依据:')} <a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/rfcs/RFC-002-rewards-opt-in.md" target="_blank" style="color:#6b7280">RFC-002</a>
|
|
4527
|
+
</div>
|
|
4528
|
+
`, 'me')
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
window.doDeactivateRewards = async () => {
|
|
4532
|
+
const msg = document.getElementById('rwd-deact-msg')
|
|
4533
|
+
if (!msg) return
|
|
4534
|
+
if (!confirm(t('确认退出共建身份?未来 commission 将直接入公益,可随时再申请。'))) return
|
|
4535
|
+
|
|
4536
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
4537
|
+
let passkeyToken
|
|
4538
|
+
try {
|
|
4539
|
+
passkeyToken = await requestPasskeyGate('rewards_deactivate', {})
|
|
4540
|
+
} catch (e) {
|
|
4541
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
4542
|
+
return
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4545
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4546
|
+
const res = await POST('/rewards/deactivate', { webauthn_token: passkeyToken })
|
|
4547
|
+
if (res.error) {
|
|
4548
|
+
msg.innerHTML = alert$('error', res.error)
|
|
4549
|
+
return
|
|
4550
|
+
}
|
|
4551
|
+
msg.innerHTML = alert$('success', t('✅ 已退出共建身份'))
|
|
4552
|
+
setTimeout(() => renderRewardsMe(document.getElementById('app')), 1200)
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4555
|
+
async function renderApplyVerifier(app) { return _renderApplyGovernance(app, 'verifier') }
|
|
4556
|
+
async function renderApplyArbitrator(app) { return _renderApplyGovernance(app, 'arbitrator') }
|
|
4557
|
+
|
|
4558
|
+
// ─── Onboarding 学习包 + 题目(spec §4.1 + §4.3,#1093 阶段 2a)─────
|
|
4559
|
+
// 案例研读(§4.2)留下一个 PR
|
|
4560
|
+
async function renderOnboarding(app, role) {
|
|
4561
|
+
if (!state.user) { renderLogin(); return }
|
|
4562
|
+
const returnNav = role === 'arbitrator' ? 'me' : 'verify-tasks'
|
|
4563
|
+
const roleTitle = role === 'arbitrator' ? t('仲裁员上岗 onboarding') : t('审核员上岗 onboarding')
|
|
4564
|
+
const roleIcon = role === 'arbitrator' ? '⚖' : '🛡'
|
|
4565
|
+
|
|
4566
|
+
app.innerHTML = shell(loading$(), returnNav)
|
|
4567
|
+
|
|
4568
|
+
// 同时加载 quiz 题库 + 案例库 + 当前 progress
|
|
4569
|
+
const [quizData, casesData, progress] = await Promise.all([
|
|
4570
|
+
GET('/governance/onboarding/quiz?role=' + role),
|
|
4571
|
+
GET('/governance/onboarding/cases?role=' + role),
|
|
4572
|
+
GET('/governance/onboarding/progress'),
|
|
4573
|
+
])
|
|
4574
|
+
|
|
4575
|
+
if (quizData.error) { app.innerHTML = shell(alert$('error', quizData.error), returnNav); return }
|
|
4576
|
+
if (casesData.error) { app.innerHTML = shell(alert$('error', casesData.error), returnNav); return }
|
|
4577
|
+
|
|
4578
|
+
const currentApp = (progress.applications || []).find(a => a.role === role && a.status === 'pending_onboarding')
|
|
4579
|
+
if (!currentApp) {
|
|
4580
|
+
const roleLabel = role === 'arbitrator' ? t('仲裁员') : t('审核员')
|
|
4581
|
+
app.innerHTML = shell(`
|
|
4582
|
+
<h1 class="page-title">${roleIcon} ${roleTitle}</h1>
|
|
4583
|
+
<div class="card" style="background:#fff7ed;border-color:#fdba74">
|
|
4584
|
+
<div style="font-weight:600;color:#7c2d12">${t('请先提交申请')} (${roleLabel})</div>
|
|
4585
|
+
<div style="font-size:13px;color:#7c2d12;margin-top:8px"><a href="#apply-${role}">#apply-${role}</a> ${t('完成申请')}</div>
|
|
4586
|
+
</div>
|
|
4587
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4588
|
+
`, returnNav)
|
|
4589
|
+
return
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
const studyDocs = [
|
|
4593
|
+
{ name: 'META-RULES-FULL.md', desc: t('10 元规则,特别是 #5 不偏袒 / #6 不滥用'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md' },
|
|
4594
|
+
{ name: 'CHARTER.md §3.2 + §6', desc: t('权力边界:多签 + 修改流程'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/CHARTER.md' },
|
|
4595
|
+
{ name: 'SECURITY.md §Iron-Rule', desc: t('真人 Passkey 7 条路径'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/SECURITY.md' },
|
|
4596
|
+
{ name: 'ECONOMIC-MODEL.md §11', desc: t('经济博弈原则 + 关系层估值层'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md' },
|
|
4597
|
+
{ name: 'ARBITRATION-PLAYBOOK.md', desc: t('案例决策树 + 4 种结算路径') + (role === 'arbitrator' ? ' (' + t('必读') + ')' : ''), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md' },
|
|
4598
|
+
{ name: 'MLM-COMPLIANCE.md', desc: t('合规边界'), link: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md' },
|
|
4599
|
+
]
|
|
4600
|
+
|
|
4601
|
+
const studySection = studyDocs.map((d, i) => `
|
|
4602
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:8px 0;border-bottom:1px solid #f3f4f6;cursor:pointer">
|
|
4603
|
+
<input type="checkbox" class="study-cb" data-idx="${i}" onchange="updateOnboardingState()">
|
|
4604
|
+
<div style="flex:1">
|
|
4605
|
+
<a href="${d.link}" target="_blank" rel="noopener" style="font-weight:600;color:#1d4ed8;font-size:13px;text-decoration:none">${d.name} ↗</a>
|
|
4606
|
+
<div style="font-size:11px;color:#6b7280;margin-top:2px">${d.desc}</div>
|
|
4607
|
+
</div>
|
|
4608
|
+
</label>`).join('')
|
|
4609
|
+
|
|
4610
|
+
const quizSection = (quizData.questions || []).map((q, idx) => {
|
|
4611
|
+
if (q.type === 'multiple_choice') {
|
|
4612
|
+
const opts = (q.options || []).map(opt => `
|
|
4613
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:6px 0;cursor:pointer">
|
|
4614
|
+
<input type="radio" name="${q.id}" value="${opt.key}" onchange="updateOnboardingState()">
|
|
4615
|
+
<span style="font-size:13px;color:#374151">${opt.key}. ${state._lang === 'en' ? opt.text_en : opt.text_zh}</span>
|
|
4616
|
+
</label>`).join('')
|
|
4617
|
+
return `
|
|
4618
|
+
<div class="card" style="margin-bottom:12px">
|
|
4619
|
+
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:8px">${idx + 1}. ${state._lang === 'en' ? q.question_en : q.question_zh}</div>
|
|
4620
|
+
${opts}
|
|
4621
|
+
</div>`
|
|
4622
|
+
} else {
|
|
4623
|
+
return `
|
|
4624
|
+
<div class="card" style="margin-bottom:12px">
|
|
4625
|
+
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:8px">${idx + 1}. ${state._lang === 'en' ? q.question_en : q.question_zh}</div>
|
|
4626
|
+
<textarea class="form-control short-answer" data-qid="${q.id}" rows="4" placeholder="${t('至少 N 字符').replace('N', String(q.min_chars || 50))}" oninput="updateOnboardingState()" style="font-size:13px"></textarea>
|
|
4627
|
+
</div>`
|
|
4628
|
+
}
|
|
4629
|
+
}).join('')
|
|
4630
|
+
|
|
4631
|
+
const lastScore = currentApp.quiz_score
|
|
4632
|
+
const scoreBanner = lastScore != null ? `
|
|
4633
|
+
<div class="card" style="margin-bottom:16px;background:${lastScore >= 80 ? '#f0fdf4;border-color:#86efac' : '#fff7ed;border-color:#fdba74'}">
|
|
4634
|
+
<div style="font-weight:600">${t('上次得分')}: ${lastScore}% ${lastScore >= 80 ? '✅ ' + t('已合格,等待 maintainer 激活') : '⚠️ ' + t('未合格,可重试')}</div>
|
|
4635
|
+
</div>` : ''
|
|
4636
|
+
|
|
4637
|
+
app.innerHTML = shell(`
|
|
4638
|
+
<h1 class="page-title">${roleIcon} ${roleTitle}</h1>
|
|
4639
|
+
|
|
4640
|
+
${scoreBanner}
|
|
4641
|
+
|
|
4642
|
+
<div class="card" style="margin-bottom:16px;background:#eff6ff;border-color:#93c5fd">
|
|
4643
|
+
<div style="font-weight:600;font-size:13px;color:#1e3a8a;margin-bottom:6px">📚 ${t('§4.1 学习包')}</div>
|
|
4644
|
+
<div style="font-size:12px;color:#1e40af;margin-bottom:10px">${t('阅读以下文档,勾选已读。本步骤不写入 DB(仅本地标记),实际理解由题目和案例考察。')}</div>
|
|
4645
|
+
${studySection}
|
|
4646
|
+
</div>
|
|
4647
|
+
|
|
4648
|
+
<div class="card" style="margin-bottom:16px;background:#f5f3ff;border-color:#c4b5fd">
|
|
4649
|
+
<div style="font-weight:600;font-size:13px;color:#5b21b6;margin-bottom:6px">📝 ${t('§4.3 题目')}</div>
|
|
4650
|
+
<div style="font-size:12px;color:#6b21a8;margin-bottom:10px">
|
|
4651
|
+
${(quizData.questions || []).length} ${t('题(多选 + 短答),合格线 80%')}(${quizData.questions ? Math.ceil(quizData.questions.length * 0.8) : 12}/${quizData.questions ? quizData.questions.length : 15})
|
|
4652
|
+
</div>
|
|
4653
|
+
${quizSection}
|
|
4654
|
+
</div>
|
|
4655
|
+
|
|
4656
|
+
<button class="btn btn-primary" id="onb-submit-btn" onclick="doSubmitOnboarding('${role}')" disabled style="opacity:0.5">
|
|
4657
|
+
${t('提交题目并评分')}
|
|
4658
|
+
</button>
|
|
4659
|
+
|
|
4660
|
+
<div id="onb-msg" style="margin-top:12px"></div>
|
|
4661
|
+
|
|
4662
|
+
<!-- §4.2 案例研读(spec §4.2,task #1093 阶段 2b) -->
|
|
4663
|
+
<div class="card" style="margin-top:24px;margin-bottom:16px;background:#ecfdf5;border-color:#86efac">
|
|
4664
|
+
<div style="font-weight:600;font-size:13px;color:#065f46;margin-bottom:6px">📚 ${t('§4.2 案例研读')}</div>
|
|
4665
|
+
<div style="font-size:12px;color:#047857;margin-bottom:10px">
|
|
4666
|
+
${casesData.total} ${t('个案例,每个需选 verdict + 写理由 ≥ 200 字。本步骤')}<strong>${t('不自动评分')}</strong>${t(',maintainer 上岗签字前对比 expected verdict 评估你的 reasoning 方向。')}
|
|
4667
|
+
</div>
|
|
4668
|
+
${(casesData.cases || []).map((c, idx) => `
|
|
4669
|
+
<details style="margin-bottom:12px;background:white;padding:10px;border-radius:6px;border:1px solid #d1fae5" ${idx === 0 ? 'open' : ''}>
|
|
4670
|
+
<summary style="font-weight:600;font-size:13px;cursor:pointer;color:#065f46">${idx + 1}. ${state._lang === 'en' ? c.scenario_en : c.scenario_zh}</summary>
|
|
4671
|
+
<div style="margin-top:10px">
|
|
4672
|
+
<div style="font-size:12px;color:#374151;font-weight:600;margin-bottom:4px">${t('已知事实')}:</div>
|
|
4673
|
+
<ul style="margin:0;padding-left:18px;font-size:12px;color:#4b5563;line-height:1.7">
|
|
4674
|
+
${(state._lang === 'en' ? c.facts_en : c.facts_zh).map(f => `<li>${f}</li>`).join('')}
|
|
4675
|
+
</ul>
|
|
4676
|
+
<div style="font-size:12px;color:#374151;font-weight:600;margin-top:10px;margin-bottom:4px">${t('选择你的 verdict')}:</div>
|
|
4677
|
+
${(c.decision_options || []).map(opt => `
|
|
4678
|
+
<label style="display:flex;align-items:flex-start;gap:8px;padding:4px 0;cursor:pointer">
|
|
4679
|
+
<input type="radio" name="case-${c.id}-verdict" value="${opt.key}" onchange="updateOnboardingState()">
|
|
4680
|
+
<span style="font-size:12px;color:#374151">${opt.key}: ${state._lang === 'en' ? opt.text_en : opt.text_zh}</span>
|
|
4681
|
+
</label>`).join('')}
|
|
4682
|
+
<div style="font-size:12px;color:#374151;font-weight:600;margin-top:10px;margin-bottom:4px">${t('reasoning(≥ N 字符)').replace('N', String(c.min_review_chars))}:</div>
|
|
4683
|
+
<textarea class="form-control case-reasoning" data-case-id="${c.id}" rows="5" placeholder="${t('解释你为什么选这个 verdict,引用具体事实和 spec 原则...')}" oninput="updateOnboardingState()" style="font-size:12px;width:100%"></textarea>
|
|
4684
|
+
</div>
|
|
4685
|
+
</details>`).join('')}
|
|
4686
|
+
</div>
|
|
4687
|
+
|
|
4688
|
+
<button class="btn btn-primary" id="onb-case-submit-btn" onclick="doSubmitCaseReview('${role}')" disabled style="opacity:0.5">
|
|
4689
|
+
${t('提交案例 review')}
|
|
4690
|
+
</button>
|
|
4691
|
+
|
|
4692
|
+
<div id="onb-case-msg" style="margin-top:12px"></div>
|
|
4693
|
+
<div style="margin-top:16px"><a href="#${returnNav}" style="font-size:13px;color:#6b7280">← ${t('返回')}</a></div>
|
|
4694
|
+
`, returnNav)
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
window.updateOnboardingState = () => {
|
|
4698
|
+
const allDocsChecked = Array.from(document.querySelectorAll('.study-cb')).every(cb => cb.checked)
|
|
4699
|
+
const allMcqAnswered = Array.from(document.querySelectorAll('[name^="mcq-"]')).reduce((acc, el) => {
|
|
4700
|
+
const name = el.name
|
|
4701
|
+
if (!acc.has(name)) acc.set(name, false)
|
|
4702
|
+
if (el.checked) acc.set(name, true)
|
|
4703
|
+
return acc
|
|
4704
|
+
}, new Map())
|
|
4705
|
+
const mcqOk = Array.from(allMcqAnswered.values()).every(v => v) && allMcqAnswered.size > 0
|
|
4706
|
+
const allShortAnswered = Array.from(document.querySelectorAll('.short-answer')).every(ta => ta.value.trim().length >= 50)
|
|
4707
|
+
const btn = document.getElementById('onb-submit-btn')
|
|
4708
|
+
if (btn) {
|
|
4709
|
+
const ok = allDocsChecked && mcqOk && allShortAnswered
|
|
4710
|
+
btn.disabled = !ok
|
|
4711
|
+
btn.style.opacity = ok ? '1' : '0.5'
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
// §4.2 案例 submit 独立启用:每个 case 必须选 verdict + reasoning ≥ 200 字
|
|
4715
|
+
const caseVerdictGroups = new Map()
|
|
4716
|
+
document.querySelectorAll('[name^="case-"][type="radio"]').forEach(el => {
|
|
4717
|
+
if (!caseVerdictGroups.has(el.name)) caseVerdictGroups.set(el.name, false)
|
|
4718
|
+
if (el.checked) caseVerdictGroups.set(el.name, true)
|
|
4719
|
+
})
|
|
4720
|
+
const caseVerdictsOk = caseVerdictGroups.size > 0 && Array.from(caseVerdictGroups.values()).every(v => v)
|
|
4721
|
+
const allCaseReasoningOk = Array.from(document.querySelectorAll('.case-reasoning')).every(ta => ta.value.trim().length >= 200)
|
|
4722
|
+
const caseBtn = document.getElementById('onb-case-submit-btn')
|
|
4723
|
+
if (caseBtn) {
|
|
4724
|
+
const ok = caseVerdictsOk && allCaseReasoningOk
|
|
4725
|
+
caseBtn.disabled = !ok
|
|
4726
|
+
caseBtn.style.opacity = ok ? '1' : '0.5'
|
|
4727
|
+
}
|
|
4728
|
+
}
|
|
4729
|
+
|
|
4730
|
+
// ─── /#governance-me 用户治理面板(#1093 阶段 4)─────
|
|
4731
|
+
// spec docs/GOVERNANCE-ONBOARDING.md §6.1 §7.2:active role 卸任 / auto_deactivate 申诉
|
|
4732
|
+
async function renderGovernanceMe(app) {
|
|
4733
|
+
if (!state.user) { renderLogin(); return }
|
|
4734
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
4735
|
+
const data = await GET('/governance/onboarding/my')
|
|
4736
|
+
if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'me'); return }
|
|
4737
|
+
|
|
4738
|
+
const items = data.items || []
|
|
4739
|
+
const now = Math.floor(Date.now() / 1000)
|
|
4740
|
+
|
|
4741
|
+
// active 角色:有 action 在 ('apply' 但 status='active') 或 ('activate' 且 status='active')
|
|
4742
|
+
// 简化:把 status='active' 全列(可能为 apply row 或 activate row)
|
|
4743
|
+
const actives = items.filter(i => i.status === 'active')
|
|
4744
|
+
const pendings = items.filter(i => i.status === 'pending_onboarding')
|
|
4745
|
+
// auto_deactivate 行(可申诉)
|
|
4746
|
+
const autoDeactivates = items.filter(i => i.action === 'auto_deactivate')
|
|
4747
|
+
// pending appeals
|
|
4748
|
+
const pendingAppeals = items.filter(i => i.action === 'appeal' && i.status === 'pending_review')
|
|
4749
|
+
|
|
4750
|
+
const roleNames = {
|
|
4751
|
+
arbitrator: { zh: '仲裁员', icon: '⚖' },
|
|
4752
|
+
verifier: { zh: '审核员', icon: '🛡' },
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
// active 列表(可卸任)— 按 role 去重(可能有 apply + activate 两行,只显示一个)
|
|
4756
|
+
const seenActive = new Set()
|
|
4757
|
+
const activeBlocks = actives.filter(a => {
|
|
4758
|
+
if (seenActive.has(a.role)) return false
|
|
4759
|
+
seenActive.add(a.role)
|
|
4760
|
+
return true
|
|
4761
|
+
}).map(a => {
|
|
4762
|
+
const rn = roleNames[a.role] || { zh: a.role, icon: '·' }
|
|
4763
|
+
return `
|
|
4764
|
+
<div class="card" style="margin-bottom:12px;border-color:#86efac;background:#f0fdf4">
|
|
4765
|
+
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
|
4766
|
+
<div>
|
|
4767
|
+
<div style="font-weight:600;font-size:15px">${rn.icon} ${t(rn.zh)} <span style="color:#16a34a;font-size:12px;margin-left:6px">● ${t('在岗')}</span></div>
|
|
4768
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('上岗时间')}: ${new Date(a.created_at * 1000).toLocaleDateString()}</div>
|
|
4769
|
+
</div>
|
|
4770
|
+
<button class="btn btn-sm btn-outline" style="color:#dc2626;border-color:#fca5a5" onclick="openResignModal('${a.role}')">${t('卸任')}</button>
|
|
4771
|
+
</div>
|
|
4772
|
+
</div>`
|
|
4773
|
+
}).join('')
|
|
4774
|
+
|
|
4775
|
+
// 申诉窗口检查(14d)
|
|
4776
|
+
const windowDays = 14
|
|
4777
|
+
const appealableBlocks = autoDeactivates.filter(a => (now - a.created_at) <= windowDays * 86400).map(a => {
|
|
4778
|
+
const rn = roleNames[a.role] || { zh: a.role, icon: '·' }
|
|
4779
|
+
const daysLeft = windowDays - Math.floor((now - a.created_at) / 86400)
|
|
4780
|
+
// 检查是否已 appeal
|
|
4781
|
+
const hasAppeal = items.some(i => i.action === 'appeal' && i.source_application_id === a.id)
|
|
4782
|
+
return `
|
|
4783
|
+
<div class="card" style="margin-bottom:12px;border-color:#fde68a;background:#fffbeb">
|
|
4784
|
+
<div style="font-weight:600;font-size:14px;color:#92400e">${rn.icon} ${t('被自动卸任')}: ${t(rn.zh)}</div>
|
|
4785
|
+
<div style="font-size:12px;color:#78350f;margin-top:6px">${t('生效时间')}: ${new Date(a.created_at * 1000).toLocaleDateString()} · ${t('申诉窗口剩余')}: ${daysLeft} ${t('天')}</div>
|
|
4786
|
+
${hasAppeal
|
|
4787
|
+
? `<div style="margin-top:10px;font-size:12px;color:#1e40af">${t('已提交申诉,等待 maintainer 裁决')}</div>`
|
|
4788
|
+
: `<button class="btn btn-sm btn-primary" style="margin-top:10px" onclick="openAppealModal('${a.id}', '${a.role}')">${t('提交申诉')}</button>`}
|
|
4789
|
+
</div>`
|
|
4790
|
+
}).join('')
|
|
4791
|
+
|
|
4792
|
+
const pendingAppealBlocks = pendingAppeals.map(a => {
|
|
4793
|
+
const rn = roleNames[a.role] || { zh: a.role, icon: '·' }
|
|
4794
|
+
return `
|
|
4795
|
+
<div class="card" style="margin-bottom:12px;border-color:#93c5fd;background:#eff6ff">
|
|
4796
|
+
<div style="font-weight:600;font-size:14px;color:#1e3a8a">${rn.icon} ${t('申诉审核中')}: ${t(rn.zh)}</div>
|
|
4797
|
+
<div style="font-size:12px;color:#1e40af;margin-top:6px">${t('提交时间')}: ${new Date(a.created_at * 1000).toLocaleDateString()}</div>
|
|
4798
|
+
<div style="font-size:12px;color:#374151;margin-top:8px;padding:8px;background:#fff;border-radius:4px">${escHtml((a.appeal_reason || '').slice(0, 200))}${(a.appeal_reason || '').length > 200 ? '...' : ''}</div>
|
|
4799
|
+
</div>`
|
|
4800
|
+
}).join('')
|
|
4801
|
+
|
|
4802
|
+
const pendingBlocks = pendings.map(p => {
|
|
4803
|
+
const rn = roleNames[p.role] || { zh: p.role, icon: '·' }
|
|
4804
|
+
return `
|
|
4805
|
+
<div class="card" style="margin-bottom:12px">
|
|
4806
|
+
<div style="font-weight:600;font-size:14px">${rn.icon} ${t(rn.zh)} <span style="color:#d97706;font-size:12px;margin-left:6px">${t('申请审核中')}</span></div>
|
|
4807
|
+
<div style="margin-top:8px"><a href="#onboarding-${p.role}" class="btn btn-sm btn-outline">${t('继续 onboarding')}</a></div>
|
|
4808
|
+
</div>`
|
|
4809
|
+
}).join('')
|
|
4810
|
+
|
|
4811
|
+
const emptyState = (actives.length + autoDeactivates.length + pendings.length + pendingAppeals.length) === 0 ? `
|
|
4812
|
+
<div class="card" style="text-align:center;padding:24px">
|
|
4813
|
+
<div style="font-size:14px;color:#6b7280">${t('暂无治理岗位记录')}</div>
|
|
4814
|
+
<div style="margin-top:12px;display:flex;gap:8px;justify-content:center">
|
|
4815
|
+
<a href="#apply-arbitrator" class="btn btn-sm btn-outline">⚖ ${t('申请仲裁员')}</a>
|
|
4816
|
+
<a href="#apply-verifier" class="btn btn-sm btn-outline">🛡 ${t('申请审核员')}</a>
|
|
4817
|
+
</div>
|
|
4818
|
+
</div>` : ''
|
|
4819
|
+
|
|
4820
|
+
app.innerHTML = shell(`
|
|
4821
|
+
<h1 class="page-title">⚖️ ${t('我的治理岗位')}</h1>
|
|
4822
|
+
<div class="card" style="margin-bottom:16px;background:#eff6ff;border-color:#93c5fd">
|
|
4823
|
+
<div style="font-size:13px;color:#1e3a8a">${t('spec docs/GOVERNANCE-ONBOARDING.md §6 §7')}</div>
|
|
4824
|
+
<div style="font-size:12px;color:#1e40af;margin-top:4px">${t('phase A 治理 = 关系层数据采集 + 公共贡献意愿表达,不预设估值层')}</div>
|
|
4825
|
+
</div>
|
|
4826
|
+
${activeBlocks ? `<h2 style="font-size:14px;color:#374151;margin:16px 0 8px">${t('当前在岗')}</h2>${activeBlocks}` : ''}
|
|
4827
|
+
${appealableBlocks ? `<h2 style="font-size:14px;color:#92400e;margin:16px 0 8px">⚠️ ${t('待申诉(14 天内)')}</h2>${appealableBlocks}` : ''}
|
|
4828
|
+
${pendingAppealBlocks ? `<h2 style="font-size:14px;color:#1e40af;margin:16px 0 8px">${t('申诉审核中')}</h2>${pendingAppealBlocks}` : ''}
|
|
4829
|
+
${pendingBlocks ? `<h2 style="font-size:14px;color:#374151;margin:16px 0 8px">${t('待审申请')}</h2>${pendingBlocks}` : ''}
|
|
4830
|
+
${emptyState}
|
|
4831
|
+
<div style="margin-top:16px"><a href="#me" style="font-size:13px;color:#6b7280">← ${t('返回 #me')}</a></div>
|
|
4255
4832
|
`, 'me')
|
|
4256
4833
|
}
|
|
4257
4834
|
|
|
4258
|
-
window.
|
|
4259
|
-
const
|
|
4835
|
+
window.openResignModal = (role) => {
|
|
4836
|
+
const expected = `RESIGN ${role}`
|
|
4837
|
+
const html = `
|
|
4838
|
+
<div id="resign-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px">
|
|
4839
|
+
<div style="background:#fff;border-radius:8px;max-width:480px;width:100%;padding:20px">
|
|
4840
|
+
<h3 style="margin:0 0 12px;color:#dc2626">⚠️ ${t('卸任治理岗位')}: ${role}</h3>
|
|
4841
|
+
<div style="font-size:13px;color:#374151;line-height:1.6;margin-bottom:12px">
|
|
4842
|
+
<ul style="padding-left:20px;margin:0">
|
|
4843
|
+
<li>${t('卸任后历史履职记录保留(关系层,不可逆)')}</li>
|
|
4844
|
+
<li>${t('卸任不影响 reputation / dev_contribution')}</li>
|
|
4845
|
+
<li>${t('30 天内不能重新申请同一角色(冷却期)')}</li>
|
|
4846
|
+
<li>${t('已 assigned 但未完成的 case 必须先完成 / 转交,否则无法卸任')}</li>
|
|
4847
|
+
</ul>
|
|
4848
|
+
</div>
|
|
4849
|
+
<div style="font-size:13px;color:#374151;margin-bottom:8px">${t('输入')} <code style="background:#fee2e2;padding:2px 6px;border-radius:4px;color:#991b1b">${expected}</code> ${t('确认')}:</div>
|
|
4850
|
+
<input id="resign-confirm-input" type="text" placeholder="${expected}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;font-family:monospace" oninput="document.getElementById('resign-submit-btn').disabled = this.value !== '${expected}'">
|
|
4851
|
+
<div id="resign-msg" style="margin-top:12px"></div>
|
|
4852
|
+
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
|
|
4853
|
+
<button class="btn btn-sm btn-outline" onclick="document.getElementById('resign-modal').remove()">${t('取消')}</button>
|
|
4854
|
+
<button id="resign-submit-btn" class="btn btn-sm btn-primary" style="background:#dc2626;border-color:#dc2626" disabled onclick="doResignGovernance('${role}')">🔑 ${t('Passkey 签发卸任')}</button>
|
|
4855
|
+
</div>
|
|
4856
|
+
</div>
|
|
4857
|
+
</div>`
|
|
4858
|
+
document.body.insertAdjacentHTML('beforeend', html)
|
|
4859
|
+
}
|
|
4860
|
+
|
|
4861
|
+
window.doResignGovernance = async (role) => {
|
|
4862
|
+
const expected = `RESIGN ${role}`
|
|
4863
|
+
const input = document.getElementById('resign-confirm-input')
|
|
4864
|
+
const msg = document.getElementById('resign-msg')
|
|
4865
|
+
if (!input || input.value !== expected) return
|
|
4866
|
+
|
|
4867
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请求 Passkey...')}</div>`
|
|
4868
|
+
|
|
4869
|
+
let webauthnToken
|
|
4870
|
+
try {
|
|
4871
|
+
webauthnToken = await requestPasskeyGate('governance_resign', { role, action: 'resign' })
|
|
4872
|
+
} catch (e) {
|
|
4873
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
4874
|
+
return
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4878
|
+
const res = await POST('/governance/onboarding/resign', {
|
|
4879
|
+
role,
|
|
4880
|
+
confirm_text: expected,
|
|
4881
|
+
webauthn_token: webauthnToken,
|
|
4882
|
+
})
|
|
4883
|
+
if (res.error) {
|
|
4884
|
+
if (res.code === 'ACTIVE_CASES_EXIST') {
|
|
4885
|
+
// placeholder pattern(避免 t() 拼接)
|
|
4886
|
+
const tpl = t('尚有 {n} 个未结案 dispute,请先完成裁决后再卸任')
|
|
4887
|
+
msg.innerHTML = alert$('error', tpl.replace('{n}', String(res.open_case_count)))
|
|
4888
|
+
} else {
|
|
4889
|
+
msg.innerHTML = alert$('error', res.error)
|
|
4890
|
+
}
|
|
4891
|
+
return
|
|
4892
|
+
}
|
|
4893
|
+
msg.innerHTML = `<div class="alert alert-success">${t('卸任成功。冷却期至')} ${new Date(res.cooldown_until * 1000).toLocaleDateString()}</div>`
|
|
4894
|
+
setTimeout(() => {
|
|
4895
|
+
const modal = document.getElementById('resign-modal')
|
|
4896
|
+
if (modal) modal.remove()
|
|
4897
|
+
renderGovernanceMe(document.getElementById('app'))
|
|
4898
|
+
}, 1500)
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
window.openAppealModal = (sourceAppId, role) => {
|
|
4902
|
+
const html = `
|
|
4903
|
+
<div id="appeal-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px">
|
|
4904
|
+
<div style="background:#fff;border-radius:8px;max-width:520px;width:100%;padding:20px">
|
|
4905
|
+
<h3 style="margin:0 0 12px">${t('对自动卸任提出申诉')}: ${role}</h3>
|
|
4906
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px;line-height:1.6">
|
|
4907
|
+
${t('请详细说明:为何 outlier 计数 / COI 警告 / inactive 不应导致卸任。理由至少 100 字符,maintainer 群多签审议,通过后恢复 active 状态。')}
|
|
4908
|
+
</div>
|
|
4909
|
+
<textarea id="appeal-reason-input" rows="6" placeholder="${t('详细说明你的申诉理由(≥100 字符)')}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;font-size:13px;font-family:inherit" oninput="document.getElementById('appeal-char-count').textContent = this.value.length; document.getElementById('appeal-submit-btn').disabled = this.value.length < 100"></textarea>
|
|
4910
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('字符数')}: <span id="appeal-char-count">0</span> / 100</div>
|
|
4911
|
+
<div id="appeal-msg" style="margin-top:12px"></div>
|
|
4912
|
+
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
|
|
4913
|
+
<button class="btn btn-sm btn-outline" onclick="document.getElementById('appeal-modal').remove()">${t('取消')}</button>
|
|
4914
|
+
<button id="appeal-submit-btn" class="btn btn-sm btn-primary" disabled onclick="doSubmitAppeal('${sourceAppId}')">${t('提交申诉')}</button>
|
|
4915
|
+
</div>
|
|
4916
|
+
</div>
|
|
4917
|
+
</div>`
|
|
4918
|
+
document.body.insertAdjacentHTML('beforeend', html)
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4921
|
+
window.doSubmitAppeal = async (sourceAppId) => {
|
|
4922
|
+
const input = document.getElementById('appeal-reason-input')
|
|
4923
|
+
const msg = document.getElementById('appeal-msg')
|
|
4924
|
+
if (!input || input.value.length < 100) return
|
|
4925
|
+
|
|
4926
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4927
|
+
const res = await POST('/governance/onboarding/appeal', {
|
|
4928
|
+
source_application_id: sourceAppId,
|
|
4929
|
+
appeal_reason: input.value.trim(),
|
|
4930
|
+
})
|
|
4931
|
+
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
4932
|
+
msg.innerHTML = `<div class="alert alert-success">${t('申诉已提交,等待 maintainer 裁决')}</div>`
|
|
4933
|
+
setTimeout(() => {
|
|
4934
|
+
const modal = document.getElementById('appeal-modal')
|
|
4935
|
+
if (modal) modal.remove()
|
|
4936
|
+
renderGovernanceMe(document.getElementById('app'))
|
|
4937
|
+
}, 1500)
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
// ─── /admin/governance(maintainer activation,#1093 阶段 3)─────
|
|
4941
|
+
async function renderAdminGovernance(app) {
|
|
4942
|
+
if (!state.user) { renderLogin(); return }
|
|
4943
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
4944
|
+
|
|
4945
|
+
// 阶段 4:同时拉 appeals
|
|
4946
|
+
const [data, appealsData] = await Promise.all([
|
|
4947
|
+
GET('/admin/governance/applications'),
|
|
4948
|
+
GET('/admin/governance/appeals'),
|
|
4949
|
+
])
|
|
4950
|
+
if (data.error) { app.innerHTML = shell(alert$('error', data.error), 'admin'); return }
|
|
4951
|
+
|
|
4952
|
+
const items = data.items || []
|
|
4953
|
+
const appeals = (appealsData && !appealsData.error) ? (appealsData.items || []) : []
|
|
4954
|
+
if (items.length === 0 && appeals.length === 0) {
|
|
4955
|
+
app.innerHTML = shell(`
|
|
4956
|
+
<h1 class="page-title">⚖️ ${t('治理岗位激活')}</h1>
|
|
4957
|
+
<div class="card" style="background:#f0fdf4;border-color:#86efac">
|
|
4958
|
+
<div style="font-weight:600;color:#065f46">${t('当前无待激活申请')}</div>
|
|
4959
|
+
<div style="font-size:13px;color:#047857;margin-top:8px">${t('所有 governance application 均已 active 或未达 onboarding 完成')}</div>
|
|
4960
|
+
</div>
|
|
4961
|
+
<div style="margin-top:16px"><a href="#admin" style="font-size:13px;color:#6b7280">← ${t('返回 admin')}</a></div>
|
|
4962
|
+
`, 'admin')
|
|
4963
|
+
return
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4966
|
+
const list = items.map(a => {
|
|
4967
|
+
const quizOk = a.quiz_passed_at != null
|
|
4968
|
+
const caseOk = a.has_case_review === 1 || a.has_case_review === true
|
|
4969
|
+
const onboardingComplete = quizOk && caseOk
|
|
4970
|
+
const roleLabel = a.role === 'arbitrator' ? t('仲裁员') : t('审核员')
|
|
4971
|
+
return `
|
|
4972
|
+
<div class="card" style="margin-bottom:12px;border-color:${onboardingComplete ? '#86efac' : '#fde68a'}">
|
|
4973
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px">
|
|
4974
|
+
<div style="flex:1">
|
|
4975
|
+
<div style="font-weight:600;font-size:14px">${a.role === 'arbitrator' ? '⚖' : '🛡'} ${escHtml(a.user_name || '')} <span style="color:#6b7280;font-size:12px">(${a.handle ? '@' + escHtml(a.handle) : a.user_id})</span></div>
|
|
4976
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('申请')}: ${roleLabel} · ${t('Region')}: ${escHtml(a.region || '—')} · ${t('Email')}: ${a.email ? '✓' : '✗'}</div>
|
|
4977
|
+
<div style="font-size:12px;margin-top:6px;display:flex;gap:12px">
|
|
4978
|
+
<span style="color:${quizOk ? '#16a34a' : '#dc2626'}">${quizOk ? '✓' : '✗'} ${t('题目')} ${a.quiz_score != null ? a.quiz_score + '%' : '—'}</span>
|
|
4979
|
+
<span style="color:${caseOk ? '#16a34a' : '#dc2626'}">${caseOk ? '✓' : '✗'} ${t('案例 review')}</span>
|
|
4980
|
+
</div>
|
|
4981
|
+
</div>
|
|
4982
|
+
<div style="display:flex;flex-direction:column;gap:6px;align-items:flex-end">
|
|
4983
|
+
<button class="btn btn-sm btn-outline" onclick="viewGovernanceApplication('${a.id}')">${t('查看详情')}</button>
|
|
4984
|
+
${onboardingComplete ? `<button class="btn btn-sm btn-primary" onclick="doActivateGovernance('${a.id}', '${a.user_id}')">🔑 ${t('激活')}</button>` : `<span style="font-size:11px;color:#dc2626">${t('onboarding 未完成')}</span>`}
|
|
4985
|
+
</div>
|
|
4986
|
+
</div>
|
|
4987
|
+
<div id="gov-app-detail-${a.id}" style="margin-top:10px"></div>
|
|
4988
|
+
</div>`
|
|
4989
|
+
}).join('')
|
|
4990
|
+
|
|
4991
|
+
// 阶段 4:待裁决申诉列表
|
|
4992
|
+
const appealsList = appeals.map(ap => {
|
|
4993
|
+
const roleLabel = ap.role === 'arbitrator' ? t('仲裁员') : t('审核员')
|
|
4994
|
+
const reasonShort = (ap.appeal_reason || '').slice(0, 280)
|
|
4995
|
+
return `
|
|
4996
|
+
<div class="card" style="margin-bottom:12px;border-color:#fde68a;background:#fffbeb">
|
|
4997
|
+
<div style="display:flex;justify-content:space-between;gap:10px">
|
|
4998
|
+
<div style="flex:1">
|
|
4999
|
+
<div style="font-weight:600;font-size:14px">${ap.role === 'arbitrator' ? '⚖' : '🛡'} ${escHtml(ap.user_name || '')} <span style="color:#6b7280;font-size:12px">(${ap.handle ? '@' + escHtml(ap.handle) : ap.user_id})</span></div>
|
|
5000
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('申诉')}: ${roleLabel} · ${t('提交于')} ${new Date(ap.created_at * 1000).toLocaleDateString()}${ap.auto_deactivate_at ? ' · ' + t('自动卸任于') + ' ' + new Date(ap.auto_deactivate_at * 1000).toLocaleDateString() : ''}</div>
|
|
5001
|
+
<div style="font-size:13px;color:#374151;margin-top:8px;padding:8px;background:#fff;border-radius:4px;line-height:1.6">${escHtml(reasonShort)}${(ap.appeal_reason || '').length > 280 ? '...' : ''}</div>
|
|
5002
|
+
</div>
|
|
5003
|
+
</div>
|
|
5004
|
+
<div style="margin-top:10px;display:flex;gap:6px;justify-content:flex-end">
|
|
5005
|
+
<button class="btn btn-sm btn-outline" style="color:#dc2626;border-color:#fca5a5" onclick="openResolveAppealModal('${ap.id}', 'reject')">${t('驳回')}</button>
|
|
5006
|
+
<button class="btn btn-sm btn-primary" style="background:#16a34a;border-color:#16a34a" onclick="openResolveAppealModal('${ap.id}', 'accept')">${t('接受恢复')}</button>
|
|
5007
|
+
</div>
|
|
5008
|
+
</div>`
|
|
5009
|
+
}).join('')
|
|
5010
|
+
|
|
5011
|
+
app.innerHTML = shell(`
|
|
5012
|
+
<h1 class="page-title">⚖️ ${t('治理岗位激活')}</h1>
|
|
5013
|
+
${appeals.length > 0 ? `
|
|
5014
|
+
<h2 style="font-size:15px;color:#92400e;margin:0 0 8px">${t('待裁决申诉')} (${appeals.length})</h2>
|
|
5015
|
+
<div class="card" style="margin-bottom:12px;background:#fff7ed;border-color:#fdba74">
|
|
5016
|
+
<div style="font-size:12px;color:#7c2d12">${t('spec §7.2:申诉通过 → 恢复 active + 抹除 outlier;驳回 → 公开理由(对应元规则 #1 当一切可见)')}</div>
|
|
5017
|
+
</div>
|
|
5018
|
+
${appealsList}
|
|
5019
|
+
<h2 style="font-size:15px;color:#374151;margin:24px 0 8px">${t('待激活申请')} (${items.length})</h2>
|
|
5020
|
+
` : ''}
|
|
5021
|
+
<div class="card" style="margin-bottom:16px;background:#eff6ff;border-color:#93c5fd">
|
|
5022
|
+
<div style="font-size:13px;color:#1e3a8a">${t('待激活申请')}: <strong>${items.length}</strong></div>
|
|
5023
|
+
<div style="font-size:12px;color:#1e40af;margin-top:6px">${t('激活前:代码自动 re-gate(eligibility 二次校验)+ Iron-Rule Passkey ceremony,防 maintainer 漏检')}</div>
|
|
5024
|
+
</div>
|
|
5025
|
+
${list}
|
|
5026
|
+
<div style="margin-top:16px"><a href="#admin" style="font-size:13px;color:#6b7280">← ${t('返回 admin')}</a></div>
|
|
5027
|
+
`, 'admin')
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
window.openResolveAppealModal = (appealId, decision) => {
|
|
5031
|
+
const isAccept = decision === 'accept'
|
|
5032
|
+
const html = `
|
|
5033
|
+
<div id="resolve-appeal-modal" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px">
|
|
5034
|
+
<div style="background:#fff;border-radius:8px;max-width:520px;width:100%;padding:20px">
|
|
5035
|
+
<h3 style="margin:0 0 12px;color:${isAccept ? '#16a34a' : '#dc2626'}">${isAccept ? '✅ ' + t('接受申诉(恢复 active)') : '❌ ' + t('驳回申诉')}</h3>
|
|
5036
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px;line-height:1.6">
|
|
5037
|
+
${isAccept
|
|
5038
|
+
? t('user.roles 将恢复角色,可立即继续履职(抹除 outlier 计数,审计留痕)')
|
|
5039
|
+
: t('user 维持 inactive 状态。处置理由会公开(元规则 #1 当一切可见)')}
|
|
5040
|
+
</div>
|
|
5041
|
+
<textarea id="resolve-text-input" rows="5" placeholder="${t('处置理由(≥30 字符,公开)')}" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;font-size:13px;font-family:inherit" oninput="document.getElementById('resolve-char-count').textContent = this.value.length; document.getElementById('resolve-submit-btn').disabled = this.value.length < 30"></textarea>
|
|
5042
|
+
<div style="font-size:12px;color:#6b7280;margin-top:4px">${t('字符数')}: <span id="resolve-char-count">0</span> / 30</div>
|
|
5043
|
+
<div id="resolve-msg" style="margin-top:12px"></div>
|
|
5044
|
+
<div style="margin-top:16px;display:flex;gap:8px;justify-content:flex-end">
|
|
5045
|
+
<button class="btn btn-sm btn-outline" onclick="document.getElementById('resolve-appeal-modal').remove()">${t('取消')}</button>
|
|
5046
|
+
<button id="resolve-submit-btn" class="btn btn-sm btn-primary" disabled onclick="doResolveAppeal('${appealId}', '${decision}')">🔑 ${t('Passkey 签发裁决')}</button>
|
|
5047
|
+
</div>
|
|
5048
|
+
</div>
|
|
5049
|
+
</div>`
|
|
5050
|
+
document.body.insertAdjacentHTML('beforeend', html)
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
window.doResolveAppeal = async (appealId, decision) => {
|
|
5054
|
+
const input = document.getElementById('resolve-text-input')
|
|
5055
|
+
const msg = document.getElementById('resolve-msg')
|
|
5056
|
+
if (!input || input.value.length < 30) return
|
|
5057
|
+
const resolution_text = input.value.trim()
|
|
5058
|
+
|
|
5059
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请求 Passkey...')}</div>`
|
|
5060
|
+
let webauthnToken
|
|
5061
|
+
try {
|
|
5062
|
+
webauthnToken = await requestPasskeyGate('governance_appeal_resolve', { appeal_application_id: appealId, decision })
|
|
5063
|
+
} catch (e) {
|
|
5064
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
5065
|
+
return
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
5069
|
+
const res = await POST('/admin/governance/resolve-appeal', {
|
|
5070
|
+
appeal_application_id: appealId,
|
|
5071
|
+
decision,
|
|
5072
|
+
resolution_text,
|
|
5073
|
+
webauthn_token: webauthnToken,
|
|
5074
|
+
})
|
|
5075
|
+
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
5076
|
+
msg.innerHTML = `<div class="alert alert-success">${t('裁决已记录,user 已通知')}</div>`
|
|
5077
|
+
setTimeout(() => {
|
|
5078
|
+
const modal = document.getElementById('resolve-appeal-modal')
|
|
5079
|
+
if (modal) modal.remove()
|
|
5080
|
+
renderAdminGovernance(document.getElementById('app'))
|
|
5081
|
+
}, 1200)
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
window.viewGovernanceApplication = async (appId) => {
|
|
5085
|
+
const detailEl = document.getElementById('gov-app-detail-' + appId)
|
|
5086
|
+
if (!detailEl) return
|
|
5087
|
+
detailEl.innerHTML = `<div style="font-size:12px;color:#6b7280">${t('加载中...')}</div>`
|
|
5088
|
+
const data = await GET('/admin/governance/application/' + encodeURIComponent(appId))
|
|
5089
|
+
if (data.error) { detailEl.innerHTML = alert$('error', data.error); return }
|
|
5090
|
+
|
|
5091
|
+
const a = data.application
|
|
5092
|
+
const cases = data.cases_with_expected || []
|
|
5093
|
+
const reviewObj = data.parsed_review
|
|
5094
|
+
const reviews = reviewObj?.reviews || []
|
|
5095
|
+
|
|
5096
|
+
// Map case_id → submitted review
|
|
5097
|
+
const reviewMap = new Map(reviews.map(r => [r.case_id, r]))
|
|
5098
|
+
|
|
5099
|
+
const caseRows = cases.map(c => {
|
|
5100
|
+
const submitted = reviewMap.get(c.id)
|
|
5101
|
+
const match = submitted && submitted.chosen_verdict === c.expected_verdict
|
|
5102
|
+
return `
|
|
5103
|
+
<div style="margin-bottom:8px;padding:8px;background:#f9fafb;border-radius:4px;font-size:12px">
|
|
5104
|
+
<div style="font-weight:600;color:#374151">${c.id}: ${state._lang === 'en' ? c.scenario_en : c.scenario_zh}</div>
|
|
5105
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:6px">
|
|
5106
|
+
<div>
|
|
5107
|
+
<div style="color:#6b7280">${t('申请者选')}:</div>
|
|
5108
|
+
<div style="color:${match ? '#16a34a' : '#dc2626'};font-weight:600">${submitted ? submitted.chosen_verdict : '—'}</div>
|
|
5109
|
+
</div>
|
|
5110
|
+
<div>
|
|
5111
|
+
<div style="color:#6b7280">${t('expected')}:</div>
|
|
5112
|
+
<div style="font-weight:600;color:#047857">${c.expected_verdict}</div>
|
|
5113
|
+
</div>
|
|
5114
|
+
</div>
|
|
5115
|
+
<div style="margin-top:6px">
|
|
5116
|
+
<div style="color:#6b7280">${t('reasoning')}:</div>
|
|
5117
|
+
<div style="white-space:pre-wrap;padding:6px;background:white;border-radius:4px;border:1px solid #e5e7eb">${escHtml(submitted?.reasoning || '—')}</div>
|
|
5118
|
+
</div>
|
|
5119
|
+
<div style="margin-top:6px;font-size:11px;color:#6b7280">${t('key principles')}: ${c.key_principles.join(' / ')}</div>
|
|
5120
|
+
</div>`
|
|
5121
|
+
}).join('')
|
|
5122
|
+
|
|
5123
|
+
detailEl.innerHTML = `
|
|
5124
|
+
<div style="border-top:1px solid #e5e7eb;padding-top:10px">
|
|
5125
|
+
<div style="font-weight:600;font-size:13px;margin-bottom:6px">${t('案例 review 对比')}:</div>
|
|
5126
|
+
${caseRows || '<div style="font-size:12px;color:#6b7280">' + t('无 review 数据') + '</div>'}
|
|
5127
|
+
</div>`
|
|
5128
|
+
}
|
|
5129
|
+
|
|
5130
|
+
window.doActivateGovernance = async (applicationId, targetUserId) => {
|
|
5131
|
+
if (!confirm(t('确认激活该申请?将自动 re-gate eligibility + Passkey 签发'))) return
|
|
5132
|
+
|
|
5133
|
+
let webauthnToken
|
|
5134
|
+
try {
|
|
5135
|
+
webauthnToken = await requestPasskeyGate('governance_activate', { application_id: applicationId, target_user_id: targetUserId })
|
|
5136
|
+
} catch (e) {
|
|
5137
|
+
alert(t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
5138
|
+
return
|
|
5139
|
+
}
|
|
5140
|
+
|
|
5141
|
+
const res = await POST('/admin/governance/activate', {
|
|
5142
|
+
application_id: applicationId,
|
|
5143
|
+
webauthn_token: webauthnToken,
|
|
5144
|
+
})
|
|
5145
|
+
|
|
5146
|
+
if (res.error) {
|
|
5147
|
+
if (res.missing_requirements) {
|
|
5148
|
+
alert(t('代码自动 re-gate 失败') + ': ' + res.missing_requirements.join(', '))
|
|
5149
|
+
} else {
|
|
5150
|
+
alert(res.error)
|
|
5151
|
+
}
|
|
5152
|
+
return
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
alert(t('✅ 激活成功') + ': ' + res.role)
|
|
5156
|
+
renderAdminGovernance(document.getElementById('app'))
|
|
5157
|
+
}
|
|
5158
|
+
|
|
5159
|
+
window.doSubmitCaseReview = async (role) => {
|
|
5160
|
+
const msg = document.getElementById('onb-case-msg')
|
|
5161
|
+
if (!msg) return
|
|
5162
|
+
|
|
5163
|
+
// 收集 case reviews
|
|
5164
|
+
const reviews = []
|
|
5165
|
+
const caseVerdictMap = new Map()
|
|
5166
|
+
document.querySelectorAll('[name^="case-"][type="radio"]:checked').forEach(el => {
|
|
5167
|
+
// name format: case-<id>-verdict
|
|
5168
|
+
const caseId = el.name.replace(/^case-/, '').replace(/-verdict$/, '')
|
|
5169
|
+
caseVerdictMap.set(caseId, el.value)
|
|
5170
|
+
})
|
|
5171
|
+
document.querySelectorAll('textarea.case-reasoning').forEach(ta => {
|
|
5172
|
+
const caseId = ta.dataset.caseId
|
|
5173
|
+
const verdict = caseVerdictMap.get(caseId)
|
|
5174
|
+
if (caseId && verdict) {
|
|
5175
|
+
reviews.push({ case_id: caseId, chosen_verdict: verdict, reasoning: ta.value.trim() })
|
|
5176
|
+
}
|
|
5177
|
+
})
|
|
5178
|
+
|
|
4260
5179
|
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
4261
|
-
const res = await POST('/
|
|
5180
|
+
const res = await POST('/governance/onboarding/case-review', { role, reviews })
|
|
5181
|
+
|
|
5182
|
+
if (res.error) {
|
|
5183
|
+
if (res.errors && Array.isArray(res.errors)) {
|
|
5184
|
+
const detail = res.errors.map(e => `${e.case_id}: ${e.reason}`).join('<br>')
|
|
5185
|
+
msg.innerHTML = alert$('error', t('案例 review 不完整') + ':<br>' + detail)
|
|
5186
|
+
} else {
|
|
5187
|
+
msg.innerHTML = alert$('error', res.error)
|
|
5188
|
+
}
|
|
5189
|
+
return
|
|
5190
|
+
}
|
|
5191
|
+
|
|
5192
|
+
msg.innerHTML = `
|
|
5193
|
+
<div class="alert alert-success">
|
|
5194
|
+
<div style="font-weight:600">✅ ${t('案例 review 已提交')} (${res.submitted_count} ${t('个案例')})</div>
|
|
5195
|
+
<div style="margin-top:8px;font-size:13px">${t('maintainer 上岗签字前会对比 expected verdict 评估你的 reasoning 方向')}</div>
|
|
5196
|
+
</div>`
|
|
5197
|
+
}
|
|
5198
|
+
|
|
5199
|
+
window.doSubmitOnboarding = async (role) => {
|
|
5200
|
+
const msg = document.getElementById('onb-msg')
|
|
5201
|
+
if (!msg) return
|
|
5202
|
+
|
|
5203
|
+
// 收集答案
|
|
5204
|
+
const answers = []
|
|
5205
|
+
// multi-choice
|
|
5206
|
+
const radioGroups = new Map()
|
|
5207
|
+
document.querySelectorAll('input[type="radio"][name^="mcq-"]:checked').forEach(el => {
|
|
5208
|
+
radioGroups.set(el.name, el.value)
|
|
5209
|
+
})
|
|
5210
|
+
radioGroups.forEach((answer, qid) => answers.push({ question_id: qid, answer }))
|
|
5211
|
+
// short-answer
|
|
5212
|
+
document.querySelectorAll('textarea.short-answer').forEach(ta => {
|
|
5213
|
+
answers.push({ question_id: ta.dataset.qid, answer: ta.value.trim() })
|
|
5214
|
+
})
|
|
5215
|
+
|
|
5216
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('评分中...')}</div>`
|
|
5217
|
+
const res = await POST('/governance/onboarding/quiz-submit', { role, answers })
|
|
4262
5218
|
if (res.error) { msg.innerHTML = alert$('error', res.error); return }
|
|
4263
|
-
|
|
4264
|
-
|
|
5219
|
+
|
|
5220
|
+
const detail = (res.per_question || []).map(p => `
|
|
5221
|
+
<span style="display:inline-block;margin:2px;padding:2px 6px;background:${p.ok ? '#dcfce7' : '#fee2e2'};color:${p.ok ? '#166534' : '#991b1b'};font-size:11px;border-radius:4px">${p.id} ${p.ok ? '✓' : '✗'}</span>`).join('')
|
|
5222
|
+
|
|
5223
|
+
if (res.passed) {
|
|
5224
|
+
msg.innerHTML = `
|
|
5225
|
+
<div class="alert alert-success">
|
|
5226
|
+
<div style="font-weight:600">✅ ${t('合格')}! ${t('得分')}: ${res.score_pct}% (${res.correct}/${res.total})</div>
|
|
5227
|
+
<div style="margin-top:8px;font-size:12px">${detail}</div>
|
|
5228
|
+
<div style="margin-top:10px;font-size:13px">${t('下一步:等待 maintainer 激活,本阶段未上线 admin UI')}</div>
|
|
5229
|
+
</div>`
|
|
5230
|
+
} else {
|
|
5231
|
+
msg.innerHTML = `
|
|
5232
|
+
<div class="alert alert-error">
|
|
5233
|
+
<div style="font-weight:600">⚠️ ${t('未合格')}: ${res.score_pct}% < ${res.pass_threshold}%</div>
|
|
5234
|
+
<div style="margin-top:8px;font-size:12px">${detail}</div>
|
|
5235
|
+
<div style="margin-top:10px;font-size:13px">${t('可重试提交')}</div>
|
|
5236
|
+
</div>`
|
|
5237
|
+
}
|
|
5238
|
+
}
|
|
5239
|
+
|
|
5240
|
+
window.updateGovApplyState = () => {
|
|
5241
|
+
const cb1 = document.getElementById('gov-consent-1')
|
|
5242
|
+
const cb2 = document.getElementById('gov-consent-2')
|
|
5243
|
+
const btn = document.getElementById('gov-submit-btn')
|
|
5244
|
+
if (cb1 && cb2 && btn) {
|
|
5245
|
+
const ok = cb1.checked && cb2.checked && !cb2.disabled
|
|
5246
|
+
btn.disabled = !ok
|
|
5247
|
+
btn.style.opacity = ok ? '1' : '0.5'
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
|
|
5251
|
+
window.doSubmitGovernanceApply = async (role, pageLoadedAt) => {
|
|
5252
|
+
const msg = document.getElementById('gov-apply-msg')
|
|
5253
|
+
if (!msg) return
|
|
5254
|
+
|
|
5255
|
+
// 1. 生成 consent_hash(client-side SHA-256)
|
|
5256
|
+
// PR #22 review fix P1-2:与 server 同步的 disclosure_version,server 重建 hash 校验内容(防"任意 16 字符过关")
|
|
5257
|
+
// 文本格式必须与 src/pwa/routes/governance-onboarding.ts expectedConsentHash() 一致
|
|
5258
|
+
const GOVERNANCE_APPLY_DISCLOSURE_VERSION = 'v1.0-2026-06-02'
|
|
5259
|
+
const consentText = 'governance_apply|disclosure=' + GOVERNANCE_APPLY_DISCLOSURE_VERSION + '|role=' + role + '|user=' + state.user.id + '|page_loaded_at=' + pageLoadedAt
|
|
5260
|
+
const encoder = new TextEncoder()
|
|
5261
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(consentText))
|
|
5262
|
+
const consentHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2,'0')).join('')
|
|
5263
|
+
|
|
5264
|
+
// 2. Passkey 签发(spec §3.1 Iron-Rule 真人 gate)
|
|
5265
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('请通过 Passkey / 生物识别确认...')}</div>`
|
|
5266
|
+
let passkeyToken
|
|
5267
|
+
try {
|
|
5268
|
+
passkeyToken = await requestPasskeyGate('governance_apply', { role, consent_hash: consentHash })
|
|
5269
|
+
} catch (e) {
|
|
5270
|
+
msg.innerHTML = alert$('error', t('Passkey 签发失败:') + ' ' + (e?.message || e))
|
|
5271
|
+
return
|
|
5272
|
+
}
|
|
5273
|
+
|
|
5274
|
+
// 3. POST /api/governance/onboarding/apply
|
|
5275
|
+
msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
5276
|
+
const res = await POST('/governance/onboarding/apply', {
|
|
5277
|
+
role,
|
|
5278
|
+
consent_hash: consentHash,
|
|
5279
|
+
passkey_sig: passkeyToken,
|
|
5280
|
+
iron_rule_method: 'passkey',
|
|
5281
|
+
page_loaded_at: pageLoadedAt,
|
|
5282
|
+
})
|
|
5283
|
+
|
|
5284
|
+
if (res.error) {
|
|
5285
|
+
if (res.missing_requirements && Array.isArray(res.missing_requirements)) {
|
|
5286
|
+
msg.innerHTML = alert$('error', t('门槛未达标') + ': ' + res.missing_requirements.join(', '))
|
|
5287
|
+
} else {
|
|
5288
|
+
msg.innerHTML = alert$('error', res.error)
|
|
5289
|
+
}
|
|
5290
|
+
return
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
msg.innerHTML = alert$('success', t('申请已提交,等待 maintainer review') + ' (' + (res.application_id || '') + ')')
|
|
5294
|
+
setTimeout(() => navigate('#me'), 2500)
|
|
4265
5295
|
}
|
|
4266
5296
|
|
|
4267
5297
|
window.doWithdrawVerifierApp = async () => {
|
|
@@ -5213,6 +6243,15 @@ function renderWelcome(app) {
|
|
|
5213
6243
|
<a class="w-btn-full w-btn-outline" href="https://t.me/webazer" target="_blank" rel="noopener noreferrer" style="display:flex;align-items:center;justify-content:center;text-decoration:none">${T('加入 Telegram', 'Join Telegram')}</a>
|
|
5214
6244
|
</div>
|
|
5215
6245
|
</div>
|
|
6246
|
+
<div class="w-card w-join-card">
|
|
6247
|
+
<div class="w-join-card-left">
|
|
6248
|
+
<div class="w-card-title">📧 ${T('邮件联系', 'Email us')}</div>
|
|
6249
|
+
<div class="w-card-desc">${T('合作 / 反馈 / 合规咨询', 'Partnerships / feedback / compliance')}</div>
|
|
6250
|
+
</div>
|
|
6251
|
+
<div class="w-join-card-right">
|
|
6252
|
+
<a class="w-btn-full w-btn-outline" href="mailto:contact@webaz.xyz" style="display:flex;align-items:center;justify-content:center;text-decoration:none;font-family:ui-monospace,SF Mono,Menlo,monospace;font-size:13px">contact@webaz.xyz</a>
|
|
6253
|
+
</div>
|
|
6254
|
+
</div>
|
|
5216
6255
|
<div class="w-card w-join-card">
|
|
5217
6256
|
<div class="w-join-card-left">
|
|
5218
6257
|
<div class="w-card-title">🚀 ${T('立即注册', 'Sign up now')}</div>
|
|
@@ -5233,7 +6272,7 @@ function renderWelcome(app) {
|
|
|
5233
6272
|
<a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/META-RULES-FULL.md" target="_blank" rel="noopener">${T('完整元规则', 'Full Meta-Rules')}</a>
|
|
5234
6273
|
<a href="https://github.com/seasonsagents-art/webaz" target="_blank" rel="noopener">GitHub</a>
|
|
5235
6274
|
<a href="#">${T('协议白皮书', 'Whitepaper')}</a>
|
|
5236
|
-
<a href="mailto:contact@webaz.xyz"
|
|
6275
|
+
<a href="mailto:contact@webaz.xyz">📧 contact@webaz.xyz</a>
|
|
5237
6276
|
</div>
|
|
5238
6277
|
</footer>
|
|
5239
6278
|
</div>
|
|
@@ -5306,6 +6345,132 @@ function renderWelcome(app) {
|
|
|
5306
6345
|
})
|
|
5307
6346
|
} catch { /* never break the page */ }
|
|
5308
6347
|
}
|
|
6348
|
+
|
|
6349
|
+
// 2026-06-02 W3.5-B:/governance-onboarding 公开页(无 auth)
|
|
6350
|
+
// 引导潜在 arbitrator/verifier 申请人;phase A 无报酬声明;现有岗位人数 stat
|
|
6351
|
+
async function renderGovernanceOnboarding(app) {
|
|
6352
|
+
const en = window._lang === 'en'
|
|
6353
|
+
const T = (zh, e) => en && e ? e : zh
|
|
6354
|
+
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
6355
|
+
app.innerHTML = `
|
|
6356
|
+
${preLaunchBannerHTML()}
|
|
6357
|
+
<div 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">
|
|
6358
|
+
<header style="text-align:center;margin-bottom:40px">
|
|
6359
|
+
<h1 style="font-size:clamp(28px,5vw,36px);margin:0 0 12px;color:#18181B">⚖️ ${T('治理上岗', 'Governance Onboarding')}</h1>
|
|
6360
|
+
<p style="color:#52525B;font-size:16px;margin:0">${T('phase A:公共贡献,无报酬', 'Phase A: public contribution, no compensation')}</p>
|
|
6361
|
+
</header>
|
|
6362
|
+
|
|
6363
|
+
<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:24px;margin-bottom:24px">
|
|
6364
|
+
<h2 style="font-size:22px;margin:0 0 16px;color:#18181B">📊 ${T('当前在岗', 'Currently active')}</h2>
|
|
6365
|
+
<div id="gov-stats" style="min-height:80px">
|
|
6366
|
+
<div style="color:#a1a1aa;font-size:14px;text-align:center;padding:20px">${T('加载中…', 'Loading…')}</div>
|
|
6367
|
+
</div>
|
|
6368
|
+
</section>
|
|
6369
|
+
|
|
6370
|
+
<section style="background:#fee2e2;border:1px solid #ef4444;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
6371
|
+
<h3 style="margin:0 0 12px;color:#991b1b;font-size:18px">⚠️ ${T('重要:这不是赚钱机会', 'Important: this is NOT an income opportunity')}</h3>
|
|
6372
|
+
<ul style="margin:0;padding-left:20px;color:#7f1d1d;line-height:1.8;font-size:14px">
|
|
6373
|
+
<li>${T('phase A 不发任何现金 / WAZ 报酬', 'No cash / WAZ compensation in phase A')}</li>
|
|
6374
|
+
<li>${T('治理是公共贡献,非雇佣关系', 'Governance is public contribution, not employment')}</li>
|
|
6375
|
+
<li>${T('履职记录关系层永久保留,估值层 phase D DAO 决定', 'Performance records kept in relationship layer (permanent), valuation by phase D DAO')}</li>
|
|
6376
|
+
<li>${T('误以为 income 来源会失望 — 请先确认意愿', 'Mistaking it as income source will disappoint — confirm intent first')}</li>
|
|
6377
|
+
</ul>
|
|
6378
|
+
</section>
|
|
6379
|
+
|
|
6380
|
+
<section style="background:#fff;border:1px solid #e4e4e7;border-radius:12px;padding:24px;margin-bottom:24px">
|
|
6381
|
+
<h3 style="margin:0 0 16px;color:#18181B;font-size:18px">📋 ${T('资格门槛', 'Eligibility')}</h3>
|
|
6382
|
+
<div id="gov-thresholds" style="color:#52525B;font-size:14px;line-height:1.8">
|
|
6383
|
+
<div style="color:#a1a1aa;text-align:center;padding:12px">${T('加载阈值…', 'Loading thresholds…')}</div>
|
|
6384
|
+
</div>
|
|
6385
|
+
</section>
|
|
6386
|
+
|
|
6387
|
+
<section style="background:#dbeafe;border:1px solid #3b82f6;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
6388
|
+
<h3 style="margin:0 0 12px;color:#1e40af;font-size:18px">🛡️ ${T('履职范围', 'Duties')}</h3>
|
|
6389
|
+
<ul style="margin:0;padding-left:20px;color:#1e3a8a;line-height:1.8;font-size:14px">
|
|
6390
|
+
<li><strong>arbitrator</strong> — ${T('仲裁纠纷 dispute(Iron-Rule 真人 Passkey 必备)', 'Arbitrate disputes (Iron-Rule requires real human Passkey)')}</li>
|
|
6391
|
+
<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>
|
|
6393
|
+
</ul>
|
|
6394
|
+
</section>
|
|
6395
|
+
|
|
6396
|
+
<section style="background:#fef3c7;border:1px solid #f59e0b;border-radius:12px;padding:20px;margin-bottom:24px">
|
|
6397
|
+
<h3 style="margin:0 0 12px;color:#92400e;font-size:18px">🚀 ${T('如何申请', 'How to apply')}</h3>
|
|
6398
|
+
<ol style="margin:0;padding-left:20px;color:#78350f;line-height:1.8;font-size:14px">
|
|
6399
|
+
<li>${T('登录(若未登录)→ 进入 #me', 'Log in (if needed) → go to #me')}</li>
|
|
6400
|
+
<li>${T('达成资格门槛(Passkey + 30 天 + 5 笔订单 + reputation)', 'Meet thresholds (Passkey + 30d + 5 orders + reputation)')}</li>
|
|
6401
|
+
<li>${T('点 [申请审核员] 入口(verifier)', 'Click [Apply to be verifier] entry')}</li>
|
|
6402
|
+
<li>${T('阅读披露 + 双勾选 + 8s 反诱导延迟 + Passkey 签发', 'Read disclosure + dual checkbox + 8s anti-railroad delay + Passkey sign')}</li>
|
|
6403
|
+
<li>${T('完成 onboarding(学习包 + 案例 + 80% 测试)', 'Complete onboarding (study pack + cases + 80% quiz)')}</li>
|
|
6404
|
+
<li>${T('maintainer Iron-Rule 签发激活', 'Maintainer Iron-Rule signing activation')}</li>
|
|
6405
|
+
</ol>
|
|
6406
|
+
</section>
|
|
6407
|
+
|
|
6408
|
+
<section style="border-top:1px solid #e4e4e7;padding-top:20px;color:#71717a;font-size:13px;line-height:1.7">
|
|
6409
|
+
<p style="margin:0 0 8px"><strong>${T('完整规范', 'Full spec')}:</strong></p>
|
|
6410
|
+
<ul style="margin:0;padding-left:20px">
|
|
6411
|
+
<li><a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md" target="_blank" rel="noopener" style="color:#1d4ed8">GOVERNANCE-ONBOARDING.md</a> — ${T('资格 / 流程 / 卸任 / 申诉', 'eligibility / flow / resignation / appeal')}</li>
|
|
6412
|
+
<li><a href="https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md" target="_blank" rel="noopener" style="color:#1d4ed8">ARBITRATION-PLAYBOOK.md</a> — ${T('arbitrator 决策框架 + 5 模拟案例', 'arbitrator decision framework + 5 simulated cases')}</li>
|
|
6413
|
+
<li>${T('机读 JSON 端点', 'Machine-readable JSON endpoint')}: <code style="background:#f4f4f5;padding:2px 6px;border-radius:4px">/api/governance/onboarding-stats</code></li>
|
|
6414
|
+
</ul>
|
|
6415
|
+
</section>
|
|
6416
|
+
</div>
|
|
6417
|
+
`
|
|
6418
|
+
|
|
6419
|
+
try {
|
|
6420
|
+
const r = await fetch('/api/governance/onboarding-stats', { signal: AbortSignal.timeout(10000) })
|
|
6421
|
+
const j = await r.json()
|
|
6422
|
+
const stats = document.getElementById('gov-stats')
|
|
6423
|
+
const thr = document.getElementById('gov-thresholds')
|
|
6424
|
+
stats.innerHTML = `
|
|
6425
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px">
|
|
6426
|
+
<div style="text-align:center;padding:16px;background:#f4f4f5;border-radius:8px">
|
|
6427
|
+
<div style="font-size:32px;font-weight:700;color:#18181B">${j.active_arbitrators || 0}</div>
|
|
6428
|
+
<div style="font-size:13px;color:#71717a;margin-top:4px">${T('在岗 arbitrator', 'Active arbitrators')}</div>
|
|
6429
|
+
</div>
|
|
6430
|
+
<div style="text-align:center;padding:16px;background:#f4f4f5;border-radius:8px">
|
|
6431
|
+
<div style="font-size:32px;font-weight:700;color:#18181B">${j.active_verifiers || 0}</div>
|
|
6432
|
+
<div style="font-size:13px;color:#71717a;margin-top:4px">${T('在岗 verifier', 'Active verifiers')}</div>
|
|
6433
|
+
</div>
|
|
6434
|
+
<div style="text-align:center;padding:16px;background:#f4f4f5;border-radius:8px">
|
|
6435
|
+
<div style="font-size:32px;font-weight:700;color:#18181B">${j.pending_applications || 0}</div>
|
|
6436
|
+
<div style="font-size:13px;color:#71717a;margin-top:4px">${T('待审申请', 'Pending applications')}</div>
|
|
6437
|
+
</div>
|
|
6438
|
+
</div>
|
|
6439
|
+
<p style="text-align:center;color:#a1a1aa;font-size:12px;margin:12px 0 0">${T('phase A 暂多为 fixture 数据;launch 后接受真实申请', 'Phase A counts are mostly fixtures; real applications post-launch')}</p>
|
|
6440
|
+
`
|
|
6441
|
+
// 真实 enforced 门槛(role-split),来自 onboarding-stats.eligibility — 与 server 代码同步
|
|
6442
|
+
const el = j.eligibility || {}
|
|
6443
|
+
const arb = el.arbitrator || {}
|
|
6444
|
+
const ver = el.verifier || {}
|
|
6445
|
+
const yes = T('需', 'required')
|
|
6446
|
+
thr.innerHTML = `
|
|
6447
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
|
|
6448
|
+
<div style="background:#f4f4f5;border-radius:8px;padding:12px">
|
|
6449
|
+
<div style="font-weight:700;margin-bottom:6px">⚖️ Arbitrator</div>
|
|
6450
|
+
<div style="font-size:13px;line-height:1.9;color:#3f3f46">
|
|
6451
|
+
📅 ${T('注册', 'Registered')} ≥ <strong>${esc(arb.registration_days ?? '?')}</strong> ${T('天', 'd')}<br>
|
|
6452
|
+
📦 ${T('完成订单', 'Completed orders')} ≥ <strong>${esc(arb.completed_orders ?? '?')}</strong><br>
|
|
6453
|
+
⭐ ${T('信誉', 'Reputation')} ≥ <strong>${esc(arb.reputation ?? '?')}</strong><br>
|
|
6454
|
+
💰 ${T('钱包余额', 'Balance')} ≥ <strong>${esc(arb.balance_waz ?? '?')}</strong> WAZ<br>
|
|
6455
|
+
✉️ ${T('邮箱验证', 'Email verified')} · 🚫 ${T('零仲裁判输', 'Zero disputes lost')} · ✅ ${T('未曾暂停', 'Never suspended')}
|
|
6456
|
+
</div>
|
|
6457
|
+
</div>
|
|
6458
|
+
<div style="background:#f4f4f5;border-radius:8px;padding:12px">
|
|
6459
|
+
<div style="font-weight:700;margin-bottom:6px">🔍 Verifier</div>
|
|
6460
|
+
<div style="font-size:13px;line-height:1.9;color:#3f3f46">
|
|
6461
|
+
📅 ${T('注册', 'Registered')} ≥ <strong>${esc(ver.registration_days ?? '?')}</strong> ${T('天', 'd')}<br>
|
|
6462
|
+
📦 ${T('完成订单', 'Completed orders')} ≥ <strong>${esc(ver.completed_orders ?? '?')}</strong><br>
|
|
6463
|
+
✉️ ${T('邮箱验证', 'Email verified')} · 🚫 ${T('零仲裁判输', 'Zero disputes lost')} · ✅ ${T('未曾暂停', 'Never suspended')}
|
|
6464
|
+
</div>
|
|
6465
|
+
</div>
|
|
6466
|
+
</div>
|
|
6467
|
+
<div style="margin-top:10px;font-size:13px;color:#3f3f46">📝 ${T('两岗均需 onboarding 测试合格分', 'Both roles: onboarding quiz pass score')} ≥ <strong>${esc(j.quiz_pass_score ?? 80)}%</strong></div>
|
|
6468
|
+
`
|
|
6469
|
+
} catch (e) {
|
|
6470
|
+
document.getElementById('gov-stats').innerHTML = `<div style="color:#dc2626;text-align:center;padding:20px;font-size:14px">${T('加载失败', 'Load failed')}: ${esc(e.message || 'unknown')}</div>`
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
|
|
5309
6474
|
function renderRule(num, text) {
|
|
5310
6475
|
const [zh, en] = text.includes(' / ') ? text.split(' / ') : [text, '']
|
|
5311
6476
|
return `<div class="w-rule w-rule-item">
|
|
@@ -5549,11 +6714,11 @@ async function renderPromoter(app) {
|
|
|
5549
6714
|
const pending = atomic.score?.pending_score || 0
|
|
5550
6715
|
const _mlmMax = Number(state.user?.region_max_levels ?? 1)
|
|
5551
6716
|
const _kpiRestricted = _mlmMax <= 1
|
|
5552
|
-
// 两个语义独立的 gate(三级奖励 ≠ PV
|
|
6717
|
+
// 两个语义独立的 gate(三级奖励 ≠ PV 双轨系统)—— 2026-06-04 已解耦:
|
|
5553
6718
|
// - _kpiRestricted (max ≤ 1):紧张地区 — KPI 改时间线 / team 隐 WAZ
|
|
5554
|
-
// - _pvAllowed
|
|
5555
|
-
//
|
|
5556
|
-
const _pvAllowed =
|
|
6719
|
+
// - _pvAllowed:PV 双轨/对碰系统是否开启,读 region_pv_enabled(独立旋钮,不再绑 max≥3)
|
|
6720
|
+
// commission 层级(max_levels) 与 PV 系统(pv_enabled) 分离:可单独开某辖区到 L2/L3 而 PV 仍关。
|
|
6721
|
+
const _pvAllowed = Number(state.user?.region_pv_enabled ?? 0) === 1
|
|
5557
6722
|
// 紧张地区:拼装最近奖励时间线(commission + 对碰 binary 混合,按时间倒序取 5 条)
|
|
5558
6723
|
let kpiBar = ''
|
|
5559
6724
|
if (_kpiRestricted) {
|
|
@@ -5892,8 +7057,8 @@ async function renderPromoter(app) {
|
|
|
5892
7057
|
// 最右侧地区显示(所有模式都加)
|
|
5893
7058
|
const _userRegion = state.user?.region || 'global'
|
|
5894
7059
|
const _regionChip = `<span style="font-size:11px;color:#6b7280;white-space:nowrap;font-weight:400">${regionLabel(_userRegion)}</span>`
|
|
5895
|
-
// PV 双轨系统:只在 _pvAllowed
|
|
5896
|
-
//
|
|
7060
|
+
// PV 双轨系统:只在 _pvAllowed(region_pv_enabled=1,2026-06-04 解耦,独立于 max_levels)才显示完整 tier/对碰/弱腿;
|
|
7061
|
+
// 未开启 PV 的地区 → 走精简卡(分数仍后台计算累积,待 pv_enabled 开启/迁移后兑现)
|
|
5897
7062
|
const atomicSection = atomic.left_invite_url
|
|
5898
7063
|
? (!_pvAllowed
|
|
5899
7064
|
? `<div style="margin-bottom:12px;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:14px">
|
|
@@ -8518,6 +9683,128 @@ async function renderFeedback(app) {
|
|
|
8518
9683
|
`, 'me')
|
|
8519
9684
|
}
|
|
8520
9685
|
|
|
9686
|
+
// ─── RFC-004 build feedback(用→建):浮动入口弹窗 + 闭环视图 ───────────
|
|
9687
|
+
window.openBuildFeedback = () => {
|
|
9688
|
+
if (!state.user) { toast$(t('请先登录')); navigate('#login'); return }
|
|
9689
|
+
const page = (location.hash || '#/').split('?')[0] // 非 PII:只取当前页面路由作上下文
|
|
9690
|
+
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()">
|
|
9692
|
+
<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
|
+
<h2 style="font-size:16px;font-weight:700;margin-bottom:4px">💬 ${t('反馈 / 建议')}</h2>
|
|
9694
|
+
<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>
|
|
9695
|
+
<div class="form-group">
|
|
9696
|
+
<label class="form-label">${t('类型')}</label>
|
|
9697
|
+
<select class="form-control" id="bfb-type">
|
|
9698
|
+
<option value="ux_issue">${t('体验问题(用着别扭)')}</option>
|
|
9699
|
+
<option value="bug">${t('Bug(报错 / 坏了)')}</option>
|
|
9700
|
+
<option value="proposal">${t('改进提案(建设 · 需绑 Passkey,被采纳记共建信誉)')}</option>
|
|
9701
|
+
</select>
|
|
9702
|
+
</div>
|
|
9703
|
+
<div class="form-group">
|
|
9704
|
+
<label class="form-label">${t('描述')} *</label>
|
|
9705
|
+
<textarea class="form-control" id="bfb-text" rows="5" maxlength="2000" placeholder="${t('发生了什么 / 你的想法(≥5 字)')}"></textarea>
|
|
9706
|
+
</div>
|
|
9707
|
+
<div id="bfb-msg" style="margin:8px 0"></div>
|
|
9708
|
+
<div style="display:flex;gap:8px">
|
|
9709
|
+
<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>
|
|
9711
|
+
</div>
|
|
9712
|
+
<div style="text-align:center;margin-top:12px">
|
|
9713
|
+
<a onclick="this.closest('.js-modal').remove();navigate('#build-feedback')" style="font-size:12px;color:#4f46e5;cursor:pointer">${t('查看我的反馈进度 →')}</a>
|
|
9714
|
+
</div>
|
|
9715
|
+
</div>
|
|
9716
|
+
</div>`
|
|
9717
|
+
const div = document.createElement('div'); div.innerHTML = html; document.body.appendChild(div.firstElementChild)
|
|
9718
|
+
}
|
|
9719
|
+
|
|
9720
|
+
window.submitBuildFeedback = async (page) => {
|
|
9721
|
+
const type = document.getElementById('bfb-type').value
|
|
9722
|
+
const text = document.getElementById('bfb-text').value.trim()
|
|
9723
|
+
const msg = document.getElementById('bfb-msg')
|
|
9724
|
+
if (text.length < 5) { msg.innerHTML = alert$('error', t('请至少写 5 个字')); return }
|
|
9725
|
+
msg.innerHTML = loading$()
|
|
9726
|
+
const res = await POST('/build-feedback', { type, text, area: page, scene: [{ page }] })
|
|
9727
|
+
if (res.error) { msg.innerHTML = alert$('error', res.error); return } // 如 proposal 无 Passkey → 后端返回引导
|
|
9728
|
+
document.querySelector('.js-modal')?.remove()
|
|
9729
|
+
toast$(res.status === 'duplicate' ? t('已有类似建议,已合并 🙌') : t('反馈已提交,谢谢! 🙏'))
|
|
9730
|
+
}
|
|
9731
|
+
|
|
9732
|
+
async function renderMyBuildFeedback(app) {
|
|
9733
|
+
if (!state.user) { renderLogin(); return }
|
|
9734
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
9735
|
+
const r = await GET('/build-feedback/mine')
|
|
9736
|
+
const list = r?.feedback || []
|
|
9737
|
+
const SL = () => ({ received: t('已收到'), triaged: t('已分类'), in_progress: t('处理中'), resolved: t('已采纳 ✅'), declined: t('未采纳'), duplicate: t('已合并') })
|
|
9738
|
+
const SC = { received: '#d97706', triaged: '#4f46e5', in_progress: '#4f46e5', resolved: '#16a34a', declined: '#6b7280', duplicate: '#6b7280' }
|
|
9739
|
+
const TL = () => ({ ux_issue: t('体验问题'), bug: 'Bug', proposal: t('提案') })
|
|
9740
|
+
const rows = list.length === 0
|
|
9741
|
+
? `<div style="text-align:center;padding:40px;color:#9ca3af"><div style="font-size:48px;margin-bottom:8px">💬</div><div style="font-size:13px">${t('还没有反馈 — 用着有问题随时点左下角 💬')}</div></div>`
|
|
9742
|
+
: list.map(f => `
|
|
9743
|
+
<div class="card" style="padding:12px;margin-bottom:8px">
|
|
9744
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px">
|
|
9745
|
+
<span style="font-size:11px;color:#6b7280">${TL()[f.type] || f.type}${f.area ? ` · <code style="background:#f3f4f6;padding:1px 4px;border-radius:4px">${escHtml(String(f.area))}</code>` : ''}</span>
|
|
9746
|
+
<span style="font-size:10px;font-weight:600;color:#fff;background:${SC[f.status] || '#6b7280'};padding:2px 8px;border-radius:99px;white-space:nowrap">${SL()[f.status] || f.status}</span>
|
|
9747
|
+
</div>
|
|
9748
|
+
<div style="font-size:13px;color:#374151">${escHtml(String(f.body || ''))}</div>
|
|
9749
|
+
${f.resolution ? `<div style="font-size:11px;color:#6b7280;margin-top:6px;padding-top:6px;border-top:1px solid #f3f4f6">↳ ${escHtml(String(f.resolution))}</div>` : ''}
|
|
9750
|
+
${Number(f.credited_points) > 0 ? `<div style="font-size:11px;color:#16a34a;margin-top:4px">🏅 +${f.credited_points} ${t('共建信誉')}</div>` : ''}
|
|
9751
|
+
</div>`).join('')
|
|
9752
|
+
app.innerHTML = shell(`
|
|
9753
|
+
<h1 class="page-title">💬 ${t('我的反馈')}</h1>
|
|
9754
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:14px">${t('你提的问题 / 建议进展 — 被采纳的提案会记入共建信誉')}</div>
|
|
9755
|
+
<button class="btn btn-primary btn-sm" style="margin-bottom:12px" onclick="openBuildFeedback()">+ ${t('新反馈 / 建议')}</button>
|
|
9756
|
+
${rows}
|
|
9757
|
+
`, 'me')
|
|
9758
|
+
}
|
|
9759
|
+
|
|
9760
|
+
// RFC-006 Gap 2:贡献者【自查】看板 — KPI / 等级 / 限制 + 申诉。仅自查(私密),建设信誉独立池不解锁交易角色。
|
|
9761
|
+
async function renderMyContributions(app) {
|
|
9762
|
+
if (!state.user) { renderLogin(); return }
|
|
9763
|
+
app.innerHTML = shell(loading$(), 'me')
|
|
9764
|
+
const p = await GET('/build-reputation/me')
|
|
9765
|
+
if (!p || p.error) { app.innerHTML = shell(`<div class="card" style="padding:16px;color:#b91c1c">${escHtml(p?.error || t('加载失败'))}</div>`, 'me'); return }
|
|
9766
|
+
const lang = window._lang === 'zh' ? 'zh' : 'en'
|
|
9767
|
+
const tier = p.tier || {}
|
|
9768
|
+
const k = p.kpi || {}
|
|
9769
|
+
const stat = (label, val) => `<div class="card" style="padding:10px;text-align:center"><div style="font-size:20px;font-weight:700;color:#4f46e5">${val ?? 0}</div><div style="font-size:11px;color:#6b7280">${label}</div></div>`
|
|
9770
|
+
const provRows = (p.provenance || []).map(x => {
|
|
9771
|
+
const m = { human: t('真人'), ai_assisted: t('AI 辅助'), ai_authored: t('AI 主笔'), unspecified: t('未声明') }
|
|
9772
|
+
return `<span style="font-size:11px;color:#6b7280;margin-right:10px">${m[x.provenance] || x.provenance}: <b>${x.count}</b></span>`
|
|
9773
|
+
}).join('')
|
|
9774
|
+
const restr = (p.restrictions || [])
|
|
9775
|
+
const restrHtml = restr.length === 0
|
|
9776
|
+
? `<div style="font-size:12px;color:#16a34a">✓ ${t('无限制')}</div>`
|
|
9777
|
+
: restr.map(r => `<div class="card" style="padding:10px;margin-bottom:6px;border-left:3px solid #dc2626">
|
|
9778
|
+
<div style="font-size:12px;color:#dc2626;font-weight:600">${escHtml(String(r.reason_code || ''))} · ${escHtml(String(r.severity || ''))}</div>
|
|
9779
|
+
<div style="font-size:12px;color:#374151">${escHtml(String(r.reason_detail || ''))}</div>
|
|
9780
|
+
<div style="font-size:11px;color:#6b7280;margin-top:4px">${t('如有异议可申诉')} → <code>/api/me/agents/strikes/${escHtml(String(r.id))}/appeal</code></div>
|
|
9781
|
+
</div>`).join('')
|
|
9782
|
+
app.innerHTML = shell(`
|
|
9783
|
+
<h1 class="page-title">🛠️ ${t('我的共建')}</h1>
|
|
9784
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:14px">${t('你的建设贡献(独立于交易信誉 — 不影响 verifier/仲裁准入)')}</div>
|
|
9785
|
+
<div class="card" style="padding:14px;margin-bottom:12px;background:linear-gradient(135deg,#eef2ff,#faf5ff)">
|
|
9786
|
+
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
9787
|
+
<div><div style="font-size:11px;color:#6b7280">${t('建设等级')}</div><div style="font-size:18px;font-weight:700">${lang === 'zh' ? (tier.label_zh || '') : (tier.label_en || '')}</div></div>
|
|
9788
|
+
<div style="text-align:right"><div style="font-size:11px;color:#6b7280">${t('建设积分')}</div><div style="font-size:18px;font-weight:700;color:#4f46e5">${p.build_points ?? 0}</div></div>
|
|
9789
|
+
</div>
|
|
9790
|
+
<div style="font-size:11px;color:#6b7280;margin-top:8px">${t('当前可参与')}:${lang === 'zh' ? (tier.caps_zh || '') : (tier.caps_en || '')}${tier.next_at ? ` · ${t('下一级')} @ ${tier.next_at}` : ''}</div>
|
|
9791
|
+
</div>
|
|
9792
|
+
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:12px">
|
|
9793
|
+
${stat(t('认领中'), k.tasks_claimed)}
|
|
9794
|
+
${stat(t('审核中'), k.tasks_in_review)}
|
|
9795
|
+
${stat(t('已验收'), k.tasks_done)}
|
|
9796
|
+
${stat(t('提反馈'), k.feedback_submitted)}
|
|
9797
|
+
${stat(t('被采纳'), k.feedback_accepted)}
|
|
9798
|
+
${stat(t('提任务'), k.tasks_created)}
|
|
9799
|
+
</div>
|
|
9800
|
+
${provRows ? `<div class="card" style="padding:10px;margin-bottom:12px"><div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('署名构成(自报)')}</div>${provRows}</div>` : ''}
|
|
9801
|
+
${!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>` : ''}
|
|
9802
|
+
<div style="font-size:13px;font-weight:600;margin:14px 0 6px">${t('限制与申诉')}</div>
|
|
9803
|
+
${restrHtml}
|
|
9804
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:14px">${t('看板仅自己可见,不做公开排行。')}</div>
|
|
9805
|
+
`, 'me')
|
|
9806
|
+
}
|
|
9807
|
+
|
|
8521
9808
|
// W7 客服 ticket-thread 视图
|
|
8522
9809
|
const TICKET_TYPE_META = {
|
|
8523
9810
|
created: { icon: '🛟', title: '新建工单', border: '#d97706' },
|
|
@@ -9572,6 +10859,11 @@ window.sharePromoLink = async (productId, title) => {
|
|
|
9572
10859
|
// 商品级 verified 校验 + 拿/建 shareable,走 /s/<id> 短链
|
|
9573
10860
|
const res = await POST(`/products/${productId}/get-or-create-share`)
|
|
9574
10861
|
if (res.error) {
|
|
10862
|
+
if (res.error === 'rewards_opt_in_required') {
|
|
10863
|
+
const msg = (window._lang === 'en' ? res.message_en : res.message_zh) || res.message_zh || res.error
|
|
10864
|
+
const missing = Array.isArray(res.missing_requirements) ? `\n\n${t('待补')}: ${res.missing_requirements.join(', ')}` : ''
|
|
10865
|
+
return alert(`⚠ ${msg}${missing}\n\n${t('前往 #me 申请共建身份')}`)
|
|
10866
|
+
}
|
|
9575
10867
|
return alert(`⚠ ${res.error}${res.completed_orders === 0 ? '\n' + t('完成该商品的购买后再分享') : ''}`)
|
|
9576
10868
|
}
|
|
9577
10869
|
const link = `${location.origin}${res.short_url}`
|
|
@@ -19303,6 +20595,45 @@ function buildDisputeHtml(dispute, user) {
|
|
|
19303
20595
|
<button class="btn btn-primary btn-sm" style="width:auto" onclick="handleDisputeRespond('${dispute.id}','${dispute.order_id}')">提交反驳证据</button>
|
|
19304
20596
|
</div>` : ''
|
|
19305
20597
|
|
|
20598
|
+
// ── 仲裁员:暂停 / 恢复自动判定时钟(playbook §2.1)────
|
|
20599
|
+
// 补证据期 > 48h 时必须显式暂停,避免被协议层 48h 沉默判架空
|
|
20600
|
+
const isPaused = dispute.auto_judge_paused_until && dispute.auto_judge_paused_until * 1000 > Date.now()
|
|
20601
|
+
const pauseSection = isArbitrator && dispute.status !== 'resolved' && !dispute.ruling_type ? (
|
|
20602
|
+
isPaused ? `
|
|
20603
|
+
<div style="margin-top:12px;background:#fef3c7;border:1px solid #fcd34d;border-radius:8px;padding:12px">
|
|
20604
|
+
<div style="font-size:13px;font-weight:600;color:#92400e;margin-bottom:6px">⏸️ ${t('自动判定时钟已冻结')}</div>
|
|
20605
|
+
<div style="font-size:12px;color:#78350f;line-height:1.6;margin-bottom:8px">
|
|
20606
|
+
${t('到期')}: ${new Date(dispute.auto_judge_paused_until * 1000).toLocaleString()}<br>
|
|
20607
|
+
${t('理由')}: ${escHtml(dispute.auto_judge_pause_reason || '—')}
|
|
20608
|
+
</div>
|
|
20609
|
+
<button class="btn btn-sm btn-outline" style="font-size:12px" onclick="handleArbitratorResume('${dispute.id}')">▶️ ${t('立即解冻(直接进入裁决/补证后)')}</button>
|
|
20610
|
+
</div>
|
|
20611
|
+
` : `
|
|
20612
|
+
<div style="margin-top:12px">
|
|
20613
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:12px" onclick="(function(){var s=document.getElementById('pause-section-${dispute.id}');s.style.display=s.style.display==='none'?'':'none'})()">⏸️ ${t('暂停自动判定时钟')}</button>
|
|
20614
|
+
<div id="pause-section-${dispute.id}" style="display:none;margin-top:10px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;padding:12px">
|
|
20615
|
+
<div style="font-size:12px;color:#92400e;line-height:1.6;margin-bottom:8px">${t('playbook §2.1:补证据期 > 48h 时显式调用,冻结协议自动判定时钟。最大窗口 7 天。')}</div>
|
|
20616
|
+
<div id="pause-msg-${dispute.id}"></div>
|
|
20617
|
+
<div style="margin-bottom:8px">
|
|
20618
|
+
<label style="font-size:12px;color:#6b7280;display:block;margin-bottom:4px">${t('暂停理由(≥10 字符,公开 audit_log)')}</label>
|
|
20619
|
+
<textarea class="form-control" id="pause-reason-${dispute.id}" rows="3" placeholder="${t('例:已通知 buyer 补开箱视频,等 72h')}" style="font-size:13px"></textarea>
|
|
20620
|
+
</div>
|
|
20621
|
+
<div style="margin-bottom:10px">
|
|
20622
|
+
<label style="font-size:12px;color:#6b7280;display:block;margin-bottom:4px">${t('冻结到期')}</label>
|
|
20623
|
+
<select class="form-control" id="pause-hours-${dispute.id}" style="font-size:13px">
|
|
20624
|
+
<option value="24">24h</option>
|
|
20625
|
+
<option value="48" selected>48h</option>
|
|
20626
|
+
<option value="72">72h</option>
|
|
20627
|
+
<option value="120">5d</option>
|
|
20628
|
+
<option value="168">7d ${t('(最大)')}</option>
|
|
20629
|
+
</select>
|
|
20630
|
+
</div>
|
|
20631
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="handleArbitratorPause('${dispute.id}')">⏸️ ${t('暂停时钟')}</button>
|
|
20632
|
+
</div>
|
|
20633
|
+
</div>
|
|
20634
|
+
`
|
|
20635
|
+
) : ''
|
|
20636
|
+
|
|
19306
20637
|
// ── 仲裁员:发起补充证据请求 ──────────────────
|
|
19307
20638
|
const requestEvidenceSection = isArbitrator && dispute.status !== 'resolved' ? `
|
|
19308
20639
|
<div style="margin-top:12px">
|
|
@@ -19435,6 +20766,7 @@ function buildDisputeHtml(dispute, user) {
|
|
|
19435
20766
|
${respondSection}
|
|
19436
20767
|
${partyAddEvidenceSection}
|
|
19437
20768
|
${uploadBlobSection}
|
|
20769
|
+
${pauseSection}
|
|
19438
20770
|
${requestEvidenceSection}
|
|
19439
20771
|
${arbitrateSection}
|
|
19440
20772
|
|
|
@@ -19707,6 +21039,30 @@ window.handleRequestEvidence = async (disputeId) => {
|
|
|
19707
21039
|
setTimeout(() => renderDisputeDetail(document.getElementById('app'), disputeId), 1200)
|
|
19708
21040
|
}
|
|
19709
21041
|
|
|
21042
|
+
// task #1093 stage 6: arbitrator pause / resume auto-judge clock (playbook §2.1)
|
|
21043
|
+
window.handleArbitratorPause = async (disputeId) => {
|
|
21044
|
+
const reason = document.getElementById('pause-reason-' + disputeId)?.value?.trim()
|
|
21045
|
+
const hours = Number(document.getElementById('pause-hours-' + disputeId)?.value) || 48
|
|
21046
|
+
const msgEl = document.getElementById('pause-msg-' + disputeId)
|
|
21047
|
+
if (!reason || reason.length < 10) {
|
|
21048
|
+
msgEl.innerHTML = alert$('error', t('暂停理由至少 10 字符'))
|
|
21049
|
+
return
|
|
21050
|
+
}
|
|
21051
|
+
const untilTs = Math.floor(Date.now() / 1000) + hours * 3600
|
|
21052
|
+
msgEl.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('提交中...')}</div>`
|
|
21053
|
+
const res = await POST(`/disputes/${disputeId}/arbitrator-pause-auto-judge`, { reason, until_ts: untilTs })
|
|
21054
|
+
if (res.error) { msgEl.innerHTML = alert$('error', res.error); return }
|
|
21055
|
+
msgEl.innerHTML = alert$('success', t('自动判定时钟已冻结'))
|
|
21056
|
+
setTimeout(() => renderDisputeDetail(document.getElementById('app'), disputeId), 1200)
|
|
21057
|
+
}
|
|
21058
|
+
|
|
21059
|
+
window.handleArbitratorResume = async (disputeId) => {
|
|
21060
|
+
if (!confirm(t('确认立即解冻自动判定时钟? 协议层 48h 沉默判定将重新生效。'))) return
|
|
21061
|
+
const res = await POST(`/disputes/${disputeId}/arbitrator-resume-auto-judge`, {})
|
|
21062
|
+
if (res.error) { alert(res.error); return }
|
|
21063
|
+
renderDisputeDetail(document.getElementById('app'), disputeId)
|
|
21064
|
+
}
|
|
21065
|
+
|
|
19710
21066
|
// 当事方提交证据
|
|
19711
21067
|
window.handleSubmitEvidence = async (requestId, disputeId) => {
|
|
19712
21068
|
const type = document.getElementById(`er-type-${requestId}`)?.value
|
|
@@ -23553,8 +24909,7 @@ async function renderWallet(app) {
|
|
|
23553
24909
|
|
|
23554
24910
|
<!-- 卖家收入分类速览(仅 seller 显示)- 2026-05-24 对碰列只在 PV 允许地区显示 -->
|
|
23555
24911
|
${state.user?.role === 'seller' ? (() => {
|
|
23556
|
-
const
|
|
23557
|
-
const _pvOK = _maxLvl >= 3
|
|
24912
|
+
const _pvOK = Number(state.user?.region_pv_enabled ?? 0) === 1 // 2026-06-04 解耦:PV 列读 pv_enabled,不再绑 max≥3
|
|
23558
24913
|
const cols = _pvOK ? 4 : 3
|
|
23559
24914
|
return `
|
|
23560
24915
|
<div class="card" style="margin-bottom:12px;padding:14px">
|
|
@@ -24548,25 +25903,9 @@ if ('serviceWorker' in navigator) {
|
|
|
24548
25903
|
navigator.serviceWorker.register('/sw.js').catch(() => {})
|
|
24549
25904
|
}
|
|
24550
25905
|
|
|
24551
|
-
//
|
|
24552
|
-
|
|
24553
|
-
window.
|
|
24554
|
-
e.preventDefault()
|
|
24555
|
-
_deferredInstallPrompt = e
|
|
24556
|
-
// 触发一次重渲,让 navbar install 按钮出现
|
|
24557
|
-
try { if (typeof route === 'function') route() } catch {}
|
|
24558
|
-
})
|
|
24559
|
-
window.addEventListener('appinstalled', () => {
|
|
24560
|
-
_deferredInstallPrompt = null
|
|
24561
|
-
try { if (typeof route === 'function') route() } catch {}
|
|
24562
|
-
})
|
|
24563
|
-
window.canInstallPWA = () => !!_deferredInstallPrompt
|
|
24564
|
-
window.doInstallPWA = async () => {
|
|
24565
|
-
if (!_deferredInstallPrompt) return
|
|
24566
|
-
_deferredInstallPrompt.prompt()
|
|
24567
|
-
try { await _deferredInstallPrompt.userChoice } catch {}
|
|
24568
|
-
_deferredInstallPrompt = null
|
|
24569
|
-
}
|
|
25906
|
+
// 上面 line ~25263 已注册 beforeinstallprompt 监听 + doInstallPWA(带 iOS/通用引导 fallback);
|
|
25907
|
+
// 此处只补一个 canInstallPWA helper 给 navbar 按钮判 visibility 用。
|
|
25908
|
+
window.canInstallPWA = () => !!window._installPromptEvent || isIOS()
|
|
24570
25909
|
|
|
24571
25910
|
// 离线状态全局 banner
|
|
24572
25911
|
function ensureOfflineBanner() {
|
|
@@ -27834,9 +29173,20 @@ async function renderLeaderboard(app) {
|
|
|
27834
29173
|
// 2026-05-23 隐私第一原理:buyers/sellers 榜不再展示 GMV 金额(运营状态私密)
|
|
27835
29174
|
const tabBtn = (k, label) => `<button onclick="setLeaderboardKind('${k}')" style="padding:6px 14px;border-radius:99px;font-size:12px;font-weight:600;cursor:pointer;border:1px solid ${kind===k?'#4f46e5':'#e5e7eb'};background:${kind===k?'#eef2ff':'#fff'};color:${kind===k?'#4338ca':'#6b7280'}">${label}</button>`
|
|
27836
29175
|
|
|
29176
|
+
// #1080 audit: spec §6.1 mandatory banner for arbitrators/verifiers leaderboards
|
|
29177
|
+
// (governance roles where reward-distribution misinterpretation is highest risk)
|
|
29178
|
+
const isGovernanceLeaderboard = kind === 'arbitrators' || kind === 'verifiers'
|
|
29179
|
+
const governanceBanner = isGovernanceLeaderboard ? `
|
|
29180
|
+
<div style="margin-bottom:14px;padding:12px;background:#fffbeb;border:1px solid #fcd34d;border-radius:8px;font-size:12px;color:#7c2d12;line-height:1.6">
|
|
29181
|
+
<div style="font-weight:600;margin-bottom:4px">📊 ${t('本榜单仅展示履职数据,不构成 token / 经济利益分配')}</div>
|
|
29182
|
+
<div style="opacity:0.85">${t('Reward distribution 机制由 phase D first DAO 决定。详 docs/GOVERNANCE-LEADERBOARD-SPEC.md。')}</div>
|
|
29183
|
+
</div>
|
|
29184
|
+
` : ''
|
|
29185
|
+
|
|
27837
29186
|
document.getElementById('lb-root').innerHTML = `
|
|
27838
29187
|
<h2 style="font-size:18px;font-weight:700;margin-bottom:6px">🏆 ${t('排行榜')}</h2>
|
|
27839
29188
|
<div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('协议不分发流量;越多真实推荐 / 点赞 → 越靠前')}</div>
|
|
29189
|
+
${governanceBanner}
|
|
27840
29190
|
<div style="display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap">
|
|
27841
29191
|
${tabBtn('products', '📦 ' + t('商品榜'))}
|
|
27842
29192
|
${tabBtn('value_products', '💎 ' + t('性价比榜'))}
|
|
@@ -27901,16 +29251,21 @@ async function renderLeaderboard(app) {
|
|
|
27901
29251
|
`
|
|
27902
29252
|
}).join('') :
|
|
27903
29253
|
kind === 'arbitrators' ? items.map((u, i) => {
|
|
27904
|
-
|
|
27905
|
-
|
|
29254
|
+
// #1080 audit: spec §4 — fairness 阈值 < 10 cases(比 accuracy 的 < 5 更严,因为
|
|
29255
|
+
// fairness 公式需要更多样本才统计稳定)。当前 arbitrator metric 只 fairness,所以用 10。
|
|
29256
|
+
const casesCount = Number(u.cases_count || 0)
|
|
29257
|
+
const isInsufficient = casesCount < 10
|
|
29258
|
+
const fs = isInsufficient ? t('insufficient data') : (u.fairness_score != null ? (u.fairness_score * 100).toFixed(1) + '%' : '—')
|
|
29259
|
+
// spec §3:无 "best/worst" 价值判断 — 中性灰色显示,不再 green/yellow/red 暗示评级
|
|
29260
|
+
const fsColor = isInsufficient ? '#9ca3af' : '#374151'
|
|
27906
29261
|
const totalRatings = Number(u.total_yes || 0) + Number(u.total_no || 0)
|
|
27907
29262
|
return `
|
|
27908
|
-
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('
|
|
27909
|
-
<div style="font-size:24px;font-weight:800;color
|
|
29263
|
+
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('仲裁员公平评价(< 10 案显 insufficient data,spec §4)')}">
|
|
29264
|
+
<div style="font-size:24px;font-weight:800;color:#9ca3af;min-width:32px;text-align:center">${i+1}</div>
|
|
27910
29265
|
<div style="flex:1;min-width:0">
|
|
27911
29266
|
<div style="font-weight:600;font-size:14px">⚖ @${escHtml(u.handle || u.name?.slice(0,8) || '?')}</div>
|
|
27912
29267
|
<div style="font-size:11px;color:#6b7280;margin-top:2px">
|
|
27913
|
-
${
|
|
29268
|
+
${casesCount} ${t('案')}
|
|
27914
29269
|
${totalRatings > 0 ? ` · 👍 ${u.total_yes} / 👎 ${u.total_no}` : ` · <span style="color:#9ca3af">${t('暂无公众评价')}</span>`}
|
|
27915
29270
|
</div>
|
|
27916
29271
|
</div>
|
|
@@ -27922,22 +29277,25 @@ async function renderLeaderboard(app) {
|
|
|
27922
29277
|
`
|
|
27923
29278
|
}).join('') :
|
|
27924
29279
|
kind === 'verifiers' ? items.map((u, i) => {
|
|
27925
|
-
|
|
27926
|
-
const
|
|
27927
|
-
const
|
|
29280
|
+
// #1080 audit: spec §4 < 5 cases → "insufficient data"
|
|
29281
|
+
const tasksDone = Number(u.tasks_done || 0)
|
|
29282
|
+
const isInsufficient = tasksDone < 5
|
|
29283
|
+
const acc = isInsufficient ? t('insufficient data') : (u.accuracy != null ? (u.accuracy * 100).toFixed(1) + '%' : '—')
|
|
29284
|
+
// spec §3: 无 "best/worst" 价值判断 — 中性显示
|
|
29285
|
+
const accColor = isInsufficient ? '#9ca3af' : '#374151'
|
|
27928
29286
|
const wrongCount = Number(u.tasks_wrong || 0)
|
|
27929
29287
|
return `
|
|
27930
|
-
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('正确数 / 总任务数 · 命中率
|
|
27931
|
-
<div style="font-size:24px;font-weight:800;color
|
|
29288
|
+
<div class="card" style="padding:12px;margin-bottom:8px;display:flex;gap:10px;align-items:center" title="${t('正确数 / 总任务数 · 命中率(< 5 任务显 insufficient data,spec §4)')}">
|
|
29289
|
+
<div style="font-size:24px;font-weight:800;color:#9ca3af;min-width:32px;text-align:center">${i+1}</div>
|
|
27932
29290
|
<div style="flex:1;min-width:0">
|
|
27933
29291
|
<div style="font-weight:600;font-size:14px;display:flex;align-items:center;gap:6px;flex-wrap:wrap">
|
|
27934
29292
|
@${escHtml(u.handle || u.name?.slice(0,8) || '?')}
|
|
27935
29293
|
${u.tier ? `<span style="font-size:10px;background:#ede9fe;color:#5b21b6;padding:1px 6px;border-radius:99px">${u.tier}</span>` : ''}
|
|
27936
|
-
${
|
|
29294
|
+
${isInsufficient ? `<span style="font-size:10px;background:#fef3c7;color:#92400e;padding:1px 6px;border-radius:99px">${t('新人')}</span>` : ''}
|
|
27937
29295
|
</div>
|
|
27938
29296
|
<div style="font-size:11px;color:#6b7280;margin-top:2px">
|
|
27939
|
-
✓ ${u.tasks_correct} ${t('正确')} / ${
|
|
27940
|
-
${wrongCount > 0 ? ` · <span style="color:#
|
|
29297
|
+
✓ ${u.tasks_correct} ${t('正确')} / ${tasksDone} ${t('总任务')}
|
|
29298
|
+
${wrongCount > 0 ? ` · <span style="color:#9ca3af">✗ ${wrongCount} ${t('偏离')}</span>` : ''}
|
|
27941
29299
|
</div>
|
|
27942
29300
|
</div>
|
|
27943
29301
|
<div style="text-align:right">
|
|
@@ -28913,6 +30271,13 @@ async function renderBuyerMyHome(app) {
|
|
|
28913
30271
|
`
|
|
28914
30272
|
}
|
|
28915
30273
|
|
|
30274
|
+
// 阶段 4(#1093):新治理 onboarding 入口 — 始终可访问我的治理岗位面板(卸任 / 申诉 / 历史)
|
|
30275
|
+
const governanceMeSection = (isExternalArb || isExternalVerifier) ? `
|
|
30276
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
30277
|
+
${card('🏛', t('我的治理岗位'), t('在岗 / 申诉 / 卸任'), '#governance-me')}
|
|
30278
|
+
</div>
|
|
30279
|
+
` : ''
|
|
30280
|
+
|
|
28916
30281
|
// ③ 通用索赔任务 tile
|
|
28917
30282
|
const claimsTile = myClaimTasks > 0 ? `
|
|
28918
30283
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px">
|
|
@@ -28921,11 +30286,12 @@ async function renderBuyerMyHome(app) {
|
|
|
28921
30286
|
` : ''
|
|
28922
30287
|
|
|
28923
30288
|
// 拼装:有内容才显标题
|
|
28924
|
-
const hasAnyTrust = verifierSection || arbSection || claimsTile
|
|
30289
|
+
const hasAnyTrust = verifierSection || arbSection || claimsTile || governanceMeSection
|
|
28925
30290
|
const trustGrid = hasAnyTrust ? `
|
|
28926
30291
|
${(!isExternalVerifier && !isExternalArb) ? `<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">🛡 ${t('信任与协议')}</div>` : ''}
|
|
28927
30292
|
${verifierSection}
|
|
28928
30293
|
${arbSection}
|
|
30294
|
+
${governanceMeSection}
|
|
28929
30295
|
${claimsTile}
|
|
28930
30296
|
` : ''
|
|
28931
30297
|
|
|
@@ -30487,7 +31853,7 @@ async function renderCharityFund(app) {
|
|
|
30487
31853
|
}[k] || '·')
|
|
30488
31854
|
document.getElementById('cf-root').innerHTML = `
|
|
30489
31855
|
<h2 style="font-size:18px;font-weight:700;margin-bottom:6px">🏦 ${t('慈善基金')}</h2>
|
|
30490
|
-
<div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('协议持有 · 公开账本 · 任何人可捐款 ·
|
|
31856
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:14px;line-height:1.5">${t('协议持有 · 公开账本 · 任何人可捐款 · 专款专用于慈善许愿 · 治理委员会决定出金')}</div>
|
|
30491
31857
|
|
|
30492
31858
|
<div class="card" style="padding:18px;margin-bottom:14px;background:linear-gradient(135deg,#fef2f2,#fff7ed)">
|
|
30493
31859
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
|
@@ -30501,9 +31867,6 @@ async function renderCharityFund(app) {
|
|
|
30501
31867
|
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:6px;font-size:10px;text-align:left;background:#fff;padding:8px;border-radius:6px">
|
|
30502
31868
|
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">💝 ${t('用户捐款')}</span><span style="font-weight:700;color:#9333ea">${Number(f.total_donated||0).toFixed(1)}</span></div>
|
|
30503
31869
|
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">🙏 ${t('还愿转入')}</span><span style="font-weight:700;color:#ea580c">${Number(f.total_redirected||0).toFixed(1)}</span></div>
|
|
30504
|
-
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">🔗 ${t('分享链兜底')}</span><span style="font-weight:700;color:#0369a1">${Number(f.total_chain_gap||0).toFixed(1)}</span></div>
|
|
30505
|
-
<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">👤 ${t('孤儿订单兜底')}</span><span style="font-weight:700;color:#7c3aed">${Number(f.total_orphan_sponsor||0).toFixed(1)}</span></div>
|
|
30506
|
-
${Number(f.total_region_cap||0) > 0 ? `<div style="display:flex;justify-content:space-between"><span style="color:#9ca3af">⚖️ ${t('区域禁 MLM 入池')}</span><span style="font-weight:700;color:#dc2626">${Number(f.total_region_cap||0).toFixed(1)}</span></div>` : ''}
|
|
30507
31870
|
</div>
|
|
30508
31871
|
<div style="display:flex;justify-content:space-between;margin-top:6px;font-size:10px;background:#fff;padding:6px 8px;border-radius:6px">
|
|
30509
31872
|
<span style="color:#9ca3af">🤲 ${t('累计已拨款')}</span><span style="font-weight:700;color:#16a34a">${Number(f.total_disbursed||0).toFixed(1)}</span>
|