添加后台进程修复等功能
This commit is contained in:
@@ -1,106 +1,332 @@
|
|||||||
from flask import Flask, render_template, request, jsonify
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
from flask_cors import CORS # 处理跨域
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import schedule # 定时任务库
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ====================== 基础配置 ======================
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
# 允许所有跨域请求(外网访问必备)
|
||||||
|
CORS(app, resources=r"/*")
|
||||||
|
|
||||||
# 配置
|
# 目录配置(自动创建不存在的目录)
|
||||||
TASKS_DIR = os.path.join(os.getcwd(), 'tasks')
|
BASE_DIR = os.getcwd()
|
||||||
CONFIG_FILE = 'config.json'
|
TASKS_DIR = os.path.join(BASE_DIR, "tasks") # 脚本存储目录
|
||||||
LOG_LINES = 200
|
LOG_DIR = os.path.join(BASE_DIR, "logs") # 日志存储目录
|
||||||
|
CONFIG_FILE = os.path.join(BASE_DIR, "config.json") # 配置文件
|
||||||
|
PROCESSES_FILE = os.path.join(BASE_DIR, "processes.json") # 进程持久化文件
|
||||||
|
|
||||||
# 全局变量
|
# 常量配置
|
||||||
SCRIPT_CONFIGS = {}
|
LOG_LINES = 200 # 内存中保留的日志行数(防止内存溢出)
|
||||||
running_processes = {}
|
MONITOR_INTERVAL = 10 # 长期脚本监控间隔(秒)
|
||||||
script_outputs = {}
|
SCHEDULER_INTERVAL = 1 # 定时任务调度间隔(秒)
|
||||||
|
|
||||||
os.makedirs(TASKS_DIR, exist_ok=True)
|
# ====================== 全局变量 ======================
|
||||||
|
SCRIPT_CONFIGS = {} # 脚本配置:{脚本名: {mode, interval, unit...}}
|
||||||
|
running_processes = {} # 长期运行的进程:{脚本名: subprocess.Popen对象}
|
||||||
|
script_outputs = {} # 内存日志缓存:{脚本名: [日志行1, 日志行2...]}
|
||||||
|
scheduled_jobs = {} # 定时任务:{脚本名: schedule.Job对象}
|
||||||
|
scheduler_running = True # 定时调度器运行标志
|
||||||
|
monitor_running = True # 长期脚本监控运行标志
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== 初始化函数 ======================
|
||||||
|
def init_dirs():
|
||||||
|
"""初始化必要目录(tasks/logs)"""
|
||||||
|
for dir_path in [TASKS_DIR, LOG_DIR]:
|
||||||
|
if not os.path.exists(dir_path):
|
||||||
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
|
app.logger.info(f"创建目录:{dir_path}")
|
||||||
|
|
||||||
|
|
||||||
def load_configs():
|
def load_configs():
|
||||||
|
"""加载脚本配置(从config.json)"""
|
||||||
global SCRIPT_CONFIGS
|
global SCRIPT_CONFIGS
|
||||||
try:
|
try:
|
||||||
if os.path.exists(CONFIG_FILE):
|
if os.path.exists(CONFIG_FILE):
|
||||||
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||||||
SCRIPT_CONFIGS = json.load(f)
|
SCRIPT_CONFIGS = json.load(f)
|
||||||
if not isinstance(SCRIPT_CONFIGS, dict):
|
# 校验配置格式,确保每个脚本有基础配置
|
||||||
|
for script_name, config in SCRIPT_CONFIGS.items():
|
||||||
|
if "mode" not in config:
|
||||||
|
config["mode"] = "long-running" # 默认长期运行
|
||||||
|
if config["mode"] == "interval" and ("interval" not in config or "unit" not in config):
|
||||||
|
config["interval"] = 1
|
||||||
|
config["unit"] = "hours" # 默认每小时执行
|
||||||
|
else:
|
||||||
SCRIPT_CONFIGS = {}
|
SCRIPT_CONFIGS = {}
|
||||||
except:
|
app.logger.info("配置文件不存在,初始化空配置")
|
||||||
|
except Exception as e:
|
||||||
SCRIPT_CONFIGS = {}
|
SCRIPT_CONFIGS = {}
|
||||||
|
app.logger.error(f"加载配置失败:{str(e)},使用空配置")
|
||||||
|
|
||||||
|
|
||||||
def save_configs():
|
def save_configs():
|
||||||
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
"""保存脚本配置到config.json"""
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
||||||
json.dump(SCRIPT_CONFIGS, f, indent=2, ensure_ascii=False)
|
json.dump(SCRIPT_CONFIGS, f, indent=2, ensure_ascii=False)
|
||||||
|
app.logger.info("配置保存成功")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"保存配置失败:{str(e)}")
|
||||||
|
raise # 抛出异常让接口返回错误
|
||||||
|
|
||||||
|
|
||||||
|
def save_processes():
|
||||||
|
"""保存当前运行的长期任务进程信息到文件"""
|
||||||
|
processes_data = {}
|
||||||
|
for script_name, proc in running_processes.items():
|
||||||
|
# 仅保存长期运行模式的进程
|
||||||
|
if SCRIPT_CONFIGS.get(script_name, {}).get("mode") == "long-running":
|
||||||
|
processes_data[script_name] = {
|
||||||
|
"pid": proc.pid,
|
||||||
|
"script_name": script_name,
|
||||||
|
"start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"cmd": proc.args # 记录启动命令(用于校验身份)
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
with open(PROCESSES_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(processes_data, f, indent=2, ensure_ascii=False)
|
||||||
|
app.logger.info("进程信息已持久化")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"保存进程信息失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
def is_process_alive(pid, expected_cmd):
|
||||||
|
"""验证进程是否存活且命令行匹配"""
|
||||||
|
try:
|
||||||
|
# Windows系统通过tasklist获取进程信息
|
||||||
|
if os.name == "nt":
|
||||||
|
# 执行tasklist命令获取进程详情
|
||||||
|
result = subprocess.run(
|
||||||
|
["tasklist", "/fi", f"PID eq {pid}", "/fo", "csv", "/v"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
output = result.stdout
|
||||||
|
# 检查PID是否存在
|
||||||
|
if f"{pid}" not in output:
|
||||||
|
return False
|
||||||
|
# 检查命令行是否包含目标脚本路径
|
||||||
|
script_path = get_script_path(expected_cmd[-1]) # 提取脚本路径
|
||||||
|
return script_path in output
|
||||||
|
else:
|
||||||
|
# Linux/macOS系统
|
||||||
|
result = subprocess.run(
|
||||||
|
["ps", "-p", str(pid), "-o", "cmd="],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
return result.returncode == 0 and expected_cmd[-1] in result.stdout
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"验证进程失败:{str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def load_processes():
|
||||||
|
"""程序启动时加载并验证持久化的进程信息"""
|
||||||
|
global running_processes
|
||||||
|
if not os.path.exists(PROCESSES_FILE):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(PROCESSES_FILE, "r", encoding="utf-8") as f:
|
||||||
|
processes_data = json.load(f)
|
||||||
|
|
||||||
|
for script_name, data in processes_data.items():
|
||||||
|
pid = data["pid"]
|
||||||
|
cmd = data["cmd"]
|
||||||
|
# 验证进程是否存活且匹配脚本
|
||||||
|
if is_process_alive(pid, cmd):
|
||||||
|
# 构造伪Popen对象(仅保留必要属性用于管理)
|
||||||
|
proc = subprocess.Popen()
|
||||||
|
proc.pid = pid
|
||||||
|
proc.args = cmd
|
||||||
|
running_processes[script_name] = proc
|
||||||
|
app.logger.info(f"成功接管进程:{script_name}(PID: {pid})")
|
||||||
|
else:
|
||||||
|
app.logger.warning(f"进程已结束或不匹配:{script_name}(PID: {pid})")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"加载进程信息失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== 日志处理函数 ======================
|
||||||
|
def get_log_path(script_name):
|
||||||
|
"""获取脚本对应的日志文件路径"""
|
||||||
|
return os.path.join(LOG_DIR, f"{os.path.splitext(script_name)[0]}.log")
|
||||||
|
|
||||||
|
|
||||||
|
def load_script_log(script_name):
|
||||||
|
"""加载脚本的历史日志(从日志文件)"""
|
||||||
|
log_path = get_log_path(script_name)
|
||||||
|
logs = []
|
||||||
|
if os.path.exists(log_path):
|
||||||
|
try:
|
||||||
|
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
# 读取最后LOG_LINES行,避免大文件加载缓慢
|
||||||
|
lines = f.readlines()[-LOG_LINES:]
|
||||||
|
logs = [line.strip() for line in lines if line.strip()]
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"加载{script_name}日志失败:{str(e)}")
|
||||||
|
logs = [f"⚠️ 加载历史日志失败:{str(e)}"]
|
||||||
|
return logs
|
||||||
|
|
||||||
|
|
||||||
|
def write_script_log(script_name, content):
|
||||||
|
"""写入日志到文件和内存缓存"""
|
||||||
|
# 格式化日志(带时间戳)
|
||||||
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
log_line = f"[{timestamp}] {content}"
|
||||||
|
|
||||||
|
# 1. 写入内存缓存(保留最后LOG_LINES行)
|
||||||
|
if script_name not in script_outputs:
|
||||||
|
script_outputs[script_name] = []
|
||||||
|
script_outputs[script_name].append(log_line)
|
||||||
|
script_outputs[script_name] = script_outputs[script_name][-LOG_LINES:]
|
||||||
|
|
||||||
|
# 2. 写入日志文件
|
||||||
|
log_path = get_log_path(script_name)
|
||||||
|
try:
|
||||||
|
with open(log_path, "a", encoding="utf-8", errors="ignore") as f:
|
||||||
|
f.write(log_line + "\n")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"写入{script_name}日志失败:{str(e)}")
|
||||||
|
# 同时记录日志写入错误到内存
|
||||||
|
error_line = f"[{timestamp}] ⚠️ 日志写入失败:{str(e)}"
|
||||||
|
script_outputs[script_name].append(error_line)
|
||||||
|
script_outputs[script_name] = script_outputs[script_name][-LOG_LINES:]
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== 定时任务处理 ======================
|
||||||
|
def add_scheduled_task(script_name):
|
||||||
|
"""添加定时任务(基于配置的interval和unit)"""
|
||||||
|
global scheduled_jobs
|
||||||
|
try:
|
||||||
|
# 获取脚本配置
|
||||||
|
config = SCRIPT_CONFIGS.get(script_name)
|
||||||
|
if not config or config["mode"] != "interval":
|
||||||
|
app.logger.error(f"{script_name}配置无效,无法添加定时任务")
|
||||||
|
return False
|
||||||
|
|
||||||
|
interval = int(config.get("interval", 1))
|
||||||
|
unit = config.get("unit", "hours")
|
||||||
|
|
||||||
|
# 定义定时任务执行函数
|
||||||
|
def run_scheduled_script():
|
||||||
|
script_path = get_script_path(script_name)
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
write_script_log(script_name, f"❌ 脚本不存在:{script_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构建执行命令
|
||||||
|
if script_name.endswith(".bat"):
|
||||||
|
cmd = ["cmd", "/c", script_path]
|
||||||
|
elif script_name.endswith(".py"):
|
||||||
|
cmd = ["python", script_path]
|
||||||
|
else:
|
||||||
|
write_script_log(script_name, "❌ 不支持的脚本类型")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 执行脚本(单次)
|
||||||
|
try:
|
||||||
|
write_script_log(script_name, "✅ 定时任务开始执行")
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
cwd=TASKS_DIR,
|
||||||
|
timeout=3600 # 超时1小时(可根据需求调整)
|
||||||
|
)
|
||||||
|
# 记录执行结果
|
||||||
|
if result.returncode == 0:
|
||||||
|
write_script_log(script_name, "✅ 定时任务执行成功")
|
||||||
|
if result.stdout:
|
||||||
|
write_script_log(script_name, f"📝 输出:{result.stdout.strip()}")
|
||||||
|
else:
|
||||||
|
write_script_log(script_name, f"❌ 定时任务执行失败(返回码:{result.returncode})")
|
||||||
|
if result.stdout:
|
||||||
|
write_script_log(script_name, f"📝 错误输出:{result.stdout.strip()}")
|
||||||
|
except Exception as e:
|
||||||
|
write_script_log(script_name, f"❌ 定时任务执行异常:{str(e)}")
|
||||||
|
|
||||||
|
# 根据unit设置定时频率
|
||||||
|
job = None
|
||||||
|
if unit == "minutes":
|
||||||
|
job = schedule.every(interval).minutes.do(run_scheduled_script)
|
||||||
|
elif unit == "hours":
|
||||||
|
job = schedule.every(interval).hours.do(run_scheduled_script)
|
||||||
|
elif unit == "days":
|
||||||
|
job = schedule.every(interval).days.do(run_scheduled_script)
|
||||||
|
else:
|
||||||
|
write_script_log(script_name, f"❌ 不支持的时间单位:{unit}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 保存定时任务
|
||||||
|
scheduled_jobs[script_name] = job
|
||||||
|
write_script_log(script_name, f"⏰ 定时任务已添加(每{interval}{unit}执行一次)")
|
||||||
|
app.logger.info(f"{script_name}定时任务添加成功(每{interval}{unit})")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
write_script_log(script_name, f"❌ 添加定时任务失败:{str(e)}")
|
||||||
|
app.logger.error(f"{script_name}添加定时任务失败:{str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remove_scheduled_task(script_name):
|
||||||
|
"""移除定时任务"""
|
||||||
|
global scheduled_jobs
|
||||||
|
if script_name in scheduled_jobs:
|
||||||
|
schedule.cancel_job(scheduled_jobs[script_name])
|
||||||
|
del scheduled_jobs[script_name]
|
||||||
|
write_script_log(script_name, "⏹️ 定时任务已移除")
|
||||||
|
app.logger.info(f"{script_name}定时任务移除成功")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def run_scheduler():
|
||||||
|
"""定时任务调度线程(循环检查任务)"""
|
||||||
|
global scheduler_running
|
||||||
|
app.logger.info("定时任务调度线程已启动")
|
||||||
|
while scheduler_running:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(SCHEDULER_INTERVAL)
|
||||||
|
app.logger.info("定时任务调度线程已停止")
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== 长期运行脚本处理 ======================
|
||||||
def get_script_path(script_name):
|
def get_script_path(script_name):
|
||||||
|
"""获取脚本的完整路径"""
|
||||||
return os.path.join(TASKS_DIR, script_name)
|
return os.path.join(TASKS_DIR, script_name)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
def start_long_running_script(script_name):
|
||||||
def index():
|
"""启动长期运行的脚本(带输出捕获)"""
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/scripts')
|
|
||||||
def api_scripts():
|
|
||||||
if not os.path.exists(TASKS_DIR):
|
|
||||||
return jsonify([])
|
|
||||||
|
|
||||||
files = []
|
|
||||||
for f in os.listdir(TASKS_DIR):
|
|
||||||
path = os.path.join(TASKS_DIR, f)
|
|
||||||
if os.path.isfile(path) and f.endswith(('.bat', '.py')):
|
|
||||||
files.append(f)
|
|
||||||
return jsonify(sorted(files))
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/config')
|
|
||||||
def api_get_config():
|
|
||||||
return jsonify(SCRIPT_CONFIGS)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/config', methods=['POST'])
|
|
||||||
def api_save_config():
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
return jsonify({"error": "Invalid data format"}), 400
|
|
||||||
|
|
||||||
for script_name, config in data.items():
|
|
||||||
SCRIPT_CONFIGS[script_name] = config
|
|
||||||
|
|
||||||
save_configs()
|
|
||||||
return jsonify({"msg": "配置已保存"})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/start/<script_name>')
|
|
||||||
def api_start_script(script_name):
|
|
||||||
script_name = unquote(script_name)
|
|
||||||
if script_name not in SCRIPT_CONFIGS:
|
|
||||||
return jsonify({"error": "未找到配置"}), 404
|
|
||||||
|
|
||||||
if script_name in running_processes:
|
|
||||||
return jsonify({"error": "已在运行"}), 400
|
|
||||||
|
|
||||||
script_path = get_script_path(script_name)
|
script_path = get_script_path(script_name)
|
||||||
if not os.path.exists(script_path):
|
if not os.path.exists(script_path):
|
||||||
return jsonify({"error": f"脚本不存在: {script_path}"}), 404
|
write_script_log(script_name, f"❌ 脚本不存在:{script_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
if script_name.endswith('.bat'):
|
# 构建执行命令
|
||||||
cmd = ['cmd', '/c', script_path]
|
if script_name.endswith(".bat"):
|
||||||
elif script_name.endswith('.py'):
|
cmd = ["cmd", "/c", script_path]
|
||||||
cmd = ['python', script_path]
|
elif script_name.endswith(".py"):
|
||||||
|
cmd = ["python", script_path]
|
||||||
else:
|
else:
|
||||||
return jsonify({"error": "不支持的类型"}), 400
|
write_script_log(script_name, "❌ 不支持的脚本类型")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 定义子线程执行函数
|
||||||
def target():
|
def target():
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
@@ -109,73 +335,383 @@ def api_start_script(script_name):
|
|||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
text=True,
|
text=True,
|
||||||
encoding='utf-8',
|
encoding="utf-8",
|
||||||
|
errors="ignore",
|
||||||
cwd=TASKS_DIR
|
cwd=TASKS_DIR
|
||||||
)
|
)
|
||||||
|
# 记录运行中的进程
|
||||||
running_processes[script_name] = proc
|
running_processes[script_name] = proc
|
||||||
script_outputs[script_name] = []
|
write_script_log(script_name, "✅ 长期运行脚本已启动")
|
||||||
|
# 保存进程信息
|
||||||
|
save_processes()
|
||||||
|
|
||||||
while True:
|
# 实时捕获输出
|
||||||
|
while proc.poll() is None:
|
||||||
line = proc.stdout.readline()
|
line = proc.stdout.readline()
|
||||||
if line:
|
if line:
|
||||||
line = f"[{time.strftime('%H:%M:%S')}] {line.strip()}"
|
write_script_log(script_name, line.strip())
|
||||||
script_outputs[script_name].append(line)
|
time.sleep(0.1) # 减少CPU占用
|
||||||
script_outputs[script_name] = script_outputs[script_name][-LOG_LINES:]
|
|
||||||
if proc.poll() is not None:
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
script_outputs.setdefault(script_name, []).append(f"❌ 执行错误: {e}")
|
|
||||||
finally:
|
|
||||||
running_processes.pop(script_name, None)
|
|
||||||
|
|
||||||
|
# 进程退出处理
|
||||||
|
exit_code = proc.returncode
|
||||||
|
if exit_code == 0:
|
||||||
|
write_script_log(script_name, f"ℹ️ 脚本正常退出(返回码:{exit_code})")
|
||||||
|
else:
|
||||||
|
write_script_log(script_name, f"❌ 脚本异常退出(返回码:{exit_code})")
|
||||||
|
except Exception as e:
|
||||||
|
write_script_log(script_name, f"❌ 脚本执行异常:{str(e)}")
|
||||||
|
finally:
|
||||||
|
# 移除进程记录(监控线程会重启)
|
||||||
|
running_processes.pop(script_name, None)
|
||||||
|
save_processes() # 更新进程信息
|
||||||
|
app.logger.info(f"{script_name}长期运行进程已退出")
|
||||||
|
|
||||||
|
# 启动子线程
|
||||||
thread = threading.Thread(target=target, daemon=True)
|
thread = threading.Thread(target=target, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
return jsonify({"msg": f"✅ 开始执行: {script_name}"})
|
return True
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/stop/<script_name>')
|
def monitor_long_running_scripts():
|
||||||
def api_stop_script(script_name):
|
"""监控长期运行的脚本(仅重启长期模式脚本,排除单次/定时)"""
|
||||||
script_name = unquote(script_name)
|
global monitor_running
|
||||||
if script_name not in running_processes:
|
app.logger.info("长期脚本监控线程已启动")
|
||||||
return jsonify({"msg": "未在运行"})
|
while monitor_running:
|
||||||
|
# 遍历所有配置,仅处理「长期运行」模式的脚本
|
||||||
|
for script_name, config in SCRIPT_CONFIGS.items():
|
||||||
|
if config.get("mode") == "long-running": # 仅长期模式需要监控重启
|
||||||
|
# 检查脚本是否在运行(进程存在且未退出)
|
||||||
|
is_running = script_name in running_processes and running_processes[script_name].poll() is None
|
||||||
|
if not is_running:
|
||||||
|
app.logger.info(f"{script_name}长期脚本未运行,尝试重启")
|
||||||
|
write_script_log(script_name, "⚠️ 脚本未运行,尝试重启...")
|
||||||
|
start_long_running_script(script_name)
|
||||||
|
time.sleep(MONITOR_INTERVAL) # 每10秒检查一次
|
||||||
|
app.logger.info("长期脚本监控线程已停止")
|
||||||
|
|
||||||
proc = running_processes[script_name]
|
|
||||||
proc.terminate()
|
# ====================== 单次运行脚本处理 ======================
|
||||||
|
def start_single_run_script(script_name):
|
||||||
|
"""启动单次运行的脚本(执行一次后停止)"""
|
||||||
|
script_path = get_script_path(script_name)
|
||||||
|
if not os.path.exists(script_path):
|
||||||
|
write_script_log(script_name, f"❌ 脚本不存在:{script_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 构建执行命令
|
||||||
|
if script_name.endswith(".bat"):
|
||||||
|
cmd = ["cmd", "/c", script_path]
|
||||||
|
elif script_name.endswith(".py"):
|
||||||
|
cmd = ["python", script_path]
|
||||||
|
else:
|
||||||
|
write_script_log(script_name, "❌ 不支持的脚本类型")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 定义子线程执行函数
|
||||||
|
def target():
|
||||||
try:
|
try:
|
||||||
proc.wait(timeout=5)
|
write_script_log(script_name, "✅ 单次运行脚本已启动")
|
||||||
except:
|
# 执行脚本(带超时控制)
|
||||||
proc.kill()
|
proc = subprocess.Popen(
|
||||||
running_processes.pop(script_name, None)
|
cmd,
|
||||||
script_outputs.setdefault(script_name, []).append("⏹️ 已停止")
|
stdout=subprocess.PIPE,
|
||||||
return jsonify({"msg": f"⏹️ 已停止: {script_name}"})
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="ignore",
|
||||||
|
cwd=TASKS_DIR
|
||||||
|
)
|
||||||
|
# 记录进程(用于停止功能)
|
||||||
|
running_processes[script_name] = proc
|
||||||
|
|
||||||
|
# 实时捕获输出
|
||||||
|
while proc.poll() is None:
|
||||||
|
line = proc.stdout.readline()
|
||||||
|
if line:
|
||||||
|
write_script_log(script_name, line.strip())
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
@app.route('/api/status/<script_name>')
|
# 记录执行结果
|
||||||
def api_status(script_name):
|
exit_code = proc.returncode
|
||||||
script_name = unquote(script_name)
|
if exit_code == 0:
|
||||||
output = script_outputs.get(script_name, [])
|
write_script_log(script_name, f"✅ 单次运行脚本执行成功(返回码:{exit_code})")
|
||||||
is_running = script_name in running_processes
|
else:
|
||||||
return jsonify({
|
write_script_log(script_name, f"❌ 单次运行脚本执行失败(返回码:{exit_code})")
|
||||||
"running": is_running,
|
|
||||||
"output": output
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/send-input/<script_name>', methods=['POST'])
|
|
||||||
def api_send_input(script_name):
|
|
||||||
script_name = unquote(script_name)
|
|
||||||
if script_name not in running_processes:
|
|
||||||
return jsonify({"error": "脚本未运行"}), 400
|
|
||||||
|
|
||||||
proc = running_processes[script_name]
|
|
||||||
try:
|
|
||||||
proc.stdin.write(request.data + "\n")
|
|
||||||
proc.stdin.flush()
|
|
||||||
return jsonify({"msg": "输入已发送"})
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
write_script_log(script_name, f"❌ 单次运行脚本异常:{str(e)}")
|
||||||
|
finally:
|
||||||
|
# 移除进程记录(执行完成/异常终止)
|
||||||
|
running_processes.pop(script_name, None)
|
||||||
|
app.logger.info(f"{script_name}单次运行脚本已结束")
|
||||||
|
|
||||||
|
# 启动子线程
|
||||||
|
thread = threading.Thread(target=target, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== Flask接口 ======================
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
"""前端页面入口"""
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/scripts")
|
||||||
|
def api_scripts():
|
||||||
|
"""获取脚本列表(含修改时间,支持前端排序)"""
|
||||||
|
try:
|
||||||
|
scripts = []
|
||||||
|
if os.path.exists(TASKS_DIR):
|
||||||
|
for f in os.listdir(TASKS_DIR):
|
||||||
|
f_path = os.path.join(TASKS_DIR, f)
|
||||||
|
# 仅保留.bat/.py文件
|
||||||
|
if os.path.isfile(f_path) and f.endswith((".bat", ".py")):
|
||||||
|
# 获取文件修改时间(时间戳,单位:秒)
|
||||||
|
modify_time = os.path.getmtime(f_path)
|
||||||
|
# 格式化时间为可读格式(备用,前端也可自行格式化)
|
||||||
|
modify_time_str = datetime.fromtimestamp(modify_time).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
scripts.append({
|
||||||
|
"name": f,
|
||||||
|
"modify_time": modify_time, # 时间戳(用于排序)
|
||||||
|
"modify_time_str": modify_time_str # 格式化时间(用于显示)
|
||||||
|
})
|
||||||
|
# 默认按修改时间降序排序(后端先排序,减少前端计算)
|
||||||
|
scripts_sorted = sorted(scripts, key=lambda x: x["modify_time"], reverse=True)
|
||||||
|
return jsonify(scripts_sorted), 200
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"获取脚本列表失败:{str(e)}")
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
@app.route("/api/scripts/search")
|
||||||
|
def api_script_search():
|
||||||
|
"""搜索脚本(支持模糊匹配名称)"""
|
||||||
|
try:
|
||||||
|
# 获取前端传入的搜索关键词
|
||||||
|
keyword = request.args.get("keyword", "").strip().lower()
|
||||||
|
scripts = []
|
||||||
|
if os.path.exists(TASKS_DIR):
|
||||||
|
for f in os.listdir(TASKS_DIR):
|
||||||
|
f_path = os.path.join(TASKS_DIR, f)
|
||||||
|
if os.path.isfile(f_path) and f.endswith((".bat", ".py")):
|
||||||
|
# 模糊匹配(不区分大小写)
|
||||||
|
if keyword in f.lower():
|
||||||
|
modify_time = os.path.getmtime(f_path)
|
||||||
|
modify_time_str = datetime.fromtimestamp(modify_time).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
scripts.append({
|
||||||
|
"name": f,
|
||||||
|
"modify_time": modify_time,
|
||||||
|
"modify_time_str": modify_time_str
|
||||||
|
})
|
||||||
|
# 按修改时间降序排序
|
||||||
|
scripts_sorted = sorted(scripts, key=lambda x: x["modify_time"], reverse=True)
|
||||||
|
return jsonify(scripts_sorted), 200
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"搜索脚本失败:{str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/config")
|
||||||
|
def api_get_config():
|
||||||
|
"""获取所有脚本配置"""
|
||||||
|
return jsonify(SCRIPT_CONFIGS), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/config", methods=["POST"])
|
||||||
|
def api_save_config():
|
||||||
|
"""保存脚本配置(支持三种模式)"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return jsonify({"error": "无效的配置格式(需为JSON对象)"}), 400
|
||||||
|
|
||||||
|
# 更新配置并保存
|
||||||
|
for script_name, config in data.items():
|
||||||
|
# 校验配置必填项
|
||||||
|
if "mode" not in config:
|
||||||
|
return jsonify({"error": f"{script_name}缺少'mode'配置(支持:long-running/interval/single-run)"}), 400
|
||||||
|
# 仅定时模式需要校验interval和unit,单次/长期模式无需
|
||||||
|
if config["mode"] == "interval" and ("interval" not in config or "unit" not in config):
|
||||||
|
return jsonify({"error": f"{script_name}定时模式需配置'interval'和'unit'"}), 400
|
||||||
|
# 保存配置
|
||||||
|
SCRIPT_CONFIGS[script_name] = config
|
||||||
|
|
||||||
|
save_configs()
|
||||||
|
return jsonify({"msg": "配置已保存"}), 200
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"保存配置失败:{str(e)}")
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/start/<script_name>")
|
||||||
|
def api_start_script(script_name):
|
||||||
|
"""启动脚本(支持三种模式:长期/定时/单次)"""
|
||||||
|
script_name = unquote(script_name) # 解码URL编码的脚本名
|
||||||
|
|
||||||
|
# 1. 校验前置条件
|
||||||
|
if script_name not in SCRIPT_CONFIGS:
|
||||||
|
return jsonify({"error": f"未找到{script_name}的配置,请先保存配置"}), 404
|
||||||
|
|
||||||
|
config = SCRIPT_CONFIGS[script_name]
|
||||||
|
mode = config.get("mode", "long-running") # 默认长期运行
|
||||||
|
|
||||||
|
# 2. 根据模式处理
|
||||||
|
if mode == "long-running":
|
||||||
|
# 检查是否已在运行
|
||||||
|
if script_name in running_processes and running_processes[script_name].poll() is None:
|
||||||
|
return jsonify({"error": f"{script_name}已在运行"}), 400
|
||||||
|
# 启动长期脚本
|
||||||
|
success = start_long_running_script(script_name)
|
||||||
|
if success:
|
||||||
|
return jsonify({"msg": f"{script_name}(长期运行模式)已启动"}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"{script_name}启动失败,请查看日志"}), 500
|
||||||
|
|
||||||
|
elif mode == "interval":
|
||||||
|
# 检查是否已添加定时任务
|
||||||
|
if script_name in scheduled_jobs:
|
||||||
|
return jsonify({"error": f"{script_name}定时任务已存在"}), 400
|
||||||
|
# 添加定时任务
|
||||||
|
success = add_scheduled_task(script_name)
|
||||||
|
if success:
|
||||||
|
return jsonify({"msg": f"{script_name}(定时模式)已调度"}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"{script_name}定时任务添加失败"}), 500
|
||||||
|
|
||||||
|
elif mode == "single-run":
|
||||||
|
# 检查是否已在运行
|
||||||
|
if script_name in running_processes and running_processes[script_name].poll() is None:
|
||||||
|
return jsonify({"error": f"{script_name}已在运行"}), 400
|
||||||
|
# 启动单次脚本
|
||||||
|
success = start_single_run_script(script_name)
|
||||||
|
if success:
|
||||||
|
return jsonify({"msg": f"{script_name}(单次模式)已启动"}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"{script_name}启动失败,请查看日志"}), 500
|
||||||
|
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"不支持的运行模式:{mode}"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/stop/<script_name>")
|
||||||
|
def api_stop_script(script_name):
|
||||||
|
"""停止脚本(根据模式停止对应进程或任务)"""
|
||||||
|
script_name = unquote(script_name)
|
||||||
|
config = SCRIPT_CONFIGS.get(script_name)
|
||||||
|
if not config:
|
||||||
|
return jsonify({"error": f"未找到{script_name}的配置"}), 404
|
||||||
|
|
||||||
|
mode = config.get("mode", "long-running")
|
||||||
|
stopped = False
|
||||||
|
|
||||||
|
# 处理长期运行模式
|
||||||
|
if mode == "long-running":
|
||||||
|
if script_name in running_processes:
|
||||||
|
proc = running_processes[script_name]
|
||||||
|
try:
|
||||||
|
# 终止进程
|
||||||
|
proc.terminate()
|
||||||
|
# 等待进程退出
|
||||||
|
time.sleep(1)
|
||||||
|
if proc.poll() is None:
|
||||||
|
proc.kill() # 强制终止
|
||||||
|
stopped = True
|
||||||
|
running_processes.pop(script_name, None)
|
||||||
|
save_processes() # 更新进程信息
|
||||||
|
write_script_log(script_name, "⏹️ 长期运行脚本已停止")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"停止{script_name}失败:{str(e)}")
|
||||||
|
return jsonify({"error": f"停止失败:{str(e)}"}), 500
|
||||||
|
|
||||||
|
# 处理定时模式
|
||||||
|
elif mode == "interval":
|
||||||
|
stopped = remove_scheduled_task(script_name)
|
||||||
|
|
||||||
|
# 处理单次运行模式
|
||||||
|
elif mode == "single-run":
|
||||||
|
if script_name in running_processes:
|
||||||
|
proc = running_processes[script_name]
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
time.sleep(1)
|
||||||
|
if proc.poll() is None:
|
||||||
|
proc.kill()
|
||||||
|
stopped = True
|
||||||
|
running_processes.pop(script_name, None)
|
||||||
|
write_script_log(script_name, "⏹️ 单次运行脚本已停止")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"停止{script_name}失败:{str(e)}")
|
||||||
|
return jsonify({"error": f"停止失败:{str(e)}"}), 500
|
||||||
|
|
||||||
|
if stopped:
|
||||||
|
return jsonify({"msg": f"{script_name}已停止"}), 200
|
||||||
|
else:
|
||||||
|
return jsonify({"error": f"{script_name}未在运行或无法停止"}), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/status/<script_name>")
|
||||||
|
def api_get_status(script_name):
|
||||||
|
"""获取脚本运行状态和输出日志"""
|
||||||
|
script_name = unquote(script_name)
|
||||||
|
config = SCRIPT_CONFIGS.get(script_name, {})
|
||||||
|
mode = config.get("mode", "long-running")
|
||||||
|
|
||||||
|
# 1. 检查运行状态
|
||||||
|
status = "stopped"
|
||||||
|
running = False
|
||||||
|
scheduled = False
|
||||||
|
|
||||||
|
if mode == "long-running":
|
||||||
|
running = script_name in running_processes and running_processes[script_name].poll() is None
|
||||||
|
status = "running" if running else "stopped"
|
||||||
|
elif mode == "interval":
|
||||||
|
scheduled = script_name in scheduled_jobs
|
||||||
|
status = "scheduled" if scheduled else "stopped"
|
||||||
|
elif mode == "single-run":
|
||||||
|
running = script_name in running_processes and running_processes[script_name].poll() is None
|
||||||
|
status = "running" if running else "stopped"
|
||||||
|
|
||||||
|
# 2. 获取调度信息(仅定时模式)
|
||||||
|
schedule_info = ""
|
||||||
|
if mode == "interval" and scheduled:
|
||||||
|
interval = config.get("interval", 1)
|
||||||
|
unit = config.get("unit", "hours")
|
||||||
|
schedule_info = f"每{interval}{unit}执行一次"
|
||||||
|
|
||||||
|
# 3. 获取输出日志(内存缓存 + 历史日志)
|
||||||
|
logs = load_script_log(script_name) # 加载文件日志
|
||||||
|
# 合并内存缓存(内存日志更新更快)
|
||||||
|
if script_name in script_outputs:
|
||||||
|
logs = logs + script_outputs[script_name][len(logs):]
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"status": status,
|
||||||
|
"running": running,
|
||||||
|
"scheduled": scheduled,
|
||||||
|
"mode": mode,
|
||||||
|
"schedule_info": schedule_info,
|
||||||
|
"output": logs
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# ====================== 程序入口 ======================
|
||||||
|
def init_app():
|
||||||
|
"""应用初始化总函数"""
|
||||||
|
init_dirs()
|
||||||
load_configs()
|
load_configs()
|
||||||
app.run(host='0.0.0.0', port=5000, debug=False)
|
load_processes() # 加载并接管进程
|
||||||
|
# 启动监控线程和调度线程
|
||||||
|
threading.Thread(target=monitor_long_running_scripts, daemon=True).start()
|
||||||
|
threading.Thread(target=run_scheduler, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
|
||||||
|
# 注册退出钩子,确保进程信息最终被保存
|
||||||
|
atexit.register(save_processes)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_app()
|
||||||
|
# 启动Flask服务(debug模式便于开发,生产环境需关闭)
|
||||||
|
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||||
+8
-6
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"3.bat": {
|
"3.bat": {
|
||||||
"script": "3.bat",
|
|
||||||
"mode": "interval",
|
"mode": "interval",
|
||||||
"interval": "1",
|
"interval": 1,
|
||||||
"unit": "hours"
|
"unit": "minutes"
|
||||||
},
|
},
|
||||||
"2.bat": {
|
"2.bat": {
|
||||||
"script": "2.bat",
|
|
||||||
"mode": "long-running"
|
"mode": "long-running"
|
||||||
},
|
},
|
||||||
"1.bat": {
|
"1.bat": {
|
||||||
"script": "1.bat",
|
"mode": "single-run"
|
||||||
"mode": "long-running"
|
},
|
||||||
|
"[object Object]": {
|
||||||
|
"mode": "interval",
|
||||||
|
"interval": 1,
|
||||||
|
"unit": "minutes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+643
-140
@@ -1,39 +1,131 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>脚本管理平台</title>
|
<title>脚本管理平台</title>
|
||||||
|
<!-- 引入Bootstrap和图标库 -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
||||||
<style>
|
<style>
|
||||||
|
/* 全局样式 */
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', sans-serif;
|
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 主容器样式调整:适配拖拽分隔线 */
|
||||||
.main-container {
|
.main-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 250px 1fr;
|
/* 初始布局:左侧280px + 分隔线4px + 右侧自适应 */
|
||||||
gap: 15px;
|
grid-template-columns: 540px 4px 1fr;
|
||||||
|
gap: 0; /* 取消间隙,避免分隔线与面板之间有空隙 */
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 左侧脚本列表面板 */
|
||||||
#script-list-panel {
|
#script-list-panel {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px 0 0 8px; /* 左侧圆角,右侧与分隔线贴合 */
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
width: 100%; /* 宽度由grid控制,内部自适应 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右侧面板样式调整 */
|
||||||
|
#right-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 15px; /* 右侧与分隔线保持原有的15px间隙 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽分隔线样式 */
|
||||||
|
.resize-handle {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
cursor: col-resize; /* 鼠标悬停时显示水平调整光标 */
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
.resize-handle:hover {
|
||||||
|
background-color: #0d6efd; /* 悬停时变蓝色,提示可拖拽 */
|
||||||
|
}
|
||||||
|
.resize-handle:active {
|
||||||
|
background-color: #0b5ed7; /* 拖拽时加深蓝色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧输出区域 */
|
||||||
#output-area {
|
#output-area {
|
||||||
background: #000;
|
background: #1e1e1e;
|
||||||
color: #0f0;
|
color: #dcdcdc;
|
||||||
font-family: 'Consolas', monospace;
|
font-family: 'Consolas', 'Microsoft YaHei Mono', monospace;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow-y: auto;
|
overflow-y: auto; /* 保持滚动 */
|
||||||
height: calc(100% - 220px);
|
height: 500px; /* 固定高度 500px */
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
/* 输出行样式(区分类型) */
|
||||||
|
.output-line {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索框样式优化 */
|
||||||
|
#script-search {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
/* 排序下拉框样式 */
|
||||||
|
#script-sort {
|
||||||
|
width: auto;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
/* 脚本项的修改时间显示优化 */
|
||||||
|
.mode-info .text-xs {
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
/* 响应式调整:小屏幕时搜索框与排序控件换行 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-container {
|
||||||
|
grid-template-columns: 1fr; /* 单列布局 */
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
#resize-handle {
|
||||||
|
display: none; /* 小屏幕隐藏分隔线 */
|
||||||
|
}
|
||||||
|
#right-panel {
|
||||||
|
padding-left: 0; /* 取消右侧间隙 */
|
||||||
|
}
|
||||||
|
#script-list-panel {
|
||||||
|
border-radius: 8px; /* 恢复全圆角 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line .timestamp {
|
||||||
|
color: #888; /* 时间戳灰色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line.success {
|
||||||
|
color: #4CAF50; /* 成功绿色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line.error {
|
||||||
|
color: #f44336; /* 错误红色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line.warning {
|
||||||
|
color: #ff9800; /* 警告橙色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-line.info {
|
||||||
|
color: #2196F3; /* 信息蓝色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置面板 */
|
||||||
#config-panel {
|
#config-panel {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -41,86 +133,192 @@
|
|||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 脚本列表项 */
|
||||||
.script-item {
|
.script-item {
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 8px;
|
||||||
border-left: 3px solid transparent;
|
border-left: 3px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-item:hover {
|
.script-item:hover {
|
||||||
background: #e9ecef;
|
background: #e9ecef;
|
||||||
|
transform: translateX(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 脚本项激活状态 */
|
||||||
.script-item.active {
|
.script-item.active {
|
||||||
border-left-color: #0d6efd;
|
border-left-color: #0d6efd; /* 蓝色边框 */
|
||||||
background: #e9ecef;
|
background: #e9ecef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 状态指示器(圆形) */
|
||||||
.status-indicator {
|
.status-indicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 10px;
|
width: 12px;
|
||||||
height: 10px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 6px;
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态颜色:运行中(绿)、调度中(蓝)、停止(红) */
|
||||||
|
.status-running {
|
||||||
|
background: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-scheduled {
|
||||||
|
background: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-stopped {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态文本样式 */
|
||||||
|
.status-text {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.running {
|
||||||
|
background: #d1e7dd;
|
||||||
|
color: #198754;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.scheduled {
|
||||||
|
background: #cfe2ff;
|
||||||
|
color: #0d6efd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text.stopped {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式优化 */
|
||||||
|
.btn-sm {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 清空输出按钮 */
|
||||||
|
#clear-output {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整(小屏幕单列布局) */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.main-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output-area {
|
||||||
|
height: 300px;
|
||||||
}
|
}
|
||||||
.status-running { background: #198754; }
|
|
||||||
.status-stopped { background: #dc3545; }
|
|
||||||
.form-control, .form-select {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h4 class="mb-3">脚本管理平台</h4>
|
<!-- 页面标题 -->
|
||||||
|
<h4 class="mb-3 d-flex justify-content-between align-items-center">
|
||||||
|
<span>脚本管理平台</span>
|
||||||
|
<small class="text-muted" id="system-status">系统运行中</small>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<!-- 主容器 -->
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<!-- 左侧脚本列表 -->
|
<!-- 左侧脚本列表面板 -->
|
||||||
<div id="script-list-panel">
|
<div id="script-list-panel" class="panel-resizable">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex flex-column gap-3 mb-3">
|
||||||
<h5>脚本列表</h5>
|
<!-- 新增:搜索框 -->
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</span>
|
||||||
|
<input type="text" id="script-search" class="form-control" placeholder="搜索脚本(名称模糊匹配)"
|
||||||
|
oninput="searchScripts()">
|
||||||
|
<button class="btn btn-outline-secondary" type="button" onclick="clearSearch()">
|
||||||
|
<i class="bi bi-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- 新增:排序控制 -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="mb-0">脚本列表</h5>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<select id="script-sort" class="form-select form-select-sm" onchange="sortScripts()">
|
||||||
|
<option value="modify_time_desc">最近修改时间 ↓</option>
|
||||||
|
<option value="name_asc">名称 ↓(A-Z)</option>
|
||||||
|
<option value="name_desc">名称 ↑(Z-A)</option>
|
||||||
|
</select>
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="loadScripts()">
|
<button class="btn btn-sm btn-outline-secondary" onclick="loadScripts()">
|
||||||
<i class="bi bi-arrow-clockwise"></i> 刷新
|
<i class="bi bi-arrow-clockwise"></i> 刷新
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 脚本列表容器 -->
|
||||||
<div id="script-list"></div>
|
<div id="script-list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 新增:拖拽分隔线 -->
|
||||||
<!-- 右侧主区域 -->
|
<div id="resize-handle" class="resize-handle" title="拖拽调整宽度"></div>
|
||||||
<div>
|
<!-- 右侧:输出区域 + 配置面板 -->
|
||||||
<!-- 输出控制台 -->
|
<div id="right-panel">
|
||||||
|
<!-- 输出控制台(带清空按钮) -->
|
||||||
|
<div class="position-relative">
|
||||||
|
<button id="clear-output" class="btn btn-sm btn-outline-light" onclick="clearOutput()">
|
||||||
|
<i class="bi bi-trash"></i> 清空输出
|
||||||
|
</button>
|
||||||
<div id="output-area"></div>
|
<div id="output-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 配置面板 -->
|
<!-- 脚本配置面板 -->
|
||||||
<div id="config-panel">
|
<div id="config-panel">
|
||||||
<h5>脚本配置</h5>
|
<h5 class="mb-3">脚本配置</h5>
|
||||||
<form id="config-form">
|
<form id="config-form">
|
||||||
|
<!-- 选择脚本 -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">选择脚本</label>
|
<label class="form-label">选择脚本 <span class="text-danger">*</span></label>
|
||||||
<select class="form-select" id="config-script" required>
|
<select class="form-select" id="config-script" required>
|
||||||
<option value="">请选择脚本...</option>
|
<option value="">请选择脚本...</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 运行模式 -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">运行模式</label>
|
<label class="form-label">运行模式 <span class="text-danger">*</span></label>
|
||||||
<select class="form-select" id="config-mode">
|
<select class="form-select" id="config-mode" required>
|
||||||
<option value="long-running">长期运行</option>
|
<option value="single-run">单次运行(执行后自动停止)</option>
|
||||||
|
<option value="long-running">长期运行(退出自动重启)</option>
|
||||||
<option value="interval">定时执行</option>
|
<option value="interval">定时执行</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 定时执行配置(默认隐藏) -->
|
||||||
<div id="interval-config" style="display: none;">
|
<div id="interval-config" style="display: none;">
|
||||||
<label class="form-label">执行周期</label>
|
<label class="form-label">执行周期 <span class="text-danger">*</span></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="number" class="form-control" id="config-interval" value="1" min="1">
|
<input type="number" class="form-control" id="config-interval" value="1" min="1" required>
|
||||||
<select class="form-select" id="config-unit">
|
<select class="form-select" id="config-unit" required>
|
||||||
<option value="minutes">分钟</option>
|
<option value="minutes">分钟</option>
|
||||||
<option value="hours">小时</option>
|
<option value="hours">小时</option>
|
||||||
<option value="days">天</option>
|
<option value="days">天</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-text">定时任务将按设定周期自动执行</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 保存配置按钮 -->
|
||||||
<button type="submit" class="btn btn-primary w-100">
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
<i class="bi bi-save"></i> 保存配置
|
<i class="bi bi-save"></i> 保存配置
|
||||||
</button>
|
</button>
|
||||||
@@ -129,161 +327,466 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 引入依赖JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
let currentScript = null;
|
// 全局变量
|
||||||
let allScripts = [];
|
const OUTPUT_UPDATE_INTERVAL = 2000; // 输出更新间隔(2秒)
|
||||||
|
let allScripts = []; // 存储所有脚本对象:[{name: "xxx.bat", modify_time: ...}, ...]
|
||||||
|
let filteredScripts = []; // 过滤后的脚本列表
|
||||||
|
let currentScript = null; // 当前选中的脚本名称
|
||||||
|
let currentSearchKeyword = "";
|
||||||
|
|
||||||
|
// ====================== 初始化 ======================
|
||||||
|
$(function () {
|
||||||
// 加载脚本列表
|
// 加载脚本列表
|
||||||
|
loadScripts();
|
||||||
|
// 监听运行模式切换(显示/隐藏定时配置)
|
||||||
|
$('#config-mode').change(function () {
|
||||||
|
toggleIntervalConfig($(this).val());
|
||||||
|
});
|
||||||
|
// 监听配置表单提交
|
||||||
|
$('#config-form').submit(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
saveConfig();
|
||||||
|
});
|
||||||
|
// 定时更新输出区域
|
||||||
|
setInterval(() => {
|
||||||
|
if (currentScript) updateOutput();
|
||||||
|
}, OUTPUT_UPDATE_INTERVAL);
|
||||||
|
// 初始化拖拽功能
|
||||||
|
initResizeHandle();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ====================== 脚本列表相关 ======================
|
||||||
|
/**
|
||||||
|
* 加载脚本列表(从后端API获取)
|
||||||
|
*/
|
||||||
function loadScripts() {
|
function loadScripts() {
|
||||||
$.get('/api/scripts', function(files) {
|
$.get('/api/scripts', function(scripts) {
|
||||||
allScripts = files;
|
// 校验后端返回数据格式
|
||||||
|
if (!Array.isArray(scripts)) {
|
||||||
|
alert('加载脚本失败:后端返回非数组格式');
|
||||||
|
allScripts = [];
|
||||||
|
filteredScripts = [];
|
||||||
renderScriptList();
|
renderScriptList();
|
||||||
|
updateConfigScriptSelect();
|
||||||
const select = $('#config-script');
|
return;
|
||||||
select.empty().append('<option value="">请选择脚本...</option>');
|
|
||||||
files.forEach(f => select.append(`<option value="${f}">${f}</option>`));
|
|
||||||
|
|
||||||
if (currentScript) {
|
|
||||||
select.val(currentScript);
|
|
||||||
loadConfig(currentScript);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 过滤并验证脚本对象
|
||||||
|
allScripts = scripts.filter(item => {
|
||||||
|
return typeof item === 'object' && item !== null &&
|
||||||
|
'name' in item && typeof item.name === 'string' && item.name.trim() !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
currentSearchKeyword = "";
|
||||||
|
$('#script-search').val("");
|
||||||
|
sortScripts(); // 排序后渲染列表
|
||||||
|
updateConfigScriptSelect();
|
||||||
|
|
||||||
|
// 恢复当前选中脚本状态
|
||||||
|
if (currentScript) {
|
||||||
|
const scriptExists = allScripts.some(s => s.name === currentScript);
|
||||||
|
if (scriptExists) {
|
||||||
|
$('#config-script').val(currentScript);
|
||||||
|
loadConfig(currentScript);
|
||||||
|
updateOutput();
|
||||||
|
} else {
|
||||||
|
currentScript = null;
|
||||||
|
$('#output-area').empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).fail(function(xhr) {
|
||||||
|
alert('加载脚本列表失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||||
|
allScripts = [];
|
||||||
|
filteredScripts = [];
|
||||||
|
renderScriptList();
|
||||||
|
updateConfigScriptSelect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 渲染脚本列表
|
/**
|
||||||
|
* 搜索脚本(实时模糊匹配)
|
||||||
|
*/
|
||||||
|
function searchScripts() {
|
||||||
|
const keyword = $('#script-search').val().trim().toLowerCase();
|
||||||
|
currentSearchKeyword = keyword;
|
||||||
|
|
||||||
|
if (keyword === "") {
|
||||||
|
filteredScripts = [...allScripts];
|
||||||
|
} else {
|
||||||
|
filteredScripts = allScripts.filter(scriptObj =>
|
||||||
|
scriptObj.name.toLowerCase().includes(keyword)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
sortScripts(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空搜索
|
||||||
|
*/
|
||||||
|
function clearSearch() {
|
||||||
|
$('#script-search').val("");
|
||||||
|
currentSearchKeyword = "";
|
||||||
|
filteredScripts = [...allScripts];
|
||||||
|
sortScripts(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排序脚本(支持按修改时间/名称排序)
|
||||||
|
* @param {boolean} reload - 是否重新加载全部脚本(默认true)
|
||||||
|
*/
|
||||||
|
function sortScripts(reload = true) {
|
||||||
|
if (reload) {
|
||||||
|
filteredScripts = [...allScripts];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortType = $('#script-sort').val();
|
||||||
|
switch (sortType) {
|
||||||
|
case "modify_time_desc":
|
||||||
|
filteredScripts.sort((a, b) => b.modify_time - a.modify_time);
|
||||||
|
break;
|
||||||
|
case "name_asc":
|
||||||
|
filteredScripts.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
break;
|
||||||
|
case "name_desc":
|
||||||
|
filteredScripts.sort((a, b) => b.name.localeCompare(a.name));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
renderScriptList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染脚本列表
|
||||||
|
*/
|
||||||
function renderScriptList() {
|
function renderScriptList() {
|
||||||
const container = $('#script-list').empty();
|
const container = $('#script-list').empty();
|
||||||
|
if (filteredScripts.length === 0) {
|
||||||
|
const tip = currentSearchKeyword ?
|
||||||
|
`<i class="bi bi-search"></i> 未找到包含"${currentSearchKeyword}"的脚本` :
|
||||||
|
`<i class="bi bi-folder-open"></i> tasks目录下无脚本文件(支持.bat/.py)`;
|
||||||
|
container.append(`
|
||||||
|
<div class="text-center text-muted py-3">
|
||||||
|
${tip}
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
allScripts.forEach(scriptName => {
|
// 遍历过滤后的脚本对象数组
|
||||||
$.get(`/api/status/${encodeURIComponent(scriptName)}`, function(status) {
|
filteredScripts.forEach(scriptObj => {
|
||||||
const isRunning = status.running;
|
const scriptName = scriptObj.name.trim();
|
||||||
const isActive = currentScript === scriptName;
|
if (!scriptName) return;
|
||||||
|
|
||||||
const item = $(`
|
const modifyTimeStr = scriptObj.modify_time_str || '未知时间';
|
||||||
<div class="script-item ${isActive ? 'active' : ''}" onclick="selectScript('${scriptName}')">
|
getScriptStatus(scriptName, function(status) {
|
||||||
|
// 状态样式处理
|
||||||
|
const statusClass = status.status === 'running' ? 'status-running' :
|
||||||
|
status.status === 'scheduled' ? 'status-scheduled' : 'status-stopped';
|
||||||
|
const statusText = status.status === 'running' ? '运行中' :
|
||||||
|
status.status === 'scheduled' ? '已调度' : '停止';
|
||||||
|
const statusTextClass = `status-text ${status.status}`;
|
||||||
|
|
||||||
|
// 按钮处理
|
||||||
|
let btnClass, btnIcon, btnText, btnClick;
|
||||||
|
if (status.running || status.scheduled) {
|
||||||
|
btnClass = 'btn-danger';
|
||||||
|
btnIcon = 'bi-stop-fill';
|
||||||
|
btnText = '停止';
|
||||||
|
btnClick = `stopScript('${escapeHtml(scriptName)}')`;
|
||||||
|
} else {
|
||||||
|
btnClass = 'btn-success';
|
||||||
|
btnIcon = 'bi-play-fill';
|
||||||
|
btnText = '启动';
|
||||||
|
btnClick = `startScript('${escapeHtml(scriptName)}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建脚本项HTML
|
||||||
|
const scriptItemHtml = `
|
||||||
|
<div class="script-item ${currentScript === scriptName ? 'active' : ''}"
|
||||||
|
onclick="selectScript('${escapeHtml(scriptName)}')">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<span class="status-indicator ${isRunning ? 'status-running' : 'status-stopped'}"></span>
|
<div class="d-flex align-items-center">
|
||||||
<strong>${scriptName}</strong>
|
<span class="status-indicator ${statusClass}"></span>
|
||||||
|
<span class="script-name">${escapeHtml(scriptName)}</span>
|
||||||
|
<span class="${statusTextClass}">${statusText}</span>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm ${isRunning ? 'btn-danger' : 'btn-success'}"
|
<div class="mode-info text-xs mt-1">
|
||||||
onclick="event.stopPropagation(); ${isRunning ? `stopScript('${scriptName}')` : `startScript('${scriptName}')`}">
|
<span class="text-muted">加载模式信息中...</span>
|
||||||
${isRunning ? '<i class="bi bi-stop-fill"></i>' : '<i class="bi bi-play-fill"></i>'}
|
</div>
|
||||||
${isRunning ? '停止' : '启动'}
|
</div>
|
||||||
|
<button class="btn btn-sm ${btnClass}"
|
||||||
|
onclick="event.stopPropagation(); ${btnClick}">
|
||||||
|
<i class="bi ${btnIcon}"></i> ${btnText}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
`;
|
||||||
|
container.append(scriptItemHtml);
|
||||||
|
|
||||||
|
// 获取并更新模式信息
|
||||||
|
getScriptMode(scriptName, function(mode) {
|
||||||
|
const modeText = mode === 'long-running' ? '长期运行' :
|
||||||
|
mode === 'interval' ? '定时执行' : '单次运行';
|
||||||
|
const scheduleInfo = status.schedule_info ? ` | ${status.schedule_info}` : '';
|
||||||
|
$(`.script-item:has(.script-name:contains('${escapeHtml(scriptName)}')) .mode-info`).html(`
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span class="text-muted">模式:${modeText}${scheduleInfo}</span>
|
||||||
|
<span class="text-muted">最近修改:${modifyTimeStr}</span>
|
||||||
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
container.append(item);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
if (currentScript) updateOutput();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择脚本
|
/**
|
||||||
function selectScript(name) {
|
* 更新配置面板的脚本下拉框
|
||||||
currentScript = name;
|
*/
|
||||||
$('.script-item').removeClass('active');
|
function updateConfigScriptSelect() {
|
||||||
$(`.script-item:contains('${name}')`).addClass('active');
|
const select = $('#config-script').empty();
|
||||||
$('#config-script').val(name);
|
select.append('<option value="">请选择脚本...</option>');
|
||||||
loadConfig(name);
|
|
||||||
|
filteredScripts.forEach(scriptObj => {
|
||||||
|
if (typeof scriptObj === 'object' && scriptObj !== null &&
|
||||||
|
typeof scriptObj.name === 'string' && scriptObj.name.trim()) {
|
||||||
|
|
||||||
|
const scriptName = scriptObj.name.trim();
|
||||||
|
const escapedName = escapeHtml(scriptName);
|
||||||
|
select.append(`<option value="${escapedName}">${escapedName}</option>`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 恢复当前选中状态
|
||||||
|
if (currentScript) {
|
||||||
|
const escapedScript = escapeHtml(currentScript);
|
||||||
|
if (select.find(`option[value="${escapedScript}"]`).length) {
|
||||||
|
select.val(escapedScript);
|
||||||
|
} else {
|
||||||
|
currentScript = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 脚本操作相关 ======================
|
||||||
|
/**
|
||||||
|
* 选择脚本
|
||||||
|
* @param {string} scriptName - 脚本名称
|
||||||
|
*/
|
||||||
|
function selectScript(scriptName) {
|
||||||
|
currentScript = scriptName;
|
||||||
|
$('#config-script').val(scriptName);
|
||||||
|
loadConfig(scriptName);
|
||||||
updateOutput();
|
updateOutput();
|
||||||
|
renderScriptList(); // 刷新列表以更新选中状态
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载配置
|
/**
|
||||||
|
* 启动脚本
|
||||||
|
* @param {string} scriptName - 脚本名称
|
||||||
|
*/
|
||||||
|
function startScript(scriptName) {
|
||||||
|
$.get(`/api/start/${encodeURIComponent(scriptName)}`)
|
||||||
|
.done(function(data) {
|
||||||
|
alert(data.msg);
|
||||||
|
updateOutput();
|
||||||
|
renderScriptList();
|
||||||
|
})
|
||||||
|
.fail(function(xhr) {
|
||||||
|
alert('启动失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止脚本
|
||||||
|
* @param {string} scriptName - 脚本名称
|
||||||
|
*/
|
||||||
|
function stopScript(scriptName) {
|
||||||
|
$.get(`/api/stop/${encodeURIComponent(scriptName)}`)
|
||||||
|
.done(function(data) {
|
||||||
|
alert(data.msg);
|
||||||
|
updateOutput();
|
||||||
|
renderScriptList();
|
||||||
|
})
|
||||||
|
.fail(function(xhr) {
|
||||||
|
alert('停止失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取脚本状态
|
||||||
|
* @param {string} scriptName - 脚本名称
|
||||||
|
* @param {function} callback - 回调函数
|
||||||
|
*/
|
||||||
|
function getScriptStatus(scriptName, callback) {
|
||||||
|
$.get(`/api/status/${encodeURIComponent(scriptName)}`)
|
||||||
|
.done(function(data) {
|
||||||
|
callback(data);
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
callback({ status: 'stopped', running: false, scheduled: false });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取脚本运行模式
|
||||||
|
* @param {string} scriptName - 脚本名称
|
||||||
|
* @param {function} callback - 回调函数
|
||||||
|
*/
|
||||||
|
function getScriptMode(scriptName, callback) {
|
||||||
|
$.get('/api/config')
|
||||||
|
.done(function(configs) {
|
||||||
|
const mode = configs[scriptName]?.mode || 'single-run';
|
||||||
|
callback(mode);
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
callback('single-run');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 配置相关 ======================
|
||||||
|
/**
|
||||||
|
* 加载脚本配置
|
||||||
|
* @param {string} scriptName - 脚本名称
|
||||||
|
*/
|
||||||
function loadConfig(scriptName) {
|
function loadConfig(scriptName) {
|
||||||
$.get('/api/config', function(configs) {
|
$.get('/api/config')
|
||||||
const config = configs[scriptName];
|
.done(function(configs) {
|
||||||
if (config) {
|
const config = configs[scriptName] || { mode: 'single-run' };
|
||||||
$('#config-mode').val(config.mode || 'long-running');
|
$('#config-mode').val(config.mode);
|
||||||
toggleInterval(config.mode);
|
toggleIntervalConfig(config.mode);
|
||||||
|
|
||||||
if (config.mode === 'interval') {
|
if (config.mode === 'interval') {
|
||||||
$('#config-interval').val(config.interval || 1);
|
$('#config-interval').val(config.interval || 1);
|
||||||
$('#config-unit').val(config.unit || 'hours');
|
$('#config-unit').val(config.unit || 'hours');
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.fail(function() {
|
||||||
|
alert('加载配置失败');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换间隔配置显示
|
/**
|
||||||
function toggleInterval(mode) {
|
* 保存脚本配置
|
||||||
$('#interval-config').toggle(mode === 'interval');
|
*/
|
||||||
}
|
function saveConfig() {
|
||||||
|
|
||||||
// 启动脚本
|
|
||||||
function startScript(name) {
|
|
||||||
$.get(`/api/start/${encodeURIComponent(name)}`, function(res) {
|
|
||||||
renderScriptList();
|
|
||||||
}).fail(function(xhr) {
|
|
||||||
alert('启动失败: ' + (xhr.responseJSON?.error || xhr.statusText));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 停止脚本
|
|
||||||
function stopScript(name) {
|
|
||||||
$.get(`/api/stop/${encodeURIComponent(name)}`, function() {
|
|
||||||
renderScriptList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新输出
|
|
||||||
function updateOutput() {
|
|
||||||
if (!currentScript) return;
|
|
||||||
|
|
||||||
$.get(`/api/status/${encodeURIComponent(currentScript)}`, function(res) {
|
|
||||||
const output = $('#output-area').empty();
|
|
||||||
res.output.forEach(line => {
|
|
||||||
output.append($('<div>').text(line).addClass('output-line'));
|
|
||||||
});
|
|
||||||
output.scrollTop(output[0].scrollHeight);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
$('#config-form').submit(function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const scriptName = $('#config-script').val();
|
const scriptName = $('#config-script').val();
|
||||||
if (!scriptName) return alert('请选择脚本');
|
if (!scriptName) {
|
||||||
|
alert('请选择脚本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const mode = $('#config-mode').val();
|
||||||
script: scriptName,
|
const config = { mode };
|
||||||
mode: $('#config-mode').val()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (config.mode === 'interval') {
|
if (mode === 'interval') {
|
||||||
config.interval = $('#config-interval').val();
|
config.interval = parseInt($('#config-interval').val());
|
||||||
config.unit = $('#config-unit').val();
|
config.unit = $('#config-unit').val();
|
||||||
}
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/config',
|
url: '/api/config',
|
||||||
method: 'POST',
|
type: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
data: JSON.stringify({ [scriptName]: config })
|
data: JSON.stringify({ [scriptName]: config }),
|
||||||
}).done(function() {
|
success: function(data) {
|
||||||
alert('配置已保存');
|
alert(data.msg);
|
||||||
renderScriptList();
|
renderScriptList();
|
||||||
}).fail(function(xhr) {
|
},
|
||||||
alert('保存失败: ' + (xhr.responseJSON?.error || xhr.statusText));
|
error: function(xhr) {
|
||||||
|
alert('保存失败:' + (xhr.responseJSON?.error || xhr.statusText));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示/隐藏定时配置区域
|
||||||
|
* @param {string} mode - 运行模式
|
||||||
|
*/
|
||||||
|
function toggleIntervalConfig(mode) {
|
||||||
|
if (mode === 'interval') {
|
||||||
|
$('#interval-config').show();
|
||||||
|
} else {
|
||||||
|
$('#interval-config').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 输出相关 ======================
|
||||||
|
/**
|
||||||
|
* 更新输出区域
|
||||||
|
*/
|
||||||
|
function updateOutput() {
|
||||||
|
if (!currentScript) return;
|
||||||
|
|
||||||
|
$.get(`/api/status/${encodeURIComponent(currentScript)}`)
|
||||||
|
.done(function(data) {
|
||||||
|
const outputArea = $('#output-area');
|
||||||
|
outputArea.empty();
|
||||||
|
|
||||||
|
(data.output || []).forEach(line => {
|
||||||
|
// 简单的日志类型识别和样式处理
|
||||||
|
let className = 'output-line';
|
||||||
|
if (line.includes('❌')) className += ' error';
|
||||||
|
else if (line.includes('✅')) className += ' success';
|
||||||
|
else if (line.includes('⚠️')) className += ' warning';
|
||||||
|
else if (line.includes('⏰') || line.includes('ℹ️')) className += ' info';
|
||||||
|
|
||||||
|
outputArea.append(`<div class="${className}">${escapeHtml(line)}</div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 初始化
|
// 滚动到底部
|
||||||
$(function() {
|
outputArea.scrollTop(outputArea[0].scrollHeight);
|
||||||
loadScripts();
|
|
||||||
$('#config-mode').change(function() {
|
|
||||||
toggleInterval($(this).val());
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 每2秒更新状态
|
/**
|
||||||
setInterval(() => {
|
* 清空输出区域
|
||||||
if (currentScript) updateOutput();
|
*/
|
||||||
}, 2000);
|
function clearOutput() {
|
||||||
|
$('#output-area').empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 工具函数 ======================
|
||||||
|
/**
|
||||||
|
* HTML特殊字符转义
|
||||||
|
* @param {string} unsafe - 待转义的字符串
|
||||||
|
* @returns {string} 转义后的字符串
|
||||||
|
*/
|
||||||
|
function escapeHtml(unsafe) {
|
||||||
|
if (!unsafe) return '';
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化拖拽分隔线功能
|
||||||
|
*/
|
||||||
|
function initResizeHandle() {
|
||||||
|
const handle = $('#resize-handle');
|
||||||
|
const leftPanel = $('#script-list-panel');
|
||||||
|
let isResizing = false;
|
||||||
|
|
||||||
|
handle.mousedown(function(e) {
|
||||||
|
isResizing = true;
|
||||||
|
$(document).mousemove(function(e) {
|
||||||
|
if (!isResizing) return;
|
||||||
|
// 限制最小宽度为200px
|
||||||
|
const newWidth = Math.max(200, e.pageX - leftPanel.offset().left);
|
||||||
|
$('.main-container').css('grid-template-columns', `${newWidth}px 4px 1fr`);
|
||||||
});
|
});
|
||||||
|
$(document).mouseup(function() {
|
||||||
|
isResizing = false;
|
||||||
|
});
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user