pikiclaw 0.2.64 → 0.2.66

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 CHANGED
@@ -98,10 +98,6 @@ npx pikiclaw@latest
98
98
 
99
99
  <img src="docs/promo-dashboard-config.png" alt="Config" width="700">
100
100
 
101
- **插件中心** — 浏览器操控、桌面自动化
102
-
103
- <img src="docs/promo-dashboard-extensions.png" alt="Extensions" width="700">
104
-
105
101
  **会话管理** — 按 Agent 分组的会话泳道
106
102
 
107
103
  <img src="docs/promo-dashboard-sessions.png" alt="Sessions" width="700">
@@ -162,7 +158,7 @@ npx pikiclaw@latest --doctor
162
158
 
163
159
  可选 GUI 能力:
164
160
 
165
- - 浏览器自动化:通过 `@playwright/mcp` 补充接入,默认支持 Chrome extension mode,也可切到 headless / isolated 模式
161
+ - 浏览器自动化:通过 `@playwright/mcp` 管理一个专用的持久化 Chrome profile;第一次使用时在这个自动化浏览器里登录需要的网站,后续任务会复用同一个 profile
166
162
  - macOS 桌面自动化:通过 Appium Mac2 提供 `desktop_open_app`、`desktop_snapshot`、`desktop_click`、`desktop_type`、`desktop_screenshot` 等工具
167
163
 
168
164
  ---
@@ -196,17 +192,13 @@ npx pikiclaw@latest --doctor
196
192
  ## Config And Setup Notes
197
193
 
198
194
  - 持久化配置在 `~/.pikiclaw/setting.json`
199
- - Dashboard 是主配置入口,环境变量仍然可用
200
- - 浏览器 GUI 相关常用变量:
201
- - `PIKICLAW_BROWSER_GUI`
202
- - `PIKICLAW_BROWSER_USE_EXTENSION`
203
- - `PIKICLAW_BROWSER_HEADLESS`
204
- - `PIKICLAW_BROWSER_ISOLATED`
205
- - `PLAYWRIGHT_MCP_EXTENSION_TOKEN`
195
+ - Dashboard 是主配置入口,其他运行时配置仍然可用
206
196
  - 桌面 GUI 相关常用变量:
207
197
  - `PIKICLAW_DESKTOP_GUI`
208
198
  - `PIKICLAW_DESKTOP_APPIUM_URL`
209
199
 
200
+ 浏览器自动化由 dashboard 和本地运行时共同管理,会自动创建并复用专用的 Chrome profile 目录。你只需要在这个专用浏览器里登录需要自动化的网站账号一次。
201
+
210
202
  如果要启用 macOS 桌面自动化,需要先准备 Appium Mac2:
211
203
 
212
204
  ```bash
@@ -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
@@ -10,6 +10,7 @@ import { execSync, spawn } from 'node:child_process';
10
10
  import { getActiveUserConfig, onUserConfigChange, resolveUserWorkdir, setUserWorkdir } from './user-config.js';
11
11
  import { doStream, getSessions, getSessionTail, getUsage, initializeProjectSkills, listAgents, listModels, listSkills, stageSessionFiles, isPendingSessionId, normalizeClaudeModelId, } from './code-agent.js';
12
12
  import { getDriver, hasDriver, allDriverIds } from './agent-driver.js';
13
+ import { resolveGuiIntegrationConfig } from './mcp-bridge.js';
13
14
  import { terminateProcessTree } from './process-control.js';
14
15
  import { VERSION } from './version.js';
15
16
  import { buildHumanLoopResponse, createEmptyHumanLoopAnswer, currentHumanLoopQuestion, isHumanLoopAwaitingText, setHumanLoopOption, setHumanLoopText, skipHumanLoopQuestion, } from './human-loop.js';
@@ -221,6 +222,23 @@ function buildMcpDeliveryPrompt() {
221
222
  'This is an IM conversation, so pay attention to the IM tools.',
222
223
  ].join('\n');
223
224
  }
225
+ function buildBrowserAutomationPrompt(browserEnabled) {
226
+ if (!browserEnabled) {
227
+ return [
228
+ '[Browser Automation]',
229
+ 'Managed browser automation is disabled by default for this session.',
230
+ process.platform === 'darwin'
231
+ ? 'On macOS, operate your main browser directly with native commands such as open, osascript, and screencapture when needed.'
232
+ : 'Use native OS or browser commands directly when browser automation is not enabled.',
233
+ ].join('\n');
234
+ }
235
+ return [
236
+ '[Browser Automation]',
237
+ 'A Playwright MCP browser server is already configured to use the local Chrome channel with a persistent profile.',
238
+ 'Do not call browser_install unless a browser tool explicitly reports that Chrome or the browser is missing.',
239
+ 'If you need a new tab, use browser_tabs with action="new".',
240
+ ].join('\n');
241
+ }
224
242
  function configModelValue(config, agent) {
225
243
  switch (agent) {
226
244
  case 'claude': return normalizeClaudeModelId(config.claudeModel || process.env.CLAUDE_MODEL || 'claude-opus-4-6');
@@ -986,6 +1004,7 @@ export class Bot {
986
1004
  const totalMem = os.totalmem(), freeMem = os.freemem();
987
1005
  const memory = getHostMemoryUsageData(totalMem, freeMem);
988
1006
  const cpuUsage = getHostCpuUsageData();
1007
+ const [loadOne, loadFive, loadFifteen] = os.loadavg();
989
1008
  let disk = null;
990
1009
  const battery = getHostBatteryData();
991
1010
  try {
@@ -1004,6 +1023,7 @@ export class Bot {
1004
1023
  hostName: getHostDisplayName(),
1005
1024
  cpuModel: cpus[0]?.model || 'unknown', cpuCount: cpus.length,
1006
1025
  cpuUsage,
1026
+ loadAverage: { one: loadOne, five: loadFive, fifteen: loadFifteen },
1007
1027
  totalMem, freeMem, memoryUsed: memory.usedBytes, memoryAvailable: memory.availableBytes, memoryPercent: memory.percent, memorySource: memory.source,
1008
1028
  disk, battery, topProcs,
1009
1029
  selfPid: process.pid, selfRss: mem.rss, selfHeap: mem.heapUsed,
@@ -1072,11 +1092,15 @@ export class Bot {
1072
1092
  const resolvedModel = cs.modelId || this.modelForAgent(cs.agent);
1073
1093
  const agentConfig = this.agentConfigs[cs.agent] || {};
1074
1094
  const extraArgs = agentConfig.extraArgs || [];
1095
+ const browserEnabled = resolveGuiIntegrationConfig(getActiveUserConfig()).browserEnabled;
1075
1096
  this.log(`[runStream] agent=${cs.agent} session=${cs.sessionId || '(new)'} workdir=${this.workdir} timeout=${this.runTimeout}s attachments=${attachments.length}`);
1076
1097
  this.log(`[runStream] ${cs.agent} config: model=${resolvedModel} extraArgs=[${extraArgs.join(' ')}]`);
1077
1098
  const isFirstTurnOfSession = !cs.sessionId || isPendingSessionId(cs.sessionId);
1099
+ const mcpSystemPrompt = mcpSendFile
1100
+ ? appendExtraPrompt(buildMcpDeliveryPrompt(), buildBrowserAutomationPrompt(browserEnabled))
1101
+ : '';
1078
1102
  const effectiveSystemPrompt = isFirstTurnOfSession
1079
- ? (mcpSendFile ? appendExtraPrompt(systemPrompt, buildMcpDeliveryPrompt()) : systemPrompt)
1103
+ ? appendExtraPrompt(systemPrompt, mcpSystemPrompt)
1080
1104
  : undefined;
1081
1105
  const opts = {
1082
1106
  agent: cs.agent, prompt, workdir: this.workdir, timeout: this.runTimeout,