evolclaw 2.2.0 → 2.4.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 +49 -27
- package/data/evolclaw.sample.json +6 -3
- package/dist/agents/claude-runner.js +125 -52
- package/dist/agents/codex-runner.js +10 -5
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +283 -95
- package/dist/channels/feishu.js +556 -96
- package/dist/channels/wechat.js +98 -74
- package/dist/cli.js +232 -57
- package/dist/config.js +185 -31
- package/dist/core/channel-loader.js +11 -4
- package/dist/core/command-handler.js +803 -247
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +217 -0
- package/dist/core/{message-processor.js → message/message-processor.js} +411 -124
- package/dist/core/{message-queue.js → message/message-queue.js} +1 -1
- package/dist/{utils → core/message}/stream-debouncer.js +1 -1
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/core/permission.js +212 -11
- package/dist/core/{adapters → session/adapters}/claude-session-file-adapter.js +2 -2
- package/dist/core/{adapters → session/adapters}/codex-session-file-adapter.js +117 -52
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/{utils → core/session}/session-file-health.js +1 -1
- package/dist/core/{session-manager.js → session/session-manager.js} +61 -11
- package/dist/index.js +140 -57
- package/dist/{core/ipc-server.js → ipc.js} +36 -1
- package/dist/types.js +3 -0
- package/dist/utils/cross-platform.js +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +55 -150
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/{core → utils}/stats-collector.js +16 -0
- package/package.json +3 -3
- package/dist/core/message-bridge.js +0 -187
- package/dist/utils/init-feishu.js +0 -263
- package/dist/utils/init-wechat.js +0 -172
- package/dist/utils/ipc-client.js +0 -36
- package/dist/utils/permission-utils.js +0 -71
- /package/dist/{utils → core/message}/message-cache.js +0 -0
- /package/dist/{utils → core/message}/stream-idle-monitor.js +0 -0
- /package/dist/core/{session-file-adapter.js → session/session-file-adapter.js} +0 -0
package/dist/channels/feishu.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as lark from '@larksuiteoapi/node-sdk';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import imageType from 'image-type';
|
|
5
|
-
import {
|
|
5
|
+
import { sanitizeFileName, saveToUploads, validateImage } from '../utils/media-cache.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
7
|
import { hasRichContent, renderAllRichContent, checkDependencies } from '../utils/rich-content-renderer.js';
|
|
8
8
|
export class FeishuChannel {
|
|
@@ -16,11 +16,12 @@ export class FeishuChannel {
|
|
|
16
16
|
seenThreads = new Set(); // 已见的 thread_id,用于判断话题创建消息
|
|
17
17
|
userNameCache = new Map(); // userId -> userName
|
|
18
18
|
recallHandler;
|
|
19
|
+
interactionCallback;
|
|
19
20
|
connected = false;
|
|
20
21
|
enableRichContent;
|
|
21
22
|
constructor(config) {
|
|
22
23
|
this.config = config;
|
|
23
|
-
this.enableRichContent = config.enableRichContent ??
|
|
24
|
+
this.enableRichContent = config.enableRichContent ?? false; // 默认关闭
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* 预填充已知的 thread_id(重启后从数据库恢复,避免误判话题创建)
|
|
@@ -112,7 +113,7 @@ export class FeishuChannel {
|
|
|
112
113
|
const quotedContent = res.data.items[0].body.content;
|
|
113
114
|
if (quotedMsgType === 'text') {
|
|
114
115
|
const parsed = JSON.parse(quotedContent);
|
|
115
|
-
quotedText = `> ${parsed.text}\n\n`;
|
|
116
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> ${parsed.text}\n> ================\n\n`;
|
|
116
117
|
}
|
|
117
118
|
else if (quotedMsgType === 'post') {
|
|
118
119
|
const parsed = JSON.parse(quotedContent);
|
|
@@ -128,7 +129,7 @@ export class FeishuChannel {
|
|
|
128
129
|
text += '\n';
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
|
-
quotedText = `> ${text.trim()}\n\n`;
|
|
132
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> ${text.trim()}\n> ================\n\n`;
|
|
132
133
|
}
|
|
133
134
|
else if (quotedMsgType === 'image') {
|
|
134
135
|
const parsed = JSON.parse(quotedContent);
|
|
@@ -139,10 +140,10 @@ export class FeishuChannel {
|
|
|
139
140
|
const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.parent_id, projectPath);
|
|
140
141
|
if (imageData) {
|
|
141
142
|
quotedImages.push(imageData);
|
|
142
|
-
quotedText = `> [引用的图片]\n\n`;
|
|
143
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [引用的图片]\n> ================\n\n`;
|
|
143
144
|
}
|
|
144
145
|
else {
|
|
145
|
-
quotedText = `> [图片消息]\n\n`;
|
|
146
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [图片消息]\n> ================\n\n`;
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
149
|
else if (quotedMsgType === 'file') {
|
|
@@ -154,14 +155,14 @@ export class FeishuChannel {
|
|
|
154
155
|
: process.cwd();
|
|
155
156
|
const quotedFilePath = await this.downloadFile(quotedFileKey, quotedFileName, msg.parent_id, projectPath);
|
|
156
157
|
if (quotedFilePath) {
|
|
157
|
-
quotedText = `> [引用的文件:${quotedFileName}]\n> 文件已保存到:${quotedFilePath}\n\n`;
|
|
158
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [引用的文件:${quotedFileName}]\n> 文件已保存到:${quotedFilePath}\n> ================\n\n`;
|
|
158
159
|
}
|
|
159
160
|
else {
|
|
160
|
-
quotedText = `> [文件消息]\n\n`;
|
|
161
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [文件消息]\n> ================\n\n`;
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
else {
|
|
164
|
-
quotedText = `> [${quotedMsgType}消息]\n\n`;
|
|
165
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [${quotedMsgType}消息]\n> ================\n\n`;
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
168
|
catch (err) {
|
|
@@ -267,7 +268,46 @@ export class FeishuChannel {
|
|
|
267
268
|
}
|
|
268
269
|
},
|
|
269
270
|
'im.message.message_read_v1': async () => { },
|
|
270
|
-
'im.message.reaction.created_v1': async () => { }
|
|
271
|
+
'im.message.reaction.created_v1': async () => { },
|
|
272
|
+
'card.action.trigger': async (data) => {
|
|
273
|
+
try {
|
|
274
|
+
const action = data?.action;
|
|
275
|
+
if (!action?.value)
|
|
276
|
+
return;
|
|
277
|
+
const value = action.value;
|
|
278
|
+
const requestId = value._request_id;
|
|
279
|
+
if (!requestId) {
|
|
280
|
+
logger.debug('[Feishu] Card action without _request_id, ignoring');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Legacy field change (non-form select_static with _field_key): ignore silently
|
|
284
|
+
if (value._field_key) {
|
|
285
|
+
logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Form submit: `action.form_value` contains all field values from form container
|
|
289
|
+
const formValues = action.form_value || {};
|
|
290
|
+
const response = {
|
|
291
|
+
type: 'interaction.response',
|
|
292
|
+
id: requestId,
|
|
293
|
+
action: value._action || 'submit',
|
|
294
|
+
values: { ...formValues, ...value },
|
|
295
|
+
operatorId: data.operator?.open_id,
|
|
296
|
+
};
|
|
297
|
+
// Remove internal fields from values
|
|
298
|
+
delete response.values._request_id;
|
|
299
|
+
delete response.values._action;
|
|
300
|
+
delete response.values._card_title;
|
|
301
|
+
logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
|
|
302
|
+
this.interactionCallback?.(response);
|
|
303
|
+
// Return updated card (buttons disabled + result shown)
|
|
304
|
+
const cardTitle = value._card_title || '操作';
|
|
305
|
+
return this.buildResolvedCard(cardTitle, response);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
logger.error('[Feishu] Failed to handle card action:', err);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
271
311
|
});
|
|
272
312
|
this.wsClient = new lark.WSClient({
|
|
273
313
|
appId: this.config.appId,
|
|
@@ -290,6 +330,9 @@ export class FeishuChannel {
|
|
|
290
330
|
onRecall(handler) {
|
|
291
331
|
this.recallHandler = handler;
|
|
292
332
|
}
|
|
333
|
+
onInteraction(callback) {
|
|
334
|
+
this.interactionCallback = callback;
|
|
335
|
+
}
|
|
293
336
|
onProjectPathRequest(provider) {
|
|
294
337
|
this.projectPathProvider = provider;
|
|
295
338
|
}
|
|
@@ -323,6 +366,19 @@ export class FeishuChannel {
|
|
|
323
366
|
logger.warn('[Feishu] Attempted to send empty message, skipping');
|
|
324
367
|
return;
|
|
325
368
|
}
|
|
369
|
+
// 飞书消息内容限制约 30KB(text)/ 150KB(post),安全阈值 28000 字符
|
|
370
|
+
// 超长消息自动拆分,按段落边界分割
|
|
371
|
+
const MAX_CONTENT_LENGTH = 28000;
|
|
372
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
373
|
+
logger.info(`[Feishu] Message too long (${content.length} chars), splitting into parts`);
|
|
374
|
+
const parts = splitLongMessage(content, MAX_CONTENT_LENGTH);
|
|
375
|
+
for (let i = 0; i < parts.length; i++) {
|
|
376
|
+
// 首条消息保留 reply 选项,后续消息不再 reply
|
|
377
|
+
const partOptions = i === 0 ? options : { ...options, replyToMessageId: undefined };
|
|
378
|
+
await this.sendMessage(chatId, parts[i], partOptions);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
326
382
|
logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
|
|
327
383
|
try {
|
|
328
384
|
// 检测富内容并渲染(受 enableRichContent 开关控制,且依赖必须可用)
|
|
@@ -348,7 +404,7 @@ export class FeishuChannel {
|
|
|
348
404
|
const useMarkdown = !options?.forceText && hasMarkdownSyntax(content);
|
|
349
405
|
const hasMention = !!(options?.mentionUserIds && options.mentionUserIds.length > 0);
|
|
350
406
|
const hasRichImages = richItemsWithKeys.length > 0;
|
|
351
|
-
//
|
|
407
|
+
// 消息类型决策:有 Markdown / @ / 富内容图片 → post,否则 text
|
|
352
408
|
const msgType = (useMarkdown || hasMention || hasRichImages) ? 'post' : 'text';
|
|
353
409
|
let msgContent;
|
|
354
410
|
if (msgType === 'post') {
|
|
@@ -406,7 +462,7 @@ export class FeishuChannel {
|
|
|
406
462
|
}
|
|
407
463
|
else {
|
|
408
464
|
await this.client.im.message.create({
|
|
409
|
-
params: { receive_id_type: 'chat_id' },
|
|
465
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
410
466
|
data: { receive_id: chatId, msg_type: msgType, content: msgContent }
|
|
411
467
|
});
|
|
412
468
|
}
|
|
@@ -423,6 +479,12 @@ export class FeishuChannel {
|
|
|
423
479
|
logger.warn('[Feishu] Message withdrawn (230011), retrying without reply');
|
|
424
480
|
return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
|
|
425
481
|
}
|
|
482
|
+
// 230025: 消息内容超长,截断后重试
|
|
483
|
+
if (error.response?.data?.code === 230025) {
|
|
484
|
+
logger.warn(`[Feishu] Message too long (230025, ${content.length} chars), truncating`);
|
|
485
|
+
const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
|
|
486
|
+
return this.sendMessage(chatId, truncated, options);
|
|
487
|
+
}
|
|
426
488
|
logger.error('[Feishu] Failed to send message:', error);
|
|
427
489
|
throw error;
|
|
428
490
|
}
|
|
@@ -471,7 +533,7 @@ export class FeishuChannel {
|
|
|
471
533
|
}
|
|
472
534
|
else {
|
|
473
535
|
await this.client.im.message.create({
|
|
474
|
-
params: { receive_id_type: 'chat_id' },
|
|
536
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
475
537
|
data: {
|
|
476
538
|
receive_id: chatId,
|
|
477
539
|
msg_type: 'file',
|
|
@@ -519,7 +581,7 @@ export class FeishuChannel {
|
|
|
519
581
|
}
|
|
520
582
|
else {
|
|
521
583
|
await this.client.im.message.create({
|
|
522
|
-
params: { receive_id_type: 'chat_id' },
|
|
584
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
523
585
|
data: { receive_id: chatId, msg_type: 'image', content: msgContent }
|
|
524
586
|
});
|
|
525
587
|
}
|
|
@@ -609,28 +671,17 @@ export class FeishuChannel {
|
|
|
609
671
|
logger.warn('[Feishu] Empty response from image download');
|
|
610
672
|
return null;
|
|
611
673
|
}
|
|
612
|
-
//
|
|
613
|
-
const
|
|
614
|
-
if (
|
|
615
|
-
logger.warn(
|
|
616
|
-
return null;
|
|
617
|
-
}
|
|
618
|
-
// 白名单验证:只允许常见的图片格式
|
|
619
|
-
const allowedMimes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
620
|
-
if (!allowedMimes.includes(type.mime)) {
|
|
621
|
-
logger.warn('[Feishu] Unsupported image type:', type.mime);
|
|
622
|
-
return null;
|
|
623
|
-
}
|
|
624
|
-
// 大小限制:10MB
|
|
625
|
-
if (buffer.length > 10 * 1024 * 1024) {
|
|
626
|
-
logger.warn('[Feishu] Image too large:', buffer.length, 'bytes');
|
|
674
|
+
// 统一图片验证(类型白名单 + 大小限制)
|
|
675
|
+
const result = await validateImage(buffer);
|
|
676
|
+
if (result.mime === null) {
|
|
677
|
+
logger.warn(`[Feishu] Image validation failed: ${result.reason}`);
|
|
627
678
|
return null;
|
|
628
679
|
}
|
|
629
680
|
const base64Data = buffer.toString('base64');
|
|
630
|
-
logger.debug('[Feishu] Image downloaded successfully, type:',
|
|
681
|
+
logger.debug('[Feishu] Image downloaded successfully, type:', result.mime, 'size:', base64Data.length);
|
|
631
682
|
return {
|
|
632
683
|
data: base64Data,
|
|
633
|
-
mimeType:
|
|
684
|
+
mimeType: result.mime
|
|
634
685
|
};
|
|
635
686
|
}
|
|
636
687
|
logger.error('[Feishu] Image download failed: no valid method');
|
|
@@ -671,11 +722,7 @@ export class FeishuChannel {
|
|
|
671
722
|
logger.warn('[Feishu] Empty response from file download');
|
|
672
723
|
return null;
|
|
673
724
|
}
|
|
674
|
-
const
|
|
675
|
-
ensureDir(uploadsDir);
|
|
676
|
-
const filePath = path.join(uploadsDir, fileName);
|
|
677
|
-
fs.writeFileSync(filePath, buffer);
|
|
678
|
-
logger.info('[Feishu] File downloaded successfully:', filePath, 'size:', buffer.length);
|
|
725
|
+
const { filePath } = saveToUploads(buffer, sanitizeFileName(fileName), projectPath);
|
|
679
726
|
return filePath;
|
|
680
727
|
}
|
|
681
728
|
logger.error('[Feishu] File download failed: no valid method');
|
|
@@ -686,6 +733,102 @@ export class FeishuChannel {
|
|
|
686
733
|
return null;
|
|
687
734
|
}
|
|
688
735
|
}
|
|
736
|
+
async sendInteraction(chatId, interaction, options) {
|
|
737
|
+
if (!this.client)
|
|
738
|
+
return false;
|
|
739
|
+
const card = buildInteractionCard(interaction);
|
|
740
|
+
if (!card)
|
|
741
|
+
return false;
|
|
742
|
+
try {
|
|
743
|
+
let messageId;
|
|
744
|
+
if (options?.replyToMessageId) {
|
|
745
|
+
const replyData = {
|
|
746
|
+
msg_type: 'interactive',
|
|
747
|
+
content: JSON.stringify(card),
|
|
748
|
+
};
|
|
749
|
+
if (options.replyInThread)
|
|
750
|
+
replyData.reply_in_thread = true;
|
|
751
|
+
const res = await this.client.im.message.reply({
|
|
752
|
+
path: { message_id: options.replyToMessageId },
|
|
753
|
+
data: replyData,
|
|
754
|
+
});
|
|
755
|
+
messageId = res?.data?.message_id;
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
const res = await this.client.im.message.create({
|
|
759
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
760
|
+
data: {
|
|
761
|
+
receive_id: chatId,
|
|
762
|
+
msg_type: 'interactive',
|
|
763
|
+
content: JSON.stringify(card),
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
messageId = res?.data?.message_id;
|
|
767
|
+
}
|
|
768
|
+
logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
|
|
769
|
+
return messageId || false;
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
const detail = error?.response?.data || error?.message || error;
|
|
773
|
+
logger.error(`[Feishu] Failed to send interaction card (id=${interaction.id}, replyTo=${options?.replyToMessageId || 'none'}):`, detail);
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
async patchInteractionCard(messageId, card) {
|
|
778
|
+
if (!this.client)
|
|
779
|
+
return;
|
|
780
|
+
try {
|
|
781
|
+
await this.client.im.message.patch({
|
|
782
|
+
path: { message_id: messageId },
|
|
783
|
+
data: { content: JSON.stringify(card) },
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
logger.warn(`[Feishu] Failed to patch card ${messageId}:`, error?.response?.data || error?.message);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
buildResolvedCard(cardTitle, response) {
|
|
791
|
+
const action = response.action;
|
|
792
|
+
const labelMap = {
|
|
793
|
+
'allow': '✅ 已允许',
|
|
794
|
+
'always': '🔓 已设为始终允许',
|
|
795
|
+
'deny': '❌ 已拒绝',
|
|
796
|
+
'cancel': '取消',
|
|
797
|
+
'submit': '✅ 已提交',
|
|
798
|
+
};
|
|
799
|
+
const statusText = labelMap[action] || `✅ ${action}`;
|
|
800
|
+
const now = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
801
|
+
// Build summary of selected values
|
|
802
|
+
const elements = [];
|
|
803
|
+
if (response.values && action === 'submit') {
|
|
804
|
+
const entries = Object.entries(response.values).filter(([k]) => !k.startsWith('_'));
|
|
805
|
+
if (entries.length > 0) {
|
|
806
|
+
const lines = entries.map(([k, v]) => {
|
|
807
|
+
const display = Array.isArray(v) ? v.join(', ') : String(v);
|
|
808
|
+
return `**${k}**: ${display}`;
|
|
809
|
+
});
|
|
810
|
+
elements.push({ tag: 'markdown', content: lines.join('\n') });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
elements.push({ tag: 'markdown', content: `操作时间:${now}` });
|
|
814
|
+
return {
|
|
815
|
+
toast: {
|
|
816
|
+
type: 'success',
|
|
817
|
+
content: statusText,
|
|
818
|
+
},
|
|
819
|
+
card: {
|
|
820
|
+
type: 'raw',
|
|
821
|
+
data: {
|
|
822
|
+
config: { wide_screen_mode: true },
|
|
823
|
+
header: {
|
|
824
|
+
template: action === 'deny' ? 'red' : 'green',
|
|
825
|
+
title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
|
|
826
|
+
},
|
|
827
|
+
elements,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
};
|
|
831
|
+
}
|
|
689
832
|
addAckReaction(messageId) {
|
|
690
833
|
if (!this.client)
|
|
691
834
|
return;
|
|
@@ -697,6 +840,260 @@ export class FeishuChannel {
|
|
|
697
840
|
}).catch(() => { });
|
|
698
841
|
}
|
|
699
842
|
}
|
|
843
|
+
// ── 交互卡片构建工具 ──
|
|
844
|
+
export function buildInteractionCard(interaction) {
|
|
845
|
+
const { kind } = interaction;
|
|
846
|
+
if (kind.kind === 'action') {
|
|
847
|
+
return buildActionCard(interaction.id, kind);
|
|
848
|
+
}
|
|
849
|
+
if (kind.kind === 'form') {
|
|
850
|
+
return buildFormCard(interaction.id, kind);
|
|
851
|
+
}
|
|
852
|
+
// menu kind: not rendered as card (handled via menu.response JSON)
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
export function buildActionCard(requestId, action) {
|
|
856
|
+
const elements = [];
|
|
857
|
+
// Body text
|
|
858
|
+
if (action.body) {
|
|
859
|
+
elements.push({ tag: 'markdown', content: action.body });
|
|
860
|
+
}
|
|
861
|
+
// Buttons row
|
|
862
|
+
const buttons = action.buttons.map(btn => {
|
|
863
|
+
const buttonEl = {
|
|
864
|
+
tag: 'button',
|
|
865
|
+
text: { tag: 'plain_text', content: btn.label },
|
|
866
|
+
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
867
|
+
value: {
|
|
868
|
+
_request_id: requestId,
|
|
869
|
+
_action: btn.key,
|
|
870
|
+
_card_title: action.title,
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
if (btn.confirm) {
|
|
874
|
+
buttonEl.confirm = {
|
|
875
|
+
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
876
|
+
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return buttonEl;
|
|
880
|
+
});
|
|
881
|
+
elements.push({
|
|
882
|
+
tag: 'action',
|
|
883
|
+
actions: buttons,
|
|
884
|
+
});
|
|
885
|
+
return {
|
|
886
|
+
config: { wide_screen_mode: true },
|
|
887
|
+
header: {
|
|
888
|
+
template: 'blue',
|
|
889
|
+
title: { tag: 'plain_text', content: action.title },
|
|
890
|
+
},
|
|
891
|
+
elements,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
export function buildFormCard(requestId, form) {
|
|
895
|
+
// Use Feishu card v2 form container: all fields wrapped in a `form` tag.
|
|
896
|
+
// On submit, callback receives `action.form_value` with all field values keyed by `name`.
|
|
897
|
+
// This eliminates per-field callbacks when selecting dropdown options.
|
|
898
|
+
const formElements = [];
|
|
899
|
+
// Body text
|
|
900
|
+
if (form.body) {
|
|
901
|
+
formElements.push({ tag: 'markdown', content: form.body });
|
|
902
|
+
}
|
|
903
|
+
// Fields — inside form, components use `name` (not `value`) for identification
|
|
904
|
+
for (const field of form.fields) {
|
|
905
|
+
formElements.push(buildFormFieldElement(field));
|
|
906
|
+
if (field.hint) {
|
|
907
|
+
formElements.push({
|
|
908
|
+
tag: 'note',
|
|
909
|
+
elements: [{ tag: 'plain_text', content: field.hint }],
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// Submit button (inside form, uses form_action_type)
|
|
914
|
+
const submitBtn = {
|
|
915
|
+
tag: 'button',
|
|
916
|
+
text: { tag: 'plain_text', content: form.submitLabel || '确认' },
|
|
917
|
+
type: form.submitStyle === 'danger' ? 'danger' : 'primary',
|
|
918
|
+
form_action_type: 'submit',
|
|
919
|
+
name: 'submit',
|
|
920
|
+
value: {
|
|
921
|
+
_request_id: requestId,
|
|
922
|
+
_action: 'submit',
|
|
923
|
+
_card_title: form.title,
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
if (form.submitConfirm) {
|
|
927
|
+
submitBtn.confirm = {
|
|
928
|
+
title: { tag: 'plain_text', content: form.submitConfirm.title },
|
|
929
|
+
text: { tag: 'plain_text', content: form.submitConfirm.body },
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const actions = [submitBtn];
|
|
933
|
+
if (form.cancelable !== false) {
|
|
934
|
+
actions.push({
|
|
935
|
+
tag: 'button',
|
|
936
|
+
text: { tag: 'plain_text', content: '取消' },
|
|
937
|
+
type: 'default',
|
|
938
|
+
// Cancel is NOT form_action_type — it's a regular button that triggers callback directly
|
|
939
|
+
value: {
|
|
940
|
+
_request_id: requestId,
|
|
941
|
+
_action: 'cancel',
|
|
942
|
+
_card_title: form.title,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
formElements.push({ tag: 'action', actions });
|
|
947
|
+
return {
|
|
948
|
+
schema: '2.0',
|
|
949
|
+
config: { update_multi: true },
|
|
950
|
+
header: {
|
|
951
|
+
template: 'blue',
|
|
952
|
+
title: { tag: 'plain_text', content: form.title },
|
|
953
|
+
},
|
|
954
|
+
body: {
|
|
955
|
+
elements: [
|
|
956
|
+
{
|
|
957
|
+
tag: 'form',
|
|
958
|
+
name: requestId,
|
|
959
|
+
elements: formElements,
|
|
960
|
+
},
|
|
961
|
+
],
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/** Build a field element for use inside a form container (uses `name` for identification) */
|
|
966
|
+
export function buildFormFieldElement(field) {
|
|
967
|
+
switch (field.type) {
|
|
968
|
+
case 'select': {
|
|
969
|
+
const options = field.options.map(opt => ({
|
|
970
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
971
|
+
value: opt.value,
|
|
972
|
+
}));
|
|
973
|
+
const selectedOpt = field.options.find(opt => opt.selected);
|
|
974
|
+
return {
|
|
975
|
+
tag: 'select_static',
|
|
976
|
+
name: field.key,
|
|
977
|
+
placeholder: { tag: 'plain_text', content: field.placeholder || `选择${field.label}` },
|
|
978
|
+
options,
|
|
979
|
+
...(selectedOpt ? { initial_option: selectedOpt.value } : {}),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
case 'text': {
|
|
983
|
+
return {
|
|
984
|
+
tag: 'input',
|
|
985
|
+
name: field.key,
|
|
986
|
+
placeholder: { tag: 'plain_text', content: field.placeholder || `输入${field.label}` },
|
|
987
|
+
...(field.defaultValue != null ? { default_value: String(field.defaultValue) } : {}),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
case 'toggle': {
|
|
991
|
+
const checked = field.defaultValue ?? false;
|
|
992
|
+
return {
|
|
993
|
+
tag: 'select_static',
|
|
994
|
+
name: field.key,
|
|
995
|
+
placeholder: { tag: 'plain_text', content: field.label },
|
|
996
|
+
options: [
|
|
997
|
+
{ text: { tag: 'plain_text', content: '开启' }, value: 'true' },
|
|
998
|
+
{ text: { tag: 'plain_text', content: '关闭' }, value: 'false' },
|
|
999
|
+
],
|
|
1000
|
+
initial_option: checked ? 'true' : 'false',
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
case 'multi-select': {
|
|
1004
|
+
const options = field.options.map(opt => ({
|
|
1005
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
1006
|
+
value: opt.value,
|
|
1007
|
+
}));
|
|
1008
|
+
const selectedValues = field.options.filter(opt => opt.selected).map(opt => opt.value);
|
|
1009
|
+
return {
|
|
1010
|
+
tag: 'multi_select_static',
|
|
1011
|
+
name: field.key,
|
|
1012
|
+
placeholder: { tag: 'plain_text', content: `选择${field.label}` },
|
|
1013
|
+
options,
|
|
1014
|
+
...(selectedValues.length > 0 ? { initial_options: selectedValues } : {}),
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
default:
|
|
1018
|
+
return { tag: 'markdown', content: `[不支持的字段类型: ${field.type}]` };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
export function buildFieldElement(requestId, field) {
|
|
1022
|
+
switch (field.type) {
|
|
1023
|
+
case 'select': {
|
|
1024
|
+
const options = field.options.map(opt => ({
|
|
1025
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
1026
|
+
value: opt.value,
|
|
1027
|
+
}));
|
|
1028
|
+
const selectedOpt = field.options.find(opt => opt.selected);
|
|
1029
|
+
return {
|
|
1030
|
+
tag: 'action',
|
|
1031
|
+
actions: [{
|
|
1032
|
+
tag: 'select_static',
|
|
1033
|
+
placeholder: { tag: 'plain_text', content: field.placeholder || `选择${field.label}` },
|
|
1034
|
+
options,
|
|
1035
|
+
...(selectedOpt ? { initial_option: selectedOpt.value } : {}),
|
|
1036
|
+
value: {
|
|
1037
|
+
_request_id: requestId,
|
|
1038
|
+
_field_key: field.key,
|
|
1039
|
+
},
|
|
1040
|
+
}],
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
case 'text': {
|
|
1044
|
+
// Feishu cards don't have a native text input component.
|
|
1045
|
+
// Use a note element as placeholder label; actual input via form submit.
|
|
1046
|
+
return {
|
|
1047
|
+
tag: 'note',
|
|
1048
|
+
elements: [
|
|
1049
|
+
{ tag: 'plain_text', content: `${field.label}: ${field.placeholder || '(请在提交时输入)'}` },
|
|
1050
|
+
],
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
case 'toggle': {
|
|
1054
|
+
const checked = field.defaultValue ?? false;
|
|
1055
|
+
return {
|
|
1056
|
+
tag: 'action',
|
|
1057
|
+
actions: [{
|
|
1058
|
+
tag: 'select_static',
|
|
1059
|
+
placeholder: { tag: 'plain_text', content: field.label },
|
|
1060
|
+
options: [
|
|
1061
|
+
{ text: { tag: 'plain_text', content: '开启' }, value: 'true' },
|
|
1062
|
+
{ text: { tag: 'plain_text', content: '关闭' }, value: 'false' },
|
|
1063
|
+
],
|
|
1064
|
+
initial_option: checked ? 'true' : 'false',
|
|
1065
|
+
value: {
|
|
1066
|
+
_request_id: requestId,
|
|
1067
|
+
_field_key: field.key,
|
|
1068
|
+
},
|
|
1069
|
+
}],
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
case 'multi-select': {
|
|
1073
|
+
// Feishu cards: use multi_select_static (checker)
|
|
1074
|
+
const options = field.options.map(opt => ({
|
|
1075
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
1076
|
+
value: opt.value,
|
|
1077
|
+
}));
|
|
1078
|
+
const selectedValues = field.options.filter(opt => opt.selected).map(opt => opt.value);
|
|
1079
|
+
return {
|
|
1080
|
+
tag: 'action',
|
|
1081
|
+
actions: [{
|
|
1082
|
+
tag: 'multi_select_static',
|
|
1083
|
+
placeholder: { tag: 'plain_text', content: `选择${field.label}` },
|
|
1084
|
+
options,
|
|
1085
|
+
...(selectedValues.length > 0 ? { initial_options: selectedValues } : {}),
|
|
1086
|
+
value: {
|
|
1087
|
+
_request_id: requestId,
|
|
1088
|
+
_field_key: field.key,
|
|
1089
|
+
},
|
|
1090
|
+
}],
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
default:
|
|
1094
|
+
return { tag: 'markdown', content: `[不支持的字段类型: ${field.type}]` };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
700
1097
|
function displayWidth(str) {
|
|
701
1098
|
let width = 0;
|
|
702
1099
|
for (const ch of str) {
|
|
@@ -735,6 +1132,26 @@ function convertTablesToText(text) {
|
|
|
735
1132
|
return '```\n' + [headerStr, sepStr, ...rowStrs].join('\n') + '\n```';
|
|
736
1133
|
});
|
|
737
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* 按段落边界拆分超长消息
|
|
1137
|
+
* 优先在 \n\n 处分割,其次 \n,最后强制截断
|
|
1138
|
+
*/
|
|
1139
|
+
function splitLongMessage(content, maxLength) {
|
|
1140
|
+
const parts = [];
|
|
1141
|
+
let remaining = content;
|
|
1142
|
+
while (remaining.length > maxLength) {
|
|
1143
|
+
let splitAt = remaining.lastIndexOf('\n\n', maxLength);
|
|
1144
|
+
if (splitAt <= 0)
|
|
1145
|
+
splitAt = remaining.lastIndexOf('\n', maxLength);
|
|
1146
|
+
if (splitAt <= 0)
|
|
1147
|
+
splitAt = maxLength;
|
|
1148
|
+
parts.push(remaining.slice(0, splitAt).trimEnd());
|
|
1149
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
1150
|
+
}
|
|
1151
|
+
if (remaining)
|
|
1152
|
+
parts.push(remaining);
|
|
1153
|
+
return parts;
|
|
1154
|
+
}
|
|
738
1155
|
export function markdownToFeishuPost(markdown, defaultTitle) {
|
|
739
1156
|
const match = markdown.match(/^# (.+)$/m);
|
|
740
1157
|
const title = match?.[1] ?? defaultTitle ?? '';
|
|
@@ -747,6 +1164,29 @@ export function markdownToFeishuPost(markdown, defaultTitle) {
|
|
|
747
1164
|
}
|
|
748
1165
|
};
|
|
749
1166
|
}
|
|
1167
|
+
/**
|
|
1168
|
+
* 将 Markdown 内容转为飞书消息卡片格式(interactive msg_type)
|
|
1169
|
+
* 飞书卡片的 markdown 组件支持完整 Markdown 渲染(代码块、表格、列表等)
|
|
1170
|
+
* 当前消息类型决策统一走 post + md tag,此函数为 interactive 卡片场景预留。
|
|
1171
|
+
*/
|
|
1172
|
+
export function markdownToFeishuCard(markdown, defaultTitle) {
|
|
1173
|
+
const match = markdown.match(/^# (.+)$/m);
|
|
1174
|
+
const title = match?.[1] ?? defaultTitle;
|
|
1175
|
+
let body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
|
|
1176
|
+
body = convertTablesToText(body).trim();
|
|
1177
|
+
const card = {
|
|
1178
|
+
config: { wide_screen_mode: true },
|
|
1179
|
+
elements: [
|
|
1180
|
+
{ tag: 'markdown', content: body }
|
|
1181
|
+
]
|
|
1182
|
+
};
|
|
1183
|
+
if (title) {
|
|
1184
|
+
card.header = {
|
|
1185
|
+
title: { tag: 'plain_text', content: title }
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
return card;
|
|
1189
|
+
}
|
|
750
1190
|
export function hasMarkdownSyntax(text) {
|
|
751
1191
|
const markdownPatterns = [
|
|
752
1192
|
/^#{1,6}\s/m, /\*\*.*?\*\*/, /\*.*?\*/, /__.*?__/, /_.*?_/, /~~.*?~~/,
|
|
@@ -755,73 +1195,93 @@ export function hasMarkdownSyntax(text) {
|
|
|
755
1195
|
];
|
|
756
1196
|
return markdownPatterns.some(pattern => pattern.test(text));
|
|
757
1197
|
}
|
|
1198
|
+
import { normalizeChannelInstances } from '../config.js';
|
|
758
1199
|
export class FeishuChannelPlugin {
|
|
759
1200
|
name = 'feishu';
|
|
760
1201
|
isEnabled(config) {
|
|
761
|
-
const
|
|
762
|
-
if (
|
|
1202
|
+
const raw = config.channels?.feishu;
|
|
1203
|
+
if (!raw)
|
|
1204
|
+
return false;
|
|
1205
|
+
if (Array.isArray(raw)) {
|
|
1206
|
+
return raw.some(inst => inst.enabled !== false && inst.appId && inst.appSecret);
|
|
1207
|
+
}
|
|
1208
|
+
if (raw.enabled === false)
|
|
763
1209
|
return false;
|
|
764
|
-
return !!(
|
|
1210
|
+
return !!(raw.appId && raw.appSecret);
|
|
1211
|
+
}
|
|
1212
|
+
async createChannels(config) {
|
|
1213
|
+
const instances = normalizeChannelInstances(config.channels?.feishu, 'feishu');
|
|
1214
|
+
const result = [];
|
|
1215
|
+
for (const inst of instances) {
|
|
1216
|
+
if (inst.enabled === false || !inst.appId || !inst.appSecret)
|
|
1217
|
+
continue;
|
|
1218
|
+
const channel = new FeishuChannel({
|
|
1219
|
+
appId: inst.appId,
|
|
1220
|
+
appSecret: inst.appSecret,
|
|
1221
|
+
enableRichContent: config.enableRichContent,
|
|
1222
|
+
});
|
|
1223
|
+
const adapter = {
|
|
1224
|
+
channelName: inst.name,
|
|
1225
|
+
sendText: (id, text, context) => channel.sendMessage(id, text, context),
|
|
1226
|
+
sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
|
|
1227
|
+
sendImage: (id, png, context) => channel.sendImage(id, png, context),
|
|
1228
|
+
acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); },
|
|
1229
|
+
sendInteraction: (id, interaction, context) => channel.sendInteraction(id, interaction, context),
|
|
1230
|
+
patchInteractionCard: (messageId, card) => channel.patchInteractionCard(messageId, card),
|
|
1231
|
+
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1232
|
+
};
|
|
1233
|
+
const policy = {
|
|
1234
|
+
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
1235
|
+
canListProjects: (chatType, identity) => identity === 'owner',
|
|
1236
|
+
canCreateSession: (chatType, identity) => true,
|
|
1237
|
+
canDeleteSession: (chatType, identity) => true,
|
|
1238
|
+
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
1239
|
+
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
1240
|
+
showMiddleResult: (chatType, identity) => {
|
|
1241
|
+
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
1242
|
+
if (mode === 'none')
|
|
1243
|
+
return false;
|
|
1244
|
+
if (mode === 'dm-only')
|
|
1245
|
+
return chatType === 'private';
|
|
1246
|
+
if (mode === 'owner-dm-only')
|
|
1247
|
+
return chatType === 'private' && identity === 'owner';
|
|
1248
|
+
return true;
|
|
1249
|
+
},
|
|
1250
|
+
showIdleMonitor: (chatType, identity) => {
|
|
1251
|
+
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
1252
|
+
if (mode === 'none')
|
|
1253
|
+
return false;
|
|
1254
|
+
if (mode === 'dm-only')
|
|
1255
|
+
return chatType === 'private';
|
|
1256
|
+
if (mode === 'owner-dm-only')
|
|
1257
|
+
return chatType === 'private' && identity === 'owner';
|
|
1258
|
+
return true;
|
|
1259
|
+
},
|
|
1260
|
+
accumulateErrors: (chatType, identity) => true,
|
|
1261
|
+
};
|
|
1262
|
+
const options = {
|
|
1263
|
+
fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
|
|
1264
|
+
supportsImages: true,
|
|
1265
|
+
flushDelay: inst.flushDelay,
|
|
1266
|
+
};
|
|
1267
|
+
result.push({
|
|
1268
|
+
channelType: 'feishu',
|
|
1269
|
+
adapter,
|
|
1270
|
+
channel,
|
|
1271
|
+
policy,
|
|
1272
|
+
options,
|
|
1273
|
+
connect: () => channel.connect(),
|
|
1274
|
+
disconnect: () => channel.disconnect(),
|
|
1275
|
+
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
return result;
|
|
765
1279
|
}
|
|
766
1280
|
async createChannel(config) {
|
|
767
|
-
const
|
|
768
|
-
if (
|
|
1281
|
+
const instances = await this.createChannels(config);
|
|
1282
|
+
if (instances.length === 0) {
|
|
769
1283
|
throw new Error('Feishu config missing');
|
|
770
1284
|
}
|
|
771
|
-
|
|
772
|
-
appId: feishuConfig.appId,
|
|
773
|
-
appSecret: feishuConfig.appSecret,
|
|
774
|
-
enableRichContent: feishuConfig.enableRichContent,
|
|
775
|
-
});
|
|
776
|
-
const adapter = {
|
|
777
|
-
name: 'feishu',
|
|
778
|
-
sendText: (id, text, context) => channel.sendMessage(id, text, context),
|
|
779
|
-
sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
|
|
780
|
-
sendImage: (id, png, context) => channel.sendImage(id, png, context),
|
|
781
|
-
acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); },
|
|
782
|
-
};
|
|
783
|
-
const policy = {
|
|
784
|
-
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
785
|
-
canListProjects: (chatType, identity) => identity === 'owner',
|
|
786
|
-
canCreateSession: (chatType, identity) => true,
|
|
787
|
-
canDeleteSession: (chatType, identity) => true,
|
|
788
|
-
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
789
|
-
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
790
|
-
showMiddleResult: (chatType, identity) => {
|
|
791
|
-
const mode = feishuConfig.showActivities ?? config.showActivities ?? 'all';
|
|
792
|
-
if (mode === 'none')
|
|
793
|
-
return false;
|
|
794
|
-
if (mode === 'dm-only')
|
|
795
|
-
return chatType === 'private';
|
|
796
|
-
if (mode === 'owner-dm-only')
|
|
797
|
-
return chatType === 'private' && identity === 'owner';
|
|
798
|
-
return true;
|
|
799
|
-
},
|
|
800
|
-
showIdleMonitor: (chatType, identity) => {
|
|
801
|
-
const mode = feishuConfig.showActivities ?? config.showActivities ?? 'all';
|
|
802
|
-
if (mode === 'none')
|
|
803
|
-
return false;
|
|
804
|
-
if (mode === 'dm-only')
|
|
805
|
-
return chatType === 'private';
|
|
806
|
-
if (mode === 'owner-dm-only')
|
|
807
|
-
return chatType === 'private' && identity === 'owner';
|
|
808
|
-
return true;
|
|
809
|
-
},
|
|
810
|
-
accumulateErrors: (chatType, identity) => true,
|
|
811
|
-
};
|
|
812
|
-
const options = {
|
|
813
|
-
fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
|
|
814
|
-
supportsImages: true,
|
|
815
|
-
flushDelay: feishuConfig.flushDelay,
|
|
816
|
-
};
|
|
817
|
-
return {
|
|
818
|
-
adapter,
|
|
819
|
-
channel,
|
|
820
|
-
policy,
|
|
821
|
-
options,
|
|
822
|
-
connect: () => channel.connect(),
|
|
823
|
-
disconnect: () => channel.disconnect(),
|
|
824
|
-
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
825
|
-
};
|
|
1285
|
+
return instances[0];
|
|
826
1286
|
}
|
|
827
1287
|
}
|