Compare commits

...

5 Commits

Author SHA1 Message Date
Cp0204
668897d1df 🎨 优化正则处理预览UI 增强引导
Some checks are pending
Docker Publish / build-and-push (push) Waiting to run
2025-04-20 11:23:48 +08:00
Cp0204
9866a9d93d 增加正则处理预览功能
- 后端添加正则处理预览逻辑,改 POST 接收前端请求
- 前端增加正则处理按钮
- 优化文件列表展示预览结果
2025-04-20 11:11:56 +08:00
Cp0204
9b9c5fe00a 调整单个任务执行逻辑
- 单任务执行前无须保存,由前端传递参数
- 单任务执行无视设定周期,始终执行
2025-04-19 23:55:09 +08:00
Cp0204
dc8362db08 🎨 优化任务周期提示输出 2025-04-19 22:01:10 +08:00
Cp0204
bc2cd1504e 🐛 修复星期全关依然运行的BUG 2025-04-19 21:59:06 +08:00
3 changed files with 166 additions and 83 deletions

View File

@ -1,6 +1,7 @@
# !/usr/bin/env python3 # !/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from flask import ( from flask import (
json,
Flask, Flask,
url_for, url_for,
session, session,
@ -23,6 +24,7 @@ import logging
import base64 import base64
import sys import sys
import os import os
import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, parent_dir) sys.path.insert(0, parent_dir)
@ -175,20 +177,22 @@ def update():
# 处理运行脚本请求 # 处理运行脚本请求
@app.route("/run_script_now", methods=["GET"]) @app.route("/run_script_now", methods=["POST"])
def run_script_now(): def run_script_now():
if not is_login(): if not is_login():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
task_index = request.args.get("task_index", "") tasklist = request.json.get("tasklist", [])
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH, task_index] command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH]
logging.info( logging.info(
f">>> 手动运行任务{int(task_index)+1 if task_index.isdigit() else 'all'}" f">>> 手动运行任务 [{tasklist[0].get('taskname') if len(tasklist)>0 else 'ALL'}] 开始执行..."
) )
def generate_output(): def generate_output():
# 设置环境变量 # 设置环境变量
process_env = os.environ.copy() process_env = os.environ.copy()
process_env["PYTHONIOENCODING"] = "utf-8" process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
process = subprocess.Popen( process = subprocess.Popen(
command, command,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -255,12 +259,12 @@ def get_task_suggestions():
return jsonify({"success": True, "message": f"error: {str(e)}"}) return jsonify({"success": True, "message": f"error: {str(e)}"})
@app.route("/get_share_detail") @app.route("/get_share_detail", methods=["POST"])
def get_share_detail(): def get_share_detail():
if not is_login(): if not is_login():
return jsonify({"success": False, "message": "未登录"}) return jsonify({"success": False, "message": "未登录"})
shareurl = request.args.get("shareurl", "") shareurl = request.json.get("shareurl", "")
stoken = request.args.get("stoken", "") stoken = request.json.get("stoken", "")
account = Quark("", 0) account = Quark("", 0)
pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl) pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
if not stoken: if not stoken:
@ -271,6 +275,24 @@ def get_share_detail():
share_detail["paths"] = paths share_detail["paths"] = paths
share_detail["stoken"] = stoken share_detail["stoken"] = stoken
# 正则处理预览
def preview_regex(share_detail):
regex = request.json.get("regex")
pattern, replace = account.magic_regex_func(
regex.get("pattern", ""),
regex.get("replace", ""),
regex.get("taskname", ""),
)
for item in share_detail["list"]:
file_name = item["file_name"]
if re.search(pattern, item["file_name"]):
item["file_name_re"] = (
re.sub(pattern, replace, file_name) if replace != "" else file_name
)
return share_detail
share_detail = preview_regex(share_detail)
return jsonify({"success": True, "data": share_detail}) return jsonify({"success": True, "data": share_detail})

View File

@ -298,7 +298,7 @@
<div class="input-group"> <div class="input-group">
<input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)"> <input type="text" name="shareurl[]" class="form-control" v-model="task.shareurl" placeholder="必填" @blur="changeShareurl(task)">
<div class="input-group-append" v-if="task.shareurl"> <div class="input-group-append" v-if="task.shareurl">
<button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;showFolderSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button> <button type="button" class="btn btn-outline-secondary" @click="fileSelect.selectDir=true;fileSelect.previewRegex=false;showShareSelect(index)" title="选择文件夹"><i class="bi bi-folder"></i></button>
<div class="input-group-text"> <div class="input-group-text">
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a> <a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
</div> </div>
@ -323,7 +323,7 @@
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<div class="input-group-prepend"> <div class="input-group-prepend">
<span class="input-group-text">正则处理</span> <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=true;fileSelect.previewRegex=true;showShareSelect(index)" title="预览正则处理效果">正则处理</button>
</div> </div>
<input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex"> <input type="text" name="pattern[]" class="form-control" v-model="task.pattern" placeholder="匹配表达式" list="magicRegex">
<input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式"> <input type="text" name="replace[]" class="form-control" v-model="task.replace" placeholder="替换表达式">
@ -344,7 +344,7 @@
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid"> <input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid">
<div class="input-group-append" v-if="task.shareurl"> <div class="input-group-append" v-if="task.shareurl">
<button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false; showFolderSelect(index)">选择</button> <button class="btn btn-outline-secondary" type="button" @click="fileSelect.selectDir=false;fileSelect.previewRegex=false;showShareSelect(index)">选择</button>
</div> </div>
</div> </div>
</div> </div>
@ -424,8 +424,9 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title"> <h5 class="modal-title">
<b v-if="fileSelect.selectDir">选择{{fileSelect.selectShare ? '分享' : '保存'}}文件夹</b> <b v-if="fileSelect.previewRegex">正则处理预览</b>
<b v-else>选择文件</b> <b v-else-if="fileSelect.selectDir">选择{{fileSelect.selectShare ? '需转存的' : '保存到的'}}文件夹</b>
<b v-else>选择起始文件</b>
<div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div> <div v-if="modalLoading" class="spinner-border spinner-border-sm m-1" role="status"></div>
</h5> </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
@ -446,30 +447,41 @@
</ol> </ol>
</nav> </nav>
<!-- 文件列表 --> <!-- 文件列表 -->
<div class="mb-3" v-if="fileSelect.previewRegex">
<div><b>匹配表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].pattern"></span></div>
<div><b>替换表达式:</b><span class="badge badge-info" v-html="formData.tasklist[fileSelect.index].replace"></span></div>
</div>
<table class="table table-hover table-sm"> <table class="table table-hover table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">文件名</th> <th scope="col">文件名</th>
<th scope="col" v-if="fileSelect.selectShare">正则处理</th>
<template v-if="!fileSelect.previewRegex">
<th scope="col">大小</th> <th scope="col">大小</th>
<th scope="col">修改日期 ↓</th> <th scope="col">修改日期 ↓</th>
<th scope="col" v-if="!fileSelect.selectShare">操作</th> <th scope="col" v-if="!fileSelect.selectShare">操作</th>
</template>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(file, key) in fileSelect.fileList" :key="key" @click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : null) : selectStartFid(file.fid)" :class="{'cursor-pointer': (fileSelect.selectDir && file.dir)}"> <tr v-for="(file, key) in fileSelect.fileList" :key="key" @click="fileSelect.selectDir ? (file.dir ? navigateTo(file.fid, file.file_name) : null) : selectStartFid(file.fid)" :class="{'cursor-pointer': fileSelect.selectDir ? file.dir : true}">
<td><i class="bi" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i> {{file.file_name}}</td> <td><i class="bi" :class="file.dir ? 'bi-folder-fill text-warning' : 'bi-file-earmark'"></i> {{file.file_name}}</td>
<td v-if="fileSelect.selectShare" :class="file.file_name_re ? 'text-success' : 'text-danger'">{{file.file_name_re || '&times;'}}</td>
<template v-if="!fileSelect.previewRegex">
<td v-if="file.dir">{{ file.include_items }}项</td> <td v-if="file.dir">{{ file.include_items }}项</td>
<td v-else>{{file.size | size}}</td> <td v-else>{{file.size | size}}</td>
<td>{{file.updated_at | ts2date}}</td> <td>{{file.updated_at | ts2date}}</td>
<td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td> <td v-if="!fileSelect.selectShare"><a @click.stop.prevent="deleteFile(file.fid, file.file_name, file.dir)">删除</a></td>
</template>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="modal-footer" v-if="fileSelect.selectDir"> <div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
<button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">选择当前文件夹</button> <span v-html="fileSelect.selectShare ? '转存:' : '保存到:'"></span>
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare" @click="selectCurrentFolder(true)">选择当前文件夹+/任务名称</button> <button type="button" class="btn btn-primary btn-sm" @click="selectCurrentFolder()">当前文件夹</button>
<button type="button" class="btn btn-primary btn-sm" v-if="!fileSelect.selectShare" @click="selectCurrentFolder(true)">当前文件夹<span class="badge badge-light" v-html="'/'+formData.tasklist[fileSelect.index].taskname"></span></button>
</div> </div>
</div> </div>
</div> </div>
@ -535,6 +547,7 @@
paths: [], paths: [],
selectDir: true, selectDir: true,
selectShare: true, selectShare: true,
previewRegex: false,
}, },
}, },
filters: { filters: {
@ -776,36 +789,74 @@
clearData(target) { clearData(target) {
this[target] = ""; this[target] = "";
}, },
runScriptNow(task_index = "") { async runScriptNow(task_index = null) {
if (this.configModified) { body = {};
if (task_index != null) {
task = { ...this.formData.tasklist[task_index] };
delete task.runweek;
delete task.enddate;
body = {
"tasklist": [task]
};
} else if (this.configModified) {
if (!confirm('配置已修改但未保存,是否继续运行?')) { if (!confirm('配置已修改但未保存,是否继续运行?')) {
return; return;
} }
} }
$('#logModal').modal('toggle') $('#logModal').modal('toggle');
this.modalLoading = true this.modalLoading = true;
this.run_log = '' this.run_log = '';
const source = new EventSource(`/run_script_now?task_index=${task_index}`); try {
source.onmessage = (event) => { // 1. 发送 POST 请求
if (event.data == "[DONE]") { const response = await fetch(`/run_script_now`, {
this.modalLoading = false method: 'POST',
source.close(); headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
// 2. 处理 SSE 流
const reader = response.body.getReader();
const decoder = new TextDecoder();
let partialData = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream complete.');
this.modalLoading = false;
// 运行后刷新数据 // 运行后刷新数据
this.fetchData(); this.fetchData();
} else { break;
this.run_log += event.data + '\n'; }
partialData += decoder.decode(value);
const lines = partialData.split('\n').filter(line => line.trim() !== '');
for (const line of lines) {
if (line.startsWith('data:')) {
const eventData = line.substring(5).trim();
if (eventData === '[DONE]') {
this.modalLoading = false;
this.fetchData();
return;
}
this.run_log += eventData + '\n';
// 在更新 run_log 后将滚动条滚动到底部 // 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => { this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body'); const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight; modalBody.scrollTop = modalBody.scrollHeight;
}); });
} else {
console.warn('Unexpected line:', line);
} }
}; }
source.onerror = (error) => { partialData = '';
this.modalLoading = false }
} catch (error) {
this.modalLoading = false;
console.error('Error:', error); console.error('Error:', error);
source.close(); }
};
}, },
getParentDirectory(path) { getParentDirectory(path) {
parentDir = path.substring(0, path.lastIndexOf('/')) parentDir = path.substring(0, path.lastIndexOf('/'))
@ -867,7 +918,7 @@
}, },
selectSuggestion(index, suggestion) { selectSuggestion(index, suggestion) {
this.smart_param.showSuggestions = false; this.smart_param.showSuggestions = false;
this.showFolderSelect(index, suggestion.shareurl); this.showShareSelect(index, suggestion.shareurl);
}, },
addMagicRegex() { addMagicRegex() {
const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`; const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`;
@ -919,13 +970,14 @@
this.modalLoading = false; this.modalLoading = false;
}).catch(error => { }).catch(error => {
console.error('Error /get_savepath_detail:', error); console.error('Error /get_savepath_detail:', error);
this.fileSelect = { error: "获取文件夹列表失败" }; this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false; this.modalLoading = false;
}); });
}, },
showSavepathSelect(index) { showSavepathSelect(index) {
this.fileSelect.selectShare = false; this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true; this.fileSelect.selectDir = true;
this.fileSelect.previewRegex = false;
this.fileSelect.error = undefined; this.fileSelect.error = undefined;
this.fileSelect.fileList = []; this.fileSelect.fileList = [];
this.fileSelect.paths = []; this.fileSelect.paths = [];
@ -935,10 +987,13 @@
}, },
getShareDetail() { getShareDetail() {
this.modalLoading = true; this.modalLoading = true;
axios.get('/get_share_detail', { axios.post('/get_share_detail', {
params: {
shareurl: this.fileSelect.shareurl, shareurl: this.fileSelect.shareurl,
stoken: this.fileSelect.stoken stoken: this.fileSelect.stoken,
regex: {
pattern: this.formData.tasklist[this.fileSelect.index].pattern,
replace: this.formData.tasklist[this.fileSelect.index].replace,
taskname: this.formData.tasklist[this.fileSelect.index].taskname
} }
}).then(response => { }).then(response => {
if (response.data.success) { if (response.data.success) {
@ -951,11 +1006,11 @@
this.modalLoading = false; this.modalLoading = false;
}).catch(error => { }).catch(error => {
console.error('Error getting folders:', error); console.error('Error getting folders:', error);
this.fileSelect = { error: "获取文件夹列表失败" }; this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false; this.modalLoading = false;
}); });
}, },
showFolderSelect(index, shareurl = "") { showShareSelect(index, shareurl = null) {
this.fileSelect.selectShare = true; this.fileSelect.selectShare = true;
this.fileSelect.fileList = []; this.fileSelect.fileList = [];
this.fileSelect.paths = []; this.fileSelect.paths = [];

View File

@ -632,7 +632,6 @@ class Quark:
else: else:
return False return False
except Exception as e: except Exception as e:
if os.environ.get("DEBUG") == True:
print(f"转存测试失败: {str(e)}") print(f"转存测试失败: {str(e)}")
def do_save_task(self, task): def do_save_task(self, task):
@ -905,7 +904,7 @@ def do_save(account, tasklist=[]):
# 获取全部保存目录fid # 获取全部保存目录fid
account.update_savepath_fid(tasklist) account.update_savepath_fid(tasklist)
def check_date(task): def is_time(task):
return ( return (
not task.get("enddate") not task.get("enddate")
or ( or (
@ -913,15 +912,13 @@ def do_save(account, tasklist=[]):
<= datetime.strptime(task["enddate"], "%Y-%m-%d").date() <= datetime.strptime(task["enddate"], "%Y-%m-%d").date()
) )
) and ( ) and (
not task.get("runweek") "runweek" not in task
# 星期一为0星期日为6 # 星期一为0星期日为6
or (datetime.today().weekday() + 1 in task.get("runweek")) or (datetime.today().weekday() + 1 in task.get("runweek"))
) )
# 执行任务 # 执行任务
for index, task in enumerate(tasklist): for index, task in enumerate(tasklist):
# 判断任务期限
if check_date(task):
print() print()
print(f"#{index+1}------------------") print(f"#{index+1}------------------")
print(f"任务名称: {task['taskname']}") print(f"任务名称: {task['taskname']}")
@ -931,13 +928,17 @@ def do_save(account, tasklist=[]):
print(f"正则匹配: {task['pattern']}") print(f"正则匹配: {task['pattern']}")
if task.get("replace"): if task.get("replace"):
print(f"正则替换: {task['replace']}") print(f"正则替换: {task['replace']}")
if task.get("enddate"):
print(f"任务截止: {task['enddate']}")
if task.get("ignore_extension"):
print(f"忽略后缀: {task['ignore_extension']}")
if task.get("update_subdir"): if task.get("update_subdir"):
print(f"更子目录: {task['update_subdir']}") print(f"更子目录: {task['update_subdir']}")
if task.get("runweek") or task.get("enddate"):
print(
f"运行周期: WK{task.get("runweek",[])} ~ {task.get('enddate','forever')}"
)
print() print()
# 判断任务周期
if not is_time(task):
print(f"任务不在运行周期内,跳过")
else:
is_new_tree = account.do_save_task(task) is_new_tree = account.do_save_task(task)
is_rename = account.do_rename_task(task) is_rename = account.do_rename_task(task)
@ -977,7 +978,13 @@ def main():
print() print()
# 读取启动参数 # 读取启动参数
config_path = sys.argv[1] if len(sys.argv) > 1 else "quark_config.json" config_path = sys.argv[1] if len(sys.argv) > 1 else "quark_config.json"
task_index = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else "" # 从环境变量中获取 TASKLIST
tasklist_from_env = []
if tasklist_json := os.environ.get("TASKLIST"):
try:
tasklist_from_env = json.loads(tasklist_json)
except Exception as e:
print(f"从环境变量解析任务列表失败 {e}")
# 检查本地文件是否存在,如果不存在就下载 # 检查本地文件是否存在,如果不存在就下载
if not os.path.exists(config_path): if not os.path.exists(config_path):
if os.environ.get("QUARK_COOKIE"): if os.environ.get("QUARK_COOKIE"):
@ -1008,7 +1015,7 @@ def main():
accounts = [Quark(cookie, index) for index, cookie in enumerate(cookies)] accounts = [Quark(cookie, index) for index, cookie in enumerate(cookies)]
# 签到 # 签到
print(f"===============签到任务===============") print(f"===============签到任务===============")
if type(task_index) is int: if tasklist_from_env:
verify_account(accounts[0]) verify_account(accounts[0])
else: else:
for account in accounts: for account in accounts:
@ -1019,11 +1026,10 @@ def main():
if accounts[0].is_active and cookie_form_file: if accounts[0].is_active and cookie_form_file:
print(f"===============转存任务===============") print(f"===============转存任务===============")
# 任务列表 # 任务列表
tasklist = CONFIG_DATA.get("tasklist", []) if tasklist_from_env:
if type(task_index) is int: do_save(accounts[0], tasklist_from_env)
do_save(accounts[0], [tasklist[task_index]])
else: else:
do_save(accounts[0], tasklist) do_save(accounts[0], CONFIG_DATA.get("tasklist", []))
print() print()
# 通知 # 通知
if NOTIFYS: if NOTIFYS: