@wu529778790/open-im 1.7.0-beta.3 → 1.7.0-beta.4

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
@@ -57,9 +57,11 @@ The config file is stored at `~/.open-im/config.json` by default.
57
57
  | `open-im stop` | Stop the background service |
58
58
  | `open-im dev` | Run in the foreground for development/debugging |
59
59
 
60
- ## Graphical Config Page
60
+ ## Server Deployment & Config Page
61
61
 
62
- Open the config page at **http://127.0.0.1:39282** (or the URL shown after `open-im start`). The page includes:
62
+ ### Local (with browser)
63
+
64
+ Open the config page at [`http://127.0.0.1:39282`](http://127.0.0.1:39282) (or the URL shown after `open-im start`). The page includes:
63
65
 
64
66
  - **Dashboard** – Configured / Enabled platform count and service status (Idle or Running)
65
67
  - **Platforms** – Enable and configure Telegram, Feishu, QQ, WeCom, and DingTalk (credentials, proxy, per-platform AI tool, allowed user IDs). Each platform has a “Test Configuration” button.
@@ -68,10 +70,55 @@ Open the config page at **http://127.0.0.1:39282** (or the URL shown after `open
68
70
 
69
71
  WeChat is not in the web UI; configure it in `~/.open-im/config.json` or via `open-im init` if needed.
70
72
 
71
- - `open-im start` serves the config page and the bridge.
73
+ - `open-im start` serves both the config page and the bridge.
72
74
  - `open-im dev` opens the page automatically only when setup is incomplete.
73
75
  - To open the page when config already exists, run `open-im start` and visit the URL above.
74
76
 
77
+ ### On a headless server (no GUI)
78
+
79
+ Many servers do not have a desktop environment or browser. In that case, trying to auto-launch a browser (`xdg-open`, `open`, `start`) is unnecessary and may even fail. Use this pattern instead:
80
+
81
+ - **1) Disable automatic browser launch**
82
+
83
+ On the server:
84
+
85
+ ```bash
86
+ export OPEN_IM_NO_BROWSER=1
87
+ open-im start
88
+ ```
89
+
90
+ This starts the bridge and the config web server in the background without attempting to open a browser.
91
+
92
+ - **2) Verify that the config page is listening on the server**
93
+
94
+ On the server:
95
+
96
+ ```bash
97
+ ss -lntp | grep 39282 # or: netstat -lntp | grep 39282
98
+ curl -v http://127.0.0.1:39282/
99
+ ```
100
+
101
+ If you see a `LISTEN` line for `127.0.0.1:39282` and `curl` returns HTML, the config UI is running.
102
+
103
+ - **3) Access the config UI from your local machine via SSH tunnel**
104
+
105
+ Instead of exposing port 39282 to the public internet, use SSH port forwarding:
106
+
107
+ ```bash
108
+ # On your local machine:
109
+ ssh -L 39282:127.0.0.1:39282 user@your-server-ip
110
+ ```
111
+
112
+ Then open in your local browser:
113
+
114
+ ```text
115
+ http://127.0.0.1:39282/
116
+ ```
117
+
118
+ This safely tunnels the config page from the server to your local browser.
119
+
120
+ > If you really want to expose the config UI directly, you can change the listener in `config-web.ts` from `server.listen(port, "127.0.0.1", ...)` to `0.0.0.0` and open port 39282 in your firewall / security group. For security reasons, SSH tunneling is strongly recommended instead.
121
+
75
122
  ## Session Behavior
76
123
 
77
124
  Session context is stored locally in `~/.open-im/data/sessions.json` and is separate from the IM chat history itself. Each user has an independent session directory and session metadata. Sending `/new` resets the current AI session.
package/README.zh-CN.md CHANGED
@@ -57,9 +57,17 @@ open-im start
57
57
  | `open-im stop` | 停止后台服务 |
58
58
  | `open-im dev` | 前台运行(调试模式) |
59
59
 
60
- ## 图形化配置页面
60
+ ## 服务器部署与图形化配置
61
61
 
62
- 在浏览器中打开 **http://127.0.0.1:39282**(或执行 `open-im start` 后提示的地址),页面结构如下:
62
+ ### 本机(带浏览器)使用
63
+
64
+ 在本机直接运行:
65
+
66
+ ```bash
67
+ open-im start
68
+ ```
69
+
70
+ 然后在浏览器中打开 [`http://127.0.0.1:39282`](http://127.0.0.1:39282)(或命令行里提示的地址),页面结构如下:
63
71
 
64
72
  - **概览** – 已配置/已启用平台数量、服务状态(未启动或运行中)
65
73
  - **平台配置** – 启用并填写 Telegram、飞书、QQ、企业微信、钉钉的凭证(Bot Token/App ID/Secret、代理、该平台使用的 AI 工具、白名单用户 ID)。每个平台提供「校验配置」按钮
@@ -72,6 +80,51 @@ open-im start
72
80
  - `open-im dev` 仅在未完成配置时自动打开页面
73
81
  - 已有配置但想手动打开时,执行 `open-im start` 后访问上述地址即可
74
82
 
83
+ ### 在服务器上部署(无图形界面)
84
+
85
+ 很多服务器没有桌面环境和浏览器,此时「自动打开浏览器」既没意义,还可能因为缺少 `xdg-open` 报错。推荐如下用法:
86
+
87
+ - **1)关闭自动打开浏览器**
88
+
89
+ 在服务器上设置环境变量,然后启动:
90
+
91
+ ```bash
92
+ export OPEN_IM_NO_BROWSER=1
93
+ open-im start
94
+ ```
95
+
96
+ 这样只会在后台启动服务与配置页面,不会尝试执行 `xdg-open` / `open` / `start`。
97
+
98
+ - **2)检查配置页面是否已在服务器本机监听**
99
+
100
+ 在服务器上执行:
101
+
102
+ ```bash
103
+ ss -lntp | grep 39282 # 或 netstat -lntp | grep 39282
104
+ curl -v http://127.0.0.1:39282/
105
+ ```
106
+
107
+ 若看到 `LISTEN 0 ... 127.0.0.1:39282` 且 `curl` 返回 HTML,则说明 Web 配置页已正常启动。
108
+
109
+ - **3)通过 SSH 隧道在本地浏览器访问**
110
+
111
+ 不建议直接对外开放 39282 端口,而是使用 SSH 端口转发:
112
+
113
+ ```bash
114
+ # 在本地电脑执行,将本地 39282 转发到服务器 127.0.0.1:39282
115
+ ssh -L 39282:127.0.0.1:39282 user@your-server-ip
116
+ ```
117
+
118
+ 然后在本地浏览器访问:
119
+
120
+ ```text
121
+ http://127.0.0.1:39282/
122
+ ```
123
+
124
+ 即可打开服务器上的配置页面。
125
+
126
+ > 如确有需要,也可以自行修改 `config-web.ts` 中的监听地址,将 `server.listen(port, "127.0.0.1", ...)` 调整为 `0.0.0.0`,并在防火墙/安全组放行 39282 端口。但出于安全考虑,官方推荐使用 SSH 隧道方式。
127
+
75
128
  ## 会话说明
76
129
 
77
130
  会话上下文保存在本地 `~/.open-im/data/sessions.json`,与 IM 聊天记录本身无关。每个用户有独立会话目录和 session 信息,发送 `/new` 会重置当前 AI 会话。
@@ -459,18 +459,38 @@ function toFileConfig(payload, existing) {
459
459
  };
460
460
  }
461
461
  function openBrowser(url) {
462
+ // 显式关闭自动打开浏览器(服务器环境推荐设置)
462
463
  if (process.env.OPEN_IM_NO_BROWSER === "1") {
463
464
  return;
464
465
  }
466
+ // 在无 TTY 且无图形环境(常见于服务器)时直接跳过,避免无意义的 xdg-open 调用
467
+ if (!process.stdout.isTTY && !process.env.DISPLAY) {
468
+ log.info(`Skipping browser launch for URL ${url} (no TTY/DISPLAY detected).`);
469
+ return;
470
+ }
471
+ const safeSpawn = (command, args) => {
472
+ try {
473
+ const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: process.platform === "win32" });
474
+ // 防止 ENOENT 之类的错误变成未捕获异常
475
+ child.on("error", (error) => {
476
+ log.warn(`Failed to launch browser command "${command}": ${error.code ?? error.message}`);
477
+ });
478
+ child.unref();
479
+ }
480
+ catch (error) {
481
+ log.warn(`Failed to spawn browser command "${command}": ${error instanceof Error ? error.message : String(error)}`);
482
+ }
483
+ };
465
484
  if (process.platform === "win32") {
466
- spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true }).unref();
485
+ safeSpawn("cmd", ["/c", "start", "", url]);
467
486
  return;
468
487
  }
469
488
  if (process.platform === "darwin") {
470
- spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
489
+ safeSpawn("open", [url]);
471
490
  return;
472
491
  }
473
- spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
492
+ // linux / 其他 UNIX 平台:优先尝试 xdg-open,失败时仅记录日志,不抛出
493
+ safeSpawn("xdg-open", [url]);
474
494
  }
475
495
  export function getWebConfigPort() {
476
496
  const fromEnv = process.env.OPEN_IM_WEB_PORT ? parseInt(process.env.OPEN_IM_WEB_PORT, 10) : NaN;
package/dist/index.js CHANGED
@@ -103,6 +103,7 @@ function buildShutdownMessage(uptimeMinutes) {
103
103
  ].join("\n");
104
104
  }
105
105
  export async function main() {
106
+ const startupCwd = process.cwd();
106
107
  if (needsSetup()) {
107
108
  const saved = process.stdin.isTTY
108
109
  ? (await runWebConfigFlow({ mode: "dev", cwd: process.cwd() })) === "saved"
@@ -158,9 +159,12 @@ export async function main() {
158
159
  });
159
160
  log.info("Starting open-im bridge...");
160
161
  log.info(`AI 工具: ${getConfiguredAiCommands(config).join(", ")}`);
161
- log.info(`默认会话目录: ${config.claudeWorkDir}`);
162
+ log.info(`默认会话目录(本次启动 cwd): ${startupCwd}`);
163
+ if (startupCwd !== config.claudeWorkDir) {
164
+ log.info(`历史默认会话目录(配置中的 claudeWorkDir): ${config.claudeWorkDir}`);
165
+ }
162
166
  log.info(`启用平台: ${config.enabledPlatforms.join(", ")}`);
163
- const sessionManager = new SessionManager(config.claudeWorkDir);
167
+ const sessionManager = new SessionManager(startupCwd, config.claudeWorkDir);
164
168
  // CLI 工具(Codex/CodeBuddy)的 session 是进程级别的,服务重启后一定无效。
165
169
  // 启动时仅清除 CLI 工具自己的 sessionId,保留 Claude 的持久上下文。
166
170
  sessionManager.clearAllCliSessionIds();
@@ -241,7 +245,7 @@ export async function main() {
241
245
  log.info(`Successfully initialized platforms: ${successfulPlatforms.join(", ")}`);
242
246
  // Send notification only to successfully initialized platforms
243
247
  for (const platform of successfulPlatforms) {
244
- const startupMsg = buildStartupMessage(platform, APP_VERSION, resolvePlatformAiCommand(config, platform), config.claudeWorkDir, sessionManager);
248
+ const startupMsg = buildStartupMessage(platform, APP_VERSION, resolvePlatformAiCommand(config, platform), startupCwd, sessionManager);
245
249
  await sendLifecycleNotification(platform, startupMsg).catch((err) => {
246
250
  log.warn(`Failed to send startup notification to ${platform}:`, err);
247
251
  });
@@ -5,7 +5,11 @@ export declare class SessionManager {
5
5
  private convSessionMap;
6
6
  private defaultWorkDir;
7
7
  private saveTimer;
8
- constructor(defaultWorkDir: string);
8
+ /**
9
+ * @param defaultWorkDir 本次进程的默认工作目录(通常为进程启动时的 cwd)
10
+ * @param previousDefaultWorkDir 旧版本/旧配置使用的默认目录(用于迁移仍跟随默认值的会话)
11
+ */
12
+ constructor(defaultWorkDir: string, previousDefaultWorkDir?: string);
9
13
  getSessionIdForConv(userId: string, convId: string, toolId: ToolId): string | undefined;
10
14
  setSessionIdForConv(userId: string, convId: string, toolId: ToolId, sessionId: string): void;
11
15
  /** 清除指定会话的 sessionId(用于 SDK 报 "No conversation found" 时) */
@@ -30,9 +30,13 @@ export class SessionManager {
30
30
  convSessionMap = new Map();
31
31
  defaultWorkDir;
32
32
  saveTimer = null;
33
- constructor(defaultWorkDir) {
33
+ /**
34
+ * @param defaultWorkDir 本次进程的默认工作目录(通常为进程启动时的 cwd)
35
+ * @param previousDefaultWorkDir 旧版本/旧配置使用的默认目录(用于迁移仍跟随默认值的会话)
36
+ */
37
+ constructor(defaultWorkDir, previousDefaultWorkDir) {
34
38
  this.defaultWorkDir = defaultWorkDir;
35
- this.load();
39
+ this.load(previousDefaultWorkDir);
36
40
  }
37
41
  getSessionIdForConv(userId, convId, toolId) {
38
42
  const s = this.sessions.get(userId);
@@ -234,12 +238,16 @@ export class SessionManager {
234
238
  throw new Error(`目录不存在: \`${resolved}\``);
235
239
  return realpath(resolved);
236
240
  }
237
- load() {
241
+ load(previousDefaultWorkDir) {
238
242
  try {
239
243
  if (existsSync(SESSIONS_FILE)) {
240
244
  const data = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
241
245
  for (const [k, v] of Object.entries(data)) {
242
246
  if (v && typeof v.workDir === 'string') {
247
+ // 如果该会话目录等于旧默认目录,则迁移到新的默认目录(认为用户没有手动 /cd 过)
248
+ if (previousDefaultWorkDir && v.workDir === previousDefaultWorkDir) {
249
+ v.workDir = this.defaultWorkDir;
250
+ }
243
251
  if (!v.activeConvId)
244
252
  v.activeConvId = randomBytes(4).toString('hex');
245
253
  if (!v.sessionIds)
@@ -35,6 +35,7 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
35
35
  let wasThinking = false;
36
36
  let thinkingText = '';
37
37
  let currentSessionId = ctx.sessionId;
38
+ let hadSessionInvalid = false;
38
39
  let activeHandle = null;
39
40
  const toolLines = [];
40
41
  const minDelta = platformAdapter.minContentDeltaChars ?? 0;
@@ -105,8 +106,11 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
105
106
  log.info(`[AITask] No threadId or convId, sessionId not persisted to storage`);
106
107
  },
107
108
  onSessionInvalid: () => {
109
+ hadSessionInvalid = true;
108
110
  if (ctx.convId)
109
111
  sessionManager.clearSessionForConv(ctx.userId, ctx.convId, aiCommand);
112
+ const ok = sessionManager.newSession(ctx.userId);
113
+ log.info(`[AITask] Session invalid for user ${ctx.userId}, aiCommand=${aiCommand}; auto /new applied, ok=${ok}`);
110
114
  },
111
115
  onThinking: (t) => {
112
116
  if (!firstContentLogged) {
@@ -208,8 +212,11 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
208
212
  else if (aiCommand === 'codex' && isUsageLimitError(error)) {
209
213
  log.info(`Keeping codex session for user ${ctx.userId} after usage limit error`);
210
214
  }
215
+ const friendlyError = hadSessionInvalid
216
+ ? '当前 Claude 会话已失效,已自动执行 /new 重置会话,请重新发送刚才的问题。'
217
+ : error;
211
218
  try {
212
- await platformAdapter.sendError(error);
219
+ await platformAdapter.sendError(friendlyError);
213
220
  }
214
221
  catch (err) {
215
222
  log.error('Failed to send error:', err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.7.0-beta.3",
3
+ "version": "1.7.0-beta.4",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",