@wu529778790/open-im 1.8.1 → 1.8.3-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/access/access-control.js +1 -1
- package/dist/adapters/claude-sdk-adapter.js +71 -58
- package/dist/cli.js +5 -2
- package/dist/commands/handler.d.ts +0 -1
- package/dist/commands/handler.js +6 -20
- package/dist/config-web.js +24 -9
- package/dist/config.d.ts +11 -0
- package/dist/config.js +27 -2
- package/dist/dingtalk/event-handler.js +7 -3
- package/dist/dingtalk/message-sender.js +13 -0
- package/dist/feishu/event-handler.js +39 -19
- package/dist/index.js +9 -0
- package/dist/manager-control.js +7 -0
- package/dist/qq/client.js +6 -1
- package/dist/qq/event-handler.js +5 -2
- package/dist/qq/message-sender.js +11 -0
- package/dist/service-control.js +4 -0
- package/dist/session/session-manager.js +11 -1
- package/dist/shared/chat-user-map.js +11 -0
- package/dist/shared/media-storage.js +27 -0
- package/dist/telegram/client.js +3 -1
- package/dist/telegram/event-handler.js +41 -9
- package/dist/telegram/message-sender.js +13 -0
- package/dist/wechat/auth/qclaw-api.js +1 -1
- package/dist/wechat/client.d.ts +10 -3
- package/dist/wechat/client.js +70 -214
- package/dist/wechat/event-handler.js +7 -4
- package/dist/wechat/qclaw-transport.d.ts +66 -0
- package/dist/wechat/qclaw-transport.js +303 -0
- package/dist/wechat/transport.d.ts +41 -0
- package/dist/wechat/transport.js +5 -0
- package/dist/wechat/workbuddy-transport.d.ts +33 -0
- package/dist/wechat/workbuddy-transport.js +145 -0
- package/dist/wework/event-handler.js +28 -4
- package/dist/wework/message-sender.js +53 -21
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ export class AccessControl {
|
|
|
8
8
|
}
|
|
9
9
|
isAllowed(userId) {
|
|
10
10
|
if (this.allowedUserIds.size === 0) {
|
|
11
|
-
log.
|
|
11
|
+
log.warn(`Allowing user ${userId} — no whitelist configured. Set allowedUserIds to restrict access.`);
|
|
12
12
|
return true;
|
|
13
13
|
}
|
|
14
14
|
const allowed = this.allowedUserIds.has(userId);
|
|
@@ -16,6 +16,21 @@ const log = createLogger('ClaudeSDK');
|
|
|
16
16
|
const activeSessions = new Map();
|
|
17
17
|
// 存储正在进行的流式迭代器,用于中断
|
|
18
18
|
const activeStreams = new Set();
|
|
19
|
+
// Mutex to serialize process.chdir() calls across concurrent users
|
|
20
|
+
let chdirMutex = Promise.resolve();
|
|
21
|
+
function withChdirMutex(fn) {
|
|
22
|
+
const previous = chdirMutex;
|
|
23
|
+
let resolve;
|
|
24
|
+
chdirMutex = new Promise((r) => { resolve = r; });
|
|
25
|
+
return previous.then(() => {
|
|
26
|
+
try {
|
|
27
|
+
return fn();
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
19
34
|
function isStreamEvent(msg) {
|
|
20
35
|
return msg.type === 'stream_event';
|
|
21
36
|
}
|
|
@@ -37,39 +52,51 @@ function isResult(msg) {
|
|
|
37
52
|
* @param permissionMode 权限模式
|
|
38
53
|
* @returns SDKSession 对象和实际的 sessionId
|
|
39
54
|
*/
|
|
40
|
-
async function getOrCreateSession(sessionId,
|
|
41
|
-
model, permissionMode) {
|
|
55
|
+
async function getOrCreateSession(sessionId, workDir, model, permissionMode) {
|
|
42
56
|
const resolvedModel = model?.trim() || 'claude-opus-4-5';
|
|
43
57
|
const sessionOptions = {
|
|
44
58
|
model: resolvedModel,
|
|
45
59
|
permissionMode,
|
|
46
|
-
// 可以添加其他选项,如 hooks, allowedTools 等
|
|
47
60
|
};
|
|
48
61
|
const baseUrl = process.env.ANTHROPIC_BASE_URL ?? '(default)';
|
|
49
|
-
log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl}`);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
62
|
+
log.info(`[V2] getOrCreateSession model param=${String(model ?? '')} resolved=${resolvedModel} baseUrl=${baseUrl} workDir=${workDir}`);
|
|
63
|
+
// Use mutex to serialize process.chdir() calls across concurrent users
|
|
64
|
+
return withChdirMutex(() => {
|
|
65
|
+
let session;
|
|
66
|
+
const originalCwd = process.cwd();
|
|
53
67
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
68
|
+
if (workDir && workDir !== originalCwd) {
|
|
69
|
+
process.chdir(workDir);
|
|
70
|
+
}
|
|
71
|
+
if (sessionId) {
|
|
72
|
+
// 尝试恢复已有会话
|
|
73
|
+
try {
|
|
74
|
+
log.info(`Attempting to resume session: ${sessionId}`);
|
|
75
|
+
session = unstable_v2_resumeSession(sessionId, sessionOptions);
|
|
76
|
+
activeSessions.set(sessionId, session);
|
|
77
|
+
log.info(`Successfully resumed session: ${sessionId}`);
|
|
78
|
+
return { session, sessionId };
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
log.warn(`Failed to resume session ${sessionId}, creating new one: ${err}`);
|
|
82
|
+
// 恢复失败,创建新会话
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 创建新会话
|
|
86
|
+
session = unstable_v2_createSession(sessionOptions);
|
|
87
|
+
// 新会话的 sessionId 需要从第一个消息中获取
|
|
88
|
+
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
89
|
+
const tempId = `pending-${Date.now()}`;
|
|
90
|
+
activeSessions.set(tempId, session);
|
|
91
|
+
log.info(`Created new session (tempId: ${tempId})`);
|
|
92
|
+
return { session, sessionId: tempId };
|
|
59
93
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
94
|
+
finally {
|
|
95
|
+
if (workDir && workDir !== originalCwd) {
|
|
96
|
+
process.chdir(originalCwd);
|
|
97
|
+
}
|
|
63
98
|
}
|
|
64
|
-
}
|
|
65
|
-
// 创建新会话
|
|
66
|
-
session = unstable_v2_createSession(sessionOptions);
|
|
67
|
-
// 新会话的 sessionId 需要从第一个消息中获取
|
|
68
|
-
// 暂时返回 undefined,稍后在 init 消息中获取
|
|
69
|
-
const tempId = `pending-${Date.now()}`;
|
|
70
|
-
activeSessions.set(tempId, session);
|
|
71
|
-
log.info(`Created new session (tempId: ${tempId})`);
|
|
72
|
-
return { session, sessionId: tempId };
|
|
99
|
+
});
|
|
73
100
|
}
|
|
74
101
|
export class ClaudeSDKAdapter {
|
|
75
102
|
toolId = 'claude-sdk';
|
|
@@ -105,14 +132,6 @@ export class ClaudeSDKAdapter {
|
|
|
105
132
|
let actualSessionId;
|
|
106
133
|
let pendingTempId; // 记录临时 ID,用于 abort 时清理
|
|
107
134
|
let runSettled = false;
|
|
108
|
-
let timeoutId = null;
|
|
109
|
-
const timeoutMs = options?.timeoutMs ?? 600_000;
|
|
110
|
-
const clearRunTimeout = () => {
|
|
111
|
-
if (timeoutId !== null) {
|
|
112
|
-
clearTimeout(timeoutId);
|
|
113
|
-
timeoutId = null;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
135
|
const permissionMode = options?.skipPermissions
|
|
117
136
|
? 'bypassPermissions'
|
|
118
137
|
: options?.permissionMode === 'acceptEdits'
|
|
@@ -121,15 +140,6 @@ export class ClaudeSDKAdapter {
|
|
|
121
140
|
? 'plan'
|
|
122
141
|
: 'default';
|
|
123
142
|
const runSession = async () => {
|
|
124
|
-
timeoutId = setTimeout(() => {
|
|
125
|
-
if (runSettled)
|
|
126
|
-
return;
|
|
127
|
-
runSettled = true;
|
|
128
|
-
clearRunTimeout();
|
|
129
|
-
log.warn(`[ClaudeSDK] Request timeout after ${timeoutMs}ms`);
|
|
130
|
-
abortController.abort();
|
|
131
|
-
callbacks.onError(`请求超时(${Math.round(timeoutMs / 1000)}s),请重试或缩短问题。`);
|
|
132
|
-
}, timeoutMs);
|
|
133
143
|
try {
|
|
134
144
|
// 检查环境变量
|
|
135
145
|
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
|
|
@@ -209,7 +219,6 @@ export class ClaudeSDKAdapter {
|
|
|
209
219
|
// 检查会话错误
|
|
210
220
|
if (!success) {
|
|
211
221
|
runSettled = true;
|
|
212
|
-
clearRunTimeout();
|
|
213
222
|
const noConvErr = errs.find((e) => e.includes('No conversation found') || e.includes('session not found'));
|
|
214
223
|
if (noConvErr) {
|
|
215
224
|
log.warn(`Session ${actualSessionId} not found, may need to create new one`);
|
|
@@ -237,25 +246,31 @@ export class ClaudeSDKAdapter {
|
|
|
237
246
|
result.result = accumulated;
|
|
238
247
|
}
|
|
239
248
|
runSettled = true;
|
|
240
|
-
clearRunTimeout();
|
|
241
249
|
callbacks.onComplete(result);
|
|
242
250
|
return;
|
|
243
251
|
}
|
|
244
252
|
}
|
|
245
253
|
// 如果流正常结束但没有收到 result 消息
|
|
246
|
-
if (!streamClosed
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
254
|
+
if (!streamClosed) {
|
|
255
|
+
if (accumulated) {
|
|
256
|
+
log.info('Stream ended without result message, using accumulated text');
|
|
257
|
+
runSettled = true;
|
|
258
|
+
callbacks.onComplete({
|
|
259
|
+
success: true,
|
|
260
|
+
result: accumulated,
|
|
261
|
+
accumulated,
|
|
262
|
+
cost: 0,
|
|
263
|
+
durationMs: 0,
|
|
264
|
+
numTurns: 1,
|
|
265
|
+
toolStats,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// 流结束但无 result 也无 accumulated:必须触发回调,否则 Promise 永远挂起
|
|
270
|
+
log.warn('Stream ended with no result and no accumulated text, calling onError to prevent stuck state');
|
|
271
|
+
runSettled = true;
|
|
272
|
+
callbacks.onError('AI 响应异常结束(无输出),请重试');
|
|
273
|
+
}
|
|
259
274
|
}
|
|
260
275
|
}
|
|
261
276
|
finally {
|
|
@@ -266,7 +281,6 @@ export class ClaudeSDKAdapter {
|
|
|
266
281
|
catch (err) {
|
|
267
282
|
if (abortController.signal.aborted) {
|
|
268
283
|
log.info('Session run aborted');
|
|
269
|
-
clearRunTimeout();
|
|
270
284
|
// 清理 pending tempId(abort 可能在 init 消息之前发生)
|
|
271
285
|
const idToClean = actualSessionId ?? pendingTempId;
|
|
272
286
|
if (idToClean?.startsWith('pending-')) {
|
|
@@ -276,7 +290,6 @@ export class ClaudeSDKAdapter {
|
|
|
276
290
|
return;
|
|
277
291
|
}
|
|
278
292
|
runSettled = true;
|
|
279
|
-
clearRunTimeout();
|
|
280
293
|
const errorObj = err;
|
|
281
294
|
const msg = errorObj.message || String(err);
|
|
282
295
|
log.error(`Claude SDK V2 error: ${msg}`);
|
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,7 @@ async function cmdStart() {
|
|
|
50
50
|
console.log("\nopen-im is already running in the background.");
|
|
51
51
|
console.log(` pid: ${status.pid}`);
|
|
52
52
|
console.log(` config page: ${getWebConfigUrl()}`);
|
|
53
|
-
|
|
53
|
+
process.exit(0);
|
|
54
54
|
}
|
|
55
55
|
if (!(await ensureConfigured("start"))) {
|
|
56
56
|
process.exit(1);
|
|
@@ -67,17 +67,19 @@ async function cmdStart() {
|
|
|
67
67
|
console.log(" A one-time login URL (with login_token) has been printed by the config-web server logger.");
|
|
68
68
|
console.log(" Please use that URL (replacing 127.0.0.1 with your server IP/hostname) for the first login.");
|
|
69
69
|
}
|
|
70
|
+
process.exit(0);
|
|
70
71
|
}
|
|
71
72
|
async function cmdStop() {
|
|
72
73
|
const status = getManagerStatus();
|
|
73
74
|
if (!status.pid) {
|
|
74
75
|
console.log("open-im is not running in the background.");
|
|
75
|
-
|
|
76
|
+
process.exit(0);
|
|
76
77
|
}
|
|
77
78
|
await stopBackgroundService();
|
|
78
79
|
const result = await stopManagerProcess();
|
|
79
80
|
console.log("\nopen-im stopped.");
|
|
80
81
|
console.log(` pid: ${result.pid}`);
|
|
82
|
+
process.exit(0);
|
|
81
83
|
}
|
|
82
84
|
async function cmdRestart() {
|
|
83
85
|
const status = getManagerStatus();
|
|
@@ -98,6 +100,7 @@ async function cmdRestart() {
|
|
|
98
100
|
console.log("\nopen-im restarted in the background.");
|
|
99
101
|
console.log(` pid: ${child.pid}`);
|
|
100
102
|
console.log(` config page: ${getWebConfigUrl()}`);
|
|
103
|
+
process.exit(0);
|
|
101
104
|
}
|
|
102
105
|
async function cmdInit() {
|
|
103
106
|
console.log("\nopen-im CLI setup\n");
|
|
@@ -19,7 +19,6 @@ export declare class CommandHandler {
|
|
|
19
19
|
private deps;
|
|
20
20
|
constructor(deps: CommandHandlerDeps);
|
|
21
21
|
dispatch(text: string, chatId: string, userId: string, platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'wechat' | 'wework' | 'workbuddy', handleClaudeRequest: ClaudeRequestHandler): Promise<boolean>;
|
|
22
|
-
private getClearHistoryHint;
|
|
23
22
|
private handleHelp;
|
|
24
23
|
private handleNew;
|
|
25
24
|
private handlePwd;
|
package/dist/commands/handler.js
CHANGED
|
@@ -16,9 +16,9 @@ export class CommandHandler {
|
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
if (t === '/help')
|
|
19
|
-
return this.handleHelp(chatId
|
|
19
|
+
return this.handleHelp(chatId);
|
|
20
20
|
if (t === '/new')
|
|
21
|
-
return this.handleNew(chatId, userId
|
|
21
|
+
return this.handleNew(chatId, userId);
|
|
22
22
|
if (t === '/pwd')
|
|
23
23
|
return this.handlePwd(chatId, userId);
|
|
24
24
|
if (t === '/status')
|
|
@@ -33,18 +33,7 @@ export class CommandHandler {
|
|
|
33
33
|
}
|
|
34
34
|
return false;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
return platform === 'feishu'
|
|
38
|
-
? '💡 提示:如需清除本对话的历史消息,请点击飞书聊天右上角「...」→ 清除聊天记录'
|
|
39
|
-
: platform === 'wechat'
|
|
40
|
-
? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
|
|
41
|
-
: platform === 'dingtalk'
|
|
42
|
-
? '💡 提示:如需清除本对话的历史消息,请在钉钉中清空聊天记录'
|
|
43
|
-
: platform === 'workbuddy'
|
|
44
|
-
? '💡 提示:如需清除本对话的历史消息,请清除聊天记录'
|
|
45
|
-
: '💡 提示:如需清除本对话的历史消息,请点击 Telegram 聊天右上角 ⋮ → 清除历史';
|
|
46
|
-
}
|
|
47
|
-
async handleHelp(chatId, platform) {
|
|
36
|
+
async handleHelp(chatId) {
|
|
48
37
|
const help = [
|
|
49
38
|
'📋 可用命令:',
|
|
50
39
|
'',
|
|
@@ -53,16 +42,14 @@ export class CommandHandler {
|
|
|
53
42
|
'/status - 显示状态',
|
|
54
43
|
'/cd <路径> - 切换工作目录',
|
|
55
44
|
'/pwd - 当前工作目录',
|
|
56
|
-
'',
|
|
57
|
-
this.getClearHistoryHint(platform),
|
|
58
45
|
].join('\n');
|
|
59
46
|
await this.deps.sender.sendTextReply(chatId, help);
|
|
60
47
|
return true;
|
|
61
48
|
}
|
|
62
|
-
async handleNew(chatId, userId
|
|
49
|
+
async handleNew(chatId, userId) {
|
|
63
50
|
const ok = this.deps.sessionManager.newSession(userId);
|
|
64
51
|
await this.deps.sender.sendTextReply(chatId, ok
|
|
65
|
-
?
|
|
52
|
+
? '✅ AI 会话已重置,下一条消息将使用全新上下文。'
|
|
66
53
|
: '当前没有活动会话。');
|
|
67
54
|
return true;
|
|
68
55
|
}
|
|
@@ -103,8 +90,7 @@ export class CommandHandler {
|
|
|
103
90
|
try {
|
|
104
91
|
const resolved = await this.deps.sessionManager.setWorkDir(userId, dir);
|
|
105
92
|
await this.deps.sender.sendTextReply(chatId, `📁 工作目录已切换到: ${escapePathForMarkdown(resolved)}\n\n` +
|
|
106
|
-
`🔄 AI
|
|
107
|
-
this.getClearHistoryHint(platform));
|
|
93
|
+
`🔄 AI 会话已重置,下一条消息将使用全新上下文。`);
|
|
108
94
|
}
|
|
109
95
|
catch (err) {
|
|
110
96
|
await this.deps.sender.sendTextReply(chatId, err instanceof Error ? err.message : String(err));
|
package/dist/config-web.js
CHANGED
|
@@ -180,10 +180,20 @@ function clean(value) {
|
|
|
180
180
|
const trimmed = value.trim();
|
|
181
181
|
return trimmed ? trimmed : undefined;
|
|
182
182
|
}
|
|
183
|
+
const MAX_REQUEST_BODY_BYTES = 1 * 1024 * 1024; // 1 MB
|
|
183
184
|
function readJson(request) {
|
|
184
185
|
return new Promise((resolve, reject) => {
|
|
185
186
|
const chunks = [];
|
|
186
|
-
|
|
187
|
+
let totalBytes = 0;
|
|
188
|
+
request.on("data", (chunk) => {
|
|
189
|
+
totalBytes += chunk.length;
|
|
190
|
+
if (totalBytes > MAX_REQUEST_BODY_BYTES) {
|
|
191
|
+
reject(new Error("Request body too large (max 1 MB)"));
|
|
192
|
+
request.destroy();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
chunks.push(Buffer.from(chunk));
|
|
196
|
+
});
|
|
187
197
|
request.on("end", () => {
|
|
188
198
|
try {
|
|
189
199
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
@@ -200,6 +210,11 @@ function json(response, statusCode, body) {
|
|
|
200
210
|
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
201
211
|
response.end(JSON.stringify(body));
|
|
202
212
|
}
|
|
213
|
+
function maskSecret(value) {
|
|
214
|
+
if (!value || value.length <= 4)
|
|
215
|
+
return value ? "****" : "";
|
|
216
|
+
return value.slice(0, 2) + "****" + value.slice(-2);
|
|
217
|
+
}
|
|
203
218
|
function buildInitialPayload(file) {
|
|
204
219
|
// Load Claude settings from ~/.claude/settings.json
|
|
205
220
|
const claudeEnv = loadClaudeSettingsEnv();
|
|
@@ -208,7 +223,7 @@ function buildInitialPayload(file) {
|
|
|
208
223
|
telegram: {
|
|
209
224
|
enabled: file.platforms?.telegram?.enabled ?? Boolean(file.platforms?.telegram?.botToken),
|
|
210
225
|
aiCommand: file.platforms?.telegram?.aiCommand ?? "",
|
|
211
|
-
botToken: file.platforms?.telegram?.botToken
|
|
226
|
+
botToken: maskSecret(file.platforms?.telegram?.botToken),
|
|
212
227
|
proxy: file.platforms?.telegram?.proxy ?? "",
|
|
213
228
|
allowedUserIds: (file.platforms?.telegram?.allowedUserIds ?? []).join(", "),
|
|
214
229
|
},
|
|
@@ -216,36 +231,36 @@ function buildInitialPayload(file) {
|
|
|
216
231
|
enabled: file.platforms?.feishu?.enabled ?? Boolean(file.platforms?.feishu?.appId && file.platforms?.feishu?.appSecret),
|
|
217
232
|
aiCommand: file.platforms?.feishu?.aiCommand ?? "",
|
|
218
233
|
appId: file.platforms?.feishu?.appId ?? "",
|
|
219
|
-
appSecret: file.platforms?.feishu?.appSecret
|
|
234
|
+
appSecret: maskSecret(file.platforms?.feishu?.appSecret),
|
|
220
235
|
allowedUserIds: (file.platforms?.feishu?.allowedUserIds ?? []).join(", "),
|
|
221
236
|
},
|
|
222
237
|
qq: {
|
|
223
238
|
enabled: file.platforms?.qq?.enabled ?? Boolean(file.platforms?.qq?.appId && file.platforms?.qq?.secret),
|
|
224
239
|
aiCommand: file.platforms?.qq?.aiCommand ?? "",
|
|
225
240
|
appId: file.platforms?.qq?.appId ?? "",
|
|
226
|
-
secret: file.platforms?.qq?.secret
|
|
241
|
+
secret: maskSecret(file.platforms?.qq?.secret),
|
|
227
242
|
allowedUserIds: (file.platforms?.qq?.allowedUserIds ?? []).join(", "),
|
|
228
243
|
},
|
|
229
244
|
wework: {
|
|
230
245
|
enabled: file.platforms?.wework?.enabled ?? Boolean(file.platforms?.wework?.corpId && file.platforms?.wework?.secret),
|
|
231
246
|
aiCommand: file.platforms?.wework?.aiCommand ?? "",
|
|
232
247
|
corpId: file.platforms?.wework?.corpId ?? "",
|
|
233
|
-
secret: file.platforms?.wework?.secret
|
|
248
|
+
secret: maskSecret(file.platforms?.wework?.secret),
|
|
234
249
|
allowedUserIds: (file.platforms?.wework?.allowedUserIds ?? []).join(", "),
|
|
235
250
|
},
|
|
236
251
|
dingtalk: {
|
|
237
252
|
enabled: file.platforms?.dingtalk?.enabled ?? Boolean(file.platforms?.dingtalk?.clientId && file.platforms?.dingtalk?.clientSecret),
|
|
238
253
|
aiCommand: file.platforms?.dingtalk?.aiCommand ?? "",
|
|
239
254
|
clientId: file.platforms?.dingtalk?.clientId ?? "",
|
|
240
|
-
clientSecret: file.platforms?.dingtalk?.clientSecret
|
|
255
|
+
clientSecret: maskSecret(file.platforms?.dingtalk?.clientSecret),
|
|
241
256
|
cardTemplateId: file.platforms?.dingtalk?.cardTemplateId ?? "",
|
|
242
257
|
allowedUserIds: (file.platforms?.dingtalk?.allowedUserIds ?? []).join(", "),
|
|
243
258
|
},
|
|
244
259
|
workbuddy: {
|
|
245
260
|
enabled: file.platforms?.workbuddy?.enabled ?? Boolean(file.platforms?.workbuddy?.accessToken && file.platforms?.workbuddy?.refreshToken && file.platforms?.workbuddy?.userId),
|
|
246
261
|
aiCommand: file.platforms?.workbuddy?.aiCommand ?? "",
|
|
247
|
-
accessToken: file.platforms?.workbuddy?.accessToken
|
|
248
|
-
refreshToken: file.platforms?.workbuddy?.refreshToken
|
|
262
|
+
accessToken: maskSecret(file.platforms?.workbuddy?.accessToken),
|
|
263
|
+
refreshToken: maskSecret(file.platforms?.workbuddy?.refreshToken),
|
|
249
264
|
userId: file.platforms?.workbuddy?.userId ?? "",
|
|
250
265
|
baseUrl: file.platforms?.workbuddy?.baseUrl ?? "",
|
|
251
266
|
allowedUserIds: (file.platforms?.workbuddy?.allowedUserIds ?? []).join(", "),
|
|
@@ -258,7 +273,7 @@ function buildInitialPayload(file) {
|
|
|
258
273
|
claudeConfigPath: process.platform === 'win32'
|
|
259
274
|
? getClaudeConfigHome() + "\\.claude\\settings.json"
|
|
260
275
|
: getClaudeConfigHome() + "/.claude/settings.json",
|
|
261
|
-
claudeAuthToken: claudeEnv.ANTHROPIC_AUTH_TOKEN
|
|
276
|
+
claudeAuthToken: maskSecret(claudeEnv.ANTHROPIC_AUTH_TOKEN),
|
|
262
277
|
claudeBaseUrl: claudeEnv.ANTHROPIC_BASE_URL ?? "",
|
|
263
278
|
claudeModel: claudeEnv.ANTHROPIC_MODEL ?? "",
|
|
264
279
|
claudeProxy: file.tools?.claude?.proxy ?? "",
|
package/dist/config.d.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface Config {
|
|
|
64
64
|
wechat?: {
|
|
65
65
|
enabled: boolean;
|
|
66
66
|
aiCommand?: AiCommand;
|
|
67
|
+
loginMode?: 'qclaw' | 'workbuddy';
|
|
67
68
|
wsUrl?: string;
|
|
68
69
|
token?: string;
|
|
69
70
|
jwtToken?: string;
|
|
@@ -71,6 +72,10 @@ export interface Config {
|
|
|
71
72
|
guid?: string;
|
|
72
73
|
userId?: string;
|
|
73
74
|
allowedUserIds: string[];
|
|
75
|
+
workbuddyAccessToken?: string;
|
|
76
|
+
workbuddyRefreshToken?: string;
|
|
77
|
+
workbuddyBaseUrl?: string;
|
|
78
|
+
workbuddyHostId?: string;
|
|
74
79
|
};
|
|
75
80
|
wework?: {
|
|
76
81
|
enabled: boolean;
|
|
@@ -122,6 +127,8 @@ interface FilePlatformWechat {
|
|
|
122
127
|
appId?: string;
|
|
123
128
|
appSecret?: string;
|
|
124
129
|
aiCommand?: AiCommand;
|
|
130
|
+
/** 连接模式:qclaw(QClaw JPRX 网关)或 workbuddy(Centrifuge) */
|
|
131
|
+
loginMode?: 'qclaw' | 'workbuddy';
|
|
125
132
|
token?: string;
|
|
126
133
|
jwtToken?: string;
|
|
127
134
|
loginKey?: string;
|
|
@@ -129,6 +136,10 @@ interface FilePlatformWechat {
|
|
|
129
136
|
userId?: string;
|
|
130
137
|
wsUrl?: string;
|
|
131
138
|
allowedUserIds?: string[];
|
|
139
|
+
workbuddyAccessToken?: string;
|
|
140
|
+
workbuddyRefreshToken?: string;
|
|
141
|
+
workbuddyBaseUrl?: string;
|
|
142
|
+
workbuddyHostId?: string;
|
|
132
143
|
}
|
|
133
144
|
export interface FilePlatformWework {
|
|
134
145
|
enabled?: boolean;
|
package/dist/config.js
CHANGED
|
@@ -251,6 +251,7 @@ export function loadConfig() {
|
|
|
251
251
|
// 微信支持两种协议:
|
|
252
252
|
// 1. AGP 协议:token + guid + userId(推荐)
|
|
253
253
|
// 2. 标准协议:appId + appSecret
|
|
254
|
+
const wechatLoginMode = fileWechat?.loginMode ?? 'qclaw';
|
|
254
255
|
const wechatToken = process.env.WECHAT_TOKEN ??
|
|
255
256
|
fileWechat?.token;
|
|
256
257
|
const wechatJwtToken = fileWechat?.jwtToken;
|
|
@@ -265,6 +266,15 @@ export function loadConfig() {
|
|
|
265
266
|
fileWechat?.appSecret;
|
|
266
267
|
const wechatWsUrl = process.env.WECHAT_WS_URL ??
|
|
267
268
|
fileWechat?.wsUrl;
|
|
269
|
+
// 微信 WorkBuddy 模式凭证(loginMode === 'workbuddy' 时使用)
|
|
270
|
+
const wechatWorkbuddyAccessToken = process.env.WECHAT_WORKBUDDY_ACCESS_TOKEN ??
|
|
271
|
+
fileWechat?.workbuddyAccessToken;
|
|
272
|
+
const wechatWorkbuddyRefreshToken = process.env.WECHAT_WORKBUDDY_REFRESH_TOKEN ??
|
|
273
|
+
fileWechat?.workbuddyRefreshToken;
|
|
274
|
+
const wechatWorkbuddyBaseUrl = process.env.WECHAT_WORKBUDDY_BASE_URL ??
|
|
275
|
+
fileWechat?.workbuddyBaseUrl;
|
|
276
|
+
const wechatWorkbuddyHostId = process.env.WECHAT_WORKBUDDY_HOST_ID ??
|
|
277
|
+
fileWechat?.workbuddyHostId;
|
|
268
278
|
const weworkCorpId = process.env.WEWORK_CORP_ID ??
|
|
269
279
|
fileWework?.corpId;
|
|
270
280
|
const weworkSecret = process.env.WEWORK_SECRET ??
|
|
@@ -302,10 +312,15 @@ export function loadConfig() {
|
|
|
302
312
|
const telegramEnabled = !!telegramBotToken && (telegramEnabledFlag !== false);
|
|
303
313
|
const feishuEnabled = !!(feishuAppId && feishuAppSecret) && (feishuEnabledFlag !== false);
|
|
304
314
|
const qqEnabled = !!(qqAppId && qqSecret) && (qqEnabledFlag !== false);
|
|
305
|
-
// 微信启用条件:
|
|
315
|
+
// 微信启用条件:
|
|
316
|
+
// - qclaw 模式:AGP 协议凭证(token + guid + userId)或 标准协议凭证(appId + appSecret)
|
|
317
|
+
// - workbuddy 模式:workbuddy OAuth 凭证
|
|
306
318
|
const hasWechatAGPCreds = !!(wechatToken && wechatGuid && wechatUserId);
|
|
307
319
|
const hasWechatStandardCreds = !!(wechatAppId && wechatAppSecret);
|
|
308
|
-
const
|
|
320
|
+
const hasWechatWorkbuddyCreds = !!(wechatWorkbuddyAccessToken && wechatWorkbuddyRefreshToken);
|
|
321
|
+
const wechatEnabled = (wechatLoginMode === 'workbuddy'
|
|
322
|
+
? hasWechatWorkbuddyCreds
|
|
323
|
+
: (hasWechatAGPCreds || hasWechatStandardCreds)) && (wechatEnabledFlag !== false);
|
|
309
324
|
// 企业微信只需要 corpId (botId) 和 secret
|
|
310
325
|
const weworkEnabled = !!(weworkCorpId && weworkSecret) && (weworkEnabledFlag !== false);
|
|
311
326
|
const dingtalkEnabled = !!(dingtalkClientId && dingtalkClientSecret) && (dingtalkEnabledFlag !== false);
|
|
@@ -558,6 +573,7 @@ export function loadConfig() {
|
|
|
558
573
|
? {
|
|
559
574
|
enabled: true,
|
|
560
575
|
aiCommand: normalizeAiCommand(file.platforms?.wechat?.aiCommand, aiCommand),
|
|
576
|
+
loginMode: wechatLoginMode,
|
|
561
577
|
wsUrl: wechatWsUrl,
|
|
562
578
|
token: wechatToken,
|
|
563
579
|
jwtToken: wechatJwtToken,
|
|
@@ -565,10 +581,15 @@ export function loadConfig() {
|
|
|
565
581
|
guid: wechatGuid,
|
|
566
582
|
userId: wechatUserId,
|
|
567
583
|
allowedUserIds: wechatAllowedUserIds,
|
|
584
|
+
workbuddyAccessToken: wechatWorkbuddyAccessToken,
|
|
585
|
+
workbuddyRefreshToken: wechatWorkbuddyRefreshToken,
|
|
586
|
+
workbuddyBaseUrl: wechatWorkbuddyBaseUrl,
|
|
587
|
+
workbuddyHostId: wechatWorkbuddyHostId,
|
|
568
588
|
}
|
|
569
589
|
: {
|
|
570
590
|
enabled: false,
|
|
571
591
|
aiCommand: normalizeAiCommand(file.platforms?.wechat?.aiCommand, aiCommand),
|
|
592
|
+
loginMode: wechatLoginMode,
|
|
572
593
|
wsUrl: wechatWsUrl,
|
|
573
594
|
token: wechatToken,
|
|
574
595
|
jwtToken: wechatJwtToken,
|
|
@@ -576,6 +597,10 @@ export function loadConfig() {
|
|
|
576
597
|
guid: wechatGuid,
|
|
577
598
|
userId: wechatUserId,
|
|
578
599
|
allowedUserIds: wechatAllowedUserIds,
|
|
600
|
+
workbuddyAccessToken: wechatWorkbuddyAccessToken,
|
|
601
|
+
workbuddyRefreshToken: wechatWorkbuddyRefreshToken,
|
|
602
|
+
workbuddyBaseUrl: wechatWorkbuddyBaseUrl,
|
|
603
|
+
workbuddyHostId: wechatWorkbuddyHostId,
|
|
579
604
|
},
|
|
580
605
|
wework: weworkEnabled
|
|
581
606
|
? {
|
|
@@ -99,7 +99,8 @@ async function buildMediaPrompt(message, kind, robotCodeFallback) {
|
|
|
99
99
|
fallbackExtension: kind === 'image' ? 'jpg' : 'bin',
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
|
-
catch {
|
|
102
|
+
catch (err) {
|
|
103
|
+
log.warn('Failed to download DingTalk media from URL:', err);
|
|
103
104
|
localPath = undefined;
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -118,7 +119,8 @@ async function buildMediaPrompt(message, kind, robotCodeFallback) {
|
|
|
118
119
|
localPath = await saveBufferMedia(downloaded.buffer, extension, basenameHint);
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
|
-
catch {
|
|
122
|
+
catch (err) {
|
|
123
|
+
log.warn('Failed to download DingTalk media via robotCode:', err);
|
|
122
124
|
localPath = undefined;
|
|
123
125
|
}
|
|
124
126
|
}
|
|
@@ -194,7 +196,9 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
194
196
|
try {
|
|
195
197
|
await sendTextReply(chatId, '启动 AI 处理失败,请重试。');
|
|
196
198
|
}
|
|
197
|
-
catch {
|
|
199
|
+
catch (err) {
|
|
200
|
+
log.warn('Failed to send startup error reply:', err);
|
|
201
|
+
}
|
|
198
202
|
return;
|
|
199
203
|
}
|
|
200
204
|
const stopTyping = startTypingLoop(chatId);
|
|
@@ -24,6 +24,19 @@ const FLOW_STATUS = {
|
|
|
24
24
|
};
|
|
25
25
|
let senderSettings = {};
|
|
26
26
|
const streamStates = new Map();
|
|
27
|
+
// Periodic cleanup of orphaned stream states (max 30 minutes)
|
|
28
|
+
const STREAM_MAX_AGE_MS = 30 * 60 * 1000;
|
|
29
|
+
setInterval(() => {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
for (const [id, state] of streamStates) {
|
|
32
|
+
// streamStates in DingTalk don't have createdAt, clean up by size
|
|
33
|
+
if (streamStates.size > 50) {
|
|
34
|
+
streamStates.delete(id);
|
|
35
|
+
log.info(`Cleaned up old DingTalk stream state: ${id}`);
|
|
36
|
+
break; // Clean one at a time to avoid blocking
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}, STREAM_MAX_AGE_MS);
|
|
27
40
|
function generateMessageId() {
|
|
28
41
|
return `${Date.now()}-${randomBytes(6).toString('hex')}`;
|
|
29
42
|
}
|