@wu529778790/open-im 1.10.9-beta.10 → 1.10.9-beta.12

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.
@@ -382,30 +382,29 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode, onS
382
382
  if (!sessionId) {
383
383
  const latest = findLatestClaudeSession(workDir);
384
384
  if (latest) {
385
- // 安全检查:如果 CLI 正在使用该 session(文件 30 秒内有写入),不能接管
386
- if (isCliSessionActive(latest.sessionId, latest.filePath)) {
387
- log.info(`CLI is actively using session ${latest.sessionId}, skipping auto-resume`);
385
+ // 检测 CLI 是否正在使用该 session(用于日志,不阻止 resume)
386
+ const cliActive = isCliSessionActive(latest.sessionId, latest.filePath);
387
+ if (cliActive) {
388
+ log.info(`CLI is actively using session ${latest.sessionId}, attempting resume anyway (SDK handles concurrency)`);
388
389
  }
389
- else {
390
- // 验证文件内容一致性
391
- if (validateSessionFile(latest.filePath, latest.sessionId)) {
392
- try {
393
- log.info(`Auto-resuming latest CLI session: ${latest.sessionId}`);
394
- session = unstable_v2_resumeSession(latest.sessionId, sessionOptions);
395
- activeSessions.set(latest.sessionId, session);
396
- sessionWorkDirs.set(latest.sessionId, workDir);
397
- sessionLastUsed.set(latest.sessionId, Date.now());
398
- log.info(`Successfully auto-resumed CLI session: ${latest.sessionId}`);
399
- return { session, sessionId: latest.sessionId };
400
- }
401
- catch (err) {
402
- log.warn(`Failed to auto-resume CLI session ${latest.sessionId}, skipping auto-resume: ${err}`);
403
- }
390
+ // 验证文件内容一致性
391
+ if (validateSessionFile(latest.filePath, latest.sessionId)) {
392
+ try {
393
+ log.info(`Auto-resuming latest CLI session: ${latest.sessionId}${cliActive ? ' (CLI active)' : ''}`);
394
+ session = unstable_v2_resumeSession(latest.sessionId, sessionOptions);
395
+ activeSessions.set(latest.sessionId, session);
396
+ sessionWorkDirs.set(latest.sessionId, workDir);
397
+ sessionLastUsed.set(latest.sessionId, Date.now());
398
+ log.info(`Successfully auto-resumed CLI session: ${latest.sessionId}`);
399
+ return { session, sessionId: latest.sessionId };
404
400
  }
405
- else {
406
- log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
401
+ catch (err) {
402
+ log.warn(`Failed to auto-resume CLI session ${latest.sessionId}, creating new one: ${err}`);
407
403
  }
408
404
  }
405
+ else {
406
+ log.warn(`Session file validation failed for ${latest.sessionId}, skipping`);
407
+ }
409
408
  }
410
409
  }
411
410
  // 创建新会话
@@ -5,6 +5,7 @@ const PLATFORM_LABELS = {
5
5
  wework: "企业微信",
6
6
  dingtalk: "钉钉",
7
7
  workbuddy: "WorkBuddy",
8
+ clawbot: "ClawBot",
8
9
  };
9
10
  export const CHANNEL_CAPABILITIES = {
10
11
  telegram: {
@@ -31,6 +32,10 @@ export const CHANNEL_CAPABILITIES = {
31
32
  inbound: { text: "native", image: "none", file: "none", voice: "none", video: "none" },
32
33
  outbound: { streamEdit: "none", streamPush: "none", image: "none", card: "none", typing: "none" },
33
34
  },
35
+ clawbot: {
36
+ inbound: { text: "native", image: "none", file: "none", voice: "none", video: "none" },
37
+ outbound: { streamEdit: "none", streamPush: "none", image: "none", card: "none", typing: "none" },
38
+ },
34
39
  };
35
40
  function listPreferredPlatforms(kind) {
36
41
  return Object.entries(CHANNEL_CAPABILITIES)
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ClawBot Client - WeChat iLink API long-polling client
3
+ *
4
+ * Receives messages via long-polling ilink/bot/getupdates
5
+ * and dispatches them to the event handler.
6
+ */
7
+ import type { Config } from '../config.js';
8
+ import type { ClawBotState } from './types.js';
9
+ export declare function getChannelState(): ClawBotState;
10
+ export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
11
+ export declare function stopClawbot(): void;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ClawBot Client - WeChat iLink API long-polling client
3
+ *
4
+ * Receives messages via long-polling ilink/bot/getupdates
5
+ * and dispatches them to the event handler.
6
+ */
7
+ import { createLogger } from '../logger.js';
8
+ import { jitteredDelay } from '../shared/reconnect.js';
9
+ import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
10
+ const log = createLogger('ClawBot');
11
+ const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
12
+ let pollController = null;
13
+ let channelState = 'disconnected';
14
+ let messageHandler = null;
15
+ let stateChangeHandler = null;
16
+ let reconnectTimer = null;
17
+ let reconnectAttempt = 0;
18
+ let stopped = false;
19
+ let apiUrl = 'http://127.0.0.1:26322';
20
+ let apiToken = '';
21
+ let lastUpdateId = 0;
22
+ export function getChannelState() {
23
+ return channelState;
24
+ }
25
+ export async function initClawbot(config, eventHandler, onStateChange) {
26
+ const pc = config.platforms?.clawbot;
27
+ if (!pc?.enabled) {
28
+ throw new Error('ClawBot platform not enabled');
29
+ }
30
+ if (!pc.apiToken) {
31
+ throw new Error('ClawBot apiToken required');
32
+ }
33
+ apiUrl = pc.apiUrl ?? 'http://127.0.0.1:26322';
34
+ apiToken = pc.apiToken;
35
+ messageHandler = eventHandler;
36
+ stateChangeHandler = onStateChange ?? null;
37
+ stopped = false;
38
+ reconnectAttempt = 0;
39
+ lastUpdateId = 0;
40
+ // Verify connectivity
41
+ try {
42
+ const res = await fetchApi('/ilink/bot/getupdates?timeout=1');
43
+ if (!res.ok) {
44
+ throw new Error(`API check failed: ${res.error ?? 'unknown'}`);
45
+ }
46
+ log.info(`ClawBot API reachable at ${apiUrl}`);
47
+ }
48
+ catch (err) {
49
+ log.error('ClawBot API connectivity check failed:', err);
50
+ throw err;
51
+ }
52
+ updateState('connected');
53
+ startPolling();
54
+ log.info('ClawBot client initialized');
55
+ }
56
+ function startPolling() {
57
+ if (stopped || pollController)
58
+ return;
59
+ pollController = new AbortController();
60
+ const signal = pollController.signal;
61
+ (async () => {
62
+ log.info('ClawBot long-polling started');
63
+ while (!stopped && !signal.aborted) {
64
+ try {
65
+ const url = `/ilink/bot/getupdates?timeout=30${lastUpdateId > 0 ? `&offset=${lastUpdateId + 1}` : ''}`;
66
+ const res = await fetchApi(url, signal);
67
+ if (signal.aborted)
68
+ break;
69
+ if (!res.ok) {
70
+ log.warn(`ClawBot getupdates error: ${res.error ?? 'unknown'}`);
71
+ await sleep(CLAWBOT_POLL_INTERVAL_MS, signal);
72
+ continue;
73
+ }
74
+ const updates = res.result ?? [];
75
+ for (const update of updates) {
76
+ if (signal.aborted)
77
+ break;
78
+ lastUpdateId = Math.max(lastUpdateId, update.update_id);
79
+ const msg = update.message;
80
+ if (!msg?.text)
81
+ continue;
82
+ const chatId = msg.chat?.id ?? msg.from?.id ?? '';
83
+ const msgId = String(msg.message_id ?? update.update_id);
84
+ const content = msg.text;
85
+ if (!chatId) {
86
+ log.warn('ClawBot message missing chatId, skipping');
87
+ continue;
88
+ }
89
+ log.info(`ClawBot message: chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
90
+ if (messageHandler) {
91
+ try {
92
+ await messageHandler(chatId, msgId, content);
93
+ }
94
+ catch (err) {
95
+ log.error('Error in ClawBot message handler:', err);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ catch (err) {
101
+ if (signal.aborted)
102
+ break;
103
+ if (err instanceof Error && err.name === 'AbortError')
104
+ break;
105
+ log.error('ClawBot polling error:', err);
106
+ updateState('error');
107
+ scheduleReconnect();
108
+ return;
109
+ }
110
+ }
111
+ })();
112
+ }
113
+ function scheduleReconnect() {
114
+ if (stopped)
115
+ return;
116
+ if (reconnectTimer) {
117
+ clearTimeout(reconnectTimer);
118
+ reconnectTimer = null;
119
+ }
120
+ const baseDelay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
121
+ const delay = jitteredDelay(baseDelay);
122
+ reconnectAttempt++;
123
+ log.info(`ClawBot reconnecting in ${delay}ms (attempt ${reconnectAttempt})...`);
124
+ reconnectTimer = setTimeout(() => {
125
+ reconnectTimer = null;
126
+ if (stopped)
127
+ return;
128
+ updateState('connected');
129
+ startPolling();
130
+ }, delay);
131
+ }
132
+ function updateState(state) {
133
+ channelState = state;
134
+ stateChangeHandler?.(state);
135
+ log.debug(`ClawBot state: ${state}`);
136
+ }
137
+ async function fetchApi(path, signal) {
138
+ const url = `${apiUrl}${path}`;
139
+ const res = await fetch(url, {
140
+ headers: { Authorization: `Bearer ${apiToken}` },
141
+ signal,
142
+ });
143
+ return res.json();
144
+ }
145
+ function sleep(ms, signal) {
146
+ return new Promise((resolve) => {
147
+ if (signal?.aborted) {
148
+ resolve();
149
+ return;
150
+ }
151
+ const timer = setTimeout(resolve, ms);
152
+ signal?.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
153
+ });
154
+ }
155
+ export function stopClawbot() {
156
+ log.info('Stopping ClawBot client...');
157
+ stopped = true;
158
+ if (pollController) {
159
+ pollController.abort();
160
+ pollController = null;
161
+ }
162
+ if (reconnectTimer) {
163
+ clearTimeout(reconnectTimer);
164
+ reconnectTimer = null;
165
+ }
166
+ messageHandler = null;
167
+ updateState('disconnected');
168
+ log.info('ClawBot client stopped');
169
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ClawBot Event Handler - Handle messages from iLink API long-polling
3
+ */
4
+ import type { Config } from '../config.js';
5
+ import type { SessionManager } from '../session/session-manager.js';
6
+ export interface ClawBotEventHandlerHandle {
7
+ stop: () => void;
8
+ runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
9
+ getRunningTaskCount: () => number;
10
+ handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
11
+ }
12
+ export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager): ClawBotEventHandlerHandle;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * ClawBot Event Handler - Handle messages from iLink API long-polling
3
+ */
4
+ import { sendTextReply, sendErrorReply } from './message-sender.js';
5
+ import { startTaskCleanup } from '../shared/task-cleanup.js';
6
+ import { CLAWBOT_THROTTLE_MS } from '../constants.js';
7
+ import { createLogger } from '../logger.js';
8
+ import { createPlatformEventContext } from '../platform/create-event-context.js';
9
+ import { createPlatformAIRequestHandler } from '../platform/handle-ai-request.js';
10
+ import { handleTextFlow } from '../platform/handle-text-flow.js';
11
+ const log = createLogger('ClawBotHandler');
12
+ export function setupClawbotHandlers(config, sessionManager) {
13
+ const ctx = createPlatformEventContext({
14
+ platform: 'clawbot',
15
+ allowedUserIds: config.clawbotAllowedUserIds,
16
+ config,
17
+ sessionManager,
18
+ sender: {
19
+ sendTextReply: async (chatId, text) => {
20
+ await sendTextReply(chatId, text);
21
+ },
22
+ },
23
+ });
24
+ const stopTaskCleanup = startTaskCleanup(ctx.runningTasks);
25
+ const platformSender = {
26
+ sendThinkingMessage: async (_chatId, _replyToMessageId, _toolId) => {
27
+ return 'clawbot_no_thinking';
28
+ },
29
+ sendTextReply: async (chatId, text) => {
30
+ await sendTextReply(chatId, text);
31
+ },
32
+ startTyping: (_chatId) => {
33
+ return () => { };
34
+ },
35
+ };
36
+ async function handleEvent(chatId, msgId, content) {
37
+ log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
38
+ const userId = chatId;
39
+ const text = content.trim();
40
+ const msgIdSender = {
41
+ ...platformSender,
42
+ sendTextReply: async (c, t) => {
43
+ await sendTextReply(c, t);
44
+ },
45
+ };
46
+ const handleAIRequest = createPlatformAIRequestHandler({
47
+ platform: 'clawbot',
48
+ config,
49
+ sessionManager,
50
+ sender: msgIdSender,
51
+ throttleMs: CLAWBOT_THROTTLE_MS,
52
+ runningTasks: ctx.runningTasks,
53
+ taskKeyBuilder: (userId, _msgId) => `${userId}:${msgId}`,
54
+ taskCallbacksFactory: ({ chatId: c }) => ({
55
+ streamUpdate: async () => { },
56
+ sendComplete: async (content) => {
57
+ await sendTextReply(c, content);
58
+ },
59
+ sendError: async (error) => {
60
+ await sendErrorReply(c, error);
61
+ },
62
+ }),
63
+ });
64
+ await handleTextFlow({
65
+ platform: 'clawbot',
66
+ userId,
67
+ chatId,
68
+ text,
69
+ ctx,
70
+ handleAIRequest,
71
+ sendTextReply: (c, t) => sendTextReply(c, t),
72
+ workDir: sessionManager.getWorkDir(userId),
73
+ convId: sessionManager.getConvId(userId),
74
+ accessDeniedMessage: (uid) => `抱歉,您没有访问权限。\n您的 ID: ${uid}`,
75
+ queueFullMessage: '请求队列已满,请稍后再试。',
76
+ queuedMessage: '您的请求已排队等待。',
77
+ });
78
+ }
79
+ return {
80
+ stop: () => stopTaskCleanup(),
81
+ runningTasks: ctx.runningTasks,
82
+ getRunningTaskCount: () => ctx.runningTasks.size,
83
+ handleEvent,
84
+ };
85
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * ClawBot Message Sender - Send messages via iLink API
3
+ */
4
+ export declare function initClawBotSender(url: string, token: string): void;
5
+ /**
6
+ * Send text reply to a ClawBot chat, splitting long messages automatically.
7
+ */
8
+ export declare function sendTextReply(chatId: string, text: string): Promise<void>;
9
+ /**
10
+ * Send error reply to a ClawBot chat.
11
+ */
12
+ export declare function sendErrorReply(chatId: string, error: string): Promise<void>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ClawBot Message Sender - Send messages via iLink API
3
+ */
4
+ import { createLogger } from '../logger.js';
5
+ import { splitLongContent, toReplyPlainText } from '../shared/utils.js';
6
+ import { MAX_CLAWBOT_MESSAGE_LENGTH } from '../constants.js';
7
+ import { getChannelState } from './client.js';
8
+ const log = createLogger('ClawBotSender');
9
+ let apiUrl = 'http://127.0.0.1:26322';
10
+ let apiToken = '';
11
+ export function initClawBotSender(url, token) {
12
+ apiUrl = url;
13
+ apiToken = token;
14
+ }
15
+ async function postMessage(chatId, text) {
16
+ if (getChannelState() !== 'connected') {
17
+ log.warn('ClawBot not connected, cannot send message');
18
+ return false;
19
+ }
20
+ try {
21
+ const res = await fetch(`${apiUrl}/ilink/bot/sendmessage`, {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ Authorization: `Bearer ${apiToken}`,
26
+ },
27
+ body: JSON.stringify({ chat_id: chatId, text }),
28
+ });
29
+ const data = await res.json();
30
+ if (!data.ok) {
31
+ log.error(`ClawBot sendmessage failed: ${data.error ?? 'unknown'}`);
32
+ return false;
33
+ }
34
+ return true;
35
+ }
36
+ catch (err) {
37
+ log.error('ClawBot sendmessage error:', err);
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * Send text reply to a ClawBot chat, splitting long messages automatically.
43
+ */
44
+ export async function sendTextReply(chatId, text) {
45
+ const plainText = toReplyPlainText(text);
46
+ const parts = splitLongContent(plainText, MAX_CLAWBOT_MESSAGE_LENGTH);
47
+ if (parts.length === 1) {
48
+ log.info(`Sending ClawBot reply to chatId=${chatId}, len=${plainText.length}`);
49
+ await postMessage(chatId, plainText);
50
+ return;
51
+ }
52
+ log.info(`Sending ClawBot reply in ${parts.length} parts to chatId=${chatId}, totalLen=${plainText.length}`);
53
+ for (let i = 0; i < parts.length; i++) {
54
+ const partText = i === 0
55
+ ? `${parts[i]}\n\n_(1/${parts.length})_`
56
+ : `_(续 ${i + 1}/${parts.length})_\n\n${parts[i]}`;
57
+ await postMessage(chatId, partText);
58
+ log.info(`ClawBot part ${i + 1}/${parts.length} sent`);
59
+ }
60
+ }
61
+ /**
62
+ * Send error reply to a ClawBot chat.
63
+ */
64
+ export async function sendErrorReply(chatId, error) {
65
+ log.warn(`Sending ClawBot error to chatId=${chatId}`);
66
+ await postMessage(chatId, `错误: ${error}`);
67
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ClawBot QR Code Login - WeChat iLink API
3
+ *
4
+ * Calls https://ilinkai.weixin.qq.com to get QR code and poll for login confirmation.
5
+ * Returns bot_token on success.
6
+ */
7
+ export interface QRLoginSession {
8
+ sessionKey: string;
9
+ qrcode: string;
10
+ qrcodeUrl: string;
11
+ startedAt: number;
12
+ }
13
+ export interface QRLoginResult {
14
+ connected: boolean;
15
+ botToken?: string;
16
+ accountId?: string;
17
+ baseUrl?: string;
18
+ userId?: string;
19
+ message: string;
20
+ }
21
+ export declare function fetchQRCode(): Promise<{
22
+ qrcode: string;
23
+ qrcodeUrl: string;
24
+ }>;
25
+ /**
26
+ * Start QR code login session. Returns QR code URL for display.
27
+ */
28
+ export declare function startQRLogin(): Promise<QRLoginSession>;
29
+ /**
30
+ * Wait for user to scan QR code. Polls until confirmed or timeout.
31
+ * Returns bot_token on success.
32
+ */
33
+ export declare function waitForQRLogin(session: QRLoginSession, onStatusChange?: (status: string) => void): Promise<QRLoginResult>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * ClawBot QR Code Login - WeChat iLink API
3
+ *
4
+ * Calls https://ilinkai.weixin.qq.com to get QR code and poll for login confirmation.
5
+ * Returns bot_token on success.
6
+ */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { createLogger } from '../logger.js';
9
+ const log = createLogger('ClawBotQR');
10
+ const ILINK_BASE_URL = 'https://ilinkai.weixin.qq.com';
11
+ const ILINK_APP_ID = 'bot';
12
+ const BOT_TYPE = '3';
13
+ const POLL_TIMEOUT_MS = 35_000;
14
+ const LOGIN_TIMEOUT_MS = 5 * 60_000;
15
+ function buildHeaders() {
16
+ return {
17
+ 'Content-Type': 'application/json',
18
+ 'AuthorizationType': 'ilink_bot_token',
19
+ 'X-WECHAT-UIN': randomUUID(),
20
+ 'iLink-App-Id': ILINK_APP_ID,
21
+ 'iLink-App-ClientVersion': '131588', // 2.4.4 encoded
22
+ };
23
+ }
24
+ export async function fetchQRCode() {
25
+ const url = `${ILINK_BASE_URL}/ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
26
+ const res = await fetch(url, {
27
+ method: 'POST',
28
+ headers: buildHeaders(),
29
+ body: JSON.stringify({ local_token_list: [] }),
30
+ });
31
+ const data = await res.json();
32
+ if (!data.qrcode) {
33
+ throw new Error('Failed to get QR code from iLink API');
34
+ }
35
+ return { qrcode: data.qrcode, qrcodeUrl: data.qrcode_img_content };
36
+ }
37
+ async function pollQRStatus(qrcode) {
38
+ const url = `${ILINK_BASE_URL}/ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), POLL_TIMEOUT_MS);
41
+ try {
42
+ const headers = buildHeaders();
43
+ delete headers['Content-Type'];
44
+ const res = await fetch(url, {
45
+ method: 'GET',
46
+ headers,
47
+ signal: controller.signal,
48
+ });
49
+ clearTimeout(timer);
50
+ return await res.json();
51
+ }
52
+ catch (err) {
53
+ clearTimeout(timer);
54
+ if (err instanceof Error && err.name === 'AbortError') {
55
+ return { status: 'wait' };
56
+ }
57
+ throw err;
58
+ }
59
+ }
60
+ /**
61
+ * Start QR code login session. Returns QR code URL for display.
62
+ */
63
+ export async function startQRLogin() {
64
+ log.info('Starting ClawBot QR login...');
65
+ const { qrcode, qrcodeUrl } = await fetchQRCode();
66
+ log.info(`QR code received, url=${qrcodeUrl}`);
67
+ return {
68
+ sessionKey: randomUUID(),
69
+ qrcode,
70
+ qrcodeUrl,
71
+ startedAt: Date.now(),
72
+ };
73
+ }
74
+ /**
75
+ * Wait for user to scan QR code. Polls until confirmed or timeout.
76
+ * Returns bot_token on success.
77
+ */
78
+ export async function waitForQRLogin(session, onStatusChange) {
79
+ const deadline = Date.now() + LOGIN_TIMEOUT_MS;
80
+ let currentBaseUrl = ILINK_BASE_URL;
81
+ let redirectCount = 0;
82
+ log.info('Waiting for QR code scan...');
83
+ while (Date.now() < deadline) {
84
+ const result = await pollQRStatus(session.qrcode);
85
+ onStatusChange?.(result.status);
86
+ switch (result.status) {
87
+ case 'wait':
88
+ case 'scaned':
89
+ break;
90
+ case 'scaned_but_redirect':
91
+ if (result.redirect_host) {
92
+ currentBaseUrl = `https://${result.redirect_host}`;
93
+ redirectCount++;
94
+ log.info(`IDC redirect to ${result.redirect_host} (${redirectCount})`);
95
+ }
96
+ break;
97
+ case 'confirmed':
98
+ if (!result.bot_token) {
99
+ return { connected: false, message: '登录失败:服务器未返回 bot_token' };
100
+ }
101
+ log.info(`Login confirmed! botId=${result.ilink_bot_id}`);
102
+ return {
103
+ connected: true,
104
+ botToken: result.bot_token,
105
+ accountId: result.ilink_bot_id,
106
+ baseUrl: result.baseurl,
107
+ userId: result.ilink_user_id,
108
+ message: '登录成功',
109
+ };
110
+ case 'expired':
111
+ return { connected: false, message: '二维码已过期,请重新生成' };
112
+ case 'binded_redirect':
113
+ return { connected: false, message: '已连接过,无需重复连接' };
114
+ default:
115
+ log.warn(`Unknown QR status: ${result.status}`);
116
+ }
117
+ await new Promise(r => setTimeout(r, 1000));
118
+ }
119
+ return { connected: false, message: '登录超时,请重试' };
120
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ClawBot Types - WeChat iLink API
3
+ */
4
+ /** Connection state */
5
+ export type ClawBotState = 'disconnected' | 'connecting' | 'connected' | 'error';
6
+ /** ClawBot configuration */
7
+ export interface ClawBotConfig {
8
+ /** iLink API base URL (default: http://127.0.0.1:26322) */
9
+ apiUrl: string;
10
+ /** Bearer token for authentication */
11
+ apiToken: string;
12
+ }
13
+ /** Message from getupdates */
14
+ export interface ClawBotUpdate {
15
+ update_id: number;
16
+ message?: {
17
+ message_id: number;
18
+ from?: {
19
+ id: string;
20
+ name?: string;
21
+ };
22
+ chat?: {
23
+ id: string;
24
+ type?: string;
25
+ };
26
+ text?: string;
27
+ date?: number;
28
+ };
29
+ }
30
+ /** getupdates response */
31
+ export interface ClawBotUpdatesResponse {
32
+ ok: boolean;
33
+ result?: ClawBotUpdate[];
34
+ error?: string;
35
+ }
36
+ /** sendmessage response */
37
+ export interface ClawBotSendResponse {
38
+ ok: boolean;
39
+ result?: {
40
+ message_id: number;
41
+ };
42
+ error?: string;
43
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * ClawBot Types - WeChat iLink API
3
+ */
4
+ export {};
@@ -1,5 +1,5 @@
1
1
  import type { LogLevel } from '../logger.js';
2
- export type Platform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
2
+ export type Platform = 'clawbot' | 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wework' | 'workbuddy';
3
3
  export type AiCommand = 'claude' | 'codex' | 'codebuddy';
4
4
  export interface Config {
5
5
  enabledPlatforms: Platform[];
@@ -21,6 +21,7 @@ export interface Config {
21
21
  weworkAllowedUserIds: string[];
22
22
  dingtalkAllowedUserIds: string[];
23
23
  workbuddyAllowedUserIds: string[];
24
+ clawbotAllowedUserIds: string[];
24
25
  codexCliPath: string;
25
26
  codebuddyCliPath: string;
26
27
  /** Claude 访问 API 的代理(如 http://127.0.0.1:7890) */
@@ -87,6 +88,13 @@ export interface Config {
87
88
  guid?: string;
88
89
  workspacePath?: string;
89
90
  };
91
+ clawbot?: {
92
+ enabled: boolean;
93
+ aiCommand?: AiCommand;
94
+ allowedUserIds: string[];
95
+ apiUrl?: string;
96
+ apiToken?: string;
97
+ };
90
98
  };
91
99
  }
92
100
  export interface FilePlatformTelegram {
@@ -147,6 +155,13 @@ export interface FilePlatformWorkBuddy {
147
155
  guid?: string;
148
156
  workspacePath?: string;
149
157
  }
158
+ export interface FilePlatformClawbot {
159
+ enabled?: boolean;
160
+ aiCommand?: AiCommand;
161
+ allowedUserIds?: string[];
162
+ apiUrl?: string;
163
+ apiToken?: string;
164
+ }
150
165
  export interface FileToolClaude {
151
166
  cliPath?: string;
152
167
  workDir?: string;
@@ -180,6 +195,7 @@ export interface FileConfig {
180
195
  wework?: FilePlatformWework;
181
196
  dingtalk?: FilePlatformDingtalk;
182
197
  workbuddy?: FilePlatformWorkBuddy;
198
+ clawbot?: FilePlatformClawbot;
183
199
  };
184
200
  /** @deprecated 仅旧配置兼容;运行时以各 platforms.*.aiCommand 为准 */
185
201
  aiCommand?: string;
@@ -163,6 +163,7 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
163
163
  const fileWework = file.platforms?.wework;
164
164
  const fileDingtalk = file.platforms?.dingtalk;
165
165
  const fileWorkbuddy = file.platforms?.workbuddy;
166
+ const fileClawbot = file.platforms?.clawbot;
166
167
  const telegramBotToken = env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
167
168
  const feishuAppId = env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
168
169
  const feishuAppSecret = env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
@@ -212,6 +213,12 @@ export function getHealthPlatformSnapshot(file, env = process.env) {
212
213
  healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
213
214
  message: workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId ? "OAuth credentials configured" : "Missing credentials",
214
215
  },
216
+ clawbot: {
217
+ configured: !!fileClawbot?.apiToken,
218
+ enabled: !!fileClawbot?.apiToken && fileClawbot?.enabled !== false,
219
+ healthy: !!fileClawbot?.apiToken,
220
+ message: fileClawbot?.apiToken ? "API Token configured" : "Missing API Token",
221
+ },
215
222
  };
216
223
  }
217
224
  function splitCsv(value) {
@@ -325,6 +332,13 @@ function buildInitialPayload(file) {
325
332
  baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
326
333
  allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
327
334
  },
335
+ clawbot: {
336
+ enabled: file.platforms?.clawbot?.enabled ?? Boolean(file.platforms?.clawbot?.apiToken),
337
+ aiCommand: normalizeAiCommand(file.platforms?.clawbot?.aiCommand, "claude"),
338
+ apiUrl: file.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322",
339
+ apiToken: maskSecret(file.platforms?.clawbot?.apiToken),
340
+ allowedUserIds: (file.platforms?.clawbot?.allowedUserIds ?? []).join(", "),
341
+ },
328
342
  },
329
343
  ai: {
330
344
  claudeWorkDir: file.tools?.claude?.workDir ?? process.cwd(),
@@ -388,6 +402,8 @@ function validatePayload(payload) {
388
402
  errors.push("WorkBuddy refresh token is required.");
389
403
  if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.userId))
390
404
  errors.push("WorkBuddy user ID is required.");
405
+ if (payload.platforms.clawbot.enabled && !clean(payload.platforms.clawbot.apiToken))
406
+ errors.push("ClawBot API token is required.");
391
407
  if (!clean(payload.ai.claudeWorkDir))
392
408
  errors.push("Default work directory is required.");
393
409
  return errors;
@@ -447,6 +463,11 @@ function validateConfigForPlatform(platform, config) {
447
463
  errors.push("WorkBuddy user ID is required and must be a non-empty string.");
448
464
  }
449
465
  break;
466
+ case "clawbot":
467
+ if (!c.apiToken || typeof c.apiToken !== "string" || !clean(c.apiToken)) {
468
+ errors.push("ClawBot API token is required and must be a non-empty string.");
469
+ }
470
+ break;
450
471
  default:
451
472
  errors.push(`Unknown platform: ${platform}`);
452
473
  }
@@ -476,6 +497,7 @@ function createProbeConfig(values) {
476
497
  weworkAllowedUserIds: [],
477
498
  dingtalkAllowedUserIds: [],
478
499
  workbuddyAllowedUserIds: [],
500
+ clawbotAllowedUserIds: [],
479
501
  codexCliPath: "codex",
480
502
  claudeWorkDir: process.cwd(),
481
503
  claudeSessionIdleTtlMinutes: 30,
@@ -598,6 +620,21 @@ async function probeWorkBuddy(config) {
598
620
  }
599
621
  return "WorkBuddy credentials are valid.";
600
622
  }
623
+ async function probeClawBot(config) {
624
+ const apiUrl = clean(String(config.apiUrl ?? "http://127.0.0.1:26322"));
625
+ const apiToken = clean(String(config.apiToken ?? ""));
626
+ if (!apiToken)
627
+ throw new Error("ClawBot API token is required.");
628
+ const response = await fetch(`${apiUrl}/ilink/bot/getupdates?timeout=1`, {
629
+ headers: { Authorization: `Bearer ${apiToken}` },
630
+ signal: AbortSignal.timeout(TEST_TIMEOUT_MS),
631
+ });
632
+ const body = await readJsonResponse(response);
633
+ if (!response.ok || body.ok !== true) {
634
+ throw new Error(String(body.error ?? body.description ?? `HTTP ${response.status}`));
635
+ }
636
+ return "ClawBot API reachable.";
637
+ }
601
638
  export async function testPlatformConfig(platform, config) {
602
639
  const errors = validateConfigForPlatform(platform, config);
603
640
  if (errors.length > 0) {
@@ -616,6 +653,8 @@ export async function testPlatformConfig(platform, config) {
616
653
  return probeDingTalk(config);
617
654
  case "workbuddy":
618
655
  return probeWorkBuddy(config);
656
+ case "clawbot":
657
+ return probeClawBot(config);
619
658
  default:
620
659
  throw new Error(`Unknown platform: ${platform}`);
621
660
  }
@@ -711,6 +750,14 @@ function toFileConfig(payload, existing) {
711
750
  baseUrl: clean(payload.platforms.workbuddy.baseUrl),
712
751
  allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
713
752
  },
753
+ clawbot: {
754
+ ...existing.platforms?.clawbot,
755
+ enabled: payload.platforms.clawbot.enabled,
756
+ aiCommand: persistedPlatformAi(payload.platforms.clawbot.aiCommand),
757
+ apiUrl: clean(payload.platforms.clawbot.apiUrl) ?? "http://127.0.0.1:26322",
758
+ apiToken: resolveSecret(payload.platforms.clawbot.apiToken, existing.platforms?.clawbot?.apiToken),
759
+ allowedUserIds: splitCsv(payload.platforms.clawbot.allowedUserIds),
760
+ },
714
761
  },
715
762
  };
716
763
  }
@@ -1071,6 +1118,30 @@ export async function startWebConfigServer(options) {
1071
1118
  }
1072
1119
  return;
1073
1120
  }
1121
+ if (request.method === "POST" && requestUrl.pathname === "/api/clawbot/qr-login/start") {
1122
+ try {
1123
+ const { startQRLogin } = await import("./clawbot/qr-login.js");
1124
+ const session = await startQRLogin();
1125
+ json(response, 200, { success: true, qrcodeUrl: session.qrcodeUrl, sessionKey: session.sessionKey }, request);
1126
+ }
1127
+ catch (error) {
1128
+ json(response, 500, { success: false, error: toErrorMessage(error) }, request);
1129
+ }
1130
+ return;
1131
+ }
1132
+ if (request.method === "POST" && requestUrl.pathname === "/api/clawbot/qr-login/wait") {
1133
+ try {
1134
+ const body = await readJson(request);
1135
+ const { waitForQRLogin } = await import("./clawbot/qr-login.js");
1136
+ const session = { sessionKey: body.sessionKey, qrcode: body.qrcode, qrcodeUrl: body.qrcodeUrl, startedAt: Date.now() };
1137
+ const result = await waitForQRLogin(session);
1138
+ json(response, 200, { success: result.connected, botToken: result.botToken, userId: result.userId, message: result.message }, request);
1139
+ }
1140
+ catch (error) {
1141
+ json(response, 500, { success: false, error: toErrorMessage(error) }, request);
1142
+ }
1143
+ return;
1144
+ }
1074
1145
  if (request.method === "GET" && tryServeDashboardStatic(requestUrl, request, response, mergeCors)) {
1075
1146
  return;
1076
1147
  }
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { Platform, AiCommand, Config, FilePlatformTelegram, FilePlatformFeishu, FilePlatformQQ, FilePlatformWechat, FilePlatformWework, FilePlatformDingtalk, FilePlatformWorkBuddy, FileToolClaude, FileToolCodex, FileToolCodeBuddy, FileConfig, } from './config/types.js';
1
+ export type { Platform, AiCommand, Config, FilePlatformTelegram, FilePlatformFeishu, FilePlatformQQ, FilePlatformWechat, FilePlatformWework, FilePlatformDingtalk, FilePlatformWorkBuddy, FilePlatformClawbot, FileToolClaude, FileToolCodex, FileToolCodeBuddy, FileConfig, } from './config/types.js';
2
2
  import type { Platform, AiCommand, Config } from './config/types.js';
3
3
  export { CONFIG_PATH, loadFileConfig, saveFileConfig, getClaudeConfigHome, getClaudeSdkRuntimeIssue, hasCodeBuddyAuthIndicators, loadClaudeSettingsEnv, saveClaudeSettingsEnv, normalizeAiCommand, hasCodexAuth, parseCommaSeparated, CLAUDE_AUTH_ENV_KEYS, refreshClaudeEnvToProcess, processEnvForNonClaudeCliChild, CODEX_AUTH_PATHS, } from './config/file-io.js';
4
4
  /** 检测是否需要交互式配置(无 token 且无环境变量) */
package/dist/config.js CHANGED
@@ -23,7 +23,9 @@ function resolveFilePlatformAi(file, platform) {
23
23
  ? pf?.wework?.aiCommand
24
24
  : platform === 'dingtalk'
25
25
  ? pf?.dingtalk?.aiCommand
26
- : pf?.workbuddy?.aiCommand;
26
+ : platform === 'clawbot'
27
+ ? pf?.clawbot?.aiCommand
28
+ : pf?.workbuddy?.aiCommand;
27
29
  return normalizeAiCommand(raw ?? process.env.AI_COMMAND ?? file.aiCommand, 'claude');
28
30
  }
29
31
  // Re-export file I/O and credential helpers from sub-modules
@@ -53,6 +55,7 @@ export function needsSetup() {
53
55
  const ww = file.platforms?.wework;
54
56
  const dt = file.platforms?.dingtalk;
55
57
  const wb = file.platforms?.workbuddy;
58
+ const cb = file.platforms?.clawbot;
56
59
  // Also check legacy platforms.wechat for migration path
57
60
  const legacyWc = file.platforms?.wechat;
58
61
  const hasTelegram = !!tg?.botToken;
@@ -61,8 +64,9 @@ export function needsSetup() {
61
64
  const hasWework = !!(ww?.corpId && ww?.secret);
62
65
  const hasDingtalk = !!(dt?.clientId && dt?.clientSecret);
63
66
  const hasWorkBuddy = !!(wb?.accessToken && wb?.refreshToken && wb?.userId);
67
+ const hasClawBot = !!(cb?.apiToken);
64
68
  const hasLegacyWechat = !!(legacyWc?.workbuddyAccessToken && legacyWc?.workbuddyRefreshToken);
65
- return !hasTelegram && !hasFeishu && !hasQQ && !hasWework && !hasDingtalk && !hasWorkBuddy && !hasLegacyWechat;
69
+ return !hasTelegram && !hasFeishu && !hasQQ && !hasWework && !hasDingtalk && !hasWorkBuddy && !hasClawBot && !hasLegacyWechat;
66
70
  }
67
71
  export function loadConfig() {
68
72
  const file = loadFileConfig();
@@ -73,6 +77,7 @@ export function loadConfig() {
73
77
  const fileQQ = file.platforms?.qq;
74
78
  const fileWework = file.platforms?.wework;
75
79
  const fileDingtalk = file.platforms?.dingtalk;
80
+ const fileClawbot = file.platforms?.clawbot;
76
81
  // Auto-migrate legacy platforms.wechat WorkBuddy credentials → platforms.workbuddy
77
82
  const legacyWechat = file.platforms?.wechat;
78
83
  const fileWorkBuddy = file.platforms?.workbuddy ?? (legacyWechat?.workbuddyAccessToken && legacyWechat?.workbuddyRefreshToken
@@ -122,6 +127,11 @@ export function loadConfig() {
122
127
  fileWorkBuddy?.guid;
123
128
  const workbuddyWorkspacePath = process.env.WORKBUDDY_WORKSPACE_PATH ??
124
129
  fileWorkBuddy?.workspacePath;
130
+ // ClawBot credentials
131
+ const clawbotApiUrl = process.env.CLAWBOT_API_URL ??
132
+ fileClawbot?.apiUrl;
133
+ const clawbotApiToken = process.env.CLAWBOT_API_TOKEN ??
134
+ fileClawbot?.apiToken;
125
135
  // 2. 计算启用平台
126
136
  const enabledPlatforms = [];
127
137
  const telegramEnabledFlag = fileTelegram?.enabled;
@@ -130,12 +140,14 @@ export function loadConfig() {
130
140
  const weworkEnabledFlag = fileWework?.enabled;
131
141
  const dingtalkEnabledFlag = fileDingtalk?.enabled;
132
142
  const workbuddyEnabledFlag = fileWorkBuddy?.enabled;
143
+ const clawbotEnabledFlag = fileClawbot?.enabled;
133
144
  const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
134
145
  const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
135
146
  const qqEnabled = !!(qqAppId && qqSecret) && (qqEnabledFlag !== false);
136
147
  const weworkEnabled = !!(weworkCorpId && weworkSecret) && (weworkEnabledFlag !== false);
137
148
  const dingtalkEnabled = !!(dingtalkClientId && dingtalkClientSecret) && (dingtalkEnabledFlag !== false);
138
149
  const workbuddyEnabled = !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && (workbuddyEnabledFlag !== false);
150
+ const clawbotEnabled = !!clawbotApiToken && (clawbotEnabledFlag !== false);
139
151
  if (telegramEnabled)
140
152
  enabledPlatforms.push('telegram');
141
153
  if (feishuEnabled)
@@ -148,6 +160,8 @@ export function loadConfig() {
148
160
  enabledPlatforms.push('dingtalk');
149
161
  if (workbuddyEnabled)
150
162
  enabledPlatforms.push('workbuddy');
163
+ if (clawbotEnabled)
164
+ enabledPlatforms.push('clawbot');
151
165
  if (enabledPlatforms.length === 0) {
152
166
  throw new Error('至少需要配置 Telegram、Feishu、WeChat、WeWork 或 DingTalk 其中一个平台(可以通过环境变量或 config.json)');
153
167
  }
@@ -174,6 +188,9 @@ export function loadConfig() {
174
188
  const workbuddyAllowedUserIds = process.env.WORKBUDDY_ALLOWED_USER_IDS !== undefined
175
189
  ? parseCommaSeparated(process.env.WORKBUDDY_ALLOWED_USER_IDS)
176
190
  : fileWorkBuddy?.allowedUserIds ?? allowedUserIds;
191
+ const clawbotAllowedUserIds = process.env.CLAWBOT_ALLOWED_USER_IDS !== undefined
192
+ ? parseCommaSeparated(process.env.CLAWBOT_ALLOWED_USER_IDS)
193
+ : fileClawbot?.allowedUserIds ?? allowedUserIds;
177
194
  // 5. AI / 工作目录 / 安全配置(从 tools 读取)
178
195
  const tc = file.tools?.claude ?? {};
179
196
  const tcod = file.tools?.codex ?? {};
@@ -507,6 +524,21 @@ export function loadConfig() {
507
524
  guid: workbuddyGuid,
508
525
  workspacePath: workbuddyWorkspacePath,
509
526
  },
527
+ clawbot: clawbotEnabled
528
+ ? {
529
+ enabled: true,
530
+ aiCommand: resolveFilePlatformAi(file, 'clawbot'),
531
+ allowedUserIds: clawbotAllowedUserIds,
532
+ apiUrl: clawbotApiUrl,
533
+ apiToken: clawbotApiToken,
534
+ }
535
+ : {
536
+ enabled: false,
537
+ aiCommand: resolveFilePlatformAi(file, 'clawbot'),
538
+ allowedUserIds: clawbotAllowedUserIds,
539
+ apiUrl: clawbotApiUrl,
540
+ apiToken: clawbotApiToken,
541
+ },
510
542
  };
511
543
  return {
512
544
  enabledPlatforms,
@@ -528,6 +560,7 @@ export function loadConfig() {
528
560
  weworkAllowedUserIds,
529
561
  dingtalkAllowedUserIds,
530
562
  workbuddyAllowedUserIds,
563
+ clawbotAllowedUserIds,
531
564
  codexCliPath,
532
565
  codebuddyCliPath,
533
566
  claudeProxy,
@@ -562,6 +595,9 @@ export function getPlatformsWithCredentials(config) {
562
595
  const wb = config.platforms.workbuddy;
563
596
  if (wb?.accessToken && wb?.refreshToken)
564
597
  r.push('workbuddy');
598
+ const cb = config.platforms.clawbot;
599
+ if (cb?.apiToken)
600
+ r.push('clawbot');
565
601
  return r;
566
602
  }
567
603
  export function resolvePlatformAiCommand(config, platform) {
@@ -38,3 +38,9 @@ export declare const MAX_WEWORK_MESSAGE_LENGTH = 2048;
38
38
  export declare const MAX_DINGTALK_MESSAGE_LENGTH = 2048;
39
39
  /** WeChat KF (微信客服) 单条消息最大字符数 */
40
40
  export declare const MAX_WORKBUDDY_MESSAGE_LENGTH = 2000;
41
+ /** ClawBot 流式更新节流 */
42
+ export declare const CLAWBOT_THROTTLE_MS = 1000;
43
+ /** ClawBot 单条消息最大字符数 */
44
+ export declare const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
45
+ /** ClawBot 长轮询间隔 */
46
+ export declare const CLAWBOT_POLL_INTERVAL_MS = 3000;
package/dist/constants.js CHANGED
@@ -68,3 +68,9 @@ export const MAX_WEWORK_MESSAGE_LENGTH = 2048;
68
68
  export const MAX_DINGTALK_MESSAGE_LENGTH = 2048;
69
69
  /** WeChat KF (微信客服) 单条消息最大字符数 */
70
70
  export const MAX_WORKBUDDY_MESSAGE_LENGTH = 2000;
71
+ /** ClawBot 流式更新节流 */
72
+ export const CLAWBOT_THROTTLE_MS = 1000;
73
+ /** ClawBot 单条消息最大字符数 */
74
+ export const MAX_CLAWBOT_MESSAGE_LENGTH = 2000;
75
+ /** ClawBot 长轮询间隔 */
76
+ export const CLAWBOT_POLL_INTERVAL_MS = 3000;
package/dist/index.js CHANGED
@@ -24,6 +24,10 @@ import { setupDingTalkHandlers } from "./dingtalk/event-handler.js";
24
24
  import { initWorkBuddy, stopWorkBuddy } from "./workbuddy/client.js";
25
25
  import { setupWorkBuddyHandlers } from "./workbuddy/event-handler.js";
26
26
  import { sendTextReply as sendWorkBuddyTextReply } from "./workbuddy/message-sender.js";
27
+ import { initClawbot, stopClawbot } from "./clawbot/client.js";
28
+ import { setupClawbotHandlers } from "./clawbot/event-handler.js";
29
+ import { sendTextReply as sendClawbotTextReply } from "./clawbot/message-sender.js";
30
+ import { initClawBotSender } from "./clawbot/message-sender.js";
27
31
  import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
28
32
  import { SessionManager } from "./session/session-manager.js";
29
33
  import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
@@ -94,6 +98,19 @@ const PLATFORM_MODULES = {
94
98
  stop: () => stopWorkBuddy(),
95
99
  sendNotification: (chatId, msg) => sendWorkBuddyTextReply(null, chatId, msg, randomUUID()),
96
100
  },
101
+ clawbot: {
102
+ init: async (config, sessionManager) => {
103
+ const pc = config.platforms.clawbot;
104
+ if (pc?.apiUrl && pc?.apiToken) {
105
+ initClawBotSender(pc.apiUrl, pc.apiToken);
106
+ }
107
+ const handle = setupClawbotHandlers(config, sessionManager);
108
+ await initClawbot(config, handle.handleEvent);
109
+ return handle;
110
+ },
111
+ stop: () => stopClawbot(),
112
+ sendNotification: (chatId, msg) => sendClawbotTextReply(chatId, msg),
113
+ },
97
114
  };
98
115
  async function sendLifecycleNotification(platform, message) {
99
116
  const mod = PLATFORM_MODULES[platform];
package/dist/setup.js CHANGED
@@ -31,6 +31,7 @@ function getConfiguredPlatforms(existing) {
31
31
  { k: "wework", label: "企业微信" },
32
32
  { k: "dingtalk", label: "钉钉" },
33
33
  { k: "workbuddy", label: "WorkBuddy (微信)" },
34
+ { k: "clawbot", label: "ClawBot (微信 iLink)" },
34
35
  ];
35
36
  return names
36
37
  .filter(({ k }) => {
@@ -49,6 +50,8 @@ function getConfiguredPlatforms(existing) {
49
50
  return !!(p.corpId && p.secret);
50
51
  if (k === "dingtalk")
51
52
  return !!(p.clientId && p.clientSecret);
53
+ if (k === "clawbot")
54
+ return !!p.apiToken;
52
55
  return false;
53
56
  })
54
57
  .map(({ label }) => label);
@@ -125,12 +128,19 @@ function printManualInstructions(configPath) {
125
128
  "workbuddyRefreshToken": "",
126
129
  "userId": "",
127
130
  "allowedUserIds": ["允许访问的微信用户 ID(可选)"]
131
+ },
132
+ "clawbot": {
133
+ "enabled": false,
134
+ "aiCommand": "claude",
135
+ "apiUrl": "http://127.0.0.1:26322",
136
+ "apiToken": "你的 ClawBot Bearer Token(可选)",
137
+ "allowedUserIds": ["允许访问的微信用户 ID(可选)"]
128
138
  }
129
139
  }
130
140
  }`);
131
141
  console.log("");
132
- console.log("提示:至少需要配置 Telegram、Feishu、QQ、WeChat、WeWork 或 DingTalk 其中一个平台");
133
- console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、QQ_BOT_APPID=xxx、WECHAT_WORKBUDDY_ACCESS_TOKEN=xxx、WEWORK_CORP_ID=xxx 或 DINGTALK_CLIENT_ID=xxx 后再运行");
142
+ console.log("提示:至少需要配置 Telegram、Feishu、QQ、WeChat、WeWork、DingTalkClawBot 其中一个平台");
143
+ console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、QQ_BOT_APPID=xxx、WECHAT_WORKBUDDY_ACCESS_TOKEN=xxx、WEWORK_CORP_ID=xxx、DINGTALK_CLIENT_ID=xxxCLAWBOT_API_TOKEN=xxx 后再运行");
134
144
  console.log("");
135
145
  }
136
146
  const CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
@@ -317,6 +327,11 @@ export async function runInteractiveSetup() {
317
327
  (hasWc ? " ✓已配置" : ""),
318
328
  value: "workbuddy",
319
329
  },
330
+ {
331
+ title: "ClawBot 微信 iLink (需要 API Token)" +
332
+ (!!existing?.platforms?.clawbot?.apiToken ? " ✓已配置" : ""),
333
+ value: "clawbot",
334
+ },
320
335
  { title: "配置多个平台", value: "multi" },
321
336
  ],
322
337
  initial: 0,
@@ -339,6 +354,7 @@ export async function runInteractiveSetup() {
339
354
  { title: "企业微信 (WeWork)" + (hasWw ? " ✓已配置" : ""), value: "wework", selected: hasWw },
340
355
  { title: "钉钉 (DingTalk)" + (hasDt ? " ✓已配置" : ""), value: "dingtalk", selected: hasDt },
341
356
  { title: "WorkBuddy 微信客服 (WeChat KF)" + (hasWc ? " ✓已配置" : ""), value: "workbuddy", selected: hasWc },
357
+ { title: "ClawBot 微信 iLink" + (!!existing?.platforms?.clawbot?.apiToken ? " ✓已配置" : ""), value: "clawbot" },
342
358
  ],
343
359
  }, { onCancel });
344
360
  if (!multiResp.platforms || multiResp.platforms.length === 0) {
@@ -563,6 +579,82 @@ export async function runInteractiveSetup() {
563
579
  return false;
564
580
  }
565
581
  }
582
+ if (selectedPlatforms.includes("clawbot")) {
583
+ const hasCbToken = !!existing?.platforms?.clawbot?.apiToken;
584
+ const cbModeResp = await prompts({
585
+ type: "select",
586
+ name: "mode",
587
+ message: "ClawBot 微信 iLink",
588
+ choices: [
589
+ { title: "扫码登录(自动获取 Token)", value: "qr" },
590
+ { title: "使用已有 Token" + (hasCbToken ? " ✓" : ""), value: "token", disabled: !hasCbToken },
591
+ ],
592
+ initial: 0,
593
+ }, { onCancel });
594
+ let cbApiToken = "";
595
+ let cbApiUrl = existing?.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322";
596
+ if (cbModeResp.mode === "qr") {
597
+ console.log("\n正在获取二维码...\n");
598
+ try {
599
+ const { startQRLogin, waitForQRLogin } = await import("./clawbot/qr-login.js");
600
+ const session = await startQRLogin();
601
+ console.log("请用微信扫描以下链接中的二维码:");
602
+ console.log(session.qrcodeUrl);
603
+ console.log("\n等待扫码...(最长 5 分钟)\n");
604
+ const result = await waitForQRLogin(session, (status) => {
605
+ if (status === "scaned")
606
+ process.stdout.write("已扫码,验证中...\n");
607
+ });
608
+ if (result.connected && result.botToken) {
609
+ cbApiToken = result.botToken;
610
+ console.log("\n✅ 登录成功!");
611
+ if (result.userId) {
612
+ console.log(` 用户 ID: ${result.userId}`);
613
+ console.log(" 提示:可将此 ID 添加到白名单");
614
+ }
615
+ }
616
+ else {
617
+ console.log(`\n❌ 登录失败: ${result.message}`);
618
+ if (platform === "clawbot")
619
+ return false;
620
+ }
621
+ }
622
+ catch (err) {
623
+ console.error(`\n❌ 二维码获取失败: ${err instanceof Error ? err.message : String(err)}`);
624
+ if (platform === "clawbot")
625
+ return false;
626
+ }
627
+ }
628
+ else {
629
+ const cbResp = await prompts([
630
+ {
631
+ type: "text",
632
+ name: "apiUrl",
633
+ message: "ClawBot iLink API 地址(默认 http://127.0.0.1:26322)",
634
+ initial: cbApiUrl,
635
+ },
636
+ {
637
+ type: "text",
638
+ name: "apiToken",
639
+ message: "ClawBot Bearer Token",
640
+ initial: existing?.platforms?.clawbot?.apiToken ?? "",
641
+ validate: (v) => (v.trim() ? true : "Token 不能为空"),
642
+ },
643
+ ], { onCancel });
644
+ cbApiUrl = cbResp.apiUrl?.trim() || cbApiUrl;
645
+ cbApiToken = cbResp.apiToken?.trim() || "";
646
+ }
647
+ if (cbApiToken) {
648
+ config.platforms.clawbot = {
649
+ enabled: true,
650
+ apiUrl: cbApiUrl,
651
+ apiToken: cbApiToken,
652
+ };
653
+ }
654
+ else if (platform === "clawbot") {
655
+ return false;
656
+ }
657
+ }
566
658
  if (selectedPlatforms.includes("wework")) {
567
659
  const weworkResp = await prompts([
568
660
  {
@@ -687,6 +779,15 @@ export async function runInteractiveSetup() {
687
779
  initial: dtIds,
688
780
  });
689
781
  }
782
+ if (selectedPlatforms.includes("clawbot")) {
783
+ const cbIds = existing?.platforms?.clawbot?.allowedUserIds?.join(", ") ?? "";
784
+ commonPrompts.push({
785
+ type: "text",
786
+ name: "clawbotAllowedUserIds",
787
+ message: "ClawBot 白名单用户 ID(可选,逗号分隔,留空=所有人可访问)",
788
+ initial: cbIds,
789
+ });
790
+ }
690
791
  commonPrompts.push({
691
792
  type: "text",
692
793
  name: "workDir",
@@ -796,6 +897,9 @@ export async function runInteractiveSetup() {
796
897
  const dingtalkIds = selectedPlatforms.includes("dingtalk")
797
898
  ? parseIds(commonResp.dingtalkAllowedUserIds)
798
899
  : parseIds(existing?.platforms?.dingtalk?.allowedUserIds?.join(", "));
900
+ const clawbotIds = selectedPlatforms.includes("clawbot")
901
+ ? parseIds(commonResp.clawbotAllowedUserIds)
902
+ : parseIds(existing?.platforms?.clawbot?.allowedUserIds?.join(", "));
799
903
  // 增量合并:以已有配置为底,只覆盖本次选中的平台(不写入根级旧字段 telegramBotToken 等)
800
904
  const base = existing
801
905
  ? JSON.parse(JSON.stringify(existing))
@@ -985,6 +1089,27 @@ export async function runInteractiveSetup() {
985
1089
  else {
986
1090
  outPlatforms.dingtalk = { enabled: false, aiCommand: "claude", allowedUserIds: dingtalkIds };
987
1091
  }
1092
+ if (selectedPlatforms.includes("clawbot")) {
1093
+ const cbConfig = config.platforms?.clawbot;
1094
+ const baseCb = base?.platforms?.clawbot;
1095
+ outPlatforms.clawbot = {
1096
+ enabled: true,
1097
+ aiCommand: defaultPlatformAi(baseCb?.aiCommand),
1098
+ apiUrl: String(cbConfig?.apiUrl ?? baseCb?.apiUrl ?? "http://127.0.0.1:26322"),
1099
+ apiToken: String(cbConfig?.apiToken ?? baseCb?.apiToken ?? ""),
1100
+ allowedUserIds: clawbotIds,
1101
+ };
1102
+ }
1103
+ else if (basePlatforms?.clawbot) {
1104
+ outPlatforms.clawbot = {
1105
+ ...basePlatforms.clawbot,
1106
+ aiCommand: defaultPlatformAi(basePlatforms.clawbot.aiCommand),
1107
+ allowedUserIds: clawbotIds.length > 0 ? clawbotIds : basePlatforms.clawbot.allowedUserIds ?? [],
1108
+ };
1109
+ }
1110
+ else {
1111
+ outPlatforms.clawbot = { enabled: false, aiCommand: "claude", allowedUserIds: clawbotIds };
1112
+ }
988
1113
  const dir = dirname(configPath);
989
1114
  if (!existsSync(dir)) {
990
1115
  mkdirSync(dir, { recursive: true });
@@ -1005,8 +1130,9 @@ const PLATFORM_LABELS = {
1005
1130
  wework: "企业微信",
1006
1131
  dingtalk: "钉钉",
1007
1132
  workbuddy: "WorkBuddy 微信客服",
1133
+ clawbot: "ClawBot 微信 iLink",
1008
1134
  };
1009
- const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "workbuddy"];
1135
+ const ALL_PLATFORMS = ["telegram", "feishu", "qq", "wework", "dingtalk", "workbuddy", "clawbot"];
1010
1136
  /**
1011
1137
  * 启动时让用户选择要启用的平台(无论单通道还是多通道)
1012
1138
  * 显示全部 4 个平台,已配置的预选;若用户选择未配置的,引导运行 init
@@ -6,8 +6,8 @@ export interface DingTalkActiveTarget {
6
6
  updatedAt: number;
7
7
  }
8
8
  export declare function loadActiveChats(): void;
9
- export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy'): string | undefined;
10
- export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', chatId: string): void;
9
+ export declare function getActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy' | 'clawbot'): string | undefined;
10
+ export declare function setActiveChatId(platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy' | 'clawbot', chatId: string): void;
11
11
  export declare function getDingTalkActiveTarget(): DingTalkActiveTarget | undefined;
12
12
  export declare function setDingTalkActiveTarget(target: Omit<DingTalkActiveTarget, 'updatedAt'>): void;
13
13
  export declare function flushActiveChats(): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.10.9-beta.10",
3
+ "version": "1.10.9-beta.12",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",