feat(config): add config calibration repair flow

This commit is contained in:
晴天
2026-04-01 22:46:36 +08:00
parent 476b1a64b0
commit aad8043196
6 changed files with 1056 additions and 45 deletions

View File

@@ -187,6 +187,7 @@ export const api = {
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 }) },
@@ -210,6 +211,13 @@ export const api = {
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 }) },

View File

@@ -103,6 +103,20 @@ export default {
policyDefault: _('默认只建议当前面板已验证的推荐稳定版。如需尝试其它版本或最新特性,请到「关于」页手动切换版本并自行验证兼容性;若希望面板优先适配最新版,欢迎提交 issue。', 'Only the panel-verified recommended stable version is suggested by default. To try other versions or latest features, manually switch in the About page and verify compatibility yourself. To request newer version support, file an issue.', '預設只建議目前面板已驗證的推薦穩定版。如需尝試其它版本或最新特性,請到「關於」頁手動切換版本並自行驗證相容性;若希望面板優先適配最新版,欢迎提交 issue。'),
configEditor: _('配置文件编辑', 'Config Editor', '設定檔案編輯'),
configEditorHint: _('直接编辑 openclaw.json 主配置文件。保存前会自动创建备份,修改后可能需要重启 Gateway 生效。', 'Edit the openclaw.json config file directly. A backup is auto-created before saving. Changes may require a Gateway restart.', '直接編輯 openclaw.json 主設定檔案。儲存前會自動建立備份,修改后可能需要重啟 Gateway 生效。'),
configCalibration: _('配置校准', 'Config Calibration', '設定校準'),
configCalibrationHint: _('用于修复损坏、截断或不安全的 openclaw.json。会先创建修复前备份再按所选模式校准核心配置。', 'Repair a broken, truncated, or unsafe openclaw.json. A pre-repair backup is created first, then core settings are calibrated using the selected mode.', '用於修復損壞、截斷或不安全的 openclaw.json。會先建立修復前備份再按所選模式校準核心設定。'),
calibrateInherit: _('继承校准', 'Inherit Calibration', '繼承校準'),
calibrateReset: _('完全初始化修复', 'Full Initialization Repair', '完全初始化修復'),
calibrateInheritHint: _('保留现有模型、渠道、Agent、绑定、认证档案等业务配置只修复 Gateway、工具配置和必要默认项。', 'Preserve models, channels, agents, bindings, auth profiles, and other business config while repairing Gateway, tool settings, and required defaults.', '保留現有模型、渠道、Agent、綁定、認證檔案等業務設定只修復 Gateway、工具設定和必要預設值。'),
calibrateResetHint: _('重建一份安全的基线配置再择优继承模型、渠道、Agent、绑定、认证档案等关键业务配置。适合配置严重损坏时使用。', 'Rebuild a safe baseline config, then selectively inherit critical business config such as models, channels, agents, bindings, and auth profiles. Best for severely broken configs.', '重建一份安全的基線設定再擇優繼承模型、渠道、Agent、綁定、認證檔案等關鍵業務設定。適合設定嚴重損壞時使用。'),
calibrateInheritConfirm: _('确定按“继承校准”修复配置吗?\n会优先保留现有业务配置并先创建修复前备份。', 'Run "Inherit Calibration" now?\nExisting business config will be preserved as much as possible, and a pre-repair backup will be created first.', '確定按「繼承校準」修復設定嗎?\n會優先保留現有業務設定並先建立修復前備份。'),
calibrateResetConfirm: _('确定按“完全初始化修复”修复配置吗?\n会重建安全基线并仅继承关键业务配置同样会先创建修复前备份。', 'Run "Full Initialization Repair" now?\nA safe baseline will be rebuilt and only critical business config will be inherited; a pre-repair backup will also be created first.', '確定按「完全初始化修復」修復設定嗎?\n會重建安全基線並僅繼承關鍵業務設定同樣會先建立修復前備份。'),
calibrating: _('正在校准配置...', 'Calibrating config...', '正在校準設定...'),
calibrationDone: _('配置校准完成', 'Config calibration complete', '設定校準完成'),
calibrationSummary: _('已按 {mode} 完成修复,来源:{source},继承项:{count}', 'Repair completed with {mode}. Source: {source}. Inherited items: {count}', '已按 {mode} 完成修復,來源:{source},繼承項:{count}'),
calibrationSourceCurrent: _('当前配置', 'Current config', '目前設定'),
calibrationSourceBackup: _('备份配置', 'Backup config', '備份設定'),
calibrationSourceEmpty: _('空白基线', 'Empty baseline', '空白基線'),
saveAndRestart: _('保存并重启', 'Save & Restart', '儲存並重啟'),
saveOnly: _('仅保存', 'Save Only', '僅儲存'),
reloadConfig: _('重新加载', 'Reload', '重新載入'),

View File

@@ -48,6 +48,19 @@ export async function render() {
<div id="config-editor-status" style="font-size:var(--font-size-xs);margin-bottom:6px;min-height:18px"></div>
<textarea id="config-editor-area" class="form-input" style="font-family:var(--font-mono);font-size:12px;min-height:320px;resize:vertical;tab-size:2;white-space:pre;overflow-x:auto" spellcheck="false" disabled></textarea>
</div>
<div class="config-section" id="config-calibration-section">
<div class="config-section-title">${t('services.configCalibration')}</div>
<div class="form-hint" style="margin-bottom:var(--space-sm)">${t('services.configCalibrationHint')}</div>
<div style="display:flex;gap:var(--space-sm);flex-wrap:wrap;margin-bottom:var(--space-sm)">
<button class="btn btn-primary btn-sm" data-action="calibrate-config-inherit">${t('services.calibrateInherit')}</button>
<button class="btn btn-secondary btn-sm" data-action="calibrate-config-reset">${t('services.calibrateReset')}</button>
</div>
<div style="display:grid;gap:8px;margin-bottom:var(--space-sm)">
<div class="setup-inline-note">${t('services.calibrateInheritHint')}</div>
<div class="setup-inline-note">${t('services.calibrateResetHint')}</div>
</div>
<div id="config-calibration-status" style="font-size:var(--font-size-xs);min-height:18px;color:var(--text-tertiary)"></div>
</div>
<div class="config-section" id="backup-section">
<div class="config-section-title">${t('services.configBackup')}</div>
<div class="form-hint" style="margin-bottom:var(--space-sm)">${t('services.configBackupHint')}</div>
@@ -561,6 +574,12 @@ function bindEvents(page) {
case 'reload-config':
await loadConfigEditor(page)
break
case 'calibrate-config-inherit':
await handleCalibrateConfig(page, 'inherit')
break
case 'calibrate-config-reset':
await handleCalibrateConfig(page, 'reset')
break
case 'create-backup':
await handleCreateBackup(page)
break
@@ -740,6 +759,40 @@ async function handleDeleteBackup(name, page) {
await loadBackups(page)
}
function calibrationSourceLabel(source) {
if (source === 'backup') return t('services.calibrationSourceBackup')
if (source === 'current') return t('services.calibrationSourceCurrent')
return t('services.calibrationSourceEmpty')
}
async function handleCalibrateConfig(page, mode) {
const yes = await showConfirm(mode === 'reset'
? t('services.calibrateResetConfirm')
: t('services.calibrateInheritConfirm'))
if (!yes) return
const status = page.querySelector('#config-calibration-status')
if (status) status.innerHTML = `<span style="color:var(--text-tertiary)">${t('services.calibrating')}</span>`
const result = await api.calibrateOpenclawConfig(mode)
const summary = t('services.calibrationSummary', {
mode: mode === 'reset' ? t('services.calibrateReset') : t('services.calibrateInherit'),
source: calibrationSourceLabel(result?.source),
count: String(result?.inheritedKeys?.length || 0),
})
const warnings = Array.isArray(result?.warnings) ? result.warnings.filter(Boolean) : []
if (status) status.innerHTML = `<span style="color:var(--success)">${escapeHtml(summary)}</span>${warnings.length ? `<br><span style="color:var(--warning)">${escapeHtml(warnings.join(''))}</span>` : ''}`
toast(t('services.calibrationDone') + ' · ' + summary, 'success')
if (warnings.length) toast(warnings.join(''), 'warning')
await Promise.all([
loadConfigEditor(page),
loadBackups(page),
loadServices(page),
])
}
// ===== 配置文件编辑器 =====
let _configOriginal = ''