@wu529778790/open-im 1.5.5-beta.2 → 1.5.5-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/dist/cli.js +43 -35
- package/dist/config-web.d.ts +4 -0
- package/dist/config-web.js +45 -24
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/manager-control.d.ts +17 -0
- package/dist/manager-control.js +160 -0
- package/dist/manager.d.ts +1 -0
- package/dist/manager.js +37 -0
- package/dist/service-control.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { main, needsSetup, runInteractiveSetup } from "./index.js";
|
|
3
3
|
import { loadConfig } from "./config.js";
|
|
4
4
|
import { checkAndUpdate } from "./check-update.js";
|
|
5
|
-
import { runWebConfigFlow } from "./config-web.js";
|
|
6
|
-
import {
|
|
5
|
+
import { getWebConfigUrl, openWebConfigUrl, runWebConfigFlow } from "./config-web.js";
|
|
6
|
+
import { getManagerStatus, startManagerProcess, stopManagerProcess } from "./manager-control.js";
|
|
7
|
+
import { stopBackgroundService } from "./service-control.js";
|
|
7
8
|
async function ensureConfigured(mode) {
|
|
8
9
|
const forceWeb = process.env.OPEN_IM_FORCE_WEB === "1";
|
|
9
10
|
if (mode !== "init" && !needsSetup()) {
|
|
@@ -30,76 +31,83 @@ async function ensureConfigured(mode) {
|
|
|
30
31
|
return false;
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
// ============================================================================
|
|
34
|
-
// 命令处理
|
|
35
|
-
// ============================================================================
|
|
36
34
|
async function cmdStart() {
|
|
37
|
-
const status =
|
|
35
|
+
const status = getManagerStatus();
|
|
38
36
|
if (status.running && status.pid) {
|
|
39
|
-
console.log("\
|
|
40
|
-
console.log(`
|
|
37
|
+
console.log("\nopen-im is already running in the background.");
|
|
38
|
+
console.log(` pid: ${status.pid}`);
|
|
39
|
+
console.log(` config page: ${getWebConfigUrl()}`);
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
|
-
removePid();
|
|
44
42
|
if (!(await ensureConfigured("start"))) {
|
|
45
43
|
process.exit(1);
|
|
46
44
|
}
|
|
47
|
-
// 检查并自动更新到最新版本
|
|
48
45
|
const { updated } = await checkAndUpdate();
|
|
49
46
|
if (updated) {
|
|
50
47
|
process.exit(0);
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
process.env.OPEN_IM_AUTO_OPEN_CONFIG_ONCE = "1";
|
|
50
|
+
try {
|
|
51
|
+
const child = await startManagerProcess(process.cwd());
|
|
52
|
+
console.log("\nopen-im started in the background.");
|
|
53
|
+
console.log(` pid: ${child.pid}`);
|
|
54
|
+
console.log(` config page: ${getWebConfigUrl()}`);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
delete process.env.OPEN_IM_AUTO_OPEN_CONFIG_ONCE;
|
|
58
|
+
}
|
|
55
59
|
}
|
|
56
60
|
async function cmdStop() {
|
|
57
|
-
const status =
|
|
61
|
+
const status = getManagerStatus();
|
|
58
62
|
if (!status.pid) {
|
|
59
|
-
console.log("open-im
|
|
63
|
+
console.log("open-im is not running in the background.");
|
|
60
64
|
return;
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
console.log(
|
|
66
|
+
await stopBackgroundService();
|
|
67
|
+
const result = await stopManagerProcess();
|
|
68
|
+
console.log("\nopen-im stopped.");
|
|
69
|
+
console.log(` pid: ${result.pid}`);
|
|
65
70
|
}
|
|
66
71
|
async function cmdInit() {
|
|
67
|
-
console.log("\
|
|
72
|
+
console.log("\nopen-im local control\n");
|
|
73
|
+
const status = getManagerStatus();
|
|
74
|
+
if (status.running && status.pid) {
|
|
75
|
+
openWebConfigUrl();
|
|
76
|
+
console.log(`Config page is already running: ${getWebConfigUrl()}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
68
79
|
const saved = await ensureConfigured("init");
|
|
69
80
|
if (!saved) {
|
|
70
|
-
console.log("\
|
|
81
|
+
console.log("\nConfiguration was not completed.");
|
|
71
82
|
process.exit(1);
|
|
72
83
|
}
|
|
73
|
-
console.log("\
|
|
74
|
-
console.log("\
|
|
84
|
+
console.log("\nConfiguration saved.");
|
|
85
|
+
console.log("\nYou can start the app with:");
|
|
75
86
|
console.log(" open-im start");
|
|
76
87
|
console.log(" open-im dev");
|
|
77
88
|
}
|
|
78
89
|
async function cmdDev() {
|
|
79
90
|
if (!(await ensureConfigured("dev"))) {
|
|
80
|
-
console.log("
|
|
91
|
+
console.log("Configuration was not completed.");
|
|
81
92
|
process.exit(1);
|
|
82
93
|
}
|
|
83
94
|
await main();
|
|
84
95
|
}
|
|
85
96
|
function showHelp(exitCode = 0) {
|
|
86
97
|
console.log(`
|
|
87
|
-
|
|
98
|
+
Usage: open-im <command>
|
|
88
99
|
|
|
89
|
-
|
|
90
|
-
start
|
|
91
|
-
stop
|
|
92
|
-
init
|
|
93
|
-
dev
|
|
100
|
+
Commands:
|
|
101
|
+
start Run the full app in the background
|
|
102
|
+
stop Stop the full app
|
|
103
|
+
init Open the local web configuration page
|
|
104
|
+
dev Run in the foreground for debugging
|
|
94
105
|
|
|
95
|
-
|
|
96
|
-
-h, --help
|
|
106
|
+
Options:
|
|
107
|
+
-h, --help Show this help message
|
|
97
108
|
`);
|
|
98
109
|
process.exit(exitCode);
|
|
99
110
|
}
|
|
100
|
-
// ============================================================================
|
|
101
|
-
// 命令路由
|
|
102
|
-
// ============================================================================
|
|
103
111
|
const cmd = process.argv[2];
|
|
104
112
|
const commands = {
|
|
105
113
|
start: cmdStart,
|
|
@@ -123,6 +131,6 @@ else if (commands[cmd]) {
|
|
|
123
131
|
});
|
|
124
132
|
}
|
|
125
133
|
else {
|
|
126
|
-
console.error(
|
|
134
|
+
console.error(`Unknown command: ${cmd}`);
|
|
127
135
|
showHelp(1);
|
|
128
136
|
}
|
package/dist/config-web.d.ts
CHANGED
|
@@ -5,9 +5,13 @@ export interface StartedWebConfigServer {
|
|
|
5
5
|
url: string;
|
|
6
6
|
waitForResult: Promise<WebFlowResult>;
|
|
7
7
|
}
|
|
8
|
+
export declare function getWebConfigPort(): number;
|
|
9
|
+
export declare function getWebConfigUrl(): string;
|
|
10
|
+
export declare function openWebConfigUrl(): void;
|
|
8
11
|
export declare function startWebConfigServer(options: {
|
|
9
12
|
mode: WebFlowMode;
|
|
10
13
|
cwd: string;
|
|
14
|
+
persistent?: boolean;
|
|
11
15
|
}): Promise<StartedWebConfigServer>;
|
|
12
16
|
export declare function runWebConfigFlow(options: {
|
|
13
17
|
mode: WebFlowMode;
|
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 { WEB_CONFIG_PORT } from "./constants.js";
|
|
4
5
|
import { CONFIG_PATH, loadConfig, loadFileConfig, saveFileConfig } from "./config.js";
|
|
5
6
|
import { getServiceStatus, startBackgroundService, stopBackgroundService } from "./service-control.js";
|
|
6
7
|
function splitCsv(value) {
|
|
@@ -72,7 +73,7 @@ function buildInitialPayload(file) {
|
|
|
72
73
|
defaultPermissionMode: file.defaultPermissionMode ?? "ask",
|
|
73
74
|
hookPort: file.hookPort ?? 35801,
|
|
74
75
|
logDir: file.logDir ?? "",
|
|
75
|
-
logLevel: file.logLevel ?? "
|
|
76
|
+
logLevel: file.logLevel ?? "default",
|
|
76
77
|
useSdkMode: file.useSdkMode ?? true,
|
|
77
78
|
},
|
|
78
79
|
};
|
|
@@ -108,10 +109,10 @@ function toFileConfig(payload, existing) {
|
|
|
108
109
|
return {
|
|
109
110
|
...existing,
|
|
110
111
|
aiCommand: payload.ai.aiCommand,
|
|
111
|
-
defaultPermissionMode: payload.ai.defaultPermissionMode,
|
|
112
|
+
defaultPermissionMode: payload.ai.defaultPermissionMode ?? existing.defaultPermissionMode ?? "ask",
|
|
112
113
|
hookPort: payload.ai.hookPort,
|
|
113
|
-
logDir: clean(payload.ai.logDir),
|
|
114
|
-
logLevel: payload.ai.logLevel,
|
|
114
|
+
logDir: payload.ai.logDir === undefined ? existing.logDir : clean(payload.ai.logDir),
|
|
115
|
+
logLevel: payload.ai.logLevel === "default" ? undefined : payload.ai.logLevel,
|
|
115
116
|
useSdkMode: payload.ai.useSdkMode,
|
|
116
117
|
tools: {
|
|
117
118
|
claude: {
|
|
@@ -183,6 +184,16 @@ function openBrowser(url) {
|
|
|
183
184
|
}
|
|
184
185
|
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
185
186
|
}
|
|
187
|
+
export function getWebConfigPort() {
|
|
188
|
+
const fromEnv = process.env.OPEN_IM_WEB_PORT ? parseInt(process.env.OPEN_IM_WEB_PORT, 10) : NaN;
|
|
189
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : WEB_CONFIG_PORT;
|
|
190
|
+
}
|
|
191
|
+
export function getWebConfigUrl() {
|
|
192
|
+
return `http://127.0.0.1:${getWebConfigPort()}`;
|
|
193
|
+
}
|
|
194
|
+
export function openWebConfigUrl() {
|
|
195
|
+
openBrowser(getWebConfigUrl());
|
|
196
|
+
}
|
|
186
197
|
const PAGE_HTML = String.raw `<!doctype html>
|
|
187
198
|
<html lang="en">
|
|
188
199
|
<head>
|
|
@@ -264,10 +275,8 @@ const PAGE_HTML = String.raw `<!doctype html>
|
|
|
264
275
|
<label>Codex proxy<input id="ai-codexProxy" class="mono" placeholder="Optional" /></label>
|
|
265
276
|
<label>Claude timeout (ms)<input id="ai-claudeTimeoutMs" type="number" min="1" /></label>
|
|
266
277
|
<label>Claude model<input id="ai-claudeModel" placeholder="Optional" /></label>
|
|
267
|
-
<label>Permission mode<select id="ai-defaultPermissionMode"><option value="ask">ask</option><option value="accept-edits">accept-edits</option><option value="plan">plan</option><option value="yolo">yolo</option></select></label>
|
|
268
278
|
<label>Hook port<input id="ai-hookPort" type="number" min="1" /></label>
|
|
269
|
-
<label>Log
|
|
270
|
-
<label>Log level<select id="ai-logLevel"><option value="DEBUG">DEBUG</option><option value="INFO">INFO</option><option value="WARN">WARN</option><option value="ERROR">ERROR</option></select></label>
|
|
279
|
+
<label>Log level<select id="ai-logLevel"><option value="default">default</option><option value="DEBUG">DEBUG</option><option value="INFO">INFO</option><option value="WARN">WARN</option><option value="ERROR">ERROR</option></select></label>
|
|
271
280
|
</div>
|
|
272
281
|
<div class="actions" style="margin-top:14px">
|
|
273
282
|
<label class="toggle"><input id="ai-claudeSkipPermissions" type="checkbox" /> Auto-approve tool permissions</label>
|
|
@@ -279,15 +288,15 @@ const PAGE_HTML = String.raw `<!doctype html>
|
|
|
279
288
|
<div class="actions">
|
|
280
289
|
<button id="validateButton" class="warning">Validate</button>
|
|
281
290
|
<button id="saveButton" class="secondary">Save config</button>
|
|
282
|
-
<button id="startButton">Start
|
|
283
|
-
<button id="stopButton" class="danger">Stop
|
|
291
|
+
<button id="startButton">Start bridge</button>
|
|
292
|
+
<button id="stopButton" class="danger">Stop bridge</button>
|
|
284
293
|
</div>
|
|
285
294
|
<div class="message" id="message"></div>
|
|
286
295
|
</section>
|
|
287
296
|
</div>
|
|
288
297
|
</div>
|
|
289
298
|
<script>
|
|
290
|
-
const ids = ["telegram-enabled","telegram-botToken","telegram-proxy","telegram-allowedUserIds","feishu-enabled","feishu-appId","feishu-appSecret","feishu-allowedUserIds","wework-enabled","wework-corpId","wework-secret","wework-allowedUserIds","dingtalk-enabled","dingtalk-clientId","dingtalk-clientSecret","dingtalk-cardTemplateId","dingtalk-allowedUserIds","ai-aiCommand","ai-claudeCliPath","ai-claudeWorkDir","ai-claudeSkipPermissions","ai-claudeTimeoutMs","ai-claudeModel","ai-cursorCliPath","ai-codexCliPath","ai-codexProxy","ai-
|
|
299
|
+
const ids = ["telegram-enabled","telegram-botToken","telegram-proxy","telegram-allowedUserIds","feishu-enabled","feishu-appId","feishu-appSecret","feishu-allowedUserIds","wework-enabled","wework-corpId","wework-secret","wework-allowedUserIds","dingtalk-enabled","dingtalk-clientId","dingtalk-clientSecret","dingtalk-cardTemplateId","dingtalk-allowedUserIds","ai-aiCommand","ai-claudeCliPath","ai-claudeWorkDir","ai-claudeSkipPermissions","ai-claudeTimeoutMs","ai-claudeModel","ai-cursorCliPath","ai-codexCliPath","ai-codexProxy","ai-hookPort","ai-logLevel","ai-useSdkMode"];
|
|
291
300
|
const el = (id) => document.getElementById(id);
|
|
292
301
|
const setMessage = (text, type="") => { const node = el("message"); node.textContent = text; node.className = ("message " + type).trim(); };
|
|
293
302
|
const setBusy = (busy) => ["validateButton","saveButton","startButton","stopButton"].forEach((id) => { el(id).disabled = busy; });
|
|
@@ -303,10 +312,10 @@ const PAGE_HTML = String.raw `<!doctype html>
|
|
|
303
312
|
? ("Enabled platforms: " + enabled.join(", ") + " | AI tool: " + aiTool)
|
|
304
313
|
: ("No platform enabled yet | AI tool: " + aiTool);
|
|
305
314
|
}
|
|
306
|
-
const payload = () => ({ platforms: { telegram: { enabled: el("telegram-enabled").checked, botToken: el("telegram-botToken").value, proxy: el("telegram-proxy").value, allowedUserIds: el("telegram-allowedUserIds").value }, feishu: { enabled: el("feishu-enabled").checked, appId: el("feishu-appId").value, appSecret: el("feishu-appSecret").value, allowedUserIds: el("feishu-allowedUserIds").value }, wework: { enabled: el("wework-enabled").checked, corpId: el("wework-corpId").value, secret: el("wework-secret").value, allowedUserIds: el("wework-allowedUserIds").value }, dingtalk: { enabled: el("dingtalk-enabled").checked, clientId: el("dingtalk-clientId").value, clientSecret: el("dingtalk-clientSecret").value, cardTemplateId: el("dingtalk-cardTemplateId").value, allowedUserIds: el("dingtalk-allowedUserIds").value } }, ai: { aiCommand: el("ai-aiCommand").value, claudeCliPath: el("ai-claudeCliPath").value, claudeWorkDir: el("ai-claudeWorkDir").value, claudeSkipPermissions: el("ai-claudeSkipPermissions").checked, claudeTimeoutMs: Number(el("ai-claudeTimeoutMs").value || "0"), claudeModel: el("ai-claudeModel").value, cursorCliPath: el("ai-cursorCliPath").value, codexCliPath: el("ai-codexCliPath").value, codexProxy: el("ai-codexProxy").value,
|
|
315
|
+
const payload = () => ({ platforms: { telegram: { enabled: el("telegram-enabled").checked, botToken: el("telegram-botToken").value, proxy: el("telegram-proxy").value, allowedUserIds: el("telegram-allowedUserIds").value }, feishu: { enabled: el("feishu-enabled").checked, appId: el("feishu-appId").value, appSecret: el("feishu-appSecret").value, allowedUserIds: el("feishu-allowedUserIds").value }, wework: { enabled: el("wework-enabled").checked, corpId: el("wework-corpId").value, secret: el("wework-secret").value, allowedUserIds: el("wework-allowedUserIds").value }, dingtalk: { enabled: el("dingtalk-enabled").checked, clientId: el("dingtalk-clientId").value, clientSecret: el("dingtalk-clientSecret").value, cardTemplateId: el("dingtalk-cardTemplateId").value, allowedUserIds: el("dingtalk-allowedUserIds").value } }, ai: { aiCommand: el("ai-aiCommand").value, claudeCliPath: el("ai-claudeCliPath").value, claudeWorkDir: el("ai-claudeWorkDir").value, claudeSkipPermissions: el("ai-claudeSkipPermissions").checked, claudeTimeoutMs: Number(el("ai-claudeTimeoutMs").value || "0"), claudeModel: el("ai-claudeModel").value, cursorCliPath: el("ai-cursorCliPath").value, codexCliPath: el("ai-codexCliPath").value, codexProxy: el("ai-codexProxy").value, hookPort: Number(el("ai-hookPort").value || "0"), logLevel: el("ai-logLevel").value, useSdkMode: el("ai-useSdkMode").checked } });
|
|
307
316
|
async function request(path, options={}) { const response = await fetch(path, { headers: { "content-type": "application/json" }, ...options }); const body = await response.json(); if (!response.ok) throw new Error(body.error || "Request failed"); return body; }
|
|
308
|
-
function fill(data, meta) { el("configPath").textContent = meta.configPath; el("modeBadge").textContent = "Flow: " + meta.mode; el("telegram-enabled").checked = data.platforms.telegram.enabled; el("telegram-botToken").value = data.platforms.telegram.botToken; el("telegram-proxy").value = data.platforms.telegram.proxy; el("telegram-allowedUserIds").value = data.platforms.telegram.allowedUserIds; el("feishu-enabled").checked = data.platforms.feishu.enabled; el("feishu-appId").value = data.platforms.feishu.appId; el("feishu-appSecret").value = data.platforms.feishu.appSecret; el("feishu-allowedUserIds").value = data.platforms.feishu.allowedUserIds; el("wework-enabled").checked = data.platforms.wework.enabled; el("wework-corpId").value = data.platforms.wework.corpId; el("wework-secret").value = data.platforms.wework.secret; el("wework-allowedUserIds").value = data.platforms.wework.allowedUserIds; el("dingtalk-enabled").checked = data.platforms.dingtalk.enabled; el("dingtalk-clientId").value = data.platforms.dingtalk.clientId; el("dingtalk-clientSecret").value = data.platforms.dingtalk.clientSecret; el("dingtalk-cardTemplateId").value = data.platforms.dingtalk.cardTemplateId; el("dingtalk-allowedUserIds").value = data.platforms.dingtalk.allowedUserIds; el("ai-aiCommand").value = data.ai.aiCommand; el("ai-claudeCliPath").value = data.ai.claudeCliPath; el("ai-claudeWorkDir").value = data.ai.claudeWorkDir; el("ai-claudeSkipPermissions").checked = data.ai.claudeSkipPermissions; el("ai-claudeTimeoutMs").value = String(data.ai.claudeTimeoutMs); el("ai-claudeModel").value = data.ai.claudeModel; el("ai-cursorCliPath").value = data.ai.cursorCliPath; el("ai-codexCliPath").value = data.ai.codexCliPath; el("ai-codexProxy").value = data.ai.codexProxy; el("ai-
|
|
309
|
-
async function refreshStatus() { const data = await request("/api/service/status"); el("serviceState").textContent = data.running ? ("
|
|
317
|
+
function fill(data, meta) { el("configPath").textContent = meta.configPath; el("modeBadge").textContent = "Flow: " + meta.mode; el("telegram-enabled").checked = data.platforms.telegram.enabled; el("telegram-botToken").value = data.platforms.telegram.botToken; el("telegram-proxy").value = data.platforms.telegram.proxy; el("telegram-allowedUserIds").value = data.platforms.telegram.allowedUserIds; el("feishu-enabled").checked = data.platforms.feishu.enabled; el("feishu-appId").value = data.platforms.feishu.appId; el("feishu-appSecret").value = data.platforms.feishu.appSecret; el("feishu-allowedUserIds").value = data.platforms.feishu.allowedUserIds; el("wework-enabled").checked = data.platforms.wework.enabled; el("wework-corpId").value = data.platforms.wework.corpId; el("wework-secret").value = data.platforms.wework.secret; el("wework-allowedUserIds").value = data.platforms.wework.allowedUserIds; el("dingtalk-enabled").checked = data.platforms.dingtalk.enabled; el("dingtalk-clientId").value = data.platforms.dingtalk.clientId; el("dingtalk-clientSecret").value = data.platforms.dingtalk.clientSecret; el("dingtalk-cardTemplateId").value = data.platforms.dingtalk.cardTemplateId; el("dingtalk-allowedUserIds").value = data.platforms.dingtalk.allowedUserIds; el("ai-aiCommand").value = data.ai.aiCommand; el("ai-claudeCliPath").value = data.ai.claudeCliPath; el("ai-claudeWorkDir").value = data.ai.claudeWorkDir; el("ai-claudeSkipPermissions").checked = data.ai.claudeSkipPermissions; el("ai-claudeTimeoutMs").value = String(data.ai.claudeTimeoutMs); el("ai-claudeModel").value = data.ai.claudeModel; el("ai-cursorCliPath").value = data.ai.cursorCliPath; el("ai-codexCliPath").value = data.ai.codexCliPath; el("ai-codexProxy").value = data.ai.codexProxy; el("ai-hookPort").value = String(data.ai.hookPort); el("ai-logLevel").value = data.ai.logLevel; el("ai-useSdkMode").checked = data.ai.useSdkMode; updateVisualState(); }
|
|
318
|
+
async function refreshStatus() { const data = await request("/api/service/status"); el("serviceState").textContent = data.running ? ("Bridge running (pid " + data.pid + ")") : "Bridge stopped"; el("statusMeta").textContent = data.running ? "Bridge worker is active." : "Bridge worker is currently stopped."; }
|
|
310
319
|
async function boot() { setBusy(true); try { const data = await request("/api/config"); fill(data.payload, data.meta); await refreshStatus(); setMessage("Control surface ready.", "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } setInterval(() => { refreshStatus().catch(() => {}); }, 5000); ids.forEach((id) => { const node = el(id); if (node) node.addEventListener("input", updateVisualState); if (node) node.addEventListener("change", updateVisualState); }); }
|
|
311
320
|
async function validate() { setBusy(true); try { const data = await request("/api/config/validate", { method: "POST", body: JSON.stringify(payload()) }); setMessage(data.message, "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
|
|
312
321
|
async function save() { setBusy(true); try { const data = await request("/api/config/save?final=1", { method: "POST", body: JSON.stringify(payload()) }); setMessage(data.message, "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
|
|
@@ -374,7 +383,7 @@ export async function startWebConfigServer(options) {
|
|
|
374
383
|
saveFileConfig(toFileConfig(body, loadFileConfig()));
|
|
375
384
|
loadConfig();
|
|
376
385
|
json(response, 200, { message: "Configuration saved." });
|
|
377
|
-
if (requestUrl.searchParams.get("final") === "1") {
|
|
386
|
+
if (!options.persistent && requestUrl.searchParams.get("final") === "1") {
|
|
378
387
|
setTimeout(() => finishFlow("saved"), 120);
|
|
379
388
|
}
|
|
380
389
|
}
|
|
@@ -391,8 +400,10 @@ export async function startWebConfigServer(options) {
|
|
|
391
400
|
try {
|
|
392
401
|
loadConfig();
|
|
393
402
|
const started = startBackgroundService(options.cwd);
|
|
394
|
-
json(response, 200, { message: `
|
|
395
|
-
|
|
403
|
+
json(response, 200, { message: `Bridge started with pid ${started.pid}.`, pid: started.pid });
|
|
404
|
+
if (!options.persistent) {
|
|
405
|
+
setTimeout(() => finishFlow("saved"), 120);
|
|
406
|
+
}
|
|
396
407
|
}
|
|
397
408
|
catch (error) {
|
|
398
409
|
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -402,7 +413,7 @@ export async function startWebConfigServer(options) {
|
|
|
402
413
|
if (request.method === "POST" && requestUrl.pathname === "/api/service/stop") {
|
|
403
414
|
try {
|
|
404
415
|
const result = await stopBackgroundService();
|
|
405
|
-
json(response, 200, { message: result.pid ? `
|
|
416
|
+
json(response, 200, { message: result.pid ? `Bridge stopped (pid ${result.pid}).` : "Bridge was already stopped." });
|
|
406
417
|
}
|
|
407
418
|
catch (error) {
|
|
408
419
|
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
@@ -411,8 +422,16 @@ export async function startWebConfigServer(options) {
|
|
|
411
422
|
}
|
|
412
423
|
json(response, 404, { error: "Not found." });
|
|
413
424
|
});
|
|
414
|
-
|
|
415
|
-
|
|
425
|
+
const port = getWebConfigPort();
|
|
426
|
+
await new Promise((resolve, reject) => {
|
|
427
|
+
server.once("error", (error) => {
|
|
428
|
+
if (error.code === "EADDRINUSE") {
|
|
429
|
+
reject(new Error(`Web config port ${port} is already in use. Close the existing listener or change OPEN_IM_WEB_PORT.`));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
reject(error);
|
|
433
|
+
});
|
|
434
|
+
server.listen(port, "127.0.0.1", () => resolve());
|
|
416
435
|
});
|
|
417
436
|
const address = server.address();
|
|
418
437
|
if (!address || typeof address === "string") {
|
|
@@ -424,10 +443,12 @@ export async function startWebConfigServer(options) {
|
|
|
424
443
|
waitForResult,
|
|
425
444
|
};
|
|
426
445
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
446
|
+
if (!options.persistent) {
|
|
447
|
+
timer = setTimeout(() => {
|
|
448
|
+
server.close();
|
|
449
|
+
settle("cancel");
|
|
450
|
+
}, 15 * 60 * 1000);
|
|
451
|
+
}
|
|
431
452
|
server.on("close", () => {
|
|
432
453
|
if (timer)
|
|
433
454
|
clearTimeout(timer);
|
|
@@ -439,7 +460,7 @@ export async function startWebConfigServer(options) {
|
|
|
439
460
|
server.close();
|
|
440
461
|
settle("cancel");
|
|
441
462
|
},
|
|
442
|
-
url: `http://127.0.0.1:${
|
|
463
|
+
url: `http://127.0.0.1:${port}`,
|
|
443
464
|
waitForResult,
|
|
444
465
|
};
|
|
445
466
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare const APP_HOME: string;
|
|
2
2
|
/** 优雅关闭 HTTP 端口(stop 命令通过此端口触发 shutdown) */
|
|
3
3
|
export declare const SHUTDOWN_PORT = 39281;
|
|
4
|
+
/** 本地 Web 配置页固定端口 */
|
|
5
|
+
export declare const WEB_CONFIG_PORT = 39282;
|
|
4
6
|
export declare const IMAGE_DIR: string;
|
|
5
7
|
export declare const TERMINAL_ONLY_COMMANDS: Set<string>;
|
|
6
8
|
/** CardKit 流式更新节流:80ms(约 12 次/秒,cardElement.content 专为打字机设计,支持更高频率) */
|
package/dist/constants.js
CHANGED
|
@@ -3,6 +3,8 @@ import { homedir, tmpdir } from "node:os";
|
|
|
3
3
|
export const APP_HOME = join(homedir(), ".open-im");
|
|
4
4
|
/** 优雅关闭 HTTP 端口(stop 命令通过此端口触发 shutdown) */
|
|
5
5
|
export const SHUTDOWN_PORT = 39281;
|
|
6
|
+
/** 本地 Web 配置页固定端口 */
|
|
7
|
+
export const WEB_CONFIG_PORT = 39282;
|
|
6
8
|
export const IMAGE_DIR = join(tmpdir(), "open-im-images");
|
|
7
9
|
export const TERMINAL_ONLY_COMMANDS = new Set([
|
|
8
10
|
"/context",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function getManagerPid(): number | null;
|
|
2
|
+
export declare function removeManagerPid(): void;
|
|
3
|
+
export declare function removeManagerReady(): void;
|
|
4
|
+
export declare function writeManagerReady(): void;
|
|
5
|
+
export declare function isManagerReady(): boolean;
|
|
6
|
+
export declare function writeManagerPid(pid: number): void;
|
|
7
|
+
export declare function getManagerStatus(): {
|
|
8
|
+
running: boolean;
|
|
9
|
+
pid: number | null;
|
|
10
|
+
};
|
|
11
|
+
export declare function startManagerProcess(cwd: string): Promise<{
|
|
12
|
+
pid: number;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function stopManagerProcess(): Promise<{
|
|
15
|
+
pid: number | null;
|
|
16
|
+
stopped: boolean;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { APP_HOME } from "./constants.js";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PID_FILE = join(APP_HOME, "open-im.pid");
|
|
8
|
+
const READY_FILE = join(APP_HOME, "open-im.ready");
|
|
9
|
+
function getManagerEntry() {
|
|
10
|
+
const extension = extname(fileURLToPath(import.meta.url));
|
|
11
|
+
if (extension === ".ts") {
|
|
12
|
+
return {
|
|
13
|
+
command: process.execPath,
|
|
14
|
+
args: ["--import", "tsx", join(__dirname, "manager.ts")],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
command: process.execPath,
|
|
19
|
+
args: [join(__dirname, "manager.js")],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function isRunning(pid) {
|
|
23
|
+
try {
|
|
24
|
+
if (process.platform === "win32") {
|
|
25
|
+
const result = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/NH"], {
|
|
26
|
+
stdio: "pipe",
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
}).toString();
|
|
29
|
+
return result.includes(String(pid));
|
|
30
|
+
}
|
|
31
|
+
process.kill(pid, 0);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function getManagerPid() {
|
|
39
|
+
if (!existsSync(PID_FILE))
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
43
|
+
return Number.isNaN(pid) ? null : pid;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function removeManagerPid() {
|
|
50
|
+
try {
|
|
51
|
+
if (existsSync(PID_FILE))
|
|
52
|
+
unlinkSync(PID_FILE);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* ignore */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function removeManagerReady() {
|
|
59
|
+
try {
|
|
60
|
+
if (existsSync(READY_FILE))
|
|
61
|
+
unlinkSync(READY_FILE);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
export function writeManagerReady() {
|
|
68
|
+
if (!existsSync(APP_HOME))
|
|
69
|
+
mkdirSync(APP_HOME, { recursive: true });
|
|
70
|
+
writeFileSync(READY_FILE, "1", "utf-8");
|
|
71
|
+
}
|
|
72
|
+
export function isManagerReady() {
|
|
73
|
+
return existsSync(READY_FILE);
|
|
74
|
+
}
|
|
75
|
+
export function writeManagerPid(pid) {
|
|
76
|
+
if (!existsSync(APP_HOME))
|
|
77
|
+
mkdirSync(APP_HOME, { recursive: true });
|
|
78
|
+
writeFileSync(PID_FILE, String(pid), "utf-8");
|
|
79
|
+
}
|
|
80
|
+
export function getManagerStatus() {
|
|
81
|
+
const pid = getManagerPid();
|
|
82
|
+
if (!pid)
|
|
83
|
+
return { running: false, pid: null };
|
|
84
|
+
if (!isRunning(pid)) {
|
|
85
|
+
removeManagerReady();
|
|
86
|
+
removeManagerPid();
|
|
87
|
+
return { running: false, pid: null };
|
|
88
|
+
}
|
|
89
|
+
return { running: true, pid };
|
|
90
|
+
}
|
|
91
|
+
export async function startManagerProcess(cwd) {
|
|
92
|
+
const current = getManagerStatus();
|
|
93
|
+
if (current.running && current.pid) {
|
|
94
|
+
if (isManagerReady()) {
|
|
95
|
+
return { pid: current.pid };
|
|
96
|
+
}
|
|
97
|
+
throw new Error("Manager process exists but is not ready yet.");
|
|
98
|
+
}
|
|
99
|
+
removeManagerReady();
|
|
100
|
+
removeManagerPid();
|
|
101
|
+
const entry = getManagerEntry();
|
|
102
|
+
const child = spawn(entry.command, entry.args, {
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: "ignore",
|
|
105
|
+
cwd,
|
|
106
|
+
env: process.env,
|
|
107
|
+
windowsHide: process.platform === "win32",
|
|
108
|
+
});
|
|
109
|
+
child.unref();
|
|
110
|
+
if (!child.pid) {
|
|
111
|
+
throw new Error("Failed to start manager process.");
|
|
112
|
+
}
|
|
113
|
+
writeManagerPid(child.pid);
|
|
114
|
+
const deadline = Date.now() + 8000;
|
|
115
|
+
while (Date.now() < deadline) {
|
|
116
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
117
|
+
if (!isRunning(child.pid)) {
|
|
118
|
+
removeManagerReady();
|
|
119
|
+
removeManagerPid();
|
|
120
|
+
throw new Error("Manager process exited before becoming ready.");
|
|
121
|
+
}
|
|
122
|
+
if (isManagerReady()) {
|
|
123
|
+
return { pid: child.pid };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
removeManagerReady();
|
|
127
|
+
removeManagerPid();
|
|
128
|
+
try {
|
|
129
|
+
process.kill(child.pid, "SIGTERM");
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* ignore */
|
|
133
|
+
}
|
|
134
|
+
throw new Error("Manager process did not become ready in time.");
|
|
135
|
+
}
|
|
136
|
+
export async function stopManagerProcess() {
|
|
137
|
+
const pid = getManagerPid();
|
|
138
|
+
if (!pid) {
|
|
139
|
+
removeManagerReady();
|
|
140
|
+
return { pid: null, stopped: false };
|
|
141
|
+
}
|
|
142
|
+
if (!isRunning(pid)) {
|
|
143
|
+
removeManagerReady();
|
|
144
|
+
removeManagerPid();
|
|
145
|
+
return { pid, stopped: true };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pid, "SIGTERM");
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
/* ignore */
|
|
153
|
+
}
|
|
154
|
+
if (isRunning(pid)) {
|
|
155
|
+
process.kill(pid, "SIGKILL");
|
|
156
|
+
}
|
|
157
|
+
removeManagerReady();
|
|
158
|
+
removeManagerPid();
|
|
159
|
+
return { pid, stopped: true };
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/manager.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { APP_HOME } from "./constants.js";
|
|
4
|
+
import { openWebConfigUrl, startWebConfigServer } from "./config-web.js";
|
|
5
|
+
import { removeManagerPid, removeManagerReady, writeManagerReady } from "./manager-control.js";
|
|
6
|
+
import { startBackgroundService, stopBackgroundService } from "./service-control.js";
|
|
7
|
+
const CONFIG_UI_ONCE_FILE = join(APP_HOME, ".config-ui-once");
|
|
8
|
+
async function main() {
|
|
9
|
+
const web = await startWebConfigServer({ mode: "start", cwd: process.cwd(), persistent: true });
|
|
10
|
+
startBackgroundService(process.cwd());
|
|
11
|
+
writeManagerReady();
|
|
12
|
+
if (process.env.OPEN_IM_AUTO_OPEN_CONFIG_ONCE === "1" && !existsSync(CONFIG_UI_ONCE_FILE)) {
|
|
13
|
+
if (!existsSync(APP_HOME))
|
|
14
|
+
mkdirSync(APP_HOME, { recursive: true });
|
|
15
|
+
writeFileSync(CONFIG_UI_ONCE_FILE, "1", "utf-8");
|
|
16
|
+
openWebConfigUrl();
|
|
17
|
+
}
|
|
18
|
+
const shutdown = async () => {
|
|
19
|
+
await web.close().catch(() => { });
|
|
20
|
+
await stopBackgroundService().catch(() => { });
|
|
21
|
+
removeManagerReady();
|
|
22
|
+
removeManagerPid();
|
|
23
|
+
process.exit(0);
|
|
24
|
+
};
|
|
25
|
+
process.on("SIGINT", () => shutdown().catch(() => process.exit(1)));
|
|
26
|
+
process.on("SIGTERM", () => shutdown().catch(() => process.exit(1)));
|
|
27
|
+
}
|
|
28
|
+
const isEntry = process.argv[1]?.replace(/\\/g, "/").endsWith("/manager.js") ||
|
|
29
|
+
process.argv[1]?.replace(/\\/g, "/").endsWith("/manager.ts");
|
|
30
|
+
if (isEntry) {
|
|
31
|
+
main().catch((error) => {
|
|
32
|
+
console.error("Manager fatal error:", error);
|
|
33
|
+
removeManagerReady();
|
|
34
|
+
removeManagerPid();
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
37
|
+
}
|
package/dist/service-control.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, extname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const PID_FILE = join(APP_HOME, "open-im.pid");
|
|
7
|
+
const PID_FILE = join(APP_HOME, "open-im-worker.pid");
|
|
8
8
|
const PORT_FILE = join(APP_HOME, "open-im.port");
|
|
9
9
|
function getServiceEntry() {
|
|
10
10
|
const extension = extname(fileURLToPath(import.meta.url));
|