@wu529778790/open-im 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -3
- package/dist/commands/handler.d.ts +1 -1
- package/dist/config.d.ts +19 -1
- package/dist/config.js +69 -4
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/index.js +59 -12
- package/dist/setup.js +140 -29
- package/dist/shared/active-chats.d.ts +2 -2
- package/dist/wechat/auth/device-bind.d.ts +13 -0
- package/dist/wechat/auth/device-bind.js +76 -0
- package/dist/wechat/auth/device-guid.d.ts +5 -0
- package/dist/wechat/auth/device-guid.js +28 -0
- package/dist/wechat/auth/environments.d.ts +5 -0
- package/dist/wechat/auth/environments.js +21 -0
- package/dist/wechat/auth/index.d.ts +7 -0
- package/dist/wechat/auth/index.js +5 -0
- package/dist/wechat/auth/qclaw-api.d.ts +26 -0
- package/dist/wechat/auth/qclaw-api.js +100 -0
- package/dist/wechat/auth/types.d.ts +18 -0
- package/dist/wechat/auth/types.js +4 -0
- package/dist/wechat/auth/wechat-login.d.ts +17 -0
- package/dist/wechat/auth/wechat-login.js +172 -0
- package/dist/wechat/client.js +11 -2
- package/dist/wework/client.d.ts +46 -0
- package/dist/wework/client.js +356 -0
- package/dist/wework/event-handler.d.ts +12 -0
- package/dist/wework/event-handler.js +245 -0
- package/dist/wework/message-sender.d.ts +57 -0
- package/dist/wework/message-sender.js +258 -0
- package/dist/wework/types.d.ts +156 -0
- package/dist/wework/types.js +6 -0
- package/package.json +4 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeWork (企业微信) Event Handler - Handle WeWork message events
|
|
3
|
+
*/
|
|
4
|
+
import { AccessControl } from '../access/access-control.js';
|
|
5
|
+
import { RequestQueue } from '../queue/request-queue.js';
|
|
6
|
+
import { sendThinkingMessage, updateMessage, sendFinalMessages, sendTextReply, startTypingLoop, sendPermissionCard, sendModeCard, setCurrentReqId, } from './message-sender.js';
|
|
7
|
+
import { registerPermissionSender } from '../hook/permission-server.js';
|
|
8
|
+
import { CommandHandler } from '../commands/handler.js';
|
|
9
|
+
import { getAdapter } from '../adapters/registry.js';
|
|
10
|
+
import { runAITask } from '../shared/ai-task.js';
|
|
11
|
+
import { startTaskCleanup } from '../shared/task-cleanup.js';
|
|
12
|
+
import { WEWORK_THROTTLE_MS } from '../constants.js';
|
|
13
|
+
import { setActiveChatId } from '../shared/active-chats.js';
|
|
14
|
+
import { setChatUser } from '../shared/chat-user-map.js';
|
|
15
|
+
import { createLogger } from '../logger.js';
|
|
16
|
+
const log = createLogger('WeWorkHandler');
|
|
17
|
+
export function setupWeWorkHandlers(config, sessionManager) {
|
|
18
|
+
const accessControl = new AccessControl(config.weworkAllowedUserIds);
|
|
19
|
+
const requestQueue = new RequestQueue();
|
|
20
|
+
const runningTasks = new Map();
|
|
21
|
+
const stopTaskCleanup = startTaskCleanup(runningTasks);
|
|
22
|
+
const commandHandler = new CommandHandler({
|
|
23
|
+
config,
|
|
24
|
+
sessionManager,
|
|
25
|
+
requestQueue,
|
|
26
|
+
sender: { sendTextReply, sendModeCard },
|
|
27
|
+
getRunningTasksSize: () => runningTasks.size,
|
|
28
|
+
});
|
|
29
|
+
registerPermissionSender('wework', { sendTextReply, sendPermissionCard });
|
|
30
|
+
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, reqId) {
|
|
31
|
+
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
32
|
+
if (reqId)
|
|
33
|
+
setCurrentReqId(reqId);
|
|
34
|
+
try {
|
|
35
|
+
const toolAdapter = getAdapter(config.aiCommand);
|
|
36
|
+
if (!toolAdapter) {
|
|
37
|
+
log.error(`[handleAIRequest] No adapter found for: ${config.aiCommand}`);
|
|
38
|
+
await sendTextReply(chatId, `未配置 AI 工具: ${config.aiCommand}`, reqId);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const sessionId = convId ? sessionManager.getSessionIdForConv(userId, convId) : undefined;
|
|
42
|
+
log.info(`[handleAIRequest] Running ${config.aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
43
|
+
const toolId = config.aiCommand;
|
|
44
|
+
let msgId;
|
|
45
|
+
try {
|
|
46
|
+
msgId = await sendThinkingMessage(chatId, replyToMessageId, toolId, reqId);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
log.error('Failed to send thinking message:', err);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const stopTyping = startTypingLoop(chatId);
|
|
53
|
+
const taskKey = `${userId}:${msgId}`;
|
|
54
|
+
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'wework', taskKey }, prompt, toolAdapter, {
|
|
55
|
+
throttleMs: WEWORK_THROTTLE_MS,
|
|
56
|
+
streamUpdate: async (content, toolNote) => {
|
|
57
|
+
const note = toolNote ? '输出中...\n' + toolNote : '输出中...';
|
|
58
|
+
try {
|
|
59
|
+
await updateMessage(chatId, msgId, content, 'streaming', note, toolId, reqId);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
log.debug('Stream update failed:', err);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
sendComplete: async (content, note) => {
|
|
66
|
+
await sendFinalMessages(chatId, msgId, content, note ?? '', toolId, reqId);
|
|
67
|
+
},
|
|
68
|
+
sendError: async (error) => {
|
|
69
|
+
await updateMessage(chatId, msgId, `错误:${error}`, 'error', '执行失败', toolId, reqId);
|
|
70
|
+
},
|
|
71
|
+
extraCleanup: () => {
|
|
72
|
+
stopTyping();
|
|
73
|
+
runningTasks.delete(taskKey);
|
|
74
|
+
},
|
|
75
|
+
onTaskReady: (state) => {
|
|
76
|
+
runningTasks.set(taskKey, state);
|
|
77
|
+
},
|
|
78
|
+
sendImage: async (path) => {
|
|
79
|
+
// WeWork image handling
|
|
80
|
+
await sendTextReply(chatId, `图片已保存: ${path}`, reqId);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
setCurrentReqId(null);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Extract text content from WeWork message body
|
|
90
|
+
*/
|
|
91
|
+
function extractTextContent(data) {
|
|
92
|
+
const body = data.body;
|
|
93
|
+
// Direct text message
|
|
94
|
+
if (body.msgtype === 'text' && body.text?.content) {
|
|
95
|
+
return body.text.content.trim();
|
|
96
|
+
}
|
|
97
|
+
// Mixed message (text + images)
|
|
98
|
+
if (body.msgtype === 'mixed' && body.mixed?.msg_item) {
|
|
99
|
+
const textItems = body.mixed.msg_item
|
|
100
|
+
.filter(item => item.msgtype === 'text' && item.text?.content)
|
|
101
|
+
.map(item => item.text.content)
|
|
102
|
+
.join('\n');
|
|
103
|
+
return textItems;
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extract image content from WeWork message body
|
|
109
|
+
*/
|
|
110
|
+
function extractImageContent(data) {
|
|
111
|
+
const body = data.body;
|
|
112
|
+
// Direct image message
|
|
113
|
+
if (body.msgtype === 'image' && body.image) {
|
|
114
|
+
return {
|
|
115
|
+
url: body.image.url,
|
|
116
|
+
base64: body.image.base64,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// Mixed message with images
|
|
120
|
+
if (body.msgtype === 'mixed' && body.mixed?.msg_item) {
|
|
121
|
+
const firstImage = body.mixed.msg_item.find(item => item.msgtype === 'image' && item.image);
|
|
122
|
+
if (firstImage?.image) {
|
|
123
|
+
return {
|
|
124
|
+
url: firstImage.image.url,
|
|
125
|
+
base64: firstImage.image.base64,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Handle incoming WeWork callback event
|
|
133
|
+
*/
|
|
134
|
+
async function handleEvent(data) {
|
|
135
|
+
log.info('[handleEvent] Called with data:', JSON.stringify(data).slice(0, 800));
|
|
136
|
+
const reqId = data.headers?.req_id ?? '';
|
|
137
|
+
setCurrentReqId(reqId);
|
|
138
|
+
try {
|
|
139
|
+
const body = data.body;
|
|
140
|
+
const msgType = body.msgtype;
|
|
141
|
+
const fromUser = body.from.userid;
|
|
142
|
+
// 单聊时 chatid 可能不返回,用 userid 作为会话标识
|
|
143
|
+
const chatId = body.chatid ?? fromUser;
|
|
144
|
+
const chatType = body.chattype;
|
|
145
|
+
log.info(`WeWork event: msgType=${msgType}, from=${fromUser}, chatId=${chatId}, chatType=${chatType}`);
|
|
146
|
+
// Access control check
|
|
147
|
+
if (!accessControl.isAllowed(fromUser)) {
|
|
148
|
+
log.warn(`Access denied for sender: ${fromUser}`);
|
|
149
|
+
await sendTextReply(fromUser, `抱歉,您没有访问权限。\n您的 ID: ${fromUser}`, reqId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
log.info(`Access granted for sender: ${fromUser}`);
|
|
153
|
+
setActiveChatId('wework', fromUser);
|
|
154
|
+
setChatUser(fromUser, fromUser);
|
|
155
|
+
// Handle text messages
|
|
156
|
+
if (msgType === 'text' || msgType === 'mixed') {
|
|
157
|
+
const text = extractTextContent(data);
|
|
158
|
+
if (!text) {
|
|
159
|
+
log.debug('[MSG] No text content found in message');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
log.info(`[MSG] Type=${msgType}, User=${fromUser}, Length=${text.length}, Content="${text}"`);
|
|
163
|
+
// Handle commands (sync, uses setCurrentReqId)
|
|
164
|
+
try {
|
|
165
|
+
const handleAIRequestWithReqId = (u, c, p, w, conv, tc, replyTo) => handleAIRequest(u, c, p, w, conv, tc, replyTo, reqId);
|
|
166
|
+
const handled = await commandHandler.dispatch(text, fromUser, fromUser, 'wework', handleAIRequestWithReqId);
|
|
167
|
+
if (handled) {
|
|
168
|
+
log.info(`Command handled for message: ${text}`);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
log.error('Error in commandHandler.dispatch:', err);
|
|
174
|
+
}
|
|
175
|
+
// Handle AI request
|
|
176
|
+
log.info(`Enqueueing AI request for: ${text}`);
|
|
177
|
+
const workDir = sessionManager.getWorkDir(fromUser);
|
|
178
|
+
const convId = sessionManager.getConvId(fromUser);
|
|
179
|
+
const enqueueResult = requestQueue.enqueue(fromUser, convId, text, async (prompt) => {
|
|
180
|
+
log.info(`Executing AI request for: ${prompt}`);
|
|
181
|
+
await handleAIRequest(fromUser, fromUser, prompt, workDir, convId, undefined, undefined, reqId);
|
|
182
|
+
});
|
|
183
|
+
if (enqueueResult === 'rejected') {
|
|
184
|
+
await sendTextReply(fromUser, '请求队列已满,请稍后再试。', reqId);
|
|
185
|
+
}
|
|
186
|
+
else if (enqueueResult === 'queued') {
|
|
187
|
+
await sendTextReply(fromUser, '您的请求已排队等待。', reqId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Handle image messages
|
|
191
|
+
else if (msgType === 'image') {
|
|
192
|
+
const imageData = extractImageContent(data);
|
|
193
|
+
if (!imageData) {
|
|
194
|
+
log.warn('[MSG] Image message has no content');
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const imageDesc = imageData.url ? `URL: ${imageData.url}` : `Base64数据 (${imageData.base64?.length || 0} 字符)`;
|
|
198
|
+
log.info(`Processing image message from ${fromUser}, ${imageDesc}`);
|
|
199
|
+
// TODO: Implement image analysis
|
|
200
|
+
const prompt = `用户发送了一张图片。请分析图片内容。`;
|
|
201
|
+
const workDir = sessionManager.getWorkDir(fromUser);
|
|
202
|
+
const convId = sessionManager.getConvId(fromUser);
|
|
203
|
+
requestQueue.enqueue(fromUser, convId, prompt, async (p) => {
|
|
204
|
+
await handleAIRequest(fromUser, fromUser, p, workDir, convId, undefined, undefined, reqId);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
// Handle file messages
|
|
208
|
+
else if (msgType === 'file') {
|
|
209
|
+
log.info(`[MSG] File message from ${fromUser} - not supported`);
|
|
210
|
+
await sendTextReply(fromUser, '文件消息暂不支持', reqId);
|
|
211
|
+
}
|
|
212
|
+
// Handle voice messages
|
|
213
|
+
else if (msgType === 'voice') {
|
|
214
|
+
log.info(`[MSG] Voice message from ${fromUser} - not supported`);
|
|
215
|
+
await sendTextReply(fromUser, '语音消息暂不支持', reqId);
|
|
216
|
+
}
|
|
217
|
+
// Handle video messages
|
|
218
|
+
else if (msgType === 'video') {
|
|
219
|
+
log.info(`[MSG] Video message from ${fromUser} - not supported`);
|
|
220
|
+
await sendTextReply(fromUser, '视频消息暂不支持', reqId);
|
|
221
|
+
}
|
|
222
|
+
// Handle stream messages (WebSocket streaming response)
|
|
223
|
+
else if (msgType === 'stream') {
|
|
224
|
+
log.debug(`[MSG] Stream message from ${fromUser}, streamId=${body.stream?.id}`);
|
|
225
|
+
// Stream messages are typically responses, not requests
|
|
226
|
+
// We can ignore them or handle them if needed
|
|
227
|
+
}
|
|
228
|
+
// Unsupported message type
|
|
229
|
+
else {
|
|
230
|
+
log.warn(`[MSG] Unsupported message type: ${msgType}, fromUser=${fromUser}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
log.error('[handleEvent] Error processing event:', err);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
setCurrentReqId(null);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
stop: () => stopTaskCleanup(),
|
|
242
|
+
getRunningTaskCount: () => runningTasks.size,
|
|
243
|
+
handleEvent,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeWork (企业微信) Message Sender - Send messages to WeWork
|
|
3
|
+
* 通过 WebSocket aibot_respond_msg 发送,需透传 req_id
|
|
4
|
+
*/
|
|
5
|
+
export declare function setCurrentReqId(reqId: string | null): void;
|
|
6
|
+
type MessageStatus = 'thinking' | 'streaming' | 'done' | 'error';
|
|
7
|
+
/**
|
|
8
|
+
* Send thinking message to WeWork
|
|
9
|
+
* Returns a stream ID that can be used for updates
|
|
10
|
+
* @param reqId - 消息回调的 req_id,用于 WebSocket 回复
|
|
11
|
+
*/
|
|
12
|
+
export declare function sendThinkingMessage(chatId: string, _replyToMessageId: string | undefined, toolId?: string, reqId?: string): Promise<string>;
|
|
13
|
+
/**
|
|
14
|
+
* Update existing message in WeWork
|
|
15
|
+
* Note: WeWork doesn't support message editing, so we send new stream messages
|
|
16
|
+
*/
|
|
17
|
+
export declare function updateMessage(chatId: string, streamId: string, content: string, status: MessageStatus, note?: string, toolId?: string, reqId?: string): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Send final messages to WeWork (handle long content)
|
|
20
|
+
*/
|
|
21
|
+
export declare function sendFinalMessages(chatId: string, streamId: string, fullContent: string, note: string, toolId?: string, reqId?: string): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* 主动推送文本(用于启动/关闭通知等,无需 req_id)
|
|
24
|
+
*/
|
|
25
|
+
export declare function sendProactiveTextReply(chatId: string, text: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Send simple text reply to WeWork
|
|
28
|
+
* @param threadCtxOrReqId - 兼容 MessageSender 的 threadCtx;若为 string 则作为 reqId 使用
|
|
29
|
+
*/
|
|
30
|
+
export declare function sendTextReply(chatId: string, text: string, threadCtxOrReqId?: import('../shared/types.js').ThreadContext | string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Send permission card with action buttons (for permission prompts)
|
|
33
|
+
* Note: WeWork doesn't support interactive cards, so we send text with instructions
|
|
34
|
+
*/
|
|
35
|
+
export declare function sendPermissionCard(chatId: string, requestId: string, toolName: string, toolInput: string, reqId?: string): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Send mode switch card
|
|
38
|
+
*/
|
|
39
|
+
export declare function sendModeCard(chatId: string, _userId: string, currentMode: string, reqId?: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Send image reply
|
|
42
|
+
* Note: WeWork requires media_id for image messages
|
|
43
|
+
*/
|
|
44
|
+
export declare function sendImageReply(chatId: string, imagePath: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Send directory selection (not supported in WeWork, use text instead)
|
|
47
|
+
*/
|
|
48
|
+
export declare function sendDirectorySelection(chatId: string, currentDir: string, _userId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Start typing indicator (WeWork doesn't support this)
|
|
51
|
+
*/
|
|
52
|
+
export declare function startTypingLoop(_chatId: string): () => void;
|
|
53
|
+
/**
|
|
54
|
+
* Send error message
|
|
55
|
+
*/
|
|
56
|
+
export declare function sendErrorMessage(chatId: string, error: string, reqId?: string): Promise<void>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeWork (企业微信) Message Sender - Send messages to WeWork
|
|
3
|
+
* 通过 WebSocket aibot_respond_msg 发送,需透传 req_id
|
|
4
|
+
*/
|
|
5
|
+
import { sendText, sendStream, sendProactiveMessage } from './client.js';
|
|
6
|
+
import { createLogger } from '../logger.js';
|
|
7
|
+
import { splitLongContent } from '../shared/utils.js';
|
|
8
|
+
import { MAX_WEWORK_MESSAGE_LENGTH } from '../constants.js';
|
|
9
|
+
import { randomBytes } from 'node:crypto';
|
|
10
|
+
const log = createLogger('WeWorkSender');
|
|
11
|
+
/** 当前同步处理中的 req_id(仅用于 commandHandler 等同步调用) */
|
|
12
|
+
let currentReqId = null;
|
|
13
|
+
export function setCurrentReqId(reqId) {
|
|
14
|
+
currentReqId = reqId;
|
|
15
|
+
}
|
|
16
|
+
function getReqId(explicitReqId) {
|
|
17
|
+
const id = explicitReqId ?? currentReqId;
|
|
18
|
+
if (!id) {
|
|
19
|
+
log.warn('No req_id - cannot send WeWork reply');
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
const STATUS_CONFIG = {
|
|
25
|
+
thinking: { icon: '🔵', title: '思考中' },
|
|
26
|
+
streaming: { icon: '🔄', title: '执行中' },
|
|
27
|
+
done: { icon: '✅', title: '完成' },
|
|
28
|
+
error: { icon: '❌', title: '错误' },
|
|
29
|
+
};
|
|
30
|
+
const TOOL_DISPLAY_NAMES = {
|
|
31
|
+
claude: 'Claude Code',
|
|
32
|
+
codex: 'Codex',
|
|
33
|
+
cursor: 'Cursor',
|
|
34
|
+
};
|
|
35
|
+
function getToolTitle(toolId, status) {
|
|
36
|
+
const name = TOOL_DISPLAY_NAMES[toolId] ?? toolId;
|
|
37
|
+
const statusText = STATUS_CONFIG[status].title;
|
|
38
|
+
return status === 'done' ? name : `${name} - ${statusText}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Generate unique request ID
|
|
42
|
+
*/
|
|
43
|
+
function generateReqId() {
|
|
44
|
+
return `${Date.now()}-${randomBytes(8).toString('hex')}`;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate unique stream ID for WeWork streaming responses
|
|
48
|
+
*/
|
|
49
|
+
function generateStreamId() {
|
|
50
|
+
return `${Date.now()}-${randomBytes(8).toString('hex')}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format message for WeWork (markdown-like format)
|
|
54
|
+
*/
|
|
55
|
+
function formatWeWorkMessage(title, content, status, note) {
|
|
56
|
+
const statusConfig = STATUS_CONFIG[status];
|
|
57
|
+
let message = `${statusConfig.icon} **${title}**\n\n`;
|
|
58
|
+
if (content) {
|
|
59
|
+
message += `${content}\n\n`;
|
|
60
|
+
}
|
|
61
|
+
else if (status === 'thinking') {
|
|
62
|
+
message += `_正在思考,请稍候..._\n\n💭 **准备中**\n\n`;
|
|
63
|
+
}
|
|
64
|
+
if (note) {
|
|
65
|
+
message += `---\n\n💡 **${note}**`;
|
|
66
|
+
}
|
|
67
|
+
return message;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Local tracking for stream states
|
|
71
|
+
* WeWork doesn't support message editing, so we track stream IDs locally
|
|
72
|
+
*/
|
|
73
|
+
const streamStates = new Map();
|
|
74
|
+
/**
|
|
75
|
+
* Send thinking message to WeWork
|
|
76
|
+
* Returns a stream ID that can be used for updates
|
|
77
|
+
* @param reqId - 消息回调的 req_id,用于 WebSocket 回复
|
|
78
|
+
*/
|
|
79
|
+
export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'claude', reqId) {
|
|
80
|
+
const streamId = generateStreamId();
|
|
81
|
+
const title = getToolTitle(toolId, 'thinking');
|
|
82
|
+
const content = formatWeWorkMessage(title, '', 'thinking');
|
|
83
|
+
try {
|
|
84
|
+
log.info(`Sending thinking message to user ${chatId}, streamId=${streamId}`);
|
|
85
|
+
// Store initial stream state
|
|
86
|
+
streamStates.set(streamId, { content: '', chatId });
|
|
87
|
+
// Send initial stream message (not finished)
|
|
88
|
+
sendStream(getReqId(reqId), streamId, content, false);
|
|
89
|
+
log.info(`Thinking message sent: ${streamId}`);
|
|
90
|
+
return streamId;
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
log.error('Failed to send thinking message:', err);
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Update existing message in WeWork
|
|
99
|
+
* Note: WeWork doesn't support message editing, so we send new stream messages
|
|
100
|
+
*/
|
|
101
|
+
export async function updateMessage(chatId, streamId, content, status, note, toolId = 'claude', reqId) {
|
|
102
|
+
const title = getToolTitle(toolId, status);
|
|
103
|
+
const message = formatWeWorkMessage(title, content, status, note);
|
|
104
|
+
try {
|
|
105
|
+
// Update stream state
|
|
106
|
+
streamStates.set(streamId, { content, chatId });
|
|
107
|
+
// Send stream update (not finished yet)
|
|
108
|
+
sendStream(getReqId(reqId), streamId, message, false);
|
|
109
|
+
log.info(`Message updated: ${status}, streamId=${streamId}`);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
log.error('Failed to update message:', err);
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Send final messages to WeWork (handle long content)
|
|
118
|
+
*/
|
|
119
|
+
export async function sendFinalMessages(chatId, streamId, fullContent, note, toolId = 'claude', reqId) {
|
|
120
|
+
const title = getToolTitle(toolId, 'done');
|
|
121
|
+
const parts = splitLongContent(fullContent, MAX_WEWORK_MESSAGE_LENGTH);
|
|
122
|
+
// Send final stream message to finish the stream
|
|
123
|
+
const finalMessage = formatWeWorkMessage(title, parts[0], 'done', parts.length > 1 ? `内容较长,已分段发送 (1/${parts.length})` : note);
|
|
124
|
+
try {
|
|
125
|
+
sendStream(getReqId(reqId), streamId, finalMessage, true);
|
|
126
|
+
log.info(`Final stream message sent, streamId=${streamId}`);
|
|
127
|
+
// Clean up stream state
|
|
128
|
+
streamStates.delete(streamId);
|
|
129
|
+
// Send remaining parts as separate messages
|
|
130
|
+
for (let i = 1; i < parts.length; i++) {
|
|
131
|
+
try {
|
|
132
|
+
const partContent = `${parts[i]}\n\n_*(续 ${i + 1}/${parts.length})*_`;
|
|
133
|
+
const partMessage = formatWeWorkMessage(title, partContent, 'done', i === parts.length - 1 ? note : undefined);
|
|
134
|
+
sendText(getReqId(reqId), partMessage);
|
|
135
|
+
log.info(`Final message part ${i + 1}/${parts.length} sent`);
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
log.error(`Failed to send part ${i + 1}:`, err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
log.error('Failed to send final messages:', err);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 主动推送文本(用于启动/关闭通知等,无需 req_id)
|
|
148
|
+
*/
|
|
149
|
+
export async function sendProactiveTextReply(chatId, text) {
|
|
150
|
+
const message = formatWeWorkMessage('📢 open-im', text, 'done');
|
|
151
|
+
try {
|
|
152
|
+
sendProactiveMessage(chatId, message);
|
|
153
|
+
log.info(`Proactive text sent to user ${chatId}`);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
log.error('Failed to send proactive text:', err);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Send simple text reply to WeWork
|
|
161
|
+
* @param threadCtxOrReqId - 兼容 MessageSender 的 threadCtx;若为 string 则作为 reqId 使用
|
|
162
|
+
*/
|
|
163
|
+
export async function sendTextReply(chatId, text, threadCtxOrReqId) {
|
|
164
|
+
const message = formatWeWorkMessage('📢 open-im', text, 'done');
|
|
165
|
+
const reqId = typeof threadCtxOrReqId === 'string' ? threadCtxOrReqId : undefined;
|
|
166
|
+
try {
|
|
167
|
+
sendText(getReqId(reqId), message);
|
|
168
|
+
log.info(`Text reply sent to user ${chatId}`);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
log.error('Failed to send text reply:', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Send permission card with action buttons (for permission prompts)
|
|
176
|
+
* Note: WeWork doesn't support interactive cards, so we send text with instructions
|
|
177
|
+
*/
|
|
178
|
+
export async function sendPermissionCard(chatId, requestId, toolName, toolInput, reqId) {
|
|
179
|
+
const message = `🔐 **权限请求**
|
|
180
|
+
|
|
181
|
+
**工具:** \`${toolName}\`
|
|
182
|
+
|
|
183
|
+
**参数:**
|
|
184
|
+
\`\`\`
|
|
185
|
+
${toolInput.length > 300 ? toolInput.slice(0, 300) + '...' : toolInput}
|
|
186
|
+
\`\`\`
|
|
187
|
+
|
|
188
|
+
请回复以下命令进行操作:
|
|
189
|
+
• \`/allow\` - 允许
|
|
190
|
+
• \`/deny\` - 拒绝
|
|
191
|
+
|
|
192
|
+
**请求 ID:** \`${requestId.slice(-8)}\``;
|
|
193
|
+
try {
|
|
194
|
+
sendText(getReqId(reqId), message);
|
|
195
|
+
log.info(`Permission card sent to user ${chatId}`);
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
log.error('Failed to send permission card:', err);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Send mode switch card
|
|
203
|
+
*/
|
|
204
|
+
export async function sendModeCard(chatId, _userId, currentMode, reqId) {
|
|
205
|
+
const { MODE_LABELS } = await import('../permission-mode/types.js');
|
|
206
|
+
const message = `🔐 **权限模式**
|
|
207
|
+
|
|
208
|
+
**当前模式:** \`${MODE_LABELS[currentMode] || currentMode}\`
|
|
209
|
+
|
|
210
|
+
发送命令切换模式:
|
|
211
|
+
• \`/mode ask\` - 每次询问
|
|
212
|
+
• \`/mode accept-edits\` - 自动批准编辑
|
|
213
|
+
• \`/mode plan\` - 仅分析
|
|
214
|
+
• \`/mode yolo\` - 跳过所有权限`;
|
|
215
|
+
try {
|
|
216
|
+
sendText(getReqId(reqId), message);
|
|
217
|
+
log.info(`Mode card sent to user ${chatId}`);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
log.error('Failed to send mode card:', err);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Send image reply
|
|
225
|
+
* Note: WeWork requires media_id for image messages
|
|
226
|
+
*/
|
|
227
|
+
export async function sendImageReply(chatId, imagePath) {
|
|
228
|
+
// For now, send text with image path
|
|
229
|
+
// TODO: Implement media upload and send with media_id
|
|
230
|
+
await sendTextReply(chatId, `图片已保存: ${imagePath}`);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Send directory selection (not supported in WeWork, use text instead)
|
|
234
|
+
*/
|
|
235
|
+
export async function sendDirectorySelection(chatId, currentDir, _userId) {
|
|
236
|
+
await sendTextReply(chatId, `📁 当前目录: \`${currentDir}\`\n\n请使用 \`/cd <目录>\` 命令切换目录`);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Start typing indicator (WeWork doesn't support this)
|
|
240
|
+
*/
|
|
241
|
+
export function startTypingLoop(_chatId) {
|
|
242
|
+
// WeWork doesn't have a typing indicator like Telegram
|
|
243
|
+
// Return a no-op function
|
|
244
|
+
return () => { };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Send error message
|
|
248
|
+
*/
|
|
249
|
+
export async function sendErrorMessage(chatId, error, reqId) {
|
|
250
|
+
const message = formatWeWorkMessage('❌ 错误', error, 'error');
|
|
251
|
+
try {
|
|
252
|
+
sendText(getReqId(reqId), message);
|
|
253
|
+
log.info(`Error message sent to user ${chatId}`);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
log.error('Failed to send error message:', err);
|
|
257
|
+
}
|
|
258
|
+
}
|