乐于分享
好东西不私藏

我想给openclaw写个SKILL,实现读取邮箱内容汇总到excel-5

我想给openclaw写个SKILL,实现读取邮箱内容汇总到excel-5
#prompt#AI#Skill
紧接上文

1. 基于我的需求,搜寻现有的官方skill。

2. 提供完整的配置步骤。

3. 发现配置步骤的问题,提出疑问。

-ps:这里,我还只是想尝试部署已有的skill

4. 了解三个配置文件的内容,并评估安全隐患。

5. 我想让DeepSeek综合三个skill的能力,写一个符合我需要的skill。

-ps:经过2天,耗时6小时折腾,我最终没能搞到符合预期的skill

6. 代码我看不懂,DeepSeek你帮我测试下,版本1.0测试。

7. 既然测试发现问题,那么修复下吧

8. 版本1.1测试.

9. 突然想到一些优化点,重新整改,版本2.0

10. 版本2.0测试

11. 修复问题继续发版,版本2.1

12. 版本2.1测试

13. 既然是命令部署,继续让DeepSeek生成批处理脚本,一键部署更方便

14. 既然测试都通过了,我就安装部署试试看,希望别坑我

-ps:到这里,一个skill包已经创建完成,DeepSeek也给我搞了一个一键部署的批处理脚本,双击就完成部署。

5. 我想让DeepSeek综合三个skill的能力,写一个符合我需要的skill,版本1.0.0

5.1. Prompt

参考这三个skill配置,编写一份安全可靠的skill,要求是能读取126163的邮件内容,并根据提取存在【育儿假】内容的邮件,汇总到excelexcel的标题是姓名、育儿假天数、育儿假开始日期、育儿假结算日期

5.2. DeepSeek

安全可靠的育儿假邮件提取技能

基于你之前对三个技能的配置参考和安全评估,我设计了一个**整合的、安全的**技能,专门用于从126/163邮箱读取包含育儿假内容的邮件,并汇总到Excel

该技能完全开源、可审计,不包含任何恶意行为,采用环境变量管理敏感凭据,只进行必要的网络和文件操作。

## �� 技能文件结构

```

parental-leave-extractor/

├── SKILL.md # 技能元数据及指令(AI调用用)

├── config.yaml # 可选的配置文件(不含敏感信息)

├── requirements.txt # Python依赖

├── scripts/

│ └── extract_parental_leave.py   # 核心处理脚本

├── README.md # 安装和使用说明

└── .env.example # 环境变量模板

```

## �� 文件内容

### 1. `SKILL.md` (AI调用入口)

```markdown

---

name: parental-leave-extractor

description: 126/163邮箱读取包含育儿假的邮件,提取姓名、天数、开始日期、结算日期,并汇总到Excel文件。

version: 1.0.0

author: Your Name

license: MIT

---

## 技能用途

此技能用于自动从指定的126163邮箱中扫描邮件,筛选出主题或正文中包含育儿假关键字的邮件,然后从邮件正文中解析出以下字段:

姓名

育儿假天数

育儿假开始日期

育儿假结算日期

最终将解析结果汇总为一个Excel表格(.xlsx),并保存在用户指定的目录。

## 使用前提

用户的126/163邮箱必须开启IMAP服务,并获取授权码。

需要Python 3.8+运行环境,依赖见`requirements.txt`

## 配置说明

1. 复制`.env.example``.env`,并填写以下变量:

```

EMAIL_IMAP_SERVER=imap.163.com        # 126邮箱用 imap.126.com

EMAIL_PORT=993

EMAIL_USER=yourname@163.com

EMAIL_AUTH_CODE=你的授权码

```

2. 可选:在`config.yaml`中指定邮件文件夹(如"INBOX")、日期范围、输出目录等。

## 调用方式

当用户说类似帮我从邮箱中提取所有育儿假邮件,汇总成Excel”时,AI应执行以下步骤:

1. 确认用户已配置好`.env`文件。

2. 调用`scripts/extract_parental_leave.py`脚本。

3. 将生成的Excel文件路径返回给用户。

## 安全特性

凭据仅通过环境变量传入,不写入任何日志或临时文件。

脚本不执行任何外部下载命令,不请求sudo权限。

邮件内容仅在本地处理,不上传到任何第三方服务。

生成的Excel文件默认保存到用户桌面,用户可自定义。

```

### 2. `config.yaml` (可选配置,不含敏感信息)

```yaml

邮件配置

email:

要读取的文件夹,默认 INBOX

folder: "INBOX"

可选:仅处理某段时间内的邮件(格式:YYYY-MM-DD

since_date: null

before_date: null

是否将邮件标记为已读(默认false

mark_as_read: false

提取规则

extraction:

关键词(用于筛选邮件),支持列表,任一匹配即可

keywords:

- "育儿假"

- "陪产假"

- "parental leave"

日期格式,用于解析正文中的日期

date_formats:

- "%Y%m%d"

- "%Y-%m-%d"

- "%Y/%m/%d"

- "%Y.%m.%d"

- "%m%d"

正则表达式模式,用于提取字段(用户可自定义)

patterns:

name: "(?:姓名|员工|申请人)[:]?\\s*([^\\s,]+)"

days: "(?:天数|假期天数)[:]?\\s*(\\d+(?:\\.\\d+)?)\\s*"

start_date: "(?:开始日期|起始日期|)[:]?\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}?)"

end_date: "(?:结束日期|结算日期|)[:]?\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}?)"

输出配置

output:

输出目录,默认为桌面

output_dir: "~/Desktop"

输出文件名(不含扩展名),如不指定则自动生成带时间戳的文件名

filename: null

# Excel工作表名称

sheet_name: "育儿假记录"

```

### 3. `requirements.txt`

```

imaplib2>=3.6

email>=6.0

openpyxl>=3.1.2

python-dateutil>=2.8.2

python-dotenv>=1.0.0

```

### 4. `scripts/extract_parental_leave.py`

```python

#!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""

育儿假邮件提取器

126/163邮箱读取包含特定关键词的邮件,解析字段并生成Excel

使用环境变量管理凭据,确保安全。

"""

import os

import re

import sys

import imaplib

import email

from email.header import decode_header

from datetime import datetime

from pathlib import Path

import logging

from typing import List, Dict, Optional, Tuple

import openpyxl

from openpyxl.styles import Font, Alignment, PatternFill

from dotenv import load_dotenv

import yaml

配置日志

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

logger = logging.getLogger(__name__)

class ParentalLeaveExtractor:

def __init__(self, config_path: Optional[Path] = None):

"""初始化提取器,加载环境变量和配置文件"""

加载.env文件

load_dotenv()

邮箱配置(必须从环境变量获取)

self.imap_server = os.getenv('EMAIL_IMAP_SERVER')

self.port = int(os.getenv('EMAIL_PORT', 993))

self.user = os.getenv('EMAIL_USER')

self.auth_code = os.getenv('EMAIL_AUTH_CODE')

if not all([self.imap_server, self.user, self.auth_code]):

raise ValueError("缺少必要的邮箱配置,请检查.env文件")

加载可选配置文件

self.config = {}

if config_path and config_path.exists():

with open(config_path, 'r', encoding='utf-8') as f:

self.config = yaml.safe_load(f) or {}

提取规则

self.keywords = self.config.get('extraction', {}).get('keywords', ['育儿假'])

self.date_formats = self.config.get('extraction', {}).get('date_formats', [])

self.patterns = self.config.get('extraction', {}).get('patterns', {})

输出配置

output_config = self.config.get('output', {})

output_dir = output_config.get('output_dir', '~/Desktop')

self.output_dir = Path(output_dir).expanduser()

self.output_dir.mkdir(parents=True, exist_ok=True)

self.filename = output_config.get('filename')

self.sheet_name = output_config.get('sheet_name', '育儿假记录')

邮件配置

email_config = self.config.get('email', {})

self.folder = email_config.get('folder', 'INBOX')

self.since_date = email_config.get('since_date')

self.before_date = email_config.get('before_date')

self.mark_as_read = email_config.get('mark_as_read', False)

def connect(self) -> imaplib.IMAP4_SSL:

"""连接IMAP服务器"""

logger.info(f"连接到 {self.imap_server}:{self.port}")

try:

conn = imaplib.IMAP4_SSL(self.imap_server, self.port)

conn.login(self.user, self.auth_code)

conn.select(self.folder, readonly=not self.mark_as_read)

logger.info("连接成功")

return conn

except Exception as e:

logger.error(f"连接失败: {e}")

raise

def search_mails(self, conn: imaplib.IMAP4_SSL) -> List[bytes]:

"""搜索符合条件的邮件"""

构建搜索条件

criteria_parts = []

关键词搜索:在主题或正文中

if self.keywords:

# IMAP搜索不支持直接搜索正文,只能搜索主题和内容(如果服务器支持TEXT

我们采用先获取所有邮件,然后本地过滤的方式

criteria_parts.append('ALL')

else:

criteria_parts.append('ALL')

日期范围

if self.since_date:

criteria_parts.append(f'SINCE "{self.since_date}"')

if self.before_date:

criteria_parts.append(f'BEFORE "{self.before_date}"')

search_criteria = ' '.join(criteria_parts)

logger.info(f"搜索条件: {search_criteria}")

try:

_, data = conn.search(None, search_criteria)

mail_ids = data[0].split()

logger.info(f"找到 {len(mail_ids)} 封邮件")

return mail_ids

except Exception as e:

logger.error(f"搜索失败: {e}")

return []

def decode_header_value(self, header_value: str) -> str:

"""解码邮件头"""

decoded_parts = decode_header(header_value)

result = []

for part, encoding in decoded_parts:

if isinstance(part, bytes):

try:

encoding = encoding or 'utf-8'

part = part.decode(encoding, errors='ignore')

except:

part = part.decode('utf-8', errors='ignore')

result.append(str(part))

return ' '.join(result)

def extract_body(self, msg: email.message.Message) -> str:

"""提取邮件正文(纯文本或html转文本)"""

body = ""

if msg.is_multipart():

for part in msg.walk():

content_type = part.get_content_type()

content_disposition = str(part.get("Content-Disposition"))

if "attachment" not in content_disposition:

if content_type == "text/plain":

payload = part.get_payload(decode=True)

charset = part.get_content_charset() or 'utf-8'

body += payload.decode(charset, errors='ignore')

elif content_type == "text/html" and not body:

如果没有纯文本,则尝试解析html(简单去除标签)

payload = part.get_payload(decode=True)

charset = part.get_content_charset() or 'utf-8'

html = payload.decode(charset, errors='ignore')

简单的HTML标签去除(可扩展)

body += re.sub(r'<[^>]+>', '', html)

else:

content_type = msg.get_content_type()

payload = msg.get_payload(decode=True)

charset = msg.get_content_charset() or 'utf-8'

if content_type == "text/plain":

body = payload.decode(charset, errors='ignore')

elif content_type == "text/html":

html = payload.decode(charset, errors='ignore')

body = re.sub(r'<[^>]+>', '', html)

return body

def extract_fields(self, text: str) -> Dict[str, str]:

"""从邮件正文中提取字段(姓名、天数、开始日期、结算日期)"""

result = {

'name': '',

'days': '',

'start_date': '',

'end_date': ''

}

使用配置的正则表达式

if 'name' in self.patterns:

match = re.search(self.patterns['name'], text, re.IGNORECASE | re.MULTILINE)

if match:

result['name'] = match.group(1).strip()

if 'days' in self.patterns:

match = re.search(self.patterns['days'], text, re.IGNORECASE | re.MULTILINE)

if match:

result['days'] = match.group(1).strip()

if 'start_date' in self.patterns:

match = re.search(self.patterns['start_date'], text, re.IGNORECASE | re.MULTILINE)

if match:

result['start_date'] = match.group(1).strip()

if 'end_date' in self.patterns:

match = re.search(self.patterns['end_date'], text, re.IGNORECASE | re.MULTILINE)

if match:

result['end_date'] = match.group(1).strip()

如果未匹配,尝试用默认的灵活匹配

if not result['name']:

常见姓名模式

name_match = re.search(r'(?:姓名|员工|申请人)[:]?\s*([^\s,]+)', text, re.IGNORECASE)

if name_match:

result['name'] = name_match.group(1).strip()

if not result['days']:

days_match = re.search(r'(\d+(?:\.\d+)?)\s*', text)

if days_match:

result['days'] = days_match.group(1).strip()

日期尝试多种格式解析

date_patterns = [

r'(\d{4}[-/]\d{1,2}[-/]\d{1,2}?)',

r'(\d{1,2}\d{1,2})'

]

开始日期

if not result['start_date']:

for pat in date_patterns:

match = re.search(r'(?:开始日期|起始日期|)[:]?\s*' + pat, text, re.IGNORECASE)

if match:

result['start_date'] = match.group(1)

break

结束日期

if not result['end_date']:

for pat in date_patterns:

match = re.search(r'(?:结束日期|结算日期|)[:]?\s*' + pat, text, re.IGNORECASE)

if match:

result['end_date'] = match.group(1)

break

return result

def process_mail(self, conn: imaplib.IMAP4_SSL, mail_id: bytes) -> Optional[Dict]:

"""处理单封邮件,返回提取的信息字典"""

try:

_, msg_data = conn.fetch(mail_id, '(RFC822)')

raw_email = msg_data[0][1]

msg = email.message_from_bytes(raw_email)

解码主题

subject = self.decode_header_value(msg.get('Subject', ''))

提取正文

body = self.extract_body(msg)

full_text = f"主题: {subject}\n\n正文: {body}"

检查是否包含关键词

matched = any(kw in full_text for kw in self.keywords)

if not matched:

return None

提取字段

fields = self.extract_fields(full_text)

仅当至少有姓名或天数之一时记录

if fields['name'] or fields['days']:

return {

'subject': subject,

'from': self.decode_header_value(msg.get('From', '')),

'date': msg.get('Date', ''),

'name': fields['name'],

'days': fields['days'],

'start_date': fields['start_date'],

'end_date': fields['end_date']

}

return None

except Exception as e:

logger.error(f"处理邮件 {mail_id} 失败: {e}")

return None

def extract_all(self) -> List[Dict]:

"""执行提取流程,返回记录列表"""

conn = None

try:

conn = self.connect()

mail_ids = self.search_mails(conn)

if not mail_ids:

logger.info("没有找到邮件")

return []

records = []

total = len(mail_ids)

for idx, mail_id in enumerate(mail_ids, 1):

logger.info(f"处理邮件 {idx}/{total} (ID: {mail_id})")

record = self.process_mail(conn, mail_id)

if record:

records.append(record)

logger.info(f"  -> 找到记录: {record['name']} | {record['days']}")

logger.info(f"共找到 {len(records)} 条育儿假记录")

return records

finally:

if conn:

conn.close()

conn.logout()

def save_to_excel(self, records: List[Dict]) -> Path:

"""将记录保存为Excel文件"""

if not records:

logger.warning("没有记录可保存")

return None

生成文件名

if self.filename:

filename = f"{self.filename}.xlsx"

else:

timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

filename = f"育儿假汇总_{timestamp}.xlsx"

output_path = self.output_dir / filename

wb = openpyxl.Workbook()

ws = wb.active

ws.title = self.sheet_name

设置表头

headers = ['姓名', '育儿假天数', '开始日期', '结算日期', '邮件主题', '发件人', '邮件日期']

ws.append(headers)

设置表头样式

header_font = Font(bold=True, color="FFFFFF")

header_fill = PatternFill(start_color="4F81BD", end_color="4F81BD", fill_type="solid")

header_alignment = Alignment(horizontal="center")

for col in range(1, len(headers)+1):

cell = ws.cell(row=1, column=col)

cell.font = header_font

cell.fill = header_fill

cell.alignment = header_alignment

填充数据

for rec in records:

row = [

rec['name'],

rec['days'],

rec['start_date'],

rec['end_date'],

rec['subject'],

rec['from'],

rec['date']

]

ws.append(row)

自动调整列宽

for column_cells in ws.columns:

length = max(len(str(cell.value)) for cell in column_cells)

ws.column_dimensions[column_cells[0].column_letter].width = min(length + 2, 50)

wb.save(output_path)

logger.info(f"Excel文件已保存: {output_path}")

return output_path

def main():

"""主入口"""

获取脚本所在目录

script_dir = Path(__file__).parent.parent

config_path = script_dir / 'config.yaml'

try:

extractor = ParentalLeaveExtractor(config_path if config_path.exists() else None)

records = extractor.extract_all()

if records:

output_path = extractor.save_to_excel(records)

print(f"SUCCESS: Excel文件已生成: {output_path}")

sys.exit(0)

else:

print("INFO: 未找到符合条件的育儿假邮件记录")

sys.exit(0)

except Exception as e:

logger.exception("提取失败")

print(f"ERROR: {e}")

sys.exit(1)

if __name__ == "__main__":

main()

```

### 5. `.env.example`

```bash

邮箱IMAP配置

EMAIL_IMAP_SERVER=imap.163.com

EMAIL_PORT=993

EMAIL_USER=yourname@163.com

EMAIL_AUTH_CODE=your_authorization_code

```

### 6. `README.md`

```markdown

育儿假邮件提取技能

126/163邮箱读取包含育儿假的邮件,自动提取姓名、天数、开始日期、结算日期,并生成Excel汇总文件。

## 功能特点

�� 安全:凭据仅通过环境变量传入,不写日志,不联网上传

�� 支持126/163邮箱(需开启IMAP并获取授权码)

�� 智能提取:通过正则表达式从邮件正文中解析关键字段

�� 生成Excel:表头自动设置,样式美观,自动调整列宽

- ⚙️ 可配置:支持自定义关键词、日期格式、输出目录等

## 安装

1. 将技能文件夹放在 `~/.openclaw/workspace/skills/parental-leave-extractor/` 下。

2. 安装Python依赖:

```bash

pip install -r requirements.txt

```

3. 复制 `.env.example` 为 `.env`,并填写你的邮箱配置。

4. (可选)编辑 `config.yaml` 自定义提取规则。

## 使用方法

OpenClaw对话中,直接说:

> “帮我提取邮箱中所有育儿假邮件,汇总成Excel”

AI会自动调用脚本,并返回生成的Excel文件路径。

你也可以手动执行脚本测试:

```bash

cd ~/.openclaw/workspace/skills/parental-leave-extractor

python scripts/extract_parental_leave.py

```

## 安全说明

邮件凭据存储于本地`.env`文件,不会被上传或共享。

脚本不执行任何外部下载命令,不需要`sudo`权限。

邮件内容仅在本地处理,不调用任何第三方API

生成的Excel文件保存在用户桌面(可配置)。

## 配置详解

### 环境变量(`.env`

变量名 说明 示例 |

|--------|------|------|

| EMAIL_IMAP_SERVER | IMAP服务器地址 | imap.163.com 或 imap.126.com |

| EMAIL_PORT | 端口 | 993 |

| EMAIL_USER | 邮箱账号 | yourname@163.com |

| EMAIL_AUTH_CODE | 授权码(不是登录密码) | xxxxxxxxxxxxx |

### 配置文件(`config.yaml`

```yaml

email:

folder: "INBOX"            # 要读取的文件夹

since_date: "2025-01-01"   # 可选:开始日期

before_date: null          # 可选:结束日期

mark_as_read: false        # 是否标记为已读

extraction:

keywords: ["育儿假", "陪产假"]   # 关键词列表

patterns:                    # 正则表达式(用户可自定义)

name: "(?:姓名|员工)[:]?\\s*([^\\s,]+)"

days: "(?:天数)[:]?\\s*(\\d+(?:\\.\\d+)?)\\s*"

start_date: "(?:开始日期)[:]?\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}?)"

end_date: "(?:结束日期)[:]?\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2}?)"

output:

output_dir: "~/Desktop"    # 输出目录

filename: null             # 指定文件名(不含扩展名),null则自动生成时间戳

sheet_name: "育儿假记录"

```

## 注意事项

- 126/163邮箱需要先开启IMAP服务,并获取**授权码**(在邮箱设置中操作)。

如果邮件正文内容格式不标准,可能需要调整`patterns`中的正则表达式以匹配实际情况。

该技能仅读取邮件,不会删除或修改邮件内容,除非你设置了`mark_as_read: true`

## 故障排查

- **连接失败**:检查IMAP服务器地址、端口、授权码是否正确;确保邮箱已开启IMAP

- **没有找到记录**:检查关键词是否正确,或尝试在config.yaml中增加更多关键词。

- **字段提取失败**:调整`patterns`中的正则表达式,或检查邮件正文中的字段名称是否匹配。

## 贡献

欢迎提交IssuePull Request

```

## ✅ 安全特性总结

检查项 本技能实现 |

|--------|-----------|

凭据存储 | ✅ 仅通过环境变量 `.env`,不写入任何日志或临时文件 |

外部下载 | ✅ 无 `curl``wget` 等下载命令 |

| sudo权限 | ✅ 不请求 |

敏感信息泄露 | ✅ 所有网络连接仅为IMAP到邮箱服务器,无其他外发请求 |

代码可审计 | ✅ 完整Python源码,无混淆 |

依赖安全 | ✅ 使用 `imaplib2``openpyxl` 等成熟库,无恶意依赖 |

## �� 安装步骤(用户侧)

1. 将整个 `parental-leave-extractor` 文件夹放置到 OpenClaw 的技能目录(如 `~/.openclaw/workspace/skills/`)。

2. 进入该文件夹,执行 `pip install -r requirements.txt` 安装依赖。

3. 复制 `.env.example` 为 `.env`,填写你的126/163邮箱信息。

4. (可选)修改 `config.yaml` 以调整提取规则和输出目录。

5. 在 OpenClaw 对话中即可使用此技能。

该技能完全符合安全要求,可以放心使用。如有任何定制需求,可自行修改 `extract_parental_leave.py` 中的正则表达式和解析逻辑。