pikiclaw 0.2.65 → 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.
@@ -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,
@@ -0,0 +1,189 @@
1
+ import { Channel, DEFAULT_CHANNEL_CAPABILITIES, splitText, sleep, } from './channel-base.js';
2
+ import { WEIXIN_LIMITS } from './constants.js';
3
+ import { extractWeixinTextBody, markdownToWeixinPlainText, normalizeWeixinBaseUrl, WeixinMessageType, weixinGetConfig, weixinGetUpdates, weixinSendTextMessage, weixinSendTyping, } from './weixin-api.js';
4
+ const WEIXIN_MAX_MESSAGE_LENGTH = WEIXIN_LIMITS.maxMessageLength;
5
+ const WEIXIN_MAX_RETRY_DELAY_MS = WEIXIN_LIMITS.maxRetryDelay;
6
+ function describeError(error) {
7
+ return error instanceof Error ? error.message : String(error ?? 'unknown error');
8
+ }
9
+ function isAbortError(error) {
10
+ return error instanceof Error && error.name === 'AbortError';
11
+ }
12
+ export class WeixinChannel extends Channel {
13
+ capabilities = {
14
+ ...DEFAULT_CHANNEL_CAPABILITIES,
15
+ typingIndicators: true,
16
+ };
17
+ knownChats = new Set();
18
+ token;
19
+ accountId;
20
+ baseUrl;
21
+ pollTimeout;
22
+ allowedChatIds;
23
+ messageHandlers = new Set();
24
+ errorHandlers = new Set();
25
+ chatMeta = new Map();
26
+ stopping = false;
27
+ updateBuf = '';
28
+ constructor(opts) {
29
+ super();
30
+ this.token = opts.token;
31
+ this.accountId = opts.accountId;
32
+ this.baseUrl = normalizeWeixinBaseUrl(opts.baseUrl);
33
+ this.pollTimeout = opts.pollTimeout ?? WEIXIN_LIMITS.longPollTimeout;
34
+ this.allowedChatIds = opts.allowedChatIds;
35
+ }
36
+ onMessage(handler) {
37
+ this.messageHandlers.add(handler);
38
+ return this;
39
+ }
40
+ onError(handler) {
41
+ this.errorHandlers.add(handler);
42
+ return this;
43
+ }
44
+ async connect() {
45
+ const shortId = this.accountId.length > 18 ? `${this.accountId.slice(0, 8)}...${this.accountId.slice(-6)}` : this.accountId;
46
+ this.bot = {
47
+ id: this.accountId,
48
+ username: `weixin_${shortId}`,
49
+ displayName: `Weixin ${shortId}`,
50
+ };
51
+ return this.bot;
52
+ }
53
+ async listen() {
54
+ this.stopping = false;
55
+ let retryDelayMs = 1_000;
56
+ while (!this.stopping) {
57
+ try {
58
+ const response = await weixinGetUpdates({
59
+ baseUrl: this.baseUrl,
60
+ token: this.token,
61
+ getUpdatesBuf: this.updateBuf,
62
+ timeoutMs: this.pollTimeout,
63
+ });
64
+ retryDelayMs = 1_000;
65
+ if (response.get_updates_buf !== undefined)
66
+ this.updateBuf = response.get_updates_buf || '';
67
+ if ((response.ret ?? 0) !== 0 || (response.errcode ?? 0) !== 0) {
68
+ throw new Error(`Weixin getupdates failed: ${response.errmsg || response.errcode || response.ret}`);
69
+ }
70
+ for (const message of response.msgs || []) {
71
+ await this.dispatchInboundMessage(message);
72
+ }
73
+ }
74
+ catch (error) {
75
+ if (this.stopping || isAbortError(error))
76
+ break;
77
+ this.emitError(new Error(`Weixin polling failed: ${describeError(error)}`));
78
+ await sleep(retryDelayMs);
79
+ retryDelayMs = Math.min(retryDelayMs * 2, WEIXIN_MAX_RETRY_DELAY_MS);
80
+ }
81
+ }
82
+ }
83
+ disconnect() {
84
+ this.stopping = true;
85
+ }
86
+ async send(chatId, text, _opts) {
87
+ const meta = this.chatMeta.get(String(chatId));
88
+ if (!meta?.contextToken)
89
+ throw new Error('Weixin context token is missing for this chat.');
90
+ const plain = markdownToWeixinPlainText(text) || String(text || '').trim();
91
+ const chunks = splitText(plain, WEIXIN_MAX_MESSAGE_LENGTH).map(chunk => chunk.trim()).filter(Boolean);
92
+ let lastMessageId = null;
93
+ for (const chunk of chunks) {
94
+ await weixinSendTextMessage({
95
+ baseUrl: this.baseUrl,
96
+ token: this.token,
97
+ toUserId: meta.userId,
98
+ text: chunk,
99
+ contextToken: meta.contextToken,
100
+ });
101
+ lastMessageId = `wx:${Date.now().toString(36)}:${Math.random().toString(36).slice(2, 8)}`;
102
+ }
103
+ return lastMessageId;
104
+ }
105
+ async editMessage(_chatId, _msgId, _text, _opts) { }
106
+ async deleteMessage(_chatId, _msgId) { }
107
+ async sendTyping(chatId, _opts) {
108
+ const meta = this.chatMeta.get(String(chatId));
109
+ if (!meta?.contextToken || !meta.userId)
110
+ return;
111
+ let typingTicket = meta.typingTicket;
112
+ if (!typingTicket) {
113
+ const config = await weixinGetConfig({
114
+ baseUrl: this.baseUrl,
115
+ token: this.token,
116
+ userId: meta.userId,
117
+ contextToken: meta.contextToken,
118
+ });
119
+ typingTicket = String(config.typing_ticket || '').trim();
120
+ if (!typingTicket)
121
+ return;
122
+ meta.typingTicket = typingTicket;
123
+ this.chatMeta.set(String(chatId), meta);
124
+ }
125
+ await weixinSendTyping({
126
+ baseUrl: this.baseUrl,
127
+ token: this.token,
128
+ userId: meta.userId,
129
+ typingTicket,
130
+ });
131
+ }
132
+ composeChatId(userId) {
133
+ return `${this.accountId}:${userId}`;
134
+ }
135
+ isAllowed(chatId, userId) {
136
+ if (!this.allowedChatIds?.size)
137
+ return true;
138
+ return this.allowedChatIds.has(chatId) || this.allowedChatIds.has(userId);
139
+ }
140
+ async dispatchInboundMessage(message) {
141
+ if ((message.message_type ?? WeixinMessageType.USER) !== WeixinMessageType.USER)
142
+ return;
143
+ const userId = String(message.from_user_id || '').trim();
144
+ if (!userId)
145
+ return;
146
+ const chatId = this.composeChatId(userId);
147
+ if (!this.isAllowed(chatId, userId))
148
+ return;
149
+ const existing = this.chatMeta.get(chatId);
150
+ const contextToken = String(message.context_token || existing?.contextToken || '').trim();
151
+ const meta = {
152
+ userId,
153
+ contextToken,
154
+ typingTicket: existing?.contextToken === contextToken ? existing?.typingTicket : undefined,
155
+ };
156
+ this.chatMeta.set(chatId, meta);
157
+ this.knownChats.add(chatId);
158
+ const ctx = {
159
+ chatId,
160
+ messageId: String(message.message_id || message.seq || Date.now()),
161
+ from: { userId },
162
+ reply: (text, opts) => this.send(chatId, text, opts),
163
+ editReply: (msgId, text, opts) => this.editMessage(chatId, msgId, text, opts),
164
+ sendTyping: () => this.sendTyping(chatId),
165
+ channel: this,
166
+ raw: message,
167
+ };
168
+ const payload = {
169
+ text: extractWeixinTextBody(message),
170
+ files: [],
171
+ };
172
+ for (const handler of this.messageHandlers) {
173
+ try {
174
+ await handler(payload, ctx);
175
+ }
176
+ catch (error) {
177
+ this.emitError(error instanceof Error ? error : new Error(describeError(error)));
178
+ }
179
+ }
180
+ }
181
+ emitError(error) {
182
+ for (const handler of this.errorHandlers) {
183
+ try {
184
+ handler(error);
185
+ }
186
+ catch { }
187
+ }
188
+ }
189
+ }
@@ -3,7 +3,12 @@ export function hasConfiguredChannelToken(config, channel, tokenOverride) {
3
3
  case 'telegram':
4
4
  return !!(config.telegramBotToken || tokenOverride);
5
5
  case 'feishu':
6
- return !!(config.feishuAppId || tokenOverride);
6
+ return !!((config.feishuAppId && config.feishuAppSecret) || tokenOverride);
7
+ case 'weixin':
8
+ return !!(config.channels?.includes('weixin')
9
+ && config.weixinBaseUrl
10
+ && config.weixinBotToken
11
+ && config.weixinAccountId);
7
12
  case 'whatsapp':
8
13
  return !!tokenOverride;
9
14
  }
@@ -13,9 +18,12 @@ export function resolveConfiguredChannels(opts) {
13
18
  if (rawChannels) {
14
19
  return rawChannels.split(',').map(channel => channel.trim().toLowerCase()).filter(Boolean);
15
20
  }
16
- if (opts.config.channels?.length)
17
- return opts.config.channels;
21
+ if (opts.config.channels?.length) {
22
+ return opts.config.channels.filter(channel => hasConfiguredChannelToken(opts.config, channel, opts.tokenOverride));
23
+ }
18
24
  const detected = [];
25
+ if (hasConfiguredChannelToken(opts.config, 'weixin', opts.tokenOverride))
26
+ detected.push('weixin');
19
27
  if (hasConfiguredChannelToken(opts.config, 'feishu', opts.tokenOverride))
20
28
  detected.push('feishu');
21
29
  if (hasConfiguredChannelToken(opts.config, 'telegram', opts.tokenOverride))
package/dist/cli.js CHANGED
@@ -212,8 +212,8 @@ Run a bot that forwards IM messages to a local AI coding agent
212
212
  (Claude Code or Codex CLI), streams responses in real-time, and manages
213
213
  sessions, models, and workdirs.
214
214
 
215
- Channels are auto-detected from configured tokens. If both Feishu and
216
- Telegram tokens are present, both channels launch simultaneously.
215
+ Channels are auto-detected from configured credentials. If multiple
216
+ validated channels are enabled, they launch simultaneously.
217
217
 
218
218
  Usage:
219
219
  npx pikiclaw # auto-detect from config/env
@@ -248,6 +248,11 @@ Environment variables (Telegram):
248
248
  TELEGRAM_BOT_TOKEN Telegram bot token (from @BotFather)
249
249
  TELEGRAM_ALLOWED_CHAT_IDS Comma-separated allowed Telegram chat IDs
250
250
 
251
+ Environment variables (Weixin):
252
+ WEIXIN_BASE_URL Weixin API base URL (default: https://ilinkai.weixin.qq.com)
253
+ WEIXIN_BOT_TOKEN Weixin bot token (normally configured from dashboard QR login)
254
+ WEIXIN_ACCOUNT_ID Weixin bot account ID
255
+
251
256
  Environment variables (per agent):
252
257
  CLAUDE_MODEL Claude model name
253
258
  CLAUDE_PERMISSION_MODE Permission mode (default: bypassPermissions)
@@ -277,6 +282,7 @@ Environment variables (Feishu):
277
282
  FEISHU_ALLOWED_CHAT_IDS Comma-separated allowed Feishu chat IDs
278
283
 
279
284
  Notes:
285
+ - weixin setup is QR-based in the dashboard and currently supports text-only replies.
280
286
  - whatsapp is planned but not implemented yet.
281
287
  - --safe-mode delegates to the agent's own permission model; it does not add
282
288
  a pikiclaw-specific approval workflow.
@@ -530,6 +536,14 @@ async function launchChannels(channels, dashboard) {
530
536
  await bot.run();
531
537
  break;
532
538
  }
539
+ case 'weixin': {
540
+ const { WeixinBot } = await import('./bot-weixin.js');
541
+ const bot = new WeixinBot();
542
+ if (dashboard)
543
+ dashboard.attachBot(bot);
544
+ await bot.run();
545
+ break;
546
+ }
533
547
  case 'whatsapp':
534
548
  process.stderr.write('WhatsApp channel is not yet implemented. Coming soon.\n');
535
549
  break;
@@ -1,7 +1,9 @@
1
1
  import * as lark from '@larksuiteoapi/node-sdk';
2
2
  import { validateTelegramToken } from './setup-wizard.js';
3
3
  import { VALIDATION_TIMEOUTS } from './constants.js';
4
+ import { normalizeWeixinBaseUrl, weixinGetUpdates } from './weixin-api.js';
4
5
  const DEFAULT_FEISHU_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.feishuDefault;
6
+ const DEFAULT_WEIXIN_VALIDATION_TIMEOUT_MS = VALIDATION_TIMEOUTS.weixinDefault;
5
7
  function feishuValidationLog(appId, message) {
6
8
  const ts = new Date().toISOString().slice(11, 19);
7
9
  process.stdout.write(`[feishu-validate ${ts}] app=${appId} ${message}\n`);
@@ -250,12 +252,68 @@ export async function validateFeishuConfig(appId, appSecret, options = {}) {
250
252
  };
251
253
  }
252
254
  }
255
+ export async function validateWeixinConfig(baseUrl, botToken, accountId, options = {}) {
256
+ const normalizedBaseUrl = normalizeWeixinBaseUrl(baseUrl);
257
+ const trimmedToken = String(botToken || '').trim();
258
+ const trimmedAccountId = String(accountId || '').trim();
259
+ const timeoutMs = Number.isFinite(options.timeoutMs) && Number(options.timeoutMs) > 0
260
+ ? Math.round(Number(options.timeoutMs))
261
+ : DEFAULT_WEIXIN_VALIDATION_TIMEOUT_MS;
262
+ if (!trimmedToken && !trimmedAccountId && !String(baseUrl || '').trim()) {
263
+ return {
264
+ state: missingChannelState('weixin', 'Weixin is not configured.'),
265
+ account: null,
266
+ normalizedBaseUrl,
267
+ };
268
+ }
269
+ if (!trimmedToken || !trimmedAccountId) {
270
+ return {
271
+ state: invalidChannelState('weixin', 'Weixin requires Base URL, Bot Token, and Account ID.'),
272
+ account: null,
273
+ normalizedBaseUrl,
274
+ };
275
+ }
276
+ try {
277
+ const response = await weixinGetUpdates({
278
+ baseUrl: normalizedBaseUrl,
279
+ token: trimmedToken,
280
+ getUpdatesBuf: '',
281
+ timeoutMs,
282
+ });
283
+ if ((response.ret ?? 0) !== 0 || (response.errcode ?? 0) !== 0) {
284
+ const detail = String(response.errmsg || response.errcode || 'credentials rejected').trim();
285
+ return {
286
+ state: invalidChannelState('weixin', `Weixin rejected these credentials: ${detail}`),
287
+ account: null,
288
+ normalizedBaseUrl,
289
+ };
290
+ }
291
+ return {
292
+ state: readyChannelState('weixin', `Weixin account ${trimmedAccountId} verified.`),
293
+ account: {
294
+ accountId: trimmedAccountId,
295
+ baseUrl: normalizedBaseUrl,
296
+ },
297
+ normalizedBaseUrl,
298
+ };
299
+ }
300
+ catch (error) {
301
+ const message = error instanceof Error ? error.message : String(error ?? 'unknown error');
302
+ return {
303
+ state: errorChannelState('weixin', `Failed to reach Weixin: ${message}`),
304
+ account: null,
305
+ normalizedBaseUrl,
306
+ };
307
+ }
308
+ }
253
309
  export async function collectChannelSetupStates(config) {
254
- const [telegram, feishu] = await Promise.all([
310
+ const [telegram, feishu, weixin] = await Promise.all([
255
311
  validateTelegramConfig(config.telegramBotToken, config.telegramAllowedChatIds),
256
312
  validateFeishuConfig(config.feishuAppId, config.feishuAppSecret),
313
+ validateWeixinConfig(config.weixinBaseUrl, config.weixinBotToken, config.weixinAccountId),
257
314
  ]);
258
315
  return [
316
+ weixin.state,
259
317
  telegram.state,
260
318
  feishu.state,
261
319
  ];
package/dist/constants.js CHANGED
@@ -170,6 +170,18 @@ export const FEISHU_LIMITS = {
170
170
  /** Feishu bot rendering limit for card payloads. */
171
171
  export const FEISHU_BOT_CARD_MAX = 25_000;
172
172
  // ---------------------------------------------------------------------------
173
+ // Channels — Weixin
174
+ // ---------------------------------------------------------------------------
175
+ /** Weixin channel transport constants. */
176
+ export const WEIXIN_LIMITS = {
177
+ /** Conservative text split budget for plain-text replies. */
178
+ maxMessageLength: 1200,
179
+ /** Long-poll timeout for getupdates. */
180
+ longPollTimeout: 35_000,
181
+ /** Maximum back-off delay for polling retries. */
182
+ maxRetryDelay: 60_000,
183
+ };
184
+ // ---------------------------------------------------------------------------
173
185
  // Config validation
174
186
  // ---------------------------------------------------------------------------
175
187
  /** Timeouts for channel credential validation flows. */
@@ -180,6 +192,10 @@ export const VALIDATION_TIMEOUTS = {
180
192
  feishuBotInfo: 5_000,
181
193
  /** Timeout for Telegram token validation (setup wizard). */
182
194
  telegramToken: 8_000,
195
+ /** Default timeout for Weixin credential validation. */
196
+ weixinDefault: 8_000,
197
+ /** Long-poll timeout for dashboard QR login wait calls. */
198
+ weixinQrPoll: 35_000,
183
199
  };
184
200
  // ---------------------------------------------------------------------------
185
201
  // Agent auto-update