@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmanrui/dsh-im",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
4
4
  "description": "把九种 IM 机器人和公网 AI Office 接入本机 DeepSeek Harness。 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -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?.messageId ? [stream.messageId] : [],
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.slice(0, 49)}…`;
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
- let messageId = null;
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
- messageId = await this.#sendCard(chatId, cardId, options.replyTo);
189
- let sequence = 0;
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
- if (next === lastContent) return;
196
- if (next.length > MAX_STREAM_CHARS) {
197
- throw new Error(`Feishu stream content exceeds ${MAX_STREAM_CHARS} characters`);
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 finishResponse = await this.#client.cardkit.v1.card.settings({
214
- path: { card_id: cardId },
215
- data: {
216
- settings: JSON.stringify({
217
- config: {
218
- streaming_mode: false,
219
- summary: { content: summaryOf(lastContent) || t('回答完成') },
220
- },
221
- }),
222
- sequence: ++sequence,
223
- uuid: `settings_${cardId}_${sequence}`,
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
- if (messageId) await this.#recall(messageId);
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