@wu529778790/open-im 1.0.0 → 1.0.2-beta.0

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.
@@ -2,6 +2,25 @@ import type { ToolAdapter, RunCallbacks, RunOptions, RunHandle } from './tool-ad
2
2
  export declare class ClaudeAdapter implements ToolAdapter {
3
3
  private cliPath;
4
4
  readonly toolId = "claude";
5
- constructor(cliPath: string);
5
+ constructor(cliPath: string, adapterOptions?: {
6
+ useProcessPool?: boolean;
7
+ idleTimeoutMs?: number;
8
+ });
6
9
  run(prompt: string, sessionId: string | undefined, workDir: string, callbacks: RunCallbacks, options?: RunOptions): RunHandle;
10
+ /**
11
+ * Get the number of cached entries in the pool.
12
+ */
13
+ static getCacheSize(): number;
14
+ /**
15
+ * Get the number of active processes in the pool.
16
+ */
17
+ static getActiveProcessCount(): number;
18
+ /**
19
+ * Terminate all cached entries and processes.
20
+ */
21
+ static terminateAll(): void;
22
+ /**
23
+ * Destroy the process pool and cleanup resources.
24
+ */
25
+ static destroy(): void;
7
26
  }
@@ -1,17 +1,75 @@
1
1
  import { runClaude } from '../claude/cli-runner.js';
2
+ import { ClaudeProcessPool } from '../claude/process-pool.js';
3
+ // Global process pool instance
4
+ let processPool = null;
2
5
  export class ClaudeAdapter {
3
6
  cliPath;
4
7
  toolId = 'claude';
5
- constructor(cliPath) {
8
+ constructor(cliPath, adapterOptions) {
6
9
  this.cliPath = cliPath;
10
+ const useProcessPool = adapterOptions?.useProcessPool ?? true;
11
+ const idleTimeoutMs = adapterOptions?.idleTimeoutMs ?? 2 * 60 * 1000; // 2 minutes default
12
+ if (useProcessPool && !processPool) {
13
+ // Initialize process pool with configurable idle timeout
14
+ processPool = new ClaudeProcessPool(idleTimeoutMs);
15
+ }
7
16
  }
8
17
  run(prompt, sessionId, workDir, callbacks, options) {
9
- return runClaude(this.cliPath, prompt, sessionId, workDir, callbacks, {
18
+ const opts = {
10
19
  skipPermissions: options?.skipPermissions,
11
20
  timeoutMs: options?.timeoutMs,
12
21
  model: options?.model,
13
22
  chatId: options?.chatId,
14
23
  hookPort: options?.hookPort,
15
- });
24
+ };
25
+ // Use process pool if enabled and userId is available
26
+ if (processPool && opts.chatId) {
27
+ let aborted = false;
28
+ // Execute using process pool with userId from chatId
29
+ processPool
30
+ .execute(opts.chatId, sessionId, this.cliPath, prompt, workDir, callbacks, opts)
31
+ .catch((err) => {
32
+ if (!aborted && callbacks.onError) {
33
+ callbacks.onError(err.message);
34
+ }
35
+ });
36
+ return {
37
+ abort: () => {
38
+ aborted = true;
39
+ processPool.terminate(opts.chatId, sessionId);
40
+ },
41
+ };
42
+ }
43
+ // Fall back to original implementation
44
+ return runClaude(this.cliPath, prompt, sessionId, workDir, callbacks, opts);
45
+ }
46
+ /**
47
+ * Get the number of cached entries in the pool.
48
+ */
49
+ static getCacheSize() {
50
+ return processPool?.size() ?? 0;
51
+ }
52
+ /**
53
+ * Get the number of active processes in the pool.
54
+ */
55
+ static getActiveProcessCount() {
56
+ return processPool?.activeCount() ?? 0;
57
+ }
58
+ /**
59
+ * Terminate all cached entries and processes.
60
+ */
61
+ static terminateAll() {
62
+ if (processPool) {
63
+ processPool.terminateAll();
64
+ }
65
+ }
66
+ /**
67
+ * Destroy the process pool and cleanup resources.
68
+ */
69
+ static destroy() {
70
+ if (processPool) {
71
+ processPool.destroy();
72
+ processPool = null;
73
+ }
16
74
  }
17
75
  }
@@ -2,3 +2,7 @@ import type { Config } from '../config.js';
2
2
  import type { ToolAdapter } from './tool-adapter.interface.js';
3
3
  export declare function initAdapters(config: Config): void;
4
4
  export declare function getAdapter(aiCommand: string): ToolAdapter | undefined;
5
+ /**
6
+ * Cleanup all adapter resources.
7
+ */
8
+ export declare function cleanupAdapters(): void;
@@ -3,9 +3,20 @@ const adapters = new Map();
3
3
  export function initAdapters(config) {
4
4
  adapters.clear();
5
5
  if (config.aiCommand === 'claude') {
6
- adapters.set('claude', new ClaudeAdapter(config.claudeCliPath));
6
+ // Enable process pool with 2 minute idle timeout
7
+ adapters.set('claude', new ClaudeAdapter(config.claudeCliPath, {
8
+ useProcessPool: true,
9
+ idleTimeoutMs: 2 * 60 * 1000, // 2 minutes
10
+ }));
7
11
  }
8
12
  }
9
13
  export function getAdapter(aiCommand) {
10
14
  return adapters.get(aiCommand);
11
15
  }
16
+ /**
17
+ * Cleanup all adapter resources.
18
+ */
19
+ export function cleanupAdapters() {
20
+ ClaudeAdapter.destroy();
21
+ adapters.clear();
22
+ }
@@ -48,6 +48,7 @@ export function runClaude(cliPath, prompt, sessionId, workDir, callbacks, option
48
48
  stdio: ["ignore", "pipe", "pipe"],
49
49
  env,
50
50
  shell: true,
51
+ windowsHide: true,
51
52
  });
52
53
  }
53
54
  else {
@@ -0,0 +1,83 @@
1
+ export interface ClaudeRunCallbacks {
2
+ onText: (accumulated: string) => void;
3
+ onThinking?: (accumulated: string) => void;
4
+ onToolUse?: (toolName: string, toolInput?: Record<string, unknown>) => void;
5
+ onComplete: (result: {
6
+ success: boolean;
7
+ result: string;
8
+ accumulated: string;
9
+ cost: number;
10
+ durationMs: number;
11
+ model?: string;
12
+ numTurns: number;
13
+ toolStats: Record<string, number>;
14
+ }) => void;
15
+ onError: (error: string) => void;
16
+ onSessionId?: (sessionId: string) => void;
17
+ }
18
+ export interface ClaudeResult {
19
+ success: boolean;
20
+ result: string;
21
+ accumulated: string;
22
+ cost: number;
23
+ durationMs: number;
24
+ model?: string;
25
+ numTurns: number;
26
+ toolStats: Record<string, number>;
27
+ }
28
+ export interface ClaudeRunOptions {
29
+ skipPermissions?: boolean;
30
+ timeoutMs?: number;
31
+ model?: string;
32
+ chatId?: string;
33
+ hookPort?: number;
34
+ }
35
+ /**
36
+ * Process pool that manages cached session configurations.
37
+ *
38
+ * Since Claude CLI doesn't support persistent mode, we use this pool to:
39
+ * 1. Cache active sessions for faster resume using --resume
40
+ * 2. Track which sessions are actively being used
41
+ * 3. Clean up stale entries
42
+ *
43
+ * The main benefit is that resumed sessions don't need to reload conversation history.
44
+ */
45
+ export declare class ClaudeProcessPool {
46
+ private entries;
47
+ private activeProcesses;
48
+ private cleanupTimer;
49
+ private readonly ttl;
50
+ constructor(ttlMs?: number);
51
+ /**
52
+ * Execute a prompt, reusing cached session if available.
53
+ */
54
+ execute(userId: string, sessionId: string | undefined, cliPath: string, prompt: string, workDir: string, callbacks: ClaudeRunCallbacks, options?: ClaudeRunOptions): Promise<ClaudeResult>;
55
+ /**
56
+ * Run a Claude CLI process for a single request.
57
+ */
58
+ private runProcess;
59
+ /**
60
+ * Clean up expired entries.
61
+ */
62
+ private cleanup;
63
+ /**
64
+ * Terminate the active process for a session.
65
+ */
66
+ terminate(userId: string, sessionId: string | undefined): void;
67
+ /**
68
+ * Terminate all active processes and clear cache.
69
+ */
70
+ terminateAll(): void;
71
+ /**
72
+ * Get the number of cached entries.
73
+ */
74
+ size(): number;
75
+ /**
76
+ * Get the number of active processes.
77
+ */
78
+ activeCount(): number;
79
+ /**
80
+ * Destroy the process pool and cleanup resources.
81
+ */
82
+ destroy(): void;
83
+ }
@@ -0,0 +1,291 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createInterface } from "node:readline";
3
+ import { createLogger } from "../logger.js";
4
+ import { parseStreamLine, extractTextDelta, extractThinkingDelta, extractResult, } from "./stream-parser.js";
5
+ import { isStreamInit, isContentBlockStart, isContentBlockDelta, isContentBlockStop, } from "./types.js";
6
+ const log = createLogger("ProcessPool");
7
+ /**
8
+ * Process pool that manages cached session configurations.
9
+ *
10
+ * Since Claude CLI doesn't support persistent mode, we use this pool to:
11
+ * 1. Cache active sessions for faster resume using --resume
12
+ * 2. Track which sessions are actively being used
13
+ * 3. Clean up stale entries
14
+ *
15
+ * The main benefit is that resumed sessions don't need to reload conversation history.
16
+ */
17
+ export class ClaudeProcessPool {
18
+ entries = new Map();
19
+ activeProcesses = new Map();
20
+ cleanupTimer = null;
21
+ ttl;
22
+ constructor(ttlMs = 2 * 60 * 1000) {
23
+ this.ttl = ttlMs;
24
+ log.info(`Process pool created with TTL: ${ttlMs}ms`);
25
+ // Periodic cleanup of expired entries
26
+ this.cleanupTimer = setInterval(() => {
27
+ this.cleanup();
28
+ }, 60 * 1000); // Every minute
29
+ }
30
+ /**
31
+ * Execute a prompt, reusing cached session if available.
32
+ */
33
+ async execute(userId, sessionId, cliPath, prompt, workDir, callbacks, options) {
34
+ const key = `${userId}:${sessionId || "default"}`;
35
+ // Update cache entry (tracks active sessions)
36
+ const entry = this.entries.get(key);
37
+ if (entry) {
38
+ entry.lastUsed = Date.now();
39
+ }
40
+ else {
41
+ this.entries.set(key, { lastUsed: Date.now() });
42
+ }
43
+ // Check if there's an active process for this session
44
+ const activePid = this.activeProcesses.get(key);
45
+ if (activePid && !activePid.killed) {
46
+ log.info(`Session has active process: key=${key}, pid=${activePid.pid}`);
47
+ // Wait a bit for the previous process to complete
48
+ await new Promise(resolve => setTimeout(resolve, 100));
49
+ }
50
+ // Run the Claude CLI process
51
+ return this.runProcess(key, cliPath, prompt, sessionId, workDir, callbacks, options || {});
52
+ }
53
+ /**
54
+ * Run a Claude CLI process for a single request.
55
+ */
56
+ runProcess(key, cliPath, prompt, sessionId, workDir, callbacks, options) {
57
+ return new Promise((resolve, reject) => {
58
+ const args = [
59
+ "-p",
60
+ "--output-format",
61
+ "stream-json",
62
+ "--verbose",
63
+ "--include-partial-messages",
64
+ ];
65
+ if (options.skipPermissions)
66
+ args.push("--dangerously-skip-permissions");
67
+ if (options.model)
68
+ args.push("--model", options.model);
69
+ if (sessionId)
70
+ args.push("--resume", sessionId);
71
+ args.push("--", prompt);
72
+ // Environment setup
73
+ const env = {};
74
+ for (const [k, v] of Object.entries(process.env)) {
75
+ if (k === "CLAUDECODE")
76
+ continue;
77
+ if (v !== undefined)
78
+ env[k] = v;
79
+ }
80
+ if (options.chatId)
81
+ env.CC_IM_CHAT_ID = options.chatId;
82
+ if (options.hookPort)
83
+ env.CC_IM_HOOK_PORT = String(options.hookPort);
84
+ // Platform-specific spawn
85
+ let child;
86
+ if (process.platform === "win32") {
87
+ const isGitBash = process.env.MSYSTEM ||
88
+ process.env.MINGW_PREFIX ||
89
+ process.env.SHELL?.includes("bash");
90
+ if (isGitBash) {
91
+ child = spawn(cliPath, args, {
92
+ cwd: workDir,
93
+ stdio: ["ignore", "pipe", "pipe"],
94
+ env,
95
+ shell: true,
96
+ windowsHide: true,
97
+ });
98
+ }
99
+ else {
100
+ child = spawn(cliPath, args, {
101
+ cwd: workDir,
102
+ stdio: ["ignore", "pipe", "pipe"],
103
+ env,
104
+ windowsHide: true,
105
+ });
106
+ }
107
+ }
108
+ else {
109
+ child = spawn(cliPath, args, {
110
+ cwd: workDir,
111
+ stdio: ["ignore", "pipe", "pipe"],
112
+ env,
113
+ });
114
+ }
115
+ log.info(`Started process: pid=${child.pid}, key=${key}`);
116
+ // Track active process
117
+ this.activeProcesses.set(key, child);
118
+ // State tracking
119
+ let accumulated = "";
120
+ let accumulatedThinking = "";
121
+ let model = "";
122
+ const toolStats = {};
123
+ const pendingToolInputs = new Map();
124
+ const startTime = Date.now();
125
+ const rl = createInterface({ input: child.stdout });
126
+ rl.on("line", (line) => {
127
+ const event = parseStreamLine(line);
128
+ if (!event)
129
+ return;
130
+ if (isStreamInit(event)) {
131
+ model = event.model;
132
+ callbacks.onSessionId?.(event.session_id);
133
+ return;
134
+ }
135
+ const delta = extractTextDelta(event);
136
+ if (delta) {
137
+ accumulated += delta.text;
138
+ callbacks.onText(accumulated);
139
+ return;
140
+ }
141
+ const thinking = extractThinkingDelta(event);
142
+ if (thinking) {
143
+ accumulatedThinking += thinking.text;
144
+ callbacks.onThinking?.(accumulatedThinking);
145
+ return;
146
+ }
147
+ if (isContentBlockStart(event) &&
148
+ event.event.content_block?.type === "tool_use") {
149
+ const name = event.event.content_block.name;
150
+ if (name)
151
+ pendingToolInputs.set(event.event.index, { name, json: "" });
152
+ return;
153
+ }
154
+ if (isContentBlockDelta(event) &&
155
+ event.event.delta?.type === "input_json_delta") {
156
+ const pending = pendingToolInputs.get(event.event.index);
157
+ if (pending)
158
+ pending.json += event.event.delta.partial_json ?? "";
159
+ return;
160
+ }
161
+ if (isContentBlockStop(event)) {
162
+ const pending = pendingToolInputs.get(event.event.index);
163
+ if (pending) {
164
+ toolStats[pending.name] = (toolStats[pending.name] || 0) + 1;
165
+ let input;
166
+ try {
167
+ input = JSON.parse(pending.json);
168
+ }
169
+ catch {
170
+ /* empty */
171
+ }
172
+ callbacks.onToolUse?.(pending.name, input);
173
+ pendingToolInputs.delete(event.event.index);
174
+ }
175
+ return;
176
+ }
177
+ const result = extractResult(event);
178
+ if (result) {
179
+ const fullResult = {
180
+ ...result,
181
+ accumulated,
182
+ model,
183
+ toolStats,
184
+ };
185
+ if (!accumulated && fullResult.result) {
186
+ accumulated = fullResult.result;
187
+ }
188
+ callbacks.onComplete(fullResult);
189
+ resolve(fullResult);
190
+ }
191
+ });
192
+ let exitCode = null;
193
+ let rlClosed = false;
194
+ let childClosed = false;
195
+ let resolved = false;
196
+ const finalize = () => {
197
+ if (!rlClosed || !childClosed || resolved)
198
+ return;
199
+ this.activeProcesses.delete(key);
200
+ resolved = true;
201
+ if (exitCode !== null && exitCode !== 0) {
202
+ const errorMsg = `Claude CLI exited with code ${exitCode}`;
203
+ callbacks.onError(errorMsg);
204
+ reject(new Error(errorMsg));
205
+ }
206
+ // If exitCode is 0 and we haven't resolved yet, the result was already sent
207
+ // via the extractResult handler. This is just cleanup.
208
+ };
209
+ child.on("close", (code) => {
210
+ log.info(`Process closed: code=${code}, pid=${child.pid}, key=${key}`);
211
+ exitCode = code;
212
+ childClosed = true;
213
+ finalize();
214
+ });
215
+ rl.on("close", () => {
216
+ rlClosed = true;
217
+ finalize();
218
+ });
219
+ child.on("error", (err) => {
220
+ log.error(`Process error: ${err.message}, pid=${child.pid}, key=${key}`);
221
+ this.activeProcesses.delete(key);
222
+ const errorMsg = `Failed to start Claude CLI: ${err.message}`;
223
+ callbacks.onError(errorMsg);
224
+ reject(new Error(errorMsg));
225
+ });
226
+ });
227
+ }
228
+ /**
229
+ * Clean up expired entries.
230
+ */
231
+ cleanup() {
232
+ const now = Date.now();
233
+ let cleaned = 0;
234
+ for (const [key, entry] of this.entries.entries()) {
235
+ if (now - entry.lastUsed > this.ttl) {
236
+ this.entries.delete(key);
237
+ cleaned++;
238
+ }
239
+ }
240
+ if (cleaned > 0) {
241
+ log.info(`Cleaned up ${cleaned} expired entries, ${this.entries.size} remaining`);
242
+ }
243
+ }
244
+ /**
245
+ * Terminate the active process for a session.
246
+ */
247
+ terminate(userId, sessionId) {
248
+ const key = `${userId}:${sessionId || "default"}`;
249
+ const child = this.activeProcesses.get(key);
250
+ if (child && !child.killed) {
251
+ child.kill("SIGTERM");
252
+ this.activeProcesses.delete(key);
253
+ }
254
+ // Also remove from cache
255
+ this.entries.delete(key);
256
+ }
257
+ /**
258
+ * Terminate all active processes and clear cache.
259
+ */
260
+ terminateAll() {
261
+ for (const child of this.activeProcesses.values()) {
262
+ if (!child.killed) {
263
+ child.kill("SIGTERM");
264
+ }
265
+ }
266
+ this.activeProcesses.clear();
267
+ this.entries.clear();
268
+ }
269
+ /**
270
+ * Get the number of cached entries.
271
+ */
272
+ size() {
273
+ return this.entries.size;
274
+ }
275
+ /**
276
+ * Get the number of active processes.
277
+ */
278
+ activeCount() {
279
+ return this.activeProcesses.size;
280
+ }
281
+ /**
282
+ * Destroy the process pool and cleanup resources.
283
+ */
284
+ destroy() {
285
+ if (this.cleanupTimer) {
286
+ clearInterval(this.cleanupTimer);
287
+ this.cleanupTimer = null;
288
+ }
289
+ this.terminateAll();
290
+ }
291
+ }
@@ -1,28 +1,65 @@
1
1
  import { getClient } from './client.js';
2
- import { messageCard } from '@larksuiteoapi/node-sdk';
3
2
  import { readFileSync } from 'node:fs';
4
3
  import { createLogger } from '../logger.js';
5
4
  import { splitLongContent } from '../shared/utils.js';
6
5
  import { MAX_FEISHU_MESSAGE_LENGTH } from '../constants.js';
7
6
  const log = createLogger('FeishuSender');
8
- const STATUS_ICONS = {
9
- thinking: '🔵',
10
- streaming: '🔵',
11
- done: '🟢',
12
- error: '🔴',
7
+ const STATUS_CONFIG = {
8
+ thinking: { icon: '🔵', template: 'blue', title: '思考中' },
9
+ streaming: { icon: '🔄', template: 'blue', title: '执行中' },
10
+ done: { icon: '', template: 'green', title: '完成' },
11
+ error: { icon: '', template: 'red', title: '错误' },
13
12
  };
14
13
  const TOOL_DISPLAY_NAMES = {
15
- claude: 'claude-code',
16
- codex: 'codex',
17
- cursor: 'cursor',
14
+ claude: 'Claude Code',
15
+ codex: 'Codex',
16
+ cursor: 'Cursor',
18
17
  };
19
18
  function getToolTitle(toolId, status) {
20
19
  const name = TOOL_DISPLAY_NAMES[toolId] ?? toolId;
21
- if (status === 'thinking')
22
- return `${name} - 思考中...`;
23
- if (status === 'error')
24
- return `${name} - 错误`;
25
- return name;
20
+ const statusText = STATUS_CONFIG[status].title;
21
+ return status === 'done' ? name : `${name} - ${statusText}`;
22
+ }
23
+ /**
24
+ * Create Feishu interactive card with native lark_md support
25
+ * Feishu natively supports Markdown through the `lark_md` tag
26
+ */
27
+ function createFeishuCard(title, content, status, note) {
28
+ const statusConfig = STATUS_CONFIG[status];
29
+ const elements = [];
30
+ // Main content - use native lark_md tag
31
+ elements.push({
32
+ tag: 'div',
33
+ text: {
34
+ tag: 'lark_md',
35
+ content: content || '_处理中..._',
36
+ },
37
+ });
38
+ // Add note separator and hint if provided
39
+ if (note) {
40
+ elements.push({ tag: 'hr' });
41
+ elements.push({
42
+ tag: 'div',
43
+ text: {
44
+ tag: 'lark_md',
45
+ content: `**💡 ${note}**`,
46
+ },
47
+ });
48
+ }
49
+ const card = {
50
+ config: {
51
+ wide_screen_mode: true,
52
+ },
53
+ header: {
54
+ template: statusConfig.template,
55
+ title: {
56
+ content: `${statusConfig.icon} ${title}`,
57
+ tag: 'plain_text',
58
+ },
59
+ },
60
+ elements,
61
+ };
62
+ return JSON.stringify(card);
26
63
  }
27
64
  async function getTenantAccessToken() {
28
65
  const client = getClient();
@@ -39,14 +76,9 @@ async function getTenantAccessToken() {
39
76
  }
40
77
  export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'claude') {
41
78
  const client = getClient();
42
- // Use SDK's built-in card builder for simpler messages
43
- const cardContent = messageCard.defaultCard({
44
- title: `${STATUS_ICONS.thinking} ${getToolTitle(toolId, 'thinking')}`,
45
- content: '正在思考...\n\n请稍候...',
46
- });
79
+ const cardContent = createFeishuCard(getToolTitle(toolId, 'thinking'), '_正在思考,请稍候..._\n\n💭 **准备中**', 'thinking');
47
80
  try {
48
81
  log.info(`Sending thinking message to chat ${chatId}, replyTo: ${replyToMessageId}`);
49
- // 注意:飞书 create 接口不支持 uuid 参数,传 uuid 会导致请求失败
50
82
  const resp = await client.im.message.create({
51
83
  data: {
52
84
  receive_id: chatId,
@@ -68,17 +100,9 @@ export async function sendThinkingMessage(chatId, replyToMessageId, toolId = 'cl
68
100
  }
69
101
  export async function updateMessage(chatId, messageId, content, status, note, toolId = 'claude') {
70
102
  const client = getClient();
71
- // Build content with note
72
- let fullContent = content;
73
- if (note) {
74
- fullContent = `${content}\n\n─────────\n${note}`;
75
- }
76
- const icon = STATUS_ICONS[status];
77
- const title = getToolTitle(toolId, status);
78
- const cardContent = messageCard.defaultCard({
79
- title: `${icon} ${title}`,
80
- content: fullContent,
81
- });
103
+ const icon = STATUS_CONFIG[status].icon;
104
+ const title = `${icon} ${getToolTitle(toolId, status)}`;
105
+ const cardContent = createFeishuCard(title, content, status, note);
82
106
  // Try to use patch API for in-place update (streaming)
83
107
  try {
84
108
  const resp = await client.im.message.patch({
@@ -132,10 +156,7 @@ export async function sendFinalMessages(chatId, messageId, fullContent, note, to
132
156
  const parts = splitLongContent(fullContent, MAX_FEISHU_MESSAGE_LENGTH);
133
157
  // If content fits in one message, try patch for smooth transition
134
158
  if (parts.length === 1) {
135
- const cardContent = messageCard.defaultCard({
136
- title: `${STATUS_ICONS.done} ${getToolTitle(toolId, 'done')}`,
137
- content: fullContent,
138
- });
159
+ const cardContent = createFeishuCard(getToolTitle(toolId, 'done'), fullContent, 'done');
139
160
  // Try to use patch API for in-place update
140
161
  try {
141
162
  const resp = await client.im.message.patch({
@@ -169,10 +190,8 @@ export async function sendFinalMessages(chatId, messageId, fullContent, note, to
169
190
  // Send new messages
170
191
  for (let i = 0; i < parts.length; i++) {
171
192
  try {
172
- const cardContent = messageCard.defaultCard({
173
- title: `${STATUS_ICONS.done} ${getToolTitle(toolId, 'done')}`,
174
- content: i === 0 ? parts[0] : parts[i] + `\n\n(续 ${i + 1}/${parts.length})`,
175
- });
193
+ const partContent = i === 0 ? parts[0] : `${parts[i]}\n\n_*(续 ${i + 1}/${parts.length})*_`;
194
+ const cardContent = createFeishuCard(getToolTitle(toolId, 'done'), partContent, 'done');
176
195
  await client.im.message.create({
177
196
  data: {
178
197
  receive_id: chatId,
@@ -189,11 +208,7 @@ export async function sendFinalMessages(chatId, messageId, fullContent, note, to
189
208
  }
190
209
  export async function sendTextReply(chatId, text) {
191
210
  const client = getClient();
192
- // Use SDK's built-in card builder for simpler messages
193
- const cardContent = messageCard.defaultCard({
194
- title: 'open-im',
195
- content: text,
196
- });
211
+ const cardContent = createFeishuCard('📢 open-im', text, 'done');
197
212
  try {
198
213
  await client.im.message.create({
199
214
  data: {
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { sendTextReply as sendTelegramTextReply } from "./telegram/message-sende
11
11
  import { initFeishu, stopFeishu } from "./feishu/client.js";
12
12
  import { setupFeishuHandlers } from "./feishu/event-handler.js";
13
13
  import { sendTextReply as sendFeishuTextReply } from "./feishu/message-sender.js";
14
- import { initAdapters } from "./adapters/registry.js";
14
+ import { initAdapters, cleanupAdapters } from "./adapters/registry.js";
15
15
  import { SessionManager } from "./session/session-manager.js";
16
16
  import { loadActiveChats, getActiveChatId, flushActiveChats, } from "./shared/active-chats.js";
17
17
  import { initLogger, createLogger, closeLogger } from "./logger.js";
@@ -100,6 +100,7 @@ export async function main() {
100
100
  feishuHandle?.stop();
101
101
  stopFeishu();
102
102
  sessionManager.destroy();
103
+ cleanupAdapters();
103
104
  flushActiveChats();
104
105
  closeLogger();
105
106
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.0.0",
3
+ "version": "1.0.2-beta.0",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, Cursor)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",