@wu529778790/open-im 1.11.1-beta.0 → 1.11.1-beta.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.
@@ -21,6 +21,7 @@ let channelState = 'disconnected';
21
21
  let messageHandler = null;
22
22
  let stateChangeHandler = null;
23
23
  let reconnectTimer = null;
24
+ let watchdogTimer = null;
24
25
  let reconnectAttempt = 0;
25
26
  let fatal = false;
26
27
  let stopped = false;
@@ -28,6 +29,13 @@ let apiUrl = 'https://ilinkai.weixin.qq.com';
28
29
  let apiToken = '';
29
30
  /** Opaque cursor for getupdates pagination (replaces numeric offset) */
30
31
  let getUpdatesBuf = '';
32
+ /** Timestamp of last successful poll response (for watchdog) */
33
+ let lastResponseAt = 0;
34
+ /** Per-request timeout for long-polling (ms) */
35
+ const POLL_REQUEST_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
36
+ /** Watchdog interval: force reconnect if no response for this long (ms) */
37
+ const WATCHDOG_INTERVAL_MS = 60_000; // check every 60s
38
+ const WATCHDOG_STALE_MS = 5 * 60 * 1000; // 5 minutes without response = stale
31
39
  export function getChannelState() {
32
40
  return channelState;
33
41
  }
@@ -58,14 +66,21 @@ function startPolling() {
58
66
  return;
59
67
  pollController = new AbortController();
60
68
  const signal = pollController.signal;
69
+ // Start watchdog to detect stale connections (e.g. Mac sleep / network drop)
70
+ startWatchdog();
61
71
  (async () => {
62
72
  log.info('ClawBot long-polling started');
63
73
  while (!stopped && !signal.aborted) {
64
74
  try {
75
+ // Combine the poll controller signal with a per-request timeout
76
+ const timeoutSignal = AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS);
77
+ const combinedSignal = AbortSignal.any([signal, timeoutSignal]);
65
78
  const res = await postApi('/ilink/bot/getupdates', {
66
79
  get_updates_buf: getUpdatesBuf,
67
80
  base_info: BASE_INFO,
68
- }, signal);
81
+ }, combinedSignal);
82
+ // Record successful response time for watchdog
83
+ lastResponseAt = Date.now();
69
84
  if (signal.aborted)
70
85
  break;
71
86
  if (!res.ok) {
@@ -168,6 +183,35 @@ function updateState(state) {
168
183
  stateChangeHandler?.(state);
169
184
  log.debug(`ClawBot state: ${state}`);
170
185
  }
186
+ /**
187
+ * Watchdog: periodically check if the poll loop is alive.
188
+ * After Mac sleep or network drop, the fetch may hang without throwing.
189
+ * If no successful response for WATCHDOG_STALE_MS, force a reconnect.
190
+ */
191
+ function startWatchdog() {
192
+ stopWatchdog();
193
+ lastResponseAt = Date.now();
194
+ watchdogTimer = setInterval(() => {
195
+ if (stopped)
196
+ return;
197
+ const elapsed = Date.now() - lastResponseAt;
198
+ if (elapsed > WATCHDOG_STALE_MS) {
199
+ log.warn(`ClawBot watchdog: no response for ${Math.round(elapsed / 1000)}s, forcing reconnect`);
200
+ if (pollController) {
201
+ pollController.abort();
202
+ pollController = null;
203
+ }
204
+ updateState('error');
205
+ scheduleReconnect();
206
+ }
207
+ }, WATCHDOG_INTERVAL_MS);
208
+ }
209
+ function stopWatchdog() {
210
+ if (watchdogTimer) {
211
+ clearInterval(watchdogTimer);
212
+ watchdogTimer = null;
213
+ }
214
+ }
171
215
  /**
172
216
  * Extract text content from an iLink message's item_list.
173
217
  * Returns the first text item found, or a placeholder for media types.
@@ -272,6 +316,7 @@ export function stopClawbot() {
272
316
  clearTimeout(reconnectTimer);
273
317
  reconnectTimer = null;
274
318
  }
319
+ stopWatchdog();
275
320
  messageHandler = null;
276
321
  // Don't clear context_token here — it's persisted for startup notifications
277
322
  updateState('disconnected');
package/dist/index.js CHANGED
@@ -35,7 +35,7 @@ import { destroyAllLiveChildren } from "./shared/process-kill.js";
35
35
  import { initLogger, createLogger, closeLogger, emitStructuredEvent, shutdownLoggerTelemetry, } from "./logger.js";
36
36
  import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
37
37
  import { createRequire } from "node:module";
38
- import { escapePathForMarkdown } from "./shared/utils.js";
38
+ import { escapePathForMarkdown, getAIToolDisplayName } from "./shared/utils.js";
39
39
  import { applyOpenImGitCoauthorToProcessEnv } from "./shared/git-coauthor.js";
40
40
  import { emitInterruptedTerminals } from "./shared/task-cleanup.js";
41
41
  const require = createRequire(import.meta.url);
@@ -127,6 +127,15 @@ async function sendLifecycleNotification(platform, message) {
127
127
  await mod.sendNotification(chatId, message);
128
128
  log.info(`[${platform}] Lifecycle notification sent successfully`);
129
129
  }
130
+ const PLATFORM_DISPLAY_NAMES = {
131
+ telegram: 'Telegram',
132
+ feishu: '飞书',
133
+ wework: '企业微信',
134
+ dingtalk: '钉钉',
135
+ qq: 'QQ',
136
+ workbuddy: '微信',
137
+ clawbot: '微信 (ClawBot)',
138
+ };
130
139
  function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, sessionManager) {
131
140
  let sessionDir;
132
141
  // Telegram 私聊、企业微信当前实现里,活跃 chatId 可直接对应到 session userId。
@@ -136,27 +145,23 @@ function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, se
136
145
  sessionDir = sessionManager.getWorkDir(activeChatId);
137
146
  }
138
147
  }
139
- const lines = [
140
- `**服务已启动**`,
148
+ const platformName = PLATFORM_DISPLAY_NAMES[platform] ?? platform;
149
+ const toolName = getAIToolDisplayName(aiCommand);
150
+ const dir = sessionDir ? escapePathForMarkdown(sessionDir) : '发送 `/pwd` 查看';
151
+ return [
152
+ `✅ open-im v${appVersion} 已就绪`,
141
153
  "",
142
- `- 版本: \`open-im v${appVersion}\``,
143
- `- 当前渠道: \`${platform}\``,
144
- `- 当前渠道 CLI: \`${aiCommand}\``,
145
- ];
146
- if (sessionDir) {
147
- lines.push(`- 会话目录: ${escapePathForMarkdown(sessionDir)}`);
148
- }
149
- else {
150
- lines.push(`- 会话目录: 发送 \`/pwd\` 查看`);
151
- }
152
- return lines.join("\n");
154
+ `📱 平台: ${platformName}`,
155
+ `🤖 AI: ${toolName}`,
156
+ `📁 目录: ${dir}`,
157
+ ].join("\n");
153
158
  }
154
159
  function buildShutdownMessage(uptimeMinutes) {
160
+ const uptime = uptimeMinutes < 1 ? '< 1' : String(uptimeMinutes);
155
161
  return [
156
- `**服务正在关闭**`,
162
+ `🛑 open-im 正在关闭`,
157
163
  "",
158
- `- 服务: \`open-im\``,
159
- `- 运行时长: \`${uptimeMinutes} 分钟\``,
164
+ `⏱️ 运行时长: ${uptime} 分钟`,
160
165
  ].join("\n");
161
166
  }
162
167
  export async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.1-beta.0",
3
+ "version": "1.11.1-beta.2",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",