Skip to content
Open
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
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Send these as WeChat messages:
| `/claude` | Switch default agent to Claude |
| `/cwd /path/to/project` | Switch workspace directory |
| `/new` | Start a new conversation (clear session) |
| `/cron list\|add\|delete\|enable\|disable` | Manage scheduled tasks |
| `/info` | Show current agent info |
| `/help` | Show help message |

Expand Down Expand Up @@ -151,6 +152,68 @@ Supported media types: images (png, jpg, gif, webp), videos (mp4, mov), files (p

Set `WECLAW_API_ADDR` to change the listen address (e.g. `0.0.0.0:18011`).

## Cron (Scheduled Tasks)

Schedule recurring or one-shot tasks that send messages to AI agents on a timer:

```
/cron add "standup" every:24h "summarize yesterday's work"
/cron add "check" */30 * * * * "check for new PRs"
/cron add "reminder" at:2026-04-01T09:00:00 "sprint review today"
/cron list
/cron enable <id>
/cron disable <id>
/cron delete <id>
```

**Schedule formats:**

| Format | Example | Description |
|--------|---------|-------------|
| `every:<duration>` | `every:5m`, `every:24h` | Fixed interval |
| Cron expression | `*/30 * * * *`, `0 9 * * 1-5` | Standard 5-field cron |
| `at:<time>` | `at:2026-04-01T09:00:00` | One-shot (auto-disables after run) |

Jobs are persisted to `~/.weclaw/cron/jobs.json` and survive restarts. Each job can optionally target a specific agent.

## Heartbeat

Periodic agent check-in driven by a user-authored checklist. Create `~/.weclaw/HEARTBEAT.md`:

```markdown
# Heartbeat checklist
- Check if any urgent emails arrived
- Scan for open PRs needing review
- Check CI pipeline status
```

WeClaw reads this file on each heartbeat interval, sends it to the default agent, and:
- Agent replies `HEARTBEAT_OK` → suppressed (nothing to report)
- Agent replies with content → delivered to the target user with `[Heartbeat]` prefix
- Duplicate replies within 24h are suppressed to avoid noise

**Config:**

```json
{
"heartbeat": {
"enabled": true,
"interval": "30m",
"active_hours": "09:00-18:00",
"timezone": "Asia/Shanghai",
"target_user": "your_wechat_user_id"
}
}
```

| Option | Default | Description |
|--------|---------|-------------|
| `enabled` | `false` | Enable heartbeat runner |
| `interval` | `30m` | Time between heartbeat checks |
| `active_hours` | — | Only run during these hours (e.g. `09:00-18:00`) |
| `timezone` | local | IANA timezone for active hours |
| `target_user` | — | WeChat user ID to send heartbeat results to |

## Configuration

Config file: `~/.weclaw/config.json`
Expand Down Expand Up @@ -180,6 +243,13 @@ Config file: `~/.weclaw/config.json`
"api_key": "sk-xxx",
"model": "openclaw:main"
}
},
"heartbeat": {
"enabled": true,
"interval": "30m",
"active_hours": "09:00-18:00",
"timezone": "Asia/Shanghai",
"target_user": "your_wechat_user_id"
}
}
```
Expand Down
70 changes: 70 additions & 0 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ docker run -it -v ~/.weclaw:/root/.weclaw ghcr.io/fastclaw-ai/weclaw start
| `/claude` | 切换默认 Agent 为 Claude |
| `/cwd /path/to/project` | 切换工作目录 |
| `/new` | 开始新对话(清除会话) |
| `/cron list\|add\|delete\|enable\|disable` | 管理定时任务 |
| `/info` | 查看当前 Agent 信息 |
| `/help` | 查看帮助信息 |

Expand Down Expand Up @@ -152,6 +153,68 @@ curl -X POST http://127.0.0.1:18011/api/send \

设置 `WECLAW_API_ADDR` 环境变量可更改监听地址(如 `0.0.0.0:18011`)。

## 定时任务(Cron)

在聊天中管理定时或一次性任务,定时发消息给 AI Agent 执行:

```
/cron add "standup" every:24h "总结昨天的工作"
/cron add "check" */30 * * * * "检查新的 PR"
/cron add "reminder" at:2026-04-01T09:00:00 "今天下午 Sprint 评审"
/cron list
/cron enable <id>
/cron disable <id>
/cron delete <id>
```

**调度格式:**

| 格式 | 示例 | 说明 |
|------|------|------|
| `every:<时长>` | `every:5m`、`every:24h` | 固定间隔 |
| Cron 表达式 | `*/30 * * * *`、`0 9 * * 1-5` | 标准 5 字段 cron |
| `at:<时间>` | `at:2026-04-01T09:00:00` | 一次性(执行后自动禁用) |

任务持久化到 `~/.weclaw/cron/jobs.json`,重启不丢失。每个任务可选指定目标 Agent。

## 心跳(Heartbeat)

基于用户编写的检查清单,定时让 Agent 做自主巡检。创建 `~/.weclaw/HEARTBEAT.md`:

```markdown
# 心跳检查清单
- 检查是否有紧急邮件
- 扫描需要 review 的 PR
- 检查 CI 流水线状态
```

WeClaw 每次心跳间隔读取此文件,发给默认 Agent:
- Agent 回复 `HEARTBEAT_OK` → 吞掉(一切正常)
- Agent 回复有内容 → 以 `[Heartbeat]` 前缀发送给目标用户
- 24 小时内重复回复自动去重

**配置:**

```json
{
"heartbeat": {
"enabled": true,
"interval": "30m",
"active_hours": "09:00-18:00",
"timezone": "Asia/Shanghai",
"target_user": "your_wechat_user_id"
}
}
```

| 选项 | 默认值 | 说明 |
|------|--------|------|
| `enabled` | `false` | 启用心跳 |
| `interval` | `30m` | 心跳间隔 |
| `active_hours` | — | 仅在此时段运行(如 `09:00-18:00`) |
| `timezone` | 本地 | 活跃时段的时区(IANA 格式) |
| `target_user` | — | 接收心跳结果的微信用户 ID |

## 配置

配置文件路径:`~/.weclaw/config.json`
Expand Down Expand Up @@ -181,6 +244,13 @@ curl -X POST http://127.0.0.1:18011/api/send \
"api_key": "sk-xxx",
"model": "openclaw:main"
}
},
"heartbeat": {
"enabled": true,
"interval": "30m",
"active_hours": "09:00-18:00",
"timezone": "Asia/Shanghai",
"target_user": "your_wechat_user_id"
}
}
```
Expand Down
38 changes: 38 additions & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,44 @@ func runStart(cmd *cobra.Command, args []string) error {
log.Printf("Image save directory: %s", cfg.SaveDir)
}

// Initialize cron scheduler
cronStorePath, err := messaging.DefaultCronStorePath()
if err != nil {
log.Printf("Warning: failed to resolve cron store path: %v", err)
} else {
cronStore := messaging.NewCronStore(cronStorePath)
if err := cronStore.Load(); err != nil {
log.Printf("Warning: failed to load cron store: %v", err)
}
handler.SetCronStore(cronStore)

// Use the first account's client for sending cron messages
firstClient := ilink.NewClient(accounts[0])
targetUser := cfg.Heartbeat.TargetUser

cronScheduler := messaging.NewCronScheduler(cronStore, firstClient, targetUser, func(name string) agent.Agent {
if name == "" {
return handler.GetDefaultAgent()
}
ag, err := handler.GetAgent(ctx, name)
if err != nil {
return nil
}
return ag
})
go cronScheduler.Start(ctx)

// Start heartbeat runner
if cfg.Heartbeat.Enabled {
hbRunner, err := messaging.NewHeartbeatRunner(cfg.Heartbeat, firstClient, targetUser, handler.GetDefaultAgent)
if err != nil {
log.Printf("Warning: failed to start heartbeat runner: %v", err)
} else {
go hbRunner.Start(ctx)
}
}
}

// Start default agent initialization in background so monitors can start immediately
go func() {
if cfg.DefaultAgent == "" {
Expand Down
10 changes: 10 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ type Config struct {
APIAddr string `json:"api_addr,omitempty"`
SaveDir string `json:"save_dir,omitempty"`
Agents map[string]AgentConfig `json:"agents"`
Heartbeat HeartbeatConfig `json:"heartbeat,omitempty"`
}

// HeartbeatConfig holds heartbeat runner configuration.
type HeartbeatConfig struct {
Enabled bool `json:"enabled"`
Interval string `json:"interval,omitempty"` // e.g. "30m"
ActiveHours string `json:"active_hours,omitempty"` // e.g. "09:00-18:00"
Timezone string `json:"timezone,omitempty"` // IANA timezone
TargetUser string `json:"target_user,omitempty"` // user ID to send heartbeat results to
}

// AgentConfig holds configuration for a single agent.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mdp/qrterminal/v3 v3.2.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/net v0.52.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mdp/qrterminal/v3 v3.2.1 h1:6+yQjiiOsSuXT5n9/m60E54vdgFsw0zhADHhHLrFet4=
github.com/mdp/qrterminal/v3 v3.2.1/go.mod h1:jOTmXvnBsMy5xqLniO0R++Jmjs2sTm9dFSuQ5kpz/SU=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
Expand Down
Loading
Loading