@wu529778790/open-im 1.5.3 → 1.5.4

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.
@@ -8,6 +8,8 @@ import { splitLongContent, getAIToolDisplayName } from '../shared/utils.js';
8
8
  import { MAX_WEWORK_MESSAGE_LENGTH } from '../constants.js';
9
9
  import { randomBytes } from 'node:crypto';
10
10
  const log = createLogger('WeWorkSender');
11
+ const STREAM_SEND_INTERVAL_MS = 900;
12
+ const STREAM_SAFE_TTL_MS = 5 * 60 * 1000;
11
13
  /** 当前同步处理中的 req_id(仅用于 commandHandler 等同步调用) */
12
14
  let currentReqId = null;
13
15
  export function setCurrentReqId(reqId) {
@@ -61,11 +63,61 @@ function formatWeWorkMessage(title, content, status, note) {
61
63
  }
62
64
  return message;
63
65
  }
64
- /**
65
- * Local tracking for stream states
66
- * WeWork doesn't support message editing, so we track stream IDs locally
67
- */
68
66
  const streamStates = new Map();
67
+ function sleep(ms) {
68
+ return new Promise((resolve) => setTimeout(resolve, ms));
69
+ }
70
+ function getOrCreateStreamState(streamId, chatId) {
71
+ const existing = streamStates.get(streamId);
72
+ if (existing)
73
+ return existing;
74
+ const state = {
75
+ chatId,
76
+ content: '',
77
+ createdAt: Date.now(),
78
+ lastSentAt: 0,
79
+ closed: false,
80
+ expired: false,
81
+ flushing: false,
82
+ expireLogged: false,
83
+ };
84
+ streamStates.set(streamId, state);
85
+ return state;
86
+ }
87
+ function markExpired(state, streamId) {
88
+ state.expired = true;
89
+ if (!state.expireLogged) {
90
+ state.expireLogged = true;
91
+ log.warn(`Stream expired locally, switching to text fallback: streamId=${streamId}`);
92
+ }
93
+ }
94
+ async function flushStreamUpdate(streamId, state) {
95
+ if (state.flushing || state.closed || state.expired)
96
+ return;
97
+ state.flushing = true;
98
+ try {
99
+ while (state.pendingUpdate && !state.closed && !state.expired) {
100
+ const queued = state.pendingUpdate;
101
+ state.pendingUpdate = undefined;
102
+ if (Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS) {
103
+ markExpired(state, streamId);
104
+ break;
105
+ }
106
+ const elapsed = Date.now() - state.lastSentAt;
107
+ if (elapsed < STREAM_SEND_INTERVAL_MS) {
108
+ await sleep(STREAM_SEND_INTERVAL_MS - elapsed);
109
+ }
110
+ if (state.closed || state.expired)
111
+ break;
112
+ sendStream(getReqId(queued.reqId), streamId, queued.message, false);
113
+ state.lastSentAt = Date.now();
114
+ log.info(`Message updated: ${queued.status}, streamId=${streamId}`);
115
+ }
116
+ }
117
+ finally {
118
+ state.flushing = false;
119
+ }
120
+ }
69
121
  /**
70
122
  * Send thinking message to WeWork
71
123
  * Returns a stream ID that can be used for updates
@@ -78,7 +130,7 @@ export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'c
78
130
  try {
79
131
  log.info(`Sending thinking message to user ${chatId}, streamId=${streamId}`);
80
132
  // Store initial stream state
81
- streamStates.set(streamId, { content: '', chatId });
133
+ getOrCreateStreamState(streamId, chatId);
82
134
  // Send initial stream message (not finished)
83
135
  sendStream(getReqId(reqId), streamId, content, false);
84
136
  log.info(`Thinking message sent: ${streamId}`);
@@ -96,12 +148,18 @@ export async function sendThinkingMessage(chatId, _replyToMessageId, toolId = 'c
96
148
  export async function updateMessage(chatId, streamId, content, status, note, toolId = 'claude', reqId) {
97
149
  const title = getToolTitle(toolId, status);
98
150
  const message = formatWeWorkMessage(title, content, status, note);
151
+ const state = getOrCreateStreamState(streamId, chatId);
99
152
  try {
100
- // Update stream state
101
- streamStates.set(streamId, { content, chatId });
102
- // Send stream update (not finished yet)
103
- sendStream(getReqId(reqId), streamId, message, false);
104
- log.info(`Message updated: ${status}, streamId=${streamId}`);
153
+ state.chatId = chatId;
154
+ state.content = content;
155
+ if (state.closed)
156
+ return;
157
+ if (Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS) {
158
+ markExpired(state, streamId);
159
+ return;
160
+ }
161
+ state.pendingUpdate = { message, status, reqId };
162
+ await flushStreamUpdate(streamId, state);
105
163
  }
106
164
  catch (err) {
107
165
  log.error('Failed to update message:', err);
@@ -117,9 +175,26 @@ export async function sendFinalMessages(chatId, streamId, fullContent, note, too
117
175
  // Send final stream message to finish the stream
118
176
  const finalMessage = formatWeWorkMessage(title, parts[0], 'done', parts.length > 1 ? `内容较长,已分段发送 (1/${parts.length})` : note);
119
177
  try {
120
- sendStream(getReqId(reqId), streamId, finalMessage, true);
121
- log.info(`Final stream message sent, streamId=${streamId}`);
122
- // Clean up stream state
178
+ const state = streamStates.get(streamId);
179
+ const shouldFallbackToText = !!state && (state.expired || Date.now() - state.createdAt >= STREAM_SAFE_TTL_MS);
180
+ if (state) {
181
+ state.closed = true;
182
+ state.pendingUpdate = undefined;
183
+ }
184
+ if (!shouldFallbackToText) {
185
+ if (state) {
186
+ const elapsed = Date.now() - state.lastSentAt;
187
+ if (elapsed < STREAM_SEND_INTERVAL_MS) {
188
+ await sleep(STREAM_SEND_INTERVAL_MS - elapsed);
189
+ }
190
+ }
191
+ sendStream(getReqId(reqId), streamId, finalMessage, true);
192
+ log.info(`Final stream message sent, streamId=${streamId}`);
193
+ }
194
+ else {
195
+ sendText(getReqId(reqId), finalMessage);
196
+ log.info(`Final stream expired, sent text fallback instead: streamId=${streamId}`);
197
+ }
123
198
  streamStates.delete(streamId);
124
199
  // Send remaining parts as separate messages
125
200
  for (let i = 1; i < parts.length; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",