@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.
- package/dist/adapters/claude-sdk-adapter.js +19 -20
- package/dist/channels/capabilities.js +5 -0
- package/dist/clawbot/client.d.ts +11 -0
- package/dist/clawbot/client.js +169 -0
- package/dist/clawbot/event-handler.d.ts +12 -0
- package/dist/clawbot/event-handler.js +85 -0
- package/dist/clawbot/message-sender.d.ts +12 -0
- package/dist/clawbot/message-sender.js +67 -0
- package/dist/clawbot/qr-login.d.ts +33 -0
- package/dist/clawbot/qr-login.js +120 -0
- package/dist/clawbot/types.d.ts +43 -0
- package/dist/clawbot/types.js +4 -0
- package/dist/config/types.d.ts +17 -1
- package/dist/config-web.js +71 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +38 -2
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +6 -0
- package/dist/index.js +17 -0
- package/dist/setup.js +129 -3
- package/dist/shared/active-chats.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
406
|
-
log.warn(`
|
|
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
|
+
}
|
package/dist/config/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/config-web.js
CHANGED
|
@@ -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
|
-
:
|
|
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) {
|
package/dist/constants.d.ts
CHANGED
|
@@ -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 或
|
|
133
|
-
console.log("或设置环境变量: TELEGRAM_BOT_TOKEN=xxx、FEISHU_APP_ID=xxx、QQ_BOT_APPID=xxx、WECHAT_WORKBUDDY_ACCESS_TOKEN=xxx、WEWORK_CORP_ID=xxx 或
|
|
142
|
+
console.log("提示:至少需要配置 Telegram、Feishu、QQ、WeChat、WeWork、DingTalk 或 ClawBot 其中一个平台");
|
|
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=xxx 或 CLAWBOT_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;
|