Skill: shianaixuexi-cell/wechat-article-reader-0 (version 1.0.0)

--- name: wechat-article-reader description: 读取微信公众号文章链接，提取关键信息并生成摘要 version: 1.0.0 author: OpenClaw Community tags: [wechat, article, reader, summary] --- # WeChat Article Reader 读取微信公众号文章链接的Skill，可以提取文章标题、作者、发布时间等关键信息，并生成内容摘要。 ## 功能特性 - 自动识别微信公众号文章链接 - 提取文章关键信息（标题、作者、发布时间、公众号名称） - 生成文章内容摘要 - 支持飞书平台使用 ## 使用方法 ### 方法1：直接发送链接 直接将公众号文章链接发送给OpenClaw： ``` https://mp.weixin.qq.com/s/xxxxx ``` ### 方法2：明确指令 ``` 读取这篇文章：[链接] ``` ### 方法3：请求摘要 ``` 帮我总结这篇公众号文章：[链接] ``` ## 返回信息格式 Skill将返回以下关键信息： ``` 📄 文章标题：[标题] 👤 作者：[作者] 🏢 公众号：[公众号名称] 📅 发布时间：[时间] 📝 内容摘要： [AI生成的摘要] 🔑 关键点： • 关键点1 • 关键点2 • 关键点3 ``` ## 技术说明 - 使用requests获取网页内容 - 使用BeautifulSoup解析HTML - 处理微信公众号的反爬机制 - 智能提取文章正文内容 ## 依赖项 - requests>=2.31.0 - beautifulsoup4>=4.12.0 - lxml>=4.9.0 - fake-useragent>=1.4.0 ## 注意事项 - 仅支持微信公众号官方文章链接（mp.weixin.qq.com） - 部分文章可能因公众号设置无法访问 - 建议配合飞书WebHook使用以获得最佳体验 ## 常见问题 **Q: 为什么有些文章无法读取？** A: 可能是文章已删除、设置为私密，或需要关注公众号才能阅读。 **Q: 摘要质量如何保证？** A: Skill会提取文章核心段落，由OpenClaw的大模型能力进行智能摘要。 **Q: 支持其他平台的文章吗？** A: 当前版本仅支持微信公众号，后续版本可能扩展到其他平台。

path: main.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
微信公众号文章读取 Skill
支持提取文章关键信息并生成摘要
"""

import json
import re
import sys
from typing import Dict, Any, Optional
from urllib.parse import urlparse

try:
    import requests
    from bs4 import BeautifulSoup
    from fake_useragent import UserAgent
except ImportError as e:
    print(json.dumps({
        "status": "error",
        "message": f"缺少必要依赖: {str(e)}。请运行: pip install -r requirements.txt"
    }))
    sys.exit(1)


class WeChatArticleReader:
    """微信公众号文章读取器"""

    def __init__(self, timeout: int = 30, max_retries: int = 3):
        self.timeout = timeout
        self.max_retries = max_retries
        self.ua = UserAgent()
        self.session = requests.Session()

    def is_wechat_url(self, url: str) -> bool:
        """检查是否为微信公众号链接"""
        parsed = urlparse(url)
        return 'mp.weixin.qq.com' in parsed.netloc

    def fetch_article(self, url: str) -> Dict[str, Any]:
        """获取文章内容"""
        if not self.is_wechat_url(url):
            return {
                "status": "error",
                "message": "不是有效的微信公众号文章链接"
            }

        headers = {
            'User-Agent': self.ua.random,
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Accept-Encoding': 'gzip, deflate, br',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1'
        }

        for attempt in range(self.max_retries):
            try:
                response = self.session.get(
                    url,
                    headers=headers,
                    timeout=self.timeout,
                    allow_redirects=True
                )

                if response.status_code == 200:
                    return self.parse_article(response.text, url)
                elif response.status_code == 404:
                    return {
                        "status": "error",
                        "message": "文章不存在或已被删除"
                    }
                else:
                    return {
                        "status": "error",
                        "message": f"请求失败，状态码: {response.status_code}"
                    }

            except requests.exceptions.Timeout:
                if attempt < self.max_retries - 1:
                    continue
                return {
                    "status": "error",
                    "message": "请求超时，请检查网络连接"
                }
            except Exception as e:
                return {
                    "status": "error",
                    "message": f"获取文章时出错: {str(e)}"
                }

    def parse_article(self, html: str, url: str) -> Dict[str, Any]:
        """解析文章内容"""
        soup = BeautifulSoup(html, 'lxml')

        # 提取文章标题
        title = self.extract_title(soup)

        # 提取公众号名称
        account = self.extract_account(soup)

        # 提取作者
        author = self.extract_author(soup)

        # 提取发布时间
        publish_time = self.extract_publish_time(soup)

        # 提取正文内容
        content = self.extract_content(soup)

        # 生成摘要（提取前几段）
        summary = self.generate_summary(content)

        if not title and not content:
            return {
                "status": "error",
                "message": "无法解析文章内容，可能需要关注公众号才能阅读"
            }

        return {
            "status": "success",
            "data": {
                "title": title,
                "author": author,
                "account": account,
                "publish_time": publish_time,
                "content": content,
                "summary": summary,
                "url": url
            }
        }

    def extract_title(self, soup: BeautifulSoup) -> Optional[str]:
        """提取文章标题"""
        # 尝试多种方式获取标题
        selectors = [
            'meta[property="og:title"]',
            'h1.rich_media_title',
            'h1',
            '.rich_media_title',
            '#activity-name'
        ]

        for selector in selectors:
            element = soup.select_one(selector)
            if element:
                if element.name == 'meta':
                    title = element.get('content', '').strip()
                else:
                    title = element.get_text().strip()
                if title:
                    return title
        return None

    def extract_account(self, soup: BeautifulSoup) -> Optional[str]:
        """提取公众号名称"""
        selectors = [
            'meta[property="og:site_name"]',
            '.rich_media_meta_link',
            '#js_profile_qrcode_img'
        ]

        for selector in selectors:
            element = soup.select_one(selector)
            if element:
                if element.name == 'meta':
                    account = element.get('content', '').strip()
                else:
                    account = element.get_text().strip()
                if account:
                    return account
        return "未知公众号"

    def extract_author(self, soup: BeautifulSoup) -> Optional[str]:
        """提取作者信息"""
        selectors = [
            '.rich_media_meta_text',
            '#meta_content',
            '.author'
        ]

        for selector in selectors:
            element = soup.select_one(selector)
            if element:
                author = element.get_text().strip()
                if author and len(author) < 50:  # 避免误取长文本
                    return author
        return None

    def extract_publish_time(self, soup: BeautifulSoup) -> Optional[str]:
        """提取发布时间"""
        selectors = [
            'meta[property="article:published_time"]',
            '#publish_time',
            '.publish_time'
        ]

        for selector in selectors:
            element = soup.select_one(selector)
            if element:
                if element.name == 'meta':
                    time = element.get('content', '').strip()
                else:
                    time = element.get_text().strip()
                if time:
                    return time
        return None

    def extract_content(self, soup: BeautifulSoup) -> str:
        """提取正文内容"""
        # 微信公众号正文通常在 #js_content 中
        content_div = soup.select_one('#js_content')
        if not content_div:
            content_div = soup.select_one('.rich_media_content')

        if content_div:
            # 移除脚本和样式标签
            for script in content_div(['script', 'style']):
                script.decompose()

            # 获取文本内容
            content = content_div.get_text(separator='\n', strip=True)
            # 清理多余的空行
            content = re.sub(r'\n{3,}', '\n\n', content)
            return content.strip()

        return ""

    def generate_summary(self, content: str, max_length: int = 500) -> str:
        """生成文章摘要"""
        if not content:
            return ""

        # 按段落分割
        paragraphs = [p.strip() for p in content.split('\n') if p.strip()]

        if not paragraphs:
            return content[:max_length]

        # 取前几段作为摘要
        summary = ""
        for para in paragraphs[:5]:
            if len(summary) + len(para) > max_length:
                break
            summary += para + "\n\n"

        # 如果摘要太短，取第一段的前max_length个字符
        if len(summary.strip()) < 100 and paragraphs:
            summary = paragraphs[0][:max_length] + "..."

        return summary.strip()

    def format_output(self, result: Dict[str, Any]) -> str:
        """格式化输出结果"""
        if result["status"] == "error":
            return f"❌ {result['message']}"

        data = result["data"]
        output = []

        output.append("📄 " + (data.get("title") or "未知标题"))
        output.append("")

        if data.get("author"):
            output.append(f"👤 作者：{data['author']}")
        if data.get("account"):
            output.append(f"🏢 公众号：{data['account']}")
        if data.get("publish_time"):
            output.append(f"📅 发布时间：{data['publish_time']}")

        output.append("")
        output.append("📝 内容摘要：")
        output.append("")
        output.append(data.get("summary") or "无法生成摘要")

        if data.get("content"):
            content_lines = data["content"].split('\n')
            if len(content_lines) > 10:
                output.append("")
                output.append("...")
                output.append("(完整内容已省略，文章较长)")

        return "\n".join(output)


def handle(request: Dict[str, Any]) -> Dict[str, Any]:
    """
    OpenClaw Skill 主入口函数
    :param request: 包含用户消息和上下文的请求
    :return: 技能执行结果
    """
    # 获取用户消息
    message = request.get("message", "")
    url = extract_url(message)

    if not url:
        return {
            "status": "error",
            "message": "未找到有效的微信公众号文章链接。请发送以 mp.weixin.qq.com 开头的链接。"
        }

    # 创建读取器实例
    config = request.get("config", {})
    timeout = config.get("timeout", 30)
    max_retries = config.get("max_retries", 3)

    reader = WeChatArticleReader(timeout=timeout, max_retries=max_retries)

    # 获取文章
    result = reader.fetch_article(url)

    # 格式化输出
    if result["status"] == "success":
        formatted_output = reader.format_output(result)
        return {
            "status": "success",
            "message": formatted_output,
            "data": result["data"]
        }
    else:
        return result


def extract_url(text: str) -> Optional[str]:
    """从文本中提取微信公众号链接"""
    # 匹配微信公众号链接
    pattern = r'https?://mp\.weixin\.qq\.com/s[^\s<>"]+'
    match = re.search(pattern, text)

    if match:
        return match.group(0)
    return None


if __name__ == "__main__":
    # 测试代码
    test_url = "https://mp.weixin.qq.com/s/test"

    test_request = {
        "message": f"请帮我读取这篇文章：{test_url}",
        "config": {
            "timeout": 30,
            "max_retries": 3
        }
    }

    result = handle(test_request)
    print(json.dumps(result, ensure_ascii=False, indent=2))


path: README.md
content: # 微信公众号文章读取 Skill

让OpenClaw能够读取微信公众号文章链接的智能技能插件。

## ✨ 功能特性

- 🔗 **自动识别**：自动检测微信公众号文章链接
- 📊 **关键信息提取**：提取标题、作者、公众号名称、发布时间
- 📝 **智能摘要**：自动生成文章内容摘要
- 🚀 **即插即用**：支持飞书、Telegram、Discord等平台
- 🛡️ **反爬处理**：智能处理反爬机制

## 📦 安装方法

### 方法1：通过ClawHub安装（推荐）

```bash
# 搜索skill
clawhub search wechat-article-reader

# 安装
clawhub install wechat-article-reader
```

### 方法2：手动安装

```bash
# 1. 克隆或下载本仓库到OpenClaw工作区
cd ~/.openclaw/workspace/skills
git clone <repository-url> wechat-article-reader

# 2. 安装依赖
cd wechat-article-reader
pip install -r requirements.txt

# 3. 重启OpenClaw服务
openclaw restart
```

### 方法3：直接对话安装

直接在飞书中告诉OpenClaw：
```
请帮我安装这个skill：github.com/your-repo/wechat-article-reader
```

## 🎯 使用方法

### 基础用法

直接将公众号文章链接发送给OpenClaw：

```
https://mp.weixin.qq.com/s/xxxxx
```

### 明确指令

```
读取这篇文章：https://mp.weixin.qq.com/s/xxxxx
```

### 请求摘要

```
帮我总结这篇公众号文章：https://mp.weixin.qq.com/s/xxxxx
```

## 📤 返回格式

Skill会返回以下结构化信息：

```
📄 [文章标题]

👤 作者：[作者名称]
🏢 公众号：[公众号名称]
📅 发布时间：[YYYY-MM-DD]

📝 内容摘要：
[文章核心内容摘要，约3-5段]

🔑 关键点：
• 关键点1
• 关键点2
• 关键点3
```

## 🔧 配置选项

可以在 `skill.json` 中修改以下配置：

```json
{
  "config": {
    "timeout": 30,              // 请求超时时间（秒）
    "max_retries": 3,           // 最大重试次数
    "user_agent_rotation": true, // 是否轮换User-Agent
    "summary_max_length": 500    // 摘要最大长度
  }
}
```

## 🛠️ 技术架构

- **网络请求**：requests + fake-useragent
- **HTML解析**：BeautifulSoup + lxml
- **反爬处理**：User-Agent轮换 + 请求重试
- **内容提取**：智能提取规则 + 清洗算法

## 📁 文件结构

```
wechat-article-reader/
├── SKILL.md          # Skill元信息和说明
├── skill.json        # Skill配置文件
├── main.py           # 主逻辑代码
├── requirements.txt  # Python依赖
├── README.md         # 使用文档
└── test.py          # 测试脚本（可选）
```

## ⚠️ 注意事项

1. **链接格式**：仅支持 `mp.weixin.qq.com` 域名的链接
2. **访问限制**：部分文章可能需要关注公众号才能阅读
3. **网络环境**：需要能访问微信公众号的服务器
4. **文章时效**：已删除或私密文章无法读取

## 🐛 常见问题

### Q1: 提示"文章不存在或已被删除"
**A**: 可能原因：
- 文章已被作者删除
- 文章设置为私密（需要关注公众号）
- 链接格式不正确

### Q2: 请求超时怎么办？
**A**:
- 检查网络连接
- 在 `skill.json` 中增加 `timeout` 值
- 增加 `max_retries` 重试次数

### Q3: 摘要质量不好？
**A**: Skill提取文章核心段落，OpenClaw的大模型会进行二次优化。可以：
- 在指令中明确要求"详细摘要"或"简洁摘要"
- 配合OpenClaw的总结能力使用

### Q4: 支持其他平台吗？
**A**: 当前版本仅支持微信公众号。如有需求可以提交Issue或PR。

## 🚀 开发路线图

- [ ] 支持其他文章平台（知乎、掘金等）
- [ ] 图片提取和保存
- [ ] 视频链接识别
- [ ] 批量文章处理
- [ ] 历史文章缓存

## 🤝 贡献指南

欢迎提交Issue和Pull Request！

1. Fork本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 开启Pull Request

## 📄 许可证

MIT License

## 🙏 致谢

- OpenClaw社区提供的Skill开发框架
- BeautifulSoup和requests项目的维护者

## 📞 联系方式

如有问题或建议，请通过以下方式联系：

- 提交GitHub Issue
- 加入OpenClaw社区讨论

---

**Made with ❤️ for OpenClaw Community**


path: requirements.txt
content: requests>=2.31.0
beautifulsoup4>=4.12.0
lxml>=4.9.0
fake-useragent>=1.4.0


path: skill.json
content: {
  "name": "wechat-article-reader",
  "version": "1.0.0",
  "description": "读取微信公众号文章链接，提取关键信息并生成摘要",
  "author": "OpenClaw Community",
  "entry": "main.py",
  "type": "processor",
  "triggers": [
    {
      "type": "url",
      "pattern": "mp.weixin.qq.com",
      "description": "检测到微信公众号链接时自动触发"
    },
    {
      "type": "keyword",
      "keywords": ["读取", "总结", "摘要", "公众号"],
      "description": "包含相关关键词时触发"
    }
  ],
  "dependencies": [
    "requests>=2.31.0",
    "beautifulsoup4>=4.12.0",
    "lxml>=4.9.0",
    "fake-useragent>=1.4.0"
  ],
  "config": {
    "timeout": 30,
    "max_retries": 3,
    "user_agent_rotation": true,
    "summary_max_length": 500
  },
  "permissions": [
    "network:read",
    "network:http"
  ],
  "platforms": ["feishu", "telegram", "discord", "web"],
  "categories": ["content-processing", "social-media"]
}


path: test.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
微信公众号文章读取 Skill 测试脚本
"""

import json
from main import WeChatArticleReader, handle


def test_url_extraction():
    """测试URL提取功能"""
    print("=" * 50)
    print("测试1: URL提取功能")
    print("=" * 50)

    from main import extract_url

    test_cases = [
        ("这是文章链接：https://mp.weixin.qq.com/s/abc123", "https://mp.weixin.qq.com/s/abc123"),
        ("https://mp.weixin.qq.com/s/test123 请帮我读取", "https://mp.weixin.qq.com/s/test123"),
        ("这不是公众号链接 https://baidu.com", None),
        ("", None)
    ]

    for i, (text, expected) in enumerate(test_cases, 1):
        result = extract_url(text)
        status = "✓" if result == expected else "✗"
        print(f"{status} 测试用例 {i}: {text[:30]}...")
        if result != expected:
            print(f"  期望: {expected}")
            print(f"  实际: {result}")
    print()


def test_wechat_url_detection():
    """测试微信URL检测"""
    print("=" * 50)
    print("测试2: 微信URL检测")
    print("=" * 50)

    reader = WeChatArticleReader()

    test_urls = [
        ("https://mp.weixin.qq.com/s/abc123", True),
        ("https://mp.weixin.qq.com/s/def456?param=123", True),
        ("https://www.baidu.com", False),
        ("https://weixin.qq.com", False)
    ]

    for url, expected in test_urls:
        result = reader.is_wechat_url(url)
        status = "✓" if result == expected else "✗"
        print(f"{status} {url}: {'是微信链接' if result else '不是微信链接'}")
    print()


def test_html_parsing():
    """测试HTML解析功能"""
    print("=" * 50)
    print("测试3: HTML解析功能")
    print("=" * 50)

    reader = WeChatArticleReader()

    # 模拟微信文章HTML
    mock_html = """
    <html>
    <head>
        <meta property="og:title" content="测试文章标题" />
        <meta property="og:site_name" content="测试公众号" />
        <meta property="article:published_time" content="2026-03-16" />
    </head>
    <body>
        <h1 class="rich_media_title">测试文章标题</h1>
        <div class="rich_media_meta_text">测试作者</div>
        <div id="js_content">
            <p>这是第一段内容。</p>
            <p>这是第二段内容。</p>
            <p>这是第三段内容。</p>
        </div>
    </body>
    </html>
    """

    result = reader.parse_article(mock_html, "https://mp.weixin.qq.com/s/test")

    if result["status"] == "success":
        data = result["data"]
        print("✓ 解析成功！")
        print(f"  标题: {data['title']}")
        print(f"  作者: {data['author']}")
        print(f"  公众号: {data['account']}")
        print(f"  发布时间: {data['publish_time']}")
        print(f"  内容长度: {len(data['content'])} 字符")
        print(f"  摘要: {data['summary'][:50]}...")
    else:
        print(f"✗ 解析失败: {result['message']}")
    print()


def test_handle_function():
    """测试主处理函数"""
    print("=" * 50)
    print("测试4: 主处理函数")
    print("=" * 50)

    # 注意：这只是模拟测试，实际测试需要真实的微信文章链接
    test_request = {
        "message": "请帮我读取这篇文章：https://mp.weixin.qq.com/s/test",
        "config": {
            "timeout": 10,
            "max_retries": 2
        }
    }

    print("提示: 此测试需要真实的微信文章链接才能完整运行")
    print("模拟请求结构:")
    print(json.dumps(test_request, ensure_ascii=False, indent=2))
    print()

    # result = handle(test_request)
    # print("返回结果:")
    # print(json.dumps(result, ensure_ascii=False, indent=2))


def main():
    """运行所有测试"""
    print("\n" + "=" * 50)
    print("微信公众号文章读取 Skill - 测试套件")
    print("=" * 50 + "\n")

    try:
        test_url_extraction()
        test_wechat_url_detection()
        test_html_parsing()
        test_handle_function()

        print("=" * 50)
        print("✓ 所有测试完成！")
        print("=" * 50)

    except Exception as e:
        print(f"\n✗ 测试过程中出错: {str(e)}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()


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