f2309bf2d1
- Define clear file storage rules: - content/downloads/ for video files - content/subtitles/ for subtitle files - frames/ for video frames - Track all created files in process results - Add directory structure documentation - Show file counts and total size in status
655 lines
24 KiB
Python
655 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
KB Workflow - Knowledge Base Workflow Orchestration
|
|
|
|
知识库工作流编排技能。
|
|
|
|
## 文件存放规则
|
|
|
|
### 知识库目录结构
|
|
|
|
```
|
|
knowledge-base/
|
|
├── pending/ # 用户放入待处理内容 (*.md)
|
|
├── metadata/ # 链接解析结果 (*.link.json)
|
|
├── content/ # 内容提取产物
|
|
│ ├── json/ # 元数据 (*.meta.json)
|
|
│ ├── subtitles/ # 字幕文件 (*.srt/*.lrc)
|
|
│ └── downloads/ # 视频文件 (*.mp4/*.flv)
|
|
├── frames/ # 视频截帧 (*.jpg)
|
|
├── analyzed/ # 分析报告 (*.md)
|
|
├── archive/ # 最终归档 (YYYY-MM-DD/)
|
|
└── reports/ # 汇报文件 (*.json)
|
|
```
|
|
|
|
### 文件保留规则
|
|
|
|
| 文件类型 | 保留位置 | 说明 |
|
|
|---------|---------|------|
|
|
| 原始输入 | `pending/` | 用户放入 |
|
|
| 链接信息 | `metadata/*.link.json` | ✅ 保留 |
|
|
| 元数据 | `content/json/*.meta.json` | ✅ 保留 |
|
|
| 视频文件 | `content/downloads/` | ✅ 保留 |
|
|
| 字幕文件 | `content/subtitles/` | ✅ 保留 |
|
|
| 截帧文件 | `frames/` | ✅ 保留 |
|
|
| 分析报告 | `analyzed/*.md` | ✅ 保留 |
|
|
| 归档文件 | `archive/YYYY-MM-DD/` | ✅ 保留 |
|
|
|
|
Usage:
|
|
python kb_workflow.py --trigger manual --scope pending --limit 5
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import argparse
|
|
import logging
|
|
import subprocess
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any
|
|
import shutil
|
|
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
logger = logging.getLogger('KBWorkflow')
|
|
|
|
|
|
class KBWorkflow:
|
|
"""Knowledge Base Workflow Orchestrator"""
|
|
|
|
# 工作流阶段目录
|
|
STAGES = ['pending', 'metadata', 'content', 'analyzed']
|
|
|
|
def __init__(self,
|
|
base_path: str = None,
|
|
skills_path: str = None,
|
|
output_format: str = "json"):
|
|
"""
|
|
Initialize KB Workflow
|
|
|
|
Args:
|
|
base_path: Base path for knowledge base
|
|
skills_path: Path to skills directory
|
|
output_format: Output format (json, text)
|
|
"""
|
|
self.base_path = Path(base_path) if base_path else Path(__file__).resolve().parent.parent / "knowledge-base"
|
|
self.skills_path = Path(skills_path) if skills_path else Path(__file__).resolve().parent.parent
|
|
self.output_format = output_format
|
|
|
|
# 创建完整的目录结构
|
|
self._ensure_directories()
|
|
|
|
logger.info(f"KB Workflow initialized")
|
|
logger.info(f"Base: {self.base_path}")
|
|
|
|
def _ensure_directories(self):
|
|
"""创建完整的目录结构"""
|
|
# 主阶段目录
|
|
for subdir in self.STAGES:
|
|
dir_path = self.base_path / subdir
|
|
if not dir_path.exists():
|
|
dir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 内容子目录
|
|
content_subdirs = ['json', 'downloads', 'subtitles']
|
|
for subdir in content_subdirs:
|
|
dir_path = self.base_path / 'content' / subdir
|
|
if not dir_path.exists():
|
|
dir_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 截帧目录
|
|
frames_dir = self.base_path / 'frames'
|
|
if not frames_dir.exists():
|
|
frames_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 归档目录
|
|
archive_dir = self.base_path / 'archive'
|
|
if not archive_dir.exists():
|
|
archive_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 汇报目录
|
|
reports_dir = self.base_path / 'reports'
|
|
if not reports_dir.exists():
|
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
logger.info("Directory structure created")
|
|
|
|
def get_directory_structure(self) -> Dict[str, str]:
|
|
"""获取目录结构说明"""
|
|
return {
|
|
'pending': '待处理内容 (*.md)',
|
|
'metadata': '链接解析结果 (*.link.json)',
|
|
'content/json': '元数据 (*.meta.json)',
|
|
'content/downloads': '视频文件 (*.mp4/*.flv)',
|
|
'content/subtitles': '字幕文件 (*.srt/*.lrc)',
|
|
'frames': '视频截帧 (*.jpg)',
|
|
'analyzed': '分析报告 (*.md)',
|
|
'archive/YYYY-MM-DD': '归档文件',
|
|
'reports': '汇报文件 (*.json)'
|
|
}
|
|
|
|
# ==================== 阶段1: 链接解析 ====================
|
|
|
|
def _extract_link(self, file_path: Path) -> Optional[Dict[str, Any]]:
|
|
"""阶段1: 解析链接"""
|
|
try:
|
|
content = file_path.read_text(encoding='utf-8')
|
|
|
|
import re
|
|
url_pattern = r'https?://[^\s<>"]+'
|
|
urls = re.findall(url_pattern, content)
|
|
|
|
if not urls:
|
|
return None
|
|
|
|
url = urls[0]
|
|
result = {
|
|
"platform": self._detect_platform(url),
|
|
"type": self._detect_type(url),
|
|
"id": self._extract_id(url),
|
|
"url": url,
|
|
"original_content": content
|
|
}
|
|
|
|
logger.info(f"Parsed: {result['platform']}/{result['type']} - {result['id']}")
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Link extraction failed: {e}")
|
|
return None
|
|
|
|
def _detect_platform(self, url: str) -> str:
|
|
"""检测平台"""
|
|
if 'bilibili.com' in url or 'b23.tv' in url:
|
|
return 'bilibili'
|
|
elif 'zhihu.com' in url or 'zhuanlan.zhihu.com' in url:
|
|
return 'zhihu'
|
|
elif 'xiaohongshu.com' in url:
|
|
return 'xiaohongshu'
|
|
return 'unknown'
|
|
|
|
def _detect_type(self, url: str) -> str:
|
|
"""检测内容类型"""
|
|
if '/video/' in url:
|
|
return 'video'
|
|
elif '/read/' in url or '/p/' in url:
|
|
return 'article'
|
|
elif '/zvideo/' in url:
|
|
return 'zvideo'
|
|
elif 'space.bilibili.com' in url:
|
|
return 'up主'
|
|
return 'unknown'
|
|
|
|
def _extract_id(self, url: str) -> str:
|
|
"""提取内容ID"""
|
|
import re
|
|
bv = re.search(r'BV[a-zA-Z0-9]{10}', url)
|
|
if bv: return bv.group()
|
|
cv = re.search(r'cv(\d+)', url)
|
|
if cv: return f"cv{cv.group(1)}"
|
|
zhuanlan = re.search(r'zhihu\.com/p/(\d+)', url)
|
|
if zhuanlan: return zhuanlan.group(1)
|
|
zvideo = re.search(r'/zvideo/(\d+)', url)
|
|
if zvideo: return zvideo.group(1)
|
|
num = re.search(r'/(\d+)', url)
|
|
if num: return num.group(1)
|
|
return ''
|
|
|
|
# ==================== 阶段2: 元数据提取 ====================
|
|
|
|
def _extract_metadata(self, file_path: Path, link_info: Dict) -> Dict[str, Any]:
|
|
"""阶段2: 提取元数据"""
|
|
platform = link_info.get('platform', '')
|
|
content_type = link_info.get('type', '')
|
|
content_id = link_info.get('id', '')
|
|
|
|
metadata = {
|
|
"platform": platform,
|
|
"type": content_type,
|
|
"id": content_id,
|
|
"url": link_info.get('url', ''),
|
|
"source_file": file_path.name,
|
|
"extracted_at": datetime.now().isoformat()
|
|
}
|
|
|
|
try:
|
|
if platform == 'bilibili':
|
|
metadata = self._extract_bilibili_metadata(content_type, content_id, metadata)
|
|
elif platform == 'zhihu':
|
|
metadata = self._extract_zhihu_metadata(content_type, content_id, metadata)
|
|
except Exception as e:
|
|
logger.error(f"Metadata extraction failed: {e}")
|
|
metadata["error"] = str(e)
|
|
|
|
return metadata
|
|
|
|
def _extract_bilibili_metadata(self, content_type: str, content_id: str, metadata: Dict) -> Dict:
|
|
"""提取B站元数据"""
|
|
try:
|
|
extractor = self.skills_path / "kb-bilibili-metadata" / "bilibili_metadata.py"
|
|
|
|
if not extractor.exists():
|
|
return metadata
|
|
|
|
cmd = [sys.executable, str(extractor), '--id', content_id, '--type', content_type]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
|
|
if result.returncode == 0:
|
|
data = json.loads(result.stdout)
|
|
if data.get('success'):
|
|
metadata.update(data.get('data', {}))
|
|
logger.info(f"Metadata: {metadata.get('title', '')[:30]}...")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Bilibili metadata failed: {e}")
|
|
|
|
return metadata
|
|
|
|
def _extract_zhihu_metadata(self, content_type: str, content_id: str, metadata: Dict) -> Dict:
|
|
"""提取知乎元数据"""
|
|
try:
|
|
extractor = self.skills_path / "kb-zhihu-metadata" / "zhihu_metadata.py"
|
|
|
|
if not extractor.exists():
|
|
return metadata
|
|
|
|
cmd = [sys.executable, str(extractor), '--id', content_id, '--type', content_type]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
|
|
if result.returncode == 0:
|
|
data = json.loads(result.stdout)
|
|
if data.get('success'):
|
|
metadata.update(data)
|
|
logger.info(f"Metadata: {metadata.get('title', '')[:30]}...")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Zhihu metadata failed: {e}")
|
|
|
|
return metadata
|
|
|
|
# ==================== 阶段3: 内容获取 ====================
|
|
|
|
def _extract_content(self, file_path: Path, metadata: Dict) -> Dict[str, Any]:
|
|
"""阶段3: 获取内容"""
|
|
content = {
|
|
"metadata_id": metadata.get('id', ''),
|
|
"source_file": file_path.name,
|
|
"extracted_at": datetime.now().isoformat(),
|
|
"downloads": {}, # 视频文件
|
|
"subtitles": {} # 字幕文件
|
|
}
|
|
|
|
platform = metadata.get('platform', '')
|
|
content_type = metadata.get('type', '')
|
|
content_id = metadata.get('id', '')
|
|
|
|
try:
|
|
if platform == 'bilibili' and content_type == 'video':
|
|
content = self._extract_bilibili_video(content_id, content)
|
|
elif platform == 'bilibili' and content_type == 'article':
|
|
content = self._extract_bilibili_article(content_id, content)
|
|
except Exception as e:
|
|
logger.error(f"Content extraction failed: {e}")
|
|
content["error"] = str(e)
|
|
|
|
return content
|
|
|
|
def _extract_bilibili_video(self, bvid: str, content: Dict) -> Dict:
|
|
"""提取B站视频"""
|
|
downloads_dir = self.base_path / 'content' / 'downloads'
|
|
subtitles_dir = self.base_path / 'content' / 'subtitles'
|
|
|
|
try:
|
|
extractor = self.skills_path / "kb-bilibili-content" / "bilibili_content.py"
|
|
|
|
# 1. 下载视频
|
|
video_path = downloads_dir / f"{bvid}.flv"
|
|
if not video_path.exists():
|
|
cmd = [sys.executable, str(extractor), '--id', bvid, '--type', 'video', '--output', str(downloads_dir)]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
if result.returncode == 0 and video_path.exists():
|
|
content["downloads"]["video"] = str(video_path)
|
|
logger.info(f"Video downloaded: {video_path.name}")
|
|
|
|
# 2. 提取字幕
|
|
cmd = [sys.executable, str(extractor), '--id', bvid, '--type', 'subtitle']
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
|
|
if result.returncode == 0:
|
|
data = json.loads(result.stdout)
|
|
if data.get('subtitles'):
|
|
content["subtitles"] = data.get('subtitles', [])
|
|
logger.info(f"Subtitles: {len(content['subtitles'])} found")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Bilibili video extraction failed: {e}")
|
|
|
|
return content
|
|
|
|
def _extract_bilibili_article(self, cvid: str, content: Dict) -> Dict:
|
|
"""提取B站专栏"""
|
|
try:
|
|
extractor = self.skills_path / "kb-bilibili-content" / "bilibili_content.py"
|
|
|
|
cmd = [sys.executable, str(extractor), '--id', cvid, '--type', 'article']
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
|
|
if result.returncode == 0:
|
|
data = json.loads(result.stdout)
|
|
if data.get('success'):
|
|
content["article_content"] = data.get('content', '')[:10000]
|
|
logger.info(f"Article: {len(content.get('article_content', ''))} chars")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Bilibili article extraction failed: {e}")
|
|
|
|
return content
|
|
|
|
# ==================== 阶段4: 深度分析 ====================
|
|
|
|
def _analyze_content(self, metadata: Dict, content: Dict) -> str:
|
|
"""阶段4: 深度分析"""
|
|
try:
|
|
analyzer = self.skills_path / "kb-deep-analyzer" / "deep_analyzer.py"
|
|
|
|
if not analyzer.exists():
|
|
return f"# {metadata.get('title', 'Unknown')}\n\nDeep analyzer not found"
|
|
|
|
import tempfile
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
json.dump(metadata, f, ensure_ascii=False)
|
|
meta_path = f.name
|
|
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
json.dump(content, f, ensure_ascii=False)
|
|
content_path = f.name
|
|
|
|
try:
|
|
output_file = str(self.base_path / 'analyzed' / f"{Path(meta_path).stem}.md")
|
|
|
|
cmd = [sys.executable, str(analyzer), '--metadata', meta_path,
|
|
'--content', content_path, '--output', output_file, '--no-frontmatter']
|
|
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
|
|
if result.returncode == 0 and Path(output_file).exists():
|
|
report = Path(output_file).read_text(encoding='utf-8')
|
|
logger.info(f"Analysis: {len(report)} chars")
|
|
return report
|
|
else:
|
|
logger.error(f"Analysis failed: {result.stderr}")
|
|
|
|
finally:
|
|
for p in [meta_path, content_path]:
|
|
if Path(p).exists():
|
|
Path(p).unlink()
|
|
|
|
except Exception as e:
|
|
logger.error(f"Deep analysis failed: {e}")
|
|
return f"# {metadata.get('title', 'Unknown')}\n\nError: {str(e)}"
|
|
|
|
return f"# {metadata.get('title', 'Unknown')}\n\n"
|
|
|
|
# ==================== 主处理流程 ====================
|
|
|
|
def _save_json(self, data: Dict, subdir: str, base_name: str, suffix: str) -> Path:
|
|
"""保存JSON文件"""
|
|
file_name = base_name + suffix
|
|
file_path = self.base_path / subdir / file_name
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
logger.info(f"Saved: {subdir}/{file_name}")
|
|
return file_path
|
|
|
|
def _move_to_stage(self, file_path: Path, from_stage: str, to_stage: str) -> Path:
|
|
"""移动文件到指定阶段"""
|
|
new_path = self.base_path / to_stage / file_path.name
|
|
if file_path.exists():
|
|
file_path.rename(new_path)
|
|
logger.info(f"Moved: {file_path.name} → {to_stage}/")
|
|
return new_path
|
|
|
|
def process_item(self, item: Dict) -> Dict[str, Any]:
|
|
"""处理单个项目"""
|
|
result = {
|
|
"item": item,
|
|
"success": False,
|
|
"stages_completed": [],
|
|
"files_created": [],
|
|
"error": None
|
|
}
|
|
|
|
file_path = Path(item['path'])
|
|
base_name = file_path.stem
|
|
|
|
try:
|
|
logger.info(f"Processing: {file_path.name}")
|
|
|
|
# 阶段1: 链接解析
|
|
link_info = self._extract_link(file_path)
|
|
if not link_info:
|
|
result["error"] = "无法解析链接"
|
|
return result
|
|
|
|
result["stages_completed"].append("link_parsing")
|
|
self._save_json(link_info, 'metadata', base_name, '.link.json')
|
|
|
|
# 阶段2: 元数据提取
|
|
logger.info(f"Stage 2: Extracting metadata...")
|
|
metadata = self._extract_metadata(file_path, link_info)
|
|
if 'error' not in metadata:
|
|
self._save_json(metadata, 'content/json', base_name, '.meta.json')
|
|
result["stages_completed"].append("metadata_extraction")
|
|
|
|
# 阶段3: 内容获取
|
|
if 'error' not in metadata:
|
|
logger.info(f"Stage 3: Extracting content...")
|
|
content = self._extract_content(file_path, metadata)
|
|
if 'error' not in content:
|
|
self._save_json(content, 'content/json', base_name, '.content.json')
|
|
result["stages_completed"].append("content_extraction")
|
|
|
|
# 列出下载的文件
|
|
downloads = content.get('downloads', {})
|
|
for key, path in downloads.items():
|
|
result["files_created"].append(f"content/downloads/{Path(path).name}")
|
|
|
|
# 阶段4: 深度分析
|
|
logger.info(f"Stage 4: Deep analysis...")
|
|
report = self._analyze_content(metadata, content)
|
|
|
|
report_path = self.base_path / 'analyzed' / f"{base_name}.md"
|
|
with open(report_path, 'w', encoding='utf-8') as f:
|
|
f.write(report)
|
|
|
|
result["files_created"].append(f"analyzed/{report_path.name}")
|
|
result["stages_completed"].append("deep_analysis")
|
|
|
|
# 移动原文件到metadata阶段
|
|
self._move_to_stage(file_path, 'pending', 'metadata')
|
|
|
|
result["success"] = len(result["stages_completed"]) >= 2
|
|
logger.info(f"Complete: {len(result['stages_completed'])} stages, {len(result['files_created'])} files")
|
|
|
|
except Exception as e:
|
|
result["error"] = str(e)
|
|
logger.error(f"Processing failed: {e}")
|
|
|
|
return result
|
|
|
|
def process_all(self, scope: str = "pending", limit: int = None) -> Dict[str, Any]:
|
|
"""处理所有项目"""
|
|
summary = {"processed": 0, "failed": 0, "results": []}
|
|
|
|
inbox = self.scan_inbox(scope=scope)
|
|
|
|
for stage, items in inbox.items():
|
|
if limit and summary['processed'] >= limit:
|
|
break
|
|
|
|
for item in items:
|
|
if limit and summary['processed'] >= limit:
|
|
break
|
|
|
|
result = self.process_item(item)
|
|
summary["results"].append(result)
|
|
|
|
if result["success"]:
|
|
summary["processed"] += 1
|
|
else:
|
|
summary["failed"] += 1
|
|
|
|
return summary
|
|
|
|
# ==================== 状态和汇报 ====================
|
|
|
|
def scan_inbox(self, scope: str = "all") -> Dict[str, List[Dict]]:
|
|
"""扫描收件箱"""
|
|
result = {}
|
|
|
|
if scope == "all":
|
|
stages = self.STAGES
|
|
elif scope in self.STAGES:
|
|
stages = [scope]
|
|
else:
|
|
stages = self.STAGES
|
|
|
|
for stage in stages:
|
|
dir_path = self.base_path / stage
|
|
items = []
|
|
|
|
if dir_path.exists():
|
|
for item_path in dir_path.iterdir():
|
|
if item_path.is_file():
|
|
items.append({
|
|
'name': item_path.name,
|
|
'path': str(item_path),
|
|
'stage': stage,
|
|
'modified': datetime.fromtimestamp(
|
|
item_path.stat().st_mtime
|
|
).isoformat()
|
|
})
|
|
|
|
result[stage] = items
|
|
logger.info(f"Scanned {stage}: {len(items)} items")
|
|
|
|
return result
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""获取状态"""
|
|
inbox = self.scan_inbox(scope="all")
|
|
|
|
# 统计各类文件
|
|
file_counts = {}
|
|
total_size = 0
|
|
|
|
for root, dirs, files in os.walk(self.base_path):
|
|
rel_path = Path(root).relative_to(self.base_path)
|
|
for f in files:
|
|
file_path = Path(root) / f
|
|
ext = file_path.suffix
|
|
file_counts[ext] = file_counts.get(ext, 0) + 1
|
|
total_size += file_path.stat().st_size
|
|
|
|
return {
|
|
"status": "ready",
|
|
"timestamp": datetime.now().isoformat(),
|
|
"stages": inbox,
|
|
"total_items": sum(len(items) for items in inbox.values()),
|
|
"file_counts": file_counts,
|
|
"total_size_mb": round(total_size / 1024 / 1024, 2),
|
|
"directories": self.get_directory_structure()
|
|
}
|
|
|
|
def archive_old_items(self, days: int = 30) -> Dict[str, Any]:
|
|
"""归档旧项目"""
|
|
archive_path = self.base_path / "archive" / datetime.now().strftime('%Y-%m-%d')
|
|
archive_path.mkdir(parents=True, exist_ok=True)
|
|
|
|
result = {"archived": 0, "errors": []}
|
|
cutoff = datetime.now() - timedelta(days=days)
|
|
|
|
for stage in self.STAGES:
|
|
stage_path = self.base_path / stage
|
|
|
|
if not stage_path.exists():
|
|
continue
|
|
|
|
for item_path in stage_path.iterdir():
|
|
if item_path.is_file():
|
|
mtime = datetime.fromtimestamp(item_path.stat().st_mtime)
|
|
|
|
if mtime < cutoff:
|
|
try:
|
|
new_path = archive_path / item_path.name
|
|
item_path.rename(new_path)
|
|
result["archived"] += 1
|
|
except Exception as e:
|
|
result["errors"].append(str(e))
|
|
|
|
logger.info(f"Archived {result['archived']} items")
|
|
return result
|
|
|
|
|
|
# ==================== CLI ====================
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description='KB Workflow - Knowledge Base Workflow Orchestration',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
## 文件存放规则
|
|
|
|
pending/ 用户放入待处理内容 (*.md)
|
|
metadata/ 链接解析结果 (*.link.json)
|
|
content/
|
|
├── json/ 元数据 (*.meta.json)
|
|
├── downloads/ 视频文件 (*.mp4/*.flv)
|
|
└── subtitles/ 字幕文件 (*.srt/*.lrc)
|
|
frames/ 视频截帧 (*.jpg)
|
|
analyzed/ 分析报告 (*.md)
|
|
archive/ 归档文件 (YYYY-MM-DD/)
|
|
reports/ 汇报文件 (*.json)
|
|
|
|
示例:
|
|
python kb_workflow.py --trigger status
|
|
python kb_workflow.py --trigger manual --scope pending --limit 5
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('--trigger', type=str, default='status',
|
|
choices=['status', 'manual', 'archive', 'report'],
|
|
help='触发类型')
|
|
parser.add_argument('--scope', type=str, default='pending',
|
|
choices=['all', 'pending', 'metadata', 'content', 'analyzed'],
|
|
help='处理范围')
|
|
parser.add_argument('--limit', type=int, default=None,
|
|
help='处理数量限制')
|
|
parser.add_argument('--days', type=int, default=30,
|
|
help='归档天数')
|
|
|
|
args = parser.parse_args()
|
|
|
|
workflow = KBWorkflow()
|
|
|
|
if args.trigger == 'status':
|
|
result = workflow.get_status()
|
|
elif args.trigger == 'manual':
|
|
result = workflow.process_all(scope=args.scope, limit=args.limit)
|
|
elif args.trigger == 'archive':
|
|
result = workflow.archive_old_items(days=args.days)
|
|
else:
|
|
result = {"status": "unknown"}
|
|
|
|
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|