@wu529778790/open-im 0.4.0 → 1.0.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/cli.js CHANGED
@@ -1,78 +1,45 @@
1
1
  #!/usr/bin/env node
2
- import { main, needsSetup, runInteractiveSetup } from './index.js';
3
- import { loadConfig } from './config.js';
4
- import { spawn, execFileSync } from 'node:child_process';
5
- import { fileURLToPath } from 'node:url';
6
- import { dirname, join } from 'node:path';
7
- import { mkdir, writeFile, rm, readFile } from 'node:fs/promises';
8
- import { existsSync, readFileSync } from 'node:fs';
9
- import { platform, homedir } from 'node:os';
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
- // PID 文件路径
13
- const PID_DIR = join(homedir(), '.open-im');
14
- const PID_FILE = join(PID_DIR, 'daemon.pid');
15
- const STOP_FILE = join(PID_DIR, 'stop.flag');
16
- const CONFIG_FILE = join(PID_DIR, 'config.json');
17
- // 保存 PID 到文件
18
- async function savePid(pid) {
19
- try {
20
- await mkdir(PID_DIR, { recursive: true });
21
- await writeFile(PID_FILE, String(pid), 'utf-8');
22
- }
23
- catch (err) {
24
- console.error('无法保存 PID 文件:', err);
25
- }
26
- }
27
- // 读取 PID 文件
28
- async function readPid() {
2
+ import { spawn } from "node:child_process";
3
+ import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { main, needsSetup, runInteractiveSetup } from "./index.js";
7
+ import { loadConfig } from "./config.js";
8
+ import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const PID_FILE = join(APP_HOME, "open-im.pid");
11
+ const PORT_FILE = join(APP_HOME, "open-im.port");
12
+ const INDEX_JS = join(__dirname, "index.js");
13
+ function getPid() {
14
+ if (!existsSync(PID_FILE))
15
+ return null;
29
16
  try {
30
- if (!existsSync(PID_FILE)) {
31
- return null;
32
- }
33
- const content = await readFile(PID_FILE, 'utf-8');
34
- return parseInt(content.trim(), 10);
17
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
18
+ return isNaN(pid) ? null : pid;
35
19
  }
36
20
  catch {
37
21
  return null;
38
22
  }
39
23
  }
40
- // 删除 PID 文件
41
- async function removePidFile() {
24
+ function writePid(pid) {
42
25
  try {
43
- if (existsSync(PID_FILE)) {
44
- await rm(PID_FILE);
45
- }
26
+ writeFileSync(PID_FILE, String(pid), "utf-8");
46
27
  }
47
- catch {
48
- // 忽略删除错误
28
+ catch (err) {
29
+ console.error("无法写入 PID 文件:", err);
49
30
  }
50
31
  }
51
- // 更新工作目录到配置文件
52
- async function updateWorkDir(workDir) {
32
+ function removePid() {
53
33
  try {
54
- await mkdir(PID_DIR, { recursive: true });
55
- let config = {};
56
- if (existsSync(CONFIG_FILE)) {
57
- try {
58
- config = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
59
- }
60
- catch {
61
- // 忽略解析错误
62
- }
63
- }
64
- // 更新工作目录
65
- config.claudeWorkDir = workDir;
66
- await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
34
+ if (existsSync(PID_FILE))
35
+ unlinkSync(PID_FILE);
67
36
  }
68
- catch (err) {
69
- console.error('无法保存工作目录配置:', err);
37
+ catch {
38
+ /* ignore */
70
39
  }
71
40
  }
72
- // 检查进程是否在运行
73
- async function isProcessRunning(pid) {
41
+ function isRunning(pid) {
74
42
  try {
75
- // 尝试发送信号 0(不杀死进程,只检查是否存在)
76
43
  process.kill(pid, 0);
77
44
  return true;
78
45
  }
@@ -80,160 +47,112 @@ async function isProcessRunning(pid) {
80
47
  return false;
81
48
  }
82
49
  }
83
- // 停止服务 - 创建停止标记文件,让服务优雅关闭
84
- async function stopService() {
85
- const pid = await readPid();
86
- if (!pid) {
87
- console.log('未找到运行中的服务(PID 文件不存在)');
88
- return;
89
- }
90
- const running = await isProcessRunning(pid);
91
- if (!running) {
92
- console.log(`服务未运行(进程 ${pid} 不存在)`);
93
- await removePidFile();
50
+ async function cmdStart() {
51
+ const pid = getPid();
52
+ if (pid && isRunning(pid)) {
53
+ console.log(`open-im 已在后台运行 (pid=${pid})`);
94
54
  return;
95
55
  }
96
- try {
97
- // 创建停止标记文件,服务会定期检查并优雅关闭
98
- await mkdir(PID_DIR, { recursive: true });
99
- await writeFile(STOP_FILE, Date.now().toString(), 'utf-8');
100
- console.log('正在停止服务...');
101
- // 等待进程退出(最多 10 秒)
102
- const maxWait = 10000;
103
- const interval = 200;
104
- let waited = 0;
105
- while (waited < maxWait) {
106
- await new Promise(resolve => setTimeout(resolve, interval));
107
- if (!(await isProcessRunning(pid))) {
108
- await removePidFile();
109
- console.log('服务已停止');
110
- return;
111
- }
112
- waited += interval;
113
- }
114
- // 超时后强制终止
115
- console.log('等待超时,强制终止服务...');
116
- const isWindows = platform() === 'win32';
117
- if (isWindows) {
118
- execFileSync('taskkill', ['/F', '/PID', String(pid)], { stdio: 'ignore' });
119
- }
120
- else {
121
- process.kill(pid, 'SIGKILL');
122
- }
123
- await removePidFile();
124
- console.log('服务已强制停止');
125
- }
126
- catch (err) {
127
- const errorMsg = err instanceof Error ? err.message : String(err);
128
- console.error(`停止服务失败: ${errorMsg}`);
129
- await removePidFile();
130
- }
131
- }
132
- // 启动服务(后台)
133
- async function startService() {
134
- // 首先检查是否需要配置
56
+ removePid();
57
+ // 在前台先完成配置校验与配置向导(与 dev 行为保持一致)
135
58
  if (needsSetup()) {
136
- console.log('\n━━━ open-im 首次配置 ━━━\n');
137
- console.log('检测到未配置,需要先完成配置才能启动服务\n');
59
+ console.log("\n━━━ open-im 首次配置 ━━━\n");
60
+ console.log("检测到尚未配置,将先进入配置向导...\n");
138
61
  const saved = await runInteractiveSetup();
139
62
  if (!saved) {
140
- console.log('配置未完成,取消启动。');
63
+ console.log("配置未完成,已取消启动。");
141
64
  process.exit(1);
142
65
  }
143
- console.log('');
66
+ console.log("");
144
67
  }
145
- // 验证配置是否有效(避免有配置文件但缺少必要字段的情况)
68
+ // 校验配置是否有效(避免后台静默失败)
146
69
  try {
147
70
  loadConfig();
148
71
  }
149
72
  catch (err) {
150
- console.error('配置无效或缺少必要字段:', err instanceof Error ? err.message : err);
151
- console.log('\n请运行以下命令重新配置:\n npx @wu529778790/open-im\n');
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ console.error("配置无效或缺少必要字段:", msg);
75
+ console.log("\n请运行以下命令重新配置:\n npx @wu529778790/open-im dev\n或:\n npx @wu529778790/open-im init\n");
152
76
  process.exit(1);
153
77
  }
154
- // 显示配置的工作目录(不自动更新,避免在不同目录重启时导致配置混乱)
155
- const config = loadConfig();
156
- console.log(`使用配置的工作目录: ${config.claudeWorkDir}`);
157
- // 后台启动 - 跨平台方案
158
- const distPath = join(__dirname, '..', 'dist', 'index.js');
159
- // 使用 detached 模式创建独立进程
160
- const child = spawn(process.execPath, [distPath], {
78
+ const child = spawn(process.execPath, [INDEX_JS], {
161
79
  detached: true,
162
- stdio: ['ignore', 'ignore', 'ignore'],
163
- windowsHide: true // Windows 上隐藏控制台窗口
80
+ stdio: "ignore",
81
+ cwd: process.cwd(),
82
+ env: process.env,
83
+ windowsHide: process.platform === "win32",
164
84
  });
165
- // 保存 PID
166
- await savePid(child.pid);
167
- // 让子进程独立于父进程
168
85
  child.unref();
169
- console.log(`服务已在后台启动 (PID: ${child.pid})`);
86
+ writePid(child.pid);
87
+ console.log(`open-im 已在后台启动 (pid=${child.pid})`);
170
88
  }
171
- const args = process.argv.slice(2);
172
- if (args[0] === 'init') {
173
- // 手动触发配置
174
- console.log('\n━━━ open-im 配置向导 ━━━\n');
175
- const saved = await runInteractiveSetup();
176
- if (!saved) {
177
- console.log('配置未完成。');
178
- process.exit(1);
89
+ async function cmdStop() {
90
+ const pid = getPid();
91
+ if (!pid) {
92
+ console.log("open-im 未在后台运行");
93
+ return;
179
94
  }
180
- console.log('\n✅ 配置完成!现在可以运行以下命令启动服务:\n open-im start\n');
181
- }
182
- else if (args[0] === 'stop') {
183
- stopService().catch((err) => {
184
- console.error('停止服务时出错:', err);
185
- process.exit(1);
186
- });
187
- }
188
- else if (args[0] === 'restart') {
189
- console.log('正在重启服务...\n');
190
- await stopService().catch((err) => {
191
- console.error('停止服务时出错:', err);
192
- });
193
- // 等待进程完全退出 AND Telegram API 释放连接(至少 3 秒)
194
- // Telegram 需要时间释放 bot 实例,否则会出现 409 Conflict 错误
195
- const pid = await readPid();
196
- if (pid) {
197
- // 持续检查直到进程真正退出(最多 15 秒)
198
- const maxWait = 15000;
199
- const checkInterval = 500;
200
- let waited = 0;
201
- while (waited < maxWait) {
202
- if (!(await isProcessRunning(pid))) {
203
- // 进程已退出,再等待 3 秒让 Telegram API 完全释放
204
- const remainingWait = 3000;
205
- console.log(`进程已退出,等待 ${remainingWait / 1000} 秒让 Telegram API 释放连接...`);
206
- await new Promise(resolve => setTimeout(resolve, remainingWait));
207
- break;
95
+ if (!isRunning(pid)) {
96
+ removePid();
97
+ console.log("open-im 进程已不存在");
98
+ return;
99
+ }
100
+ const port = existsSync(PORT_FILE)
101
+ ? parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10) || SHUTDOWN_PORT
102
+ : SHUTDOWN_PORT;
103
+ try {
104
+ const res = await fetch(`http://127.0.0.1:${port}/shutdown`, {
105
+ signal: AbortSignal.timeout(3000),
106
+ });
107
+ if (res.ok) {
108
+ for (let i = 0; i < 50; i++) {
109
+ await new Promise((r) => setTimeout(r, 100));
110
+ if (!isRunning(pid))
111
+ break;
208
112
  }
209
- await new Promise(resolve => setTimeout(resolve, checkInterval));
210
- waited += checkInterval;
211
- }
212
- if (waited >= maxWait) {
213
- console.log('警告: 进程退出超时,继续启动...');
214
113
  }
215
114
  }
216
- console.log('\n正在重新启动服务...\n');
217
- await startService();
115
+ catch {
116
+ /* HTTP 失败则用 SIGTERM 兜底 */
117
+ process.kill(pid, "SIGTERM");
118
+ await new Promise((r) => setTimeout(r, 500));
119
+ }
120
+ if (isRunning(pid)) {
121
+ process.kill(pid, "SIGKILL");
122
+ }
123
+ removePid();
124
+ try {
125
+ if (existsSync(PORT_FILE))
126
+ unlinkSync(PORT_FILE);
127
+ }
128
+ catch {
129
+ /* ignore */
130
+ }
131
+ console.log(`open-im 已停止 (pid=${pid})`);
218
132
  }
219
- else if (args[0] === 'start') {
220
- await startService();
133
+ const cmd = process.argv[2];
134
+ if (cmd === "start") {
135
+ cmdStart().catch((err) => {
136
+ console.error(err);
137
+ process.exit(1);
138
+ });
221
139
  }
222
- else if (args[0] === 'run' || args.length === 0) {
223
- // 前台运行(默认命令)
224
- console.log('\n🚀 正在前台启动 open-im 服务...\n');
225
- console.log('💡 提示:按 Ctrl+C 可随时停止服务\n');
226
- main().catch((err) => {
140
+ else if (cmd === "stop") {
141
+ cmdStop().catch((err) => {
227
142
  console.error(err);
228
143
  process.exit(1);
229
144
  });
230
145
  }
231
- else {
232
- // 兼容旧版本,无参数时也运行
233
- console.log('\n🚀 正在前台启动 open-im 服务...\n');
234
- console.log('💡 提示:按 Ctrl+C 可随时停止服务\n');
146
+ else if (cmd === "dev" || cmd === "run" || cmd === undefined) {
235
147
  main().catch((err) => {
236
148
  console.error(err);
237
149
  process.exit(1);
238
150
  });
239
151
  }
152
+ else {
153
+ console.log(`用法: open-im [start|stop|dev]
154
+ start - 后台运行
155
+ stop - 停止后台进程
156
+ dev - 前台运行(调试),Ctrl+C 停止`);
157
+ process.exit(cmd === "--help" || cmd === "-h" ? 0 : 1);
158
+ }
package/dist/config.d.ts CHANGED
@@ -3,8 +3,12 @@ export type Platform = 'feishu' | 'telegram';
3
3
  export type AiCommand = 'claude' | 'codex' | 'cursor';
4
4
  export interface Config {
5
5
  enabledPlatforms: Platform[];
6
- telegramBotToken: string;
6
+ telegramBotToken?: string;
7
+ feishuAppId?: string;
8
+ feishuAppSecret?: string;
7
9
  allowedUserIds: string[];
10
+ telegramAllowedUserIds: string[];
11
+ feishuAllowedUserIds: string[];
8
12
  aiCommand: AiCommand;
9
13
  claudeCliPath: string;
10
14
  claudeWorkDir: string;
@@ -16,7 +20,13 @@ export interface Config {
16
20
  logLevel: LogLevel;
17
21
  platforms: {
18
22
  telegram?: {
23
+ enabled: boolean;
19
24
  proxy?: string;
25
+ allowedUserIds: string[];
26
+ };
27
+ feishu?: {
28
+ enabled: boolean;
29
+ allowedUserIds: string[];
20
30
  };
21
31
  };
22
32
  }
package/dist/config.js CHANGED
@@ -19,24 +19,60 @@ function loadFileConfig() {
19
19
  }
20
20
  /** 检测是否需要交互式配置(无 token 且无环境变量) */
21
21
  export function needsSetup() {
22
+ // 环境变量已提供任一平台的凭证,则认为已配置
22
23
  if (process.env.TELEGRAM_BOT_TOKEN)
23
24
  return false;
25
+ if (process.env.FEISHU_APP_ID && process.env.FEISHU_APP_SECRET)
26
+ return false;
24
27
  const file = loadFileConfig();
25
- return !file.telegramBotToken;
28
+ const tg = file.platforms?.telegram;
29
+ const fs = file.platforms?.feishu;
30
+ const hasTelegram = !!tg?.botToken;
31
+ const hasFeishu = !!(fs?.appId && fs?.appSecret);
32
+ return !hasTelegram && !hasFeishu;
26
33
  }
27
34
  function parseCommaSeparated(value) {
28
35
  return value.split(',').map((s) => s.trim()).filter(Boolean);
29
36
  }
30
37
  export function loadConfig() {
31
38
  const file = loadFileConfig();
32
- const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ?? file.telegramBotToken ?? '';
33
- const enabledPlatforms = telegramBotToken ? ['telegram'] : [];
39
+ const fileTelegram = file.platforms?.telegram;
40
+ const fileFeishu = file.platforms?.feishu;
41
+ // 1. 加载各平台凭证(env 优先,其次新结构,最后旧字段)
42
+ const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN ??
43
+ fileTelegram?.botToken ??
44
+ file.telegramBotToken;
45
+ const feishuAppId = process.env.FEISHU_APP_ID ??
46
+ fileFeishu?.appId ??
47
+ file.feishuAppId;
48
+ const feishuAppSecret = process.env.FEISHU_APP_SECRET ??
49
+ fileFeishu?.appSecret ??
50
+ file.feishuAppSecret;
51
+ // 2. 计算启用平台
52
+ const enabledPlatforms = [];
53
+ const telegramEnabledFlag = fileTelegram?.enabled;
54
+ const feishuEnabledFlag = fileFeishu?.enabled;
55
+ const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
56
+ const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
57
+ if (telegramEnabled)
58
+ enabledPlatforms.push('telegram');
59
+ if (feishuEnabled)
60
+ enabledPlatforms.push('feishu');
34
61
  if (enabledPlatforms.length === 0) {
35
- throw new Error('至少需要配置 TELEGRAM_BOT_TOKEN');
62
+ throw new Error('至少需要配置 Telegram 或 Feishu 其中一个平台(可以通过环境变量或 config.json)');
36
63
  }
64
+ // 3. 全局白名单(旧字段,向后兼容,主要用于作为 per-platform 的兜底)
37
65
  const allowedUserIds = process.env.ALLOWED_USER_IDS !== undefined
38
66
  ? parseCommaSeparated(process.env.ALLOWED_USER_IDS)
39
67
  : file.allowedUserIds ?? [];
68
+ // 4. 分平台白名单(新字段)
69
+ const telegramAllowedUserIds = process.env.TELEGRAM_ALLOWED_USER_IDS !== undefined
70
+ ? parseCommaSeparated(process.env.TELEGRAM_ALLOWED_USER_IDS)
71
+ : fileTelegram?.allowedUserIds ?? allowedUserIds;
72
+ const feishuAllowedUserIds = process.env.FEISHU_ALLOWED_USER_IDS !== undefined
73
+ ? parseCommaSeparated(process.env.FEISHU_ALLOWED_USER_IDS)
74
+ : fileFeishu?.allowedUserIds ?? allowedUserIds;
75
+ // 5. AI / 工作目录 / 安全配置
40
76
  const aiCommand = (process.env.AI_COMMAND ?? file.aiCommand ?? 'claude');
41
77
  const claudeCliPath = process.env.CLAUDE_CLI_PATH ?? file.claudeCliPath ?? 'claude';
42
78
  const claudeWorkDir = process.env.CLAUDE_WORK_DIR ?? file.claudeWorkDir ?? process.cwd();
@@ -51,6 +87,7 @@ export function loadConfig() {
51
87
  const claudeTimeoutMs = process.env.CLAUDE_TIMEOUT_MS !== undefined
52
88
  ? parseInt(process.env.CLAUDE_TIMEOUT_MS, 10) || 600000
53
89
  : file.claudeTimeoutMs ?? 600000;
90
+ // 6. 校验 Claude CLI
54
91
  if (aiCommand === 'claude') {
55
92
  if (isAbsolute(claudeCliPath) || claudeCliPath.includes('/') || claudeCliPath.includes('\\')) {
56
93
  try {
@@ -89,18 +126,39 @@ export function loadConfig() {
89
126
  }
90
127
  }
91
128
  }
129
+ // 7. 日志与平台配置
92
130
  const logDir = process.env.LOG_DIR ?? file.logDir ?? join(APP_HOME, 'logs');
93
131
  const logLevel = (process.env.LOG_LEVEL?.toUpperCase() ?? file.logLevel ?? 'INFO');
94
- // Platform-specific proxy configuration
95
132
  const platforms = {
96
- telegram: {
97
- proxy: process.env.TELEGRAM_PROXY ?? file?.platforms?.telegram?.proxy,
98
- },
133
+ telegram: telegramEnabled
134
+ ? {
135
+ enabled: true,
136
+ proxy: process.env.TELEGRAM_PROXY ?? file.platforms?.telegram?.proxy,
137
+ allowedUserIds: telegramAllowedUserIds,
138
+ }
139
+ : {
140
+ enabled: false,
141
+ proxy: process.env.TELEGRAM_PROXY ?? file.platforms?.telegram?.proxy,
142
+ allowedUserIds: telegramAllowedUserIds,
143
+ },
144
+ feishu: feishuEnabled
145
+ ? {
146
+ enabled: true,
147
+ allowedUserIds: feishuAllowedUserIds,
148
+ }
149
+ : {
150
+ enabled: false,
151
+ allowedUserIds: feishuAllowedUserIds,
152
+ },
99
153
  };
100
154
  return {
101
155
  enabledPlatforms,
102
- telegramBotToken,
156
+ telegramBotToken: telegramBotToken ?? '',
157
+ feishuAppId: feishuAppId ?? '',
158
+ feishuAppSecret: feishuAppSecret ?? '',
103
159
  allowedUserIds,
160
+ telegramAllowedUserIds,
161
+ feishuAllowedUserIds,
104
162
  aiCommand,
105
163
  claudeCliPath,
106
164
  claudeWorkDir,
@@ -1,7 +1,12 @@
1
1
  export declare const APP_HOME: string;
2
+ /** 优雅关闭 HTTP 端口(stop 命令通过此端口触发 shutdown) */
3
+ export declare const SHUTDOWN_PORT = 39281;
2
4
  export declare const IMAGE_DIR: string;
3
5
  export declare const READ_ONLY_TOOLS: string[];
4
6
  export declare const TERMINAL_ONLY_COMMANDS: Set<string>;
5
7
  export declare const DEDUP_TTL_MS: number;
6
8
  export declare const THROTTLE_MS = 200;
9
+ /** Telegram 编辑消息节流:600ms,避免 API 限速且更流畅 */
10
+ export declare const TELEGRAM_THROTTLE_MS = 600;
7
11
  export declare const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
12
+ export declare const MAX_FEISHU_MESSAGE_LENGTH = 4000;
package/dist/constants.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { join } from 'node:path';
2
2
  import { homedir, tmpdir } from 'node:os';
3
3
  export const APP_HOME = join(homedir(), '.open-im');
4
+ /** 优雅关闭 HTTP 端口(stop 命令通过此端口触发 shutdown) */
5
+ export const SHUTDOWN_PORT = 39281;
4
6
  export const IMAGE_DIR = join(tmpdir(), 'open-im-images');
5
7
  export const READ_ONLY_TOOLS = [
6
8
  'Read', 'Glob', 'Grep', 'WebFetch', 'WebSearch', 'Task', 'TodoRead',
@@ -12,4 +14,7 @@ export const TERMINAL_ONLY_COMMANDS = new Set([
12
14
  ]);
13
15
  export const DEDUP_TTL_MS = 5 * 60 * 1000;
14
16
  export const THROTTLE_MS = 200;
17
+ /** Telegram 编辑消息节流:600ms,避免 API 限速且更流畅 */
18
+ export const TELEGRAM_THROTTLE_MS = 600;
15
19
  export const MAX_TELEGRAM_MESSAGE_LENGTH = 4000;
20
+ export const MAX_FEISHU_MESSAGE_LENGTH = 4000;
@@ -0,0 +1,5 @@
1
+ import { Client } from '@larksuiteoapi/node-sdk';
2
+ import type { Config } from '../config.js';
3
+ export declare function getClient(): Client;
4
+ export declare function initFeishu(config: Config, eventHandler: (data: unknown) => Promise<void>): Promise<void>;
5
+ export declare function stopFeishu(): void;
@@ -0,0 +1,69 @@
1
+ import { Client, WSClient, EventDispatcher, LoggerLevel } from '@larksuiteoapi/node-sdk';
2
+ import { createLogger } from '../logger.js';
3
+ const log = createLogger('Feishu');
4
+ let client = null;
5
+ let wsClient = null;
6
+ export function getClient() {
7
+ if (!client)
8
+ throw new Error('Feishu client not initialized');
9
+ return client;
10
+ }
11
+ export async function initFeishu(config, eventHandler) {
12
+ if (!config.feishuAppId || !config.feishuAppSecret) {
13
+ throw new Error('Feishu app_id and app_secret are required');
14
+ }
15
+ client = new Client({
16
+ appId: config.feishuAppId,
17
+ appSecret: config.feishuAppSecret,
18
+ loggerLevel: LoggerLevel.info,
19
+ disableTokenCache: false,
20
+ });
21
+ // Create event dispatcher for WebSocket events
22
+ const eventDispatcher = new EventDispatcher({});
23
+ // Register event handler for message received
24
+ // Note: register() takes an object with event type as key and handler as value
25
+ eventDispatcher.register({
26
+ 'im.message.receive_v1': async (data) => {
27
+ log.info('[EVENT] Received Feishu message event');
28
+ log.info('[EVENT] Event data:', JSON.stringify(data).slice(0, 500));
29
+ try {
30
+ await eventHandler(data);
31
+ log.info('[EVENT] Event handler called successfully');
32
+ }
33
+ catch (err) {
34
+ log.error('[EVENT] Error calling event handler:', err);
35
+ }
36
+ },
37
+ });
38
+ // Register catch-all handler using wildcard
39
+ eventDispatcher.register({
40
+ '*': (data) => {
41
+ log.info('Received Feishu event (catch-all):', JSON.stringify(data).slice(0, 500));
42
+ // Don't call eventHandler for catch-all, let specific handlers handle it
43
+ },
44
+ });
45
+ // Start WebSocket connection for event receiving
46
+ wsClient = new WSClient({
47
+ appId: config.feishuAppId,
48
+ appSecret: config.feishuAppSecret,
49
+ loggerLevel: LoggerLevel.info,
50
+ });
51
+ try {
52
+ // WSClient.start() requires eventDispatcher parameter
53
+ await wsClient.start({ eventDispatcher });
54
+ log.info('Feishu WebSocket started');
55
+ }
56
+ catch (err) {
57
+ log.error('Failed to start Feishu WebSocket:', err);
58
+ throw err;
59
+ }
60
+ log.info('Feishu client initialized');
61
+ }
62
+ export function stopFeishu() {
63
+ if (wsClient) {
64
+ wsClient.close();
65
+ wsClient = null;
66
+ log.info('Feishu WebSocket closed');
67
+ }
68
+ client = null;
69
+ }
@@ -0,0 +1,8 @@
1
+ import type { Config } from '../config.js';
2
+ import type { SessionManager } from '../session/session-manager.js';
3
+ export interface FeishuEventHandlerHandle {
4
+ stop: () => void;
5
+ getRunningTaskCount: () => number;
6
+ handleEvent: (data: unknown) => Promise<void>;
7
+ }
8
+ export declare function setupFeishuHandlers(config: Config, sessionManager: SessionManager): FeishuEventHandlerHandle;