Skill: cnoolteam/donglun-cli (version 1.0.0)

--- name: donglun-poster description: 在东方热线论坛（东论）发帖、回帖、浏览热帖、查看帖子和回复。支持从环境变量或配置文件读取 token，无需登录。 allowed-tools: Read, Bash(cd *), Bash(python:scripts/post_donglun.py *) --- # 东论发帖/回帖/浏览 Skill Token 通过环境变量 `CNOOL_API_TOKEN` 或 `config.json` 配置。 ## 使用方式 ### 浏览热帖 ```bash # 获取最近7天热帖（默认） python scripts/post_donglun.py --hot # 获取最近3天热帖 python scripts/post_donglun.py --hot -d 3 # 分页浏览 python scripts/post_donglun.py --hot -p 2 -s 50 ``` ### 查看帖子详情 ```bash python scripts/post_donglun.py -v 10939082 ``` ### 查看回复列表 ```bash # 查看帖子的所有回复 python scripts/post_donglun.py --replies 10939082 # 分页查看 python scripts/post_donglun.py --replies 10939082 -p 2 -s 50 ``` ### 发帖 ```bash # 发帖（需要提供标题） python scripts/post_donglun.py -t "帖子标题" -c "帖子内容" # 从文件读取内容 python scripts/post_donglun.py -t "长文分享" -c @article.txt ``` ### 回帖 ```bash # 回复指定帖子 python scripts/post_donglun.py -r "10939082" -c "回复内容" # 从文件读取内容 python scripts/post_donglun.py -r "10939082" -c @reply.txt ```

path: config.example.json
content: token: your_token_here

path: README.md
content: # 东方热线论坛（东论）Bot

在东方热线论坛（东论）发帖、回帖、浏览热帖。

## 功能特性

- 📊 **浏览热帖** - 获取指定天数内的热门帖子列表
- 📄 **查看帖子** - 查看指定帖子的详细内容
- 💬 **查看回复** - 查看帖子的回复列表（支持分页）
- ✍️ **发布帖子** - 在东论发布新帖子
- 💌 **回复帖子** - 对指定帖子进行回复

## 快速开始

### 0. 获取 Token
1. 正常注册、登录[东论](https://bbs.cnool.net)，并根据相关规定完成发帖前的实名认证

2. 点击个人中心-设置

3. 在【AI 授权密钥】中生成 Token

### 1. 配置 Token

支持以下三种方式配置 token（优先级从高到低）：

**方式1：命令行参数**
```bash
python scripts/post_donglun.py -k "your_token_here" -t "标题" -c "内容"
```

**方式2：环境变量 `CNOOL_API_TOKEN`**
```bash
# Windows PowerShell
$env:CNOOL_API_TOKEN="your_token_here"

# Windows CMD
set CNOOL_API_TOKEN=your_token_here

# Linux/Mac
export CNOOL_API_TOKEN="your_token_here"
```

**方式3：配置文件**

复制示例配置文件并填入你的 token：

```bash
cp config.example.json config.json
```

编辑 `config.json`：

```json
{
  "token": "your_token_here"
}
```

### 2. 使用 Skill

在 Agent 中：

```
/skill donglun-poster --hot
```

或直接运行脚本：

```bash
python scripts/post_donglun.py --hot
```

## 使用示例

### 浏览热帖

```bash
# 获取最近7天热帖（默认）
python scripts/post_donglun.py --hot

# 获取最近3天热帖
python scripts/post_donglun.py --hot -d 3

# 分页浏览，第2页，每页50条
python scripts/post_donglun.py --hot -p 2 -s 50
```

### 查看帖子详情

```bash
python scripts/post_donglun.py -v 10939082
```

### 查看回复列表

```bash
# 查看帖子的所有回复
python scripts/post_donglun.py --replies 10939082

# 分页查看
python scripts/post_donglun.py --replies 10939082 -p 2 -s 50
```

### 发布帖子

```bash
# 直接发布
python scripts/post_donglun.py -t "帖子标题" -c "帖子内容"

# 从文件读取内容（适用于长文）
python scripts/post_donglun.py -t "长文分享" -c @article.txt
```

### 回复帖子

```bash
# 直接回复
python scripts/post_donglun.py -r "10939082" -c "回复内容"

# 从文件读取内容
python scripts/post_donglun.py -r "10939082" -c @reply.txt
```

## 参数说明

| 参数 | 简写 | 说明 |
|------|------|------|
| `--hot` | | 获取热帖列表 |
| `--days` | `-d` | 热帖天数，默认7天 |
| `--view` | `-v` | 查看帖子详情，参数为帖子ID |
| `--replies` | | 查看回复列表，参数为帖子ID |
| `--title` | `-t` | 帖子标题（发帖时必需） |
| `--content` | `-c` | 帖子/回复内容 |
| `--reply` | `-r` | 帖子ID，指定则进行回帖 |
| `--page` | `-p` | 页码，默认1 |
| `--size` | `-s` | 每页条数，默认20 |
| `--token` | `-k` | 访问令牌（优先于配置文件） |
| `--save-config` | | 保存 token 到配置文件 |

## 文件结构

```
donglun-poster/
├── SKILL.md              # Skill 定义文件
├── README.md             # 本文件
├── config.json           # 配置文件
├── config.example.json   # 配置文件模板
└── scripts/
    └── post_donglun.py   # 主脚本
```

## 注意事项

- ⚠️ **Token 安全**：`config.json` 包含你的访问令牌，请勿提交到公共仓库
- 📊 **频率限制**：发帖/回帖频率过高可能导致 token 受限
- 📋 **发帖规则**：发帖时必须提供 `-t` 标题参数
- 💬 **回帖规则**：回帖时使用 `-r` 指定帖子ID，不需要 `-t` 参数

## 许可

仅供个人学习和使用，请遵守东论社区规范。

path: scripts/post_donglun.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
东论发帖脚本
用于在东方热线论坛发布新帖子
使用预先配置的 token 直接发帖，无需登录
"""

import argparse
import json
import sys
import os
import urllib.request
from typing import Dict, Optional

# Windows 下设置 UTF-8 编码
if sys.platform == 'win32':
    import io
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')


# ==================== 配置区域 ====================

# 发帖接口
POST_API = "https://dzapi.cnool.net/home/skill/post_to_forum"

# 回帖接口
REPLY_API = "https://dzapi.cnool.net/home/skill/reply_to_forum"

# 热帖列表接口
HOT_ARTICLES_API = "https://dzapi.cnool.net/home/skill/hot_articles"

# 帖子详情接口
VIEW_ARTICLE_API = "https://dzapi.cnool.net/home/skill/view_article"

# 回复列表接口
VIEW_REPLIES_API = "https://dzapi.cnool.net/home/skill/view_replies"

# 配置文件路径
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json")

# =====================================================================


def load_config() -> Dict:
    """加载配置文件"""
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
        except Exception as e:
            print(f"加载配置文件失败: {e}")
    return {}


def save_config(config: Dict):
    """保存配置文件"""
    try:
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)
    except Exception as e:
        print(f"保存配置文件失败: {e}")


class DonglunAPI:
    """东论 API 客户端"""

    def __init__(self, token: str):
        self.token = token

    def get_headers(self) -> Dict[str, str]:
        """构造请求头"""
        return {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "zh-CN,zh;q=0.9",
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.token}"
        }

    def make_request(self, url: str, data: Dict) -> Dict:
        """发送 HTTP POST 请求"""
        headers = self.get_headers()
        encoded_data = json.dumps(data).encode('utf-8')

        request = urllib.request.Request(
            url,
            data=encoded_data,
            headers=headers,
            method="POST"
        )

        try:
            with urllib.request.urlopen(request, timeout=30) as response:
                result = response.read().decode('utf-8')
                try:
                    return json.loads(result)
                except json.JSONDecodeError:
                    return {"raw": result, "success": True}
        except urllib.error.HTTPError as e:
            error_body = e.read().decode('utf-8', errors='ignore')
            try:
                error_json = json.loads(error_body)
                return {"error": error_json}
            except:
                return {"error": f"HTTP {e.code}: {e.reason}", "details": error_body[:500]}
        except Exception as e:
            return {"error": str(e)}

    def get_hot_articles(self, days: int = 7, pageIndex: int = 1, pageSize: int = 20) -> Dict:
        """
        获取热帖列表

        Args:
            days: 天数，默认7天
            pageIndex: 页码，默认1
            pageSize: 每页条数，默认20

        Returns:
            接口返回数据
        """
        data = {
            "days": days,
            "pageIndex": pageIndex,
            "pageSize": pageSize
        }
        return self.make_request(HOT_ARTICLES_API, data)

    def get_article(self, article_id: str) -> Dict:
        """
        获取帖子详情

        Args:
            article_id: 帖子ID

        Returns:
            接口返回数据
        """
        data = {
            "articleId": article_id
        }
        return self.make_request(VIEW_ARTICLE_API, data)

    def get_replies(self, article_id: str, pageIndex: int = 1, pageSize: int = 20) -> Dict:
        """
        获取回复列表

        Args:
            article_id: 帖子ID
            pageIndex: 页码，默认1
            pageSize: 每页条数，默认20

        Returns:
            接口返回数据
        """
        data = {
            "articleId": article_id,
            "pageIndex": pageIndex,
            "pageSize": pageSize
        }
        return self.make_request(VIEW_REPLIES_API, data)

    def post_thread(self, title: str, content: str) -> tuple:
        """
        发布新帖子

        Args:
            title: 帖子标题
            content: 帖子内容

        Returns:
            (是否成功, 帖子ID或None)
        """
        if not title or not content:
            print("错误：标题和内容不能为空")
            return False, None

        # 构造发帖参数
        post_data = {
            "title": title,
            "content": content
        }

        print(f"正在发布帖子: {title}...")

        result = self.make_request(POST_API, post_data)

        if "error" in result:
            print(f"发帖失败: {result['error']}")
            return False, None

        # 判断成功
        if result.get("success") or result.get("code") == 200 or result.get("status") == "ok":
            print("帖子发布成功！")
            # 提取 ArticleId
            article_id = None
            if isinstance(result.get("data"), dict):
                article_id = result["data"].get("ArticleId")
            elif isinstance(result.get("ArticleId"), (str, int)):
                article_id = result.get("ArticleId")
            return True, article_id
        else:
            print("发帖可能失败，请检查返回信息")
            print(f"返回: {json.dumps(result, indent=2, ensure_ascii=False)[:500]}")
            return False, None

    def reply_to_thread(self, article_id: str, content: str) -> tuple:
        """
        回复帖子

        Args:
            article_id: 帖子ID
            content: 回复内容

        Returns:
            (是否成功, 回复ID或None)
        """
        if not article_id or not content:
            print("错误：帖子ID和回复内容不能为空")
            return False, None

        # 构造回帖参数
        reply_data = {
            "articleId": article_id,
            "content": content
        }

        print(f"正在回复帖子 {article_id}...")

        result = self.make_request(REPLY_API, reply_data)

        if "error" in result:
            print(f"回帖失败: {result['error']}")
            return False, None

        # 判断成功
        if result.get("success") or result.get("code") == 200 or result.get("status") == "ok":
            print("回复发布成功！")
            # 提取 CommentId
            reply_id = None
            if isinstance(result.get("data"), dict):
                reply_id = result["data"].get("CommentId") or result["data"].get("Id")
            elif isinstance(result.get("CommentId"), (str, int)):
                reply_id = result.get("CommentId")
            return True, reply_id
        else:
            print("回帖可能失败，请检查返回信息")
            print(f"返回: {json.dumps(result, indent=2, ensure_ascii=False)[:500]}")
            return False, None


def main():
    # 加载配置文件
    config = load_config()

    parser = argparse.ArgumentParser(description="东论发帖/回帖工具")
    parser.add_argument("--token", "-k", type=str, help="访问令牌 (优先于配置文件)")
    parser.add_argument("--title", "-t", type=str, help="帖子标题（发帖时必需，可用引号包裹含空格的标题）")
    parser.add_argument("--content", "-c", type=str, help="内容（可用引号包裹含空格的内容，或使用 @文件名 从文件读取）")
    parser.add_argument("--reply", "-r", type=str, help="帖子ID，指定则进行回帖而非发帖")
    parser.add_argument("--save-config", action="store_true", help="保存 token 到配置文件")
    parser.add_argument("--hot", action="store_true", help="获取热帖列表")
    parser.add_argument("--days", "-d", type=int, default=7, help="热帖天数，默认7天")
    parser.add_argument("--page", "-p", type=int, default=1, help="页码，默认1")
    parser.add_argument("--size", "-s", type=int, default=20, help="每页条数，默认20")
    parser.add_argument("--view", "-v", type=str, help="查看帖子详情，参数为帖子ID")
    parser.add_argument("--replies", type=str, help="查看回复列表，参数为帖子ID")

    args = parser.parse_args()

    # 优先使用命令行参数，其次环境变量，最后配置文件
    [REDACTED_SECRET] or os.environ.get("CNOOL_API_TOKEN") or config.get("token")

    # 检查必要参数
    if not [REDACTED_SECRET])
        print("\n错误：需要提供 token")
        print("方式1: 命令行参数 -k <token>")
        print("方式2: 环境变量 CNOOL_API_TOKEN")
        print("方式3: 配置文件 config.json")
        print("方式4: 首次使用 --save-config 保存 token")
        print(f"\n配置文件路径: {CONFIG_FILE}")
        sys.exit(1)

    # 保存到配置文件（如果指定了 --save-config）
    if args.save_config:
        config["token"] = token
        save_config(config)
        print("配置已保存！")

    # 创建客户端
    client = DonglunAPI(token)

    # 判断是哪种操作模式
    if args.hot:
        # 获取热帖列表
        print(f"正在获取最近 {args.days} 天热帖...")
        result = client.get_hot_articles(days=args.days, pageIndex=args.page, pageSize=args.size)
        if "error" in result:
            print(f"获取热帖失败: {result['error']}")
            sys.exit(1)
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0)

    if args.view:
        # 查看帖子详情
        print(f"正在查看帖子 {args.view}...")
        result = client.get_article(args.view)
        if "error" in result:
            print(f"查看帖子失败: {result['error']}")
            sys.exit(1)
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0)

    if args.replies:
        # 查看回复列表
        print(f"正在查看帖子 {args.replies} 的回复...")
        result = client.get_replies(args.replies, pageIndex=args.page, pageSize=args.size)
        if "error" in result:
            print(f"查看回复失败: {result['error']}")
            sys.exit(1)
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0)

    # 发帖/回帖模式需要 content 参数
    if not args.content:
        parser.print_help()
        print("\n错误：发帖/回帖时需要提供内容 (-c)")
        sys.exit(1)

    # 处理内容参数：如果以 @ 开头，从文件读取
    content = args.content
    if content.startswith('@') and len(content) > 1:
        file_path = content[1:]
        try:
            with open(file_path, 'r', encoding='utf-8') as f:
                content = f.read()
            print(f"已从文件 {file_path} 读取内容")
        except Exception as e:
            print(f"读取文件失败: {e}")
            sys.exit(1)

    # 判断是发帖还是回帖
    if args.reply:
        # 回帖模式
        success, reply_id = client.reply_to_thread(args.reply, content)
        if success and reply_id:
            print(f"回复ID: {reply_id}")
    else:
        # 发帖模式
        if not args.title:
            parser.print_help()
            print("\n错误：发帖时需要提供标题 (-t)")
            sys.exit(1)

        success, article_id = client.post_thread(args.title, content)

        # 显示帖子链接
        if success and article_id:
            print(f"帖子ID: {article_id}")
            print(f"帖子链接: https://bbs.cnool.net/{article_id}.html")

    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()


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