@xmanrui/dsh-im 3.1.0 → 3.1.1
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/client.js +1 -1
- package/lib/index.js +195 -192
- package/package.json +1 -1
- package/src/channels/feishu/bridge.mjs +2 -1
- package/src/channels/feishu/feishu-channel.mjs +100 -44
- package/src/channels/shared/i18n-en/feishu.mjs +2 -0
package/package.json
CHANGED
|
@@ -49,6 +49,7 @@ import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
|
49
49
|
import { deliverOutboundArtifacts } from '../shared/semantic/artifact-delivery.mjs';
|
|
50
50
|
import {
|
|
51
51
|
createDeliveryReceipt,
|
|
52
|
+
providerMessageIdsFor,
|
|
52
53
|
} from '../shared/semantic/delivery.mjs';
|
|
53
54
|
import {
|
|
54
55
|
channelDeliveryFailure,
|
|
@@ -3315,7 +3316,7 @@ export class FeishuHarnessBridge {
|
|
|
3315
3316
|
createDeliveryReceipt({
|
|
3316
3317
|
deliveryId: messageId,
|
|
3317
3318
|
presentation: 'feishu-cardkit',
|
|
3318
|
-
providerMessageIds: stream
|
|
3319
|
+
providerMessageIds: providerMessageIdsFor(stream),
|
|
3319
3320
|
}),
|
|
3320
3321
|
);
|
|
3321
3322
|
this.#status.streamResponses = (this.#status.streamResponses ?? 0) + 1;
|
|
@@ -126,7 +126,37 @@ function deliveryUuid(file, chatId, messageType) {
|
|
|
126
126
|
|
|
127
127
|
function summaryOf(text) {
|
|
128
128
|
const summary = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
129
|
-
return summary.length <= 50 ? summary : `${summary
|
|
129
|
+
return summary.length <= 50 ? summary : `${streamTextPrefix(summary, 49)}…`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function streamTextPrefix(text, maxChars) {
|
|
133
|
+
let end = Math.min(text.length, maxChars);
|
|
134
|
+
// Keep a UTF-16 surrogate pair together when the limit lands inside an emoji.
|
|
135
|
+
const before = text.charCodeAt(end - 1);
|
|
136
|
+
const after = text.charCodeAt(end);
|
|
137
|
+
if (before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff) end -= 1;
|
|
138
|
+
return text.slice(0, end);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function streamPreview(text) {
|
|
142
|
+
if (text.length <= MAX_STREAM_CHARS) return text;
|
|
143
|
+
const notice = `\n\n${t('内容较长,生成完成后将分段发送完整回答。')}`;
|
|
144
|
+
return streamTextPrefix(text, MAX_STREAM_CHARS - notice.length) + notice;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function splitStreamContent(text) {
|
|
148
|
+
const chunks = [];
|
|
149
|
+
let remaining = text;
|
|
150
|
+
while (remaining.length > MAX_STREAM_CHARS) {
|
|
151
|
+
const prefix = streamTextPrefix(remaining, MAX_STREAM_CHARS);
|
|
152
|
+
let end = prefix.lastIndexOf('\n') + 1;
|
|
153
|
+
if (end < MAX_STREAM_CHARS * 0.6) end = prefix.length;
|
|
154
|
+
chunks.push(remaining.slice(0, end));
|
|
155
|
+
// Unlike plain-text fallback splitting, keep all whitespace in the answer.
|
|
156
|
+
remaining = remaining.slice(end);
|
|
157
|
+
}
|
|
158
|
+
if (remaining) chunks.push(remaining);
|
|
159
|
+
return chunks;
|
|
130
160
|
}
|
|
131
161
|
|
|
132
162
|
function streamingCard(initialText) {
|
|
@@ -164,7 +194,7 @@ export class VerifiedFeishuChannel {
|
|
|
164
194
|
fileMessageTimeoutMs = MAX_FILE_OPERATION_TIMEOUT_MS,
|
|
165
195
|
}) {
|
|
166
196
|
this.#client = client;
|
|
167
|
-
this.#initialText = initialText ?? t(DEFAULT_INITIAL_TEXT);
|
|
197
|
+
this.#initialText = String(initialText ?? t(DEFAULT_INITIAL_TEXT)) || '…';
|
|
168
198
|
this.#fileUploadTimeoutMs = boundedFileTimeout(fileUploadTimeoutMs, 'fileUploadTimeoutMs');
|
|
169
199
|
this.#fileMessageTimeoutMs = boundedFileTimeout(fileMessageTimeoutMs, 'fileMessageTimeoutMs');
|
|
170
200
|
}
|
|
@@ -174,59 +204,40 @@ export class VerifiedFeishuChannel {
|
|
|
174
204
|
throw new Error('Feishu stream requires a markdown producer');
|
|
175
205
|
}
|
|
176
206
|
|
|
177
|
-
|
|
178
|
-
const cardResponse = assertApiSuccess('Feishu card.create', await this.#client.cardkit.v1.card.create({
|
|
179
|
-
data: {
|
|
180
|
-
type: 'card_json',
|
|
181
|
-
data: JSON.stringify(streamingCard(this.#initialText)),
|
|
182
|
-
},
|
|
183
|
-
}));
|
|
184
|
-
const cardId = cardResponse?.data?.card_id;
|
|
185
|
-
if (!cardId) throw new Error('Feishu card.create returned no card_id');
|
|
186
|
-
|
|
207
|
+
const cards = [];
|
|
187
208
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
const firstCard = await this.#createStreamCard(chatId, options.replyTo);
|
|
210
|
+
cards.push(firstCard);
|
|
190
211
|
let lastContent = this.#initialText;
|
|
191
212
|
const controller = {
|
|
192
|
-
messageId,
|
|
213
|
+
messageId: firstCard.messageId,
|
|
193
214
|
setContent: async (content) => {
|
|
194
215
|
const next = String(content ?? '') || '…';
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
const response = await this.#client.cardkit.v1.cardElement.content({
|
|
200
|
-
path: { card_id: cardId, element_id: STREAM_ELEMENT_ID },
|
|
201
|
-
data: {
|
|
202
|
-
content: next,
|
|
203
|
-
sequence: ++sequence,
|
|
204
|
-
uuid: `content_${cardId}_${sequence}`,
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
assertApiSuccess('Feishu cardElement.content', response);
|
|
216
|
+
await this.#updateStreamCard(firstCard, streamPreview(next));
|
|
217
|
+
// Updates are replaceable snapshots, including progress/tool text.
|
|
218
|
+
// Retain the full latest snapshot even when its preview is unchanged.
|
|
208
219
|
lastContent = next;
|
|
209
220
|
},
|
|
210
221
|
};
|
|
211
222
|
|
|
212
223
|
await input.markdown(controller);
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
assertApiSuccess('Feishu card.settings', finishResponse);
|
|
227
|
-
return { messageId };
|
|
224
|
+
const chunks = splitStreamContent(lastContent);
|
|
225
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
226
|
+
const card = index === 0
|
|
227
|
+
? firstCard
|
|
228
|
+
: await this.#createStreamCard(chatId, options.replyTo);
|
|
229
|
+
if (index > 0) cards.push(card);
|
|
230
|
+
await this.#updateStreamCard(card, chunk);
|
|
231
|
+
await this.#finishStreamCard(card);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
messageId: firstCard.messageId,
|
|
235
|
+
providerMessageIds: cards.map((card) => card.messageId),
|
|
236
|
+
};
|
|
228
237
|
} catch (error) {
|
|
229
|
-
|
|
238
|
+
// Preserve the existing provider-error fallback contract: the bridge
|
|
239
|
+
// resends the completed answer, so remove any cards it would duplicate.
|
|
240
|
+
for (const card of cards) await this.#recall(card.messageId);
|
|
230
241
|
throw error;
|
|
231
242
|
}
|
|
232
243
|
}
|
|
@@ -369,6 +380,51 @@ export class VerifiedFeishuChannel {
|
|
|
369
380
|
});
|
|
370
381
|
}
|
|
371
382
|
|
|
383
|
+
async #createStreamCard(chatId, replyTo) {
|
|
384
|
+
const content = streamPreview(this.#initialText);
|
|
385
|
+
const response = assertApiSuccess('Feishu card.create', await this.#client.cardkit.v1.card.create({
|
|
386
|
+
data: {
|
|
387
|
+
type: 'card_json',
|
|
388
|
+
data: JSON.stringify(streamingCard(content)),
|
|
389
|
+
},
|
|
390
|
+
}));
|
|
391
|
+
const cardId = response?.data?.card_id;
|
|
392
|
+
if (!cardId) throw new Error('Feishu card.create returned no card_id');
|
|
393
|
+
const messageId = await this.#sendCard(chatId, cardId, replyTo);
|
|
394
|
+
return { cardId, messageId, content, sequence: 0 };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async #updateStreamCard(card, content) {
|
|
398
|
+
if (content === card.content) return;
|
|
399
|
+
const response = await this.#client.cardkit.v1.cardElement.content({
|
|
400
|
+
path: { card_id: card.cardId, element_id: STREAM_ELEMENT_ID },
|
|
401
|
+
data: {
|
|
402
|
+
content,
|
|
403
|
+
sequence: ++card.sequence,
|
|
404
|
+
uuid: `content_${card.cardId}_${card.sequence}`,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
assertApiSuccess('Feishu cardElement.content', response);
|
|
408
|
+
card.content = content;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async #finishStreamCard(card) {
|
|
412
|
+
const response = await this.#client.cardkit.v1.card.settings({
|
|
413
|
+
path: { card_id: card.cardId },
|
|
414
|
+
data: {
|
|
415
|
+
settings: JSON.stringify({
|
|
416
|
+
config: {
|
|
417
|
+
streaming_mode: false,
|
|
418
|
+
summary: { content: summaryOf(card.content) || t('回答完成') },
|
|
419
|
+
},
|
|
420
|
+
}),
|
|
421
|
+
sequence: ++card.sequence,
|
|
422
|
+
uuid: `settings_${card.cardId}_${card.sequence}`,
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
assertApiSuccess('Feishu card.settings', response);
|
|
426
|
+
}
|
|
427
|
+
|
|
372
428
|
async #sendCard(chatId, cardId, replyTo) {
|
|
373
429
|
const content = JSON.stringify({ type: 'card', data: { card_id: cardId } });
|
|
374
430
|
const response = replyTo
|
|
@@ -335,6 +335,8 @@ export default {
|
|
|
335
335
|
// feishu/feishu-channel.mjs
|
|
336
336
|
'正在生成…': 'Generating…',
|
|
337
337
|
'回答完成': 'Answer complete',
|
|
338
|
+
'内容较长,生成完成后将分段发送完整回答。':
|
|
339
|
+
'This response is long. The complete answer will be sent in parts when generation finishes.',
|
|
338
340
|
'飞书机器人': 'Feishu bot',
|
|
339
341
|
|
|
340
342
|
// feishu/message-utils.mjs
|