mirror of
https://github.com/Usagi-org/ai-goofish-monitor.git
synced 2025-11-25 19:37:37 +08:00
feat: 添加重新生成AI标准功能
This commit is contained in:
@@ -10,6 +10,7 @@ load_dotenv()
|
|||||||
# --- File Paths & Directories ---
|
# --- File Paths & Directories ---
|
||||||
STATE_FILE = "xianyu_state.json"
|
STATE_FILE = "xianyu_state.json"
|
||||||
IMAGE_SAVE_DIR = "images"
|
IMAGE_SAVE_DIR = "images"
|
||||||
|
CONFIG_FILE = "config.json"
|
||||||
os.makedirs(IMAGE_SAVE_DIR, exist_ok=True)
|
os.makedirs(IMAGE_SAVE_DIR, exist_ok=True)
|
||||||
|
|
||||||
# 任务隔离的临时图片目录前缀
|
# 任务隔离的临时图片目录前缀
|
||||||
|
|||||||
46
src/file_operator.py
Normal file
46
src/file_operator.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import aiofiles
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class FileOperator:
|
||||||
|
def __init__(self, filepath: str):
|
||||||
|
self.filepath = filepath
|
||||||
|
|
||||||
|
async def read(self) -> str | None:
|
||||||
|
"""
|
||||||
|
读取
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(self.filepath, 'r', encoding='utf-8') as f:
|
||||||
|
content_str = await f.read()
|
||||||
|
if content_str.strip():
|
||||||
|
return content_str
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"文件 {self.filepath} 不存在")
|
||||||
|
return None
|
||||||
|
except PermissionError:
|
||||||
|
print(f"错误:没有权限读取文件 {self.filepath}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取文件 {self.filepath} 时发生错误: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def write(self, content: str) -> bool:
|
||||||
|
"""
|
||||||
|
写入
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
Path(self.filepath).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
async with aiofiles.open(self.filepath, 'w', encoding='utf-8') as f:
|
||||||
|
await f.write(content)
|
||||||
|
return True
|
||||||
|
|
||||||
|
except PermissionError:
|
||||||
|
print(f"错误:没有权限写入文件 {self.filepath}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"写入文件 {self.filepath} 时发生错误: {e}")
|
||||||
|
return False
|
||||||
95
src/task.py
Normal file
95
src/task.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from src.config import CONFIG_FILE
|
||||||
|
from src.file_operator import FileOperator
|
||||||
|
|
||||||
|
|
||||||
|
class Task(BaseModel):
|
||||||
|
task_name: str
|
||||||
|
enabled: bool
|
||||||
|
keyword: str
|
||||||
|
description: str
|
||||||
|
max_pages: int
|
||||||
|
personal_only: bool
|
||||||
|
min_price: Optional[str] = None
|
||||||
|
max_price: Optional[str] = None
|
||||||
|
cron: Optional[str] = None
|
||||||
|
ai_prompt_base_file: str
|
||||||
|
ai_prompt_criteria_file: str
|
||||||
|
is_running: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
|
class TaskUpdate(BaseModel):
|
||||||
|
task_name: Optional[str] = None
|
||||||
|
enabled: Optional[bool] = None
|
||||||
|
keyword: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
max_pages: Optional[int] = None
|
||||||
|
personal_only: Optional[bool] = None
|
||||||
|
min_price: Optional[str] = None
|
||||||
|
max_price: Optional[str] = None
|
||||||
|
cron: Optional[str] = None
|
||||||
|
ai_prompt_base_file: Optional[str] = None
|
||||||
|
ai_prompt_criteria_file: Optional[str] = None
|
||||||
|
is_running: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
async def add_task(task: Task) -> bool:
|
||||||
|
config_file_op = FileOperator(CONFIG_FILE)
|
||||||
|
|
||||||
|
config_data_str = await config_file_op.read()
|
||||||
|
config_data = json.loads(config_data_str) if config_data_str else []
|
||||||
|
config_data.append(task)
|
||||||
|
|
||||||
|
return await config_file_op.write(json.dumps(config_data, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
async def update_task(task_id: int, task: Task) -> bool:
|
||||||
|
config_file_op = FileOperator(CONFIG_FILE)
|
||||||
|
|
||||||
|
config_data_str = await config_file_op.read()
|
||||||
|
|
||||||
|
if not config_data_str:
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_data = json.loads(config_data_str)
|
||||||
|
|
||||||
|
if len(config_data) <= task_id:
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_data[task_id] = task
|
||||||
|
|
||||||
|
return await config_file_op.write(json.dumps(config_data, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_task(task_id: int) -> Task | None:
|
||||||
|
config_file_op = FileOperator(CONFIG_FILE)
|
||||||
|
config_data_str = await config_file_op.read()
|
||||||
|
|
||||||
|
if not config_data_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
config_data = json.loads(config_data_str)
|
||||||
|
if len(config_data) <= task_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return config_data[task_id]
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_task(task_id: int) -> bool:
|
||||||
|
config_file_op = FileOperator(CONFIG_FILE)
|
||||||
|
config_data_str = await config_file_op.read()
|
||||||
|
if not config_data_str:
|
||||||
|
return True
|
||||||
|
|
||||||
|
config_data = json.loads(config_data_str)
|
||||||
|
|
||||||
|
if len(config_data) <= task_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
config_data.pop(task_id)
|
||||||
|
|
||||||
|
return await config_file_op.write(json.dumps(config_data, ensure_ascii=False, indent=2))
|
||||||
@@ -279,6 +279,21 @@ h2 {
|
|||||||
color: #ff4d4f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.criteria {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-criteria {
|
||||||
|
display: flex;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* Toggle Switch */
|
/* Toggle Switch */
|
||||||
.switch {
|
.switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -682,6 +682,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
return '<p>没有找到任何任务。请点击右上角“创建新任务”来添加一个。</p>';
|
return '<p>没有找到任何任务。请点击右上角“创建新任务”来添加一个。</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshBtn = '<svg class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M914.17946 324.34283C854.308387 324.325508 750.895846 324.317788 750.895846 324.317788 732.045471 324.317788 716.764213 339.599801 716.764213 358.451121 716.764213 377.30244 732.045471 392.584453 750.895846 392.584453L955.787864 392.584453C993.448095 392.584453 1024 362.040424 1024 324.368908L1024 119.466667C1024 100.615347 1008.718742 85.333333 989.868367 85.333333 971.017993 85.333333 955.736735 100.615347 955.736735 119.466667L955.736735 256.497996C933.314348 217.628194 905.827487 181.795372 873.995034 149.961328 778.623011 54.584531 649.577119 0 511.974435 0 229.218763 0 0 229.230209 0 512 0 794.769791 229.218763 1024 511.974435 1024 794.730125 1024 1023.948888 794.769791 1023.948888 512 1023.948888 493.148681 1008.66763 477.866667 989.817256 477.866667 970.966881 477.866667 955.685623 493.148681 955.685623 512 955.685623 757.067153 757.029358 955.733333 511.974435 955.733333 266.91953 955.733333 68.263265 757.067153 68.263265 512 68.263265 266.932847 266.91953 68.266667 511.974435 68.266667 631.286484 68.266667 743.028524 115.531923 825.725634 198.233152 862.329644 234.839003 892.298522 277.528256 914.17946 324.34283L914.17946 324.34283Z" fill="#389BFF"></path></svg>'
|
||||||
|
|
||||||
const tableHeader = `
|
const tableHeader = `
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -722,7 +724,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<td>${task.min_price || '不限'} - ${task.max_price || '不限'}</td>
|
<td>${task.min_price || '不限'} - ${task.max_price || '不限'}</td>
|
||||||
<td>${task.personal_only ? '<span class="tag personal">个人闲置</span>' : ''}</td>
|
<td>${task.personal_only ? '<span class="tag personal">个人闲置</span>' : ''}</td>
|
||||||
<td>${task.max_pages || 3}</td>
|
<td>${task.max_pages || 3}</td>
|
||||||
<td>${(task.ai_prompt_criteria_file || 'N/A').replace('prompts/', '')}</td>
|
<td><div class="criteria"><button class="refresh-criteria" title="重新生成AI标准" data-task-id="${task.id}">${refreshBtn}</button>${(task.ai_prompt_criteria_file || 'N/A').replace('prompts/', '')}</div></td>
|
||||||
<td>${task.cron || '未设置'}</td>
|
<td>${task.cron || '未设置'}</td>
|
||||||
<td>
|
<td>
|
||||||
${actionButton}
|
${actionButton}
|
||||||
@@ -1306,6 +1308,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const container = document.getElementById('tasks-table-container');
|
const container = document.getElementById('tasks-table-container');
|
||||||
const tasks = await fetchTasks();
|
const tasks = await fetchTasks();
|
||||||
container.innerHTML = renderTasksTable(tasks);
|
container.innerHTML = renderTasksTable(tasks);
|
||||||
|
} else if (button.matches('.refresh-criteria')) {
|
||||||
|
const task = JSON.parse(row.dataset.task);
|
||||||
|
const modal = document.getElementById('refresh-criteria-modal');
|
||||||
|
const textarea = document.getElementById('refresh-criteria-description');
|
||||||
|
textarea.value = task['description'] || '';
|
||||||
|
modal.dataset.taskId = taskId;
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
setTimeout(() => modal.classList.add('visible'), 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1398,6 +1408,57 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- refresh criteria Modal Logic ---
|
||||||
|
const refreshCriteriaModal = document.getElementById('refresh-criteria-modal');
|
||||||
|
if (refreshCriteriaModal) {
|
||||||
|
const form = document.getElementById('refresh-criteria-form');
|
||||||
|
const closeModalBtn = document.getElementById('close-refresh-criteria-btn');
|
||||||
|
const cancelBtn = document.getElementById('cancel-refresh-criteria-btn');
|
||||||
|
const refreshBtn = document.getElementById('refresh-criteria-btn');
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
refreshCriteriaModal.classList.remove('visible');
|
||||||
|
setTimeout(() => {
|
||||||
|
refreshCriteriaModal.style.display = 'none';
|
||||||
|
form.reset(); // Reset form on close
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
closeModalBtn.addEventListener('click', closeModal);
|
||||||
|
cancelBtn.addEventListener('click', closeModal);
|
||||||
|
|
||||||
|
let canClose = false;
|
||||||
|
refreshCriteriaModal.addEventListener('mousedown', event => {
|
||||||
|
canClose = event.target === refreshCriteriaModal;
|
||||||
|
});
|
||||||
|
refreshCriteriaModal.addEventListener('mouseup', (event) => {
|
||||||
|
// Close if clicked on the overlay background
|
||||||
|
if (canClose && event.target === refreshCriteriaModal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
refreshBtn.addEventListener('click', async () => {
|
||||||
|
if (form.checkValidity() === false) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const btnText = refreshBtn.querySelector('.btn-text');
|
||||||
|
const spinner = refreshBtn.querySelector('.spinner');
|
||||||
|
btnText.style.display = 'none';
|
||||||
|
spinner.style.display = 'inline-block';
|
||||||
|
refreshBtn.disabled = true;
|
||||||
|
|
||||||
|
const taskId = refreshCriteriaModal.dataset.taskId
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const result = await updateTask(taskId, {description: formData.get('description')});
|
||||||
|
if (result && result.task) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
refreshLoginStatusWidget();
|
refreshLoginStatusWidget();
|
||||||
|
|||||||
@@ -235,6 +235,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- refresh criteria Modal -->
|
||||||
|
<div id="refresh-criteria-modal" class="modal-overlay" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>重新生成AI标准</h2>
|
||||||
|
<button id="close-refresh-criteria-btn" class="close-button">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="refresh-criteria-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="refresh-criteria-description">详细购买需求</label>
|
||||||
|
<textarea id="refresh-criteria-description" name="description" rows="6"
|
||||||
|
placeholder="请用自然语言详细描述你的购买需求,AI将根据此描述生成分析标准。例如:我想买一台95新以上的索尼A7M4相机,预算在10000到13000元之间,快门数要低于5000。必须是国行且配件齐全。优先考虑个人卖家..."
|
||||||
|
required></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="cancel-refresh-criteria-btn" class="control-button">取消</button>
|
||||||
|
<button id="refresh-criteria-btn" class="control-button primary-btn">
|
||||||
|
<span class="btn-text">重新生成</span>
|
||||||
|
<span class="spinner" style="display: none;"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- JSON Viewer Modal -->
|
<!-- JSON Viewer Modal -->
|
||||||
<div id="json-viewer-modal" class="modal-overlay" style="display: none;">
|
<div id="json-viewer-modal" class="modal-overlay" style="display: none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -263,7 +291,8 @@
|
|||||||
<p>此方法用于在无法运行图形化浏览器的服务器上更新闲鱼登录凭证。</p>
|
<p>此方法用于在无法运行图形化浏览器的服务器上更新闲鱼登录凭证。</p>
|
||||||
<p>安装Chrome扩展来提取闲鱼登录状态:</p>
|
<p>安装Chrome扩展来提取闲鱼登录状态:</p>
|
||||||
<ol class="instructions">
|
<ol class="instructions">
|
||||||
<li>在Chrome浏览器中安装 <a href="https://chromewebstore.google.com/detail/xianyu-login-state-extrac/eidlpfjiodpigmfcahkmlenhppfklcoa" target="_blank" rel="noopener noreferrer">闲鱼登录状态提取扩展</a></li>
|
<li>在Chrome浏览器中安装 <a href="https://chromewebstore.google.com/detail/xianyu-login-state-extrac/eidlpfjiodpigmfcahkmlenhppfklcoa" target="_blank"
|
||||||
|
rel="noopener noreferrer">闲鱼登录状态提取扩展</a></li>
|
||||||
<li>打开并登录 <a href="https://www.goofish.com" target="_blank"
|
<li>打开并登录 <a href="https://www.goofish.com" target="_blank"
|
||||||
rel="noopener noreferrer">闲鱼官网</a>。
|
rel="noopener noreferrer">闲鱼官网</a>。
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ from typing import List, Optional
|
|||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
from src.file_operator import FileOperator
|
||||||
|
from src.task import get_task, update_task
|
||||||
|
|
||||||
|
|
||||||
class Task(BaseModel):
|
class Task(BaseModel):
|
||||||
task_name: str
|
task_name: str
|
||||||
enabled: bool
|
enabled: bool
|
||||||
keyword: str
|
keyword: str
|
||||||
|
description: str
|
||||||
max_pages: int
|
max_pages: int
|
||||||
personal_only: bool
|
personal_only: bool
|
||||||
min_price: Optional[str] = None
|
min_price: Optional[str] = None
|
||||||
@@ -39,6 +43,7 @@ class TaskUpdate(BaseModel):
|
|||||||
task_name: Optional[str] = None
|
task_name: Optional[str] = None
|
||||||
enabled: Optional[bool] = None
|
enabled: Optional[bool] = None
|
||||||
keyword: Optional[str] = None
|
keyword: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
max_pages: Optional[int] = None
|
max_pages: Optional[int] = None
|
||||||
personal_only: Optional[bool] = None
|
personal_only: Optional[bool] = None
|
||||||
min_price: Optional[str] = None
|
min_price: Optional[str] = None
|
||||||
@@ -511,6 +516,7 @@ async def generate_task(req: TaskGenerateRequest, username: str = Depends(verify
|
|||||||
"min_price": req.min_price,
|
"min_price": req.min_price,
|
||||||
"max_price": req.max_price,
|
"max_price": req.max_price,
|
||||||
"cron": req.cron,
|
"cron": req.cron,
|
||||||
|
"description": req.description,
|
||||||
"ai_prompt_base_file": "prompts/base_prompt.txt",
|
"ai_prompt_base_file": "prompts/base_prompt.txt",
|
||||||
"ai_prompt_criteria_file": output_filename,
|
"ai_prompt_criteria_file": output_filename,
|
||||||
"is_running": False
|
"is_running": False
|
||||||
@@ -564,17 +570,12 @@ async def create_task(task: Task, username: str = Depends(verify_credentials)):
|
|||||||
|
|
||||||
|
|
||||||
@app.patch("/api/tasks/{task_id}", response_model=dict)
|
@app.patch("/api/tasks/{task_id}", response_model=dict)
|
||||||
async def update_task(task_id: int, task_update: TaskUpdate, username: str = Depends(verify_credentials)):
|
async def update_task_api(task_id: int, task_update: TaskUpdate, username: str = Depends(verify_credentials)):
|
||||||
"""
|
"""
|
||||||
更新指定ID任务的属性。
|
更新指定ID任务的属性。
|
||||||
"""
|
"""
|
||||||
try:
|
task = await get_task(task_id)
|
||||||
async with aiofiles.open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
if not task:
|
||||||
tasks = json.loads(await f.read())
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"读取或解析配置文件失败: {e}")
|
|
||||||
|
|
||||||
if not (0 <= task_id < len(tasks)):
|
|
||||||
raise HTTPException(status_code=404, detail="任务未找到。")
|
raise HTTPException(status_code=404, detail="任务未找到。")
|
||||||
|
|
||||||
# 更新数据
|
# 更新数据
|
||||||
@@ -583,27 +584,38 @@ async def update_task(task_id: int, task_update: TaskUpdate, username: str = Dep
|
|||||||
if not update_data:
|
if not update_data:
|
||||||
return JSONResponse(content={"message": "数据无变化,未执行更新。"}, status_code=200)
|
return JSONResponse(content={"message": "数据无变化,未执行更新。"}, status_code=200)
|
||||||
|
|
||||||
|
if 'description' in update_data:
|
||||||
|
criteria_filename = task.get('ai_prompt_criteria_file')
|
||||||
|
criteria_file_op = FileOperator(criteria_filename)
|
||||||
|
try:
|
||||||
|
generated_criteria = await generate_criteria(
|
||||||
|
user_description=update_data.get("description"),
|
||||||
|
reference_file_path="prompts/macbook_criteria.txt" # 使用默认的macbook标准作为参考
|
||||||
|
)
|
||||||
|
if not generated_criteria:
|
||||||
|
raise HTTPException(status_code=500, detail="AI未能生成分析标准。")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"调用AI生成标准时出错: {e}")
|
||||||
|
|
||||||
|
success = await criteria_file_op.write(generated_criteria)
|
||||||
|
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=500, detail=f"更新的AI标准写入出错")
|
||||||
|
|
||||||
# 如果任务从“启用”变为“禁用”,且正在运行,则先停止它
|
# 如果任务从“启用”变为“禁用”,且正在运行,则先停止它
|
||||||
if 'enabled' in update_data and not update_data['enabled']:
|
if 'enabled' in update_data and not update_data['enabled']:
|
||||||
if scraper_processes.get(task_id):
|
if scraper_processes.get(task_id):
|
||||||
print(f"任务 '{tasks[task_id]['task_name']}' 已被禁用,正在停止其进程...")
|
print(f"任务 '{tasks[task_id]['task_name']}' 已被禁用,正在停止其进程...")
|
||||||
await stop_task_process(task_id) # 这会处理进程和is_running状态
|
await stop_task_process(task_id) # 这会处理进程和is_running状态
|
||||||
|
|
||||||
tasks[task_id].update(update_data)
|
task.update(update_data)
|
||||||
|
|
||||||
# 异步写回文件
|
success = await update_task(task_id, task)
|
||||||
try:
|
|
||||||
async with aiofiles.open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
|
||||||
await f.write(json.dumps(tasks, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
await reload_scheduler_jobs()
|
if not success:
|
||||||
|
|
||||||
updated_task = tasks[task_id]
|
|
||||||
updated_task['id'] = task_id
|
|
||||||
return {"message": "任务更新成功。", "task": updated_task}
|
|
||||||
except Exception as e:
|
|
||||||
raise HTTPException(status_code=500, detail=f"写入配置文件时发生错误: {e}")
|
raise HTTPException(status_code=500, detail=f"写入配置文件时发生错误: {e}")
|
||||||
|
|
||||||
|
return {"message": "任务更新成功。", "task": task}
|
||||||
|
|
||||||
async def start_task_process(task_id: int, task_name: str):
|
async def start_task_process(task_id: int, task_name: str):
|
||||||
"""内部函数:启动一个指定的任务进程。"""
|
"""内部函数:启动一个指定的任务进程。"""
|
||||||
|
|||||||
Reference in New Issue
Block a user