@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/README.md +77 -103
- package/dist/access/access-control.js +9 -2
- package/dist/claude/cli-runner.js +79 -28
- package/dist/cli.js +102 -183
- package/dist/config.d.ts +11 -1
- package/dist/config.js +67 -9
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +5 -0
- package/dist/feishu/client.d.ts +5 -0
- package/dist/feishu/client.js +69 -0
- package/dist/feishu/event-handler.d.ts +8 -0
- package/dist/feishu/event-handler.js +255 -0
- package/dist/feishu/message-sender.d.ts +7 -0
- package/dist/feishu/message-sender.js +253 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +85 -69
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +160 -71
- package/dist/telegram/client.d.ts +2 -2
- package/dist/telegram/client.js +11 -23
- package/dist/telegram/event-handler.d.ts +3 -3
- package/dist/telegram/event-handler.js +84 -71
- package/dist/telegram/message-sender.d.ts +1 -1
- package/dist/telegram/message-sender.js +72 -89
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,78 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { fileURLToPath } from
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
41
|
-
async function removePidFile() {
|
|
24
|
+
function writePid(pid) {
|
|
42
25
|
try {
|
|
43
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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(
|
|
137
|
-
console.log(
|
|
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
|
-
|
|
151
|
-
console.
|
|
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:
|
|
163
|
-
|
|
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
|
-
|
|
86
|
+
writePid(child.pid);
|
|
87
|
+
console.log(`open-im 已在后台启动 (pid=${child.pid})`);
|
|
170
88
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
33
|
-
const
|
|
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('至少需要配置
|
|
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
|
-
|
|
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,
|
package/dist/constants.d.ts
CHANGED
|
@@ -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;
|