pikiclaw 0.2.65 → 0.2.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -138
- package/dist/bot-weixin.js +268 -0
- package/dist/bot.js +2 -0
- package/dist/channel-weixin.js +189 -0
- package/dist/cli-channels.js +11 -3
- package/dist/cli.js +16 -2
- package/dist/config-validation.js +59 -1
- package/dist/constants.js +16 -0
- package/dist/dashboard-routes-config.js +45 -1
- package/dist/dashboard-ui.js +10 -10
- package/dist/dashboard.js +29 -7
- package/dist/onboarding.js +25 -1
- package/dist/user-config.js +6 -0
- package/dist/weixin-api.js +489 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
**Put the world's smartest AI agents in your pocket. Command local Claude, Codex & Gemini via best IM.**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
*Let the best IM app become a top-tier Agent console on your computer*
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
```
|
|
10
|
+
npx pikiclaw@latest
|
|
11
|
+
```
|
|
12
12
|
|
|
13
|
-
<p
|
|
13
|
+
<p>
|
|
14
14
|
<a href="https://www.npmjs.com/package/pikiclaw"><img src="https://img.shields.io/npm/v/pikiclaw" alt="npm"></a>
|
|
15
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
16
16
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node.js-18+-green.svg" alt="Node.js 18+"></a>
|
|
@@ -18,188 +18,191 @@
|
|
|
18
18
|
|
|
19
19
|
</div>
|
|
20
20
|
|
|
21
|
+
## Demo
|
|
22
|
+
|
|
23
|
+
> Real task: ask pikiclaw to gather and summarize today's AI news — the agent reads, writes, and sends results back through Telegram, all from your phone.
|
|
24
|
+
|
|
25
|
+
<video src="docs/promo-demo.mp4" width="700" controls muted></video>
|
|
26
|
+
|
|
27
|
+
> Basic operations: send a message, watch the agent stream, receive files back.
|
|
28
|
+
|
|
29
|
+
<img src="docs/promo-basic-ops.gif" alt="Basic operations" width="700">
|
|
30
|
+
|
|
21
31
|
---
|
|
22
32
|
|
|
23
33
|
## Why pikiclaw?
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
Most "IM + Agent" solutions either reinvent the agent (worse than official CLIs), run in remote sandboxes (not your environment), or only support short conversations (unusable for real tasks).
|
|
26
36
|
|
|
27
|
-
|
|
28
|
-
- 要么跑在远端沙盒里,不是你的环境
|
|
29
|
-
- 要么只能短对话,不适合长任务
|
|
37
|
+
pikiclaw takes a different approach:
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
- 用你自己的电脑,而不是陌生沙盒
|
|
35
|
-
- 用你已经在用的 IM,而不是再学一套远程控制方式
|
|
39
|
+
- **Official Agent CLIs** — Claude Code, Codex, Gemini CLI as-is, not a home-grown wrapper
|
|
40
|
+
- **Your own machine** — local files, local tools, local environment
|
|
41
|
+
- **Your existing IM** — Telegram, Feishu, or WeChat, no new app to learn
|
|
36
42
|
|
|
37
43
|
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
You (Telegram / Feishu / WeChat)
|
|
45
|
+
|
|
|
46
|
+
v
|
|
41
47
|
pikiclaw
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
Claude Code / Codex / Gemini
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
|
|
|
49
|
+
v
|
|
50
|
+
Claude Code / Codex / Gemini CLI
|
|
51
|
+
|
|
|
52
|
+
v
|
|
53
|
+
Your Computer
|
|
48
54
|
```
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
### 在 Telegram 里长这样
|
|
53
|
-
|
|
54
|
-
<table>
|
|
55
|
-
<tr>
|
|
56
|
-
<td align=”center”><b>命令与 Agent 切换</b><br><img src=”docs/promo-tg-commands.png” alt=”Commands” width=”320”></td>
|
|
57
|
-
<td align=”center”><b>代码审查</b><br><img src=”docs/promo-tg-task.png” alt=”Code review” width=”320”></td>
|
|
58
|
-
</tr>
|
|
59
|
-
<tr>
|
|
60
|
-
<td align=”center”><b>多轮编码 + 文件回传</b><br><img src=”docs/promo-tg-complex.png” alt=”Complex task” width=”320”></td>
|
|
61
|
-
<td align=”center”><b>状态监控 + 会话管理</b><br><img src=”docs/promo-tg-sessions.png” alt=”Sessions” width=”320”></td>
|
|
62
|
-
</tr>
|
|
63
|
-
</table>
|
|
56
|
+
It's designed for the moment you walk away from your desk — the agent keeps working locally, and you stay in control from your phone.
|
|
64
57
|
|
|
65
58
|
---
|
|
66
59
|
|
|
67
60
|
## Quick Start
|
|
68
61
|
|
|
69
|
-
###
|
|
62
|
+
### Prerequisites
|
|
70
63
|
|
|
71
64
|
- Node.js 18+
|
|
72
|
-
-
|
|
73
|
-
- [`claude`](https://docs.anthropic.com/en/docs/claude-code)
|
|
74
|
-
- [`codex`](https://github.com/openai/codex)
|
|
75
|
-
- [`gemini`](https://github.com/google-gemini/gemini-cli)
|
|
76
|
-
- Telegram Bot Token
|
|
65
|
+
- At least one Agent CLI installed and logged in:
|
|
66
|
+
- [`claude`](https://docs.anthropic.com/en/docs/claude-code) (Claude Code)
|
|
67
|
+
- [`codex`](https://github.com/openai/codex) (Codex CLI)
|
|
68
|
+
- [`gemini`](https://github.com/google-gemini/gemini-cli) (Gemini CLI)
|
|
69
|
+
- A bot token for your IM channel (Telegram Bot Token, Feishu app credentials, or WeChat account)
|
|
77
70
|
|
|
78
|
-
###
|
|
71
|
+
### Install & Launch
|
|
79
72
|
|
|
80
73
|
```bash
|
|
81
74
|
cd your-workspace
|
|
82
75
|
npx pikiclaw@latest
|
|
83
76
|
```
|
|
84
77
|
|
|
85
|
-
|
|
78
|
+
<img src="docs/promo-install.gif" alt="Quick install" width="700">
|
|
86
79
|
|
|
87
|
-
|
|
80
|
+
This opens the **Web Dashboard** at `http://localhost:3939`, where you can:
|
|
88
81
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
82
|
+
- Connect IM channels (Telegram / Feishu / WeChat)
|
|
83
|
+
- Configure agents and models
|
|
84
|
+
- Manage macOS system permissions
|
|
85
|
+
- Set up browser & desktop automation extensions
|
|
86
|
+
- Monitor sessions and system resources
|
|
93
87
|
|
|
94
88
|
<details>
|
|
95
|
-
<summary>
|
|
89
|
+
<summary>Alternative: terminal setup wizard</summary>
|
|
96
90
|
|
|
97
|
-
|
|
91
|
+
```bash
|
|
92
|
+
npx pikiclaw@latest --setup # interactive terminal wizard
|
|
93
|
+
npx pikiclaw@latest --doctor # check environment only
|
|
94
|
+
```
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
</details>
|
|
100
97
|
|
|
101
|
-
|
|
98
|
+
---
|
|
102
99
|
|
|
103
|
-
|
|
100
|
+
## Dashboard
|
|
104
101
|
|
|
105
|
-
|
|
102
|
+
<details>
|
|
103
|
+
<summary>Expand to see all dashboard pages</summary>
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
**IM Access** — Telegram, Feishu, WeChat channel status and configuration
|
|
108
106
|
|
|
109
|
-
|
|
110
|
-
npx pikiclaw@latest --setup
|
|
111
|
-
```
|
|
107
|
+
<img src="docs/promo-dashboard-im.png" alt="IM Access" width="700">
|
|
112
108
|
|
|
113
|
-
|
|
109
|
+
**Agent Config** — Default agent / model / reasoning effort, available agents overview
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
<img src="docs/promo-dashboard-agents.png" alt="Agent Config" width="700">
|
|
112
|
+
|
|
113
|
+
**System Permissions** — macOS accessibility, screen recording, disk access
|
|
114
|
+
|
|
115
|
+
<img src="docs/promo-dashboard-permissions.png" alt="Permissions" width="700">
|
|
116
|
+
|
|
117
|
+
**Extensions** — Managed browser & desktop automation (Appium Mac2)
|
|
118
|
+
|
|
119
|
+
<img src="docs/promo-dashboard-extensions.png" alt="Extensions" width="700">
|
|
120
|
+
|
|
121
|
+
**Sessions** — Per-agent session list and runtime status
|
|
122
|
+
|
|
123
|
+
<img src="docs/promo-dashboard-sessions.png" alt="Sessions" width="700">
|
|
124
|
+
|
|
125
|
+
**System Info** — Working directory, CPU / memory / disk monitoring
|
|
126
|
+
|
|
127
|
+
<img src="docs/promo-dashboard-system.png" alt="System Info" width="700">
|
|
128
|
+
|
|
129
|
+
</details>
|
|
118
130
|
|
|
119
131
|
---
|
|
120
132
|
|
|
121
|
-
##
|
|
133
|
+
## Features
|
|
122
134
|
|
|
123
|
-
### Channels
|
|
135
|
+
### Channels & Agents
|
|
124
136
|
|
|
125
|
-
- Telegram
|
|
126
|
-
- Claude Code
|
|
127
|
-
-
|
|
137
|
+
- Telegram, Feishu, and WeChat — run one or all simultaneously
|
|
138
|
+
- Claude Code, Codex CLI, and Gemini CLI via unified driver registry
|
|
139
|
+
- Model listing, session management, and usage tracking through a single interface
|
|
128
140
|
|
|
129
141
|
### Runtime
|
|
130
142
|
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
143
|
+
- Streaming preview with continuous message updates
|
|
144
|
+
- Session switching, resume, and multi-turn conversations
|
|
145
|
+
- Working directory browsing and switching
|
|
146
|
+
- File attachments automatically enter the session workspace
|
|
147
|
+
- Long-task sleep prevention, watchdog, and auto-restart
|
|
148
|
+
- Long text auto-splitting; images and files sent back to IM directly
|
|
149
|
+
- Light / dark theme and i18n (Chinese & English)
|
|
138
150
|
|
|
139
151
|
### Skills
|
|
140
152
|
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
153
|
+
- Project-level skills at `.pikiclaw/skills/*/SKILL.md`
|
|
154
|
+
- Compatible with `.claude/commands/*.md`
|
|
155
|
+
- Legacy `.claude/skills` / `.agents/skills` support with migration path
|
|
156
|
+
- Trigger via `/skills` and `/sk_<name>` in chat
|
|
145
157
|
|
|
146
158
|
### Codex Human Loop
|
|
147
159
|
|
|
148
|
-
|
|
160
|
+
When Codex requests additional user input mid-task, pikiclaw surfaces the question as an interactive prompt in your IM. Reply there and the task continues.
|
|
149
161
|
|
|
150
|
-
### MCP
|
|
162
|
+
### MCP & GUI Automation
|
|
151
163
|
|
|
152
|
-
|
|
164
|
+
Each agent stream launches a session-scoped MCP bridge that injects local tools:
|
|
153
165
|
|
|
154
|
-
|
|
166
|
+
- `im_list_files` — list session workspace files
|
|
167
|
+
- `im_send_file` — send files back to IM in real time
|
|
155
168
|
|
|
156
|
-
|
|
157
|
-
- `im_send_file`:把文件实时发回 IM
|
|
169
|
+
Optional GUI capabilities:
|
|
158
170
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
- 浏览器自动化:通过 `@playwright/mcp` 管理一个专用的持久化 Chrome profile;第一次使用时在这个自动化浏览器里登录需要的网站,后续任务会复用同一个 profile
|
|
162
|
-
- macOS 桌面自动化:通过 Appium Mac2 提供 `desktop_open_app`、`desktop_snapshot`、`desktop_click`、`desktop_type`、`desktop_screenshot` 等工具
|
|
171
|
+
- **Browser automation** — managed Chrome profile via `@playwright/mcp`; log in once, reuse across tasks
|
|
172
|
+
- **macOS desktop automation** — Appium Mac2 with `desktop_open_app`, `desktop_snapshot`, `desktop_click`, `desktop_type`, `desktop_screenshot`
|
|
163
173
|
|
|
164
174
|
---
|
|
165
175
|
|
|
166
176
|
## Commands
|
|
167
177
|
|
|
168
|
-
|
|
|
178
|
+
| Command | Description |
|
|
169
179
|
|---|---|
|
|
170
|
-
| `/start` |
|
|
171
|
-
| `/sessions` |
|
|
172
|
-
| `/agents` |
|
|
173
|
-
| `/models` |
|
|
174
|
-
| `/switch` |
|
|
175
|
-
| `/status` |
|
|
176
|
-
| `/host` |
|
|
177
|
-
| `/skills` |
|
|
178
|
-
| `/restart` |
|
|
179
|
-
| `/sk_<name>` |
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
<details>
|
|
184
|
-
<summary>Telegram 命令效果预览</summary>
|
|
185
|
-
|
|
186
|
-
<img src="docs/promo-tg-commands.png" alt="Commands in Telegram" width="360">
|
|
187
|
-
|
|
188
|
-
</details>
|
|
180
|
+
| `/start` | Show entry info, current agent, working directory |
|
|
181
|
+
| `/sessions` | View, switch, or create sessions |
|
|
182
|
+
| `/agents` | Switch agent |
|
|
183
|
+
| `/models` | View and switch model / reasoning effort |
|
|
184
|
+
| `/switch` | Browse and switch working directory |
|
|
185
|
+
| `/status` | Runtime status, tokens, usage, session info |
|
|
186
|
+
| `/host` | Host CPU / memory / disk / battery |
|
|
187
|
+
| `/skills` | Browse project skills |
|
|
188
|
+
| `/restart` | Restart and re-launch bot |
|
|
189
|
+
| `/sk_<name>` | Run a project skill |
|
|
190
|
+
|
|
191
|
+
Plain text messages are forwarded directly to the current agent.
|
|
189
192
|
|
|
190
193
|
---
|
|
191
194
|
|
|
192
|
-
##
|
|
195
|
+
## Configuration
|
|
193
196
|
|
|
194
|
-
-
|
|
195
|
-
- Dashboard
|
|
196
|
-
- 桌面 GUI 相关常用变量:
|
|
197
|
-
- `PIKICLAW_DESKTOP_GUI`
|
|
198
|
-
- `PIKICLAW_DESKTOP_APPIUM_URL`
|
|
197
|
+
- Persistent config lives in `~/.pikiclaw/setting.json`
|
|
198
|
+
- The Dashboard is the primary configuration interface
|
|
199
199
|
|
|
200
|
-
|
|
200
|
+
<details>
|
|
201
|
+
<summary>GUI automation setup</summary>
|
|
201
202
|
|
|
202
|
-
|
|
203
|
+
**Browser automation** is managed by the dashboard and runtime together — a dedicated Chrome profile is created and reused automatically. Just log in to the sites you need once in that browser.
|
|
204
|
+
|
|
205
|
+
**macOS desktop automation** requires Appium Mac2:
|
|
203
206
|
|
|
204
207
|
```bash
|
|
205
208
|
npm install -g appium
|
|
@@ -207,15 +210,21 @@ appium driver install mac2
|
|
|
207
210
|
appium
|
|
208
211
|
```
|
|
209
212
|
|
|
210
|
-
|
|
213
|
+
Then grant macOS Accessibility permission to your terminal app.
|
|
214
|
+
|
|
215
|
+
Relevant environment variables:
|
|
216
|
+
- `PIKICLAW_DESKTOP_GUI`
|
|
217
|
+
- `PIKICLAW_DESKTOP_APPIUM_URL`
|
|
218
|
+
|
|
219
|
+
</details>
|
|
211
220
|
|
|
212
221
|
---
|
|
213
222
|
|
|
214
223
|
## Roadmap
|
|
215
224
|
|
|
216
|
-
-
|
|
217
|
-
-
|
|
218
|
-
-
|
|
225
|
+
- Expand session-scoped MCP bridge into a more complete top-level tool layer
|
|
226
|
+
- Improve GUI automation, especially browser + desktop tool coordination
|
|
227
|
+
- More IM channels (WhatsApp, etc.)
|
|
219
228
|
|
|
220
229
|
---
|
|
221
230
|
|
|
@@ -229,25 +238,15 @@ npm run build
|
|
|
229
238
|
npm test
|
|
230
239
|
```
|
|
231
240
|
|
|
232
|
-
常用命令:
|
|
233
|
-
|
|
234
241
|
```bash
|
|
235
|
-
npm run dev
|
|
236
|
-
npm run build
|
|
237
|
-
npm test
|
|
238
|
-
npm run test:e2e
|
|
239
|
-
npx
|
|
240
|
-
npx pikiclaw@latest --doctor
|
|
242
|
+
npm run dev # local dev (--no-daemon, logs to ~/.pikiclaw/dev/dev.log)
|
|
243
|
+
npm run build # production build
|
|
244
|
+
npm test # unit tests
|
|
245
|
+
npm run test:e2e # end-to-end tests
|
|
246
|
+
npx pikiclaw@latest --doctor # environment check
|
|
241
247
|
```
|
|
242
248
|
|
|
243
|
-
|
|
244
|
-
同时会把本次启动的全部日志写到 `~/.pikiclaw/dev/dev.log`,并在每次启动时先清空旧日志。
|
|
245
|
-
|
|
246
|
-
更多实现细节见:
|
|
247
|
-
|
|
248
|
-
- [ARCHITECTURE.md](ARCHITECTURE.md)
|
|
249
|
-
- [INTEGRATION.md](INTEGRATION.md)
|
|
250
|
-
- [TESTING.md](TESTING.md)
|
|
249
|
+
See also: [ARCHITECTURE.md](ARCHITECTURE.md) · [INTEGRATION.md](INTEGRATION.md) · [TESTING.md](TESTING.md)
|
|
251
250
|
|
|
252
251
|
---
|
|
253
252
|
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Bot, buildPrompt, fmtUptime, normalizeAgent, parseAllowedChatIds, } from './bot.js';
|
|
5
|
+
import { BOT_SHUTDOWN_FORCE_EXIT_MS, buildSessionTaskId } from './bot-orchestration.js';
|
|
6
|
+
import { shutdownAllDrivers } from './agent-driver.js';
|
|
7
|
+
import { registerProcessRuntime } from './process-control.js';
|
|
8
|
+
import { WeixinChannel } from './channel-weixin.js';
|
|
9
|
+
import { getActiveUserConfig } from './user-config.js';
|
|
10
|
+
const SHUTDOWN_EXIT_CODE = {
|
|
11
|
+
SIGINT: 130,
|
|
12
|
+
SIGTERM: 143,
|
|
13
|
+
};
|
|
14
|
+
function describeError(error) {
|
|
15
|
+
return error instanceof Error ? error.message : String(error ?? 'unknown error');
|
|
16
|
+
}
|
|
17
|
+
export class WeixinBot extends Bot {
|
|
18
|
+
botToken;
|
|
19
|
+
accountId;
|
|
20
|
+
baseUrl;
|
|
21
|
+
channel;
|
|
22
|
+
nextTaskId = 1;
|
|
23
|
+
shutdownInFlight = false;
|
|
24
|
+
shutdownExitCode = null;
|
|
25
|
+
shutdownForceExitTimer = null;
|
|
26
|
+
signalHandlers = {};
|
|
27
|
+
processRuntimeCleanup = null;
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
const config = getActiveUserConfig();
|
|
31
|
+
if (process.env.WEIXIN_ALLOWED_USER_IDS) {
|
|
32
|
+
for (const id of parseAllowedChatIds(process.env.WEIXIN_ALLOWED_USER_IDS))
|
|
33
|
+
this.allowedChatIds.add(id);
|
|
34
|
+
}
|
|
35
|
+
this.baseUrl = String(config.weixinBaseUrl || process.env.WEIXIN_BASE_URL || '').trim();
|
|
36
|
+
this.botToken = String(config.weixinBotToken || process.env.WEIXIN_BOT_TOKEN || '').trim();
|
|
37
|
+
this.accountId = String(config.weixinAccountId || process.env.WEIXIN_ACCOUNT_ID || '').trim();
|
|
38
|
+
if (!this.baseUrl || !this.botToken || !this.accountId) {
|
|
39
|
+
throw new Error('Missing Weixin credentials. Configure via dashboard QR login first.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
onManagedConfigChange(config, opts = {}) {
|
|
43
|
+
const nextBaseUrl = String(config.weixinBaseUrl || process.env.WEIXIN_BASE_URL || '').trim();
|
|
44
|
+
const nextBotToken = String(config.weixinBotToken || process.env.WEIXIN_BOT_TOKEN || '').trim();
|
|
45
|
+
const nextAccountId = String(config.weixinAccountId || process.env.WEIXIN_ACCOUNT_ID || '').trim();
|
|
46
|
+
if (nextBaseUrl && nextBaseUrl !== this.baseUrl) {
|
|
47
|
+
this.baseUrl = nextBaseUrl;
|
|
48
|
+
if (!opts.initial)
|
|
49
|
+
this.log('weixin baseUrl reloaded from setting.json');
|
|
50
|
+
}
|
|
51
|
+
if (nextBotToken && nextBotToken !== this.botToken) {
|
|
52
|
+
this.botToken = nextBotToken;
|
|
53
|
+
if (!opts.initial)
|
|
54
|
+
this.log('weixin botToken reloaded from setting.json');
|
|
55
|
+
}
|
|
56
|
+
if (nextAccountId && nextAccountId !== this.accountId) {
|
|
57
|
+
this.accountId = nextAccountId;
|
|
58
|
+
if (!opts.initial)
|
|
59
|
+
this.log('weixin accountId reloaded from setting.json');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
installSignalHandlers() {
|
|
63
|
+
this.removeSignalHandlers();
|
|
64
|
+
const onSigint = () => this.beginShutdown('SIGINT');
|
|
65
|
+
const onSigterm = () => this.beginShutdown('SIGTERM');
|
|
66
|
+
this.signalHandlers = { SIGINT: onSigint, SIGTERM: onSigterm };
|
|
67
|
+
process.once('SIGINT', onSigint);
|
|
68
|
+
process.once('SIGTERM', onSigterm);
|
|
69
|
+
}
|
|
70
|
+
removeSignalHandlers() {
|
|
71
|
+
for (const signal of Object.keys(this.signalHandlers)) {
|
|
72
|
+
const handler = this.signalHandlers[signal];
|
|
73
|
+
if (handler)
|
|
74
|
+
process.off(signal, handler);
|
|
75
|
+
}
|
|
76
|
+
this.signalHandlers = {};
|
|
77
|
+
}
|
|
78
|
+
clearShutdownForceExitTimer() {
|
|
79
|
+
if (!this.shutdownForceExitTimer)
|
|
80
|
+
return;
|
|
81
|
+
clearTimeout(this.shutdownForceExitTimer);
|
|
82
|
+
this.shutdownForceExitTimer = null;
|
|
83
|
+
}
|
|
84
|
+
cleanupRuntimeForExit() {
|
|
85
|
+
try {
|
|
86
|
+
this.channel.disconnect();
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
this.stopKeepAlive();
|
|
90
|
+
shutdownAllDrivers();
|
|
91
|
+
}
|
|
92
|
+
beginShutdown(signal) {
|
|
93
|
+
if (this.shutdownInFlight)
|
|
94
|
+
return;
|
|
95
|
+
this.shutdownInFlight = true;
|
|
96
|
+
this.shutdownExitCode = SHUTDOWN_EXIT_CODE[signal];
|
|
97
|
+
this.log(`${signal}, shutting down...`);
|
|
98
|
+
this.cleanupRuntimeForExit();
|
|
99
|
+
this.clearShutdownForceExitTimer();
|
|
100
|
+
this.shutdownForceExitTimer = setTimeout(() => {
|
|
101
|
+
this.log(`shutdown still pending after ${Math.floor(BOT_SHUTDOWN_FORCE_EXIT_MS / 1000)}s, forcing exit`);
|
|
102
|
+
process.exit(this.shutdownExitCode ?? 1);
|
|
103
|
+
}, BOT_SHUTDOWN_FORCE_EXIT_MS);
|
|
104
|
+
this.shutdownForceExitTimer.unref?.();
|
|
105
|
+
}
|
|
106
|
+
resolveSession(chatId, title, files) {
|
|
107
|
+
return this.ensureSessionForChat(chatId, title, files);
|
|
108
|
+
}
|
|
109
|
+
buildStatusText(chatId) {
|
|
110
|
+
const status = this.getStatusData(chatId);
|
|
111
|
+
return [
|
|
112
|
+
`Agent: ${status.agent}`,
|
|
113
|
+
`Model: ${status.model || '-'}`,
|
|
114
|
+
`Session: ${status.sessionId || 'new'}`,
|
|
115
|
+
`Tasks: ${status.activeTasksCount}`,
|
|
116
|
+
`Workdir: ${status.workdir}`,
|
|
117
|
+
`Uptime: ${fmtUptime(status.uptime)}`,
|
|
118
|
+
].join('\n');
|
|
119
|
+
}
|
|
120
|
+
async handleCommand(text, ctx) {
|
|
121
|
+
const [rawCommand, ...rest] = text.trim().slice(1).split(/\s+/);
|
|
122
|
+
const command = rawCommand?.toLowerCase() || '';
|
|
123
|
+
const args = rest.join(' ').trim();
|
|
124
|
+
switch (command) {
|
|
125
|
+
case 'help':
|
|
126
|
+
await ctx.reply([
|
|
127
|
+
'/help',
|
|
128
|
+
'/new',
|
|
129
|
+
'/status',
|
|
130
|
+
'/agent codex|claude|gemini',
|
|
131
|
+
].join('\n'));
|
|
132
|
+
return true;
|
|
133
|
+
case 'new':
|
|
134
|
+
this.resetConversationForChat(ctx.chatId);
|
|
135
|
+
await ctx.reply('Started a new session.');
|
|
136
|
+
return true;
|
|
137
|
+
case 'status':
|
|
138
|
+
await ctx.reply(this.buildStatusText(ctx.chatId));
|
|
139
|
+
return true;
|
|
140
|
+
case 'agent':
|
|
141
|
+
if (!args) {
|
|
142
|
+
await ctx.reply('Usage: /agent codex|claude|gemini');
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const agent = normalizeAgent(args);
|
|
147
|
+
this.switchAgentForChat(ctx.chatId, agent);
|
|
148
|
+
await ctx.reply(`Agent switched to ${agent}.`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
await ctx.reply('Usage: /agent codex|claude|gemini');
|
|
152
|
+
}
|
|
153
|
+
return true;
|
|
154
|
+
default:
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
createMcpSendFile(chatId) {
|
|
159
|
+
return async (filePath) => {
|
|
160
|
+
try {
|
|
161
|
+
await this.channel.send(chatId, `Artifact ready: ${path.basename(filePath)}\n${filePath}`);
|
|
162
|
+
return { ok: true };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
return { ok: false, error: describeError(error) };
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
async sendResult(chatId, result) {
|
|
170
|
+
const text = result.ok
|
|
171
|
+
? (result.message.trim() || 'Task finished.')
|
|
172
|
+
: ['Task failed.', result.error || result.message || 'Unknown error.'].filter(Boolean).join('\n');
|
|
173
|
+
await this.channel.send(chatId, text);
|
|
174
|
+
}
|
|
175
|
+
async handleMessage(msg, ctx) {
|
|
176
|
+
const text = msg.text.trim();
|
|
177
|
+
if (text.startsWith('/') && await this.handleCommand(text, ctx))
|
|
178
|
+
return;
|
|
179
|
+
if (!text && !msg.files.length) {
|
|
180
|
+
await ctx.reply('This Weixin channel currently supports text input only.');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const session = this.resolveSession(ctx.chatId, text, msg.files);
|
|
184
|
+
const prompt = buildPrompt(text, msg.files);
|
|
185
|
+
const taskId = buildSessionTaskId(session, this.nextTaskId++);
|
|
186
|
+
this.beginTask({
|
|
187
|
+
taskId,
|
|
188
|
+
chatId: ctx.chatId,
|
|
189
|
+
agent: session.agent,
|
|
190
|
+
sessionKey: session.key,
|
|
191
|
+
prompt,
|
|
192
|
+
startedAt: Date.now(),
|
|
193
|
+
sourceMessageId: ctx.messageId,
|
|
194
|
+
});
|
|
195
|
+
void this.queueSessionTask(session, async () => {
|
|
196
|
+
const abortController = new AbortController();
|
|
197
|
+
const task = this.markTaskRunning(taskId, () => abortController.abort());
|
|
198
|
+
if (task?.cancelled) {
|
|
199
|
+
this.finishTask(taskId);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
let typingTimer = null;
|
|
203
|
+
try {
|
|
204
|
+
await ctx.sendTyping().catch(() => { });
|
|
205
|
+
typingTimer = setInterval(() => {
|
|
206
|
+
void ctx.sendTyping().catch(() => { });
|
|
207
|
+
}, 4_000);
|
|
208
|
+
typingTimer.unref?.();
|
|
209
|
+
const result = await this.runStream(prompt, session, msg.files, () => { }, undefined, this.createMcpSendFile(ctx.chatId), abortController.signal);
|
|
210
|
+
await this.sendResult(ctx.chatId, result);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
await ctx.reply(`Error: ${describeError(error)}`);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (typingTimer)
|
|
217
|
+
clearInterval(typingTimer);
|
|
218
|
+
this.finishTask(taskId);
|
|
219
|
+
this.syncSelectedChats(session);
|
|
220
|
+
}
|
|
221
|
+
}).catch(error => {
|
|
222
|
+
this.finishTask(taskId);
|
|
223
|
+
this.log(`weixin queue execution failed: ${describeError(error)}`);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
async run() {
|
|
227
|
+
const tmpDir = path.join(os.tmpdir(), 'pikiclaw');
|
|
228
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
229
|
+
this.channel = new WeixinChannel({
|
|
230
|
+
token: this.botToken,
|
|
231
|
+
accountId: this.accountId,
|
|
232
|
+
baseUrl: this.baseUrl,
|
|
233
|
+
allowedChatIds: this.allowedChatIds.size ? new Set([...this.allowedChatIds].map(value => String(value))) : undefined,
|
|
234
|
+
});
|
|
235
|
+
this.processRuntimeCleanup?.();
|
|
236
|
+
this.processRuntimeCleanup = registerProcessRuntime({
|
|
237
|
+
label: 'weixin',
|
|
238
|
+
getActiveTaskCount: () => this.activeTasks.size,
|
|
239
|
+
prepareForRestart: () => this.cleanupRuntimeForExit(),
|
|
240
|
+
});
|
|
241
|
+
this.installSignalHandlers();
|
|
242
|
+
try {
|
|
243
|
+
const bot = await this.channel.connect();
|
|
244
|
+
this.connected = true;
|
|
245
|
+
this.log(`bot: ${bot.displayName} (id=${bot.id})`);
|
|
246
|
+
for (const agent of this.fetchAgents().agents) {
|
|
247
|
+
this.log(`agent ${agent.agent}: ${agent.path || 'NOT FOUND'}`);
|
|
248
|
+
}
|
|
249
|
+
this.log(`config: agent=${this.defaultAgent} workdir=${this.workdir} timeout=${this.runTimeout}s`);
|
|
250
|
+
this.channel.onMessage((msg, ctx) => this.handleMessage(msg, ctx));
|
|
251
|
+
this.channel.onError(error => this.log(`error: ${describeError(error)}`));
|
|
252
|
+
this.startKeepAlive();
|
|
253
|
+
this.log('✓ Weixin connected, long-polling started — ready to receive messages');
|
|
254
|
+
await this.channel.listen();
|
|
255
|
+
this.stopKeepAlive();
|
|
256
|
+
this.log('stopped');
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
this.stopKeepAlive();
|
|
260
|
+
this.clearShutdownForceExitTimer();
|
|
261
|
+
this.removeSignalHandlers();
|
|
262
|
+
this.processRuntimeCleanup?.();
|
|
263
|
+
this.processRuntimeCleanup = null;
|
|
264
|
+
if (this.shutdownInFlight)
|
|
265
|
+
process.exit(this.shutdownExitCode ?? 1);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
package/dist/bot.js
CHANGED
|
@@ -1004,6 +1004,7 @@ export class Bot {
|
|
|
1004
1004
|
const totalMem = os.totalmem(), freeMem = os.freemem();
|
|
1005
1005
|
const memory = getHostMemoryUsageData(totalMem, freeMem);
|
|
1006
1006
|
const cpuUsage = getHostCpuUsageData();
|
|
1007
|
+
const [loadOne, loadFive, loadFifteen] = os.loadavg();
|
|
1007
1008
|
let disk = null;
|
|
1008
1009
|
const battery = getHostBatteryData();
|
|
1009
1010
|
try {
|
|
@@ -1022,6 +1023,7 @@ export class Bot {
|
|
|
1022
1023
|
hostName: getHostDisplayName(),
|
|
1023
1024
|
cpuModel: cpus[0]?.model || 'unknown', cpuCount: cpus.length,
|
|
1024
1025
|
cpuUsage,
|
|
1026
|
+
loadAverage: { one: loadOne, five: loadFive, fifteen: loadFifteen },
|
|
1025
1027
|
totalMem, freeMem, memoryUsed: memory.usedBytes, memoryAvailable: memory.availableBytes, memoryPercent: memory.percent, memorySource: memory.source,
|
|
1026
1028
|
disk, battery, topProcs,
|
|
1027
1029
|
selfPid: process.pid, selfRss: mem.rss, selfHeap: mem.heapUsed,
|