Skill: crayfish-ai/paper-management-system (version 2.0.2)

--- name: paper-management-system description: 文献管理系统 - 自动化PDF文献索引、搜索、AI提炼工具。当用户需要管理PDF文献、自动索引、搜索文献、提取元数据时激活。 metadata: { "openclaw": { "emoji": "📚", "tags": ["pdf", "papers", "research", "academic", "indexing"] } } --- # Paper Management System 文献管理系统 - 自动化 PDF 文献管理工具（v2.0） ## 功能特性 - 自动索引：扫描 PDF，提取元数据 - 智能搜索：按关键词/年份/作者搜索 - AI 提炼：生成结构化摘要 - 自动重命名：`作者_年份_关键词.pdf` - 增量处理 + Hash 去重 - 飞书通知（需 feishu-relay） ## 所需环境变量 | 变量名 | 必须 | 说明 | |--------|------|------| | `PAPERMGR_PAPERS_DIR` | 否 | PDF 存储目录（默认 `./papers`） | | `PAPERMGR_DOWNLOADS_DIR` | 否 | 下载目录（默认 `./downloads`） | | `PAPERMGR_DATABASE_PATH` | 否 | 数据库路径（默认 `./data/index.db`） | | `PAPERMGR_AI_ENABLED` | 否 | 启用 AI（默认 false） | | `OPENAI_API_KEY` | 否 | OpenAI API Key | ## 调用方式 ### 自动（cron） ```bash */30 * * * * /path/to/scripts/auto_index.sh ``` ### 手动 ```bash python3 scripts/paper_manager.py index # 索引 python3 scripts/paper_manager.py rename # 重命名 python3 scripts/paper_manager.py search <关键词> # 搜索 python3 scripts/paper_manager.py status # 状态 ``` ## 输入 - PDF 文件（放入 `downloads/` 或 `papers/` 目录） ## 输出 - SQLite 数据库（`data/index.db`） - 重命名后的 PDF 文件 - AI 摘要（`ai_summary` 字段，可选） - 飞书通知（可选） ## 发布信息 - 版本：v2.0.0 - 许可证：MIT - GitHub：https://github.com/crayfish-ai/paper-management-system

path: README.md
content: # Paper Management System

自动化 PDF 文献管理工具——索引、搜索、AI 提炼、飞书通知。

## 功能特性

- 📄 **自动索引**：扫描 PDF 文件，提取元数据（标题、作者、DOI）
- 🔍 **智能搜索**：按关键词、年份、作者搜索文献
- 🤖 **AI 提炼**：基于 PDF 全文生成结构化摘要
- 📚 **自动重命名**：按 `第一作者_年份_关键词.pdf` 规范命名
- 🔄 **增量处理**：仅处理新文件，不重复索引
- 🗑️ **Hash 去重**：自动跳过重复文件
- 📬 **飞书通知**：处理完成后自动推送结果（需配置 notify）

## 目录结构

```
paper-management-system/
├── README.md
├── SKILL.md
├── .env.example
├── config.example.yaml
├── scripts/
│   ├── auto_index.sh        # 定时索引入口（cron）
│   ├── paper_manager.py     # 索引+重命名核心
│   ├── extract_fulltext.py  # PDF 全文提取
│   ├── ai_summarize.py     # AI 摘要生成
│   └── config.py           # 配置管理
├── papers/                  # PDF 文件目录（需手动创建或配置）
├── downloads/               # 下载目录（自动移动新文件）
├── data/                   # 数据库目录
└── logs/                   # 日志目录
```

## 安装步骤

### 1. 克隆或解压

```bash
git clone https://github.com/crayfish-ai/paper-management-system.git
cd paper-management-system
```

### 2. 配置

```bash
cp config.example.yaml config.yaml
# 编辑 config.yaml，填入必要配置
```

或使用环境变量（推荐）：

```bash
export PAPERMGR_PAPERS_DIR=/path/to/your/papers
export PAPERMGR_DOWNLOADS_DIR=/path/to/downloads
export PAPERMGR_DATABASE_PATH=/path/to/data/index.db
```

### 3. 安装依赖

```bash
pip install -r requirements.txt
```

### 4. 配置定时任务

```bash
# 每30分钟运行一次
*/30 * * * * /path/to/paper-management-system/scripts/auto_index.sh
```

## 配置说明

| 配置项 | 环境变量 | 说明 | 默认值 |
|--------|---------|------|--------|
| papers.dir | PAPERMGR_PAPERS_DIR | PDF 存储目录 | `./papers` |
| downloads.dir | PAPERMGR_DOWNLOADS_DIR | 下载目录 | `./downloads` |
| database.path | PAPERMGR_DATABASE_PATH | SQLite 数据库路径 | `./data/index.db` |
| ai.enabled | PAPERMGR_AI_ENABLED | 启用 AI 提炼 | false |
| ai.api_key | OPENAI_API_KEY | OpenAI API Key | - |
| notification.enabled | PAPERMGR_NOTIFICATION_ENABLED | 启用通知 | false |

## 使用流程

### 自动流程（cron）

1. 将 PDF 文件放入 `downloads/` 目录
2. `auto_index.sh` 自动：
   - 移动文件到 `papers/` 目录
   - 索引新 PDF（提取元数据）
   - 按规范重命名
   - 提取全文
   - 生成 AI 摘要（如启用）

### 手动命令

```bash
# 索引所有 PDF
python3 scripts/paper_manager.py index

# 按规范重命名
python3 scripts/paper_manager.py rename

# 搜索文献
python3 scripts/paper_manager.py search "瘢痕"

# 查看状态
python3 scripts/paper_manager.py status
```

## 依赖

- Python 3.8+
- PyMuPDF（PDF 元数据提取）
- sqlite3（内置）
- OpenAI API（可选，用于 AI 摘要）

## 与 OpenClaw 集成

本系统可作为 OpenClaw skill 使用，通过 `feishu-relay` 发送飞书通知。


path: config.example.yaml
content: # Paper Management System Configuration

database:
  path: ./data/index.db

papers:
  dir: ./papers
  extensions:
    - .pdf

downloads:
  dir: ./downloads
  keywords:
    - 科研通
    - ablesci

logging:
  path: ./logs/auto_index.log
  level: INFO

ai:
  enabled: false
  provider: openai
  model: gpt-3.5-turbo
  api_key: ""
  max_text_length: 50000

notification:
  enabled: false
  cmd: ""


path: requirements.txt
content: # Paper Management System Dependencies

# PDF processing
PyMuPDF>=1.23.0

# AI summarization (optional)
openai>=1.0.0
anthropic>=0.20.0

# Configuration
pyyaml>=6.0

# Development
pytest>=7.0.0
pytest-cov>=4.0.0


path: scripts/ai_summarize.py
content: #!/usr/bin/env python3
"""
AI智能文献提炼模块

功能：
- 读取新提取全文的文献
- 生成结构化摘要
- 保存到ai_summary字段
- 发送通知（可配置）

配置（config.yaml 或环境变量 PAPERMGR_*）：
- ai.enabled: 是否启用AI提炼
- ai.provider: AI提供商 (openai/anthropic)
- ai.model: 模型名称
- notification.enabled: 是否发送通知
- notification.cmd: 通知命令路径（或 stdout）
"""

import sqlite3
import sys
import os
import re
import subprocess
from pathlib import Path

# Add project directory to path
PROJECT_DIR = Path(__file__).parent
sys.path.insert(0, str(PROJECT_DIR))

from config import get_config

def get_db():
    """获取数据库连接"""
    cfg = get_config()
    return sqlite3.connect(cfg.db_path)

def get_papers_need_summary():
    """获取需要AI提炼的文献"""
    conn = get_db()
    conn.row_factory = sqlite3.Row
    papers = conn.execute("""
        SELECT id, title, authors, doi, year, journal, full_text 
        FROM papers 
        WHERE full_text IS NOT NULL 
        AND (ai_summary IS NULL OR ai_summary = '') 
        AND LENGTH(full_text) > 1000
        ORDER BY id DESC
        LIMIT 10
    """).fetchall()
    conn.close()
    return papers

def generate_summary(title, authors, journal, year, full_text):
    """生成结构化中文摘要"""
    summary_parts = []
    
    # 1. 研究背景/目的
    background = extract_section(full_text, 
        ["Background", "OBJECTIVE", "AIM", "PURPOSE", "Introduction", "INTRODUCTION"],
        ["Methods", "METHODS", "Material", "MATERIAL", "方法"])
    if background:
        summary_parts.append(f"📌 **研究背景**: {background[:200]}...")
    
    # 2. 研究方法
    methods = extract_section(full_text,
        ["Methods", "METHODS", "Methodology", "方法", "材料与方法"],
        ["Results", "RESULTS", "Findings", "FINDINGS", "结果"])
    if methods:
        summary_parts.append(f"🔬 **研究方法**: {methods[:200]}...")
    
    # 3. 主要结果
    results = extract_section(full_text,
        ["Results", "RESULTS", "Findings", "FINDINGS", "结果"],
        ["Conclusion", "CONCLUSION", "Discussion", "DISCUSSION", "结论", "讨论"])
    if results:
        summary_parts.append(f"📊 **主要结果**: {results[:300]}...")
    
    # 4. 结论
    conclusion = extract_section(full_text,
        ["Conclusion", "CONCLUSION", "Conclusions", "CONCLUSIONS", "结论"],
        ["References", "REFERENCES", "Acknowledgment", "参考文献"])
    if conclusion:
        summary_parts.append(f"💡 **核心结论**: {conclusion[:200]}...")
    
    # 兜底：使用abstract
    if not summary_parts:
        abstract_match = re.search(r'Abstract[\s:]*(.*?)(?:Keywords|Introduction|$)', 
                                   full_text, re.DOTALL | re.IGNORECASE)
        if abstract_match:
            summary_parts.append(f"📝 **摘要**: {abstract_match.group(1)[:500]}...")
    
    header_lines = ["📄 **文献AI提炼**", ""]
    if title:
        header_lines.append(f"**标题**: {title}")
    if authors:
        header_lines.append(f"**作者**: {authors}")
    if journal:
        header_lines.append(f"**期刊**: {journal}")
    if year:
        header_lines.append(f"**年份**: {year}")
    header_lines.append("")
    
    header = "\n".join(header_lines)
    return header + "\n\n".join(summary_parts)

def extract_section(text, start_keywords, end_keywords):
    """提取特定章节"""
    text = re.sub(r'ARTICLE IN PRESS|ACCEPTED MANUSCRIPT|© The Author\(s\).*?licence', '', text, flags=re.IGNORECASE)
    text = re.sub(r'https?://\S+', '', text)
    
    start_pattern = '|'.join(re.escape(k) for k in start_keywords)
    end_pattern = '|'.join(re.escape(k) for k in end_keywords)
    
    patterns = [
        rf'(?:{start_pattern})[\s:]+\n?(.*?)(?:(?:{end_pattern})|$)',
        rf'(?:{start_pattern})\s*\n\s*\n?(.*?)(?:(?:{end_pattern})|$)',
        rf'\d+\.\s*(?:{start_pattern})[\s:]*\n?(.*?)(?:(?:{end_pattern})|$)',
    ]
    
    for pattern in patterns:
        match = re.search(pattern, text, re.DOTALL | re.IGNORECASE)
        if match:
            section = match.group(1).strip()
            section = re.sub(r'\s+', ' ', section)
            section = re.sub(r'^[\s\W]+', '', section)
            if len(section) > 20:
                return section
    return ""

def save_summary(paper_id, summary):
    """保存AI提炼结果"""
    conn = get_db()
    conn.execute("UPDATE papers SET ai_summary = ? WHERE id = ?", (summary, paper_id))
    conn.commit()
    conn.close()

def send_notification(title, message, notify_cmd):
    """发送通知（适配器模式）"""
    if not notify_cmd or notify_cmd.strip() == "":
        print(f"[NOTIFICATION DISABLED] {title}: {message[:100]}...")
        return True
    
    if notify_cmd.lower() == "stdout":
        print(f"\n{'='*60}")
        print(f"通知: {title}")
        print(f"{'='*60}")
        print(message)
        print(f"{'='*60}\n")
        return True
    
    # 执行外部通知命令
    try:
        cmd = f'{notify_cmd} "{title}" "{message[:1500]}"'
        result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
        if result.returncode == 0:
            print(f"✓ 通知已发送")
            return True
        else:
            print(f"✗ 通知失败: {result.stderr[:200]}")
            return False
    except subprocess.TimeoutExpired:
        print(f"✗ 通知超时")
        return False
    except Exception as e:
        print(f"✗ 通知异常: {e}")
        return False

def main():
    cfg = get_config()
    
    # 检查是否启用
    if not cfg.ai_enabled:
        print("AI提炼功能未启用（ai.enabled=false）")
        return
    
    papers = get_papers_need_summary()
    
    if not papers:
        print("没有需要提炼的文献")
        return
    
    print(f"发现 {len(papers)} 篇需要AI提炼的文献")
    
    # 获取通知配置
    notify_cmd = cfg.notification_cmd if cfg.notification_enabled else ""
    
    for i, paper in enumerate(papers):
        title_display = paper['title'][:50] if paper['title'] else f"ID:{paper['id']}"
        print(f"\n处理 [{i+1}/{len(papers)}]: {title_display}...")
        
        # 生成摘要
        summary = generate_summary(
            paper['title'],
            paper['authors'],
            paper['journal'],
            paper['year'],
            paper['full_text']
        )
        
        # 保存到数据库
        save_summary(paper['id'], summary)
        print(f"✓ 已保存提炼内容 (ID: {paper['id']})")
        
        # 发送通知（只发送最新的一篇）
        if i == 0 and notify_cmd:
            send_notification("📚 新文献AI提炼", summary, notify_cmd)

if __name__ == "__main__":
    main()


path: scripts/auto_index.sh
content: #!/bin/bash
#=============================================================================
# Paper Management System - Auto Index Script
#
# 功能：增量索引 + 自动移动下载文件 + 全文提取 + AI提炼
# 配置：通过环境变量 PAPERMGR_* 设置，或使用默认值（相对于项目目录）
#=============================================================================

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"

# Default values relative to project directory
PAPERS_DIR="${PAPERMGR_PAPERS_DIR:-${PROJECT_DIR}/papers}"
DOWNLOADS_DIR="${PAPERMGR_DOWNLOADS_DIR:-${PROJECT_DIR}/downloads}"
DB_PATH="${PAPERMGR_DATABASE_PATH:-${PROJECT_DIR}/data/index.db}"
LOG="${PAPERMGR_LOGGING_PATH:-${PROJECT_DIR}/logs/auto_index.log}"

mkdir -p "$(dirname "$LOG")" 2>/dev/null || true
mkdir -p "$PAPERS_DIR" 2>/dev/null || true
mkdir -p "$(dirname "$DB_PATH")" 2>/dev/null || true

cd "$PAPERS_DIR"

#=============================================================================
# Step 1: 从 Downloads 目录移动新PDF文件
#=============================================================================
MOVED_COUNT=0
for f in "$DOWNLOADS_DIR"/*.pdf 2>/dev/null; do
    if [ -f "$f" ]; then
        if [[ "$(basename "$f")" == *"科研通"* ]] || [[ "$(basename "$f")" == *"ablesci"* ]]; then
            FILE_HASH=$(md5sum "$f" | awk '{print $1}')
            EXISTS=$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM papers WHERE file_hash='$FILE_HASH';" 2>/dev/null || echo "0")
            
            if [ "$EXISTS" = "0" ]; then
                mv "$f" "$PAPERS_DIR/"
                echo "[$(date)] 移动文件: $(basename "$f")" >> "$LOG"
                ((MOVED_COUNT++))
            else
                echo "[$(date)] 跳过已存在: $(basename "$f")" >> "$LOG"
                rm -f "$f"
            fi
        fi
    fi
done

if [ "$MOVED_COUNT" -gt 0 ]; then
    echo "[$(date)] 从Downloads移动了 $MOVED_COUNT 个新文件" >> "$LOG"
fi

#=============================================================================
# Step 2: 检测并索引新文件
#=============================================================================
NEW_COUNT=$(python3 -c "
import sys
sys.path.insert(0, '$PROJECT_DIR/scripts')
from config import get_config
from paper_manager import get_db
import hashlib, glob

cfg = get_config()
conn = get_db()
indexed = set(r[0] for r in conn.execute('SELECT file_hash FROM papers').fetchall())
new = 0
for f in glob.glob(cfg.papers_dir + '/*.pdf'):
    try:
        h = hashlib.md5(open(f,'rb').read()).hexdigest()
        if h not in indexed:
            new += 1
    except:
        pass
conn.close()
print(new)
" 2>/dev/null || echo "0")

if [ "$NEW_COUNT" -gt 0 ]; then
    echo "[$(date)] 发现 $NEW_COUNT 个新文件，开始索引..." >> "$LOG"
    
    python3 "${PROJECT_DIR}/scripts/paper_manager.py" index >> "$LOG" 2>&1
    python3 "${PROJECT_DIR}/scripts/paper_manager.py" rename >> "$LOG" 2>&1
    python3 "${PROJECT_DIR}/scripts/extract_fulltext.py" >> "$LOG" 2>&1

    if python3 -c "
import sys
sys.path.insert(0, '$PROJECT_DIR/scripts')
from config import get_config
cfg = get_config()
print('true' if cfg.ai_enabled else 'false')
" 2>/dev/null | grep -q "true"; then
        echo "[$(date)] 开始AI提炼..." >> "$LOG"
        python3 "${PROJECT_DIR}/scripts/ai_summarize.py" >> "$LOG" 2>&1
    fi

    echo "[$(date)] 索引+全文+AI提炼完成" >> "$LOG"
else
    echo "[$(date)] 没有新文件" >> "$LOG"
fi


path: scripts/config.py
content: #!/usr/bin/env python3
"""
Configuration loader for Paper Management System

Supports multiple config sources (in order of precedence):
1. config.yaml in project directory
2. Environment variables with PAPERMGR_ prefix
3. Default values (relative to project directory)

Usage:
    from config import get_config
    cfg = get_config()
    db_path = cfg.db_path
"""

import os
import yaml
from pathlib import Path
from typing import Any, Optional

# Detect project directory (parent of scripts/)
_PROJECT_DIR = Path(__file__).parent.parent.resolve()

# Default configuration values (relative to project directory)
DEFAULTS = {
    "database.path": str(_PROJECT_DIR / "data" / "index.db"),
    "papers.dir": str(_PROJECT_DIR / "papers"),
    "papers.extensions": [".pdf"],
    "downloads.dir": str(_PROJECT_DIR / "downloads"),
    "downloads.keywords": ["科研通", "ablesci"],
    "logging.path": str(_PROJECT_DIR / "logs" / "auto_index.log"),
    "logging.level": "INFO",
    "ai.enabled": False,
    "ai.provider": "openai",
    "ai.model": "gpt-3.5-turbo",
    "ai.api_key": "",
    "ai.max_text_length": 50000,
    "notification.enabled": False,
    "notification.cmd": "",
}


class Config:
    """Configuration manager with environment variable override support"""
    
    _instance: Optional['Config'] = None
    _config: dict = {}
    
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._load()
        return cls._instance
    
    def _load(self):
        self._config = DEFAULTS.copy()
        
        config_paths = [
            Path(__file__).parent / "config.yaml",
            Path(__file__).parent.parent / "config.yaml",
        ]
        
        for path in config_paths:
            if path.exists():
                with open(path, 'r') as f:
                    file_config = yaml.safe_load(f)
                    if file_config:
                        self._merge_config(self._config, file_config)
                break
        
        for key in list(self._config.keys()):
            env_key = f"PAPERMGR_{key.upper().replace('.', '_')}"
            if env_key in os.environ:
                value = os.environ[env_key]
                default_value = self._config.get(key)
                if isinstance(default_value, bool):
                    value = value.lower() in ('true', '1', 'yes')
                elif isinstance(default_value, list):
                    value = value.split(',')
                self._config[key] = value
    
    def _merge_config(self, base: dict, override: dict):
        for key, value in override.items():
            if key in base and isinstance(base[key], dict) and isinstance(value, dict):
                self._merge_config(base[key], value)
            else:
                base[key] = value
    
    def get(self, key: str, default: Any = None) -> Any:
        return self._config.get(key, default)
    
    @property
    def db_path(self) -> str:
        return self.get("database.path")
    
    @property
    def papers_dir(self) -> str:
        return self.get("papers.dir")
    
    @property
    def downloads_dir(self) -> str:
        return self.get("downloads.dir")
    
    @property
    def notification_cmd(self) -> str:
        return self.get("notification.cmd")
    
    @property
    def notification_enabled(self) -> bool:
        return self.get("notification.enabled")
    
    @property
    def ai_enabled(self) -> bool:
        return self.get("ai.enabled")
    
    @property
    def ai_api_key(self) -> str:
        [REDACTED_SECRET]"OPENAI_API_KEY", "")
        if not [REDACTED_SECRET] = self.get("ai.api_key", "")
        return api_key


def get_config() -> Config:
    return Config()


path: scripts/extract_fulltext.py
content: #!/usr/bin/env python3
"""
Full-text extraction for PDFs
写入 index.db 的 full_text 字段
"""

import fitz
import sqlite3
import os
import sys
import glob
import json
import time
from pathlib import Path

# Add project directory to path
PROJECT_DIR = Path(__file__).parent
sys.path.insert(0, str(PROJECT_DIR))

from config import get_config

def get_db():
    """Get database connection"""
    cfg = get_config()
    conn = sqlite3.connect(cfg.db_path)
    conn.row_factory = sqlite3.Row
    conn.execute("PRAGMA journal_mode=WAL")
    return conn

def get_log_path():
    """Get log file path from config"""
    cfg = get_config()
    log_dir = os.path.dirname(cfg.get("logging.path", str(Path(__file__).parent.parent / "logs" / "extract_fulltext.log")))
    return os.path.join(log_dir, "extract_fulltext.log")

def get_failed_path():
    """Get failed list path from config"""
    cfg = get_config()
    log_dir = os.path.dirname(cfg.get("logging.path", str(Path(__file__).parent.parent / "logs" / "extract_failed.json")))
    return os.path.join(log_dir, "extract_failed.json")

def extract_fulltext(pdf_path, max_pages=0):
    """Extract PDF full text, max_pages=0 means all pages"""
    try:
        doc = fitz.open(pdf_path)
        total_pages = len(doc)
        
        if max_pages > 0:
            pages = range(min(max_pages, total_pages))
        else:
            pages = range(total_pages)
        
        texts = []
        for i in pages:
            text = doc[i].get_text()
            if text and text.strip():
                texts.append(text.strip())
        
        doc.close()
        full = '\n\n'.join(texts)
        
        # Skip if text is too short (likely scanned PDF)
        if len(full.strip()) < 50:
            return None, f"文本不足50字符(共{total_pages}页)，可能是扫描件"
        
        return full, None
    except Exception as e:
        return None, str(e)

def extract_all():
    """Extract full text for all papers without full_text"""
    cfg = get_config()
    papers_dir = cfg.papers_dir
    
    conn = get_db()
    rows = conn.execute(
        "SELECT id, filepath, title FROM papers WHERE full_text IS NULL OR full_text = ''"
    ).fetchall()
    total = len(rows)
    success = 0
    failed = []
    
    LOG_FILE = get_log_path()
    FAILED_FILE = get_failed_path()
    
    log_lines = [f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 开始全文提取，共 {total} 篇待处理"]
    
    for idx, row in enumerate(rows):
        pid = row['id']
        fpath = row['filepath']
        title = (row['title'] or os.path.basename(fpath))[:60]
        
        if not os.path.exists(fpath):
            failed.append({"id": pid, "file": fpath, "title": title, "error": "文件不存在"})
            continue
        
        fsize = os.path.getsize(fpath)
        if fsize < 1024:
            failed.append({"id": pid, "file": fpath, "title": title, "error": f"文件过小({fsize}B)"})
            continue
        
        text, error = extract_fulltext(fpath)
        
        if text:
            conn.execute("UPDATE papers SET full_text = ? WHERE id = ?", (text, pid))
            success += 1
            if (idx + 1) % 20 == 0:
                conn.commit()
                print(f"  进度: {idx+1}/{total}，成功: {success}，失败: {len(failed)}", flush=True)
        else:
            failed.append({"id": pid, "file": fpath, "title": title, "error": error})
    
    conn.commit()
    conn.close()
    
    log_lines.append(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 提取完成: 成功 {success}, 失败 {len(failed)}")
    
    # Write logs
    os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)
    with open(LOG_FILE, 'w') as f:
        f.write('\n'.join(log_lines) + '\n')
    
    with open(FAILED_FILE, 'w') as f:
        json.dump(failed, f, ensure_ascii=False, indent=2)
    
    print(f"\n=== 完成 ===")
    print(f"成功: {success}")
    print(f"失败: {len(failed)}")
    print(f"日志: {LOG_FILE}")
    print(f"失败列表: {FAILED_FILE}")

# Keep backward compatibility
def main():
    extract_all()

if __name__ == '__main__':
    main()


path: skill.json
content: {
  "name": "paper-management-system",
  "version": "2.0.0",
  "description": "文献管理系统 - 自动化PDF文献索引、搜索、AI提炼工具。当用户需要管理PDF文献、自动索引、搜索文献、提取元数据时激活。",
  "author": "crayfish-ai",
  "license": "MIT",
  "homepage": "https://github.com/crayfish-ai/paper-management-system",
  "issues": "https://github.com/crayfish-ai/paper-management-system/issues",
  "entry": "scripts/paper_manager.py",
  "config": {
    "PAPERMGR_PAPERS_DIR": {
      "type": "string",
      "description": "PDF 存储目录",
      "default": "./papers"
    },
    "PAPERMGR_DOWNLOADS_DIR": {
      "type": "string",
      "description": "下载目录（自动移动新文件）",
      "default": "./downloads"
    },
    "PAPERMGR_DATABASE_PATH": {
      "type": "string",
      "description": "SQLite 数据库路径",
      "default": "./data/index.db"
    },
    "PAPERMGR_AI_ENABLED": {
      "type": "boolean",
      "description": "是否启用 AI 摘要生成",
      "default": false
    },
    "OPENAI_API_KEY": {
      "type": "string",
      "description": "OpenAI API Key（用于 AI 摘要）",
      "sensitive": true
    }
  },
  "permissions": {
    "description": "本 skill 需要本地文件系统读写权限（PDF 存储、数据库、日志）以及网络访问（OpenAI API，可选）。",
    "filesystem": ["papers directory", "data directory", "downloads directory", "logs directory"],
    "network": ["api.openai.com（可选）"]
  },
  "capabilities": {
    "core": ["pdf_index", "pdf_rename", "search", "extract_fulltext"],
    "opt_in": ["ai_summarize", "feishu_notify", "cron_dispatch"]
  },
  "safety": {
    "no_system_modification": true,
    "no_root_required": true,
    "no_auto_migration": true,
    "no_env_injection": true
  },
  "documentation": {
    "readme": "README.md",
    "skill": "SKILL.md"
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)