@xmanrui/dsh-im 0.7.0 → 0.7.2
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/lib/index.js +119 -121
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +423 -8
- package/src/channels/dingtalk/harness-client.mjs +16 -366
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +11 -1
- package/src/channels/discord/harness-client.mjs +10 -2
- package/src/channels/feishu/bridge.mjs +571 -50
- package/src/channels/feishu/feishu-runtime.mjs +41 -1
- package/src/channels/feishu/harness-client.mjs +16 -335
- package/src/channels/qq/harness-client.mjs +10 -2
- package/src/channels/qq/qq-bridge.mjs +428 -28
- package/src/channels/qq/qq-runtime.mjs +14 -3
- package/src/channels/shared/harness-approval.mjs +472 -0
- package/src/channels/shared/harness-client.mjs +858 -0
- package/src/channels/shared/harness-question.mjs +85 -0
- package/src/channels/shared/text-harness-bridge.mjs +486 -24
- package/src/channels/slack/harness-client.mjs +10 -2
- package/src/channels/slack/slack-runtime.mjs +11 -1
- package/src/channels/telegram/harness-client.mjs +10 -2
- package/src/channels/telegram/telegram-runtime.mjs +15 -4
- package/src/channels/wecom/harness-client.mjs +10 -2
- package/src/channels/wecom/wecom-bridge.mjs +434 -14
- package/src/channels/wecom/wecom-runtime.mjs +6 -0
- package/src/channels/weixin/harness-client.mjs +16 -326
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +451 -22
- package/src/channels/weixin/weixin-runtime.mjs +56 -7
- package/src/channels/whatsapp/harness-client.mjs +10 -2
- package/src/channels/whatsapp/whatsapp-runtime.mjs +1 -0
|
@@ -5,9 +5,18 @@ import {
|
|
|
5
5
|
isBotSender,
|
|
6
6
|
splitText,
|
|
7
7
|
} from './message-utils.mjs';
|
|
8
|
+
import {
|
|
9
|
+
harnessAnswerForQuestion,
|
|
10
|
+
harnessQuestionText,
|
|
11
|
+
validHarnessQuestion,
|
|
12
|
+
} from '../shared/harness-question.mjs';
|
|
13
|
+
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
8
14
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
9
15
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
10
16
|
|
|
17
|
+
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
18
|
+
const RESOLVED_REPLY_TTL_MS = 30 * 60_000;
|
|
19
|
+
|
|
11
20
|
const HELP_TEXT = [
|
|
12
21
|
'北汇星河 AIOS 已连接 DeepSeek Harness。',
|
|
13
22
|
'',
|
|
@@ -21,16 +30,50 @@ const HELP_TEXT = [
|
|
|
21
30
|
'/help 显示本帮助',
|
|
22
31
|
].join('\n');
|
|
23
32
|
|
|
33
|
+
function nonEmptyString(value) {
|
|
34
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function senderOpenId(event) {
|
|
38
|
+
return nonEmptyString(event?.sender?.sender_id?.open_id)
|
|
39
|
+
?? nonEmptyString(event?.sender?.sender_id?.user_id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canClaimInteractionReply(event, pending) {
|
|
43
|
+
return pending.needsPresentation !== true
|
|
44
|
+
&& pending.questions[pending.index]
|
|
45
|
+
&& senderOpenId(event) === pending.actor
|
|
46
|
+
&& event?.message?.message_type === 'text'
|
|
47
|
+
&& nonEmptyString(extractText(event));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureStatus(status) {
|
|
51
|
+
for (const key of ['messagesReceived', 'messagesReplied', 'messagesRejected']) {
|
|
52
|
+
status[key] ??= 0;
|
|
53
|
+
}
|
|
54
|
+
status.lastMessageAt ??= null;
|
|
55
|
+
status.lastReplyAt ??= null;
|
|
56
|
+
status.lastRejectedAt ??= null;
|
|
57
|
+
status.lastError ??= null;
|
|
58
|
+
}
|
|
59
|
+
|
|
24
60
|
export class FeishuHarnessBridge {
|
|
25
61
|
#client;
|
|
26
62
|
#channel;
|
|
27
63
|
#harness;
|
|
28
64
|
#state;
|
|
29
65
|
#queues = new Map();
|
|
66
|
+
#pendingInteractions = new Map();
|
|
67
|
+
#interactionKeys = new Map();
|
|
68
|
+
#resolvedQuestionReplies = new Map();
|
|
30
69
|
#acceptedMessageIds = new Set();
|
|
70
|
+
#interactionTasks = new Set();
|
|
71
|
+
#approvals;
|
|
31
72
|
#status;
|
|
32
73
|
#allowedSenderOpenIds;
|
|
33
74
|
#replyTimeoutMs;
|
|
75
|
+
#logger;
|
|
76
|
+
#signal;
|
|
34
77
|
|
|
35
78
|
constructor({
|
|
36
79
|
client,
|
|
@@ -39,8 +82,13 @@ export class FeishuHarnessBridge {
|
|
|
39
82
|
state,
|
|
40
83
|
status,
|
|
41
84
|
allowedSenderOpenIds = new Set(),
|
|
42
|
-
replyTimeoutMs =
|
|
85
|
+
replyTimeoutMs = 600_000,
|
|
86
|
+
logger = console,
|
|
87
|
+
signal,
|
|
43
88
|
}) {
|
|
89
|
+
if (!client || !harness || !state || !status) {
|
|
90
|
+
throw new TypeError('Feishu bridge dependencies are required');
|
|
91
|
+
}
|
|
44
92
|
this.#client = client;
|
|
45
93
|
this.#channel = channel;
|
|
46
94
|
this.#harness = harness;
|
|
@@ -48,55 +96,210 @@ export class FeishuHarnessBridge {
|
|
|
48
96
|
this.#status = status;
|
|
49
97
|
this.#allowedSenderOpenIds = allowedSenderOpenIds;
|
|
50
98
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
99
|
+
this.#logger = logger;
|
|
100
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'Feishu', logger });
|
|
101
|
+
this.#signal = signal;
|
|
102
|
+
ensureStatus(this.#status);
|
|
51
103
|
}
|
|
52
104
|
|
|
53
105
|
accept(event) {
|
|
54
|
-
|
|
55
|
-
|
|
106
|
+
if (this.#signal?.aborted) return Promise.resolve();
|
|
107
|
+
const messageId = nonEmptyString(event?.message?.message_id);
|
|
108
|
+
if (!messageId || isBotSender(event)) return Promise.resolve();
|
|
56
109
|
if (!isAllowedSender(event, this.#allowedSenderOpenIds)) {
|
|
57
110
|
this.#status.messagesRejected += 1;
|
|
58
111
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
59
|
-
|
|
60
|
-
return;
|
|
112
|
+
this.#logger.warn?.('[dsh-feishu] ignored a message from a sender outside the allowlist');
|
|
113
|
+
return Promise.resolve();
|
|
114
|
+
}
|
|
115
|
+
if (this.#state.hasSeen(messageId) || this.#acceptedMessageIds.has(messageId)) {
|
|
116
|
+
return Promise.resolve();
|
|
61
117
|
}
|
|
62
|
-
|
|
118
|
+
|
|
119
|
+
let key;
|
|
120
|
+
try {
|
|
121
|
+
key = conversationKey(event);
|
|
122
|
+
} catch {
|
|
123
|
+
this.#status.messagesRejected += 1;
|
|
124
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
|
|
63
128
|
this.#acceptedMessageIds.add(messageId);
|
|
64
129
|
const processingReaction = this.#addReaction(messageId, 'OnIt');
|
|
130
|
+
if (this.#isResolvedQuestionReply(event, key)) {
|
|
131
|
+
const current = Promise.resolve()
|
|
132
|
+
.then(() => this.#discardResolvedInteractionReply(event, messageId))
|
|
133
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
134
|
+
.catch((error) => this.#handleMessageFailure(
|
|
135
|
+
event,
|
|
136
|
+
messageId,
|
|
137
|
+
processingReaction,
|
|
138
|
+
error,
|
|
139
|
+
))
|
|
140
|
+
.finally(() => this.#acceptedMessageIds.delete(messageId));
|
|
141
|
+
return current;
|
|
142
|
+
}
|
|
143
|
+
const pending = this.#pendingInteractions.get(key);
|
|
144
|
+
const approvalReply = this.#approvals.claimReply({
|
|
145
|
+
key,
|
|
146
|
+
actor: senderOpenId(event),
|
|
147
|
+
messageId,
|
|
148
|
+
text: extractText(event) ?? '',
|
|
149
|
+
addressed: event?.message?.chat_type === 'p2p'
|
|
150
|
+
|| (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0),
|
|
151
|
+
hasPendingQuestion: Boolean(pending),
|
|
152
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
153
|
+
? pending.queue
|
|
154
|
+
: null,
|
|
155
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
156
|
+
send: (text) => this.#send(event.message.chat_id, text),
|
|
157
|
+
});
|
|
158
|
+
if (approvalReply) {
|
|
159
|
+
const processing = approvalReply.process(async () => {
|
|
160
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
161
|
+
await this.#state.markSeen(messageId);
|
|
162
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
163
|
+
this.#status.messagesReceived += 1;
|
|
164
|
+
return true;
|
|
165
|
+
});
|
|
166
|
+
let current;
|
|
167
|
+
current = processing
|
|
168
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
169
|
+
.catch((error) => this.#handleMessageFailure(
|
|
170
|
+
event,
|
|
171
|
+
messageId,
|
|
172
|
+
processingReaction,
|
|
173
|
+
error,
|
|
174
|
+
))
|
|
175
|
+
.finally(() => {
|
|
176
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
177
|
+
this.#interactionTasks.delete(current);
|
|
178
|
+
});
|
|
179
|
+
this.#interactionTasks.add(current);
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
182
|
+
if (pending && senderOpenId(event) !== pending.actor) {
|
|
183
|
+
return this.#enqueueMessage(event, messageId, key, processingReaction);
|
|
184
|
+
}
|
|
185
|
+
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
186
|
+
return this.#enqueueMessage(event, messageId, key, processingReaction);
|
|
187
|
+
}
|
|
188
|
+
if (pending) {
|
|
189
|
+
if (canClaimInteractionReply(event, pending)) pending.claimedReplyMessageId = messageId;
|
|
190
|
+
const previous = pending.queue ?? Promise.resolve();
|
|
191
|
+
const processing = previous
|
|
192
|
+
.catch(() => undefined)
|
|
193
|
+
.then(() => this.#processInteractionReply(
|
|
194
|
+
event,
|
|
195
|
+
messageId,
|
|
196
|
+
key,
|
|
197
|
+
pending,
|
|
198
|
+
processingReaction,
|
|
199
|
+
));
|
|
200
|
+
pending.queue = processing;
|
|
65
201
|
|
|
66
|
-
|
|
202
|
+
const releaseInteraction = () => {
|
|
203
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
204
|
+
pending.claimedReplyMessageId = null;
|
|
205
|
+
}
|
|
206
|
+
if (pending.queue === processing) pending.queue = null;
|
|
207
|
+
};
|
|
208
|
+
let current;
|
|
209
|
+
current = processing
|
|
210
|
+
.then(
|
|
211
|
+
() => {
|
|
212
|
+
releaseInteraction();
|
|
213
|
+
return this.#finishReaction(messageId, processingReaction, 'DONE');
|
|
214
|
+
},
|
|
215
|
+
(error) => {
|
|
216
|
+
releaseInteraction();
|
|
217
|
+
return this.#handleMessageFailure(
|
|
218
|
+
event,
|
|
219
|
+
messageId,
|
|
220
|
+
processingReaction,
|
|
221
|
+
error,
|
|
222
|
+
);
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
.finally(() => {
|
|
226
|
+
releaseInteraction();
|
|
227
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
228
|
+
this.#interactionTasks.delete(current);
|
|
229
|
+
});
|
|
230
|
+
this.#interactionTasks.add(current);
|
|
231
|
+
return current;
|
|
232
|
+
}
|
|
233
|
+
return this.#enqueueMessage(event, messageId, key, processingReaction);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
#enqueueMessage(event, messageId, key, processingReaction, {
|
|
237
|
+
releaseMessageId = true,
|
|
238
|
+
alreadyRecorded = false,
|
|
239
|
+
finalize = true,
|
|
240
|
+
} = {}) {
|
|
67
241
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
68
|
-
const
|
|
242
|
+
const work = previous
|
|
69
243
|
.catch(() => undefined)
|
|
70
|
-
.then(() => this.#handle(event, key))
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
244
|
+
.then(() => this.#handle(event, key, { alreadyRecorded }));
|
|
245
|
+
const settled = finalize
|
|
246
|
+
? work
|
|
247
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
248
|
+
.catch((error) => this.#handleMessageFailure(
|
|
249
|
+
event,
|
|
250
|
+
messageId,
|
|
251
|
+
processingReaction,
|
|
252
|
+
error,
|
|
253
|
+
))
|
|
254
|
+
: work;
|
|
255
|
+
let current;
|
|
256
|
+
current = settled.finally(() => {
|
|
257
|
+
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
258
|
+
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
259
|
+
});
|
|
260
|
+
this.#queues.set(key, current);
|
|
261
|
+
return current;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async #handleMessageFailure(event, messageId, processingReaction, error) {
|
|
265
|
+
if (this.#signal?.aborted) {
|
|
266
|
+
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.#logger.error?.('[dsh-feishu] message handling failed:', error?.message ?? String(error));
|
|
270
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
271
|
+
await this.#finishReaction(messageId, processingReaction, 'ERROR');
|
|
272
|
+
await this.#send(
|
|
273
|
+
event.message.chat_id,
|
|
274
|
+
'处理失败,请稍后重试。如果问题持续,请在 DeepSeek Harness 的飞书插件页面检查连接状态。',
|
|
275
|
+
).catch(() => undefined);
|
|
86
276
|
}
|
|
87
277
|
|
|
88
278
|
async waitForIdle() {
|
|
89
|
-
await Promise.allSettled([
|
|
279
|
+
await Promise.allSettled([
|
|
280
|
+
...this.#queues.values(),
|
|
281
|
+
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
282
|
+
pending.queue ? [pending.queue] : []
|
|
283
|
+
)),
|
|
284
|
+
...this.#interactionTasks,
|
|
285
|
+
]);
|
|
90
286
|
}
|
|
91
287
|
|
|
92
|
-
async #handle(event, key) {
|
|
288
|
+
async #handle(event, key, { alreadyRecorded = false } = {}) {
|
|
289
|
+
this.#signal?.throwIfAborted();
|
|
93
290
|
const messageId = event.message.message_id;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
291
|
+
if (!alreadyRecorded) {
|
|
292
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
293
|
+
await this.#state.markSeen(messageId);
|
|
294
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
295
|
+
this.#status.messagesReceived += 1;
|
|
296
|
+
}
|
|
97
297
|
|
|
98
298
|
const text = extractText(event);
|
|
99
|
-
if (!text)
|
|
299
|
+
if (!text) {
|
|
300
|
+
await this.#send(event.message.chat_id, '目前仅支持文字消息。');
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
100
303
|
|
|
101
304
|
if (text === '/help') {
|
|
102
305
|
await this.#send(event.message.chat_id, HELP_TEXT);
|
|
@@ -108,7 +311,7 @@ export class FeishuHarnessBridge {
|
|
|
108
311
|
return;
|
|
109
312
|
}
|
|
110
313
|
if (text === '/status') {
|
|
111
|
-
await this.#harness.ensureRunning();
|
|
314
|
+
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
112
315
|
await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
|
|
113
316
|
return;
|
|
114
317
|
}
|
|
@@ -120,11 +323,30 @@ export class FeishuHarnessBridge {
|
|
|
120
323
|
return;
|
|
121
324
|
}
|
|
122
325
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
326
|
+
this.#logger.info?.(`[dsh-feishu] processing ${event.message.chat_type} message ${messageId}`);
|
|
327
|
+
try {
|
|
328
|
+
await this.#answerWithStream(event, key, text);
|
|
329
|
+
this.#status.messagesReplied += 1;
|
|
330
|
+
this.#status.lastReplyAt = new Date().toISOString();
|
|
331
|
+
this.#status.lastError = null;
|
|
332
|
+
} finally {
|
|
333
|
+
await this.#cancelPendingInteraction(key);
|
|
334
|
+
await this.#approvals.closeRoute(key);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#interactionAskOptions(event, key) {
|
|
339
|
+
return {
|
|
340
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
341
|
+
signal: this.#signal,
|
|
342
|
+
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
343
|
+
key,
|
|
344
|
+
actor: senderOpenId(event),
|
|
345
|
+
chatId: event.message.chat_id,
|
|
346
|
+
requiresMention: event.message.chat_type !== 'p2p',
|
|
347
|
+
}),
|
|
348
|
+
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
349
|
+
};
|
|
128
350
|
}
|
|
129
351
|
|
|
130
352
|
async #answerWithStream(event, key, text) {
|
|
@@ -136,7 +358,9 @@ export class FeishuHarnessBridge {
|
|
|
136
358
|
state: this.#state,
|
|
137
359
|
key,
|
|
138
360
|
text,
|
|
139
|
-
|
|
361
|
+
createOptions: { signal: this.#signal },
|
|
362
|
+
existsOptions: { signal: this.#signal },
|
|
363
|
+
askOptions: this.#interactionAskOptions(event, key),
|
|
140
364
|
});
|
|
141
365
|
for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
|
|
142
366
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
@@ -149,18 +373,21 @@ export class FeishuHarnessBridge {
|
|
|
149
373
|
await this.#channel.stream(chatId, {
|
|
150
374
|
markdown: async (controller) => {
|
|
151
375
|
promptStarted = true;
|
|
376
|
+
const askOptions = {
|
|
377
|
+
...this.#interactionAskOptions(event, key),
|
|
378
|
+
onUpdate: async (update) => {
|
|
379
|
+
await controller.setContent(this.#progressText(update));
|
|
380
|
+
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
|
381
|
+
},
|
|
382
|
+
};
|
|
152
383
|
({ answer: completedAnswer } = await askInWorkspaceSession({
|
|
153
384
|
harness: this.#harness,
|
|
154
385
|
state: this.#state,
|
|
155
386
|
key,
|
|
156
387
|
text,
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
await controller.setContent(this.#progressText(update));
|
|
161
|
-
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
|
162
|
-
},
|
|
163
|
-
},
|
|
388
|
+
createOptions: { signal: this.#signal },
|
|
389
|
+
existsOptions: { signal: this.#signal },
|
|
390
|
+
askOptions,
|
|
164
391
|
}));
|
|
165
392
|
await controller.setContent(completedAnswer);
|
|
166
393
|
},
|
|
@@ -169,26 +396,315 @@ export class FeishuHarnessBridge {
|
|
|
169
396
|
} catch (error) {
|
|
170
397
|
this.#status.streamErrors = (this.#status.streamErrors ?? 0) + 1;
|
|
171
398
|
if (completedAnswer) {
|
|
172
|
-
|
|
399
|
+
this.#logger.warn?.(
|
|
400
|
+
'[dsh-feishu] native stream failed after generation; sending final text:',
|
|
401
|
+
error.message,
|
|
402
|
+
);
|
|
173
403
|
for (const chunk of splitText(completedAnswer)) await this.#send(chatId, chunk);
|
|
174
404
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
175
405
|
return;
|
|
176
406
|
}
|
|
177
407
|
if (promptStarted) throw error;
|
|
178
408
|
|
|
179
|
-
|
|
409
|
+
this.#logger.warn?.('[dsh-feishu] native stream unavailable; using text fallback:', error.message);
|
|
180
410
|
const { answer } = await askInWorkspaceSession({
|
|
181
411
|
harness: this.#harness,
|
|
182
412
|
state: this.#state,
|
|
183
413
|
key,
|
|
184
414
|
text,
|
|
185
|
-
|
|
415
|
+
createOptions: { signal: this.#signal },
|
|
416
|
+
existsOptions: { signal: this.#signal },
|
|
417
|
+
askOptions: this.#interactionAskOptions(event, key),
|
|
186
418
|
});
|
|
187
419
|
for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
|
|
188
420
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
189
421
|
}
|
|
190
422
|
}
|
|
191
423
|
|
|
424
|
+
async #processInteractionReply(event, messageId, key, expected, processingReaction) {
|
|
425
|
+
this.#signal?.throwIfAborted();
|
|
426
|
+
const current = this.#pendingInteractions.get(key);
|
|
427
|
+
const claimed = expected.claimedReplyMessageId === messageId;
|
|
428
|
+
if (!current || current !== expected || current.submitting) {
|
|
429
|
+
if (this.#isResolvedQuestionReply(event, key)) {
|
|
430
|
+
return this.#discardResolvedInteractionReply(event, messageId);
|
|
431
|
+
}
|
|
432
|
+
if (claimed && (!current || current !== expected)) {
|
|
433
|
+
return this.#discardResolvedInteractionReply(event, messageId);
|
|
434
|
+
}
|
|
435
|
+
return this.#enqueueMessage(event, messageId, key, processingReaction, {
|
|
436
|
+
releaseMessageId: false,
|
|
437
|
+
finalize: false,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
441
|
+
await this.#state.markSeen(messageId);
|
|
442
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
443
|
+
this.#status.messagesReceived += 1;
|
|
444
|
+
|
|
445
|
+
const text = extractText(event);
|
|
446
|
+
if (!text) {
|
|
447
|
+
await this.#send(event.message.chat_id, '请用文字回答当前问题。');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const pending = this.#pendingInteractions.get(key);
|
|
452
|
+
if (!pending || pending !== expected || pending.submitting) {
|
|
453
|
+
if (this.#isResolvedQuestionReply(event, key)) {
|
|
454
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (claimed && (!pending || pending !== expected)) {
|
|
458
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
return this.#enqueueMessage(event, messageId, key, processingReaction, {
|
|
462
|
+
releaseMessageId: false,
|
|
463
|
+
alreadyRecorded: true,
|
|
464
|
+
finalize: false,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
pending.chatId = event.message.chat_id;
|
|
468
|
+
if (pending.needsPresentation) {
|
|
469
|
+
try {
|
|
470
|
+
await this.#presentInteraction(pending);
|
|
471
|
+
} catch {
|
|
472
|
+
this.#status.lastError = '飞书交互问题发送失败。';
|
|
473
|
+
this.#logger.error?.('[dsh-feishu] failed to retry an interaction question');
|
|
474
|
+
pending.interaction.reconnect?.();
|
|
475
|
+
}
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const question = pending.questions[pending.index];
|
|
479
|
+
if (!question) return;
|
|
480
|
+
|
|
481
|
+
pending.answers.push(harnessAnswerForQuestion(question, text));
|
|
482
|
+
pending.index += 1;
|
|
483
|
+
if (pending.index < pending.questions.length) {
|
|
484
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
485
|
+
pending.claimedReplyMessageId = null;
|
|
486
|
+
}
|
|
487
|
+
pending.needsPresentation = true;
|
|
488
|
+
try {
|
|
489
|
+
await this.#presentInteraction(pending);
|
|
490
|
+
} catch {
|
|
491
|
+
this.#status.lastError = '飞书交互问题发送失败。';
|
|
492
|
+
this.#logger.error?.('[dsh-feishu] failed to send the next interaction question');
|
|
493
|
+
pending.interaction.reconnect?.();
|
|
494
|
+
}
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
pending.submitting = true;
|
|
499
|
+
try {
|
|
500
|
+
await pending.interaction.respond({
|
|
501
|
+
ok: true,
|
|
502
|
+
value: {
|
|
503
|
+
sessionId: pending.sessionId,
|
|
504
|
+
answer: { answers: pending.answers },
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
this.#rememberResolvedInteraction(key, pending);
|
|
508
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
509
|
+
this.#status.lastError = null;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
if (this.#signal?.aborted) return;
|
|
512
|
+
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
513
|
+
if (error?.code === 'interaction-not-pending') {
|
|
514
|
+
this.#rememberResolvedInteraction(key, pending);
|
|
515
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
516
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
pending.submitting = false;
|
|
520
|
+
pending.answers.pop();
|
|
521
|
+
pending.index -= 1;
|
|
522
|
+
this.#status.lastError = '回答提交失败。';
|
|
523
|
+
this.#logger.error?.('[dsh-feishu] failed to answer a Harness interaction');
|
|
524
|
+
await this.#send(event.message.chat_id, '回答提交失败,请重新发送当前问题的答案。')
|
|
525
|
+
.catch(() => undefined);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async #handleInteraction(interaction, {
|
|
530
|
+
key,
|
|
531
|
+
actor,
|
|
532
|
+
chatId,
|
|
533
|
+
requiresMention,
|
|
534
|
+
}) {
|
|
535
|
+
if (await this.#approvals.handleRequested(interaction, {
|
|
536
|
+
key,
|
|
537
|
+
actor,
|
|
538
|
+
requiresMention,
|
|
539
|
+
send: (text) => this.#send(chatId, text),
|
|
540
|
+
})) return;
|
|
541
|
+
|
|
542
|
+
// Approval requests return above; the existing question state machine stays unchanged.
|
|
543
|
+
if (interaction?.kind !== 'question') return;
|
|
544
|
+
const questions = interaction?.payload?.questions;
|
|
545
|
+
const interactionId = typeof interaction?.interactionId === 'string'
|
|
546
|
+
? interaction.interactionId
|
|
547
|
+
: interaction?.rpcId;
|
|
548
|
+
if (typeof interaction.rpcId !== 'string'
|
|
549
|
+
|| typeof interactionId !== 'string'
|
|
550
|
+
|| typeof interaction.sessionId !== 'string'
|
|
551
|
+
|| !Array.isArray(questions)
|
|
552
|
+
|| questions.length === 0
|
|
553
|
+
|| questions.some((question) => !validHarnessQuestion(question))) {
|
|
554
|
+
this.#logger.warn?.('[dsh-feishu] ignored an invalid Harness question interaction');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (interaction.recovered === true) {
|
|
559
|
+
await interaction.respond({
|
|
560
|
+
ok: false,
|
|
561
|
+
error: {
|
|
562
|
+
code: 'cancelled',
|
|
563
|
+
message: 'Feishu safely cancelled an interaction left by an earlier client.',
|
|
564
|
+
details: {},
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
await this.#send(
|
|
568
|
+
chatId,
|
|
569
|
+
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
570
|
+
).catch(() => undefined);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const existing = this.#pendingInteractions.get(key);
|
|
575
|
+
if (existing?.interactionId === interactionId) {
|
|
576
|
+
existing.interaction = interaction;
|
|
577
|
+
if (existing.needsPresentation) await this.#presentInteraction(existing);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (this.#interactionKeys.has(interactionId)) return;
|
|
581
|
+
if (existing) {
|
|
582
|
+
await interaction.respond({
|
|
583
|
+
ok: false,
|
|
584
|
+
error: {
|
|
585
|
+
code: 'cancelled',
|
|
586
|
+
message: 'Feishu is already handling another user interaction.',
|
|
587
|
+
details: {},
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const pending = {
|
|
594
|
+
kind: 'question',
|
|
595
|
+
interactionId,
|
|
596
|
+
sessionId: interaction.sessionId,
|
|
597
|
+
interaction,
|
|
598
|
+
key,
|
|
599
|
+
actor,
|
|
600
|
+
requiresMention,
|
|
601
|
+
questions,
|
|
602
|
+
answers: [],
|
|
603
|
+
index: 0,
|
|
604
|
+
chatId,
|
|
605
|
+
queue: null,
|
|
606
|
+
claimedReplyMessageId: null,
|
|
607
|
+
submitting: false,
|
|
608
|
+
needsPresentation: true,
|
|
609
|
+
questionMessageIds: new Set(),
|
|
610
|
+
inactive: false,
|
|
611
|
+
};
|
|
612
|
+
this.#pendingInteractions.set(key, pending);
|
|
613
|
+
this.#interactionKeys.set(pending.interactionId, key);
|
|
614
|
+
await this.#presentInteraction(pending);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async #handleInteractionResolved(resolution) {
|
|
618
|
+
if (await this.#approvals.handleResolved(resolution)) return;
|
|
619
|
+
const interactionId = resolution?.interactionId;
|
|
620
|
+
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
621
|
+
const key = this.#interactionKeys.get(interactionId);
|
|
622
|
+
if (!key) return;
|
|
623
|
+
const pending = this.#pendingInteractions.get(key);
|
|
624
|
+
if (pending) this.#rememberResolvedInteraction(key, pending);
|
|
625
|
+
this.#clearPendingInteraction(key, interactionId);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async #presentInteraction(pending) {
|
|
629
|
+
const question = pending.questions[pending.index];
|
|
630
|
+
if (!question) return;
|
|
631
|
+
const messageId = await this.#send(
|
|
632
|
+
pending.chatId,
|
|
633
|
+
harnessQuestionText(
|
|
634
|
+
question,
|
|
635
|
+
pending.index,
|
|
636
|
+
pending.questions.length,
|
|
637
|
+
{ requiresMention: pending.requiresMention },
|
|
638
|
+
),
|
|
639
|
+
);
|
|
640
|
+
if (messageId) {
|
|
641
|
+
pending.questionMessageIds.add(messageId);
|
|
642
|
+
if (pending.inactive) this.#rememberResolvedInteraction(pending.key, pending);
|
|
643
|
+
}
|
|
644
|
+
pending.needsPresentation = false;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
#rememberResolvedInteraction(key, pending) {
|
|
648
|
+
const expiresAt = Date.now() + RESOLVED_REPLY_TTL_MS;
|
|
649
|
+
for (const messageId of pending.questionMessageIds ?? []) {
|
|
650
|
+
this.#resolvedQuestionReplies.set(messageId, { key, expiresAt });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
#isResolvedQuestionReply(event, key) {
|
|
655
|
+
const now = Date.now();
|
|
656
|
+
for (const [messageId, resolution] of this.#resolvedQuestionReplies) {
|
|
657
|
+
if (resolution.expiresAt <= now) this.#resolvedQuestionReplies.delete(messageId);
|
|
658
|
+
}
|
|
659
|
+
for (const reference of [event?.message?.parent_id, event?.message?.root_id]) {
|
|
660
|
+
const resolution = this.#resolvedQuestionReplies.get(reference);
|
|
661
|
+
if (resolution?.key === key && resolution.expiresAt > now) return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async #discardResolvedInteractionReply(event, messageId) {
|
|
667
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
668
|
+
await this.#state.markSeen(messageId);
|
|
669
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
670
|
+
this.#status.messagesReceived += 1;
|
|
671
|
+
await this.#send(event.message.chat_id, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
#takePendingInteraction(key, interactionId) {
|
|
675
|
+
const pending = this.#pendingInteractions.get(key);
|
|
676
|
+
if (!pending
|
|
677
|
+
|| (interactionId !== undefined && pending.interactionId !== interactionId)) return null;
|
|
678
|
+
this.#pendingInteractions.delete(key);
|
|
679
|
+
this.#interactionKeys.delete(pending.interactionId);
|
|
680
|
+
pending.inactive = true;
|
|
681
|
+
return pending;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
#clearPendingInteraction(key, interactionId) {
|
|
685
|
+
return this.#takePendingInteraction(key, interactionId) !== null;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
async #cancelPendingInteraction(key) {
|
|
689
|
+
const pending = this.#takePendingInteraction(key);
|
|
690
|
+
if (!pending || pending.kind !== 'question') return;
|
|
691
|
+
this.#rememberResolvedInteraction(key, pending);
|
|
692
|
+
try {
|
|
693
|
+
await pending.interaction.respond({
|
|
694
|
+
ok: false,
|
|
695
|
+
error: {
|
|
696
|
+
code: 'cancelled',
|
|
697
|
+
message: 'The Feishu interaction ended before the user answered.',
|
|
698
|
+
details: {},
|
|
699
|
+
},
|
|
700
|
+
}, { signal: AbortSignal.timeout(5_000) });
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (error?.code !== 'interaction-not-pending') {
|
|
703
|
+
this.#logger.warn?.('[dsh-feishu] failed to cancel a pending Harness interaction');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
192
708
|
#progressText(update) {
|
|
193
709
|
if (update.type === 'text' && update.text) return update.text;
|
|
194
710
|
if (update.type === 'tool') {
|
|
@@ -206,12 +722,12 @@ export class FeishuHarnessBridge {
|
|
|
206
722
|
return reactionId;
|
|
207
723
|
} catch (error) {
|
|
208
724
|
this.#status.reactionErrors = (this.#status.reactionErrors ?? 0) + 1;
|
|
209
|
-
|
|
725
|
+
this.#logger.warn?.(`[dsh-feishu] unable to add ${emojiType} reaction:`, error.message);
|
|
210
726
|
return null;
|
|
211
727
|
}
|
|
212
728
|
}
|
|
213
729
|
|
|
214
|
-
async #
|
|
730
|
+
async #removeProcessingReaction(messageId, processingReaction) {
|
|
215
731
|
const reactionId = await processingReaction;
|
|
216
732
|
if (reactionId && this.#channel?.removeReaction) {
|
|
217
733
|
try {
|
|
@@ -219,9 +735,13 @@ export class FeishuHarnessBridge {
|
|
|
219
735
|
this.#status.reactionsRemoved = (this.#status.reactionsRemoved ?? 0) + 1;
|
|
220
736
|
} catch (error) {
|
|
221
737
|
this.#status.reactionErrors = (this.#status.reactionErrors ?? 0) + 1;
|
|
222
|
-
|
|
738
|
+
this.#logger.warn?.('[dsh-feishu] unable to remove processing reaction:', error.message);
|
|
223
739
|
}
|
|
224
740
|
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async #finishReaction(messageId, processingReaction, finalEmojiType) {
|
|
744
|
+
await this.#removeProcessingReaction(messageId, processingReaction);
|
|
225
745
|
await this.#addReaction(messageId, finalEmojiType);
|
|
226
746
|
}
|
|
227
747
|
|
|
@@ -237,5 +757,6 @@ export class FeishuHarnessBridge {
|
|
|
237
757
|
if (response?.code && response.code !== 0) {
|
|
238
758
|
throw new Error(`Feishu send failed: ${response.msg || response.code}`);
|
|
239
759
|
}
|
|
760
|
+
return nonEmptyString(response?.data?.message_id);
|
|
240
761
|
}
|
|
241
762
|
}
|