feat: improve gateway compatibility and complete i18n cleanup

This commit is contained in:
晴天
2026-04-01 15:06:25 +08:00
parent 57b8b25946
commit b427a6b000
59 changed files with 6830 additions and 964 deletions

View File

@@ -4,6 +4,8 @@
* 自动注入当前页面上下文到 AI 助手会话
*/
import { t } from '../lib/i18n.js'
const BOT_ICON = '<svg viewBox="0 0 24 24"><path d="M12 8V4H8"/><rect x="5" y="8" width="14" height="12" rx="2"/><path d="M9 13h0"/><path d="M15 13h0"/><path d="M10 17h4"/></svg>'
const POS_KEY = 'clawpanel-fab-pos'
@@ -41,7 +43,7 @@ export function initAIFab() {
export function openAIDrawerWithError(errorCtx) {
sessionStorage.setItem('assistant-error-context', JSON.stringify({
scene: errorCtx.scene || '',
title: errorCtx.title || '操作失败',
title: errorCtx.title || t('common.operationFailed'),
hint: errorCtx.hint || '',
error: truncate(errorCtx.error || '', 3000),
ts: Date.now(),
@@ -53,7 +55,7 @@ export function openAIDrawerWithError(errorCtx) {
_fab.el.classList.add('has-error')
} else {
import('./toast.js')
.then(({ toast }) => toast('已保存诊断上下文,可从侧边栏进入「晴辰助手」继续处理', 'info'))
.then(({ toast }) => toast(t('assistant.contextSavedToast', { assistant: t('sidebar.assistant') }), 'info'))
.catch(() => {})
}
} else {
@@ -71,7 +73,7 @@ function truncate(str, max) {
function createFab() {
const fab = document.createElement('button')
fab.className = 'ai-fab'
fab.title = 'AI 助手'
fab.title = t('sidebar.assistant')
fab.innerHTML = BOT_ICON
document.body.appendChild(fab)
@@ -228,7 +230,7 @@ function showDragHintOnce(el) {
if (!el || localStorage.getItem(HINT_KEY)) return
const tip = document.createElement('div')
tip.className = 'ai-fab-hint'
tip.textContent = '长按可拖动'
tip.textContent = t('assistant.dragHint')
el.appendChild(tip)
localStorage.setItem(HINT_KEY, '1')
setTimeout(() => tip.remove(), 4000)

View File

@@ -10,6 +10,8 @@
* 6. 不在聊天/助手页面时触发(避免打断对话)
*/
import { t } from '../lib/i18n.js'
const KEYS = {
firstOpen: 'clawpanel_first_open',
openCount: 'clawpanel_open_count',
@@ -66,23 +68,23 @@ export function tryShowEngagement() {
_showing = true
localStorage.setItem(KEYS.lastShown, String(Date.now()))
const shareText = '推荐一个开源的 OpenClaw 管理面板 — ClawPanel一键搭建、便捷管理模型和 Agent还内置 AI 助手帮你排查问题小白也能轻松上手https://claw.qt.cool'
const shareText = t('engagement.shareText')
const overlay = document.createElement('div')
overlay.className = 'engage-overlay'
overlay.innerHTML = `
<div class="engage-modal">
<button class="engage-close" title="关闭">&times;</button>
<button class="engage-close" title="${t('common.close')}">&times;</button>
<div class="engage-header">
<div class="engage-icon">
<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
</div>
<div class="engage-title">感谢你使用 ClawPanel</div>
<div class="engage-title">${t('engagement.title')}</div>
</div>
<div class="engage-message">
ClawPanel 是一个<strong>完全开源、免费</strong>的项目,由晴辰云团队专职维护、持续更新。如果它帮到了你,对我们最大的鼓励就是:
${t('engagement.message')}
</div>
<div class="engage-actions-grid">
@@ -91,8 +93,8 @@ export function tryShowEngagement() {
<svg viewBox="0 0 24 24" width="22" height="22" fill="#f59e0b" stroke="#f59e0b" stroke-width="1"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</div>
<div class="engage-action-text">
<div class="engage-action-title">GitHub Star</div>
<div class="engage-action-desc">点个 Star 是最直接的支持</div>
<div class="engage-action-title">${t('engagement.starTitle')}</div>
<div class="engage-action-desc">${t('engagement.starDesc')}</div>
</div>
</a>
<div class="engage-action-card engage-action-share" data-action="copy-share">
@@ -100,34 +102,34 @@ export function tryShowEngagement() {
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
</div>
<div class="engage-action-text">
<div class="engage-action-title">分享给朋友</div>
<div class="engage-action-desc">复制推荐文案,让更多人知道</div>
<div class="engage-action-title">${t('engagement.shareTitle')}</div>
<div class="engage-action-desc">${t('engagement.shareDesc')}</div>
</div>
</div>
</div>
<div class="engage-section-label">扫码加入社区交流群,第一时间获取更新和帮助</div>
<div class="engage-section-label">${t('engagement.communityLabel')}</div>
<div class="engage-qrcodes">
<a class="engage-qr-item" href="https://qt.cool/c/OpenClaw" target="_blank" rel="noopener">
<img src="/images/OpenClaw-QQ.png" alt="QQ 交流群" />
<div class="engage-qr-label">QQ 群</div>
<img src="/images/OpenClaw-QQ.png" alt="${t('engagement.qqAlt')}" />
<div class="engage-qr-label">${t('engagement.qqLabel')}</div>
</a>
<a class="engage-qr-item" href="https://qt.cool/c/OpenClawWx" target="_blank" rel="noopener">
<img src="/images/OpenClawWx.png" alt="微信交流群" />
<div class="engage-qr-label">微信群</div>
<img src="/images/OpenClawWx.png" alt="${t('engagement.wechatAlt')}" />
<div class="engage-qr-label">${t('engagement.wechatLabel')}</div>
</a>
<a class="engage-qr-item" href="https://qt.cool/c/OpenClawDY" target="_blank" rel="noopener">
<img src="/images/OpenClaw-DY.png" alt="抖音交流群" />
<div class="engage-qr-label">抖音群</div>
<img src="/images/OpenClaw-DY.png" alt="${t('engagement.douyinAlt')}" />
<div class="engage-qr-label">${t('engagement.douyinLabel')}</div>
</a>
<a class="engage-qr-item" href="https://qt.cool/c/feishu" target="_blank" rel="noopener">
<img src="https://qt.cool/c/feishu/qr.png" alt="飞书交流群" />
<div class="engage-qr-label">飞书群</div>
<img src="https://qt.cool/c/feishu/qr.png" alt="${t('engagement.feishuAlt')}" />
<div class="engage-qr-label">${t('engagement.feishuLabel')}</div>
</a>
</div>
<div class="engage-footer">
<span class="engage-today-dismiss">今日不再弹窗</span>
<span class="engage-today-dismiss">${t('engagement.dismissToday')}</span>
</div>
</div>
`
@@ -146,7 +148,7 @@ export function tryShowEngagement() {
overlay.querySelector('[data-action="copy-share"]').onclick = () => {
navigator.clipboard.writeText(shareText).then(() => {
const desc = overlay.querySelector('[data-action="copy-share"] .engage-action-desc')
if (desc) { desc.textContent = '✅ 已复制,去分享吧!'; setTimeout(() => { desc.textContent = '复制推荐文案,让更多人知道' }, 2000) }
if (desc) { desc.textContent = t('engagement.shareCopied'); setTimeout(() => { desc.textContent = t('engagement.shareDesc') }, 2000) }
})
}
}

View File

@@ -2,6 +2,8 @@
* Modal 弹窗组件
*/
import { t } from '../lib/i18n.js'
// 转义 HTML 属性值,防止双引号等字符破坏 HTML 结构
function escapeAttr(str) {
if (!str) return ''
@@ -24,11 +26,11 @@ export function showConfirm(message) {
overlay.className = 'modal-overlay'
overlay.innerHTML = `
<div class="modal" style="max-width:400px">
<div class="modal-title">确认操作</div>
<div class="modal-title">${t('common.confirmAction')}</div>
<div style="font-size:var(--font-size-sm);color:var(--text-secondary);white-space:pre-wrap;line-height:1.6">${escapeAttr(message)}</div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="cancel">取消</button>
<button class="btn btn-danger btn-sm" data-action="confirm">确定</button>
<button class="btn btn-secondary btn-sm" data-action="cancel">${t('common.cancel')}</button>
<button class="btn btn-danger btn-sm" data-action="confirm">${t('common.confirm')}</button>
</div>
</div>
`
@@ -91,8 +93,8 @@ export function showModal({ title, fields, onConfirm }) {
<div class="modal-title">${title}</div>
${fieldHtml}
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="cancel">取消</button>
<button class="btn btn-primary btn-sm" data-action="confirm">确定</button>
<button class="btn btn-secondary btn-sm" data-action="cancel">${t('common.cancel')}</button>
<button class="btn btn-primary btn-sm" data-action="confirm">${t('common.confirm')}</button>
</div>
</div>
`
@@ -156,7 +158,7 @@ export function showContentModal({ title, content, buttons = [], width = 480 })
<div class="modal-title">${title}</div>
<div class="modal-content-body">${content}</div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="cancel">取消</button>
<button class="btn btn-secondary btn-sm" data-action="cancel">${t('common.cancel')}</button>
${btnsHtml}
</div>
</div>
@@ -190,14 +192,14 @@ export function showUpgradeModal(title) {
overlay.className = 'modal-overlay'
overlay.innerHTML = `
<div class="modal" style="max-width:520px">
<div class="modal-title">${title || '升级 OpenClaw'}</div>
<div class="modal-title">${title || t('common.upgradeOpenClaw')}</div>
<div class="upgrade-progress-wrap">
<div class="upgrade-progress-bar"><div class="upgrade-progress-fill" style="width:0%"></div></div>
<div class="upgrade-progress-text">准备中...</div>
<div class="upgrade-progress-text">${t('common.preparing')}</div>
</div>
<div class="upgrade-log-box"></div>
<div class="modal-actions">
<button class="btn btn-secondary btn-sm" data-action="close">关闭</button>
<button class="btn btn-secondary btn-sm" data-action="close">${t('common.close')}</button>
</div>
</div>
`
@@ -237,7 +239,7 @@ export function showUpgradeModal(title) {
_taskBar.className = 'upgrade-task-bar'
_taskBar.innerHTML = `
<span class="upgrade-task-bar-text">${text.textContent}</span>
<button class="btn btn-sm upgrade-task-bar-open">查看详情</button>
<button class="btn btn-sm upgrade-task-bar-open">${t('common.viewDetails')}</button>
<button class="btn btn-sm btn-ghost upgrade-task-bar-dismiss">×</button>
`
_taskBar.querySelector('.upgrade-task-bar-open').onclick = reopenModal
@@ -276,16 +278,16 @@ export function showUpgradeModal(title) {
setProgress(pct) {
fill.style.width = pct + '%'
let statusText
if (pct >= 100) statusText = '完成'
else if (pct >= 75) statusText = '正在安装...'
else if (pct >= 30) statusText = '正在下载依赖...'
else statusText = '准备中...'
if (pct >= 100) statusText = t('common.completed')
else if (pct >= 75) statusText = t('common.installingProgress')
else if (pct >= 30) statusText = t('common.downloadingDependencies')
else statusText = t('common.preparing')
text.textContent = statusText
updateTaskBar(statusText)
},
setDone(msg) {
_finished = true
text.textContent = msg || '升级完成'
text.textContent = msg || t('common.upgradeCompleted')
fill.style.width = '100%'
fill.classList.add('done')
if (_taskBar) { _taskBar.remove(); _taskBar = null }
@@ -293,11 +295,11 @@ export function showUpgradeModal(title) {
},
setError(msg) {
_finished = true
text.textContent = msg || '升级失败'
text.textContent = msg || t('common.upgradeFailed')
fill.classList.add('error')
if (_taskBar) {
const span = _taskBar.querySelector('.upgrade-task-bar-text')
if (span) { span.textContent = msg || '升级失败'; span.style.color = 'var(--error)' }
if (span) { span.textContent = msg || t('common.upgradeFailed'); span.style.color = 'var(--error)' }
}
closeBtn.focus()
},

View File

@@ -139,8 +139,8 @@ export function renderSidebar(el) {
<img src="/images/logo.png" alt="ClawPanel">
</div>
<span class="sidebar-title">ClawPanel</span>
<button class="sidebar-collapse-btn" id="btn-sidebar-collapse" title="折叠/展开">${collapsed ? '»' : '«'}</button>
<button class="sidebar-close-btn" id="btn-sidebar-close" title="关闭菜单">&times;</button>
<button class="sidebar-collapse-btn" id="btn-sidebar-collapse" title="${t('sidebar.collapse')}">${collapsed ? '»' : '«'}</button>
<button class="sidebar-close-btn" id="btn-sidebar-close" title="${t('sidebar.closeMenu')}">&times;</button>
</div>
${showSwitcher ? `<div class="instance-switcher" id="instance-switcher">
<button class="instance-current" id="btn-instance-toggle">