Compare commits

...

7 Commits

Author SHA1 Message Date
Y_C_Z
f1df77f8f2
Merge 50d01bb4d8 into 668897d1df 2025-04-20 11:24:00 +08:00
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
Y_C_Z
50d01bb4d8
每次转存都清空目标文件夹 2025-04-16 12:13:33 +08:00
3 changed files with 177 additions and 83 deletions

View File

@ -1,6 +1,7 @@
# !/usr/bin/env python3
# -*- coding: utf-8 -*-
from flask import (
json,
Flask,
url_for,
session,
@ -23,6 +24,7 @@ import logging
import base64
import sys
import os
import re
parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
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():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
task_index = request.args.get("task_index", "")
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH, task_index]
tasklist = request.json.get("tasklist", [])
command = [PYTHON_PATH, "-u", SCRIPT_PATH, CONFIG_PATH]
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():
# 设置环境变量
process_env = os.environ.copy()
process_env["PYTHONIOENCODING"] = "utf-8"
if tasklist:
process_env["TASKLIST"] = json.dumps(tasklist, ensure_ascii=False)
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
@ -255,12 +259,12 @@ def get_task_suggestions():
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():
if not is_login():
return jsonify({"success": False, "message": "未登录"})
shareurl = request.args.get("shareurl", "")
stoken = request.args.get("stoken", "")
shareurl = request.json.get("shareurl", "")
stoken = request.json.get("stoken", "")
account = Quark("", 0)
pwd_id, passcode, pdir_fid, paths = account.extract_url(shareurl)
if not stoken:
@ -271,6 +275,24 @@ def get_share_detail():
share_detail["paths"] = paths
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})

View File

@ -298,7 +298,7 @@
<div class="input-group">
<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">
<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">
<a target="_blank" :href="task.shareurl"><i class="bi bi-box-arrow-up-right"></i></a>
</div>
@ -323,7 +323,7 @@
<div class="col-sm-10">
<div class="input-group">
<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>
<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="替换表达式">
@ -344,7 +344,7 @@
<div class="input-group">
<input type="text" class="form-control" placeholder="可选,只转存修改日期>此文件的文件" name="startfid[]" v-model="task.startfid">
<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>
@ -424,8 +424,9 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<b v-if="fileSelect.selectDir">选择{{fileSelect.selectShare ? '分享' : '保存'}}文件夹</b>
<b v-else>选择文件</b>
<b v-if="fileSelect.previewRegex">正则处理预览</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>
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
@ -446,30 +447,41 @@
</ol>
</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">
<thead>
<tr>
<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 v-if="!fileSelect.previewRegex">
<th scope="col">大小</th>
<th scope="col">修改日期 ↓</th>
<th scope="col" v-if="!fileSelect.selectShare">操作</th>
</template>
</tr>
</thead>
<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 v-if="file.dir">{{ file.include_items }}项</td>
<td v-else>{{file.size | size}}</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" :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-else>{{file.size | size}}</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>
</template>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer" v-if="fileSelect.selectDir">
<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)">选择当前文件夹+/任务名称</button>
<div class="modal-footer" v-if="fileSelect.selectDir && !fileSelect.previewRegex">
<span v-html="fileSelect.selectShare ? '转存:' : '保存到:'"></span>
<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>
@ -535,6 +547,7 @@
paths: [],
selectDir: true,
selectShare: true,
previewRegex: false,
},
},
filters: {
@ -776,36 +789,74 @@
clearData(target) {
this[target] = "";
},
runScriptNow(task_index = "") {
if (this.configModified) {
async runScriptNow(task_index = null) {
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('配置已修改但未保存,是否继续运行?')) {
return;
}
}
$('#logModal').modal('toggle')
this.modalLoading = true
this.run_log = ''
const source = new EventSource(`/run_script_now?task_index=${task_index}`);
source.onmessage = (event) => {
if (event.data == "[DONE]") {
this.modalLoading = false
source.close();
// 运行后刷新数据
this.fetchData();
} else {
this.run_log += event.data + '\n';
// 在更新 run_log 后将滚动条滚动到底部
this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight;
});
$('#logModal').modal('toggle');
this.modalLoading = true;
this.run_log = '';
try {
// 1. 发送 POST 请求
const response = await fetch(`/run_script_now`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
};
source.onerror = (error) => {
this.modalLoading = false
// 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();
break;
}
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 后将滚动条滚动到底部
this.$nextTick(() => {
const modalBody = document.querySelector('.modal-body');
modalBody.scrollTop = modalBody.scrollHeight;
});
} else {
console.warn('Unexpected line:', line);
}
}
partialData = '';
}
} catch (error) {
this.modalLoading = false;
console.error('Error:', error);
source.close();
};
}
},
getParentDirectory(path) {
parentDir = path.substring(0, path.lastIndexOf('/'))
@ -867,7 +918,7 @@
},
selectSuggestion(index, suggestion) {
this.smart_param.showSuggestions = false;
this.showFolderSelect(index, suggestion.shareurl);
this.showShareSelect(index, suggestion.shareurl);
},
addMagicRegex() {
const newKey = `$MAGIC_${Object.keys(this.formData.magic_regex).length + 1}`;
@ -919,13 +970,14 @@
this.modalLoading = false;
}).catch(error => {
console.error('Error /get_savepath_detail:', error);
this.fileSelect = { error: "获取文件夹列表失败" };
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showSavepathSelect(index) {
this.fileSelect.selectShare = false;
this.fileSelect.selectDir = true;
this.fileSelect.previewRegex = false;
this.fileSelect.error = undefined;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];
@ -935,10 +987,13 @@
},
getShareDetail() {
this.modalLoading = true;
axios.get('/get_share_detail', {
params: {
shareurl: this.fileSelect.shareurl,
stoken: this.fileSelect.stoken
axios.post('/get_share_detail', {
shareurl: this.fileSelect.shareurl,
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 => {
if (response.data.success) {
@ -951,11 +1006,11 @@
this.modalLoading = false;
}).catch(error => {
console.error('Error getting folders:', error);
this.fileSelect = { error: "获取文件夹列表失败" };
this.fileSelect.error = "获取文件夹列表失败";
this.modalLoading = false;
});
},
showFolderSelect(index, shareurl = "") {
showShareSelect(index, shareurl = null) {
this.fileSelect.selectShare = true;
this.fileSelect.fileList = [];
this.fileSelect.paths = [];

View File

@ -632,8 +632,7 @@ class Quark:
else:
return False
except Exception as e:
if os.environ.get("DEBUG") == True:
print(f"转存测试失败: {str(e)}")
print(f"转存测试失败: {str(e)}")
def do_save_task(self, task):
# 判断资源失效记录
@ -692,6 +691,17 @@ class Quark:
to_pdir_fid = self.savepath_fid[savepath]
dir_file_list = self.ls_dir(to_pdir_fid)
# print("dir_file_list: ", dir_file_list)
# 清空目标文件夹
fid_list = [item["fid"] for item in dir_file_list]
if fid_list:
self.delete(fid_list)
recycle_list = self.recycle_list()
record_id_list = [
item["record_id"] for item in recycle_list if item["fid"] in fid_list
]
self.recycle_remove(record_id_list)
# 重新获取目标目录文件列表
dir_file_list = self.ls_dir(to_pdir_fid)
tree.create_node(
savepath,
@ -905,7 +915,7 @@ def do_save(account, tasklist=[]):
# 获取全部保存目录fid
account.update_savepath_fid(tasklist)
def check_date(task):
def is_time(task):
return (
not task.get("enddate")
or (
@ -913,31 +923,33 @@ def do_save(account, tasklist=[]):
<= datetime.strptime(task["enddate"], "%Y-%m-%d").date()
)
) and (
not task.get("runweek")
"runweek" not in task
# 星期一为0星期日为6
or (datetime.today().weekday() + 1 in task.get("runweek"))
)
# 执行任务
for index, task in enumerate(tasklist):
# 判断任务期限
if check_date(task):
print()
print(f"#{index+1}------------------")
print(f"任务名称: {task['taskname']}")
print(f"分享链接: {task['shareurl']}")
print(f"保存路径: {task['savepath']}")
if task.get("pattern"):
print(f"正则匹配: {task['pattern']}")
if task.get("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"):
print(f"更子目录: {task['update_subdir']}")
print()
print()
print(f"#{index+1}------------------")
print(f"任务名称: {task['taskname']}")
print(f"分享链接: {task['shareurl']}")
print(f"保存路径: {task['savepath']}")
if task.get("pattern"):
print(f"正则匹配: {task['pattern']}")
if task.get("replace"):
print(f"正则替换: {task['replace']}")
if task.get("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()
# 判断任务周期
if not is_time(task):
print(f"任务不在运行周期内,跳过")
else:
is_new_tree = account.do_save_task(task)
is_rename = account.do_rename_task(task)
@ -977,7 +989,13 @@ def main():
print()
# 读取启动参数
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 os.environ.get("QUARK_COOKIE"):
@ -1008,7 +1026,7 @@ def main():
accounts = [Quark(cookie, index) for index, cookie in enumerate(cookies)]
# 签到
print(f"===============签到任务===============")
if type(task_index) is int:
if tasklist_from_env:
verify_account(accounts[0])
else:
for account in accounts:
@ -1019,11 +1037,10 @@ def main():
if accounts[0].is_active and cookie_form_file:
print(f"===============转存任务===============")
# 任务列表
tasklist = CONFIG_DATA.get("tasklist", [])
if type(task_index) is int:
do_save(accounts[0], [tasklist[task_index]])
if tasklist_from_env:
do_save(accounts[0], tasklist_from_env)
else:
do_save(accounts[0], tasklist)
do_save(accounts[0], CONFIG_DATA.get("tasklist", []))
print()
# 通知
if NOTIFYS: