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,要求是能读取126、163的邮件内容,并根据提取存在【育儿假】内容的邮件,汇总到excel,excel的标题是姓名、育儿假天数、育儿假开始日期、育儿假结算日期
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
---
## 技能用途
此技能用于自动从指定的126或163邮箱中扫描邮件,筛选出主题或正文中包含“育儿假”关键字的邮件,然后从邮件正文中解析出以下字段:
- 姓名
- 育儿假天数
- 育儿假开始日期
- 育儿假结算日期
最终将解析结果汇总为一个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`中的正则表达式,或检查邮件正文中的字段名称是否匹配。
## 贡献
欢迎提交Issue和Pull 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` 中的正则表达式和解析逻辑。
夜雨聆风