@wu529778790/open-im 1.5.5-beta.8 → 1.6.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 CHANGED
@@ -7,6 +7,7 @@
7
7
  - 多平台:支持 Telegram、飞书、企业微信、钉钉、QQ、微信(测试中),可同时启用
8
8
  - 多 AI 工具:支持 Claude、Codex、Cursor
9
9
  - 流式输出:实时回传 AI 回复与工具执行进度(目前钉钉暂未实现流式传输)
10
+ - 图形化配置页面 / cli配置引导
10
11
  - 会话隔离:每个用户独立维护本地会话,`/new` 可重置
11
12
  - 常用命令:支持 `/help`、`/new`、`/cd`、`/pwd`、`/status`
12
13
 
@@ -40,6 +41,13 @@ open-im start
40
41
  | `open-im stop` | 停止后台服务 |
41
42
  | `open-im dev` | 前台运行(调试模式) |
42
43
 
44
+ ## 图形化配置页面
45
+
46
+ - 默认地址:`http://127.0.0.1:39282`
47
+ - `open-im start` 会同时提供本地配置页面
48
+ - `open-im dev` 仅在未完成配置时自动打开页面
49
+ - 已有配置但想手动打开时,使用 `open-im start`
50
+
43
51
  ## 会话说明
44
52
 
45
53
  会话上下文保存在本地 `~/.open-im/data/sessions.json`,与 IM 聊天记录本身无关。每个用户有独立会话目录和 session 信息,发送 `/new` 会重置当前 AI 会话。
@@ -110,8 +118,7 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
110
118
  "enabled": false,
111
119
  "allowedUserIds": [],
112
120
  "appId": "YOUR_QQ_APP_ID",
113
- "secret": "YOUR_QQ_APP_SECRET",
114
- "sandbox": false
121
+ "secret": "YOUR_QQ_APP_SECRET"
115
122
  },
116
123
  "wework": {
117
124
  "enabled": false,
@@ -207,7 +214,7 @@ Claude 默认使用 Agent SDK,不依赖本地 `claude` 可执行文件;通
207
214
 
208
215
  **Telegram 无响应**:检查网络,必要时在 Telegram 平台配置中添加 `"proxy": "http://127.0.0.1:7890"` 或设置 `TELEGRAM_PROXY`。
209
216
 
210
- **QQ 无法连接**:确认机器人已在 QQ 开放平台创建并启用,检查 `QQ_BOT_APPID`、`QQ_BOT_SECRET` 或 `platforms.qq` 配置是否正确。沙箱环境仅用于开发测试,生产环境请确保 `sandbox` 设置为 `false`。
217
+ **QQ 无法连接**:确认机器人已在 QQ 开放平台创建并启用,检查 `QQ_BOT_APPID`、`QQ_BOT_SECRET` 或 `platforms.qq` 配置是否正确。
211
218
 
212
219
  **QQ 重复回复**:如遇到消息重复发送,请确保使用最新版本,该问题已在近期修复。
213
220
 
package/dist/cli.js CHANGED
@@ -1,222 +1,130 @@
1
1
  #!/usr/bin/env node
2
- import { spawn, execFileSync } 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
2
  import { main, needsSetup, runInteractiveSetup } from "./index.js";
7
3
  import { loadConfig } from "./config.js";
8
- import { runPlatformSelectionPrompt } from "./setup.js";
9
4
  import { checkAndUpdate } from "./check-update.js";
10
- import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const PID_FILE = join(APP_HOME, "open-im.pid");
13
- const PORT_FILE = join(APP_HOME, "open-im.port");
14
- const INDEX_JS = join(__dirname, "index.js");
15
- // ============================================================================
16
- // PID 文件管理
17
- // ============================================================================
18
- function getPid() {
19
- if (!existsSync(PID_FILE))
20
- return null;
21
- try {
22
- const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
23
- return isNaN(pid) ? null : pid;
24
- }
25
- catch {
26
- return null;
27
- }
28
- }
29
- function writePid(pid) {
30
- try {
31
- writeFileSync(PID_FILE, String(pid), "utf-8");
32
- }
33
- catch (err) {
34
- console.error("无法写入 PID 文件:", err);
35
- }
36
- }
37
- function removePid() {
38
- try {
39
- if (existsSync(PID_FILE))
40
- unlinkSync(PID_FILE);
41
- }
42
- catch {
43
- /* ignore */
44
- }
45
- }
46
- function isRunning(pid) {
47
- try {
48
- // Windows 下使用 tasklist 检查进程是否存在
49
- if (process.platform === 'win32') {
50
- const result = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH'], {
51
- stdio: 'pipe',
52
- windowsHide: true
53
- }).toString();
54
- return result.includes(String(pid));
5
+ import { getWebConfigUrl, runWebConfigFlow } from "./config-web.js";
6
+ import { getManagerStatus, startManagerProcess, stopManagerProcess } from "./manager-control.js";
7
+ import { stopBackgroundService } from "./service-control.js";
8
+ async function ensureConfigured(mode) {
9
+ if (mode === "init") {
10
+ if (!process.stdin.isTTY) {
11
+ console.error("CLI setup requires an interactive terminal.");
12
+ return false;
55
13
  }
56
- // Unix 系统使用 kill 0 信号检查
57
- process.kill(pid, 0);
58
- return true;
59
- }
60
- catch {
61
- return false;
62
- }
63
- }
64
- // ============================================================================
65
- // 配置校验
66
- // ============================================================================
67
- async function validateOrSetup() {
68
- if (needsSetup()) {
69
- console.log("\n━━━ open-im 首次配置 ━━━\n");
70
- console.log("检测到尚未配置,将先进入配置向导...\n");
71
14
  const saved = await runInteractiveSetup();
72
- if (!saved) {
73
- console.log("配置未完成,已取消启动。");
15
+ if (!saved)
16
+ return false;
17
+ try {
18
+ loadConfig();
19
+ return true;
20
+ }
21
+ catch (error) {
22
+ console.error(error instanceof Error ? error.message : String(error));
74
23
  return false;
75
24
  }
76
- console.log("");
77
25
  }
26
+ if (!needsSetup()) {
27
+ try {
28
+ loadConfig();
29
+ return true;
30
+ }
31
+ catch (error) {
32
+ console.error(error instanceof Error ? error.message : String(error));
33
+ }
34
+ }
35
+ const result = await runWebConfigFlow({ mode, cwd: process.cwd() });
36
+ if (result !== "saved")
37
+ return false;
78
38
  try {
79
39
  loadConfig();
80
40
  return true;
81
41
  }
82
- catch (err) {
83
- const msg = err instanceof Error ? err.message : String(err);
84
- console.error("配置无效或缺少必要字段:", msg);
85
- console.log("\n请运行以下命令重新配置:\n npx @wu529778790/open-im init");
42
+ catch (error) {
43
+ console.error(error instanceof Error ? error.message : String(error));
86
44
  return false;
87
45
  }
88
46
  }
89
- // ============================================================================
90
- // 命令处理
91
- // ============================================================================
92
47
  async function cmdStart() {
93
- const pid = getPid();
94
- if (pid && isRunning(pid)) {
95
- console.log("\n🟢 open-im 已在后台运行");
96
- console.log(` pid: ${pid}`);
48
+ const status = getManagerStatus();
49
+ if (status.running && status.pid) {
50
+ console.log("\nopen-im is already running in the background.");
51
+ console.log(` pid: ${status.pid}`);
52
+ console.log(` config page: ${getWebConfigUrl()}`);
97
53
  return;
98
54
  }
99
- else {
100
- removePid(); // 清理可能存在的陈旧 PID 文件
101
- }
102
- if (!(await validateOrSetup())) {
55
+ if (!(await ensureConfigured("start"))) {
103
56
  process.exit(1);
104
57
  }
105
- // 检查并自动更新到最新版本
106
58
  const { updated } = await checkAndUpdate();
107
59
  if (updated) {
108
60
  process.exit(0);
109
61
  }
110
- // TTY 时在父进程让用户选择要启用的平台,再启动子进程
111
- let config = loadConfig();
112
- if (process.stdin.isTTY) {
113
- const updated = await runPlatformSelectionPrompt(config);
114
- if (!updated) {
115
- console.log("已取消启动。");
116
- process.exit(0);
117
- }
118
- }
119
- const child = spawn(process.execPath, [INDEX_JS], {
120
- detached: true,
121
- stdio: "ignore",
122
- cwd: process.cwd(),
123
- env: process.env,
124
- windowsHide: process.platform === "win32",
125
- });
126
- child.unref();
127
- writePid(child.pid);
128
- console.log("\n🟢 open-im 已在后台启动");
129
- console.log(` pid: ${child.pid}`);
62
+ const child = await startManagerProcess(process.cwd());
63
+ console.log("\nopen-im started in the background.");
64
+ console.log(` pid: ${child.pid}`);
65
+ console.log(` config page: ${getWebConfigUrl()}`);
130
66
  }
131
67
  async function cmdStop() {
132
- const pid = getPid();
133
- if (!pid) {
134
- console.log("open-im 未在后台运行");
68
+ const status = getManagerStatus();
69
+ if (!status.pid) {
70
+ console.log("open-im is not running in the background.");
135
71
  return;
136
72
  }
137
- if (!isRunning(pid)) {
138
- removePid();
139
- console.log("open-im 进程已不存在");
140
- return;
141
- }
142
- const port = existsSync(PORT_FILE)
143
- ? parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10) || SHUTDOWN_PORT
144
- : SHUTDOWN_PORT;
145
- try {
146
- const res = await fetch(`http://127.0.0.1:${port}/shutdown`, {
147
- signal: AbortSignal.timeout(3000),
148
- });
149
- if (res.ok) {
150
- for (let i = 0; i < 50; i++) {
151
- await new Promise((r) => setTimeout(r, 100));
152
- if (!isRunning(pid))
153
- break;
154
- }
155
- }
156
- }
157
- catch {
158
- // HTTP 失败则用 SIGTERM 兜底
159
- process.kill(pid, "SIGTERM");
160
- await new Promise((r) => setTimeout(r, 500));
161
- }
162
- if (isRunning(pid)) {
163
- process.kill(pid, "SIGKILL");
164
- }
165
- removePid();
166
- try {
167
- if (existsSync(PORT_FILE))
168
- unlinkSync(PORT_FILE);
169
- }
170
- catch {
171
- /* ignore */
172
- }
173
- console.log("\n🔴 open-im 已停止");
174
- console.log(` pid: ${pid}`);
73
+ await stopBackgroundService();
74
+ const result = await stopManagerProcess();
75
+ console.log("\nopen-im stopped.");
76
+ console.log(` pid: ${result.pid}`);
175
77
  }
176
78
  async function cmdInit() {
177
- console.log("\n━━━ open-im 配置向导 ━━━\n");
178
- const saved = await runInteractiveSetup();
179
- if (saved) {
180
- console.log("\n✅ 配置完成!");
181
- console.log("\n现在可以运行以下命令启动服务:");
182
- console.log(" open-im start # 后台运行");
183
- console.log(" open-im dev # 前台运行(调试)");
79
+ console.log("\nopen-im CLI setup\n");
80
+ const saved = await ensureConfigured("init");
81
+ if (!saved) {
82
+ console.log("\nConfiguration was not completed.");
83
+ process.exit(1);
184
84
  }
185
- else {
186
- console.log("\n❌ 配置未完成,已取消。");
85
+ console.log("\nConfiguration saved.");
86
+ console.log("\nYou can start the app with:");
87
+ console.log(" open-im start");
88
+ console.log(" open-im dev");
89
+ }
90
+ async function cmdDev() {
91
+ if (!(await ensureConfigured("dev"))) {
92
+ console.log("Configuration was not completed.");
187
93
  process.exit(1);
188
94
  }
95
+ await main();
189
96
  }
190
97
  function showHelp(exitCode = 0) {
191
98
  console.log(`
192
- 用法: open-im <command>
99
+ Usage: open-im <command>
100
+
101
+ Commands:
102
+ start Run the full app in the background and serve the local config page
103
+ stop Stop the full app
104
+ init Run CLI setup
105
+ dev Run in the foreground for debugging
193
106
 
194
- 命令:
195
- start 后台运行服务
196
- stop 停止后台服务
197
- init 配置向导(首次或追加配置,保留已有平台配置并更新 config.json)
198
- dev 前台运行(调试模式),Ctrl+C 停止
107
+ Local config page:
108
+ http://127.0.0.1:39282
109
+ start keeps it available; dev opens it only during initial setup
199
110
 
200
- 选项:
201
- -h, --help 显示此帮助信息
111
+ Options:
112
+ -h, --help Show this help message
202
113
  `);
203
114
  process.exit(exitCode);
204
115
  }
205
- // ============================================================================
206
- // 命令路由
207
- // ============================================================================
208
116
  const cmd = process.argv[2];
209
117
  const commands = {
210
118
  start: cmdStart,
211
119
  stop: cmdStop,
212
120
  init: cmdInit,
213
- dev: main,
121
+ dev: cmdDev,
214
122
  };
215
123
  if (cmd === "--help" || cmd === "-h") {
216
124
  showHelp(0);
217
125
  }
218
126
  else if (cmd === undefined) {
219
- main().catch((err) => {
127
+ cmdDev().catch((err) => {
220
128
  console.error(err);
221
129
  process.exit(1);
222
130
  });
@@ -228,6 +136,6 @@ else if (commands[cmd]) {
228
136
  });
229
137
  }
230
138
  else {
231
- console.error(`未知命令: ${cmd}`);
139
+ console.error(`Unknown command: ${cmd}`);
232
140
  showHelp(1);
233
141
  }
@@ -0,0 +1,20 @@
1
+ type WebFlowMode = "init" | "start" | "dev";
2
+ type WebFlowResult = "saved" | "cancel";
3
+ export interface StartedWebConfigServer {
4
+ close: () => Promise<void>;
5
+ url: string;
6
+ waitForResult: Promise<WebFlowResult>;
7
+ }
8
+ export declare function getWebConfigPort(): number;
9
+ export declare function getWebConfigUrl(): string;
10
+ export declare function openWebConfigUrl(): void;
11
+ export declare function startWebConfigServer(options: {
12
+ mode: WebFlowMode;
13
+ cwd: string;
14
+ persistent?: boolean;
15
+ }): Promise<StartedWebConfigServer>;
16
+ export declare function runWebConfigFlow(options: {
17
+ mode: WebFlowMode;
18
+ cwd: string;
19
+ }): Promise<WebFlowResult>;
20
+ export {};