186 lines
6.6 KiB
Python
186 lines
6.6 KiB
Python
"""ArchiveProcessor - Copy analysis reports to archive directory
|
|
|
|
Final step in pipeline that copies the generated analysis.md
|
|
to the archive directory organized by platform.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import shutil
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from kb_workflow.pipeline.config import get_settings
|
|
from kb_workflow.pipeline.schemas import ArchiveInput, ArchiveOutput
|
|
from kb_workflow.pipeline.state import WorkflowStateManager
|
|
from kb_workflow.pipeline.processors.base import BaseProcessor
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ArchiveProcessor(BaseProcessor):
|
|
"""Archive 处理器 - 归档分析报告
|
|
|
|
处理流程:
|
|
1. 检查步骤是否已完成(幂等执行)
|
|
2. 检查归档目录是否存在相同 content_id 的文件
|
|
3. 如果存在重复,询问用户是否覆盖
|
|
4. 复制 md 文件到归档目录
|
|
5. 记录处理状态到 workflow_state.json
|
|
|
|
归档目录结构:
|
|
{KB_ARCHIVE_PATH}/{platform}/{YYYYMMDD}_{content_id}_analysis.md
|
|
"""
|
|
|
|
STEP_NAME = "archive"
|
|
|
|
async def process(self, input_data: ArchiveInput) -> ArchiveOutput:
|
|
"""执行归档处理
|
|
|
|
Args:
|
|
input_data: Archive 输入数据
|
|
|
|
Returns:
|
|
Archive 处理结果
|
|
"""
|
|
try:
|
|
import sys
|
|
|
|
# Step 1: 检查是否已完成(幂等执行)
|
|
if self.state_manager:
|
|
state = self.state_manager.load()
|
|
if state and state.is_step_completed(self.STEP_NAME):
|
|
logger.info(f"Step {self.STEP_NAME} already completed, skipping")
|
|
return ArchiveOutput(
|
|
success=True,
|
|
skipped=True,
|
|
platform=input_data.platform,
|
|
archive_file=""
|
|
)
|
|
|
|
# Step 2: 准备归档路径
|
|
archive_base = Path(self.settings.archive_path)
|
|
platform_dir = archive_base / input_data.platform
|
|
platform_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 文件命名: {YYYYMMDD}_{content_id}_analysis.md
|
|
timestamp = datetime.now().strftime("%Y%m%d")
|
|
archive_filename = f"{timestamp}_{input_data.content_id}_analysis.md"
|
|
archive_path = platform_dir / archive_filename
|
|
|
|
# Step 3: 检查重复
|
|
existing_file = self._find_existing_archive(
|
|
platform_dir, input_data.content_id
|
|
)
|
|
if existing_file:
|
|
# 检测是否为非交互式环境
|
|
is_non_interactive = not sys.stdin.isatty()
|
|
should_overwrite = self._ask_overwrite(existing_file, is_non_interactive)
|
|
if not should_overwrite:
|
|
logger.info(f"User chose not to overwrite, skipping archive")
|
|
return ArchiveOutput(
|
|
success=True,
|
|
skipped=True,
|
|
platform=input_data.platform,
|
|
archive_file=str(existing_file)
|
|
)
|
|
# 删除旧文件
|
|
existing_file.unlink()
|
|
logger.info(f"Removed existing archive: {existing_file}")
|
|
|
|
# Step 4: 复制文件
|
|
source_path = Path(input_data.analysis_file)
|
|
if not source_path.exists():
|
|
raise FileNotFoundError(f"Analysis file not found: {source_path}")
|
|
|
|
shutil.copy2(source_path, archive_path)
|
|
logger.info(f"Archived: {source_path} -> {archive_path}")
|
|
|
|
# Step 5: 记录成功状态
|
|
if self.state_manager:
|
|
state = self.state_manager.load()
|
|
if state:
|
|
state.mark_step_completed(
|
|
self.STEP_NAME,
|
|
f"archived to {archive_path}"
|
|
)
|
|
self.state_manager.save(state)
|
|
|
|
return ArchiveOutput(
|
|
success=True,
|
|
skipped=False,
|
|
platform=input_data.platform,
|
|
archive_file=str(archive_path)
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Archive failed: {e}")
|
|
|
|
if self.state_manager:
|
|
state = self.state_manager.load()
|
|
if state:
|
|
state.mark_step_failed(self.STEP_NAME, str(e))
|
|
self.state_manager.save(state)
|
|
|
|
return ArchiveOutput(
|
|
success=False,
|
|
error_message=str(e),
|
|
platform=input_data.platform,
|
|
archive_file=""
|
|
)
|
|
|
|
def _find_existing_archive(self, platform_dir: Path, content_id: str) -> Optional[Path]:
|
|
"""查找已存在的归档文件
|
|
|
|
使用 glob 模式 `*_{content_id}_analysis.md` 搜索
|
|
|
|
Args:
|
|
platform_dir: 平台目录路径
|
|
content_id: 内容 ID
|
|
|
|
Returns:
|
|
已存在的文件路径,如果不存在返回 None
|
|
"""
|
|
pattern = f"*_{content_id}_analysis.md"
|
|
matches = list(platform_dir.glob(pattern))
|
|
return matches[0] if matches else None
|
|
|
|
def _ask_overwrite(self, existing_file: Path, is_non_interactive: bool = False) -> bool:
|
|
"""询问用户是否覆盖已存在的文件
|
|
|
|
使用 rich.prompt.Confirm 模式(与 Pipeline._ask_rerun_step 相同)
|
|
|
|
Args:
|
|
existing_file: 已存在的文件路径
|
|
is_non_interactive: 是否为非交互式环境(stdin不是tty)
|
|
|
|
Returns:
|
|
True 如果用户选择覆盖
|
|
"""
|
|
# 非交互式模式:使用配置决定
|
|
if is_non_interactive:
|
|
if self.settings.overwrite_archives:
|
|
logger.info(f"Non-interactive mode: Overwriting existing archive (KB_OVERWRITE_ARCHIVES=true): {existing_file.name}")
|
|
return True
|
|
else:
|
|
logger.info(f"Non-interactive mode: Skipping existing archive (KB_OVERWRITE_ARCHIVES=false): {existing_file.name}")
|
|
return False
|
|
|
|
# 交互式模式:询问用户
|
|
try:
|
|
from rich.console import Console
|
|
from rich.prompt import Confirm
|
|
|
|
console = Console()
|
|
return Confirm.ask(
|
|
f"Archive already exists: {existing_file.name}\nOverwrite?",
|
|
default=False
|
|
)
|
|
except ImportError:
|
|
response = input(
|
|
f"Archive already exists: {existing_file.name}\nOverwrite? [y/N] "
|
|
)
|
|
return response.lower() in ('y', 'yes')
|