Skill: 2720480371/wechat-look (version 1.0.1)

--- name: wechat-look-ocr description: 读取微信公众号文章的专用工具，支持OCR文字识别。自动规范化URL并提取文章内容，识别图片中的中英文文字。 runtime: python requirements: - python >= 3.7 - nodejs >= 18 - npm dependencies: - requests - subprocess - json - pathlib install_steps: - cd ocr_node && npm install --- # WeChat Look OCR - 微信文章阅读工具（支持OCR） ## 📋 安装要求 ### 系统要求 - **Python**: 3.7 或更高版本 - **Node.js**: 18 或更高版本 - **npm**: Node.js 包管理器 ### 安装步骤 1. **安装系统依赖**： ```bash # Ubuntu/Debian sudo apt install python3 nodejs npm # macOS brew install python node # Windows # 从 https://nodejs.org/ 下载 Node.js # 从 https://python.org/ 下载 Python ``` 2. **安装技能**： ```bash openclaw skill install wechat-look-ocr ``` 3. **安装 Node.js 依赖**： ```bash cd ~/.openclaw/skills/wechat-look-ocr/ocr_node npm install ``` ## 功能特性 - **自动URL规范化**: 自动添加`?scene=1`参数绕过验证码 - **内容提取**: 从HTML中提取纯文本内容 - **OCR文字识别**: 自动识别图片中的中英文文字 - **中英文支持**: 支持中文简体和英文OCR识别 - **智能回退**: 中英文识别失败时自动回退到英文 - **错误处理**: 友好的错误提示和重试机制 - **安全合规**: 遵守OpenClaw安全规范，标记外部内容为未信任源 ## 使用方法 在OpenClaw中直接使用： ``` 读取微信文章 https://mp.weixin.qq.com/s/xxx ``` ## URL规范化规则 - 无查询参数 → 添加 `?scene=1` - 有查询参数 → 确保包含 `scene=1` (覆盖重复参数) ## 🔧 技术实现 ### 系统架构 该技能采用 Python + Node.js 混合架构： - **Python 层**：处理 URL 规范化、HTML 内容提取、图片 URL 提取 - **Node.js 层**：运行 OCR 识别（使用 Tesseract.js） - **通信方式**：Python 通过 subprocess 启动 Node.js 脚本 ### 实现原理 1. **URL检测**: 检查是否为微信文章URL 2. **参数规范化**: 添加或更新`scene=1`参数 3. **内容获取**: 使用 requests 库获取页面内容 4. **文本提取**: 从HTML中提取纯文本内容 5. **图片处理**: 提取所有图片URL 6. **OCR识别**: 启动 Node.js 子进程进行文字识别 7. **结果整合**: 合并文本内容和OCR结果 8. **结果返回**: 提供结构化响应 ## 📦 依赖详情 ### Python 依赖 - `requests` - HTTP 请求库 - `subprocess` - 启动 Node.js 进程 - `json` - JSON 数据处理 - `pathlib` - 路径操作 - `re` - 正则表达式 ### Node.js 依赖 - `tesseract.js` - OCR 识别引擎 - `node-fetch` - HTTP 请求库 ### 运行时行为 该技能在运行时会： 1. 向微信服务器发送 HTTP 请求获取文章页面 2. 下载文章中的图片用于 OCR 识别 3. 启动本地 Node.js 进程进行文字识别 4. 返回整合的文本和 OCR 结果 ## 示例输出 ```json { "title": "文章标题", "author": "作者名", "text_content": "提取的正文内容", "image_count": 5, "ocr_text": "[图片1] 识别的文字内容...", "url": "规范化后的URL", "status": "success" } ``` ## 注意事项 - 仅支持微信公众号文章链接 - 遵守微信访问频率限制 - 外部内容标记为未信任源 - 如遇到验证码问题，请确保URL正确包含`scene=1`

path: README.md
content: # WeChat Look - 微信文章阅读工具

一个专门为 OpenClaw 设计的微信文章读取技能，能够自动处理URL规范化并提取文章内容。

## 快速开始

### 安装

```bash
# 从本地目录安装
openclaw skill install ./skills/wechat-look

# 使用完整路径
openclaw skill install ~/.openclaw/workspace/skills/wechat-look
```

### 使用示例

在 OpenClaw 对话中：

```
读取微信文章 https://mp.weixin.qq.com/s/D7vSSNGCQFT4NAdY6loPWA
```

或者：

```
帮我读这篇文章：https://mp.weixin.qq.com/s/S3AF2BKqRYcxHaI8uZ71BA
```

## 功能特性

✅ **智能URL处理**
- 自动检测微信文章URL
- 自动添加`scene=1`参数绕过验证码
- 正确处理现有查询参数

✅ **内容提取**
- 从HTML中提取纯文本内容
- 保留文章标题和作者信息
- 提供结构化输出格式

✅ **错误处理**
- 友好的错误提示
- 网络异常重试机制
- 无效URL检测

## 技术实现

### Python 核心逻辑

```python
def normalize_wechat_url(url):
    """规范化微信文章URL"""
    if not url.startswith('https://mp.weixin.qq.com/s/'):
        return url
    
    from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
    
    parsed = urlparse(url)
    params = parse_qs(parsed.query)
    params['scene'] = ['1']
    
    new_query = urlencode(params, doseq=True)
    new_url = urlunparse((
        parsed.scheme, parsed.netloc, parsed.path,
        parsed.params, new_query, parsed.fragment
    ))
    
    return new_url
```

### 工作流程

1. **输入验证**: 检查URL格式
2. **URL规范化**: 添加`scene=1`参数
3. **内容获取**: 使用浏览器工具读取页面
4. **文本提取**: 从HTML中提取正文内容
5. **结果格式化**: 返回结构化数据

## 测试用例

```python
# 测试用例
test_urls = [
    "https://mp.weixin.qq.com/s/D7vSSNGCQFT4NAdY6loPWA",
    "https://mp.weixin.qq.com/s/S3AF2BKqRYcxHaI8uZ71BA?param=value",
    "https://example.com/not-wechat"  # 非微信URL
]

# 预期结果
# URL 1 -> https://mp.weixin.qq.com/s/D7vSSNGCQFT4NAdY6loPWA?scene=1
# URL 2 -> https://mp.weixin.qq.com/s/S3AF2BKqRYcxHaI8uZ71BA?param=value&scene=1
# URL 3 -> https://example.com/not-wechat (保持不变)
```

## 注意事项

⚠️ **访问限制**: 微信有访问频率限制，请合理使用

⚠️ **安全规范**: 所有外部内容标记为未信任源

⚠️ **验证码**: 确保URL包含正确的`scene=1`参数

## 故障排除

### Q: 遇到验证码怎么办？
A: 确保URL正确包含`scene=1`参数，格式为`?scene=1`而不是`&scene=1`

### Q: 无法获取文章内容？
A: 检查网络连接，确认URL有效，等待页面完全加载

### Q: 提取的内容不完整？
A: 某些文章内容可能需要JavaScript渲染，当前版本可能无法完全捕获动态内容

## 贡献指南

欢迎提交Issue和Pull Request来改进这个技能！

1. 遵循OpenClaw技能开发规范
2. 添加适当的测试用例
3. 更新文档和示例

path: _meta.json
content: {
  "ownerId": "2720480371",
  "slug": "wechat-look-ocr",
  "version": "1.0.1",
  "publishedAt": 1775803200000,
  "name": "WeChat Look OCR",
  "description": "微信文章阅读工具，支持OCR文字识别",
  "requirements": {
    "python": ">=3.7",
    "nodejs": ">=18",
    "npm": true
  },
  "dependencies": {
    "python": ["requests"],
    "nodejs": ["tesseract.js", "node-fetch"]
  },
  "installSteps": [
    "cd ocr_node && npm install"
  ]
}

path: ocr_node/high_performance_ocr.js
content: const Tesseract = require('tesseract.js');
const sharp = require('sharp');

class HighPerformanceOCR {
  constructor(options = {}) {
    this.options = {
      language: options.language || 'eng+chi_sim',
      maxImageSize: options.maxImageSize || 2000000, // 2MP
      enhance: options.enhance !== false,
      verbose: options.verbose || false
    };
    this.worker = null;
  }

  async initialize() {
    if (!this.worker) {
      this.worker = await Tesseract.createWorker(this.options.language);
    }
    return this.worker;
  }

  async recognize(imageBuffer, options = {}) {
    const startTime = Date.now();
    
    try {
      // 确保worker已初始化
      await this.initialize();
      
      // 图像预处理
      const processedBuffer = await this.preprocessImage(imageBuffer, {
        ...this.options,
        ...options
      });
      
      // OCR识别
      const result = await this.worker.recognize(processedBuffer, {
        logger: this.options.verbose ? m => console.log(m.status) : () => {}
      });
      
      const processingTime = Date.now() - startTime;
      
      if (this.options.verbose) {
        console.error(`OCR完成 - 耗时: ${processingTime}ms, 置信度: ${result.data.confidence}%`);
      }
      
      return {
        text: result.data.text || '',
        confidence: result.data.confidence || 0,
        processingTime,
        words: result.data.words || []
      };
      
    } catch (error) {
      console.error('OCR处理失败:', error.message);
      throw error;
    }
  }

  async preprocessImage(imageBuffer, options) {
    try {
      const metadata = await sharp(imageBuffer).metadata();
      const imagePixels = metadata.width * metadata.height;
      
      // 智能尺寸优化
      let resizeOptions = null;
      
      if (imagePixels > options.maxImageSize) {
        const scale = Math.sqrt(options.maxImageSize / imagePixels);
        resizeOptions = {
          width: Math.round(metadata.width * scale),
          height: Math.round(metadata.height * scale)
        };
      }
      
      // 图像处理流水线
      let pipeline = sharp(imageBuffer);
      
      if (resizeOptions) {
        pipeline = pipeline.resize(resizeOptions);
      }
      
      if (options.enhance) {
        pipeline = pipeline
          .modulate({ brightness: 1.05 })
          .normalize()
          .sharpen(0.3);
      }
      
      return pipeline
        .jpeg({ quality: 85, mozjpeg: true })
        .toBuffer();
        
    } catch (error) {
      console.error('图像预处理失败:', error.message);
      // 如果预处理失败，返回原始图像
      return imageBuffer;
    }
  }

  async terminate() {
    if (this.worker) {
      await this.worker.terminate();
      this.worker = null;
    }
  }
}

module.exports = { HighPerformanceOCR };

path: ocr_node/high_performance_ocr_simple.js
content: const Tesseract = require('tesseract.js');

class HighPerformanceOCR {
  constructor(options = {}) {
    this.options = {
      language: options.language || 'eng+chi_sim',
      verbose: options.verbose || false
    };
    this.worker = null;
  }

  async initialize() {
    if (!this.worker) {
      this.worker = await Tesseract.createWorker();
      await this.worker.loadLanguage(this.options.language);
      await this.worker.initialize(this.options.language);
    }
    return this.worker;
  }

  async recognize(imageBuffer) {
    await this.initialize();
    const result = await this.worker.recognize(imageBuffer, {
      logger: this.options.verbose ? m => console.log(m) : () => {}
    });
    return { text: result.data.text || '' };
  }

  async terminate() {
    if (this.worker) {
      await this.worker.terminate();
      this.worker = null;
    }
  }
}

module.exports = { HighPerformanceOCR };


path: ocr_node/ocr.js
content: #!/usr/bin/env node
/**
 * ocr.js - 使用 HighPerformanceOCR (Tesseract.js) 进行OCR识别
 * 用法: node ocr.js <image_url>
 * 输出: JSON 格式的识别结果
 */

const { HighPerformanceOCR } = require('./high_performance_ocr_simple');
const fetch = require('node-fetch');
const path = require('path');

async function downloadImage(imageUrl) {
  try {
    const response = await fetch(imageUrl, {
      timeout: 10000,
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; wechat-look-ocr/1.0)'
      }
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.buffer();
  } catch (error) {
    throw new Error(`下载图片失败: ${error.message}`);
  }
}

async function runOCR(imageUrl, options = {}) {
  let ocr = null;
  
  try {
    // 下载图片
    console.error(`开始下载图片: ${imageUrl}`);
    const imageBuffer = await downloadImage(imageUrl);
    
    // 初始化OCR
    ocr = new HighPerformanceOCR({
      language: options.language || 'eng+chi_sim',
      verbose: options.verbose || false,
      enhance: true
    });
    
    // 执行OCR
    console.error('开始OCR识别...');
    const result = await ocr.recognize(imageBuffer, options);
    
    // 输出JSON结果
    const output = {
      text: result.text.trim(),
      confidence: result.confidence,
      processingTime: result.processingTime,
      wordCount: result.text.trim().length
    };
    
    console.log(JSON.stringify(output, null, 2));
    
  } catch (error) {
    const errorOutput = {
      error: error.message,
      text: '',
      confidence: 0,
      processingTime: 0,
      wordCount: 0
    };
    
    console.log(JSON.stringify(errorOutput, null, 2));
    process.exit(1);
    
  } finally {
    if (ocr) {
      await ocr.terminate();
    }
  }
}

// 主程序
const args = process.argv.slice(2);

if (args.length === 0) {
  console.log(JSON.stringify({
    error: '请提供图片URL作为参数',
    usage: 'node ocr.js <image_url> [language]',
    example: 'node ocr.js https://example.com/image.jpg chi_sim',
    text: '',
    confidence: 0,
    processingTime: 0,
    wordCount: 0
  }, null, 2));
  process.exit(1);
}

const imageUrl = args[0];
const language = args[1] || 'eng+chi_sim';

runOCR(imageUrl, { language, verbose: true }).catch(error => {
  console.error('程序执行失败:', error.message);
  process.exit(1);
});

path: ocr_node/ocr_eng_only.js
content: #!/usr/bin/env node
/**
 * ocr_eng_only.js - 仅使用英文语言包，避免网络下载
 */

const Tesseract = require('tesseract.js');
const fetch = require('node-fetch');

async function downloadImage(imageUrl) {
  try {
    const response = await fetch(imageUrl, {
      timeout: 10000,
      headers: {
        'User-Agent': 'Mozilla/5.0 (compatible; wechat-look-ocr/1.0)'
      }
    });
    
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return await response.buffer();
  } catch (error) {
    throw new Error(`下载图片失败: ${error.message}`);
  }
}

async function runOCR(imageUrl) {
  const startTime = Date.now();
  
  try {
    console.error(`开始处理: ${imageUrl}`);
    
    // 下载图片
    const imageBuffer = await downloadImage(imageUrl);
    
    console.error('开始OCR识别...');
    
    // 仅使用英文，避免下载中文语言包
    const result = await Tesseract.recognize(imageBuffer, 'eng');
    
    const processingTime = Date.now() - startTime;
    
    const output = {
      text: result.data.text.trim(),
      confidence: result.data.confidence,
      wordCount: result.data.text.trim().length,
      processingTime: processingTime
    };
    
    console.log(JSON.stringify(output, null, 2));
    
  } catch (error) {
    const errorOutput = {
      error: error.message,
      text: '',
      confidence: 0,
      wordCount: 0,
      processingTime: Date.now() - startTime
    };
    
    console.log(JSON.stringify(errorOutput, null, 2));
    process.exit(1);
  }
}

// 主程序
const args = process.argv.slice(2);

if (args.length === 0) {
  console.log(JSON.stringify({
    error: '请提供图片URL',
    usage: 'node ocr_eng_only.js <image_url>',
    text: '',
    confidence: 0,
    wordCount: 0,
    processingTime: 0
  }, null, 2));
  process.exit(1);
}

runOCR(args[0]).catch(error => {
  console.error('程序执行失败:', error.message);
  process.exit(1);
});

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