@wu529778790/open-im 1.5.4-beta.0 → 1.5.5-beta.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/dist/cli.js +45 -150
- package/dist/config-web.d.ts +16 -0
- package/dist/config-web.js +455 -0
- package/dist/config.d.ts +86 -0
- package/dist/config.js +8 -2
- package/dist/index.js +5 -9
- package/dist/service-control.d.ts +15 -0
- package/dist/service-control.js +131 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,88 +1,32 @@
|
|
|
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 {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 { runWebConfigFlow } from "./config-web.js";
|
|
6
|
+
import { getServiceStatus, removePid, startBackgroundService, stopBackgroundService } from "./service-control.js";
|
|
7
|
+
async function ensureConfigured(mode) {
|
|
8
|
+
const forceWeb = process.env.OPEN_IM_FORCE_WEB === "1";
|
|
9
|
+
if (mode !== "init" && !needsSetup()) {
|
|
10
|
+
try {
|
|
11
|
+
loadConfig();
|
|
12
|
+
return true;
|
|
55
13
|
}
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
const saved = await runInteractiveSetup();
|
|
72
|
-
if (!saved) {
|
|
73
|
-
console.log("配置未完成,已取消启动。");
|
|
74
|
-
return false;
|
|
14
|
+
catch (error) {
|
|
15
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
75
16
|
}
|
|
76
|
-
console.log("");
|
|
77
17
|
}
|
|
18
|
+
if (!process.stdin.isTTY && !forceWeb) {
|
|
19
|
+
return runInteractiveSetup();
|
|
20
|
+
}
|
|
21
|
+
const result = await runWebConfigFlow({ mode, cwd: process.cwd() });
|
|
22
|
+
if (result !== "saved")
|
|
23
|
+
return false;
|
|
78
24
|
try {
|
|
79
25
|
loadConfig();
|
|
80
26
|
return true;
|
|
81
27
|
}
|
|
82
|
-
catch (
|
|
83
|
-
|
|
84
|
-
console.error("配置无效或缺少必要字段:", msg);
|
|
85
|
-
console.log("\n请运行以下命令重新配置:\n npx @wu529778790/open-im init");
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
86
30
|
return false;
|
|
87
31
|
}
|
|
88
32
|
}
|
|
@@ -90,16 +34,14 @@ async function validateOrSetup() {
|
|
|
90
34
|
// 命令处理
|
|
91
35
|
// ============================================================================
|
|
92
36
|
async function cmdStart() {
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
37
|
+
const status = getServiceStatus();
|
|
38
|
+
if (status.running && status.pid) {
|
|
95
39
|
console.log("\n🟢 open-im 已在后台运行");
|
|
96
|
-
console.log(` pid: ${pid}`);
|
|
40
|
+
console.log(` pid: ${status.pid}`);
|
|
97
41
|
return;
|
|
98
42
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
if (!(await validateOrSetup())) {
|
|
43
|
+
removePid();
|
|
44
|
+
if (!(await ensureConfigured("start"))) {
|
|
103
45
|
process.exit(1);
|
|
104
46
|
}
|
|
105
47
|
// 检查并自动更新到最新版本
|
|
@@ -107,85 +49,38 @@ async function cmdStart() {
|
|
|
107
49
|
if (updated) {
|
|
108
50
|
process.exit(0);
|
|
109
51
|
}
|
|
110
|
-
|
|
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);
|
|
52
|
+
const child = startBackgroundService(process.cwd());
|
|
128
53
|
console.log("\n🟢 open-im 已在后台启动");
|
|
129
54
|
console.log(` pid: ${child.pid}`);
|
|
130
55
|
}
|
|
131
56
|
async function cmdStop() {
|
|
132
|
-
const
|
|
133
|
-
if (!pid) {
|
|
57
|
+
const status = getServiceStatus();
|
|
58
|
+
if (!status.pid) {
|
|
134
59
|
console.log("open-im 未在后台运行");
|
|
135
60
|
return;
|
|
136
61
|
}
|
|
137
|
-
|
|
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
|
-
}
|
|
62
|
+
const result = await stopBackgroundService();
|
|
173
63
|
console.log("\n🔴 open-im 已停止");
|
|
174
|
-
console.log(` pid: ${pid}`);
|
|
64
|
+
console.log(` pid: ${result.pid}`);
|
|
175
65
|
}
|
|
176
66
|
async function cmdInit() {
|
|
177
|
-
console.log("\n━━━ open-im
|
|
178
|
-
const saved = await
|
|
179
|
-
if (saved) {
|
|
180
|
-
console.log("\n✅ 配置完成!");
|
|
181
|
-
console.log("\n现在可以运行以下命令启动服务:");
|
|
182
|
-
console.log(" open-im start # 后台运行");
|
|
183
|
-
console.log(" open-im dev # 前台运行(调试)");
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
67
|
+
console.log("\n━━━ open-im 本地控制台 ━━━\n");
|
|
68
|
+
const saved = await ensureConfigured("init");
|
|
69
|
+
if (!saved) {
|
|
186
70
|
console.log("\n❌ 配置未完成,已取消。");
|
|
187
71
|
process.exit(1);
|
|
188
72
|
}
|
|
73
|
+
console.log("\n✅ 配置完成!");
|
|
74
|
+
console.log("\n现在可以运行以下命令启动服务:");
|
|
75
|
+
console.log(" open-im start");
|
|
76
|
+
console.log(" open-im dev");
|
|
77
|
+
}
|
|
78
|
+
async function cmdDev() {
|
|
79
|
+
if (!(await ensureConfigured("dev"))) {
|
|
80
|
+
console.log("配置未完成,已取消启动。");
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
await main();
|
|
189
84
|
}
|
|
190
85
|
function showHelp(exitCode = 0) {
|
|
191
86
|
console.log(`
|
|
@@ -194,7 +89,7 @@ function showHelp(exitCode = 0) {
|
|
|
194
89
|
命令:
|
|
195
90
|
start 后台运行服务
|
|
196
91
|
stop 停止后台服务
|
|
197
|
-
init
|
|
92
|
+
init 打开本地 Web 配置页
|
|
198
93
|
dev 前台运行(调试模式),Ctrl+C 停止
|
|
199
94
|
|
|
200
95
|
选项:
|
|
@@ -210,13 +105,13 @@ const commands = {
|
|
|
210
105
|
start: cmdStart,
|
|
211
106
|
stop: cmdStop,
|
|
212
107
|
init: cmdInit,
|
|
213
|
-
dev:
|
|
108
|
+
dev: cmdDev,
|
|
214
109
|
};
|
|
215
110
|
if (cmd === "--help" || cmd === "-h") {
|
|
216
111
|
showHelp(0);
|
|
217
112
|
}
|
|
218
113
|
else if (cmd === undefined) {
|
|
219
|
-
|
|
114
|
+
cmdDev().catch((err) => {
|
|
220
115
|
console.error(err);
|
|
221
116
|
process.exit(1);
|
|
222
117
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
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 startWebConfigServer(options: {
|
|
9
|
+
mode: WebFlowMode;
|
|
10
|
+
cwd: string;
|
|
11
|
+
}): Promise<StartedWebConfigServer>;
|
|
12
|
+
export declare function runWebConfigFlow(options: {
|
|
13
|
+
mode: WebFlowMode;
|
|
14
|
+
cwd: string;
|
|
15
|
+
}): Promise<WebFlowResult>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { CONFIG_PATH, loadConfig, loadFileConfig, saveFileConfig } from "./config.js";
|
|
5
|
+
import { getServiceStatus, startBackgroundService, stopBackgroundService } from "./service-control.js";
|
|
6
|
+
function splitCsv(value) {
|
|
7
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
function clean(value) {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
return trimmed ? trimmed : undefined;
|
|
12
|
+
}
|
|
13
|
+
function readJson(request) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
request.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
17
|
+
request.on("end", () => {
|
|
18
|
+
try {
|
|
19
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
20
|
+
resolve((raw ? JSON.parse(raw) : {}));
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
reject(error);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
request.on("error", reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
function json(response, statusCode, body) {
|
|
30
|
+
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
31
|
+
response.end(JSON.stringify(body));
|
|
32
|
+
}
|
|
33
|
+
function buildInitialPayload(file) {
|
|
34
|
+
return {
|
|
35
|
+
platforms: {
|
|
36
|
+
telegram: {
|
|
37
|
+
enabled: file.platforms?.telegram?.enabled ?? Boolean(file.platforms?.telegram?.botToken),
|
|
38
|
+
botToken: file.platforms?.telegram?.botToken ?? "",
|
|
39
|
+
proxy: file.platforms?.telegram?.proxy ?? "",
|
|
40
|
+
allowedUserIds: (file.platforms?.telegram?.allowedUserIds ?? []).join(", "),
|
|
41
|
+
},
|
|
42
|
+
feishu: {
|
|
43
|
+
enabled: file.platforms?.feishu?.enabled ?? Boolean(file.platforms?.feishu?.appId && file.platforms?.feishu?.appSecret),
|
|
44
|
+
appId: file.platforms?.feishu?.appId ?? "",
|
|
45
|
+
appSecret: file.platforms?.feishu?.appSecret ?? "",
|
|
46
|
+
allowedUserIds: (file.platforms?.feishu?.allowedUserIds ?? []).join(", "),
|
|
47
|
+
},
|
|
48
|
+
wework: {
|
|
49
|
+
enabled: file.platforms?.wework?.enabled ?? Boolean(file.platforms?.wework?.corpId && file.platforms?.wework?.secret),
|
|
50
|
+
corpId: file.platforms?.wework?.corpId ?? "",
|
|
51
|
+
secret: file.platforms?.wework?.secret ?? "",
|
|
52
|
+
wsUrl: file.platforms?.wework?.wsUrl ?? "",
|
|
53
|
+
allowedUserIds: (file.platforms?.wework?.allowedUserIds ?? []).join(", "),
|
|
54
|
+
},
|
|
55
|
+
dingtalk: {
|
|
56
|
+
enabled: file.platforms?.dingtalk?.enabled ?? Boolean(file.platforms?.dingtalk?.clientId && file.platforms?.dingtalk?.clientSecret),
|
|
57
|
+
clientId: file.platforms?.dingtalk?.clientId ?? "",
|
|
58
|
+
clientSecret: file.platforms?.dingtalk?.clientSecret ?? "",
|
|
59
|
+
cardTemplateId: file.platforms?.dingtalk?.cardTemplateId ?? "",
|
|
60
|
+
allowedUserIds: (file.platforms?.dingtalk?.allowedUserIds ?? []).join(", "),
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
ai: {
|
|
64
|
+
aiCommand: file.aiCommand ?? "claude",
|
|
65
|
+
claudeCliPath: file.tools?.claude?.cliPath ?? "claude",
|
|
66
|
+
claudeWorkDir: file.tools?.claude?.workDir ?? process.cwd(),
|
|
67
|
+
claudeSkipPermissions: file.tools?.claude?.skipPermissions ?? true,
|
|
68
|
+
claudeTimeoutMs: file.tools?.claude?.timeoutMs ?? 600000,
|
|
69
|
+
claudeModel: file.tools?.claude?.model ?? "",
|
|
70
|
+
cursorCliPath: file.tools?.cursor?.cliPath ?? "agent",
|
|
71
|
+
codexCliPath: file.tools?.codex?.cliPath ?? "codex",
|
|
72
|
+
codexProxy: file.tools?.codex?.proxy ?? "",
|
|
73
|
+
defaultPermissionMode: file.defaultPermissionMode ?? "ask",
|
|
74
|
+
hookPort: file.hookPort ?? 35801,
|
|
75
|
+
logDir: file.logDir ?? "",
|
|
76
|
+
logLevel: file.logLevel ?? "INFO",
|
|
77
|
+
useSdkMode: file.useSdkMode ?? true,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function validatePayload(payload) {
|
|
82
|
+
const errors = [];
|
|
83
|
+
const enabledCount = Object.values(payload.platforms).filter((item) => item.enabled).length;
|
|
84
|
+
if (enabledCount === 0)
|
|
85
|
+
errors.push("At least one platform must be enabled.");
|
|
86
|
+
if (payload.platforms.telegram.enabled && !clean(payload.platforms.telegram.botToken))
|
|
87
|
+
errors.push("Telegram bot token is required.");
|
|
88
|
+
if (payload.platforms.feishu.enabled && !clean(payload.platforms.feishu.appId))
|
|
89
|
+
errors.push("Feishu app ID is required.");
|
|
90
|
+
if (payload.platforms.feishu.enabled && !clean(payload.platforms.feishu.appSecret))
|
|
91
|
+
errors.push("Feishu app secret is required.");
|
|
92
|
+
if (payload.platforms.wework.enabled && !clean(payload.platforms.wework.corpId))
|
|
93
|
+
errors.push("WeWork corp ID is required.");
|
|
94
|
+
if (payload.platforms.wework.enabled && !clean(payload.platforms.wework.secret))
|
|
95
|
+
errors.push("WeWork secret is required.");
|
|
96
|
+
if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientId))
|
|
97
|
+
errors.push("DingTalk client ID is required.");
|
|
98
|
+
if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientSecret))
|
|
99
|
+
errors.push("DingTalk client secret is required.");
|
|
100
|
+
if (!clean(payload.ai.claudeWorkDir))
|
|
101
|
+
errors.push("Default work directory is required.");
|
|
102
|
+
if (!Number.isFinite(payload.ai.claudeTimeoutMs) || payload.ai.claudeTimeoutMs <= 0)
|
|
103
|
+
errors.push("Claude timeout must be positive.");
|
|
104
|
+
if (!Number.isFinite(payload.ai.hookPort) || payload.ai.hookPort <= 0)
|
|
105
|
+
errors.push("Hook port must be positive.");
|
|
106
|
+
return errors;
|
|
107
|
+
}
|
|
108
|
+
function toFileConfig(payload, existing) {
|
|
109
|
+
return {
|
|
110
|
+
...existing,
|
|
111
|
+
aiCommand: payload.ai.aiCommand,
|
|
112
|
+
defaultPermissionMode: payload.ai.defaultPermissionMode,
|
|
113
|
+
hookPort: payload.ai.hookPort,
|
|
114
|
+
logDir: clean(payload.ai.logDir),
|
|
115
|
+
logLevel: payload.ai.logLevel,
|
|
116
|
+
useSdkMode: payload.ai.useSdkMode,
|
|
117
|
+
tools: {
|
|
118
|
+
claude: {
|
|
119
|
+
...existing.tools?.claude,
|
|
120
|
+
cliPath: clean(payload.ai.claudeCliPath) ?? "claude",
|
|
121
|
+
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
122
|
+
skipPermissions: payload.ai.claudeSkipPermissions,
|
|
123
|
+
timeoutMs: payload.ai.claudeTimeoutMs,
|
|
124
|
+
model: clean(payload.ai.claudeModel),
|
|
125
|
+
},
|
|
126
|
+
cursor: {
|
|
127
|
+
...existing.tools?.cursor,
|
|
128
|
+
cliPath: clean(payload.ai.cursorCliPath) ?? "agent",
|
|
129
|
+
skipPermissions: existing.tools?.cursor?.skipPermissions ?? payload.ai.claudeSkipPermissions,
|
|
130
|
+
},
|
|
131
|
+
codex: {
|
|
132
|
+
...existing.tools?.codex,
|
|
133
|
+
cliPath: clean(payload.ai.codexCliPath) ?? "codex",
|
|
134
|
+
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
135
|
+
skipPermissions: existing.tools?.codex?.skipPermissions ?? payload.ai.claudeSkipPermissions,
|
|
136
|
+
proxy: clean(payload.ai.codexProxy),
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
platforms: {
|
|
140
|
+
...existing.platforms,
|
|
141
|
+
telegram: {
|
|
142
|
+
...existing.platforms?.telegram,
|
|
143
|
+
enabled: payload.platforms.telegram.enabled,
|
|
144
|
+
botToken: clean(payload.platforms.telegram.botToken),
|
|
145
|
+
proxy: clean(payload.platforms.telegram.proxy),
|
|
146
|
+
allowedUserIds: splitCsv(payload.platforms.telegram.allowedUserIds),
|
|
147
|
+
},
|
|
148
|
+
feishu: {
|
|
149
|
+
...existing.platforms?.feishu,
|
|
150
|
+
enabled: payload.platforms.feishu.enabled,
|
|
151
|
+
appId: clean(payload.platforms.feishu.appId),
|
|
152
|
+
appSecret: clean(payload.platforms.feishu.appSecret),
|
|
153
|
+
allowedUserIds: splitCsv(payload.platforms.feishu.allowedUserIds),
|
|
154
|
+
},
|
|
155
|
+
wework: {
|
|
156
|
+
...existing.platforms?.wework,
|
|
157
|
+
enabled: payload.platforms.wework.enabled,
|
|
158
|
+
corpId: clean(payload.platforms.wework.corpId),
|
|
159
|
+
secret: clean(payload.platforms.wework.secret),
|
|
160
|
+
wsUrl: clean(payload.platforms.wework.wsUrl),
|
|
161
|
+
allowedUserIds: splitCsv(payload.platforms.wework.allowedUserIds),
|
|
162
|
+
},
|
|
163
|
+
dingtalk: {
|
|
164
|
+
...existing.platforms?.dingtalk,
|
|
165
|
+
enabled: payload.platforms.dingtalk.enabled,
|
|
166
|
+
clientId: clean(payload.platforms.dingtalk.clientId),
|
|
167
|
+
clientSecret: clean(payload.platforms.dingtalk.clientSecret),
|
|
168
|
+
cardTemplateId: clean(payload.platforms.dingtalk.cardTemplateId),
|
|
169
|
+
allowedUserIds: splitCsv(payload.platforms.dingtalk.allowedUserIds),
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function openBrowser(url) {
|
|
175
|
+
if (process.env.OPEN_IM_NO_BROWSER === "1") {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (process.platform === "win32") {
|
|
179
|
+
spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore", windowsHide: true }).unref();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (process.platform === "darwin") {
|
|
183
|
+
spawn("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
187
|
+
}
|
|
188
|
+
const PAGE_HTML = String.raw `<!doctype html>
|
|
189
|
+
<html lang="en">
|
|
190
|
+
<head>
|
|
191
|
+
<meta charset="utf-8" />
|
|
192
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
193
|
+
<title>open-im local control</title>
|
|
194
|
+
<style>
|
|
195
|
+
:root{--bg:#f2ead8;--panel:rgba(255,251,242,.9);--ink:#13231a;--muted:#56675f;--line:rgba(19,35,26,.12);--green:#1a6a44;--orange:#cf6f31;--red:#9d4236}
|
|
196
|
+
*{box-sizing:border-box}body{margin:0;font-family:Georgia,"Times New Roman",serif;color:var(--ink);background:linear-gradient(135deg,#ebe1ce,#f8f3e9)}
|
|
197
|
+
.shell{padding:24px 16px 40px}.frame{max-width:1180px;margin:0 auto;background:var(--panel);border:1px solid var(--line);box-shadow:0 28px 70px rgba(19,35,26,.14)}
|
|
198
|
+
.hero,.toolbar,.section,.footer{padding:20px 22px;border-bottom:1px solid var(--line)}.hero{background:linear-gradient(120deg,rgba(19,35,26,.96),rgba(26,106,68,.92));color:#f7f0df}
|
|
199
|
+
.hero h1,.hero p{margin:0}.hero h1{font-size:clamp(2rem,4vw,3.4rem);line-height:.95}.hero p{margin-top:12px;max-width:720px}
|
|
200
|
+
.pill{display:inline-flex;align-items:center;gap:8px;padding:8px 12px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,.62);font-size:.9rem}
|
|
201
|
+
.toolbar,.grid,.two-col,.footer,.actions{display:grid;gap:14px}.status-row{display:flex;flex-wrap:wrap;gap:10px}.grid{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}
|
|
202
|
+
.panel{padding:16px;border:1px solid var(--line);background:rgba(255,255,255,.46);transition:opacity .18s ease,transform .18s ease}.panel.off{opacity:.58}.panel-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:12px}
|
|
203
|
+
h2,h3{margin:0}label{display:grid;gap:6px;color:var(--muted);font-size:.92rem}input,select,textarea{width:100%;padding:11px 12px;border:1px solid rgba(19,35,26,.14);background:rgba(255,255,255,.84);font:inherit;color:var(--ink)}
|
|
204
|
+
textarea{min-height:74px;resize:vertical}.two-col{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.toggle{display:inline-flex;align-items:center;gap:10px;color:var(--ink)}.toggle input{width:18px;height:18px}
|
|
205
|
+
.actions{display:flex;flex-wrap:wrap;gap:10px}button{border:0;padding:12px 16px;font:inherit;cursor:pointer;color:#fff7eb;background:var(--ink)}button.secondary{background:var(--green)}button.warning{background:var(--orange)}button.danger{background:var(--red)}button:disabled{opacity:.5;cursor:wait}
|
|
206
|
+
.message{min-height:24px;color:var(--muted)}.message.success{color:var(--green)}.message.error{color:var(--red)}.mono{font-family:Consolas,monospace}.summary{color:var(--muted)}.note{border-left:4px solid var(--orange)}
|
|
207
|
+
</style>
|
|
208
|
+
</head>
|
|
209
|
+
<body>
|
|
210
|
+
<div class="shell">
|
|
211
|
+
<div class="frame">
|
|
212
|
+
<section class="hero">
|
|
213
|
+
<div class="pill">open-im local control</div>
|
|
214
|
+
<h1>Configure fast. Start clean.</h1>
|
|
215
|
+
<p>Local-only configuration for Telegram, Feishu, WeWork, and DingTalk. No accounts. No remote state. No database.</p>
|
|
216
|
+
</section>
|
|
217
|
+
<section class="toolbar">
|
|
218
|
+
<div class="status-row">
|
|
219
|
+
<div class="pill mono" id="configPath"></div>
|
|
220
|
+
<div class="pill" id="serviceState"></div>
|
|
221
|
+
<div class="pill" id="modeBadge"></div>
|
|
222
|
+
</div>
|
|
223
|
+
<div id="statusMeta"></div>
|
|
224
|
+
<div class="summary" id="liveSummary"></div>
|
|
225
|
+
</section>
|
|
226
|
+
<section class="section">
|
|
227
|
+
<div class="panel-head"><h2>Platforms</h2><div>Disabled platforms keep their saved values.</div></div>
|
|
228
|
+
<div class="grid">
|
|
229
|
+
<article class="panel" id="telegram-panel">
|
|
230
|
+
<div class="panel-head"><h3>Telegram</h3><label class="toggle"><input id="telegram-enabled" type="checkbox" /> Enabled</label></div>
|
|
231
|
+
<label>Bot token<input id="telegram-botToken" placeholder="123456:ABC..." /></label>
|
|
232
|
+
<label>Proxy<input id="telegram-proxy" placeholder="http://127.0.0.1:7890" /></label>
|
|
233
|
+
<label>Allowed user IDs<textarea id="telegram-allowedUserIds" placeholder="Comma-separated IDs"></textarea></label>
|
|
234
|
+
</article>
|
|
235
|
+
<article class="panel" id="feishu-panel">
|
|
236
|
+
<div class="panel-head"><h3>Feishu</h3><label class="toggle"><input id="feishu-enabled" type="checkbox" /> Enabled</label></div>
|
|
237
|
+
<label>App ID<input id="feishu-appId" /></label>
|
|
238
|
+
<label>App Secret<input id="feishu-appSecret" /></label>
|
|
239
|
+
<label>Allowed user IDs<textarea id="feishu-allowedUserIds" placeholder="Comma-separated IDs"></textarea></label>
|
|
240
|
+
</article>
|
|
241
|
+
<article class="panel" id="wework-panel">
|
|
242
|
+
<div class="panel-head"><h3>WeWork</h3><label class="toggle"><input id="wework-enabled" type="checkbox" /> Enabled</label></div>
|
|
243
|
+
<label>Corp ID / Bot ID<input id="wework-corpId" /></label>
|
|
244
|
+
<label>Secret<input id="wework-secret" /></label>
|
|
245
|
+
<label>WebSocket URL<input id="wework-wsUrl" placeholder="Optional" /></label>
|
|
246
|
+
<label>Allowed user IDs<textarea id="wework-allowedUserIds" placeholder="Comma-separated IDs"></textarea></label>
|
|
247
|
+
</article>
|
|
248
|
+
<article class="panel" id="dingtalk-panel">
|
|
249
|
+
<div class="panel-head"><h3>DingTalk</h3><label class="toggle"><input id="dingtalk-enabled" type="checkbox" /> Enabled</label></div>
|
|
250
|
+
<label>Client ID / AppKey<input id="dingtalk-clientId" /></label>
|
|
251
|
+
<label>Client Secret / AppSecret<input id="dingtalk-clientSecret" /></label>
|
|
252
|
+
<label>Card template ID<input id="dingtalk-cardTemplateId" placeholder="Optional" /></label>
|
|
253
|
+
<label>Allowed user IDs<textarea id="dingtalk-allowedUserIds" placeholder="Comma-separated IDs"></textarea></label>
|
|
254
|
+
</article>
|
|
255
|
+
</div>
|
|
256
|
+
</section>
|
|
257
|
+
<section class="section">
|
|
258
|
+
<div class="panel-head"><h2>AI Tooling</h2><div>WeChat is intentionally excluded from this first version.</div></div>
|
|
259
|
+
<article class="panel note">Claude credentials are still read from environment variables or <span class="mono">~/.claude/settings.json</span>. This page manages local bridge config, not Claude account auth.</article>
|
|
260
|
+
<article class="panel">
|
|
261
|
+
<div class="two-col">
|
|
262
|
+
<label>Default AI tool<select id="ai-aiCommand"><option value="claude">claude</option><option value="codex">codex</option><option value="cursor">cursor</option></select></label>
|
|
263
|
+
<label>Default work directory<input id="ai-claudeWorkDir" class="mono" /></label>
|
|
264
|
+
<label>Claude CLI path<input id="ai-claudeCliPath" class="mono" /></label>
|
|
265
|
+
<label>Cursor CLI path<input id="ai-cursorCliPath" class="mono" /></label>
|
|
266
|
+
<label>Codex CLI path<input id="ai-codexCliPath" class="mono" /></label>
|
|
267
|
+
<label>Codex proxy<input id="ai-codexProxy" class="mono" placeholder="Optional" /></label>
|
|
268
|
+
<label>Claude timeout (ms)<input id="ai-claudeTimeoutMs" type="number" min="1" /></label>
|
|
269
|
+
<label>Claude model<input id="ai-claudeModel" placeholder="Optional" /></label>
|
|
270
|
+
<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>
|
|
271
|
+
<label>Hook port<input id="ai-hookPort" type="number" min="1" /></label>
|
|
272
|
+
<label>Log directory<input id="ai-logDir" class="mono" /></label>
|
|
273
|
+
<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>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="actions" style="margin-top:14px">
|
|
276
|
+
<label class="toggle"><input id="ai-claudeSkipPermissions" type="checkbox" /> Auto-approve tool permissions</label>
|
|
277
|
+
<label class="toggle"><input id="ai-useSdkMode" type="checkbox" /> Use Claude SDK mode</label>
|
|
278
|
+
</div>
|
|
279
|
+
</article>
|
|
280
|
+
</section>
|
|
281
|
+
<section class="footer">
|
|
282
|
+
<div class="actions">
|
|
283
|
+
<button id="validateButton" class="warning">Validate</button>
|
|
284
|
+
<button id="saveButton" class="secondary">Save config</button>
|
|
285
|
+
<button id="startButton">Start service</button>
|
|
286
|
+
<button id="stopButton" class="danger">Stop service</button>
|
|
287
|
+
</div>
|
|
288
|
+
<div class="message" id="message"></div>
|
|
289
|
+
</section>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
<script>
|
|
293
|
+
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-wsUrl","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-defaultPermissionMode","ai-hookPort","ai-logDir","ai-logLevel","ai-useSdkMode"];
|
|
294
|
+
const el = (id) => document.getElementById(id);
|
|
295
|
+
const setMessage = (text, type="") => { const node = el("message"); node.textContent = text; node.className = ("message " + type).trim(); };
|
|
296
|
+
const setBusy = (busy) => ["validateButton","saveButton","startButton","stopButton"].forEach((id) => { el(id).disabled = busy; });
|
|
297
|
+
function updateVisualState() {
|
|
298
|
+
const enabled = [];
|
|
299
|
+
[["telegram","Telegram"],["feishu","Feishu"],["wework","WeWork"],["dingtalk","DingTalk"]].forEach(([key,label]) => {
|
|
300
|
+
const active = el(key + "-enabled").checked;
|
|
301
|
+
el(key + "-panel").classList.toggle("off", !active);
|
|
302
|
+
if (active) enabled.push(label);
|
|
303
|
+
});
|
|
304
|
+
const aiTool = el("ai-aiCommand").value;
|
|
305
|
+
el("liveSummary").textContent = enabled.length
|
|
306
|
+
? ("Enabled platforms: " + enabled.join(", ") + " | AI tool: " + aiTool)
|
|
307
|
+
: ("No platform enabled yet | AI tool: " + aiTool);
|
|
308
|
+
}
|
|
309
|
+
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, wsUrl: el("wework-wsUrl").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, defaultPermissionMode: el("ai-defaultPermissionMode").value, hookPort: Number(el("ai-hookPort").value || "0"), logDir: el("ai-logDir").value, logLevel: el("ai-logLevel").value, useSdkMode: el("ai-useSdkMode").checked } });
|
|
310
|
+
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; }
|
|
311
|
+
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-wsUrl").value = data.platforms.wework.wsUrl; 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-defaultPermissionMode").value = data.ai.defaultPermissionMode; el("ai-hookPort").value = String(data.ai.hookPort); el("ai-logDir").value = data.ai.logDir; el("ai-logLevel").value = data.ai.logLevel; el("ai-useSdkMode").checked = data.ai.useSdkMode; updateVisualState(); }
|
|
312
|
+
async function refreshStatus() { const data = await request("/api/service/status"); el("serviceState").textContent = data.running ? ("Service running (pid " + data.pid + ")") : "Service stopped"; el("statusMeta").textContent = data.running ? "Background bridge process is active." : "No background bridge process is active."; }
|
|
313
|
+
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); }); }
|
|
314
|
+
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); } }
|
|
315
|
+
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); } }
|
|
316
|
+
async function startService() { setBusy(true); try { await request("/api/config/save", { method: "POST", body: JSON.stringify(payload()) }); const data = await request("/api/service/start", { method: "POST" }); await refreshStatus(); setMessage(data.message, "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
|
|
317
|
+
async function stopService() { setBusy(true); try { const data = await request("/api/service/stop", { method: "POST" }); await refreshStatus(); setMessage(data.message, "success"); } catch (error) { setMessage(error.message || String(error), "error"); } finally { setBusy(false); } }
|
|
318
|
+
el("validateButton").onclick = validate; el("saveButton").onclick = save; el("startButton").onclick = startService; el("stopButton").onclick = stopService; boot();
|
|
319
|
+
</script>
|
|
320
|
+
</body>
|
|
321
|
+
</html>`;
|
|
322
|
+
export async function startWebConfigServer(options) {
|
|
323
|
+
let timer = null;
|
|
324
|
+
let settled = false;
|
|
325
|
+
let settle;
|
|
326
|
+
const waitForResult = new Promise((resolve) => {
|
|
327
|
+
settle = (value) => {
|
|
328
|
+
if (settled)
|
|
329
|
+
return;
|
|
330
|
+
settled = true;
|
|
331
|
+
resolve(value);
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
const server = createServer(async (request, response) => {
|
|
335
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
336
|
+
const finishFlow = (result) => {
|
|
337
|
+
if (timer)
|
|
338
|
+
clearTimeout(timer);
|
|
339
|
+
server.close();
|
|
340
|
+
settle(result);
|
|
341
|
+
};
|
|
342
|
+
if (request.method === "GET" && requestUrl.pathname === "/") {
|
|
343
|
+
response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
344
|
+
response.end(PAGE_HTML);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (request.method === "GET" && requestUrl.pathname === "/api/config") {
|
|
348
|
+
json(response, 200, {
|
|
349
|
+
payload: buildInitialPayload(loadFileConfig()),
|
|
350
|
+
meta: { configPath: CONFIG_PATH, mode: options.mode },
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/config/validate") {
|
|
355
|
+
try {
|
|
356
|
+
const body = await readJson(request);
|
|
357
|
+
const errors = validatePayload(body);
|
|
358
|
+
if (errors.length > 0) {
|
|
359
|
+
json(response, 400, { error: errors.join(" ") });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
json(response, 200, { message: "Configuration looks internally consistent." });
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
366
|
+
}
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/config/save") {
|
|
370
|
+
try {
|
|
371
|
+
const body = await readJson(request);
|
|
372
|
+
const errors = validatePayload(body);
|
|
373
|
+
if (errors.length > 0) {
|
|
374
|
+
json(response, 400, { error: errors.join(" ") });
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
saveFileConfig(toFileConfig(body, loadFileConfig()));
|
|
378
|
+
loadConfig();
|
|
379
|
+
json(response, 200, { message: "Configuration saved." });
|
|
380
|
+
if (requestUrl.searchParams.get("final") === "1") {
|
|
381
|
+
setTimeout(() => finishFlow("saved"), 120);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
386
|
+
}
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (request.method === "GET" && requestUrl.pathname === "/api/service/status") {
|
|
390
|
+
json(response, 200, getServiceStatus());
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/service/start") {
|
|
394
|
+
try {
|
|
395
|
+
loadConfig();
|
|
396
|
+
const started = startBackgroundService(options.cwd);
|
|
397
|
+
json(response, 200, { message: `Background service started with pid ${started.pid}.`, pid: started.pid });
|
|
398
|
+
setTimeout(() => finishFlow("saved"), 120);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/service/stop") {
|
|
406
|
+
try {
|
|
407
|
+
const result = await stopBackgroundService();
|
|
408
|
+
json(response, 200, { message: result.pid ? `Background service stopped (pid ${result.pid}).` : "No background service was running." });
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
json(response, 404, { error: "Not found." });
|
|
416
|
+
});
|
|
417
|
+
await new Promise((resolve) => {
|
|
418
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
419
|
+
});
|
|
420
|
+
const address = server.address();
|
|
421
|
+
if (!address || typeof address === "string") {
|
|
422
|
+
server.close();
|
|
423
|
+
settle("cancel");
|
|
424
|
+
return {
|
|
425
|
+
close: async () => { },
|
|
426
|
+
url: "",
|
|
427
|
+
waitForResult,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
timer = setTimeout(() => {
|
|
431
|
+
server.close();
|
|
432
|
+
settle("cancel");
|
|
433
|
+
}, 15 * 60 * 1000);
|
|
434
|
+
server.on("close", () => {
|
|
435
|
+
if (timer)
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
close: async () => {
|
|
440
|
+
if (timer)
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
server.close();
|
|
443
|
+
settle("cancel");
|
|
444
|
+
},
|
|
445
|
+
url: `http://127.0.0.1:${address.port}`,
|
|
446
|
+
waitForResult,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
export async function runWebConfigFlow(options) {
|
|
450
|
+
const started = await startWebConfigServer(options);
|
|
451
|
+
openBrowser(started.url);
|
|
452
|
+
console.log(`Opened local configuration page: ${started.url}`);
|
|
453
|
+
console.log(process.env.OPEN_IM_NO_BROWSER === "1" ? "Browser launch disabled. Open the URL manually." : "Save the configuration in your browser to continue.");
|
|
454
|
+
return started.waitForResult;
|
|
455
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -73,6 +73,92 @@ export interface Config {
|
|
|
73
73
|
};
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
export interface FilePlatformTelegram {
|
|
77
|
+
enabled?: boolean;
|
|
78
|
+
botToken?: string;
|
|
79
|
+
allowedUserIds?: string[];
|
|
80
|
+
proxy?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface FilePlatformFeishu {
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
appId?: string;
|
|
85
|
+
appSecret?: string;
|
|
86
|
+
allowedUserIds?: string[];
|
|
87
|
+
}
|
|
88
|
+
export interface FilePlatformWechat {
|
|
89
|
+
enabled?: boolean;
|
|
90
|
+
appId?: string;
|
|
91
|
+
appSecret?: string;
|
|
92
|
+
token?: string;
|
|
93
|
+
jwtToken?: string;
|
|
94
|
+
loginKey?: string;
|
|
95
|
+
guid?: string;
|
|
96
|
+
userId?: string;
|
|
97
|
+
wsUrl?: string;
|
|
98
|
+
allowedUserIds?: string[];
|
|
99
|
+
}
|
|
100
|
+
export interface FilePlatformWework {
|
|
101
|
+
enabled?: boolean;
|
|
102
|
+
corpId?: string;
|
|
103
|
+
secret?: string;
|
|
104
|
+
wsUrl?: string;
|
|
105
|
+
allowedUserIds?: string[];
|
|
106
|
+
}
|
|
107
|
+
export interface FilePlatformDingtalk {
|
|
108
|
+
enabled?: boolean;
|
|
109
|
+
clientId?: string;
|
|
110
|
+
clientSecret?: string;
|
|
111
|
+
allowedUserIds?: string[];
|
|
112
|
+
cardTemplateId?: string;
|
|
113
|
+
}
|
|
114
|
+
export interface FileToolClaude {
|
|
115
|
+
cliPath?: string;
|
|
116
|
+
workDir?: string;
|
|
117
|
+
skipPermissions?: boolean;
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
model?: string;
|
|
120
|
+
}
|
|
121
|
+
export interface FileToolCursor {
|
|
122
|
+
cliPath?: string;
|
|
123
|
+
/** 是否跳过权限确认(默认 true,与 tools.claude 共用权限服务器) */
|
|
124
|
+
skipPermissions?: boolean;
|
|
125
|
+
}
|
|
126
|
+
export interface FileToolCodex {
|
|
127
|
+
cliPath?: string;
|
|
128
|
+
workDir?: string;
|
|
129
|
+
/** 是否跳过权限确认(默认 true) */
|
|
130
|
+
skipPermissions?: boolean;
|
|
131
|
+
/** HTTP/HTTPS 代理,用于访问 chatgpt.com(如 http://127.0.0.1:7890) */
|
|
132
|
+
proxy?: string;
|
|
133
|
+
}
|
|
134
|
+
export interface FileConfig {
|
|
135
|
+
telegramBotToken?: string;
|
|
136
|
+
feishuAppId?: string;
|
|
137
|
+
feishuAppSecret?: string;
|
|
138
|
+
allowedUserIds?: string[];
|
|
139
|
+
platforms?: {
|
|
140
|
+
telegram?: FilePlatformTelegram;
|
|
141
|
+
feishu?: FilePlatformFeishu;
|
|
142
|
+
wechat?: FilePlatformWechat;
|
|
143
|
+
wework?: FilePlatformWework;
|
|
144
|
+
dingtalk?: FilePlatformDingtalk;
|
|
145
|
+
};
|
|
146
|
+
env?: Record<string, string>;
|
|
147
|
+
aiCommand?: string;
|
|
148
|
+
tools?: {
|
|
149
|
+
claude?: FileToolClaude;
|
|
150
|
+
cursor?: FileToolCursor;
|
|
151
|
+
codex?: FileToolCodex;
|
|
152
|
+
};
|
|
153
|
+
defaultPermissionMode?: 'ask' | 'accept-edits' | 'plan' | 'yolo';
|
|
154
|
+
hookPort?: number;
|
|
155
|
+
logDir?: string;
|
|
156
|
+
logLevel?: LogLevel;
|
|
157
|
+
useSdkMode?: boolean;
|
|
158
|
+
}
|
|
159
|
+
export declare const CONFIG_PATH: string;
|
|
160
|
+
export declare function loadFileConfig(): FileConfig;
|
|
161
|
+
export declare function saveFileConfig(raw: FileConfig): void;
|
|
76
162
|
/** 检查是否已配置 Claude API 凭证 */
|
|
77
163
|
export declare function hasClaudeCredentials(): boolean;
|
|
78
164
|
/** 检测是否需要交互式配置(无 token 且无环境变量) */
|
package/dist/config.js
CHANGED
|
@@ -9,7 +9,7 @@ import { execFileSync } from 'node:child_process';
|
|
|
9
9
|
import { join, dirname, isAbsolute } from 'node:path';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
11
|
import { APP_HOME } from './constants.js';
|
|
12
|
-
const CONFIG_PATH = join(APP_HOME, 'config.json');
|
|
12
|
+
export const CONFIG_PATH = join(APP_HOME, 'config.json');
|
|
13
13
|
const CODEX_AUTH_PATHS = [
|
|
14
14
|
join(homedir(), '.codex', 'auth.json'),
|
|
15
15
|
join(homedir(), '.config', 'codex', 'auth.json'),
|
|
@@ -99,7 +99,7 @@ function ensureToolsSkipPermissions(raw) {
|
|
|
99
99
|
}
|
|
100
100
|
return changed;
|
|
101
101
|
}
|
|
102
|
-
function loadFileConfig() {
|
|
102
|
+
export function loadFileConfig() {
|
|
103
103
|
try {
|
|
104
104
|
const raw = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
105
105
|
if (!raw || typeof raw !== 'object')
|
|
@@ -119,6 +119,12 @@ function loadFileConfig() {
|
|
|
119
119
|
return {};
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
export function saveFileConfig(raw) {
|
|
123
|
+
const dir = dirname(CONFIG_PATH);
|
|
124
|
+
if (!existsSync(dir))
|
|
125
|
+
mkdirSync(dir, { recursive: true });
|
|
126
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(raw, null, 2), 'utf-8');
|
|
127
|
+
}
|
|
122
128
|
/** 获取用户主目录(兼容不同运行环境,如 launchd、systemd 等) */
|
|
123
129
|
function getClaudeConfigHome() {
|
|
124
130
|
return process.env.HOME || process.env.USERPROFILE || homedir();
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { writeFileSync, existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { loadConfig, needsSetup } from "./config.js";
|
|
5
|
-
import { runInteractiveSetup,
|
|
5
|
+
import { runInteractiveSetup, runClaudeApiSetup } from "./setup.js";
|
|
6
|
+
import { runWebConfigFlow } from "./config-web.js";
|
|
6
7
|
// 导出供 cli.ts 使用
|
|
7
8
|
export { needsSetup, runInteractiveSetup };
|
|
8
9
|
import { initTelegram, stopTelegram } from "./telegram/client.js";
|
|
@@ -91,7 +92,9 @@ function buildStartupMessage(platform, appVersion, aiCommand, defaultWorkDir, su
|
|
|
91
92
|
}
|
|
92
93
|
export async function main() {
|
|
93
94
|
if (needsSetup()) {
|
|
94
|
-
const saved =
|
|
95
|
+
const saved = process.stdin.isTTY
|
|
96
|
+
? (await runWebConfigFlow({ mode: "dev", cwd: process.cwd() })) === "saved"
|
|
97
|
+
: await runInteractiveSetup();
|
|
95
98
|
if (!saved)
|
|
96
99
|
process.exit(1);
|
|
97
100
|
}
|
|
@@ -114,13 +117,6 @@ export async function main() {
|
|
|
114
117
|
throw err;
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
|
-
// 有 TTY 时让用户选择要启用的平台(无论单通道还是多通道)
|
|
118
|
-
if (process.stdin.isTTY) {
|
|
119
|
-
const updated = await runPlatformSelectionPrompt(config);
|
|
120
|
-
if (!updated)
|
|
121
|
-
process.exit(0);
|
|
122
|
-
config = updated;
|
|
123
|
-
}
|
|
124
120
|
initLogger(config.logDir, config.logLevel);
|
|
125
121
|
loadActiveChats();
|
|
126
122
|
initPermissionModes();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function getPid(): number | null;
|
|
2
|
+
export declare function writePid(pid: number): void;
|
|
3
|
+
export declare function removePid(): void;
|
|
4
|
+
export declare function isRunning(pid: number): boolean;
|
|
5
|
+
export declare function getServiceStatus(): {
|
|
6
|
+
running: boolean;
|
|
7
|
+
pid: number | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function startBackgroundService(cwd: string): {
|
|
10
|
+
pid: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function stopBackgroundService(): Promise<{
|
|
13
|
+
pid: number | null;
|
|
14
|
+
stopped: boolean;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { APP_HOME, SHUTDOWN_PORT } from "./constants.js";
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const PID_FILE = join(APP_HOME, "open-im.pid");
|
|
8
|
+
const PORT_FILE = join(APP_HOME, "open-im.port");
|
|
9
|
+
function getServiceEntry() {
|
|
10
|
+
const extension = extname(fileURLToPath(import.meta.url));
|
|
11
|
+
if (extension === ".ts") {
|
|
12
|
+
return {
|
|
13
|
+
command: process.execPath,
|
|
14
|
+
args: ["--import", "tsx", join(__dirname, "index.ts")],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
command: process.execPath,
|
|
19
|
+
args: [join(__dirname, "index.js")],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function getPid() {
|
|
23
|
+
if (!existsSync(PID_FILE))
|
|
24
|
+
return null;
|
|
25
|
+
try {
|
|
26
|
+
const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
|
|
27
|
+
return Number.isNaN(pid) ? null : pid;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function writePid(pid) {
|
|
34
|
+
writeFileSync(PID_FILE, String(pid), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
export function removePid() {
|
|
37
|
+
try {
|
|
38
|
+
if (existsSync(PID_FILE))
|
|
39
|
+
unlinkSync(PID_FILE);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function isRunning(pid) {
|
|
46
|
+
try {
|
|
47
|
+
if (process.platform === "win32") {
|
|
48
|
+
const result = execFileSync("tasklist", ["/FI", `PID eq ${pid}`, "/NH"], {
|
|
49
|
+
stdio: "pipe",
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
}).toString();
|
|
52
|
+
return result.includes(String(pid));
|
|
53
|
+
}
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function getServiceStatus() {
|
|
62
|
+
const pid = getPid();
|
|
63
|
+
if (!pid)
|
|
64
|
+
return { running: false, pid: null };
|
|
65
|
+
if (!isRunning(pid)) {
|
|
66
|
+
removePid();
|
|
67
|
+
return { running: false, pid: null };
|
|
68
|
+
}
|
|
69
|
+
return { running: true, pid };
|
|
70
|
+
}
|
|
71
|
+
export function startBackgroundService(cwd) {
|
|
72
|
+
const current = getServiceStatus();
|
|
73
|
+
if (current.running && current.pid) {
|
|
74
|
+
return { pid: current.pid };
|
|
75
|
+
}
|
|
76
|
+
removePid();
|
|
77
|
+
const entry = getServiceEntry();
|
|
78
|
+
const child = spawn(entry.command, entry.args, {
|
|
79
|
+
detached: true,
|
|
80
|
+
stdio: "ignore",
|
|
81
|
+
cwd,
|
|
82
|
+
env: process.env,
|
|
83
|
+
windowsHide: process.platform === "win32",
|
|
84
|
+
});
|
|
85
|
+
child.unref();
|
|
86
|
+
if (!child.pid) {
|
|
87
|
+
throw new Error("Failed to start background service.");
|
|
88
|
+
}
|
|
89
|
+
writePid(child.pid);
|
|
90
|
+
return { pid: child.pid };
|
|
91
|
+
}
|
|
92
|
+
export async function stopBackgroundService() {
|
|
93
|
+
const pid = getPid();
|
|
94
|
+
if (!pid)
|
|
95
|
+
return { pid: null, stopped: false };
|
|
96
|
+
if (!isRunning(pid)) {
|
|
97
|
+
removePid();
|
|
98
|
+
return { pid, stopped: true };
|
|
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 response = await fetch(`http://127.0.0.1:${port}/shutdown`, {
|
|
105
|
+
signal: AbortSignal.timeout(3000),
|
|
106
|
+
});
|
|
107
|
+
if (response.ok) {
|
|
108
|
+
for (let index = 0; index < 50; index += 1) {
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
110
|
+
if (!isRunning(pid))
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
process.kill(pid, "SIGTERM");
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
118
|
+
}
|
|
119
|
+
if (isRunning(pid)) {
|
|
120
|
+
process.kill(pid, "SIGKILL");
|
|
121
|
+
}
|
|
122
|
+
removePid();
|
|
123
|
+
try {
|
|
124
|
+
if (existsSync(PORT_FILE))
|
|
125
|
+
unlinkSync(PORT_FILE);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* ignore */
|
|
129
|
+
}
|
|
130
|
+
return { pid, stopped: true };
|
|
131
|
+
}
|