Skip to content

Commit 1c4be32

Browse files
idoubiclaude
andcommitted
Add daemon mode, HTTP API, and CLI send command
- Default to background mode (weclaw start), use -f for foreground - Auto-detect first login: show QR in foreground, then switch to daemon - Add weclaw stop / weclaw status commands - Add HTTP API server (default 127.0.0.1:18011) for proactive messaging - Add weclaw send --to --text CLI command - Add --api-addr flag and config.api_addr support - Add launchd/systemd service templates - Make typing indicator async (non-blocking) - Update README with background mode, proactive messaging, and service docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9bd32b5 commit 1c4be32

12 files changed

Lines changed: 500 additions & 5 deletions

File tree

.air.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ testdata_dir = "testdata"
33
tmp_dir = "tmp"
44

55
[build]
6-
args_bin = ["start"]
6+
args_bin = ["start", "-f"]
77
bin = "./weclaw"
88
cmd = "go build -o ./weclaw ."
99
delay = 1000

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ Send these as WeChat messages:
7878

7979
Switching default agent is persisted to config — survives restarts.
8080

81+
## Proactive Messaging
82+
83+
Send messages to WeChat users without waiting for them to message first.
84+
85+
**CLI:**
86+
87+
```bash
88+
weclaw send --to "user_id@im.wechat" --text "Hello from weclaw"
89+
```
90+
91+
**HTTP API** (runs on `127.0.0.1:18011` when `weclaw start` is running):
92+
93+
```bash
94+
curl -X POST http://127.0.0.1:18011/api/send \
95+
-H "Content-Type: application/json" \
96+
-d '{"to": "user_id@im.wechat", "text": "Hello from weclaw"}'
97+
```
98+
99+
Set `WECLAW_API_ADDR` to change the listen address (e.g. `0.0.0.0:18011`).
100+
81101
## Configuration
82102

83103
Config file: `~/.weclaw/config.json`
@@ -110,6 +130,40 @@ Environment variables:
110130
- `OPENCLAW_GATEWAY_URL` — OpenClaw HTTP fallback endpoint
111131
- `OPENCLAW_GATEWAY_TOKEN` — OpenClaw API token
112132

133+
## Background Mode
134+
135+
```bash
136+
# Start (runs in background by default)
137+
weclaw start
138+
139+
# Check if running
140+
weclaw status
141+
142+
# Stop
143+
weclaw stop
144+
145+
# Run in foreground (for debugging)
146+
weclaw start -f
147+
```
148+
149+
Logs are written to `~/.weclaw/weclaw.log`.
150+
151+
### System service (auto-start on boot)
152+
153+
**macOS (launchd):**
154+
155+
```bash
156+
cp service/com.fastclaw.weclaw.plist ~/Library/LaunchAgents/
157+
launchctl load ~/Library/LaunchAgents/com.fastclaw.weclaw.plist
158+
```
159+
160+
**Linux (systemd):**
161+
162+
```bash
163+
sudo cp service/weclaw.service /etc/systemd/system/
164+
sudo systemctl enable --now weclaw
165+
```
166+
113167
## Docker
114168

115169
```bash

README_CN.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ docker run -it -v ~/.weclaw:/root/.weclaw ghcr.io/fastclaw-ai/weclaw start
7878

7979
切换默认 Agent 会写入配置文件,重启后仍然生效。
8080

81+
## 主动推送消息
82+
83+
无需等待用户发消息,主动向微信用户推送消息。
84+
85+
**命令行:**
86+
87+
```bash
88+
weclaw send --to "user_id@im.wechat" --text "你好,来自 weclaw"
89+
```
90+
91+
**HTTP API**`weclaw start` 运行时,默认监听 `127.0.0.1:18011`):
92+
93+
```bash
94+
curl -X POST http://127.0.0.1:18011/api/send \
95+
-H "Content-Type: application/json" \
96+
-d '{"to": "user_id@im.wechat", "text": "你好,来自 weclaw"}'
97+
```
98+
99+
设置 `WECLAW_API_ADDR` 环境变量可更改监听地址(如 `0.0.0.0:18011`)。
100+
81101
## 配置
82102

83103
配置文件路径:`~/.weclaw/config.json`
@@ -110,6 +130,40 @@ docker run -it -v ~/.weclaw:/root/.weclaw ghcr.io/fastclaw-ai/weclaw start
110130
- `OPENCLAW_GATEWAY_URL` — OpenClaw HTTP 回退地址
111131
- `OPENCLAW_GATEWAY_TOKEN` — OpenClaw API Token
112132

133+
## 后台运行
134+
135+
```bash
136+
# 启动(默认后台运行)
137+
weclaw start
138+
139+
# 查看状态
140+
weclaw status
141+
142+
# 停止
143+
weclaw stop
144+
145+
# 前台运行(调试用)
146+
weclaw start -f
147+
```
148+
149+
日志输出到 `~/.weclaw/weclaw.log`
150+
151+
### 系统服务(开机自启)
152+
153+
**macOS (launchd):**
154+
155+
```bash
156+
cp service/com.fastclaw.weclaw.plist ~/Library/LaunchAgents/
157+
launchctl load ~/Library/LaunchAgents/com.fastclaw.weclaw.plist
158+
```
159+
160+
**Linux (systemd):**
161+
162+
```bash
163+
sudo cp service/weclaw.service /etc/systemd/system/
164+
sudo systemctl enable --now weclaw
165+
```
166+
113167
## Docker
114168

115169
```bash

api/server.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
"net/http"
9+
10+
"github.com/fastclaw-ai/weclaw/ilink"
11+
"github.com/fastclaw-ai/weclaw/messaging"
12+
)
13+
14+
// Server provides an HTTP API for sending messages.
15+
type Server struct {
16+
clients []*ilink.Client
17+
addr string
18+
}
19+
20+
// NewServer creates an API server.
21+
func NewServer(clients []*ilink.Client, addr string) *Server {
22+
if addr == "" {
23+
addr = "127.0.0.1:18011"
24+
}
25+
return &Server{clients: clients, addr: addr}
26+
}
27+
28+
// SendRequest is the JSON body for POST /api/send.
29+
type SendRequest struct {
30+
To string `json:"to"`
31+
Text string `json:"text"`
32+
}
33+
34+
// Run starts the HTTP server. Blocks until ctx is cancelled.
35+
func (s *Server) Run(ctx context.Context) error {
36+
mux := http.NewServeMux()
37+
mux.HandleFunc("/api/send", s.handleSend)
38+
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
39+
w.WriteHeader(http.StatusOK)
40+
fmt.Fprintln(w, "ok")
41+
})
42+
43+
srv := &http.Server{Addr: s.addr, Handler: mux}
44+
45+
go func() {
46+
<-ctx.Done()
47+
srv.Shutdown(context.Background())
48+
}()
49+
50+
log.Printf("[api] listening on %s", s.addr)
51+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
52+
return err
53+
}
54+
return nil
55+
}
56+
57+
func (s *Server) handleSend(w http.ResponseWriter, r *http.Request) {
58+
if r.Method != http.MethodPost {
59+
http.Error(w, "POST only", http.StatusMethodNotAllowed)
60+
return
61+
}
62+
63+
var req SendRequest
64+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
65+
http.Error(w, "invalid JSON: "+err.Error(), http.StatusBadRequest)
66+
return
67+
}
68+
69+
if req.To == "" || req.Text == "" {
70+
http.Error(w, `"to" and "text" are required`, http.StatusBadRequest)
71+
return
72+
}
73+
74+
if len(s.clients) == 0 {
75+
http.Error(w, "no accounts configured", http.StatusServiceUnavailable)
76+
return
77+
}
78+
79+
// Use the first client
80+
client := s.clients[0]
81+
if err := messaging.SendTextReply(r.Context(), client, req.To, req.Text, "", ""); err != nil {
82+
log.Printf("[api] send failed: %v", err)
83+
http.Error(w, "send failed: "+err.Error(), http.StatusInternalServerError)
84+
return
85+
}
86+
87+
log.Printf("[api] sent message to %s: %q", req.To, req.Text)
88+
w.Header().Set("Content-Type", "application/json")
89+
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
90+
}

cmd/send.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os/signal"
7+
"syscall"
8+
9+
"github.com/fastclaw-ai/weclaw/ilink"
10+
"github.com/fastclaw-ai/weclaw/messaging"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
var (
15+
sendTo string
16+
sendText string
17+
)
18+
19+
func init() {
20+
sendCmd.Flags().StringVar(&sendTo, "to", "", "Target user ID (ilink user ID)")
21+
sendCmd.Flags().StringVar(&sendText, "text", "", "Message text to send")
22+
sendCmd.MarkFlagRequired("to")
23+
sendCmd.MarkFlagRequired("text")
24+
rootCmd.AddCommand(sendCmd)
25+
}
26+
27+
var sendCmd = &cobra.Command{
28+
Use: "send",
29+
Short: "Send a message to a WeChat user",
30+
Example: ` weclaw send --to "user_id@im.wechat" --text "Hello from weclaw"`,
31+
RunE: func(cmd *cobra.Command, args []string) error {
32+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
33+
defer cancel()
34+
35+
accounts, err := ilink.LoadAllCredentials()
36+
if err != nil {
37+
return fmt.Errorf("load credentials: %w", err)
38+
}
39+
if len(accounts) == 0 {
40+
return fmt.Errorf("no accounts found, run 'weclaw start' first")
41+
}
42+
43+
// Use the first account
44+
client := ilink.NewClient(accounts[0])
45+
if err := messaging.SendTextReply(ctx, client, sendTo, sendText, "", ""); err != nil {
46+
return fmt.Errorf("send failed: %w", err)
47+
}
48+
49+
fmt.Println("Message sent")
50+
return nil
51+
},
52+
}

0 commit comments

Comments
 (0)