@seasonkoh/webaz 0.1.28 → 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/README.zh-CN.md +7 -6
- package/dist/bond-refund-blockers.js +50 -0
- package/dist/bond-slash.js +100 -0
- package/dist/bond-terms.js +31 -0
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +99 -0
- package/dist/direct-pay-cancel-refund.js +160 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +231 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +50 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-fee-prepay-request.js +80 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-pay-returns.js +104 -0
- package/dist/direct-pay-stock.js +9 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +171 -0
- package/dist/direct-receive-deposits.js +365 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/free-shipping.js +37 -0
- package/dist/fx-rates.js +78 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +777 -1
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +1 -0
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +114 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +11 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +99 -32
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +110 -41
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +133 -31
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +16 -4
- package/dist/layer3-trust/L3-1-dispute-engine/mutual-cancel.js +156 -0
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/platform-receive-accounts.js +94 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/arbitration-read-admin.js +37 -0
- package/dist/pwa/arbitrator-lifecycle.js +100 -0
- package/dist/pwa/contract-fingerprint.js +18 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +42 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/human-presence.js +2 -6
- package/dist/pwa/public/app-account.js +9 -9
- package/dist/pwa/public/app-admin-disputes.js +55 -0
- package/dist/pwa/public/app-agent-appeal.js +90 -0
- package/dist/pwa/public/app-agent-approvals.js +93 -0
- package/dist/pwa/public/app-agent-pair.js +127 -0
- package/dist/pwa/public/app-ai.js +10 -10
- package/dist/pwa/public/app-arbitrator-admin.js +87 -0
- package/dist/pwa/public/app-arbitrator-entry.js +9 -0
- package/dist/pwa/public/app-bond-deferral-ui.js +9 -0
- package/dist/pwa/public/app-bond-refund-ui.js +66 -0
- package/dist/pwa/public/app-bond-slash-ui.js +74 -0
- package/dist/pwa/public/app-bond-terms-ui.js +23 -0
- package/dist/pwa/public/app-bond-ui.js +108 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution-hub.js +23 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-cancel-refund.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-copy.js +12 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-history.js +39 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-fee-request.js +81 -0
- package/dist/pwa/public/app-direct-pay-fee-requests-admin.js +70 -0
- package/dist/pwa/public/app-direct-pay-memo.js +14 -0
- package/dist/pwa/public/app-direct-pay-negotiation.js +35 -0
- package/dist/pwa/public/app-direct-pay-pay.js +15 -0
- package/dist/pwa/public/app-direct-pay-paymodal.js +32 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-reconcile.js +19 -0
- package/dist/pwa/public/app-direct-pay-returns.js +56 -0
- package/dist/pwa/public/app-direct-pay-reveal.js +82 -0
- package/dist/pwa/public/app-direct-pay-sales-report.js +70 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +226 -0
- package/dist/pwa/public/app-discover.js +20 -20
- package/dist/pwa/public/app-dispute-close-ui.js +38 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-free-shipping-ui.js +29 -0
- package/dist/pwa/public/app-gmv-rail-split.js +12 -0
- package/dist/pwa/public/app-listing-commerce-ui.js +72 -0
- package/dist/pwa/public/app-listings.js +4 -4
- package/dist/pwa/public/app-mutual-cancel.js +54 -0
- package/dist/pwa/public/app-notif-templates-orders.js +43 -0
- package/dist/pwa/public/app-notif-templates.js +22 -0
- package/dist/pwa/public/app-order-accept-ui.js +158 -0
- package/dist/pwa/public/app-order-errors.js +50 -0
- package/dist/pwa/public/app-order-labels.js +19 -0
- package/dist/pwa/public/app-order-rail-filter.js +26 -0
- package/dist/pwa/public/app-platform-receive-accounts.js +140 -0
- package/dist/pwa/public/app-poll-governor.js +22 -0
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +10 -10
- package/dist/pwa/public/app-purchase-terms-ui.js +68 -0
- package/dist/pwa/public/app-sale-regions-ui.js +38 -0
- package/dist/pwa/public/app-seller.js +5 -5
- package/dist/pwa/public/app-shop.js +19 -19
- package/dist/pwa/public/app-trade-tax-ui.js +33 -0
- package/dist/pwa/public/app.js +264 -295
- package/dist/pwa/public/i18n.js +1068 -197
- package/dist/pwa/public/index.html +58 -0
- package/dist/pwa/public/openapi.json +1845 -653
- package/dist/pwa/public/style.css +3 -0
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +533 -0
- package/dist/pwa/routes/admin-protocol-params.js +16 -0
- package/dist/pwa/routes/admin-reports.js +31 -5
- package/dist/pwa/routes/agent-governance.js +1 -1
- package/dist/pwa/routes/agent-grants.js +253 -32
- package/dist/pwa/routes/analytics.js +2 -0
- package/dist/pwa/routes/arbitrator.js +67 -14
- package/dist/pwa/routes/bond-seller.js +162 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-cancel-refund.js +111 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +79 -0
- package/dist/pwa/routes/direct-pay-pending-accept.js +222 -0
- package/dist/pwa/routes/direct-pay-returns.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +248 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/disputes-read.js +20 -6
- package/dist/pwa/routes/disputes-write.js +91 -33
- package/dist/pwa/routes/external-anchors.js +4 -2
- package/dist/pwa/routes/fee-prepay-requests.js +66 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/governance-onboarding.js +46 -8
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +6 -2
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/mutual-cancel.js +62 -0
- package/dist/pwa/routes/orders-action.js +297 -16
- package/dist/pwa/routes/orders-create.js +21 -6
- package/dist/pwa/routes/orders-read.js +106 -8
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/platform-receive-accounts.js +111 -0
- package/dist/pwa/routes/products-create.js +15 -4
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +37 -3
- package/dist/pwa/routes/profile-identity.js +4 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +60 -4
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/seller-directpay-report.js +110 -0
- package/dist/pwa/routes/seller-quota.js +5 -1
- package/dist/pwa/routes/shipping-templates.js +219 -0
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/snf.js +4 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +3 -3
- package/dist/pwa/server.js +112 -132
- package/dist/runtime/agent-grant-scopes.js +69 -1
- package/dist/runtime/webaz-schema-helpers.js +161 -3
- package/dist/sale-regions.js +116 -0
- package/dist/shipping-templates.js +119 -0
- package/dist/store-verification.js +77 -0
- package/dist/trade-tax.js +99 -0
- package/dist/trade-terms.js +76 -0
- package/dist/version.js +1 -1
- package/package.json +128 -2
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// RFC-020 — Agent 授权请求审批页(#agent-approvals)。一个已连接的 agent 请求【更多 SAFE 作用域 / 一个能力包】,
|
|
2
|
+
// 真人在这里审核并用 Passkey 批准或拒绝。批准 = 扩展该 agent 已有的委托凭证(仍只 SAFE、可撤销、按时长)。
|
|
3
|
+
// 经典脚本,全局函数;globals(GET/POST/state/shell/t/escHtml/fmtTime/navigate/loading$/toast$/requestPasskeyGate)调用时解析。
|
|
4
|
+
// 商家视角:不需要懂 scope/complete/verify —— 只看【哪个 agent · 要做什么 · 风险 · 多久】。批准需 Passkey(扩权=提权)。
|
|
5
|
+
;(function () {
|
|
6
|
+
const DURATION_LABEL = { once: '一次性', '1h': '1 小时', '24h': '24 小时', '7d': '7 天', '30d': '30 天' }
|
|
7
|
+
const RISK = {
|
|
8
|
+
low: { label: '低风险', color: '#16a34a', bg: '#dcfce7' },
|
|
9
|
+
medium: { label: '中风险', color: '#b45309', bg: '#fef3c7' },
|
|
10
|
+
high: { label: '高风险', color: '#dc2626', bg: '#fee2e2' },
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Lightweight badge: fill #aa-pending-badge with a pending count (called from the account/settings page).
|
|
14
|
+
async function hydrateAgentApprovalsBadge() {
|
|
15
|
+
const el = document.getElementById('aa-pending-badge'); if (!el || !state.user) return
|
|
16
|
+
const r = await GET('/agent-grants/permission-requests').catch(() => null)
|
|
17
|
+
const n = (r && Array.isArray(r.requests)) ? r.requests.length : 0
|
|
18
|
+
el.innerHTML = n > 0 ? `<span style="display:inline-block;min-width:16px;height:16px;line-height:16px;text-align:center;font-size:10px;color:#fff;background:#dc2626;border-radius:999px;padding:0 5px;margin-left:4px">${n}</span>` : ''
|
|
19
|
+
}
|
|
20
|
+
window.hydrateAgentApprovalsBadge = hydrateAgentApprovalsBadge
|
|
21
|
+
|
|
22
|
+
function renderAgentApprovals(app) {
|
|
23
|
+
if (!state.user) { app.innerHTML = shell(`<div class="empty">${t('请先登录以审核 agent 授权请求')}</div>`, 'me'); return }
|
|
24
|
+
app.innerHTML = shell(`
|
|
25
|
+
<div class="page-header"><h2>${t('🔔 Agent 授权请求')}</h2></div>
|
|
26
|
+
<div style="font-size:12px;color:#6b7280;padding:0 4px 12px;line-height:1.6">${t('一个已连接的 AI agent 请求更多【安全只读/草稿】权限。批准只会扩展它已有的委托凭证 —— 仍然作用域受限、可随时撤销,永远动不了资金、投票、仲裁或改密钥。')}</div>
|
|
27
|
+
<div id="aa-body">${loading$()}</div>
|
|
28
|
+
`, 'me')
|
|
29
|
+
setTimeout(aaHydrate, 30)
|
|
30
|
+
}
|
|
31
|
+
window.renderAgentApprovals = renderAgentApprovals
|
|
32
|
+
|
|
33
|
+
async function aaHydrate() {
|
|
34
|
+
const box = document.getElementById('aa-body'); if (!box) return
|
|
35
|
+
const r = await GET('/agent-grants/permission-requests').catch(() => null)
|
|
36
|
+
if (!r || r.error) { box.innerHTML = `<div class="card" style="padding:16px;color:#991b1b">${escHtml((r && r.error) || t('无法读取授权请求,请重试。'))}</div>`; return }
|
|
37
|
+
const reqs = Array.isArray(r.requests) ? r.requests : []
|
|
38
|
+
if (reqs.length === 0) {
|
|
39
|
+
box.innerHTML = `<div class="empty" style="padding:40px 16px;text-align:center">
|
|
40
|
+
<div style="font-size:32px;margin-bottom:8px">✅</div>
|
|
41
|
+
<div style="font-weight:600;margin-bottom:4px">${t('暂无待处理的授权请求')}</div>
|
|
42
|
+
<div style="color:#9ca3af;font-size:12px">${t('当你的 agent 请求更多权限时,会出现在这里等你批准。')}</div>
|
|
43
|
+
</div>`
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
box.innerHTML = reqs.map(aaCard).join('')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function aaCard(r) {
|
|
50
|
+
const risk = RISK[r.risk_level] || RISK.low
|
|
51
|
+
const scopes = Array.isArray(r.requested_scopes) ? r.requested_scopes : []
|
|
52
|
+
// Prefer the human-readable bundle summary; otherwise show the individual safe-scope chips.
|
|
53
|
+
const what = r.human_summary
|
|
54
|
+
? `<div style="font-size:13px;color:#374151;line-height:1.7">${escHtml(r.human_summary)}</div>`
|
|
55
|
+
: `<div style="margin-top:2px">${scopes.map(s => `<span style="display:inline-block;font-size:11px;color:#4f46e5;background:#eef2ff;padding:2px 8px;border-radius:4px;margin:2px 4px 2px 0">${escHtml(String(s))}</span>`).join('') || `<span style="font-size:12px;color:#9ca3af">${t('(无 —— 仅基础只读)')}</span>`}</div>`
|
|
56
|
+
const dur = DURATION_LABEL[r.duration] || r.duration || '—'
|
|
57
|
+
return `<div class="card" style="margin-bottom:12px;padding:16px;border:1px solid #e5e7eb" data-aa-id="${escHtml(String(r.id))}">
|
|
58
|
+
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
|
|
59
|
+
<div style="font-weight:600">${escHtml(r.agent_label || t('未命名 Agent'))} <span style="font-size:10px;color:#9ca3af">(${t('未验证')})</span></div>
|
|
60
|
+
<span style="font-size:11px;color:${risk.color};background:${risk.bg};padding:2px 8px;border-radius:999px">${t(risk.label)}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:2px">${t('它想做什么')}:</div>
|
|
63
|
+
${what}
|
|
64
|
+
${r.reason ? `<div style="font-size:12px;color:#6b7280;margin-top:8px">${t('用途(agent 自述)')}: ${escHtml(r.reason)} <span style="font-size:10px;color:#9ca3af">(${t('未验证')})</span></div>` : ''}
|
|
65
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:8px">${t('授权时长')}: <strong style="color:#374151">${t(dur)}</strong> · ${t('请求于')} ${r.created_at ? fmtTime(r.created_at) : '—'}</div>
|
|
66
|
+
<div style="display:flex;gap:8px;margin-top:12px">
|
|
67
|
+
<button class="btn btn-outline" style="flex:1;color:#dc2626;border-color:#fecaca" onclick="aaReject('${escHtml(String(r.id))}')">${t('拒绝')}</button>
|
|
68
|
+
<button class="btn btn-primary" style="flex:2" onclick="aaApprove('${escHtml(String(r.id))}')">🔑 ${t('用 Passkey 批准')}</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>`
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
window.aaApprove = async (id) => {
|
|
74
|
+
const card = document.querySelector(`[data-aa-id="${(window.CSS && CSS.escape) ? CSS.escape(id) : id}"]`)
|
|
75
|
+
const btn = card ? card.querySelector('.btn-primary') : null; if (btn) btn.disabled = true
|
|
76
|
+
let token
|
|
77
|
+
// Passkey bound to THIS request (a token minted for request A can't approve B — server re-validates).
|
|
78
|
+
try { token = await requestPasskeyGate('agent_permission_approve', { request_id: id }) }
|
|
79
|
+
catch (e) { if (window.dpPromptRegisterPasskey && e && e.code === 'NO_PASSKEY_REGISTERED') { await window.dpPromptRegisterPasskey(e) } else { toast$((e && e.message) || t('Passkey 验证已取消'), 'error') } if (btn) btn.disabled = false; return }
|
|
80
|
+
const r = await POST('/agent-grants/permission-requests/' + encodeURIComponent(id) + '/approve', { webauthn_token: token }).catch(() => null)
|
|
81
|
+
if (!r || r.error) { toast$((r && r.error) || t('批准失败,请重试'), 'error'); if (btn) btn.disabled = false; return }
|
|
82
|
+
toast$(t('已批准 —— 该 agent 的权限已扩展'))
|
|
83
|
+
aaHydrate(); hydrateAgentApprovalsBadge()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
window.aaReject = async (id) => {
|
|
87
|
+
if (!confirm(t('确认拒绝这个授权请求?'))) return
|
|
88
|
+
const r = await POST('/agent-grants/permission-requests/' + encodeURIComponent(id) + '/reject', {}).catch(() => null)
|
|
89
|
+
if (!r || r.error) { toast$((r && r.error) || t('操作失败'), 'error'); return }
|
|
90
|
+
toast$(t('已拒绝该授权请求'))
|
|
91
|
+
aaHydrate(); hydrateAgentApprovalsBadge()
|
|
92
|
+
}
|
|
93
|
+
})()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// RFC-020 — Agent 配对授权页(#pair)。真人登录后审核 agent 的委托请求,Passkey 批准 / 拒绝。
|
|
2
|
+
// 经典脚本,全局函数;globals(GET/POST/state/shell/t/escHtml/fmtTime/navigate/loading$/toast$/requestPasskeyGate)调用时解析。
|
|
3
|
+
// 安全姿态:配对只授 SAFE scope(只读/草稿,可撤销,短期,永不碰钱/投票/仲裁/密钥)。
|
|
4
|
+
// 反钓鱼:口令(user_code)是【必须核对】的安全值 —— 大号显示 + 强制"与 agent 屏幕一致"确认才解锁批准;label/reason 是 agent 自称,标注未验证。
|
|
5
|
+
// 未登录由 app.js 路由守卫拦截并存 webaz_intended_hash(含 ?code=),登录后跳回本页 —— 不丢 code。
|
|
6
|
+
;(function () {
|
|
7
|
+
let _code = null
|
|
8
|
+
|
|
9
|
+
function renderAgentPair(app) {
|
|
10
|
+
if (!state.user) { app.innerHTML = shell(`<div class="empty">${t('请先登录以审核 agent 授权请求')}</div>`, 'me'); return }
|
|
11
|
+
_code = (state._urlQuery && typeof state._urlQuery.code === 'string') ? state._urlQuery.code.trim().toUpperCase().slice(0, 32) : ''
|
|
12
|
+
app.innerHTML = shell(`
|
|
13
|
+
<div class="page-header"><h2>${t('🔗 授权 AI Agent')}</h2></div>
|
|
14
|
+
<div style="font-size:12px;color:#6b7280;padding:0 4px 12px;line-height:1.6">${t('一个 AI agent 请求代表你执行【安全只读/草稿】操作。它拿到的是作用域受限、短期、可随时撤销的委托凭证 —— 不是你的账号或密钥,永远动不了资金、投票、仲裁或改密钥。')}</div>
|
|
15
|
+
<div id="pair-body">${loading$()}</div>
|
|
16
|
+
`, 'me')
|
|
17
|
+
setTimeout(dpairHydrate, 30)
|
|
18
|
+
}
|
|
19
|
+
window.renderAgentPair = renderAgentPair
|
|
20
|
+
|
|
21
|
+
async function dpairHydrate() {
|
|
22
|
+
const box = document.getElementById('pair-body'); if (!box) return
|
|
23
|
+
if (!_code || _code.length < 4) { // 无 code / 手动输入路径(抗钓鱼正路:人对着 agent 屏幕输口令)
|
|
24
|
+
box.innerHTML = `<div class="card" style="padding:16px">
|
|
25
|
+
<div style="font-size:13px;margin-bottom:8px">${t('请输入你的 agent 屏幕上显示的配对口令:')}</div>
|
|
26
|
+
<input id="pair-code-input" maxlength="32" placeholder="${t('配对口令')}" style="width:100%;font-size:16px;letter-spacing:2px;text-transform:uppercase;padding:10px;border:1px solid #d1d5db;border-radius:8px;text-align:center;font-family:monospace">
|
|
27
|
+
<button class="btn btn-primary" style="width:100%;margin-top:10px" onclick="dpairSubmitCode()">${t('查看授权请求')}</button>
|
|
28
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:8px;line-height:1.6">${t('只输入你自己的 agent 显示给你的口令。不要输入别人发来的口令。')}</div>
|
|
29
|
+
</div>`
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const r = await GET('/agent-grants/pair/' + encodeURIComponent(_code)).catch(() => null)
|
|
33
|
+
if (!r || r.error) return void dpairShowError(r && (r.error_code || r.error))
|
|
34
|
+
dpairRenderConsent(r.consent || {})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
window.dpairSubmitCode = () => {
|
|
38
|
+
const v = (document.getElementById('pair-code-input')?.value || '').trim().toUpperCase()
|
|
39
|
+
if (v.length < 4) { toast$(t('请输入有效口令'), 'error'); return }
|
|
40
|
+
_code = v; dpairHydrate()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function dpairShowError(codeOrMsg) {
|
|
44
|
+
const box = document.getElementById('pair-body'); if (!box) return
|
|
45
|
+
const map = {
|
|
46
|
+
pairing_not_found: t('配对口令无效或不存在。请核对你的 agent 显示的口令。'),
|
|
47
|
+
pairing_not_pending_or_expired: t('该配对请求已过期、已被处理或已失效。请让你的 agent 重新发起配对。'),
|
|
48
|
+
pairing_not_pending: t('该配对请求已被处理或已失效。'),
|
|
49
|
+
}
|
|
50
|
+
const msg = map[codeOrMsg] || t('无法读取该配对请求,请重试。')
|
|
51
|
+
box.innerHTML = `<div class="card" style="padding:16px;border:1px solid #fecaca;background:#fef2f2">
|
|
52
|
+
<div style="font-size:14px;font-weight:600;color:#991b1b;margin-bottom:6px">⚠️ ${t('无法授权')}</div>
|
|
53
|
+
<div style="font-size:13px;color:#7f1d1d;line-height:1.6">${escHtml(msg)}</div>
|
|
54
|
+
<button class="btn btn-sm" style="margin-top:10px" onclick="navigate('#pair')">${t('手动输入口令')}</button>
|
|
55
|
+
</div>`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function dpairRenderConsent(c) {
|
|
59
|
+
const box = document.getElementById('pair-body'); if (!box) return
|
|
60
|
+
const caps = Array.isArray(c.capabilities) ? c.capabilities : []
|
|
61
|
+
const capHtml = caps.length
|
|
62
|
+
? caps.map(cap => `<span style="display:inline-block;font-size:11px;color:#4f46e5;background:#eef2ff;padding:2px 8px;border-radius:4px;margin:2px 4px 2px 0">${escHtml(String(cap.capability || cap))}</span>`).join('')
|
|
63
|
+
: `<span style="font-size:12px;color:#9ca3af">${t('(无 —— 仅基础只读)')}</span>`
|
|
64
|
+
box.innerHTML = `
|
|
65
|
+
<div class="card" style="padding:16px;border:2px solid #c7d2fe;margin-bottom:12px">
|
|
66
|
+
<div style="text-align:center;margin-bottom:12px">
|
|
67
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:4px">${t('配对口令 —— 请核对与你的 agent 屏幕一致')}</div>
|
|
68
|
+
<div style="font-size:28px;font-weight:800;letter-spacing:4px;font-family:monospace;color:#4338ca">${escHtml(String(_code))}</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div style="font-size:12px;color:#991b1b;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:10px;line-height:1.6;margin-bottom:12px">
|
|
71
|
+
⚠️ <strong>${t('只在这个口令与你的 agent 屏幕显示的一致、且是你主动发起时才批准。')}</strong> ${t('如果这是别人发给你的链接,或你并没有在配对 agent —— 请拒绝。')}
|
|
72
|
+
</div>
|
|
73
|
+
<div style="font-size:13px;line-height:1.9">
|
|
74
|
+
<div><span style="color:#6b7280">${t('Agent 自称')}:</span> <strong>${escHtml(c.agent_label || t('未命名'))}</strong> <span style="font-size:10px;color:#9ca3af">(${t('未验证')})</span></div>
|
|
75
|
+
${c.reason ? `<div><span style="color:#6b7280">${t('用途(agent 自述)')}:</span> ${escHtml(c.reason)} <span style="font-size:10px;color:#9ca3af">(${t('未验证')})</span></div>` : ''}
|
|
76
|
+
<div style="margin-top:6px"><span style="color:#6b7280">${t('请求的权限')}:</span><div style="margin-top:4px">${capHtml}</div></div>
|
|
77
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:6px">${t('有效期至')} ${c.expires_at ? fmtTime(c.expires_at) : '—'}</div>
|
|
78
|
+
</div>
|
|
79
|
+
<label style="display:flex;align-items:flex-start;gap:8px;margin:14px 0 4px;font-size:12px;color:#374151;cursor:pointer">
|
|
80
|
+
<input type="checkbox" id="pair-confirm" onchange="document.getElementById('pair-approve-btn').disabled=!this.checked" style="margin-top:2px">
|
|
81
|
+
<span>${t('我确认这个口令与我的 agent 显示一致,且这是我本人主动发起的配对。')}</span>
|
|
82
|
+
</label>
|
|
83
|
+
<div style="display:flex;gap:8px;margin-top:12px">
|
|
84
|
+
<button class="btn btn-outline" style="flex:1;color:#dc2626;border-color:#fecaca" onclick="dpairReject()">${t('拒绝')}</button>
|
|
85
|
+
<button class="btn btn-primary" id="pair-approve-btn" disabled style="flex:2" onclick="dpairApprove()">🔑 ${t('用 Passkey 批准')}</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
window.dpairApprove = async () => {
|
|
91
|
+
const btn = document.getElementById('pair-approve-btn'); if (btn) btn.disabled = true
|
|
92
|
+
let token
|
|
93
|
+
try { token = await requestPasskeyGate('agent_pair_approve', { user_code: _code }) }
|
|
94
|
+
catch (e) { if (window.dpPromptRegisterPasskey && e && e.code === 'NO_PASSKEY_REGISTERED') { await window.dpPromptRegisterPasskey(e) } else { toast$((e && e.message) || t('Passkey 验证已取消'), 'error') } if (btn) btn.disabled = false; return }
|
|
95
|
+
const r = await POST('/agent-grants/pair/' + encodeURIComponent(_code) + '/approve', { webauthn_token: token }).catch(() => null)
|
|
96
|
+
if (!r || r.error) { toast$((r && r.error) || t('批准失败,请重试'), 'error'); if (btn) btn.disabled = false; return }
|
|
97
|
+
dpairShowResult('approved', r)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.dpairReject = async () => {
|
|
101
|
+
if (!confirm(t('确认拒绝这个 agent 配对请求?'))) return
|
|
102
|
+
const r = await POST('/agent-grants/pair/' + encodeURIComponent(_code) + '/reject', {}).catch(() => null)
|
|
103
|
+
if (!r || r.error) { toast$((r && r.error) || t('操作失败'), 'error'); return }
|
|
104
|
+
dpairShowResult('rejected', r)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function dpairShowResult(kind, r) {
|
|
108
|
+
const box = document.getElementById('pair-body'); if (!box) return
|
|
109
|
+
if (kind === 'approved') {
|
|
110
|
+
const caps = Array.isArray(r.capabilities) ? r.capabilities : []
|
|
111
|
+
box.innerHTML = `<div class="card" style="padding:20px;text-align:center;border:1px solid #bbf7d0;background:#f0fdf4">
|
|
112
|
+
<div style="font-size:36px;margin-bottom:8px">✅</div>
|
|
113
|
+
<div style="font-size:16px;font-weight:700;color:#166534;margin-bottom:6px">${t('已授权')}</div>
|
|
114
|
+
<div style="font-size:12px;color:#374151;margin-bottom:10px">${t('该 agent 现在可用你批准的作用域了(短期、可随时撤销)。回到你的 agent 完成配对即可。')}</div>
|
|
115
|
+
<div style="margin-bottom:12px">${caps.map(cap => `<span style="display:inline-block;font-size:11px;color:#4f46e5;background:#eef2ff;padding:2px 8px;border-radius:4px;margin:2px">${escHtml(String(cap.capability || cap))}</span>`).join('') || `<span style="font-size:11px;color:#9ca3af">${t('基础只读')}</span>`}</div>
|
|
116
|
+
<button class="btn btn-primary" onclick="navigate('#agents')">${t('查看 / 撤销已连接的 Agent')}</button>
|
|
117
|
+
</div>`
|
|
118
|
+
} else {
|
|
119
|
+
box.innerHTML = `<div class="card" style="padding:20px;text-align:center;border:1px solid #e5e7eb">
|
|
120
|
+
<div style="font-size:36px;margin-bottom:8px">🚫</div>
|
|
121
|
+
<div style="font-size:16px;font-weight:700;color:#374151;margin-bottom:6px">${t('已拒绝')}</div>
|
|
122
|
+
<div style="font-size:12px;color:#6b7280;margin-bottom:12px">${t('该配对请求已被拒绝,不会签发任何凭证。')}</div>
|
|
123
|
+
<button class="btn btn-outline btn-sm" onclick="navigate('#agents')">${t('已连接的 Agent')}</button>
|
|
124
|
+
</div>`
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
})()
|
|
@@ -912,7 +912,7 @@ async function aiChatTurn(conversation, userText, attachments, onProgress) {
|
|
|
912
912
|
conversation.messages.push({ role: 'user', content: toolResults })
|
|
913
913
|
}
|
|
914
914
|
await aiSaveConversation(conversation)
|
|
915
|
-
return { content: [{ type: 'text', text: '⚠️ 达到工具调用上限,请重新提问或细化问题。' }] }
|
|
915
|
+
return { content: [{ type: 'text', text: t('⚠️ 达到工具调用上限,请重新提问或细化问题。') }] }
|
|
916
916
|
}
|
|
917
917
|
|
|
918
918
|
function aiCreateConversation() {
|
|
@@ -1096,7 +1096,7 @@ function renderAIStatusBar() {
|
|
|
1096
1096
|
${visionChip}
|
|
1097
1097
|
<span style="font-weight:600;font-size:13px;color:#111827;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(provider.name)}</span>
|
|
1098
1098
|
<span style="color:#d1d5db;font-size:11px;flex-shrink:0">·</span>
|
|
1099
|
-
<span style="color:#6b7280;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(curModel?.label || modelId)}</span>
|
|
1099
|
+
<span style="color:#6b7280;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtml(t(curModel?.label || modelId))}</span>
|
|
1100
1100
|
</div>
|
|
1101
1101
|
${aiTTS.supported() ? `
|
|
1102
1102
|
<button onclick="aiToggleTTS()" title="${aiTTS.isEnabled() ? t('点击关闭朗读') : t('点击开启朗读 AI 回话')}"
|
|
@@ -1326,16 +1326,16 @@ async function renderAIDemo(app) {
|
|
|
1326
1326
|
${d.expectedSteps.map((s, j) => `
|
|
1327
1327
|
<div style="display:flex;gap:8px;padding:4px 0;font-size:11px;color:#374151">
|
|
1328
1328
|
<span style="color:#9ca3af;flex-shrink:0">${j+1}.</span>
|
|
1329
|
-
<code style="background:#eef2ff;color:#3730a3;padding:1px 6px;border-radius:4px;font-size:10px;font-family:ui-monospace,Consolas,monospace;flex-shrink:0">${escHtml(s.tool)}</code>
|
|
1329
|
+
<code style="background:#eef2ff;color:#3730a3;padding:1px 6px;border-radius:4px;font-size:10px;font-family:ui-monospace,Consolas,monospace;flex-shrink:0">${escHtml(t(s.tool))}</code>
|
|
1330
1330
|
<span style="color:#6b7280">${t(s.desc)}</span>
|
|
1331
1331
|
</div>`).join('')}
|
|
1332
1332
|
</div>
|
|
1333
1333
|
</details>
|
|
1334
1334
|
<div style="display:flex;gap:8px">
|
|
1335
|
-
<button data-prompt="${escAttr(d.prompt)}" onclick="runAIDemo(this.dataset.prompt)" style="flex:1;padding:11px;background:#007aff;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer">${t('一键运行')} →</button>
|
|
1335
|
+
<button data-prompt="${escAttr(t(d.prompt))}" onclick="runAIDemo(this.dataset.prompt)" style="flex:1;padding:11px;background:#007aff;color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer">${t('一键运行')} →</button>
|
|
1336
1336
|
</div>
|
|
1337
1337
|
<div style="margin-top:8px;font-size:10px;color:#9ca3af;padding:6px 8px;background:#f9fafb;border-radius:6px;line-height:1.5">
|
|
1338
|
-
💬 ${t('提示原文')}:${escHtml(d.prompt.slice(0, 80))}${d.prompt.length > 80 ? '…' : ''}
|
|
1338
|
+
💬 ${t('提示原文')}:${escHtml(t(d.prompt).slice(0, 80))}${t(d.prompt).length > 80 ? '…' : ''}
|
|
1339
1339
|
</div>
|
|
1340
1340
|
</div>
|
|
1341
1341
|
`).join('')
|
|
@@ -1488,7 +1488,7 @@ async function renderAIRecommend(app) {
|
|
|
1488
1488
|
<!-- 快速模板(intent 空对话才显示)-->
|
|
1489
1489
|
<div style="margin-top:8px;display:flex;gap:6px;overflow-x:auto;padding-bottom:2px;scrollbar-width:none">
|
|
1490
1490
|
${AI_QUICK_TEMPLATES.map(t2 => `
|
|
1491
|
-
<button onclick="aiFillTemplate('${t2.prompt.replace(/'/g, "\\'")}')"
|
|
1491
|
+
<button onclick="aiFillTemplate('${t(t2.prompt).replace(/'/g, "\\'")}')"
|
|
1492
1492
|
style="background:#fff;border:1px dashed #c7d2fe;border-radius:8px;padding:6px 10px;font-size:11px;color:#4338ca;cursor:pointer;white-space:nowrap;flex-shrink:0;font-weight:500">
|
|
1493
1493
|
${t2.icon} ${t(t2.label)}
|
|
1494
1494
|
</button>`).join('')}
|
|
@@ -1959,8 +1959,8 @@ window.aiOpenProviderConfig = (pid) => {
|
|
|
1959
1959
|
<div class="form-group">
|
|
1960
1960
|
<label class="form-label">${t('协议格式')}</label>
|
|
1961
1961
|
<select id="ai-pcfg-cformat" class="form-control" style="font-size:13px">
|
|
1962
|
-
<option value="openai" ${cFormat==='openai'?'selected':''}
|
|
1963
|
-
<option value="anthropic" ${cFormat==='anthropic'?'selected':''}
|
|
1962
|
+
<option value="openai" ${cFormat==='openai'?'selected':''}>${t('OpenAI 兼容 (chat/completions)')}</option>
|
|
1963
|
+
<option value="anthropic" ${cFormat==='anthropic'?'selected':''}>${t('Anthropic 兼容 (messages)')}</option>
|
|
1964
1964
|
</select>
|
|
1965
1965
|
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('多数自建 agent / 代理用 OpenAI 协议')}</div>
|
|
1966
1966
|
</div>
|
|
@@ -1968,7 +1968,7 @@ window.aiOpenProviderConfig = (pid) => {
|
|
|
1968
1968
|
<div class="form-group">
|
|
1969
1969
|
<label class="form-label">${t('默认模型')}</label>
|
|
1970
1970
|
<select id="ai-pcfg-model" class="form-control" style="font-size:13px">
|
|
1971
|
-
${p.models.map(m => `<option value="${m.id}" ${m.id === (localStorage.getItem('webaz_ai_model_' + p.id) || p.defaultModel) ? 'selected' : ''}>${escHtml(m.label)}</option>`).join('')}
|
|
1971
|
+
${p.models.map(m => `<option value="${m.id}" ${m.id === (localStorage.getItem('webaz_ai_model_' + p.id) || p.defaultModel) ? 'selected' : ''}>${escHtml(t(m.label))}</option>`).join('')}
|
|
1972
1972
|
</select>
|
|
1973
1973
|
</div>
|
|
1974
1974
|
`}
|
|
@@ -1991,7 +1991,7 @@ window.aiSaveProviderConfig = (pid, mode) => {
|
|
|
1991
1991
|
|
|
1992
1992
|
if (isCustom) {
|
|
1993
1993
|
// custom: name + model + format 都来自用户填的字段
|
|
1994
|
-
const cname = document.getElementById('ai-pcfg-cname')?.value?.trim() || '我的 Agent'
|
|
1994
|
+
const cname = document.getElementById('ai-pcfg-cname')?.value?.trim() || t('我的 Agent')
|
|
1995
1995
|
const cmodel = document.getElementById('ai-pcfg-cmodel')?.value?.trim()
|
|
1996
1996
|
const clabel = document.getElementById('ai-pcfg-clabel')?.value?.trim()
|
|
1997
1997
|
const cformat = document.getElementById('ai-pcfg-cformat')?.value || 'openai'
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// PR-F:仲裁员管理 admin UI(最小)。唯一授权源 = active arbitrator_whitelist;所有变更 = ROOT + 现场真人 Passkey + 后端审计。
|
|
2
|
+
// grant:输入 user_id/@handle → /admin/users/lookup 解析 → requestPasskeyGate('arbitrator_grant',{user_id}) → POST。
|
|
3
|
+
// 目标须真人 + 已注册 Passkey + 非当事人 + 非 agent/system(后端 grantArbitrator 校验,前端只带输入)。中文 t(),英文 i18n.js。
|
|
4
|
+
window.renderAdminArbitrators = async function (app) {
|
|
5
|
+
if (!state.user) { renderLogin(); return }
|
|
6
|
+
if ((typeof isAdmin === 'function' && !isAdmin()) || (state.user.admin_type || 'root') !== 'root') { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限根管理员')}</div>`, 'admin'); return } // PR-F:先短路判 isAdmin(),否则无 admin_type 用户被 `||'root'` 默认当 root 放行
|
|
7
|
+
app.innerHTML = shell(`
|
|
8
|
+
<h1 class="page-title">⚖ ${t('仲裁员管理')}</h1>
|
|
9
|
+
<div style="margin-bottom:8px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('← 返回')}</button></div>
|
|
10
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('授权真人仲裁员(唯一授权源=active 白名单)。目标须已注册 Passkey、非本案当事人、非 agent/系统账号。授权/暂停/撤销均需你现场 Passkey,后端留痕。')}</div>
|
|
11
|
+
<div class="card" style="margin-bottom:12px">
|
|
12
|
+
<div style="font-size:13px;font-weight:700;margin-bottom:6px">${t('授权新仲裁员')}</div>
|
|
13
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
14
|
+
<input class="form-control" id="arb-grant-q" placeholder="${t('user_id 或 @handle')}" style="flex:1;min-width:200px">
|
|
15
|
+
<button class="btn btn-primary btn-sm" style="width:auto" onclick="arbAdminGrant()">${t('授权(真人 Passkey)')}</button>
|
|
16
|
+
</div>
|
|
17
|
+
<div id="arb-grant-msg" style="margin-top:8px"></div>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="arb-roster">${loading$()}</div>
|
|
20
|
+
`, 'admin')
|
|
21
|
+
window.arbAdminHydrate()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
window.arbAdminHydrate = async () => {
|
|
25
|
+
const box = document.getElementById('arb-roster'); if (!box) return
|
|
26
|
+
const r = await GET('/admin/arbitrators')
|
|
27
|
+
if (r.error) { box.innerHTML = alert$('error', r.error || t('加载失败')); return }
|
|
28
|
+
const rows = r.arbitrators || []
|
|
29
|
+
box.innerHTML = `<div style="font-size:13px;font-weight:700;margin-bottom:6px">${t('仲裁员名册')}(${rows.length})</div>` +
|
|
30
|
+
(rows.length ? rows.map(x => window.arbAdminCard(x)).join('') : `<div style="font-size:12px;color:#9ca3af">${t('暂无仲裁员')}</div>`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
window.arbAdminCard = (r) => {
|
|
34
|
+
const badge = r.status === 'active' ? 'background:#dcfce7;color:#166534' : r.status === 'suspended' ? 'background:#fef9c3;color:#854d0e' : 'background:#fee2e2;color:#991b1b'
|
|
35
|
+
const label = r.status === 'active' ? t('在岗') : r.status === 'suspended' ? t('已暂停') : t('已撤销')
|
|
36
|
+
return `<div style="border:1px solid #e5e7eb;border-radius:10px;padding:10px 12px;margin-bottom:8px" data-arb="${escHtml(r.user_id)}">
|
|
37
|
+
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px">
|
|
38
|
+
<code style="font-size:12px">${escHtml(r.user_id)}</code>
|
|
39
|
+
<span style="font-size:11px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap;${badge}">${label}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<div style="font-size:11px;color:#9ca3af;margin:2px 0 6px">${r.is_system ? t('内部账号(is_system)') + ' · ' : ''}${escHtml(r.note || '')}${r.suspended_at ? ' · ' + t('暂停于') + ' ' + escHtml(r.suspended_at) : ''}${r.revoked_at ? ' · ' + t('撤销于') + ' ' + escHtml(r.revoked_at) : ''}</div>
|
|
42
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
43
|
+
${r.status === 'active' ? `<button class="btn btn-outline btn-sm" style="width:auto" onclick="arbAdminMutate('${escHtml(r.user_id)}','suspend')">${t('暂停')}</button>` : ''}
|
|
44
|
+
${r.status === 'suspended' ? `<button class="btn btn-primary btn-sm" style="width:auto" onclick="arbAdminMutate('${escHtml(r.user_id)}','reinstate')">${t('恢复')}</button>` : ''}
|
|
45
|
+
${r.status !== 'revoked' ? `<button class="btn btn-outline btn-sm" style="width:auto;color:#dc2626;border-color:#dc2626" onclick="arbAdminMutate('${escHtml(r.user_id)}','revoke')">${t('撤销(终态)')}</button>` : `<span style="font-size:11px;color:#9ca3af">${t('已永久撤销,不可再授权')}</span>`}
|
|
46
|
+
</div>
|
|
47
|
+
</div>`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
window.arbAdminGrant = async () => {
|
|
51
|
+
const q = (document.getElementById('arb-grant-q')?.value || '').trim()
|
|
52
|
+
const msg = document.getElementById('arb-grant-msg')
|
|
53
|
+
const show = (type, m) => { if (msg) msg.innerHTML = alert$(type, m) }
|
|
54
|
+
if (!q) return show('error', t('请输入 user_id 或 @handle'))
|
|
55
|
+
const look = await GET('/admin/users/lookup?q=' + encodeURIComponent(q))
|
|
56
|
+
if (look.error || !look.user) return show('error', look.error || t('用户不存在'))
|
|
57
|
+
const userId = look.user.id
|
|
58
|
+
let token
|
|
59
|
+
try { token = await requestPasskeyGate('arbitrator_grant', { user_id: userId }) }
|
|
60
|
+
catch (e) { return show('error', (e && e.message ? e.message + ' — ' : '') + t('需先注册 Passkey')) }
|
|
61
|
+
const res = await POST('/admin/arbitrators/grant', { user_id: userId, webauthn_token: token })
|
|
62
|
+
if (res.error) return show('error', res.error) // 后端已给清晰中文(NOT_HUMAN / PASSKEY_REQUIRED / REVOKED_TERMINAL 等)
|
|
63
|
+
show('success', t('已授权') + ':' + escHtml(look.user.name || userId))
|
|
64
|
+
window.arbAdminHydrate()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// PR-F:whitelist-only 仲裁员(role≠arbitrator,如被授权的买家)在个人页看不到"仲裁台"入口 —— 补一张跟随
|
|
68
|
+
// can_arbitrate 的入口卡(纯 UI;#disputes 本就可达,后端 can_arbitrate/COI/assign 才是边界)。
|
|
69
|
+
window.arbTaishCard = () => `
|
|
70
|
+
<div style="font-size:12px;color:#6b7280;font-weight:600;margin:14px 0 6px">⚖ ${t('仲裁工作')}</div>
|
|
71
|
+
<div style="margin-bottom:10px">
|
|
72
|
+
<div class="card" onclick="location.hash='#disputes'" style="padding:14px;cursor:pointer;display:flex;align-items:center;gap:10px;min-height:64px">
|
|
73
|
+
<div style="font-size:22px">⚖</div>
|
|
74
|
+
<div><div style="font-weight:600;font-size:14px">${t('仲裁台')}</div><div style="font-size:12px;color:#6b7280">${t('待响应 / 仲裁中 / 已结')}</div></div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>`
|
|
77
|
+
|
|
78
|
+
window.arbAdminMutate = async (userId, action) => {
|
|
79
|
+
if (action === 'revoke' && !(await confirmModal(t('撤销是终态,不可再授权该用户。确定撤销?'), t('撤销'), { danger: true }))) return
|
|
80
|
+
let token
|
|
81
|
+
try { token = await requestPasskeyGate('arbitrator_' + action, { user_id: userId }) }
|
|
82
|
+
catch (e) { if (typeof toast$ === 'function') toast$((e && e.message ? e.message + ' — ' : '') + t('需先注册 Passkey'), 'error'); return }
|
|
83
|
+
const res = await POST('/admin/arbitrators/' + userId + '/' + action, { webauthn_token: token })
|
|
84
|
+
if (res.error) { if (typeof toast$ === 'function') toast$(res.error, 'error'); return }
|
|
85
|
+
if (typeof toast$ === 'function') toast$(t('操作成功'), 'success')
|
|
86
|
+
window.arbAdminHydrate()
|
|
87
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// PR-E:仲裁能力入口/按钮跟随后端 can_arbitrate(= active arbitrator_whitelist),不再看 user.role。
|
|
2
|
+
// 前端仅 UX 跟随;真正的安全边界在后端(isEligibleArbitrator + assigned/claim + COI)。boot 时拉一次,
|
|
3
|
+
// suspended/revoked/role-only → can_arbitrate=false → 不显示入口/按钮;whitelist-only(role=buyer)→ true。
|
|
4
|
+
window.arbEntryHydrate = async () => {
|
|
5
|
+
const before = state.canArbitrate // PR-F fix:入口不 await(不阻塞 boot),状态回来若首次变 true → 重渲染当前页,让首屏也出现入口
|
|
6
|
+
try { const s = await GET('/arbitrator/status'); state.canArbitrate = !!(s && s.can_arbitrate); state.arbitratorStatus = (s && s.arbitrator_status) || 'none' }
|
|
7
|
+
catch { state.canArbitrate = false; state.arbitratorStatus = 'none' } // active / suspended / revoked / none
|
|
8
|
+
if (state.canArbitrate && !before && typeof route === 'function') route() // 仅"变为可仲裁"时重渲染;非仲裁员(恒 false)不触发
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// 缓交收口通知模板(B4)。UI ONLY —— 提醒/到期/转正式全在后端 cron/路由。
|
|
2
|
+
;(function () {
|
|
3
|
+
const S = window._notifSub
|
|
4
|
+
const P = (emoji, titleZh, bodyZh) => (p) => ({ title: emoji + ' ' + t(titleZh), body: S(t(bodyZh), p) })
|
|
5
|
+
Object.assign(window.NOTIF_TEMPLATES, {
|
|
6
|
+
deferral_expiring_soon: P('⏰', '保证金缓交即将到期', '你的缓交资格将于 {expires} 到期。请在到期前缴纳履约保证金转正式(设置页-直付履约保证金),否则宽限期后直付资格将关闭。'),
|
|
7
|
+
deferral_expired: P('🚫', '保证金缓交已到期', '缓交资格已到期。若未缴纳保证金,直付资格已关闭(缴纳并经运营确认后可重新开通);在途订单不受影响,请正常履约完成。'),
|
|
8
|
+
})
|
|
9
|
+
})()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// 保证金退出退还 UI(B2)。UI ONLY —— blockers/冷静期/凭据/复核全在后端。
|
|
2
|
+
// 经 app-bond-ui.js 的两个 hook 注入:卖家卡尾 bondRefundBlock(s) / admin 行尾 bondAdmRefundActions(d)。
|
|
3
|
+
;(function () {
|
|
4
|
+
const S = window._notifSub
|
|
5
|
+
const P = (emoji, titleZh, bodyZh) => (p) => ({ title: emoji + ' ' + t(titleZh), body: S(t(bodyZh), p) })
|
|
6
|
+
Object.assign(window.NOTIF_TEMPLATES, {
|
|
7
|
+
bond_refund_requested: P('↩️', '保证金退出申请待处理', '卖家 {seller} 申请退还履约保证金(冷静期 {days} 天,期间其直付资格已暂停)。冷静期满且复核无未了结责任后,场外退还并在 admin 后台记录执行。'),
|
|
8
|
+
bond_refund_executed: P('✅', '履约保证金已退还', '你的保证金已在协议外退还并记录(凭据:{evidence})。直付资格随保证金退出关闭;重新缴纳后可再次开通。'),
|
|
9
|
+
})
|
|
10
|
+
const BLOCKER_LABEL = () => ({
|
|
11
|
+
OPEN_DIRECT_PAY_ORDERS: t('有在途直付订单'), OPEN_CANCEL_REFUND_HANDSHAKE: t('取消退款握手进行中'),
|
|
12
|
+
OPEN_RETURN_FLOW: t('退货/售后流进行中'), UNPAID_PLATFORM_FEES: t('平台服务费欠费未结清'),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// 卖家卡尾:locked → 申请退出(或 blockers 列表);refunding → 冷静期说明 + 撤销
|
|
16
|
+
window.bondRefundBlock = (s) => {
|
|
17
|
+
const d = s.deposit
|
|
18
|
+
if (!d) return ''
|
|
19
|
+
if (d.status === 'locked' && s.refund) {
|
|
20
|
+
if (s.refund.can_request) {
|
|
21
|
+
return `<div style="margin-top:8px;border-top:1px dashed #e5e7eb;padding-top:8px">
|
|
22
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:11px" onclick="bondRefundRequest()">↩️ ${t('申请退出并退还保证金')}</button>
|
|
23
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('申请后进入')} ${s.refund.cooling_days} ${t('天冷静期,期间直付资格暂停;可随时撤销。')}</div></div>`
|
|
24
|
+
}
|
|
25
|
+
const L = BLOCKER_LABEL()
|
|
26
|
+
return `<div style="margin-top:8px;border-top:1px dashed #e5e7eb;padding-top:8px;font-size:11px;color:#92400e">
|
|
27
|
+
${t('暂不能申请退还 —— 有未了结的直付责任')}:${(s.refund.blockers || []).map(b => `${L[b.code] || b.code}${b.count ? `(${b.count})` : ''}`).join('、')}</div>`
|
|
28
|
+
}
|
|
29
|
+
if (d.status === 'refunding') {
|
|
30
|
+
return `<div style="margin-top:8px;border-top:1px dashed #e5e7eb;padding-top:8px;font-size:12px;color:#92400e">
|
|
31
|
+
⏳ ${t('退出申请处理中')}:${t('冷静期')} ${s.refund ? s.refund.cooling_days : 14} ${t('天')}(${t('自申请时起')});${t('期间直付资格暂停。冷静期满、复核无未了结责任后平台场外退还并记录。')}
|
|
32
|
+
<div style="margin-top:6px"><button class="btn btn-outline btn-sm" style="width:auto;font-size:11px" onclick="bondRefundCancel()">${t('撤销退出申请(恢复资格)')}</button></div></div>`
|
|
33
|
+
}
|
|
34
|
+
if (d.status === 'refunded') {
|
|
35
|
+
return `<div style="margin-top:8px;font-size:12px;color:#16a34a">✅ ${t('保证金已退还')}${d.refund_evidence_ref ? `(${t('凭据')} ${escHtml(d.refund_evidence_ref)})` : ''};${t('直付资格已关闭,重新缴纳后可再次开通。')}</div>`
|
|
36
|
+
}
|
|
37
|
+
return ''
|
|
38
|
+
}
|
|
39
|
+
window.bondRefundRequest = async () => {
|
|
40
|
+
if (typeof confirmModal === 'function' && !(await confirmModal(t('确认申请退出并退还保证金?冷静期内你的直付资格将暂停(不可接新直付单),可随时撤销。'), t('申请退出'), { danger: true }))) return
|
|
41
|
+
const r = await POST('/direct-receive/bond-refund-request', {})
|
|
42
|
+
if (r.error) { toast$(r.error_code === 'REFUND_BLOCKED' ? t('有未了结的直付责任,暂不能申请退还') : r.error, 'error'); window.bondHydrateSeller(); return }
|
|
43
|
+
toast$(t('退出申请已提交,进入冷静期'), 'success'); window.bondHydrateSeller()
|
|
44
|
+
}
|
|
45
|
+
window.bondRefundCancel = async () => {
|
|
46
|
+
const r = await POST('/direct-receive/bond-refund-request/cancel', {})
|
|
47
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
48
|
+
toast$(t('已撤销,直付资格已恢复'), 'success'); window.bondHydrateSeller()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// admin 行尾:refunding 行给"记录场外退还"按钮(Passkey;冷静期未满/复核有责任会被后端拒,明示)
|
|
52
|
+
window.bondAdmRefundActions = (d) => {
|
|
53
|
+
if (d.status !== 'refunding') return ''
|
|
54
|
+
return `<div style="margin-top:8px"><button class="btn btn-primary btn-sm" style="width:auto;font-size:11px" onclick="bondAdmExecuteRefund('${d.id}')">↩️ ${t('已场外退还,记录执行(Passkey)')}</button>
|
|
55
|
+
<span style="font-size:11px;color:#9ca3af;margin-left:6px">${t('冷静期未满或复核有未了结责任会被拒,属预期')}</span></div>`
|
|
56
|
+
}
|
|
57
|
+
window.bondAdmExecuteRefund = async (id) => {
|
|
58
|
+
const evidence = prompt(t('场外退还凭据(转账单号/链上 tx,必填)'), ''); if (!evidence || !evidence.trim()) return
|
|
59
|
+
let token
|
|
60
|
+
try { token = await requestPasskeyGate('direct_receive_bond_refund', { deposit_id: id, evidence_ref: evidence.trim() }) }
|
|
61
|
+
catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
|
|
62
|
+
const r = await POST(`/admin/direct-receive/deposits/${id}/execute-refund`, { evidence_ref: evidence.trim(), webauthn_token: token })
|
|
63
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
64
|
+
toast$(t('退还已记录,卖家已通知'), 'success'); window.bondAdmHydrate()
|
|
65
|
+
}
|
|
66
|
+
})()
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// 保证金罚没 UI(B3)。UI ONLY —— 口径校验/冷静期/幂等全在后端。人工铁律:提案→冷静期→ROOT+Passkey 执行。
|
|
2
|
+
// 注入:卖家卡 bondSlashNotice(s)(app-bond-ui 净零 hook);admin 队列页追加提案区(包装 bondAdmHydrate)。
|
|
3
|
+
;(function () {
|
|
4
|
+
const S = window._notifSub
|
|
5
|
+
const P = (emoji, titleZh, bodyZh) => (p) => ({ title: emoji + ' ' + t(titleZh), body: S(t(bodyZh), p) })
|
|
6
|
+
Object.assign(window.NOTIF_TEMPLATES, {
|
|
7
|
+
bond_slash_proposed: P('⚠️', '保证金罚没提案(待复核)', '因争议 {dispute} 裁定卖家责任,平台已发起保证金罚没提案。冷静期 {days} 天内如有异议请联系平台并提供依据;冷静期满后将复核执行(全额罚没,进入处罚金专户,平台不获益)。'),
|
|
8
|
+
bond_slash_cancelled: P('✅', '保证金罚没提案已撤销', '此前的罚没提案经复核已撤销,你的保证金不受影响。'),
|
|
9
|
+
bond_slash_executed: P('❌', '保证金已罚没', '依据争议 {dispute} 的卖家责任裁定,你的履约保证金已全额罚没(进入处罚金专户,平台不获益),直付资格已吊销。重新缴纳保证金并通过审核后可再次申请开通。'),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
// 卖家卡:待复核提案警示(冷静期=申诉窗)
|
|
13
|
+
window.bondSlashNotice = (s) => {
|
|
14
|
+
const p = s.pending_slash
|
|
15
|
+
if (!p) return ''
|
|
16
|
+
return `<div style="margin-top:8px;padding:8px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;font-size:12px;color:#991b1b">
|
|
17
|
+
⚠️ ${t('保证金罚没提案待复核')}:${t('依据争议')} ${escHtml(p.dispute_id)}${p.reason ? ` · ${escHtml(p.reason)}` : ''}<br>
|
|
18
|
+
${t('冷静期至')} ${fmtTime(p.cooling_until)} —— ${t('如有异议请在此期间联系平台并提供依据。')}</div>`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// admin:队列页追加"罚没提案"区(包装既有 hydrate;容器自建,不动 capped 文件)
|
|
22
|
+
const _origHydrate = window.bondAdmHydrate
|
|
23
|
+
window.bondAdmHydrate = async (status) => {
|
|
24
|
+
await _origHydrate(status)
|
|
25
|
+
const box = document.getElementById('bond-adm-box'); if (!box) return
|
|
26
|
+
let sec = document.getElementById('bond-slash-sec')
|
|
27
|
+
if (!sec) { box.insertAdjacentHTML('afterend', `<div id="bond-slash-sec" style="margin-top:16px"></div>`); sec = document.getElementById('bond-slash-sec') }
|
|
28
|
+
const r = await GET('/admin/direct-receive/bond-slash').catch(() => null)
|
|
29
|
+
const items = (r && r.proposals) || []
|
|
30
|
+
sec.innerHTML = `<h2 style="font-size:15px;font-weight:700;margin-bottom:8px">⚖️ ${t('罚没提案')}(${t('人工铁律:提案→冷静期→Passkey 执行,绝不自动')})</h2>
|
|
31
|
+
<div style="font-size:11px;color:#6b7280;margin-bottom:8px">${t('仅「仲裁裁定卖家责的直付争议」可提案(refund_buyer / partial_refund);v1 全额罚没,进处罚金专户(只进不出),平台不获益。对 locked 行用下方表单提案。')}</div>
|
|
32
|
+
<div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap">
|
|
33
|
+
<input class="form-control" id="bslash-dep" placeholder="${t('deposit_id')}" style="flex:1;min-width:120px;font-size:12px">
|
|
34
|
+
<input class="form-control" id="bslash-dsp" placeholder="${t('依据 dispute_id')}" style="flex:1;min-width:120px;font-size:12px">
|
|
35
|
+
<input class="form-control" id="bslash-note" placeholder="${t('说明(卖家可见,可选)')}" style="flex:2;min-width:160px;font-size:12px">
|
|
36
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:11px" onclick="bondSlashPropose()">${t('发起提案')}</button>
|
|
37
|
+
</div>
|
|
38
|
+
${items.length === 0 ? `<div class="alert alert-info">${t('暂无罚没提案')}</div>` : items.map(p => `
|
|
39
|
+
<div class="card" style="font-size:12px">
|
|
40
|
+
<div style="display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap">
|
|
41
|
+
<div><strong>${escHtml(p.seller_name || p.seller_id)}</strong> · ${t('依据争议')} ${escHtml(p.dispute_id)} · ${t('存款')} ${escHtml(p.deposit_id)}</div>
|
|
42
|
+
<div><strong>${({ proposed: t('待执行(冷静期)'), executed: t('已执行'), cancelled: t('已撤销') })[p.status] || p.status}</strong></div>
|
|
43
|
+
</div>
|
|
44
|
+
<div style="color:#6b7280;margin-top:4px">${p.reason ? escHtml(p.reason) + ' · ' : ''}${t('冷静期至')} ${fmtTime(p.cooling_until)} · ${fmtTime(p.proposed_at)}</div>
|
|
45
|
+
${p.status === 'proposed' ? `<div style="display:flex;gap:8px;margin-top:8px">
|
|
46
|
+
<button class="btn btn-primary btn-sm" style="width:auto;font-size:11px" onclick="bondSlashExecute('${p.id}')">${t('执行罚没(Passkey;冷静期未满会被拒)')}</button>
|
|
47
|
+
<button class="btn btn-outline btn-sm" style="width:auto;font-size:11px" onclick="bondSlashCancel('${p.id}')">${t('撤销提案')}</button>
|
|
48
|
+
</div>` : ''}
|
|
49
|
+
</div>`).join('')}`
|
|
50
|
+
}
|
|
51
|
+
window.bondSlashPropose = async () => {
|
|
52
|
+
const dep = (document.getElementById('bslash-dep')?.value || '').trim()
|
|
53
|
+
const dsp = (document.getElementById('bslash-dsp')?.value || '').trim()
|
|
54
|
+
if (!dep || !dsp) return void toast$(t('须提供 deposit_id 与依据 dispute_id'), 'error')
|
|
55
|
+
const r = await POST('/admin/direct-receive/bond-slash/propose', { deposit_id: dep, dispute_id: dsp, reason: (document.getElementById('bslash-note')?.value || '').trim() || undefined })
|
|
56
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
57
|
+
toast$(t('提案已发起,卖家已通知(冷静期=申诉窗)'), 'success'); window.bondAdmHydrate()
|
|
58
|
+
}
|
|
59
|
+
window.bondSlashExecute = async (id) => {
|
|
60
|
+
if (typeof confirmModal === 'function' && !(await confirmModal(t('确认执行罚没?保证金将全额进入处罚金专户(不可逆),卖家直付资格吊销。'), t('执行罚没'), { danger: true }))) return
|
|
61
|
+
let token
|
|
62
|
+
try { token = await requestPasskeyGate('direct_pay_bond_slash', { proposal_id: id }) }
|
|
63
|
+
catch (e) { if (window.dpPromptRegisterPasskey) await window.dpPromptRegisterPasskey(e); return }
|
|
64
|
+
const r = await POST(`/admin/direct-receive/bond-slash/${id}/execute`, { webauthn_token: token })
|
|
65
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
66
|
+
toast$(t('罚没已执行,卖家已通知'), 'success'); window.bondAdmHydrate()
|
|
67
|
+
}
|
|
68
|
+
window.bondSlashCancel = async (id) => {
|
|
69
|
+
const note = prompt(t('撤销说明(卖家可见,可空)'), '') ?? ''
|
|
70
|
+
const r = await POST(`/admin/direct-receive/bond-slash/${id}/cancel`, { note })
|
|
71
|
+
if (r.error) return void toast$(r.error, 'error')
|
|
72
|
+
toast$(t('提案已撤销'), 'success'); window.bondAdmHydrate()
|
|
73
|
+
}
|
|
74
|
+
})()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 保证金条款 + 多币种账户选择 UI(Lock B 放行配套)。UI ONLY —— 条款版本/账户校验在后端(428 TERMS_NOT_AGREED)。
|
|
2
|
+
;(function () {
|
|
3
|
+
// 账户单选(多币种;admin 在 #admin/platform-receive 维护)。选中项凭据/币种由后端按账户推导。
|
|
4
|
+
window.bondAccountSelector = (s) => {
|
|
5
|
+
const accs = s.payment_accounts || []
|
|
6
|
+
if (!accs.length) return t('暂无收款方式,请联系平台')
|
|
7
|
+
return accs.map((a, i) => `<label style="display:flex;align-items:flex-start;gap:8px;margin-bottom:6px;cursor:pointer">
|
|
8
|
+
<input type="radio" name="bond-acc" value="${escHtml(a.id)}" ${i === 0 ? 'checked' : ''} style="margin-top:3px">
|
|
9
|
+
<span style="font-size:12px"><strong>${escHtml(a.label || a.method || '')}</strong> · ${escHtml(a.currency || '')}<br><span style="color:#6b7280">${escHtml(a.instruction || '')}</span></span>
|
|
10
|
+
</label>`).join('')
|
|
11
|
+
}
|
|
12
|
+
// 条款(折叠展示全文)+ 强制勾选;版本存全局供提交带上(后端精确匹配)
|
|
13
|
+
window.bondTermsBlock = (s) => {
|
|
14
|
+
if (!s.terms) return ''
|
|
15
|
+
window._bondTermsVersion = s.terms.version
|
|
16
|
+
const text = (window._lang === 'en' ? s.terms.en : s.terms.zh) || s.terms.zh
|
|
17
|
+
return `<details style="margin-bottom:8px"><summary style="cursor:pointer;font-size:12px;font-weight:600">📜 ${t('保证金条款')}(${escHtml(s.terms.version)})</summary>
|
|
18
|
+
<div style="font-size:11px;color:#4b5563;white-space:pre-wrap;line-height:1.7;padding:8px;background:#f9fafb;border-radius:8px;margin-top:6px">${escHtml(text)}</div></details>
|
|
19
|
+
<label style="display:flex;align-items:center;gap:8px;font-size:12px;color:#374151;margin-bottom:8px;cursor:pointer">
|
|
20
|
+
<input type="checkbox" id="bond-terms-agree" style="width:16px;height:16px">${t('我已阅读并同意当前版本保证金条款(缴纳前必须同意)')}
|
|
21
|
+
</label>`
|
|
22
|
+
}
|
|
23
|
+
})()
|