@wu529778790/open-im 1.10.7 → 1.10.8-beta.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/dist/logger.js CHANGED
@@ -4,7 +4,7 @@ import { finished } from 'node:stream/promises';
4
4
  import { sanitize } from './sanitize.js';
5
5
  import { APP_HOME } from './constants.js';
6
6
  import { sanitizeTelemetryData } from './telemetry/telemetry-sanitize.js';
7
- import { enqueueTelemetryLine, initTelemetryUpload, shutdownTelemetryUpload, } from './telemetry/telemetry-upload.js';
7
+ import { enqueueTelemetryLine, getTelemetryUploadStats, initTelemetryUpload, shutdownTelemetryUpload, } from './telemetry/telemetry-upload.js';
8
8
  const DEFAULT_LOG_DIR = join(APP_HOME, 'logs');
9
9
  const MAX_LOG_FILES = 10;
10
10
  const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
@@ -13,6 +13,9 @@ let minLevel = LOG_LEVELS.DEBUG;
13
13
  let logStream;
14
14
  let eventsStream;
15
15
  let telemetryEnabled = false;
16
+ let telemetryStatsTimer = null;
17
+ let lastTelemetryStatsSignature = '';
18
+ const TELEMETRY_STATS_INTERVAL_MS = 5 * 60_000;
16
19
  function pad(n) {
17
20
  return String(n).padStart(2, '0');
18
21
  }
@@ -75,6 +78,11 @@ export function initLogger(dirOrOpts, level, telemetry) {
75
78
  rotateOldLogs();
76
79
  logStream = createWriteStream(join(logDir, getLogFileName()), { flags: 'a' });
77
80
  telemetryEnabled = !!tel?.enabled;
81
+ if (telemetryStatsTimer) {
82
+ clearInterval(telemetryStatsTimer);
83
+ telemetryStatsTimer = null;
84
+ }
85
+ lastTelemetryStatsSignature = '';
78
86
  if (eventsStream) {
79
87
  eventsStream.end();
80
88
  eventsStream = undefined;
@@ -87,11 +95,24 @@ export function initLogger(dirOrOpts, level, telemetry) {
87
95
  url: tel?.url,
88
96
  token: tel?.token,
89
97
  });
98
+ telemetryStatsTimer = setInterval(() => {
99
+ emitTelemetryUploadStats(false);
100
+ }, TELEMETRY_STATS_INTERVAL_MS);
90
101
  }
91
102
  else {
92
103
  initTelemetryUpload({ enabled: false });
93
104
  }
94
105
  }
106
+ function emitTelemetryUploadStats(force) {
107
+ if (!telemetryEnabled)
108
+ return;
109
+ const stats = getTelemetryUploadStats();
110
+ const signature = JSON.stringify(stats);
111
+ if (!force && signature === lastTelemetryStatsSignature)
112
+ return;
113
+ lastTelemetryStatsSignature = signature;
114
+ emitStructuredEvent('Telemetry', 'telemetry.upload.stats', stats);
115
+ }
95
116
  function write(level, tag, msg, ...args) {
96
117
  if (LOG_LEVELS[level] < minLevel)
97
118
  return;
@@ -131,6 +152,11 @@ export function emitStructuredEvent(tag, event, data, level = 'INFO', msg = '')
131
152
  enqueueTelemetryLine(line);
132
153
  }
133
154
  export async function shutdownLoggerTelemetry() {
155
+ emitTelemetryUploadStats(true);
156
+ if (telemetryStatsTimer) {
157
+ clearInterval(telemetryStatsTimer);
158
+ telemetryStatsTimer = null;
159
+ }
134
160
  await shutdownTelemetryUpload();
135
161
  }
136
162
  export async function closeLogger() {
@@ -10,6 +10,29 @@ const log = createLogger('AITask');
10
10
  function isUsageLimitError(error) {
11
11
  return /usage limit/i.test(error) || /try again at\s+\d{1,2}:\d{2}\s*(AM|PM)/i.test(error);
12
12
  }
13
+ function classifyErrorType(error) {
14
+ const s = error.toLowerCase();
15
+ if (s.includes('aborted'))
16
+ return 'aborted';
17
+ if (isUsageLimitError(error) || s.includes('rate limit') || s.includes('quota'))
18
+ return 'limit';
19
+ if (s.includes('invalid api key') || s.includes('unauthorized') || s.includes('401'))
20
+ return 'auth';
21
+ if (s.includes('model') && (s.includes('not support') || s.includes('not found') || s.includes('invalid'))) {
22
+ return 'model';
23
+ }
24
+ if (s.includes('process exited') || s.includes('exit code'))
25
+ return 'process';
26
+ if (s.includes('timeout') ||
27
+ s.includes('etimedout') ||
28
+ s.includes('econnreset') ||
29
+ s.includes('enotfound') ||
30
+ s.includes('eai_again') ||
31
+ s.includes('network')) {
32
+ return 'network';
33
+ }
34
+ return 'unknown';
35
+ }
13
36
  function buildCompletionNote(result, sessionManager, ctx) {
14
37
  const toolInfo = formatToolStats(result.toolStats, result.numTurns);
15
38
  const parts = [];
@@ -224,6 +247,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
224
247
  toolId: aiCommand,
225
248
  durationMs: Date.now() - taskState.startedAt,
226
249
  errorSnippet: sanitize(String(error).slice(0, 400)),
250
+ errorType: classifyErrorType(String(error)),
227
251
  });
228
252
  if (isUsageLimitError(error)) {
229
253
  // Usage limit errors: keep session for all tools (user can retry later)
@@ -261,6 +285,17 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
261
285
  taskState = {
262
286
  handle: {
263
287
  abort: () => {
288
+ if (!settled) {
289
+ emitStructuredEvent('AITask', 'ai.task.error', {
290
+ platform: ctx.platform,
291
+ taskKey: ctx.taskKey,
292
+ userKey: hashUserId(ctx.userId),
293
+ toolId: aiCommand,
294
+ durationMs: Date.now() - taskState.startedAt,
295
+ errorSnippet: 'aborted',
296
+ errorType: 'aborted',
297
+ });
298
+ }
264
299
  activeHandle?.abort();
265
300
  cleanup();
266
301
  settle();
@@ -286,6 +321,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
286
321
  toolId: aiCommand,
287
322
  durationMs: 0,
288
323
  errorSnippet: sanitize(String(err).slice(0, 400)),
324
+ errorType: classifyErrorType(String(err)),
289
325
  });
290
326
  platformAdapter
291
327
  .sendError(`内部错误:${err instanceof Error ? err.message : String(err)}`)
@@ -1,3 +1,11 @@
1
+ interface TelemetryUploadStats {
2
+ postedBatches: number;
3
+ postedLines: number;
4
+ retryableFailures: number;
5
+ dropped4xxBatches: number;
6
+ dropped4xxLines: number;
7
+ networkFailures: number;
8
+ }
1
9
  export declare function initTelemetryUpload(opts: {
2
10
  enabled: boolean;
3
11
  url?: string;
@@ -9,3 +17,5 @@ export declare function initTelemetryUpload(opts: {
9
17
  */
10
18
  export declare function enqueueTelemetryLine(line: string): void;
11
19
  export declare function shutdownTelemetryUpload(): Promise<void>;
20
+ export declare function getTelemetryUploadStats(): Readonly<TelemetryUploadStats>;
21
+ export {};
@@ -18,6 +18,22 @@ let uploadEnabled = false;
18
18
  let endpoint;
19
19
  let bearer;
20
20
  let flushing = false;
21
+ const stats = {
22
+ postedBatches: 0,
23
+ postedLines: 0,
24
+ retryableFailures: 0,
25
+ dropped4xxBatches: 0,
26
+ dropped4xxLines: 0,
27
+ networkFailures: 0,
28
+ };
29
+ function resetStats() {
30
+ stats.postedBatches = 0;
31
+ stats.postedLines = 0;
32
+ stats.retryableFailures = 0;
33
+ stats.dropped4xxBatches = 0;
34
+ stats.dropped4xxLines = 0;
35
+ stats.networkFailures = 0;
36
+ }
21
37
  function clearIdleTimer() {
22
38
  if (idleTimer) {
23
39
  clearTimeout(idleTimer);
@@ -58,9 +74,22 @@ async function postBatch(lines) {
58
74
  catch {
59
75
  /* ignore body read errors */
60
76
  }
61
- return res.ok;
77
+ if (res.ok) {
78
+ stats.postedBatches += 1;
79
+ stats.postedLines += lines.length;
80
+ return true;
81
+ }
82
+ // 4xx(除 408/429)通常是请求本身不可恢复(鉴权/格式错误),不应无限重试。
83
+ if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
84
+ stats.dropped4xxBatches += 1;
85
+ stats.dropped4xxLines += lines.length;
86
+ return true;
87
+ }
88
+ stats.retryableFailures += 1;
89
+ return false;
62
90
  }
63
91
  catch {
92
+ stats.networkFailures += 1;
64
93
  return false;
65
94
  }
66
95
  }
@@ -116,6 +145,7 @@ export function initTelemetryUpload(opts) {
116
145
  endpoint = opts.url;
117
146
  bearer = opts.token;
118
147
  backoffMs = INITIAL_BACKOFF_MS;
148
+ resetStats();
119
149
  if (!uploadEnabled) {
120
150
  queue = [];
121
151
  }
@@ -172,10 +202,32 @@ export async function shutdownTelemetryUpload() {
172
202
  if (br)
173
203
  headers.authorization = `Bearer ${br}`;
174
204
  const res = await fetch(ep, { method: 'POST', headers, body });
175
- await res.text().catch(() => { });
205
+ try {
206
+ if (typeof res.text === 'function') {
207
+ await res.text();
208
+ }
209
+ }
210
+ catch {
211
+ /* ignore body read errors */
212
+ }
213
+ if (res.ok) {
214
+ stats.postedBatches += 1;
215
+ stats.postedLines += batch.length;
216
+ }
217
+ else if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429) {
218
+ stats.dropped4xxBatches += 1;
219
+ stats.dropped4xxLines += batch.length;
220
+ }
221
+ else {
222
+ stats.retryableFailures += 1;
223
+ }
176
224
  }
177
225
  catch {
226
+ stats.networkFailures += 1;
178
227
  /* best effort,静默 */
179
228
  }
180
229
  }
181
230
  }
231
+ export function getTelemetryUploadStats() {
232
+ return { ...stats };
233
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.10.7",
3
+ "version": "1.10.8-beta.1",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -28,6 +28,7 @@
28
28
  "lint": "eslint src",
29
29
  "web:build": "npm --prefix web run build",
30
30
  "web:dev": "npm --prefix web run dev",
31
+ "telemetry:report": "node scripts/telemetry-health-report.mjs",
31
32
  "prepublishOnly": "npm run build"
32
33
  },
33
34
  "keywords": [