@wu529778790/open-im 1.9.3-beta.3 → 1.9.3-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude-sdk-adapter.js +42 -3
- package/dist/config-web.js +20 -9
- package/dist/index.js +1 -1
- package/dist/session/session-manager.js +44 -28
- package/package.json +1 -1
|
@@ -16,6 +16,28 @@ const log = createLogger('ClaudeSDK');
|
|
|
16
16
|
const activeSessions = new Map();
|
|
17
17
|
// 存储正在进行的流式迭代器,用于中断
|
|
18
18
|
const activeStreams = new Set();
|
|
19
|
+
// 空闲会话清理:跟踪最后使用时间,定期清除超时会话
|
|
20
|
+
const sessionLastUsed = new Map();
|
|
21
|
+
const SESSION_IDLE_TTL_MS = 30 * 60 * 1000; // 30 分钟未使用则清理
|
|
22
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 每 5 分钟检查一次
|
|
23
|
+
const cleanupInterval = setInterval(() => {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const [id, lastUsed] of sessionLastUsed) {
|
|
26
|
+
if (now - lastUsed > SESSION_IDLE_TTL_MS) {
|
|
27
|
+
const session = activeSessions.get(id);
|
|
28
|
+
if (session) {
|
|
29
|
+
try {
|
|
30
|
+
session.close();
|
|
31
|
+
}
|
|
32
|
+
catch { /* ignore */ }
|
|
33
|
+
activeSessions.delete(id);
|
|
34
|
+
}
|
|
35
|
+
sessionLastUsed.delete(id);
|
|
36
|
+
log.info(`Cleaned up idle session (unused ${Math.round((now - lastUsed) / 60000)}min): ${id}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, CLEANUP_INTERVAL_MS);
|
|
40
|
+
cleanupInterval.unref(); // 不阻止进程退出
|
|
19
41
|
// Mutex to serialize process.chdir() calls across concurrent users
|
|
20
42
|
let chdirMutex = Promise.resolve();
|
|
21
43
|
function withChdirMutex(fn) {
|
|
@@ -60,7 +82,9 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
60
82
|
};
|
|
61
83
|
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
|
|
62
84
|
log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl} workDir=${workDir}`);
|
|
63
|
-
//
|
|
85
|
+
// NOTE: process.chdir() 是进程级全局副作用,在并发服务器中不理想。
|
|
86
|
+
// 但 SDK 的 createSession/resumeSession 不接受 cwd 参数,且这些调用是同步的,
|
|
87
|
+
// 所以 mutex + try/finally 已是最优方案。如果 SDK 未来支持 cwd 选项,应移除 chdir。
|
|
64
88
|
return withChdirMutex(() => {
|
|
65
89
|
let session;
|
|
66
90
|
const originalCwd = process.cwd();
|
|
@@ -73,6 +97,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
73
97
|
const existing = activeSessions.get(sessionId);
|
|
74
98
|
if (existing) {
|
|
75
99
|
log.info(`Reusing existing in-memory session: ${sessionId}`);
|
|
100
|
+
sessionLastUsed.set(sessionId, Date.now());
|
|
76
101
|
return { session: existing, sessionId };
|
|
77
102
|
}
|
|
78
103
|
// 内存中没有,尝试通过 resume 恢复(会启动新 CLI 进程)
|
|
@@ -80,6 +105,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
80
105
|
log.info(`Attempting to resume session: ${sessionId}`);
|
|
81
106
|
session = unstable_v2_resumeSession(sessionId, sessionOptions);
|
|
82
107
|
activeSessions.set(sessionId, session);
|
|
108
|
+
sessionLastUsed.set(sessionId, Date.now());
|
|
83
109
|
log.info(`Successfully resumed session: ${sessionId}`);
|
|
84
110
|
return { session, sessionId };
|
|
85
111
|
}
|
|
@@ -94,6 +120,7 @@ async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
|
94
120
|
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
95
121
|
const tempId = `pending-${Date.now()}`;
|
|
96
122
|
activeSessions.set(tempId, session);
|
|
123
|
+
sessionLastUsed.set(tempId, Date.now());
|
|
97
124
|
log.info(`Created new session (tempId: ${tempId})`);
|
|
98
125
|
return { session, sessionId: tempId };
|
|
99
126
|
}
|
|
@@ -110,6 +137,7 @@ export class ClaudeSDKAdapter {
|
|
|
110
137
|
* 清理所有活跃的 SDK 会话和流
|
|
111
138
|
*/
|
|
112
139
|
static destroy() {
|
|
140
|
+
clearInterval(cleanupInterval);
|
|
113
141
|
for (const stream of activeStreams) {
|
|
114
142
|
try {
|
|
115
143
|
if (stream && typeof stream.return === 'function') {
|
|
@@ -130,6 +158,7 @@ export class ClaudeSDKAdapter {
|
|
|
130
158
|
}
|
|
131
159
|
}
|
|
132
160
|
activeSessions.clear();
|
|
161
|
+
sessionLastUsed.clear();
|
|
133
162
|
}
|
|
134
163
|
run(prompt, sessionId, workDir, callbacks, options) {
|
|
135
164
|
log.info(`[V2] run() entry model=${String(options?.model ?? '')} baseUrl=${process.env.ANTHROPIC_BASE_URL ?? '(default)'}`);
|
|
@@ -185,6 +214,9 @@ export class ClaudeSDKAdapter {
|
|
|
185
214
|
activeSessions.delete(idToClean);
|
|
186
215
|
}
|
|
187
216
|
activeSessions.set(newSessionId, session);
|
|
217
|
+
sessionLastUsed.set(newSessionId, Date.now());
|
|
218
|
+
if (idToClean)
|
|
219
|
+
sessionLastUsed.delete(idToClean);
|
|
188
220
|
actualSessionId = newSessionId;
|
|
189
221
|
log.info(`[V2] Got actual sessionId: ${newSessionId}`);
|
|
190
222
|
callbacks.onSessionId?.(newSessionId);
|
|
@@ -313,8 +345,15 @@ export class ClaudeSDKAdapter {
|
|
|
313
345
|
callbacks.onError(msg);
|
|
314
346
|
}
|
|
315
347
|
};
|
|
316
|
-
//
|
|
317
|
-
runSession()
|
|
348
|
+
// 启动会话(不等待),catch 兜底防止 unhandledRejection 导致用户请求挂起
|
|
349
|
+
runSession().catch((err) => {
|
|
350
|
+
if (!runSettled) {
|
|
351
|
+
runSettled = true;
|
|
352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
353
|
+
log.error(`Unhandled runSession error: ${msg}`);
|
|
354
|
+
callbacks.onError(msg);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
318
357
|
return {
|
|
319
358
|
abort: () => {
|
|
320
359
|
log.info('Aborting session run');
|
package/dist/config-web.js
CHANGED
|
@@ -180,6 +180,15 @@ function clean(value) {
|
|
|
180
180
|
const trimmed = value.trim();
|
|
181
181
|
return trimmed ? trimmed : undefined;
|
|
182
182
|
}
|
|
183
|
+
function isMasked(value) {
|
|
184
|
+
return typeof value === "string" && value.includes("****");
|
|
185
|
+
}
|
|
186
|
+
/** 如果前端传回的是掩码值(包含 ****),保留 existing 中的真实密钥,避免覆盖 */
|
|
187
|
+
function resolveSecret(incoming, existing) {
|
|
188
|
+
if (isMasked(incoming))
|
|
189
|
+
return existing;
|
|
190
|
+
return clean(incoming);
|
|
191
|
+
}
|
|
183
192
|
const MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
184
193
|
function readJson(request) {
|
|
185
194
|
return new Promise((resolve, reject) => {
|
|
@@ -558,8 +567,10 @@ export async function testPlatformConfig(platform, config) {
|
|
|
558
567
|
function toFileConfig(payload, existing) {
|
|
559
568
|
// Save Claude environment variables to ~/.claude/settings.json
|
|
560
569
|
const claudeEnv = {};
|
|
561
|
-
|
|
562
|
-
|
|
570
|
+
const existingClaudeEnv = loadClaudeSettingsEnv();
|
|
571
|
+
const resolvedAuthToken = resolveSecret(payload.ai.claudeAuthToken, existingClaudeEnv.ANTHROPIC_AUTH_TOKEN);
|
|
572
|
+
if (resolvedAuthToken)
|
|
573
|
+
claudeEnv.ANTHROPIC_AUTH_TOKEN = resolvedAuthToken;
|
|
563
574
|
if (payload.ai.claudeBaseUrl)
|
|
564
575
|
claudeEnv.ANTHROPIC_BASE_URL = payload.ai.claudeBaseUrl;
|
|
565
576
|
if (payload.ai.claudeModel)
|
|
@@ -600,7 +611,7 @@ function toFileConfig(payload, existing) {
|
|
|
600
611
|
...existing.platforms?.telegram,
|
|
601
612
|
enabled: payload.platforms.telegram.enabled,
|
|
602
613
|
aiCommand: clean(payload.platforms.telegram.aiCommand),
|
|
603
|
-
botToken:
|
|
614
|
+
botToken: resolveSecret(payload.platforms.telegram.botToken, existing.platforms?.telegram?.botToken),
|
|
604
615
|
proxy: clean(payload.platforms.telegram.proxy),
|
|
605
616
|
allowedUserIds: splitCsv(payload.platforms.telegram.allowedUserIds),
|
|
606
617
|
},
|
|
@@ -609,7 +620,7 @@ function toFileConfig(payload, existing) {
|
|
|
609
620
|
enabled: payload.platforms.feishu.enabled,
|
|
610
621
|
aiCommand: clean(payload.platforms.feishu.aiCommand),
|
|
611
622
|
appId: clean(payload.platforms.feishu.appId),
|
|
612
|
-
appSecret:
|
|
623
|
+
appSecret: resolveSecret(payload.platforms.feishu.appSecret, existing.platforms?.feishu?.appSecret),
|
|
613
624
|
allowedUserIds: splitCsv(payload.platforms.feishu.allowedUserIds),
|
|
614
625
|
},
|
|
615
626
|
qq: {
|
|
@@ -617,7 +628,7 @@ function toFileConfig(payload, existing) {
|
|
|
617
628
|
enabled: payload.platforms.qq.enabled,
|
|
618
629
|
aiCommand: clean(payload.platforms.qq.aiCommand),
|
|
619
630
|
appId: clean(payload.platforms.qq.appId),
|
|
620
|
-
secret:
|
|
631
|
+
secret: resolveSecret(payload.platforms.qq.secret, existing.platforms?.qq?.secret),
|
|
621
632
|
allowedUserIds: splitCsv(payload.platforms.qq.allowedUserIds),
|
|
622
633
|
},
|
|
623
634
|
wework: {
|
|
@@ -625,7 +636,7 @@ function toFileConfig(payload, existing) {
|
|
|
625
636
|
enabled: payload.platforms.wework.enabled,
|
|
626
637
|
aiCommand: clean(payload.platforms.wework.aiCommand),
|
|
627
638
|
corpId: clean(payload.platforms.wework.corpId),
|
|
628
|
-
secret:
|
|
639
|
+
secret: resolveSecret(payload.platforms.wework.secret, existing.platforms?.wework?.secret),
|
|
629
640
|
allowedUserIds: splitCsv(payload.platforms.wework.allowedUserIds),
|
|
630
641
|
},
|
|
631
642
|
dingtalk: {
|
|
@@ -633,7 +644,7 @@ function toFileConfig(payload, existing) {
|
|
|
633
644
|
enabled: payload.platforms.dingtalk.enabled,
|
|
634
645
|
aiCommand: clean(payload.platforms.dingtalk.aiCommand),
|
|
635
646
|
clientId: clean(payload.platforms.dingtalk.clientId),
|
|
636
|
-
clientSecret:
|
|
647
|
+
clientSecret: resolveSecret(payload.platforms.dingtalk.clientSecret, existing.platforms?.dingtalk?.clientSecret),
|
|
637
648
|
cardTemplateId: clean(payload.platforms.dingtalk.cardTemplateId),
|
|
638
649
|
allowedUserIds: splitCsv(payload.platforms.dingtalk.allowedUserIds),
|
|
639
650
|
},
|
|
@@ -641,8 +652,8 @@ function toFileConfig(payload, existing) {
|
|
|
641
652
|
...existing.platforms?.workbuddy,
|
|
642
653
|
enabled: payload.platforms.workbuddy.enabled,
|
|
643
654
|
aiCommand: clean(payload.platforms.workbuddy.aiCommand),
|
|
644
|
-
accessToken:
|
|
645
|
-
refreshToken:
|
|
655
|
+
accessToken: resolveSecret(payload.platforms.workbuddy.accessToken, existing.platforms?.workbuddy?.accessToken),
|
|
656
|
+
refreshToken: resolveSecret(payload.platforms.workbuddy.refreshToken, existing.platforms?.workbuddy?.refreshToken),
|
|
646
657
|
userId: clean(payload.platforms.workbuddy.userId),
|
|
647
658
|
baseUrl: clean(payload.platforms.workbuddy.baseUrl),
|
|
648
659
|
allowedUserIds: splitCsv(payload.platforms.workbuddy.allowedUserIds),
|
package/dist/index.js
CHANGED
|
@@ -294,7 +294,7 @@ export async function main() {
|
|
|
294
294
|
process.on("SIGTERM", () => shutdown().catch(() => process.exit(1)));
|
|
295
295
|
// Global error handlers to prevent unhandled crashes
|
|
296
296
|
process.on("unhandledRejection", (reason) => {
|
|
297
|
-
log.error("Unhandled Promise rejection:", reason);
|
|
297
|
+
log.error("Unhandled Promise rejection (this indicates a bug — the affected request may hang without a response):", reason);
|
|
298
298
|
});
|
|
299
299
|
process.on("uncaughtException", (err) => {
|
|
300
300
|
const msg = err?.message ?? String(err);
|
|
@@ -250,33 +250,46 @@ export class SessionManager {
|
|
|
250
250
|
}
|
|
251
251
|
load(previousDefaultWorkDir) {
|
|
252
252
|
try {
|
|
253
|
-
if (existsSync(SESSIONS_FILE))
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
253
|
+
if (!existsSync(SESSIONS_FILE))
|
|
254
|
+
return;
|
|
255
|
+
const raw = JSON.parse(readFileSync(SESSIONS_FILE, 'utf-8'));
|
|
256
|
+
// v2 格式: { sessions: {...}, convSessionMap: {...} }
|
|
257
|
+
// v1 格式: 直接是 Record<string, UserSession>
|
|
258
|
+
const isV2 = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object' && !raw.workDir;
|
|
259
|
+
const sessionData = isV2 ? raw.sessions : raw;
|
|
260
|
+
const convMapData = isV2 ? raw.convSessionMap : undefined;
|
|
261
|
+
for (const [k, v] of Object.entries(sessionData)) {
|
|
262
|
+
if (v && typeof v.workDir === 'string') {
|
|
263
|
+
// 如果该会话目录等于旧默认目录,则迁移到新的默认目录(认为用户没有手动 /cd 过)
|
|
264
|
+
if (previousDefaultWorkDir && v.workDir === previousDefaultWorkDir) {
|
|
265
|
+
v.workDir = this.defaultWorkDir;
|
|
266
|
+
}
|
|
267
|
+
if (!v.activeConvId)
|
|
268
|
+
v.activeConvId = randomBytes(4).toString('hex');
|
|
269
|
+
if (!v.sessionIds)
|
|
270
|
+
v.sessionIds = {};
|
|
271
|
+
if ('sessionId' in v) {
|
|
272
|
+
log.warn(`Legacy shared sessionId found for user ${k}; clearing it to avoid cross-tool resume conflicts`);
|
|
273
|
+
}
|
|
274
|
+
delete v.sessionId;
|
|
275
|
+
if (v.threads) {
|
|
276
|
+
for (const thread of Object.values(v.threads)) {
|
|
277
|
+
if (!thread.sessionIds)
|
|
278
|
+
thread.sessionIds = {};
|
|
279
|
+
if ('sessionId' in thread) {
|
|
280
|
+
log.warn(`Legacy thread sessionId found for user ${k}; clearing it during session migration`);
|
|
277
281
|
}
|
|
282
|
+
delete thread.sessionId;
|
|
278
283
|
}
|
|
279
|
-
|
|
284
|
+
}
|
|
285
|
+
this.sessions.set(k, v);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
// 恢复 convSessionMap
|
|
289
|
+
if (convMapData && typeof convMapData === 'object') {
|
|
290
|
+
for (const [k, v] of Object.entries(convMapData)) {
|
|
291
|
+
if (typeof v === 'string') {
|
|
292
|
+
this.convSessionMap.set(k, v);
|
|
280
293
|
}
|
|
281
294
|
}
|
|
282
295
|
}
|
|
@@ -312,10 +325,13 @@ export class SessionManager {
|
|
|
312
325
|
const dir = dirname(SESSIONS_FILE);
|
|
313
326
|
if (!existsSync(dir))
|
|
314
327
|
mkdirSync(dir, { recursive: true });
|
|
315
|
-
const
|
|
328
|
+
const sessions = {};
|
|
316
329
|
for (const [k, v] of this.sessions)
|
|
317
|
-
|
|
318
|
-
|
|
330
|
+
sessions[k] = v;
|
|
331
|
+
const convSessionMapObj = {};
|
|
332
|
+
for (const [k, v] of this.convSessionMap)
|
|
333
|
+
convSessionMapObj[k] = v;
|
|
334
|
+
writeFileSync(SESSIONS_FILE, JSON.stringify({ sessions, convSessionMap: convSessionMapObj }, null, 2), 'utf-8');
|
|
319
335
|
}
|
|
320
336
|
catch (err) {
|
|
321
337
|
log.error('Failed to save sessions:', err);
|