@wu529778790/open-im 1.11.1-beta.18 → 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];
@@ -131,7 +132,9 @@ function startPolling() {
131
132
  cacheContextToken(chatId, msg.context_token);
132
133
  setClawbotContextToken(msg.context_token);
133
134
  }
134
- userMessages.push({ chatId, msgId, content: extracted, contextToken: msg.context_token });
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 });
135
138
  }
136
139
  // Step 2: Aggregate consecutive messages from the same user
137
140
  // ClawBot splits image+text into separate messages; combine them
@@ -139,21 +142,24 @@ function startPolling() {
139
142
  for (const m of userMessages) {
140
143
  const last = aggregated[aggregated.length - 1];
141
144
  if (last && last.chatId === m.chatId) {
142
- // Same user — merge content (image marker + text)
145
+ // Same user — merge content and image paths
143
146
  last.content = `${last.content}\n${m.content}`;
147
+ if (m.imagePaths?.length) {
148
+ last.imagePaths = [...(last.imagePaths ?? []), ...m.imagePaths];
149
+ }
144
150
  }
145
151
  else {
146
- aggregated.push({ chatId: m.chatId, msgId: m.msgId, content: m.content });
152
+ aggregated.push({ chatId: m.chatId, msgId: m.msgId, content: m.content, imagePaths: m.imagePaths });
147
153
  }
148
154
  }
149
155
  // Step 3: Dispatch aggregated messages
150
156
  for (const m of aggregated) {
151
157
  if (signal.aborted)
152
158
  break;
153
- log.info(`ClawBot message: chatId=${m.chatId}, msgId=${m.msgId}, content="${m.content.substring(0, 100)}"`);
159
+ log.info(`ClawBot message: chatId=${m.chatId}, msgId=${m.msgId}, content="${m.content.substring(0, 100)}", images=${m.imagePaths?.length ?? 0}`);
154
160
  if (messageHandler) {
155
161
  try {
156
- await messageHandler(m.chatId, m.msgId, m.content);
162
+ await messageHandler(m.chatId, m.msgId, m.content, m.imagePaths);
157
163
  }
158
164
  catch (err) {
159
165
  log.error('Error in ClawBot message handler:', err);
@@ -232,6 +238,48 @@ function stopWatchdog() {
232
238
  watchdogTimer = null;
233
239
  }
234
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
+ }
235
283
  /**
236
284
  * Extract text content from an iLink message's item_list.
237
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.18",
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",