Skill: anydebug/ai-news-publisher (version 1.0.0)

--- name: ai-news-publisher description: AI新闻一键发布技能。从36氪/虎嗅获取AI新闻，改写成爆款文章，发布到微信公众号。触发词：发布AI新闻、公众号发文章、科技新闻发布、爆款文章。 --- # AI新闻一键发布 一键从科技媒体获取AI新闻，改写成爆款文章，发布到微信公众号草稿箱。 完全独立，不依赖任何第三方技能。 ## 目录结构 - `scripts/`: 包含微信发布脚本(`publish.sh`)、环境配置(`setup.sh`)和新闻抓取脚本(`scrape_news.py`)。 - `assets/`: 默认封面图(`default-cover.jpg`)。 - `references/`: 包含详细的写作指南和Scrapling抓取/解析说明。 ## 环境准备与依赖 - 依赖：确保已安装 `scrapling` (Python) 和 `@wenyan-md/cli` (Node.js)。 - 凭证：环境变量须包含 `WECHAT_APP_ID` 和 `WECHAT_APP_SECRET`（或通过 `source scripts/setup.sh` 加载）。 - 权限：确保当前服务器IP（可通过 `curl ifconfig.me` 获取）已加入公众号开发基本配置的白名单。 ## 完整工作流程 当触发本技能时，请严格按以下步骤执行： 1. **抓取新闻列表** 使用 `scrapling` 获取 36氪 或 虎嗅 的AI频道新闻列表： ```bash scrapling extract get "https://www.36kr.com/information/AI/" /tmp/ai-news.md ``` *(注：如果遇到反爬拦截，可使用 `fetch --network-idle` 或 `stealthy-fetch`)* 2. **去重与选题策划** - 提取最新10条新闻的标题、摘要、链接。 - 读取已发布记录：`cat memory/selected-topics.md`，排除近3天已发布或相似的主题。 - 选出1条最具爆款潜质的新闻（热点性、争议性、情绪价值等）。 3. **抓取原文与改写** - 抓取选中新闻的原文内容。 - **极其重要**：在改写之前，请务必阅读 [references/writing-guide.md](references/writing-guide.md) 获取爆款标题公式、结构模板和行文节奏指南。 - 按照指南，改写为 1000-2000 字的高质量文章，包含钩子、痛点、分析、互动引导和金句。 4. **格式化文章** 生成包含必需 Frontmatter 的 Markdown 文件： ```markdown --- title: 爆款标题（≤30字） cover: /root/.openclaw/workspace/skills/ai-news-publisher/assets/default-cover.jpg --- # 正文开始... ``` *(注意：封面必须是绝对路径，正文段落宜简短并适当加粗)* 5. **发布到草稿箱** 使用专用脚本发布（推荐使用 lapis 主题和 solarized-light 代码高亮）： ```bash /root/.openclaw/workspace/skills/ai-news-publisher/scripts/publish.sh /tmp/final-article.md lapis solarized-light ``` 6. **记录选题与收尾** - 记录本次发布： ```bash echo "- $(date +%H:%M) | 文章标题 | 原文链接" >> memory/selected-topics.md ``` - 向用户返回文章草稿箱链接及预览信息。 ## 错误处理 - **抓取失败**：依次降级尝试 `fetch` -> `stealthy-fetch` -> 更换新闻源。 - **发布报错 (ip not in whitelist)**：提示用户将服务器公网IP加白。 - **发布报错 (title/cover missing)**：检查 Markdown 头部是否严格按照要求包含了 `title` 和 `cover` 字段。 ## 详细指南（按需参考） 在使用此技能时，遇到具体细节问题请查阅以下参考文件： - [writing-guide.md](references/writing-guide.md) - **写作必读**：爆款文章结构、标题公式、金句模板及排版要求。 - [scrapling-fetching.md](references/scrapling-fetching.md) - 抓取指南：命令用法、隐身模式、代理配置。 - [scrapling-parsing.md](references/scrapling-parsing.md) - 解析指南：CSS/XPath选择器用法与API。

path: references/.ipynb_checkpoints/scrapling-fetching-checkpoint.md
content: # Scrapling 抓取指南

## 1. CLI 命令速查

| 命令 | 用途 | 适用场景 |
|-----|------|---------|
| `get` | HTTP GET 请求 | 静态页面、博客、新闻 |
| `fetch` | 浏览器模式 | 动态内容、SPA |
| `stealthy-fetch` | 隐身浏览器 | 反爬保护、Cloudflare |

**常用参数**：
- `--timeout 60` : 超时秒数
- `--network-idle` : 等待网络空闲
- `--wait 2000` : 额外等待毫秒
- `-s ".selector"` : CSS 选择器
- `--wait-selector ".loaded"` : 等待元素出现
- `--proxy "http://host:port"` : 使用代理

## 2. CLI 抓取示例

**抓取 36氪 AI 频道**：
```bash
# 方式 1: 简单请求（适合静态页面）
scrapling extract get "https://www.36kr.com/information/AI/" /tmp/36kr-ai.md

# 方式 2: 浏览器模式（适合动态内容）
scrapling extract fetch "https://www.36kr.com/information/AI/" /tmp/36kr-ai.md --network-idle

# 方式 3: 隐身模式（适合反爬保护）
scrapling extract stealthy-fetch "https://www.36kr.com/information/AI/" /tmp/36kr-ai.md
```

**抓取虎嗅 AI 相关**：
```bash
scrapling extract get "https://www.huxiu.com/channel/106.html" /tmp/huxiu-ai.md
```

**使用 CSS 选择器提取特定内容**：
```bash
# 只提取文章列表
scrapling extract get "https://www.36kr.com/information/AI/" /tmp/articles.md -s ".article-item"
```

## 3. Python 脚本抓取 (复杂场景)

对于复杂抓取，使用 `scripts/scrape_news.py`：

```python
from scrapling.fetchers import Fetcher, StealthyFetcher

# 简单抓取
page = Fetcher.get('https://www.36kr.com/information/AI/')
titles = page.css('.article-item .title::text').getall()

# 隐身抓取（绕过反爬）
page = StealthyFetcher.fetch('https://www.36kr.com/information/AI/', headless=True)
articles = page.css('.article-item')

for article in articles:
    title = article.css('.title::text').get()
    link = article.css('a::attr(href)').get()
    print(f"{title}: {link}")
```

path: references/.ipynb_checkpoints/scrapling-parsing-checkpoint.md
content: # Scrapling 解析 API 速查

在使用 Python 脚本提取网页内容时，参考以下常用的解析方法。

## 1. CSS 选择器
```python
# 获取所有匹配的文本
page.css('.quote .text::text').getall()

# 获取第一个匹配的文本
page.css('.quote')[0].css('.text::text').get()

# 获取属性
page.css('a::attr(href)').get()
```

## 2. XPath
```python
page.xpath('//div[@class="quote"]/span/text()').getall()
```

## 3. BeautifulSoup 风格
```python
# 查找所有匹配的元素
page.find_all('div', class_='quote')
page.find_all('div', {'class': 'quote'})

# 按文本查找
page.find_by_text('quote', tag='div')
```

## 4. 元素导航
```python
element.parent           # 父元素
element.next_sibling     # 下一个兄弟元素
element.below_elements() # 下方的元素
element.find_similar()   # 查找相似结构的元素
```

path: references/.ipynb_checkpoints/writing-guide-checkpoint.md
content: # 爆款写作与选题指南

## 1. 选题策划（从 10 选 1）
从每日新闻中选出**1个最爆款**，评估标准如下：
1. **热点性** - 关联当前热点/趋势
2. **争议性** - 有观点冲突
3. **实用性** - 解决读者痛点
4. **新鲜感** - 独家/首发/新视角
5. **情绪价值** - 激发好奇/焦虑/希望

## 2. 爆款标题公式
| 类型 | 示例 |
|-----|------|
| 数字+痛点 | 《AI淘汰了50%程序员，这3个技能让你翻盘》 |
| 对比反差 | 《月薪3千和3万，差的不是技术》 |
| 悬念好奇 | 《OpenAI深夜发布，硅谷炸了》 |
| 利益承诺 | 《学会这个AI工具，效率提升10倍》 |
| 情绪触发 | 《被AI取代的第1天，我哭了》 |

**标题自检清单**：
- [ ] 有具体数字？
- [ ] 触发情绪？
- [ ] 制造好奇？
- [ ] 承诺价值？
- [ ] 30字以内？

## 3. 开头钩子（3秒定生死）
- **扎心问题**：你有没有想过，为什么你的AI文章没人看？
- **意外故事**：昨天，一个程序员朋友被裁员了。不是因为他技术不行，而是因为他不会用AI。
- **惊人数据**：ChatGPT上线2个月，用户破亿。TikTok用了9个月，Instagram用了2年半。
- **认知反差**：很多人以为AI会让程序员失业。错。AI会让不会用AI的程序员失业。

## 4. 内容节奏
**黄金结构**：钩子 → 痛点 → 分析 → 方案 → 行动

**节奏控制**：
- 每 300 字一个小高潮（新观点/新数据/小故事/新问题）
- 每段结尾留悬念
- 适时插入金句

**金句示例**：
- 💡 记住：工具不会淘汰人，会工具的人淘汰不会工具的人。
- ⚡ AI不是威胁，是杠杆。
- 📌 不是AI替代你，是用AI的人替代你。

## 5. 结尾行动
- **互动引导**：你怎么看？评论区聊聊。有收获？点个"在看"让更多人看到。
- **行动号召**：现在就去试试，3分钟搞定。收藏这篇文章，明天就能用上。

## 6. 常用结构模板
**热点事件类**：
1. 开头：事件概述（3句话）
2. 痛点：这个事件对你意味着什么
3. 分析：为什么发生，背后逻辑
4. 影响：对你的影响（利/弊）
5. 行动：你应该怎么做
6. 结尾：金句总结 + 互动引导

**新功能/产品类**：
1. 开头：产品/功能介绍
2. 痛点：解决什么问题
3. 功能：具体怎么用
4. 价值：能帮你做什么
5. 案例：真实使用场景
6. 行动：如何获取
7. 结尾：鼓励尝试 + 互动

**趋势分析类**：
1. 开头：趋势现象描述
2. 数据：支撑数据/证据
3. 分析：为什么会这样
4. 判断：未来会怎样
5. 建议：你应该怎么做
6. 结尾：引发思考 + 互动

path: references/scrapling-fetching.md
content: # Scrapling 抓取指南

## 1. CLI 命令速查

| 命令 | 用途 | 适用场景 |
|-----|------|---------|
| `get` | HTTP GET 请求 | 静态页面、博客、新闻 |
| `fetch` | 浏览器模式 | 动态内容、SPA |
| `stealthy-fetch` | 隐身浏览器 | 反爬保护、Cloudflare |

**常用参数**：
- `--timeout 60` : 超时秒数
- `--network-idle` : 等待网络空闲
- `--wait 2000` : 额外等待毫秒
- `-s ".selector"` : CSS 选择器
- `--wait-selector ".loaded"` : 等待元素出现
- `--proxy "http://host:port"` : 使用代理

## 2. CLI 抓取示例

**抓取 36氪 AI 频道**：
```bash
# 方式 1: 简单请求（适合静态页面）
scrapling extract get "https://www.36kr.com/information/AI/" /tmp/36kr-ai.md

# 方式 2: 浏览器模式（适合动态内容）
scrapling extract fetch "https://www.36kr.com/information/AI/" /tmp/36kr-ai.md --network-idle

# 方式 3: 隐身模式（适合反爬保护）
scrapling extract stealthy-fetch "https://www.36kr.com/information/AI/" /tmp/36kr-ai.md
```

**抓取虎嗅 AI 相关**：
```bash
scrapling extract get "https://www.huxiu.com/channel/106.html" /tmp/huxiu-ai.md
```

**使用 CSS 选择器提取特定内容**：
```bash
# 只提取文章列表
scrapling extract get "https://www.36kr.com/information/AI/" /tmp/articles.md -s ".article-item"
```

## 3. Python 脚本抓取 (复杂场景)

对于复杂抓取，使用 `scripts/scrape_news.py`：

```python
from scrapling.fetchers import Fetcher, StealthyFetcher

# 简单抓取
page = Fetcher.get('https://www.36kr.com/information/AI/')
titles = page.css('.article-item .title::text').getall()

# 隐身抓取（绕过反爬）
page = StealthyFetcher.fetch('https://www.36kr.com/information/AI/', headless=True)
articles = page.css('.article-item')

for article in articles:
    title = article.css('.title::text').get()
    link = article.css('a::attr(href)').get()
    print(f"{title}: {link}")
```

path: references/scrapling-parsing.md
content: # Scrapling 解析 API 速查

在使用 Python 脚本提取网页内容时，参考以下常用的解析方法。

## 1. CSS 选择器
```python
# 获取所有匹配的文本
page.css('.quote .text::text').getall()

# 获取第一个匹配的文本
page.css('.quote')[0].css('.text::text').get()

# 获取属性
page.css('a::attr(href)').get()
```

## 2. XPath
```python
page.xpath('//div[@class="quote"]/span/text()').getall()
```

## 3. BeautifulSoup 风格
```python
# 查找所有匹配的元素
page.find_all('div', class_='quote')
page.find_all('div', {'class': 'quote'})

# 按文本查找
page.find_by_text('quote', tag='div')
```

## 4. 元素导航
```python
element.parent           # 父元素
element.next_sibling     # 下一个兄弟元素
element.below_elements() # 下方的元素
element.find_similar()   # 查找相似结构的元素
```

path: references/writing-guide.md
content: # 爆款写作与选题指南

## 1. 选题策划（从 10 选 1）
从每日新闻中选出**1个最爆款**，评估标准如下：
1. **热点性** - 关联当前热点/趋势
2. **争议性** - 有观点冲突
3. **实用性** - 解决读者痛点
4. **新鲜感** - 独家/首发/新视角
5. **情绪价值** - 激发好奇/焦虑/希望

## 2. 爆款标题公式
| 类型 | 示例 |
|-----|------|
| 数字+痛点 | 《AI淘汰了50%程序员，这3个技能让你翻盘》 |
| 对比反差 | 《月薪3千和3万，差的不是技术》 |
| 悬念好奇 | 《OpenAI深夜发布，硅谷炸了》 |
| 利益承诺 | 《学会这个AI工具，效率提升10倍》 |
| 情绪触发 | 《被AI取代的第1天，我哭了》 |

**标题自检清单**：
- [ ] 有具体数字？
- [ ] 触发情绪？
- [ ] 制造好奇？
- [ ] 承诺价值？
- [ ] 30字以内？

## 3. 开头钩子（3秒定生死）
- **扎心问题**：你有没有想过，为什么你的AI文章没人看？
- **意外故事**：昨天，一个程序员朋友被裁员了。不是因为他技术不行，而是因为他不会用AI。
- **惊人数据**：ChatGPT上线2个月，用户破亿。TikTok用了9个月，Instagram用了2年半。
- **认知反差**：很多人以为AI会让程序员失业。错。AI会让不会用AI的程序员失业。

## 4. 内容节奏
**黄金结构**：钩子 → 痛点 → 分析 → 方案 → 行动

**节奏控制**：
- 每 300 字一个小高潮（新观点/新数据/小故事/新问题）
- 每段结尾留悬念
- 适时插入金句

**金句示例**：
- 💡 记住：工具不会淘汰人，会工具的人淘汰不会工具的人。
- ⚡ AI不是威胁，是杠杆。
- 📌 不是AI替代你，是用AI的人替代你。

## 5. 结尾行动
- **互动引导**：你怎么看？评论区聊聊。有收获？点个"在看"让更多人看到。
- **行动号召**：现在就去试试，3分钟搞定。收藏这篇文章，明天就能用上。

## 6. 常用结构模板
**热点事件类**：
1. 开头：事件概述（3句话）
2. 痛点：这个事件对你意味着什么
3. 分析：为什么发生，背后逻辑
4. 影响：对你的影响（利/弊）
5. 行动：你应该怎么做
6. 结尾：金句总结 + 互动引导

**新功能/产品类**：
1. 开头：产品/功能介绍
2. 痛点：解决什么问题
3. 功能：具体怎么用
4. 价值：能帮你做什么
5. 案例：真实使用场景
6. 行动：如何获取
7. 结尾：鼓励尝试 + 互动

**趋势分析类**：
1. 开头：趋势现象描述
2. 数据：支撑数据/证据
3. 分析：为什么会这样
4. 判断：未来会怎样
5. 建议：你应该怎么做
6. 结尾：引发思考 + 互动

path: scripts/.ipynb_checkpoints/publish-checkpoint.sh
content: #!/usr/bin/env bash
# ai-news-publisher: 发布 Markdown 到微信公众号草稿箱
# Usage: ./publish.sh <markdown-file> [theme] [highlight]

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# 技能目录
SKILL_DIR="/root/.openclaw/workspace/skills/ai-news-publisher"
DEFAULT_COVER="$SKILL_DIR/assets/default-cover.jpg"
DEFAULT_THEME="lapis"
DEFAULT_HIGHLIGHT="solarized-light"
TOOLS_MD="$HOME/.openclaw/workspace/TOOLS.md"

# 检查 wenyan-cli 是否安装
check_wenyan() {
    if ! command -v wenyan &> /dev/null; then
        echo -e "${YELLOW}wenyan-cli 未安装，正在安装...${NC}"
        npm install -g @wenyan-md/cli
        if [ $? -eq 0 ]; then
            echo -e "${GREEN}✅ wenyan-cli 安装成功${NC}"
        else
            echo -e "${RED}❌ 安装失败，请手动运行: npm install -g @wenyan-md/cli${NC}"
            exit 1
        fi
    fi
}

# 从 TOOLS.md 读取环境变量
load_credentials() {
    if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
        if [ -f "$TOOLS_MD" ]; then
            echo -e "${YELLOW}从 TOOLS.md 读取凭证...${NC}"
            export WECHAT_APP_ID=$(grep "export WECHAT_APP_ID=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_ID=//' | tr -d ' ')
            export WECHAT_APP_SECRET=$(grep "export WECHAT_APP_SECRET=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_SECRET=//' | tr -d ' ')
        fi
    fi
}

# 检查环境变量
check_env() {
    load_credentials
    
    if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
        echo -e "${RED}❌ 环境变量未设置！${NC}"
        echo ""
        echo -e "${YELLOW}请在 TOOLS.md 中添加微信公众号凭证：${NC}"
        echo ""
        echo "  ## 🔐 WeChat Official Account"
        echo "  export WECHAT_APP_ID=your_app_id"
        echo "  export WECHAT_APP_SECRET=your_app_secret"
        echo ""
        echo -e "${YELLOW}或者运行：${NC}"
        echo "  source $SKILL_DIR/scripts/setup.sh"
        exit 1
    fi
}

# 检查文件
check_file() {
    local file="$1"
    if [ ! -f "$file" ]; then
        echo -e "${RED}❌ 文件不存在: $file${NC}"
        exit 1
    fi
}

# 添加默认封面
add_cover() {
    local file="$1"
    if ! grep -q "^cover:" "$file"; then
        echo -e "${YELLOW}添加默认封面...${NC}"
        sed -i "/^title:/a cover: $DEFAULT_COVER" "$file"
    fi
}

# 发布函数
publish() {
    local file="$1"
    local theme="${2:-$DEFAULT_THEME}"
    local highlight="${3:-$DEFAULT_HIGHLIGHT}"
    
    echo -e "${GREEN}📝 准备发布文章...${NC}"
    echo "  文件: $file"
    echo "  主题: $theme"
    echo "  代码高亮: $highlight"
    echo ""
    
    # 执行发布
    wenyan publish -f "$file" -t "$theme" -h "$highlight"
    
    if [ $? -eq 0 ]; then
        echo ""
        echo -e "${GREEN}✅ 发布成功！${NC}"
        echo -e "${YELLOW}📱 请前往公众号后台草稿箱查看：${NC}"
        echo "  https://mp.weixin.qq.com/"
    else
        echo ""
        echo -e "${RED}❌ 发布失败！${NC}"
        echo -e "${YELLOW}💡 常见问题：${NC}"
        echo "  1. IP 未在白名单 → 运行 'curl ifconfig.me' 获取 IP，添加到公众号后台"
        echo "  2. Frontmatter 缺失 → 文件顶部添加 title + cover"
        echo "  3. API 凭证错误 → 检查 TOOLS.md 中的凭证"
        exit 1
    fi
}

# 显示帮助
show_help() {
    echo "Usage: $0 <markdown-file> [theme] [highlight]"
    echo ""
    echo "Examples:"
    echo "  $0 article.md"
    echo "  $0 article.md lapis"
    echo "  $0 article.md lapis solarized-light"
    echo ""
    echo "Available themes:"
    echo "  default, lapis, phycat, ..."
    echo "  Run 'wenyan theme -l' to see all"
    echo ""
    echo "Available highlights:"
    echo "  atom-one-dark, atom-one-light, dracula, github-dark, github,"
    echo "  monokai, solarized-dark, solarized-light, xcode"
}

# 主函数
main() {
    if [ $# -eq 0 ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
        show_help
        exit 0
    fi
    
    local file="$1"
    local theme="$2"
    local highlight="$3"
    
    check_wenyan
    check_env
    check_file "$file"
    add_cover "$file"
    publish "$file" "$theme" "$highlight"
}

main "$@"


path: scripts/.ipynb_checkpoints/scrape_news-checkpoint.py
content: #!/usr/bin/env python3
"""
新闻抓取脚本
使用 Scrapling 抓取科技新闻

Usage:
    python scrape_news.py --source 36kr --output /tmp/news.json
    python scrape_news.py --source huxiu --count 10
"""

import argparse
import json
import sys
from datetime import datetime

try:
    from scrapling.fetchers import Fetcher, StealthyFetcher
except ImportError:
    print("❌ Scrapling 未安装，请运行: pip install 'scrapling[all]>=0.4.2'")
    sys.exit(1)

# 新闻源配置
SOURCES = {
    "36kr": {
        "url": "https://www.36kr.com/information/AI/",
        # 使用 a 标签提取，匹配 /p/ 开头的文章链接
        "link_pattern": "/p/",
    },
    "huxiu": {
        "url": "https://www.huxiu.com/channel/106.html",
        "link_pattern": "/article/",
    },
}


def scrape_36kr(count=10, use_stealth=False):
    """抓取 36氪 AI 频道"""
    config = SOURCES["36kr"]
    
    print(f"📡 正在抓取 36氪 AI 频道...")
    
    if use_stealth:
        page = StealthyFetcher.fetch(config["url"], headless=True)
    else:
        page = Fetcher.get(config["url"], stealthy_headers=True)
    
    # 提取所有 /p/ 开头的链接
    link_pattern = config["link_pattern"]
    all_links = page.css(f'a[href*="{link_pattern}"]')
    
    seen = set()
    results = []
    for link in all_links:
        href = link.attrib.get('href', '')
        title = link.css('::text').get() or ''
        
        # 过滤条件
        if not href or not title:
            continue
        if href in seen:
            continue
        if len(title.strip()) < 5:  # 过滤太短的标题
            continue
            
        seen.add(href)
        
        # 补全链接
        if href.startswith("/"):
            full_url = f"https://www.36kr.com{href}"
        else:
            full_url = href
        
        results.append({
            "title": title.strip(),
            "link": full_url,
            "summary": "",
            "source": "36kr",
            "scraped_at": datetime.now().isoformat(),
        })
        
        if len(results) >= count:
            break
    
    print(f"✅ 抓取到 {len(results)} 条新闻")
    return results


def scrape_huxiu(count=10, use_stealth=False):
    """抓取虎嗅 AI 频道"""
    config = SOURCES["huxiu"]
    
    print(f"📡 正在抓取虎嗅 AI 频道...")
    
    if use_stealth:
        page = StealthyFetcher.fetch(config["url"], headless=True)
    else:
        page = Fetcher.get(config["url"], stealthy_headers=True)
    
    # 提取所有文章链接
    link_pattern = config["link_pattern"]
    all_links = page.css(f'a[href*="{link_pattern}"]')
    
    seen = set()
    results = []
    for link in all_links:
        href = link.attrib.get('href', '')
        title = link.css('::text').get() or ''
        
        if not href or not title:
            continue
        if href in seen:
            continue
        if len(title.strip()) < 5:
            continue
            
        seen.add(href)
        
        if href.startswith("/"):
            full_url = f"https://www.huxiu.com{href}"
        else:
            full_url = href
        
        results.append({
            "title": title.strip(),
            "link": full_url,
            "summary": "",
            "source": "huxiu",
            "scraped_at": datetime.now().isoformat(),
        })
        
        if len(results) >= count:
            break
    
    print(f"✅ 抓取到 {len(results)} 条新闻")
    return results


def scrape_article_content(url, use_stealth=False):
    """抓取文章正文"""
    print(f"📄 正在抓取文章: {url}")
    
    if use_stealth:
        page = StealthyFetcher.fetch(url, headless=True)
    else:
        page = Fetcher.get(url, stealthy_headers=True)
    
    # 尝试多种选择器
    content = (
        page.css("article .content::text").getall() or
        page.css(".article-content::text").getall() or
        page.css(".content::text").getall() or
        []
    )
    
    return "\n".join(content)


def main():
    parser = argparse.ArgumentParser(description="新闻抓取脚本")
    parser.add_argument("--source", choices=["36kr", "huxiu"], default="36kr", help="新闻源")
    parser.add_argument("--output", "-o", help="输出文件路径 (JSON)")
    parser.add_argument("--count", "-c", type=int, default=10, help="抓取数量")
    parser.add_argument("--stealth", action="store_true", help="使用隐身模式")
    parser.add_argument("--content", metavar="URL", help="抓取指定文章的正文")
    
    args = parser.parse_args()
    
    if args.content:
        # 抓取单篇文章
        content = scrape_article_content(args.content, args.stealth)
        print(content)
        return
    
    # 抓取新闻列表
    if args.source == "36kr":
        articles = scrape_36kr(args.count, args.stealth)
    else:
        articles = scrape_huxiu(args.count, args.stealth)
    
    # 输出
    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            json.dump(articles, f, ensure_ascii=False, indent=2)
        print(f"📝 已保存到: {args.output}")
    else:
        print(json.dumps(articles, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/.ipynb_checkpoints/setup-checkpoint.sh
content: #!/usr/bin/env bash
# setup.sh - 加载微信公众号环境变量
# Usage: source ./setup.sh

SKILL_DIR="/root/.openclaw/workspace/skills/ai-news-publisher"
TOOLS_MD="$HOME/.openclaw/workspace/TOOLS.md"

# 检查 TOOLS.md
if [ ! -f "$TOOLS_MD" ]; then
    echo "❌ 找不到 TOOLS.md: $TOOLS_MD"
    echo ""
    echo "请添加微信公众号凭证："
    echo ""
    echo "  ## 🔐 WeChat Official Account"
    echo "  export WECHAT_APP_ID=your_app_id"
    echo "  export WECHAT_APP_SECRET=your_app_secret"
    exit 1
fi

# 提取凭证
WECHAT_APP_ID=$(grep "export WECHAT_APP_ID=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_ID=//' | tr -d ' ')
WECHAT_APP_SECRET=$(grep "export WECHAT_APP_SECRET=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_SECRET=//' | tr -d ' ')

if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
    echo "❌ 无法从 TOOLS.md 读取凭证！"
    echo ""
    echo "请确保格式正确："
    echo "  export WECHAT_APP_ID=your_app_id"
    echo "  export WECHAT_APP_SECRET=your_app_secret"
    exit 1
fi

export WECHAT_APP_ID
export WECHAT_APP_SECRET

echo "✅ 微信公众号环境变量已加载！"
echo ""
echo "  WECHAT_APP_ID=${WECHAT_APP_ID:0:10}..."
echo "  WECHAT_APP_SECRET=******"


path: scripts/publish.sh
content: #!/usr/bin/env bash
# ai-news-publisher: 发布 Markdown 到微信公众号草稿箱
# Usage: ./publish.sh <markdown-file> [theme] [highlight]

set -e

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# 技能目录
SKILL_DIR="/root/.openclaw/workspace/skills/ai-news-publisher"
DEFAULT_COVER="$SKILL_DIR/assets/default-cover.jpg"
DEFAULT_THEME="lapis"
DEFAULT_HIGHLIGHT="solarized-light"
TOOLS_MD="$HOME/.openclaw/workspace/TOOLS.md"

# 检查 wenyan-cli 是否安装
check_wenyan() {
    if ! command -v wenyan &> /dev/null; then
        echo -e "${YELLOW}wenyan-cli 未安装，正在安装...${NC}"
        npm install -g @wenyan-md/cli
        if [ $? -eq 0 ]; then
            echo -e "${GREEN}✅ wenyan-cli 安装成功${NC}"
        else
            echo -e "${RED}❌ 安装失败，请手动运行: npm install -g @wenyan-md/cli${NC}"
            exit 1
        fi
    fi
}

# 从 TOOLS.md 读取环境变量
load_credentials() {
    if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
        if [ -f "$TOOLS_MD" ]; then
            echo -e "${YELLOW}从 TOOLS.md 读取凭证...${NC}"
            export WECHAT_APP_ID=$(grep "export WECHAT_APP_ID=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_ID=//' | tr -d ' ')
            export WECHAT_APP_SECRET=$(grep "export WECHAT_APP_SECRET=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_SECRET=//' | tr -d ' ')
        fi
    fi
}

# 检查环境变量
check_env() {
    load_credentials
    
    if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
        echo -e "${RED}❌ 环境变量未设置！${NC}"
        echo ""
        echo -e "${YELLOW}请在 TOOLS.md 中添加微信公众号凭证：${NC}"
        echo ""
        echo "  ## 🔐 WeChat Official Account"
        echo "  export WECHAT_APP_ID=your_app_id"
        echo "  export WECHAT_APP_SECRET=your_app_secret"
        echo ""
        echo -e "${YELLOW}或者运行：${NC}"
        echo "  source $SKILL_DIR/scripts/setup.sh"
        exit 1
    fi
}

# 检查文件
check_file() {
    local file="$1"
    if [ ! -f "$file" ]; then
        echo -e "${RED}❌ 文件不存在: $file${NC}"
        exit 1
    fi
}

# 添加默认封面
add_cover() {
    local file="$1"
    if ! grep -q "^cover:" "$file"; then
        echo -e "${YELLOW}添加默认封面...${NC}"
        sed -i "/^title:/a cover: $DEFAULT_COVER" "$file"
    fi
}

# 发布函数
publish() {
    local file="$1"
    local theme="${2:-$DEFAULT_THEME}"
    local highlight="${3:-$DEFAULT_HIGHLIGHT}"
    
    echo -e "${GREEN}📝 准备发布文章...${NC}"
    echo "  文件: $file"
    echo "  主题: $theme"
    echo "  代码高亮: $highlight"
    echo ""
    
    # 执行发布
    wenyan publish -f "$file" -t "$theme" -h "$highlight"
    
    if [ $? -eq 0 ]; then
        echo ""
        echo -e "${GREEN}✅ 发布成功！${NC}"
        echo -e "${YELLOW}📱 请前往公众号后台草稿箱查看：${NC}"
        echo "  https://mp.weixin.qq.com/"
    else
        echo ""
        echo -e "${RED}❌ 发布失败！${NC}"
        echo -e "${YELLOW}💡 常见问题：${NC}"
        echo "  1. IP 未在白名单 → 运行 'curl ifconfig.me' 获取 IP，添加到公众号后台"
        echo "  2. Frontmatter 缺失 → 文件顶部添加 title + cover"
        echo "  3. API 凭证错误 → 检查 TOOLS.md 中的凭证"
        exit 1
    fi
}

# 显示帮助
show_help() {
    echo "Usage: $0 <markdown-file> [theme] [highlight]"
    echo ""
    echo "Examples:"
    echo "  $0 article.md"
    echo "  $0 article.md lapis"
    echo "  $0 article.md lapis solarized-light"
    echo ""
    echo "Available themes:"
    echo "  default, lapis, phycat, ..."
    echo "  Run 'wenyan theme -l' to see all"
    echo ""
    echo "Available highlights:"
    echo "  atom-one-dark, atom-one-light, dracula, github-dark, github,"
    echo "  monokai, solarized-dark, solarized-light, xcode"
}

# 主函数
main() {
    if [ $# -eq 0 ] || [ "$1" == "-h" ] || [ "$1" == "--help" ]; then
        show_help
        exit 0
    fi
    
    local file="$1"
    local theme="$2"
    local highlight="$3"
    
    check_wenyan
    check_env
    check_file "$file"
    add_cover "$file"
    publish "$file" "$theme" "$highlight"
}

main "$@"


path: scripts/scrape_news.py
content: #!/usr/bin/env python3
"""
新闻抓取脚本
使用 Scrapling 抓取科技新闻

Usage:
    python scrape_news.py --source 36kr --output /tmp/news.json
    python scrape_news.py --source huxiu --count 10
"""

import argparse
import json
import sys
from datetime import datetime

try:
    from scrapling.fetchers import Fetcher, StealthyFetcher
except ImportError:
    print("❌ Scrapling 未安装，请运行: pip install 'scrapling[all]>=0.4.2'")
    sys.exit(1)

# 新闻源配置
SOURCES = {
    "36kr": {
        "url": "https://www.36kr.com/information/AI/",
        # 使用 a 标签提取，匹配 /p/ 开头的文章链接
        "link_pattern": "/p/",
    },
    "huxiu": {
        "url": "https://www.huxiu.com/channel/106.html",
        "link_pattern": "/article/",
    },
}


def scrape_36kr(count=10, use_stealth=False):
    """抓取 36氪 AI 频道"""
    config = SOURCES["36kr"]
    
    print(f"📡 正在抓取 36氪 AI 频道...")
    
    if use_stealth:
        page = StealthyFetcher.fetch(config["url"], headless=True)
    else:
        page = Fetcher.get(config["url"], stealthy_headers=True)
    
    # 提取所有 /p/ 开头的链接
    link_pattern = config["link_pattern"]
    all_links = page.css(f'a[href*="{link_pattern}"]')
    
    seen = set()
    results = []
    for link in all_links:
        href = link.attrib.get('href', '')
        title = link.css('::text').get() or ''
        
        # 过滤条件
        if not href or not title:
            continue
        if href in seen:
            continue
        if len(title.strip()) < 5:  # 过滤太短的标题
            continue
            
        seen.add(href)
        
        # 补全链接
        if href.startswith("/"):
            full_url = f"https://www.36kr.com{href}"
        else:
            full_url = href
        
        results.append({
            "title": title.strip(),
            "link": full_url,
            "summary": "",
            "source": "36kr",
            "scraped_at": datetime.now().isoformat(),
        })
        
        if len(results) >= count:
            break
    
    print(f"✅ 抓取到 {len(results)} 条新闻")
    return results


def scrape_huxiu(count=10, use_stealth=False):
    """抓取虎嗅 AI 频道"""
    config = SOURCES["huxiu"]
    
    print(f"📡 正在抓取虎嗅 AI 频道...")
    
    if use_stealth:
        page = StealthyFetcher.fetch(config["url"], headless=True)
    else:
        page = Fetcher.get(config["url"], stealthy_headers=True)
    
    # 提取所有文章链接
    link_pattern = config["link_pattern"]
    all_links = page.css(f'a[href*="{link_pattern}"]')
    
    seen = set()
    results = []
    for link in all_links:
        href = link.attrib.get('href', '')
        title = link.css('::text').get() or ''
        
        if not href or not title:
            continue
        if href in seen:
            continue
        if len(title.strip()) < 5:
            continue
            
        seen.add(href)
        
        if href.startswith("/"):
            full_url = f"https://www.huxiu.com{href}"
        else:
            full_url = href
        
        results.append({
            "title": title.strip(),
            "link": full_url,
            "summary": "",
            "source": "huxiu",
            "scraped_at": datetime.now().isoformat(),
        })
        
        if len(results) >= count:
            break
    
    print(f"✅ 抓取到 {len(results)} 条新闻")
    return results


def scrape_article_content(url, use_stealth=False):
    """抓取文章正文"""
    print(f"📄 正在抓取文章: {url}")
    
    if use_stealth:
        page = StealthyFetcher.fetch(url, headless=True)
    else:
        page = Fetcher.get(url, stealthy_headers=True)
    
    # 尝试多种选择器
    content = (
        page.css("article .content::text").getall() or
        page.css(".article-content::text").getall() or
        page.css(".content::text").getall() or
        []
    )
    
    return "\n".join(content)


def main():
    parser = argparse.ArgumentParser(description="新闻抓取脚本")
    parser.add_argument("--source", choices=["36kr", "huxiu"], default="36kr", help="新闻源")
    parser.add_argument("--output", "-o", help="输出文件路径 (JSON)")
    parser.add_argument("--count", "-c", type=int, default=10, help="抓取数量")
    parser.add_argument("--stealth", action="store_true", help="使用隐身模式")
    parser.add_argument("--content", metavar="URL", help="抓取指定文章的正文")
    
    args = parser.parse_args()
    
    if args.content:
        # 抓取单篇文章
        content = scrape_article_content(args.content, args.stealth)
        print(content)
        return
    
    # 抓取新闻列表
    if args.source == "36kr":
        articles = scrape_36kr(args.count, args.stealth)
    else:
        articles = scrape_huxiu(args.count, args.stealth)
    
    # 输出
    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            json.dump(articles, f, ensure_ascii=False, indent=2)
        print(f"📝 已保存到: {args.output}")
    else:
        print(json.dumps(articles, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()


path: scripts/setup.sh
content: #!/usr/bin/env bash
# setup.sh - 加载微信公众号环境变量
# Usage: source ./setup.sh

SKILL_DIR="/root/.openclaw/workspace/skills/ai-news-publisher"
TOOLS_MD="$HOME/.openclaw/workspace/TOOLS.md"

# 检查 TOOLS.md
if [ ! -f "$TOOLS_MD" ]; then
    echo "❌ 找不到 TOOLS.md: $TOOLS_MD"
    echo ""
    echo "请添加微信公众号凭证："
    echo ""
    echo "  ## 🔐 WeChat Official Account"
    echo "  export WECHAT_APP_ID=your_app_id"
    echo "  export WECHAT_APP_SECRET=your_app_secret"
    exit 1
fi

# 提取凭证
WECHAT_APP_ID=$(grep "export WECHAT_APP_ID=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_ID=//' | tr -d ' ')
WECHAT_APP_SECRET=$(grep "export WECHAT_APP_SECRET=" "$TOOLS_MD" | head -1 | sed 's/.*export WECHAT_APP_SECRET=//' | tr -d ' ')

if [ -z "$WECHAT_APP_ID" ] || [ -z "$WECHAT_APP_SECRET" ]; then
    echo "❌ 无法从 TOOLS.md 读取凭证！"
    echo ""
    echo "请确保格式正确："
    echo "  export WECHAT_APP_ID=your_app_id"
    echo "  export WECHAT_APP_SECRET=your_app_secret"
    exit 1
fi

export WECHAT_APP_ID
export WECHAT_APP_SECRET

echo "✅ 微信公众号环境变量已加载！"
echo ""
echo "  WECHAT_APP_ID=${WECHAT_APP_ID:0:10}..."
echo "  WECHAT_APP_SECRET=******"


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