aicq-openclaw-plugin 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +388 -1
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10965,9 +10965,48 @@ const _T = {
10965
10965
  tab_agents: { zh: '\u{1F4CB} \u667A\u80FD\u4F53', en: '\u{1F4CB} Agents' },
10966
10966
  tab_bindings: { zh: '\u{1F517} \u7ED1\u5B9A', en: '\u{1F517} Bindings' },
10967
10967
  tab_channels: { zh: '\u{1F4E1} \u9891\u9053', en: '\u{1F4E1} Channels' },
10968
+ // Backup
10969
+ nav_backup: { zh: '\u5907\u4EFD', en: 'Backup' },
10970
+ backup_config: { zh: '\u{1F4BE} \u5907\u4EFD\u914D\u7F6E', en: '\u{1F4BE} Backup Configuration' },
10971
+ backup_desc: { zh: '\u5907\u4EFD\u548C\u6062\u590D\u60A8\u7684AICQ\u63D2\u4EF6\u914D\u7F6E\u3002\u652F\u6301\u672C\u5730\u5907\u4EFD\u3001\u6587\u4EF6\u5BFC\u5165\u3001Google Drive\u4E91\u7AEF\u5907\u4EFD\u548CAICQ\u4E91\u7AEF\u5907\u4EFD\u3002', en: 'Backup and restore your AICQ plugin configuration. Supports local backup, file import, Google Drive cloud backup, and AICQ cloud backup.' },
10972
+ backup_local: { zh: '\u{1F4BE} \u5907\u4EFD\u5230\u672C\u5730', en: '\u{1F4BE} Backup to Local' },
10973
+ backup_local_desc: { zh: '\u521B\u5EFA\u4E00\u4E2A\u5E26\u65F6\u95F4\u6233\u7684\u914D\u7F6E\u6587\u4EF6\u5907\u4EFD\u526F\u672C\uFF0C\u4FDD\u5B58\u5728\u914D\u7F6E\u6587\u4EF6\u6240\u5728\u76EE\u5F55\u4E2D\u3002', en: 'Create a timestamped backup copy of the configuration file, saved in the config directory.' },
10974
+ backup_local_btn: { zh: '\u7ACB\u5373\u5907\u4EFD', en: 'Backup Now' },
10975
+ backup_local_success: { zh: '\u672C\u5730\u5907\u4EFD\u6210\u529F\uFF01', en: 'Local backup created successfully!' },
10976
+ backup_local_fail: { zh: '\u672C\u5730\u5907\u4EFD\u5931\u8D25', en: 'Local backup failed' },
10977
+ backup_import: { zh: '\u{1F4C2} \u5BFC\u5165\u914D\u7F6E', en: '\u{1F4C2} Import Configuration' },
10978
+ backup_import_desc: { zh: '\u4ECE\u5907\u4EFD\u6587\u4EF6\u6062\u590D\u914D\u7F6E\u3002\u5C06\u8986\u76D6\u5F53\u524D\u914D\u7F6E\u3002', en: 'Restore configuration from a backup file. This will overwrite the current configuration.' },
10979
+ backup_import_select: { zh: '\u9009\u62E9\u5907\u4EFD\u6587\u4EF6', en: 'Select Backup File' },
10980
+ backup_import_btn: { zh: '\u6062\u590D\u914D\u7F6E', en: 'Restore Config' },
10981
+ backup_import_success: { zh: '\u914D\u7F6E\u5DF2\u4ECE\u5907\u4EFD\u6062\u590D\uFF01', en: 'Configuration restored from backup!' },
10982
+ backup_import_fail: { zh: '\u914D\u7F6E\u6062\u590D\u5931\u8D25', en: 'Configuration restore failed' },
10983
+ backup_import_confirm: { zh: '\u786E\u5B9A\u8981\u4ECE\u5907\u4EFD\u6062\u590D\u914D\u7F6E\u5417\uFF1F\u8FD9\u5C06\u8986\u76D6\u5F53\u524D\u914D\u7F6E\u3002', en: 'Restore from backup? This will overwrite the current configuration.' },
10984
+ backup_list: { zh: '\u{1F4CB} \u5907\u4EFD\u5217\u8868', en: '\u{1F4CB} Backup List' },
10985
+ backup_list_desc: { zh: '\u67E5\u770B\u6240\u6709\u672C\u5730\u5907\u4EFD\u6587\u4EF6\u3002', en: 'View all local backup files.' },
10986
+ backup_list_empty: { zh: '\u6682\u65E0\u672C\u5730\u5907\u4EFD', en: 'No local backups found' },
10987
+ backup_delete: { zh: '\u5220\u9664', en: 'Delete' },
10988
+ backup_delete_confirm: { zh: '\u786E\u5B9A\u5220\u9664\u6B64\u5907\u4EFD\u6587\u4EF6\u5417\uFF1F', en: 'Delete this backup file?' },
10989
+ backup_deleted: { zh: '\u5907\u4EFD\u5DF2\u5220\u9664', en: 'Backup deleted' },
10990
+ backup_google: { zh: '\u2601\uFE0F \u5907\u4EFD\u5230\u8C37\u6B4C\u786C\u76D8', en: '\u2601\uFE0F Backup to Google Drive' },
10991
+ backup_google_desc: { zh: '\u5C06\u914D\u7F6E\u5907\u4EFD\u5230Google Drive\u4E91\u7AEF\u5B58\u50A8\u3002\u9700\u8981\u767B\u5F55Google\u8D26\u53F7\u6388\u6743\u3002', en: 'Backup configuration to Google Drive cloud storage. Requires Google account login authorization.' },
10992
+ backup_google_btn: { zh: '\u8FDE\u63A5Google\u8D26\u53F7', en: 'Connect Google Account' },
10993
+ backup_google_login: { zh: '\u767B\u5F55Google\u8D26\u53F7', en: 'Login to Google' },
10994
+ backup_google_hint: { zh: '\u70B9\u51FB\u6309\u94AE\u8DF3\u8F6C\u5230Google\u6388\u6743\u9875\u9762\uFF0C\u6388\u6743\u540E\u5373\u53EF\u4F7F\u7528\u4E91\u7AEF\u5907\u4EFD\u529F\u80FD\u3002', en: 'Click the button to go to the Google authorization page. After authorization, cloud backup will be available.' },
10995
+ backup_google_setup: { zh: '\u9700\u8981\u914D\u7F6EGoogle API\u51ED\u636E', en: 'Google API credentials required' },
10996
+ backup_google_setup_desc: { zh: 'Google Drive\u5907\u4EFD\u529F\u80FD\u9700\u8981\u914D\u7F6EGoogle Cloud\u9879\u76EEAPI\u51ED\u636E\u3002\u8BF7\u53C2\u8003 Google Cloud Console \u6587\u6863\u8BBE\u7F6E\u3002', en: 'Google Drive backup requires Google Cloud project API credentials. Please refer to Google Cloud Console documentation.' },
10997
+ backup_google_setup_btn: { zh: '\u67E5\u770B\u8BBE\u7F6E\u6307\u5357', en: 'View Setup Guide' },
10998
+ backup_aicq: { zh: '\u{1F517} \u5907\u4EFD\u5230AICQ\u4E91\u7AEF', en: '\u{1F517} Backup to AICQ Cloud' },
10999
+ backup_aicq_desc: { zh: '\u5C06\u914D\u7F6E\u5907\u4EFD\u5230AICQ\u4E91\u7AEF\u670D\u52A1\u5668\u3002\u9700\u8981\u6CE8\u518CAICQ\u8D26\u53F7\u3002', en: 'Backup configuration to AICQ cloud server. Requires an AICQ account.' },
11000
+ backup_aicq_btn: { zh: '\u6CE8\u518CAICQ\u8D26\u53F7', en: 'Register AICQ Account' },
11001
+ backup_aicq_hint: { zh: 'AICQ\u4E91\u7AEF\u5907\u4EFD\u529F\u80FD\u6B63\u5728\u5F00\u53D1\u4E2D\u3002\u6CE8\u518C\u8D26\u53F7\u540E\u5C06\u7B2C\u4E00\u65F6\u95F4\u901A\u77E5\u60A8\u3002', en: 'AICQ cloud backup is under development. Register an account to be notified when available.' },
11002
+ backup_aicq_coming: { zh: '\u5373\u5C06\u4E0A\u7EBF', en: 'Coming Soon' },
11003
+ backup_export: { zh: '\u{1F4E4} \u5BFC\u51FA\u914D\u7F6E\u6587\u4EF6', en: '\u{1F4E4} Export Config File' },
11004
+ backup_export_btn: { zh: '\u4E0B\u8F7D\u914D\u7F6E', en: 'Download Config' },
11005
+ backup_export_fail: { zh: '\u5BFC\u51FA\u5931\u8D25', en: 'Export failed' },
11006
+ size: { zh: '\u5927\u5C0F', en: 'Size' },
10968
11007
  };
10969
11008
  function t(key) { return (_T[key] && _T[key][_lang]) || key; }
10970
- function translateStatic() { document.querySelectorAll('[data-i18n]').forEach(el => { const k = el.getAttribute('data-i18n'); if (k && _T[k]) { el.textContent = _T[k][_lang] || el.textContent; } }); document.querySelectorAll('[data-i18n-ph]').forEach(el => { const k = el.getAttribute('data-i18n-ph'); if (k && _T[k]) { el.placeholder = _T[k][_lang] || el.placeholder; } }); }
11009
+ function translateStatic() { document.querySelectorAll('[data-i18n]').forEach(el => { const k = el.getAttribute('data-i18n'); if (k && _T[k]) { el.textContent = _T[k][_lang] || el.textContent; } }); document.querySelectorAll('[data-i18n-ph]').forEach(el => { const k = el.getAttribute('data-i18n-ph'); if (k && _T[k]) { el.placeholder = _T[k][_lang] || el.placeholder; } }); document.querySelectorAll('[data-t]').forEach(el => { const k = el.getAttribute('data-t'); if (k && _T[k]) { el.textContent = _T[k][_lang] || el.textContent; } }); }
10971
11010
 
10972
11011
  // \u2500\u2500 Globals \u2500\u2500
10973
11012
  const API = '/api';
@@ -11089,6 +11128,7 @@ function loadPage(page) {
11089
11128
  case 'models': loadModels(); break;
11090
11129
  case 'settings': loadSettings(); break;
11091
11130
  case 'openclaw': loadOpenClawConfig(); break;
11131
+ case 'backup': loadBackup(); break;
11092
11132
  }
11093
11133
  }
11094
11134
 
@@ -12904,12 +12944,119 @@ async function saveOpenClawConfig() {
12904
12944
  }
12905
12945
  }
12906
12946
 
12947
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12948
+ // PAGE: Backup
12949
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12950
+ async function loadBackup() {
12951
+ // The backup page content is static HTML with data-t attributes,
12952
+ // so we just need to trigger translation for any dynamic parts
12953
+ translateStatic();
12954
+ }
12955
+
12956
+ async function doBackupLocal() {
12957
+ const btn = document.getElementById('btn-backup-local');
12958
+ if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
12959
+ try {
12960
+ const resp = await api('/backup/local', { method: 'POST', body: JSON.stringify({}) });
12961
+ if (resp.success) {
12962
+ toast(t('backup_local_success'), 'ok');
12963
+ refreshBackupList();
12964
+ } else {
12965
+ toast(resp.error || t('backup_local_fail'), 'err');
12966
+ }
12967
+ } catch(e) {
12968
+ toast(e.message || t('backup_local_fail'), 'err');
12969
+ }
12970
+ if (btn) { btn.disabled = false; btn.textContent = t('backup_local_btn'); }
12971
+ }
12972
+
12973
+ async function doExportConfig() {
12974
+ const btn = document.getElementById('btn-backup-export');
12975
+ if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
12976
+ try {
12977
+ const resp = await fetch(API + '/backup/export', { method: 'POST', headers: { 'Content-Type': 'application/json' } });
12978
+ if (!resp.ok) throw new Error('Export failed');
12979
+ const blob = await resp.blob();
12980
+ const url = URL.createObjectURL(blob);
12981
+ const a = document.createElement('a');
12982
+ a.href = url;
12983
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
12984
+ a.download = 'aicq-config-backup-' + ts + '.json';
12985
+ document.body.appendChild(a);
12986
+ a.click();
12987
+ document.body.removeChild(a);
12988
+ URL.revokeObjectURL(url);
12989
+ toast('Config exported!', 'ok');
12990
+ } catch(e) {
12991
+ toast(e.message || t('backup_export_fail'), 'err');
12992
+ }
12993
+ if (btn) { btn.disabled = false; btn.textContent = t('backup_export_btn'); }
12994
+ }
12995
+
12996
+ async function showBackupList() {
12997
+ const card = document.getElementById('backup-list-card');
12998
+ card.style.display = card.style.display === 'none' ? 'block' : 'none';
12999
+ if (card.style.display === 'block') refreshBackupList();
13000
+ }
13001
+
13002
+ async function refreshBackupList() {
13003
+ const container = document.getElementById('backup-list-content');
13004
+ try {
13005
+ const resp = await api('/backup/list', { method: 'POST', body: JSON.stringify({}) });
13006
+ const backups = resp.backups || [];
13007
+ if (backups.length === 0) {
13008
+ container.innerHTML = '<div class="empty"><div class="icon">\u{1F4CB}</div><p>' + t('backup_list_empty') + '</p></div>';
13009
+ return;
13010
+ }
13011
+ let html = '<table><thead><tr><th>' + t('config_file_label') + '</th><th>' + t('size') + '</th><th>' + t('time') + '</th><th>' + t('actions') + '</th></tr></thead><tbody>';
13012
+ for (const b of backups) {
13013
+ const size = (b.size / 1024).toFixed(1) + ' KB';
13014
+ const time = timeAgo(new Date(b.modified));
13015
+ html += '<tr><td class="mono" style="max-width:300px;overflow:hidden;text-overflow:ellipsis;">' + escHtml(b.filename) + '</td><td>' + size + '</td><td>' + time + '</td><td class="actions-cell"><button class="btn btn-sm btn-ok" onclick="doImportBackup(\\'' + escHtml(b.path) + '\\')">' + t('backup_import_btn') + '</button></td></tr>';
13016
+ }
13017
+ html += '</tbody></table>';
13018
+ container.innerHTML = html;
13019
+ } catch(e) {
13020
+ container.innerHTML = '<div class="empty"><p>Failed to load backups</p></div>';
13021
+ }
13022
+ }
13023
+
13024
+ async function doImportBackup(backupPath) {
13025
+ if (!confirm(t('backup_import_confirm'))) return;
13026
+ try {
13027
+ const resp = await api('/backup/import', { method: 'POST', body: JSON.stringify({ configPath: backupPath }) });
13028
+ if (resp.success) {
13029
+ toast(t('backup_import_success'), 'ok');
13030
+ refreshBackupList();
13031
+ } else {
13032
+ toast(resp.error || t('backup_import_fail'), 'err');
13033
+ }
13034
+ } catch(e) {
13035
+ toast(e.message || t('backup_import_fail'), 'err');
13036
+ }
13037
+ }
13038
+
13039
+ async function doGoogleAuth() {
13040
+ try {
13041
+ const resp = await api('/backup/google-drive-auth', { method: 'POST', body: JSON.stringify({}) });
13042
+ if (resp.authUrl) {
13043
+ window.open(resp.authUrl, '_blank');
13044
+ toast(t('backup_google_login'), 'info');
13045
+ } else {
13046
+ toast(resp.error || t('backup_google_setup'), 'warn');
13047
+ }
13048
+ } catch(e) {
13049
+ toast(e.message || t('backup_google_setup'), 'warn');
13050
+ }
13051
+ }
13052
+
12907
13053
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12908
13054
  // INIT
12909
13055
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12910
13056
  document.addEventListener('DOMContentLoaded', () => {
12911
13057
  $$('.nav-item').forEach(n => n.addEventListener('click', () => navigate(n.dataset.page)));
12912
13058
  $('.toggle-btn')?.addEventListener('click', toggleSidebar);
13059
+ translateStatic();
12913
13060
 
12914
13061
  // Load dashboard
12915
13062
  navigate('dashboard');
@@ -12962,6 +13109,7 @@ var HTML = `<!DOCTYPE html>
12962
13109
  <div class="nav-group">
12963
13110
  <div class="nav-group-title">System</div>
12964
13111
  <div class="nav-item" data-page="settings"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">Settings</span></div>
13112
+ <div class="nav-item" data-page="backup"><span class="nav-icon">\u{1F4BE}</span><span class="nav-label" data-t="nav_backup">Backup</span></div>
12965
13113
  </div>
12966
13114
  <div class="nav-group">
12967
13115
  <div class="nav-group-title">OpenClaw</div>
@@ -13012,6 +13160,92 @@ var HTML = `<!DOCTYPE html>
13012
13160
  <!-- OpenClaw Config -->
13013
13161
  <div class="page" id="page-openclaw"><div id="openclaw-content"></div></div>
13014
13162
 
13163
+ <!-- Backup -->
13164
+ <div class="page" id="page-backup">
13165
+ <div class="card">
13166
+ <div class="card-header">
13167
+ <div class="card-title" data-t="backup_config"></div>
13168
+ </div>
13169
+ <div class="section-desc" data-t="backup_desc"></div>
13170
+ </div>
13171
+
13172
+ <div class="stats-grid">
13173
+ <!-- Local Backup -->
13174
+ <div class="card" style="cursor:default">
13175
+ <div class="stat-icon" style="background: var(--ok-bg);">\u{1F4BE}</div>
13176
+ <div class="stat-label" data-t="backup_local"></div>
13177
+ <div class="stat-sub" data-t="backup_local_desc"></div>
13178
+ <div style="margin-top: 12px;">
13179
+ <button class="btn btn-ok" onclick="doBackupLocal()" id="btn-backup-local">
13180
+ <span data-t="backup_local_btn"></span>
13181
+ </button>
13182
+ </div>
13183
+ </div>
13184
+
13185
+ <!-- Import Config -->
13186
+ <div class="card" style="cursor:default">
13187
+ <div class="stat-icon" style="background: var(--info-bg);">\u{1F4C2}</div>
13188
+ <div class="stat-label" data-t="backup_import"></div>
13189
+ <div class="stat-sub" data-t="backup_import_desc"></div>
13190
+ <div id="backup-import-area" style="margin-top: 12px;">
13191
+ <button class="btn btn-primary" onclick="showBackupList()">
13192
+ <span data-t="backup_import_select"></span>
13193
+ </button>
13194
+ </div>
13195
+ </div>
13196
+
13197
+ <!-- Google Drive -->
13198
+ <div class="card" style="cursor:default">
13199
+ <div class="stat-icon" style="background: rgba(66,133,244,.08);">\u2601\uFE0F</div>
13200
+ <div class="stat-label" data-t="backup_google"></div>
13201
+ <div class="stat-sub" data-t="backup_google_desc"></div>
13202
+ <div style="margin-top: 12px;">
13203
+ <button class="btn btn-warn" onclick="doGoogleAuth()">
13204
+ <span data-t="backup_google_btn"></span>
13205
+ </button>
13206
+ <p style="font-size: 11px; color: var(--text3); margin-top: 8px;" data-t="backup_google_hint"></p>
13207
+ </div>
13208
+ </div>
13209
+
13210
+ <!-- AICQ Cloud -->
13211
+ <div class="card" style="cursor:default">
13212
+ <div class="stat-icon" style="background: var(--accent-bg);">\u{1F517}</div>
13213
+ <div class="stat-label" data-t="backup_aicq"></div>
13214
+ <div class="stat-sub" data-t="backup_aicq_desc"></div>
13215
+ <div style="margin-top: 12px;">
13216
+ <button class="btn btn-primary" disabled onclick="alert(t('backup_aicq_coming'))">
13217
+ <span data-t="backup_aicq_btn"></span>
13218
+ </button>
13219
+ <p style="font-size: 11px; color: var(--text3); margin-top: 8px;" data-t="backup_aicq_hint"></p>
13220
+ </div>
13221
+ </div>
13222
+ </div>
13223
+
13224
+ <!-- Export -->
13225
+ <div class="card">
13226
+ <div class="card-header">
13227
+ <div class="card-title">\u{1F4E4} <span data-t="backup_export"></span></div>
13228
+ </div>
13229
+ <p class="section-desc" style="margin-bottom: 12px;">
13230
+ <button class="btn btn-default" onclick="doExportConfig()" id="btn-backup-export">
13231
+ <span data-t="backup_export_btn"></span>
13232
+ </button>
13233
+ </p>
13234
+ </div>
13235
+
13236
+ <!-- Backup List (hidden by default, shown by modal) -->
13237
+ <div class="card" id="backup-list-card" style="display:none;">
13238
+ <div class="card-header">
13239
+ <div class="card-title" data-t="backup_list"></div>
13240
+ <button class="btn btn-sm btn-ghost" onclick="refreshBackupList()">\u21BB\uFE0F</button>
13241
+ </div>
13242
+ <div class="section-desc" data-t="backup_list_desc"></div>
13243
+ <div id="backup-list-content">
13244
+ <div class="empty"><div class="icon">\u{1F4CB}</div><p data-t="backup_list_empty"></p></div>
13245
+ </div>
13246
+ </div>
13247
+ </div>
13248
+
13015
13249
  </div>
13016
13250
  </main>
13017
13251
  </div>
@@ -14668,6 +14902,159 @@ function createManagementHandler(ctx) {
14668
14902
  name: "AICQ Encrypted Chat"
14669
14903
  });
14670
14904
  }
14905
+ if (apiPath === "/backup/local" && method === "POST") {
14906
+ const result = readConfig();
14907
+ if (!result)
14908
+ return json(res, { success: false, message: "No config file found" }, 400);
14909
+ const configDir = path5.dirname(result.configPath);
14910
+ const configBasename = path5.basename(result.configPath, ".json");
14911
+ const now = /* @__PURE__ */ new Date();
14912
+ const ts = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + "-" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
14913
+ const backupFilename = configBasename + "-backup-" + ts + ".json";
14914
+ const backupPath = path5.join(configDir, backupFilename);
14915
+ const backupData = {
14916
+ ...result.config,
14917
+ _backupMeta: {
14918
+ source: result.configPath,
14919
+ timestamp: now.toISOString(),
14920
+ version: "1.2.0"
14921
+ }
14922
+ };
14923
+ const storePath = path5.join(configDir, ".aicq-data", "plugin-store.json");
14924
+ try {
14925
+ if (fs5.existsSync(storePath)) {
14926
+ const storeRaw = fs5.readFileSync(storePath, "utf-8");
14927
+ backupData._pluginStore = JSON.parse(storeRaw);
14928
+ logger.info("[API] Plugin store data included in backup");
14929
+ }
14930
+ } catch {
14931
+ }
14932
+ try {
14933
+ const backupJson = JSON.stringify(backupData, null, 2);
14934
+ fs5.writeFileSync(backupPath, backupJson, "utf-8");
14935
+ const stats = fs5.statSync(backupPath);
14936
+ logger.info("[API] Local backup created: " + backupPath + " (" + stats.size + " bytes)");
14937
+ return json(res, {
14938
+ success: true,
14939
+ backupPath,
14940
+ backupSize: stats.size,
14941
+ timestamp: now.toISOString()
14942
+ });
14943
+ } catch (err) {
14944
+ const msg = err instanceof Error ? err.message : String(err);
14945
+ logger.error("[API] Backup failed: " + msg);
14946
+ return json(res, { success: false, message: "Failed to create backup: " + msg }, 500);
14947
+ }
14948
+ }
14949
+ if (apiPath === "/backup/import" && method === "POST") {
14950
+ const body = await readBody(req);
14951
+ const configPath = body.configPath;
14952
+ if (!configPath)
14953
+ return json(res, { success: false, message: "Missing configPath in request body" }, 400);
14954
+ if (!fs5.existsSync(configPath)) {
14955
+ return json(res, { success: false, message: "Backup file not found: " + configPath }, 400);
14956
+ }
14957
+ try {
14958
+ const raw = fs5.readFileSync(configPath, "utf-8");
14959
+ const backupData = JSON.parse(raw);
14960
+ const knownKeys = ["agents", "bindings", "auth", "env", "models", "agent"];
14961
+ const hasValidStructure = knownKeys.some((k) => k in backupData) || "_backupMeta" in backupData;
14962
+ if (!hasValidStructure) {
14963
+ return json(res, { success: false, message: "Invalid backup file: does not contain a recognized config structure" }, 400);
14964
+ }
14965
+ const cleanConfig = { ...backupData };
14966
+ delete cleanConfig._backupMeta;
14967
+ delete cleanConfig._pluginStore;
14968
+ const currentConfigPath = findConfigPath();
14969
+ if (!currentConfigPath) {
14970
+ return json(res, { success: false, message: "No active config file found to overwrite" }, 400);
14971
+ }
14972
+ fs5.writeFileSync(currentConfigPath, JSON.stringify(cleanConfig, null, 2), "utf-8");
14973
+ logger.info("[API] Config restored from backup: " + configPath + " -> " + currentConfigPath);
14974
+ return json(res, { success: true, message: "Config restored from " + path5.basename(configPath) });
14975
+ } catch (err) {
14976
+ const msg = err instanceof Error ? err.message : String(err);
14977
+ logger.error("[API] Import failed: " + msg);
14978
+ return json(res, { success: false, message: "Failed to import backup: " + msg }, 500);
14979
+ }
14980
+ }
14981
+ if (apiPath === "/backup/list" && method === "POST") {
14982
+ const configPath = findConfigPath();
14983
+ if (!configPath)
14984
+ return json(res, { backups: [], message: "No config file found" });
14985
+ const configDir = path5.dirname(configPath);
14986
+ const backups = [];
14987
+ try {
14988
+ const files = fs5.readdirSync(configDir);
14989
+ for (const file of files) {
14990
+ if (file.match(/-backup-\d{8}-\d{6}\.json$/)) {
14991
+ const filePath = path5.join(configDir, file);
14992
+ try {
14993
+ const stats = fs5.statSync(filePath);
14994
+ backups.push({
14995
+ filename: file,
14996
+ path: filePath,
14997
+ size: stats.size,
14998
+ modified: stats.mtime.toISOString()
14999
+ });
15000
+ } catch {
15001
+ }
15002
+ }
15003
+ }
15004
+ } catch (err) {
15005
+ const msg = err instanceof Error ? err.message : String(err);
15006
+ logger.warn("[API] Failed to list backups: " + msg);
15007
+ }
15008
+ backups.sort((a, b) => b.modified.localeCompare(a.modified));
15009
+ logger.info("[API] Listed " + backups.length + " local backup(s)");
15010
+ return json(res, { backups });
15011
+ }
15012
+ if (apiPath === "/backup/export" && method === "POST") {
15013
+ const result = readConfig();
15014
+ if (!result)
15015
+ return json(res, { success: false, message: "No config file found" }, 400);
15016
+ try {
15017
+ const configJson = JSON.stringify(result.config, null, 2);
15018
+ const now = /* @__PURE__ */ new Date();
15019
+ const ts = now.getFullYear().toString() + String(now.getMonth() + 1).padStart(2, "0") + String(now.getDate()).padStart(2, "0") + "-" + String(now.getHours()).padStart(2, "0") + String(now.getMinutes()).padStart(2, "0") + String(now.getSeconds()).padStart(2, "0");
15020
+ const downloadFilename = "aicq-config-backup-" + ts + ".json";
15021
+ corsHeaders(res);
15022
+ res.setHeader("Content-Type", "application/octet-stream");
15023
+ res.setHeader("Content-Disposition", 'attachment; filename="' + downloadFilename + '"');
15024
+ res.end(configJson);
15025
+ logger.info("[API] Config exported as download: " + downloadFilename);
15026
+ return;
15027
+ } catch (err) {
15028
+ const msg = err instanceof Error ? err.message : String(err);
15029
+ logger.error("[API] Export failed: " + msg);
15030
+ return json(res, { success: false, message: "Export failed: " + msg }, 500);
15031
+ }
15032
+ }
15033
+ if (apiPath === "/backup/google-drive-auth" && method === "POST") {
15034
+ const redirectUri = encodeURIComponent("http://localhost:3000/api/backup/google-drive/callback");
15035
+ const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/userinfo.profile");
15036
+ const authUrl = "https://accounts.google.com/o/oauth2/v2/auth?client_id=YOUR_CLIENT_ID&redirect_uri=" + redirectUri + "&response_type=code&scope=" + scope + "&access_type=offline&prompt=consent";
15037
+ logger.info("[API] Google Drive OAuth initiated");
15038
+ return json(res, {
15039
+ requiresAuth: true,
15040
+ authUrl,
15041
+ instructions: "Google Drive backup requires OAuth2 setup. Replace YOUR_CLIENT_ID with your Google API client ID. Create credentials at https://console.cloud.google.com/apis/credentials and enable the Google Drive API."
15042
+ });
15043
+ }
15044
+ if (apiPath === "/backup/google-drive" && method === "POST") {
15045
+ logger.info("[API] Google Drive backup requested (not yet configured)");
15046
+ return json(res, {
15047
+ error: "Google Drive backup requires OAuth2 setup. Please configure Google API credentials first.",
15048
+ setup: "See https://console.cloud.google.com/apis/credentials"
15049
+ }, 501);
15050
+ }
15051
+ if (apiPath === "/backup/aicq-cloud" && method === "POST") {
15052
+ logger.info("[API] AICQ cloud backup requested (not yet available)");
15053
+ return json(res, {
15054
+ error: "AICQ cloud backup is not yet available. Please register an AICQ account first.",
15055
+ registerUrl: "https://aicq.online/register"
15056
+ }, 501);
15057
+ }
14671
15058
  res.writeHead(404, { "Content-Type": "application/json" });
14672
15059
  res.end(JSON.stringify({ error: "Not found: " + apiPath }));
14673
15060
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicq-openclaw-plugin",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "AICQ OpenClaw plugin - end-to-end encrypted P2P chat for AI agents with offline queue support",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",