mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-05-07 04:42:46 +08:00
365 lines
20 KiB
JavaScript
365 lines
20 KiB
JavaScript
/**
|
||
* Tauri API 封装层
|
||
* Tauri 环境用 invoke,Web 模式走 dev-api 后端
|
||
*/
|
||
|
||
import { t } from './i18n.js'
|
||
|
||
const isTauri = !!window.__TAURI_INTERNALS__
|
||
|
||
// 仅在 Node.js 后端实现的命令(Tauri Rust 不处理),强制走 webInvoke
|
||
const WEB_ONLY_CMDS = new Set([
|
||
'instance_list', 'instance_add', 'instance_remove', 'instance_set_active',
|
||
'instance_health_check', 'instance_health_all',
|
||
'docker_info', 'docker_list_containers', 'docker_create_container',
|
||
'docker_start_container', 'docker_stop_container', 'docker_restart_container',
|
||
'docker_remove_container', 'docker_pull_image', 'docker_pull_status',
|
||
'docker_list_images', 'docker_list_nodes', 'docker_add_node',
|
||
'docker_remove_node', 'docker_cluster_overview',
|
||
'get_deploy_mode',
|
||
])
|
||
|
||
// 预加载 Tauri invoke,避免每次 API 调用都做动态 import
|
||
const _invokeReady = isTauri
|
||
? import('@tauri-apps/api/core').then(m => m.invoke)
|
||
: null
|
||
|
||
// 简单缓存:避免页面切换时重复请求后端
|
||
const _cache = new Map()
|
||
const _inflight = new Map() // in-flight 请求去重,防止缓存过期后同一命令并发 spawn 多个进程
|
||
const CACHE_TTL = 15000 // 15秒
|
||
|
||
// 网络请求日志(用于调试)
|
||
const _requestLogs = []
|
||
const MAX_LOGS = 100
|
||
|
||
function logRequest(cmd, args, duration, cached = false) {
|
||
const log = {
|
||
timestamp: Date.now(),
|
||
time: new Date().toLocaleTimeString('zh-CN', { hour12: false, fractionalSecondDigits: 3 }),
|
||
cmd,
|
||
args: JSON.stringify(args),
|
||
duration: duration ? `${duration}ms` : '-',
|
||
cached
|
||
}
|
||
_requestLogs.push(log)
|
||
if (_requestLogs.length > MAX_LOGS) {
|
||
_requestLogs.shift()
|
||
}
|
||
}
|
||
|
||
// 导出日志供调试页面使用
|
||
export function getRequestLogs() {
|
||
return _requestLogs.slice()
|
||
}
|
||
|
||
export function clearRequestLogs() {
|
||
_requestLogs.length = 0
|
||
}
|
||
|
||
function cachedInvoke(cmd, args = {}, ttl = CACHE_TTL) {
|
||
const key = cmd + JSON.stringify(args)
|
||
const cached = _cache.get(key)
|
||
if (cached && Date.now() - cached.ts < ttl) {
|
||
logRequest(cmd, args, 0, true)
|
||
return Promise.resolve(cached.val)
|
||
}
|
||
// in-flight 去重:同一个 key 的请求正在执行中,复用同一个 Promise
|
||
// 避免缓存过期瞬间多个调用者同时 spawn 进程(ARM 设备上的 CPU 爆满根因)
|
||
if (_inflight.has(key)) {
|
||
return _inflight.get(key)
|
||
}
|
||
const p = invoke(cmd, args).then(val => {
|
||
_cache.set(key, { val, ts: Date.now() })
|
||
_inflight.delete(key)
|
||
return val
|
||
}).catch(err => {
|
||
_inflight.delete(key)
|
||
throw err
|
||
})
|
||
_inflight.set(key, p)
|
||
return p
|
||
}
|
||
|
||
// 清除指定命令的缓存(写操作后调用)
|
||
function invalidate(...cmds) {
|
||
if (!cmds.length) {
|
||
_cache.clear()
|
||
return
|
||
}
|
||
for (const [k] of _cache) {
|
||
if (cmds.some(c => k.startsWith(c))) _cache.delete(k)
|
||
}
|
||
}
|
||
|
||
// 导出 invalidate 供外部使用
|
||
export { invalidate }
|
||
|
||
async function invoke(cmd, args = {}) {
|
||
const start = Date.now()
|
||
if (_invokeReady && !WEB_ONLY_CMDS.has(cmd)) {
|
||
const tauriInvoke = await _invokeReady
|
||
const result = await tauriInvoke(cmd, args)
|
||
const duration = Date.now() - start
|
||
logRequest(cmd, args, duration, false)
|
||
return result
|
||
}
|
||
// Web 模式:调用 dev-api 后端(真实数据)
|
||
const result = await webInvoke(cmd, args)
|
||
const duration = Date.now() - start
|
||
logRequest(cmd, args, duration, false)
|
||
return result
|
||
}
|
||
|
||
// Web 模式:通过 Vite 开发服务器的 API 端点调用真实后端
|
||
async function webInvoke(cmd, args) {
|
||
const resp = await fetch(`/__api/${cmd}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(args),
|
||
})
|
||
if (resp.status === 401) {
|
||
// Tauri 模式下不触发登录浮层(Tauri 有自己的认证流程)
|
||
if (!isTauri && window.__clawpanel_show_login) window.__clawpanel_show_login()
|
||
throw new Error(t('common.loginRequired'))
|
||
}
|
||
// 检测后端是否可用:如果返回的是 HTML(非 JSON),说明后端未运行
|
||
const ct = (resp.headers.get('content-type') || '').toLowerCase()
|
||
if (ct.includes('text/html') || ct.includes('text/plain')) {
|
||
throw new Error(t('common.backendWebModeRequired'))
|
||
}
|
||
if (!resp.ok) {
|
||
const data = await resp.json().catch(() => ({ error: `HTTP ${resp.status}` }))
|
||
throw new Error(data.error || `HTTP ${resp.status}`)
|
||
}
|
||
return resp.json()
|
||
}
|
||
|
||
// 后端连接状态
|
||
let _backendOnline = null // null=未检测, true=在线, false=离线
|
||
const _backendListeners = []
|
||
|
||
export function onBackendStatusChange(fn) {
|
||
_backendListeners.push(fn)
|
||
return () => { const i = _backendListeners.indexOf(fn); if (i >= 0) _backendListeners.splice(i, 1) }
|
||
}
|
||
|
||
export function isBackendOnline() { return _backendOnline }
|
||
|
||
function _setBackendOnline(v) {
|
||
if (_backendOnline !== v) {
|
||
_backendOnline = v
|
||
_backendListeners.forEach(fn => { try { fn(v) } catch {} })
|
||
}
|
||
}
|
||
|
||
// 后端健康检查
|
||
export async function checkBackendHealth() {
|
||
if (isTauri) { _setBackendOnline(true); return true }
|
||
try {
|
||
const resp = await fetch('/__api/health', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
||
const ok = resp.ok
|
||
_setBackendOnline(ok)
|
||
return ok
|
||
} catch {
|
||
_setBackendOnline(false)
|
||
return false
|
||
}
|
||
}
|
||
|
||
// 配置保存后防抖重载 Gateway(3 秒内多次写入只触发一次重载)
|
||
let _reloadTimer = null
|
||
function _debouncedReloadGateway() {
|
||
clearTimeout(_reloadTimer)
|
||
_reloadTimer = setTimeout(() => { invoke('reload_gateway').catch(() => {}) }, 3000)
|
||
}
|
||
|
||
// 导出 API
|
||
export const api = {
|
||
// 服务管理(状态用短缓存,操作不缓存)
|
||
getServicesStatus: () => cachedInvoke('get_services_status', {}, 10000),
|
||
startService: (label) => { invalidate('get_services_status'); return invoke('start_service', { label }) },
|
||
stopService: (label) => { invalidate('get_services_status'); return invoke('stop_service', { label }) },
|
||
restartService: (label) => { invalidate('get_services_status'); return invoke('restart_service', { label }) },
|
||
guardianStatus: () => invoke('guardian_status'),
|
||
|
||
// 配置(读缓存,写清缓存)
|
||
getVersionInfo: () => cachedInvoke('get_version_info', {}, 30000),
|
||
getStatusSummary: () => cachedInvoke('get_status_summary', {}, 60000),
|
||
readOpenclawConfig: () => cachedInvoke('read_openclaw_config'),
|
||
calibrateOpenclawConfig: (mode = 'inherit') => { invalidate('read_openclaw_config', 'check_installation', 'list_backups', 'get_services_status', 'get_status_summary'); return invoke('calibrate_openclaw_config', { mode }).then(r => { _debouncedReloadGateway(); return r }) },
|
||
writeOpenclawConfig: (config) => { invalidate('read_openclaw_config'); return invoke('write_openclaw_config', { config }).then(r => { _debouncedReloadGateway(); return r }) },
|
||
readMcpConfig: () => cachedInvoke('read_mcp_config'),
|
||
writeMcpConfig: (config) => { invalidate('read_mcp_config'); return invoke('write_mcp_config', { config }) },
|
||
reloadGateway: () => invoke('reload_gateway'),
|
||
restartGateway: () => invoke('restart_gateway'),
|
||
doctorCheck: () => invoke('doctor_check'),
|
||
doctorFix: () => invoke('doctor_fix'),
|
||
listOpenclawVersions: (source = 'chinese') => invoke('list_openclaw_versions', { source }),
|
||
upgradeOpenclaw: (source = 'chinese', version = null, method = 'auto') => invoke('upgrade_openclaw', { source, version, method }),
|
||
uninstallOpenclaw: (cleanConfig = false) => invoke('uninstall_openclaw', { cleanConfig }),
|
||
installGateway: () => invoke('install_gateway'),
|
||
uninstallGateway: () => invoke('uninstall_gateway'),
|
||
getNpmRegistry: () => cachedInvoke('get_npm_registry', {}, 30000),
|
||
setNpmRegistry: (registry) => { invalidate('get_npm_registry'); return invoke('set_npm_registry', { registry }) },
|
||
testModel: (baseUrl, apiKey, modelId, apiType = null) => invoke('test_model', { baseUrl, apiKey, modelId, apiType }),
|
||
listRemoteModels: (baseUrl, apiKey, apiType = null) => invoke('list_remote_models', { baseUrl, apiKey, apiType }),
|
||
|
||
// Agent 管理
|
||
listAgents: () => cachedInvoke('list_agents'),
|
||
getAgentDetail: (id) => cachedInvoke('get_agent_detail', { id }, 5000),
|
||
listAgentFiles: (id) => cachedInvoke('list_agent_files', { id }, 5000),
|
||
readAgentFile: (id, name) => invoke('read_agent_file', { id, name }),
|
||
writeAgentFile: (id, name, content) => { invalidate('list_agent_files', 'read_agent_file'); return invoke('write_agent_file', { id, name, content }) },
|
||
getAgentWorkspaceInfo: (id) => cachedInvoke('get_agent_workspace_info', { id }, 5000),
|
||
listAgentWorkspaceEntries: (id, relativePath) => cachedInvoke('list_agent_workspace_entries', { id, relativePath: relativePath || null }, 5000),
|
||
readAgentWorkspaceFile: (id, relativePath) => cachedInvoke('read_agent_workspace_file', { id, relativePath }, 5000),
|
||
writeAgentWorkspaceFile: (id, relativePath, content) => {
|
||
invalidate('get_agent_workspace_info', 'list_agent_workspace_entries', 'read_agent_workspace_file', 'list_agent_files', 'read_agent_file')
|
||
return invoke('write_agent_workspace_file', { id, relativePath, content })
|
||
},
|
||
updateAgentConfig: (id, config) => { invalidate('list_agents', 'get_agent_detail'); return invoke('update_agent_config', { id, config }) },
|
||
addAgent: (name, model, workspace) => { invalidate('list_agents'); return invoke('add_agent', { name, model, workspace: workspace || null }) },
|
||
deleteAgent: (id) => { invalidate('list_agents', 'get_agent_detail'); return invoke('delete_agent', { id }) },
|
||
updateAgentIdentity: (id, name, emoji) => { invalidate('list_agents', 'get_agent_detail'); return invoke('update_agent_identity', { id, name, emoji }) },
|
||
updateAgentModel: (id, model) => { invalidate('list_agents', 'get_agent_detail'); return invoke('update_agent_model', { id, model }) },
|
||
backupAgent: (id) => invoke('backup_agent', { id }),
|
||
|
||
// 日志(短缓存)
|
||
readLogTail: (logName, lines = 100) => cachedInvoke('read_log_tail', { logName, lines }, 5000),
|
||
searchLog: (logName, query, maxResults = 50) => invoke('search_log', { logName, query, maxResults }),
|
||
|
||
// 记忆文件
|
||
listMemoryFiles: (category, agentId) => cachedInvoke('list_memory_files', { category, agentId: agentId || null }),
|
||
readMemoryFile: (path, agentId) => cachedInvoke('read_memory_file', { path, agentId: agentId || null }, 5000),
|
||
writeMemoryFile: (path, content, category, agentId) => { invalidate('list_memory_files', 'read_memory_file'); return invoke('write_memory_file', { path, content, category: category || 'memory', agentId: agentId || null }) },
|
||
deleteMemoryFile: (path, agentId) => { invalidate('list_memory_files'); return invoke('delete_memory_file', { path, agentId: agentId || null }) },
|
||
exportMemoryZip: (category, agentId) => invoke('export_memory_zip', { category, agentId: agentId || null }),
|
||
|
||
// 消息渠道管理
|
||
readPlatformConfig: (platform, accountId) => invoke('read_platform_config', { platform, accountId: accountId || null }),
|
||
saveMessagingPlatform: (platform, form, accountId, agentId) => { invalidate('list_configured_platforms', 'read_openclaw_config', 'read_platform_config'); return invoke('save_messaging_platform', { platform, form, accountId: accountId || null, agentId: agentId || null }) },
|
||
removeMessagingPlatform: (platform, accountId) => { invalidate('list_configured_platforms', 'read_openclaw_config', 'read_platform_config'); return invoke('remove_messaging_platform', { platform, accountId: accountId || null }) },
|
||
toggleMessagingPlatform: (platform, enabled) => { invalidate('list_configured_platforms', 'read_openclaw_config', 'read_platform_config'); return invoke('toggle_messaging_platform', { platform, enabled }) },
|
||
verifyBotToken: (platform, form) => invoke('verify_bot_token', { platform, form }),
|
||
diagnoseChannel: (platform, accountId) => invoke('diagnose_channel', { platform, accountId: accountId || null }),
|
||
repairQqbotChannelSetup: () => {
|
||
invalidate('list_configured_platforms', 'read_openclaw_config', 'read_platform_config')
|
||
return invoke('repair_qqbot_channel_setup')
|
||
},
|
||
listConfiguredPlatforms: () => cachedInvoke('list_configured_platforms', {}, 5000),
|
||
getChannelPluginStatus: (pluginId) => invoke('get_channel_plugin_status', { pluginId }),
|
||
installQqbotPlugin: (version = null) => invoke('install_qqbot_plugin', { version }),
|
||
installChannelPlugin: (packageName, pluginId, version = null) => invoke('install_channel_plugin', { packageName, pluginId, version }),
|
||
runChannelAction: (platform, action, version = null) => invoke('run_channel_action', { platform, action, version }),
|
||
checkWeixinPluginStatus: () => invoke('check_weixin_plugin_status'),
|
||
|
||
// Agent 渠道绑定管理
|
||
getAgentBindings: (agentId) => invoke('get_agent_bindings', { agentId }),
|
||
listAllBindings: () => invoke('list_all_bindings'),
|
||
saveAgentBinding: (agentId, channel, accountId, bindingConfig) => { invalidate('read_openclaw_config', 'list_configured_platforms'); return invoke('save_agent_binding', { agentId, channel, accountId: accountId || null, bindingConfig: bindingConfig || {} }) },
|
||
deleteAgentBinding: (agentId, channel, accountId, bindingConfig) => { invalidate('read_openclaw_config', 'list_configured_platforms'); return invoke('delete_agent_binding', { agentId, channel, accountId: accountId || null, bindingConfig: bindingConfig || null }) },
|
||
deleteAgentAllBindings: (agentId) => { invalidate('read_openclaw_config', 'list_configured_platforms'); return invoke('delete_agent_all_bindings', { agentId }) },
|
||
|
||
// 面板配置 (clawpanel.json)
|
||
getOpenclawDir: () => invoke('get_openclaw_dir'),
|
||
relaunchApp: () => invoke('relaunch_app'),
|
||
readPanelConfig: () => invoke('read_panel_config'),
|
||
writePanelConfig: (config) => { invalidate(); return invoke('write_panel_config', { config }).then(r => { invoke('invalidate_path_cache').catch(() => {}); return r }) },
|
||
testProxy: (url) => invoke('test_proxy', { url: url || null }),
|
||
|
||
// 安装/部署
|
||
checkInstallation: () => cachedInvoke('check_installation', {}, 60000),
|
||
initOpenclawConfig: () => { invalidate('check_installation'); return invoke('init_openclaw_config') },
|
||
checkNode: () => cachedInvoke('check_node', {}, 60000),
|
||
checkNodeAtPath: (nodeDir) => invoke('check_node_at_path', { nodeDir }),
|
||
checkOpenclawAtPath: (cliPath) => invoke('check_openclaw_at_path', { cliPath }),
|
||
scanNodePaths: () => invoke('scan_node_paths'),
|
||
scanOpenclawPaths: () => invoke('scan_openclaw_paths'),
|
||
saveCustomNodePath: (nodeDir) => invoke('save_custom_node_path', { nodeDir }).then(r => { invalidate('check_node', 'get_services_status'); invoke('invalidate_path_cache').catch(() => {}); return r }),
|
||
invalidatePathCache: () => invoke('invalidate_path_cache'),
|
||
checkGit: () => cachedInvoke('check_git', {}, 60000),
|
||
autoInstallGit: () => invoke('auto_install_git'),
|
||
configureGitHttps: () => invoke('configure_git_https'),
|
||
getDeployConfig: () => cachedInvoke('get_deploy_config'),
|
||
patchModelVision: () => invoke('patch_model_vision'),
|
||
checkPanelUpdate: () => invoke('check_panel_update'),
|
||
writeEnvFile: (path, config) => invoke('write_env_file', { path, config }),
|
||
|
||
// 备份管理
|
||
listBackups: () => cachedInvoke('list_backups'),
|
||
createBackup: () => { invalidate('list_backups'); return invoke('create_backup') },
|
||
restoreBackup: (name) => invoke('restore_backup', { name }),
|
||
deleteBackup: (name) => { invalidate('list_backups'); return invoke('delete_backup', { name }) },
|
||
|
||
// 设备密钥 + Gateway 握手
|
||
createConnectFrame: (nonce, gatewayToken) => invoke('create_connect_frame', { nonce, gatewayToken }),
|
||
|
||
// 设备配对
|
||
autoPairDevice: () => invoke('auto_pair_device'),
|
||
checkPairingStatus: () => invoke('check_pairing_status'),
|
||
pairingListChannel: (channel) => invoke('pairing_list_channel', { channel }),
|
||
pairingApproveChannel: (channel, code, notify = false) => invoke('pairing_approve_channel', { channel, code, notify }),
|
||
|
||
// AI 助手工具
|
||
assistantExec: (command, cwd) => invoke('assistant_exec', { command, cwd: cwd || null }),
|
||
assistantReadFile: (path) => invoke('assistant_read_file', { path }),
|
||
assistantWriteFile: (path, content) => invoke('assistant_write_file', { path, content }),
|
||
assistantListDir: (path) => invoke('assistant_list_dir', { path }),
|
||
assistantSystemInfo: () => invoke('assistant_system_info'),
|
||
assistantListProcesses: (filter) => invoke('assistant_list_processes', { filter: filter || null }),
|
||
assistantCheckPort: (port) => invoke('assistant_check_port', { port }),
|
||
assistantWebSearch: (query, maxResults) => invoke('assistant_web_search', { query, max_results: maxResults || 5 }),
|
||
assistantFetchUrl: (url) => invoke('assistant_fetch_url', { url }),
|
||
|
||
// Skills 管理(openclaw skills CLI)
|
||
skillsList: () => invoke('skills_list'),
|
||
skillsInfo: (name) => invoke('skills_info', { name }),
|
||
skillsCheck: () => invoke('skills_check'),
|
||
skillsInstallDep: (kind, spec) => invoke('skills_install_dep', { kind, spec }),
|
||
skillsSkillHubCheck: () => invoke('skills_skillhub_check'),
|
||
skillsSkillHubSetup: (cliOnly = true) => invoke('skills_skillhub_setup', { cliOnly }),
|
||
skillsSkillHubSearch: (query) => invoke('skills_skillhub_search', { query }),
|
||
skillsSkillHubInstall: (slug) => invoke('skills_skillhub_install', { slug }),
|
||
skillsClawHubSearch: (query) => invoke('skills_clawhub_search', { query }),
|
||
skillsClawHubInstall: (slug) => invoke('skills_clawhub_install', { slug }),
|
||
skillsUninstall: (name) => invoke('skills_uninstall', { name }),
|
||
|
||
// 实例管理
|
||
instanceList: () => cachedInvoke('instance_list', {}, 10000),
|
||
instanceAdd: (instance) => { invalidate('instance_list'); return invoke('instance_add', instance) },
|
||
instanceRemove: (id) => { invalidate('instance_list'); return invoke('instance_remove', { id }) },
|
||
instanceSetActive: (id) => { invalidate('instance_list'); _cache.clear(); return invoke('instance_set_active', { id }) },
|
||
instanceHealthCheck: (id) => invoke('instance_health_check', { id }),
|
||
instanceHealthAll: () => invoke('instance_health_all'),
|
||
|
||
// Docker 管理(当前由 Web/dev-api 提供)
|
||
dockerInfo: (nodeId) => invoke('docker_info', { nodeId: nodeId || null }),
|
||
dockerListContainers: (nodeId, all = true) => invoke('docker_list_containers', { nodeId: nodeId || null, all }),
|
||
dockerCreateContainer: (payload) => invoke('docker_create_container', payload || {}),
|
||
dockerStartContainer: (nodeId, containerId) => invoke('docker_start_container', { nodeId: nodeId || null, containerId }),
|
||
dockerStopContainer: (nodeId, containerId) => invoke('docker_stop_container', { nodeId: nodeId || null, containerId }),
|
||
dockerRestartContainer: (nodeId, containerId) => invoke('docker_restart_container', { nodeId: nodeId || null, containerId }),
|
||
dockerRemoveContainer: (nodeId, containerId, force = false) => invoke('docker_remove_container', { nodeId: nodeId || null, containerId, force }),
|
||
dockerPullImage: (payload) => invoke('docker_pull_image', payload || {}),
|
||
dockerPullStatus: (requestId) => invoke('docker_pull_status', { requestId }),
|
||
dockerListImages: (nodeId) => invoke('docker_list_images', { nodeId: nodeId || null }),
|
||
dockerListNodes: () => invoke('docker_list_nodes', {}),
|
||
dockerAddNode: (name, endpoint) => invoke('docker_add_node', { name, endpoint }),
|
||
dockerRemoveNode: (nodeId) => invoke('docker_remove_node', { nodeId }),
|
||
dockerClusterOverview: () => invoke('docker_cluster_overview', {}),
|
||
|
||
|
||
// 前端热更新
|
||
checkFrontendUpdate: () => invoke('check_frontend_update'),
|
||
downloadFrontendUpdate: (url, expectedHash) => invoke('download_frontend_update', { url, expectedHash: expectedHash || '' }),
|
||
rollbackFrontendUpdate: () => invoke('rollback_frontend_update'),
|
||
getUpdateStatus: () => invoke('get_update_status'),
|
||
|
||
// 数据目录 & 图片存储
|
||
ensureDataDir: () => invoke('assistant_ensure_data_dir'),
|
||
saveImage: (id, data) => invoke('assistant_save_image', { id, data }),
|
||
loadImage: (id) => invoke('assistant_load_image', { id }),
|
||
deleteImage: (id) => invoke('assistant_delete_image', { id }),
|
||
}
|