fix: 合规审查卡死修复 + 小红书复制格式 + today-only过滤
This commit is contained in:
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
from typing import Optional, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -17,6 +18,7 @@ if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
|
||||
_running_processes: Dict[str, dict] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
def _get_cmd():
|
||||
script_path = PROJECT_ROOT / "scripts" / "collector.py"
|
||||
@@ -29,21 +31,26 @@ def _get_cmd():
|
||||
|
||||
def run_collector():
|
||||
"""运行选题收集脚本(非阻塞,后台运行)"""
|
||||
cmd = _get_cmd()
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
cwd=PROJECT_ROOT
|
||||
)
|
||||
_running_processes["collector"] = {
|
||||
"pid": proc.pid,
|
||||
"started_at": time.time(),
|
||||
"process": proc
|
||||
}
|
||||
with _lock:
|
||||
existing = _running_processes.get("collector", {})
|
||||
proc = existing.get("process")
|
||||
if proc and proc.poll() is None:
|
||||
raise RuntimeError("已有内容采集任务正在运行")
|
||||
cmd = _get_cmd()
|
||||
proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
cwd=PROJECT_ROOT
|
||||
)
|
||||
_running_processes["collector"] = {
|
||||
"pid": proc.pid,
|
||||
"started_at": time.time(),
|
||||
"process": proc,
|
||||
}
|
||||
logger.info("Collector started in background (pid=%s)", proc.pid)
|
||||
return {"ok": True, "pid": proc.pid}
|
||||
return {"ok": True, "pid": proc.pid, "_proc": proc}
|
||||
|
||||
def run_collector_blocking(timeout: int = 300):
|
||||
"""运行选题收集脚本(阻塞,带超时,给定时任务用)"""
|
||||
@@ -60,22 +67,27 @@ def run_collector_blocking(timeout: int = 300):
|
||||
return {"ok": True, "output": result.stdout}
|
||||
|
||||
def get_collector_status() -> Optional[Dict]:
|
||||
"""获取当前采集任务状态"""
|
||||
"""获取当前采集任务状态(不消费子进程 stdout,留给 _monitor_subprocess)"""
|
||||
info = _running_processes.get("collector")
|
||||
if not info:
|
||||
return None
|
||||
proc: subprocess.Popen = info["process"]
|
||||
if "finished" in info:
|
||||
return {
|
||||
"status": "completed" if info.get("returncode") == 0 else "failed",
|
||||
"pid": info["pid"],
|
||||
"elapsed": round(time.time() - info["started_at"], 1),
|
||||
"returncode": info.get("returncode"),
|
||||
}
|
||||
if proc.poll() is not None:
|
||||
stdout, stderr = proc.communicate()
|
||||
info["returncode"] = proc.returncode
|
||||
info["finished"] = True
|
||||
elapsed = time.time() - info["started_at"]
|
||||
del _running_processes["collector"]
|
||||
return {
|
||||
"status": "completed" if proc.returncode == 0 else "failed",
|
||||
"pid": info["pid"],
|
||||
"elapsed": round(elapsed, 1),
|
||||
"returncode": proc.returncode,
|
||||
"stdout": stdout.strip()[-500:],
|
||||
"stderr": stderr.strip()[-500:],
|
||||
}
|
||||
return {
|
||||
"status": "running",
|
||||
|
||||
@@ -2,8 +2,10 @@ import subprocess
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from typing import Optional, Dict
|
||||
import threading
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,6 +14,7 @@ if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
|
||||
_running_processes: Dict[str, dict] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
def _get_cmd(topic_id: str = None):
|
||||
script_path = PROJECT_ROOT / "scripts" / "creator.py"
|
||||
@@ -21,15 +24,22 @@ def _get_cmd(topic_id: str = None):
|
||||
cmd = [str(venv_python), str(script_path)] if venv_python.exists() else ["python3", str(script_path)]
|
||||
if topic_id:
|
||||
cmd.extend(["--topic-id", topic_id])
|
||||
else:
|
||||
cmd.append("--today-only")
|
||||
return cmd
|
||||
|
||||
def run_creator(topic_id: str = None):
|
||||
"""非阻塞:后台启动创作脚本,立即返回"""
|
||||
cmd = _get_cmd(topic_id)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=str(PROJECT_ROOT))
|
||||
_running_processes["generator"] = {"pid": proc.pid, "started_at": time.time(), "process": proc, "topic_id": topic_id}
|
||||
with _lock:
|
||||
existing = _running_processes.get("generator", {})
|
||||
proc = existing.get("process")
|
||||
if proc and proc.poll() is None:
|
||||
raise RuntimeError("已有创作任务正在运行")
|
||||
cmd = _get_cmd(topic_id)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=str(PROJECT_ROOT))
|
||||
_running_processes["generator"] = {"pid": proc.pid, "started_at": time.time(), "process": proc, "topic_id": topic_id}
|
||||
logger.info("Creator started in background (pid=%s, topic_id=%s)", proc.pid, topic_id)
|
||||
return {"ok": True, "pid": proc.pid, "topic_id": topic_id}
|
||||
return {"ok": True, "pid": proc.pid, "topic_id": topic_id, "_proc": proc}
|
||||
|
||||
def run_creator_blocking(topic_id: str = None, timeout: int = 1800):
|
||||
"""阻塞版:带超时,给定时任务使用"""
|
||||
@@ -93,9 +103,11 @@ def get_generator_status() -> Optional[Dict]:
|
||||
if not info:
|
||||
return None
|
||||
proc = info["process"]
|
||||
if "finished" in info:
|
||||
return {"status": "completed" if info.get("returncode") == 0 else "failed", "pid": info["pid"], "elapsed": round(time.time() - info["started_at"], 1), "returncode": info.get("returncode"), "topic_id": info.get("topic_id")}
|
||||
if proc.poll() is not None:
|
||||
stdout, stderr = proc.communicate()
|
||||
info["returncode"] = proc.returncode
|
||||
info["finished"] = True
|
||||
elapsed = time.time() - info["started_at"]
|
||||
del _running_processes["generator"]
|
||||
return {"status": "completed" if proc.returncode == 0 else "failed", "pid": info["pid"], "elapsed": round(elapsed, 1), "returncode": proc.returncode, "topic_id": info["topic_id"]}
|
||||
return {"status": "running", "pid": info["pid"], "elapsed": round(time.time() - info["started_at"], 1)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict
|
||||
|
||||
@@ -14,6 +15,7 @@ if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
|
||||
_running_processes: Dict[str, dict] = {}
|
||||
_lock = threading.Lock()
|
||||
|
||||
def _get_cmd(topic_ids: List[str] = None):
|
||||
script_path = PROJECT_ROOT / "scripts" / "compliance_optimizer.py"
|
||||
@@ -23,15 +25,22 @@ def _get_cmd(topic_ids: List[str] = None):
|
||||
cmd = [str(venv_python), str(script_path)] if venv_python.exists() else ["python3", str(script_path)]
|
||||
if topic_ids:
|
||||
cmd.extend(["--topic-ids", ','.join(topic_ids)])
|
||||
else:
|
||||
cmd.append("--today-only")
|
||||
return cmd
|
||||
|
||||
def run_optimizer(topic_ids: List[str] = None):
|
||||
"""非阻塞:后台启动合规审查脚本,立即返回"""
|
||||
cmd = _get_cmd(topic_ids)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=str(PROJECT_ROOT))
|
||||
_running_processes["optimizer"] = {"pid": proc.pid, "started_at": time.time(), "process": proc, "topic_ids": topic_ids}
|
||||
with _lock:
|
||||
existing = _running_processes.get("optimizer", {})
|
||||
proc = existing.get("process")
|
||||
if proc and proc.poll() is None:
|
||||
raise RuntimeError("已有合规审查任务正在运行,请等待完成后再试")
|
||||
cmd = _get_cmd(topic_ids)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=str(PROJECT_ROOT))
|
||||
_running_processes["optimizer"] = {"pid": proc.pid, "started_at": time.time(), "process": proc, "topic_ids": topic_ids}
|
||||
logger.info("Optimizer started in background (pid=%s, topic_ids=%s)", proc.pid, topic_ids)
|
||||
return {"ok": True, "pid": proc.pid, "topic_ids": topic_ids}
|
||||
return {"ok": True, "pid": proc.pid, "topic_ids": topic_ids, "_proc": proc}
|
||||
|
||||
def run_optimizer_blocking(topic_ids: List[str] = None, timeout: int = 600):
|
||||
"""阻塞版:带超时,给定时任务使用"""
|
||||
@@ -54,9 +63,11 @@ def get_optimizer_status() -> Optional[Dict]:
|
||||
if not info:
|
||||
return None
|
||||
proc = info["process"]
|
||||
if "finished" in info:
|
||||
return {"status": "completed" if info.get("returncode") == 0 else "failed", "pid": info["pid"], "elapsed": round(time.time() - info["started_at"], 1), "returncode": info.get("returncode")}
|
||||
if proc.poll() is not None:
|
||||
stdout, stderr = proc.communicate()
|
||||
info["returncode"] = proc.returncode
|
||||
info["finished"] = True
|
||||
elapsed = time.time() - info["started_at"]
|
||||
del _running_processes["optimizer"]
|
||||
return {"status": "completed" if proc.returncode == 0 else "failed", "pid": info["pid"], "elapsed": round(elapsed, 1), "returncode": proc.returncode, "stdout": stdout.strip()[-300:], "stderr": stderr.strip()[-300:]}
|
||||
return {"status": "completed" if proc.returncode == 0 else "failed", "pid": info["pid"], "elapsed": round(elapsed, 1), "returncode": proc.returncode}
|
||||
return {"status": "running", "pid": info["pid"], "elapsed": round(time.time() - info["started_at"], 1)}
|
||||
|
||||
@@ -31,10 +31,38 @@ MODULES = {
|
||||
"scheduled_task_monitor": {"name": "⏰ 任务监控", "cron": "*"},
|
||||
}
|
||||
|
||||
LOG_FILE_MAP = {
|
||||
"scheduled_refresh_search_cache": "opencode_search",
|
||||
"scheduled_fetch_trends": "trends",
|
||||
"scheduled_collect": "collector",
|
||||
"scheduled_generate": "creator",
|
||||
"scheduled_optimize": "optimizer",
|
||||
"scheduled_optimize_sources": "optimizer_sources",
|
||||
"scheduled_metrics_sync": "metrics_sync",
|
||||
"scheduled_reset_search_usage": "reset_search_usage",
|
||||
"scheduled_task_monitor": "task_monitor",
|
||||
}
|
||||
|
||||
def _log_to_file(module_id: str, status: str, message: str = None, error_trace: str = None):
|
||||
log_name = LOG_FILE_MAP.get(module_id, module_id)
|
||||
log_dir = PROJECT_ROOT / "automation" / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_file = log_dir / f"{log_name}_{datetime.now().strftime('%Y-%m-%d')}.log"
|
||||
ts = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
line = f"[{ts}] [{status.upper()}] {message or ''}"
|
||||
if error_trace:
|
||||
line += f"\n{error_trace[:500]}"
|
||||
try:
|
||||
with open(log_file, 'a', encoding='utf-8') as f:
|
||||
f.write(line + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _log_task(module_id: str, status: str, message: str = None,
|
||||
error_trace: str = None, result_data: dict = None,
|
||||
started_at: datetime = None, finished_at: datetime = None,
|
||||
triggered_by: str = "scheduler", next_run_time: datetime = None):
|
||||
_log_to_file(module_id, status, message, error_trace)
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import TaskLog
|
||||
@@ -159,7 +187,7 @@ class TaskScheduler:
|
||||
logger.info("[Scheduled] Fetching hot trends...")
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).parent.parent.parent.parent / "scripts" / "trends.py")],
|
||||
[sys.executable, str(PROJECT_ROOT / "scripts" / "trends.py")],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
if result.returncode == 0:
|
||||
@@ -190,7 +218,7 @@ class TaskScheduler:
|
||||
logger.info("[Scheduled] Refreshing search cache via opencode...")
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).parent.parent.parent.parent / "scripts" / "opencode_search.py"), "--refresh-cache"],
|
||||
[sys.executable, str(PROJECT_ROOT / "scripts" / "opencode_search.py"), "--refresh-cache"],
|
||||
capture_output=True, text=True, timeout=600
|
||||
)
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
@@ -285,10 +313,10 @@ class TaskScheduler:
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.exception("[Scheduled] Collection failed: %s", e)
|
||||
|
||||
def _run_optimize_sources(self):
|
||||
def _run_optimize_sources(self, triggered_by="scheduler"):
|
||||
"""AI自动优化采集类别与信息源:对比市场热点和当前配置,给出调整建议"""
|
||||
started = datetime.now(timezone.utc)
|
||||
_log_task("scheduled_optimize_sources", "running", started_at=started)
|
||||
_log_task("scheduled_optimize_sources", "running", started_at=started, triggered_by=triggered_by)
|
||||
try:
|
||||
logger.info("[Scheduled] Starting source optimization with AI...")
|
||||
from .nvidia_client import call_llm
|
||||
@@ -337,19 +365,19 @@ class TaskScheduler:
|
||||
"sources_assessed": len(result.get("source_assessment", [])),
|
||||
"suggested_cats": len(result.get("suggested_new_categories", [])),
|
||||
"suggested_srcs": len(result.get("suggested_new_sources", []))},
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
started_at=started, finished_at=datetime.now(timezone.utc), triggered_by=triggered_by)
|
||||
db.close()
|
||||
except Exception as e:
|
||||
_log_task("scheduled_optimize_sources", "failed",
|
||||
message=str(e),
|
||||
error_trace=traceback.format_exc(),
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
started_at=started, finished_at=datetime.now(timezone.utc), triggered_by=triggered_by)
|
||||
logger.exception("[Scheduled] Source AI optimization failed: %s", e)
|
||||
|
||||
def _run_metrics_sync(self):
|
||||
def _run_metrics_sync(self, triggered_by="scheduler"):
|
||||
"""定时从各平台公开API获取发布文章的效果数据(当前仅支持知乎)"""
|
||||
started = datetime.now(timezone.utc)
|
||||
_log_task("scheduled_metrics_sync", "running", started_at=started)
|
||||
_log_task("scheduled_metrics_sync", "running", started_at=started, triggered_by=triggered_by)
|
||||
try:
|
||||
logger.info("[Scheduled] Starting metrics sync (zhihu auto-fetch)...")
|
||||
from ..database import SessionLocal
|
||||
@@ -411,7 +439,7 @@ class TaskScheduler:
|
||||
_log_task("scheduled_metrics_sync", "success",
|
||||
message=f"同步完成,{count} 篇知乎文章",
|
||||
result_data={"articles_synced": count},
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
started_at=started, finished_at=datetime.now(timezone.utc), triggered_by=triggered_by)
|
||||
# 生成指标反馈:按 field 聚合表现,写入 metrics_feedback.json 供 collector 读取
|
||||
try:
|
||||
import json as json_mod
|
||||
@@ -436,7 +464,7 @@ class TaskScheduler:
|
||||
"top_domains": [(f, s) for f, s, _ in scored[:5]],
|
||||
"detail": [{"field": f, "score": s, "articles": c} for f, s, c in scored],
|
||||
}
|
||||
feedback_file = Path(__file__).parent.parent.parent.parent / "automation" / "data" / "metrics_feedback.json"
|
||||
feedback_file = PROJECT_ROOT / "automation" / "data" / "metrics_feedback.json"
|
||||
feedback_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
feedback_file.write_text(json_mod.dumps(feedback_data, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
logger.info("[Scheduled] Metrics feedback written: top domain %s (score %.1f)", scored[0][0], scored[0][1])
|
||||
@@ -445,14 +473,14 @@ class TaskScheduler:
|
||||
else:
|
||||
_log_task("scheduled_metrics_sync", "success",
|
||||
message="无已发布的知乎文章",
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
started_at=started, finished_at=datetime.now(timezone.utc), triggered_by=triggered_by)
|
||||
logger.info("[Scheduled] Metrics sync: no zhihu articles to sync")
|
||||
db.close()
|
||||
except Exception as e:
|
||||
_log_task("scheduled_metrics_sync", "failed",
|
||||
message=str(e),
|
||||
error_trace=traceback.format_exc(),
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
started_at=started, finished_at=datetime.now(timezone.utc), triggered_by=triggered_by)
|
||||
logger.exception("[Scheduled] Metrics sync failed: %s", e)
|
||||
|
||||
def _run_reset_search_usage(self):
|
||||
|
||||
Reference in New Issue
Block a user