Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/auto-reading.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ jobs:
WEREAD_CURL_STRING: ${{ secrets.WEREAD_CURL_STRING }}
# 多用户并发控制,未设置默认 1(顺序执行)
MAX_CONCURRENT_USERS: ${{ secrets.MAX_CONCURRENT_USERS || 1 }}
# 单用户连续阅读失败上限,未设置时默认 5
MAX_CONSECUTIVE_FAILURES: ${{ secrets.MAX_CONSECUTIVE_FAILURES || 5 }}
# 通知相关环境变量
PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ on:
branches: [ main, dev ]
paths:
- 'weread-bot.py'
- 'tests/**'
- 'requirements.txt'
- '.github/workflows/ci.yml'
pull_request:
branches: [ main ]
paths:
- 'weread-bot.py'
- 'tests/**'
- 'requirements.txt'
- '.github/workflows/ci.yml'

Expand Down Expand Up @@ -50,6 +52,10 @@ jobs:
run: |
python -m py_compile weread-bot.py

- name: Run behavior tests
run: |
python -m unittest discover -s tests -v

- name: Test script help
run: |
python weread-bot.py --help
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ eggs/
.eggs/
lib/
lib64/
temp_files/
parts/
sdist/
var/
Expand Down
44 changes: 27 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
# Repository Guidelines
# AGENTS.md

## Project Structure & Module Organization
Main automation logic lives in `weread-bot.py`, orchestrating scheduling, reading simulation, and notifications. Configuration templates (`config.yaml.example`) stay at the root; copy to `config.yaml` for local overrides. Docs and deployment references belong under `docs/`, persistent logs under `logs/`, and disposable experiments or generators inside `tmp/` to keep history clean.
本文件适用于整个仓库。用户明确要求优先;子目录存在更近的 `AGENTS.md` 时,以更近的规则为准。

## Build, Test, and Development Commands
- `python -m venv venv && source venv/bin/activate`: provision the Python 3.9+ environment expected by schedulers and HTTP clients.
- `pip install -r requirements.txt`: install runtime dependencies (requests, schedule, apprise, PyYAML, urllib3).
- `python weread-bot.py --config config.yaml --verbose`: run an immediate session with explicit config and expanded logging for debugging.
- `python weread-bot.py --mode scheduled --config config.yaml`: exercise the cron-driven path and confirm the schedule block parses correctly.
- `python weread-bot.py --mode daemon --config config.yaml`: validate long-lived loops, session intervals, and graceful signal handling.
## 单文件约束

## Coding Style & Naming Conventions
Follow PEP 8 with four-space indentation, snake_case functions, and PascalCase dataclasses or enums. Extend the existing dataclasses instead of injecting bare dicts so validation stays centralized. Reuse module-level constants for defaults, add type hints on new functions, and route diagnostics through the configured logger rather than `print`. Align YAML keys and examples with the provided templates.
- 所有运行时代码必须集中在 `weread-bot.py`。
- 新功能、修复和重构始终采用单文件实现。不得拆分模块、创建包、增加辅助源码文件,或把运行时代码移入其他文件。
- 测试可修改 `tests/` 中的现有文件,但不得借测试或脚本绕过单文件约束。
- 优先复用现有实现。非必要不新增文件、依赖或抽象层。

## Testing Guidelines
There is no dedicated unit-test suite yet; rely on scenario-driven runs. Use anonymized CURL payloads and short `TARGET_DURATION` windows (5-10 minutes) when iterating locally, then inspect `logs/weread.log` for retry cadence, notification output, and multi-user sequencing issues. When extending notification transports or scheduling, outline the manual test matrix and attach relevant log excerpts in the PR.
## 目录约定

## Commit & Pull Request Guidelines
Use Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`) as demonstrated in recent history. Each PR should explain the motivation, reference related issues, list new environment variables or config keys, and include reproduction steps plus verification evidence (command snippets or screenshots for doc changes). Keep PRs scoped to a single concern and update affected docs such as `docs/github-action-autoread-guide.md`.
- `weread-bot.py`:唯一的运行时代码文件。
- `config.yaml.example`:配置模板;本地配置使用 `config.yaml`。
- `tests/`:自动化测试。
- `docs/`:使用和部署文档。
- `scripts/`:用户明确要求的独立工具脚本,不得存放项目运行时代码。

## Security & Configuration Tips
Never commit CURL strings or API tokens; load them via environment variables like `export WEREAD_CURL_BASH_FILE_PATH=/secure/curl.txt` or GitHub Actions secrets. Store per-user overrides under `config.yaml`'s `curl_config.users` array, scrub logs before sharing, rotate cookies regularly, and keep any `.env` files outside version control.
新建文档前先检查现有内容,能更新就不重复创建。代码或配置变更影响使用方式时,同步更新相关文档和配置示例。

## 开发规范

- 使用 Python 3.9+,遵循 PEP 8,四空格缩进。
- 函数和变量使用 `snake_case`,数据类和枚举使用 `PascalCase`。
- 新增函数写类型注解;配置数据优先复用现有数据类和校验逻辑。
- 复用模块级默认值,YAML 键名与 `config.yaml.example` 保持一致。
- 日志统一使用现有 logger,不使用 `print` 输出诊断信息。

## 安全要求

- 密钥、Token、Cookie 和 CURL 内容只能从环境变量或本地安全配置读取。
- 敏感信息不得写入代码、文档、测试数据或提交记录。分享日志前先完成脱敏。
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

WeRead Bot 是一个易用的微信读书自动阅读机器人,通过模拟真实用户阅读行为来积累阅读时长,支持多用户、多种运行模式(立即执行、定时任务、守护进程)、执行历史和多平台通知,适用于需要提升微信读书等级或完成阅读任务的用户场景。

运行代码集中在 `weread-bot.py`。下载该文件并安装 `requirements.txt` 中的依赖即可运行。

感谢 [findmover/wxread](https://github.com/findmover/wxread) 提供思路和部分代码支持。

## 适合哪些用户
Expand Down Expand Up @@ -182,6 +184,13 @@ python weread-bot.py --help
- 历史记录只保存时间、状态、用户数、阅读统计和失败分类等摘要,不会保存 Cookie、请求头或原始 CURL。
- 如果 `curl_config.users[].reading_overrides` 中存在不支持的键,程序会直接提示对应配置路径。
- 如果 `curl_config.users[].cookie_refresh_ql` 不是布尔值,程序会直接提示对应配置路径。
- 配置文件存在但无法读取、YAML语法错误、范围越界或类型错误时,程序会在发起网络请求前退出。

退出码:

- `0`:所有用户会话成功,或只读诊断命令正常结束。
- `1`:配置错误、认证失败、运行失败或部分用户失败。
- `130`:用户或系统中断。

### 方式六:Docker 方式运行

Expand Down Expand Up @@ -281,6 +290,7 @@ open config-generator.html
| 阅读模式 | `READING_MODE` | `smart_random` | smart_random/sequential/pure_random |
| 目标时长 | `TARGET_DURATION` | `60-70` | 目标阅读时长(分钟) |
| 阅读间隔 | `READING_INTERVAL` | `25-35` | 每次请求间隔(秒) |
| 连续失败上限 | `MAX_CONSECUTIVE_FAILURES` | `5` | 达到上限后结束当前用户会话并记为失败 |
| 书籍连续性 | `BOOK_CONTINUITY` | `0.8` | 继续当前书籍的概率(0-1) |
| 章节连续性 | `CHAPTER_CONTINUITY` | `0.7` | 顺序阅读章节的概率(0-1) |

Expand Down Expand Up @@ -361,6 +371,7 @@ history:
说明:

- `--validate-config` 与 `--dry-run` 不会写入历史,避免污染真实执行结果。
- 每个真实会话结束后立即写一条历史。scheduled和daemon模式不会等主循环退出。
- 历史文件内容损坏时,程序会记录警告并回退为空历史,不会阻断主流程。
- 历史记录不会保存敏感请求数据,例如 Cookie、请求头和原始 CURL。

Expand All @@ -381,6 +392,8 @@ history:

**注意:通知配置采用多通道模式,支持同时启用多个通知服务**

环境变量为空字符串或只包含空白时,程序会把它视为未设置,继续读取 YAML 配置或内置默认值。通知通道的必填密钥为空时,该通道不会自动启用。

**触发策略示例:**

```yaml
Expand Down Expand Up @@ -551,6 +564,7 @@ python weread-bot.py --mode immediate
```
- 程序启动后立即开始一次阅读会话
- 完成目标时长后自动退出
- 会话失败或部分用户失败时返回退出码1
- 适合单次使用或手动控制

### 定时执行模式(scheduled)
Expand All @@ -572,6 +586,8 @@ schedule:
- `"30 9,18 * * *"` - 每天9:30和18:30执行(多时间点)
- `"0 8,12,18 * * *"` - 每天8:00、12:00、18:00执行

单次会话失败会写入历史,进程继续等待下一次cron时间。

### 守护进程模式(daemon)

```bash
Expand All @@ -588,6 +604,8 @@ daemon:

- 程序持续运行,自动管理会话间隔
- 支持每日会话次数限制
- 每次已尝试会话都计入每日次数,失败也计数
- 单次失败后按 `session_interval` 等待,进程继续运行
- 自动处理跨天重置
- 支持优雅关闭(Ctrl+C)

Expand Down
2 changes: 2 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ reading:
use_curl_data_first: true
# CURL 数据缺失或无效时是否回退到本配置的 books(true=回退,false=不回退可能报错)
fallback_to_config: true
# 连续阅读请求失败多少次后结束当前用户会话;环境变量:MAX_CONSECUTIVE_FAILURES
max_consecutive_failures: 5

# 书籍配置列表
# 章节索引优先级:配置的索引值 > CURL提取的值 > 自动计算的索引
Expand Down
9 changes: 9 additions & 0 deletions docs/github-action-autoread-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ curl 'https://weread.qq.com/web/book/read' -H 'cookie: wr_skey=user2; ...' --dat
|------------|------|------|
| `TARGET_DURATION` | 默认阅读时长,格式如 `5-10`;用于定时触发或手动触发留空时的默认值 | 1-2 |
| `MAX_CONCURRENT_USERS` | 多用户并发数量(>=1) | 1 |
| `MAX_CONSECUTIVE_FAILURES` | 单个用户连续阅读失败上限,达到后结束该会话 | 5 |
| `HACK_COOKIE_REFRESH_QL` | Cookie 刷新兼容开关全局默认值,遇到刷新失败可切换 true/false | false |
| `NOTIFICATION_ONLY_ON_FAILURE` | 仅失败通知开关(true/false),覆盖 workflow 运行参数 | false |
| `HISTORY_ENABLED` | 是否启用执行历史持久化 | true |
Expand Down Expand Up @@ -268,6 +269,14 @@ history:
- ❌ **失败**: 检查错误日志,通常是配置问题
- ⏸️ **取消**: 手动取消或超时(2小时)

程序退出码如下:

- `0`:所有用户成功。
- `1`:配置错误、运行失败或部分用户失败。
- `130`:手动取消或系统中断。

scheduled和daemon模式会为每次会话单独写历史。单次失败不会结束常驻进程。

## ❓ 常见问题

### Q: Action 运行失败怎么办?
Expand Down
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""WeRead Bot regression tests."""
70 changes: 70 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import importlib.util
import sys
import uuid
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]


def load_weread_bot():
"""重新加载 weread-bot.py,隔离类变量和模块级状态。"""
module_name = f"weread_bot_test_{uuid.uuid4().hex}"
spec = importlib.util.spec_from_file_location(
module_name, ROOT / "weread-bot.py"
)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
except Exception:
sys.modules.pop(module_name, None)
raise
return module


class FakeClock:
def __init__(self, current=0.0):
self.current = float(current)

def monotonic(self) -> float:
return self.current

def advance(self, seconds: float) -> None:
self.current += float(seconds)


class FakeHttpClient:
def __init__(self, json_responses=None, raw_responses=None):
self.json_responses = list(json_responses or [])
self.raw_responses = list(raw_responses or [])
self.json_calls = []
self.raw_calls = []
self.close_calls = 0

async def post_json(self, url, data, headers, cookies):
self.json_calls.append((url, data.copy(), headers.copy(), cookies.copy()))
response = self.json_responses.pop(0)
if isinstance(response, Exception):
raise response
return response

async def post_raw(
self, url, headers=None, cookies=None, json_data=None, data=None
):
self.raw_calls.append(
{
"url": url,
"headers": headers,
"cookies": cookies,
"json_data": json_data,
"data": data,
}
)
response = self.raw_responses.pop(0) if self.raw_responses else None
if isinstance(response, Exception):
raise response
return response

async def close(self):
self.close_calls += 1
Loading
Loading