diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..9f9a093 Binary files /dev/null and b/__pycache__/app.cpython-312.pyc differ diff --git a/alert_config.json b/alert_config.json new file mode 100644 index 0000000..9002f40 --- /dev/null +++ b/alert_config.json @@ -0,0 +1,12 @@ +{ + "thresholds": { + "cpu": 75, + "memory": 80, + "disk": 85, + "interval": 60 + }, + "web_settings": { + "external_ip": "121.40.164.32", + "refresh_interval": 30 + } +} \ No newline at end of file diff --git a/app.py b/app.py index 7278620..c867f28 100644 --- a/app.py +++ b/app.py @@ -695,34 +695,34 @@ def api_cron_categories(): @app.route('/api/cron/sync', methods=['POST']) def api_cron_sync(): - """从系统 crontab 同步到数据库""" + """从系统 crontab 同步到数据库(清空旧的,只保留当前系统实际存在的)""" crons = get_system_crons() user_crons = [c for c in crons if c['source'] == 'user'] db = get_db() c = db.cursor() + # 清空数据库中的旧任务 + c.execute("DELETE FROM cron_history WHERE task_id IN (SELECT id FROM cron_tasks)") + c.execute("DELETE FROM cron_logs WHERE task_id IN (SELECT id FROM cron_tasks)") + c.execute("DELETE FROM cron_tasks") + synced_count = 0 for cron in user_crons: - # 检查是否已存在 - c.execute("SELECT id FROM cron_tasks WHERE expression = ? AND command = ?", (cron['expression'], cron['command'])) - existing = c.fetchone() + # 尝试从命令推断分类和名称 + name = guess_cron_name(cron['command']) + category = guess_cron_category(cron['command']) - if not existing: - # 尝试从命令推断分类和名称 - name = guess_cron_name(cron['command']) - category = guess_cron_category(cron['command']) - - c.execute('''INSERT INTO cron_tasks (name, expression, command, description, category, enabled) - VALUES (?, ?, ?, ?, ?, 1)''', - (name, cron['expression'], cron['command'], '', category)) - synced_count += 1 + c.execute('''INSERT INTO cron_tasks (name, expression, command, description, category, enabled) + VALUES (?, ?, ?, ?, ?, 1)''', + (name, cron['expression'], cron['command'], '', category)) + synced_count += 1 db.commit() - log_message(f"从系统 crontab 同步了 {synced_count} 个任务") + log_message(f"从系统 crontab 同步了 {synced_count} 个任务(已清空旧任务)") - return jsonify({'message': f'同步完成,新增 {synced_count} 个任务'}) + return jsonify({'message': f'同步完成,共 {synced_count} 个任务', 'count': synced_count}) # ==================== 系统资源监控 API ==================== @@ -929,6 +929,181 @@ def get_next_run_time(expression): return '无法计算' +# ==================== 配置管理 API ==================== + +CONFIG_FILE = 'config.json' + +def load_full_config(): + """加载完整配置(合并项目和预警配置)""" + # 加载 projects.json 完整数据 + try: + with open(PROJECTS_FILE, 'r', encoding='utf-8') as f: + projects_data = json.load(f) + except: + projects_data = {'server': {}, 'projects': [], 'external_services': []} + + # 加载预警配置 + alert_config = {} + if os.path.exists('alert_config.json'): + try: + with open('alert_config.json', 'r', encoding='utf-8') as f: + alert_config = json.load(f) + except: + pass + + # 合并配置 + config = { + "version": "1.0", + "exported_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "server": projects_data.get('server', {}), + "projects": projects_data.get('projects', []), + "external_services": projects_data.get('external_services', []), + "web_settings": { + "external_ip": alert_config.get('web_settings', {}).get('external_ip', '121.40.164.32'), + "refresh_interval": alert_config.get('web_settings', {}).get('refresh_interval', 30) + }, + "alerts": alert_config.get('alerts', alert_config) # 支持两种结构 + } + return config + +def save_full_config(config): + """保存完整配置""" + # 保存 projects 部分 + projects_data = { + "server": config.get('server', {}), + "projects": config.get('projects', []), + "external_services": config.get('external_services', []) + } + with open(PROJECTS_FILE, 'w', encoding='utf-8') as f: + json.dump(projects_data, f, ensure_ascii=False, indent=2) + + # 保存预警配置和网页设置 + alert_config = { + "web_settings": config.get('web_settings', {}), + "thresholds": config.get('alerts', {}).get('thresholds', config.get('alerts', {})) + } + with open('alert_config.json', 'w', encoding='utf-8') as f: + json.dump(alert_config, f, ensure_ascii=False, indent=2) + + return True + +@app.route('/api/config/export', methods=['GET']) +def api_config_export(): + """导出配置文件""" + try: + config = load_full_config() + return jsonify(config) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/config/import', methods=['POST']) +def api_config_import(): + """导入配置文件""" + try: + config = request.get_json() + if not config: + return jsonify({'error': '无效的配置数据'}), 400 + + # 验证配置结构 + required_keys = ['server', 'projects'] + for key in required_keys: + if key not in config: + return jsonify({'error': f'缺少必要字段: {key}'}), 400 + + # 备份当前配置 + backup_time = datetime.now().strftime('%Y%m%d_%H%M%S') + if os.path.exists(PROJECTS_FILE): + shutil.copy(PROJECTS_FILE, f'{PROJECTS_FILE}.bak.{backup_time}') + if os.path.exists('alert_config.json'): + shutil.copy('alert_config.json', f'alert_config.json.bak.{backup_time}') + + # 保存新配置 + save_full_config(config) + + return jsonify({ + 'message': '配置导入成功', + 'projects_count': len(config.get('projects', [])), + 'services_count': len(config.get('external_services', [])) + }) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/alerts', methods=['GET']) +def api_get_alerts(): + """获取预警配置""" + try: + if os.path.exists('alert_config.json'): + with open('alert_config.json', 'r', encoding='utf-8') as f: + return jsonify(json.load(f)) + return jsonify({}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/alerts', methods=['POST']) +def api_save_alerts(): + """保存预警配置""" + try: + config = request.get_json() + + # 加载现有配置 + existing_config = {} + if os.path.exists('alert_config.json'): + try: + with open('alert_config.json', 'r', encoding='utf-8') as f: + existing_config = json.load(f) + except: + pass + + # 合并配置(保留web_settings) + if 'thresholds' in config: + existing_config['thresholds'] = config['thresholds'] + if 'web_settings' in config: + existing_config['web_settings'] = config['web_settings'] + + with open('alert_config.json', 'w', encoding='utf-8') as f: + json.dump(existing_config, f, ensure_ascii=False, indent=2) + return jsonify({'message': '保存成功'}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/web-settings', methods=['GET']) +def api_get_web_settings(): + """获取网页设置""" + try: + if os.path.exists('alert_config.json'): + with open('alert_config.json', 'r', encoding='utf-8') as f: + config = json.load(f) + web_settings = config.get('web_settings', {}) + return jsonify(web_settings) + return jsonify({'external_ip': '121.40.164.32', 'refresh_interval': 30}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/web-settings', methods=['POST']) +def api_save_web_settings(): + """保存网页设置""" + try: + settings = request.get_json() + + # 加载现有配置 + existing_config = {} + if os.path.exists('alert_config.json'): + try: + with open('alert_config.json', 'r', encoding='utf-8') as f: + existing_config = json.load(f) + except: + pass + + # 更新网页设置 + existing_config['web_settings'] = settings + + with open('alert_config.json', 'w', encoding='utf-8') as f: + json.dump(existing_config, f, ensure_ascii=False, indent=2) + return jsonify({'message': '保存成功'}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + # ==================== 邮件通知 API ==================== @app.route('/api/email/send', methods=['POST']) @@ -1261,11 +1436,18 @@ HTML_TEMPLATE = '''
对外IP: - +
+ +
间隔: @@ -1962,24 +2144,50 @@ HTML_TEMPLATE = ''' } // 加载阈值设置 - function loadThresholds() { - const saved = localStorage.getItem('systemThresholds'); - if (saved) { - thresholds = JSON.parse(saved); - document.getElementById('thresholdCpu').value = thresholds.cpu || 80; - document.getElementById('thresholdMemory').value = thresholds.memory || 85; - document.getElementById('thresholdDisk').value = thresholds.disk || 90; - document.getElementById('thresholdInterval').value = thresholds.interval || 60; + async function loadThresholds() { + try { + const response = await fetch('/api/alerts'); + const config = await response.json(); + + if (config.thresholds) { + thresholds = config.thresholds; + document.getElementById('thresholdCpu').value = thresholds.cpu || 80; + document.getElementById('thresholdMemory').value = thresholds.memory || 85; + document.getElementById('thresholdDisk').value = thresholds.disk || 90; + document.getElementById('thresholdInterval').value = thresholds.interval || 60; + } + } catch (e) { + console.error('加载阈值配置失败:', e); } } // 保存阈值设置 - function saveThresholds() { + async function saveThresholds() { thresholds.cpu = parseInt(document.getElementById('thresholdCpu').value) || 80; thresholds.memory = parseInt(document.getElementById('thresholdMemory').value) || 85; thresholds.disk = parseInt(document.getElementById('thresholdDisk').value) || 90; thresholds.interval = parseInt(document.getElementById('thresholdInterval').value) || 60; - localStorage.setItem('systemThresholds', JSON.stringify(thresholds)); + + try { + // 先获取现有配置 + let config = {}; + try { + const response = await fetch('/api/alerts'); + config = await response.json(); + } catch (e) {} + + // 更新阈值 + config.thresholds = thresholds; + + // 保存到服务器 + await fetch('/api/alerts', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + } catch (e) { + console.error('保存阈值配置失败:', e); + } } // 桌面通知开关(已在外部声明 desktopNotifyEnabled) @@ -2278,9 +2486,19 @@ HTML_TEMPLATE = ''' } // IP 保存 - function saveExternalIp() { + async function saveExternalIp() { externalIp = document.getElementById('externalIp').value.trim(); - localStorage.setItem('externalIp', externalIp); + + try { + await fetch('/api/web-settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ external_ip: externalIp, refresh_interval: parseInt(document.getElementById('refreshInterval').value) || 30 }) + }); + } catch (e) { + console.error('保存失败:', e); + } + renderProjects(); } @@ -3360,6 +3578,64 @@ HTML_TEMPLATE = ''' } } + // 导出配置 + async function exportConfig() { + try { + const response = await fetch('/api/config/export'); + const config = await response.json(); + + // 创建下载文件 + const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `project-panel-config-${new Date().toISOString().slice(0,10)}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + alert('配置导出成功!'); + } catch (e) { + alert('导出失败: ' + e.message); + } + } + + // 导入配置 + async function importConfig(event) { + const file = event.target.files[0]; + if (!file) return; + + if (!confirm('导入配置将覆盖当前所有设置,是否继续?')) { + event.target.value = ''; + return; + } + + try { + const text = await file.text(); + const config = JSON.parse(text); + + const response = await fetch('/api/config/import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(config) + }); + + const result = await response.json(); + + if (result.error) { + alert('导入失败: ' + result.error); + } else { + alert(`配置导入成功!\n项目数: ${result.projects_count}\n外部服务数: ${result.services_count}`); + location.reload(); + } + } catch (e) { + alert('导入失败: ' + e.message); + } + + event.target.value = ''; + } + function scrollToTop() { window.scrollTo({ top: 0, behavior: 'smooth' }); } function scrollToBottom() { window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); } @@ -3382,15 +3658,43 @@ HTML_TEMPLATE = ''' // 初始化 loadProjects(); + loadWebSettings(); // 加载网页设置 - let refreshIntervalMs = parseInt(localStorage.getItem('refreshInterval') || '30') * 1000; - document.getElementById('refreshInterval').value = refreshIntervalMs / 1000; + let refreshIntervalMs = 30 * 1000; // 默认值 let refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs); + // 加载网页设置 + async function loadWebSettings() { + try { + const response = await fetch('/api/web-settings'); + const settings = await response.json(); + + if (settings.external_ip) { + externalIp = settings.external_ip; + document.getElementById('externalIp').value = settings.external_ip; + } + if (settings.refresh_interval) { + document.getElementById('refreshInterval').value = settings.refresh_interval; + clearInterval(refreshTimer); + refreshIntervalMs = settings.refresh_interval * 1000; + refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, refreshIntervalMs); + } + } catch (e) { + console.error('加载网页设置失败:', e); + } + } + function updateRefreshInterval() { const seconds = Math.max(5, Math.min(300, parseInt(document.getElementById('refreshInterval').value) || 30)); document.getElementById('refreshInterval').value = seconds; - localStorage.setItem('refreshInterval', seconds); + + // 保存到服务器 + fetch('/api/web-settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ external_ip: externalIp, refresh_interval: seconds }) + }).catch(e => console.error('保存失败:', e)); + clearInterval(refreshTimer); refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, seconds * 1000); } diff --git a/cron_manager.db b/cron_manager.db index 90ef91f..671868c 100644 Binary files a/cron_manager.db and b/cron_manager.db differ diff --git a/logs/app.log b/logs/app.log index 6da08d6..b0eb8fd 100644 --- a/logs/app.log +++ b/logs/app.log @@ -30271,3 +30271,34 @@ Press CTRL+C to quit 192.168.2.8 - - [24/Apr/2026 10:48:24] "GET /api/projects HTTP/1.1" 200 - 127.0.0.1 - - [24/Apr/2026 10:48:27] "GET / HTTP/1.1" 200 - 192.168.2.14 - - [24/Apr/2026 10:48:28] "GET /api/projects HTTP/1.1" 200 - +[2026-06-01 11:53:42] ⚠️ 进程收到 SIGTERM 信号,即将退出! +[2026-06-01 11:53:43] ================================================== +[2026-06-01 11:53:43] 项目服务管理面板 v2.0.0 启动 +[2026-06-01 11:53:43] 访问地址: http://localhost:16022 +[2026-06-01 11:53:43] 进程PID: 776708 +[2026-06-01 11:53:43] ================================================== +[2026-06-01 11:56:06] ⚠️ 进程收到 SIGTERM 信号,即将退出! +[2026-06-01 11:56:07] ================================================== +[2026-06-01 11:56:07] 项目服务管理面板 v2.0.0 启动 +[2026-06-01 11:56:07] 访问地址: http://localhost:16022 +[2026-06-01 11:56:07] 进程PID: 777176 +[2026-06-01 11:56:07] ================================================== +[2026-06-01 12:01:26] ⚠️ 进程收到 SIGTERM 信号,即将退出! +[2026-06-01 12:01:28] ================================================== +[2026-06-01 12:01:28] 项目服务管理面板 v2.0.0 启动 +[2026-06-01 12:01:28] 访问地址: http://localhost:16022 +[2026-06-01 12:01:28] 进程PID: 778445 +[2026-06-01 12:01:28] ================================================== +[2026-06-01 12:02:47] ⚠️ 进程收到 SIGTERM 信号,即将退出! +[2026-06-01 12:02:49] ================================================== +[2026-06-01 12:02:49] 项目服务管理面板 v2.0.0 启动 +[2026-06-01 12:02:49] 访问地址: http://localhost:16022 +[2026-06-01 12:02:49] 进程PID: 778757 +[2026-06-01 12:02:49] ================================================== +[2026-06-01 12:11:08] ⚠️ 进程收到 SIGTERM 信号,即将退出! +[2026-06-01 12:11:34] ================================================== +[2026-06-01 12:11:34] 项目服务管理面板 v2.0.0 启动 +[2026-06-01 12:11:34] 访问地址: http://localhost:16022 +[2026-06-01 12:11:34] 进程PID: 780597 +[2026-06-01 12:11:34] ================================================== +[2026-06-01 12:12:13] 从系统 crontab 同步了 0 个任务(已清空旧任务) diff --git a/logs/pdf-translate-web.log b/logs/pdf-translate-web.log new file mode 100644 index 0000000..cbfa2c8 --- /dev/null +++ b/logs/pdf-translate-web.log @@ -0,0 +1,6 @@ + +================================================== +[2026-06-01T11:59:52.266560] start +Command: mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown +Directory: /home/openclaw/.openclaw/workspace-hz4th_coder/works/pdf-translate-web +/bin/sh: 1: disown: not found