@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.
@@ -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
+ }