@wu529778790/open-im 1.7.1-beta.1 → 1.7.1-beta.3
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 +69 -22
- package/README.zh-CN.md +68 -21
- package/dist/cli.js +7 -0
- package/dist/config-web.d.ts +1 -0
- package/dist/config-web.js +155 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,48 +76,95 @@ WeChat is not in the web UI; configure it in `~/.open-im/config.json` or via `op
|
|
|
76
76
|
|
|
77
77
|
### On a headless server (no GUI)
|
|
78
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
|
|
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 these patterns instead.
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
#### 1) Disable automatic browser launch
|
|
82
82
|
|
|
83
|
-
|
|
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) Safest option: SSH tunnel to local browser
|
|
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
|
+
#### 4) Optional: Remote access with one-time login link
|
|
121
|
+
|
|
122
|
+
If you want to open the config UI directly from another device without SSH tunneling, you can bind the web config server to all interfaces and use a one-time login URL:
|
|
123
|
+
|
|
124
|
+
- **Bind to all interfaces and keep the browser closed on the server:**
|
|
84
125
|
|
|
85
126
|
```bash
|
|
86
127
|
export OPEN_IM_NO_BROWSER=1
|
|
128
|
+
export OPEN_IM_WEB_HOST=0.0.0.0
|
|
87
129
|
open-im start
|
|
88
130
|
```
|
|
89
131
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- **2) Verify that the config page is listening on the server**
|
|
132
|
+
- By default, `OPEN_IM_WEB_HOST` is `127.0.0.1` (local only).
|
|
133
|
+
- Setting it to `0.0.0.0` makes the config page listen on all interfaces.
|
|
93
134
|
|
|
94
|
-
|
|
135
|
+
- **On startup, open-im will log a one-time login URL**, for example:
|
|
95
136
|
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
|
|
137
|
+
```text
|
|
138
|
+
━━━━━━━━ Web Config Login ━━━━━━━━
|
|
139
|
+
Host binding : 0.0.0.0
|
|
140
|
+
Login URL : http://127.0.0.1:39282/?login_token=xxxx
|
|
141
|
+
Note: replace 127.0.0.1 with your server IP or hostname when opening from another device.
|
|
142
|
+
This login link is valid for approximately 15 minutes and can be used only once.
|
|
143
|
+
After login, subsequent requests will use a short-lived session cookie.
|
|
99
144
|
```
|
|
100
145
|
|
|
101
|
-
|
|
146
|
+
- **From your laptop/phone**, replace `127.0.0.1` with the server IP or hostname and open the URL in a browser:
|
|
102
147
|
|
|
103
|
-
|
|
148
|
+
```text
|
|
149
|
+
http://your-server-ip:39282/?login_token=xxxx
|
|
150
|
+
```
|
|
104
151
|
|
|
105
|
-
|
|
152
|
+
The first successful visit:
|
|
106
153
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
```
|
|
154
|
+
- Consumes the one-time `login_token` (subsequent uses will fail with 401);
|
|
155
|
+
- Creates a short-lived session and sets a `openim_session` cookie in your browser;
|
|
156
|
+
- Redirects you to the config page without query parameters.
|
|
111
157
|
|
|
112
|
-
|
|
158
|
+
After that, as long as the `openim_session` cookie is valid and the process is still running, you can continue visiting:
|
|
113
159
|
|
|
114
160
|
```text
|
|
115
|
-
http://
|
|
161
|
+
http://your-server-ip:39282/
|
|
116
162
|
```
|
|
117
163
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
>
|
|
164
|
+
> Security notes:
|
|
165
|
+
>
|
|
166
|
+
> - Binding `OPEN_IM_WEB_HOST=0.0.0.0` exposes the config port on all interfaces. Always combine this with firewall rules / security groups and consider fronting the port with HTTPS + auth via a reverse proxy.
|
|
167
|
+
> - When in doubt, prefer SSH tunneling (step 3) over direct exposure.
|
|
121
168
|
|
|
122
169
|
## Session Behavior
|
|
123
170
|
|
package/README.zh-CN.md
CHANGED
|
@@ -84,46 +84,93 @@ open-im start
|
|
|
84
84
|
|
|
85
85
|
很多服务器没有桌面环境和浏览器,此时「自动打开浏览器」既没意义,还可能因为缺少 `xdg-open` 报错。推荐如下用法:
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
#### 1)关闭自动打开浏览器
|
|
88
88
|
|
|
89
|
-
|
|
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
|
+
#### 4)可选:在服务器上直接访问的一次性登录链接
|
|
127
|
+
|
|
128
|
+
如果你确实希望在服务器上绑定到公网 IP,从其他设备直接访问配置页面,可以:
|
|
129
|
+
|
|
130
|
+
- **将 Web 配置服务绑定到所有网卡:**
|
|
90
131
|
|
|
91
132
|
```bash
|
|
92
133
|
export OPEN_IM_NO_BROWSER=1
|
|
134
|
+
export OPEN_IM_WEB_HOST=0.0.0.0
|
|
93
135
|
open-im start
|
|
94
136
|
```
|
|
95
137
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
- **2)检查配置页面是否已在服务器本机监听**
|
|
138
|
+
- 默认情况下,`OPEN_IM_WEB_HOST` 为 `127.0.0.1`(仅本机访问)。
|
|
139
|
+
- 设置为 `0.0.0.0` 后,配置页面会监听在所有网卡上。
|
|
99
140
|
|
|
100
|
-
|
|
141
|
+
- **启动后,open-im 会在日志中输出一次性登录链接**,类似:
|
|
101
142
|
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
```text
|
|
144
|
+
━━━━━━━━ Web Config Login ━━━━━━━━
|
|
145
|
+
Host binding : 0.0.0.0
|
|
146
|
+
Login URL : http://127.0.0.1:39282/?login_token=xxxx
|
|
147
|
+
Note: replace 127.0.0.1 with your server IP or hostname when opening from another device.
|
|
148
|
+
This login link is valid for approximately 15 minutes and can be used only once.
|
|
149
|
+
After login, subsequent requests will use a short-lived session cookie.
|
|
105
150
|
```
|
|
106
151
|
|
|
107
|
-
|
|
152
|
+
- **在本地电脑或手机浏览器中**,将 `127.0.0.1` 换成服务器 IP 或域名,打开该链接:
|
|
108
153
|
|
|
109
|
-
|
|
154
|
+
```text
|
|
155
|
+
http://your-server-ip:39282/?login_token=xxxx
|
|
156
|
+
```
|
|
110
157
|
|
|
111
|
-
|
|
158
|
+
第一次成功访问会:
|
|
112
159
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```
|
|
160
|
+
- 消费掉这枚一次性 `login_token`(后续再访问同一链接会 401);
|
|
161
|
+
- 在浏览器中创建一个短期会话,设置 `openim_session` Cookie;
|
|
162
|
+
- 自动重定向到不带参数的配置页。
|
|
117
163
|
|
|
118
|
-
|
|
164
|
+
之后,只要 `openim_session` Cookie 仍然有效、进程仍在运行,就可以直接访问:
|
|
119
165
|
|
|
120
166
|
```text
|
|
121
|
-
http://
|
|
167
|
+
http://your-server-ip:39282/
|
|
122
168
|
```
|
|
123
169
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
>
|
|
170
|
+
> 安全提示:
|
|
171
|
+
>
|
|
172
|
+
> - 将 `OPEN_IM_WEB_HOST=0.0.0.0` 意味着该端口会对所有网卡开放,请务必结合防火墙/安全组、尽量配合 HTTPS + 反向代理(例如 Nginx/Caddy 的 Basic Auth 或 OIDC 登录)一起使用。
|
|
173
|
+
> - 如无把握,优先使用上面的 SSH 隧道方案(第 3 步),安全性更高。
|
|
127
174
|
|
|
128
175
|
## 会话说明
|
|
129
176
|
|
package/dist/cli.js
CHANGED
|
@@ -60,6 +60,13 @@ async function cmdStart() {
|
|
|
60
60
|
console.log("\nopen-im started in the background.");
|
|
61
61
|
console.log(` pid: ${child.pid}`);
|
|
62
62
|
console.log(` config page: ${getWebConfigUrl()}`);
|
|
63
|
+
if (process.env.OPEN_IM_WEB_HOST && process.env.OPEN_IM_WEB_HOST !== "127.0.0.1") {
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log("NOTE:");
|
|
66
|
+
console.log(" The config page is bound to OPEN_IM_WEB_HOST.");
|
|
67
|
+
console.log(" A one-time login URL (with login_token) has been printed by the config-web server logger.");
|
|
68
|
+
console.log(" Please use that URL (replacing 127.0.0.1 with your server IP/hostname) for the first login.");
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
async function cmdStop() {
|
|
65
72
|
const status = getManagerStatus();
|
package/dist/config-web.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface StartedWebConfigServer {
|
|
|
5
5
|
close: () => Promise<void>;
|
|
6
6
|
url: string;
|
|
7
7
|
waitForResult: Promise<WebFlowResult>;
|
|
8
|
+
loginUrl?: string;
|
|
8
9
|
}
|
|
9
10
|
export declare function getHealthPlatformSnapshot(file: FileConfig, env?: NodeJS.ProcessEnv): Record<string, {
|
|
10
11
|
configured: boolean;
|
package/dist/config-web.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
4
5
|
import { DWClient } from "dingtalk-stream";
|
|
5
6
|
import { WEB_CONFIG_PORT } from "./constants.js";
|
|
6
7
|
import { CONFIG_PATH, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, loadConfig, loadFileConfig, saveFileConfig } from "./config.js";
|
|
@@ -10,6 +11,99 @@ import { initWeWork, stopWeWork } from "./wework/client.js";
|
|
|
10
11
|
import { createLogger } from "./logger.js";
|
|
11
12
|
const log = createLogger("ConfigWeb");
|
|
12
13
|
const TEST_TIMEOUT_MS = 10000;
|
|
14
|
+
const pendingLogins = new Map();
|
|
15
|
+
const activeSessions = new Map();
|
|
16
|
+
function getWebConfigHost() {
|
|
17
|
+
const envHost = process.env.OPEN_IM_WEB_HOST?.trim();
|
|
18
|
+
if (envHost)
|
|
19
|
+
return envHost;
|
|
20
|
+
return "127.0.0.1";
|
|
21
|
+
}
|
|
22
|
+
function generateRandomToken(bytes = 32) {
|
|
23
|
+
return randomBytes(bytes).toString("base64url");
|
|
24
|
+
}
|
|
25
|
+
function cleanupExpiredAuth(now) {
|
|
26
|
+
for (const [token, info] of pendingLogins) {
|
|
27
|
+
if (info.expiresAt <= now)
|
|
28
|
+
pendingLogins.delete(token);
|
|
29
|
+
}
|
|
30
|
+
for (const [sessionId, info] of activeSessions) {
|
|
31
|
+
if (info.expiresAt <= now)
|
|
32
|
+
activeSessions.delete(sessionId);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function createLoginToken(ttlMs) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
cleanupExpiredAuth(now);
|
|
38
|
+
const token = generateRandomToken(32);
|
|
39
|
+
pendingLogins.set(token, { expiresAt: now + ttlMs });
|
|
40
|
+
return token;
|
|
41
|
+
}
|
|
42
|
+
function createSession(request, ttlMs) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
cleanupExpiredAuth(now);
|
|
45
|
+
const sessionId = generateRandomToken(32);
|
|
46
|
+
const remoteAddr = request.socket.remoteAddress;
|
|
47
|
+
const userAgent = typeof request.headers["user-agent"] === "string" ? request.headers["user-agent"] : undefined;
|
|
48
|
+
activeSessions.set(sessionId, {
|
|
49
|
+
expiresAt: now + ttlMs,
|
|
50
|
+
remoteAddr,
|
|
51
|
+
userAgent,
|
|
52
|
+
});
|
|
53
|
+
return sessionId;
|
|
54
|
+
}
|
|
55
|
+
function parseCookies(request) {
|
|
56
|
+
const header = request.headers.cookie;
|
|
57
|
+
if (!header)
|
|
58
|
+
return {};
|
|
59
|
+
const cookies = {};
|
|
60
|
+
const parts = header.split(";");
|
|
61
|
+
for (const part of parts) {
|
|
62
|
+
const [rawKey, ...rest] = part.split("=");
|
|
63
|
+
const key = rawKey.trim();
|
|
64
|
+
if (!key)
|
|
65
|
+
continue;
|
|
66
|
+
const value = rest.join("=").trim();
|
|
67
|
+
cookies[key] = decodeURIComponent(value);
|
|
68
|
+
}
|
|
69
|
+
return cookies;
|
|
70
|
+
}
|
|
71
|
+
function getSessionIdFromRequest(request) {
|
|
72
|
+
const cookies = parseCookies(request);
|
|
73
|
+
const sessionId = cookies.openim_session;
|
|
74
|
+
return sessionId && typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
75
|
+
}
|
|
76
|
+
function isSessionValid(request) {
|
|
77
|
+
const sessionId = getSessionIdFromRequest(request);
|
|
78
|
+
if (!sessionId)
|
|
79
|
+
return false;
|
|
80
|
+
const info = activeSessions.get(sessionId);
|
|
81
|
+
if (!info)
|
|
82
|
+
return false;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (info.expiresAt <= now) {
|
|
85
|
+
activeSessions.delete(sessionId);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Optional: tie session to basic client fingerprint (remote address)
|
|
89
|
+
const remoteAddr = request.socket.remoteAddress;
|
|
90
|
+
if (info.remoteAddr && remoteAddr && remoteAddr !== info.remoteAddr) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
function buildSessionCookie(sessionId, ttlMs) {
|
|
96
|
+
const maxAgeSec = Math.floor(ttlMs / 1000);
|
|
97
|
+
const parts = [
|
|
98
|
+
`openim_session=${encodeURIComponent(sessionId)}`,
|
|
99
|
+
"Path=/",
|
|
100
|
+
"HttpOnly",
|
|
101
|
+
"SameSite=Lax",
|
|
102
|
+
`Max-Age=${maxAgeSec}`,
|
|
103
|
+
];
|
|
104
|
+
// 不设置 Secure,方便本地 http 使用;如果放在 https 反代后,可以在代理层加 Secure
|
|
105
|
+
return parts.join("; ");
|
|
106
|
+
}
|
|
13
107
|
export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
14
108
|
const fileTelegram = file.platforms?.telegram;
|
|
15
109
|
const fileFeishu = file.platforms?.feishu;
|
|
@@ -514,6 +608,7 @@ export async function startWebConfigServer(options) {
|
|
|
514
608
|
resolve(value);
|
|
515
609
|
};
|
|
516
610
|
});
|
|
611
|
+
const host = getWebConfigHost();
|
|
517
612
|
const server = createServer(async (request, response) => {
|
|
518
613
|
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
519
614
|
const finishFlow = (result) => {
|
|
@@ -522,6 +617,43 @@ export async function startWebConfigServer(options) {
|
|
|
522
617
|
server.close();
|
|
523
618
|
settle(result);
|
|
524
619
|
};
|
|
620
|
+
// Auth gating:
|
|
621
|
+
// - 当仅绑定 127.0.0.1 时,保持完全本地免登录(向后兼容)
|
|
622
|
+
// - 当绑定到 0.0.0.0 或其他地址时,启用一次性登录 + Session Cookie 机制
|
|
623
|
+
const isLocalOnly = host === "127.0.0.1";
|
|
624
|
+
const hasLoginTokenFeature = !isLocalOnly;
|
|
625
|
+
if (hasLoginTokenFeature) {
|
|
626
|
+
const loginToken = requestUrl.searchParams.get("login_token");
|
|
627
|
+
if (loginToken) {
|
|
628
|
+
const info = pendingLogins.get(loginToken);
|
|
629
|
+
const now = Date.now();
|
|
630
|
+
if (info && info.expiresAt > now) {
|
|
631
|
+
// 有效的一次性登录 token:创建会话,设置 Cookie,并重定向到去掉 login_token 的 URL
|
|
632
|
+
pendingLogins.delete(loginToken);
|
|
633
|
+
const sessionTtlMs = 24 * 60 * 60 * 1000; // 24 小时
|
|
634
|
+
const sessionId = createSession(request, sessionTtlMs);
|
|
635
|
+
const cookie = buildSessionCookie(sessionId, sessionTtlMs);
|
|
636
|
+
requestUrl.searchParams.delete("login_token");
|
|
637
|
+
const redirectPath = requestUrl.pathname + (requestUrl.search ? requestUrl.search : "");
|
|
638
|
+
response.writeHead(302, {
|
|
639
|
+
Location: redirectPath || "/",
|
|
640
|
+
"Set-Cookie": cookie,
|
|
641
|
+
});
|
|
642
|
+
response.end();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
// 无效或过期的一次性 token
|
|
646
|
+
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
|
647
|
+
response.end("Invalid or expired login link. Please generate a new one from the server.");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// 其他请求:必须已有有效 session
|
|
651
|
+
if (!isSessionValid(request)) {
|
|
652
|
+
response.writeHead(401, { "content-type": "text/plain; charset=utf-8" });
|
|
653
|
+
response.end("Unauthorized. Please open the latest login URL from the server output.");
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
525
657
|
if (request.method === "GET" && requestUrl.pathname === "/") {
|
|
526
658
|
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
527
659
|
response.end(PAGE_HTML);
|
|
@@ -676,7 +808,7 @@ export async function startWebConfigServer(options) {
|
|
|
676
808
|
}
|
|
677
809
|
reject(error);
|
|
678
810
|
});
|
|
679
|
-
server.listen(port,
|
|
811
|
+
server.listen(port, host, () => resolve());
|
|
680
812
|
});
|
|
681
813
|
const address = server.address();
|
|
682
814
|
if (!address || typeof address === "string") {
|
|
@@ -698,6 +830,24 @@ export async function startWebConfigServer(options) {
|
|
|
698
830
|
if (timer)
|
|
699
831
|
clearTimeout(timer);
|
|
700
832
|
});
|
|
833
|
+
let loginUrlForReturn;
|
|
834
|
+
// 当绑定到非 127.0.0.1(例如 0.0.0.0)时,为远程访问生成一次性登录链接
|
|
835
|
+
if (host !== "127.0.0.1") {
|
|
836
|
+
const loginTtlMs = 15 * 60 * 1000; // 15 分钟内有效
|
|
837
|
+
const loginToken = createLoginToken(loginTtlMs);
|
|
838
|
+
const displayHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
839
|
+
const baseUrl = `http://${displayHost}:${port}`;
|
|
840
|
+
const loginUrl = `${baseUrl}/?login_token=${encodeURIComponent(loginToken)}`;
|
|
841
|
+
loginUrlForReturn = loginUrl;
|
|
842
|
+
log.info("━━━━━━━━ Web Config Login ━━━━━━━━");
|
|
843
|
+
log.info(`Host binding : ${host}`);
|
|
844
|
+
log.info(`Login URL : ${loginUrl}`);
|
|
845
|
+
if (host === "0.0.0.0") {
|
|
846
|
+
log.info("Note: replace 127.0.0.1 with your server IP or hostname when opening from another device.");
|
|
847
|
+
}
|
|
848
|
+
log.info(`This login link is valid for approximately ${Math.floor(loginTtlMs / 60000)} minutes and can be used only once.`);
|
|
849
|
+
log.info("After login, subsequent requests will use a short-lived session cookie.");
|
|
850
|
+
}
|
|
701
851
|
return {
|
|
702
852
|
close: async () => {
|
|
703
853
|
if (timer)
|
|
@@ -706,13 +856,15 @@ export async function startWebConfigServer(options) {
|
|
|
706
856
|
settle("cancel");
|
|
707
857
|
},
|
|
708
858
|
url: `http://127.0.0.1:${port}`,
|
|
859
|
+
loginUrl: loginUrlForReturn,
|
|
709
860
|
waitForResult,
|
|
710
861
|
};
|
|
711
862
|
}
|
|
712
863
|
export async function runWebConfigFlow(options) {
|
|
713
864
|
const started = await startWebConfigServer(options);
|
|
714
|
-
|
|
715
|
-
|
|
865
|
+
const targetUrl = started.loginUrl ?? started.url;
|
|
866
|
+
openBrowser(targetUrl);
|
|
867
|
+
log.info(`Opened local configuration page: ${targetUrl}`);
|
|
716
868
|
log.info(process.env.OPEN_IM_NO_BROWSER === "1" ? "Browser launch disabled. Open the URL manually." : "Save the configuration in your browser to continue.");
|
|
717
869
|
return started.waitForResult;
|
|
718
870
|
}
|