@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.
- package/dist/adapters/claude-adapter.d.ts +20 -1
- package/dist/adapters/claude-adapter.js +61 -3
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +12 -1
- package/dist/claude/cli-runner.js +1 -0
- package/dist/claude/process-pool.d.ts +83 -0
- package/dist/claude/process-pool.js +291 -0
- package/dist/feishu/message-sender.js +59 -44
- package/dist/index.js +2 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
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: '
|
|
16
|
-
codex: '
|
|
17
|
-
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 =
|
|
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
|
|
173
|
-
|
|
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
|
-
|
|
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);
|