#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ KB Workflow - Knowledge Base Workflow Orchestrator 知识库工作流辅助工具函数。 此模块提供查重、文件操作、Git 管理等工具函数, 供主代理在执行 SKILL.md 中定义的 workflow 时调用。 """ import os import sys import json import argparse import subprocess import shutil import re from datetime import datetime from pathlib import Path from typing import Dict, List, Optional, Any # ==================== 子技能映射 ==================== METADATA_AGENTS = { "bilibili": "kb-bilibili-metadata", "zhihu": "kb-zhihu-metadata", } CONTENT_AGENTS = { "bilibili": "kb-bilibili-content", } # ==================== 工具函数 ==================== def extract_platform(filename: str) -> str: """从文件名提取平台(第一个下划线之前)""" match = re.match(r'^([a-zA-Z0-9]+)_(.+)$', filename) if match: return match.group(1) return "unknown" def extract_base_name(filename: str) -> str: """从文件名提取基础名称(去掉扩展名)""" return Path(filename).stem def get_knowledge_base_path() -> Path: """获取知识库根目录""" base_path_str = os.environ.get('KB_BASE_PATH', '') if base_path_str: return Path(base_path_str).expanduser() return Path.home() / "work" / "knowledge-base" def ensure_directories(base_path: Path = None) -> Dict[str, Path]: """确保目录存在""" if base_path is None: base_path = get_knowledge_base_path() dirs = { "base": base_path, "pending": base_path / "pending", "metadata": base_path / "metadata", "content": base_path / "content", "content_downloads": base_path / "content" / "downloads", "content_subtitles": base_path / "content" / "subtitles", "analyzed": base_path / "analyzed", "archive": base_path / "archive", "archive_subscriptions": base_path / "archive" / "subscriptions", } for name, dir_path in dirs.items(): if isinstance(dir_path, Path): dir_path.mkdir(parents=True, exist_ok=True) return dirs def check_duplicate(base_path: Path = None, url: str = None, platform: str = None, content_id: str = None) -> Dict[str, Any]: """查重检查""" if base_path is None: base_path = get_knowledge_base_path() metadata_dir = base_path / "metadata" archive_dir = base_path / "archive" # 从URL提取ID if url and not content_id: bv_match = re.search(r'BV[a-zA-Z0-9]+', url) content_id = bv_match.group() if bv_match else None result = {"duplicate": False, "status": None, "files": []} if metadata_dir.exists(): for f in metadata_dir.iterdir(): if f.is_file() and content_id and content_id in f.name: result["duplicate"] = True result["status"] = "metadata" result["files"].append(str(f)) if archive_dir.exists(): for date_dir in archive_dir.iterdir(): if date_dir.is_dir(): for f in date_dir.iterdir(): if f.is_file() and content_id and content_id in f.name: result["duplicate"] = True result["status"] = "archived" result["files"].append(str(f)) return result def archive_and_git( analyzed_file: str, base_path: Path = None, git_root: Path = None, git_user: str = None, git_email: str = None, git_remote: str = None ) -> Dict[str, Any]: """归档并提交Git""" if base_path is None: base_path = get_knowledge_base_path() if git_root is None: git_root = base_path / "archive" if git_user is None: git_user = os.environ.get('KB_GIT_USER', '') if git_email is None: git_email = os.environ.get('KB_GIT_EMAIL', '') if git_remote is None: git_remote = os.environ.get('KB_GIT_REMOTE', '') result = { "success": False, "archive_path": "", "git_commit": False, "git_push": None, "error": None } try: # 1. 创建归档目录 date_str = datetime.now().strftime('%Y-%m-%d') date_dir = base_path / "archive" / date_str date_dir.mkdir(parents=True, exist_ok=True) # 2. 复制分析报告 src_file = Path(analyzed_file) archive_file = date_dir / src_file.name shutil.copy(src_file, archive_file) result["archive_path"] = str(archive_file) # 3. 复制元数据 meta_file = base_path / "metadata" / f"{src_file.stem}.meta.json" if meta_file.exists(): shutil.copy(meta_file, date_dir / meta_file.name) # 4. Git 操作 git_dir = git_root / ".git" if not git_dir.exists(): subprocess.run(['git', 'init'], cwd=str(git_root), capture_output=True) if git_user: subprocess.run(['git', 'config', 'user.name', git_user], cwd=str(git_root)) if git_email: subprocess.run(['git', 'config', 'user.email', git_email], cwd=str(git_root)) if (git_root / ".git").exists(): subprocess.run(['git', 'add', '.'], cwd=str(git_root), capture_output=True) commit_msg = f"Archive: {src_file.name} - {datetime.now().isoformat()}" commit_result = subprocess.run( ['git', 'commit', '-m', commit_msg], cwd=str(git_root), capture_output=True, text=True ) result["git_commit"] = commit_result.returncode == 0 if git_remote and commit_result.returncode == 0: push_result = subprocess.run( ['git', 'push', 'origin', 'master'], cwd=str(git_root), capture_output=True, text=True, timeout=60 ) result["git_push"] = {"success": push_result.returncode == 0} result["success"] = True except Exception as e: result["error"] = str(e) return result def generate_summary(url: str, stages: List[Dict], final_file: str, archive_result: Dict = None) -> str: """生成处理完成汇报""" # 提取视频标题(如果有) title_match = re.search(r'com/video/([^?]+)', url) title = title_match.group(1) if title_match else "未知" summary = f""" ✅ **处理完成!** **视频:** {title} **处理阶段:** """ for stage in stages: stage_name = stage.get('stage', 'unknown') emoji = "✓" if stage.get('success') else "○" summary += f"{emoji} {stage_name}\n" if final_file: summary += f"\n**最终文件:** `{final_file}`" if archive_result: if archive_result.get('git_commit'): summary += f"\n**Git:** 已提交" if archive_result.get('git_push', {}).get('success'): summary += " ✓" elif archive_result.get('git_push'): summary += " ✗" return summary # ==================== CLI ==================== def main(): """CLI入口 - 用于命令行测试""" parser = argparse.ArgumentParser( description='KB Workflow - Knowledge Base Workflow Orchestrator', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" 使用示例: # 查重 python kb_workflow.py --check --url "https://www.bilibili.com/video/BVxxx" # 归档 python kb_workflow.py --archive "/path/to/analyzed.md" """ ) parser.add_argument('--check', action='store_true', help='查重') parser.add_argument('--url', type=str, help='要检查的URL') parser.add_argument('--archive', type=str, help='归档的文件路径') parser.add_argument('--base-path', type=str, help='获取知识库根目录') parser.add_argument('--pending', action='store_true', help='获取url解析目录') parser.add_argument('--metadata', action='store_true', help='获取元数据目录') parser.add_argument('--content', action='store_true', help='获取内容目录') parser.add_argument('--content_downloads', action='store_true', help='获取主内容下载目录') parser.add_argument('--content_subtitles', action='store_true', help='获取字幕目录') parser.add_argument('--analyzed', action='store_true', help='获取深度分析目录') parser.add_argument('--archive', action='store_true', help='获取归档目录') parser.add_argument('--archive_subscriptions', action='store_true', help='获取订阅信息目录') parser.add_argument('--extract_platform', type=str, help='要提取平台名称的文件名') args = parser.parse_args() base_path = Path(args.base_path) if args.base_path else None # 获取目录参数处理 dirs = ensure_directories(base_path) if args.extract_platform: platform = extract_platform(args.extract_platform) print(json.dumps({"platform": platform}, ensure_ascii=False, indent=2)) return # 目录路径查询 dir_map = { "pending": dirs.get("pending"), "metadata": dirs.get("metadata"), "content": dirs.get("content"), "content_downloads": dirs.get("content_downloads"), "content_subtitles": dirs.get("content_subtitles"), "analyzed": dirs.get("analyzed"), "archive": dirs.get("archive"), "archive_subscriptions": dirs.get("archive_subscriptions"), } found = False for arg_name, dir_path in dir_map.items(): if getattr(args, arg_name, False) and dir_path: print(json.dumps({"path": str(dir_path)}, ensure_ascii=False, indent=2)) found = True if found: return if args.check and args.url: result = check_duplicate(base_path, args.url) print(json.dumps(result, ensure_ascii=False, indent=2)) elif args.archive: result = archive_and_git(args.archive, base_path) print(json.dumps(result, ensure_ascii=False, indent=2)) else: parser.print_help() if __name__ == '__main__': main()