mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-06 20:02:49 +08:00
feat: improve channel and setup guidance flows
This commit is contained in:
@@ -49,6 +49,7 @@ export default {
|
||||
channelsTitle: _('渠道绑定', 'Channel Bindings'),
|
||||
channelsDesc: _('管理此 Agent 绑定的消息渠道', 'Manage message channels bound to this Agent.'),
|
||||
addBinding: _('+ 添加绑定', '+ Add Binding'),
|
||||
manageChannels: _('去消息渠道配置', 'Open Channels Page'),
|
||||
noBindings: _('此 Agent 尚未绑定任何渠道', 'This Agent has no bindings yet.'),
|
||||
removeBinding: _('解绑', 'Unbind'),
|
||||
bindingRemoved: _('已解除绑定', 'Binding removed'),
|
||||
|
||||
@@ -240,6 +240,13 @@ export default {
|
||||
operations: _('操作', 'Operations'),
|
||||
setup: _('接入', 'Setup'),
|
||||
close: _('关闭', 'Close', '關閉'),
|
||||
manualCommands: _('手动命令', 'Manual Commands', '手動命令'),
|
||||
manualCommandsHint: _('如果面板自动安装失败,可以复制下面的命令到终端手动执行。', 'If panel-based installation fails, copy the commands below and run them manually in your terminal.', '如果面板自動安裝失敗,可以複製下面的命令到終端手動執行。'),
|
||||
manualInstallCommand: _('手动安装命令', 'Manual Install Command', '手動安裝命令'),
|
||||
manualInstallHint: _('用于手动安装 {platform} 对应插件。', 'Use this command to manually install the plugin for {platform}.', '用於手動安裝 {platform} 對應外掛。'),
|
||||
manualLoginCommand: _('手动登录命令', 'Manual Login Command', '手動登入命令'),
|
||||
manualLoginHint: _('插件安装完成后,可在终端执行此命令继续登录流程。', 'After plugin installation completes, run this command in your terminal to continue the login flow.', '外掛安裝完成後,可在終端執行此命令繼續登入流程。'),
|
||||
copyCommandFailed: _('复制命令失败', 'Failed to copy command', '複製命令失敗'),
|
||||
pluginStatusFailed: _('无法获取插件状态', 'Failed to get plugin status', '無法取得外掛狀態'),
|
||||
pluginInstalled: _('已安装', 'Installed', '已安裝', 'プラグインインストール済み'),
|
||||
version: _('版本', 'Version'),
|
||||
|
||||
@@ -5,6 +5,7 @@ export default {
|
||||
desc: _('安装和配置 OpenClaw', 'Install and configure OpenClaw', '安裝和設定 OpenClaw', 'OpenClaw のインストールと設定', 'OpenClaw 설치 및 설정', 'Cài đặt và cấu hình OpenClaw', 'Instalar y configurar OpenClaw', 'Instalar e configurar OpenClaw', 'Установка и настройка OpenClaw', 'Installer et configurer OpenClaw', 'OpenClaw installieren und konfigurieren'),
|
||||
headerTitle: _('欢迎使用 ClawPanel', 'Welcome to ClawPanel', '', 'ClawPanel へようこそ', 'ClawPanel에 오신 것을 환영합니다', 'Chào mừng đến ClawPanel', 'Bienvenido a ClawPanel', 'Bem-vindo ao ClawPanel', 'Добро пожаловать в ClawPanel', 'Bienvenue sur ClawPanel', 'Willkommen bei ClawPanel'),
|
||||
headerDesc: _('OpenClaw AI Agent 框架的桌面管理面板', 'Desktop management panel for OpenClaw AI Agent framework', '', 'OpenClaw AI Agent フレームワークのデスクトップ管理パネル', 'OpenClaw AI Agent 프레임워크의 데스크톱 관리 패널'),
|
||||
officialWebsite: _('官网', 'Official Website', '官網', '公式サイト', '공식 웹사이트', 'Trang chính thức', 'Sitio oficial', 'Site oficial', 'Официальный сайт', 'Site officiel', 'Offizielle Website'),
|
||||
recheck: _('重新检测', 'Re-detect', '重新檢測', '再検出', '재감지', 'Kiểm tra lại', 'Verificar de nuevo', 'Verificar novamente', 'Проверить снова', 'Revérifier', 'Erneut prüfen'),
|
||||
stepNode: _('Node.js 环境', 'Node.js Environment', 'Node.js 環境', 'Node.js 環境', 'Node.js 환경', 'Môi trường Node.js', 'Entorno Node.js', 'Ambiente Node.js', 'Среда Node.js', 'Environnement Node.js', 'Node.js-Umgebung'),
|
||||
installed: _('已安装', 'Installed', '已安裝', 'インストール済み', '설치됨', 'Đã cài đặt', 'Instalado', 'Instalado', 'Установлено', 'Installé', 'Installiert'),
|
||||
@@ -149,6 +150,7 @@ export default {
|
||||
restoreFailed: _('恢复失败: {err}', 'Restore failed: {err}', '恢復失敗: {err}'),
|
||||
initializing: _('初始化中...', 'Initializing...'),
|
||||
configCreated: _('配置文件已创建', 'Config file created', '設定檔案已建立'),
|
||||
configRestored: _('已从备份恢复配置文件', 'Config restored from backup', '已從備份還原設定檔案'),
|
||||
configExists: _('配置文件已存在', 'Config file already exists', '設定檔案已存在'),
|
||||
initFailed: _('初始化失败: {err}', 'Init failed: {err}', '初始化失敗: {err}'),
|
||||
scanning: _('扫描中...', 'Scanning...', '掃描中...'),
|
||||
|
||||
@@ -7,12 +7,21 @@ import { toast } from '../components/toast.js'
|
||||
import { showConfirm } from '../components/modal.js'
|
||||
import { CHANNEL_LABELS } from '../lib/channel-labels.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { navigate } from '../router.js'
|
||||
|
||||
function esc(str) {
|
||||
if (!str) return ''
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
}
|
||||
|
||||
function openChannelsBindingPage(agentId) {
|
||||
const params = new URLSearchParams()
|
||||
params.set('tab', 'agents')
|
||||
params.set('agent', agentId || 'main')
|
||||
params.set('action', 'bind')
|
||||
navigate(`/channels?${params.toString()}`)
|
||||
}
|
||||
|
||||
export async function render() {
|
||||
const params = new URLSearchParams(location.hash.split('?')[1] || '')
|
||||
const agentId = params.get('id') || 'main'
|
||||
@@ -568,10 +577,6 @@ async function openFileEditor(container, state, name, isNew = false) {
|
||||
async function renderChannels(container, state) {
|
||||
const bindings = state.detail?.bindings || []
|
||||
|
||||
// 获取已配置的渠道
|
||||
let platforms = []
|
||||
try { platforms = await api.listConfiguredPlatforms() } catch {}
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="agent-channels-section">
|
||||
<div class="agent-section-header">
|
||||
@@ -579,7 +584,7 @@ async function renderChannels(container, state) {
|
||||
<h3 class="agent-section-title">${t('agentDetail.channelsTitle')}</h3>
|
||||
<p class="agent-section-desc">${t('agentDetail.channelsDesc')}</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" id="btn-add-binding">${t('agentDetail.addBinding')}</button>
|
||||
<button class="btn btn-sm btn-primary" id="btn-add-binding">${t('agentDetail.manageChannels')}</button>
|
||||
</div>
|
||||
<div id="agent-bindings-list"></div>
|
||||
</div>
|
||||
@@ -588,7 +593,7 @@ async function renderChannels(container, state) {
|
||||
renderBindingsList(container, state, bindings)
|
||||
|
||||
container.querySelector('#btn-add-binding').addEventListener('click', () => {
|
||||
showAddBindingDialog(container, state, platforms)
|
||||
openChannelsBindingPage(state.agentId)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -627,7 +632,6 @@ function renderBindingsList(container, state, bindings) {
|
||||
try {
|
||||
await api.deleteAgentBinding(state.agentId, channel, account, binding?.match || null)
|
||||
toast(t('agentDetail.bindingRemoved'), 'success')
|
||||
// 刷新
|
||||
invalidate('get_agent_detail')
|
||||
state.detail = await api.getAgentDetail(state.agentId)
|
||||
renderBindingsList(container, state, state.detail.bindings || [])
|
||||
@@ -636,62 +640,3 @@ function renderBindingsList(container, state, bindings) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function showAddBindingDialog(container, state, platforms) {
|
||||
const overlay = document.createElement('div')
|
||||
overlay.className = 'modal-overlay'
|
||||
|
||||
// 构建渠道选项:已配置的渠道 + 所有已知渠道
|
||||
const channels = new Set()
|
||||
for (const p of platforms) {
|
||||
if (p.platform || p.id) channels.add(p.platform || p.id)
|
||||
}
|
||||
// 确保常用渠道在列表中
|
||||
for (const key of Object.keys(CHANNEL_LABELS)) channels.add(key)
|
||||
|
||||
const channelOptions = [...channels].map(ch =>
|
||||
`<option value="${esc(ch)}">${esc(CHANNEL_LABELS[ch] || ch)}</option>`
|
||||
).join('')
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="modal" style="max-width:400px">
|
||||
<div class="modal-title">${t('agentDetail.addBinding')}</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">${t('agentDetail.selectChannel')}</label>
|
||||
<select class="form-input" id="bind-channel">${channelOptions}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">${t('agentDetail.accountOptional')}</label>
|
||||
<input class="form-input" id="bind-account" placeholder="${t('agentDetail.accountOptionalPlaceholder')}">
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<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>
|
||||
`
|
||||
document.body.appendChild(overlay)
|
||||
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove() })
|
||||
overlay.querySelector('[data-action="cancel"]').onclick = () => overlay.remove()
|
||||
overlay.querySelector('[data-action="confirm"]').onclick = async () => {
|
||||
const channel = overlay.querySelector('#bind-channel').value
|
||||
const account = overlay.querySelector('#bind-account').value.trim() || null
|
||||
if (!channel) return
|
||||
try {
|
||||
await api.saveAgentBinding(state.agentId, channel, account)
|
||||
toast(t('agentDetail.bindingAdded'), 'success')
|
||||
overlay.remove()
|
||||
invalidate('get_agent_detail')
|
||||
state.detail = await api.getAgentDetail(state.agentId)
|
||||
renderBindingsList(container, state, state.detail.bindings || [])
|
||||
} catch (e) {
|
||||
toast(t('agentDetail.bindingFailed') + ': ' + e, 'error')
|
||||
overlay.remove()
|
||||
}
|
||||
}
|
||||
overlay.querySelector('[data-action="confirm"]').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') overlay.querySelector('[data-action="confirm"]').click()
|
||||
if (e.key === 'Escape') overlay.remove()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -245,6 +245,26 @@ const PLATFORM_REGISTRY = {
|
||||
},
|
||||
}
|
||||
|
||||
function parseChannelsRouteIntent() {
|
||||
const params = new URLSearchParams(location.hash.split('?')[1] || '')
|
||||
return {
|
||||
tab: params.get('tab') || '',
|
||||
action: params.get('action') || '',
|
||||
agentId: params.get('agent') || '',
|
||||
}
|
||||
}
|
||||
|
||||
function activateChannelsPageTab(page, key = 'channels') {
|
||||
const activeKey = key === 'agents' ? 'agents' : 'channels'
|
||||
page.querySelectorAll('#channels-page-tabs .tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.chTab === activeKey)
|
||||
})
|
||||
const listEl = page.querySelector('#channels-panel-list')
|
||||
const agentsEl = page.querySelector('#channels-panel-agents')
|
||||
if (listEl) listEl.style.display = activeKey === 'channels' ? '' : 'none'
|
||||
if (agentsEl) agentsEl.style.display = activeKey === 'agents' ? '' : 'none'
|
||||
}
|
||||
|
||||
// ── 页面生命周期 ──
|
||||
|
||||
export async function render() {
|
||||
@@ -275,7 +295,14 @@ export async function render() {
|
||||
|
||||
bindChannelTabs(page)
|
||||
|
||||
const state = { configured: [], bindings: [], agents: [] }
|
||||
const state = {
|
||||
configured: [],
|
||||
bindings: [],
|
||||
agents: [],
|
||||
routeIntent: parseChannelsRouteIntent(),
|
||||
routeIntentConsumed: false,
|
||||
routeIntentHintShown: false,
|
||||
}
|
||||
await loadPlatforms(page, state)
|
||||
|
||||
return page
|
||||
@@ -284,12 +311,7 @@ export async function render() {
|
||||
function bindChannelTabs(page) {
|
||||
page.querySelectorAll('#channels-page-tabs .tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const key = tab.dataset.chTab
|
||||
page.querySelectorAll('#channels-page-tabs .tab').forEach(t => t.classList.toggle('active', t === tab))
|
||||
const listEl = page.querySelector('#channels-panel-list')
|
||||
const agentsEl = page.querySelector('#channels-panel-agents')
|
||||
if (listEl) listEl.style.display = key === 'channels' ? '' : 'none'
|
||||
if (agentsEl) agentsEl.style.display = key === 'agents' ? '' : 'none'
|
||||
activateChannelsPageTab(page, tab.dataset.chTab)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -321,6 +343,40 @@ async function loadPlatforms(page, state) {
|
||||
renderConfigured(page, state)
|
||||
renderAvailable(page, state)
|
||||
renderAgentBindings(page, state)
|
||||
applyRouteIntent(page, state)
|
||||
}
|
||||
|
||||
function applyRouteIntent(page, state) {
|
||||
const intent = state.routeIntent
|
||||
if (!intent) return
|
||||
|
||||
const requestedTab = intent.action === 'bind'
|
||||
? 'agents'
|
||||
: (intent.tab === 'agents' ? 'agents' : intent.tab === 'channels' ? 'channels' : '')
|
||||
|
||||
if (requestedTab) activateChannelsPageTab(page, requestedTab)
|
||||
|
||||
if (state.routeIntentConsumed) return
|
||||
|
||||
if (intent.action === 'bind' && intent.agentId) {
|
||||
const enabledConfigured = (state.configured || []).filter(p => p.enabled !== false)
|
||||
if (!enabledConfigured.length) {
|
||||
activateChannelsPageTab(page, 'channels')
|
||||
if (!state.routeIntentHintShown) {
|
||||
state.routeIntentHintShown = true
|
||||
toast(t('channels.enableChannelFirst'), 'info')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
state.routeIntentConsumed = true
|
||||
const targetCard = Array.from(page.querySelectorAll('.agent-binding-card')).find(card => card.dataset.agentId === intent.agentId)
|
||||
targetCard?.scrollIntoView?.({ block: 'center', behavior: 'smooth' })
|
||||
setTimeout(() => openAddAgentBindingModal(intent.agentId, page, state), 0)
|
||||
return
|
||||
}
|
||||
|
||||
if (requestedTab) state.routeIntentConsumed = true
|
||||
}
|
||||
|
||||
// ── 已配置平台渲染 ──
|
||||
@@ -1037,6 +1093,102 @@ function openExternalUrl(href) {
|
||||
import('@tauri-apps/plugin-shell').then(({ open }) => open(href)).catch(() => window.open(href, '_blank'))
|
||||
}
|
||||
|
||||
function getManualCommandSpecs(pid, reg) {
|
||||
if (pid === 'weixin') {
|
||||
return [
|
||||
{
|
||||
id: 'install',
|
||||
title: t('channels.manualInstallCommand'),
|
||||
hint: t('channels.manualInstallHint', { platform: reg.label }),
|
||||
command: 'npx -y @tencent-weixin/openclaw-weixin-cli@latest install',
|
||||
},
|
||||
{
|
||||
id: 'login',
|
||||
title: t('channels.manualLoginCommand'),
|
||||
hint: t('channels.manualLoginHint'),
|
||||
command: 'openclaw channels login --channel openclaw-weixin',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (!['qqbot', 'feishu', 'dingtalk'].includes(pid) || !reg.pluginRequired) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{
|
||||
id: 'install',
|
||||
title: t('channels.manualInstallCommand'),
|
||||
hint: t('channels.manualInstallHint', { platform: reg.label }),
|
||||
command: `openclaw plugins install ${reg.pluginRequired}`,
|
||||
}]
|
||||
}
|
||||
|
||||
function buildManualCommandPanel(commandSpecs) {
|
||||
if (!commandSpecs.length) return ''
|
||||
|
||||
return `
|
||||
<div style="margin-top:var(--space-md);padding:12px 14px;background:var(--bg-tertiary);border-radius:var(--radius-md)">
|
||||
<div style="font-weight:600;font-size:var(--font-size-sm);margin-bottom:6px">${t('channels.manualCommands')}</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.7;margin-bottom:10px">${t('channels.manualCommandsHint')}</div>
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
${commandSpecs.map(spec => `
|
||||
<div style="background:var(--bg-secondary);border:1px solid var(--border-primary);border-radius:var(--radius-md);padding:10px 12px">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
|
||||
<div style="min-width:0;flex:1">
|
||||
<div style="font-weight:600;font-size:var(--font-size-sm)">${spec.title}</div>
|
||||
<div style="font-size:var(--font-size-xs);color:var(--text-secondary);line-height:1.6;margin-top:4px">${spec.hint}</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-xs btn-secondary" data-manual-copy="${escapeAttr(spec.id)}">${t('common.copy')}</button>
|
||||
</div>
|
||||
<pre style="margin:8px 0 0;font-family:var(--font-mono);font-size:11px;white-space:pre-wrap;word-break:break-all;color:var(--text-primary)">${escapeAttr(spec.command)}</pre>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
async function copyTextToClipboard(text) {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text)
|
||||
return
|
||||
}
|
||||
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = text
|
||||
textarea.setAttribute('readonly', '')
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
const ok = document.execCommand('copy')
|
||||
textarea.remove()
|
||||
if (!ok) throw new Error('copy failed')
|
||||
}
|
||||
|
||||
function bindManualCommandCopy(root, commandSpecs) {
|
||||
if (!root || !commandSpecs.length) return
|
||||
|
||||
const commandMap = new Map(commandSpecs.map(spec => [spec.id, spec.command]))
|
||||
root.querySelectorAll('[data-manual-copy]').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const command = commandMap.get(btn.dataset.manualCopy)
|
||||
if (!command) return
|
||||
const prev = btn.textContent
|
||||
try {
|
||||
await copyTextToClipboard(command)
|
||||
btn.textContent = t('common.copied')
|
||||
toast(t('common.copied'), 'success')
|
||||
setTimeout(() => {
|
||||
btn.textContent = prev
|
||||
}, 1200)
|
||||
} catch (e) {
|
||||
toast(t('channels.copyCommandFailed') + ': ' + e, 'error')
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** QQ:展示后端完整诊断(凭证 + Gateway + 插件 + chatCompletions);可选一键修复插件 */
|
||||
function showQqDiagnoseModal(result, options = {}) {
|
||||
const accountId = options.accountId != null ? options.accountId : null
|
||||
@@ -1288,6 +1440,9 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
${t('channels.detectingPlugin')}
|
||||
</div>` : ''
|
||||
|
||||
const manualCommandSpecs = getManualCommandSpecs(pid, reg)
|
||||
const manualCommandHtml = buildManualCommandPanel(manualCommandSpecs)
|
||||
|
||||
const actionOnlyBtns = reg.actions?.length ? `
|
||||
<div style="padding:12px 14px;background:var(--bg-tertiary);border-radius:var(--radius-md)">
|
||||
<div style="font-weight:600;font-size:var(--font-size-sm);margin-bottom:8px">${t('channels.operations')}</div>
|
||||
@@ -1300,12 +1455,13 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
|
||||
const modal = showContentModal({
|
||||
title: `${reg.label} ${t('channels.setup')}`,
|
||||
content: actionOnlyGuide + pluginStatusHtml + actionOnlyBtns,
|
||||
content: actionOnlyGuide + pluginStatusHtml + manualCommandHtml + actionOnlyBtns,
|
||||
buttons: [
|
||||
{ label: t('channels.close'), className: 'btn btn-secondary', id: 'btn-close' },
|
||||
],
|
||||
width: 560,
|
||||
})
|
||||
bindManualCommandCopy(modal, manualCommandSpecs)
|
||||
modal.querySelector('#btn-close')?.addEventListener('click', () => modal.close?.() || modal.remove?.())
|
||||
modal.addEventListener('click', (e) => {
|
||||
const a = e.target.closest('a[href]')
|
||||
@@ -1620,6 +1776,9 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
</details>
|
||||
` : ''
|
||||
|
||||
const manualCommandSpecs = getManualCommandSpecs(pid, reg)
|
||||
const manualCommandHtml = buildManualCommandPanel(manualCommandSpecs)
|
||||
|
||||
const pairingHtml = reg.pairingChannel ? `
|
||||
<div style="margin-top:var(--space-md);padding:12px 14px;background:var(--bg-tertiary);border-radius:var(--radius-md)">
|
||||
<div style="font-weight:600;font-size:var(--font-size-sm);margin-bottom:6px">${t('channels.pairingApproval')}</div>
|
||||
@@ -1654,6 +1813,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
${accountIdHtml}
|
||||
${agentBindingHtml}
|
||||
</form>
|
||||
${manualCommandHtml}
|
||||
${actionPanelHtml}
|
||||
${pairingHtml}
|
||||
<div id="verify-result" style="margin-top:var(--space-sm)"></div>
|
||||
@@ -1673,6 +1833,7 @@ async function openConfigDialog(pid, page, state, accountId) {
|
||||
],
|
||||
width: 520,
|
||||
})
|
||||
bindManualCommandCopy(modal, manualCommandSpecs)
|
||||
|
||||
// 外部链接用系统浏览器打开
|
||||
modal.addEventListener('click', (e) => {
|
||||
|
||||
@@ -55,7 +55,7 @@ function renderDetectionHint(pathValue, sourceLabel = '') {
|
||||
if (!normalizedPath && !normalizedSource) return ''
|
||||
return `
|
||||
<div class="setup-inline-note" style="margin-top:8px;line-height:1.6">
|
||||
${normalizedPath ? `<div><span style="color:var(--text-secondary)">${t('setup.detectedPathLabel')}:</span> <code style="font-size:11px">${escapeHtml(normalizedPath)}</code></div>` : ''}
|
||||
${normalizedPath ? `<div><span style="color:var(--text-secondary)">${t('setup.detectedPathLabel')}:</span> <code class="setup-path-code" title="${escapeHtml(normalizedPath)}">${escapeHtml(normalizedPath)}</code></div>` : ''}
|
||||
${normalizedSource ? `<div${normalizedPath ? ' style="margin-top:4px"' : ''}><span style="color:var(--text-secondary)">${t('setup.detectedFromLabel')}:</span> ${escapeHtml(normalizedSource)}</div>` : ''}
|
||||
</div>
|
||||
`
|
||||
@@ -67,7 +67,7 @@ function renderStatusCard(title, ok, meta) {
|
||||
<div class="setup-status-icon">${ok ? '✓' : '✦'}</div>
|
||||
<div class="setup-status-body">
|
||||
<div class="setup-status-title">${title}</div>
|
||||
<div class="setup-status-meta">${escapeHtml(meta)}</div>
|
||||
<div class="setup-status-meta" title="${escapeHtml(meta)}">${escapeHtml(meta)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
@@ -85,6 +85,13 @@ export async function render() {
|
||||
<div class="setup-hero-copy">
|
||||
<h1 class="setup-hero-title">${t('setup.headerTitle')}</h1>
|
||||
<p class="setup-hero-desc">${t('setup.headerDesc')}</p>
|
||||
<div class="setup-hero-site-row">
|
||||
<a class="setup-hero-site-link" href="https://claw.qt.cool" target="_blank" rel="noopener noreferrer" title="https://claw.qt.cool">
|
||||
${icon('link', 14)}
|
||||
<span class="setup-hero-site-label">${t('setup.officialWebsite')}</span>
|
||||
<span class="setup-hero-site-value">claw.qt.cool</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setup-hero-actions">
|
||||
@@ -131,18 +138,6 @@ async function runDetect(page) {
|
||||
let config = configRes.status === 'fulfilled' ? configRes.value : { installed: false }
|
||||
const version = versionRes.status === 'fulfilled' ? versionRes.value : null
|
||||
|
||||
// CLI 已装但配置缺失 → 自动创建默认配置
|
||||
if (cliOk && !config.installed) {
|
||||
try {
|
||||
const initResult = await api.initOpenclawConfig()
|
||||
if (initResult?.created) {
|
||||
config = await api.checkInstallation()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[setup] 自动初始化配置失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Git 已安装时,自动配置 HTTPS 替代 SSH(静默执行)
|
||||
if (git.installed) {
|
||||
api.configureGitHttps().catch(() => {})
|
||||
@@ -277,7 +272,7 @@ function renderSteps(page, { node, git, cliOk, config, version }) {
|
||||
${stepIcon(config.installed)} ${t('setup.stepConfig')}
|
||||
</div>
|
||||
${config.installed
|
||||
? `<p style="color:var(--success);font-size:var(--font-size-sm)">${t('setup.configAt', { path: config.path || '' })}</p>
|
||||
? `<p class="setup-path-text" style="color:var(--success);font-size:var(--font-size-sm)" title="${escapeHtml(config.path || '')}">${t('setup.configAt', { path: config.path || '' })}</p>
|
||||
${renderDetectionHint(config.path)}`
|
||||
: `<p style="color:var(--text-secondary);font-size:var(--font-size-sm);margin-bottom:var(--space-sm)">
|
||||
${t('setup.configMissing')}
|
||||
@@ -694,7 +689,9 @@ function bindEvents(page, nodeOk, detectState) {
|
||||
btn.textContent = t('setup.initializing')
|
||||
try {
|
||||
const result = await api.initOpenclawConfig()
|
||||
if (result?.created) {
|
||||
if (result?.restored) {
|
||||
toast(t('setup.configRestored'), 'success')
|
||||
} else if (result?.created) {
|
||||
toast(t('setup.configCreated'), 'success')
|
||||
} else {
|
||||
toast(result?.message || t('setup.configExists'), 'info')
|
||||
|
||||
@@ -1248,6 +1248,43 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.setup-hero-site-row {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.setup-hero-site-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
max-width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--bg-secondary));
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 20%, var(--border-primary));
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: 1.4;
|
||||
transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
|
||||
}
|
||||
|
||||
.setup-hero-site-link:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, var(--border-primary));
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--bg-secondary));
|
||||
}
|
||||
|
||||
.setup-hero-site-label {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.setup-hero-site-value {
|
||||
min-width: 0;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.setup-hero-title {
|
||||
margin: 0 0 6px;
|
||||
font-size: clamp(28px, 4vw, 36px);
|
||||
@@ -1333,6 +1370,9 @@
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.setup-main-grid {
|
||||
@@ -1379,6 +1419,25 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.setup-path-text {
|
||||
margin: 0;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.setup-path-code {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-left: 4px;
|
||||
vertical-align: top;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.setup-help-details {
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
@@ -1466,6 +1525,11 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.setup-hero-site-link {
|
||||
border-radius: var(--radius-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.setup-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user