diff --git a/src/pages/services.js b/src/pages/services.js
index 9bb3a20..540617b 100644
--- a/src/pages/services.js
+++ b/src/pages/services.js
@@ -34,6 +34,17 @@ export async function render() {
配置备份
备份范围:openclaw.json 主配置文件(含模型、Provider、Gateway 设置)。Agent 数据和记忆文件不在此备份范围内。
@@ -56,7 +67,7 @@ export async function render() {
}
async function loadAll(page) {
- const tasks = [loadVersion(page), loadServices(page), loadBackups(page)]
+ const tasks = [loadVersion(page), loadServices(page), loadBackups(page), loadConfigEditor(page)]
if (!isInDocker()) tasks.push(loadRegistry(page))
await Promise.all(tasks)
}
@@ -269,6 +280,15 @@ function bindEvents(page) {
case 'restart':
await handleServiceAction(action, btn.dataset.label, page)
break
+ case 'save-config':
+ await handleSaveConfig(page, true)
+ break
+ case 'save-config-only':
+ await handleSaveConfig(page, false)
+ break
+ case 'reload-config':
+ await loadConfigEditor(page)
+ break
case 'create-backup':
await handleCreateBackup(page)
break
@@ -423,6 +443,100 @@ async function handleDeleteBackup(name, page) {
await loadBackups(page)
}
+// ===== 配置文件编辑器 =====
+
+let _configOriginal = ''
+
+async function loadConfigEditor(page) {
+ const section = page.querySelector('#config-editor-section')
+ const area = page.querySelector('#config-editor-area')
+ const status = page.querySelector('#config-editor-status')
+ const btnSave = page.querySelector('[data-action="save-config"]')
+ const btnSaveOnly = page.querySelector('[data-action="save-config-only"]')
+
+ try {
+ const config = await api.readOpenclawConfig()
+ const json = JSON.stringify(config, null, 2)
+ _configOriginal = json
+ area.value = json
+ area.disabled = false
+ btnSave.disabled = false
+ btnSaveOnly.disabled = false
+ section.style.display = ''
+ status.innerHTML = `
已加载 · ${(json.length / 1024).toFixed(1)} KB`
+
+ // 实时检测 JSON 语法
+ area.oninput = () => {
+ try {
+ JSON.parse(area.value)
+ const changed = area.value !== _configOriginal
+ status.innerHTML = changed
+ ? '
● 有未保存的修改'
+ : '
无修改'
+ btnSave.disabled = !changed
+ btnSaveOnly.disabled = !changed
+ } catch (e) {
+ status.innerHTML = `
JSON 语法错误: ${e.message.split(' at ')[0]}`
+ btnSave.disabled = true
+ btnSaveOnly.disabled = true
+ }
+ }
+ } catch {
+ // openclaw.json 不存在,隐藏编辑器
+ section.style.display = 'none'
+ }
+}
+
+async function handleSaveConfig(page, restart) {
+ const area = page.querySelector('#config-editor-area')
+ const status = page.querySelector('#config-editor-status')
+
+ let config
+ try {
+ config = JSON.parse(area.value)
+ } catch (e) {
+ toast('JSON 格式错误,无法保存', 'error')
+ return
+ }
+
+ status.innerHTML = '
自动备份中...'
+
+ try {
+ // 保存前自动备份
+ await api.createBackup()
+ } catch (e) {
+ const yes = await showConfirm('自动备份失败: ' + e + '\n\n是否仍然继续保存?')
+ if (!yes) return
+ }
+
+ status.innerHTML = '
保存中...'
+
+ try {
+ await api.writeOpenclawConfig(config)
+ _configOriginal = area.value
+ toast('配置已保存' + (restart ? ',正在重启 Gateway...' : ''), 'success')
+ status.innerHTML = '
已保存'
+
+ page.querySelector('[data-action="save-config"]').disabled = true
+ page.querySelector('[data-action="save-config-only"]').disabled = true
+
+ if (restart) {
+ try {
+ await api.restartGateway()
+ toast('Gateway 已重启', 'success')
+ } catch (e) {
+ toast('配置已保存,但 Gateway 重启失败: ' + e, 'warning')
+ }
+ await loadServices(page)
+ }
+
+ await loadBackups(page)
+ } catch (e) {
+ toast('保存失败: ' + e, 'error')
+ status.innerHTML = `
保存失败: ${e}`
+ }
+}
+
// ===== 升级操作 =====
async function doUpgradeWithModal(source, page) {