@wu529778790/open-im 1.11.8-beta.2 → 1.11.8-beta.21
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/adapters/codebuddy-adapter.js +3 -17
- package/dist/adapters/codex-adapter.js +3 -16
- package/dist/adapters/opencode-adapter.d.ts +0 -5
- package/dist/adapters/opencode-adapter.js +10 -25
- package/dist/adapters/registry.js +10 -1
- package/dist/clawbot/client.js +72 -78
- package/dist/clawbot/qr-login.d.ts +1 -0
- package/dist/clawbot/qr-login.js +7 -0
- package/dist/codebuddy/cli-runner.d.ts +2 -22
- package/dist/codebuddy/cli-runner.js +9 -27
- package/dist/codex/cli-runner.d.ts +2 -22
- package/dist/codex/cli-runner.js +24 -75
- package/dist/commands/handler.d.ts +1 -0
- package/dist/commands/handler.js +34 -0
- package/dist/config/types.d.ts +23 -0
- package/dist/config-web-auth.d.ts +13 -0
- package/dist/config-web-auth.js +114 -0
- package/dist/config-web-browser.d.ts +4 -0
- package/dist/config-web-browser.js +48 -0
- package/dist/config-web-cors.d.ts +5 -0
- package/dist/config-web-cors.js +48 -0
- package/dist/config-web-health.d.ts +7 -0
- package/dist/config-web-health.js +65 -0
- package/dist/config-web-http.d.ts +3 -0
- package/dist/config-web-http.js +31 -0
- package/dist/config-web-page-i18n.d.ts +36 -2
- package/dist/config-web-page-i18n.js +36 -2
- package/dist/config-web-payload.d.ts +84 -0
- package/dist/config-web-payload.js +260 -0
- package/dist/config-web-probes.d.ts +2 -0
- package/dist/config-web-probes.js +271 -0
- package/dist/config-web.d.ts +3 -11
- package/dist/config-web.js +45 -815
- package/dist/config.js +22 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.js +11 -0
- package/dist/opencode/cli-runner.d.ts +2 -22
- package/dist/opencode/cli-runner.js +47 -68
- package/dist/opencode/sdk-manager.d.ts +11 -0
- package/dist/opencode/sdk-manager.js +46 -0
- package/dist/opencode/sdk-runner.d.ts +7 -0
- package/dist/opencode/sdk-runner.js +225 -0
- package/dist/platform/handle-ai-request.js +18 -1
- package/dist/qq/client.js +45 -53
- package/dist/shared/ai-task.d.ts +15 -0
- package/dist/shared/ai-task.js +174 -2
- package/dist/shared/cli-runner-base.d.ts +39 -0
- package/dist/shared/cli-runner-base.js +108 -0
- package/dist/shared/connection-manager.d.ts +111 -0
- package/dist/shared/connection-manager.js +157 -0
- package/dist/shared/session-invalid-detector.d.ts +5 -0
- package/dist/shared/session-invalid-detector.js +20 -0
- package/dist/wework/client.js +79 -127
- package/dist/workbuddy/client.js +40 -74
- package/package.json +4 -1
- package/web/dist/assets/index-6e51Wtaa.css +1 -0
- package/web/dist/assets/index-Da9qKWA2.js +57 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-XKVTb-0p.css +0 -1
- package/web/dist/assets/index-yDVGC_84.js +0 -57
package/dist/config-web.js
CHANGED
|
@@ -1,822 +1,30 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { createServer } from "node:http";
|
|
3
2
|
import { URL } from "node:url";
|
|
4
|
-
import { randomBytes } from "node:crypto";
|
|
5
3
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
6
4
|
import { join, dirname } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { CONFIG_PATH, getClaudeConfigHome, loadClaudeSettingsEnv, saveClaudeSettingsEnv, loadConfig, loadFileConfig, saveFileConfig, normalizeAiCommand, CODEX_AUTH_PATHS, } from "./config.js";
|
|
5
|
+
import { getPublicWebDashboardUrl } from "./constants.js";
|
|
6
|
+
import { CONFIG_PATH, getClaudeConfigHome, loadClaudeSettingsEnv, loadConfig, loadFileConfig, saveFileConfig, CODEX_AUTH_PATHS, } from "./config.js";
|
|
10
7
|
import { getWebDistDir, tryServeDashboardStatic } from "./config-web-static.js";
|
|
8
|
+
import { getWebConfigHost, allowRemoteApiWithoutAuth, consumeLoginToken, createSession, isSessionValid, buildSessionCookie, generateLoginUrl, } from "./config-web-auth.js";
|
|
9
|
+
import { corsHeadersFor, mergeCors } from "./config-web-cors.js";
|
|
10
|
+
export { getHealthPlatformSnapshot } from "./config-web-health.js";
|
|
11
|
+
import { getHealthPlatformSnapshot } from "./config-web-health.js";
|
|
12
|
+
import { readJson, jsonResponse as json } from "./config-web-http.js";
|
|
13
|
+
import { openBrowser, getWebConfigPort, } from "./config-web-browser.js";
|
|
14
|
+
export { getWebConfigUrl } from "./config-web-browser.js";
|
|
15
|
+
import { buildInitialPayload, validatePayload, toFileConfig, isMasked, clean, } from "./config-web-payload.js";
|
|
16
|
+
import { testPlatformConfig, toErrorMessage } from "./config-web-probes.js";
|
|
17
|
+
export { testPlatformConfig } from "./config-web-probes.js";
|
|
11
18
|
import { getServiceStatus, startBackgroundService, stopBackgroundService } from "./service-control.js";
|
|
12
|
-
import { initWeWork, stopWeWork } from "./wework/client.js";
|
|
13
19
|
import { createLogger } from "./logger.js";
|
|
14
20
|
const log = createLogger("ConfigWeb");
|
|
15
21
|
// 已移至 constants.ts;
|
|
22
|
+
// getClaudeSettingsPath is used by /api/claude/settings handler
|
|
16
23
|
function getClaudeSettingsPath() {
|
|
17
24
|
const home = getClaudeConfigHome();
|
|
18
25
|
const baseDir = join(home, ".claude");
|
|
19
26
|
return join(baseDir, "settings.json");
|
|
20
27
|
}
|
|
21
|
-
const pendingLogins = new Map();
|
|
22
|
-
const activeSessions = new Map();
|
|
23
|
-
function getWebConfigHost() {
|
|
24
|
-
const envHost = process.env.OPEN_IM_WEB_HOST?.trim();
|
|
25
|
-
if (envHost)
|
|
26
|
-
return envHost;
|
|
27
|
-
return "127.0.0.1";
|
|
28
|
-
}
|
|
29
|
-
/** 设为 true 时,非本机绑定的 Web 配置服务跳过登录 Cookie 校验(仅适用于受信网络;生产建议配合 HTTPS 反向代理) */
|
|
30
|
-
function allowRemoteApiWithoutAuth() {
|
|
31
|
-
return process.env.OPEN_IM_ALLOW_REMOTE_API === "true";
|
|
32
|
-
}
|
|
33
|
-
/** 是否允许该浏览器 Origin(与 getPublicWebDashboardUrl() 同源时始终允许,便于反代 / 自定义 OPEN_IM_PUBLIC_WEB_URL) */
|
|
34
|
-
function isCorsOriginAllowed(origin) {
|
|
35
|
-
const publicWeb = getPublicWebDashboardUrl();
|
|
36
|
-
try {
|
|
37
|
-
if (origin === new URL(publicWeb).origin)
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
if (origin === publicWeb)
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
const allowlist = splitCsv(process.env.OPEN_IM_CORS_ORIGINS);
|
|
45
|
-
if (allowlist.length === 0)
|
|
46
|
-
return true;
|
|
47
|
-
return allowlist.includes(origin);
|
|
48
|
-
}
|
|
49
|
-
/** 有 Origin 时返回 CORS 响应头;无 Origin(如本地 file:// 或同源内置页)不返回,行为与原来一致 */
|
|
50
|
-
function corsHeadersFor(request) {
|
|
51
|
-
const originRaw = request.headers.origin;
|
|
52
|
-
if (!originRaw || typeof originRaw !== "string")
|
|
53
|
-
return undefined;
|
|
54
|
-
if (!isCorsOriginAllowed(originRaw))
|
|
55
|
-
return undefined;
|
|
56
|
-
const requestedHeaders = request.headers["access-control-request-headers"];
|
|
57
|
-
const allowHeaders = typeof requestedHeaders === "string" && requestedHeaders.trim()
|
|
58
|
-
? requestedHeaders
|
|
59
|
-
: "Content-Type, Authorization";
|
|
60
|
-
return {
|
|
61
|
-
"Access-Control-Allow-Origin": originRaw,
|
|
62
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
63
|
-
"Access-Control-Allow-Headers": allowHeaders,
|
|
64
|
-
"Access-Control-Allow-Credentials": "true",
|
|
65
|
-
"Access-Control-Max-Age": "86400",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function mergeCors(request, headers) {
|
|
69
|
-
const cors = corsHeadersFor(request);
|
|
70
|
-
if (!cors)
|
|
71
|
-
return headers;
|
|
72
|
-
return { ...headers, ...cors };
|
|
73
|
-
}
|
|
74
|
-
function generateRandomToken(bytes = 32) {
|
|
75
|
-
return randomBytes(bytes).toString("base64url");
|
|
76
|
-
}
|
|
77
|
-
function cleanupExpiredAuth(now) {
|
|
78
|
-
for (const [token, info] of pendingLogins) {
|
|
79
|
-
if (info.expiresAt <= now)
|
|
80
|
-
pendingLogins.delete(token);
|
|
81
|
-
}
|
|
82
|
-
for (const [sessionId, info] of activeSessions) {
|
|
83
|
-
if (info.expiresAt <= now)
|
|
84
|
-
activeSessions.delete(sessionId);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function createLoginToken(ttlMs) {
|
|
88
|
-
const now = Date.now();
|
|
89
|
-
cleanupExpiredAuth(now);
|
|
90
|
-
const token = generateRandomToken(32);
|
|
91
|
-
pendingLogins.set(token, { expiresAt: now + ttlMs });
|
|
92
|
-
return token;
|
|
93
|
-
}
|
|
94
|
-
function createSession(request, ttlMs) {
|
|
95
|
-
const now = Date.now();
|
|
96
|
-
cleanupExpiredAuth(now);
|
|
97
|
-
const sessionId = generateRandomToken(32);
|
|
98
|
-
const remoteAddr = request.socket.remoteAddress;
|
|
99
|
-
const userAgent = typeof request.headers["user-agent"] === "string" ? request.headers["user-agent"] : undefined;
|
|
100
|
-
activeSessions.set(sessionId, {
|
|
101
|
-
expiresAt: now + ttlMs,
|
|
102
|
-
remoteAddr,
|
|
103
|
-
userAgent,
|
|
104
|
-
});
|
|
105
|
-
return sessionId;
|
|
106
|
-
}
|
|
107
|
-
function parseCookies(request) {
|
|
108
|
-
const header = request.headers.cookie;
|
|
109
|
-
if (!header)
|
|
110
|
-
return {};
|
|
111
|
-
const cookies = {};
|
|
112
|
-
const parts = header.split(";");
|
|
113
|
-
for (const part of parts) {
|
|
114
|
-
const [rawKey, ...rest] = part.split("=");
|
|
115
|
-
const key = rawKey.trim();
|
|
116
|
-
if (!key)
|
|
117
|
-
continue;
|
|
118
|
-
const value = rest.join("=").trim();
|
|
119
|
-
cookies[key] = decodeURIComponent(value);
|
|
120
|
-
}
|
|
121
|
-
return cookies;
|
|
122
|
-
}
|
|
123
|
-
function getSessionIdFromRequest(request) {
|
|
124
|
-
const cookies = parseCookies(request);
|
|
125
|
-
const sessionId = cookies.openim_session;
|
|
126
|
-
return sessionId && typeof sessionId === "string" && sessionId.length > 0 ? sessionId : null;
|
|
127
|
-
}
|
|
128
|
-
function isSessionValid(request) {
|
|
129
|
-
const sessionId = getSessionIdFromRequest(request);
|
|
130
|
-
if (!sessionId)
|
|
131
|
-
return false;
|
|
132
|
-
const info = activeSessions.get(sessionId);
|
|
133
|
-
if (!info)
|
|
134
|
-
return false;
|
|
135
|
-
const now = Date.now();
|
|
136
|
-
if (info.expiresAt <= now) {
|
|
137
|
-
activeSessions.delete(sessionId);
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
// Optional: tie session to basic client fingerprint (remote address)
|
|
141
|
-
const remoteAddr = request.socket.remoteAddress;
|
|
142
|
-
if (info.remoteAddr && remoteAddr && remoteAddr !== info.remoteAddr) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
function buildSessionCookie(sessionId, ttlMs) {
|
|
148
|
-
const maxAgeSec = Math.floor(ttlMs / 1000);
|
|
149
|
-
const parts = [
|
|
150
|
-
`openim_session=${encodeURIComponent(sessionId)}`,
|
|
151
|
-
"Path=/",
|
|
152
|
-
"HttpOnly",
|
|
153
|
-
"SameSite=Lax",
|
|
154
|
-
`Max-Age=${maxAgeSec}`,
|
|
155
|
-
];
|
|
156
|
-
// 不设置 Secure,方便本地 http 使用;如果放在 https 反代后,可以在代理层加 Secure
|
|
157
|
-
return parts.join("; ");
|
|
158
|
-
}
|
|
159
|
-
export function getHealthPlatformSnapshot(file, env = process.env) {
|
|
160
|
-
const fileTelegram = file.platforms?.telegram;
|
|
161
|
-
const fileFeishu = file.platforms?.feishu;
|
|
162
|
-
const fileQQ = file.platforms?.qq;
|
|
163
|
-
const fileWework = file.platforms?.wework;
|
|
164
|
-
const fileDingtalk = file.platforms?.dingtalk;
|
|
165
|
-
const fileWorkbuddy = file.platforms?.workbuddy;
|
|
166
|
-
const fileClawbot = file.platforms?.clawbot;
|
|
167
|
-
const telegramBotToken = env.TELEGRAM_BOT_TOKEN ?? fileTelegram?.botToken ?? file.telegramBotToken;
|
|
168
|
-
const feishuAppId = env.FEISHU_APP_ID ?? fileFeishu?.appId ?? file.feishuAppId;
|
|
169
|
-
const feishuAppSecret = env.FEISHU_APP_SECRET ?? fileFeishu?.appSecret ?? file.feishuAppSecret;
|
|
170
|
-
const qqAppId = env.QQ_BOT_APPID ?? env.QQ_APP_ID ?? fileQQ?.appId;
|
|
171
|
-
const qqSecret = env.QQ_BOT_SECRET ?? env.QQ_SECRET ?? fileQQ?.secret;
|
|
172
|
-
const weworkCorpId = env.WEWORK_CORP_ID ?? fileWework?.corpId;
|
|
173
|
-
const weworkSecret = env.WEWORK_SECRET ?? fileWework?.secret;
|
|
174
|
-
const dingtalkClientId = env.DINGTALK_CLIENT_ID ?? fileDingtalk?.clientId;
|
|
175
|
-
const dingtalkClientSecret = env.DINGTALK_CLIENT_SECRET ?? fileDingtalk?.clientSecret;
|
|
176
|
-
const workbuddyAccessToken = fileWorkbuddy?.accessToken;
|
|
177
|
-
const workbuddyRefreshToken = fileWorkbuddy?.refreshToken;
|
|
178
|
-
const workbuddyUserId = fileWorkbuddy?.userId;
|
|
179
|
-
return {
|
|
180
|
-
telegram: {
|
|
181
|
-
configured: !!telegramBotToken,
|
|
182
|
-
enabled: !!telegramBotToken && fileTelegram?.enabled !== false,
|
|
183
|
-
healthy: !!telegramBotToken,
|
|
184
|
-
message: telegramBotToken ? "Token configured" : "Token not configured",
|
|
185
|
-
},
|
|
186
|
-
feishu: {
|
|
187
|
-
configured: !!(feishuAppId && feishuAppSecret),
|
|
188
|
-
enabled: !!(feishuAppId && feishuAppSecret) && fileFeishu?.enabled !== false,
|
|
189
|
-
healthy: !!(feishuAppId && feishuAppSecret),
|
|
190
|
-
message: feishuAppId && feishuAppSecret ? "App ID and Secret configured" : "Missing credentials",
|
|
191
|
-
},
|
|
192
|
-
qq: {
|
|
193
|
-
configured: !!(qqAppId && qqSecret),
|
|
194
|
-
enabled: !!(qqAppId && qqSecret) && fileQQ?.enabled !== false,
|
|
195
|
-
healthy: !!(qqAppId && qqSecret),
|
|
196
|
-
message: qqAppId && qqSecret ? "App ID and Secret configured" : "Missing credentials",
|
|
197
|
-
},
|
|
198
|
-
wework: {
|
|
199
|
-
configured: !!(weworkCorpId && weworkSecret),
|
|
200
|
-
enabled: !!(weworkCorpId && weworkSecret) && fileWework?.enabled !== false,
|
|
201
|
-
healthy: !!(weworkCorpId && weworkSecret),
|
|
202
|
-
message: weworkCorpId && weworkSecret ? "Corp ID and Secret configured" : "Missing credentials",
|
|
203
|
-
},
|
|
204
|
-
dingtalk: {
|
|
205
|
-
configured: !!(dingtalkClientId && dingtalkClientSecret),
|
|
206
|
-
enabled: !!(dingtalkClientId && dingtalkClientSecret) && fileDingtalk?.enabled !== false,
|
|
207
|
-
healthy: !!(dingtalkClientId && dingtalkClientSecret),
|
|
208
|
-
message: dingtalkClientId && dingtalkClientSecret ? "Client ID and Secret configured" : "Missing credentials",
|
|
209
|
-
},
|
|
210
|
-
workbuddy: {
|
|
211
|
-
configured: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
212
|
-
enabled: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId) && fileWorkbuddy?.enabled !== false,
|
|
213
|
-
healthy: !!(workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId),
|
|
214
|
-
message: workbuddyAccessToken && workbuddyRefreshToken && workbuddyUserId ? "OAuth credentials configured" : "Missing credentials",
|
|
215
|
-
},
|
|
216
|
-
clawbot: {
|
|
217
|
-
configured: !!fileClawbot?.apiToken,
|
|
218
|
-
enabled: !!fileClawbot?.apiToken && fileClawbot?.enabled !== false,
|
|
219
|
-
healthy: !!fileClawbot?.apiToken,
|
|
220
|
-
message: fileClawbot?.apiToken ? "API Token configured" : "Missing API Token",
|
|
221
|
-
},
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
function splitCsv(value) {
|
|
225
|
-
if (!value)
|
|
226
|
-
return [];
|
|
227
|
-
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
228
|
-
}
|
|
229
|
-
function clean(value) {
|
|
230
|
-
if (!value)
|
|
231
|
-
return undefined;
|
|
232
|
-
const trimmed = value.trim();
|
|
233
|
-
return trimmed ? trimmed : undefined;
|
|
234
|
-
}
|
|
235
|
-
function persistedPlatformAi(v) {
|
|
236
|
-
// 经 registry 规范化;此前硬编码只认 codex/codebuddy/claude,会把 opencode 降级为 claude。
|
|
237
|
-
return normalizeAiCommand(clean(v), "claude");
|
|
238
|
-
}
|
|
239
|
-
function isMasked(value) {
|
|
240
|
-
return typeof value === "string" && value.includes("****");
|
|
241
|
-
}
|
|
242
|
-
/** 如果前端传回的是掩码值(包含 ****),保留 existing 中的真实密钥,避免覆盖 */
|
|
243
|
-
function resolveSecret(incoming, existing) {
|
|
244
|
-
if (isMasked(incoming))
|
|
245
|
-
return existing;
|
|
246
|
-
return clean(incoming);
|
|
247
|
-
}
|
|
248
|
-
const MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
249
|
-
function readJson(request) {
|
|
250
|
-
return new Promise((resolve, reject) => {
|
|
251
|
-
const chunks = [];
|
|
252
|
-
let totalBytes = 0;
|
|
253
|
-
request.on("data", (chunk) => {
|
|
254
|
-
totalBytes += chunk.length;
|
|
255
|
-
if (totalBytes > MAX_REQUEST_BODY_BYTES) {
|
|
256
|
-
reject(new Error("Request body too large (max 1 MB)"));
|
|
257
|
-
request.destroy();
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
chunks.push(Buffer.from(chunk));
|
|
261
|
-
});
|
|
262
|
-
request.on("end", () => {
|
|
263
|
-
try {
|
|
264
|
-
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
265
|
-
resolve((raw ? JSON.parse(raw) : {}));
|
|
266
|
-
}
|
|
267
|
-
catch (error) {
|
|
268
|
-
reject(error);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
request.on("error", reject);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
function json(response, statusCode, body, request) {
|
|
275
|
-
response.writeHead(statusCode, mergeCors(request, { "content-type": "application/json; charset=utf-8" }));
|
|
276
|
-
response.end(JSON.stringify(body));
|
|
277
|
-
}
|
|
278
|
-
function maskSecret(value) {
|
|
279
|
-
if (!value || value.length <= 4)
|
|
280
|
-
return value ? "****" : "";
|
|
281
|
-
return value.slice(0, 2) + "****" + value.slice(-2);
|
|
282
|
-
}
|
|
283
|
-
function buildInitialPayload(file) {
|
|
284
|
-
// Load Claude settings from ~/.claude/settings.json
|
|
285
|
-
const claudeEnv = loadClaudeSettingsEnv();
|
|
286
|
-
return {
|
|
287
|
-
platforms: {
|
|
288
|
-
telegram: {
|
|
289
|
-
enabled: file.platforms?.telegram?.enabled ?? Boolean(file.platforms?.telegram?.botToken),
|
|
290
|
-
aiCommand: normalizeAiCommand(file.platforms?.telegram?.aiCommand, "claude"),
|
|
291
|
-
botToken: maskSecret(file.platforms?.telegram?.botToken),
|
|
292
|
-
proxy: file.platforms?.telegram?.proxy ?? "",
|
|
293
|
-
allowedUserIds: (file.platforms?.telegram?.allowedUserIds ?? []).join(", "),
|
|
294
|
-
},
|
|
295
|
-
feishu: {
|
|
296
|
-
enabled: file.platforms?.feishu?.enabled ?? Boolean(file.platforms?.feishu?.appId && file.platforms?.feishu?.appSecret),
|
|
297
|
-
aiCommand: normalizeAiCommand(file.platforms?.feishu?.aiCommand, "claude"),
|
|
298
|
-
appId: file.platforms?.feishu?.appId ?? "",
|
|
299
|
-
appSecret: maskSecret(file.platforms?.feishu?.appSecret),
|
|
300
|
-
allowedUserIds: (file.platforms?.feishu?.allowedUserIds ?? []).join(", "),
|
|
301
|
-
},
|
|
302
|
-
qq: {
|
|
303
|
-
enabled: file.platforms?.qq?.enabled ?? Boolean(file.platforms?.qq?.appId && file.platforms?.qq?.secret),
|
|
304
|
-
aiCommand: normalizeAiCommand(file.platforms?.qq?.aiCommand, "claude"),
|
|
305
|
-
appId: file.platforms?.qq?.appId ?? "",
|
|
306
|
-
secret: maskSecret(file.platforms?.qq?.secret),
|
|
307
|
-
allowedUserIds: (file.platforms?.qq?.allowedUserIds ?? []).join(", "),
|
|
308
|
-
},
|
|
309
|
-
wework: {
|
|
310
|
-
enabled: file.platforms?.wework?.enabled ?? Boolean(file.platforms?.wework?.corpId && file.platforms?.wework?.secret),
|
|
311
|
-
aiCommand: normalizeAiCommand(file.platforms?.wework?.aiCommand, "claude"),
|
|
312
|
-
corpId: file.platforms?.wework?.corpId ?? "",
|
|
313
|
-
secret: maskSecret(file.platforms?.wework?.secret),
|
|
314
|
-
allowedUserIds: (file.platforms?.wework?.allowedUserIds ?? []).join(", "),
|
|
315
|
-
},
|
|
316
|
-
dingtalk: {
|
|
317
|
-
enabled: file.platforms?.dingtalk?.enabled ?? Boolean(file.platforms?.dingtalk?.clientId && file.platforms?.dingtalk?.clientSecret),
|
|
318
|
-
aiCommand: normalizeAiCommand(file.platforms?.dingtalk?.aiCommand, "claude"),
|
|
319
|
-
clientId: file.platforms?.dingtalk?.clientId ?? "",
|
|
320
|
-
clientSecret: maskSecret(file.platforms?.dingtalk?.clientSecret),
|
|
321
|
-
cardTemplateId: file.platforms?.dingtalk?.cardTemplateId ?? "",
|
|
322
|
-
allowedUserIds: (file.platforms?.dingtalk?.allowedUserIds ?? []).join(", "),
|
|
323
|
-
},
|
|
324
|
-
workbuddy: {
|
|
325
|
-
enabled: file.platforms?.workbuddy?.enabled ?? Boolean(file.platforms?.workbuddy?.accessToken && file.platforms?.workbuddy?.refreshToken && file.platforms?.workbuddy?.userId),
|
|
326
|
-
aiCommand: normalizeAiCommand(file.platforms?.workbuddy?.aiCommand, "claude"),
|
|
327
|
-
accessToken: maskSecret(file.platforms?.workbuddy?.accessToken),
|
|
328
|
-
refreshToken: maskSecret(file.platforms?.workbuddy?.refreshToken),
|
|
329
|
-
userId: file.platforms?.workbuddy?.userId ?? "",
|
|
330
|
-
baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
|
|
331
|
-
allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
|
|
332
|
-
},
|
|
333
|
-
clawbot: {
|
|
334
|
-
enabled: file.platforms?.clawbot?.enabled ?? Boolean(file.platforms?.clawbot?.apiToken),
|
|
335
|
-
aiCommand: normalizeAiCommand(file.platforms?.clawbot?.aiCommand, "claude"),
|
|
336
|
-
apiUrl: file.platforms?.clawbot?.apiUrl ?? "http://127.0.0.1:26322",
|
|
337
|
-
apiToken: maskSecret(file.platforms?.clawbot?.apiToken),
|
|
338
|
-
allowedUserIds: (file.platforms?.clawbot?.allowedUserIds ?? []).join(", "),
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
ai: {
|
|
342
|
-
claudeWorkDir: file.tools?.claude?.workDir ?? process.cwd(),
|
|
343
|
-
claudeConfigPath: process.platform === 'win32'
|
|
344
|
-
? getClaudeConfigHome() + "\\.claude\\settings.json"
|
|
345
|
-
: getClaudeConfigHome() + "/.claude/settings.json",
|
|
346
|
-
claudeAuthToken: maskSecret(claudeEnv.ANTHROPIC_AUTH_TOKEN),
|
|
347
|
-
claudeBaseUrl: claudeEnv.ANTHROPIC_BASE_URL ?? "",
|
|
348
|
-
claudeModel: claudeEnv.ANTHROPIC_MODEL ?? "",
|
|
349
|
-
claudeProxy: file.tools?.claude?.proxy ?? "",
|
|
350
|
-
codexCliPath: file.tools?.codex?.cliPath ?? "codex",
|
|
351
|
-
codebuddyCliPath: file.tools?.codebuddy?.cliPath ?? "codebuddy",
|
|
352
|
-
opencodeCliPath: file.tools?.opencode?.cliPath ?? "opencode",
|
|
353
|
-
codexProxy: file.tools?.codex?.proxy ?? "",
|
|
354
|
-
codexApiKey: (() => {
|
|
355
|
-
if (process.env.OPENAI_API_KEY)
|
|
356
|
-
return maskSecret(process.env.OPENAI_API_KEY);
|
|
357
|
-
for (const p of CODEX_AUTH_PATHS) {
|
|
358
|
-
try {
|
|
359
|
-
if (existsSync(p)) {
|
|
360
|
-
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
361
|
-
const key = raw?.openai_api_key ?? raw?.apiKey;
|
|
362
|
-
if (typeof key === "string" && key)
|
|
363
|
-
return maskSecret(key);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
catch { /* ignore */ }
|
|
367
|
-
}
|
|
368
|
-
return "";
|
|
369
|
-
})(),
|
|
370
|
-
logDir: file.logDir ?? "",
|
|
371
|
-
logLevel: file.logLevel ?? "default",
|
|
372
|
-
},
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
function validatePayload(payload) {
|
|
376
|
-
const errors = [];
|
|
377
|
-
const enabledCount = Object.values(payload.platforms).filter((item) => item.enabled).length;
|
|
378
|
-
if (enabledCount === 0)
|
|
379
|
-
errors.push("At least one platform must be enabled.");
|
|
380
|
-
if (payload.platforms.telegram.enabled && !clean(payload.platforms.telegram.botToken))
|
|
381
|
-
errors.push("Telegram bot token is required.");
|
|
382
|
-
if (payload.platforms.feishu.enabled && !clean(payload.platforms.feishu.appId))
|
|
383
|
-
errors.push("Feishu app ID is required.");
|
|
384
|
-
if (payload.platforms.feishu.enabled && !clean(payload.platforms.feishu.appSecret))
|
|
385
|
-
errors.push("Feishu app secret is required.");
|
|
386
|
-
if (payload.platforms.qq.enabled && !clean(payload.platforms.qq.appId))
|
|
387
|
-
errors.push("QQ app ID is required.");
|
|
388
|
-
if (payload.platforms.qq.enabled && !clean(payload.platforms.qq.secret))
|
|
389
|
-
errors.push("QQ app secret is required.");
|
|
390
|
-
if (payload.platforms.wework.enabled && !clean(payload.platforms.wework.corpId))
|
|
391
|
-
errors.push("WeWork corp ID is required.");
|
|
392
|
-
if (payload.platforms.wework.enabled && !clean(payload.platforms.wework.secret))
|
|
393
|
-
errors.push("WeWork secret is required.");
|
|
394
|
-
if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientId))
|
|
395
|
-
errors.push("DingTalk client ID is required.");
|
|
396
|
-
if (payload.platforms.dingtalk.enabled && !clean(payload.platforms.dingtalk.clientSecret))
|
|
397
|
-
errors.push("DingTalk client secret is required.");
|
|
398
|
-
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.accessToken))
|
|
399
|
-
errors.push("WorkBuddy access token is required.");
|
|
400
|
-
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.refreshToken))
|
|
401
|
-
errors.push("WorkBuddy refresh token is required.");
|
|
402
|
-
if (payload.platforms.workbuddy.enabled && !clean(payload.platforms.workbuddy.userId))
|
|
403
|
-
errors.push("WorkBuddy user ID is required.");
|
|
404
|
-
if (payload.platforms.clawbot.enabled && !clean(payload.platforms.clawbot.apiToken))
|
|
405
|
-
errors.push("ClawBot API token is required.");
|
|
406
|
-
if (!clean(payload.ai.claudeWorkDir))
|
|
407
|
-
errors.push("Default work directory is required.");
|
|
408
|
-
return errors;
|
|
409
|
-
}
|
|
410
|
-
function validateConfigForPlatform(platform, config) {
|
|
411
|
-
const errors = [];
|
|
412
|
-
const c = config;
|
|
413
|
-
switch (platform) {
|
|
414
|
-
case "telegram":
|
|
415
|
-
if (!c.botToken || typeof c.botToken !== "string" || !clean(c.botToken)) {
|
|
416
|
-
errors.push("Telegram bot token is required and must be a non-empty string.");
|
|
417
|
-
}
|
|
418
|
-
if (c.proxy && typeof c.proxy !== "string") {
|
|
419
|
-
errors.push("Proxy must be a string if provided.");
|
|
420
|
-
}
|
|
421
|
-
break;
|
|
422
|
-
case "feishu":
|
|
423
|
-
if (!c.appId || typeof c.appId !== "string" || !clean(c.appId)) {
|
|
424
|
-
errors.push("Feishu app ID is required and must be a non-empty string.");
|
|
425
|
-
}
|
|
426
|
-
if (!c.appSecret || typeof c.appSecret !== "string" || !clean(c.appSecret)) {
|
|
427
|
-
errors.push("Feishu app secret is required and must be a non-empty string.");
|
|
428
|
-
}
|
|
429
|
-
break;
|
|
430
|
-
case "qq":
|
|
431
|
-
if (!c.appId || typeof c.appId !== "string" || !clean(c.appId)) {
|
|
432
|
-
errors.push("QQ app ID is required and must be a non-empty string.");
|
|
433
|
-
}
|
|
434
|
-
if (!c.secret || typeof c.secret !== "string" || !clean(c.secret)) {
|
|
435
|
-
errors.push("QQ app secret is required and must be a non-empty string.");
|
|
436
|
-
}
|
|
437
|
-
break;
|
|
438
|
-
case "wework":
|
|
439
|
-
if (!c.corpId || typeof c.corpId !== "string" || !clean(c.corpId)) {
|
|
440
|
-
errors.push("WeWork corp ID is required and must be a non-empty string.");
|
|
441
|
-
}
|
|
442
|
-
if (!c.secret || typeof c.secret !== "string" || !clean(c.secret)) {
|
|
443
|
-
errors.push("WeWork secret is required and must be a non-empty string.");
|
|
444
|
-
}
|
|
445
|
-
break;
|
|
446
|
-
case "dingtalk":
|
|
447
|
-
if (!c.clientId || typeof c.clientId !== "string" || !clean(c.clientId)) {
|
|
448
|
-
errors.push("DingTalk client ID is required and must be a non-empty string.");
|
|
449
|
-
}
|
|
450
|
-
if (!c.clientSecret || typeof c.clientSecret !== "string" || !clean(c.clientSecret)) {
|
|
451
|
-
errors.push("DingTalk client secret is required and must be a non-empty string.");
|
|
452
|
-
}
|
|
453
|
-
break;
|
|
454
|
-
case "workbuddy":
|
|
455
|
-
if (!c.accessToken || typeof c.accessToken !== "string" || !clean(c.accessToken)) {
|
|
456
|
-
errors.push("WorkBuddy access token is required and must be a non-empty string.");
|
|
457
|
-
}
|
|
458
|
-
if (!c.refreshToken || typeof c.refreshToken !== "string" || !clean(c.refreshToken)) {
|
|
459
|
-
errors.push("WorkBuddy refresh token is required and must be a non-empty string.");
|
|
460
|
-
}
|
|
461
|
-
if (!c.userId || typeof c.userId !== "string" || !clean(c.userId)) {
|
|
462
|
-
errors.push("WorkBuddy user ID is required and must be a non-empty string.");
|
|
463
|
-
}
|
|
464
|
-
break;
|
|
465
|
-
case "clawbot":
|
|
466
|
-
if (!c.apiToken || typeof c.apiToken !== "string" || !clean(c.apiToken)) {
|
|
467
|
-
errors.push("ClawBot API token is required and must be a non-empty string.");
|
|
468
|
-
}
|
|
469
|
-
break;
|
|
470
|
-
default:
|
|
471
|
-
errors.push(`Unknown platform: ${platform}`);
|
|
472
|
-
}
|
|
473
|
-
return errors;
|
|
474
|
-
}
|
|
475
|
-
function toErrorMessage(error) {
|
|
476
|
-
return error instanceof Error ? error.message : String(error);
|
|
477
|
-
}
|
|
478
|
-
async function readJsonResponse(response) {
|
|
479
|
-
const text = await response.text();
|
|
480
|
-
if (!text)
|
|
481
|
-
return {};
|
|
482
|
-
try {
|
|
483
|
-
return JSON.parse(text);
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
throw new Error(`Unexpected non-JSON response: ${text.slice(0, 200)}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
function createProbeConfig(values) {
|
|
490
|
-
return {
|
|
491
|
-
enabledPlatforms: [],
|
|
492
|
-
allowedUserIds: [],
|
|
493
|
-
telegramAllowedUserIds: [],
|
|
494
|
-
feishuAllowedUserIds: [],
|
|
495
|
-
qqAllowedUserIds: [],
|
|
496
|
-
weworkAllowedUserIds: [],
|
|
497
|
-
dingtalkAllowedUserIds: [],
|
|
498
|
-
workbuddyAllowedUserIds: [],
|
|
499
|
-
clawbotAllowedUserIds: [],
|
|
500
|
-
codexCliPath: "codex",
|
|
501
|
-
claudeWorkDir: process.cwd(),
|
|
502
|
-
claudeSessionIdleTtlMinutes: 30,
|
|
503
|
-
logDir: "",
|
|
504
|
-
logLevel: "INFO",
|
|
505
|
-
telemetry: { enabled: true },
|
|
506
|
-
codebuddyCliPath: "codebuddy",
|
|
507
|
-
opencodeCliPath: "opencode",
|
|
508
|
-
platforms: {},
|
|
509
|
-
...values,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
async function probeTelegram(config) {
|
|
513
|
-
const botToken = clean(String(config.botToken ?? ""));
|
|
514
|
-
if (!botToken)
|
|
515
|
-
throw new Error("Telegram bot token is required.");
|
|
516
|
-
const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`, {
|
|
517
|
-
signal: AbortSignal.timeout(PLATFORM_TEST_TIMEOUT_MS),
|
|
518
|
-
});
|
|
519
|
-
const body = await readJsonResponse(response);
|
|
520
|
-
if (!response.ok || body.ok !== true) {
|
|
521
|
-
throw new Error(String(body.description ?? body.error_code ?? `HTTP ${response.status}`));
|
|
522
|
-
}
|
|
523
|
-
const result = (body.result ?? {});
|
|
524
|
-
const username = typeof result.username === "string" ? `@${result.username}` : "bot";
|
|
525
|
-
return `Telegram reachable as ${username}.`;
|
|
526
|
-
}
|
|
527
|
-
async function probeFeishu(config) {
|
|
528
|
-
const appId = clean(String(config.appId ?? ""));
|
|
529
|
-
const appSecret = clean(String(config.appSecret ?? ""));
|
|
530
|
-
if (!appId || !appSecret)
|
|
531
|
-
throw new Error("Feishu app ID and app secret are required.");
|
|
532
|
-
const response = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
|
|
533
|
-
method: "POST",
|
|
534
|
-
headers: { "content-type": "application/json" },
|
|
535
|
-
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
536
|
-
signal: AbortSignal.timeout(PLATFORM_TEST_TIMEOUT_MS),
|
|
537
|
-
});
|
|
538
|
-
const body = await readJsonResponse(response);
|
|
539
|
-
if (!response.ok || body.code !== 0) {
|
|
540
|
-
throw new Error(String(body.msg ?? body.message ?? `HTTP ${response.status}`));
|
|
541
|
-
}
|
|
542
|
-
return "Feishu credentials are valid.";
|
|
543
|
-
}
|
|
544
|
-
async function probeQQ(config) {
|
|
545
|
-
const appId = clean(String(config.appId ?? ""));
|
|
546
|
-
const secret = clean(String(config.secret ?? ""));
|
|
547
|
-
if (!appId || !secret)
|
|
548
|
-
throw new Error("QQ app ID and app secret are required.");
|
|
549
|
-
const response = await fetch("https://bots.qq.com/app/getAppAccessToken", {
|
|
550
|
-
method: "POST",
|
|
551
|
-
headers: { "content-type": "application/json" },
|
|
552
|
-
body: JSON.stringify({ appId, clientSecret: secret }),
|
|
553
|
-
signal: AbortSignal.timeout(PLATFORM_TEST_TIMEOUT_MS),
|
|
554
|
-
});
|
|
555
|
-
const body = await readJsonResponse(response);
|
|
556
|
-
if (!response.ok || typeof body.access_token !== "string" || body.access_token.length === 0) {
|
|
557
|
-
throw new Error(String(body.message ?? `HTTP ${response.status}`));
|
|
558
|
-
}
|
|
559
|
-
return "QQ credentials are valid.";
|
|
560
|
-
}
|
|
561
|
-
async function probeWeWork(config) {
|
|
562
|
-
const corpId = clean(String(config.corpId ?? ""));
|
|
563
|
-
const secret = clean(String(config.secret ?? ""));
|
|
564
|
-
if (!corpId || !secret)
|
|
565
|
-
throw new Error("WeWork corp ID and secret are required.");
|
|
566
|
-
try {
|
|
567
|
-
await initWeWork(createProbeConfig({ weworkCorpId: corpId, weworkSecret: secret }), async () => { });
|
|
568
|
-
return "WeWork WebSocket authentication succeeded.";
|
|
569
|
-
}
|
|
570
|
-
finally {
|
|
571
|
-
stopWeWork();
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
async function probeDingTalk(config) {
|
|
575
|
-
const clientId = clean(String(config.clientId ?? ""));
|
|
576
|
-
const clientSecret = clean(String(config.clientSecret ?? ""));
|
|
577
|
-
if (!clientId || !clientSecret)
|
|
578
|
-
throw new Error("DingTalk client ID and client secret are required.");
|
|
579
|
-
const client = new DWClient({
|
|
580
|
-
clientId,
|
|
581
|
-
clientSecret,
|
|
582
|
-
keepAlive: false,
|
|
583
|
-
debug: false,
|
|
584
|
-
});
|
|
585
|
-
const token = await Promise.race([
|
|
586
|
-
client.getAccessToken(),
|
|
587
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error("DingTalk access token request timed out.")), PLATFORM_TEST_TIMEOUT_MS)),
|
|
588
|
-
]);
|
|
589
|
-
if (typeof token !== "string" || token.length === 0) {
|
|
590
|
-
throw new Error("DingTalk did not return an access token.");
|
|
591
|
-
}
|
|
592
|
-
return "DingTalk credentials are valid.";
|
|
593
|
-
}
|
|
594
|
-
async function probeWorkBuddy(config) {
|
|
595
|
-
const accessToken = clean(String(config.accessToken ?? ""));
|
|
596
|
-
const refreshToken = clean(String(config.refreshToken ?? ""));
|
|
597
|
-
const userId = clean(String(config.userId ?? ""));
|
|
598
|
-
if (!accessToken || !refreshToken || !userId)
|
|
599
|
-
throw new Error("WorkBuddy access token, refresh token, and user ID are required.");
|
|
600
|
-
const baseUrl = clean(String(config.baseUrl ?? "")) || "https://copilot.tencent.com";
|
|
601
|
-
// Validate credentials by attempting to register workspace (same endpoint as runtime)
|
|
602
|
-
const response = await fetch(`${baseUrl}/v2/agentos/localagent/registerWorkspace`, {
|
|
603
|
-
method: "POST",
|
|
604
|
-
headers: {
|
|
605
|
-
"content-type": "application/json",
|
|
606
|
-
"authorization": `Bearer ${accessToken}`,
|
|
607
|
-
},
|
|
608
|
-
body: JSON.stringify({
|
|
609
|
-
userId,
|
|
610
|
-
hostId: "open-im-test",
|
|
611
|
-
workspaceId: "open-im-test-workspace",
|
|
612
|
-
workspaceName: "OpenIM Test Workspace",
|
|
613
|
-
localAgentType: "ide",
|
|
614
|
-
}),
|
|
615
|
-
signal: AbortSignal.timeout(PLATFORM_TEST_TIMEOUT_MS),
|
|
616
|
-
});
|
|
617
|
-
if (!response.ok) {
|
|
618
|
-
const body = await response.text();
|
|
619
|
-
throw new Error(`WorkBuddy authentication failed: ${body.slice(0, 200) || `HTTP ${response.status}`}`);
|
|
620
|
-
}
|
|
621
|
-
return "WorkBuddy credentials are valid.";
|
|
622
|
-
}
|
|
623
|
-
async function probeClawBot(config) {
|
|
624
|
-
const apiUrl = clean(String(config.apiUrl ?? "http://127.0.0.1:26322"));
|
|
625
|
-
const apiToken = clean(String(config.apiToken ?? ""));
|
|
626
|
-
if (!apiToken)
|
|
627
|
-
throw new Error("ClawBot API token is required.");
|
|
628
|
-
const { randomUUID } = await import('node:crypto');
|
|
629
|
-
const response = await fetch(`${apiUrl}/ilink/bot/getupdates?timeout=1&bot_token=${encodeURIComponent(apiToken)}`, {
|
|
630
|
-
headers: {
|
|
631
|
-
'Content-Type': 'application/json',
|
|
632
|
-
'AuthorizationType': 'ilink_bot_token',
|
|
633
|
-
'iLink-App-Id': 'bot',
|
|
634
|
-
'iLink-App-ClientVersion': '131588',
|
|
635
|
-
'X-WECHAT-UIN': randomUUID(),
|
|
636
|
-
},
|
|
637
|
-
signal: AbortSignal.timeout(PLATFORM_TEST_TIMEOUT_MS),
|
|
638
|
-
});
|
|
639
|
-
const body = await readJsonResponse(response);
|
|
640
|
-
const ok = body.ok === true || body.ret === 0 || body.ret === '0';
|
|
641
|
-
if (!response.ok || !ok) {
|
|
642
|
-
throw new Error(String(body.error ?? body.description ?? `HTTP ${response.status}`));
|
|
643
|
-
}
|
|
644
|
-
return "ClawBot API reachable.";
|
|
645
|
-
}
|
|
646
|
-
export async function testPlatformConfig(platform, config) {
|
|
647
|
-
const errors = validateConfigForPlatform(platform, config);
|
|
648
|
-
if (errors.length > 0) {
|
|
649
|
-
throw new Error(errors.join("; "));
|
|
650
|
-
}
|
|
651
|
-
switch (platform) {
|
|
652
|
-
case "telegram":
|
|
653
|
-
return probeTelegram(config);
|
|
654
|
-
case "feishu":
|
|
655
|
-
return probeFeishu(config);
|
|
656
|
-
case "qq":
|
|
657
|
-
return probeQQ(config);
|
|
658
|
-
case "wework":
|
|
659
|
-
return probeWeWork(config);
|
|
660
|
-
case "dingtalk":
|
|
661
|
-
return probeDingTalk(config);
|
|
662
|
-
case "workbuddy":
|
|
663
|
-
return probeWorkBuddy(config);
|
|
664
|
-
case "clawbot":
|
|
665
|
-
return probeClawBot(config);
|
|
666
|
-
default:
|
|
667
|
-
throw new Error(`Unknown platform: ${platform}`);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
function toFileConfig(payload, existing) {
|
|
671
|
-
// Save Claude environment variables to ~/.claude/settings.json
|
|
672
|
-
const claudeEnv = {};
|
|
673
|
-
const existingClaudeEnv = loadClaudeSettingsEnv();
|
|
674
|
-
const resolvedAuthToken = resolveSecret(payload.ai.claudeAuthToken, existingClaudeEnv.ANTHROPIC_AUTH_TOKEN);
|
|
675
|
-
if (resolvedAuthToken)
|
|
676
|
-
claudeEnv.ANTHROPIC_AUTH_TOKEN = resolvedAuthToken;
|
|
677
|
-
if (payload.ai.claudeBaseUrl)
|
|
678
|
-
claudeEnv.ANTHROPIC_BASE_URL = payload.ai.claudeBaseUrl;
|
|
679
|
-
if (payload.ai.claudeModel)
|
|
680
|
-
claudeEnv.ANTHROPIC_MODEL = payload.ai.claudeModel;
|
|
681
|
-
if (Object.keys(claudeEnv).length > 0) {
|
|
682
|
-
saveClaudeSettingsEnv(claudeEnv);
|
|
683
|
-
}
|
|
684
|
-
// claudeConfigPath is informational only, not saved
|
|
685
|
-
const { env: _discardLegacyRootEnv, aiCommand: _discardLegacyGlobalAi, ...existingWithoutRootEnv } = existing;
|
|
686
|
-
return {
|
|
687
|
-
...existingWithoutRootEnv,
|
|
688
|
-
logDir: payload.ai.logDir === undefined ? existing.logDir : clean(payload.ai.logDir),
|
|
689
|
-
logLevel: payload.ai.logLevel === "default" ? undefined : payload.ai.logLevel,
|
|
690
|
-
tools: {
|
|
691
|
-
claude: {
|
|
692
|
-
...existing.tools?.claude,
|
|
693
|
-
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
694
|
-
proxy: clean(payload.ai.claudeProxy),
|
|
695
|
-
// model is now saved to ~/.claude/settings.json as env var
|
|
696
|
-
},
|
|
697
|
-
codex: {
|
|
698
|
-
...existing.tools?.codex,
|
|
699
|
-
cliPath: clean(payload.ai.codexCliPath) ?? "codex",
|
|
700
|
-
workDir: clean(payload.ai.claudeWorkDir) ?? process.cwd(),
|
|
701
|
-
proxy: clean(payload.ai.codexProxy),
|
|
702
|
-
},
|
|
703
|
-
codebuddy: {
|
|
704
|
-
...existing.tools?.codebuddy,
|
|
705
|
-
cliPath: clean(payload.ai.codebuddyCliPath) ?? "codebuddy",
|
|
706
|
-
},
|
|
707
|
-
opencode: {
|
|
708
|
-
...existing.tools?.opencode,
|
|
709
|
-
cliPath: clean(payload.ai.opencodeCliPath) ?? "opencode",
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
platforms: {
|
|
713
|
-
...existing.platforms,
|
|
714
|
-
telegram: {
|
|
715
|
-
...existing.platforms?.telegram,
|
|
716
|
-
enabled: payload.platforms.telegram.enabled,
|
|
717
|
-
aiCommand: persistedPlatformAi(payload.platforms.telegram.aiCommand),
|
|
718
|
-
botToken: resolveSecret(payload.platforms.telegram.botToken, existing.platforms?.telegram?.botToken),
|
|
719
|
-
proxy: clean(payload.platforms.telegram.proxy),
|
|
720
|
-
allowedUserIds: splitCsv(payload.platforms.telegram.allowedUserIds),
|
|
721
|
-
},
|
|
722
|
-
feishu: {
|
|
723
|
-
...existing.platforms?.feishu,
|
|
724
|
-
enabled: payload.platforms.feishu.enabled,
|
|
725
|
-
aiCommand: persistedPlatformAi(payload.platforms.feishu.aiCommand),
|
|
726
|
-
appId: clean(payload.platforms.feishu.appId),
|
|
727
|
-
appSecret: resolveSecret(payload.platforms.feishu.appSecret, existing.platforms?.feishu?.appSecret),
|
|
728
|
-
allowedUserIds: splitCsv(payload.platforms.feishu.allowedUserIds),
|
|
729
|
-
},
|
|
730
|
-
qq: {
|
|
731
|
-
...existing.platforms?.qq,
|
|
732
|
-
enabled: payload.platforms.qq.enabled,
|
|
733
|
-
aiCommand: persistedPlatformAi(payload.platforms.qq.aiCommand),
|
|
734
|
-
appId: clean(payload.platforms.qq.appId),
|
|
735
|
-
secret: resolveSecret(payload.platforms.qq.secret, existing.platforms?.qq?.secret),
|
|
736
|
-
allowedUserIds: splitCsv(payload.platforms.qq.allowedUserIds),
|
|
737
|
-
},
|
|
738
|
-
wework: {
|
|
739
|
-
...existing.platforms?.wework,
|
|
740
|
-
enabled: payload.platforms.wework.enabled,
|
|
741
|
-
aiCommand: persistedPlatformAi(payload.platforms.wework.aiCommand),
|
|
742
|
-
corpId: clean(payload.platforms.wework.corpId),
|
|
743
|
-
secret: resolveSecret(payload.platforms.wework.secret, existing.platforms?.wework?.secret),
|
|
744
|
-
allowedUserIds: splitCsv(payload.platforms.wework.allowedUserIds),
|
|
745
|
-
},
|
|
746
|
-
dingtalk: {
|
|
747
|
-
...existing.platforms?.dingtalk,
|
|
748
|
-
enabled: payload.platforms.dingtalk.enabled,
|
|
749
|
-
aiCommand: persistedPlatformAi(payload.platforms.dingtalk.aiCommand),
|
|
750
|
-
clientId: clean(payload.platforms.dingtalk.clientId),
|
|
751
|
-
clientSecret: resolveSecret(payload.platforms.dingtalk.clientSecret, existing.platforms?.dingtalk?.clientSecret),
|
|
752
|
-
cardTemplateId: clean(payload.platforms.dingtalk.cardTemplateId),
|
|
753
|
-
allowedUserIds: splitCsv(payload.platforms.dingtalk.allowedUserIds),
|
|
754
|
-
},
|
|
755
|
-
workbuddy: {
|
|
756
|
-
...existing.platforms?.workbuddy,
|
|
757
|
-
enabled: payload.platforms.workbuddy.enabled,
|
|
758
|
-
aiCommand: persistedPlatformAi(payload.platforms.workbuddy.aiCommand),
|
|
759
|
-
accessToken: resolveSecret(payload.platforms.workbuddy.accessToken, existing.platforms?.workbuddy?.accessToken),
|
|
760
|
-
refreshToken: resolveSecret(payload.platforms.workbuddy.refreshToken, existing.platforms?.workbuddy?.refreshToken),
|
|
761
|
-
userId: clean(payload.platforms.workbuddy.userId),
|
|
762
|
-
baseUrl: clean(payload.platforms.workbuddy.baseUrl),
|
|
763
|
-
allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
|
|
764
|
-
},
|
|
765
|
-
clawbot: {
|
|
766
|
-
...existing.platforms?.clawbot,
|
|
767
|
-
enabled: payload.platforms.clawbot.enabled,
|
|
768
|
-
aiCommand: persistedPlatformAi(payload.platforms.clawbot.aiCommand),
|
|
769
|
-
apiUrl: clean(payload.platforms.clawbot.apiUrl) ?? "http://127.0.0.1:26322",
|
|
770
|
-
apiToken: resolveSecret(payload.platforms.clawbot.apiToken, existing.platforms?.clawbot?.apiToken),
|
|
771
|
-
allowedUserIds: splitCsv(payload.platforms.clawbot.allowedUserIds),
|
|
772
|
-
},
|
|
773
|
-
},
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
function openBrowser(url) {
|
|
777
|
-
// 显式关闭自动打开浏览器(服务器环境推荐设置)
|
|
778
|
-
if (process.env.OPEN_IM_NO_BROWSER === "1") {
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
// 在无 TTY 且无图形环境(常见于服务器)时直接跳过,避免无意义的 xdg-open 调用
|
|
782
|
-
if (!process.stdout.isTTY && !process.env.DISPLAY) {
|
|
783
|
-
log.info(`Skipping browser launch for URL ${url} (no TTY/DISPLAY detected).`);
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
const safeSpawn = (command, args) => {
|
|
787
|
-
try {
|
|
788
|
-
const child = spawn(command, args, { detached: true, stdio: "ignore", windowsHide: process.platform === "win32" });
|
|
789
|
-
// 防止 ENOENT 之类的错误变成未捕获异常
|
|
790
|
-
child.on("error", (error) => {
|
|
791
|
-
log.warn(`Failed to launch browser command "${command}": ${error.code ?? error.message}`);
|
|
792
|
-
});
|
|
793
|
-
child.unref();
|
|
794
|
-
}
|
|
795
|
-
catch (error) {
|
|
796
|
-
log.warn(`Failed to spawn browser command "${command}": ${error instanceof Error ? error.message : String(error)}`);
|
|
797
|
-
}
|
|
798
|
-
};
|
|
799
|
-
if (process.platform === "win32") {
|
|
800
|
-
safeSpawn("cmd", ["/c", "start", "", url]);
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
if (process.platform === "darwin") {
|
|
804
|
-
safeSpawn("open", [url]);
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
// linux / 其他 UNIX 平台:优先尝试 xdg-open,失败时仅记录日志,不抛出
|
|
808
|
-
safeSpawn("xdg-open", [url]);
|
|
809
|
-
}
|
|
810
|
-
export function getWebConfigPort() {
|
|
811
|
-
const fromEnv = process.env.OPEN_IM_WEB_PORT ? parseInt(process.env.OPEN_IM_WEB_PORT, 10) : NaN;
|
|
812
|
-
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : WEB_CONFIG_PORT;
|
|
813
|
-
}
|
|
814
|
-
export function getWebConfigUrl() {
|
|
815
|
-
return `http://127.0.0.1:${getWebConfigPort()}`;
|
|
816
|
-
}
|
|
817
|
-
function openWebConfigUrl() {
|
|
818
|
-
openBrowser(getPublicWebDashboardUrl());
|
|
819
|
-
}
|
|
820
28
|
export async function startWebConfigServer(options) {
|
|
821
29
|
let timer = null;
|
|
822
30
|
let settled = false;
|
|
@@ -854,11 +62,9 @@ export async function startWebConfigServer(options) {
|
|
|
854
62
|
if (shouldRequireAuth) {
|
|
855
63
|
const loginToken = requestUrl.searchParams.get("login_token");
|
|
856
64
|
if (loginToken) {
|
|
857
|
-
const info =
|
|
858
|
-
|
|
859
|
-
if (info && info.expiresAt > now) {
|
|
65
|
+
const info = consumeLoginToken(loginToken);
|
|
66
|
+
if (info) {
|
|
860
67
|
// 有效的一次性登录 token:创建会话,设置 Cookie,并重定向到去掉 login_token 的 URL
|
|
861
|
-
pendingLogins.delete(loginToken);
|
|
862
68
|
const sessionTtlMs = 24 * 60 * 60 * 1000; // 24 小时
|
|
863
69
|
const sessionId = createSession(request, sessionTtlMs);
|
|
864
70
|
const cookie = buildSessionCookie(sessionId, sessionTtlMs);
|
|
@@ -1149,7 +355,7 @@ export async function startWebConfigServer(options) {
|
|
|
1149
355
|
try {
|
|
1150
356
|
const { startQRLogin } = await import("./clawbot/qr-login.js");
|
|
1151
357
|
const session = await startQRLogin();
|
|
1152
|
-
json(response, 200, { success: true, qrcodeUrl: session.qrcodeUrl, sessionKey: session.sessionKey }, request);
|
|
358
|
+
json(response, 200, { success: true, qrcodeUrl: session.qrcodeUrl, qrcodeImage: session.qrcodeImage, qrcode: session.qrcode, sessionKey: session.sessionKey }, request);
|
|
1153
359
|
}
|
|
1154
360
|
catch (error) {
|
|
1155
361
|
json(response, 500, { success: false, error: toErrorMessage(error) }, request);
|
|
@@ -1169,6 +375,33 @@ export async function startWebConfigServer(options) {
|
|
|
1169
375
|
}
|
|
1170
376
|
return;
|
|
1171
377
|
}
|
|
378
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/workbuddy/qr-login/start") {
|
|
379
|
+
try {
|
|
380
|
+
const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
|
|
381
|
+
const QRCode = (await import("qrcode")).default;
|
|
382
|
+
const oauth = new WorkBuddyOAuth();
|
|
383
|
+
const { authUrl, state } = await oauth.fetchAuthState();
|
|
384
|
+
const qrcodeImage = await QRCode.toDataURL(authUrl, { width: 280, margin: 1 });
|
|
385
|
+
json(response, 200, { success: true, qrcodeImage, state, authUrl }, request);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
json(response, 500, { success: false, error: toErrorMessage(error) }, request);
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (request.method === "POST" && requestUrl.pathname === "/api/workbuddy/qr-login/wait") {
|
|
393
|
+
try {
|
|
394
|
+
const body = await readJson(request);
|
|
395
|
+
const { WorkBuddyOAuth } = await import("./workbuddy/oauth.js");
|
|
396
|
+
const oauth = new WorkBuddyOAuth();
|
|
397
|
+
const result = await oauth.pollToken(body.state);
|
|
398
|
+
json(response, 200, { success: true, accessToken: result.accessToken, refreshToken: result.refreshToken, userId: result.userId }, request);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
json(response, 500, { success: false, error: toErrorMessage(error) }, request);
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
1172
405
|
if (request.method === "GET" && tryServeDashboardStatic(requestUrl, request, response, mergeCors)) {
|
|
1173
406
|
return;
|
|
1174
407
|
}
|
|
@@ -1209,11 +442,8 @@ export async function startWebConfigServer(options) {
|
|
|
1209
442
|
// 当绑定到非 127.0.0.1(例如 0.0.0.0)时,为远程访问生成一次性登录链接
|
|
1210
443
|
if (host !== "127.0.0.1") {
|
|
1211
444
|
const loginTtlMs = 15 * 60 * 1000; // 15 分钟内有效
|
|
1212
|
-
|
|
1213
|
-
const
|
|
1214
|
-
const baseUrl = `http://${displayHost}:${port}`;
|
|
1215
|
-
const loginUrl = `${baseUrl}/?login_token=${encodeURIComponent(loginToken)}`;
|
|
1216
|
-
loginUrlForReturn = loginUrl;
|
|
445
|
+
loginUrlForReturn = generateLoginUrl(host, port, loginTtlMs);
|
|
446
|
+
const loginUrl = loginUrlForReturn;
|
|
1217
447
|
log.info("━━━━━━━━ Web Config Login ━━━━━━━━");
|
|
1218
448
|
log.info(`Host binding : ${host}`);
|
|
1219
449
|
log.info(`Login URL : ${loginUrl}`);
|