正在读取 Agent「${selectedAgent}」的 workspace...
`
const soul = await loadOpenClawSoul(selectedAgent)
btn.disabled = false
btn.innerHTML = origHTML
if (!soul) {
statusEl.innerHTML = `
内置 AI
这是 ClawPanel 内置的 AI 助手,独立于 OpenClaw,使用你在右上角「设置」中配置的 API。
如需与 OpenClaw Agent 对话,请前往「实时聊天」页面。
`
}
// ── 工具函数 ──
function escHtml(str) {
const d = document.createElement('div')
d.textContent = str || ''
return d.innerHTML
}
function sendIcon() {
return '
`
// 缓存 DOM 引用
_messagesEl = page.querySelector('#ast-messages')
_queueEl = page.querySelector('#ast-queue')
_textarea = page.querySelector('#ast-textarea')
_sendBtn = page.querySelector('#ast-send-btn')
_sessionListEl = page.querySelector('#ast-session-list')
// 渲染
renderSessionList()
renderMessages()
renderQueue()
applyModeStyle(page, currentMode())
// 滑块需要等 DOM 绘制完毕才能获取正确位置
requestAnimationFrame(() => positionModeSlider(page, currentMode()))
// 如果有后台流式正在进行,恢复 UI 状态
if (_isStreaming) {
_sendBtn.innerHTML = stopIcon()
startStreamRefresh()
}
// 检查是否有从 setup 页面带来的自动提问
const autoPrompt = sessionStorage.getItem('assistant-auto-prompt')
if (autoPrompt) {
sessionStorage.removeItem('assistant-auto-prompt')
// 自动切换到执行模式
if (currentMode() === 'chat') {
_config.mode = 'execute'
saveConfig()
page.querySelectorAll('.ast-mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === 'execute'))
}
// 延迟发送,确保页面渲染完成
setTimeout(() => sendMessage(autoPrompt), 300)
}
// 检查是否有错误上下文待处理(显示 banner,不自动发送)
checkErrorContext()
if (_errorContext) {
setTimeout(() => renderErrorBanner(), 100)
}
// 监听实时错误注入(用户已在助手页面时,其他页面发生错误)
window.addEventListener('assistant-error-injected', () => {
checkErrorContext()
if (_errorContext) renderErrorBanner()
})
// ── 事件绑定 ──
// 右键调试菜单(事件委托)
_messagesEl.addEventListener('contextmenu', (e) => {
const msgEl = e.target.closest('[data-msg-idx]')
if (!msgEl) return
showMsgContextMenu(e, parseInt(msgEl.dataset.msgIdx))
})
// 发送(流式中输入排队,空输入时点按钮停止流式)
_sendBtn.addEventListener('click', () => {
if (_isStreaming && !_textarea.value.trim() && _pendingImages.length === 0) { stopStreaming(); return }
if (_textarea.value.trim() || _pendingImages.length > 0) {
sendMessage(_textarea.value)
_textarea.value = ''
autoResize(_textarea)
}
})
// Enter 发送,Shift+Enter 换行
_textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
if (!_textarea.value.trim() && _pendingImages.length === 0) return
sendMessage(_textarea.value)
_textarea.value = ''
autoResize(_textarea)
}
})
// 自动高度
_textarea.addEventListener('input', () => autoResize(_textarea))
// 图片上传按钮
const fileInput = page.querySelector('#ast-file-input')
page.querySelector('#ast-btn-attach').addEventListener('click', () => fileInput.click())
fileInput.addEventListener('change', () => {
for (const file of fileInput.files) addImageFromFile(file)
fileInput.value = ''
})
// 粘贴图片(Ctrl+V)
_textarea.addEventListener('paste', (e) => {
const items = e.clipboardData?.items
if (!items) return
let hasImage = false
for (const item of items) {
if (item.type.startsWith('image/')) {
addImageFromClipboard(item)
hasImage = true
}
}
if (hasImage) e.preventDefault()
})
// 拖拽图片
const mainEl = page.querySelector('.ast-main')
mainEl.addEventListener('dragover', (e) => {
e.preventDefault()
mainEl.classList.add('ast-drag-over')
})
mainEl.addEventListener('dragleave', (e) => {
if (!mainEl.contains(e.relatedTarget)) mainEl.classList.remove('ast-drag-over')
})
mainEl.addEventListener('drop', (e) => {
e.preventDefault()
mainEl.classList.remove('ast-drag-over')
for (const file of e.dataTransfer.files) addImageFromFile(file)
})
// 图片预览删除
page.querySelector('#ast-image-preview').addEventListener('click', (e) => {
const delBtn = e.target.closest('[data-img-del]')
if (delBtn) removeImage(delBtn.dataset.imgDel)
})
// 队列事件委托
_queueEl.addEventListener('click', (e) => {
// 插队发送
const sendBtn = e.target.closest('[data-queue-send]')
if (sendBtn) {
const id = sendBtn.dataset.queueSend
const idx = _messageQueue.findIndex(m => m.id === id)
if (idx === -1) return
const item = _messageQueue.splice(idx, 1)[0]
renderQueue()
if (_isStreaming) stopStreaming()
setTimeout(() => sendMessageDirect(item.text), 150)
return
}
// 删除
const delBtn = e.target.closest('[data-queue-del]')
if (delBtn) {
const id = delBtn.dataset.queueDel
_messageQueue = _messageQueue.filter(m => m.id !== id)
renderQueue()
return
}
// 编辑(点击文字或编辑按钮)
const editTarget = e.target.closest('[data-queue-edit]') || e.target.closest('[data-queue-edit-btn]')
if (editTarget) {
const id = editTarget.dataset.queueEdit || editTarget.dataset.queueEditBtn
const item = _messageQueue.find(m => m.id === id)
if (!item) return
const queueItem = _queueEl.querySelector(`[data-queue-id="${id}"]`)
if (!queueItem || queueItem.classList.contains('editing')) return
queueItem.classList.add('editing')
const textEl = queueItem.querySelector('.ast-queue-text')
const input = document.createElement('textarea')
input.className = 'ast-queue-edit-input'
input.value = item.text
input.rows = 1
textEl.replaceWith(input)
input.focus()
input.style.height = Math.min(input.scrollHeight, 100) + 'px'
// 保存编辑
const save = () => {
const newText = input.value.trim()
if (newText) item.text = newText
renderQueue()
}
input.addEventListener('blur', save)
input.addEventListener('keydown', (ev) => {
if (ev.key === 'Enter' && !ev.shiftKey) { ev.preventDefault(); save() }
if (ev.key === 'Escape') renderQueue()
})
input.addEventListener('input', () => {
input.style.height = 'auto'
input.style.height = Math.min(input.scrollHeight, 100) + 'px'
})
}
})
// 侧边栏切换
page.querySelector('#ast-btn-toggle').addEventListener('click', () => {
page.querySelector('#ast-sidebar').classList.toggle('open')
})
// 新建会话
page.querySelector('#ast-btn-new').addEventListener('click', () => {
createSession()
renderSessionList()
renderMessages()
})
// 模式切换
page.querySelector('#ast-mode-selector').addEventListener('click', (e) => {
const btn = e.target.closest('.ast-mode-btn')
if (!btn) return
const modeKey = btn.dataset.mode
if (!MODES[modeKey] || modeKey === currentMode()) return
_config.mode = modeKey
saveConfig()
page.querySelectorAll('.ast-mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === modeKey))
applyModeStyle(page, modeKey)
playModeTransition(page, modeKey)
})
// 设置
page.querySelector('#ast-btn-settings').addEventListener('click', showSettings)
// 会话列表事件委托
_sessionListEl.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('[data-delete]')
if (deleteBtn) {
e.stopPropagation()
const id = deleteBtn.dataset.delete
showConfirm('确定删除这个会话吗?').then(ok => {
if (!ok) return
deleteSession(id)
renderSessionList()
renderMessages()
})
return
}
const item = e.target.closest('.ast-session-item')
if (item) {
_currentSessionId = item.dataset.id
renderSessionList()
renderMessages()
// 切换到正在流式的会话时,启动刷新
if (_isStreaming && getSessionStatus(_currentSessionId) === 'streaming') {
startStreamRefresh()
} else {
stopStreamRefresh()
}
}
})
// 欢迎页技能卡片 & 快捷按钮委托
_messagesEl.addEventListener('click', (e) => {
const skillCard = e.target.closest('.ast-skill-card')
if (skillCard) {
const skill = BUILTIN_SKILLS.find(s => s.id === skillCard.dataset.skill)
if (!skill) return
// 技能需要工具 → 自动切换到执行模式(如果当前是聊天模式)
if (skill.tools.length > 0 && currentMode() === 'chat') {
_config.mode = 'execute'
saveConfig()
page.querySelectorAll('.ast-mode-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === 'execute'))
toast('已自动切换到执行模式', 'info')
}
sendMessage(skill.prompt)
return
}
const quickBtn = e.target.closest('.ast-quick-btn')
if (quickBtn) {
const prompt = quickBtn.dataset.prompt
if (prompt) sendMessage(prompt)
}
})
return page
}
function autoResize(textarea) {
textarea.style.height = 'auto'
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'
}
export function cleanup() {
flushSave()
stopStreaming()
stopStreamRefresh()
_pendingImages = []
_page = null
_messagesEl = null
_queueEl = null
_textarea = null
_sendBtn = null
_sessionListEl = null
}