Skill: yujing2013/mutual-fund-monthly-update (version 2.3.0)

--- name: mutual-fund-monthly-update description: 基金月报信息提取。支持单文件上传和批量处理文件夹。自动学习Excel模板，从PDF月报提取数据，生成两份Excel（PDF信息Excel + 用户模板Excel）。 version: 2.2.2 author: Yujing2013 tags: [基金, 月报, Excel, PDF, 模板, 批量处理] --- # 基金月报信息提取 **一句话说明**：上传Excel模板和PDF月报，AI自动提取数据并按模板格式生成新Excel。 --- ## 输入 | 类型 | 说明 | 必需 | |-----|------|------| | Excel模板 | 用户自定义的Excel格式 | 可选 | | PDF文件 | 基金月报PDF（支持文本/图表/扫描版） | 必需 | **两种上传方式：** 1. **逐个上传文件**：发送完所有文件后说"**好了**"或"**开始处理**" 2. **批量处理文件夹**：提供文件夹路径，AI自动扫描处理 --- ## 输出 **AI自动生成两份Excel：** | 文件 | 说明 | |-----|------| | **PDF信息Excel** | AI格式，不同基金分不同Sheet，同一基金不同时间在同一Sheet | | **用户模板Excel** | 按用户模板格式填充，保持原有样式和公式 | --- ## 产品功能 ✅ **智能提取**：文本+OCR双重提取，确保图表数据不遗漏 ✅ **模板学习**：自动学习用户Excel模板，精确填入数据 ✅ **批量处理**：支持文件夹批量处理，一次处理多个基金 ✅ **自动分类**：自动识别基金名称和日期，智能分类 ✅ **格式保持**：保持模板原有的数据类型、样式、公式 ✅ **模糊匹配**：字段名智能匹配，适应不同表述方式 **提取的数据：** - 核心指标：久期、YTM、基金规模 - 分布数据：行业分布、地区分布、信用评级分布 - 其他：十大持仓、派息记录、基金表现 --- ## 快速开始 ``` 方式1：上传文件 用户：[发送模板.xlsx] + [发送PDF文件] 用户：好了 AI：[生成两份Excel] 方式2：批量处理 用户：请处理这个文件夹：/path/to/folder/ AI：[扫描 → 提取 → 生成Excel] ``` --- ## 详细说明 - `references/` - 技术文档（模板学习、字段映射、OCR规则、批量处理、交互规则） - `SECURITY_REVIEW.md` - 安全评估报告

path: scripts/auto_update_final.py
content: #!/usr/bin/env python3
"""
互认基金月度数据自动更新脚本 - 完整版
支持ZIP打包上传
"""

import re
import json
import openpyxl
from pathlib import Path
import pdfplumber
import zipfile
import tempfile
import shutil


class FundUpdater:
    """基金数据更新器"""
    
    def __init__(self, skill_path):
        """初始化"""
        template_path = Path(skill_path) / 'references/extraction_templates.json'
        with open(template_path, 'r', encoding='utf-8') as f:
            self.templates = json.load(f)
        
        self.pdf_to_sheet = {
            '华夏': '华夏', '南方东英': '南方东英', '博时': '博时', '弘收': '弘收',
            '高腾': '高腾亚洲', '摩根亚洲债券': '摩根亚洲', '摩根国际': '摩根国际',
            '易方达': '易方达', '汇丰亚洲债券': '汇丰亚洲债券', '汇丰亚洲高收益': '汇丰亚洲高收益',
            '汇丰亚洲高入息': '汇丰亚洲高入息', '汇丰亚洲多元资产': '汇丰亚洲多元资产高入息',
            '海通': '海通亚洲', '东亚联丰亚洲债券': '东亚联丰亚洲债券及货币',
            '东亚联丰亚洲策略': '东亚联丰亚洲策略', '中银香港全天候中国': '中银香港全天候中国高息',
            '中银香港全天候亚洲': '中银香港全天候亚洲',
        }
    
    def process_zip(self, zip_path, output_dir):
        """处理ZIP文件"""
        # 创建临时目录
        temp_dir = tempfile.mkdtemp()
        
        try:
            # 解压ZIP
            with zipfile.ZipFile(zip_path, 'r') as zip_ref:
                zip_ref.extractall(temp_dir)
            
            # 查找Excel文件
            excel_files = [f for f in Path(temp_dir).rglob('*.xlsx') if not f.name.startswith('._')]
            if not excel_files:
                return None, "未找到Excel文件"
            
            excel_path = excel_files[0]
            
            # 查找PDF文件
            pdf_files = [f for f in Path(temp_dir).rglob('*.pdf') if not f.name.startswith('._')]
            if not pdf_files:
                return None, "未找到PDF文件"
            
            # 生成输出路径
            output_name = excel_path.stem + '_已更新.xlsx'
            output_path = Path(output_dir) / output_name
            
            # 执行更新
            count = self.update_excel(str(excel_path), str(temp_dir), str(output_path))
            
            return output_path, f"成功更新 {count} 个基金"
        
        finally:
            # 清理临时目录
            shutil.rmtree(temp_dir, ignore_errors=True)
    
    def update_excel(self, excel_path, pdf_dir, output_path):
        """更新Excel"""
        wb = openpyxl.load_workbook(excel_path)
        pdf_dir = Path(pdf_dir)
        
        # PDF内容到Sheet的映射
        pdf_content_to_sheet = [
            ('华夏精选固定收益', '华夏'),
            ('華夏精選固定收益', '华夏'),  # 繁体中文
            ('南方东英', '南方东英'),
            ('博時精選新興市場', '博时'),
            ('弘收', '弘收'),
            ('高騰亞洲收益', '高腾亚洲'),
            ('摩根亚洲债券', '摩根亚洲'),
            ('摩根国际', '摩根国际'),
            ('易方达', '易方达'),
            ('汇丰亚洲债券', '汇丰亚洲债券'),
            ('滙豐亞洲高收益', '汇丰亚洲高收益'),
            ('滙豐亞洲高入息', '汇丰亚洲高入息'),
            ('滙豐亞洲多元資產', '汇丰亚洲多元资产高入息'),
            ('海通亞洲', '海通亚洲'),
            ('东亚联丰亚洲债券', '东亚联丰亚洲债券及货币'),
            ('東亞聯豐亞洲策略', '东亚联丰亚洲策略'),
            ('中銀香港全天候中國', '中银香港全天候中国高息'),
            ('中銀香港全天候亞洲', '中银香港全天候亚洲'),
        ]
        
        seen = set()
        new_month_date = None  # 新的月度日期（YYYYMM格式）
        
        # 查找所有PDF文件（排除macOS隐藏文件）
        pdf_files = [f for f in pdf_dir.rglob('*.pdf') if not f.name.startswith('._')]
        
        for pdf_path in sorted(pdf_files):
            # 从PDF内容识别基金名称
            with pdfplumber.open(pdf_path) as pdf:
                text = pdf.pages[0].extract_text()
            
            # 匹配Sheet
            sheet_name = None
            for keyword, name in pdf_content_to_sheet:
                if keyword in text:
                    sheet_name = name
                    break
            
            if not sheet_name or sheet_name in seen or sheet_name not in wb.sheetnames:
                continue
            
            seen.add(sheet_name)
            ws = wb[sheet_name]
            
            # === 0. 更新日期字段 ===
            # 提取新的月度日期
            if not new_month_date:
                match = re.search(r'(\d{2})(\d{2})', pdf_path.name)
                if match:
                    new_month_date = f"{2000 + int(match.group(1))}{int(match.group(2)):02d}"
            
            if new_month_date:
                self._update_dates(ws, new_month_date)
            
            # === 1. 数据左移 ===
            self._left_shift_data(ws)
            
            # === 2. 提取新数据 ===
            date, duration, ytm = self._extract_from_pdf(pdf_path, sheet_name, text)
            
            # === 3. 填入新数据 ===
            if duration:
                ws.cell(row=12, column=6).value = duration
            
            if ytm:
                ws.cell(row=13, column=6).value = ytm
        
        wb.save(output_path)
        wb.close()
        
        return len(seen)
    
    def _update_dates(self, ws, new_month_date):
        """更新日期字段：上月日期=原本月日期，本月日期=新月度日期"""
        # 行7列3: 上月日期
        # 行8列3: 本月日期
        
        current_month_date = ws.cell(row=8, column=3).value
        
        if current_month_date:
            # 将原本月日期更新到上月日期
            ws.cell(row=7, column=3).value = current_month_date
        
        # 将新的月度日期更新到本月日期
        ws.cell(row=8, column=3).value = new_month_date
    
    def _left_shift_data(self, ws):
        """数据左移：列6→列4（只修改蓝色数据格，不修改绿色标题格）"""
        
        # 核心指标（行12-14）- 只移动数据，不修改标题
        for row in [12, 13, 14]:
            val = ws.cell(row=row, column=6).value
            if val:
                ws.cell(row=row, column=4).value = val
        
        # 行业分布（行18-31）- 移动名称和数据
        for row in range(18, 32):
            name = ws.cell(row=row, column=5).value
            val = ws.cell(row=row, column=6).value
            if name:
                ws.cell(row=row, column=3).value = name
            if val:
                ws.cell(row=row, column=4).value = val
        
        # 区域分布（行36-49）
        for row in range(36, 50):
            name = ws.cell(row=row, column=5).value
            val = ws.cell(row=row, column=6).value
            if name:
                ws.cell(row=row, column=3).value = name
            if val:
                ws.cell(row=row, column=4).value = val
        
        # 信用评级（行52-65）
        for row in range(52, 66):
            name = ws.cell(row=row, column=5).value
            val = ws.cell(row=row, column=6).value
            if name:
                ws.cell(row=row, column=3).value = name
            if val:
                ws.cell(row=row, column=4).value = val
    
    def _extract_from_pdf(self, pdf_path, sheet_name, text=None):
        """从PDF提取数据"""
        # 提取日期
        match = re.search(r'(\d{2})(\d{2})', pdf_path.name)
        date = f"{2000 + int(match.group(1))}{int(match.group(2)):02d}" if match else None
        
        # 匹配模板
        template = next((t for k, t in self.templates.items() if k in sheet_name or sheet_name in k), None)
        if not template:
            return date, None, None
        
        # 如果没有提供text，读取PDF
        if text is None:
            with pdfplumber.open(pdf_path) as pdf:
                text = pdf.pages[0].extract_text()
        
        # 提取久期
        duration = None
        duration_config = template.get('duration', {})
        if duration_config.get('pattern'):
            match = re.search(duration_config['pattern'], text)
            if match:
                duration = float(match.group(1))
        
        # 提取YTM
        ytm = None
        ytm_config = template.get('ytm', {})
        if ytm_config.get('pattern'):
            match = re.search(ytm_config['pattern'], text, re.DOTALL)
            if match:
                ytm = float(match.group(1)) / 100
        
        return date, duration, ytm


def main():
    """主函数"""
    import sys
    
    if len(sys.argv) < 3:
        print("用法: python auto_update_final.py <zip_path> <output_dir>")
        print("   或: python auto_update_final.py <excel_path> <pdf_dir> <output_path>")
        sys.exit(1)
    
    skill_path = Path(__file__).parent.parent
    updater = FundUpdater(skill_path)
    
    if len(sys.argv) == 3:
        # ZIP模式
        output_path, message = updater.process_zip(sys.argv[1], sys.argv[2])
        print(f"{message}")
        if output_path:
            print(f"文件: {output_path}")
    else:
        # 独立文件模式
        count = updater.update_excel(sys.argv[1], sys.argv[2], sys.argv[3])
        print(f"✅ 已更新 {count} 个基金")


if __name__ == '__main__':
    main()


path: scripts/update_fund_excel.py
content: #!/usr/bin/env python3
"""
互认基金月度Excel更新脚本
功能：解析PDF月报，更新Excel中对应基金Sheet的数据

依赖：
- openpyxl>=3.0.0
- pdfplumber>=0.7.0

请确保已安装依赖：pip install openpyxl pdfplumber
"""

import os
import sys
import re
import shutil
from pathlib import Path
from datetime import datetime

# 检查依赖，不自动安装
try:
    import openpyxl
    from openpyxl.utils import get_column_letter
except ImportError:
    print("❌ 缺少依赖：openpyxl")
    print("请运行：pip install openpyxl")
    sys.exit(1)

try:
    import pdfplumber
except ImportError:
    print("❌ 缺少依赖：pdfplumber")
    print("请运行：pip install pdfplumber")
    sys.exit(1)

# ==================== 基金匹配规则 ====================
FUND_MAPPING = {
    "华夏": "华夏",
    "南方东英": "南方东英",
    "易方达": "易方达",
    "汇丰亚洲高收益": "汇丰亚洲高收益",
    "汇丰亚洲高入息": "汇丰亚洲多元资产高入息",
    "汇丰亚洲多元资产": "汇丰亚洲多元资产高入息",
    "汇丰亚洲债券": "汇丰亚洲债券",
    "博时": "博时",
    "弘收": "弘收",
    "高腾": "高腾",
    "海通": "海通亚洲",
    "东亚联丰亚洲债券": "东亚联丰亚洲债券及货币",
    "东亚联丰亚洲策略": "东亚联丰亚洲策略",
    "摩根亚洲债券": "摩根亚洲",
    "摩根国际": "摩根国际",
    "中银香港全天候中国": "中银香港全天候中国高息",
    "中银香港全天候亚洲": "中银香港全天候亚洲",
}

# ==================== PDF数据提取器 ====================

def extract_huaxia_pdf(pdf_path, manual_data=None):
    """
    提取华夏基金PDF数据
    
    注意：PDF中的行业/区域分布以图表形式呈现，无法通过文本提取。
    需要手动提供图表数据或从图片中提取。
    """
    data = {
        "date": None,
        "duration": None,
        "ytm": None,
        "industry": {},
        "region": {},
        "credit": {},
    }
    
    with pdfplumber.open(pdf_path) as pdf:
        full_text = ""
        for page in pdf.pages:
            full_text += page.extract_text() or ""
        
        # 移除换行符，合并文本
        merged_text = full_text.replace('\n', ' ').replace('\r', ' ')
        # 移除多余空格
        merged_text = re.sub(r'\s+', ' ', merged_text)
        
        # 提取截至日期
        date_match = re.search(r'截至\s*(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日', merged_text)
        if date_match:
            year, month, day = date_match.groups()
            data["date"] = f"{year}.{int(month):02d}.{int(day):02d}"
        
        # 提取久期（PDF两列并排导致文字混合，直接匹配数字+Years）
        # 模式：数字.Years 或 数字 Yea (Years被截断)
        duration_match = re.search(r'([\d.]+)\s*Yea(?:rs?)?', merged_text, re.IGNORECASE)
        if duration_match:
            try:
                val = float(duration_match.group(1))
                # 验证范围（久期通常在0-30年）
                if 0 < val < 30:
                    data["duration"] = val
            except:
                pass
        
        # 提取到期收益率
        ytm_match = re.search(r'加權平均最差收益率\d*\s*([\d.]+)\s*%', merged_text)
        if ytm_match:
            data["ytm"] = float(ytm_match.group(1)) / 100
    
    # 如果提供了手动数据，合并
    if manual_data:
        if "industry" in manual_data:
            data["industry"] = manual_data["industry"]
        if "region" in manual_data:
            data["region"] = manual_data["region"]
        if "credit" in manual_data:
            data["credit"] = manual_data["credit"]
    
    return data


def parse_filename(filename):
    """解析Excel文件名，提取年月信息"""
    # 支持带UUID后缀的文件名
    # 格式：互认基金月度更新_YYYYMMvsYYYYMM---UUID.xlsx
    match = re.search(r'_(\d{6})vs(\d{6})(?:---[a-f0-9-]+)?\.xlsx$', filename)
    if match:
        last_last_month = match.group(1)
        last_month = match.group(2)
        year = int(last_month[:4])
        month = int(last_month[4:6])
        if month == 12:
            this_month = f"{year + 1}01"
        else:
            this_month = f"{year}{month + 1:02d}"
        return last_last_month, last_month, this_month
    return None, None, None


def match_pdf_to_sheet(pdf_filename):
    """根据PDF文件名匹配对应的Excel Sheet名"""
    pdf_name = Path(pdf_filename).stem
    
    for keyword, sheet_name in FUND_MAPPING.items():
        if keyword in pdf_name:
            return sheet_name
    return None


def update_sheet(ws, fund_data, last_month, this_month):
    """
    更新Sheet数据
    
    三原则：
    1. 名称 & 顺序完全沿用 Excel 原有格式
    2. 数值精准对应 PDF
    3. 本月没有的就空着
    """
    # 更新日期
    ws.cell(row=7, column=3).value = last_month
    ws.cell(row=8, column=3).value = this_month
    
    # 核心指标左移
    old_duration = ws.cell(row=12, column=6).value
    old_ytm = ws.cell(row=13, column=6).value
    
    if old_duration is not None:
        ws.cell(row=12, column=4).value = old_duration
    if old_ytm is not None:
        ws.cell(row=13, column=4).value = old_ytm
    
    # 填充新的本月数据
    if fund_data.get("duration") is not None:
        ws.cell(row=12, column=6).value = fund_data["duration"]
    if fund_data.get("ytm") is not None:
        ws.cell(row=13, column=6).value = fund_data["ytm"]
    
    # 行业分布：左移 + 按原有名称填充PDF数据
    for row_idx in range(18, 29):
        excel_name = ws.cell(row=row_idx, column=5).value
        
        # 左移
        ws.cell(row=row_idx, column=3).value = excel_name
        ws.cell(row=row_idx, column=4).value = ws.cell(row=row_idx, column=6).value
        
        # 原则2 & 3：PDF有数据就填，没有就空着
        if excel_name and excel_name in fund_data.get("industry", {}):
            ws.cell(row=row_idx, column=5).value = excel_name
            ws.cell(row=row_idx, column=6).value = fund_data["industry"][excel_name]
        elif excel_name:
            ws.cell(row=row_idx, column=5).value = excel_name
            ws.cell(row=row_idx, column=6).value = None
    
    # 区域分布：左移 + 按原有名称填充PDF数据
    for row_idx in range(32, 44):
        excel_name = ws.cell(row=row_idx, column=5).value
        
        if not excel_name:
            continue
        
        # 左移
        ws.cell(row=row_idx, column=3).value = excel_name
        ws.cell(row=row_idx, column=4).value = ws.cell(row=row_idx, column=6).value
        
        # 原则2 & 3
        if excel_name in fund_data.get("region", {}):
            ws.cell(row=row_idx, column=5).value = excel_name
            ws.cell(row=row_idx, column=6).value = fund_data["region"][excel_name]
        else:
            ws.cell(row=row_idx, column=5).value = excel_name
            ws.cell(row=row_idx, column=6).value = None
    
    return True


def main(excel_path, pdf_paths, manual_data_dict=None):
    """
    主函数
    
    Args:
        excel_path: Excel文件路径
        pdf_paths: PDF文件路径列表
        manual_data_dict: 手动数据字典 {sheet_name: data}
            用于提供PDF图表中无法提取的数据
    """
    print("=" * 60)
    print("互认基金月度Excel更新工具")
    print("=" * 60)
    
    # 解析文件名
    excel_name = Path(excel_path).name
    last_last_month, last_month, this_month = parse_filename(excel_name)
    
    if not last_month:
        print(f"❌ 无法解析Excel文件名: {excel_name}")
        return None
    
    print(f"\n📂 输入Excel: {excel_name}")
    print(f"   上上月: {last_last_month}")
    print(f"   上月: {last_month}")
    print(f"   本月: {this_month}")
    
    # 复制Excel到新文件
    output_name = f"互认基金月度更新_{last_month}vs{this_month}.xlsx"
    output_path = Path(excel_path).parent / output_name
    shutil.copy(excel_path, output_path)
    print(f"\n📄 输出Excel: {output_name}")
    
    # 打开工作簿
    wb = openpyxl.load_workbook(output_path)
    
    # 处理每个PDF
    matched_funds = []
    unmatched_pdfs = []
    
    for pdf_path in pdf_paths:
        pdf_name = Path(pdf_path).name
        sheet_name = match_pdf_to_sheet(pdf_name)
        
        if not sheet_name:
            unmatched_pdfs.append(pdf_name)
            print(f"\n⚠️ 未匹配: {pdf_name}")
            continue
        
        if sheet_name not in wb.sheetnames:
            unmatched_pdfs.append(f"{pdf_name} (Sheet不存在: {sheet_name})")
            print(f"\n⚠️ Sheet不存在: {sheet_name}")
            continue
        
        print(f"\n✅ 匹配成功: {pdf_name} → {sheet_name}")
        matched_funds.append(f"{Path(pdf_name).stem}→{sheet_name}")
        
        # 获取手动数据（如果有）
        manual_data = None
        if manual_data_dict and sheet_name in manual_data_dict:
            manual_data = manual_data_dict[sheet_name]
        
        # 提取PDF数据
        fund_data = extract_huaxia_pdf(pdf_path, manual_data)
        print(f"   提取数据:")
        print(f"     日期: {fund_data.get('date')}")
        print(f"     久期: {fund_data.get('duration')}")
        print(f"     YTM: {fund_data.get('ytm')}")
        print(f"     行业数: {len(fund_data.get('industry', {}))}")
        print(f"     区域数: {len(fund_data.get('region', {}))}")
        
        # 更新Sheet
        ws = wb[sheet_name]
        update_sheet(ws, fund_data, last_month, this_month)
        print(f"   ✅ Sheet已更新")
    
    # 保存工作簿
    wb.save(output_path)
    wb.close()
    
    # 生成报告
    print("\n" + "=" * 60)
    print("📊 更新报告")
    print("=" * 60)
    print(f"输入Excel文件名: {excel_name}")
    print(f"解析结果: 上月年月={last_month}, 本月年月={this_month}")
    print(f"输出Excel文件名: {output_name}")
    print(f"上传PDF数量: {len(pdf_paths)}个")
    print(f"匹配成功的基金: {len(matched_funds)}个")
    for i, fund in enumerate(matched_funds, 1):
        print(f"   {i}. {fund}")
    
    if unmatched_pdfs:
        print(f"未匹配的PDF: {len(unmatched_pdfs)}个")
        for pdf in unmatched_pdfs:
            print(f"   - {pdf}")
    
    print(f"\n✅ 完成！输出文件: {output_path}")
    
    return output_path


if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("用法: python update_fund_excel.py <excel_file> <pdf_file1> [pdf_file2] ...")
        print("\n注意: PDF中的行业/区域分布图表数据无法自动提取")
        print("需要在代码中手动提供 manual_data_dict 参数")
        sys.exit(1)
    
    excel_file = sys.argv[1]
    pdf_files = sys.argv[2:]
    
    main(excel_file, pdf_files)


path: references/batch_processing.md
content: # 批量处理文件夹逻辑

## 功能说明

当用户提供文件夹路径时，AI自动批量处理文件夹中的所有PDF文件。

---

## 触发条件

```
用户输入包含以下关键词：
- "文件夹路径"
- "批量处理"
- "处理这个文件夹"
- 文件路径（以/或盘符开头）

示例：
- "请处理这个文件夹：/Users/yujing/Documents/基金月报/"
- "批量处理：C:\\Users\\yujing\\基金月报\\"
- "/path/to/folder"
```

---

## 处理流程

### 步骤1：扫描文件夹

```python
import os
from pathlib import Path

def scan_folder(folder_path):
    """扫描文件夹，识别PDF和Excel文件"""
    
    folder = Path(folder_path)
    
    # 扫描PDF文件
    pdf_files = sorted(folder.glob("*.pdf"))
    
    # 扫描Excel文件（可能是模板）
    excel_files = sorted(folder.glob("*.xlsx"))
    
    return {
        "pdf_files": [str(f) for f in pdf_files],
        "excel_files": [str(f) for f in excel_files],
        "folder_name": folder.name
    }
```

### 步骤2：批量提取PDF数据

```python
def batch_extract_pdfs(pdf_files):
    """批量提取所有PDF数据"""
    
    all_data = {}  # {基金名: {日期: 数据}}
    
    for pdf_path in pdf_files:
        # 提取PDF数据（文本+OCR）
        data = extract_pdf_complete(pdf_path)
        
        # 识别基金名称和日期
        fund_name = identify_fund_name(data, pdf_path)
        date = identify_report_date(data, pdf_path)
        
        # 存储数据
        if fund_name not in all_data:
            all_data[fund_name] = {}
        
        all_data[fund_name][date] = data
    
    return all_data
```

### 步骤3：识别基金名称

```python
def identify_fund_name(data, pdf_path):
    """识别基金名称"""
    
    # 方法1：从PDF内容中提取
    if "基金名称" in data:
        return data["基金名称"]
    
    # 方法2：从文件名中提取
    filename = Path(pdf_path).stem
    
    # 常见格式：
    # 汇丰亚洲债券_202510.pdf
    # 汇丰亚洲债券-2025年10月.pdf
    # 汇丰亚洲债券.pdf
    
    # 移除日期部分
    import re
    name = re.sub(r'[_\-\s]*\d{4}[_\-\s年]*\d{1,2}[_\-\s月]*', '', filename)
    name = re.sub(r'[_\-\s]*\d{8}', '', name)
    
    return name.strip()
```

### 步骤4：识别报告日期

```python
def identify_report_date(data, pdf_path):
    """识别报告日期"""
    
    # 方法1：从PDF内容中提取
    if "报告日期" in data:
        return data["报告日期"]
    
    # 方法2：从文件名中提取
    filename = Path(pdf_path).stem
    
    import re
    
    # 格式1：202510
    match = re.search(r'(\d{4})(\d{2})', filename)
    if match:
        year, month = match.groups()
        return f"{year}{month}"
    
    # 格式2：2025年10月
    match = re.search(r'(\d{4})年(\d{1,2})月', filename)
    if match:
        year, month = match.groups()
        return f"{year}{month.zfill(2)}"
    
    # 格式3：2025-10
    match = re.search(r'(\d{4})-(\d{2})', filename)
    if match:
        year, month = match.groups()
        return f"{year}{month}"
    
    # 如果无法识别，使用文件修改时间
    import os
    mtime = os.path.getmtime(pdf_path)
    from datetime import datetime
    dt = datetime.fromtimestamp(mtime)
    return dt.strftime("%Y%m")
```

### 步骤5：生成PDF信息Excel（AI格式）

```python
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Border, Side, Alignment

def generate_pdf_info_excel(all_data, output_path):
    """生成PDF信息Excel（AI格式）"""
    
    wb = Workbook()
    
    # 删除默认Sheet
    wb.remove(wb.active)
    
    # 按基金名称排序
    sorted_funds = sorted(all_data.keys())
    
    for fund_name in sorted_funds:
        # 创建Sheet
        ws = wb.create_sheet(title=fund_name[:31])  # Sheet名最多31字符
        
        # 获取该基金的所有日期，按时间升序排序
        dates = sorted(all_data[fund_name].keys())
        
        # 写入表头
        ws['A1'] = '指标'
        for i, date in enumerate(dates, start=2):
            ws.cell(row=1, column=i, value=date)
        
        # 收集所有字段（合并所有日期的字段）
        all_fields = set()
        for date in dates:
            all_fields.update(all_data[fund_name][date].keys())
        
        # 写入数据
        row_idx = 2
        for field in sorted(all_fields):
            ws.cell(row=row_idx, column=1, value=field)
            
            for col_idx, date in enumerate(dates, start=2):
                value = all_data[fund_name][date].get(field, "")
                ws.cell(row=row_idx, column=col_idx, value=value)
            
            row_idx += 1
        
        # 应用样式
        apply_styles(ws, row_idx, len(dates) + 1)
    
    # 保存Excel
    wb.save(output_path)
    return output_path

def apply_styles(ws, row_count, col_count):
    """应用样式"""
    
    # 标题行样式
    header_fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
    header_font = Font(bold=True, color="FFFFFF")
    
    for col in range(1, col_count + 1):
        cell = ws.cell(row=1, column=col)
        cell.fill = header_fill
        cell.font = header_font
        cell.alignment = Alignment(horizontal="center", vertical="center")
    
    # 边框
    thin_border = Border(
        left=Side(style='thin'),
        right=Side(style='thin'),
        top=Side(style='thin'),
        bottom=Side(style='thin')
    )
    
    for row in range(1, row_count + 1):
        for col in range(1, col_count + 1):
            ws.cell(row=row, column=col).border = thin_border
    
    # 列宽
    ws.column_dimensions['A'].width = 20
    for col in range(2, col_count + 1):
        ws.column_dimensions[chr(64 + col)].width = 15
```

### 步骤6：检查并应用用户模板

```python
def check_and_apply_template(excel_files, all_data):
    """检查是否有用户模板，如果有则应用"""
    
    if not excel_files:
        return None
    
    # 识别模板文件（通常是名称包含"模板"或"template"的文件）
    template_file = None
    for excel_file in excel_files:
        if any(keyword in excel_file.lower() for keyword in ['模板', 'template', '更新']):
            template_file = excel_file
            break
    
    if not template_file:
        # 如果没有明显模板，使用第一个Excel文件
        template_file = excel_files[0]
    
    # 学习模板结构
    template_structure = learn_template(template_file)
    
    # 批量填充数据
    output_path = fill_template_with_batch_data(template_file, all_data)
    
    return output_path
```

---

## 数据结构

### all_data 结构

```python
all_data = {
    "汇丰亚洲债券": {
        "202510": {
            "基金名称": "汇丰亚洲债券基金",
            "报告日期": "202510",
            "久期": 5.89,
            "到期收益率": 0.0554,
            "基金规模": 28.93,
            "行业分布": {
                "金融": 0.25,
                "能源": 0.15,
                ...
            },
            "地区分布": {...},
            "信用评级分布": {...},
            "十大持仓": [...],
            ...
        },
        "202511": {...},
        "202512": {...}
    },
    "摩根亚洲债券": {
        "202511": {...},
        "202512": {...}
    },
    ...
}
```

---

## 输出文件

### 文件命名规则

```
PDF信息Excel：{文件夹名}_PDF信息提取_{时间戳}.xlsx
用户模板Excel：{文件夹名}_已填充_{时间戳}.xlsx

示例：
基金月报_PDF信息提取_20260313_163000.xlsx
基金月报_已填充_20260313_163000.xlsx
```

### 输出位置

```
默认输出到：
- 原文件夹路径下（推荐）
- 或 /root/.openclaw/media/outbound/（远程环境）
```

---

## 错误处理

### 情况1：文件夹路径不存在

```
AI回复：
❌ 文件夹路径不存在：{路径}

请检查路径是否正确。
```

### 情况2：文件夹中没有PDF文件

```
AI回复：
⚠️ 文件夹中没有找到PDF文件：{路径}

发现文件：
- {file1.xlsx}
- {file2.docx}
- ...

请确认文件夹中包含PDF文件。
```

### 情况3：部分PDF提取失败

```
AI回复：
⚠️ 部分PDF提取失败：

成功：
- 汇丰亚洲债券_202510.pdf ✓
- 汇丰亚洲债券_202511.pdf ✓

失败：
- 损坏的文件.pdf ✗（文件损坏）
- 加密的文件.pdf ✗（需要密码）

已生成可用数据的Excel。
```

---

## 性能优化

### 批量处理优化

```python
# 使用多进程加速（可选）
from multiprocessing import Pool

def batch_extract_pdfs_parallel(pdf_files, workers=4):
    """并行提取PDF数据"""
    
    with Pool(workers) as pool:
        results = pool.map(extract_pdf_complete, pdf_files)
    
    # 整理结果
    all_data = {}
    for pdf_path, data in zip(pdf_files, results):
        fund_name = identify_fund_name(data, pdf_path)
        date = identify_report_date(data, pdf_path)
        
        if fund_name not in all_data:
            all_data[fund_name] = {}
        
        all_data[fund_name][date] = data
    
    return all_data
```

### 进度反馈

```python
def batch_extract_with_progress(pdf_files):
    """带进度反馈的批量提取"""
    
    total = len(pdf_files)
    all_data = {}
    
    for i, pdf_path in enumerate(pdf_files, start=1):
        print(f"正在处理：{i}/{total} - {Path(pdf_path).name}")
        
        data = extract_pdf_complete(pdf_path)
        fund_name = identify_fund_name(data, pdf_path)
        date = identify_report_date(data, pdf_path)
        
        if fund_name not in all_data:
            all_data[fund_name] = {}
        
        all_data[fund_name][date] = data
    
    return all_data
```

---

## 示例对话

### 完整流程示例

```
用户：请处理这个文件夹：/Users/yujing/Documents/基金月报/

AI：正在扫描文件夹...

📁 文件夹：基金月报
├── PDF文件：15个
└── Excel文件：1个（可能是模板）

正在提取PDF数据（15个文件）...

✅ 提取完成：
- 汇丰亚洲债券：3个月（202510、202511、202512）
- 摩根亚洲债券：2个月（202511、202512）
- 中银香港全天候中国：2个月（202511、202512）
- 博时新兴市场：1个月（202512）
- 华夏海外收益：2个月（202511、202512）
- 南方多元配置：2个月（202511、202512）

正在生成Excel...

✅ PDF信息Excel已生成（AI格式）
  - Sheet 1: 汇丰亚洲债券（3个月数据）
  - Sheet 2: 摩根亚洲债券（2个月数据）
  - Sheet 3: 中银香港全天候中国（2个月数据）
  - Sheet 4: 博时新兴市场（1个月数据）
  - Sheet 5: 华夏海外收益（2个月数据）
  - Sheet 6: 南方多元配置（2个月数据）

✅ 用户模板Excel已生成（按模板格式填充）

[发送两个Excel文件]
```

---

## 技术依赖

### 额外依赖

```python
# 文件扫描
import os
from pathlib import Path

# 多进程（可选）
from multiprocessing import Pool

# 日期处理
from datetime import datetime
```

### 兼容性

```
- macOS: /Users/xxx/
- Linux: /home/xxx/
- Windows: C:\\Users\\xxx\\
- 相对路径: ./folder/
- 绝对路径: /full/path/to/folder/
```


path: references/field_mapping.md
content: # 字段映射规则

## 映射原理

AI通过字段名匹配，将模板中的字段名映射到PDF中的对应数据。

---

## 核心指标映射

| 模板字段 | PDF匹配字段（任一） | 数据类型 |
|---------|-------------------|---------|
| 久期 | 久期、组合久期、平均年期、Duration、有效久期、修正久期 | 数值 |
| YTM | 到期收益率、YTM、期满收益率、Yield to Maturity、平均到期收益率 | 百分比 |
| 基金规模 | 基金规模、资产净值、AUM、NAV、基金资产净值 | 数值（亿） |
| 平均评级 | 平均评级、平均信贷质量、Average Rating、信贷质量 | 文本 |
| 平均到期日 | 平均到期日、平均年期、Average Maturity | 数值（年） |
| 持仓数量 | 持仓数量、持仓数目、Number of Holdings | 数值 |
| 发行人数量 | 发行人数量、发行人数目、Number of Issuers | 数值 |

---

## 分布数据映射

### 行业分布

| 模板字段 | PDF匹配字段（任一） |
|---------|-------------------|
| 行业分布 | 行业分布、行业配置、Sector Distribution、Sector Allocation |
| 政府 | 政府、国债、Government、Sovereign |
| 银行 | 银行、Banking、Banks |
| 金融 | 金融、金融服务、Financial Services |
| 能源 | 能源、Energy |
| 通信 | 通信、Telecommunications、Telecom |
| 消费 | 消费、消费品、Consumer |
| 房地产 | 房地产、地产、Real Estate、Property |
| 保险 | 保险、Insurance |
| 公用事业 | 公用事业、公共事业、Utilities |
| 基础物料 | 基础物料、原材料、Basic Materials |
| 其他行业 | 其他行业、Other、Others |

### 地区分布

| 模板字段 | PDF匹配字段（任一） |
|---------|-------------------|
| 地区分布 | 地区分布、地区配置、市场分布、Geographic Distribution |
| 中国内地 | 中国内地、内地、中国大陆、China Mainland |
| 中国香港 | 中国香港、香港、Hong Kong、HK |
| 美国 | 美国、USA、United States |
| 日本 | 日本、Japan |
| 印尼 | 印尼、印度尼西亚、Indonesia |
| 印度 | 印度、India |
| 韩国 | 韩国、南韩、Korea、South Korea |
| 菲律宾 | 菲律宾、Philippines |
| 马来西亚 | 马来西亚、Malaysia |
| 澳大利亚 | 澳大利亚、澳洲、Australia |
| 新加坡 | 新加坡、Singapore |
| 其他地区 | 其他地区、Other Regions |

### 信用评级分布

| 模板字段 | PDF匹配字段（任一） |
|---------|-------------------|
| 信用评级 | 信用评级分布、评级分布、Credit Rating Distribution |
| AAA | AAA、Aaa |
| AA | AA、Aa |
| A | A |
| BBB | BBB、Baa |
| BB | BB、Ba |
| B | B |
| CCC | CCC、Caa |
| 无评级 | 无评级、未评级、Not Rated、NR、Unrated |

---

## 日期识别

### PDF日期格式识别

| PDF格式 | 示例 | 标准化结果 |
|--------|------|----------|
| YYYY年MM月DD日 | 2025年10月31日 | 202510 |
| YYYY.MM.DD | 2025.10.31 | 202510 |
| YYYY/MM/DD | 2025/10/31 | 202510 |
| YYYY-MM | 2025-10 | 202510 |
| MM/DD/YYYY | 10/31/2025 | 202510 |
| Oct 2025 | October 2025 | 202510 |
| 2025年10月 | 2025年10月 | 202510 |

### 模板日期格式适配

| 模板格式 | AI处理 |
|---------|--------|
| 10月 | 匹配月份数字 |
| 202510 | 匹配YYYYMM |
| 2025-10 | 匹配YYYY-MM |
| Oct-25 | 匹配英文月份 |

---

## 基金名称识别

### 识别规则

```
PDF中的基金名称：
- 汇丰亚洲债券基金 → 汇丰亚洲债券
- 摩根亚洲总收益债券基金 → 摩根亚洲总收益
- 博时新兴市场债券基金 → 博时新兴市场

标准化处理：
1. 提取基金名称
2. 去除"基金"、"Fund"等后缀
3. 作为Sheet名称
```

### 同产品判断

```
判断为同一产品的条件：
1. 基金名称相同（去除日期后）
2. 同一股份类别

示例：
- 汇丰亚洲债券基金 10月.pdf
- 汇丰亚洲债券基金 11月.pdf
→ 判断为同一产品，放在同一Sheet
```

---

## 数据格式转换

### 百分比转换

```
PDF数据 → 模板格式

情况1：PDF=5.54，模板=%
  → 输出：5.54%

情况2：PDF=5.54%，模板=小数
  → 输出：0.0554

情况3：PDF=554bp，模板=%
  → 输出：5.54%
```

### 金额转换

```
PDF数据 → 模板格式

情况1：PDF=2,893,380,420，模板=亿
  → 输出：28.93亿

情况2：PDF=28.93亿美元，模板=数字
  → 输出：28.93

情况3：PDF=$2.89B，模板=亿
  → 输出：28.93亿
```

---

## 模糊匹配算法

### 匹配流程

```python
def match_field(template_field, pdf_fields):
    # 1. 完全匹配
    if template_field in pdf_fields:
        return pdf_fields[template_field]
    
    # 2. 去空格/大小写匹配
    normalized = template_field.lower().replace(" ", "")
    for key in pdf_fields:
        if key.lower().replace(" ", "") == normalized:
            return pdf_fields[key]
    
    # 3. 包含匹配
    for key in pdf_fields:
        if template_field in key or key in template_field:
            return pdf_fields[key]
    
    # 4. 同义词匹配
    synonyms = get_synonyms(template_field)
    for key in pdf_fields:
        if any(syn in key for syn in synonyms):
            return pdf_fields[key]
    
    # 5. 未匹配
    return None
```

### 同义词表

```python
synonyms = {
    "久期": ["duration", "久期", "组合久期"],
    "YTM": ["ytm", "yield", "收益率", "到期收益率"],
    "基金规模": ["aum", "nav", "规模", "资产净值"],
    # ... 更多同义词
}
```

---

## 匹配结果标记

### 标记规则

| 匹配状态 | 标记方式 | 说明 |
|---------|---------|------|
| 完全匹配 | 正常显示 | 无需标注 |
| 模糊匹配 | 黄色高亮 | 用户需确认 |
| 未匹配 | 红色高亮 + "N/A" | 用户需手动填充 |
| 多个候选 | 橙色高亮 | AI选择了最佳匹配 |

### 输出说明

```
AI生成Excel后，附带说明：
- 完全匹配：12个字段
- 模糊匹配：3个字段（已黄色标记）
- 未匹配：2个字段（已红色标记，需手动填充）
```


path: references/interaction_rules.md
content: # 交互规则

## 问题场景

用户在聊天框中一个一个发送文件，AI如何知道用户是否发完了？

---

## 解决方案

### 方案1：用户明确确认（推荐）

**规则：用户发送完所有文件后，说"好了"或"开始处理"**

```
用户：[发送模板.xlsx]
AI：收到：模板.xlsx（等待更多文件...）

用户：[发送汇丰10月.pdf]
AI：收到：汇丰10月.pdf（等待更多文件...）

用户：[发送汇丰11月.pdf]
AI：收到：汇丰11月.pdf（等待更多文件...）

用户：[发送汇丰12月.pdf]
AI：收到：汇丰12月.pdf（等待更多文件...）

用户：好了
AI：收到4个文件，开始处理...
    [提取数据 → 生成Excel]
```

**触发词：**
- "好了"
- "开始处理"
- "处理"
- "可以开始了"
- "文件发完了"

---

### 方案2：超时自动开始（备选）

**规则：如果用户超过30秒没有发送新文件，自动开始处理**

```
用户：[发送模板.xlsx]
AI：收到：模板.xlsx（等待更多文件...）

用户：[发送汇丰10月.pdf]
AI：收到：汇丰10月.pdf（等待更多文件...）

... 用户超过30秒没有发送新文件 ...

AI：未收到新文件，开始处理...
    [提取数据 → 生成Excel]
```

**超时设置：**
- 默认超时：30秒
- 可配置超时：60秒、90秒

---

### 方案3：AI主动询问（兜底）

**规则：如果AI不确定用户是否发完，主动询问**

```
用户：[发送模板.xlsx]
AI：收到：模板.xlsx

用户：[发送汇丰10月.pdf]
AI：收到：汇丰10月.pdf

... 等待10秒 ...

AI：您还有其他文件要发送吗？如果没有，请说"好了"，我开始处理。
```

---

## AI执行逻辑

### 判断规则

```python
def should_start_processing(user_message, time_since_last_file):
    """判断是否应该开始处理"""
    
    # 规则1：用户明确确认
    if user_message in ["好了", "开始处理", "处理", "可以开始了", "文件发完了"]:
        return True
    
    # 规则2：超时自动开始
    if time_since_last_file > 30:  # 30秒超时
        return True
    
    # 规则3：用户发送了非文件消息（可能是确认）
    if not is_file_message(user_message):
        return True
    
    # 否则，继续等待
    return False


def process_incoming_files(files_received, user_message):
    """处理接收到的文件"""
    
    # 添加文件到列表
    files_received.append(user_message)
    
    # 回复确认
    filename = get_filename(user_message)
    reply(f"收到：{filename}（等待更多文件...）")
    
    # 不立即处理，等待用户确认或超时
    return files_received
```

---

## 实现细节

### 文件接收流程

```
步骤1：用户发送文件
    ↓
步骤2：AI识别文件类型（PDF/Excel）
    ↓
步骤3：AI回复确认收到
    ↓
步骤4：AI等待
    ├─ 用户发送更多文件 → 回到步骤1
    ├─ 用户说"好了" → 开始处理
    ├─ 超过30秒 → 开始处理
    └─ 用户发送其他消息 → 询问确认
```

### 状态管理

```python
# 会话状态
session_state = {
    "files_received": [],  # 已接收的文件列表
    "last_file_time": None,  # 最后一个文件的时间
    "is_waiting": True,  # 是否在等待更多文件
    "user_confirmed": False  # 用户是否确认
}
```

---

## 用户体验优化

### 清晰的提示

**文件接收时：**
```
AI：✅ 收到：汇丰10月.pdf
    📁 已接收：1个文件
    💡 发送更多文件，或说"好了"开始处理
```

**用户确认时：**
```
AI：收到确认！
    📁 总计：4个文件
    🚀 开始处理...
```

**处理完成时：**
```
AI：✅ 处理完成！
    📊 生成Excel：2个文件
    [发送Excel文件]
```

---

## 错误处理

### 情况1：用户忘记确认

```
用户：[发送文件1]
用户：[发送文件2]
... 用户忘记说"好了" ...

AI：您还有其他文件要发送吗？
    如果没有，请说"好了"，我开始处理。
```

### 情况2：用户中途离开

```
用户：[发送文件1]
... 用户离开很长时间 ...

AI：超过30秒未收到新文件，开始处理...
    📁 已接收：1个文件
```

### 情况3：用户误操作

```
用户：[发送文件1]
用户：[发送文件2]
用户：算了，重新发
AI：好的，已清空文件列表。
    请重新发送文件。
```

---

## 推荐流程（最佳实践）

```
用户操作：
1. 发送Excel模板（如果有）
2. 发送PDF文件（一个或多个）
3. 说"好了"或"开始处理"

AI响应：
1. 收到每个文件时，回复"收到：xxx（等待更多文件...）"
2. 收到确认后，回复"收到确认！总计：N个文件，开始处理..."
3. 处理完成后，发送生成的Excel文件
```

---

## 代码示例

```python
class FileProcessor:
    def __init__(self):
        self.files = []
        self.last_file_time = None
        self.timeout = 30  # 秒
    
    def add_file(self, file_path):
        """添加文件"""
        self.files.append(file_path)
        self.last_file_time = time.time()
        return f"✅ 收到：{os.path.basename(file_path)}\n📁 已接收：{len(self.files)}个文件\n💡 发送更多文件，或说"好了"开始处理"
    
    def should_start(self, user_message):
        """判断是否应该开始处理"""
        # 用户确认
        if user_message.strip() in ["好了", "开始处理", "处理"]:
            return True
        
        # 超时
        if self.last_file_time and time.time() - self.last_file_time > self.timeout:
            return True
        
        return False
    
    def process(self):
        """开始处理"""
        result = f"收到确认！\n📁 总计：{len(self.files)}个文件\n🚀 开始处理..."
        
        # 提取数据、生成Excel
        # ...
        
        return result
```

---

## 总结

**推荐方案：用户明确确认 + 超时自动开始**

1. **用户明确确认**：发送完所有文件后，说"好了"或"开始处理"
2. **超时自动开始**：如果用户超过30秒没有发送新文件，自动开始处理
3. **AI主动询问**：如果AI不确定，主动询问用户是否发完了

**避免的问题：**
- ✅ 不会在中途就开始处理
- ✅ 不会遗漏文件
- ✅ 用户体验清晰明确


path: references/extraction_templates.json
content: {
  "version": "2.2.2",
  "description": "基金PDF数据提取模板配置",
  "funds": {
    "华夏": {
      "pdf_patterns": ["华夏", "华夏基金"],
      "sheet_name": "华夏"
    },
    "南方东英": {
      "pdf_patterns": ["南方东英", "南方"],
      "sheet_name": "南方东英"
    },
    "易方达": {
      "pdf_patterns": ["易方达"],
      "sheet_name": "易方达"
    },
    "汇丰亚洲债券": {
      "pdf_patterns": ["汇丰亚洲债券", "汇丰亚洲高收益债券"],
      "sheet_name": "汇丰亚洲债券"
    },
    "汇丰亚洲高收益": {
      "pdf_patterns": ["汇丰亚洲高收益"],
      "sheet_name": "汇丰亚洲高收益"
    },
    "汇丰亚洲多元资产高入息": {
      "pdf_patterns": ["汇丰亚洲高入息", "汇丰亚洲多元资产"],
      "sheet_name": "汇丰亚洲多元资产高入息"
    },
    "博时": {
      "pdf_patterns": ["博时"],
      "sheet_name": "博时"
    },
    "弘收": {
      "pdf_patterns": ["弘收"],
      "sheet_name": "弘收"
    },
    "高腾": {
      "pdf_patterns": ["高腾"],
      "sheet_name": "高腾亚洲"
    },
    "海通亚洲": {
      "pdf_patterns": ["海通"],
      "sheet_name": "海通亚洲"
    },
    "东亚联丰亚洲债券及货币": {
      "pdf_patterns": ["东亚联丰亚洲债券"],
      "sheet_name": "东亚联丰亚洲债券及货币"
    },
    "东亚联丰亚洲策略": {
      "pdf_patterns": ["东亚联丰亚洲策略"],
      "sheet_name": "东亚联丰亚洲策略"
    },
    "摩根亚洲": {
      "pdf_patterns": ["摩根亚洲债券"],
      "sheet_name": "摩根亚洲"
    },
    "摩根国际": {
      "pdf_patterns": ["摩根国际"],
      "sheet_name": "摩根国际"
    },
    "中银香港全天候中国高息": {
      "pdf_patterns": ["中银香港全天候中国"],
      "sheet_name": "中银香港全天候中国高息"
    },
    "中银香港全天候亚洲": {
      "pdf_patterns": ["中银香港全天候亚洲"],
      "sheet_name": "中银香港全天候亚洲"
    }
  },
  "extraction_fields": {
    "core_metrics": {
      "久期": ["久期", "组合久期", "平均存续期", "Duration"],
      "到期收益率": ["到期收益率", "YTM", "平均最低孳息率", "Yield"],
      "基金规模": ["基金规模", "资产净值", "AUM", "总资产值"]
    },
    "distribution": {
      "行业分布": ["行业分布", "行业配置", "Sector"],
      "地区分布": ["地区分布", "地区配置", "区域分布", "Geographic"],
      "信用评级分布": ["信用评级分布", "评级分布", "信贷评级"]
    }
  }
}


path: references/ocr_rules.md
content: # OCR识别规则

## ⚠️ 核心原则

**OCR是必需步骤，不是可选功能！**

### 为什么必须使用OCR？

```
原因1：图表数据无法通过文本提取
- 行业分布、地区分布、信用评级分布通常以图表形式存在
- pdfplumber只能提取文本和表格，无法识别图表中的数据
- 不使用OCR会导致关键数据遗漏

原因2：避免信息丢失
- 文本提取可能遗漏图片中的关键信息
- 双重提取策略（文本+OCR）确保数据完整
- 宁可多识别，不可漏数据

原因3：用户期望
- 用户期望AI能提取所有可见数据
- 漏掉分布数据会导致用户需要手动补充
- 违背"自动化"的初衷
```

---

## 双重提取策略（强制执行）

### 流程图

```
PDF输入
    ↓
    ├─→ 方法A：文本提取（pdfplumber）
    │   - 提取文本内容
    │   - 提取表格数据
    │   - 识别字段名
    │
    ├─→ 方法B：OCR识别（pytesseract）★ 必须执行
    │   - PDF转图片（300 DPI）
    │   - 全页OCR识别
    │   - 提取图表数据
    │
    ↓
合并结果
    - 文本提取 + OCR识别 = 完整数据
    - 对比验证
    - 去重合并
    ↓
PDF信息Excel
```

### Python实现

```python
def extract_pdf_complete(pdf_path):
    """完整提取PDF数据（双重提取）"""
    
    # 方法A：文本提取
    text_data = extract_text(pdfplumber.open(pdf_path))
    
    # 方法B：OCR识别（必须执行！）
    images = convert_from_path(pdf_path, dpi=300)
    ocr_data = []
    for img in images:
        text = pytesseract.image_to_string(img, lang='chi_sim+eng')
        ocr_data.append(text)
    
    # 合并结果
    complete_data = merge_results(text_data, ocr_data)
    
    return complete_data
```

---

## OCR流程（详细步骤）

### 步骤1：PDF转图片

```python
from pdf2image import convert_from_path

# 高分辨率转换（300 DPI）
images = convert_from_path(
    pdf_path,
    dpi=300,  # 高分辨率，确保图表清晰
    fmt='png',
    first_page=1,
    last_page=None  # 转换所有页
)
```

### 步骤2：OCR识别

```python
import pytesseract

# 配置
pytesseract.pytesseract.tesseract_cmd = '/usr/bin/tesseract'
OCR_LANG = 'chi_sim+eng'  # 中文简体 + 英文
OCR_CONFIG = '--oem 3 --psm 6'  # 优化配置

# 识别每一页
for page_num, img in enumerate(images):
    # 全页OCR
    text = pytesseract.image_to_string(img, lang=OCR_LANG, config=OCR_CONFIG)
    
    # 处理识别结果
    lines = text.split('\n')
    for line in lines:
        # 查找关键数据
        if any(keyword in line for keyword in ['行業', '行业', '分布', '區域', '区域', '評級', '评级']):
            extract_distribution_data(line)
```

### 步骤3：数据提取

```python
import re

def extract_distribution_data(text):
    """从OCR文本中提取分布数据"""
    
    # 行业分布示例：周期性消费品 24.4%
    industry_pattern = r'(周期性消费品|金融|房地产|公用事业|工业|能源|政府|基本原料|通讯|其他|现金|非周期性消费品)\s+(\d+\.?\d*)\%'
    
    # 地区分布示例：中国内地 45.2%
    region_pattern = r'(中国内地|中国香港|美国|新加坡|英国|其他)\s+(\d+\.?\d*)\%'
    
    # 信用评级分布示例：AAA 10.2%
    rating_pattern = r'(AAA|AA\+|AA|AA-|A\+|A|A-|BBB\+|BBB|BBB-|BB|B|NR)\s+(\d+\.?\d*)\%'
    
    # 提取数据
    for pattern in [industry_pattern, region_pattern, rating_pattern]:
        matches = re.findall(pattern, text)
        for match in matches:
            name, value = match
            # 存储数据
            save_data(name, float(value) / 100)  # 转换为小数
```

---

## 重点识别对象

### 必须识别的数据

```
1. 行业分布（Industry Distribution）
   - 基本原料、通讯、周期性消费品、非周期性消费品
   - 能源、房地产、金融、工业、科技、公用事业
   - 政府、现金、其他

2. 地区分布（Geographic Distribution）
   - 中国内地、中国香港、中国澳门
   - 美国、新加坡、英国、印尼、韩国
   - 其他亚太、其他欧洲、其他地区

3. 信用评级分布（Credit Rating Distribution）
   - AAA、AA+、AA、AA-、A+、A、A-
   - BBB+、BBB、BBB-、BB、B、NR

4. 核心指标
   - 久期、组合久期、平均存续期
   - 到期收益率、YTM、孳息率
   - 基金规模、资产净值、AUM
```

---

## OCR准确度优化

### 提高识别准确度的方法

```python
# 方法1：高分辨率（300 DPI以上）
images = convert_from_path(pdf_path, dpi=300)

# 方法2：中英文混合识别
text = pytesseract.image_to_string(img, lang='chi_sim+eng')

# 方法3：图像预处理
import cv2

def preprocess_image(img):
    # 转灰度
    gray = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2GRAY)
    
    # 二值化（增强对比度）
    _, binary = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    
    # 去噪
    denoised = cv2.medianBlur(binary, 3)
    
    return denoised

# 方法4：对比验证
# OCR提取的数据，与文本提取的数据对比，确保一致性
```

---

## 数据校验

### 识别后必须校验

```python
def validate_ocr_data(field_name, value, all_values):
    """校验OCR识别的数据"""
    
    # 1. 范围校验
    if '久期' in field_name:
        if not (0 < value < 20):
            return False, "久期值异常"
    
    if '收益率' in field_name or 'YTM' in field_name:
        if not (0 < value < 1):  # 百分比形式（0.067 = 6.7%）
            return False, "收益率值异常"
    
    # 2. 分布数据总和校验
    if '分布' in field_name:
        total = sum(all_values)
        if not (0.95 < total < 1.05):  # 允许5%误差
            return False, f"分布数据总和异常：{total:.2%}"
    
    # 3. 与文本提取对比
    text_value = get_text_extracted_value(field_name)
    if text_value and abs(value - text_value) > 0.01:
        return False, f"OCR与文本提取不一致：OCR={value}, 文本={text_value}"
    
    return True, "校验通过"
```

---

## 特殊情况处理

### 情况1：饼图/环形图

```
问题：饼图的角度难以精确识别

处理策略：
1. 尝试OCR识别图例文字和百分比
2. 如果图例清晰，提取数据
3. 如果图例模糊：
   - 在Excel中标记"需手动输入"
   - 附注说明：饼图数据，OCR识别准确度低
```

### 情况2：复杂图表

```
问题：三维图表、渐变色图表难以识别

处理策略：
1. 尝试OCR识别
2. 如果识别失败：
   - 在Excel中预留空白单元格
   - 标记"图表复杂，需手动输入"
3. 提供图表截图位置，方便用户手动查看
```

### 情况3：表格图片

```
问题：表格以图片形式存在，pdfplumber无法提取

处理策略：
1. OCR识别整页
2. 从OCR文本中重构表格
3. 使用正则表达式提取行列数据
```

---

## 输出格式

### PDF信息Excel中标注数据来源

```
| 字段 | 值 | 数据来源 |
|------|-----|---------|
| 组合久期 | 2.3年 | 文本提取 |
| 平均最低孳息率 | 6.7% | 文本提取 |
| 周期性消费品 | 24.4% | OCR识别 |
| 金融 | 20.1% | OCR识别 |
| 房地产 | 12.9% | OCR识别 |
```

### 生成说明

```
AI生成Excel后，附带说明：

📊 数据提取报告：
- 文本提取字段：10个（准确度：高）
- OCR识别字段：15个（准确度：中）
- 需手动输入：0个

OCR识别详情：
- 行业分布：12项（来源：第1页图表）
- 地区分布：8项（来源：第1页图表）
- 信用评级分布：10项（来源：第1页图表）

建议：
- OCR识别准确度中等，建议对照PDF验证
- 所有分布数据已提取，无需手动补充
```

---

## 系统依赖（必须安装）

### Python包

```bash
pip install pdfplumber openpyxl pdf2image pytesseract Pillow
```

### 系统依赖

```bash
# Ubuntu/Debian
sudo apt-get install tesseract-ocr tesseract-ocr-chi-sim poppler-utils

# macOS
brew install tesseract tesseract-lang poppler

# Windows
# 1. 下载Tesseract：https://github.com/UB-Mannheim/tesseract/wiki
# 2. 下载Poppler：https://blog.alivate.com.au/poppler-windows/
# 3. 添加到系统PATH
```

### 检查安装

```python
import pytesseract
import pdf2image

# 检查Tesseract
try:
    version = pytesseract.get_tesseract_version()
    print(f"✅ Tesseract已安装：{version}")
except:
    print("❌ Tesseract未安装")

# 检查中文语言包
langs = pytesseract.get_languages()
if 'chi_sim' in langs:
    print("✅ 中文语言包已安装")
else:
    print("❌ 中文语言包未安装")
```

---

## 错误处理

### OCR识别失败

```
如果OCR完全失败：
1. 检查Tesseract是否安装
2. 检查中文语言包是否安装
3. 检查Poppler是否安装
4. 在Excel中标记所有分布字段为"OCR失败，需手动输入"
5. 向用户报告错误原因
```

### 数据不一致

```
如果文本提取与OCR识别结果不一致：
1. 优先使用文本提取结果（准确度更高）
2. 在Excel中用黄色标记不一致的字段
3. 附注说明：文本提取=X，OCR识别=Y
4. 让用户选择正确值
```

---

## 总结

**OCR识别三原则：**

1. **必须执行**：每次处理PDF都必须进行OCR识别
2. **双重提取**：文本提取 + OCR识别 = 完整数据
3. **宁多勿少**：宁可多识别一些数据，不可漏掉关键信息

**记住：漏掉分布数据 = 用户体验差 = 失败的自动化**


path: references/template_learning.md
content: # 模板学习逻辑

## 学习目标

精确学习用户Excel模板，确保生成的Excel与模板结构完全一致。

---

## 学习流程

### 第一步：结构扫描

```python
# 扫描模板所有单元格
for sheet in workbook:
    for row in sheet.rows:
        for cell in row:
            记录：
            - 单元格坐标 (A1, B3, ...)
            - 单元格值
            - 单元格样式
            - 是否合并单元格
```

### 第二步：字段识别

**识别哪些单元格需要填充数据：**

| 单元格类型 | 特征 | 处理方式 |
|-----------|------|---------|
| 行标题 | 文本，在第一列 | 作为字段名，建立映射 |
| 列标题 | 文本，在第一行 | 作为日期标识 |
| 数据单元格 | 空值或数值 | 需要填充数据 |
| 固定文本 | 非空文本（非标题） | 保持不变 |
| 公式单元格 | 包含公式 | 保持公式不变 |

**示例：**

```
模板：
| A        | B      | C      | D      |
| 指标     | 10月   | 11月   | 12月   |  ← 列标题
| 久期     |        |        |        |  ← 行标题，B/C/D空，需填充
| YTM      | 5.54%  |        |        |  ← B有示例值，C/D需填充
| 基金名称 | 汇丰   | 汇丰   | 汇丰   |  ← 固定文本，保持不变

AI学习结果：
- 行标题：久期、YTM（需填充）
- 列标题：10月、11月、12月（日期）
- 数据区域：B2:D3（需填充）
- 固定文本：基金名称行（保持不变）
```

### 第三步：映射建立

**建立字段名→PDF数据的映射关系：**

```python
映射表 = {
    "久期": {
        "PDF字段": ["久期", "组合久期", "Duration", "平均年期"],
        "位置": "B2",
        "格式": "数值，保留2位小数"
    },
    "YTM": {
        "PDF字段": ["到期收益率", "YTM", "期满收益率", "Yield"],
        "位置": "B3",
        "格式": "百分比"
    },
    "行业分布": {
        "PDF字段": ["行业分布", "行业配置", "Sector Distribution"],
        "位置": "B10:B20",  # 多行数据
        "格式": "百分比"
    }
}
```

### 第四步：样式提取

**提取并记录所有样式信息：**

```python
样式记录 = {
    "B2": {
        "font": {
            "name": "微软雅黑",
            "size": 11,
            "bold": False,
            "color": "000000"
        },
        "fill": {
            "patternType": "solid",
            "fgColor": "FFFFFF"
        },
        "border": {
            "left": {"style": "thin"},
            "right": {"style": "thin"},
            "top": {"style": "thin"},
            "bottom": {"style": "thin"}
        },
        "alignment": {
            "horizontal": "center",
            "vertical": "center"
        },
        "number_format": "0.00"
    }
}
```

### 第五步：合并单元格识别

**识别并记录合并单元格范围：**

```python
合并区域记录 = [
    "A1:D1",  # 标题行
    "A10:A15", # 行业分布标题
    "A16:A20"  # 地区分布标题
]
```

---

## 模板类型识别

### 类型1：横向时间对比

```
| 指标 | 10月 | 11月 | 12月 |
| 久期 |      |      |      |
```

**特点**：日期在列标题，数据按月横向展开

### 类型2：纵向时间对比

```
| 日期   | 久期 | YTM  |
| 10月   |      |      |
| 11月   |      |      |
```

**特点**：日期在行标题，数据按月纵向展开

### 类型3：多Sheet对比

```
Sheet1: 汇丰亚洲债券
Sheet2: 摩根亚洲债券
```

**特点**：每个Sheet一个产品，Sheet内按月对比

---

## 智能推断规则

### 规则1：空单元格推断

```
模板：
| 指标 | 10月 | 11月 | 12月 |
| 久期 | 5.89 |      |      |

AI推断：
- B列有示例值 5.89
- C、D列为空，需要填充
- 推断格式：数值，保留2位小数
```

### 规则2：数据格式推断

```
模板示例值：5.54%
AI推断：
- 格式为百分比
- PDF提取的 5.54 转换为 5.54%
```

### 规则3：扩展列推断

```
模板有3列（10月、11月、12月）
用户发送4个PDF（10月、11月、12月、1月）

AI处理：
- 识别模板只有3列
- 自动扩展第4列（1月）
- 复制前一列的样式
```

---

## 错误处理

### 情况1：字段无法映射

```
模板字段："组合久期"
PDF中找不到匹配字段

AI处理：
1. 尝试模糊匹配（久期、Duration）
2. 如果仍无法匹配，标记为"未找到"
3. 在输出Excel中用红色高亮显示
```

### 情况2：PDF数据缺失

```
模板要求：信用评级分布（AAA/AA/A/BBB...）
PDF中只有：评级分布（无详细分类）

AI处理：
1. 提取可用的评级数据
2. 无法填充的单元格标记为"N/A"
3. 在输出说明中注明
```

### 情况3：格式冲突

```
模板要求：百分比格式（5.54%）
PDF数据：小数格式（0.0554）

AI处理：
- 自动转换为模板要求的格式
- 0.0554 → 5.54%
```

---

## 字段模糊匹配规则（v2.1新增）

### 匹配策略

**三层匹配机制：**

1. **精确匹配**（优先级最高）
   - 模板字段名完全等于PDF字段名
   - 示例：模板"久期" = PDF"久期"

2. **别名匹配**（优先级中等）
   - 使用预定义的字段别名表
   - 示例：模板"组合久期" → PDF["久期", "Duration", "平均年期"]

3. **模糊匹配**（优先级最低）
   - 关键词匹配（包含关系）
   - 语义相似度匹配（基于词向量）
   - 示例：模板"到期收益率" → PDF"平均最低孳息率"（含"收益率"）

### 字段别名表

```python
FIELD_ALIASES = {
    "久期": ["久期", "组合久期", "平均存续期", "Duration", "平均年期", "存续期"],
    "到期收益率": ["到期收益率", "YTM", "期满收益率", "Yield", "平均最低孳息率", "孳息率"],
    "基金规模": ["基金规模", "资产净值", "AUM", "总资产值", "规模"],
    "行业分布": ["行业分布", "行业配置", "Sector", "行业"],
    "地区分布": ["地区分布", "地区配置", "区域分布", "Geographic", "地区"],
    "信用评级": ["信用评级分布", "评级分布", "信贷评级", "Credit Rating", "评级"],
    "基本原料": ["基本原料", "基础材料", "Materials"],
    "通讯": ["通讯", "通信", "Telecom", "Telecommunications"],
    "周期性消费品": ["周期性消费品", "可选消费", "Consumer Discretionary"],
    "非周期性消费品": ["非周期性消费品", "必需消费", "Consumer Staples"],
    "能源": ["能源", "Energy"],
    "房地产": ["房地产", "地产", "Real Estate"],
    "金融": ["金融", "Financials"],
    "工业": ["工业", "Industrials"],
    "科技": ["科技", "Technology"],
    "公用事业": ["公用事业", "公用", "Utilities"],
    "政府": ["政府", "Government", "国债"],
    "现金": ["现金", "现金等值", "Cash"],
}
```

### 匹配示例

```
场景1：模板"组合久期" → PDF"平均存续期：2.3年"
匹配过程：
1. 精确匹配：失败
2. 别名匹配："组合久期" 在 FIELD_ALIASES["久期"] 中
3. 在PDF中搜索"久期"相关字段，找到"平均存续期"
4. 提取数据：2.3

场景2：模板"组合到期收益率" → PDF"平均最低孳息率：6.7%"
匹配过程：
1. 精确匹配：失败
2. 别名匹配："组合到期收益率" → FIELD_ALIASES["到期收益率"]
3. 在PDF中搜索"收益率"、"孳息率"、"YTM"
4. 找到"平均最低孳息率：6.7%"
5. 提取数据：6.7%

场景3：模板"金融" → PDF"金融 20.1%"
匹配过程：
1. 精确匹配：在行业分布图表中找到"金融 20.1%"
2. 提取数据：0.201（转换为小数）
```

---

## 数据类型保持规则（v2.1新增）

### 核心原则

**不要改变模板中每个格子的数据类型！**

### 数据类型识别

```python
def get_cell_data_type(cell):
    """识别单元格的数据类型"""
    
    # 1. 检查数字格式
    if cell.number_format:
        if '%' in cell.number_format:
            return 'percentage'  # 百分比
        elif cell.number_format.startswith('0.00'):
            return 'decimal'  # 小数
        elif cell.number_format.startswith('0'):
            return 'integer'  # 整数
    
    # 2. 检查值类型
    if isinstance(cell.value, (int, float)):
        return 'number'  # 数字
    elif isinstance(cell.value, str):
        if cell.value.endswith('%'):
            return 'percentage_text'  # 百分比文本
        else:
            return 'text'  # 文本
    
    # 3. 默认
    return 'unknown'
```

### 数据类型转换规则

| 模板类型 | PDF数据 | 转换规则 | 示例 |
|---------|---------|---------|------|
| 百分比格 | 6.7 | `value / 100` → `0.067` → 单元格显示 `6.7%` | 6.7 → 0.067 |
| 百分比格 | 6.7% | 去掉%，转为小数 | "6.7%" → 0.067 |
| 小数格 | 2.3年 | 提取数字 | "2.3年" → 2.3 |
| 文本格 | 开放申购 | 保持文本 | "开放申购" → "开放申购" |
| 整数格 | 253.25百万 | 提取数字（按情况处理） | "253.25百万" → 253.25 |

### 错误示例（避免）

```
❌ 错误做法：
模板单元格格式：百分比
PDF数据：6.7
AI写入：ws['F12'] = '6.7%'  # 错误！写入文本

✅ 正确做法：
模板单元格格式：百分比
PDF数据：6.7
AI写入：ws['F12'] = 0.067  # 正确！写入小数，Excel自动显示为6.7%
```

---

## PDF信息Excel生成规则（v2.1新增）

### 为什么先生成PDF信息Excel？

1. **用户可对照检查**：PDF信息Excel包含所有提取数据，用户可验证准确性
2. **透明化过程**：用户能看到AI提取了什么
3. **便于调试**：如果映射出错，用户可快速定位问题

### PDF信息Excel结构

```
Sheet 1: PDF提取数据

一、基金基本信息
| 字段 | 值 |
| 基金名称 | 中银香港全天候中国高息债券基金 |
| 基金经理 | 中银香港资产管理有限公司 |
| 总资产值 | 253.25百万人民币 |
| 报告日期 | 2025年12月31日 |

二、核心指标
| 指标 | 值 |
| 组合久期 | 2.3年 |
| 平均最低孳息率 | 6.7% |

三、行业分布
| 行业 | 占比 |
| 周期性消费品 | 24.4% |
| 金融 | 20.1% |
| 房地产 | 12.9% |
| ... | ... |

四、地区分布
| 地区 | 占比 |
| ... | ... |

五、信用评级分布
| 评级 | 占比 |
| ... | ... |

说明：
- 本Excel包含从PDF中提取的所有数据
- 部分数据通过OCR识别，可能有误差
- 数据来源：XXX基金2025年12月月报
```

---

## 学习结果存储

**每次学习都是临时的，不永久保存：**

```python
# 学习结果存储在内存中
template_learning_result = {
    "structure": {...},
    "mapping": {...},
    "styles": {...},
    "merged_cells": [...]
}

# 生成Excel后清除
del template_learning_result
```

**原因**：
- 每次用户的模板可能不同
- 不需要"记住"之前的模板
- 每次都是全新的学习


path: SECURITY_REVIEW.md
content: # SKILL SECURITY REVIEW

**Skill Name:** mutual-fund-monthly-update
**Version:** 2.2.2
**Author:** Yujing2013
**Review Date:** 2026-03-13
**Reviewer:** AI Agent (OpenClaw)

---

## 📋 PURPOSE & CAPABILITY

### What does this skill do?

This skill extracts data from fund monthly report PDFs and generates Excel files.

**Core capabilities:**
1. PDF text extraction (pdfplumber)
2. OCR recognition for charts/images (pytesseract)
3. Excel template learning and filling (openpyxl)
4. Batch processing of multiple PDFs from a folder
5. Intelligent field mapping and fuzzy matching

### What is the intended use case?

Users upload PDF monthly reports and optionally an Excel template, and the skill automatically extracts data and generates formatted Excel files.

---

## 🎯 INSTRUCTION SCOPE

### File Access

**READ:**
- ✅ PDF files (user-provided)
- ✅ Excel files (user-provided templates)
- ✅ Folders (user-specified paths)

**WRITE:**
- ✅ Excel files (output files)
- ✅ PDF to image conversion (temporary /tmp files)

**Scope:**
- Only processes files in user-specified paths
- Does NOT access:
  - ~/.ssh, ~/.aws, ~/.config
  - System files outside workspace
  - MEMORY.md, USER.md, SOUL.md, IDENTITY.md

### Network Access

**NONE** - This skill does NOT make network requests

✅ No external API calls
✅ No data transmission to external servers
✅ All processing is local

### Command Execution

**Executed commands:**
- Python scripts (pdfplumber, openpyxl, pytesseract, pdf2image)
- System commands:
  - `tesseract` (OCR engine)
  - `pdftoppm` (PDF to image conversion, via pdf2image)

**Risk:** Low - Only standard data processing commands

---

## 🔧 INSTALL MECHANISM

### Dependencies

**Python packages (via pip):**
```
pdfplumber>=0.7.0
openpyxl>=3.0.0
pdf2image>=1.16.0
pytesseract>=0.3.10
Pillow>=9.0.0
```

**System dependencies (manual install required):**
```
tesseract-ocr
tesseract-ocr-chi-sim (Chinese language pack)
poppler-utils (for PDF to image conversion)
```

### Installation Risks

| Risk | Level | Mitigation |
|------|-------|------------|
| pip install from PyPI | 🟡 Medium | All packages are well-known, official packages |
| System dependencies | 🟡 Medium | Requires sudo for apt-get install |
| Binary downloads | 🟡 Medium | tesseract and poppler are official packages |

### Recommendations

- ✅ Install Python packages in virtual environment
- ✅ System dependencies should be installed by system administrator
- ✅ No custom binaries or scripts from unknown sources

---

## 🔐 CREDENTIALS

### Environment Variables

**NONE** - This skill does NOT use environment variables

✅ No API keys required
✅ No tokens required
✅ No credentials storage

### Output Files

**Default output paths:**
- `/root/.openclaw/media/outbound/` (remote environment)
- User-specified folder (batch processing)

**Risk:** Low - Output files are standard Excel files, no sensitive data exposure

---

## 💾 PERSISTENCE & PRIVILEGE

### File Persistence

- **Temporary files:** PDF to image conversions stored in `/tmp/` (auto-cleaned)
- **Output files:** Excel files in user-specified locations
- **No hidden files:** Does not create hidden files or modify system configs

### Privilege Requirements

- **Normal user privileges:** Sufficient for all operations
- **No sudo required:** For skill execution (only for system dependency installation)
- **No root access:** Never required

### Memory Persistence

- **No persistent memory:** Skill does not modify MEMORY.md, USER.md, SOUL.md, IDENTITY.md
- **Session-based:** All learning is temporary per session

---

## ⚠️ WHAT TO CONSIDER

### Security Considerations

| Consideration | Risk | Mitigation |
|--------------|------|------------|
| File path validation | 🟡 Medium | Should validate user-provided paths to prevent directory traversal |
| PDF parsing vulnerabilities | 🟡 Medium | pdfplumber is well-maintained, but PDF parsing has historical vulnerabilities |
| OCR accuracy | 🟢 Low | OCR may produce errors, but no security risk |
| Temporary file cleanup | 🟢 Low | Files in /tmp are auto-cleaned by OS |

### Privacy Considerations

| Consideration | Risk | Mitigation |
|--------------|------|------------|
| PDF content access | 🟢 Low | Only processes user-provided files |
| No data exfiltration | ✅ None | All processing is local, no network calls |
| No credential storage | ✅ None | Does not store or transmit credentials |

### Performance Considerations

| Consideration | Impact | Notes |
|--------------|--------|-------|
| OCR processing | Medium | OCR is CPU-intensive, may take time for large PDFs |
| PDF to image conversion | Medium | High-resolution images (300 DPI) consume memory |
| Batch processing | High | Processing many PDFs may take significant time |

---

## 🚨 RED FLAGS CHECK

**Check for these red flags:**

- ✅ NO curl/wget to unknown URLs
- ✅ NO sends data to external servers
- ✅ NO requests credentials/tokens/API keys
- ✅ NO reads ~/.ssh, ~/.aws, ~/.config without clear reason
- ✅ NO accesses MEMORY.md, USER.md, SOUL.md, IDENTITY.md
- ✅ NO uses base64 decode on anything
- ✅ NO uses eval() or exec() with external input
- ✅ NO modifies system files outside workspace
- ✅ NO network calls to IPs instead of domains
- ✅ NO obfuscated code (compressed, encoded, minified)
- ✅ NO requests elevated/sudo permissions (for skill execution)
- ✅ NO accesses browser cookies/sessions
- ✅ NO touches credential files

**Result:** ✅ NO RED FLAGS FOUND

---

## ⚠️ WHAT TO CONSIDER BEFORE INSTALLING

### Network/Behavior

**Issue:** Scripts previously auto-installed dependencies via `pip install`
**Fixed:** ✅ Removed auto-installation code in v2.2.2
**Current behavior:** Scripts check for dependencies and exit with error if missing

**Action required:**
- Pre-install dependencies manually: `pip install pdfplumber openpyxl pdf2image pytesseract Pillow`
- Install system dependencies: `sudo apt-get install tesseract-ocr poppler-utils`

### Missing File Risks

**Issue:** `auto_update_final.py` expected `extraction_templates.json` which was missing
**Fixed:** ✅ Created `extraction_templates.json` in v2.2.2

**Files now included:**
- `references/extraction_templates.json` ✅
- All required configuration files ✅

### File Path Concerns

**Issue:** Batch processing scans user-specified folders
**Mitigation:** 
- Only scans folders explicitly provided by user
- Does NOT scan system directories
- Does NOT access files outside specified paths

**Recommendation:**
- Validate folder paths before processing
- Test in isolated environment first

### Precautions

**Before using this skill:**
1. ✅ Install dependencies manually (see INSTALL MECHANISM section)
2. ✅ Test in isolated/virtual environment
3. ✅ Verify file paths are correct
4. ✅ Review extracted data for accuracy
5. ✅ Check OCR results (may have errors)

---

## 🔧 INSTALL MECHANISM (UPDATED)

### Dependencies

**Python packages (manual install required):**
```bash
pip install pdfplumber>=0.7.0
pip install openpyxl>=3.0.0
pip install pdf2image>=1.16.0
pip install pytesseract>=0.3.10
pip install Pillow>=9.0.0
```

**System dependencies (manual install required):**
```bash
# Ubuntu/Debian
sudo apt-get install tesseract-ocr tesseract-ocr-chi-sim poppler-utils

# macOS
brew install tesseract tesseract-lang poppler
```

### Installation Steps

1. Install Python packages (see above)
2. Install system dependencies (see above)
3. Extract skill to workspace
4. Verify all files present:
   - `SKILL.md`
   - `SECURITY_REVIEW.md`
   - `references/` directory (6 files)
   - `scripts/` directory (4 files)

**⚠️ Do NOT rely on auto-installation** - Scripts will NOT auto-install dependencies

---

## 📊 RISK CLASSIFICATION

**Overall Risk Level:** 🟢 LOW

| Category | Risk Level |
|----------|-----------|
| Purpose | 🟢 LOW - Data extraction and formatting |
| File Access | 🟢 LOW - Only user-provided files |
| Network Access | 🟢 LOW - None |
| Credentials | 🟢 LOW - None required |
| Privilege | 🟢 LOW - Normal user privileges |
| Dependencies | 🟡 MEDIUM - System dependencies required |
| Red Flags | 🟢 LOW - None found |

---

## ✅ VERDICT

**SAFE TO INSTALL** ✅

**Security Issues Fixed in v2.2.2:**
1. ✅ Removed auto-installation of dependencies (no more pip calls during execution)
2. ✅ Created missing `extraction_templates.json` file
3. ✅ Updated security documentation to accurately reflect behavior

**Recommendation:** Install with standard precautions:
1. ✅ Pre-install dependencies manually (pip + system dependencies)
2. ✅ Test in isolated environment first
3. ✅ Verify all files are present after extraction
4. ✅ Review extracted data for accuracy

**Important Notes:**
- Scripts will NOT auto-install dependencies (changed in v2.2.2)
- Scripts will exit with error if dependencies are missing
- All required configuration files are now included
- No network access during execution

---

## 📝 NOTES

**Changes in v2.2.2:**
- ✅ Removed auto-installation code from scripts
- ✅ Created missing `extraction_templates.json` file
- ✅ Updated security documentation

**Positive observations:**
- Clear, well-documented skill
- No network requests during execution (after fix)
- No credentials required
- No red flags found
- Minimal permission scope
- Well-organized reference documentation
- Security issues promptly addressed

**Areas for improvement:**
- Consider adding file path validation to prevent directory traversal
- Add more error handling for malformed PDFs
- Document system dependency installation in SKILL.md

**Trust level:** High - Author is known user (Yujing2013), security issues identified and fixed

---

## 🔄 REVIEW HISTORY

| Date | Version | Reviewer | Result | Notes |
|------|---------|----------|--------|-------|
| 2026-03-13 16:49 | 2.2.2 | AI Agent (OpenClaw) | ⚠️ Issues Found | Auto-install, missing files |
| 2026-03-13 16:56 | 2.2.2 | AI Agent (OpenClaw) | ✅ SAFE TO INSTALL | Issues fixed |

---

**Review completed by:** AI Agent (OpenClaw)
**Review date:** 2026-03-13 16:56 GMT+8
**Status:** ✅ All security issues resolved


path: requirements.txt
content: # Core dependencies
pdfplumber>=0.7.0
openpyxl>=3.0.0

# Optional: OCR for chart extraction
pytesseract>=0.3.10
Pillow>=9.0.0


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