fix: 修复功能空壳问题 + 新增模型测试

- 服务管理:动态扫描 LaunchAgents plist,不再硬编码 4 个服务
- 服务启停:检查 launchctl 执行结果,失败时返回 stderr
- 配置保存:Gateway/模型配置保存后自动重载 Gateway 服务使配置生效
- 模型测试:新增 test_model 命令,向 provider 发送 chat completion 验证连通性
- 新增 reqwest 依赖用于 HTTP 请求
This commit is contained in:
晴天
2026-02-27 01:14:34 +08:00
parent fedd2f66fc
commit 0f79ce338f
8 changed files with 565 additions and 47 deletions

View File

@@ -18,10 +18,13 @@ async function invoke(cmd, args = {}) {
function mockInvoke(cmd, args) {
const mocks = {
get_services_status: () => [
{ label: 'ai.openclaw.gateway', pid: 54284, running: true, description: 'OpenClaw Gateway' },
{ label: 'com.openclaw.guardian.watch', pid: 54301, running: true, description: '健康监控 (60s)' },
{ label: 'ai.openclaw.gateway', pid: null, running: false, description: 'OpenClaw Gateway' },
{ label: 'com.cftunnel.cloudflared', pid: 35218, running: true, description: 'cftunnel 隧道服务' },
{ label: 'com.openclaw.guardian.watch', pid: 55290, running: true, description: '健康监控 (60s)' },
{ label: 'com.openclaw.guardian.backup', pid: null, running: false, description: '配置备份 (3600s)' },
{ label: 'com.openclaw.watchdog', pid: 54320, running: true, description: '看门狗 (120s)' },
{ label: 'com.openclaw.watchdog', pid: null, running: false, description: '看门狗 (120s)' },
{ label: 'com.openclaw.webhook-router', pid: 38983, running: true, description: 'Webhook 路由' },
{ label: 'com.openclaw.webhook-tunnel', pid: null, running: false, description: 'Webhook SSH 隧道' },
],
get_version_info: () => ({
current: '2026.2.23',
@@ -99,6 +102,8 @@ function mockInvoke(cmd, args) {
start_service: () => true,
stop_service: () => true,
restart_service: () => true,
reload_gateway: () => 'Gateway 已重载',
test_model: ({ base_url, model_id }) => `模型 ${model_id} 连通正常 (mock)`,
write_env_file: () => true,
list_backups: () => [
{ name: 'openclaw-20260226-143000.json', size: 8542, created_at: 1740577800 },
@@ -138,6 +143,8 @@ export const api = {
writeOpenclawConfig: (config) => invoke('write_openclaw_config', { config }),
readMcpConfig: () => invoke('read_mcp_config'),
writeMcpConfig: (config) => invoke('write_mcp_config', { config }),
reloadGateway: () => invoke('reload_gateway'),
testModel: (baseUrl, apiKey, modelId) => invoke('test_model', { base_url: baseUrl, api_key: apiKey, model_id: modelId }),
// 日志
readLogTail: (logName, lines = 100) => invoke('read_log_tail', { logName, lines }),

View File

@@ -122,7 +122,13 @@ async function saveConfig(page, state) {
try {
await api.writeOpenclawConfig(state.config)
toast('Gateway 配置已保存', 'success')
toast('Gateway 配置已保存,正在重载服务...', 'info')
try {
await api.reloadGateway()
toast('Gateway 已重载,配置已生效', 'success')
} catch (e) {
toast('配置已保存,但重载 Gateway 失败: ' + e, 'warning')
}
} catch (e) {
toast('保存失败: ' + e, 'error')
}

View File

@@ -145,6 +145,7 @@ function renderModelCards(providerKey, models, primary) {
</div>
</div>
<div style="display:flex;gap:6px;flex-shrink:0">
<button class="btn btn-sm btn-secondary" data-action="test-model">测试</button>
${!isPrimary ? `<button class="btn btn-sm btn-secondary" data-action="set-primary">设为主模型</button>` : ''}
<button class="btn btn-sm btn-secondary" data-action="edit-model">编辑</button>
<button class="btn btn-sm btn-danger" data-action="delete-model">删除</button>
@@ -191,6 +192,10 @@ function bindProviderEvents(page, state) {
renderProviders(page, state)
renderDefaultBar(page, state)
toast(`已设为主模型: ${full}`, 'success')
} else if (action === 'test-model') {
const card = btn.closest('.model-card')
const idx = parseInt(card.dataset.index)
testModel(btn, state, providerKey, idx)
}
}
})
@@ -214,7 +219,13 @@ function bindTopActions(page, state) {
btn.textContent = '保存中...'
try {
await api.writeOpenclawConfig(state.config)
toast('模型配置已保存', 'success')
toast('模型配置已保存,正在重载 Gateway...', 'info')
try {
await api.reloadGateway()
toast('Gateway 已重载,模型配置已生效', 'success')
} catch (e) {
toast('配置已保存,但重载 Gateway 失败: ' + e, 'warning')
}
} catch (e) {
toast('保存失败: ' + e, 'error')
} finally {
@@ -236,7 +247,13 @@ function bindTopActions(page, state) {
applyDefaultModel(state)
await api.writeOpenclawConfig(state.config)
renderDefaultBar(page, state)
toast('默认模型已应用', 'success')
toast('默认模型已应用,正在重载 Gateway...', 'info')
try {
await api.reloadGateway()
toast('Gateway 已重载,默认模型已生效', 'success')
} catch (e) {
toast('配置已保存,但重载 Gateway 失败: ' + e, 'warning')
}
} catch (e) {
toast('应用失败: ' + e, 'error')
} finally {
@@ -350,3 +367,24 @@ function editModel(page, state, providerKey, idx) {
},
})
}
// 测试模型连通性
async function testModel(btn, state, providerKey, idx) {
const provider = state.config.models.providers[providerKey]
const model = provider.models[idx]
const modelId = typeof model === 'string' ? model : model.id
btn.disabled = true
const origText = btn.textContent
btn.textContent = '测试中...'
try {
const reply = await api.testModel(provider.baseUrl, provider.apiKey || '', modelId)
toast(`${modelId} 连通正常: "${reply.slice(0, 60)}"`, 'success')
} catch (e) {
toast(`${modelId} 测试失败: ${e}`, 'error')
} finally {
btn.disabled = false
btn.textContent = origText
}
}