@wu529778790/open-im 1.11.1-beta.17 → 1.11.1-beta.19

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.
@@ -10,5 +10,5 @@
10
10
  import type { Config } from '../config.js';
11
11
  import type { ClawBotState } from './types.js';
12
12
  export declare function getChannelState(): ClawBotState;
13
- export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
13
+ export declare function initClawbot(config: Config, eventHandler: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>, onStateChange?: (state: ClawBotState) => void): Promise<void>;
14
14
  export declare function stopClawbot(): void;
@@ -12,6 +12,7 @@ import { createLogger } from '../logger.js';
12
12
  import { jitteredDelay, isFatalReconnectError, SLOW_PROBE_MS } from '../shared/reconnect.js';
13
13
  import { cacheContextToken } from './message-sender.js';
14
14
  import { setClawbotContextToken, clearClawbotContextToken } from '../shared/active-chats.js';
15
+ import { decryptAes256CbcMedia, saveBufferMedia, createMediaTargetPath } from '../shared/media-storage.js';
15
16
  import { CLAWBOT_POLL_INTERVAL_MS } from '../constants.js';
16
17
  const log = createLogger('ClawBot');
17
18
  const RECONNECT_DELAYS_MS = [3000, 5000, 10000, 20000, 30000];
@@ -110,6 +111,8 @@ function startPolling() {
110
111
  }
111
112
  // Process messages
112
113
  const messages = res.messages ?? [];
114
+ // Step 1: Extract valid USER messages and cache context tokens
115
+ const userMessages = [];
113
116
  for (const msg of messages) {
114
117
  if (signal.aborted)
115
118
  break;
@@ -120,7 +123,6 @@ function startPolling() {
120
123
  continue;
121
124
  const chatId = msg.from_user_id ?? '';
122
125
  const msgId = String(msg.message_id ?? msg.seq ?? '');
123
- const content = extracted;
124
126
  if (!chatId) {
125
127
  log.warn('ClawBot message missing from_user_id, skipping');
126
128
  continue;
@@ -130,10 +132,34 @@ function startPolling() {
130
132
  cacheContextToken(chatId, msg.context_token);
131
133
  setClawbotContextToken(msg.context_token);
132
134
  }
133
- log.info(`ClawBot message: chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
135
+ // Extract and download images from message
136
+ const imagePaths = await extractImages(msg);
137
+ userMessages.push({ chatId, msgId, content: extracted, imagePaths: imagePaths.length > 0 ? imagePaths : undefined });
138
+ }
139
+ // Step 2: Aggregate consecutive messages from the same user
140
+ // ClawBot splits image+text into separate messages; combine them
141
+ const aggregated = [];
142
+ for (const m of userMessages) {
143
+ const last = aggregated[aggregated.length - 1];
144
+ if (last && last.chatId === m.chatId) {
145
+ // Same user — merge content and image paths
146
+ last.content = `${last.content}\n${m.content}`;
147
+ if (m.imagePaths?.length) {
148
+ last.imagePaths = [...(last.imagePaths ?? []), ...m.imagePaths];
149
+ }
150
+ }
151
+ else {
152
+ aggregated.push({ chatId: m.chatId, msgId: m.msgId, content: m.content, imagePaths: m.imagePaths });
153
+ }
154
+ }
155
+ // Step 3: Dispatch aggregated messages
156
+ for (const m of aggregated) {
157
+ if (signal.aborted)
158
+ break;
159
+ log.info(`ClawBot message: chatId=${m.chatId}, msgId=${m.msgId}, content="${m.content.substring(0, 100)}", images=${m.imagePaths?.length ?? 0}`);
134
160
  if (messageHandler) {
135
161
  try {
136
- await messageHandler(chatId, msgId, content);
162
+ await messageHandler(m.chatId, m.msgId, m.content, m.imagePaths);
137
163
  }
138
164
  catch (err) {
139
165
  log.error('Error in ClawBot message handler:', err);
@@ -212,6 +238,48 @@ function stopWatchdog() {
212
238
  watchdogTimer = null;
213
239
  }
214
240
  }
241
+ /**
242
+ * Extract and download images from a ClawBot message's item_list.
243
+ * Images are encrypted with AES and hosted on CDN.
244
+ */
245
+ async function extractImages(msg) {
246
+ if (!msg.item_list?.length)
247
+ return [];
248
+ const paths = [];
249
+ for (const item of msg.item_list) {
250
+ if (item.type !== 2 /* MessageItemType.IMAGE */)
251
+ continue;
252
+ const media = item.image_item?.media;
253
+ if (!media?.cdn_url)
254
+ continue;
255
+ try {
256
+ // Download from CDN
257
+ const response = await fetch(media.cdn_url, { signal: AbortSignal.timeout(30_000) });
258
+ if (!response.ok) {
259
+ log.warn(`Image download failed: HTTP ${response.status}`);
260
+ continue;
261
+ }
262
+ const buffer = Buffer.from(await response.arrayBuffer());
263
+ // Decrypt if AES key provided
264
+ let decrypted;
265
+ if (media.aes_key) {
266
+ decrypted = decryptAes256CbcMedia(buffer, media.aes_key);
267
+ }
268
+ else {
269
+ decrypted = buffer;
270
+ }
271
+ // Save to disk
272
+ const targetPath = createMediaTargetPath('.jpg', `clawbot-${Date.now()}`);
273
+ await saveBufferMedia(decrypted, targetPath);
274
+ paths.push(targetPath);
275
+ log.info(`ClawBot image saved: ${targetPath}`);
276
+ }
277
+ catch (err) {
278
+ log.warn('Failed to process ClawBot image:', err);
279
+ }
280
+ }
281
+ return paths;
282
+ }
215
283
  /**
216
284
  * Extract text content from an iLink message's item_list.
217
285
  * Returns the first text item found, or a placeholder for media types.
@@ -7,6 +7,6 @@ export interface ClawBotEventHandlerHandle {
7
7
  stop: () => void;
8
8
  runningTasks: Map<string, import('../shared/ai-task.js').TaskRunState>;
9
9
  getRunningTaskCount: () => number;
10
- handleEvent: (chatId: string, msgId: string, content: string) => Promise<void>;
10
+ handleEvent: (chatId: string, msgId: string, content: string, imagePaths?: string[]) => Promise<void>;
11
11
  }
12
12
  export declare function setupClawbotHandlers(config: Config, sessionManager: SessionManager): ClawBotEventHandlerHandle;
@@ -33,8 +33,8 @@ export function setupClawbotHandlers(config, sessionManager) {
33
33
  return () => { };
34
34
  },
35
35
  };
36
- async function handleEvent(chatId, msgId, content) {
37
- log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}"`);
36
+ async function handleEvent(chatId, msgId, content, imagePaths) {
37
+ log.info(`[handleEvent] chatId=${chatId}, msgId=${msgId}, content="${content.substring(0, 100)}", images=${imagePaths?.length ?? 0}`);
38
38
  const userId = chatId;
39
39
  const text = content.trim();
40
40
  const msgIdSender = {
@@ -61,11 +61,17 @@ export function setupClawbotHandlers(config, sessionManager) {
61
61
  },
62
62
  }),
63
63
  });
64
+ // Prepend image paths to prompt so Claude can read them with its Read tool
65
+ let enrichedText = text;
66
+ if (imagePaths?.length) {
67
+ const imgRefs = imagePaths.map(p => `[用户发送了图片: ${p}]`).join('\n');
68
+ enrichedText = `${imgRefs}\n${text}`;
69
+ }
64
70
  await handleTextFlow({
65
71
  platform: 'clawbot',
66
72
  userId,
67
73
  chatId,
68
- text,
74
+ text: enrichedText,
69
75
  ctx,
70
76
  handleAIRequest,
71
77
  sendTextReply: (c, t) => sendTextReply(c, t),
@@ -1095,6 +1095,21 @@ export async function startWebConfigServer(options) {
1095
1095
  json(response, 200, { platforms, serviceStatus: getServiceStatus() }, request);
1096
1096
  return;
1097
1097
  }
1098
+ if (request.method === "GET" && requestUrl.pathname === "/api/metrics") {
1099
+ const file = loadFileConfig();
1100
+ const platforms = getHealthPlatformSnapshot(file);
1101
+ const status = getServiceStatus();
1102
+ const uptime = process.uptime();
1103
+ const mem = process.memoryUsage();
1104
+ json(response, 200, {
1105
+ uptime: Math.round(uptime),
1106
+ memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
1107
+ service: { running: status.running, pid: status.pid },
1108
+ platforms: Object.entries(platforms).map(([k, v]) => ({ name: k, configured: v.configured, enabled: v.enabled })),
1109
+ timestamp: new Date().toISOString(),
1110
+ }, request);
1111
+ return;
1112
+ }
1098
1113
  if (request.method === "POST" && requestUrl.pathname === "/api/service/start") {
1099
1114
  try {
1100
1115
  const config = loadConfig();
package/dist/logger.d.ts CHANGED
@@ -23,6 +23,11 @@ export declare function createLogger(tag: string): {
23
23
  debug: (msg: string, ...args: unknown[]) => void;
24
24
  infoEvent: (event: string, data?: Record<string, unknown>, msg?: string) => void;
25
25
  };
26
+ /**
27
+ * Audit log — records user interactions for debugging and compliance.
28
+ * Always enabled, writes to audit.log.
29
+ */
30
+ export declare function auditLog(platform: string, userId: string, action: string, detail?: Record<string, unknown>): void;
26
31
  export declare function emitStructuredEvent(tag: string, event: string, data?: Record<string, unknown>, level?: LogLevel, msg?: string): void;
27
32
  export declare function shutdownLoggerTelemetry(): Promise<void>;
28
33
  export declare function closeLogger(): Promise<void>;
package/dist/logger.js CHANGED
@@ -12,6 +12,7 @@ let logDir = DEFAULT_LOG_DIR;
12
12
  let minLevel = LOG_LEVELS.DEBUG;
13
13
  let logStream;
14
14
  let eventsStream;
15
+ let auditStream;
15
16
  let telemetryEnabled = false;
16
17
  let telemetryStatsTimer = null;
17
18
  let lastTelemetryStatsSignature = '';
@@ -77,6 +78,12 @@ export function initLogger(dirOrOpts, level, telemetry) {
77
78
  mkdirSync(logDir, { recursive: true });
78
79
  rotateOldLogs();
79
80
  logStream = createWriteStream(join(logDir, getLogFileName()), { flags: 'a' });
81
+ // Audit log — always enabled, separate file
82
+ if (auditStream) {
83
+ auditStream.end();
84
+ auditStream = undefined;
85
+ }
86
+ auditStream = createWriteStream(join(logDir, 'audit.log'), { flags: 'a' });
80
87
  telemetryEnabled = !!tel?.enabled;
81
88
  if (telemetryStatsTimer) {
82
89
  clearInterval(telemetryStatsTimer);
@@ -135,6 +142,20 @@ export function createLogger(tag) {
135
142
  infoEvent: (event, data, msg) => emitStructuredEvent(tag, event, data, 'INFO', msg),
136
143
  };
137
144
  }
145
+ /**
146
+ * Audit log — records user interactions for debugging and compliance.
147
+ * Always enabled, writes to audit.log.
148
+ */
149
+ export function auditLog(platform, userId, action, detail) {
150
+ const entry = {
151
+ ts: new Date().toISOString(),
152
+ platform,
153
+ userId,
154
+ action,
155
+ ...detail,
156
+ };
157
+ auditStream?.write(JSON.stringify(entry) + '\n');
158
+ }
138
159
  export function emitStructuredEvent(tag, event, data, level = 'INFO', msg = '') {
139
160
  if (!telemetryEnabled)
140
161
  return;
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { setActiveChatId } from '../shared/active-chats.js';
19
19
  import { setChatUser } from '../shared/chat-user-map.js';
20
- import { createLogger } from '../logger.js';
20
+ import { createLogger, auditLog } from '../logger.js';
21
21
  import { handleEnqueueResult, DEFAULT_QUEUE_FULL_MESSAGE, DEFAULT_QUEUED_MESSAGE } from '../shared/utils.js';
22
22
  const log = createLogger('TextFlow');
23
23
  /** Default access-denied message. */
@@ -79,6 +79,8 @@ export async function handleTextFlow(params) {
79
79
  if (!text) {
80
80
  return true;
81
81
  }
82
+ // Audit: log user interaction
83
+ auditLog(platform, userId, 'message', { text: text.substring(0, 200) });
82
84
  // 6. Enqueue AI request
83
85
  if (customEnqueue) {
84
86
  // Platform-specific enqueue (e.g., WorkBuddy with extra context)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.11.1-beta.17",
3
+ "version": "1.11.1-beta.19",
4
4
  "description": "Your AI coding assistant, in every chat app. Multi-platform IM bridge for Claude Code, Codex, and CodeBuddy.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",